iOS证书(.p12)和描述文件(.mobileprovision)申请-中文版-适用于uniapp的ios证书申请

uniapp的ios证书怎么申请 请看这个文档

iOS有两种证书和描述文件:

证书类型使用场景
开发(Development)证书和描述文件用于开发测试,在 HBuilderX 中打包后可在真机环境通过Safari调试
发布(Distribution)证书和描述文件用于提交 AppStore,在 HBuilderX 中提交云打包后提交到 AppStore 审核发布

准备环境

  • 必需要有苹果开发者账号,并且加入了 “iOS Developer Program”
  • Mac OS 10.9以上系统(如果已经申请p12证书则不需要)

登录 iOS Dev Center

打开网站 iOS Dev Center
使用苹果开发者账号登录 iOS Dev Center:

登录成功后在页面左侧选择 “Certificates,IDs & Profiles” 进入证书管理页面:

在证书管理页面,可以看到所有已经申请的证书及描述文件:

下面我们从头开始学习一下如何申请开发证书、发布证书及相对应的描述文件。

首先需要申请苹果 App ID (App的唯一标识)

如果已经申请,可跳过此节

选择页面的 “Identifiers” 可查看到已申请的所有 App 应用标识,点击页面上的加号来创建一个新的应用标识:

选择标识类型为 “App IDs”,然后点击 “Continue”

平台选择 “iOS,tvOS,watchOS”,Bundle ID 选择 “Explicit”,在 Description 中填写描述,然后填写 Bundle ID,Bundle ID 要保持唯一性,建议填写反域名加应用标识的格式 如:“io.dcloud.hellouniapp”, 然后点击 “Continue”
注意:在 HBuilderX 中 App 提交云端打包时界面上的 AppID 栏填写的就是这个 Bundle ID

接下来需要选择应用需要使用的服务(如需要使用到消息推送功能,则选择“Push Notifications”),然后点击 “Continue”
注意:如果App用不到的服务一定不要勾选,以免响应审核

确认后选择提交,回到 identifiers 页面即可看到刚创建的App ID:

至此,App ID 已经创建完毕,接下来开始创建开发证书,在创建开发证书前,需要先生成证书请求文件

生成证书请求文件

不管是申请开发 (Development) 证书还是发布 (Distribution) 证书,都需要使用证书请求 (.certSigningRequest) 文件,证书请求文件需在Mac OS上使用 “钥匙串访问” 工具生成。

在“Spltlight Search”中搜索“钥匙串”并打开 “钥匙串访问” 工具:

打开菜单 “钥匙串访问”->“证书助理”,选择“从证书颁发机构请求证书…”:

打开创建请求证书页面,在页面中输入用户邮件地址、常用名称,选择存储到磁盘,点击 “继续” :注意 红色箭头也要钩上

文件名称为“CertificateSigningRequest.certSigningRequest”,选择保存位置,点击 “存储” 将证书请求文件保存到指定路径下,后面申请开发(Development)证书和发布(Production)证书时需要用到

申请开发(Development)证书和描述文件

开发(Development)证书及对应的描述文件用于开发阶段使用,可以直接将 App 安装到手机上,一个描述文件最多绑定100台测试设备(开发证书不能用于发布应用到 App Store)。

申请开发(Development)证书

在证书管理页面选择 “Certificates” 可查看到已申请的所有证书(TYPE:Development 为开发证书,Distribution为发布证书),点击页面的加号来创建一个新的证书:

在 “Software” 栏下选中 “iOS App Development” 然后点击 “Continue”:

接下来需要用到刚刚生成的证书请求文件,点击“Choose File…”选择刚刚保存到本地的 “CertificateSigningRequest.certSigningRequest”文件,点击 “Continue” 生成证书文件:

生成证书后选择 “Download” 将证书下到本地 (ios_development.cer):

双击保存到本地的 ios_development.cer 文件,会自动打开 “钥匙串访问” 工具说明导入证书成功,可以在证书列表中看到刚刚导入的证书,接下来需要导出 .p12 证书文件,选中导入的证书,右键选择 “导出…”:

输入文件名、选择路径后点击 “存储”: 如果没有p12的文件格式 把钥匙串选项卡点击到我的证书页面

输入密码及确认密码后点击 “好”:

至此,我们已经完成了开发证书的制作(得到了 xxx.p12 证书文件),接下来,继续生成开发阶段所需的描述文件,在生成描述文件之前,需要先添加调试设备(iPhone 、iPad)

