Mybatis(11月17日)

概述

MyBatis 是一款优秀的持久层框架,用于简化 JDBC 开发

框架:

框架就是一个半成品软件,是一套可重用的、通用的、软件基础代码模型
在框架的基础之上构建软件编写更加高效、规范、通用、可扩展

JDBC 的缺点

硬编码

  1. 注册驱动,获取链接
  2. SQL 语句

操作繁琐

  1. 手动设置参数
  2. 手动封装结果集

mybatis 配置信息

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!-- 链接数据库只需要一个属性来配置文件,就会自动解析 -->
    <properties resource="druid.properties"/>
    <!--  设置别名,然后就可以使用别名来代替前面的路径 -->
    <typeAliases>
        <typeAlias type="com.domain.User" alias="User"/>
    </typeAliases>
    
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="${driver}"/>
                <property name="url" value="${url}"/>
                <property name="username" value="${username}"/>
                <property name="password" value="${password}"/>
            </dataSource>
        </environment>
    </environments>
    <!-- 映射文件,sql映射 -->
    <mappers>
        <mapper resource="mapper/User.xml"/>
    </mappers>
</configuration>

SQL 映射文件

  • parameterType 包装成什么类型
  • namespace 接口的位置
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--
    namespace 是用于调用的时候的映射,对应绑定的接口类
 -->
<mapper namespace="com.dao.klo">
    <insert id="add" parameterType="User">
        <!-- 这里的#username 就等于是用 ? 的方式,等方法调用的时候,会传递一个参数,就会自动映射到username的属性上 -->
        insert into user (id,username,password) values (#{id},#{username},#{password})
    </insert>
</mapper>

快速入门

加载配置(Resources . getResourceAsStream)

// 加载 mybatis 核心配置文件
InputStream asStream = Resources.getResourceAsStream("mybatis-config.xml");

获取工厂

会话工厂,根据配置文件创建工厂
作用:创建SqlSession

// 加载 mybatis 核心配置文件
InputStream asStream = Resources.getResourceAsStream("mybatis-config.xml");
// 获取 SqlSessionFactory
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(asStream);

获取执行语句对象(openSession)

会话,是一个接口,面向用户(程序员)的接口
作用:操作数据库(发出sql增、删、改、查)

sqlSession 需要释放资源

// 加载 mybatis 核心配置文件
InputStream asStream = Resources.getResourceAsStream("mybatis-config.xml");
// 获取 SqlSessionFactory
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(asStream);
// 获取执行语句对象
SqlSession sqlSession = factory.openSession();

mapper 代理开发

定义与SQL映射文件同名的Mapper接口,并且将Mapper接口和SQL映射文件放置在同一目录下。
注意:resources资源目录和 java 目录编译后在同一级

在 Mapper 接口中定义方法,方法名就是SQL映射文件中sql语句的id,并保持参数类型和返回值类型一致

执行 sql (getMapper)

传入接口类,然后调用里面的方法

// 加载 mybatis 核心配置文件
InputStream asStream = Resources.getResourceAsStream("mybatis-config.xml");
// 获取 SqlSessionFactory
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(asStream);
// 获取执行语句对象
SqlSession sqlSession = factory.openSession();
// 执行 sql
List<com.domain.User> users = sqlSession.getMapper(User.class).selectAll();

提交

sqlSession.commit();

mapper 代理包扫描

不需要指定路径了就

    <mappers>
<!--        <mapper resource="com/mapper/User.xml"/>-->
<!--        可以使用包扫描的方式-->
        <package name="com.mapper"/>
    </mappers>

细节:数据库字段名和 javaBean属性不匹配

  1. 给数据库字段起别名(缺点:每一个都要写别名)
  2. 使用 sql 片段(缺点:限制了字段个数)
  3. 使用 resultMap (常用)

数据库里user_name 和 JavaBean类 属性username 对应不上,获取的是 null

<mapper namespace="com.example.mappers.brandMappers">
    <select id="findAll" resultType="com.example.beans.User">
        select * from tb_brand;
    </select>
</mapper>

使用 resultMap 解决

  • id 主键的方式
  • result 普通字段
  • column 数据库字段
  • property JavaBean属性
  • resultMap 替换 resultType
<mapper namespace="com.example.mappers.brandMappers">
    <!--  id 表示给这个 resultMap 起的名字  type 表示对应的那个 JavaBean 类  -->
    <resultMap id="brandMap" type="com.example.beans.Brand">
        <!--   id 表示主键     -->
        <id column="id" property="id"/>
        <!--   result 表示普通字段     -->
        <result column="brand_name" property="brandName"/>
        <result column="company_name" property="companyName"/>
    </resultMap>
    <!--  记得把  resultType 换成 resultMap 并且设置名称 -->
    <select id="findAll" resultMap="brandMap">
        select *
        from tb_brand;
    </select>
</mapper>

参数占位符

#号

会替换掉 ?号,为了防止sql注入
参数传递时用 #

  • 参数:parameterType 可省略不写

特殊符号

CD = 这样就可以写特殊符号了

<![CDATA[
        
        ]]>

$号

拼接 sql
动态查询一张表,表名不固定,可以使用
但是 sql 注入一定会存在

参数接收

  1. 手动分配 @Param
    @Param(“status”) int status 指定后面这个 status 在 sql 语句中叫 status
  2. 封装成对象(名字要一一对应)

模糊查询 like

<select id="selectByCondition" resultMap="brandMap">
    select *
    from tb_brand
    where status = #{status}
      and company_name like #{companyName}
      and brand_name like #{brandName}
</select>

第一种:参数形式

接口

List<Brand> selectByCondition(@Param("status") int status, @Param("companyName") String companyName,
                              @Param("brandName") String brandName);

实现

// 获取执行
SqlSession sqlSession = build.openSession();
// 处理参数,因为 模糊查询需要匹配%
int status = 1;
String companyName = "华为";
String brandName = "华为";
companyName = "%" + companyName + "%";
brandName = "%" + brandName + "%";
// 执行 sql 
List<Brand> brands = sqlSession.getMapper(brandMappers.class).selectByCondition(status, companyName, brandName);
System.out.println(brands);
sqlSession.close();

第二种:封装成对象传入

接口

List<Brand> selectByCondition(Brand brand);

实现

// 获取执行
SqlSession sqlSession = build.openSession();
// 处理参数,因为 模糊查询需要匹配%
int status = 1;
String companyName = "华为";
String brandName = "华为";
companyName = "%" + companyName + "%";
brandName = "%" + brandName + "%";
// 封装对象
Brand brand = new Brand(status, companyName, brandName);
// 执行 sql
List<Brand> brands = sqlSession.getMapper(brandMappers.class).selectByCondition(brand);
System.out.println(brands);
sqlSession.close();

第三种:map 集合也可以,保证键名和 sql 字段一样就可以

多个参数

如果没有进行 @Param 注解,那么会进行 map 封装

  • 两种形式:依次进行存放。键 = 值形式
    • 形式1:map . put( arg0, 参数1 )
    • 形式1:map . put( param1, 参数值1 )
      建议使用 @Param 注解

动态 sql

if

test 后面 跟表达式

  • 利用where标签代替where
  • 去除了第一个 and
  • 如果都为空,去除本身 where
<select id="selectByCondition" resultMap="brandMap">
    select *
    from tb_brand
    <where>
        <if test="status != null">
            status = #{status}
        </if>
        <if test="companyName != null">
            and company_name like #{companyName}
        </if>
        <if test="brandName != null">
            and brand_name like #{brandName}
        </if>
    </where>
</select>

choose

<select id="selectBySingle" resultMap="brandMap">
    select *
    from tb_brand
    where
    <choose># 相当于 switch
        <when test="status != null"> # 相当于 case
            status = #{status}
        </when>
        <when test="companyName != null and companyName != ''"># 相当于 case
            company_name like #{companyName}
        </when>
        <when test="brandName != null and brandName != ''"># 相当于 case
            brand_name like #{brandName}
        </when>
        <otherwise>
            1 = 1
        </otherwise>
    </choose>
</select>

if forEach

Where

forEach

  • collection 要遍历哪一个数组 这里面的值 数组 = array 集合 list。可以使用别名

批量添加

<insert id="addArray">
    insert into user(username, password) values
    <foreach collection="list" item="user" separator=",">
        (#{user.username},#{user.password})
    </foreach>
</insert>

批量删除

<delete id="deleteArray">
    delete from user where id in
    <foreach collection="list" item="item" separator="," open="(" close=")">
        ${item}
    </foreach>
</delete>

map 遍历查询

  1. collection 一定要指定
  2. index 是key
  3. key必须使用$符号,占用
<select id="selectAll" resultType="com.example.domin.IomBaseDataNe">
    select * from iom_basedata_ne
    <where>
        <foreach collection="map" index="key" item="value" separator="and">
            ${key} = #{value}
        </foreach>
    </where>
</select>

一对一

  • property 给 user注入
  • column 主表链接条件
  • javaType 返回的类型在java中是什么类型

新增语句

主键返回,就是没有新增 id 字段,由数据库自动新增,但是实例 bean类,id 字段为空,那么就需要主键返回

  • keyProperty 指定返回的字段
  • useGeneratedKeys 默认false,获取返回值
<insert id="insertOne" useGeneratedKeys="true" keyProperty="id">
    insert into tb_brand(brand_name, company_name, ordered, description, status)
    values (#{brandName}, #{companyName}, #{ordered}, #{description}, #{status})
</insert>

map 注入

<resultMap id="andUser" type="AddressAndUser" autoMapping="true">
    <id property="userId" column="user_id"/>
    <!--   property 给 user注入    -->
    <!--   column 主表链接条件    -->
    <!--   javaType 返回的类型在java中是什么类型    -->
    <association property="user" column="user_id" javaType="User"
                 select="com.example.mappers.UserMappers.findOneByIdUsers"/>
</resultMap>

更新语句

<update id="replaceByid">
    update tb_brand
    <set>
        <if test="brandName != null and brandName != ''">
            brand_name = #{brandName},
        </if>
        <if test="companyName != null and companyName != ''">
            company_name = #{companyName},
        </if>
        <if test="ordered != null">
            ordered = #{ordered},
        </if>
        <if test="description != null and description != ''">
            description = #{description},
        </if>
        <if test="status != null">
            status = #{status}
        </if>
    </set>
    where id = #{id};
</update>

第一种(不常用)

sql 语句

<select id="getAddressAndUser" resultType="AddressAndUser">
    select *
    from t_user
    where id = #{id};
</select>

第二种(一般)

<resultMap id="andUser" type="AddressAndUser" autoMapping="true">
    <id property="id" column="id"/>
    <association property="user" javaType="User">
        <id property="id" column="id"/>
        <result property="username" column="username"/>
        <result property="password" column="password"/>
        <result property="nickname" column="nickname"/>
    </association>
</resultMap>

<select id="getAddressAndUser" resultMap="andUser">
    select *
    from t_address ta
             left join t_user tu on tu.id = ta.user_id
    where ta.id = #{id};
</select>

第三种(常用)

就是把第二种拆分开了

<resultMap id="andUser" type="AddressAndUser" autoMapping="true">
    <id property="id" column="id"/>
    <association property="user" javaType="User" autoMapping="true" resultMap="UserAtt"/>
</resultMap>

<resultMap id="UserAtt" type="User" autoMapping="true">
    <id property="id" column="id"/>
    <result property="username" column="username"/>
    <result property="password" column="password"/>
    <result property="nickname" column="nickname"/>
</resultMap>

一对多

<mapper namespace="com.example.mappers.UserMappers">
    <resultMap id="byAdress" type="UserAndManyAdress" autoMapping="true">
        <id property="id" column="id"/>
        <collection property="addressList" column="id" autoMapping="true" ofType="address">
            <id column="taid" property="userId"/>
            <result column="taid" property="userId"/>
        </collection>
    </resultMap>

    <select id="manyAddress" resultMap="byAdress">
        select *, t.id tid, ta.id taid
        from t_user t
                 left join t_address ta on t.id = ta.user_id
        where t.id = #{id}
    </select>
</mapper>

分步查询

优势:将多表链接查询,改为分步单表。数据量大会提高性能

延迟加载

就是使用到的时候才会被加载

开启迟加载(默认没有开启延迟加载)

  • lazyLoadingEnabled 默认 false,懒加载没有打开
  • aggressiveLazyLoading 默认 true 积极加载的意思

案例

mybatis 设置 延迟加载
<settings>
      <setting name="lazyLoadingEnabled" value="true"/>
      <setting name="aggressiveLazyLoading" value="false"/>
  </settings>
环境配置
<mapper namespace="com.example.mappers.ordersMapper">

    <resultMap id="findMap" type="orders" autoMapping="true">
        <!--    对订单信息进行配置    -->
        <id property="id" column="id"/>
        <result property="userId" column="user_id"/>
        <!--    实现用户延迟加载    -->
        <!--    select 指定延迟加载需要执行的 sql 语句    -->
        <!--    colum 关联用户信息的列    -->
        <association property="user" column="user_id" javaType="user" select="com.example.mappers.UserMapper.findUserByid">

        </association>
    </resultMap>

    <select id="findOrdersUserLazyLoading" resultMap="findMap">
        select *
        from orders
    </select>
</mapper>
延迟加载对象
<mapper namespace="com.example.mappers.UserMapper">
	<!-- 延迟加载 -->
    <select id="findUserByid" resultType="com.example.beans.User">
        select *
        from user
        where id = #{id}
    </select>
</mapper>
运行代码
public static void main(String[] args) throws IOException {
    // 配置
    InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
    // 获取工厂
    SqlSessionFactory build = new SqlSessionFactoryBuilder().build(inputStream);
    // 获取执行
    SqlSession sqlSession = build.openSession(true);

    List<Orders> loading = sqlSession.getMapper(ordersMapper.class).findOrdersUserLazyLoading();
    for (Orders orders : loading) {
        // 执行到这里才实现按需要加载
        User user = orders.getUser();
    }

    sqlSession.close();
}
刚开始执行查询到了三个结果,执行了一个 sql
Preparing: select * from orders
mappers.ordersMapper.findOrdersUserLazyLoading]-==> Parameters: 
mappers.ordersMapper.findOrdersUserLazyLoading]-<==      Total: 3
继续执行,执行到了才查询第二个 sql
mappers.UserMapper.findUserByid]-==>  Preparing: select * from user where id = ?
mappers.UserMapper.findUserByid]-==> Parameters: 1(Integer)
mappers.UserMapper.findUserByid]-<==      Total: 1

处理器

创建处理器,并继承BaseTypeHandler<Double> 泛型表示数据库的字段类型

根据情况会拦截数据库表每一个字段

下面的 resultMap 为每个字段加处理器,这样才能被拦截

<resultMap id="lossTestMap" autoMapping="true" type="demo.grid.vo.IomPerFormAnceDataVoTest">
    <result column="subtract" property="subtract" typeHandler="demo.grid.handler.DoubleTypeHandler"/>
    <result column="lastsubtractSrc" property="lastSubtractSrc" typeHandler="demo.grid.handler.DoubleTypeHandler"/>
    <result column="predictPower" property="predictPower" typeHandler="demo.grid.handler.DoubleTypeHandler"/>
    <result column="lastsubtract" property="lastsubtract" typeHandler="demo.grid.handler.DoubleTypeHandler"/>
</resultMap>
<select id="lossTest" resultMap="lossTestMap">
    select
           subtract,       
           lastsubtractSrc, 
           lastsubtract,   
           predictPower 
    from xxx
</select>
public class DoubleTypeHandler extends BaseTypeHandler<Double> {

    /**
     * 执行插入或者更新时参数非空,则调用
     */
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, Double parameter, JdbcType jdbcType) throws SQLException {

    }

    /**
     * 从数据库查询的某个字段非空则调用---这里处理的是,四舍五入并保留两位小数
     */
    @Override
    public Double getNullableResult(ResultSet rs, String columnName) throws SQLException {
        double v = rs.getDouble(columnName);
        if (v == 0.0) {
            return null;
        } else {
            return Math.round(v * 100.0) / 100.0;
        }
    }

    /**
     * 执行插入或者更新时参数为空,则调用
     */
    @Override
    public Double getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return null;
    }

    /**
     * 从数据库中查询出来的某个字段为空,则调用
     */
    @Override
    public Double getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return null;
    }
}

缓存

缓存:只针对于查询
用于减轻数据压力
如果缓冲区域有数据,不需要再去数据库中查询,大大提高性能

一级缓存(SqlSession级别 - - - 默认开启)

  • 操作数据库时需要 构造 sqlSession 对象
  • 对象中有一个数据结构 hashMap 存储缓存数据
  • 不同的 sqlSession 之间缓存数据区域 HashMap 是互相不影响的

就算 mapper 对象不一样,也是只查询一次,其实跟mapper对象关系不大

  • 执行原理
    • 第一次发起查询用户1
    • 先去缓存中找是否有id为1 的用户
    • 如果没有就从数据库查到,并存到一级缓存中
    • 如果有直接从缓存获取

如果 sqlSession 去执行commit 操作(执行插入、更新、删除)会清空 sqlsession 中的一级缓存,避免脏读

// 获取执行
SqlSession sqlSession = build.openSession(true);

// 执行 sql
List<User> select = sqlSession.getMapper(cacheMapper.class).getSelect(12);
System.out.println(select);

// 再打印一次
List<User> select2 = sqlSession.getMapper(cacheMapper.class).getSelect(12);
System.out.println(select2);
sqlSession.close();

打印结果

可以看出 Total: 1,只去数据库查询了一次,也就是说一级缓存默认开启的

[com.example.mappers.cacheMapper.getSelect]-<==      Total: 1
[User{id = 12, username = 3109, password = 1234, nickname = null}]
[User{id = 12, username = 3109, password = 1234, nickname = null}]

它的范围就是 sqlSession,如果这个对象获取了两个,那么缓存也就是两个独立的。就会去数据库查询两次。sqlsession关闭即销毁

一级缓存失效四种情况

只要 SqlSession 没有 flush 或 close,它就存在。

当调用 SqlSession 的修改,添加,删除,commit(),close()等方法时,就会清空一级缓存。

二级缓存(mapper)

开启二级缓存

在 mybatis-config.xml设置

<setting name="cacheEnabled" value="true"/>

开启本 UserMapper.xml 的二级缓存 也要配置

<mapper namespace="com.example.mappers.UserMapper">
	<!-- type属性:执行缓存的类,默认就是 cache -->
    <cache/>

案例

配置信息和一级缓存,还有上面的

必须关闭资源,才能写入到二级缓存

// 创建代理对象
SqlSession sqlSession1 = build.openSession();
SqlSession sqlSession2 = build.openSession();
SqlSession sqlSession3 = build.openSession();

User userByid1 = sqlSession1.getMapper(UserMapper.class).findUserByid(1);
System.out.println(userByid1);
sqlSession1.close(); // 不执行 关闭操作无法写入到二级缓存区域

User userByid2 = sqlSession2.getMapper(UserMapper.class).findUserByid(1);
System.out.println(userByid2);
sqlSession2.close();

User userByid3 = sqlSession3.getMapper(UserMapper.class).findUserByid(1);
System.out.println(userByid3);
sqlSession3.close();

调用 pojo类实现序列化接口(需要对应的JavaBean 类实现 Serializable接口)

为了将缓存取出执行反序列化操作,因为二级缓存存储介质多种多样

多个 sqlSession 可以共用二级缓存

  • 首先开启二级缓存
  • 每一个UserMapper有一个二级缓存区域
  • 缓存区域是按照 namespace分
  • 也就是买一个 namespace mapper有一个二级缓存区域
  • 如果namespace相同,他们将会使用相同的二级缓存区域
  • 如果有一个执行 commit sql 那么就会清空二级缓存

命中率 0

[com.example.mappers.UserMapper]-Cache Hit Ratio [com.example.mappers.UserMapper]: 0.0

这时,sqlSession4 更新了数据,那么清空了二级缓存。这时其他在查询都是就没有了

User user = new User();
user.setUsername("张ming明");
user.setId(1);
sqlSession4.getMapper(UserMapper.class).updateById(user);
sqlSession4.commit();
sqlSession4.close();

禁用二级缓存 配置(useCache)

在对应的sql 映射文件中设置xml 文件中设置 useCache=“false”

<select id="findUserByid" resultType="com.example.beans.User" useCache="false">
    select *
    from user
    where id = #{id}
</select>

刷新缓存(清空缓存)

  • flushCache=“false” 不刷新缓存
  • 默认是true 刷新
<insert id="insertUser" parameterType="cn.itcast.mybatis.po.User" flushCache="false">

echcache 分布式缓存框架,整合mybatis

不使用分布缓存,缓存的数据在各各服务单独存储,不方便系统 开发。所以要使用分布式缓存对缓存数据进行集中管理。

就是让多个服务器之间缓存进行统一管理

  • mybatis无法实现分布式缓存,需要和其它分布式缓存框架进行整合。

设置缓存逻辑

mybatis提供了一个cache接口,如果要实现自己的缓存逻辑,实现cache接口开发即可。

mybatis 和 ehcache 整合包中提供了一个 cache 接口的实现类。

需要在cache <cache type = " 指定实现cache接口的实现类 "/>

需要导入 jar包 & ehcache 配置文件

  • mybatis-ehcache
  • ehcache-core

使用整合好的
<cache type=“org.mybatis.caches.ehcache.EhcacheCache”/>

配置文件

<ehcache>
	<diskStore path="D:\ffff"/>
	<defaultCache
			maxElementsInMemory="10000"
			eternal="false"
			timeToIdleSeconds="120"
			timeToLiveSeconds="120"
			overflowToDisk="true"
			maxElementsOnDisk="10000000"
			diskPersistent="false"
			diskExpiryThreadIntervalSeconds="120"

	/>
	<!--
       diskStore:为缓存路径,ehcache分为内存和磁盘两级,此属性定义磁盘的缓存位置。参数解释如下:
       user.home – 用户主目录
       user.dir  – 用户当前工作目录
       java.io.tmpdir – 默认临时文件路径
     -->

	<!--
        maxElementsInMemory="10000"  // 最大缓存个数
        eternal="true"  // 缓存对象是否永久有效,一旦设置了永久缓存timeout将不起作用,
        timeToIdleSeconds="120" // 表示缓存对象在失效前的允许闲置时间
        timeToLiveSeconds="120" //   eternal=false 对象不是永久有效时,该属性才生效;表示缓存对象在失效前允许存活的时间
        overflowToDisk="true" // overflowToDisk 表示当内存中的对象数量达到 maxElementslnMemory时, Ehcache 是否将对象写到磁盘中
        maxElementsOnDisk="10000000"
        diskPersistent="false" // 是否缓存虚拟机重启期数据
        diskExpiryThreadIntervalSeconds="120" // 表示磁盘失效线程运行时时间
        memoryStoreEvictionPolicy="LRU"

    -->

	<cache name="User_cache"
		   maxElementsInMemory="10000"
		   eternal="true"
		   timeToIdleSeconds="120"
		   timeToLiveSeconds="120"
		   overflowToDisk="true"
		   maxElementsOnDisk="10000000"
		   diskPersistent="false"
		   diskExpiryThreadIntervalSeconds="120"

	/>
</ehcache>

二级缓存应用场景

  • 访问多的查询
  • 用户对查询结果实时性要求不高
  • 耗时较高的统计分析sql、电话账单查询sql等
  • 实现方法如下:通过设置刷新间隔时间
  • 设置缓存刷新间隔flushInterval,比如设置为30分钟

局限性

对数据量大,并且细粒度高的数实现不好

  • 比如;商品信息缓存,存入一万个,有一个商品更新了,所有都要清空
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值