2026-06-15-process-equipment-link.md 22 KB

工艺设备关联设备管理 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

-- 工艺设备关联:三表新增 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(按实际连接串调整):

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
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 之后添加:

equipment_id = Column(String(64), nullable=True, comment="关联设备ID")
  • Step 2: BizProcessRouteStep 增加字段

在 BizProcessRouteStep.description 之后添加同样一行。

  • Step 3: BizProductionTaskStep 增加字段

在 BizProductionTaskStep.description 之后添加同样一行。

  • Step 4: 重启后端确认 ORM 无报错

Run:

cd backend && source venv/bin/activate && python -c "from app.models.models import BizProcess, BizProcessRouteStep, BizProductionTaskStep; print('ok')"

Expected: ok

  • Step 5: Commit
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 之后添加:

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
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:

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
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

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(...) 前调用:

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(含显式传空字符串清空)时:

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
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 字段替换为:

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 调用处示例:

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:

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
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

"equipmentId": s.equipment_id or "",
"equipmentName": s.equipment_name or "",
  • Step 2: add_step 复制 equipment_id

BizProcessRouteStep(...) 构造处增加:

equipment_id=proc.equipment_id,
equipment_name=proc.equipment_name,
  • Step 3: copy_step 复制 equipment_id
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):

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:

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
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

"equipmentId": s.equipment_id or "",
  • Step 2: _clone_route_to_task 复制 equipment_id
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
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
equipment_id=src.equipment_id,
equipment_name=src.equipment_name,
  • Step 7: TaskStepAdd / TaskStepUpdate schema
# TaskStepAdd
equipment_id: Optional[str] = Field(None, alias="equipmentId")

# TaskStepUpdate
equipment_id: Optional[str] = Field(None, alias="equipmentId")

删除两个 schema 中的 equipment_name 可写字段。

  • Step 8: Commit
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: 编写迁移脚本

#!/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:

cd backend && source venv/bin/activate && python scripts/migrate_process_equipment_id.py --dry-run

Expected: 打印各表匹配数量,无异常。

  • Step 3: Commit
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

import { fetchAllEquipments, /* 现有 imports */ } from '@/api/production'
  • Step 2: 增加设备选项与 label helper
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
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。

  • Step 5: 本地 dev 冒烟

Run: cd web-admin && pnpm dev,打开 /#/production/process(或实际菜单路径),新增/编辑工艺选设备并保存。

  • Step 6: Commit
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
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
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
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 模板
<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
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:

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:

./scripts/update-graph.sh all
  • Step 4: 更新设计文档状态

将 docs/superpowers/specs/2026-06-15-process-equipment-link-design.md 头部 状态: 待确认 改为 状态: 已实现。

  • Step 5: Commit
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 设检查点

你希望用哪种方式开始实现?