Files
zkxw/docs/superpowers/specs/2026-05-19-protocol-donation-convert-design.md
T
2026-05-19 12:03:08 +08:00

340 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 细胞存储 ⇄ 科研捐赠 转移改造方案
> 日期: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` |
---
*文档结束*