From 554063b40c6ca2a0a10404cf9d3161ecd09fb7ba Mon Sep 17 00:00:00 2001 From: malei <> Date: Sat, 1 Aug 2026 22:29:20 +0800 Subject: [PATCH] docs: define persisted business events --- .agents/common-context.md | 2 +- P0-研发落地清单.md | 5 +- coverage/ontology-coverage-audit.md | 33 ++++++----- graph/ontology.jsonl | 37 +++++++----- ontology/actions.md | 21 ++++--- ontology/events.md | 54 +++++++++++++----- ontology/objects.md | 35 ++++++++++-- ontology/relations.md | 8 ++- ontology/rules.md | 9 +++ 产品设计文档.md | 13 +++++ 本期主线-下一步产品优化点.md | 10 ++-- 架构决策-业务事件落库-2026-08-01.md | 84 ++++++++++++++++++++++++++++ 门店管理后台-PhaseA-交付目标.md | 2 +- 门店管理后台-PhaseA-线框说明.md | 2 +- 门店管理后台-PhaseA-编码任务brief.md | 10 ++-- 门店管理后台-PhaseA-页面清单PRD.md | 16 +++--- 门店管理后台升级方案-对标宠老板.md | 2 +- 17 files changed, 260 insertions(+), 83 deletions(-) create mode 100644 架构决策-业务事件落库-2026-08-01.md diff --git a/.agents/common-context.md b/.agents/common-context.md index 6d766f4..77cd953 100644 --- a/.agents/common-context.md +++ b/.agents/common-context.md @@ -45,7 +45,7 @@ 项目领域知识以本体论框架建模为 SSOT,位于 `docs/ontology/`: -- `objects.md` — 实体(`store_customer` 已落为 JPA `StoreCustomer`;`admin_console`/`workbench` 为后台读模型;P1/P2 gap 占位见 membership 等) +- `objects.md` — 实体(`store_customer`、`business_event` 已落为 JPA;`admin_console`/`workbench` 为后台读模型;P1/P2 gap 占位见 membership 等) - `actions.md` — 动作(controller 接口 + 客户端埋点 + **admin_* 后台动作 documented**) - `events.md` — 事件(含 `workbench_viewed` gap) - `rules.md` — 规则(含 `rule:BR-ADMIN-001`…`004` 后台门禁与范围闸门) diff --git a/P0-研发落地清单.md b/P0-研发落地清单.md index ee3fc0d..0735a84 100644 --- a/P0-研发落地清单.md +++ b/P0-研发落地清单.md @@ -59,6 +59,7 @@ | **A12** | **最小测试基线**:补 `SessionTokenServiceTest`、`ReportServiceTest`、`ReportControllerTest`、`AppointmentServiceTest` | `mvn test` 不再输出 `No tests to run`,新增用例全部通过 | Core Flow QA / System Architect | ✅ 已实现(Batch D) | `mvn -f backend/pom.xml test` | 对应 P0-10;持续回归数量以 Surefire 当次统计为准 | | **A13** | **预约/报告身份语义拆分**:客户、创建人、服务技师、报告作者不得复用同一语义字段 | customer 自约身份从 session 派生;门店代约必须提供真实宠主;宠主报告按预约客户归属查询;legacy 字段只做兼容 | System Architect / Backend Core / Report Media Backend | ✅ 已实现(Phase 0 RC1 后) | `mvn -f backend/pom.xml test` + H5/小程序构建 | 迁移见 `backend/db/migrations/20260801_split_service_identity.sql` | | **A14** | **StoreCustomer 门店客户主档**:客户列表不再临时拼成无 ID 投影 | 预约/留资自动建档;同店账号与手机号唯一;API 返回 storeCustomerId 且手机脱敏;历史数据可回填 | Backend Core / Report Media Backend / Data Model | ✅ 已实现(待生产迁移) | `mvn -f backend/pom.xml test` + `npm --prefix admin run build` | 迁移见 `backend/db/migrations/20260801_create_store_customer.sql` | +| **A15** | **BusinessEvent 业务事件落库**:经营漏斗不再依赖日志或前端猜测 | 首批 7 类事实统一落库;稳定事件幂等;metadata 不含 token/手机号/IP;工作台已打开可统计;历史可靠事实可回填 | Backend Core / Report Media Backend / Data Model | ✅ 已实现(待生产迁移) | `mvn -f backend/pom.xml test` + `npm --prefix admin run build` + 本体漂移审计 | 迁移见 `backend/db/migrations/20260801_create_business_event.sql` | --- @@ -102,8 +103,8 @@ | ID | 需求 | 验收标准 | Owner | 状态 | 验收命令 | 备注 | |----|------|----------|-------|------|----------|------| | **E1** | **分享卡片元信息规则** | 小程序 `onShareAppMessage`:标题「宠物名 + 服务 + 门店」;封面优先级 成片封面 > 服务后首图 > Logo | Customer Experience FE | ✅ 已实现(`reportShare.js`) | 手工 smoke + 代码审查 | 见 `frontend/src/utils/reportShare.js` | -| **E2** | **埋点:报告打开** | 上报:`report_token` 匿名 hash、首次/再次、时间戳;落地页 H5 亦要能报 | Report Share QA / Backend Ops | 🟡 部分实现:日志已有,缺事件 schema 与统计落地 | 见《本期主线》埋点 schema 表 | 对应 P0-10、P1-4 | -| **E3** | **埋点事件 schema**(最小集) | 定义 `report_open`、`report_reopen`、`highlight_success`、`highlight_failed`、`video_play`、`video_save`、`lead_submit` 字段、来源、统计窗口 | Report Share QA / Product Design | ✅ 已冻结(Batch A 文档) | 见《本期主线-下一步产品优化点》埋点 schema 表 | 对应 P1-4 | +| **E2** | **埋点:报告打开** | 前端提交 token 定位报告;事件表仅存 report ID、首次/再次和时间,不存 token/hash;H5/小程序均上报 | Report Share QA / Backend Ops | ✅ 已落 BusinessEvent 并进入工作台去重统计(待生产迁移) | `BusinessEventServiceTest` + admin build | 对应 P0-10、P1-4 | +| **E3** | **埋点事件 schema**(最小集) | 定义 `report_opened`、`report_reopened`、`highlight_succeeded`、`highlight_failed`、`video_play`、`video_save`、`lead_submitted` 字段、来源、统计窗口 | Report Share QA / Product Design | ✅ 已冻结;核心履约/打开/留资已落库,成片与客户端播放事件待接入 | 见《本期主线-下一步产品优化点》埋点 schema 表 | 对应 P1-4 | --- diff --git a/coverage/ontology-coverage-audit.md b/coverage/ontology-coverage-audit.md index 5f65ead..52655be 100644 --- a/coverage/ontology-coverage-audit.md +++ b/coverage/ontology-coverage-audit.md @@ -3,7 +3,7 @@ > 本体条目 ↔ 文档章节 ↔ 代码路径 ↔ 测试用例 三方对齐审计。 > 证据强度:`anchored`(三处对齐)/ `documented`(仅文档)/ `implemented`(仅代码)/ `gap`(缺失)。 > -> **刷新时间**:2026-07-10(Batch 补全:domain 回填、敏感字段规则、寄语/退订/周期规则、客户端埋点 Action、P1/P2 gap 占位) +> **刷新时间**:2026-08-01(BusinessEvent 落库、工作台事件漏斗、StoreCustomer 主档与身份语义收口后刷新) ## 审计方法 @@ -18,14 +18,14 @@ | 类型 | 数量 | |------|------| -| Entity | 20 | -| Action | 56 | -| Event | 23 | -| Rule | 34 | -| Relation | 56 | -| **合计** | **189** | +| Entity | 25 | +| Action | 64 | +| Event | 26 | +| Rule | 40 | +| Relation | 81 | +| **合计** | **236** | -证据强度:`anchored` 158(83.6%)/ `documented` 18 / `gap` 11 / `implemented` 2。 +证据强度:`anchored` 185(78.4%)/ `documented` 37 / `gap` 12 / `implemented` 2。 > anchored 占比相对 Batch 2 下降,是因为主动录入了 P1/P2 `gap` 占位与客户端埋点 `documented` 条目;端点漂移仍为 0。 @@ -36,7 +36,7 @@ python3 docs/graph/validate_ontology.py docs python3 docs/graph/audit_drift.py ``` -当前结果:校验通过;HTTP 端点 52/52、JPA Entity 11/11 对齐;非 JPA 领域实体 9 个(含 HighlightVideo、SessionToken、CurrentUser、HighlightFailReason 与 5 个 roadmap gap)。 +当前结果:校验通过;HTTP 端点 55/55、JPA Entity 13/13 对齐;非 JPA 领域实体 12 个。 ## 元数据卫生 @@ -64,11 +64,13 @@ python3 docs/graph/audit_drift.py | session_token | ✅ | ✅ | SessionTokenServiceTest | anchored | jpa:false | | report_testimonial | ✅ | ✅ | ReportTestimonialServiceTest | anchored | | | service_interval | ✅ | ✅ | 间接 ReportLeadControllerTest | anchored | | +| store_customer | ✅ | ✅ | StoreCustomerServiceTest / IdentityOwnershipQueryTest | anchored | 稳定门店客户关系 | +| business_event | ✅ | ✅ | BusinessEventServiceTest / MigrationTest | anchored | 不可变低敏事件;工作台去重统计 | | highlight_fail_reason | ✅ | ✅ 枚举 | ReportHighlightVideoServiceTest | anchored | jpa:false | | current_user | ✅ | ✅ record | SessionTokenServiceTest | anchored | jpa:false | | membership 等 5 个 | 方向库 | — | — | gap | roadmap 占位 | -## 规则覆盖度(34 条) +## 规则覆盖度(40 条) | 规则 | 测试 | 证据 | |------|------|------| @@ -88,6 +90,7 @@ python3 docs/graph/audit_drift.py | BR-CONFIG-001/002 | —(配置) | anchored | | BR-TST-001 一报告一寄语 | ReportTestimonialServiceTest | anchored | | BR-UNSUB-001 / BR-INT-001 | ReportLeadControllerTest | anchored | +| BR-SC-001 / BR-BE-001 | StoreCustomerServiceTest / BusinessEventServiceTest / MigrationTest | anchored | ## 待补 gap 清单 @@ -100,7 +103,7 @@ python3 docs/graph/audit_drift.py | ~~高~~ | ~~寄语幂等独立用例~~ | ✅ `ReportTestimonialServiceTest` | | ~~中~~ | ~~`rule:BR-STORE-003` 公开 Store 字段裁剪~~ | ✅ `toPublicStoreView` | | 中 | `uk_report_appointment` 上线迁移 | 🟡 运维 | -| 中 | 埋点事件可统计落地 | 🟡 P1 | +| ~~中~~ | ~~预约/履约/报告打开/留资事件可统计落地~~ | ✅ BusinessEvent;成片/播放/保存事件另行接入 | | 中 | production profile 实测启动 | 🟡 | | 低 | 成片真 FFmpeg 端到端集成测(本机有 ffmpeg) | 🟡 单测已覆盖状态机与归类 | | 低 | `action:logout` / `event:session_revoked` 服务端吊销 | 🟡 gap | @@ -114,14 +117,14 @@ python3 docs/graph/audit_drift.py 本期不实现,仅 `evidence=gap` + `jpa=false`,避免本体膨胀成未实现产品说明书。受 `rule:BR-ADMIN-004` 约束,**不得写入 Phase A 门店后台设计**。 -## 门店 Web 后台 Phase A 预录入(2026-07-10) +## 门店 Web 后台 Phase A 状态(2026-08-01) -> 产品:`docs/门店管理后台升级方案-对标宠老板.md`。全部 `documented` / `gap`,**尚未编码**。 +> 产品:`docs/门店管理后台升级方案-对标宠老板.md`。后台工程已建立;各对象按当前 anchored/documented/gap 证据维护。 | ID | 类型 | 用途 | |----|------|------| | `entity:admin_console` | Entity | Web 管理端壳 | -| `entity:workbench` / `workbench_todo_item` | Entity | 今日工作台与待办 | +| `entity:workbench` / `workbench_todo_item` | Entity | 今日工作台与待办;报告事件漏斗已 anchored | | `entity:store_customer` | Entity | 门店客户稳定主档(非会员卡) | | `action:view_workbench` 等 8 个 admin_* | Action | 工作台/列表/设置/排班 | | `rule:BR-ADMIN-001`…`004` | Rule | 角色、店范围、内部字段、范围闸门 | @@ -131,4 +134,4 @@ python3 docs/graph/audit_drift.py **页面清单 PRD(已写)**:`docs/门店管理后台-PhaseA-页面清单PRD.md`(A0–A7 全页字段/筛选/API/验收)。 -当前 validate 规模以脚本输出为准(约 222 条目)。 +当前 validate 规模以脚本输出为准(236 条目)。 diff --git a/graph/ontology.jsonl b/graph/ontology.jsonl index 4fe9139..4b44d4b 100644 --- a/graph/ontology.jsonl +++ b/graph/ontology.jsonl @@ -3,6 +3,7 @@ {"id":"entity:appointment","type":"Entity","name":"Appointment","nameZh":"预约","domain":"appointment","fields":["id","pet_name","pet_type","service_type","appointment_time","status","store_id","user_id","customer_user_id","created_by_user_id","assigned_user_id","pet_id","remark","create_time","update_time","deleted"],"systems":["Backend Core"],"code":"backend/src/main/java/com/petstore/entity/Appointment.java; backend/src/main/java/com/petstore/service/AppointmentService.java","doc":"docs/产品设计文档.md §三 t_appointment §5.2","test":"AppointmentServiceTest; AppointmentControllerDetailTest; AppointmentControllerIdentityTest","evidence":"anchored"} {"id":"entity:report","type":"Entity","name":"Report","nameZh":"服务报告","domain":"report","fields":["id","appointment_id","remark","user_id","customer_user_id","author_staff_id","store_id","report_token","pet_name","service_type","appointment_time","staff_name","highlight_video_url","highlight_video_status","highlight_video_error","highlight_duration_sec","highlight_compose_mode","highlight_fail_reason","highlight_share_cover_url","create_time","update_time","deleted"],"systems":["Report Media Backend"],"code":"backend/src/main/java/com/petstore/entity/Report.java; backend/src/main/java/com/petstore/service/ReportService.java","doc":"docs/产品设计文档.md §三 t_report §5.3; docs/API-报告成片字段契约.md","test":"ReportServiceTest; ReportControllerTest","evidence":"anchored"} {"id":"entity:report_lead","type":"Entity","name":"ReportLead","nameZh":"留资线索","domain":"lead","fields":["id","report_id","store_id","phone","pet_name","service_type","wechat_openid","wechat_unionid","reminder_type","remind_date","remind_status","unsubscribe_token","client_ip","create_time","update_time","deleted"],"systems":["Report Media Backend"],"code":"backend/src/main/java/com/petstore/controller/ReportLeadController.java; backend/src/main/java/com/petstore/entity/ReportLead.java","doc":"docs/本期主线-下一步产品优化点.md §C 留资","test":"ReportLeadControllerTest","evidence":"anchored"} +{"id":"entity:business_event","type":"Entity","name":"BusinessEvent","nameZh":"不可变业务事件","domain":"platform","jpa":true,"fields":["id","event_id","event_type","event_version","store_id","store_customer_id","aggregate_type","aggregate_id","actor_user_id","actor_role","source","occurred_at","metadata_json","idempotency_key","create_time"],"systems":["Backend Core","Report Media Backend","Store Admin FE"],"code":"backend/src/main/java/com/petstore/entity/BusinessEvent.java; backend/src/main/java/com/petstore/service/BusinessEventService.java","doc":"docs/ontology/objects.md#entity:business_event; docs/架构决策-业务事件落库-2026-08-01.md","test":"BusinessEventServiceTest; BusinessEventMigrationTest","evidence":"anchored"} {"id":"action:register_boss","type":"Action","name":"registerBoss","nameZh":"老板入驻","inputs":["storeName","bossName","phone","password"],"outputs":["entity:store","entity:user","session_token"],"entrypoints":["POST /api/user/register-boss","UserService#registerBoss"],"appliesRules":["rule:BR-AUTH-001"],"code":"backend/src/main/java/com/petstore/service/UserService.java#registerBoss","evidence":"anchored","domain":"user"} {"id":"action:update_store","type":"Action","name":"updateStore","nameZh":"更新店铺","inputs":["entity:store"],"outputs":["entity:store"],"entrypoints":["PUT /api/store/update","StoreController#update","StoreService#update"],"appliesRules":["rule:BR-STORE-001"],"code":"backend/src/main/java/com/petstore/controller/StoreController.java#update","test":"StoreControllerTest","evidence":"anchored","domain":"store"} {"id":"action:delete_store","type":"Action","name":"deleteStore","nameZh":"删除店铺","inputs":["id"],"outputs":[],"entrypoints":["DELETE /api/store/delete","StoreController#delete"],"appliesRules":["rule:BR-STORE-001"],"code":"backend/src/main/java/com/petstore/controller/StoreController.java#delete","test":"StoreControllerTest","evidence":"anchored","domain":"store"} @@ -10,27 +11,29 @@ {"id":"action:login","type":"Action","name":"login","nameZh":"手机号验证码登录","inputs":["phone","code"],"outputs":["entity:user","entity:store","session_token"],"entrypoints":["POST /api/user/login","UserService#login"],"appliesRules":["rule:BR-AUTH-001","rule:BR-AUTH-002"],"code":"backend/src/main/java/com/petstore/service/UserService.java#login","test":"UserServiceLoginStrategyTest","evidence":"anchored","domain":"auth"} {"id":"action:wx_phone_login","type":"Action","name":"wxPhoneLogin","nameZh":"微信手机号授权登录","inputs":["phoneCode","loginCode"],"outputs":["entity:user","entity:store","session_token"],"entrypoints":["POST /api/user/wx-phone-login","UserController#wxPhoneLogin","UserService#loginByVerifiedPhone"],"appliesRules":["rule:BR-AUTH-001"],"code":"backend/src/main/java/com/petstore/controller/UserController.java#wxPhoneLogin","evidence":"anchored","domain":"auth"} {"id":"action:bind_wechat","type":"Action","name":"bindWechat","nameZh":"绑定微信标识","inputs":["userId","openid","unionid"],"outputs":["entity:user"],"entrypoints":["UserService#bindWechatMiniIdentity"],"appliesRules":[],"code":"backend/src/main/java/com/petstore/service/UserService.java#bindWechatMiniIdentity","evidence":"anchored","domain":"auth"} -{"id":"action:create_appointment","type":"Action","name":"createAppointment","nameZh":"创建预约","inputs":["petName","petType","serviceType","appointmentTime","storeId","remark","petId","customerUserId","customerPhone","customerName"],"outputs":["entity:appointment"],"entrypoints":["POST /api/appointment/create","AppointmentController#create","AppointmentService#createBooking"],"appliesRules":["rule:BR-APPT-001","rule:BR-APPT-003","rule:BR-APPT-004","rule:BR-SC-001"],"code":"backend/src/main/java/com/petstore/controller/AppointmentController.java#create; backend/src/main/java/com/petstore/service/StoreCustomerService.java#touchBooking","test":"AppointmentControllerIdentityTest; AppointmentServiceTest; StoreCustomerServiceTest","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"],"code":"backend/src/main/java/com/petstore/controller/AppointmentController.java#start","test":"AppointmentServiceTest","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"],"code":"backend/src/main/java/com/petstore/controller/AppointmentController.java#updateStatus","test":"AppointmentServiceTest","evidence":"anchored","domain":"appointment"} +{"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:start_service","type":"Action","name":"startService","nameZh":"开始服务","inputs":["appointmentId"],"outputs":["entity:appointment"],"entrypoints":["POST /api/appointment/start","AppointmentController#start","AppointmentService#startService"],"appliesRules":["rule:BR-APPT-001","rule:BR-APPT-002","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/controller/AppointmentController.java#start; backend/src/main/java/com/petstore/service/BusinessEventService.java#recordAppointmentStatusChanged","test":"AppointmentServiceTest; BusinessEventServiceTest","evidence":"anchored","domain":"appointment"} +{"id":"action:transition_appointment_status","type":"Action","name":"transitionAppointmentStatus","nameZh":"更新预约状态","inputs":["id","status"],"outputs":["entity:appointment"],"entrypoints":["PUT /api/appointment/status","AppointmentController#updateStatus","AppointmentService#transitionStatus"],"appliesRules":["rule:BR-APPT-001","rule:BR-APPT-002","rule:BR-APPT-003","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/controller/AppointmentController.java#updateStatus; backend/src/main/java/com/petstore/service/BusinessEventService.java#recordAppointmentStatusChanged","test":"AppointmentServiceTest; BusinessEventServiceTest","evidence":"anchored","domain":"appointment"} {"id":"action:get_appointment_detail","type":"Action","name":"getAppointmentDetail","nameZh":"预约详情","inputs":["id"],"outputs":["entity:appointment"],"entrypoints":["GET /api/appointment/detail","AppointmentController#detail"],"appliesRules":["rule:BR-APPT-002","rule:BR-APPT-003"],"code":"backend/src/main/java/com/petstore/controller/AppointmentController.java#detail","test":"AppointmentControllerDetailTest","evidence":"anchored","domain":"appointment"} {"id":"action:list_appointments","type":"Action","name":"listAppointments","nameZh":"预约列表","inputs":["status","page","pageSize"],"outputs":["entity:appointment"],"entrypoints":["GET /api/appointment/list","AppointmentController#list"],"appliesRules":["rule:BR-APPT-002","rule:BR-APPT-003"],"code":"backend/src/main/java/com/petstore/controller/AppointmentController.java#list","evidence":"anchored","domain":"appointment"} -{"id":"action:submit_report","type":"Action","name":"submitReport","nameZh":"提交报告","inputs":["appointmentId","remark","images"],"outputs":["reportToken","reportId"],"entrypoints":["POST /api/report/create","ReportController#create","ReportService#create"],"appliesRules":["rule:BR-RPT-001","rule:BR-RPT-002","rule:BR-RPT-003","rule:BR-RPT-004","rule:BR-RPT-005"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#create","test":"ReportControllerTest; ReportServiceTest","evidence":"anchored","domain":"report","note":"authorStaffId 从登录上下文派生,customerUserId 从预约派生"} +{"id":"action:submit_report","type":"Action","name":"submitReport","nameZh":"提交报告","inputs":["appointmentId","remark","images"],"outputs":["reportToken","reportId"],"entrypoints":["POST /api/report/create","ReportController#create","ReportService#create"],"appliesRules":["rule:BR-RPT-001","rule:BR-RPT-002","rule:BR-RPT-003","rule:BR-RPT-004","rule:BR-RPT-005","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#create; backend/src/main/java/com/petstore/service/ReportService.java#create; backend/src/main/java/com/petstore/service/BusinessEventService.java#recordReportSubmitted","test":"ReportControllerTest; ReportServiceTest; BusinessEventServiceTest","evidence":"anchored","domain":"report","note":"authorStaffId 从登录上下文派生,customerUserId 从预约派生;报告提交与事件同事务"} {"id":"action: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":[],"entrypoints":["POST /api/report/open-track","ReportController#trackReportOpen"],"appliesRules":["rule:BR-RPT-007"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#trackReportOpen","evidence":"anchored","domain":"report"} +{"id":"action:track_report_open","type":"Action","name":"trackReportOpen","nameZh":"报告打开埋点","inputs":["token","visitType"],"outputs":["entity:business_event"],"entrypoints":["POST /api/report/open-track","ReportController#trackReportOpen"],"appliesRules":["rule:BR-RPT-007","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#trackReportOpen; backend/src/main/java/com/petstore/service/BusinessEventService.java#recordReportOpenByToken","test":"ReportControllerTest; BusinessEventServiceTest","evidence":"anchored","domain":"report"} {"id":"action:delete_report","type":"Action","name":"deleteReport","nameZh":"删除报告","inputs":["id"],"outputs":[],"entrypoints":["DELETE /api/report/delete","ReportController#delete"],"appliesRules":["rule:BR-RPT-005"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#delete","evidence":"anchored","domain":"report"} -{"id":"action:submit_lead","type":"Action","name":"submitLead","nameZh":"宠主留资","inputs":["token","phone","consent","loginCode"],"outputs":["leadId","remindDate","unsubscribeToken","wechatBound"],"entrypoints":["POST /api/report/{token}/reminder","ReportLeadController#submitReminder","ReportLeadService#submit"],"appliesRules":["rule:BR-LEAD-001","rule:BR-SC-001"],"code":"backend/src/main/java/com/petstore/controller/ReportLeadController.java#submitReminder; backend/src/main/java/com/petstore/service/StoreCustomerService.java#touchLead","evidence":"anchored","test":"ReportLeadControllerTest; ReportLeadServiceStoreCustomerTest; StoreCustomerServiceTest","domain":"lead"} +{"id":"action:submit_lead","type":"Action","name":"submitLead","nameZh":"宠主留资","inputs":["token","phone","consent","loginCode"],"outputs":["leadId","remindDate","unsubscribeToken","wechatBound"],"entrypoints":["POST /api/report/{token}/reminder","ReportLeadController#submitReminder","ReportLeadService#submit"],"appliesRules":["rule:BR-LEAD-001","rule:BR-SC-001","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/controller/ReportLeadController.java#submitReminder; backend/src/main/java/com/petstore/service/ReportLeadService.java#submit; backend/src/main/java/com/petstore/service/StoreCustomerService.java#touchLead; backend/src/main/java/com/petstore/service/BusinessEventService.java#recordLeadSubmitted","evidence":"anchored","test":"ReportLeadControllerTest; ReportLeadServiceStoreCustomerTest; StoreCustomerServiceTest; BusinessEventServiceTest","domain":"lead"} {"id":"action:unsubscribe","type":"Action","name":"unsubscribe","nameZh":"一键退订","inputs":["unsubscribeToken"],"outputs":[],"entrypoints":["POST /api/report/unsubscribe","ReportLeadController#unsubscribe"],"appliesRules":["rule:BR-UNSUB-001"],"code":"backend/src/main/java/com/petstore/controller/ReportLeadController.java#unsubscribe","evidence":"anchored","domain":"lead","test":"ReportLeadControllerTest"} {"id":"action: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","inviteCode"],"emittedBy":["action:register_boss"],"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"],"code":"backend/src/main/java/com/petstore/auth/SessionTokenService.java#issue","test":"SessionTokenServiceTest","evidence":"anchored"} {"id":"event:wx_bound","type":"Event","name":"WxBound","nameZh":"微信标识绑定","domain":"auth","payload":["userId","openid","unionid"],"emittedBy":["action:bind_wechat"],"code":"backend/src/main/java/com/petstore/service/UserService.java#bindWechatMiniIdentity","evidence":"anchored"} -{"id":"event:appointment_created","type":"Event","name":"AppointmentCreated","nameZh":"预约创建","domain":"appointment","payload":["appointmentId","storeId","customerUserId","createdByUserId","appointmentTime"],"emittedBy":["action:create_appointment"],"code":"backend/src/main/java/com/petstore/service/AppointmentService.java#createBooking","evidence":"anchored"} -{"id":"event:appointment_status_changed","type":"Event","name":"AppointmentStatusChanged","nameZh":"预约状态迁移","domain":"appointment","payload":["appointmentId","fromStatus","toStatus","assignedUserId"],"emittedBy":["action:start_service","action:transition_appointment_status","action:submit_report"],"appliesRules":["rule:BR-APPT-001"],"code":"backend/src/main/java/com/petstore/service/AppointmentService.java#startService; #transitionStatus; ReportService#create","test":"AppointmentServiceTest; ReportServiceTest#createDoingAppointmentMarksDone","evidence":"anchored"} -{"id":"event:report_submitted","type":"Event","name":"ReportSubmitted","nameZh":"报告提交","domain":"report","payload":["reportId","appointmentId","storeId","reportToken"],"emittedBy":["action:submit_report"],"appliesRules":["rule:BR-RPT-001","rule:BR-RPT-002","rule:BR-RPT-004"],"code":"backend/src/main/java/com/petstore/service/ReportService.java#create","test":"ReportServiceTest","evidence":"anchored"} -{"id":"event:report_opened","type":"Event","name":"ReportOpened","nameZh":"报告首次打开","domain":"report","payload":["reportTokenHash","isFirstOpen","source","ts"],"emittedBy":["action:track_report_open"],"appliesRules":["rule:BR-RPT-007"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#trackReportOpen","doc":"docs/本期主线-下一步产品优化点.md 埋点事件 schema","evidence":"anchored"} -{"id":"event:report_reopened","type":"Event","name":"ReportReopened","nameZh":"报告再次打开","domain":"report","payload":["reportTokenHash","isFirstOpen","source","ts"],"emittedBy":["action:track_report_open"],"doc":"docs/本期主线-下一步产品优化点.md 埋点事件 schema","evidence":"documented"} -{"id":"event:lead_submitted","type":"Event","name":"LeadSubmitted","nameZh":"留资提交","domain":"lead","payload":["reportTokenHash","storeId","hasPhone","hasPetName","repeatSubmit","ts"],"emittedBy":["action:submit_lead"],"appliesRules":["rule:BR-LEAD-001"],"code":"backend/src/main/java/com/petstore/controller/ReportLeadController.java#submitReminder","doc":"docs/本期主线-下一步产品优化点.md 埋点事件 schema","evidence":"documented"} +{"id":"event:appointment_created","type":"Event","name":"AppointmentCreated","nameZh":"预约创建","domain":"appointment","payload":["storeId","storeCustomerId","aggregateId","actorUserId","actorRole","bookingOrigin","occurredAt"],"emittedBy":["action:create_appointment"],"appliesRules":["rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/service/AppointmentService.java#createBooking; backend/src/main/java/com/petstore/service/BusinessEventService.java#recordAppointmentCreated","test":"AppointmentServiceTest; BusinessEventServiceTest","evidence":"anchored"} +{"id":"event:appointment_status_changed","type":"Event","name":"AppointmentStatusChanged","nameZh":"预约状态迁移","domain":"appointment","payload":["storeId","storeCustomerId","aggregateId","fromStatus","toStatus","actorUserId","actorRole","occurredAt"],"emittedBy":["action:start_service","action:transition_appointment_status","action:submit_report"],"appliesRules":["rule:BR-APPT-001","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/service/AppointmentService.java#startService; #transitionStatus; backend/src/main/java/com/petstore/service/ReportService.java#create; backend/src/main/java/com/petstore/service/BusinessEventService.java#recordAppointmentStatusChanged","test":"AppointmentServiceTest; ReportServiceTest#createDoingAppointmentMarksDone; BusinessEventServiceTest","evidence":"anchored"} +{"id":"event:service_started","type":"Event","name":"ServiceStarted","nameZh":"服务开始","domain":"appointment","payload":["storeId","storeCustomerId","appointmentId","actorUserId","actorRole","occurredAt"],"emittedBy":["action:start_service"],"appliesRules":["rule:BR-APPT-001","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/service/BusinessEventService.java#recordAppointmentStatusChanged","test":"BusinessEventServiceTest#statusToDoingRecordsGenericAndServiceStartedFacts","evidence":"anchored"} +{"id":"event:service_completed","type":"Event","name":"ServiceCompleted","nameZh":"服务完成","domain":"appointment","payload":["storeId","storeCustomerId","appointmentId","actorUserId","actorRole","occurredAt"],"emittedBy":["action:transition_appointment_status","action:submit_report"],"appliesRules":["rule:BR-APPT-001","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/service/BusinessEventService.java#recordAppointmentStatusChanged; backend/src/main/java/com/petstore/service/ReportService.java#create","test":"AppointmentServiceTest; ReportServiceTest","evidence":"anchored"} +{"id":"event:report_submitted","type":"Event","name":"ReportSubmitted","nameZh":"报告提交","domain":"report","payload":["storeId","storeCustomerId","reportId","actorUserId","actorRole","occurredAt"],"emittedBy":["action:submit_report"],"appliesRules":["rule:BR-RPT-001","rule:BR-RPT-002","rule:BR-RPT-004","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/service/ReportService.java#create; backend/src/main/java/com/petstore/service/BusinessEventService.java#recordReportSubmitted","test":"ReportServiceTest; BusinessEventServiceTest","evidence":"anchored"} +{"id":"event:report_opened","type":"Event","name":"ReportOpened","nameZh":"报告首次打开","domain":"report","payload":["storeId","storeCustomerId","reportId","visitType","occurredAt"],"emittedBy":["action:track_report_open"],"appliesRules":["rule:BR-RPT-007","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#trackReportOpen; backend/src/main/java/com/petstore/service/BusinessEventService.java#recordReportOpenByToken","doc":"docs/本期主线-下一步产品优化点.md 埋点事件 schema","test":"ReportControllerTest; BusinessEventServiceTest","evidence":"anchored"} +{"id":"event:report_reopened","type":"Event","name":"ReportReopened","nameZh":"报告再次打开","domain":"report","payload":["storeId","storeCustomerId","reportId","visitType","occurredAt"],"emittedBy":["action:track_report_open"],"appliesRules":["rule:BR-RPT-007","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/service/BusinessEventService.java#recordReportOpenByToken","doc":"docs/本期主线-下一步产品优化点.md 埋点事件 schema","test":"BusinessEventServiceTest#reportOpenStoresReportIdButNeverToken","evidence":"anchored"} +{"id":"event:lead_submitted","type":"Event","name":"LeadSubmitted","nameZh":"留资提交","domain":"lead","payload":["storeId","storeCustomerId","leadId","repeatSubmit","occurredAt"],"emittedBy":["action:submit_lead"],"appliesRules":["rule:BR-LEAD-001","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/service/ReportLeadService.java#submit; backend/src/main/java/com/petstore/service/BusinessEventService.java#recordLeadSubmitted","doc":"docs/本期主线-下一步产品优化点.md 埋点事件 schema","test":"ReportLeadServiceStoreCustomerTest; BusinessEventMigrationTest","evidence":"anchored"} {"id":"rule:BR-AUTH-001","type":"Rule","name":"HMAC session token 鉴权","domain":"auth","appliesTo":["action:register_boss","action:login","action:wx_phone_login","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:get_report_by_token","action:track_report_open","action:delete_report","action:list_leads"],"code":"backend/src/main/java/com/petstore/auth/AuthInterceptor.java; SessionTokenService.java; CurrentUserContext.java","test":"SessionTokenServiceTest","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"} @@ -48,6 +51,7 @@ {"id":"rule:BR-RPT-007","type":"Rule","name":"token hash 日志","domain":"report","appliesTo":["action:track_report_open"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#shortTokenHash; #trackReportOpen","evidence":"anchored"} {"id":"rule:BR-LEAD-001","type":"Rule","name":"留资去重","domain":"lead","appliesTo":["action:submit_lead"],"code":"backend/src/main/java/com/petstore/service/ReportLeadService.java#submit","evidence":"anchored","test":"ReportLeadControllerTest#submitLeadRepeatSubmitReturnsAlreadySubmitted"} {"id":"rule:BR-SC-001","type":"Rule","name":"门店客户主档唯一性与合并","domain":"admin","appliesTo":["action:create_appointment","action:submit_lead","action:admin_list_service_customers"],"code":"backend/src/main/java/com/petstore/service/StoreCustomerService.java","doc":"docs/ontology/rules.md#rule:BR-SC-001; docs/架构决策-门店客户主档-2026-08-01.md","test":"StoreCustomerServiceTest; ReportLeadServiceStoreCustomerTest; IdentityOwnershipQueryTest","evidence":"anchored"} +{"id":"rule:BR-BE-001","type":"Rule","name":"业务事件不可变幂等与低敏","domain":"platform","appliesTo":["action:create_appointment","action:start_service","action:transition_appointment_status","action:submit_report","action:track_report_open","action:submit_lead","action:view_workbench"],"code":"backend/src/main/java/com/petstore/service/BusinessEventService.java; backend/src/main/java/com/petstore/service/AdminBusinessEventService.java","doc":"docs/ontology/rules.md#rule:BR-BE-001; docs/架构决策-业务事件落库-2026-08-01.md","test":"BusinessEventServiceTest; BusinessEventMigrationTest; AdminBusinessEventControllerTest","evidence":"anchored"} {"id":"rule:BR-LEAD-002","type":"Rule","name":"留资标识脱敏","domain":"lead","appliesTo":["action:list_leads"],"code":"backend/src/main/java/com/petstore/controller/ReportLeadController.java#maskIdentifier; #leads","frontend":"frontend/src/pages/mine/Leads.vue","evidence":"anchored"} {"id":"rule:BR-LEAD-003","type":"Rule","name":"回访池跨店 403","domain":"lead","appliesTo":["action:list_leads"],"code":"backend/src/main/java/com/petstore/controller/ReportLeadController.java#leads","evidence":"anchored"} {"id":"rel:store_has_users","type":"Relation","subject":"entity:store","predicate":"HAS","object":"entity:user","cardinality":"1—N","evidence":"anchored","doc":"User.store_id → Store.id","domain":"store"} @@ -70,9 +74,13 @@ {"id":"rel:wx_phone_login_emits_wx_bound","type":"Relation","subject":"action:wx_phone_login","predicate":"EMITS","object":"event:wx_bound","evidence":"anchored","domain":"auth"} {"id":"rel:create_appointment_emits_appointment_created","type":"Relation","subject":"action:create_appointment","predicate":"EMITS","object":"event:appointment_created","evidence":"anchored","domain":"appointment"} {"id":"rel:start_service_emits_status_changed","type":"Relation","subject":"action:start_service","predicate":"EMITS","object":"event:appointment_status_changed","evidence":"anchored","domain":"appointment"} +{"id":"rel:start_service_emits_service_started","type":"Relation","subject":"action:start_service","predicate":"EMITS","object":"event:service_started","evidence":"anchored","domain":"appointment"} {"id":"rel:transition_status_emits_status_changed","type":"Relation","subject":"action:transition_appointment_status","predicate":"EMITS","object":"event:appointment_status_changed","evidence":"anchored","domain":"appointment"} {"id":"rel:submit_report_emits_report_submitted","type":"Relation","subject":"action:submit_report","predicate":"EMITS","object":"event:report_submitted","evidence":"anchored","domain":"report"} {"id":"rel:submit_report_emits_status_changed","type":"Relation","subject":"action:submit_report","predicate":"EMITS","object":"event:appointment_status_changed","evidence":"anchored","doc":"doing→done","domain":"report"} +{"id":"rel:submit_report_emits_service_completed","type":"Relation","subject":"action:submit_report","predicate":"EMITS","object":"event:service_completed","evidence":"anchored","domain":"report"} +{"id":"rel:store_has_business_events","type":"Relation","subject":"entity:store","predicate":"HAS","object":"entity:business_event","evidence":"anchored","cardinality":"1—N","domain":"platform","doc":"BusinessEvent.store_id"} +{"id":"rel:store_customer_has_business_events","type":"Relation","subject":"entity:store_customer","predicate":"HAS","object":"entity:business_event","evidence":"anchored","cardinality":"1—N","domain":"platform","doc":"BusinessEvent.store_customer_id 可空"} {"id":"rel:track_report_open_emits_report_opened","type":"Relation","subject":"action:track_report_open","predicate":"EMITS","object":"event:report_opened","evidence":"anchored","domain":"report"} {"id":"rel:track_report_open_emits_report_reopened","type":"Relation","subject":"action:track_report_open","predicate":"EMITS","object":"event:report_reopened","evidence":"documented","domain":"report"} {"id":"rel:submit_lead_emits_lead_submitted","type":"Relation","subject":"action:submit_lead","predicate":"EMITS","object":"event:lead_submitted","evidence":"documented","domain":"lead"} @@ -192,14 +200,14 @@ {"id":"rel:br_int_001_applies","type":"Relation","subject":"rule:BR-INT-001","predicate":"APPLIES_TO","object":"action:get_report_suggestion","domain":"service_type","evidence":"anchored"} {"id":"rel:br_store_003_applies","type":"Relation","subject":"rule:BR-STORE-003","predicate":"APPLIES_TO","object":"action:get_store","domain":"store","evidence":"anchored"} {"id":"entity:admin_console","type":"Entity","name":"AdminConsole","nameZh":"门店 Web 管理后台","domain":"admin","jpa":false,"fields":["surface","navModules","storeScope","roleGate"],"systems":["Store Admin FE","Backend Core"],"code":"","doc":"docs/ontology/objects.md#entity:admin_console","test":"","evidence":"documented","note":"Phase A 产品预录入;独立 Web 端,与 uni-app 小程序并存。对标宠老板「店铺概况」入口,不做收银/库存。详见 docs/门店管理后台升级方案-对标宠老板.md","status":"planned"} -{"id":"entity:workbench","type":"Entity","name":"Workbench","nameZh":"今日工作台","domain":"admin","jpa":false,"fields":["todayAppointmentCount","todayDoneCount","pendingLeadCount","highlightFailedCount","todoItems","reportFunnel","anomalyList"],"systems":["Store Admin FE","Backend Core","Report Media Backend"],"code":"","doc":"docs/ontology/objects.md#entity:workbench","test":"","evidence":"documented","note":"登录落点;含今日待办弹层与指标卡。聚合 appointment/report/lead/highlight 现有数据,非独立表。","status":"planned"} +{"id":"entity:workbench","type":"Entity","name":"Workbench","nameZh":"今日工作台","domain":"admin","jpa":false,"fields":["todayAppointmentCount","todayDoneCount","pendingLeadCount","highlightFailedCount","todoItems","reportFunnel","anomalyList"],"systems":["Store Admin FE","Backend Core","Report Media Backend"],"code":"admin/src/views/WorkbenchView.vue; backend/src/main/java/com/petstore/service/AdminBusinessEventService.java","doc":"docs/ontology/objects.md#entity:workbench","test":"AdminBusinessEventServiceTest","evidence":"anchored","note":"登录落点;待办复用现有接口,报告提交/打开/留资来自 BusinessEvent;发送与转预约未跟踪时显示未知。","status":"implemented"} {"id":"entity:workbench_todo_item","type":"Entity","name":"WorkbenchTodoItem","nameZh":"工作台待办项","domain":"admin","jpa":false,"fields":["kind","count","drilldownAction","filter"],"systems":["Store Admin FE"],"code":"","doc":"docs/ontology/objects.md#entity:workbench_todo_item","test":"","evidence":"documented","note":"kind: today_appointments | pending_report | highlight_anomaly | pending_lead","status":"planned"} {"id":"entity:store_customer","type":"Entity","name":"StoreCustomer","nameZh":"门店客户主档(瘦身)","domain":"admin","jpa":true,"fields":["id","store_id","customer_user_id","phone","display_name","source","first_contact_at","last_contact_at","create_time","update_time","deleted"],"systems":["Store Admin FE","Backend Core","Report Media Backend"],"code":"backend/src/main/java/com/petstore/entity/StoreCustomer.java; backend/src/main/java/com/petstore/service/StoreCustomerService.java; backend/src/main/java/com/petstore/service/AdminServiceCustomerService.java","doc":"docs/ontology/objects.md#entity:store_customer; docs/架构决策-门店客户主档-2026-08-01.md","test":"StoreCustomerServiceTest; AdminServiceCustomerServiceTest; IdentityOwnershipQueryTest","evidence":"anchored","note":"门店与客户的稳定关系实体;API 动态聚合宠物/到店/报告/留资,不含余额/次卡/积分。","status":"implemented"} {"id":"rule:BR-ADMIN-001","type":"Rule","name":"adminRoleGate","nameZh":"门店后台角色门禁(boss/staff 可读写)","domain":"admin","appliesTo":["action:view_workbench","action:admin_list_appointments","action:admin_list_reports","action:admin_list_leads","action:admin_list_service_customers","action:admin_update_settings"],"code":"","doc":"docs/ontology/rules.md#rule:BR-ADMIN-001; docs/门店管理后台-PhaseA-页面清单PRD.md","test":"","evidence":"documented","note":"boss 与 staff 均可进入 admin_console 并编辑本店数据;customer 禁止。删店/部分设置仍仅 boss(BR-STORE-001/BR-USER-001)。产品决策 2026-07-10。"} {"id":"rule:BR-ADMIN-002","type":"Rule","name":"adminStoreScope","nameZh":"后台数据仅本店 storeId","domain":"admin","appliesTo":["action:view_workbench","action:admin_list_appointments","action:admin_list_reports","action:admin_list_leads","action:admin_list_service_customers","action:admin_update_settings"],"code":"","doc":"docs/ontology/rules.md#rule:BR-ADMIN-002","test":"","evidence":"documented","note":"所有后台查询/写操作强制 current.storeId;忽略跨店请求参数。连锁同步不在 Phase A。"} {"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"],"code":"","doc":"docs/ontology/rules.md#rule:BR-ADMIN-003","test":"","evidence":"documented","note":"经 BR-ADMIN-001 鉴权后,后台可返回 inviteCode、完整报告内部字段等;公开 API 仍受 BR-STORE-003/BR-RPT-006 约束。手机号默认仍脱敏,明文需更高权限(预留)。"} {"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":["entity:current_user"],"outputs":["entity:workbench","entity:workbench_todo_item"],"entrypoints":["Admin UI /workbench (planned)"],"appliesRules":["rule:BR-ADMIN-001","rule:BR-ADMIN-002","rule:BR-ADMIN-004"],"code":"","doc":"docs/ontology/actions.md#action:view_workbench","evidence":"documented","note":"登录落点;聚合现有 appointment/report/lead/highlight 统计。可弹今日待办。"} +{"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/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#reportFunnel; backend/src/main/java/com/petstore/service/AdminBusinessEventService.java; admin/src/views/WorkbenchView.vue","doc":"docs/ontology/actions.md#action:view_workbench","test":"AdminBusinessEventControllerTest; AdminBusinessEventServiceTest","evidence":"anchored","note":"事件漏斗按会话 storeId 与日期汇总;待办继续复用现有 appointment/report/lead 接口。"} {"id":"action:drilldown_workbench_todo","type":"Action","name":"drilldownWorkbenchTodo","nameZh":"工作台待办下钻","domain":"admin","inputs":["entity:workbench_todo_item"],"outputs":[],"entrypoints":["Admin UI workbench todo click (planned)"],"appliesRules":["rule:BR-ADMIN-001","rule:BR-ADMIN-002"],"code":"","doc":"docs/ontology/actions.md#action:drilldown_workbench_todo","evidence":"documented","note":"按 kind 跳转 admin_list_appointments / reports / leads 并带筛选。"} {"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)。"} @@ -213,6 +221,7 @@ {"id":"rel:workbench_reads_appointments","type":"Relation","subject":"entity:workbench","predicate":"AGGREGATES","object":"entity:appointment","domain":"admin","evidence":"documented","cardinality":"1—N","doc":"今日预约指标"} {"id":"rel:workbench_reads_reports","type":"Relation","subject":"entity:workbench","predicate":"AGGREGATES","object":"entity:report","domain":"admin","evidence":"documented","cardinality":"1—N","doc":"待出报告/成片异常"} {"id":"rel:workbench_reads_leads","type":"Relation","subject":"entity:workbench","predicate":"AGGREGATES","object":"entity:report_lead","domain":"admin","evidence":"documented","cardinality":"1—N","doc":"待回访"} +{"id":"rel:workbench_reads_business_events","type":"Relation","subject":"entity:workbench","predicate":"AGGREGATES","object":"entity:business_event","domain":"admin","evidence":"anchored","cardinality":"1—N","doc":"报告提交/打开/留资去重漏斗"} {"id":"rel:store_has_service_customers","type":"Relation","subject":"entity:store","predicate":"HAS","object":"entity:store_customer","domain":"admin","evidence":"anchored","cardinality":"1—N","doc":"StoreCustomer.store_id"} {"id":"rel:service_customer_is_user","type":"Relation","subject":"entity:store_customer","predicate":"LINKS","object":"entity:user","domain":"admin","evidence":"anchored","cardinality":"N—0..1","doc":"StoreCustomer.customer_user_id;留资客户可先为空"} {"id":"rel:service_customer_has_pets","type":"Relation","subject":"entity:store_customer","predicate":"HAS","object":"entity:pet","domain":"admin","evidence":"anchored","cardinality":"1—N","doc":"customer_user_id -> Pet.owner_user_id(动态读模型)"} diff --git a/ontology/actions.md b/ontology/actions.md index bc3bab8..2b6533c 100644 --- a/ontology/actions.md +++ b/ontology/actions.md @@ -159,11 +159,12 @@ ### action:track_report_open -报告打开埋点:日志只记 token SHA-256 前 8 位 hash。 +报告打开埋点:服务端用 token 定位 Report 后写入低敏 BusinessEvent;事件表不保存 token/hash,日志只记 SHA-256 前 8 位 hash。 - **入口**:`POST /api/report/open-track` → `ReportController#trackReportOpen`(公开) -- **适用规则**:`rule:BR-RPT-007`(token hash 日志) -- **代码**:`backend/src/main/java/com/petstore/controller/ReportController.java#trackReportOpen` +- **适用规则**:`rule:BR-RPT-007`(token hash 日志)、`rule:BR-BE-001`(不可变与低敏事件) +- **代码**:`ReportController#trackReportOpen`、`BusinessEventService#recordReportOpenByToken` +- **测试**:`ReportControllerTest`、`BusinessEventServiceTest` - **证据**:`anchored` ### action:delete_report @@ -184,7 +185,7 @@ - **入口**:`POST /api/report/{token}/reminder` → `ReportLeadController#submitReminder` → `ReportLeadService#submit`(公开) - **输入**:`phone`、`consent`、`loginCode?` - **输出**:`leadId`、`remindDate`、`unsubscribeToken`、`wechatBound` -- **适用规则**:`rule:BR-LEAD-001`(去重)、`rule:BR-SC-001`(门店客户建档/绑定) +- **适用规则**:`rule:BR-LEAD-001`(去重)、`rule:BR-SC-001`(门店客户建档/绑定)、`rule:BR-BE-001`(低敏事件) - **代码**:`backend/src/main/java/com/petstore/controller/ReportLeadController.java#submitReminder` - **测试**:`ReportLeadControllerTest`、`ReportLeadServiceStoreCustomerTest` - **证据**:`anchored` @@ -581,19 +582,21 @@ --- -## 门店 Web 后台(Phase A · documented) +## 门店 Web 后台(Phase A) > 产品:`docs/门店管理后台升级方案-对标宠老板.md`。优先复用现有 `/api/**`,不另造状态机。 ### action:view_workbench -查看今日工作台(登录落点;可弹今日待办)。 +查看今日工作台(登录落点;可弹今日待办)。报告提交、打开与留资三项通过服务端事件汇总,未建模的报告发送/转预约保持未知。 -- **入口**:Admin UI `/workbench`(planned) +- **入口**:Admin UI `/workbench`;`GET /api/admin/workbench/report-funnel?date=yyyy-MM-dd` - **输出**:`entity:workbench`、`entity:workbench_todo_item` -- **适用规则**:`rule:BR-ADMIN-001`、`rule:BR-ADMIN-002`、`rule:BR-ADMIN-004` +- **适用规则**:`rule:BR-ADMIN-001`、`rule:BR-ADMIN-002`、`rule:BR-ADMIN-004`、`rule:BR-BE-001` - **触发事件**:`event:workbench_viewed`(可选,gap) -- **证据**:`documented` +- **代码**:`AdminBusinessEventController`、`AdminBusinessEventService`、`admin/src/views/WorkbenchView.vue` +- **测试**:`AdminBusinessEventControllerTest`、`AdminBusinessEventServiceTest` +- **证据**:`anchored`(事件漏斗已落地;其他待办仍复用现有接口聚合) ### action:drilldown_workbench_todo diff --git a/ontology/events.md b/ontology/events.md index 2384036..db07d89 100644 --- a/ontology/events.md +++ b/ontology/events.md @@ -40,8 +40,9 @@ 预约创建成功(`status=new`,下单即占号生效)。 - **触发动作**:`action:create_appointment` -- **payload**:`appointmentId`、`storeId`、`customerUserId`、`createdByUserId`、`appointmentTime` -- **代码**:`backend/src/main/java/com/petstore/service/AppointmentService.java#createBooking` +- **持久字段**:`storeId`、`storeCustomerId`、`aggregateType=appointment`、`aggregateId`、`actorUserId/role`、`bookingOrigin` +- **代码**:`backend/src/main/java/com/petstore/service/AppointmentService.java#createBooking`、`BusinessEventService#recordAppointmentCreated` +- **测试**:`AppointmentServiceTest`、`BusinessEventServiceTest` - **证据**:`anchored` ### event:appointment_status_changed @@ -49,12 +50,34 @@ 预约状态迁移(`new→doing`、`new→cancel`、`doing→done`、`doing→cancel`)。 - **触发动作**:`action:start_service`(`new→doing`)、`action:transition_appointment_status`、`action:submit_report`(`doing→done`) -- **payload**:`appointmentId`、`fromStatus`、`toStatus`、`assignedStaffId?` +- **持久字段**:`aggregateId=appointmentId`、`fromStatus`、`toStatus`、`actorUserId/role`、`storeCustomerId?` - **适用规则**:`rule:BR-APPT-001` -- **代码**:`backend/src/main/java/com/petstore/service/AppointmentService.java#startService`/`#transitionStatus`、`backend/src/main/java/com/petstore/service/ReportService.java#create`(置 done) +- **代码**:`AppointmentService#startService`/`#transitionStatus`、`ReportService#create`、`BusinessEventService#recordAppointmentStatusChanged` - **测试**:`AppointmentServiceTest`、`ReportServiceTest#createDoingAppointmentMarksDone` - **证据**:`anchored` +### event:service_started + +服务正式开始(`new -> doing`),是履约时长与服务漏斗的开始事实。 + +- **触发动作**:`action:start_service` +- **持久字段**:`aggregateId=appointmentId`、`storeId`、`storeCustomerId?`、`actorUserId/role`、`occurredAt` +- **幂等键**:`service_started:{appointmentId}` +- **代码**:`backend/src/main/java/com/petstore/service/BusinessEventService.java#recordAppointmentStatusChanged` +- **测试**:`BusinessEventServiceTest#statusToDoingRecordsGenericAndServiceStartedFacts` +- **证据**:`anchored` + +### event:service_completed + +服务完成(`doing -> done`),可由报告提交或合法手工完成触发。 + +- **触发动作**:`action:transition_appointment_status`、`action:submit_report` +- **持久字段**:同 `event:service_started` +- **幂等键**:`service_completed:{appointmentId}` +- **代码**:`BusinessEventService#recordAppointmentStatusChanged`、`ReportService#create` +- **测试**:`AppointmentServiceTest`、`ReportServiceTest` +- **证据**:`anchored` + ## Report 相关 ### event:report_submitted @@ -62,9 +85,9 @@ 报告提交成功,关联预约置 `done`。 - **触发动作**:`action:submit_report` -- **payload**:`reportId`、`appointmentId`、`storeId`、`reportToken` +- **持久字段**:`aggregateId=reportId`、`storeId`、`storeCustomerId?`、`actorUserId/role`;不保存 report token/hash - **适用规则**:`rule:BR-RPT-001`、`rule:BR-RPT-002`、`rule:BR-RPT-004` -- **代码**:`backend/src/main/java/com/petstore/service/ReportService.java#create` +- **代码**:`backend/src/main/java/com/petstore/service/ReportService.java#create`、`BusinessEventService#recordReportSubmitted` - **测试**:`ReportServiceTest` - **证据**:`anchored` @@ -73,19 +96,21 @@ 报告页首次打开(本地 storage 标记首次)。 - **触发动作**:`action:track_report_open` -- **payload**:`reportTokenHash`(SHA-256 前 8 位)、`isFirstOpen`、`source`、`ts` +- **持久字段**:`aggregateId=reportId`、`storeId`、`storeCustomerId?`、`visitType=first`、`occurredAt`;事件表不保存 token/hash - **适用规则**:`rule:BR-RPT-007`(token hash 日志) -- **代码**:`backend/src/main/java/com/petstore/controller/ReportController.java#trackReportOpen` +- **代码**:`ReportController#trackReportOpen`、`BusinessEventService#recordReportOpenByToken` +- **测试**:`ReportControllerTest#reportOpenPersistsLowSensitivityBusinessEvent`、`BusinessEventServiceTest#reportOpenStoresReportIdButNeverToken` - **文档**:`docs/本期主线-下一步产品优化点.md 埋点事件 schema` -- **证据**:`anchored`(日志埋点已实现;统计落地 `documented`) +- **证据**:`anchored`(事件已落库并进入工作台去重统计;日志仅保留短 hash) ### event:report_reopened 报告页再次打开(非首次)。 - **触发动作**:`action:track_report_open` -- **payload**:同 `event:report_opened`,`isFirstOpen=false` -- **证据**:`documented`(schema 已冻结,前端 `isFirstOpen` 判断已实现) +- **持久字段**:同 `event:report_opened`,`visitType=repeat` +- **代码/测试**:同 `event:report_opened` +- **证据**:`anchored` ## ReportLead 相关 @@ -94,11 +119,12 @@ 宠主留资提交成功(含重复提交幂等更新)。 - **触发动作**:`action:submit_lead` -- **payload**:`reportTokenHash`、`storeId`、`hasPhone`、`hasPetName`、`repeatSubmit`、`ts` +- **持久字段**:`aggregateId=leadId`、`storeId`、`storeCustomerId?`、`repeatSubmit`;不复制手机号、IP、openid 或 token - **适用规则**:`rule:BR-LEAD-001` -- **代码**:`backend/src/main/java/com/petstore/controller/ReportLeadController.java#submitReminder` +- **代码**:`ReportLeadService#submit`、`BusinessEventService#recordLeadSubmitted` +- **测试**:`ReportLeadServiceStoreCustomerTest`、`BusinessEventMigrationTest` - **文档**:`docs/本期主线-下一步产品优化点.md 埋点事件 schema` -- **证据**:`documented`(埋点 schema 已冻结,事件上报与统计落地 `gap`) +- **证据**:`anchored` --- diff --git a/ontology/objects.md b/ontology/objects.md index 72383bd..27d094c 100644 --- a/ontology/objects.md +++ b/ontology/objects.md @@ -144,6 +144,29 @@ - **测试**:暂无(Batch D 未补 `ReportLeadControllerTest`,列为 `documented` gap) - **证据**:`anchored`(脱敏与跨店校验已实现并测试覆盖;去重逻辑 `documented`) +## entity:business_event + +**不可变业务事件**。预约、履约、报告打开与留资等经营事实的统一持久化入口,为报告漏斗和 StoreCustomer 时间线提供可统计事实源。 + +| 字段 | 类型 | 说明 | +|------|------|------| +| id / event_id | BIGINT / VARCHAR(36) | 内部主键 / 随机事件 ID | +| event_type / event_version | VARCHAR(64) / INT | 事件类型与 schema 版本 | +| store_id / store_customer_id | BIGINT | 门店范围 / 可选客户关系 | +| aggregate_type / aggregate_id | VARCHAR(32) / BIGINT | 关联业务对象 | +| actor_user_id / actor_role | BIGINT / VARCHAR(16) | 操作人快照,匿名事件可空 | +| source | VARCHAR(32) | customer / admin / public_report / system / migration | +| occurred_at / create_time | DATETIME | 发生时间 / 入库时间 | +| metadata_json | TEXT | 服务端白名单低敏标量 JSON | +| idempotency_key | VARCHAR(128) | 可选幂等键 | + +- **隐私**:不保存手机号、报告 token/hash、媒体 URL、IP、openid/unionid、备注或原始请求体 +- **不变量**:只追加,不更新、不软删;通用事件明细不直接暴露给前端 +- **归属系统**:Backend Core(跨 Report Media Backend 写入点) +- **代码**:`backend/src/main/java/com/petstore/entity/BusinessEvent.java`、`BusinessEventService.java` +- **测试**:`BusinessEventServiceTest`、`BusinessEventMigrationTest` +- **证据**:`anchored` + --- ## Batch 2 录入实体 @@ -337,10 +360,10 @@ --- -## Batch:门店 Web 后台预录入(Phase A · documented) +## Batch:门店 Web 后台(Phase A) > 产品依据:`docs/门店管理后台升级方案-对标宠老板.md`。 -> **尚未编码**;设计与实现须引用下列 ID。收银/会员资产仍见下方 gap 占位。 +> 各对象按实际证据标记;收银/会员资产仍见下方 gap 占位。 ## entity:admin_console @@ -359,17 +382,19 @@ ## entity:workbench -**今日工作台**(非 JPA 聚合视图)。指标卡 + 今日待办 + 报告漏斗摘要;数据来自 appointment/report/lead/highlight,**无独立表**。 +**今日工作台**(非 JPA 聚合视图)。指标卡 + 今日待办 + 报告漏斗摘要;待办来自 appointment/report/lead/highlight,报告漏斗来自 BusinessEvent,**无独立工作台表**。 | 字段(概念) | 说明 | |------|------| | todayAppointmentCount / todayDoneCount | 今日预约 / 今日完成 | | pendingLeadCount / highlightFailedCount | 待回访 / 成片失败 | | todoItems | `entity:workbench_todo_item[]` | -| reportFunnel | 已发/已打开/已留资/报告转预约(缺埋点时可显示 —) | +| reportFunnel | 已提交/已打开/已留资/报告转预约;未跟踪的发送与转预约返回未知 | | anomalyList | 成片失败或待回访最近 N 条 | -- **证据**:`documented` +- **代码**:`admin/src/views/WorkbenchView.vue`、`AdminBusinessEventController`、`AdminBusinessEventService` +- **测试**:`AdminBusinessEventControllerTest`、`AdminBusinessEventServiceTest` +- **证据**:`anchored` - **动作**:`action:view_workbench` ## entity:workbench_todo_item diff --git a/ontology/relations.md b/ontology/relations.md index 28bf146..24e0dd3 100644 --- a/ontology/relations.md +++ b/ontology/relations.md @@ -16,6 +16,8 @@ | `Report` 1—N `ReportImage` | 报告含多张照片(before/after/during) | `ReportImage.report_id` → `Report.id` | | `Report` 1—N `ReportLead` | 一份报告可多次留资(同手机号去重) | `ReportLead.report_id` → `Report.id` | | `ReportLead` N—1 `Store` | 留资归属门店(回访池范围) | `ReportLead.store_id` → `Store.id` | +| `Store` 1—N `BusinessEvent` | 所有可统计事实强制归属门店 | `BusinessEvent.store_id` → `Store.id` | +| `StoreCustomer` 1—N `BusinessEvent` | 客户时间线;无客户语义事件可空 | `BusinessEvent.store_customer_id` → `StoreCustomer.id` | ## 动作—事件(触发) @@ -25,9 +27,9 @@ | `action:login` | `event:session_issued` | | `action:wx_phone_login` | `event:session_issued`、`event:wx_bound`(若 loginCode 有) | | `action:create_appointment` | `event:appointment_created` | -| `action:start_service` | `event:appointment_status_changed`(new→doing) | +| `action:start_service` | `event:appointment_status_changed`(new→doing)、`event:service_started` | | `action:transition_appointment_status` | `event:appointment_status_changed` | -| `action:submit_report` | `event:report_submitted`、`event:appointment_status_changed`(doing→done) | +| `action:submit_report` | `event:report_submitted`、`event:appointment_status_changed`(doing→done)、`event:service_completed` | | `action:track_report_open` | `event:report_opened` / `event:report_reopened` | | `action:submit_lead` | `event:lead_submitted` | @@ -53,6 +55,7 @@ | `rule:BR-LEAD-001` | `action:submit_lead` | | `rule:BR-LEAD-002` | `action:list_leads` | | `rule:BR-LEAD-003` | `action:list_leads` | +| `rule:BR-BE-001` | 所有已落库事件触发动作、`action:view_workbench` | ## 实体—归属系统 @@ -63,6 +66,7 @@ | `entity:appointment` | Backend Core | | `entity:report` | Report Media Backend | | `entity:report_lead` | Report Media Backend | +| `entity:business_event` | Backend Core(跨域写入底座) | --- diff --git a/ontology/rules.md b/ontology/rules.md index ec5249b..3a47f80 100644 --- a/ontology/rules.md +++ b/ontology/rules.md @@ -373,3 +373,12 @@ - **代码**:`backend/src/main/java/com/petstore/service/StoreCustomerService.java` - **测试**:`StoreCustomerServiceTest`、`ReportLeadServiceStoreCustomerTest`、`IdentityOwnershipQueryTest` - **证据**:`anchored` + +### rule:BR-BE-001 + +**业务事件不可变、幂等与低敏**:BusinessEvent 只追加,不更新/软删;稳定业务结果使用 idempotencyKey 去重。metadata 只允许服务端白名单标量,不得保存手机号、报告 token/hash、媒体 URL、IP、openid/unionid、备注、内容全文、密码、密钥或原始请求体。管理端仅返回当前门店聚合,不开放通用事件明细。 + +- **适用动作**:`action:create_appointment`、`action:start_service`、`action:transition_appointment_status`、`action:submit_report`、`action:track_report_open`、`action:submit_lead`、`action:view_workbench` +- **代码**:`backend/src/main/java/com/petstore/service/BusinessEventService.java`、`AdminBusinessEventService.java` +- **测试**:`BusinessEventServiceTest`、`BusinessEventMigrationTest`、`AdminBusinessEventControllerTest` +- **证据**:`anchored` diff --git a/产品设计文档.md b/产品设计文档.md index acddf94..112b687 100644 --- a/产品设计文档.md +++ b/产品设计文档.md @@ -101,6 +101,19 @@ | create_time / update_time | DATETIME | 创建/更新时间 | | deleted | TINYINT | 软删除标记 | +### t_business_event(不可变业务事件) +| 字段 | 类型 | 说明 | +|------|------|------| +| id / event_id | BIGINT / VARCHAR(36) | 内部主键 / 随机事件 ID | +| event_type / event_version | VARCHAR(64) / INT | 过去时事件类型 / schema 版本 | +| store_id / store_customer_id | BIGINT | 门店范围 / 可选客户关系 | +| aggregate_type / aggregate_id | VARCHAR(32) / BIGINT | 关联预约、报告或留资 | +| actor_user_id / actor_role | BIGINT / VARCHAR(16) | 操作人快照,匿名事件可空 | +| source | VARCHAR(32) | customer / admin / public_report / system / migration | +| occurred_at / create_time | DATETIME(3) | 业务发生 / 入库时间 | +| metadata_json | TEXT | 服务端白名单低敏维度,不存原始请求 | +| idempotency_key | VARCHAR(128) | 可选稳定幂等键 | + ### t_appointment(预约表) | 字段 | 类型 | 说明 | |------|------|------| diff --git a/本期主线-下一步产品优化点.md b/本期主线-下一步产品优化点.md index 2e4c0bf..bce310f 100644 --- a/本期主线-下一步产品优化点.md +++ b/本期主线-下一步产品优化点.md @@ -33,20 +33,20 @@ ### 埋点事件 schema(最小集 · 本期冻结) -> 范围:本期 P0 仅要求事件**可统计**(日志或入库均可),不要求看板。字段建议统一小驼峰;时间戳一律毫秒;`reportTokenHash` 为 token SHA-256 前 8 位 hex,不输出原始 token。 +> 范围:事件已统一落 `BusinessEvent`。客户端仍以 token 调公开接口,但事件表仅保存内部 aggregate ID、门店/客户关系和低敏维度;token/hash 只用于现有短日志,不进入事件 metadata。 | 事件名 | 触发时机 | 关键字段 | 来源 | 统计窗口 | Owner | 落地方式 | |--------|----------|----------|------|----------|-------|----------| -| `report_open` | 报告页首次打开(本地 storage 标记首次) | `reportTokenHash`、`storeId`、`petName?`、`serviceType?`、`isFirstOpen`、`ts`、`source`(`link`/`qrcode`/`share`) | H5 报告页 / 小程序报告页 | 日 / 周 | Report Share QA | `POST /api/report/open-track` 或像素 | -| `report_reopen` | 报告页再次打开(非首次) | 同上,`isFirstOpen=false` | 同上 | 日 / 周 | Report Share QA | 同上 | +| `report_opened` | 报告页首次打开(本地 storage 标记首次) | 持久化:`reportId`、`storeId`、`storeCustomerId?`、`visitType=first`、`occurredAt` | H5 报告页 / 小程序报告页 | 日 / 周 | Report Share QA | `POST /api/report/open-track` → BusinessEvent | +| `report_reopened` | 报告页再次打开(非首次) | 同上,`visitType=repeat` | 同上 | 日 / 周 | Report Share QA | 同上 | | `highlight_success` | 成片生成成功 | `reportTokenHash`、`appointmentId`、`durationSec`、`composeMode`、`ts` | 后端成片任务 | 日 | Report Media Backend | 任务回调日志 | | `highlight_failed` | 成片生成失败 | `reportTokenHash`、`appointmentId`、`failReason`(`NETWORK`/`ASSET`/`BUSY`/`UNKNOWN`)、`ts` | 同上 | 日 | Report Media Backend | 同上 | | `video_play` | 报告页成片点击播放 | `reportTokenHash`、`ts`、`source` | 报告页 | 日 / 周 | Customer Experience FE | 前端日志或接口 | | `video_save` | 报告页成片保存到相册 | `reportTokenHash`、`ts` | 同上 | 日 / 周 | Customer Experience FE | 同上 | -| `lead_submit` | 留资提交成功 | `reportTokenHash`、`storeId`、`hasPhone`、`hasPetName`、`ts` | H5 报告页 / 小程序 | 日 / 周 | Report Share QA | `POST /api/report/{token}/reminder` | +| `lead_submitted` | 留资提交成功 | 持久化:`leadId`、`storeId`、`storeCustomerId?`、`repeatSubmit`、`occurredAt`;不复制手机号/IP/token | H5 报告页 / 小程序 | 日 / 周 | Report Share QA | `POST /api/report/{token}/reminder` → BusinessEvent | 字段口径: -- `reportTokenHash`:`SHA-256(token)` 前 8 位 hex,**不输出原始 token**。 +- `reportTokenHash`:仅旧日志使用 `SHA-256(token)` 前 8 位 hex;BusinessEvent 不保存 token 或 hash。 - `source`:枚举 `link` / `qrcode` / `share` / `menu` / `push`,缺失填 `unknown`。 - `isFirstOpen`:本地 storage 标记,不依赖后端。 - 统计窗口默认日/周;老板端日报或最小看板放到 P1。 diff --git a/架构决策-业务事件落库-2026-08-01.md b/架构决策-业务事件落库-2026-08-01.md new file mode 100644 index 0000000..3b4aeb2 --- /dev/null +++ b/架构决策-业务事件落库-2026-08-01.md @@ -0,0 +1,84 @@ +# 架构决策:BusinessEvent 业务事件落库 + +> 日期:2026-08-01 +> +> 状态:已采纳,待生产数据迁移 +> +> Owner Agent:Backend Core(报告/留资写入点联动 Report Media Backend) +> +> Service Lock:`BusinessEvent*`、预约/报告/留资事件调用点、工作台报告漏斗 +> +> Paired QA:Core Flow QA + Report Share QA +> +> Data Model Review Required:Yes +> +> Access / Privacy Review Required:Yes + +## 1. 决策背景 + +此前本体中的 Event 多数只表达“动作发生后应存在的事实”,并没有统一事件表。报告打开仅写应用日志,工作台只能由前端临时拼预约、报告和留资,无法可靠支持经营漏斗、StoreCustomer 时间线、回访归因和复购统计。 + +## 2. 冻结模型 + +`BusinessEvent` 是不可变事实,不是业务对象当前状态,也不是消息队列。 + +| 字段 | 语义 | +|---|---| +| `event_id` | 随机事件 ID;不使用数据库自增 ID 做外部关联 | +| `event_type` / `event_version` | 过去时事件名与 schema 版本 | +| `store_id` | 强制门店数据范围 | +| `store_customer_id` | 可选门店客户稳定关系,供客户时间线使用 | +| `aggregate_type` / `aggregate_id` | 事件对应的 Appointment、Report 或 ReportLead | +| `actor_user_id` / `actor_role` | 操作人快照;匿名公开事件可空 | +| `source` | customer / admin / public_report / system / migration | +| `occurred_at` | 业务事实发生时间 | +| `metadata_json` | 服务端白名单低敏维度,不接受原始请求体 | +| `idempotency_key` | 可选稳定键,保证一个业务结果只落一条 | +| `create_time` | 事件入库时间 | + +事件表只追加,不提供更新、软删除或通用明细 API。 + +## 3. 第一批真实事件 + +| 事件 | 可靠触发点 | 幂等口径 | +|---|---|---| +| `appointment_created` | 预约保存成功 | appointment ID | +| `appointment_status_changed` | 合法状态迁移保存成功 | appointment ID + 目标状态 | +| `service_started` | `new -> doing` | appointment ID | +| `service_completed` | `doing -> done` | appointment ID | +| `report_submitted` | 报告及图片保存成功 | report ID | +| `report_opened` / `report_reopened` | 公开报告打开埋点 | 每次有效打开;统计按 report ID 去重 | +| `lead_submitted` | 每次留资提交成功 | 可重复记录;漏斗按 lead ID 去重 | + +`report_sent`、`follow_up_completed`、`rebook_created` 尚无可靠状态机或归因字段,本阶段明确不伪造;对应工作台指标返回 `null + trackingReady=false`。 + +## 4. 隐私边界 + +- 事件表不得保存手机号、报告 token 或 token hash、媒体 URL、IP、openid/unionid、备注、内容全文、密码、密钥或任意请求体。 +- 报告打开接口只用 token 在服务端定位 Report,事件仅保存内部 report ID;日志继续只记 SHA-256 前 8 位 hex。 +- 管理端只开放按当前会话 `storeId` 聚合的漏斗结果,不开放事件明细。 + +## 5. 一致性与迁移 + +- 预约、状态迁移、报告提交与事件写入处于同一数据库事务。 +- 留资沿用现有并发幂等事务边界;重复提交可产生动作事件,漏斗按 lead ID 去重,不放大转化数。 +- 历史迁移只回填可从数据库确定的预约创建、报告提交、由报告确认的服务完成和留资提交。 +- 历史报告打开与开始服务时间无法可靠恢复,不做猜测性回填。 + +迁移顺序: + +1. `20260801_split_service_identity.sql` +2. `20260801_create_store_customer.sql` +3. `20260801_create_business_event.sql` + +## 6. BusinessEvent 与 AuditLog 边界 + +BusinessEvent 记录“经营上发生了什么”;AuditLog 记录“谁对受保护配置或数据做了什么变更”。本阶段先完成服务闭环事实,后续在门店开通、员工权限、设置修改和敏感操作进入生产化时建立独立 AuditLog,不把安全审计字段混入经营事件。 + +## 7. 验收 + +- `mvn test` 全绿,迁移 SQL 集成测试通过。 +- `npm run build`(admin)通过,工作台“已打开”来自事件汇总。 +- customer 访问工作台事件汇总为 403;boss/staff 只能读取本店。 +- 事件 metadata 隐私单测证明不含原始 token、手机号或 IP。 +- 本体校验通过且 controller/entity 0 漂移。 diff --git a/门店管理后台-PhaseA-交付目标.md b/门店管理后台-PhaseA-交付目标.md index ab6fca8..2aa90f4 100644 --- a/门店管理后台-PhaseA-交付目标.md +++ b/门店管理后台-PhaseA-交付目标.md @@ -20,7 +20,7 @@ | 模块 | 状态 | 主要缺口 | |------|------|----------| | A0 壳层登录 | ~95% | 生产微信登录策略另议 | -| A1 工作台 | ~90% | 待办弹层已做;漏斗「已打开/转预约」仍占位 | +| A1 工作台 | ~95% | 待办弹层已做;漏斗提交/打开/留资已接 BusinessEvent,转预约仍待归因模型 | | A2 预约 | ~95% | 详情/筛选/待出报告/宠主脱敏已做 | | A6 排班 | ~90% | 与小程序对照验收 | | A3 报告 | ~95% | 详情抽屉+失败文案+H5 | diff --git a/门店管理后台-PhaseA-线框说明.md b/门店管理后台-PhaseA-线框说明.md index e446e6d..f89bd11 100644 --- a/门店管理后台-PhaseA-线框说明.md +++ b/门店管理后台-PhaseA-线框说明.md @@ -42,7 +42,7 @@ 今日待办 [待开始 n] [服务中 n] [待出报告 n] [成片异常 n] [待回访 n] -报告漏斗 已发 | 已打开— | 已留资 | 转预约— +报告漏斗 已提交 | 已打开 | 已留资 | 转预约— 异常最近 5 条 …………………… [去处理] diff --git a/门店管理后台-PhaseA-编码任务brief.md b/门店管理后台-PhaseA-编码任务brief.md index 2a20a74..218e84c 100644 --- a/门店管理后台-PhaseA-编码任务brief.md +++ b/门店管理后台-PhaseA-编码任务brief.md @@ -35,7 +35,7 @@ Workstream 建议名:`Store Admin Console` | Paired QA | Core Flow QA | | Workstream | Store Admin Console | | Track | Coding | -| Status | Ready(待授权) | +| Status | Done(工作台事件漏斗已接入) | | Repo/Path | `admin/**` | | Service Lock | `admin-scaffold` | | Depends On | 用户授权编码;选型确认 Vue3+Element Plus | @@ -101,12 +101,12 @@ python3 /Users/apple/_src/petstore/docs/graph/audit_drift.py | Status | Ready(待授权) | | Repo/Path | `admin/src/**`(shell/workbench/appointments) | | Service Lock | `admin-a1-pages` | -| Depends On | T1 | -| ontologyRefs | `[entity:admin_console, entity:workbench, action:view_workbench, action:start_service, rule:BR-ADMIN-001, rule:BR-APPT-001]` | +| Depends On | T1;`entity:business_event` | +| ontologyRefs | `[entity:admin_console, entity:workbench, entity:business_event, action:view_workbench, action:start_service, rule:BR-ADMIN-001, rule:BR-APPT-001, rule:BR-BE-001]` | | Access / Privacy Review Required | Yes | -**验收**:对齐 PRD §1–3 与线框 A0–A2;staff/boss 可开始服务;customer 进不去;待办下钻参数正确。 -**验证**:按 PRD §3.6 / §2.6 手工冒烟 + 构建通过。 +**验收**:对齐 PRD §1–3 与线框 A0–A2;staff/boss 可开始服务;customer 进不去;待办下钻参数正确;报告提交/打开/留资来自本店 BusinessEvent 汇总,未跟踪的转预约显示未知。 +**验证**:`AdminBusinessEventControllerTest` + `AdminBusinessEventServiceTest` + admin 构建;发布后按 PRD §3.6 / §2.6 手工冒烟。 --- diff --git a/门店管理后台-PhaseA-页面清单PRD.md b/门店管理后台-PhaseA-页面清单PRD.md index f407330..11d24fa 100644 --- a/门店管理后台-PhaseA-页面清单PRD.md +++ b/门店管理后台-PhaseA-页面清单PRD.md @@ -90,7 +90,7 @@ entity:admin_console ```text 顶栏 4 指标卡 今日待办条(或登录弹层关闭后的常驻摘要) -报告漏斗四格(可先占位「—」) +报告漏斗四格(提交/打开/留资已由 BusinessEvent 汇总;转预约仍为「—」) 异常列表(成片失败 / 待回访 最近 5 条) 右侧:门店卡片(店名、营业时段摘要、员工数)+ 帮助链接 ``` @@ -119,16 +119,16 @@ entity:admin_console ### 2.4 报告漏斗四格(差异化) -| 格 | 口径 | A 期无埋点时 | -|----|------|----------------| -| 今日已发报告 | 今日新建报告数 | 可算 | -| 已打开 | `report_open` 去重 | 显示「—」,链到 B1 说明 | -| 已留资 | 今日新增 lead | 可算 | -| 报告转预约 | 从报告页带来的预约 | 无可靠归因则「—」 | +| 格 | 口径 | 当前实现 | +|----|------|----------| +| 今日已提交报告 | `report_submitted` 按 report 去重 | BusinessEvent 汇总;发送状态尚未建模,不写“已发” | +| 已打开 | `report_opened/reopened` 按 report 去重 | BusinessEvent 汇总 | +| 已留资 | `lead_submitted` 按 lead 去重 | BusinessEvent 汇总 | +| 报告转预约 | `rebook_created` | 尚无可靠归因,显示「—」 | ### 2.5 API / 聚合 -优先前端或 BFF 聚合现有接口;若延迟高,再补 `GET /api/admin/workbench`(实现时同步本体)。 +待办与异常仍复用现有接口;报告漏斗使用 `GET /api/admin/workbench/report-funnel?date=yyyy-MM-dd`,`storeId` 从会话派生。 复用候选:`/api/appointment/list`、`/api/report/list`、`/api/report/leads`、`/api/store/get`(后台可用内部字段,`BR-ADMIN-003`)。 diff --git a/门店管理后台升级方案-对标宠老板.md b/门店管理后台升级方案-对标宠老板.md index 8cbc5a3..e52f498 100644 --- a/门店管理后台升级方案-对标宠老板.md +++ b/门店管理后台升级方案-对标宠老板.md @@ -443,7 +443,7 @@ Web 后台主攻:**经营、检索、批量、报表、设置**;**允许「 └──────────────────┴──────────────────────────┴──────────────────┘ ``` -**报告漏斗四格**(替代微信商城四格):`今日已发报告` · `已打开` · `已留资` · `从报告发起的预约`——直接打在我们差异化上;缺埋点统计时先显示「—」并链到 B1。 +**报告漏斗四格**(替代微信商城四格):`今日已提交报告` · `已打开` · `已留资` · `从报告发起的预约`——前三项已由 BusinessEvent 汇总;报告发送和转预约归因未建模前显示「—」,不以提交冒充发送。 ### 11.7 截图覆盖度与仍缺清单(2026-07-10 刷新)