JDBC与mysql数据类型的映射&通用crud操作的BaseDAO<T>的简单实现&dbutils工具类的简单使用

1 篇文章 0 订阅

目录

1 JDBC与mysql常用数据类型的对应

1.1 日期类型

1.2  整形

1.3  浮点数与定点数

1.4  文本字符串类型

1.5 二进制

 1.6 json类型

 1.7 总结

2  实现一个BaseDAO

2.1 三层DAO关系

2.2 javabean

2.3 实现BaseDAO

3 bdUtils工具类

3.1 简单说明其常用方法

3.2  ResultSetHandler的探索

3.3  自定义类实现ResultSetHandler<>接口

3.4 改变实体类setXxx与getXxx方法


昨天通过druid连接mysql,使用dbutils工具类实现基本的crud操作,遇见了一个问题,后来发现是我javabean对象的数据类型错了,与数据库对不上号,今天总结一下mysql常用的数据类型在jdbc中的对应,对DAO以及dbUtils的理解再此奉上。

1 JDBC与mysql常用数据类型的对应

1.1 日期类型

        日期与时间是重要的信息,在我们的系统中,几乎所有的数据表都用得到。我们需要知道数据的 时间标签,从而进行数据查询、统计和处理。 在mysql中常用的日期类型有:
  • year            年
  • date            年-月-日
  • time            时:分:秒
  • datetime    年-月-日  时:分:秒
  • timestamp 年-月-日  时:分:秒

 我们现在建一个表test_time

create table test_date(
myYear year,                        # 年
myDate date,                        # 年月日
myTime time,                        # 时分秒
myDateTime datetime,        #    年月日时分秒    范围:1000年到9999年
myTimestamp timestamp        #    年月日时分秒     范围:1970年到2038年
)character set utf8 collate utf8_bin engine innodb;

insert into test_date values('2022','2022-4-25','12:12:12','2022-12-12 11:12:13','2022-12-12 13:12:11');
select * from test_date;

 

 在java中连接数据库,打印这一行的值和类型,类型通过反射获取

Connection conn = JDBCUtilsByDruid.getConnection();
        String sql = "select * from test_date";
        PreparedStatement ps = conn.prepareStatement(sql);
        ResultSet rs = ps.executeQuery();
        ResultSetMetaData rsmd = rs.getMetaData();
        int columnCount = rsmd.getColumnCount();
        if (rs.next()) {
            for (int i = 0; i < columnCount; i++) {
                String columnLabel = rsmd.getColumnLabel(i + 1);//得到标签名
                Object columnValue = rs.getObject(i + 1);//当前字段值
                String typeName = columnValue.getClass().getTypeName();//当前字段在java中的类型
                System.out.println(columnLabel + " :" + columnValue + " ,java类型:" + typeName);
            }
        }

得到运行结果:

 我们在定义对应的字段的类型时,就可以按照上面的结果导包。注意一点,我都是用的getObject(index)接收数据,因为不确定数据类型,但是可以看出值的实际类型不是Object,通过强制转型可以不报错的赋值给他实际类型的引用。

1.2  整形

演示5个整形,加一个无符号整形

类型字节有符号范围无符号范围
tinyint1-128~127(-2^7~2^7-1)0~255(0~2^8-1)
smallint2
-32768~32767
0~65535
mediumint3
-8388608~8388607
0~16777215
int4
2147483648~2147483647
0~4294967295
bigint8-2^63~2^63-10~2^64-1

这里说明一下,1个字节所表示的最大二进制无符号整数:11111111(8个1),对应10进制就是255。

下面建表:

create table test_int(
myTinyint tinyint,
mySmallint smallint,
myMediumint mediumint,
myInt int,
myBigInt bigint,
myUnsignedInt int unsigned,  -- 演示无符号 整数
myIntZeroFill int(5) ,        -- 查看定宽
myIntNotFill int(5) ZEROFILL    -- 查看定宽并填充0
)character set utf8 collate utf8_bin engine innodb;

