交付出件箱

← 返回出件箱

两端新首页 + 家长打卡 + 互扫绑定 · 实施梳理与影响面 v2

⚠️ 9-24 更正:本文第 4.3 节与 H8「红线文件冻结、认领后查不出来」有误 —— 可见性 6 处已于 9-17 上生产;真正的坑是另两道门,见 home-redesign-decisions-v3-20260924.md 第二节。

2026-09-23 · 效果图 zhaoyang-home-6styles-20260921-flat1.html
基线:origin/test(本 worktree 是 main,落后 test 1362 commit,实施必须从 test 拉分支)
方法:8 路并行子代理物理 grep + 主会话复核 + 与 Phase 83 批次五会话直接对接
前作:home-redesign-verify-20260923.md(v1,本文件更正其中一处并大幅扩写)


零、先更正 v1 的一处错误

v1 写「Phase 83 批次五要改 chat/page.tsx:791 家长端密码门」—— 这条不成立。

物理核实:

$ sed -n '789,791p' web/app/h5/chat/page.tsx
    // Phase 46.8 · 席位门控决策(阻塞式 · 替代 magic-link)
    //   enable_password_login=true 且本设备无有效 seat token → 弹阻塞登录窗
    //   家长流(enable_password_login=false)→ 永不开门(红线:原逻辑不变)

$ grep -rn "enable_password_login" app/models/database.py
94: comment="是否开启多租户用户密码登录(首页密码登录入口 · 功能待实现 · 当前仅存配置)"

这段管的是 Phase 46.8 时代的 role='h5_user' 老家长,不是 Phase 74 的家长子席位;而且这个开关在模型里明写「功能待实现 · 仅存配置」。批次五那边也已自行纠正过这条(它一开始按字面理解,被长官当场纠正)。

真实撞车点是 :861(品牌码微信失效兜底)、:930-980(微信身份带入)、以及批次五要新建的「点任意功能全量拦截」gate。


一、五件事的关系(一张图看懂先后)

                 ┌─────────────────────────────────────────┐
                 │  硬前置 P0 · 登录层抽取(370行鉴权gate)    │  ← 不做这个,两个首页都落不了地
                 └───────────────┬─────────────────────────┘
                                 │
        ┌────────────────────────┼────────────────────────┐
        ▼                        ▼                        ▼
 ┌─────────────┐        ┌─────────────┐          ┌──────────────┐
 │ 件1 家长端   │        │ 件1 门店端   │          │ Phase83 批次五│
 │ 新首页       │        │ 新首页       │          │ (登录体系改造) │
 └──────┬──────┘        └──────┬──────┘          └──────┬───────┘
        │                      │                         │
        │  需要               │  需要                    │ 会在每个功能入口
        ▼                      ▼                         ▼ 挂全量拦截gate
 ┌─────────────┐        ┌─────────────┐          ←─── 必须与件1对齐接法
 │ 件2 家长打卡 │───────→│ 门店记录汇总 │
 │ (新表+新API) │  数据源 │ (依赖件2)    │
 └─────────────┘        └─────────────┘
                                 ▲
 ┌─────────────┐                 │
 │ 件3 互扫绑定 │─────────────────┘  门店端首页的"绑定家长"入口
 │ (后端已通)   │
 └──────┬──────┘
        │ 卡在两个独立前置
        ├─→ P1 扫码能力(全仓0实现)
        └─→ P2 可见性红线文件解冻(写得进查不出)

三个跨件硬前置(详见第六节):
- P0 登录层抽取 —— 两个首页的地基
- P1 扫码能力 —— 互扫绑定的地基,全仓 0 实现
- P2 触达能力 —— 「提醒未记录家长」的地基,当前几乎为零


二、件 1 · 两个新首页

2.1 做什么

家长端 11 个区块、门店端 13 个区块(v1 已逐块 verify,此处只列判定结论):

判定 家长端 门店端
✅ 直接复用 品牌头卡、档案头、孩子切换/添加、AI解读卡、常用功能入口 品牌头卡、示范档案、同款案例、常用工具 8 项
🟡 需改造 最近一次检查(补度数/视力的「较上次」)、趋势图(从 1 条扩到 3 条) 最近处理列表(补「结论已读/未读」字段)、快捷操作(新增检查缺首页级入口、海报入口要从设置页挪)
❌ 新建 消息通知条、今日护眼打卡(见件 2) 双 tab、待办聚合 API、今日数据统计 API、待跟进复查、家长记录汇总(依赖件 2)、家长连接统计 API
🛑 建议摘出 眼轴百分位 眼轴百分位

