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

453 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 工艺路线默认设备 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 行)
- [x] **Step 1: 写迁移 SQL**
```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` 之间插入:
```python
equipment_ids = Column(String(1000), nullable=True, comment="默认设备ID(逗号分隔)")
equipment_name = Column(String(1000), nullable=True, comment="默认设备名称快照(逗号分隔)")
```
- [ ] **Step 3: 语法校验**
```bash
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(若用户要求提交)**
```bash
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` 增加:
```python
equipment_ids: Optional[List[str]] = Field(None, alias="equipmentIds")
```
- [ ] **Step 2: `_route_row_to_dict` 返回设备**
在 `process_route.py` 顶部增加导入:
```python
from app.crud.equipment_snapshot import parse_equipment_ids, resolve_equipment_list_snapshot
```
`_route_row_to_dict` 在 `"isCurrent"` 之后增加:
```python
"equipmentIds": parse_equipment_ids(getattr(r, "equipment_ids", None)),
"equipmentName": getattr(r, "equipment_name", None) or "",
```
- [ ] **Step 3: `update_route` 支持 equipment_ids**
将 `update_route` 改为(保留原有字段循环,并单独处理设备):
```python
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(...)` 增加:
```python
equipment_ids=payload["equipment_ids"] if "equipment_ids" in payload else None,
```
且仅当 `"equipment_ids" in payload` 时才传入;推荐写法:
```python
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: 冒烟**
```bash
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(若用户要求提交)**
```bash
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(...)` **之后**增加:
```python
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: 语法校验**
```bash
cd backend && source venv/bin/activate && python -c "from app.crud.production_task import create_production_task; print('ok')"
```
Expected: `ok`
- [ ] **Step 3: Commit(若用户要求提交)**
```bash
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 中增加:
```ts
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` 附近)增加:
```ts
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` 后:
```ts
routeEquipmentIds.value = parseEquipmentIds(detail.equipmentIds || detail.equipmentName)
```
`handleOpenConfig` 开头重置:
```ts
selectedVirtualStep.value = null
routeEquipmentIds.value = []
```
- [ ] **Step 4: 选中互斥**
```ts
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: 保存默认设备**
```ts
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>` **之前**插入(对齐派发任务):
```vue
<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「请选择…」
示例结构:
```vue
<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: 更新图谱**
```bash
./scripts/update-graph.sh backend
./scripts/update-graph.sh frontend
```
- [ ] **Step 11: Commit(若用户要求提交)**
```bash
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 连续做并设检查点
选哪种?