SSM 框架搭建 sckill 秒杀系统 —— step.3 业务逻辑层建设

索引:

  1. SSM 框架搭建 sckill (秒杀系统) —— step.1 前期准备 + 框架配置
  2. SSM 框架搭建 sckill (秒杀系统) —— step.2 持久层建设
  3. SSM 框架搭建 sckill 秒杀系统 —— step.3 业务逻辑层建设
  4. SSM 框架搭建 sckill 秒杀系统 —— step.4 web控制层建设
  5. SSM 框架搭建 sckill 秒杀系统 —— step.5 web展示层建设

一、Service 层简介

       Service 层是服务层,也被称为业务逻辑层,是整个系统实现系统所需的业务逻辑功能的核心部分,承担着通过调用 DAO 接口实现数据的操纵与存储、利用 JavaBean 实现对系统所需数据处理并存储页面渲染功能的关键层级。
小贴士: 此处 Service 层的构建方式是使用接口 + 实现类的形式进行构建的。这样做是出于 Java 架构思想中开闭原则(即对拓展开放,对修改关闭)。(就通常而言,这是一种编程习惯,更适用项目的开发。)通过接口 + 实现类的组合,在需要向系统中添加新功能时,无需对原有功能的源码进行修改,只需添加新的抽象方法,并在实现类中进行实现就可以达到目的,并且,由于定义接口的存在,使得代码的重用率大大提高,在移植项目框架时,只需要重新定义方法的实现,而不需要整体替换。从而达到松耦合的效果,使得系统的维护更加便捷。

二、Service 层搭建

       如图所示,Service 层的接口及实现类存放与 service 文件夹下,通常,接口抽象方法的实现类放于 service/impl 目录下,以 “对应接口名 + Impl” 的方式命名。
在这里插入图片描述

(1) Service 接口定义

       由于在该 Seckill 秒杀系统较为简洁。主要的业务逻辑只需要:“1.对所有的秒杀记录进行查询;2.对单个特定秒杀记录进行查询;3.通过时间,控制秒杀的开启;4.执行秒杀操作” 四个核心功能。因此接口中,我们只需要对这四个方法进行定义。定义方式为,在接口 SeckillService 中定义:

public interface SeckillService {

    /**
     * 查询所有秒杀记录
     * @return
     */
    List<SeckillTab> getSeckillList();

    /**
     * 查询单个秒杀记录
     * @param seckillId
     * @return
     */
    SeckillTab getSeckillById(@Param("seckillId") long seckillId);

    /**
     * 目的:秒杀开启时输出秒杀地址,否则输出系统时间和秒杀时间
     * DTO :封装数据传输,
     * @param seckillId
     */
    Exposer exportSeckillUrl(@Param("seckill") long seckillId);

    /**
     * 执行秒杀操作
     * 内部 md5 比对方式如果不匹配,则用户ID被篡改,拒绝执行秒杀
     * 当抛出异常时,应该告诉接口使用方,可能会输出什么样的异常
     * SeckillException : 告诉用户秒杀错误
     * RepeatKillException : 告诉用户已经秒杀成功
     * SeckillCloseException : 告诉用户秒杀已经关闭
     * @param seckillId
     * @param userPhone
     * @param md5
     */
    SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
            throws SeckillException, RepeatKillException, SeckillCloseException;
}

(2)系统异常的定义

       考虑到系统的稳定性。在系统运行时,会由于一些非正常操作,或者缓存、信息传输延时等原因导致的运行期异常。为与 Java 自带的运行期异常进行区分。视频中定义了三种异常的抛出类,分别是: RepeatKillException (重复秒杀异常)、SeckillCloseException (通道关闭后秒杀异常)和 SeckillException (系统运行异常)。在 根目录下定义 exception 目录作为异常类的储存目录,三种异常分别配置如下:
在这里插入图片描述

  1. SeckillException (系统运行异常):
package org.seckill.exception;