insert into test_int values(123,-123,1234567,-1234567,1234567890123,6,34,34);
select * from test_int;

 

 

 通过jdbc获取该行的所有数据,并打印

 

 我们可以看到bigint,无符号整数,定宽度并用0填充的是Long,其余全是Integer类型的。

1.3  浮点数与定点数

  • float
  • double
  • numeric(m,n)     浮点数
  • decimal(m,n)     定点数

在官方文档中表名,未来float和double可能被抛弃,所以不建议使用。

create table test_float(
myFloat float,
myDouble double,
myNumeric numeric(5,2),
myDecimal decimal(5,2)
)character set utf8 collate utf8_bin engine innodb;

insert into test_float values(123.4,123.4,123.456,145.65);
select * from test_float;

 

 

 BigDecimal是java.math包提供的一个API类,来对超过16位有效位的数进行精确的运算。双精度浮点型变量double可以处理16位有效数,但在实际应用中,可能需要对更大或者更小的数进行运算和处理。传统的float和double是近似值保存数据,就拿一个经典例子,1.0-0.9打印结果0.09999999999998,而BigDecimal就可以很好的解决这个问题。

如果你要转化为其他类型,BigDecimal提供有方法

  • toString()                 将BigDecimal对象的数值转换成字符串。
  • doubleValue()         将BigDecimal对象中的值以双精度数返回。
  • floatValue()              将BigDecimal对象中的值以单精度数返回。
  • longValue()              将BigDecimal对象中的值以长整数返回。
  • intValue()                  将BigDecimal对象中的值以整数返回。

1.4  文本字符串类型

其实文本字符串类型没啥好说的,就是返回String,四个text就写一个为代表。

 text文本类型,可以存比较大的文本段,搜索速度稍慢,因此如果不是特别大的内容,建议使用 char和 varchar来代替。还有 TEXT 类型不用加默认值,加了也没用。而且 text blob 类型的数据删除后容易导致 “空洞 ,使得文件碎片比较多,所以频繁使用的表不建议包含 TEXT 类型字段,建议单独分出去,单独用 一个表。
然后是枚举类型enumset类型
enum类型字段是要求从给定的值中选一个
set类型是要求从给定的多个值中选一个或多个进行组合,用双(单)引号全部包着,用英文的逗号隔开

create table test_str(
myChar char(32),
myVarchar varchar(33),
myText text,
myEnum enum('男','女','人妖'),
mySet set('看美女','看腿','目不转睛的看')
)character set utf8 collate utf8_bin engine innodb;

insert into test_str values('零','伊蕾娜','白毛','女','看美女,看腿');
select * from test_str;

 

 返回类型全是String。

1.5 二进制

 主要有三个类型

  • binary
  • varbinary
  • bolb

先建表

create table test_binary (
myBinary  binary(32) ,
myVarbinary1  varbinary(32),
myVarbinary2  varbinary(32),
myBolb blob
)character set utf8 collate utf8_bin engine innodb;

再传入一数据,再得到数据,打印 

 Connection conn = JDBCUtilsByDruid.getConnection();
        //向表中插入一个blob类型的数据
        String sql="insert into test_binary values(?,?,?,?)";
        PreparedStatement ps = conn.prepareStatement(sql);
        //填充撒4个占位符
        ps.setObject(1,"伊蕾娜");
        ps.setObject(2,"零".getBytes(StandardCharsets.UTF_8));
        FileInputStream fs1 = new FileInputStream("src/typeConversion/binary.txt");
        FileInputStream fs2 = new FileInputStream("src/typeConversion/零.jpg");
        ps.setObject(3,fs1);
        ps.setObject(4,fs2);
        ps.execute();

        String sql1 = "select * from test_binary";
        ps = conn.prepareStatement(sql1);
        ResultSet rs = ps.executeQuery();
        ResultSetMetaData rsmd = rs.getMetaData();
        int columnCount = rsmd.getColumnCount();
        if (rs.next()) {
            for (int i = 0; i < columnCount; i++) {
                String columnLabel = rsmd.getColumnLabel(i + 1);//得到标签名
                Object columnValue = rs.getObject(i + 1);//当前字段值
                String typeName = columnValue.getClass().getTypeName();//当前字段在java中的类型
                System.out.println(columnLabel + " :" + columnValue + " ,java类型:" + typeName);
            }
        }

 

 好了,差不多都是这些类型。实际中通常不会向数据库中存大数据,因为会加重数据库的负担,通常都是向数据库存文件地址。

 1.6 json类型

