依据材料:
/home/dev2/reports/demo-profile-scope-20260914.md(长官 18 条拍板口径)+
/home/dev2/reports/refractive-record-demo-single-child-20260914-b-gold.html(定稿方案图,仅两节)。
本文档所有改动点均经本会话独立物理 grep 重新核实(不转抄 scope 文档结论),证据见第七节。
本文档只写计划,不改业务代码、不 commit、不发版、不碰数据库。
给每个机构固定放一份只读的"张思源"示范视光档案(8岁男孩,6次检查,数据体现"干预前眼轴长得快、干预后明显变缓"),门店端和家长端都能看到,长得跟真档案一样但点任何写操作都弹提示引导去建真档案。零档案的家长也能看到这份示范,用来在没有真实客户数据前先证明系统能测出什么、防控做出来是什么样子。
划分原则:数据层先行(后端接口和前端都依赖它下发的标记字段);门店端与家长端的"示范档案展示 + 专属拦截"可独立验收独立发 test;家长端"记录编辑按钮"那一条因为改动的是现网通用逻辑、影响全部真实用户,单独隔离成一批,不与示范档案其余改动混发。
| 批次 | 内容 | 依赖 | 预估工作量(诚实口径,非小时数) |
|---|---|---|---|
| 批1 | 数据层:is_sample 标记字段 + migration + 种子数据脚本(1 patient + 6 records + 12 eyes + 6 份 AI 分析预置 JSON(口径19)+ 最后一条门店结论草稿/推送快照/推送时间戳(口径20)) |
无 | 约 3-4 个文件(1 模型改动 + 1 migration + 1 种子脚本 + 1 处机构创建钩子待定位);种子脚本字段量比原估更大——6 条记录 × 约 15 个临床字段 + 6 条 ai_analysis_text JSON(每条含 plan_4l 四层文案,需照 refractive_record_service.py:2159-2213 的清洗规则手写、四层缺一不可)+ 最后一条的 store_conclusion_text/store_conclusion_pushed_text/store_conclusion_pushed_at 三字段(长官已定稿文案,逐字照抄;✅ 文案原与数据表存在 3 处数字出入,9-14 长官已拍板选 A 改文案数字、不改数据表,文案已更正,详见 scope 文档四节),需要逐字段核对第四节表格,不确定量上调 |
| 批2 | 后端接口:家长端列表注入示范档案 + 门店端/家长端写操作接口后端兜底拦截 | 批1(需要 is_sample 字段和种子数据已存在) |
约 2-3 个文件,7-9 处改动点;门店端具体有多少个写接口需要加拦截不确定,需实施时逐个 grep 落实(本计划只锁定了 trigger_analysis 一处的精确 endpoint,其余按前端按钮反推) |
| 批3 | 门店端(平板)前端:患者列表页渲染示范档案区(视觉扒方案图令牌)+ 9 个写操作按钮加拦截 | 批2(需要接口下发 is_sample 字段) |
约 4 个文件,10 处改动点 |
| 批4 | 家长端前端:个人档案列表页渲染示范档案区 + 空态卡文案调整 +「添加检查单」按钮拦截 + 分享卡片机构名替换(4 处) | 批2 | 约 2 个文件,6 处改动点 |
| 批5(隔离,不与示范档案其余批次同批发布) | 家长端记录级「编辑」按钮:detail.editable !== false 隐藏判据改为"不隐藏,点击弹提示" |
无(不依赖示范档案数据层,是独立的现网逻辑变更;但建议放在批1-4之后单独排期评审) | 约 1 个文件、1 处改动,但影响面覆盖全部生产真实家长用户,代码量小、风险评估工作量大,不可用代码行数衡量 |
批1→批2→批3/批4(批3、批4可并行,互不依赖对方代码)。批5 全程独立,建议单独走一次评审+SQL摸底后再发布,不卡示范档案主线进度。
目标:给示范档案打上物理标记,并在库里(或按后续决策虚拟拼接)准备好 1 条患者 + 6 条检查记录 + 12 条眼别子记录 + 6 条记录每条都要有的预置 AI 分析文案(口径19,不是只做最后一条)+ 最后一条记录的门店结论草稿/推送快照/推送时间戳(口径20),供批2接口读取。
改哪些文件:
app/models/flywheel.py:43-50(Patient 类,紧邻 owner_type/owner_seat_id 定义处)—— 仿照现有写法新增 is_sample = Column(Boolean, nullable=False, server_default=text("'false'"), comment='示范档案标记 · 系统内置样板,不可编辑')。已核实该处现网写法(见七§1),新列插入位置紧跟 owner_seat_id 之后即可,不影响其他字段。migrations/versions/ 新增一个 migration 文件,格式必须仿照 migrations/versions/store_poster_demo_flag.py(七§11 已贴全文关键片段)—— 该文件是本仓库唯一一次"加标记位字段"的先例,且踩过一个真实生产事故:down_revision 必须选main 上也存在的祖先,不能选"建迁移时 test 的队尾"(选择性晋级模式下 main 迁移集是 test 的子集,选错会导致生产 alembic 修订树断链、家长入口 500)。实施时必须先跑 alembic heads 确认当前 test/main 两侧的实际头节点,不能凭本计划编造具体 down_revision 值。新增一次性种子数据脚本(建议路径 scripts/seed_demo_profile.py,仿照仓库现有 scripts/backfill*.py 系列命名与结构,本计划未逐一核实该目录下现有脚本的具体写法,实施时应先读 1-2 个同类脚本对齐风格)—— 写入本文档口径不变的字段值,即 scope 文档四节 6 行表格(2025-05-01 至 2026-08-01,眼轴/角膜曲率/球镜/视力/4L防控措施逐条照抄该表,不得改动数值),created_by_user_id 取值需刻意选一个不指向 seat_type='parent' 席位的值(理由见批2、七§9)。
ai_analysis_text(app/models/refractive_record.py:148)要写入一份 JSON 字符串,结构固定为 {"kind":"4l_analysis","p1_diag":..,"p2_advice":..,"p3_warning":..,"plan_4l":{"l1":..,"l2":..,"l3":..,"l4":..},"plan_drugs_plus":..,"model_version":"qwen3.6-27b-awq-int4"}(出处 app/services/refractive_record_service.py:2221-2229)。plan_4l 四层每层内容要照该记录当次的 4L 防控措施(scope 文档四节表格「备注」列)手写,每层 ≤80 字、四层缺一不可(真实生成逻辑的清洗规则见 refractive_record_service.py:2159-2213,种子脚本走的是预置写死路径不实际调用这段逻辑,但内容仍应遵循同一格式约束,否则前端渲染会露怪)。6 条记录的 ai_analysis_status(:151)统一写 done,ai_analysis_updated_at(:154)写对应检查日期当天。⚠️ 该字段严禁在批2/批4 的任何接口响应里下发给家长端(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)写一个非空时间戳(如检查日期后 1-2 天,体现"门店看完后补录结论并推送"的真实节奏);前 5 条记录这三个字段保持 NULL,不要连带写。长度需在 2000 字上限内(app/schemas/refractive_record.py:289-294 已核实该上限)。长官定稿文案(逐字照抄,不许改写)——```
家长您好,这是思源本次复查的结果。
孩子这次眼轴右眼 23.57mm、左眼 23.52mm,两眼都在稳定范围内。
从去年 5 月建档到现在一年多,右眼眼轴一共长了 0.37mm。刚开始那半年长得比较快,半年就长了 0.20mm;从我们给孩子安排防控之后,每三个月只长 0.05mm 左右,速度明显慢下来了,这是个很好的变化。
孩子目前在坚持的:
- 每天弥散红光 2 次
- 家里保持全光谱照明
- 每天用读写台拉远 30 分钟
- 离焦镜全天佩戴
现在孩子各项指标都挺稳,建议继续保持现在这套方案,三个月后带孩子再来复查一次。有任何变化随时联系我们。
```
✅ 已解决:9-14 长官拍板选 A(改文案数字、不改数据表),文案已更正为与数据表一致(总增量 0.37mm、半年增量 0.20mm、建档月 5 月)。种子脚本实施时按上方已更正的文案原样写入即可,不再需要脚本注释里标注差异。以下为当时发现问题的原始记录,留作决策依据:
⚠️ ~~该文案里的眼轴总增量/半年增量/建档月份三个数字与 scope 文档四节最终数据表存在出入~~(表格算出总增量 0.37mm、半年增量 0.20mm、建档月为 5 月;文案原写的是 0.60mm/0.22mm/3月),scope 文档四节末尾已详细列出核对过程并标注"待长官定夺"。
- 前 4 项 AI 生成门槛字段(unaided_va_raw/subjective_sphere/subjective_cylinder/axial_length,出处 refractive_record_service.py:983-988)必须双眼(OD/OS)都填值——scope 文档四节表格本身已经双眼填全(裸眼视力/主觉球镜/主觉柱镜/眼轴逐条都有 OD、OS 两列),种子脚本照抄该表即可自然满足这道门槛,不需要额外补字段,但实施时要在验收阶段显式核对一遍(见下方验收标准新增项),不能想当然。
4. 待定位:若走"每机构一份真实行"方案(见第四节技术决策),需要在机构创建入口加一个自动播种钩子 + 对存量机构跑一次性回填。机构创建入口的具体 file:line 本计划未定位,标注"未找到,实施时需先 grep 机构创建相关 service/API 确认"。
验收标准:
```
# 迁移执行后确认新列存在
sudo db-query test "SELECT column_name FROM information_schema.columns WHERE table_name='patients' AND column_name='is_sample'"
# 期望: 返回 1 行
# 种子脚本跑完后确认数据落地
sudo db-query test "SELECT p.id, p.name, p.is_sample, COUNT(r.id) AS record_count FROM patients p LEFT JOIN refractive_records r ON r.patient_id=p.id WHERE p.is_sample=true GROUP BY p.id,p.name,p.is_sample"
# 期望: name=张思源, is_sample=true, record_count=6
# AI 预置文案非空(旧版验收项,保留)
sudo db-query test "SELECT ai_analysis_text IS NOT NULL, store_conclusion_pushed_text IS NOT NULL FROM refractive_records WHERE patient_id=(SELECT id FROM patients WHERE is_sample=true) ORDER BY visit_at DESC LIMIT 1"
# 期望: 两列均为 true
# 【口径19 新增】6 条记录必须每条都是 done,不是只有最后一条
sudo db-query test "SELECT visit_at, ai_analysis_status, (ai_analysis_text IS NOT NULL) AS has_text FROM refractive_records WHERE patient_id=(SELECT id FROM patients WHERE is_sample=true) ORDER BY visit_at"
# 期望: 返回 6 行,ai_analysis_status 全部为 done,has_text 全部为 true
# 【口径19 新增】6 份 JSON 里 plan_4l 四层都不为空(用 jsonb 校验,防止某层被漏写)
sudo db-query test "SELECT visit_at, (ai_analysis_text::jsonb->'plan_4l'->>'l1' IS NOT NULL AND ai_analysis_text::jsonb->'plan_4l'->>'l1' != '') AS l1_ok, (ai_analysis_text::jsonb->'plan_4l'->>'l4' IS NOT NULL AND ai_analysis_text::jsonb->'plan_4l'->>'l4' != '') AS l4_ok FROM refractive_records WHERE patient_id=(SELECT id FROM patients WHERE is_sample=true) ORDER BY visit_at"
# 期望: 6 行 l1_ok/l4_ok(以及未列出的 l2/l3,实施时补全)均为 true
# 【口径20 新增】只有第 6 条(最后一条)非空,前 5 条应为 NULL
sudo db-query test "SELECT visit_at, store_conclusion_pushed_at IS NOT NULL AS pushed FROM refractive_records WHERE patient_id=(SELECT id FROM patients WHERE is_sample=true) ORDER BY visit_at"
# 期望: 6 行里只有最后一行(visit_at 最大,即 2026-08-01)pushed=true,其余 5 行 pushed=false
```
风险:migration down_revision 选错(参照 store_poster_demo_flag.py 教训,7-31 生产事故);种子数据 6×15 字段手滑錯位(建议脚本写完后跑一次逐字段 diff 对照本文档表格);created_by_user_id 取值如果不慎指向真实 parent 席位,会破坏"记录不可编辑"这个依赖批2/批5 现有机制的隐式前提(scope 文档五-6 已警示);新增风险(口径19):6 份 plan_4l JSON 手写工作量不小(24 段短文案,每段 ≤80 字还要贴合当次检查的 4L 措施,不能 6 条抄同一段),建议实施时先写一份 6×4 的文案草稿表逐层过一遍字数再落库;✅ 原风险(口径20)已解决:门店结论定稿文案原与数据表 3 处数字不一致(见上方"改哪些文件"第2点),9-14 长官已拍板选 A(改文案数字、不改数据表),文案已更正,种子脚本落库时直接照更正后的文案写入即可。
不做什么:不改任何前端代码;不改 search_patients/list_my_children 查询逻辑(留给批2);不在本批决定"每机构一份 vs 全局虚拟拼接"最终方案(见第四节,本批默认按推荐方案排期,若长官改拍板需回头调整种子脚本的落库范围)。
目标:家长端"我的孩子"列表把示范档案拼进返回;门店端/家长端所有写操作接口加一道"若目标是示范档案则 403"的服务端兜底,不能只依赖前端拦截。
改哪些文件:
app/api/parent_patients.py:217-233(list_my_children)—— 在 svc.search_patients(...) 返回结果之后,查询本机构 is_sample=true 的那条 patient,拼进返回列表末尾,且要覆盖"真实列表为空"和"真实列表非空"两种情况都要展示(对应拍板项11 + 方案图第382行"对所有家长恒常展示")。app/api/parent_patients.py:882 起(create_my_child_record)—— 函数体前段加一行判断:若 patient.is_sample 为真,raise HTTPException(403, "示范档案不可新增检查")。app/api/parent_patients.py:1050-1060(PATCH 更新记录的既有 403 判据附近)—— 核实批1种子数据的 created_by_user_id 取值确实会让 _record_editable_by_parent(:869-878)返回 False(七§9 已贴函数体),若成立则这条路径不需要新写代码,只需要在批2验收里加一条"确认现有机制生效"的检查项,不当作新开发工作量。门店端写接口(app/api/refractive_records.py / 对应 app/services/refractive_record_service.py 里的 create_record/update_patient/bind_patient_seat/unbind_patient_seat/delete_record/patch_conclusion/push_conclusion)—— 本计划已精确定位 trigger_analysis(app/api/refractive_records.py:311,七§4)需加 is_sample 判断;其余 6 个接口的精确 file:line 本计划未逐一定位,需实施时对照批3前端按钮清单逐个 grep 落实,标注"未找到,实施时需确认"。
验收标准:
```
# 零档案家长测试账号 → 列表应含 1 条 is_sample
curl -s -H "Authorization: Bearer <家长token>" "$API/api/parent/patients" | jq '.[] | select(.is_sample==true)'
# 期望: 有输出
# 尝试对示范档案新增检查 → 403
curl -s -o /dev/null -w "%{http_code}" -X POST -H "Authorization: Bearer <家长token>" \
"$API/api/parent/patients/<示范patient_id>/records" -d '{}'
# 期望: 403
# 门店端尝试触发重新分析 → 403(需先确认该 endpoint 已加判断)
curl -s -o /dev/null -w "%{http_code}" -X POST -H "Authorization: Bearer <门店token>" \
"$API/api/h5/refractive-records/records/<示范record_id>/analysis"
# 期望: 403
```
风险:门店端写接口数量本计划未逐一核实,可能有遗漏(比如"解绑"接口是否也要挡,虽然示范档案本来就不该被绑定);list_my_children 拼接逻辑要注意分页参数(offset/limit)不要把示范档案挤出真实数据分页范围。
不做什么:不改前端;不改 search_patients 的 org_id 强制过滤逻辑(除非第四节技术决策改成"全局虚拟拼接"方案)。
目标:患者列表页顶部渲染示范档案区(视觉照抄方案图令牌);9 个写操作按钮加 is_sample 判断,点击后弹提示不执行原逻辑。
改哪些文件:
web/app/h5/tablet/pane/panes/refractive-record/frame1-child-list.tsx:88-93 附近(patients.map 渲染前)—— 新增示范档案区块,视觉令牌必须原样照抄方案图 <style> 段(已 grep 核实,七§12):.demo-badge(background:#fef3c7; color:#b45309; box-shadow: inset 0 0 0 1px #fde68a)、.demo-card(border:1px solid #fde68a; border-left:3px solid #f59e0b)、分隔线 background:#e6edf3; height:1px,不得自造色值。frame3-patient-profile.tsx:195,204,228,246(修改档案/新增检查/解绑/绑定 4 个按钮)—— 各自 onClick 里加"若 patient.is_sample → 弹提示 → return"。frame4-record-view.tsx:1187,1321,1333,1343,1407(编辑/重新生成×3处/保存结论 5 个按钮)—— 同上判断。若 is_sample 字段未在 patient 对象透传到 frame3/frame4/frame5 组件层,需要在 refractive-record-pane.tsx 补一处 props 透传(本计划未逐一核实该文件当前 props 结构是否已含 patient 全量字段,标注"需实施时确认")。
验收标准:门店端 test 环境登录任一机构账号,进患者列表 → 看到示范档案区(金色徽标+浅灰分隔线,风格与真实列表区分)→ 点开示范档案详情 → 依次点 9 个按钮,每次都弹提示、URL/数据均无变化(用浏览器网络面板确认没有对应的 PATCH/POST 请求真正发出)。
风险:9 处判断分散在 3 个文件,容易漏改一处,建议改完后 grep -c "is_sample" 数一遍确认命中数 ≥ 9(每处判断至少引用一次该字段)。
不做什么:不动"新建档案""搜索"等公共入口(拍板项7明确不拦);不动机构管理后台等其他统计报表(scope 文档五-5 已提示需另查,不在本批范围)。
目标:个人档案列表页渲染示范档案区 + 调整零档案空态卡文案 +「添加检查单」按钮拦截 + 分享卡片机构名替换为「示范档案机构」。
改哪些文件:
web/app/h5/family/page.tsx:127-181(children.length > 0/children.length === 0 两个互斥分支,七§10 已证实是两个独立 if 块)—— 需要重构成"示范档案区在外层恒常渲染,两个分支各自只管真实列表/空态卡各自的那部分",不能只在其中一个分支里加。空态卡文案(现网"暂无儿童档案"一类文案,具体文案本计划未逐字核实原文,实施时对照现有文案微调)按方案图第382行的建议改为提示"以下为示范档案,添加孩子后显示您的真实档案"一类表述。web/app/h5/family/[id]/page.tsx:2422-2427(添加检查单按钮)—— 加 patient.is_sample 判断,弹提示不跳转。web/app/h5/family/[id]/page.tsx:742,1132,1440,1967(4 处 orgName 取值,七§8 已核实四处写法完全一致)—— 需要在这 4 处让取值链条对示范档案统一产出「示范档案机构」,而不是真实机构名或兜底的「赵阳智脑」。必须 4 处同步改,改完用 grep -c "orgDisplayName || getOrgNameFromSeatToken" 确认命中数不变、但新增了示范档案分支判断的行数一致(4处)。web/app/h5/family/page.tsx:233-246(「编辑档案」pill,当前渲染条件为每张真实卡片恒常显示)—— 示范档案卡片同样要渲染这颗 pill~~。口径22(9-14 最新拍板)改为:family/page.tsx:233-246 处的「编辑档案」pill 示范档案卡片不渲染(patient.is_sample 为真时该 pill 分支直接跳过,列表卡片只保留「示范档案」+「只读」两枚徽标,原因是长官实际查看效果图后判断卡片空间放不下,非逻辑判断变化);真实档案该处逻辑不变。【9-14 长官四次拍板 · 口径22 新增】 web/app/h5/family/[id]/page.tsx(档案详情页)—— ⚠️ 物理核实:现网该文件目前没有独立的"编辑档案"入口(现有「编辑」按钮 :2757 是检查记录级、批5范围,与编辑档案是两个功能)。需新增一个「编辑档案」UI 元素,样式对齐现有列表 pill 令牌(Pencil w-3 h-3 strokeWidth 2.5 + text-[11px] font-bold text-[#157c67] bg-[#e7f5ee] rounded-full px-1.5 py-0.5 gap-1),放置位置本计划未定(建议紧邻患者姓名/头像区,具体位置需实施前对照现网详情页头部结构定点);onClick 按 patient.is_sample 分流 —— 示范档案弹提示"示范档案不可编辑",真实档案沿用列表页原有 router.push('/h5/family/new?pid=...') 跳转逻辑。这是本次口径22新增的实施工作量,不是简单加一行判断。
验收标准:
# 零档案家长测试账号打开个人档案列表 → 看到示范档案卡(金色徽标)排在空态卡上方
# 点击「添加检查单」→ 弹提示,不跳转 exam/new
# 打开分享 → Canvas 卡片上机构名渲染为「示范档案机构」,不是真实机构名
# 【口径22 新增】个人档案列表页示范档案卡片 → 只显示「示范档案」+「只读」两枚徽标,不渲染「编辑档案」pill
# 【口径22 新增】点进示范档案详情页(family/[id]/page.tsx)→「编辑档案」入口照常显示,点击弹提示"示范档案不可编辑",不跳转 family/new
# 【口径22 新增】真实档案对照组:列表卡片「编辑档案」pill 照常显示且可点击跳转 family/new?pid=X(确认改动未误伤真实档案)
这几项需要真机/Playwright 渲染截图确认(CSS/Canvas 绘制内容用 innerText 看不出,按 ~/.claude/rules/prototype-mockup-standards.md 铁律必须真渲染)。
风险:✅ 已按口径22重新闭环 —— 【中】列表卡片级「编辑档案」pill 与口径7层级差异:原方案图(七§13)显示示范卡片在列表卡片层级不渲染真实卡片才有的「编辑档案」pill(只显示一个"只读" pill),这与口径7"按钮不隐藏、点了弹提示"字面上作用的层级是否一致此前存疑。9-14 长官第一次拍板(口径21,已被口径22覆盖,原文保留作决策记录)明确"编辑按钮要留啊,只是点击的时候弹出提示"——列表层级也不例外,示范档案卡片的「编辑档案」pill 与详情页内部按钮口径统一显示。长官实际查看效果图后 9-14 五次拍板(口径22,覆盖口径21):判断列表卡片空间放不下该 pill,改为列表层级不显示(卡片只留「示范档案」+「只读」两枚徽标),详情页层级照常显示(需在 family/[id]/page.tsx 新增该入口,见本批「改哪些文件」第 5 条)。方案图已按口径22同步改回不显示(方案图第270-281行),本条不再是待确认项,原文保留作决策记录。
不做什么:不在本批处理批5(记录级编辑按钮)的通用逻辑变更;不改家长自建档案(family/new/page.tsx)的表单逻辑(示范档案是门店建的,不会流经这条路径)。
目标:落实长官 9-14 二次拍板口径14 —— 把 web/app/h5/family/[id]/page.tsx:2748-2757 的 {detail.editable !== false && (...编辑按钮...)} 改成"按钮恒常渲染,editable===false 时点击弹提示,不再直接隐藏"。
改哪些文件:web/app/h5/family/[id]/page.tsx:2748-2757,单处改动。
验收标准:
# 找一个现网真实存在的、由门店代录记录的家长档案(非示范档案)
curl -s -H "Authorization: Bearer <该家长token>" "$API/api/parent/patients/<pid>/records/<rid>" | jq '.editable'
# 期望: false(该记录门店代录,editable 天然为 false)
# 家长端页面此时应显示「编辑」按钮(不再隐藏),点击后弹提示而非跳转编辑页
风险(五节已列为最高优先级):_record_editable_by_parent(app/api/parent_patients.py:869-878)是现网通用机制,不是示范档案专属代码路径。这处改动同时影响生产环境所有真实家长用户查看自己孩子被门店录入的记录时的按钮可见性——这是一次现网交互行为变更,不是"加个 if 分支"就完事。建议实施前先跑一次 SQL 摸底现网影响面:
sql
-- 建议实施前跑(test 库先验证语法,生产库只读查询):
SELECT COUNT(DISTINCT r.patient_id)
FROM refractive_records r
JOIN users u ON u.id = r.created_by_user_id
WHERE u.seat_type != 'parent';
-- 摸底:现网有多少个家长档案下存在"门店代录"的记录,即本次改动实际会让多少家长首次看到「编辑」按钮
不做什么:不与批1-4 同一个 test MR 发布(遵循项目"发版少发、攒几个功能一起发"的原则做批次隔离是反过来的用法——这里是反向隔离:因为批5的风险性质与示范档案完全不同[一个是新功能上线、一个是现网行为变更],混在一起会让"发生产"时无法单独回滚其中一项,故建议单独走一次评审 + 单独发布节奏,不代表要拆成两次发版申请,具体发版节奏由长官定)。
已拍板,实施时照做别再问(摘自 scope 文档 18 条口径):
- 只保留一个孩子:张思源,男,8 岁起建档
- 统一文案:徽标「示范档案」,区块标题「示范档案区」
- 6 条检查记录,间隔 3 个月,全字段写全(具体数值见 scope 文档四节表格,本计划批1种子脚本直接照抄,不得改动)
- 数据走势:前期眼轴增长快(0.40mm/年)→干预后明显趋缓(约0.227mm/年,降幅约43%)
- 干预手段用 4L 体系无品牌选项(离焦镜/其他Led红光/全光谱吸顶灯/其他读写台),禁用 OK 镜与阿托品(已 grep 核实 prevention.ts 这些枚举值真实存在,七§7)
- 门店端和家长端都展示;家长端只放门店录入的记录
- 完全只读:不能编辑/新增/删除,不做关闭开关
- 交互:按钮全部显示 + 点了弹提示(两端一致),公共功能(新建档案/搜索)不拦
- AI 分析:直接展示预置结论,不调模型;「重新生成」按钮保留,点了弹提示
- 分享报告保留可正常分享;分享卡片机构名改为「示范档案机构」(9-14 定稿,非此前口径的「示范档案」)
- 家长端记录级「编辑」按钮统一改为"不隐藏,点了弹提示"(口径14,批5单独实施)
- 【口径19】6 条检查记录每条都要有 AI 分析,不是只做最后一条:落在 RefractiveRecord.ai_analysis_text/ai_analysis_status/ai_analysis_updated_at 三字段(app/models/refractive_record.py:148,151,154),ai_analysis_text 是 JSON 字符串(结构见批1「改哪些文件」第3点),plan_4l 四层每层 ≤80 字、缺一不可,6 条 ai_analysis_status 全部预置为 done;⚠️ 该字段严禁下发到家长端任何接口响应(已 grep 核实两处禁令注释)
- 【口径20】最后一条记录要把门店结论推送给家长:落在 store_conclusion_text/store_conclusion_pushed_text/store_conclusion_pushed_at 三字段(:157,160,163),推送 = 草稿拷贝成快照 + 打时间戳,不发任何通知(已 grep 验证 0 命中);长官定稿文案见 scope 文档四节,✅ 文案原有 3 处数字与最终数据表出入,9-14 长官已拍板选 A 改文案数字,现已一致(见批1 风险)
- 【口径21 · ⚠️已被口径22覆盖,原文保留作决策记录,勿按本条实施】列表卡片层级的「编辑档案」pill 同样不例外:长官原话"编辑按钮要留啊,只是点击的时候弹出提示"。此前方案图与口径7曾存在"详情页按钮级 vs 列表卡片层级"的层级理解分歧(见五-4、六-3),9-14 长官第一次拍板明确拍板消除该分歧:家长端个人档案列表页示范档案卡片的「编辑档案」pill 照常显示,门店端患者列表行如有对应操作入口同样显示(经核实门店端患者列表真实行本就无编辑入口,不涉及);点击一律弹提示不可编辑,不隐藏、不跳转编辑页。方案图曾按此同步落实(第270-278行 + 口径表第391行),该决定已被下方口径22推翻
- 【口径22,覆盖口径21】家长端列表卡片「编辑档案」pill 改回不显示,详情页层级照常显示:长官实际查看效果图后判断示范卡片在列表卡片层级空间放不下该 pill,拍板列表卡片层级不再显示「编辑档案」pill(卡片只留「示范档案」+「只读」两枚徽标);但改动仅限列表卡片层级——进入档案详情页后,「编辑档案」入口照常显示,点击同样弹提示不可编辑,不跳转真实编辑流程,与口径7详情页层级要求一致。列表层级空间受限的例外 ≠ 逻辑上"无入口只读"。⚠️ 物理事实:现网 family/[id]/page.tsx 目前没有独立的「编辑档案」入口,需新增(见批4「改哪些文件」第5条)。方案图已按口径22同步改回不显示该 pill(方案图第270-281行 + 口径表第391行)(已拍板,勿再问)
本计划新增的推荐方案(非长官已拍板,标注清楚,详见第六节):
- 数据模型走"每机构一份真实 DB 行"路线,理由:search_patients 强制按 org_id 过滤(七§10 已核实签名),走这条路线门店端搜索天然能带出示范档案,不需要改 search_patients 逻辑本身;代价是需要一个"机构创建时自动播种 + 存量机构一次性回填"的机制(批1第4条,未定位机构创建入口,待实施时确认)。若长官更倾向"全局唯一一份、接口层虚拟拼接"方案,批1的种子脚本落库范围与批2的 list_my_children 拼接逻辑都需要相应调整,但批3/批4/批5 的前端改动不受影响。
_record_editable_by_parent 判据是现网通用机制,详见批5小节,建议先摸底 SQL 影响范围再排期发布。trigger_analysis 一处 endpoint,其余 6 个(新增检查/修改档案/绑定/解绑/保存结论/保存记录)的精确 file:line 需实施时对照前端按钮反查落实,可能有遗漏。family/[id]/page.tsx 新增该入口,现网物理没有,见批4「改哪些文件」第5条,是本次口径22新增的实施工作量)。方案图已同步改回不显示该 pill,不再是排期发布前的风险项,原文保留作决策记录。page.tsx:742,1132,1440,1967 写法完全一致,七§8),改漏一处会导致分享卡片机构名不统一。created_by_user_id 取值依赖一个隐式行为:该取值间接决定 _record_editable_by_parent 返回 False,这是"顺带满足了需求"而非专门为示范档案设计的机制,后续如果 _creator_seat_type 判据逻辑改动,示范档案的只读性可能被意外打破(scope 文档五-6 已提示,批1实施时需要在种子脚本注释里写清楚这层依赖)。web/app/h5/tablet 目录下无"患者总数"类展示(七§6 对照组已验证查法有效),机构管理后台等其他统计报表未核实,若存在会被示范档案的计数污染 +1。plan_4l JSON 是手写工作量,不是简单填字段:每条记录 4 层 × ≤80 字,共 24 段短文案,还要分别贴合该条检查当次实际使用的 4L 措施(不能 6 条复制粘贴同一套文案,否则示范档案的"AI 分析"看起来像假的),批1 工作量估计已上调,实施时建议先出一份草稿表过一遍字数再落库。family/[id]/page.tsx 物理没有该入口,需新增(见批4「改哪些文件」第5条),不再是待确认项。$ sed -n '40,56p' app/models/flywheel.py
# ═══ [Phase 74 · 家长子席位档案归属] ═══
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',
)
guardian_relation = Column(...)
id_card_no = Column(...)
$ sed -n '217,233p' 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,
)
$ grep -n "新建档案\|children.length" web/app/h5/family/page.tsx
127: {!loading && !loadError && children.length > 0 && (
136: 新建档案
151: {!loading && !loadError && children.length === 0 && (
$ grep -n "trigger_analysis\|async def trigger" app/api/refractive_records.py
311:async def trigger_analysis(
$ sed -n '307,319p' app/api/refractive_records.py
@router.post("/records/{record_id}/analysis")
async def trigger_analysis(
record_id: UUID,
seat: dict = Depends(require_store_seat),
db: AsyncSession = Depends(get_db),
) -> dict[str, Any]:
"""手动触发一次赵阳智脑分析生成(不消耗秒读额度 · 异步生成 · 立即返回 pending)"""
await svc.require_feature(db, seat["org_id"])
return await svc.trigger_analysis(db, seat["org_id"], record_id)
$ grep -n "onOpenEditPatient\|onNewRecord\|onUnbind\|onOpenBindSelector" web/app/h5/tablet/pane/panes/refractive-record/frame3-patient-profile.tsx
195: onClick={onOpenEditPatient}
204: onClick={onNewRecord}
228: onClick={onUnbind}
246: onClick={onOpenBindSelector}
$ grep -n "onEdit\|handleGenerateAnalysis\|handleSaveConclusion" web/app/h5/tablet/pane/panes/refractive-record/frame4-record-view.tsx
1187: <button type="button" onClick={onEdit}
1321,1333,1343: <button type="button" onClick={() => void handleGenerateAnalysis()}
1407: <button type="button" onClick={() => void handleSaveConclusion()}
$ grep -n "onSave" web/app/h5/tablet/pane/panes/refractive-record/frame5-record-entry.tsx
750: <button type="button" onClick={onSave} disabled={saving}
$ grep -rln "患者总数\|总人数\|档案总数\|患者数" web/app/h5/tablet --include=*.tsx
(0 命中)
$ grep -rl "新建档案" web/app/h5/tablet --include=*.tsx | wc -l
2
(对照组命中 2 → 证明查法有效,"0 命中"是真的没有;但仅核实 tablet 目录,机构管理后台等其他统计报表未核实,见五-8)
$ grep -n "离焦镜\|其他Led红光\|全光谱吸顶灯\|其他读写台\|OK镜\|阿托品" web/app/h5/family/\[id\]/exam/new/prevention.ts
16: "离焦镜",
18: "OK镜",
30: "其他Led红光",
34: "全光谱吸顶灯",
56: "其他读写台",
65: "0.01%低浓度阿托品",
$ grep -n "orgDisplayName\|getOrgNameFromSeatToken" web/app/h5/family/\[id\]/page.tsx
99:function getOrgNameFromSeatToken(): string | null {
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 "_record_editable_by_parent\|_creator_seat_type\|create_my_child_record\b" app/api/parent_patients.py
857:async def _creator_seat_type(db: AsyncSession, created_by_user_id: int) -> Optional[str]:
869:async def _record_editable_by_parent(db: AsyncSession, created_by_user_id: int) -> bool:
878: return await _creator_seat_type(db, created_by_user_id) == "parent"
882:async def create_my_child_record(
$ sed -n '1058,1060p' app/api/parent_patients.py
if not await _record_editable_by_parent(db, rec.created_by_user_id):
raise HTTPException(status_code=403, detail="该记录由门店录入,家长端不可修改")
$ grep -n "添加检查单\|exam/new" web/app/h5/family/\[id\]/page.tsx
2422: onClick={() => router.push(`/h5/family/${pid}/exam/new`)}
2427: 添加检查单
2752: router.push(`/h5/family/${pid}/exam/new?rid=${rid}`);
$ sed -n '166,178p' 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,
include_bound: bool = False,
offset: int = 0,
) -> list[dict[str, Any]]:
$ grep -n "is_demo_store" app/models/database.py
106: is_demo_store = Column(
$ head -30 migrations/versions/store_poster_demo_flag.py
"""体验演示门店标记 · 门店海报强制统一昵称
...
🔴 down_revision 为什么是 phase63_002 而不是建迁移时的队尾(7-31 生产事故教训):
本迁移最初写 down_revision = "phase66_001"(建它时 test 的队尾)。但选择性晋级模式下
**main 的迁移集是 test 的子集** —— ... 单独把本迁移晋级 main 后,生产 alembic 找不到
父节点,整棵修订树加载失败(KeyError: 'phase66_001')...
关键认知:down_revision 是**排队号不是依赖声明**。
"""
revision = "store_poster_demo_flag"
down_revision = "phase63_002"
$ grep -n "demo-badge {" -A 4 /home/dev2/reports/refractive-record-demo-single-child-20260914-b-gold.html
66: .demo-badge {
67- padding:3px 12px; letter-spacing:.2px;
68- background:#fef3c7; color:#b45309;
69- box-shadow: inset 0 0 0 1px #fde68a, 0 1px 2px rgba(180,83,9,.10);
$ grep -n "demo-card {" -A 4 /home/dev2/reports/refractive-record-demo-single-child-20260914-b-gold.html
71: .demo-card {
72- border-radius:18px; overflow:hidden; background:#fff; border:1px solid #fde68a;
73- border-left:3px solid #f59e0b;
$ grep -n "e6edf3" /home/dev2/reports/refractive-record-demo-single-child-20260914-b-gold.html | head -3
12: border:1px solid #e6edf3;
151: <div style="height:1px; background:#e6edf3; margin:14px 2px;"></div>
303: <!-- 示范档案区与真实档案的分隔线(与门店端同款·同一令牌 #e6edf3) -->
$ sed -n '135,141p' /home/dev2/reports/refractive-record-demo-single-child-20260914-b-gold.html
<span class="pill-neutral demo-badge">示范档案</span>
<span style="font-size:12.5px; color:#43617a;">男 · 9岁3个月 · 2017-03-15</span>
<span style="font-size:12.5px; color:#43617a;">· 138****2201</span>
</div>
<span style="flex-shrink:0; font-size:12px; white-space:nowrap; color:#8aa0b3;">最近检查 08-01</span>
<span class="pill-neutral" style="flex-shrink:0; font-size:11px; padding:2px 10px;">只读</span>
$ grep -n "编辑档案.*显示\|示范卡片不显示" /home/dev2/reports/refractive-record-demo-single-child-20260914-b-gold.html
385: ...每张真实卡片都显示(批 P 起不再只有第一张)。示范卡片不显示 —— 9-14 拍板...
$ 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 '2579,2581p' app/services/refractive_record_service.py
rec.store_conclusion_pushed_text = rec.store_conclusion_text
rec.store_conclusion_pushed_at = datetime.now(timezone.utc)
# (函数体内 0 处调用 sms/wechat/notify/send_message 类接口 · 已 grep 核实)
$ python3 -c "
rows={1:(23.20,23.15),3:(23.40,23.35),6:(23.57,23.52)}
od1,os1=rows[1]; od3,os3=rows[3]; od6,os6=rows[6]
print('总增量 OD/OS:', round(od6-od1,2), round(os6-os1,2))
print('半年增量 OD/OS:', round(od3-od1,2), round(os3-os1,2))"
总增量 OD/OS: 0.37 0.37
半年增量 OD/OS: 0.2 0.2
# 长官定稿文案写"一共长了0.60mm"/"半年长了0.22mm",与上面实算的 0.37/0.20 不符
✅ 已解决:9-14 长官拍板选 A(改文案数字、不改数据表),批1种子数据文案已更正为"一共长了0.37mm"/"半年长了0.20mm"/"去年5月建档",与本节实算结果一致。上方代码块为当时发现问题的原始核实记录,保留不改。