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,三页复用) |
Files:
docs/sql/2026-06-15-process-equipment-id.sqlModify: docs/sql/zkxw.sql(三表 CREATE TABLE 段落同步加字段)
[ ] Step 1: 创建增量 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);
Run(按实际连接串调整):
mysql -u root -p zkxw < docs/sql/2026-06-15-process-equipment-id.sql
Expected: 3 条 ALTER TABLE 成功,无报错。
在 docs/sql/zkxw.sql 的 biz_process、biz_process_route_step、biz_production_task_step 建表语句中,description 后插入同样的 equipment_id 列定义;biz_process 增加 idx_process_equipment 索引。
git add docs/sql/2026-06-15-process-equipment-id.sql docs/sql/zkxw.sql
git commit -m "feat: 三表新增 equipment_id 字段以关联设备管理"
Files:
Modify: backend/app/models/models.py
[ ] Step 1: BizProcess 增加字段
在 BizProcess.description 之后添加:
equipment_id = Column(String(64), nullable=True, comment="关联设备ID")
在 BizProcessRouteStep.description 之后添加同样一行。
在 BizProductionTaskStep.description 之后添加同样一行。
Run:
cd backend && source venv/bin/activate && python -c "from app.models.models import BizProcess, BizProcessRouteStep, BizProductionTaskStep; print('ok')"
Expected: ok
git add backend/app/models/models.py
git commit -m "feat: Model 增加 equipment_id 字段"
Files:
Modify: backend/app/crud/equipment.py
[ ] Step 1: 添加 resolve_equipment_snapshot
在文件顶部 import 区追加 Tuple(若尚未导入),并在 get_equipment 之后添加:
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
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()
Run:
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
git add backend/app/crud/equipment.py
git commit -m "feat: 设备快照解析与删除引用检查"
Files:
Modify: backend/app/crud/process.py
[ ] Step 1: _row_to_dict 增加 equipmentId
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 "",
# ... 其余不变
}
函数参数 equipment_name 改为 equipment_id: Optional[str] = None,并在 obj = BizProcess(...) 前调用:
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,
当 equipment_id is not None(含显式传空字符串清空)时:
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 逻辑。
git add backend/app/crud/process.py
git commit -m "feat: 工艺 CRUD 支持 equipment_id 并写入名称快照"
Files:
Modify: backend/app/api/production.py
[ ] Step 1: ProcessCreateBody / ProcessUpdateBody
将 equipment_name 字段替换为:
equipment_id: Optional[str] = Field(None, alias="equipmentId")
Create/Update 路由调用 create_process / update_process 时传 equipment_id=body.equipment_id(Update 从 payload 取 equipment_id key)。
create_process 调用处示例:
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,
# ...
)
Run:
cd backend && source venv/bin/activate && python -c "from app.api.production import ProcessCreateBody; print(ProcessCreateBody.model_fields.keys())"
Expected: 输出含 equipment_id,不含可写 equipment_name。
git add backend/app/api/production.py
git commit -m "feat: 工艺 API 改为接收 equipmentId"
Files:
backend/app/crud/process_route.pyModify: backend/app/schemas/process_route.py
[ ] Step 1: _step_row_to_dict 增加 equipmentId
"equipmentId": s.equipment_id or "",
"equipmentName": s.equipment_name or "",
BizProcessRouteStep(...) 构造处增加:
equipment_id=proc.equipment_id,
equipment_name=proc.equipment_name,
equipment_id=src.equipment_id,
equipment_name=src.equipment_name,
在 update_step 中,若 kwargs.get("equipment_id") 不为 None(用 sentinel 区分未传 vs 清空,或直接检查 key in kwargs):
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(不再接受纯文本写入)。
backend/app/schemas/process_route.py:
equipment_id: Optional[str] = Field(None, alias="equipmentId")
# 删除 equipment_name 字段
找到 update_route_step / StepUpdateBody 使用处,确保 payload 映射 equipment_id。
git add backend/app/crud/process_route.py backend/app/schemas/process_route.py backend/app/api/production.py
git commit -m "feat: 工艺路线步骤支持 equipmentId 复制与更新"
Files:
backend/app/crud/production_task.pyModify: backend/app/schemas/production_task.py
[ ] Step 1: _step_to_dict 增加 equipmentId
"equipmentId": s.equipment_id or "",
equipment_id=rs.equipment_id,
equipment_name=rs.equipment_name,
BizProductionTaskStep(...) 从工艺复制处增加 equipment_id=proc.equipment_id。
改为接受 equipment_id,经 resolve_equipment_snapshot 写入;移除 equipment_name 直写。
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。
equipment_id=src.equipment_id,
equipment_name=src.equipment_name,
# TaskStepAdd
equipment_id: Optional[str] = Field(None, alias="equipmentId")
# TaskStepUpdate
equipment_id: Optional[str] = Field(None, alias="equipmentId")
删除两个 schema 中的 equipment_name 可写字段。
git add backend/app/crud/production_task.py backend/app/schemas/production_task.py backend/app/api/production.py
git commit -m "feat: 生产任务步骤支持 equipmentId 复制与更新"
Files:
Create: backend/scripts/migrate_process_equipment_id.py
[ ] Step 1: 编写迁移脚本
#!/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()
Run:
cd backend && source venv/bin/activate && python scripts/migrate_process_equipment_id.py --dry-run
Expected: 打印各表匹配数量,无异常。
git add backend/scripts/migrate_process_equipment_id.py
git commit -m "feat: 历史工艺设备名称回填 equipment_id 脚本"
Files:
web-admin/src/views/production/process/index.vue可选: web-admin/src/utils/equipment-select.ts
[ ] Step 1: 引入 fetchAllEquipments
import { fetchAllEquipments, /* 现有 imports */ } from '@/api/production'
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()。
const formData = reactive({
// ...
equipmentId: '',
// 移除 equipmentName 作为表单字段(展示用 detail 快照即可)
})
resetForm、handleEdit、handleSubmit 中:
detail.equipmentId提交 { equipmentId: formData.equipmentId || undefined },不再传 equipmentName
[ ] Step 4: 模板替换 el-input → el-select
<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。
Run: cd web-admin && pnpm dev,打开 /#/production/process(或实际菜单路径),新增/编辑工艺选设备并保存。
git add web-admin/src/views/production/process/index.vue
git commit -m "feat: 工艺表单设备改为下拉关联设备管理"
Files:
Modify: web-admin/src/views/production/process/ProcessRouteTab.vue
[ ] Step 1: 加载 equipmentOptions(同 Task 9)
在 loadReferenceData 或组件 mount 时调用 fetchAllEquipments()。
将 selectedStep.equipmentName 的 el-input 改为 el-select,v-model="selectedStep.equipmentId"。
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
}
当 !selectedStep.equipmentId && selectedStep.equipmentName 时显示「历史文本:xxx」。
git add web-admin/src/views/production/process/ProcessRouteTab.vue
git commit -m "feat: 工艺路线步骤设备改为下拉选择"
Files:
web-admin/src/views/production/production-work-order/StepDetailPanel.vueModify: web-admin/src/views/production/production-work-order/useTaskStepEditor.ts
[ ] Step 1: useTaskStepEditor 加载 equipmentOptions
在 editor 初始化或 loadTaskDetail 后加载 fetchAllEquipments(),export equipmentOptions 与 equipmentOptionLabel。
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)
<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>
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: 生产任务步骤设备改为下拉选择"
Files:
无新文件
[ ] Step 1: 字段一致性检查
Run:
cd backend && source venv/bin/activate && python scripts/check_field_consistency.py
Expected: process 相关无新增不一致告警(若有,按 _row_to_dict 补全 Create/Update schema 与前端 formData)。
| # | 操作 | 预期 |
|---|---|---|
| 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 |
Run:
./scripts/update-graph.sh all
将 docs/superpowers/specs/2026-06-15-process-equipment-link-design.md 头部 状态: 待确认 改为 状态: 已实现。
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 章节 | 对应 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.
两种执行方式:
你希望用哪种方式开始实现?