Files
zkxw/docs/superpowers/plans/2026-07-28-process-route-default-equipment.md
T
2026-07-28 21:33:15 +08:00

15 KiB
Raw Blame History

工艺路线默认设备 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: 在工艺路线「路线图配置」中预制路线级默认设备,派发生产任务选中该路线时自动带入任务「设备」面板。

Architecture: biz_process_route 新增 equipment_ids / equipment_name;路线更新接口落库快照;创建任务在 _clone_route_to_task 同时把路线默认设备拷到 biz_production_task。前端在 ProcessRouteTab 路线图配置中间栏加「默认设备」虚拟卡,右侧复用 TaskEquipmentPanel。

Tech Stack: FastAPI / SQLAlchemy / Pydantic v2、Vue 3、Element Plus、现有 resolve_equipment_list_snapshot / TaskEquipmentPanel

Spec: docs/superpowers/specs/2026-07-28-process-route-default-equipment-design.md


File Structure

File Responsibility
docs/sql/alter_biz_process_route_equipment.sql ALTER 增加路线默认设备字段
backend/app/models/models.py BizProcessRoute 列定义
backend/app/schemas/process_route.py UpdateBody 增加 equipmentIds
backend/app/crud/process_route.py 序列化 + update_route 快照写入
backend/app/api/production.py update route 传入 equipment_ids
backend/app/crud/production_task.py 创建任务时拷贝路线默认设备
web-admin/src/views/production/process/ProcessRouteTab.vue 路线图配置 UI + 保存
web-admin/src/views/production/production-work-order/TaskEquipmentPanel.vue 复用(只读引用,不改行为除非必要)

派发任务前端无需改:创建后 useTaskStepEditor 已从任务详情读 equipmentIds。


Task 1: DB 迁移 + Model

Files:

  • Create: docs/sql/alter_biz_process_route_equipment.sql

  • Modify: backend/app/models/models.py(BizProcessRoute,约 2768–2786 行)

  • Step 1: 写迁移 SQL

-- 工艺路线增加路线级默认设备(派发任务时带入任务设备面板)
-- 执行:在目标库运行本脚本。

ALTER TABLE `biz_process_route`
  ADD COLUMN `equipment_ids` varchar(1000) DEFAULT NULL COMMENT '默认设备ID(biz_equipment.id,逗号分隔)' AFTER `is_current`,
  ADD COLUMN `equipment_name` varchar(1000) DEFAULT NULL COMMENT '默认设备名称快照(逗号分隔)' AFTER `equipment_ids`;
  • Step 2: 更新 SQLAlchemy 模型

在 BizProcessRoute 中,is_current 与 tenant_no 之间插入:

    equipment_ids = Column(String(1000), nullable=True, comment="默认设备ID(逗号分隔)")
    equipment_name = Column(String(1000), nullable=True, comment="默认设备名称快照(逗号分隔)")
  • Step 3: 语法校验
cd backend && source venv/bin/activate && python -c "from app.models.models import BizProcessRoute; assert hasattr(BizProcessRoute, 'equipment_ids'); print('ok')"

Expected: ok

  • Step 4: Commit(若用户要求提交)
git add docs/sql/alter_biz_process_route_equipment.sql backend/app/models/models.py
git commit -m "$(cat <<'EOF'
feat(process-route): add route-level default equipment columns

Store default equipment_ids/equipment_name on biz_process_route for task dispatch autofill.
EOF
)"

Task 2: Schema + CRUD 序列化与更新

Files:

  • Modify: backend/app/schemas/process_route.py

  • Modify: backend/app/crud/process_route.py

  • Modify: backend/app/api/production.py(update_process_route)

  • Step 1: UpdateBody 增加字段

在 ProcessRouteUpdateBody 增加:

    equipment_ids: Optional[List[str]] = Field(None, alias="equipmentIds")
  • Step 2: _route_row_to_dict 返回设备

在 process_route.py 顶部增加导入:

from app.crud.equipment_snapshot import parse_equipment_ids, resolve_equipment_list_snapshot

_route_row_to_dict 在 "isCurrent" 之后增加:

        "equipmentIds": parse_equipment_ids(getattr(r, "equipment_ids", None)),
        "equipmentName": getattr(r, "equipment_name", None) or "",
  • Step 3: update_route 支持 equipment_ids

将 update_route 改为(保留原有字段循环,并单独处理设备):

def update_route(db: Session, route_id: str, *, user_id: Optional[str] = None, **kwargs) -> BizProcessRoute:
    obj = get_route(db, route_id)
    if not obj:
        raise ValueError("路线不存在")
    for field in ["route_code", "route_name", "product_type_id", "version", "remark"]:
        val = kwargs.get(field)
        if val is not None:
            setattr(obj, field, (val or "").strip() or None if field != "route_code" else (val or "").strip())
    if "equipment_ids" in kwargs:
        eids, enames = resolve_equipment_list_snapshot(
            db, kwargs.get("equipment_ids"), obj.tenant_no or "zkxw"
        )
        obj.equipment_ids = eids
        obj.equipment_name = enames
    obj.update_by = user_id
    obj.update_time = datetime.now()
    db.add(obj)
    db.commit()
    db.refresh(obj)
    return obj

