From 133834ed025ec48448db985cf024dd99bd62dff2 Mon Sep 17 00:00:00 2001 From: malei <> Date: Sun, 2 Aug 2026 01:02:10 +0800 Subject: [PATCH] docs: define store onboarding and invitation boundary --- P0-研发落地清单.md | 4 +- coverage/ontology-coverage-audit.md | 27 +++--- graph/ontology.jsonl | 50 +++++++---- ontology/actions.md | 56 ++++++++---- ontology/events.md | 18 ++-- ontology/objects.md | 49 ++++++++++- ontology/relations.md | 12 ++- ontology/rules.md | 54 +++++++++--- ...-onboarding-staff-invitation-2026-08-02.md | 59 +++++++++++++ qa-reports/scripts/admin-phase-a-api-smoke.sh | 2 +- 产品设计文档.md | 62 +++++++++---- 架构决策-门店开通与员工邀请-2026-08-02.md | 87 +++++++++++++++++++ 生产发布与回滚Runbook-2026-08-01.md | 26 +++--- 生产监控与告警基线-2026-08-01.md | 2 +- 14 files changed, 405 insertions(+), 103 deletions(-) create mode 100644 qa-reports/phase0-store-onboarding-staff-invitation-2026-08-02.md create mode 100644 架构决策-门店开通与员工邀请-2026-08-02.md diff --git a/P0-研发落地清单.md b/P0-研发落地清单.md index b737943..27ea8b1 100644 --- a/P0-研发落地清单.md +++ b/P0-研发落地清单.md @@ -62,6 +62,7 @@ | **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` | | **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`;单角色账号冲突明确拒绝 | --- @@ -74,6 +75,7 @@ | **B3** | **报告提交锁死提示** | 提交前弹窗固定文案:「提交后不可修改,请确认照片与备注无误」 | Store Miniapp FE | 🟡 部分实现:核对文案与位置 | 手工 smoke | 与产品 v1 锁死策略一致 | | **B4** | **「我的」老板端分组标题** | 至少两组:`店铺与经营` / `团队与账号`;高频在上;使用 `.module-title` | Store Miniapp FE | ✅ 已实现 | `npm --prefix frontend run build:h5` + 手工 smoke | 见《产品全方位优化说明》§2.2 | | **B5** | **报告前后照片必填前端校验** | 无 before 提示「请至少上传1张服务前照片」;无 after 提示「请至少上传1张服务后照片」 | Store Miniapp FE / Customer Experience FE | ✅ 已实现(Batch A) | `npm --prefix frontend run build:h5` + `npm --prefix frontend run build:mp-weixin` | 对应 A7、P0-4 | +| **B6** | **小程序开通向导与员工邀请卡片** | 老板可查看/完成开通清单;创建邀请后原始口令只显示一次,可直接 `open-type=share` 转发;员工预览脱敏信息并经微信手机号核验加入 | Store Miniapp FE / Backend Core | ✅ 代码完成,待微信真机 smoke | H5 + mp-weixin 构建;老板/员工双微信真机验收 | H5 只预览,不绕过微信核验 | --- @@ -132,7 +134,7 @@ | ID | 需求 | 验收标准 | Owner | 状态 | 验收命令 | 备注 | |----|------|----------|-------|------|----------|------| -| **H1** | **后端可重复发布门禁** | README + production profile + 配置校验 + schema/18 项只读预检 + liveness/readiness + 只读 smoke + systemd/Nginx 模板 | Backend Ops | ✅ 仓库能力已实现;待真实环境执行 | backend 全测/打包 + `bash -n deploy/*.sh` + Runbook | 不把模板/本地通过写成已上线 | +| **H1** | **后端可重复发布门禁** | README + production profile + 配置校验 + schema/24 项只读预检 + 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 f12e260..f1d4c75 100644 --- a/coverage/ontology-coverage-audit.md +++ b/coverage/ontology-coverage-audit.md @@ -3,7 +3,7 @@ > 本体条目 ↔ 文档章节 ↔ 代码路径 ↔ 测试用例 三方对齐审计。 > 证据强度:`anchored`(三处对齐)/ `documented`(仅文档)/ `implemented`(仅代码)/ `gap`(缺失)。 > -> **刷新时间**:2026-08-01(BusinessEvent、StoreCustomer、身份语义与工作台行动聚合收口后刷新) +> **刷新时间**:2026-08-02(门店开通、一次性员工邀请、AuditLog 与活跃账号鉴权收口后刷新) ## 审计方法 @@ -18,14 +18,14 @@ | 类型 | 数量 | |------|------| -| Entity | 25 | -| Action | 64 | -| Event | 26 | -| Rule | 40 | -| Relation | 81 | -| **合计** | **236** | +| Entity | 27 | +| Action | 73 | +| Event | 27 | +| Rule | 46 | +| Relation | 85 | +| **合计** | **258** | -证据强度:`anchored` 185(78.4%)/ `documented` 37 / `gap` 12 / `implemented` 2。 +证据强度:`anchored` 212(82.2%)/ `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 端点 55/55、JPA Entity 13/13 对齐;非 JPA 领域实体 12 个。 +当前结果:校验通过;HTTP 端点 65/65、JPA Entity 15/15 对齐;非 JPA 领域实体 12 个;代码与本体均无单边漂移。 ## 元数据卫生 @@ -66,11 +66,13 @@ python3 docs/graph/audit_drift.py | service_interval | ✅ | ✅ | 间接 ReportLeadControllerTest | anchored | | | store_customer | ✅ | ✅ | StoreCustomerServiceTest / IdentityOwnershipQueryTest | anchored | 稳定门店客户关系 | | business_event | ✅ | ✅ | BusinessEventServiceTest / MigrationTest | anchored | 不可变低敏事件;工作台去重统计 | +| staff_invitation | ✅ | ✅ | StaffInvitationServiceTest / StoreOnboardingMigrationTest | anchored | 原始 token 只显一次;库内 hash、限时、可撤销、手机号绑定 | +| audit_log | ✅ | ✅ | AuditLogServiceTest / StoreOnboardingMigrationTest | anchored | 低敏不可变安全/配置审计 | | highlight_fail_reason | ✅ | ✅ 枚举 | ReportHighlightVideoServiceTest | anchored | jpa:false | | current_user | ✅ | ✅ record | SessionTokenServiceTest | anchored | jpa:false | | membership 等 5 个 | 方向库 | — | — | gap | roadmap 占位 | -## 规则覆盖度(40 条) +## 规则覆盖度(46 条) | 规则 | 测试 | 证据 | |------|------|------| @@ -92,6 +94,9 @@ python3 docs/graph/audit_drift.py | BR-TST-001 一报告一寄语 | ReportTestimonialServiceTest | anchored | | BR-UNSUB-001 / BR-INT-001 | ReportLeadControllerTest | anchored | | BR-SC-001 / BR-BE-001 | StoreCustomerServiceTest / BusinessEventServiceTest / MigrationTest | anchored | +| BR-ONBOARD-001 | MerchantOnboardingServiceTest / StoreOnboardingServiceTest / StoreServiceTest | anchored | +| BR-INVITE-001 | StaffInvitationServiceTest / AuthInterceptorActiveUserTest | anchored | +| BR-AUDIT-001 | AuditLogServiceTest / StoreOnboardingMigrationTest | anchored | ## 待补 gap 清单 @@ -135,4 +140,4 @@ python3 docs/graph/audit_drift.py **页面清单 PRD(已写)**:`docs/门店管理后台-PhaseA-页面清单PRD.md`(A0–A7 全页字段/筛选/API/验收)。 -当前 validate 规模以脚本输出为准(236 条目)。 +当前 validate 规模以脚本输出为准(258 条目)。 diff --git a/graph/ontology.jsonl b/graph/ontology.jsonl index 710a5af..06d9cde 100644 --- a/graph/ontology.jsonl +++ b/graph/ontology.jsonl @@ -1,13 +1,13 @@ -{"id":"entity:store","type":"Entity","name":"Store","nameZh":"门店","domain":"store","fields":["id","name","logo","phone","address","latitude","longitude","intro","owner_id","invite_code","booking_day_start","booking_last_slot_start","booking_capacity","create_time","update_time","deleted"],"systems":["Backend Core"],"code":"backend/src/main/java/com/petstore/entity/Store.java","doc":"docs/产品设计文档.md §三 t_store","test":"StoreControllerTest","evidence":"anchored"} +{"id":"entity:store","type":"Entity","name":"Store","nameZh":"门店","domain":"store","fields":["id","name","logo","phone","address","latitude","longitude","intro","owner_id","invite_code","booking_day_start","booking_last_slot_start","booking_capacity","onboarding_status","onboarding_completed_at","onboarding_completed_by_user_id","create_time","update_time","deleted"],"systems":["Backend Core"],"code":"backend/src/main/java/com/petstore/entity/Store.java","doc":"docs/产品设计文档.md §三 t_store; docs/架构决策-门店开通与员工邀请-2026-08-02.md","test":"StoreControllerTest; StoreOnboardingServiceTest; StoreOnboardingMigrationTest","evidence":"anchored","note":"invite_code 仅保留数据库兼容,不再返回客户端或作为注册凭证"} {"id":"entity:user","type":"Entity","name":"User","nameZh":"用户","domain":"user","fields":["id","username","password","name","phone","avatar","store_id","role","wechat_openid","wechat_unionid","create_time","update_time","deleted"],"systems":["Backend Core"],"code":"backend/src/main/java/com/petstore/entity/User.java","doc":"docs/产品设计文档.md §三 t_user; docs/.agents/common-context.md 术语规则","test":"UserServiceLoginStrategyTest; UserJsonIgnoreTest","evidence":"anchored"} {"id":"entity:appointment","type":"Entity","name":"Appointment","nameZh":"预约","domain":"appointment","fields":["id","pet_name","pet_type","service_type","appointment_time","duration_minutes","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; docs/架构决策-真实预约容量模型-2026-08-01.md","test":"AppointmentServiceTest; BookingCapacityServiceTest; AppointmentControllerDetailTest; AppointmentControllerIdentityTest; BookingCapacityMigrationTest","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","send_status","sent_at","sent_by_user_id","send_channel","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","Store Miniapp FE","Store Admin FE"],"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/架构决策-报告确认发送状态-2026-08-02.md","test":"ReportServiceTest; ReportControllerTest; ReportSendMigrationTest","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:register_boss","type":"Action","name":"registerBoss","nameZh":"微信核验老板入驻","inputs":["storeName","bossName","phoneCode","loginCode"],"outputs":["entity:store","entity:user","session_token","entity:business_event","entity:audit_log"],"entrypoints":["POST /api/onboarding/register-boss","MerchantOnboardingController#registerBoss","MerchantOnboardingService#registerVerifiedBoss"],"appliesRules":["rule:BR-ONBOARD-001","rule:BR-AUDIT-001"],"code":"backend/src/main/java/com/petstore/controller/MerchantOnboardingController.java#registerBoss; backend/src/main/java/com/petstore/service/MerchantOnboardingService.java#registerVerifiedBoss","test":"MerchantOnboardingServiceTest; BusinessEventServiceTest","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"} -{"id":"action:get_store_by_invite_code","type":"Action","name":"getStoreByInviteCode","nameZh":"按邀请码查门店","inputs":["code"],"outputs":["entity:store"],"entrypoints":["GET /api/store/invite-code","StoreController#getByInviteCode"],"appliesRules":["rule:BR-STORE-002"],"code":"backend/src/main/java/com/petstore/controller/StoreController.java#getByInviteCode","test":"StoreControllerTest","evidence":"anchored","domain":"store"} +{"id":"action:get_store_by_invite_code","type":"Action","name":"getStoreByInviteCode","nameZh":"永久邀请码查询(停用)","inputs":["code"],"outputs":[],"entrypoints":["GET /api/store/invite-code","StoreController#getByInviteCode"],"appliesRules":["rule:BR-STORE-002"],"code":"backend/src/main/java/com/petstore/controller/StoreController.java#getByInviteCode","test":"StoreControllerTest","evidence":"anchored","domain":"store","status":"deprecated","deprecated":true,"note":"固定返回 LEGACY_INVITE_DISABLED,不查询或返回门店"} {"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"} @@ -24,8 +24,8 @@ {"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:store_registered","type":"Event","name":"StoreRegistered","nameZh":"门店入驻","domain":"store","payload":["storeId","bossUserId","occurredAt"],"emittedBy":["action:register_boss"],"code":"backend/src/main/java/com/petstore/service/BusinessEventService.java#recordStoreRegistered","test":"BusinessEventServiceTest","evidence":"anchored","note":"持久化 BusinessEvent 与 AuditLog,均不含手机号或 legacy inviteCode"} +{"id":"event:session_issued","type":"Event","name":"SessionIssued","nameZh":"签发会话令牌","domain":"auth","payload":["userId","storeId","role","exp"],"emittedBy":["action:register_boss","action:login","action:wx_phone_login","action:accept_staff_invitation"],"code":"backend/src/main/java/com/petstore/auth/SessionTokenService.java#issue","test":"SessionTokenServiceTest","evidence":"anchored"} {"id":"event: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":["storeId","storeCustomerId","aggregateId","actorUserId","actorRole","bookingOrigin","durationMinutes","occurredAt"],"emittedBy":["action:create_appointment"],"appliesRules":["rule:BR-BE-001","rule:BR-APPT-004"],"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"} @@ -36,10 +36,10 @@ {"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-001","type":"Rule","name":"HMAC session token + 活跃账号范围鉴权","domain":"auth","appliesTo":["action:update_store","action:delete_store","action:get_store_by_invite_code","action:create_appointment","action:start_service","action:transition_appointment_status","action:get_appointment_detail","action:list_appointments","action:submit_report","action:delete_report","action:list_leads","action:get_store_onboarding","action:complete_store_onboarding","action:create_staff_invitation","action:list_staff_invitations","action:revoke_staff_invitation","action:list_staff","action:delete_staff"],"code":"backend/src/main/java/com/petstore/auth/AuthInterceptor.java; SessionTokenService.java; CurrentUserContext.java","test":"SessionTokenServiceTest; AuthInterceptorActiveUserTest","evidence":"anchored","note":"除签名和过期外,每次受保护请求校验账号仍存在且 role/storeId 与 token 一致,门店角色还须归属有效 Store"} {"id":"rule:BR-AUTH-002","type":"Rule","name":"生产登录策略:仅微信授权","domain":"auth","appliesTo":["action:login","action:wx_phone_login"],"code":"backend/src/main/java/com/petstore/service/UserService.java#login; SmsController.java","test":"UserServiceLoginStrategyTest","evidence":"anchored"} {"id":"rule:BR-STORE-001","type":"Rule","name":"店铺更新/删除仅 boss + 同店","domain":"store","appliesTo":["action:update_store","action:delete_store"],"code":"backend/src/main/java/com/petstore/controller/StoreController.java#update; #delete","test":"StoreControllerTest","evidence":"anchored"} -{"id":"rule:BR-STORE-002","type":"Rule","name":"邀请码查询仅 boss/staff + 同店","domain":"store","appliesTo":["action:get_store_by_invite_code"],"code":"backend/src/main/java/com/petstore/controller/StoreController.java#getByInviteCode","test":"StoreControllerTest#inviteCodeCrossStoreForbidden","evidence":"anchored"} +{"id":"rule:BR-STORE-002","type":"Rule","name":"legacy 永久邀请码禁用","domain":"store","appliesTo":["action:get_store_by_invite_code","action:register_staff"],"code":"backend/src/main/java/com/petstore/controller/StoreController.java#getByInviteCode; backend/src/main/java/com/petstore/controller/UserController.java#registerStaff","test":"StoreControllerTest","evidence":"anchored","note":"legacy invite_code 保留数据库兼容,不返回客户端、不再作为注册凭证"} {"id":"rule:BR-APPT-001","type":"Rule","name":"预约状态机","domain":"appointment","appliesTo":["action:start_service","action:transition_appointment_status","action:submit_report"],"code":"backend/src/main/java/com/petstore/service/AppointmentService.java#startService; #transitionStatus","test":"AppointmentServiceTest","evidence":"anchored"} {"id":"rule:BR-APPT-002","type":"Rule","name":"预约跨店 403","domain":"appointment","appliesTo":["action:list_appointments","action:start_service","action:transition_appointment_status","action:get_appointment_detail"],"code":"backend/src/main/java/com/petstore/controller/AppointmentController.java","test":"AppointmentControllerDetailTest","evidence":"anchored"} {"id":"rule:BR-APPT-003","type":"Rule","name":"预约客户归属与跨用户 403","domain":"appointment","appliesTo":["action:create_appointment","action:get_appointment_detail","action:transition_appointment_status"],"code":"backend/src/main/java/com/petstore/controller/AppointmentController.java#create; #detail; #updateStatus; backend/src/main/java/com/petstore/service/AppointmentService.java#createBooking","test":"AppointmentControllerIdentityTest; AppointmentControllerDetailTest#customerCannotViewOthersAppointment","evidence":"anchored"} @@ -110,7 +110,7 @@ {"id":"action:upload_image","type":"Action","name":"uploadImage","nameZh":"上传图片/视频","inputs":["file"],"outputs":["url"],"entrypoints":["POST /api/upload/image","FileController#uploadImage"],"appliesRules":["rule:BR-UPLOAD-001","rule:BR-UPLOAD-002"],"code":"backend/src/main/java/com/petstore/controller/FileController.java#uploadImage","evidence":"anchored","domain":"upload"} {"id":"action:get_upload_image","type":"Action","name":"getUploadImage","nameZh":"读取上传资源","inputs":["path"],"outputs":[],"entrypoints":["GET /api/upload/image/**","FileController#getImage"],"appliesRules":["rule:BR-UPLOAD-001"],"code":"backend/src/main/java/com/petstore/controller/FileController.java#getImage","evidence":"anchored","domain":"upload"} {"id":"action:generate_highlight","type":"Action","name":"generateHighlight","nameZh":"触发成片生成","inputs":["reportId","composeMode"],"outputs":["entity:highlight_video"],"entrypoints":["POST /api/report/highlight/start","ReportController#startHighlight","ReportHighlightVideoService#requestGenerate"],"appliesRules":["rule:BR-HL-001","rule:BR-HL-002"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#startHighlight; service/ReportHighlightVideoService.java#requestGenerate","doc":"docs/洗美报告短视频成片方案.md","evidence":"anchored","domain":"report","test":"ReportHighlightVideoServiceTest"} -{"id":"action:create_staff","type":"Action","name":"createStaff","nameZh":"老板创建员工","inputs":["name","phone"],"outputs":["entity:user"],"entrypoints":["POST /api/user/create-staff","UserController#createStaff","UserService#createStaff"],"appliesRules":["rule:BR-USER-001"],"code":"backend/src/main/java/com/petstore/controller/UserController.java#createStaff","evidence":"anchored","domain":"user"} +{"id":"action:create_staff","type":"Action","name":"createStaff","nameZh":"直接创建员工(停用)","inputs":["name","phone"],"outputs":[],"entrypoints":["POST /api/user/create-staff","UserController#createStaff"],"appliesRules":["rule:BR-USER-001"],"code":"backend/src/main/java/com/petstore/controller/UserController.java#createStaff","evidence":"anchored","domain":"user","status":"deprecated","deprecated":true,"note":"固定返回 DIRECT_STAFF_CREATION_DISABLED,员工必须接受限时邀请"} {"id":"action:list_staff","type":"Action","name":"listStaff","nameZh":"员工列表","inputs":[],"outputs":["entity:user"],"entrypoints":["GET /api/user/staff-list","UserController#staffList"],"appliesRules":["rule:BR-USER-001"],"code":"backend/src/main/java/com/petstore/controller/UserController.java#staffList","evidence":"anchored","domain":"user"} {"id":"action:delete_staff","type":"Action","name":"deleteStaff","nameZh":"删除员工","inputs":["staffId"],"outputs":[],"entrypoints":["DELETE /api/user/staff","UserController#deleteStaff"],"appliesRules":["rule:BR-USER-001"],"code":"backend/src/main/java/com/petstore/controller/UserController.java#deleteStaff","evidence":"anchored","domain":"user"} {"id":"action:update_user","type":"Action","name":"updateUser","nameZh":"更新当前用户信息","inputs":["name","phone","avatar","code"],"outputs":["entity:user"],"entrypoints":["PUT /api/user/update","UserController#updateUser","UserService#updateUser"],"appliesRules":["rule:BR-USER-002","rule:BR-AUTH-002"],"code":"backend/src/main/java/com/petstore/controller/UserController.java#updateUser","evidence":"anchored","domain":"user"} @@ -125,7 +125,7 @@ {"id":"event:service_type_created","type":"Event","name":"ServiceTypeCreated","nameZh":"服务类型创建","domain":"service_type","payload":["serviceTypeId","storeId","name"],"emittedBy":["action:create_service_type"],"code":"backend/src/main/java/com/petstore/service/ServiceTypeService.java#create","evidence":"anchored"} {"id":"event:service_type_deleted","type":"Event","name":"ServiceTypeDeleted","nameZh":"服务类型删除","domain":"service_type","payload":["serviceTypeId","storeId"],"emittedBy":["action:delete_service_type"],"code":"backend/src/main/java/com/petstore/service/ServiceTypeService.java#delete","evidence":"anchored"} {"id":"event:pet_created","type":"Event","name":"PetCreated","nameZh":"宠物档案创建","domain":"pet","payload":["petId","ownerUserId","name","petType"],"emittedBy":["action:create_pet"],"code":"backend/src/main/java/com/petstore/service/PetService.java#create","evidence":"anchored"} -{"id":"event:staff_created","type":"Event","name":"StaffCreated","nameZh":"员工创建","domain":"user","payload":["staffId","storeId","name","phone"],"emittedBy":["action:create_staff"],"code":"backend/src/main/java/com/petstore/service/UserService.java#createStaff","evidence":"anchored"} +{"id":"event:staff_invitation_accepted","type":"Event","name":"StaffInvitationAccepted","nameZh":"员工接受邀请","domain":"user","payload":["storeId","staffUserId","invitationId","verificationMethod","occurredAt"],"emittedBy":["action:accept_staff_invitation"],"code":"backend/src/main/java/com/petstore/service/StaffInvitationService.java#acceptVerified; backend/src/main/java/com/petstore/service/AuditLogService.java#record","test":"StaffInvitationServiceTest","evidence":"anchored","note":"以 AuditLog 持久化,不含手机号、token、openid 或 unionid"} {"id":"event:user_updated","type":"Event","name":"UserUpdated","nameZh":"用户信息更新","domain":"user","payload":["userId","updatedFields"],"emittedBy":["action:update_user"],"code":"backend/src/main/java/com/petstore/service/UserService.java#updateUser","evidence":"anchored"} {"id":"rule:BR-UPLOAD-001","type":"Rule","name":"上传路径归一化","domain":"upload","appliesTo":["action:upload_image","action:get_upload_image"],"code":"backend/src/main/java/com/petstore/controller/FileController.java#resolveSafePath; #uploadRoot","evidence":"anchored"} {"id":"rule:BR-UPLOAD-002","type":"Rule","name":"上传扩展名/MIME 校验","domain":"upload","appliesTo":["action:upload_image"],"code":"backend/src/main/java/com/petstore/controller/FileController.java#isAllowedMediaType; #hasMediaExtension","evidence":"anchored"} @@ -135,11 +135,11 @@ {"id":"rule:BR-SCH-002","type":"Rule","name":"手动占用容量语义:walk_in 消耗一份,blocked 关闭全部","domain":"schedule","appliesTo":["action:day_agenda","action:create_schedule_block"],"code":"backend/src/main/java/com/petstore/service/ScheduleService.java; backend/src/main/java/com/petstore/service/BookingCapacityService.java","test":"BookingCapacityServiceTest; ScheduleControllerTest","evidence":"anchored"} {"id":"rule:BR-ST-001","type":"Rule","name":"服务类型仅 boss + 同店 + 系统默认不可改删","domain":"service_type","appliesTo":["action:create_service_type","action:update_service_type","action:delete_service_type","action:init_service_types"],"code":"backend/src/main/java/com/petstore/controller/ServiceTypeController.java","test":"ServiceTypeControllerTest","evidence":"anchored"} {"id":"rule:BR-PET-001","type":"Rule","name":"宠物跨用户/跨店","domain":"pet","appliesTo":["action:list_pets","action:create_pet","action:update_pet","action:delete_pet","action:get_pet_history"],"code":"backend/src/main/java/com/petstore/controller/PetController.java","evidence":"anchored","test":"PetControllerTest"} -{"id":"rule:BR-USER-001","type":"Rule","name":"员工管理仅 boss + 同店","domain":"user","appliesTo":["action:create_staff","action:list_staff","action:delete_staff"],"code":"backend/src/main/java/com/petstore/controller/UserController.java","evidence":"anchored"} +{"id":"rule:BR-USER-001","type":"Rule","name":"员工管理与邀请仅 boss + 同店","domain":"user","appliesTo":["action:create_staff","action:list_staff","action:delete_staff","action:create_staff_invitation","action:list_staff_invitations","action:revoke_staff_invitation"],"code":"backend/src/main/java/com/petstore/controller/UserController.java; backend/src/main/java/com/petstore/controller/StaffInvitationController.java","test":"StaffInvitationServiceTest","evidence":"anchored","note":"门店成员可看成员列表;仅 boss 可创建/查看/撤销邀请和删除本店 staff,不能删除 boss"} {"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 + 18 项只读不变量;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 + 24 项只读不变量;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"} @@ -156,7 +156,7 @@ {"id":"rel:create_service_type_emits_type_created","type":"Relation","subject":"action:create_service_type","predicate":"EMITS","object":"event:service_type_created","evidence":"anchored","domain":"service_type"} {"id":"rel:delete_service_type_emits_type_deleted","type":"Relation","subject":"action:delete_service_type","predicate":"EMITS","object":"event:service_type_deleted","evidence":"anchored","domain":"service_type"} {"id":"rel:create_pet_emits_pet_created","type":"Relation","subject":"action:create_pet","predicate":"EMITS","object":"event:pet_created","evidence":"anchored","domain":"pet"} -{"id":"rel:create_staff_emits_staff_created","type":"Relation","subject":"action:create_staff","predicate":"EMITS","object":"event:staff_created","evidence":"anchored","domain":"user"} +{"id":"rel:accept_staff_invitation_emits_accepted","type":"Relation","subject":"action:accept_staff_invitation","predicate":"EMITS","object":"event:staff_invitation_accepted","evidence":"anchored","domain":"user"} {"id":"rel:update_user_emits_user_updated","type":"Relation","subject":"action:update_user","predicate":"EMITS","object":"event:user_updated","evidence":"anchored","domain":"user"} {"id":"rel:bind_wechat_emits_wx_bound","type":"Relation","subject":"action:bind_wechat","predicate":"EMITS","object":"event:wx_bound","evidence":"anchored","domain":"auth"} {"id":"entity:report_testimonial","type":"Entity","name":"ReportTestimonial","nameZh":"宠主寄语","domain":"report","fields":["id","report_id","store_id","content","is_public","create_time","update_time"],"systems":["Report Media Backend"],"code":"backend/src/main/java/com/petstore/entity/ReportTestimonial.java; service/ReportTestimonialService.java","doc":"docs/ontology/objects.md#entity:report_testimonial","test":"ReportTestimonialServiceTest","evidence":"anchored"} @@ -167,9 +167,9 @@ {"id":"action:get_report_suggestion","type":"Action","name":"getReportSuggestion","nameZh":"报告下次建议日期","inputs":["token"],"outputs":[],"entrypoints":["GET /api/report/{token}/suggestion","ReportLeadController#suggestion"],"appliesRules":["rule:BR-INT-001"],"code":"backend/src/main/java/com/petstore/controller/ReportLeadController.java#suggestion","evidence":"anchored","domain":"lead"} {"id":"action:submit_testimonial","type":"Action","name":"submitTestimonial","nameZh":"宠主寄语提交","inputs":["token","content","isPublic"],"outputs":["entity:report_testimonial"],"entrypoints":["POST /api/report/{token}/testimonial","ReportController#submitTestimonial"],"appliesRules":["rule:BR-TST-001"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#submitTestimonial","evidence":"anchored","domain":"report","test":"ReportTestimonialServiceTest"} {"id":"action:get_legacy_upload","type":"Action","name":"getLegacyUpload","nameZh":"兼容旧路径读取上传资源","inputs":["path"],"outputs":[],"entrypoints":["GET /api/upload/legacy/**","FileController#getLegacyImage"],"appliesRules":["rule:BR-UPLOAD-001"],"code":"backend/src/main/java/com/petstore/controller/FileController.java#getLegacyImage","evidence":"anchored","domain":"upload"} -{"id":"action:register_staff","type":"Action","name":"registerStaff","nameZh":"员工注册(邀请码)","inputs":["phone","password","name","inviteCode"],"outputs":["entity:user","entity:store","session_token"],"entrypoints":["POST /api/user/register-staff","UserController#registerStaff","UserService#registerStaff"],"appliesRules":["rule:BR-AUTH-001"],"code":"backend/src/main/java/com/petstore/controller/UserController.java#registerStaff","evidence":"anchored","domain":"user"} +{"id":"action:register_staff","type":"Action","name":"registerStaff","nameZh":"永久邀请码员工注册(停用)","inputs":["phone","password","name","inviteCode"],"outputs":[],"entrypoints":["POST /api/user/register-staff","UserController#registerStaff"],"appliesRules":["rule:BR-STORE-002"],"code":"backend/src/main/java/com/petstore/controller/UserController.java#registerStaff","evidence":"anchored","domain":"user","status":"deprecated","deprecated":true,"note":"固定返回 LEGACY_INVITE_DISABLED"} {"id":"action:send_sms","type":"Action","name":"sendSms","nameZh":"发送短信验证码","inputs":["phone"],"outputs":[],"entrypoints":["POST /api/sms/send","SmsController#send"],"appliesRules":["rule:BR-AUTH-002"],"code":"backend/src/main/java/com/petstore/controller/SmsController.java#send","evidence":"anchored","domain":"auth"} -{"id":"action:register_store","type":"Action","name":"registerStore","nameZh":"店铺注册(旧入口)","inputs":["entity:store"],"outputs":["entity:store"],"entrypoints":["POST /api/store/register","StoreController#register","StoreService#create"],"appliesRules":[],"code":"backend/src/main/java/com/petstore/controller/StoreController.java#register","evidence":"gap","domain":"store","status":"deprecated","deprecated":true,"doc":"deprecated:勿作为新集成入口"} +{"id":"action:register_store","type":"Action","name":"registerStore","nameZh":"店铺注册(旧入口停用)","inputs":["entity:store"],"outputs":[],"entrypoints":["POST /api/store/register","StoreController#register"],"appliesRules":[],"code":"backend/src/main/java/com/petstore/controller/StoreController.java#register","evidence":"anchored","domain":"store","status":"deprecated","deprecated":true,"doc":"固定返回 LEGACY_STORE_REGISTRATION_DISABLED"} {"id":"action:delete_appointment","type":"Action","name":"deleteAppointment","nameZh":"删除预约","inputs":["id"],"outputs":[],"entrypoints":["DELETE /api/appointment/delete","AppointmentController#delete"],"appliesRules":["rule:BR-APPT-002"],"code":"backend/src/main/java/com/petstore/controller/AppointmentController.java#delete","evidence":"anchored","domain":"appointment"} {"id":"action:list_reports","type":"Action","name":"listReports","nameZh":"报告列表","inputs":["page","pageSize"],"outputs":["entity:report"],"entrypoints":["GET /api/report/list","ReportController#list"],"appliesRules":["rule:BR-RPT-005"],"code":"backend/src/main/java/com/petstore/controller/ReportController.java#list","evidence":"anchored","domain":"report"} {"id":"action:wechat_authorize","type":"Action","name":"wechatAuthorize","nameZh":"获取微信授权跳转URL","inputs":[],"outputs":[],"entrypoints":["GET /api/wechat/authorize","WechatController#getAuthorizeUrl"],"appliesRules":[],"code":"backend/src/main/java/com/petstore/controller/WechatController.java#getAuthorizeUrl","evidence":"gap","domain":"auth","status":"deprecated","deprecated":true,"doc":"deprecated:勿作为新集成入口"} @@ -185,7 +185,7 @@ {"id":"entity:service_package","type":"Entity","name":"ServicePackage","nameZh":"套餐","domain":"growth","jpa":false,"fields":[],"systems":[],"code":"","doc":"docs/ontology/objects.md#待录入(P1/P2 方向,gap 证据)","test":"","evidence":"gap","status":"roadmap","note":"Phase A 明确不做;次卡/套餐属 C 期。"} {"id":"entity:report_history","type":"Entity","name":"ReportHistory","nameZh":"宠主历史报告汇总","domain":"report","jpa":false,"fields":[],"systems":[],"code":"","doc":"docs/ontology/objects.md#待录入(P1/P2 方向,gap 证据)","test":"","evidence":"gap","status":"roadmap","note":"P2 方向库占位,不展开字段"} {"id":"entity:analytics_dashboard","type":"Entity","name":"AnalyticsDashboard","nameZh":"经营分析看板","domain":"analytics","jpa":false,"fields":[],"systems":[],"code":"","doc":"docs/ontology/objects.md#待录入(P1/P2 方向,gap 证据)","test":"","evidence":"gap","status":"roadmap","note":"B 期经营看板;A 期工作台仅轻量指标卡,不展开本实体。"} -{"id":"rule:BR-USER-003","type":"Rule","name":"userSensitiveFieldsJsonIgnore","nameZh":"User 敏感字段不序列化","domain":"user","appliesTo":["action:login","action:wx_phone_login","action:get_user_info","action:update_user","action:register_boss","action:register_staff","action:list_staff"],"code":"backend/src/main/java/com/petstore/entity/User.java","doc":"docs/ontology/rules.md#rule:BR-USER-003","test":"UserJsonIgnoreTest","evidence":"anchored"} +{"id":"rule:BR-USER-003","type":"Rule","name":"userSensitiveFieldsJsonIgnore","nameZh":"User 敏感字段不序列化","domain":"user","appliesTo":["action:login","action:wx_phone_login","action:get_user_info","action:update_user","action:register_boss","action:accept_staff_invitation","action:list_staff"],"code":"backend/src/main/java/com/petstore/entity/User.java","doc":"docs/ontology/rules.md#rule:BR-USER-003","test":"UserJsonIgnoreTest","evidence":"anchored"} {"id":"rule:BR-STORE-003","type":"Rule","name":"publicStoreFieldTrim","nameZh":"公开门店字段裁剪","domain":"store","appliesTo":["action:get_store","action:list_stores"],"code":"backend/src/main/java/com/petstore/controller/StoreController.java#toPublicStoreView","doc":"docs/ontology/rules.md#rule:BR-STORE-003","test":"StoreControllerTest#publicGetHidesOwnerIdAndInviteCode; StoreControllerTest#publicListHidesOwnerIdAndInviteCode","evidence":"anchored","note":"公开 get/list 经 toPublicStoreView 裁剪,不含 ownerId/inviteCode/deleted"} {"id":"rule:BR-TST-001","type":"Rule","name":"oneReportOneTestimonial","nameZh":"一报告一寄语(重复提交更新)","domain":"report","appliesTo":["action:submit_testimonial"],"code":"backend/src/main/java/com/petstore/service/ReportTestimonialService.java","doc":"docs/ontology/rules.md#rule:BR-TST-001","test":"ReportTestimonialServiceTest","evidence":"anchored"} {"id":"rule:BR-UNSUB-001","type":"Rule","name":"unsubscribeByToken","nameZh":"凭 unsubscribeToken 一键退订","domain":"lead","appliesTo":["action:unsubscribe"],"code":"backend/src/main/java/com/petstore/controller/ReportLeadController.java#unsubscribe","doc":"docs/ontology/rules.md#rule:BR-UNSUB-001","test":"ReportLeadControllerTest","evidence":"anchored"} @@ -211,7 +211,7 @@ {"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-003","type":"Rule","name":"adminInternalFieldsAllowed","nameZh":"后台内部字段按用途最小返回","domain":"admin","appliesTo":["action:admin_list_reports","action:admin_update_settings","action:admin_list_service_customers","action:list_staff_invitations"],"code":"","doc":"docs/ontology/rules.md#rule:BR-ADMIN-003","test":"","evidence":"documented","note":"后台可返回报告内部状态;legacy inviteCode 不再返回。员工邀请列表仅返回脱敏手机号且不返回 token/hash,原始 token 仅创建响应返回一次。"} {"id":"rule:BR-ADMIN-004","type":"Rule","name":"adminPhaseAOutOfScope","nameZh":"Phase A 后台范围闸门","domain":"admin","appliesTo":["entity:admin_console","entity:workbench"],"code":"","doc":"docs/ontology/rules.md#rule:BR-ADMIN-004","test":"","evidence":"documented","note":"禁止将 membership/stored_value/service_package/收银/库存/寄养/押金/点餐/支付宝渠道 写入 Phase A 后台范围或设计稿。上述保持 evidence=gap。"} {"id":"action:view_workbench","type":"Action","name":"viewWorkbench","nameZh":"查看今日行动工作台","domain":"admin","inputs":["date","entity:current_user"],"outputs":["entity:workbench","entity:workbench_todo_item"],"entrypoints":["GET /api/admin/workbench/actions","GET /api/admin/workbench/report-funnel","Admin UI /workbench"],"appliesRules":["rule:BR-ADMIN-001","rule:BR-ADMIN-002","rule:BR-ADMIN-004","rule:BR-BE-001"],"code":"backend/src/main/java/com/petstore/controller/AdminBusinessEventController.java#actions; #reportFunnel; backend/src/main/java/com/petstore/service/AdminWorkbenchService.java; backend/src/main/java/com/petstore/service/AdminBusinessEventService.java; admin/src/views/WorkbenchView.vue","doc":"docs/ontology/actions.md#action:view_workbench","test":"AdminBusinessEventControllerTest; AdminWorkbenchServiceTest; AdminBusinessEventServiceTest","evidence":"anchored","note":"按会话 storeId 聚合完整本店行动与低敏预览;事件漏斗按日期汇总。"} {"id":"action:drilldown_workbench_todo","type":"Action","name":"drilldownWorkbenchTodo","nameZh":"工作台行动下钻","domain":"admin","inputs":["entity:workbench_todo_item"],"outputs":[],"entrypoints":["Admin UI workbench action click"],"appliesRules":["rule:BR-ADMIN-001","rule:BR-ADMIN-002"],"code":"admin/src/views/WorkbenchView.vue; admin/src/views/AppointmentsView.vue","doc":"docs/ontology/actions.md#action:drilldown_workbench_todo","evidence":"anchored","note":"按 kind 跳转 appointments/reports/leads 并带筛选;预约预览可按受保护 ID 详情打开。"} @@ -219,7 +219,7 @@ {"id":"action:admin_list_reports","type":"Action","name":"adminListReports","nameZh":"后台报告列表","domain":"admin","inputs":["dateFrom","dateTo","highlightStatus","page","pageSize"],"outputs":["entity:report","entity:highlight_video"],"entrypoints":["Admin UI /reports (planned)","reuses GET /api/report/list"],"appliesRules":["rule:BR-ADMIN-001","rule:BR-ADMIN-002","rule:BR-ADMIN-003","rule:BR-RPT-005"],"code":"backend/.../ReportController.java#list (reuse)","doc":"docs/ontology/actions.md#action:admin_list_reports","evidence":"documented","note":"含成片三态、复制公开链接、触发重生成(复用 generate_highlight)。"} {"id":"action:admin_list_leads","type":"Action","name":"adminListLeads","nameZh":"后台回访线索列表","domain":"admin","inputs":["remindStatus","remindDateTo","page","pageSize"],"outputs":["entity:report_lead"],"entrypoints":["Admin UI /leads (planned)","reuses GET /api/report/leads"],"appliesRules":["rule:BR-ADMIN-001","rule:BR-ADMIN-002","rule:BR-LEAD-002","rule:BR-LEAD-003"],"code":"backend/.../ReportLeadController.java#leads (reuse)","doc":"docs/ontology/actions.md#action:admin_list_leads","evidence":"documented"} {"id":"action:admin_list_service_customers","type":"Action","name":"adminListServiceCustomers","nameZh":"后台服务客户列表","domain":"admin","inputs":["source","hasPendingLead","lastVisitFrom","lastVisitTo","q","page","pageSize"],"outputs":["entity:store_customer"],"entrypoints":["GET /api/admin/service-customers","Admin UI /customers"],"appliesRules":["rule:BR-ADMIN-001","rule:BR-ADMIN-002","rule:BR-ADMIN-003","rule:BR-ADMIN-004","rule:BR-SC-001"],"code":"backend/src/main/java/com/petstore/controller/AdminServiceCustomerController.java#listServiceCustomers; backend/src/main/java/com/petstore/service/AdminServiceCustomerService.java","doc":"docs/ontology/actions.md#action:admin_list_service_customers; docs/门店管理后台-PhaseA-页面清单PRD.md §7.6","test":"AdminServiceCustomerControllerTest; AdminServiceCustomerServiceTest","evidence":"anchored","note":"以 t_store_customer 稳定主档为入口,动态聚合宠物/到店/报告/留资;返回 storeCustomerId 和脱敏手机号。"} -{"id":"action:admin_update_settings","type":"Action","name":"adminUpdateSettings","nameZh":"后台门店设置读写","domain":"admin","inputs":["entity:store","entity:service_type","entity:user"],"outputs":["entity:store","entity:service_type","entity:user"],"entrypoints":["GET /api/admin/store","Admin UI /settings","PUT /api/store/update"],"appliesRules":["rule:BR-ADMIN-001","rule:BR-ADMIN-002","rule:BR-ADMIN-003","rule:BR-STORE-001","rule:BR-ST-001","rule:BR-USER-001"],"code":"","doc":"docs/ontology/actions.md#action:admin_update_settings","evidence":"anchored","note":"后台读本店含 inviteCode(GET /api/admin/store);写资料复用 PUT /api/store/update(仅 boss);服务类型/员工复用既有 API。","code_refs":["backend/src/main/java/com/petstore/controller/AdminStoreController.java#currentStore","backend/src/main/java/com/petstore/controller/StoreController.java#update"]} +{"id":"action:admin_update_settings","type":"Action","name":"adminUpdateSettings","nameZh":"后台门店设置读写","domain":"admin","inputs":["entity:store","entity:service_type","entity:user"],"outputs":["entity:store","entity:service_type","entity:user","entity:audit_log"],"entrypoints":["GET /api/admin/store","Admin UI /settings","PUT /api/store/update"],"appliesRules":["rule:BR-ADMIN-001","rule:BR-ADMIN-002","rule:BR-ADMIN-003","rule:BR-STORE-001","rule:BR-ST-001","rule:BR-USER-001","rule:BR-AUDIT-001"],"code":"","doc":"docs/ontology/actions.md#action:admin_update_settings","evidence":"anchored","note":"后台不再读取 legacy inviteCode;写资料复用 PUT /api/store/update 并写低敏审计;员工改走限时邀请。","code_refs":["backend/src/main/java/com/petstore/controller/AdminStoreController.java#currentStore","backend/src/main/java/com/petstore/controller/StoreController.java#update"]} {"id":"action:admin_manage_schedule","type":"Action","name":"adminManageSchedule","nameZh":"后台排班与占用","domain":"admin","inputs":["date","entity:schedule_block"],"outputs":["entity:schedule_block","entity:appointment"],"entrypoints":["Admin UI /schedule","GET /api/schedule/day","POST /api/schedule/block","DELETE /api/schedule/block"],"appliesRules":["rule:BR-ADMIN-001","rule:BR-ADMIN-002","rule:BR-SCH-001","rule:BR-SCH-002","rule:BR-APPT-004"],"code":"","doc":"docs/ontology/actions.md#action:admin_manage_schedule","evidence":"anchored","note":"Web 排班日视图展示容量占用;增删带时长占用复用 ScheduleController。"} {"id":"event:workbench_viewed","type":"Event","name":"workbenchViewed","nameZh":"工作台已打开","domain":"admin","emittedBy":["action:view_workbench"],"payload":["storeId","userId","ts"],"code":"","doc":"docs/ontology/events.md#event:workbench_viewed","evidence":"gap","note":"可选埋点;A 期可不实现上报。"} {"id":"rel:admin_console_has_workbench","type":"Relation","subject":"entity:admin_console","predicate":"HAS","object":"entity:workbench","domain":"admin","evidence":"documented","cardinality":"1—1","doc":"登录默认工作台"} @@ -240,3 +240,19 @@ {"id":"rel:admin_list_leads_reads_lead","type":"Relation","subject":"action:admin_list_leads","predicate":"READS","object":"entity:report_lead","domain":"admin","evidence":"documented"} {"id":"rel:admin_list_customers_reads_sc","type":"Relation","subject":"action:admin_list_service_customers","predicate":"READS","object":"entity:store_customer","domain":"admin","evidence":"anchored"} {"id":"rel:drilldown_todo_to_list_appts","type":"Relation","subject":"action:drilldown_workbench_todo","predicate":"ROUTES_TO","object":"action:admin_list_appointments","domain":"admin","evidence":"anchored","doc":"kind=overdue_appointment|in_service|upcoming_today;按 ID 或服务端口径筛选"} +{"id":"entity:staff_invitation","type":"Entity","name":"StaffInvitation","nameZh":"一次性员工邀请","domain":"user","jpa":true,"fields":["id","invitation_id","token_hash","store_id","invited_name","invited_phone","status","expires_at","created_by_user_id","accepted_by_user_id","accepted_at","revoked_by_user_id","revoked_at","create_time","update_time"],"systems":["Backend Core","Store Miniapp FE","Store Admin FE"],"code":"backend/src/main/java/com/petstore/entity/StaffInvitation.java; backend/src/main/java/com/petstore/service/StaffInvitationService.java","doc":"docs/ontology/objects.md#entity:staff_invitation; docs/架构决策-门店开通与员工邀请-2026-08-02.md","test":"StaffInvitationServiceTest; StoreOnboardingMigrationTest","evidence":"anchored","note":"原始 token 只在创建响应返回一次;库内仅 SHA-256;列表/预览只返回 maskedPhone"} +{"id":"entity:audit_log","type":"Entity","name":"AuditLog","nameZh":"不可变操作审计","domain":"platform","jpa":true,"fields":["id","audit_id","store_id","actor_user_id","actor_role","action","target_type","target_id","outcome","metadata_json","occurred_at","create_time"],"systems":["Backend Core"],"code":"backend/src/main/java/com/petstore/entity/AuditLog.java; backend/src/main/java/com/petstore/service/AuditLogService.java","doc":"docs/ontology/objects.md#entity:audit_log; docs/架构决策-门店开通与员工邀请-2026-08-02.md","test":"AuditLogServiceTest; StoreOnboardingMigrationTest","evidence":"anchored","note":"只追加;metadata 禁止 phone/token/url/openid/unionid/password/secret/IP/地址坐标/原始请求"} +{"id":"action:legacy_register_boss_disabled","type":"Action","name":"legacyRegisterBossDisabled","nameZh":"旧老板注册停用","domain":"user","inputs":["legacyRequest"],"outputs":[],"entrypoints":["POST /api/user/register-boss","UserController#registerBoss"],"appliesRules":["rule:BR-ONBOARD-001"],"code":"backend/src/main/java/com/petstore/controller/UserController.java#registerBoss","evidence":"anchored","status":"deprecated","deprecated":true,"note":"固定返回 LEGACY_REGISTRATION_DISABLED"} +{"id":"action:get_store_onboarding","type":"Action","name":"getStoreOnboarding","nameZh":"查看门店开通清单","domain":"store","inputs":["entity:current_user"],"outputs":["status","steps","missingRequiredSteps","readyToComplete"],"entrypoints":["GET /api/admin/onboarding","AdminOnboardingController#status","StoreOnboardingService#getStatus"],"appliesRules":["rule:BR-AUTH-001","rule:BR-ONBOARD-001"],"code":"backend/src/main/java/com/petstore/controller/AdminOnboardingController.java#status; backend/src/main/java/com/petstore/service/StoreOnboardingService.java#getStatus; admin/src/views/SettingsView.vue; frontend/src/pages/mine/Onboarding.vue","test":"StoreOnboardingServiceTest","evidence":"anchored"} +{"id":"action:complete_store_onboarding","type":"Action","name":"completeStoreOnboarding","nameZh":"显式完成门店开通","domain":"store","inputs":["entity:current_user"],"outputs":["entity:store","entity:audit_log"],"entrypoints":["POST /api/admin/onboarding/complete","AdminOnboardingController#complete","StoreOnboardingService#complete"],"appliesRules":["rule:BR-AUTH-001","rule:BR-ONBOARD-001","rule:BR-AUDIT-001"],"code":"backend/src/main/java/com/petstore/controller/AdminOnboardingController.java#complete; backend/src/main/java/com/petstore/service/StoreOnboardingService.java#complete","test":"StoreOnboardingServiceTest","evidence":"anchored"} +{"id":"action:create_staff_invitation","type":"Action","name":"createStaffInvitation","nameZh":"创建一次性员工邀请","domain":"user","inputs":["name","phone","validDays","entity:current_user"],"outputs":["entity:staff_invitation","inviteToken","invitePath","entity:audit_log"],"entrypoints":["POST /api/admin/staff-invitations","StaffInvitationController#create","StaffInvitationService#create"],"appliesRules":["rule:BR-AUTH-001","rule:BR-USER-001","rule:BR-INVITE-001","rule:BR-AUDIT-001"],"code":"backend/src/main/java/com/petstore/controller/StaffInvitationController.java#create; backend/src/main/java/com/petstore/service/StaffInvitationService.java#create","test":"StaffInvitationServiceTest","evidence":"anchored","note":"inviteToken 仅当前创建响应出现一次"} +{"id":"action:list_staff_invitations","type":"Action","name":"listStaffInvitations","nameZh":"员工邀请记录","domain":"user","inputs":["entity:current_user"],"outputs":["entity:staff_invitation"],"entrypoints":["GET /api/admin/staff-invitations","StaffInvitationController#list","StaffInvitationService#list"],"appliesRules":["rule:BR-AUTH-001","rule:BR-USER-001","rule:BR-INVITE-001"],"code":"backend/src/main/java/com/petstore/controller/StaffInvitationController.java#list; backend/src/main/java/com/petstore/service/StaffInvitationService.java#list","test":"StaffInvitationServiceTest","evidence":"anchored","note":"不返回 invitedPhone、tokenHash 或原始 token"} +{"id":"action:revoke_staff_invitation","type":"Action","name":"revokeStaffInvitation","nameZh":"撤销员工邀请","domain":"user","inputs":["invitationId","entity:current_user"],"outputs":["entity:staff_invitation","entity:audit_log"],"entrypoints":["DELETE /api/admin/staff-invitations","StaffInvitationController#revoke","StaffInvitationService#revoke"],"appliesRules":["rule:BR-AUTH-001","rule:BR-USER-001","rule:BR-INVITE-001","rule:BR-AUDIT-001"],"code":"backend/src/main/java/com/petstore/controller/StaffInvitationController.java#revoke; backend/src/main/java/com/petstore/service/StaffInvitationService.java#revoke","test":"StaffInvitationServiceTest","evidence":"anchored"} +{"id":"action:preview_staff_invitation","type":"Action","name":"previewStaffInvitation","nameZh":"公开预览员工邀请","domain":"user","inputs":["token"],"outputs":["storeName","invitedName","maskedPhone","status","expiresAt","canAccept"],"entrypoints":["GET /api/staff-invitations/preview","StaffInvitationController#preview","StaffInvitationService#preview"],"appliesRules":["rule:BR-INVITE-001"],"code":"backend/src/main/java/com/petstore/controller/StaffInvitationController.java#preview; backend/src/main/java/com/petstore/service/StaffInvitationService.java#preview","test":"StaffInvitationServiceTest","evidence":"anchored"} +{"id":"action:accept_staff_invitation","type":"Action","name":"acceptStaffInvitation","nameZh":"微信核验并接受员工邀请","domain":"user","inputs":["token","phoneCode","loginCode"],"outputs":["entity:user","entity:store","session_token","entity:audit_log"],"entrypoints":["POST /api/staff-invitations/accept","StaffInvitationController#accept","StaffInvitationService#acceptVerified"],"appliesRules":["rule:BR-INVITE-001","rule:BR-AUDIT-001","rule:BR-USER-003"],"code":"backend/src/main/java/com/petstore/controller/StaffInvitationController.java#accept; backend/src/main/java/com/petstore/service/StaffInvitationService.java#acceptVerified; frontend/src/pages/login/Login.vue","test":"StaffInvitationServiceTest","evidence":"anchored"} +{"id":"rule:BR-ONBOARD-001","type":"Rule","name":"verifiedMerchantOnboarding","nameZh":"微信核验入驻与显式开通","domain":"store","appliesTo":["action:register_boss","action:legacy_register_boss_disabled","action:get_store_onboarding","action:complete_store_onboarding"],"code":"backend/src/main/java/com/petstore/service/MerchantOnboardingService.java; backend/src/main/java/com/petstore/service/StoreOnboardingService.java","doc":"docs/ontology/rules.md#rule:BR-ONBOARD-001","test":"StoreOnboardingServiceTest","evidence":"anchored","note":"新老板手机号由微信核验;完成开通要求门店资料、预约容量、至少一个服务项目,邀请员工为选填;历史状态 unknown 不推断"} +{"id":"rule:BR-INVITE-001","type":"Rule","name":"oneTimeStaffInvitation","nameZh":"一次性员工邀请安全边界","domain":"user","appliesTo":["action:create_staff_invitation","action:list_staff_invitations","action:revoke_staff_invitation","action:preview_staff_invitation","action:accept_staff_invitation"],"code":"backend/src/main/java/com/petstore/service/StaffInvitationService.java; backend/src/main/java/com/petstore/controller/StaffInvitationController.java","doc":"docs/ontology/rules.md#rule:BR-INVITE-001","test":"StaffInvitationServiceTest; StoreOnboardingMigrationTest","evidence":"anchored","note":"256-bit token 原文只返回一次,库内 SHA-256;手机号绑定且由微信核验;1-30 天、一次使用、可撤销;账号/微信冲突拒绝,不升级 customer 角色"} +{"id":"rule:BR-AUDIT-001","type":"Rule","name":"immutableLowSensitivityAudit","nameZh":"不可变低敏操作审计","domain":"platform","appliesTo":["action:register_boss","action:update_store","action:complete_store_onboarding","action:create_staff_invitation","action:revoke_staff_invitation","action:accept_staff_invitation","action:delete_staff"],"code":"backend/src/main/java/com/petstore/service/AuditLogService.java","doc":"docs/ontology/rules.md#rule:BR-AUDIT-001","test":"AuditLogServiceTest; StoreOnboardingMigrationTest","evidence":"anchored","note":"只追加;metadata 仅服务端白名单标量且禁止 PII、凭证、地址坐标和原始请求"} +{"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"} diff --git a/ontology/actions.md b/ontology/actions.md index 3d1f096..36ef058 100644 --- a/ontology/actions.md +++ b/ontology/actions.md @@ -6,13 +6,13 @@ ### action:register_boss -老板入驻:创建 `Store` + `boss` `User`,签发 `session_token`。 +老板入驻:微信核验手机号后,在同一事务创建 `Store` + `boss` `User`,签发 `session_token`,写 `store_registered` BusinessEvent 与 AuditLog。 -- **入口**:`POST /api/user/register-boss` → `UserService#registerBoss`(公开,在 AuthInterceptor allowlist) -- **输入**:`storeName`、`bossName`、`phone`、`password` -- **输出**:`entity:store`、`entity:user`、`session_token` -- **适用规则**:`rule:BR-AUTH-001`(session token 签发) -- **代码**:`backend/src/main/java/com/petstore/service/UserService.java#registerBoss` +- **入口**:`POST /api/onboarding/register-boss` → `MerchantOnboardingController#registerBoss` → `MerchantOnboardingService#registerVerifiedBoss`(公开) +- **输入**:`storeName`、`bossName`、微信 `phoneCode`、可选 `loginCode`;不接受手填手机号或明文密码 +- **输出**:`entity:store`、`entity:user`、`session_token`、`entity:business_event`、`entity:audit_log` +- **适用规则**:`rule:BR-ONBOARD-001`、`rule:BR-AUDIT-001` +- **代码**:`backend/src/main/java/com/petstore/service/MerchantOnboardingService.java` - **证据**:`anchored` ### action:update_store @@ -39,10 +39,10 @@ ### action:get_store_by_invite_code -按邀请码查门店:仅 boss/staff 且邀请码对应门店必须为当前用户所属门店。 +legacy 永久邀请码查询入口已停用,固定返回 `410 / LEGACY_INVITE_DISABLED`,不再查询或返回门店。 - **入口**:`GET /api/store/invite-code` → `StoreController#getByInviteCode` -- **适用规则**:`rule:BR-STORE-002`(邀请码同店校验) +- **适用规则**:`rule:BR-STORE-002`(legacy 禁用) - **代码**:`backend/src/main/java/com/petstore/controller/StoreController.java#getByInviteCode` - **测试**:`StoreControllerTest` - **证据**:`anchored` @@ -388,9 +388,9 @@ ### action:create_staff -老板创建员工:仅 boss;`storeId=current.storeId`。 +旧“直接创建员工”入口已停用,固定返回 `DIRECT_STAFF_CREATION_DISABLED`;新员工必须由老板创建限时邀请后本人接受。 -- **入口**:`POST /api/user/create-staff` → `UserController#createStaff` → `UserService#createStaff` +- **入口**:`POST /api/user/create-staff` → `UserController#createStaff`(deprecated) - **适用规则**:`rule:BR-USER-001`(仅 boss) - **代码**:`backend/src/main/java/com/petstore/controller/UserController.java#createStaff` - **证据**:`anchored` @@ -406,7 +406,7 @@ ### action:delete_staff -删除员工:仅 boss;目标员工必须属于本店。 +删除员工:仅 boss;目标必须是本店 `staff`,不能删除 boss。删除后活跃账号校验会立即让旧 session token 失效,并写低敏 AuditLog。 - **入口**:`DELETE /api/user/staff` → `UserController#deleteStaff` - **适用规则**:`rule:BR-USER-001` @@ -490,11 +490,10 @@ ### action:register_staff -员工注册(邀请码方式):公开接口,凭邀请码加入对应门店。 +永久邀请码员工注册已停用,固定返回 `410 / LEGACY_INVITE_DISABLED`。 -- **入口**:`POST /api/user/register-staff` → `UserController#registerStaff` → `UserService#registerStaff`(公开) -- **输出**:`entity:user`(staff)、`entity:store`、`session_token` -- **适用规则**:`rule:BR-AUTH-001` +- **入口**:`POST /api/user/register-staff` → `UserController#registerStaff`(公开、deprecated) +- **适用规则**:`rule:BR-STORE-002` - **代码**:`backend/src/main/java/com/petstore/controller/UserController.java#registerStaff` - **证据**:`anchored` @@ -509,13 +508,32 @@ ### action:register_store -店铺注册(旧入口):公开接口,创建 Store。**deprecated**:与 `action:register_boss` 重复,勿作为新集成入口。 +店铺注册旧入口固定返回 `LEGACY_STORE_REGISTRATION_DISABLED`,不创建 Store。 -- **入口**:`POST /api/store/register` → `StoreController#register` → `StoreService#create` -- **输出**:`entity:store` +- **入口**:`POST /api/store/register` → `StoreController#register` - **状态**:`deprecated` - **代码**:`backend/src/main/java/com/petstore/controller/StoreController.java#register` -- **证据**:`gap`(与 `register_boss` 重复,建议废弃或合并) +- **证据**:`anchored` + +## 门店开通与员工邀请(Phase 0) + +### action:get_store_onboarding / action:complete_store_onboarding + +- `GET /api/admin/onboarding`:boss/staff 查看 4 步清单;必填为门店资料、预约容量、至少一个服务项目,邀请员工为选填。 +- `POST /api/admin/onboarding/complete`:仅 boss;必填项全部满足才写 `completed_at/by` 与 AuditLog;重复确认幂等。 +- 历史门店迁移为 `unknown`,不推测已经完成。 + +### action:create_staff_invitation / action:list_staff_invitations / action:revoke_staff_invitation + +- `POST /api/admin/staff-invitations`:仅 boss;按姓名、本人手机号、1~30 天有效期创建。返回原始 `inviteToken` 和小程序 `invitePath` **一次**。 +- `GET /api/admin/staff-invitations`:仅 boss;只返回脱敏手机号和状态,不返回原始 token/hash。 +- `DELETE /api/admin/staff-invitations?invitationId=`:仅 boss + 同店;已接受不能撤销,重复撤销幂等。 + +### action:preview_staff_invitation / action:accept_staff_invitation + +- `GET /api/staff-invitations/preview?token=`:公开预览门店、受邀姓名、脱敏手机号、状态和有效期。 +- `POST /api/staff-invitations/accept`:公开但必须提交微信 `phoneCode`;服务端换得的手机号必须与邀请绑定手机号一致。成功后创建 staff、消费邀请、写 AuditLog 并签发新 session。 +- 已存在 User、openid 或 unionid 冲突时拒绝;本期不把 customer 静默升级为 staff,避免单角色模型混淆。 ## 删除/列表类(protected) diff --git a/ontology/events.md b/ontology/events.md index d6f486c..479abaf 100644 --- a/ontology/events.md +++ b/ontology/events.md @@ -6,10 +6,11 @@ ### event:store_registered -老板入驻成功,`Store` + `boss` `User` 创建完成,`session_token` 签发。 +微信核验手机号后的老板入驻成功,`Store` + `boss` `User` 创建完成,`session_token` 签发;同时以 BusinessEvent 和 AuditLog 持久化。 - **触发动作**:`action:register_boss` -- **payload**:`storeId`、`bossUserId`、`inviteCode` +- **payload**:`storeId`、`bossUserId`、`occurredAt`;不含手机号或 legacy inviteCode +- **代码**:`BusinessEventService#recordStoreRegistered`、`MerchantOnboardingService#registerVerifiedBoss` - **证据**:`anchored` ## User / 鉴权相关 @@ -18,7 +19,7 @@ 登录/注册成功后签发 HMAC session token。 -- **触发动作**:`action:register_boss`、`action:login`、`action:wx_phone_login`、`register_staff` +- **触发动作**:`action:register_boss`、`action:login`、`action:wx_phone_login`、`action:accept_staff_invitation` - **payload**:`userId`、`storeId`、`role`、`exp` - **代码**:`backend/src/main/java/com/petstore/auth/SessionTokenService.java#issue` - **测试**:`SessionTokenServiceTest` @@ -244,13 +245,14 @@ - **代码**:`backend/src/main/java/com/petstore/service/PetService.java#create` - **证据**:`anchored` -### event:staff_created +### event:staff_invitation_accepted -老板创建员工。 +受邀员工通过微信手机号核验后首次接受邀请并创建 staff 账号;以 AuditLog 持久化。 -- **触发动作**:`action:create_staff` -- **payload**:`staffId`、`storeId`、`name`、`phone` -- **代码**:`backend/src/main/java/com/petstore/service/UserService.java#createStaff` +- **触发动作**:`action:accept_staff_invitation` +- **payload**:`storeId`、`staffUserId`、`invitationId`、`verificationMethod`、`occurredAt`;不含手机号、token 或微信标识 +- **代码**:`StaffInvitationService#acceptVerified`、`AuditLogService#record` +- **测试**:`StaffInvitationServiceTest` - **证据**:`anchored` ### event:user_updated diff --git a/ontology/objects.md b/ontology/objects.md index 817e416..e5c9a07 100644 --- a/ontology/objects.md +++ b/ontology/objects.md @@ -1,6 +1,6 @@ # Objects(实体) -> 持久名词 + 字段 + 归属系统。本批覆盖 P0 核心闭环的 5 个实体。 +> 持久名词 + 字段 + 归属系统。以 `graph/ontology.jsonl` 为机器可审计真源。 ## entity:store @@ -17,10 +17,13 @@ | longitude | Double | 经度(公开报告页不返回) | | intro | TEXT | 简介 | | owner_id | BIGINT | 老板用户 ID | -| invite_code | VARCHAR(8) | 员工邀请码 | +| invite_code | VARCHAR(8) | legacy 兼容字段;不返回客户端、不再作为员工注册凭证 | | booking_day_start | LocalTime | 预约首号 | | booking_last_slot_start | LocalTime | 预约末号 | | booking_capacity | INT | 门店承诺的并发接待数(1~10) | +| onboarding_status | VARCHAR(16) | unknown(历史不可推断)/ in_progress / completed | +| onboarding_completed_at | DATETIME | 老板显式完成开通时间 | +| onboarding_completed_by_user_id | BIGINT | 显式完成开通的老板 | | create_time / update_time | DATETIME | 时间戳 | | deleted | TINYINT(1) | 软删 | @@ -30,6 +33,48 @@ - **测试**:`StoreControllerTest` - **证据**:`anchored` +## entity:staff_invitation + +**一次性员工邀请**。老板按员工本人手机号创建,1~30 天有效、可撤销且只能接受一次。 + +| 字段 | 类型 | 说明 | +|------|------|------| +| invitation_id | VARCHAR(36) | 对外随机 ID,管理端撤销使用 | +| token_hash | VARCHAR(64) | 256-bit 原始 token 的 SHA-256;原始 token 不落库 | +| store_id | BIGINT | 受邀门店 | +| invited_name / invited_phone | VARCHAR | 受邀姓名 / 服务端核验手机号;API 只返回脱敏手机号 | +| status | VARCHAR(16) | pending / accepted / revoked / expired | +| expires_at | DATETIME | 到期时间 | +| created_by_user_id | BIGINT | 创建邀请的老板 | +| accepted_by_user_id / accepted_at | BIGINT / DATETIME | 首次成功接受回执 | +| revoked_by_user_id / revoked_at | BIGINT / DATETIME | 撤销回执 | +| create_time / update_time | DATETIME | 时间戳 | + +- **安全边界**:原始 token 仅在创建响应返回一次;列表/预览不返回 `token_hash`、原始 token 或完整手机号。 +- **代码**:`backend/src/main/java/com/petstore/entity/StaffInvitation.java`、`service/StaffInvitationService.java` +- **测试**:`StaffInvitationServiceTest`、`StoreOnboardingMigrationTest` +- **证据**:`anchored` + +## entity:audit_log + +**不可变操作审计**。记录开店、门店配置和员工权限类操作,与可统计经营事实 `BusinessEvent` 分开。 + +| 字段 | 类型 | 说明 | +|------|------|------| +| audit_id | VARCHAR(36) | 随机审计 ID | +| store_id | BIGINT | 强制门店范围 | +| actor_user_id / actor_role | BIGINT / VARCHAR | 操作人快照 | +| action | VARCHAR(64) | 受控动作名 | +| target_type / target_id | VARCHAR | 目标类型 / 安全引用 ID | +| outcome | VARCHAR(16) | success 等受控结果 | +| metadata_json | TEXT | 服务端白名单低敏标量 JSON | +| occurred_at / create_time | DATETIME | 发生 / 入库时间 | + +- **禁止字段**:手机号、token/URL、openid/unionid、密码/密钥、IP、地址/坐标、备注/内容和原始请求体。 +- **代码**:`backend/src/main/java/com/petstore/entity/AuditLog.java`、`service/AuditLogService.java` +- **测试**:`AuditLogServiceTest`、`StoreOnboardingMigrationTest` +- **证据**:`anchored` + ## entity:user **用户**。三角色:`boss`(老板)/ `staff`(员工)/ `customer`(宠主)。 diff --git a/ontology/relations.md b/ontology/relations.md index e3e59c2..c1255b8 100644 --- a/ontology/relations.md +++ b/ontology/relations.md @@ -18,6 +18,8 @@ | `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` | +| `Store` 1—N `StaffInvitation` | 一个门店可签发多次限时邀请 | `StaffInvitation.store_id` → `Store.id` | +| `Store` 1—N `AuditLog` | 安全/配置审计强制归属门店 | `AuditLog.store_id` → `Store.id` | ## 动作—事件(触发) @@ -33,6 +35,7 @@ | `action:confirm_report_sent` | `event:report_sent`(仅首次确认) | | `action:track_report_open` | `event:report_opened` / `event:report_reopened` | | `action:submit_lead` | `event:lead_submitted` | +| `action:accept_staff_invitation` | `event:staff_invitation_accepted`、`event:session_issued` | ## 规则—动作(适用) @@ -41,7 +44,7 @@ | `rule:BR-AUTH-001` | 所有 protected 接口 | | `rule:BR-AUTH-002` | `action:login`、`action:wx_phone_login` | | `rule:BR-STORE-001` | `action:update_store`、`action:delete_store` | -| `rule:BR-STORE-002` | `action:get_store_by_invite_code` | +| `rule:BR-STORE-002` | legacy 永久邀请码查询/注册停用入口 | | `rule:BR-APPT-001` | `action:start_service`、`action:transition_appointment_status`、`action:submit_report` | | `rule:BR-APPT-002` | `action:list_appointments`、`action:start_service`、`action:transition_appointment_status`、`action:get_appointment_detail`、`delete` | | `rule:BR-APPT-003` | `action:get_appointment_detail`、`action:transition_appointment_status`(仅 cancel) | @@ -96,7 +99,7 @@ | `action:create_service_type` | `event:service_type_created` | | `action:delete_service_type` | `event:service_type_deleted` | | `action:create_pet` | `event:pet_created` | -| `action:create_staff` | `event:staff_created` | +| `action:accept_staff_invitation` | `event:staff_invitation_accepted` | | `action:update_user` | `event:user_updated` | | `action:bind_wechat` | `event:wx_bound`(已在 Batch 1 录入) | | `action:submit_testimonial` | `event:testimonial_saved` | @@ -115,7 +118,10 @@ | `rule:BR-SCH-001` | `action:day_agenda`、`action:create_schedule_block`、`action:delete_schedule_block` | | `rule:BR-ST-001` | `action:create_service_type`、`action:update_service_type`、`action:delete_service_type`、`action:init_service_types` | | `rule:BR-PET-001` | `action:list_pets`、`action:create_pet`、`action:update_pet`、`action:delete_pet`、`action:get_pet_history` | -| `rule:BR-USER-001` | `action:create_staff`、`action:list_staff`、`action:delete_staff` | +| `rule:BR-USER-001` | 员工列表/删除与邀请创建/列表/撤销 | +| `rule:BR-ONBOARD-001` | 老板入驻、查看/完成开通清单 | +| `rule:BR-INVITE-001` | 员工邀请创建/列表/撤销/预览/接受 | +| `rule:BR-AUDIT-001` | 门店和员工权限类敏感操作 | | `rule:BR-USER-002` | `action:update_user`、`action:get_user_info` | | `rule:BR-USER-003` | 返回 `User` 的登录/注册/信息类动作 | | `rule:BR-STORE-003` | `action:get_store`、`action:list_stores` | diff --git a/ontology/rules.md b/ontology/rules.md index 256a4fe..efec688 100644 --- a/ontology/rules.md +++ b/ontology/rules.md @@ -6,11 +6,11 @@ ### rule:BR-AUTH-001 -**HMAC session token 鉴权**:protected `/api/**` 要求 `Authorization: Bearer `;token 格式 `base64url(payload).base64url(sig)`,HMAC-SHA256,默认 7 天有效期;public allowlist 见 `AuthInterceptor.PUBLIC_PATTERNS`。 +**HMAC session token + 活跃账号范围鉴权**:protected `/api/**` 要求 `Authorization: Bearer `;token 为 HMAC-SHA256、默认 7 天有效期。签名通过后仍须读取活跃 User,确认账号未删除且 `role/storeId` 与 token 快照一致;boss/staff 还须确认所属 Store 仍为有效门店。删员工、停用门店或权限范围变化会立即令旧 token 失效。public allowlist 见 `AuthInterceptor.PUBLIC_PATTERNS`。 - **适用动作**:所有 protected 接口 - **代码**:`backend/src/main/java/com/petstore/auth/AuthInterceptor.java`、`SessionTokenService.java`、`CurrentUserContext.java` -- **测试**:`SessionTokenServiceTest` +- **测试**:`SessionTokenServiceTest`、`AuthInterceptorActiveUserTest` - **证据**:`anchored` ### rule:BR-AUTH-002 @@ -35,11 +35,11 @@ ### rule:BR-STORE-002 -**邀请码查询仅 boss/staff + 同店**:按 code 查到的 store.id 必须等于 `current.storeId`;新员工注册预览走公开 `/api/user/register-staff`(请求体含 inviteCode)。 +**legacy 永久邀请码禁用**:`invite_code` 仅保留数据库兼容,不返回客户端、不再作为员工注册凭证;`GET /api/store/invite-code` 与 `POST /api/user/register-staff` 固定返回停用业务码,不查询门店或创建账号。 -- **适用动作**:`action:get_store_by_invite_code` -- **代码**:`backend/src/main/java/com/petstore/controller/StoreController.java#getByInviteCode` -- **测试**:`StoreControllerTest#inviteCodeCrossStoreForbidden` +- **适用动作**:`action:get_store_by_invite_code`、`action:register_staff` +- **代码**:`StoreController#getByInviteCode`、`UserController#registerStaff` +- **测试**:`StoreControllerTest` - **证据**:`anchored` ### rule:BR-STORE-003 @@ -280,10 +280,11 @@ ### rule:BR-USER-001 -**员工管理仅 boss + 同店**:`create_staff`/`list_staff`/`delete_staff` 仅 `isBoss()`;`storeId=current.storeId`;`delete_staff` 校验目标员工属于本店。 +**员工管理与邀请仅 boss + 同店**:门店成员可查看本店成员列表;只有 boss 可创建/查看/撤销本店邀请并删除本店 `staff`。不能删除 boss,不能直接创建员工账号;员工须本人接受邀请。 -- **适用动作**:`action:create_staff`、`action:list_staff`、`action:delete_staff` -- **代码**:`backend/src/main/java/com/petstore/controller/UserController.java` +- **适用动作**:`action:create_staff`、`action:list_staff`、`action:delete_staff`、`action:create_staff_invitation`、`action:list_staff_invitations`、`action:revoke_staff_invitation` +- **代码**:`UserController.java`、`StaffInvitationController.java` +- **测试**:`StaffInvitationServiceTest` - **证据**:`anchored` ### rule:BR-USER-002 @@ -298,11 +299,38 @@ **User 敏感字段不序列化**:`password`、`wechatOpenid`、`wechatUnionid` 必须 `@JsonIgnore`,禁止随 API JSON 返回。 -- **适用动作**:`action:login`、`action:wx_phone_login`、`action:get_user_info`、`action:update_user`、`action:register_boss`、`action:register_staff`、`action:list_staff` +- **适用动作**:`action:login`、`action:wx_phone_login`、`action:get_user_info`、`action:update_user`、`action:register_boss`、`action:accept_staff_invitation`、`action:list_staff` - **代码**:`backend/src/main/java/com/petstore/entity/User.java` - **测试**:`UserJsonIgnoreTest` - **证据**:`anchored` +### rule:BR-ONBOARD-001 + +**微信核验入驻与显式开通**:新老板只能用微信 `phoneCode` 核验手机号后入驻,不接收手填手机号或明文密码。历史门店开通状态迁移为 `unknown`,不得推测。完成开通必须满足:门店名称/电话/地址完整、预约首末时段与并发容量合法、至少一个服务项目;邀请员工为选填,支持单人门店。 + +- **适用动作**:`action:register_boss`、`action:get_store_onboarding`、`action:complete_store_onboarding` +- **代码**:`MerchantOnboardingService.java`、`StoreOnboardingService.java` +- **测试**:`StoreOnboardingServiceTest`、`StoreOnboardingMigrationTest` +- **证据**:`anchored` + +### rule:BR-INVITE-001 + +**一次性员工邀请安全边界**:邀请 token 为 256-bit 随机值,原文只在创建响应出现一次,数据库只存 SHA-256;邀请绑定手机号、1~30 天有效、可撤销且仅首次接受有效。接受时必须由微信换得的手机号精确匹配。已有 User/openid/unionid 冲突均拒绝,本期不把 customer 静默升级为 staff。 + +- **适用动作**:创建、列表、撤销、预览、接受员工邀请 +- **代码**:`StaffInvitationService.java`、`StaffInvitationController.java` +- **测试**:`StaffInvitationServiceTest`、`StoreOnboardingMigrationTest` +- **证据**:`anchored` + +### rule:BR-AUDIT-001 + +**不可变低敏操作审计**:AuditLog 只追加,强制 `storeId`、受控 action/target/outcome。metadata 只允许服务端白名单标量,禁止手机号、token/URL、openid/unionid、密码/密钥、IP、地址/坐标、备注/内容和原始请求体。 + +- **适用动作**:门店入驻/完成开通/更新设置、员工邀请创建/撤销/接受、删除员工 +- **代码**:`AuditLogService.java` +- **测试**:`AuditLogServiceTest`、`StoreOnboardingMigrationTest` +- **证据**:`anchored` + ## Config ### rule:BR-CONFIG-001 @@ -326,7 +354,7 @@ ### rule:BR-CONFIG-003 -**生产发布只读门禁**:正式切流前必须按固定顺序完成备份和五个版本化迁移,再以最终生产配置运行 JPA schema `validate` 与 18 项只读数据不变量检查。运行期 readiness 必须同时覆盖数据库、磁盘、上传目录和 FFmpeg/ffprobe;上线自动 smoke 只允许 GET,不创建或修改业务数据。 +**生产发布只读门禁**:正式切流前必须按固定顺序完成备份和六个版本化迁移,再以最终生产配置运行 JPA schema `validate` 与 24 项只读数据不变量检查。运行期 readiness 必须同时覆盖数据库、磁盘、上传目录和 FFmpeg/ffprobe;上线自动 smoke 只允许 GET,不创建或修改业务数据。 - **适用动作**:—(发布全局约束) - **代码**:`backend/src/main/java/com/petstore/config/ProductionDatabasePreflightRunner.java`、`PetstoreRuntimeHealthIndicator.java`、`backend/deploy/release-preflight.sh`、`production-smoke.sh` @@ -382,9 +410,9 @@ ### rule:BR-ADMIN-003 -**后台可看内部字段**:经鉴权后可返回公开 API 裁剪掉的字段(如 inviteCode、报告内部字段);与 `rule:BR-STORE-003` / `rule:BR-RPT-006` 对称。手机号默认仍脱敏,明文权限预留。 +**后台内部字段按用途最小返回**:经鉴权后可返回报告内部状态等必要字段;legacy `inviteCode` 不再返回。邀请列表仅返回脱敏手机号且不返回 token/hash,原始 token 只在创建响应返回一次。 -- **适用动作**:`action:admin_list_reports`、`action:admin_update_settings`、`action:admin_list_service_customers` +- **适用动作**:`action:admin_list_reports`、`action:admin_update_settings`、`action:admin_list_service_customers`、`action:list_staff_invitations` - **证据**:`documented` ### rule:BR-ADMIN-004 diff --git a/qa-reports/phase0-store-onboarding-staff-invitation-2026-08-02.md b/qa-reports/phase0-store-onboarding-staff-invitation-2026-08-02.md new file mode 100644 index 0000000..16ab6bc --- /dev/null +++ b/qa-reports/phase0-store-onboarding-staff-invitation-2026-08-02.md @@ -0,0 +1,59 @@ +# Phase 0 门店开通与员工邀请验收记录 + +> 日期:2026-08-02 +> 状态:Code Complete / Ready for Release,待生产迁移与微信真机验收 +> 生产数据库与服务器:本批未触碰 + +## 1. 自动验证 + +| 检查 | 结果 | +|------|------| +| Backend tests | PASS(193 tests,0 failure/error/skip) | +| Backend package | PASS | +| Admin production build | PASS | +| H5 build | PASS | +| mp-weixin build + app-json verification | PASS | +| Ontology validation/drift | PASS(258 条;65/65 HTTP、15/15 JPA、0 drift) | +| Release shell syntax | PASS(`backend/deploy/*.sh`) | + +## 2. 自动化覆盖 + +- token 原文与数据库 hash 分离、手机号脱敏; +- 邀请手机号不匹配不创建账号; +- 接受成功创建 staff、消费邀请、写低敏 AuditLog; +- 开通必填项缺失不能完成,单人门店可完成; +- 已完成开通的门店不能再清空名称、电话或地址; +- 老板微信核验注册不产生可登录密码,并同时写入 `store_registered` BusinessEvent 与 AuditLog; +- Audit metadata 拒绝 token/手机号; +- 门店/角色/删除状态变化会立即让旧 session 失效; +- Nginx Petstore 专用 access log 只记录 `$uri`,不记录携带报告/邀请 token 的 query; +- 第六个迁移包含 token hash、状态机与六项回执不变量。 + +## 3. 三角色真机验收(发布前必做) + +### Boss + +- [ ] 新微信手机号创建门店,无手填手机号/明文密码;自动登录后进入开通清单。 +- [ ] 缺地址时不能完成;补齐资料、确认容量和服务后可完成;单人门店无需先邀请员工。 +- [ ] 创建 3/7/14/30 天邀请,创建后只显示一次原始口令;可分享小程序卡片。 +- [ ] 邀请列表只有脱敏手机号;pending 可撤销,accepted 不能撤销。 + +### Staff + +- [ ] 从卡片进入看到正确门店、姓名、脱敏手机号与有效期。 +- [ ] 不匹配微信手机号被拒绝且不创建账号;匹配手机号成功加入并直接获得 staff session。 +- [ ] 邀请重复打开幂等登录同一 staff;过期/撤销邀请有明确提示。 +- [ ] 被老板删除后,已有页面下一次受保护请求立即 401 并要求重新登录。 + +### Customer / 权限反例 + +- [ ] customer 不能查看/创建/撤销门店邀请,也不能完成开通。 +- [ ] 已有 customer 手机号接受员工邀请明确返回账号冲突,不静默改变角色。 +- [ ] legacy 永久邀请码查询/注册、直接创建员工、旧老板/门店注册入口均返回明确停用码且无写入。 + +## 4. 发布门禁 + +- [ ] 按固定顺序执行第六个迁移 `20260802_create_store_onboarding.sql`,六项验证均为 0。 +- [ ] 以最终生产配置运行 JPA `validate` + 24 项只读不变量预检。 +- [ ] 微信体验版双微信账号完成 Boss → 分享 → Staff 接受全链路;证据不包含完整手机号或 token。 +- [ ] 旧版本回滚已确认忽略新增表/列;不执行 down migration。 diff --git a/qa-reports/scripts/admin-phase-a-api-smoke.sh b/qa-reports/scripts/admin-phase-a-api-smoke.sh index c7baa9e..934780c 100755 --- a/qa-reports/scripts/admin-phase-a-api-smoke.sh +++ b/qa-reports/scripts/admin-phase-a-api-smoke.sh @@ -48,7 +48,7 @@ auth_get() { curl -sf -H "$AUTH" "$@"; } auth_post() { curl -sf -H "$AUTH" -H 'Content-Type: application/json' -X POST "$@"; } step "2) customer 门禁:用 customer 应 403(若有)— 跳过;校验 admin store" -auth_get "$BASE/admin/store" | python3 -c 'import json,sys; d=json.load(sys.stdin); assert d.get("code")==200, d; assert "inviteCode" in d["data"]; print("store", d["data"].get("name"), "invite", d["data"].get("inviteCode"))' +auth_get "$BASE/admin/store" | python3 -c 'import json,sys; d=json.load(sys.stdin); assert d.get("code")==200, d; assert "inviteCode" not in d["data"]; assert "onboardingStatus" in d["data"]; print("store", d["data"].get("name"), "onboarding", d["data"].get("onboardingStatus"))' step "3) 预约列表(含 admin enrichment)" auth_get "$BASE/appointment/list?page=1&pageSize=20" | python3 -c ' diff --git a/产品设计文档.md b/产品设计文档.md index c1bf8f2..3d69d99 100644 --- a/产品设计文档.md +++ b/产品设计文档.md @@ -2,8 +2,8 @@ > **本体对齐**:本文档与 [`ontology/objects.md`](./ontology/objects.md) 对齐(`entity:store`/`entity:user`/`entity:appointment`/`entity:report`),规则见 [`ontology/rules.md`](./ontology/rules.md)。冲突以本体为准。 > -> 版本:v1.9 -> 日期:2026-04-18(v1.9:对外品牌统一为「宠小它」及主/副 Slogan;v1.8:成片「可变时长 + 双编排」与 §5.3 对齐专文 §0) +> 版本:v2.0 +> 日期:2026-08-02(v2.0:面向 3~5 家独立单店真实试点,补齐门店开通、一次性员工邀请与审计边界) > **品牌**:宠小它 · **主 Slogan**:用心宠小它,温暖伴一生 · **副 Slogan**:智慧宠物门店服务系统 > **Logo / VI**:[《宠小它-Logo与品牌识别说明》](./宠小它-Logo与品牌识别说明.md)(含概念草案图) @@ -20,7 +20,7 @@ ## 一、产品定位 -**平台定位:** 宠物店服务管理 SaaS 平台,支持多家宠物店入驻 +**平台定位:** 面向独立宠物洗护单店的服务管理 SaaS;平台可承载多家独立试点门店,但本期不做连锁多门店组织。 **目标用户(v1):** - 宠物店老板 / 员工(管理预约 + 填写报告 + 发送报告 + 回访池) @@ -37,8 +37,18 @@ > - 完整经营分析看板。 **登录方式:** -- 微信授权登录(老板/员工主要登录方式) -- 手机号+短信验证码(备用) +- 生产:微信手机号授权登录;老板入驻和员工接受邀请均必须经微信核验手机号。 +- 开发:手机号+演示短信验证码;production profile 默认关闭,不作为生产备用通道。 + +**门店开通流程:** + +```text +老板微信核验手机号创建门店 +→ 补全门店名称/电话/地址 +→ 确认预约首末时段与并发容量 +→ 确认至少一个服务项目 +→ 显式完成开通(邀请员工为选填,支持单人门店) +``` --- @@ -71,10 +81,13 @@ | address | VARCHAR | 地址 | | intro | TEXT | 简介 | | owner_id | BIGINT | 老板用户ID | -| invite_code | VARCHAR | 员工邀请码(8位) | +| invite_code | VARCHAR | legacy 兼容字段;不返回客户端、不再作为注册凭证 | | booking_day_start | TIME | 每日首个容量桶开始时间 | | booking_last_slot_start | TIME | 每日最后一个容量桶开始时间 | | booking_capacity | INT | 门店并发接待数(1~10) | +| onboarding_status | VARCHAR | unknown / in_progress / completed | +| onboarding_completed_at | DATETIME | 老板显式完成开通时间 | +| onboarding_completed_by_user_id | BIGINT | 完成开通的老板用户 | | create_time | DATETIME | 创建时间 | | update_time | DATETIME | 更新时间 | @@ -83,7 +96,7 @@ |------|------|------| | id | BIGINT | 主键 | | username | VARCHAR | 用户名 | -| password | VARCHAR | 密码 | +| password | VARCHAR | legacy 服务端兼容字段;生产微信注册只写不可登录哨兵值,不接收或下发密码 | | name | VARCHAR | 姓名/技师名 | | phone | VARCHAR | 手机号 | | avatar | VARCHAR | 头像 | @@ -92,6 +105,23 @@ | create_time | DATETIME | 创建时间 | | update_time | DATETIME | 更新时间 | +### t_staff_invitation(员工邀请) + +| 字段 | 类型 | 说明 | +|------|------|------| +| invitation_id | VARCHAR(36) | 对外随机 ID | +| token_hash | VARCHAR(64) | 原始 256-bit token 的 SHA-256;原文不落库 | +| store_id | BIGINT | 门店 | +| invited_name / invited_phone | VARCHAR | 受邀姓名 / 服务端核验手机号 | +| status | VARCHAR | pending / accepted / revoked / expired | +| expires_at | DATETIME | 1~30 天有效期 | +| accepted_by_user_id / accepted_at | BIGINT / DATETIME | 接受回执 | +| revoked_by_user_id / revoked_at | BIGINT / DATETIME | 撤销回执 | + +### t_audit_log(不可变操作审计) + +记录门店入驻/开通/设置与员工邀请/删除等操作。metadata 仅允许服务端白名单低敏标量,禁止手机号、token、微信标识、密码/密钥、IP、地址坐标和原始请求体。 + ### t_store_customer(门店客户主档) | 字段 | 类型 | 说明 | |------|------|------| @@ -212,8 +242,9 @@ **手机号+短信验证码登录**(备用) **员工入职流程:** -- 老板后台生成邀请码 → 员工点链接注册 → 自动关联店铺 -- 老板后台直接新增员工 → 系统生成随机6位密码 → 员工用手机号+密码登录 +- 老板按员工本人手机号创建 1~30 天有效的一次性邀请;原始 token 仅创建响应显示一次,数据库只保存 hash +- 员工从小程序卡片进入,核对门店、姓名与脱敏手机号,再用微信核验的同一手机号接受邀请并自动关联门店 +- 邀请可在接受前撤销,过期或已使用后不可再次创建账号;不支持永久邀请码、直接建号或随机密码登录 ### 5.2 预约管理 @@ -318,9 +349,10 @@ ### 5.6 老板后台(合并到「我的」页面) **员工管理:** -- 员工邀请码(复制分享) -- 新增员工(姓名+手机号,系统生成密码) -- 删除员工 +- 老板按员工本人微信手机号创建 1~30 天有效的一次性邀请 +- 小程序直接转发带受控 path 的邀请卡片;原始口令仅创建后显示一次 +- 员工从卡片进入,查看门店/姓名/脱敏手机号,经微信核验后加入 +- 老板查看邀请状态、撤销待接受邀请、删除已加入员工 **服务类型:** - 新增服务类型 @@ -351,8 +383,8 @@ | 优先级 | 功能 | 状态 | |--------|------|------| -| P0 | 老板入驻/登录 | ✅ | -| P0 | 员工邀请码注册 | ✅ | +| P0 | 微信核验老板入驻 + 门店开通清单 | ✅ 代码完成,待生产迁移/真机验收 | +| P0 | 一次性员工邀请、微信核验接受、撤销与状态 | ✅ 代码完成,待生产迁移/真机验收 | | P0 | 员工登录(微信+验证码) | ✅ | | P0 | 预约列表 | ✅ | | P0 | 预约创建 | ✅ | @@ -365,7 +397,7 @@ | P0 | H5报告页(链接+二维码) | ✅ | | P1 | 报告过程素材回传(duringMedia) | 🔨 进行中 | | **P0** | **洗美过程短视频自动成片(可变时长、双编排、模板拼接)** | **🔨 本期迭代重点** | -| P0 | 老板后台(员工管理+邀请码) | ✅ 已合并到index.html | +| P0 | 老板后台(开通清单+员工邀请中心) | ✅ Admin 与小程序均已接入 | | P0 | 老板后台(预约概览) | ✅ 已合并到index.html | | P1 | 服务类型管理(老板后台) | ✅ | | P1 | 店铺设置(老板后台) | ✅ | diff --git a/架构决策-门店开通与员工邀请-2026-08-02.md b/架构决策-门店开通与员工邀请-2026-08-02.md new file mode 100644 index 0000000..3efc7bf --- /dev/null +++ b/架构决策-门店开通与员工邀请-2026-08-02.md @@ -0,0 +1,87 @@ +# 架构决策:门店开通、一次性员工邀请与操作审计 + +> 日期:2026-08-02 +> 状态:Accepted / Code Complete,待生产迁移与微信真机验收 +> Owner:Backend Core + Store Miniapp FE + Store Admin FE +> Review:Data Model / Architecture / Access & Privacy = Yes + +## 1. 决策背景 + +旧流程把 `Store.invite_code` 当作永久共享凭证,公开员工注册只凭 8 位码和手填手机号即可创建 staff;老板注册也接受未核验手机号和明文密码。Admin/小程序的“直接创建员工”还生成无法安全交付的初始密码。该模型不满足真实试点的身份核验、撤销、有效期、审计和离职失效要求。 + +本批目标不是扩展组织/权限系统,而是让 3~5 家独立单店能自行完成开通,并让受邀员工本人安全加入。 + +## 2. 已冻结决策 + +### 2.1 门店开通 + +- 新老板只走 `POST /api/onboarding/register-boss`,服务端用微信 `phoneCode` 换取并核验手机号;不接受手填手机号或明文密码。 +- 新门店状态为 `in_progress`;历史门店无法可靠判断是否已完成开通,迁移统一写 `unknown`。 +- 开通清单四步:门店资料、预约容量、服务项目为必填;邀请员工为选填,支持单人门店。 +- 只有 boss 可显式完成;完成回执保存 `completed_at/by` 并写 AuditLog。清单不阻断已有业务接口,避免历史门店升级即停摆。 + +### 2.2 员工邀请 + +- legacy `invite_code` 仅保留数据库兼容,不返回 Admin/小程序,不再参与查询或注册。 +- 老板创建 `StaffInvitation`:姓名、本人微信手机号、1~30 天有效期。 +- token 使用 32 字节 `SecureRandom`(256-bit),原文只在创建响应返回一次;数据库仅保存 SHA-256。 +- 邀请状态:`pending → accepted`,或 `pending → revoked/expired`;只能首次接受,已接受不能撤销。 +- 预览只返回门店名、受邀姓名、脱敏手机号、状态和有效期。 +- 接受时必须由微信换得的手机号与邀请绑定手机号精确匹配;成功后才创建 staff、消费邀请并签发 session。 +- 已存在 User/openid/unionid 冲突均拒绝。本期 User 仍是单角色模型,不把 customer 静默升级为 staff;试点遇到冲突时使用独立工作手机号,后续如有真实频次再引入 Account + StoreMembership。 + +### 2.3 会话与离职 + +HMAC 签名和过期校验之后,每次受保护请求继续读取活跃 User,校验账号未删除且 `role/storeId` 与 token 一致;boss/staff 还必须归属仍有效的 Store。删除员工、停用门店或权限范围变化后,旧 token 立即 401,无需等待 7 天到期。 + +### 2.4 AuditLog 与 BusinessEvent + +- `BusinessEvent` 记录可统计经营事实;本批补齐 `store_registered`。 +- `AuditLog` 记录门店开通/设置与员工权限操作,只追加、不更新/软删。 +- Audit metadata 只允许服务端白名单标量,禁止手机号、token/URL、openid/unionid、密码/密钥、IP、地址/坐标、备注/内容和原始请求体。 + +## 3. API 契约 + +| 方法 | 路径 | 权限 | 关键语义 | +|------|------|------|----------| +| POST | `/api/onboarding/register-boss` | public + 微信手机号核验 | 原子创建门店/老板,返回 session | +| GET | `/api/admin/onboarding` | boss/staff + 本店 | 计算开通清单 | +| POST | `/api/admin/onboarding/complete` | boss + 本店 | 必填项齐全后显式完成 | +| POST | `/api/admin/staff-invitations` | boss + 本店 | 原始 token 仅本响应一次 | +| GET | `/api/admin/staff-invitations` | boss + 本店 | 脱敏列表,无 token/hash | +| DELETE | `/api/admin/staff-invitations` | boss + 本店 | 撤销 pending 邀请 | +| GET | `/api/staff-invitations/preview` | public | 脱敏预览 | +| POST | `/api/staff-invitations/accept` | public + 微信手机号核验 | 手机号匹配才创建 staff | + +旧 `/api/user/register-boss`、`/api/user/register-staff`、`/api/user/create-staff`、`/api/store/register`、`/api/store/invite-code` 保留兼容入口但只返回明确 410 业务码,不产生写入。 + +## 4. 数据迁移与不变量 + +迁移:`backend/db/migrations/20260802_create_store_onboarding.sql`,在前五个迁移之后第六个执行。 + +新增 6 项生产只读不变量,总数由 18 增至 24: + +1. 门店开通状态枚举合法; +2. completed 必须有完成回执; +3. unknown/in_progress 不得带完成回执; +4. 邀请状态枚举合法; +5. accepted 与接受回执一致; +6. revoked 与撤销回执一致。 + +## 5. 前端行为 + +- Admin 设置页顶部显示开通清单,员工页改为邀请中心;创建后必须立即复制,之后只能撤销并重建。 +- 小程序老板可直接用 `open-type=share` 转发带受控 path 的邀请卡片。 +- 员工从卡片进入可先匿名预览,再点击微信手机号授权接受。 +- H5 允许预览,但不会提供绕过微信手机号核验的接受路径。 + +## 6. 明确不做 + +- 不引入多门店组织、复杂 RBAC、StoreMembership、审批流或通用 IAM。 +- 不接入短信生产登录,不恢复明文密码交付。 +- 不调用微信 URL Link/小程序码后台 API;试点先使用小程序原生分享卡片,Admin 提供复制消息作为补充。 +- 不自动执行生产迁移、不改生产数据。 + +## 7. 回滚 + +应用回滚只切回前一 backend/Admin/H5/小程序版本;数据库新增列/表/索引保留,不执行 down migration,不覆盖已经产生的邀请、审计或员工账号。旧应用会忽略新增结构,但 legacy 注册仍应保持关闭,避免重新暴露永久凭证路径。 diff --git a/生产发布与回滚Runbook-2026-08-01.md b/生产发布与回滚Runbook-2026-08-01.md index b805cf5..f708e93 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,或预检的 18 项只读不变量非 0; +- 任一迁移末尾验证查询非 0,或预检的 24 项只读不变量非 0; - `nginx -t` 失败,或 diff 显示将修改 Gitea/GitLab 的 server block; - readiness 非 `UP`,上传目录/FFmpeg/数据库任一依赖失败; - 发现凭据、完整 token、手机号或私密媒体地址进入日志/工单/截图; @@ -108,8 +108,9 @@ shasum -a 256 /secure/backup/petstore-before--.sql 3. `backend/db/migrations/20260801_create_business_event.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` -禁止跳步、交换顺序或让应用自动补表。任一验证项非 0,停止发布并由 Data Model/业务 owner 判断修复方案;不得猜测性回填客户身份、报告作者或事件。 +禁止跳步、交换顺序或让应用自动补表。任一验证项非 0,停止发布并由 Data Model/业务 owner 判断修复方案;不得猜测性回填客户身份、报告作者、事件、历史发送事实或历史开通状态。 ### 5.3 生产只读预检 @@ -128,7 +129,7 @@ systemd-run --wait --pipe --collect \ ./deploy/release-preflight.sh ``` -通过条件:静态配置检查通过、JPA `validate` 通过、18 项数据不变量全部为 0,并以退出码 0 结束。预检不会执行迁移或修复数据。 +通过条件:静态配置检查通过、JPA `validate` 通过、24 项数据不变量全部为 0,并以退出码 0 结束。预检不会执行迁移或修复数据。 ## 6. 构建发布产物 @@ -170,8 +171,9 @@ npm run build:mp-weixin 1. 将后端 `current` 指向新版本目录,执行 `systemctl daemon-reload`,只重启 Petstore 独立 unit。 2. 确认进程稳定并通过本机 readiness,再切 admin/H5 静态目录 `current`。 3. 以 `backend/deploy/nginx-petstore.conf.example` 新建 Petstore 专用配置文件;替换域名、证书路径和静态根目录。 -4. 执行 `nginx -t`。检查配置 diff 仅增加/修改 Petstore 的三个 server block,不改 Gitea/GitLab upstream、端口、证书和 location。 -5. 只执行 Nginx reload,不停止 Nginx;reload 后同时检查 Petstore、Gitea、GitLab 的既有健康入口。 +4. 保留模板中的 `petstore_safe` access log 格式,只记录 `$uri`,不得改回会包含 query 的 `$request` / `$request_uri`,避免报告 token 与邀请 token 进入访问日志。 +5. 执行 `nginx -t`。检查配置 diff 仅增加/修改 Petstore 的日志格式和三个 server block,不改 Gitea/GitLab upstream、端口、证书和 location。 +6. 只执行 Nginx reload,不停止 Nginx;reload 后同时检查 Petstore、Gitea、GitLab 的既有健康入口。 证书续签必须沿用服务器现有 ACME/证书管理方式,为 Petstore 新域名单独申请/续签;不要替换其他产品证书。续签后先 `nginx -t` 再 reload,并记录证书到期时间。 @@ -190,11 +192,11 @@ API_ORIGIN=https:// ./deploy/production-smoke.sh 仅在预先标记的试点门店和测试宠主上执行,记录匿名化预约 ID/报告 ID,不记录完整 token/手机号: -1. **boss**:登录 admin,核对门店营业时段、并发容量、服务时长;创建员工;查看工作台、客户、日程。 -2. **staff**:代客预约,开始服务,上传最小合规素材,提交一约一报告,复制报告话术;确认重复提交被拒绝。 +1. **boss**:用微信核验手机号创建试点门店;在 Admin/小程序补齐资料、容量、服务并显式完成开通;创建一次性员工邀请、保存创建证据后用小程序分享卡片;查看工作台、客户、日程。 +2. **staff**:从分享卡片进入,核对门店/姓名/脱敏手机号;用受邀微信手机号成功加入。用不匹配手机号验证被拒绝;加入后代客预约、开始服务、提交一约一报告并确认发送。 3. **customer**:小程序自助预约并验证长服务跨时段占号;打开匿名报告页;提交提醒留资;再次预约。 -4. **权限反例**:customer 不可进入后台;A 店账号不可读取 B 店预约/报告/线索;公开报告响应不含内部身份字段。 -5. **异常反例**:无容量、非法状态迁移、缺前/后照片、错误 token、FFmpeg 失败均有可理解提示。 +4. **权限反例**:customer 不可进入后台;A 店账号不可读取 B 店预约/报告/线索/邀请;legacy 永久邀请码与直接建员工均返回停用码;删除员工后旧 session 立即 401。 +5. **异常反例**:邀请手机号不匹配/过期/撤销/重复使用、无容量、非法状态迁移、缺前/后照片、错误报告 token、FFmpeg 失败均有可理解提示。 ## 9. 回滚 @@ -206,7 +208,7 @@ API_ORIGIN=https:// ./deploy/production-smoke.sh ### 9.2 数据库回滚边界 -本批五个迁移以新增列/表/索引和兼容回填为主,旧应用应忽略新增结构。因此常规回滚只回应用,不删除列、表、索引,不执行自动 down migration,也不把已产生的新业务数据覆盖回旧备份。 +本批六个迁移以新增列/表/索引和兼容回填为主,旧应用应忽略新增结构。因此常规回滚只回应用,不删除列、表、索引,不执行自动 down migration,也不把已产生的新业务数据覆盖回旧备份。 只有确认发生灾难性数据破坏、已冻结所有写入、明确接受备份时间点之后的数据丢失,并取得用户/业务负责人、DBA 和隐私责任人的书面授权后,才可执行整库恢复。恢复必须先在隔离库验证,再制定专门变更单;本 Runbook 不授权该操作。 @@ -216,7 +218,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 59443d6..6795ee1 100644 --- a/生产监控与告警基线-2026-08-01.md +++ b/生产监控与告警基线-2026-08-01.md @@ -83,7 +83,7 @@ - [ ] 外部 HTTPS 探测覆盖 liveness/readiness、admin、H5 报告入口; - [ ] 5xx、延迟、DB 池、磁盘、证书和进程重启已配置告警; - [ ] FFmpeg/上传失败有可聚合字段,而非只能搜索自由文本; -- [ ] BusinessEvent 日漏斗和 18 项只读数据审计有每日结果; +- [ ] BusinessEvent 日漏斗和 24 项只读数据审计有每日结果; - [ ] 通知渠道、Primary/Backup 值班人和升级联系人完成真实测试; - [ ] 日志脱敏抽查通过,访问权限和保留周期已批准; - [ ] 发布标记能关联四仓 commit 与 RC;