自助终端 Kiosk — CUJ 关键用户旅程

Layer 1-3 完成 | 8 个页面 · 4 条 CUJ · 状态机驱动 · 7 项修复 · 全部 BDD 场景已验证

更新日期: 2026-02-07 | 方法论: CUJ 四步走

Step 1: CUJ 优先级矩阵

8 个页面 | 3 个 Cubit | 1 个 integration_test + 1 个 mock 集成测试 + 8 个 Widget 测试 + 6 个单元测试

CUJ 优先级 描述 触发点 业务价值 测试状态
K-CUJ-1 P0 Walk-in 签到 客人到店,已有预约 减轻前台工作量,加快排队速度 已覆盖
K-CUJ-2 P0 Walk-in 现场预约 客人想当天预约 转化 Walk-in 客人,增加预约量 已覆盖
K-CUJ-3 P0 签到未果转预约 客人签到查不到预约 挽回流失客人,将困惑转化为预约 已覆盖
K-CUJ-4 P1 管理员初始设置 首次配置 Kiosk 一次性设置,启用所有其他旅程 已覆盖

K-CUJ-1: Walk-in 签到

P0 已有预约的客人自助签到

Layer 1: CUJ 分析

触发点 客人到店,已有预约,需要通知前台自己到了。看到门口的 iPad Kiosk,决定自助签到而非排队等前台。
阶段 1. 触屏唤醒2. 选择"签到"3. 输入手机号4. 查看预约列表5. 确认签到6. 完成
情感曲线 到店 — 中性 看到 Kiosk — 好奇 输入手机号 — 轻微焦虑(能找到吗?) 看到预约列表 — 安心 签到成功 — 满足
触点 WelcomePageModeSelectionPagePhoneInputPageCheckinPageSuccessPage
痛点
  • 手机号纯数字键盘输入,没有自动补全/建议,容易输错
  • 如果手机号和预约对不上,客人不知道原因(输错了?预约被取消了?)
  • 多个预约时,列表信息密度可能不够(哪个是今天要做的?)
机会点
  • 未来支持扫预约确认短信中的 QR 码一键签到
  • 手机号输入时实时验证格式(10位数字提示)
  • 签到后显示预计等待时间(排队位次)

Layer 2: User Flow (代码实际行为)

