docs: define follow-up task and rebook contracts

This commit is contained in:
malei 2026-08-02 02:11:28 +08:00
parent 8c7e6aec56
commit 598a250c81
19 changed files with 405 additions and 87 deletions

View File

@ -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`;不宣称微信投递成功 | | **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`;单角色账号冲突明确拒绝 | | **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`;既有事件不改写 | | **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 | 状态 | 验收命令 | 备注 | | 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 | 真实配置不得提交 | | **H2** | **前端可重复发布门禁** | admin/H5/小程序真实域名配置先门禁再构建;仓库占位值不能进入正式包;微信合法域名、体验版、静态回滚路径明确 | Frontend Release Ops | ✅ 仓库能力已实现;待真实环境执行 | admin/frontend preflight + 三端构建 + Runbook | 真实配置不得提交 |
| **H3** | **P0 通过条件** | 见下方「P0 通过条件」块 | System Architect / Core Flow QA | 🟡 待人工 smoke 复核 | 见下方 | 对应 P0-10 | | **H3** | **P0 通过条件** | 见下方「P0 通过条件」块 | System Architect / Core Flow QA | 🟡 待人工 smoke 复核 | 见下方 | 对应 P0-10 |
@ -164,7 +165,7 @@
- 报告前后照片缺失返回对应业务码。 - 报告前后照片缺失返回对应业务码。
- A 店 boss/staff 不能查 B 店 leads/list/reportcustomer 不能用任意 `userId` 创建他人宠物/预约。 - A 店 boss/staff 不能查 B 店 leads/list/reportcustomer 不能用任意 `userId` 创建他人宠物/预约。
- 生产配置扫描不含真实 DB/微信密钥,最终 profile 为 `ddl-auto=validate`CORS 仅允许显式生产 HTTPS 源;只读生产预检通过。 - 生产配置扫描不含真实 DB/微信密钥,最终 profile 为 `ddl-auto=validate`CORS 仅允许显式生产 HTTPS 源;只读生产预检通过。
- 已按顺序完成身份、StoreCustomer、BusinessEvent、预约容量个生产迁移,并归档备份与验证证据。 - 已按固定顺序完成身份、StoreCustomer、BusinessEvent、预约容量、报告发送、门店开通、客户时间线与 FollowUpTask 八个生产迁移,并归档备份与验证证据。
- liveness/readiness、只读 smoke 与 boss/staff/customer 三角色真实环境 smoke 通过Gitea/GitLab 未受 Petstore 配置影响。 - liveness/readiness、只读 smoke 与 boss/staff/customer 三角色真实环境 smoke 通过Gitea/GitLab 未受 Petstore 配置影响。
- 文档口径扫描:`rg "到店不排队|会员储值 \| 路线图 P0|宠主通过微信查看服务报告v2|无限制,随便选" docs` 无命中。 - 文档口径扫描:`rg "到店不排队|会员储值 \| 路线图 P0|宠主通过微信查看服务报告v2|无限制,随便选" docs` 无命中。

View File

@ -3,7 +3,7 @@
> 本体条目 ↔ 文档章节 ↔ 代码路径 ↔ 测试用例 三方对齐审计。 > 本体条目 ↔ 文档章节 ↔ 代码路径 ↔ 测试用例 三方对齐审计。
> 证据强度:`anchored`(三处对齐)/ `documented`(仅文档)/ `implemented`(仅代码)/ `gap`(缺失)。 > 证据强度:`anchored`(三处对齐)/ `documented`(仅文档)/ `implemented`(仅代码)/ `gap`(缺失)。
> >
> **刷新时间**2026-08-02StoreCustomer 事实时间线收口后刷新) > **刷新时间**2026-08-02FollowUpTask 回访闭环收口后刷新)
## 审计方法 ## 审计方法
@ -18,14 +18,14 @@
| 类型 | 数量 | | 类型 | 数量 |
|------|------| |------|------|
| Entity | 28 | | Entity | 29 |
| Action | 74 | | Action | 77 |
| Event | 27 | | Event | 33 |
| Rule | 47 | | Rule | 49 |
| Relation | 89 | | Relation | 102 |
| **合计** | **265** | | **合计** | **290** |
证据强度:`anchored` 21982.6%/ `documented` 33 / `gap` 11 / `implemented` 2。 证据强度:`anchored` 24985.9%/ `documented` 28 / `gap` 11 / `implemented` 2。
> anchored 占比相对 Batch 2 下降,是因为主动录入了 P1/P2 `gap` 占位与客户端埋点 `documented` 条目;端点漂移仍为 0。 > 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 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 | | | appointment | ✅ | ✅ | AppointmentServiceTest / DetailTest | anchored | |
| report | ✅ | ✅ | ReportServiceTest / ControllerTest | anchored | uk 上线需清重 | | report | ✅ | ✅ | ReportServiceTest / ControllerTest | anchored | uk 上线需清重 |
| report_lead | ✅ | ✅ | ReportLeadControllerTest | anchored | | | report_lead | ✅ | ✅ | ReportLeadControllerTest | anchored | |
| follow_up_task | ✅ | ✅ | FollowUpTaskServiceTest / MigrationTest / ControllerTest | anchored | 留资事实与操作状态拆分;真实预约引用 |
| pet | ✅ | ✅ | PetControllerTest | anchored | | | pet | ✅ | ✅ | PetControllerTest | anchored | |
| service_type | ✅ | ✅ | ServiceTypeControllerTest | anchored | | | service_type | ✅ | ✅ | ServiceTypeControllerTest | anchored | |
| schedule_block | ✅ | ✅ | ScheduleControllerTest | anchored | | | schedule_block | ✅ | ✅ | ScheduleControllerTest | anchored | |
@ -73,7 +74,7 @@ python3 docs/graph/audit_drift.py
| current_user | ✅ | ✅ record | SessionTokenServiceTest | anchored | jpa:false | | current_user | ✅ | ✅ record | SessionTokenServiceTest | anchored | jpa:false |
| membership 等 5 个 | 方向库 | — | — | gap | roadmap 占位 | | membership 等 5 个 | 方向库 | — | — | gap | roadmap 占位 |
## 规则覆盖度47 条) ## 规则覆盖度49 条)
| 规则 | 测试 | 证据 | | 规则 | 测试 | 证据 |
|------|------|------| |------|------|------|
@ -99,6 +100,7 @@ python3 docs/graph/audit_drift.py
| BR-INVITE-001 | StaffInvitationServiceTest / AuthInterceptorActiveUserTest | anchored | | BR-INVITE-001 | StaffInvitationServiceTest / AuthInterceptorActiveUserTest | anchored |
| BR-AUDIT-001 | AuditLogServiceTest / StoreOnboardingMigrationTest | anchored | | BR-AUDIT-001 | AuditLogServiceTest / StoreOnboardingMigrationTest | anchored |
| BR-SC-002 客户时间线低敏投影 | StoreCustomerTimelineServiceTest / AdminServiceCustomerControllerTest | anchored | | BR-SC-002 客户时间线低敏投影 | StoreCustomerTimelineServiceTest / AdminServiceCustomerControllerTest | anchored |
| BR-FU-001/002 回访状态机与真实再次预约 | FollowUpTaskServiceTest / AppointmentServiceTest / MigrationTest | anchored |
## 待补 gap 清单 ## 待补 gap 清单
@ -134,7 +136,7 @@ python3 docs/graph/audit_drift.py
| `entity:admin_console` | Entity | Web 管理端壳 | | `entity:admin_console` | Entity | Web 管理端壳 |
| `entity:workbench` / `workbench_todo_item` | Entity | 服务端准确行动聚合、低敏预览与报告事件漏斗均 anchored | | `entity:workbench` / `workbench_todo_item` | Entity | 服务端准确行动聚合、低敏预览与报告事件漏斗均 anchored |
| `entity:store_customer` | Entity | 门店客户稳定主档(非会员卡) | | `entity:store_customer` | Entity | 门店客户稳定主档(非会员卡) |
| `action:view_workbench` 等 8 个 admin_* | Action | 工作台/列表/设置/排班 | | `action:view_workbench`、8 个 admin_* 与 3 个回访命令 | Action | 工作台/列表/设置/排班/领取/改期/关闭 |
| `rule:BR-ADMIN-001`…`004` | Rule | 角色、店范围、内部字段、范围闸门 | | `rule:BR-ADMIN-001`…`004` | Rule | 角色、店范围、内部字段、范围闸门 |
| `event:workbench_viewed` | Event | 可选埋点gap | | `event:workbench_viewed` | Event | 可选埋点gap |
@ -142,4 +144,4 @@ python3 docs/graph/audit_drift.py
**页面清单 PRD已写**`docs/门店管理后台-PhaseA-页面清单PRD.md`A0A7 全页字段/筛选/API/验收)。 **页面清单 PRD已写**`docs/门店管理后台-PhaseA-页面清单PRD.md`A0A7 全页字段/筛选/API/验收)。
当前 validate 规模以脚本输出为准265 条目)。 当前 validate 规模以脚本输出为准290 条目)。

View File

@ -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: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: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: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: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: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"} {"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:get_report_by_token","type":"Action","name":"getReportByToken","nameZh":"公开报告查询","inputs":["token"],"outputs":["entity:report"],"entrypoints":["GET /api/report/get","ReportController#getByAppointmentId"],"appliesRules":["rule:BR-RPT-006"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#getByAppointmentId","test":"ReportControllerTest#publicTokenQueryHidesInternalFields","evidence":"anchored","domain":"report"}
{"id":"action:track_report_open","type":"Action","name":"trackReportOpen","nameZh":"报告打开埋点","inputs":["token","visitType"],"outputs":["entity:business_event"],"entrypoints":["POST /api/report/open-track","ReportController#trackReportOpen"],"appliesRules":["rule:BR-RPT-007","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#trackReportOpen; backend/src/main/java/com/petstore/service/BusinessEventService.java#recordReportOpenByToken","test":"ReportControllerTest; BusinessEventServiceTest","evidence":"anchored","domain":"report"} {"id":"action:track_report_open","type":"Action","name":"trackReportOpen","nameZh":"报告打开埋点","inputs":["token","visitType"],"outputs":["entity:business_event"],"entrypoints":["POST /api/report/open-track","ReportController#trackReportOpen"],"appliesRules":["rule:BR-RPT-007","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#trackReportOpen; backend/src/main/java/com/petstore/service/BusinessEventService.java#recordReportOpenByToken","test":"ReportControllerTest; BusinessEventServiceTest","evidence":"anchored","domain":"report"}
{"id":"action:delete_report","type":"Action","name":"deleteReport","nameZh":"删除报告","inputs":["id"],"outputs":[],"entrypoints":["DELETE /api/report/delete","ReportController#delete"],"appliesRules":["rule:BR-RPT-005"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#delete","evidence":"anchored","domain":"report"} {"id":"action:delete_report","type":"Action","name":"deleteReport","nameZh":"删除报告","inputs":["id"],"outputs":[],"entrypoints":["DELETE /api/report/delete","ReportController#delete"],"appliesRules":["rule:BR-RPT-005"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#delete","evidence":"anchored","domain":"report"}
{"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: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":[],"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: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":"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: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"} {"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_opened","type":"Event","name":"ReportOpened","nameZh":"报告首次打开","domain":"report","payload":["storeId","storeCustomerId","reportId","visitType","occurredAt"],"emittedBy":["action:track_report_open"],"appliesRules":["rule:BR-RPT-007","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#trackReportOpen; backend/src/main/java/com/petstore/service/BusinessEventService.java#recordReportOpenByToken","doc":"docs/本期主线-下一步产品优化点.md 埋点事件 schema","test":"ReportControllerTest; BusinessEventServiceTest","evidence":"anchored"}
{"id":"event:report_reopened","type":"Event","name":"ReportReopened","nameZh":"报告再次打开","domain":"report","payload":["storeId","storeCustomerId","reportId","visitType","occurredAt"],"emittedBy":["action:track_report_open"],"appliesRules":["rule:BR-RPT-007","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/service/BusinessEventService.java#recordReportOpenByToken","doc":"docs/本期主线-下一步产品优化点.md 埋点事件 schema","test":"BusinessEventServiceTest#reportOpenStoresReportIdButNeverToken","evidence":"anchored"} {"id":"event:report_reopened","type":"Event","name":"ReportReopened","nameZh":"报告再次打开","domain":"report","payload":["storeId","storeCustomerId","reportId","visitType","occurredAt"],"emittedBy":["action:track_report_open"],"appliesRules":["rule:BR-RPT-007","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/service/BusinessEventService.java#recordReportOpenByToken","doc":"docs/本期主线-下一步产品优化点.md 埋点事件 schema","test":"BusinessEventServiceTest#reportOpenStoresReportIdButNeverToken","evidence":"anchored"}
{"id":"event:lead_submitted","type":"Event","name":"LeadSubmitted","nameZh":"留资提交","domain":"lead","payload":["storeId","storeCustomerId","leadId","repeatSubmit","occurredAt"],"emittedBy":["action:submit_lead"],"appliesRules":["rule:BR-LEAD-001","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/service/ReportLeadService.java#submit; backend/src/main/java/com/petstore/service/BusinessEventService.java#recordLeadSubmitted","doc":"docs/本期主线-下一步产品优化点.md 埋点事件 schema","test":"ReportLeadServiceStoreCustomerTest; BusinessEventMigrationTest","evidence":"anchored"} {"id":"event:lead_submitted","type":"Event","name":"LeadSubmitted","nameZh":"留资提交","domain":"lead","payload":["storeId","storeCustomerId","leadId","repeatSubmit","occurredAt"],"emittedBy":["action:submit_lead"],"appliesRules":["rule:BR-LEAD-001","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/service/ReportLeadService.java#submit; backend/src/main/java/com/petstore/service/BusinessEventService.java#recordLeadSubmitted","doc":"docs/本期主线-下一步产品优化点.md 埋点事件 schema","test":"ReportLeadServiceStoreCustomerTest; BusinessEventMigrationTest","evidence":"anchored"}
{"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-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-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 保留数据库兼容,不返回客户端、不再作为注册凭证"} {"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-RPT-008","type":"Rule","name":"报告发送显式确认且首次回执不可覆盖","domain":"report","appliesTo":["action:confirm_report_sent"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#confirmSent; backend/src/main/java/com/petstore/service/ReportService.java#confirmSent; backend/src/main/java/com/petstore/mapper/ReportMapper.java#findByIdAndDeletedFalseForUpdate","test":"ReportControllerTest; ReportServiceTest; ReportSendMigrationTest","doc":"docs/架构决策-报告确认发送状态-2026-08-02.md","evidence":"anchored"}
{"id":"rule:BR-LEAD-001","type":"Rule","name":"留资去重","domain":"lead","appliesTo":["action:submit_lead"],"code":"backend/src/main/java/com/petstore/service/ReportLeadService.java#submit","evidence":"anchored","test":"ReportLeadControllerTest#submitLeadRepeatSubmitReturnsAlreadySubmitted"} {"id":"rule:BR-LEAD-001","type":"Rule","name":"留资去重","domain":"lead","appliesTo":["action:submit_lead"],"code":"backend/src/main/java/com/petstore/service/ReportLeadService.java#submit","evidence":"anchored","test":"ReportLeadControllerTest#submitLeadRepeatSubmitReturnsAlreadySubmitted"}
{"id":"rule:BR-SC-001","type":"Rule","name":"门店客户主档唯一性与合并","domain":"admin","appliesTo":["action:create_appointment","action:submit_lead","action:admin_list_service_customers"],"code":"backend/src/main/java/com/petstore/service/StoreCustomerService.java","doc":"docs/ontology/rules.md#rule:BR-SC-001; docs/架构决策-门店客户主档-2026-08-01.md","test":"StoreCustomerServiceTest; ReportLeadServiceStoreCustomerTest; IdentityOwnershipQueryTest; StoreCustomerTimelineMigrationTest","evidence":"anchored","note":"被吸收投影软删除并指向同店 canonical既有 BusinessEvent 不改写"} {"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-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":"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"} {"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-USER-002","type":"Rule","name":"用户更新仅本人","domain":"user","appliesTo":["action:update_user","action:get_user_info"],"code":"backend/src/main/java/com/petstore/controller/UserController.java#updateUser; #info","evidence":"anchored"}
{"id":"rule:BR-CONFIG-001","type":"Rule","name":"生产配置环境变量","domain":"config","appliesTo":[],"code":"backend/src/main/resources/application.yml; application-example.yml","doc":"backend/README.md 环境变量表","evidence":"anchored"} {"id":"rule:BR-CONFIG-001","type":"Rule","name":"生产配置环境变量","domain":"config","appliesTo":[],"code":"backend/src/main/resources/application.yml; application-example.yml","doc":"backend/README.md 环境变量表","evidence":"anchored"}
{"id":"rule:BR-CONFIG-002","type":"Rule","name":"production profile 强制只读 schema 安全配置","domain":"config","appliesTo":[],"code":"backend/src/main/resources/application.yml production profile; backend/src/main/java/com/petstore/config/ProductionConfigurationValidator.java; backend/src/main/java/com/petstore/PetstoreApplication.java","doc":"backend/README.md production profile","test":"ProductionConfigurationValidatorTest","evidence":"anchored","note":"生产禁用自动建表/show-sql/SMS 万能码/默认数据初始化,并拒绝弱密钥、占位域名和宽 CORS"} {"id":"rule:BR-CONFIG-002","type":"Rule","name":"production profile 强制只读 schema 安全配置","domain":"config","appliesTo":[],"code":"backend/src/main/resources/application.yml production profile; backend/src/main/java/com/petstore/config/ProductionConfigurationValidator.java; backend/src/main/java/com/petstore/PetstoreApplication.java","doc":"backend/README.md production profile","test":"ProductionConfigurationValidatorTest","evidence":"anchored","note":"生产禁用自动建表/show-sql/SMS 万能码/默认数据初始化,并拒绝弱密钥、占位域名和宽 CORS"}
{"id":"rule:BR-CONFIG-003","type":"Rule","name":"生产发布只读门禁","domain":"config","appliesTo":[],"code":"backend/src/main/java/com/petstore/config/ProductionDatabasePreflightRunner.java; backend/src/main/java/com/petstore/config/PetstoreRuntimeHealthIndicator.java; backend/deploy/release-preflight.sh; backend/deploy/production-smoke.sh","doc":"docs/生产发布与回滚Runbook-2026-08-01.md; docs/生产监控与告警基线-2026-08-01.md","evidence":"anchored","note":"备份和七迁移后运行 schema validate + 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:user_owns_pets","type":"Relation","subject":"entity:user","predicate":"OWNS","object":"entity:pet","cardinality":"1—N","evidence":"anchored","doc":"Pet.owner_user_id → User.id","domain":"user"}
{"id":"rel:pet_has_appointments","type":"Relation","subject":"entity:pet","predicate":"HAS","object":"entity:appointment","cardinality":"1—N","evidence":"anchored","doc":"Appointment.pet_id可空","domain":"pet"} {"id":"rel:pet_has_appointments","type":"Relation","subject":"entity:pet","predicate":"HAS","object":"entity:appointment","cardinality":"1—N","evidence":"anchored","doc":"Appointment.pet_id可空","domain":"pet"}
{"id":"rel:store_has_service_types","type":"Relation","subject":"entity:store","predicate":"HAS","object":"entity:service_type","cardinality":"1—N","evidence":"anchored","doc":"ServiceType.store_idNULL=系统默认)","domain":"store"} {"id":"rel:store_has_service_types","type":"Relation","subject":"entity:store","predicate":"HAS","object":"entity:service_type","cardinality":"1—N","evidence":"anchored","doc":"ServiceType.store_idNULL=系统默认)","domain":"store"}
@ -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_int_001_applies","type":"Relation","subject":"rule:BR-INT-001","predicate":"APPLIES_TO","object":"action:get_report_suggestion","domain":"service_type","evidence":"anchored"}
{"id":"rel:br_store_003_applies","type":"Relation","subject":"rule:BR-STORE-003","predicate":"APPLIES_TO","object":"action:get_store","domain":"store","evidence":"anchored"} {"id":"rel:br_store_003_applies","type":"Relation","subject":"rule:BR-STORE-003","predicate":"APPLIES_TO","object":"action:get_store","domain":"store","evidence":"anchored"}
{"id":"entity:admin_console","type":"Entity","name":"AdminConsole","nameZh":"门店 Web 管理后台","domain":"admin","jpa":false,"fields":["surface","navModules","storeScope","roleGate"],"systems":["Store Admin FE","Backend Core"],"code":"","doc":"docs/ontology/objects.md#entity:admin_console","test":"","evidence":"documented","note":"Phase A 产品预录入;独立 Web 端,与 uni-app 小程序并存。对标宠老板「店铺概况」入口,不做收银/库存。详见 docs/门店管理后台升级方案-对标宠老板.md","status":"planned"} {"id":"entity:admin_console","type":"Entity","name":"AdminConsole","nameZh":"门店 Web 管理后台","domain":"admin","jpa":false,"fields":["surface","navModules","storeScope","roleGate"],"systems":["Store Admin FE","Backend Core"],"code":"","doc":"docs/ontology/objects.md#entity:admin_console","test":"","evidence":"documented","note":"Phase A 产品预录入;独立 Web 端,与 uni-app 小程序并存。对标宠老板「店铺概况」入口,不做收银/库存。详见 docs/门店管理后台升级方案-对标宠老板.md","status":"planned"}
{"id":"entity:workbench","type":"Entity","name":"Workbench","nameZh":"今日行动工作台","domain":"admin","jpa":false,"fields":["todayAppointmentCount","todayDoneCount","overdueAppointmentCount","inServiceCount","upcomingTodayCount","highlightAnomalyCount","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_dueitems 不含手机号/token/媒体 URL","status":"implemented"} {"id":"entity:workbench_todo_item","type":"Entity","name":"WorkbenchTodoItem","nameZh":"工作台行动组","domain":"admin","jpa":false,"fields":["kind","urgency","count","items","drilldownAction","filter"],"systems":["Store Admin FE","Backend Core"],"code":"backend/src/main/java/com/petstore/service/AdminWorkbenchService.java; admin/src/views/WorkbenchView.vue","doc":"docs/ontology/objects.md#entity:workbench_todo_item","test":"AdminWorkbenchServiceTest","evidence":"anchored","note":"kind: overdue_appointment | in_service | upcoming_today | highlight_anomaly | unsent_report | follow_up_dueitems 不含手机号/token/媒体 URL","status":"implemented"}
{"id":"entity:store_customer","type":"Entity","name":"StoreCustomer","nameZh":"门店客户主档(瘦身)","domain":"admin","jpa":true,"fields":["id","store_id","customer_user_id","phone","display_name","source","first_contact_at","last_contact_at","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":"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 禁止。删店/部分设置仍仅 bossBR-STORE-001/BR-USER-001。产品决策 2026-07-10。"} {"id":"rule:BR-ADMIN-001","type":"Rule","name":"adminRoleGate","nameZh":"门店后台角色门禁boss/staff 可读写)","domain":"admin","appliesTo":["action:view_workbench","action:admin_list_appointments","action:admin_list_reports","action:admin_list_leads","action: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 禁止。删店/部分设置仍仅 bossBR-STORE-001/BR-USER-001。产品决策 2026-07-10。"}
{"id":"rule:BR-ADMIN-002","type":"Rule","name":"adminStoreScope","nameZh":"后台数据仅本店 storeId","domain":"admin","appliesTo":["action:view_workbench","action:admin_list_appointments","action:admin_list_reports","action:admin_list_leads","action:admin_list_service_customers","action:admin_update_settings"],"code":"","doc":"docs/ontology/rules.md#rule:BR-ADMIN-002","test":"","evidence":"documented","note":"所有后台查询/写操作强制 current.storeId忽略跨店请求参数。连锁同步不在 Phase A。"} {"id":"rule:BR-ADMIN-002","type":"Rule","name":"adminStoreScope","nameZh":"后台数据仅本店 storeId","domain":"admin","appliesTo":["action:view_workbench","action:admin_list_appointments","action:admin_list_reports","action:admin_list_leads","action: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-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":"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: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: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_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_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_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_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。"} {"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_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_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_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: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: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留资客户可先为空"} {"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: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_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_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: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":"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"} {"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_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: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":"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":"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":"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: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: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_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":"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_contactrebooked 不可手工选择"}
{"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"}

View File

@ -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 设计。 P1/P2 方向(会员/储值/套餐/历史报告/看板)以 `gap` 证据条目录入,不展开字段;受 `rule:BR-ADMIN-004` 约束不得偷渡进 Phase A 设计。

View File

@ -84,13 +84,13 @@ legacy 永久邀请码查询入口已停用,固定返回 `410 / LEGACY_INVITE_
### action:create_appointment ### action:create_appointment
创建预约:按服务时长跨半小时容量桶占号,并保存时长快照;取消不占号;客户与创建人分开记录。 创建预约:按服务时长跨半小时容量桶占号,并保存时长快照;取消不占号;客户与创建人分开记录。可选携带 `followUpTaskId`,在真实保存成功后闭环再次预约归因。
- **入口**`POST /api/appointment/create` → `AppointmentController#create``AppointmentService#createBooking` - **入口**`POST /api/appointment/create` → `AppointmentController#create``AppointmentService#createBooking`
- **输入**`petName`、`petType`、`serviceType`、`appointmentTime`、`storeId`、`remark?`、`petId?`;门店代客预约另需 `customerUserId``customerPhone` - **输入**`petName`、`petType`、`serviceType`、`appointmentTime`、`storeId`、`remark?`、`petId?`、`followUpTaskId?`;门店代客预约另需 `customerUserId``customerPhone`
- **输出**`entity:appointment` - **输出**`entity:appointment`;携带任务且成功时更新 `entity:follow_up_task`
- **身份口径**customer 自约时客户=创建人=current.userIdboss/staff 代约时创建人=current.userId客户按请求解析legacy userId 镜像客户 - **身份口径**customer 自约时客户=创建人=current.userIdboss/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` - **代码**`backend/src/main/java/com/petstore/controller/AppointmentController.java#create`
- **证据**`anchored` - **证据**`anchored`
@ -619,7 +619,7 @@ legacy 永久邀请码查询入口已停用,固定返回 `410 / LEGACY_INVITE_
### action:view_workbench ### action:view_workbench
查看今日行动工作台(登录落点)。服务端按完整本店数据聚合逾期预约、服务中、今日待开始、成片异常、新报告待发送和到期回访,返回准确总数与低敏预览;报告提交、显式确认发送、打开与留资通过 BusinessEvent 汇总,仅未建模的转预约保持未知 查看今日行动工作台(登录落点)。服务端按完整本店数据聚合逾期预约、服务中、今日待开始、成片异常、新报告待发送和到期 FollowUpTask返回准确总数与低敏预览报告提交、显式确认发送、打开、留资与回访后真实再次预约均通过 BusinessEvent 汇总
- **入口**Admin UI `/workbench``GET /api/admin/workbench/actions`;兼容 `GET /api/admin/workbench/report-funnel?date=yyyy-MM-dd` - **入口**Admin UI `/workbench``GET /api/admin/workbench/actions`;兼容 `GET /api/admin/workbench/report-funnel?date=yyyy-MM-dd`
- **输出**`entity:workbench`、`entity:workbench_todo_item` - **输出**`entity:workbench`、`entity:workbench_todo_item`
@ -656,11 +656,44 @@ legacy 永久邀请码查询入口已停用,固定返回 `410 / LEGACY_INVITE_
### action:admin_list_leads ### action:admin_list_leads
后台回访线索列表 后台回访任务池。导航路径保留 `/leads`,操作对象已由原始留资切换为 `FollowUpTask`
- **入口**Admin UI `/leads`planned复用 `GET /api/report/leads` - **入口**Admin UI `/leads``GET /api/admin/follow-up-tasks`
- **适用规则**`rule:BR-ADMIN-001`、`rule:BR-ADMIN-002`、`rule:BR-LEAD-002`、`rule:BR-LEAD-003` - **输入**`status`、`dueDateTo`、`page`、`pageSize`
- **证据**`documented` - **输出**`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 ### action:admin_list_service_customers
@ -691,7 +724,7 @@ legacy 永久邀请码查询入口已停用,固定返回 `410 / LEGACY_INVITE_
- **输入**`storeCustomerId`、`page`、`pageSize``storeId` 强制从当前会话派生 - **输入**`storeCustomerId`、`page`、`pageSize``storeId` 强制从当前会话派生
- **输出**`entity:customer_timeline` - **输出**`entity:customer_timeline`
- **适用规则**`rule:BR-AUTH-001`、`rule:BR-ADMIN-001``003`、`rule:BR-SC-001`、`rule:BR-SC-002`、`rule:BR-BE-001` - **适用规则**`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` - **代码/测试**`StoreCustomerTimelineService`、`CustomersView.vue``StoreCustomerTimelineServiceTest`、`AdminServiceCustomerControllerTest`
- **证据**`anchored` - **证据**`anchored`

