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

顾客模块重做设计(以数据库设计为准)

1. 背景

当前顾客模块已经做过一轮实现,但起点选错了:

  • 后端 Prisma / migration 使用了另一套顾客字段模型
  • 前端表单和列表也围绕这套错误字段模型做了适配
  • 这套模型与用户明确指定的 数据库设计.md 不一致

用户已经明确要求:

  • 顾客模块必须以 backend/docs/数据库设计.md 中 customer 表定义为唯一事实来源
  • 当前偏离这份设计的实现,需要整体回到这份 SQL 上重做
  • 开发完成后,旧文档内容也要被新内容替换,避免继续误导后续开发

本次设计用于重做顾客模块最小闭环:

  • 新建顾客
  • 顾客分页列表与筛选
  • 顾客统计接口

顾客详情页仍然是下一个 case,本次不实现。

2. 范围

本次实现

  1. 按 数据库设计.md 重建 customer 表对应的 Prisma 模型与 migration
  2. 重做 POST /api/v1/customers
  3. 重做 GET /api/v1/customers
  4. 重做 GET /api/v1/customers/summary
  5. 前端新建页、列表页、统计卡片按真实字段重新适配
  6. 保留头像上传能力,上传 type 固定为 avatar
  7. 开发完成后,用新实现对应的内容替换旧的顾客文档描述

本次不实现

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

3. 设计原则

3.1 唯一事实来源

本次顾客模块的结构定义,唯一以 数据库设计.md 为准。

这意味着:

  • Prisma model 要贴近这份表结构
  • migration 要贴近这份表结构
  • DTO 和 service 以这份字段语义为中心
  • 前端做适配,不反向驱动数据库列设计

3.2 旧错误模型不继续兼容

当前仓库里已经存在一套错误起点的字段模型,例如:

  • kana
  • avatar_url
  • contact_type
  • line_status
  • last_visit_date
  • next_visit_date
  • visit_cycle
  • hair_type
  • hair_condition
  • concern
  • desired_image
  • formula_note
  • private_note
  • follow_status
  • follow_timing
  • follow_message

这些字段不再作为顾客主模型继续保留。

处理原则:

  • 如果能映射到新表语义,就在接口层做一次性迁移映射
  • 如果无法映射,则直接删除旧设计残留,不做双模型并存

3.3 头像策略

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

默认头像 URL:

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

3.4 分页策略

  • 顾客列表采用 page + pageSize
  • 即使只有 1 页,前端也保留分页控件显示
  • total 表示当前筛选条件下的真实总数

4. 数据模型

4.1 customer 表字段

本次实现必须与以下字段语义对齐:

  • id
  • name
  • name_pronunciation
  • phone
  • email
  • birthday
  • avatar
  • expect_contact
  • hair_length
  • hair_texture
  • notice
  • allergy
  • inner_remark
  • next_follow_date
  • recommend_visit_date
  • first_suggest_hair
  • plan_date
  • is_first_register
  • visit_purpose
  • trouble
  • consult_content
  • expect_look
  • create_time
  • update_time
  • is_deleted

4.2 与关联表的关系

当前数据库设计文档中的 customer 表没有 hairstylist_id 字段,但业务约束要求所有顾客数据按 JWT 中的发型师隔离。

因此本次需要做一个明确落地:

  • 在实现层补充 hairstylist_id 字段到真实表结构

原因:

  • 没有 hairstylist_id,无法满足“所有顾客数据必须按当前发型师隔离”的硬约束
  • 仅靠手机号或外部表无法可靠完成创建、列表和统计的隔离
  • 当前项目的鉴权与其他业务表都已经围绕 hairstylist_id 做隔离

因此最终真实落地的 customer 表应视为:

  • 以 数据库设计.md 为主体
  • 补入 hairstylist_id 这一业务必需字段

这是本次唯一允许的结构性补充,其余字段语义不另起新模型。

4.3 枚举建议

