客人端 App — CUJ 关键用户旅程
关键用户旅程 - 用户流程 - BDD 场景 - E2E 测试映射
spec: 070-design-system-tokens / US4 | 更新时间: 2026-02-07
目录
CUJ 总览与优先级矩阵
| CUJ |
优先级 |
描述 |
触发点 |
业务价值 |
E2E 测试状态 |
| CUJ-1 |
P0 |
新用户单服务预约 |
打开 App / 营销链接 |
核心营收路径 - 首次转化 |
部分覆盖 |
| CUJ-2 |
P0 |
老用户多服务购物车预约 |
已登录用户浏览服务 |
提高客单价 - 多项目结账 |
部分覆盖 |
| CUJ-3 |
P0 |
深度链接预约 |
营销链接 /booking/:store?ref=xxx |
营销活动转化追踪 |
缺失 |
| CUJ-4 |
P1 |
预约管理 |
用户点击"预约"标签页 |
减少爽约,支持自助服务 |
部分覆盖 |
| CUJ-5 |
P1 |
OTP 验证码登录 |
结账时 / 用户点击"个人中心" |
用户获取,会员转化 |
需重构 |
| CUJ-6 |
P2 |
账户与个人资料管理 |
用户点击"个人中心"标签页(已登录) |
留存,个性化 |
已覆盖 |
| CUJ-7 |
P2 |
门店发现与切换 |
用户想要查找/切换门店 |
多门店覆盖 |
已覆盖 |
E2E 覆盖状态
| 测试文件 |
覆盖的 CUJ |
测试场景 |
| complete_booking_test.dart |
CUJ-1 (部分) |
App 启动、服务浏览、标签页导航 |
| store_selection_test.dart |
CUJ-1, CUJ-7 |
门店信息展示、营业时间、联系方式 |
| service_selection_test.dart |
CUJ-1, CUJ-2 |
服务列表、卡片、图片、加载状态 |
| employee_selection_test.dart |
CUJ-1, CUJ-2 |
"任意技师"选项、技师卡片 |
| time_slot_test.dart |
CUJ-1, CUJ-2 |
日历、时间网格、时段选择 |
| cart_flow_test.dart |
CUJ-2 (部分) |
购物车增删改查、角标、持久化、总价 |
| appointment_list_test.dart |
CUJ-4 (部分) |
列表、空状态、刷新、筛选 |
| phone_login_test.dart |
CUJ-5 (旧体系) |
手机号校验、验证码(使用旧 booking API,需迁移到新 OTP) |
| email_login_test.dart |
CUJ-5 (旧体系) |
邮箱+密码登录(需迁移到 OTP) |
| register_test.dart |
CUJ-5 (旧体系) |
注册表单校验(需迁移到 OTP) |
| profile_test.dart |
CUJ-6 |
个人资料展示、编辑、退出登录 |
| settings_test.dart |
CUJ-6 |
语言切换、通知设置 |
覆盖缺口汇总
| 缺口 | CUJ | 缺失内容 | 状态 |
| 结账登录流程新增 |
CUJ-1, CUJ-2 |
购物车 → 结账时触发 OTP 登录(手机号/邮箱+验证码),Flutter 端尚未适配新 OTP 体系 |
Flutter 需重构 LoginCubit |
| 定金支付流程新增 |
CUJ-1, CUJ-2 |
门店设有定金政策时:显示金额 + 退订规则 → 跳转 CodePay Hosted Checkout WebView → 支付完成回调 |
后端 spec 已定义,Flutter 未实现 |
| 结账完整流程 |
CUJ-1, CUJ-2 |
购物车 → 结账 → 登录 → 定金(可选) → 成功页面,端到端完整路径未测试 |
缺失 |
| 深度链接入口 |
CUJ-3 |
没有测试 /booking/:store?ref=xxx 深度链接处理 |
缺失 |
| 预约详情 |
CUJ-4 |
详情页(TODO 占位符)、取消预约、改期、退订规则展示 |
AppointmentDetailPage 未实现 |
| OTP 登录体系新增 |
CUJ-5 |
Flutter 仍使用旧 email+password LoginCubit,需迁移到统一 OTP API(/api/guest-auth/send-code + /verify-code) |
后端已就绪,Flutter 需适配 |
关键依赖说明: CUJ-1 和 CUJ-2 的结账尾部流程(OTP 登录 + 定金支付)依赖两个未完成的功能:
(1) Flutter OTP 登录适配(060-guest-auth-redesign),
(2) CodePay Hosted Checkout 集成(053-booking-deposit-cancellation-policy + 015-pos-payment)。
详见
CodePay RSA 认证文档。
共享结账流程(CUJ-1 & CUJ-2 共用尾部)
CUJ-1(单服务)和 CUJ-2(多服务)在加入购物车后共享相同的结账流程:
flowchart TD
A["购物车页面"] --> B["点击结账"]
B --> C{"用户已登录?"}
C -->|"是"| F["订单确认页"]
C -->|"否"| D["OTP 登录页"]
D --> D1["输入手机号或邮箱"]
D1 --> D2["发送验证码"]
D2 --> D3["输入 6 位验证码"]
D3 --> D4{"验证成功?"}
D4 -->|"是"| F
D4 -->|"否"| D5["错误提示 + 重试"]
D5 --> D3
F --> G{"门店有定金政策?"}
G -->|"否"| K["提交预约"]
G -->|"是"| H["显示定金金额 + 退订规则"]
H --> I["跳转 CodePay 支付页面 (WebView)"]
I --> I1{"支付结果?"}
I1 -->|"成功"| K
I1 -->|"失败/取消"| I2["返回订单确认页 + 提示"]
I2 --> H
K --> L{"提交成功?"}
L -->|"是"| M["预约成功页"]
L -->|"否"| N["报错 + 重试"]
M --> O["查看预约 / 返回首页"]
style A fill:#2196F3,stroke:#1565C0,color:#fff
style D fill:#FF9800,stroke:#E65100,color:#fff
style I fill:#9C27B0,stroke:#6A1B9A,color:#fff
style M fill:#4CAF50,stroke:#2E7D32,color:#fff
style N fill:#f44336,stroke:#c62828,color:#fff
共享结账 BDD 场景
场景 S.1: 未登录用户结账触发 OTP 登录
Given 用户未登录,购物车中有 1 个服务
When 用户点击"结账"
Then 跳转到 OTP 登录页面
When 用户输入手机号 "2125551234"
And 点击"发送验证码"
Then 调用 POST /api/guest-auth/send-code
And 出现 6 位验证码输入框
When 用户输入正确验证码
Then 调用 POST /api/guest-auth/verify-code
And 用户以已识别客人身份登录
And 跳转到订单确认页
测试: checkout_otp_login_test.dart (新增)
场景 S.2: 邮箱 OTP 登录
Given 用户未登录,在 OTP 登录页面
When 用户切换到"邮箱"标签
And 输入邮箱 "guest@example.com"
And 点击"发送验证码"
Then 调用 POST /api/guest-auth/send-code(channel=email)
And 出现验证码输入框
When 用户输入正确验证码
Then 登录成功并跳转到订单确认页
测试: checkout_otp_login_test.dart (新增)
场景 S.3: 已登录用户直接进入订单确认
Given 用户已登录
And 购物车中有项目
When 用户点击"结账"
Then 直接跳转到订单确认页(跳过 OTP 登录)
测试: checkout_logged_in_test.dart (新增)
场景 S.4: 定金支付 - 门店有定金政策
Given 用户已登录,在订单确认页
And 门店设有定金政策(30% 定金,24 小时免费取消)
Then 页面显示定金金额(如 $19.14)
And 显示退订规则: "24 小时前免费取消,迟到取消扣除定金"
When 用户点击"支付定金"
Then 打开 CodePay Hosted Checkout 页面(WebView)
When 用户在 CodePay 页面完成支付
Then 后端收到 Webhook 确认支付成功
And WebView 关闭,显示预约成功页
测试: deposit_payment_test.dart (新增)
场景 S.5: 定金支付失败/取消
Given 用户在 CodePay 支付页面
When 支付失败或用户取消支付
Then WebView 关闭
And 返回订单确认页
And 显示"支付未完成,请重试"提示
And 预约未被创建
测试: deposit_payment_test.dart (新增)
场景 S.6: 无定金政策 - 直接预约
Given 用户已登录,在订单确认页
And 门店无定金政策
When 用户点击"确认预约"
Then 直接提交预约(无需支付)
And 显示预约成功页
测试: checkout_no_deposit_test.dart (新增)
CUJ-1: 新用户单服务预约
P0 核心营收路径 - 新用户/匿名用户预约单个服务
用户流程
flowchart TD
A["启动 App"] --> B["首页 / 预约首页"]
B --> C{"门店已预选?"}
C -->|"是"| D["选择服务"]
C -->|"否"| E["选择门店"]
E --> D
D --> F["选择技师"]
F --> G{"指定技师?"}
G -->|"是"| H["选择具体技师"]
G -->|"任意技师"| I["跳过"]
H --> J["选择时段"]
I --> J
J --> K["选择日期 + 时间"]
K --> L["加入购物车"]
L --> M["购物车页面(1 项)"]
M --> N["点击结账"]
N --> CHECKOUT["共享结账流程"]
style A fill:#2196F3,stroke:#1565C0,color:#fff
style L fill:#FF9800,stroke:#E65100,color:#fff
style CHECKOUT fill:#9C27B0,stroke:#6A1B9A,color:#fff
流程变更说明: 选择服务/技师/时间后,项目先加入
购物车,再从购物车进入结账。
结账时触发 OTP 登录(如未登录),之后根据门店政策决定是否需要支付定金。
完整结账流程见
共享结账流程。
BDD 场景
场景 1.1: 完整单服务预约 - 正常路径
Given 用户首次打开 App(匿名状态)
And 默认门店已预配置
When 用户选择服务 "Gel Manicure"
And 选择"任意技师"
And 选择今天第一个可用时段
And 点击"加入购物车"
Then 购物车角标显示 "1"
When 用户打开购物车并点击"结账"
Then 跳转到 OTP 登录页(因为未登录)
When 用户输入手机号并验证
And 在订单确认页点击"确认预约"
Then 显示预约成功页面,包含预约 ID
And 购物车被清空
测试: booking_e2e_happy_path_test.dart (新增)
场景 1.2: 选择指定技师
Given 用户在技师选择页面
When 用户点击某个技师卡片
Then 该技师被高亮选中
And "继续"按钮变为可用
When 用户点击"继续"
Then 时段选择页面只显示该技师的可用时段
测试: employee_selection_test.dart (已有 - 已覆盖)
场景 1.3: 加入购物车后继续浏览
Given 用户已将一个服务加入购物车
When 用户点击"继续浏览"而非"去购物车"
Then 返回服务列表页面
And 购物车角标显示 "1"
And 用户可以添加更多服务
测试: cart_flow_test.dart (已有 - 部分覆盖)
场景 1.4: 时段不可用(竞态条件)
Given 用户购物车中有一个服务+时段
And 另一个用户同时预约了相同时段
When 用户提交预约
Then 显示错误信息"该时段已不可用"
And 用户可以返回选择其他时间
测试: booking_e2e_happy_path_test.dart (新增 - 异常场景)
CUJ-2: 老用户多服务购物车预约
P0 提高客单价 - 用户通过购物车预约多个服务
用户流程
flowchart TD
A["首页 - 浏览服务"] --> B["选择服务1 + 技师 + 时间"]
B --> C["加入购物车"]
C --> D["继续浏览"]
D --> E["选择服务2 + 技师 + 时间"]
E --> F["加入购物车"]
F --> G["购物车角标显示 2"]
G --> H["打开购物车页面"]
H --> I{"编辑项目?"}
I -->|"是"| J["修改技师/时间"]
I -->|"否"| K["点击结账"]
J --> K
K --> CHECKOUT["共享结账流程"]
style A fill:#2196F3,stroke:#1565C0,color:#fff
style C fill:#FF9800,stroke:#E65100,color:#fff
style F fill:#FF9800,stroke:#E65100,color:#fff
style CHECKOUT fill:#9C27B0,stroke:#6A1B9A,color:#fff
与 CUJ-1 的区别: CUJ-2 的核心差异在于用户添加
多个服务到购物车,
每个服务可以独立配置技师和时间。结账流程与 CUJ-1 完全相同,见
共享结账流程。
BDD 场景
场景 2.1: 添加多个服务到购物车
Given 用户在预约首页
When 用户选择 "Gel Manicure" + 任意技师 + 10:00 AM,加入购物车
And 继续选择 "Pedicure" + Alice + 11:00 AM,加入购物车
Then 购物车角标显示 "2"
When 用户打开购物车页面
Then 两个项目都列出,各自显示技师、时间、价格和时长
And 总价等于两项服务的价格之和
测试: cart_flow_test.dart (已有 - 已覆盖)
场景 2.2: 购物车中修改项目配置
Given 购物车包含 2 个服务
When 用户点击项目1的"修改"
And 更换技师为 "Bob"
And 更换时间为 "2:00 PM"
Then 项目1更新为 "Bob, 2:00 PM"
And 项目2不受影响
测试: cart_checkout_test.dart (新增)
场景 2.3: 时段冲突检测
Given 购物车包含 "Gel Manicure" (10:00 AM, 45 分钟)
When 用户为第二个服务选择同一技师 10:15 AM
Then 时段显示为不可用(灰色)
And 提示"该时段与购物车中的项目冲突"
测试: cart_conflict_test.dart (新增)
场景 2.4: 购物车持久化
Given 用户购物车中有项目
When App 关闭后重新打开
Then 购物车项目仍然存在,配置信息不变
测试: cart_flow_test.dart (已有 - 已覆盖)
CUJ-3: 深度链接预约
P0 营销活动转化 - 用户通过 /booking/:store?ref=xxx 打开 App
用户流程
flowchart TD
A["打开链接: /booking/salon001?ref=instagram_jan"] --> B["_BookingDeepLinkHandler"]
B --> C["在 BookingBloc 中设置追踪 ref"]
C --> D["加载门店 salon001 数据"]
D --> E{"门店存在?"}
E -->|"是"| F["跳转到预约首页 - 门店已预选"]
E -->|"否"| G["错误: 门店不存在"]
F --> H["继续正常预约流程..."]
H --> I["预约成功: booking.tracking_ref = instagram_jan"]
style A fill:#9C27B0,stroke:#6A1B9A,color:#fff
style F fill:#4CAF50,stroke:#2E7D32,color:#fff
style G fill:#f44336,stroke:#c62828,color:#fff
BDD 场景
场景 3.1: 有效深度链接
Given 用户通过链接 /booking/salon001?ref=ig_promo 打开 App
When 深度链接被处理
Then 预约首页加载,门店 "salon001" 已预选
And 追踪标记 "ig_promo" 存储在 BookingBloc 中
When 用户完成预约
Then 创建的预约包含 tracking_ref = "ig_promo"
测试: deep_link_booking_test.dart (新增)
场景 3.2: 无效门店深度链接
Given 用户通过链接 /booking/invalid_store 打开 App
When 深度链接处理器尝试加载门店
Then 显示错误信息"门店不存在"
And 用户可以手动导航到门店选择页面
测试: deep_link_booking_test.dart (新增)
CUJ-4: 预约管理
P1 减少爽约 - 用户查看、取消或改期预约
用户流程
flowchart TD
A["预约标签页"] --> B["查看预约列表"]
B --> C{"筛选?"}
C -->|"即将到来"| D["显示即将到来的预约"]
C -->|"历史"| E["显示已完成/已取消的预约"]
D --> F["点击预约卡片"]
F --> G["预约详情页"]
G --> H{"操作?"}
H -->|"取消"| I["确认取消对话框"]
I --> I1{"在免费取消窗口内?"}
I1 -->|"是"| J["免费取消,退还定金"]
I1 -->|"否"| J1["显示取消费用提示"]
J1 --> J2{"用户确认?"}
J2 -->|"是"| J3["取消并扣除费用"]
J2 -->|"否"| G
H -->|"改期"| M["选择时段"]
M --> N["选择新时间"]
N --> O["确认改期"]
H -->|"仅查看"| P["查看详情后返回"]
style A fill:#2196F3,stroke:#1565C0,color:#fff
style J fill:#4CAF50,stroke:#2E7D32,color:#fff
style J3 fill:#f44336,stroke:#c62828,color:#fff
style O fill:#4CAF50,stroke:#2E7D32,color:#fff
BDD 场景
场景 4.1: 查看预约列表
Given 用户有 3 个即将到来的预约
When 用户点击"预约"标签页
Then 列表显示 3 张预约卡片
And 每张卡片显示日期、时间、服务名称和状态
测试: appointment_list_test.dart (已有 - 已覆盖)
场景 4.2: 查看预约详情
Given 用户在预约列表页面
When 用户点击某个预约卡片
Then 详情页展示:
- 服务名称、时长、价格
- 技师姓名
- 日期和时间
- 门店名称和地址
- 状态(已确认/待确认/已取消)
- 定金信息(如已支付)
- 退订规则
测试: appointment_detail_test.dart (新增 - AppointmentDetailPage 尚未实现)
场景 4.3: 免费取消预约(窗口期内)
Given 用户在预约详情页面
And 预约在 24 小时后开始(在免费取消窗口内)
And 已支付 $19.14 定金
When 用户点击"取消预约"
Then 对话框显示"免费取消,定金将退还"
When 用户确认取消
Then 预约状态变为"已取消"
And 定金退还流程启动
测试: appointment_management_test.dart (新增)
场景 4.4: 迟到取消(窗口期外)
Given 用户在预约详情页面
And 预约在 2 小时后开始(超出免费取消窗口)
When 用户点击"取消预约"
Then 对话框显示"迟到取消,定金 $19.14 将不予退还"
When 用户确认取消
Then 预约状态变为"已取消"
And 定金被没收
测试: appointment_management_test.dart (新增)
场景 4.5: 下拉刷新
Given 用户正在查看预约列表
When 用户下拉刷新
Then 短暂出现加载指示器
And 列表更新为服务器最新数据
测试: appointment_list_test.dart (已有 - 已覆盖)
CUJ-5: OTP 验证码登录
P1 用户获取 - 统一的手机号/邮箱 OTP 验证码登录
体系变更: 原 CUJ-5 使用邮箱+密码登录体系。后端已完成 060-guest-auth-redesign 重构,
统一为 OTP 验证码登录(/api/guest-auth/send-code + /api/guest-auth/verify-code),
支持手机号和邮箱两种渠道。
Flutter 适配状态: 尚未适配。当前 LoginCubit 仍使用旧的 email+password 体系,
phone_login_page 仍调用旧的 /api/public/booking/verify-phone 端点。
用户流程
flowchart TD
A["个人中心 - 未登录"] --> B{"选择方式"}
B -->|"手机号"| C["输入手机号"]
C --> D["发送验证码 (SMS)"]
D --> E["输入 6 位验证码"]
E --> F{"验证成功?"}
F -->|"是"| G["登录成功 - 跳转首页"]
F -->|"否"| H["错误提示 + 重试"]
H --> E
B -->|"邮箱"| I["输入邮箱"]
I --> J["发送验证码 (Email)"]
J --> K["输入 6 位验证码"]
K --> L{"验证成功?"}
L -->|"是"| G
L -->|"否"| M["错误提示 + 重试"]
M --> K
style A fill:#2196F3,stroke:#1565C0,color:#fff
style G fill:#4CAF50,stroke:#2E7D32,color:#fff
BDD 场景
场景 5.1: 手机号 OTP 登录
Given 用户在登录页面
When 用户输入手机号 "2125551234"
And 点击"发送验证码"
Then 调用 POST /api/guest-auth/send-code {contact: "2125551234"}
And 后端自动识别为 SMS 渠道
And 出现验证码输入框 + 60 秒倒计时
When 用户输入正确验证码
Then 调用 POST /api/guest-auth/verify-code
And 返回 JWT token
And 跳转首页,个人中心显示用户姓名
测试: otp_login_test.dart (新增 - 替代旧 phone_login_test.dart)
场景 5.2: 邮箱 OTP 登录
Given 用户在登录页面
When 用户切换到"邮箱"标签
And 输入邮箱 "guest@example.com"
And 点击"发送验证码"
Then 调用 POST /api/guest-auth/send-code {contact: "guest@example.com"}
And 后端自动识别为 Email 渠道
And 出现验证码输入框
When 用户输入正确验证码
Then 登录成功并跳转首页
测试: otp_login_test.dart (新增)
场景 5.3: 验证码过期/错误
Given 用户已收到验证码但超过 5 分钟
When 用户输入过期的验证码
Then 显示错误"验证码已过期,请重新发送"
And "重新发送"按钮变为可用
测试: otp_login_test.dart (新增)
场景 5.4: 新用户自动注册
Given 用户输入的手机号在系统中不存在
When 用户完成 OTP 验证
Then 系统自动创建 guest 记录
And 用户以新客人身份登录
And 无需额外注册步骤
测试: otp_login_test.dart (新增)
CUJ-6: 账户与个人资料管理
P2 用户留存 - 已登录用户管理个人资料和偏好设置
用户流程
flowchart TD
A["个人中心 - 已登录"] --> B["账户信息页面"]
B --> C{"操作?"}
C -->|"编辑资料"| D["编辑姓名 / 邮箱 / 手机号"]
D --> E["保存修改"]
C -->|"语言"| I["切换 中/英"]
I --> J["界面立即更新"]
C -->|"会员"| K["查看会员等级和积分"]
C -->|"退出登录"| L["确认退出"]
L --> M["跳转到首页 - 访客状态"]
style A fill:#2196F3,stroke:#1565C0,color:#fff
BDD 场景
所有场景已被现有测试覆盖: profile_test.dart, settings_test.dart
CUJ-7: 门店发现与切换
P2 多门店覆盖 - 用户查找并切换门店
用户流程
flowchart TD
A["预约首页"] --> B["点击门店名称 / 切换门店"]
B --> C["门店选择页面"]
C --> D["浏览门店列表"]
D --> E["查看门店: 名称、地址、营业时间"]
E --> F["选择门店"]
F --> G["返回预约首页 - 已切换门店"]
G --> H["服务列表更新为所选门店"]
style A fill:#2196F3,stroke:#1565C0,color:#fff
style H fill:#4CAF50,stroke:#2E7D32,color:#fff
BDD 场景
所有场景已被现有测试覆盖: store_selection_test.dart
待新增 E2E 测试汇总
| 优先级 |
测试文件 (新增) |
CUJ |
测试场景 |
前置依赖 |
| P0 |
booking_e2e_happy_path_test.dart |
CUJ-1 |
完整端到端: 选服务 -> 选技师 -> 选时间 -> 加入购物车 -> 结账 -> OTP 登录 -> 成功 |
OTP 登录适配 |
| P0 |
checkout_otp_login_test.dart |
CUJ-1, CUJ-2 |
结账触发 OTP 登录: 手机号+验证码、邮箱+验证码、已登录跳过 |
OTP 登录适配 |
| P0 |
deposit_payment_test.dart |
CUJ-1, CUJ-2 |
定金支付: CodePay WebView 支付、支付成功/失败、无定金策略直接预约 |
CodePay Hosted Checkout 集成 |
| P0 |
cart_checkout_test.dart |
CUJ-2 |
购物车 -> 修改项目配置 -> 结账 -> 成功 |
OTP 登录适配 |
| P0 |
deep_link_booking_test.dart |
CUJ-3 |
深度链接处理、门店自动选择、追踪标记 |
无 |
| P1 |
otp_login_test.dart |
CUJ-5 |
独立 OTP 登录: 手机号/邮箱发验证码、验证成功/失败、新用户自动注册 |
OTP 登录适配 |
| P1 |
appointment_management_test.dart |
CUJ-4 |
预约详情、免费取消、迟到取消、定金退还 |
AppointmentDetailPage 实现 |
| P1 |
cart_conflict_test.dart |
CUJ-2 |
购物车时段冲突检测 |
无 |
Flutter 适配任务
| 任务 |
说明 |
影响的 CUJ |
相关 Spec |
| OTP LoginCubit 重构 |
将 email+password LoginCubit 迁移到统一 OTP 体系,调用新 API /api/guest-auth/send-code + /verify-code |
CUJ-1, CUJ-2, CUJ-5 |
060-guest-auth-redesign |
| CodePay WebView 集成 |
结账时打开 CodePay Hosted Checkout WebView 进行定金支付,处理支付回调 |
CUJ-1, CUJ-2 |
053-booking-deposit / 015-pos-payment |
| AppointmentDetailPage 实现 |
预约详情页(当前 TODO 占位符),含定金信息、退订规则、取消/改期操作 |
CUJ-4 |
- |
基于 spec 070-design-system-tokens / US4 生成
方法: CUJ -> 用户流程 (Mermaid) -> BDD (Given-When-Then) -> integration_test (Dart)
关联文档: CodePay RSA 认证 |
CodePay 完整支付流程