# Actions（动作）

> 读写实体的命令/用例。每个动作标注入口（controller/service 方法）、输入输出实体、适用规则。

## Store 相关

### action:register_boss

老板入驻：微信核验手机号后，在同一事务创建 `Store` + `boss` `User`，签发 `session_token`，写 `store_registered` BusinessEvent 与 AuditLog。

- **入口**：`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

更新店铺信息：仅 boss 且只能改自己所属门店。

- **入口**：`PUT /api/store/update` → `StoreController#update` → `StoreService#update`
- **输入**：`entity:store`（id 必须等于 `current.storeId`）
- **输出**：`entity:store`
- **适用规则**：`rule:BR-STORE-001`（仅 boss + 同店）
- **代码**：`backend/src/main/java/com/petstore/controller/StoreController.java#update`
- **测试**：`StoreControllerTest`
- **证据**：`anchored`

### action:delete_store

删除店铺：仅 boss 且只能删自己所属门店（软删）。

- **入口**：`DELETE /api/store/delete` → `StoreController#delete`
- **适用规则**：`rule:BR-STORE-001`
- **代码**：`backend/src/main/java/com/petstore/controller/StoreController.java#delete`
- **测试**：`StoreControllerTest`
- **证据**：`anchored`

### action:get_store_by_invite_code

legacy 永久邀请码查询入口已停用，固定返回 `410 / LEGACY_INVITE_DISABLED`，不再查询或返回门店。

- **入口**：`GET /api/store/invite-code` → `StoreController#getByInviteCode`
- **适用规则**：`rule:BR-STORE-002`（legacy 禁用）
- **代码**：`backend/src/main/java/com/petstore/controller/StoreController.java#getByInviteCode`
- **测试**：`StoreControllerTest`
- **证据**：`anchored`

## User / 鉴权相关

### action:login

手机号 + 短信验证码登录。**生产禁用**，仅 dev 模式（万能验证码 123456）可用。

- **入口**：`POST /api/user/login` → `UserService#login`（公开）
- **输入**：`phone`、`code`
- **输出**：`entity:user`、`entity:store`、`session_token`
- **适用规则**：`rule:BR-AUTH-001`、`rule:BR-AUTH-002`（生产 SMS 禁用）
- **代码**：`backend/src/main/java/com/petstore/service/UserService.java#login`
- **测试**：`UserServiceLoginStrategyTest`
- **证据**：`anchored`

### action:wx_phone_login

微信小程序手机号授权登录：`getPhoneNumber` 的 phoneCode + `uni.login` 的 loginCode 换手机号并沉淀 openid。

- **入口**：`POST /api/user/wx-phone-login` → `UserController#wxPhoneLogin` → `UserService#loginByVerifiedPhone`（公开）
- **输出**：`entity:user`、`entity:store`、`session_token`、可能绑定 `wechat_openid`/`wechat_unionid`
- **适用规则**：`rule:BR-AUTH-001`
- **代码**：`backend/src/main/java/com/petstore/controller/UserController.java#wxPhoneLogin`
- **证据**：`anchored`（生产登录主路径）

### action:bind_wechat

绑定小程序 openid/unionid；若标识曾绑定其他账号则先解绑。

- **入口**：`UserController#wxPhoneLogin` 内调用 `UserService#bindWechatMiniIdentity`
- **适用规则**：—
- **代码**：`backend/src/main/java/com/petstore/service/UserService.java#bindWechatMiniIdentity`
- **证据**：`anchored`

## Appointment 相关

### action:create_appointment

创建预约：按服务时长跨半小时容量桶占号，并保存时长快照；取消不占号；客户与创建人分开记录。可选携带 `followUpTaskId`，在真实保存成功后闭环再次预约归因。

- **入口**：`POST /api/appointment/create` → `AppointmentController#create` → `AppointmentService#createBooking`
- **输入**：`petName`、`petType`、`serviceType`、`appointmentTime`、`storeId`、`remark?`、`petId?`、`followUpTaskId?`；门店代客预约另需 `customerUserId` 或 `customerPhone`
- **输出**：`entity:appointment`；携带任务且成功时更新 `entity:follow_up_task`
- **身份口径**：customer 自约时客户=创建人=current.userId；boss/staff 代约时创建人=current.userId，客户按请求解析，legacy userId 镜像客户
- **适用规则**：`rule:BR-APPT-001`（状态机）、`rule:BR-APPT-003`（客户归属）、`rule:BR-APPT-004`（服务时长 + 门店容量 + 并发锁）、`rule:BR-SC-001`（门店客户建档）、`rule:BR-FU-002`（真实再次预约）
- **代码**：`backend/src/main/java/com/petstore/controller/AppointmentController.java#create`
- **证据**：`anchored`

