交付出件箱

← 返回出件箱

示范档案 · 开发计划(2026-09-14)

依据材料:/home/dev2/reports/demo-profile-scope-20260914.md(长官 18 条拍板口径)+
/home/dev2/reports/refractive-record-demo-single-child-20260914-b-gold.html(定稿方案图,仅两节)。
本文档所有改动点均经本会话独立物理 grep 重新核实(不转抄 scope 文档结论),证据见第七节。
本文档只写计划,不改业务代码、不 commit、不发版、不碰数据库。

一、一句话说清要做什么(给长官,3 句以内)

给每个机构固定放一份只读的"张思源"示范视光档案(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:数据层(is_sample 标记 + 种子数据)

# 种子脚本跑完后确认数据落地
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
```

批2:后端接口(注入 + 写操作拦截)

# 尝试对示范档案新增检查 → 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
```

批3:门店端(平板)前端

批4:家长端前端(示范档案展示 + 专属拦截)

批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 的前端改动不受影响。

五、风险清单(按严重度排序)

  1. 【高】批5 影响面覆盖全部生产真实家长用户,非示范档案专属:_record_editable_by_parent 判据是现网通用机制,详见批5小节,建议先摸底 SQL 影响范围再排期发布。
  2. 【高】数据模型技术方案未拍板:每机构一份真实行 vs 全局虚拟拼接,决定批1、批2的具体实现方式,建议实施前先定(见第四节推荐方案 + 第六节)。
  3. 【中】门店端后端写接口清单未逐一定位:批2只精确定位了 trigger_analysis 一处 endpoint,其余 6 个(新增检查/修改档案/绑定/解绑/保存结论/保存记录)的精确 file:line 需实施时对照前端按钮反查落实,可能有遗漏。
  4. ✅ 已按口径22重新闭环 —— 【中】列表卡片级「编辑档案」pill 与详情页按钮级"不隐藏"口径的层级差异:原方案图显示示范卡片在列表层级不显示编辑入口(只显示"只读" pill),这与口径7字面表述的层级是否一致此前存疑。9-14 长官第一次拍板(口径21,已被口径22覆盖,原文保留作决策记录):列表层级也不例外,「编辑档案」pill 照常显示。长官实际查看效果图后 9-14 五次拍板(口径22,覆盖口径21):列表层级空间放不下,改回不显示;但详情页层级照常显示(需在 family/[id]/page.tsx 新增该入口,现网物理没有,见批4「改哪些文件」第5条,是本次口径22新增的实施工作量)。方案图已同步改回不显示该 pill,不再是排期发布前的风险项,原文保留作决策记录。
  5. 【中】分享卡片机构名替换点分散:4 处独立 Canvas 绘制取值需同步改(已 grep 核实 page.tsx:742,1132,1440,1967 写法完全一致,七§8),改漏一处会导致分享卡片机构名不统一。
  6. 【中】"新增检查"类接口后端兜底缺失是现网既有问题,不只是示范档案的问题:批2要补的后端校验本身是"防绕过前端拦截"的通用安全加固,建议实施时同时确认这条校验不会误伤真实档案的正常新增检查流程。
  7. 【低】种子数据 created_by_user_id 取值依赖一个隐式行为:该取值间接决定 _record_editable_by_parent 返回 False,这是"顺带满足了需求"而非专门为示范档案设计的机制,后续如果 _creator_seat_type 判据逻辑改动,示范档案的只读性可能被意外打破(scope 文档五-6 已提示,批1实施时需要在种子脚本注释里写清楚这层依赖)。
  8. 【低】统计类展示范围未全面核实:仅核实了 web/app/h5/tablet 目录下无"患者总数"类展示(七§6 对照组已验证查法有效),机构管理后台等其他统计报表未核实,若存在会被示范档案的计数污染 +1。
  9. 【中】口径19 的 6 份 plan_4l JSON 是手写工作量,不是简单填字段:每条记录 4 层 × ≤80 字,共 24 段短文案,还要分别贴合该条检查当次实际使用的 4L 措施(不能 6 条复制粘贴同一套文案,否则示范档案的"AI 分析"看起来像假的),批1 工作量估计已上调,实施时建议先出一份草稿表过一遍字数再落库。
  10. ✅ 已闭环 —— 【中】口径20 门店结论定稿文案与数据表存在 3 处数字不一致(9-14 新发现,当日已拍板解决):建档月份(3月 vs 表格5月)、总增量(0.60mm vs 实算0.37mm)、半年增量(0.22mm vs 实算0.20mm),详见批1「改哪些文件」第2点 + scope 文档四节核对表。9-14 长官已拍板选 A(改文案数字、不改数据表),批1文案已更正为 5月/0.37mm/0.20mm,与数据表一致,不再是排期发布前的风险项。

六、还没定的

  1. 数据模型走哪条路:每机构一份真实 DB 行 vs 全局唯一一份接口层虚拟拼接(第四节已给推荐方案,待长官/实施前拍板)。
  2. 二维码是否在分享卡片显示:长官原话"建议不显示",标注为待最终确认。
  3. ~~列表卡片层级的编辑入口与详情页按钮级"不隐藏"口径的适用范围~~ —— ✅ 已按口径22重新解决:9-14 长官第一次拍板(口径21,已被口径22覆盖,原文保留作决策记录):「编辑按钮要留啊,只是点击的时候弹出提示」,列表卡片层级同样不例外照常显示。长官实际查看效果图后 9-14 五次拍板(口径22,覆盖口径21):列表卡片层级空间放不下,改回不显示该 pill(卡片只留「示范档案」+「只读」两枚徽标);详情页层级照常显示,点击弹提示不可编辑——现网 family/[id]/page.tsx 物理没有该入口,需新增(见批4「改哪些文件」第5条),不再是待确认项。
  4. 批5 是否需要先完成 SQL 摸底再排期,还是直接按拍板口径全量切换(第三节批5"不做什么"已说明发布节奏由长官定,但"要不要先摸底"本身也是一个待决策项)。
  5. 门店端写接口的完整清单:批2里 6 个后端 endpoint 的精确 file:line 待实施时逐一 grep 确认,本计划未能在派单前锁定,列为遗留核实项而非"待拍板"事项。

七、附:物理核实证据

§1 Patient 模型字段(is_sample 拟插入位置)

$ 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(...)

§2 list_my_children 注入点

$ 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,
    )

