《狂人日记NO.9》— 前后端一把梭,我的全栈实录

前言

2026年上半年,我主导完成了一个"卫星网络仿真可视化平台"的前后端全栈开发。

这是一个面向卫星网络架构设计与评估的专业工具------它能在三维地球上实时渲染成千上万颗卫星组成的多层星座、星间/星地通信链路、地面信关站与用户终端,支持仿真回放、任务链推演和网络架构优化三大核心业务场景。

回头看这段开发历程,从 3D 渲染的性能优化到数据库的分表策略,从实时通信的协议设计到场景渲染的分层架构------每一步都值得记录。

一切的起点

故事的开始,源于一个朴素的需求:在浏览器里看到卫星网络的运行状态

传统的卫星网络仿真大多依赖离线计算与静态图表输出,缺乏直观的可视化手段。我们需要的是一个可交互、可回放、可优化的 Web 端可视化平台。

技术选型上,Cesium 是唯一能在浏览器中渲染真实地球和太空的开源方案。但 Cesium 只提供了"画布",离一个完整的卫星网络仿真平台还有很远------我们需要的是:

  • 成千上万颗卫星的轨道计算与实时渲染
  • 星间/星地链路的动态连线
  • 业务流的多跳路径可视化
  • GB 级仿真数据的存储、查询与推送
  • 网络架构优化的版本对比与交互

这就是整个项目的起点------把一个专业仿真工具搬上 Web


整体技术架构

项目采用了前后端分离架构,中间通过 HTTP API + WebSocket 双通道通信。

前端:Vue 3 + TypeScript + Vite 构建,以 Cesium 为 3D 地球引擎,Pinia 管理业务状态,ECharts/D3 支撑数据图表。

后端:NestJS + TypeORM + MySQL,通过 WebSocket 网关实现仿真数据实时推送,支持 Docker 容器化部署。

共享层 :通过 npm 包 snss-types 实现前后端类型定义一致,避免接口对接中的类型漂移。


前端:从 Pinia 到 Class-Based Store 的架构演进

做复杂可视化应用,状态管理是第一个要解决的问题。

最初我们使用标准的 Pinia defineStore,将卫星、节点、链路等数据全部存入 Pinia state。随着状态字段膨胀到 50+ 个、数据量达到 10000+ 条时,一个问题逐渐凸显:深度响应式开销过大

Vue 的 reactive 会递归追踪数组中每个元素的每个字段,当 watch 开启 deep: true 时,性能急剧下降。而仿真场景中数据更新频率极高------时间轴每帧都要刷新链路拥塞状态、业务流路径。

于是我们做了一次重大重构------Class-Based Store ,用 ref/shallowRef 替代 reactive,精确控制响应式粒度。

typescript 复制代码
// 核心设计:get/set 自动解包,外部调用无感知
class BusinessClassStore {
  // 基本类型用 ref
  private _currentCaseId = ref<string>('')
  get currentCaseId() { return this._currentCaseId.value }
  set currentCaseId(v: string) { this._currentCaseId.value = v }

  // 数组用 shallowRef 避免深层响应式
  private _satelliteList = shallowRef<SatelliteItem[]>([])
  get satelliteList() { return this._satelliteList.value }
  set satelliteList(v: SatelliteItem[]) { this._satelliteList.value = v }
}

export const businessStore = new BusinessClassStore()

关键设计决策:

  • 双层响应式 :基础类型用 ref,大数组用 shallowRef,只有整体替换才触发更新------action 中始终用 this.linkList = [...] 整体赋值
  • get/set 拦截器 :外部调用 businessStore.satelliteList 直接拿值,无需 .value,API 完全兼容原 Pinia store
  • 浅监听体系 :9 个关键组件共 30+ 个 watch 全部使用浅监听(如 watch(() => store.linkList, ...)),配合 action 中整体重新赋值,既保证了渲染响应,又避免了深层追踪的性能损耗
  • 模板不受影响 :Pinia state 本身是 reactive 的,模板 v-for 仍能正确响应 push/splice,但 watch 浅监听要求 action 整体替换

