交付出件箱

← 返回出件箱

Phase 48B · 扫码绑定 · 技术方案 v0.1

2026-09-24 · 阶段 3 architecture · 基线 origin/test fb0dc9d7 · 分支 feature/scan-binding-48b
上游:需求原文 ~/reports/brand-partner-spec-v1-20260915.md(Phase 48 · 规则 9/10/13/14/16/18)·
总方案 ~/reports/home-redesign-master-plan-20260924.md 第五节 · 方案 ~/reports/scan-binding-plan-v1-20260924.md ·
方案图 scan-binding-realpage-v1-20260924.html(长官 9-24 晚过目通过,「还没有账号」提示页保留;9-24 深夜定 D1–D3,图随之增量补 5g、改一行)
本文不写测试代码、不写完整实现代码。所有「文件:行号」均为本分支物理 grep/sed 结果(见第二节证据)。


〇、结论先行

项 结论
数据库 不建表、不加列、无 alembic 迁移。patients.service_org_id / service_org_bound_at 已存在(test/生产 information_schema 实查)
后端 改 5 个文件:refractive_record_service.py(档案与记录读写门 6 处 + 3 个末位参)、parent_patients.py(家长看记录详情 1 处)、brand_code_claim_service.py、brand_code_claim.py(新端点全挂在已注册路由上)、jwt_local.py(机构码令牌)。不改 app/main.py、seat_service.py、seat_auth.py(登录改造批次五正在改这几处)
前端 改 6 个现有文件,新建 3 个页面 + 3 个组件 + 1 个扫码工具
存量影响 生产:service_org_id 非空 0 行;家长名下孩子所属机构≠家长账号机构 0 行;检查记录机构≠档案机构 0 行 → 可见性改动对现有数据结果集零变化(第二节 V14)
发版独立性 本件要动的后端/前端文件 origin/main 与 origin/test 内容一致(git diff --stat 空)→ 可单独发生产

一、编号与登记


二、物理核查证据(15 维)

方法:派 5 个只读助手分查(第 5 个为独立审查后补查记录级读写),每路把全部命令写成可重跑脚本,输出原样落盘;主会话亲手重跑 5 个脚本并 diff:VA/VB/VD/VE 逐字节一致,VC 仅第 4 行时间戳不同。另派 1 个没参与的审查员对照代码挑错:查出记录级读写遗漏(已补为 V15 与 4.1 第 5、6 条)、两处行号偏差(已改);它报的 seat_auth.py 路由行号偏差经主会话复核不成立(装饰器就在 :463)。数据库 3 路由主会话亲跑(生产走 seatops 通道,避开 db-prod 的 RLS 假 0)。
证据归档:~/reports/48b-verify-20260924/(VA–VE 的 .sh / .out / 结论,md5 与 /tmp/48b-verify/ 源一致)。0 命中项均带对照组。

