MyBatis-Plus的简介和使用

前言

        后端框架已经搭建完毕,现在就可以开始编写后端代码啦。在开发过程中,我经常为需要重复的编写实体类属性和sql编写感到困扰。实体类如果少的话手动编码也可以接受,但如果实体类一多,那编码效率将惨不忍睹。

        现在解决问题的东西来了,它就是MyBatis-Plus。听名字就知道它与持久层框架Mybatis有很大关系,毕竟它是Mybatis的增强工具。MyBatis-Plus在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。

        使用MyBatis-Plus只是我觉得它很方便,但真到了实际项目中,逻辑复杂的项目,就要斟酌一下了。


一、MyBatis-Plus的简介

1、特性

  • 无侵入:只做增强不做改变,引入它不会对现有工程产生影响。
  • 损耗小:启动即会自动注入基本 CURD,性能基本无损耗。
  • 强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作。
  • 支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件。
  • 支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题。
  • 支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作。
  • 支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )。
  • 内置代码生成器:可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码。
  • 内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询。
  • 分页插件支持多种数据库:支持 MySQL、MariaDB、Oracle、Postgre、SQLServer 等多种数据库。
  • 内置性能分析插件:可输出 Sql 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询。
  • 内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作。

2、框架结构

在这里插入图片描述
右边框框里的为mybatis-plus的结构:annotation(注解)、extension(扩展)、core(核心)、generator(代码生成器)、mybatis-plus-boot-starter(springboot启动器)。左边为mybatis-plus的原理。

在这里插入图片描述

3、添加依赖

<!--mybatis-plus(springboot版)-->
<dependency>
	<groupId>com.baomidou</groupId>
	<artifactId>mybatis-plus-boot-starter</artifactId>
	<version>3.4.0</version>
</dependency>

二、lombok的简介与使用

mybatis-plus可以开启使用Lombok,关于Lombok的简介和使用,我以前的博客有写到。这是链接:https://blog.csdn.net/xzys430/article/details/108864096

三、swagger的简介与使用

mybatis-plus还可以开启swagger,关于swagger的简介与使用,我以前的博客也写了。这里是链接:https://blog.csdn.net/xzys430/article/details/108727362

四、代码生成器

1、添加依赖

        MyBatis-Plus 从 3.0.3 之后移除了代码生成器与模板引擎的默认依赖,需要手动添加相关依赖。以下是AutoGenerator代码生成器和freemarker模板引擎依赖(模板引擎选一种,也可以自定义模板引擎):

<!--mybatis-plus代码生成器-->
<dependency>
	<groupId>com.baomidou</groupId>
	<artifactId>mybatis-plus-generator</artifactId>
	<version>3.4.0</version>
</dependency>
<!--Velocity(默认)模板引擎-->
<dependency>
    <groupId>org.apache.velocity</groupId>
    <artifactId>velocity-engine-core</artifactId>
    <version>2.2</version>
</dependency>
<!--freemarker模板引擎(博主用的)-->
<dependency>
	<groupId>org.freemarker</groupId>
	<artifactId>freemarker</artifactId>
	<version>2.3.30</version>
</dependency>
<!--beetl模板引擎-->
<dependency>
    <groupId>com.ibeetl</groupId>
    <artifactId>beetl</artifactId>
    <version>3.2.1.RELEASE</version>
</dependency>

自定义模板引擎例子

// AutoGenerator代码生成器
AutoGenerator generator = new AutoGenerator();

// freemarker engine
generator.setTemplateEngine(new FreemarkerTemplateEngine());

// beetl engine
generator.setTemplateEngine(new BeetlTemplateEngine());

// custom engine 
generator.setTemplateEngine(new CustomTemplateEngine());

generator.setTemplateEngine(自定义模板引擎);

2、自定义参数

        AutoGenerator 是 MyBatis-Plus 的代码生成器,通过 AutoGenerator 可以快速生成 Entity、Mapper、Mapper XML、Service、Controller 等各个模块的代码,极大的提升了开发效率。

        MyBatis-Plus 的代码生成器提供了大量的自定义参数供用户选择,能够满足绝大部分人的使用需求。下面两个链接看完后,这篇文章的代码生成器部分可以跳过了。

官方代码生成器使用教程链接

我的自定义参数类GitHub链接(有注释)

配置 GlobalConfig(全局配置)

