2026-06-28-customer-module-create-list-summary-design.md 11 KB

顾客模块设计(新建 / 列表 / 统计)

1. 背景

当前项目已经具备:

  • 发型师登录闭环
  • 最小工作台入口
  • GCS 文件上传能力

顾客模块目前在前端已经存在:

  • 新建顾客页面
  • 顾客列表页面
  • 顾客统计卡片

但后端 customers 模块仍是占位实现,数据库也缺少真正承载顾客档案主数据的 customer 主表。

本次 case 的目标是先完成顾客模块的最小业务闭环:

  • 新建顾客
  • 顾客分页列表 + 条件查询
  • 顾客统计接口

顾客详情页明确留到下一个 case。

2. 范围

本次实现

  1. 新建顾客并写入数据库
  2. 头像可选;未上传时使用默认头像 URL
  3. 顾客列表分页查询
  4. 顾客列表条件筛选
  5. 顾客顶部统计接口
  6. 前端顾客新建页与列表页对接真实后端接口

本次不实现

  1. 顾客详情页
  2. 编辑顾客
  3. 顾客施术记录详情聚合
  4. 顾客提醒发送
  5. 顾客 AI 模拟详情页聚合
  6. 顾客历史记录接口

3. 设计原则

3.1 数据对齐策略

本次采用:

以现有数据库设计为准,前端做适配

解释:

  • 数据库结构优先贴近现有 SQL 文档与既有业务表关系
  • 前端当前表单字段不直接决定数据库列名
  • 后端负责做字段名映射与值规范化

3.2 头像策略

  • 顾客头像不是必填
  • 前端上传头像时,复用已完成的存储能力
  • 上传 type 固定使用:avatar
  • 若前端未上传头像,则后端在创建顾客时自动写入默认头像 URL:

https://storage.googleapis.com/aitori-public/style/sample/066c7ba22ea149268444bc2d919d5b97.png

3.3 分页策略

  • 顾客列表采用页码分页
  • 查询参数使用:page + pageSize

3.4 查询语义

  • 搜索词:模糊查询
  • 到店状态:日期范围查询
  • 跟进:按业务枚举查询
  • 发型:按发长精确匹配

3.5 统计语义

  • 今天到店:按计划到店日判断,即 nextVisit
  • 需跟进:按 followStatus 判断

4. 数据模型设计

4.1 新增主表

本次新增 customer 主表,作为顾客档案主数据的真实来源。

该表用于承接:

  • 顾客基本资料
  • 顾客联系信息
  • 咨询与发质信息
  • 跟进信息

并作为以下表的逻辑上游主表:

  • customer_treatment_record.customer_id
  • ai_hair_simulation.customer_id

本次不强制要求一次性补完数据库外键约束,但 Prisma 与业务语义上必须明确该主从关系。

4.2 建议字段

建议字段如下:

  • id
  • hairstylist_id
  • name
  • kana
  • phone
  • email
  • birthday
  • avatar_url
  • contact_type
  • line_status
  • last_visit_date
  • next_visit_date
  • visit_cycle
  • hair_length
  • hair_type
  • hair_condition
  • concern
  • desired_image
  • formula_note
  • allergy_note
  • private_note
  • follow_status
  • follow_timing
  • follow_message
  • create_time
  • update_time
  • is_deleted

4.3 索引

本次 migration 只新增 customer 表及必要索引,不修改既有旧表结构。

建议索引:

  • idx_hairstylist_id
  • idx_phone
  • idx_next_visit_date
  • idx_follow_status
  • idx_hair_length
  • idx_is_deleted
  • 组合索引:(hairstylist_id, is_deleted)

可选唯一约束:

  • (hairstylist_id, phone, is_deleted)

说明:

  • 若允许同一个发型师名下同手机号只存在一条未删除顾客记录,则增加唯一约束
  • 若当前业务暂不确认,先不加唯一约束,避免阻塞本次实现

本次默认先不强制该唯一约束,避免因历史脏数据或录入场景不明确导致创建失败。

5. 值规范化设计

后端负责把前端表单值规范化后再入库。

