当前位置:   article > 正文

osgb加载调研报告_osg加载损坏的osgb文件会闪退吗

osg加载损坏的osgb文件会闪退吗

背景

1. 三维引擎
  本文要介绍的是用cesium加载OSGB。cesium是web端三维引擎,基于WebGL开发的,WebGL又是OpenGL ES的B端API封装集,openGL ES是 openGL的一个子集,OpenGL是 Open Graphics Library 是个定义了一个跨编程语言、跨平台的编程接口规格的专业的图形程序接口。它用于三维图象(二维的亦可),是一个功能强大,调用方便的底层图形库。
2. 浏览器支持
  如果要使用cesium.js,要求浏览器必须支持WebGL,WebGL支持测试:https://get.webgl.org,主要浏览器支持如图:

   

  1. 模型格式介绍

  3.1 OSGB介绍

倾斜摄影中会经常使用到的格式主要为以下几种:OSGB、OBJ,FBX,STL、3Dtiles等。目前市面上生产的倾斜模型OSGB占了大部分,尤其是ContextCapture(Smart3D)处理的倾斜摄影三维模型,(无人机处理后的数据多为osgb)。 OSGB是国际通用三维场景格式,数据的组织结构如下:Data目录为数据入口目录,“Data” 目录同级放置一个 metadata.xml 文件用来记录模型的位置信息。Data目录下包含很多子目录,如下,每个子目录为一个根块,每个根块是一个树形结构,是一个LOD层级结构。Data目录下的每个瓦片目录下,必须有个和目录名同名的 osgb 文件,否则无法识别根节点。简单理解,每一个osgb文件就是一个三维切片。

具体结构如图:

此类数据的特点是文件碎、数量多、体积大,很难高效的进行网络发布,这也导致它在应用方面受到很多限制。osgb也是OSG引擎的自有格式,OSG可以直接读取osgb文件,但是OSG引擎是桌面端三维地球开源引擎,无法进行现在主流的web端应用,一些主流的web三维引擎都不支持直接加载osgb,本文介绍的cesium就需要借助工具转换成3dtilesCeciumlab、OSGBLab等多种转换工具都可以实现转换。

3.2 3dtiles介绍

  • 是Cesium于2016年3月定义的一种三维模型瓦片数据格式,目前已经是OGC标准之一,3dtiles将海量三维数据以分块分层的形式组织起来,大大减轻了浏览器和GPU的负担,大大提高了数据加载效率。3dtiles专为流式传输和渲染3D地理数据而设计的,如倾斜摄影测量、BIM、点云、建筑数据等。在glTF的基础上,加入了分层LOD的结构后得到的产品,降低海量数据可视化过程中的浏览器负担,减少WebGL绘制请求的数量。是专门为大量地理3D数据流式传输和海量渲染而设计的一种格式是cesium的御用格式。入口文件是tileset.json各级瓦片用文件夹来组织(类似套娃),目录中有零散的*.json文件叶子节点有*.b3dm、*.i3dm等格式具体结构如图:

Cesium学习:

 1.项目引入

  先安装cesium:npm install cesium(cesium版本更新比较频繁,建议使用固定版本)

  1.1 vue2+webpack项目

  vue.config.js配置如下:

const path = require('path')

const webpack = require('webpack')

const { defineConfig } = require('@vue/cli-service')

const CopyWebpackPlugin = require("copy-webpack-plugin")

//定义cesium源码路径

let cesiumSource = './node_modules/cesium/Source'

let cesiumWorkers = '../Build/Cesium/Workers'