json是一种轻量级的 数据交换格式 。简洁和清晰的层次结构使得 json成 为理想的数据交换语言。它易于人阅读和编写,同时也易于机器解析和生成,并有效地提升网络传输效 率。JSON 可以将 JavaScript 对象中表示的一组数据转换为字符串,然后就可以在网络或者程序之间轻 松地传递这个字符串,并在需要的时候将它还原为各编程语言所支持的数据格式。

创建一个json字段

create table test_json(
myJson json
)character set utf8 collate utf8_bin engine innodb;

insert into test_json values('{"零":"从零开始的魔法书","伊蕾娜":"魔女之旅","动漫":{"人活着":"就是为了白毛"}}');

 查询

select myJson -> '$."零"' as '零' ,
             myJson -> '$."伊蕾娜"' as '伊蕾娜',
           myJson -> '$."动漫"."人活着"' as '看动漫'
             from test_json;
 

 

 在jdbc中得到一个String类型的结果

 1.7 总结

类型mysqlJDBC
整形

int

int unsigned

int (L) zerofill

java.langInteger
bigIntjava.lang.Long
浮点型与定点型

            float

java.langFloat
doublejava.langDouble

numeic(m,n)

decimal(m,n)

java.math.Decimal
日期类型

year(yyyy或yy)

date(yyyy-MM-DD)

java.sql.Date
time(HH:mm:ss)java.sql.time
datetime(yyyy-MM-DD HH:mm:ss)java.time.LocalDateTime
timstamp(yyyy-MM-DD HH:mm:ss)java.sql.Timestamp
文本类型

varchar(m)

char(m)

text(L)

java.lang.String
Enum枚举类型enum(a,b,......)java.lang.String
Set类型set(a,b,c.......)java.lang.String
json类型jsonjava.lang.String

2  实现一个BaseDAO

2.1 三层DAO关系

Dao专门负责一些对数据库的访问,然后是业务处理层,用来使用户和数据库交互的中间层,可以对用户的请求做出处理的,最一层就是用户使用的层。

control层负责控制,会有参数传进来,告诉你具体做什么,然后传到service服务层,这层只显示服务的名称,具体操作还是到dao层里执行,其实一层dao就可以解决所有问题,不过三层看起来层次更加清晰。

为什么要写一个BaseDAO?因为当对数据库有大量的crud操作的时候,这些操作都有一个公共的部分代码,如果不写一个基本的通用操作,那么就会造成大量的代码冗余,大大增加开发成本,就好比编写函数是一个道理,说白了就是提高代码复用率。

如果说cotroller就像服务员提交菜单(客户端业务),那么service就是厨师,根据菜单作出对应的菜(具体业务逻辑),而做菜的原料就是厨房打杂(DAO)提供,而BaseDAO就像加工原材料的基础技能,每个打杂(DAO)都只对一种原料(表)进行基本加工。这三层子上而下互不干扰。

2.2 javabean

ORM中:

  • 类  —> 表
  • 属性 —>字段
  • 对象 —> 记录

javabean也就是实体类

特点:

  • 必须为public class
  • 必须提供私有属性,属性名与表中字段一致
  • 提供公共的setXXX和getXXX,XXX为属性名
  • 提供公共的无参构造器

2.3 实现BaseDAO

