diff --git a/P0-研发落地清单.md b/P0-研发落地清单.md index 1f23aec..c22184f 100644 --- a/P0-研发落地清单.md +++ b/P0-研发落地清单.md @@ -64,6 +64,7 @@ | **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`;不宣称微信投递成功 | | **A18** | **门店开通 + 一次性员工邀请 + AuditLog** | 老板入驻须微信核验;历史开通状态 unknown;完成清单需资料/容量/服务;邀请 token 只显一次且库内仅 hash,手机号微信核验、限时/一次/可撤销;删员工立即失效旧会话 | Backend Core / Store Miniapp FE / Store Admin FE / Data Model | ✅ 代码完成(待生产迁移与微信真机验收) | backend 全测 + admin/H5/小程序构建 + 本体漂移审计 | 迁移见 `backend/db/migrations/20260802_create_store_onboarding.sql`;单角色账号冲突明确拒绝 | | **A19** | **StoreCustomer 事实时间线**:客户详情不再只显示当前聚合快照 | 以 canonical `storeCustomerId` 同店校验;BusinessEvent 按发生时间稳定分页;合并前别名事件不断档;预约/报告/回访提供低敏摘要和跳转;不返回手机号原文、备注、媒体、token 或原始 metadata | Backend Core / Store Admin FE | ✅ 代码完成(待生产迁移) | backend 全测 + admin build + 本体漂移审计 | 迁移见 `backend/db/migrations/20260802_create_store_customer_timeline.sql`;既有事件不改写 | +| **A20** | **FollowUpTask 回访状态机与真实再次预约归因** | 留资与操作任务拆分;领取人独占处理;未接通/稍后联系可改期;终态不可回退;退订取消开放任务;只有真实预约保存成功才能记 `rebooked/rebook_created`;跨店 404,事件/审计低敏 | Backend Core / Store Admin FE / Data Model | ✅ 代码完成(待生产迁移与三角色验收) | backend 全测 + admin build + 本体漂移审计 | 迁移见 `backend/db/migrations/20260802_create_follow_up_task.sql`;`ReportLead.remind_status` 仅兼容投影 | --- @@ -135,7 +136,7 @@ | ID | 需求 | 验收标准 | Owner | 状态 | 验收命令 | 备注 | |----|------|----------|-------|------|----------|------| -| **H1** | **后端可重复发布门禁** | README + production profile + 配置校验 + schema/26 项只读预检 + liveness/readiness + 只读 smoke + systemd/Nginx 模板 | Backend Ops | ✅ 仓库能力已实现;待真实环境执行 | backend 全测/打包 + `bash -n deploy/*.sh` + Runbook | 不把模板/本地通过写成已上线 | +| **H1** | **后端可重复发布门禁** | README + production profile + 配置校验 + schema/31 项只读预检 + 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 | @@ -164,7 +165,7 @@ - 报告前后照片缺失返回对应业务码。 - A 店 boss/staff 不能查 B 店 leads/list/report;customer 不能用任意 `userId` 创建他人宠物/预约。 - 生产配置扫描不含真实 DB/微信密钥,最终 profile 为 `ddl-auto=validate`,CORS 仅允许显式生产 HTTPS 源;只读生产预检通过。 -- 已按顺序完成身份、StoreCustomer、BusinessEvent、预约容量四个生产迁移,并归档备份与验证证据。 +- 已按固定顺序完成身份、StoreCustomer、BusinessEvent、预约容量、报告发送、门店开通、客户时间线与 FollowUpTask 八个生产迁移,并归档备份与验证证据。 - liveness/readiness、只读 smoke 与 boss/staff/customer 三角色真实环境 smoke 通过;Gitea/GitLab 未受 Petstore 配置影响。 - 文档口径扫描:`rg "到店不排队|会员储值 \| 路线图 P0|宠主通过微信查看服务报告(v2)|无限制,随便选" docs` 无命中。 diff --git a/coverage/ontology-coverage-audit.md b/coverage/ontology-coverage-audit.md index 7fc69f4..1c9ef58 100644 --- a/coverage/ontology-coverage-audit.md +++ b/coverage/ontology-coverage-audit.md @@ -3,7 +3,7 @@ > 本体条目 ↔ 文档章节 ↔ 代码路径 ↔ 测试用例 三方对齐审计。 > 证据强度:`anchored`(三处对齐)/ `documented`(仅文档)/ `implemented`(仅代码)/ `gap`(缺失)。 > -> **刷新时间**:2026-08-02(StoreCustomer 事实时间线收口后刷新) +> **刷新时间**:2026-08-02(FollowUpTask 回访闭环收口后刷新) ## 审计方法 @@ -18,14 +18,14 @@ | 类型 | 数量 | |------|------| -| Entity | 28 | -| Action | 74 | -| Event | 27 | -| Rule | 47 | -| Relation | 89 | -| **合计** | **265** | +| Entity | 29 | +| Action | 77 | +| Event | 33 | +| Rule | 49 | +| Relation | 102 | +| **合计** | **290** | -证据强度:`anchored` 219(82.6%)/ `documented` 33 / `gap` 11 / `implemented` 2。 +证据强度:`anchored` 249(85.9%)/ `documented` 28 / `gap` 11 / `implemented` 2。 > anchored 占比相对 Batch 2 下降,是因为主动录入了 P1/P2 `gap` 占位与客户端埋点 `documented` 条目;端点漂移仍为 0。 @@ -36,7 +36,7 @@ python3 docs/graph/validate_ontology.py docs python3 docs/graph/audit_drift.py ``` -当前结果:校验通过;HTTP 端点 66/66、JPA Entity 15/15 对齐;非 JPA 领域实体 13 个;代码与本体均无单边漂移。 +当前结果:校验通过;HTTP 端点 70/70、JPA Entity 16/16 对齐;非 JPA 领域实体 13 个;代码与本体均无单边漂移。 ## 元数据卫生 @@ -56,6 +56,7 @@ python3 docs/graph/audit_drift.py | appointment | ✅ | ✅ | AppointmentServiceTest / DetailTest | anchored | | | report | ✅ | ✅ | ReportServiceTest / ControllerTest | anchored | uk 上线需清重 | | report_lead | ✅ | ✅ | ReportLeadControllerTest | anchored | | +| follow_up_task | ✅ | ✅ | FollowUpTaskServiceTest / MigrationTest / ControllerTest | anchored | 留资事实与操作状态拆分;真实预约引用 | | pet | ✅ | ✅ | PetControllerTest | anchored | | | service_type | ✅ | ✅ | ServiceTypeControllerTest | anchored | | | schedule_block | ✅ | ✅ | ScheduleControllerTest | anchored | | @@ -73,7 +74,7 @@ python3 docs/graph/audit_drift.py | current_user | ✅ | ✅ record | SessionTokenServiceTest | anchored | jpa:false | | membership 等 5 个 | 方向库 | — | — | gap | roadmap 占位 | -## 规则覆盖度(47 条) +## 规则覆盖度(49 条) | 规则 | 测试 | 证据 | |------|------|------| @@ -99,6 +100,7 @@ python3 docs/graph/audit_drift.py | BR-INVITE-001 | StaffInvitationServiceTest / AuthInterceptorActiveUserTest | anchored | | BR-AUDIT-001 | AuditLogServiceTest / StoreOnboardingMigrationTest | anchored | | BR-SC-002 客户时间线低敏投影 | StoreCustomerTimelineServiceTest / AdminServiceCustomerControllerTest | anchored | +| BR-FU-001/002 回访状态机与真实再次预约 | FollowUpTaskServiceTest / AppointmentServiceTest / MigrationTest | anchored | ## 待补 gap 清单 @@ -134,7 +136,7 @@ python3 docs/graph/audit_drift.py | `entity:admin_console` | Entity | Web 管理端壳 | | `entity:workbench` / `workbench_todo_item` | Entity | 服务端准确行动聚合、低敏预览与报告事件漏斗均 anchored | | `entity:store_customer` | Entity | 门店客户稳定主档(非会员卡) | -| `action:view_workbench` 等 8 个 admin_* | Action | 工作台/列表/设置/排班 | +| `action:view_workbench`、8 个 admin_* 与 3 个回访命令 | Action | 工作台/列表/设置/排班/领取/改期/关闭 | | `rule:BR-ADMIN-001`…`004` | Rule | 角色、店范围、内部字段、范围闸门 | | `event:workbench_viewed` | Event | 可选埋点(gap) | @@ -142,4 +144,4 @@ python3 docs/graph/audit_drift.py **页面清单 PRD(已写)**:`docs/门店管理后台-PhaseA-页面清单PRD.md`(A0–A7 全页字段/筛选/API/验收)。 -当前 validate 规模以脚本输出为准(265 条目)。 +当前 validate 规模以脚本输出为准(290 条目)。 diff --git a/graph/ontology.jsonl b/graph/ontology.jsonl index d5f2f29..96f2e76 100644 --- a/graph/ontology.jsonl +++ b/graph/ontology.jsonl @@ -11,7 +11,7 @@ {"id":"action:login","type":"Action","name":"login","nameZh":"手机号验证码登录","inputs":["phone","code"],"outputs":["entity:user","entity:store","session_token"],"entrypoints":["POST /api/user/login","UserService#login"],"appliesRules":["rule:BR-AUTH-001","rule:BR-AUTH-002"],"code":"backend/src/main/java/com/petstore/service/UserService.java#login","test":"UserServiceLoginStrategyTest","evidence":"anchored","domain":"auth"} {"id":"action:wx_phone_login","type":"Action","name":"wxPhoneLogin","nameZh":"微信手机号授权登录","inputs":["phoneCode","loginCode"],"outputs":["entity:user","entity:store","session_token"],"entrypoints":["POST /api/user/wx-phone-login","UserController#wxPhoneLogin","UserService#loginByVerifiedPhone"],"appliesRules":["rule:BR-AUTH-001"],"code":"backend/src/main/java/com/petstore/controller/UserController.java#wxPhoneLogin","evidence":"anchored","domain":"auth"} {"id":"action:bind_wechat","type":"Action","name":"bindWechat","nameZh":"绑定微信标识","inputs":["userId","openid","unionid"],"outputs":["entity:user"],"entrypoints":["UserService#bindWechatMiniIdentity"],"appliesRules":[],"code":"backend/src/main/java/com/petstore/service/UserService.java#bindWechatMiniIdentity","evidence":"anchored","domain":"auth"} -{"id":"action:create_appointment","type":"Action","name":"createAppointment","nameZh":"创建预约","inputs":["petName","petType","serviceType","appointmentTime","storeId","remark","petId","customerUserId","customerPhone","customerName"],"outputs":["entity:appointment"],"entrypoints":["POST /api/appointment/create","AppointmentController#create","AppointmentService#createBooking"],"appliesRules":["rule:BR-APPT-001","rule:BR-APPT-003","rule:BR-APPT-004","rule:BR-SC-001","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/controller/AppointmentController.java#create; backend/src/main/java/com/petstore/service/AppointmentService.java#createBooking; backend/src/main/java/com/petstore/service/StoreCustomerService.java#touchBooking; backend/src/main/java/com/petstore/service/BusinessEventService.java#recordAppointmentCreated","test":"AppointmentControllerIdentityTest; AppointmentServiceTest; StoreCustomerServiceTest; BusinessEventServiceTest","evidence":"anchored","domain":"appointment"} +{"id":"action:create_appointment","type":"Action","name":"createAppointment","nameZh":"创建预约","inputs":["petName","petType","serviceType","appointmentTime","storeId","remark","petId","customerUserId","customerPhone","customerName","followUpTaskId"],"outputs":["entity:appointment","entity:follow_up_task"],"entrypoints":["POST /api/appointment/create","AppointmentController#create","AppointmentService#createBooking"],"appliesRules":["rule:BR-APPT-001","rule:BR-APPT-003","rule:BR-APPT-004","rule:BR-SC-001","rule:BR-BE-001","rule:BR-FU-002"],"code":"backend/src/main/java/com/petstore/controller/AppointmentController.java#create; backend/src/main/java/com/petstore/service/AppointmentService.java#createBooking; backend/src/main/java/com/petstore/service/FollowUpTaskService.java#prepareRebook; #markRebooked; backend/src/main/java/com/petstore/service/StoreCustomerService.java#touchBooking; backend/src/main/java/com/petstore/service/BusinessEventService.java#recordAppointmentCreated","test":"AppointmentControllerIdentityTest; AppointmentServiceTest; FollowUpTaskServiceTest; StoreCustomerServiceTest; BusinessEventServiceTest","evidence":"anchored","domain":"appointment"} {"id":"action:start_service","type":"Action","name":"startService","nameZh":"开始服务","inputs":["appointmentId"],"outputs":["entity:appointment"],"entrypoints":["POST /api/appointment/start","AppointmentController#start","AppointmentService#startService"],"appliesRules":["rule:BR-APPT-001","rule:BR-APPT-002","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/controller/AppointmentController.java#start; backend/src/main/java/com/petstore/service/BusinessEventService.java#recordAppointmentStatusChanged","test":"AppointmentServiceTest; BusinessEventServiceTest","evidence":"anchored","domain":"appointment"} {"id":"action:transition_appointment_status","type":"Action","name":"transitionAppointmentStatus","nameZh":"更新预约状态","inputs":["id","status"],"outputs":["entity:appointment"],"entrypoints":["PUT /api/appointment/status","AppointmentController#updateStatus","AppointmentService#transitionStatus"],"appliesRules":["rule:BR-APPT-001","rule:BR-APPT-002","rule:BR-APPT-003","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/controller/AppointmentController.java#updateStatus; backend/src/main/java/com/petstore/service/BusinessEventService.java#recordAppointmentStatusChanged","test":"AppointmentServiceTest; BusinessEventServiceTest","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"} @@ -21,8 +21,8 @@ {"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"} -{"id":"action:submit_lead","type":"Action","name":"submitLead","nameZh":"宠主留资","inputs":["token","phone","consent","loginCode"],"outputs":["leadId","remindDate","unsubscribeToken","wechatBound"],"entrypoints":["POST /api/report/{token}/reminder","ReportLeadController#submitReminder","ReportLeadService#submit"],"appliesRules":["rule:BR-LEAD-001","rule:BR-SC-001","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/controller/ReportLeadController.java#submitReminder; backend/src/main/java/com/petstore/service/ReportLeadService.java#submit; backend/src/main/java/com/petstore/service/StoreCustomerService.java#touchLead; backend/src/main/java/com/petstore/service/BusinessEventService.java#recordLeadSubmitted","evidence":"anchored","test":"ReportLeadControllerTest; ReportLeadServiceStoreCustomerTest; StoreCustomerServiceTest; BusinessEventServiceTest","domain":"lead"} -{"id":"action:unsubscribe","type":"Action","name":"unsubscribe","nameZh":"一键退订","inputs":["unsubscribeToken"],"outputs":[],"entrypoints":["POST /api/report/unsubscribe","ReportLeadController#unsubscribe"],"appliesRules":["rule:BR-UNSUB-001"],"code":"backend/src/main/java/com/petstore/controller/ReportLeadController.java#unsubscribe","evidence":"anchored","domain":"lead","test":"ReportLeadControllerTest"} +{"id":"action:submit_lead","type":"Action","name":"submitLead","nameZh":"宠主留资","inputs":["token","phone","consent","loginCode"],"outputs":["leadId","remindDate","unsubscribeToken","wechatBound","entity:follow_up_task"],"entrypoints":["POST /api/report/{token}/reminder","ReportLeadController#submitReminder","ReportLeadService#submit"],"appliesRules":["rule:BR-LEAD-001","rule:BR-SC-001","rule:BR-BE-001","rule:BR-FU-001"],"code":"backend/src/main/java/com/petstore/controller/ReportLeadController.java#submitReminder; backend/src/main/java/com/petstore/service/ReportLeadService.java#submit; backend/src/main/java/com/petstore/service/FollowUpTaskService.java#ensureForLead; backend/src/main/java/com/petstore/service/StoreCustomerService.java#touchLead; backend/src/main/java/com/petstore/service/BusinessEventService.java#recordLeadSubmitted","evidence":"anchored","test":"ReportLeadControllerTest; ReportLeadServiceStoreCustomerTest; FollowUpTaskServiceTest; StoreCustomerServiceTest; BusinessEventServiceTest","domain":"lead"} +{"id":"action:unsubscribe","type":"Action","name":"unsubscribe","nameZh":"一键退订","inputs":["unsubscribeToken"],"outputs":["entity:follow_up_task"],"entrypoints":["POST /api/report/unsubscribe","ReportLeadController#unsubscribe"],"appliesRules":["rule:BR-UNSUB-001","rule:BR-FU-001"],"code":"backend/src/main/java/com/petstore/controller/ReportLeadController.java#unsubscribe; backend/src/main/java/com/petstore/service/FollowUpTaskService.java#cancelForUnsubscribe","evidence":"anchored","domain":"lead","test":"ReportLeadControllerTest; FollowUpTaskServiceTest"} {"id":"action:list_leads","type":"Action","name":"listLeads","nameZh":"回访池列表","inputs":["status"],"outputs":["entity:report_lead"],"entrypoints":["GET /api/report/leads","ReportLeadController#leads"],"appliesRules":["rule:BR-LEAD-002","rule:BR-LEAD-003"],"code":"backend/src/main/java/com/petstore/controller/ReportLeadController.java#leads","evidence":"anchored","test":"ReportLeadControllerTest","domain":"lead"} {"id":"event:store_registered","type":"Event","name":"StoreRegistered","nameZh":"门店入驻","domain":"store","payload":["storeId","bossUserId","occurredAt"],"emittedBy":["action:register_boss"],"code":"backend/src/main/java/com/petstore/service/BusinessEventService.java#recordStoreRegistered","test":"BusinessEventServiceTest","evidence":"anchored","note":"持久化 BusinessEvent 与 AuditLog,均不含手机号或 legacy inviteCode"} {"id":"event:session_issued","type":"Event","name":"SessionIssued","nameZh":"签发会话令牌","domain":"auth","payload":["userId","storeId","role","exp"],"emittedBy":["action:register_boss","action:login","action:wx_phone_login","action:accept_staff_invitation"],"code":"backend/src/main/java/com/petstore/auth/SessionTokenService.java#issue","test":"SessionTokenServiceTest","evidence":"anchored"} @@ -36,7 +36,7 @@ {"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"} -{"id":"rule:BR-AUTH-001","type":"Rule","name":"HMAC session token + 活跃账号范围鉴权","domain":"auth","appliesTo":["action:update_store","action:delete_store","action:get_store_by_invite_code","action:create_appointment","action:start_service","action:transition_appointment_status","action:get_appointment_detail","action:list_appointments","action:submit_report","action:delete_report","action:list_leads","action:get_store_onboarding","action:complete_store_onboarding","action:create_staff_invitation","action:list_staff_invitations","action:revoke_staff_invitation","action:list_staff","action:delete_staff"],"code":"backend/src/main/java/com/petstore/auth/AuthInterceptor.java; SessionTokenService.java; CurrentUserContext.java","test":"SessionTokenServiceTest; AuthInterceptorActiveUserTest","evidence":"anchored","note":"除签名和过期外,每次受保护请求校验账号仍存在且 role/storeId 与 token 一致,门店角色还须归属有效 Store"} +{"id":"rule:BR-AUTH-001","type":"Rule","name":"HMAC session token + 活跃账号范围鉴权","domain":"auth","appliesTo":["action:update_store","action:delete_store","action:get_store_by_invite_code","action:create_appointment","action:start_service","action:transition_appointment_status","action:get_appointment_detail","action:list_appointments","action:submit_report","action:delete_report","action:list_leads","action:admin_list_leads","action:start_follow_up_task","action:reschedule_follow_up_task","action:close_follow_up_task","action:get_store_onboarding","action:complete_store_onboarding","action:create_staff_invitation","action:list_staff_invitations","action:revoke_staff_invitation","action:list_staff","action:delete_staff"],"code":"backend/src/main/java/com/petstore/auth/AuthInterceptor.java; SessionTokenService.java; CurrentUserContext.java","test":"SessionTokenServiceTest; AuthInterceptorActiveUserTest; AdminFollowUpTaskControllerTest","evidence":"anchored","note":"除签名和过期外,每次受保护请求校验账号仍存在且 role/storeId 与 token 一致,门店角色还须归属有效 Store"} {"id":"rule:BR-AUTH-002","type":"Rule","name":"生产登录策略:仅微信授权","domain":"auth","appliesTo":["action:login","action:wx_phone_login"],"code":"backend/src/main/java/com/petstore/service/UserService.java#login; SmsController.java","test":"UserServiceLoginStrategyTest","evidence":"anchored"} {"id":"rule:BR-STORE-001","type":"Rule","name":"店铺更新/删除仅 boss + 同店","domain":"store","appliesTo":["action:update_store","action:delete_store"],"code":"backend/src/main/java/com/petstore/controller/StoreController.java#update; #delete","test":"StoreControllerTest","evidence":"anchored"} {"id":"rule:BR-STORE-002","type":"Rule","name":"legacy 永久邀请码禁用","domain":"store","appliesTo":["action:get_store_by_invite_code","action:register_staff"],"code":"backend/src/main/java/com/petstore/controller/StoreController.java#getByInviteCode; backend/src/main/java/com/petstore/controller/UserController.java#registerStaff","test":"StoreControllerTest","evidence":"anchored","note":"legacy invite_code 保留数据库兼容,不返回客户端、不再作为注册凭证"} @@ -54,7 +54,7 @@ {"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; StoreCustomerTimelineMigrationTest","evidence":"anchored","note":"被吸收投影软删除并指向同店 canonical;既有 BusinessEvent 不改写"} -{"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:start_follow_up_task","action:reschedule_follow_up_task","action:close_follow_up_task","action:unsubscribe","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; FollowUpTaskServiceTest","evidence":"anchored"} {"id":"rule:BR-LEAD-002","type":"Rule","name":"留资标识脱敏","domain":"lead","appliesTo":["action:list_leads"],"code":"backend/src/main/java/com/petstore/controller/ReportLeadController.java#maskIdentifier; #leads","frontend":"frontend/src/pages/mine/Leads.vue","evidence":"anchored"} {"id":"rule:BR-LEAD-003","type":"Rule","name":"回访池跨店 403","domain":"lead","appliesTo":["action:list_leads"],"code":"backend/src/main/java/com/petstore/controller/ReportLeadController.java#leads","evidence":"anchored"} {"id":"rel:store_has_users","type":"Relation","subject":"entity:store","predicate":"HAS","object":"entity:user","cardinality":"1—N","evidence":"anchored","doc":"User.store_id → Store.id","domain":"store"} @@ -139,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 + 26 项只读不变量;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 + 31 项只读不变量;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"} @@ -206,18 +206,18 @@ {"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","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","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":"服务端按完整本店数据准确聚合行动;回访待办来自 FollowUpTask;每组最多 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","merged_into_store_customer_id","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; StoreCustomerTimelineMigrationTest","evidence":"anchored","note":"门店与客户的稳定关系实体;被吸收投影以同店 canonical 指针保留不可变事件连续性;不含余额/次卡/积分。","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。"} +{"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:start_follow_up_task","action:reschedule_follow_up_task","action:close_follow_up_task","action:admin_list_service_customers","action:admin_update_settings"],"code":"backend/src/main/java/com/petstore/controller/AdminFollowUpTaskController.java","doc":"docs/ontology/rules.md#rule:BR-ADMIN-001; docs/门店管理后台-PhaseA-页面清单PRD.md","test":"AdminFollowUpTaskControllerTest","evidence":"anchored","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:start_follow_up_task","action:reschedule_follow_up_task","action:close_follow_up_task","action:admin_list_service_customers","action:admin_update_settings"],"code":"backend/src/main/java/com/petstore/controller/AdminFollowUpTaskController.java; backend/src/main/java/com/petstore/service/FollowUpTaskService.java","doc":"docs/ontology/rules.md#rule:BR-ADMIN-002","test":"AdminFollowUpTaskControllerTest; FollowUpTaskServiceTest","evidence":"anchored","note":"所有后台查询/写操作强制 current.storeId;忽略跨店请求参数。连锁同步不在 Phase A。"} {"id":"rule:BR-ADMIN-003","type":"Rule","name":"adminInternalFieldsAllowed","nameZh":"后台内部字段按用途最小返回","domain":"admin","appliesTo":["action:admin_list_reports","action:admin_update_settings","action:admin_list_service_customers","action:list_staff_invitations"],"code":"","doc":"docs/ontology/rules.md#rule:BR-ADMIN-003","test":"","evidence":"documented","note":"后台可返回报告内部状态;legacy inviteCode 不再返回。员工邀请列表仅返回脱敏手机号且不返回 token/hash,原始 token 仅创建响应返回一次。"} {"id":"rule:BR-ADMIN-004","type":"Rule","name":"adminPhaseAOutOfScope","nameZh":"Phase A 后台范围闸门","domain":"admin","appliesTo":["entity:admin_console","entity:workbench"],"code":"","doc":"docs/ontology/rules.md#rule:BR-ADMIN-004","test":"","evidence":"documented","note":"禁止将 membership/stored_value/service_package/收银/库存/寄养/押金/点餐/支付宝渠道 写入 Phase A 后台范围或设计稿。上述保持 evidence=gap。"} {"id":"action:view_workbench","type":"Action","name":"viewWorkbench","nameZh":"查看今日行动工作台","domain":"admin","inputs":["date","entity:current_user"],"outputs":["entity:workbench","entity:workbench_todo_item"],"entrypoints":["GET /api/admin/workbench/actions","GET /api/admin/workbench/report-funnel","Admin UI /workbench"],"appliesRules":["rule:BR-ADMIN-001","rule:BR-ADMIN-002","rule:BR-ADMIN-004","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/controller/AdminBusinessEventController.java#actions; #reportFunnel; backend/src/main/java/com/petstore/service/AdminWorkbenchService.java; backend/src/main/java/com/petstore/service/AdminBusinessEventService.java; admin/src/views/WorkbenchView.vue","doc":"docs/ontology/actions.md#action:view_workbench","test":"AdminBusinessEventControllerTest; AdminWorkbenchServiceTest; AdminBusinessEventServiceTest","evidence":"anchored","note":"按会话 storeId 聚合完整本店行动与低敏预览;事件漏斗按日期汇总。"} {"id":"action:drilldown_workbench_todo","type":"Action","name":"drilldownWorkbenchTodo","nameZh":"工作台行动下钻","domain":"admin","inputs":["entity:workbench_todo_item"],"outputs":[],"entrypoints":["Admin UI workbench action click"],"appliesRules":["rule:BR-ADMIN-001","rule:BR-ADMIN-002"],"code":"admin/src/views/WorkbenchView.vue; admin/src/views/AppointmentsView.vue","doc":"docs/ontology/actions.md#action:drilldown_workbench_todo","evidence":"anchored","note":"按 kind 跳转 appointments/reports/leads 并带筛选;预约预览可按受保护 ID 详情打开。"} {"id":"action:admin_list_appointments","type":"Action","name":"adminListAppointments","nameZh":"后台预约列表","domain":"admin","inputs":["status","dateFrom","dateTo","assignedUserId","page","pageSize"],"outputs":["entity:appointment"],"entrypoints":["Admin UI /appointments (planned)","reuses GET /api/appointment/list"],"appliesRules":["rule:BR-ADMIN-001","rule:BR-ADMIN-002","rule:BR-APPT-001","rule:BR-APPT-002"],"code":"backend/.../AppointmentController.java#list (reuse)","doc":"docs/ontology/actions.md#action:admin_list_appointments","evidence":"documented","note":"Web 列表增强筛选;业务规则复用现有 Appointment*,不另造状态机。;Web 允许调用 start_service(产品决策 2026-07-10)"} {"id":"action:admin_list_reports","type":"Action","name":"adminListReports","nameZh":"后台报告列表","domain":"admin","inputs":["dateFrom","dateTo","highlightStatus","page","pageSize"],"outputs":["entity:report","entity:highlight_video"],"entrypoints":["Admin UI /reports (planned)","reuses GET /api/report/list"],"appliesRules":["rule:BR-ADMIN-001","rule:BR-ADMIN-002","rule:BR-ADMIN-003","rule:BR-RPT-005"],"code":"backend/.../ReportController.java#list (reuse)","doc":"docs/ontology/actions.md#action:admin_list_reports","evidence":"documented","note":"含成片三态、复制公开链接、触发重生成(复用 generate_highlight)。"} -{"id":"action:admin_list_leads","type":"Action","name":"adminListLeads","nameZh":"后台回访线索列表","domain":"admin","inputs":["remindStatus","remindDateTo","page","pageSize"],"outputs":["entity:report_lead"],"entrypoints":["Admin UI /leads (planned)","reuses GET /api/report/leads"],"appliesRules":["rule:BR-ADMIN-001","rule:BR-ADMIN-002","rule:BR-LEAD-002","rule:BR-LEAD-003"],"code":"backend/.../ReportLeadController.java#leads (reuse)","doc":"docs/ontology/actions.md#action:admin_list_leads","evidence":"documented"} +{"id":"action:admin_list_leads","type":"Action","name":"adminListLeads","nameZh":"后台回访任务池","domain":"admin","inputs":["status","dueDateTo","page","pageSize"],"outputs":["entity:follow_up_task"],"entrypoints":["GET /api/admin/follow-up-tasks","AdminFollowUpTaskController#list","Admin UI /leads"],"appliesRules":["rule:BR-AUTH-001","rule:BR-ADMIN-001","rule:BR-ADMIN-002","rule:BR-FU-001"],"code":"backend/src/main/java/com/petstore/controller/AdminFollowUpTaskController.java#list; backend/src/main/java/com/petstore/service/FollowUpTaskService.java#list; admin/src/views/LeadsView.vue","doc":"docs/ontology/actions.md#action:admin_list_leads; docs/架构决策-FollowUpTask回访状态机与再次预约归因-2026-08-02.md","test":"AdminFollowUpTaskControllerTest; FollowUpTaskServiceTest","evidence":"anchored"} {"id":"action:admin_list_service_customers","type":"Action","name":"adminListServiceCustomers","nameZh":"后台服务客户列表","domain":"admin","inputs":["source","hasPendingLead","lastVisitFrom","lastVisitTo","q","page","pageSize"],"outputs":["entity:store_customer"],"entrypoints":["GET /api/admin/service-customers","Admin UI /customers"],"appliesRules":["rule:BR-ADMIN-001","rule:BR-ADMIN-002","rule:BR-ADMIN-003","rule:BR-ADMIN-004","rule:BR-SC-001"],"code":"backend/src/main/java/com/petstore/controller/AdminServiceCustomerController.java#listServiceCustomers; backend/src/main/java/com/petstore/service/AdminServiceCustomerService.java","doc":"docs/ontology/actions.md#action:admin_list_service_customers; docs/门店管理后台-PhaseA-页面清单PRD.md §7.6","test":"AdminServiceCustomerControllerTest; AdminServiceCustomerServiceTest","evidence":"anchored","note":"以 t_store_customer 稳定主档为入口,动态聚合宠物/到店/报告/留资;返回 storeCustomerId 和脱敏手机号。"} {"id":"action:admin_update_settings","type":"Action","name":"adminUpdateSettings","nameZh":"后台门店设置读写","domain":"admin","inputs":["entity:store","entity:service_type","entity:user"],"outputs":["entity:store","entity:service_type","entity:user","entity:audit_log"],"entrypoints":["GET /api/admin/store","Admin UI /settings","PUT /api/store/update"],"appliesRules":["rule:BR-ADMIN-001","rule:BR-ADMIN-002","rule:BR-ADMIN-003","rule:BR-STORE-001","rule:BR-ST-001","rule:BR-USER-001","rule:BR-AUDIT-001"],"code":"","doc":"docs/ontology/actions.md#action:admin_update_settings","evidence":"anchored","note":"后台不再读取 legacy inviteCode;写资料复用 PUT /api/store/update 并写低敏审计;员工改走限时邀请。","code_refs":["backend/src/main/java/com/petstore/controller/AdminStoreController.java#currentStore","backend/src/main/java/com/petstore/controller/StoreController.java#update"]} {"id":"action:admin_manage_schedule","type":"Action","name":"adminManageSchedule","nameZh":"后台排班与占用","domain":"admin","inputs":["date","entity:schedule_block"],"outputs":["entity:schedule_block","entity:appointment"],"entrypoints":["Admin UI /schedule","GET /api/schedule/day","POST /api/schedule/block","DELETE /api/schedule/block"],"appliesRules":["rule:BR-ADMIN-001","rule:BR-ADMIN-002","rule:BR-SCH-001","rule:BR-SCH-002","rule:BR-APPT-004"],"code":"","doc":"docs/ontology/actions.md#action:admin_manage_schedule","evidence":"anchored","note":"Web 排班日视图展示容量占用;增删带时长占用复用 ScheduleController。"} @@ -226,7 +226,7 @@ {"id":"rel:workbench_has_todo_items","type":"Relation","subject":"entity:workbench","predicate":"HAS","object":"entity:workbench_todo_item","domain":"admin","evidence":"anchored","cardinality":"1—N","doc":"按优先级排序的行动组"} {"id":"rel:workbench_reads_appointments","type":"Relation","subject":"entity:workbench","predicate":"AGGREGATES","object":"entity:appointment","domain":"admin","evidence":"documented","cardinality":"1—N","doc":"今日预约指标"} {"id":"rel:workbench_reads_reports","type":"Relation","subject":"entity:workbench","predicate":"AGGREGATES","object":"entity:report","domain":"admin","evidence":"documented","cardinality":"1—N","doc":"待出报告/成片异常"} -{"id":"rel:workbench_reads_leads","type":"Relation","subject":"entity:workbench","predicate":"AGGREGATES","object":"entity:report_lead","domain":"admin","evidence":"documented","cardinality":"1—N","doc":"待回访"} +{"id":"rel:workbench_reads_leads","type":"Relation","subject":"entity:workbench","predicate":"AGGREGATES","object":"entity:follow_up_task","domain":"admin","evidence":"anchored","cardinality":"1—N","doc":"到期开放回访任务"} {"id":"rel:workbench_reads_business_events","type":"Relation","subject":"entity:workbench","predicate":"AGGREGATES","object":"entity:business_event","domain":"admin","evidence":"anchored","cardinality":"1—N","doc":"报告提交/打开/留资去重漏斗"} {"id":"rel:store_has_service_customers","type":"Relation","subject":"entity:store","predicate":"HAS","object":"entity:store_customer","domain":"admin","evidence":"anchored","cardinality":"1—N","doc":"StoreCustomer.store_id"} {"id":"rel:service_customer_is_user","type":"Relation","subject":"entity:store_customer","predicate":"LINKS","object":"entity:user","domain":"admin","evidence":"anchored","cardinality":"N—0..1","doc":"StoreCustomer.customer_user_id;留资客户可先为空"} @@ -237,7 +237,7 @@ {"id":"rel:br_admin_004_gates_console","type":"Relation","subject":"rule:BR-ADMIN-004","predicate":"CONSTRAINS","object":"entity:admin_console","domain":"admin","evidence":"documented","doc":"Phase A 范围闸门"} {"id":"rel:admin_list_appts_reads_appointment","type":"Relation","subject":"action:admin_list_appointments","predicate":"READS","object":"entity:appointment","domain":"admin","evidence":"documented"} {"id":"rel:admin_list_reports_reads_report","type":"Relation","subject":"action:admin_list_reports","predicate":"READS","object":"entity:report","domain":"admin","evidence":"documented"} -{"id":"rel:admin_list_leads_reads_lead","type":"Relation","subject":"action:admin_list_leads","predicate":"READS","object":"entity:report_lead","domain":"admin","evidence":"documented"} +{"id":"rel:admin_list_leads_reads_lead","type":"Relation","subject":"action:admin_list_leads","predicate":"READS","object":"entity:follow_up_task","domain":"admin","evidence":"anchored"} {"id":"rel:admin_list_customers_reads_sc","type":"Relation","subject":"action:admin_list_service_customers","predicate":"READS","object":"entity:store_customer","domain":"admin","evidence":"anchored"} {"id":"rel:drilldown_todo_to_list_appts","type":"Relation","subject":"action:drilldown_workbench_todo","predicate":"ROUTES_TO","object":"action:admin_list_appointments","domain":"admin","evidence":"anchored","doc":"kind=overdue_appointment|in_service|upcoming_today;按 ID 或服务端口径筛选"} {"id":"entity:staff_invitation","type":"Entity","name":"StaffInvitation","nameZh":"一次性员工邀请","domain":"user","jpa":true,"fields":["id","invitation_id","token_hash","store_id","invited_name","invited_phone","status","expires_at","created_by_user_id","accepted_by_user_id","accepted_at","revoked_by_user_id","revoked_at","create_time","update_time"],"systems":["Backend Core","Store Miniapp FE","Store Admin FE"],"code":"backend/src/main/java/com/petstore/entity/StaffInvitation.java; backend/src/main/java/com/petstore/service/StaffInvitationService.java","doc":"docs/ontology/objects.md#entity:staff_invitation; docs/架构决策-门店开通与员工邀请-2026-08-02.md","test":"StaffInvitationServiceTest; StoreOnboardingMigrationTest","evidence":"anchored","note":"原始 token 只在创建响应返回一次;库内仅 SHA-256;列表/预览只返回 maskedPhone"} @@ -256,10 +256,35 @@ {"id":"rel:store_has_staff_invitations","type":"Relation","subject":"entity:store","predicate":"HAS","object":"entity:staff_invitation","cardinality":"1—N","domain":"user","evidence":"anchored","doc":"StaffInvitation.store_id"} {"id":"rel:store_has_audit_logs","type":"Relation","subject":"entity:store","predicate":"HAS","object":"entity:audit_log","cardinality":"1—N","domain":"platform","evidence":"anchored","doc":"AuditLog.store_id"} {"id":"rel:staff_invitation_creates_user","type":"Relation","subject":"action:accept_staff_invitation","predicate":"CREATES","object":"entity:user","domain":"user","evidence":"anchored","doc":"仅微信核验手机号匹配且无账号冲突时创建 staff"} -{"id":"entity:customer_timeline","type":"Entity","name":"CustomerTimeline","nameZh":"门店客户事实时间线","domain":"admin","jpa":false,"fields":["customer","total","page","pageSize","hasMore","list[eventId,eventType,occurredAt,title,description,actorRole,actorName,appointment?,report?,lead?]"],"systems":["Store Admin FE","Backend Core"],"code":"backend/src/main/java/com/petstore/service/StoreCustomerTimelineService.java; admin/src/views/CustomersView.vue","doc":"docs/ontology/objects.md#entity:customer_timeline; docs/架构决策-StoreCustomer事实时间线-2026-08-02.md","test":"StoreCustomerTimelineServiceTest; AdminServiceCustomerControllerTest","evidence":"anchored","note":"由 BusinessEvent 只读投影;不返回手机号原文、备注、媒体、token 或原始 metadata"} +{"id":"entity:customer_timeline","type":"Entity","name":"CustomerTimeline","nameZh":"门店客户事实时间线","domain":"admin","jpa":false,"fields":["customer","total","page","pageSize","hasMore","list[eventId,eventType,occurredAt,title,description,actorRole,actorName,appointment?,report?,lead?,followUpTask?]"],"systems":["Store Admin FE","Backend Core"],"code":"backend/src/main/java/com/petstore/service/StoreCustomerTimelineService.java; admin/src/views/CustomersView.vue","doc":"docs/ontology/objects.md#entity:customer_timeline; docs/架构决策-StoreCustomer事实时间线-2026-08-02.md","test":"StoreCustomerTimelineServiceTest; AdminServiceCustomerControllerTest","evidence":"anchored","note":"由 BusinessEvent 只读投影;FollowUpTask 仅低敏摘要;不返回手机号原文、备注、媒体、token 或原始 metadata"} {"id":"action:view_store_customer_timeline","type":"Action","name":"viewStoreCustomerTimeline","nameZh":"查看门店客户事实时间线","domain":"admin","inputs":["storeCustomerId","page","pageSize","entity:current_user"],"outputs":["entity:customer_timeline"],"entrypoints":["GET /api/admin/service-customers/timeline","AdminServiceCustomerController#timeline","StoreCustomerTimelineService#timeline","Admin UI /customers drawer"],"appliesRules":["rule:BR-AUTH-001","rule:BR-ADMIN-001","rule:BR-ADMIN-002","rule:BR-ADMIN-003","rule:BR-SC-001","rule:BR-SC-002","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/controller/AdminServiceCustomerController.java#timeline; backend/src/main/java/com/petstore/service/StoreCustomerTimelineService.java; admin/src/views/CustomersView.vue","doc":"docs/ontology/actions.md#action:view_store_customer_timeline; docs/架构决策-StoreCustomer事实时间线-2026-08-02.md","test":"StoreCustomerTimelineServiceTest; AdminServiceCustomerControllerTest","evidence":"anchored"} {"id":"rule:BR-SC-002","type":"Rule","name":"lowSensitivityCustomerTimelineProjection","nameZh":"客户时间线同店低敏事实投影","domain":"admin","appliesTo":["action:view_store_customer_timeline"],"code":"backend/src/main/java/com/petstore/service/StoreCustomerTimelineService.java; backend/src/main/java/com/petstore/controller/AdminServiceCustomerController.java","doc":"docs/ontology/rules.md#rule:BR-SC-002; docs/架构决策-StoreCustomer事实时间线-2026-08-02.md","test":"StoreCustomerTimelineServiceTest; AdminServiceCustomerControllerTest; StoreCustomerTimelineMigrationTest","evidence":"anchored","note":"以 current.storeId 校验并解析 canonical + 同店别名;BusinessEvent 按 occurredAt/id 稳定倒序分页;仅白名单摘要,不返回原始 metadata/PII/凭证/备注/媒体"} {"id":"rel:store_customer_has_timeline","type":"Relation","subject":"entity:store_customer","predicate":"HAS_VIEW","object":"entity:customer_timeline","cardinality":"1—1","domain":"admin","evidence":"anchored","doc":"按稳定 StoreCustomer.id 生成只读时间线"} {"id":"rel:business_events_project_to_customer_timeline","type":"Relation","subject":"entity:business_event","predicate":"PROJECTS_TO","object":"entity:customer_timeline","cardinality":"N—1","domain":"admin","evidence":"anchored","doc":"仅 store_customer_id 归属且通过白名单转换的事实"} {"id":"rel:view_customer_timeline_reads_store_customer","type":"Relation","subject":"action:view_store_customer_timeline","predicate":"READS","object":"entity:store_customer","domain":"admin","evidence":"anchored"} {"id":"rel:view_customer_timeline_reads_business_events","type":"Relation","subject":"action:view_store_customer_timeline","predicate":"READS","object":"entity:business_event","domain":"admin","evidence":"anchored"} +{"id":"entity:follow_up_task","type":"Entity","name":"FollowUpTask","nameZh":"门店回访执行任务","domain":"lead","jpa":true,"fields":["id","task_id","store_id","store_customer_id","source_lead_id","source_report_id","status","outcome","due_date","assigned_user_id","attempt_count","last_attempt_outcome","last_contacted_at","last_contacted_by_user_id","closed_at","closed_by_user_id","rebooked_appointment_id","create_time","update_time","version"],"systems":["Backend Core","Store Admin FE"],"code":"backend/src/main/java/com/petstore/entity/FollowUpTask.java; backend/src/main/java/com/petstore/service/FollowUpTaskService.java; admin/src/views/LeadsView.vue","doc":"docs/ontology/objects.md#entity:follow_up_task; docs/架构决策-FollowUpTask回访状态机与再次预约归因-2026-08-02.md","test":"FollowUpTaskServiceTest; FollowUpTaskMigrationTest; AdminFollowUpTaskControllerTest","evidence":"anchored","note":"ReportLead 保留同意与来源事实;本实体承载领取、改期、终态和真实预约引用;同一 sourceLeadId 至多一个开放任务"} +{"id":"action:start_follow_up_task","type":"Action","name":"startFollowUpTask","nameZh":"领取回访任务","domain":"lead","inputs":["taskId","entity:current_user"],"outputs":["entity:follow_up_task"],"entrypoints":["POST /api/admin/follow-up-tasks/{taskId}/start","AdminFollowUpTaskController#start","FollowUpTaskService#start"],"appliesRules":["rule:BR-AUTH-001","rule:BR-ADMIN-001","rule:BR-ADMIN-002","rule:BR-FU-001","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/controller/AdminFollowUpTaskController.java#start; backend/src/main/java/com/petstore/service/FollowUpTaskService.java#start","doc":"docs/ontology/actions.md#action:start_follow_up_task","test":"AdminFollowUpTaskControllerTest; FollowUpTaskServiceTest","evidence":"anchored"} +{"id":"action:reschedule_follow_up_task","type":"Action","name":"rescheduleFollowUpTask","nameZh":"登记回访未完成并改期","domain":"lead","inputs":["taskId","reason","dueDate","entity:current_user"],"outputs":["entity:follow_up_task"],"entrypoints":["POST /api/admin/follow-up-tasks/{taskId}/reschedule","AdminFollowUpTaskController#reschedule","FollowUpTaskService#reschedule"],"appliesRules":["rule:BR-AUTH-001","rule:BR-ADMIN-001","rule:BR-ADMIN-002","rule:BR-FU-001","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/controller/AdminFollowUpTaskController.java#reschedule; backend/src/main/java/com/petstore/service/FollowUpTaskService.java#reschedule","doc":"docs/ontology/actions.md#action:reschedule_follow_up_task","test":"AdminFollowUpTaskControllerTest; FollowUpTaskServiceTest","evidence":"anchored"} +{"id":"action:close_follow_up_task","type":"Action","name":"closeFollowUpTask","nameZh":"关闭回访任务","domain":"lead","inputs":["taskId","outcome","entity:current_user"],"outputs":["entity:follow_up_task"],"entrypoints":["POST /api/admin/follow-up-tasks/{taskId}/close","AdminFollowUpTaskController#close","FollowUpTaskService#close"],"appliesRules":["rule:BR-AUTH-001","rule:BR-ADMIN-001","rule:BR-ADMIN-002","rule:BR-FU-001","rule:BR-FU-002","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/controller/AdminFollowUpTaskController.java#close; backend/src/main/java/com/petstore/service/FollowUpTaskService.java#close","doc":"docs/ontology/actions.md#action:close_follow_up_task","test":"AdminFollowUpTaskControllerTest; FollowUpTaskServiceTest","evidence":"anchored","note":"仅 not_interested/invalid_contact;rebooked 不可手工选择"} +{"id":"event:follow_up_created","type":"Event","name":"FollowUpCreated","nameZh":"回访任务已创建","domain":"lead","payload":["storeId","storeCustomerId","aggregateId","dueDate","occurredAt"],"emittedBy":["action:submit_lead"],"appliesRules":["rule:BR-FU-001","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/service/FollowUpTaskService.java#ensureForLead; backend/src/main/java/com/petstore/service/BusinessEventService.java#recordFollowUpCreated","test":"FollowUpTaskServiceTest; FollowUpTaskMigrationTest; BusinessEventServiceTest","evidence":"anchored"} +{"id":"event:follow_up_started","type":"Event","name":"FollowUpStarted","nameZh":"员工已开始回访","domain":"lead","payload":["storeId","storeCustomerId","aggregateId","actorUserId","actorRole","occurredAt"],"emittedBy":["action:start_follow_up_task"],"appliesRules":["rule:BR-FU-001","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/service/FollowUpTaskService.java#start; backend/src/main/java/com/petstore/service/BusinessEventService.java#recordFollowUpStarted","test":"FollowUpTaskServiceTest; BusinessEventServiceTest","evidence":"anchored"} +{"id":"event:follow_up_rescheduled","type":"Event","name":"FollowUpRescheduled","nameZh":"回访已改期","domain":"lead","payload":["storeId","storeCustomerId","aggregateId","attemptOutcome","attemptCount","dueDate","occurredAt"],"emittedBy":["action:reschedule_follow_up_task"],"appliesRules":["rule:BR-FU-001","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/service/FollowUpTaskService.java#reschedule; backend/src/main/java/com/petstore/service/BusinessEventService.java#recordFollowUpRescheduled","test":"FollowUpTaskServiceTest; BusinessEventServiceTest","evidence":"anchored"} +{"id":"event:follow_up_completed","type":"Event","name":"FollowUpCompleted","nameZh":"回访任务已完成","domain":"lead","payload":["storeId","storeCustomerId","aggregateId","outcome","attemptCount","occurredAt"],"emittedBy":["action:close_follow_up_task","action:create_appointment"],"appliesRules":["rule:BR-FU-001","rule:BR-FU-002","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/service/FollowUpTaskService.java#close; #markRebooked; backend/src/main/java/com/petstore/service/BusinessEventService.java#recordFollowUpCompleted","test":"FollowUpTaskServiceTest; AppointmentServiceTest; BusinessEventServiceTest","evidence":"anchored"} +{"id":"event:follow_up_canceled","type":"Event","name":"FollowUpCanceled","nameZh":"回访任务已取消","domain":"lead","payload":["storeId","storeCustomerId","aggregateId","outcome","occurredAt"],"emittedBy":["action:unsubscribe"],"appliesRules":["rule:BR-FU-001","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/service/FollowUpTaskService.java#cancelForUnsubscribe; backend/src/main/java/com/petstore/service/BusinessEventService.java#recordFollowUpCanceled","test":"FollowUpTaskServiceTest; BusinessEventServiceTest","evidence":"anchored"} +{"id":"event:rebook_created","type":"Event","name":"RebookCreated","nameZh":"回访后真实再次预约已创建","domain":"lead","payload":["storeId","storeCustomerId","aggregateId","bookingOrigin","occurredAt"],"emittedBy":["action:create_appointment"],"appliesRules":["rule:BR-FU-002","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/service/FollowUpTaskService.java#markRebooked; backend/src/main/java/com/petstore/service/BusinessEventService.java#recordRebookCreated","test":"AppointmentServiceTest; BusinessEventServiceTest; AdminBusinessEventServiceTest","evidence":"anchored"} +{"id":"rule:BR-FU-001","type":"Rule","name":"followUpTaskStateAndOwnership","nameZh":"回访任务状态机与同店领取权","domain":"lead","appliesTo":["action:submit_lead","action:unsubscribe","action:admin_list_leads","action:start_follow_up_task","action:reschedule_follow_up_task","action:close_follow_up_task"],"code":"backend/src/main/java/com/petstore/service/FollowUpTaskService.java; backend/src/main/java/com/petstore/controller/AdminFollowUpTaskController.java","doc":"docs/ontology/rules.md#rule:BR-FU-001; docs/架构决策-FollowUpTask回访状态机与再次预约归因-2026-08-02.md","test":"FollowUpTaskServiceTest; AdminFollowUpTaskControllerTest; FollowUpTaskMigrationTest","evidence":"anchored","note":"pending 无领取人;in_progress 必须有且仅领取人可操作;终态不回退;退订不增加联系次数;跨店 404"} +{"id":"rule:BR-FU-002","type":"Rule","name":"realRebookAttribution","nameZh":"真实再次预约归因","domain":"lead","appliesTo":["action:create_appointment","action:close_follow_up_task"],"code":"backend/src/main/java/com/petstore/service/AppointmentService.java#createBooking; backend/src/main/java/com/petstore/service/FollowUpTaskService.java#prepareRebook; #markRebooked","doc":"docs/ontology/rules.md#rule:BR-FU-002; docs/架构决策-FollowUpTask回访状态机与再次预约归因-2026-08-02.md","test":"AppointmentServiceTest; FollowUpTaskServiceTest; AdminBusinessEventServiceTest","evidence":"anchored","note":"rebooked 不可手工选择;仅同店同 canonical 客户的真实预约保存成功后同事务闭环;容量失败不闭环"} +{"id":"rel:lead_has_follow_up_tasks","type":"Relation","subject":"entity:report_lead","predicate":"HAS","object":"entity:follow_up_task","cardinality":"1—N","domain":"lead","evidence":"anchored","doc":"同一留资可有多个终结轮次,但至多一个开放任务"} +{"id":"rel:store_customer_has_follow_up_tasks","type":"Relation","subject":"entity:store_customer","predicate":"HAS","object":"entity:follow_up_task","cardinality":"1—N","domain":"lead","evidence":"anchored","doc":"FollowUpTask.store_customer_id 指向同店 canonical 主档"} +{"id":"rel:follow_up_task_rebooks_appointment","type":"Relation","subject":"entity:follow_up_task","predicate":"REBOOKS","object":"entity:appointment","cardinality":"N—0..1","domain":"lead","evidence":"anchored","doc":"仅真实预约保存成功后写 rebooked_appointment_id"} +{"id":"rel:submit_lead_creates_follow_up_task","type":"Relation","subject":"action:submit_lead","predicate":"CREATES","object":"entity:follow_up_task","domain":"lead","evidence":"anchored","doc":"开放任务存在时复用;前序终态后创建新轮次"} +{"id":"rel:submit_lead_emits_follow_up_created","type":"Relation","subject":"action:submit_lead","predicate":"EMITS","object":"event:follow_up_created","domain":"lead","evidence":"anchored"} +{"id":"rel:start_follow_up_emits_started","type":"Relation","subject":"action:start_follow_up_task","predicate":"EMITS","object":"event:follow_up_started","domain":"lead","evidence":"anchored"} +{"id":"rel:reschedule_follow_up_emits_rescheduled","type":"Relation","subject":"action:reschedule_follow_up_task","predicate":"EMITS","object":"event:follow_up_rescheduled","domain":"lead","evidence":"anchored"} +{"id":"rel:close_follow_up_emits_completed","type":"Relation","subject":"action:close_follow_up_task","predicate":"EMITS","object":"event:follow_up_completed","domain":"lead","evidence":"anchored"} +{"id":"rel:unsubscribe_emits_follow_up_canceled","type":"Relation","subject":"action:unsubscribe","predicate":"EMITS","object":"event:follow_up_canceled","domain":"lead","evidence":"anchored"} +{"id":"rel:create_appointment_emits_follow_up_completed","type":"Relation","subject":"action:create_appointment","predicate":"EMITS","object":"event:follow_up_completed","domain":"lead","evidence":"anchored","doc":"仅携带已领取任务且预约保存成功"} +{"id":"rel:create_appointment_emits_rebook_created","type":"Relation","subject":"action:create_appointment","predicate":"EMITS","object":"event:rebook_created","domain":"lead","evidence":"anchored"} +{"id":"rel:br_fu_001_applies_follow_up","type":"Relation","subject":"rule:BR-FU-001","predicate":"APPLIES_TO","object":"action:start_follow_up_task","domain":"lead","evidence":"anchored"} +{"id":"rel:br_fu_002_applies_create_appointment","type":"Relation","subject":"rule:BR-FU-002","predicate":"APPLIES_TO","object":"action:create_appointment","domain":"lead","evidence":"anchored"} diff --git a/ontology/README.md b/ontology/README.md index bdfd6d5..abcfaad 100644 --- a/ontology/README.md +++ b/ontology/README.md @@ -59,9 +59,9 @@ rel:subject_predicate_object # rel:report_belongs_to_appointment ## 范围 -已覆盖 P0 主闭环实体与动作,并补录:`Pet`、`ServiceType`、`ScheduleBlock`、`HighlightVideo`、`SessionToken`、`ReportImage`、`ReportTestimonial`、`ServiceInterval`、`HighlightFailReason`、`CurrentUser`,以及成片/上传/鉴权/寄语/退订相关规则。 +已覆盖 P0 主闭环实体与动作,并补录:`Pet`、`ServiceType`、`ScheduleBlock`、`HighlightVideo`、`SessionToken`、`ReportImage`、`ReportTestimonial`、`ServiceInterval`、`HighlightFailReason`、`CurrentUser`、`FollowUpTask`,以及成片/上传/鉴权/寄语/退订/回访状态机/真实再次预约相关规则。 -**门店 Web 后台 Phase A**:`AdminConsole`、`Workbench`、`WorkbenchTodoItem` 仍是非 JPA 读模型;`StoreCustomer` 已落为 JPA 稳定主档。`BR-ADMIN-001`…`004` 与 admin actions 继续约束后台;收银/会员资产仍为 gap。 +**门店 Web 后台 Phase A**:`AdminConsole`、`Workbench`、`WorkbenchTodoItem` 仍是非 JPA 读模型;`StoreCustomer` 与 `FollowUpTask` 已落为 JPA 稳定业务对象。`BR-ADMIN-001`…`004`、`BR-FU-001/002` 与 admin actions 继续约束后台;收银/会员资产仍为 gap。 P1/P2 方向(会员/储值/套餐/历史报告/看板)以 `gap` 证据条目录入,不展开字段;受 `rule:BR-ADMIN-004` 约束不得偷渡进 Phase A 设计。 diff --git a/ontology/actions.md b/ontology/actions.md index 6c7f680..ac6a5c4 100644 --- a/ontology/actions.md +++ b/ontology/actions.md @@ -84,13 +84,13 @@ legacy 永久邀请码查询入口已停用,固定返回 `410 / LEGACY_INVITE_ ### action:create_appointment -创建预约:按服务时长跨半小时容量桶占号,并保存时长快照;取消不占号;客户与创建人分开记录。 +创建预约:按服务时长跨半小时容量桶占号,并保存时长快照;取消不占号;客户与创建人分开记录。可选携带 `followUpTaskId`,在真实保存成功后闭环再次预约归因。 - **入口**:`POST /api/appointment/create` → `AppointmentController#create` → `AppointmentService#createBooking` -- **输入**:`petName`、`petType`、`serviceType`、`appointmentTime`、`storeId`、`remark?`、`petId?`;门店代客预约另需 `customerUserId` 或 `customerPhone` -- **输出**:`entity:appointment` +- **输入**:`petName`、`petType`、`serviceType`、`appointmentTime`、`storeId`、`remark?`、`petId?`、`followUpTaskId?`;门店代客预约另需 `customerUserId` 或 `customerPhone` +- **输出**:`entity:appointment`;携带任务且成功时更新 `entity:follow_up_task` - **身份口径**:customer 自约时客户=创建人=current.userId;boss/staff 代约时创建人=current.userId,客户按请求解析,legacy userId 镜像客户 -- **适用规则**:`rule:BR-APPT-001`(状态机)、`rule:BR-APPT-003`(客户归属)、`rule:BR-APPT-004`(服务时长 + 门店容量 + 并发锁)、`rule:BR-SC-001`(门店客户建档) +- **适用规则**:`rule:BR-APPT-001`(状态机)、`rule:BR-APPT-003`(客户归属)、`rule:BR-APPT-004`(服务时长 + 门店容量 + 并发锁)、`rule:BR-SC-001`(门店客户建档)、`rule:BR-FU-002`(真实再次预约) - **代码**:`backend/src/main/java/com/petstore/controller/AppointmentController.java#create` - **证据**:`anchored` @@ -619,7 +619,7 @@ legacy 永久邀请码查询入口已停用,固定返回 `410 / LEGACY_INVITE_ ### action:view_workbench -查看今日行动工作台(登录落点)。服务端按完整本店数据聚合逾期预约、服务中、今日待开始、成片异常、新报告待发送和到期回访,返回准确总数与低敏预览;报告提交、显式确认发送、打开与留资通过 BusinessEvent 汇总,仅未建模的转预约保持未知。 +查看今日行动工作台(登录落点)。服务端按完整本店数据聚合逾期预约、服务中、今日待开始、成片异常、新报告待发送和到期 FollowUpTask,返回准确总数与低敏预览;报告提交、显式确认发送、打开、留资与回访后真实再次预约均通过 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` @@ -656,11 +656,44 @@ legacy 永久邀请码查询入口已停用,固定返回 `410 / LEGACY_INVITE_ ### action:admin_list_leads -后台回访线索列表。 +后台回访任务池。导航路径保留 `/leads`,操作对象已由原始留资切换为 `FollowUpTask`。 -- **入口**:Admin UI `/leads`(planned);复用 `GET /api/report/leads` -- **适用规则**:`rule:BR-ADMIN-001`、`rule:BR-ADMIN-002`、`rule:BR-LEAD-002`、`rule:BR-LEAD-003` -- **证据**:`documented` +- **入口**:Admin UI `/leads`;`GET /api/admin/follow-up-tasks` +- **输入**:`status`、`dueDateTo`、`page`、`pageSize` +- **输出**:`entity:follow_up_task` +- **适用规则**:`rule:BR-ADMIN-001`、`rule:BR-ADMIN-002`、`rule:BR-FU-001` +- **代码/测试**:`AdminFollowUpTaskController#list`、`FollowUpTaskService#list`、`admin/src/views/LeadsView.vue`;`AdminFollowUpTaskControllerTest`、`FollowUpTaskServiceTest` +- **证据**:`anchored` + +### action:start_follow_up_task + +领取待处理回访任务;同一任务只能由一名门店员工持有。 + +- **入口**:`POST /api/admin/follow-up-tasks/{taskId}/start` +- **输出**:`entity:follow_up_task` +- **适用规则**:`rule:BR-FU-001` +- **代码/测试**:`AdminFollowUpTaskController#start`、`FollowUpTaskService#start`;`FollowUpTaskServiceTest` +- **证据**:`anchored` + +### action:reschedule_follow_up_task + +领取人登记 `no_answer/follow_later` 并设置下次日期,联系次数加一,任务回到 `pending`。 + +- **入口**:`POST /api/admin/follow-up-tasks/{taskId}/reschedule` +- **输入**:`reason`、`dueDate` +- **输出**:`entity:follow_up_task` +- **适用规则**:`rule:BR-FU-001` +- **证据**:`anchored` + +### action:close_follow_up_task + +领取人以 `not_interested/invalid_contact` 关闭本轮任务;`rebooked` 不允许手工选择。 + +- **入口**:`POST /api/admin/follow-up-tasks/{taskId}/close` +- **输入**:`outcome` +- **输出**:`entity:follow_up_task` +- **适用规则**:`rule:BR-FU-001`、`rule:BR-FU-002` +- **证据**:`anchored` ### action:admin_list_service_customers @@ -691,7 +724,7 @@ legacy 永久邀请码查询入口已停用,固定返回 `410 / LEGACY_INVITE_ - **输入**:`storeCustomerId`、`page`、`pageSize`;`storeId` 强制从当前会话派生 - **输出**:`entity:customer_timeline` - **适用规则**:`rule:BR-AUTH-001`、`rule:BR-ADMIN-001`~`003`、`rule:BR-SC-001`、`rule:BR-SC-002`、`rule:BR-BE-001` -- **行为**:解析 canonical 主档及其同店合并别名,按 `occurredAt DESC, id DESC` 分页读取全部关联 BusinessEvent;批量补齐预约/报告/回访的白名单摘要并提供详情跳转 ID,既有事件不改写 +- **行为**:解析 canonical 主档及其同店合并别名,按 `occurredAt DESC, id DESC` 分页读取全部关联 BusinessEvent;批量补齐预约/报告/留资/FollowUpTask 的白名单摘要并提供详情跳转 ID,既有事件不改写 - **代码/测试**:`StoreCustomerTimelineService`、`CustomersView.vue`;`StoreCustomerTimelineServiceTest`、`AdminServiceCustomerControllerTest` - **证据**:`anchored` diff --git a/ontology/events.md b/ontology/events.md index 479abaf..a47fd24 100644 --- a/ontology/events.md +++ b/ontology/events.md @@ -139,6 +139,54 @@ - **文档**:`docs/本期主线-下一步产品优化点.md 埋点事件 schema` - **证据**:`anchored` +### event:follow_up_created + +回访任务已创建;由新留资或可靠历史待办回填触发。 + +- **触发动作**:`action:submit_lead` +- **payload**:`storeId`、`storeCustomerId`、`aggregateId=taskInternalId`、`dueDate`、`occurredAt` +- **证据**:`anchored` + +### event:follow_up_started + +门店员工已领取并开始处理回访。 + +- **触发动作**:`action:start_follow_up_task` +- **payload**:`storeId`、`storeCustomerId`、`aggregateId=taskInternalId`、`actorUserId`、`actorRole` +- **证据**:`anchored` + +### event:follow_up_rescheduled + +本次联系未形成终态并已设置下次回访日。 + +- **触发动作**:`action:reschedule_follow_up_task` +- **payload**:`attemptOutcome`、`attemptCount`、`dueDate`;不含手机号或自由文本 +- **证据**:`anchored` + +### event:follow_up_completed + +回访任务以员工处理结果或真实再次预约完成。 + +- **触发动作**:`action:close_follow_up_task`、`action:create_appointment` +- **payload**:`outcome`、`attemptCount` +- **证据**:`anchored` + +### event:follow_up_canceled + +宠主退订导致开放任务取消;不伪装成员工联系。 + +- **触发动作**:`action:unsubscribe` +- **payload**:`outcome=unsubscribed` +- **证据**:`anchored` + +### event:rebook_created + +回访任务关联的真实同店预约已保存成功。 + +- **触发动作**:`action:create_appointment` +- **payload**:`storeId`、`storeCustomerId`、`aggregateId=appointmentId`、`bookingOrigin=follow_up` +- **证据**:`anchored` + --- ## Batch 2 录入事件 diff --git a/ontology/objects.md b/ontology/objects.md index 21677e2..d3b3dea 100644 --- a/ontology/objects.md +++ b/ontology/objects.md @@ -195,6 +195,30 @@ - **测试**:暂无(Batch D 未补 `ReportLeadControllerTest`,列为 `documented` gap) - **证据**:`anchored`(脱敏与跨店校验已实现并测试覆盖;去重逻辑 `documented`) +## entity:follow_up_task + +**门店回访执行任务**。`ReportLead` 保留宠主同意和来源事实,本实体承载员工可领取、可改期、可终结的操作状态以及真实再次预约引用。 + +| 字段 | 类型 | 说明 | +|------|------|------| +| id / task_id | BIGINT / VARCHAR(36) | 内部主键 / Admin 对外随机 ID | +| store_id / store_customer_id | BIGINT | 同店范围 / canonical 客户主档 | +| source_lead_id / source_report_id | BIGINT | 来源留资 / 报告 | +| status | VARCHAR(16) | pending / in_progress / completed / canceled | +| outcome | VARCHAR(24) | rebooked / not_interested / invalid_contact / unsubscribed | +| due_date / assigned_user_id | DATE / BIGINT | 回访日 / 当前领取人 | +| attempt_count / last_attempt_outcome | INT / VARCHAR(24) | 已联系次数 / no_answer 或 follow_later | +| last_contacted_at / last_contacted_by_user_id | DATETIME / BIGINT | 最近联系回执 | +| closed_at / closed_by_user_id | DATETIME / BIGINT | 终态回执;退订取消无员工关闭人 | +| rebooked_appointment_id | BIGINT | 仅真实同店预约保存成功后写入 | +| create_time / update_time / version | DATETIME / DATETIME / BIGINT | 时间戳与并发版本 | + +- **归属系统**:Backend Core + Store Admin FE +- **代码**:`backend/src/main/java/com/petstore/entity/FollowUpTask.java`、`service/FollowUpTaskService.java` +- **文档**:`docs/架构决策-FollowUpTask回访状态机与再次预约归因-2026-08-02.md` +- **测试**:`FollowUpTaskServiceTest`、`FollowUpTaskMigrationTest`、`AdminFollowUpTaskControllerTest` +- **证据**:`anchored` + ## entity:business_event **不可变业务事件**。预约、履约、报告打开与留资等经营事实的统一持久化入口,为报告漏斗和 StoreCustomer 时间线提供可统计事实源。 @@ -399,13 +423,13 @@ ## entity:customer_timeline -**门店客户事实时间线**(非 JPA 只读模型)。以 canonical `StoreCustomer.id` 为稳定客户关系键,把已经持久化的 `BusinessEvent` 投影为门店可理解的预约、履约、报告发送/打开和回访意向记录;被合并投影的事件通过别名指针一并读取,不改写历史事件。 +**门店客户事实时间线**(非 JPA 只读模型)。以 canonical `StoreCustomer.id` 为稳定客户关系键,把已经持久化的 `BusinessEvent` 投影为门店可理解的预约、履约、报告发送/打开、留资和回访任务记录;被合并投影的事件通过别名指针一并读取,不改写历史事件。 | 字段 | 说明 | |------|------| | customer | 客户称呼、脱敏手机号、首次来源、首次/最近接触时间 | | total / page / pageSize / hasMore | 稳定倒序分页信息 | -| list | `eventId/eventType/occurredAt/title/description` 与可选预约/报告/回访低敏摘要 | +| list | `eventId/eventType/occurredAt/title/description` 与可选预约/报告/留资/FollowUpTask 低敏摘要 | - **归属系统**:Backend Core / Store Admin FE - **代码**:`backend/src/main/java/com/petstore/service/StoreCustomerTimelineService.java`、`admin/src/views/CustomersView.vue` @@ -450,7 +474,7 @@ ## entity:workbench -**今日工作台**(非 JPA 聚合视图)。指标卡 + 按履约优先级排序的行动队列 + 报告漏斗摘要;行动来自 appointment/report/lead/highlight,报告漏斗来自 BusinessEvent,**无独立工作台表**。服务端按完整本店数据计算准确总数,不再由前端拉取前 200 条估算。 +**今日工作台**(非 JPA 聚合视图)。指标卡 + 按履约优先级排序的行动队列 + 报告漏斗摘要;行动来自 appointment/report/follow_up_task/highlight,报告漏斗来自 BusinessEvent,**无独立工作台表**。服务端按完整本店数据计算准确总数,不再由前端拉取前 200 条估算。 | 字段(概念) | 说明 | |------|------| @@ -458,7 +482,7 @@ | overdueAppointmentCount / inServiceCount / upcomingTodayCount | 逾期未开始 / 服务中 / 今日待开始 | | dueFollowUpCount / highlightAnomalyCount / unsentReportCount | 到期回访 / 成片失败或处理超时 / 新报告待发送 | | actionGroups | `entity:workbench_todo_item[]`(每组最多 3 条低敏预览) | -| reportFunnel | 已提交/已确认发送/已打开/已留资/报告转预约;仅转预约仍返回未知 | +| reportFunnel | 已提交/已确认发送/已打开/已留资/回访后真实再次预约;全部来自 BusinessEvent | | anomalyList | 成片失败或待回访最近 N 条 | - **代码**:`admin/src/views/WorkbenchView.vue`、`AdminBusinessEventController`、`AdminWorkbenchService`、`AdminBusinessEventService` diff --git a/ontology/relations.md b/ontology/relations.md index f8c98f5..71e0cea 100644 --- a/ontology/relations.md +++ b/ontology/relations.md @@ -16,6 +16,9 @@ | `Report` 1—N `ReportImage` | 报告含多张照片(before/after/during) | `ReportImage.report_id` → `Report.id` | | `Report` 1—N `ReportLead` | 一份报告可多次留资(同手机号去重) | `ReportLead.report_id` → `Report.id` | | `ReportLead` N—1 `Store` | 留资归属门店(回访池范围) | `ReportLead.store_id` → `Store.id` | +| `ReportLead` 1—N `FollowUpTask` | 同一留资可跨多个终结轮次形成任务,但至多一个开放任务 | `FollowUpTask.source_lead_id` → `ReportLead.id` | +| `StoreCustomer` 1—N `FollowUpTask` | 回访任务归属 canonical 门店客户 | `FollowUpTask.store_customer_id` → `StoreCustomer.id` | +| `FollowUpTask` N—0..1 `Appointment` | 仅真实再次预约成功后引用 | `FollowUpTask.rebooked_appointment_id` → `Appointment.id` | | `Store` 1—N `BusinessEvent` | 所有可统计事实强制归属门店 | `BusinessEvent.store_id` → `Store.id` | | `StoreCustomer` 1—N `BusinessEvent` | 客户时间线;无客户语义事件可空 | `BusinessEvent.store_customer_id` → `StoreCustomer.id` | | `Store` 1—N `StaffInvitation` | 一个门店可签发多次限时邀请 | `StaffInvitation.store_id` → `Store.id` | @@ -35,6 +38,12 @@ | `action:confirm_report_sent` | `event:report_sent`(仅首次确认) | | `action:track_report_open` | `event:report_opened` / `event:report_reopened` | | `action:submit_lead` | `event:lead_submitted` | +| `action:submit_lead` | `event:follow_up_created`(首次或终态后新轮次) | +| `action:start_follow_up_task` | `event:follow_up_started` | +| `action:reschedule_follow_up_task` | `event:follow_up_rescheduled` | +| `action:close_follow_up_task` | `event:follow_up_completed` | +| `action:unsubscribe` | `event:follow_up_canceled` | +| `action:create_appointment` | `event:follow_up_completed`、`event:rebook_created`(携带任务且真实保存) | | `action:accept_staff_invitation` | `event:staff_invitation_accepted`、`event:session_issued` | ## 规则—动作(适用) @@ -60,6 +69,8 @@ | `rule:BR-LEAD-002` | `action:list_leads` | | `rule:BR-LEAD-003` | `action:list_leads` | | `rule:BR-BE-001` | 所有已落库事件触发动作、`action:view_workbench` | +| `rule:BR-FU-001` | 回访任务创建、列表、领取、改期、关闭与退订 | +| `rule:BR-FU-002` | `action:create_appointment`、`action:close_follow_up_task` | ## 实体—归属系统 @@ -71,6 +82,7 @@ | `entity:report` | Report Media Backend | | `entity:report_lead` | Report Media Backend | | `entity:business_event` | Backend Core(跨域写入底座) | +| `entity:follow_up_task` | Backend Core + Store Admin FE | --- @@ -162,7 +174,7 @@ |------|------| | `admin_console` HAS `workbench` | 登录默认工作台 | | `workbench` HAS `workbench_todo_item` | 按优先级排序的行动组(准确总数 + 低敏预览) | -| `workbench` AGGREGATES `appointment` / `report` / `report_lead` | 指标与异常来源 | +| `workbench` AGGREGATES `appointment` / `report` / `follow_up_task` | 指标与异常来源 | | `store` HAS `store_customer` | 一店多个稳定客户关系 | | `store_customer` LINKS `user` | 可选关联全局 customer 账号;留资客户可先不关联 | | `store_customer` HAS `pet` | 通过 customer user 关联一人多宠 | @@ -174,3 +186,4 @@ | `store_customer` HAS_VIEW `customer_timeline` | 一个稳定门店客户主档对应一个动态事实视图 | | `business_event` PROJECTS_TO `customer_timeline` | 多条不可变事实经低敏白名单投影为时间线 | | `view_store_customer_timeline` READS `store_customer` / `business_event` | 同店校验后分页读取主档与事实 | +| `admin_list_leads` READS `follow_up_task` | `/leads` 导航承载真实操作任务池,不直接编辑留资事实 | diff --git a/ontology/rules.md b/ontology/rules.md index e85f257..7b194eb 100644 --- a/ontology/rules.md +++ b/ontology/rules.md @@ -193,6 +193,24 @@ - **代码**:`backend/src/main/java/com/petstore/controller/ReportLeadController.java#leads` - **证据**:`anchored` +### rule:BR-FU-001 + +**回访任务状态机与同店领取权**:每个 `sourceLeadId` 至多一个 `pending/in_progress` 任务;`pending` 无领取人,`in_progress` 必须有领取人,只有当前领取人能改期、关闭或再次预约。`no_answer/follow_later` 增加联系次数并回到待领取;`completed/canceled` 不回退;退订取消开放任务但不增加员工联系次数。所有范围从 session 派生,跨店按 404。 + +- **适用动作**:`action:submit_lead`、`action:unsubscribe`、`action:admin_list_leads`、`action:start_follow_up_task`、`action:reschedule_follow_up_task`、`action:close_follow_up_task` +- **代码**:`backend/src/main/java/com/petstore/service/FollowUpTaskService.java`、`AdminFollowUpTaskController.java` +- **测试**:`FollowUpTaskServiceTest`、`AdminFollowUpTaskControllerTest`、`FollowUpTaskMigrationTest` +- **证据**:`anchored` + +### rule:BR-FU-002 + +**真实再次预约归因**:`rebooked` 不可手工关闭。只有已领取任务与本次预约属于同店同一 canonical StoreCustomer,且预约通过容量校验并真实保存后,才在同一事务写 `rebooked_appointment_id`、终态与 `rebook_created`;任一失败保持任务未闭环。 + +- **适用动作**:`action:create_appointment`、`action:close_follow_up_task` +- **代码**:`AppointmentService#createBooking`、`FollowUpTaskService#prepareRebook/#markRebooked` +- **测试**:`AppointmentServiceTest`、`FollowUpTaskServiceTest` +- **证据**:`anchored` + --- ## Batch 2 录入规则 @@ -354,7 +372,7 @@ ### rule:BR-CONFIG-003 -**生产发布只读门禁**:正式切流前必须按固定顺序完成备份和七个版本化迁移,再以最终生产配置运行 JPA schema `validate` 与 26 项只读数据不变量检查。运行期 readiness 必须同时覆盖数据库、磁盘、上传目录和 FFmpeg/ffprobe;上线自动 smoke 只允许 GET,不创建或修改业务数据。 +**生产发布只读门禁**:正式切流前必须按固定顺序完成备份和八个版本化迁移,再以最终生产配置运行 JPA schema `validate` 与 31 项只读数据不变量检查。运行期 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/phase1-follow-up-task-rebook-2026-08-02.md b/qa-reports/phase1-follow-up-task-rebook-2026-08-02.md new file mode 100644 index 0000000..bc0bb6b --- /dev/null +++ b/qa-reports/phase1-follow-up-task-rebook-2026-08-02.md @@ -0,0 +1,54 @@ +# Phase 1 FollowUpTask 与真实再次预约交付证据 + +> 日期:2026-08-02 +> 范围:Backend + Admin + Docs/Ontology +> 状态:Code Complete / Local Gates Passed / Production Not Migrated + +## 1. 交付结果 + +- `ReportLead` 收口为宠主同意与来源事实;新增 `FollowUpTask` 作为门店可执行回访对象。 +- 状态机支持领取、未接通/稍后联系改期、无意向/无效联系方式关闭,以及退订取消。 +- Admin `/leads` 已切换为任务池;同店员工可实际联系、读取真实号源并代客创建预约。 +- `rebooked` 只由真实 Appointment 保存成功触发;容量、客户归属或状态校验失败时不闭环。 +- 工作台到期回访改读 FollowUpTask;报告漏斗的回访后预约改读 `rebook_created`。 +- 客户事实时间线新增回访创建/领取/改期/完成/取消/再次预约的低敏投影。 + +## 2. 自动化证据 + +| Gate | 结果 | +|------|------| +| Backend full test | PASS:217 tests,0 failures,0 errors,0 skipped | +| Admin production build | PASS:Vue type-check + Vite,1671 modules transformed | +| Ontology validation | PASS:290 entries,引用完整 | +| Endpoint drift | PASS:70/70 HTTP,code-only=0,ontology-only=0 | +| Entity drift | PASS:16/16 JPA,missing=0,extra=0 | +| Diff whitespace | PASS:backend/admin/docs `git diff --check` | + +关键新增覆盖:`FollowUpTaskMigrationTest`、`FollowUpTaskServiceTest`、`AdminFollowUpTaskControllerTest`,以及 Appointment/BusinessEvent/Workbench/Timeline 的回归用例。反例包含跨店 404、他人领取冲突、未领取关闭拒绝、退订不虚增联系次数、跨店预约拒绝、容量不足不触碰任务。 + +## 3. Data Model Review + +结论:PASS(代码与迁移层面)。 + +- `t_follow_up_task` 使用随机 `task_id` 对外、自增 ID 内部关联、`@Version` 与悲观锁处理竞争。 +- 生产迁移只回填可靠的历史 `pending` 留资;不猜测其他历史结果。 +- 31 项生产只读预检中新增五项,覆盖客户 canonical 范围、状态/领取/回执、开放唯一性、真实 Appointment 与 BusinessEvent 引用。 +- 迁移为向前兼容新增表;常规应用回滚不得删除表或覆盖已产生任务/事件。 + +## 4. Privacy Review + +结论:PASS(静态契约与自动化层面)。 + +- 完整手机号仅在本店已认证 boss/staff 的开放回访任务中返回,目的限定为实际联系;终态任务只返回脱敏手机号。 +- Workbench、CustomerTimeline、BusinessEvent、AuditLog 不保存或返回完整手机号。 +- 事件 metadata 白名单不含手机号、token、openid/unionid、IP、备注、媒体 URL 或原始请求。 +- 跨店任务使用 404,降低可枚举性;customer 无任务接口权限。 + +## 5. 尚未完成的外部证据 + +- 未在生产或真实试点库执行第八个迁移 `20260802_create_follow_up_task.sql`。 +- 未以最终 production 环境运行 schema validate + 31 项只读预检。 +- 未完成 boss/staff/customer 三角色真实环境 smoke,尤其是双员工抢领、退订与无号源再次预约。 +- 未在 3~5 家试点门店验证任务领取习惯、接通率、再次预约率和约 200 单完整服务样本。 + +上述项目进入试点 RC 门禁,不因本地自动化通过而标记为上线完成。 diff --git a/产品设计文档.md b/产品设计文档.md index a2be19c..652dfdf 100644 --- a/产品设计文档.md +++ b/产品设计文档.md @@ -2,8 +2,8 @@ > **本体对齐**:本文档与 [`ontology/objects.md`](./ontology/objects.md) 对齐(`entity:store`/`entity:user`/`entity:appointment`/`entity:report`),规则见 [`ontology/rules.md`](./ontology/rules.md)。冲突以本体为准。 > -> 版本:v2.0 -> 日期:2026-08-02(v2.0:面向 3~5 家独立单店真实试点,补齐门店开通、一次性员工邀请与审计边界) +> 版本:v2.1 +> 日期:2026-08-02(v2.1:补齐可执行回访任务状态机与真实再次预约归因) > **品牌**:宠小它 · **主 Slogan**:用心宠小它,温暖伴一生 · **副 Slogan**:智慧宠物门店服务系统 > **Logo / VI**:[《宠小它-Logo与品牌识别说明》](./宠小它-Logo与品牌识别说明.md)(含概念草案图) @@ -149,6 +149,22 @@ | metadata_json | TEXT | 服务端白名单低敏维度,不存原始请求 | | idempotency_key | VARCHAR(128) | 可选稳定幂等键 | +### t_follow_up_task(回访执行任务) +| 字段 | 类型 | 说明 | +|------|------|------| +| id / task_id | BIGINT / VARCHAR(36) | 内部主键 / Admin 对外随机 ID | +| store_id / store_customer_id | BIGINT | 门店范围 / canonical 客户关系 | +| source_lead_id / source_report_id | BIGINT | 来源留资 / 来源报告 | +| status | VARCHAR(16) | pending / in_progress / completed / canceled | +| outcome | VARCHAR(24) | rebooked / not_interested / invalid_contact / unsubscribed;仅终态 | +| due_date / assigned_user_id | DATE / BIGINT | 下次回访日 / 当前领取人 | +| attempt_count | INT | 已形成回执的员工联系次数 | +| last_attempt_outcome | VARCHAR(24) | no_answer / follow_later;仅非终态联系结果 | +| last_contacted_at / last_contacted_by_user_id | DATETIME / BIGINT | 最近联系回执 | +| closed_at / closed_by_user_id | DATETIME / BIGINT | 终结回执;退订取消无员工关闭人 | +| rebooked_appointment_id | BIGINT | 仅真实预约保存成功后写入 | +| create_time / update_time / version | DATETIME / DATETIME / BIGINT | 时间戳与乐观锁版本 | + ### t_appointment(预约表) | 字段 | 类型 | 说明 | |------|------|------| @@ -218,13 +234,14 @@ | 功能 | 老板(boss) | 员工(staff) | |------|-------------|---------------| | 预约列表 | ✅ | ✅ | -| 新建预约 | ✅ | ✅ | +| 新建预约 / 回访后代客预约 | ✅ | ✅ | | 开始服务/填写报告 | ✅ | ✅ | | 发送报告 | ✅ | ✅ | | 员工管理 | ✅ | ❌ | | 邀请码 | ✅ | ❌ | | 店铺设置 | ✅ | ❌ | | 服务类型管理 | ✅ | ❌ | +| 领取、改期、关闭本店回访任务 | ✅ | ✅ | ### 页面入口 - **index.html(统一入口)**:老板/员工同一套页面,通过「我的」页面区分功能 @@ -255,6 +272,7 @@ - 建档人 = 当前登录用户 - 号源以服务项目预计时长、门店营业容量时段、门店并发接待数、线上预约与手动占用共同计算;创建时检查服务覆盖的每个半小时桶,并串行化同店容量判定避免超卖 - 线上预约当前不预选技师,容量是门店对外承诺的并发接待数;个人技师日历在具备预约前分配技师能力后再引入 +- 从回访任务发起时携带 `followUpTaskId`;仅在同店、同 canonical 客户、当前员工已领取且预约真实保存成功后,任务才能以 `rebooked` 完成 **服务类型:** - 系统默认:洗澡、美容、洗澡+美容、剪指甲、驱虫 @@ -372,6 +390,13 @@ - 每条事实可跳转对应预约、报告或回访池;不在时间线复制备注、媒体或联系方式原文 - 时间线只读且不承载会员、余额、次卡、积分或可编辑营销标签 +**回访任务池(Admin):** +- `ReportLead` 保存宠主同意和来源事实,`FollowUpTask` 保存员工协作状态;Admin 不再直接改留资状态 +- 员工先领取任务,领取人才能登记未接通/稍后联系、关闭本轮或发起再次预约;他人已领取返回冲突 +- 未接通/稍后联系会增加联系次数并回到待领取;暂无意向/联系方式无效为完成终态;退订为系统取消且不增加员工次数 +- `rebooked` 不提供手工关闭选项,必须通过真实预约创建且容量校验成功;工作台漏斗只统计 `rebook_created` +- 同店已认证员工仅在开放任务中可见完整手机号用于实际联系;终态任务、工作台、时间线、事件和审计不复制手机号原文 + --- ## 六、技术方案 @@ -406,6 +431,7 @@ | **P0** | **洗美过程短视频自动成片(可变时长、双编排、模板拼接)** | **🔨 本期迭代重点** | | P0 | 老板后台(开通清单+员工邀请中心) | ✅ Admin 与小程序均已接入 | | P0 | 服务客户事实时间线(Admin) | ✅ 代码完成,含 StoreCustomer 合并别名历史连续性,待生产迁移 | +| P0 | 可执行回访任务池 + 真实再次预约归因(Admin) | ✅ 代码完成,待生产迁移与三角色验收 | | P0 | 老板后台(预约概览) | ✅ 已合并到index.html | | P1 | 服务类型管理(老板后台) | ✅ | | P1 | 店铺设置(老板后台) | ✅ | @@ -462,6 +488,9 @@ CREATE TABLE t_report_image ( INDEX idx_report_image_report_id (report_id), INDEX idx_report_image_report_type_sort (report_id, photo_type, sort_order) ); + +-- 回访任务完整建表、历史回填和五项验证见: +-- backend/db/migrations/20260802_create_follow_up_task.sql ``` --- diff --git a/架构决策-FollowUpTask回访状态机与再次预约归因-2026-08-02.md b/架构决策-FollowUpTask回访状态机与再次预约归因-2026-08-02.md new file mode 100644 index 0000000..25f7d35 --- /dev/null +++ b/架构决策-FollowUpTask回访状态机与再次预约归因-2026-08-02.md @@ -0,0 +1,61 @@ +# 架构决策:FollowUpTask 回访状态机与再次预约归因 + +> 日期:2026-08-02 +> 状态:Accepted / Code Complete +> 本体:`entity:follow_up_task`、`rule:BR-FU-001`、`rule:BR-FU-002` + +## 1. 决策 + +`ReportLead` 继续保存宠主同意、来源报告、建议日期与退订凭证等留资事实;门店员工的领取、联系、改期、关闭和再次预约改由独立 `FollowUpTask` 承载。不得继续用 `ReportLead.remind_status` 同时表达同意事实和多人协作状态。 + +留资成功后,同一 `sourceLeadId` 至多存在一个开放任务。重复提交复用开放任务并同步建议日期;上一轮已终结后再次提交,创建新的任务轮次。历史迁移只为 `remind_status=pending` 的线索回填任务,不推测 `sent/canceled/unsubscribed` 的真实处理结果。 + +`ReportLead.remind_status` 暂保留为兼容投影:新建或改期为 `pending`,无意向/无效联系方式为 `canceled`,真实再次预约为 `sent`,退订为 `unsubscribed`。Admin 回访工作流与经营统计不再以该兼容字段为事实源。 + +## 2. 状态机 + +```text +pending --领取--> in_progress +in_progress --未接通/稍后联系--> pending(attempt_count + 1,更新 due_date) +in_progress --暂无意向/联系方式无效--> completed(attempt_count + 1) +in_progress --真实预约创建成功--> completed / rebooked(attempt_count + 1) +pending|in_progress --宠主退订--> canceled / unsubscribed(不增加员工联系次数) +``` + +- `pending` 不允许有领取人;`in_progress` 必须有领取人。 +- 只有领取该任务的当前员工才能改期、关闭或发起再次预约。 +- 两名员工并发领取时通过悲观锁和 `@Version` 防止静默覆盖;他人已领取返回 409。 +- `completed/canceled` 为终态,不回退。跨店任务按不存在返回 404。 +- `no_answer/follow_later` 仅是非终态联系结果;`rebooked/not_interested/invalid_contact/unsubscribed` 仅是终态结果。 + +## 3. 真实再次预约归因 + +`rebooked` 不是员工可手工选择的关闭结果。只有 `POST /api/appointment/create` 在同一事务中满足下列条件,才能写入: + +1. `followUpTaskId` 属于当前门店,处于 `in_progress` 且由当前员工领取; +2. 任务的 canonical `StoreCustomer` 与本次代客预约解析出的客户一致; +3. 预约通过未来时间、服务时长和每个半小时桶容量校验并真实保存; +4. 保存后任务写 `rebooked_appointment_id`,同时落 `follow_up_completed` 与 `rebook_created` 事实。 + +无号源、客户不匹配、状态冲突或预约保存失败时,整笔事务回滚,任务保持处理中。漏斗中的“回访后再次预约”只统计 `rebook_created`,不从手机号相同或后续页面点击推断。 + +## 4. 权限、隐私与审计 + +- `GET/POST /api/admin/follow-up-tasks/**` 仅本店 boss/staff,`storeId` 和操作人均从 session 派生。 +- 同店已认证员工为了实际电话联系可在开放任务中看到完整手机号;终态任务只返回脱敏手机号。工作台、客户时间线、BusinessEvent 和 AuditLog 只保留脱敏或低敏字段。 +- BusinessEvent metadata 仅允许 `dueDate/attemptOutcome/outcome/attemptCount/bookingOrigin` 等白名单标量,不存手机号、备注、微信标识、token、IP 或请求体。 +- 领取、改期、关闭和真实再次预约均写不可变低敏 AuditLog;退订由公开凭证触发,不伪装成员工联系次数或操作人。 + +## 5. 迁移与验收 + +迁移:`backend/db/migrations/20260802_create_follow_up_task.sql`,固定在 StoreCustomer 时间线迁移后第八个执行。末尾五项验证必须为 0: + +1. 待处理历史线索无开放任务; +2. 任务客户归属或 canonical 范围无效; +3. 状态、结果、领取人、联系回执或真实预约引用不一致; +4. 同一线索存在多个开放任务; +5. 任务缺少创建事实。 + +生产只读预检同步检查上述不变量,并额外要求 `rebooked_appointment_id` 指向同店、未删除的真实预约以及对应 `rebook_created` 事件。 + +自动化验收覆盖:历史回填、状态机、并发领取冲突、跨店 404、非领取人拒绝、退订取消、真实预约成功闭环、容量失败不闭环、客户时间线低敏投影、Admin 控制器权限与 production build。 diff --git a/架构决策-StoreCustomer事实时间线-2026-08-02.md b/架构决策-StoreCustomer事实时间线-2026-08-02.md index 6023499..7b2bbd0 100644 --- a/架构决策-StoreCustomer事实时间线-2026-08-02.md +++ b/架构决策-StoreCustomer事实时间线-2026-08-02.md @@ -15,14 +15,14 @@ - 按 `occurredAt DESC, id DESC` 对 canonical 与别名事件统一稳定分页;同一时刻用数据库事件 ID 决定顺序。 - 展示预约创建/状态、开始/完成服务、报告提交/确认发送/首次打开/再次打开、回访意向等已经落库的事实。 -- 批量补齐预约、报告、回访对象的当前低敏摘要和详情跳转 ID;对象已删除时仍保留事件标题和发生时间,不伪造摘要。 -- Admin 客户抽屉可跳到预约详情、报告列表和回访池;本批不创建回访任务或再次预约归因。 +- 批量补齐预约、报告、留资和 FollowUpTask 的当前低敏摘要与详情跳转 ID;对象已删除时仍保留事件标题和发生时间,不伪造摘要。 +- Admin 客户抽屉可跳到预约详情、报告列表和回访任务池;后续已按独立决策落地 FollowUpTask 与真实再次预约归因,时间线只读取其不可变事件,不承担任务状态写入。 ## 3. 隐私与数据边界 允许返回:客户称呼、脱敏手机号、宠物名、服务类型、预约时间、当前业务状态、发送渠道、提醒日期、操作人显示名/角色。 -禁止返回:手机号原文、预约/报告备注、图片/视频 URL、报告或邀请 token/hash、openid/unionid、IP、请求体和原始 `metadata_json`。服务端只解析 `fromStatus/toStatus/channel/repeatSubmit` 四个白名单 metadata 字段。 +禁止返回:手机号原文、预约/报告备注、图片/视频 URL、报告或邀请 token/hash、openid/unionid、IP、请求体和原始 `metadata_json`。服务端只解析状态变化、发送渠道、重复提交、回访日期/结果/次数和预约来源等受控低敏 metadata 字段。 时间线不承载会员等级、余额、次卡、积分、营销标签或自由文本跟进记录。这些能力若进入后续阶段,必须使用独立对象与状态机,不得改写历史事实。 diff --git a/生产发布与回滚Runbook-2026-08-01.md b/生产发布与回滚Runbook-2026-08-01.md index 6870a2a..ae3186f 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,或预检的 26 项只读不变量非 0; +- 任一迁移末尾验证查询非 0,或预检的 31 项只读不变量非 0; - `nginx -t` 失败,或 diff 显示将修改 Gitea/GitLab 的 server block; - readiness 非 `UP`,上传目录/FFmpeg/数据库任一依赖失败; - 发现凭据、完整 token、手机号或私密媒体地址进入日志/工单/截图; @@ -110,6 +110,7 @@ shasum -a 256 /secure/backup/petstore-before--.sql 5. `backend/db/migrations/20260801_create_report_send_status.sql` 6. `backend/db/migrations/20260802_create_store_onboarding.sql` 7. `backend/db/migrations/20260802_create_store_customer_timeline.sql` +8. `backend/db/migrations/20260802_create_follow_up_task.sql` 禁止跳步、交换顺序或让应用自动补表。任一验证项非 0,停止发布并由 Data Model/业务 owner 判断修复方案;不得猜测性回填客户身份、报告作者、事件、历史发送事实或历史开通状态,也不得改写既有 BusinessEvent 来迁移客户归属。 @@ -130,7 +131,7 @@ systemd-run --wait --pipe --collect \ ./deploy/release-preflight.sh ``` -通过条件:静态配置检查通过、JPA `validate` 通过、26 项数据不变量全部为 0,并以退出码 0 结束。预检不会执行迁移或修复数据。 +通过条件:静态配置检查通过、JPA `validate` 通过、31 项数据不变量全部为 0,并以退出码 0 结束。预检不会执行迁移或修复数据。 ## 6. 构建发布产物 @@ -209,7 +210,7 @@ API_ORIGIN=https:// ./deploy/production-smoke.sh ### 9.2 数据库回滚边界 -本批七个迁移以新增列/表/索引和兼容回填为主,旧应用应忽略新增结构。因此常规回滚只回应用,不删除列、表、索引,不执行自动 down migration,也不把已产生的新业务数据覆盖回旧备份。 +本批八个迁移以新增列/表/索引和兼容回填为主,旧应用应忽略新增结构。因此常规回滚只回应用,不删除列、表、索引,不执行自动 down migration,也不把已产生的新业务数据覆盖回旧备份。 只有确认发生灾难性数据破坏、已冻结所有写入、明确接受备份时间点之后的数据丢失,并取得用户/业务负责人、DBA 和隐私责任人的书面授权后,才可执行整库恢复。恢复必须先在隔离库验证,再制定专门变更单;本 Runbook 不授权该操作。 @@ -219,7 +220,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 641f168..7b81e6f 100644 --- a/生产监控与告警基线-2026-08-01.md +++ b/生产监控与告警基线-2026-08-01.md @@ -58,9 +58,9 @@ - 服务时长/预约快照非法、容量超卖; - 新报告已提交但长期未确认发送; - 已确认发送但长期未打开; -- 回访后没有明确状态或再次预约归因。 +- FollowUpTask 状态/领取人/终态结果不一致,或 `rebooked` 未指向同店有效预约及 `rebook_created` 事实。 -发布前的 `release-preflight.sh` 已覆盖前四类结构/数据不变量;运行期应每天做等价的只读审计或报表,不自动修复。 +发布前的 `release-preflight.sh` 已覆盖上述可由数据库可靠验证的结构/数据不变量;运行期应每天做等价的只读审计或报表,不自动修复。 ## 5. 日志与隐私 @@ -83,7 +83,7 @@ - [ ] 外部 HTTPS 探测覆盖 liveness/readiness、admin、H5 报告入口; - [ ] 5xx、延迟、DB 池、磁盘、证书和进程重启已配置告警; - [ ] FFmpeg/上传失败有可聚合字段,而非只能搜索自由文本; -- [ ] BusinessEvent 日漏斗和 26 项只读数据审计有每日结果; +- [ ] BusinessEvent 日漏斗和 31 项只读数据审计有每日结果; - [ ] 通知渠道、Primary/Backup 值班人和升级联系人完成真实测试; - [ ] 日志脱敏抽查通过,访问权限和保留周期已批准; - [ ] 发布标记能关联四仓 commit 与 RC; diff --git a/门店管理后台-PhaseA-交付目标.md b/门店管理后台-PhaseA-交付目标.md index 71e3ac6..fb7b4a4 100644 --- a/门店管理后台-PhaseA-交付目标.md +++ b/门店管理后台-PhaseA-交付目标.md @@ -20,11 +20,11 @@ | 模块 | 状态 | 主要缺口 | |------|------|----------| | A0 壳层登录 | ~95% | 生产微信登录策略另议 | -| A1 工作台 | ~99% | 服务端准确行动聚合、低敏预览、报告待发送行动与显式发送漏斗已做;仅转预约归因和真实环境 smoke 待补 | +| A1 工作台 | ~99% | 服务端准确行动聚合、低敏预览、报告待发送与回访后真实再次预约漏斗已做;真实环境 smoke 待补 | | A2 预约 | ~95% | 详情/筛选/待出报告/宠主脱敏已做 | | A6 排班 | ~90% | 与小程序对照验收 | | A3 报告 | ~95% | 详情抽屉+失败文案+H5 | -| A4 线索 | ~90% | 到期筛+打开报告 | +| A4 回访任务 | ~99% | 领取/改期/关闭/真实再次预约已做;真实环境三角色 smoke 待补 | | A5 客户 | ~95% | 详情抽屉+跳转 | | A7 设置 | ~90% | 坐标/logo 可选增强 | | T6 QA | 清单已出 | **待人工冒烟** | diff --git a/门店管理后台-PhaseA-线框说明.md b/门店管理后台-PhaseA-线框说明.md index 0f285d3..291b976 100644 --- a/门店管理后台-PhaseA-线框说明.md +++ b/门店管理后台-PhaseA-线框说明.md @@ -48,7 +48,7 @@ ⑤ 报告尚未发送 n 最近 3 条 …………………… [全部处理] ⑥ 回访日期已到 n 最近 3 条 …………………… [全部处理] -报告漏斗 已提交 | 已确认发送 | 已打开 | 已留资 | 转预约— +报告漏斗 已提交 | 已确认发送 | 已打开 | 已留资 | 回访后预约 ``` **主操作**:点指标、行动组或具体低敏预览下钻;工作台常驻,不用先关弹窗。 @@ -111,20 +111,21 @@ --- -## A4 回访线索 `/leads` +## A4 回访任务 `/leads` -**一句话**:把该回访的留资列出来,点开关联报告跟进。 +**一句话**:把留资转成可领取、可改期、可闭环且能真实发起再次预约的门店行动。 ```text -[状态▾] [到期≤今天] [日期区间] [查询] +[状态▾] [到期≤今天] [查询] -宠物 | 服务 | 手机(脱敏) | 建议日 | 状态 | 微信 | 操作 -─────┼──────┼────────────┼────────┼──────┼──────┼──────── -… | … | 138**** | 07-10 | 待回访| 是 | 打开报告 +客户/手机 | 宠物/服务 | 计划日 | 状态/领取人 | 次数 | 操作 +──────────┼───────────┼────────┼─────────────┼──────┼──────────────── +… | … | 07-10 | 待领取 | 0 | 领取 +… | … | 07-10 | 我处理中 | 1 | 改期 关闭 再次预约 ``` -**主操作**:筛待回访;打开关联报告公开页。 -**不做**:跟进备注 CRM、群发、发券。 +**主操作**:领取;登记未接通/稍后联系并改期;关闭本轮;选择真实号源并创建再次预约。 +**不做**:自由文本 CRM、群发、发券、手工标记 rebooked。 --- diff --git a/门店管理后台-PhaseA-页面清单PRD.md b/门店管理后台-PhaseA-页面清单PRD.md index 3c0da23..155abd8 100644 --- a/门店管理后台-PhaseA-页面清单PRD.md +++ b/门店管理后台-PhaseA-页面清单PRD.md @@ -45,7 +45,7 @@ entity:admin_console ├── /appointments 预约中心 ├── /schedule 排班占用 ├── /reports 服务报告 -├── /leads 回访线索 +├── /leads 回访任务 ├── /customers 服务客户 └── /settings 门店设置(资料 / 服务类型 / 员工) ``` @@ -90,9 +90,9 @@ entity:admin_console ```text 顶栏 4 指标卡 常驻行动队列(不再每日强弹) -报告漏斗四格(提交/打开/留资已由 BusinessEvent 汇总;转预约仍为「—」) +报告漏斗(提交/发送/打开/留资/回访后再次预约均由 BusinessEvent 汇总) 每组最多 3 条低敏预览 + 准确总数 + 列表下钻 -报告漏斗(提交 / 显式确认发送 / 打开 / 留资 / 再预约未知) +报告漏斗(提交 / 显式确认发送 / 打开 / 留资 / 回访后真实再次预约) ``` ### 2.2 顶栏指标卡 @@ -113,7 +113,7 @@ entity:admin_console | `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` | +| `follow_up_due` | 回访日期已到 | FollowUpTask 为 `pending/in_progress` 且 `dueDate≤今天` | 回访任务 `due=today` | 优先级固定为:逾期预约 → 服务中 → 今日待开始 → 成片异常 → 报告待发送 → 到期回访。每组只返回最多 3 条预览,预览不含手机号、报告 token、媒体 URL 或微信标识;准确总数由服务端按完整数据计算。工作台不再每日强弹,避免值班人员为了关弹窗而忽略真正行动。 @@ -127,7 +127,7 @@ entity:admin_console | 已确认发送 | `report_sent` 按 report 去重 | 仅员工显式确认事实;复制/二维码/预览不计入 | | 已打开 | `report_opened/reopened` 按 report 去重 | BusinessEvent 汇总 | | 已留资 | `lead_submitted` 按 lead 去重 | BusinessEvent 汇总 | -| 报告转预约 | `rebook_created` | 尚无可靠归因,显示「—」 | +| 回访后再次预约 | `rebook_created` 按预约去重 | 仅真实预约创建成功后统计 | ### 2.5 API / 聚合 @@ -273,32 +273,40 @@ entity:admin_console --- -## 6. 回访线索 +## 6. 回访任务 **页面 ID**:`A4-leads` **路由**:`/leads` -**ontologyRefs**:`[entity:report_lead, action:admin_list_leads, rule:BR-LEAD-001, rule:BR-LEAD-002, rule:BR-LEAD-003, rule:BR-ADMIN-001, rule:BR-ADMIN-002]` +**ontologyRefs**:`[entity:report_lead, entity:follow_up_task, action:admin_list_leads, action:start_follow_up_task, action:reschedule_follow_up_task, action:close_follow_up_task, rule:BR-FU-001, rule:BR-FU-002, rule:BR-ADMIN-001, rule:BR-ADMIN-002]` ### 6.1 筛选 -`remindStatus`(pending / 全部)、到期(≤今天)、日期区间 +`status`(open / pending / in_progress / completed / canceled / all)、到期(≤今天)、分页 ### 6.2 列表列 -宠物名、服务、脱敏手机、建议日期、状态、微信绑定(是/否 + 脱敏 openid 若有)、操作:查看关联报告公开页 +客户、联系电话、宠物名、服务、计划回访日、状态、领取人、联系次数、最近结果。完整手机号只在本店已认证员工的开放任务中返回,用于实际电话联系;终态任务、工作台与客户时间线均脱敏。 + +操作:待领取任务可领取;本人处理中任务可登记未接通/稍后联系并改期、以暂无意向/联系方式无效关闭,或进入代客预约并读取真实号源。`rebooked` 不提供手工结果。 ### 6.3 不做(A 期) -跟进备注 CRM、短信群发、改等级、发券 +自由文本跟进备注 CRM、短信群发、改等级、发券、手工标记再次预约 ### 6.4 API 复用 -- `GET /api/report/leads` +- `GET /api/admin/follow-up-tasks` +- `POST /api/admin/follow-up-tasks/{taskId}/start` +- `POST /api/admin/follow-up-tasks/{taskId}/reschedule` +- `POST /api/admin/follow-up-tasks/{taskId}/close` +- `GET /api/appointment/available-slots` + `POST /api/appointment/create`(携带 `followUpTaskId`) ### 6.5 验收 -- [ ] 无完整 openid/unionid -- [ ] 跨店 403 +- [ ] 无 openid/unionid/token/备注;事件、审计和时间线无完整手机号 +- [ ] 跨店任务按 404;customer 禁止 +- [ ] 他人已领取返回冲突;非领取人不能改期/关闭/再次预约 +- [ ] 无号源或预约失败时任务不闭环;成功时生成真实 appointment 与 `rebook_created` - [ ] 工作台「待回访」下钻数字一致 --- @@ -416,7 +424,7 @@ API:`user/staff-list`、`create-staff`、删除接口 | A2 预约 | `admin_list_appointments` | `appointment` | `/api/appointment/*` | | A6 排班 | `admin_manage_schedule` | `schedule_block` | `/api/schedule/*` | | A3 报告 | `admin_list_reports` | `report` / `highlight_video` | `/api/report/*` | -| A4 线索 | `admin_list_leads` | `report_lead` | `/api/report/leads` | +| A4 回访任务 | `admin_list_leads` + 3 个回访命令 | `follow_up_task` / `report_lead` | `/api/admin/follow-up-tasks/*` + `/api/appointment/*` | | A5 客户 | `admin_list_service_customers` | `store_customer` | **已实现** `GET /api/admin/service-customers` | | A7 设置 | `admin_update_settings` | `store` / `service_type` / `user` | store/service-type/user | diff --git a/门店管理后台升级方案-对标宠老板.md b/门店管理后台升级方案-对标宠老板.md index 77401cd..eeb4e5e 100644 --- a/门店管理后台升级方案-对标宠老板.md +++ b/门店管理后台升级方案-对标宠老板.md @@ -86,7 +86,7 @@ ├── 预约与排班 # 日/周视图、号源、手动占用、改派/取消 ├── 服务报告 # 列表、详情、成片状态、重新生成、分享链接管理 ├── 客户与宠物 # 宠主/宠物档案(服务过的)、历史报告入口 -├── 回访线索 # Leads 池、提醒日期、退订状态、跟进备注(可后补) +├── 回访任务 # 可领取、改期、关闭、真实再次预约;自由文本备注后补 ├── 经营数据 # 打开率、成片成功率、留资转化、预约完成率(依赖埋点落地) ├── 门店设置 # 资料、营业时段、服务类型、员工与邀请码 └──(后期)会员与资产 # 次卡/储值/套餐 — 对标宠老板会员,但不做库存 @@ -108,7 +108,7 @@ Web 后台主攻:**经营、检索、批量、报表、设置**;**允许「 | A1 工作台 | 登录落点;「今日待办」弹层(预约/待报告/成片异常/待回访)+ 顶栏 4 指标卡;卡片下钻列表 | 现有 API 聚合 | | A2 预约中心 | 列表筛选(状态/日期/技师)、详情、取消/开始(权限同现网) | Appointment* | | A3 报告中心 | 报告列表、公开链接复制、成片三态、触发重生成 | Report* / Highlight | -| A4 线索中心 | 回访池、脱敏展示、建议日期 | ReportLead* | +| A4 回访任务 | FollowUpTask 领取/改期/关闭/真实再次预约 | ReportLead + FollowUpTask | | A5 设置中心 | 门店资料、营业时段、服务类型、员工 | Store/User/ServiceType | | A6 排班 | 日视图 + 手动占用 | Schedule* | @@ -443,7 +443,7 @@ Web 后台主攻:**经营、检索、批量、报表、设置**;**允许「 └──────────────────┴──────────────────────────┴──────────────────┘ ``` -**报告漏斗**(替代微信商城状态):`今日已提交报告` · `已确认发送` · `已打开` · `已留资` · `从报告发起的预约`——前四项已由 BusinessEvent 汇总;发送只认员工显式确认,不以提交、复制链接或展示二维码冒充;仅转预约归因仍显示「—」。 +**报告漏斗**(替代微信商城状态):`今日已提交报告` · `已确认发送` · `已打开` · `已留资` · `回访后真实再次预约`——均由 BusinessEvent 汇总;发送只认员工显式确认,不以提交、复制链接或展示二维码冒充;再次预约只认携带 FollowUpTask 且真实保存成功的 Appointment。 ### 11.7 截图覆盖度与仍缺清单(2026-07-10 刷新)