### action:start_service

开始服务：仅 `new → doing`，指定技师（谁点谁服务）；仅 boss/staff 且同店。

- **入口**：`POST /api/appointment/start` → `AppointmentController#start` → `AppointmentService#startService`
- **适用规则**：`rule:BR-APPT-001`、`rule:BR-APPT-002`（仅 boss/staff + 同店）
- **代码**：`backend/src/main/java/com/petstore/controller/AppointmentController.java#start`
- **测试**：`AppointmentServiceTest`
- **证据**：`anchored`

### action:transition_appointment_status

更新预约状态：合法迁移见 `AppointmentService#transitionStatus`；customer 仅可取消自己的。

- **入口**：`PUT /api/appointment/status` → `AppointmentController#updateStatus`
- **适用规则**：`rule:BR-APPT-001`、`rule:BR-APPT-003`（customer 仅取消自己的）
- **代码**：`backend/src/main/java/com/petstore/controller/AppointmentController.java#updateStatus`
- **测试**：`AppointmentServiceTest`
- **证据**：`anchored`

### action:get_appointment_detail

预约详情：customer 仅本人，boss/staff 仅同店。

- **入口**：`GET /api/appointment/detail` → `AppointmentController#detail`
- **适用规则**：`rule:BR-APPT-002`、`rule:BR-APPT-003`
- **代码**：`backend/src/main/java/com/petstore/controller/AppointmentController.java#detail`
- **测试**：`AppointmentControllerDetailTest`
- **证据**：`anchored`

### action:list_appointments

预约列表：身份从上下文派生，customer 看自己，boss/staff 看本店。

- **入口**：`GET /api/appointment/list` → `AppointmentController#list`
- **适用规则**：`rule:BR-APPT-002`、`rule:BR-APPT-003`
- **代码**：`backend/src/main/java/com/petstore/controller/AppointmentController.java#list`
- **证据**：`anchored`

## Report 相关

### action:submit_report

提交报告：仅 boss/staff；`authorStaffId` 从上下文派生，`customerUserId` 从预约派生；报告所属门店必须等于当前用户门店。

- **入口**：`POST /api/report/create` → `ReportController#create` → `ReportService#create`
- **输入**：`appointmentId`、`remark?`、`images`；身份字段不从请求体采信
- **输出**：`reportToken`、`reportId`
- **适用规则**：`rule:BR-RPT-001`（必须绑定预约）、`rule:BR-RPT-002`（一约一份）、`rule:BR-RPT-003`（前后照片必填）、`rule:BR-RPT-004`（仅 doing 预约）、`rule:BR-RPT-005`（跨店 403）
- **代码**：`backend/src/main/java/com/petstore/controller/ReportController.java#create`
- **测试**：`ReportControllerTest`、`ReportServiceTest`
- **证据**：`anchored`

### action:get_report_by_token

公开报告查询：凭 token 匿名访问，字段裁剪不返回 top-level `userId/storeId/reportToken`。

- **入口**：`GET /api/report/get?token=` → `ReportController#getByAppointmentId`（公开）
- **适用规则**：`rule:BR-RPT-006`（公开字段裁剪）
- **代码**：`backend/src/main/java/com/petstore/controller/ReportController.java#getByAppointmentId`
- **测试**：`ReportControllerTest#publicTokenQueryHidesInternalFields`
- **证据**：`anchored`

### action:confirm_report_sent

门店员工在宠主实际收到报告后显式确认发送。复制链接、生成/展示二维码、打开 H5 或预览报告都不自动计为发送；首次确认写入不可覆盖的回执，重复确认幂等成功。

- **入口**：`POST /api/report/confirm-sent` → `ReportController#confirmSent` → `ReportService#confirmSent`
- **输入**：`reportId`、`channel=wechat|qr|other`；门店、员工与角色均从会话派生
- **输出**：`sendStatus`、`sentAt`、`sendChannel`、`alreadyConfirmed`
- **适用规则**：`rule:BR-RPT-005`、`rule:BR-RPT-008`、`rule:BR-BE-001`
- **触发事件**：`event:report_sent`（仅首次确认）
- **代码**：`ReportController#confirmSent`、`ReportService#confirmSent`、`BusinessEventService#recordReportSent`
- **测试**：`ReportControllerTest`、`ReportServiceTest`、`BusinessEventServiceTest`
- **证据**：`anchored`

### action:track_report_open

报告打开埋点：服务端用 token 定位 Report 后写入低敏 BusinessEvent；事件表不保存 token/hash，日志只记 SHA-256 前 8 位 hash。