/**
 * 秒杀相关业务异常
 * 所有相关异常的父类,从理论上来讲,所有属于该系统的报错都由该类进行处理,
 * 但是这样就失去了进行异常类定义的意义,无法对报错的原因进行精确的定位,
 * 因此该类只是作为整个系统运行时的总体异常类。
 */

public class SeckillException extends RuntimeException {
    public SeckillException(String message) {
        super(message);
    }

    public SeckillException(String message, Throwable cause) {
        super(message, cause);
    }
}
  1. SeckillCloseException (秒杀接口关闭异常):
public class SeckillCloseException extends SeckillException {
    public SeckillCloseException(String message) {
        super(message);
    }

    public SeckillCloseException(String message, Throwable cause) {
        super(message, cause);
    }
}
  1. RepeatKillException (重复秒杀异常):
package org.seckill.exception;

/**
 * 重复秒杀异常(本质上:运行期异常)  —— 属于秒杀相关业务异常
 * 用户通过重复执行秒杀程序提高秒杀成功率时,当秒杀成功时,产生重复秒杀异常
 * 系统能够识别重复秒杀异常并阻止重复秒杀行为
 */

public class RepeatKillException extends SeckillException {
    public RepeatKillException(String message) {
        super(message);
    }

    public RepeatKillException(String message, Throwable cause) {
        super(message, cause);
    }
}

(3)常量判定符 - 数据字典 建设

       一个良好的系统在实现 if - else 逻辑分辨时,通常使用 enum (枚举)来定义具有特定意义的 key 键,而非单纯的数字或文字。这样的好处是:

  1. 通过特定意义的 key 键的定义,使得每个 key 键都有自己的注释,最直观的表现是代码的可读性得到极大的提升;
  2. 由于系统的功能具有部分重叠与逻辑关联的特性,key 键的定义通常可以得到广泛的使用,当一个位置上的 key 值需要改变时,只需要改动 key 键的定义,就可以完成判定规则的修改。从而大大提高了系统的可维护性;(如果使用的是单纯的数字或文字,当一处 key 被修改后,为保证系统的正常运行,必须对整个系统进行检查,防止同样的定义在系统的相关功能区应用。)
           数据字典的定义是建立在系统根目录下的 enums 目录下,如图所示:
    在这里插入图片描述
            枚举型数据字典 SeckillStatEnum 定义如下:
public enum SeckillStatEnum {
    SUCCESS(1, "秒杀成功"),
    END(0, "秒杀结束"),
    REPEAT_KILL(-1, "重复秒杀"),
    INNER_ERROR(-2, "系统异常"),
    DATA_REWTIRE(-3, "数据篡改");

    private int state;
    private String stateInfo;

    SeckillStatEnum(int state, String stateInfo) {
        this.state = state;
        this.stateInfo = stateInfo;
    }

    public int getState() {
        return state;
    }

    public String getStateInfo() {
        return stateInfo;
    }

	// 重定义枚举索引方法,通过 state 索引对应的枚举值 key
    public static SeckillStatEnum stateOf(int index){
        for (SeckillStatEnum state: values()){
            if(state.getState() == index){
                return state;
            }
        }
        return null;
    }
}

(4)DTO 数据传输对象的构建

       DTO 是数据传输对象(Data Transfer Object)的缩写,用于 MVC 设计模式下,充当 Model 的角色(即 JavaBean)。该类定义在根目录下的 dto 文件夹中,如图所示:
