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