我们实现一个通用的增删改查,要传入

  • Connection conn        数据库的连接
  • String sql                     执行的sql语句
  • Object...params         传入的要填充占位符的参数,由于类型未知,所以使用Object

 2.3.1 先实现dml(增删改)操作

 public void update(Connection conn, String sql, Object... args) {
        PreparedStatement ps = null;
        try {
            ps = conn.prepareStatement(sql);
            //填充占位符
            for (int i = 0; i < args.length; i++) {
                ps.setObject(i + 1, args[i]);
            }
            ps.execute();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("update出错");
        } finally {
            JDBCUtils.closeResource(null, ps);
        }
    }

为什么捕获了异常还要抛出异常,因为在进行事务的时候,如果抛出异常,在Filter会捕获异常,进行事务回滚,还有catch异常还可以使用finally语句,进行资源的关闭,就是prepareStatement和ResultSet的关闭,这里因为是dml操作,所以没有ResultSet,而连接conn则是在Filter进行关闭。

2.3.2 查询返回单个特殊值

 public Object queryScalar(Connection conn, String sql, Object... args) {
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            ps = conn.prepareStatement(sql);
            for (int i = 0; i < args.length; i++) {
                ps.setObject(i + 1, args[i]);
            }
            rs = ps.executeQuery();
            if (rs.next()) {
                return rs.getObject(1);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            JDBCUtils.closeResource(null, ps, rs);
        }
        return null;
    }

 2.3.3 查询返回一行记录

也就是返回该表对应的实体类的一个实例,将查询的结果封装在该实例中,就是创建一个实例obj,只给与查询结果对应的属性赋值,其余属性就位默认值,然后将该对象返回。怎么确定到底是那个类?传入一个Class对象,利用反射技术创建实例,给属性赋值。

因为我们每次都要手动传入一个Class对象,对于同一张表,我们对这张表的原始增删改查操作都在同一个DAO类  XXXDAOImpl  extend BaseDAO<XXX>  implement  XXXDAO  

所以我们使用泛型

public abstract class oldBaseDAO<T> {

    private Class<?> clazz = null;

    {
        //获取当前子类继承父类中的泛型
        Type genericSuperclass = this.getClass().getGenericSuperclass();
        ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass;
        Type[] arguments = parameterizedType.getActualTypeArguments();
        clazz = (Class<?>) arguments[0];
    }

 在子类XXXDAOImpl被实例时,优先调用父类的普通代码块,这个this就是子类本身,this.getclass()就是子类XXXDAOImpl,getGenericSuperclass就是获取父类的类型,通过强制转型转化为ParameterizedType 类型,调用ParameterizedType 类的方法getActualTypeArguments()获取真实类型的数组,而子类在继承父类的时候XXXDAOImpl  extend BaseDAO<XXX>  implement  XXXDAO     如这里所示就已经指明了具体类型 XXX 该数组的第一个元素就是XXX.class

这样我们就获取到了,但是注意,如果子类继承父类时依旧写的XXXDAOImpl <T> extend BaseDAO<T>  implement  XXXDAO  ,那么获取的类型是T。

这个时候就可以开始写了

  public T querySingle(Connection conn, String sql, Object... args) {
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            ps = conn.prepareStatement(sql);
            //填充占位符
            for (int i = 0; i < args.length; i++) {
                ps.setObject(i + 1, args[i]);
            }
            rs = ps.executeQuery();
            //得到元数据,里面可以得到查询的标签名
            ResultSetMetaData rsmd = rs.getMetaData();
            int columnCount = rsmd.getColumnCount();
            //
            T t = null;
            if (rs.next()) {
                t = (T) clazz.newInstance();
                for (int i = 0; i < columnCount; i++) {
                    Field field = clazz.getDeclaredField(rsmd.getColumnName(i + 1));
                    field.setAccessible(true);
                    field.set(t, rs.getObject(i + 1));
                }
            }
            return t;
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("查询单行出错");
        } finally {
            JDBCUtils.closeResource(null, ps, rs);
        }
    }

 

