199 lines
7.4 KiB
Markdown
199 lines
7.4 KiB
Markdown
# 协议号自动生成设计文档
|
||
|
||
> 日期:2026-05-27
|
||
> 方案:**B — 提交时后端生成**
|
||
> 状态:待评审
|
||
|
||
## 背景
|
||
|
||
采集协议单录入/管理、捐赠协议单录入/管理 4 个界面,新增弹窗中协议号当前为**下拉选择号池**(`biz_collection_protocol_no`)。需改为按时间序列自动编号,生成后不可修改。
|
||
|
||
## 已确认决策
|
||
|
||
| 项 | 决策 |
|
||
|----|------|
|
||
| 实现方案 | **方案 B**:保存(create)时由后端原子生成,前端新增态不预占号 |
|
||
| 采集协议前缀 | `XBCC` |
|
||
| 捐赠协议前缀 | `JZCC` |
|
||
| 日期段 | `YYYYMMDD`(服务器本地时区,建议 `Asia/Shanghai`) |
|
||
| 序号 | 6 位左补零,当日从 `000001` 递增 |
|
||
| 示例 | 采集:`XBCC20260527000001`;捐赠:`JZCC20260527000003` |
|
||
| 序号范围 | **按前缀独立计数**(XBCC 与 JZCC 各自维护当日序号,互不影响) |
|
||
| 不可修改 | 创建后协议号只读;编辑/更新接口拒绝修改 |
|
||
|
||
## 编号格式
|
||
|
||
```
|
||
{PREFIX}{YYYYMMDD}{SEQ6}
|
||
|
||
PREFIX 采集 = XBCC | 捐赠 = JZCC
|
||
YYYYMMDD 创建时刻服务器日期
|
||
SEQ6 同租户、同前缀、同日期下递增,000001–999999
|
||
```
|
||
|
||
总长度:4 + 8 + 6 = **18** 字符(现有字段足够)。
|
||
|
||
## 现状与改动范围
|
||
|
||
### 共用组件(改 2 个弹窗覆盖 4 页)
|
||
|
||
| 页面 | 弹窗 |
|
||
|------|------|
|
||
| 采集协议单录入 | `web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue` |
|
||
| 采集协议单管理 | 同上 |
|
||
| 捐赠协议单录入 | `web-admin/src/views/research-donation/list/DonationFormDialog.vue` |
|
||
| 捐赠协议单管理 | 同上 |
|
||
|
||
### 后端
|
||
|
||
| 模块 | 改动 |
|
||
|------|------|
|
||
| `backend/app/crud/collection_protocol_no.py` 或新建 `biz_no_generator.py` | 原子发号函数 |
|
||
| `backend/app/crud/collection_protocol.py` | `create_collection_protocol` 内发号;`update` 禁止改 `protocol_no` |
|
||
| `backend/app/crud/research_donation.py` | `create_research_donation` 内发号;`update` 禁止改 `donation_no` |
|
||
| `backend/app/api/collection_protocol.py` | CreateBody 中 `protocolNo` / `protocolNoId` 改为可选或忽略 |
|
||
| `backend/app/api/research_donation.py` | CreateBody 中 `donationNo` 改为可选或忽略 |
|
||
|
||
## 方案 B 数据流
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant U as 用户
|
||
participant FE as 前端弹窗
|
||
participant API as 后端 create
|
||
participant DB as MySQL
|
||
|
||
U->>FE: 打开新增弹窗
|
||
FE->>U: 协议号展示「保存后自动生成」(只读占位)
|
||
U->>FE: 填写表单并提交
|
||
FE->>API: POST create(不传 protocolNo / donationNo)
|
||
API->>DB: 事务:查当日最大序号 + 1
|
||
DB-->>API: 新号 XBCC/JZCC...
|
||
API->>DB: INSERT 协议单/捐赠单
|
||
API-->>FE: 返回含 protocolNo / donationNo
|
||
FE->>U: 成功提示,列表刷新
|
||
```
|
||
|
||
### 为何选方案 B
|
||
|
||
- 用户取消新增不产生跳号
|
||
- 并发安全:序号仅在 create 事务内分配
|
||
- 跨日边界以**提交时刻**为准,无「打开弹窗到保存跨零点」歧义
|
||
|
||
## 发号算法
|
||
|
||
### 序号来源(同前缀、同租户、同日期)
|
||
|
||
在事务内查询以下三处 `protocol_no` / `donation_no` 的最大后缀,取 **MAX + 1**:
|
||
|
||
1. `biz_collection_protocol.protocol_no` — 匹配 `XBCC{date}%`(仅采集发号时)
|
||
2. `biz_research_donation.donation_no` — 匹配 `JZCC{date}%`(仅捐赠发号时)
|
||
3. `biz_collection_protocol_no.protocol_no` — 同前缀同日期(兼容历史号池数据,避免撞号)
|
||
|
||
过滤条件:`tenant_no = 当前租户`,`del_flag = 0`。
|
||
|
||
### 并发控制
|
||
|
||
- 使用 `SELECT ... FOR UPDATE` 锁定序列表行,或
|
||
- 新建 `biz_biz_no_sequence` 表:`(tenant_no, prefix, biz_date, last_seq)` 唯一键,update 时 `last_seq = last_seq + 1`
|
||
|
||
**推荐序列表**(清晰、可扩展):
|
||
|
||
```sql
|
||
CREATE TABLE biz_biz_no_sequence (
|
||
id VARCHAR(64) NOT NULL PRIMARY KEY,
|
||
tenant_no VARCHAR(64) NOT NULL,
|
||
prefix VARCHAR(16) NOT NULL COMMENT 'XBCC / JZCC',
|
||
biz_date CHAR(8) NOT NULL COMMENT 'YYYYMMDD',
|
||
last_seq INT NOT NULL DEFAULT 0 COMMENT '当日已分配最大序号',
|
||
-- 公共字段 tenant_no 已在业务字段,其余 public fields 按 database-conventions
|
||
...
|
||
UNIQUE KEY uk_tenant_prefix_date (tenant_no, prefix, biz_date)
|
||
);
|
||
```
|
||
|
||
发号:`INSERT ... ON DUPLICATE KEY UPDATE last_seq = last_seq + 1`,再读回 `last_seq` 拼完整编号。
|
||
|
||
### 唯一性兜底
|
||
|
||
生成后仍调用现有 `assert_biz_no_available`(采集与捐赠跨表校验),防止极端竞态。
|
||
|
||
## 前端 UI
|
||
|
||
### 新增态
|
||
|
||
- 协议号字段:`el-input`,`disabled`,placeholder / 固定文案:**「保存后自动生成」**
|
||
- 移除:`el-select` 远程检索、`protocolNoOptions`、`remoteSearchProtocolNos`、占用/下发校验
|
||
- 表单校验:`protocolNo` / `donationNo` **新增时不必填**(由后端生成)
|
||
- 提交 payload:**不传** `protocolNo` / `protocolNoId` / `donationNo`(或传空,后端忽略)
|
||
|
||
### 编辑态
|
||
|
||
- 协议号:`el-input` 只读展示已有值
|
||
- 提交 payload:**不传**协议号字段
|
||
|
||
### 创建成功
|
||
|
||
- 使用接口返回的 `protocolNo` / `donationNo` 刷新列表(现有逻辑一般已满足)
|
||
|
||
## 后端 API 契约变更
|
||
|
||
### 采集协议单 POST create
|
||
|
||
- **Before**:`protocolNo` 必填,`protocolNoId` 可选
|
||
- **After**:`protocolNo`、`protocolNoId` 均可选;若客户端传入非空 `protocolNo`,**忽略**并服务端生成(或 400 拒绝客户端传号,推荐忽略以降低前端耦合)
|
||
- **Response**:保持返回完整协议对象,含生成的 `protocolNo`
|
||
|
||
### 科研捐赠 POST create
|
||
|
||
- **Before**:`donationNo` 必填
|
||
- **After**:`donationNo` 可选,服务端生成
|
||
- **Response**:含 `donationNo`
|
||
|
||
### PUT update
|
||
|
||
- 若 body 含 `protocolNo` / `donationNo` 且与库中不一致 → **400**「协议号不可修改」
|
||
- 或不接受该字段(Pydantic 从 UpdateBody 移除)
|
||
|
||
## 特殊场景
|
||
|
||
| 场景 | 行为 |
|
||
|------|------|
|
||
| 协议转科研 / 转存储 | **保留原号**,不走新发号(现有 `protocol_donation_convert` 逻辑不变) |
|
||
| 历史数据 | 旧号格式不受影响;新规则仅作用于新 create |
|
||
| 号池 Tab(采集协议号管理) | 短期保留,与新自动发号并行;新单不再依赖下拉选号 |
|
||
| `protocol_no_id` | 新单可置 `NULL`;若需审计可发号时同步写入号池并标记已使用 |
|
||
| 单日超 999999 条 | 返回业务错误「当日序号已用尽」 |
|
||
|
||
## 测试要点
|
||
|
||
1. 同租户同日前后 create 两条采集单 → `...000001`、`...000002`
|
||
2. 同租户同日采集 + 捐赠 → XBCC 与 JZCC 序号各自从 1 起
|
||
3. 并发 10 次 create → 无重复号
|
||
4. 编辑提交不改号;强行 API 改号 → 400
|
||
5. 转科研/转存储后编号与源单一致
|
||
|
||
## 不在本次范围
|
||
|
||
- 改造「采集协议号」Tab 的批量生成/下发流程
|
||
- 历史协议单批量洗号
|
||
- 小程序端(若存在同类选号逻辑,另开任务)
|
||
|
||
## 实现文件清单(预估)
|
||
|
||
**后端**
|
||
|
||
- `docs/sql/biz_biz_no_sequence.sql`(序列表 DDL)
|
||
- `backend/app/crud/biz_no_generator.py`(新建)
|
||
- `backend/app/crud/collection_protocol.py`
|
||
- `backend/app/crud/research_donation.py`
|
||
- `backend/app/api/collection_protocol.py`
|
||
- `backend/app/api/research_donation.py`
|
||
- `backend/app/models/models.py`(序列表 Model,可选)
|
||
|
||
**前端**
|
||
|
||
- `web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue`
|
||
- `web-admin/src/views/research-donation/list/DonationFormDialog.vue`
|
||
- `web-admin/src/types/api.d.ts`(CreateBody 字段可选化)
|