Mybatis开发中老生常谈的一些要点

模糊查询

like中分右模糊、左模糊,右模糊比如’abc%‘时,扫描索引高效。当模糊查询含左模糊时,比如’%abc’时,会进行全表扫描低效

使用bind标签(优先推荐)

<if test="@org.springframework.util.StringUtils@hasText(field)'">
      <bind name="fieldLike" value="'%'+fieldLike+'%'"/>
       and fieldLike like #{fieldLike}
 </if>

注意:if标签里面的test判断是可以使用工具类来做判断的,其格式:@完整的包名类名@方法名(传参)

使用CONCAT函数(兼容性差,不推荐)

Oracle如:fieldLike LIKE  CONCAT(CONCAT('%',#{fieldLike},'%')

注意: Oracle的concat只支持两个参数,Mysql的concat支持多个参数,因此用了2个concat。

使用引号(兼容性差,不推荐)

Mysql:fieldLike like  "%"#{fieldLike}"%"
Oracle:fieldLike like '%'|| #{fieldLike} || '%'


注意: Mysql中,因为#{…}解析成sql语句时候,会在变量外侧自动加单引号’ ',所以这里 % 需要使用双引号" ",不能使用单引号 ’ ',不然会查不到任何结果。

使用$符(存在Sql注入,不推荐)

fieldLike like '%${fieldLike}%'

代码拼接(侵入式,不推荐)

fieldLike like #{fieldLike}

注意:fieldLike必须在代码中拼接成%value%

传参传递

Mybatis对传入的参数处理:

  1. 如果没有加@Param注解,那么在多参数的情况下默认会给占位符添加arg0,arg1....
  2. 如果添加了注解那么就会把arg替换为@Param的value的值,然后默认还会增加通用的占位符名称param1,param2...
  3. 如果是单个参数,则会区分集合、数组类型,还是其他类型,若集合、数组会被返回Map(具体参考wrapCollection或wrapToMapIfCollection方法返回),如果是其他类型,在没有注解的时候基础类型的占位符可以随便填写,对象类型可以直接使用参数名.属性来作为占位符,在有注解的时候占位符是@Param的value.属性或者param1.属性。

示例类型:

方法参数占位符格式说明
(Integer i)#{i}#内可以是任意字母都行,如#{a}
(Integer i,String x)

#{arg0},#{arg1} |

#{param1},#{param2}

包含两种设置值的方式,arg和param,一个从0开始,一个从1开始,一次向后累加

(User user)#{name}User对象包含name属性
(User user,Integer i)

#{arg0.name},#{arg1} |

#{param1.name},#{param2}

多参数默认就是从arg0,arg1….或者param1,param2….,arg和param也可以混用,在赋值的时候,这些都会存在一个Map中

(List<Integer> ids)

<foreach item="item" collection="list" separator="," open="("close=")" index="">

#{item}

</foreach>

collection可以传入list或者collection。注意,

只有传入的List的子类才能使用list,详情看ParamNameResolver中wrapCollection或wrapToMapIfCollection方法

(List<Integer> ids,Integer i)

<foreach item="item" collection="arg0" separator="," open="("close=")" index="">

#{item}

</foreach>

and id=#{arg1}

arg0和arg1还是可以替换为param1和param2
(Integer[] ids)

<foreach item="item" collection="array" separator="," open="("close=")" index="">

#{item}

</foreach>

collection只能传入array
(@Param("i") Integer i)#{i} | #{param1}只能传入@Param所指定名称或者param1
(@Param("user")User user)#{user.id} |  #{param1.id}只能传入@Param所指定名称或者param1
(@Param("ids")List<Integer> ids)

<foreach item="item" collection="ids" separator="," open="("close=")" index="">

#{item}

</foreach>

collection可以是@Param所指定名称或者param1

原理:详解org.apache.ibatis.reflection.ParamNameResolver
  • Mybatis的Mapper参数解析,返回参数的位置和参数的标记:

       主要逻辑:1)若没有 @Param注解,则会返回arg0、arg1... 

        2)若有 @Param注解,则会返回param1, param2, ...

  • Mybatis的Mapper参数标记和具体值关系映射解析:
  • Mybatis的Mapper参数集合/数组解析:

        若参数Collection的所有子类,则都可以传入collection属性,如果还是List的子类,那么还可以传入list属性;否则是数组,则传入array属性

