feat(models): Task/Product 模型扩充 + 4个Alembic迁移

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表
This commit is contained in:
2026-08-05 14:00:23 +08:00
parent 82f474f71f
commit e0c01a562f
9 changed files with 216 additions and 12 deletions

View File

@ -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;")

View File

@ -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')

View File

@ -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')

View File

@ -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')

View File

@ -2,12 +2,13 @@
from app.models.base import Base from app.models.base import Base
from app.models.production_order import ProductionOrder from app.models.production_order import ProductionOrder
from app.models.product import Product 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 from app.models.task_log import TaskLog
__all__ = [ __all__ = [
"Base", "Base",
"ProductionOrder", "ProductionOrder",
"Product", "Product",
"Task", "Task",
"TaskRecord",
"TaskLog", "TaskLog",
] ]

View File

@ -1,11 +1,12 @@
"""产品模型""" """产品模型"""
import uuid import uuid
from datetime import datetime, timezone from datetime import datetime
from sqlalchemy import String, DateTime, ForeignKey from sqlalchemy import String, DateTime, ForeignKey
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base from app.models.base import Base
from app.core.time_utils import get_beijing_time
class Product(Base): class Product(Base):
@ -18,15 +19,33 @@ class Product(Base):
String(16), unique=True, index=True, nullable=False, comment="产品序列号(16位)", String(16), unique=True, index=True, nullable=False, comment="产品序列号(16位)",
) )
# ---- 物理外键(关联本库 production_orders ---- # ---- 物理外键(关联本库 production_orders— 可选 ----
order_id: Mapped[uuid.UUID] = mapped_column( order_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("production_orders.id"), nullable=False, comment="所属订单ID", 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无物理约束 ---- # ---- 逻辑外键(关联老系统物料表,仅存储 ID无物理约束 ----
material_id: Mapped[str | None] = mapped_column( material_id: Mapped[str | None] = mapped_column(
String(64), nullable=True, comment="物料ID(逻辑外键→老系统)", 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( parent_product_id: Mapped[uuid.UUID | None] = mapped_column(
@ -41,8 +60,12 @@ class Product(Base):
status: Mapped[str] = mapped_column( status: Mapped[str] = mapped_column(
String(50), nullable=False, default="pending", comment="产品状态", 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( created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), comment="创建时间", DateTime(timezone=True), default=get_beijing_time, comment="创建时间",
) )
# ---- 关系 ---- # ---- 关系 ----

View File

@ -1,11 +1,12 @@
"""生产订单模型""" """生产订单模型"""
import uuid import uuid
from datetime import datetime, timezone from datetime import datetime
from sqlalchemy import String, DateTime from sqlalchemy import String, DateTime
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import Base from app.models.base import Base
from app.core.time_utils import get_beijing_time
class ProductionOrder(Base): class ProductionOrder(Base):
@ -24,7 +25,7 @@ class ProductionOrder(Base):
String(50), nullable=False, default="pending", comment="订单状态", String(50), nullable=False, default="pending", comment="订单状态",
) )
created_at: Mapped[datetime] = mapped_column( 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: def __repr__(self) -> str:

View File

@ -1,11 +1,12 @@
"""任务模型 — 支持无限嵌套、任务裂变分支、返工闭环""" """任务模型 — 支持无限嵌套、任务裂变分支、返工闭环"""
import uuid import uuid
from datetime import datetime, timezone from datetime import datetime
from sqlalchemy import String, DateTime, Boolean, ForeignKey from sqlalchemy import String, DateTime, Boolean, ForeignKey
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base from app.models.base import Base
from app.core.time_utils import get_beijing_time
# 任务状态常量 # 任务状态常量
TASK_STATUS_PENDING = "PENDING" # 待接收 TASK_STATUS_PENDING = "PENDING" # 待接收
@ -52,7 +53,7 @@ class Task(Base):
# ---- 时间追踪 ---- # ---- 时间追踪 ----
created_at: Mapped[datetime] = mapped_column( 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( received_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True, comment="操作员确认接收时间", DateTime(timezone=True), nullable=True, comment="操作员确认接收时间",
@ -77,6 +78,39 @@ class Task(Base):
child_tasks: Mapped[list["Task"]] = relationship( child_tasks: Mapped[list["Task"]] = relationship(
"Task", back_populates="parent_task", lazy="selectin", "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: def __repr__(self) -> str:
return f"<Task {self.task_name}>" return f"<Task {self.task_name}>"
# ============================================================
# 任务进度记录 — 随时备注/传图
# ============================================================
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"<TaskRecord {self.id} @ {self.created_at}>"

View File

@ -1,11 +1,12 @@
"""任务操作日志模型""" """任务操作日志模型"""
import uuid import uuid
from datetime import datetime, timezone from datetime import datetime
from sqlalchemy import String, DateTime, ForeignKey, Text from sqlalchemy import String, DateTime, ForeignKey, Text
from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base from app.models.base import Base
from app.core.time_utils import get_beijing_time
class TaskLog(Base): class TaskLog(Base):
@ -34,7 +35,7 @@ class TaskLog(Base):
) )
created_at: Mapped[datetime] = mapped_column( created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), comment="创建时间", DateTime(timezone=True), default=get_beijing_time, comment="创建时间",
) )
# ---- 关系 ---- # ---- 关系 ----