Spring Cloud + Vue前后端分离-第10章 基于阿里云OSS的文件上传

 源代码在GitHub - 629y/course: Spring Cloud + Vue前后端分离-在线课程

Spring Cloud + Vue前后端分离-第10章 基于阿里云OSS的文件上传

前面介绍的文件上传是基于本地文件服务器的文件上传,但是自己搭文件服务器会有很多运维的问题,比如磁盘满了要扩容,高峰期要增加带宽,低谷期要减少带宽,为了安全,我们还要对文件做备份等等。

所以一般会选择云平台来存储文件,云平台有很多,比如阿里云、腾讯云、百度云等,云平台做的最好的是亚马逊,我们这里选择国内最大的阿里云。

10-1 阿里云OSS简介

面向海量数据规模的分布式存储服务

从浏览器通过ECS( 服务器)访问OSS,算内网访问,不算流量费,但是要考虑ECS的带宽问题。从浏览器直接访问OSS资源,算外网访问OSS,需付流量费,但是可以不用考虑带宽。

10-2 基于OSS接口的文件上传

阿里云OSS控台的使用

bucket:存储空间,名称必须是全阿里云唯一

这个文件地址可以直接打开观看

基于OSS接口实现文件上传

1.大文件断点续传与极速秒传:基于OSS接口实现文件上传,增加文件追加上传oss-append 和简单上传oss-simple

pom.xml

OssController.java

package com.course.file.controller.admin;

import com.alibaba.fastjson.JSONObject;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.model.AppendObjectRequest;
import com.aliyun.oss.model.AppendObjectResult;
import com.aliyun.oss.model.ObjectMetadata;
import com.aliyun.oss.model.PutObjectRequest;
import com.course.server.dto.FileDto;
import com.course.server.dto.ResponseDto;
import com.course.server.enums.FileUseEnum;
import com.course.server.service.FileService;
import com.course.server.util.Base64ToMultipartFile;
import com.course.server.util.UuidUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import java.io.ByteArrayInputStream;

@RestController
@RequestMapping("/admin")
public class OssController {
    private static final Logger LOG = LoggerFactory.getLogger(OssController.class);

    @Value("${oss.accessKeyId}")
    private String accessKeyId;

    @Value("${oss.accessKeySecret}")
    private String accessKeySecret;

    @Value("${oss.endpoint}")
    private String endpoint;

    @Value("${oss.bucket}")
    private String bucket;

    @Value("${oss.domain}")
    private String ossDomain;

    public static final String BUSINESS_NAME = "文件上传";

    @Resource
    private FileService fileService;