注意:清空设备时前端应传 equipmentIds: [],且 API 必须把该键放入 kwargs(见下一步 exclude_unset 行为——空列表仍会 set,可进入 "equipment_ids" in kwargs)。

  • Step 4: API 传入 equipment_ids

update_process_route 中 update_route(...) 增加:

            equipment_ids=payload["equipment_ids"] if "equipment_ids" in payload else None,

且仅当 "equipment_ids" in payload 时才传入;推荐写法:

        payload = body.model_dump(exclude_unset=True, by_alias=False)
        update_kwargs = {
            "user_id": user.get("user_id"),
            "route_code": payload.get("route_code"),
            "route_name": payload.get("route_name"),
            "product_type_id": payload.get("product_type_id"),
            "version": payload.get("version"),
            "remark": payload.get("remark"),
        }
        if "equipment_ids" in payload:
            update_kwargs["equipment_ids"] = payload.get("equipment_ids")
        obj = route_svc.update_route(db, route_id, **update_kwargs)
  • Step 5: 冒烟
cd backend && source venv/bin/activate && python -c "
from app.schemas.process_route import ProcessRouteUpdateBody
from app.crud.process_route import _route_row_to_dict
print(ProcessRouteUpdateBody.model_fields.get('equipment_ids') is not None)
print('ok')
"

Expected: True 然后 ok

  • Step 6: Commit(若用户要求提交)
git add backend/app/schemas/process_route.py backend/app/crud/process_route.py backend/app/api/production.py
git commit -m "$(cat <<'EOF'
feat(process-route): serialize and update route default equipment

Expose equipmentIds on route APIs and persist name snapshot via resolve_equipment_list_snapshot.
EOF
)"

Task 3: 创建任务时带入路线默认设备

Files:

  • Modify: backend/app/crud/production_task.py(create_production_task,约 1017–1019 行)

  • Step 1: 在 clone 步骤后写入任务设备

在 if route_id: 块内,_clone_route_to_task(...) 之后增加:

    route_id = data.get("route_id")
    if route_id:
        _clone_route_to_task(db, task.id, route_id, tenant_no, user_id, create_dept_id)
        route = (
            db.query(BizProcessRoute)
            .filter(
                BizProcessRoute.id == route_id,
                BizProcessRoute.tenant_no == tenant_no,
                BizProcessRoute.del_flag == 0,
            )
            .first()
        )
        if route and (getattr(route, "equipment_ids", None) or getattr(route, "equipment_name", None)):
            task.equipment_ids = route.equipment_ids
            task.equipment_name = route.equipment_name
            db.add(task)

确认文件顶部 / 模型导入已包含 BizProcessRoute(若无则从 app.models.models 导入)。

拷贝策略:直接复制路线已存快照,不二次 resolve(与「创建时快照」一致;设备已删时任务仍保留历史名称,与任务后续再保存时再校验的行为可接受)。

  • Step 2: 语法校验
cd backend && source venv/bin/activate && python -c "from app.crud.production_task import create_production_task; print('ok')"

Expected: ok

  • Step 3: Commit(若用户要求提交)
git add backend/app/crud/production_task.py
git commit -m "$(cat <<'EOF'
feat(production-task): copy route default equipment on task create

When dispatching with a process route, autofill task equipment from route defaults.
EOF
)"

Task 4: 路线图配置 UI — 默认设备

Files:

  • Modify: web-admin/src/views/production/process/ProcessRouteTab.vue

  • Step 1: 增加 import

在 script 中增加:

import TaskEquipmentPanel from '@/views/production/production-work-order/TaskEquipmentPanel.vue'
import { fetchAllEquipments } from '@/api/production'
import { parseEquipmentIds } from '@/utils/equipment-select'

(fetchAllEquipments 可并入现有 @/api/production 解构导入列表。)

  • Step 2: 状态与计算属性

在路线图配置相关 state(selectedStepId 附近)增加:

  const selectedVirtualStep = ref<'equipment' | null>(null)
  const routeEquipmentIds = ref<string[]>([])
  const equipmentOptions = ref<Record<string, any>[]>([])
  const equipmentSaving = ref(false)

  const virtualEquipmentCount = computed(() => routeEquipmentIds.value.length)
  const virtualEquipmentStatus = computed(() =>
    routeEquipmentIds.value.length > 0 ? 'filled' : 'empty'
  )
  • Step 3: 加载参考数据时拉设备;详情加载时回填

loadConfigReferenceData 的 Promise.all 增加 fetchAllEquipments(),结果赋给 equipmentOptions。

loadRouteDetail 在拿到 detail 后:

      routeEquipmentIds.value = parseEquipmentIds(detail.equipmentIds || detail.equipmentName)