- **入口**：`POST /api/report/open-track` → `ReportController#trackReportOpen`（公开）
- **适用规则**：`rule:BR-RPT-007`（token hash 日志）、`rule:BR-BE-001`（不可变与低敏事件）
- **代码**：`ReportController#trackReportOpen`、`BusinessEventService#recordReportOpenByToken`
- **测试**：`ReportControllerTest`、`BusinessEventServiceTest`
- **证据**：`anchored`

### action:delete_report

删除报告：仅 boss/staff 且同店。

- **入口**：`DELETE /api/report/delete` → `ReportController#delete`
- **适用规则**：`rule:BR-RPT-005`
- **代码**：`backend/src/main/java/com/petstore/controller/ReportController.java#delete`
- **证据**：`anchored`（同店校验用 `reportService.list(storeId,null)` 遍历，性能非最优，列为已知 gap）

## ReportLead 相关

### action:submit_lead

宠主留资：凭 `reportToken` 匿名提交手机号，同报告同手机号去重。

- **入口**：`POST /api/report/{token}/reminder` → `ReportLeadController#submitReminder` → `ReportLeadService#submit`（公开）
- **输入**：`phone`、`consent`、`loginCode?`
- **输出**：`leadId`、`remindDate`、`unsubscribeToken`、`wechatBound`
- **适用规则**：`rule:BR-LEAD-001`（去重）、`rule:BR-SC-001`（门店客户建档/绑定）、`rule:BR-BE-001`（低敏事件）
- **代码**：`backend/src/main/java/com/petstore/controller/ReportLeadController.java#submitReminder`
- **测试**：`ReportLeadControllerTest`、`ReportLeadServiceStoreCustomerTest`
- **证据**：`anchored`

### action:unsubscribe

一键退订：凭 `unsubscribeToken`。

- **入口**：`POST /api/report/unsubscribe` → `ReportLeadController#unsubscribe`（公开）
- **适用规则**：`rule:BR-UNSUB-001`
- **代码**：`backend/src/main/java/com/petstore/controller/ReportLeadController.java#unsubscribe`
- **测试**：`ReportLeadControllerTest`
- **证据**：`anchored`

### action:list_leads

回访池列表：仅 boss/staff，`storeId` 从上下文派生；返回脱敏 openid/unionid。

- **入口**：`GET /api/report/leads` → `ReportLeadController#leads`
- **适用规则**：`rule:BR-LEAD-002`（标识脱敏）、`rule:BR-LEAD-003`（跨店 403）
- **代码**：`backend/src/main/java/com/petstore/controller/ReportLeadController.java#leads`
- **证据**：`anchored`

---

## Batch 2 录入动作

## Pet 相关

### action:list_pets

宠物列表：customer 查自己宠物（`ownerUserId=current.userId`）；boss/staff 查本店服务过的宠物（`storeId=current.storeId`）。

- **入口**：`GET /api/pet/list` → `PetController#list`
- **适用规则**：`rule:BR-PET-001`（跨用户/跨店）
- **代码**：`backend/src/main/java/com/petstore/controller/PetController.java#list`
- **证据**：`anchored`

### action:create_pet

创建宠物档案：customer 强制 `ownerUserId=current.userId`；boss/staff 用 body.ownerUserId（代客建档）。

- **入口**：`POST /api/pet/create` → `PetController#create` → `PetService#create`
- **适用规则**：`rule:BR-PET-001`
- **代码**：`backend/src/main/java/com/petstore/controller/PetController.java#create`
- **证据**：`anchored`

### action:update_pet

更新宠物：`operatorUserId`/`role` 从上下文派生。

- **入口**：`PUT /api/pet/update` → `PetController#update` → `PetService#update`
- **适用规则**：`rule:BR-PET-001`
- **代码**：`backend/src/main/java/com/petstore/controller/PetController.java#update`
- **证据**：`anchored`

### action:delete_pet

删除宠物：`operatorUserId`/`role` 从上下文派生。

- **入口**：`DELETE /api/pet/delete` → `PetController#delete` → `PetService#delete`
- **适用规则**：`rule:BR-PET-001`
- **代码**：`backend/src/main/java/com/petstore/controller/PetController.java#delete`
- **证据**：`anchored`

### action:get_pet_history

宠物服务时间线：宠主本人或本店员工/老板可查。

- **入口**：`GET /api/pet/history` → `PetController#history` → `PetService#getServiceHistory`
- **适用规则**：`rule:BR-PET-001`
- **代码**：`backend/src/main/java/com/petstore/controller/PetController.java#history`
- **证据**：`anchored`

## ServiceType 相关

### action:list_service_types

服务类型列表：公开接口，不传 storeId 返回系统默认；传 storeId 返回系统默认 + 该店自定义，同名时本店定义优先；每项返回预计服务时长。

