交付出件箱

← 返回出件箱

渠道机构地理分布驾驶舱大屏 · 调研报告

版本 v1.0 · 2026-08-14 · 面向内部业务/管理层
调研人:dev1(东京隔离开发机)
调研对象仓库:doctor-ai-brain(核心) + settlement(结算/渠道域)


0. 一句话结论

能做,但这不是一个"前端画张地图"的活。

地图渲染、点位闪烁、统计卡片这些都是成熟技术,占不到总工作量的三分之一。真正的工程量集中在两个数据缺口上:

  1. 机构现在没有可用的地理坐标 —— 全库没有省/市字段,没有行政区划字典,机构地址是"选填",经纬度只有配置过门店名片的机构才有。地图有屏幕,但没有点可以点亮。
  2. 系统现在没有"在线状态"这个概念 —— 登录是无状态 JWT,登完就没有心跳,退出也不落地。"谁现在在线"目前无法回答,只能回答"谁最近登录过"。

好消息有两条,且是决定性的:

因此建议的路径是:先补数据,再上大屏。P0 阶段(约 2 周)用"签约地址 + 登录 IP 双轨定位"把静态地图和统计卡先跑通,P1 阶段(约 1 周)再补准实时刷新和在线状态口径。


1. 调研方法与证据边界(必读)

本报告所有技术结论均来自对代码的物理检索,每条结论在第 11 节给出 文件:行号 出处,可逐条复核。

明确的边界,不允许模糊:

项目 状态
代码层结论(表结构、字段、写入链路、部署参数) ✅ 已物理 grep 实证,可信
数据层结论(多少机构填了地址、IP 覆盖率、日均登录量) ❌ 未实测。本机是隔离开发机,按环境规矩不连生产库
性能结论(查询耗时、大屏并发) ❌ 未实测,缺生产同栈

凡涉及"填充率/数据量/耗时"的判断,本报告一律不给数字,而是把需要在测试环境或生产库执行的 SQL 整理进第 10 节,由具备权限的同事执行后回填。这是本项目的数据完整性铁律要求——没有实测数字就不下数据结论。


2. 业务现状盘点

2.1 主体与账号模型(与业务描述一致,代码实证如下)

系统里存在四类账号,全部收敛在统一用户表 users 中,用 role 区分:

角色 role 值 关联 登录入口
平台超管 admin 无 管理后台
平台员工 platform_staff 无 管理后台
渠道账号 channel channel_id → channels.id 渠道 H5
机构管理员 org_admin linked_org_id → organizations.id 管理后台
机构席位 org_seat linked_org_id → organizations.id 席位密码登录(H5/平板)

机构主体是 organizations 表;机构的签约信息(含归属渠道)在 org_signup_profiles 表,该表以 org_id 作主键与机构 1:1,channel_id 外键指向 channels。

这意味着"机构归属哪个渠道"是一个已经建模好的、可直接查询的强关联,不是靠命名约定猜出来的。

2.2 现有管理视图能力盘点

现有页面 后端 现在能看什么 缺什么
平台看板 /dashboard admin_dashboard.py 用户数/机构数/今日登录数/最近 8 条活动(含 IP 文本) 无地域维度,IP 只是一串字符
机构管理 /admin/orgs 机构 CRUD 相关端点 机构列表、席位、访问链接 纯表格,无地图
机构洞察 /admin/org-insights admin_activity.py 转化/风险/增长三榜、活跃机构数 无地域维度
席位活动 /admin/seat-activity admin_activity.py 行为流水 + 登录记录合并时间线 单机构视角,无全局
审计日志 /admin/audit 审计端点 登录成功/失败/锁定明细 纯表格
结算总览 /admin/minicup 结算域 dashboard.py 结算待办 与机构地理无关

结论:业务方描述的"仅能表格查询、缺乏全局视图"完全属实,且现有六个视图里没有任何一个带地理维度。


3. 目标能力矩阵(逐条对照建设目标)