2.2 怎么做 —— 核心问题:chat/page.tsx 是"整站大门"不是"聊天页"

3412 行里焊了四样东西:

部分 内容 行号 拆分处置
AI 对话本体 messages/sendText/SSE 流式/语音 1484-1497, 2012-2286 留在对话页
首页壳 品牌头卡 StoreBrandHero、头图数据横条、底部功能胶囊条 2500-2545, 3007-3230 搬去新首页
独立挂件 历史解读、4L 报告详情、上传流程、量身定案 3299-3350 跟入口走(首页触发就搬首页)
🔴 登录大门 鉴权 gate 总控(?t= / ?code= / ?login=seat / 老URL拦截 / 席位微信OAuth / 静默重登 / 兜底失败,7+ 分支) 764-1136,370 行 抽成公共层,谁都不重写

2.3 影响谁(拆首页的完整影响面)

必须跟着改的(代码内):

文件:行 现在指向 为什么必须改
deploy/nginx/chat.tongban.love.conf:156 return 301 /h5/chat 网站根地址落地页
app/services/org_service.py:370 {host}/h5/chat?t={token} 机构 access_link 分享链接
app/api/temp_link.py:11 同上 临时链接
web/app/h5/brand-scan/page.tsx:99 /h5/chat?login=seat 品牌码扫码落地
web/app/h5/tablet/page.tsx:333 /h5/chat 平板端家长身份兜底
web/app/h5/family/page.tsx:125 /h5/chat 档案页回首页
web/app/h5/settings/page.tsx:145、ParentAccountCard.tsx:73 /h5/chat 登出落地
web/app/h5/store-refractive/page.tsx:100,199,209 /h5/chat 门禁失败/返回
web/app/h5/family/[id]/page.tsx:587 /h5/chat 鉴权失败兜底
web/app/h5/tablet/components/*(4 个首页壳组件) — 平板端是平行实现,不会自动联动,必须人工同步

🔴 会直接坏掉的(如果只删首页壳不抽登录层):
- web/app/h5/find-similar/page.tsx:37,216 和 web/app/h5/eye-nav/page.tsx:205 —— 代码注释原文「chat 有完整登录 UI 兜底」,这两个页面把 /h5/chat 当通用救援页。登录 UI 被搬走 → 用户卡在没有登录框的裸对话页。

⚠️ 代码之外的(改代码没用,必须通知运维):
- 微信公众号菜单 URL、二维码物料落地地址,配的都是 /h5/chat?login=seat 之类。在微信后台配置里,仓库 grep 不到。漏改 = 所有扫码/菜单入口全挂。

必须补的一个洞:
- session_id 只活在内存 state,不在 URL 上(grep session_id 确认无 ?session_id= 机制)。从新首页跳进对话页会开新会话、接不上刚才那段。需要新增透传或"自动选中最近会话"。
- 流式读取无 AbortController(grep AbortController 0 命中,对照组 grep -c sessionId = 19 证明查法有效)→ 聊到一半跳走,前端丢这条回复。

可以顺手变干净的:
- isParentSeat 在 chat 页驱动了 11 处分支,其中 :3013 整条功能胶囊区就是"首页入口区"(家长 4 个 / 门店 7 个,互斥)。拆成两个独立首页后这个三元判断应当消失,各自首页写死自己那套。


三、件 2 · 家长每日护眼打卡(长官已拍板:要做)

3.1 做什么

家长端:四类设备当日记录(离焦镜 h / 红光仪 次 / 护眼灯 h / 读写台 h)+ 完成度 2/4
门店端:本店家长记录汇总(已记录/未记录家长数、今日记录条数、每类设备记录人数与均值)+ 提醒未记录家长

3.2 怎么做

① 数据表:必须新建,不能扩展现有 DeviceData —— 三条依据:
- 鉴权体系不同:device_report.py:34-63 走厂商 X-API-Key/X-API-Secret,打卡要走家长登录态
- 数据语义不同:DeviceData.data 是厂商自由 JSONB 无 schema,打卡是 4 个强结构化字段
- 归属维度不同:DeviceData 按 phone 路由、无 patient_id、无按天唯一约束(同一天可插无数条)

另:DeviceTypeConfig 现有 4 个值是 desk_lamp / distance_screen / posture_sensor / outdoor_tracker(migrations/versions/015:65-71),与打卡四类完全不重合。

② 挂 patient(孩子)维度,不挂 user(家长)维度:
- 语义上"孩子今天戴了多久"属于孩子
- 家长多孩子是现成机制(parent_patients.py:223 list_my_children 按 owner_seat_id 查列表,无数量限制)
- 门店端汇总要复用现成的机构可见性判据(按 patient 过滤),挂 user 维度就得另起一套

③ 「今天」按 Asia/Shanghai 切天:
- 仓库正确惯例:activity_rollup_task.py:37,65-75、admin_activity.py:37,157-166(注释"D17 天边界")、admin_brand_code.py:253 func.date(func.timezone("Asia/Shanghai", ...))
- ⚠️ 反面教材别学:h5_chat.py:153,325、sms_verify_service.py:55 用裸 date.today(),是已有技术债

④ 门店端汇总的 WHERE 必须复用 _org_visibility_clause(refractive_record_service.py:111-131):

未被认领(service_org_id IS NULL) → 按 org_id 可见
已被认领 → 只由 service_org_id 可见,原 org_id 不再可见

自己写 Patient.org_id == org_id 会漏掉"已认领"和"家长自建"两类,统计口径跟其他门店页对不上。

⑤ 接口挂哪个中间件(app/auth/middleware.py):
- 家长端打卡提交/查询 → require_h5_user(157-322)或 require_org_seat(322-397)
- 门店端汇总/提醒 → 必须 require_store_seat(398-409,seat_type=="parent" 直接 403)—— 这是 Phase 74 专门堵"家长 token 打门店接口"的门,不能只用宽松的 require_org_seat

3.3 可复用件(比预想的多)

件 位置 复用程度
数值/时长输入 tablet/pane/panes/refractive-record/constrained-number-picker.tsx(带 min/max/step/decimals),门店端 record/new/page.tsx:520-650 已 1:1 移植过 高,改个单位就能用
底部弹层骨架 useSheetBackButton hook + upload-sheet.tsx / today-detail-sheet.tsx 等 高
四设备视觉骨架 today-band.tsx(4 卡完成度)+ today-detail-sheet.tsx 高,但只有展示、无表单交互,提交逻辑全新写

惊喜发现:today-band.tsx:44-79 的 mock 里已硬编码四个设备 label:大路灯 / 红光仪 / 离焦镜 / 读写台;today-detail-sheet.tsx 里已有「红光仪 · 650nm · 3 分钟 × 2 次 / 天」的文案 —— 跟效果图和长官口径完全一致。组件注释写着「等 Phase 21B 设备数据打通后接真实接口」,说明这套视觉件当初就是为这个场景设计的,只是一直没接后端。

⚠️ 命名待对齐:代码里叫「大路灯」,效果图叫「护眼灯」,是不是同一个东西要长官确认。

3.4 🔴 最大风险:「提醒未记录家长」的触达能力几乎为零

通道 现状 能不能用
微信订阅消息 wechat_client.py:22-46 send_subscribe_message 函数体只有一行 logger.info + 固定返回 {"errcode":0},注释写「真实 HTTP 接入将在 Go-Live 前补齐」;且 wechat_push.py 接收方是 doctor_id(医生),openid 还是 f"openid_for_{doctor_id}" 硬编码占位 ❌ 纯 stub,对任何人都不可用
短信 sms_service.py:34-77 是真实接入的阿里云 SDK(非 stub),但只有一个已注册模板("方案完成");验证码模板注释写着「TODO Go-Live:阿里云后台注册新 template + sign」;sms_notify.py:38 手机号还是硬编码 "13800138000" stub 🟡 通道真的在,但要新申请模板 + 接真实手机号 + 做限流(按条计费,门店高频点会烧钱)
站内消息 全仓无 notification/inbox 模型 ❌ 不存在

这条必须单独拍板:是走短信(要先走阿里云模板申请流程,不在开发周期内能自动完成),还是等微信订阅消息 Go-Live,还是本期先不做"主动提醒"、只做"未记录名单展示"。


四、件 3 · 双向扫码绑定

4.1 现状:只做了一半,而且是没有入口的那一半

环节 状态
门店扫家长码 · 家长码签发(jwt_local.py:251-266,HS256,TTL 24h,payload 只装 seat_id) ✅
门店扫家长码 · 后端 4 端点(brand_code_claim.py:57/87/117/161 = family-code / resolve / claim / release) ✅
门店扫家长码 · 集成测试 9 条(tests/integration/test_brand_code_claim.py) ✅
门店扫家长码 · 家长端「我的二维码」页 ❌ 全仓 0 命中
门店扫家长码 · 门店端「扫一扫」页 ❌ 全仓 0 命中
门店扫家长码 · 「已绑别家确认转入」弹窗 ❌ 不存在
门店扫家长码 · 认领后本机构档案列表可见 🟡 写得进,查不出(见 4.3)
家长扫门店码 · 后端 ❌ 不存在
家长扫门店码 · 前端 ❌ 不存在

9-17 的缺口清单 phase48a-gap-list-20260917.md:22 原文已记载此事,结论是「48B 作为独立功能目前完全不可用(无入口)…建议下批做」。

4.2 🔴 前置 P1:全项目没有任何"扫一扫"能力

$ grep -rn "jsweixin|scanQRCode|getUserMedia|html5-qrcode|jsQR|BarcodeDetector" --include=*.ts --include=*.tsx --include=*.py .
(扫码相关 0 命中;命中的是 settlement 的微信支付 WeixinJSBridge 和 test-audio 的麦克风 getUserMedia,均无关)

现有能"扫"的只有品牌码,那是微信原生相机识别二维码里的网址直接打开页面,代码侧零扫码逻辑。

→ 做这个功能第一步是先接扫码能力。走微信 JSSDK 要配 JS 安全域名 + 后端签名接口,是独立前置工作量。

4.3 🔴 前置 P2:认领了也查不出来(红线文件冻结中)

认领会写对 patients.service_org_id / service_org_bound_at,但读取过滤条件在 refractive_record_service.py::_patients_filtered_stmt —— 那是长官明令冻结的红线文件,phase48a-visibility-pending-20260916.md 列了 6 处待改,状态「已阻塞,等门店手机端视光档案那条线合并后再做」。

不解冻 = 扫码绑定做完也是白做。

4.4 ⚖️ 一个待拍板的安全口径

claim_patient(brand_code_claim_service.py:104-140):孩子已绑 A 店,B 店扫到码,只要 B 店自己把 force_transfer 置 true 就能直接抢过来,不需要 A 店或家长任何确认。测试 test_06_claim_transfer_with_force_updates_org_and_bound_at 白纸黑字验证了这个行为(不带 flag 返 409 提示,带了就直接改)。

解绑侧反而是收紧的:只有当前服务机构能解绑自己名下的(release_patient_claim:143-159,否则 409)。

4.5 顺带澄清:三条绑定关系互不联动(是有意设计)

关系 落库 写入方
孩子 ↔ 服务机构 patients.service_org_id / service_org_bound_at brand_code_claim_service.py:104/143
孩子 ↔ 家长子席位 patients.owner_seat_id refractive_record_service.py:539/587
家长微信身份 ↔ 品牌方机构 brand_code_activations 表 brand_code_service.py:116

三者代码层面互不调用(claim_patient 注释明写「本批完全不碰 owner_seat_id」),是刻意的独立判据,不是遗漏。


五、件 4 · 与 Phase 83 批次五的协同

5.1 澄清一个误会

「二维码与端口绑定断开」是遥控自动生成的乱名,跟二维码/端口无关(对面会话原话),是上一个会话(Phase 83 发生产)留下的名字。它真正在做的是:

Phase 83 批次五 = 家长席位账号加大写 T 前缀 + 新席位改用「手机号+密码」登录 + 首登强制收手机号 + 品牌码扫码家长补一条账号密码兜底 + 品牌码后台页加手机号列和重置密码按钮。

5.2 撞车面(对面已确认)

项 说明
🔴 全量拦截 gate 长官拍板「品牌码家长点任意功能就弹初始化弹窗」是全量拦截,而本仓前端没有集中式路由保护机制(各页面内联判断)。对面要新建一个共享 gate 挂到每个功能入口 —— 新首页恰好就是"一堆功能入口",两边必须对齐接法
🟠 chat/page.tsx:861 品牌码家长微信失效时的手机号+密码兜底出口
🟠 chat/page.tsx:930-980 微信身份带入(wx-identity 返回体加手机号字段)
🟠 tablet/page.tsx:322-342 平板端同款改动
🟢 对面明确不动 首页视觉/布局/卡片结构/功能入口排布

当前无实际文件冲突 —— 对面只出了方案,一行代码没改,分支干净(feature/subseat-login-v2,基线 origin/test e6770bb7)。

5.3 批次五的硬阻塞(会连带卡住我们)

🔴 #730 #731 #732 #733 必须先上生产。生产 alembic 还停在 phase48a_002,批次五的迁移要接在 phase83_001(批次三)之后,中间断一环跑不起来。这四个 MR 已在 test 验完,等长官一句话。


六、三个跨件硬前置(做任何一件之前都要先解决)

前置 挡住谁 内容 能不能并行
P0 登录层抽取 件 1(两个首页) 把 chat/page.tsx:764-1136 那 370 行鉴权 gate 抽成公共层(Next layout 或共享 hook),新首页与对话页都套在下面。必须与批次五的全量拦截 gate 对齐挂载点 必须最先做,且要跟批次五同桌定
P1 扫码能力 件 3(互扫绑定) 接微信 JSSDK(JS 安全域名 + 后端签名接口)或引扫码库。全仓 0 实现 可与 P0 并行
P2 触达能力 件 2 的"提醒未记录家长" 阿里云短信新模板申请(走运维流程)+ 真实手机号接通 + 限流设计;或等微信订阅消息 Go-Live 申请流程需提前启动,不占开发工时
P3 可见性解冻 件 3 的"认领后可见" refractive_record_service.py::_patients_filtered_stmt 红线文件 6 处待改 需长官解冻授权

七、建议批次与顺序

批 内容 前置 后端量
批 0 P0 登录层抽取(与批次五同桌定 gate 挂载点) 批次五 #730-#733 先上生产 0(纯重构)
批 1 两个新首页容器 + 已有能力重排(品牌卡、孩子切换、最近检查、AI解读、示范档案、同款案例、工具入口、快捷操作)+ 全套跳转目标改造 + 通知运维改公众号菜单 URL 批 0 0(纯前端)
批 2 三个只读聚合端点(门店待办 3 类 / 今日数据 4 项 / 家长连接统计) — 小
批 3 趋势图补两条 + 度数/裸眼视力的「较上次」+ 最近处理列表补已读状态 — 小
批 4 家长每日打卡(新表 + 录入/汇总 API + 家长端表单 + 门店端汇总) — 大
批 5 互扫绑定前端(家长出码页 + 门店扫一扫页 + 转入确认弹窗) P1 + P3 中(后端已通)
批 6 「提醒未记录家长」+ 复查提醒体系 P2 + 业务规则定义 中
摘出 眼轴百分位 卡门诊 5411 条数据入库,文件至今未上服务器 —

八、待长官拍板

# 决策点 我的建议
H1 新首页走独立路由(家长 /h5/home、门店 /h5/store-home),对话页回归纯对话? 是。冲突面最小、可灰度、能让 isParentSeat 那 11 处分支消失
H2 首页改版与批次五谁先谁后 批次五先(已拍板 13 条且卡着生产迁移);我们做 P0 登录层抽取时与它同桌定 gate 挂载点
H3 「提醒未记录家长」走短信(要先申请阿里云模板)/ 等微信订阅消息 Go-Live / 本期只做名单展示不做主动提醒? 本期只做名单展示,主动提醒单独立项(模板申请周期不可控)
H4 打卡「护眼灯」与代码里现有的「大路灯」是不是同一个设备? 需您确认命名
H5 门店端「未记录家长数」在多孩子家庭下怎么算 —— 任一孩子没打卡算未记录,还是全部孩子都没打卡才算? 建议"任一孩子未打卡即算未记录"(更保守、不漏人)
H6 打卡当天能否重复提交覆盖? 建议可覆盖(UNIQUE(patient_id, device_type, 本地日期)),家长填错能改
H7 互扫绑定的抢占口径:B 店能不带原店/家长同意直接把孩子转过来,保留还是加确认? 需您定。现状对已服务机构不友好
H8 是否解冻可见性红线文件(refractive_record_service.py 6 处)?不解冻则件 3 做完也查不出来 需您授权
H9 眼轴百分位确认摘出本期? 建议摘出

九、我没验的 / 说不准的

  1. 全程没在真机或 test 上打开过任何页面 —— 全部是静态代码 grep,无渲染证据
  2. 公众号菜单/二维码物料的实际配置在微信后台,代码里查不到,我只能推断它指向 /h5/chat 系列 URL —— 需要运维确认实际配的是什么
  3. 流式回复中断后后端是否增量落库没验(只验了前端无中断保护)
  4. 工作量是相对大小,不是人天 —— 未做任务拆解
  5. 批次五的完整实施细节以对面会话的方案文档和它给我的答复为准,我只 verify 了与首页改版直接相关的部分
  6. 平板端 /h5/tablet 本轮只验到"是平行实现、不会自动联动",没有逐块盘点它的首页壳 —— 若本期要一并覆盖平板,需要再查一轮

8 路并行 grep + 主会话复核 + 跨会话对接。0 命中处均带对照组。本机为云端隔离开发机,非验收环境。