311 lines
11 KiB
Markdown
311 lines
11 KiB
Markdown
# 库位层级编号与储物格完整编号设计文档
|
||
|
||
> 日期: 2026-06-17
|
||
> 状态: 已确认
|
||
> 范围: 后端 + web-admin + SQL 迁移
|
||
> 关联模块: 库位管理、细胞产品库、生产任务、出库放行
|
||
|
||
## 1. 背景
|
||
|
||
### 1.1 现状
|
||
|
||
库位管理由两张表组成(见 `docs/superpowers/specs/2026-05-22-storage-slot-occupancy-design.md`):
|
||
|
||
- `biz_storage_facility`:树形结构(液氮罐 → 层 → 盒),字段含 `node_name`、`node_type`、`is_leaf`,**无编号字段**
|
||
- `biz_storage_facility_slot`:叶子节点下的储物格,`slot_code` 仅存盒内坐标(如 `1-3`)
|
||
|
||
业务展示时,储物格位置拼接为 **名称路径 + 本地编码**:
|
||
|
||
```
|
||
液氮罐A / 第1层 / 盒01 / 1-3
|
||
```
|
||
|
||
名称路径用 ` / ` 分隔(`buildTreePathLabelFromFlat` / `_build_facility_path_label`)。
|
||
|
||
### 1.2 客户新需求
|
||
|
||
- 树形库位 **每一层** 都有独立 **编号**
|
||
- 储物格 **完整编号** = 从根到叶所有层级编号 + 储物格本地编号,段间用 `-` 连接
|
||
- 示例:罐 `1` → 层 `2` → 盒 `03` → 格 `1-3` → 完整编号 **`1-2-03-1-3`**
|
||
|
||
### 1.3 需求确认
|
||
|
||
| 维度 | 决策 |
|
||
|---|---|
|
||
| 节点编号格式 | **单段**,不含 `-`(字母或数字,如 `1`、`03`、`A`) |
|
||
| 格子本地编码 | 保持 **`行-列`**(如 `1-3`) |
|
||
| 完整编号生成 | **动态计算**(节点存 `node_code`,格子存本地 `slot_code`) |
|
||
| 编号修改策略 | **允许修改**;修改后 **批量刷新关联快照** |
|
||
| 名称 vs 编号 | **分离**:`node_name` 继续用于名称路径;`node_code` 用于编号路径 |
|
||
|
||
## 2. 方案选型
|
||
|
||
| 方案 | 描述 | 结论 |
|
||
|---|---|---|
|
||
| **A(采用)** | 节点新增 `node_code`;`slot_code` 存本地坐标;API 返回 `fullSlotCode` | ✅ 推荐 |
|
||
| B | `slot_code` 直接存完整编号 | ❌ 父级改编号需批量改格子;字段冗余 |
|
||
| C | 双字段 `slot_code` + `full_slot_code` 冗余存储 | ❌ 同步成本高 |
|
||
|
||
### 2.1 编号修改策略选型
|
||
|
||
| 方案 | 描述 | 结论 |
|
||
|---|---|---|
|
||
| A | 允许改,快照不更新 | ❌ |
|
||
| B | 有子节点/格子后禁止改 | ❌ |
|
||
| **C(采用)** | 允许改,并批量更新关联快照 | ✅ |
|
||
|
||
## 3. 数据模型
|
||
|
||
### 3.1 `biz_storage_facility` 新增字段
|
||
|
||
```sql
|
||
ALTER TABLE biz_storage_facility
|
||
ADD COLUMN node_code VARCHAR(32) NOT NULL DEFAULT '' COMMENT '节点编号(单段,不含连字符)'
|
||
AFTER node_name;
|
||
```
|
||
|
||
**约束与校验(应用层 + 可选 DB):**
|
||
|
||
| 规则 | 说明 |
|
||
|---|---|
|
||
| 格式 | `/^[A-Za-z0-9]+$/`,**禁止 `-`** |
|
||
| 兄弟唯一 | 同一 `parent_id` + `tenant_no` 下 `node_code` 不可重复 |
|
||
| 必填 | 新建节点必填;迁移后存量须补全 |
|
||
| 根节点 | 同样必须有 `node_code` |
|
||
|
||
**索引建议:**
|
||
|
||
```sql
|
||
CREATE INDEX idx_sf_parent_code ON biz_storage_facility (parent_id, tenant_no, node_code, del_flag);
|
||
```
|
||
|
||
### 3.2 `biz_storage_facility_slot` 不变
|
||
|
||
- `slot_code` 仍存盒内本地坐标(`{row}-{col}`)
|
||
- 不新增 `full_slot_code` 列
|
||
|
||
### 3.3 完整编号计算规则
|
||
|
||
```python
|
||
def build_node_code_path(node_id: str, facility_by_id: dict) -> str:
|
||
"""从根到当前节点的 node_code,用 '-' 连接。例:'1-2-03'"""
|
||
|
||
def build_full_slot_code(facility_node_id: str, slot_code: str, facility_by_id: dict) -> str:
|
||
"""例:build_node_code_path(...) + '-' + slot_code → '1-2-03-1-3'"""
|
||
```
|
||
|
||
- `facility_node_id`:格子所属叶子节点 ID
|
||
- `slot_code`:本地 `行-列`
|
||
- 若 `slot_code` 为空,仅返回节点路径编号(一般不应出现在叶子格子上)
|
||
|
||
**租户内完整编号唯一性:** 由树结构 + 兄弟唯一 `node_code` + 叶子内 `slot_code` 唯一共同保证;叶子内 `slot_code` 在同一 `facility_node_id` 下应已有业务约束(批量生成不重复)。
|
||
|
||
### 3.4 字段长度
|
||
|
||
完整编号示例:`1-2-03-1-3`(约 10 字符)。深层树或长编号预估 ≤ 64 字符,当前 `slot_code VARCHAR(64)` 足够存储本地坐标;`fullSlotCode` 仅 API 返回,不入库。
|
||
|
||
## 4. 编号变更级联(策略 C)
|
||
|
||
### 4.1 触发时机
|
||
|
||
`update_facility` 当 `node_code` 发生变化时,在同一事务内执行级联刷新。
|
||
|
||
### 4.2 影响范围计算
|
||
|
||
1. 收集被修改节点及其 **所有后代** 节点 ID
|
||
2. 找出这些节点下所有 **叶子节点**(`is_leaf=1`)
|
||
3. 查出这些叶子下全部未删除的 `biz_storage_facility_slot`
|
||
4. 对每个 slot 重新计算 `fullSlotCode`
|
||
|
||
### 4.3 快照表刷新
|
||
|
||
当前持久化 `storage_slot_name` 的表:
|
||
|
||
| 表 | 字段 | 刷新方式 |
|
||
|---|---|---|
|
||
| `biz_outbound_release_cell` | `storage_slot_name` | 按 `cell_product_id` → `biz_cell_product.storage_slot_id` 匹配受影响 slot,更新为新 `fullSlotCode` |
|
||
|
||
**运行时计算、无 DB 快照的模块**(改编号后自动生效,无需刷表):
|
||
|
||
- `biz_cell_product` 列表/详情中的 `storageSlotName`(`_enrich_product_dicts` 动态拼接)
|
||
- 生产任务产出/原材料展示(join 细胞产品 enrich)
|
||
- 库位占用弹窗标题
|
||
- 储物格下拉选项(`storage-slot-options.ts`)
|
||
|
||
### 4.4 级联服务伪代码
|
||
|
||
```python
|
||
def cascade_refresh_slot_name_snapshots(db, *, changed_node_ids: list[str], tenant_no: str):
|
||
affected_slot_ids = collect_slot_ids_under_nodes(db, changed_node_ids, tenant_no)
|
||
if not affected_slot_ids:
|
||
return 0
|
||
facility_by_id = load_all_facilities_map(db, tenant_no)
|
||
slot_full_map = {
|
||
sid: build_full_slot_code(slot.facility_node_id, slot.slot_code, facility_by_id)
|
||
for sid, slot in load_slots_by_ids(db, affected_slot_ids)
|
||
}
|
||
# 刷新 outbound_release_cell 快照
|
||
updated = refresh_outbound_release_cell_slot_names(db, slot_full_map, tenant_no)
|
||
return updated
|
||
```
|
||
|
||
### 4.5 格子本地编码变更
|
||
|
||
`update_slot` 当 `slot_code` 变化时,同样触发上述快照刷新(仅该 slot ID 范围)。
|
||
|
||
## 5. 后端 API
|
||
|
||
### 5.1 设施节点
|
||
|
||
| 方法 | 路径 | 变更 |
|
||
|---|---|---|
|
||
| GET | `/storage-facilities` | 返回增加 `nodeCode` |
|
||
| POST | `/storage-facilities` | 请求体增加 `nodeCode`(必填) |
|
||
| PUT | `/storage-facilities/{id}` | 可改 `nodeCode`;变更时级联刷新快照 |
|
||
|
||
**CreateBody / UpdateBody 新增:**
|
||
|
||
```python
|
||
node_code: str = Field(..., alias="nodeCode", min_length=1, max_length=32)
|
||
```
|
||
|
||
**校验错误文案:**
|
||
|
||
- 格式非法:`节点编号只能包含字母和数字,不能包含连字符`
|
||
- 兄弟重复:`同级节点编号已存在`
|
||
|
||
### 5.2 储物格
|
||
|
||
| 方法 | 路径 | 变更 |
|
||
|---|---|---|
|
||
| GET | `/storage-facility-slots` | 每条增加 `fullSlotCode` |
|
||
| POST | `/storage-facility-slots` | 响应含 `fullSlotCode`;`slot_code` 仍为本地坐标 |
|
||
| PUT | `/storage-facility-slots/{id}` | 同上;改 `slot_code` 时刷新快照 |
|
||
| POST | `/storage-facility-slots/batch` | **移除 `codePrefix`**;本地编码 `{row}-{col}` |
|
||
|
||
### 5.3 统一展示字段
|
||
|
||
`_slot_to_dict` 增加:
|
||
|
||
```python
|
||
"fullSlotCode": build_full_slot_code(...)
|
||
```
|
||
|
||
`_enrich_product_dicts` 中 `storageSlotName` 由:
|
||
|
||
```
|
||
{node_name_path} / {slot_code}
|
||
```
|
||
|
||
改为:
|
||
|
||
```
|
||
{fullSlotCode}
|
||
```
|
||
|
||
可选:保留 `storageSlotPath`(名称路径)供 tooltip,非必须。
|
||
|
||
## 6. 前端(web-admin)
|
||
|
||
### 6.1 库位管理页
|
||
|
||
**文件:** `web-admin/src/views/operations/storage-facility/index.vue`
|
||
|
||
| 区域 | 变更 |
|
||
|---|---|
|
||
| 节点弹窗 | 新增「节点编号」表单项(必填);hint:单段字母/数字,不含 `-` |
|
||
| 左侧树 | 展示 `[nodeCode] nodeName` 或 `nodeCode · nodeName` |
|
||
| 格子卡片 | 主文案显示 `fullSlotCode`(替代当前列序号 `cellIndex+1`) |
|
||
| 批量生成 | 移除「编码前缀」;预览示例完整编号 |
|
||
| 格子编辑/占用弹窗 | 展示完整编号;本地坐标可编辑时仍编辑 `slotCode`(`行-列`) |
|
||
|
||
**货架行分组:** 继续使用本地 `slotCode` 调用 `parseSlotCode`,**不要**对 `fullSlotCode` 做行/列解析。
|
||
|
||
### 6.2 储物格下拉
|
||
|
||
**文件:** `web-admin/src/utils/storage-slot-options.ts`
|
||
|
||
- `mapSlotsToSelectOptions`:`name` 改为 `fullSlotCode`
|
||
- 可选副标题:`nodePath` 作 title/tooltip
|
||
|
||
### 6.3 关联页面(只读展示同步)
|
||
|
||
以下页面无需改交互,随 API `storageSlotName` / 下拉 `name` 变更自动显示新格式:
|
||
|
||
- 细胞产品库 `cell-product/index.vue`
|
||
- 生产任务产出/原材料相关组件
|
||
- 出库放行物料选择
|
||
|
||
## 7. 历史数据迁移
|
||
|
||
**文件:** `docs/sql/migrations/2026-06-17-storage-facility-node-code.sql`
|
||
|
||
```sql
|
||
-- 1. 加字段(允许 DEFAULT '' 过渡)
|
||
ALTER TABLE biz_storage_facility
|
||
ADD COLUMN node_code VARCHAR(32) NOT NULL DEFAULT '' COMMENT '节点编号(单段,不含连字符)'
|
||
AFTER node_name;
|
||
|
||
-- 2. 存量补编号(需按租户人工确认后执行,示例按 order_num)
|
||
-- UPDATE biz_storage_facility SET node_code = LPAD(CAST(order_num + 1 AS CHAR), 2, '0') WHERE node_code = '' AND del_flag = 0;
|
||
|
||
-- 3. 补编号完成后,业务侧应禁止 node_code 为空的新增/编辑
|
||
|
||
-- 4. 刷新 outbound_release_cell 快照(部署后由后端脚本或一次性 SQL 执行,依赖新编号已补全)
|
||
```
|
||
|
||
**迁移注意:**
|
||
|
||
- 存量 `slot_code` 无需修改
|
||
- 补全 `node_code` 后,运行一次性脚本刷新 `biz_outbound_release_cell.storage_slot_name`
|
||
- 若某节点无法自动补编号,库位管理页编辑补录后再统一刷新
|
||
|
||
## 8. 校验与测试
|
||
|
||
### 8.1 单元/集成测试点
|
||
|
||
| # | 场景 | 期望 |
|
||
|---|---|---|
|
||
| 1 | 新建节点缺 `nodeCode` | 400 |
|
||
| 2 | `nodeCode` 含 `-` | 400 |
|
||
| 3 | 同级重复 `nodeCode` | 400 |
|
||
| 4 | 批量生成 4×6 | `fullSlotCode` 为 `{祖先}-{row}-{col}` |
|
||
| 5 | 修改层编号 `2→5` | 其下所有 slot 的 `fullSlotCode` 前缀变化 |
|
||
| 6 | 修改层编号后 | `biz_outbound_release_cell.storage_slot_name` 同步更新 |
|
||
| 7 | 修改格子 `slot_code` | 关联 outbound 快照更新 |
|
||
| 8 | 细胞产品列表 | `storageSlotName` 显示 `fullSlotCode` |
|
||
| 9 | 下拉选格 | 选项文案为 `fullSlotCode` |
|
||
|
||
### 8.2 回归页面
|
||
|
||
- `/#/operations/storage-facility` — 树、节点 CRUD、批量生成、格子展示
|
||
- 细胞产品库 — 储物格列、选格下拉
|
||
- 生产任务执行 — 产出选格
|
||
- 出库放行 — 储物格快照列
|
||
|
||
## 9. 不在范围
|
||
|
||
- 小程序端库位展示(若未来有)
|
||
- 生产物料库 `storage_location` 纯文本字段(不关联库位主数据)
|
||
- 物理标签重打流程(系统只保证数据一致,线下标签由业务自行处理)
|
||
- 将名称路径改为 `-` 分隔(名称路径仍用 ` / `)
|
||
|
||
## 10. 实施文件清单
|
||
|
||
| 层级 | 文件 | 操作 |
|
||
|---|---|---|
|
||
| SQL | `docs/sql/migrations/2026-06-17-storage-facility-node-code.sql` | 新增 |
|
||
| Model | `backend/app/models/models.py` | 修改 |
|
||
| Service | `backend/app/services/storage_facility_code.py`(或 crud 内) | 新增 |
|
||
| CRUD | `backend/app/crud/storage_facility.py` | 修改 |
|
||
| CRUD | `backend/app/crud/cell_product.py` | 修改 `_enrich` 拼接 |
|
||
| CRUD | `backend/app/crud/outbound_release_material.py` | 可选:抽取共用刷新 |
|
||
| API | `backend/app/api/operations.py` | 修改 schema |
|
||
| 前端 | `web-admin/src/views/operations/storage-facility/index.vue` | 修改 |
|
||
| 前端 | `web-admin/src/utils/storage-slot-options.ts` | 修改 |
|
||
| 前端 | `web-admin/src/api/admin.ts` | 类型字段补充 |
|
||
|
||
---
|
||
|
||
**评审确认项:** 若本设计无异议,下一步编写实施计划(`docs/superpowers/plans/2026-06-17-storage-facility-node-code.md`)并开始开发。
|