Files
zkxw/docs/superpowers/specs/2026-06-17-storage-facility-node-code-design.md
T
2026-06-13 19:47:22 +08:00

311 lines
11 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-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`)并开始开发。