添加调试设备

开发描述文件必须绑定调试设备,只有授权的设备才可以直接安装 App,所以在申请开发描述文件之前,先添加调试的设备。
(如果已经添加设备,可跳过此节)

在证书管理页面选择 “Devices”,可查看到已添加的所有设备信息,点击页面上的加号来添加一个新设备:

填写设备名称 和 UDID(设备标识):

获取设备UDID方法,将设备连接到电脑,启动 iTunes,点击此区域可切换显示设备的 UDID,右键选择复制

输入完成后,点击“Continue” 继续完成添加即可;
接下来继续申请描述文件

申请开发 (Development) 描述文件

在证书管理页面选择 “Profiles”,可查看到已申请的所有描述文件,点击页面上的加号来添加一个新的描述文件:

在 “Development” 栏下选中 “iOS App Development”,点击“Continue”按钮:

这里要选择之前创建的 “App ID” (这里是“io.dcloud.hellouniapp”),点击“Continue”:

接下来选择需要绑定的证书,这里建议直接勾选 “Select All”,点击“Continue”:

选择授权调试设备,这里建议直接勾选 “Select All”,点击 “Continue”:

输入描述文件的名称(如“HelloUniAppProfile”), 点击 “Generate” 生成描述文件:

点击“Download”下载保存开发描述文件(文件后缀为 .mobileprovision)

至此,我们已经得到了开发证书(.p12)及对应的描述文件(.mobileprovision),接下看一下如何制作发布证书及发布描述文件

申请发布(Distribution)证书和描述文件

发布 (Production) 证书用于正式发布环境下使用,用于提交到Appstore审核发布。发布证书打包的 ipa,不可以直接安装到手机上

申请发布(Production)证书

在证书管理页面选择 “Certificates” 可查看到已申请的所有证书(TYPE:Development 为开发证书,Distribution为发布证书),点击页面的加号来创建一个新的证书:

在 “Software” 栏下选中 “App Store and Ad Hoc”,点击 “Continue”:

接下来同样需要用到之前生成的证书请求文件,点击“Choose File…”选择刚刚保存到本地的 “CertificateSigningRequest.certSigningRequest”文件,点击 “Continue” 生成证书文件:

生成证书成功,选择“Download” 将证书下载到本地 (ios_production.cer):

同样双击保存到本地的 ios_production.cer 文件将证书导入到 “钥匙串访问”工具中,可以在证书列表中看到刚刚导入的证书,接下来需要导出 .p12 证书文件,选中导入的证书,右键选择 “导出…”:

输入文件名、选择路径后点击 “存储”:

输入密码及确认密码后点击 “好”:

至此,我们已经完成了发布证书的制作(得到了 xxx.p12 证书文件),接下来,继续生成发布描述文件

申请发布 (Distribution) 描述文件

在证书管理页面选择 “Profiles”,可查看到已申请的所有描述文件,点击页面上的加号来添加一个新的描述文件:

在 “Distribution” 栏下选中 “App Store”,点击“Continue”按钮:

这里要选择之前创建的 “App ID” (这里是“io.dcloud.hellouniapp”),点击“Continue”:

接下来选择需要绑定的发布证书(iOS Distribution),这里勾选刚刚生成的发布证书”,点击“Continue”:

接下来输入描述文件的名称(如“HelloUniAppProfileDistribution”), 点击 “Generate” 生成描述文件:

然后点击 “Download” 将描述文件下载到本地(文件后缀为 .mobileprovision)

至此,我们已经得到了发布证书(.p12)及对应的发布描述文件(.mobileprovision)

In-Depth Analysis of vite.config.js: Detailed Explanation and Practice of Common Configuration Items

In the field of front-end development, Vite has become the preferred build tool for many developers due to its fast cold start, efficient Hot Module Replacement (HMR), and other features. The vite.config.js file, as the core configuration file of Vite, empowers developers to customize various aspects of project development and building. A deep understanding of its configuration items enables developers to adjust flexibly according to project requirements, enhancing development efficiency and project performance. Let’s now analyze the usage of common configuration items in vite.config.js one by one.

1. Basic Configuration: defineConfig and Core Plugins

defineConfig serves as the entry function for Vite configuration, used to encapsulate the entire configuration object. In the configuration file, you first need to import defineConfig and use it to define the basic configuration of the project.

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [
    vue()
  ]
});