# 建设目标 现状判定 说明
1 中国电子地图底图 🆕 需新建 前端无任何地图库,recharts 不支持地理投影
2 机构登录行为实时上屏 🟡 半成品 登录事实已落库且带真实 IP;缺的是"推到前端"的通道
3 对应地区自动点亮 🔴 卡点 无省市字段、无坐标、无 IP 归属地能力,地区无法判定
4 下钻:机构名称 ✅ 现成 organizations.name
5 下钻:归属渠道 ✅ 现成 org_signup_profiles.channel_id → channels.name,同库可 JOIN
6 下钻:登录 IP ✅ 现成 auth_audit_logs.ip(INET 列,生产记录真实公网 IP)
7 下钻:登录时段 ✅ 现成 auth_audit_logs.created_at(带索引)
8 下钻:在线状态 🔴 需新建 系统无在线概念,见 G3
9 下钻:机构基础信息 ✅ 现成 机构名/联系人/电话/启用状态/创建时间 + 签约档案
10 全国活跃机构数卡片 ✅ 可复用 现有 org-insights 已有活跃机构口径,可复用不重造
11 各省份活跃度卡片 🔴 卡点 同 #3,依赖省份归属能力
12 渠道对应机构分布卡片 ✅ 可复用 同库 JOIN,只差地理维度

判定汇总:12 项里 6 项现成、2 项可复用、1 项半成品、3 项需新建,其中 3 个"红灯"全部指向同一个根因——地理维度缺失。


4. 数据源盘点(本报告核心章节)

4.1 机构主体 organizations

字段 类型 对大屏的价值 备注
id / name int / varchar(200) 下钻主信息 ✅
contact_name / contact_phone varchar 下钻基础信息 可空
is_active bool 机构启停 机构级只有人工开关,无到期时间
created_at timestamptz 入驻时间 ✅
store_address varchar(200) 地理候选源 A ⚠️ 门店名片用途,席位自己在 H5 设置页填
latitude / longitude numeric(10,7) 坐标候选源 A ⚠️ 由微信选点回填,只有配置过名片的机构才有

关键判断:latitude/longitude 是 Phase 61 为"分享名片一键导航"设计的,来源是席位在 H5 手动选点,业务上从不强制。把它当地图的唯一数据源会导致大面积空白。覆盖率必须实测(第 10 节 SQL-1)。

4.2 渠道与签约 channels / org_signup_profiles

字段 对大屏的价值
channels.name / status / rate_percent 渠道名称、启停
org_signup_profiles.channel_id 机构 → 渠道归属(外键强关联)
org_signup_profiles.address 地理候选源 B(签约地址,varchar 240)
org_signup_profiles.status 签约状态(已开通/已到期/已停用等 9 态)

重大有利结论:结算域虽然在 Phase 60 被拆成独立服务 settlement-api,但生产部署是同库共存(doctor_ai_brain),只是换了受限账号 settlement_rw 做权限隔离。核心侧管理端用的是超管账号,可以直接 JOIN 这三张表,无需跨服务 HTTP 编排。

这一条把方案复杂度降了一个量级——否则"渠道分布卡片"就得走服务间接口、处理超时和一致性。

关于地址来源的取舍:签约地址 address 是入驻资料的一部分,业务语义比门店名片地址更正式、更可能被认真填写。但代码层它是 Optional(提交审核时校验的是执照/门头照/视光师资质/设备照四类文件,没有校验地址非空),所以同样存在空值风险,必须实测(SQL-2)。

4.3 登录事实 auth_audit_logs

这是"登录实时上屏"的事实源,字段完全够用:

字段 类型 用途
user_id int(可空,索引) 关联到席位/机构管理员/渠道账号
event_type varchar(20) 5 类事件,登录成功为 login_success
ip INET 登录 IP(原生 IP 类型,可直接做网段运算)
user_agent text 终端类型
detail JSONB 席位登录带 channel=seat_password + device_id
created_at timestamptz(索引) 登录时段

写入链路已验证覆盖全部角色:

IP 真实性已验证(这一点最容易被想当然搞错):

Nginx 侧传了 X-Real-IP / X-Forwarded-For;容器侧 uvicorn 启动带了 --proxy-headers --forwarded-allow-ips *。两边配套,因此 request.client.host 拿到的是真实客户端公网 IP,不是 Docker 网关地址。

旁证:席位登录限流那段代码里留有生产实测记录——"全量 38 个有失败记录的 IP"。如果记的是网关地址,这个数只可能是 1。

4.4 行为活跃 seat_activity_log

