Web 账户与用户设置模块 — CUJ 关键用户旅程

Account, Profile & User Settings | 更新时间: 2026-02-08

TOUCHES (Pages 6): account/membership/page.tsx, account/points/page.tsx, account/gift-cards/page.tsx, account/gift-cards/purchase/page.tsx, account/preferences/page.tsx, settings/page.tsx

TOUCHES (Components 11): Settings/UserPreferencesPanel.tsx, Settings/SettingValueInput.tsx, Settings/SettingSourceBadge.tsx, Auth/PermissionGate.tsx, Auth/ProtectedRoute.tsx, Loyalty/TierBadge.tsx, Loyalty/PointsBalance.tsx, Loyalty/TierProgress.tsx, Loyalty/GiftCardBalance.tsx, Loyalty/LoyaltyPaymentOption.tsx

TOUCHES (Stores/Hooks 5): contexts/AuthContext.tsx, stores/guest-auth-store.ts, hooks/useLoyaltyApi.ts, hooks/useSettings.ts, hooks/usePermissions.ts

TOUCHES (Backend API 2): api/user.js (preferences, profile, switch-store, permissions), api/guest-auth.js (register, login, OTP, profile, password, sessions)

TOUCHES (Backend Services 4): services/guest-auth/guestAuthService.js, services/guest-auth/guestPasswordService.js, services/guest-auth/guestEmailService.js, services/guest-auth/otpService.js
双角色说明: 本模块覆盖两类用户的账户自服务: 员工 Auth 登录/登出流程在 CUJ-A,本文档聚焦登录后的账户管理操作。

账户系统架构

flowchart TB
    subgraph Employee["员工/管理员账户"]
        EP["/settings 用户设置"]
        UPP["UserPreferencesPanel"]
        AC["AuthContext"]
        UAPI["api/user.js"]
    end

    subgraph Guest["客户 (Guest) 账户"]
        MEM["/account/membership"]
        PTS["/account/points"]
        GC["/account/gift-cards"]
        GCP["/account/gift-cards/purchase"]
        PREF["/account/preferences"]
        GAS["guest-auth-store.ts"]
        GAPI["api/guest-auth.js"]
        OTP["otpService.js"]
    end

    subgraph Shared["共享组件"]
        TB["TierBadge"]
        PB["PointsBalance"]
        TP["TierProgress"]
        GCB["GiftCardBalance"]
    end

    EP --> UPP --> AC --> UAPI
    MEM --> TB & PB & TP
    PTS --> PB
    GC --> GCB
    GCP --> GAPI
    MEM & PTS & GC --> GAS --> GAPI --> OTP

两层客户身份模型

级别身份创建方式能力数据
1匿名客人Kiosk/Walk-in(无可验证联系方式)仅有预约记录,不可登录name, source
2会员手机号或邮箱 OTP 验证登录、查看积分/会员、购买礼品卡phone/email, access_token/refresh_token(JWT), 可选 password_hash
Guest Auth 改版 (060-guest-auth-redesign): 旧流程: 邮箱+密码注册 → 新流程: send-code (OTP) → verify-code → 自动创建/升级账户 → 可选设置密码。 guest-auth-store.ts 使用 Zustand 管理 access_token / refresh_token / expires_at(JWT-only)。

目录

CUJ 总览与优先级矩阵

CUJ优先级角色描述触发点业务价值E2E 状态
K1 P0 Guest OTP 注册与登录 客户首次访问 /account 获客 — 低门槛注册转化 部分覆盖
K2 P1 Guest 密码管理 注册后设密码 / 忘记密码 安全 — 账户保护 部分覆盖
K3 P1 Guest 会员中心 查看会员等级和权益 留存 — 会员体系可见性 未覆盖
K4 P1 Guest 积分查看与历史 查看积分余额和交易 留存 — 激励消费循环 未覆盖
K5 P1 Guest 我的礼品卡与购买 查看/购买礼品卡 营收 — 预付费 未覆盖
K6 P0 Employee 员工用户偏好设置 /settings 页面 效率 — 个性化工作环境 部分覆盖
K7 P0 Employee 门店切换 管理多门店用户 效率 — 多门店管理 已覆盖
K8 P2 Guest 通知偏好设置 /account/preferences 合规 — opt-in/opt-out 未覆盖
K9 P2 Guest Session 管理 查看/撤销活跃会话 安全 — 多设备管理 未覆盖

