petstore-docs/ontology/rules.md

26 KiB
Raw Blame History

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.javaSessionTokenService.javaCurrentUserContext.java
  • 测试SessionTokenServiceTestAuthInterceptorActiveUserTest
  • 证据anchored

rule:BR-AUTH-002

生产登录策略:生产只走微信授权(/api/user/wx-phone-loginSMS 验证码登录 dev-onlyuniversalSmsCode 为空 = 生产禁用);SmsController#send 生产返回 403 SMS_LOGIN_DISABLED

  • 适用动作action:loginaction:wx_phone_login
  • 代码backend/src/main/java/com/petstore/service/UserService.java#loginSmsController.java
  • 测试UserServiceLoginStrategyTest
  • 证据anchored

Store

rule:BR-STORE-001

店铺更新/删除仅 boss + 同店update/deleteisBoss() && current.storeId != nullstore.id 必须等于 current.storeId

  • 适用动作action:update_storeaction: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-codePOST /api/user/register-staff 固定返回停用业务码,不查询门店或创建账号。

  • 适用动作action:get_store_by_invite_codeaction:register_staff
  • 代码StoreController#getByInviteCodeUserController#registerStaff
  • 测试StoreControllerTest
  • 证据anchored

rule:BR-STORE-003

公开门店字段裁剪:公开接口 GET /api/store/getGET /api/store/list 仅返回预约/展示所需字段id/name/address/坐标/phone/logo/intro/营业时段),不得返回 ownerIdinviteCodedeleted 等内部字段(与 rule:BR-RPT-006 对称)。

  • 适用动作action:get_storeaction:list_stores
  • 代码backend/src/main/java/com/petstore/controller/StoreController.java#toPublicStoreView
  • 测试StoreControllerTest#publicGetHidesOwnerIdAndInviteCode#publicListHidesOwnerIdAndInviteCode
  • 证据anchored

Appointment

rule:BR-APPT-001

预约状态机:仅允许 new→doingstartService)、new→canceldoing→done(报告提交触发)、doing→cancel(若产品允许);done/cancel 为终态不可回退;→doing 必须走 /appointment/start;非法迁移返回 INVALID_STATUS / CANCEL_NOT_ALLOWED / USE_START_ENDPOINT

  • 适用动作action:start_serviceaction:transition_appointment_statusaction:submit_report
  • 代码backend/src/main/java/com/petstore/service/AppointmentService.java#startService/#transitionStatus
  • 测试AppointmentServiceTest
  • 证据anchored

rule:BR-APPT-002

预约跨店 403boss/staff 操作预约list/start/status/detail/delete仅限本店 storeId == current.storeId,跨店返回 FORBIDDEN

  • 适用动作action:list_appointmentsaction:start_serviceaction:transition_appointment_statusaction:get_appointment_detaildelete
  • 代码backend/src/main/java/com/petstore/controller/AppointmentController.java
  • 测试AppointmentControllerDetailTest
  • 证据anchored

rule:BR-APPT-003