Phase 52 建的 append-only 行为流水,含 actor_type / org_id / event_type(功能打开/数据生成/提问/管理查看)/ created_at,并有 (org_id, created_at) 复合索引。

用途:如果"活跃"的口径要定义成"有实际业务行为"而不是"登录过",这张表就是事实源,且索引已经就位。

4.5 在线状态 —— 现状为零

必须把话说清楚:系统目前无法回答"这个机构现在是否在线"。

所以"在线状态"这四个字,必须先定义口径,再谈实现,见 G3 与决策点 D5。


5. 缺口清单

G1 · 机构地理坐标缺失(最高优先级 · 决定项目成败)

现象:全库没有 province / city 字段,没有行政区划字典表;坐标只在门店名片场景稀疏存在;两处地址字段均为选填。

影响:地图能画出来,但"点亮对应地区"和"各省份活跃度"两个核心目标直接无法实现。

建议:
1. 新增机构地理信息落地字段(省 / 市 / 区 / 经度 / 纬度 / 来源 / 置信度 / 更新时间)——建议独立小表 org_geo,不往 organizations 上堆列,避免与结算域的表属主权限打架;
2. 存量数据一次性回填:取"签约地址 → 门店名片地址"的优先级,批量地理编码;解析不出来的进人工待办清单,不允许静默留空;
3. 增量:入驻审核通过时同步解析并落库;
4. 兜底:解析失败的机构在大屏上进"未定位机构"计数卡,明示数量,绝不假装全国已覆盖。

G2 · IP 归属地能力为零

现象:全仓检索 geoip / ip2region / MaxMind / GeoLite 均 0 命中,无任何 IP 归属地库或服务。

影响:"登录点亮地区"这条实时链路缺最后一环——有 IP,但翻译不成省份。

建议:引入离线 IP 归属地库(如 ip2region xdb 或 MaxMind GeoLite2 City),装进 API 容器随镜像走。理由:

代价是需要定期更新库文件(季度级),可挂进现有的自动更新脚本。

G3 · 在线状态无定义

现象:见 4.5。

建议:不要为了"在线"去做长连接心跳(成本高、收益低)。推荐用活跃度分级替代二元在线:

状态 口径建议 大屏表现
在线 最近 15 分钟内有登录或业务行为 点位常亮 + 呼吸动效
今日活跃 今日有登录或业务行为 点位亮色
近期活跃 7 日内有活动 点位淡色
沉默 7 日以上无活动 点位灰色

阈值需业务方拍板(D5)。这个口径的最大好处:零新增埋点,纯查询即可实现,且口径可解释、可审计。

G4 · 实时上屏通道缺失

现象:管理端目前全是请求-响应式接口,没有面向管理后台的推送通道。

建议:先用轮询,不要一上来就上 SSE/WebSocket。理由:

若后续业务确有秒级需求,再单独立项升级为 SSE。

G5 · 审计留存策略:声明与实现不一致

现象:auth_audit_logs 模型注释写着"6 月留存 · APScheduler 每天 02:00 清",但全仓检索定时任务,只有 GPU 调度用到 APScheduler,没有任何审计清理任务的实现。

影响(对本项目反而偏正面,但必须说清):登录历史很可能从上线起一条没删,历史热力图/趋势回溯的数据基础比预期更好。但这是"注释与实现不符"的状态,属于隐患——哪天有人按注释补上清理任务,大屏的历史视图会突然被截断。

建议:本次立项时明确留存策略并写进文档,如果大屏需要长周期回溯,就把留存要求写死;同时按第 10 节 SQL-5 实测最早记录时间,确认真实留存跨度。

G6 · 两个低频但真实的边界缺陷

  1. 取不到客户端地址时会静默丢审计:拿不到 request.client 时,IP 传的是字面量字符串 "unknown",写入 INET 列会抛类型错误,而审计写入被 try/except 包住只打 warning 日志——这条登录记录会静默消失。生产走 Nginx 时极少触发,但大屏一旦把"登录数"当业务指标,就应该把它改成写 NULL 而不是丢整行。
  2. 根目录 Dockerfile.api 的启动命令没有 --proxy-headers:生产用的是 deploy/Dockerfile.api(参数齐全),根目录那份是历史遗留。若哪天有人误用它构建,所有登录 IP 会集体退化成网关地址,地图会瞬间全国只剩一个点。建议本次一并对齐或删除。

