docs: add sample freight batch design for shared transport cost

Capture the agreed approach: freight batch entity linked at sample level from protocol forms, dictionary-backed transport modes, internal cost only.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-10-07 23:09:40 +08:00
1 parent 818b809614
commit a6138d60bc
1 file changed
+259
@@ -0,0 +1,259 @@
# 样本运费批次 — 实现设计(方案 1)
> 日期:2026-10-07
> 状态:已确认(2026-10-07)
> 关联:细胞存储 / 科研捐赠「采集协议单」样本及运输信息;内部成本登记(不向客户收取)
---
## 1. 背景
### 1.1 现状
细胞存储与科研捐赠协议单共用 `SampleTransportFormSection`:
- 运输挂在**每个样本**上(`biz_protocol_sample_transport` / `biz_research_donation_sample_transport`)。
- 运输类型为固定枚举:`kdys`(快递运输)/ `ryxd`(人员携带)。
- 快递场景下人工填写快递公司、单号、目的站点、**样本级快递费用**、预计到达等。
- 批量「配置运输信息」只能把同一套信息复制到**当前协议单内**勾选样本,无法表达「多协议单共用一笔运费、只收一次」。
### 1.2 客户诉求
- **运输方式**:默认「顺丰快递」「专线车」;支持填写其他方式;若字典中不存在则自动加入字典,后续可选。
- **运费**:单位元;可对应多个样本(含跨多个协议单),例如同一专线车只登记一次费用。
- 本阶段**只登记内部成本**,不向客户收取,不进合同金额 / 缴费。
- 旧物流细节(单号、站点等)本阶段不要;后续再扩展。
### 1.3 已确认决策
| 项 | 结论 |
|----|------|
| 录入时机 | 录采集协议单时填写(已知会与其他单共用) |
| 关联粒度 | 样本级(可跨协议;单内也可仅部分样本) |
| 与旧运输 | 替换样本快递费用与旧运输录入;单号/站点等本阶段不做 |
| 费用性质 | 仅内部成本登记 |
| 架构 | 运费批次实体 + 协议单内新建/关联(方案 1);暂不做独立运费中心 |
---
## 2. 目标与非目标
### 2.1 目标
- 新建「运费批次」:运输方式(字典)+ 运费金额;多样本可关联同一批次。
- 支持跨细胞存储 / 科研捐赠协议单的样本共用一笔运费。
- 协议单 Tab「样本及运输信息」提供「配置运费」:新建 / 关联已有 / 修改方式与金额 / 取消关联。
- 运输方式字典预置「顺丰快递」「专线车」;手输不存在项时自动写入字典。
- 列表与查看弹窗展示运输方式 + 运费(及共用信息);合同/缴费逻辑不读运费。
### 2.2 非目标
- 独立运费菜单/业务中心(后续可加轻量查询列表)。
- 运费计入合同金额、缴费、向客户分摊或开票。
- 快递单号、目的站点、预计到达、携带人等物流字段。
- 旧运输记录自动迁移为运费批次。
- 小程序端改造。
- 物理删除旧运输表(保留兼容历史只读数据)。
---
## 3. 数据模型
### 3.1 运费批次主表 `biz_sample_freight`
| 字段 | 说明 |
|------|------|
| `id` | UUID PK |
| `transport_mode` | 运输方式字典值(`sample_transport_mode`) |
| `freight_amount` | 运费(元),`Numeric(18,2)`,≥ 0 |
| 公共字段 | `tenant_no`、`order_num`、`remark`、`create_by`、`create_dept_id`、`create_time`、`update_by`、`update_time`、`del_flag` |
字段顺序遵循 database-conventions:`id` → 业务字段 → 公共字段。
### 3.2 样本关联表 `biz_sample_freight_item`
| 字段 | 说明 |
|------|------|
| `id` | UUID PK |
| `freight_id` | 运费批次 ID |
| `sample_source` | `cell_storage` \| `research_donation` |
| `sample_id` | 样本主键 |
| `sample_code` | 样本编码快照(可选,便于展示) |
| `protocol_id` | 所属协议单 ID(便于反查) |
| 公共字段 | 同上 |
**约束(业务层强制,建议唯一索引):**
同一租户下,`(sample_source, sample_id)` 且 `del_flag=0` 至多对应一条关联(一个样本最多挂一笔运费)。
### 3.3 字典 `sample_transport_mode`
- 预置两项:
| dict_value | dict_label |
|------------|------------|
| `sfkd` | 顺丰快递 |
| `zxc` | 专线车 |
- 创建/更新运费时:前端可传 `transportMode`(已有 value)或手输文案。后端解析顺序:
1. 按 `dict_value` 命中 → 使用该项;
2. 否则按 `dict_label` trim 后精确匹配 → 使用已有项;
3. 否则自动新增:`dict_label` = trim 后文案,`dict_value` = 同文案(若 value 冲突则加短后缀保证唯一),再保存运费。
- 仅当前租户自动新增;不做跨租户共享。
### 3.4 与旧运输表
- `biz_protocol_sample_transport`、`biz_research_donation_sample_transport`:**保留不删**。
- 本阶段协议单保存路径**不再写入**旧运输明细。
- 历史数据不自动迁移;需补录时人工「配置运费」。
- 下游若仍读旧运输字段:无数据则展示空,不阻断主流程;物流扩展时再对齐。
---
## 4. 协议单交互
细胞存储 `ProtocolFormDialog` / `ProtocolViewDialog` 与科研捐赠 `DonationFormDialog` / `DonationViewDialog` 行为一致;公共组件由「配置运输信息」改为「配置运费」(可演进现有 `SampleTransportFormSection`)。
### 4.1 列表
- 移除列:运输类型、目的站点、预计到达、快递/携带摘要。
- 新增:运输方式、运费(元);共用时显示同一金额,并标注共用(如关联样本数或「共用」)。
- 未关联运费显示「—」。
### 4.2 工具栏
1. **添加样本**(不变)。
2. **配置运费**:须先勾选样本,否则禁用。
3. **取消运费关联**:对勾选且已关联的样本解除关联;批次下若无剩余样本则软删批次。
### 4.3 「配置运费」弹窗
**模式 A · 新建运费**
- 运输方式:可搜索下拉,允许手输;提交时字典无则自动新增。
- 运费(元):必填,≥ 0。
- 确定:创建批次并挂上当前勾选样本。
**模式 B · 关联已有运费**
- 分页检索近期批次:运输方式、金额、已关联样本数、创建时间。
- 确定:将勾选样本挂到该批次;若样本已有运费,提示将替换原关联后执行。
默认「新建」;可切换「关联已有」。
### 4.4 编辑与跨单
- 勾选样本若同属一批次,可在弹窗中改运输方式/金额;保存前提示「将同步 N 个关联样本」。
- 协议单 B 可通过「关联已有」挂到协议单 A 创建的批次。
- 查看态仅展示运输方式 + 运费(及共用信息)。
### 4.5 样本价格(细胞存储)
- 样本「原价格 / 优惠价格」与合同金额联动逻辑保持不变。
- 运费与合同金额、缴费**无联动**。
---
## 5. 接口与保存规则
### 5.1 运费 API(前缀 `/api/admin`)
| 方法 | 路径 | 用途 |
|------|------|------|
| GET | `/sample-freights` | 分页检索(关联已有) |
| GET | `/sample-freights/{id}` | 详情(含关联样本:来源、协议、编码) |
| POST | `/sample-freights` | 新建批次并挂样本 |
| PUT | `/sample-freights/{id}` | 更新方式/金额 |
| POST | `/sample-freights/{id}/link` | 追加关联 |
| POST | `/sample-freights/{id}/unlink` | 取消关联;无样本则软删批次 |
请求/响应字段使用 camelCase(`transportMode`、`freightAmount`、`sampleSource`、`sampleId`、`protocolId`、`sampleCode`、`linkedSampleCount` 等)。
创建/更新时执行运输方式字典自动入库。
### 5.2 与协议单保存
**已落库样本**(编辑已有协议,或协议已保存过):
- 「配置运费」确定后**立即**调用运费 API。
- 协议单保存不再嵌套旧 `transport` 整包写入。
**未落库草稿样本**(新建协议未保存):
- 前端用 `freightDraft.clientKey` 让多个草稿样本共用同一笔草稿运费。
- 协议单创建/更新 body 中样本可带:
- `freightId`:关联已有批次;或
- `freightDraft: { clientKey, transportMode, freightAmount }`:同一 `clientKey` 只创建一笔批次再挂样本。
- 保存成功后详情返回真实 `freightId`;之后改价走独立 API。
### 5.3 校验
- `transportMode`、`freightAmount` 必填;金额 ≥ 0。
- 同一租户样本不可同时挂两笔运费;重新配置 = 解旧链再挂新链(须提示)。
- 全部查询/写入按当前用户 `tenant_no` 隔离,并套数据权限(与协议样本一致)。
### 5.4 协议详情 enrichment
样本返回例如:
```json
"freight": {
"id": "...",
"transportMode": "zxc",
"transportModeLabel": "专线车",
"freightAmount": 800,
"linkedSampleCount": 3
}
```
### 5.5 历史数据
- 旧运输只读保留,不迁移。
- 新编辑以运费批次为准。
---
## 6. 前端改造要点
| 位置 | 改动 |
|------|------|
| `sample-transport-form-section` | 改为运费配置(或拆出 `SampleFreightFormSection`);列表列与弹窗按 §4 |
| `ProtocolFormDialog` / `DonationFormDialog` | 接入运费草稿与保存 payload;去掉对旧 transport 必填依赖 |
| `ProtocolViewDialog` / `DonationViewDialog` | 样本运输区改为方式 + 运费 |
| API 模块 | 新增 `sample-freight.ts`(或等价) |
| 字典 | 初始化 `sample_transport_mode`;下拉允许创建 |
UI 遵循 Fresh Mint / 现有协议弹窗约定:`label-position="top"`、`dialog-form-layout`、无 backdrop blur、按钮带图标。
---
## 7. 风险与对策
| 风险 | 对策 |
|------|------|
| 新建协议样本无服务端 ID | `freightDraft.clientKey`,协议保存时建批次 |
| 跨单改价影响他人样本 | 改价前提示同步关联样本数 |
| 字典手输脏数据/重复 | 租户内按 trim 后的 label 去重;实现阶段可再加强规范化 |
| 下游仍读旧运输 | 空值展示,不阻断;后续扩展再对齐 |
| 历史仅有旧运输 | 不迁移;人工补录运费 |
---
## 8. 验收标准
1. 预置「顺丰快递」「专线车」可选;手输新方式保存后,再次打开下拉可选到。
2. 同一协议单内多样本可共用一笔运费,金额只录一次。
3. 不同协议单(含细胞存储与科研捐赠)的样本可关联同一笔运费。
4. 修改批次金额/方式后,所有关联样本展示一致。
5. 取消关联后样本运费为空;批次无样本后软删。
6. 合同金额、缴费明细不因运费变化。
7. 新保存协议单不再依赖旧运输明细写入;查看弹窗不再展示单号/站点等旧字段。
---
## 9. 实现顺序(建议)
1. SQL:字典类型/数据 + `biz_sample_freight` + `biz_sample_freight_item`(及唯一索引)。
2. 后端:model / crud / api;字典自动入库;协议详情 enrichment;协议保存处理 `freightId` / `freightDraft`,停止写旧运输。
3. 前端:API + 协议单公共运费组件 + 查看态。
4. 测试:共用、跨来源关联、草稿 clientKey、字典自动新增、合同金额不受影响。
5. 按仓库约定增量更新 graphify(backend / web-admin)。