From 1caea9cb4c5d62bd6364204652f8b9fd8b601e4c Mon Sep 17 00:00:00 2001 From: liuyun Date: Wed, 14 Oct 2026 21:54:56 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=9C=8D=E5=8A=A1=E6=9C=BA=E6=9E=84?= =?UTF-8?q?=E7=AB=AF=20C2=20=E6=B4=BE=E5=8D=95=E5=B7=A5=E5=8D=95=E5=AE=9E?= =?UTF-8?q?=E6=96=BD=E8=AE=A1=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- .../plans/2026-10-14-服务机构端-C2派单工单.md | 315 ++++++++++++++++++ 1 file changed, 315 insertions(+) create mode 100644 docs/superpowers/plans/2026-10-14-服务机构端-C2派单工单.md diff --git a/docs/superpowers/plans/2026-10-14-服务机构端-C2派单工单.md b/docs/superpowers/plans/2026-10-14-服务机构端-C2派单工单.md new file mode 100644 index 0000000..b33c064 --- /dev/null +++ b/docs/superpowers/plans/2026-10-14-服务机构端-C2派单工单.md @@ -0,0 +1,315 @@ +# 服务机构端 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、`@map` snake_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 + +1. 时间冲突边界:两工单恰好首尾相接(09:00-10:00 与 10:00-11:00)不算重叠;重叠判据用严格 `<`。 +2. L1 例外方向:waiting_period 允许派单、其他非 active 态(如 benefit_expired/hosp_pause)拒绝。 +3. 项数校验边界:附加项 ≤5;居家 care_mode 按周次/月时长、不按项数。 +4. 计划变更留旧版:change 后旧计划 changed、新计划 active,历史工单 plan_id 不变。 +5. 个人项目库越界: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 末尾) + +```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** + +```bash +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: + +```ts + // 服务机构端·计划/工单(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'): + +```ts + { 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 + +```bash +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**(昆明项数规则常量) + +```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 | 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** + +```bash +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: 实现时间冲突检测** + +```ts +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** + +```bash +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** + +```bash +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 一致。