flowchart TD A["欢迎页
点击任意位置"] --> B["模式选择
goToModeSelection()"] B -->|"签到"| C["手机号输入页
goToPhoneInput()"] C --> D["输入手机号"] D --> E["点击查询
lookupPhone()"] E --> F{"找到预约?"} F -->|"是"| G["签到页 - 显示预约列表"] F -->|"否"| H["签到页 - 空状态
→ K-CUJ-3"] G --> I["选择预约"] I --> J["点击签到
checkin(apt)"] J --> K{"签到成功?"} K -->|"是"| L["成功页
15s 自动返回"] K -->|"否"| M["错误提示 + 重试"] G -->|"返回箭头"| N["手机号输入页
retryPhoneInput()"] L --> O["欢迎页"] style A fill:#0B6E6E,stroke:#064E4E,color:#fff style L fill:#4CAF50,stroke:#2E7D32,color:#fff style H fill:#FF9800,stroke:#E65100,color:#fff style O fill:#0B6E6E,stroke:#064E4E,color:#fff style N fill:#1565C0,stroke:#0D47A1,color:#fff

✅ 已修复 (Step 2)

Layer 3: BDD 场景

场景 1.1: 正常签到流程 (Happy Path)

Given Kiosk 处于欢迎页
When 客人点击屏幕
  And 选择"签到"
  And 输入手机号 "2125551234"
  And 点击"查询"
Then 显示该手机号对应的当天预约列表
When 客人点击自己的预约
  And 点击"签到"
Then 成功页显示确认详情
  And Kiosk 15 秒后自动返回欢迎页

Tests: integration_test/app_test.dart (flow 2), test/widget/checkin_page_test.dart, test/widget/phone_input_page_test.dart

场景 1.2: 多个预约时选择正确的一个

Given 客人今天有 2 个预约(10:00 AM Manicure, 2:00 PM Pedicure)
When 客人查询手机号
Then 列表显示 2 条预约,各自有时间和服务名称
When 客人点击 "10:00 AM Manicure"
  And 确认签到
Then 仅该预约被签到,另一条不受影响

Tests: test/unit/kiosk_cubit_test.dart, test/widget/checkin_page_test.dart

K-CUJ-2: Walk-in 现场预约

P0 通过 Kiosk 进行当天预约(确认后自动签到)

Layer 1: CUJ 分析

触发点 客人路过/到店,没有预约,想当场预约并等待服务。或者被同行朋友带来,临时起意。
阶段 1. 触屏唤醒2. 选择"预约"3. 选服务4. 选技师(可选)5. 选时间6. 填写信息7. 确认8. 自动签到+完成
情感曲线 好奇 — 看到 Kiosk 选服务 — 犹豫(哪个适合我?) 选技师 — 纠结或直接跳过 选时间 — 焦虑(还有空位吗?) 填手机号 — 抵触(隐私) 确认成功 — 满足+期待
触点 WelcomePageModeSelectionPageBookingWizardPage(Step 1-4)→ SuccessPage
痛点
  • 4 步向导步骤偏多,客人站着操作 iPad 会累
  • 手机号收集引起隐私顾虑(虽然可跳过,但 UI 引导强烈)
  • 服务列表没有图片/描述,不了解的客人难以选择
  • 没有可用时段时缺少引导(死胡同)
  • 技师选择页没有照片/评分,"任意技师"成为默认逃避选项
机会点
  • 智能推荐热门服务 / "今日特惠" 标签,减少选择焦虑
  • 技师照片 + 好评率帮助决策
  • 手机号输入时强调好处:"输入手机号可收到短信提醒"
  • 无可用时段时推荐下一个最近可用日期(当前限当天)

Layer 2: User Flow (代码实际行为)

flowchart TD A["欢迎页"] --> B["模式选择"] B -->|"预约"| C["Step 1: 选择服务
goToBooking() → loadServices()"] C --> D["选择一项服务"] D --> F["Step 2: 选择技师
loadEmployees()"] F --> H{"指定技师?"} H -->|"选择具体技师"| G["Step 3: 选择时间
loadTimeSlots()"] H -->|"任意可用技师 (null)"| G G --> I["选择时间段"] I --> J["Step 4: 确认"] J --> K["输入姓名(必填)"] K --> L["输入手机号(可选)"] L --> M["确认预约
confirmBooking()"] M --> N{"预约成功?"} N -->|"是"| O["成功页
已自动签到 · 15s 返回"] N -->|"否"| P["错误提示 + 重试"] C -->|"Step 1 返回"| Q["模式选择页
goToModeSelection()"] O --> R["欢迎页"] style A fill:#0B6E6E,stroke:#064E4E,color:#fff style O fill:#4CAF50,stroke:#2E7D32,color:#fff style P fill:#f44336,stroke:#c62828,color:#fff style Q fill:#1565C0,stroke:#0D47A1,color:#fff style R fill:#0B6E6E,stroke:#064E4E,color:#fff

✅ 已修复 (Step 2)

Layer 3: BDD 场景

场景 2.1: 完整预约流程 — 任意技师 (Happy Path)

Given Kiosk 处于模式选择页面
When 客人选择"预约"
  And 选择服务 "Gel Manicure"
  And 选择"任意可用技师"
  And 选择第一个可用时间段
  And 输入姓名 "Jane Doe"
  And 点击"确认预约"(跳过手机号)
Then 成功页显示预约详情
  And 预约已自动签到
  And 显示确认号

Tests: integration_test/app_test.dart (flows 3 & 4), test/widget/booking_wizard_page_test.dart

场景 2.2: 指定技师的预约

Given 客人处于 Step 2(选择技师)
  And 技师列表显示 "Sarah" 和 "Lisa"
When 客人选择 "Sarah"
Then 进入 Step 3,时间段仅显示 Sarah 的可用时间
When 客人完成后续步骤并确认
Then 预约关联到技师 Sarah

Tests: test/widget/booking_wizard_page_test.dart, test/unit/kiosk_cubit_test.dart

场景 2.3: 带手机号的预约

Given 客人处于 Step 4(确认)
When 客人输入姓名 "John" 和手机号 "2125559999"
  And 确认预约
Then 客人记录通过手机号关联
  And 未来可通过手机号查询该预约进行签到

Tests: test/widget/booking_wizard_page_test.dart

场景 2.4: 向导中返回上一步

Given 客人处于 Step 3(选择时间)
When 客人点击返回按钮
Then 返回 Step 2(选择技师)
  And 之前选择的技师仍然高亮
When 在 Step 1 点击返回
Then 返回模式选择页(goToModeSelection()

Tests: test/widget/booking_wizard_page_test.dart

K-CUJ-3: 签到未果转预约

P0 签到查不到预约的客人被引导转为现场预约

Layer 1: CUJ 分析

触发点 客人以为自己有预约来签到,但查询不到。可能原因:记错了日期、手机号输入有误、预约已被取消、预约是用别的手机号。
阶段 1. 选择"签到"2. 输入手机号3. 查询失败4. 看到"未找到"提示5. 决定是否预约6. 进入预约向导7. 完成预约+自动签到
情感曲线 自信 — "我有预约" 输入手机号 — 平静 未找到 — 困惑/沮丧 看到选项 — 犹豫(怎么办?) 决定预约 — 恢复信心 完成预约 — 释然
触点 PhoneInputPageCheckinPage(空状态)→ BookingWizardPage(Step 1-4)→ SuccessPage
痛点
  • "未找到"时客人不知道为什么 — 是手机号错了?预约被取消了?记错日期了?缺乏诊断信息
  • 没有"重新输入手机号"的明确选项 — 如果只是输错一位数字,客人想改而不是放弃
  • 签到→预约的流程切换不够顺滑 — 心理上从"确认到达"切换到"重新选择服务"需要情绪调整
  • 之前输入的手机号没有带入预约流程 — 到 Step 4 时还需要重新输入
机会点
  • 提示可能原因:"请确认手机号是否正确" / "您的预约可能不是今天"
  • 提供"重新输入手机号"按钮,和"立即预约"并列
  • 一键跳转预约时,自动将已输入的手机号带入 Step 4 确认页
  • 记录"签到未果"事件用于运营分析(有多少客人走到这步?转化率如何?)

Layer 2: User Flow (代码实际行为)

flowchart TD A["手机号输入页"] --> B["输入手机号"] B --> C["点击查询
lookupPhone()"] C --> D{"找到预约?"} D -->|"是"| E["→ K-CUJ-1 签到页(有数据)"] D -->|"否 / API 错误"| F["签到页 — 空状态
icon + '未找到预约'"] F --> G{"客人选择"} G -->|"'Book New' 按钮"| H["进入预约向导 Step 1
startBookingFromCheckin()"] G -->|"'重新输入手机号' 按钮"| I2["手机号输入页
retryPhoneInput()"] G -->|"返回箭头"| I["手机号输入页
retryPhoneInput()"] H --> J["→ K-CUJ-2 预约流程
phoneNumber 保留在 state 中"] J --> K["成功页
已自动签到"] style F fill:#FF9800,stroke:#E65100,color:#fff style K fill:#4CAF50,stroke:#2E7D32,color:#fff style E fill:#4CAF50,stroke:#2E7D32,color:#fff style I fill:#1565C0,stroke:#0D47A1,color:#fff style I2 fill:#1565C0,stroke:#0D47A1,color:#fff

✅ 已修复 (Step 2 + Step 3)

Layer 3: BDD 场景

场景 3.1: 签到未果后转预约 (Happy Path)

Given 客人输入手机号 "2125551234" 查询签到
  And 该手机号今天没有预约
When 查询返回空结果
Then 显示签到页空状态(icon + "未找到预约"文案)
  And 显示"Book New Appointment"按钮
When 客人点击"Book New Appointment"
Then 进入预约向导 Step 1
When 客人完成 4 步预约流程
Then 成功页显示预约已确认且已自动签到

Tests: test/widget/checkin_page_test.dart (空状态), test/unit/kiosk_cubit_test.dart

场景 3.1b: 手机号自动带入预约 Step 4

Given 客人在签到流程中输入了手机号 "2125551234"
  And 查询未找到预约后点击"Book New"
When 客人到达 Step 4 确认页
Then 手机号输入框应预填 "2125551234"
  And 客人无需重新输入手机号

Tests: test/widget/booking_wizard_page_test.dart ✅ 已实现 — StatefulWidget + TextEditingController 预填

场景 3.2: 手机号输错后重新输入

Given 客人输入了错误的手机号,查询未找到预约
When 客人点击"重新输入手机号"
Then 返回手机号输入页
  And 输入框已清空,准备重新输入
When 客人输入正确的手机号
  And 查询找到预约
Then 进入正常签到流程(K-CUJ-1)

Tests: test/widget/checkin_page_test.dart ✅ 已实现 — retryPhoneInput() + 按钮 + i18n

场景 3.3: 签到未果后放弃离开

Given 客人查询签到未找到预约
When 客人不操作,等待 120 秒
Then Kiosk 自动返回欢迎页(idle 超时)

Tests: ✅ idle 超时已在 KioskShellPage 实现(Listener + 120s Timer + 10s 倒计时警告浮层)

K-CUJ-4: 管理员初始设置

P1 一次性配置 — 管理员登录并为 Kiosk 选择门店

Layer 1: CUJ 分析

触发点 新 Kiosk iPad 到店,或需要重新配置(更换门店/重置设备)。由门店管理员操作。
阶段 1. 启动 App2. 管理员登录3. 选择门店4. 配置保存5. 进入运行模式
情感曲线 拿到设备 — 期待 登录 — 找密码有点烦 选门店 — 简单直接 配置完成 — 满足
触点 AdminLoginPageStoreSelectionPageWelcomePage(配置完成后落地页)
痛点
  • 登录凭据需要管理员记住(没有"记住我"或生物识别)
  • 重置入口通过长按左上角触发,不容易被发现(but 这也是防止客人误触的设计)
  • 配置后无法远程修改(换门店需要物理接触设备)
机会点
  • QR 码快速配置(管理后台生成,iPad 扫码即完成)
  • MDM(移动设备管理)集成,远程推送配置
  • 管理后台可查看 Kiosk 在线状态和最后活跃时间

Layer 2: User Flow

flowchart TD A["应用启动"] --> B{"已有配置?"} B -->|"否"| C["管理员登录页"] B -->|"是"| D["欢迎页"] C --> E["输入邮箱 + 密码"] E --> F{"登录成功?"} F -->|"是"| G["门店选择页"] F -->|"否"| H["错误提示"] H --> C G --> I["从列表中选择门店"] I --> J["配置保存到 SecureStorage"] J --> D style A fill:#0B6E6E,stroke:#064E4E,color:#fff style D fill:#4CAF50,stroke:#2E7D32,color:#fff style H fill:#f44336,stroke:#c62828,color:#fff

Layer 3: BDD 场景

场景 4.1: 首次管理员设置 (Happy Path)

Given Kiosk 没有已保存的配置(首次启动)
When 用户输入有效邮箱和密码
  And 从列表中选择门店 "Main Street Salon"
Then 配置(token, storeId, tenant)被持久化保存到 SecureStorage
  And 自动跳转到欢迎页
  And 后续启动直接进入欢迎页

Tests: integration_test/app_test.dart (flow 1), test/widget/admin_login_page_test.dart, test/unit/admin_setup_cubit_test.dart

场景 4.2: 登录失败

Given Kiosk 显示配置登录页
When 用户输入错误密码
Then 显示错误提示(凭据无效)
  And 停留在登录页,可重试

Tests: test/widget/admin_login_page_test.dart, test/unit/admin_setup_cubit_test.dart

场景 4.3: 管理员重置 Kiosk

Given Kiosk 已配置并显示欢迎页
When 用户长按左上角
Then 显示设置底部弹窗
When 用户点击"重置"
Then SecureStorage 中的配置被清除
  And 跳转到管理员登录页

Tests: test/widget/kiosk_settings_sheet_test.dart

设计决策

预约约束

手机号收集策略

客人身份模型

第一层: 匿名客人 — 仅有姓名,source: 'kiosk',无手机号/邮箱
第二层: 已识别客人 — 有手机号,未来可通过手机号查询签到
第三层: 注册用户 — 完整账户(有密码),可登录客人移动端 App

SMS 验证码跳过

Idle 超时机制

测试覆盖率总结

CUJ integration_test Mock 集成测试 Widget 测试 单元测试 状态
K-CUJ-1: 签到 app_test.dart (flow 2) kiosk_flow_test.dart phone_input, checkin_page kiosk_cubit 完整覆盖
K-CUJ-2: 预约 app_test.dart (flows 3, 4) kiosk_flow_test.dart booking_wizard, success_page kiosk_cubit 完整覆盖
K-CUJ-3: 签到未果转预约 checkin_page (空状态 + 重新输入按钮) kiosk_cubit (retryPhoneInput + selectEmployee) 完整覆盖
K-CUJ-4: 管理员设置 app_test.dart (flow 1) admin_login, settings_sheet admin_setup_cubit 完整覆盖

修复记录 (Step 2 + Step 3)

类型问题CUJBDD 场景状态
✅ 已修复 "重新输入手机号"按钮 — retryPhoneInput() + UI + i18n K-CUJ-3 3.2 checkin_page.dart, kiosk_cubit.dart, app_en/zh.arb
✅ 已修复 手机号预填 Step 4 — StatefulWidget + TextEditingController K-CUJ-3 3.1b booking_wizard_page.dart
✅ 已修复 签到页返回 → 手机号输入页(retryPhoneInput) K-CUJ-1 checkin_page.dart
✅ 已修复 预约 Step 1 返回 → 模式选择页(goToModeSelection) K-CUJ-2 2.4 kiosk_cubit.dart
✅ 已修复 空状态添加诊断提示文案 K-CUJ-3 checkin_page.dart, app_en/zh.arb
✅ 已修复 手机号输入页返回 → 模式选择页(goToModeSelection) K-CUJ-1 phone_input_page.dart
✅ 已修复 selectEmployee(null) 无法复原为"任意技师" — copyWith 陷阱 K-CUJ-2 2.1 kiosk_cubit.dart — 手动 emit 绕过 copyWith
✅ 已确认 Idle 超时 — KioskShellPage 已实现 120s + 10s 倒计时 K-CUJ-3 3.3 kiosk_shell_page.dart

Kiosk 自助服务 iPad App — CUJ 四步走文档
方法: Layer 1 CUJ → Layer 2 User Flow (Mermaid) → Layer 3 BDD (Given-When-Then) → Layer 4 E2E 测试映射
更新日期: 2026-02-07