15 KiB
工艺路线默认设备 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 改为三态:
selectedVirtualStep === 'equipment'→ 标题「默认设备」+TaskEquipmentPanelselectedStep→ 现有步骤详情- 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: 手动验收清单
- 本地执行
docs/sql/alter_biz_process_route_equipment.sql - 打开
/#/production-group/production-base/process→ 工艺路线 → 路线图配置 → 点「默认设备」勾选保存 → 刷新仍在 - 生产工单派发任务选该路线 → 编辑阶段「设备」已预填
- 任务改设备不影响路线;路线改设备不影响已派发任务
- 不选路线派发 → 设备为空
- 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.
两种执行方式:
- Subagent-Driven(推荐) — 每任务开新子代理,任务间复审
- Inline Execution — 本会话按 executing-plans 连续做并设检查点
选哪种?