建议 Prisma 侧按以下枚举建模:

  • expect_contact: LINE | PHONE | EMAIL | NONE
  • hair_length: SHORT | MEDIUM | LONG | UNCONFIRM
  • hair_texture: UNCONFIRM | THIN | NORMAL | HARD_CRUDE | CURLY
  • first_suggest_hair: AUTO | LONG | MEDIUM | SHORT
  • visit_purpose: CUT_DYE | CUT | DYE | CONSULT_SIM

5. 前后端字段映射

5.1 新建页表单到数据库字段

前端字段与数据库字段重新对齐如下:

  • name -> name
  • kana -> name_pronunciation
  • phone -> phone
  • email -> email
  • birthday -> birthday
  • image -> avatar
  • contact -> expect_contact
  • hairLength -> hair_length
  • hairType -> hair_texture
  • formulaNote / hairCondition -> 不再保留旧字段语义
  • allergyNote -> allergy
  • privateNote -> inner_remark
  • followTiming -> next_follow_date
  • nextVisit -> recommend_visit_date
  • followStatus -> 不再直存数据库枚举,改由日期业务推导
  • visitCycle -> 不再作为独立列,按需要映射到 visit_purpose
  • lastVisit -> 本次不作为主字段落库
  • concern -> trouble
  • desiredImage -> expect_look
  • followMessage -> consult_content 或 notice 不合适,故本次应从前端表单中移除旧语义字段
  • lineStatus -> 本次没有对应列,应从当前顾客主表单移除
  • notice 新增独立承接“发型护理注意事项”
  • planDate 对应 plan_date
  • isFirstRegister 对应 is_first_register
  • visitPurpose 对应 visit_purpose

5.2 关键调整

本次不是简单改字段名,而是把前端表单也重新收口到真实业务语义:

  • 删掉没有数据库落点的旧字段
  • 补上数据库真实字段但前端当前未明确承接的字段
  • 避免继续出现“前端名字看似能填,数据库其实存的是另一回事”

6. API 设计

6.1 新建顾客

POST /api/v1/customers

请求体以真实业务字段为中心,建议形态:

type CreateCustomerRequest = {
  name: string
  namePronunciation?: string
  phone: string
  email?: string
  birthday?: string
  avatar?: string
  expectContact?: 'LINE' | 'PHONE' | 'EMAIL' | 'NONE'
  hairLength?: 'SHORT' | 'MEDIUM' | 'LONG' | 'UNCONFIRM'
  hairTexture?: 'UNCONFIRM' | 'THIN' | 'NORMAL' | 'HARD_CRUDE' | 'CURLY'
  notice?: string
  allergy?: string
  innerRemark?: string
  nextFollowDate?: string
  recommendVisitDate?: string
  firstSuggestHair?: 'AUTO' | 'LONG' | 'MEDIUM' | 'SHORT'
  planDate?: string
  isFirstRegister?: boolean
  visitPurpose?: 'CUT_DYE' | 'CUT' | 'DYE' | 'CONSULT_SIM'
  trouble?: string
  consultContent?: string
  expectLook?: string
}

规则:

  • name 必填
  • phone 必填
  • avatar 非必填
  • 若 avatar 未传,则后端自动写默认头像 URL
  • hairstylist_id 不从前端传,后端从 JWT 中写入

6.2 顾客分页列表

GET /api/v1/customers

查询参数:

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

返回:

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

6.3 顾客统计

GET /api/v1/customers/summary

返回:

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

7. 查询规则

7.1 搜索词 q

模糊查询以下字段:

  • name
  • phone
  • inner_remark
  • trouble
  • consult_content

7.2 到店状态 visitStatus

按 plan_date 判断:

  • TODAY: plan_date = 今天
  • THIS_WEEK: plan_date 落在本周

这里明确结论:

  • 当前 SQL 设计不支持“30日以上未到店”的精确含义
  • 本次必须去掉旧模型里依赖 last_visit_date 的实现
  • 前端筛选项需要同步收口,不能保留一个数据库无法正确支持的条件

7.3 跟进状态 followStatus