CUJ-K1: Guest OTP 注册与登录

P0 获客 — 客户通过 OTP 验证码低门槛注册或登录

TOUCHES: stores/guest-auth-store.tsapi/guest-auth.js (POST /send-code, POST /verify-code, POST /login, POST /refresh) → services/guest-auth/guestAuthService.js, services/guest-auth/otpService.js

用户流程

flowchart TD
    A["客户访问 /account/*"] --> B{"已登录?"}
    B -->|"是"| C["进入账户页面"]
    B -->|"否"| D["登录/注册页面"]
    D --> E["输入手机号或邮箱"]
    E --> F["POST /send-code"]
    F --> G["收到 OTP 验证码"]
    G --> H["输入验证码"]
    H --> I["POST /verify-code"]
    I --> J{"账户存在?"}
    J -->|"新用户"| K["自动创建 Guest + 返回 tokens"]
    J -->|"已有账户"| L["返回 tokens"]
    K --> M["可选: POST /set-password"]
    L --> C
    M --> C

    style D fill:#2196F3,stroke:#1565C0,color:#fff
    style C fill:#4CAF50,stroke:#2E7D32,color:#fff

BDD 场景

场景 K1.1: OTP 新用户注册

Given 客户未注册
When 输入手机号 +1234567890
  And 收到并输入 6 位 OTP 验证码
Then 自动创建 Guest 记录(login_type: 'member')
  And 返回 JWT access_token + refresh_token
  And guest-auth-store 保存 tokens 和 guest 信息
  And 跳转到账户首页

测试: guest-auth.test.js (部分)

场景 K1.2: OTP 已有用户登录

Given 客户已有账户
When 输入已注册的手机号
  And 验证 OTP
Then 返回该 Guest 的 tokens
  And 无需重复创建账户

场景 K1.3: 密码登录(已设密码的用户)

Given 客户已设置密码
When 选择"密码登录"模式
  And 输入邮箱和密码
Then POST /login 验证凭据
  And 返回 tokens 并登录

测试: guest-auth.test.js

场景 K1.4: Token 自动刷新

Given access_token 即将过期(guest-auth-store 的 isTokenExpired() 返回 true)
When 发起 API 请求
Then 自动调用 POST /refresh 获取新 tokens
  And 更新 guest-auth-store
  And 原始请求继续执行

CUJ-K2: Guest 密码管理

P1 安全 — 设置密码、修改密码、忘记密码重置

TOUCHES: api/guest-auth.js (POST /set-password, POST /password-change, POST /password-reset/request, POST /password-reset/confirm) → services/guest-auth/guestPasswordService.js, services/guest-auth/guestEmailService.js

BDD 场景

场景 K2.1: OTP 注册后设置密码

Given 客户通过 OTP 注册后尚未设置密码
When 选择"设置密码"
  And 输入新密码(满足强度要求)
Then POST /set-password 保存密码
  And 后续可使用密码登录

场景 K2.2: 已登录修改密码

Given 客户已登录(login_type: 'member')
When 输入当前密码和新密码
Then POST /password-change 验证旧密码并更新
  And 所有其他会话被撤销(revokeAllGuestTokens)

场景 K2.3: 忘记密码重置

Given 客户忘记密码
When 在登录页点击"忘记密码"
  And 输入注册邮箱
Then POST /password-reset/request 发送重置邮件
  And 点击邮件链接 → POST /password-reset/confirm 设置新密码
  And 重置成功后可用新密码登录

CUJ-K3: 会员中心

P1 留存 — 客户查看自己的会员等级、权益和升级进度

TOUCHES: account/membership/page.tsxLoyalty/TierBadge.tsx, Loyalty/PointsBalance.tsx, Loyalty/TierProgress.tsxhooks/useLoyaltyApi.ts (useCustomerMembership) → Guest Auth JWT 验证