6. 待拍板的决策点

以下五条需要业务方明确,技术侧不替业务做主。每条给出推荐方案与理由。

D1 · 地图上的"位置"到底代表什么?

选项 含义 优点 缺点
A. 签约地址 机构注册在哪 稳定、权威、可核对 不反映实际使用地;地址缺失即无位置
B. 登录 IP 归属地 人这次在哪登的 天然实时,覆盖率高 城市级精度;移动网络/VPN 会漂移
C. 双轨(推荐) 底图点位=签约地址;点亮=IP 归属地 兼顾"机构在哪"和"谁在用";两者不一致时可标记异地登录,本身就是有价值的风控信号 实现成本略高

推荐 C。理由:业务描述里"地域分布"和"登录实时点亮"其实是两个不同的问题,用一个数据源硬扛必然有一边失真。

D2 · 刷新频率

推荐 15 秒轮询。可配置为 5~60 秒。理由见 G4。

D3 · 地图渲染技术选型

选项 优点 缺点
A. ECharts + 自托管中国 GeoJSON(推荐) 无需 Key、无外网依赖、机构地址与坐标不出网、离线可用、省市下钻成熟 需新增依赖,底图为矢量非卫星图
B. 高德/百度 JS API 底图精美、自带地理编码 需申请 Key 并计费;机构坐标与登录位置要发给第三方;页面依赖外网

推荐 A。合规和可控性优先,且内部驾驶舱不需要卫星底图。注意现有前端技术栈是 Next.js 15 + shadcn/ui + Tailwind + Recharts,Recharts 不支持地理投影,必须新增地图库——这是本项目唯一需要新增的前端依赖,需按技术栈规矩走审批。

D4 · IP 与地理信息的展示权限与脱敏

登录 IP 属于敏感信息。推荐:

需业务方确认是否接受。

D5 · 活跃/在线的统计口径

需明确三件事:

  1. "活跃"算登录还是算业务行为(或两者取并集)?推荐取并集;
  2. "在线"的时间窗取多少分钟?推荐 15 分钟;
  3. 统计卡片的周期是日/周/月?推荐大屏默认"今日",可切换 7 日 / 30 日。

7. 建议技术架构

7.1 数据层

动作 内容
新增 org_geo 表 org_id(PK,FK) / province / city / district / lng / lat / source(签约地址|门店名片|人工) / confidence / updated_at
新增 IP 归属地能力 离线库文件随镜像分发 + 解析结果 Redis 缓存(IP→省市,TTL 24h)
索引优化 auth_audit_logs 增加 (event_type, created_at) 复合索引——大屏所有查询都是"最近 N 分钟的 login_success",现有单列索引不够
存量回填 一次性脚本 + 人工待办清单,回填后必须跑第 10 节的填充率 SQL 验收,任一省份列为 0 不许标完成

7.2 服务层(核心侧,超管账号,可跨结算表 JOIN)

新增只读端点,统一前缀 /api/admin/geo-cockpit:

端点 用途
GET /summary 顶部统计卡:全国活跃机构数、在线数、今日登录次数、覆盖省份数、未定位机构数
GET /provinces 各省份聚合:机构总数 / 活跃数 / 今日登录数(驱动地图着色)
GET /points 点位列表:机构坐标 + 活跃状态(支持按省份、渠道筛选)
GET /recent-logins 最近 N 条登录事件(驱动"点亮"动效与右侧滚动栏)
GET /orgs/{org_id} 下钻详情:机构基础信息 + 归属渠道 + 最近登录 IP/时间/终端 + 活跃状态
GET /channels 渠道维度分布:各渠道机构数与省份分布

权限:require_role("admin"),并在菜单注册表新增 platform.geo_cockpit 菜单键(本项目菜单权限有单一事实源注册表,新页面必须注册,否则 RBAC 不认)。

7.3 前端层


8. 分期路线与工作量估算

⚠️ 以下为经验估算,非实测,且不含需求评审与验收往返时间。

