touyun-elderly-care/docs/superpowers/plans/2026-10-15-服务机构端-C4质控闸门.md
liuyun 7c26bdbd7a docs: 服务机构端 C4 质控闸门实施计划
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-15 01:20:34 +08:00

12 KiB
Raw Blame History

服务机构端 C4 质控闸门 实施计划

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: 在 C3 骨架上落地「质控闸门」:qc 表 + 补正表、全量质控(定时任务)、放行/退回(4001 自审自放行)、五类主动推送聚合、异常联动改计划、补正、admin 质控中心页。

Architecture: 纯新增。后端新增 qc 模块(依赖 record/abnormal/plan/order/elder),前端质控中心页。复用 C3 的 record(submitted)、abnormal、C2 的 plan.change、C1 的 elder。

Tech Stack: NestJS 12 + Prisma 6 + MySQL 8(utf8mb4_general_ci)、Vue 3 + Vite + Pinia + UnoCSS。

Spec: openspec/changes/svc-org-c4-qc/(proposal + specs + design + tasks);设计背景 docs/superpowers/specs/2026-10-15-服务机构端-C4质控闸门-design.md。

Global Constraints

  • 机构=租户:业务表带 tenant_code、UUID v7、@map snake_case;新表 utf8mb4_general_ci。
  • 表命名:qc(无前缀,质控记录)、ho_record_correction(补正属留痕域 ho_)。
  • 不动脚手架文件;业务代码落 src/qc/、src/pages/ltc/qc/。
  • 放行前自检按序:四要素(1004) → 必拍(3001) → 幂等 → 自审自放行(4001)。
  • 五类推送 = pending-list 跨表聚合(非 qc 落库);每类带消除条件。
  • 补正 = 新增说明不改原记录;不可补正签到时间/定位/照片;≥3 次进质控重点。
  • 权限码 qc.*;错误码域 46;规则号 1004/3001/4001 写 message。
  • ⚠️ 迁移链损坏:migrate dev 不可用,用 migrate diff --from-schema-datasource + 手工迁移目录 + migrate deploy。

Review Focus

  1. 自审自放行:record.created_by === ctx.userId(放行人=录入人)→ 4001 拒绝(不因角色并集放宽)。
  2. 放行自检顺序:四要素 → 必拍 → 幂等 → 自审,任一不过不写 pass_time。
  3. 补正不改原值:correct 只写 correction + 生成 recheck qc,不更新 record 的签到时间/定位/照片/项目。
  4. 五类推送消除条件:每类处理完后从 pending-list 消失(否则列表只长不消)。
  5. 定时任务跨租户:@Cron 无请求上下文,用 prisma.raw(无租户注入)跨租户扫描,否则只扫到单租户。

Task 1: Prisma 模型 + 迁移

Files:

  • Modify: sources/ec-nest/prisma/schema.prisma
  • Modify: sources/ec-nest/src/prisma/prisma.service.ts

Interfaces:

  • Produces: Qc/RecordCorrection 模型。

  • Step 1: 追加 2 个模型

/// 质控记录
model Qc {
  id         String    @id @default(uuid(7)) @db.VarChar(36)
  tenantCode String    @map("tenant_code") @db.VarChar(64)
  recordId   String    @map("record_id") @db.VarChar(36)
  qcType     String    @default("normal") @map("qc_type") @db.VarChar(20)
  qcResult   String    @default("pending") @map("qc_result") @db.VarChar(20)
  qcNote     String?   @map("qc_note") @db.VarChar(200)
  qcStaffId  String?   @map("qc_staff_id") @db.VarChar(36)
  qcTime     DateTime? @map("qc_time")
  passTime   DateTime? @map("pass_time")
  firstQcId  String?   @map("first_qc_id") @db.VarChar(36)
  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, recordId])
  @@index([tenantCode, qcResult])
  @@map("qc")
}

/// 补正说明(留痕域 ho_)
model RecordCorrection {
  id             String   @id @default(uuid(7)) @db.VarChar(36)
  tenantCode     String   @map("tenant_code") @db.VarChar(64)
  recordId       String   @map("record_id") @db.VarChar(36)
  qcId           String?  @map("qc_id") @db.VarChar(36)
  correctionNote String   @map("correction_note") @db.VarChar(200)
  correctedBy    String   @map("corrected_by") @db.VarChar(36)
  correctedAt    DateTime @default(now()) @map("corrected_at")

  @@index([recordId])
  @@map("ho_record_correction")
}
  • Step 2: SOFT_DELETE 加 Qc(prisma.service.ts 第 8 行集合加 'Qc';RecordCorrection 子表不入)

  • Step 3: 迁移(utf8mb4_general_ci)

