unsetunset一、写在前面:为什么要做这个项目?unsetunset
做后端的朋友都知道,Kibana 很强,但有时候客户要的是更炫酷的演示效果。
比如:
实时网络攻击地图,要能看到飞线在地球上穿梭
全球服务器热力图,要3D效果,要能拖拽旋转
电商实时交易流,要科幻感,要一眼抓住眼球
这些需求,通常让后端工程师头疼:
WebGL、Three.js 这些前端技术,我们不太熟啊!
但有了 Cursor + AI 辅助,这事儿就简单了。今天我就用大白话,手把手带你从零搭建一个
3D 攻防态势大屏,全程不写一行 CSS(Tailwind 搞定),10 分钟让 ES 数据变成会动的 3D 地球。
unsetunset二、技术栈选型:为什么选这些?unsetunset
2.1 前端技术栈
React + TypeScript:类型安全,开发体验好
Vite:启动快,热更新快,适合快速迭代
react-globe.gl:基于 Three.js 封装,代码量少,效果炸裂
Tailwind CSS:不用手写 CSS,样式全靠类名
ECharts:图表库,做 TOP5 排行榜
为什么不用原生 Three.js?因为
react-globe.gl
已经把地球、飞线、标签这些常用功能封装好了,我们只需要传数据,不用关心 WebGL 细节。
2.2 后端技术栈
FastAPI:Python 异步框架,写接口快Elasticsearch 8.x:聚合查询,拿 TOP50 攻击对python-dotenv:环境变量管理
为什么选 FastAPI?因为它自动生成 Swagger 文档,接口测试方便,而且性能好。
unsetunset三、核心实现:三步走策略unsetunset
3.1 第一步:先让地球转起来(视觉底座)
不管数据,先把炫酷的 3D 地球做出来,镇住场子。
3.1.1 安装依赖
cd frontend npm install react-globe.gl three @types/three npm install -D tailwindcss @tailwindcss/vite3.1.2 创建地球组件
核心代码在src/components/CyberGlobe.tsx:
import * as THREE from 'three' export default function CyberGlobe() { return ( <Globe backgroundColor="#000011" // 深邃太空黑 enablePointerInteraction // 允许拖拽 globeMaterial={ new THREE.MeshStandardMaterial({ map: createTechTexture(), // 科技感贴图 emissive: new THREE.Color('#1d4ed8'), emissiveIntensity: 0.18, }) } /> ) }关键点:
1.背景色:#000011是深蓝黑,比纯黑更有层次
2.地球材质:用程序化生成的 Canvas 贴图,不用找图片资源
3.自转:通过controls.autoRotate = true实现
3.1.3 添加大气层辉光
为了让地球更有“科幻感”,我们加一层青色发光边缘:
useEffect(() => { const scene = globe.scene() const glowGeom = new THREE.SphereGeometry(101.2, 64, 64) const glowMat = new THREE.MeshBasicMaterial({ color: '#22d3ee', transparent: true, opacity: 0.18, blending: THREE.AdditiveBlending, // 叠加混合 side: THREE.BackSide, // 只渲染背面,形成边缘光 }) scene.add(new THREE.Mesh(glowGeom, glowMat)) }, [])效果:地球边缘会有一圈淡淡的青色光晕,像科幻电影里的星球。
3.2 第二步:接入攻击数据(飞线动画)
地球有了,接下来让攻击流量在地球上飞起来。
3.2.1 数据结构定义
type AttackArc = { startLat: number // 攻击源纬度 startLng: number // 攻击源经度 endLat: number // 目标纬度 endLng: number // 目标经度 label: string // 攻击类型:DDoS、SQL注入等 color: string // 颜色:红色=严重,黄色=中等 count: number // 攻击次数 sourceCountry: string // 来源国家 }3.2.2 渲染飞线
<Globe arcsData={attacks} // 攻击数据数组 arcStartLat={(d) => d.startLat} arcStartLng={(d) => d.startLng} arcEndLat={(d) => d.endLat} arcEndLng={(d) => d.endLng} arcColor={(d) => d.color} arcDashLength={ 0.38 } // 虚线长度 arcDashGap={ 1.6 } // 虚线间隔 arcDashAnimateTime={ 2200 } // 动画时长(毫秒) />效果:每条攻击会显示成一条带动画的虚线,从源点飞向目标,像导弹轨迹。
3.2.3 添加攻击标签
<Globe labelsData={attacks} labelLat={(d) => d.startLat} labelLng={(d) => d.startLng} labelText={(d) => shortLabel(d.label)} // 避免中文变 ???? labelColor={(d) => d.color} />注意:react-globe.gl的标签默认不支持中文(会显示????),所以我们用shortLabel()转成英文短码(如DDoS、SQLi)。
3.3 第三步:对接 Elasticsearch(数据聚合)
前端效果有了,现在把真实的 ES 数据接进来。
3.3.1 ES 聚合查询 DSL
核心逻辑在backend/main.py的get_attacks()函数:
body = { "size": 0, # 只要聚合结果,不要原始文档 "query": { "bool": { "filter": [ {"range": {"@timestamp": {"gte": "now-15m", "lte": "now"}}}, {"exists": {"field": "source.geo.location"}}, {"exists": {"field": "dest.geo.location"}}, ] } }, "aggs": { "top_pairs": { "multi_terms": { # 组合键聚合:源IP + 目的IP "terms": [ {"field": "source.ip"}, {"field": "dest.ip"} ], "size": 50, # TOP50 "order": {"_count": "desc"} }, "aggs": { "sample": { "top_hits": { # 从每个 bucket 里取一条样本 "size": 1, "_source": { "includes": [ "source.geo.location", "dest.geo.location", "event.action", "source.geo.country_name" ] } } } } } } }这个查询的逻辑:
1.时间过滤:只查最近 15 分钟的数据
2.multi_terms 聚合:按“源IP + 目的IP”组合键分组,统计攻击次数
3.top_hits 子聚合:从每个分组里取一条样本,拿到地理坐标和国家名
4.排序:按_count降序,取前 50 个
为什么用 multi_terms?
因为我们要找的是“哪些 IP 对攻击最频繁”,而不是单个 IP。
multi_terms可以同时按两个字段分组,正好满足需求。
3.3.2 数据清洗
ES 返回的是 buckets 结构,我们需要转成前端需要的格式:
for bucket in buckets: source_ip, dest_ip = bucket['key'] count = bucket['doc_count'] # 从 top_hits 里取地理坐标 hit = bucket['sample']['hits']['hits'][0] source_geo = hit['_source']['source']['geo']['location'] dest_geo = hit['_source']['dest']['geo']['location'] out.append({ "startLat": source_geo['lat'], "startLng": source_geo['lon'], "endLat": dest_geo['lat'], "endLng": dest_geo['lon'], "label": hit['_source']['event']['action'], "color": _color_for_label(...), # 根据攻击类型选颜色 "count": count, "sourceIp": source_ip, "destIp": dest_ip, "sourceCountry": hit['_source']['source']['geo']['country_name'] })关键点:
-geo.location可能是{"lat": 1.0, "lon": 2.0}或[lon, lat],需要统一处理
攻击类型映射颜色:DDoS=红色,SQL注入=黄色,XSS=橙色
3.3.3 容错处理
ES 连接失败时,返回 mock 数据,保证演示不中断:
try: resp = es.search(index=ES_INDEX, body=body) except (ESConnectionError, ApiError): if MOCK_ON_ES_DOWN: return _mock_attacks(20) # 返回模拟数据 return []unsetunset四、UI 完善:HUD 面板 + 核心国家unsetunset
4.1 左右 HUD 面板
4.1.1 左侧:实时攻击列表![]()
样式要点:
hud-panel:半透明黑底 + 青色细线边框 + 背景模糊等宽字体(JetBrains Mono):数字对齐好看
4.1.2 右侧:来源国家 TOP5
用 ECharts 做横向柱状图:
国家名中文化:
用i18n-iso-countries自动翻译:
import countries from 'i18n-iso-countries' countries.registerLocale(zh) function normalizeCountryName(name: string) { if (hasCJK(name)) return name // 已经是中文,直接返回 return countries.getName(name, 'zh') || '其他' }4.2 核心国家标注
在地球上固定标注 8 个核心国家(美国、中国、俄罗斯等),用脉冲光圈显示攻击强度:
const corePoints = [ { code: '中国', name: '中国', lat: 39.9042, lng: 116.4074 }, { code: '美国', name: '美国', lat: 38.9072, lng: -77.0369 }, // ... ] <Globe pointsData={corePoints} // 固定点位 ringsData={coreRings} // 脉冲环 ringMaxRadius={(d) => 2.8 + d.intensity * 5.5} // 强度越大,环越大 ringPropagationSpeed={(d) => 0.9 + d.intensity * 2.0} // 强度越大,速度越快 />效果:每个核心国家会有一个青色脉冲环,攻击越多,环越大、越快。
unsetunset五、数据造数:一键灌入演示数据unsetunset
5.1 造数脚本
backend/seed_data.py可以快速生成大量测试数据:
python seed_data.py --count 200000 --hot-pairs 200 --minutes 15 --refresh参数说明:
--count:总文档数(建议 20 万起步)
--hot-pairs:热点 IP 对数量(越小越集中,飞线更“爆炸”)
--minutes:时间窗口(默认 15 分钟,匹配接口查询)
--delete-index:删除旧索引,重新开始(危险操作)
核心逻辑:
1.预生成若干“热点 IP 对”,78% 的数据走这些热点,保证 TOP50 有戏
2.国家名直接用中文(从ZH_COUNTRIES列表选),避免前端翻译问题
3.用helpers.streaming_bulk()批量写入,性能好
unsetunset六、部署与运行unsetunset
6.1 环境准备
后端:
cd backend pip install -r requirements.txt # 配置 .env 文件(ES 地址、账号密码) python -m uvicorn main:app --reload --port 8000前端:
cd frontend npm install npm run dev # 开发模式 # 或 npm run build && npm run preview # 生产模式6.2 访问地址
前端大屏:http://localhost:5173
后端接口:http://127.0.0.1:8000/api/attacks
API 文档:http://127.0.0.1:8000/docs
unsetunset七、踩坑总结unsetunset
7.1 中文标签显示
????
问题:react-globe.gl的labelsData用 Canvas 渲染,默认字体不支持中文。
解决: 用htmlElementsData+ DOM 渲染,或者用shortLabel()转成英文短码。
7.2 国家名中英混杂
问题:ES 返回的国家名可能是英文,前端显示混乱。
解决:
1.造数时直接用中文国家名(seed_data.py)
2.前端用i18n-iso-countries自动翻译 3.未命中映射的统一显示“其他”,避免混杂
7.3 ES 连接失败导致 500
问题:ES 未启动时,接口返回 500,前端报错。
解决:加 try-catch,ES 失败时返回 mock 数据,保证演示不中断。
unsetunset八、项目结构unsetunset
es3dPrj/ ├── backend/ │ ├── main.py # FastAPI 接口 │ ├── seed_data.py # 数据造数脚本 │ ├── requirements.txt # Python 依赖 │ └── .env # ES 配置 └── frontend/ ├── src/ │ ├── components/ │ │ ├── CyberGlobe.tsx # 3D 地球组件 │ │ ├── HudLeft.tsx # 左侧攻击列表 │ │ └── HudRight.tsx # 右侧 TOP5 图表 │ ├── lib/ │ │ ├── api.ts # 接口调用 │ │ ├── attackTypes.ts # 数据类型定义 │ │ ├── coreCountries.ts # 核心国家配置 │ │ └── countryNormalize.ts # 国家名标准化 │ └── App.tsx # 主入口 └── package.jsonunsetunset九、总结unsetunset
这个项目展示了如何用AI 辅助开发,快速实现一个“看起来很难”的 3D 可视化大屏:
1.前端:React + Three.js,10 分钟出效果
2.后端:FastAPI + ES 聚合,数据清洗简单
3.数据:一键造数,演示不愁
核心价值:
-后端工程师也能做出炫酷的前端效果-ES 聚合查询 + 3D 可视化,数据展示更直观-代码结构清晰,易于扩展和维护
unsetunset十、参考资料unsetunset
react-globe.gl 官方文档
https://github.com/vasturiano/react-globe.gl
FastAPI 官方文档
https://fastapi.tiangolo.com/
Text2DSL——自然语言转 Elasticsearch / Easysearch DSL 神器
基于 Easysearch + Flip 的多模态图像搜索引擎系统实战指南
打造你的企业级智能文档问答系统——Everything plus RAG 实战指南
更短时间更快习得更多干货!
和全球 2100+ Elastic 爱好者一起精进!
AI时代,比同事抢先一步学习进阶干货!