在这里插入图片描述

       在刚开始学习的时候,也是有这么一个疑问,我们从数据库中获得的数据已经被封装在了 entity 类中,在页面渲染时,直接使用 entity 对象数据不是简洁明了么,为什么还要专门开一个空间封装数据传输对象?
       但事实上, DTO 的存在并非多此一举。这是由 entity 类自身定义决定的。我们为了操作的方便,通常会在 entity 类中添加一定的数据冗余,而这些冗余在很多时候只是为了处理业务逻辑时能够更加快捷,更多的时候,前端并不需要这些数据,甚至于前端要展示的只是整个数据表的很小一部分数据,如果不加区分的一股脑全部传输给前端,会对数据传输造成很大的负担,而且有数据泄露的风险。
       DTO 则是一种针对的是前端的 UI 需求而进行封装的特定封装类型。通过继承和包含 entity 类,添加上一些特定的 key 键,来实现数据对应读取的效果。在减轻数据传输负担的同时,有效防止了后台数据的泄露。
       在本 Seckill 秒杀系统中,总共定义了三个封装类型: Exposer (秒杀地址暴露接口 DTO)、SeckillExecution (秒杀操作执行后,反馈数据封装 DTO)、SeckillResult (封装 json 格式数据的反馈类型数据)。

  1. Exposer 类的定义:
public class Exposer {
    private  boolean exposed;    // 布尔值 : 是否给予用户秒杀地址
    private String md5;          // 一种加密措施(算法), md5是一种在 Java 中封装好的算法,返回一个唯一的字符串,用以充当验证码的作用
    private long seckillId;
    private long now;            // 系统当前时间(毫秒)
    private long start;          // 秒杀开启时间
    private long end;            // 秒杀结束时间

    public Exposer() {}
    public Exposer(boolean exposed, String md5, long seckillId) {
        this.exposed = exposed;
        this.md5 = md5;
        this.seckillId = seckillId;
    }

    public Exposer(boolean exposed, long seckillId, long now, long start, long end) {
        this.exposed = exposed;
        this.seckillId = seckillId;
        this.now = now;
        this.start = start;
        this.end = end;
    }

    public Exposer(boolean exposed, long seckillId) {
        this.exposed = exposed;
        this.seckillId = seckillId;
    }

    public boolean isExposed() {
        return exposed;
    }

    public void setExposed(boolean exposed) {
        this.exposed = exposed;
    }

    public String getMd5() {
        return md5;
    }

    public void setMd5(String md5) {
        this.md5 = md5;
    }

    public long getSeckillId() {
        return seckillId;
    }

    public void setSeckillId(long seckillId) {
        this.seckillId = seckillId;
    }

    public long getNow() {
        return now;
    }

    public void setNow(long now) {
        this.now = now;
    }

    public long getStart() {
        return start;
    }

    public void setStart(long start) {
        this.start = start;
    }

    public long getEnd() {
        return end;
    }

    public void setEnd(long end) {
        this.end = end;
    }

    @Override
    public String toString() {
        return "Exposer{" +
                "exposed=" + exposed +
                ", md5='" + md5 + '\'' +
                ", seckillId=" + seckillId +
                ", now=" + now +
                ", start=" + start +
                ", end=" + end +
                '}';
    }
}
  1. SeckillExecution 类的定义:
public class SeckillExecution {
    private long seckillId;
    private int state;            // 秒杀状态
    private String stateInfo;     // 秒杀状态描述
    private SucKilled sucKilled;  // 秒杀成功对象

    public SeckillExecution() {}
    public SeckillExecution(long seckillId, SeckillStatEnum seckillStatEnum, SucKilled sucKilled) {
        this.seckillId = seckillId;
        this.state = seckillStatEnum.getState();
        this.stateInfo = seckillStatEnum.getStateInfo();
        this.sucKilled = sucKilled;
    }
    public SeckillExecution(long seckillId, SeckillStatEnum seckillStatEnum) {
        this.seckillId = seckillId;
        this.state = seckillStatEnum.getState();
        this.stateInfo = seckillStatEnum.getStateInfo();
    }

    public long getSeckillId() {
        return seckillId;
    }

    public void setSeckillId(long seckillId) {
        this.seckillId = seckillId;
    }

    public int getState() {
        return state;
    }

    public void setState(int state) {
        this.state = state;
    }

    public String getStateInfo() {
        return stateInfo;
    }

    public void setStateInfo(String stateInfo) {
        this.stateInfo = stateInfo;
    }