- **入口**：`GET /api/service-type/list` → `ServiceTypeController#list`（公开，在 AuthInterceptor allowlist）
- **代码**：`backend/src/main/java/com/petstore/controller/ServiceTypeController.java#list`
- **证据**：`anchored`

### action:create_service_type

新增服务类型：仅 boss；`storeId=current.storeId`；必须填写 30 分钟粒度的预计时长。

- **入口**：`POST /api/service-type/create` → `ServiceTypeController#create` → `ServiceTypeService#create`
- **适用规则**：`rule:BR-ST-001`（仅 boss + 同店）
- **代码**：`backend/src/main/java/com/petstore/controller/ServiceTypeController.java#create`
- **测试**：`ServiceTypeControllerTest`
- **证据**：`anchored`

### action:update_service_type

编辑服务类型：仅 boss；必须为本店自定义（系统默认 `storeId` 为空不可改、他店不可改）；名称和预计时长一起更新。

- **入口**：`PUT /api/service-type/update` → `ServiceTypeController#update`
- **适用规则**：`rule:BR-ST-001`
- **代码**：`backend/src/main/java/com/petstore/controller/ServiceTypeController.java#update`
- **测试**：`ServiceTypeControllerTest`
- **证据**：`anchored`

### action:delete_service_type

删除服务类型：仅 boss；必须为本店自定义（系统默认不可删、他店不可删）。

- **入口**：`DELETE /api/service-type/delete` → `ServiceTypeController#delete`
- **适用规则**：`rule:BR-ST-001`
- **代码**：`backend/src/main/java/com/petstore/controller/ServiceTypeController.java#delete`
- **测试**：`ServiceTypeControllerTest`
- **证据**：`anchored`

### action:init_service_types

初始化系统默认服务类型：仅 boss/staff（启动期已自动初始化）。

- **入口**：`POST /api/service-type/init` → `ServiceTypeController#init`
- **代码**：`backend/src/main/java/com/petstore/controller/ServiceTypeController.java#init`
- **测试**：`ServiceTypeControllerTest`
- **证据**：`anchored`

## Schedule 相关

### action:day_agenda

门店某日日程：仅 boss/staff；`storeId=current.storeId`；每个半小时桶返回预约/手动占用列表及已用、总计、剩余容量。

- **入口**：`GET /api/schedule/day` → `ScheduleController#day` → `ScheduleService#dayAgenda`
- **适用规则**：`rule:BR-SCH-001`（仅 boss/staff + 跨店 403）
- **代码**：`backend/src/main/java/com/petstore/controller/ScheduleController.java#day`
- **测试**：`ScheduleControllerTest`
- **证据**：`anchored`

### action:create_schedule_block

手动占用连续半小时档：仅 boss/staff；`storeId=current.storeId`、`createdByUserId=current.userId`；`walk_in` 消耗一个并发名额，`blocked` 关闭全部名额。

- **入口**：`POST /api/schedule/block` → `ScheduleController#createBlock` → `ScheduleService#createBlock`
- **适用规则**：`rule:BR-SCH-001`、`rule:BR-SCH-002`、`rule:BR-APPT-004`
- **代码**：`backend/src/main/java/com/petstore/controller/ScheduleController.java#createBlock`
- **测试**：`ScheduleControllerTest`
- **证据**：`anchored`

### action:delete_schedule_block

取消手动占用：仅 boss/staff；service 层做同店校验（`operatorStoreId`）。

- **入口**：`DELETE /api/schedule/block` → `ScheduleController#deleteBlock` → `ScheduleService#deleteBlock`
- **适用规则**：`rule:BR-SCH-001`
- **代码**：`backend/src/main/java/com/petstore/controller/ScheduleController.java#deleteBlock`、`service/ScheduleService.java#deleteBlock`
- **测试**：`ScheduleControllerTest`
- **证据**：`anchored`

## Upload / Highlight 相关

### action:upload_image

上传图片/视频：扩展名/MIME 校验 + 路径归一化防逃逸；需登录。

- **入口**：`POST /api/upload/image` → `FileController#uploadImage`
- **适用规则**：`rule:BR-UPLOAD-001`（路径归一化）、`rule:BR-UPLOAD-002`（扩展名/MIME 校验）
- **代码**：`backend/src/main/java/com/petstore/controller/FileController.java#uploadImage`
- **证据**：`anchored`

### action:get_upload_image

读取上传资源：公开（在 AuthInterceptor allowlist `GET /api/upload/image/**`）；路径归一化防逃逸。

- **入口**：`GET /api/upload/image/**` → `FileController#getImage`
- **适用规则**：`rule:BR-UPLOAD-001`
- **代码**：`backend/src/main/java/com/petstore/controller/FileController.java#getImage`
- **证据**：`anchored`

