Conversion Sub-DAG — 预约转化链路审计

5 个入口 / 1 个汇聚点 / conflict-detector + notification-dispatcher 覆盖检查
2026-03-22 | Spec 098-business-dag-audit > conversion

收敛覆盖度

核心问题:5 个预约入口是否全部经过统一的 booking-service + conflict-detector 管线?

5
总入口数
3
完整覆盖
2
绕过管线
60%
收敛率
3/5
入口经过 conflict-detector
3/5
入口触发 notification-dispatcher

转化链路 DAG

完整管线 (conflict + notification)
绕过管线 (直接 INSERT)
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
    

边审计明细

入口 → conflict-detector

状态涉及端口代码路径备注
OK Online Booking → conflict-detector 3000 (public API) frontend/.../booking/[store]/verify/page.tsxuseConfirmBooking()POST /api/public/booking/confirmconflictDetector.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.jsPOST /api/voice/internal/appointmentsapi/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.jscreate_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/appointmentsapi/appointments.js L861 this.checkMultiServiceConflicts() 完整链路。appointments.js 内部有独立实现的 checkMultiServiceConflicts() (L2680) 和 checkEmployeeConflict() (L2807), 与 conflict-detector.js 单例逻辑等效但为独立副本。 同时使用 acquireMultiBookingLocks() (L887)。

预约创建 → notification-dispatcher

状态涉及端口代码路径备注
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() (旧管道双写模式)。

架构备注

conflict-detector 双副本问题

系统存在两份冲突检测实现:

1. services/booking/conflict-detector.js — 单例 class,被 booking-service.js 和 public/booking.js 使用

2. api/appointments.js 内部的 checkMultiServiceConflicts() / checkEmployeeConflict() — 独立实现,被 Front Desk 使用

两者逻辑等效但代码独立维护,存在分歧风险。建议统一到 conflict-detector.js 单例。

booking-service.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)都各自内联了部分或全部步骤。 没有真正的"单一预约创建入口"。

断裂点与修复建议

严重 (FAIL) — 绕过冲突检测

建议 (WARN) — 架构改进

各入口详细链路

1. Online Booking (Web/Mobile)

链路路径

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

2. Kiosk Walk-in (iPad Terminal)

链路路径

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 (匿名客人可能静默跳过)

3. Phone Bot (Twilio Voice Agent)

链路路径

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

4. Agent Chat (AI SOP Walk-in Tool)

链路路径

agents/tools/appointment.tools.jscreate_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

5. Front Desk (Staff Web Portal)

链路路径

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