阶段 3 architecture · 2026-09-29 · 基线 origin/test
83ca4937
编号 verify:Phase 87|phase87在 docs/ 0 处(仅 dev2 memory 2 处为本议题预占)· 对照Phase 833 处命中
核心 9 个文件与第一轮 verify 基线7f6d6a1f对比git diff --stat空,行号沿用有效
verify 报告:~/reports/brand-code-hard-expiry-verify-20260929.md(13 维 + D1-D5 第二轮)
| # | 口径 | 来源 |
|---|---|---|
| R1 | 品牌码新扫码建的家长号,从扫码当天起 365 天硬到期 | 9-18 |
| R2 | 到期后所有入口都挡住 | 9-18 |
| R3 | 续期 = 机构在品牌码页点「延期」(已有功能,只改 users.seat_expires_at) |
9-18 / 9-21 |
| R4 | 老号不动(到期字段 NULL 照旧永久);生产品牌码 0 个,无存量 | 9-18 |
| D1 | 所有到期号统一提示「服务已到期,请联系机构续期」(平台号的「席位已到期」一起换) | 9-29 按建议 |
| D2 | 品牌码页到期日/状态/统计跟着延期走(= 家长号真实到期日) | 9-29 按建议 |
| D2′ | 收费不管:导出表「到期日 / 状态 / 是否计费 / 合计」维持开通记录 365 天,不动 | 9-29 长官「不用管收费 你就给延期就行」 |
| D3 | 同一微信在本机构有停用的家长号(不论来源),再扫码拦下「账号已停用,请联系机构」 | 9-29 长官「就先不放进来了」 |
| D4 | 已登录家长遇到期,被踢出的登录窗显示「服务已到期,请联系机构续期」 | 9-29 按建议 |
| D5 | 到期提醒脚本自动包含品牌码号,不改代码 | 9-29 按建议 |
brand_code_service.py:81-113 _provision_family_seat:generate_seats 未传 valid_days,到期两字段都 NULL;:108-113 第二次提交写微信绑定brand_code_service.py:261-274 activate_brand_code:命中/新建后直接 _issue_seat_tokens,不查到期brand_code_service.py:59-78 _find_existing_family_seat:条件含 User.is_active.is_(True) → 停用号查不到 → 走新建middleware.py:387-389 require_org_seat:只查 is_active / 设备号 / device_epoch,不查到期seat_expiry.py:22-43 assert_seat_org_usable:唯一时间门,文案「席位已到期」;middleware.py:289 采用函数内 importseat_auth.py:558-564 登录拦截,文案「席位已到期」(独立一份,不走 assert)seat_auth.py:644-647 首登激活:仅 seat_expires_at IS NULL and seat_valid_days IS NOT NULL 时写 → 建号时直接写 seat_expires_at、seat_valid_days 留 NULL,首登不会覆盖seat_service.py extend_seat:old 非 NULL → max(now, old) + N 天,正常可延admin_brand_code.py:筛选 :184-193、行 :314-315、统计 :426-444 全用 BrandCodeActivation.expires_at;_bulk_enrich_family_seats :215-219 已单独查 users(id, phone)auth.ts refreshSeatToken(:706-723)/fetchSeatOrgFeatures(:296-312)只返 bool/三态,403 原因丢失;page.tsx:1214 handleSeatKicked → setSeatKicked(true);page.tsx:3407-3409 <SeatLoginSheet kicked=…>;seat-login-sheet.tsx:349 固定「请重新登录以继续使用」_provision_family_seat 第二次提交处:同一个 now 写 seat_bound_at 和 seat_expires_at = now + timedelta(days=ACTIVATION_VALID_DAYS)(常量 :39 已存在)。
不改 generate_seats 签名(结算按位置参数调用);seat_valid_days 保持 NULL。同步改 docstring :94-96 那段「留 None(永久)」的旧说明。
activate_brand_code 在 seat, is_new = await _find_or_provision_family_seat(...)(:261)之后、刷新昵称/记开通/签 token 之前:
assert_seat_org_usable(None, seat)(函数内 import,同 middleware:289 写法)。到期 → 403,不刷新昵称、不记开通、不发 token。
_find_existing_family_seat 加关键字参数 active: bool = True,条件改为 User.is_active.is_(active)(默认值保持原行为)。
_find_or_provision_family_seat 在「找不到可用号」后、新建前:用 active=False 再查一次,命中 → HTTPException(403, "账号已停用,请联系机构")(在锁内抛,finally 照常释放锁)。
require_org_seat 在 if seat is None or not seat.is_active 之后(:389 后)加 assert_seat_org_usable(None, seat)。
影响:门店号(约 23 个接口)到期也会被挡;生产实测门店号 136 个中已到期 23 个、到期后登录过 0 个,实际影响≈0(长官已知晓)。
seat_expiry.py 与 seat_auth.py:563 两处 "席位已到期" → "服务已到期,请联系机构续期"(全角逗号)。
前端无按旧字符串做判断的代码(login-diagnosis.ts:69 自行按时间生成标签,不读后端文字)。
_activation_filter_stmt:outerjoin User(family_seat_user_id == User.id)改为无条件做一次(昵称分支只加 where,不再各自 join),状态筛选用eff = func.coalesce(User.seat_expires_at, BrandCodeActivation.expires_at)。get_brand_code_summary 的 active / expiring_within_30d:同样 outerjoin + eff;total、new_this_month 不变。_bulk_enrich_family_seats::216 那条查询多取 User.seat_expires_at,放进结果 seat_expires_at。_to_activation_record:exp = info.get("seat_expires_at") or row.expires_at;expires_at / is_expired 用 exp。_build_export_workbook 与 export_brand_code_activations 的计费统计(:750-760)全部维持开通记录口径。导出共用筛选函数,所以「按状态筛选后导出」的行集合会跟页面一致,但行内「到期日/状态/是否计费」仍是计费口径 —— 这是有意为之。brand-code/page.tsx:400 只改注释(字段名不变)。auth.ts:新增模块级 let seatKickReason: string | null + 导出 takeSeatKickReason()(读后清空)与 noteSeatKickReason(detail);refreshSeatToken 和 fetchSeatOrgFeatures 收到 403 时读 detail 记下(读失败不影响原返回值)。page.tsx:handleSeatKicked 取 takeSeatKickReason() 存进新 state seatKickMessage;:2119 等「原请求直接 403」的调用点先从响应读 detail 调 noteSeatKickReason。seat-login-sheet.tsx:新增可选 prop kickedMessage?: string,kicked && kickedMessage 时替换 :349 的「请重新登录以继续使用」;不传时行为逐字节不变。平板端不改。scripts/seat_expiry_reminder.py(D5 自动覆盖)brand_code_activations 表与计费口径、_record_activationgenerate_seats / extend_seat / 登录接口逻辑(只换文案)parent_quota_service.py(窗口随到期日走,延期顺带恢复部分额度,长官已知晓)见 test-coverage-analysis.md「Phase 87」节。旧文案断言 4 处同步改:tests/api/test_seat_extend.py:373、tests/integration/test_seat_expiry_e2e.py:177/474、tests/integration/test_login_alert_org_nickname_e2e.py:107。
改登录相关 → 先发 test,长官真机验(扫码新号 / 手工把 test 号到期日改到过去看提示 / 停用后重扫 / 延期后页面日期)再发生产。