handleOpenConfig 开头重置:

    selectedVirtualStep.value = null
    routeEquipmentIds.value = []
  • Step 4: 选中互斥
  function selectVirtualEquipment() {
    selectedVirtualStep.value = 'equipment'
    selectedStepId.value = ''
  }

  function selectStep(step: Record<string, any>) {
    selectedVirtualStep.value = null
    selectedStepId.value = step.id
  }

拖入步骤 / 复制步骤后若自动选中步骤,保持 selectedVirtualStep = null(现有 selectedStepId = newStep.id 即可,并显式 selectedVirtualStep.value = null)。

  • Step 5: 保存默认设备
  async function saveRouteEquipment() {
    if (!configRouteId.value) return
    equipmentSaving.value = true
    try {
      await fetchUpdateProcessRoute(configRouteId.value, {
        equipmentIds: routeEquipmentIds.value
      })
      ElMessage.success('默认设备已保存')
    } catch {
      /* 全局处理 */
    } finally {
      equipmentSaving.value = false
    }
  }
  • Step 6: 模板 — 中间栏虚拟卡

在 .route-config-center-dropzone 内、<draggable> 之前插入(对齐派发任务):

              <div
                class="virtual-step-card"
                :class="{ 'is-active': selectedVirtualStep === 'equipment' }"
                @click="selectVirtualEquipment"
              >
                <div class="virtual-step-icon virtual-step-icon--equipment">🔧</div>
                <div class="virtual-step-info">
                  <div class="virtual-step-name">默认设备</div>
                </div>
                <el-tag v-if="virtualEquipmentStatus === 'filled'" type="success" size="small"
                  >已选 {{ virtualEquipmentCount }} 台</el-tag
                >
                <el-tag v-else type="info" size="small">暂无</el-tag>
              </div>
              <div class="step-divider"><span>工艺步骤</span></div>
  • Step 7: 模板 — 右侧面板

将右侧 route-config-right 改为三态:

  1. selectedVirtualStep === 'equipment' → 标题「默认设备」+ TaskEquipmentPanel
  2. selectedStep → 现有步骤详情
  3. else → 现有 empty「请选择…」

示例结构:

          <div class="route-config-right">
            <template v-if="selectedVirtualStep === 'equipment'">
              <div class="route-config-right-header">
                <h3>默认设备</h3>
              </div>
              <div class="route-config-right-body">
                <TaskEquipmentPanel
                  :equipment-ids="routeEquipmentIds"
                  :equipment-options="equipmentOptions"
                  :saving="equipmentSaving"
                  @update:equipment-ids="(ids) => (routeEquipmentIds = ids)"
                  @save="saveRouteEquipment"
                />
              </div>
            </template>
            <template v-else-if="selectedStep">
              <!-- 现有步骤详情不变 -->
            </template>
            <template v-else>
              <div class="route-config-right-empty">
                <el-icon :size="48" color="#d1d5db"><Rank /></el-icon>
                <p>请选择默认设备或左侧步骤查看详情</p>
              </div>
            </template>
          </div>

注意:routeEquipmentIds 是 ref,模板里不用 .value;@update 回调里应写 routeEquipmentIds = ids(模板自动解包)或脚本内函数赋值 routeEquipmentIds.value = ids。

  • Step 8: 样式

在本文件 scoped style 中复制 DispatchTaskDialog.vue 的 .virtual-step-card / .virtual-step-icon / .virtual-step-info / .virtual-step-name / .step-divider(Fresh Mint,勿改 Token)。可省略 emoji 专用色。

  • Step 9: 手动验收清单
  1. 本地执行 docs/sql/alter_biz_process_route_equipment.sql
  2. 打开 /#/production-group/production-base/process → 工艺路线 → 路线图配置 → 点「默认设备」勾选保存 → 刷新仍在
  3. 生产工单派发任务选该路线 → 编辑阶段「设备」已预填
  4. 任务改设备不影响路线;路线改设备不影响已派发任务
  5. 不选路线派发 → 设备为空
  • Step 10: 更新图谱
./scripts/update-graph.sh backend
./scripts/update-graph.sh frontend
  • Step 11: Commit(若用户要求提交)
git add web-admin/src/views/production/process/ProcessRouteTab.vue
git commit -m "$(cat <<'EOF'
feat(web-admin): configure process route default equipment in route map

Add virtual default-equipment panel in route config and persist via update route API.
EOF
)"

Spec coverage(自检)

Spec 要求 Task
biz_process_route 加字段 Task 1
列表/详情返回 equipmentIds Task 2
PUT 更新 + 快照 Task 2
创建任务拷贝到任务 Task 3
路线图配置虚拟卡 + TaskEquipmentPanel Task 4
派发后可改、互不影响 Task 3 拷贝一次 + 现有任务保存
不做步骤级 UI / 不做编辑弹窗字段 未列入任务

Placeholder scan

无 TBD / 「类似 Task N」占位。


Execution

Plan complete and saved to docs/superpowers/plans/2026-07-28-process-route-default-equipment.md.

两种执行方式:

  1. Subagent-Driven(推荐) — 每任务开新子代理,任务间复审
  2. Inline Execution — 本会话按 executing-plans 连续做并设检查点

选哪种?