62 lines
4.3 KiB
Markdown
62 lines
4.3 KiB
Markdown
# 架构决策:FollowUpTask 回访状态机与再次预约归因
|
||
|
||
> 日期:2026-08-02
|
||
> 状态:Accepted / Code Complete
|
||
> 本体:`entity:follow_up_task`、`rule:BR-FU-001`、`rule:BR-FU-002`
|
||
|
||
## 1. 决策
|
||
|
||
`ReportLead` 继续保存宠主同意、来源报告、建议日期与退订凭证等留资事实;门店员工的领取、联系、改期、关闭和再次预约改由独立 `FollowUpTask` 承载。不得继续用 `ReportLead.remind_status` 同时表达同意事实和多人协作状态。
|
||
|
||
留资成功后,同一 `sourceLeadId` 至多存在一个开放任务。重复提交复用开放任务并同步建议日期;上一轮已终结后再次提交,创建新的任务轮次。历史迁移只为 `remind_status=pending` 的线索回填任务,不推测 `sent/canceled/unsubscribed` 的真实处理结果。
|
||
|
||
`ReportLead.remind_status` 暂保留为兼容投影:新建或改期为 `pending`,无意向/无效联系方式为 `canceled`,真实再次预约为 `sent`,退订为 `unsubscribed`。Admin 回访工作流与经营统计不再以该兼容字段为事实源。
|
||
|
||
## 2. 状态机
|
||
|
||
```text
|
||
pending --领取--> in_progress
|
||
in_progress --未接通/稍后联系--> pending(attempt_count + 1,更新 due_date)
|
||
in_progress --暂无意向/联系方式无效--> completed(attempt_count + 1)
|
||
in_progress --真实预约创建成功--> completed / rebooked(attempt_count + 1)
|
||
pending|in_progress --宠主退订--> canceled / unsubscribed(不增加员工联系次数)
|
||
```
|
||
|
||
- `pending` 不允许有领取人;`in_progress` 必须有领取人。
|
||
- 只有领取该任务的当前员工才能改期、关闭或发起再次预约。
|
||
- 两名员工并发领取时通过悲观锁和 `@Version` 防止静默覆盖;他人已领取返回 409。
|
||
- `completed/canceled` 为终态,不回退。跨店任务按不存在返回 404。
|
||
- `no_answer/follow_later` 仅是非终态联系结果;`rebooked/not_interested/invalid_contact/unsubscribed` 仅是终态结果。
|
||
|
||
## 3. 真实再次预约归因
|
||
|
||
`rebooked` 不是员工可手工选择的关闭结果。只有 `POST /api/appointment/create` 在同一事务中满足下列条件,才能写入:
|
||
|
||
1. `followUpTaskId` 属于当前门店,处于 `in_progress` 且由当前员工领取;
|
||
2. 任务的 canonical `StoreCustomer` 与本次代客预约解析出的客户一致;
|
||
3. 预约通过未来时间、服务时长和每个半小时桶容量校验并真实保存;
|
||
4. 保存后任务写 `rebooked_appointment_id`,同时落 `follow_up_completed` 与 `rebook_created` 事实。
|
||
|
||
无号源、客户不匹配、状态冲突或预约保存失败时,整笔事务回滚,任务保持处理中。漏斗中的“回访后再次预约”只统计 `rebook_created`,不从手机号相同或后续页面点击推断。
|
||
|
||
## 4. 权限、隐私与审计
|
||
|
||
- `GET/POST /api/admin/follow-up-tasks/**` 仅本店 boss/staff,`storeId` 和操作人均从 session 派生。
|
||
- 同店已认证员工为了实际电话联系可在开放任务中看到完整手机号;终态任务只返回脱敏手机号。工作台、客户时间线、BusinessEvent 和 AuditLog 只保留脱敏或低敏字段。
|
||
- BusinessEvent metadata 仅允许 `dueDate/attemptOutcome/outcome/attemptCount/bookingOrigin` 等白名单标量,不存手机号、备注、微信标识、token、IP 或请求体。
|
||
- 领取、改期、关闭和真实再次预约均写不可变低敏 AuditLog;退订由公开凭证触发,不伪装成员工联系次数或操作人。
|
||
|
||
## 5. 迁移与验收
|
||
|
||
迁移:`backend/db/migrations/20260802_create_follow_up_task.sql`,固定在 StoreCustomer 时间线迁移后第八个执行。末尾五项验证必须为 0:
|
||
|
||
1. 待处理历史线索无开放任务;
|
||
2. 任务客户归属或 canonical 范围无效;
|
||
3. 状态、结果、领取人、联系回执或真实预约引用不一致;
|
||
4. 同一线索存在多个开放任务;
|
||
5. 任务缺少创建事实。
|
||
|
||
生产只读预检同步检查上述不变量,并额外要求 `rebooked_appointment_id` 指向同店、未删除的真实预约以及对应 `rebook_created` 事件。
|
||
|
||
自动化验收覆盖:历史回填、状态机、并发领取冲突、跨店 404、非领取人拒绝、退订取消、真实预约成功闭环、容量失败不闭环、客户时间线低敏投影、Admin 控制器权限与 production build。
|