Among them, the plugins configuration item is crucial for introducing various Vite plugins. For example, in a Vue project, the @vitejs/plugin-vue plugin enables Vite to support the parsing and processing of .vue single – file components, including template compilation, style extraction, and other functions. Besides the Vue plugin, there are many other plugins like @vitejs/plugin-react for React support. Developers can choose appropriate plugins according to the project’s technology stack.

2. Path Resolution: resolve Configuration

The resolve configuration item mainly deals with module resolution – related functions, and the most commonly used is the alias configuration. By setting aliases, you can simplify module import paths, improving code readability and maintainability.

import { defineConfig, URL } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [
    vue()
  ],
  resolve: {
    alias: {
      '@': URL.fileURLToPath(new URL('./src', import.meta.url))
    }
  }
});

In the above code, @ is set as an alias for the project’s src directory. In the code, you can import modules using the form import { component } from ‘@/components’ instead of relative paths. This is especially useful in large – scale projects, effectively avoiding the confusion of path levels. Additionally, the extensions property can be configured to automatically complete file extensions, reducing the amount of input when importing modules.

3. Development Server: server Configuration

The server configuration item defines the behavior of the development server. Properly configuring server can optimize the development experience during local development.

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [
    vue()
  ],
  server: {
    port: 3000,
    open: true,
    proxy: {
      '/api': {
        target: 'https://api.example.com',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  }
});
  • port: Specifies the port number on which the development server starts. The default is 5173, and developers can modify it according to their needs.
  • open: When set to true, the browser will automatically open to access the development address after the project starts.
  • proxy: Used to configure the proxy server in the development environment. When the project needs to interact with the backend API, the proxy can solve cross – origin issues. In the above code, requests starting with /api are proxied to https://api.example.com, and the /api part in the request path is removed.

4. Building Configuration: build Configuration

The build configuration item is mainly used for settings related to project building, including the output directory, resource processing, code optimization, etc.

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [
    vue()
  ],
  build: {
    outDir: 'dist',
    assetsDir: 'assets',
    sourcemap: false,
    rollupOptions: {
      output: {
        manualChunks(id) {
          if (id.includes('node_modules')) {
            return id.toString().split('node_modules/')[1].split('/')[0].toString();
          }
        }
      }
    }
  }
});
  • outDir: Specifies the output directory after the project is built. The default is dist, which can be modified as needed.
  • assetsDir: Sets the storage directory for static resources after building.
  • sourcemap: Controls whether to generate sourcemap files. true means generating them, which is convenient for debugging; false means not generating them. In a production environment, it is usually set to false to reduce file size.
  • rollupOptions: Through rollupOptions, you can perform more granular configuration of the underlying Rollup build. The manualChunks configuration in the above code realizes the 分包 (chunking) of dependencies in node_modules, improving code loading performance.

5. Other Common Configurations

5.1 Optimization Configuration: optimizeDeps

The optimizeDeps configuration item controls the pre – building process of Vite’s dependencies. During development, Vite automatically pre – builds dependencies to improve performance, but sometimes manual configuration is required.

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [
    vue()
  ],
  optimizeDeps: {
    include: ['lodash-es']
  }
});

The include property can specify modules that need to be pre – built. Adding some frequently used modules that are not automatically pre – built can enhance the development experience.

5.2 Style Configuration: css