module.exports = defineConfig({

  configureWebpack: {

    resolve: {

      alias: {

        'cesium': path.resolve(__dirname, cesiumSource)

      },

    },

    plugins: [

      // 拷贝静态资源

      new CopyWebpackPlugin({

        patterns: [

          // 前端运行时用到的 WebWorker 的构建版本(WebWorker 由于一些原因,在前端运行时仍然用 CommonJS 格式加载)

          { from: path.join(cesiumSource, cesiumWorkers), to: 'Workers' },

          // 图片或 JSON 等前端运行时可能用到的资源

          { from: path.join(cesiumSource, 'Assets'), to: 'Assets' },

          // 主要是各个 CesiumJS 自带的界面小部件的 CSS 文件

          { from: path.join(cesiumSource, 'Widgets'), to: 'Widgets' },

          // WebAssembly 等前端运行时可能用到的第三方资源

          { from: path.join(cesiumSource, 'ThirdParty/Workers'), to: 'ThirdParty/Workers' }

        ]

      }),

      // 定义 Cesium 从哪里加载资源,如果使用默认的'',变成了绝对路径了,所以这里使用'./',使用相对路径

      new webpack.DefinePlugin({

        CESIUM_BASE_URL: JSON.stringify('./')

      })

    ],

    module: {

      // 为了解决Error: Cannot find module "."

      unknownContextRegExp:  /^.\/.*$/,

      // 让Webpack打印载入特定库时候的警告

      unknownContextCritical: false

    }

  },

  transpileDependencies: true

})

在使用时引入:

import * as Cesium from 'cesium/Cesium'

import 'cesium/Widgets/widgets.css'

1.2 vue3+vite项目

安装插件:npm i vite-plugin-cesium

vite.config.ts配置如下:

import { defineConfig } from 'vite'

import vue from '@vitejs/plugin-vue'

import cesium from 'vite-plugin-cesium'

// https://vitejs.dev/config/

export default defineConfig({

  plugins: [vue(), cesium()],

})

在使用时引入:

import * as Cesium from 'cesium/Cesium'

  1. 主要功能介绍(详情见api文档)

官方示例:Cesium Sandcastle

官网地址:Cesium: The Platform for 3D Geospatial

中文API:  Viewer - Cesium Documentation

    1. Viewer 

  Viewer是一切API的开始点,构建应用程序的基本小部件。它将所有标准的 Cesium 小部件组合到一个可重用的包中有很多属性,主要包括一些小控件的显示隐藏,图层的设置,场景等等一系列设置。

    1. 场景Scene

Scene场景是所有3D图形对象的容器(HTML canvas),Scene不是由我们直接创建,它是在Viewer或CesiumWidget内部隐式创建的。在场景对象中我们可以控制:globe椭圆体、imageryLayers底图、terrainProvider地形、camera相机、skyBox天空盒、sun太阳、moon月亮、primitives默认矢量数据层、postProcessStages后处理效果等等。

Scene场景渲染监听事件触发顺序:(addEventListener和removeEventListener绑定和解绑)

viewer.scene.preUpdate 在更新或呈现场景之前将引发的事件

viewer.scene.postUpdate 在场景更新后以及渲染场景之前立即引发的事件

viewer.scene.preRender 在场景更新后以及渲染场景之前将引发的事件

viewer.scene.postRender 在渲染场景后立即引发的事件

    1. 相机Camera

  相机控制了场景的观察视角。有很多相机操控方法,比如旋转、缩放、平移以及飞行定位。

  2.3.1 Camera类(相机)

描述了相机的当前状态,包括:位置(position),朝向( orientation), 参考空间( reference frame), 视锥体(view frustum),可以调用move*、zoom*、look* 、twist* 、rotate* 、setView、flyTo 等方法进行控制相机操作

  2.3.2 ScreenSpaceCameraController 类(屏幕控件相机控制器)

把屏幕空间的用户输入(鼠标拖拽点击或者触摸事件)转换为三维世界的相机移动 。它包含一些属性,可以启用/禁用某种用户输入,修改惯性、最小最大缩放距离等。

  2.3.3 相机相关事件(addEventListener 和 removeEventListener进行绑定和解绑 )

viewer.camera.moveStart 相机开始移动时将引发的事件

viewer.camera.moveEnd 相机停止移动时将引发的事件

viewer.camera.changed 相机更改后将引发的事件

    1. 影像图层ImageryLayer

   Cesium支持多种服务来源的高精度影像地图数据的加载和渲染。图层支持排序和透明混合。每个图层的亮度(brightness),对比度(contrast),伽马校正(gamma),色调(hue),饱和度(saturation)都可以动态修改。可以在Viewer构造时进行参数设置,也可以在构造后通过viewer.scene.imageryLayers(ImageryLayerCollection类)控制。详情见api文档。

    1. 地形图层TerrainProvider

   支持渐进流式加载和渲染全球高精度地形,并且包含海、湖、河等水面效果。相对2D地图,山峰、山谷等其他地形特征的更适宜在这种3D地球中展示。地形数据集是巨大的,通常都是GB或者TB级别。在普通3D引擎中,使用底层图形API去高效实现地形数据的可视化需要做很多事情。Cesium已经完成这部分工作,我们只需要拿来用。主要是修改viewer.terrainProvider属性。以下列举简单三种情况:

(1) 使用Cesium在线Ion地形:

viewer.terrainProvider = Cesium.createWorldTerrain();

(2)使用自己的服务:

viewer.terrainProvider = new Cesium.CesiumTerrainProvider({ url:"自己服务" });

(3)不使用地形:

   viewer.terrainProvider = new Cesium.EllipsoidTerrainProvider();

    1. Primitive 与 Enity

  Cesium加载点线面矢量数据分为两部分:

  2.6.1 Primitive API

 面向三维图形开发者,更底层一些(灵活、性能高、使用复杂),主要目的是为了完成(可视化)任务的最少的抽象需求。要求我们以一个图形开发者的方式去思考,并且使用了一些图形学术语。它是为了最高效最灵活的实现可视化效果,忽略了API的一致性。他们每种都有自己的独特的性能提升方式,也需要遵守不同的优化原则。

由两个部分组成:

(1)几何形状(Geometry):定义了Primitive的结构,例如三角形、线条、点等

(2) 外观(Appearance ):定义Primitive的着色(Sharding),包括GLSL(OpenGL着色语言)顶点着色器和片段着色器(vertex and fragment shaders),以及渲染状态(render state)

优势:

  (1)性能:绘制大量Primitive时,可以将其合并为单个Geometry以减轻CPU负担、更好的使用GPU。合并Primitive由web worker线程执行,UI保持响应性

(2)灵活性:Geometry与Appearance 解耦,两者可以分别进行修改

(3)低级别访问:易于编写GLSL 顶点、片段着色器、使用自定义的渲染状态

劣势:

  (1)需要编写更多地代码

(2)需要对图形编程有更多的理解,特别是OpenGL的知识

2.6.2 Entity API

是数据驱动更高级一些(性能略低、接口一致、容易使用),主要目的是定义一组高级对象,它们把可视化和信息存储到统一的数据结果中,这个对象叫Entity。 它让我们更加关注我们的数据展示而不是底层的可视化机制。它提供了很方便的创建复杂的与静态数据相匹配的随时间变化的可视化效果。Entity内部也是使用了Primitive,暴露一些一致性的、容易去学习和使用的接口。通常使用 viewer.entities.add 方法进行添加Entity矢量数据,或者使用CustomDataSource对象进行管理,支持的类型见下图:

    1. 材质( Material、Fabric)

  Fabric 是Cesium中基于JSON格式来描述Material的机制。材质描述多边形、折线、椭球等对象的外观特征。材质可以简单的是覆盖一张图片,或者是条纹或者棋盘图案。使用Fabric 和GLSL,可以从零开始写脚本新建材质,也可以从现有的材质中派生。比如潮湿碎裂的砖块可以使用程序生成的纹理、凹凸贴图和反射贴图来组合。对象通过material 属性来支持材质效果。

    1. 坐标系

地图坐标系:(各个地图坐标之间可以互相转换)

WGS-84国际标准:GPS坐标(Google Earth或者GPS模块)

GCJ-02中国坐标偏移标准(又名“火星坐标系):Google Map、高德、腾讯等

BD-09百度坐标偏移标准: Baidu Map

      1. 屏幕坐标(像素值)

  (1) 二维笛卡尔平面坐标

屏幕左上角为原点(0,0),单位为像素值,屏幕水平方向为X轴,向右为正,垂直方向为Y轴,向下为正,通过鼠标点击直接获取的坐标就是屏幕坐标了,单位是像素值,通过new Cesium.Cartesian2(x, y)创建

(2) 三维笛卡尔空间坐标(世界坐标)

     用来做空间位置的变化如平移、旋转和缩放等等,通过new Cesium.Cartesian3(x, y, z)创建,cesium采用的是右手坐标系

      1. 地理坐标系

  (1) 地理坐标系(弧度)

  Cesium中的地理坐标单位默认是弧度制,用Cartographic变量表示,通过new Cesium.Cartographic(longitude, latitude, height)创建,其中这里的参数是用弧度表示的经纬度,new Cesium.Cartographic(longitude, latitude, height)   注:这里的经纬度是用弧度表示的,经纬度其实就是角度。弧度即角度对应弧长是半径的倍数。