    @PostMapping("/oss-append")
    public ResponseDto fileUpload(@RequestBody FileDto fileDto) throws Exception {
        LOG.info("上传文件开始");
        String use = fileDto.getUse();
        String key = fileDto.getKey();
        String suffix = fileDto.getSuffix();
        Integer shardIndex = fileDto.getShardIndex();
        Integer shardSize = fileDto.getShardSize();
        String shardBase64 = fileDto.getShard();
        MultipartFile shard = Base64ToMultipartFile.base64ToMultipart(shardBase64);

        // 保存文件到本地
        FileUseEnum useEnum = FileUseEnum.getByCode(use);

//        //如果文件夹不存在则创建
        String dir = useEnum.name().toLowerCase();
//        File fullDir = new File(FILE_PATH + dir);
//        if (!fullDir.exists()) {
//            fullDir.mkdir();
//        }

//        String path = dir + File.separator + key + "." + suffix + "." + fileDto.getShardIndex();
        String path = new StringBuffer(dir)
                .append("/")
                .append(key)
                .append(".")
                .append(suffix)
                .toString(); // course\6sfSqfOwzmik4A4icMYuUe.mp4
//        String localPath = new StringBuffer(path)
//                .append(".")
//                .append(fileDto.getShardIndex())
//                .toString(); // course\6sfSqfOwzmik4A4icMYuUe.mp4.1
//        String fullPath = FILE_PATH + localPath;
//        File dest = new File(fullPath);
//        shard.transferTo(dest);
//        LOG.info(dest.getAbsolutePath());

        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

        ObjectMetadata meta = new ObjectMetadata();
// 指定上传的内容类型。
        meta.setContentType("text/plain");

        // 通过AppendObjectRequest设置多个参数。
        AppendObjectRequest appendObjectRequest = new AppendObjectRequest(bucket, path, new ByteArrayInputStream(shard.getBytes()), meta);

        // 通过AppendObjectRequest设置单个参数。
        // 设置存储空间名称。
        //appendObjectRequest.setBucketName("<yourBucketName>");
        // 设置文件名称。
        //appendObjectRequest.setKey("<yourObjectName>");
        // 设置待追加的内容。有两种可选类型:InputStream类型和File类型。这里为InputStream类型。
        //appendObjectRequest.setInputStream(new ByteArrayInputStream(content1.getBytes()));
        // 设置待追加的内容。有两种可选类型:InputStream类型和File类型。这里为File类型。
        //appendObjectRequest.setFile(new File("<yourLocalFile>"));
        // 指定文件的元信息,第一次追加时有效。
        //appendObjectRequest.setMetadata(meta);

        // 第一次追加。
        // 设置文件的追加位置。
//        appendObjectRequest.setPosition(0L);
        appendObjectRequest.setPosition((long) ((shardIndex - 1) * shardSize));
        AppendObjectResult appendObjectResult = ossClient.appendObject(appendObjectRequest);
        // 文件的64位CRC值。此值根据ECMA-182标准计算得出。
        System.out.println(appendObjectResult.getObjectCRC());
        System.out.println(JSONObject.toJSONString(appendObjectResult));

//        // 第二次追加。
//        // nextPosition指明下一次请求中应当提供的Position,即文件当前的长度。
//        appendObjectRequest.setPosition(appendObjectResult.getNextPosition());
//        appendObjectRequest.setInputStream(new ByteArrayInputStream(content2.getBytes()));
//        appendObjectResult = ossClient.appendObject(appendObjectRequest);
//
//        // 第三次追加。
//        appendObjectRequest.setPosition(appendObjectResult.getNextPosition());
//        appendObjectRequest.setInputStream(new ByteArrayInputStream(content3.getBytes()));
//        appendObjectResult = ossClient.appendObject(appendObjectRequest);

        // 关闭OSSClient。
        ossClient.shutdown();

        LOG.info("保存文件记录开始");
        fileDto.setPath(path);
        fileService.save(fileDto);

        ResponseDto responseDto = new ResponseDto();
        fileDto.setPath(ossDomain + path);
        responseDto.setContent(fileDto);

//        if (fileDto.getShardIndex().equals(fileDto.getShardTotal())) {
//            this.merge(fileDto);
//        }
        return responseDto;
    }


    @PostMapping("/oss-simple")
    public ResponseDto fileUpload(@RequestParam MultipartFile file, String use) throws Exception {
        LOG.info("上传文件开始");
        FileUseEnum useEnum = FileUseEnum.getByCode(use);
        String key = UuidUtil.getShortUuid();
        String fileName = file.getOriginalFilename();
        String suffix = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
        String dir = useEnum.name().toLowerCase();
        String path = dir + "/" + key + "." + suffix;

        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

        // 创建PutObjectRequest对象。
//        String content = "Hello OSS";
        // <yourObjectName>表示上传文件到OSS时需要指定包含文件后缀在内的完整路径,例如abc/efg/123.jpg。
        PutObjectRequest putObjectRequest = new PutObjectRequest(bucket, path, new ByteArrayInputStream(file.getBytes()));

        // 如果需要上传时设置存储类型与访问权限,请参考以下示例代码。
        // ObjectMetadata metadata = new ObjectMetadata();
        // metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString());
        // metadata.setObjectAcl(CannedAccessControlList.Private);
        // putObjectRequest.setMetadata(metadata);

        // 上传字符串。
        ossClient.putObject(putObjectRequest);

//        LOG.info("保存文件记录开始");
//        fileDto.setPath(path);
//        fileService.save(fileDto);

        ResponseDto responseDto = new ResponseDto();
        FileDto fileDto = new FileDto();
        fileDto.setPath(ossDomain + path);
        responseDto.setContent(fileDto);

        return responseDto;
    }
}

