# 架构决策:预约与报告身份字段拆分 > 日期: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 主档、订单/支付或多门店组织;这些保持在后续独立阶段。