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 安装依赖
go
cd
frontend
npm
install
react-globe.gl
three
@types/three
npm
install
-D
tailwindcss
@tailwindcss/vite
3.1.2 创建地球组件
核心代码在 src/components/CyberGlobe.tsx:
go
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 添加大气层辉光
为了让地球更有"科幻感",我们加一层青色发光边缘:
go
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 数据结构定义
go
type
AttackArc = {
startLat: number
// 攻击源纬度
startLng: number
// 攻击源经度
endLat: number
// 目标纬度
endLng: number
// 目标经度
label: string
// 攻击类型:DDoS、SQL注入等
color: string
// 颜色:红色=严重,黄色=中等
count: number
// 攻击次数
sourceCountry: string
// 来源国家
}
3.2.2 渲染飞线
go
<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 添加攻击标签
go
<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() 函数:
go
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 结构,我们需要转成前端需要的格式:
go
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 数据,保证演示不中断:
go
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 自动翻译:
go
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 个核心国家(美国、中国、俄罗斯等),用脉冲光圈显示攻击强度:




go
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 可以快速生成大量测试数据:
go
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 环境准备
后端:
go
cd backend
pip install -r requirements.txt
# 配置 .env 文件(ES 地址、账号密码)
python -m uvicorn main:app --reload --port 8000
前端:
go
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
go
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.json
unsetunset九、总结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 官方文档
Text2DSL------自然语言转 Elasticsearch / Easysearch DSL 神器
基于 Easysearch + Flip 的多模态图像搜索引擎系统实战指南
打造你的企业级智能文档问答系统------Everything plus RAG 实战指南

更短时间更快习得更多干货!
和全球 2100+ Elastic 爱好者一起精进!

AI时代,比同事抢先一步学习进阶干货!