View File

@ -139,6 +139,54 @@
- **文档**`docs/本期主线-下一步产品优化点.md 埋点事件 schema` - **文档**`docs/本期主线-下一步产品优化点.md 埋点事件 schema`
- **证据**`anchored` - **证据**`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 录入事件 ## Batch 2 录入事件

View File

@ -195,6 +195,30 @@
- **测试**暂无Batch D 未补 `ReportLeadControllerTest`,列为 `documented` gap - **测试**暂无Batch D 未补 `ReportLeadControllerTest`,列为 `documented` gap
- **证据**`anchored`(脱敏与跨店校验已实现并测试覆盖;去重逻辑 `documented` - **证据**`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 ## entity:business_event
**不可变业务事件**。预约、履约、报告打开与留资等经营事实的统一持久化入口,为报告漏斗和 StoreCustomer 时间线提供可统计事实源。 **不可变业务事件**。预约、履约、报告打开与留资等经营事实的统一持久化入口,为报告漏斗和 StoreCustomer 时间线提供可统计事实源。
@ -399,13 +423,13 @@
## entity:customer_timeline ## entity:customer_timeline
**门店客户事实时间线**(非 JPA 只读模型)。以 canonical `StoreCustomer.id` 为稳定客户关系键,把已经持久化的 `BusinessEvent` 投影为门店可理解的预约、履约、报告发送/打开和回访意向记录;被合并投影的事件通过别名指针一并读取,不改写历史事件。 **门店客户事实时间线**(非 JPA 只读模型)。以 canonical `StoreCustomer.id` 为稳定客户关系键,把已经持久化的 `BusinessEvent` 投影为门店可理解的预约、履约、报告发送/打开、留资和回访任务记录;被合并投影的事件通过别名指针一并读取,不改写历史事件。
| 字段 | 说明 | | 字段 | 说明 |
|------|------| |------|------|
| customer | 客户称呼、脱敏手机号、首次来源、首次/最近接触时间 | | customer | 客户称呼、脱敏手机号、首次来源、首次/最近接触时间 |
| total / page / pageSize / hasMore | 稳定倒序分页信息 | | total / page / pageSize / hasMore | 稳定倒序分页信息 |
| list | `eventId/eventType/occurredAt/title/description` 与可选预约/报告/回访低敏摘要 | | list | `eventId/eventType/occurredAt/title/description` 与可选预约/报告/留资/FollowUpTask 低敏摘要 |
- **归属系统**Backend Core / Store Admin FE - **归属系统**Backend Core / Store Admin FE
- **代码**`backend/src/main/java/com/petstore/service/StoreCustomerTimelineService.java`、`admin/src/views/CustomersView.vue` - **代码**`backend/src/main/java/com/petstore/service/StoreCustomerTimelineService.java`、`admin/src/views/CustomersView.vue`
@ -450,7 +474,7 @@
## entity:workbench ## 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 | 逾期未开始 / 服务中 / 今日待开始 | | overdueAppointmentCount / inServiceCount / upcomingTodayCount | 逾期未开始 / 服务中 / 今日待开始 |
| dueFollowUpCount / highlightAnomalyCount / unsentReportCount | 到期回访 / 成片失败或处理超时 / 新报告待发送 | | dueFollowUpCount / highlightAnomalyCount / unsentReportCount | 到期回访 / 成片失败或处理超时 / 新报告待发送 |
| actionGroups | `entity:workbench_todo_item[]`(每组最多 3 条低敏预览) | | actionGroups | `entity:workbench_todo_item[]`(每组最多 3 条低敏预览) |
| reportFunnel | 已提交/已确认发送/已打开/已留资/报告转预约;仅转预约仍返回未知 | | reportFunnel | 已提交/已确认发送/已打开/已留资/回访后真实再次预约;全部来自 BusinessEvent |
| anomalyList | 成片失败或待回访最近 N 条 | | anomalyList | 成片失败或待回访最近 N 条 |
- **代码**`admin/src/views/WorkbenchView.vue`、`AdminBusinessEventController`、`AdminWorkbenchService`、`AdminBusinessEventService` - **代码**`admin/src/views/WorkbenchView.vue`、`AdminBusinessEventController`、`AdminWorkbenchService`、`AdminBusinessEventService`