    public SucKilled getSucKilled() {
        return sucKilled;
    }

    public void setSucKilled(SucKilled sucKilled) {
        this.sucKilled = sucKilled;
    }

    @Override
    public String toString() {
        return "SeckillExecution{" +
                "seckillId=" + seckillId +
                ", state=" + state +
                ", stateInfo='" + stateInfo + '\'' +
                ", sucKilled=" + sucKilled +
                '}';
    }
}
  1. SeckillResult 类的定义:
// 所有ajax请求返回类型: 封装 json 结果
public class SeckillResult<T> {
    private boolean success;
    private T data;
    private String error;

    public SeckillResult(){}
    public SeckillResult(boolean success, T data) {
        this.success = success;
        this.data = data;
    }

    public SeckillResult(boolean success, String error) {
        this.success = success;
        this.error = error;
    }

    public boolean isSuccess() {
        return success;
    }

    public T getData() {
        return data;
    }

    public String getError() {
        return error;
    }
}

(5)Service 接口的实现

       准备工作做完,就是进行 Service 接口方法的实现。Service 接口实现类存放在 service/impl 路径下。以 “对应接口 + Impl” 的形式进行命名。
       以下是 SeckillServiceImpl 类的定义和实现:

/**
 * Service层实现类,实现Service接口
 * 命名习惯: 接口名 + Impl
 * spring 提供注解类型:
 * --@Component : 代表所有组件,当不知道类属于dao, service,conroller等时使用。统称spring组件实例
 * --@Service : 特指服务层代码
 * --@Controller : 特指控制区代码
 */

// 在 Service 层,的实现类前 @Service 注释必须写,
// 这是告诉 Spring 将该类中的方法导入到 Spring 管理池中,如果不写,Service 接口将找不到实现类,故而报错
@Service
public class SeckillServiceImpl implements SeckillService {
    // 日志的应用
    private Logger logger = LoggerFactory.getLogger(this.getClass());
    // 注入Service依赖
    @Autowired  // 自动注入
    private SeckillDao seckillDao;
    @Autowired
    private SuccessKilledDao successKilledDao;
    private final String slat = "the@SLAT&confusion_md5#slat";  // 用于混淆 md5

    @Override  // 注释,表 实现/重写 父类方法
    public List<SeckillTab> getSeckillList() {
        return seckillDao.queryAll(0, 100);
    }

    @Override
    public SeckillTab getSeckillById(long seckillId) {
        return seckillDao.queryById(seckillId);
    }

    @Override
    public Exposer exportSeckillUrl(long seckillId) {
        SeckillTab seckillTab = seckillDao.queryById(seckillId);
        if(seckillTab == null){
            return new Exposer(false, seckillId);
        }
        Date startTime = seckillTab.getStartTime();
        Date endTime = seckillTab.getEndTime();
        Date nowTime = new Date();
        if(nowTime.getTime() < startTime.getTime()||nowTime.getTime()>endTime.getTime()){
            return new Exposer(false, seckillId, nowTime.getTime(), startTime.getTime(), endTime.getTime());
        }
        // md5 本质: 对任意字符串转换为特定长度的编码 —— 不可逆!!
        String md5 = getMD5(seckillId);
        return new Exposer(true, md5, seckillId);
    }

    // 自定义生成 md5 函数
    private String getMD5(long seckillId){
        String base = seckillId + '@' +slat + '#';
        // DigestUtils.md5DigestAsHex() 用于生成 md5 , 参数为二进制(byte),用 .getBytes()得到字符串二进制
        String md5 = DigestUtils.md5DigestAsHex(base.getBytes());
        return md5;
    }

