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:
@ -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;")
|
||||||
@ -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')
|
||||||
@ -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')
|
||||||
35
backend/alembic/versions/e5f6a7b8c9d0_add_task_records.py
Normal file
35
backend/alembic/versions/e5f6a7b8c9d0_add_task_records.py
Normal 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')
|
||||||
@ -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",
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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="创建时间",
|
||||||
)
|
)
|
||||||
|
|
||||||
# ---- 关系 ----
|
# ---- 关系 ----
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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}>"
|
||||||
|
|||||||
@ -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="创建时间",
|
||||||
)
|
)
|
||||||
|
|
||||||
# ---- 关系 ----
|
# ---- 关系 ----
|
||||||
|
|||||||
Reference in New Issue
Block a user