application.properties

big-file.vue

file.vue

演示:头像上传是用的单文件传输到OSS;视频上传是用的分片传输到OSS

teacher.vue

测试

上传视频和头像

10-3{white}阿里云视频点播服务介绍{white}{white}

阿里云视频点播服务介绍

视频点播控台的使用

视频点播帮助文档概览

10-4 基于OSS原生SDK上传视频到点播服务

VOD上传并加密转码

VOD上传并加密转码

1.视频加密与授权播放:基于 OSS原生SDK上传视频到点播服务,官方示例代码

pom.xml(course)

pom.xml(server)

VodUtil.java

package com.course.server.util;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.aliyun.oss.OSSClient;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.http.FormatType;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.vod.model.v20170321.*;
import org.apache.commons.codec.binary.Base64;

import java.io.File;
import java.io.InputStream;

public class VodUtil {

    /**
     * 使用AK初始化VOD客户端
     * @param accessKeyId
     * @param accessKeySecret
     * @return
     * @throws ClientException
     */
    public static DefaultAcsClient initVodClient(String accessKeyId, String accessKeySecret) throws ClientException {
        // 点播服务接入区域,国内请填cn-shanghai,其他区域请参考文档[点播中心](~~98194~~)
        String regionId = "cn-shanghai";
        DefaultProfile profile = DefaultProfile.getProfile(regionId, accessKeyId, accessKeySecret);
        DefaultAcsClient client = new DefaultAcsClient(profile);
        return client;
    }

    /**
     * 获取视频上传地址和凭证
     * @param vodClient
     * @return
     * @throws ClientException
     */
    public static CreateUploadVideoResponse createUploadVideo(DefaultAcsClient vodClient, String fileName) throws ClientException {
        CreateUploadVideoRequest request = new CreateUploadVideoRequest();
        request.setFileName(fileName);
        request.setTitle(fileName);
        //request.setDescription("this is desc");
        //request.setTags("tag1,tag2");
//        request.setCoverURL("http://vod.aliyun.com/test_cover_url.jpg");
//        request.setCateId(1000115308L);
//        request.setTemplateGroupId("78fffb8c0c2426efd5baaaafed76fe36");
        //request.setWorkflowId("");
        //request.setStorageLocation("");
        //request.setAppId("app-1000000");
        //设置请求超时时间
        request.setSysReadTimeout(1000);
        request.setSysConnectTimeout(1000);
        return vodClient.getAcsResponse(request);
    }

    /**
     * 使用上传凭证和地址初始化OSS客户端(注意需要先Base64解码并Json Decode再传入)
     * @param uploadAuth
     * @param uploadAddress
     * @return
     */
    public static OSSClient initOssClient(JSONObject uploadAuth, JSONObject uploadAddress) {
        String endpoint = uploadAddress.getString("Endpoint");
        String accessKeyId = uploadAuth.getString("AccessKeyId");
        String accessKeySecret = uploadAuth.getString("AccessKeySecret");
        String securityToken = uploadAuth.getString("SecurityToken");
        return new OSSClient(endpoint, accessKeyId, accessKeySecret, securityToken);
    }

    /**
     * 简单上传
     * @param ossClient
     * @param uploadAddress
     * @param inputStream
     */
    public static void uploadLocalFile(OSSClient ossClient, JSONObject uploadAddress, InputStream inputStream){
        String bucketName = uploadAddress.getString("Bucket");
        String objectName = uploadAddress.getString("FileName");
        // 单文件上传
        ossClient.putObject(bucketName, objectName, inputStream);

        /* 视频点播不支持追加上传
        // 追加上传
        ObjectMetadata meta = new ObjectMetadata();
        meta.setContentType("text/plain");
        AppendObjectRequest request = new AppendObjectRequest(bucketName, objectName, file, meta);
        request.setPosition(0L);
        ossClient.appendObject(request);*/
    }

