react+Ts+批量上传华为云OBS


前言

大概内容:

商品管理,上传图片没有批量功能,所以开发前端批量直传华为云OBS后台再去获取入库。


一、华为云OBS

直接看这里OBS BrowserJS:官方地址
开发之前还是要多看看官方文档,我这边用到的就一个上传所以很简单,上代码。

二、开发步骤

1.使用npm安装(推荐)

运行npm -v命令查看npm版本并确保npm已安装。
运行npm install esdk-obs-browserjs命令执行安装。

2.使用源码安装

以安装OBS BrowserJS SDK最新版本为例,步骤如下:

下载OBS BrowserJS SDK开发包。
解压该开发包,可以看到其中包含examples文件夹(示例代码),dist文件夹(SDK库文件)和README.txt(SDK版本特性描述文件)。

<script src="./esdk-obs-browserjs-without-polyfill-x.x.x.min.js"></script>

官方描述

3.直接上代码(npm方式)

下面案例可供参考

import React from 'react';
import {
  Alert,
  Breadcrumb,
  Button,
  Icon,
  message,
  Spin,
  Steps,
  Upload
} from 'antd';
import ObsClient from 'esdk-obs-browserjs';
import { createBrowserHistory } from 'history'
const history = createBrowserHistory();

const Dragger = Upload.Dragger;
const Step = Steps.Step;
let limitMessage = true;

const steps = [
  {
    title: '上传图片',
    content: 'Second-content'
  },
  {
    title: '完成',
    content: 'Third-content'
  }
];

/**
 * 导入
 * @returns {Promise<IAsyncResult<T>>}
 */
const imporImgtGoods = (imgList) => {
  return fetch('/upload', {
    method: 'POST',
    body: JSON.stringify(imgList)
  });
};

/**
 * 获取obs参数
 * @returns {Promise<IAsyncResult<T>>}
 */
const getObsMessage = () => {
  return fetch('/get/obs/config');
};

export default class GoodsImgImport extends React.Component<any, any> {
  constructor(props) {
    super(props);
    this.state = {
      current: 0,
      loading: false,
      isImport: true,
      fileList: [],
      imgList: [],
      accessKeyId: '',
      accessKeySecret: '',
      bucketName: '',
      endPoint: ''
    };
  }
  componentWillMount(): void {
    this.getObsMessage();
  }
  next() {
    const current = this.state.current + 1;
    this.setState({ current });
  }
  prev() {
    const current = this.state.current - 1;
    this.setState({ current });
  }

