# 架构决策：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。