用户流程

flowchart TD
    A["客户已登录 (Guest JWT)"] --> B["/account/membership"]
    B --> C["加载会员信息"]
    C --> D["TierBadge 等级徽章"]
    C --> E["PointsBalance 积分余额"]
    C --> F["TierProgress 升级进度条"]
    C --> G["权益列表 (折扣%/积分倍率)"]
    G --> H{"操作?"}
    H -->|"查积分"| I["/account/points"]
    H -->|"买礼品卡"| J["/account/gift-cards"]

    style B fill:#2196F3,stroke:#1565C0,color:#fff
    style D fill:#4CAF50,stroke:#2E7D32,color:#fff

会员等级展示数据

字段组件说明
当前等级TierBadge等级名称 + 颜色标记
积分余额PointsBalance当前可用积分
折扣比例当前等级享受的折扣百分比
积分倍率消费积分倍率(如 1.5x)
升级进度TierProgress当前消费 vs 下一等级门槛,进度条
当期/累计消费period_spend / lifetime_spend

BDD 场景

场景 K3.1: 查看会员等级

Given 客户已通过 Guest Auth 登录(login_type: 'member')
When 导航到 /account/membership
Then TierBadge 显示当前等级(如 "Gold")
  And PointsBalance 显示积分余额
  And TierProgress 进度条显示距离下一等级还差多少消费
  And 权益列表显示: 折扣比例、积分倍率等

测试: 未覆盖

场景 K3.2: 从会员中心导航

Given 客户在会员中心
When 点击"查看积分历史"
Then 跳转到 /account/points
When 点击"购买礼品卡"
Then 跳转到 /account/gift-cards

CUJ-K4: 积分查看与历史

P1 留存 — 查看积分余额、交易历史、即将过期的积分

TOUCHES: account/points/page.tsxLoyalty/PointsBalance.tsxhooks/useLoyaltyApi.ts (usePointsAccount, usePointsTransactions)

交易类型

类型图标颜色说明
earngreen消费赚取积分
redeemred兑换消耗积分
expiregray积分过期失效
adjustblue管理员手动调整
bonusgold活动奖励积分

BDD 场景

场景 K4.1: 查看积分历史

Given 客户在 /account/points
When 页面加载
Then 顶部显示当前积分余额(PointsBalance)
  And Tab 切换: 全部交易 / 赚取记录 / 兑换记录
  And 每条记录显示: 类型图标、金额(+/-)、时间、描述
  And 支持分页

测试: 未覆盖

场景 K4.2: 即将过期积分提醒

Given 客户有积分即将在 30 天内过期
When 进入积分页面
Then 显示过期提醒卡片: "X 积分将在 Y 天后过期"
  And 提醒引导客户尽快消费或兑换

CUJ-K5: 我的礼品卡与购买

P1 营收 — 查看持有的礼品卡、购买新礼品卡赠送他人

TOUCHES: account/gift-cards/page.tsx, account/gift-cards/purchase/page.tsxLoyalty/GiftCardBalance.tsxhooks/useLoyaltyApi.ts (useCustomerGiftCards, usePurchaseGiftCard)

用户流程

flowchart TD
    A["/account/gift-cards"] --> B["礼品卡列表"]
    B --> C{"操作?"}
    C -->|"查看"| D["卡片详情"]
    D --> E["显示/隐藏卡号 (脱敏)"]
    D --> F["复制卡号"]
    D --> G["余额 / 状态"]
    C -->|"购买"| H["/account/gift-cards/purchase"]
    H --> I["选择金额"]
    I --> J["预设: $25/$50/$75/$100"]
    I --> K["自定义: $10-$500"]
    I --> L["选择配送方式"]
    L --> M["Email / SMS / 打印"]
    M --> N["填写收件人信息 + 个性化消息"]
    N --> O["确认购买"]

    style B fill:#2196F3,stroke:#1565C0,color:#fff
    style O fill:#4CAF50,stroke:#2E7D32,color:#fff

BDD 场景

