# 架构决策：预约与报告身份字段拆分

> 日期：2026-08-01
>
> 状态：已采纳，兼容迁移期
>
> 范围：Backend Core、Report Media Backend、Store Miniapp、Store Admin

## 1. 决策背景

RC1 之前，`Appointment.user_id` 同时被解释为客户或创建人；门店代客预约会把操作员工写入该字段。`Report.user_id` 实际保存技师/报告提交人，但宠主报告列表又曾把它当客户归属。两处语义冲突会污染客户档案、报告可见性、回访归因和后续经营指标。

## 2. 冻结语义

| 聚合 | 字段 | 语义 |
|---|---|---|
| Appointment | `customer_user_id` | 被服务的客户账号 |
| Appointment | `created_by_user_id` | 实际创建预约的登录账号，可为 customer/boss/staff |
| Appointment | `assigned_user_id` | 服务技师；API 新增语义别名 `assignedStaffId` |
| Appointment | `user_id` | legacy 兼容字段；新数据镜像 `customer_user_id` |
| Report | `customer_user_id` | 报告归属客户，从 Appointment 派生 |
| Report | `author_staff_id` | 实际提交报告的门店账号 |
| Report | `user_id` | legacy 兼容字段；新数据镜像 `author_staff_id` |
| Report | `staff_name` | 服务技师姓名快照，优先取 Appointment.assigned_user_id |

“服务技师”和“报告作者”允许不同：老板可代员工提交报告，但报告仍展示实际服务技师。

## 3. 写入规则

### 宠主自行预约

- `customerUserId = current.userId`
- `createdByUserId = current.userId`
- 忽略请求体中的身份字段

### 门店代客预约

- `createdByUserId = current.userId`
- 必须提交 `customerUserId` 或 `customerPhone`
- 手机号不存在时创建 customer 账号；若手机号已绑定 boss/staff，则拒绝静默复用
- `petId` 存在时必须属于解析出的客户

### 提交报告

- `authorStaffId = current.userId`
- `customerUserId = appointment.customerUserId`
- 服务技师继续来自 `appointment.assignedUserId`

## 4. 历史回填

迁移脚本：`backend/db/migrations/20260801_split_service_identity.sql`。

预约客户按以下证据顺序回填：

1. `pet.owner_user_id`
2. legacy `user_id` 且对应账号角色为 customer
3. 无可靠证据则保持空值，进入人工修正清单

报告客户只从关联预约回填；`report.user_id` 仅用于回填 `author_staff_id`，禁止当作客户。

## 5. 兼容与退出条件

- 兼容期继续读写 legacy `userId` / `assignedUserId`，同时输出 canonical 字段。
- 客户预约查询优先 `customer_user_id`，字段为空时回退 legacy `user_id`。
- 客户报告查询通过 `report.customer_user_id` 或关联预约归属，不再按 `report.user_id` 查询。
- 只有在生产回填完成、旧客户端退出、关键指标稳定后，才可另立迁移删除 legacy 字段。

## 6. 非目标

本决策不引入多角色账号、跨店客户共享、StoreCustomer 主档、订单/支付或多门店组织；这些保持在后续独立阶段。