The css configuration item is used to handle style – related settings, such as preprocessor configuration and PostCSS configuration.

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [
    vue()
  ],
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@import "@/styles/variables.scss";`
      }
    }
  }
});

The above code sets a globally imported variable file for the SCSS preprocessor through preprocessorOptions. In all .scss files in the project, these variables can be used directly without repeated imports.

By understanding these configuration items of vite.config.js, developers can customize Vite flexibly according to actual project requirements. Whether it is optimizing the development experience or improving the performance of the built project, they can handle it with ease. As projects continue to evolve and technologies update, there are still many advanced configurations of Vite waiting to be explored, allowing developers to fully unleash its powerful capabilities.

WeChat Payment Integration Guide with MidwayJS and TypeORM

1. Technology Stack Overview

  • MidwayJS: Node.js framework from Alibaba
  • TypeORM: Robust ORM framework for relational databases
  • wechatpay-node-v3: Official WeChat Pay V3 SDK
  • MySQL: Relational database (Replaceable as needed)

2. Project Configuration Setup

1. WeChat Payment Configuration

// config/config.default.ts
export default {
  payment: {
    wechat: {
      appId: 'Your public account ID',
      mchId: 'Merchant ID',
      privateKey: fs.readFileSync('apiclient_key.pem'),
      certSerialNo: 'Certificate serial number',
      apiKey: 'APIv3 key',
      publicKey: fs.readFileSync('certificate.pem'),
      notifyUrl: 'Payment callback URL',
      refundNotifyUrl: 'Refund notification URL'
    }
  }
}

2. Database Entity Design

// Payment order entity
@Entity()
export class WechatOrder {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ unique: true })
  outTradeNo: string; // Merchant order number

  @Column()
  totalAmount: number; // Order amount (in cents)

  @Column()
  status: string; // Order status
}

// Status log entity
@Entity()
export class WechatStatusLog {
  @PrimaryGeneratedColumn()
  id: number;

  @ManyToOne(() => WechatOrder)
  order: WechatOrder;

  @Column()
  status: string; // Status value

  @Column()
  remark: string; // Change description
}

3. Core Function Implementation

1. Create Payment Order

async createOrder(params: WechatCreateOrderParams) {
  // 1. Create database record
  const order = new WechatOrder();
  await this.wechatOrderModel.save(order);
  
  // 2. Call WeChat API
  const result = await this.wechatPay.transactions_jsapi({
    description: params.description,
    out_trade_no: params.outTradeNo,
    amount: { total: params.totalAmount }
  });

  // 3. Return prepay ID
  return { prepayId: result.prepay_id };
}

2. Query Order Status

async queryOrder(outTradeNo: string) {
  // 1. Find local order
  const order = await this.wechatOrderModel.findOne({ where: { outTradeNo } });
  
  // 2. Call WeChat query API
  const result = await this.wechatPay.query({ out_trade_no: outTradeNo });
  
  // 3. Sync and log status changes
  if (result.trade_state !== order.status) {
    await this.updateOrderStatus(order, result);
    await this.recordStatusChange(...);
  }
}

3. Handle Payment Notification

async handleNotify(params: any) {
  // 1. Verify merchant order
  const order = await this.wechatOrderModel.findOne({
    where: { outTradeNo: params.out_trade_no }
  });
  
  // 2. Update order status
  if (params.trade_state !== order.status) {
    order.status = params.trade_state;
    await this.wechatOrderModel.save(order);
    await this.recordStatusChange(...);
  }
  
  return true; // Return success response
}

4. Process Refund Request

async refund(params: WechatRefundParams) {
  // 1. Generate refund ID
  const outRefundNo = `${params.outTradeNo}_${Date.now()}`;
  
  // 2. Call refund API
  const result = await this.wechatPay.refund({
    out_trade_no: params.outTradeNo,
    out_refund_no: outRefundNo,
    amount: { refund: params.refundAmount }
  });
  
  // 3. Update order status
  order.status = 'REFUND';
  await this.wechatOrderModel.save(order);
}

4. Best Practice Recommendations

1. Status Management

  • Maintain complete state transitions (NOTPAY → SUCCESS/CLOSED/REFUND)
  • Use dedicated status log table
  • Regular reconciliation with WeChat

2. Error Handling

  • Implement retry mechanism for API calls
  • Use transactions for database operations
  • Add comprehensive logging

3. Security Measures

  • Verify WeChat callback signatures
  • Encrypt sensitive data
  • Store amounts in cents

5. Common Issues Troubleshooting

1. Certificate Configuration

  • Verify certificate file paths
  • Ensure serial number matches merchant platform
  • Use correct PEM format

2. Callback Handling

  • Check reverse proxy configurations
  • Return proper success response
  • Log raw callback data

3. Signature Verification

  • Validate APIv3 key correctness
  • Ensure server time synchronization
  • Verify signature algorithm

6. Optimization Directions

  1. Distributed Lock: Prevent duplicate callback processing
  2. Auto Reconciliation: Daily order verification job
  3. Monitoring System: Track failure rates & response times
  4. Multi-merchant Support: Dynamic configuration loading

This implementation provides a robust WeChat payment integration solution covering essential payment workflows. The modular design allows easy extension for business-specific requirements like marketing campaigns or data analytics dashboards.

NUXT3 $fetch request encapsulation interceptor

// 拦截器
//文件位置:/composables/useRequest.ts
import {ElMessage} from 'element-plus';

type Response = {
    url: string;
    body: any,
    status: number;
    type: string,
    statusText?: string;
    _data?: any;
    headers?: object,
    ok?: boolean,
    redirected?: boolean,
    bodyUsed?: boolean,
};
type ResponseData = {
    code: number,
    msg: string,
    data: object | object[]
}

export const useRequest = async (url: string, options: object) => {
    const router = useRouter();
    const metchArr = [
        `/admin/create`,
        `/admin/login`,
        `/user/login`,
        `/news/add`,
        `/news/delete`,
        `/sendEmail`,
        `/user/create/sendEmail`
    ]
    const ignore = metchArr.indexOf(url) !== -1;
    let headers;
    if (!ignore) {
        headers = {
            Authorization: 'Bearer ' + localStorage.getItem('token') || null,
        };
    } else {
        headers = {};
    }
    const defaultOptions: object = {
        //baseURL也可以在nuxt.config.ts中定义然后此处引入
        baseURL: "http://localhost:7001",
        headers: headers,
        //响应拦截
        onResponse({response}: { response: Response }) {
            console.log("response", response);
            const res = response._data;
            if (response.status == 200) {
                if (!res.success && res.code !== 20000) {
                    ElMessage.error(res.message || '请求失败');
                } else if (res.code && res.code !== 20000) {
                    // 根据返回的错误码显示不同的提示
                    switch (res.code) {
                        case 40000:
                            ElMessage.error(res.message || '请求失败');
                            break;
                        case 40001:
                            ElMessage.error(res.message || '邮箱验证码不一致');
                            break;
                        case 40002:
                            ElMessage.error(res.message || '两次密码输入不一致');
                            break;
                        case 40100:
                            ElMessage.warning(res.message || '未授权');
                            break;
                        case 40300:
                            ElMessage.warning(res.message || '禁止访问');
                            break;
                        case 40400:
                            ElMessage.warning(res.message || '未找到');
                            break;
                        case 50000:
                            ElMessage.error(res.message || '服务器错误');
                            break;
                        case 50001:
                            ElMessage.error(res.message || '数据库错误');
                            break;
                        case 50002:
                            ElMessage.error(res.message || 'Redis错误');
                            break;
                        case 40411:
                            ElMessage.error(res.message || '登录失败');
                            break;
                        default:
                            ElMessage.error('未知错误');
                    }
                }
            } else {
                ElMessage.error(res.message || '请求失败');
            }
            return response;
        },
        //响应错误拦截
        onResponseError({response}: { response: Response }) {
            console.log("response-error", response);
            const res = response._data;
        },
    };
    const newOptions: object = {...defaultOptions, ...options};
    //采用element-plus进行请求时的加载
    //const loadingInstance = ElLoading.service({fullscreen:false});
    const {data, pending, refresh} = await useFetch(url, newOptions);
    return {data, refresh};
};




// 使用
// 用户注册
export interface IEmailOptions {
    emailAddress: string;
}

export const SEND_EMAIL = (params: IEmailOptions) => {
    return new Promise(async (resolve, reject) => {
        try {
            const {data: response} = await useRequest('/nodeMaliler/sendEmail', {
                method: 'POST',
                body: params,
            })
            resolve(response)
        } catch (e) {
            reject(e)
        }
    })
}

Addressing Discrepancies Between three.js GLTF Model Rendering and GLTFViewer Website Previews

Addressing Discrepancies Between three.js GLTF Model Rendering and GLTFViewer Website Previews: Causes – Lighting and Environment Maps

function init(e) {  //声明初始化函数
      $("#preview3D").html("");   //获取容器dom对象
      let camera, renderer, controls, guiControls, domW, domH, animateId;   //声明变量
      domW = $("#preview3D").css("width").replace("px", "") * 1;     //拿宽高
      domH = $("#preview3D").css("height").replace("px", "") * 1;
      // 实例化renderer
      renderer = new THREE.WebGLRenderer({
        antialias: true,
        alpha: true,
      });
      // 背景色
      renderer.setClearColor(0x666666, 0);
      // 屏幕物理像素和px比率
      renderer.setPixelRatio(window.devicePixelRatio);
      // three.js 的色彩空间渲染方式  【重要】
      renderer.outputEncoding = THREE.sRGBEncoding;
      // 这个不知道干嘛用的 反正我加上了
      renderer.textureEncoding = THREE.sRGBEncoding;
      // 设置canvas宽高
      renderer.setSize(domW, domH);
      // 开启渲染阴影
      renderer.shadowMap.enabled = true;
      renderer.hadowMapEnabled = true;
      // 将renderer 加到dom元素上
      $("#preview3D")[0].appendChild(renderer.domElement);
      // 实例化场景
      scene = new THREE.Scene();
      // 添加灯光 【重要】
      function addLights(level) {
        scene.add(new THREE.AmbientLight(0xffffff, 0.3));
        var light = new THREE.DirectionalLight(0xffffff, 0.8 * Math.PI);
        light.position.set(0, 50, 0);
        scene.add(light);
      }
      addLights();
      // 灯光参数我是按照gltfViewer中的参数输入的,为了保持一直可以根据需求动态添加
      /*添加背景  */
      createObjDetailScene();
      // 添加背景声明函数
      function createObjDetailScene() {
          // 实例化透视投影镜头
        camera = new THREE.PerspectiveCamera(45, domW / domH, 1, 1000);
        camera.position.set(0, 0, -1);
        camera.lookAt(scene.position);
        // 注意参数
        initContent();
        /* 场景中的内容 */
        function initContent() {
          let object;
          // 加载 glTF 格式的模型,实例化gltfLoader加载器
          let loader = new THREE.GLTFLoader(); /*实例化加载器*/
          var pmremGenerator = new THREE.PMREMGenerator(renderer);
          pmremGenerator.compileEquirectangularShader();
          let flag = false;
             // 这里应该是根据需要走判断 本身发暗的模型就用sunset  
             // 发亮急用court
             // 不需要环境的使用null
          let str = "img/venice_sunset_1k.hdr";
           //   str = "img/footprint_court_2k.hdr";
          console.log(str);
          try {
          // 单张环境贴图加载器实例化
            new THREE.RGBELoader()
              .setDataType(THREE.UnsignedByteType)
              .load(str, function (texture) {
                var envMap = pmremGenerator.fromEquirectangular(texture)
                  .texture;
                scene.environment = envMap;
                texture.dispose();
                pmremGenerator.dispose();
              });
          } catch (e) {
            console.log(e);
          }
          // 加载gltf模型
          loader.load(url, function (gltf) {
            console.log(gltf)
            gltf.scene.traverse(function (child) {
              if (child instanceof THREE.Mesh) {
                //设置模型生成阴影并接收阴影
                child.castShadow = true;
                child.receiveShadow = true;
              }
            });
            object = gltf.scene
             object.scale.set(100, 100, 100);
            let center = getCenterPosition(object);
            object.position.set(-center.x, -center.y, -center.z);
            scene.add(gltf.scene);
          },xhr=>{
            console.log(xhr)
          },err=>{
            console.log(err)
          });
        }
        // 控制器 放大倍数  缩小倍数  是否自动旋转及速度
        controls = new THREE.OrbitControls(camera, renderer.domElement);
        controls.minDistance = 200;
        controls.maxDistance = 800;
        controls.autoRotate = true;
        controls.autoRotateSpeed = 1.7;
        animate();
        // 根据屏幕大小变化自动放大缩小canvas
        window.addEventListener("resize", onWindowResize, false);
        function onWindowResize(event) {
          SCREEN_WIDTH = $("#preview3D").css("width").replace("px", "") * 1;
          SCREEN_HEIGHT = $("#preview3D").css("height").replace("px", "") * 1;
          renderer.setSize(SCREEN_WIDTH, SCREEN_HEIGHT);
          if (camera) {
            camera.aspect = SCREEN_WIDTH / SCREEN_HEIGHT;
            camera.updateProjectionMatrix();
          }
        }

        function animate() {
          animateId = requestAnimationFrame(animate);
          controls.update();
          renderer.render(scene, camera);
        }
      }
      const getCenterPosition = (object) => {
        let p;
        if (object.isMesh) {
          object.geometry.computeBoundingBox();
          p = object.geometry.boundingBox.getCenter(new THREE.Vector3()); //.multiplyScalar(1) //加上这个表示放大了几倍 1为放大倍数
        } else {
          p = new THREE.Box3()
            .setFromObject(object)
            .getCenter(new THREE.Vector3());
        }
        return p;
      };
    };