阶段 内容 估算 交付标志
P0 · 数据地基 org_geo 表 + IP 归属地库接入 + 存量地址批量地理编码 + 填充率验收 4~5 人日 填充率 SQL 全部 >0,未定位清单明确
P0 · 后端接口 6 个只读端点 + 复合索引 + 菜单注册 3~4 人日 接口真打通过,无 mock
P0 · 前端大屏 地图 + KPI 卡 + 点位下钻 + 渠道分布卡 5~6 人日 测试环境可投屏演示
P0 小计 约 12~15 人日(2~3 周) 静态版大屏可用
P1 · 准实时 15 秒轮询增量、点亮动效、在线状态口径落地、token 静默续期 3~4 人日 登录后 15 秒内上屏
P2 · 增强(可选) 异地登录标记、省份下钻到地市、历史回放、大屏主题与投屏模式 5~8 人日 按业务优先级排

关键前置:P0 的三块里,数据地基必须先做完。先做前端会得到一张漂亮但没有点的空地图,这是本项目最需要避免的返工。


9. 风险与合规

# 风险 等级 应对
R1 地址填充率过低,地图大面积空白 🔴 高 上线前先跑 SQL-1/2 摸底;低于阈值则先补数据再排期,不硬上
R2 IP 归属地精度只到城市级,移动网络会跨市漂移 🟡 中 大屏标注"登录位置为 IP 归属地,仅供参考";机构位置以签约地址为准
R3 登录 IP + 地理位置上屏涉及经营信息与个人信息 🟡 中 权限限 admin;员工视角脱敏;投屏模式默认打码(D4)
R4 第三方地理编码会把机构地址发出去 🟡 中 优先离线方案;若必须用在线地理编码,仅在一次性回填时使用,且经业务确认
R5 大屏常驻导致登录态过期白屏 🟡 中 实现静默续期 + 断线重连提示(P1 必做)
R6 大屏轮询查询打在核心库上 🟢 低 加复合索引 + 结果缓存 10 秒;生产库内存配置偏紧,需在测试环境压一轮
R7 若误用根目录 Dockerfile 构建,IP 全部退化 🟢 低 本次一并对齐或删除(G6-2)

10. 上线前必须实测的 SQL 清单

以下 SQL 必须在具备权限的环境(测试库/生产库)执行,结果回填后本报告才算完成数据侧论证。本机不连生产库,不代跑,也不估算。

-- SQL-1 门店名片坐标覆盖率(判断候选源 A 可用性)
SELECT COUNT(*) AS org_total,
       COUNT(*) FILTER (WHERE latitude IS NOT NULL AND longitude IS NOT NULL) AS has_latlng,
       COUNT(*) FILTER (WHERE store_address IS NOT NULL AND store_address <> '') AS has_store_addr
FROM organizations
WHERE is_active = true;

-- SQL-2 签约地址覆盖率(判断候选源 B 可用性 · 大概率是主力数据源)
SELECT COUNT(*) AS profile_total,
       COUNT(*) FILTER (WHERE address IS NOT NULL AND address <> '') AS has_address,
       COUNT(DISTINCT channel_id) AS channel_cnt
FROM org_signup_profiles;

-- SQL-3 双源合并后的真实可定位率(决定地图是否有意义)
SELECT COUNT(*) AS total,
       COUNT(*) FILTER (
         WHERE COALESCE(NULLIF(p.address,''), NULLIF(o.store_address,'')) IS NOT NULL
      ) AS locatable
FROM organizations o
LEFT JOIN org_signup_profiles p ON p.org_id = o.id
WHERE o.is_active = true;

-- SQL-4 登录 IP 真实性抽验(若绝大多数落在 172./10./192.168. 私网段 → 代理头配置有问题,立即停止排期)
SELECT COUNT(*) AS login_30d,
       COUNT(DISTINCT ip) AS distinct_ip,
       COUNT(*) FILTER (WHERE ip << '172.16.0.0/12' OR ip << '10.0.0.0/8' OR ip << '192.168.0.0/16') AS private_ip
FROM auth_audit_logs
WHERE event_type = 'login_success' AND created_at > now() - interval '30 days';

-- SQL-5 审计真实留存跨度(验证 G5:清理任务未实现的推断)
SELECT MIN(created_at) AS earliest, MAX(created_at) AS latest, COUNT(*) AS rows
FROM auth_audit_logs;