这套方案在保持开发者体验不变的前提下,将状态变更的响应式追踪成本降到了最低。


前端:双模式 3D 渲染引擎的性能优化

卫星网络可视化最大的挑战在于量级------成千上万颗卫星在三维地球上的实时位置更新。

Cesium 提供了两种渲染模式,我们同时做了实现,让用户在不同数据量下自由切换:

模式一:Entity 模式(精细渲染)

适合少量卫星的高精度展示:

javascript 复制代码
// 每个卫星是独立的 Entity,支持 billboard + model + label + path
const entity = dataSource.entities.add({
  position: SampledPositionProperty, // 拉格朗日插值
  billboard: { image, scale },
  model: { uri: gltfModel }, // 3D 模型
  label: { text, font },
  path: { material, width }, // 轨道线
})

// 卫星姿态随速度方向自动调整
entity.orientation = new VelocityOrientationProperty(entity.position)
  • 使用 SampledPositionProperty + LagrangePolynomialApproximation 实现平滑插值
  • VelocityOrientationProperty 让卫星始终"头朝前飞",视觉效果专业
  • 支持卫星传感器视椎体(SatelliteSensor),可展示卫星对地覆盖范围

模式二:Primitive 批量渲染

针对成千上万颗卫星的高性能场景,关键在于批量坐标更新轨道计算性能

Primitive 批量坐标更新流程

Cesium 的 BillboardCollectionLabelCollection 是高性能渲染的关键------它们将成千上万个点/标签打包为一次 GPU DrawCall,而非逐元素渲染。

整个批量坐标更新分为三步:

Step 1:初始化时批量创建 Primitive,位置先用零向量占位

javascript 复制代码
// BillboardCollection + LabelCollection 各一次 DrawCall 渲染全部卫星
const sateBillboardCollection = new Cesium.BillboardCollection({
  modelMatrix: new Cesium.Matrix4(), // 共享变换矩阵
})
const sateLabelCollection = new Cesium.LabelCollection({
  modelMatrix: new Cesium.Matrix4(),
})

for (const item of satelliteList) {
  // 每个卫星一个 Billboard,初始位置填 (0,0,0)
  const billboard = sateBillboardCollection.add({
    id: item.id,
    image: 'resources/satellite/icons/icon1.png',
    position: Cesium.Cartesian3.ZERO, // 零向量占位
    scaleByDistance: new Cesium.NearFarScalar(1e4, 1, 1e8, 0.5),
  })
  sateBillboardMap[item.id] = billboard
  // 同步创建 label...
}

Step 2:启动自研轨道外推库,初始化实时传播器

javascript 复制代码
import * as KBSatellite from 'kbe3d-satellite'

// 传入全体卫星的 TLE 参数,返回实时传播控制器
const liveControl = await KBSatellite.sgp4OrbitLive(satelliteList, {
  isGetDetails: false, // 不返回速度/经纬度,减少计算量
  isUseGPU: false, // 可切换 GPU 模式
})

Step 3:60ms 定时器批量更新所有 Primitive 的坐标

javascript 复制代码
// 预分配 scratch 对象,复用避免 GC
const sateTempCart3 = {} // { satelliteId: Cartesian3 }

