diff --git a/P0-研发落地清单.md b/P0-研发落地清单.md index 27ea8b1..1f23aec 100644 --- a/P0-研发落地清单.md +++ b/P0-研发落地清单.md @@ -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 | diff --git a/coverage/ontology-coverage-audit.md b/coverage/ontology-coverage-audit.md index f1d4c75..7fc69f4 100644 --- a/coverage/ontology-coverage-audit.md +++ b/coverage/ontology-coverage-audit.md @@ -3,7 +3,7 @@ > 本体条目 ↔ 文档章节 ↔ 代码路径 ↔ 测试用例 三方对齐审计。 > 证据强度:`anchored`(三处对齐)/ `documented`(仅文档)/ `implemented`(仅代码)/ `gap`(缺失)。 > -> **刷新时间**:2026-08-02(门店开通、一次性员工邀请、AuditLog 与活跃账号鉴权收口后刷新) +> **刷新时间**:2026-08-02(StoreCustomer 事实时间线收口后刷新) ## 审计方法 @@ -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` 212(82.2%)/ `documented` 33 / `gap` 11 / `implemented` 2。 +证据强度:`anchored` 219(82.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`(A0–A7 全页字段/筛选/API/验收)。 -当前 validate 规模以脚本输出为准(258 条目)。 +当前 validate 规模以脚本输出为准(265 条目)。 diff --git a/graph/ontology.jsonl b/graph/ontology.jsonl index 06d9cde..d5f2f29 100644 --- a/graph/ontology.jsonl +++ b/graph/ontology.jsonl @@ -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_id(NULL=系统默认)","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_due;items 不含手机号/token/媒体 URL","status":"implemented"} -{"id":"entity:store_customer","type":"Entity","name":"StoreCustomer","nameZh":"门店客户主档(瘦身)","domain":"admin","jpa":true,"fields":["id","store_id","customer_user_id","phone","display_name","source","first_contact_at","last_contact_at","create_time","update_time","deleted"],"systems":["Store Admin FE","Backend Core","Report Media Backend"],"code":"backend/src/main/java/com/petstore/entity/StoreCustomer.java; backend/src/main/java/com/petstore/service/StoreCustomerService.java; backend/src/main/java/com/petstore/service/AdminServiceCustomerService.java","doc":"docs/ontology/objects.md#entity:store_customer; docs/架构决策-门店客户主档-2026-08-01.md","test":"StoreCustomerServiceTest; AdminServiceCustomerServiceTest; IdentityOwnershipQueryTest","evidence":"anchored","note":"门店与客户的稳定关系实体;API 动态聚合宠物/到店/报告/留资,不含余额/次卡/积分。","status":"implemented"} +{"id":"entity:store_customer","type":"Entity","name":"StoreCustomer","nameZh":"门店客户主档(瘦身)","domain":"admin","jpa":true,"fields":["id","store_id","customer_user_id","phone","display_name","source","first_contact_at","last_contact_at","merged_into_store_customer_id","create_time","update_time","deleted"],"systems":["Store Admin FE","Backend Core","Report Media Backend"],"code":"backend/src/main/java/com/petstore/entity/StoreCustomer.java; backend/src/main/java/com/petstore/service/StoreCustomerService.java; backend/src/main/java/com/petstore/service/AdminServiceCustomerService.java","doc":"docs/ontology/objects.md#entity:store_customer; docs/架构决策-门店客户主档-2026-08-01.md","test":"StoreCustomerServiceTest; AdminServiceCustomerServiceTest; IdentityOwnershipQueryTest; StoreCustomerTimelineMigrationTest","evidence":"anchored","note":"门店与客户的稳定关系实体;被吸收投影以同店 canonical 指针保留不可变事件连续性;不含余额/次卡/积分。","status":"implemented"} {"id":"rule:BR-ADMIN-001","type":"Rule","name":"adminRoleGate","nameZh":"门店后台角色门禁(boss/staff 可读写)","domain":"admin","appliesTo":["action:view_workbench","action:admin_list_appointments","action:admin_list_reports","action:admin_list_leads","action:admin_list_service_customers","action:admin_update_settings"],"code":"","doc":"docs/ontology/rules.md#rule:BR-ADMIN-001; docs/门店管理后台-PhaseA-页面清单PRD.md","test":"","evidence":"documented","note":"boss 与 staff 均可进入 admin_console 并编辑本店数据;customer 禁止。删店/部分设置仍仅 boss(BR-STORE-001/BR-USER-001)。产品决策 2026-07-10。"} {"id":"rule:BR-ADMIN-002","type":"Rule","name":"adminStoreScope","nameZh":"后台数据仅本店 storeId","domain":"admin","appliesTo":["action:view_workbench","action:admin_list_appointments","action:admin_list_reports","action:admin_list_leads","action:admin_list_service_customers","action:admin_update_settings"],"code":"","doc":"docs/ontology/rules.md#rule:BR-ADMIN-002","test":"","evidence":"documented","note":"所有后台查询/写操作强制 current.storeId;忽略跨店请求参数。连锁同步不在 Phase A。"} {"id":"rule:BR-ADMIN-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"} diff --git a/ontology/actions.md b/ontology/actions.md index 36ef058..6c7f680 100644 --- a/ontology/actions.md +++ b/ontology/actions.md @@ -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 后台排班与手动占用(日视图)。 diff --git a/ontology/objects.md b/ontology/objects.md index e5c9a07..21677e2 100644 --- a/ontology/objects.md +++ b/ontology/objects.md @@ -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` 只统计已开始/已完成的历史服务。 diff --git a/ontology/relations.md b/ontology/relations.md index c1255b8..f8c98f5 100644 --- a/ontology/relations.md +++ b/ontology/relations.md @@ -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` | 同店校验后分页读取主档与事实 | diff --git a/ontology/rules.md b/ontology/rules.md index efec688..e85f257 100644 --- a/ontology/rules.md +++ b/ontology/rules.md @@ -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、备注、内容全文、密码、密钥或原始请求体。管理端仅返回当前门店聚合,不开放通用事件明细。 diff --git a/产品设计文档.md b/产品设计文档.md index 3d69d99..a2be19c 100644 --- a/产品设计文档.md +++ b/产品设计文档.md @@ -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 | 店铺设置(老板后台) | ✅ | diff --git a/架构决策-StoreCustomer事实时间线-2026-08-02.md b/架构决策-StoreCustomer事实时间线-2026-08-02.md new file mode 100644 index 0000000..6023499 --- /dev/null +++ b/架构决策-StoreCustomer事实时间线-2026-08-02.md @@ -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、备注或媒体字段。 +- 本体校验与代码端点漂移均为 0;Backend 全测与 Admin production build 通过。 + +数据库迁移:`backend/db/migrations/20260802_create_store_customer_timeline.sql`,在门店开通迁移后第七个执行;末尾两项别名不变量必须为 0。 diff --git a/架构决策-门店客户主档-2026-08-01.md b/架构决策-门店客户主档-2026-08-01.md index 259d0d9..f59e740 100644 --- a/架构决策-门店客户主档-2026-08-01.md +++ b/架构决策-门店客户主档-2026-08-01.md @@ -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. 非目标 本阶段不引入会员等级、积分、储值、次卡、标签群发、跨店共享或客户合并后台。这些需要独立的权限、资产和审计模型。 diff --git a/生产发布与回滚Runbook-2026-08-01.md b/生产发布与回滚Runbook-2026-08-01.md index f708e93..6870a2a 100644 --- a/生产发布与回滚Runbook-2026-08-01.md +++ b/生产发布与回滚Runbook-2026-08-01.md @@ -29,7 +29,7 @@ | Priority | P0 | | Repo/Path | `backend/deploy/**`、前端发布配置、`docs/**` | | Service Lock | Petstore backend unit + Petstore 专用 Nginx server blocks + Petstore static roots | -| Depends On | 冻结 RC、真实域名/TLS、备份、六个迁移、微信合法域名 | +| Depends On | 冻结 RC、真实域名/TLS、备份、七个迁移、微信合法域名 | | Acceptance | 预检、构建、readiness、只读 smoke、三角色人工 smoke、回滚演练均有证据 | | RC Included | Yes | | Due Date | TBD | @@ -47,7 +47,7 @@ - 四仓 commit、构建产物或校验和与 RC manifest 不一致; - 生产域名仍为 localhost、`.invalid`、`api.s-good.com` 或 `www.s-good.com` 等非本项目域名; - 数据库备份未完成、校验和缺失或未在隔离库验证可恢复; -- 任一迁移末尾验证查询非 0,或预检的 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--.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:// ./deploy/production-smoke.sh ### 9.2 数据库回滚边界 -本批六个迁移以新增列/表/索引和兼容回填为主,旧应用应忽略新增结构。因此常规回滚只回应用,不删除列、表、索引,不执行自动 down migration,也不把已产生的新业务数据覆盖回旧备份。 +本批七个迁移以新增列/表/索引和兼容回填为主,旧应用应忽略新增结构。因此常规回滚只回应用,不删除列、表、索引,不执行自动 down migration,也不把已产生的新业务数据覆盖回旧备份。 只有确认发生灾难性数据破坏、已冻结所有写入、明确接受备份时间点之后的数据丢失,并取得用户/业务负责人、DBA 和隐私责任人的书面授权后,才可执行整库恢复。恢复必须先在隔离库验证,再制定专门变更单;本 Runbook 不授权该操作。 @@ -218,7 +219,7 @@ API_ORIGIN=https:// ./deploy/production-smoke.sh - 四仓冻结 commit 与产物校验和; - 备份及隔离恢复验证; -- 六个迁移执行和末尾验证结果; +- 七个迁移执行和末尾验证结果; - 最终配置门禁与后端只读预检; - `nginx -t`、readiness、自动只读 smoke; - boss/staff/customer 三角色人工 smoke; diff --git a/生产监控与告警基线-2026-08-01.md b/生产监控与告警基线-2026-08-01.md index 6795ee1..641f168 100644 --- a/生产监控与告警基线-2026-08-01.md +++ b/生产监控与告警基线-2026-08-01.md @@ -83,7 +83,7 @@ - [ ] 外部 HTTPS 探测覆盖 liveness/readiness、admin、H5 报告入口; - [ ] 5xx、延迟、DB 池、磁盘、证书和进程重启已配置告警; - [ ] FFmpeg/上传失败有可聚合字段,而非只能搜索自由文本; -- [ ] BusinessEvent 日漏斗和 24 项只读数据审计有每日结果; +- [ ] BusinessEvent 日漏斗和 26 项只读数据审计有每日结果; - [ ] 通知渠道、Primary/Backup 值班人和升级联系人完成真实测试; - [ ] 日志脱敏抽查通过,访问权限和保留周期已批准; - [ ] 发布标记能关联四仓 commit 与 RC;