    /**
     * 上传本地文件
     * @param ossClient
     * @param uploadAddress
     * @param localFile
     */
    public static void uploadLocalFile(OSSClient ossClient, JSONObject uploadAddress, String localFile){
        String bucketName = uploadAddress.getString("Bucket");
        String objectName = uploadAddress.getString("FileName");
        File file = new File(localFile);
        // 单文件上传
        ossClient.putObject(bucketName, objectName, file);

        /* 视频点播不支持追加上传
        // 追加上传
        ObjectMetadata meta = new ObjectMetadata();
        meta.setContentType("text/plain");
        AppendObjectRequest request = new AppendObjectRequest(bucketName, objectName, file, meta);
        request.setPosition(0L);
        ossClient.appendObject(request);*/
    }

    /**
     * 刷新上传凭证
     * @param vodClient
     * @return
     * @throws ClientException
     */
    public static RefreshUploadVideoResponse refreshUploadVideo(DefaultAcsClient vodClient) throws ClientException {
        RefreshUploadVideoRequest request = new RefreshUploadVideoRequest();
        request.setAcceptFormat(FormatType.JSON);
        request.setVideoId("VideoId");
        //设置请求超时时间
        request.setSysReadTimeout(1000);
        request.setSysConnectTimeout(1000);
        return vodClient.getAcsResponse(request);
    }

    /**
     * 获取源文件信息
     * @param client 发送请求客户端
     * @return GetMezzanineInfoResponse 获取源文件信息响应数据
     * @throws Exception
     */
    public static GetMezzanineInfoResponse getMezzanineInfo(DefaultAcsClient client, String videoId) throws Exception {
        GetMezzanineInfoRequest request = new GetMezzanineInfoRequest();
        request.setVideoId(videoId);
        //源片下载地址过期时间
        request.setAuthTimeout(3600L);
        return client.getAcsResponse(request);
    }

    /**
     * 获取播放凭证函数
     * @param client
     * @return
     * @throws Exception
     */
    public static GetVideoPlayAuthResponse getVideoPlayAuth(DefaultAcsClient client, String videoId) throws Exception {
        GetVideoPlayAuthRequest request = new GetVideoPlayAuthRequest();
        request.setVideoId(videoId);
        return client.getAcsResponse(request);
    }

    public static void main(String[] argv) {
        //您的AccessKeyId
        String accessKeyId = "xxxxxxxxxxxxx";
        //您的AccessKeySecret
        String accessKeySecret = "xxxxxxxxxxxxx";
        //需要上传到VOD的本地视频文件的完整路径,需要包含文件扩展名
        String localFile = "/Users/zhaoxiaoyun/Downloads/fourcats.mp4";
        try {
            // 初始化VOD客户端并获取上传地址和凭证
            DefaultAcsClient vodClient = initVodClient(accessKeyId, accessKeySecret);
            String fileName = "test.mp4";
            CreateUploadVideoResponse createUploadVideoResponse = createUploadVideo(vodClient, fileName);
            // 执行成功会返回VideoId、UploadAddress和UploadAuth
            String videoId = createUploadVideoResponse.getVideoId();
            JSONObject uploadAuth = JSONObject.parseObject(
                    Base64.decodeBase64(createUploadVideoResponse.getUploadAuth()), JSONObject.class);
            JSONObject uploadAddress = JSONObject.parseObject(
                    Base64.decodeBase64(createUploadVideoResponse.getUploadAddress()), JSONObject.class);
            // 使用UploadAuth和UploadAddress初始化OSS客户端
            OSSClient ossClient = initOssClient(uploadAuth, uploadAddress);
            // 上传文件,注意是同步上传会阻塞等待,耗时与文件大小和网络上行带宽有关
            uploadLocalFile(ossClient, uploadAddress, localFile);
            System.out.println("上传视频成功, VideoId : " + videoId); // 7d6b8c07ab48456e932187080f42e88f

            GetMezzanineInfoResponse response = new GetMezzanineInfoResponse();
            response = getMezzanineInfo(vodClient, videoId);
            System.out.println("获取视频信息, response : " + JSON.toJSONString(response));
        } catch (Exception e) {
            System.out.println("上传视频失败, ErrorMessage : " + e.getLocalizedMessage());
        }
    }
}

 记得修改自己的

