2.9 KiB
2.9 KiB
架构决策:StoreCustomer 门店客户主档
日期:2026-08-01
状态:已采纳,待生产数据迁移
范围:Backend Core、Report Media Backend、Store Admin、Data Model
1. 决策背景
原「服务客户」列表每次请求时才从 User、Pet、Appointment、Report 和 ReportLead 拼装。同一个客户在不同门店没有稳定关系 ID,留资手机号与后续 customer 账号也可能生成两条投影,无法可靠承载客户时间线、回访任务和再次预约。
2. 冻结模型
StoreCustomer 是「门店与客户的关系主档」,不是全局账号,也不是会员资产。
| 字段 | 语义 |
|---|---|
id |
本店客户稳定 ID,后续时间线/回访任务的关联键 |
store_id |
门店数据范围 |
customer_user_id |
全局 customer 账号;可空,便于先承接留资 |
phone |
本店最后确认联系号;后台 API 默认脱敏 |
display_name |
门店视角称呼 |
source |
首次来源:customer_booking / assisted_booking / report_lead |
first_contact_at / last_contact_at |
本店关系首次/最近触达时间 |
merged_into_store_customer_id |
软删除投影并入的同店 canonical 主档;保留不可变事件归属连续性 |
同店分别对 (store_id, customer_user_id) 和 (store_id, phone) 加唯一约束。customer_user_id 为空时可先以手机号建档;后续识别到 customer 账号时绑定或合并,禁止同手机号指向两个 customer。
3. 写入入口
- 宠主自助预约:建档来源
customer_booking。 - boss/staff 代客预约:建档来源
assisted_booking。 - 报告留资:建档来源
report_lead;若手机号已对应 customer,直接绑定。 - 后续触达只更新
last_contact_at,不覆盖可靠的首次来源。
4. 读模型与口径
GET /api/admin/service-customers 以 StoreCustomer 为列表入口,再聚合宠物、预约、报告和留资,返回稳定 storeCustomerId。
lastVisitAt只计算已开始/已完成的历史服务;未来预约不算到店。lastReportId、leadStatus、pets仍从事实表实时读取,避免将易变经营指标重复写入主档。- 返回
phoneMasked,不返回明文phone。
5. 历史迁移
按顺序执行:
backend/db/migrations/20260801_split_service_identity.sqlbackend/db/migrations/20260801_create_store_customer.sql
第二个脚本先从已确定客户归属的预约建档,再合并报告留资。末尾三个验证计数必须全部为 0 才可发布新后端。
事实时间线阶段追加执行 backend/db/migrations/20260802_create_store_customer_timeline.sql:它只增加合并别名指针与索引,不改写既有 BusinessEvent。迁移末尾两个别名验证计数必须全部为 0。
6. 非目标
本阶段不引入会员等级、积分、储值、次卡、标签群发、跨店共享或客户合并后台。这些需要独立的权限、资产和审计模型。