docs: 服务机构端 C3 履约留痕实施计划
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
9f789fccb5
commit
398eae800f
352
docs/superpowers/plans/2026-10-15-服务机构端-C3履约留痕.md
Normal file
352
docs/superpowers/plans/2026-10-15-服务机构端-C3履约留痕.md
Normal file
@ -0,0 +1,352 @@
|
|||||||
|
# 服务机构端 C3 履约留痕 实施计划
|
||||||
|
|
||||||
|
> **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:** 在 C2 骨架上落地「履约留痕」:护理员签到/签退/项目执行/拍照/异常上报(ho_record 四表)+ 垫付台账与结转(is_prepaid)+ admin 工单留痕/异常列表两页。
|
||||||
|
|
||||||
|
**Architecture:** 纯新增。后端新增 record/abnormal 模块,前端工单留痕页 + 异常列表页 + 服务对象页结转入口。复用 C2 的 order 八态、plan/plan_item、staff、MinIO StorageService。
|
||||||
|
|
||||||
|
**Tech Stack:** NestJS 12 + Prisma 6 + MySQL 8(utf8mb4_general_ci)、Vue 3 + Vite + Pinia + UnoCSS。
|
||||||
|
|
||||||
|
**Spec:** `openspec/changes/svc-org-c3-record/`(proposal + specs + design + tasks);设计背景 `docs/superpowers/specs/2026-10-15-服务机构端-C3履约留痕-design.md`。
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- 机构=租户:业务表带 `tenant_code`、UUID v7、`@map` snake_case;新表 `utf8mb4_general_ci`(迁移 SQL 手改)。
|
||||||
|
- 表命名:`ho_record`/`ho_record_item`/`ho_record_photo`/`ho_record_abnormal`(前缀 ho);`settlement`/`report` 无前缀预留空表。禁用 checkin/clockin/punch。
|
||||||
|
- 不动脚手架文件;业务代码落 `src/record/`、`src/abnormal/`、`src/pages/ltc/record/`。
|
||||||
|
- 签到/签退时间、时长一律服务器取(接口不接受客户端传入 checkin_time/checkout_time/duration_min)。
|
||||||
|
- `exec_result` 枚举 `done`/`undo_not_done`/`undo_refused`(禁用 not_done)。
|
||||||
|
- 必拍未拍 → 签退硬拒 3001(+拒拍出路);定位失败 3002 不阻塞;垫付 3004 仅标记;扑空 3003 转 not_met。
|
||||||
|
- L3:留痕必须关联有效工单;L1 例外:`waiting_period` 老人留痕 is_prepaid=1、billable=0。
|
||||||
|
- 权限码 record.*/abnormal.*;错误码域 45;规则号写 message(循 C2 R3 口径)。
|
||||||
|
- ⚠️ 迁移链已损坏:`migrate dev` 不可用,用 `migrate diff --from-schema-datasource` + 手工迁移目录 + `migrate deploy`。
|
||||||
|
|
||||||
|
## Review Focus
|
||||||
|
|
||||||
|
1. **服务器取时**:checkin_time/checkout_time/duration_min 必须服务端生成,客户端传入被忽略(防补录时间)。
|
||||||
|
2. **必拍未拍边界**:photo_required 且 exec_result=done 的项目必须有照片(或 is_refused_photo=1),否则签退 3001 硬拒。
|
||||||
|
3. **垫付判定时点**:record 创建时 elder.treat_status === waiting_period → is_prepaid=1、billable=0;active → 0/1。
|
||||||
|
4. **结转不改原始时间戳**:carryover 复制出的新 record 签到时间/定位/照片仍是原始值,仅 source_prepaid_of 标记。
|
||||||
|
5. **扑空 vs 逾期未做**:扑空有到访时间+定位+原因(order.not_met_* + record_abnormal),不产生 record、不计费;逾期未做=到点无记录。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Prisma 模型 + 迁移
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `sources/ec-nest/prisma/schema.prisma`
|
||||||
|
- Modify: `sources/ec-nest/src/prisma/prisma.service.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Produces: `Record`/`RecordItem`/`RecordPhoto`/`RecordAbnormal`/`Settlement`/`Report` 模型。
|
||||||
|
|
||||||
|
- [ ] **Step 1: 追加 6 个模型**(schema.prisma 末尾)
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
/// 履约留痕(主)
|
||||||
|
model Record {
|
||||||
|
id String @id @default(uuid(7)) @db.VarChar(36)
|
||||||
|
tenantCode String @map("tenant_code") @db.VarChar(64)
|
||||||
|
orderId String @map("order_id") @db.VarChar(36)
|
||||||
|
elderId String @map("elder_id") @db.VarChar(36)
|
||||||
|
assigneeId String @map("assignee_id") @db.VarChar(36)
|
||||||
|
checkinTime DateTime? @map("checkin_time")
|
||||||
|
checkoutTime DateTime? @map("checkout_time")
|
||||||
|
durationMin Int? @map("duration_min")
|
||||||
|
checkinLat Decimal? @map("checkin_lat") @db.Decimal(10, 6)
|
||||||
|
checkinLng Decimal? @map("checkin_lng") @db.Decimal(10, 6)
|
||||||
|
locateStatus String @default("ok") @map("locate_status") @db.VarChar(20)
|
||||||
|
recordStatus String @default("serving") @map("record_status") @db.VarChar(20)
|
||||||
|
voiceFile String? @map("voice_file") @db.VarChar(200)
|
||||||
|
offlineFlag Boolean @default(false) @map("offline_flag")
|
||||||
|
clientMd5 String? @map("client_md5") @db.VarChar(32)
|
||||||
|
submitTime DateTime? @map("submit_time")
|
||||||
|
actualDurationTier String? @map("actual_duration_tier") @db.VarChar(10)
|
||||||
|
bizLine String @default("ltci") @map("biz_line") @db.VarChar(20)
|
||||||
|
isPrepaid Boolean @default(false) @map("is_prepaid")
|
||||||
|
billable Boolean @default(true) @map("billable")
|
||||||
|
sourcePrepaidOf String? @map("source_prepaid_of") @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")
|
||||||
|
|
||||||
|
@@unique([tenantCode, clientMd5])
|
||||||
|
@@index([tenantCode, orderId])
|
||||||
|
@@index([tenantCode, elderId])
|
||||||
|
@@index([tenantCode, assigneeId, checkinTime])
|
||||||
|
@@map("ho_record")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 留痕项目明细
|
||||||
|
model RecordItem {
|
||||||
|
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)
|
||||||
|
svcId String @map("svc_id") @db.VarChar(36)
|
||||||
|
svcNameSnapshot String @map("svc_name_snapshot") @db.VarChar(100)
|
||||||
|
execResult String @map("exec_result") @db.VarChar(20)
|
||||||
|
undoReason String? @map("undo_reason") @db.VarChar(200)
|
||||||
|
extraFlag Boolean @default(false) @map("extra_flag")
|
||||||
|
photoRequired Boolean @default(false) @map("photo_required")
|
||||||
|
isRefusedPhoto Boolean @default(false) @map("is_refused_photo")
|
||||||
|
refuseReason String? @map("refuse_reason") @db.VarChar(200)
|
||||||
|
|
||||||
|
@@index([recordId])
|
||||||
|
@@map("ho_record_item")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 留痕照片
|
||||||
|
model RecordPhoto {
|
||||||
|
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)
|
||||||
|
svcId String? @map("svc_id") @db.VarChar(36)
|
||||||
|
filePath String @map("file_path") @db.VarChar(255)
|
||||||
|
fileSize Int @map("file_size")
|
||||||
|
visibleToFamily Boolean @default(false) @map("visible_to_family")
|
||||||
|
uploadBatch String? @map("upload_batch") @db.VarChar(36)
|
||||||
|
keepUntil DateTime? @map("keep_until")
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
|
||||||
|
@@index([recordId])
|
||||||
|
@@map("ho_record_photo")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 异常上报
|
||||||
|
model RecordAbnormal {
|
||||||
|
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)
|
||||||
|
orderId String? @map("order_id") @db.VarChar(36)
|
||||||
|
elderId String @map("elder_id") @db.VarChar(36)
|
||||||
|
reporterId String @map("reporter_id") @db.VarChar(36)
|
||||||
|
abnormalType String @map("abnormal_type") @db.VarChar(20)
|
||||||
|
abnormalDesc String? @map("abnormal_desc") @db.Text
|
||||||
|
voiceFile String? @map("voice_file") @db.VarChar(200)
|
||||||
|
hasPhoto Boolean @default(false) @map("has_photo")
|
||||||
|
visitTime DateTime? @map("visit_time")
|
||||||
|
visitLat Decimal? @map("visit_lat") @db.Decimal(10, 6)
|
||||||
|
visitLng Decimal? @map("visit_lng") @db.Decimal(10, 6)
|
||||||
|
notifyStatus String @default("pending") @map("notify_status") @db.VarChar(20)
|
||||||
|
handleStatus String @default("open") @map("handle_status") @db.VarChar(20)
|
||||||
|
handleResult String? @map("handle_result") @db.VarChar(200)
|
||||||
|
linkPlanChange Boolean @default(false) @map("link_plan_change")
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
createdBy String? @map("created_by") @db.VarChar(36)
|
||||||
|
|
||||||
|
@@index([tenantCode, elderId])
|
||||||
|
@@index([tenantCode, notifyStatus])
|
||||||
|
@@map("ho_record_abnormal")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 结算表(二期预留,建空表不写业务数据)
|
||||||
|
model Settlement {
|
||||||
|
id String @id @default(uuid(7)) @db.VarChar(36)
|
||||||
|
tenantCode String @map("tenant_code") @db.VarChar(64)
|
||||||
|
settlePeriod String @map("settle_period") @db.VarChar(20)
|
||||||
|
amountTotal Decimal? @map("amount_total") @db.Decimal(12, 2)
|
||||||
|
amountPassed Decimal? @map("amount_passed") @db.Decimal(12, 2)
|
||||||
|
amountRejected Decimal? @map("amount_rejected") @db.Decimal(12, 2)
|
||||||
|
amountFund Decimal? @map("amount_fund") @db.Decimal(12, 2)
|
||||||
|
amountSelfPay Decimal? @map("amount_self_pay") @db.Decimal(12, 2)
|
||||||
|
submitDate DateTime? @map("submit_date") @db.Date
|
||||||
|
payDate DateTime? @map("pay_date") @db.Date
|
||||||
|
exceptionReason String? @map("exception_reason") @db.VarChar(200)
|
||||||
|
handleDeadline DateTime? @map("handle_deadline") @db.Date
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
|
||||||
|
@@map("settlement")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 上报表(二期预留,建空表不写业务数据)
|
||||||
|
model Report {
|
||||||
|
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)
|
||||||
|
orderId String @map("order_id") @db.VarChar(36)
|
||||||
|
reportStatus String @default("pending") @map("report_status") @db.VarChar(20)
|
||||||
|
reportTime DateTime? @map("report_time")
|
||||||
|
officialNo String? @map("official_no") @db.VarChar(50)
|
||||||
|
locCode String @map("loc_code") @db.VarChar(12)
|
||||||
|
natCodeSnapshot Json? @map("nat_code_snapshot")
|
||||||
|
adapterResult String? @map("adapter_result") @db.VarChar(200)
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
|
||||||
|
@@map("report")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: SOFT_DELETE 只加 `Record`**(prisma.service.ts 第 8 行集合加 `'Record'`;子表 item/photo/abnormal 循 ElderStatusLog 先例不入,settlement/report 预留不入)
|
||||||
|
|
||||||
|
- [ ] **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/c3.sql`;手工落盘为迁移目录 `20261015xxxxxx_c3_record/migration.sql`,把 `utf8mb4_unicode_ci` 全改 `utf8mb4_general_ci`;`pnpm exec prisma migrate deploy && pnpm exec prisma generate`。(`migrate dev` 因迁移链损坏不可用。)
|
||||||
|
|
||||||
|
- [ ] **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(c3): record/item/photo/abnormal + settlement/report 数据模型"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: 权限码 + 错误码 + APIS
|
||||||
|
|
||||||
|
**Files:** `permissions.ts`、`error-codes.ts`、`tenant-template.ts`(APIS + DICTS 补扑空分类)
|
||||||
|
|
||||||
|
- [ ] **Step 1:** permissions.ts 追加:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
record: {
|
||||||
|
checkin: 'record:checkin', itemSave: 'record:item-save', photoUpload: 'record:photo-upload',
|
||||||
|
checkout: 'record:checkout', offlineSync: 'record:offline-sync', detail: 'record:detail',
|
||||||
|
list: 'record:list', prepaidList: 'record:prepaid-list', prepaidCarryover: 'record:prepaid-carryover',
|
||||||
|
},
|
||||||
|
abnormal: { report: 'abnormal:report', list: 'abnormal:list' },
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2:** error-codes.ts 补域 45:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// 服务机构端·履约留痕/异常(45)
|
||||||
|
RECORD_NOT_FOUND: 45001,
|
||||||
|
RECORD_ORDER_INVALID: 45002,
|
||||||
|
RECORD_STATUS_INVALID: 45003,
|
||||||
|
RECORD_PHOTO_REQUIRED: 45004, // 必拍未拍(3001)
|
||||||
|
RECORD_MD5_DUP: 45005,
|
||||||
|
ABNORMAL_NOT_FOUND: 45006,
|
||||||
|
PREPAID_CARRYOVER_INVALID: 45007,
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3:** APIS 登记(parent 用 'record' 或 'qc'/'elder'):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{ code: Permissions.record.checkin, name: '签到', method: 'POST', path: '/api/v1/record/checkin', parent: 'record' },
|
||||||
|
{ code: Permissions.record.itemSave, name: '保存项目', method: 'POST', path: '/api/v1/record/item/save', parent: 'record' },
|
||||||
|
{ code: Permissions.record.photoUpload, name: '上传照片', method: 'POST', path: '/api/v1/record/photo/upload', parent: 'record' },
|
||||||
|
{ code: Permissions.record.checkout, name: '签退', method: 'POST', path: '/api/v1/record/checkout', parent: 'record' },
|
||||||
|
{ code: Permissions.record.offlineSync, name: '离线补传', method: 'POST', path: '/api/v1/record/offline/sync', parent: 'record' },
|
||||||
|
{ code: Permissions.record.detail, name: '留痕详情', method: 'GET', path: '/api/v1/record/:id', parent: 'record' },
|
||||||
|
{ code: Permissions.record.list, name: '留痕列表', method: 'GET', path: '/api/v1/record', parent: 'record' },
|
||||||
|
{ code: Permissions.record.prepaidList, name: '垫付台账', method: 'GET', path: '/api/v1/record/prepaid-list', parent: 'record' },
|
||||||
|
{ code: Permissions.record.prepaidCarryover, name: '结转', method: 'POST', path: '/api/v1/record/prepaid/carryover', parent: 'record' },
|
||||||
|
{ code: Permissions.abnormal.report, name: '异常上报', method: 'POST', path: '/api/v1/abnormal', parent: 'record' },
|
||||||
|
{ code: Permissions.abnormal.list, name: '异常列表', method: 'GET', path: '/api/v1/abnormal', parent: 'record' },
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4:** DICTS `abnormal_type` 若未含「未遇(扑空)」则补一项 `{ label: '未遇(扑空)', value: 'not_met', colorType: 'info', sort: 10 }`(builtIn)。
|
||||||
|
|
||||||
|
- [ ] **Step 5:** 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(c3): record/abnormal 权限码/错误码/APIS"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: record 模块(履约留痕 + 垫付结转)
|
||||||
|
|
||||||
|
**Files:** 新建 `src/record/{record.module,record.controller,record.service,dto,record.service.spec}.ts`;注册 module。
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `PrismaService.client`、`getRequestContext()`、`Permissions.record.*`、`ErrorCodes.RECORD_*`、order(八态)、elder(treat_status)、staff、`StorageService`(MinIO)、`AuditLogsModule`。
|
||||||
|
- Produces: `RecordService`(checkin/itemSave/photoUpload/checkout/offlineSync/detail/list/prepaidList/prepaidCarryover)。
|
||||||
|
|
||||||
|
- [ ] **Step 1: 写失败测试**(record.service.spec.ts):签到 L3(order 无效拒绝)+ 服务器取时(客户端传入被忽略)+ 定位失败不阻塞;签退必拍 3001 + 垫付 is_prepaid;exec_result 枚举;结转不改时间戳;离线 client_md5 去重。
|
||||||
|
|
||||||
|
- [ ] **Step 2: 跑测试确认失败**
|
||||||
|
|
||||||
|
- [ ] **Step 3: 实现 record.service**
|
||||||
|
|
||||||
|
关键逻辑:
|
||||||
|
- `checkin(dto)`:`order.findUnique(orderId)` → 校验存在 + `orderStatus ∈ {assigned, serving}` + `elderId`/`assigneeId` 匹配(L3);`checkinTime = new Date()`(忽略客户端传入);`locateStatus` 取 dto(ok/failed/denied);`elder.findUnique` → `isPrepaid = elder.treatStatus === 'waiting_period'`、`billable = !isPrepaid`;`record.create({ recordStatus: 'serving', ... })`。
|
||||||
|
- `itemSave(dto)`:record 存在且 `serving`;每个 item `svcId` 落本机构 L4(`svcOrg.findFirst`);`execResult ∈ {done, undo_not_done, undo_refused}`;整表覆盖(先删后写,循 elder-svc PUT 先例)。
|
||||||
|
- `photoUpload(dto, file)`:record 存在;file.size ≤ 500KB(否则拒);`StorageService.upload` → `record_photo.create({ filePath, fileSize, visibleToFamily: false, keepUntil: now+7d })`。注意 `StorageService.buildKey` 硬编码 `elder/` 前缀——给 buildKey 加可选 prefix 参数(默认 'elder'),record 照片传 `record/`(改 `src/storage/storage.service.ts`,不改 elder 现有调用)。
|
||||||
|
- `checkout(dto)`:record 存在且 `serving`;遍历 record_item 找 `photoRequired && execResult==='done' && !isRefusedPhoto` 且无对应 `record_photo` → `throw RECORD_PHOTO_REQUIRED`(message 含 3001);`checkoutTime = new Date()`、`durationMin = round((checkout-checkin)/60000)`、`recordStatus='submitted'`、`submitTime=now`。
|
||||||
|
- `offlineSync(dto)`:dto.clientMd5 必填;`record.findFirst({ clientMd5 })` 命中则幂等返回已存在;否则建完整 record(checkin/checkout/items/photos 一次性)。
|
||||||
|
- `detail(id)`/`list(q)`:详情含 item/photo/abnormal;列表按 elder/assignee/日期/状态筛选。
|
||||||
|
- `prepaidList(elderId)`:`is_prepaid=true` 的记录列表。
|
||||||
|
- `prepaidCarryover(dto)`:校验所有 recordIds `is_prepaid=true` 且同 elder;对每条:`record.create({ ...原字段, isPrepaid:false, billable:true, sourcePrepaidOf:原id, checkinTime/checkoutTime/checkinLat/checkinLng 原样 })` + 复制 item/photo;原记录不改。
|
||||||
|
|
||||||
|
- [ ] **Step 4: 实现 controller + dto + module**(`@Controller('record')`;写操作 `@AuditLog`;`checkin_time/checkout_time/duration_min` 不在 DTO,防客户端传入;注册 module)
|
||||||
|
|
||||||
|
- [ ] **Step 5: 测试通过 + tsc + Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git -C sources/ec-nest add src/record src/app.module.ts
|
||||||
|
git -C sources/ec-nest commit -m "feat(c3): 履约留痕模块(签到/签退/项目/照片/离线/垫付结转)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: abnormal 模块(异常 + 扑空)
|
||||||
|
|
||||||
|
**Files:** 新建 `src/abnormal/{abnormal.module,abnormal.controller,abnormal.service,dto,abnormal.service.spec}.ts`;注册 module。
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `PrismaService`、`Permissions.abnormal.*`、`ErrorCodes.ABNORMAL_*`、order(八态)、elder。
|
||||||
|
- Produces: `AbnormalService`(report/list)+ 扑空(not_met)流转。
|
||||||
|
|
||||||
|
- [ ] **Step 1: 写失败测试**(abnormal.service.spec.ts):异常上报分类必选其余可选;扑空(abnormal_type=not_met)→ order 转 not_met + 不产生 record + 到访时间/定位/原因必留;列表筛选。
|
||||||
|
|
||||||
|
- [ ] **Step 2: 跑测试确认失败**
|
||||||
|
|
||||||
|
- [ ] **Step 3: 实现 abnormal.service**
|
||||||
|
|
||||||
|
关键逻辑:
|
||||||
|
- `report(dto)`:`abnormal_type` 必填(其余可选);写 `record_abnormal`(record_id 或 order_id 可空)。
|
||||||
|
- 扑空分支:`dto.abnormalType === 'not_met'` 时 → `order.update({ orderStatus:'not_met', notMetReason: dto.abnormalDesc, notMetAt: dto.visitTime ?? now })`(visit_time/visit_lat/visit_lng/原因必留)→ 不产生 record。
|
||||||
|
- `list(q)`:按 abnormal_type/notify_status/handle_status/elder 筛选。
|
||||||
|
|
||||||
|
- [ ] **Step 4: 实现 controller + dto + module**(`@Controller('abnormal')`;`@AuditLog`;注册 module)
|
||||||
|
|
||||||
|
- [ ] **Step 5: 测试通过 + tsc + Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git -C sources/ec-nest add src/abnormal src/app.module.ts
|
||||||
|
git -C sources/ec-nest commit -m "feat(c3): 异常上报模块(9宫格/扑空转 not_met)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: 前端
|
||||||
|
|
||||||
|
**Files:** `src/api/record.ts`、`src/api/abnormal.ts`;`src/pages/ltc/record/index.vue`(替换占位);`src/pages/ltc/qc/index.vue`(异常列表,或独立);`src/pages/ltc/elder/` 扩展(垫付台账 + 结转入口)。
|
||||||
|
|
||||||
|
- [ ] **Step 1: api 封装**(`http` + 类型对齐后端 DTO;record 列表/详情/签到/签退/项目/照片/离线/垫付台账/结转 + abnormal 上报/列表)。
|
||||||
|
|
||||||
|
- [ ] **Step 2: 工单留痕页**(`defineOptions({name:'record'})`):列表(筛选老人/护理员/日期/状态)+ 详情(签到签退时间/时长/项目 exec_result/照片/异常/定位/垫付标记)。复用 ty-search-form + ty-table + showDialog。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 异常列表页**:分类/状态/关联老人 + 语音/照片入口。
|
||||||
|
|
||||||
|
- [ ] **Step 4: 服务对象页扩展**:垫付台账区块(is_prepaid 记录)+ 「结转垫付期服务」按钮(勾选 → `prepaidCarryover`)。
|
||||||
|
|
||||||
|
- [ ] **Step 5: type-check + Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git -C sources/ec-admin add src/api/record.ts src/api/abnormal.ts src/pages/ltc/record src/pages/ltc/qc src/pages/ltc/elder
|
||||||
|
git -C sources/ec-admin commit -m "feat(c3): 工单留痕页 + 异常列表 + 服务对象垫付结转"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: 端到端验证
|
||||||
|
|
||||||
|
- [ ] **Step 1:** 起后端 + 登录,冒烟:签到 → 项目 → 照片 → 签退(必拍 3001 硬拒)→ 详情。
|
||||||
|
- [ ] **Step 2:** waiting_period 老人签到/签退 → is_prepaid=1、billable=0。
|
||||||
|
- [ ] **Step 3:** 扑空:abnormal_type=not_met → 工单转 not_met + 不产生 record。
|
||||||
|
- [ ] **Step 4:** 结转:勾选垫付记录 → 正式记录生成,原始时间戳不变。
|
||||||
|
- [ ] **Step 5:** 离线补传 client_md5 去重幂等;审计落库。停后端清理。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-Review Notes
|
||||||
|
|
||||||
|
- Spec 覆盖:data-model 6 实体→T1;api-contract→T3/T4;backend/record→T3;backend/abnormal→T4;frontend/ad/record→T5。
|
||||||
|
- Review Focus 5 条均有测试(T3/T4)。
|
||||||
|
- 类型一致:`Record`/`RecordItem`/`RecordPhoto`/`RecordAbnormal`/`Settlement`/`Report` 模型名、`Permissions.record.*`/`abnormal.*`、`ErrorCodes.RECORD_*` 在 T1/T2/T3/T4 一致。
|
||||||
|
- 命名纪律:exec_result=undo_not_done;表名 ho_*;禁用 checkin/clockin/punch。
|
||||||
Loading…
x
Reference in New Issue
Block a user