touyun-elderly-care/docs/superpowers/specs/2026-10-15-服务机构端-C3履约留痕-design.md
liuyun 45aaeb9d43 docs: C3 留痕四表前缀改为 ho(用户指定)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-15 00:11:18 +08:00

8.4 KiB
Raw Permalink Blame History

服务机构端 C3 履约留痕 · 设计文档

本文是 C3 履约留痕的设计背景,规范权威见 openspec/changes/svc-org-c3-record/(proposal + specs + design + tasks)与 PRD《20261011-投云长护险管理系统-P0 PRD(交付开发·V1)》。

Context

C0–C2 已落地:elder(十态)、plan/plan_item、order(八态,含 not_met_reason/not_met_at)、elder_svc、staff、svc_org(L4)、排班派单。C3 新增履约留痕:护理员签到/签退/项目执行/拍照/异常上报 + 垫付台账与结转 + admin「工单留痕/异常列表」两页。录入主战场是护理员 APP(尚未开发),本期交付后端 record 模块 + admin Web,录入流程靠 API 冒烟验收(同 C2)。

Goals / Non-Goals

Goals:record/record_item/record_photo/record_abnormal 四表 + 录入 API(签到/项目/照片/签退/离线补传/详情)+ 异常上报/列表 + 垫付台账与结转(is_prepaid)+ admin 工单留痕/异常列表两页 + settlement/report 二期预留建空表。

Non-Goals:质控放行/退回(qc 表 + 4001 自审自放行,C4)、补正(质控退回联动,C4)、离线队列+自动补传(APP 期,本期只做 client_md5 去重)、家属端、结算对账(二期)。

Decisions

  1. 表命名:ho_record/ho_record_item/ho_record_photo/ho_record_abnormal(前缀 ho,禁用 checkin/clockin/punch)。settlement/report 为二期预留、本期建空表不写业务数据。qc 表 C4 建。
  2. 模块划分:src/record/(record + item + photo + 签到/签退/离线)+ src/abnormal/(异常有独立生命周期 notify/handle,供 C4 质控联动)。垫付结转并入 record 模块。
  3. record 生命周期:serving(签到后)→ submitted(签退后);returned/passed 留 C4 质控。
  4. 服务器取时:checkin_time/checkout_time/duration_min 一律系统取,接口不接受客户端传入(防补录时间)。submit_time 系统取。
  5. is_prepaid:record 创建时若 elder.treat_status = waiting_period → is_prepaid=1、billable=0(不计费、不进上报池,L1 例外不被误拦)。
  6. 结转:待遇转「正常享受」后,档案页「结转垫付期服务」按钮 → 勾选若干 is_prepaid=1 record → 生成对应正式 record(复制,is_prepaid=0 + 来源标记),原垫付记录永久保留只读、不改原始时间戳(签到时间/定位/照片仍是原始值)。
  7. 扑空(未遇):异常上报专门子类 → 工单转 not_met(not_met_reason/not_met_at)+ 写 record_abnormal(abnormal_type=扑空分类,含到访时间/定位/原因)→ 不产生 record、不计费。与「逾期未做」严格区分。
  8. exec_result 枚举:done/undo_not_done(本次未执行)/undo_refused(老人拒绝)——措辞纪律。
  9. 照片:复用 C1 MinIO StorageService,压缩后 ≤500KB,visible_to_family 恒 0,keep_until 补传后 ≥7 天。
  10. 必拍未拍:签退硬拒 3001 + 拒拍出路(is_refused_photo=1+原因,放行)。
  11. client_md5 去重:离线补传按 client_md5 幂等,静默不弹窗。
  12. 主键口径:全站已用 UUID v7 @db.VarChar(36)(PRD 写 varchar(32)),C3 循代码库口径 VarChar(36)(字段一致唯一锚点)。

数据模型(DDL 概览)