View File

@ -16,6 +16,9 @@
| `Report` 1—N `ReportImage` | 报告含多张照片before/after/during | `ReportImage.report_id``Report.id` | | `Report` 1—N `ReportImage` | 报告含多张照片before/after/during | `ReportImage.report_id``Report.id` |
| `Report` 1—N `ReportLead` | 一份报告可多次留资(同手机号去重) | `ReportLead.report_id``Report.id` | | `Report` 1—N `ReportLead` | 一份报告可多次留资(同手机号去重) | `ReportLead.report_id``Report.id` |
| `ReportLead` N—1 `Store` | 留资归属门店(回访池范围) | `ReportLead.store_id``Store.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` | | `Store` 1—N `BusinessEvent` | 所有可统计事实强制归属门店 | `BusinessEvent.store_id``Store.id` |
| `StoreCustomer` 1—N `BusinessEvent` | 客户时间线;无客户语义事件可空 | `BusinessEvent.store_customer_id``StoreCustomer.id` | | `StoreCustomer` 1—N `BusinessEvent` | 客户时间线;无客户语义事件可空 | `BusinessEvent.store_customer_id``StoreCustomer.id` |
| `Store` 1—N `StaffInvitation` | 一个门店可签发多次限时邀请 | `StaffInvitation.store_id``Store.id` | | `Store` 1—N `StaffInvitation` | 一个门店可签发多次限时邀请 | `StaffInvitation.store_id``Store.id` |
@ -35,6 +38,12 @@
| `action:confirm_report_sent` | `event:report_sent`(仅首次确认) | | `action:confirm_report_sent` | `event:report_sent`(仅首次确认) |
| `action:track_report_open` | `event:report_opened` / `event:report_reopened` | | `action:track_report_open` | `event:report_opened` / `event:report_reopened` |
| `action:submit_lead` | `event:lead_submitted` | | `action:submit_lead` | `event:lead_submitted` |
| `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` | | `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-002` | `action:list_leads` |
| `rule:BR-LEAD-003` | `action:list_leads` | | `rule:BR-LEAD-003` | `action:list_leads` |
| `rule:BR-BE-001` | 所有已落库事件触发动作、`action:view_workbench` | | `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` | Report Media Backend |
| `entity:report_lead` | Report Media Backend | | `entity:report_lead` | Report Media Backend |
| `entity:business_event` | Backend Core跨域写入底座 | | `entity:business_event` | Backend Core跨域写入底座 |
| `entity:follow_up_task` | Backend Core + Store Admin FE |
--- ---
@ -162,7 +174,7 @@
|------|------| |------|------|
| `admin_console` HAS `workbench` | 登录默认工作台 | | `admin_console` HAS `workbench` | 登录默认工作台 |
| `workbench` HAS `workbench_todo_item` | 按优先级排序的行动组(准确总数 + 低敏预览) | | `workbench` HAS `workbench_todo_item` | 按优先级排序的行动组(准确总数 + 低敏预览) |
| `workbench` AGGREGATES `appointment` / `report` / `report_lead` | 指标与异常来源 | | `workbench` AGGREGATES `appointment` / `report` / `follow_up_task` | 指标与异常来源 |
| `store` HAS `store_customer` | 一店多个稳定客户关系 | | `store` HAS `store_customer` | 一店多个稳定客户关系 |
| `store_customer` LINKS `user` | 可选关联全局 customer 账号;留资客户可先不关联 | | `store_customer` LINKS `user` | 可选关联全局 customer 账号;留资客户可先不关联 |
| `store_customer` HAS `pet` | 通过 customer user 关联一人多宠 | | `store_customer` HAS `pet` | 通过 customer user 关联一人多宠 |
@ -174,3 +186,4 @@
| `store_customer` HAS_VIEW `customer_timeline` | 一个稳定门店客户主档对应一个动态事实视图 | | `store_customer` HAS_VIEW `customer_timeline` | 一个稳定门店客户主档对应一个动态事实视图 |
| `business_event` PROJECTS_TO `customer_timeline` | 多条不可变事实经低敏白名单投影为时间线 | | `business_event` PROJECTS_TO `customer_timeline` | 多条不可变事实经低敏白名单投影为时间线 |
| `view_store_customer_timeline` READS `store_customer` / `business_event` | 同店校验后分页读取主档与事实 | | `view_store_customer_timeline` READS `store_customer` / `business_event` | 同店校验后分页读取主档与事实 |
| `admin_list_leads` READS `follow_up_task` | `/leads` 导航承载真实操作任务池,不直接编辑留资事实 |