2.3.4查询返回多行记录

 public List<T> queryMutil(Connection conn, String sql, Object... args) {
        PreparedStatement ps = null;
        ResultSet rs = null;
        List<T> list = new ArrayList<>();
        try {
            ps = conn.prepareStatement(sql);
            for (int i = 0; i < args.length; i++) {
                ps.setObject(i + 1, args[i]);
            }
            rs = ps.executeQuery();
            ResultSetMetaData rsmd = rs.getMetaData();
            int columnCount = rsmd.getColumnCount();

            while (rs.next()) {
                T t = (T) clazz.newInstance();
                for (int i = 0; i < columnCount; i++) {
                    Field field = clazz.getDeclaredField(rsmd.getColumnName(i + 1));
                    field.setAccessible(true);
                    field.set(t, rs.getObject(i + 1));
                }
                list.add(t);
            }
            return list;
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("查询返回多行记录出错");
        } finally {
            JDBCUtils.closeResource(null, ps, rs);
        }
    }

 说明一下,我们这里是通过反射直接获取属性,然后filed.set()方法来设置值的。

3 bdUtils工具类

3.1 简单说明其常用方法

这是个好东西啊,简单说一下吧,用得最多的就那四五个。

我们导入一个包就可以使用,然后new 一个

private QueryRunner qr = new QueryRunner();

QueryRunner  中有很多现成的crud方法使用。

  • qr.update(conn,sql,params)        conn是连接,sql是dml操作语句,params是要传入的占位符填充参数,没有就不写  。返回一个int 类型,表示操作改变的行数
  • qr.query(conn,sql,ResultSetHandler<? extends Object>,params)   
    • 返回单个特殊值        qr.query(conn,sql,new ScalarHandler(),params)
    • 返回一行记录             qr.query(conn,sql,new  BeanHanler<>(clazz) ,params)
    • 返回多行记录              qr.query(conn,sql,new BeanListHandler<>(clazz),params)

3.2  ResultSetHandler的探索

这个工具类底层自动帮我们关闭prepareStatement和ResultSet,我们也不用在DAO层关闭连接,所以直接执行crud操作,给service层提供数据,出错就抛出异常,不需要考虑资源的关闭,这就是DAO层,又叫作数据持久层或者数据连接层。

看看底层源码

 

 query方法的返回值由ResultSetHandler<>接口的实现类的handle(rs)决定。如果我们不重写handler方法,那么就使用他底层提供的几个实现类的handler,这些实现类,比如BeanHanler<>(clazz)返回单行记录,将记录封装在一个实例中。

实例类型就由我们传入的clazz 决定,通过反射调用无参构造器,再根据handler(rs)方法中的

rs属性,也就是ResultSet  rs  ,封装了结果集的对象,我们通过rs.getMetaData()得到元数据,里面有标签名XXX,默认就是对象对应的一个属性名XXX,再赋值。

这里有不同,之前我们是通过反射得到属性,直接设置属性的值。但这里是通过setXXX方法来设置值的,也就是说,底层给属性private double salary设置值newSalary,是通过反射调用setSalary()方法设置值的。方法名setSalary去掉set,剩下的部分Salary首字母小写:salary就是要设置的属性名。有个例外,对于Boolean类型的属性,其set方法是isXXX()。

这里就完美解释了为什么javabean需要满足上面4条要求。 

3.3  自定义类实现ResultSetHandler<>接口

我们写一个表

 再写一个对应的实体类,所有属性的set和get方法全部一键自动生成

public class UserDetail {

    private Integer id;
    private String nickName;
    private Integer age = -2;
    private String sex;
    private Integer bestFriend;

 

我们可以自定义我们的ResultSetHandler<>(),重写hanler方法来自定义返回值

 

Connection conn = JDBCUtilsByDruid.getConnection();
        QueryRunner qr = new QueryRunner();
        String sql = "select * from userDetail where id=?";

