petstore-docs/ontology/rules.md

443 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Rules规则 / 不变量)
> 业务约束、决策条件、权限边界。每条规则标注适用动作、落地代码、测试。
## 鉴权
### rule:BR-AUTH-001
**HMAC session token + 活跃账号范围鉴权**protected `/api/**` 要求 `Authorization: Bearer <token>`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`、`AuthInterceptorActiveUserTest`
- **证据**`anchored`
### rule:BR-AUTH-002
**生产登录策略**:生产只走微信授权(`/api/user/wx-phone-login`SMS 验证码登录 dev-only`universalSmsCode` 为空 = 生产禁用);`SmsController#send` 生产返回 403 `SMS_LOGIN_DISABLED`
- **适用动作**`action:login`、`action:wx_phone_login`
- **代码**`backend/src/main/java/com/petstore/service/UserService.java#login`、`SmsController.java`
- **测试**`UserServiceLoginStrategyTest`
- **证据**`anchored`
## Store
### rule:BR-STORE-001
**店铺更新/删除仅 boss + 同店**`update`/`delete` 仅 `isBoss() && current.storeId != null``store.id` 必须等于 `current.storeId`
- **适用动作**`action:update_store`、`action:delete_store`
- **代码**`backend/src/main/java/com/petstore/controller/StoreController.java#update`/`#delete`
- **测试**`StoreControllerTest`
- **证据**`anchored`
### rule:BR-STORE-002
**legacy 永久邀请码禁用**`invite_code` 仅保留数据库兼容,不返回客户端、不再作为员工注册凭证;`GET /api/store/invite-code` 与 `POST /api/user/register-staff` 固定返回停用业务码,不查询门店或创建账号。
- **适用动作**`action:get_store_by_invite_code`、`action:register_staff`
- **代码**`StoreController#getByInviteCode`、`UserController#registerStaff`
- **测试**`StoreControllerTest`
- **证据**`anchored`
### rule:BR-STORE-003
**公开门店字段裁剪**:公开接口 `GET /api/store/get`、`GET /api/store/list` 仅返回预约/展示所需字段id/name/address/坐标/phone/logo/intro/营业时段),**不得**返回 `ownerId`、`inviteCode`、`deleted` 等内部字段(与 `rule:BR-RPT-006` 对称)。
- **适用动作**`action:get_store`、`action:list_stores`
- **代码**`backend/src/main/java/com/petstore/controller/StoreController.java#toPublicStoreView`
- **测试**`StoreControllerTest#publicGetHidesOwnerIdAndInviteCode`、`#publicListHidesOwnerIdAndInviteCode`
- **证据**`anchored`
## Appointment
### rule:BR-APPT-001
**预约状态机**:仅允许 `new→doing``startService`)、`new→cancel`、`doing→done`(报告提交触发)、`doing→cancel`(若产品允许);`done`/`cancel` 为终态不可回退;`→doing` 必须走 `/appointment/start`;非法迁移返回 `INVALID_STATUS` / `CANCEL_NOT_ALLOWED` / `USE_START_ENDPOINT`
- **适用动作**`action:start_service`、`action:transition_appointment_status`、`action:submit_report`
- **代码**`backend/src/main/java/com/petstore/service/AppointmentService.java#startService`/`#transitionStatus`
- **测试**`AppointmentServiceTest`
- **证据**`anchored`
### rule:BR-APPT-002
**预约跨店 403**boss/staff 操作预约list/start/status/detail/delete仅限本店 `storeId == current.storeId`,跨店返回 `FORBIDDEN`
- **适用动作**`action:list_appointments`、`action:start_service`、`action:transition_appointment_status`、`action:get_appointment_detail`、`delete`
- **代码**`backend/src/main/java/com/petstore/controller/AppointmentController.java`
- **测试**`AppointmentControllerDetailTest`
- **证据**`anchored`
### rule:BR-APPT-003
**预约客户归属与跨用户 403**customer 仅可查看/取消自己的预约(`customerUserId == current.userId`;未回填旧数据可回退 legacy `userId`不可操作他人预约。customer 自约时客户与创建人均从上下文派生boss/staff 代客预约必须解析真实 customer禁止把操作员工写成客户。`petId` 存在时必须属于该客户。
- **适用动作**`action:create_appointment`、`action:get_appointment_detail`、`action:transition_appointment_status`(仅 cancel
- **代码**`backend/src/main/java/com/petstore/controller/AppointmentController.java#detail`/`#updateStatus`
- **测试**`AppointmentControllerIdentityTest`、`AppointmentControllerDetailTest#customerCannotViewOthersAppointment`
- **证据**`anchored`
### rule:BR-APPT-004
**真实预约容量校验**:开始时间按半小时对齐;服务必须在 `booking_day_start` 到末容量桶结束时刻内完成。预约保存服务时长快照,并检查覆盖的每个半小时容量桶;有效预约与 `walk_in` 各消耗一个 `booking_capacity``blocked` 关闭覆盖范围内全部容量。创建预约/占用时悲观锁定门店行,避免并发超卖;取消预约不占容量。
- **适用动作**`action:create_appointment`、`action:get_available_slots`、`action:create_schedule_block`
- **代码**`backend/src/main/java/com/petstore/service/AppointmentService.java#createBooking`、`BookingCapacityService`、`ScheduleService#createBlock`
- **测试**`BookingCapacityServiceTest`、`AppointmentServiceTest`、`BookingCapacityMigrationTest`
- **文档**`docs/产品设计文档.md §5.2`
- **证据**`anchored`
## Report
### rule:BR-RPT-001
**报告必须绑定预约**`appointmentId` 为空返回 `APPOINTMENT_REQUIRED`;无预约报告不作为 P0 主路径。
- **适用动作**`action:submit_report`
- **代码**`backend/src/main/java/com/petstore/controller/ReportController.java#create`、`ReportService.java#create`
- **测试**`ReportControllerTest#createMissingAppointmentIdReturnsBizCode`、`ReportServiceTest#createWithNullAppointmentIdThrows`
- **证据**`anchored`
### rule:BR-RPT-002
**一约一份报告**`Report.appointment_id` 唯一约束 `uk_report_appointment`;服务层 `existsByAppointmentIdAndDeletedFalse` 重复提交抛 `IllegalStateException`Controller 转 409 `REPORT_ALREADY_EXISTS`
- **适用动作**`action:submit_report`
- **代码**`backend/src/main/java/com/petstore/entity/Report.java`、`mapper/ReportMapper.java`、`service/ReportService.java#create`、`controller/ReportController.java#create`
- **测试**`ReportServiceTest#createDuplicateReportThrows`、`ReportControllerTest#createDuplicateReportReturnsAlreadyExists`
- **证据**`anchored`(上线前需清理历史重复数据,见 `backend/README.md` 数据库迁移)
### rule:BR-RPT-003
**报告前后照片必填**:服务前/服务后至少各 1 张有效照片(`mediaType` 非 video缺 before 返回 `BEFORE_PHOTO_REQUIRED`,缺 after 返回 `AFTER_PHOTO_REQUIRED`;服务过程中素材可选。
- **适用动作**`action:submit_report`
- **代码**`backend/src/main/java/com/petstore/controller/ReportController.java#hasPhotoType`、`frontend/src/pages/report/Report.vue#submitReport`
- **测试**`ReportControllerTest#createMissingBeforePhotoReturnsBizCode`/`createMissingAfterPhotoReturnsBizCode`
- **文档**`docs/产品设计文档.md §5.3 报告提交必填项`
- **证据**`anchored`(前后端双校验)
### rule:BR-RPT-004
**报告仅 doing 预约可提交**:关联预约状态非 `doing` 返回 `INVALID_STATUS`「请先开始服务后再提交报告」;提交后 `doing→done`
- **适用动作**`action:submit_report`
- **代码**`backend/src/main/java/com/petstore/controller/ReportController.java#create`、`service/ReportService.java#create`
- **测试**`ReportServiceTest#createDoingAppointmentMarksDone`
- **证据**`anchored`
### rule:BR-RPT-005
**报告跨店 403**:仅 boss/staff 可创建/删除报告;报告所属门店必须等于 `current.storeId`,跨店返回 `FORBIDDEN`
- **适用动作**`action:submit_report`、`action:delete_report`
- **代码**`backend/src/main/java/com/petstore/controller/ReportController.java#create`/`#delete`
- **测试**`ReportControllerTest#createCrossStoreReturnsForbidden`、`createNonStaffReturnsForbidden`
- **证据**`anchored`
### rule:BR-RPT-006
**公开报告字段裁剪**`GET /api/report/get?token=`publicTokenRequest不返回 top-level `userId`/`storeId`/`reportToken``store` 对象内可返回 `id`(用于预约跳转)+ `name`/`logo`/`phone`/`address`,不返回 `latitude`/`longitude`;非 publicappointmentId 查询)保留全部字段供门店端使用。
- **适用动作**`action:get_report_by_token`
- **代码**`backend/src/main/java/com/petstore/controller/ReportController.java#getByAppointmentId`
- **测试**`ReportControllerTest#publicTokenQueryHidesInternalFields`
- **前端**`frontend/src/pages/report-view/reportView.vue``reminderToken` 只读路由 token`bookingStoreId` 只读 `store.id`
- **证据**`anchored`
### rule:BR-RPT-007
**token hash 日志**:报告打开埋点日志只记 `tokenHash`SHA-256 前 8 位 hex不输出 token 前缀。
- **适用动作**`action:track_report_open`
- **代码**`backend/src/main/java/com/petstore/controller/ReportController.java#shortTokenHash`/`#trackReportOpen`
- **证据**`anchored`
### rule:BR-RPT-008
**报告发送必须显式确认且首次回执不可覆盖**:复制链接、二维码、预览和打开报告均不自动改变发送状态。迁移前历史报告为 `unknown`,迁移后新报告为 `unsent`;仅本店 boss/staff 可选择 `wechat|qr|other` 确认为 `sent`。报告行以悲观锁串行确认;首次写入 `sent_at/sent_by_user_id/send_channel` 和幂等 `report_sent` 事件,重复确认返回 `alreadyConfirmed=true` 并保留首次回执。
- **适用动作**`action:confirm_report_sent`
- **代码**`ReportController#confirmSent`、`ReportService#confirmSent`、`ReportMapper#findByIdAndDeletedFalseForUpdate`
- **测试**`ReportControllerTest`、`ReportServiceTest`、`ReportSendMigrationTest`
- **文档**`docs/架构决策-报告确认发送状态-2026-08-02.md`
- **证据**`anchored`
## ReportLead
### rule:BR-LEAD-001
**留资去重**:同一 `report_token` + 同一手机号二次提交幂等更新,不重复插库,返回 `repeatSubmit=true`
- **适用动作**`action:submit_lead`
- **代码**`backend/src/main/java/com/petstore/service/ReportLeadService.java#submit`
- **证据**`documented`(逻辑已实现,未补测试用例)
### rule:BR-LEAD-002
**留资标识脱敏**`/api/report/leads` 不返回完整 `wechatOpenid`/`wechatUnionid`,只返回 `wechatBound`boolean+ `wechatOpenidMasked` + `wechatUnionidMasked`首4+****+末4过短返回 `****`)。
- **适用动作**`action:list_leads`
- **代码**`backend/src/main/java/com/petstore/controller/ReportLeadController.java#maskIdentifier`/`#leads`
- **前端**`frontend/src/pages/mine/Leads.vue`(只渲染 `wechatBound`/`wechatOpenidMasked`
- **证据**`anchored`
### rule:BR-LEAD-003
**回访池跨店 403**`/api/report/leads` 仅 boss/staff`storeId` 从 `CurrentUserContext` 派生,忽略 query `storeId`
- **适用动作**`action:list_leads`
- **代码**`backend/src/main/java/com/petstore/controller/ReportLeadController.java#leads`
- **证据**`anchored`
---
## Batch 2 录入规则
## Upload
### rule:BR-UPLOAD-001
**上传路径归一化**`FileController` 用 `Paths.get(uploadPath).toAbsolutePath().normalize()``resolveSafePath` 反斜杠统一、去前导斜杠、`normalize` 后校验 `target.startsWith(root)`,逃逸返回 400。
- **适用动作**`action:upload_image`、`action:get_upload_image`、`getLegacyImage`
- **代码**`backend/src/main/java/com/petstore/controller/FileController.java#resolveSafePath`/`#uploadRoot`
- **证据**`anchored`
### rule:BR-UPLOAD-002
**上传扩展名/MIME 校验**:原文件有扩展名必须在图片/视频白名单内,否则拒绝;无扩展名时 MIME 必须是 `image/*`/`video/*`/`application/octet-stream`/空(小程序兼容),否则拒绝。
- **适用动作**`action:upload_image`
- **代码**`backend/src/main/java/com/petstore/controller/FileController.java#isAllowedMediaType`/`#hasMediaExtension`
- **证据**`anchored`
## Highlight Video
### rule:BR-HL-001
**成片仅 boss/staff 可触发**`operatorUserId`/`role` 从 `CurrentUserContext` 派生不信任请求体customer 触发返回 403。
- **适用动作**`action:generate_highlight`
- **代码**`backend/src/main/java/com/petstore/controller/ReportController.java#startHighlight`
- **证据**`anchored`
### rule:BR-HL-002
**成片三态 + composeMode**`highlight_video_status` 枚举 `processing`/`done`/`failed``composeMode` 枚举 `preset`/`interleave`;失败原因归类 `material`/`service`/`network`/`unknown`(对宠主展示归类中文,不暴露堆栈);时长随素材变化(配置级上限 `HIGHLIGHT_MAX_DURATION_SEC`)。
- **适用动作**`action:generate_highlight`
- **代码**`backend/src/main/java/com/petstore/entity/Report.java``highlight_*` 字段)、`service/ReportHighlightVideoService.java`
- **文档**`docs/洗美报告短视频成片方案.md`、`docs/API-报告成片字段契约.md`
- **证据**`anchored`字段与状态已实现FFmpeg 集成测试 `gap`
## Schedule
### rule:BR-SCH-001
**排班仅 boss/staff + 跨店 403**`day`/`block`(create/delete) 仅 `isStoreUser()``storeId=current.storeId`(忽略请求体);`deleteBlock` 经 service `operatorStoreId` 校验同店。
- **适用动作**`action:day_agenda`、`action:create_schedule_block`、`action:delete_schedule_block`
- **代码**`backend/src/main/java/com/petstore/controller/ScheduleController.java`、`service/ScheduleService.java#deleteBlock`
- **测试**`ScheduleControllerTest`
- **证据**`anchored`
### rule:BR-SCH-002
**手动占用容量语义**`durationMinutes` 为 30480 的 30 分钟倍数;`walk_in` 在覆盖的每个容量桶消耗一个并发名额,`blocked` 仅允许创建在没有预约或其他占用的范围,并关闭全部号源。
- **适用动作**`action:day_agenda`、`action:create_schedule_block`
- **代码**`backend/src/main/java/com/petstore/service/ScheduleService.java`、`BookingCapacityService.java`
- **测试**`BookingCapacityServiceTest`、`ScheduleControllerTest`
- **证据**`anchored`
## ServiceType
### rule:BR-ST-001
**服务类型仅 boss + 同店 + 系统默认不可改删**`create`/`update`/`delete` 仅 `isBoss()``storeId=current.storeId``update`/`delete` 时 `existing.storeId` 必须非空且等于 `current.storeId`(系统默认 `storeId=null` 不可改删,他店不可改删);预计时长须为 30480 的 30 分钟倍数;`init` 仅 boss/staff。
- **适用动作**`action:create_service_type`、`action:update_service_type`、`action:delete_service_type`、`action:init_service_types`
- **代码**`backend/src/main/java/com/petstore/controller/ServiceTypeController.java`
- **测试**`ServiceTypeControllerTest`
- **证据**`anchored`
## Pet
### rule:BR-PET-001
**宠物跨用户/跨店**customer `list`/`create` 强制 `ownerUserId=current.userId`boss/staff `list` 查本店服务过的宠物(`storeId=current.storeId``update`/`delete`/`history` 的 `operatorUserId`/`role` 从上下文派生。
- **适用动作**`action:list_pets`、`action:create_pet`、`action:update_pet`、`action:delete_pet`、`action:get_pet_history`
- **代码**`backend/src/main/java/com/petstore/controller/PetController.java`
- **测试**`PetControllerTest`
- **证据**`anchored`
## User 管理
### rule:BR-USER-001
**员工管理与邀请仅 boss + 同店**:门店成员可查看本店成员列表;只有 boss 可创建/查看/撤销本店邀请并删除本店 `staff`。不能删除 boss不能直接创建员工账号员工须本人接受邀请。
- **适用动作**`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
**用户更新仅本人**`update_user` 的 `id=current.userId`(覆盖请求体);手机号变更需 SMS 验证码(生产 `rule:BR-AUTH-002` 禁用)。
- **适用动作**`action:update_user`、`action:get_user_info`
- **代码**`backend/src/main/java/com/petstore/controller/UserController.java#updateUser`/`#info`
- **证据**`anchored`
### rule:BR-USER-003
**User 敏感字段不序列化**`password`、`wechatOpenid`、`wechatUnionid` 必须 `@JsonIgnore`,禁止随 API JSON 返回。
- **适用动作**`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邀请绑定手机号、130 天有效、可撤销且仅首次接受有效。接受时必须由微信换得的手机号精确匹配。已有 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
**生产配置环境变量**`application.yml` 的 `datasource.url/username/password`、`wechat.appid/appsecret`、`auth.session-secret`、`upload.path` 等敏感项全部改为 `${ENV_VAR:默认}`;不在源码写真实密钥。
- **适用动作**:—(全局约束)
- **代码**`backend/src/main/resources/application.yml`、`application-example.yml`
- **文档**`backend/README.md 环境变量表`
- **证据**`anchored`(已暴露凭据需运维轮换)
### rule:BR-CONFIG-002
**production profile**`--spring.profiles.active=production` 时 `ddl-auto=validate`(不改表)、`show-sql=false`、`SMS_UNIVERSAL_CODE` 默认空(关闭万能验证码 `rule:BR-AUTH-002`),并跳过开发期默认服务初始化。最终配置不满足强密钥、显式生产 HTTPS 域名、绝对上传目录和显式 CORS 源时启动失败。
- **适用动作**:—(全局约束)
- **代码**`backend/src/main/resources/application.yml`multi-document production profile、`ProductionConfigurationValidator.java`、`PetstoreApplication.java`
- **测试**`ProductionConfigurationValidatorTest`
- **文档**`backend/README.md production profile`
- **证据**`anchored`(真实生产值与启动证据仍属发布门禁)
### rule:BR-CONFIG-003
**生产发布只读门禁**:正式切流前必须按固定顺序完成备份和六个版本化迁移,再以最终生产配置运行 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`
- **文档**`docs/生产发布与回滚Runbook-2026-08-01.md`、`docs/生产监控与告警基线-2026-08-01.md`
- **证据**`anchored`(生产备份、迁移和 live smoke 证据待发布时补齐)
## 寄语 / 退订 / 建议周期
### rule:BR-TST-001
**一报告一寄语**:同一 `report_id` 仅保留一条寄语;重复提交更新 `content`/`is_public`/`update_time`,不新增行。
- **适用动作**`action:submit_testimonial`
- **代码**`backend/src/main/java/com/petstore/service/ReportTestimonialService.java`
- **测试**`ReportTestimonialServiceTest`
- **证据**`anchored`
### rule:BR-UNSUB-001
**凭 unsubscribeToken 一键退订**:公开接口,令牌无效返回明确错误;成功后 `remind_status=unsubscribed`
- **适用动作**`action:unsubscribe`
- **代码**`backend/src/main/java/com/petstore/controller/ReportLeadController.java#unsubscribe`
- **测试**`ReportLeadControllerTest`
- **证据**`anchored`
### rule:BR-INT-001
**服务建议周期推算**:公开 `suggestion` 按报告服务类型 + 宠物类型读取 `entity:service_interval`(门店覆盖优先于系统默认)推算下次建议日期。
- **适用动作**`action:get_report_suggestion`
- **代码**`backend/src/main/java/com/petstore/service/ReportLeadService.java#suggestNext`
- **测试**`ReportLeadControllerTest`
- **证据**`anchored`
## 门店 Web 后台Phase A
### rule:BR-ADMIN-001
**门店后台角色门禁**`entity:admin_console` 仅 `boss` / `staff` 可进且 **均可编辑本店数据**`customer` 禁止。删店、创建/删除员工等仍遵循既有仅 boss 规则(`rule:BR-STORE-001`、`rule:BR-USER-001`)。
- **适用动作**`action:view_workbench`、`action:admin_list_*`、`action:admin_update_settings`、`action:admin_manage_schedule`
- **产品决策**2026-07-10staff 可编辑,非只读)
- **文档**`docs/门店管理后台-PhaseA-页面清单PRD.md`
- **证据**`documented`
### rule:BR-ADMIN-002
**后台数据仅本店**:所有后台查询/写强制 `current.storeId`;忽略跨店参数;连锁规则同步不在 Phase A。
- **适用动作**:同上
- **证据**`documented`
### rule:BR-ADMIN-003
**后台内部字段按用途最小返回**经鉴权后可返回报告内部状态等必要字段legacy `inviteCode` 不再返回。邀请列表仅返回脱敏手机号且不返回 token/hash原始 token 只在创建响应返回一次。
- **适用动作**`action:admin_list_reports`、`action:admin_update_settings`、`action:admin_list_service_customers`、`action:list_staff_invitations`
- **证据**`documented`
### rule:BR-ADMIN-004
**Phase A 后台范围闸门**:禁止将 `membership` / 储值 / 次卡套餐 / 收银 / 库存 / 寄养 / 押金 / 点餐 / 支付宝渠道 写入 Phase A 后台范围或设计稿;上述保持 `evidence=gap`
- **适用**`entity:admin_console`、`entity:workbench` 及一切 admin Phase A 动作
- **文档**`docs/门店管理后台升级方案-对标宠老板.md`
- **证据**`documented`
### rule:BR-SC-001
**门店客户主档唯一性与合并**:同一门店内,`customer_user_id` 和手机号分别只能对应一条有效 StoreCustomer。留资可先按手机号建档后续预约/登录识别到 customer 后绑定或合并。不得将 boss/staff 绑定为客户主档。
- **适用动作**`action:create_appointment`、`action:submit_lead`、`action:admin_list_service_customers`
- **代码**`backend/src/main/java/com/petstore/service/StoreCustomerService.java`
- **测试**`StoreCustomerServiceTest`、`ReportLeadServiceStoreCustomerTest`、`IdentityOwnershipQueryTest`
- **证据**`anchored`
### rule:BR-BE-001
**业务事件不可变、幂等与低敏**BusinessEvent 只追加,不更新/软删;稳定业务结果使用 idempotencyKey 去重。metadata 只允许服务端白名单标量,不得保存手机号、报告 token/hash、媒体 URL、IP、openid/unionid、备注、内容全文、密码、密钥或原始请求体。管理端仅返回当前门店聚合不开放通用事件明细。
- **适用动作**`action:create_appointment`、`action:start_service`、`action:transition_appointment_status`、`action:submit_report`、`action:track_report_open`、`action:submit_lead`、`action:view_workbench`
- **代码**`backend/src/main/java/com/petstore/service/BusinessEventService.java`、`AdminBusinessEventService.java`
- **测试**`BusinessEventServiceTest`、`BusinessEventMigrationTest`、`AdminBusinessEventControllerTest`
- **证据**`anchored`