    @Override
    public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException, RepeatKillException, SeckillCloseException {
        try {
            if(md5 == null || !md5.equals(getMD5(seckillId))){
                throw new SeckillException("seckill info rewrite");
            }else{
                // 执行秒杀操作
                // 操作逻辑: 减库存 + 记录购买行为
                Date nowTime = new Date();
                // 减库存
                int updateCount = seckillDao.reduceNumber(seckillId, nowTime);
                if(updateCount <= 0){
                    // 没有更新记录,秒杀已经结束
                    throw new SeckillCloseException("seckill was closed");
                }else{
                    // 记录购买行为
                    int insertCount = successKilledDao.insertSuccessKilled(seckillId, userPhone);
                    if(insertCount <= 0){  // 重复秒杀,无效,抛出重复秒杀异常
                        throw new RepeatKillException("seckill repeated");
                    }else{  // 秒杀成功,打印秒杀记录
                        SucKilled sucKilled = successKilledDao.queryByIdwithSeckill(seckillId, userPhone);
                        return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, sucKilled);
                    }
                }
            }
        }catch(SeckillCloseException es){
            throw es;  // 抛出“秒杀窗口关闭异常”
        }catch(RepeatKillException er){
            throw er;  // 抛出 “重复秒杀异常”
        }catch (Exception e){
            logger.error(e.getMessage(), e);
            // 将所有编译期异常转化为运行期异常, RuntimeException -> rowback(回滚机制)
            throw new SeckillException("seckill inner error:" + e.getMessage());
        }
    }
}

三、整合 - Spring 框架管理 Service 层

       从配置代码数量的角度看,Service 层的 Spring 管理配置是几个层中配置最简单的一个。同样的,在 resources/spring 路径下,创建 spring-service.xml 文件,进行整合配置。配置如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
						   http://www.springframework.org/schema/beans/spring-beans.xsd
						   http://www.springframework.org/schema/tx
						   http://www.springframework.org/schema/tx/spring-tx.xsd
						   http://www.springframework.org/schema/context
						   http://www.springframework.org/schema/context/spring-context.xsd">
    <!-- 配置并使用spring声明式事务 -->
    <!-- 扫描 service 包下所有使用注解的类型,初始化该类型并放入spring容器中 -->
    <context:component-scan base-package="org.seckill.service" />

    <!-- 配置事务管理器, MyBatis 采用jdbc 事务管理器 -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!-- 注入数据库连接池 -->
        <property name="dataSource" ref="dataSource" />
    </bean>

    <!-- 配置基于注解的声明式事务,默认使用注解的方式来管理事务行为 -->
    <tx:annotation-driven transaction-manager="transactionManager" />
</beans>

       注意:此处需回头检查 Service 实现类前,是否带了 Spring 的@Service注释,只有带了该注释,Spring 才会将该实现类中的方法纳入 Spring 管理池进行管理。否则,将会在调用时,发生报错,提示方法未纳入管理。(接口不需要写@Service注释 )
       至此,Service 层的创建和配置工作就算告一段落了。
**小贴士:**Service 层所用特殊注释标签及含义

注解名用途解释
@Component通用组件代表所有组件,当不知道该 Java 类具体属于 dao, service, conroller 等时使用。统称 spring 组件实例
@Controllerweb 页面控制组件,作用于表现层(spring-mvc 的注解)
@Service服务层控制,作用于业务逻辑层
@Autowired对类成员变量、方法及构造函数进行标注,完成自动装配工作

四、测试 Service 层接口功能

       和 DAO 层实现完毕一样, Service 层的接口方法也要进行测试操作。测试类生成方法同 DAO 接口层。将光标置于接口名上, alt + enter(回车) ,点击 Create Test ,便可在弹出的窗口中快捷建立 test 文件夹内对应的 JUnit 4 测试类。如图所示:
在这里插入图片描述
       选中所有要测试的方法,点击 OK ,自动生成 Test 测试类。
在这里插入图片描述
测试类代码如下:

package org.seckill.service;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.seckill.dto.Exposer;
import org.seckill.dto.SeckillExecution;
import org.seckill.entity.SeckillTab;
import org.seckill.exception.RepeatKillException;
import org.seckill.exception.SeckillCloseException;
import org.seckill.exception.SeckillException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import java.util.List;