预约客户归属与跨用户 403customer 仅可查看/取消自己的预约(customerUserId == current.userId;未回填旧数据可回退 legacy userId不可操作他人预约。customer 自约时客户与创建人均从上下文派生boss/staff 代客预约必须解析真实 customer禁止把操作员工写成客户。petId 存在时必须属于该客户。

  • 适用动作action:create_appointmentaction:get_appointment_detailaction:transition_appointment_status(仅 cancel
  • 代码backend/src/main/java/com/petstore/controller/AppointmentController.java#detail/#updateStatus
  • 测试AppointmentControllerIdentityTestAppointmentControllerDetailTest#customerCannotViewOthersAppointment
  • 证据anchored

rule:BR-APPT-004

真实预约容量校验:开始时间按半小时对齐;服务必须在 booking_day_start 到末容量桶结束时刻内完成。预约保存服务时长快照,并检查覆盖的每个半小时容量桶;有效预约与 walk_in 各消耗一个 booking_capacityblocked 关闭覆盖范围内全部容量。创建预约/占用时悲观锁定门店行,避免并发超卖;取消预约不占容量。

  • 适用动作action:create_appointmentaction:get_available_slotsaction:create_schedule_block
  • 代码backend/src/main/java/com/petstore/service/AppointmentService.java#createBookingBookingCapacityServiceScheduleService#createBlock
  • 测试BookingCapacityServiceTestAppointmentServiceTestBookingCapacityMigrationTest
  • 文档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#createReportService.java#create
  • 测试ReportControllerTest#createMissingAppointmentIdReturnsBizCodeReportServiceTest#createWithNullAppointmentIdThrows
  • 证据anchored

rule:BR-RPT-002

一约一份报告Report.appointment_id 唯一约束 uk_report_appointment;服务层 existsByAppointmentIdAndDeletedFalse 重复提交抛 IllegalStateExceptionController 转 409 REPORT_ALREADY_EXISTS

  • 适用动作action:submit_report
  • 代码backend/src/main/java/com/petstore/entity/Report.javamapper/ReportMapper.javaservice/ReportService.java#createcontroller/ReportController.java#create
  • 测试ReportServiceTest#createDuplicateReportThrowsReportControllerTest#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#hasPhotoTypefrontend/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#createservice/ReportService.java#create
  • 测试ReportServiceTest#createDoingAppointmentMarksDone
  • 证据anchored

rule:BR-RPT-005

报告跨店 403:仅 boss/staff 可创建/删除报告;报告所属门店必须等于 current.storeId,跨店返回 FORBIDDEN

  • 适用动作action:submit_reportaction:delete_report
  • 代码backend/src/main/java/com/petstore/controller/ReportController.java#create/#delete
  • 测试ReportControllerTest#createCrossStoreReturnsForbiddencreateNonStaffReturnsForbidden
  • 证据anchored

rule:BR-RPT-006

公开报告字段裁剪GET /api/report/get?token=publicTokenRequest不返回 top-level userId/storeId/reportTokenstore 对象内可返回 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.vuereminderToken 只读路由 tokenbookingStoreId 只读 store.id
  • 证据anchored

rule:BR-RPT-007

token hash 日志:报告打开埋点日志只记 tokenHashSHA-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#confirmSentReportService#confirmSentReportMapper#findByIdAndDeletedFalseForUpdate
  • 测试ReportControllerTestReportServiceTestReportSendMigrationTest
  • 文档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,只返回 wechatBoundboolean+ 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/staffstoreIdCurrentUserContext 派生,忽略 query storeId

  • 适用动作action:list_leads
  • 代码backend/src/main/java/com/petstore/controller/ReportLeadController.java#leads
  • 证据anchored

Batch 2 录入规则

Upload

rule:BR-UPLOAD-001

上传路径归一化FileControllerPaths.get(uploadPath).toAbsolutePath().normalize()resolveSafePath 反斜杠统一、去前导斜杠、normalize 后校验 target.startsWith(root),逃逸返回 400。

  • 适用动作action:upload_imageaction:get_upload_imagegetLegacyImage
  • 代码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/roleCurrentUserContext 派生不信任请求体customer 触发返回 403。

  • 适用动作action:generate_highlight
  • 代码backend/src/main/java/com/petstore/controller/ReportController.java#startHighlight
  • 证据anchored

rule:BR-HL-002

成片三态 + composeModehighlight_video_status 枚举 processing/done/failedcomposeMode 枚举 preset/interleave;失败原因归类 material/service/network/unknown(对宠主展示归类中文,不暴露堆栈);时长随素材变化(配置级上限 HIGHLIGHT_MAX_DURATION_SEC)。

  • 适用动作action:generate_highlight
  • 代码backend/src/main/java/com/petstore/entity/Report.javahighlight_* 字段)、service/ReportHighlightVideoService.java
  • 文档docs/洗美报告短视频成片方案.mddocs/API-报告成片字段契约.md
  • 证据anchored字段与状态已实现FFmpeg 集成测试 gap

Schedule

rule:BR-SCH-001

排班仅 boss/staff + 跨店 403day/block(create/delete) 仅 isStoreUser()storeId=current.storeId(忽略请求体);deleteBlock 经 service operatorStoreId 校验同店。

  • 适用动作action:day_agendaaction:create_schedule_blockaction:delete_schedule_block
  • 代码backend/src/main/java/com/petstore/controller/ScheduleController.javaservice/ScheduleService.java#deleteBlock
  • 测试ScheduleControllerTest
  • 证据anchored

rule:BR-SCH-002

手动占用容量语义durationMinutes 为 30480 的 30 分钟倍数;walk_in 在覆盖的每个容量桶消耗一个并发名额,blocked 仅允许创建在没有预约或其他占用的范围,并关闭全部号源。

  • 适用动作action:day_agendaaction:create_schedule_block
  • 代码backend/src/main/java/com/petstore/service/ScheduleService.javaBookingCapacityService.java
  • 测试BookingCapacityServiceTestScheduleControllerTest
  • 证据anchored

ServiceType

rule:BR-ST-001

服务类型仅 boss + 同店 + 系统默认不可改删create/update/deleteisBoss()storeId=current.storeIdupdate/deleteexisting.storeId 必须非空且等于 current.storeId(系统默认 storeId=null 不可改删,他店不可改删);预计时长须为 30480 的 30 分钟倍数;init 仅 boss/staff。

  • 适用动作action:create_service_typeaction:update_service_typeaction:delete_service_typeaction:init_service_types
  • 代码backend/src/main/java/com/petstore/controller/ServiceTypeController.java
  • 测试ServiceTypeControllerTest
  • 证据anchored

Pet

rule:BR-PET-001

宠物跨用户/跨店customer list/create 强制 ownerUserId=current.userIdboss/staff list 查本店服务过的宠物(storeId=current.storeIdupdate/delete/historyoperatorUserId/role 从上下文派生。

  • 适用动作action:list_petsaction:create_petaction:update_petaction:delete_petaction: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_staffaction:list_staffaction:delete_staffaction:create_staff_invitationaction:list_staff_invitationsaction:revoke_staff_invitation
  • 代码UserController.javaStaffInvitationController.java
  • 测试StaffInvitationServiceTest
  • 证据anchored

rule:BR-USER-002

用户更新仅本人update_userid=current.userId(覆盖请求体);手机号变更需 SMS 验证码(生产 rule:BR-AUTH-002 禁用)。

  • 适用动作action:update_useraction:get_user_info
  • 代码backend/src/main/java/com/petstore/controller/UserController.java#updateUser/#info
  • 证据anchored

rule:BR-USER-003

User 敏感字段不序列化passwordwechatOpenidwechatUnionid 必须 @JsonIgnore,禁止随 API JSON 返回。

  • 适用动作action:loginaction:wx_phone_loginaction:get_user_infoaction:update_useraction:register_bossaction:accept_staff_invitationaction:list_staff
  • 代码backend/src/main/java/com/petstore/entity/User.java
  • 测试UserJsonIgnoreTest
  • 证据anchored

rule:BR-ONBOARD-001

微信核验入驻与显式开通:新老板只能用微信 phoneCode 核验手机号后入驻,不接收手填手机号或明文密码。历史门店开通状态迁移为 unknown,不得推测。完成开通必须满足:门店名称/电话/地址完整、预约首末时段与并发容量合法、至少一个服务项目;邀请员工为选填,支持单人门店。

  • 适用动作action:register_bossaction:get_store_onboardingaction:complete_store_onboarding
  • 代码MerchantOnboardingService.javaStoreOnboardingService.java
  • 测试StoreOnboardingServiceTestStoreOnboardingMigrationTest
  • 证据anchored

rule:BR-INVITE-001

一次性员工邀请安全边界:邀请 token 为 256-bit 随机值,原文只在创建响应出现一次,数据库只存 SHA-256邀请绑定手机号、130 天有效、可撤销且仅首次接受有效。接受时必须由微信换得的手机号精确匹配。已有 User/openid/unionid 冲突均拒绝,本期不把 customer 静默升级为 staff。

  • 适用动作:创建、列表、撤销、预览、接受员工邀请
  • 代码StaffInvitationService.javaStaffInvitationController.java
  • 测试StaffInvitationServiceTestStoreOnboardingMigrationTest
  • 证据anchored

rule:BR-AUDIT-001

不可变低敏操作审计AuditLog 只追加,强制 storeId、受控 action/target/outcome。metadata 只允许服务端白名单标量禁止手机号、token/URL、openid/unionid、密码/密钥、IP、地址/坐标、备注/内容和原始请求体。

  • 适用动作:门店入驻/完成开通/更新设置、员工邀请创建/撤销/接受、删除员工
  • 代码AuditLogService.java
  • 测试AuditLogServiceTestStoreOnboardingMigrationTest
  • 证据anchored

Config

rule:BR-CONFIG-001

生产配置环境变量application.ymldatasource.url/username/passwordwechat.appid/appsecretauth.session-secretupload.path 等敏感项全部改为 ${ENV_VAR:默认};不在源码写真实密钥。

  • 适用动作:—(全局约束)
  • 代码backend/src/main/resources/application.ymlapplication-example.yml
  • 文档backend/README.md 环境变量表
  • 证据anchored(已暴露凭据需运维轮换)

rule:BR-CONFIG-002

production profile--spring.profiles.active=productionddl-auto=validate(不改表)、show-sql=falseSMS_UNIVERSAL_CODE 默认空(关闭万能验证码 rule:BR-AUTH-002),并跳过开发期默认服务初始化。最终配置不满足强密钥、显式生产 HTTPS 域名、绝对上传目录和显式 CORS 源时启动失败。

  • 适用动作:—(全局约束)
  • 代码backend/src/main/resources/application.ymlmulti-document production profileProductionConfigurationValidator.javaPetstoreApplication.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.javaPetstoreRuntimeHealthIndicator.javabackend/deploy/release-preflight.shproduction-smoke.sh
  • 文档docs/生产发布与回滚Runbook-2026-08-01.mddocs/生产监控与告警基线-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_consoleboss / staff 可进且 均可编辑本店数据customer 禁止。删店、创建/删除员工等仍遵循既有仅 boss 规则(rule:BR-STORE-001rule:BR-USER-001)。

  • 适用动作action:view_workbenchaction:admin_list_*action:admin_update_settingsaction: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_reportsaction:admin_update_settingsaction:admin_list_service_customersaction:list_staff_invitations
  • 证据documented

rule:BR-ADMIN-004

Phase A 后台范围闸门:禁止将 membership / 储值 / 次卡套餐 / 收银 / 库存 / 寄养 / 押金 / 点餐 / 支付宝渠道 写入 Phase A 后台范围或设计稿;上述保持 evidence=gap

  • 适用entity:admin_consoleentity:workbench 及一切 admin Phase A 动作
  • 文档docs/门店管理后台升级方案-对标宠老板.md
  • 证据documented

rule:BR-SC-001

门店客户主档唯一性与合并:同一门店内,customer_user_id 和手机号分别只能对应一条有效 StoreCustomer。留资可先按手机号建档后续预约/登录识别到 customer 后绑定或合并。不得将 boss/staff 绑定为客户主档。

  • 适用动作action:create_appointmentaction:submit_leadaction:admin_list_service_customers
  • 代码backend/src/main/java/com/petstore/service/StoreCustomerService.java
  • 测试StoreCustomerServiceTestReportLeadServiceStoreCustomerTestIdentityOwnershipQueryTest
  • 证据anchored

rule:BR-BE-001

业务事件不可变、幂等与低敏BusinessEvent 只追加,不更新/软删;稳定业务结果使用 idempotencyKey 去重。metadata 只允许服务端白名单标量,不得保存手机号、报告 token/hash、媒体 URL、IP、openid/unionid、备注、内容全文、密码、密钥或原始请求体。管理端仅返回当前门店聚合不开放通用事件明细。

  • 适用动作action:create_appointmentaction:start_serviceaction:transition_appointment_statusaction:submit_reportaction:track_report_openaction:submit_leadaction:view_workbench
  • 代码backend/src/main/java/com/petstore/service/BusinessEventService.javaAdminBusinessEventService.java
  • 测试BusinessEventServiceTestBusinessEventMigrationTestAdminBusinessEventControllerTest
  • 证据anchored