场景 K5.1: 查看我的礼品卡

Given 客户在 /account/gift-cards
When 页面加载
Then 显示客户名下所有礼品卡
  And 每张卡显示: 状态(active/used/expired)、余额、脱敏卡号
  And 点击可切换显示完整卡号
  And 可以复制卡号到剪贴板

测试: 未覆盖

场景 K5.2: 购买礼品卡(预设金额)

Given 客户在 /account/gift-cards/purchase
When 选择预设金额 $50
  And 选择配送方式 "Email"
  And 填写收件人邮箱和个性化消息
  And 确认购买
Then 礼品卡创建成功
  And 收件人收到邮件通知
  And 新卡出现在"我的礼品卡"列表

场景 K5.3: 自定义金额验证

Given 客户选择"自定义金额"
When 输入 $5(低于最低 $10)
Then 显示验证错误: "金额必须在 $10-$500 之间"
When 修改为 $75
Then 验证通过,可继续购买

CUJ-K6: 员工用户偏好设置

P0 效率 — 员工自定义语言、默认门店、通知和日历设置

TOUCHES: settings/page.tsx (4 tabs) → Settings/UserPreferencesPanel.tsxcontexts/AuthContext.tsx (updatePreferences) → api/user.js (GET/PUT /user/preferences, GET /user/profile)

/settings 页面 Tab 结构

Tab内容可编辑
Preferences语言 (EN/ZH)、默认门店、默认视图 (day/week/month)Yes
Profile姓名、邮箱、角色、所属门店Read-only(管理员修改)
Notifications邮件通知、SMS 通知、推送通知开关Yes
Advanced日历开始时间、结束时间、时间槽时长Yes

UserPreferences 数据结构

字段类型默认值说明
languageenum'en'界面语言 (en/zh)
themeenum'light'主题(预留)
default_store_idstring?primary_store_id登录后默认门店
default_viewenum'week'日历默认视图
sidebar_collapsedbooleanfalse侧边栏收起状态
notifications.emailbooleantrue邮件通知总开关
notifications.smsbooleantrueSMS 通知总开关
notifications.pushbooleantrue推送通知总开关
calendar_settings.start_hournumber9日历开始时间
calendar_settings.end_hournumber21日历结束时间
calendar_settings.slot_durationnumber30时间槽时长(分钟)

BDD 场景

场景 K6.1: 修改界面语言

Given 员工在 /settings Preferences tab
When 将语言从 English 切换为 中文
  And 点击保存
Then PUT /user/preferences 保存到后端
  And AuthContext 更新 preferences
  And 整个界面切换为中文(next-intl 生效)

场景 K6.2: 设置默认门店

Given 员工有多家门店权限
When 在 Preferences 中选择"门店 B"为默认
Then 下次登录后自动定位到门店 B
  And 预约板和报表自动过滤为门店 B

场景 K6.3: 自定义日历时段

Given 员工在 Advanced tab
When 将日历开始时间改为 8:00,时间槽改为 15 分钟
Then 预约板日历从 8:00 开始显示
  And 时间槽缩小为 15 分钟一格

CUJ-K7: 员工门店切换

P0 效率 — 多门店员工在不同门店间快速切换上下文

TOUCHES: contexts/AuthContext.tsx (switchStore, checkStoreAccess) → api/user.js (POST /user/switch-store)

BDD 场景

场景 K7.1: 切换当前门店

Given 员工有门店 A 和门店 B 的访问权限
When 从顶部导航栏的门店选择器切换到门店 B
Then POST /user/switch-store 更新上下文
  And AuthContext.permissions.currentStore 更新为门店 B
  And 页面数据刷新为门店 B 的数据
  And 侧边栏和面包屑反映新门店

测试: 已覆盖

场景 K7.2: 无权限门店阻止

Given 员工只有门店 A 的权限
When 尝试通过 URL 直接访问门店 C 的数据
Then checkStoreAccess 返回 false
  And 显示权限不足提示
  And 重定向回有权限的门店

CUJ-K8: 通知偏好设置

P2 合规 — 客户管理通知接收偏好 (opt-in/opt-out)