5.1 发长

  • Short -> SHORT
  • Medium -> MEDIUM
  • Long -> LONG

5.2 联系方式

  • LINE -> LINE
  • Phone -> PHONE
  • Email -> EMAIL

5.3 LINE 关联状态

  • 已关联 / 已連携 -> LINKED
  • 未关联 / 未連携 -> UNLINKED
  • 邀请已发送 -> INVITED
  • 保留 -> KEEP

5.4 跟进状态

本次采用规范枚举存储:

  • NONE
  • NEED_FOLLOW_UP
  • SENT

若前端传入的是展示文案,后端负责映射成上述规范值。

6. 前后端字段映射

前端字段与数据库字段映射如下:

  • image -> avatar_url
  • contact -> contact_type
  • lineStatus -> line_status
  • lastVisit -> last_visit_date
  • nextVisit -> next_visit_date
  • hairLength -> hair_length
  • hairType -> hair_type
  • hairCondition -> hair_condition
  • desiredImage -> desired_image
  • formulaNote -> formula_note
  • allergyNote -> allergy_note
  • privateNote -> private_note
  • followStatus -> follow_status
  • followTiming -> follow_timing
  • followMessage -> follow_message

说明:

  • 前端提交结构尽量保持现状
  • 后端 DTO / mapper 负责做字段映射与值规范化
  • 返回给前端时再映射回前端当前约定字段

7. API 设计

7.1 新建顾客

POST /api/v1/customers

请求体:

type CreateCustomerRequest = {
  name: string
  phone: string
  kana?: string
  email?: string
  birthday?: string
  image?: string
  contact?: string
  lineStatus?: string
  lastVisit?: string
  nextVisit?: string
  visitCycle?: string
  hairLength?: string
  hairType?: string
  hairCondition?: string
  concern?: string
  desiredImage?: string
  formulaNote?: string
  allergyNote?: string
  privateNote?: string
  followStatus?: string
  followTiming?: string
  followMessage?: string
}

规则:

  • name 必填
  • phone 必填
  • image 非必填
  • 若 image 未传,则后端自动写入默认头像 URL
  • 头像上传由前端先调用存储上传接口完成,再将上传结果 URL 传给顾客接口

返回:

  • 返回创建后的顾客对象
  • id 对前端按字符串返回
  • 日期字段统一返回 YYYY-MM-DD

7.2 顾客分页列表

GET /api/v1/customers

查询参数:

type ListCustomersQuery = {
  page?: number
  pageSize?: number
  q?: string
  visitStatus?: 'ALL' | 'TODAY' | 'THIS_WEEK' | 'OVER_30_DAYS_NO_VISIT'
  followStatus?: 'ALL' | 'NEED_FOLLOW_UP' | 'SENT'
  hairLength?: 'ALL' | 'SHORT' | 'MEDIUM' | 'LONG'
}

返回:

type ListCustomersResponse = {
  items: CustomerListItem[]
  total: number
  page: number
  pageSize: number
}

约束:

  • total 表示当前筛选条件下的总条数
  • 用于前端展示“搜索结果 X 件”
  • items 只返回列表页面当前真正需要的字段

7.3 顾客统计

GET /api/v1/customers/summary

返回:

type CustomerSummaryResponse = {
  total: number
  today: number
  followUp: number
  simulated: number
}

说明:

  • 统计接口归属顾客域内,不继续使用 /stylist/dashboard/customer-summary
  • 前端后续改为调用顾客域内接口

8. 列表项设计

本次列表项只返回当前前端真正用到的字段,不提前把详情页字段全部塞入列表返回。

建议列表项字段:

  • id
  • name
  • phone
  • image
  • lastVisit
  • hairLength
  • concern
  • followStatus

9. 查询规则

9.1 搜索词 q

模糊查询以下字段:

  • name
  • phone
  • concern
  • private_note

9.2 到店状态 visitStatus

ALL

不加筛选条件

TODAY

next_visit_date = 今天

THIS_WEEK

next_visit_date 落在本周范围内

OVER_30_DAYS_NO_VISIT

last_visit_date < 今天 - 30天