function updateSatelliteLive() {
  // 1) 从自研轨道库批量获取当前时刻所有卫星的 ECI 坐标
  liveControl.update(currentTime).then((res) => {
    // 2) 计算 ICRF → Fixed 旋转矩阵(只需算一次!)
    icrfToFixed = Cesium.Transforms.computeIcrfToFixedMatrix(now)

    // 3) 遍历所有卫星,仅更新 ICRF 坐标(不转换到 ECF)
    res.forEach((value, key) => {
      const pos = value.positionEci // 单位:km
      sateTempCart3[key].x = pos.x * 1000
      sateTempCart3[key].y = pos.y * 1000
      sateTempCart3[key].z = pos.z * 1000
      sateBillboardMap[key].position = sateTempCart3[key]
      sateLabelMap[key].position = sateTempCart3[key]
    })

    // 4) 一次赋值 modelMatrix,完成全体坐标变换!
    //    Cesium GPU 侧自动将 ICRF 坐标旋转到 ECF
    sateBillboardCollection.modelMatrix
      = Cesium.Matrix4.fromRotationTranslation(icrfToFixed)
    sateLabelCollection.modelMatrix
      = Cesium.Matrix4.fromRotationTranslation(icrfToFixed)
  })
}

// 60ms 定时器(约 16fps 更新频率,视觉上完全平滑)
setInterval(updateSatelliteLive, 60)

为什么这样设计?

ICRF 坐标 + 共享 modelMatrix 是核心优化。SGP4 轨道外推输出的天然是 ICRF(地心惯性系)坐标。常规做法是逐颗卫星做 ICRF→ECF 矩阵乘法转换------26000 颗卫星意味着每帧 26000 次 3×3 矩阵乘法。

我们利用 Cesium 的 modelMatrix 机制:将所有卫星位置保持在 ICRF 坐标系,把 ICRF→ECF 旋转矩阵整体赋值给 Collection 的 modelMatrix,Cesium 的 GPU 着色器会自动对 Collection 内所有点做坐标变换。这样原本 26000 次 CPU 矩阵乘法变成了 1 次 CPU 赋值 + 1 次 GPU 并行变换。

scratch 对象预分配 是另一大优化。sateTempCart3 在初始化时一次性创建所有卫星的 Cartesian3 占位对象,后续更新只修改其 x/y/z 分量,全程零 GC。链路渲染中也大量使用模块级 _scratchCarto1_scratchGeodesic 等预分配对象。

实测效果:在万星场景下,Primitive 模式渲染帧率稳定在 60fps 以上(浏览器刷新率上限),CPU 占用相比 Entity 模式降低 80% 以上。


前端:useMapScene 架构 ------ 分层组合 + 依赖注入

3D 场景的渲染逻辑极其复杂------卫星轨道计算、节点位置更新、链路动态连线、业务流路径变化、时间轴帧数据驱动------如果把这些全部塞进一个文件,不出意外会变成一个 5000+ 行的"上帝函数"。

我们的设计是分层组合 + provide/inject 依赖注入

整体架构

scss 复制代码
useMapScene()                       ← 主入口,编排子模块
├── useMapSceneSatellite()          ← 卫星渲染(Entity + Primitive 双模式)
├── useMapSceneNode()               ← 地面节点渲染
├── useMapSceneLink()               ← 星间/星地链路渲染
├── useMapSceneTaskFlow()           ← 任务链/业务流渲染
└── useMapSceneTimeline()           ← 时间轴帧数据驱动引擎

每个子模块是独立的 composable,返回 render/clear/pick/toggle 等方法。useMapScene 负责编排调用顺序和依赖注入。

依赖注入设计

子模块之间存在依赖关系------业务流需要获取卫星和节点的当前位置来绘制路径,时间轴引擎需要同时驱动卫星、节点、链路、业务流四个模块的更新。

我们通过 Vue 的 provide/inject 实现模块间解耦:

