Files
zkxw/docs/superpowers/specs/2026-05-27-auto-protocol-no-design.md
T
2026-05-28 00:15:29 +08:00

199 lines
7.4 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-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 字段可选化)