218 lines
7.9 KiB
Markdown
218 lines
7.9 KiB
Markdown
# 样本接收 — 实验室入库编码
|
||
|
||
**日期**:2026-06-03
|
||
**状态**:已实现
|
||
**关联**:`web-admin/src/views/quality-inspection/sample-reception/index.vue`、`backend/app/crud/sample_reception.py`、`backend/app/crud/biz_no_generator.py`、`docs/superpowers/specs/2026-05-19-unified-sample-reception-design.md`
|
||
|
||
---
|
||
|
||
## 1. 已确认业务规则
|
||
|
||
| 项 | 规则 |
|
||
|----|------|
|
||
| 字段名称 | **实验室入库编码** |
|
||
| 编码格式 | `RK` + `YYYYMMDD` + **3 位序号**(左补零),例:`RK20260603001` |
|
||
| 日期段 | **`YYYYMMDD` = 样本接收时间的日期部分**(非服务器当天、非打开弹窗当天) |
|
||
| 序号规则 | 同一租户、同一接收日期下递增;默认自动生成;用户可在提交前手动修改 |
|
||
| 并发 | 自动分配须原子占号,避免多人同时拿到相同默认序号 |
|
||
| 锁定 | 保存成功后编码**只读**,编辑已接收记录时不可修改 |
|
||
| 结论条件 | **仅接收(`js`)** 生成并保存入库编码;**拒收(`bj`)** 不生成、不保存、不展示该字段 |
|
||
| 适用范围 | 单条接收、批量接收、查看详情、已接收列表(仅 `js` 有值) |
|
||
|
||
---
|
||
|
||
## 2. 方案(已选 A)
|
||
|
||
| 方案 | 说明 | 结论 |
|
||
|------|------|------|
|
||
| **A. 预占 + 提交校验** | 打开弹窗/改接收日期时调 allocate;提交校验格式、日期一致性与唯一性 | **采用** |
|
||
| B. 仅提交时分配 | 预览码不锁号 | 不满足并发要求 |
|
||
| C. 前端生成 | 无服务端原子序号 | 不采用 |
|
||
|
||
---
|
||
|
||
## 3. 数据模型
|
||
|
||
### 3.1 表变更
|
||
|
||
`biz_sample_reception` 新增:
|
||
|
||
```sql
|
||
ALTER TABLE biz_sample_reception
|
||
ADD COLUMN lab_storage_code VARCHAR(32) NULL
|
||
COMMENT '实验室入库编码 RK+接收日期YYYYMMDD+3位序号'
|
||
AFTER sample_code;
|
||
```
|
||
|
||
索引(软删除场景):
|
||
|
||
```sql
|
||
CREATE UNIQUE INDEX uk_sr_tenant_lab_code
|
||
ON biz_sample_reception (tenant_no, lab_storage_code, del_flag);
|
||
```
|
||
|
||
> 说明:`del_flag=0` 时租户内唯一;历史软删记录保留原编码,不参与业务冲突。
|
||
|
||
### 3.2 序号表复用
|
||
|
||
复用现有 `biz_biz_no_sequence`,`prefix = 'RK'`,`biz_date = YYYYMMDD`(**来自接收时间日期**)。
|
||
|
||
- 序号宽度:**3 位**,上限 999/日/租户
|
||
- 与 `allocate_next_biz_no()` 相同:`INSERT ... ON DUPLICATE KEY UPDATE last_seq = LAST_INSERT_ID(last_seq + 1)`
|
||
|
||
### 3.3 手动编码与序号同步
|
||
|
||
用户手动输入 `RK20260603088` 且提交成功时,在同一事务内:
|
||
|
||
```
|
||
last_seq = MAX(last_seq, 88)
|
||
```
|
||
|
||
避免后续自动分配仍从较小序号开始导致撞号。
|
||
|
||
分配前亦取:
|
||
|
||
```
|
||
next_seq = MAX(last_seq, MAX(已存在编码末三位)) + 1
|
||
```
|
||
|
||
(限定同一 `tenant_no + prefix + biz_date`)
|
||
|
||
---
|
||
|
||
## 4. 后端 API
|
||
|
||
### 4.1 预分配编码
|
||
|
||
```
|
||
POST /web/admin/sample-receptions/allocate-lab-storage-codes
|
||
```
|
||
|
||
**Body**
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| `receptionDate` | string | 必填,`YYYY-MM-DD`,由前端从「样本接收时间」截取 |
|
||
| `count` | int | 默认 1,批量接收为 N,上限 50 |
|
||
|
||
**Response**
|
||
|
||
```json
|
||
{ "codes": ["RK20260603001", "RK20260603002"] }
|
||
```
|
||
|
||
**逻辑**
|
||
|
||
1. 校验 `receptionDate` 格式
|
||
2. 对 `biz_date = receptionDate.replace('-','')` 原子递增 `count` 次
|
||
3. 返回连续编码列表
|
||
|
||
权限:与样本接收提交一致(`require_admin_or_super_admin` 或现有接收写权限)。
|
||
|
||
### 4.2 创建 / 更新接收
|
||
|
||
`SampleReceptionCreateBody` / `SampleReceptionUpdateBody` 增加:
|
||
|
||
- `labStorageCode`(**仅 `acceptConclusion=js` 时必填**;`bj` 时忽略且存 `NULL`;更新不可改)
|
||
|
||
**创建校验(`acceptConclusion=js`)**
|
||
|
||
1. `labStorageCode` 非空
|
||
2. 正则:`^RK\d{8}\d{3}$`
|
||
3. 编码中 `YYYYMMDD` **必须等于** `receptionTime` 的日期部分
|
||
4. 租户内 `del_flag=0` 唯一
|
||
5. 成功后同步 `biz_biz_no_sequence.last_seq`(见 §3.3)
|
||
|
||
**创建校验(`acceptConclusion=bj`)**
|
||
|
||
- 不得传入有效 `labStorageCode`(传入非空则 400,或一律忽略并置 `NULL`)
|
||
- 不调用 allocate、不占用 RK 序号
|
||
|
||
**更新**
|
||
|
||
- 忽略客户端传入的 `labStorageCode` 变更(或 API 不接受该字段),保持原值
|
||
|
||
### 4.3 列表与详情
|
||
|
||
`_reception_to_dict` / 列表项增加 `labStorageCode`;已接收列表关键词搜索纳入该字段。
|
||
|
||
---
|
||
|
||
## 5. 前端交互
|
||
|
||
### 5.1 单条接收弹窗
|
||
|
||
1. 默认「是否接收」= **接收(`js`)**;此时:
|
||
- 打开弹窗 → 默认接收时间 = 当前时间 → 调 `allocate(receptionDate, count=1)` → 填入「实验室入库编码」
|
||
2. 用户切换为 **拒收(`bj`)**:
|
||
- **隐藏**「实验室入库编码」表单项
|
||
- 清空本地编码值;**不再**占用/提交编码
|
||
3. 用户从 `bj` 切回 `js`:按当前接收时间日期重新 `allocate(1)`(除非编辑已保存的 `js` 记录)
|
||
4. 用户修改**样本接收时间**且**日期变化**(且当前为 `js`):
|
||
- 若编码未被手动编辑 → 自动按新日期重新 `allocate(1)` 并替换
|
||
- 若已手动编辑 → 不自动改码;提交时若日期与编码不一致,提示错误
|
||
5. 编码输入框:新建 `js` 时可编辑;编辑已保存 `js` 记录时 `disabled`;`bj` 记录不展示
|
||
6. 字段位置:「接收登记」区,与接收温度/体积同级(`form-grid-3`)
|
||
7. label 提示:`<span class="form-label-tip">RK+接收日期+序号,保存后不可修改</span>`
|
||
|
||
### 5.2 批量接收
|
||
|
||
1. 打开弹窗 → 每条 entry 默认 `receptionTime = now`、`acceptConclusion = js`
|
||
2. **仅 `acceptConclusion=js` 的 entry** 参与 allocate:按接收日期分组,`allocate(count=组内 js 条数)`
|
||
3. 每条 `js` entry 增加 `labStorageCode`;改接收时间日期时,同 §5.1 规则
|
||
4. entry 切为 `bj` → 隐藏该条编码字段并清空;切回 `js` → 为该条单独 re-allocate
|
||
5. 提交:`js` 携带 `labStorageCode`;`bj` 不传或传空
|
||
|
||
### 5.3 查看详情 / 已接收列表
|
||
|
||
- 详情:`js` 展示「实验室入库编码」;`bj` 不展示该字段(或整块接收信息不含此项)
|
||
- 已接收 Tab:`js` 显示编码;`bj` 显示 `-`
|
||
- 筛选关键词 placeholder 增加「入库编码」(仅匹配有编码的 `js` 记录)
|
||
|
||
---
|
||
|
||
## 6. 并发与异常
|
||
|
||
| 场景 | 处理 |
|
||
|------|------|
|
||
| 两人同时打开弹窗 | allocate 返回不同序号 |
|
||
| 用户改码为已存在编码 | 提交 400:「实验室入库编码已存在」 |
|
||
| 当日序号 > 999 | allocate / 提交返回明确错误 |
|
||
| 批量部分失败 | 已成功记录保留编码;失败项可重试(新 allocate) |
|
||
| 取消弹窗未提交 | 已占序号允许空号,不回滚 sequence |
|
||
| `js` 改 `bj` 再提交 | 已 allocate 的序号允许空号;落库 `lab_storage_code=NULL` |
|
||
|
||
---
|
||
|
||
## 7. 影响文件(实现清单)
|
||
|
||
| 层级 | 文件 |
|
||
|------|------|
|
||
| SQL | `docs/sql/sample_reception_add_lab_storage_code.sql`、`docs/sql/zkxw.sql` |
|
||
| 后端 | `models.py`、`crud/biz_no_generator.py`(或 `sample_reception_lab_code.py`)、`crud/sample_reception.py`、`api/sample_reception.py` |
|
||
| 前端 | `api/sample-reception.ts`、`views/quality-inspection/sample-reception/index.vue` |
|
||
| 关联只读 | 协议查看弹窗中样本接收区块(若展示接收信息) |
|
||
| 校验 | `scripts/check_field_consistency.py` |
|
||
|
||
---
|
||
|
||
## 8. 非目标
|
||
|
||
- 事后修改已保存的入库编码
|
||
- 跨租户共享序号
|
||
- 拒收(`bj`)生成或保存入库编码
|
||
|
||
---
|
||
|
||
## 9. 测试要点
|
||
|
||
1. 同日连续接收 3 条 → `001/002/003`
|
||
2. 接收时间日期为昨天 → 编码日期段为昨天,序号独立计数
|
||
3. 两人并发打开单条接收 → 默认编码不同
|
||
4. 手动改为已存在编码 → 提交失败
|
||
5. 手动改为 `...088` 后,下一条自动分配 > 088
|
||
6. 保存后编辑弹窗 → 编码 disabled
|
||
7. 批量 5 条同日期且均为 `js` → 连续 5 码;混合两个日期 → 各日期独立序号
|
||
8. 单条先选 `js` 占号后改 `bj` 提交 → 无入库编码;库中 `lab_storage_code` 为 NULL
|
||
9. 批量中 3 条 `js` + 2 条 `bj` → 仅 3 条 allocate 并保存编码
|