# 协议号自动生成设计文档 > 日期: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 字段可选化)