diff --git a/P0-研发落地清单.md b/P0-研发落地清单.md index 8d460d6..b737943 100644 --- a/P0-研发落地清单.md +++ b/P0-研发落地清单.md @@ -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` | | **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`;不等同排队/准点承诺 | +| **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 | 状态 | 验收命令 | 备注 | |----|------|----------|-------|------|----------|------| | **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 锁死策略一致 | | **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 | @@ -131,7 +132,7 @@ | 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 | 真实配置不得提交 | | **H3** | **P0 通过条件** | 见下方「P0 通过条件」块 | System Architect / Core Flow QA | 🟡 待人工 smoke 复核 | 见下方 | 对应 P0-10 | diff --git a/coverage/ontology-coverage-audit.md b/coverage/ontology-coverage-audit.md index 74afbdb..f12e260 100644 --- a/coverage/ontology-coverage-audit.md +++ b/coverage/ontology-coverage-audit.md @@ -104,7 +104,7 @@ python3 docs/graph/audit_drift.py | ~~高~~ | ~~寄语幂等独立用例~~ | ✅ `ReportTestimonialServiceTest` | | ~~中~~ | ~~`rule:BR-STORE-003` 公开 Store 字段裁剪~~ | ✅ `toPublicStoreView` | | 中 | `uk_report_appointment` 上线迁移 | 🟡 运维 | -| ~~中~~ | ~~预约/履约/报告打开/留资事件可统计落地~~ | ✅ BusinessEvent;成片/播放/保存事件另行接入 | +| ~~中~~ | ~~预约/履约/报告提交/显式发送/打开/留资事件可统计落地~~ | ✅ BusinessEvent;成片/播放/保存事件另行接入 | | 中 | production profile + 只读预检在真实生产配置实测 | 🟡 发布门禁 | | 低 | 成片真 FFmpeg 端到端集成测(本机有 ffmpeg) | 🟡 单测已覆盖状态机与归类 | | 低 | `action:logout` / `event:session_revoked` 服务端吊销 | 🟡 gap | diff --git a/graph/ontology.jsonl b/graph/ontology.jsonl index 99564ed..710a5af 100644 --- a/graph/ontology.jsonl +++ b/graph/ontology.jsonl @@ -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: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: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: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"} @@ -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: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: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: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"} @@ -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_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_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_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"} @@ -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-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-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-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"} @@ -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_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: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_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"} @@ -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-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-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: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_id(NULL=系统默认)","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_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: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_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_due;items 不含手机号/token/媒体 URL","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 | unsent_report | follow_up_due;items 不含手机号/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":"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 禁止。删店/部分设置仍仅 boss(BR-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。"} diff --git a/ontology/actions.md b/ontology/actions.md index ecc3b27..3d1f096 100644 --- a/ontology/actions.md +++ b/ontology/actions.md @@ -157,6 +157,19 @@ - **测试**:`ReportControllerTest#publicTokenQueryHidesInternalFields` - **证据**:`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 报告打开埋点:服务端用 token 定位 Report 后写入低敏 BusinessEvent;事件表不保存 token/hash,日志只记 SHA-256 前 8 位 hash。 @@ -588,7 +601,7 @@ ### action:view_workbench -查看今日行动工作台(登录落点)。服务端按完整本店数据聚合逾期预约、服务中、今日待开始、成片异常和到期回访,返回准确总数与低敏预览;报告提交、打开与留资通过 BusinessEvent 汇总,未建模的报告发送/转预约保持未知。 +查看今日行动工作台(登录落点)。服务端按完整本店数据聚合逾期预约、服务中、今日待开始、成片异常、新报告待发送和到期回访,返回准确总数与低敏预览;报告提交、显式确认发送、打开与留资通过 BusinessEvent 汇总,仅未建模的转预约保持未知。 - **入口**:Admin UI `/workbench`;`GET /api/admin/workbench/actions`;兼容 `GET /api/admin/workbench/report-funnel?date=yyyy-MM-dd` - **输出**:`entity:workbench`、`entity:workbench_todo_item` @@ -617,11 +630,11 @@ ### action:admin_list_reports -后台报告列表(成片三态、复制公开链接、触发重生成)。 +后台报告列表(成片三态、发送三态、复制公开链接、显式确认发送、触发重生成)。 -- **入口**:Admin UI `/reports`(planned);复用 `GET /api/report/list` + `action:generate_highlight` -- **适用规则**:`rule:BR-ADMIN-001`、`rule:BR-ADMIN-002`、`rule:BR-ADMIN-003`、`rule:BR-RPT-005` -- **证据**:`documented` +- **入口**: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-RPT-008` +- **证据**:`anchored` ### action:admin_list_leads diff --git a/ontology/events.md b/ontology/events.md index 28e86a4..d6f486c 100644 --- a/ontology/events.md +++ b/ontology/events.md @@ -103,6 +103,18 @@ - **文档**:`docs/本期主线-下一步产品优化点.md 埋点事件 schema` - **证据**:`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 报告页再次打开(非首次)。 diff --git a/ontology/objects.md b/ontology/objects.md index f18a1e9..817e416 100644 --- a/ontology/objects.md +++ b/ontology/objects.md @@ -102,6 +102,10 @@ | service_type | VARCHAR | 服务类型(冗余) | | appointment_time | DATETIME | 服务时间(冗余) | | 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_status | VARCHAR(32) | processing / done / failed | | highlight_video_error | VARCHAR(500) | 错误信息 | @@ -392,9 +396,9 @@ |------|------| | todayAppointmentCount / todayDoneCount | 今日预约 / 今日完成 | | overdueAppointmentCount / inServiceCount / upcomingTodayCount | 逾期未开始 / 服务中 / 今日待开始 | -| dueFollowUpCount / highlightAnomalyCount | 到期回访 / 成片失败或处理超时 | +| dueFollowUpCount / highlightAnomalyCount / unsentReportCount | 到期回访 / 成片失败或处理超时 / 新报告待发送 | | actionGroups | `entity:workbench_todo_item[]`(每组最多 3 条低敏预览) | -| reportFunnel | 已提交/已打开/已留资/报告转预约;未跟踪的发送与转预约返回未知 | +| reportFunnel | 已提交/已确认发送/已打开/已留资/报告转预约;仅转预约仍返回未知 | | anomalyList | 成片失败或待回访最近 N 条 | - **代码**:`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 | | count | 数量 | | items | 最多 3 条 resource ID + 宠物/服务/时间低敏预览;无手机号、token、媒体 URL | diff --git a/ontology/relations.md b/ontology/relations.md index 26237db..e3e59c2 100644 --- a/ontology/relations.md +++ b/ontology/relations.md @@ -30,6 +30,7 @@ | `action:start_service` | `event:appointment_status_changed`(new→doing)、`event:service_started` | | `action:transition_appointment_status` | `event:appointment_status_changed` | | `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:submit_lead` | `event:lead_submitted` | diff --git a/ontology/rules.md b/ontology/rules.md index f5968f6..256a4fe 100644 --- a/ontology/rules.md +++ b/ontology/rules.md @@ -156,6 +156,16 @@ - **代码**:`backend/src/main/java/com/petstore/controller/ReportController.java#shortTokenHash`/`#trackReportOpen` - **证据**:`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 ### rule:BR-LEAD-001 @@ -316,7 +326,7 @@ ### 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` diff --git a/qa-reports/phase0-report-send-status-2026-08-02.md b/qa-reports/phase0-report-send-status-2026-08-02.md new file mode 100644 index 0000000..cf323b5 --- /dev/null +++ b/qa-reports/phase0-report-send-status-2026-08-02.md @@ -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` | PASS:175 tests,0 failures,0 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 | PASS:242 条目、引用完整 | +| Ontology drift | PASS:57/57 HTTP endpoints,13/13 JPA entities,0 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` 漏斗数字。 +- [ ] 生产变更另行授权;本交付未连接或修改生产数据库。 diff --git a/产品设计文档.md b/产品设计文档.md index 539e0d2..c1bf8f2 100644 --- a/产品设计文档.md +++ b/产品设计文档.md @@ -45,15 +45,16 @@ ## 二、核心功能闭环(v1) ``` -预约创建(宠主/老板/员工,下单即生效)→ 待开始(new) → 开始服务(剪毛/正式开做时点击,谁点谁技师)→ 进行中(doing) → 填写报告 → 发送报告 → 已完成(done) +预约创建(宠主/老板/员工,下单即生效)→ 待开始(new) → 开始服务(剪毛/正式开做时点击,谁点谁技师)→ 进行中(doing) → 提交报告并完成(done) → 发送给宠主 → 显式确认发送 ↘ 已取消(cancel) → 在「已完成」Tab 与完成单并列展示 ``` **报告分享流程(v1 重点):** ``` -填写报告(照片+备注)→ 点击「发送报告」→ 选择发送方式 - ├── 复制链接(发给宠主微信) - └── 下载二维码(现场扫码) +填写并提交报告(照片+备注)→ 选择传播方式 → 宠主实际收到 → 员工「确认已发送」 + ├── 复制链接/话术(发给宠主微信) + ├── 当面展示二维码(现场扫码) + └── 其他可核实方式 ``` --- @@ -151,6 +152,10 @@ | service_type | VARCHAR | 服务类型(冗余) | | appointment_time | DATETIME | 服务时间(冗余) | | 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 | 创建时间 | | update_time | DATETIME | 更新时间 | @@ -272,11 +277,18 @@ ### 5.4 发送报告(v1 核心功能) -**操作入口:** 报告填写完成后,点击「发送报告」按钮 +**操作入口:** 报告提交成功后进入「发送给宠主」弹层;后台报告列表也可继续处理待发送报告。 **发送方式:** 1. **复制链接** — 生成报告链接,一键复制到剪贴板,可发给宠主微信 -2. **下载二维码** — 生成报告二维码图片,保存到相册,可打印或现场扫码 +2. **当面扫码** — 展示报告二维码,宠主现场扫码打开 +3. **其他方式** — 仅在员工能够确认宠主实际收到时使用 + +**确认口径:** +- 复制链接、复制话术、生成/展示二维码、预览或打开报告,**都不自动等于已发送**。 +- 宠主实际收到后,由本店 boss/staff 选择真实渠道并点击「确认已发送」。 +- 首次确认写入 `sent_at/sent_by_user_id/send_channel` 和 `report_sent` 事件;重复点击幂等成功,不覆盖首次回执。 +- 历史报告没有可靠发送证据,迁移后统一为 `unknown`;只有迁移后新报告默认 `unsent` 并进入工作台待办。 **报告链接:** - 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_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 ( id BIGINT PRIMARY KEY AUTO_INCREMENT, diff --git a/本期主线-下一步产品优化点.md b/本期主线-下一步产品优化点.md index bce310f..46d341b 100644 --- a/本期主线-下一步产品优化点.md +++ b/本期主线-下一步产品优化点.md @@ -37,6 +37,7 @@ | 事件名 | 触发时机 | 关键字段 | 来源 | 统计窗口 | 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_reopened` | 报告页再次打开(非首次) | 同上,`visitType=repeat` | 同上 | 日 / 周 | Report Share QA | 同上 | | `highlight_success` | 成片生成成功 | `reportTokenHash`、`appointmentId`、`durationSec`、`composeMode`、`ts` | 后端成片任务 | 日 | Report Media Backend | 任务回调日志 | diff --git a/架构决策-业务事件落库-2026-08-01.md b/架构决策-业务事件落库-2026-08-01.md index 3b4aeb2..2f78844 100644 --- a/架构决策-业务事件落库-2026-08-01.md +++ b/架构决策-业务事件落库-2026-08-01.md @@ -47,10 +47,11 @@ | `service_started` | `new -> doing` | appointment ID | | `service_completed` | `doing -> done` | appointment ID | | `report_submitted` | 报告及图片保存成功 | report ID | +| `report_sent` | 门店员工首次显式确认宠主实际收到 | report ID;`report_sent:{reportId}` | | `report_opened` / `report_reopened` | 公开报告打开埋点 | 每次有效打开;统计按 report 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. 隐私边界 diff --git a/架构决策-报告确认发送状态-2026-08-02.md b/架构决策-报告确认发送状态-2026-08-02.md new file mode 100644 index 0000000..80dadba --- /dev/null +++ b/架构决策-报告确认发送状态-2026-08-02.md @@ -0,0 +1,84 @@ +# 架构决策:报告显式确认发送状态 + +日期: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/小程序构建及本体校验/漂移审计通过。 diff --git a/生产发布与回滚Runbook-2026-08-01.md b/生产发布与回滚Runbook-2026-08-01.md index bd58f2b..b805cf5 100644 --- a/生产发布与回滚Runbook-2026-08-01.md +++ b/生产发布与回滚Runbook-2026-08-01.md @@ -29,7 +29,7 @@ | Priority | P0 | | Repo/Path | `backend/deploy/**`、前端发布配置、`docs/**` | | Service Lock | Petstore backend unit + Petstore 专用 Nginx server blocks + Petstore static roots | -| Depends On | 冻结 RC、真实域名/TLS、备份、四个迁移、微信合法域名 | +| Depends On | 冻结 RC、真实域名/TLS、备份、五个迁移、微信合法域名 | | Acceptance | 预检、构建、readiness、只读 smoke、三角色人工 smoke、回滚演练均有证据 | | RC Included | Yes | | Due Date | TBD | @@ -47,7 +47,7 @@ - 四仓 commit、构建产物或校验和与 RC manifest 不一致; - 生产域名仍为 localhost、`.invalid`、`api.s-good.com` 或 `www.s-good.com` 等非本项目域名; - 数据库备份未完成、校验和缺失或未在隔离库验证可恢复; -- 任一迁移末尾验证查询非 0,或预检的 15 项只读不变量非 0; +- 任一迁移末尾验证查询非 0,或预检的 18 项只读不变量非 0; - `nginx -t` 失败,或 diff 显示将修改 Gitea/GitLab 的 server block; - readiness 非 `UP`,上传目录/FFmpeg/数据库任一依赖失败; - 发现凭据、完整 token、手机号或私密媒体地址进入日志/工单/截图; @@ -107,6 +107,7 @@ shasum -a 256 /secure/backup/petstore-before--.sql 2. `backend/db/migrations/20260801_create_store_customer.sql` 3. `backend/db/migrations/20260801_create_business_event.sql` 4. `backend/db/migrations/20260801_create_booking_capacity.sql` +5. `backend/db/migrations/20260801_create_report_send_status.sql` 禁止跳步、交换顺序或让应用自动补表。任一验证项非 0,停止发布并由 Data Model/业务 owner 判断修复方案;不得猜测性回填客户身份、报告作者或事件。 @@ -127,7 +128,7 @@ systemd-run --wait --pipe --collect \ ./deploy/release-preflight.sh ``` -通过条件:静态配置检查通过、JPA `validate` 通过、15 项数据不变量全部为 0,并以退出码 0 结束。预检不会执行迁移或修复数据。 +通过条件:静态配置检查通过、JPA `validate` 通过、18 项数据不变量全部为 0,并以退出码 0 结束。预检不会执行迁移或修复数据。 ## 6. 构建发布产物 @@ -205,7 +206,7 @@ API_ORIGIN=https:// ./deploy/production-smoke.sh ### 9.2 数据库回滚边界 -本批四个迁移以新增列/表/索引和兼容回填为主,旧应用应忽略新增结构。因此常规回滚只回应用,不删除列、表、索引,不执行自动 down migration,也不把已产生的新业务数据覆盖回旧备份。 +本批五个迁移以新增列/表/索引和兼容回填为主,旧应用应忽略新增结构。因此常规回滚只回应用,不删除列、表、索引,不执行自动 down migration,也不把已产生的新业务数据覆盖回旧备份。 只有确认发生灾难性数据破坏、已冻结所有写入、明确接受备份时间点之后的数据丢失,并取得用户/业务负责人、DBA 和隐私责任人的书面授权后,才可执行整库恢复。恢复必须先在隔离库验证,再制定专门变更单;本 Runbook 不授权该操作。 @@ -215,7 +216,7 @@ API_ORIGIN=https:// ./deploy/production-smoke.sh - 四仓冻结 commit 与产物校验和; - 备份及隔离恢复验证; -- 四个迁移执行和末尾验证结果; +- 五个迁移执行和末尾验证结果; - 最终配置门禁与后端只读预检; - `nginx -t`、readiness、自动只读 smoke; - boss/staff/customer 三角色人工 smoke; diff --git a/生产监控与告警基线-2026-08-01.md b/生产监控与告警基线-2026-08-01.md index eebe956..59443d6 100644 --- a/生产监控与告警基线-2026-08-01.md +++ b/生产监控与告警基线-2026-08-01.md @@ -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 无报告、报告无客户/作者、重复报告; - BusinessEvent 缺失或幂等键重复; - 服务时长/预约快照非法、容量超卖; -- 报告已提交但长期未打开; +- 新报告已提交但长期未确认发送; +- 已确认发送但长期未打开; - 回访后没有明确状态或再次预约归因。 发布前的 `release-preflight.sh` 已覆盖前四类结构/数据不变量;运行期应每天做等价的只读审计或报表,不自动修复。 @@ -82,7 +83,7 @@ - [ ] 外部 HTTPS 探测覆盖 liveness/readiness、admin、H5 报告入口; - [ ] 5xx、延迟、DB 池、磁盘、证书和进程重启已配置告警; - [ ] FFmpeg/上传失败有可聚合字段,而非只能搜索自由文本; -- [ ] BusinessEvent 日漏斗和 15 项只读数据审计有每日结果; +- [ ] BusinessEvent 日漏斗和 18 项只读数据审计有每日结果; - [ ] 通知渠道、Primary/Backup 值班人和升级联系人完成真实测试; - [ ] 日志脱敏抽查通过,访问权限和保留周期已批准; - [ ] 发布标记能关联四仓 commit 与 RC; diff --git a/试点门店开通清单-2026-08-01.md b/试点门店开通清单-2026-08-01.md index d8f3905..338803e 100644 --- a/试点门店开通清单-2026-08-01.md +++ b/试点门店开通清单-2026-08-01.md @@ -96,7 +96,8 @@ | 完整服务数 | 符合上方五项的服务 | 判断样本进度 | | 预约完成率 | done / 有效预约 | 判断主链路稳定性 | | 报告提交率 | 有报告的 done / done | 判断员工执行成本 | -| 报告打开率 | 首次打开报告 / 已提交报告 | 判断宠主价值 | +| 报告确认发送率 | 显式确认发送 / 已提交报告 | 判断门店触达执行 | +| 报告打开率 | 首次打开报告 / 已确认发送报告 | 判断宠主价值;历史 unknown 不纳入分母 | | 留资率 | 去重留资 / 首次打开报告 | 判断回访意愿 | | 再次预约率 | 报告后窗口内产生新预约的 StoreCustomer / 可观察客户 | 判断闭环价值 | | 容量冲突率 | 因无容量被拒绝的请求 / 可约请求 | 校准容量而非承诺零等待 | diff --git a/门店管理后台-PhaseA-交付目标.md b/门店管理后台-PhaseA-交付目标.md index e9c396a..71e3ac6 100644 --- a/门店管理后台-PhaseA-交付目标.md +++ b/门店管理后台-PhaseA-交付目标.md @@ -20,7 +20,7 @@ | 模块 | 状态 | 主要缺口 | |------|------|----------| | A0 壳层登录 | ~95% | 生产微信登录策略另议 | -| A1 工作台 | ~98% | 服务端准确行动聚合 + 低敏预览已做;发送状态与转预约归因仍待后续模型,真实环境 smoke 待补 | +| A1 工作台 | ~99% | 服务端准确行动聚合、低敏预览、报告待发送行动与显式发送漏斗已做;仅转预约归因和真实环境 smoke 待补 | | A2 预约 | ~95% | 详情/筛选/待出报告/宠主脱敏已做 | | A6 排班 | ~90% | 与小程序对照验收 | | A3 报告 | ~95% | 详情抽屉+失败文案+H5 | diff --git a/门店管理后台-PhaseA-线框说明.md b/门店管理后台-PhaseA-线框说明.md index 93c8a7b..0f285d3 100644 --- a/门店管理后台-PhaseA-线框说明.md +++ b/门店管理后台-PhaseA-线框说明.md @@ -45,9 +45,10 @@ ② 服务中等待收尾 n 最近 3 条 …………………… [全部处理] ③ 今天接下来到店 n 最近 3 条 …………………… [全部处理] ④ 成片异常 n 最近 3 条 …………………… [全部处理] -⑤ 回访日期已到 n 最近 3 条 …………………… [全部处理] +⑤ 报告尚未发送 n 最近 3 条 …………………… [全部处理] +⑥ 回访日期已到 n 最近 3 条 …………………… [全部处理] -报告漏斗 已提交 | 确认发送— | 已打开 | 已留资 | 转预约— +报告漏斗 已提交 | 已确认发送 | 已打开 | 已留资 | 转预约— ``` **主操作**:点指标、行动组或具体低敏预览下钻;工作台常驻,不用先关弹窗。 diff --git a/门店管理后台-PhaseA-编码任务brief.md b/门店管理后台-PhaseA-编码任务brief.md index 218e84c..5b53908 100644 --- a/门店管理后台-PhaseA-编码任务brief.md +++ b/门店管理后台-PhaseA-编码任务brief.md @@ -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]` | | Access / Privacy Review Required | Yes | -**验收**:对齐 PRD §1–3 与线框 A0–A2;staff/boss 可开始服务;customer 进不去;待办下钻参数正确;报告提交/打开/留资来自本店 BusinessEvent 汇总,未跟踪的转预约显示未知。 +**验收**:对齐 PRD §1–3 与线框 A0–A2;staff/boss 可开始服务;customer 进不去;待办下钻参数正确;报告提交/显式确认发送/打开/留资来自本店 BusinessEvent 汇总,未跟踪的转预约显示未知。 **验证**:`AdminBusinessEventControllerTest` + `AdminBusinessEventServiceTest` + admin 构建;发布后按 PRD §3.6 / §2.6 手工冒烟。 --- diff --git a/门店管理后台-PhaseA-页面清单PRD.md b/门店管理后台-PhaseA-页面清单PRD.md index 45dc2df..3c0da23 100644 --- a/门店管理后台-PhaseA-页面清单PRD.md +++ b/门店管理后台-PhaseA-页面清单PRD.md @@ -92,7 +92,7 @@ entity:admin_console 常驻行动队列(不再每日强弹) 报告漏斗四格(提交/打开/留资已由 BusinessEvent 汇总;转预约仍为「—」) 每组最多 3 条低敏预览 + 准确总数 + 列表下钻 -报告漏斗(提交 / 发送未知 / 打开 / 留资 / 再预约未知) +报告漏斗(提交 / 显式确认发送 / 打开 / 留资 / 再预约未知) ``` ### 2.2 顶栏指标卡 @@ -112,9 +112,10 @@ entity:admin_console | `in_service` | 服务中,等待收尾 | 所有 `doing`;提交报告后自动退出 | 预约列表 `status=doing&missingReport=1` | | `upcoming_today` | 今天接下来要到店 | 今日尚未到时刻的 `new` | 预约列表 `status=new&date=today` | | `highlight_anomaly` | 成片异常 | `failed` + `processing` 超过 30 分钟 | 报告列表 `highlightStatus=anomaly` | +| `unsent_report` | 报告尚未发送 | 仅 `sendStatus=unsent`;历史 `unknown` 不进入待办 | 报告列表 `sendStatus=unsent` | | `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 汇总 | | 已留资 | `lead_submitted` 按 lead 去重 | BusinessEvent 汇总 | | 报告转预约 | `rebook_created` | 尚无可靠归因,显示「—」 | diff --git a/门店管理后台升级方案-对标宠老板.md b/门店管理后台升级方案-对标宠老板.md index dcd841c..77401cd 100644 --- a/门店管理后台升级方案-对标宠老板.md +++ b/门店管理后台升级方案-对标宠老板.md @@ -36,7 +36,7 @@ |------|------|------| | 预约状态机 / 号源 / 排班占用 | ✅ | 小程序门店端 | | 服务报告 + 前后照片 + 成片 | ✅ 差异化核心 | 小程序 + 公开 H5 | -| 报告分享 / 打开埋点 / 留资回访 | ✅ 部分 | 公开页 + Leads | +| 报告分享 / 显式发送确认 / 打开埋点 / 留资回访 | ✅ | 小程序 + Admin + 公开页 + Leads | | 门店设置 / 员工 / 服务类型 | ✅ 基础 | 小程序「我的」 | | 会员卡 / 储值 / 套餐 | ❌ gap 占位 | 本体未展开 | | 收银 / 库存 / 寄养 / 提成 / 进货 | ❌ | 明确不在 P0 | @@ -443,7 +443,7 @@ Web 后台主攻:**经营、检索、批量、报表、设置**;**允许「 └──────────────────┴──────────────────────────┴──────────────────┘ ``` -**报告漏斗四格**(替代微信商城四格):`今日已提交报告` · `已打开` · `已留资` · `从报告发起的预约`——前三项已由 BusinessEvent 汇总;报告发送和转预约归因未建模前显示「—」,不以提交冒充发送。 +**报告漏斗**(替代微信商城状态):`今日已提交报告` · `已确认发送` · `已打开` · `已留资` · `从报告发起的预约`——前四项已由 BusinessEvent 汇总;发送只认员工显式确认,不以提交、复制链接或展示二维码冒充;仅转预约归因仍显示「—」。 ### 11.7 截图覆盖度与仍缺清单(2026-07-10 刷新)