# 架构决策：报告显式确认发送状态

日期：2026-08-02  
状态：Accepted / Implemented（生产迁移待执行）  
Owner：Report Media Backend / Store Miniapp FE / Store Admin FE / Data Model  
Paired QA：Report Share QA

## 1. 决策背景

报告提交后，门店可以复制链接、复制话术、展示二维码或预览公开页，但这些动作都不能证明宠主已经收到。若把任一传播准备动作自动记为“已发送”，工作台待办、发送率与后续打开率都会被系统性高估，也无法支持真实门店试点。

当前系统没有微信消息投递回执，因此只能记录门店员工能够负责确认的业务事实：**宠主已通过某种方式实际收到报告**。

## 2. 决策

### 2.1 状态模型

`Report.send_status` 只有三态：

- `unknown`：迁移前历史报告，无法可靠判断是否发送；
- `unsent`：迁移后新报告，尚未被员工显式确认发送；
- `sent`：本店 boss/staff 已显式确认宠主收到。

历史报告统一回填 `unknown`，不回填 `report_sent` 事件，也不进入“报告尚未发送”行动组。迁移后新报告由应用和数据库默认值共同保证为 `unsent`。

### 2.2 首次确认回执

首次确认同时写入：

- `sent_at`：确认发生时间；
- `sent_by_user_id`：当前会话员工；
- `send_channel`：`wechat | qr | other`；
- 不可变 `BusinessEvent(event_type=report_sent)`，幂等键 `report_sent:{reportId}`。

报告行以悲观写锁串行确认。状态进入 `sent` 后不可撤销，也不可用后续点击覆盖首次时间、员工或渠道；重复确认返回成功并带 `alreadyConfirmed=true`，不重复保存、不重复发事件。

确认发送不改写 `Report.update_time`：该字段目前参与成片 `processing` 超时判断，发送回执以独立 `sent_at` 表达，避免传播动作重置成片异常计时。

### 2.3 不构成发送的动作

以下动作永远不自动改变发送状态：

- 复制公开链接；
- 复制话术；
- 生成或展示二维码；
- 预览或打开 H5；
- 宠主打开埋点。

产品文案不得宣称“微信发送成功”或第三方投递成功，只能表达“已记录员工确认”。

## 3. 权限与隐私

- `POST /api/report/confirm-sent` 是受保护接口，仅 boss/staff 可调用；`storeId/userId/role` 全部从会话派生。
- 服务层再次校验报告属于当前门店，跨店返回 403。
- 公开 token 报告响应不返回 `sendStatus/sentAt/sentByUserId/sendChannel`。
- `report_sent.metadata_json` 只保存低敏 `channel`，不保存 token、链接、手机号、微信标识或请求体。

## 4. 工作台与漏斗

- 工作台新增 `unsent_report`，只统计 `send_status=unsent`，按报告创建时间升序提醒；历史 `unknown` 不进入行动队列。
- `reportSentCount` 按当日 `report_sent` 的 report ID 去重，`reportSentTrackingReady=true`。
- 漏斗口径为 `report_submitted → report_sent → report_opened/reopened → lead_submitted`；再次预约仍保持未知，直到可靠归因模型落地。

## 5. 数据迁移与发布门禁

迁移脚本：`backend/db/migrations/20260801_create_report_send_status.sql`。

固定迁移顺序中本脚本排在预约容量之后。生产只读预检新增三项不变量：

1. 发送状态只能是 `unknown|unsent|sent`；
2. `sent` 必须有完整首次回执；
3. `unknown|unsent` 不得残留回执字段。

因此生产预检从 15 项增至 18 项；本批版本化迁移从四个增至五个。常规回滚仍只回应用，不自动删除新增列和索引。

## 6. 验收

- 新报告创建后为 `unsent`；历史迁移后为 `unknown`。
- 复制链接、二维码、预览均不调用确认接口。
- 首次确认写回执和 `report_sent`；重复确认不覆盖、不重复写事件。
- customer 与跨店员工均不能确认。
- Admin 和门店小程序都要求用户选择真实发送方式并再次确认。
- 工作台只提醒 `unsent`；漏斗发送数来自 `report_sent`。
- backend 全测、admin/H5/小程序构建及本体校验/漂移审计通过。