javascript 复制代码
// useMapScene.tsx --- 主入口组合各模块
export async function useMapScene() {
  // STEP 1: 卫星模块
  const mapSceneSatellite = useMapSceneSatellite()
  const { renderSatellites, clearSatellites, pickSatelliteById, toggleShowSatellite, ... }
    = mapSceneSatellite
  provide('pickSatelliteById', pickSatelliteById)
  provide('toggleShowSatellite', toggleShowSatellite)
  // ...

  // STEP 2: 节点模块
  const mapSceneNode = useMapSceneNode()
  const { renderNodes, clearNodes, pickNodeById, toggleShowNode, ... }
    = mapSceneNode
  provide('pickNodeById', pickNodeById)
  provide('toggleShowNode', toggleShowNode)
  // ...

  // STEP 3: 链路模块
  const mapSceneLink = useMapSceneLink()
  // ...

  // STEP 4: 任务流模块(需要卫星+节点模块引用)
  const mapSceneTaskFlow = useMapSceneTaskFlow({
    satellite: mapSceneSatellite,  // 直接传入引用
    node: mapSceneNode,
  })

  // STEP 5: 时间轴引擎(需要全部四个模块引用)
  const mapSceneTimeline = useMapSceneTimeline({
    satellite: mapSceneSatellite,
    node: mapSceneNode,
    link: mapSceneLink,
    taskFlow: mapSceneTaskFlow,
  })
}

注意两种注入方式的选择:

  • 面板控制方法toggleShowSatellite 等)用 provide/inject------由左侧面板组件通过 inject 获取后绑定到开关按钮
  • 模块间引用mapSceneSatellite 等)用直接传参 ------useMapSceneTaskFlowuseMapSceneTimeline 通过参数获取兄弟模块的引用,避免用 inject 构造循环依赖

渲染编排:一个起点,五个步骤

场景渲染的入口只有一个 renderMapScene(),按顺序驱动所有子模块:

javascript 复制代码
async function renderMapScene() {
  await clearMapScene()
  sceneControl.setPlayStep(1)
  await sceneControl.setTimeline(startTime, endTime, clockRange)

  // 按序渲染:卫星 → 节点 → 链路 → 任务流 → 时间轴
  await renderSatellites(isUsePerformace) // 卫星必须先渲染(链路需要其位置)
  await renderNodes(isUsePerformace) // 节点其次(链路也需要其位置)
  await renderLinks() // 链路依赖卫星+节点位置
  await renderTaskFlow() // 任务流依赖链路数据
  await renderTimeline() // 时间轴引擎最后启动,驱动全部
}

渲染顺序很关键:链路渲染需要卫星和节点的位置信息,所以卫星和节点必须在链路之前完成;时间轴引擎作为"总调度",必须在其余四个模块就绪后才能启动。

点击事件分流

Cesium 的 3D 场景只有一个全局点击事件,但需要分流到卫星详情、节点详情、链路详情、业务流详情四个不同的面板。我们在 useMapScene 中统一监听,根据拾取目标的 ID 前缀分流:

javascript 复制代码
function bindMapClickEvent(e) {
  const primitive = e.data.scenePicked?.primitive
  const id = primitive?.id

  if (id?.startsWith('satellite-')) {
    pickSatellitePrimitive(primitive) // 触发卫星拾取 → 飞行动画
    emitter.emit('卫星详情', id) // 事件总线通知右侧面板
  }
  else if (id?.startsWith('node-')) {
    pickNodeEntity(entity)
    emitter.emit('节点详情', id)
  }
  else if (id?.startsWith('link-')) {
    pickLinkEntity(entity)
    emitter.emit('链路详情', id)
  }
  // ...
}

这样,3D 场景的交互逻辑完全封装在 useMapScene 内部,上层组件只需监听 mitt 事件总线即可响应。

这套架构的核心价值在于:每个模块独立开发、独立测试 。新增一种渲染元素(比如"干扰区域可视化"),只需要新建一个 useMapSceneInterference.tsx,在 useMapScene 中注册即可,不会影响已有模块。

让大量卫星/节点/链路变得"有辨识度",是一个被低估的难题。

我们设计了一套三层继承 + extend 覆盖的样式系统:

arduino 复制代码
第 1 层:类型基础样式(由数据字段决定)
├── 卫星:遥感卫星 > GEO 层 > MEO 层 > LEO 层 > 默认
├── 节点:信关站 > 测控中心 > 其他
└── 链路:星间链路 > 星地链路 > 馈电链路

第 2 层:单元素个性样式(item.style)
└── 覆盖第 1 层同名属性

第 3 层:extend 动态覆盖(由版本对比计算)
├── ADD:绿色闪烁 + "【新增】"标记
├── REMOVE:红色渐变淡出 + "【删除】"标记
├── PLUS:黄色闪烁 + "【扩容增量:{mbps}】"标记
├── HIGH:红色闪烁 + "【高亮】"标记
├── congestion_0/1/2/3:绿/黄/橙/红四级拥塞色
└── down:灰色不可用状态

样式合并算法:

javascript 复制代码
actionSetConfigStyle(items) {
  const baseStyle = config[item.satelliteType]?.[item.layer] ?? defaultStyle
  const result = cloneDeep(baseStyle)  // 深拷贝防止污染全局配置

  // 合并 item 自身样式
  if (item.style) merge(result, item.style)

  // 合并 extend 覆盖(ADD/REMOVE/PLUS/HIGH)
  if (item.extend) {
    for (const extend of item.extend) {
      if (extend.type === 'ADD') merge(result, addOverlayStyle)
      if (extend.type === 'REMOVE') merge(result, removeOverlayStyle)
      // ...
    }
  }

  // 应用到各图形类型
  applyPointStyle(result.point)
  applyBillboardStyle(result.billboard)
  applyLabelStyle(result.label)
  applyPolylineStyle(result.polyline)
}

这套系统的精髓在于:版本 diff 自动驱动样式变化 。当用户点击"回退至上一版本"时,新旧数据 diff 自动生成 ADD/REMOVE/PLUS 标记,场景中的卫星/链路自动变色------用户一眼就能看到"这一步优化增删了什么"。


后端:动态分表与大数据量处理

仿真数据有个特点:单个案例小,但时间轴大得离谱

一个典型场景的 timeline.jsonl(时间轴数据)可能达到数 GB,包含数千帧、每帧数千条链路状态记录。传统的"一张大表存所有"方案不可行------单表膨胀到几千万行后查询会慢到不可接受。

我们的方案是动态分表(Sharding by case + scene)

arduino 复制代码
MySQL my_db/
├── model_entity                    ← 固定表(模型元数据)
├── ste_{case_id}_{scene_id}        ← 按 case+scene 动态创建的 task 表
├── sfe_{case_id}_{scene_id}        ← 按 case+scene 动态创建的 flow 表
└── sle_{case_id}_{scene_id}        ← 按 case+scene 动态创建的 timeline 表

表的生命周期管理

javascript 复制代码
// 上传案例时自动建表
async uploadCaseZipFile(file) {
  const { caseId, scenes } = await parseZipFile(file)

  for (const scene of scenes) {
    const tableName = `sle_${caseId}_${scene.id}`  // 超过64字符自动 MD5
    await createTableFromTemplate(SceneTimelineEntity, tableName)
    await bulkInsert(tableName, scene.timelineData, 10)  // JSONL 流式读取
  }

  // 更新缓存
  await initCaseCache()
}

// 删除案例时自动删表
async deleteCaseById(caseId) {
  const tables = await getTablesByPrefix(`%${caseId}%`)
  for (const table of tables) {
    await queryRunner.dropTable(table)
  }
  await removeCaseFiles(caseId)
}

流式读取避免内存爆炸

对于 GB 级的 timeline.jsonl,使用 Node.js readline 逐行读取:

javascript 复制代码
const rl = readline.createInterface({
  input: fs.createReadStream(timelinePath),
  crlfDelay: Infinity,
})

let batch = []
for await (const line of rl) {
  batch.push(JSON.parse(line))
  if (batch.length >= 10) {
    await repository.insert(batch) // 批量插入
    batch = []
  }
}