### action:generate_highlight

触发服务回顾短片生成：仅 boss/staff；`operatorUserId`/`role` 从上下文派生；异步 FFmpeg 拼接。

- **入口**：`POST /api/report/highlight/start` → `ReportController#startHighlight` → `ReportHighlightVideoService#requestGenerate`
- **适用规则**：`rule:BR-HL-001`（仅 boss/staff）、`rule:BR-HL-002`（三态 + composeMode）
- **代码**：`backend/src/main/java/com/petstore/controller/ReportController.java#startHighlight`、`service/ReportHighlightVideoService.java#requestGenerate`
- **文档**：`docs/洗美报告短视频成片方案.md`
- **证据**：`anchored`（接口与上下文派生已实现；FFmpeg 集成测试 `gap`）

## User 管理相关

### action:create_staff

旧“直接创建员工”入口已停用，固定返回 `DIRECT_STAFF_CREATION_DISABLED`；新员工必须由老板创建限时邀请后本人接受。

- **入口**：`POST /api/user/create-staff` → `UserController#createStaff`（deprecated）
- **适用规则**：`rule:BR-USER-001`（仅 boss）
- **代码**：`backend/src/main/java/com/petstore/controller/UserController.java#createStaff`
- **证据**：`anchored`

### action:list_staff

员工列表：仅 boss/staff；`storeId=current.storeId`。

- **入口**：`GET /api/user/staff-list` → `UserController#staffList`
- **适用规则**：`rule:BR-USER-001`
- **代码**：`backend/src/main/java/com/petstore/controller/UserController.java#staffList`
- **证据**：`anchored`

### action:delete_staff

删除员工：仅 boss；目标必须是本店 `staff`，不能删除 boss。删除后活跃账号校验会立即让旧 session token 失效，并写低敏 AuditLog。

- **入口**：`DELETE /api/user/staff` → `UserController#deleteStaff`
- **适用规则**：`rule:BR-USER-001`
- **代码**：`backend/src/main/java/com/petstore/controller/UserController.java#deleteStaff`
- **证据**：`anchored`

### action:update_user

更新当前登录用户信息（头像/姓名/手机号）：`id=current.userId`（覆盖请求体）；手机号变更需 SMS 验证码（生产禁用）。

- **入口**：`PUT /api/user/update` → `UserController#updateUser` → `UserService#updateUser`
- **适用规则**：`rule:BR-USER-002`（仅本人）、`rule:BR-AUTH-002`（生产手机号变更禁用）
- **代码**：`backend/src/main/java/com/petstore/controller/UserController.java#updateUser`
- **证据**：`anchored`

---

## Batch 2.1 补录动作（audit_drift 发现的遗漏）

## 公开查询类（在 AuthInterceptor allowlist）

### action:list_stores

门店列表：公开接口，经 `toPublicStoreView` 裁剪（不含 ownerId/inviteCode）。

- **入口**：`GET /api/store/list` → `StoreController#list`（公开）
- **适用规则**：`rule:BR-STORE-003`
- **代码**：`backend/src/main/java/com/petstore/controller/StoreController.java#list`
- **测试**：`StoreControllerTest#publicListHidesOwnerIdAndInviteCode`
- **证据**：`anchored`

### action:get_store

门店详情：公开接口，经 `toPublicStoreView` 裁剪（含 intro/营业时段，不含 ownerId/inviteCode/deleted）。

- **入口**：`GET /api/store/get` → `StoreController#get`（公开）
- **适用规则**：`rule:BR-STORE-003`
- **代码**：`backend/src/main/java/com/petstore/controller/StoreController.java#get`
- **测试**：`StoreControllerTest#publicGetHidesOwnerIdAndInviteCode`
- **证据**：`anchored`

### action:get_available_slots

门店某日可预约开始时段：公开接口，接收可选 `serviceType`，按服务时长检查覆盖的所有容量桶，返回开始/结束时间、容量与不可约原因。

- **入口**：`GET /api/appointment/available-slots` → `AppointmentController#availableSlots`（公开）
- **适用规则**：`rule:BR-APPT-004`（时段校验）
- **代码**：`backend/src/main/java/com/petstore/controller/AppointmentController.java#availableSlots`
- **证据**：`anchored`

### action:get_report_suggestion

报告下次建议日期：公开接口，凭 token 查询（读取 `entity:service_interval` 推算）。

- **入口**：`GET /api/report/{token}/suggestion` → `ReportLeadController#suggestion`（公开）
- **适用规则**：`rule:BR-INT-001`
- **代码**：`backend/src/main/java/com/petstore/controller/ReportLeadController.java#suggestion`
- **证据**：`anchored`