  render() {
    const { current, isImport } = this.state;
    return (
      <div>
        <Breadcrumb>
          <Breadcrumb.Item>商品图片上传</Breadcrumb.Item>
        </Breadcrumb>
        <div className="container">
          <h3>商品图片上传</h3>
          <Alert
            message="操作说明:"
            description={
              <ul>
                <li>1、商品主图命名规则:sku编号_main_xx</li>
                <li>2、商品详情图命名规则:sku编号_detail_xx</li>
                <li>
                  3、请先按照规则中的要求填写图片名称数据,未按要求填写将会导致商品图片同步失败。
                </li>
                <li>
                  4、请选择.jpg .jpeg
                  .png或.gif文件,文件大小≤20M,建议每次上传不超过200个商品图片。
                </li>
              </ul>
            }
          />

          <div style={styles.uploadTit}>
            <Steps current={current}>
              {steps.map((item) => (
                <Step key={item.title} title={item.title} />
              ))}
            </Steps>
          </div>

          {current == 0 ? (
            <Spin
              spinning={this.state.loading}
              tip="数据处理中,请耐心等待....."
            >
              <div className="steps-content" style={styles.center}>
                <Dragger
                  onChange={this.changeImage}
                  fileList={this.state.fileList}
                  name="file"
                  beforeUpload={this.beforeUpload as any}
                  multiple={true} // 默认多选
                  customRequest={this.upload} // 使用antd upload 重新定义上传方法
                  accept=".jpg,.jpeg,.png,.gif"
                  showUploadList={{
                    showPreviewIcon: false,
                    showDownloadIcon: false
                  }}
                  onRemove={(file) => {
                    const uid = file.uid;
                    let fileList = this.state.fileList.filter(
                      (f) => f.uid != uid
                    );
                    let imgList = this.state.imgList.filter(
                      (f) => f.uid != uid
                    );
                    this.setState({ fileList: fileList, imgList: imgList });
                  }}
                >
                  <div style={styles.content}>
                    <p
                      className="ant-upload-hint"
                      style={{ fontSize: 14, color: 'black' }}
                    >
                      <Icon
                        type="inbox"
                        style={{ color: '#ff9147', fontSize: '48px' }}
                      />
                      <br />
                      <span>选择图片上传(点击或拖拽图片以上传)</span>
                      <br />
                      <span>支持一张或多张图片上传</span>
                    </p>
                  </div>
                </Dragger>

                <p style={styles.grey}>
                  请选择.jpg .jpeg
                  .png或.gif文件格式,文件大小≤20M,建议每次导入不超过200个商品图片。
                </p>

                <Button
                  type="primary"
                  onClick={this.confirmImport}
                  disabled={isImport}
                >
                  确认导入
                </Button>
              </div>
            </Spin>
          ) : null}
          {current == 1 ? (
            <div className="steps-content" style={styles.center}>
              <div style={styles.center}>
                <p style={styles.greyBig}>
                  上传成功,同步任务已生成,请耐心等待任务结束!
                </p>
                <p style={styles.grey1}>
                  您可以前往商品列表查看已上传的商品图片,或是继续导入。
                </p>
              </div>

              <Button type="primary" onClick={this._init}>
                继续导入
              </Button>
              <Button
                style={{ marginLeft: 20 }}
                type="primary"
                onClick={this._viewProgress}
              >
                前往商品列表
              </Button>
            </div>
          ) : null}
        </div>
      </div>
    );
  }
  _viewProgress = () => {
    //跳转到商品列表
    history.push({
      pathname: '/goods-list'
    });
  };

  _init = () => {
    let loading = false;
    let isImport = true;
    this.setState({
      loading,
      isImport
    });
    this.prev();
  };

  _next = () => {
    this.next();
  };

  confirmImport = async () => {
    const { imgList } = this.state;
    try {
      let data = [];
      imgList.forEach((ele) => {
        data.push(ele.name);
      });
      const importRes: any = await imporImgtGoods(data);
      //成功状态
      if (importRes.res.code =='0000') {
        this.next();
      } else {
        message.error(importRes.res.message);
      }
    } catch (error) {
      message.error('网络异常');
    }
  };

  changeImage = (info) => {
    // console.log(info, 'info');
    let fileList = info.fileList;
    const status = info.file.status;
    let loading = true;
    if (status == 'uploading') {
      this.setState({ loading, fileList: [...fileList] });
    }
    if (status === 'done') {
      let isImport = false;
      let fileName = '';
      fileName = info.file.name;
      loading = false;
      message.success(fileName + '上传成功');
      this.setState({
        loading,
        fileList: [...fileList],
        isImport
      });
    } else if (status === 'error') {
      message.error('上传失败');
      loading = false;
      this.setState({ loading, fileList: [...fileList] });
    }
  };
  // 上传前的检验,指定上传类型,判断大小等
  beforeUpload = async (file, fileList) => {
    let fileName = file.name.toLowerCase();
    // console.log(fileName, fileList, fileList.length);
    if (fileList.length > 200) {
      return new Promise((resolve, reject) => {
        if (limitMessage) {
          message.error('建议每次上传不超过200个商品图片');
          limitMessage = false;
          setTimeout(() => {
            limitMessage = true;
          }, 1000);
        }
        return reject(false);
      });
    }
    let index = fileName.indexOf('_');
    let fileNameNull = fileName.substring(0, index);
    if (
      fileName.indexOf('_main_') != -1 ||
      fileName.indexOf('_detail_') != -1
    ) {
      if (fileNameNull) {
        return new Promise((resolve, reject) => {
          if (file?.size > 20 * 1024 * 1024) {
            message.error('请上传20M以内的文件');
            return reject(false);
          }

          return resolve(true);
        });
      } else {
        return new Promise((resolve, reject) => {
          message.error(fileName + '不符合上传规则');
          return reject(false);
        });
      }
    } else {
      return new Promise((resolve, reject) => {
        message.error(fileName + '不符合上传规则');
        return reject(false);
      });
    }
  };

