2026-08-24 · dev2 · 对最新
origin/test(FETCH_HEAD)逐维git grep· 每个 0 命中都带对照组
被检对象:c-end-dev-roadmap-v2-20260824.md(路线图 v2)+ 记忆c-end-verified-tech-facts结论:抓到 3 个 P0 真问题 + 5 个 P1 计划偏差 + 3 个 P2 口径错误。其中 F2 必须长官拍板。
证据
app/services/seat_service.py:222 async def list_seats(org_id, page, size, db, q, field)
query = select(User).where(
User.role == "org_seat",
User.linked_org_id == org_id,
) ← 只有这两个条件 · 没有任何 seat_type 概念
消费方两个,都不是我方能单独控制的:
app/api/admin_seats.py:155 return await seat_service.list_seats(...) ← 后台「机构管理→席位」
app/api/internal_settlement.py:45 from app.services.seat_service import (
generate_seats, list_seats,
reset_seat_password, set_seat_status,) ← 结算系统 X-Service-Token 调
后果
total 变大修法(进 A 批)
list_seats 增加过滤:默认只返 seat_type == 'store'(用 IS NULL OR = 'store' 兼容存量)internal_settlement 的调用点不传该参数 ⇒ 结算天然排除generate_seats / set_seat_status / reset_seat_password 三个也被结算调的函数,users.phone 是全局唯一,与「正式席位手机号可再开子席位」口径物理冲突 🔴 需长官拍板证据(model + migration 双证)
app/models/user.py:47
phone = Column(String(20), unique=True, nullable=True, index=True)
migrations/versions/phase29_001_create_auth_tables.py:43
sa.Column("phone", sa.String(20), unique=True, nullable=True),
⚠️ 同文件 :98 的 ix_users_phone 是 postgresql_where=phone IS NOT NULL 的普通 partial index,
不是唯一索引 —— 但列级 unique=True 生成的 UNIQUE 约束是独立存在的,唯一性照样成立。
而首登必写 phone,绕不过去:
app/api/seat_auth.py:88 class SeatChangePasswordRequest
contact_phone: str = Field(..., min_length=1, max_length=20) ← 必填 · 后端 ^1\d{10}$ 校验
后果
门店正式席位已经绑了 138xxxx,同一个人(店员本人也是家长)拿同一手机号开子席位 →
家长首登改密写 User.phone 时 UNIQUE 约束冲突,直接报错登不进去。
这与 8-21 拍板的「正式席位手机号可再开子席位(互不影响)」物理矛盾。
三条路,请长官选
| 方案 | 做法 | 代价 |
|---|---|---|
| 甲 | 家长子席位不写 users.phone,手机号另存新字段 |
改动最小、零风险;但子席位无法用手机号检索(后台搜索少一个面) |
| 乙 | 唯一约束改成 (phone, role) 或 partial unique(排除 parent) |
要动 users 表约束 —— 🔴 门店端登录链路共用这张表,风险最高 |
| 丙 | 改口径:同一手机号全系统只能一个账号,正式席位手机号不能再开子席位 | 零代码;但推翻 8-21 已拍板的口径 |
🔴 我倾向 甲(不碰门店端在用的约束,符合"只做加法不做改法"),但这是口径变更,我不自作主张,等您定。
_ACTOR_TYPES 是 DB CheckConstraint,加一档必须写 migration;且下游有真实计数消费方证据
app/models/activity.py:33
_ACTOR_TYPES = ("org_seat", "h5_user", "temp_link", "miniapp", "admin")
app/models/activity.py:68 CheckConstraint(... name="ck_seat_activity_actor_type") ← seat_activity_log
app/models/activity.py:93 CheckConstraint(... name="ck_report_image_actor_type") ← report_image
下游真实消费方(会被虚增的地方)
app/tasks/activity_rollup_task.py:11
active_seats : count(DISTINCT actor_id WHERE actor_type='org_seat' AND event ∈ usage)
app/services/org_insight_service.py:287
o["src_counts"]["org_seat"] == 0
后果
actor_type='org_seat' ⇒ 虚增门店活跃席位数,机构洞察和日汇总全歪修法:migration 加 parent_seat 一档(两张表都要)+ 反向清查上面两个消费方逐个排除。
证据
app/services/seat_service.py:171/197 INSERT users(role='org_seat', linked_org_id=org_id, ...)
唯一调用方:
app/api/admin_seats.py:127 seats = await seat_service.generate_seats(...)
app/api/internal_settlement.py:45 (结算侧也调)
我在 v2 里写「子席位账号的唯一签发入口就是门店端开通弹层,B 不做完 D/E 连账号都造不出来」
—— 这句是错的。后台现成就能签发席位,只要加个 seat_type='parent' 入参。
修正:B(门店端)和 C(PC 后台)都不是 D/E 的硬前置。
⇒ 可以直接按长官"先出 C 端手机端"的意思走 A → D → E,B/C 往后排。
seat_service 七件套全是现成的generate_seats / list_seats / reset_seat_password / reassign_seat_user /
set_seat_status / update_seat_profile / extend_seat (全在 seat_service.py)
正好覆盖方案要的「停用 / 启用 / 换绑 / 重置密码」。⇒ C 批主要是加 seat_type 过滤 + 做页面,不是从零建。
app/api/seat_auth.py:281-283 seat_expires_at < now → 拒登录(NULL=永久) ← 惰性到期,与方案设想一致
app/api/seat_auth.py:346-347 首登激活才起算 expires_at = 激活时间 + seat_valid_days
seat_service.extend_seat(...) ← 续期现成
⇒ 「一期固定 1 年」= 建席位时传 valid_days=365,零新代码;续期也不用等二期。
app/services/seat_service.py initial_password_expires_at = now + SEAT_INITIAL_PASSWORD_VALID_DAYS
# Phase 68 · 初始密码 7 天内未激活即失效(D1/D2)
业务流程是「门店开通 → 把账号密码交给家长 → 家长回家登录」。
家长 7 天内没登录,密码就废了 ⇒ 必须二选一:
①给门店端一个"重发/重置初始密码"入口(B 批加) ②给子席位单独放宽这个天数。
不写进 tech,上线就是投诉。
web/app/h5/tablet/pane/panes/__tests__/menu-sidebar.spec.tsx:90
it('TABLET_FEATURES 冻结 8 个 active id 枚举(Phase 63 追加第 8 项)', () => {
expect(TABLET_FEATURES.map((f) => f.id)).toEqual(ALL_IDS);
});
⇒ H 批(平板家长端)一旦动 TABLET_FEATURES,这个测试立刻红 —— 现成的门店端零回归护栏。
🔴 铁律:别去改这个测试来"让它过",要另起一份家长名单(与 D-D 拍板一致)。
| 写错的 | 物理真实路径 |
|---|---|
web/components/admin/AdminConsoleShell.tsx |
web/app/(dashboard)/admin/AdminConsoleShell.tsx |
app/api/h5_refractive_records.py / /api/h5/refractive-records |
app/api/refractive_records.py |
⚠️ 这正是「对照组失效必须换查法」那条铁律救回来的两次 —— 若照旧路径写进 tech,实施会话必然踩空。
refraction_method 是自由 String(10),不是 DB 枚举 ⇒ 扩枚举比估的轻app/models/flywheel.py:124
refraction_method = Column(String(10), comment='小瞳/散瞳/电脑') ← 无 CheckConstraint
⇒ 扩成「无 / 小瞳 / 快散 / 中散 / 慢散」不需要 migration 改约束,只改注释 + 应用层校验。
(⚠️ String(10) 够放,最长「慢散」2 字;但要防前端传长串截断。)
web/app/h5/tablet/page.tsx:478 router.push("/h5/settings?from=%2Fh5%2Ftablet");
web/app/h5/tablet/pane/page.tsx:163 router.push(`/h5/settings?from=...`)
记忆 c-end-verified-tech-facts 写「平板首页没有进设置的口子(profile 热区坐标已弃用,是残留)」
—— 与代码不符。:478 是一条真实的 router.push。
⇒ 开工时必须确认这条是不是活路径;若是活的,家长端平板越权面多一个入口,D/H 批都要堵。
| # | 维度 | 结果 | 对照组 |
|---|---|---|---|
| 1 | Phase 74 号 | 0 命中 · 空闲 | Phase 73 = 4 文件 ✅ |
| 2 | 席位 = users 表 role='org_seat' + linked_org_id |
属实,无独立 org_seats 表 ⇒ seat_type 加 users 正确 |
__tablename__ 查法有效 ✅ |
| 3 | SeatLoginResponse 仅 4 字段 |
属实(access/refresh/org_features/must_change_password) | 同文件 org_features = 8 ✅ |
| 4 | OrgBrainConfig database.py:61 · enable_refractive_record :113 |
属实 · opt-in 可照抄 | — |
| 5 | /api/h5/store-card 可多带一列 |
h5_store_card.py:145 select · :193 进返回体 |
— |
| 6 | SeatStoreCard.tsx:316 gate 写法 |
属实 {data.enable_tablet_home_branding && ( |
— |
| 7 | 设置页两分支 settings/page.tsx:176/178 |
属实 ⇒ 第三分支方案成立 | — |
| 8 | 报告秒读文件齐 + extract 签名 |
vllm_client.py:704 async def extract(self, image_path, image_type) -> dict |
4 文件全在 ✅ |
| 9 | vision_exams 表 |
app/models/flywheel.py:54 |
— |
| 10 | 秒读现在无配额 | four_l_report.py quota 0 命中 |
h5_quota_usage = 3 文件 ✅ |
| 11 | 后台平台管理组 | 看板/机构管理/机构洞察/席位活动 ⇒ 加一行即可 | — |
| 12 | 门店设置页两端同一路由 | 平板 :478/:163、手机 menu-sidebar.tsx:228 全指 /h5/settings ⇒ 一份实现覆盖两端成立 |
— |
| 13 | 档案搜索 org 级泄漏 | refractive_records.py:40 search_patients(db, seat["org_id"], ...) 属实 |
require_org_seat = 3 文件 ✅ |
list_seats 加 seat_type 过滤(F1)· _ACTOR_TYPES migration 两张表(F3)extend_seat 现成,零成本(F6)| 待决 | 选项 |
|---|---|
| 🔴 F2 手机号唯一冲突 | 甲(家长不写 users.phone · 我倾向)/ 乙(改唯一约束 · 风险最高)/ 丙(改口径) |
| F7 初始密码 7 天 | 给门店"重发"入口 / 给子席位放宽天数 |
| 顺序 | A→D→E→F→G→B→C→H(先出 C 端)还是 B 插到 A 之后 |
云机环境 · 非验收证据。本文所有代码引用均为本机对 origin/test(FETCH_HEAD)的真实 git grep 输出。