⚠️ 已被 v1.2 取代 —— 现行正本是
dual-cockpit-proposal-v1.2-20260819.md(v1.2 是在本文上做增量,本文各节仍然有效,可当详档查阅)。
2026-08-19 · 在 v1.0(08-18)基础上补齐 5 个缺口,取代 v1.0,读这一份即可
沿用 08-15 调研定稿的 9 条口径规则与四大缺口结论,不新增任何统计口径
本版为纯文档,零代码改动;所有代码事实均本轮重新 grep 实证(证据见第二节)
结论:v1.0 定的方向全部成立,本版把 5 个"只给了一句话"的地方落成了可执行的表;最重要的一条是——A屏光点的数据链路本轮已逐段 grep 走通,从"等摸底才知道行不行"变成"代码层确定可行,摸底只是复核数量"。
| # | v1.0 的状态 | v1.1 做了什么 |
|---|---|---|
| 1 | A屏光点改用登录 IP,但"IP 从哪来"只有一句话 | 补第二节:五段链路逐段 grep 实证到 file:line,并挖出 5 条 v1.0 没写的硬约束 |
| 2 | B屏"沿用 08-15 十二模块,不重复展开" | 补第三节:12 模块 × 两屏 × 三层级 完整落位表 |
| 3 | 联动下钻——08-15 调研就标了"缺系统章节" | 补第四节:下钻规则表(选中态、回退、跨模块联动) |
| 4 | "一套 API 两张皮"只有一句约定 | 补第五节:8 个接口清单(口径出处 + 刷新频率 + 两屏取数差异) |
| 5 | 摸底 SQL 沿用 08-14 的 10 段 | 补第六节:按 D7 修订——1 段降级、1 段升级、新增 1 段直接产出 A屏地图长什么样 |
| 6 | 验收只有"性能三原则" | 补第七节:11 项可执行验收清单 |
唯一仍卡住的事没变:摸底 SQL 要有生产库权限的同事跑,云机不连生产库。
| 交付物 | 文件 | 状态 |
|---|---|---|
| 调研定稿(12 模块·九章) | cockpit-research-20260815.md |
✅ 正本,未动 |
| 单屏原型图 | cockpit-mockup-20260815.html |
✅ 08-18 真渲染修瑕全绿 |
| A屏科技风原型 | cockpit-a-mockup-20260818.html |
✅ 已出,等长官看图 |
| B屏深色管控原型 | cockpit-b-mockup-20260818.html |
✅ 已出,等长官看图 |
| 双屏方案 v1.0 | dual-cockpit-proposal-20260818.md |
🔄 被本版取代 |
| 摸底 SQL | geo-cockpit-baseline-sql-20260814.md |
⏸ 仍无人执行(08-14 至今) |
| 双屏方案 v1.1 | 本文件 | ✅ 本轮产出 |
结论:链路五段全部走通,全是现成字段、零补录;生产反代的真实 IP 透传在代码层已经开着,v1.0 列为头号风险的"IP 全落内网"在代码层不成立。
| # | 环节 | 物理证据 | 结论 |
|---|---|---|---|
| 1 | 登录事件落表 | app/models/audit.py:23 AuthAuditLog → 表 auth_audit_logs;event_type 有 CHECK 约束,5 类含 login_success |
✅ 有事实表 |
| 2 | 事件带 IP | app/models/audit.py:37 ip = Column(INET, nullable=True);login_attempts 同款 :74 |
✅ 有 IP 列,但可空 |
| 3 | 机构端登录也落这张表 | app/api/seat_auth.py:173 调 auth_service._write_audit(...);写入体 app/services/auth_service.py:498 |
✅ 席位登录进审计(不只平台账号) |
| 4 | 事件能映射到机构 | auth_audit_logs.user_id → app/models/user.py:58 linked_org_id;CHECK 约束 user.py:142 强制 role='org_seat' 必有 linked_org_id(org_admin 见 :140,渠道 channel_id 见 :143) |
✅ 机构/渠道归属确定,无需补录 |
| 5 | IP 是真实公网 IP | 取值 app/api/seat_auth.py:135 用 request.client.host;生产 uvicorn 带 --proxy-headers --forwarded-allow-ips *(deploy/Dockerfile.api:67),nginx 设 XFF(deploy/nginx/chat.tongban.love.conf:108-109);生产 compose 用的正是这份(deploy/docker-compose.prod.yml:78) |
✅ 透传链路成立 |
| — | 落省份 | ip2region 离线库 XdbSearchIP,云机实测 1.55ms/8 次 |
✅ 现成 |
基线说明:以上行号取自本 worktree HEAD
8f92bd1b(2026-08-12),该分支落后origin/test641 个提交,行号在 test 上可能漂移,符号名不会。
| # | 约束 | 物理证据 | 对大屏的影响 |
|---|---|---|---|
| 1 | 审计日志只留 6 个月,APScheduler 每天 02:00 清 | app/models/audit.py:24 |
光点/登录趋势回溯窗口上限 6 个月;跨年同比不能靠这张表,得走结算域订单数据 |
| 2 | 审计写入是 best-effort,异常被 except 吞掉只打 warning |
app/services/auth_service.py:508-509 |
🔴 审计表的登录条数不是权威计数。KPI「今日活跃」必须走现有活跃口径(app/api/admin_activity.py,三源=seat_activity_log+auth_audit_logs,见 :4 文件头),不许大屏自己 COUNT 审计表——否则两处数字对不上 |
| 3 | ip 列 nullable |
app/models/audit.py:37 |
光点必须显式处理 NULL:不画、且在"未定位"计数里体现(沿用已定规则第 6 条"未定位机构必须上屏") |
| 4 | 事件类型无 logout |
app/models/audit.py:47-49 CHECK 只 5 类 |
印证 08-15「无在线状态」结论:在线人数做不了,只能做活跃四级 |
| 5 | 🔴 结算域审计表 audit_logs 生产账号只有 INSERT,没有 SELECT |
settlement/db/grants.sql:48 GRANT INSERT ON audit_logs TO settlement_rw;(:59-60 明确不转属主) |
B屏若要展示"谁处置了什么"的操作留痕,现在读不出来——需要单独开只读账号或视图。这是 v1.0 漏掉的工程前提,建议列为 D8 待拍板 |
| v1.0 的说法 | v1.1 修正 |
|---|---|
| §七「登录 IP 内网占比高 → 先修部署配置再上光点」是并列风险之一 | 代码+部署配置层已透传,这条从"待验风险"降为"跑数复核";仍要跑 SQL【2】,因为配置文件对 ≠ 生产实际生效(可能存在多层反代) |
| A屏 KPI「今日活跃」数据来源写「模块1 顶部指标子集」 | 明确=复用 admin_activity.py 现有口径,不自算(见约束 2) |
结论:A屏只承载 12 模块中的 5 个(且都是"数字/形状"层),B屏 12 模块一个不少;差异全在"信息是否点名到人"。
| # | 模块(08-15 定稿原名) | A屏 | B屏地图/侧栏 | B屏下钻面板 | 说明 |
|---|---|---|---|---|---|
| 1 | 顶部核心指标(6 项) | ✅ 取 4 项做超大 KPI | ✅ 6 项全留 | — | A屏取:在营机构/生效席位/今日活跃/本期续费完成率 |
| 2 | 中央地图(三层信息) | ✅ 热力+光点,无标注无点色 | ✅ 省级色块,点省份下钻 | — | D7 已定不采集坐标 → 两屏均无机构点位 |
| 3 | 渠道产能榜(TOP10) | ✅ 降为 TOP5,轮播卡·左 | ✅ TOP10 全留 | — | A屏三维度自动轮换,B屏手动切 |
| 4 | 审批实时列表 | ❌ 不上屏 | ✅ | ✅ 名单+处置 | 含机构名,A屏脱敏原则排除 |
| 5 | 月度业务曲线(近 12 月·三线) | ✅ 轮播卡·左(与模块3 交替) | ✅ | — | |
| 6 | 续费预警列表 | ⚠️ 只给计数,轮播卡·右 | ✅ 完整名单 | ✅ 该机构席位批次明细 | A屏永不出名单(D5 开关控制是否连计数也隐藏) |
| 7 | 异常告警列表 | ⚠️ 同上,异常数>0 红闪 | ✅ | ✅ | 同上 |
| 8 | 席位增购排行榜 | ❌ | ✅ 右侧切换区 | — | |
| 9 | 续费周期甘特图 | ❌ | ✅ 右侧切换区 | ✅ 单机构甘特 | echarts CustomChart |
| 10 | 未来 12 个月到期分布 | ❌ | ✅ 右侧切换区 | — | |
| 11 | 底部智能洞察带 | ✅ 走马灯,最多两句 | ✅ | — | |
| 12 | 机构详情面板(弹层) | ❌ A屏零交互,无弹层 | — | ✅ B屏专属 | |
| 新 | 大区分布横条(七大区) | ✅ 轮播卡·右(与预警交替) | ✅ 地图上方筛选条 | — | v1.0 已把"环形"改横条(七类超分类色板 5 槽上限) |
A屏上屏元素合计 5 类(KPI 4 个 / 地图 / 轮播卡左 / 轮播卡右 / 走马灯)——与 v1.0 §2.2 一致,本表只是把它对回了 12 模块编号。
结论:B屏只允许一个全局选中态(省份),下钻面板是唯一的"点名到人"出口;A屏零交互,本节完全不适用。
| 层级 | 入口 | 展示 | 退出 |
|---|---|---|---|
| L0 全国 | 默认态 | 12 模块全国口径 | — |
| L1 省份 | 点地图省份 / 点大区横条 | 侧栏模块 3/5/6/7/8/10 全部切到该省口径,标题追加「· 广东」;地图该省高亮描边 | 点空白 / 面包屑「全国」 |
| L2 机构 | 在 L1 的预警列表 / 审批列表 / 省份机构清单里点某一行 | 模块 12 详情面板(弹层):机构基本信息 + 席位批次甘特 + 续费历史 + 处置按钮 | 关闭弹层,回到 L1 |
| 规则 | 内容 | 为什么 |
|---|---|---|
| 单一选中态 | 全屏同时只有一个省份被选中;再点另一省=替换,不是叠加 | 多选会让"续费完成率"分母口径变模糊,违反"不新增口径" |
| 顶部 KPI 不跟随 | 模块 1 六项永远是全国口径,下钻时不变,但加一行小字「当前筛选:广东」 | KPI 是大屏定盘星,跟着变会让人以为全国数掉了 |
| 未定位机构 | L0 计数上屏;L1 任何省份都不含它们;单独一个「未定位 N 家」入口直接进 L2 清单 | 已定规则第 6 条「未定位机构必须上屏,不隐藏」 |
| 无数据省份 | 用面板底色 #0F2137,不进热力渐变,可点(进 L1 显示空态) |
与"0 家"区分,避免最浅档被误读成 0 |
| 处置动作 | 只在 L2 面板内,L1 列表不放按钮 | 防误操作;也让 B屏投屏模式只需屏蔽 L2 即可全脱敏 |
| A屏 | 本节全部不适用(零交互、无弹层、无面包屑) | v1.0 §2.3 |
结论:8 个接口,A屏用其中 4 个且都是 B屏的子集;口径全部指向现有实现,接口层只做聚合不做新算法。
⚠️ 接口路径为建议命名,当前代码中不存在(已
git ls-files核对);口径出处列才是现有物理代码。
| # | 接口(建议) | 供给 | 口径出处(现有代码) | 刷新 | A屏 | B屏 |
|---|---|---|---|---|---|---|
| 1 | GET /cockpit/kpi |
模块 1 六项 | 活跃口径 app/api/admin_activity.py;席位/订单 settlement/backend/app/models/settlement.py:510,584 |
30s | ✅ 取 4 项 | ✅ 全 6 项 |
| 2 | GET /cockpit/geo/heat |
地图省份热力 | 同 1 的活跃口径 + 省→大区映射(本期新建) | 5min | ✅ | ✅ |
| 3 | GET /cockpit/geo/pings?since= |
登录光点(增量) | auth_audit_logs(event_type='login_success', ip) + ip2region |
15s | ✅ | 可选开关 |
| 4 | GET /cockpit/insights |
模块 11 洞察带 | 由 1/2/6/7 结果二次生成,无独立查询 | 5min | ✅ | ✅ |
| 5 | GET /cockpit/channels/ranking?dim= |
模块 3、8 | Channel settlement.py:86;席位批次 :584 |
5min | ❌ | ✅ |
| 6 | GET /cockpit/trend/monthly |
模块 5、10 | StoreServiceOrder settlement.py:510 |
5min | ❌ | ✅ |
| 7 | GET /cockpit/alerts?type= |
模块 4、6、7 | 席位到期 settlement.py:584-613 |
60s | ❌(只取 count,走接口 1) | ✅ |
| 8 | GET /cockpit/org/{id} |
模块 12 详情面板 | 同 5/6/7 + 甘特 | 按需 | ❌ | ✅ |
| 约定 | 内容 |
|---|---|
| 口径零分叉 | A屏不允许有 B屏没有的字段;接口 1 返回 6 项,A屏自己只渲染 4 项 |
| 权限 | A屏用只读展示账号,服务端白名单只放行接口 1/2/3/4,且响应中不含机构名/IP 明文;不靠前端"不显示" |
| 渠道隔离 | B屏渠道账号复用结算侧现成实现 settlement/backend/app/auth/zhaoyang_permissions.py:293-315(服务端按 sub 反查 channel_id,不信任客户端声明) |
结论:原 10 段有 1 段降级、1 段升级、需新增 1 段;新增的那段跑完就直接知道 A屏地图长什么样,建议排第一。
| 段 | 原用途 | v1.1 处置 | 理由 |
|---|---|---|---|
| 【1】机构地址覆盖率 | 判"可定位率"三档 | 🔽 降为选修 | D7 已定不采集坐标,判据作废;仍可跑,为将来"地址地理编码"增强路径留底数 |
| 【2】登录 IP 真实性体检 | 判是否要先修反代 | 🔼 升为第一优先 | 本轮已证代码层透传开着(2.1 第 5 段),此查询变成"验证配置真的生效"的唯一手段 |
| 【3】谁在登录 | 下钻面板给谁看 | 保留 | — |
| 【4】渠道维度分布 + 登录留存多久 | 渠道卡数据基础 | 保留,留存部分可跳过 | 留存已由代码确定 = 6 个月(audit.py:24) |
| 【5】销售额三口径 | 主口径选哪个 | 保留 | — |
| 【6】经营数据时间跨度 | 趋势能回溯多久 | 保留 | 与【4】留存不同源:这查订单表,不受 6 月清理影响 |
| 【7】席位到期分布 | 预警负载 | 保留 | — |
| 【8】未激活/永久席位 | 续费分母 | 保留 | — |
| 【9】续费与流失历史 | 曲线/甘特回溯 | 保留 | — |
| 【10】待审批积压 | 审批列表参数 | 保留 | — |
| 【11】新增 | A屏地图预演 | 🆕 新增,建议排第一 | 见下 |
用途:把 A屏地图"提前跑出来"——近 7 日 login_success 按机构去重后按省聚合,直接看出热力图有几个省亮、多少条 IP 为空、多少落私网。
-- 【11】A屏地图预演 · 近7日登录事件的机构覆盖与 IP 可用性
-- 只读;不含 ip2region 解析(需应用层),先看原始可用性
SELECT
COUNT(*) AS 登录成功事件数,
COUNT(DISTINCT u.linked_org_id) AS 覆盖机构数,
COUNT(*) FILTER (WHERE a.ip IS NULL) AS IP为空条数,
COUNT(*) FILTER (WHERE a.ip << '10.0.0.0/8'::inet
OR a.ip << '172.16.0.0/12'::inet
OR a.ip << '192.168.0.0/16'::inet
OR a.ip << '127.0.0.0/8'::inet) AS 私网IP条数,
COUNT(DISTINCT a.ip) AS 去重IP数
FROM auth_audit_logs a
JOIN users u ON u.id = a.user_id
WHERE a.event_type = 'login_success'
AND a.created_at > now() - interval '7 days'
AND u.linked_org_id IS NOT NULL;
怎么读:
| 结果 | 判断 |
|---|---|
| 覆盖机构数 ≥ 在营机构数 × 30% | A屏地图有内容,光点方案直接做 |
| 私网IP条数 / 总数 > 20% | 反代配置在生产未真正生效,先修部署再上光点(与 SQL【2】互为印证) |
| IP为空条数占比高 | 光点会稀疏,需在图例注明"仅统计可定位登录" |
| 覆盖机构数 很低 | 不是数据问题是业务问题(机构本身登录少),此时地图价值有限,热力改用「近 30 日」口径 |
结论:性能三条 + 视觉三条 + 数据五条;每条都能跑出个数字,不靠"看着还行"。
| # | 类别 | 验收项 | 判定 |
|---|---|---|---|
| 1 | 性能 | 投屏机 1080p 连续 30 分钟 | 帧率 ≥30fps,内存无上涨趋势 |
| 2 | 性能 | 动效只用 transform/opacity |
DevTools 无 layout thrash |
| 3 | 性能 | 数据刷新与动画解耦 | echarts setOption 增量合并,刷新不整屏重绘 |
| 4 | 视觉 | echarts 不出大屏目录 | grep -rln "echarts" web/app --include=*.tsx \| grep -v "/cockpit/" \| wc -l = 0 |
| 5 | 视觉 | 无全量 barrel import | grep -rn "from ['\"]echarts['\"]" web/app web/components \| wc -l = 0;对照组 echarts/core > 0 |
| 6 | 视觉 | 大屏 echarts chunk | gzip < 200 KB(贴 next build First Load JS) |
| 7 | 数据 | 两屏 KPI 逐项对比 | A屏 4 个数字 = B屏对应 4 个,不允许有 1 的差 |
| 8 | 数据 | 大屏活跃数 vs 现有活跃页 | 同一时刻取数,数字一致(证明复用口径没自算) |
| 9 | 数据 | 省份合计 | 34 省热力值合计 + 未定位数 = 全国总数 |
| 10 | 数据 | 光点 NULL/私网处理 | 抽查:ip IS NULL 的登录事件在图上 0 个点,且计入"未定位" |
| 11 | 权限 | A屏展示账号越权 | 直接请求接口 5/6/7/8 → 403;接口 1-4 响应体 grep 不到机构名与 IP |
| # | 决策 | 状态 |
|---|---|---|
| D1 | B屏地图续费状态点色 | ✅ 随 D7 了结(无点位无点色,状态放下钻清单徽章) |
| D2 | 深色科技风两屏统一 | 推荐统一(一套令牌,B屏只换皮不减密度) |
| D3 | A屏是否显示机构名 | 推荐不显示 |
| D4 | 深色令牌 v1 色板 | 推荐按 v1.0 第五节定稿(已过色觉/对比度机器校验) |
| D5 | A屏预警/异常计数是否对外显示 | 推荐做成开关,默认隐藏 |
| D6 | A屏热力口径:今日 or 近 7 日活跃 | 推荐近 7 日(B屏维持今日不变) |
| D7 | 是否采集机构坐标 | ✅ 已定(08-18 长官拍板):不采集 |
| D8 | B屏是否展示结算域操作留痕 | 🆕 新增待拍板。现状:audit_logs 生产账号无 SELECT 权限(settlement/db/grants.sql:48)。要展示需单开只读账号或视图(属主不转、不做 schema 迁移)。若本期不做,B屏「谁处置了什么」这一栏直接不上 |
| 序 | 事项 | 谁做 | 阻塞谁 |
|---|---|---|---|
| 1 | 长官看 A/B 两张原型图 + 拍 D2–D6、D8 | 长官 | 全部 |
| 2 | 跑摸底 SQL(先跑新增第【11】段和第【2】段) | 有生产库权限的同事 | 地图口径与光点是否成立 |
| 3 | 视 2 的结果决定:光点直接做 / 先修反代 / 热力改 30 日口径 | 主会话出结论页 | 实施 |
云机不连生产库,第 2 项云机做不了,这一条从 08-14 挂到今天没变。
本方案纯文档,零代码改动。第二节全部结论均为本轮(2026-08-19)在 worktree HEAD 8f92bd1b 上重新 grep 实证,含对照组自证;行号在 origin/test 上可能漂移,符号名不会。数据类数字(机构数、覆盖率等)一律未实测,需摸底 SQL 出结果。
云机环境 · 非验收证据。