当前项目已经具备:
顾客模块目前在前端已经存在:
但后端 customers 模块仍是占位实现,数据库也缺少真正承载顾客档案主数据的 customer 主表。
本次 case 的目标是先完成顾客模块的最小业务闭环:
顾客详情页明确留到下一个 case。
本次采用:
以现有数据库设计为准,前端做适配
解释:
type 固定使用:avatarhttps://storage.googleapis.com/aitori-public/style/sample/066c7ba22ea149268444bc2d919d5b97.png
page + pageSize今天到店:按计划到店日判断,即 nextVisit需跟进:按 followStatus 判断本次新增 customer 主表,作为顾客档案主数据的真实来源。
该表用于承接:
并作为以下表的逻辑上游主表:
customer_treatment_record.customer_idai_hair_simulation.customer_id本次不强制要求一次性补完数据库外键约束,但 Prisma 与业务语义上必须明确该主从关系。
建议字段如下:
idhairstylist_idnamekanaphoneemailbirthdayavatar_urlcontact_typeline_statuslast_visit_datenext_visit_datevisit_cyclehair_lengthhair_typehair_conditionconcerndesired_imageformula_noteallergy_noteprivate_notefollow_statusfollow_timingfollow_messagecreate_timeupdate_timeis_deleted本次 migration 只新增 customer 表及必要索引,不修改既有旧表结构。
建议索引:
idx_hairstylist_ididx_phoneidx_next_visit_dateidx_follow_statusidx_hair_lengthidx_is_deleted(hairstylist_id, is_deleted)可选唯一约束:
(hairstylist_id, phone, is_deleted)说明:
本次默认先不强制该唯一约束,避免因历史脏数据或录入场景不明确导致创建失败。
后端负责把前端表单值规范化后再入库。
Short -> SHORTMedium -> MEDIUMLong -> LONGLINE -> LINEPhone -> PHONEEmail -> EMAIL已关联 / 已連携 -> LINKED未关联 / 未連携 -> UNLINKED邀请已发送 -> INVITED保留 -> KEEP本次采用规范枚举存储:
NONENEED_FOLLOW_UPSENT若前端传入的是展示文案,后端负责映射成上述规范值。
前端字段与数据库字段映射如下:
image -> avatar_urlcontact -> contact_typelineStatus -> line_statuslastVisit -> last_visit_datenextVisit -> next_visit_datehairLength -> hair_lengthhairType -> hair_typehairCondition -> hair_conditiondesiredImage -> desired_imageformulaNote -> formula_noteallergyNote -> allergy_noteprivateNote -> private_notefollowStatus -> follow_statusfollowTiming -> follow_timingfollowMessage -> follow_message说明:
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返回:
id 对前端按字符串返回YYYY-MM-DDGET /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 表示当前筛选条件下的总条数items 只返回列表页面当前真正需要的字段GET /api/v1/customers/summary
返回:
type CustomerSummaryResponse = {
total: number
today: number
followUp: number
simulated: number
}
说明:
/stylist/dashboard/customer-summary本次列表项只返回当前前端真正用到的字段,不提前把详情页字段全部塞入列表返回。
建议列表项字段:
idnamephoneimagelastVisithairLengthconcernfollowStatusq模糊查询以下字段:
namephoneconcernprivate_notevisitStatusALL不加筛选条件
TODAYnext_visit_date = 今天
THIS_WEEKnext_visit_date 落在本周范围内
OVER_30_DAYS_NO_VISITlast_visit_date < 今天 - 30天
followStatusALL不加筛选条件
NEED_FOLLOW_UPfollow_status = NEED_FOLLOW_UP
SENTfollow_status = SENT
hairLengthALL不加筛选条件
SHORT | MEDIUM | LONG精确匹配 hair_length
所有统计均只统计:
is_deleted = 0统计当前发型师名下未删除顾客总数
统计:
next_visit_date = 今天统计:
follow_status = NEED_FOLLOW_UP统计规则:
ai_hair_simulation 中存在记录customer_id 非空customer_id 去重实现建议:
customer 为主表ai_hair_simulation避免把其他发型师的数据混入当前统计结果。
本次所有顾客接口都必须按当前登录发型师进行数据隔离。
包括:
后端从 JWT 中读取当前用户 sub,作为 hairstylist_id。
规则:
hairstylist_id本次 CustomersService 只实现:
create()list()getSummary()本次不实现:
getById()update()records / reminders / detail 聚合说明:
本次前端只做必要适配:
type=avatarpagepageSizevisitStatusfollowStatushairLength/customers/summarytotal本次不强做复杂分页交互设计,只保留当前页面所需的最小可用能力。
至少覆盖:
hairstylist_id至少覆盖:
401完成标准:
当前前端 CustomerListPage 里仍有部分本地过滤逻辑。
本次实现时需要收敛为:
当前前端页面使用的是中文 / 日文展示文案。
本次实现时必须保证:
不能把多语言文案直接写入数据库作为业务值。
本次不得为了“顺手支持详情页”而扩展实现范围。
即使 getById() 和 update() 当前接口已存在占位,也不在本次 case 中完成。
本次 case 的完成定义为:
customer 主表与 Prisma model