### action:submit_testimonial

宠主寄语提交：公开接口，凭 token 提交；同一报告保留一条，重复提交更新。

- **入口**：`POST /api/report/{token}/testimonial` → `ReportController#submitTestimonial`（公开）
- **输出**：`entity:report_testimonial`
- **适用规则**：`rule:BR-TST-001`
- **触发事件**：`event:testimonial_saved`
- **代码**：`backend/src/main/java/com/petstore/controller/ReportController.java#submitTestimonial`
- **证据**：`anchored`

### action:get_legacy_upload

兼容旧路径读取上传资源：公开接口，路径归一化防逃逸。

- **入口**：`GET /api/upload/legacy/**` → `FileController#getLegacyImage`（公开）
- **适用规则**：`rule:BR-UPLOAD-001`
- **代码**：`backend/src/main/java/com/petstore/controller/FileController.java#getLegacyImage`
- **证据**：`anchored`

## 注册/短信类（公开）

### action:register_staff

永久邀请码员工注册已停用，固定返回 `410 / LEGACY_INVITE_DISABLED`。

- **入口**：`POST /api/user/register-staff` → `UserController#registerStaff`（公开、deprecated）
- **适用规则**：`rule:BR-STORE-002`
- **代码**：`backend/src/main/java/com/petstore/controller/UserController.java#registerStaff`
- **证据**：`anchored`

### action:send_sms

发送短信验证码：公开接口；**生产禁用**（`universalSmsCode` 为空时返回 403 `SMS_LOGIN_DISABLED`）。

- **入口**：`POST /api/sms/send` → `SmsController#send`（公开）
- **适用规则**：`rule:BR-AUTH-002`
- **代码**：`backend/src/main/java/com/petstore/controller/SmsController.java#send`
- **证据**：`anchored`

### action:register_store

店铺注册旧入口固定返回 `LEGACY_STORE_REGISTRATION_DISABLED`，不创建 Store。

- **入口**：`POST /api/store/register` → `StoreController#register`
- **状态**：`deprecated`
- **代码**：`backend/src/main/java/com/petstore/controller/StoreController.java#register`
- **证据**：`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）

### action:delete_appointment

删除预约：仅 boss/staff 且同店。

- **入口**：`DELETE /api/appointment/delete` → `AppointmentController#delete`
- **适用规则**：`rule:BR-APPT-002`
- **代码**：`backend/src/main/java/com/petstore/controller/AppointmentController.java#delete`
- **证据**：`anchored`

### action:list_reports

报告列表：boss/staff 查本店，customer 查自己相关报告；身份从上下文派生。

- **入口**：`GET /api/report/list` → `ReportController#list`
- **适用规则**：`rule:BR-RPT-005`（跨店 403）
- **代码**：`backend/src/main/java/com/petstore/controller/ReportController.java#list`
- **证据**：`anchored`

## 客户端埋点 / 会话生命周期

### action:track_video_play

成片点击播放埋点（前端客户端事件）。

- **入口**：frontend `reportView` / `videoPlayer`（client-emitted）
- **触发事件**：`event:video_play`
- **证据**：`documented`（schema 已冻结；上报与统计落地 `gap`）

### action:track_video_save

成片保存到相册埋点（前端客户端事件）。

- **入口**：frontend `reportView` / `videoPlayer`（client-emitted）
- **触发事件**：`event:video_save`
- **证据**：`documented`（schema 已冻结；上报与统计落地 `gap`）

### action:logout

退出登录 / 吊销会话：当前仅前端清本地 token，**无服务端吊销**。

- **入口**：—（未实现）
- **触发事件**：`event:session_revoked`
- **适用规则**：`rule:BR-AUTH-001`
- **证据**：`gap`

### action:get_user_info

获取当前登录用户信息：`userId=current.userId`。

- **入口**：`GET /api/user/info` → `UserController#info`
- **适用规则**：`rule:BR-USER-002`、`rule:BR-USER-003`
- **代码**：`backend/src/main/java/com/petstore/controller/UserController.java#info`
- **证据**：`anchored`

---

## 门店 Web 后台（Phase A）

> 产品：`docs/门店管理后台升级方案-对标宠老板.md`。优先复用现有 `/api/**`，不另造状态机。

### action:view_workbench

查看今日行动工作台（登录落点）。服务端按完整本店数据聚合逾期预约、服务中、今日待开始、成片异常、新报告待发送和到期 FollowUpTask，返回准确总数与低敏预览；报告提交、显式确认发送、打开、留资与回访后真实再次预约均通过 BusinessEvent 汇总。