9.3 跟进状态 followStatus

ALL

不加筛选条件

NEED_FOLLOW_UP

follow_status = NEED_FOLLOW_UP

SENT

follow_status = SENT

9.4 发长 hairLength

ALL

不加筛选条件

SHORT | MEDIUM | LONG

精确匹配 hair_length

10. 统计规则

所有统计均只统计:

  • 当前登录发型师名下数据
  • is_deleted = 0

10.1 顾客总数

统计当前发型师名下未删除顾客总数

10.2 今天到店

统计:

  • next_visit_date = 今天

10.3 需跟进

统计:

  • follow_status = NEED_FOLLOW_UP

10.4 已模拟

统计规则:

  • 在 ai_hair_simulation 中存在记录
  • customer_id 非空
  • 按 customer_id 去重
  • 仅统计当前发型师名下顾客

实现建议:

  • 以 customer 为主表
  • 关联 ai_hair_simulation
  • 对顾客维度去重计数

避免把其他发型师的数据混入当前统计结果。

11. 鉴权与数据隔离

本次所有顾客接口都必须按当前登录发型师进行数据隔离。

包括:

  • 新建顾客
  • 顾客列表
  • 顾客统计

后端从 JWT 中读取当前用户 sub,作为 hairstylist_id。

规则:

  • 创建时写入 hairstylist_id
  • 查询时只能查当前发型师自己的顾客
  • 统计时只能统计当前发型师自己的顾客

12. 后端模块边界

本次 CustomersService 只实现:

  • create()
  • list()
  • getSummary()

本次不实现:

  • getById()
  • update()
  • records / reminders / detail 聚合

说明:

  • 顾客详情页是下一个 case
  • 本次只覆盖当前前端真正依赖的最小闭环

13. 前端改动边界

本次前端只做必要适配:

  1. 顾客头像上传时,调用存储上传接口并使用 type=avatar
  2. 顾客列表接口补充:
    • page
    • pageSize
    • visitStatus
    • followStatus
    • hairLength
  3. 顶部统计接口改为调用 /customers/summary
  4. 分页结构对接真实 total

本次不强做复杂分页交互设计,只保留当前页面所需的最小可用能力。

14. 测试设计

14.1 单元测试

至少覆盖:

  1. 新建顾客时未传头像,自动写默认头像 URL
  2. 新建顾客时写入当前 hairstylist_id
  3. 列表筛选参数可正确转换为查询条件
  4. 统计逻辑符合定义

14.2 e2e 测试

至少覆盖:

  1. 未登录访问顾客接口返回 401
  2. 新建顾客成功
  3. 顾客列表分页成功
  4. 顾客列表筛选成功
  5. 顾客统计接口成功

14.3 联调验收

完成标准:

  1. 可从前端“新建顾客”页面真实创建顾客并写入数据库
  2. 未上传头像时,数据库保存默认头像 URL
  3. 顾客列表支持真实分页与筛选
  4. 顶部四个统计值来自真实数据库
  5. “搜索结果 X 件”显示当前筛选后的真实总数
  6. 全链路不使用 mock 成功结果

15. 风险与约束

15.1 前端现有列表筛选逻辑

当前前端 CustomerListPage 里仍有部分本地过滤逻辑。

本次实现时需要收敛为:

  • 优先由后端执行筛选
  • 前端不再依赖本地 mock 语义推导过滤结果

15.2 枚举展示文案问题

当前前端页面使用的是中文 / 日文展示文案。

本次实现时必须保证:

  • 数据库存规范值
  • 前端显示文案通过映射层转换

不能把多语言文案直接写入数据库作为业务值。

15.3 详情页延后

本次不得为了“顺手支持详情页”而扩展实现范围。

即使 getById() 和 update() 当前接口已存在占位,也不在本次 case 中完成。

16. 最终结论

本次 case 的完成定义为:

  1. 新增 customer 主表与 Prisma model
  2. 打通顾客新建
  3. 打通顾客分页列表与筛选
  4. 打通顾客 summary 统计
  5. 前端只做必要适配
  6. 顾客详情页留给下一个 case