Web 客户模块 — CUJ 关键用户旅程

Guest/Customer Management · CRM · VIP · Tags | 更新时间: 2026-02-08

TOUCHES 追溯

📄 Pages (7): guests/page.tsx (客户列表), guests/[id]/page.tsx (客户详情), customers/create/page.tsx (创建客户), admin/guests/page.tsx (管理员视图), admin/guests/[id]/page.tsx (管理员详情), admin/guests/duplicates/page.tsx (重复检测), admin/guests/blacklist/page.tsx (黑名单管理)
🧩 Components (15): GuestTable.tsx (可调列宽数据表), GuestCreateForm.tsx (创建表单), GuestEditForm.tsx (编辑表单), AppointmentHistory.tsx (预约时间线), ServicePreferences.tsx (服务偏好编辑), VipBadge.tsx (VIP 徽章), VipProgress.tsx (VIP 进度条), GuestNotes.tsx (备注 CRUD), GuestRelationships.tsx (关系列表), RelationshipEditModal.tsx (关系编辑弹窗), GuestGiftCards.tsx (礼品卡摘要), GuestTagsList.tsx (标签筛选页), GuestMergeWizard.tsx (合并向导), BlacklistWarning.tsx (黑名单横幅), AddGuestModal.tsx (预约中添加客户)
🏪 Stores / Hooks: guest-auth-store.ts (Zustand 客户认证状态)
⚙️ Backend (4 API + 4 Services): api/guests.js (14 endpoints: CRUD + VIP + blacklist + duplicates + merge + inferred-time), api/guest-notes.js (5 endpoints: CRUD + pin), api/guest-relationships.js (5 endpoints: CRUD + types), api/guest-auth.js (6 endpoints: OTP + JWT), services/guest-auth/guestAuthService.js, services/guest-auth/otpService.js, services/guest-auth/guestPasswordService.js, services/guest/inferredPreferenceService.js

架构概览:客户数据模型

erDiagram
    guests ||--o{ guest_notes : "has notes"
    guests ||--o{ guest_tags : "has tags"
    guests ||--o{ guest_relationships : "has relationships"
    guests ||--o{ appointments : "has appointments"
    guests ||--o{ gift_cards : "owns/receives"
    guests ||--o{ guest_verification_codes : "OTP codes"
    guests ||--o{ guest_merge_logs : "merge audit"

    guests {
        string id PK "guest_{ts}_{random}"
        string name "Full name"
        string phone "Optional for anonymous"
        string email "Optional"
        enum account_type "anonymous | identified | registered"
        string password_hash "Only for registered"
        enum vip_status "none | bronze | silver | gold | platinum"
        int vip_points "Loyalty points"
        int visit_count "Total visits"
        numeric total_spent "Lifetime value"
        boolean blacklist_status "Block flag"
        string blacklist_reason "Admin note"
        jsonb service_preferences "Allergies, prefs, comms"
        string preferred_language "en | zh | es | vi | ko"
        string inferred_preferred_time "AI calculated"
        string source "kiosk | web_booking | staff | zenoti"
    }

三级客户模型

层级account_type要求能力来源
Tier 1: 匿名客人 anonymous 仅姓名(可空) Kiosk 预约、Walk-in 记录 Kiosk(无手机号时)、Walk-in
Tier 2: 已识别客人 identified 手机号已验证 手机号查找、SMS 通知、Guest App 预约 Kiosk(提供手机号)、Web Booking、Staff 创建
Tier 3: 注册会员 registered Email + 手机号 + 密码 Guest App 登录、忠诚度积分、管理个人资料 Guest App 注册、OTP 验证后设密码

VIP 等级体系

等级积分范围图标样式
none0 – 99隐藏
bronze100 – 499Award琥珀色
silver500 – 1,499Star石板灰
gold1,500 – 4,999Crown金色
platinum5,000+Gem紫色

权限矩阵

操作所需权限说明
查看客户列表/详情guests:view所有员工可见
创建/编辑客户guests:create / guests:edit前台+管理员
删除客户guests:delete需检查无活跃预约
VIP / 黑名单 / 合并guests:manage仅管理员

目录

CUJ 总览与优先级矩阵

CUJ优先级描述触发点业务价值E2E 状态
E1 P0 客户列表与搜索 侧边栏 → Guests CRM 基础 — 快速定位客户 已覆盖
E2 P0 客户详情 360° 视图 点击客户行 完整客户画像 — 预约/消费/备注/VIP/礼品卡 已覆盖
E3 P1 创建新客户 Walk-in / 电话预约 客户数据积累,三级模型起点 已覆盖
E4 P1 客户备注管理 客户详情页 → 备注 Tab 服务质量 — 技师间信息共享 部分覆盖
E5 P1 服务偏好与过敏记录 客户详情页 → 偏好 Tab 个性化服务 — 过敏安全 部分覆盖
E6 P1 VIP 忠诚度管理 客户详情页 → VIP 区域 客户留存 — 5 级积分体系 缺失
E7 P2 客户关系与标签 客户详情 → 关系/标签区域 家庭关联、自动化营销分群 部分覆盖
E8 P2 重复客户检测与合并 管理员 → 重复客户页面 数据质量 — 消除重复记录 缺失
E9 P2 黑名单管理 管理员 → 黑名单页面 风险控制 — 阻止问题客户预约 缺失

CUJ-E1: 客户列表与搜索

P0 CRM 基础 — 搜索和浏览客户

TOUCHES: guests/page.tsx, GuestTable.tsx, useStoreFilter, GET /api/guests

用户流程

flowchart TD
    A["侧边栏点击 Guests"] --> B["客户列表 /guests"]
    B --> C{"操作"}
    C -->|"搜索"| D["输入姓名/手机号 → 实时筛选"]
    C -->|"筛选"| E["门店筛选 useStoreFilter + 显示非活跃"]
    C -->|"点击行"| F["跳转客户详情 /guests/id"]
    C -->|"添加客户"| G["打开创建表单/弹窗"]
    D --> B
    E --> B

    style B fill:#2196F3,stroke:#1565C0,color:#fff

BDD 场景

场景 E1.1: 客户列表加载

Given 员工已登录且拥有 guests:view 权限
When 导航到 /guests
Then 显示客户列表:姓名、客户编码、手机号、邮箱、性别、注册日期、来源、VIP 状态
  And 默认分页 20 条/页,支持排序
  And GuestTable 支持列宽拖拽调整

测试: customers/list.spec.ts

场景 E1.2: 搜索客户(姓名 + 手机号)

Given 客户列表页面
When 在搜索框输入 "Jane"
Then 列表实时筛选姓名包含 "Jane" 的客户
When 输入手机号 "212"
Then 列表筛选手机号包含 "212" 的客户
  And 搜索同时匹配姓名和手机号字段

测试: customers/list.spec.ts

场景 E1.3: 门店筛选与非活跃客户

Given 多门店租户,当前选择 "Store A"
When 通过 useStoreFilter 切换到 "Store B"
Then 列表仅显示 Store B 关联的客户
When 勾选 "显示非活跃客户"
Then 列表包含 is_active: false 的客户记录

CUJ-E2: 客户详情 360° 视图

P0 完整客户画像 — 查看客户全维度信息

TOUCHES: guests/[id]/page.tsx, AppointmentHistory.tsx, ServicePreferences.tsx, VipBadge.tsx, VipProgress.tsx, GuestNotes.tsx, GuestRelationships.tsx, GuestGiftCards.tsx, GuestEditForm.tsx, BlacklistWarning.tsx, GET /api/guests/{id}, GET /api/guests/{id}/history

用户流程

flowchart TD
    A["客户列表点击行"] --> B["客户详情页 /guests/id"]
    B --> H{"黑名单?"}
    H -->|"是"| BW["BlacklistWarning 横幅"]
    H -->|"否"| C

    B --> C["头部: 姓名 + VipBadge + 基础统计"]
    C --> TAB{"Tab 切换"}
    TAB --> T1["基本信息: 姓名、手机、邮箱、性别、DOB、语言"]
    TAB --> T2["预约历史: AppointmentHistory 时间线"]
    TAB --> T3["消费记录: 总额、平均客单价"]
    TAB --> T4["备注: GuestNotes (6 类型)"]
    TAB --> T5["偏好: ServicePreferences"]
    TAB --> T6["礼品卡: GuestGiftCards"]
    TAB --> T7["关系: GuestRelationships"]
    TAB --> T8["标签: GuestTagsList"]

    T1 --> ED{"编辑?"}
    ED -->|"是"| EF["GuestEditForm 内联编辑"]

    style B fill:#2196F3,stroke:#1565C0,color:#fff
    style BW fill:#c62828,stroke:#b71c1c,color:#fff

BDD 场景

场景 E2.1: 查看客户概览

Given 客户 "Jane Doe" 存在且有 15 次到访
When 点击 Jane 进入详情页
Then 头部显示: 姓名、VipBadge(根据等级显示图标)、VipProgress 进度条
  And 显示统计: 到访次数(15)、消费总额、平均客单价、首次/最近到访日期
  And 显示客户来源标记 (kiosk / web_booking / staff / zenoti)

测试: customers/details.spec.ts

场景 E2.2: 查看预约历史时间线

Given Jane 有 15 条预约记录
When 切换到"预约历史" Tab
Then AppointmentHistory 组件显示时间线
  And 每条显示: 日期、时间、状态徽章(8 种状态)、技师、服务列表、金额
  And 已完成预约显示评分 (1-5) 和反馈(如有)
  And 支持展开/收起查看详情

场景 E2.3: 编辑客户基本信息

Given 员工拥有 guests:edit 权限
When 在基本信息 Tab 点击"编辑"
Then GuestEditForm 切换为编辑模式
  And 可修改: 姓名、手机号、邮箱、性别、出生日期、偏好语言
When 保存修改
Then 调用 PUT /api/guests/{id} 更新
  And 页面刷新显示更新后的信息

场景 E2.4: 查看礼品卡摘要

Given Jane 持有 3 张礼品卡(2 active, 1 expired)
When 切换到"礼品卡" Tab
Then GuestGiftCards 显示: 总卡数(3)、活跃卡数(2)、总余额
  And 每张卡显示: 卡号、余额、状态、购买日期、过期日、持有类型(buyer/recipient)

CUJ-E3: 创建新客户

P1 客户数据积累 — Walk-in、电话预约、预约中添加

TOUCHES: customers/create/page.tsx, GuestCreateForm.tsx, AddGuestModal.tsx, POST /api/guests

BDD 场景

场景 E3.1: 通过专用页面创建客户

Given 员工拥有 guests:create 权限
When 导航到 /customers/create
  And 填写姓名 "New Guest"、手机号 "2125551234"
  And 可选填: 邮箱、性别、出生日期、偏好、备注
  And 点击保存
Then POST /api/guests 创建记录,ID 格式 guest_{ts}_{random}
  And account_type 默认为 identified(有手机号)
  And 跳转到 /admin/guests

测试: customers/create.spec.ts

场景 E3.2: 重复手机号检测

Given 手机号 "2125551234" 已关联客户 "Jane Doe"
When 创建新客户使用相同手机号
Then 显示警告: "该手机号已关联客户 Jane Doe"
  And 提供跳转到已有客户的链接
  And 不阻止创建(允许覆盖,但记录警告)

场景 E3.3: 预约流程中创建客户

Given 员工在预约看板创建预约
When 客户搜索无结果
  And 点击"新建客户"
Then AddGuestModal 弹出(精简表单: 姓名 + 手机号)
When 填写并保存
Then 客户创建成功,自动关联到当前预约
💡 三级模型起点: Staff 创建的客户默认 account_type: 'identified'(有手机号)或 'anonymous'(仅姓名)。客户后续可通过 Guest App 注册升级为 'registered'

CUJ-E4: 客户备注管理

P1 服务质量 — 技师间信息共享,私密备注保护

TOUCHES: GuestNotes.tsx, GET/POST/PUT/DELETE /api/guests/{id}/notes, PUT /api/guests/{id}/notes/{noteId}/pin

备注类型

类型键值用途颜色
通用general一般性备注默认
服务service服务相关(如偏好的指甲形状)蓝色
行为behavior客户行为特征(如常迟到)橙色
偏好preference个人偏好(如喜欢安静)绿色
医疗medical健康相关(如孕期、皮肤敏感)红色
其他other未分类备注灰色

BDD 场景

场景 E4.1: 创建和置顶备注

Given 客户详情页 → 备注 Tab
When 点击"添加备注"
  And 选择类型 "medical",输入 "对丙烯酸过敏,必须使用无丙烯酸产品"
  And 保存
Then 备注创建,显示红色标记(medical 类型)
  And 显示创建者姓名和时间
When 点击置顶图标
Then 备注固定在列表顶部(is_pinned: true

场景 E4.2: 私密备注权限控制

Given 用户已登录并拥有对应功能所需权限与范围(scope),创建了一条私密备注 (is_private: true)
When 普通员工查看该客户备注
Then 私密备注不显示
When 拥有私密备注查看权限的用户查看
Then 私密备注可见,带 🔒 标记

场景 E4.3: 编辑和删除备注

Given 一条已有备注
When 点击编辑 → 修改内容/类型/私密标记 → 保存
Then 备注更新,显示 updated_at 时间
When 点击删除 → 确认
Then 备注软删除(设置 deleted_at,不从数据库物理删除)

CUJ-E5: 服务偏好与过敏记录

P1 个性化服务 — 偏好技师、过敏安全、沟通设置

TOUCHES: ServicePreferences.tsx, PUT /api/guests/{id}/preferences, guests.service_preferences (JSONB)

偏好数据结构

字段类型说明
preferred_servicesID[]偏好服务多选
preferred_employeesID[]偏好技师多选
allergiesstring[]预设: Acetone, Acrylics, Gel, Latex, Fragrance, Formaldehyde + 自定义
special_requeststext自由文本(max 500 字符)
preferred_time_slotsenum[]morning (9-12) / afternoon (12-17) / evening (17-21)
communication_preferencesobjectreminder_sms, reminder_email, marketing_sms, marketing_email

BDD 场景

场景 E5.1: 编辑服务偏好

Given 客户详情页 → 偏好 Tab
When 选择偏好服务 "Gel Manicure", "Pedicure"
  And 选择偏好技师 "Lily"
  And 勾选过敏: "Acrylics", 自定义添加 "Toluene"
  And 时段偏好: "afternoon"
  And 保存
Then PUT /api/guests/{id}/preferences 更新 JSONB 字段
  And 过敏信息在预约创建时自动提示技师

场景 E5.2: 沟通偏好设置

Given 客户默认 reminder_sms=true, marketing_sms=false
When 客户要求关闭短信提醒
  And 取消勾选 reminder_sms
Then 该客户不再收到预约提醒短信
  And 营销消息系统检查此标记后跳过发送
💡 AI 推断偏好: inferredPreferenceService.js 基于预约历史自动推断偏好时段 (inferred_preferred_time),支持单个刷新和批量重算。API: GET/POST /api/guests/{id}/inferred-timePOST /api/guests/inferred-time/batch

CUJ-E6: VIP 忠诚度管理

P1 客户留存 — 5 级 VIP 体系,积分累积与等级升降

TOUCHES: VipBadge.tsx, VipProgress.tsx, PUT /api/guests/{id}/vip, guests.vip_status, guests.vip_points

BDD 场景

场景 E6.1: 查看 VIP 状态与进度

Given 客户 Jane 积分 480 (bronze 级)
When 查看客户详情
Then VipBadge 显示 Award 图标 + "Bronze" 文字
  And VipProgress 显示进度条: 480/500(距 Silver 还需 20 分)
  And 琥珀色样式

场景 E6.2: 管理员调整 VIP 等级

Given 用户已登录并拥有 guests:manage 权限,且具备对应范围(scope)
When 手动将 Jane 升级为 Gold,设置积分 2000
Then PUT /api/guests/{id}/vip 更新
  And VipBadge 切换为 Crown 图标 + 金色样式
  And VipProgress 显示 2000/5000(距 Platinum 还需 3000 分)

场景 E6.3: 满级 Platinum 展示

Given 客户积分 5000+
When 查看 VIP 进度
Then VipBadge 显示 Gem 图标 + 紫色样式
  And VipProgress 显示满级祝贺消息(无进度条)

CUJ-E7: 客户关系与标签

P2 关联客户 — 家庭/朋友关系 + 自动/手动标签分群

TOUCHES: GuestRelationships.tsx, RelationshipEditModal.tsx, GuestTagsList.tsx, api/guest-relationships.js (5 endpoints)

关系类型

类型键值说明
朋友friend朋友关系
家庭family家庭成员
同事colleague工作同事
伴侣partner配偶/伴侣
其他other自定义标签补充

标签类别

类别来源示例
engagementAutofrequent visitor, at risk, dormant
birthdayAuto月份标签
demographicsAuto年龄段、来源渠道
purchase_historyAutohigh spender, loyal, new
membershipAutoVIP 等级标签
customManual员工手动添加

BDD 场景

场景 E7.1: 添加客户关系

Given 客户详情页 → 关系区域
When 点击"添加关系"
Then RelationshipEditModal 打开
When 搜索并选择 "John Doe",关系类型 "family",自定义标签 "Sister"
  And 保存
Then 关系双向创建(Jane 的页面显示 John,John 的页面也显示 Jane)
  And 显示同行到访次数和最近同行日期

测试: customers/relationships.spec.ts

场景 E7.2: 按标签筛选客户

Given 用户已登录并拥有对应功能所需权限与范围(scope),进入标签筛选页面
When 选择标签 "high spender" (purchase_history 类别)
Then GuestTagsList 显示所有带此标签的客户卡片
  And 每张卡片显示: 姓名、最近到访、联系方式
  And 标签显示来源标记 (auto / manual)

CUJ-E8: 重复客户检测与合并

P2 数据质量 — 检测并合并重复客户记录

TOUCHES: admin/guests/duplicates/page.tsx, GuestMergeWizard.tsx, GET /api/guests/duplicates, POST /api/guests/merge, guest_merge_logs (审计表)

合并流程

flowchart TD
    A["管理员 → /admin/guests/duplicates"] --> B["系统检测重复组"]
    B --> C["显示: 相似度分数 + 匹配原因(手机/邮箱)"]
    C --> D{"选择操作"}
    D -->|"合并"| S1["Step 1: 选择主记录"]
    S1 --> S2["Step 2: 选择要合并的副本"]
    S2 --> S3["确认合并(不可逆警告)"]
    S3 --> R["合并结果"]
    R --> R1["预约历史合并到主记录"]
    R --> R2["积分累加,VIP 取最高"]
    R --> R3["副本标记 inactive"]
    R --> R4["guest_merge_logs 审计记录"]
    D -->|"忽略"| IGN["标记为已忽略"]

    style S3 fill:#c62828,stroke:#b71c1c,color:#fff

BDD 场景

场景 E8.1: 查看重复客户

Given 用户已登录并拥有 guests:manage 权限,且具备对应范围(scope)
When 导航到 /admin/guests/duplicates
Then 显示系统检测到的疑似重复组
  And 每组显示: 相似度分数、匹配原因(同手机号/同邮箱)
  And 按相似度降序排列

场景 E8.2: 执行合并(3 步向导)

Given 检测到 "Jane Doe" (phone: 212-555-1234) 和 "J. Doe" (phone: 212-555-1234) 为重复
When 点击"合并"
Then GuestMergeWizard Step 1: 选择主记录(toggle 按钮)
When 选择 "Jane Doe" 为主记录 → 下一步
Then Step 2: 确认要合并的副本(checkbox)
When 确认合并
Then POST /api/guests/merge 执行
  And "J. Doe" 的预约历史转移到 "Jane Doe"
  And VIP 积分累加,等级取最高
  And "J. Doe" 标记为 inactive
  And Step 3: 显示合并成功确认

场景 E8.3: 合并约束 — 有活跃预约

Given "J. Doe" 有一个 status='scheduled' 的活跃预约
When 尝试合并
Then 系统提示错误: "副本客户有活跃预约,请先处理后再合并"
  And 合并按钮禁用

CUJ-E9: 黑名单管理

P2 风险控制 — 管理问题客户,阻止预约

TOUCHES: admin/guests/blacklist/page.tsx, BlacklistWarning.tsx, PUT /api/guests/{id}/blacklist, guests.blacklist_status, guests.blacklist_reason

BDD 场景

场景 E9.1: 添加客户到黑名单

Given 用户已登录并拥有对应功能所需权限与范围(scope),在客户详情页,拥有 guests:manage 权限
When 点击"加入黑名单"
  And 输入原因 "多次爽约,造成排班浪费"
  And 确认
Then PUT /api/guests/{id}/blacklist 设置 blacklist_status=true
  And 记录 blacklisted_at 时间 + blacklisted_by 操作人
  And 详情页顶部显示 BlacklistWarning 红色横幅
  And 该客户在公共预约流程中被阻止

场景 E9.2: 黑名单列表管理

Given 用户已登录并拥有对应功能所需权限与范围(scope),导航到 /admin/guests/blacklist
When 页面加载
Then 显示所有黑名单客户列表
  And 每条显示: 姓名、联系方式、加入原因、操作人、日期
  And 支持按姓名/邮箱/手机号搜索

场景 E9.3: 移除黑名单

Given 客户 "Bad Guest" 在黑名单中
When 用户点击"移除黑名单" → 确认
Then blacklist_status 设为 false
  And BlacklistWarning 横幅消失
  And 客户恢复预约能力

跨模块链接

关联模块关联点说明
CUJ-G (Public Booking) 三级客户模型 → 预约创建 匿名/已识别/注册客户都可预约;黑名单客户被阻止
CUJ-B (Appointments) 客户详情 → 预约历史 AppointmentHistory 组件显示该客户的所有预约时间线
CUJ-A (Auth) Guest Auth (OTP) guestAuthService 管理三级升级流程: anonymous → identified → registered
CUJ-F (Payments) 客户消费记录 + 礼品卡 客户详情页显示消费总额、客单价、礼品卡余额
CUJ-I (Marketing) 标签系统 → 营销分群 Auto/Manual 标签用于自动化营销触发器筛选目标客户

业务规则

#规则实现位置
1 三级客户模型: anonymous (仅姓名) → identified (手机号验证) → registered (密码保护)。Kiosk 来源跳过 SMS 验证 (source: 'kiosk' + X-Device-Type: kiosk) guestAuthService.js, otpService.js
2 客户 ID 格式: guest_{timestamp}_{random},不使用 UUID guestAuthService.generateGuestId()
3 合并不可逆: 副本记录标记 inactive,预约/积分转移到主记录,审计日志记录在 guest_merge_logs POST /api/guests/merge
4 黑名单阻止预约: blacklist_status=true 的客户在公共预约和 Guest App 中被阻止创建新预约 api/guests.js, 预约创建逻辑
5 备注隐私: is_private: true 的备注仅创建者和经理/管理员可见,普通员工的 GET 请求自动过滤 api/guest-notes.js
6 偏好存储: 服务偏好、过敏、沟通偏好统一存储在 guests.service_preferences JSONB 字段 PUT /api/guests/{id}/preferences
7 删除客户检查: 有活跃预约 (status ∈ pending/scheduled/confirmed/checked_in/in_progress) 的客户不可删除 DELETE /api/guests/{id}