TOUCHES: account/preferences/page.tsx (⚠️ DEPRECATED)
⚠️ 当前状态: account/preferences/page.tsx 的后端 API (/api/marketing-automation/preferences) 已移除。 页面源码注释标注为 deprecated,待迁移到 001-email-sms-integration 系统。 以下 BDD 场景描述的是目标状态

BDD 场景

场景 K8.1: 管理通知偏好(目标状态)

Given 客户在 /account/preferences
When 页面加载
Then 显示通知开关列表:
  And 邮件通知 (总开关)
  And 短信通知 (总开关)
  And 预约提醒
  And 服务回访
  And 营销邮件
  And 生日祝福

测试: 未覆盖 (后端 API deprecated)

场景 K8.2: 取消营销订阅

Given 客户不想接收营销邮件
When 关闭"营销邮件"开关并保存
Then 后续不再发送营销邮件
  And 功能性通知(预约提醒)不受影响
  And 符合 CAN-SPAM / GDPR 合规要求

CUJ-K9: Guest Session 管理

P2 安全 — 查看活跃会话、撤销异常登录

TOUCHES: api/guest-auth.js (GET /sessions, POST /logout)

BDD 场景

场景 K9.1: 查看活跃会话

Given 客户已登录(login_type: 'member')
When 查看会话管理
Then GET /sessions 返回所有活跃会话
  And 每个会话显示: 设备信息、登录时间、最后活跃时间
  And 当前会话高亮标记

场景 K9.2: 登出所有设备

Given 客户发现异常会话
When 点击"登出所有设备"
Then 撤销所有 refresh tokens
  And 仅保留当前会话
  And 其他设备需要重新登录

跨模块依赖

依赖方向说明
K → CUJ-A (Auth)员工 Auth(登录/JWT)在 CUJ-A;本文档覆盖登录后的账户管理
K → CUJ-M (Loyalty/GiftCards)K3/K4/K5 的会员、积分、礼品卡数据来源
K → CUJ-J (Settings)K6 UserPreferences 是 Settings 4 级体系中最底层的 User 级别
K → CUJ-L (Stores)K7 门店切换需要门店列表;K6 默认门店选择
CUJ-I (Marketing) → KK8 通知偏好影响营销邮件发送的 opt-in/opt-out
CUJ-G (Booking) → KGuest 在 Booking 中创建,升级到 K1 的 member 身份

业务规则

#规则实现位置
1两层客户身份: 匿名客人(无可验证联系方式)与会员(有手机号/邮箱并完成 OTP)。只有 member 才能访问 /account/* 页面guest-auth middleware + guest-auth-store
2OTP 优先: 060-guest-auth-redesign 后,新用户通过 send-code → verify-code 注册;旧 register 端点标记 deprecated。密码为可选(/set-password)guest-auth.js + otpService.js
3Token 管理: guest-auth-store 使用 Zustand 持久化到 localStorage;JWT access_token + refresh_token 双 token 机制;isTokenExpired() 检查过期guest-auth-store.ts
4密码修改撤销会话: 修改密码后 revokeAllGuestTokens 撤销所有其他会话,强制重新登录guestPasswordService.js
5礼品卡金额范围: 自定义金额 $10-$500,预设选项 $25/$50/$75/$100。配送方式: Email / SMS / 打印account/gift-cards/purchase/page.tsx
6员工偏好作用域: UserPreferences 是 Settings 4 级体系 (Platform→Tenant→Store→User) 的最底层,仅影响当前用户,不影响其他人settings/page.tsx + AuthContext
7门店切换即时生效: switchStore 更新 AuthContext.permissions.currentStore,所有数据查询自动过滤为新门店AuthContext.tsx + api/user.js
8Profile 只读: 员工不能自行修改姓名/邮箱/角色,必须由管理员在 /employees/[id] 修改settings/page.tsx Profile tab
9Rate Limiting: guest-auth 的 send-code / verify-code / login / register / password-reset 均有频率限制,防止暴力破解guest-auth.js (express-rate-limit)
跨文档参考: