Java之JDBC增删查改

本文详细介绍了JDBC的基本概念、体系结构,展示了如何通过JDBC进行数据库连接、PreparedStatement实现CRUD操作,以及ORM编程的应用。涵盖了JDBCUtils工具类和Java与SQL的数据类型转换。
摘要由CSDN通过智能技术生成

前言

JDBC作为Java操作数据库的规范,也是核心基础,以后的框架的原理也是封装了JDBC,利用反射、注解和设计模式来完成简化。

一、JDBC体系结构

  • JDBC接口(API)包括两个层次:
  • 面向应用的API:Java API,抽象接口,供应用程序开发人员使用(连接数据库,执行SQL语句,获得结果)。
  • 面向数据库的API:Java Driver API,供开发商开发数据库驱动程序用。

JDBC是sun公司提供一套用于数据库操作的接口,java程序员只需要面向这套接口编程即可。

不同的数据库厂商,需要针对这套接口,提供不同实现。不同的实现的集合,即为不同数据库的驱动。 ————面向接口编程

Java如何操作数据库如下:
在这里插入图片描述

二、JDBC程序编写步骤

在这里插入图片描述

三、JDBC连接

package com.xhu.java.jdbc;

import com.mysql.jdbc.Driver;
import org.junit.Test;

import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;

/**
 * 请开启服务
 */
public class TestConnection {
    @Test
    public void testConn1() {
        //设置需登录的属性文件
        Properties pro = new Properties();
        pro.setProperty("user", "root");
        pro.setProperty("password", "lls");
        //获取连接
        try (Connection conn = new Driver().connect("jdbc:mysql://localhost:3306/test", pro);) {
            System.out.println(conn);
            if (conn != null)
                conn.close();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
    }

    @Test
    public void testConn2() throws Exception {
        //屏蔽具体的第三方实现,利用反射实现
        //1.获取class对象
        Class driverC = Class.forName("com.mysql.jdbc.Driver");
        //2.设置需登录的属性文件
        Properties pro = new Properties();
        pro.setProperty("user", "root");
        pro.setProperty("password", "lls");
        //3.通过反射拿到构造器
        Constructor dc = driverC.getDeclaredConstructor();
        //4.通过拿到的构造器来实例对象
        java.sql.Driver driver = (Driver) dc.newInstance();
        //5.根据实例的对象来获取连接
        Connection conn = driver.connect("jdbc:mysql://localhost:3306/test", pro);

        System.out.println(conn);
        if (conn != null)
            conn.close();
    }

    @Test
    public void testConn3() throws Exception {
        //使用DriverManager
        //1.获取class对象
        Class driverC = Class.forName("com.mysql.jdbc.Driver");
        //2.设置需登录的属性文件
        Properties pro = new Properties();
        pro.setProperty("user", "root");
        pro.setProperty("password", "lls");
        //3.通过反射拿到构造器
        Constructor dc = driverC.getDeclaredConstructor();
        //4.通过拿到的构造器来实例对象
        java.sql.Driver driver = (Driver) dc.newInstance();
        //5.注册driver
        DriverManager.registerDriver(driver);
        //6.用注册的manager来获取连接
        Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", pro);
        System.out.println(conn);
        conn.close();
    }

    @Test
    public void testConn4() throws Exception {
        //使用DriverManager,MySQL提供静态代码块来注册了driver
        //1.加载类,此时JVM加载类、链接、初始化,在初始化时,静态代码块和静态赋值语句一起合并到clinit()方法中,并执行。
        /*static {
            try {
                DriverManager.registerDriver(new Driver());
            } catch (SQLException var1) {
                throw new RuntimeException("Can't register driver!");
            }
        }*/
        Class.forName("com.mysql.jdbc.Driver");
        //2.设置需登录的属性文件
        Properties pro = new Properties();
        pro.setProperty("user", "root");
        pro.setProperty("password", "lls");
        //3.用注册的manager来获取连接
        Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", pro);
        System.out.println(conn);
        conn.close();
    }

    @Test
    public void testConn5() throws Exception {
        //要变化的不写入代码,而是让代码去读配置文件.
        //1)这样不用重新编译Java文件,只需改改配置文件。2)代码与数据分离,耦合性低(非硬编码)。
        //1.对目标文件创建节点流
        InputStream is = this.getClass().getClassLoader().getResourceAsStream("jdbc.properties");
        //2.用Properties将文件读入
        Properties pro = new Properties();
        pro.load(is);
        is.close();
        //3.获取properties里value
        String driverClass = pro.getProperty("driverClass");
        String url = pro.getProperty("url");
        String user = pro.getProperty("user");
        String password = pro.getProperty("password");
        //4.获取Class对象,用于反射操作。
        Class driverC = Class.forName(driverClass);
        //5.用注册的manager来获取连接
        Connection conn = DriverManager.getConnection(url, user, password);
        System.out.println(conn);
        conn.close();
    }
}

四、PreparedStatement实现CRUD操作

1、操作和访问数据库

  • 数据库连接被用于向数据库服务器发送命令和 SQL 语句,并接受数据库服务器返回的结果。其实一个数据库连接就是一个Socket连接。

  • 在 java.sql 包中有 3 个接口分别定义了对数据库的调用的不同方式:

    • Statement:用于执行静态 SQL 语句并返回它所生成结果的对象,会出现SQL注入问题。
    • PrepatedStatement:SQL 语句被预编译并存储在此对象中,可以使用此对象多次高效地执行该语句。
    • CallableStatement:用于执行 SQL 存储过程
      在这里插入图片描述

2、CRUD

package com.xhu.java.jdbc;

import org.junit.Test;

import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.*;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;

public class TestCRUD {
    @Test
    public void test_JDBCUtils() {
        //别名要对应POJO类中的属性名,才可以用反射注入数据。
        String sql = "select order_id orderId,order_name orderName,order_date orderDate from `order`";
        List<Order> orders = JDBCUtils.getMultiInstance(Order.class, sql);
        orders.forEach(System.out::println);
    }

    @Test
    public void test_getMultiInstance() throws Exception {
        String sql = "select order_id orderId,order_name orderName,order_date orderDate from `order`";
        List<Order> orders = getMultiInstance(Order.class, sql);
        System.out.println(Arrays.toString(orders.toArray()));
        orders.forEach(System.out::println);
    }

    /**
     * @param c
     * @param sql
     * @param args
     * @param <T>
     * @return
     * @throws Exception
     * @function 获取多条记录
     */
    public <T> List<T> getMultiInstance(Class<T> c, String sql, Object... args) throws Exception {
        //1.获取连接
        Connection conn = getConn();
        //2.获取预处理SQL对象
        PreparedStatement ps = conn.prepareStatement(sql);
        //3.占位符赋值
        for (int i = 0; i < args.length; i++) {
            ps.setObject(i + 1, args[i]);
        }
        //4.执行SQL
        ResultSet res = ps.executeQuery();
        //5.处理结果集
        /**
         * @Reflect
         * res将查询出数据的字段数据即元数据信息封装在Meta-Data中
         * 获取元数据,再获取查询的列数以及字段名
         * 通过res获取字段的对应值
         * 通过反射+字段名把对应值动态注入进去
         */
        List<T> resList = new ArrayList<>();
        while (res.next()) {
            ResultSetMetaData metaData = res.getMetaData();
            int cCount = metaData.getColumnCount();
            T t = c.getConstructor(null).newInstance();
            //反射注入需要的class对象
            Class cusC = t.getClass();
            for (int i = 0; i < cCount; i++) {
                //获取字段名
                String columnName = metaData.getColumnLabel(i + 1);//获取别名对应POJO类,如果没有别名就是原名
                Object columnValue = res.getObject(i + 1);
                //反射实现动态注入数据
                //获取任意权限的字段
                Field field = cusC.getDeclaredField(columnName);
                //获取private的权限
                field.setAccessible(true);
                //设置属性值
                field.set(t, columnValue);
            }
            resList.add(t);
        }

        //6.关闭资源
        closeQuery(ps, conn, res);
        //7.返回查询结果
        return resList;
    }

    @Test
    public void test_getInstance() throws Exception {
        String sql = "select id,name,email,birth from customers where id = ?";
        System.out.println(getInstance(Customer.class, sql, 1));
    }

    /**
     * @param c
     * @param sql
     * @param args
     * @param <T>
     * @return T
     * @throws Exception
     * @function 各种类的通用查询
     */
    public <T> T getInstance(Class<T> c, String sql, Object... args) throws Exception {
        //1.获取连接
        Connection conn = getConn();
        //2.获取预处理SQL对象
        PreparedStatement ps = conn.prepareStatement(sql);
        //3.占位符赋值
        for (int i = 0; i < args.length; i++) {
            ps.setObject(i + 1, args[i]);
        }
        //4.执行SQL
        ResultSet res = ps.executeQuery();
        //5.处理结果集
        /**
         * @Reflect
         * res将查询出数据的字段数据即元数据信息封装在Meta-Data中
         * 获取元数据,再获取查询的列数以及字段名
         * 通过res获取字段的对应值
         * 通过反射+字段名把对应值动态注入进去
         */
        T t = null;
        if (res.next()) {
            ResultSetMetaData metaData = res.getMetaData();
            int cCount = metaData.getColumnCount();
            t = c.getConstructor(null).newInstance();
            //反射注入需要的class对象
            Class cusC = t.getClass();
            for (int i = 0; i < cCount; i++) {
                //获取字段名
                String columnName = metaData.getColumnLabel(i + 1);//获取别名对应POJO类,如果没有别名就是原名
                Object columnValue = res.getObject(i + 1);
                //反射实现动态注入数据
                String setMethodName = "set" + (char) (columnName.charAt(0) - 32) + columnName.substring(1);
                String getMethodName = "get" + (char) (columnName.charAt(0) - 32) + columnName.substring(1);
                Method method = cusC.getDeclaredMethod(setMethodName, cusC.getDeclaredMethod(getMethodName, null).getReturnType());
                method.invoke(t, columnValue);
                /**
                 * 方式二
                 * //获取任意权限的字段
                 * Field field = cusC.getDeclaredField(columnName);
                 * //获取private的权限
                 * field.setAccessible(true);
                 * //设置属性值
                 * field.set(cus, columnName);
                 */

            }
        }
        //6.关闭资源
        closeQuery(ps, conn, res);
        //7.返回查询结果
        return t;
    }

    @Test
    public void selectBySQL() throws Exception {
        //根据id来查
        String sql = "select id,name,email,birth from customers where id = ?";
        Customer customer = selectCommon(sql, 1);
        System.out.println(customer);
        //根据name来查
        String sql2 = "select id,name,email,birth from customers where name = ?";
        customer = selectCommon(sql2, "周杰伦");
        System.out.println(customer);
    }

    /**
     * @param sql
     * @param args
     * @return Customer类
     * @throws Exception
     * @function 一个类的通用操作
     */
    public Customer selectCommon(String sql, Object... args) throws Exception {
        //1.获取连接
        Connection conn = getConn();
        //2.获取预处理SQL对象
        PreparedStatement ps = conn.prepareStatement(sql);
        //3.占位符赋值
        for (int i = 0; i < args.length; i++) {
            ps.setObject(i + 1, args[i]);
        }
        //4.执行SQL
        ResultSet res = ps.executeQuery();
        //5.处理结果集
        /**
         * @Reflect
         * res将查询出数据的字段数据即元数据信息封装在Meta-Data中
         * 获取元数据,再获取查询的列数以及字段名
         * 通过res获取字段的对应值
         * 通过反射+字段名把对应值动态注入进去
         */
        Customer cus = null;
        if (res.next()) {
            System.out.println("有结果");
            ResultSetMetaData metaData = res.getMetaData();
            int cCount = metaData.getColumnCount();
            cus = new Customer();
            //反射注入需要的class对象
            Class cusC = cus.getClass();
            for (int i = 0; i < cCount; i++) {
                //获取字段名
                String columnName = metaData.getColumnLabel(i + 1);//获取别名对应POJO类,如果没有别名就是原名
                Object columnValue = res.getObject(i + 1);
                //反射实现动态注入数据
                String setMethodName = "set" + (char) (columnName.charAt(0) - 32) + columnName.substring(1);
                String getMethodName = "get" + (char) (columnName.charAt(0) - 32) + columnName.substring(1);
                Method method = cusC.getDeclaredMethod(setMethodName, cusC.getDeclaredMethod(getMethodName, null).getReturnType());
                method.invoke(cus, columnValue);
                /**
                 * 方式二
                 * //获取任意权限的字段
                 * Field field = cusC.getDeclaredField(columnName);
                 * //获取private的权限
                 * field.setAccessible(true);
                 * //设置属性值
                 * field.set(cus, columnName);
                 */

            }
        }
        //6.关闭资源
        closeQuery(ps, conn, res);
        //7.返回查询结果
        return cus;
    }