Run: cd sources/ec-nest && pnpm exec prisma migrate diff --from-schema-datasource prisma/schema.prisma --to-schema-datamodel prisma/schema.prisma --script > /tmp/c4.sql;手工落盘迁移目录 20261015xxxxxx_c4_qc/migration.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(c4): qc + record_correction 数据模型"

Task 2: 权限码 + 错误码 + APIS

Files: permissions.ts、error-codes.ts、tenant-template.ts(APIS)

  • Step 1: permissions.ts 追加:
  qc: {
    pendingList: 'qc:pending-list', detail: 'qc:detail', pass: 'qc:pass', return: 'qc:return',
    exceptionLink: 'qc:exception-link', correct: 'qc:correct', batchPass: 'qc:batch-pass',
  },
  • Step 2: error-codes.ts 补域 46:
  // 服务机构端·质控(46)
  QC_NOT_FOUND: 46001,
  QC_ALREADY_HANDLED: 46002,
  QC_SELF_REVIEW: 46003, // 自审自放行(4001)
  QC_ELEMENT_MISSING: 46004, // 缺要素(1004)
  QC_NOTE_REQUIRED: 46005,
  QC_BATCH_UNSAFE: 46006,
  QC_RECORD_NOT_SUBMITTED: 46007,
  • Step 3: APIS 登记(parent 'qc'):
  { code: Permissions.qc.pendingList, name: '待质控列表', method: 'GET', path: '/api/v1/qc/pending/list', parent: 'qc' },
  { code: Permissions.qc.detail, name: '质控详情', method: 'GET', path: '/api/v1/qc/:id', parent: 'qc' },
  { code: Permissions.qc.pass, name: '放行', method: 'POST', path: '/api/v1/qc/:id/pass', parent: 'qc' },
  { code: Permissions.qc.return, name: '退回', method: 'POST', path: '/api/v1/qc/:id/return', parent: 'qc' },
  { code: Permissions.qc.exceptionLink, name: '异常联动改计划', method: 'POST', path: '/api/v1/qc/exception/link', parent: 'qc' },
  { code: Permissions.qc.correct, name: '补正', method: 'POST', path: '/api/v1/qc/correct', parent: 'qc' },
  { code: Permissions.qc.batchPass, name: '批量放行', method: 'POST', path: '/api/v1/qc/batch-pass', parent: 'qc' },
  • 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(c4): qc 权限码/错误码/APIS"

Task 3: qc 模块

Files: 新建 src/qc/{qc.module,qc.controller,qc.service,dto,qc.service.spec}.ts;注册 module(本任务直接改 app.module.ts,本 change 单任务串行)。

Interfaces:

  • Consumes: PrismaService(client + raw)、getRequestContext()、Permissions.qc.*、ErrorCodes.QC_*、record/abnormal/plan/order/elder 模型。

  • Produces: QcService(generatePending/pendingList/detail/pass/return/exceptionLink/correct/batchPass + @Cron 定时任务)。

  • Step 1: 写失败测试(qc.service.spec.ts):定时任务生成 pending qc;放行自检四步(1004/3001/幂等/4001);退回 qc_note 必填;补正不改原值 + 生成 recheck;批量放行排除异常记录;五类推送消除条件。

  • Step 2: 跑测试确认失败

  • Step 3: 实现 qc.service