View File

@ -193,6 +193,24 @@
- **代码**`backend/src/main/java/com/petstore/controller/ReportLeadController.java#leads` - **代码**`backend/src/main/java/com/petstore/controller/ReportLeadController.java#leads`
- **证据**`anchored` - **证据**`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 录入规则 ## Batch 2 录入规则
@ -354,7 +372,7 @@
### rule:BR-CONFIG-003 ### 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` - **代码**`backend/src/main/java/com/petstore/config/ProductionDatabasePreflightRunner.java`、`PetstoreRuntimeHealthIndicator.java`、`backend/deploy/release-preflight.sh`、`production-smoke.sh`

View File

@ -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 | PASS217 tests0 failures0 errors0 skipped |
| Admin production build | PASSVue type-check + Vite1671 modules transformed |
| Ontology validation | PASS290 entries引用完整 |
| Endpoint drift | PASS70/70 HTTPcode-only=0ontology-only=0 |
| Entity drift | PASS16/16 JPAmissing=0extra=0 |
| Diff whitespace | PASSbackend/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尤其是双员工抢领、退订与无号源再次预约。
- 未在 35 家试点门店验证任务领取习惯、接通率、再次预约率和约 200 单完整服务样本。
上述项目进入试点 RC 门禁,不因本地自动化通过而标记为上线完成。