-- SQL-6 登录角色构成(确认席位登录是否占主体 · 影响下钻信息设计)
SELECT u.role, COUNT(*) AS login_cnt, COUNT(DISTINCT u.linked_org_id) AS org_cnt
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 '30 days'
GROUP BY u.role ORDER BY login_cnt DESC;

-- SQL-7 渠道维度机构分布(渠道卡片数据基础)
SELECT c.name AS channel_name, COUNT(p.org_id) AS org_cnt,
       COUNT(*) FILTER (WHERE p.status = '已开通') AS activated_cnt
FROM channels c
LEFT JOIN org_signup_profiles p ON p.channel_id = c.id
GROUP BY c.name ORDER BY org_cnt DESC;

-- SQL-8 日均登录量(评估轮询频率与大屏刷新压力)
SELECT date_trunc('day', created_at) AS d, COUNT(*) AS login_cnt
FROM auth_audit_logs
WHERE event_type = 'login_success' AND created_at > now() - interval '30 days'
GROUP BY 1 ORDER BY 1 DESC;

验收红线:SQL-3 的 locatable / total 若低于业务可接受阈值(建议不低于 70%),不应直接进入前端开发,应先安排数据补录。


11. 证据索引(可逐条复核)

结论 证据位置
机构表字段与经纬度用途 app/models/organization.py:11-58
统一用户表六角色 / 渠道关联 app/models/user.py:26-70,135-153
渠道表定义 app/models/settlement.py:66-105
签约档案含 channel_id 与 address app/models/settlement.py:109-145、settlement/backend/app/models/settlement.py:213
登录审计表字段(IP 为 INET) app/models/audit.py:24-60
管理端登录写审计 app/services/auth_service.py:249-256
席位登录写审计 app/api/seat_auth.py:362-369
审计写入失败被吞(G6-1) app/services/auth_service.py:487-510
取不到客户端时 IP 传 "unknown" app/api/auth.py:69、app/api/seat_auth.py:135
Nginx 传真实 IP 头 deploy/nginx/chat.tongban.love.conf:107-111
生产 uvicorn 带 proxy-headers deploy/Dockerfile.api:67
根目录 Dockerfile 缺该参数(G6-2) Dockerfile.api:12
结算与核心同库共存、账号隔离 settlement/deploy/docker-compose.prod.yml:32-34、settlement/deploy/README.md:24、settlement/db/grants.sql:33-48
入驻表单 address 为选填 settlement/backend/app/api/settlement/store_signup.py:98,125-136
行为流水表与索引 app/models/activity.py:55-77
access token 30 分钟 app/auth/jwt_local.py:26
现有看板 KPI 口径 app/api/admin_dashboard.py:54-110
现有机构洞察/席位活动 app/api/admin_activity.py:1-45
对外 API 会话管理(不覆盖席位) app/api/admin_sessions.py:1-30
菜单注册单一事实源 app/core/menu_registry.py:17-38
前端无地图库、图表用 Recharts web/package.json:14-44
全仓无 IP 归属地库 检索 geoip\|ip2region\|maxmind\|geolite → 0 业务命中
无审计清理定时任务(G5) 检索 APScheduler → 仅 app/services/gpu_scheduler_service.py:7

12. 结论与建议

  1. 项目可行,核心数据关系(机构 ↔ 渠道 ↔ 登录事实)已经建模完整且同库可查,这是最大的有利条件。
  2. 不要按"做个大屏"排期,要按"补地理数据 → 建接口 → 画大屏"三段排期,数据地基是关键路径。
  3. 先做第 10 节的 SQL-1/2/3/4 摸底,这是四条一天之内就能出结果的查询,却直接决定排期是 2 周还是 6 周。建议在正式立项评审前完成。
  4. 五个决策点(D1~D5)需要业务方拍板,特别是 D1(位置的业务含义)和 D5(活跃/在线口径),这两条定了,技术方案才能定稿。
  5. 本报告为调研结论,不含任何已实施的代码改动。进入实施需按项目需求变更流程立项,分配 Phase 编号,并补充技术方案文档。

本报告结论基于代码物理检索,数据层结论待第 10 节 SQL 实测回填。本机为隔离开发环境,未连接生产库,不提供任何未经实测的数据指标。