日期:2026-05-22 状态:已确认,待实施 范围:生产任务执行弹窗 — 原材料/产出与细胞产品库 + 库位联动
BizProductionTaskOutput 表已有 cell_product_id、storage_slot_id 字段(未使用)BizCellProduct 表已有 production_task_id 字段(未使用)BizCellProduct CRUD 已有完整的库位占用自动创建/释放逻辑_refresh_slot_cache() 已确保库位缓存一致性| 维度 | 决策 |
|---|---|
| 产出联动模式 | 产出即入库 — 保存产出时自动在 biz_cell_product 创建记录 + 写入库位占用 |
| 删除恢复策略 | 删除即释放 — 删除产出/原材料后立即释放库位,重新添加需重新选位 |
| 产出表单范围 | 完整表单 — 与细胞产品库新增弹窗字段一致 |
| 原材料面板 | 同步增强 — 选择细胞产品时展示完整产品信息 |
| 产出交互方式 | 子弹窗 + 列表 — 添加/编辑弹出表单弹窗,列表展示核心字段 |
| 实现架构 | 后端一体化 — 一个事务内完成 diff + 产品 CRUD + 库位联动 |
所有需要的字段已存在,只需激活:
biz_production_task_output.cell_product_id — 开始写入实际产品 IDbiz_production_task_output.storage_slot_id — 开始写入实际库位 IDbiz_cell_product.production_task_id — 产出创建时写入任务 ID产出保存 (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()
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 不变更结构,但后端增加联动逻辑。
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)
backend/app/api/production.py
set_task_outputs 和 set_task_inputs 路由的请求体 schema 更新,其余不变。
TaskOutputProductDialog.vueweb-admin/src/views/production/production-work-order/TaskOutputProductDialog.vuevisible, mode (add/edit), productData, dropdownOptionscell-product/index.vue 新增弹窗一致)fetchNextCellProductCode)save(formData) / cancelTaskOutputList.vueweb-admin/src/views/production/production-work-order/TaskOutputList.vueoutputs, readonlyedit(row) / delete(index) / add()TaskInputEnhancedTable.vueweb-admin/src/views/production/production-work-order/TaskInputEnhancedTable.vueinputs, cellProductOptions, readonlyupdate(inputs) / add() / remove(index)useTaskStepEditor.ts产出相关改造:
addTaskOutputRow() → 不再添加空行,改为设置 outputDialogVisible = true, outputDialogMode = 'add'editTaskOutputRow(row) → 设置 outputDialogVisible = true, outputDialogMode = 'edit', editingOutputData = rowremoveTaskOutputRow(i) → 确认后从列表移除(实际库位释放在 saveTaskOutputs 时后端处理)saveTaskOutputs() → 提交增强后的完整数据到后端outputDialogVisible, outputDialogMode, editingOutputDatahandleOutputDialogSave(formData) — 将表单数据合并到 taskOutputs 列表原材料相关改造:
onCellProductInputSelect(row, val) → 从 cellProductMasterById 填充完整信息(产品类型、代次、储物格等)loadInputDropdownData() — 加载细胞产品列表(含完整字段)TaskExecutionDialog.vue<TaskOutputList> + <TaskOutputProductDialog><TaskInputEnhancedTable>loadDropdownData() 逻辑api/production-task.tsfetchSetTaskOutputs 参数适配新 schema(增加完整细胞产品字段)| 文件 | 操作 |
|---|---|
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 参数适配 |