07d035d406ec66443dcac8aa4a6f893e5c7cafef(sudo test-inspect deploy 实测,与交办时一致)docs/superpowers/plans/impl/phase83-subseat-brandcode-batch1.md:772-801(验收标准)、docs/superpowers/plans/tech/phase83-batch5-login-overhaul.md、方案图 ~/reports/subseat-login-plan-v2-20260924.htmlP83验收A-0925(id=96),前缀 QEYXLP83验收B-0925(id=97),无前缀(用于对照"没设前缀被拦"场景)| # | 操作 | 请求 | 响应 | 结论 |
|---|---|---|---|---|
| 1 | 管理员登录 | POST /api/auth/login(ljg_admin) |
200,role=admin |
通过 |
| 2 | 新建机构 A/B | POST /api/v1/admin/orgs ×2 |
200,id=96/97,subseat_prefix:null |
通过(确认 OrgItem 响应体已带 subseat_prefix 字段) |
| 3a | A 无前缀 · 后台开家长子席位 | POST /api/admin/parent-seats/provision {org_id:96,count:1} |
409 该机构还没设置子席位账号前缀,请先到『机构管理 → 更多 → 子席位前缀』里设置 |
通过,文案与代码/方案图逐字一致 |
| 3b | A 无前缀 · 开启品牌码 | POST /api/admin/orgs/96/brand-code |
409(同上文案) |
通过 |
| 4a | 前缀非法值 | PUT .../96/subseat-prefix {subseat_prefix:"bf1"} |
422 只能填 1-6 个英文字母 |
通过 |
| 4b | 给 A 设前缀(输入小写 qeyxl) |
PUT |
200 {subseat_prefix:"QEYXL",locked:false} |
通过,自动转大写 |
| 4c | GET A 前缀 | GET |
{subseat_prefix:"QEYXL",locked:false} |
通过 |
| 4d | 给 B 设同一前缀 | PUT .../97/subseat-prefix {subseat_prefix:"qeyxl"} |
409 前缀 QEYXL 已被其他机构使用,请换一个 |
通过,跨机构唯一性生效 |
| 5a | A 开 2 个家长子席位 | POST /api/admin/parent-seats/provision {org_id:96,count:2} |
200,账号 = QEYXL100001 / QEYXL100002 |
通过,格式与连号规则一致(密码已打码,未落盘) |
| 5b | 开号后再改前缀 | PUT .../96/subseat-prefix {subseat_prefix:"ZZZZZ"} |
409 已开过子席位,前缀不能再改 |
通过 |
| 5c | GET A 前缀(应 locked) | GET |
{subseat_prefix:"QEYXL",locked:true} |
通过 |
| 6 | 机构列表带前缀 | GET /api/v1/admin/orgs |
A 记录含 "subseat_prefix":"QEYXL" |
通过(附带发现:开号后 enable_parent_subseat 被自动置 true,符合 admin_parent_seats.py 注释设计) |
| 7a | A 生成 3 个门店席位 → 取 1 个,账号+初始密码登录(scene=store) |
POST /admin/orgs/96/seats/generate→POST /api/seat/login |
200,must_change_password:true,phone_login_enabled:true |
通过 |
| 7b-1 | 改密不带手机号 | POST /api/seat/change-password {new_password:"654321"} |
422 请填写手机号 |
通过 |
| 7b-2 | 改密带随机测试手机号(199 开头,先 sudo db-query test 查 0 命中,但见下方 C 节 RLS 假 0 说明;最终以本次改密接口未撞 409 为准) |
POST ... {new_password:"654321", contact_phone:"199******56"} |
200 密码修改成功 |
通过 |
| 7c-1 | 手机号+新密码登录(scene=store) |
POST /api/seat/login |
200,must_change_password:false |
通过 |
| 7c-2 | 账号+新密码登录(应拒,新号只认手机号) | POST /api/seat/login |
401 用户名或密码错误 |
通过,且服务端 test-inspect trace 实测抓到对应 error_type=seat_login_401 审计(账号已打码),证明走的是真实新代码路径 |
| 7c-3 | 不存在的测试手机号登录 | POST /api/seat/login |
401 用户名或密码错误 |
通过,test-inspect trace 实测抓到 error_type=seat_login_401_phone_not_found(与 tech §6.2 步骤 1 设计的免告警专属 error_type 完全对应),审计手机号已打码(19***19) |
| 7d | 门店 token 查 store-card | GET /api/h5/store-card |
subseat_prefix_set:true |
通过 |
| 8a | A 有前缀后开启品牌码 | POST /api/admin/orgs/96/brand-code |
200,返回 code_token/qr_url |
通过 |
| 8b | 开通名单接口 | GET /api/admin/orgs/96/brand-code/activations |
{records:[],total:0} |
空列表(无人扫码,正常);family_seat_phone 字段以代码为证 —— app/api/admin_brand_code.py:145 ActivationRecord.family_seat_phone: Optional[str] = None,:294 赋值 info.get("phone") |
| 9 | 家长号用手机号真登录(需真实微信授权) | — | — | 未验:云机无法完成真实微信 OAuth 握手,如实标注,与交办说明一致 |
补充佐证:test-inspect trace doctor-api 实测只抓到与本次操作精确对应的 2 条 INCIDENT(见上表 7c-2/7c-3),无其他异常,说明本次全部 API 操作在服务端侧干净、无非预期报错。
存放目录:/home/dev2/reports/phase83-b5-test-shots/(10 张,均已用 Read 工具亲眼看过,文件名 ASCII)
| 文件 | 对应方案图块 | 内容 | 结论 |
|---|---|---|---|
b1-orgs-more-menu.png |
第 12 块 | A 机构行「更多」下拉展开 | 一致:「子席位前缀」为下拉第一项,绿色(text-emerald-600),与「头图」同组、下方一条分隔线隔开生命周期操作 |
b2-orgs-prefix-modal-locked.png |
第 12 块 | A 已锁定态前缀弹框 | 一致:输入框显示 QEYXL 且置灰不可编辑,橙字「已开过子席位,前缀不能再改」,「保存」按钮置灰 |
b3-brandcode-confirm-noprefix.png |
第 14 块 | B(无前缀)开启品牌码二次确认框 | 一致:橙字提示「该机构还没设置子席位账号前缀…」,「确认开启」按钮置灰态 |
b4-provision-noprefix.png |
第 13 块 | 开通子席位弹窗选中 B(无前缀) | 一致:橙字提示,「确认开通」按钮置灰 |
b5-provision-withprefix.png |
第 13 块 | 同弹窗选中 A(有前缀) | 一致:灰字「账号前缀:QEYXL,新开的号接着往下排」,「确认开通」按钮恢复可点(深色可用态) |
b6-brandcode-page-header.png |
第 10 块 | A 品牌码后台整页 | 表头含「手机号」列 —— 一致;「操作」列因当前 0 条开通记录暂无行可看到「重置密码」按钮的渲染态,该按钮的存在已用代码核实(admin/orgs/[id]/brand-code/page.tsx:685-693),页面确认没有「换绑」操作 —— 一致 |
b7-mobile-initial.png |
第 9 块 | 手机端(微信UA)进入 /h5/chat?login=seat,微信身份换取环节桩过(见下方说明)后自动弹出登录框 |
一致:直接弹出登录框(未卡在微信授权页) |
b8-mobile-login-sheet.png |
第 9 块 | 登录框写法 | 一致:该门店席位是全新号、本微信无任何绑定记录(bound_seats:[])→ 账号框占位符「席位账号」,密码框占位符「管理员给您的 6 位初始密码」,与决策 14「没有 / 全是账号 → 席位账号」规则一致 |
b9-mobile-init-modal.png |
第 4 块 | 真实账号+初始密码登录成功(打真后端)后弹出的首登改密窗 | 一致:手机号栏标题「手机号」+ 绿色小标签「用于登录」;底部文案「设置后,管理员给您的初始密码立即失效,以后请用手机号 + 这个密码登录」,与 seat-change-password-sheet.tsx:259-313 逐字对上 |
b10-mobile-settings-page.png |
第 15 块(反向用例) | 完成改密后进入 /h5/settings,「家长子席位」区块 |
一致:A 已有前缀 → 「开通席位」按钮为正常可点绿色态,未出现「店里还没设置家长账号前缀…」的橙色警告(该警告只在无前缀时出现,已在代码 SeatStoreCard.tsx 确认;本截图是"有前缀不应报警"的反向验证) |
关于微信身份握手的说明(如实披露):云机无法完成真实微信 OAuth(需要真实公众号 + 真实用户微信客户端),这在交办说明第 9 条已预期到。为了仍能验证「登录框写法」「真实账号密码登录」「首登改密」这几项与登录改造直接相关的核心逻辑,本次只对 POST /api/seat/wx-identity(微信身份换取接口)用 Playwright page.route() 做了桩(返回 bound_seats:[]、无 seat_username),使页面能跳过不可能在云机完成的 OAuth 跳转、进入登录框。除此之外的全部请求都是真实网络请求打真实后端:账号+密码登录、首登改密提交,均是 Playwright 真实填表 + 真实点击触发的 fetch,打的是 test.tongban.love 上跑着 07d035d4 的真实后端,并在服务端 test-inspect trace 里留下了可核对的真实审计记录(见 A 节 7c-2/7c-3)。提交登录表单前已清空桩出来的假 wx_identity_token,确保它不会被当作真微信身份令牌传给后端。
sudo db-query test,只读)organizations(id=96) subseat_prefix = QEYXL ✅
org_parent_seat_counter(org_id=96) last_seq = 100002 ✅(开 2 个号后从 100000 起步,连号到 100002)
全库 subseat_prefix 非空的机构数 = 1(只有 A;B 的 409 没有留下任何脏数据) ✅
org_parent_seat_counter 表总行数 = 1(只有 A 这一行,B 未成功建号) ✅
users.phone_login_enabled 直查:
SELECT count(*) FROM users WHERE phone_login_enabled = true; -- 返回 0
这是假 0,不是真实结果 —— users 表开了 RLS(sudo db-query test 用的只读角色不在放行名单),已用同表 n_live_tup 交叉验证:验收开始前 n_live_tup=392,本次共新建 7 个席位(2 家长 + 5 门店)后变为 n_live_tup=399,差值 7 与本次实际新建的席位数完全吻合,证明查询路径没问题、只是 RLS 挡住了行级读取。phone_login_enabled 的真实值改用 API 响应佐证(权威性等价于直查该字段):A 节 7a/7c-1 两次登录响应均返回 phone_login_enabled:true,与 tech §3.2 第 4 条「所有新建席位一律 phone_login_enabled=True」的设计一致。
sudo test-inspect deploy:已部署 commit 07d035d406ec66443dcac8aa4a6f893e5c7cafef,该 commit 于 2026-09-25 10:20:25 部署成功(耗时 263s,"五条实证通过"),坏提交/失败计数均为"无"sudo test-inspect health:settlement-api → HTTP 200deploy/post-deploy-verify.sh:1-17):该脚本由 auto-update.sh 在"本轮真 recreate 过容器"后自动调用,内含三道检查 ——①容器全 Running ②/health 200 ③chat-e2e 全链路真打(容器内跑 scripts/verify.sh),结果落盘到 /opt/doctor-ai-brain/.verify-server/last.json 与 /var/log/post-deploy-verify.logsudo test-inspect(子命令仅 ps/ls {wxpay,data,deploy,secrets}/trace/envkeys/health/deploy)不含读取 .verify-server/last.json 或 /var/log/post-deploy-verify.log 原文的能力,因此拿不到本次 07d035d4 部署的 chat-e2e 结果原文。间接佐证:test-inspect deploy 显示该 commit 部署 OK 且巡检心跳持续正常(56 秒前);test-inspect trace 显示的两条 INCIDENT 与本次真打操作精确对应(见 A 节),说明当前跑的确实是本批新代码而非旧缓存镜像。但这不能等价于"chat-e2e 通过"的直接证据,如需该原文建议走工单或请有权限者直接登录 ECS 查看上述两个文件。| # | 验收项 | 覆盖情况 |
|---|---|---|
| 1 | 家长新号格式「前缀+6位数字」,机构内100001连号,两机构互不影响 | ✅ 通过(A: QEYXL100001/100002;两机构互不影响靠 per-org 计数表设计 + B 未开号无脏数据佐证) |
| 2 | 无前缀时后台开子席位/门店开家长号/开启品牌码/重开品牌码四处拦下 | 部分验证:后台开子席位 ✅、开启品牌码 ✅ 实测 409;门店开家长号(store_subseat.py _require_org_subseat_prefix)、重开品牌码(set_brand_code_status)两处未实测(不在交办 A 节列出的步骤内),已读代码确认逻辑与前两处一致 |
| 3 | 前缀规则(1-6字母/大写/唯一/锁定/三种报错) | ✅ 全部实测通过 |
| 4 | 老号行为一字不差 | 未验:需要一个"未改造过的历史账号"做对照,而验收铁律禁止改动任何已有账号;tech 文档自己也把这条列为"只能靠代码审查+回归用例托底"的定性全称命题,已读代码确认改动只在"查用户"这一步前后加分支、其余步骤原样执行 |
| 5 | 新号只认手机号,登录框显示「手机号」,微信自动填手机号 | 部分验证:✅ 新号账号+新密码登录返 401(A 节 7c-2);微信自动填手机号需要真实微信绑定关系,未验 |
| 6 | 首次登录/刚重置的新号弹设置框(手机号必填) | ✅ 通过(422 + b9 截图) |
| 7 | 一老一新登录框「手机号或账号」 | 未验:需要构造"同一微信绑定一老一新两个号"的场景,超出本次可控测试数据范围 |
| 8 | 同手机号门店+家长新号密码不同/相同的候选逻辑 | 未验:需要专门构造门店号与家长号共用同一手机号的场景,本次未构造(可行但需额外步骤,时间关系未做) |
| 9 | 门店专用页和平板只找门店号 | 部分验证:scene=store 场景已通过真实登录验证账号/手机号路径均正常;scene=tablet 未实测(平板端 UI 本次未跑) |
| 10 | 手机号查无此人不推告警不计冷却,审计打码 | ✅ 通过(401 + trace 日志确认专属 error_type + 手机号打码) |
| 11 | 带小写字母的老门店号原样能登(test 18 个,挑一个真登) | 未验:已用 seatops remote test 只读通道查到真实存在的小写门店号样例(如 org5-6896c1),但要真登必须知道其密码,而验收铁律明确禁止重置任何已有账号密码,两者冲突,如实标注未验;精确匹配优先的逻辑已读代码确认(seat_auth.py 先 strip() 原样精确查,查不到且含小写才再按 upper() 查一次) |
| 12 | 家长检查单页(family/[id]/exam/new)登录框只找家长号(scene=parent) |
未验:本次未跑该页面 |
| 13 | 新号没设密码先走找回仍能账号登 | 未验 |
| 14 | 品牌码家长没设密码时改密有手机号栏不卡 422 | 未验:需要真实品牌码家长(依赖真实微信扫码),不可行 |
| 15 | 初始化弹窗手机号栏统一显示「手机号」(家长版/门店版/新老号) | 部分验证:✅ 门店新号已实测(b9);家长版/门店老号未实测,已读代码确认 seat-change-password-sheet.tsx 已删除 phoneLabel 三元式,统一为「手机号」 |
| 16 | 平板登录框写法与手机端一致 | 未验:本次未跑平板端 UI |
| 17 | 新号找回密码成功回填手机号 | 未验 |
| 18 | test_phase74_admin_parent_seats.py 3 预存失败已修,25 全绿 |
不在本次任务范围(pytest 由实施/dev 会话跑,交办的 A/B/C/D 四节未要求跑 pytest) |
| 19 | 结算侧清理孤儿席位不删品牌码家长 | 未验:该清理是全库级操作,可能影响本次新建测试数据之外的既有孤儿门店号,验收铁律禁止修改任何已有数据,不便触发,建议另开授权窗口用专门构造的数据验证 |
| 20 | 新号设完密码后下次登录预填手机号,一次成功不报错 | 未验 |
| 21 | 品牌码家长没设密码点功能先弹设密码框 | 未验(依赖真实品牌码家长) |
| 22 | 换微信用手机号登录被拦 | 未验(依赖真实微信绑定关系) |
| 23 | 自助找回密码后新号自动填回手机号 | 未验 |
| 24 | 品牌码新扫码家长点任意功能先弹设置框,老用户重扫免密 | 未验(依赖真实微信扫码) |
| 25 | 品牌码后台页有手机号列和重置密码按钮,没有换绑 | ✅ 手机号列表头已实测(b6);重置密码按钮已代码确认存在(0 条记录故按钮未渲染出来,但代码确认);页面确认没有「换绑」操作 |
| 26 | 后端 pytest / 前端 vitest / build / chat-e2e 全通过 | chat-e2e:机制确认存在,原文未获取(见 D 节);pytest/vitest/build 不在本次任务范围 |
| 27 | test 环境真点 16 块全部与方案图一致(截图) | 本次覆盖第 4、9、10、12、13、14、15 共 7 块(见 B 节截图逐条结论),其余 9 块(第 1/2/3/5/6/7/8/11/16 块,多数依赖真实微信授权或老号/新号共存场景)未覆盖,原因同上表对应项 |
| 28 | 数据完整性 SQL 真打通过 | ✅ subseat_prefix/计数表直查通过;users.phone_login_enabled 受 RLS 假 0,已用 API 响应 + n_live_tup 交叉验证佐证(见 C 节) |
本次在 test 环境针对批次五的核心新增能力(前缀维护接口三态、跨机构唯一性、开号连号、四处开号守门中的两处、手机号/账号双模登录判定、首登改密手机号必填与文案改造、品牌码开启前置校验与后台页手机号列)做了真实接口调用 + 真实页面点击 + 数据库交叉验证,全部通过,且服务端审计日志(test-inspect trace)与预期的新代码行为精确对应,证明当前 test 部署(07d035d4)确实跑的是本批代码。
未覆盖的项目集中在两类:① 依赖真实微信授权/扫码的场景(家长号手机号登录、品牌码家长首次设置、微信换绑拦截等)—— 云机结构性做不到,已如实标注;② 需要构造"老号+新号共存""同手机号双席位"等特殊历史数据组合的场景 —— 本次为了不碰任何已有数据、只用新建的专用测试机构,未构造这类组合,可在后续专门授权的窗口里补测。
内网报告链接(团队可点):http://100.113.74.62:8812/reports/phase83-b5-test-acceptance-20260925.md
长官令:"内部绕过微信授权用新号,把没测试的都测一下"。做法:用后端自身的
issue_wx_identity_token()在集成测试里签一张合法微信身份凭证(登录接口seat_auth.py:782自己也调它,仓库现有test_phase74/test_phase83姊妹文件一直这么做)。真 OAuth 只是"拿到 openid/unionid"那步网络握手,云机连不上;凭证之后的校验链路(席位查找/密码/绑定校验/scene 限定)一步不跳。这是标准 e2e 自动化测试,不改任何产品代码。文件:tests/integration/test_phase83_batch5_extra_coverage.py(提交 023098c0)。
| 原未验项 | 补测用例 | 结果 |
|---|---|---|
| ⑬ 新号没设密码先走找回,之后仍能账号登 | test_a1 | ✅ PASSED |
| ⑭ 品牌码家长没设密码改密:不带手机号 422「请填写手机号」/带则 200 | test_b1 | ✅ PASSED |
| ⑨ 家长号 scene=chat 手机号+密码登录成功 | test_c1 | ✅ PASSED |
| ⑨ 换一个微信身份登该号 → 403「登录失败,请席位本人登录,感谢」 | test_c2 | ✅ PASSED |
| ⑪ 带小写字母门店号"先原样精确查"有效、大小写变体 401 不 500 | test_d1 | ✅ PASSED |
| ⑤⑦ bound_seats 老号/新号/一老一新 login_by 判定 | 现有 test_phase83_batch5_wx_forgot_pwd.py test_01-05 | ✅ PASSED(核实已覆盖) |
| ⑧ 同手机号门店+家长新号:密码不同各进各号/相同弹候选/chosen_username 重提交 | 现有 test_phase83_batch5_seat_login.py test_05-06 | ✅ PASSED(核实已覆盖) |
| ⑫ scene=parent 只找家长号、scene=store/tablet 只找门店号 | 现有 seat_login test_07-08 | ✅ PASSED(核实已覆盖) |
主会话亲跑合计 19 passed(新 5 条 + 姊妹覆盖 14 条);测试数据残留 0、明文手机号 0(SQL 实查)。