级联查询

1、一对一:使用association标签

2、一对多:使用collection标签

 若上述标签使用select属性,则存在N+1问题:即多一个关联,SQL就多执行一次,且会存在多余字段,因此为了解决该问题,该标签需要javaType或ofType属性,然后select标签使用级联查询。

示例:

<resultMap id="one2OneMap" type="com.ouo.mask.model.MaskRule">
        <id property="id" column="id"/>
        <result property="type" column="type"/>
        <result property="enabled" column="enabled"/>
        <result property="rule" column="rule"/>
        <result property="desc" column="desc"/>
        <!--<association property="maskRule" column="id" select="SensitiveFieldMapper.getSensitiveFieldMapperBySfId" />-->
        <association property="sensitiveField" column="id" columnPrefix="sf_"
                     javaType="com.ouo.mask.model.SensitiveField">
            <id property="id" column="id"/>
            <result property="name" column="name"/>
            <result property="type" column="type"/>
            <result property="state" column="state"/>
            <result property="desc" column="desc"/>
        </association>

</resultMap>

参数映射

MyBatis的映射原理主要包括两个方面:SQL语句的映射和Java对象的映射。SQL语句的映射是指将SQL语句和Java方法之间的映射关系定义在XML文件或注解中,从而实现数据库操作的自动化。Java对象的映射是指将数据库表中的数据映射到Java对象中,从而实现数据的持久化和操作。进入XMLMapperBuilder类中,找到bindMapperForNamespace

 Configuration类实际上将addMapper和getMapper委派给了MapperRegistry来执行:

  • addMapper方法会针对这个Mapper接口生成一个MapperProxyFactory工厂类。
  • getMapper方法,会通MapperProxyFactory工厂类,返回一个Mapper接口的动态代理类。

当调用sqlSession.getMapper()方法时,就会创建一个新的动态代理对象。首先,Mybatis会调用XMLMapperBuilder类的bindMapperForNamespace()方法,根据xml文件中的namespace中的属性,去找Mapper接口,如果找到,就通过configuration类(然后是MapperRegistry 类)的addMapper()方法将其注册到MapperRegistry 类的 HashMap中,key是Mapper接口的class对象,value是当前的Mapper的代理工厂对象mapperProxyFactory,然后调用该对象的newInstance()方法去实例化对象,newInstance()里面返回的就是基于jdk的动态代理生成的Mapper接口的代理对象,在代理类MapperProxy 中完成了CRUD的调用。

1)Mapper中传递多个参数时,可使用@Param指定

2)Mapper中传递javabean时,可指定javabean类型

3)Mapper默认映射按照驼峰命名(即MyBatis 提供自动映射功能,只要返回 SQL 列名和 JavaBean 的属性一致),若字段名与属性名不一致,用别名解决或@TableName 修饰javabean属性或resultMap自定义标签相关的属性

4)排除非表字段:使用 transient 关键字修饰或使用 static 修饰或使用 TableField 注解


枚举映射处理:

1、数据库值序列化为枚举(3.1.0开始

  • 存储枚举的名称:mybatis对枚举类型的默认处理方式,使用的类型处理器是org.apache.ibatis.type.EnumTypeHandler
  • 存储枚举的索引:需要手动指定typeHandler 设置类型处理为org.apache.ibatis.type.EnumOrdinalTypeHandler。示例:#{gender,typeHandler=org.apache.ibatis.type.EnumOrdinalTypeHandler}
  • 存储枚举的值:需要创建一个自定义的类型处理器继承BaseTypeHandler,然后如上手动指定typeHandler

而对于mybatis-plus采用通用枚举,需要如下处理:

  • 声明通用枚举属性:使用 @EnumValue 注解枚举属性或枚举实现 IEnum 接口
  • 配置扫描通用枚举:

    mybatis-plus:

          # 支持统配符 * 或者 ; 分割多个包路径

        typeEnumsPackage: com.baomidou.springboot.entity.enums

若返回是resultMap或传入参数parameterMap,则需要手动指定字段的枚举转换,否则枚举注解是不起效果

2、枚举序列化成数据库值

  • 使用@JsonValue修饰枚举内部属性:一个类只能用一个,当加上@JsonValue注解时,序列化时只返回这一个字段的值,而不是这个类的属性键值对(即这个类其他属性的值不会被序列化),可以接受前端(枚举序号或枚举值)反序列化成枚举,也可以响应前端(枚举序列化成枚举值),默认按照枚举名称或枚举索引映射。示例:

    public enum MaskEnum {

        // TODO: 假名
        ALIAS(10),
        // TODO: 掩盖
        HIDE(20),
        // TODO: 哈希
        HASH(30),
        // TODO: 替换
        REPLACE(40),
        // TODO: 置空
        EMPTY(50);
        
        @JsonValue //标记响应json值
        private int code;

        MaskTypeEnum(int code) {
            this.code = code;
        }

    }

  • 自定义定义的 Converter 和 ConverterFactory: 注册到 Spring 的类型转换器中,示例

    @Configuration

    public class WebConfig implements WebMvcConfigurer {

        @Override

        public void addFormatters(FormatterRegistry registry) {

            registry.addConverterFactory(new CodeToEnumConverterFactory());

        }

    }

  • 修改Json枚举序列化配置:同时还需要在枚举中复写toString方法,示例

    @Bean
    public Jackson2ObjectMapperBuilderCustomizer customizer(){
        //Jackson全局配置
        return builder -> builder.featuresToEnable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING);
    }

若Jackson局部使用序列化,示例:

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(SerializationFeature.WRITE_ENUMS_USING_TO_STRING, true);

对于Fastjson全局配置,示例:

@Bean
public HttpMessageConverters fastJsonHttpMessageConverters() {
    //定义一个convert转换消息的对象
    FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
    //添加fastjson的配置信息,比如是否要格式化返回的json数据;
    FastJsonConfig fastJsonConfig = new FastJsonConfig();
    fastJsonConfig.setWriterFeatures(
        //是否输出值为null的字段,默认为false
        JSONWriter.Feature.WriteMapNullValue,
        //将Collection类型字段的字段空值输出为[]
        JSONWriter.Feature.WriteNullListAsEmpty,
        //将字符串类型字段的空值输出为空字符串
        JSONWriter.Feature.WriteNullStringAsEmpty,
        //将枚举转成字符串
        //SerializerFeature.WriteEnumUsingToString
        JSONWriter.Feature.WriteEnumUsingToString
    );
    //在convert中添加配置信息
    fastConverter.setFastJsonConfig(fastJsonConfig);
    //设置支持的媒体类型
    fastConverter.setSupportedMediaTypes(Collections.singletonList(
        MediaType.APPLICATION_JSON));
    //设置默认字符集
    fastConverter.setDefaultCharset(StandardCharsets.UTF_8);

    //解决返回字符串带双引号问题
    StringHttpMessageConverter stringConverter = new StringHttpMessageConverter();
    stringConverter.setSupportedMediaTypes(
        Collections.singletonList(MediaType.TEXT_PLAIN));
    stringConverter.setDefaultCharset(StandardCharsets.UTF_8);

    return new HttpMessageConverters(fastConverter, stringConverter);
}

对于Fastjson局部使用序列号,示例:

@JSONField(serialzeFeatures= SerializerFeature.WriteEnumUsingToString)
private UserStatus status;

分页查询

使用拦截器分页(推荐)

可以通过实现Interceptor接口,例如第三方PageHelper

<!--MyBatis 分页插件: MyBatis PageHelper-->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.2.5</version>
</dependency>

在application.properties配置文件中添加MyBatis PageHelper的配置项:

# PageHelper 分页插件配置
pagehelper.helperDialect=mysql
pagehelper.reasonable=true
pagehelper.supportMethodsArguments=true
pagehelper.params=count=countSql

使用示例:

public PageInfo findPage(@RequestParam int pageNum, @RequestParam int pageSize) {
    // 设置分页查询参数
    PageHelper.startPage(pageNum,pageSize);
    // 封装分页查询结果到 PageInfo 对象中以获取相关分页信息
    PageInfo pageInfo = new PageInfo(studentMapper.findList());
    System.out.println("总页数: " + pageInfo.getPages());
    System.out.println("总记录数: " + pageInfo.getTotal());
    System.out.println("当前页数: " + pageInfo.getPageNum());
    System.out.println("当前页面记录数量: " + pageInfo.getSize());

    return pageInfo;
}

使用RowBounds分页(不推荐)

示例:List<User> queryUsersByPage(String userName, RowBounds rowBounds);

在查询数据库时,如果没有limit语句,则ResultSet中会包含所有满足条件的数据,都是一次获取所有符合条件的数据,然后在内存中对大数据进行操作,实现分页效果(对内存的消耗很大,容易内存溢出)。

有两种方式,一种是RowBounds作为参数传入Service,另一种是使用适配器,这个适配器很简单,写个RowBounds的子类,在子类中重写hashCode&equals方法,在Service中使用新的类。

Mysql使用limit分页

示例:select * from stu limit m, n;

注意:起始m = (startPage-1)*pageSize,取几行n = pageSize
(1)第一个参数值m表示起始行,第二个参数表示取多少行(页面大小)
(2)m= (2-1)*10+1,n=10 ,表示 limit 11,10从11行开始,取10行,即第2页数据。
(3)m、n参数值不能在语句当中写计算表达式,因此传参时必须计算好值。

Oracle使用rownum分页

示例:select * from (select rownum rn,a.* from (业务查询) a where rownum <= x)where rn >= y;

注意:
(1)>= y,<= x表示从第y行(起始行)~x行(结束行) 

(2)x = startPage*pageSize,y = (startPage-1)*pageSize+1
(3)rownum只能比较小于,不能比较大于,因为rownum是先查询后排序的,例如你的条件为rownum>1,当查询到第一条数据,rownum为1,则不符合条件。第2、3...类似,一直不符合条件,所以一直没有返回结果。所以查询的时候需要设置别名,然后查询完成之后再通过调用别名进行大于的判断

扩展:
利用分析函数

      row_number() over ( partition by col1 order by col2 )                              

比如想取出100-150条记录,按照tname排序
    select tname,tabtype from (                              

    select tname,tabtype,row_number() over ( order by tname ) rn from tab                

) where rn between 100 and 150;

Mybatis-plus分页

<!-- MyBatisX插件 -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.1.0</version>
</dependency>

1、内置分页

@Test
public void testPage() {
    System.out.println("----- selectPage method test ------");
    //分页参数
    Page<User> page = Page.of(1,10);
    //queryWrapper组装查询where条件
    LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(User::getAge,13);
    userMapper.selectPage(page,queryWrapper);
    page.getRecords().forEach(System.out::println);
}

2、PaginationInnerInterceptor分页插件配置添加到Spring IoC管理:

/**
 * 分页插件
 * 也可以不传DbType.MYSQL,为空时会通过URL判断数据库类型
 * if (url.contains(":mysql:") || url.contains(":cobar:")) {
 *              return DbType.MYSQL;
 */
@Bean 
public MybatisPlusInterceptor mybatisPlusInterceptor() {
    //最新版3.5.0分页插件注册写法:
    MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
    //添加乐观锁
    mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
    //添加分页
    mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
    return mybatisPlusInterceptor;
}

使用示例:

IPage<MaskRule> list(@Param("page") IPage<MaskRule> page, @Param("maskRule") MaskRule maskRule);

注意:若第一个参数不是IPage子类,则必须使用@Param指定名称,否则会报错。具体详见MybatisPlusInterceptor

目前具有拦截器:

  • 自动分页: PaginationInnerInterceptor

  • 多租户: TenantLineInnerInterceptor

  • 动态表名: DynamicTableNameInnerInterceptor

  • 乐观锁: OptimisticLockerInnerInterceptor

  • sql 性能规范: IllegalSQLInnerInterceptor

  • 防止全表更新与删除: BlockAttackInnerInterceptor

