Public Booking, Checkin & Terminal | 更新时间: 2026-02-09
booking/[store]/page.tsx (服务选择),
booking/[store]/employee/page.tsx (技师选择),
booking/[store]/time/page.tsx (时间选择),
booking/[store]/verify/page.tsx (OTP验证+确认),
booking/[store]/success/page.tsx (成功页),
booking/my-appointments/page.tsx (我的预约),
booking/test/page.tsx (测试页),
admin/checkin/page.tsx (签到管理),
admin/checkin/config/page.tsx (签到配置),
admin/checkin/queue/page.tsx (排队管理),
admin/checkin/stats/page.tsx (签到统计),
checkin/[store_code]/page.tsx (公共签到),
checkin/[store_code]/success/page.tsx (签到成功),
terminal/[store_code]/page.tsx (终端欢迎页),
terminal/[store_code]/queue/page.tsx (终端排队),
terminal/[store_code]/select/page.tsx (终端选服务),
terminal/[store_code]/confirm/page.tsx (终端确认),
terminal/[store_code]/success/page.tsx (终端成功),
terminal/[store_code]/walkin/page.tsx (终端Walk-in)
Booking/ServiceSelector.tsx,
Booking/TechnicianSelector.tsx,
Booking/TimeSlotPicker.tsx,
Booking/WalkInModal.tsx,
Booking/WalkInButton.tsx,
Booking/GuestSearch.tsx,
Booking/EditAppointmentForm.tsx,
Booking/AppointmentForm.tsx,
Booking/index.tsx
stores/booking-store.ts (购物车状态, SessionStorage),
stores/guest-auth-store.ts (客户认证, LocalStorage),
hooks/api/useBookingApi.ts (API 请求封装)
backend/api/public/booking.js (公共预约 API),
backend/api/booking-config.js (预约配置),
backend/api/checkin-config.js (签到配置),
backend/api/guest-auth.js (客户认证 060)
flowchart TB
subgraph "Public Booking Pages"
P1["[store]/page.tsx\n服务选择"]
P2["[store]/employee/page.tsx\n技师选择"]
P3["[store]/time/page.tsx\n时间选择"]
P4["[store]/verify/page.tsx\nOTP验证+确认"]
P5["[store]/success/page.tsx\n成功页"]
P6["my-appointments/page.tsx\n我的预约"]
end
subgraph "State Management"
BS["booking-store.ts\nZustand + SessionStorage"]
GAS["guest-auth-store.ts\nZustand + LocalStorage"]
end
subgraph "Booking Components"
SS["ServiceSelector"]
TS["TechnicianSelector"]
TSP["TimeSlotPicker"]
GS["GuestSearch"]
end
subgraph "Backend API"
API1["GET /public/booking/stores"]
API2["GET /public/booking/services"]
API3["GET /public/booking/employees"]
API4["GET /public/booking/slots"]
API5["POST /public/booking/confirm"]
API6["POST /guest-auth/send-code"]
API7["POST /guest-auth/verify-code"]
end
P1 --> BS
P2 --> BS
P3 --> BS
P4 --> BS
P4 --> GAS
P5 --> BS
P1 -.-> SS
P2 -.-> TS
P3 -.-> TSP
P1 --> API2
P2 --> API3
P3 --> API4
P4 --> API6
P4 --> API7
P4 --> API5
| 字段 | 类型 | 说明 |
|---|---|---|
guestCarts | GuestCart[] | 多客户购物车数组(Group Booking) |
guestCarts[].guestId | string | 客户标识 |
guestCarts[].isPrimary | boolean | 是否主客户([0]始终为true) |
guestCarts[].items | BookingItem[] | 该客户的服务列表 |
BookingItem.id | string | 购物车项唯一ID |
BookingItem.service | Service | 服务对象(名称、价格、时长) |
BookingItem.employee_id | string | null | 技师ID,'any' 表示任意技师 |
BookingItem.date | string | null | YYYY-MM-DD,未选时为null |
BookingItem.time | string | null | HH:MM,未选时为null |
| Source | 说明 | 入口 |
|---|---|---|
customer_web | Web 在线预约 | /booking/[store] |
customer_mobile | Guest App 预约 | Flutter Guest App |
kiosk | 门店自助终端 | /terminal/[store_code] |
walk_in | 前台 Walk-in 登记 | WalkInModal |
admin | 后台手动创建 | QuickBookingForm |
| CUJ | 优先级 | 描述 | 触发点 | 业务价值 | E2E 状态 |
|---|---|---|---|---|---|
| CUJ-G1 | P0 | 公共在线预约 — 单服务 | 客户访问 /booking/[store] | 核心营收 — 在线获客 | 已覆盖 |
| CUJ-G2 | P0 | 公共在线预约 — 多服务购物车 | 客户添加多个服务到购物车 | 提升客单价 | 已覆盖 |
| CUJ-G3 | P1 | Group Booking(多人预约) | 客户为同伴添加服务 | 团体客户获客 | 部分覆盖 |
| CUJ-G4 | P0 | 客户 OTP 身份验证 | 进入验证页 /verify | 客户身份识别 + 防滥用 | 已覆盖 |
| CUJ-G5 | P0 | 自助终端 — Check In | 客户在门店终端签到 | 前台效率 — 自助签到 | 已覆盖 |
| CUJ-G6 | P0 | 自助终端 — Book Appointment | Walk-in 客户自助预约 | 减少前台工作 | 已覆盖 |
| CUJ-G7 | P1 | 我的预约(客户端) | 客户查看/取消预约 | 自助服务,减少爽约 | 部分覆盖 |
| CUJ-G8 | P1 | 签到管理(后台) | 管理员配置签到+查看统计 | 自定义签到体验 | 已覆盖 |
| CUJ-G9 | P1 | Walk-in 快速登记 | 前台在看板打开 WalkInModal | 不丢失 Walk-in 客户 | 已覆盖 |
P0 在线获客 — 客户通过 /booking/[store] 自助预约单个服务
flowchart TD
A["访问 /booking/store001\n服务列表页"] --> B["浏览服务分类\n选择一个服务"]
B --> C["添加到购物车\nautomatic navigate"]
C --> D["/employee 选择技师\nAny Available 或指定"]
D --> E["/time 选择日期时间\n30天内可选"]
E --> F{"购物车完整?\nisAllComplete()"}
F -->|"是"| G["点击 Proceed to Booking"]
F -->|"否"| B
G --> H["/verify 身份验证\n见 CUJ-G4"]
H --> I["确认预约\n填写备注+通知偏好"]
I --> J["POST /public/booking/confirm"]
J --> K{成功?}
K -->|"是"| L["/success 成功页\n显示确认号"]
K -->|"否"| M["错误提示\n重试"]
style A fill:#2196F3,stroke:#1565C0,color:#fff
style L fill:#4CAF50,stroke:#2E7D32,color:#fff
style M fill:#f44336,stroke:#c62828,color:#fff
/en/qqnails/booking/store001pending,source = customer_web
测试: web-booking.spec.ts
'any'测试: web-booking.spec.ts
P0 提升客单价 — 客户可在一次预约中选择多个服务
flowchart TD
A["服务列表页"] --> B["选择第1个服务"]
B --> C["配置技师+时间"]
C --> D["返回服务列表\n购物车底栏显示1项"]
D --> E["选择第2个服务"]
E --> F["配置技师+时间\n排除已占用时段"]
F --> G["返回服务列表\n购物车底栏显示2项"]
G --> H{"继续添加?"}
H -->|"是"| I["选择第N个服务"]
H -->|"否"| J["点击购物车\n查看所有项"]
J --> K["确认全部完整"]
K --> L["Proceed to Booking"]
style A fill:#2196F3,stroke:#1565C0,color:#fff
style L fill:#4CAF50,stroke:#2E7D32,color:#fff
测试: web-booking.spec.ts
getExcludedSlots() 返回已被购物车占用的时段
sub_appointments 数组P1 团体客户 — 主客户为同伴也预约服务
flowchart TD
A["主客户配置完自己的服务"] --> B["点击 Add Guest"]
B --> C["输入同伴姓名\n可选手机号"]
C --> D["为同伴选择服务"]
D --> E["配置技师+时间"]
E --> F{"继续添加同伴?"}
F -->|"是"| B
F -->|"否"| G["全部配置完成\nProceed to Booking"]
G --> H["确认提交"]
H --> I["后端创建 companion_appointments\n生成 group_code"]
style A fill:#2196F3,stroke:#1565C0,color:#fff
style I fill:#4CAF50,stroke:#2E7D32,color:#fff
group_code(如 GROUP-ABC123)POST /public/booking/confirmsub_appointments[] — 主客户的服务列表companion_appointments[] — 同伴的 {guest_name, phone?, items[]}P0 身份识别 — 060-guest-auth-redesign: 手机/邮箱 OTP 验证
flowchart TD
A["进入 /verify 页\n购物车已完整"] --> B["Step 1: 身份验证"]
B --> C{"选择验证方式"}
C -->|"手机"| D["输入手机号\nSend Code"]
C -->|"邮箱"| E["输入邮箱\nSend Code"]
D --> F["POST /guest-auth/send-code\n发送6位OTP"]
E --> F
F --> G["输入验证码\n+ 输入姓名"]
G --> H["POST /guest-auth/verify-code"]
H --> I{验证成功?}
I -->|"是"| J["获得 JWT tokens\n存入 guest-auth-store"]
I -->|"否"| K["错误提示\n可重发验证码"]
K --> F
J --> L["Step 2: 确认预约"]
L --> M["选择通知偏好\nSMS / Email"]
M --> N["可选: 填写邮箱\n可选: 备注"]
N --> O["点击确认预约"]
style A fill:#2196F3,stroke:#1565C0,color:#fff
style J fill:#4CAF50,stroke:#2E7D32,color:#fff
style K fill:#f44336,stroke:#c62828,color:#fff
POST /guest-auth/send-code 发送 SMSPOST /guest-auth/verify-code 成功测试: web-booking.spec.ts
notification_preferences: {sms: true, email: true} 随预约提交P0 自助签到 — 有预约的客户通过门店终端签到
flowchart TD
A["终端欢迎页\n/terminal/store_code"] --> B["点击 Check In"]
B --> C["输入手机号\n/checkin/store_code"]
C --> D{"找到今日预约?"}
D -->|"是"| E["显示今日预约列表"]
E --> F["选择要签到的预约"]
F --> G["状态变为 checked_in"]
G --> H["成功页\n/checkin/store_code/success"]
D -->|"否"| I["提示: 未找到预约"]
I --> J["建议预约或 Walk-in"]
style A fill:#2196F3,stroke:#1565C0,color:#fff
style H fill:#4CAF50,stroke:#2E7D32,color:#fff
style I fill:#FF9800,stroke:#E65100,color:#fff
checked_in测试: terminal-checkin.spec.ts
P0 自助下单 — Walk-in 客户通过终端预约+自动签到
flowchart TD
A["终端欢迎页"] --> B["点击 Book Appointment"]
B --> C["选择服务\n/terminal/store/select"]
C --> D["选择技师"]
D --> E["选择时间(仅当天)"]
E --> F["确认页\n/terminal/store/confirm"]
F --> G["输入姓名(必填)\n手机号(可跳过)"]
G --> H["点击确认"]
H --> I["创建预约 + 自动签到\nsource: kiosk"]
I --> J["成功页\n/terminal/store/success"]
style A fill:#2196F3,stroke:#1565C0,color:#fff
style J fill:#4CAF50,stroke:#2E7D32,color:#fff
source: 'kiosk'),不做姓名去重source: 'kiosk' + X-Device-Type: kiosk)kiosk
测试: kiosk-booking.spec.ts
appointments.guest_id NOT NULL(匿名也创建 guest 记录)测试: kiosk-booking.spec.ts
P1 自助服务 — 已验证客户查看和管理自己的预约
/booking/my-appointments测试: 部分覆盖
/booking/my-appointmentsP1 后台配置 — 管理员配置签到流程、管理排队、查看统计
/admin/checkin/config测试: full-flow/08-checkin.spec.ts
/admin/checkin/queue/admin/checkin/statsP1 不丢客户 — 前台通过 WalkInModal 快速登记无预约客户
checked_in)walk_in测试: booking/walk-in.spec.ts
| 从 G 模块 | 关联 CUJ | 关系 |
|---|---|---|
| G1/G2 在线预约 | CUJ-B1 创建预约 | 在线预约 → 后台预约列表显示 |
| G4 OTP 验证 | CUJ-A 认证 | guest-auth 独立于 employee auth |
| G5/G6 终端签到 | CUJ-B 预约状态 | 签到 → checked_in → in_service |
| G9 Walk-in | CUJ-E 客户管理 | Walk-in 创建/复用 guest 记录 |
| G1 确认后 | CUJ-F 支付 | 预约完成后进入结账 |
POST /public/booking/confirm 使用 PostgreSQL Advisory Locks(acquireMultiBookingLocks)防止并发提交导致同一时段重复预约。锁在事务结束后自动释放。
employee_available_start_times 表计算max_duration_minutes >= service.durationpendingchecked_in(跳过 pending)?ref=campaign_code 会被 booking-store 捕获为 trackingRef,随预约提交到后端,用于营销实验转化追踪。