This commit is contained in:
zlwu committed 2026-05-27 05:23:28 +08:00
1 parent ad8f8d696f
commit 41aa90aa05
115 files changed
+29626 -10210

No files matched your search

+23
View File
@@ -0,0 +1,23 @@
-- 输注计划管理 · 出库交接
CREATE TABLE IF NOT EXISTS `biz_outbound_handover` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`infusion_schedule_id` varchar(64) NOT NULL COMMENT '输注计划ID',
`handover_user_id` varchar(64) DEFAULT NULL COMMENT '交接人用户ID',
`handover_person` varchar(64) DEFAULT NULL COMMENT '交接人姓名快照',
`handover_time` datetime DEFAULT NULL COMMENT '交接时间',
`handover_location` varchar(200) DEFAULT NULL COMMENT '交接地点',
`receiver_person` varchar(200) DEFAULT NULL COMMENT '接收人(文本)',
`tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) DEFAULT NULL COMMENT '交接备注',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_oh_schedule` (`infusion_schedule_id`, `del_flag`),
KEY `idx_oh_tenant` (`tenant_no`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='输注计划-出库交接';
+71
View File
@@ -0,0 +1,71 @@
-- 出库放行审核 · 出库物料准备(台账,不联动库存)
CREATE TABLE IF NOT EXISTS `biz_outbound_release_material` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`infusion_schedule_id` varchar(64) NOT NULL COMMENT '输注计划ID',
`paper_attachments` varchar(4000) DEFAULT NULL COMMENT '其他纸质材料附件(逗号分隔URL)',
`tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_orm_schedule` (`infusion_schedule_id`, `del_flag`),
KEY `idx_orm_tenant` (`tenant_no`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='出库放行-出库物料准备主表';
CREATE TABLE IF NOT EXISTS `biz_outbound_release_cell` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`material_id` varchar(64) NOT NULL COMMENT '关联 biz_outbound_release_material.id',
`infusion_schedule_id` varchar(64) NOT NULL COMMENT '输注计划ID',
`cell_product_id` varchar(64) NOT NULL COMMENT '细胞产品ID',
`product_code` varchar(128) DEFAULT NULL COMMENT '产品编码快照',
`product_name` varchar(200) DEFAULT NULL COMMENT '产品名称快照',
`product_type_name` varchar(100) DEFAULT NULL COMMENT '产品类型快照',
`spec_name` varchar(100) DEFAULT NULL COMMENT '规格快照',
`passage` varchar(32) DEFAULT NULL COMMENT '代次快照',
`batch_no` varchar(64) DEFAULT NULL COMMENT '批次号快照',
`storage_slot_name` varchar(200) DEFAULT NULL COMMENT '储物格快照',
`product_status` varchar(32) DEFAULT NULL COMMENT '保存时状态快照',
`tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_orc_schedule_product` (`infusion_schedule_id`, `cell_product_id`, `del_flag`),
KEY `idx_orc_product` (`cell_product_id`, `del_flag`),
KEY `idx_orc_material` (`material_id`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='出库放行-出库细胞(台账)';
CREATE TABLE IF NOT EXISTS `biz_outbound_release_qc_material` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`material_id` varchar(64) NOT NULL COMMENT '关联 biz_outbound_release_material.id',
`infusion_schedule_id` varchar(64) NOT NULL COMMENT '输注计划ID',
`qc_material_id` varchar(64) NOT NULL COMMENT '质检物料ID',
`qty` decimal(18,4) NOT NULL COMMENT '计划用量(台账)',
`material_code` varchar(64) DEFAULT NULL COMMENT '物料编码快照',
`material_name` varchar(200) DEFAULT NULL COMMENT '物料名称快照',
`spec_model` varchar(200) DEFAULT NULL COMMENT '规格型号快照',
`unit` varchar(32) DEFAULT NULL COMMENT '单位快照',
`tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_orqm_schedule_material` (`infusion_schedule_id`, `qc_material_id`, `del_flag`),
KEY `idx_orqm_material` (`material_id`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='出库放行-质检物料(台账)';
@@ -0,0 +1,22 @@
-- 出库放行审核 · 其他纸质材料子表(逐条:名称、份数、附件)
CREATE TABLE IF NOT EXISTS `biz_outbound_release_paper` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`material_id` varchar(64) NOT NULL COMMENT '关联 biz_outbound_release_material.id',
`infusion_schedule_id` varchar(64) NOT NULL COMMENT '输注计划ID',
`paper_name` varchar(200) NOT NULL COMMENT '材料名称',
`copies` int(11) NOT NULL DEFAULT 1 COMMENT '份数',
`attachment_url` varchar(2000) DEFAULT NULL COMMENT '附件URL',
`tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除',
PRIMARY KEY (`id`),
KEY `idx_orp_schedule` (`infusion_schedule_id`, `del_flag`),
KEY `idx_orp_material` (`material_id`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='出库放行-其他纸质材料(台账)';
@@ -0,0 +1,4 @@
-- 出库放行审核表新增审核附件和审核人姓名字段
ALTER TABLE biz_outbound_release_review
ADD COLUMN review_attachments VARCHAR(4000) NULL COMMENT '审核附件(逗号分隔URL)' AFTER review_remark,
ADD COLUMN review_by_name VARCHAR(64) NULL COMMENT '审核人姓名' AFTER reviewer_user_id;
@@ -0,0 +1,349 @@
# 输注计划管理 · 出库交接设计
> 日期: 2026-05-27
> 状态: 已实现
> 父模块: 中试服务管理 → 输注计划管理
> 关联: `2026-05-27-outbound-release-material-design.md`(出库物料台账)、`outbound-release-review`
> **库存策略(已确认)**: **台账模式 — 与出库物料准备一致,保存时不联动库存**
## 1. 背景与目标
### 现状
- **出库放行审核**页已实现「出库物料准备」:按输注计划登记细胞 / 质检物料 / 纸质材料(台账,不扣库存)。
- **输注计划管理**页仅有编辑、确认、完成、取消、查看,无出库相关操作。
- 全库尚无独立的「出库交接」记录表与 API。
### 目标
在**输注计划管理**列表操作列新增「**出库交接**」按钮。点击后打开弹窗:
| 区块 | 行为 |
|------|------|
| 出库物料 | 展示并可编辑(与「出库物料准备」相同三区 + 物料备注) |
| 交接信息 | 填写交接人、交接时间、交接地点、接收人、备注 |
| 保存 | **一次提交**,同时保存物料台账 + 交接记录 |
### 非目标
- 不改动细胞产品状态、不扣质检物料库存(沿用台账策略)。
- v1 **不自动修改**输注计划 `schedule_status` / `infusion_status`(避免与「完成」结算逻辑冲突)。
- 不与中试申请单「交付方式 / 交接温度」合并。
---
## 2. 已确认的产品规则
| 项 | 决策 |
|----|------|
| **交接人** | 默认当前登录用户;`el-select` 远程搜索 `sys_user`(参考 `dept/index.vue` 负责人选择器);存 `handoverUserId` + 姓名快照 `handoverPerson` |
| **接收人** | 自由文本 `el-input`,存 `receiverPerson` |
| **交接时间** | 必填;`el-date-picker` type=`datetime`;**首次打开且无历史记录时默认此刻**(客户端 `new Date()`,格式 `YYYY-MM-DD HH:mm:ss`) |
| **交接地点** | 可选文本 |
| **备注** | 可选文本(交接区块独立备注,与物料 remark 分开) |
| **再次编辑** | **已交接后仍可再次打开「出库交接」编辑并 upsert**(非只读、非一次性锁定) |
| **查看弹窗** | **输注计划「查看」弹窗须只读展示交接信息**(见 §6.5) |
| **按钮可见性** | `infusionStatus !== 'yqx'`(已取消计划不显示) |
| **保存后状态** | v1 不改输注/生产状态 |
---
## 3. 架构决策
| 项 | 决策 |
|----|------|
| 方案 | **方案 A**:独立交接表 + 组合弹窗 + 单事务保存 |
| UI 模式 | **P22** 审核/子表编辑弹窗 + 表单区(参考 `OutboundReleaseMaterialDialog` + `ScheduleEditDialog`) |
| 物料数据 | 复用 `biz_outbound_release_material` 及 cells / qc / paper 子表(与放行审核共用) |
| 交接数据 | 新建 `biz_outbound_handover`(1:1 输注计划) |
| API 归属 | 挂在 **输注计划管理** 路由 `/web/admin/pilot-infusion-schedule-mgmt/` |
| 组件复用 | 从 `OutboundReleaseMaterialDialog.vue` 抽取 `OutboundMaterialFormSections.vue` 公共组件 |
### 与出库物料准备的关系
```
出库放行审核 ──「出库物料」──┐
├──► biz_outbound_release_material (+ 子表)
输注计划管理 ──「出库交接」──┘ │
└──► biz_outbound_handover(仅交接页写入)
```
两处编辑**同一份物料台账**;交接页额外写入交接元数据。
---
## 4. 数据库设计
SQL 文件:`docs/sql/outbound_handover.sql`
### 4.1 `biz_outbound_handover`(交接主表,1:1 输注计划)
```sql
CREATE TABLE IF NOT EXISTS `biz_outbound_handover` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`infusion_schedule_id` varchar(64) NOT NULL COMMENT '输注计划ID',
`handover_user_id` varchar(64) DEFAULT NULL COMMENT '交接人用户ID',
`handover_person` varchar(64) DEFAULT NULL COMMENT '交接人姓名快照',
`handover_time` datetime DEFAULT NULL COMMENT '交接时间',
`handover_location` varchar(200) DEFAULT NULL COMMENT '交接地点',
`receiver_person` varchar(200) DEFAULT NULL COMMENT '接收人(文本)',
`tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) DEFAULT NULL COMMENT '交接备注',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_oh_schedule` (`infusion_schedule_id`, `del_flag`),
KEY `idx_oh_tenant` (`tenant_no`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='输注计划-出库交接';
```
> 物料表结构不变,见 `docs/sql/outbound_release_material.sql`、`docs/sql/outbound_release_paper_material.sql`。
---
## 5. API 设计
前缀:`/web/admin/pilot-infusion-schedule-mgmt`
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/{scheduleId}/outbound-handover` | 获取交接信息 + 出库物料(编辑弹窗用) |
| PUT | `/{scheduleId}/outbound-handover` | 保存交接 + 出库物料(单事务) |
| GET | `/detail?scheduleId=` | **扩展**:响应增加 `outboundHandover`(查看弹窗用,见 §5.4) |
### 5.1 GET 响应
```json
{
"scheduleSummary": {
"applicationNo": "SQ20260527001",
"patientName": "张三",
"infusionTime": "2026-05-28 10:00:00"
},
"outboundMaterial": {
"cells": [],
"qcMaterials": [],
"paperMaterials": [],
"remark": ""
},
"handover": {
"handoverUserId": "user-uuid",
"handoverPerson": "李四",
"handoverTime": "2026-05-27 14:30:00",
"handoverLocation": "中试库房",
"receiverPerson": "王五(渠道)",
"remark": ""
}
}
```
- 若无交接记录:`handover` 各字段为空;编辑弹窗前端默认 **当前用户 + 此刻**。
- `outboundMaterial` 结构同 `GET .../outbound-materials`。
### 5.2 PUT Body
```json
{
"outboundMaterial": {
"remark": "",
"cells": [{ "cellProductId": "..." }],
"qcMaterials": [{ "materialId": "...", "qty": 1.5 }],
"paperMaterials": [{ "paperName": "知情同意书", "copies": 2, "attachmentUrl": "..." }]
},
"handover": {
"handoverUserId": "user-uuid",
"handoverTime": "2026-05-27 14:30:00",
"handoverLocation": "中试库房",
"receiverPerson": "王五",
"remark": ""
}
}
```
- `handoverPerson` **不由前端提交**,后端根据 `handoverUserId` 查 `sys_user.nick_name` 写入快照。
- 校验:`handoverUserId`、`handoverTime`、`receiverPerson` 必填;物料校验复用 `save_outbound_material` 规则。
### 5.3 保存流程(单事务)
```
save_outbound_handover(schedule_id, payload):
1. 校验输注计划存在 + 租户 + 数据权限
2. 校验 handover 必填字段;handoverUserId 对应用户存在
3. 调用 save_outbound_material(...) — 全量替换物料台账
4. upsert biz_outbound_handover(写 handover_person 快照)
5. db.commit()
```
### 5.4 扩展输注计划 Detail(查看弹窗)
现有 `GET /web/admin/pilot-infusion-schedule-mgmt/detail` 响应增加:
```json
{
"...": "原有 schedule / application / patient / products 字段",
"outboundHandover": {
"handoverPerson": "李四",
"handoverTime": "2026-05-27 14:30:00",
"handoverLocation": "中试库房",
"receiverPerson": "王五",
"remark": ""
}
}
```
- 无交接记录时:`outboundHandover` 为 `null` 或各字段空;查看弹窗 Section 仍展示,字段为 `—`。
- 查看弹窗**仅展示交接五字段**,不在此版扩展出库物料只读区(物料仍通过「出库交接」编辑弹窗维护)。
---
## 6. 前端设计
### 6.1 页面改动
| 文件 | 改动 |
|------|------|
| `infusion-schedule-management/index.vue` | 操作列「出库交接」;`min-width` 调至 ≥ 500px |
| `InfusionScheduleHandoverDialog.vue` | **新建**组合弹窗 |
| `ScheduleViewDialog.vue` | 新增「出库交接」只读 Section |
| `components/business/outbound-material-form-sections/index.vue` | **新建**(从 MaterialDialog 抽取) |
| `OutboundReleaseMaterialDialog.vue` | 改为引用公共组件(可选,建议同期重构) |
| `api/admin.ts` 或 `api/infusion-schedule-handover.ts` | fetch/save API |
### 6.2 弹窗结构
```
标题:出库交接
宽度:1120px(与出库物料准备一致)
class:dialog-form-layout
┌─ 摘要条 ─────────────────────────────────────┐
│ 申请单号 / 患者姓名 / 输注时间 │
├─ OutboundMaterialFormSections ──────────────┤
│ 出库细胞 | 使用的物料 | 其他纸质材料 | 物料备注 │
├─ 交接信息 (form-grid-2, label-position=top) ─┤
│ 交接人 [用户搜索 select,默认当前用户] │
│ 交接时间 [datetime picker,默认 now] │
│ 交接地点 [text] │
│ 接收人 [text,必填] │
│ 备注 [textarea,通栏] │
├─ footer: 取消 / 保存 ─────────────────────────┤
```
### 6.3 交接人选择器
- 复用 `fetchUserList({ keyword, status: '1' })` 远程搜索。
- 打开弹窗时:
- 若 GET 返回已有 `handoverUserId` → 回填;
- 否则默认 `useUserStore().info.userId` / `nickName`,并写入 options 首项。
- 选项 label:`nickName · userNo · phoneNo`(与部门负责人选择器一致)。
- `@change` 时更新本地 `handoverUserId`;展示名由后端快照为准。
### 6.4 交互规则
- 保存成功:`ElMessage.success('出库交接已保存')`。
- 物料区校验、附件上传、细胞选择逻辑与 `OutboundReleaseMaterialDialog` 完全一致。
- 取消关闭不保存;destroy-on-close 重置状态。
- **已保存过的计划**:再次点「出库交接」正常打开并回填;保存为 upsert,不提示「已完成不可改」。
### 6.5 查看弹窗(`ScheduleViewDialog.vue`)
在现有 `SystemViewDialog` sections 中,于「产品列表」与「其他信息」之间插入:
```text
Section 标题:出库交接
fields(只读 ViewField):
交接人 handoverPerson
交接时间 handoverTime
交接地点 handoverLocation
接收人 receiverPerson
备注 remark(span: true)
```
- 数据来源:`fetchInfusionScheduleMgmtDetail` 返回的 `outboundHandover`(后端 detail 聚合查询 `biz_outbound_handover`)。
- 无记录:该 Section 仍展示,各字段值为 `—`,或在 Section 内用一行 muted 文案「暂无出库交接记录」(与 `SystemViewDialog` 空值展示习惯一致,实现时二选一,推荐字段 `—` + section 始终可见)。
- 布局:`grid-columns` 保持与现有一致(3 列);备注 `span: true`。
---
## 7. 后端模块划分
| 文件 | 职责 |
|------|------|
| `app/crud/outbound_handover.py` | GET/PUT 聚合、upsert 交接、按 schedule 查询交接(供 detail 用) |
| `app/crud/pilot_infusion_schedule_mgmt.py` | detail 聚合 `outboundHandover` |
| `app/schemas/outbound_handover.py` | Pydantic |
| `app/api/pilot_infusion_schedule_mgmt.py` | outbound-handover 两个端点 + detail 扩展 |
| `app/models/models.py` | `BizOutboundHandover` ORM |
**不修改:** `cell_product`、`qc_material_usage` 库存逻辑。
---
## 8. 权限
- 建议菜单/按钮权限:`pilot-service:infusion-schedule-management:handover`
- v1 可与列表 edit 权限共用(`require_admin_or_super_admin`),上线前按产品确认。
---
## 9. 错误处理
| 场景 | HTTP | 说明 |
|------|------|------|
| 输注计划不存在 | 404 | — |
| 交接人用户不存在 | 400 | handoverUserId 无效 |
| 接收人为空 | 400 | — |
| 交接时间为空 | 400 | — |
| 物料校验失败 | 400 | 复用现有物料错误文案 |
---
## 10. 测试要点
1. 首次保存:物料 + 交接均写入;交接人快照与所选用户 nickName 一致。
2. 再次打开:物料与交接均正确回填;交接人 select 显示已选用户。
3. 新建弹窗默认:交接人 = 当前用户,交接时间 = **打开弹窗此刻**。
4. 已交接计划:再次打开编辑弹窗可修改并保存;查看弹窗展示最新交接信息。
5. 查看弹窗:有交接记录时五字段正确展示;无记录时 Section 可见且为空态。
6. 放行审核页改动物料后,交接页打开应看到最新物料。
7. 交接页改动物料后,放行审核页「出库物料」应看到同步结果。
8. 已取消计划(`yqx`)不显示「出库交接」按钮。
9. 保存后:`biz_cell_product` / `biz_qc_material.stock_qty` **不变**。
---
## 11. OpenSpec 增量(建议)
- `infusion-schedule-outbound-handover-api`: GET/PUT outbound-handover
- `infusion-schedule-outbound-handover-ui`: 列表按钮 + 组合弹窗 + 物料组件抽取
---
## 12. 工作量估算
| 项 | 人天 |
|----|------|
| SQL + Model + CRUD + API | 0.5 |
| 抽取物料公共组件 + 交接弹窗 | 1.0 |
| 查看弹窗 Section + detail 聚合 | 0.25 |
| 列表入口 + 联调 | 0.5 |
| **合计** | **~2.25** |
---
## 附录:决策记录
| 日期 | 议题 | 决策 |
|------|------|------|
| 2026-05-27 | 交接人 / 接收人 | 交接人:默认当前用户 + 用户表搜索;接收人:文本 |
| 2026-05-27 | 数据模型 | 独立 `biz_outbound_handover`,物料共用现有表 |
| 2026-05-27 | 保存语义 | 单事务:物料 + 交接一并保存 |
| 2026-05-27 | 状态联动 | v1 不改 schedule/infusion 状态 |
| 2026-05-27 | 交接时间 | 首次打开默认此刻 |
| 2026-05-27 | 编辑策略 | 已交接后可再次编辑(upsert) |
| 2026-05-27 | 查看弹窗 | 输注计划 ScheduleViewDialog 展示交接五字段 |
@@ -0,0 +1,284 @@
# 出库放行审核 · 出库物料准备(台账)设计
> 日期: 2026-05-27
> 状态: 待审阅
> 父模块: 中试服务管理 → 出库放行审核
> 关联: `openspec/changes/outbound-release-review`、方案 A(独立弹窗 + 子表)
> **库存策略(已确认)**: **台账模式 — 出库物料准备界面仅记录,不联动库存**
## 1. 背景与目标
### 现状
- 出库放行审核列表/查看/审核已实现,审核记录存于 `biz_outbound_release_review`。
- 每条输注计划尚无「出库物料准备」维护入口。
- 细胞产品库、质检物料库已有独立库存/状态管理(生产任务、质检任务等场景会真实扣减)。
### 目标
针对**每条输注计划**,提供「**出库物料准备**」界面,用于登记拟出库内容(台账),**保存时不改变细胞产品状态、不扣质检物料库存**:
| 区块 | 数据来源 | 保存行为 |
|------|----------|----------|
| 出库细胞 | 细胞产品库勾选 | 写入关联 ID + 字段快照,**不改** `biz_cell_product` |
| 使用的物料 | 质检物料库勾选 + 用量 | 写入关联 ID + 用量 + 快照,**不扣** `stock_qty` |
| 其他纸质材料 | 附件上传 | 仅存 URL |
### 非目标
- **不在本界面做真实出库**(不改细胞状态、不释库位、不写 `biz_qc_material_usage`)。
- 不与「交付物料清单」(`biz_pilot_delivery_material`)合并或自动同步。
- 批量审核弹窗不涉及出库物料(仍按条维护)。
- 真实出库若后续需要,另开能力/节点实现(本 spec 不覆盖)。
## 2. 架构决策
| 项 | 决策 |
|----|------|
| UI 定位 | 「出库物料**准备**」— 台账登记,非库存事务 |
| UI | 列表操作列「出库物料」→ `OutboundReleaseMaterialDialog.vue` |
| 数据 | 1 主表 + 2 子表;纸质附件放主表 |
| 保存语义 | **全量替换**:软删旧子表行 → 写入新行 + 主数据快照 |
| 库存 | **零联动**;前端可展示当前库存/状态作参考,保存时不校验库存是否足够 |
| 复杂度 | 无 revert/apply、无 `qc_material_usage` 扩展、无库位占用变更 |
## 3. 数据库设计
SQL 文件:`docs/sql/outbound_release_material.sql`
### 3.1 `biz_outbound_release_material`(主表,1:1 输注计划)
```sql
CREATE TABLE `biz_outbound_release_material` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`infusion_schedule_id` varchar(64) NOT NULL COMMENT '输注计划ID',
`paper_attachments` varchar(4000) DEFAULT NULL COMMENT '其他纸质材料附件(逗号分隔URL)',
`tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw',
`order_num` int(11) DEFAULT NULL,
`remark` varchar(512) DEFAULT NULL,
`create_by` varchar(64) DEFAULT NULL,
`create_dept_id` varchar(64) NOT NULL DEFAULT '',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_by` varchar(64) DEFAULT NULL,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`del_flag` tinyint(4) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_orm_schedule` (`infusion_schedule_id`, `del_flag`),
KEY `idx_orm_tenant` (`tenant_no`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='出库放行-出库物料准备主表';
```
### 3.2 `biz_outbound_release_cell`(出库细胞行)
```sql
CREATE TABLE `biz_outbound_release_cell` (
`id` varchar(64) NOT NULL,
`material_id` varchar(64) NOT NULL COMMENT '关联 biz_outbound_release_material.id',
`infusion_schedule_id` varchar(64) NOT NULL COMMENT '冗余,便于查询',
`cell_product_id` varchar(64) NOT NULL COMMENT '细胞产品ID',
`product_code` varchar(128) DEFAULT NULL COMMENT '保存时快照',
`product_name` varchar(200) DEFAULT NULL,
`product_type_name` varchar(100) DEFAULT NULL,
`spec_name` varchar(100) DEFAULT NULL,
`passage` varchar(32) DEFAULT NULL,
`batch_no` varchar(64) DEFAULT NULL,
`storage_slot_name` varchar(200) DEFAULT NULL,
`product_status` varchar(32) DEFAULT NULL COMMENT '保存时状态快照(只读展示)',
`tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw',
`order_num` int(11) DEFAULT NULL,
`remark` varchar(512) DEFAULT NULL,
`create_by` varchar(64) DEFAULT NULL,
`create_dept_id` varchar(64) NOT NULL DEFAULT '',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_by` varchar(64) DEFAULT NULL,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`del_flag` tinyint(4) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_orc_schedule_product` (`infusion_schedule_id`, `cell_product_id`, `del_flag`),
KEY `idx_orc_product` (`cell_product_id`, `del_flag`),
KEY `idx_orc_material` (`material_id`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='出库放行-出库细胞(台账)';
```
> 不需要 `product_status_before` / `storage_slot_id` 回滚字段。
### 3.3 `biz_outbound_release_qc_material`(质检物料行)
```sql
CREATE TABLE `biz_outbound_release_qc_material` (
`id` varchar(64) NOT NULL,
`material_id` varchar(64) NOT NULL COMMENT '关联 biz_outbound_release_material.id',
`infusion_schedule_id` varchar(64) NOT NULL,
`qc_material_id` varchar(64) NOT NULL,
`qty` decimal(18,4) NOT NULL COMMENT '计划用量(台账)',
`material_code` varchar(64) DEFAULT NULL,
`material_name` varchar(200) DEFAULT NULL,
`spec_model` varchar(200) DEFAULT NULL,
`unit` varchar(32) DEFAULT NULL,
`tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw',
`order_num` int(11) DEFAULT NULL,
`remark` varchar(512) DEFAULT NULL,
`create_by` varchar(64) DEFAULT NULL,
`create_dept_id` varchar(64) NOT NULL DEFAULT '',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_by` varchar(64) DEFAULT NULL,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`del_flag` tinyint(4) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_orqm_schedule_material` (`infusion_schedule_id`, `qc_material_id`, `del_flag`),
KEY `idx_orqm_material` (`material_id`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='出库放行-质检物料(台账)';
```
> **不**关联 `biz_qc_material_usage`;**不**扩展 `source_type`。
## 4. 核心业务流程
### 4.1 保存出库物料准备(单事务)
```
save_outbound_release_material(schedule_id, payload):
1. 校验输注计划存在 + 租户 + 数据权限
2. 校验 payload:
- cells 内 cell_product_id 不重复
- qcMaterials 内 material_id 不重复
- qty > 0
- 关联主数据存在且未软删(产品/物料)
3. upsert 主表(paper_attachments、remark)
4. 软删本 schedule 下旧 cell / qc_material 子行
5. 对每个 cell_product_id:读主数据 → 写快照 → insert 子行
6. 对每个 qc_material + qty:读主数据 → 写快照 → insert 子行
7. db.commit()
```
**明确不做:**
- 不调用 `cell_product.update` / 不改 `product_status`
- 不调用 `occupancy_svc.remove_by_item`
- 不调用 `qc_material_usage.deduct_*`
- 不校验质检库存是否 ≥ 用量(用量仅作计划登记)
### 4.2 编辑
- 再次保存 = 全量替换子表(软删旧行 + 插入新行)。
- 无库存回滚逻辑。
### 4.3 业务校验(台账级)
| 规则 | 说明 |
|------|------|
| 同计划细胞不重复 | 同一 `infusion_schedule_id` 下 `cell_product_id` 唯一 |
| 同计划物料不重复 | 同一 schedule 下 `qc_material_id` 唯一 |
| 跨计划可重复引用 | 不同输注计划可登记同一细胞/物料(台账互不排斥) |
| 主数据存在性 | 保存时产品/物料须存在;已软删的主数据不允许新增引用 |
| 细胞状态 | **不限制**可选状态;选择器展示状态供人判断,保存时不拦截 |
### 4.4 细胞产品选择范围
- 从细胞产品库分页/搜索勾选(复用 `TaskInputEnhancedTable` 选择弹窗模式)。
- 列表展示编码、名称、类型、代次、储物格、**当前状态**(参考信息)。
- **可选增强(v1.1)**:默认筛选与本计划出库生产工单产出相关的产品,仍允许从全库补选。
## 5. API 设计
前缀:`/web/admin/pilot-release-reviews`
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/{scheduleId}/outbound-materials` | 获取出库物料准备(主表 + 细胞行 + 质检行) |
| PUT | `/{scheduleId}/outbound-materials` | 全量保存(仅写台账) |
**PUT Body:**
```json
{
"paperAttachments": "url1,url2",
"remark": "",
"cells": [{ "cellProductId": "..." }],
"qcMaterials": [{ "materialId": "...", "qty": 1.5, "remark": "" }]
}
```
- `cells` / `qcMaterials` 可为空数组。
- `GET .../detail` 响应增加 `outboundMaterial` 域(查看弹窗只读)。
## 6. 前端设计
### 6.1 模式
- **P22** 审核列表 + 子表编辑弹窗。
- 弹窗标题建议:**「出库物料准备」**(与「真实出库」区分)。
- 参考:
- 细胞选择:`TaskInputEnhancedTable.vue`
- 质检物料:`qc-task/index.vue` 物料使用区(**去掉库存不足拦截**)
- 纸质附件:`OutboundReleaseReviewDialog.vue` 卡片上传
### 6.2 页面改动
| 文件 | 改动 |
|------|------|
| `outbound-release-review/index.vue` | 操作列「出库物料」 |
| `OutboundReleaseMaterialDialog.vue` | **新建** |
| `OutboundReleaseViewDialog.vue` | Section「出库物料准备」只读 |
| `api/outbound-release-review.ts` | fetch/save API |
### 6.3 交互规则
- 保存成功:「出库物料准备已保存」(**不提**库存更新)。
- 质检物料表可展示「当前库存」列作参考,**不**因库存不足阻止保存。
- 删除行:前端移除,点保存后生效。
- 操作列宽度:约 260px(查看 / 审核 / 出库物料)。
## 7. 后端模块划分
| 文件 | 职责 |
|------|------|
| `app/crud/outbound_release_material.py` | 查询、全量 save、主数据快照 |
| `app/schemas/outbound_release_material.py` | Pydantic |
| `app/api/outbound_release_review.py` | 两个端点 |
| `app/models/models.py` | 三个 ORM 模型 |
**不修改:** `qc_material_usage.py`、`cell_product.py` 库存/状态逻辑。
## 8. 错误处理
| 场景 | HTTP | 说明 |
|------|------|------|
| 细胞/物料不存在 | 400 | ID 无效或已删除 |
| 同计划重复勾选 | 400 | 同一产品/物料不可重复 |
| qty ≤ 0 | 400 | 用量须大于 0 |
| 输注计划不存在 | 404 | — |
## 9. 测试要点
1. 保存后:`biz_cell_product` 状态/库位**不变**;`biz_qc_material.stock_qty` **不变**。
2. 编辑替换细胞/物料行:仅子表与快照更新。
3. 两计划登记同一细胞:均允许保存(台账)。
4. 查看弹窗正确展示快照与附件。
5. 主数据后续变更:已保存台账仍显示保存时快照。
## 10. OpenSpec 增量
- `outbound-release-material-api`: GET/PUT outbound-materials(台账 CRUD)
- `outbound-release-material-ui`: 出库物料准备弹窗 + 查看 Section
## 11. 工作量估算
| 项 | 人天 |
|----|------|
| SQL + Model + CRUD | 1.0 |
| API + 详情聚合 | 0.5 |
| 前端弹窗 + 查看 | 1.5 |
| 联调 | 0.5 |
| **合计** | **~3.5** |
---
## 附录:策略变更记录
| 版本 | 策略 | 说明 |
|------|------|------|
| v0(讨论稿) | B 真实出库 | 保存时改状态 + 扣库存 + 回滚 |
| **v1(当前)** | **台账模式** | 出库物料**准备**界面仅记录;与交付清单类似,不联动库存 |
若后续需要在「审核通过」或独立「确认出库」节点做真实出库,应新建 spec,与本台账表通过 `infusion_schedule_id` 衔接,避免在本界面混入库存事务。