缓存

  • 一级缓存:在执行器对象 Executor 里,而每个执行器又归属于 SqlSession,所以常说一级缓存的作用域是 SqlSession级别的。
  • 二级缓存/事务缓存:在 CacheExecutor 里,在 MappedStatement 里,也就是Mapper 中 cache 或者 cacheRef 标签,但统一都由 CacheExecutor 里的 TransactionalCacheManager进行管理,解决脏读

MyBatis 默认开启了⼀级缓存,⼀级缓存是在SqlSession 层⾯进⾏缓存的。即同⼀个SqlSession ,多次调⽤同⼀个Mapper和同⼀个⽅法的同⼀个参数,只会进⾏⼀次数据库查询,然后把数据缓存到缓冲中,以后直接先从缓存中取出数据,不会直接去查数据库。
但是不同的SqlSession对象,因为不同的SqlSession都是相互隔离的,所以相同的Mapper、参数和⽅法,还是会再次发送到SQL到数据库去执⾏,返回结果。

⼆级缓存是mapper级别的缓存,它的实现机制跟⼀级缓存差不多,也是基于PerpetualCache的HashMap本地存储。作⽤域为mapper的namespace,可以⾃定义存储,⽐如Ehcache。Mybatis的⼆级缓存是跨Session的,每个Mapper享有同⼀个⼆级缓存域。

Mybatis内部存储缓存使⽤⼀个HashMap,key为hashCode+sqlId+Sql语句,value为从查询出来映射⽣成的Java对象。

二级缓存开启:

1、在mybatis-config.xml 配置文件中的 cacheEnabled 配置,它是二级缓存的总开关,只有该配置为 true ,后面的缓存配置才会生效。默认为 true,即二级缓存默认是开启的。

2、Mapper.xml 配置文件中配置的 <cache> 和 <cache-ref>标签,如果 Mapper.xml 配置文件中配置了这两个标签中的任何一个,则表示开启了二级缓存的功能,如果配置了 <cache> 标签,则在解析配置文件的时候,会为该配置文件指定的 namespace 创建相应的 Cache 对象作为其二级缓存(默认为 PerpetualCache 对象),如果配置了 <cache-ref> 节点,则通过 ref 属性的namespace值引用别的Cache对象作为其二级缓存。通过 <cache> 和 <cache-ref> 标签来管理其在namespace中二级缓存功能的开启和关闭。

例如:<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>

flushInterval(刷新间隔)可以被设置为任意的正整数,⽽且它们代表⼀个合理的毫秒形式的时间段。默认情况是不设置,也就是没有刷新间隔,缓存仅仅调⽤语句时刷新。 
size(引⽤数⽬)可以被设置为任意正整数,注:缓存的对象数⽬和运⾏环境的可⽤内存资源数⽬。默认值是1024。 
readOnly(只读)属性可以被设置为true或false。只读的缓存会给所有调⽤者返回缓存对象的相同实例。因此这些对象不能被修改,这提供了很重要的性能优势。可读写的缓存会返回缓存对象的拷贝(通过序列化)。这会慢⼀些,但是安全,因此默认是false。

3、<select> 节点中的 useCache 属性也可以开启二级缓存,该属性表示查询的结果是否要存入到二级缓存中,该属性默认为 true,也就是说 <select> 标签默认会把查询结果放入到二级缓存中。

例如:<select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.String" useCache="false">

若需要立刻刷新statement缓存,则设置statement(即select 标签)配置中的flushCache=“true“属性,就会默认刷新缓存,相反如果是false就不会了。

注意:⽆论是⼀级缓存还是⼆级缓存,C/U/D增删改操作提交和回滚事务后都会清空PerpetualCache缓存区域,使缓存失效。合理利⽤⼆级缓存可以提⾼系统性能,减少数据库压⼒。但是,如果使⽤不当可那个会出现缓存⼀致性问题,对查询结果实时性要求不⾼,此时可采⽤mybatis⼆级缓存技术降低数据库访问量,提⾼访问速度。在按照 MyBatis 规范使用 SqlSession 的情况下,一级缓存不存在并发问题,但二级缓存会存在并发和事务(即多个事务共用一个缓存实例,会导致【脏读】)问题,因此可以使用SynchronizedCache解决Cache并发问题,而TransactionalCache解决事务缓存【脏读】问题,但是不可重复读还是会存在,同时一级缓存同样会存在【脏读】