角度转弧度: π / 180 × 角度

弧度变角度: 180 / π × 弧度

(2) 地理坐标系(经纬度)WGS84

  Cesuim中没有具体的经纬度对象,要得到经纬度首先需要计算为弧度,再进行转换

      1. 坐标转换

  鼠标单击获取二维笛卡尔平面坐标:

   let handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas)

      // 左键点击事件

      let leftclick = Cesium.ScreenSpaceEventType.LEFT_CLICK

      handler.setInputAction((e) => {

        let windowPosition = e.position

      }, leftclick)

(1) 二维笛卡尔平面坐标转成三维笛卡尔空间坐标

三维模式:

    // 三维模式下

        let ray = viewer.camera.getPickRay(windowPosition)

        let cartesian = viewer.scene.globe.pick(ray, viewer.scene)

二维模式:

// 二维模式下

        let cartesian = viewer.scene.camera.pickEllipsoid(windowPosition, viewer.scene.globe.ellipsoid)

(2) 三维笛卡尔空间坐标转成二维笛卡尔平面坐标

let pick = Cesium.SceneTransforms.wgs84ToWindowCoordinates(viewer.scene, cartesian)

(3) 三维笛卡尔空间坐标转成地理坐标系(弧度)

let cartographic = Cesium.Cartographic.fromCartesian(cartesian)

(4) 地理坐标系(弧度)转成三维笛卡尔空间坐标

let position = Cesium.Cartesian3.fromRadians(lng, lat, height)

(5) 三维笛卡尔空间坐标转换为地理坐标(经纬度)

 let cartographic=Cesium.Cartographic.fromCartesian(cartesian)

 let lat=Cesium.Math.toDegrees(cartographic.latitude)

 let lng=Cesium.Math.toDegrees(cartographic.longitude)

 let height=cartographic.height

(6) 度数与弧度互转

Cesium.Math.toDegrees(radians)

Cesium.Math.toRadians(degrees)

(7) 地理坐标(经纬度) 转换为 三维笛卡尔空间坐标

let position = Cesium.Cartesian3.fromDegrees(longitude, latitude, height)

    1. Property机制

  Cesium宣称自己是数据驱动和time-dynamicvisualization,这些可都是仰仗Property系

统来实现的。Property最大的特点是和时间相互关联,在不同的时间可以动态地返回不同

的属性值。而Entity则可以感知这些Property的变化,在不同的时间驱动物体进行动态展示。

简单分类如下:

2.10 模型加载

  2.10.1 glb/gltf格式数据简单举例:

  entities加载:

       // 使用entities方法加载

        viewer.entities.add({

          name: 'xx'

          position: Cesium.Cartesian3.fromDegrees(longitude, latitude, height),

          model: {

            uri: '数据路径',

          }

        });

primitives加载:

  // 使用primitives方法加载

        scene.primitives.add(Cesium.Model.fromGltf({  

          url : '数据路径'   

        }))

2.10.2 3DTiles格式模型

  // 加载3dtiles

        let tileset = viewer.scene.primitives.add(new Cesium.Cesium3DTileset({

          url: 'http://xxx/tileset.json', // 模型服务地址

          maximumScreenSpaceError: 1

        }))

        tileset.readyPromise.then((tileset) => {

          let boundingSphere = tileset.boundingSphere;

          const hpr = new Cesium.HeadingPitchRange(0.0,-0.5,boundingSphere.radius*2)

          viewer.camera.flyToBoundingSphere(boundingSphere, hpr) // 视角定位至模型

          tileset.style = new Cesium.Cesium3DTileStyle({ // 设置样式

            color: {

              conditions:[

                ["${floor}>=300","rgba(45,0,75,0.5)"],["${floor}>=25","rgb(252,230,200)"],

                ["${floor}>=5","rgb(198,106,11)"],

                ["true","rgb(127,59,8)"]

              ]

            }

          })

        })