通用:UUID v7、tenant_code、@map snake_case、utf8mb4_general_ci。record 入 SOFT_DELETE;record_item/record_photo/record_abnormal 为子表(循 ElderStatusLog/PlanItem 先例:带 tenant_code、无 deleted_at、不入 SOFT_DELETE)。

  • record(ho_record):id、tenant_code、order_id、elder_id、assignee_id、checkin_time、checkout_time、duration_min、checkin_lat、checkin_lng、locate_status(ok/failed/denied)、record_status(serving/submitted)、voice_file、offline_flag、client_md5、submit_time、actual_duration_tier、biz_line(恒 ltci)、is_prepaid、billable、source_prepaid_of(结转来源标记)、时间戳 + deleted_at。索引 (tenant_code, order_id)、(tenant_code, elder_id)、(tenant_code, assignee_id, checkin_time)。
  • record_item:id、tenant_code、record_id、svc_id、svc_name_snapshot、exec_result(done/undo_not_done/undo_refused)、undo_reason、extra_flag、photo_required、is_refused_photo、refuse_reason。索引 (record_id)。
  • record_photo:id、tenant_code、record_id、svc_id、file_path、file_size、visible_to_family(恒 0)、upload_batch、keep_until。索引 (record_id)。
  • record_abnormal:id、tenant_code、record_id(可空,扑空时无)、order_id(可空,扑空时指向工单)、elder_id、reporter_id、abnormal_type(必选)、abnormal_desc、voice_file、has_photo、notify_status、handle_status、handle_result、link_plan_change。索引 (tenant_code, elder_id)、(tenant_code, notify_status)。
  • settlement(预留空表):id、tenant_code、settle_period、amount_total、amount_passed、amount_rejected、amount_fund、amount_self_pay、submit_date、pay_date、exception_reason、handle_deadline。
  • report(预留空表):id、tenant_code、record_id、order_id、report_status、report_time、official_no、loc_code、nat_code_snapshot、adapter_result。

表前缀(定):留痕四表前缀 ho(用户指定)——ho_record/ho_record_item/ho_record_photo/ho_record_abnormal;settlement/report 为二期预留,无前缀(与 PRD §7.3 一致)。

接口(@Controller('record') / @Controller('abnormal'))

record

  • POST /record/checkin — 签到:校验 L3(order 存在、order_status ∈ {assigned, serving}、elder/assignee 匹配)→ 系统取 checkin_time → 定位可选(失败不阻塞 3002)→ 建 record(serving) → 返回 record_id。
  • POST /record/item/save — 保存项目执行:record_id + items[](svc_id 落本机构 L4、exec_result 枚举)。
  • POST /record/photo/upload — 上传照片:record_id + svc_id + file(≤500KB)→ MinIO → 返回 photo_id。
  • POST /record/checkout — 签退提交:record_id → 必拍未拍硬拒 3001(+拒拍出路)→ 系统取 checkout_time + duration_min → record_status=submitted。
  • POST /record/offline/sync — 离线补传:client_md5 去重幂等。
  • GET /record/detail — 详情。
  • GET /record/list — 工单留痕列表(admin B3,质控员/院长;筛选 elder/assignee/日期/状态)。
  • GET /record/prepaid-list — 垫付台账列表(档案页,按 elderId 查 is_prepaid=1)。
  • POST /record/prepaid/carryover — 结转:勾选 record_ids → 生成正式 record(复制、is_prepaid=0、source_prepaid_of=原 id)→ 原记录保留只读。

abnormal

  • POST /abnormal/report — 异常上报:abnormal_type 必选,其余可选;可关联 record 或 order(扑空)。
  • GET /abnormal/list — 异常列表(admin B4)。

权限码:record.checkin/itemSave/photoUpload/checkout/offlineSync/detail/list/prepaidList/prepaidCarryover、abnormal.report/list。错误码域 45(RECORD_* / ABNORMAL_*);规则号 3001/3002/3003/3004 写 message 文本(循 C2 R3 口径)。

硬锁与校验

  • L3:留痕必须关联有效工单(order 存在且非 cancelled/not_met/done)。
  • L1 例外:waiting_period → is_prepaid=1、billable=0(不误拦)。
  • 必拍未拍 → 3001 硬拒;定位失败 → 3002 标记不阻塞;垫付 → 3004 标记不计费;扑空 → 3003 状态变更。
  • 服务器取时:接口不接受 checkin_time/checkout_time/duration_min。

Risks / Trade-offs

  • [离线补传幂等] → client_md5 唯一约束 + upsert;APP 期补本地队列。
  • [扑空 vs 逾期未做] → 扑空有到访时间/定位/原因佐证;逾期未做=到点无记录(C4 质控发现)。
  • [结转=复制而非改标记] → 两套并存,合规(不自动算钱);代价=存储翻倍但可追溯。
  • [必拍档项目清单的 source of truth] → photo_tier 来自 plan_item/order.item_list 快照;C3 落 record_item.photo_required 快照。

Migration Plan

新增 6 表(record/record_item/record_photo/record_abnormal + settlement/report 预留)。utf8mb4_general_ci 手改。⚠️ 迁移链已损坏(tech-debt):migrate dev 不可用,用 migrate diff --from-schema-datasource + 手工迁移目录 + migrate deploy。