主键生成

具体原理请参考SelectKeyGenerator源码

1、数据库支持自动生成主键

若数据库支持自动生成主键的字段(比如 MySQL和 SQL Server),则可以设置useGeneratedKeys=”true”,利用 statement.getGenreatadKeys()生成,然后再把keyProperty 设置到目标属性上。

示例:

<insert id="addEmployee" useGeneratedKeys="true" keyProperty="id" databaseId="mysql">        

        insert into employee(last_name,age,email)   values(#{lastName},#{age},#{email})   

</insert>


2、数据库不支持自动生成主键

对于不支持自增型主键的数据库(例如Oracle),则可以使用 selectKey 子元素:selectKey 元素将会首先运行,id 会被设置,然后插入语句会被调用。例如Oracle 不支持自增:

1)使用"序列"来模拟自增,每次插入的数据的主键是从序列中拿到的
2)使用UUID:select replace(uuid(),'-','') from dual 

示例:

 <insert id="addEmployee" databaseId="oracle">        

      <selectKey keyProperty="id" order="BEFORE" resultType="Integer">           

              select EMPLOYEE_SEQ.nextval from dual        

      </selectKey>        

      <!-- 插入的主键是从序列中获取的 -->            

  insert into employee(id,last_name,age,email)  values(#{id},#{lastName},#{age},#{email})    

</insert>

tk.mybatis扩展

<!-- tk.mybatis插件 -->
<dependency>
    <groupId>tk.mybatis</groupId>
    <artifactId>mapper-spring-boot-starter</artifactId>
    <version>2.1.5</version>
</dependency>
<dependency>
    <groupId>javax.persistence</groupId>
    <artifactId>persistence-api</artifactId>
</dependency>

常用的ID生成注解有两个,其中java内置@GeneratedValue注解是使用最广泛和tk扩展的@KeySql注解

 MyBatis-Plus扩展(推荐)

IdType是一个枚举类,定义了生成ID的类型

  • AUTO 数据库ID自增
  • INPUT 自行设置ID
  • ID_WORKER 全局唯一ID,Long类型的主键
  • ID_WORKER_STR 字符串全局唯一ID
  • UUID或ASSIGN_UUID 全局唯一ID,UUID类型的主键
  • ASSIGN_ID,只有当用户未输入时,采用雪花算法生成一个适用于分布式环境的全局唯一主键,类型可以是String和number
  • NONE 该类型为未设置主键类型

对于INPUT类型,一种情况是程序里面自己指定主键,还有一种是利用MyBatis-Plus自带的如下主键生成器生成:

  • DB2KeyGenerator
  • H2KeyGenerator
  • KingbaseKeyGenerator
  • OracleKeyGenerator
  • PostgreKeyGenerator

使用方式:先将注解生成器添加到Spring IoC容器管理,其次在实体类上声明@KeySequence,指定使用的Sequence名称,并指定主键类型为@TableId(type = IdType.INPUT)

MyBatis Plu中自定义主键生成,则先实现IdentifierGenerator并添加到Spring IoC容器管理,其次主键类型为@TableId(type = IdType.ASSIGN_ID)

当IdType的类型为ID_WORKER、ID_WORKER_STR或者UUID时,主键由MyBatis Plus的IdWorker类生成 。

public static long getId() {   return worker.nextId();    }
public static synchronized String get32UUID() {

        return UUID.randomUUID().toString().replace("-", "");

}


1)全局生成数字型ID:895503808246718464,即Long类型ID长度超出JavaScript,因此会导致精度丢失,解决办法:将Long转成String

2)默认,mybatis plus默认使用全局唯一的数字类型 ID_WORKER(2, “全局唯一ID”),生成的ID格式:ccba0a05fcbe46898304d5213d2b551

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值