patient-picker-rail.tsx
33.9 KB
-
feat(plan): 医生筛选做成可搜索输入框(方案 A:前端模糊 + 后端等值) · ab0d4595
业务要「上次医生 / 偏好医生」筛选。医生是开集(单宿主几百个),chip 平铺放不下。
⭐ 核心取舍:**模糊只发生在前端**。 输入框在已加载的医生名单里做子串匹配,选中后发**精确姓名** → 后端仍是等值查询, 吃得到偏索引。若改成后端 ILIKE '%x%',这三条索引立刻失效、退回全分区扫 —— 就是 2026-07-29 那个「圈人 20 秒」。要真·后端模糊得先上 pg_trgm + GIN, 并确认托管 PG 允许装扩展(见迁移注释)。 附带好处:客服不用记全名、不会因打错字得到空结果还以为"真没人"。 改动: - PersonaTagFilterDim 加两个字段: · `id?` —— 筛选串 `id:value` 的维度标识。**同一特征出多个可筛字段时必填**: visit_recency 一个特征出 bucket / lastDoctor / preferredDoctor 三个,只靠 key 分不开。 不填回落 key,存量维度零影响。新增 personaTagDimId() 统一取值。 · `dynamic?` —— 取值是开集,跳过闭集校验(维度 id 仍校验,取值走参数化 equals,无注入面)。 - plan.service 的筛选组装改为**按维度 id 分组**(原按特征 key 分组,三个 visit_recency 维度会被串成一条 OR,筛出错的人)。 - 新增 GET /pac/v1/plans/doctors:本 scope 医生名单(visit_recency 的 lastDoctor ∪ preferredDoctor 去重),Redis 缓存 6h —— 医生变动以月计,不该每次开面板都跑 DISTINCT 聚合。⚠ ️ 路由声明在 @Get(':id') **之前**,否则 'doctors' 会被当成 planId。 - 前端 DoctorPicker:已选可点掉;输入为空**不铺全量名单**(几百个会把面板撑爆); 名单拉取失败则输入框仍可用,只是没候选。 - 迁移 20260729080000 补两条医生索引(与 bucket 同形态:btree(表达式, persona_id), 第二列让内层 EXISTS 走 index-only scan)。 测试:persona-spec-drift 补两条 —— · 开集维度 options 必须**空**(留样例值会让前端误以为是闭集,把没列出的医生筛没了); ·⭐ 维度 id 必须唯一(重复 id 会指向错的 dataPath,筛出错的人)。 655 tests / 42 suites 全过;types / service / web typecheck 通过; 本地 GET /plans/doctors 实测 200。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>luoqi committed