View File

@ -2,8 +2,8 @@
> **本体对齐**:本文档与 [`ontology/objects.md`](./ontology/objects.md) 对齐(`entity:store`/`entity:user`/`entity:appointment`/`entity:report`),规则见 [`ontology/rules.md`](./ontology/rules.md)。冲突以本体为准。 > **本体对齐**:本文档与 [`ontology/objects.md`](./ontology/objects.md) 对齐(`entity:store`/`entity:user`/`entity:appointment`/`entity:report`),规则见 [`ontology/rules.md`](./ontology/rules.md)。冲突以本体为准。
> >
> 版本v2.0 > 版本v2.1
> 日期2026-08-02v2.0面向 35 家独立单店真实试点,补齐门店开通、一次性员工邀请与审计边界 > 日期2026-08-02v2.1补齐可执行回访任务状态机与真实再次预约归因
> **品牌**:宠小它 · **主 Slogan**:用心宠小它,温暖伴一生 · **副 Slogan**:智慧宠物门店服务系统 > **品牌**:宠小它 · **主 Slogan**:用心宠小它,温暖伴一生 · **副 Slogan**:智慧宠物门店服务系统
> **Logo / VI**[《宠小它-Logo与品牌识别说明》](./宠小它-Logo与品牌识别说明.md)(含概念草案图) > **Logo / VI**[《宠小它-Logo与品牌识别说明》](./宠小它-Logo与品牌识别说明.md)(含概念草案图)
@ -149,6 +149,22 @@
| metadata_json | TEXT | 服务端白名单低敏维度,不存原始请求 | | metadata_json | TEXT | 服务端白名单低敏维度,不存原始请求 |
| idempotency_key | VARCHAR(128) | 可选稳定幂等键 | | 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预约表 ### t_appointment预约表
| 字段 | 类型 | 说明 | | 字段 | 类型 | 说明 |
|------|------|------| |------|------|------|
@ -218,13 +234,14 @@
| 功能 | 老板boss | 员工staff | | 功能 | 老板boss | 员工staff |
|------|-------------|---------------| |------|-------------|---------------|
| 预约列表 | ✅ | ✅ | | 预约列表 | ✅ | ✅ |
| 新建预约 | ✅ | ✅ | | 新建预约 / 回访后代客预约 | ✅ | ✅ |
| 开始服务/填写报告 | ✅ | ✅ | | 开始服务/填写报告 | ✅ | ✅ |
| 发送报告 | ✅ | ✅ | | 发送报告 | ✅ | ✅ |
| 员工管理 | ✅ | ❌ | | 员工管理 | ✅ | ❌ |
| 邀请码 | ✅ | ❌ | | 邀请码 | ✅ | ❌ |
| 店铺设置 | ✅ | ❌ | | 店铺设置 | ✅ | ❌ |
| 服务类型管理 | ✅ | ❌ | | 服务类型管理 | ✅ | ❌ |
| 领取、改期、关闭本店回访任务 | ✅ | ✅ |
### 页面入口 ### 页面入口
- **index.html统一入口**:老板/员工同一套页面,通过「我的」页面区分功能 - **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** | **洗美过程短视频自动成片(可变时长、双编排、模板拼接)** | **🔨 本期迭代重点** |
| P0 | 老板后台(开通清单+员工邀请中心) | ✅ Admin 与小程序均已接入 | | P0 | 老板后台(开通清单+员工邀请中心) | ✅ Admin 与小程序均已接入 |
| P0 | 服务客户事实时间线Admin | ✅ 代码完成,含 StoreCustomer 合并别名历史连续性,待生产迁移 | | P0 | 服务客户事实时间线Admin | ✅ 代码完成,含 StoreCustomer 合并别名历史连续性,待生产迁移 |
| P0 | 可执行回访任务池 + 真实再次预约归因Admin | ✅ 代码完成,待生产迁移与三角色验收 |
| P0 | 老板后台(预约概览) | ✅ 已合并到index.html | | P0 | 老板后台(预约概览) | ✅ 已合并到index.html |
| P1 | 服务类型管理(老板后台) | ✅ | | P1 | 服务类型管理(老板后台) | ✅ |
| P1 | 店铺设置(老板后台) | ✅ | | P1 | 店铺设置(老板后台) | ✅ |
@ -462,6 +488,9 @@ CREATE TABLE t_report_image (
INDEX idx_report_image_report_id (report_id), INDEX idx_report_image_report_id (report_id),
INDEX idx_report_image_report_type_sort (report_id, photo_type, sort_order) INDEX idx_report_image_report_type_sort (report_id, photo_type, sort_order)
); );
-- 回访任务完整建表、历史回填和五项验证见:
-- backend/db/migrations/20260802_create_follow_up_task.sql
``` ```
--- ---

