From e0c01a562f1d3ff56e698be77f2be264234521d7 Mon Sep 17 00:00:00 2001 From: duxingchen Date: Wed, 5 Aug 2026 14:00:23 +0800 Subject: [PATCH] =?UTF-8?q?feat(models):=20Task/Product=20=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E6=89=A9=E5=85=85=20+=204=E4=B8=AAAlembic=E8=BF=81?= =?UTF-8?q?=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task 模型: - 新增 received_at, completed_at, reject_reason, is_rework - 新增 TaskRecord 模型 (remark + images JSON) - 状态常量 PENDING/WIP/COMPLETED/REJECTED/ARCHIVED - 全部时间字段改用北京时间 (get_beijing_time) Product 模型: - order_id → 可选; 新增 external_serial, current_location_id - 新增 material_name/spec_model/category/material_type 快照字段 - 新增 overall_status (备货/生产/测试/维修/在库) 迁移: - b2c3: HEX计数器Sequence + external_serial + order_id nullable - c3d4: material快照4列 - d4e5: overall_status - e5f6: task_records表 --- ...product_hex_counter_and_material_fields.py | 44 +++++++++++++++++++ ...b8_add_product_material_snapshot_fields.py | 35 +++++++++++++++ ...d4e5f6a7b8c9_add_product_overall_status.py | 30 +++++++++++++ .../versions/e5f6a7b8c9d0_add_task_records.py | 35 +++++++++++++++ backend/app/models/__init__.py | 3 +- backend/app/models/product.py | 33 +++++++++++--- backend/app/models/production_order.py | 5 ++- backend/app/models/task.py | 38 +++++++++++++++- backend/app/models/task_log.py | 5 ++- 9 files changed, 216 insertions(+), 12 deletions(-) create mode 100644 backend/alembic/versions/b2c3d4e5f6a7_product_hex_counter_and_material_fields.py create mode 100644 backend/alembic/versions/c3d4e5f6a7b8_add_product_material_snapshot_fields.py create mode 100644 backend/alembic/versions/d4e5f6a7b8c9_add_product_overall_status.py create mode 100644 backend/alembic/versions/e5f6a7b8c9d0_add_task_records.py diff --git a/backend/alembic/versions/b2c3d4e5f6a7_product_hex_counter_and_material_fields.py b/backend/alembic/versions/b2c3d4e5f6a7_product_hex_counter_and_material_fields.py new file mode 100644 index 0000000..e91bc07 --- /dev/null +++ b/backend/alembic/versions/b2c3d4e5f6a7_product_hex_counter_and_material_fields.py @@ -0,0 +1,44 @@ +"""product hex counter + material fields + optional order + +Revision ID: b2c3d4e5f6a7 +Revises: a1b2c3d4e5f6 +Create Date: 2026-08-04 15:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +revision: str = 'b2c3d4e5f6a7' +down_revision: Union[str, Sequence[str], None] = 'a1b2c3d4e5f6' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # === 16进制计数器 Sequence === + op.execute("CREATE SEQUENCE IF NOT EXISTS product_hex_counter START 1;") + + # === products 表: order_id → 可选 === + op.alter_column('products', 'order_id', + existing_type=sa.UUID(), + nullable=True, + existing_comment='所属订单ID(可选)') + + # === products 表: 新增 external_serial === + op.add_column('products', sa.Column( + 'external_serial', + sa.String(length=64), + nullable=True, + comment='用户自定义产品序列号(可选)', + )) + + +def downgrade() -> None: + op.drop_column('products', 'external_serial') + op.alter_column('products', 'order_id', + existing_type=sa.UUID(), + nullable=False) + op.execute("DROP SEQUENCE IF EXISTS product_hex_counter;") diff --git a/backend/alembic/versions/c3d4e5f6a7b8_add_product_material_snapshot_fields.py b/backend/alembic/versions/c3d4e5f6a7b8_add_product_material_snapshot_fields.py new file mode 100644 index 0000000..22e6a0a --- /dev/null +++ b/backend/alembic/versions/c3d4e5f6a7b8_add_product_material_snapshot_fields.py @@ -0,0 +1,35 @@ +"""add product material snapshot fields + +Revision ID: c3d4e5f6a7b8 +Revises: b2c3d4e5f6a7 +Create Date: 2026-08-05 10:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +revision: str = 'c3d4e5f6a7b8' +down_revision: Union[str, Sequence[str], None] = 'b2c3d4e5f6a7' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column('products', sa.Column( + 'material_name', sa.String(length=255), nullable=True, comment='物料名称快照')) + op.add_column('products', sa.Column( + 'spec_model', sa.String(length=255), nullable=True, comment='规格型号快照')) + op.add_column('products', sa.Column( + 'category', sa.String(length=255), nullable=True, comment='物料分类快照')) + op.add_column('products', sa.Column( + 'material_type', sa.String(length=100), nullable=True, comment='物料类型快照')) + + +def downgrade() -> None: + op.drop_column('products', 'material_type') + op.drop_column('products', 'category') + op.drop_column('products', 'spec_model') + op.drop_column('products', 'material_name') diff --git a/backend/alembic/versions/d4e5f6a7b8c9_add_product_overall_status.py b/backend/alembic/versions/d4e5f6a7b8c9_add_product_overall_status.py new file mode 100644 index 0000000..f5b1b5b --- /dev/null +++ b/backend/alembic/versions/d4e5f6a7b8c9_add_product_overall_status.py @@ -0,0 +1,30 @@ +"""add product overall_status + +Revision ID: d4e5f6a7b8c9 +Revises: c3d4e5f6a7b8 +Create Date: 2026-08-05 12:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +revision: str = 'd4e5f6a7b8c9' +down_revision: Union[str, Sequence[str], None] = 'c3d4e5f6a7b8' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column('products', sa.Column( + 'overall_status', + sa.String(length=20), + nullable=True, + comment='宏观状态: 备货/生产/测试/维修/在库', + )) + + +def downgrade() -> None: + op.drop_column('products', 'overall_status') diff --git a/backend/alembic/versions/e5f6a7b8c9d0_add_task_records.py b/backend/alembic/versions/e5f6a7b8c9d0_add_task_records.py new file mode 100644 index 0000000..1e5e324 --- /dev/null +++ b/backend/alembic/versions/e5f6a7b8c9d0_add_task_records.py @@ -0,0 +1,35 @@ +"""add task_records table + +Revision ID: e5f6a7b8c9d0 +Revises: d4e5f6a7b8c9 +Create Date: 2026-08-06 10:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects.postgresql import UUID + + +revision: str = 'e5f6a7b8c9d0' +down_revision: Union[str, Sequence[str], None] = 'd4e5f6a7b8c9' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table('task_records', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('task_id', UUID(as_uuid=True), sa.ForeignKey('tasks.id'), nullable=False, comment='所属任务ID'), + sa.Column('remark', sa.String(length=2000), nullable=True, comment='备注文本'), + sa.Column('images', sa.String(length=4000), nullable=True, comment='图片URL列表(JSON字符串)'), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, comment='记录时间'), + sa.PrimaryKeyConstraint('id'), + ) + op.create_index(op.f('ix_task_records_task_id'), 'task_records', ['task_id'], unique=False) + + +def downgrade() -> None: + op.drop_index(op.f('ix_task_records_task_id'), table_name='task_records') + op.drop_table('task_records') diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index a9381da..d36d537 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -2,12 +2,13 @@ from app.models.base import Base from app.models.production_order import ProductionOrder from app.models.product import Product -from app.models.task import Task +from app.models.task import Task, TaskRecord from app.models.task_log import TaskLog __all__ = [ "Base", "ProductionOrder", "Product", "Task", + "TaskRecord", "TaskLog", ] diff --git a/backend/app/models/product.py b/backend/app/models/product.py index 2e1ba1f..a290099 100644 --- a/backend/app/models/product.py +++ b/backend/app/models/product.py @@ -1,11 +1,12 @@ """产品模型""" import uuid -from datetime import datetime, timezone +from datetime import datetime from sqlalchemy import String, DateTime, ForeignKey from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import Mapped, mapped_column, relationship from app.models.base import Base +from app.core.time_utils import get_beijing_time class Product(Base): @@ -18,15 +19,33 @@ class Product(Base): String(16), unique=True, index=True, nullable=False, comment="产品序列号(16位)", ) - # ---- 物理外键(关联本库 production_orders) ---- - order_id: Mapped[uuid.UUID] = mapped_column( - UUID(as_uuid=True), ForeignKey("production_orders.id"), nullable=False, comment="所属订单ID", + # ---- 物理外键(关联本库 production_orders)— 可选 ---- + order_id: Mapped[uuid.UUID | None] = mapped_column( + UUID(as_uuid=True), ForeignKey("production_orders.id"), nullable=True, comment="所属订单ID(可选)", + ) + + # ---- 外部序列号(用户自定义,可选) ---- + external_serial: Mapped[str | None] = mapped_column( + String(64), nullable=True, comment="用户自定义产品序列号(可选)", ) # ---- 逻辑外键(关联老系统物料表,仅存储 ID,无物理约束) ---- material_id: Mapped[str | None] = mapped_column( String(64), nullable=True, comment="物料ID(逻辑外键→老系统)", ) + # ---- MOM 物料快照字段(创产品时写入,防止老系统数据变动影响标签) ---- + material_name: Mapped[str | None] = mapped_column( + String(255), nullable=True, comment="物料名称快照", + ) + spec_model: Mapped[str | None] = mapped_column( + String(255), nullable=True, comment="规格型号快照", + ) + category: Mapped[str | None] = mapped_column( + String(255), nullable=True, comment="物料分类快照", + ) + material_type: Mapped[str | None] = mapped_column( + String(100), nullable=True, comment="物料类型快照", + ) # ---- 物理外键(自引用:父产品) ---- parent_product_id: Mapped[uuid.UUID | None] = mapped_column( @@ -41,8 +60,12 @@ class Product(Base): status: Mapped[str] = mapped_column( String(50), nullable=False, default="pending", comment="产品状态", ) + # 宏观流转状态 — 首次扫码时强制设定:备货/生产/测试/维修/在库 + overall_status: Mapped[str | None] = mapped_column( + String(20), nullable=True, comment="宏观状态: 备货/生产/测试/维修/在库", + ) created_at: Mapped[datetime] = mapped_column( - DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), comment="创建时间", + DateTime(timezone=True), default=get_beijing_time, comment="创建时间", ) # ---- 关系 ---- diff --git a/backend/app/models/production_order.py b/backend/app/models/production_order.py index 7fb4846..44edcba 100644 --- a/backend/app/models/production_order.py +++ b/backend/app/models/production_order.py @@ -1,11 +1,12 @@ """生产订单模型""" import uuid -from datetime import datetime, timezone +from datetime import datetime from sqlalchemy import String, DateTime from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import Mapped, mapped_column from app.models.base import Base +from app.core.time_utils import get_beijing_time class ProductionOrder(Base): @@ -24,7 +25,7 @@ class ProductionOrder(Base): String(50), nullable=False, default="pending", comment="订单状态", ) created_at: Mapped[datetime] = mapped_column( - DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), comment="创建时间", + DateTime(timezone=True), default=get_beijing_time, comment="创建时间", ) def __repr__(self) -> str: diff --git a/backend/app/models/task.py b/backend/app/models/task.py index b6df1bd..63f5997 100644 --- a/backend/app/models/task.py +++ b/backend/app/models/task.py @@ -1,11 +1,12 @@ """任务模型 — 支持无限嵌套、任务裂变分支、返工闭环""" import uuid -from datetime import datetime, timezone +from datetime import datetime from sqlalchemy import String, DateTime, Boolean, ForeignKey from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import Mapped, mapped_column, relationship from app.models.base import Base +from app.core.time_utils import get_beijing_time # 任务状态常量 TASK_STATUS_PENDING = "PENDING" # 待接收 @@ -52,7 +53,7 @@ class Task(Base): # ---- 时间追踪 ---- created_at: Mapped[datetime] = mapped_column( - DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), comment="创建时间", + DateTime(timezone=True), default=get_beijing_time, comment="创建时间", ) received_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True, comment="操作员确认接收时间", @@ -77,6 +78,39 @@ class Task(Base): child_tasks: Mapped[list["Task"]] = relationship( "Task", back_populates="parent_task", lazy="selectin", ) + records: Mapped[list["TaskRecord"]] = relationship( + "TaskRecord", back_populates="task", lazy="selectin", cascade="all, delete-orphan", + ) def __repr__(self) -> str: return f"" + + +# ============================================================ +# 任务进度记录 — 随时备注/传图 +# ============================================================ + +class TaskRecord(Base): + __tablename__ = "task_records" + + id: Mapped[int] = mapped_column( + primary_key=True, autoincrement=True, + ) + task_id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), ForeignKey("tasks.id"), nullable=False, index=True, comment="所属任务ID", + ) + remark: Mapped[str | None] = mapped_column( + String(2000), nullable=True, comment="备注文本", + ) + images: Mapped[str | None] = mapped_column( + String(4000), nullable=True, comment="图片URL列表(JSON字符串)", + ) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), default=get_beijing_time, comment="记录时间", + ) + + # ---- 关系 ---- + task: Mapped["Task"] = relationship("Task", back_populates="records") + + def __repr__(self) -> str: + return f"" diff --git a/backend/app/models/task_log.py b/backend/app/models/task_log.py index d155603..5b9c42f 100644 --- a/backend/app/models/task_log.py +++ b/backend/app/models/task_log.py @@ -1,11 +1,12 @@ """任务操作日志模型""" import uuid -from datetime import datetime, timezone +from datetime import datetime from sqlalchemy import String, DateTime, ForeignKey, Text from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import Mapped, mapped_column, relationship from app.models.base import Base +from app.core.time_utils import get_beijing_time class TaskLog(Base): @@ -34,7 +35,7 @@ class TaskLog(Base): ) created_at: Mapped[datetime] = mapped_column( - DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), comment="创建时间", + DateTime(timezone=True), default=get_beijing_time, comment="创建时间", ) # ---- 关系 ----