docs: define store customer fact timeline

This commit is contained in:
malei 2026-08-02 01:22:09 +08:00
parent 133834ed02
commit 8c7e6aec56
12 changed files with 125 additions and 24 deletions

View File

@ -63,6 +63,7 @@
| **A16** | **真实预约容量与连续排班占用**:服务时长、门店并发容量和预约时长快照共同决定号源 | 长服务跨半小时桶占号;`walk_in` 消耗一份容量,`blocked` 关闭全部;并发创建不超卖 | Backend Core / Store Admin FE / Data Model | ✅ 已实现(待生产迁移) | backend 全测 + admin/H5/小程序构建 + 本体漂移审计 | 迁移见 `backend/db/migrations/20260801_create_booking_capacity.sql`;不等同排队/准点承诺 |
| **A17** | **报告显式确认发送状态**:传播动作与发送事实分离 | 历史为 `unknown`;新报告为 `unsent`;首次确认写不可覆盖回执与幂等 `report_sent`;工作台仅提醒新报告待发送 | Report Media Backend / Store Miniapp FE / Store Admin FE / Data Model | ✅ 已实现(待生产迁移) | backend 全测 + admin/H5/小程序构建 + 本体漂移审计 | 迁移见 `backend/db/migrations/20260801_create_report_send_status.sql`;不宣称微信投递成功 |
| **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`;既有事件不改写 |
---
@ -134,7 +135,7 @@
| ID | 需求 | 验收标准 | Owner | 状态 | 验收命令 | 备注 |
|----|------|----------|-------|------|----------|------|
| **H1** | **后端可重复发布门禁** | README + production profile + 配置校验 + schema/24 项只读预检 + liveness/readiness + 只读 smoke + systemd/Nginx 模板 | Backend Ops | ✅ 仓库能力已实现;待真实环境执行 | backend 全测/打包 + `bash -n deploy/*.sh` + Runbook | 不把模板/本地通过写成已上线 |
| **H1** | **后端可重复发布门禁** | README + production profile + 配置校验 + schema/26 项只读预检 + liveness/readiness + 只读 smoke + systemd/Nginx 模板 | Backend Ops | ✅ 仓库能力已实现;待真实环境执行 | backend 全测/打包 + `bash -n deploy/*.sh` + Runbook | 不把模板/本地通过写成已上线 |
| **H2** | **前端可重复发布门禁** | admin/H5/小程序真实域名配置先门禁再构建;仓库占位值不能进入正式包;微信合法域名、体验版、静态回滚路径明确 | Frontend Release Ops | ✅ 仓库能力已实现;待真实环境执行 | admin/frontend preflight + 三端构建 + Runbook | 真实配置不得提交 |
| **H3** | **P0 通过条件** | 见下方「P0 通过条件」块 | System Architect / Core Flow QA | 🟡 待人工 smoke 复核 | 见下方 | 对应 P0-10 |

View File

@ -3,7 +3,7 @@
> 本体条目 ↔ 文档章节 ↔ 代码路径 ↔ 测试用例 三方对齐审计。
> 证据强度:`anchored`(三处对齐)/ `documented`(仅文档)/ `implemented`(仅代码)/ `gap`(缺失)。
>
> **刷新时间**2026-08-02门店开通、一次性员工邀请、AuditLog 与活跃账号鉴权收口后刷新)
> **刷新时间**2026-08-02StoreCustomer 事实时间线收口后刷新)
## 审计方法
@ -18,14 +18,14 @@
| 类型 | 数量 |
|------|------|
| Entity | 27 |
| Action | 73 |
| Entity | 28 |
| Action | 74 |
| Event | 27 |
| Rule | 46 |
| Relation | 85 |
| **合计** | **258** |
| Rule | 47 |
| Relation | 89 |
| **合计** | **265** |
证据强度:`anchored` 21282.2%/ `documented` 33 / `gap` 11 / `implemented` 2。
证据强度:`anchored` 21982.6%/ `documented` 33 / `gap` 11 / `implemented` 2。
> anchored 占比相对 Batch 2 下降,是因为主动录入了 P1/P2 `gap` 占位与客户端埋点 `documented` 条目;端点漂移仍为 0。
@ -36,7 +36,7 @@ python3 docs/graph/validate_ontology.py docs
python3 docs/graph/audit_drift.py
```
当前结果校验通过HTTP 端点 65/65、JPA Entity 15/15 对齐;非 JPA 领域实体 12 个;代码与本体均无单边漂移。
当前结果校验通过HTTP 端点 66/66、JPA Entity 15/15 对齐;非 JPA 领域实体 13 个;代码与本体均无单边漂移。
## 元数据卫生
@ -68,11 +68,12 @@ python3 docs/graph/audit_drift.py
| business_event | ✅ | ✅ | BusinessEventServiceTest / MigrationTest | anchored | 不可变低敏事件;工作台去重统计 |
| staff_invitation | ✅ | ✅ | StaffInvitationServiceTest / StoreOnboardingMigrationTest | anchored | 原始 token 只显一次;库内 hash、限时、可撤销、手机号绑定 |
| audit_log | ✅ | ✅ | AuditLogServiceTest / StoreOnboardingMigrationTest | anchored | 低敏不可变安全/配置审计 |
| customer_timeline | ✅ | ✅ read model | StoreCustomerTimelineServiceTest | anchored | StoreCustomer + BusinessEvent 的同店低敏事实投影jpa:false |
| highlight_fail_reason | ✅ | ✅ 枚举 | ReportHighlightVideoServiceTest | anchored | jpa:false |
| current_user | ✅ | ✅ record | SessionTokenServiceTest | anchored | jpa:false |
| membership 等 5 个 | 方向库 | — | — | gap | roadmap 占位 |
## 规则覆盖度46 条)
## 规则覆盖度47 条)
| 规则 | 测试 | 证据 |
|------|------|------|
@ -93,10 +94,11 @@ python3 docs/graph/audit_drift.py
| BR-CONFIG-003 生产发布只读门禁 | 配置单测 + release/smoke 脚本live 证据待发布 | anchored |
| BR-TST-001 一报告一寄语 | ReportTestimonialServiceTest | anchored |
| BR-UNSUB-001 / BR-INT-001 | ReportLeadControllerTest | anchored |
| BR-SC-001 / BR-BE-001 | StoreCustomerServiceTest / BusinessEventServiceTest / MigrationTest | anchored |
| BR-SC-001 / BR-BE-001 | StoreCustomerServiceTest / BusinessEventServiceTest / StoreCustomerTimelineMigrationTest | anchored |
| BR-ONBOARD-001 | MerchantOnboardingServiceTest / StoreOnboardingServiceTest / StoreServiceTest | anchored |
| BR-INVITE-001 | StaffInvitationServiceTest / AuthInterceptorActiveUserTest | anchored |
| BR-AUDIT-001 | AuditLogServiceTest / StoreOnboardingMigrationTest | anchored |
| BR-SC-002 客户时间线低敏投影 | StoreCustomerTimelineServiceTest / AdminServiceCustomerControllerTest | anchored |
## 待补 gap 清单
@ -140,4 +142,4 @@ python3 docs/graph/audit_drift.py
**页面清单 PRD已写**`docs/门店管理后台-PhaseA-页面清单PRD.md`A0A7 全页字段/筛选/API/验收)。
当前 validate 规模以脚本输出为准258 条目)。
当前 validate 规模以脚本输出为准265 条目)。

View File

@ -53,7 +53,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-RPT-008","type":"Rule","name":"报告发送显式确认且首次回执不可覆盖","domain":"report","appliesTo":["action:confirm_report_sent"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#confirmSent; backend/src/main/java/com/petstore/service/ReportService.java#confirmSent; backend/src/main/java/com/petstore/mapper/ReportMapper.java#findByIdAndDeletedFalseForUpdate","test":"ReportControllerTest; ReportServiceTest; ReportSendMigrationTest","doc":"docs/架构决策-报告确认发送状态-2026-08-02.md","evidence":"anchored"}
{"id":"rule:BR-LEAD-001","type":"Rule","name":"留资去重","domain":"lead","appliesTo":["action:submit_lead"],"code":"backend/src/main/java/com/petstore/service/ReportLeadService.java#submit","evidence":"anchored","test":"ReportLeadControllerTest#submitLeadRepeatSubmitReturnsAlreadySubmitted"}
{"id":"rule:BR-SC-001","type":"Rule","name":"门店客户主档唯一性与合并","domain":"admin","appliesTo":["action:create_appointment","action:submit_lead","action:admin_list_service_customers"],"code":"backend/src/main/java/com/petstore/service/StoreCustomerService.java","doc":"docs/ontology/rules.md#rule:BR-SC-001; docs/架构决策-门店客户主档-2026-08-01.md","test":"StoreCustomerServiceTest; ReportLeadServiceStoreCustomerTest; IdentityOwnershipQueryTest","evidence":"anchored"}
{"id":"rule:BR-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-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"}
@ -139,7 +139,7 @@
{"id":"rule:BR-USER-002","type":"Rule","name":"用户更新仅本人","domain":"user","appliesTo":["action:update_user","action:get_user_info"],"code":"backend/src/main/java/com/petstore/controller/UserController.java#updateUser; #info","evidence":"anchored"}
{"id":"rule:BR-CONFIG-001","type":"Rule","name":"生产配置环境变量","domain":"config","appliesTo":[],"code":"backend/src/main/resources/application.yml; application-example.yml","doc":"backend/README.md 环境变量表","evidence":"anchored"}
{"id":"rule:BR-CONFIG-002","type":"Rule","name":"production profile 强制只读 schema 安全配置","domain":"config","appliesTo":[],"code":"backend/src/main/resources/application.yml production profile; backend/src/main/java/com/petstore/config/ProductionConfigurationValidator.java; backend/src/main/java/com/petstore/PetstoreApplication.java","doc":"backend/README.md production profile","test":"ProductionConfigurationValidatorTest","evidence":"anchored","note":"生产禁用自动建表/show-sql/SMS 万能码/默认数据初始化,并拒绝弱密钥、占位域名和宽 CORS"}
{"id":"rule:BR-CONFIG-003","type":"Rule","name":"生产发布只读门禁","domain":"config","appliesTo":[],"code":"backend/src/main/java/com/petstore/config/ProductionDatabasePreflightRunner.java; backend/src/main/java/com/petstore/config/PetstoreRuntimeHealthIndicator.java; backend/deploy/release-preflight.sh; backend/deploy/production-smoke.sh","doc":"docs/生产发布与回滚Runbook-2026-08-01.md; docs/生产监控与告警基线-2026-08-01.md","evidence":"anchored","note":"备份和六迁移后运行 schema validate + 24 项只读不变量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 + 26 项只读不变量readiness 覆盖 DB/磁盘/上传/FFmpeg自动 smoke 只读"}
{"id":"rel:user_owns_pets","type":"Relation","subject":"entity:user","predicate":"OWNS","object":"entity:pet","cardinality":"1—N","evidence":"anchored","doc":"Pet.owner_user_id → User.id","domain":"user"}
{"id":"rel:pet_has_appointments","type":"Relation","subject":"entity:pet","predicate":"HAS","object":"entity:appointment","cardinality":"1—N","evidence":"anchored","doc":"Appointment.pet_id可空","domain":"pet"}
{"id":"rel:store_has_service_types","type":"Relation","subject":"entity:store","predicate":"HAS","object":"entity:service_type","cardinality":"1—N","evidence":"anchored","doc":"ServiceType.store_idNULL=系统默认)","domain":"store"}
@ -208,7 +208,7 @@
{"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_todo_item","type":"Entity","name":"WorkbenchTodoItem","nameZh":"工作台行动组","domain":"admin","jpa":false,"fields":["kind","urgency","count","items","drilldownAction","filter"],"systems":["Store Admin FE","Backend Core"],"code":"backend/src/main/java/com/petstore/service/AdminWorkbenchService.java; admin/src/views/WorkbenchView.vue","doc":"docs/ontology/objects.md#entity:workbench_todo_item","test":"AdminWorkbenchServiceTest","evidence":"anchored","note":"kind: overdue_appointment | in_service | upcoming_today | highlight_anomaly | unsent_report | follow_up_dueitems 不含手机号/token/媒体 URL","status":"implemented"}
{"id":"entity:store_customer","type":"Entity","name":"StoreCustomer","nameZh":"门店客户主档(瘦身)","domain":"admin","jpa":true,"fields":["id","store_id","customer_user_id","phone","display_name","source","first_contact_at","last_contact_at","create_time","update_time","deleted"],"systems":["Store Admin FE","Backend Core","Report Media Backend"],"code":"backend/src/main/java/com/petstore/entity/StoreCustomer.java; backend/src/main/java/com/petstore/service/StoreCustomerService.java; backend/src/main/java/com/petstore/service/AdminServiceCustomerService.java","doc":"docs/ontology/objects.md#entity:store_customer; docs/架构决策-门店客户主档-2026-08-01.md","test":"StoreCustomerServiceTest; AdminServiceCustomerServiceTest; IdentityOwnershipQueryTest","evidence":"anchored","note":"门店与客户的稳定关系实体;API 动态聚合宠物/到店/报告/留资,不含余额/次卡/积分。","status":"implemented"}
{"id":"entity:store_customer","type":"Entity","name":"StoreCustomer","nameZh":"门店客户主档(瘦身)","domain":"admin","jpa":true,"fields":["id","store_id","customer_user_id","phone","display_name","source","first_contact_at","last_contact_at","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-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","action:list_staff_invitations"],"code":"","doc":"docs/ontology/rules.md#rule:BR-ADMIN-003","test":"","evidence":"documented","note":"后台可返回报告内部状态legacy inviteCode 不再返回。员工邀请列表仅返回脱敏手机号且不返回 token/hash原始 token 仅创建响应返回一次。"}
@ -256,3 +256,10 @@
{"id":"rel:store_has_staff_invitations","type":"Relation","subject":"entity:store","predicate":"HAS","object":"entity:staff_invitation","cardinality":"1—N","domain":"user","evidence":"anchored","doc":"StaffInvitation.store_id"}
{"id":"rel:store_has_audit_logs","type":"Relation","subject":"entity:store","predicate":"HAS","object":"entity:audit_log","cardinality":"1—N","domain":"platform","evidence":"anchored","doc":"AuditLog.store_id"}
{"id":"rel:staff_invitation_creates_user","type":"Relation","subject":"action:accept_staff_invitation","predicate":"CREATES","object":"entity:user","domain":"user","evidence":"anchored","doc":"仅微信核验手机号匹配且无账号冲突时创建 staff"}
{"id":"entity:customer_timeline","type":"Entity","name":"CustomerTimeline","nameZh":"门店客户事实时间线","domain":"admin","jpa":false,"fields":["customer","total","page","pageSize","hasMore","list[eventId,eventType,occurredAt,title,description,actorRole,actorName,appointment?,report?,lead?]"],"systems":["Store Admin FE","Backend Core"],"code":"backend/src/main/java/com/petstore/service/StoreCustomerTimelineService.java; admin/src/views/CustomersView.vue","doc":"docs/ontology/objects.md#entity:customer_timeline; docs/架构决策-StoreCustomer事实时间线-2026-08-02.md","test":"StoreCustomerTimelineServiceTest; AdminServiceCustomerControllerTest","evidence":"anchored","note":"由 BusinessEvent 只读投影不返回手机号原文、备注、媒体、token 或原始 metadata"}
{"id":"action:view_store_customer_timeline","type":"Action","name":"viewStoreCustomerTimeline","nameZh":"查看门店客户事实时间线","domain":"admin","inputs":["storeCustomerId","page","pageSize","entity:current_user"],"outputs":["entity:customer_timeline"],"entrypoints":["GET /api/admin/service-customers/timeline","AdminServiceCustomerController#timeline","StoreCustomerTimelineService#timeline","Admin UI /customers drawer"],"appliesRules":["rule:BR-AUTH-001","rule:BR-ADMIN-001","rule:BR-ADMIN-002","rule:BR-ADMIN-003","rule:BR-SC-001","rule:BR-SC-002","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/controller/AdminServiceCustomerController.java#timeline; backend/src/main/java/com/petstore/service/StoreCustomerTimelineService.java; admin/src/views/CustomersView.vue","doc":"docs/ontology/actions.md#action:view_store_customer_timeline; docs/架构决策-StoreCustomer事实时间线-2026-08-02.md","test":"StoreCustomerTimelineServiceTest; AdminServiceCustomerControllerTest","evidence":"anchored"}
{"id":"rule:BR-SC-002","type":"Rule","name":"lowSensitivityCustomerTimelineProjection","nameZh":"客户时间线同店低敏事实投影","domain":"admin","appliesTo":["action:view_store_customer_timeline"],"code":"backend/src/main/java/com/petstore/service/StoreCustomerTimelineService.java; backend/src/main/java/com/petstore/controller/AdminServiceCustomerController.java","doc":"docs/ontology/rules.md#rule:BR-SC-002; docs/架构决策-StoreCustomer事实时间线-2026-08-02.md","test":"StoreCustomerTimelineServiceTest; AdminServiceCustomerControllerTest; StoreCustomerTimelineMigrationTest","evidence":"anchored","note":"以 current.storeId 校验并解析 canonical + 同店别名BusinessEvent 按 occurredAt/id 稳定倒序分页;仅白名单摘要,不返回原始 metadata/PII/凭证/备注/媒体"}
{"id":"rel:store_customer_has_timeline","type":"Relation","subject":"entity:store_customer","predicate":"HAS_VIEW","object":"entity:customer_timeline","cardinality":"1—1","domain":"admin","evidence":"anchored","doc":"按稳定 StoreCustomer.id 生成只读时间线"}
{"id":"rel:business_events_project_to_customer_timeline","type":"Relation","subject":"entity:business_event","predicate":"PROJECTS_TO","object":"entity:customer_timeline","cardinality":"N—1","domain":"admin","evidence":"anchored","doc":"仅 store_customer_id 归属且通过白名单转换的事实"}
{"id":"rel:view_customer_timeline_reads_store_customer","type":"Relation","subject":"action:view_store_customer_timeline","predicate":"READS","object":"entity:store_customer","domain":"admin","evidence":"anchored"}
{"id":"rel:view_customer_timeline_reads_business_events","type":"Relation","subject":"action:view_store_customer_timeline","predicate":"READS","object":"entity:business_event","domain":"admin","evidence":"anchored"}

View File

@ -683,6 +683,18 @@ legacy 永久邀请码查询入口已停用,固定返回 `410 / LEGACY_INVITE_
- **证据**`anchored`
- **代码**`AdminStoreController`、`StoreController#update`;服务类型/员工复用既有 API
### action:view_store_customer_timeline
查看本店某个 StoreCustomer 的事实时间线。
- **入口**`GET /api/admin/service-customers/timeline?storeCustomerId=&page=&pageSize=` → `AdminServiceCustomerController#timeline` → Admin UI `/customers` 详情抽屉
- **输入**`storeCustomerId`、`page`、`pageSize``storeId` 强制从当前会话派生
- **输出**`entity:customer_timeline`
- **适用规则**`rule:BR-AUTH-001`、`rule:BR-ADMIN-001``003`、`rule:BR-SC-001`、`rule:BR-SC-002`、`rule:BR-BE-001`
- **行为**:解析 canonical 主档及其同店合并别名,按 `occurredAt DESC, id DESC` 分页读取全部关联 BusinessEvent批量补齐预约/报告/回访的白名单摘要并提供详情跳转 ID既有事件不改写
- **代码/测试**`StoreCustomerTimelineService`、`CustomersView.vue``StoreCustomerTimelineServiceTest`、`AdminServiceCustomerControllerTest`
- **证据**`anchored`
### action:admin_manage_schedule
后台排班与手动占用(日视图)。

View File

@ -397,6 +397,21 @@
- **代码**`backend/src/main/java/com/petstore/entity/HighlightFailReason.java`
- **证据**`anchored``jpa: false`
## entity:customer_timeline
**门店客户事实时间线**(非 JPA 只读模型)。以 canonical `StoreCustomer.id` 为稳定客户关系键,把已经持久化的 `BusinessEvent` 投影为门店可理解的预约、履约、报告发送/打开和回访意向记录;被合并投影的事件通过别名指针一并读取,不改写历史事件。
| 字段 | 说明 |
|------|------|
| customer | 客户称呼、脱敏手机号、首次来源、首次/最近接触时间 |
| total / page / pageSize / hasMore | 稳定倒序分页信息 |
| list | `eventId/eventType/occurredAt/title/description` 与可选预约/报告/回访低敏摘要 |
- **归属系统**Backend Core / Store Admin FE
- **代码**`backend/src/main/java/com/petstore/service/StoreCustomerTimelineService.java`、`admin/src/views/CustomersView.vue`
- **隐私边界**:不返回手机号原文、备注、媒体、报告/邀请 token、openid/unionid、IP 或原始 `metadata_json`
- **证据**`anchored``jpa: false`
## entity:current_user
**当前登录用户上下文**(非 JPA record。由 `AuthInterceptor``entity:session_token` 解析后写入 ThreadLocal与令牌实体区分。
@ -479,6 +494,7 @@
| phone / display_name | 本店联系手机号与称呼API 手机号默认脱敏 |
| source | 首次来源customer_booking / assisted_booking / report_lead |
| first_contact_at / last_contact_at | 首次/最近接触时间 |
| merged_into_store_customer_id | 仅软删除投影使用;指向同店有效 canonical 主档,保持不可变事件连续性 |
| create_time / update_time / deleted | 审计与软删除 |
API 动态聚合字段:`pets`、`lastVisitAt`、`lastReportId`、`leadStatus`、`source`。`lastVisitAt` 只统计已开始/已完成的历史服务。

View File

@ -171,3 +171,6 @@
| `BR-ADMIN-004` CONSTRAINS `admin_console` | Phase A 范围闸门 |
| admin list actions READS 对应实体 | 列表读模型 |
| `drilldown_workbench_todo` ROUTES_TO `admin_list_*` | 待办下钻 |
| `store_customer` HAS_VIEW `customer_timeline` | 一个稳定门店客户主档对应一个动态事实视图 |
| `business_event` PROJECTS_TO `customer_timeline` | 多条不可变事实经低敏白名单投影为时间线 |
| `view_store_customer_timeline` READS `store_customer` / `business_event` | 同店校验后分页读取主档与事实 |

View File

@ -354,7 +354,7 @@
### rule:BR-CONFIG-003
**生产发布只读门禁**:正式切流前必须按固定顺序完成备份和六个版本化迁移,再以最终生产配置运行 JPA schema `validate` 与 24 项只读数据不变量检查。运行期 readiness 必须同时覆盖数据库、磁盘、上传目录和 FFmpeg/ffprobe上线自动 smoke 只允许 GET不创建或修改业务数据。
**生产发布只读门禁**:正式切流前必须按固定顺序完成备份和七个版本化迁移,再以最终生产配置运行 JPA schema `validate` 与 26 项只读数据不变量检查。运行期 readiness 必须同时覆盖数据库、磁盘、上传目录和 FFmpeg/ffprobe上线自动 smoke 只允许 GET不创建或修改业务数据。
- **适用动作**:—(发布全局约束)
- **代码**`backend/src/main/java/com/petstore/config/ProductionDatabasePreflightRunner.java`、`PetstoreRuntimeHealthIndicator.java`、`backend/deploy/release-preflight.sh`、`production-smoke.sh`
@ -425,13 +425,22 @@
### rule:BR-SC-001
**门店客户主档唯一性与合并**:同一门店内,`customer_user_id` 和手机号分别只能对应一条有效 StoreCustomer。留资可先按手机号建档后续预约/登录识别到 customer 后绑定或合并。不得将 boss/staff 绑定为客户主档。
**门店客户主档唯一性与合并**:同一门店内,`customer_user_id` 和手机号分别只能对应一条有效 StoreCustomer。留资可先按手机号建档后续预约/登录识别到 customer 后绑定或合并。被吸收投影软删除并写入同店 canonical 指针,既有 BusinessEvent 不改写,时间线必须同时读取 canonical 与别名事件。不得将 boss/staff 绑定为客户主档。
- **适用动作**`action:create_appointment`、`action:submit_lead`、`action:admin_list_service_customers`
- **代码**`backend/src/main/java/com/petstore/service/StoreCustomerService.java`
- **测试**`StoreCustomerServiceTest`、`ReportLeadServiceStoreCustomerTest`、`IdentityOwnershipQueryTest`
- **证据**`anchored`
### rule:BR-SC-002
**客户时间线同店低敏事实投影**:仅 boss/staff 可按 `current.storeId + storeCustomerId` 查看;跨店和不存在统一 404。软删除合并别名解析为同店有效 canonical并共同读取其不可变 `BusinessEvent`;按 `occurredAt DESC, id DESC` 稳定分页,只返回服务端白名单化的预约/报告/回访摘要。不得返回手机号原文、备注、媒体 URL、报告/邀请 token、微信标识、IP 或原始 `metadata_json`
- **适用动作**`action:view_store_customer_timeline`
- **代码**`backend/src/main/java/com/petstore/service/StoreCustomerTimelineService.java`、`AdminServiceCustomerController#timeline`
- **测试**`StoreCustomerTimelineServiceTest`、`AdminServiceCustomerControllerTest`
- **证据**`anchored`
### rule:BR-BE-001
**业务事件不可变、幂等与低敏**BusinessEvent 只追加,不更新/软删;稳定业务结果使用 idempotencyKey 去重。metadata 只允许服务端白名单标量,不得保存手机号、报告 token/hash、媒体 URL、IP、openid/unionid、备注、内容全文、密码、密钥或原始请求体。管理端仅返回当前门店聚合不开放通用事件明细。

View File

@ -132,6 +132,7 @@
| display_name | VARCHAR(64) | 门店视角客户称呼 |
| source | VARCHAR(32) | customer_booking / assisted_booking / report_lead |
| first_contact_at / last_contact_at | DATETIME | 首次/最近接触时间 |
| merged_into_store_customer_id | BIGINT | 软删除投影并入的同店 canonical 主档;用于保持不可变事件时间线连续 |
| create_time / update_time | DATETIME | 创建/更新时间 |
| deleted | TINYINT | 软删除标记 |
@ -365,6 +366,12 @@
- 地址
- 简介
**服务客户时间线Admin**
- 以 `StoreCustomer.id` 为稳定门店客户关系,不用手机号或临时列表行作为详情主键
- 客户详情按时间倒序展示预约创建/状态、开始与完成服务、报告提交/确认发送/打开、回访意向等已落库事实
- 每条事实可跳转对应预约、报告或回访池;不在时间线复制备注、媒体或联系方式原文
- 时间线只读且不承载会员、余额、次卡、积分或可编辑营销标签
---
## 六、技术方案
@ -398,6 +405,7 @@
| P1 | 报告过程素材回传duringMedia | 🔨 进行中 |
| **P0** | **洗美过程短视频自动成片(可变时长、双编排、模板拼接)** | **🔨 本期迭代重点** |
| P0 | 老板后台(开通清单+员工邀请中心) | ✅ Admin 与小程序均已接入 |
| P0 | 服务客户事实时间线Admin | ✅ 代码完成,含 StoreCustomer 合并别名历史连续性,待生产迁移 |
| P0 | 老板后台(预约概览) | ✅ 已合并到index.html |
| P1 | 服务类型管理(老板后台) | ✅ |
| P1 | 店铺设置(老板后台) | ✅ |

View File

@ -0,0 +1,39 @@
# 架构决策StoreCustomer 事实时间线
> 日期2026-08-02
> 状态Accepted / Code Complete
## 1. 决策
客户详情时间线以 canonical `StoreCustomer.id` 为唯一门店关系主键,以已持久化的 `BusinessEvent` 为事实源。它是只读投影,不新建可编辑“动态”表,也不从页面点击、当前对象状态或日志猜测历史。
手机号留资投影与账号主档合并时,旧投影软删除并写入 `merged_into_store_customer_id`,既有 BusinessEvent 继续引用旧 ID 且不可改写。时间线解析 canonical 后同时查询它和全部同店别名的事件,因此合并前后的客户事实保持连续。别名只允许单层指向同店、有效且未再合并的 canonical生产预检拒绝活动别名、自引用、跨店、缺失或链式目标。
`GET /api/admin/service-customers/timeline` 只接受 `storeCustomerId/page/pageSize`。门店范围从 session 派生;`current.storeId + storeCustomerId` 不匹配时按不存在返回 404避免枚举他店客户。
## 2. 展示口径
- 按 `occurredAt DESC, id DESC` 对 canonical 与别名事件统一稳定分页;同一时刻用数据库事件 ID 决定顺序。
- 展示预约创建/状态、开始/完成服务、报告提交/确认发送/首次打开/再次打开、回访意向等已经落库的事实。
- 批量补齐预约、报告、回访对象的当前低敏摘要和详情跳转 ID对象已删除时仍保留事件标题和发生时间不伪造摘要。
- Admin 客户抽屉可跳到预约详情、报告列表和回访池;本批不创建回访任务或再次预约归因。
## 3. 隐私与数据边界
允许返回:客户称呼、脱敏手机号、宠物名、服务类型、预约时间、当前业务状态、发送渠道、提醒日期、操作人显示名/角色。
禁止返回:手机号原文、预约/报告备注、图片/视频 URL、报告或邀请 token/hash、openid/unionid、IP、请求体和原始 `metadata_json`。服务端只解析 `fromStatus/toStatus/channel/repeatSubmit` 四个白名单 metadata 字段。
时间线不承载会员等级、余额、次卡、积分、营销标签或自由文本跟进记录。这些能力若进入后续阶段,必须使用独立对象与状态机,不得改写历史事实。
## 4. 验收
- 同店 boss/staff 可查customer 禁止;跨店返回 404。
- 分页上限 100顺序稳定返回 `total/page/pageSize/hasMore`
- 预约、报告、回访聚合 ID 相同时仍按 `aggregateType` 精确解析。
- canonical ID 与已合并别名 ID 均返回同一 canonical 客户摘要,并包含合并前后的全部事件。
- system/public 事件无操作人时正常返回。
- 自动化反例确认响应不包含完整手机号、token、备注或媒体字段。
- 本体校验与代码端点漂移均为 0Backend 全测与 Admin production build 通过。
数据库迁移:`backend/db/migrations/20260802_create_store_customer_timeline.sql`,在门店开通迁移后第七个执行;末尾两项别名不变量必须为 0。

View File

@ -23,6 +23,7 @@
| `display_name` | 门店视角称呼 |
| `source` | 首次来源:`customer_booking` / `assisted_booking` / `report_lead` |
| `first_contact_at` / `last_contact_at` | 本店关系首次/最近触达时间 |
| `merged_into_store_customer_id` | 软删除投影并入的同店 canonical 主档;保留不可变事件归属连续性 |
同店分别对 `(store_id, customer_user_id)``(store_id, phone)` 加唯一约束。`customer_user_id` 为空时可先以手机号建档;后续识别到 customer 账号时绑定或合并,禁止同手机号指向两个 customer。
@ -50,6 +51,8 @@
第二个脚本先从已确定客户归属的预约建档,再合并报告留资。末尾三个验证计数必须全部为 0 才可发布新后端。
事实时间线阶段追加执行 `backend/db/migrations/20260802_create_store_customer_timeline.sql`:它只增加合并别名指针与索引,不改写既有 BusinessEvent。迁移末尾两个别名验证计数必须全部为 0。
## 6. 非目标
本阶段不引入会员等级、积分、储值、次卡、标签群发、跨店共享或客户合并后台。这些需要独立的权限、资产和审计模型。

View File

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

View File

@ -83,7 +83,7 @@
- [ ] 外部 HTTPS 探测覆盖 liveness/readiness、admin、H5 报告入口;
- [ ] 5xx、延迟、DB 池、磁盘、证书和进程重启已配置告警;
- [ ] FFmpeg/上传失败有可聚合字段,而非只能搜索自由文本;
- [ ] BusinessEvent 日漏斗和 24 项只读数据审计有每日结果;
- [ ] BusinessEvent 日漏斗和 26 项只读数据审计有每日结果;
- [ ] 通知渠道、Primary/Backup 值班人和升级联系人完成真实测试;
- [ ] 日志脱敏抽查通过,访问权限和保留周期已批准;
- [ ] 发布标记能关联四仓 commit 与 RC