Files
track/backend/app/schemas/product.py
duxingchen 90d615cbb0 feat(backend): 产品列表返回当前人滞留时长 active_duration_hours
- ProductResponse 增加 active_duration_hours(小时) 字段
- get_all_products 批量计算每个产品活跃任务(WIP/PENDING)最早接手时间到现在的时长
- 复用 get_people_workload 相同的北京时间口径,前端无需额外请求
2026-08-28 10:46:10 +08:00

97 lines
3.9 KiB
Python

"""产品 Pydantic Schemas"""
from __future__ import annotations
import uuid
from datetime import datetime
from pydantic import BaseModel, Field
class ProductCreate(BaseModel):
"""创建产品 — serial_number 由后端自动生成 16 位 HEX"""
# MOM 物料挂载(必填)
material_id: str = Field(..., max_length=64, description="MOM物料ID(必选)")
material_name: str = Field("", max_length=255, description="物料名称")
spec_model: str = Field("", max_length=255, description="规格型号")
category: str = Field("", max_length=100, description="物料类别")
material_type: str = Field("", max_length=100, description="物料类型")
# 可选
external_serial: str | None = Field(None, max_length=64, description="产品序列号(用户自定义,选填)")
order_id: uuid.UUID | None = Field(None, description="所属订单ID(选填)")
order_no: str | None = Field(None, max_length=64, description="订单号(自由键入,选填)")
parent_product_id: uuid.UUID | None = Field(None, description="父产品ID")
model_config = {"from_attributes": True}
class ProductUpdate(BaseModel):
"""更新产品"""
serial_number: str | None = Field(None, min_length=16, max_length=16)
external_serial: str | None = Field(None, max_length=64)
material_id: str | None = Field(None, max_length=64)
material_name: str | None = Field(None, max_length=255)
spec_model: str | None = Field(None, max_length=255)
order_no: str | None = Field(None, max_length=64, description="订单号")
status: str | None = Field(None, max_length=50, description="产品状态")
parent_product_id: uuid.UUID | None = Field(None)
model_config = {"from_attributes": True}
class ProductResponse(BaseModel):
"""产品响应"""
id: uuid.UUID
serial_number: str
external_serial: str | None = None
order_id: uuid.UUID | None = None
order_no: str = ""
material_id: str | None
material_name: str | None = None
spec_model: str | None = None
category: str | None = None
material_type: str | None = None
parent_product_id: uuid.UUID | None
current_location_id: str | None = None
current_location_name: str | None = None
macro_status: str | None = None # 🔧 后端预计算的任务树状态(免前端逐条展开)
overall_status: str | None = None
status: str
created_at: datetime
# 🔧 最新动态 — 该产品活跃任务的最新记录
latest_record_time: datetime | None = None
latest_record_content: str | None = None
latest_record_has_images: bool = False
latest_record_assignee_id: str | None = None # 🔧 最新记录操作人(消除并发张冠李戴)
latest_record_assignee_name: str | None = None
# 🔧 当前人滞留时长 — 活跃任务(WIP/PENDING)最早接手时间到现在的时长(小时)
active_duration_hours: float | None = None
model_config = {"from_attributes": True}
class ProductScanResponse(BaseModel):
"""扫码查询响应 — 产品信息 + 完整任务树(递归嵌套)"""
id: uuid.UUID
serial_number: str
external_serial: str | None = None
order_id: uuid.UUID | None = None
order_no: str = ""
material_id: str | None
material_name: str | None = None
spec_model: str | None = None
category: str | None = None
material_type: str | None = None
parent_product_id: uuid.UUID | None
current_location_id: str | None = None
overall_status: str | None = None
status: str
created_at: datetime
top_level_tasks: list[TaskSummaryResponse] = []
task_tree: list[TaskResponse] = []
assignee_names: dict[str, str] = {} # 🔧 username→中文姓名映射
model_config = {"from_attributes": True}
# 延迟导入,避免循环引用
from app.schemas.task import TaskSummaryResponse, TaskResponse # noqa: E402
ProductScanResponse.model_rebuild()