Kaynağa Gözat

docs: rewrite customer module design around db schema

ChenYu 4 ay önce
ebeveyn
işleme
20aced8c5d

+ 275 - 347
docs/superpowers/specs/2026-06-28-customer-module-create-list-summary-design.md

@@ -1,257 +1,240 @@
-# 顾客模块设计(新建 / 列表 / 统计)
+# 顾客模块重做设计(以数据库设计为准)
 
 ## 1. 背景
 
-当前项目已经具备:
+当前顾客模块已经做过一轮实现,但起点选错了:
 
-- 发型师登录闭环
-- 最小工作台入口
-- GCS 文件上传能力
+- 后端 Prisma / migration 使用了另一套顾客字段模型
+- 前端表单和列表也围绕这套错误字段模型做了适配
+- 这套模型与用户明确指定的 [数据库设计.md](file:///Users/yuchen/Documents/hairstylist/backend/docs/数据库设计.md#L16-L54) 不一致
 
-顾客模块目前在前端已经存在:
+用户已经明确要求:
 
-- 新建顾客页面
-- 顾客列表页面
-- 顾客统计卡片
+- 顾客模块必须以 `backend/docs/数据库设计.md` 中 `customer` 表定义为唯一事实来源
+- 当前偏离这份设计的实现,需要整体回到这份 SQL 上重做
+- 开发完成后,旧文档内容也要被新内容替换,避免继续误导后续开发
 
-但后端 `customers` 模块仍是占位实现,数据库也缺少真正承载顾客档案主数据的 `customer` 主表。
-
-本次 case 的目标是先完成顾客模块的最小业务闭环:
+本次设计用于重做顾客模块最小闭环:
 
 - 新建顾客
-- 顾客分页列表 + 条件查询
+- 顾客分页列表与筛选
 - 顾客统计接口
 
-顾客详情页明确留到下一个 case。
+顾客详情页仍然是下一个 case,本次不实现。
 
 ## 2. 范围
 
 ### 本次实现
 
-1. 新建顾客并写入数据库
-2. 头像可选;未上传时使用默认头像 URL
-3. 顾客列表分页查询
-4. 顾客列表条件筛选
-5. 顾客顶部统计接口
-6. 前端顾客新建页与列表页对接真实后端接口
+1. 按 [数据库设计.md](file:///Users/yuchen/Documents/hairstylist/backend/docs/数据库设计.md#L16-L54) 重建 `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. 顾客 AI 模拟详情页聚合
-6. 顾客历史记录接口
+3. 顾客历史记录接口
+4. 提醒发送接口
+5. 施术记录聚合页
+6. AI 模拟详情聚合页
 
 ## 3. 设计原则
 
-### 3.1 数据对齐策略
-
-本次采用:
-
-`以现有数据库设计为准,前端做适配`
-
-解释:
-
-- 数据库结构优先贴近现有 SQL 文档与既有业务表关系
-- 前端当前表单字段不直接决定数据库列名
-- 后端负责做字段名映射与值规范化
-
-### 3.2 头像策略
-
-- 顾客头像不是必填
-- 前端上传头像时,复用已完成的存储能力
-- 上传 `type` 固定使用:`avatar`
-- 若前端未上传头像,则后端在创建顾客时自动写入默认头像 URL:
-
-`https://storage.googleapis.com/aitori-public/style/sample/066c7ba22ea149268444bc2d919d5b97.png`
-
-### 3.3 分页策略
-
-- 顾客列表采用页码分页
-- 查询参数使用:`page` + `pageSize`
+### 3.1 唯一事实来源
 
-### 3.4 查询语义
+本次顾客模块的结构定义,唯一以 [数据库设计.md](file:///Users/yuchen/Documents/hairstylist/backend/docs/数据库设计.md#L16-L54) 为准。
 
-- 搜索词:模糊查询
-- 到店状态:日期范围查询
-- 跟进:按业务枚举查询
-- 发型:按发长精确匹配
+这意味着:
 
-### 3.5 统计语义
+- Prisma model 要贴近这份表结构
+- migration 要贴近这份表结构
+- DTO 和 service 以这份字段语义为中心
+- 前端做适配,不反向驱动数据库列设计
 
-- `今天到店`:按计划到店日判断,即 `nextVisit`
-- `需跟进`:按 `followStatus` 判断
+### 3.2 旧错误模型不继续兼容
 
-## 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)`
+### 3.3 头像策略
 
-可选唯一约束:
+- 顾客头像不是必填
+- 前端上传头像时,继续复用已完成的存储能力
+- 上传 `type` 固定为 `avatar`
+- 若前端未上传头像,后端自动写入默认头像 URL
 
-- `(hairstylist_id, phone, is_deleted)`
+默认头像 URL:
 
-说明:
+`https://storage.googleapis.com/aitori-public/style/sample/066c7ba22ea149268444bc2d919d5b97.png`
 
-- 若允许同一个发型师名下同手机号只存在一条未删除顾客记录,则增加唯一约束
-- 若当前业务暂不确认,先不加唯一约束,避免阻塞本次实现
+### 3.4 分页策略
 
-本次默认先不强制该唯一约束,避免因历史脏数据或录入场景不明确导致创建失败。
+- 顾客列表采用 `page + pageSize`
+- 即使只有 1 页,前端也保留分页控件显示
+- `total` 表示当前筛选条件下的真实总数
 
-## 5. 值规范化设计
+## 4. 数据模型
 
-后端负责把前端表单值规范化后再入库。
+### 4.1 `customer` 表字段
 
-### 5.1 发长
+本次实现必须与以下字段语义对齐:
+
+- `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`
 
-- `Short` -> `SHORT`
-- `Medium` -> `MEDIUM`
-- `Long` -> `LONG`
+### 4.2 与关联表的关系
 
-### 5.2 联系方式
+当前数据库设计文档中的 `customer` 表没有 `hairstylist_id` 字段,但业务约束要求所有顾客数据按 JWT 中的发型师隔离。
 
-- `LINE` -> `LINE`
-- `Phone` -> `PHONE`
-- `Email` -> `EMAIL`
+因此本次需要做一个明确落地:
 
-### 5.3 LINE 关联状态
+- **在实现层补充 `hairstylist_id` 字段到真实表结构**
 
-- `已关联` / `已連携` -> `LINKED`
-- `未关联` / `未連携` -> `UNLINKED`
-- `邀请已发送` -> `INVITED`
-- `保留` -> `KEEP`
+原因:
 
-### 5.4 跟进状态
+- 没有 `hairstylist_id`,无法满足“所有顾客数据必须按当前发型师隔离”的硬约束
+- 仅靠手机号或外部表无法可靠完成创建、列表和统计的隔离
+- 当前项目的鉴权与其他业务表都已经围绕 `hairstylist_id` 做隔离
 
-本次采用规范枚举存储:
+因此最终真实落地的 `customer` 表应视为:
 
-- `NONE`
-- `NEED_FOLLOW_UP`
-- `SENT`
+- 以 [数据库设计.md](file:///Users/yuchen/Documents/hairstylist/backend/docs/数据库设计.md#L16-L54) 为主体
+- 补入 `hairstylist_id` 这一业务必需字段
 
-若前端传入的是展示文案,后端负责映射成上述规范值。
+这是本次唯一允许的结构性补充,其余字段语义不另起新模型。
 
-## 6. 前后端字段映射
+### 4.3 枚举建议
 
-前端字段与数据库字段映射如下:
+建议 Prisma 侧按以下枚举建模:
 
-- `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`
+- `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. 前后端字段映射
 
-- 前端提交结构尽量保持现状
-- 后端 DTO / mapper 负责做字段映射与值规范化
-- 返回给前端时再映射回前端当前约定字段
+### 5.1 新建页表单到数据库字段
 
-## 7. API 设计
+前端字段与数据库字段重新对齐如下:
 
-### 7.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`
 
-请求体:
+请求体以真实业务字段为中心,建议形态:
 
 ```ts
 type CreateCustomerRequest = {
   name: string
+  namePronunciation?: 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
+  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
 }
 ```
 
@@ -259,17 +242,11 @@ type CreateCustomerRequest = {
 
 - `name` 必填
 - `phone` 必填
-- `image` 非必填
-- 若 `image` 未传,则后端自动写入默认头像 URL
-- 头像上传由前端先调用存储上传接口完成,再将上传结果 URL 传给顾客接口
-
-返回:
-
-- 返回创建后的顾客对象
-- `id` 对前端按字符串返回
-- 日期字段统一返回 `YYYY-MM-DD`
+- `avatar` 非必填
+- 若 `avatar` 未传,则后端自动写默认头像 URL
+- `hairstylist_id` 不从前端传,后端从 JWT 中写入
 
-### 7.2 顾客分页列表
+### 6.2 顾客分页列表
 
 `GET /api/v1/customers`
 
@@ -280,9 +257,9 @@ 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'
+  visitStatus?: 'ALL' | 'TODAY' | 'THIS_WEEK'
+  followStatus?: 'ALL' | 'NEED_FOLLOW_UP' | 'NO_NEED_FOLLOW_UP'
+  hairLength?: 'ALL' | 'SHORT' | 'MEDIUM' | 'LONG' | 'UNCONFIRM'
 }
 ```
 
@@ -297,13 +274,7 @@ type ListCustomersResponse = {
 }
 ```
 
-约束:
-
-- `total` 表示当前筛选条件下的总条数
-- 用于前端展示“搜索结果 X 件”
-- `items` 只返回列表页面当前真正需要的字段
-
-### 7.3 顾客统计
+### 6.3 顾客统计
 
 `GET /api/v1/customers/summary`
 
@@ -318,239 +289,196 @@ type CustomerSummaryResponse = {
 }
 ```
 
-说明:
-
-- 统计接口归属顾客域内,不继续使用 `/stylist/dashboard/customer-summary`
-- 前端后续改为调用顾客域内接口
-
-## 8. 列表项设计
+## 7. 查询规则
 
-本次列表项只返回当前前端真正用到的字段,不提前把详情页字段全部塞入列表返回。
-
-建议列表项字段:
-
-- `id`
-- `name`
-- `phone`
-- `image`
-- `lastVisit`
-- `hairLength`
-- `concern`
-- `followStatus`
-
-## 9. 查询规则
-
-### 9.1 搜索词 `q`
+### 7.1 搜索词 `q`
 
 模糊查询以下字段:
 
 - `name`
 - `phone`
-- `concern`
-- `private_note`
-
-### 9.2 到店状态 `visitStatus`
-
-#### `ALL`
-
-不加筛选条件
-
-#### `TODAY`
-
-`next_visit_date = 今天`
-
-#### `THIS_WEEK`
+- `inner_remark`
+- `trouble`
+- `consult_content`
 
-`next_visit_date` 落在本周范围内
+### 7.2 到店状态 `visitStatus`
 
-#### `OVER_30_DAYS_NO_VISIT`
+按 `plan_date` 判断:
 
-`last_visit_date < 今天 - 30天`
+- `TODAY`: `plan_date = 今天`
+- `THIS_WEEK`: `plan_date` 落在本周
 
-### 9.3 跟进状态 `followStatus`
+这里明确结论:
 
-#### `ALL`
+- 当前 SQL 设计不支持“30日以上未到店”的精确含义
+- 本次必须去掉旧模型里依赖 `last_visit_date` 的实现
+- 前端筛选项需要同步收口,不能保留一个数据库无法正确支持的条件
 
-不加筛选条件
+### 7.3 跟进状态 `followStatus`
 
-#### `NEED_FOLLOW_UP`
+当前 SQL 设计没有 `follow_status` 枚举列,因此本次改为业务推导:
 
-`follow_status = NEED_FOLLOW_UP`
+- `NEED_FOLLOW_UP`: `next_follow_date` 非空,且日期小于等于今天
+- `NO_NEED_FOLLOW_UP`: 其余情况
 
-#### `SENT`
+这也意味着:
 
-`follow_status = SENT`
+- 列表中的“状态”列不再读数据库枚举
+- 前端展示文案由后端或前端映射层根据日期推导结果展示
 
-### 9.4 发长 `hairLength`
-
-#### `ALL`
-
-不加筛选条件
-
-#### `SHORT | MEDIUM | LONG`
+### 7.4 发长 `hairLength`
 
 精确匹配 `hair_length`
 
-## 10. 统计规则
+## 8. 统计规则
 
-所有统计均只统计:
+### 8.1 顾客总数
 
-- 当前登录发型师名下数据
+- 当前登录发型师名下
 - `is_deleted = 0`
 
-### 10.1 顾客总数
-
-统计当前发型师名下未删除顾客总数
-
-### 10.2 今天到店
-
-统计:
+### 8.2 今天到店
 
-- `next_visit_date = 今天`
+- `plan_date = 今天`
 
-### 10.3 需跟进
+### 8.3 需跟进
 
-统计:
+- `next_follow_date` 非空
+- `next_follow_date <= 今天`
 
-- `follow_status = NEED_FOLLOW_UP`
+### 8.4 已模拟
 
-### 10.4 已模拟
-
-统计规则:
-
-- 在 `ai_hair_simulation` 中存在记录
-- `customer_id` 非空
+- `ai_hair_simulation.customer_id` 非空
 - 按 `customer_id` 去重
 - 仅统计当前发型师名下顾客
 
-实现建议:
-
-- 以 `customer` 为主表
-- 关联 `ai_hair_simulation`
-- 对顾客维度去重计数
+## 9. 列表项返回字段
 
-避免把其他发型师的数据混入当前统计结果。
+列表页只返回当前页面真正需要的字段:
 
-## 11. 鉴权与数据隔离
+- `id`
+- `name`
+- `phone`
+- `avatar`
+- `planDate`
+- `trouble`
+- `hairLength`
+- `followState`
 
-本次所有顾客接口都必须按当前登录发型师进行数据隔离。
+其中:
 
-包括:
+- `followState` 是接口返回的展示语义字段,不要求数据库存在同名列
 
-- 新建顾客
-- 顾客列表
-- 顾客统计
+## 10. 鉴权与数据隔离
 
-后端从 JWT 中读取当前用户 `sub`,作为 `hairstylist_id`。
+所有顾客接口必须按 JWT 中的 `hairstylist_id` 隔离。
 
 规则:
 
 - 创建时写入 `hairstylist_id`
-- 查询时只能查当前发型师自己的顾客
-- 统计时只能统计当前发型师自己的顾客
+- 列表时按 `hairstylist_id` 过滤
+- 统计时按 `hairstylist_id` 过滤
 
-## 12. 后端模块边界
+## 11. 实施边界
 
-本次 `CustomersService` 只实现:
+### 11.1 后端
 
-- `create()`
-- `list()`
-- `getSummary()`
+本次重做:
 
-本次不实现:
+- Prisma schema 中 `Customer` model
+- 对应 migration
+- DTO
+- mapper
+- `CustomersService.create()`
+- `CustomersService.list()`
+- `CustomersService.getSummary()`
+
+本次不做:
 
 - `getById()`
 - `update()`
-- `records / reminders / detail 聚合`
+- 历史记录 / 提醒 / 详情聚合
 
-说明:
+### 11.2 前端
 
-- 顾客详情页是下一个 case
-- 本次只覆盖当前前端真正依赖的最小闭环
+本次重做:
 
-## 13. 前端改动边界
+- 顾客新建页字段承接
+- 顾客表单到 API payload 的映射
+- 顾客列表展示字段
+- 顾客列表筛选项语义
+- 顾客状态列显示
+- 顾客分页展示
 
-本次前端只做必要适配:
+本次不做:
 
-1. 顾客头像上传时,调用存储上传接口并使用 `type=avatar`
-2. 顾客列表接口补充:
-   - `page`
-   - `pageSize`
-   - `visitStatus`
-   - `followStatus`
-   - `hairLength`
-3. 顶部统计接口改为调用 `/customers/summary`
-4. 分页结构对接真实 `total`
+- 顾客详情页
+- 顾客编辑页
 
-本次不强做复杂分页交互设计,只保留当前页面所需的最小可用能力。
+## 12. 文档策略
 
-## 14. 测试设计
+开发完成后,旧文档必须同步替换,避免误导:
 
-### 14.1 单元测试
+- `backend/docs/数据库设计.md` 中与顾客模块相关的内容,要更新为最终真实落地版本
+- 不保留旧错误模型的描述
+- 不保留“曾经试过的另一套字段名”作为参考内容
 
-至少覆盖:
+文档目标是:
 
-1. 新建顾客时未传头像,自动写默认头像 URL
-2. 新建顾客时写入当前 `hairstylist_id`
-3. 列表筛选参数可正确转换为查询条件
-4. 统计逻辑符合定义
+- 新人只看文档就能理解真实实现
+- 文档与 Prisma / migration / API / 前端字段语义保持一致
 
-### 14.2 e2e 测试
+## 13. 测试设计
 
-至少覆盖:
+### 13.1 后端单测
 
-1. 未登录访问顾客接口返回 `401`
-2. 新建顾客成功
-3. 顾客列表分页成功
-4. 顾客列表筛选成功
-5. 顾客统计接口成功
-
-### 14.3 联调验收
-
-完成标准:
+至少覆盖:
 
-1. 可从前端“新建顾客”页面真实创建顾客并写入数据库
-2. 未上传头像时,数据库保存默认头像 URL
-3. 顾客列表支持真实分页与筛选
-4. 顶部四个统计值来自真实数据库
-5. “搜索结果 X 件”显示当前筛选后的真实总数
-6. 全链路不使用 mock 成功结果
+1. 未传头像时自动写默认头像
+2. 创建时写入当前 `hairstylist_id`
+3. 查询条件按 `plan_date / next_follow_date / hair_length` 正确构造
+4. 统计逻辑按新口径成立
 
-## 15. 风险与约束
+### 13.2 后端 e2e
 
-### 15.1 前端现有列表筛选逻辑
+至少覆盖:
 
-当前前端 `CustomerListPage` 里仍有部分本地过滤逻辑。
+1. 未登录访问返回 `401`
+2. 创建顾客成功
+3. 分页列表成功
+4. 列表筛选成功
+5. summary 成功
 
-本次实现时需要收敛为:
+### 13.3 前端测试
 
-- 优先由后端执行筛选
-- 前端不再依赖本地 mock 语义推导过滤结果
+至少覆盖:
 
-### 15.2 枚举展示文案问题
+1. 新建页 payload 字段映射符合新模型
+2. 列表状态列显示业务推导结果
+3. 分页控件始终显示
+4. 已移除不再受支持的旧筛选 / 旧字段依赖
 
-当前前端页面使用的是中文 / 日文展示文案。
+## 14. 风险与约束
 
-本次实现时必须保证:
+### 14.1 当前前端表单与 SQL 不完全同构
 
-- 数据库存规范值
-- 前端显示文案通过映射层转换
+需要明确删减旧字段,并补上新字段,不做“看起来能填,实际硬映射”的假适配。
 
-不能把多语言文案直接写入数据库作为业务值。
+### 14.2 当前筛选项存在旧模型残留
 
-### 15.3 详情页延后
+`30日以上未到店` 依赖 `last_visit_date`,而目标 SQL 中没有这个字段。本次必须同步调整前端筛选项和后端查询语义,不能保留错误筛选。
 
-本次不得为了“顺手支持详情页”而扩展实现范围。
+### 14.3 当前仓库里已有一轮错误起点实现
 
-即使 `getById()` 和 `update()` 当前接口已存在占位,也不在本次 case 中完成。
+实施时要以“替换错误模型”为目标,不做双轨兼容。
 
-## 16. 最终结论
+## 15. 最终结论
 
-本次 case 的完成定义为:
+本次 case 的正确完成定义为:
 
-1. 新增 `customer` 主表与 Prisma model
-2. 打通顾客新建
-3. 打通顾客分页列表与筛选
-4. 打通顾客 summary 统计
-5. 前端只做必要适配
-6. 顾客详情页留给下一个 case
+1. `customer` 相关实现整体回到 [数据库设计.md](file:///Users/yuchen/Documents/hairstylist/backend/docs/数据库设计.md#L16-L54) 的字段语义
+2. 唯一额外补充 `hairstylist_id` 用于数据隔离
+3. 新建、列表、统计三条链路全部改为新模型
+4. 前端同步做真实字段适配
+5. 旧错误字段模型不再保留
+6. 开发完成后同步替换旧文档内容,避免误导后续开发