效果炸裂!10分钟把 Elasticsearch 数据变成科幻级 3D 地球,这波操作稳了

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() 转成英文短码(如 DDoSSQLi)。


3.3 第三步:对接 Elasticsearch(数据聚合)

前端效果有了,现在把真实的 ES 数据接进来。

3.3.1 ES 聚合查询 DSL

核心逻辑在 backend/main.pyget_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.gllabelsData 用 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

https://github.com/vasturiano/react-globe.gl

  • FastAPI 官方文档

https://fastapi.tiangolo.com/


Text2DSL------自然语言转 Elasticsearch / Easysearch DSL 神器

基于 Easysearch + Flip 的多模态图像搜索引擎系统实战指南

打造你的企业级智能文档问答系统------Everything plus RAG 实战指南

更短时间更快习得更多干货!

和全球 2100+ Elastic 爱好者一起精进!

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