核心问题:5 个预约入口是否全部经过统一的 booking-service + conflict-detector 管线?
graph LR
%% ── Entry Points ──
OB["Online Booking (Web/Mobile)"]
KI["Kiosk Walk-in (iPad Terminal)"]
PB["Phone Bot (Twilio Voice)"]
AC["Agent Chat (AI SOP)"]
FD["Front Desk (Staff Web)"]
%% ── Unified Pipeline ──
CD["conflict-detector"]
BS["booking-service / appointments API"]
BL["booking-lock (advisory)"]
ND["notification-dispatcher"]
DB[("appointments table")]
%% ── Converged paths (OK) ──
OB -->|"/api/public/booking/confirm"| CD
KI -->|"/api/public/booking/confirm source=kiosk"| CD
FD -->|"POST /api/appointments"| CD
CD -->|"checkEmployeeConflict()"| BS
BS -->|"acquireBookingLock()"| BL
BL -->|"INSERT"| DB
BS -->|"async"| ND
%% ── Bypassed paths (FAIL) ──
PB -->|"/api/voice/internal/appointments"| DB
AC -->|"create_walkin_appointment"| DB
%% ── Styling ──
classDef entryOk fill:#0d2818,stroke:#22c55e,stroke-width:2px,color:#e0e0e0
classDef entryFail fill:#2d1014,stroke:#ef4444,stroke-width:2px,color:#e0e0e0
classDef pipeline fill:#161b22,stroke:#64ffda,stroke-width:2px,color:#e0e0e0
classDef db fill:#1c1d21,stroke:#8b949e,stroke-width:2px,color:#e0e0e0
class OB,KI,FD entryOk
class PB,AC entryFail
class CD,BS,BL,ND pipeline
class DB db
linkStyle 0 stroke:#22c55e,stroke-width:2px
linkStyle 1 stroke:#22c55e,stroke-width:2px
linkStyle 2 stroke:#22c55e,stroke-width:2px
linkStyle 3 stroke:#22c55e,stroke-width:2px
linkStyle 4 stroke:#22c55e,stroke-width:2px
linkStyle 5 stroke:#22c55e,stroke-width:2px
linkStyle 6 stroke:#22c55e,stroke-width:2px
linkStyle 7 stroke:#ef4444,stroke-width:2px,stroke-dasharray:5
linkStyle 8 stroke:#ef4444,stroke-width:2px,stroke-dasharray:5
| 状态 | 边 | 涉及端口 | 代码路径 | 备注 |
|---|---|---|---|---|
| OK | Online Booking → conflict-detector | 3000 (public API) |
frontend/.../booking/[store]/verify/page.tsx →
useConfirmBooking() →
POST /api/public/booking/confirm →
conflictDetector.checkEmployeeConflict()
|
完整链路。verify/page.tsx L316 调用 confirmMutation;
booking.js L2926 导入 conflict-detector 单例并执行
checkEmployeeConflict()。
每个 sub_appointment 逐个检查冲突。
|
| OK | Kiosk Walk-in → conflict-detector | 3000 (public API) |
frontend/.../terminal/[store_code]/walkin/page.tsx L216 →
POST /api/public/booking/confirm (source='kiosk') →
conflictDetector.checkEmployeeConflict()
|
与 Online Booking 共用同一个 confirm 端点。
Kiosk 通过 source='kiosk' + X-Device-Type: kiosk 头标识,
跳过验证码校验但冲突检测逻辑相同 (booking.js L2924-2940)。
|
| FAIL | Phone Bot → conflict-detector | 3000 (voice internal API) |
services/voice/agents/inbound/tools/create-booking.js →
POST /api/voice/internal/appointments →
api/voice/internal.js L619-712
|
直接 INSERT 到 appointments 表(L668-689), 完全绕过 conflict-detector。无冲突检查、无 advisory lock、无 notification。 存在双重预约风险。 |
| FAIL | Agent Chat → conflict-detector | N/A (in-process) |
agents/tools/appointment.tools.js →
create_walkin_appointment handler L184-227
|
直接 INSERT via tenantDb.transactionWithTenant(),
完全绕过 conflict-detector。
check_technician_availability tool 虽存在但与 create 分离,
agent 可能不调用。
|
| OK | Front Desk → conflict-detector | 3000 (admin API) |
frontend/.../appointments/ →
POST /api/appointments →
api/appointments.js L861
this.checkMultiServiceConflicts()
|
完整链路。appointments.js 内部有独立实现的
checkMultiServiceConflicts() (L2680) 和
checkEmployeeConflict() (L2807),
与 conflict-detector.js 单例逻辑等效但为独立副本。
同时使用 acquireMultiBookingLocks() (L887)。
|
| 状态 | 边 | 涉及端口 | 代码路径 | 备注 |
|---|---|---|---|---|
| OK | Online Booking → notification-dispatcher | 3000 |
api/public/booking.js L3204-3247 →
notificationDispatcher.sendBookingConfirmation() 或
automationEvents.appointmentCreated()
|
使用 setImmediate() 异步发送,不阻塞预约创建。
根据 UNIFIED_NOTIFICATION_PIPELINE feature flag
选择新旧两条通知管道。
|
| OK | Kiosk Walk-in → notification-dispatcher | 3000 | 同 Online Booking 路径(共用 confirm 端点) | Kiosk 匿名客人(无 phone/email)可能导致通知静默跳过, 但代码路径本身存在。 |
| FAIL | Phone Bot → notification-dispatcher | 3000 | api/voice/internal.js L619-712 |
无任何通知发送。INSERT 后直接 sendCreated() 响应。 客人不会收到确认短信/邮件。 |
| FAIL | Agent Chat → notification-dispatcher | N/A | agents/tools/appointment.tools.js L184-227 |
无任何通知发送。事务完成后直接返回 appointment 对象。 |
| OK | Front Desk → notification-dispatcher | 3000 |
api/appointments.js L1182-1196 →
notificationDispatcher.sendConfirmation() 或
automationEvents.appointmentCreated()
|
同样根据 feature flag 选择管道。
额外调用 notificationService.notifyAppointmentCreated()
(旧管道双写模式)。
|
系统存在两份冲突检测实现:
1. services/booking/conflict-detector.js — 单例 class,被 booking-service.js 和 public/booking.js 使用
2. api/appointments.js 内部的 checkMultiServiceConflicts() / checkEmployeeConflict() — 独立实现,被 Front Desk 使用
两者逻辑等效但代码独立维护,存在分歧风险。建议统一到 conflict-detector.js 单例。
BookingService class (services/booking/booking-service.js) 封装了 blacklist-check → conflict-detect → INSERT → notification 的完整管线。
但实际只有 booking-service.js 自身的 createBooking() 使用了这个封装。
其他 3 个入口(public/booking.js confirm、appointments.js createAppointment、voice/internal.js)都各自内联了部分或全部步骤。 没有真正的"单一预约创建入口"。
POST /api/voice/internal/appointments 直接 INSERT,
无 conflict-detector、无 advisory lock、无 notification。
conflictDetector.checkEmployeeConflict() +
acquireBookingLock() + notificationDispatcher.sendBookingConfirmation()。
create_walkin_appointment handler 直接 INSERT,
无 conflict-detector、无 advisory lock、无 notification。
conflictDetector.checkEmployeeConflict()
检查(需 tenantDb 兼容方式),事务后调用 notificationDispatcher。
BookingService.createAppointment() 方法,
将 blacklist-check、conflict-detect、advisory-lock、INSERT、notification
封装为原子管线,所有入口统一调用。
api/appointments.js 内部的冲突检测实现
应迁移到 services/booking/conflict-detector.js 单例,
避免逻辑分叉。
acquireBookingLock / acquireMultiBookingLocks)。
Phone Bot 和 Agent Chat 完全没有并发保护。
frontend/.../booking/[store]/verify/page.tsx L316 confirmMutation.mutateAsync()
→ hooks/api/useBookingApi.ts L528 fetch /api/public/booking/confirm
→ backend/api/public/booking.js L2612 router.post('/confirm')
→ L2926 conflictDetector.checkEmployeeConflict() (per sub-appointment)
→ L2968-2974 acquireMultiBookingLocks()
→ L2978-3005 INSERT INTO appointments
→ L3204-3247 notificationDispatcher.sendBookingConfirmation()
覆盖: conflict-detector YES | advisory-lock YES | notification YES
frontend/.../terminal/[store_code]/walkin/page.tsx L216 fetch /api/public/booking/confirm
→ 同 Online Booking 路径(source='kiosk', X-Device-Type: kiosk)
→ L2684 Kiosk 模式:跳过验证码但保留冲突检测
→ L2864 仅允许当天预约
覆盖: conflict-detector YES | advisory-lock YES | notification YES (匿名客人可能静默跳过)
services/voice/agents/inbound/tools/create-booking.js L58 fetch /api/voice/internal/appointments
→ api/voice/internal.js L619 router.post('/appointments')
→ L640-664 查找/创建 guest
→ L668-689 直接 INSERT INTO appointments (无冲突检查)
→ L691-704 sendCreated() (无通知)
覆盖: conflict-detector NO | advisory-lock NO | notification NO
agents/tools/appointment.tools.js → create_walkin_appointment handler L184
→ tenantDb.transactionWithTenant()
→ L200-209 直接 INSERT INTO appointments (无冲突检查)
→ L214-219 INSERT INTO sub_appointments
→ L225 return appointment (无通知)
注:check_technician_availability tool 存在 (L77-139),可检查技师可用性。
但它是独立 tool,agent 调用 create_walkin_appointment 时不强制先调用。
冲突检测依赖 LLM 判断而非代码保证。
覆盖: conflict-detector NO | advisory-lock NO | notification NO
frontend/.../appointments/ → POST /api/appointments
→ api/appointments.js L671 createAppointment()
→ L801-852 auto-assign + pendingAssignments 防重叠
→ L861 this.checkMultiServiceConflicts() (独立实现,非 conflict-detector 单例)
→ L887 acquireMultiBookingLocks()
→ L916-920 INSERT INTO appointments
→ L1191 notificationDispatcher.sendConfirmation()
覆盖: conflict-detector YES (独立副本) | advisory-lock YES | notification YES
| 入口 | conflict-detector | advisory-lock | notification | blacklist-check | 评级 |
|---|---|---|---|---|---|
| Online Booking | YES (单例) | YES | YES | N/A (guest JWT) | PASS |
| Kiosk Walk-in | YES (单例) | YES | YES | N/A (匿名) | PASS |
| Phone Bot | NO | NO | NO | NO | FAIL |
| Agent Chat | NO | NO | NO | NO | FAIL |
| Front Desk | YES (副本) | YES | YES | YES | PASS |