# 架构决策：真实预约容量模型

日期：2026-08-01  
状态：Accepted  
范围：单店 SaaS 真实试点

## 决策

1. 服务项目配置 `duration_minutes`，范围 30～480 分钟且按 30 分钟递增。
2. 预约保存 `duration_minutes` 快照，后续修改服务项目不会改变已承诺号源。
3. 门店配置 `booking_capacity`（1～10），表示同一半小时容量桶可并行服务的顾客数。
4. 可约性必须检查服务从开始到结束覆盖的每个半小时桶；有效预约和 `walk_in` 各消耗一个名额，`blocked` 关闭覆盖范围内全部名额。
5. 创建预约或手动占用时，对门店行使用悲观写锁，在同一事务内重新读取占用并判定，避免两个并发请求同时通过可用性检查。
6. `booking_last_slot_start` 保留兼容语义：它是最后一个 30 分钟容量桶的开始时间，营业容量结束时刻为其后 30 分钟；服务必须在结束时刻前完成。

## 为什么先做门店容量

现有预约在开始服务时才绑定 `assigned_user_id`，宠主下单也不选择技师。在这种数据结构下宣称已经具备“技师排班”会制造假精度。试点阶段先由老板配置门店可真实兑现的并发接待数，足以阻止长服务跨档冲突和同店并发超卖。

个人技师日历延后到预约前可分配技师、员工可维护可工作时段、服务项目可声明技师技能之后再引入。届时门店容量仍作为总上限，不会推翻本决策。

## 兼容与迁移

- 旧接口未传 `serviceType` 查询号源时采用 60 分钟安全默认值；创建预约必须解析到本店有效服务项目。
- 历史系统默认与预约按稳定名称映射回填：洗澡 60、美容 120、洗澡+美容 150、剪指甲/驱虫 30，未知服务 60 分钟。
- 旧手动占用回填为 30 分钟；旧门店并发接待数回填为 1。
- 生产不自动执行迁移，发布前按 `backend/db/migrations/README.md` 备份、测试和人工执行。

## 验收

- 120 分钟服务会占用连续四个半小时桶。
- 容量为 2 时，同一桶允许两个有效占用，第三个被拒绝。
- `walk_in` 消耗一个名额；`blocked` 覆盖处全部不可约。
- 候选服务结束晚于营业容量结束时刻时不可约。
- 并发创建不会突破门店容量。
