4.0 KiB
架构决策:报告显式确认发送状态
日期: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。
固定迁移顺序中本脚本排在预约容量之后。生产只读预检新增三项不变量:
- 发送状态只能是
unknown|unsent|sent; sent必须有完整首次回执;unknown|unsent不得残留回执字段。
因此生产预检从 15 项增至 18 项;本批版本化迁移从四个增至五个。常规回滚仍只回应用,不自动删除新增列和索引。
6. 验收
- 新报告创建后为
unsent;历史迁移后为unknown。 - 复制链接、二维码、预览均不调用确认接口。
- 首次确认写回执和
report_sent;重复确认不覆盖、不重复写事件。 - customer 与跨店员工均不能确认。
- Admin 和门店小程序都要求用户选择真实发送方式并再次确认。
- 工作台只提醒
unsent;漏斗发送数来自report_sent。 - backend 全测、admin/H5/小程序构建及本体校验/漂移审计通过。