/home/dev2/doctor-ai-brain/.claude/worktrees/agent-a9116645c38f80cebfeature/phase48a-brand-code,检查时 HEAD = 0deff182origin/test(检查时 fetch 到 94c12727)不能直接发 test。 当前 HEAD(0deff182)提交里缺失了这批工作最核心的一段逻辑(规则7可见性改写 _org_visibility_clause 及其 5 个调用点、独立验证测试文件),而且提交状态下 alembic 会产生第 4 个 head(多头污染)。这两个致命问题目前都只以未提交的工作区改动形式存在,必须先 git add + commit 补齐,再重新走一遍本报告的验证项,才能发 test。此外还发现一处规则7的真实可见性泄漏点(create_patient 的重复候选检索)和规则10("现在绑着的机构看全部历史")完全未实现。
$ git status --short
M app/services/refractive_record_service.py
M migrations/versions/phase48a_001_brand_code.py
A tests/integration/test_refractive_visibility_service_org.py
$ git log --oneline -5
0deff182 merge test into feature/phase48a-brand-code
f435f8b7 feat(phase48a): 品牌码四批成果存档
61dcf7e5 Merge ... 编辑保存报错修复+三处破坏性操作补二次确认
e1602c7a fix(store-mobile-refractive): 编辑保存报错修复 + 三处破坏性操作补二次确认
9e5ac47e Merge ... 已读回执补齐家长端接线+录入保存后跳查看页
判定:来历不明的东西——没有,但有严重遗留的未提交工作。
app/services/refractive_record_service.py 的修改 + 新测试文件 tests/integration/test_refractive_visibility_service_org.py。这两处正是规则7可见性改写(_org_visibility_clause 函数 + 5 个调用点)的全部实现,以及它对应的独立验证测试。migrations/versions/phase48a_001_brand_code.py 的改动,是把 phase48a_001 的 down_revision 从旧的 phase81_001 改挂到同步最新 test 后的新增头 demo_profile_data_fix3(文件内 2026-09-17 注释自述"二次改挂")。这些改动全部合法、内容对——但一个字节都没进 HEAD。 即:如果现在直接 push/开 MR,origin/test 上看到的代码里完全没有规则7的可见性收窄逻辑,也没有对应测试;migration 文件仍指向已经过期的 phase81_001。
$ git fetch origin test
61dcf7e5..94c12727 test -> origin/test
$ git log origin/test..HEAD --oneline
0deff182 merge test into feature/phase48a-brand-code
f435f8b7 feat(phase48a): 品牌码四批成果存档
$ git log HEAD..origin/test --oneline
94c12727 Merge ... fix: auto-update 迁移失败判定只认最终结果...
1c143d11 fix: auto-update 迁移失败判定只认治本8 最终结果...
b6f35e24 Merge ... fix: 下线 GET /api/community/personas 死接口...
a3ae6d96 fix: 下线 GET /api/community/personas 死接口...
$ git diff origin/test...HEAD --stat
22 files changed, 3365 insertions(+), 1 deletion(-)
判定:没有丢别人的东西。 HEAD..origin/test 的 4 个提交是与本分支完全无关的另两个 bugfix(auto-update 迁移告警判定、下线一个死接口),本分支落后 test 3 个提交(都是无关修复),不是"丢失",但发 test 前应先同步一次,避免 MR 里出现无意义的落后基线。diffstat 的 22 个文件全部与品牌码功能相关,没有夹带任何本分支不该动的文件。
用自写 AST 脚本(同时识别 revision = "..." 与 revision: str = "..." 两种写法)分别解析三个状态:
origin/test(100 个迁移文件):
HEADS (3): ['34ec94466f0c', 'demo_profile_data_fix3', 'phase79_001']
无 dangling down_revision,无环
HEAD 提交状态(102 个文件,即当前 git commit 会推给 test 的内容):
HEADS (4): ['34ec94466f0c', 'demo_profile_data_fix3', 'phase48a_002', 'phase79_001']
← 🔴 从 3 头变 4 头,新增一个 head!
工作区当前磁盘状态(含未提交的 migration 改动):
HEADS (3): ['34ec94466f0c', 'phase48a_002', 'phase79_001']
← 未提交的改动把 phase48a_001 的 down_revision 从 phase81_001 改挂到
demo_profile_data_fix3,头数恢复到 3,phase48a_002 顶替 demo_profile_data_fix3 当新头
链条:demo_profile_data_fix3 → phase48a_001 → phase48a_002(工作区状态下,头数正确维持 3)。
判定:当前 HEAD 提交本身是有 bug 的——如果现在就发 test,会制造一个新的第 4 个 alembic head。 修复已经在工作区里写好了(改 down_revision 指向 demo_profile_data_fix3),但没有提交。必须先提交这个 migration 改动,才能维持"3 头不变"的要求。
由于本项目历史迁移链本身存在一个与本批无关的既有缺陷:003_add_wecom_tables.py(down_revision='002')、0c5960f2e981_add_wechat_work_tables.py(down_revision='001')、80536a90a9da_add_wecom_tables.py(down_revision='006')三个迁移在同一条线性祖先链上重复 CREATE TABLE wechat_work_config,导致任何"从空库 alembic upgrade heads"的完整回放都会在 003 那一步炸 DuplicateTable。已用 git worktree add --detach 检出干净 origin/test、全新空库验证,同样报错,证明这是历史遗留问题,与本分支无关(不是本次引入的)。
因此改用等价但可行的隔离验证:以已经跑到 phase48a_002 头的现成 dev 库 doctor_ai_brain 做 CREATE DATABASE ... TEMPLATE 快照到 scratch_phase48a_roundtrip,只做 downgrade→upgrade 往返(工作区状态,即含未提交修复的 migration 文件):
$ alembic current
phase48a_002 (head)
$ alembic downgrade demo_profile_data_fix3
Running downgrade phase48a_002 -> phase48a_001, ...
Running downgrade phase48a_001 -> demo_profile_data_fix3, ...
# 降级后物理确认表/列真的被删
brand tables after downgrade (expect empty): []
patients columns after downgrade (expect empty): []
$ alembic upgrade phase48a_002
Running upgrade demo_profile_data_fix3 -> phase48a_001, ...
Running upgrade phase48a_001 -> phase48a_002, ...
# 升级后物理确认表/列/索引真的恢复
brand tables after re-upgrade (expect 2): ['brand_code_activations', 'brand_codes']
indexes: ['brand_code_activations_pkey', 'ux_brand_activations_org_openid_null_unionid', 'ux_brand_activations_org_unionid']
patients columns after re-upgrade (expect 2): ['service_org_bound_at', 'service_org_id']
验证完毕后已清理三个 scratch 库(scratch_phase48a_finalcheck/scratch_origtest_baseline/scratch_phase48a_roundtrip)与临时 worktree /tmp/origtest-check。
判定:phase48a_001/002 自身的 upgrade/downgrade 是对称、干净的,往返物理验证通过。 但前提是用了工作区里那份未提交的 migration 修复文件;如果只用当前 commit 里的旧版本(down_revision 仍指向 phase81_001),第 3 项已经证明会产生 4 头。
本批相关的 3 个测试文件,真连本机 PG(doctor_ai_brain,已迁移到 phase48a_002),无 mock:
tests/integration/test_admin_brand_code.py ......... 13 passed
tests/integration/test_brand_code_claim.py .......... 9 passed
tests/integration/test_refractive_visibility_service_org.py . 7 passed
======================= 29 passed, 10 warnings in 12.87s =======================
(已 grep 确认三个文件都无 mock/Mock/patch(,真打 PG + 真 jose JWT + 真 openpyxl 生成回读。)
test_refractive_visibility_service_org.py 是未提交的那个新文件——它测的正是也未提交的 _org_visibility_clause。如果直接以当前 commit 发 test,这个文件根本不会出现在 MR 里。
全量 pytest tests/ -q(排除 tests/e2e/tests/uat,按项目自己的规矩本机不跑全量 e2e):第一次尝试用 timeout 600 ... | tail -100 管道,10 分钟超时被杀死,tail 从未等到 EOF 刷新缓冲区,拿到 0 行有效输出([exited with code 143],纯浪费一次)。第二次改成直接重定向到文件(不经 tail 管道)重跑,进度非常缓慢——运行约 4 分钟只推进到 5%(........F.....F...F...........FF................... [ 5%],已看到多处 F),本机 2 vCPU/3.4G 配置 + 300+ 测试文件的量级,推算跑完需要 60-80 分钟量级,超出本次审查可承受的时间预算。判定:全量套件本次没有跑完,不作为本报告的阻断依据,本判定的合理性来自:3 个直接相关文件的 29 个测试已经真打全绿(见上),且本批改动的文件范围(app/services/refractive_record_service.py 的 committed diff 为空、其余全部是 NEW 文件)决定了对其它既有模块产生连带回归的概率本身就低;已观察到的早期 F 大概率是与本批无关的既有环境依赖失败(本机缺 GPU1 27B/embed 等外部服务,~/.claude/CLAUDE.md 记忆库里已多次记录"本机连不上真实模型/生产依赖"是常态),但没有逐条比对确认,如实列入"我没能验的"一节。
自己读完 _org_visibility_clause(app/services/refractive_record_service.py:111-133)与其 5 个调用点(_patients_filtered_stmt:169 / bind_patient_seat:519 / unbind_patient_seat:563 / get_org_patient:600 / _patient_is_sample:660)后,另写一份不复用现成测试文件的独立脚本(/tmp/independent_visibility_check.py),直接用库里真实存在的 3 个机构 id 造一个真 patient,跑四态 + 越权:
用机构: brand=1 A=2 B=3
建档 patient_id=daaf7edc-... org_id=1 service_org_id=None
[态1 未绑定] brand可见=True(期望True) A可见=False(期望False)
[态2 绑定A] brand可见=False(期望False) A可见=True(期望True) B可见=False(期望False,越权测试)
[态3 A转B] A可见=False(期望False) B可见=True(期望True)
[态4 解绑] brand可见=True(期望True,回落品牌方) B可见=False(期望False)
[越权] 无关机构C=3107 在绑定A状态下可见=False(期望False)
清理完成,测试患者已删除
=== 全部断言通过 ===
四态 + 越权隔离全部符合规则7预期,验证通过。但这段能通过的逻辑目前只存在于工作区未提交的改动里。
🔴 另外发现一处规则7的真实可见性泄漏,5 个调用点之外的"第 6 处":
app/services/refractive_record_service.py 第 397 行,create_patient() 函数里"疑似重复候选"检索:
suspects = (
await db.execute(
select(Patient).where(
Patient.org_id == org_id, # ← 未叠加 _org_visibility_clause
Patient.name == name,
Patient.birth_date == birth_date,
Patient.gender == gender,
)
)
).scalars().all()
...
result["duplicate_suspects"] = [
{"id": str(s.id), "name": s.name, "birth_date": ...} for s in suspects
]
场景:孩子档案 org_id=品牌方A,已被机构 B 认领(service_org_id=B)。按规则7,A 此时不应再看到这个孩子的任何信息。但如果 A 又新建一个同名同生日同性别的档案(重名孩子/双胞胎场景很常见),这条 suspects 查询只按 org_id==A 过滤,不管 service_org_id,会把这个已经被 B 认领、A 本不该再看见的孩子的 id/name/birth_date 原样塞进 API 响应 duplicate_suspects 返回给 A。这是一个真实的 PII 泄漏点,且不在 spec §5.2 列出的"至少 6 处硬编码"清单里(原清单只数到 get_org_patient/bind_patient_seat/unbind_patient_seat/_patients_filtered_stmt 两处,共 6 处,但实际上这第 397 行是清单外漏网的第 7 处)。
修法建议:suspects 查询把 Patient.org_id == org_id 换成 _org_visibility_clause(org_id)(不叠加 is_sample,因为重复检测不需要放行示范档案)。
全部新增端点(路径/方法/权限依赖):
| 端点 | 方法 | 鉴权 |
|---|---|---|
/api/admin/orgs/{org_id}/brand-code |
POST | require_role("admin") |
/api/admin/orgs/{org_id}/brand-code/status |
PATCH | require_role("admin") |
/api/admin/orgs/{org_id}/brand-code/summary |
GET | require_role("admin") |
/api/admin/orgs/{org_id}/brand-code/activations |
GET | require_role("admin") |
/api/admin/orgs/{org_id}/brand-code/export |
GET | require_role("admin") |
/api/h5/brand-code-claim/family-code |
GET | require_org_seat + seat_type=='parent' |
/api/h5/brand-code-claim/family-scan/resolve |
POST | require_store_seat |
/api/h5/brand-code-claim/family-scan/claim |
POST | require_store_seat |
/api/h5/brand-code-claim/family-scan/release |
POST | require_store_seat |
/api/public/brand-code/activate |
POST | 无(公开端点,设计如此) |
require_role("admin"),不按 org_id 做租户隔离——即任何拥有全局 admin 角色的账号可以查看/操作任意机构的品牌码。经核实这是既有平台后台惯例(app/api/admin_orgs_access_link.py 的删机构/改状态/头图等端点全部同款写法,require_role("admin") 在这个项目里语义就是平台超级管理员,不是租户自服务角色),不是本批引入的新漏洞。但 spec brand-partner-spec-v1-20260915.md §五之二「权限」写"品牌方只能看自己的码"——这条目前完全没有实现路径:本批唯一的访问入口就是平台后台 require_role("admin"),品牌方自己没有任何独立登录/查看自己码的通道。docstring 自己也承认(admin_brand_code.py:4"不新造鉴权")。这是一处已知但尚未言明的 spec 落地缺口,建议在报告里向长官明确标注,而不是留给下一批默默补。claim_family_scan:二次校验 patient_id 必须出现在 family_token 解出的孩子列表里(app/api/brand_code_claim.py:129-134),防止门店拿到任意 family_token 后越权认领无关档案——已读代码确认逻辑存在且生效(对应第 6 项独立脚本已验证越权隔离)。release_family_scan → release_patient_claim:服务层显式校验 patient.service_org_id != releasing_org_id 时 409(brand_code_claim_service.py:152-153),只有当前在服务的机构自己能解绑,符合规则7。/api/public/brand-code/activate:{brand_token, code}。brand_token 是服务端 secrets.token_urlsafe(32) 生成、落库校验(brand_codes.code_token 唯一),枚举成本等价枚举 32 字节随机数——但 brand_token 本身是设计上要公开的(印在实体卡片/海报上给人扫),它不是秘密,真正的防线是第二道 code。code 是微信 OAuth 一次性授权码,服务端调用真实微信 exchange_code(code) 换身份,微信自身保证该码单次可用、限时过期(防重放/伪造依赖微信侧机制,不是本应用自建的)。seat_auth.py 密码登录路径有专门的"同 IP 15 分钟内涉及账号数"横向扫描限流,brand_code_public.py 没有任何等价机制)。由于 code 必须真实完成一次微信授权才能换到,脚本刷量的技术门槛不低,但批量走真实授权流程(如群控/众包点击)刷开通数在业务上是可行的攻击面,而 brand_code_activations 直接是计费依据(规则20 按累计开通人数结算)。这是一处业务风险,建议列为"建议改"而非"必须改"——现有设计与代码注释本身已经权衡过枚举/伪造风险,只是没覆盖"真实但批量的授权"这类业务刷量场景。逐个比对新增/改动页面调用的接口字段:
| 前端文件 | 接口 | 比对结果 |
|---|---|---|
admin/orgs/[id]/brand-code/page.tsx |
GET .../brand-code/summary |
BrandCodeSummary{code_token,qr_url,is_enabled,enabled_at,stats{total_activations,active_activations,new_this_month,expiring_within_30d}} 与后端 BrandCodeSummaryResponse/BrandCodeStats 逐字段一致 |
| 同上 | GET .../brand-code/activations |
ActivationRecord{id,wechat_nickname,wechat_avatar,wechat_openid,activated_at,expires_at,is_expired,child_patient_count,bound_org_name,bound_at} + ActivationListResponse{records,total} 与后端 ActivationRecord/ActivationListResponse 逐字段一致 |
h5/brand-scan/page.tsx |
POST /api/public/brand-code/activate |
ActivateResponse{access_token,refresh_token,nickname,avatar,seat_type,org_features,visible_features} 与后端模型逐字段一致 |
h5/chat/page.tsx、h5/tablet/page.tsx |
seat_wx_identity(既有端点,本批扩字段) |
新增 h5_user_access_token/h5_user_refresh_token,后端 WxIdentityResponse 同名同款新增,前端类型声明与消费逐字段一致 |
admin/orgs/page.tsx |
GET /api/admin/orgs(既有端点,本批扩字段) |
新增 brand_code_enabled/brand_code_activation_count,后端 OrgItem 同名新增,前端 interface OrgItem 一致 |
判定:5 处契约全部字段级比对通过,没有发现前后端字段名不一致或缺字段的问题。
brand-partner-spec-v1-20260915.md)| 规则 | 落实情况 |
|---|---|
| 规则8/20:扫码即激活、365 天有效期 | ✅ ACTIVATION_VALID_DAYS=365 在 admin_brand_code.py/brand_code_service.py 两处同常量口径落实;_record_activation 写 expires_at = now + 365d |
| 规则3/12:去重计数、跨品牌码各自计数 | ✅ ON CONFLICT DO UPDATE(按 org+unionid 或 org+openid-null-unionid)保证同人重复扫同码不重复计数;不同品牌码(不同 brand_org_id)各自独立计数 |
| 规则20:对账只算个数不放金额 | ✅ _build_export_workbook 明确只写"计费人数""是/否计费",无单价/金额字段 |
| 导出 xlsx 且日期到日(页面到分钟) | ✅ 导出行 activated_at.date().isoformat()/expires_at.date().isoformat()(到日),页面 fmtDateTime 到分钟——分离实现,符合"导出到日、页面到分钟"的拍板 |
| 规则22:任何机构可开码默认不开,需二次确认 | ✅ enable_brand_code 数据库列 server_default=false;前端 handleBrandCodeClick 未开启先弹 brandCodeConfirm 二次确认框,确认后才调用开启接口 |
| 规则23:管理页独立整页 | ✅ admin/orgs/[id]/brand-code/page.tsx 是独立路由页面(非弹窗),符合 |
| 规则7:可见性收窄 + 编辑权跟随(规则18) | 🔴 逻辑本身写对了(第 6 项已独立验证),但代码停在工作区未提交状态,不在 HEAD 提交里 |
| 规则10:现在绑着的机构看全部历史(含别家做的),曾经绑过只看自己录的 | 🔴 完全未实现。已 grep 确认 patient_timeline(refractive_record_service.py:695-698)及其它记录级读点(:879/:1308/:3359)全部仍是旧的 RefractiveRecord.org_id == org_id,即"只看自己录的",不区分"当前是否绑定"。已用 git diff origin/test...HEAD -- app/services/refractive_record_service.py(空输出)和 git diff HEAD -- 同文件(只涉及 patient 级 5 个函数)两次核实,patient_timeline/_load_history 没有被任何提交或未提交改动触及过。这不是遗漏提交,是这批工作压根没做这块,规则10 目前对当前绑定机构也只能看到自己录的记录,看不到"曾经绑过的机构"留下的历史——与拍板口径直接矛盾。 |
| 规则18:编辑权跟随(能看=能改) | 与规则7耦合,逻辑对但同样卡在未提交 |
| §五之二「权限」品牌方只能看自己的码 | 🔴 未实现(见第7项),只有平台后台入口,无品牌方自服务通道 |
$ git status --short --ignored
M app/services/refractive_record_service.py
M migrations/versions/phase48a_001_brand_code.py
A tests/integration/test_refractive_visibility_service_org.py
!! .env / __pycache__ 等(标准 gitignore 项,非本批新增,无需处理)
没有发现临时脚本、scratch 数据、误加的日志文件被 staged。除第 1 项已详述的未提交改动外,没有其它需要清理的东西。审查过程中我自己在数据库层建过 3 个 scratch 库(scratch_phase48a_finalcheck/scratch_origtest_baseline/scratch_phase48a_roundtrip)和一个临时 worktree(/tmp/origtest-check),验证完已当场清理干净,未留痕迹在本 worktree 或仓库里。
app/services/refractive_record_service.py(_org_visibility_clause + 5 调用点)、tests/integration/test_refractive_visibility_service_org.py、migrations/versions/phase48a_001_brand_code.py(down_revision 改挂 demo_profile_data_fix3)三处当前全部只在工作区,必须 git add 后 commit,否则发 test 等于发一个"看起来做了规则7、实际代码里根本没有"的半成品,且会引入第 4 个 alembic head。app/services/refractive_record_service.py:397 create_patient() 的 suspects 查询把 Patient.org_id == org_id 换成 _org_visibility_clause(org_id),否则已被别的机构认领走的孩子的 name/birth_date/id 仍会通过"建档重名提示"泄漏给原品牌方。patient_timeline(:695-698)及相关记录级读点目前完全没有按"当前是否绑定"区分历史可见范围,与 spec 拍板口径矛盾。这不是本次遗漏提交能解决的,需要新的一段实现(按 patient.service_org_id == org_id 时放开全部 RefractiveRecord,否则维持 RefractiveRecord.org_id == org_id)。是否要在这批一起补齐还是留给下一批,建议明确问长官。brand_code_public.py::activate 除了微信 OAuth 本身的限制外没有额外限流,理论上存在"真实但批量授权"刷开通数(计费依据)的业务风险。现状的枚举防护设计是够的,缺的是防"数量异常"的运营侧监控/风控,可作为后续增强项。app/api/api_v1.py::list_orgs 新增的 brand_code_enabled/brand_code_activation_count 是逐机构循环单独查询(每个 org 两条 SQL),机构数变大后有性能隐患,建议后续改成批量 JOIN/子查询一次拿全部机构的统计。origin/test 3 个无关提交(auto-update 迁移告警判定修复 + 下线死接口),发 test 前建议先同步一次,但不阻断,且这两个提交内容与本批无冲突面。pytest tests/ -q 全量套件:两次尝试都没跑完(第一次管道缓冲问题拿到 0 输出,第二次 5% 进度后判断本机资源/时间跑不完整套,主动放弃继续等待,改以 3 个直接相关文件的 29 个真打通过 + 静态 diff 范围分析作为替代判据,见第 5 项)。这确实是没验的一块——不能排除全量套件里有本批引入的隐藏回归,只能说本批 diff 范围(committed 部分几乎全是 NEW 文件,唯一改动的既有文件 app/api/api_v1.py/app/api/seat_auth.py/app/main.py 改动点都很局部)从改动面上看连带回归概率低。如果长官要更硬的证据,需要专门开一个会话、给足时间(建议 1-2 小时)跑完全量,或者只挑与 Patient/refractive_record_service/seat_auth/api_v1.list_orgs 相关的测试目录做定向全量。exchange_code(code) 依赖真实微信服务端,本机无法用真实 code 走一遍完整授权换 openid/unionid 的链路,只读代码 + 现有集成测试(真打但用的是可控测试身份,不是真微信)确认逻辑正确,未做真实微信侧联调。next build/tsc 做编译期验证。test_13_export_xlsx_real_generation_and_readback),没有额外造大数据量场景验证。/home/dev2/doctor-ai-brain/.claude/worktrees/agent-a9116645c38f80ceb)8b654707(本次复检新增提交,父提交 3007b296)origin/test(复检时 fetch 到 dfa3b8ea,比上次报告的 94c12727 又新增 19 个无关提交,未新增任何 migration 文件——已用 git ls-tree 对照确认)1. 可见性改动未提交 → 现在是否已在 commit 里
$ git log --oneline -3
8b654707 fix(phase48a): 建档查重只回数量不回PII
3007b296 fix(phase48a): 规则10换店记录级可见性 + 建档查重PII泄漏修复
d7bc71af fix(phase48a): 可见性六处改写 + 迁移改挂最新头
git status --short 干净(复检开始时)。_org_visibility_clause 及 5 个调用点、tests/integration/test_refractive_visibility_service_org.py 均已在 d7bc71af 提交里,非本次复检新发现问题——已清。
2. 迁移挂靠 → AST 解析确认 head 数不增(AnnAssign 陷阱已避开)
自写 AST 脚本同时处理 ast.Assign(revision = "...")与 ast.AnnAssign(revision: str = "...")两种写法解析 migrations/versions/*.py 全部 102 个文件:
total files: 102
HEADS (3): ['34ec94466f0c', 'phase48a_002', 'phase79_001']
dangling down_revision: []
3 头,与 origin/test 基线(3 头)数量一致,无新增第 4 头,无 dangling 引用。git ls-tree 交叉确认 origin/test 相较 HEAD 没有新增任何 migrations/versions/*.py 文件(comm -13 输出为空)——本次基线比对未过期。已清。
3. 规则10 → 确认已实现且有测试
tests/integration/test_refractive_rule10_history_and_dup_leak.py 6 项全部真打通过(真 PG,无 mock):
test_10_1_current_binder_sees_all_records PASSED
test_10_2_previous_binder_sees_only_own_records PASSED
test_10_3_unrelated_org_sees_nothing PASSED
test_10_4_unclaimed_brand_sees_all PASSED
test_dup_leak_claimed_child_not_returned_to_original_brand PASSED
test_dup_leak_negative_control_still_detects_own_visible_duplicate PASSED
四态(当前绑定看全部/曾经绑定只看自己录的/无关机构看不到/未认领品牌方看全部)+ 越权隔离全部覆盖,落点 patient_timeline/_fetch_record/_load_store_visible_history/mark_conclusion_read 与报告第9项原文点名的 4 个行号一致(已读代码核对)。已清。
4. 查重 PII → 本次是否修掉
本次复检会话主动修复(长官 9-17 拍板)。app/services/refractive_record_service.py::create_patient 的返回体从
result["duplicate_suspects"] = [
{"id": str(s.id), "name": s.name, "birth_date": ...} for s in suspects
]
改为
result["duplicate_suspect_count"] = len(suspects)
字段选择理由:grep 全部消费方后确认前端 4 个文件(store-refractive/page.tsx+lib/api.ts、tablet refractive-record-pane.tsx+patient-rail.tsx+lib/api.ts)无一处读取候选者 id/name/birth_date,全部只用长度/计数判断是否弹提示,故直接去掉旧字段、新增 duplicate_suspect_count: int,而不是保留空数组 + 并列新字段(避免留一个恒空的死字段增加后续误用面)。另发现 web/__tests__/mobile-refractive-list.test.tsx(vitest mock)与两个后端测试文件(tests/contract/test_phase63_patients_api.py、tests/integration/test_refractive_rule10_history_and_dup_leak.py)断言旧字段,一并改成新契约。app/api/refractive_records.py::create_patient 端点返回类型是裸 dict[str, Any],无 Pydantic response_model 约束,改字段不触发额外校验层改动。app/api/parent_patients.py::create_my_child(对应 h5/family/new/page.tsx)经 grep 确认从未读取过 duplicate_suspects,无需改动。已清(本次修复)。
$ pytest tests/integration/test_refractive_rule10_history_and_dup_leak.py \
tests/integration/test_refractive_visibility_service_org.py \
tests/integration/test_admin_brand_code.py \
tests/integration/test_brand_code_claim.py \
tests/contract/test_phase63_patients_api.py -v
...
======================= 58 passed, 10 warnings in 7.06s ========================
(真连本机 PG doctor_ai_brain,已 grep 确认三份新/改动测试文件均无 mock/Mock/patch(。)
pytest tests/integration/ -q 整体真跑本分支 HEAD 状态(doctor_ai_brain 库,已迁移到 phase48a_002):
12 failed, 299 passed, 44 deselected, 21 warnings, 3 errors in 213.48s (0:03:33)
失败/报错清单(test_business_oss_integration.py×2、test_phase52_emit_points.py×2、test_phase74_admin_parent_seats.py×3、test_session_title_generation.py×2、test_share_ai_audio.py×3、test_chat_history_citation_roundtrip.py×3 error)——全部与本批改动文件(refractive_record_service.py、brand_code*、patients 相关端点)无引用重叠,人工确认无一条断言 duplicate_suspect/org_visibility/brand_code 相关符号。
用 git worktree add --detach 检出干净 origin/test(dfa3b8ea),复用同一个 .env/同一个 PG 实例,只跑上述失败清单涉及的 6 个测试文件:
13 failed, 27 passed, 16 warnings, 4 errors in 29.98s
同样 12 项(外加 1 项 test_07_lifecycle_seat_added 波动性失败,不在本分支失败清单内,判定为该用例本身 flaky,与本次改动无关)全部在干净 origin/test 上复现失败——证明这 12 failed/3 errors 是预置环境缺陷(consult_config 未初始化、FAISS 索引文件缺失、SFT TTS 未启用等本机资源缺口),不是本次改动引入的回归。验证完已 git worktree remove --force 清理临时目录。
用 CREATE DATABASE ... TEMPLATE doctor_ai_brain 建 scratch_phase48a_final_9_17(已迁移到 phase48a_002)做往返,跑完即删:
$ alembic downgrade demo_profile_data_fix3
Running downgrade phase48a_002 -> phase48a_001, ...
Running downgrade phase48a_001 -> demo_profile_data_fix3, ...
brand tables after downgrade (expect empty): []
patients columns after downgrade (expect empty): []
$ alembic upgrade phase48a_002
Running upgrade demo_profile_data_fix3 -> phase48a_001, ...
Running upgrade phase48a_001 -> phase48a_002, ...
brand tables after re-upgrade (expect 2): ['brand_code_activations', 'brand_codes']
patients columns after re-upgrade (expect 2): ['service_org_bound_at', 'service_org_id']
indexes: ['brand_code_activations_pkey', 'brand_codes_code_token_key', 'brand_codes_pkey',
'ux_brand_activations_org_openid_null_unionid', 'ux_brand_activations_org_unionid',
'ux_brand_codes_org_id']
升降级对称,表/列/索引增删全部符合预期。已 DROP DATABASE scratch_phase48a_final_9_17 清理。
4 项问题全部已清,本批可以发 test。
3007b296 提交解决,本次复检独立重新验证确认无误。d7bc71af 提交后已保持 3 头,本次复检用新的 origin/test 基线重新交叉确认仍然成立。node_modules(web/node_modules 不存在),未跑 npm run build/tsc/vitest run。前端改动(duplicate_suspects→duplicate_suspect_count 的 5 个 .ts/.tsx 文件 + 1 个 vitest 文件)只做了人工逐处 grep 比对确认类型与调用点一致,没有编译期验证,也没有真跑 mobile-refractive-list.test.tsx 里改过的 3 个用例。按仓库规矩"禁止新增依赖",本次未执行 npm install 补齐 node_modules。如需编译期证据,需长官另点头跑一次 npm install(不新增 package.json 依赖,只是补本地 node_modules)。pytest tests/ -q 全量套件(tests/integration/ 之外的 tests/contract/、tests/unit/、tests/e2e/、tests/uat/ 等其余目录):本次只针对性跑了 tests/integration/(314 项,3 分半跑完,符合任务要求)+ 本批 5 个直接相关文件(58 项)+ tests/contract/test_phase63_patients_api.py(已含在 58 项里)。未跑仓库全部测试目录,理由同上一份报告——本机资源/时间预算下全量耗时过长,且本批改动面很窄(1 个后端文件 + 4 个前端文件 + 3 个测试文件),连带回归概率低。