443 lines
26 KiB
Markdown
443 lines
26 KiB
Markdown
# 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`;非 public(appointmentId 查询)保留全部字段供门店端使用。
|
||
|
||
- **适用动作**:`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` 为 30~480 的 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` 不可改删,他店不可改删);预计时长须为 30~480 的 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;邀请绑定手机号、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
|
||
|
||
**生产配置环境变量**:`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-10(staff 可编辑,非只读)
|
||
- **文档**:`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`
|