# 细胞存储 ⇄ 科研捐赠 转移改造方案 > 日期:2026-05-19 > 状态:已实施(2026-05-19) > 关联模块:采集协议单管理、捐赠协议单管理 --- ## 1. 背景与问题 ### 1.1 业务场景 - **协议转科研**:细胞存储采集协议(`sxz`)→ 新建科研捐赠单,源协议状态 `zky`。 - **转细胞存储**:科研捐赠单(`sxz`)→ 新建采集协议单,源捐赠状态 `zckc`。 产品约定: 1. 转移时 **协议号与样本编码与源单一致**(同一业务在不同模块各一张单)。 2. 转移后用户 **可能单独调整** 采集协议号或捐赠协议号(两边不必长期同号)。 3. 二次编辑保存时不应误报「协议号已存在」「样本编码已被占用」。 ### 1.2 现状问题 | 问题 | 原因 | |------|------| | 转移后协议号为 `KY…` / `XC…` 临时串 | `convert_from_storage` / `convert_to_storage` 临时生成编号 | | 二次保存报样本编码已占用 | 全局唯一校验未识别「转移配对单」 | | 同号需求与跨表唯一校验冲突 | `protocol_no` 与 `donation_no` 互斥,未对配对单豁免 | --- ## 2. 改造目标 | # | 目标 | |---|------| | G1 | 转移瞬间:新单 `protocol_no` / `donation_no` = 源单展示号;样本 `sample_code` / `sample_code_id` 原样拷贝 | | G2 | 建立 **转移关联** 持久化记录(中间表),供保存/改号校验查询 | | G3 | 样本编码、协议号唯一性:对 **配对单据 ID** 豁免,不依赖编号字符串是否相同 | | G4 | 转移后允许用户 **独立修改** 本单协议号/捐赠号,**不自动同步** 对端 | | G5 | 非配对单据仍严格执行全局唯一,不影响手工新建/审核等流程 | --- ## 3. 方案概述 ```mermaid flowchart TB subgraph transfer [转移事务] A[源单状态变更 zky/zckc] B[新建目标单 同号同码] C[写入关联表 biz_protocol_donation_convert] end subgraph save [编辑保存] D[根据当前单 ID 查配对 protocol_id / donation_id] E[协议号校验 排除配对单] F[样本编码校验 排除配对单样本行] end transfer --> save ``` **核心原则**:配对关系由 **关联表 + 双方主键 ID** 锁定;`biz_no` 仅记录转移完成时的初始编号(审计),**不参与** 豁免判断。 --- ## 4. 数据库设计 ### 4.1 新建表 `biz_protocol_donation_convert` 脚本路径:`docs/sql/biz_protocol_donation_convert.sql` | 字段 | 类型 | 说明 | |------|------|------| | `id` | VARCHAR(64) | 主键 UUID | | `convert_type` | VARCHAR(32) | `storage_to_research` / `research_to_storage` | | `protocol_id` | VARCHAR(64) | `biz_collection_protocol.id` | | `research_donation_id` | VARCHAR(64) | `biz_research_donation.id` | | `biz_no` | VARCHAR(64) | **转移完成时** 的共用编号快照(不随改号自动更新) | | `tenant_no` | VARCHAR(64) | 租户 | | 公共字段 | — | `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag` | **索引与约束(建议)** - `UNIQUE (protocol_id)`:同一采集协议仅允许一条有效转移关联(应用层保证 `del_flag=0` 唯一)。 - `UNIQUE (research_donation_id)`:同一科研捐赠仅允许一条有效转移关联。 - `KEY (tenant_no, biz_no, del_flag)`:按初始编号排查。 **不新建** 主表字段为一期必选项;科研侧已有 `source_type`、`source_protocol_id` 可继续用于列表筛选与展示。 ### 4.2 与现有表关系 ``` biz_collection_protocol (1) ──< biz_protocol_donation_convert >── (1) biz_research_donation │ │ ├── biz_protocol_sample ├── biz_research_donation_sample ``` 一对一:一次转移产生一对主单 + 一条关联记录。 --- ## 5. 转移业务规则 ### 5.1 协议转科研(`convert_from_storage`) **前置** - 源协议存在、`tenant_no` 匹配、`del_flag=0`。 - 源协议状态 = `sxz`(生效中)。 - 该 `protocol_id` 尚未存在有效关联记录。 **事务内操作** | 步骤 | 操作 | |------|------| | 1 | 新建 `biz_research_donation`:`donation_no = proto.protocol_no`,`donation_status=dsx`,`source_type=from_storage`,`source_protocol_id=protocol_id` | | 2 | 复制 party / collection / samples(+transport),样本编码与源一致 | | 3 | 源协议 `protocol_status=zky`,写 `to_research_time` / `to_research_desc` | | 4 | 插入关联表:`convert_type=storage_to_research`,`biz_no=proto.protocol_no` | **删除**:`KY` + 时间戳 + UUID 临时号逻辑。 ### 5.2 转细胞存储(`convert_to_storage`) **前置** - 源捐赠存在、`sxz`、无有效关联。 **事务内操作** | 步骤 | 操作 | |------|------| | 1 | 新建 `biz_collection_protocol`:`protocol_no = donation.donation_no`,`protocol_status=dsx`;`protocol_no_id` 可按 `biz_no` 反查号池(可选) | | 2 | 复制子表,样本同码;价格字段置空 | | 3 | 源捐赠 `donation_status=zckc`,写 `to_storage_time` / `to_storage_desc` | | 4 | 插入关联表:`convert_type=research_to_storage`,`biz_no=donation.donation_no` | **删除**:`XC` + 时间戳临时号逻辑。 ### 5.3 转移后改号(用户可能调整) | 规则 | 说明 | |------|------| | 允许改号 | 采集协议可改 `protocol_no`;科研捐赠可改 `donation_no`(需确认前端是否已暴露字段,后端须支持校验) | | 不同步对端 | 改 A 单号 **不自动修改** B 单号(方案 A) | | 关联表 `biz_no` | **保留转移快照**,不随改号更新 | | 可选 UI 提示 | 改号成功后提示「配对单据当前编号为 xxx」(二期) | --- ## 6. 校验规则改造 ### 6.1 公共能力(新建) 位置建议:`backend/app/crud/protocol_donation_convert.py`(或合入 `research_donation.py`) | 函数 | 说明 | |------|------| | `create_convert_link(db, ...)` | 转移事务内写入关联 | | `get_convert_partner(db, tenant_no, *, protocol_id=None, donation_id=None)` | 返回 `{ protocol_id, research_donation_id, biz_no, convert_type }` 或 `None` | | `get_partner_sample_codes(db, tenant_no, protocol_id, donation_id)` | 配对单上所有 `del_flag=0` 的 `sample_code` 集合 | ### 6.2 样本编码唯一性 改造:`collection_protocol.validate_sample_codes_not_used` ```text 占用集合 = 全租户 采集样本表 ∪ 科研捐赠样本表(del_flag=0) 占用集合 -= 配对 protocol_id 下样本编码 占用集合 -= 配对 research_donation_id 下样本编码 若 待保存编码 ∩ 占用集合 非空 → 报错 ``` **调用点** - `collection_protocol.save_samples`:传入当前 `collection_protocol_id`,查 partner 后排除。 - `research_donation._create_samples`:传入当前 `research_donation_id`,查 partner 后排除。 > 本单 upsert 会先软删旧样本行,排除逻辑必须基于 **配对单 ID**,与双方编号是否一致无关。 ### 6.3 协议号 / 捐赠号跨表唯一性 改造:`create_collection_protocol`、`update_collection_protocol`、`create_donation`、`update_donation`(若支持改 `donation_no`)。 **判断「编号是否冲突」时:** ```text 冲突对象 = 其他采集协议.protocol_no(排除当前 protocol_id) ∪ 其他科研捐赠.donation_no(排除当前 donation_id) 若存在 convert 配对: - 更新采集协议时:再排除 配对 research_donation_id 对应行的 donation_no - 更新科研捐赠时:再排除 配对 protocol_id 对应行的 protocol_no 仅当 新编号 落在「冲突对象」中时报错 ``` **说明**:配对单占用 **整行单据** 豁免,不要求新号与对端当前号相同。例如协议改为 A001、捐赠仍为 A002,只要无第三方占用,均应通过。 ### 6.4 非转移场景 - 手工新建、无关联记录:保持现有严格唯一。 - 批量转移失败策略:维持现有「逐条调用、部分成功」行为。 --- ## 7. 后端改动清单 | 序号 | 文件 | 改动要点 | |------|------|----------| | 1 | `docs/sql/biz_protocol_donation_convert.sql` | 建表 | | 2 | `backend/app/models/research_donation.py` | `BizProtocolDonationConvert` | | 3 | `backend/app/models/__init__.py` | 导出 Model | | 4 | `backend/app/crud/protocol_donation_convert.py` | 关联 CRUD + 查询(新建) | | 5 | `backend/app/crud/research_donation.py` | `convert_*` 同号同码 + 写关联;`_create_samples` 传排除参数;`update_donation` 支持 `donation_no` 改号校验 | | 6 | `backend/app/crud/collection_protocol.py` | `validate_sample_codes_not_used`、`save_samples`、create/update 协议号校验 | | 7 | `backend/app/api/research_donation.py` | 无路由变更(可选详情返回 `convertPartner`) | **API**:不新增对外接口;现有: - `POST /web/admin/research-donations/convert-from-storage` - `POST /web/admin/research-donations/{id}/convert-to-storage` --- ## 8. 前端改动清单 ### 8.1 一期(随然后端联调) | 项 | 改动 | |----|------| | 转移确认文案 | 说明「将创建新单,**初始**协议号与样本编码与源单一致」 | | 编辑表单 | 转移生成的新单:**协议号/捐赠号保持可编辑**(勿做只读) | | 保存逻辑 | 无改动,依赖后端豁免 | ### 8.2 二期(体验增强) | 项 | 改动 | |----|------| | 查看弹窗 | 展示「转移关联」:对端单据类型、编号、跳转 | | 改号提示 | 保存后若与对端编号不一致,轻提示对端当前号 | --- ## 9. 历史数据 | 类型 | 处理 | |------|------| | 已用 `KY…`/`XC…` 生成的转移单 | 脚本按 `source_protocol_id`、`to_research_desc`、`remark` 尝试配对并补关联表;编号是否改为源单同号需 **业务确认后** 再批量更新 | | 无法配对 | 保持现状,仅新转移走新逻辑 | 脚本建议:`backend/scripts/backfill_protocol_donation_convert.py`(可选)。 --- ## 10. 实施步骤 | 阶段 | 内容 | 产出 | |------|------|------| | P1 | SQL + Model + 关联 CRUD | 表可建、可查询配对 | | P2 | 改造 `convert_from_storage` / `convert_to_storage` | 转移同号同码 + 写关联 | | P3 | 样本编码校验 + `save_samples` / `_create_samples` | 二次编辑样本保存通过 | | P4 | 协议号/捐赠号 create & update 豁免 | 改号不误报、可独立改号 | | P5 | 联调 + 回归 + 确认文案 | 测试清单通过 | | P6 | 历史数据脚本(可选) | 补关联记录 | --- ## 11. 测试用例 ### 11.1 转移 | # | 步骤 | 预期 | |---|------|------| | T1 | `sxz` 协议转科研 | 捐赠号=协议号;样本码一致;关联表 1 条;源单 `zky` | | T2 | `sxz` 捐赠转存储 | 协议号=捐赠号;样本码一致;关联表 1 条;源单 `zckc` | | T3 | 非 `sxz` 转移 | 400 业务错误 | | T4 | 已转移协议再次转科研 | 失败 | ### 11.2 二次保存(样本) | # | 步骤 | 预期 | |---|------|------| | T5 | 转移后不改编码,直接保存 | 成功 | | T6 | 修改非编码字段后保存 | 成功 | ### 11.3 改号 | # | 步骤 | 预期 | |---|------|------| | T7 | 只改采集协议号 | 成功;捐赠号可不变 | | T8 | 只改捐赠号 | 成功 | | T9 | 改为第三方已占用号 | 失败 | | T10 | 改为配对单当前持有的号 | 成功(豁免) | ### 11.4 回归 | # | 步骤 | 预期 | |---|------|------| | T11 | 无关联手工新建协议+样本 | 重复编码仍失败 | | T12 | 审核、状态变更、删除 | 行为不变 | --- ## 12. 风险与待定项 | 项 | 说明 | 建议 | |----|------|------| | 号池下发状态 | 转移同号后 `biz_collection_protocol_no` 是否标记已下发 | 二期与基础数据模块对齐 | | `protocol_no_id` | 转存储时是否回填号池 ID | 可选:按 `biz_no` 查池 | | 链式转移 | 同一业务多次互转 | 当前按 **一对一** 设计;若需支持需单独立项 | | 软删一方 | 另一方编辑是否仍豁免 | 建议:**仍豁免**(关联保留,占用仅计 `del_flag=0`) | --- ## 13. 已确认产品决策 | 决策 | 结论 | |------|------| | 转移初始编号 | 与源单相同 | | 转移初始样本编码 | 与源单相同 | | 转移后改号 | 允许,两边可不一致 | | 改号是否同步对端 | **否** | | 关联表 `biz_no` | 转移快照,不随改号更新 | | 豁免依据 | 关联表 **单据 ID**,非编号字符串 | --- ## 14. 参考代码位置(改造前) | 逻辑 | 文件 | |------|------| | 转移(临时号) | `backend/app/crud/research_donation.py` → `convert_from_storage` / `convert_to_storage` | | 样本编码校验 | `backend/app/crud/collection_protocol.py` → `validate_sample_codes_not_used` | | 协议号互斥 | `create_collection_protocol` / `update_collection_protocol` / `create_donation` | | 前端转移调用 | `web-admin/src/api/research-donation.ts` | --- *文档结束*