维 查什么 结论 关键证据
V1 模型字段 Patient 有 org_id / owner_type / owner_seat_id / service_org_id / service_org_bound_at / is_sample,无软删除字段(0 命中,对照组 org_id 5 命中);users 有 seat_type / linked_org_id / seat_bound_openid / seat_bound_unionid app/models/flywheel.py:16-97;VA ### A-1
V2 门店可见性 _store_visible_clause:门店自建(owner_type NULL/'seat')恒可见;家长自建要求家长席位 linked_org_id==本机构。不认 service_org_id refractive_record_service.py:79;VA ### A-2
V3 家长可见性 _patients_filtered_stmt(:175)与 get_org_patient(:613)先套机构外层门 _org_visibility_clause(:111),再 AND 家长 own_clause。家长端 org_id 恒取家长席位的 linked_org_id VA ### A-4、A-8(python 现场编译 SQL 证 AND);middleware.py:322 require_org_seat
V4 家长端接口 parent_patients.py 12 个接口全部经 get_org_patient / patient_timeline / create_record 门控,无旁路 VA ### A-5
V5 现有「绑定家长」 bind_patient_seat(:539)只许绑本机构家长席位(User.linked_org_id == org_id),已绑 409;内部自带 commit(:582) VD ### D-5
V6 建档 create_patient(:386)传 owner_seat_id 即 owner_type='parent'(:445),内部自带 commit(:458/:498);门店建档路由 refractive_records.py:74 先过 svc.require_feature VD ### D-5
V7 认领接口 路由 /api/h5/brand-code-claim(brand_code_claim.py:39,main.py:235 已注册):GET /family-code(:57)、POST /family-scan/resolve(:87)、/claim(:117)、/release(:161);claim 校验孩子必须在该家长名下(防越权);服务三分支(未认领写入 / 同机构幂等 / 别家 409 或 force_transfer 转入),只写 service_org_id + bound_at(brand_code_claim_service.py:104,commit :136),无操作人留痕 VB ### B-1、B-2;VD ### D-6
V8 令牌 family_scan 令牌 24h 只装 seat_id(jwt_local.py:248/251/269);wx_identity 令牌 10 分钟(:30/:151/:175),均 HS256 + JWT_SECRET_KEY + type 区分 VB ### B-3
V9 二维码出图 /api/qr(qr.py:26)公开 GET,文本在网址里;/family-code 把家长令牌拼进 qr_url(→ 进 nginx 日志),前端 0 处消费;qrcode.react 已是依赖且 4 处在用(前端出图可行) VB ### B-4 + 主会话 grep
V10 微信扫一扫 POST /api/wx/jssdk-config(wx_jssdk.py:117)现成;scanQRCode 代码调用 0(对照组 checkJsApi 22 命中);web/app/h5/family 下 JS-SDK 0(对照组 share 3 处);注入写法可抄 share/[token]/page.tsx:181-236(jweixin-1.6.0 :232),签名地址统一 location.href.split("#")[0] VB ### B-5;VD ### D-8
V11 微信认人 POST /api/seat/wx-identity(seat_auth.py:463)换 code → 签 10 分钟身份令牌,不写库;lookup_seat_username_by_wx(return_all=True)(seat_service.py:721)返回 [{username, seat_type}],不按机构过滤;落地组件 SeatWxAuthLanding 只有 onStart 一个参数,标题副标题写死 VB ### B-6、B-7
V12 前端入口 门店列表「返回」:213、「绑定家长」:255、选档案层 :433、BindParentSheet :496;详情页「绑定家长/解绑」:512-528、:693;家长页门禁 :76-79(非家长席位退回 /h5/chat)、「新建档案」按钮 :143-149;三个新路由 git ls-files 0(对照组 1) VC ### C-1、C-2;VD ### D-9
V13 编号 / 测试 / 冲突 仓库文档 Phase 48 进度 0 行;现有测试 test_brand_code_claim.py 9 条、test_refractive_visibility_service_org.py 7 条,均真 PG;verify-trigger-files.txt 不含本件任何文件;登录改造本地分支改了 app/main.py(3 行) + seat_service.py(102 行),本件都不动 VC ### C-4、C-6、C-7
V14 数据底数 见下表 主会话亲跑
V15 记录级读写(独立审查补查) 记录门 _fetch_record(:929-959):记录是本机构录的,或孩子由本机构服务即放行,不看孩子现在归谁 → 原机构仍能改 / 完成 / 删除 / 重跑分析旧记录;调用方 11 处(5 处带 for_update=True 写 + trigger_analysis :3203)。家长侧三处按记录机构过滤:时间线 :750-756、记录详情 parent_patients.py:629-634、标已读 :3441-3447(唯一调用方 parent_patients.py:841) VE ### E-1、E-2(主会话重跑 diff 一致)+ 主会话 grep

V14 数据底数(2026-09-24 22:0x 实查)

指标 test 生产
孩子档案总数 56 14
已被扫码认领(service_org_id 非空) 0 0
家长名下孩子(owner_seat_id 非空) 14(seat 8 / parent 6) 3(seat 3)
家长名下孩子所属机构 ≠ 家长账号机构 0 0
家长名下孩子无所属机构(org_id 空) 0 0
检查记录机构 ≠ 档案机构(非示范) 0 / 189 0 / 52
家长席位 / 门店席位 65 / 238 20 / 153
一个微信(openid)绑 2 个以上家长席位 1 1(同一微信另挂 4 个门店席位,疑似内部测试号)
一个微信(unionid)绑 2 个以上家长席位 0 0

⚠️ 纠正:第 9 轮写的「一个微信绑多个家长账号,生产 0 个」(引自登录改造会话)按 openid 实查是 1 个。不影响「默认用当前登录账号」的决定,但没登录时的多账号兜底必须做(第九节 D2)。