2.11 CZML数据

  CZML是一种用来描述动态场景的JSON架构的语言,主要用于Cesium数据与程序分离,就如同GoogleEarth和KML的关系。采用数据驱动的方式,不用写代码即可构建出丰富的动态场景。一个CZML文档包含一个JSON数组,数组中每一个对象都是一个CZML数据包(packet),一个packet对应一个场景中的对象,例如一个飞机。Cesium.CzmlDataSource.load可以加载czml 对象或者czml文档的url,CZML比较特殊的是跟时间序列相关的属性。

简单使用:

viewer.dataSources.add(Cesium.CzmlDataSource.load(czml));

2.12 交互性(鼠标动作处理器、事件)

  ScreenSpaceEventHandler类处理用户输入事件。可以添加自定义函数,以便在用户输入时对其执行,如下:

(1)setInputAction(action,type,modifier)设置事件

(2)getInputAction(type,modifier)得到交互事件

(3)removeInputAction(type,modifier)移除事件

(4)destroy()销毁Handle

(5)isDestroyed()判断是否销毁

 ScreenSpaceEventType类为事件类型,包含鼠标单击、双击、按下、抬起、滚轮、右击等,示例:

let handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas)

      // 左键点击事件

      let leftclick = Cesium.ScreenSpaceEventType.LEFT_CLICK

      handler.setInputAction((e) => {

      }, leftclick)

2.13 后期处理PostProcessStage

  Cesium支持对整个场景的后期处理(PostProcessing)功能,包括模型描边、黑白图、明亮度调整、夜视效果、环境光遮蔽等。后期处理的过程有点类似于照片的PS。

  (1)PostProcessStage:对应于某个具体的后期处理效果,它的输入为场景渲染图或者上一个后期处理的结果图,输出结果是一张处理后的图片。

(2)PostProcessStageComposite:一个集合对象,存储类型为PostProcessStage或者PostProcessStageComposite的元素。

(3)PostProcessStageLibrary:负责创建具体的后期处理效果,包括Silhouette、Bloom、AmbientOcclusion等创建返回的结果是PostProcessStageComposite或PostProcessStage类型

(4)PostProcessStageCollection:是一个集合类型的类,负责管理和维护放到集合中的元素,元素的类型是PostProcessStage或者PostProcessStageComposite。

流程如下:

2.14 粒子系统ParticleSystem

  粒子系统是一种图形技术,可以模拟复杂的物理效果。粒子系统是小图像的集合,当它们一起观看时,会形成一个更复杂的“模糊”物体,如火、烟、天气或烟花。通过使用诸如初始位置、速度和寿命等属性指定单个粒子的行为,可以控制这些复杂的效果。粒子发射器(ParticleEmitter)控制了粒子产生时候的位置以及初始速度方向,并依据emissionRate来决定每秒产生多少粒子,根据发射器类型不同决定了粒子的随机速度方向。

  1. CircleEmitter:圆形发射器使用CircleEmitter类在圆形面上随机一个位置,粒子方向是发射器的向上向量。它接受一个float参数指定了圆的半径。
  2. BoxEmitter:在盒子里(box)里随机一个位置,沿着盒子的6个面的法向量向外运动。它接受一个Cartesian3参数,定义了盒子的长宽高。
  3. ConeEmitter:锥形发射器类使用ConeEmitter在椎体顶点产生粒子粒子方向在椎体内随机一个角度向外。它接受一个float参数,制定了锥角。
  4. SphereEmitter:球形发射器使用SphereEmitter类在球体内随机产生粒子,初始速度是沿着秋心向外。它接受一个float参数指定了球体半径。
  1. 源码简单介绍

  源码地址:GitHub - CesiumGS/cesium: An open-source JavaScript library for world-class 3D globes and maps :earth_americas:(可在本地运行、查看学习,有demo)

  1. 根路径:

    .husky:CesiumJS 使用 husky + lint-staged + prettier 来管理代码风格

    CHANGES.md:发布日志,每个版本的变更记录及修复了哪些功能

    gulpfile.js:gulp的配置文件,记录了Cesium的所有打包流程,包括GLS语法的转义,压缩和未压缩库文件的打包、API文档的生成以及自动化单元测试等

index.html: 导航首页

server.js: Cesium内置的Node服务器文件

(2)Apps

    CesiumViewer: 一个简单的Cesium初始化示例。

