petstore-docs/架构决策-FollowUpTask回访状态机与再次预约归因-2026-08-02.md

62 lines
4.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 架构决策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 --未接通/稍后联系--> pendingattempt_count + 1更新 due_date
in_progress --暂无意向/联系方式无效--> completedattempt_count + 1
in_progress --真实预约创建成功--> completed / rebookedattempt_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。