当前 SQL 设计没有 follow_status 枚举列,因此本次改为业务推导:

  • NEED_FOLLOW_UP: next_follow_date 非空,且日期小于等于今天
  • NO_NEED_FOLLOW_UP: 其余情况

这也意味着:

  • 列表中的“状态”列不再读数据库枚举
  • 前端展示文案由后端或前端映射层根据日期推导结果展示

7.4 发长 hairLength

精确匹配 hair_length

8. 统计规则

8.1 顾客总数

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

8.2 今天到店

  • plan_date = 今天

8.3 需跟进

  • next_follow_date 非空
  • next_follow_date <= 今天

8.4 已模拟

  • ai_hair_simulation.customer_id 非空
  • 按 customer_id 去重
  • 仅统计当前发型师名下顾客

9. 列表项返回字段

列表页只返回当前页面真正需要的字段:

  • id
  • name
  • phone
  • avatar
  • planDate
  • trouble
  • hairLength
  • followState

其中:

  • followState 是接口返回的展示语义字段,不要求数据库存在同名列

10. 鉴权与数据隔离

所有顾客接口必须按 JWT 中的 hairstylist_id 隔离。

规则:

  • 创建时写入 hairstylist_id
  • 列表时按 hairstylist_id 过滤
  • 统计时按 hairstylist_id 过滤

11. 实施边界

11.1 后端

本次重做:

  • Prisma schema 中 Customer model
  • 对应 migration
  • DTO
  • mapper
  • CustomersService.create()
  • CustomersService.list()
  • CustomersService.getSummary()

本次不做:

  • getById()
  • update()
  • 历史记录 / 提醒 / 详情聚合

11.2 前端

本次重做:

  • 顾客新建页字段承接
  • 顾客表单到 API payload 的映射
  • 顾客列表展示字段
  • 顾客列表筛选项语义
  • 顾客状态列显示
  • 顾客分页展示

本次不做:

  • 顾客详情页
  • 顾客编辑页

12. 文档策略

开发完成后,旧文档必须同步替换,避免误导:

  • backend/docs/数据库设计.md 中与顾客模块相关的内容,要更新为最终真实落地版本
  • 不保留旧错误模型的描述
  • 不保留“曾经试过的另一套字段名”作为参考内容

文档目标是:

  • 新人只看文档就能理解真实实现
  • 文档与 Prisma / migration / API / 前端字段语义保持一致

13. 测试设计

13.1 后端单测

至少覆盖:

  1. 未传头像时自动写默认头像
  2. 创建时写入当前 hairstylist_id
  3. 查询条件按 plan_date / next_follow_date / hair_length 正确构造
  4. 统计逻辑按新口径成立

13.2 后端 e2e

至少覆盖:

  1. 未登录访问返回 401
  2. 创建顾客成功
  3. 分页列表成功
  4. 列表筛选成功
  5. summary 成功

13.3 前端测试

至少覆盖:

  1. 新建页 payload 字段映射符合新模型
  2. 列表状态列显示业务推导结果
  3. 分页控件始终显示
  4. 已移除不再受支持的旧筛选 / 旧字段依赖

14. 风险与约束

14.1 当前前端表单与 SQL 不完全同构

需要明确删减旧字段,并补上新字段,不做“看起来能填,实际硬映射”的假适配。

14.2 当前筛选项存在旧模型残留

30日以上未到店 依赖 last_visit_date,而目标 SQL 中没有这个字段。本次必须同步调整前端筛选项和后端查询语义,不能保留错误筛选。

14.3 当前仓库里已有一轮错误起点实现

实施时要以“替换错误模型”为目标,不做双轨兼容。

15. 最终结论

本次 case 的正确完成定义为:

  1. customer 相关实现整体回到 数据库设计.md 的字段语义
  2. 唯一额外补充 hairstylist_id 用于数据隔离
  3. 新建、列表、统计三条链路全部改为新模型
  4. 前端同步做真实字段适配
  5. 旧错误字段模型不再保留
  6. 开发完成后同步替换旧文档内容,避免误导后续开发