SampleData: 所有示例代码所用到的数据,包括json、geojson、topojson、kml、czml、gltf、3dtiles以及图片等。

Sandcastle: 存储Cesium的示例程序代码。

TimelineDemo: 时间轴示例代码

(3)Build(运行build/release 等指令,这个文件夹才会出现)

CesiumUnminified(运行build):打包后的Cesium库文件(未压缩),可用于项目调试

Documentation (运行build-docs):文档目录, API 手册,从开发者主页可以进去。

(4)Source

Cesium源码

(5)Specs

文件夹自动化单元测试,Cesium采用了单元测试Jasmine框架,可以实现接口的自动化测试以及接口覆盖率等统计效果

(6)ThirdParty

第三方依赖库,如代码编辑器codemirror、单元测试框架库jasmine、JavaScript语法和风格检查工具jshint等。手动升级,并将依赖列表写入根目录下的 ThirdParty.extra.json 

(7)Tools

gulp 部分任务需要用到的工具库或配置,例如 rollup 额外配置、jsdoc 配置等

项目开发

  1. 动图加载

  Cesium不支持纹理贴动图,Billboard支持单帧纹理贴图,将动图进行解析,获得时间序列对应的每帧图片,然后按照时间序列动态更新Billboard的纹理,即可实现动图纹理效果。

  常用动图加载插件推荐:

apng-js加载apng格式,github地址:GitHub - Pzhao1990/apng-js: Parse and play animated PNG (APNG)

lib-gif加载gif格式,github地址:GitHub - buzzfeed/libgif-js: JavaScript GIF parser and player

 2.数据处理、获取推荐

     2.1 阿里云地图数据获取 

         地址:DataV.GeoAtlas地理小工具系列

     2.2 地图数据下载与处理(可下载离线底图)

        软件(图新地球):图新地球 LSV-BIM+GIS三维可视化-国产三维GIS软件-中科图新

     2.3 数据转换、处理、查看

        软件(cesiumlab):首页 地球可视化实验室.团队致力于提供基础应用开发,助力数字孪生从业者,开发相关业务。

3.模型单体化

“单体化”其实指的就是每一个我们想要单独管理的对象,是一个个单独的、可以被选中的实体。对于倾斜摄影自动化建模而言,构建出来的是一个连续的Tin网,并不会把单个物体区分出来。因此,对于这样的数据,本身是无法选中单个物体的,需要进行一定的处理才能实现“单体化”。而单体化是很多需求实现的基础。

  1. 切割单体化:对倾斜摄影模型进行切 割,即把连续的三角面片网从物理上分割     开,从而实现单体化(不推荐)
  2. ID单体化:在模型制作和生产过程中,利用三角面片中每个顶点额外的存储空间,把对应的矢量面的ID值存储起来;即一个建筑所对应的三角面片的所有顶点,都存储了同一个ID值,从而实现在鼠标选中这个建筑的目的(不推荐)
  3. 动态单体化:在三维渲染的时候,动态的把对应的矢面叠加到倾斜摄影模型上,类似于一个保鲜膜从上到下完整的把对应建筑等物体的模型包裹起来,从而实现可被单独选中的效果。这种由于 是渲染时动态呈现的,可以称之为"动态单体化”(推荐)

        动态单体化实现思路:核心引用 Cesium ClassificationPrimitive类,在需要单体化的模型外层加一层矢量数据,鼠标选中时使矢量数据变化,达到所需效果,代码简单示例如下:

  ldCollection.add(new Cesium.ClassificationPrimitive({

      geometryInstances: new Cesium.GeometryInstance({

          geometry: new Cesium.PolygonGeometry({

              polygonHierarchy: new Cesium.PolygonHierarchy(

                  Cesium.Cartesian3.fromDegreesArray('坐标')

              ),

              extrudedHeight: '高度'

          }),

          attributes: {

              color: Cesium.ColorGeometryInstanceAttribute.fromColor(new Cesium.Color(1, 1, 1, 1e-4)),

              show: new Cesium.ShowGeometryInstanceAttribute(true)

          },

          id: '自定义id'

      }),

      classificationType: Cesium.ClassificationType.CESIUM_3D_TILE

    }))

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/330410
推荐阅读
相关标签
  

闽ICP备14008679号