// @RunWith(SpringJUnit4ClassRunner.class)  junit 依赖,在junit启动时,加载springIOC容器
@RunWith(SpringJUnit4ClassRunner.class)
// 告诉junit spring 的配置文件,
// 注意:大括号中的字符串配置索引地址要用逗号(",")隔开
// 在 DAO 接口测试时,只用导入 spring-dao.xml 配置,
// 在 Service 接口测试时,不仅要导入 spring-service 配置,还要导入 spring-dao.xml 配置
@ContextConfiguration({
        "classpath:spring/spring-dao.xml",
        "classpath:spring/spring-service.xml"})
public class SeckillServiceTest {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private SeckillService seckillService;

    @Test
    public void getSeckillList() {
        List<SeckillTab> list = seckillService.getSeckillList();
        logger.info("list={}", list);
        // Closing non transactional SqlSession ——> 只读操作,不是在事务控制下
    }

    @Test
    public void getSeckillById() {
        long id = 1000L;
        SeckillTab seckillTab = seckillService.getSeckillById(id);
        logger.info("seckill={}", seckillTab);
    }

    @Test
    public void exportSeckillUrl() {
//        long id = 1000L;
        long id = 1004L;
        Exposer exposer = seckillService.exportSeckillUrl(id);
        logger.info("exposer={}", exposer);
        // 秒杀未开启或已结束(报错): exposer=Exposer{exposed=false, md5='null', seckillId=1000, now=1612421304186, start=1610380800000, end=1610467200000}
        // 秒杀开启: exposer=Exposer{exposed=true, md5='da80b6ac68b1c8e104bf3964c6d6b7e3', seckillId=1004, now=0, start=0, end=0}
    }

    @Test
    public void executeSeckill() {
//        long id = 1000L;
        long id = 1004L;
        long phone = 13568957721L;
        String md5 = "da80b6ac68b1c8e104bf3964c6d6b7e3";
        try {
            SeckillExecution execution = seckillService.executeSeckill(id, phone, md5);
            logger.info("result={}", execution);
            // result=SeckillExecution{seckillId=1004, state=1, stateInfo='秒杀成功', sucKilled=SucKilled [seckillId=1004, userPhone=13568957721, state=0, killTime=Thu Feb 04 15:29:57 CST 2021]}
        } catch (RepeatKillException er) {
            logger.error(er.getMessage());
        } catch (SeckillCloseException es) {
            logger.error(es.getMessage());
        }
    }

    // 测试代码完整逻辑,注意代码的可重复执行性
    @Test  // 整合 exportSeckillUrl() 与 executeSeckill() 测试
    public void testSeckillLogic() {
        long id = 1004L;
        Exposer exposer = seckillService.exportSeckillUrl(id);
        long phone = 13568957769L;
        if(exposer.isExposed()){
            String md5 = exposer.getMd5();
            try {
                SeckillExecution execution = seckillService.executeSeckill(id, phone, md5);
                logger.info("result={}", execution);
                // result=SeckillExecution{seckillId=1004, state=1, stateInfo='秒杀成功', sucKilled=SucKilled [seckillId=1004, userPhone=13568957721, state=0, killTime=Thu Feb 04 15:29:57 CST 2021]}
            } catch (RepeatKillException er) {
                logger.error(er.getMessage());
            } catch (SeckillCloseException es) {
                logger.error(es.getMessage());
            }
        }else{
            // 秒杀未开启
            logger.warn("exposer={}", exposer);
        }
    }
}

       如果测试没有问题,那么恭喜,Service 层的搭建宣告完毕。

报错及解决方法

(1)Service 实现类未纳入 Spring 管理池管理。解决方法就是在 Service 接口的实现类前加@Service注释。报错共有两处,如图所示:

  1. Service 接口导入报错(提示不存在要导入的 bean)
    在这里插入图片描述
  2. 功能报错,(同样是提示 autowired 依赖导入失败)
    在这里插入图片描述
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

揽月泛夜舟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值