VOD上传到指定分类,并自动转码

1.视频加密与授权播放:基于 OSS原生SDK上传视频到点播服务,上传到指定分类,并自动转码

VodUtil.java

视频点播不支持追加上传

1.视频加密与授权播放:基于 OSS原生SDK上传视频到点播服务,视频点播不支持追加上传

官方示例中,用的是简单上传putObject ,我们要改成我们需要的追加上传

但是有严格封装编码要求的文件类型,都是不支持追加方式上传的;比如音视频、图片等,上传到视频点播不支持追加上传

VodUtil.java

效果就不演示了,感兴趣的自己尝试一下,把这段代码打开,把上面的单文件上传注释,然后看效果,是一直在上传,但是无法上传成功!

项目集成VOD上传

小节增加vod字段

1.视频加密与授权播放:小节增加vod字段

由于视频点播不支持追加上传,我们只能改成单文件传

all.sql

generatorConfig.xml

SectionDto.java

section.vue

简单快速的方案:在阿里云控台做文件上传和转码,然后将vod复制到我们自己的控台

增加vod组件,用于上传视频到视频点播服务

1.视频加密与授权播放:增加vod组件,用于上传视频到视频点播服务

big-file.vue

vod组件就是对big-file组件做了一层封装。程序员天然喜欢对代码做封装,要注意度,封装得越多,灵活度就越低

vod.vue

<template>
  <big-file v-bind:input-id="inputId"
            v-bind:text="text"
            v-bind:suffixs="suffixs"
            v-bind:use="use"
            v-bind:after-upload="afterUpload"
            v-bind:shard-size="shardSize"
            v-bind:url="'oss-append'">
  </big-file>
</template>

<script>
import BigFile from "./big-file";
export default {
  name: "vod",
  components: {BigFile},
  props: {
    text: {
      default: "上传VOD"
    },
    inputId: {
      default: "vod-upload"
    },
    suffixs: {
      default: []
    },
    use: {
      default: ""
    },
    shardSize: {
      default: 50 * 1024
    },
    afterUpload: {
      type: Function,
      default: null
    },
  },
}
</script>

目前为止,vod和big-file功能一样,需要先测试下vod组件是否可用

section.vue

 我们在js里对video赋值,vue会监听到值的变化,并渲染视频控件。如果还没渲染完,我们就去获取时长,这时就会得到NaN,所以需要加延时获取

测试,记得先清空数据库和oss文件

file表增加vod字段

1.视频加密与授权播放:file表增加vod字段

all.sql

generatorConfig.xml

FileDto.java

增加视频点播文件上传功能

1.视频加密与授权播放:增加视频点播文件上传功能

application.properties

vod.vue

由于视频点播不支持追加上传,所以使用vod组件进行上传的,只能有一个分片

section.vue

获取到的可播放地址是有时效的,所以就算保存到数据库也会过期,没用。以后会根据vod来播放。

文件检查时,根据是否是视频点播文件来获取视频信息

1.视频加密与授权播放:文件检查时,根据是否是视频点播文件来获取视频信息

UploadController.java

 测试 

10-6 视频授权播放功能开发

阿里云播放器的基本使用

1.视频加密与授权播放:集成阿里云播放器,制作player播放器组件

player.vue

<template>
  <div v-bind:id="playerId">
<!--要做成动态的,所以不在这个地方写了-->
<!--    <div class="prism-player" id="J_prismPlayer"></div>-->
  </div>
