14 KiB
14 KiB
生产任务 I/O 与细胞产品库联动设计
日期:2026-05-22 状态:已确认,待实施 范围:生产任务执行弹窗 — 原材料/产出与细胞产品库 + 库位联动
1. 背景
当前问题
- 产出脱节:任务产出面板只有简单字段(名称、条码、数量、单位、备注),与细胞产品库完全脱节,产出的细胞产品不会自动入库。
- 原材料信息不足:选择细胞产品作为原材料时,只显示编码和名称,无法查看产品类型、代次、储物格等关键信息。
- 库位不同步:原材料消耗和产出创建不会联动库位状态(占用/释放)。
- 先选后删除:删除已选的原材料后恢复,当前无联动处理。
已有基础
BizProductionTaskOutput表已有cell_product_id、storage_slot_id字段(未使用)BizCellProduct表已有production_task_id字段(未使用)BizCellProductCRUD 已有完整的库位占用自动创建/释放逻辑_refresh_slot_cache()已确保库位缓存一致性
2. 设计决策
| 维度 | 决策 |
|---|---|
| 产出联动模式 | 产出即入库 — 保存产出时自动在 biz_cell_product 创建记录 + 写入库位占用 |
| 删除恢复策略 | 删除即释放 — 删除产出/原材料后立即释放库位,重新添加需重新选位 |
| 产出表单范围 | 完整表单 — 与细胞产品库新增弹窗字段一致 |
| 原材料面板 | 同步增强 — 选择细胞产品时展示完整产品信息 |
| 产出交互方式 | 子弹窗 + 列表 — 添加/编辑弹出表单弹窗,列表展示核心字段 |
| 实现架构 | 后端一体化 — 一个事务内完成 diff + 产品 CRUD + 库位联动 |
3. 数据模型
3.1 无 DDL 变更
所有需要的字段已存在,只需激活:
biz_production_task_output.cell_product_id— 开始写入实际产品 IDbiz_production_task_output.storage_slot_id— 开始写入实际库位 IDbiz_cell_product.production_task_id— 产出创建时写入任务 ID
3.2 数据流向
产出保存 (set_task_outputs)
├─ 新增 → create BizCellProduct (status=frozen, production_task_id=taskId)
│ + create BizStorageSlotOccupancy (item_source=production)
│ + _refresh_slot_cache()
│ + create BizProductionTaskOutput (关联 cell_product_id)
│
├─ 删除 → update BizCellProduct (status=voided)
│ + remove BizStorageSlotOccupancy
│ + _refresh_slot_cache()
│ + delete BizProductionTaskOutput
│
└─ 修改 → update BizCellProduct 字段
+ 如库位变更:迁移 occupancy + _refresh_slot_cache()
+ update BizProductionTaskOutput
原材料保存 (set_task_inputs)
├─ 新增 cell_product → update BizCellProduct (status=used)
│ + remove BizStorageSlotOccupancy
│ + _refresh_slot_cache()
│
└─ 删除 cell_product → update BizCellProduct (status=frozen)
+ create BizStorageSlotOccupancy (恢复原位)
+ _refresh_slot_cache()
4. 后端 API 改造
4.1 Schema 变更
backend/app/schemas/production_task.py
TaskOutputItem 扩展为包含完整细胞产品字段:
class TaskOutputItem(BaseModel):
# 关联字段
cell_product_id: str | None = None # 编辑已有产出时传入
# 细胞产品完整字段
product_code: str
product_name: str
product_type_id: str | None = None
spec_id: str | None = None
passage: str | None = None # P1-P8
batch_no: str | None = None
storage_slot_id: str | None = None
expected_cell_count: float | None = None
viability: float | None = None # 0-100
freeze_date: str | None = None # YYYY-MM-DD
quantity: int = 1
unit: str | None = None
expiry_date: str | None = None # YYYY-MM-DD
product_images: str | None = None # 逗号分隔 URL
product_status: str = "frozen"
remark: str | None = None
TaskInputItem 不变更结构,但后端增加联动逻辑。
4.2 CRUD 改造
backend/app/crud/production_task.py
set_task_outputs(db, task_id, items)
伪代码:
async def set_task_outputs(db: AsyncSession, task_id: str, items: list[TaskOutputItem]):
# 1. 加载旧的 outputs
old_outputs = await _get_task_outputs(db, task_id)
old_map = {o.cell_product_id: o for o in old_outputs if o.cell_product_id}
# 2. 构建 new map
new_map = {}
for item in items:
if item.cell_product_id:
new_map[item.cell_product_id] = item
else:
new_map[f"__new_{id(item)}"] = item
# 3. 处理删除
for cp_id, old in old_map.items():
if cp_id not in new_map:
# 作废细胞产品
await cell_product_svc.update_status(db, cp_id, "voided")
# 释放库位
await occupancy_svc.remove_by_item(db, "cell_product", cp_id)
# 删除关联
await db.delete(old)
# 4. 处理新增和修改
result_outputs = []
for key, item in new_map.items():
if key.startswith("__new_"):
# 新增:创建细胞产品 + 占位
product = await cell_product_svc.create(db, {
"product_code": item.product_code,
"product_name": item.product_name,
...全部字段,
"product_status": "frozen",
"production_task_id": task_id,
})
if item.storage_slot_id:
await occupancy_svc.create(db, {
"slot_id": item.storage_slot_id,
"item_type": "cell_product",
"item_id": product.id,
"item_source": "production",
})
# 创建关联
output = BizProductionTaskOutput(
task_id=task_id,
cell_product_id=product.id,
storage_slot_id=item.storage_slot_id,
output_name=item.product_name,
output_barcode=item.product_code,
quantity=item.quantity,
unit=item.unit,
remark=item.remark,
)
result_outputs.append(output)
else:
# 修改:更新细胞产品 + 处理库位变更
await cell_product_svc.update(db, key, {...更新字段})
old_output = old_map[key]
if old_output.storage_slot_id != item.storage_slot_id:
# 库位变更:释放旧位 + 占新位
if old_output.storage_slot_id:
await occupancy_svc.remove_by_item(db, "cell_product", key)
if item.storage_slot_id:
await occupancy_svc.create(db, {...新占位})
# 更新关联表
_update_output_fields(old_output, item)
result_outputs.append(old_output)
await db.flush()
return result_outputs
set_task_inputs(db, task_id, items)
伪代码:
async def set_task_inputs(db: AsyncSession, task_id: str, items: list[TaskInputItem]):
old_inputs = await _get_task_inputs(db, task_id)
old_cp_ids = {i.input_id for i in old_inputs if i.input_type == "cell_product" and i.input_id}
new_cp_ids = {item.input_id for item in items if item.input_type == "cell_product" and item.input_id}
# 被移除的细胞产品 → 恢复 frozen + 恢复库位
for pid in old_cp_ids - new_cp_ids:
await cell_product_svc.update_status(db, pid, "frozen")
# 查找该产品原来的 slot,重新占位
product = await cell_product_svc.get(db, pid)
if product and product.storage_slot_id:
await occupancy_svc.create(db, {
"slot_id": product.storage_slot_id,
"item_type": "cell_product",
"item_id": pid,
"item_source": "production",
})
# 新增的细胞产品 → used + 释放库位
for pid in new_cp_ids - old_cp_ids:
await cell_product_svc.update_status(db, pid, "used")
await occupancy_svc.remove_by_item(db, "cell_product", pid)
# 替换 inputs
await _replace_inputs(db, task_id, items)
4.3 路由适配
backend/app/api/production.py
set_task_outputs 和 set_task_inputs 路由的请求体 schema 更新,其余不变。
5. 前端改造
5.1 新增组件
TaskOutputProductDialog.vue
- 位置:
web-admin/src/views/production/production-work-order/TaskOutputProductDialog.vue - Props:
visible,mode(add/edit),productData,dropdownOptions - 功能:
- 完整细胞产品表单(与
cell-product/index.vue新增弹窗一致) - 4列栅格布局
- 自动生成产品编码(调用
fetchNextCellProductCode) - 下拉选项由父组件传入(产品类型、规格、储物格、单位)
- 产品图片卡片化上传
- 完整细胞产品表单(与
- Emit:
save(formData)/cancel
TaskOutputList.vue
- 位置:
web-admin/src/views/production/production-work-order/TaskOutputList.vue - Props:
outputs,readonly - 展示列:序号、产品编码、产品名称、产品类型(tag)、代次、储物格位置、数量/单位、状态(tag)、操作
- Emit:
edit(row)/delete(index)/add()
TaskInputEnhancedTable.vue
- 位置:
web-admin/src/views/production/production-work-order/TaskInputEnhancedTable.vue - Props:
inputs,cellProductOptions,readonly - 功能:
- 类型切换(样本/细胞产品)
- 样本行:编码、名称、备注
- 细胞产品行:产品编码、产品名称、产品类型、代次、储物格位置、状态(tag)、备注
- 选择细胞产品时自动填充所有展示列
- Emit:
update(inputs)/add()/remove(index)
5.2 现有组件修改
useTaskStepEditor.ts
产出相关改造:
addTaskOutputRow()→ 不再添加空行,改为设置outputDialogVisible = true, outputDialogMode = 'add'editTaskOutputRow(row)→ 设置outputDialogVisible = true, outputDialogMode = 'edit', editingOutputData = rowremoveTaskOutputRow(i)→ 确认后从列表移除(实际库位释放在saveTaskOutputs时后端处理)saveTaskOutputs()→ 提交增强后的完整数据到后端- 新增状态:
outputDialogVisible,outputDialogMode,editingOutputData - 新增方法:
handleOutputDialogSave(formData)— 将表单数据合并到taskOutputs列表
原材料相关改造:
onCellProductInputSelect(row, val)→ 从cellProductMasterById填充完整信息(产品类型、代次、储物格等)- 增加
loadInputDropdownData()— 加载细胞产品列表(含完整字段)
TaskExecutionDialog.vue
- 产出面板:替换为
<TaskOutputList>+<TaskOutputProductDialog> - 原材料面板:替换为
<TaskInputEnhancedTable> - 下拉数据加载:复用细胞产品库的
loadDropdownData()逻辑
api/production-task.ts
fetchSetTaskOutputs参数适配新 schema(增加完整细胞产品字段)
6. 改造范围清单
后端
| 文件 | 操作 |
|---|---|
backend/app/schemas/production_task.py |
修改 — TaskOutputItem 扩展完整细胞产品字段 |
backend/app/crud/production_task.py |
修改 — set_task_outputs 增加 diff + 产品 CRUD + 库位联动 |
backend/app/crud/production_task.py |
修改 — set_task_inputs 增加 diff + 产品状态变更 + 库位联动 |
backend/app/api/production.py |
修改 — 路由适配新 schema |
backend/app/crud/cell_product.py |
可能修改 — 提取 create_single / update_fields 供复用 |
前端
| 文件 | 操作 |
|---|---|
web-admin/src/views/production/production-work-order/TaskOutputProductDialog.vue |
新建 |
web-admin/src/views/production/production-work-order/TaskOutputList.vue |
新建 |
web-admin/src/views/production/production-work-order/TaskInputEnhancedTable.vue |
新建 |
web-admin/src/views/production/production-work-order/useTaskStepEditor.ts |
修改 — 产出/原材料逻辑重构 |
web-admin/src/views/production/production-work-order/TaskExecutionDialog.vue |
修改 — 替换产出/原材料面板 |
web-admin/src/api/production-task.ts |
修改 — API 参数适配 |
不涉及
- 数据库 DDL(无新表、无新列)
- 工艺路线/工序步骤相关代码
- 细胞产品库独立页面
- 库位管理独立页面
7. 风险和约束
- 事务大小:如果一次产出数量很多(如50份),事务内会创建大量记录。建议限制单次产出 ≤ 20 条。
- 库位冲突:用户选择已被占用的库位时,后端应返回明确错误,前端应在下拉中标记已占位。
- 并发安全:同一库位同时被两个任务选中 → 后端需在 occupancy 表加唯一约束或乐观锁。
- 原材料恢复:删除原材料恢复细胞产品时,如果原库位已被其他产品占用 → 需要提示用户选择新库位或报错。
8. 验收标准
- 产出面板能通过子弹窗新增完整的细胞产品(所有字段可填写)
- 保存产出后,细胞产品库页面能看到新创建的产品
- 产出产品在库位管理页面显示为已占用
- 删除产出后,细胞产品状态变为已作废,库位释放
- 原材料选择细胞产品后,展示完整信息(类型、代次、库位等)
- 保存原材料后,被消耗的细胞产品状态变为已使用,库位释放
- 删除原材料中的细胞产品后,产品状态恢复为已冻存,库位恢复占用
- 查看详情(readonly模式)下所有信息正确展示