§3 家长端列表页组件

$ grep -n "新建档案\|children.length" web/app/h5/family/page.tsx
127:        {!loading && !loadError && children.length > 0 && (
136:            新建档案
151:        {!loading && !loadError && children.length === 0 && (

§4 AI 分析触发接口精确定位

$ 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)

§5 门店端写操作按钮定位(9处)

$ 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}

§6 统计展示范围(对照组验证)

$ grep -rln "患者总数\|总人数\|档案总数\|患者数" web/app/h5/tablet --include=*.tsx
(0 命中)
$ grep -rl "新建档案" web/app/h5/tablet --include=*.tsx | wc -l
2
(对照组命中 2 → 证明查法有效,"0 命中"是真的没有;但仅核实 tablet 目录,机构管理后台等其他统计报表未核实,见五-8)

§7 4L 防控措施枚举真实存在(禁用项也真实存在,证明查法有效)

$ grep -n "离焦镜\|其他Led红光\|全光谱吸顶灯\|其他读写台\|OK镜\|阿托品" web/app/h5/family/\[id\]/exam/new/prevention.ts
16:      "离焦镜",
18:      "OK镜",
30:      "其他Led红光",
34:      "全光谱吸顶灯",
56:      "其他读写台",
65:      "0.01%低浓度阿托品",

§8 分享卡片机构名 4 处取值点

$ 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() || "赵阳智脑";

§9 家长端记录级只读判据(批2/批5 都依赖的机制)

$ 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}`);

§10 search_patients 强制 org_id 隔离(支撑第四节推荐方案)

$ 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]]:

§11 migration 命名先例(is_demo_store,批1 参照写法 + down_revision 教训)

$ 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"

§12 方案图视觉令牌(批3/批4 必须原样照抄的色值)

$ 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) -->

§13 列表卡片级"只读"pill vs 详情页按钮级"不隐藏"的层级差异(本计划核实时发现,列入五-4/六-3)

$ 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 拍板...

§14 口径19/20 物理落点(9-14 本会话补充核实,支撑批1种子数据要求)

$ 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月建档",与本节实算结果一致。上方代码块为当时发现问题的原始核实记录,保留不改。