- **入口**：Admin UI `/workbench`；`GET /api/admin/workbench/actions`；兼容 `GET /api/admin/workbench/report-funnel?date=yyyy-MM-dd`
- **输出**：`entity:workbench`、`entity:workbench_todo_item`
- **适用规则**：`rule:BR-ADMIN-001`、`rule:BR-ADMIN-002`、`rule:BR-ADMIN-004`、`rule:BR-BE-001`
- **触发事件**：`event:workbench_viewed`（可选，gap）
- **代码**：`AdminBusinessEventController`、`AdminWorkbenchService`、`AdminBusinessEventService`、`admin/src/views/WorkbenchView.vue`
- **测试**：`AdminBusinessEventControllerTest`、`AdminWorkbenchServiceTest`、`AdminBusinessEventServiceTest`
- **证据**：`anchored`

### action:drilldown_workbench_todo

工作台行动组或具体预览下钻到对应列表并带筛选；预约预览可按 ID 直接打开受保护详情。

- **入口**：Admin UI workbench action click
- **适用规则**：`rule:BR-ADMIN-001`、`rule:BR-ADMIN-002`
- **代码**：`admin/src/views/WorkbenchView.vue`、`AppointmentsView.vue`
- **证据**：`anchored`

### action:admin_list_appointments

后台预约列表（增强筛选；规则复用 Appointment*）。

- **入口**：Admin UI `/appointments`（planned）；数据复用 `GET /api/appointment/list`
- **适用规则**：`rule:BR-ADMIN-001`、`rule:BR-ADMIN-002`、`rule:BR-APPT-001`、`rule:BR-APPT-002`
- **证据**：`documented`

### action:admin_list_reports

后台报告列表（成片三态、发送三态、复制公开链接、显式确认发送、触发重生成）。

- **入口**：Admin UI `/reports`；复用 `GET /api/report/list` + `action:confirm_report_sent` + `action:generate_highlight`
- **适用规则**：`rule:BR-ADMIN-001`、`rule:BR-ADMIN-002`、`rule:BR-ADMIN-003`、`rule:BR-RPT-005`、`rule:BR-RPT-008`
- **证据**：`anchored`

### action:admin_list_leads

后台回访任务池。导航路径保留 `/leads`，操作对象已由原始留资切换为 `FollowUpTask`。

- **入口**：Admin UI `/leads`；`GET /api/admin/follow-up-tasks`
- **输入**：`status`、`dueDateTo`、`page`、`pageSize`
- **输出**：`entity:follow_up_task`
- **适用规则**：`rule:BR-ADMIN-001`、`rule:BR-ADMIN-002`、`rule:BR-FU-001`
- **代码/测试**：`AdminFollowUpTaskController#list`、`FollowUpTaskService#list`、`admin/src/views/LeadsView.vue`；`AdminFollowUpTaskControllerTest`、`FollowUpTaskServiceTest`
- **证据**：`anchored`

### action:start_follow_up_task

领取待处理回访任务；同一任务只能由一名门店员工持有。

- **入口**：`POST /api/admin/follow-up-tasks/{taskId}/start`
- **输出**：`entity:follow_up_task`
- **适用规则**：`rule:BR-FU-001`
- **代码/测试**：`AdminFollowUpTaskController#start`、`FollowUpTaskService#start`；`FollowUpTaskServiceTest`
- **证据**：`anchored`

### action:reschedule_follow_up_task

领取人登记 `no_answer/follow_later` 并设置下次日期，联系次数加一，任务回到 `pending`。

- **入口**：`POST /api/admin/follow-up-tasks/{taskId}/reschedule`
- **输入**：`reason`、`dueDate`
- **输出**：`entity:follow_up_task`
- **适用规则**：`rule:BR-FU-001`
- **证据**：`anchored`

### action:close_follow_up_task

领取人以 `not_interested/invalid_contact` 关闭本轮任务；`rebooked` 不允许手工选择。

- **入口**：`POST /api/admin/follow-up-tasks/{taskId}/close`
- **输入**：`outcome`
- **输出**：`entity:follow_up_task`
- **适用规则**：`rule:BR-FU-001`、`rule:BR-FU-002`
- **证据**：`anchored`

### action:admin_list_service_customers

后台服务客户列表（瘦身主档；姓名/手机/宠物名搜索）。

- **入口**：`GET /api/admin/service-customers` → `AdminServiceCustomerController#listServiceCustomers` → Admin UI `/customers`
- **输出**：`entity:store_customer`
- **适用规则**：`rule:BR-ADMIN-001` … `BR-ADMIN-004`、`rule:BR-SC-001`
- **证据**：`anchored`
- **代码**：`backend/src/main/java/com/petstore/controller/AdminServiceCustomerController.java`、`AdminServiceCustomerService`
- **产品决策**：以 `t_store_customer` 稳定主档为入口聚合，返回 `storeCustomerId`；不做余额/次卡列、持卡会员导入
- **文档**：`docs/门店管理后台-PhaseA-页面清单PRD.md §7.6`