        ResultSetHandler<Object> resultSetHandler = new ResultSetHandler<Object>() {
            @Override
            public Object handle(ResultSet resultSet) throws SQLException {
                //这个resultSet就是返回的结果集,我们可以这个结果集自定义我们想要的返回值
                ResultSetMetaData rmds = resultSet.getMetaData();
                if (resultSet.next()) {
                    for (int i = 0; i < rmds.getColumnCount(); i++) {
                        System.out.println(rmds.getColumnLabel(i + 1) + ":" + resultSet.getObject(1 + i).toString());
                    }
                }
                return "这是我自定义的ResultSetHandler";
            }
        };
        Object query = qr.query(conn, sql, resultSetHandler, 2);
        System.out.println(query);
        conn.close();

结果:

传入我们自定义的实现了ResultSetHandler接口,重写了handler方法,该方法返回string类型的固定字符串,而query底层最后返回的就是这个handler方法的返回值。

3.4 改变实体类setXxx与getXxx方法

还是这张表 userDetail不变,bestFriend在表中是 int 类型,是某一个记录的id

现在给出它的javabean类

public class UserDetail {

    private Integer id;
    private String nickName;
    private Integer age = -2;
    private String sex;

    private Integer salary = -1;//无关属性,表中没有salary字段

    //请注意,我把这里setAge的方法体改变了
    public void setAge(Integer salary) {
        this.salary = salary;
    }

    public Integer getAge() {
        return age;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getNickName() {
        return nickName;
    }

    public void setNickName(String nickName) {
        this.nickName = nickName;
    }


    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    @Override
    public String toString() {
        return "UserDetail{" +
                "id=" + id +
                ", nickName='" + nickName + '\'' +
                ", age=" + age +
                ", sex='" + sex + '\'' +
                ", salary=" + salary +
                '}';
    }
}

 请注意,类中没有bestFriend这个属性,但不会报错,找不到bestFriend对应的set方法,就自动舍弃该字段的结果,不封装了。

还有一点,我把setAge方法改变了

public void setAge(Integer salary) {
    this.salary = salary;
}

方法内不给age赋值,给salary赋值,我们运行一下看看结果

结果显示并没有给age赋值,age为默认值-1,而调用setAge方法,方法内给salary赋值了,salary为19,由此我们证实了dbutils底层通过标签名xxx,找到setXxx()方法,再反射传参并调用该方法完成给属性xxx赋值。 执行该方法,封装完成,至于有没有属性被赋值,赋值的属性是不是 xxx 都不关心,底层只管调用setXXX方法并传入参数,且成果无异常执行完方法就ok。

下面我们再改变一下,删除salary,加上一个属性,并设置它的get和set方法.因为我们表中bestFriend字段是int类型,但我在实体类中设置的bestFriend属性为 UserDetail,这是一个我们自己定义的类,按照前面所讲的,我们是不是只改变set方法就行了?

请注意下面我设置的get和set方法,我不但改变了set方法的参数类型为Integer,因为结果集中的bestFriend字段是Integer类型的,所以将参数类型改为Integer。

那为什么将get方法的返回值也设置为Integer类型?我经过反复debug发现,如果不改get的返回值类型与结果集对应的字段类型一样,那么底层找不到该set方法,这个底层同时要求setXxx方法的参数和getXxx的返回值与结果集字段Xxx类型一致,这样才能找到setXxx方法,并传参调用该set方法。所以也就解释了上一步,光改变setAge不改变getAge但对结果没有影响,因为结果集中age和类中属性age的类型都是Integer。

private  UserDetail bestFriend =null;

public void setBestFriend(Integer bestFriend) {
    UserDetail userDetail = new UserDetail();
    userDetail.setId(bestFriend);
    this.bestFriend = userDetail;
}

public Integer getBestFriend() {
    return bestFriend.getBestFriend();
}

打印结果:

 现在终于正确了,bestFriend属性一个UserDetail类型,id属性为表中bestFriend字段的值,这是我们在setBestFriend方法设置的结果。

注意,表中bestFriend字段为int类型,但是我类中的bestFriend属性是我自己定义的类UserDetail,我使用bdutils应该怎么做?答案很简单,之前说过dbutils底层是通过setXXX给属性设置值,调用的是方法!!!那么我只需要改变setBestFriend方法的参数和方法体

这就是我所理解的dbutils。

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

luncker(摆烂版)

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

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

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

打赏作者

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

抵扣说明:

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

余额充值