This commit is contained in:
zlwu committed 2026-06-16 07:38:27 +08:00
1 parent a1c2511eb7
commit eaf7d490ec
382 files changed
+44146 -34245

No files matched your search

@@ -0,0 +1,12 @@
-- 工艺设备关联:三表新增 equipment_ids(逗号分隔),equipment_name 扩展为多设备名称快照
ALTER TABLE biz_process
ADD COLUMN equipment_ids varchar(1000) DEFAULT NULL COMMENT '关联设备ID(biz_equipment.id,逗号分隔)' AFTER description,
MODIFY COLUMN equipment_name varchar(1000) DEFAULT NULL COMMENT '预计使用设备名称(逗号分隔快照)';
ALTER TABLE biz_process_route_step
ADD COLUMN equipment_ids varchar(1000) DEFAULT NULL COMMENT '关联设备ID(biz_equipment.id,逗号分隔)' AFTER description,
MODIFY COLUMN equipment_name varchar(1000) DEFAULT NULL COMMENT '预计使用设备名称(逗号分隔快照)';
ALTER TABLE biz_production_task_step
ADD COLUMN equipment_ids varchar(1000) DEFAULT NULL COMMENT '关联设备ID(biz_equipment.id,逗号分隔)' AFTER description,
MODIFY COLUMN equipment_name varchar(1000) DEFAULT NULL COMMENT '预计使用设备名称(逗号分隔快照)';
+6 -3
View File
@@ -2659,7 +2659,8 @@ CREATE TABLE `biz_process` (
`category_id` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '工序分类id',
`process_name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '工序名称',
`description` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '描述',
`equipment_name` varchar(200) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '预计使用设备名称',
`equipment_ids` varchar(1000) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '关联设备ID(逗号分隔)',
`equipment_name` varchar(1000) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '预计使用设备名称(逗号分隔快照)',
`estimated_duration` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '预估耗时',
`sop_ids` varchar(1000) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '关联SOP文档id(逗号分隔)',
`tenant_no` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
@@ -2876,7 +2877,8 @@ CREATE TABLE `biz_process_route_step` (
`category_id` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '工序分类id',
`process_name` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '工序名称',
`description` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '描述',
`equipment_name` varchar(200) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '预计使用设备名称',
`equipment_ids` varchar(1000) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '关联设备ID(逗号分隔)',
`equipment_name` varchar(1000) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '预计使用设备名称(逗号分隔快照)',
`estimated_duration` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '预估耗时',
`sop_ids` varchar(1000) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '关联SOP文档id(逗号分隔)',
`tenant_no` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
@@ -3513,7 +3515,8 @@ CREATE TABLE `biz_production_task_step` (
`category_id` varchar(64) DEFAULT NULL COMMENT '工序分类ID',
`process_name` varchar(100) DEFAULT NULL COMMENT '工序名称',
`description` varchar(500) DEFAULT NULL COMMENT '描述',
`equipment_name` varchar(200) DEFAULT NULL COMMENT '设备名称',
`equipment_ids` varchar(1000) DEFAULT NULL COMMENT '关联设备ID(逗号分隔)',
`equipment_name` varchar(1000) DEFAULT NULL COMMENT '设备名称(逗号分隔快照)',
`estimated_duration` varchar(100) DEFAULT NULL COMMENT '预估耗时',
`actual_duration` varchar(100) DEFAULT NULL COMMENT '实际耗时',
`sop_ids` varchar(1000) DEFAULT NULL COMMENT 'SOP文档ID(逗号分隔)',
@@ -0,0 +1,789 @@
# 工艺设备关联设备管理 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 将工艺库、工艺路线步骤、生产任务步骤中的「设备名称」自由文本改为关联 `biz_equipment` 主数据(`equipment_id` + `equipment_name` 快照),三处 UI 改为设备下拉选择,步骤级可覆盖。
**Architecture:** 三张业务表新增 `equipment_id` 字段,保留 `equipment_name` 快照;后端统一 `resolve_equipment_snapshot()` 校验租户并写入名称;复制链路(工艺→路线→任务)同步复制 ID+名称;设备删除前检查工艺/路线引用。前端三页复用 `fetchAllEquipments()` 下拉。
**Tech Stack:** FastAPI + SQLAlchemy + Pydantic v2 (backend) | Vue 3 + Element Plus (web-admin) | MySQL 5.7
**设计规格文档:** `docs/superpowers/specs/2026-06-15-process-equipment-link-design.md`
---
## 文件清单
| 操作 | 文件 |
|------|------|
| 新建 | `docs/sql/2026-06-15-process-equipment-id.sql` |
| 新建 | `backend/scripts/migrate_process_equipment_id.py` |
| 修改 | `docs/sql/zkxw.sql` |
| 修改 | `backend/app/models/models.py` |
| 修改 | `backend/app/crud/equipment.py` |
| 修改 | `backend/app/crud/process.py` |
| 修改 | `backend/app/crud/process_route.py` |
| 修改 | `backend/app/crud/production_task.py` |
| 修改 | `backend/app/schemas/process_route.py` |
| 修改 | `backend/app/schemas/production_task.py` |
| 修改 | `backend/app/api/production.py` |
| 修改 | `web-admin/src/views/production/process/index.vue` |
| 修改 | `web-admin/src/views/production/process/ProcessRouteTab.vue` |
| 修改 | `web-admin/src/views/production/production-work-order/StepDetailPanel.vue` |
| 修改 | `web-admin/src/views/production/production-work-order/useTaskStepEditor.ts` |
| 可选新建 | `web-admin/src/utils/equipment-select.ts`(下拉 label/历史数据提示 helper,三页复用) |
---
### Task 1: SQL 迁移 — 三表新增 equipment_id
**Files:**
- Create: `docs/sql/2026-06-15-process-equipment-id.sql`
- Modify: `docs/sql/zkxw.sql`(三表 CREATE TABLE 段落同步加字段)
- [ ] **Step 1: 创建增量 SQL**
```sql
-- 工艺设备关联:三表新增 equipment_id
ALTER TABLE biz_process
ADD COLUMN equipment_id varchar(64) DEFAULT NULL COMMENT '关联设备ID(biz_equipment.id)' AFTER description;
ALTER TABLE biz_process_route_step
ADD COLUMN equipment_id varchar(64) DEFAULT NULL COMMENT '关联设备ID(biz_equipment.id)' AFTER description;
ALTER TABLE biz_production_task_step
ADD COLUMN equipment_id varchar(64) DEFAULT NULL COMMENT '关联设备ID(biz_equipment.id)' AFTER description;
ALTER TABLE biz_process
ADD KEY idx_process_equipment (equipment_id, del_flag);
```
- [ ] **Step 2: 在开发/测试库执行**
Run(按实际连接串调整):
```bash
mysql -u root -p zkxw < docs/sql/2026-06-15-process-equipment-id.sql
```
Expected: 3 条 `ALTER TABLE` 成功,无报错。
- [ ] **Step 3: 同步 zkxw.sql 基线**
在 `docs/sql/zkxw.sql` 的 `biz_process`、`biz_process_route_step`、`biz_production_task_step` 建表语句中,`description` 后插入同样的 `equipment_id` 列定义;`biz_process` 增加 `idx_process_equipment` 索引。
- [ ] **Step 4: Commit**
```bash
git add docs/sql/2026-06-15-process-equipment-id.sql docs/sql/zkxw.sql
git commit -m "feat: 三表新增 equipment_id 字段以关联设备管理"
```
---
### Task 2: Model — 三实体加 equipment_id
**Files:**
- Modify: `backend/app/models/models.py`
- [ ] **Step 1: BizProcess 增加字段**
在 `BizProcess.description` 之后添加:
```python
equipment_id = Column(String(64), nullable=True, comment="关联设备ID")
```
- [ ] **Step 2: BizProcessRouteStep 增加字段**
在 `BizProcessRouteStep.description` 之后添加同样一行。
- [ ] **Step 3: BizProductionTaskStep 增加字段**
在 `BizProductionTaskStep.description` 之后添加同样一行。
- [ ] **Step 4: 重启后端确认 ORM 无报错**
Run:
```bash
cd backend && source venv/bin/activate && python -c "from app.models.models import BizProcess, BizProcessRouteStep, BizProductionTaskStep; print('ok')"
```
Expected: `ok`
- [ ] **Step 5: Commit**
```bash
git add backend/app/models/models.py
git commit -m "feat: Model 增加 equipment_id 字段"
```
---
### Task 3: 设备快照 Helper + 删除引用检查
**Files:**
- Modify: `backend/app/crud/equipment.py`
- [ ] **Step 1: 添加 resolve_equipment_snapshot**
在文件顶部 import 区追加 `Tuple`(若尚未导入),并在 `get_equipment` 之后添加:
```python
def resolve_equipment_snapshot(
db: Session,
equipment_id: Optional[str],
tenant_no: str,
) -> Tuple[Optional[str], Optional[str]]:
"""校验设备并返回 (equipment_id, equipment_name) 快照。空 ID 则返回 (None, None)。"""
eid = (equipment_id or "").strip()
if not eid:
return None, None
obj = (
db.query(BizEquipment)
.filter(
BizEquipment.id == eid,
BizEquipment.tenant_no == tenant_no,
BizEquipment.del_flag == 0,
)
.first()
)
if not obj:
raise ValueError("所选设备不存在或已删除")
return obj.id, (obj.equipment_name or "").strip() or obj.equipment_code
def equipment_is_referenced(db: Session, equipment_id: str) -> bool:
from app.models.models import BizProcess, BizProcessRouteStep
eid = (equipment_id or "").strip()
if not eid:
return False
proc_hit = (
db.query(BizProcess.id)
.filter(BizProcess.equipment_id == eid, BizProcess.del_flag == 0)
.first()
)
if proc_hit:
return True
step_hit = (
db.query(BizProcessRouteStep.id)
.filter(BizProcessRouteStep.equipment_id == eid, BizProcessRouteStep.del_flag == 0)
.first()
)
return step_hit is not None
```
- [ ] **Step 2: 修改 soft_delete_equipment**
```python
def soft_delete_equipment(db: Session, equipment_id: str) -> None:
obj = get_equipment(db, equipment_id)
if not obj:
raise ValueError("设备不存在")
if equipment_is_referenced(db, equipment_id):
raise ValueError("该设备已被工艺或工艺路线引用,无法删除")
obj.del_flag = 1
obj.update_time = datetime.now()
db.add(obj)
db.commit()
```
- [ ] **Step 3: 手动验证**
Run:
```bash
cd backend && source venv/bin/activate && python -c "
from app.crud.equipment import resolve_equipment_snapshot, equipment_is_referenced
print('import ok')
"
```
Expected: `import ok`
- [ ] **Step 4: Commit**
```bash
git add backend/app/crud/equipment.py
git commit -m "feat: 设备快照解析与删除引用检查"
```
---
### Task 4: 工艺 CRUD — equipment_id 读写
**Files:**
- Modify: `backend/app/crud/process.py`
- [ ] **Step 1: _row_to_dict 增加 equipmentId**
```python
def _row_to_dict(p: BizProcess) -> Dict[str, Any]:
return {
"id": p.id,
"processCode": p.process_code,
"categoryId": p.category_id or "",
"processName": p.process_name,
"description": p.description or "",
"equipmentId": p.equipment_id or "",
"equipmentName": p.equipment_name or "",
# ... 其余不变
}
```
- [ ] **Step 2: create_process 改签名与写入**
函数参数 `equipment_name` 改为 `equipment_id: Optional[str] = None`,并在 `obj = BizProcess(...)` 前调用:
```python
from app.crud.equipment import resolve_equipment_snapshot
eid, ename = resolve_equipment_snapshot(db, equipment_id, tenant_no)
# BizProcess(...) 内:
equipment_id=eid,
equipment_name=ename,
```
- [ ] **Step 3: update_process 同样处理**
当 `equipment_id is not None`(含显式传空字符串清空)时:
```python
if equipment_id is not None:
eid, ename = resolve_equipment_snapshot(db, equipment_id, obj.tenant_no)
obj.equipment_id = eid
obj.equipment_name = ename
```
移除对 `equipment_name` 参数的直接 setattr 逻辑。
- [ ] **Step 4: Commit**
```bash
git add backend/app/crud/process.py
git commit -m "feat: 工艺 CRUD 支持 equipment_id 并写入名称快照"
```
---
### Task 5: 工艺 API Schema + 路由入参
**Files:**
- Modify: `backend/app/api/production.py`
- [ ] **Step 1: ProcessCreateBody / ProcessUpdateBody**
将 `equipment_name` 字段替换为:
```python
equipment_id: Optional[str] = Field(None, alias="equipmentId")
```
Create/Update 路由调用 `create_process` / `update_process` 时传 `equipment_id=body.equipment_id`(Update 从 payload 取 `equipment_id` key)。
- [ ] **Step 2: 确认 create/update 路由**
`create_process` 调用处示例:
```python
process_svc.create_process(
db,
process_code=body.process_code,
process_name=body.process_name,
category_id=body.category_id,
description=body.description,
equipment_id=body.equipment_id,
# ...
)
```
- [ ] **Step 3: 启动后端 smoke test**
Run:
```bash
cd backend && source venv/bin/activate && python -c "from app.api.production import ProcessCreateBody; print(ProcessCreateBody.model_fields.keys())"
```
Expected: 输出含 `equipment_id`,不含可写 `equipment_name`。
- [ ] **Step 4: Commit**
```bash
git add backend/app/api/production.py
git commit -m "feat: 工艺 API 改为接收 equipmentId"
```
---
### Task 6: 工艺路线步骤 — 复制与更新
**Files:**
- Modify: `backend/app/crud/process_route.py`
- Modify: `backend/app/schemas/process_route.py`
- [ ] **Step 1: _step_row_to_dict 增加 equipmentId**
```python
"equipmentId": s.equipment_id or "",
"equipmentName": s.equipment_name or "",
```
- [ ] **Step 2: add_step 复制 equipment_id**
`BizProcessRouteStep(...)` 构造处增加:
```python
equipment_id=proc.equipment_id,
equipment_name=proc.equipment_name,
```
- [ ] **Step 3: copy_step 复制 equipment_id**
```python
equipment_id=src.equipment_id,
equipment_name=src.equipment_name,
```
- [ ] **Step 4: update_step 支持 equipment_id**
在 `update_step` 中,若 `kwargs.get("equipment_id")` 不为 None(用 sentinel 区分未传 vs 清空,或直接检查 key in kwargs):
```python
from app.crud.equipment import resolve_equipment_snapshot
if "equipment_id" in kwargs:
eid, ename = resolve_equipment_snapshot(db, kwargs.get("equipment_id"), step.tenant_no)
step.equipment_id = eid
step.equipment_name = ename
```
从 `for field in [... "equipment_name" ...]` 循环中**移除** `equipment_name`(不再接受纯文本写入)。
- [ ] **Step 5: StepUpdateBody schema**
`backend/app/schemas/process_route.py`:
```python
equipment_id: Optional[str] = Field(None, alias="equipmentId")
# 删除 equipment_name 字段
```
- [ ] **Step 6: production.py 中 step update 路由传 equipment_id**
找到 `update_route_step` / `StepUpdateBody` 使用处,确保 payload 映射 `equipment_id`。
- [ ] **Step 7: Commit**
```bash
git add backend/app/crud/process_route.py backend/app/schemas/process_route.py backend/app/api/production.py
git commit -m "feat: 工艺路线步骤支持 equipmentId 复制与更新"
```
---
### Task 7: 生产任务步骤 — 复制与更新
**Files:**
- Modify: `backend/app/crud/production_task.py`
- Modify: `backend/app/schemas/production_task.py`
- [ ] **Step 1: _step_to_dict 增加 equipmentId**
```python
"equipmentId": s.equipment_id or "",
```
- [ ] **Step 2: _clone_route_to_task 复制 equipment_id**
```python
equipment_id=rs.equipment_id,
equipment_name=rs.equipment_name,
```
- [ ] **Step 3: add_task_step(process_id=...) 复制 equipment_id**
`BizProductionTaskStep(...)` 从工艺复制处增加 `equipment_id=proc.equipment_id`。
- [ ] **Step 4: add_task_step 无 process_id 分支**
改为接受 `equipment_id`,经 `resolve_equipment_snapshot` 写入;移除 `equipment_name` 直写。
- [ ] **Step 5: update_task_step**
```python
if "equipment_id" in data:
eid, ename = resolve_equipment_snapshot(db, data.get("equipment_id"), tenant_no)
step.equipment_id = eid
step.equipment_name = ename
```
从通用 field 循环中移除 `equipment_name`。
- [ ] **Step 6: copy_task_step 复制 equipment_id**
```python
equipment_id=src.equipment_id,
equipment_name=src.equipment_name,
```
- [ ] **Step 7: TaskStepAdd / TaskStepUpdate schema**
```python
# TaskStepAdd
equipment_id: Optional[str] = Field(None, alias="equipmentId")
# TaskStepUpdate
equipment_id: Optional[str] = Field(None, alias="equipmentId")
```
删除两个 schema 中的 `equipment_name` 可写字段。
- [ ] **Step 8: Commit**
```bash
git add backend/app/crud/production_task.py backend/app/schemas/production_task.py backend/app/api/production.py
git commit -m "feat: 生产任务步骤支持 equipmentId 复制与更新"
```
---
### Task 8: 历史数据迁移脚本
**Files:**
- Create: `backend/scripts/migrate_process_equipment_id.py`
- [ ] **Step 1: 编写迁移脚本**
```python
#!/usr/bin/env python3
"""按 equipment_name 精确匹配回填 equipment_id。用法: python scripts/migrate_process_equipment_id.py [--dry-run]"""
import argparse
from sqlalchemy import and_
from app.core.database import SessionLocal
from app.models.models import BizProcess, BizProcessRouteStep, BizProductionTaskStep, BizEquipment
TABLES = [
("biz_process", BizProcess),
("biz_process_route_step", BizProcessRouteStep),
("biz_production_task_step", BizProductionTaskStep),
]
def migrate_table(db, model, dry_run: bool) -> int:
updated = 0
rows = db.query(model).filter(model.del_flag == 0, model.equipment_id.is_(None)).all()
for row in rows:
name = (row.equipment_name or "").strip()
if not name:
continue
eq = (
db.query(BizEquipment)
.filter(
BizEquipment.tenant_no == row.tenant_no,
BizEquipment.del_flag == 0,
BizEquipment.equipment_name == name,
)
.first()
)
if not eq:
continue
if not dry_run:
row.equipment_id = eq.id
updated += 1
return updated
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--dry-run", action="store_true")
args = parser.parse_args()
db = SessionLocal()
try:
for label, model in TABLES:
n = migrate_table(db, model, args.dry_run)
print(f"{label}: matched {n}")
if not args.dry_run:
db.commit()
print("committed")
else:
print("dry-run, no commit")
finally:
db.close()
if __name__ == "__main__":
main()
```
- [ ] **Step 2: dry-run 执行**
Run:
```bash
cd backend && source venv/bin/activate && python scripts/migrate_process_equipment_id.py --dry-run
```
Expected: 打印各表匹配数量,无异常。
- [ ] **Step 3: Commit**
```bash
git add backend/scripts/migrate_process_equipment_id.py
git commit -m "feat: 历史工艺设备名称回填 equipment_id 脚本"
```
---
### Task 9: 前端 — 工艺列表/表单设备下拉
**Files:**
- Modify: `web-admin/src/views/production/process/index.vue`
- 可选: `web-admin/src/utils/equipment-select.ts`
- [ ] **Step 1: 引入 fetchAllEquipments**
```typescript
import { fetchAllEquipments, /* 现有 imports */ } from '@/api/production'
```
- [ ] **Step 2: 增加设备选项与 label helper**
```typescript
const equipmentOptions = ref<any[]>([])
function equipmentOptionLabel(row: { equipmentCode?: string; equipmentName?: string }) {
const code = row.equipmentCode || ''
const name = row.equipmentName || ''
return code && name ? `${code} / ${name}` : name || code || '-'
}
async function loadEquipmentOptions() {
equipmentOptions.value = await fetchAllEquipments()
}
```
在 `loadReferenceData()` 或 `handleAdd`/`handleEdit` 的 `Promise.all` 中加入 `loadEquipmentOptions()`。
- [ ] **Step 3: formData 改 equipmentId**
```typescript
const formData = reactive({
// ...
equipmentId: '',
// 移除 equipmentName 作为表单字段(展示用 detail 快照即可)
})
```
`resetForm`、`handleEdit`、`handleSubmit` 中:
- 读取 `detail.equipmentId`
- 提交 `{ equipmentId: formData.equipmentId || undefined }`,**不再传 equipmentName**
- [ ] **Step 4: 模板替换 el-input → el-select**
```vue
<el-form-item label="设备名称">
<el-select
v-model="formData.equipmentId"
placeholder="请选择设备"
clearable
filterable
style="width: 100%"
>
<el-option
v-for="eq in equipmentOptions"
:key="eq.id"
:label="equipmentOptionLabel(eq)"
:value="eq.id"
:disabled="eq.status === '0'"
/>
</el-select>
<p v-if="!formData.equipmentId && formDataLegacyEquipmentName" class="form-tip">
历史文本:{{ formDataLegacyEquipmentName }},请重新选择设备
</p>
</el-form-item>
```
列表列仍展示 `equipmentName`(快照),无需改 prop。
- [ ] **Step 5: 本地 dev 冒烟**
Run: `cd web-admin && pnpm dev`,打开 `/#/production/process`(或实际菜单路径),新增/编辑工艺选设备并保存。
- [ ] **Step 6: Commit**
```bash
git add web-admin/src/views/production/process/index.vue
git commit -m "feat: 工艺表单设备改为下拉关联设备管理"
```
---
### Task 10: 前端 — 工艺路线步骤设备下拉
**Files:**
- Modify: `web-admin/src/views/production/process/ProcessRouteTab.vue`
- [ ] **Step 1: 加载 equipmentOptions(同 Task 9)**
在 `loadReferenceData` 或组件 mount 时调用 `fetchAllEquipments()`。
- [ ] **Step 2: 模板替换设备输入框**
将 `selectedStep.equipmentName` 的 `el-input` 改为 `el-select`,`v-model="selectedStep.equipmentId"`。
- [ ] **Step 3: saveStepBasic 改 payload**
```typescript
async function saveStepBasic() {
// ...
await fetchUpdateRouteStep({
stepId: selectedStep.value.id,
// ...
equipmentId: selectedStep.value.equipmentId || null
})
// 本地同步展示名(可选,便于 UI 即时反馈)
const eq = equipmentOptions.value.find((e) => e.id === selectedStep.value.equipmentId)
if (eq) selectedStep.value.equipmentName = eq.equipmentName
}
```
- [ ] **Step 4: 历史数据提示**
当 `!selectedStep.equipmentId && selectedStep.equipmentName` 时显示「历史文本:xxx」。
- [ ] **Step 5: Commit**
```bash
git add web-admin/src/views/production/process/ProcessRouteTab.vue
git commit -m "feat: 工艺路线步骤设备改为下拉选择"
```
---
### Task 11: 前端 — 生产任务步骤设备下拉
**Files:**
- Modify: `web-admin/src/views/production/production-work-order/StepDetailPanel.vue`
- Modify: `web-admin/src/views/production/production-work-order/useTaskStepEditor.ts`
- [ ] **Step 1: useTaskStepEditor 加载 equipmentOptions**
在 editor 初始化或 `loadTaskDetail` 后加载 `fetchAllEquipments()`,export `equipmentOptions` 与 `equipmentOptionLabel`。
- [ ] **Step 2: saveStepBasic 改 payload**
```typescript
await fetchUpdateTaskStep(taskId.value, selectedStep.value.id, {
process_name: selectedStep.value.processName,
category_id: selectedStep.value.categoryId,
description: selectedStep.value.description,
equipment_id: selectedStep.value.equipmentId || null,
estimated_duration: selectedStep.value.estimatedDuration,
sop_ids: selectedStep.value.sopIds
}, operateScope.value)
```
- [ ] **Step 3: StepDetailPanel.vue 模板**
```vue
<el-form-item label="设备名称">
<el-select
v-model="editor.selectedStep.value.equipmentId"
placeholder="请选择设备"
clearable
filterable
:disabled="readonly"
style="width: 100%"
@change="editor.saveStepBasic()"
>
<el-option
v-for="eq in editor.equipmentOptions.value"
:key="eq.id"
:label="editor.equipmentOptionLabel(eq)"
:value="eq.id"
:disabled="eq.status === '0'"
/>
</el-select>
</el-form-item>
```
- [ ] **Step 4: Commit**
```bash
git add web-admin/src/views/production/production-work-order/StepDetailPanel.vue \
web-admin/src/views/production/production-work-order/useTaskStepEditor.ts
git commit -m "feat: 生产任务步骤设备改为下拉选择"
```
---
### Task 12: 联调验证 + 字段一致性 + 知识图谱
**Files:**
- 无新文件
- [ ] **Step 1: 字段一致性检查**
Run:
```bash
cd backend && source venv/bin/activate && python scripts/check_field_consistency.py
```
Expected: process 相关无新增不一致告警(若有,按 `_row_to_dict` 补全 Create/Update schema 与前端 formData)。
- [ ] **Step 2: 端到端手工测试清单**
| # | 操作 | 预期 |
|---|------|------|
| 1 | 设备管理新建设备 A | 成功 |
| 2 | 工艺库新建工艺,选设备 A | 列表显示 A 名称;DB 有 equipment_id |
| 3 | 工艺路线拖入该工艺 | 步骤 equipmentId = A |
| 4 | 路线步骤改选设备 B | 仅步骤变更,工艺仍为 A |
| 5 | 生产任务选该路线 | 任务步骤 equipmentId = B |
| 6 | 任务步骤改选设备 C | 保存成功 |
| 7 | 删除设备 A(工艺仍引用) | 报错「已被引用」 |
| 8 | 清空工艺设备 | equipment_id/name 均为 NULL |
- [ ] **Step 3: 更新知识图谱**
Run:
```bash
./scripts/update-graph.sh all
```
- [ ] **Step 4: 更新设计文档状态**
将 `docs/superpowers/specs/2026-06-15-process-equipment-link-design.md` 头部 `状态: 待确认` 改为 `状态: 已实现`。
- [ ] **Step 5: Commit**
```bash
git add docs/superpowers/specs/2026-06-15-process-equipment-link-design.md backend/graphify-out web-admin/graphify-out
git commit -m "docs: 工艺设备关联功能完成并更新设计状态"
```
---
## Spec 覆盖自检
| Spec 章节 | 对应 Task |
|-----------|-----------|
| 2 需求共识(步骤级覆盖) | Task 6–7, 10–11 |
| 3 数据流复制点 | Task 6–7 |
| 4 数据库变更 | Task 1–2 |
| 4.4 历史迁移 | Task 8 |
| 5.2 CRUD helper | Task 3 |
| 5.3 API Schema | Task 5–7 |
| 5.4 复制逻辑 | Task 6–7 |
| 6 前端三页 | Task 9–11 |
| 7 设备删除约束 | Task 3 |
| 8 测试要点 | Task 12 |
无遗漏项。
---
## 执行选项
Plan complete and saved to `docs/superpowers/plans/2026-06-15-process-equipment-link.md`.
**两种执行方式:**
1. **Subagent-Driven(推荐)** — 按 Task 1→12 逐任务派发子 agent,每任务完成后 review
2. **Inline Execution** — 在本会话中按 Task 批量实现,每 2–3 个 Task 设检查点
你希望用哪种方式开始实现?
@@ -0,0 +1,229 @@
# 工艺设备关联设备管理 设计文档
> 日期: 2026-06-15
> 状态: 已实现
> 关联模块: 设备管理、工艺库管理、工艺路线管理、生产工单/生产任务
---
## 1. 业务背景
【生产基础信息】→【设备管理】维护租户内设备主数据(`biz_equipment`)。
【工艺库管理】→【工艺列表】中「设备名称」目前为自由文本(`equipment_name`),与设备主数据无关联,存在录入不一致、无法校验、无法追溯等问题。
工艺还会经【工艺路线】组合,并在【生产工单】→【生产任务】中作为步骤快照执行。设备字段在三层均采用**快照复制**模式(与工序名称等字段一致),改造需兼顾主数据关联与历史快照完整性。
---
## 2. 需求共识
| # | 决策 | 说明 |
|---|------|------|
| 1 | **步骤级可覆盖(选项 A)** | 工艺路线步骤、生产任务步骤的设备**必须从设备管理选择**,但允许与工艺主数据的默认设备不同 |
| 2 | **多设备逗号存储** | 三表新增 `equipment_ids`(逗号分隔 ID),`equipment_name` 扩展为逗号分隔名称快照(varchar 1000) |
| 3 | **保留名称快照** | `equipment_name` 保存当时设备名称列表(逗号分隔),列表/导出直接展示 |
| 4 | **不自动回写下游** | 修改工艺主数据的设备后,**不**自动更新已存在的路线步骤或生产任务步骤(与现有 `process_name` 等行为一致) |
| 5 | **设备删除约束** | 被工艺/路线步骤引用的设备**禁止软删除**;停用(status=0)允许,下拉中过滤或标记异常 |
| 6 | **UI 控件** | 三处编辑界面:`el-input` 改为 `el-select`(filterable),数据源 `fetchAllEquipments()`,展示「设备编码 / 设备名称」 |
---
## 3. 数据流
```
biz_equipment(设备主数据)
↑ equipment_id 引用
biz_process(工艺默认设备)
│ add_step / 拖入克隆:复制 equipment_id + equipment_name
↓
biz_process_route_step(路线步骤,可覆盖设备)
│ _clone_route_to_task:复制 equipment_id + equipment_name
↓
biz_production_task_step(任务步骤,可覆盖设备)
```
**复制触发点(均需同步 `equipment_id` + `equipment_name`):**
| 触发场景 | 后端函数 |
|----------|----------|
| 工艺拖入路线 | `process_route.add_step()` |
| 路线步骤复制 | `process_route.copy_step()` |
| 路线克隆新版本 | `process_route` 版本复制逻辑(如有) |
| 任务选路线派工 | `production_task._clone_route_to_task()` |
| 任务手动加工艺步骤 | `production_task.add_task_step(process_id=...)` |
| 任务步骤复制 | `production_task.copy_task_step()` |
---
## 4. 数据库变更
### 4.1 变更表(3 张)
在以下表**业务字段区**(`description` 之后或同类位置)新增:
| 表 | 新增字段 | 类型 | 说明 |
|----|----------|------|------|
| `biz_process` | `equipment_id` | VARCHAR(64) DEFAULT NULL | 关联 `biz_equipment.id` |
| `biz_process_route_step` | `equipment_id` | VARCHAR(64) DEFAULT NULL | 步骤级设备,可覆盖工艺默认 |
| `biz_production_task_step` | `equipment_id` | VARCHAR(64) DEFAULT NULL | 任务步骤级设备,可覆盖路线/工艺 |
**保留现有字段:** 三表 `equipment_name` varchar(200) **不删除**,作为快照冗余。
**索引(可选,建议):**
- `idx_process_equipment(equipment_id, del_flag)` — 用于删除引用检查
- 路线/任务步骤表可不建 FK 索引(引用检查走工艺主表即可)
### 4.2 不变表
- `biz_equipment` — 结构不变,增加被引用校验逻辑
- `biz_process_route`、`biz_production_task` 及所有子表(试剂/耗材/产出/采集)— 不涉及设备
### 4.3 迁移 SQL 示例
```sql
ALTER TABLE biz_process
ADD COLUMN equipment_id varchar(64) DEFAULT NULL COMMENT '关联设备ID(biz_equipment.id)' AFTER description;
ALTER TABLE biz_process_route_step
ADD COLUMN equipment_id varchar(64) DEFAULT NULL COMMENT '关联设备ID(biz_equipment.id)' AFTER description;
ALTER TABLE biz_production_task_step
ADD COLUMN equipment_id varchar(64) DEFAULT NULL COMMENT '关联设备ID(biz_equipment.id)' AFTER description;
```
### 4.4 历史数据迁移
1. 对三表现有 `equipment_name` 非空记录,按 `tenant_no` + 精确/trim 后匹配 `biz_equipment.equipment_name` 回填 `equipment_id`
2. 无法匹配的记录:`equipment_id` 留 NULL,`equipment_name` 保留原值;列表/表单展示时提示「未关联设备(历史数据)」
3. 匹配脚本放 `backend/scripts/migrate_process_equipment_id.py`,可 `--dry-run`
---
## 5. 后端设计
### 5.1 Model(`backend/app/models/models.py`)
三个 Model 各增加:
```python
equipment_id = Column(String(64), nullable=True, comment="关联设备ID")
```
### 5.2 CRUD 约定
**写入规则(统一 helper,建议 `resolve_equipment_snapshot(db, equipment_id, tenant_no) -> (id, name)`):**
- 传入 `equipment_id` 时:校验设备存在、未删除、同租户;写入 `equipment_id` 并将 `equipment_name` 设为当前 `biz_equipment.equipment_name`
- 清空设备:`equipment_id = None`,`equipment_name = None`
- **禁止**仅传 `equipment_name` 字符串而不传 ID(API 层废弃纯文本写入,兼容读取旧数据)
**`_row_to_dict` 扩展字段:**
- `equipmentId`
- `equipmentName`(快照名称,已有)
- 列表/详情可选 enrich:`equipmentCode`、`equipmentStatus`(join 设备表,仅展示)
**设备删除(`equipment.py` `soft_delete_equipment`):**
- 删除前查询 `biz_process`、`biz_process_route_step` 是否存在 `equipment_id = ? AND del_flag = 0`
- 存在则 `raise ValueError("该设备已被工艺或工艺路线引用,无法删除")`
- 生产任务步骤**不参与**删除拦截(任务为历史快照,允许设备从主数据删除后任务仍显示快照名称;若需严格可扩展检查)
### 5.3 API Schema(`backend/app/api/production.py` 等)
**ProcessCreateBody / ProcessUpdateBody:**
```python
equipment_id: Optional[str] = Field(None, alias="equipmentId")
# equipment_name 只读出参,Create/Update 不再接受客户端直传 equipmentName
```
**ProcessRouteStepUpdateBody、ProductionTaskStepUpdateBody:**
- 同上,接受 `equipmentId`,服务端解析名称快照
**响应:** 继续返回 `equipmentId` + `equipmentName`;详情可增加 `equipmentCode` 便于展示。
### 5.4 复制逻辑改动清单
| 文件 | 函数 | 改动 |
|------|------|------|
| `crud/process_route.py` | `add_step` | 复制 `proc.equipment_id` + `proc.equipment_name` |
| `crud/process_route.py` | `copy_step` | 复制 `src.equipment_id` + `src.equipment_name` |
| `crud/process_route.py` | `update_step` | 支持 `equipment_id` 参数,经 helper 写快照 |
| `crud/production_task.py` | `_clone_route_to_task` | 复制 `rs.equipment_id` + `rs.equipment_name` |
| `crud/production_task.py` | `add_task_step` | 从工艺复制 `equipment_id` + `equipment_name` |
| `crud/production_task.py` | `update_task_step` | 支持 `equipment_id` |
| `crud/production_task.py` | `copy_task_step` | 复制 `src.equipment_id` + `src.equipment_name` |
---
## 6. 前端设计
### 6.1 改动页面
| 页面 | 文件 | 改动 |
|------|------|------|
| 工艺列表/表单 | `views/production/process/index.vue` | 设备名称 → `el-select`,绑定 `formData.equipmentId`;列表列展示 `equipmentName`(或编码/名称) |
| 工艺路线步骤 | `views/production/process/ProcessRouteTab.vue` | 步骤基本信息区同样改为设备下拉,保存传 `equipmentId` |
| 生产任务步骤 | `views/production/production-work-order/StepDetailPanel.vue` | 同上 |
| 任务步骤编辑器 | `useTaskStepEditor.ts` | 保存 payload 改为 `equipmentId`;选中时本地同步 `equipmentName` 展示 |
### 6.2 下拉数据
- 使用已有 `fetchAllEquipments()`(`/web/admin/equipments/all`)
- Option label:`{equipmentCode} / {equipmentName}`
- 过滤:`status === '1'` 正常设备可选;`status === '0'` 可显示但标记「异常」
- 历史数据 `equipmentId` 为空但 `equipmentName` 有值:select 显示 placeholder「历史文本:xxx」,引导用户重新选择
### 6.3 UI 规范
- 遵循 Fresh Mint 规范;`label-position="top"`
- 与「工序分类」下拉交互一致(filterable + clearable)
- 字段非必填(与现况一致)
---
## 7. 业务规则摘要
| 场景 | 行为 |
|------|------|
| 新建工艺并选设备 | 写 `equipment_id`,冗余 `equipment_name` |
| 工艺改设备 | 仅更新 `biz_process`;已有路线/任务步骤不变 |
| 路线步骤改设备 | 仅更新该步骤;允许与来源工艺不同 |
| 任务步骤改设备 | 仅更新该步骤;执行中可改(与现有步骤编辑一致) |
| 设备改名 | 已存快照名称不变;新保存/新复制使用新名称 |
| 设备停用 | 下拉不可选或弱提示;已关联记录仍显示快照 |
| 设备删除 | 被工艺或路线步骤引用时拒绝删除 |
---
## 8. 测试要点
1. 工艺 CRUD:选设备、清空设备、列表/详情展示
2. 拖入路线:步骤继承工艺设备;步骤级改设备后与原工艺不同
3. 生产任务:选路线克隆、手动加工艺步骤,设备正确复制;步骤级覆盖
4. 设备删除:被工艺引用时拦截;未被引用可删
5. 历史数据:仅有 `equipment_name` 无 ID 时页面不报错,可重新关联
6. 字段一致性:运行 `python scripts/check_field_consistency.py`
---
## 9. 不在本次范围
- 设备与 SOP、环境检测等业务联动
- 生产任务按设备排产/冲突检测
- 小程序端设备展示
- 自动同步工艺设备变更到已有路线/任务
---
## 10. 实现顺序建议
1. SQL + Model + 迁移脚本
2. 后端 helper + process CRUD + API
3. process_route / production_task 复制与更新逻辑
4. 设备删除引用检查
5. 前端三处下拉改造
6. 历史数据迁移 + 联调
@@ -0,0 +1,41 @@
# 入库放行查看 · 生产过程质检展示
**日期:** 2026-06-15
**状态:** 已实现
## 背景
入库放行查看弹窗原先仅展示 `trigger_source=reception` 的样本接收质检(门禁)。生产过程派发的入库质检在同一样本下不可见,审核员无法在同一上下文查阅过程质检结果。
## 目标
- 查看弹窗同时展示:**样本接收质检(门禁)** + **生产过程质检(过程参考)**
- **不改变**放行审核/入库判定:过程质检不参与门禁
## API
`GET /api/admin/storage-release-reviews/{id}` 详情响应:
| 字段 | 说明 |
|------|------|
| `qcInfo` | 不变,仅 reception 门禁质检 |
| `processQcInfoList` | 新增,同 `sample_code` 下全部 `trigger_source=production` 的 inbound 质检工单(含任务、细胞产品、生产工单号) |
后端实现:`qc_work_order.list_process_qc_info_for_sample`;放行 CRUD 调用 `_build_gate_qc_info` + `_build_process_qc_info_list`。
## 前端
- 新建 `SampleQcInfoGroupedPanel`:门禁区块 + 过程区块(空态 / 堆叠卡片)
- `StorageReleaseViewDialog` 质检信息 slot 改用分组面板(`:show-section-label="false"`,外层已有 section 标题)
- `ProtocolViewDialog`、`DonationViewDialog` 样本卡内改用分组面板(默认展示「质检信息」标题)
- `SampleQcInfoCards` 增加 `embedded` 模式供内嵌复用
## 协议 / 捐赠详情 API
每个样本项新增 `processQcInfoList`;`qcInfo` 固定为 reception 门禁质检(`get_qc_info_map_for_samples` 已加 `trigger_source=reception` 过滤)。
## 验收
1. 仅接收质检:门禁区块正常,过程区块显示「暂无生产过程质检工单」
2. 有过程质检:展示全部 production 工单及任务,任务可打开详情
3. `qcInfo` 语义不变,审核/入库接口行为不变