### action:admin_update_settings

后台门店设置（资料、营业时段、服务类型瘦身表单、员工）。

- **入口**：`GET /api/admin/store` → `AdminStoreController#currentStore`；Admin UI `/settings`；写资料 `PUT /api/store/update`
- **适用规则**：`rule:BR-ADMIN-001`、`rule:BR-ADMIN-002`、`rule:BR-ADMIN-003`、`rule:BR-STORE-001`、`rule:BR-ST-001`、`rule:BR-USER-001`
- **证据**：`anchored`
- **代码**：`AdminStoreController`、`StoreController#update`；服务类型/员工复用既有 API

### action:view_store_customer_timeline

查看本店某个 StoreCustomer 的事实时间线。

- **入口**：`GET /api/admin/service-customers/timeline?storeCustomerId=&page=&pageSize=` → `AdminServiceCustomerController#timeline` → Admin UI `/customers` 详情抽屉
- **输入**：`storeCustomerId`、`page`、`pageSize`；`storeId` 强制从当前会话派生
- **输出**：`entity:customer_timeline`
- **适用规则**：`rule:BR-AUTH-001`、`rule:BR-ADMIN-001`～`003`、`rule:BR-SC-001`、`rule:BR-SC-002`、`rule:BR-BE-001`
- **行为**：解析 canonical 主档及其同店合并别名，按 `occurredAt DESC, id DESC` 分页读取全部关联 BusinessEvent；批量补齐预约/报告/留资/FollowUpTask 的白名单摘要并提供详情跳转 ID，既有事件不改写
- **代码/测试**：`StoreCustomerTimelineService`、`CustomersView.vue`；`StoreCustomerTimelineServiceTest`、`AdminServiceCustomerControllerTest`
- **证据**：`anchored`

### action:admin_manage_schedule

后台排班与手动占用（日视图）。

- **入口**：Admin UI `/schedule`；`GET /api/schedule/day`；`POST/DELETE /api/schedule/block`
- **适用规则**：`rule:BR-ADMIN-001`、`rule:BR-ADMIN-002`、`rule:BR-SCH-001`
- **证据**：`anchored`

## 智能预约助手 M0（Batch 0 · documented）

> 下列入口已冻结但尚未注册；机器图以 `planned METHOD /path` 标识，不得误报为已实现 endpoint。

### action:create_booking_agent_session

使用已选有效 `storeId` 为当前 customer 创建短期会话，并首次记录 `booking_agent_started`。

- **计划入口**：`POST /api/booking-agent/sessions`
- **输出**：`entity:booking_agent_session`
- **规则**：`rule:BR-AUTH-001`、`rule:BR-BA-001`、`rule:BR-BA-003`、`rule:BR-BA-005`、`rule:BR-BE-001`
- **证据**：`documented`

### action:submit_booking_agent_message

在 `sessionId + current.userId` 范围内校验草稿版本，将当轮文字解析为非权威约束，再由后端解析真实宠物、服务与号源。

- **计划入口**：`POST /api/booking-agent/sessions/{sessionId}/messages`
- **输入/输出**：`entity:booking_agent_session`、`draftVersion`、`inputType`、`text`
- **规则**：`rule:BR-BA-001` … `rule:BR-BA-005`
- **触发事件**：首次进入 `confirmable` 时 `event:booking_agent_draft_ready`
- **证据**：`documented`

### action:transcribe_booking_agent_audio

在本人未终止 session 下将短音频转为可编辑文字，不自动进入意图处理。

- **计划入口**：`POST /api/booking-agent/sessions/{sessionId}/transcriptions`
- **规则**：`rule:BR-BA-001`、`rule:BR-BA-004`、`rule:BR-BA-005`
- **证据**：`documented`

### action:handoff_booking_agent_draft

返回服务端验证的部分或完整草稿，将会话转为 `fallback`，由现有表单承担最终提交。

- **计划入口**：`POST /api/booking-agent/sessions/{sessionId}/fallback`
- **输出**：一次性草稿交接对象
- **规则**：`rule:BR-BA-001`、`rule:BR-BA-003`、`rule:BR-BA-005`
- **触发事件**：`event:booking_agent_fallback`
- **证据**：`documented`

### action:cancel_booking_agent_session

由当前 customer 结束本人未终止会话，仅将 session 标记为 `cancelled`。

- **计划入口**：`DELETE /api/booking-agent/sessions/{sessionId}`
- **规则**：`rule:BR-BA-001`、`rule:BR-BA-003`
- **证据**：`documented`