  // 重点-上传方法
  upload = (data) => {
    let that = this;
    //配置里面获取
    const obsClient = new ObsClient({
      access_key_id: this.state.accessKeyId,
      secret_access_key: this.state.accessKeySecret,
      server: this.state.endPoint
    });
    // 设置表单参数
    var formParams = {
      // 设置对象访问权限为公共读
      'x-obs-acl': obsClient.enums.AclPublicRead,
      // 设置对象MIME类型
      'content-type': 'text/plain'
    };
    // 设置表单上传请求有效期,单位:秒
    var expires = 3600;
    var res = obsClient.createPostSignatureSync({
      Expires: expires,
      FormParams: formParams
    });

    //获取上传地址,可做预览
    // var rex = obsClient.createSignedUrlSync({
    //   Method: 'PUT',
    //   Bucket: 'smbdev',
    //   Key: `temp/${data.file.name}`,
    //   Expires: expires
    // });
    // console.log(rex, 'rex');
    // //所带参数以?进行分开
    // let uploadUrl = rex.SignedUrl.split('?');
    // console.log(uploadUrl, 'uploadUrl');

    obsClient.putObject(
      {
        Bucket: this.state.bucketName, // 桶名称
        Key: `temp/${data.file.name}`, // 桶内对象文件存储地址 文件夹名称+上传文件名
        SourceFile: data.file,
        Policy: res.Policy, // 策略
        Signature: res.Signature, //签名
        expires
      },
      function(err, result) {
        if (err) {
          //此处上传失败后 可以调 data.onError方法报错
          console.log('Error-->' + err);
          data.onError();
        } else {
          //此处上传成功后 可以调 data.onSuccess更改文件上传对象的状态
          console.log('Status-->', result);
          if (result.CommonMsg.Status == 200) {
            let imgList = that.state.imgList;
            let message = {
              uid: data.file.uid,
              name: `temp/${data.file.name}`
            };
            imgList.push(message);
            that.setState({ imgList: imgList });
            data.onSuccess();
          }
        }
      }
    );
  };
  //获取obs参数
  getObsMessage = async () => {
    try {
      const obsMessage: any = await getObsMessage();
      // console.log(obsMessage, 'obsMessage');
      if (obsMessage.res.code == "0000") {
        this.setState({
          accessKeyId: obsMessage.res.context.accessKeyId,
          accessKeySecret: obsMessage.res.context.accessKeySecret,
          endPoint: 'https://' + obsMessage.res.context.endPoint,
          bucketName: obsMessage.res.context.bucketName
        });
      } else {
        message.error(obsMessage.res.message);
      }
    } catch (error) {
      message.error('网络异常');
    }
  };
}

const styles = {
  uploadTit: {
    margin: '40px 200px'
  },
  content: {
    background: '#fcfcfc',
    padding: '30px 0'
  },
  grey: {
    color: '#999999',
    marginTop: 10,
    marginBottom: 10,
    marginLeft: 10
  },
  tip: {
    marginTop: 10,
    marginLeft: 10,
    color: '#333'
  },
  error: {
    color: '#e10000'
  },
  grey1: {
    color: '#666666',
    marginTop: 20,
    marginBottom: 10,
    marginLeft: 10
  },
  center: {
    textAlign: 'center',
    width: '800px',
    margin: '0 auto'
  },
  greyBig: {
    color: '#333333',
    fontSize: 16,
    fontWeight: 'bold'
  }
} as any;


4.示例图

示例图

建议这种方式:
如何在不暴露AKSK的条件下实现与OBS交互?

5.12.22 新增进度条优化

import React from 'react';
import {
  Alert,
  Breadcrumb,
  Button,
  Icon,
  message,
  Spin,
  Steps,
  Upload,
  Progress
} from 'antd';
import { createBrowserHistory } from 'history';
const history = createBrowserHistory();
import ObsClient from 'esdk-obs-browserjs';

const Dragger = Upload.Dragger;
const Step = Steps.Step;
let limitMessage = true;

const steps = [
  {
    title: '上传图片',
    content: 'Second-content'
  },
  {
    title: '完成',
    content: 'Third-content'
  }
];