关键逻辑:

  • 定时任务:@Cron('0 * * * *') generatePendingQc()——用 prisma.raw 跨租户扫描 record_status='submitted' 且无 qc 的 record → 为每条 create qc(tenantCode 取 record.tenantCode、qcType='normal'、qcResult='pending')。

  • pendingList(q):聚合五类(跨表查询):

    1. 异常待处理:recordAbnormal.count({ handleStatus:'open' })(消除条件=已处理)。
    2. 逾期未做:order timeSlotEnd < now-24h 且无 record 且 orderStatus 非 cancelled/not_met/done。
    3. 住院仍有派单:elder treatStatus='hosp_pause' 且有 orderStatus ∈ {to_assign, assigned, serving} 的 order。
    4. 计划未制定:elder benefit_start ≤ now-5工作日 且无 plan_status=active 的 plan。
    5. 留痕待放行:qc.count({ qcResult:'pending' }) + 待放行队列(pending qc 列表,含 record 摘要 + 已等待时长)。
  • pass(id):requireQc → qcResult !== 'pending' → QC_ALREADY_HANDLED(幂等);record.findUnique → ① 四要素(checkin_time/checkout_time/duration_min/assignee_id 任一缺 → QC_ELEMENT_MISSING 1004)→ ② 必拍已拍(遍历 record_item photoRequired && done && !isRefusedPhoto 无 photo → 3001,复用 C3 口径)→ ④ record.createdBy === ctx.userId → QC_SELF_REVIEW 4001;通过 → qc.update({ qcResult:'passed', qcStaffId, qcTime:now, passTime:now })。

  • return(id, dto):dto.qcNote 空 → QC_NOTE_REQUIRED;qc.update({ qcResult:'returned', qcNote, qcStaffId, qcTime:now })。

  • detail(id):qc + 关联 record(全文/项目/照片/异常/定位)+ 补正历史(correction 列表)。

  • exceptionLink(dto):abnormal.findUnique → 复用 planService.change(传 elderId + changeFromPlanId + items 含加项)生成新 plan → abnormal.update({ linkPlanChange:true }) → 返回新 plan。

  • correct(dto):record.findUnique(record_status 须 submitted)→ correction.create({ recordId, qcId, correctionNote, correctedBy: ctx.userId }) → 生成 recheck qc:qc.findFirst({ recordId, qcResult:'returned', orderBy 最新 }) → qc.create({ recordId, qcType:'recheck', qcResult:'pending', firstQcId: 首次qcId })。(不改 record 任何证据字段。)

  • batchPass(dto):对每条 record 校验「有 abnormal 关联 / 有 exec_result∈{undo_not_done,undo_refused} / 有 extra_flag / duration<5min / 有 correction」→ 命中任一则 QC_BATCH_UNSAFE;否则逐条 pass。

  • Step 4: 实现 controller + dto + module(@Controller('qc');写操作 @AuditLog;@Cron 在 service;注册 module 到 app.module.ts)。路由序:静态路径(pending/list、correct、batch-pass、exception/link)必须声明在 GET /:id 之前(否则 :id 吞掉静态段)。

  • Step 5: 测试通过 + tsc + Commit

git -C sources/ec-nest add src/qc src/app.module.ts
git -C sources/ec-nest commit -m "feat(c4): 质控模块(全量质控/放行退回/五类推送/异常联动/补正)"

Task 4: 前端

Files: src/api/qc.ts;src/pages/ltc/qc/index.vue(替换占位)。

  • Step 1: api 封装(http + 类型对齐后端 DTO;pendingList/detail/pass/return/exceptionLink/correct/batchPass)。

  • Step 2: 质控中心页(defineOptions({name:'qc'})):三区:

    • 待我处理(五类推送聚合:每类计数 + 点击跳对应队列)。
    • 待放行队列(record 列表 → 逐条放行/退回,退回选原因下拉;批量放行按钮,后端拦不可批量项)。
    • 异常处理台(abnormal 列表 → 处理/转派/关闭 + 处理意见 + 「联动改计划」入口)。
  • Step 3: type-check + Commit

git -C sources/ec-admin add src/api/qc.ts src/pages/ltc/qc
git -C sources/ec-admin commit -m "feat(c4): 质控中心页"

Task 5: 端到端验证

  • Step 1: 起后端 + 登录,冒烟:submitted 留痕 → 定时任务(或手动触发)生成 qc → 放行(4001 自审拒/通过)→ 退回 → 补正 → recheck → 放行。
  • Step 2: 五类推送:异常/逾期/住院/计划/待放行各出现与消除。
  • Step 3: 异常联动改计划生成新 plan + link_plan_change;审计落库。停后端清理。

Self-Review Notes

  • Spec 覆盖:data-model 2 实体→T1;api-contract→T3;backend/qc→T3;frontend/ad/qc→T4。
  • Review Focus 5 条均有测试(T3)。
  • 类型一致:Qc/RecordCorrection 模型名、Permissions.qc.*、ErrorCodes.QC_* 在 T1/T2/T3 一致。
  • 定时任务用 prisma.raw 跨租户(Review Focus #5)。