三、现状问题(为什么要改)

场景:孩子 P,org_id=A,家长席位 S(S.linked_org_id=A)名下,被机构 B 扫码认领后 service_org_id=B。读现有代码推演(VA ### A-8,静态推断,48B.1 第一步用真 PG 测试复现):

谁 列表 / 详情 / 录记录 / 改资料 原因
机构 B(新机构) 全看不到、改不了 外层门过了(service_org_id==B),但 _store_visible_clause(B) 要求家长属于 B
机构 A(原机构) 列表看不到(符合规则 10);但仍能改、完成、删除、重跑分析自己当初录的记录 外层门:已被认领只认 service_org_id;记录门 _fetch_record 只看记录是谁录的
家长 S 本人 自己的孩子全看不到、改不了 家长端 org_id=A,外层门对 A 为假,AND 后整体为假
家长 S(上一行修好之后) 看不到新机构 B 录的记录,打不开详情,标不了已读 时间线对「非当前机构」只放本机构录的记录;记录详情、标已读都按记录机构 = 家长账号机构过滤

即:48A 的认领接口一旦被真正用起来,孩子会同时从新机构和家长本人那里消失;原机构还能继续改旧记录。生产认领 0 行,所以还没人踩到。


四、后端改动

4.1 档案与记录读写门(refractive_record_service.py + parent_patients.py)

  1. 家长视角去掉机构外层门(_patients_filtered_stmt :175、get_org_patient :613)
  2. 家长分支(owner_seat_id is not None)只用原有 own_clause(及原有 is_sample 豁免),不再 AND _org_visibility_clause;门店分支原样保留「外层门 AND _store_visible_clause」。
  3. 函数签名不变(org_id 参数在家长分支不再参与过滤),11 个调用点不用改。
  4. 安全性:own_clause 按 owner_seat_id == 本席位 id,席位 id 唯一,家长只能看到自己名下的孩子;示范档案豁免逻辑逐字不变。
  5. 存量:家长名下孩子所属机构≠家长机构 0 行、已认领 0 行 → 现有家长看到的集合不变。
  6. 门店视角认「当前由本机构服务」(_store_visible_clause :79)
  7. 在现有两个分支之外 OR 第三个分支 Patient.service_org_id == org_id。
  8. 与外层门叠加后:B 能看到并编辑自己认领的孩子(规则 18「能看=能改」);A 列表看不到,只读历史走现有 allow_past_author 小门(规则 10)。
  9. 只读小门只认机构自己录的记录(_org_ever_authored_clause :135)
  10. 追加 ~_parent_created_clause()(:156)。
  11. 原因:改完第 1 条后,家长在孩子转走后仍能录记录,而 create_record 把记录的 org_id 记成家长账号机构(:1063 起,org_id=org_id)→ 不加这条,原机构会因为「家长自己录的记录」重新获得只读访问。规则 10 原文是「只剩自己当初录的那几条」。
  12. 存量:记录机构≠档案机构 0 行 → 现有访问不变。
  13. 写路径核对(不改代码,列清楚)
  14. 家长写接口全部经 get_org_patient(owner_seat_id=S) → 随第 1 条恢复。
  15. 机构 B 写接口经门店视角 → 随第 2 条放开。
  16. 原机构 A 档案级写路径不传 allow_past_author → 仍不能改(记录级见第 6 条)。
  17. bind/unbind_patient_seat 限定 owner_type∈(NULL,'seat') → 家长名下的孩子 B 不能解绑家长(与现状一致)。
  18. 家长看得到孩子的全部记录(不论哪家机构录的)
  19. 时间线 patient_timeline(:750-756):家长视角(owner_seat_id 有值)恒走「全部记录」分支;门店视角两个分支原样不动。
  20. 家长记录详情(parent_patients.py:629-634):去掉 RefractiveRecord.org_id == seat["org_id"],保留 id + patient_id(上一步 get_org_patient(owner_seat_id) 已证明孩子是本人的);「家长不见草稿」等原有限制不动。
  21. 标已读 mark_conclusion_read(:3441-3447,唯一调用方 parent_patients.py:841,调用前已 :836 验归属):去掉按记录机构的过滤,保留 id + patient_id。
  22. 家长改记录(parent_patients.py:1115-1117)不动:只允许改家长本人录的,这些记录的机构恒为家长账号机构。
  23. 原机构不能再改旧记录(记录门 _fetch_record :929)
  24. 加末位参 require_current_org: bool = False:为 True 时追加「孩子当前归本机构」= EXISTS(Patient.id == 记录.patient_id AND (_org_visibility_clause(org_id) OR is_sample))。示范档案保留放行,照旧由拿到记录后的 _reject_if_sample_record 给 403。
  25. 传 True 的 6 处:patch_record(:1126)、patch_conclusion(:3391)、push_conclusion(:3409)、delete_draft_shell(:3518)、complete_record(:3636)、trigger_analysis(:3203)。
  26. 只读的 get_record_dto、parent_view_dto 不传 → 原机构仍能看自己录的旧记录(规则 10)。
  27. 存量:记录机构≠档案机构 0 行、已认领 0 行 → 对现有数据判定结果不变。

4.2 放宽「绑定家长」到跨机构(R3 · bind_patient_seat :539)

4.3 建档加一个末位参数(create_patient :386)

4.4 认领服务与接口(brand_code_claim_service.py + brand_code_claim.py,全部挂在已注册路由 /api/h5/brand-code-claim 上,不改 main.py)

服务层

改动 说明
_patient_brief(:65)加字段 birth_date(选孩子显示年龄 · R5)、org_id + org_name(带进来的机构)
list_family_children(:76) 查机构名时把 org_id 一起查;返回加 family_username_masked(前 4 位 + xxxx + 后 2 位,与方案图「0012xxxx09」一致)
claim_patient(:104)加一个幂等分支 service_org_id 为空且 org_id == 认领机构 → 原样返回不写(本机构自己的孩子,不写无意义的 bound_at)
新 create_child_for_family 调 create_patient(org_id=S.linked_org_id, owner_seat_id=S.id, service_org_id=B, …)
新 link_child_for_family 调 bind_patient_seat(org_id=B, patient_id, S.id, allow_cross_org_parent=True)
留痕 认领 / 新建 / 关联三个写动作打一行结构化 logger.info(操作席位、孩子、原机构→新机构)。不复用 auth_service._write_audit(:487):它写的是登录审计表,且内部自带 commit,会把业务事务提前提交

接口

端 方法 路径 鉴权 说明
机构 POST /family-scan/resolve(已有 :87) 门店席位 返回体随服务层加字段
机构 POST /family-scan/claim(已有 :117) 门店席位 前端恒传 force_transfer=true(长官定:转店不加确认)
机构 POST /family-scan/create-child 门店席位 {family_token, 建档字段同 PatientCreateIn};与 refractive_records.py:74 一样先过 svc.require_feature
机构 POST /family-scan/link 门店席位 {family_token, patient_id};前置检查照抄 refractive_records.py:167 绑定路由
机构 GET /org-code 门店席位 返回 {org_name, landing_url, qr_url};landing_url = {host}/h5/org-bind?ot=<机构码令牌>,host 算法照 admin_brand_code.py:47 _resolve_host;qr_url 用 /api/qr(<img> 才能长按保存)
家长 GET /family-code(已有 :57) 家长席位 去掉 qr_url 字段,前端用 qrcode.react 本地出图,家长令牌不再进网址和日志(前端 0 处消费,已查)
家长 POST /org-scan/resolve 公开 {org_token} → {org_name},给「确认身份」页显示机构名
家长 POST /org-scan/children 家长席位 {org_token} → {org_name, children}(已登录路径)
家长 POST /org-scan/claim 家长席位 {org_token, patient_id}
家长 POST /org-scan/children-wx 公开 {org_token, wx_identity_token}(没登录路径)
家长 POST /org-scan/claim-wx 公开 {org_token, wx_identity_token, patient_id}

没登录时怎么认人

  1. 解 wx_identity_token,调 lookup_seat_username_by_wx(unionid, openid, db, return_all=True),只留 seat_type=='parent'。
  2. 按结果分三种:
  3. 0 个 → 404 {code: "no_parent_account"} → 前端显示「还没有开通智脑账号」
  4. 多于 1 个 → 409 {code: "multi_parent_account"} → 前端显示 5g 提示页
  5. 恰好 1 个 → 按 username 取席位
  6. 两个 claim 接口都复用现有防越权:孩子必须在 list_family_children(席位) 里,否则 404;认领恒 force_transfer=True(家长本人扫码即本人意愿)。
  7. 只绑定、不签登录令牌:没登录路径绝不下发 access/refresh token,否则等于绕过密码登录。

lookup_seat_username_by_wx 不按机构过滤。品牌码开通特意不用它(brand_code_service.py 文件头:防止把两家机构的席位错误合并);本件不建号、不合并,只是找这个微信本人的家长账号并做跨机构绑定,所以正需要不过滤机构。

4.5 机构码令牌(app/auth/jwt_local.py,照 :251/:269 家长码写法)


五、前端改动(对应方案图「全部改动一览」12 行)

共用扫码工具(新) web/app/h5/chat/lib/wx-scan.ts,放在 wx-oauth.ts 旁边:

# 文件 改什么
1 store-refractive/page.tsx(「返回」:213 左侧) 加「机构码」白底小胶囊(A3 定稿:白底、1px #e2e8f0、圆角 999、高 28、左右 10、13px/600 #2fa37c、qr-code 14px、距「返回」12px)→ /h5/store-refractive/org-code
2 同上「绑定家长」:255 先弹新组件 _components/bind-method-dialog.tsx(居中):①「扫描家长二维码」(仅微信内)②「从本机构家长中选择」→ 现有选档案层
3 新组件 _components/select-child-dialog.tsx 扫到后选孩子。每行显示:姓名 · 性别 · 年龄(birth_date)+ 所在机构。当前机构 = service_org_id ?? org_id:等于本机构 → 灰色「已在本机构」不可选;否则「当前机构:XX」(XX = 在服务的机构;没被认领过就是开家长账号的机构或扫品牌码的机构,长官 9-24 确认)。底部两个浅绿按钮「新建孩子档案」「关联已有档案」+「确认绑定」→ claim(force_transfer:true)。顶部「家长:昵称 · 账号(打码)」
4 _components/create-patient-sheet.tsx(Props :127) 保持底部弹出。加 3 个可选参数:parentLabel(多一行「家长:王女士」)、submitLabel(「保存并绑定」)、submit(替换默认建档请求 → /family-scan/create-child);不传时现有行为不变。防重复:提交前若该家长名下已有同名同生日的孩子,用现有 app/h5/components/confirm-dialog.tsx 提示直接选
5 选档案层(page.tsx:433-489) 改居中;加「关联模式」:选中后调 /family-scan/link,不再进 BindParentSheet
6 _components/bind-parent-sheet.tsx 底部弹层改居中(只改外壳,照 SeatStoreCard.tsx:922-933;尺寸口径见方案 md 第六部分:宽 328 / 四角 20 / 藏拖动条 / 标题距上沿 16)
7 [patientId]/page.tsx(:528 按钮、:693 BindParentSheet) 按钮不变,点开同第 2 行弹框;① 扫完直接调 /family-scan/link 把当前档案绑给扫到的家长,不再选孩子(长官 9-24 定 D1);成功后绑定行沿用现有「已绑定 · 账号」写法(:512)
8 新页 store-refractive/org-code/page.tsx GET /org-code,<img src=qr_url>,下方「长按二维码可保存图片」+ 两行「打印贴在前台,家长微信扫码即可绑定孩子」「只绑定孩子,不开通账号」;顶栏照 page.tsx:196-205
9 family/page.tsx(「新建档案」按钮 :143-149 下方) 一排「扫一扫」(仅微信内)「我的二维码」;有档案、没档案两种状态都显示
10 新页 family/my-qr/page.tsx GET /family-code → QRCodeSVG(family_token);灰字「有效期 24 小时 · 请勿截图转发」+「刷新二维码」
11 新页 h5/org-bind/page.tsx + seat-wx-auth-landing.tsx 加可选 title/subtitle 详见下方「落地页状态」
12 family/page.tsx 扫一扫结果 是 …/h5/org-bind?ot=… → router.push 到落地页(已登录,直接到选孩子);否则居中提示「未识别为机构二维码」+「取消 / 重新扫描」

SeatWxAuthLanding 的新参数默认值为现有写死文案,现有 3 处页面调用(brand-scan/page.tsx、chat/page.tsx、tablet/page.tsx)和组件测试 chat/components/__tests__/seat-wx-auth-landing.spec.tsx 不受影响。

落地页状态(第 11 行 · h5/org-bind/page.tsx)


六、边界清单

情况 处理
家长码过期(24h)/ 机构扫到的不是家长码 resolve 返回 401(「二维码已失效」/「令牌类型错误」,:269 现有文案)→ 弹框里一行文字 + 重新扫描
家长名下 0 个孩子 选孩子弹框显示空态,只留「新建孩子档案」
孩子已在本机构 该行不可选;服务端 claim 同样幂等
孩子在别家 直接转入,不二次确认;原机构列表消失,历史只读(规则 10)
同名同生日 前端提示直接选(不硬拦,同现有建档「疑似重复提示不硬拦」口径)
示范档案 不属于任何席位,list_family_children 天然不含
两家机构同时认领同一孩子 后写的生效(转店不加确认的必然结果),日志两条都在
机构码令牌被篡改 验签失败 401
门店席位账号的微信扫机构码 按「没有家长账号」处理(显示 5e)
解除认领(/family-scan/release 已有) 本期不加界面

七、测试计划(真 PG · 禁 mock · 新文件 tests/integration/test_scan_binding_48b.py)

先写失败测试证明第三节的问题(RED),再改代码(GREEN):

# 用例 期望
T1 家长 S 名下孩子被 B 认领、B 录了记录后,S 调列表 / 详情 / 时间线(含 B 的记录)/ B 那条记录详情 / 标已读 / 录记录 / 改资料 全部成功(改前 404 或缺记录)
T2 B 列表 / 详情 / 录记录 / 改资料 全部成功(改前 404)
T3 A 列表不含;A 曾录记录 → 档案详情与该记录只读可看,改档案 / 改记录 / 完成 / 删草稿 / 改结论 / 推结论 / 重跑分析一律 404;A 没录过、只有家长自录记录 → 详情 404 规则 10
T4 现有 tests/integration/test_brand_code_claim.py(9)、tests/integration/test_refractive_visibility_service_org.py(7)、tests/api/test_phase74_parent_patients.py(20)、tests/api/test_phase74_parent_patients_update.py(12) 全绿不回归(family-code 去 qr_url 同步改断言)
T5 /family-scan/create-child 一次写成 parent 名下 + service=B;S、B 可见,A 不可见;家长机构 = S.linked_org_id
T6 /family-scan/link 跨机构 成功;现有 /patients/{id}/bind 绑别家家长仍 404(回归)
T7 机构码令牌 往返成功;拿家长码 / 身份令牌冒充 → 401
T8 org-scan/children、claim(家长登录) 成功;claim 别人家的孩子 → 404
T9 children-wx(真 issue_wx_identity_token 签令牌,造 0 / 1 / 2 个家长席位绑同一 openid) 404 no_parent_account / 成功 / 409 multi_parent_account;响应里没有任何登录令牌字段
T10 本机构自己的孩子 claim 不写 service_org_id

八、影响面与发版


九、长官已定(2026-09-24 深夜)

# 问题 定了什么
D1 档案详情页点「绑定家长 → 扫描家长二维码」扫完怎么走 直接绑定当前档案,不再选孩子
D2 没登录的家长扫前台码,微信下有多个家长账号 显示 5g 提示页:先登录要绑定的账号,再扫一次;「去登录」按钮。方案图已补这一页
D3 选孩子弹框第三个孩子写「尚未绑定机构」 现实不会出现:家长自建档默认归开家长账号的机构,品牌码家长归扫码的品牌机构(与 create_my_child 取 seat.linked_org_id 一致)。改为「当前机构:XX」

十、子步骤(impl 用)

子步骤 内容
48B.1 档案与记录读写门 6 处(4.1)+ T1–T4(RED → GREEN)
48B.2 认领扩展:简要字段 / 幂等 / 留痕 / create_patient 末位参 / bind_patient_seat 末位参 / create-child / link + T5、T6、T10
48B.3 机构码令牌 + org-code + org-scan 5 个接口 + family-code 去网址 + T7–T9
48B.4 机构端前端(改动一览第 1–8 行)+ 扫码工具
48B.5 家长端前端(第 9–12 行)+ 落地组件加参数
48B.6 next build + 全量相关 pytest -v + 发 test + 长官真机验收