</template>
<script>
export default {
  name: "player",
  props: {
    playerId: {
      default: "player-div"
    },
  },
  data: function () {
    return {
      aliPlayer: {},//播放器实例
    }
  },
  methods: {
    playUrl(url) {
      let _this = this;
      console.log("开始播放:", url);

      //如果已经有播放器了,则将播放器div删除
      if (_this.aliPlayer) {
        _this.aliPlayer = null;
        $("#J_prismPlayer").remove();
      }

      // 初始化播放器
      $("#" + _this.playerId)
          .append("<div class=\"prism-player\" id=\"J_prismPlayer\"></div>");
      _this.aliPlayer = new Aliplayer({
        id: "J_prismPlayer",
        width: '100%',
        autoplay: false,
        source: url,
        cover: 'http://imooc-coursemac.oss-cn-shanghai.aliyuncs.com/头像1.jpg',
      }, function (player) {
        console.log('播放器创建好了。')
      });
    },
  }
}
</script>

index.html

section.vue

 video控件隐藏掉,不要删除,要通过它获取时长。也可以通过视频点播的API去获取视频时长。

测试

功能更强,可以倍速等等

获取vod授权码并授权播放

1.视频加密与授权播放:获取vod授权码并授权播放,需要在存储管理中把权限设置成公共读

默认是私有的,更改为公共读

VodUtil.java

VodController.java

player.vue

一个页面可能会放多个player组件,所以需要把id做成动态变化的,一个页面的元素,id值要是唯一的。

modal-player.vue

带有模态框的播放器组件modal-player,里面包含了player组件

<template>
  <div id="player-modal" class="modal" tabindex="-1" role="dialog">
    <div class="modal-dialog" role="document">
      <div class="modal-content">
        <div class="modal-body">
          <player v-bind:player-id="'modal-player-div'"
                  ref="player"></player>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
import Player from "./player";
export default {
  name: 'modal-player',
  components: {Player},
  data: function () {
    return {
      aliPlayer: {}, // 播放器实例
    }
  },
  methods: {
    playUrl(url) {
      let _this = this;
      _this.$refs.player.playUrl(url);
    },

    playVod(vod) {
      let _this = this;
      _this.$refs.player.playVod(vod);
      $("#player-modal").modal("show");
    }
  }
}
</script>

<style scoped>
#player-modal .modal-body {
  padding: 0;
}
</style>

section.vue

测试

  • 24
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
本文介绍了一个基于Spring Boot、Spring CloudVue前后端分离的项目实战。这个项目是一个简单的在线商城,包含了用户注册、登录、商品展示、购物车、订单管理等功能。通过这个项目,读者可以深入理解前后端分离的架构模式和互联网应用的开发方式。 首先,文介绍了前后端分离的基本概念和优势。前后端分离是将应用的前端和后端代码分开来开发,使得前端和后端具有独立的开发周期和技术栈,进而提高了开发效率和代码质量。同时,前后端分离还可以提供更好的用户体验和灵活性,对于互联网应用来说尤为重要。 接下来,文介绍了项目的架构和技术栈。项目采用了Spring Boot和Spring Cloud框架来实现后端代码,采用MyBatis作为ORM框架和Redis作为缓存中间件。同时,项目还采用了Vue.js作为前端框架和Element UI组件库来实现前端页面。通过这些开源框架和组件,可以快速搭建一个前后端分离的互联网应用。 然后,文介绍了项目的核心功能和代码实现。在用户注册和登录方面,项目采用了Spring Security框架和JWT令牌来实现用户认证和授权,保证了用户信息的安全性。在商品展示和购物车方面,项目采用了Vue.js来实现前端页面和事件处理。在订单管理方面,项目采用了MyBatis Plus来实现订单数据的持久化和分页查询。 最后,文介绍了项目的测试和优化。通过对项目的压力测试和性能测试,文发现项目还存在一些性能瓶颈和安全隐患,可以通过优化数据库查询、缓存配置和代码实现来提高应用的性能和安全性。 总之,这篇文介绍了一个基于Spring Boot、Spring CloudVue前后端分离的项目实战,通过实现一个在线商城的功能,展示了前后端分离的开发模式和互联网应用的开发技术栈。本文可以作为前后端分离开发的入门教程,也可以作为互联网应用开发的参考文档。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值