    @Test
    public void select() throws Exception {
        //1.获取连接
        Connection conn = getConn();
        //2.获取预编译处理SQL的对象
        String sql = "select * from customers where id = ?";
        PreparedStatement ps = conn.prepareStatement(sql);
        //3.赋值
        ps.setObject(1, 1);
        //4.执行得到查询的结果
        ResultSet res = ps.executeQuery();
        //5.处理结果
        /**
         * ResultSet对象的next方法作用:判断是否有下一个值,如果有就将指针下移并返回true,否则返回false。
         * Iterator对象的hasNext方法作用,只单纯判断是否有下一个,有则返回true,否则返回false。next方法作用:将指针下移并获取元素。
         */
        Customer cus = null;
        if (res.next()) {
            int id = res.getInt(1);
            String name = res.getString(2);
            String email = res.getString(3);
            Date birth = res.getDate(4);
            cus = new Customer(id, name, email, birth);
        }
        System.out.println(cus);
        //6.关闭资源
        closeQuery(ps, conn, res);
    }

    /**
     * @param ps
     * @param conn
     * @param res
     * @function 关闭连接、预处理SQL、结果集获取的三个流资源
     */
    private void closeQuery(PreparedStatement ps, Connection conn, ResultSet res) {
        close(ps, conn);
        try {
            if (res != null) res.close();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
    }

    @Test
    public void insert() {
        //1.获取连接
        Connection conn = null;
        PreparedStatement ps = null;
        try {
            conn = getConn();
            //2.通过连接+sql得到一个把SQL语句预编译的操作SQL对象PreparedStatement
            String sql = "insert into customers(name,email,birth) values(?,?,?)";//?为占位符
            ps = conn.prepareStatement(sql);
            //3.为预编译的SQL中占位符设置数值
            ps.setString(1, "lls");
            ps.setString(2, "lls@qq.com");
            ps.setDate(3, new Date(new SimpleDateFormat("yyyy-MM-dd").parse("1998-8-8").getTime()));
            //4.执行SQL
            ps.execute();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //5.关闭连接
            close(ps, conn);
        }
    }

    @Test
    public void update() throws Exception {
        //1.获取数据库连接
        Connection conn = getConn();
        //2.拿到操作SQL的对象
        String sql = "update customers set name=? where id = ?";
        PreparedStatement ps = conn.prepareStatement(sql);
        //3.为占位符赋值
        ps.setString(1, "LLS");
        ps.setInt(2, 19);
        //4.执行SQL
        ps.execute();
        //5.关闭资源
        close(ps, conn);
    }

    private Connection getConn() throws Exception {
        //要变化的不写入代码,而是让代码去读配置文件.
        //1)这样不用重新编译Java文件,只需改改配置文件。2)代码与数据分离,耦合性低(非硬编码)。
        //1.对目标文件创建节点流
        InputStream is = this.getClass().getClassLoader().getResourceAsStream("jdbc.properties");
        //2.用Properties将文件读入
        Properties pro = new Properties();
        pro.load(is);
        is.close();
        //3.获取properties里value
        String driverClass = pro.getProperty("driverClass");
        String url = pro.getProperty("url");
        String user = pro.getProperty("user");
        String password = pro.getProperty("password");
        //4.获取Class对象,用于反射操作。
        Class driverC = Class.forName(driverClass);
        //5.用注册的manager来获取连接
        Connection conn = DriverManager.getConnection(url, user, password);
        return conn;
    }

    private void close(PreparedStatement ps, Connection conn) {
        try {
            if (ps != null) ps.close();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
        try {
            if (conn != null) conn.close();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
    }

    @Test
    public void testUpdate() {
        Object[] args = new Object[]{"lls", 19};
        String sql = "update customers set name=? where id = ?";
        JDBCUtils.update(sql, args);
    }
}

3、JDBCUtils

package com.xhu.java.jdbc;

import java.io.InputStream;
import java.lang.reflect.Field;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

/**
 * @author lls
 * @version 1.0
 * @describle 数据库操作工具类
 * @date 2021-11-27
 * @method connection, insert, ...
 */
/**
 * 使用PreparedStatement有什么好处?
 * 1.SQL注入问题,在获取该对象时,就传入SQL语句进行逻辑的预编译,再注入数据时就不会改变逻辑,从而防止SQL注入问题。
 * 2.批量插入数据效率问题,批量插入数据时,该对象在预编译时对SQL已经做了语法校验,再用该对象执行等价SQL就不会校验了,而普通的Statement没执行一条SQL都要重新编译并校验一次语法。
 * 3.向数据库写二进制问题,该对象可以通过对占位符赋值IO流来写入二进制文件。
 */
/*
 * ORM编程思想  (object relational mapping)
 * 一个数据表对应一个java类
 * 表中的一条记录对应java类的一个对象
 * 表中的一个字段对应java类的一个属性
 *
 */
public class JDBCUtils {
    /**
     * @param c
     * @param sql
     * @param args
     * @param <T>
     * @return
     * @throws Exception
     * @function 获取多条记录, 通用与所有类型
     */
    public static <T> List<T> getMultiInstance(Class<T> c, String sql, Object... args) {
        //1.获取连接
        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet res = null;
        //返回结果
        List<T> resList = new ArrayList<>();
        try {
            conn = getConn();
            //2.获取预处理SQL对象
            ps = conn.prepareStatement(sql);
            //3.占位符赋值
            for (int i = 0; i < args.length; i++) {
                ps.setObject(i + 1, args[i]);
            }
            //4.执行SQL
            res = ps.executeQuery();
            //5.处理结果集
            /**
             * @Reflect
             * res将查询出数据的字段数据即元数据信息封装在Meta-Data中
             * 获取元数据,再获取查询的列数以及字段名
             * 通过res获取字段的对应值
             * 通过反射+字段名把对应值动态注入进去
             */

            while (res.next()) {
                ResultSetMetaData metaData = res.getMetaData();
                int cCount = metaData.getColumnCount();
                T t = c.getConstructor(null).newInstance();
                //反射注入需要的class对象
                Class cusC = t.getClass();
                for (int i = 0; i < cCount; i++) {
                    //获取字段名
                    String columnName = metaData.getColumnLabel(i + 1);//获取别名对应POJO类,如果没有别名就是原名
                    Object columnValue = res.getObject(i + 1);
                    //反射实现动态注入数据
                    //获取任意权限的字段
                    Field field = cusC.getDeclaredField(columnName);
                    //获取private的权限
                    field.setAccessible(true);
                    //设置属性值
                    field.set(t, columnValue);
                }
                resList.add(t);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        //6.关闭资源
        closeQuery(ps, conn, res);
        //7.返回查询结果
        return resList;
    }

    /**
     * @param sql  要执行的SQL语句
     * @param args SQL中要填充的参数
     * @function 增删改统一操作
     */
    public static void update(String sql, Object... args) {
        //1.获取连接
        Connection conn = null;
        PreparedStatement ps = null;
        try {
            conn = getConn();
            //2.通过连接+sql得到一个把SQL语句预编译的操作SQL对象PreparedStatement
            ps = conn.prepareStatement(sql);
            //3.为预编译的SQL中占位符设置数值
            for (int i = 0; i < args.length; i++) {
                ps.setObject(i + 1, args[i]);
            }
            //4.执行SQL
            ps.execute();
        } catch (Exception e) {
            System.out.println("插入异常");
            e.printStackTrace();
        } finally {
            //5.关闭连接
            close(ps, conn);
        }
    }

    /**
     * @param c 一个对象实例,对应数据库一条记录
     * @function 插入数据
     */
    public static void insert(Customer c) {
        //1.获取连接
        Connection conn = null;
        PreparedStatement ps = null;
        try {
            conn = getConn();
            //2.通过连接+sql得到一个把SQL语句预编译的操作SQL对象PreparedStatement
            String sql = "insert into customers (`name`,email,birth) values(?,?,?)";//?为占位符
            ps = conn.prepareStatement(sql);
            //3.为预编译的SQL中占位符设置数值
            ps.setString(1, c.getName());
            ps.setString(2, c.getEmail());
            ps.setDate(3, c.getBirth());
            //4.执行SQL
            ps.execute();
        } catch (Exception e) {
            System.out.println("插入异常");
            e.printStackTrace();
        } finally {
            //5.关闭连接
            close(ps, conn);
        }
    }

    /**
     * @return 数据库连接
     * @throws Exception
     * @function 获取数据库连接
     */
    private static Connection getConn() throws Exception {
        //要变化的不写入代码,而是让代码去读配置文件.
        //1)这样不用重新编译Java文件,只需改改配置文件。2)代码与数据分离,耦合性低(非硬编码)。
        //1.对目标文件创建节点流
        InputStream is = JDBCUtils.class.getClassLoader().getResourceAsStream("jdbc.properties");
        //2.用Properties将文件读入
        Properties pro = new Properties();
        pro.load(is);
        is.close();
        //3.获取properties里value
        String driverClass = pro.getProperty("driverClass");
        String url = pro.getProperty("url");
        String user = pro.getProperty("user");
        String password = pro.getProperty("password");
        //4.获取Class对象,用于反射操作。
        Class driverC = Class.forName(driverClass);
        //5.用注册的manager来获取连接
        Connection conn = DriverManager.getConnection(url, user, password);
        return conn;
    }

    /**
     * @param ps
     * @param conn
     */
    private static void close(PreparedStatement ps, Connection conn) {
        try {
            if (ps != null) ps.close();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
        try {
            if (conn != null) conn.close();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
    }

    /**
     * @param ps
     * @param conn
     * @param res
     * @function 关闭连接、预处理SQL、结果集获取的三个流资源
     */
    private static void closeQuery(PreparedStatement ps, Connection conn, ResultSet res) {
        close(ps, conn);
        try {
            if (res != null) res.close();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
    }
}

4、Java与SQL对应数据类型转换表

Java类型SQL类型
booleanBIT
byteTINYINT
shortSMALLINT
intINTEGER
longBIGINT
StringCHAR,VARCHAR,LONGVARCHAR
byte arrayBINARY , VAR BINARY
java.sql.DateDATE
java.sql.TimeTIME
java.sql.TimestampTIMESTAMP

5、JDBC API小结

  • 两种思想

    • 面向接口编程的思想

    • ORM思想(object relational mapping)

      • 一个数据表对应一个java类
      • 表中的一条记录对应java类的一个对象
      • 表中的一个字段对应java类的一个属性

    sql是需要结合列名和表的属性名来写。注意起别名。

  • 两种技术

    • JDBC结果集的元数据:ResultSetMetaData
      • 获取列数:getColumnCount()
      • 获取列的别名:getColumnLabel()
    • 通过反射,创建指定类的对象,获取指定的属性并赋值

总结

1)JDBC简介和连接。
2)JDBC的通用CRUD。

参考文献

[1] JDBC 尚硅谷

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值