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_production_task_step_assignee(对齐 biz_qc_task_assignee);步骤表增加 planned_start_time、completed_by;PUT /production-tasks/{id}/steps/dispatch 批量 sync 负责人;mine 鉴权从 assert_task_assignee 改为「任务级任一步 assignee OR 步骤级 assignee」+ 步骤写操作 assert_step_assignee;前端新增 DispatchStepDialog.vue,移除任务级负责人表单,隐藏任务转发/改派入口。
Tech Stack: MySQL 5.7 · FastAPI · SQLAlchemy · Pydantic v2 · Vue 3 · Element Plus · pnpm
Design Spec: docs/superpowers/specs/2026-06-25-production-step-dispatch-design.md
| 文件 | 职责 |
|---|---|
docs/sql/migrations/2026-06-25-production-step-dispatch.sql |
步骤字段 + assignee 表 |
docs/sql/migrations/2026-06-25-production-step-dispatch-backfill.sql |
可选:旧任务 task assignee → 全步骤 assignee |
docs/sql/zkxw.sql |
同步 DDL |
backend/app/models/models.py |
BizProductionTaskStep 新列 + BizProductionTaskStepAssignee |
backend/app/models/__init__.py |
export 新模型 |
backend/app/crud/production_task_step_assignee.py |
新建 sync / enrich / validate |
backend/app/crud/production_task.py |
mine 过滤、dispatch、assert、completed_by、create 去 assignee 必填 |
backend/app/schemas/production_task.py |
ProductionTaskStepDispatchBody;Create 去必填 assignee |
backend/app/api/production.py |
PUT steps/dispatch;_assert_* 改造 |
web-admin/src/api/production-task.ts |
fetchDispatchProductionTaskSteps;create 去 assigneeUserId |
web-admin/src/views/production/production-work-order/DispatchStepDialog.vue |
新建 步骤派发表格弹窗 |
web-admin/src/views/production/production-work-order/DispatchTaskDialog.vue |
移除任务负责人;B 入口打开步骤派发 |
web-admin/src/views/production/production-work-order/WorkOrderTaskSubList.vue |
步骤派发按钮;隐藏转发/改派;展示步骤负责人 |
web-admin/src/views/production/production-work-order/TaskExecutionDialog.vue |
mine 步骤过滤 + 只读样式 |
web-admin/src/views/production/production-work-order/StepDetailPanel.vue |
canEditStep 控制按钮 |
web-admin/src/views/production/production-work-order/useTaskStepEditor.ts |
canEditStep / 完成写 completed_by 透传 |
Files:
docs/sql/migrations/2026-06-25-production-step-dispatch.sqldocs/sql/migrations/2026-06-25-production-step-dispatch-backfill.sqlModify: docs/sql/zkxw.sql(biz_production_task_step 与新建表片段)
[ ] Step 1: 编写主 migration
-- docs/sql/migrations/2026-06-25-production-step-dispatch.sql
ALTER TABLE `biz_production_task_step`
ADD COLUMN `planned_start_time` datetime DEFAULT NULL COMMENT '计划开始时间' AFTER `step_status`;
ALTER TABLE `biz_production_task_step`
ADD COLUMN `completed_by` varchar(64) DEFAULT NULL COMMENT '完成步骤的用户ID' AFTER `completed_time`;
CREATE TABLE IF NOT EXISTS `biz_production_task_step_assignee` (
`id` varchar(64) NOT NULL COMMENT '主键',
`step_id` varchar(64) NOT NULL COMMENT '生产任务步骤ID',
`user_id` varchar(64) NOT NULL COMMENT '负责人用户ID',
`tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int DEFAULT 0 COMMENT '展示顺序',
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL COMMENT '更新时间',
`del_flag` tinyint NOT NULL DEFAULT 0 COMMENT '删除标记',
PRIMARY KEY (`id`),
KEY `idx_step_assignee_step` (`step_id`),
KEY `idx_step_assignee_user` (`user_id`, `tenant_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='生产任务步骤负责人';
-- docs/sql/migrations/2026-06-25-production-step-dispatch-backfill.sql
-- 将仍有 task.current_assignee_user_id 且无步骤负责人的任务,回填到全部步骤
INSERT INTO biz_production_task_step_assignee (
id, step_id, user_id, tenant_no, order_num,
create_by, create_dept_id, create_time, update_by, update_time, del_flag
)
SELECT
REPLACE(UUID(), '-', ''),
s.id,
t.current_assignee_user_id,
s.tenant_no,
0,
t.create_by,
IFNULL(t.create_dept_id, ''),
NOW(),
t.update_by,
NOW(),
0
FROM biz_production_task t
JOIN biz_production_task_step s ON s.task_id = t.id AND s.del_flag = 0
WHERE t.del_flag = 0
AND t.current_assignee_user_id IS NOT NULL
AND TRIM(t.current_assignee_user_id) <> ''
AND NOT EXISTS (
SELECT 1 FROM biz_production_task_step_assignee a
WHERE a.step_id = s.id AND a.del_flag = 0
);
Run:
mysql -u ... -p ... zkxw < docs/sql/migrations/2026-06-25-production-step-dispatch.sql
Expected: 两列新增成功;biz_production_task_step_assignee 表存在。
docs/sql/zkxw.sql 对应 DDLFiles:
backend/app/models/models.pyModify: backend/app/models/__init__.py
[ ] Step 1: 扩展 BizProductionTaskStep
在 step_status 后、started_time 前增加:
planned_start_time = Column(DateTime, nullable=True, comment="计划开始时间")
在 completed_time 后增加:
completed_by = Column(String(64), nullable=True, comment="完成步骤的用户ID")
BizProductionTaskStepAssignee放在 BizProductionTaskStep 附近,字段对齐 migration SQL(参照 BizQcTaskAssignee)。
backend/app/models/__init__.py:
from app.models.models import (
# ...
BizProductionTaskStepAssignee,
)
__all__ = [
# ...
"BizProductionTaskStepAssignee",
]
Files:
Modify: backend/app/schemas/production_task.py
[ ] Step 1: 创建任务 body 去掉 assignee 必填
class ProductionTaskCreate(BaseModel):
model_config = ConfigDict(populate_by_name=True)
work_order_id: str
assignee_user_id: Optional[str] = Field(None, alias="assigneeUserId") # deprecated,可选
task_type: Optional[str] = 'cell_storage'
task_name: Optional[str] = None
route_id: Optional[str] = None
description: Optional[str] = None
remark: Optional[str] = None
class ProductionTaskStepDispatchItem(BaseModel):
model_config = ConfigDict(populate_by_name=True)
step_id: str = Field(..., alias="stepId")
assignee_user_ids: List[str] = Field(..., alias="assigneeUserIds", min_length=1)
planned_start_time: Optional[datetime] = Field(None, alias="plannedStartTime")
class ProductionTaskStepDispatchBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
steps: List[ProductionTaskStepDispatchItem] = Field(..., min_length=1)
Files:
Create: backend/app/crud/production_task_step_assignee.py
[ ] Step 1: 创建模块(复制 QC 模式并改表名)
"""生产任务步骤负责人 CRUD"""
from __future__ import annotations
from datetime import datetime
from typing import Dict, List, Optional, Set
from sqlalchemy.orm import Session
from app.core.db_collation import eq_unicode
from app.core.utils import generate_uuid
from app.models.models import BizProductionTaskStepAssignee, SysUser
# validate_assignee_user_ids — 与 qc_task_assignee.validate_assignee_user_ids 相同逻辑
def sync_assignees_for_step(
db: Session,
step_id: str,
user_ids: List[str],
*,
tenant_no: str,
operator_id: Optional[str] = None,
create_dept_id: str = "",
) -> None:
"""全量替换步骤负责人(软删旧 + 插入新)。"""
# 实现同 qc_task_assignee.sync_assignees_for_task,表换 BizProductionTaskStepAssignee,FK 换 step_id
def enrich_step_assignees(
db: Session, steps: List[Dict], *, tenant_no: str
) -> None:
"""批量填充 assigneeUserIds / assigneeNames / assignees。"""
step_ids = [s["id"] for s in steps if s.get("id")]
if not step_ids:
return
rows = (
db.query(BizProductionTaskStepAssignee, SysUser)
.outerjoin(SysUser, eq_unicode(SysUser.id, BizProductionTaskStepAssignee.user_id))
.filter(
BizProductionTaskStepAssignee.step_id.in_(step_ids),
BizProductionTaskStepAssignee.tenant_no == tenant_no,
BizProductionTaskStepAssignee.del_flag == 0,
)
.order_by(BizProductionTaskStepAssignee.order_num.asc())
.all()
)
by_step: Dict[str, List] = {}
for assignee, user in rows:
by_step.setdefault(assignee.step_id, []).append((assignee, user))
for step in steps:
pairs = by_step.get(step["id"], [])
step["assigneeUserIds"] = [a.user_id for a, _ in pairs]
step["assigneeNames"] = [
f"{(u.nick_name or u.user_name or '')}({u.user_no or ''})".strip()
for _, u in pairs if u
]
step["assignees"] = [
{
"userId": a.user_id,
"nickName": (u.nick_name or u.user_name or "") if u else "",
"userNo": (u.user_no or "") if u else "",
}
for a, u in pairs
]
def user_is_step_assignee(
db: Session, step_id: str, user_id: str, tenant_no: str
) -> bool:
uid = (user_id or "").strip()
if not uid:
return False
return (
db.query(BizProductionTaskStepAssignee.id)
.filter(
BizProductionTaskStepAssignee.step_id == step_id,
BizProductionTaskStepAssignee.user_id == uid,
BizProductionTaskStepAssignee.tenant_no == tenant_no,
BizProductionTaskStepAssignee.del_flag == 0,
)
.first()
is not None
)
def user_is_any_step_assignee_of_task(
db: Session, task_id: str, user_id: str, tenant_no: str
) -> bool:
from app.models.models import BizProductionTaskStep
uid = (user_id or "").strip()
if not uid:
return False
return (
db.query(BizProductionTaskStepAssignee.id)
.join(
BizProductionTaskStep,
BizProductionTaskStep.id == BizProductionTaskStepAssignee.step_id,
)
.filter(
BizProductionTaskStep.task_id == task_id,
BizProductionTaskStep.del_flag == 0,
BizProductionTaskStepAssignee.user_id == uid,
BizProductionTaskStepAssignee.tenant_no == tenant_no,
BizProductionTaskStepAssignee.del_flag == 0,
)
.first()
is not None
)
Run:
cd backend && source venv/bin/activate
python -c "from app.crud import production_task_step_assignee; print('ok')"
Expected: ok
Files:
Modify: backend/app/crud/production_task.py
[ ] Step 1: 扩展 _step_to_dict
def _step_to_dict(s: BizProductionTaskStep) -> Dict[str, Any]:
return {
# ... existing fields ...
"stepStatus": s.step_status or "dzx",
"plannedStartTime": _fmt_dt(getattr(s, "planned_start_time", None)),
"startedTime": _fmt_dt(s.started_time),
"completedTime": _fmt_dt(s.completed_time),
"completedBy": getattr(s, "completed_by", None) or "",
"completedByName": "",
"assigneeUserIds": [],
"assigneeNames": [],
"assignees": [],
"remark": s.remark or "",
}
create_production_task 去掉 assignee 必填删除或注释:
assignee_raw = (data.get("assignee_user_id") or data.get("assigneeUserId") or "").strip()
assignee_user_id = _validate_assignee_user_id(db, assignee_raw, tenant_no)
改为:
assignee_raw = (data.get("assignee_user_id") or data.get("assigneeUserId") or "").strip()
assignee_user_id = assignee_raw.strip() or None
if assignee_raw.strip():
assignee_user_id = _validate_assignee_user_id(db, assignee_raw, tenant_no)
创建任务时:
current_assignee_user_id=assignee_user_id, # 可为 NULL
dispatch_task_stepsdef dispatch_task_steps(
db: Session,
task_id: str,
*,
tenant_no: str,
steps_payload: List[Dict[str, Any]],
operator_id: Optional[str] = None,
create_dept_id: str = "",
) -> Dict[str, Any]:
from app.crud import production_task_step_assignee as step_assignee_svc
task = get_production_task_row(db, task_id, tenant_no)
if not task:
raise ValueError("任务不存在")
if not steps_payload:
raise ValueError("请至少指定一个步骤的负责人")
step_rows = {
s.id: s
for s in db.query(BizProductionTaskStep).filter(
BizProductionTaskStep.task_id == task_id,
BizProductionTaskStep.tenant_no == tenant_no,
BizProductionTaskStep.del_flag == 0,
).all()
}
for item in steps_payload:
sid = (item.get("step_id") or item.get("stepId") or "").strip()
if sid not in step_rows:
raise ValueError(f"步骤不存在或不属于该任务: {sid}")
user_ids = item.get("assignee_user_ids") or item.get("assigneeUserIds") or []
step_assignee_svc.sync_assignees_for_step(
db, sid, user_ids,
tenant_no=tenant_no,
operator_id=operator_id,
create_dept_id=create_dept_id,
)
step = step_rows[sid]
pst = item.get("planned_start_time") or item.get("plannedStartTime")
if pst is None and "plannedStartTime" in item and item.get("plannedStartTime") is None:
step.planned_start_time = None
elif pst:
from dateutil.parser import parse as _parse_dt
step.planned_start_time = _parse_dt(str(pst)) if not isinstance(pst, datetime) else pst
step.update_by = operator_id
step.update_time = datetime.now()
db.add(step)
db.flush()
detail = get_production_task_detail(db, task_id, tenant_no)
if not detail:
raise ValueError("任务不存在")
return detail
list_production_tasks mine 过滤新增参数 step_assignee_user_id: Optional[str] = None。
在 current_assignee_user_id 过滤块 之后 增加:
sa_uid = (step_assignee_user_id or "").strip()
if sa_uid:
step_task_ids = (
db.query(BizProductionTaskStep.task_id)
.join(
BizProductionTaskStepAssignee,
BizProductionTaskStepAssignee.step_id == BizProductionTaskStep.id,
)
.filter(
BizProductionTaskStepAssignee.user_id == sa_uid,
BizProductionTaskStepAssignee.tenant_no == tenant_no,
BizProductionTaskStepAssignee.del_flag == 0,
BizProductionTaskStep.del_flag == 0,
BizProductionTask.del_flag == 0,
)
.distinct()
.subquery()
)
q = q.filter(
or_(
BizProductionTask.id.in_(step_task_ids),
BizProductionTask.current_assignee_user_id == sa_uid, # 旧数据 fallback
)
)
list_mine_production_tasksscope=current 时调用:
return list_production_tasks(
# ...
step_assignee_user_id=uid,
current_assignee_user_id=None, # 不再仅用 task 级
# ...
)
保留 current_assignee_user_id=uid 作为 OR 条件已在 Step 4 合并;或直接只传 step_assignee_user_id=uid 并在 SQL 内 OR task assignee fallback。
def assert_task_step_assignee_or_legacy(
db: Session, task_id: str, user_id: str, tenant_no: str
) -> BizProductionTask:
from app.crud import production_task_step_assignee as step_assignee_svc
task = get_production_task_row(db, task_id, tenant_no)
if not task:
raise ValueError("任务不存在")
if step_assignee_svc.user_is_any_step_assignee_of_task(db, task_id, user_id, tenant_no):
return task
if (task.current_assignee_user_id or "") == (user_id or ""):
return task
raise PermissionError("非任务步骤负责人,无权操作")
def assert_step_assignee_for_write(
db: Session,
step_id: str,
user_id: str,
tenant_no: str,
*,
manage: bool = False,
) -> BizProductionTaskStep:
if manage:
step = db.query(BizProductionTaskStep).filter(
BizProductionTaskStep.id == step_id,
BizProductionTaskStep.tenant_no == tenant_no,
BizProductionTaskStep.del_flag == 0,
).first()
if not step:
raise ValueError("步骤不存在")
return step
from app.crud import production_task_step_assignee as step_assignee_svc
step = db.query(BizProductionTaskStep).filter(
BizProductionTaskStep.id == step_id,
BizProductionTaskStep.tenant_no == tenant_no,
BizProductionTaskStep.del_flag == 0,
).first()
if not step:
raise ValueError("步骤不存在")
if not step_assignee_svc.user_is_step_assignee(db, step_id, user_id, tenant_no):
raise PermissionError("非当前步骤负责人,无权操作")
return step
update_task_step 写 completed_by当 data.get("step_status") == "ywc" 且 user_id 存在:
step.completed_by = user_id
get_production_task_detail enrich 步骤 assignee加载 steps 后调用:
from app.crud.production_task_step_assignee import enrich_step_assignees
enrich_step_assignees(db, steps, tenant_no=tenant_no)
# 可选:_enrich_completed_by_names
Files:
Modify: backend/app/api/production.py
[ ] Step 1: 改造 _assert_production_task_write_access
def _assert_production_task_write_access(
db: Session,
user: dict,
task_id: str,
tenant_no: str,
operate_scope: Optional[str],
) -> None:
scope = (operate_scope or "manage").strip().lower()
user_id = _get_current_user_id(user)
role = user.get("role")
codes = get_user_menu_codes(db, user_id) if role != SUPER_ADMIN_ROLE else set()
if scope == "mine":
if role != SUPER_ADMIN_ROLE and "production:my-task:list" not in codes:
raise PermissionError("无菜单访问权限")
production_task_svc.assert_task_step_assignee_or_legacy(
db, task_id, user_id, tenant_no
)
elif role != SUPER_ADMIN_ROLE and "production:work-order:list" not in codes:
raise PermissionError("无菜单访问权限")
def _assert_production_task_step_write_access(
db: Session,
user: dict,
step_id: str,
tenant_no: str,
operate_scope: Optional[str],
) -> None:
scope = (operate_scope or "manage").strip().lower()
user_id = _get_current_user_id(user)
role = user.get("role")
codes = get_user_menu_codes(db, user_id) if role != SUPER_ADMIN_ROLE else set()
manage = scope != "mine"
if not manage:
if role != SUPER_ADMIN_ROLE and "production:my-task:list" not in codes:
raise PermissionError("无菜单访问权限")
elif role != SUPER_ADMIN_ROLE and "production:work-order:list" not in codes:
raise PermissionError("无菜单访问权限")
production_task_svc.assert_step_assignee_for_write(
db, step_id, user_id, tenant_no, manage=manage
)
PUT /production-tasks/{task_id}/steps/dispatch@router.put("/production-tasks/{task_id}/steps/dispatch")
async def dispatch_production_task_steps_api(
task_id: str,
body: ProductionTaskStepDispatchBody,
user: dict = Depends(require_menu_code("production:work-order:list")),
db: Session = Depends(get_db),
):
try:
tenant_no = _get_tenant_no(db, user)
payload = [
{
"step_id": it.step_id,
"assignee_user_ids": it.assignee_user_ids,
"planned_start_time": it.planned_start_time,
}
for it in body.steps
]
detail = production_task_svc.dispatch_task_steps(
db,
task_id,
tenant_no=tenant_no,
steps_payload=payload,
operator_id=user.get("user_id"),
create_dept_id=user.get("dept_id", ""),
)
db.commit()
return success_response(detail, message="步骤派发成功")
except ValueError as e:
db.rollback()
return error_response(400, str(e))
在以下 handler 内 _assert_production_task_write_access 之后追加 _assert_production_task_step_write_access:
update_production_task_stepreplace_task_step_reagentsreplace_task_step_consumablesreplace_task_step_configured_reagentsreplace_task_step_data_collectionsreplace_task_step_outputsdelete_production_task_stepcopy_production_task_step示例(update_production_task_step):
_assert_production_task_write_access(db, user, task_id, tenant_no, operateScope)
_assert_production_task_step_write_access(db, user, step_id, tenant_no, operateScope)
任务级 inputs/outputs/attachments/status 仍只用 task 级 assert(D10:任一步 assignee 即可)。
Run backend,在 /docs 测试:
PUT .../steps/dispatch 返回 steps 含 assigneeUserIdsGET .../production-tasks/mine?scope=current 仅 assignee 可见Files:
Modify: web-admin/src/api/production-task.ts
[ ] Step 1: 新增 dispatch API
export function fetchDispatchProductionTaskSteps(
taskId: string,
data: {
steps: Array<{
stepId: string
assigneeUserIds: string[]
plannedStartTime?: string | null
}>
}
) {
return request.put<any>({
url: `/web/admin/production-tasks/${taskId}/steps/dispatch`,
data
})
}
DispatchTaskDialog.vue 将在 Task 9 移除该字段。
Files:
Create: web-admin/src/views/production/production-work-order/DispatchStepDialog.vue
[ ] Step 1: 实现弹窗骨架
要点:
visible, taskId, taskName?fetchProductionTaskDetail(taskId) 加载 stepsprocessName、负责人多选(remote fetchUserList)、计划开始时间rows: { stepId, processName, assigneeUserIds, plannedStartTime, userOptions }[]assigneeUserIds.length >= 1 的行,调用 fetchDispatchProductionTaskStepsUI:dialog-form-layout、label-position="top"、footer Close/Check 图标
[ ] Step 2: 回显已有 assignee
detail.steps 中若已有 assigneeUserIds / plannedStartTime,填入表单。
Files:
Modify: web-admin/src/views/production/production-work-order/DispatchTaskDialog.vue
[ ] Step 1: 移除任务级负责人
删除表单项 assigneeUserId、rules.assigneeUserId、form.assigneeUserId 及 create payload 中的 assigneeUserId。
在 handleCreateTask 成功、enterEditPhase 之后:
emit('open-step-dispatch', { taskId, taskName: form.taskName })
或由父组件监听 @success 并根据是否选了 routeId 打开 DispatchStepDialog。
推荐:Dialog 内 emit('needStepDispatch', taskId),父组件 WorkOrderTaskSubList / index.vue 统一挂载 DispatchStepDialog。
创建成功 + expectSteps 为 true 时,关闭编辑阶段或保留编辑的同时弹出步骤派发(产品:B 入口)。
Files:
Modify: web-admin/src/views/production/production-work-order/WorkOrderTaskSubList.vue
[ ] Step 1: 挂载 DispatchStepDialog
<DispatchStepDialog
v-model:visible="stepDispatchVisible"
:task-id="stepDispatchTaskId"
:task-name="stepDispatchTaskName"
@success="handleTaskMutationSuccess"
/>
在 !isMineMode 且任务非 yzf 时显示;文案:无 assignee 时「步骤派发」,否则「调整派发」。
判断:任一步 assigneeUserIds?.length 或后端返回 hasStepDispatch。
删除或 v-if="false":
ForwardTaskDialog 引用可保留文件但不挂载
[ ] Step 4: 任务卡片 meta 展示步骤负责人摘要
在任务名下方增加 muted 文本,例如:步骤负责人:张三、李四(细胞培养 09:00) — 取前 2 步或折叠展示。
Files:
web-admin/src/views/production/production-work-order/useTaskStepEditor.tsweb-admin/src/views/production/production-work-order/TaskExecutionDialog.vueModify: web-admin/src/views/production/production-work-order/StepDetailPanel.vue
[ ] Step 1: useTaskStepEditor 增加 canEditStep
import { useUserStore } from '@/store/modules/user'
const userStore = useUserStore()
function canEditStep(step: Record<string, any> | null | undefined): boolean {
if (!step) return false
if (operateScope.value !== 'mine') return true
const uid = userStore.info?.id || userStore.userId || ''
const ids: string[] = step.assigneeUserIds || []
return ids.includes(uid)
}
function visibleStepsForMine(steps: Record<string, any>[]) {
if (operateScope.value !== 'mine') return steps
return steps.filter((s) => (s.assigneeUserIds || []).length > 0)
}
导出 canEditStep、visibleStepsForMine(或 computed displayStepList)。
[ ] Step 2: TaskExecutionDialog 步骤列表
mine:v-for="element in editor.displayStepList"(仅已派步骤)
对 !canEditStep(element) 的步骤加 class is-readonly(降低 opacity,禁用 drag)
任务级「转发任务」按钮移除
[ ] Step 3: StepDetailPanel 按钮
<el-button
v-if="editor.mode.value === 'execution' && editor.canEditStep(editor.selectedStep.value)"
...
/>
保存 / 开始 / 完成均加 canEditStep 条件;只读时仅展示内容。
handleStartStep / handleCompleteStep 开头:
if (!canEditStep(step)) {
ElMessage.warning('非当前步骤负责人,无法操作')
return
}
Files:
backend/scripts/check_field_consistency.py(若涉及 CRUD 字段变更)Run: ./scripts/update-graph.sh backend frontend
[ ] Step 1: 字段一致性检查
Run:
cd backend && source venv/bin/activate
python scripts/check_field_consistency.py
Expected: 无 production_task step 相关新增不一致;若有 _row_to_dict 变更则修复 schema/前端。
Run:
./scripts/update-graph.sh backend
./scripts/update-graph.sh frontend
[ ] B 入口:生产工单 → 派发任务 → 选工艺路线 → 创建 → 步骤派发 → 用户 A/B 在「我的生产任务」见工单,执行时仅各自步骤可操作,其他只读。
[ ] A 入口:无路线创建 → 手工加 3 步 → 步骤派发 → 同上。
[ ] 一步多人:A、B 同为步骤 1 负责人;A 完成步骤 1 后 B 见只读已完成。
[ ] 同人多步:步骤 1、2 均派给 A;A 两步均可操作。
[ ] 计划开始时间:卡片/派发弹窗展示;未到时间仍可执行。
[ ] 未派步骤:mine 执行弹窗不显示该步;manage 可见可操作。
[ ] 改派:manage 调整派发后,旧 assignee mine 列表消失(若无其他步骤),新 assignee 出现。
[ ] 旧数据 backfill(若执行 backfill SQL):原 task assignee 仍能在 mine 看到任务。
[ ] 回归:任务输入/产出/完成整单、派发质检工单链路不受影响。
| Spec 要求 | 对应 Task |
|---|---|
| D1 工单+任务列表 | Task 5 mine 过滤不变外层结构 |
| D2 其他步骤只读 | Task 11 |
| D3 一步多人任一完成 | Task 5 Step 7 completed_by |
| D4 计划开始时间不阻断 | Task 8/11 无前端时间校验 |
| D5 A+B 双入口 | Task 8/9/10 |
| D6 未派步骤仅 manage | Task 11 visibleStepsForMine |
| D7 方案 1 数据模型 | Task 1–4 |
| D8 任务 assignee 非必填 | Task 3/5 |
| D9 转发 deprecated | Task 10 |
| D10 任务 I/O 任一步 assignee | Task 6 task 级 assert |
| D11 mine 不可改派 | Task 6 dispatch 仅 work-order 权限 |
无 TBD 占位;类型命名 assigneeUserIds / plannedStartTime 前后端一致。
Task 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → 9 → 10 → 11 → 12 → 13
└─ backend 可联调 ─┘ └─ frontend ─────────────┘
Backend Task 1–6 完成后即可用 Swagger 验证 mine 过滤与 dispatch;Frontend Task 7–11 依赖后端 API。