/**
 * 导入
 * @returns {Promise<IAsyncResult<T>>}
 */
const imporImgtGoods = (imgList) => {
  return fetch('/upload', {
    method: 'POST',
    body: JSON.stringify(imgList)
  });
};

/**
 * 获取obs参数
 * @returns {Promise<IAsyncResult<T>>}
 */
const getObsMessage = () => {
  return fetch('/get/obs/config');
};

export default class GoodsImport extends React.Component<any, any> {
  constructor(props) {
    super(props);
    this.state = {
      current: 0,
      loading: false,
      isImport: true,
      fileList: [],
      imgList: [],
      accessKeyId: '',
      accessKeySecret: '',
      bucketName: '',
      endPoint: '',
      num: 0,
      progress: 0
    };
  }
  componentWillMount(): void {
    this.getObsMessage();
  }
  next() {
    const current = this.state.current + 1;
    this.setState({ current });
  }
  prev() {
    const current = this.state.current - 1;
    this.setState({ current });
  }

  render() {
    const { current, isImport, loading } = this.state;
    return (
      <div>
        <Breadcrumb>
          <Breadcrumb.Item>商品图片上传</Breadcrumb.Item>
        </Breadcrumb>
        <div className="container">
          <h3>商品图片上传</h3>
          <Alert
            message="操作说明:"
            description={
              <ul>
                <li>1、商品主图命名规则:sku编号_main_xx</li>
                <li>2、商品详情图命名规则:sku编号_detail_xx</li>
                <li>
                  3、请先按照规则中的要求填写图片名称数据,未按要求填写将会导致商品图片同步失败。
                </li>
                <li>
                  4、请选择.jpg .jpeg
                  .png或.gif文件,文件大小≤20M,建议每次上传不超过200个商品图片。
                </li>
              </ul>
            }
          />

          <div style={styles.uploadTit}>
            <Steps current={current}>
              {steps.map((item) => (
                <Step key={item.title} title={item.title} />
              ))}
            </Steps>
          </div>
          {this.state.fileList && this.state.fileList.length > 0 && (
            <div style={styles.uploadTit}>
              <Progress
                percent={this.state.progress * 100}
                type="circle"//只有circle才会动态
                status="active"
                strokeColor={{
                  from: '#108ee9',
                  to: '#87d068'
                }}
              />
            </div>
          )}

          {current == 0 ? (
            <Spin
              spinning={this.state.loading}
              tip="数据处理中,请耐心等待....."
            >
              <div className="steps-content" style={styles.center}>
                <Dragger
                  onChange={this.changeImage}
                  fileList={this.state.fileList}
                  name="file"
                  beforeUpload={this.beforeUpload as any}
                  multiple={true} // 默认多选
                  customRequest={this.upload} // 使用antd upload 重新定义上传方法
                  accept=".jpg,.jpeg,.png,.gif"
                  showUploadList={{
                    showPreviewIcon: false,
                    // showRemoveIcon: false,
                    showDownloadIcon: false
                  }}
                  onRemove={(file) => {
                    const uid = file.uid;
                    let fileList = this.state.fileList.filter(
                      (f) => f.uid != uid
                    );
                    let imgList = this.state.imgList.filter(
                      (f) => f.uid != uid
                    );
                    this.setState({ fileList: fileList, imgList: imgList });
                    if (fileList.length == 0) {
                      this.setState({ num: 0, progress: 0 });
                    }
                  }}
                >
                  <div style={styles.content}>
                    <p
                      className="ant-upload-hint"
                      style={{ fontSize: 14, color: 'black' }}
                    >
                      <Icon
                        type="inbox"
                        style={{ color: '#ff9147', fontSize: '48px' }}
                      />
                      <br />
                      <span>选择图片上传(点击或拖拽图片以上传)</span>
                      <br />
                      <span>支持一张或多张图片上传</span>
                    </p>
                  </div>
                </Dragger>

                <p style={styles.grey}>
                  请选择.jpg .jpeg
                  .png或.gif文件格式,文件大小≤20M,建议每次导入不超过200个商品图片。
                </p>

                <Button
                  type="primary"
                  onClick={this.confirmImport}
                  loading={loading}
                  disabled={isImport || this.state.imgList.length == 0}
                >
                  确认导入
                </Button>
              </div>
            </Spin>
          ) : null}
          {current == 1 ? (
            <div className="steps-content" style={styles.center}>
              <div style={styles.center}>
                <p style={styles.greyBig}>
                  上传成功,同步任务已生成,请耐心等待任务结束!
                </p>
                <p style={styles.grey1}>
                  您可以前往商品列表查看已上传的商品图片,或是继续导入。
                </p>
              </div>

              <Button type="primary" onClick={this._init}>
                继续导入
              </Button>
              <Button
                style={{ marginLeft: 20 }}
                type="primary"
                onClick={this._viewProgress}
              >
                前往商品列表
              </Button>
            </div>
          ) : null}
        </div>
      </div>
    );
  }
  _viewProgress = () => {
    history.push({
      pathname: '/goods-list'
    });
  };