// 全局配置
GlobalConfig gc = new GlobalConfig();
//项目根目录
String projectPath = System.getProperty("user.dir");
//用于多个模块下生成到精确的目录下(我设置在桌面)
//String projectPath = "C:/Users/xie/Desktop";
//代码生成目录
gc.setOutputDir(projectPath + "/src/main/java");
//开发人员
gc.setAuthor("先谢郭嘉");
// 是否打开输出目录(默认值:null)
gc.setOpen(false);
//实体属性 Swagger2 注解
gc.setSwagger2(true);
//去掉接口上的I
//gc.setServiceName("%Service");
// 配置时间类型策略(date类型),如果不配置会生成LocalDate类型
gc.setDateType(DateType.ONLY_DATE);
// 是否覆盖已有文件(默认值:false)
gc.setFileOverride(true);
//把全局配置添加到代码生成器主类
mpg.setGlobalConfig(gc);

配置 DataSourceConfig(数据源配置)

// 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
//数据库连接
dsc.setUrl("jdbc:mysql://localhost:3306/blog?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=GMT%2B8");
// 数据库 schema name
//dsc.setSchemaName("public");
// 数据库类型
dsc.setDbType(DbType.MYSQL);
// 驱动名称
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
//用户名
dsc.setUsername("root");
//密码
dsc.setPassword("430423");
//把数据源配置添加到代码生成器主类
mpg.setDataSource(dsc);

配置PackageConfig(包名配置)

// 包配置
PackageConfig pc = new PackageConfig();
// 添加这个后 会以一个实体为一个模块 比如user实体会生成user模块 每个模块下都会生成三层
// pc.setModuleName(scanner("模块名"));
// 父包名。如果为空,将下面子包名必须写全部, 否则就只需写子包名
pc.setParent("com.xxgg.blog");
// Service包名
pc.setService("service");
// Entity包名
pc.setEntity("entity");
// ServiceImpl包名
pc.setServiceImpl("service.impl");
// Mapper包名
pc.setMapper("mapper");
// Controller包名
pc.setController("controller");
// Mapper.xml包名
pc.setXml("mapper");
// 把包配置添加到代码生成器主类
mpg.setPackageInfo(pc);

配置InjectionConfig(自定义配置)

// 自定义配置
InjectionConfig cfg = new InjectionConfig() {
	@Override
	public void initMap() {
		// to do nothing
	}
};

// 如果模板引擎是 freemarker
String templatePath = "/templates/mapper.xml.ftl";
// 如果模板引擎是 velocity
// String templatePath = "/templates/mapper.xml.vm";

// 自定义输出配置
List<FileOutConfig> focList = new ArrayList<>();
// 自定义配置会被优先输出
focList.add(new FileOutConfig(templatePath) {
	@Override
	public String outputFile(TableInfo tableInfo) {
	// 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
		return projectPath + "/src/main/resources/mapper/" + pc.getModuleName()
				+ "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
	}
});
/*
cfg.setFileCreate(new IFileCreate() {
	@Override
    public boolean isCreate(ConfigBuilder configBuilder, FileType fileType, String filePath) {
		// 判断自定义文件夹是否需要创建
        checkDir("调用默认方法创建的目录,自定义目录用");
        if (fileType == FileType.MAPPER) {
        	// 已经生成 mapper 文件判断存在,不想重新生成返回 false
            return !new File(filePath).exists();
        }
        // 允许生成模板文件
        return true;
   	}
});
*/
cfg.setFileOutConfigList(focList);
mpg.setCfg(cfg);

配置TemplateConfig(模板配置)

// 配置模板
TemplateConfig templateConfig = new TemplateConfig();

// 配置自定义输出模板
//指定自定义模板路径,注意不要带上.ftl/.vm, 会根据使用的模板引擎自动识别
// templateConfig.setEntity("templates/entity2.java");
// templateConfig.setService();
// templateConfig.setController();

templateConfig.setXml(null);
mpg.setTemplate(templateConfig);

配置StrategyConfig(策略配置)

// 策略配置,我喜欢叫数据库表配置
StrategyConfig strategy = new StrategyConfig();
// 数据库表映射到实体的命名策略:下划线转驼峰
strategy.setNaming(NamingStrategy.underline_to_camel);
// 数据库表字段映射到实体的命名策略, 未指定按照 naming 执行
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
// 实体是否为lombok模型(默认 false)
strategy.setEntityLombokModel(true);
// 生成 @RestController 控制器
strategy.setRestControllerStyle(true);
// 实体类主键名称设置
strategy.setSuperEntityColumns("id");
// 需要包含的表名,允许正则表达式
//这里我做了输入设置
strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
// 需要排除的表名,允许正则表达式
//strategy.setExclude("***");
// 是否生成实体时,生成字段注解 默认false;
strategy.setEntityTableFieldAnnotationEnable(true);
// 驼峰转连字符
strategy.setControllerMappingHyphenStyle(true);
// 表前缀
strategy.setTablePrefix(pc.getModuleName() + "_");
// 把数据库配置添加到代码生成器主类
mpg.setStrategy(strategy);

