16 KiB
服务机构端 C2 派单工单 实施计划
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: 在 C1 骨架上落地「派单工单」:服务计划(svc_plan/svc_plan_item)+ 八态工单(svc_order)+ 老人个人项目库(ltc_elder_svc)+ 后台排班派单页。
Architecture: 纯新增。后端新增 plan/order 模块(elder-svc 并入 plan),前端排班派单页 + 服务对象页扩展。复用 C1 的 elder 状态机、svc_org 目录、staff 护理员。
Tech Stack: NestJS 12 + Prisma 6 + MySQL 8(utf8mb4_general_ci)、Vue 3 + Vite + Pinia + UnoCSS。
Spec: openspec/changes/svc-org-c2-plan-order/(proposal + specs + design + tasks);设计背景 docs/superpowers/specs/2026-10-14-服务机构端-C2派单工单-design.md。
Global Constraints
- 机构=租户:业务表带
tenant_code、UUID v7、@mapsnake_case、软删deleted_at;新表utf8mb4_general_ci(迁移 SQL 手改)。 - 表命名:
svc_plan/svc_plan_item/svc_order(服务域)、ltc_elder_svc(老人域)。 - 不动脚手架文件;业务代码落
src/plan/、src/order/、src/pages/ltc/schedule/。 - 计划变更 MUST 新建 +
change_from_plan_id指向旧,不原地改。 - 派单 L1:
elder.treat_status ∈ {active, waiting_period};时间冲突 2001 硬拦。 - 权限码 plan./order.(C0 预留);错误码域 44。
Review Focus
- 时间冲突边界:两工单恰好首尾相接(09:00-10:00 与 10:00-11:00)不算重叠;重叠判据用严格
<。 - L1 例外方向:waiting_period 允许派单、其他非 active 态(如 benefit_expired/hosp_pause)拒绝。
- 项数校验边界:附加项 ≤5;居家 care_mode 按周次/月时长、不按项数。
- 计划变更留旧版:change 后旧计划 changed、新计划 active,历史工单 plan_id 不变。
- 个人项目库越界:PUT 的 svc_id 非本机构 L4 → 拒绝。
Task 1: Prisma 模型 + 迁移
Files:
- Modify:
sources/ec-nest/prisma/schema.prisma - Modify:
sources/ec-nest/src/prisma/prisma.service.ts
Interfaces:
-
Produces:
Plan/PlanItem/Order/ElderSvc模型。 -
Step 1: 追加 4 个模型(schema.prisma 末尾)
/// 服务计划头
model Plan {
id String @id @default(uuid(7)) @db.VarChar(36)
tenantCode String @map("tenant_code") @db.VarChar(64)
elderId String @map("elder_id") @db.VarChar(36)
planPeriodStart DateTime @map("plan_period_start") @db.Date
planPeriodEnd DateTime @map("plan_period_end") @db.Date
careLevel String @map("care_level") @db.VarChar(20)
careMode String @map("care_mode") @db.VarChar(20)
itemCount Int @default(0) @map("item_count")
itemCountRule String? @map("item_count_rule") @db.VarChar(100)
extraItemCount Int @default(0) @map("extra_item_count")
weekFreq Int? @map("week_freq")
monthHours Int? @map("month_hours")
planStatus String @default("draft") @map("plan_status") @db.VarChar(20)
confirmElder Boolean @default(false) @map("confirm_elder")
confirmGuardian Boolean @default(false) @map("confirm_guardian")
confirmTime DateTime? @map("confirm_time")
planBasis Json? @map("plan_basis")
changeFromPlanId String? @map("change_from_plan_id") @db.VarChar(36)
bizLine String @default("ltci") @map("biz_line") @db.VarChar(20)
createdAt DateTime @default(now()) @map("created_at")
createdBy String? @map("created_by") @db.VarChar(36)
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
@@index([tenantCode, elderId])
@@map("svc_plan")
}
/// 服务计划项
model PlanItem {
id String @id @default(uuid(7)) @db.VarChar(36)
planId String @map("plan_id") @db.VarChar(36)
svcId String @map("svc_id") @db.VarChar(36)
svcNameSnapshot String @map("svc_name_snapshot") @db.VarChar(100)
svcCodeSnapshot String? @map("svc_code_snapshot") @db.VarChar(20)
freqType String? @map("freq_type") @db.VarChar(20)
freqValue Int? @map("freq_value")
durationMin Int? @map("duration_min")
photoTier String @default("free") @map("photo_tier") @db.VarChar(10)
cautionNote String? @map("caution_note") @db.Text
isMedical Boolean @default(false) @map("is_medical")
needCert Boolean @default(false) @map("need_cert")
@@index([planId])
@@map("svc_plan_item")
}
/// 工单
model Order {
id String @id @default(uuid(7)) @db.VarChar(36)
tenantCode String @map("tenant_code") @db.VarChar(64)
elderId String @map("elder_id") @db.VarChar(36)
planId String @map("plan_id") @db.VarChar(36)
serviceDate DateTime @map("service_date") @db.Date
timeSlotStart DateTime @map("time_slot_start")
timeSlotEnd DateTime @map("time_slot_end")
assigneeId String? @map("assignee_id") @db.VarChar(36)
assignTime DateTime? @map("assign_time")
assignMode String? @map("assign_mode") @db.VarChar(20)
orderStatus String @default("to_assign") @map("order_status") @db.VarChar(20)
itemList Json @map("item_list")
needPhoto Boolean @default(false) @map("need_photo")
cancelReason String? @map("cancel_reason") @db.VarChar(200)
rescheduleLog Json? @map("reschedule_log")
qualificationCheck String? @map("qualification_check") @db.VarChar(20)
billable Boolean @default(true) @map("billable")
priceSnapshot Json? @map("price_snapshot")
settlePeriod String? @map("settle_period") @db.VarChar(20)
notMetReason String? @map("not_met_reason") @db.VarChar(50)
notMetAt DateTime? @map("not_met_at")
bizLine String @default("ltci") @map("biz_line") @db.VarChar(20)
createdAt DateTime @default(now()) @map("created_at")
createdBy String? @map("created_by") @db.VarChar(36)
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
@@index([tenantCode, assigneeId, serviceDate])
@@index([tenantCode, elderId])
@@map("svc_order")
}
/// 老人个人项目库
model ElderSvc {
id String @id @default(uuid(7)) @db.VarChar(36)
tenantCode String @map("tenant_code") @db.VarChar(64)
elderId String @map("elder_id") @db.VarChar(36)
svcId String @map("svc_id") @db.VarChar(36)
svcNameSnapshot String @map("svc_name_snapshot") @db.VarChar(100)
svcCodeSnapshot String? @map("svc_code_snapshot") @db.VarChar(20)
careMode String @map("care_mode") @db.VarChar(20)
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
createdBy String? @map("created_by") @db.VarChar(36)
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
@@index([tenantCode, elderId])
@@map("ltc_elder_svc")
}
-
Step 2: SOFT_DELETE 加四模型(prisma.service.ts 第 8 行集合加
'Plan', 'PlanItem', 'Order', 'ElderSvc') -
Step 3: 迁移(utf8mb4_general_ci)
Run: cd sources/ec-nest && pnpm exec prisma migrate dev --create-only --name c2_plan_order;编辑迁移 SQL 把 utf8mb4_unicode_ci 全改 utf8mb4_general_ci;pnpm exec prisma migrate deploy && pnpm exec prisma generate。
- Step 4: Commit
git -C sources/ec-nest add prisma/schema.prisma prisma/migrations src/prisma/prisma.service.ts
git -C sources/ec-nest commit -m "feat(c2): plan/plan_item/order/elder_svc 数据模型"
Task 2: 权限码 + 错误码 + APIS
Files: permissions.ts、error-codes.ts、tenant-template.ts(APIS)
-
Step 1: 确认
plan: { create, change, detail, confirm }、order: { generate, assign, reassign, cancel, detail, today }在 permissions.ts(C0 预留),缺则补。 -
Step 2: error-codes.ts 补域 44:
// 服务机构端·计划/工单(44)
PLAN_NOT_FOUND: 44001,
PLAN_ITEM_RULE: 44002,
ORDER_NOT_FOUND: 44003,
ORDER_CONFLICT: 44004,
ORDER_STATUS_INVALID: 44005,
- Step 3: APIS 登记(parent 用菜单 code 'schedule' 或 'elder'):
{ code: Permissions.plan.create, name: '制定计划', method: 'POST', path: '/api/v1/plan', parent: 'elder' },
{ code: Permissions.plan.change, name: '变更计划', method: 'POST', path: '/api/v1/plan/change', parent: 'elder' },
{ code: Permissions.plan.detail, name: '计划详情', method: 'GET', path: '/api/v1/plan/:id', parent: 'elder' },
{ code: Permissions.order.generate, name: '生成工单', method: 'POST', path: '/api/v1/order/generate', parent: 'schedule' },
{ code: Permissions.order.assign, name: '派单', method: 'POST', path: '/api/v1/order/assign', parent: 'schedule' },
{ code: Permissions.order.reassign, name: '调单', method: 'POST', path: '/api/v1/order/:id/reassign', parent: 'schedule' },
{ code: Permissions.order.cancel, name: '取消工单', method: 'POST', path: '/api/v1/order/:id/cancel', parent: 'schedule' },
{ code: Permissions.order.detail, name: '工单详情', method: 'GET', path: '/api/v1/order/:id', parent: 'schedule' },
{ code: Permissions.order.today, name: '今日任务', method: 'GET', path: '/api/v1/order/today', parent: 'schedule' },
{ code: Permissions.elder.svcSave, name: '个人项目库', method: 'PUT', path: '/api/v1/elder-svc/:elderId', parent: 'elder' },
- Step 4: tsc + Commit
git -C sources/ec-nest add src/common/permissions.ts src/common/error-codes.ts src/common/tenant-template.ts
git -C sources/ec-nest commit -m "feat(c2): plan/order 权限码/错误码/APIS"
Task 3: plan + elder-svc 模块
Files: 新建 src/plan/{plan.module,plan.controller,plan.service,plan-rule.ts,dto,plan.service.spec}.ts、src/elder-svc/{elder-svc.module,elder-svc.controller,elder-svc.service,dto,elder-svc.service.spec}.ts;注册两个 module。
Interfaces:
-
Consumes:
PrismaService.client、getRequestContext()、Permissions.plan.*、ErrorCodes.PLAN_*、svc_org(L4)。 -
Produces:
plan-rule.ts导出ITEM_COUNT_RULE(昆明项数规则常量)、checkItemCount(careMode, careLevel, itemCount, extraCount)。 -
Step 1: 写失败测试(plan.service.spec.ts):制定计划(L4 校验 + 项数 + item_count 快照)、变更留旧版(旧转 changed + change_from_plan_id)、项数不满足返回 PLAN_ITEM_RULE。
-
Step 2: 跑测试确认失败
-
Step 3: 实现 plan-rule.ts(昆明项数规则常量)
export const ITEM_COUNT_RULE = {
inst_medical: { care_1: 18, care_2: 15, care_3: 13 },
inst_nursing: { care_1: 18, care_2: 15, care_3: 13 },
home_individual: { care_1: 15, care_2: 13, care_3: 10 },
home_org: null, // 居家机构上门:按周次/月时长,不按项数
} as const;
export const EXTRA_MAX = 5;
export function checkItemCount(careMode: string, careLevel: string, itemCount: number, extraCount: number): { ok: boolean; hint: string } {
const rule = (ITEM_COUNT_RULE as Record<string, Record<string, number> | null>)[careMode];
if (rule === null || rule === undefined) return { ok: true, hint: '州市项数规则待属地实施细则明确' };
const min = rule[careLevel];
if (min === undefined) return { ok: true, hint: '' };
if (itemCount < min) return { ok: false, hint: `当前护理方式/等级需 ≥${min} 项` };
if (extraCount > EXTRA_MAX) return { ok: false, hint: `附加项最多 ${EXTRA_MAX} 项` };
return { ok: true, hint: '' };
}
-
Step 4: 实现 plan.service(制定计划:L4 校验 + checkItemCount + item_count/item_count_rule 快照;变更:新建 + change_from_plan_id + 旧转 changed;确认;按老人查)+ elder-svc.service(PUT 整表覆盖:svc_id 落本机构 L4、care_mode 变更重制旧库留档;带入)
-
Step 5: 实现 controller + dto + module(
@Controller('plan')、@Controller('elder-svc');写操作@AuditLog;注册 module) -
Step 6: 测试通过 + tsc + Commit
git -C sources/ec-nest add src/plan src/elder-svc src/app.module.ts
git -C sources/ec-nest commit -m "feat(c2): 服务计划 + 老人个人项目库模块"
Task 4: order 模块
Files: 新建 src/order/{order.module,order.controller,order.service,dto,order.service.spec}.ts;注册 module。
Interfaces:
-
Consumes:
PrismaService、getRequestContext()、Permissions.order.*、ErrorCodes.ORDER_*、elder 状态、staff。 -
Produces:
isTimeOverlap(aStart, aEnd, bStart, bEnd)、OrderService(generate/assign/reassign/cancel/detail/today/batchRecommend)。 -
Step 1: 写失败测试(order.service.spec.ts):派单 L1(waiting_period 允许、benefit_expired 拒绝 1001)、时间冲突 2001(首尾相接不算重叠)、调单写 reschedule_log、取消需 reason。
-
Step 2: 跑测试确认失败
-
Step 3: 实现时间冲突检测
export function isTimeOverlap(aStart: Date, aEnd: Date, bStart: Date, bEnd: Date): boolean {
return aStart < bEnd && bStart < aEnd; // 严格 <,首尾相接不算重叠
}
-
Step 4: 实现 order.service:generate(L2 需 active 计划 + 从 plan_item 生成 item_list 快照 + need_photo);assign(L1 校验 treat_status ∈ {active, waiting_period} + 当班 + isTimeOverlap 冲突 2001 + 资质 1006 提示);reassign(reschedule_log);cancel(cancel_reason 必填);detail;today(本人);batchRecommend(权重:同资质→历史服务过→距离近→负荷低,返回推荐+理由,不自动派)。
-
Step 5: 实现 controller + dto + module(
@Controller('order');写操作@AuditLog;注册 module) -
Step 6: 测试通过 + tsc + Commit
git -C sources/ec-nest add src/order src/app.module.ts
git -C sources/ec-nest commit -m "feat(c2): 工单模块(派单/调单/冲突/批量推荐)"
Task 5: 前端
Files: src/api/plan.ts、src/api/order.ts、src/api/elder-svc.ts;src/pages/ltc/schedule/index.vue(替换占位);src/pages/ltc/elder/ 扩展(个人项目库 + 制定计划)。
-
Step 1: api 封装(
http+ 类型对齐后端 DTO)。 -
Step 2: 排班派单页(
defineOptions({name:'schedule'})):日视图(时间轴表格/栅格,纵轴护理员、横轴时段)、工单卡片、调单面板(≤3步:点卡片→选护理员→确认)、冲突红框/空缺橙框、批量推荐列表(需逐条确认)。 -
Step 3: 服务对象页扩展:个人项目库区块(勾选 L4 项目、可空)+ 制定计划弹窗(从个人项目库带入、项数校验提示)。
-
Step 4: type-check + Commit
git -C sources/ec-admin add src/api/plan.ts src/api/order.ts src/api/elder-svc.ts src/pages/ltc/schedule src/pages/ltc/elder
git -C sources/ec-admin commit -m "feat(c2): 排班派单页 + 服务对象计划/个人项目库"
Task 6: 端到端验证
- Step 1: 起后端 + 登录,冒烟:制定计划(项数校验)→ 生成工单 → 派单 → 调单 → 取消 → 冲突硬拦。
- Step 2: L1 例外:waiting_period 可派、benefit_expired 拒。
- Step 3: 个人项目库带入计划。
- Step 4: 审计落库(派单/调单/取消/计划变更)。停后端清理。
Self-Review Notes
- Spec 覆盖:data-model 4 实体→T1;api-contract→T3/T4;backend/plan→T3;backend/order→T4;frontend/ad/schedule→T5。
- Review Focus 5 条均有测试(T3/T4)。
- 类型一致:
ITEM_COUNT_RULE/checkItemCount/isTimeOverlap/Permissions.plan.*/Permissions.order.*在 T2/T3/T4 一致。