2026-05-27-auto-protocol-no-design.md 7.4 KB

协议号自动生成设计文档

日期: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 数据流

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

推荐序列表(清晰、可扩展):

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