生成

// 在代码生成器主类上配置模板引擎
mpg.setTemplateEngine(new FreemarkerTemplateEngine());
//生成
mpg.execute();

五、CRUD接口

1、BaseMapper CRUD 接口

说明:

  • 通用 CRUD 封装BaseMapper接口,为 M y b a t i s − P l u s \color{#A4A4A4}{Mybatis-Plus} MybatisPlus启动时自动解析实体表关系映射转换为 M y b a t i s \color{#A4A4A4}{Mybatis} Mybatis内部对象注入容器
  • 泛型 T 为任意实体对象
  • 参数 S e r i a l i z a b l e \color{#A4A4A4}{Serializable} Serializable 为任意类型主键 M y b a t i s − P l u s \color{#A4A4A4}{Mybatis-Plus} MybatisPlus不推荐使用复合主键约定每一张表都有自己的唯一 id 主键
  • 对象 W r a p p e r \color{#A4A4A4}{Wrapper } Wrapper为 条件构造器

BaseMapper-CRUD官方文档:https://mybatis.plus/guide/crud-interface.html#mapper-crud-%E6%8E%A5%E5%8F%A3

Insert(增)

// 官网API
// 插入一条记录
// int insert(T entity);

@Autowired
private UserMapper userMapper;

//添加 -- BaseMapper api不支持批量添加
public void insert() {
//当前数据库id设置为自增,id策略需设置id-type配置
	User user = new User();
	user.setName("wangsong");
	user.setAge(23);
	user.setEmail("1720696548@qq.com");
	userMapper.insert(user);
	System.out.println(user.getId());
}

Delete(删)

//    官网api
//    // 根据 entity 条件,删除记录
//    int delete(@Param(Constants.WRAPPER) Wrapper<T> wrapper);
//    // 删除(根据ID 批量删除)
//    int deleteBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);
//    // 根据 ID 删除
//    int deleteById(Serializable id);
//    // 根据 columnMap 条件,删除记录
//    int deleteByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);


@Autowired
private UserMapper userMapper;

// id删除
public void deleteById() {
	userMapper.deleteById(8);
}

// ids 批量删除
public void deleteBatchIds() {
	List<Long> ids = new ArrayList<>();
	ids.add(7);
	ids.add(6);
	userMapper.deleteBatchIds(ids); 
}


// columnMap 条件删除
public void deleteByMap() {
	//kay是字段名 value是字段值
	Map<String,Object> map = new HashMap<>();
	map.put("id","6");
	userMapper.deleteByMap(map);
}


// wrapper 条件删除
public void delete() {
	// 拼接sql参考:DELETE FROM user WHERE (id = ? AND name = ?)
	QueryWrapper<User> wrapper = new QueryWrapper<>();
	// kay是字段名 value是字段值
	wrapper.eq("id",6)
	.eq("name","wangsong");
	userMapper.delete(wrapper);
}

Update(改)

//    官网api
//    //根据 whereEntity 条件,更新记录
//    int update(@Param(Constants.ENTITY) T entity, @Param(Constants.WRAPPER) Wrapper<T> updateWrapper);
//    //根据 ID 修改
//    int updateById(@Param(Constants.ENTITY) T entity);


@Autowired
private UserMapper userMapper;


// 根据id 更新修改
public void updateById() {
	User user = new User();
	//需要修改的数据 Id
	user.setId(9);
	//需要修改的数据
	user.setName("erping");
	user.setAge(23);
	user.setEmail("1720696548@qq.com");
	userMapper.updateById(user);
}


// 使用 updateWrapper 条件修改(2种方式任选其一)
public void update() {
	//方式一
	// 更新修改字段
	User user = new User();
	user.setEmail("xie430423@163.com");
	// 更新添加,未设置条件 sql: UPDATE user SET email=?  ,设置后:UPDATE user SET email=? WHERE (id = ?)
	UpdateWrapper<User> wrapper1 = new UpdateWrapper<User>();
	wrapper1.eq("id",9);
	userMapper.update(user,wrapper1);

	//方式二
//  	UpdateWrapper<User> wrapper2 = new UpdateWrapper<User>();
//  	wrapper2.set("email","666666@qq.com")   // 更新的内容,可往后 .set连接
//          	.eq("id",9);                    // 更新的条件,可往后 .eq 添加
//  	userMapper.update(null,wrapper2);
}

Select(查)

//    官网api
//
//    // 根据 ID 查询
//    T selectById(Serializable id);
//    // 根据 entity 条件,查询一条记录
//    T selectOne(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
//
//    // 查询(根据ID 批量查询)
//    List<T> selectBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList);
//    // 根据 entity 条件,查询全部记录
//    List<T> selectList(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
//    // 查询(根据 columnMap 条件)
//    List<T> selectByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);
//    // 根据 Wrapper 条件,查询全部记录
//    List<Map<String, Object>> selectMaps(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
//    // 根据 Wrapper 条件,查询全部记录。注意: 只返回第一个字段的值
//    List<Object> selectObjs(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
//
//    // 根据 entity 条件,查询全部记录(并翻页)
//    IPage<T> selectPage(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
//    // 根据 Wrapper 条件,查询全部记录(并翻页)
//    IPage<Map<String, Object>> selectMapsPage(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
//    // 根据 Wrapper 条件,查询总记录数
//    Integer selectCount(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);


@Autowired
private UserMapper userMapper;


// id查询
@Test
public void selectById(){
	User user = userMapper.selectById(1L);
	System.out.println(user.toString());
}


// ids 批量查询
public void selectBatchIds(){
	List<Long> ids = new ArrayList<>();
	ids.add(1);
	ids.add(2);
	List<User> users = userMapper.selectBatchIds(ids);
	System.out.println(users.toString());
}


// 查询全部
public void selectList(){
	List<User> users = userMapper.selectList(null);
	System.out.println(users.toString());
}

//  条件查询 queryWrapper(无条件则查询全部)
public void selectListWrapper(){
	QueryWrapper<User> wrapper = new QueryWrapper<>();
	wrapper.eq("id",1);
	List<User> users = userMapper.selectList(wrapper);
	System.out.println(users.toString());
}

//  分页查询 selectPage
//  需添加分页插件配置: 详见config/MybatisPlusConfig.java
public void selectPage(){
	// 页数 /每页记录数
	Page<User> page = new Page<>(1,2);
	// 分页 / 参数二:wrapper查询条件
	Page<User> userPage = userMapper.selectPage(page, null);
	System.out.println("总页数"+userPage.getPages());
	System.out.println("总记录数"+userPage.getTotal());
	System.out.println(userPage.getRecords().toString());
}

配置分页拦截器

//Spring boot方式
@EnableTransactionManagement
@Configuration
@MapperScan("com.xxgg.blog.mapper")
public class MybatisPlusConfig {

    /**
     * 新的分页插件,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存出现问题(该属性会在旧插件移除后一同移除)
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }

    @Bean
    public ConfigurationCustomizer configurationCustomizer() {
        return configuration -> configuration.setUseDeprecatedExecutor(false);
    }
}

2、IBaseService CRUD 接口

说明:

  • 通用 Service CRUD 封装IService接口,进一步封装 CRUD 采用 g e t 查询单行、 r e m o v e 删除、 l i s t 查询集合、 p a g e 分页 \color{#A4A4A4}{get 查询单行 、remove 删除 、list 查询集合 、page 分页} get查询单行、remove删除、list查询集合、page分页 ,前缀命名方式区分 Mapper 层避免混淆,
  • 泛型 T 为任意实体对象
  • 建议如果存在自定义通用 Service 方法的可能,请创建自己的 I B a s e S e r v i c e \color{#A4A4A4}{IBaseService} IBaseService继承 Mybatis-Plus 提供的基类
  • 对象 W r a p p e r \color{#A4A4A4}{Wrapper } Wrapper 为 条件构造器

Service官网Api文档:https://mybatis.plus/guide/crud-interface.html#service-crud

注: B a s e M a p p e r 不支持批量操作, I B a s e S e r v i c e 支持批量操作 \color{red}{注:BaseMapper 不支持批量操作,IBaseService支持批量操作} 注:BaseMapper不支持批量操作,IBaseService支持批量操作

save(增)

// 插入一条记录(选择字段,策略插入)
boolean save(T entity);
// 插入(批量)
boolean saveBatch(Collection<T> entityList);
// 插入(批量)
boolean saveBatch(Collection<T> entityList, int batchSize);

SaveOrUpdate(增或改)

// TableId 注解存在更新记录,否插入一条记录
boolean saveOrUpdate(T entity);
// 根据updateWrapper尝试更新,否继续执行saveOrUpdate(T)方法
boolean saveOrUpdate(T entity, Wrapper<T> updateWrapper);
// 批量修改插入
boolean saveOrUpdateBatch(Collection<T> entityList);
// 批量修改插入
boolean saveOrUpdateBatch(Collection<T> entityList, int batchSize);

Remove(删)

// 根据 entity 条件,删除记录
boolean remove(Wrapper<T> queryWrapper);
// 根据 ID 删除
boolean removeById(Serializable id);
// 根据 columnMap 条件,删除记录
boolean removeByMap(Map<String, Object> columnMap);
// 删除(根据ID 批量删除)
boolean removeByIds(Collection<? extends Serializable> idList);

Update(改)

// 根据 UpdateWrapper 条件,更新记录 需要设置sqlset
boolean update(Wrapper<T> updateWrapper);
// 根据 whereEntity 条件,更新记录
boolean update(T entity, Wrapper<T> updateWrapper);
// 根据 ID 选择修改
boolean updateById(T entity);
// 根据ID 批量更新
boolean updateBatchById(Collection<T> entityList);
// 根据ID 批量更新
boolean updateBatchById(Collection<T> entityList, int batchSize);

Get(按条件查)

// 根据 ID 查询
T getById(Serializable id);
// 根据 Wrapper,查询一条记录。结果集,如果是多个会抛出异常,随机取一条加上限制条件 wrapper.last("LIMIT 1")
T getOne(Wrapper<T> queryWrapper);
// 根据 Wrapper,查询一条记录
T getOne(Wrapper<T> queryWrapper, boolean throwEx);
// 根据 Wrapper,查询一条记录
Map<String, Object> getMap(Wrapper<T> queryWrapper);
// 根据 Wrapper,查询一条记录
<V> V getObj(Wrapper<T> queryWrapper, Function<? super Object, V> mapper);

List(查)

// 查询所有
List<T> list();
// 查询列表
List<T> list(Wrapper<T> queryWrapper);
// 查询(根据ID 批量查询)
Collection<T> listByIds(Collection<? extends Serializable> idList);
// 查询(根据 columnMap 条件)
Collection<T> listByMap(Map<String, Object> columnMap);
// 查询所有列表
List<Map<String, Object>> listMaps();
// 查询列表
List<Map<String, Object>> listMaps(Wrapper<T> queryWrapper);
// 查询全部记录
List<Object> listObjs();
// 查询全部记录
<V> List<V> listObjs(Function<? super Object, V> mapper);
// 根据 Wrapper 条件,查询全部记录
List<Object> listObjs(Wrapper<T> queryWrapper);
// 根据 Wrapper 条件,查询全部记录
<V> List<V> listObjs(Wrapper<T> queryWrapper, Function<? super Object, V> mapper);

Page(分页查)

// 无条件分页查询
IPage<T> page(IPage<T> page);
// 条件分页查询
IPage<T> page(IPage<T> page, Wrapper<T> queryWrapper);
// 无条件分页查询
IPage<Map<String, Object>> pageMaps(IPage<T> page);
// 条件分页查询
IPage<Map<String, Object>> pageMaps(IPage<T> page, Wrapper<T> queryWrapper);

Count(查记录数)

// 查询总记录数
int count();
// 根据 Wrapper 条件,查询总记录数
int count(Wrapper<T> queryWrapper);

Chain(链式)

query(链式查询)
// 链式查询 普通
QueryChainWrapper<T> query();
// 链式查询 lambda 式。注意:不支持 Kotlin
LambdaQueryChainWrapper<T> lambdaQuery(); 

// 示例:
query().eq("column", value).one();
lambdaQuery().eq(Entity::getId, value).list();
update(链式更改)
// 链式更改 普通
UpdateChainWrapper<T> update();
// 链式更改 lambda 式。注意:不支持 Kotlin 
LambdaUpdateChainWrapper<T> lambdaUpdate();

// 示例:
update().eq("column", value).remove();
lambdaUpdate().eq(Entity::getId, value).update(entity);

3、参数说明

下面列举前面出现的参数说明:

参数名称类型描述
idSerializable主键ID
entityT实体对象
entityListCollection实体对象集合
batchSizeint插入批次数量
pageIPage翻页对象
wrapperWrapper实体对象封装操作类
updateWrapperWrapper实体对象封装操作类 UpdateWrapper
queryWrapperWrapper实体对象封装操作类 QueryWrapper
mapperFunction<? super Object, V>转换函数
idListCollection<? extends Serializable>主键ID列表
columnMapMap<String, Object>表字段 map 对象
throwExboolean有多个 result 是否抛出异常

官方文档:https://mybatis.plus/guide/

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值