View File

@ -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 --未接通/稍后联系--> pendingattempt_count + 1更新 due_date
in_progress --暂无意向/联系方式无效--> completedattempt_count + 1
in_progress --真实预约创建成功--> completed / rebookedattempt_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。

View File

@ -15,14 +15,14 @@
- 按 `occurredAt DESC, id DESC` 对 canonical 与别名事件统一稳定分页;同一时刻用数据库事件 ID 决定顺序。 - 按 `occurredAt DESC, id DESC` 对 canonical 与别名事件统一稳定分页;同一时刻用数据库事件 ID 决定顺序。
- 展示预约创建/状态、开始/完成服务、报告提交/确认发送/首次打开/再次打开、回访意向等已经落库的事实。 - 展示预约创建/状态、开始/完成服务、报告提交/确认发送/首次打开/再次打开、回访意向等已经落库的事实。
- 批量补齐预约、报告、回访对象的当前低敏摘要和详情跳转 ID对象已删除时仍保留事件标题和发生时间不伪造摘要。 - 批量补齐预约、报告、留资和 FollowUpTask 的当前低敏摘要与详情跳转 ID对象已删除时仍保留事件标题和发生时间不伪造摘要。
- Admin 客户抽屉可跳到预约详情、报告列表和回访池;本批不创建回访任务或再次预约归因 - Admin 客户抽屉可跳到预约详情、报告列表和回访任务池;后续已按独立决策落地 FollowUpTask 与真实再次预约归因,时间线只读取其不可变事件,不承担任务状态写入
## 3. 隐私与数据边界 ## 3. 隐私与数据边界
允许返回:客户称呼、脱敏手机号、宠物名、服务类型、预约时间、当前业务状态、发送渠道、提醒日期、操作人显示名/角色。 允许返回:客户称呼、脱敏手机号、宠物名、服务类型、预约时间、当前业务状态、发送渠道、提醒日期、操作人显示名/角色。
禁止返回:手机号原文、预约/报告备注、图片/视频 URL、报告或邀请 token/hash、openid/unionid、IP、请求体和原始 `metadata_json`。服务端只解析 `fromStatus/toStatus/channel/repeatSubmit` 四个白名单 metadata 字段。 禁止返回:手机号原文、预约/报告备注、图片/视频 URL、报告或邀请 token/hash、openid/unionid、IP、请求体和原始 `metadata_json`。服务端只解析状态变化、发送渠道、重复提交、回访日期/结果/次数和预约来源等受控低敏 metadata 字段。
时间线不承载会员等级、余额、次卡、积分、营销标签或自由文本跟进记录。这些能力若进入后续阶段,必须使用独立对象与状态机,不得改写历史事实。 时间线不承载会员等级、余额、次卡、积分、营销标签或自由文本跟进记录。这些能力若进入后续阶段,必须使用独立对象与状态机,不得改写历史事实。

View File

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

View File

