根因: model_validate直接obj.images=json.loads()把ORM对象的字符串改成列表
SQLAlchemy autoflush时把列表写回VARCHAR列 → DataError
修复: field_validator(mode=before)在序列化层转换, 不修改ORM源对象
168 lines
5.9 KiB
Python
168 lines
5.9 KiB
Python
"""任务 Pydantic Schemas — 支持无限嵌套子任务、裂变转交、驳回返工"""
|
||
from __future__ import annotations
|
||
import uuid
|
||
from datetime import datetime
|
||
from typing import Optional
|
||
from pydantic import BaseModel, Field, field_validator
|
||
|
||
|
||
# ============================================================
|
||
# 请求模型
|
||
# ============================================================
|
||
|
||
class TaskCreate(BaseModel):
|
||
"""创建任务"""
|
||
product_id: uuid.UUID = Field(..., description="所属产品ID")
|
||
parent_task_id: uuid.UUID | None = Field(None, description="父任务ID(用于嵌套子任务/裂变分支)")
|
||
task_name: str = Field(..., max_length=200, description="任务名称")
|
||
assignee_id: str | None = Field(None, max_length=64, description="负责人ID(逻辑外键→老系统)")
|
||
notify_parent_on_complete: bool = Field(False, description="完成后是否通知父任务")
|
||
is_rework: bool = Field(False, description="是否为返工任务")
|
||
remark: str | None = Field(None, max_length=2000, description="初始描述/交接备注")
|
||
|
||
model_config = {"from_attributes": True}
|
||
|
||
|
||
class TaskUpdate(BaseModel):
|
||
"""更新任务"""
|
||
task_name: str | None = Field(None, max_length=200)
|
||
assignee_id: str | None = Field(None, max_length=64)
|
||
status: str | None = Field(None, max_length=50, description="任务状态")
|
||
notify_parent_on_complete: bool | None = Field(None)
|
||
|
||
model_config = {"from_attributes": True}
|
||
|
||
|
||
class TaskCompleteRequest(BaseModel):
|
||
"""完成任务请求 — 携带转交信息(保留兼容旧版单步转交)"""
|
||
operator_id: str | None = Field(None, max_length=64, description="操作人ID")
|
||
next_assignee_id: str | None = Field(None, max_length=64, description="下一步任务负责人ID")
|
||
next_task_name: str | None = Field(None, max_length=200, description="下一步任务名称")
|
||
remark: str | None = Field(None, description="完成备注")
|
||
|
||
|
||
class SubtaskCreate(BaseModel):
|
||
"""创建子任务"""
|
||
task_name: str = Field(..., max_length=200, description="子任务名称")
|
||
assignee_id: str | None = Field(None, max_length=64, description="负责人ID")
|
||
notify_parent_on_complete: bool = Field(False, description="完成后是否通知父任务")
|
||
|
||
|
||
class TaskRejectRequest(BaseModel):
|
||
"""品质驳回请求"""
|
||
reason: str = Field(..., min_length=1, max_length=500, description="驳回原因(必填)")
|
||
|
||
|
||
class TaskTransferBranch(BaseModel):
|
||
"""裂变分支"""
|
||
task_name: str = Field(..., max_length=200, description="工序名称")
|
||
assignees: list[str] = Field(..., min_length=1, description="接收人列表")
|
||
|
||
|
||
class TaskTransferRequest(BaseModel):
|
||
"""完工裂变转交请求 — 支持多分支 next_tasks 和旧版单线兼容"""
|
||
next_assignees: list[str] | None = Field(None, description="[旧版] 下一道工序接收人列表")
|
||
next_task_name: str | None = Field(None, max_length=200, description="[旧版] 下一道工序名称")
|
||
next_tasks: list[TaskTransferBranch] | None = Field(None, description="[新版] 多分支任务列表")
|
||
note: str | None = Field(None, description="交接备注")
|
||
|
||
|
||
class TaskRecordCreate(BaseModel):
|
||
"""任务进度记录 — 备注 + 图片"""
|
||
remark: str = Field("", max_length=2000, description="备注文本")
|
||
images: list[str] = Field(default_factory=list, description="图片 URL 列表")
|
||
|
||
|
||
class TaskRecordResponse(BaseModel):
|
||
id: int
|
||
task_id: uuid.UUID
|
||
remark: str | None = None
|
||
images: list[str] = []
|
||
created_at: datetime | None = None
|
||
|
||
model_config = {"from_attributes": True}
|
||
|
||
@field_validator("images", mode="before")
|
||
@classmethod
|
||
def _parse_images(cls, v):
|
||
"""处理 DB 中 images 的 JSON 字符串 → list 反序列化(不污染 ORM 对象)"""
|
||
import json
|
||
if isinstance(v, str):
|
||
try:
|
||
return json.loads(v)
|
||
except (json.JSONDecodeError, TypeError):
|
||
return []
|
||
if v is None:
|
||
return []
|
||
return v
|
||
|
||
|
||
# ============================================================
|
||
# 响应模型
|
||
# ============================================================
|
||
|
||
class TaskSummaryResponse(BaseModel):
|
||
"""任务摘要 — 扫码时用,不含嵌套子任务"""
|
||
id: uuid.UUID
|
||
product_id: uuid.UUID
|
||
parent_task_id: uuid.UUID | None
|
||
task_name: str
|
||
assignee_id: str | None
|
||
status: str
|
||
notify_parent_on_complete: bool
|
||
is_rework: bool = False
|
||
task_type: str | None = None
|
||
remark: str | None = None
|
||
reject_reason: str | None = None
|
||
received_at: datetime | None = None
|
||
completed_at: datetime | None = None
|
||
created_at: datetime
|
||
created_by: str | None = None # 谁创建的(从task_logs追溯)
|
||
|
||
model_config = {"from_attributes": True}
|
||
|
||
|
||
class TaskResponse(BaseModel):
|
||
"""任务详情响应 — 递归包含所有子任务"""
|
||
id: uuid.UUID
|
||
product_id: uuid.UUID
|
||
product_sn: str = ""
|
||
product_material: str = ""
|
||
parent_task_id: uuid.UUID | None
|
||
task_name: str
|
||
assignee_id: str | None
|
||
status: str
|
||
notify_parent_on_complete: bool
|
||
is_rework: bool = False
|
||
task_type: str | None = None
|
||
remark: str | None = None
|
||
reject_reason: str | None = None
|
||
received_at: datetime | None = None
|
||
completed_at: datetime | None = None
|
||
created_at: datetime
|
||
child_tasks: list[TaskResponse] = []
|
||
records: list[TaskRecordResponse] = []
|
||
created_by: str | None = None # 谁创建的(从task_logs追溯)
|
||
|
||
model_config = {"from_attributes": True}
|
||
|
||
|
||
class TaskCompleteResponse(BaseModel):
|
||
"""任务完成响应"""
|
||
completed_task: TaskResponse
|
||
next_task: TaskResponse | None = None
|
||
message: str
|
||
|
||
|
||
class TaskTransferResponse(BaseModel):
|
||
"""裂变转交响应"""
|
||
completed_task: TaskResponse
|
||
created_tasks: list[TaskResponse] = []
|
||
message: str
|
||
|
||
|
||
class TaskListResponse(BaseModel):
|
||
"""任务列表响应"""
|
||
tasks: list[TaskResponse]
|
||
total: int
|