这样,无论文件多大,内存占用始终可控。


后端:WebSocket 实时态势与房间调度

除了离线仿真回放,平台还支持实时态势模式------仿真计算平台持续推送数据,前端实时渲染。

我们的 WebSocket 网关借鉴了信令服务器的设计,实现了房间 + Provider 模式

核心协议设计

javascript 复制代码
// 客户端角色注册
{ type: 'register', role: 'frontend' | 'provider' }

// 服务端动态分配场景给 provider
{ type: 'assign_scene', caseId, sceneId }

// provider 推送实时帧数据
{ type: 'push_timeline_data', caseId, sceneId, frameNo, data }

// 服务端广播给同房间的所有 frontend
{ type: 'timeline_update', frameNo, data }

容错机制

  • provider 断线自动恢复:provider 重连后,服务端自动重新分配所有活跃场景
  • 房间生命周期管理:所有 frontend 离开后自动通知 provider 停止计算
  • EventEmitter 桥接:文件上传进度通过 NestJS EventEmitter 从 HTTP Controller 传递到 WebSocket Gateway,实现实时进度推送
scss 复制代码
HTTP Upload → Controller → EventEmitter.emit() → WebSocket Gateway → 广播进度

前后端类型共享:从独立 npm 包到 Monorepo 的路径

前后端分离项目最常见的痛点是:接口类型不一致导致的生产事故

前端说 CaseItem.caseName,后端返回 case_name;前端期望 number,后端返回 string------这类问题在对接阶段层出不穷。

现状:独立 npm 类型包

我们在后端仓库 src/types/ 中统一维护类型定义,通过 tsup 编译为 .d.ts 声明文件,发布为 snss-types npm 包,前端 package.json 中直接依赖:

css 复制代码
后端 src/types/
├── index.ts        ← 全部类型导出
├── case.ts         ← CaseItem, OptimizationTrace, CaseJsonData...
└── scene.ts        ← SatelliteItem, NodeItem, LinkItem, FlowItem, TimelineFrame...
        │
        ▼ tsup 编译
dist-types/
└── index.d.ts      ← 纯类型声明文件
        │
        ▼ npm publish
snss-types@1.0.11
        │
        └── 前端 package.json: "snss-types": "^1.0.11"

任何接口字段的增删改都会在编译期暴露,不会等到运行时才发现不匹配。

为什么要这样设计?

说实话,这是一个过渡方案

项目启动时,我们的定位只是一个前端可视化平台,后端功能全部由第三方计算平台通过 WebSocket 推送。因此初期并没有规划后端工程,自然也不会考虑 Monorepo。

随着业务需求演进------案例管理、数据持久化、模型 CRUD------我们意识到必须自建后端。但此时前端工程已经成型,snss-types npm 包已经在使用。如果强行改造为 Monorepo,需要同时重构两个仓库的目录结构和构建配置,时间成本太高。

于是我们选择维持 npm 包方案作为过渡:类型更新后手动发布,前后端各自 npm install 同步。

未来:迁移到 Monorepo

独立 npm 包的方案有两个痛点:

  • 发布流程繁琐 :每次类型变更需要 npm version patchnpm publish → 前后端各 npm install,链路太长
  • 版本对齐困难 :前端依赖的 snss-types 版本可能与后端源码不一致,尤其在多人协作时

因此,架构的下一步规划是迁移到 pnpm workspace Monorepo

go 复制代码
satellite-network-simulation/
├── pnpm-workspace.yaml
├── package.json                  ← 根 workspace
├── packages/
│   └── types/                    ← 共享类型包(引用本地路径,无需发布)
│       └── src/
│           ├── case.ts
│           └── scene.ts
├── apps/
│   ├── web/                      ← 现有前端工程
│   └── server/                   ← 现有后端工程