@ -58,9 +58,9 @@
- 服务时长/预约快照非法、容量超卖; - 服务时长/预约快照非法、容量超卖;
- 新报告已提交但长期未确认发送; - 新报告已提交但长期未确认发送;
- 已确认发送但长期未打开; - 已确认发送但长期未打开;
- 回访后没有明确状态或再次预约归因 - FollowUpTask 状态/领取人/终态结果不一致,或 `rebooked` 未指向同店有效预约及 `rebook_created` 事实
发布前的 `release-preflight.sh` 已覆盖前四类结构/数据不变量;运行期应每天做等价的只读审计或报表,不自动修复。 发布前的 `release-preflight.sh` 已覆盖上述可由数据库可靠验证的结构/数据不变量;运行期应每天做等价的只读审计或报表,不自动修复。
## 5. 日志与隐私 ## 5. 日志与隐私
@ -83,7 +83,7 @@
- [ ] 外部 HTTPS 探测覆盖 liveness/readiness、admin、H5 报告入口; - [ ] 外部 HTTPS 探测覆盖 liveness/readiness、admin、H5 报告入口;
- [ ] 5xx、延迟、DB 池、磁盘、证书和进程重启已配置告警; - [ ] 5xx、延迟、DB 池、磁盘、证书和进程重启已配置告警;
- [ ] FFmpeg/上传失败有可聚合字段,而非只能搜索自由文本; - [ ] FFmpeg/上传失败有可聚合字段,而非只能搜索自由文本;
- [ ] BusinessEvent 日漏斗和 26 项只读数据审计有每日结果; - [ ] BusinessEvent 日漏斗和 31 项只读数据审计有每日结果;
- [ ] 通知渠道、Primary/Backup 值班人和升级联系人完成真实测试; - [ ] 通知渠道、Primary/Backup 值班人和升级联系人完成真实测试;
- [ ] 日志脱敏抽查通过,访问权限和保留周期已批准; - [ ] 日志脱敏抽查通过,访问权限和保留周期已批准;
- [ ] 发布标记能关联四仓 commit 与 RC - [ ] 发布标记能关联四仓 commit 与 RC

View File

@ -20,11 +20,11 @@
| 模块 | 状态 | 主要缺口 | | 模块 | 状态 | 主要缺口 |
|------|------|----------| |------|------|----------|
| A0 壳层登录 | ~95% | 生产微信登录策略另议 | | A0 壳层登录 | ~95% | 生产微信登录策略另议 |
| A1 工作台 | ~99% | 服务端准确行动聚合、低敏预览、报告待发送行动与显式发送漏斗已做;仅转预约归因和真实环境 smoke 待补 | | A1 工作台 | ~99% | 服务端准确行动聚合、低敏预览、报告待发送与回访后真实再次预约漏斗已做;真实环境 smoke 待补 |
| A2 预约 | ~95% | 详情/筛选/待出报告/宠主脱敏已做 | | A2 预约 | ~95% | 详情/筛选/待出报告/宠主脱敏已做 |
| A6 排班 | ~90% | 与小程序对照验收 | | A6 排班 | ~90% | 与小程序对照验收 |
| A3 报告 | ~95% | 详情抽屉+失败文案+H5 | | A3 报告 | ~95% | 详情抽屉+失败文案+H5 |
| A4 线索 | ~90% | 到期筛+打开报告 | | A4 回访任务 | ~99% | 领取/改期/关闭/真实再次预约已做;真实环境三角色 smoke 待补 |
| A5 客户 | ~95% | 详情抽屉+跳转 | | A5 客户 | ~95% | 详情抽屉+跳转 |
| A7 设置 | ~90% | 坐标/logo 可选增强 | | A7 设置 | ~90% | 坐标/logo 可选增强 |
| T6 QA | 清单已出 | **待人工冒烟** | | T6 QA | 清单已出 | **待人工冒烟** |

View File

@ -48,7 +48,7 @@
⑤ 报告尚未发送 n 最近 3 条 …………………… [全部处理] ⑤ 报告尚未发送 n 最近 3 条 …………………… [全部处理]
⑥ 回访日期已到 n 最近 3 条 …………………… [全部处理] ⑥ 回访日期已到 n 最近 3 条 …………………… [全部处理]
报告漏斗 已提交 | 已确认发送 | 已打开 | 已留资 | 转预约— 报告漏斗 已提交 | 已确认发送 | 已打开 | 已留资 | 回访后预约
``` ```
**主操作**:点指标、行动组或具体低敏预览下钻;工作台常驻,不用先关弹窗。 **主操作**:点指标、行动组或具体低敏预览下钻;工作台常驻,不用先关弹窗。
@ -111,20 +111,21 @@
--- ---
## A4 回访线索 `/leads` ## A4 回访任务 `/leads`
**一句话**:把该回访的留资列出来,点开关联报告跟进 **一句话**:把留资转成可领取、可改期、可闭环且能真实发起再次预约的门店行动
```text ```text
[状态▾] [到期≤今天] [日期区间] [查询] [状态▾] [到期≤今天] [查询]
宠物 | 服务 | 手机(脱敏) | 建议日 | 状态 | 微信 | 操作 客户/手机 | 宠物/服务 | 计划日 | 状态/领取人 | 次数 | 操作
─────┼──────┼────────────┼────────┼──────┼──────┼──────── ──────────┼───────────┼────────┼─────────────┼──────┼────────────────
… | … | 138**** | 07-10 | 待回访| 是 | 打开报告 … | … | 07-10 | 待领取 | 0 | 领取
… | … | 07-10 | 我处理中 | 1 | 改期 关闭 再次预约
``` ```
**主操作**筛待回访;打开关联报告公开页。 **主操作**领取;登记未接通/稍后联系并改期;关闭本轮;选择真实号源并创建再次预约。
**不做**跟进备注 CRM、群发、发券 **不做**自由文本 CRM、群发、发券、手工标记 rebooked
--- ---

View File