  _init = () => {
    let loading = false;
    let isImport = true;
    let progress = 0;
    this.setState({
      loading,
      isImport,
      progress
    });
    this.prev();
  };

  _next = () => {
    this.next();
  };

  confirmImport = async () => {
    const { imgList } = this.state;
    try {
      let data = [];
      imgList.forEach((ele) => {
        data.push(ele.name);
      });
      const importRes: any = await imporImgtGoods(data);
      if (importRes.res.code == '0000') {
        this.next();
      } else {
        message.error(importRes.res.message);
      }
    } catch (error) {
      message.error('网络异常');
    }
  };

  changeImage = (info) => {
    // console.log(info, 'info');
    let fileList = info.fileList;
    const status = info.file.status;
    let loading = true;
    if (status == 'uploading') {
      this.setState({ loading, fileList: [...fileList] });
    }
    if (status === 'done') {
      let isImport = false;
      let fileName = '';
      fileName = info.file.name;
      message.success(fileName + '上传成功');
      this.setState({
        fileList: [...fileList],
        isImport
      });
    } else if (status === 'error') {
      message.error('上传失败');
      this.setState({ fileList: [...fileList] });
    }
  };
  // 上传前的检验,指定上传类型,判断大小等
  beforeUpload = async (file, fileList) => {
    let fileName = file.name.toLowerCase();
    this.setState({ progress: 0 });
    // console.log(fileName, fileList, fileList.length);
    if (fileList.length > 200) {
      return new Promise((resolve, reject) => {
        if (limitMessage) {
          message.error('建议每次上传不超过200个商品图片');
          limitMessage = false;
          setTimeout(() => {
            limitMessage = true;
          }, 1000);
        }
        return reject(false);
      });
    }
    let index = fileName.indexOf('_');
    let fileNameNull = fileName.substring(0, index);
    if (
      fileName.indexOf('_main_') != -1 ||
      fileName.indexOf('_detail_') != -1
    ) {
      if (fileNameNull) {
        return new Promise((resolve, reject) => {
          if (file?.size > 20 * 1024 * 1024) {
            message.error('请上传20M以内的文件');
            return reject(false);
          }

          return resolve(true);
        });
      } else {
        return new Promise((resolve, reject) => {
          message.error(fileName + '不符合上传规则');
          return reject(false);
        });
      }
    } else {
      return new Promise((resolve, reject) => {
        message.error(fileName + '不符合上传规则');
        return reject(false);
      });
    }
  };

