给每个机构(门店)固定放一份"假的"儿童视光档案作示范样板 —— 名叫张思源,8岁男孩,6次检查记录,眼轴数据设计成"前期长得快、中间开始干预、后期明显变缓",用来直观证明防控方案有效。门店端和家长端都能看到这份档案,长得跟真档案一模一样,但完全只读:不能改、不能删、不能加新记录。所有正常按钮照常显示,只是点了会弹提示告诉用户"这是示范,去建真档案"。这样新机构/新用户一进系统就能看到"这套系统能测出什么、防控做出来是什么样子",不用等到有真实客户数据才有得看。
web/app/h5/family/[id]/exam/new/prevention.ts 真实枚举中挑选的无品牌选项,对应关系:L1 光疗→「其他Led红光」、L2 补光→「全光谱吸顶灯」、L3 远疗→「其他读写台」、L4 镜片→「离焦镜」。⚠️ 边界:prevention.ts 里的四组枚举本身(含品牌词那些选项)一律不改、不精简、不去品牌(2026-08-21 长官已定),这条拍板只限定示范档案数据不用 OK 镜/阿托品、改挑同一枚举文件里的无品牌选项,不影响真实门店端录入时可选的全量枚举(已拍板,勿再问)RefractiveRecord 表 ai_analysis_text/ai_analysis_status/ai_analysis_updated_at 三字段(app/models/refractive_record.py:148,151,154)。⚠️ ai_analysis_text 不是纯文本,是 JSON 字符串,结构见 app/services/refractive_record_service.py:2221-2229:{"kind":"4l_analysis","p1_diag":..,"p2_advice":..,"p3_warning":..,"plan_4l":{"l1":..,"l2":..,"l3":..,"l4":..},"plan_drugs_plus":..,"model_version":..},其中 plan_4l 四层每层 ≤80 字、真实生成逻辑规定缺任一层整份作废(refractive_record_service.py:2159-2213;JSON schema 出处 four_l_summary_prompt.py:989-1004)。ai_analysis_status 取值 pending/done/failed,示范档案 6 条预置数据要写 done。真实生成还有一道最低数据门槛(refractive_record_service.py:983-988):unaided_va_raw/subjective_sphere/subjective_cylinder/axial_length 四项双眼都要有值才会触发;本文档四节 6 行数据表这四项本就双眼填全,天然满足门槛(但示范档案预置数据是直接写死 done,不实际走一遍生成流程,门槛只是"顺带满足",非强制要求)。⚠️ ai_analysis_* 三字段严禁下发到家长端任何响应(app/api/parent_patients.py:709、app/api/refractive_records.py:309 注释明令),家长端拿不到这份文案,只有门店端能看(已拍板,勿再问)store_conclusion_text(草稿)/store_conclusion_pushed_text(推送快照)/store_conclusion_pushed_at(推送时间)(app/models/refractive_record.py:157,160,163)。推送动作 = 把草稿字段原样拷贝成快照字段 + 打时间戳(refractive_record_service.py:2575-2590),不发任何短信/微信/站内通知(已 grep 验证 push_conclusion 函数体 0 命中 sms/wechat/notify 类调用)。家长端只在 store_conclusion_pushed_at 非空时才会拿到这段文案(app/api/parent_patients.py:709-721 组装逻辑),渲染在检查记录详情弹层的 StoreConclusionCard.tsx 组件里,列表页对应按钮从「查看」变成绿色「门店解读」(ExamRecordCard.tsx:124-136),家长要点进详情弹层才看得到,不是打开 App 就自动弹出。结论字段上限 2000 字(app/schemas/refractive_record.py:289-294),渲染时按「【小标题】\n正文」分段解析、L1/L2/L3/L4 开头行加粗(StoreConclusionCard.tsx:15-77)。长官已定稿的推送文案(逐字照抄,详见本文档四节末尾)(已拍板,勿再问)family/page.tsx:233-246)照常显示,不因是示范档案就在列表层级隐藏;门店端患者列表行如有对应的操作入口同样显示(经核实门店端患者列表真实行本就无此入口,不涉及)。点击一律弹提示"示范档案不可编辑",不隐藏、不跳转编辑页。此条明确消除了此前口径7(交互方式:按钮不隐藏)在"详情页按钮级"与"列表卡片层级"之间的理解分歧(已拍板,勿再问)family/[id]/page.tsx(档案详情页)目前没有独立的"编辑档案"(编辑姓名/性别/出生日期等档案本身字段)入口——「编辑档案」pill 现网唯一来源是列表页 family/page.tsx:233-246,点击后跳转 /h5/family/new?pid={id} 表单页;详情页里现有的「编辑」按钮(family/[id]/page.tsx:2757)是检查记录级编辑入口(口径14/批5范围),与"编辑档案"是两个不同功能。若要落实本条"详情页内照常显示编辑档案入口",需要在详情页新增一个 UI 元素,不是现成已有的、只需加判断就完事的东西(实施细节见 devplan 批4)。此口径同时解决了 devplan §156/§211/§223 此前标注的"列表卡片层级 vs 口径7详情页层级"混淆疑问 —— 结论:两个层级不是同一回事,互不冲突(已拍板,勿再问)web/app/h5/family/[id]/page.tsx:2776-2884,接口 /api/parent/patients/{id}/records/{rid}):渲染全量字段,共 8 大组、30+ 项(视力数据/自镜度数/本次验光/基础测量/生物测量/眼部检查/视功能/备注)。parent-readonly-dialog.tsx,接口 /api/h5/refractive-records/records/{id}/parent-view):只展示 7 项白名单指标(裸眼视力/戴镜视力/矫正视力/眼轴/角膜曲率K1/角膜曲率K2/眼压),用于验光师在店内把平板递给家长看的临时只读快照。| 项 | 结论 | 出处 |
|---|---|---|
owner_seat_id 是单值列,不是数组/关联表,一个档案物理上只能绑一个家长 |
已核实 | app/models/flywheel.py:47-50 |
绑定/解绑逻辑(bind_patient_seat/unbind_patient_seat)也设计成"一次只能绑一个" |
已核实,证实单一归属是有意设计 | app/services/refractive_record_service.py:428,460-463,469,485 |
推论:示范档案不能靠"往 owner_seat_id 塞值"的方式给"所有家长"看到 —— 那样只能绑一个家长。必须在家长端列表接口(list_my_children)里动态拼接一条虚拟数据返回给前端,不落地为某个家长专属的真实绑定关系 |
未验证,需实施时确认具体实现方式(是拼接静态种子数据,还是查一条"标记为示范"的真实 DB 行再拼进结果) | 推论,非直接 grep 结论 |
现有代码里没有任何 is_demo/示范档案/demo_patient 相关字段或逻辑,是纯新功能,数据库没有现成挂钩点 |
已核实(0 命中) | 见附录 §5 |
⚠️ 命名避坑:仓库已有 OrgBrainConfig.is_demo_store 字段,但那是"体验演示门店组"(海报强制印「测试门店」文案),跟本次"示范档案"是完全不同的两个概念,不要复用/混淆这个字段 |
已核实 | app/models/database.py:106、app/services/store_asset_service.py:124-145 |
search_patients(db, org_id, ...) 的 org_id 是强制第二个位置参数,门店端搜索天然按机构隔离 |
已核实 | app/services/refractive_record_service.py:166-167 |
待决策(未拍板):示范档案是"每个机构一份真实 DB 行"还是"全局唯一一份、所有机构都能看到"?前者门店端搜索天然带出(因为按 org_id 过滤),后者需要在 search_patients 里加特判逻辑绕过 org_id 限制。这条不在长官已拍板 13 条里,建议实施前明确 |
未拍板,建议列入六节"待确认" | 基于上一条推论 |
改动量估计:如走"每机构一份真实 DB 行"方案 —— 需要一条种子数据 migration(1 张 patients 行 + 6 张 refractive_records 行 + 对应 refractive_record_eyes 行,按机构 onboarding 时机播种或全量脚本补种);如走"全局虚拟拼接"方案 —— 不落库,后端接口层现造 dict。两种方案改动量差异大,建议实施前先拍板走哪条。
| 改动点 | file:line | 说明 |
|---|---|---|
| 家长端"我的孩子"列表接口 —— 示范档案注入点 | app/api/parent_patients.py:217-233(GET /api/parent/patients,list_my_children) |
长官拍板"零档案家长也要看到"(项11)的物理落点;需在 svc.search_patients(...) 返回结果基础上拼一条示范数据(排在真实列表最后,零档案时单独展示) |
| 门店端搜索接口 —— 示范档案自然带出(若走"每机构一份真实行"方案) | app/api/refractive_records.py(未逐一定位具体 endpoint,复用 svc.search_patients) |
未验证,若走"全局唯一"方案则需要额外改动 |
| 家长端记录编辑门 —— 已有"记录级只读"机制可直接复用 | app/api/parent_patients.py:869-875(_record_editable_by_parent)、app/api/parent_patients.py:1058-1060(编辑接口 403 拦截点) |
重要发现:该函数按记录的 created_by_user_id 所在席位 seat_type 判断是否可编辑,跟 patient.owner_type 无关。只要示范档案的 6 条记录的 created_by_user_id 不指向一个 seat_type='parent' 的真实家长席位(比如设为 NULL 或指向门店/系统席位),家长端已有逻辑会自动判定 editable=false,不需要为示范档案额外写"编辑拦截"代码 —— 但这只覆盖"编辑已有记录",不覆盖"新增检查"(见下一行) |
| 家长端"新增检查"按钮 —— 现有逻辑不会自动拦截,需要新加判断 | web/app/h5/family/[id]/page.tsx:2422(按钮)、app/api/parent_patients.py:1062 起(create_my_child_record) |
create_my_child_record 端点本身没有"该档案是否示范"的门,家长理论上仍能对着示范档案 POST 新记录。这是需要新写的拦截逻辑,前端拦(拍板项7"点了弹提示")即可满足需求,但后端也建议加一道兜底校验,防止绕过前端直接调接口 |
| AI 分析预置展示 —— 复用现有字段,不新增字段 | app/models/refractive_record.py:148(ai_analysis_text,门店端可见)、:160(store_conclusion_pushed_text,家长端可见) |
拍板项8"预置结论"直接把示范数据的这两个字段提前写死内容即可,门店端接口 POST /records/{id}/analysis(app/api/refractive_records.py:311 起 trigger_analysis)"重新生成"按钮触发时应被拦截(弹提示),不应真的调用生成逻辑覆盖预置文案 |
| 门店端生成分析不扣秒读额度,示范档案本来也不需要担心额度问题 | app/api/refractive_records.py:308-309,316,347 |
已核实:trigger_analysis 本就"不消耗秒读额度";额度扣减只挂在家长端 /4l-report 路径且 seat_type=='parent'(:347 注释),跟本次改动无关 |
| 分享卡片机构名 —— 需改成"示范档案机构"(9-14 定稿,见口径18) | web/app/h5/family/[id]/page.tsx:523,742,1132,1440,1967 |
orgDisplayName 状态 + 4 处 Canvas 绘制取值 orgDisplayName || getOrgNameFromSeatToken() || "赵阳智脑",示范档案的分享卡片需要让这个取值链条最终产出「示范档案机构」而不是真实机构名,具体改法未定(可能是前端检测到当前孩子是示范档案时直接覆盖变量,也可能后端返回时就带一个特殊机构名字段) |
| 删除功能 —— 现网没有任何删除患者档案的接口,不存在"需要额外挡删除"的负担 | 已核实,delete_patient 0 命中;delete_record(仅限记录,非档案)在 app/api/refractive_records.py:235 存在,证明查法有效 |
拍板项6"不做删除"在门店/家长两端目前本来就没有对应入口,不用额外拦 |
改动量估计:后端改动集中在 parent_patients.py(注入逻辑 + 新增记录拦截)+ 种子数据(migration 或脚本),量级中等;refractive_records.py/ai_analysis 相关字段本身不用改结构,只是灌入固定文案。
门店端(平板)所有"动到数据"的按钮清单,每个都是拍板项7要拦截的候选(公共入口如"新建档案""搜索"不拦):
| 按钮 | file:line | 触发的动作/接口 |
|---|---|---|
| 「新建档案」(列表页顶部,公共功能,不拦) | frame1-child-list.tsx:69,74 |
打开 Frame2CreateModal(新建态) |
| 「修改档案」 | frame3-patient-profile.tsx:195,200 |
onOpenEditPatient → refractive-record-pane.tsx:894(复用 Frame2CreateModal 编辑态)→ :920 onSubmitEditPatient → :427,415,434 updatePatient(...)(PATCH /patients/{id}) |
| 「新增检查」 | frame3-patient-profile.tsx:204,209 |
onNewRecord → refractive-record-pane.tsx:344 注释"newRecord() 只前端构造草稿" → 首次保存时 :583 createRecord({...}) 落库 |
| 「解绑」(已绑定家长时显示) | frame3-patient-profile.tsx:228 |
onUnbind → 对应 unbind_patient_seat(refractive_record_service.py:469) |
| 「绑定」(未绑定时显示的绑定入口) | frame3-patient-profile.tsx:246 |
onOpenBindSelector → frame3b-bind-selector.tsx → bind_patient_seat(refractive_record_service.py:428) |
| 「编辑」(记录详情页) | frame4-record-view.tsx:1187,1194 |
onEdit → 复用 Frame5RecordEntry 编辑态(refractive-record-pane.tsx:149 注释) |
| 「保存」(帧5 录入页,新建/编辑共用) | frame5-record-entry.tsx:750,753 |
onSave → createRecord(...)(新建)或对应更新接口(编辑) |
| 「重新生成」(赵阳智脑分析,3 处 tab/态) | frame4-record-view.tsx:1321,1333,1343 |
handleGenerateAnalysis() → POST /records/{id}/analysis(app/api/refractive_records.py:311) |
| 「保存」(门店结论草稿) | frame4-record-view.tsx:1407 |
handleSaveConclusion() |
改动量估计:以上 9 个按钮的 onClick 都需要加一层"若当前档案是示范档案 → 弹提示 → return,不执行原逻辑"的判断,属于逐个按钮的小改动,但点位分散在 3 个文件(frame3-patient-profile.tsx / frame4-record-view.tsx / refractive-record-pane.tsx),需要一个统一的"是否示范档案"判断依据(比如后端下发一个 is_sample: true 字段,前端存进 patient 对象里各处判断),这个字段目前不存在,需要新增。
| 按钮 | file:line | 触发的动作/接口 | 是否已有天然拦截 |
|---|---|---|---|
| 「新建档案」(顶部,公共功能,不拦) | web/app/h5/family/page.tsx:127-136 |
跳转 family/new/page.tsx |
— |
| 「保存并添加检查单」/「保存修改」(新建/编辑档案) | web/app/h5/family/new/page.tsx:453,458 |
handleSubmit → PUT /api/parent/patients/{pid}(:114,227) |
这个是家长自建档案专属表单,不会作用在示范档案(示范档案是门店建的,owner_type≠parent),但仍建议确认家长无法通过 URL 直接拼 pid 访问示范档案的编辑表单 |
| 「添加检查单」 | web/app/h5/family/[id]/page.tsx:2422 |
跳转 exam/new 页 → POST /api/parent/patients/{pid}/records(app/api/parent_patients.py:1062) |
没有,create_my_child_record 端点无示范档案判断,需要新加拦截(前端弹提示即可满足拍板项7,后端建议兜底) |
| 「提交检查单」/「保存修改」(exam/new 页) | web/app/h5/family/[id]/exam/new/page.tsx:1241,1246 |
handleSubmit |
编辑态(带 rid)会走 PATCH,已有 _record_editable_by_parent 门(见下一行);新建态(无 rid)没有门 |
「编辑」(单条记录,仅 detail.editable !== false 时显示) |
web/app/h5/family/[id]/page.tsx:2748,2757 |
跳转 exam/new?rid= → 更新接口(app/api/parent_patients.py:1058-1060) |
已有,_record_editable_by_parent(app/api/parent_patients.py:869-875)按记录创建人席位类型判断,只要示范档案记录的 created_by_user_id 不是家长席位,editable 天然为 false,前端"编辑"按钮不会显示(不是"点了弹提示",是直接不出现 —— 这跟拍板项7"按钮不隐藏"的要求不完全一致,需要长官确认:记录编辑按钮是要"沿用现有的直接隐藏"还是"改成显示+弹提示") |
| 「分享」(保留,正常可用) | web/app/h5/family/[id]/page.tsx:2410 |
buildShareCard() |
不拦,但卡片机构名要改(见 3.2) |
⚠️ 9-14 长官已拍板(口径14):家长端记录级"编辑"按钮统一改为"不隐藏,点了弹提示",不再为示范档案单独开例外。但物理事实必须写清楚:detail.editable !== false 这套隐藏判据(_record_editable_by_parent,app/api/parent_patients.py:869-875)是现网通用机制,服务于"区分门店录入 vs 家长自录"这个跟示范档案完全无关的既有功能——它同时管着所有真实家长档案里、由门店录入的检查记录的编辑按钮是否显示。把它从"隐藏"改成"显示+提示",物理上等同于把现网所有真实家长查看自己孩子被门店录入的记录时,原本不出现的"编辑"按钮改成出现(点击后弹提示、不能真编辑)。这不是"给示范档案加一个特判分支"能局部解决的,而是改动了通用组件既有渲染链路本身(detail.editable 判断 → EyeGroupTable 所在的记录详情页按钮渲染),影响面覆盖生产环境全部真实家长用户,实施时必须按这个认知设计,不能误以为只用给示范档案加特判就完事。
改动量估计:家长端主要新增点是"添加检查单"按钮的拦截(1 处)+ 分享卡片机构名取值分支(1 处逻辑,4 处渲染点共用同一变量)+ 上述编辑按钮的政策确认。
这是两套完全独立的数据、独立的可见范围,示范档案 6 条记录都要填,不能只填一套:
| AI 分析 | 门店结论 | |
|---|---|---|
| 字段 | ai_analysis_text/ai_analysis_status/ai_analysis_updated_at(app/models/refractive_record.py:148,151,154) |
store_conclusion_text(草稿)/store_conclusion_pushed_text(快照)/store_conclusion_pushed_at(:157,160,163) |
| 内容形态 | JSON 字符串(kind/p1_diag/p2_advice/p3_warning/plan_4l/plan_drugs_plus/model_version),机器结构化产出 |
纯文本(≤2000字),门店医师手写口吻,渲染时按「【小标题】\n正文」解析 |
| 谁能看 | 只有门店端(app/api/refractive_records.py:309 注释明令 ai_analysis_* 严禁出现在家长侧任何响应) |
门店端 + 家长端(家长端只在 store_conclusion_pushed_at 非空时拿到 store_conclusion_pushed_text 这一份"推送快照",看不到 store_conclusion_text 草稿字段,更看不到 ai_analysis_text 原文) |
| 谁来产出 | 27B 模型自动生成(或示范档案预置写死) | 门店医师人工撰写(或示范档案预置写死) |
| 示范档案要求 | 6 条记录每条都要有(口径19),ai_analysis_status 全部写 done |
只有最后一条要推送给家长(口径20),前 5 条 store_conclusion_pushed_at 应为 NULL |
关于"推送不发通知"这件事,对示范档案的影响:push_conclusion()(app/services/refractive_record_service.py:2575-2590)只做两件事——把草稿字段拷贝成快照字段、打一个时间戳——不调用任何短信/微信/站内通知渠道(已 grep 验证该函数体 0 命中 sms/wechat/notify 类调用)。这意味着示范档案第 6 条记录的门店结论即使预置数据里 store_conclusion_pushed_at 已经非空,家长打开 App 也不会收到任何主动提醒——必须自己点进「我的孩子」→ 对应检查记录 → 详情弹层,才能看到列表页那颗从「查看」变绿的「门店解读」按钮(ExamRecordCard.tsx:124-136)并点进去阅读。做示范讲解时需要提前带看这一步,不能指望家长自己发现。
角色设定:张思源,男,建档日期起 8 岁 0 个月。以下日期按"当前(2026-09-14)前后"设计,让示范档案看起来是"最近还在持续跟踪"(最后一条检查约 1.5 个月前),而不是一份陈旧数据。以下数值均为示范数据,非真实患者,仅用于演示防控效果曲线。
字段来源:app/models/refractive_record.py(主表 RefractiveRecord line 38-167)+ RefractiveRecordEye 子表(line 181-230,眼别 OD/OS 各一行)。
9-14 重算说明(解决口径16的两处医学硬伤 + 落实口径17的4L替换):
- 裸眼视力改用全程 5 分记录法(4.3~4.9),不再混用小数记录法(0.x),且与球镜度数单调对应:度数越深,裸眼视力越差,不再出现"球镜 -2.00D 却裸眼视力 0.3"这种两套记法混用的问题,更不允许"裸眼视力回升"这种唯有 OK 镜才能解释、但示范档案已禁用 OK 镜的矛盾数据。
- 矫正视力全程 5.0/5.0(标准全矫预期,不受球镜加深影响)。
- 眼轴年增速:干预前(第1-3次,2025-05-01~2025-11-01,共 6 个月)OD/OS 均为 (23.40-23.20)/0.5 = 0.40mm/年;干预后(第3-6次,2025-11-01~2026-08-01,共 9 个月)OD/OS 均为 (23.57-23.40)/0.75 ≈ 0.227mm/年;降幅 (0.40-0.227)/0.40 ≈ 43%,落在真实儿童常见眼轴年增速上限(0.5-0.6mm/年)以内,不再出现原 0.76mm/年的失真数值。
- 球镜/眼轴换算比例:干预前总变化 0.50D / 0.20mm = 2.5D/mm;干预后总变化 0.50D / 0.17mm ≈ 2.94D/mm,均落在"约 1mm≈2.5-3.0D"区间内。
- 干预手段改用 4L 体系(口径17):第3次起启动干预,按 L4→L1→L2+L3 的顺序分批加用真实枚举里的无品牌选项(web/app/h5/family/[id]/exam/new/prevention.ts):离焦镜(4L·L4 镜片类)、其他Led红光(4L·L1 光疗类)、全光谱吸顶灯(4L·L2 补光类)、其他读写台(4L·L3 远疗类)。全程不出现 OK 镜、不出现阿托品。
| 序号 | 检查日期 | 年龄 | AL 眼轴 OD/OS(mm) | K1/K2 OD(D) | K1/K2 OS(D) | 主觉球镜 SPH/CYL/AX OD | 主觉球镜 SPH/CYL/AX OS | 裸眼视力 OD/OS(5分制) | 矫正视力 OD/OS(5分制) | 备注/4L防控措施 |
|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 2025-05-01 | 8岁0月 | 23.20 / 23.15 | 42.00/43.50 | 42.10/43.40 | -1.00 / -0.50 / 90° | -0.75 / -0.50 / 85° | 4.7 / 4.8 | 5.0 / 5.0 | 首次建档,常规验光,建议每 3 个月复查 |
| 2 | 2025-08-01 | 8岁3月 | 23.30 / 23.25 | 42.05/43.50 | 42.10/43.45 | -1.25 / -0.50 / 90° | -1.00 / -0.50 / 85° | 4.6 / 4.7 | 5.0 / 5.0 | 眼轴增长较快(+0.10/+0.10mm),建议密切观察 |
| 3 | 2025-11-01 | 8岁6月 | 23.40 / 23.35 | 42.05/43.55 | 42.15/43.45 | -1.50 / -0.50 / 90° | -1.25 / -0.50 / 85° | 4.5 / 4.6 | 5.0 / 5.0 | 眼轴持续增长较快(+0.10/+0.10mm),本次起启动干预:配戴离焦镜(4L·L4)+ 每日户外活动≥2小时,家长已知情同意 |
| 4 | 2026-02-01 | 8岁9月 | 23.46 / 23.41 | 42.05/43.55 | 42.15/43.45 | -1.75 / -0.50 / 90° | -1.50 / -0.50 / 85° | 4.4 / 4.5 | 5.0 / 5.0 | 加用其他Led红光(4L·L1)光疗,干预后眼轴增速明显放缓(+0.06/+0.06mm) |
| 5 | 2026-05-01 | 9岁0月 | 23.51 / 23.46 | 42.10/43.55 | 42.15/43.50 | -1.75 / -0.50 / 90° | -1.50 / -0.50 / 85° | 4.4 / 4.5 | 5.0 / 5.0 | 加用全光谱吸顶灯(4L·L2)+ 其他读写台(4L·L3)改善采光与读写姿势,眼轴增速维持低位(+0.05/+0.05mm) |
| 6 | 2026-08-01 | 9岁3月 | 23.57 / 23.52 | 42.10/43.55 | 42.15/43.50 | -2.00 / -0.50 / 90° | -1.75 / -0.50 / 85° | 4.3 / 4.4 | 5.0 / 5.0 | 持续离焦镜+其他Led红光+全光谱吸顶灯+其他读写台综合方案,防控效果稳定(+0.06/+0.06mm),建议继续维持 |
未列出字段(角膜地形图/双眼视功能/身高体重等):建议全部填一个合理默认值以满足"所有字段全部写全"的拍板要求(拍板项3),具体枚举值(如 refraction_method 的可选项、visual_acuity_notation 的记法)未逐一核实,实施时需对照 app/schemas/refractive_record.py 的 RecordCreateIn 枚举校对,本文档未做这一步验证。
⚠️ 边界重申(避免误解为改动了真实枚举):上表"4L·L1~L4"仅为业务侧沟通用的分类简写,prevention.ts 里的六组枚举(眼镜类/光疗类/远疗类/理疗类/食疗类/药品类)本身及每组含品牌词的选项一律未改动,示范档案只是从"光疗类"/"远疗类"/"眼镜类"三组里各挑了一个无品牌选项,不涉及新增或删除任何枚举值。
⚠️ 图文关系说明(9-14 精简后,重要,防止对着图 grep 找不到本节数据而误判"没改"):方案图 refractive-record-demo-single-child-20260914-b-gold.html 经口径15精简后只剩两节(门店端患者列表、家长端个人档案列表),两节原本就不展示单次检查的逐条明细(只显示姓名/年龄/最近检查日期/最新眼轴等汇总字段),此前承载本表逐条明细的「6 条记录表格 + 眼轴趋势曲线」节、「检查单详情 8 大组全字段」节已按口径15整节删除。因此:
- 图上physically 不会出现 "OK镜""阿托品""其他Led红光""全光谱吸顶灯""其他读写台""离焦镜"这些词(连同它们所在的整节一起被删,不是遮蔽或隐藏)。
- 图上仅有的两处与本表相关的数字是收尾同步过的最终态:①门店端患者列表示范行的「最近检查」日期已同步为本表第 6 次的 2026-08-01;②家长端个人档案列表示范卡片的「眼轴 OD/OS」已同步为本表第 6 次的 23.57mm / 23.52mm。
- 本表(6 条完整记录 + 4L 措施)的唯一权威出处是本文档,不在图上重复展现,这是本次精简的直接结果,不是遗漏。
家长您好,这是思源本次复查的结果。
孩子这次眼轴右眼 23.57mm、左眼 23.52mm,两眼都在稳定范围内。
从去年 5 月建档到现在一年多,右眼眼轴一共长了 0.37mm。刚开始那半年长得比较快,半年就长了 0.20mm;从我们给孩子安排防控之后,每三个月只长 0.05mm 左右,速度明显慢下来了,这是个很好的变化。
孩子目前在坚持的:
- 每天弥散红光 2 次
- 家里保持全光谱照明
- 每天用读写台拉远 30 分钟
- 离焦镜全天佩戴
现在孩子各项指标都挺稳,建议继续保持现在这套方案,三个月后带孩子再来复查一次。有任何变化随时联系我们。
✅ 已解决(9-14 长官拍板选 A:改文案数字、不改数据表,上方文案已更正为"去年 5 月建档"/"一共长了 0.37mm"/"半年就长了 0.20mm",与本节数据表一致)。以下为当时发现问题的原始核对记录,留作决策依据:
⚠️ 数字核对结果(与本节 6 行数据表逐项算过,发现三处不一致,原文案原样保留不改写,交长官定夺):
1. 文案说"右眼 23.57mm、左眼 23.52mm" —— 与表格第 6 条完全一致 ✅。
2. 文案原说"去年 3 月建档" —— 表格第 1 条建档日期是 2025-05-01(五月),不是三月,相差 2 个月。
3. 文案原说"一共长了 0.60mm" —— 按表格第1条(23.20/23.15)到第6条(23.57/23.52)算,右眼、左眼实际总增量都是 0.37mm,与 0.60mm 不符(相差 0.23mm)。
4. 文案原说"半年就长了 0.22mm" —— 按表格第1条(2025-05-01)到第3条(2025-11-01,正好 6 个月)算,右眼、左眼实际半年增量都是 0.20mm,与 0.22mm 相差 0.02mm。
5. 文案说"每三个月只长 0.05mm 左右" —— 按表格第3→4条(+0.06)、第4→5条(+0.05)、第5→6条(+0.06)算,实际每季度增量在 0.05-0.06mm 之间,与"0.05mm左右"基本吻合,这一项对得上,未改。
上述 2、3、4 三处数字与本文档 9-14 二次拍板重算后的最终数据表(口径16)不一致,推测文案可能是按重算前的旧版数据口算写就。9-14 长官已就此拍板选 A(改文案数字、不改数据表),上方文案已同步更正,种子数据落库时文案与表格数字已一致,不再需要在种子脚本注释里标注这条差异。
owner_seat_id 单值列决定示范档案不能被真实"绑定"给所有家长,只能在接口层虚拟拼接;而"每机构一份真行 vs 全局一份虚拟拼接"这个技术方案分叉,长官 13 条拍板里没有明确覆盖,建议实施前先定(见 3.1 表格最后一行)。_record_editable_by_parent 判据(app/api/parent_patients.py:869-875)是现网通用机制,管着所有家长档案里"门店录入记录"的编辑按钮可见性,不是示范档案专属代码路径。改这条等于同时改变了现网所有真实家长用户查看自己孩子由门店录入记录时的按钮可见性,不是"给示范档案开个特判"能局部解决的,必须先做一次全量影响评估(现网有多少条家长档案下存在门店录入的记录、这些家长是否已习惯"看不到编辑按钮"这个现状),再动手实施,详见 3.4。createRecord 与家长端 create_my_child_record 都没有"该档案是否示范"的门,如果只在前端拦(拍板项7要求的交互),存在被绕过直接调接口写脏示范档案的风险,建议后端也加一道校验。orgDisplayName 在同一个文件里有 4 处独立的 Canvas 绘制点各自取值(page.tsx:742,1132,1440,1967),示范档案改机构名(9-14 定稿为「示范档案机构」,见口径18)要 4 处同时生效,改的时候要用 grep 数过,不能漏改一处(本文档已按方法论提醒,未实际改代码)。web/app/h5/tablet 目录下没有"患者总数"类展示(0 命中,已用"新建档案"关键词做过对照组证明查法有效),但机构管理后台/其他统计报表未逐一核实,如果别处存在患者数统计,示范档案会把计数污染 +1,需要实施前补查。created_by_user_id 取值需要谨慎选取:如果直接留空/指向一个不存在的用户 ID,会让 _creator_seat_type 返回 None(app/api/parent_patients.py:860-863),从而 _record_editable_by_parent 判定 False(不可编辑)—— 这个副作用目前看是"顺带满足了需求"(家长不能编辑示范档案记录),但需要在实施时明确写清楚这是依赖了一个未来可能被改动的隐式行为,不是专门为示范档案设计的机制,后续如果这段逻辑改了判据,示范档案的只读性可能被意外打破。created_by_user_id 对应席位 seat_type != 'parent'),再决定是直接全量切换还是先加开关分阶段上线(见 3.4、五-2 更新)。$ sed -n '42,50p' app/models/flywheel.py
# owner_type='seat'(默认/存量)= 门店建档 · 'parent' = 家长自建,门店搜索一律排除。
owner_type = Column(String(10), nullable=True, server_default=text("'seat'"), comment='档案归属 seat=门店建 / parent=家长建 · Phase 74')
owner_seat_id = Column(Integer, nullable=True, comment='owner_type=parent 时的家长席位 users.id · 家长只见自己的 · Phase 74')
$ grep -n "编辑\|onClick\|onEdit" web/app/h5/tablet/pane/panes/refractive-record/frame4-record-view.tsx | head
1187: <button type="button" onClick={onEdit}
1194: 编辑
$ grep -n "修改档案\|新增检查\|onClick" web/app/h5/tablet/pane/panes/refractive-record/frame3-patient-profile.tsx | head
195: onClick={onOpenEditPatient}
200: 修改档案
204: onClick={onNewRecord}
209: 新增检查
228: onClick={onUnbind}
246: onClick={onOpenBindSelector}
$ grep -n "保存\|onSave" web/app/h5/tablet/pane/panes/refractive-record/frame5-record-entry.tsx | head -3
65: onSave: () => void;
750: <button type="button" onClick={onSave} disabled={saving}
753: {saving ? "保存中…" : "保存"}
$ sed -n '869,876p' app/api/parent_patients.py
async def _record_editable_by_parent(db: AsyncSession, created_by_user_id: int) -> bool:
"""批B(长官9-03)· 家长端只读权限判据:精确到**记录级**,不能沿用上面
_source_from_owner_type 的档案层近似值 ...
真实判据是该条记录的建档人(RefractiveRecord.created_by_user_id)所在席位
的 seat_type 是否为 'parent'"""
return await _creator_seat_type(db, created_by_user_id) == "parent"
$ sed -n '2748,2757p' web/app/h5/family/[id]/page.tsx
{detail.editable !== false && (
<button ... aria-label="edit-exam">
编辑
$ grep -n "ai_analysis_text\|store_conclusion_pushed_text" app/models/refractive_record.py
148: ai_analysis_text = Column(
160: store_conclusion_pushed_text = Column(
$ sed -n '148,163p' app/models/refractive_record.py
ai_analysis_text = Column(
Text, nullable=True, comment="赵阳智脑分析原文(门店可见 · 批5)",
)
...
store_conclusion_pushed_text = Column(
Text, nullable=True, comment="门店结论已推送快照(家长可见 · 批5)",
)
$ sed -n '307,309p' app/api/refractive_records.py
# ═══ 批5 · 赵阳智脑分析 + 门店结论 ══════════════════════════════════════════
# 不吃秒读额度(不调 org_service.record_usage)· 只回门店端(不暴露给 parent_patients.py
# 的字段仅 store_conclusion_pushed_text/at · ai_analysis_* 严禁出现在家长侧任何响应)
$ grep -rn "delete_patient\|DELETE.*patient\|def delete.*[Pp]atient" app/api/ app/services/
(0 命中)
$ grep -rn "def delete_record\|delete.*[Rr]ecord\b" app/api/refractive_records.py | head -3
235:async def delete_record(
(对照组命中 → 证明 grep 查法有效,"delete_patient 0 命中"是真的没有,不是查法写错)
$ grep -rn "is_demo\|示范档案\|demo_patient\|DEMO_PATIENT" app web/app --include=*.py --include=*.tsx
(仅命中 OrgBrainConfig.is_demo_store 相关,与本功能无关的既有概念,详见正文 3.1 命名避坑)
$ grep -rln "患者总数\|总人数\|档案总数\|患者数" web/app/h5/tablet --include=*.tsx
(0 命中)
$ grep -rl "新建档案" web/app/h5/tablet --include=*.tsx | wc -l
2
(对照组命中 2 → 证明 grep 查法有效,"患者总数类展示 0 命中"是真的没有,不是查法写错;
但本次仅核实 tablet 目录,机构管理后台/其他统计报表未逐一核实,见五-5)
$ grep -n "orgDisplayName\|赵阳智脑" web/app/h5/family/\[id\]/page.tsx
523: const [orgDisplayName, setOrgDisplayName] = useState<string | null>(null);
742: const orgName = orgDisplayName || getOrgNameFromSeatToken() || "赵阳智脑";
1132: const orgName = orgDisplayName || getOrgNameFromSeatToken() || "赵阳智脑";
1440: const orgName = orgDisplayName || getOrgNameFromSeatToken() || "赵阳智脑";
1967: const orgName = orgDisplayName || getOrgNameFromSeatToken() || "赵阳智脑";
$ grep -n "APIRouter(prefix" app/api/parent_patients.py
57:router = APIRouter(prefix="/api/parent/patients")
$ sed -n '217,230p' app/api/parent_patients.py
@router.get("")
async def list_my_children(
query: Optional[str] = Query(None, max_length=50),
limit: int = Query(20, ge=1, le=50),
seat: dict = Depends(_require_parent),
db: AsyncSession = Depends(get_db),
) -> dict[str, Any]:
"""我的孩子列表(效果图态① · 只返本家长自建档案)..."""
patients = await svc.search_patients(
db, seat["org_id"], query, limit, owner_seat_id=seat["user_id"],
include_bound=True,
)
$ sed -n '166,168p' app/services/refractive_record_service.py
async def search_patients(
db: AsyncSession, org_id: int, query: Optional[str], limit: int,
owner_seat_id: Optional[int] = None,
$ grep -n "children.length" web/app/h5/family/page.tsx
127: {!loading && !loadError && children.length > 0 && (
151: {!loading && !loadError && children.length === 0 && (
(两个互斥分支,要让示范档案在两种情况下都渲染,需要重构这段条件判断,不能只在其中一支里加)
$ sed -n '148,163p' app/models/refractive_record.py
ai_analysis_text = Column(
Text, nullable=True, comment="赵阳智脑分析原文(门店可见 · 批5)",
)
ai_analysis_status = Column(
String(16), nullable=True, comment="分析状态 pending/done/failed(批5)",
)
ai_analysis_updated_at = Column(
TIMESTAMP(timezone=True), nullable=True, comment="分析最近更新时间(批5)",
)
store_conclusion_text = Column(
Text, nullable=True, comment="门店结论草稿(未推送 · 批5)",
)
store_conclusion_pushed_text = Column(
Text, nullable=True, comment="门店结论已推送快照(家长可见 · 批5)",
)
store_conclusion_pushed_at = Column(
TIMESTAMP(timezone=True), nullable=True, comment="结论推送时间(批5)",
)
$ sed -n '2221,2229p' app/services/refractive_record_service.py
return {
"kind": "4l_analysis",
"p1_diag": p1,
"p2_advice": p2,
"p3_warning": p3,
"plan_4l": plan_4l_clean,
"plan_drugs_plus": plan_drugs_plus,
"model_version": "qwen3.6-27b-awq-int4",
}
$ sed -n '979,988p' app/services/refractive_record_service.py
_ANALYSIS_REQUIRED_FIELDS = (
("unaided_va_raw", "裸眼视力"),
("subjective_sphere", "验光球镜"),
("subjective_cylinder", "验光柱镜"),
("axial_length", "眼轴"),
)
$ sed -n '2575,2590p' app/services/refractive_record_service.py
async def push_conclusion(db: AsyncSession, org_id: int, record_id: UUID) -> dict[str, Any]:
"""把草稿快照为推送态(家长可见)· 草稿为空 → 409"""
rec = await _fetch_record(db, org_id, record_id, for_update=True)
if not (rec.store_conclusion_text or "").strip():
raise HTTPException(status_code=409, detail="结论为空,无法推送")
rec.store_conclusion_pushed_text = rec.store_conclusion_text
rec.store_conclusion_pushed_at = datetime.now(timezone.utc)
# (下略:commit + 异常处理,函数体内 0 处调用 sms/wechat/notify/send_message 类接口)
$ grep -n "sms\|wechat\|notify\|send_message\|template_message" app/services/refractive_record_service.py | grep -i conclusion
(0 命中 · 对照组:同文件 grep "sms\|微信通知\|短信" 若命中说明查法有效 · 本函数确认无通知调用)
$ sed -n '124,136p' web/app/h5/family/[id]/_components/ExamRecordCard.tsx
{r.has_store_conclusion ? (
<span aria-label={`view-store-conclusion-${r.id}`} ...>
门店解读
<ChevronRight className="w-3.5 h-3.5" />
</span>
) : (
<span aria-label={`view-plain-${r.id}`} ...>
{busy ? "打开中…" : "查看"}
<ChevronRight className="w-3.5 h-3.5" />
</span>
)}
$ grep -n "<StoreConclusionCard" web/app/h5/family/[id]/page.tsx
2775: <StoreConclusionCard storeConclusion={detail.store_conclusion} />
$ sed -n '289,294p' app/schemas/refractive_record.py
class ConclusionPatchIn(BaseModel):
"""PATCH /records/{id}/conclusion · 保存门店结论草稿(批5)"""
model_config = ConfigDict(extra="forbid")
text: str = Field(..., max_length=2000)
事故经过:方案图 refractive-record-demo-single-child-20260914-b-gold.html 上一轮迭代汇报"已完成第 6 次检查(2026-06-15)全字段明细展示",但主会话跑物理 grep 验收时发现:
$ grep -c "视力数据\|自镜度数\|本次验光\|基础测量\|生物测量\|眼部检查" refractive-record-demo-single-child-20260914-b-gold.html(修复前版本)
视力数据=0 自镜度数=0 本次验光=0 基础测量=0 生物测量=0 眼部检查=0
即长官通过真实环境截图确认过的"检查单详情页真实分组是 8 组"这件事,在图上一个分组标题都没出现——旧版本用的是"档案头/双眼数据/视功能"这种数据库表视角的分组,不是家长端 App 真实渲染时用的分组名。对照组当时同一份文件里 示范=38、眼轴=27 命中,证明 grep 查法本身有效,不是查法写错导致的假 0——是真的没画。
根因:写图时凭对后端模型字段(RefractiveRecord/RefractiveRecordEye/RefractiveRecordVisualFunction 三张表)的印象分组罗列字段,没有去读家长端 App 真实渲染代码(web/app/h5/family/[id]/page.tsx:2776-2884)确认页面实际展示时用的分组名和顺序——这正是本项目 CLAUDE.md「tech 写作铁律」和「memory 仅参考铁律」两条都强调过的坑:凭印象/凭模型层面的理解,替代了对最终渲染代码的物理核对。
修复:9-14 本轮重写第 4 节"查看单次检查全字段数据"区块,逐字段照抄 page.tsx:2776-2884(EyeGroupTable 8 组:视力数据/自镜度数/本次验光/基础测量/生物测量/眼部检查/视功能/备注)与 page.tsx:341-472(VisualFunctionGroupCard 视功能子分组:双眼视觉/眼位与AC:A/融像与集合/调节功能)的真实标签、单位、OD/OS 结构,不再用数据库字段名代替页面分组名。修复后 grep 验收:
视力数据=1 自镜度数=1 本次验光=1 基础测量=1 生物测量=1 眼部检查=1 视功能=3 本次检查备注=1
全部 ≥1,达标。
教训固化:出图/写方案文档时,凡涉及"某个真实页面长什么样/分成几组/字段叫什么",一律先读该页面的真实渲染代码(而不是它背后的数据模型/表结构),二者的分组方式可能完全不同——本次事故就是数据库三张表的字段被直接罗列,而没有对照页面组件实际怎么分组渲染。