docs: define explicit report delivery state

This commit is contained in:
malei 2026-08-02 00:15:00 +08:00
parent 05f7c9c7ec
commit 66b844a1c6
21 changed files with 255 additions and 41 deletions

View File

@ -61,6 +61,7 @@
| **A14** | **StoreCustomer 门店客户主档**:客户列表不再临时拼成无 ID 投影 | 预约/留资自动建档同店账号与手机号唯一API 返回 storeCustomerId 且手机脱敏;历史数据可回填 | Backend Core / Report Media Backend / Data Model | ✅ 已实现(待生产迁移) | `mvn -f backend/pom.xml test` + `npm --prefix admin run build` | 迁移见 `backend/db/migrations/20260801_create_store_customer.sql` | | **A14** | **StoreCustomer 门店客户主档**:客户列表不再临时拼成无 ID 投影 | 预约/留资自动建档同店账号与手机号唯一API 返回 storeCustomerId 且手机脱敏;历史数据可回填 | Backend Core / Report Media Backend / Data Model | ✅ 已实现(待生产迁移) | `mvn -f backend/pom.xml test` + `npm --prefix admin run build` | 迁移见 `backend/db/migrations/20260801_create_store_customer.sql` |
| **A15** | **BusinessEvent 业务事件落库**:经营漏斗不再依赖日志或前端猜测 | 首批 7 类事实统一落库稳定事件幂等metadata 不含 token/手机号/IP工作台已打开可统计历史可靠事实可回填 | Backend Core / Report Media Backend / Data Model | ✅ 已实现(待生产迁移) | `mvn -f backend/pom.xml test` + `npm --prefix admin run build` + 本体漂移审计 | 迁移见 `backend/db/migrations/20260801_create_business_event.sql` | | **A15** | **BusinessEvent 业务事件落库**:经营漏斗不再依赖日志或前端猜测 | 首批 7 类事实统一落库稳定事件幂等metadata 不含 token/手机号/IP工作台已打开可统计历史可靠事实可回填 | Backend Core / Report Media Backend / Data Model | ✅ 已实现(待生产迁移) | `mvn -f backend/pom.xml test` + `npm --prefix admin run build` + 本体漂移审计 | 迁移见 `backend/db/migrations/20260801_create_business_event.sql` |
| **A16** | **真实预约容量与连续排班占用**:服务时长、门店并发容量和预约时长快照共同决定号源 | 长服务跨半小时桶占号;`walk_in` 消耗一份容量,`blocked` 关闭全部;并发创建不超卖 | Backend Core / Store Admin FE / Data Model | ✅ 已实现(待生产迁移) | backend 全测 + admin/H5/小程序构建 + 本体漂移审计 | 迁移见 `backend/db/migrations/20260801_create_booking_capacity.sql`;不等同排队/准点承诺 | | **A16** | **真实预约容量与连续排班占用**:服务时长、门店并发容量和预约时长快照共同决定号源 | 长服务跨半小时桶占号;`walk_in` 消耗一份容量,`blocked` 关闭全部;并发创建不超卖 | Backend Core / Store Admin FE / Data Model | ✅ 已实现(待生产迁移) | backend 全测 + admin/H5/小程序构建 + 本体漂移审计 | 迁移见 `backend/db/migrations/20260801_create_booking_capacity.sql`;不等同排队/准点承诺 |
| **A17** | **报告显式确认发送状态**:传播动作与发送事实分离 | 历史为 `unknown`;新报告为 `unsent`;首次确认写不可覆盖回执与幂等 `report_sent`;工作台仅提醒新报告待发送 | Report Media Backend / Store Miniapp FE / Store Admin FE / Data Model | ✅ 已实现(待生产迁移) | backend 全测 + admin/H5/小程序构建 + 本体漂移审计 | 迁移见 `backend/db/migrations/20260801_create_report_send_status.sql`;不宣称微信投递成功 |
--- ---
@ -69,7 +70,7 @@
| ID | 需求 | 验收标准 | Owner | 状态 | 验收命令 | 备注 | | ID | 需求 | 验收标准 | Owner | 状态 | 验收命令 | 备注 |
|----|------|----------|-------|------|----------|------| |----|------|----------|-------|------|----------|------|
| **B1** | **「已完成」Tab 列表默认不含已取消** | Tab「已完成」下默认仅展示 `status===done``cancel` 通过次级筛选或单独子 TAB 实现 | Store Miniapp FE | 🟡 部分实现(以现状为准做回归) | `npm --prefix frontend run build:h5` + 手工 smoke | 当前 `Home.vue` 需对照 | | **B1** | **「已完成」Tab 列表默认不含已取消** | Tab「已完成」下默认仅展示 `status===done``cancel` 通过次级筛选或单独子 TAB 实现 | Store Miniapp FE | 🟡 部分实现(以现状为准做回归) | `npm --prefix frontend run build:h5` + 手工 smoke | 当前 `Home.vue` 需对照 |
| **B2** | **发送报告成功页**:同屏 报告链接 + 二维码 + 两条可复制话术 | 话术支持变量:`{宠物名}{服务类型}{门店名}{链接}`;一键复制单条话术 | Store Miniapp FE | ✅ 已实现(`ReportShareModal.vue` | `npm --prefix frontend run build:h5` + 手工 smoke | 对齐《本期主线》A | | **B2** | **发送报告成功页**:同屏 报告链接 + 二维码 + 两条可复制话术 + 显式确认发送 | 话术支持变量;复制/预览不自动确认;宠主实际收到后选择微信/扫码/其他并确认 | Store Miniapp FE | ✅ 已实现(`ReportShareModal.vue` | `npm --prefix frontend run build:h5` + 手工 smoke | 对齐 A17不得把剪贴板动作冒充发送 |
| **B3** | **报告提交锁死提示** | 提交前弹窗固定文案:「提交后不可修改,请确认照片与备注无误」 | Store Miniapp FE | 🟡 部分实现:核对文案与位置 | 手工 smoke | 与产品 v1 锁死策略一致 | | **B3** | **报告提交锁死提示** | 提交前弹窗固定文案:「提交后不可修改,请确认照片与备注无误」 | Store Miniapp FE | 🟡 部分实现:核对文案与位置 | 手工 smoke | 与产品 v1 锁死策略一致 |
| **B4** | **「我的」老板端分组标题** | 至少两组:`店铺与经营` / `团队与账号`;高频在上;使用 `.module-title` | Store Miniapp FE | ✅ 已实现 | `npm --prefix frontend run build:h5` + 手工 smoke | 见《产品全方位优化说明》§2.2 | | **B4** | **「我的」老板端分组标题** | 至少两组:`店铺与经营` / `团队与账号`;高频在上;使用 `.module-title` | Store Miniapp FE | ✅ 已实现 | `npm --prefix frontend run build:h5` + 手工 smoke | 见《产品全方位优化说明》§2.2 |
| **B5** | **报告前后照片必填前端校验** | 无 before 提示「请至少上传1张服务前照片」无 after 提示「请至少上传1张服务后照片」 | Store Miniapp FE / Customer Experience FE | ✅ 已实现Batch A | `npm --prefix frontend run build:h5` + `npm --prefix frontend run build:mp-weixin` | 对应 A7、P0-4 | | **B5** | **报告前后照片必填前端校验** | 无 before 提示「请至少上传1张服务前照片」无 after 提示「请至少上传1张服务后照片」 | Store Miniapp FE / Customer Experience FE | ✅ 已实现Batch A | `npm --prefix frontend run build:h5` + `npm --prefix frontend run build:mp-weixin` | 对应 A7、P0-4 |
@ -131,7 +132,7 @@
| ID | 需求 | 验收标准 | Owner | 状态 | 验收命令 | 备注 | | ID | 需求 | 验收标准 | Owner | 状态 | 验收命令 | 备注 |
|----|------|----------|-------|------|----------|------| |----|------|----------|-------|------|----------|------|
| **H1** | **后端可重复发布门禁** | README + production profile + 配置校验 + schema/15 项只读预检 + liveness/readiness + 只读 smoke + systemd/Nginx 模板 | Backend Ops | ✅ 仓库能力已实现;待真实环境执行 | backend 全测/打包 + `bash -n deploy/*.sh` + Runbook | 不把模板/本地通过写成已上线 | | **H1** | **后端可重复发布门禁** | README + production profile + 配置校验 + schema/18 项只读预检 + liveness/readiness + 只读 smoke + systemd/Nginx 模板 | Backend Ops | ✅ 仓库能力已实现;待真实环境执行 | backend 全测/打包 + `bash -n deploy/*.sh` + Runbook | 不把模板/本地通过写成已上线 |
| **H2** | **前端可重复发布门禁** | admin/H5/小程序真实域名配置先门禁再构建;仓库占位值不能进入正式包;微信合法域名、体验版、静态回滚路径明确 | Frontend Release Ops | ✅ 仓库能力已实现;待真实环境执行 | admin/frontend preflight + 三端构建 + Runbook | 真实配置不得提交 | | **H2** | **前端可重复发布门禁** | admin/H5/小程序真实域名配置先门禁再构建;仓库占位值不能进入正式包;微信合法域名、体验版、静态回滚路径明确 | Frontend Release Ops | ✅ 仓库能力已实现;待真实环境执行 | admin/frontend preflight + 三端构建 + Runbook | 真实配置不得提交 |
| **H3** | **P0 通过条件** | 见下方「P0 通过条件」块 | System Architect / Core Flow QA | 🟡 待人工 smoke 复核 | 见下方 | 对应 P0-10 | | **H3** | **P0 通过条件** | 见下方「P0 通过条件」块 | System Architect / Core Flow QA | 🟡 待人工 smoke 复核 | 见下方 | 对应 P0-10 |

View File

@ -104,7 +104,7 @@ python3 docs/graph/audit_drift.py
| ~~高~~ | ~~寄语幂等独立用例~~ | ✅ `ReportTestimonialServiceTest` | | ~~高~~ | ~~寄语幂等独立用例~~ | ✅ `ReportTestimonialServiceTest` |
| ~~中~~ | ~~`rule:BR-STORE-003` 公开 Store 字段裁剪~~ | ✅ `toPublicStoreView` | | ~~中~~ | ~~`rule:BR-STORE-003` 公开 Store 字段裁剪~~ | ✅ `toPublicStoreView` |
| 中 | `uk_report_appointment` 上线迁移 | 🟡 运维 | | 中 | `uk_report_appointment` 上线迁移 | 🟡 运维 |
| ~~中~~ | ~~预约/履约/报告打开/留资事件可统计落地~~ | ✅ BusinessEvent成片/播放/保存事件另行接入 | | ~~中~~ | ~~预约/履约/报告提交/显式发送/打开/留资事件可统计落地~~ | ✅ BusinessEvent成片/播放/保存事件另行接入 |
| 中 | production profile + 只读预检在真实生产配置实测 | 🟡 发布门禁 | | 中 | production profile + 只读预检在真实生产配置实测 | 🟡 发布门禁 |
| 低 | 成片真 FFmpeg 端到端集成测(本机有 ffmpeg | 🟡 单测已覆盖状态机与归类 | | 低 | 成片真 FFmpeg 端到端集成测(本机有 ffmpeg | 🟡 单测已覆盖状态机与归类 |
| 低 | `action:logout` / `event:session_revoked` 服务端吊销 | 🟡 gap | | 低 | `action:logout` / `event:session_revoked` 服务端吊销 | 🟡 gap |

View File

@ -1,7 +1,7 @@
{"id":"entity:store","type":"Entity","name":"Store","nameZh":"门店","domain":"store","fields":["id","name","logo","phone","address","latitude","longitude","intro","owner_id","invite_code","booking_day_start","booking_last_slot_start","booking_capacity","create_time","update_time","deleted"],"systems":["Backend Core"],"code":"backend/src/main/java/com/petstore/entity/Store.java","doc":"docs/产品设计文档.md §三 t_store","test":"StoreControllerTest","evidence":"anchored"} {"id":"entity:store","type":"Entity","name":"Store","nameZh":"门店","domain":"store","fields":["id","name","logo","phone","address","latitude","longitude","intro","owner_id","invite_code","booking_day_start","booking_last_slot_start","booking_capacity","create_time","update_time","deleted"],"systems":["Backend Core"],"code":"backend/src/main/java/com/petstore/entity/Store.java","doc":"docs/产品设计文档.md §三 t_store","test":"StoreControllerTest","evidence":"anchored"}
{"id":"entity:user","type":"Entity","name":"User","nameZh":"用户","domain":"user","fields":["id","username","password","name","phone","avatar","store_id","role","wechat_openid","wechat_unionid","create_time","update_time","deleted"],"systems":["Backend Core"],"code":"backend/src/main/java/com/petstore/entity/User.java","doc":"docs/产品设计文档.md §三 t_user; docs/.agents/common-context.md 术语规则","test":"UserServiceLoginStrategyTest; UserJsonIgnoreTest","evidence":"anchored"} {"id":"entity:user","type":"Entity","name":"User","nameZh":"用户","domain":"user","fields":["id","username","password","name","phone","avatar","store_id","role","wechat_openid","wechat_unionid","create_time","update_time","deleted"],"systems":["Backend Core"],"code":"backend/src/main/java/com/petstore/entity/User.java","doc":"docs/产品设计文档.md §三 t_user; docs/.agents/common-context.md 术语规则","test":"UserServiceLoginStrategyTest; UserJsonIgnoreTest","evidence":"anchored"}
{"id":"entity:appointment","type":"Entity","name":"Appointment","nameZh":"预约","domain":"appointment","fields":["id","pet_name","pet_type","service_type","appointment_time","duration_minutes","status","store_id","user_id","customer_user_id","created_by_user_id","assigned_user_id","pet_id","remark","create_time","update_time","deleted"],"systems":["Backend Core"],"code":"backend/src/main/java/com/petstore/entity/Appointment.java; backend/src/main/java/com/petstore/service/AppointmentService.java","doc":"docs/产品设计文档.md §三 t_appointment §5.2; docs/架构决策-真实预约容量模型-2026-08-01.md","test":"AppointmentServiceTest; BookingCapacityServiceTest; AppointmentControllerDetailTest; AppointmentControllerIdentityTest; BookingCapacityMigrationTest","evidence":"anchored"} {"id":"entity:appointment","type":"Entity","name":"Appointment","nameZh":"预约","domain":"appointment","fields":["id","pet_name","pet_type","service_type","appointment_time","duration_minutes","status","store_id","user_id","customer_user_id","created_by_user_id","assigned_user_id","pet_id","remark","create_time","update_time","deleted"],"systems":["Backend Core"],"code":"backend/src/main/java/com/petstore/entity/Appointment.java; backend/src/main/java/com/petstore/service/AppointmentService.java","doc":"docs/产品设计文档.md §三 t_appointment §5.2; docs/架构决策-真实预约容量模型-2026-08-01.md","test":"AppointmentServiceTest; BookingCapacityServiceTest; AppointmentControllerDetailTest; AppointmentControllerIdentityTest; BookingCapacityMigrationTest","evidence":"anchored"}
{"id":"entity:report","type":"Entity","name":"Report","nameZh":"服务报告","domain":"report","fields":["id","appointment_id","remark","user_id","customer_user_id","author_staff_id","store_id","report_token","pet_name","service_type","appointment_time","staff_name","highlight_video_url","highlight_video_status","highlight_video_error","highlight_duration_sec","highlight_compose_mode","highlight_fail_reason","highlight_share_cover_url","create_time","update_time","deleted"],"systems":["Report Media Backend"],"code":"backend/src/main/java/com/petstore/entity/Report.java; backend/src/main/java/com/petstore/service/ReportService.java","doc":"docs/产品设计文档.md §三 t_report §5.3; docs/API-报告成片字段契约.md","test":"ReportServiceTest; ReportControllerTest","evidence":"anchored"} {"id":"entity:report","type":"Entity","name":"Report","nameZh":"服务报告","domain":"report","fields":["id","appointment_id","remark","user_id","customer_user_id","author_staff_id","store_id","report_token","pet_name","service_type","appointment_time","staff_name","send_status","sent_at","sent_by_user_id","send_channel","highlight_video_url","highlight_video_status","highlight_video_error","highlight_duration_sec","highlight_compose_mode","highlight_fail_reason","highlight_share_cover_url","create_time","update_time","deleted"],"systems":["Report Media Backend","Store Miniapp FE","Store Admin FE"],"code":"backend/src/main/java/com/petstore/entity/Report.java; backend/src/main/java/com/petstore/service/ReportService.java","doc":"docs/产品设计文档.md §三 t_report §5.3; docs/架构决策-报告确认发送状态-2026-08-02.md","test":"ReportServiceTest; ReportControllerTest; ReportSendMigrationTest","evidence":"anchored"}
{"id":"entity:report_lead","type":"Entity","name":"ReportLead","nameZh":"留资线索","domain":"lead","fields":["id","report_id","store_id","phone","pet_name","service_type","wechat_openid","wechat_unionid","reminder_type","remind_date","remind_status","unsubscribe_token","client_ip","create_time","update_time","deleted"],"systems":["Report Media Backend"],"code":"backend/src/main/java/com/petstore/controller/ReportLeadController.java; backend/src/main/java/com/petstore/entity/ReportLead.java","doc":"docs/本期主线-下一步产品优化点.md §C 留资","test":"ReportLeadControllerTest","evidence":"anchored"} {"id":"entity:report_lead","type":"Entity","name":"ReportLead","nameZh":"留资线索","domain":"lead","fields":["id","report_id","store_id","phone","pet_name","service_type","wechat_openid","wechat_unionid","reminder_type","remind_date","remind_status","unsubscribe_token","client_ip","create_time","update_time","deleted"],"systems":["Report Media Backend"],"code":"backend/src/main/java/com/petstore/controller/ReportLeadController.java; backend/src/main/java/com/petstore/entity/ReportLead.java","doc":"docs/本期主线-下一步产品优化点.md §C 留资","test":"ReportLeadControllerTest","evidence":"anchored"}
{"id":"entity:business_event","type":"Entity","name":"BusinessEvent","nameZh":"不可变业务事件","domain":"platform","jpa":true,"fields":["id","event_id","event_type","event_version","store_id","store_customer_id","aggregate_type","aggregate_id","actor_user_id","actor_role","source","occurred_at","metadata_json","idempotency_key","create_time"],"systems":["Backend Core","Report Media Backend","Store Admin FE"],"code":"backend/src/main/java/com/petstore/entity/BusinessEvent.java; backend/src/main/java/com/petstore/service/BusinessEventService.java","doc":"docs/ontology/objects.md#entity:business_event; docs/架构决策-业务事件落库-2026-08-01.md","test":"BusinessEventServiceTest; BusinessEventMigrationTest","evidence":"anchored"} {"id":"entity:business_event","type":"Entity","name":"BusinessEvent","nameZh":"不可变业务事件","domain":"platform","jpa":true,"fields":["id","event_id","event_type","event_version","store_id","store_customer_id","aggregate_type","aggregate_id","actor_user_id","actor_role","source","occurred_at","metadata_json","idempotency_key","create_time"],"systems":["Backend Core","Report Media Backend","Store Admin FE"],"code":"backend/src/main/java/com/petstore/entity/BusinessEvent.java; backend/src/main/java/com/petstore/service/BusinessEventService.java","doc":"docs/ontology/objects.md#entity:business_event; docs/架构决策-业务事件落库-2026-08-01.md","test":"BusinessEventServiceTest; BusinessEventMigrationTest","evidence":"anchored"}
{"id":"action:register_boss","type":"Action","name":"registerBoss","nameZh":"老板入驻","inputs":["storeName","bossName","phone","password"],"outputs":["entity:store","entity:user","session_token"],"entrypoints":["POST /api/user/register-boss","UserService#registerBoss"],"appliesRules":["rule:BR-AUTH-001"],"code":"backend/src/main/java/com/petstore/service/UserService.java#registerBoss","evidence":"anchored","domain":"user"} {"id":"action:register_boss","type":"Action","name":"registerBoss","nameZh":"老板入驻","inputs":["storeName","bossName","phone","password"],"outputs":["entity:store","entity:user","session_token"],"entrypoints":["POST /api/user/register-boss","UserService#registerBoss"],"appliesRules":["rule:BR-AUTH-001"],"code":"backend/src/main/java/com/petstore/service/UserService.java#registerBoss","evidence":"anchored","domain":"user"}
@ -17,6 +17,7 @@
{"id":"action:get_appointment_detail","type":"Action","name":"getAppointmentDetail","nameZh":"预约详情","inputs":["id"],"outputs":["entity:appointment"],"entrypoints":["GET /api/appointment/detail","AppointmentController#detail"],"appliesRules":["rule:BR-APPT-002","rule:BR-APPT-003"],"code":"backend/src/main/java/com/petstore/controller/AppointmentController.java#detail","test":"AppointmentControllerDetailTest","evidence":"anchored","domain":"appointment"} {"id":"action:get_appointment_detail","type":"Action","name":"getAppointmentDetail","nameZh":"预约详情","inputs":["id"],"outputs":["entity:appointment"],"entrypoints":["GET /api/appointment/detail","AppointmentController#detail"],"appliesRules":["rule:BR-APPT-002","rule:BR-APPT-003"],"code":"backend/src/main/java/com/petstore/controller/AppointmentController.java#detail","test":"AppointmentControllerDetailTest","evidence":"anchored","domain":"appointment"}
{"id":"action:list_appointments","type":"Action","name":"listAppointments","nameZh":"预约列表","inputs":["status","page","pageSize"],"outputs":["entity:appointment"],"entrypoints":["GET /api/appointment/list","AppointmentController#list"],"appliesRules":["rule:BR-APPT-002","rule:BR-APPT-003"],"code":"backend/src/main/java/com/petstore/controller/AppointmentController.java#list","evidence":"anchored","domain":"appointment"} {"id":"action:list_appointments","type":"Action","name":"listAppointments","nameZh":"预约列表","inputs":["status","page","pageSize"],"outputs":["entity:appointment"],"entrypoints":["GET /api/appointment/list","AppointmentController#list"],"appliesRules":["rule:BR-APPT-002","rule:BR-APPT-003"],"code":"backend/src/main/java/com/petstore/controller/AppointmentController.java#list","evidence":"anchored","domain":"appointment"}
{"id":"action:submit_report","type":"Action","name":"submitReport","nameZh":"提交报告","inputs":["appointmentId","remark","images"],"outputs":["reportToken","reportId"],"entrypoints":["POST /api/report/create","ReportController#create","ReportService#create"],"appliesRules":["rule:BR-RPT-001","rule:BR-RPT-002","rule:BR-RPT-003","rule:BR-RPT-004","rule:BR-RPT-005","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#create; backend/src/main/java/com/petstore/service/ReportService.java#create; backend/src/main/java/com/petstore/service/BusinessEventService.java#recordReportSubmitted","test":"ReportControllerTest; ReportServiceTest; BusinessEventServiceTest","evidence":"anchored","domain":"report","note":"authorStaffId 从登录上下文派生customerUserId 从预约派生;报告提交与事件同事务"} {"id":"action:submit_report","type":"Action","name":"submitReport","nameZh":"提交报告","inputs":["appointmentId","remark","images"],"outputs":["reportToken","reportId"],"entrypoints":["POST /api/report/create","ReportController#create","ReportService#create"],"appliesRules":["rule:BR-RPT-001","rule:BR-RPT-002","rule:BR-RPT-003","rule:BR-RPT-004","rule:BR-RPT-005","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#create; backend/src/main/java/com/petstore/service/ReportService.java#create; backend/src/main/java/com/petstore/service/BusinessEventService.java#recordReportSubmitted","test":"ReportControllerTest; ReportServiceTest; BusinessEventServiceTest","evidence":"anchored","domain":"report","note":"authorStaffId 从登录上下文派生customerUserId 从预约派生;报告提交与事件同事务"}
{"id":"action:confirm_report_sent","type":"Action","name":"confirmReportSent","nameZh":"显式确认报告已发送","inputs":["reportId","channel","entity:current_user"],"outputs":["sendStatus","sentAt","sendChannel","alreadyConfirmed","entity:business_event"],"entrypoints":["POST /api/report/confirm-sent","ReportController#confirmSent","ReportService#confirmSent"],"appliesRules":["rule:BR-RPT-005","rule:BR-RPT-008","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#confirmSent; backend/src/main/java/com/petstore/service/ReportService.java#confirmSent; backend/src/main/java/com/petstore/service/BusinessEventService.java#recordReportSent; frontend/src/components/report/ReportShareModal.vue; admin/src/views/ReportsView.vue","test":"ReportControllerTest; ReportServiceTest; BusinessEventServiceTest; ReportSendMigrationTest","evidence":"anchored","domain":"report","note":"复制链接、二维码和预览不算发送;首次确认不可覆盖,重复确认幂等成功"}
{"id":"action:get_report_by_token","type":"Action","name":"getReportByToken","nameZh":"公开报告查询","inputs":["token"],"outputs":["entity:report"],"entrypoints":["GET /api/report/get","ReportController#getByAppointmentId"],"appliesRules":["rule:BR-RPT-006"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#getByAppointmentId","test":"ReportControllerTest#publicTokenQueryHidesInternalFields","evidence":"anchored","domain":"report"} {"id":"action:get_report_by_token","type":"Action","name":"getReportByToken","nameZh":"公开报告查询","inputs":["token"],"outputs":["entity:report"],"entrypoints":["GET /api/report/get","ReportController#getByAppointmentId"],"appliesRules":["rule:BR-RPT-006"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#getByAppointmentId","test":"ReportControllerTest#publicTokenQueryHidesInternalFields","evidence":"anchored","domain":"report"}
{"id":"action:track_report_open","type":"Action","name":"trackReportOpen","nameZh":"报告打开埋点","inputs":["token","visitType"],"outputs":["entity:business_event"],"entrypoints":["POST /api/report/open-track","ReportController#trackReportOpen"],"appliesRules":["rule:BR-RPT-007","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#trackReportOpen; backend/src/main/java/com/petstore/service/BusinessEventService.java#recordReportOpenByToken","test":"ReportControllerTest; BusinessEventServiceTest","evidence":"anchored","domain":"report"} {"id":"action:track_report_open","type":"Action","name":"trackReportOpen","nameZh":"报告打开埋点","inputs":["token","visitType"],"outputs":["entity:business_event"],"entrypoints":["POST /api/report/open-track","ReportController#trackReportOpen"],"appliesRules":["rule:BR-RPT-007","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#trackReportOpen; backend/src/main/java/com/petstore/service/BusinessEventService.java#recordReportOpenByToken","test":"ReportControllerTest; BusinessEventServiceTest","evidence":"anchored","domain":"report"}
{"id":"action:delete_report","type":"Action","name":"deleteReport","nameZh":"删除报告","inputs":["id"],"outputs":[],"entrypoints":["DELETE /api/report/delete","ReportController#delete"],"appliesRules":["rule:BR-RPT-005"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#delete","evidence":"anchored","domain":"report"} {"id":"action:delete_report","type":"Action","name":"deleteReport","nameZh":"删除报告","inputs":["id"],"outputs":[],"entrypoints":["DELETE /api/report/delete","ReportController#delete"],"appliesRules":["rule:BR-RPT-005"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#delete","evidence":"anchored","domain":"report"}
@ -31,6 +32,7 @@
{"id":"event:service_started","type":"Event","name":"ServiceStarted","nameZh":"服务开始","domain":"appointment","payload":["storeId","storeCustomerId","appointmentId","actorUserId","actorRole","occurredAt"],"emittedBy":["action:start_service"],"appliesRules":["rule:BR-APPT-001","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/service/BusinessEventService.java#recordAppointmentStatusChanged","test":"BusinessEventServiceTest#statusToDoingRecordsGenericAndServiceStartedFacts","evidence":"anchored"} {"id":"event:service_started","type":"Event","name":"ServiceStarted","nameZh":"服务开始","domain":"appointment","payload":["storeId","storeCustomerId","appointmentId","actorUserId","actorRole","occurredAt"],"emittedBy":["action:start_service"],"appliesRules":["rule:BR-APPT-001","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/service/BusinessEventService.java#recordAppointmentStatusChanged","test":"BusinessEventServiceTest#statusToDoingRecordsGenericAndServiceStartedFacts","evidence":"anchored"}
{"id":"event:service_completed","type":"Event","name":"ServiceCompleted","nameZh":"服务完成","domain":"appointment","payload":["storeId","storeCustomerId","appointmentId","actorUserId","actorRole","occurredAt"],"emittedBy":["action:transition_appointment_status","action:submit_report"],"appliesRules":["rule:BR-APPT-001","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/service/BusinessEventService.java#recordAppointmentStatusChanged; backend/src/main/java/com/petstore/service/ReportService.java#create","test":"AppointmentServiceTest; ReportServiceTest","evidence":"anchored"} {"id":"event:service_completed","type":"Event","name":"ServiceCompleted","nameZh":"服务完成","domain":"appointment","payload":["storeId","storeCustomerId","appointmentId","actorUserId","actorRole","occurredAt"],"emittedBy":["action:transition_appointment_status","action:submit_report"],"appliesRules":["rule:BR-APPT-001","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/service/BusinessEventService.java#recordAppointmentStatusChanged; backend/src/main/java/com/petstore/service/ReportService.java#create","test":"AppointmentServiceTest; ReportServiceTest","evidence":"anchored"}
{"id":"event:report_submitted","type":"Event","name":"ReportSubmitted","nameZh":"报告提交","domain":"report","payload":["storeId","storeCustomerId","reportId","actorUserId","actorRole","occurredAt"],"emittedBy":["action:submit_report"],"appliesRules":["rule:BR-RPT-001","rule:BR-RPT-002","rule:BR-RPT-004","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/service/ReportService.java#create; backend/src/main/java/com/petstore/service/BusinessEventService.java#recordReportSubmitted","test":"ReportServiceTest; BusinessEventServiceTest","evidence":"anchored"} {"id":"event:report_submitted","type":"Event","name":"ReportSubmitted","nameZh":"报告提交","domain":"report","payload":["storeId","storeCustomerId","reportId","actorUserId","actorRole","occurredAt"],"emittedBy":["action:submit_report"],"appliesRules":["rule:BR-RPT-001","rule:BR-RPT-002","rule:BR-RPT-004","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/service/ReportService.java#create; backend/src/main/java/com/petstore/service/BusinessEventService.java#recordReportSubmitted","test":"ReportServiceTest; BusinessEventServiceTest","evidence":"anchored"}
{"id":"event:report_sent","type":"Event","name":"ReportSent","nameZh":"报告已确认发送","domain":"report","payload":["storeId","storeCustomerId","reportId","actorUserId","actorRole","channel","occurredAt"],"emittedBy":["action:confirm_report_sent"],"appliesRules":["rule:BR-RPT-008","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/service/ReportService.java#confirmSent; backend/src/main/java/com/petstore/service/BusinessEventService.java#recordReportSent","test":"ReportServiceTest; BusinessEventServiceTest","evidence":"anchored","note":"idempotencyKey=report_sent:{reportId};无 token/手机号/URL"}
{"id":"event:report_opened","type":"Event","name":"ReportOpened","nameZh":"报告首次打开","domain":"report","payload":["storeId","storeCustomerId","reportId","visitType","occurredAt"],"emittedBy":["action:track_report_open"],"appliesRules":["rule:BR-RPT-007","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#trackReportOpen; backend/src/main/java/com/petstore/service/BusinessEventService.java#recordReportOpenByToken","doc":"docs/本期主线-下一步产品优化点.md 埋点事件 schema","test":"ReportControllerTest; BusinessEventServiceTest","evidence":"anchored"} {"id":"event:report_opened","type":"Event","name":"ReportOpened","nameZh":"报告首次打开","domain":"report","payload":["storeId","storeCustomerId","reportId","visitType","occurredAt"],"emittedBy":["action:track_report_open"],"appliesRules":["rule:BR-RPT-007","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#trackReportOpen; backend/src/main/java/com/petstore/service/BusinessEventService.java#recordReportOpenByToken","doc":"docs/本期主线-下一步产品优化点.md 埋点事件 schema","test":"ReportControllerTest; BusinessEventServiceTest","evidence":"anchored"}
{"id":"event:report_reopened","type":"Event","name":"ReportReopened","nameZh":"报告再次打开","domain":"report","payload":["storeId","storeCustomerId","reportId","visitType","occurredAt"],"emittedBy":["action:track_report_open"],"appliesRules":["rule:BR-RPT-007","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/service/BusinessEventService.java#recordReportOpenByToken","doc":"docs/本期主线-下一步产品优化点.md 埋点事件 schema","test":"BusinessEventServiceTest#reportOpenStoresReportIdButNeverToken","evidence":"anchored"} {"id":"event:report_reopened","type":"Event","name":"ReportReopened","nameZh":"报告再次打开","domain":"report","payload":["storeId","storeCustomerId","reportId","visitType","occurredAt"],"emittedBy":["action:track_report_open"],"appliesRules":["rule:BR-RPT-007","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/service/BusinessEventService.java#recordReportOpenByToken","doc":"docs/本期主线-下一步产品优化点.md 埋点事件 schema","test":"BusinessEventServiceTest#reportOpenStoresReportIdButNeverToken","evidence":"anchored"}
{"id":"event:lead_submitted","type":"Event","name":"LeadSubmitted","nameZh":"留资提交","domain":"lead","payload":["storeId","storeCustomerId","leadId","repeatSubmit","occurredAt"],"emittedBy":["action:submit_lead"],"appliesRules":["rule:BR-LEAD-001","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/service/ReportLeadService.java#submit; backend/src/main/java/com/petstore/service/BusinessEventService.java#recordLeadSubmitted","doc":"docs/本期主线-下一步产品优化点.md 埋点事件 schema","test":"ReportLeadServiceStoreCustomerTest; BusinessEventMigrationTest","evidence":"anchored"} {"id":"event:lead_submitted","type":"Event","name":"LeadSubmitted","nameZh":"留资提交","domain":"lead","payload":["storeId","storeCustomerId","leadId","repeatSubmit","occurredAt"],"emittedBy":["action:submit_lead"],"appliesRules":["rule:BR-LEAD-001","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/service/ReportLeadService.java#submit; backend/src/main/java/com/petstore/service/BusinessEventService.java#recordLeadSubmitted","doc":"docs/本期主线-下一步产品优化点.md 埋点事件 schema","test":"ReportLeadServiceStoreCustomerTest; BusinessEventMigrationTest","evidence":"anchored"}
@ -49,6 +51,7 @@
{"id":"rule:BR-RPT-005","type":"Rule","name":"报告跨店 403","domain":"report","appliesTo":["action:submit_report","action:delete_report"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#create; #delete","test":"ReportControllerTest#createCrossStoreReturnsForbidden; createNonStaffReturnsForbidden","evidence":"anchored"} {"id":"rule:BR-RPT-005","type":"Rule","name":"报告跨店 403","domain":"report","appliesTo":["action:submit_report","action:delete_report"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#create; #delete","test":"ReportControllerTest#createCrossStoreReturnsForbidden; createNonStaffReturnsForbidden","evidence":"anchored"}
{"id":"rule:BR-RPT-006","type":"Rule","name":"公开报告字段裁剪","domain":"report","appliesTo":["action:get_report_by_token"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#getByAppointmentId","test":"ReportControllerTest#publicTokenQueryHidesInternalFields","frontend":"frontend/src/pages/report-view/reportView.vue","evidence":"anchored"} {"id":"rule:BR-RPT-006","type":"Rule","name":"公开报告字段裁剪","domain":"report","appliesTo":["action:get_report_by_token"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#getByAppointmentId","test":"ReportControllerTest#publicTokenQueryHidesInternalFields","frontend":"frontend/src/pages/report-view/reportView.vue","evidence":"anchored"}
{"id":"rule:BR-RPT-007","type":"Rule","name":"token hash 日志","domain":"report","appliesTo":["action:track_report_open"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#shortTokenHash; #trackReportOpen","evidence":"anchored"} {"id":"rule:BR-RPT-007","type":"Rule","name":"token hash 日志","domain":"report","appliesTo":["action:track_report_open"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#shortTokenHash; #trackReportOpen","evidence":"anchored"}
{"id":"rule:BR-RPT-008","type":"Rule","name":"报告发送显式确认且首次回执不可覆盖","domain":"report","appliesTo":["action:confirm_report_sent"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#confirmSent; backend/src/main/java/com/petstore/service/ReportService.java#confirmSent; backend/src/main/java/com/petstore/mapper/ReportMapper.java#findByIdAndDeletedFalseForUpdate","test":"ReportControllerTest; ReportServiceTest; ReportSendMigrationTest","doc":"docs/架构决策-报告确认发送状态-2026-08-02.md","evidence":"anchored"}
{"id":"rule:BR-LEAD-001","type":"Rule","name":"留资去重","domain":"lead","appliesTo":["action:submit_lead"],"code":"backend/src/main/java/com/petstore/service/ReportLeadService.java#submit","evidence":"anchored","test":"ReportLeadControllerTest#submitLeadRepeatSubmitReturnsAlreadySubmitted"} {"id":"rule:BR-LEAD-001","type":"Rule","name":"留资去重","domain":"lead","appliesTo":["action:submit_lead"],"code":"backend/src/main/java/com/petstore/service/ReportLeadService.java#submit","evidence":"anchored","test":"ReportLeadControllerTest#submitLeadRepeatSubmitReturnsAlreadySubmitted"}
{"id":"rule:BR-SC-001","type":"Rule","name":"门店客户主档唯一性与合并","domain":"admin","appliesTo":["action:create_appointment","action:submit_lead","action:admin_list_service_customers"],"code":"backend/src/main/java/com/petstore/service/StoreCustomerService.java","doc":"docs/ontology/rules.md#rule:BR-SC-001; docs/架构决策-门店客户主档-2026-08-01.md","test":"StoreCustomerServiceTest; ReportLeadServiceStoreCustomerTest; IdentityOwnershipQueryTest","evidence":"anchored"} {"id":"rule:BR-SC-001","type":"Rule","name":"门店客户主档唯一性与合并","domain":"admin","appliesTo":["action:create_appointment","action:submit_lead","action:admin_list_service_customers"],"code":"backend/src/main/java/com/petstore/service/StoreCustomerService.java","doc":"docs/ontology/rules.md#rule:BR-SC-001; docs/架构决策-门店客户主档-2026-08-01.md","test":"StoreCustomerServiceTest; ReportLeadServiceStoreCustomerTest; IdentityOwnershipQueryTest","evidence":"anchored"}
{"id":"rule:BR-BE-001","type":"Rule","name":"业务事件不可变幂等与低敏","domain":"platform","appliesTo":["action:create_appointment","action:start_service","action:transition_appointment_status","action:submit_report","action:track_report_open","action:submit_lead","action:view_workbench"],"code":"backend/src/main/java/com/petstore/service/BusinessEventService.java; backend/src/main/java/com/petstore/service/AdminBusinessEventService.java","doc":"docs/ontology/rules.md#rule:BR-BE-001; docs/架构决策-业务事件落库-2026-08-01.md","test":"BusinessEventServiceTest; BusinessEventMigrationTest; AdminBusinessEventControllerTest","evidence":"anchored"} {"id":"rule:BR-BE-001","type":"Rule","name":"业务事件不可变幂等与低敏","domain":"platform","appliesTo":["action:create_appointment","action:start_service","action:transition_appointment_status","action:submit_report","action:track_report_open","action:submit_lead","action:view_workbench"],"code":"backend/src/main/java/com/petstore/service/BusinessEventService.java; backend/src/main/java/com/petstore/service/AdminBusinessEventService.java","doc":"docs/ontology/rules.md#rule:BR-BE-001; docs/架构决策-业务事件落库-2026-08-01.md","test":"BusinessEventServiceTest; BusinessEventMigrationTest; AdminBusinessEventControllerTest","evidence":"anchored"}
@ -79,6 +82,7 @@
{"id":"rel:submit_report_emits_report_submitted","type":"Relation","subject":"action:submit_report","predicate":"EMITS","object":"event:report_submitted","evidence":"anchored","domain":"report"} {"id":"rel:submit_report_emits_report_submitted","type":"Relation","subject":"action:submit_report","predicate":"EMITS","object":"event:report_submitted","evidence":"anchored","domain":"report"}
{"id":"rel:submit_report_emits_status_changed","type":"Relation","subject":"action:submit_report","predicate":"EMITS","object":"event:appointment_status_changed","evidence":"anchored","doc":"doing→done","domain":"report"} {"id":"rel:submit_report_emits_status_changed","type":"Relation","subject":"action:submit_report","predicate":"EMITS","object":"event:appointment_status_changed","evidence":"anchored","doc":"doing→done","domain":"report"}
{"id":"rel:submit_report_emits_service_completed","type":"Relation","subject":"action:submit_report","predicate":"EMITS","object":"event:service_completed","evidence":"anchored","domain":"report"} {"id":"rel:submit_report_emits_service_completed","type":"Relation","subject":"action:submit_report","predicate":"EMITS","object":"event:service_completed","evidence":"anchored","domain":"report"}
{"id":"rel:confirm_report_sent_emits_report_sent","type":"Relation","subject":"action:confirm_report_sent","predicate":"EMITS","object":"event:report_sent","evidence":"anchored","domain":"report"}
{"id":"rel:store_has_business_events","type":"Relation","subject":"entity:store","predicate":"HAS","object":"entity:business_event","evidence":"anchored","cardinality":"1—N","domain":"platform","doc":"BusinessEvent.store_id"} {"id":"rel:store_has_business_events","type":"Relation","subject":"entity:store","predicate":"HAS","object":"entity:business_event","evidence":"anchored","cardinality":"1—N","domain":"platform","doc":"BusinessEvent.store_id"}
{"id":"rel:store_customer_has_business_events","type":"Relation","subject":"entity:store_customer","predicate":"HAS","object":"entity:business_event","evidence":"anchored","cardinality":"1—N","domain":"platform","doc":"BusinessEvent.store_customer_id 可空"} {"id":"rel:store_customer_has_business_events","type":"Relation","subject":"entity:store_customer","predicate":"HAS","object":"entity:business_event","evidence":"anchored","cardinality":"1—N","domain":"platform","doc":"BusinessEvent.store_customer_id 可空"}
{"id":"rel:track_report_open_emits_report_opened","type":"Relation","subject":"action:track_report_open","predicate":"EMITS","object":"event:report_opened","evidence":"anchored","domain":"report"} {"id":"rel:track_report_open_emits_report_opened","type":"Relation","subject":"action:track_report_open","predicate":"EMITS","object":"event:report_opened","evidence":"anchored","domain":"report"}
@ -135,7 +139,7 @@
{"id":"rule:BR-USER-002","type":"Rule","name":"用户更新仅本人","domain":"user","appliesTo":["action:update_user","action:get_user_info"],"code":"backend/src/main/java/com/petstore/controller/UserController.java#updateUser; #info","evidence":"anchored"} {"id":"rule:BR-USER-002","type":"Rule","name":"用户更新仅本人","domain":"user","appliesTo":["action:update_user","action:get_user_info"],"code":"backend/src/main/java/com/petstore/controller/UserController.java#updateUser; #info","evidence":"anchored"}
{"id":"rule:BR-CONFIG-001","type":"Rule","name":"生产配置环境变量","domain":"config","appliesTo":[],"code":"backend/src/main/resources/application.yml; application-example.yml","doc":"backend/README.md 环境变量表","evidence":"anchored"} {"id":"rule:BR-CONFIG-001","type":"Rule","name":"生产配置环境变量","domain":"config","appliesTo":[],"code":"backend/src/main/resources/application.yml; application-example.yml","doc":"backend/README.md 环境变量表","evidence":"anchored"}
{"id":"rule:BR-CONFIG-002","type":"Rule","name":"production profile 强制只读 schema 安全配置","domain":"config","appliesTo":[],"code":"backend/src/main/resources/application.yml production profile; backend/src/main/java/com/petstore/config/ProductionConfigurationValidator.java; backend/src/main/java/com/petstore/PetstoreApplication.java","doc":"backend/README.md production profile","test":"ProductionConfigurationValidatorTest","evidence":"anchored","note":"生产禁用自动建表/show-sql/SMS 万能码/默认数据初始化,并拒绝弱密钥、占位域名和宽 CORS"} {"id":"rule:BR-CONFIG-002","type":"Rule","name":"production profile 强制只读 schema 安全配置","domain":"config","appliesTo":[],"code":"backend/src/main/resources/application.yml production profile; backend/src/main/java/com/petstore/config/ProductionConfigurationValidator.java; backend/src/main/java/com/petstore/PetstoreApplication.java","doc":"backend/README.md production profile","test":"ProductionConfigurationValidatorTest","evidence":"anchored","note":"生产禁用自动建表/show-sql/SMS 万能码/默认数据初始化,并拒绝弱密钥、占位域名和宽 CORS"}
{"id":"rule:BR-CONFIG-003","type":"Rule","name":"生产发布只读门禁","domain":"config","appliesTo":[],"code":"backend/src/main/java/com/petstore/config/ProductionDatabasePreflightRunner.java; backend/src/main/java/com/petstore/config/PetstoreRuntimeHealthIndicator.java; backend/deploy/release-preflight.sh; backend/deploy/production-smoke.sh","doc":"docs/生产发布与回滚Runbook-2026-08-01.md; docs/生产监控与告警基线-2026-08-01.md","evidence":"anchored","note":"备份和四迁移后运行 schema validate + 15 项只读不变量readiness 覆盖 DB/磁盘/上传/FFmpeg自动 smoke 只读"} {"id":"rule:BR-CONFIG-003","type":"Rule","name":"生产发布只读门禁","domain":"config","appliesTo":[],"code":"backend/src/main/java/com/petstore/config/ProductionDatabasePreflightRunner.java; backend/src/main/java/com/petstore/config/PetstoreRuntimeHealthIndicator.java; backend/deploy/release-preflight.sh; backend/deploy/production-smoke.sh","doc":"docs/生产发布与回滚Runbook-2026-08-01.md; docs/生产监控与告警基线-2026-08-01.md","evidence":"anchored","note":"备份和五迁移后运行 schema validate + 18 项只读不变量readiness 覆盖 DB/磁盘/上传/FFmpeg自动 smoke 只读"}
{"id":"rel:user_owns_pets","type":"Relation","subject":"entity:user","predicate":"OWNS","object":"entity:pet","cardinality":"1—N","evidence":"anchored","doc":"Pet.owner_user_id → User.id","domain":"user"} {"id":"rel:user_owns_pets","type":"Relation","subject":"entity:user","predicate":"OWNS","object":"entity:pet","cardinality":"1—N","evidence":"anchored","doc":"Pet.owner_user_id → User.id","domain":"user"}
{"id":"rel:pet_has_appointments","type":"Relation","subject":"entity:pet","predicate":"HAS","object":"entity:appointment","cardinality":"1—N","evidence":"anchored","doc":"Appointment.pet_id可空","domain":"pet"} {"id":"rel:pet_has_appointments","type":"Relation","subject":"entity:pet","predicate":"HAS","object":"entity:appointment","cardinality":"1—N","evidence":"anchored","doc":"Appointment.pet_id可空","domain":"pet"}
{"id":"rel:store_has_service_types","type":"Relation","subject":"entity:store","predicate":"HAS","object":"entity:service_type","cardinality":"1—N","evidence":"anchored","doc":"ServiceType.store_idNULL=系统默认)","domain":"store"} {"id":"rel:store_has_service_types","type":"Relation","subject":"entity:store","predicate":"HAS","object":"entity:service_type","cardinality":"1—N","evidence":"anchored","doc":"ServiceType.store_idNULL=系统默认)","domain":"store"}
@ -202,8 +206,8 @@
{"id":"rel:br_int_001_applies","type":"Relation","subject":"rule:BR-INT-001","predicate":"APPLIES_TO","object":"action:get_report_suggestion","domain":"service_type","evidence":"anchored"} {"id":"rel:br_int_001_applies","type":"Relation","subject":"rule:BR-INT-001","predicate":"APPLIES_TO","object":"action:get_report_suggestion","domain":"service_type","evidence":"anchored"}
{"id":"rel:br_store_003_applies","type":"Relation","subject":"rule:BR-STORE-003","predicate":"APPLIES_TO","object":"action:get_store","domain":"store","evidence":"anchored"} {"id":"rel:br_store_003_applies","type":"Relation","subject":"rule:BR-STORE-003","predicate":"APPLIES_TO","object":"action:get_store","domain":"store","evidence":"anchored"}
{"id":"entity:admin_console","type":"Entity","name":"AdminConsole","nameZh":"门店 Web 管理后台","domain":"admin","jpa":false,"fields":["surface","navModules","storeScope","roleGate"],"systems":["Store Admin FE","Backend Core"],"code":"","doc":"docs/ontology/objects.md#entity:admin_console","test":"","evidence":"documented","note":"Phase A 产品预录入;独立 Web 端,与 uni-app 小程序并存。对标宠老板「店铺概况」入口,不做收银/库存。详见 docs/门店管理后台升级方案-对标宠老板.md","status":"planned"} {"id":"entity:admin_console","type":"Entity","name":"AdminConsole","nameZh":"门店 Web 管理后台","domain":"admin","jpa":false,"fields":["surface","navModules","storeScope","roleGate"],"systems":["Store Admin FE","Backend Core"],"code":"","doc":"docs/ontology/objects.md#entity:admin_console","test":"","evidence":"documented","note":"Phase A 产品预录入;独立 Web 端,与 uni-app 小程序并存。对标宠老板「店铺概况」入口,不做收银/库存。详见 docs/门店管理后台升级方案-对标宠老板.md","status":"planned"}
{"id":"entity:workbench","type":"Entity","name":"Workbench","nameZh":"今日行动工作台","domain":"admin","jpa":false,"fields":["todayAppointmentCount","todayDoneCount","overdueAppointmentCount","inServiceCount","upcomingTodayCount","highlightAnomalyCount","dueFollowUpCount","actionGroups","reportFunnel"],"systems":["Store Admin FE","Backend Core","Report Media Backend"],"code":"admin/src/views/WorkbenchView.vue; backend/src/main/java/com/petstore/service/AdminWorkbenchService.java; backend/src/main/java/com/petstore/service/AdminBusinessEventService.java","doc":"docs/ontology/objects.md#entity:workbench","test":"AdminWorkbenchServiceTest; AdminBusinessEventServiceTest","evidence":"anchored","note":"服务端按完整本店数据准确聚合行动;每组最多 3 条低敏预览;报告发送与转预约未跟踪时显示未知。","status":"implemented"} {"id":"entity:workbench","type":"Entity","name":"Workbench","nameZh":"今日行动工作台","domain":"admin","jpa":false,"fields":["todayAppointmentCount","todayDoneCount","overdueAppointmentCount","inServiceCount","upcomingTodayCount","highlightAnomalyCount","unsentReportCount","dueFollowUpCount","actionGroups","reportFunnel"],"systems":["Store Admin FE","Backend Core","Report Media Backend"],"code":"admin/src/views/WorkbenchView.vue; backend/src/main/java/com/petstore/service/AdminWorkbenchService.java; backend/src/main/java/com/petstore/service/AdminBusinessEventService.java","doc":"docs/ontology/objects.md#entity:workbench","test":"AdminWorkbenchServiceTest; AdminBusinessEventServiceTest","evidence":"anchored","note":"服务端按完整本店数据准确聚合行动;每组最多 3 条低敏预览;新报告待发送有行动组,发送漏斗为真实显式确认,仅转预约未知。","status":"implemented"}
{"id":"entity:workbench_todo_item","type":"Entity","name":"WorkbenchTodoItem","nameZh":"工作台行动组","domain":"admin","jpa":false,"fields":["kind","urgency","count","items","drilldownAction","filter"],"systems":["Store Admin FE","Backend Core"],"code":"backend/src/main/java/com/petstore/service/AdminWorkbenchService.java; admin/src/views/WorkbenchView.vue","doc":"docs/ontology/objects.md#entity:workbench_todo_item","test":"AdminWorkbenchServiceTest","evidence":"anchored","note":"kind: overdue_appointment | in_service | upcoming_today | highlight_anomaly | follow_up_dueitems 不含手机号/token/媒体 URL","status":"implemented"} {"id":"entity:workbench_todo_item","type":"Entity","name":"WorkbenchTodoItem","nameZh":"工作台行动组","domain":"admin","jpa":false,"fields":["kind","urgency","count","items","drilldownAction","filter"],"systems":["Store Admin FE","Backend Core"],"code":"backend/src/main/java/com/petstore/service/AdminWorkbenchService.java; admin/src/views/WorkbenchView.vue","doc":"docs/ontology/objects.md#entity:workbench_todo_item","test":"AdminWorkbenchServiceTest","evidence":"anchored","note":"kind: overdue_appointment | in_service | upcoming_today | highlight_anomaly | unsent_report | follow_up_dueitems 不含手机号/token/媒体 URL","status":"implemented"}
{"id":"entity:store_customer","type":"Entity","name":"StoreCustomer","nameZh":"门店客户主档(瘦身)","domain":"admin","jpa":true,"fields":["id","store_id","customer_user_id","phone","display_name","source","first_contact_at","last_contact_at","create_time","update_time","deleted"],"systems":["Store Admin FE","Backend Core","Report Media Backend"],"code":"backend/src/main/java/com/petstore/entity/StoreCustomer.java; backend/src/main/java/com/petstore/service/StoreCustomerService.java; backend/src/main/java/com/petstore/service/AdminServiceCustomerService.java","doc":"docs/ontology/objects.md#entity:store_customer; docs/架构决策-门店客户主档-2026-08-01.md","test":"StoreCustomerServiceTest; AdminServiceCustomerServiceTest; IdentityOwnershipQueryTest","evidence":"anchored","note":"门店与客户的稳定关系实体API 动态聚合宠物/到店/报告/留资,不含余额/次卡/积分。","status":"implemented"} {"id":"entity:store_customer","type":"Entity","name":"StoreCustomer","nameZh":"门店客户主档(瘦身)","domain":"admin","jpa":true,"fields":["id","store_id","customer_user_id","phone","display_name","source","first_contact_at","last_contact_at","create_time","update_time","deleted"],"systems":["Store Admin FE","Backend Core","Report Media Backend"],"code":"backend/src/main/java/com/petstore/entity/StoreCustomer.java; backend/src/main/java/com/petstore/service/StoreCustomerService.java; backend/src/main/java/com/petstore/service/AdminServiceCustomerService.java","doc":"docs/ontology/objects.md#entity:store_customer; docs/架构决策-门店客户主档-2026-08-01.md","test":"StoreCustomerServiceTest; AdminServiceCustomerServiceTest; IdentityOwnershipQueryTest","evidence":"anchored","note":"门店与客户的稳定关系实体API 动态聚合宠物/到店/报告/留资,不含余额/次卡/积分。","status":"implemented"}
{"id":"rule:BR-ADMIN-001","type":"Rule","name":"adminRoleGate","nameZh":"门店后台角色门禁boss/staff 可读写)","domain":"admin","appliesTo":["action:view_workbench","action:admin_list_appointments","action:admin_list_reports","action:admin_list_leads","action:admin_list_service_customers","action:admin_update_settings"],"code":"","doc":"docs/ontology/rules.md#rule:BR-ADMIN-001; docs/门店管理后台-PhaseA-页面清单PRD.md","test":"","evidence":"documented","note":"boss 与 staff 均可进入 admin_console 并编辑本店数据customer 禁止。删店/部分设置仍仅 bossBR-STORE-001/BR-USER-001。产品决策 2026-07-10。"} {"id":"rule:BR-ADMIN-001","type":"Rule","name":"adminRoleGate","nameZh":"门店后台角色门禁boss/staff 可读写)","domain":"admin","appliesTo":["action:view_workbench","action:admin_list_appointments","action:admin_list_reports","action:admin_list_leads","action:admin_list_service_customers","action:admin_update_settings"],"code":"","doc":"docs/ontology/rules.md#rule:BR-ADMIN-001; docs/门店管理后台-PhaseA-页面清单PRD.md","test":"","evidence":"documented","note":"boss 与 staff 均可进入 admin_console 并编辑本店数据customer 禁止。删店/部分设置仍仅 bossBR-STORE-001/BR-USER-001。产品决策 2026-07-10。"}
{"id":"rule:BR-ADMIN-002","type":"Rule","name":"adminStoreScope","nameZh":"后台数据仅本店 storeId","domain":"admin","appliesTo":["action:view_workbench","action:admin_list_appointments","action:admin_list_reports","action:admin_list_leads","action:admin_list_service_customers","action:admin_update_settings"],"code":"","doc":"docs/ontology/rules.md#rule:BR-ADMIN-002","test":"","evidence":"documented","note":"所有后台查询/写操作强制 current.storeId忽略跨店请求参数。连锁同步不在 Phase A。"} {"id":"rule:BR-ADMIN-002","type":"Rule","name":"adminStoreScope","nameZh":"后台数据仅本店 storeId","domain":"admin","appliesTo":["action:view_workbench","action:admin_list_appointments","action:admin_list_reports","action:admin_list_leads","action:admin_list_service_customers","action:admin_update_settings"],"code":"","doc":"docs/ontology/rules.md#rule:BR-ADMIN-002","test":"","evidence":"documented","note":"所有后台查询/写操作强制 current.storeId忽略跨店请求参数。连锁同步不在 Phase A。"}

View File

@ -157,6 +157,19 @@
- **测试**`ReportControllerTest#publicTokenQueryHidesInternalFields` - **测试**`ReportControllerTest#publicTokenQueryHidesInternalFields`
- **证据**`anchored` - **证据**`anchored`
### action:confirm_report_sent
门店员工在宠主实际收到报告后显式确认发送。复制链接、生成/展示二维码、打开 H5 或预览报告都不自动计为发送;首次确认写入不可覆盖的回执,重复确认幂等成功。
- **入口**`POST /api/report/confirm-sent` → `ReportController#confirmSent``ReportService#confirmSent`
- **输入**`reportId`、`channel=wechat|qr|other`;门店、员工与角色均从会话派生
- **输出**`sendStatus`、`sentAt`、`sendChannel`、`alreadyConfirmed`
- **适用规则**`rule:BR-RPT-005`、`rule:BR-RPT-008`、`rule:BR-BE-001`
- **触发事件**`event:report_sent`(仅首次确认)
- **代码**`ReportController#confirmSent`、`ReportService#confirmSent`、`BusinessEventService#recordReportSent`
- **测试**`ReportControllerTest`、`ReportServiceTest`、`BusinessEventServiceTest`
- **证据**`anchored`
### action:track_report_open ### action:track_report_open
报告打开埋点:服务端用 token 定位 Report 后写入低敏 BusinessEvent事件表不保存 token/hash日志只记 SHA-256 前 8 位 hash。 报告打开埋点:服务端用 token 定位 Report 后写入低敏 BusinessEvent事件表不保存 token/hash日志只记 SHA-256 前 8 位 hash。
@ -588,7 +601,7 @@
### action:view_workbench ### action:view_workbench
查看今日行动工作台(登录落点)。服务端按完整本店数据聚合逾期预约、服务中、今日待开始、成片异常和到期回访,返回准确总数与低敏预览;报告提交、打开与留资通过 BusinessEvent 汇总,未建模的报告发送/转预约保持未知。 查看今日行动工作台(登录落点)。服务端按完整本店数据聚合逾期预约、服务中、今日待开始、成片异常、新报告待发送和到期回访,返回准确总数与低敏预览;报告提交、显式确认发送、打开与留资通过 BusinessEvent 汇总,未建模的转预约保持未知。
- **入口**Admin UI `/workbench``GET /api/admin/workbench/actions`;兼容 `GET /api/admin/workbench/report-funnel?date=yyyy-MM-dd` - **入口**Admin UI `/workbench``GET /api/admin/workbench/actions`;兼容 `GET /api/admin/workbench/report-funnel?date=yyyy-MM-dd`
- **输出**`entity:workbench`、`entity:workbench_todo_item` - **输出**`entity:workbench`、`entity:workbench_todo_item`
@ -617,11 +630,11 @@
### action:admin_list_reports ### action:admin_list_reports
后台报告列表(成片三态、复制公开链接、触发重生成)。 后台报告列表(成片三态、发送三态、复制公开链接、显式确认发送、触发重生成)。
- **入口**Admin UI `/reports`planned;复用 `GET /api/report/list` + `action:generate_highlight` - **入口**Admin UI `/reports`;复用 `GET /api/report/list` + `action:confirm_report_sent` + `action:generate_highlight`
- **适用规则**`rule:BR-ADMIN-001`、`rule:BR-ADMIN-002`、`rule:BR-ADMIN-003`、`rule:BR-RPT-005` - **适用规则**`rule:BR-ADMIN-001`、`rule:BR-ADMIN-002`、`rule:BR-ADMIN-003`、`rule:BR-RPT-005`、`rule:BR-RPT-008`
- **证据**`documented` - **证据**`anchored`
### action:admin_list_leads ### action:admin_list_leads

View File

@ -103,6 +103,18 @@
- **文档**`docs/本期主线-下一步产品优化点.md 埋点事件 schema` - **文档**`docs/本期主线-下一步产品优化点.md 埋点事件 schema`
- **证据**`anchored`(事件已落库并进入工作台去重统计;日志仅保留短 hash - **证据**`anchored`(事件已落库并进入工作台去重统计;日志仅保留短 hash
### event:report_sent
门店员工首次显式确认宠主已经收到报告。历史报告不猜测、不回填该事件。
- **触发动作**`action:confirm_report_sent`
- **持久字段**`aggregateId=reportId`、`storeId`、`storeCustomerId?`、`actorUserId/role`、`occurredAt=sentAt`、`channel`;不保存 report token、手机号或 URL
- **幂等键**`report_sent:{reportId}`
- **适用规则**`rule:BR-RPT-008`、`rule:BR-BE-001`
- **代码**`ReportService#confirmSent`、`BusinessEventService#recordReportSent`
- **测试**`ReportServiceTest#firstConfirmSentPersistsImmutableReceiptAndEvent`、`BusinessEventServiceTest#reportSentIsIdempotentAndStoresOnlyChannelMetadata`
- **证据**`anchored`
### event:report_reopened ### event:report_reopened
报告页再次打开(非首次)。 报告页再次打开(非首次)。

View File

@ -102,6 +102,10 @@
| service_type | VARCHAR | 服务类型(冗余) | | service_type | VARCHAR | 服务类型(冗余) |
| appointment_time | DATETIME | 服务时间(冗余) | | appointment_time | DATETIME | 服务时间(冗余) |
| staff_name | VARCHAR | 服务技师名快照(优先取预约 assigned_user_id | | staff_name | VARCHAR | 服务技师名快照(优先取预约 assigned_user_id |
| send_status | VARCHAR(16) | unknown历史不可确认/ unsent新报告待发送/ sent已显式确认 |
| sent_at | DATETIME | 首次显式确认发送时间 |
| sent_by_user_id | BIGINT | 首次确认发送的门店员工 |
| send_channel | VARCHAR(16) | wechat / qr / other首次确认后不可覆盖 |
| highlight_video_url | VARCHAR(500) | 成片 MP4 相对 URL | | highlight_video_url | VARCHAR(500) | 成片 MP4 相对 URL |
| highlight_video_status | VARCHAR(32) | processing / done / failed | | highlight_video_status | VARCHAR(32) | processing / done / failed |
| highlight_video_error | VARCHAR(500) | 错误信息 | | highlight_video_error | VARCHAR(500) | 错误信息 |
@ -392,9 +396,9 @@
|------|------| |------|------|
| todayAppointmentCount / todayDoneCount | 今日预约 / 今日完成 | | todayAppointmentCount / todayDoneCount | 今日预约 / 今日完成 |
| overdueAppointmentCount / inServiceCount / upcomingTodayCount | 逾期未开始 / 服务中 / 今日待开始 | | overdueAppointmentCount / inServiceCount / upcomingTodayCount | 逾期未开始 / 服务中 / 今日待开始 |
| dueFollowUpCount / highlightAnomalyCount | 到期回访 / 成片失败或处理超时 | | dueFollowUpCount / highlightAnomalyCount / unsentReportCount | 到期回访 / 成片失败或处理超时 / 新报告待发送 |
| actionGroups | `entity:workbench_todo_item[]`(每组最多 3 条低敏预览) | | actionGroups | `entity:workbench_todo_item[]`(每组最多 3 条低敏预览) |
| reportFunnel | 已提交/已打开/已留资/报告转预约;未跟踪的发送与转预约返回未知 | | reportFunnel | 已提交/已确认发送/已打开/已留资/报告转预约;仅转预约仍返回未知 |
| anomalyList | 成片失败或待回访最近 N 条 | | anomalyList | 成片失败或待回访最近 N 条 |
- **代码**`admin/src/views/WorkbenchView.vue`、`AdminBusinessEventController`、`AdminWorkbenchService`、`AdminBusinessEventService` - **代码**`admin/src/views/WorkbenchView.vue`、`AdminBusinessEventController`、`AdminWorkbenchService`、`AdminBusinessEventService`
@ -408,7 +412,7 @@
| 字段 | 说明 | | 字段 | 说明 |
|------|------| |------|------|
| kind | `overdue_appointment` / `in_service` / `upcoming_today` / `highlight_anomaly` / `follow_up_due` | | kind | `overdue_appointment` / `in_service` / `upcoming_today` / `highlight_anomaly` / `unsent_report` / `follow_up_due` |
| urgency | critical / high / normal | | urgency | critical / high / normal |
| count | 数量 | | count | 数量 |
| items | 最多 3 条 resource ID + 宠物/服务/时间低敏预览无手机号、token、媒体 URL | | items | 最多 3 条 resource ID + 宠物/服务/时间低敏预览无手机号、token、媒体 URL |

View File

@ -30,6 +30,7 @@
| `action:start_service` | `event:appointment_status_changed`new→doing、`event:service_started` | | `action:start_service` | `event:appointment_status_changed`new→doing、`event:service_started` |
| `action:transition_appointment_status` | `event:appointment_status_changed` | | `action:transition_appointment_status` | `event:appointment_status_changed` |
| `action:submit_report` | `event:report_submitted`、`event:appointment_status_changed`doing→done、`event:service_completed` | | `action:submit_report` | `event:report_submitted`、`event:appointment_status_changed`doing→done、`event:service_completed` |
| `action:confirm_report_sent` | `event:report_sent`(仅首次确认) |
| `action:track_report_open` | `event:report_opened` / `event:report_reopened` | | `action:track_report_open` | `event:report_opened` / `event:report_reopened` |
| `action:submit_lead` | `event:lead_submitted` | | `action:submit_lead` | `event:lead_submitted` |

View File

@ -156,6 +156,16 @@
- **代码**`backend/src/main/java/com/petstore/controller/ReportController.java#shortTokenHash`/`#trackReportOpen` - **代码**`backend/src/main/java/com/petstore/controller/ReportController.java#shortTokenHash`/`#trackReportOpen`
- **证据**`anchored` - **证据**`anchored`
### rule:BR-RPT-008
**报告发送必须显式确认且首次回执不可覆盖**:复制链接、二维码、预览和打开报告均不自动改变发送状态。迁移前历史报告为 `unknown`,迁移后新报告为 `unsent`;仅本店 boss/staff 可选择 `wechat|qr|other` 确认为 `sent`。报告行以悲观锁串行确认;首次写入 `sent_at/sent_by_user_id/send_channel` 和幂等 `report_sent` 事件,重复确认返回 `alreadyConfirmed=true` 并保留首次回执。
- **适用动作**`action:confirm_report_sent`
- **代码**`ReportController#confirmSent`、`ReportService#confirmSent`、`ReportMapper#findByIdAndDeletedFalseForUpdate`
- **测试**`ReportControllerTest`、`ReportServiceTest`、`ReportSendMigrationTest`
- **文档**`docs/架构决策-报告确认发送状态-2026-08-02.md`
- **证据**`anchored`
## ReportLead ## ReportLead
### rule:BR-LEAD-001 ### rule:BR-LEAD-001
@ -316,7 +326,7 @@
### rule:BR-CONFIG-003 ### rule:BR-CONFIG-003
**生产发布只读门禁**:正式切流前必须按固定顺序完成备份和四个版本化迁移,再以最终生产配置运行 JPA schema `validate` 与 15 项只读数据不变量检查。运行期 readiness 必须同时覆盖数据库、磁盘、上传目录和 FFmpeg/ffprobe上线自动 smoke 只允许 GET不创建或修改业务数据。 **生产发布只读门禁**:正式切流前必须按固定顺序完成备份和五个版本化迁移,再以最终生产配置运行 JPA schema `validate` 与 18 项只读数据不变量检查。运行期 readiness 必须同时覆盖数据库、磁盘、上传目录和 FFmpeg/ffprobe上线自动 smoke 只允许 GET不创建或修改业务数据。
- **适用动作**:—(发布全局约束) - **适用动作**:—(发布全局约束)
- **代码**`backend/src/main/java/com/petstore/config/ProductionDatabasePreflightRunner.java`、`PetstoreRuntimeHealthIndicator.java`、`backend/deploy/release-preflight.sh`、`production-smoke.sh` - **代码**`backend/src/main/java/com/petstore/config/ProductionDatabasePreflightRunner.java`、`PetstoreRuntimeHealthIndicator.java`、`backend/deploy/release-preflight.sh`、`production-smoke.sh`

View File

@ -0,0 +1,59 @@
# Phase 0 报告显式确认发送:交付与验收记录
日期2026-08-02
状态Code Complete / Ready for Release生产迁移与真实环境 smoke 待执行)
## 角色与范围
| 字段 | 值 |
|---|---|
| Owner | Report Media Backend / Store Miniapp FE / Store Admin FE |
| Paired QA | Report Share QA |
| Data Model Review | Yes |
| Architecture Review | Yes |
| Access / Privacy Review | Yes |
| 范围 | Report 发送字段与迁移、确认 API、BusinessEvent、工作台、漏斗、小程序分享弹层、Admin 报告列表、本体与发布文档 |
| 明确排除 | 微信投递回执、自动发送、历史发送猜测、再次预约归因、生产数据库变更 |
## 冻结口径
- 复制链接、复制话术、二维码、预览或打开报告都不自动计为发送。
- 历史报告迁移为 `unknown`;迁移后新报告默认 `unsent`
- 仅本店 boss/staff 可选择 `wechat|qr|other` 显式确认为 `sent`
- 首次回执不可覆盖;重复确认幂等成功,不重复写 `report_sent`
- 工作台只提醒 `unsent`,不把历史 `unknown` 冒充未发送。
- 公开 token 响应不返回发送状态、回执或确认员工。
## 自动验收结果
| 检查 | 结果 |
|---|---|
| Backend `mvn test` | PASS175 tests0 failures0 errors |
| Backend `mvn -DskipTests package` | PASS |
| 迁移 H2 MySQL mode | PASS历史 unknown、新记录默认 unsent、回执一致性 |
| Admin `npm run build` | PASS |
| Frontend `npm run build:h5` | PASS |
| Frontend `npm run build:mp-weixin` | PASS`verify-mp-app-json` OK |
| Release shell syntax | PASS`release-preflight.sh`、`production-smoke.sh` |
| Ontology validate | PASS242 条目、引用完整 |
| Ontology drift | PASS57/57 HTTP endpoints13/13 JPA entities0 drift |
| `git diff --check` | PASS四仓 |
## 关键自动化覆盖
- 新报告强制 `unsent`,忽略请求体伪造的发送字段。
- 首次确认写 `sentAt/sentByUserId/sendChannel` 和低敏 `report_sent`
- 重复确认保留首次回执且不重复保存/发事件。
- customer、跨店员工、非法渠道与非法筛选状态均拒绝。
- 发送确认不改写 `Report.updateTime`,避免重置成片处理超时计时。
- Admin 工作台新增 `unsent_report`,准确总数、低敏预览和服务端发送状态筛选均覆盖。
- 公开报告响应继续隐藏内部身份、token 和发送回执。
## 发布前仍需人工完成
- [ ] 备份并在隔离库恢复验证。
- [ ] 按固定顺序执行第五个迁移 `20260801_create_report_send_status.sql`,三项验证均为 0。
- [ ] 以最终生产配置运行 JPA `validate` + 18 项只读不变量预检。
- [ ] boss/staff/customer 三角色 smoke复制不变更、三渠道确认、重复确认、跨店拒绝、公开页裁剪。
- [ ] 核对工作台 `unsent_report` 与当日 `report_sent` 漏斗数字。
- [ ] 生产变更另行授权;本交付未连接或修改生产数据库。

View File

@ -45,15 +45,16 @@
## 二、核心功能闭环v1 ## 二、核心功能闭环v1
``` ```
预约创建(宠主/老板/员工,下单即生效)→ 待开始(new) → 开始服务(剪毛/正式开做时点击,谁点谁技师)→ 进行中(doing) → 填写报告 → 发送报告 → 已完成(done) 预约创建(宠主/老板/员工,下单即生效)→ 待开始(new) → 开始服务(剪毛/正式开做时点击,谁点谁技师)→ 进行中(doing) → 提交报告并完成(done) → 发送给宠主 → 显式确认发送
↘ 已取消(cancel) → 在「已完成」Tab 与完成单并列展示 ↘ 已取消(cancel) → 在「已完成」Tab 与完成单并列展示
``` ```
**报告分享流程v1 重点):** **报告分享流程v1 重点):**
``` ```
填写报告(照片+备注)→ 点击「发送报告」→ 选择发送方式 填写并提交报告(照片+备注)→ 选择传播方式 → 宠主实际收到 → 员工「确认已发送」
├── 复制链接(发给宠主微信) ├── 复制链接/话术(发给宠主微信)
└── 下载二维码(现场扫码) ├── 当面展示二维码(现场扫码)
└── 其他可核实方式
``` ```
--- ---
@ -151,6 +152,10 @@
| service_type | VARCHAR | 服务类型(冗余) | | service_type | VARCHAR | 服务类型(冗余) |
| appointment_time | DATETIME | 服务时间(冗余) | | appointment_time | DATETIME | 服务时间(冗余) |
| staff_name | VARCHAR | 服务技师名快照(冗余) | | staff_name | VARCHAR | 服务技师名快照(冗余) |
| send_status | VARCHAR(16) | unknown历史不可确认/ unsent新报告待发送/ sent已显式确认 |
| sent_at | DATETIME | 首次显式确认发送时间 |
| sent_by_user_id | BIGINT | 首次确认发送员工 |
| send_channel | VARCHAR(16) | wechat / qr / other首次确认后不可覆盖 |
| create_time | DATETIME | 创建时间 | | create_time | DATETIME | 创建时间 |
| update_time | DATETIME | 更新时间 | | update_time | DATETIME | 更新时间 |
@ -272,11 +277,18 @@
### 5.4 发送报告v1 核心功能) ### 5.4 发送报告v1 核心功能)
**操作入口:** 报告填写完成后,点击「发送报告」按钮 **操作入口:** 报告提交成功后进入「发送给宠主」弹层;后台报告列表也可继续处理待发送报告。
**发送方式:** **发送方式:**
1. **复制链接** — 生成报告链接,一键复制到剪贴板,可发给宠主微信 1. **复制链接** — 生成报告链接,一键复制到剪贴板,可发给宠主微信
2. **下载二维码** — 生成报告二维码图片,保存到相册,可打印或现场扫码 2. **当面扫码** — 展示报告二维码,宠主现场扫码打开
3. **其他方式** — 仅在员工能够确认宠主实际收到时使用
**确认口径:**
- 复制链接、复制话术、生成/展示二维码、预览或打开报告,**都不自动等于已发送**。
- 宠主实际收到后,由本店 boss/staff 选择真实渠道并点击「确认已发送」。
- 首次确认写入 `sent_at/sent_by_user_id/send_channel``report_sent` 事件;重复点击幂等成功,不覆盖首次回执。
- 历史报告没有可靠发送证据,迁移后统一为 `unknown`;只有迁移后新报告默认 `unsent` 并进入工作台待办。
**报告链接:** **报告链接:**
- URL`https://域名/report.html?token=xxx` - URL`https://域名/report.html?token=xxx`
@ -393,6 +405,12 @@ ALTER TABLE t_appointment ADD COLUMN duration_minutes INT NOT NULL DEFAULT 60;
ALTER TABLE t_schedule_block ADD COLUMN duration_minutes INT NOT NULL DEFAULT 30; ALTER TABLE t_schedule_block ADD COLUMN duration_minutes INT NOT NULL DEFAULT 30;
ALTER TABLE t_store ADD COLUMN booking_capacity INT NOT NULL DEFAULT 1; ALTER TABLE t_store ADD COLUMN booking_capacity INT NOT NULL DEFAULT 1;
-- 报告显式确认发送字段(完整历史兼容与验证见 backend/db/migrations/20260801_create_report_send_status.sql
ALTER TABLE t_report ADD COLUMN send_status VARCHAR(16) NOT NULL DEFAULT 'unsent';
ALTER TABLE t_report ADD COLUMN sent_at DATETIME NULL;
ALTER TABLE t_report ADD COLUMN sent_by_user_id BIGINT NULL;
ALTER TABLE t_report ADD COLUMN send_channel VARCHAR(16) NULL;
-- 报告媒体表(前后图 + 过程素材) -- 报告媒体表(前后图 + 过程素材)
CREATE TABLE t_report_image ( CREATE TABLE t_report_image (
id BIGINT PRIMARY KEY AUTO_INCREMENT, id BIGINT PRIMARY KEY AUTO_INCREMENT,

View File

@ -37,6 +37,7 @@
| 事件名 | 触发时机 | 关键字段 | 来源 | 统计窗口 | Owner | 落地方式 | | 事件名 | 触发时机 | 关键字段 | 来源 | 统计窗口 | Owner | 落地方式 |
|--------|----------|----------|------|----------|-------|----------| |--------|----------|----------|------|----------|-------|----------|
| `report_sent` | 门店员工在宠主实际收到后首次显式确认 | `reportId`、`storeId`、`storeCustomerId?`、`actorUserId/role`、`channel`、`occurredAt`;不存 token/手机号/URL | 门店小程序 / Admin | 日 / 周 | Report Media Backend | `POST /api/report/confirm-sent` → BusinessEvent |
| `report_opened` | 报告页首次打开(本地 storage 标记首次) | 持久化:`reportId`、`storeId`、`storeCustomerId?`、`visitType=first`、`occurredAt` | H5 报告页 / 小程序报告页 | 日 / 周 | Report Share QA | `POST /api/report/open-track` → BusinessEvent | | `report_opened` | 报告页首次打开(本地 storage 标记首次) | 持久化:`reportId`、`storeId`、`storeCustomerId?`、`visitType=first`、`occurredAt` | H5 报告页 / 小程序报告页 | 日 / 周 | Report Share QA | `POST /api/report/open-track` → BusinessEvent |
| `report_reopened` | 报告页再次打开(非首次) | 同上,`visitType=repeat` | 同上 | 日 / 周 | Report Share QA | 同上 | | `report_reopened` | 报告页再次打开(非首次) | 同上,`visitType=repeat` | 同上 | 日 / 周 | Report Share QA | 同上 |
| `highlight_success` | 成片生成成功 | `reportTokenHash`、`appointmentId`、`durationSec`、`composeMode`、`ts` | 后端成片任务 | 日 | Report Media Backend | 任务回调日志 | | `highlight_success` | 成片生成成功 | `reportTokenHash`、`appointmentId`、`durationSec`、`composeMode`、`ts` | 后端成片任务 | 日 | Report Media Backend | 任务回调日志 |

View File

@ -47,10 +47,11 @@
| `service_started` | `new -> doing` | appointment ID | | `service_started` | `new -> doing` | appointment ID |
| `service_completed` | `doing -> done` | appointment ID | | `service_completed` | `doing -> done` | appointment ID |
| `report_submitted` | 报告及图片保存成功 | report ID | | `report_submitted` | 报告及图片保存成功 | report ID |
| `report_sent` | 门店员工首次显式确认宠主实际收到 | report ID`report_sent:{reportId}` |
| `report_opened` / `report_reopened` | 公开报告打开埋点 | 每次有效打开;统计按 report ID 去重 | | `report_opened` / `report_reopened` | 公开报告打开埋点 | 每次有效打开;统计按 report ID 去重 |
| `lead_submitted` | 每次留资提交成功 | 可重复记录;漏斗按 lead ID 去重 | | `lead_submitted` | 每次留资提交成功 | 可重复记录;漏斗按 lead ID 去重 |
`report_sent`、`follow_up_completed`、`rebook_created` 尚无可靠状态机或归因字段,本阶段明确不伪造;对应工作台指标返回 `null + trackingReady=false` > 2026-08-02 增量决策:`report_sent` 已通过显式确认状态机可靠落地;复制链接、二维码和预览不构成发送。详见《架构决策-报告确认发送状态-2026-08-02》。`follow_up_completed`、`rebook_created` 仍无可靠状态机或归因字段,本阶段明确不伪造。
## 4. 隐私边界 ## 4. 隐私边界

View File

@ -0,0 +1,84 @@
# 架构决策:报告显式确认发送状态
日期2026-08-02
状态Accepted / Implemented生产迁移待执行
OwnerReport Media Backend / Store Miniapp FE / Store Admin FE / Data Model
Paired QAReport 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/小程序构建及本体校验/漂移审计通过。

View File

@ -29,7 +29,7 @@
| Priority | P0 | | Priority | P0 |
| Repo/Path | `backend/deploy/**`、前端发布配置、`docs/**` | | Repo/Path | `backend/deploy/**`、前端发布配置、`docs/**` |
| Service Lock | Petstore backend unit + Petstore 专用 Nginx server blocks + Petstore static roots | | Service Lock | Petstore backend unit + Petstore 专用 Nginx server blocks + Petstore static roots |
| Depends On | 冻结 RC、真实域名/TLS、备份、个迁移、微信合法域名 | | Depends On | 冻结 RC、真实域名/TLS、备份、个迁移、微信合法域名 |
| Acceptance | 预检、构建、readiness、只读 smoke、三角色人工 smoke、回滚演练均有证据 | | Acceptance | 预检、构建、readiness、只读 smoke、三角色人工 smoke、回滚演练均有证据 |
| RC Included | Yes | | RC Included | Yes |
| Due Date | TBD | | Due Date | TBD |
@ -47,7 +47,7 @@
- 四仓 commit、构建产物或校验和与 RC manifest 不一致; - 四仓 commit、构建产物或校验和与 RC manifest 不一致;
- 生产域名仍为 localhost、`.invalid`、`api.s-good.com` 或 `www.s-good.com` 等非本项目域名; - 生产域名仍为 localhost、`.invalid`、`api.s-good.com` 或 `www.s-good.com` 等非本项目域名;
- 数据库备份未完成、校验和缺失或未在隔离库验证可恢复; - 数据库备份未完成、校验和缺失或未在隔离库验证可恢复;
- 任一迁移末尾验证查询非 0或预检的 15 项只读不变量非 0 - 任一迁移末尾验证查询非 0或预检的 18 项只读不变量非 0
- `nginx -t` 失败,或 diff 显示将修改 Gitea/GitLab 的 server block - `nginx -t` 失败,或 diff 显示将修改 Gitea/GitLab 的 server block
- readiness 非 `UP`,上传目录/FFmpeg/数据库任一依赖失败; - readiness 非 `UP`,上传目录/FFmpeg/数据库任一依赖失败;
- 发现凭据、完整 token、手机号或私密媒体地址进入日志/工单/截图; - 发现凭据、完整 token、手机号或私密媒体地址进入日志/工单/截图;
@ -107,6 +107,7 @@ shasum -a 256 /secure/backup/petstore-before-<rc>-<timestamp>.sql
2. `backend/db/migrations/20260801_create_store_customer.sql` 2. `backend/db/migrations/20260801_create_store_customer.sql`
3. `backend/db/migrations/20260801_create_business_event.sql` 3. `backend/db/migrations/20260801_create_business_event.sql`
4. `backend/db/migrations/20260801_create_booking_capacity.sql` 4. `backend/db/migrations/20260801_create_booking_capacity.sql`
5. `backend/db/migrations/20260801_create_report_send_status.sql`
禁止跳步、交换顺序或让应用自动补表。任一验证项非 0停止发布并由 Data Model/业务 owner 判断修复方案;不得猜测性回填客户身份、报告作者或事件。 禁止跳步、交换顺序或让应用自动补表。任一验证项非 0停止发布并由 Data Model/业务 owner 判断修复方案;不得猜测性回填客户身份、报告作者或事件。
@ -127,7 +128,7 @@ systemd-run --wait --pipe --collect \
./deploy/release-preflight.sh ./deploy/release-preflight.sh
``` ```
通过条件静态配置检查通过、JPA `validate` 通过、15 项数据不变量全部为 0并以退出码 0 结束。预检不会执行迁移或修复数据。 通过条件静态配置检查通过、JPA `validate` 通过、18 项数据不变量全部为 0并以退出码 0 结束。预检不会执行迁移或修复数据。
## 6. 构建发布产物 ## 6. 构建发布产物
@ -205,7 +206,7 @@ API_ORIGIN=https://<api-domain> ./deploy/production-smoke.sh
### 9.2 数据库回滚边界 ### 9.2 数据库回滚边界
本批个迁移以新增列/表/索引和兼容回填为主,旧应用应忽略新增结构。因此常规回滚只回应用,不删除列、表、索引,不执行自动 down migration也不把已产生的新业务数据覆盖回旧备份。 本批个迁移以新增列/表/索引和兼容回填为主,旧应用应忽略新增结构。因此常规回滚只回应用,不删除列、表、索引,不执行自动 down migration也不把已产生的新业务数据覆盖回旧备份。
只有确认发生灾难性数据破坏、已冻结所有写入、明确接受备份时间点之后的数据丢失,并取得用户/业务负责人、DBA 和隐私责任人的书面授权后,才可执行整库恢复。恢复必须先在隔离库验证,再制定专门变更单;本 Runbook 不授权该操作。 只有确认发生灾难性数据破坏、已冻结所有写入、明确接受备份时间点之后的数据丢失,并取得用户/业务负责人、DBA 和隐私责任人的书面授权后,才可执行整库恢复。恢复必须先在隔离库验证,再制定专门变更单;本 Runbook 不授权该操作。
@ -215,7 +216,7 @@ API_ORIGIN=https://<api-domain> ./deploy/production-smoke.sh
- 四仓冻结 commit 与产物校验和; - 四仓冻结 commit 与产物校验和;
- 备份及隔离恢复验证; - 备份及隔离恢复验证;
- 个迁移执行和末尾验证结果; - 个迁移执行和末尾验证结果;
- 最终配置门禁与后端只读预检; - 最终配置门禁与后端只读预检;
- `nginx -t`、readiness、自动只读 smoke - `nginx -t`、readiness、自动只读 smoke
- boss/staff/customer 三角色人工 smoke - boss/staff/customer 三角色人工 smoke

View File

@ -51,12 +51,13 @@
### 业务漏斗 ### 业务漏斗
每天按门店聚合 `appointment_created → service_started → service_completed/report_submitted → report_opened → lead_submitted → rebooked`。试点初期不把业务漏斗波动直接当系统故障,但需检查: 每天按门店聚合 `appointment_created → service_started → service_completed/report_submitted → report_sent → report_opened → lead_submitted → rebooked`。试点初期不把业务漏斗波动直接当系统故障,但需检查:
- 有 done 无报告、报告无客户/作者、重复报告; - 有 done 无报告、报告无客户/作者、重复报告;
- BusinessEvent 缺失或幂等键重复; - BusinessEvent 缺失或幂等键重复;
- 服务时长/预约快照非法、容量超卖; - 服务时长/预约快照非法、容量超卖;
- 报告已提交但长期未打开; - 新报告已提交但长期未确认发送;
- 已确认发送但长期未打开;
- 回访后没有明确状态或再次预约归因。 - 回访后没有明确状态或再次预约归因。
发布前的 `release-preflight.sh` 已覆盖前四类结构/数据不变量;运行期应每天做等价的只读审计或报表,不自动修复。 发布前的 `release-preflight.sh` 已覆盖前四类结构/数据不变量;运行期应每天做等价的只读审计或报表,不自动修复。
@ -82,7 +83,7 @@
- [ ] 外部 HTTPS 探测覆盖 liveness/readiness、admin、H5 报告入口; - [ ] 外部 HTTPS 探测覆盖 liveness/readiness、admin、H5 报告入口;
- [ ] 5xx、延迟、DB 池、磁盘、证书和进程重启已配置告警; - [ ] 5xx、延迟、DB 池、磁盘、证书和进程重启已配置告警;
- [ ] FFmpeg/上传失败有可聚合字段,而非只能搜索自由文本; - [ ] FFmpeg/上传失败有可聚合字段,而非只能搜索自由文本;
- [ ] BusinessEvent 日漏斗和 15 项只读数据审计有每日结果; - [ ] BusinessEvent 日漏斗和 18 项只读数据审计有每日结果;
- [ ] 通知渠道、Primary/Backup 值班人和升级联系人完成真实测试; - [ ] 通知渠道、Primary/Backup 值班人和升级联系人完成真实测试;
- [ ] 日志脱敏抽查通过,访问权限和保留周期已批准; - [ ] 日志脱敏抽查通过,访问权限和保留周期已批准;
- [ ] 发布标记能关联四仓 commit 与 RC - [ ] 发布标记能关联四仓 commit 与 RC

View File

@ -96,7 +96,8 @@
| 完整服务数 | 符合上方五项的服务 | 判断样本进度 | | 完整服务数 | 符合上方五项的服务 | 判断样本进度 |
| 预约完成率 | done / 有效预约 | 判断主链路稳定性 | | 预约完成率 | done / 有效预约 | 判断主链路稳定性 |
| 报告提交率 | 有报告的 done / done | 判断员工执行成本 | | 报告提交率 | 有报告的 done / done | 判断员工执行成本 |
| 报告打开率 | 首次打开报告 / 已提交报告 | 判断宠主价值 | | 报告确认发送率 | 显式确认发送 / 已提交报告 | 判断门店触达执行 |
| 报告打开率 | 首次打开报告 / 已确认发送报告 | 判断宠主价值;历史 unknown 不纳入分母 |
| 留资率 | 去重留资 / 首次打开报告 | 判断回访意愿 | | 留资率 | 去重留资 / 首次打开报告 | 判断回访意愿 |
| 再次预约率 | 报告后窗口内产生新预约的 StoreCustomer / 可观察客户 | 判断闭环价值 | | 再次预约率 | 报告后窗口内产生新预约的 StoreCustomer / 可观察客户 | 判断闭环价值 |
| 容量冲突率 | 因无容量被拒绝的请求 / 可约请求 | 校准容量而非承诺零等待 | | 容量冲突率 | 因无容量被拒绝的请求 / 可约请求 | 校准容量而非承诺零等待 |

View File

@ -20,7 +20,7 @@
| 模块 | 状态 | 主要缺口 | | 模块 | 状态 | 主要缺口 |
|------|------|----------| |------|------|----------|
| A0 壳层登录 | ~95% | 生产微信登录策略另议 | | A0 壳层登录 | ~95% | 生产微信登录策略另议 |
| A1 工作台 | ~98% | 服务端准确行动聚合 + 低敏预览已做;发送状态与转预约归因仍待后续模型,真实环境 smoke 待补 | | A1 工作台 | ~99% | 服务端准确行动聚合、低敏预览、报告待发送行动与显式发送漏斗已做;仅转预约归因和真实环境 smoke 待补 |
| A2 预约 | ~95% | 详情/筛选/待出报告/宠主脱敏已做 | | A2 预约 | ~95% | 详情/筛选/待出报告/宠主脱敏已做 |
| A6 排班 | ~90% | 与小程序对照验收 | | A6 排班 | ~90% | 与小程序对照验收 |
| A3 报告 | ~95% | 详情抽屉+失败文案+H5 | | A3 报告 | ~95% | 详情抽屉+失败文案+H5 |

View File

@ -45,9 +45,10 @@
② 服务中等待收尾 n 最近 3 条 …………………… [全部处理] ② 服务中等待收尾 n 最近 3 条 …………………… [全部处理]
③ 今天接下来到店 n 最近 3 条 …………………… [全部处理] ③ 今天接下来到店 n 最近 3 条 …………………… [全部处理]
④ 成片异常 n 最近 3 条 …………………… [全部处理] ④ 成片异常 n 最近 3 条 …………………… [全部处理]
⑤ 回访日期已到 n 最近 3 条 …………………… [全部处理] ⑤ 报告尚未发送 n 最近 3 条 …………………… [全部处理]
⑥ 回访日期已到 n 最近 3 条 …………………… [全部处理]
报告漏斗 已提交 | 确认发送 | 已打开 | 已留资 | 转预约— 报告漏斗 已提交 | 确认发送 | 已打开 | 已留资 | 转预约—
``` ```
**主操作**:点指标、行动组或具体低敏预览下钻;工作台常驻,不用先关弹窗。 **主操作**:点指标、行动组或具体低敏预览下钻;工作台常驻,不用先关弹窗。

View File

@ -105,7 +105,7 @@ python3 /Users/apple/_src/petstore/docs/graph/audit_drift.py
| ontologyRefs | `[entity:admin_console, entity:workbench, entity:business_event, action:view_workbench, action:start_service, rule:BR-ADMIN-001, rule:BR-APPT-001, rule:BR-BE-001]` | | ontologyRefs | `[entity:admin_console, entity:workbench, entity:business_event, action:view_workbench, action:start_service, rule:BR-ADMIN-001, rule:BR-APPT-001, rule:BR-BE-001]` |
| Access / Privacy Review Required | Yes | | Access / Privacy Review Required | Yes |
**验收**:对齐 PRD §13 与线框 A0A2staff/boss 可开始服务customer 进不去;待办下钻参数正确;报告提交/打开/留资来自本店 BusinessEvent 汇总,未跟踪的转预约显示未知。 **验收**:对齐 PRD §13 与线框 A0A2staff/boss 可开始服务customer 进不去;待办下钻参数正确;报告提交/显式确认发送/打开/留资来自本店 BusinessEvent 汇总,未跟踪的转预约显示未知。
**验证**`AdminBusinessEventControllerTest` + `AdminBusinessEventServiceTest` + admin 构建;发布后按 PRD §3.6 / §2.6 手工冒烟。 **验证**`AdminBusinessEventControllerTest` + `AdminBusinessEventServiceTest` + admin 构建;发布后按 PRD §3.6 / §2.6 手工冒烟。
--- ---

View File

@ -92,7 +92,7 @@ entity:admin_console
常驻行动队列(不再每日强弹) 常驻行动队列(不再每日强弹)
报告漏斗四格(提交/打开/留资已由 BusinessEvent 汇总;转预约仍为「—」) 报告漏斗四格(提交/打开/留资已由 BusinessEvent 汇总;转预约仍为「—」)
每组最多 3 条低敏预览 + 准确总数 + 列表下钻 每组最多 3 条低敏预览 + 准确总数 + 列表下钻
报告漏斗(提交 / 发送未知 / 打开 / 留资 / 再预约未知) 报告漏斗(提交 / 显式确认发送 / 打开 / 留资 / 再预约未知)
``` ```
### 2.2 顶栏指标卡 ### 2.2 顶栏指标卡
@ -112,9 +112,10 @@ entity:admin_console
| `in_service` | 服务中,等待收尾 | 所有 `doing`;提交报告后自动退出 | 预约列表 `status=doing&missingReport=1` | | `in_service` | 服务中,等待收尾 | 所有 `doing`;提交报告后自动退出 | 预约列表 `status=doing&missingReport=1` |
| `upcoming_today` | 今天接下来要到店 | 今日尚未到时刻的 `new` | 预约列表 `status=new&date=today` | | `upcoming_today` | 今天接下来要到店 | 今日尚未到时刻的 `new` | 预约列表 `status=new&date=today` |
| `highlight_anomaly` | 成片异常 | `failed` + `processing` 超过 30 分钟 | 报告列表 `highlightStatus=anomaly` | | `highlight_anomaly` | 成片异常 | `failed` + `processing` 超过 30 分钟 | 报告列表 `highlightStatus=anomaly` |
| `unsent_report` | 报告尚未发送 | 仅 `sendStatus=unsent`;历史 `unknown` 不进入待办 | 报告列表 `sendStatus=unsent` |
| `follow_up_due` | 回访日期已到 | `pending``remindDate≤今天` 或日期缺失 | 线索列表 `due=today` | | `follow_up_due` | 回访日期已到 | `pending``remindDate≤今天` 或日期缺失 | 线索列表 `due=today` |
优先级固定为:逾期预约 → 服务中 → 今日待开始 → 成片异常 → 到期回访。每组只返回最多 3 条预览,预览不含手机号、报告 token、媒体 URL 或微信标识;准确总数由服务端按完整数据计算。工作台不再每日强弹,避免值班人员为了关弹窗而忽略真正行动。 优先级固定为:逾期预约 → 服务中 → 今日待开始 → 成片异常 → 报告待发送 → 到期回访。每组只返回最多 3 条预览,预览不含手机号、报告 token、媒体 URL 或微信标识;准确总数由服务端按完整数据计算。工作台不再每日强弹,避免值班人员为了关弹窗而忽略真正行动。
**不对标宠老板**:寄养、临期商品、库存、待发货、售后。 **不对标宠老板**:寄养、临期商品、库存、待发货、售后。
@ -122,7 +123,8 @@ entity:admin_console
| 格 | 口径 | 当前实现 | | 格 | 口径 | 当前实现 |
|----|------|----------| |----|------|----------|
| 今日已提交报告 | `report_submitted` 按 report 去重 | BusinessEvent 汇总;发送状态尚未建模,不写“已发” | | 今日已提交报告 | `report_submitted` 按 report 去重 | BusinessEvent 汇总 |
| 已确认发送 | `report_sent` 按 report 去重 | 仅员工显式确认事实;复制/二维码/预览不计入 |
| 已打开 | `report_opened/reopened` 按 report 去重 | BusinessEvent 汇总 | | 已打开 | `report_opened/reopened` 按 report 去重 | BusinessEvent 汇总 |
| 已留资 | `lead_submitted` 按 lead 去重 | BusinessEvent 汇总 | | 已留资 | `lead_submitted` 按 lead 去重 | BusinessEvent 汇总 |
| 报告转预约 | `rebook_created` | 尚无可靠归因,显示「—」 | | 报告转预约 | `rebook_created` | 尚无可靠归因,显示「—」 |

View File

@ -36,7 +36,7 @@
|------|------|------| |------|------|------|
| 预约状态机 / 号源 / 排班占用 | ✅ | 小程序门店端 | | 预约状态机 / 号源 / 排班占用 | ✅ | 小程序门店端 |
| 服务报告 + 前后照片 + 成片 | ✅ 差异化核心 | 小程序 + 公开 H5 | | 服务报告 + 前后照片 + 成片 | ✅ 差异化核心 | 小程序 + 公开 H5 |
| 报告分享 / 打开埋点 / 留资回访 | ✅ 部分 | 公开页 + Leads | | 报告分享 / 显式发送确认 / 打开埋点 / 留资回访 | ✅ | 小程序 + Admin + 公开页 + Leads |
| 门店设置 / 员工 / 服务类型 | ✅ 基础 | 小程序「我的」 | | 门店设置 / 员工 / 服务类型 | ✅ 基础 | 小程序「我的」 |
| 会员卡 / 储值 / 套餐 | ❌ gap 占位 | 本体未展开 | | 会员卡 / 储值 / 套餐 | ❌ gap 占位 | 本体未展开 |
| 收银 / 库存 / 寄养 / 提成 / 进货 | ❌ | 明确不在 P0 | | 收银 / 库存 / 寄养 / 提成 / 进货 | ❌ | 明确不在 P0 |
@ -443,7 +443,7 @@ Web 后台主攻:**经营、检索、批量、报表、设置****允许「
└──────────────────┴──────────────────────────┴──────────────────┘ └──────────────────┴──────────────────────────┴──────────────────┘
``` ```
**报告漏斗四格**(替代微信商城四格):`今日已提交报告` · `已打开` · `已留资` · `从报告发起的预约`——前三项已由 BusinessEvent 汇总;报告发送和转预约归因未建模前显示「—」,不以提交冒充发送 **报告漏斗**(替代微信商城状态):`今日已提交报告` · `已确认发送` · `已打开` · `已留资` · `从报告发起的预约`——前四项已由 BusinessEvent 汇总;发送只认员工显式确认,不以提交、复制链接或展示二维码冒充;仅转预约归因仍显示「—」
### 11.7 截图覆盖度与仍缺清单2026-07-10 刷新) ### 11.7 截图覆盖度与仍缺清单2026-07-10 刷新)