  // 重点-上传方法
  upload = async (data) => {
    let that = this;
    //配置里面获取
    const obsClient = new ObsClient({
      access_key_id: this.state.accessKeyId,
      secret_access_key: this.state.accessKeySecret,
      server: this.state.endPoint
    });
    // 设置表单参数
    var formParams = {
      // 设置对象访问权限为公共读
      'x-obs-acl': obsClient.enums.AclPublicRead,
      // 设置对象MIME类型
      'content-type': 'text/plain'
    };
    // 设置表单上传请求有效期,单位:秒
    var expires = 3600;
    var res = obsClient.createPostSignatureSync({
      Expires: expires,
      FormParams: formParams
    });

    //获取上传地址,可做预览
    // var rex = obsClient.createSignedUrlSync({
    //   Method: 'PUT',
    //   Bucket: 'smbdev',
    //   Key: `smbTemp/${data.file.name}`,
    //   Expires: expires
    // });
    // console.log(rex, 'rex');
    // //所带参数以?进行分开
    // let uploadUrl = rex.SignedUrl.split('?');
    // console.log(uploadUrl, 'uploadUrl');
    let loading = true;

    obsClient.putObject(
      {
        Bucket: this.state.bucketName, // 桶名称
        Key: `smbTemp/${data.file.name}`, // 桶 内对象文件存储地址  文件夹名称 +上传文件名
        SourceFile: data.file,
        Policy: res.Policy, // 策略
        Signature: res.Signature, //签名
        expires
      },
      function(err, result) {
        if (err) {
          //此处上传失败后 可以调 data.onError 方法报错,upload 报错红色条,也可以在data 里面找到对应的方法改动
          console.log('Error-->' + err);
          loading = false;
          that.setState({ loading: loading });
          data.onError();
        } else {
          //此处上传成功后 可以调 data.onSuccess() 更改文件上传对象的状态
          console.log('Status-->', result);
          if (result.CommonMsg.Status == 200) {
            let loading = true;
            that.setState({
              num: that.state.num + 1
            });
            that.setState({
              progress: (that.state.num / that.state.fileList.length).toFixed(2)
            });
            // console.log(that.state.num, that.state.fileList);
            if (that.state.num == that.state.fileList.length) {
              loading = false;
              that.setState({
                loading: loading
              });
            }
            let imgList = that.state.imgList;
            let message = {
              uid: data.file.uid,
              name: `smbTemp/${data.file.name}`
            };
            imgList.push(message);
            that.setState({ imgList: imgList });
            data.onSuccess();
          }
        }
      }
    );
  };
  //获取obs参数
  getObsMessage = async () => {
    try {
      const obsMessage: any = await getObsMessage();
      // console.log(obsMessage, 'obsMessage');
      if (obsMessage.res.code == '0000') {
        this.setState({
          accessKeyId: obsMessage.res.context.accessKeyId,
          accessKeySecret: obsMessage.res.context.accessKeySecret,
          endPoint: 'https://' + obsMessage.res.context.endPoint,
          bucketName: obsMessage.res.context.bucketName
        });
      } else {
        message.error(obsMessage.res.message);
      }
    } catch (error) {
      message.error('网络异常');
    }
  };
}

const styles = {
  uploadTit: {
    margin: '40px 200px'
  },
  content: {
    background: '#fcfcfc',
    padding: '30px 0'
  },
  grey: {
    color: '#999999',
    marginTop: 10,
    marginBottom: 10,
    marginLeft: 10
  },
  tip: {
    marginTop: 10,
    marginLeft: 10,
    color: '#333'
  },
  error: {
    color: '#e10000'
  },
  grey1: {
    color: '#666666',
    marginTop: 20,
    marginBottom: 10,
    marginLeft: 10
  },
  center: {
    textAlign: 'center',
    width: '800px',
    margin: '0 auto'
  },
  greyBig: {
    color: '#333333',
    fontSize: 16,
    fontWeight: 'bold'
  }
} as any;

6.如果限制单个大文件上传进度条优化

//在upload自定义方法中加入官方获取进度方法
var callback = function(transferredAmount, totalAmount, totalSeconds) {
      // 获取上传平均速率(KB/S)
      // console.log((transferredAmount * 1.0) / totalSeconds / 1024);
      // 获取上传进度百分比
      // console.log((transferredAmount * 100.0) / totalAmount);
      that.setState({
        progress: Number(
          ((transferredAmount * 100.0) / totalAmount).toFixed(2)
        ),
        statistic: Number(
          ((transferredAmount * 1.0) / totalSeconds / 1024).toFixed(2)
        )
      });
      //通知组件内置进度条变化
      data.onProgress({
        percent: Number(((transferredAmount * 100.0) / totalAmount).toFixed(2))
      });
    };

如图:
示例

还有直接找到了自定义上传方法的参数
自定义上传
官方地址


总结

以上就是简单的上传方法,建议不暴露AKSK,本文只是简单参考,有问题随时沟通。

工作是为了生活,但生活不是为了工作!

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值