JDBC学习心得

JDBC是什么?

JDBC是 Java DataBase Connectivity(Java语言连接数据库)

JDBC是SUN公司制定的一套接口(interface)

为什么要制定一套接口?

​因为每一个数据库的实现原理都不一样,每一个数据库产品都有自己独特的实现原理。SUN公司为了简化开发人员对数据库的统一操作,提供了一个Java操作数据库的规范,即JDBC。JDBC接口的实现类由具体数据库厂商提供,开发人员只需要掌握JDBC接口的操作即可。

JDBC编程六步

  1. 第一步:注册驱动
  2. 第二步:获取连接
  3. 第三步:获取数据库操作对象
  4. 第四步:执行SQL语句
  5. 第五步:处理查询结果集
  6. 第六步:释放资源
/*
	JDBC编程六步
*/
import java.sql.*;

public class JDBCTest05 {
    public static void main(String[] args) {
        Connection conn = null;
        Statement st = null;
        ResultSet rs = null;
        try{
            //1、注册驱动
            Class.forName("com.mysql.jdbc.Driver");
            //2、获取连接
            conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/jdbcstudy?setSSL=true","root","root");
            //3、获取数据库操作对象
            st = conn.createStatement();
            //4、执行sql
            String sql = "SELECT * FROM users";
            rs = st.executeQuery(sql);
            //5、处理查询结果集
            // rs.next()方法将结果集指针指向下一位,可以通过while()循环遍历整个结果集
            while(rs.next()) {
                /*
                // getString()方法的特点是:不管数据库中的数据类型是什么,都以String形式取出
                String id = rs.getString(1);
                // JDBC 所有下标从1开始,不是从0开始
                // 可以用“列号”,也可以用“列名”, 建议用”列名“,提高代码健壮性,如:
                String id = rs.getString(1);
                String id = rs.getString("id");	// 建议使用这种方式
                // 注意:列名称不是表中的列名称,是查询结果集中的列名称
                // 若将sql语句改为 sql = "SELECT id AS a, loginName, email FROM diary"; 则列名id也应改为a。
                // String id = rs.getString("a");
                String id = rs.getString("id");
                String loginName = rs.getString("loginName");
                String email = rs.getString("email");
                System.out.println(id + "," + loginName + "," + email);
                 */

                // 除了可以以String类型取出之外,还可以以特定的类型取出。注意:需要一一对应
                /* 用下标取
                int id = rs.getInt(1);
                String loginName = rs.getString(2);
                System.out.println(id + "," + loginName);
                 */
                // 用”列名“取
                int id = rs.getInt("id");
                String loginName = rs.getString("loginName");
                System.out.println(id + "," + loginName);

            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //6、释放资源
            if(rs != null) {
                try {
                    rs.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if(st != null) {
                try {
                    st.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if(conn != null) {
                try {
                    conn.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

DriverManager 驱动管理器

// 通过DriverManager的方式注册驱动
/*
Driver driver = new com.mysql.jdbc.Driver();
DriverManager.registerDriver(driver);
*/

// 通过类加载的方式注册驱动
Class.forName("com.mysql.jdbc.Driver()");	

Connection建立数据库连接

// connection代表数据库
// url与对应的数据库有关,这里用MySQL举例:
// String url = "jdbc:mysql://localhost:3306/jdbcstudy?setSSL=true"
// url公式:"协议://主机地址:端口号/数据库名?参数1&参数2&参数3"
// username为数据库登录用户名,password为数据库登录密码(一般都是root)
Connection conn = DriverManager.getConnection(url, username, password);

// 事务处理机制
// 数据库设置自动提交,传入false表示关闭自动提交,默认值为true
conn.setAutoCommit(boolean);
// 事务提交
conn.commit();
// 事务回滚
conn.rollback();

Statement: 执行SQL的对象

// 创建一个statement对象
Statement statement = conn.createStatement();

String sql = ""; // 编写具体的sql语句

statement.execute(sql);			// 执行任何sql
statement.executeUpdate(sql);	// 执行 增、删、改,返回受影响的行数
statement.executeQuery(sql);	// 执行查询操作,返回一个查询结果集 ResultSet

ResultSet查询结果集:封装了所有的查询结果

// 获得查询结果集
ResultSet resultSet = statement.executeQuery(sql);
// 可以获得指定的数据类型
resultSet.getObject();
resultSet.getString();
resultSet.getInt();
resultSet.getDouble();
resultSet.getFloat();
resultSet.getDate();
//...

// 通过查询结果集的指针遍历整个查询结果
resultSet.next();			// 移动到下一个数据
resultSet.previous();		// 移动到上一个数据
resultSet.beforeFrist();	// 移动到最前面(初始位置)
resultSet.afterLast();		// 移动到最后面
resultSet.absolute(row);	// 移动到指定行

释放资源

为了保证资源一定释放,要在finally语句块(无论是否出现异常都能够执行)中关闭资源,遵循后开先关的原则。

例如:先关闭查询结果集,再关闭数据库操作对象,最后释放数据库连接。


//6、释放资源

// 关闭查询结果集
if(rs != null) {
    try {
        rs.close();
    } catch (SQLException e) {
        e.printStackTrace();
    }
}

// 关闭数据库操作对象
if(st != null) {
    try {
        st.close();
    } catch (SQLException e) {
        e.printStackTrace();
    }
}

// 释放数据库连接
if(conn != null) {
    try {
        conn.close();
    } catch (SQLException e) {
        e.printStackTrace();
    }
}

SQL 注入的问题

SQL注入即是指web应用程序对用户输入数据的合法性没有判断或过滤不严,攻击者可以在web应用程序中事先定义好的查询语句的结尾上添加额外的SQL语句,在管理员不知情的情况下实现非法操作,以此来实现欺骗数据库服务器执行非授权的任意查询,从而进一步得到相应的数据信息。

演示SQL 注入问题

创建一个测试用数据库
CREATE DATABASE jdbcStudy CHARACTER SET utf8 COLLATE utf8_general_ci;
USE jdbcStudy;

CREATE TABLE users(
id INT PRIMARY KEY,
loginName VARCHAR(40),
loginPwd VARCHAR(40),
email VARCHAR(60),
birthday DATE
);

INSERT INTO users(id,loginName,loginPwd,email,birthday)
VALUES(1,'zhangsan','123456','zs@qq.com','1980-12-04'),
(2,'lisi','123456','lisi@qq.com','1981-12-04'),
(3,'wangwu','123456','wangwu@qq.com','1979-12-04');
代码演示

实现功能:
1、需求:模拟用户登录功能的实现
2、业务描述:程序运行的时候,提供一个输入的入口,可以让用户输入用户名和密码用户输入用户名和密码之后,提交信息,java程序收集到用户信息 Java程序连接数据库验证用户名和密码是否合法。合法:显示登陆成功;不合法:显示登陆失败

import java.sql.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;

public class JDBCTest06 {
    public static void main(String[] args) {
        // 初始化一个界面
        Map<String,String> userLoginInfo = InitUI();
        // 验证用户名和密码
        boolean loginSuccess = login(userLoginInfo);
        // 最后输出结果
        System.out.println(loginSuccess ? "登陆成功" : "登陆失败");
    }

    /**
     * 用户登录
     * @param userLoginInfo 用户登录信息
     * @return false表示失败,true表示成功
     */
    private static boolean login(Map<String, String> userLoginInfo) {
        // 打标记
        boolean loginSuccess = false;

        // 单独定义变量
        String loginName = userLoginInfo.get("loginName");
        String loginPwd = userLoginInfo.get("loginPwd");

        // JDBC代码
        Connection conn = null;
        Statement st = null;
        ResultSet rs = null;

        try {
            // 1、注册驱动
            Class.forName("com.mysql.jdbc.Driver");
            // 2、获取连接
            conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/JDBCStudy?useSSL=true","root","root");
            // 3、获取数据库操作对象
            st = conn.createStatement();
            // 4、执行sql
            String sql = "SELECT * FROM users WHERE loginName = '" + loginName + "' AND loginPwd = '" + loginPwd + "'";
            rs = st.executeQuery(sql);
            // 5、处理查询结果集
            if(rs.next()) {
                loginSuccess = true;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 6、释放资源
            if(rs != null) {
                try {
                    rs.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if(st != null) {
                try {
                    st.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if(conn != null) {
                try {
                    conn.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
        return loginSuccess;
    }

    /**
     * 初始化用户界面
     * @return 用户输入的用户名和密码等登录信息
     */
    private static Map<String, String> InitUI() {
        Scanner s = new Scanner(System.in);

        System.out.print("用户名:");
        String loginName = s.nextLine();

        System.out.print("密码:");
        String loginPwd = s.nextLine();

        Map<String,String> userLoginInfo = new HashMap<>();
        userLoginInfo.put("loginName",loginName);
        userLoginInfo.put("loginPwd",loginPwd);

        return userLoginInfo;
    }
}

SQL注入后的登录结果

请添加图片描述

导致SQL注入的根本原因

用户输入的信息中含有sql语句的关键字,并且这些关键字参与sql语句的编译过程,导致sql语句的原意被扭曲,进而达到sql注入。

// 4、执行sql
String sql = "SELECT * FROM users WHERE loginName = '" + loginName + "' AND loginPwd = '" + loginPwd + "'";
// 拼接后的sql语句
// SELECT * FROM users WHERE loginName = 'gdufs' AND loginPwd = 'gdufs' or '1' = '1';
// 以上正好完成了sql语句的拼接,以下代码发送sql语句给DBMS,DBMS进行sql编译。
// 正好能将用户提供的”非法信息“编译进去,导致了原sql语句的含义被扭曲了。

解决SQL注入问题

SQL注入的根本原因是用户输入的信息中含有SQL语句的关键字,并且这些关键字参与sql语句的编译过程,导致SQL语句的原意被扭曲,进而达到SQL注入。因此,只要用户提供的信息不参与SQL语句的编译过程,问题就解决了。PreparedStatement类就能很好的帮助我们完成这个操作。PreparedStatement接口继承了java.sql.Statement,是属于预编译的数据库操作对象,它的原理是预先对sql语句的框架进行编译,然后再给sql语句传“值”。

PreparedStatement类:执行SQL的对象
// SQL语句框架,一个?表示一个占位符,一个? 接受一个“值”。注意:占位符不能用单引号括起来
String sql = "SELECT * FROM t_user WHERE loginName = ? AND loginPwd = ?";

// 程序执行到此处,会发送sql语句框架给DBMS,然后DBMS进行sql语句的预先编译
PreparedStatement ps = conn.prepareStatement(sql);

// 给占位符? 传值(第1个问号下标是1,第2个问号下标是2,JDBC中所有下标从1开始)
ps.setString(1,loginName);
ps.setString(2,loginPwd);

// 传值完毕,执行sql
rs = ps.executeQuery();		// 注意这里不需要再传入sql
代码演示
import java.sql.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;

public class JDBCTest07 {
    public static void main(String[] args) {
        // 初始化一个界面
        Map<String,String> userLoginInfo = InitUI();
        // 验证用户名和密码
        boolean loginSuccess = login(userLoginInfo);
        // 最后输出结果
        System.out.println(loginSuccess ? "登陆成功" : "登陆失败");
    }


    /**
     * 用户登录
     * @param userLoginInfo 用户登录信息
     * @return false表示失败,true表示成功
     */
    private static boolean login(Map<String, String> userLoginInfo) {
        // 打标记
        boolean loginSuccess = false;

        // 单独定义变量
        String loginName = userLoginInfo.get("loginName");
        String loginPwd = userLoginInfo.get("loginPwd");

        // JDBC代码
        Connection conn = null;
        PreparedStatement ps = null;        // 这里使用PreparedStatement(预编译的数据库操作对象)
        ResultSet rs = null;

        try {
            // 1、注册驱动
            Class.forName("com.mysql.jdbc.Driver");
            // 2、获取连接
            conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/bjpowernode?useSSL=true","root","root");
            // 3、获取预编译的数据库操作对象
            // SQL语句框架,一个?表示一个占位符,一个? 接受一个“值”。注意:占位符不能用单引号括起来
            String sql = "SELECT * FROM t_user WHERE loginName = ? AND loginPwd = ?";
            // 程序执行到此处,会发送sql语句框架给DBMS,然后DBMS进行sql语句的预先编译
            ps = conn.prepareStatement(sql);
            // 给占位符? 传值(第1个问号下标是1,第2个问号下标是2,JDBC中所有下标从1开始)
            ps.setString(1,loginName);
            ps.setString(2,loginPwd);
            // 4、执行sql
            rs = ps.executeQuery();		// 不需要再次传入sql
            // 5、处理查询结果集
            if(rs.next()) {
                loginSuccess = true;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 6、释放资源
            if(rs != null) {
                try {
                    rs.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if(ps != null) {
                try {
                    ps.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if(conn != null) {
                try {
                    conn.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
        return loginSuccess;
    }

    /**
     * 初始化用户界面
     * @return 用户输入的用户名和密码等登录信息
     */
    private static Map<String, String> InitUI() {
        Scanner s = new Scanner(System.in);

        System.out.print("用户名:");
        String loginName = s.nextLine();

        System.out.print("密码:");
        String loginPwd = s.nextLine();

        Map<String,String> userLoginInfo = new HashMap<>();
        userLoginInfo.put("loginName",loginName);
        userLoginInfo.put("loginPwd",loginPwd);

        return userLoginInfo;
    }
}

SQL注入失败

请添加图片描述

Statement和PreparedStatement比较
  1. Statement存在SQL注入问题,PreparedStatement解决了SQL注入问题。
  2. Statement是编译一次执行一次,PreparedStatement是编译一次,可执行N次。PreparedStatement效率高一些。
  3. PreparedStatement会在编译阶段做类型的安全检查

综上所诉,PreparedStatement使用较多,只有极少数的情况下需要使用Statement

注:若业务方面要求是需要进行sql语句拼接的,则需要使用Statement

PreparedStatement完成增删改操作
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

/**
 * PreparedStatement 完成 INSERT DELETE UPDATE 即 增、删、改
 */
public class JDBCTest09 {
    public static void main(String[] args) {
        Connection conn = null;
        PreparedStatement ps = null;
        try {
            // 1、注册驱动
            Class.forName("com.mysql.jdbc.Driver");
            // 2、获取连接
            conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/jdbcstudy?useSSL=true","root","root");
            // 3、获取预编译的数据库操作对象
            /*增
            String sql = "INSERT INTO users(loginName,loginPwd,email,birthday) VALUES(?,?,?,?)";
            ps = conn.prepareStatement(sql);
            ps.setString(1,zhaoliu);
            ps.setInt(2,123456);
            ps.setString(3,"zhaoliu@qq.com");
            ps.setString(4,"1982-12-04");
             */

            /*改
            String sql = "UPDATE users SET loginName = ?, email = ? WHERE id = ?";
            ps = conn.prepareStatement(sql);
            ps.setString(1,"sunqi");
            ps.setString(2,"sunqi@qq.com");
            ps.setInt(3,4);
            */

            // 删
            String sql = "DELETE FROM users WHERE id = ?";
            ps = conn.prepareStatement(sql);
            ps.setInt(1,4);

            // 4、执行sql
            int count = ps.executeUpdate();
            System.out.println(count);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if(ps != null) {
                try {
                    ps.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if(conn != null) {
                try {
                    conn.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

JDBC中的事务机制

JDBC中的事务是自动提交的:只要执行任意一条语句,则自动提交一次,这是JDBC默认的事务行为。但是在实际的业务当中,通常都是N条DML语句的共同联合才能完成的,必须保证他们这些DML语句在同一个事务中同时成功或者同时失败。

演示JDBC事务的自动提交机制

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

/**
 *	以下程序验证JDBC的事务是自动提交机制
 */
public class JDBCTest10 {
    public static void main(String[] args) {
        Connection conn = null;
        PreparedStatement ps = null;
        try {
            // 1、注册驱动
            Class.forName("com.mysql.jdbc.Driver");
            // 2、获取连接
            conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/bjpowernode?useSSL=true","root","root");
            // 3、获取预编译的数据库操作对象
            String sql = "UPDATE dept SET dname = ? WHERE deptno = ?";
            ps = conn.prepareStatement(sql);
            ps.setString(1,"x部门");
            ps.setInt(2,30);
            // 4、执行sql
            int count = ps.executeUpdate();     //执行第一条update语句
            System.out.println(count);			// 在此处加断点,可以发现数据库中数据已经更改

            // 若程序在此处出现异常,则可能会出现数据丢失、损坏等情况

            // 重新给占位符传值
            ps.setString(1,"y部门");
            ps.setInt(2,20);

            count += ps.executeUpdate();        //执行第二条update语句
            System.out.println(count);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if(ps != null) {
                try {
                    ps.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if(conn != null) {
                try {
                    conn.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

解决事自动提交机制带来的问题

在jdbcstudy数据库中创建一张测试表
drop table if exists act;
create table act(
    actno bigint,
    balance double(7,2)
);
insert into act(actno,balance) values(111,20000);
insert into act(actno,balance) values(222,0);
commit;
select * from act;
代码演示
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

/**
 *  重点三行代码:
 *      conn.setAutoCommit(false);  关闭自动提交机制,改为手动提交
 *      conn.commit();			   	事务执行完毕,可以提交,手动提交数据
 *      conn.rollback();			如果出现异常,回滚事务,确保数据不丢失
 */

public class JDBCTest11 {
    public static void main(String[] args) {
        Connection conn = null;
        PreparedStatement ps = null;
        try {
            // 1、注册驱动
            Class.forName("com.mysql.jdbc.Driver");
            // 2、获取连接
            conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/jdbc?useSSL=true","root","root");
            
            // 将自动提交机制改为手动提交
            conn.setAutoCommit(false);  // 开启事务

            // 3、获取预编译的数据库操作对象
            String sql = "UPDATE act SET balance = ? WHERE actno = ?";
            ps = conn.prepareStatement(sql);

            // 给?传值
            ps.setDouble(1,10000);
            ps.setInt(2,111);
            int count = ps.executeUpdate();

            // 让代码出异常,中断转账,代码进入catch块,事务回滚,数据库无数据丢失
            /*String s = null;
            s.toString();*/

            // 给?传值
            ps.setDouble(1,10000);
            ps.setInt(2,222);
            count += ps.executeUpdate();

            System.out.println(count == 2 ? "转账成功" : "转账失败");

            //程序能够走到这里说明以上程序没有异常,事务结束,手动提交数据
            conn.commit();  //提交事务

        } catch (Exception e) {
            // 如果出异常,手动回滚事务
            if(conn != null) {
                try {
                    conn.rollback();
                } catch (SQLException throwables) {
                    throwables.printStackTrace();
                }
            }
            e.printStackTrace();
        } finally {
            if(ps != null) {
                try {
                    ps.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
            if(conn != null) {
                try {
                    conn.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

JDBC工具类

为什么要有JDBC工具类?

在我们编写JDBC代码的时候,会出现大量重复编写的代码,例如注册驱动、获取连接、释放资源等。这不符合我们代码复用的原则,因此我们可以把这些重复编写的代码封装成静态方法供我们调用,这就是我们的JDBC工具类(JDBCUtil)。

代码演示

package utils;
import java.sql.*;

/**
 * JDBC工具类,简化JDBC编程
 */

public class JDBCUtil {
    
    private JDBCUtil() {}

    // 静态代码块在类加载时执行,并且只执行一次
    static {
        try {
            // 注册驱动
            Class.forName("com.mysql.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    /**
     * 获取数据库连接对象
     * @return 连接对象
     * @throws SQLException 抛出SQL异常
     */
    public static Connection getConnection() throws SQLException {
        return DriverManager.getConnection("jdbc:mysql://localhost:3306/jdbcstudy?useSSL=true","root","root");
    }

    /**
     * 关闭资源
     * @param conn  连接对象
     * @param ps    数据库操作对象
     * @param rs    结果集
     */
    public static void close(Connection conn, Statement ps, ResultSet rs) {
        if(rs != null) {
            try {
                rs.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if(ps != null) {
            try {
                ps.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if(conn != null) {
            try {
                conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

测试工具类

import utils.JDBCUtil;

import java.sql.*;

/**
 * 程序作用:
 *      1、测试DBUtil是否好用
 *      2、写一个模糊查询
 */
public class JDBCTest12 {
    public static void main(String[] args) {
        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // 调用工具类获取连接
            conn = JDBCUtil.getConnection();
            // 获取预编译的数据库操作对象
            // 错误写法:占位符 ? 不能在单引号里
            /*String sql = "SELECT ename FROM emp WHERE ename LIKE '_?%'";
            ps = conn.prepareStatement(sql);
            ps.setString(1,"A");*/

            // 正确写法,用占位符将单引号内的所有字符的位置占住,传入整个字符串
            String sql = "SELECT ename FROM emp WHERE ename LIKE ?";
            ps = conn.prepareStatement(sql);
            ps.setString(1,"_A%");

            rs = ps.executeQuery();
            while(rs.next()) {
                System.out.println(rs.getString("ename"));
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            // 调用工具类释放资源
            JDBCUtil.close(conn, ps, rs);	// 如果不需要创建查询结果集,可以传入null值
        }
    }
}

请添加图片描述

请添加图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值