21 KiB
Rules(规则 / 不变量)
业务约束、决策条件、权限边界。每条规则标注适用动作、落地代码、测试。
鉴权
rule:BR-AUTH-001
HMAC session token 鉴权:protected /api/** 要求 Authorization: Bearer <token>;token 格式 base64url(payload).base64url(sig),HMAC-SHA256,默认 7 天有效期;public allowlist 见 AuthInterceptor.PUBLIC_PATTERNS。
- 适用动作:所有 protected 接口
- 代码:
backend/src/main/java/com/petstore/auth/AuthInterceptor.java、SessionTokenService.java、CurrentUserContext.java - 测试:
SessionTokenServiceTest - 证据:
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
邀请码查询仅 boss/staff + 同店:按 code 查到的 store.id 必须等于 current.storeId;新员工注册预览走公开 /api/user/register-staff(请求体含 inviteCode)。
- 适用动作:
action:get_store_by_invite_code - 代码:
backend/src/main/java/com/petstore/controller/StoreController.java#getByInviteCode - 测试:
StoreControllerTest#inviteCodeCrossStoreForbidden - 证据:
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
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 + 同店:create_staff/list_staff/delete_staff 仅 isBoss();storeId=current.storeId;delete_staff 校验目标员工属于本店。
- 适用动作:
action:create_staff、action:list_staff、action:delete_staff - 代码:
backend/src/main/java/com/petstore/controller/UserController.java - 证据:
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:register_staff、action:list_staff - 代码:
backend/src/main/java/com/petstore/entity/User.java - 测试:
UserJsonIgnoreTest - 证据:
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)。
- 适用动作:—(全局约束)
- 代码:
backend/src/main/resources/application.yml(multi-document production profile) - 文档:
backend/README.md production profile - 证据:
anchored(配置已加;production 实测启动gap)
寄语 / 退订 / 建议周期
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
后台可看内部字段:经鉴权后可返回公开 API 裁剪掉的字段(如 inviteCode、报告内部字段);与 rule:BR-STORE-003 / rule:BR-RPT-006 对称。手机号默认仍脱敏,明文权限预留。
- 适用动作:
action:admin_list_reports、action:admin_update_settings、action:admin_list_service_customers - 证据:
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