迁移后,前后端直接通过 workspace:* 协议引用本地类型包,类型变更即时生效,无需任何发布流程。同时,共享的工具函数、常量、配置也可以纳入 packages/ 目录统一管理。

这是一条从"够用"到"更好"的演进路径------技术上不求一步到位,但方向要清晰。


启发与未来规划

几点启发

1. 性能优化要"提前想,分批做"

项目初期我们没有做 Primitive 模式,导致 500 颗卫星就让浏览器掉帧。后来花了大量时间重构------如果一开始就设计双模式架构,会少走很多弯路。但反过来,Entity 模式的先期实现帮助我们快速验证了交互逻辑,也不算白费。

2. 样式不能在代码里写死

卫星类型、链路状态、拥塞等级------这些都会随场景而变化。我们起初把颜色、图标路径硬编码在渲染函数里,后来需求一变就到处改代码。三层样式继承系统虽然设计复杂,但从第二版开始,新增一种卫星类型只需要在 public/config.js 里加一段 JSON 配置。

3. 分表设计是"用空间换时间"的典型案例

MySQL 单表几千万行查询会慢到不可接受,但拆成几百张小表后,每张表几万行,查询都在毫秒级。代价是管理复杂度------需要维护表的创建/销毁逻辑、需要确保命名规则一致、需要在查询时动态拼接表名。但这份复杂度集中在后端,对前端完全透明。

4. 架构演进不求一步到位

snss-types 独立 npm 包方案虽然不够"优雅",但在当时的时间约束下是务实的选择------它解决了类型一致性问题,且不影响前后端的独立开发节奏。未来迁移 Monorepo 时,因为类型定义本身就独立在 src/types/ 中,迁移成本很低。有时候"先跑起来再优化"比"设计完美再启动"更有价值。

未来规划

  • Monorepo 架构迁移:将前后端整合为 pnpm workspace Monorepo,共享类型包改为本地引用,消除发布流程,实现类型变更即时生效

写在最后

回顾这半年的开发历程,我最大的感受是:做一个"能用"的卫星仿真平台不难,做一个"好用"的很难

"能用"是数据能加载、卫星能显示、时间轴能播放。但"好用"意味着:

  • 26000 颗卫星不掉帧
  • GB 级数据秒级加载
  • 版本切换在 500ms 内完成
  • 样式系统让新人 10 分钟就能新增一种卫星类型
  • 前后端类型一致,对接不出错

这些"好用"的背后,是无数次的性能分析、架构重构和边界条件处理。如果你也在做类似的可视化项目,希望这篇文章能给你一些启发。

愿我们的代码,能带人们看见地球之外的世界。


欢迎评论

如果大家对卫星网络仿真可视化有什么看法或问题,欢迎在评论区交流讨论。也可以关注我的公众号------【诗传千古地负海涵】 🌟

相关推荐
学渣超1 小时前
记一次分布式事务数据不一致的排查之旅:从超时到索引,层层剥茧
java·后端·架构
逍遥运德1 小时前
前端工程化-03:包管理NPM-package.json 和 package-lock.json 详解
前端·前端框架·前端工程化
逗逗豆巴吧1 小时前
WPS 加载项中实现当前文档直接上传
前端·javascript·ecmascript 6
慢功夫1 小时前
💡【调试】按下 F5 后,mini-vscode 到底做了什么
前端·visual studio code
妙码生花1 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(二十一):网络请求封装优化
前端·javascript·vue.js
用户11481867894841 小时前
普通Fetch、Fetch流式、Axios、SSE:谁才是流式输出的正确操作?
前端·javascript
奇奇怪怪的1 小时前
多模态 RAG:图片与表格检索
前端
犇驫聊AI1 小时前
Claude Code 用了大半年才悟出来的 6 个技巧,第 3 个直接省一半 Token
前端·claude
北冥you鱼2 小时前
Go 语言读取链上数据:从基础到实战
开发语言·后端·golang