@ -45,7 +45,7 @@ entity:admin_console
├── /appointments 预约中心 ├── /appointments 预约中心
├── /schedule 排班占用 ├── /schedule 排班占用
├── /reports 服务报告 ├── /reports 服务报告
├── /leads 回访线索 ├── /leads 回访任务
├── /customers 服务客户 ├── /customers 服务客户
└── /settings 门店设置(资料 / 服务类型 / 员工) └── /settings 门店设置(资料 / 服务类型 / 员工)
``` ```
@ -90,9 +90,9 @@ entity:admin_console
```text ```text
顶栏 4 指标卡 顶栏 4 指标卡
常驻行动队列(不再每日强弹) 常驻行动队列(不再每日强弹)
报告漏斗四格(提交/打开/留资已由 BusinessEvent 汇总;转预约仍为「—」 报告漏斗(提交/发送/打开/留资/回访后再次预约均由 BusinessEvent 汇总
每组最多 3 条低敏预览 + 准确总数 + 列表下钻 每组最多 3 条低敏预览 + 准确总数 + 列表下钻
报告漏斗(提交 / 显式确认发送 / 打开 / 留资 / 再预约未知 报告漏斗(提交 / 显式确认发送 / 打开 / 留资 / 回访后真实预约)
``` ```
### 2.2 顶栏指标卡 ### 2.2 顶栏指标卡
@ -113,7 +113,7 @@ entity:admin_console
| `upcoming_today` | 今天接下来要到店 | 今日尚未到时刻的 `new` | 预约列表 `status=new&date=today` | | `upcoming_today` | 今天接下来要到店 | 今日尚未到时刻的 `new` | 预约列表 `status=new&date=today` |
| `highlight_anomaly` | 成片异常 | `failed` + `processing` 超过 30 分钟 | 报告列表 `highlightStatus=anomaly` | | `highlight_anomaly` | 成片异常 | `failed` + `processing` 超过 30 分钟 | 报告列表 `highlightStatus=anomaly` |
| `unsent_report` | 报告尚未发送 | 仅 `sendStatus=unsent`;历史 `unknown` 不进入待办 | 报告列表 `sendStatus=unsent` | | `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 或微信标识;准确总数由服务端按完整数据计算。工作台不再每日强弹,避免值班人员为了关弹窗而忽略真正行动。 优先级固定为:逾期预约 → 服务中 → 今日待开始 → 成片异常 → 报告待发送 → 到期回访。每组只返回最多 3 条预览,预览不含手机号、报告 token、媒体 URL 或微信标识;准确总数由服务端按完整数据计算。工作台不再每日强弹,避免值班人员为了关弹窗而忽略真正行动。
@ -127,7 +127,7 @@ entity:admin_console
| 已确认发送 | `report_sent` 按 report 去重 | 仅员工显式确认事实;复制/二维码/预览不计入 | | 已确认发送 | `report_sent` 按 report 去重 | 仅员工显式确认事实;复制/二维码/预览不计入 |
| 已打开 | `report_opened/reopened` 按 report 去重 | BusinessEvent 汇总 | | 已打开 | `report_opened/reopened` 按 report 去重 | BusinessEvent 汇总 |
| 已留资 | `lead_submitted` 按 lead 去重 | BusinessEvent 汇总 | | 已留资 | `lead_submitted` 按 lead 去重 | BusinessEvent 汇总 |
| 报告转预约 | `rebook_created` | 尚无可靠归因,显示「—」 | | 回访后再次预约 | `rebook_created` 按预约去重 | 仅真实预约创建成功后统计 |
### 2.5 API / 聚合 ### 2.5 API / 聚合
@ -273,32 +273,40 @@ entity:admin_console
--- ---
## 6. 回访线索 ## 6. 回访任务
**页面 ID**`A4-leads` **页面 ID**`A4-leads`
**路由**`/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 筛选 ### 6.1 筛选
`remindStatus`pending / 全部)、到期(≤今天)、日期区间 `status`open / pending / in_progress / completed / canceled / all、到期≤今天、分页
### 6.2 列表列 ### 6.2 列表列
宠物名、服务、脱敏手机、建议日期、状态、微信绑定(是/否 + 脱敏 openid 若有)、操作:查看关联报告公开页 客户、联系电话、宠物名、服务、计划回访日、状态、领取人、联系次数、最近结果。完整手机号只在本店已认证员工的开放任务中返回,用于实际电话联系;终态任务、工作台与客户时间线均脱敏。
操作:待领取任务可领取;本人处理中任务可登记未接通/稍后联系并改期、以暂无意向/联系方式无效关闭,或进入代客预约并读取真实号源。`rebooked` 不提供手工结果。
### 6.3 不做A 期) ### 6.3 不做A 期)
跟进备注 CRM、短信群发、改等级、发券 自由文本跟进备注 CRM、短信群发、改等级、发券、手工标记再次预约
### 6.4 API 复用 ### 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 验收 ### 6.5 验收
- [ ] 无完整 openid/unionid - [ ] 无 openid/unionid/token/备注;事件、审计和时间线无完整手机号
- [ ] 跨店 403 - [ ] 跨店任务按 404customer 禁止
- [ ] 他人已领取返回冲突;非领取人不能改期/关闭/再次预约
- [ ] 无号源或预约失败时任务不闭环;成功时生成真实 appointment 与 `rebook_created`
- [ ] 工作台「待回访」下钻数字一致 - [ ] 工作台「待回访」下钻数字一致
--- ---
@ -416,7 +424,7 @@ API`user/staff-list`、`create-staff`、删除接口
| A2 预约 | `admin_list_appointments` | `appointment` | `/api/appointment/*` | | A2 预约 | `admin_list_appointments` | `appointment` | `/api/appointment/*` |
| A6 排班 | `admin_manage_schedule` | `schedule_block` | `/api/schedule/*` | | A6 排班 | `admin_manage_schedule` | `schedule_block` | `/api/schedule/*` |
| A3 报告 | `admin_list_reports` | `report` / `highlight_video` | `/api/report/*` | | 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` | | A5 客户 | `admin_list_service_customers` | `store_customer` | **已实现** `GET /api/admin/service-customers` |
| A7 设置 | `admin_update_settings` | `store` / `service_type` / `user` | store/service-type/user | | A7 设置 | `admin_update_settings` | `store` / `service_type` / `user` | store/service-type/user |

View File

@ -86,7 +86,7 @@
├── 预约与排班 # 日/周视图、号源、手动占用、改派/取消 ├── 预约与排班 # 日/周视图、号源、手动占用、改派/取消
├── 服务报告 # 列表、详情、成片状态、重新生成、分享链接管理 ├── 服务报告 # 列表、详情、成片状态、重新生成、分享链接管理
├── 客户与宠物 # 宠主/宠物档案(服务过的)、历史报告入口 ├── 客户与宠物 # 宠主/宠物档案(服务过的)、历史报告入口
├── 回访线索 # Leads 池、提醒日期、退订状态、跟进备注(可后补) ├── 回访任务 # 可领取、改期、关闭、真实再次预约;自由文本备注后补
├── 经营数据 # 打开率、成片成功率、留资转化、预约完成率(依赖埋点落地) ├── 经营数据 # 打开率、成片成功率、留资转化、预约完成率(依赖埋点落地)
├── 门店设置 # 资料、营业时段、服务类型、员工与邀请码 ├── 门店设置 # 资料、营业时段、服务类型、员工与邀请码
└──(后期)会员与资产 # 次卡/储值/套餐 — 对标宠老板会员,但不做库存 └──(后期)会员与资产 # 次卡/储值/套餐 — 对标宠老板会员,但不做库存
@ -108,7 +108,7 @@ Web 后台主攻:**经营、检索、批量、报表、设置****允许「
| A1 工作台 | 登录落点;「今日待办」弹层(预约/待报告/成片异常/待回访)+ 顶栏 4 指标卡;卡片下钻列表 | 现有 API 聚合 | | A1 工作台 | 登录落点;「今日待办」弹层(预约/待报告/成片异常/待回访)+ 顶栏 4 指标卡;卡片下钻列表 | 现有 API 聚合 |
| A2 预约中心 | 列表筛选(状态/日期/技师)、详情、取消/开始(权限同现网) | Appointment* | | A2 预约中心 | 列表筛选(状态/日期/技师)、详情、取消/开始(权限同现网) | Appointment* |
| A3 报告中心 | 报告列表、公开链接复制、成片三态、触发重生成 | Report* / Highlight | | A3 报告中心 | 报告列表、公开链接复制、成片三态、触发重生成 | Report* / Highlight |
| A4 线索中心 | 回访池、脱敏展示、建议日期 | ReportLead* | | A4 回访任务 | FollowUpTask 领取/改期/关闭/真实再次预约 | ReportLead + FollowUpTask |
| A5 设置中心 | 门店资料、营业时段、服务类型、员工 | Store/User/ServiceType | | A5 设置中心 | 门店资料、营业时段、服务类型、员工 | Store/User/ServiceType |
| A6 排班 | 日视图 + 手动占用 | Schedule* | | A6 排班 | 日视图 + 手动占用 | Schedule* |
@ -443,7 +443,7 @@ Web 后台主攻:**经营、检索、批量、报表、设置****允许「
└──────────────────┴──────────────────────────┴──────────────────┘ └──────────────────┴──────────────────────────┴──────────────────┘
``` ```
**报告漏斗**(替代微信商城状态):`今日已提交报告` · `已确认发送` · `已打开` · `已留资` · `从报告发起的预约`——前四项已由 BusinessEvent 汇总;发送只认员工显式确认,不以提交、复制链接或展示二维码冒充;仅转预约归因仍显示「—」 **报告漏斗**(替代微信商城状态):`今日已提交报告` · `已确认发送` · `已打开` · `已留资` · `回访后真实再次预约`——均由 BusinessEvent 汇总;发送只认员工显式确认,不以提交、复制链接或展示二维码冒充;再次预约只认携带 FollowUpTask 且真实保存成功的 Appointment
### 11.7 截图覆盖度与仍缺清单2026-07-10 刷新) ### 11.7 截图覆盖度与仍缺清单2026-07-10 刷新)