340 lines
13 KiB
Markdown
340 lines
13 KiB
Markdown
# 细胞存储 ⇄ 科研捐赠 转移改造方案
|
||
|
||
> 日期: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` |
|
||
|
||
---
|
||
|
||
*文档结束*
|