MyBatisPlus条件构造器 AR 代码生成器 插件 自定义全局 公共字段填充

官方地址:https://mp.baomidou.com/guide/#%E7%89%B9%E6%80%A7

1、框架简介

(1)MyBatis-Plus (opens new window)(简称 MP)是一个 MyBatis (opens new window)的增强工具,在MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
(2)支持任何能使用 mybatis 进行 crud, 并且支持标准 sql 的数据库
(3)框架结构
在这里插入图片描述

2、集成MP

2.1 MP安装
通过Maven安装(官网给的安装都是集成springboot里面的了),但是尚硅谷教程里面这节的mybatisPlus还是单独和spring来一起的,反正最后也是用springboot,就不记录过时的上来了。

https://mp.baomidou.com/guide/install.html#release
2.2 创建测试表

创建库
CREATE DATABASE mp;
-- 使用库
USE mp;
-- 创建表
CREATE TABLE tbl_employee(
id INT(11) PRIMARY KEY AUTO_INCREMENT,
last_name VARCHAR(50),
email VARCHAR(50),
gender CHAR(1),
age int
);
INSERT INTO tbl_employee(last_name,email,gender,age) VALUES('Tom','tom@atguigu.com',1,22);
INSERT INTO tbl_employee(last_name,email,gender,age) VALUES('Jerry','jerry@atguigu.com',0,25);
INSERT INTO tbl_employee(last_name,email,gender,age) VALUES('Black','black@atguigu.com',1,30);
INSERT INTO tbl_employee(last_name,email,gender,age) VALUES('White','white@atguigu.com',0,35);

在这里插入图片描述
2.3 根据该数据表,在java中创建好对应的实体类

特别说明:定义javabean中的实体类最好使用包装类,虽然现在已经可以有自动包装,和自动拆箱。但是如果使用基本数据类型,基本数据类型都有默认值,int的默认值为0,boolean默认值为false,主要是由于mysql有非空判定,默认值会让mysql判断不准确。所以最好用包装类

@TableName
官网:https://mp.baomidou.com/guide/annotation.html#tablename

javabean代码

package com.atguigu.mp.beans;
/*
 * MybatisPlus会默认使用实体类的类名到数据中找对应的表. 
 */
//@TableName(value="tbl_employee")
public class Employee {
	/*
	 * @TableId:
	 * 	 value: 指定表中的主键列的列名, 如果实体属性名与列名一致,可以省略不指定. 
	 *   type: 指定主键策略. 
	 */
	//@TableId(value="id" , type =IdType.AUTO)
	private Integer id ;   //  int 
	
	@TableField(value = "last_name")
	private String  lastName; 
	private String  email ;
	private Integer gender; 
	private Integer age ;
	
	@TableField(exist=false)
	private Double salary ; 
	//get、set、toString等等

2.4 在项目中加入项目所需的各类配置文件
2.4.1 加入mybatis的全局配置文件mybatis-config.xml

项目/src/main/resources/mybatis-config.xml(有这个文件就行了,啥都不用写,配置交给spring做)

<?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>
	
</configuration>

2.4.2 加入log4j.xml配置文件(日志框架多了,也不是一定要用这个写日志)
项目/src/main/resources/log4j.xml
具体配置方法见https://blog.csdn.net/chengqingshihuishui/article/details/112733634
或者看官网https://my.oschina.net/xianggao/blog/523401

2.4.3加入db.propertis连接配置信息
项目/src/main/resources/db.properties

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mp
jdbc.username=root
jdbc.password=1234

2.4.5 加入spring的配置文件 applicationContext.xml
项目/src/main/applicationContext.xml
以下是mp01项目的完整版配置文件
(1)集成MybatisPlus
Mybatis-Plus 的集成非常简单,对于Spring,我们仅需要把 Mybatis 自带的MybatisSqlSessionFactoryBean替换为替换为MP自带的即可。见代码(id=“sqlSessionFactoryBean”)

<!-- 数据源 -->
	<context:property-placeholder location="classpath:db.properties"/>
	<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
		<property name="driverClass" value="${jdbc.driver}"></property>
		<property name="jdbcUrl" value="${jdbc.url}"></property>
		<property name="user" value="${jdbc.username}"></property>
		<property name="password" value="${jdbc.password}"></property>
	</bean>
	<!-- 事务管理器 -->
	<bean id="dataSourceTransactionManager" 
	class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="dataSource"></property>
	</bean>
	<!-- 基于注解的事务管理 -->
	<tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>
	<!--  配置SqlSessionFactoryBean 
		Mybatis提供的: org.mybatis.spring.SqlSessionFactoryBean
		MP提供的:com.baomidou.mybatisplus.spring.MybatisSqlSessionFactoryBean
	 -->
	<bean id="sqlSessionFactoryBean" class="com.baomidou.mybatisplus.spring.MybatisSqlSessionFactoryBean">
		<!-- 数据源 -->
		<property name="dataSource" ref="dataSource"></property>
		<property name="configLocation" value="classpath:mybatis-config.xml"></property>
		<!-- 别名处理 -->
		<property name="typeAliasesPackage" value="com.atguigu.mp.beans"></property>		
		<!-- 注入全局MP策略配置 -->
		<property name="globalConfig" ref="globalConfiguration"></property>
	</bean>
	<!-- 定义MybatisPlus的全局策略配置-->
	<bean id ="globalConfiguration" class="com.baomidou.mybatisplus.entity.GlobalConfiguration">
		<!-- 在2.3版本以后,dbColumnUnderline 默认值就是true -->
		<property name="dbColumnUnderline" value="true"></property>
		<!-- 全局的主键策略 -->
		<property name="idType" value="0"></property>
		<!-- 全局的表前缀策略配置 -->
		<property name="tablePrefix" value="tbl_"></property>
	</bean>
	<!-- 
		配置mybatis扫描mapper接口的路径
	 -->
	<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
		<property name="basePackage" value="com.atguigu.mp.mapper"></property>
	</bean>	
</beans>

2.5 测试Spring-Mybatis的环境,保证ok

package com.atguigu.mp.test;

public class TestMP {
	private ApplicationContext ioc = 
				new ClassPathXmlApplicationContext("applicationContext.xml");
    @Test
	public void testDataSource() throws Exception {
		DataSource ds  = ioc.getBean("dataSource",DataSource.class);
		System.out.println(ds);
		Connection conn = ds.getConnection();
		System.out.println(conn);
	}
}

3、入门CRUD

3.1 通用CRUD
(1)提出问题:
假设我们已存在一张tbl_employee表,且已有对应的实体类表,Employee,实现tbl_employee表的CRUD操作我们需要做什么呢?
(2)实现方式:
基于Mybatis:
需要编写EmployeeMapper接口,并手动编写CRUD方法提供 EmployeeMapper.xml映射文件,并手动编写每个方法对应的SQL语句。
基于MP:
只需要创建EmployeeMapper接口 , 并继承BaseMapper接口,这就是使用MP需要完成的所有操作,甚至不创建SQL映射文件。
**说明:**继承好该接口就ok,如果只用提供的sql语句,啥都不用写。

package com.atguigu.mp.mapper;
/**
 * Mapper接口
 * 基于Mybatis:在Mapper接口中编写CRUD相关的方法,提供Mapper接口所对应的SQL映射文件 以及 方法对应的SQL语句. 
 * 
 * 基于MP:  让XxxMapper接口继承 BaseMapper接口即可.
 * 		   BaseMapper<T> : 泛型指定的就是当前Mapper接口所操作的实体类类型 
 * 
 */
public interface EmployeeMapper extends BaseMapper<Employee> {
    //   Integer  insertEmployee(Employee employee );
	//   <insert useGeneratedKeys="true" keyProperty="id" > SQL...</insert>
}

3.2 插入操作(结合前面的实体类代码)

package com.atguigu.mp.test;
public class TestMP {
	private ApplicationContext ioc = 
				new ClassPathXmlApplicationContext("applicationContext.xml");
	private EmployeeMapper employeeMapper = 
			ioc.getBean("employeeMapper",EmployeeMapper.class);
	/**
	 * 通用 插入操作
	 */
	@Test
	public void testCommonInsert() {
		//初始化Employee对象
		Employee employee  = new Employee();
		employee.setLastName("MP");
		employee.setEmail("mp@atguigu.com");
		//employee.setGender(1);
		//employee.setAge(22);
		employee.setSalary(20000.0);
		//插入到数据库   
		// insert方法在插入时,会根据实体类的每个属性进行非空判断,只有非空的属性对应的字段才会出现到SQL语句中
		//Integer result = employeeMapper.insert(employee);  
		//insertAllColumn方法在插入时,不管属性是否非空,属性所对应的字段都会出现到SQL语句中. 
		Integer result = employeeMapper.insertAllColumn(employee);
		System.out.println("result: " + result );
		//获取当前数据在数据库中的主键值
		Integer key = employee.getId();
		System.out.println("key:" + key );
	}
}

说明:
insert方法在插入时,会根据实体类的每个属性进行非空判断,只有非空的属性对应的字段才会出现到SQL语句中:
Integer result = employeeMapper.insert(employee);
Integer result = employeeMapper.insertAllColumn(employee);方法在插入时,不管属性是否非空,属性所对应的字段都会出现到SQL语句中.

3.3 关于更新操作

package com.atguigu.mp.test;
public class TestMP {
	private ApplicationContext ioc = 
				new ClassPathXmlApplicationContext("applicationContext.xml");
	private EmployeeMapper employeeMapper = 
			ioc.getBean("employeeMapper",EmployeeMapper.class);
    /**
	 * 通用 更新操作
	 */
	@Test
	public void testCommonUpdate() {
		//初始化修改对象
		Employee employee = new Employee();
		employee.setId(7);
		employee.setLastName("小泽老师");
		employee.setEmail("xz@sina.com");
		employee.setGender(0);
		//employee.setAge(33);
		
		//Integer result = employeeMapper.updateById(employee);
		Integer result = employeeMapper.updateAllColumnById(employee);
		System.out.println("result: " + result );
	}
}

3.4 查询操作

package com.atguigu.mp.test;
public class TestMP {
	private ApplicationContext ioc = 
				new ClassPathXmlApplicationContext("applicationContext.xml");
	private EmployeeMapper employeeMapper = 
			ioc.getBean("employeeMapper",EmployeeMapper.class);
    /**
	 * 通用 查询操作
	 */
	@Test
	public void  testCommonSelect() {
		//1. 通过id查询
//		Employee employee = employeeMapper.selectById(7);
//		System.out.println(employee);
		
		//2. 通过多个列进行查询    id  +  lastName
//		Employee  employee = new Employee();
//		//employee.setId(7);
//		employee.setLastName("小泽老师");
//		employee.setGender(0);
//		
//		Employee result = employeeMapper.selectOne(employee);
//		System.out.println("result: " +result );
		
		
		//3. 通过多个id进行查询    <foreach>
//		List<Integer> idList = new ArrayList<>();
//		idList.add(4);
//		idList.add(5);
//		idList.add(6);
//		idList.add(7);
//		List<Employee> emps = employeeMapper.selectBatchIds(idList);
//		System.out.println(emps);
		
		//4. 通过Map封装条件查询
//		Map<String,Object> columnMap = new HashMap<>();
//		columnMap.put("last_name", "Tom");
//		columnMap.put("gender", 1);
//		
//		List<Employee> emps = employeeMapper.selectByMap(columnMap);
//		System.out.println(emps);
		
		//5. 假分页查询,java处理的,没得limit
		List<Employee> emps = employeeMapper.selectPage(new Page<>(3, 2), null);
		System.out.println(emps);
	}
}

**说明:**官方文档没写通用查询的方法,需要先学条件构造器,再看官方的文档。
3.5 删除操作

package com.atguigu.mp.test;
public class TestMP {
	private ApplicationContext ioc = 
				new ClassPathXmlApplicationContext("applicationContext.xml");
	private EmployeeMapper employeeMapper = 
			ioc.getBean("employeeMapper",EmployeeMapper.class);
/**
	 * 通用 删除操作
	 */
	@Test
	public void testCommonDelete() {
		//1 .根据id进行删除
		Integer result = employeeMapper.deleteById(13);
		System.out.println("result: " + result );
		//2. 根据 条件进行删除
		Map<String,Object> columnMap = new HashMap<>();
		columnMap.put("last_name", "MP");
		columnMap.put("email", "mp@atguigu.com");
		Integer result = employeeMapper.deleteByMap(columnMap);
		System.out.println("result: " + result );
		
		//3. 批量删除
//		List<Integer> idList = new ArrayList<>();
//		idList.add(3);
//		idList.add(4);
//		idList.add(5);
//		Integer result = employeeMapper.deleteBatchIds(idList);
//		System.out.println("result: " + result );
	}	
}```

3.6 CRUD小结

(1) 以上是基本的CRUD操作,如您所见,我们仅仅需要继承一个BaseMapper 即可实现大部分单表CRUD 操作。BaseMapper提供了多达17个方法给大家使用, 可以极其方便的实现单一、批量、分页等操作。极大的减少开发负担,难道这就是MP的强大之处了吗?
(2) 提出需求:
现有一个需求,我们需要分页查询tbl_employee表中,年龄在18~50之间性别为男且姓名为xx的所有用户,这时候我们该如何实现上述需求呢?
MyBatis : 需要在SQL映射文件中编写带条件查询的SQL,并基于PageHelper插件完成分页. 实现以上一个简单的需求,往往需要我们做很多重复单调的工作。普通的Mapper能够解决这类痛点吗?
MP: 依旧不用编写SQL语句, MP提供了功能强大的条件构造器EntityWrapper

4、条件构造器

4.1 简介

  1. Mybatis-Plus 通过EntityWrapper(简称EW,MP封装的一个查询条件构造器)或者Condition(与EW类似)来让用户自由的构建查询条件,简单便捷,没有额外的负担,能够有效提高开发效率
  2. 实体包装器,主要用于处理sql 拼接,排序,实体参数查询等
    注意: 使用的是数据库字段,不是Java属性!
  3. 条件参数说明:
    在这里插入图片描述
    4.2 完成上文需求
    上文的需求:我们需要分页查询tbl_employee表中,年龄在18~50之间性别为男且姓名为xx的所有用户,这时候我们该如何实现上述需求呢?
package com.atguigu.mp.test;

public class TestMP {
	private ApplicationContext ioc = 
				new ClassPathXmlApplicationContext("applicationContext.xml");
	
	private EmployeeMapper employeeMapper = 
			ioc.getBean("employeeMapper",EmployeeMapper.class);
/**
	 * 条件构造器   查询操作
	 */
	@Test
	public void testEntityWrapperSelect() {
		List<Employee > emps = employeeMapper.selectPage(
							new Page<Employee>(1,2), 
							Condition.create()
							.between("age", 18, 50)
							.eq("gender", "1")
							.eq("last_name", "Tom")
							);
		System.out.println(emps);		
	}
}

4.3 带条件的查询
(1)我们需要分页查询tbl_employee表中,年龄在18~50之间且性别为男且姓名为Tom的所有用户

package com.atguigu.mp.test;

public class TestMP {
	private ApplicationContext ioc = 
				new ClassPathXmlApplicationContext("applicationContext.xml");
	
	private EmployeeMapper employeeMapper = 
			ioc.getBean("employeeMapper",EmployeeMapper.class);
/**
	 * 条件构造器   查询操作
	 */
	@Test
	public void testEntityWrapperSelect() {
		List<Employee> emps =employeeMapper.selectPage(new Page<Employee>(1, 2),
					new EntityWrapper<Employee>()
					.between("age", 18, 50)
					.eq("gender", 1)
					.eq("last_name", "Tom")
				);
		System.out.println(emps);
	}
}

(2)查询tbl_employee表中, 性别为女并且名字中带有"老师" 或者 邮箱中带有"a"

package com.atguigu.mp.test;

public class TestMP {
	private ApplicationContext ioc = 
				new ClassPathXmlApplicationContext("applicationContext.xml");
	
	private EmployeeMapper employeeMapper = 
			ioc.getBean("employeeMapper",EmployeeMapper.class);
/**
	 * 条件构造器   查询操作
	 */
	@Test
	public void testEntityWrapperSelect() {		
		List<Employee> emps = employeeMapper.selectList(
				new EntityWrapper<Employee>()
				.eq("gender", 0)
				.like("last_name", "老师")
				//.or()    // SQL: (gender = ? AND last_name LIKE ? OR email LIKE ?)    
//				.orNew()   // SQL: (gender = ? AND last_name LIKE ?) OR (email LIKE ?) 
				.like("email", "a")
				);
		System.out.println(emps);
	}
}

(3)查询性别为女的, 根据age进行排序(asc/desc), 简单分页

package com.atguigu.mp.test;

public class TestMP {
	private ApplicationContext ioc = 
				new ClassPathXmlApplicationContext("applicationContext.xml");
	
	private EmployeeMapper employeeMapper = 
			ioc.getBean("employeeMapper",EmployeeMapper.class);
/**
	 * 条件构造器   查询操作
	 */
	@Test
	public void testEntityWrapperSelect() {
		List<Employee> emps  = employeeMapper.selectList(
				new EntityWrapper<Employee>()
				.eq("gender", 0)
				.orderBy("age")
				//.orderDesc(Arrays.asList(new String [] {"age"}))
				.last("desc limit 1,3")
				);
		System.out.println(emps);
	}
}

4.4 带条件的修改

public class TestMP {
	private ApplicationContext ioc = 
				new ClassPathXmlApplicationContext("applicationContext.xml");
	private EmployeeMapper employeeMapper = 
			ioc.getBean("employeeMapper",EmployeeMapper.class);
    /**
	 * 条件构造器  修改操作
	 */
	@Test
	public void testEntityWrapperUpdate() {
		Employee employee = new Employee();
		employee.setLastName("苍老师");
		employee.setEmail("cls@sina.com");
		employee.setGender(0);
		employeeMapper.update(employee, 
					new EntityWrapper<Employee>()
					.eq("last_name", "Tom")
					.eq("age", 44)
					);
	}
}

4.5 带条件的删除

package com.atguigu.mp.test;
public class TestMP {
	private ApplicationContext ioc = 
				new ClassPathXmlApplicationContext("applicationContext.xml");
	private EmployeeMapper employeeMapper = 
			ioc.getBean("employeeMapper",EmployeeMapper.class);
	/**
	 * 条件构造器  删除操作
	 */
	@Test
	public void testEntityWrapperDelete() {
		employeeMapper.delete(
					new EntityWrapper<Employee>()
					.eq("last_name", "Tom")
					.eq("age", 22)
				);
	}
}

5、 ActiveRecord(活动记录)

代码mp02文件夹
(1)ActiveRecord(活动记录),是一种领域模型模式,特点是一个模型类对应关系型数据库中的一个表,而模型类的一个实例对应表中的一行记录。
(2)ActiveRecord 一直广受动态语言(PHP 、Ruby 等)的喜爱,而Java 作为准静态语言,对于ActiveRecord 往往只能感叹其优雅,所以MP也在AR道路上进行了一定的探索
5.1 如何使用AR模式
仅仅需要让实体类继承Model类且实现主键指定方法,即可开启AR之旅

package com.atguigu.mp.beans;
/*
 * MybatisPlus会默认使用实体类的类名到数据中找对应的表. 
 * 
 */
public class Employee extends Model<Employee> {
	private Integer id ;   //  int 
	private String  lastName; 
	private String  email ;
	private Integer gender; 
	private Integer age ;
	
	@TableField(exist=false)
	private Double salary ; 
	
	public Double getSalary() {
		return salary;
	}
/**
	 * 指定当前实体类的主键属性
	 */
	@Override
	protected Serializable pkVal() {
		return id;
	} 
	\\get、set、toString等方法
}

5.2 AR的CRUD

package com.atguigu.mp.test;

public class TestMP {
	private ApplicationContext ioc = 
				new ClassPathXmlApplicationContext("applicationContext.xml");
	/**
	 * AR  分页复杂操作
	 */
	@Test
	public void  testARPage() {
		Employee employee = new Employee();
		Page<Employee> page = employee.selectPage(new Page<>(1, 1), 
				new EntityWrapper<Employee>().like("last_name", "老"));
		List<Employee> emps = page.getRecords();
		System.out.println(emps);
	}
	/**
	 * AR 删除操作
	 * 
	 * 注意: 删除不存在的数据 逻辑上也是属于成功的. 
	 */
	@Test
	public void testARDelete() {
		Employee employee = new Employee();
		//boolean result = employee.deleteById(2);
//		employee.setId(2);
//		boolean result = employee.deleteById();
//		System.out.println("result:" +result );
		boolean result = employee.delete(new EntityWrapper<Employee>().like("last_name", "小"));
		System.out.println(result );
	}
	/**
	 * AR 查询操作
	 */
	@Test
	public void testARSelect() {
		Employee employee = new Employee();
		//Employee result = employee.selectById(14);
//		employee.setId(14);
//		Employee result = employee.selectById();
//		System.out.println(result );
		
		
//		List<Employee> emps = employee.selectAll();
//		System.out.println(emps);
		
//		List<Employee > emps= 
//				employee.selectList(new EntityWrapper<Employee>().like("last_name", "老师"));
//		System.out.println(emps);
		
		Integer result = employee.selectCount(new EntityWrapper<Employee>().eq("gender", 0));
		System.out.println("result: " +result );
	}
	/**
	 * AR 修改操作
	 */
	@Test
	public void testARUpdate() {
		Employee employee = new Employee();
		employee.setId(20);
		employee.setLastName("宋老湿");
		employee.setEmail("sls@atguigu.com");
		employee.setGender(1);
		employee.setAge(36);
		boolean result = employee.updateById();
		System.out.println("result:" +result );
	}
	/**
	 * AR  插入操作
	 */
	@Test
	public void  testARInsert() {
		Employee employee = new Employee();
		employee.setLastName("宋老师");
		employee.setEmail("sls@atguigu.com");
		employee.setGender(1);
		employee.setAge(35);
		
		boolean result = employee.insert();
		System.out.println("result:" +result );
	}
}

5.3 AR小结

  1. AR 模式提供了一种更加便捷的方式实现CRUD 操作,其本质还是调用的Mybatis 对应的方法,类似于语法糖
    语法糖是指计算机语言中添加的某种语法,这种语法对原本语言的功能并没有影响。可以更方便开发者使用,可以避免出错的机会,让程序可读性更好。
  2. 到此,我们简单领略了Mybatis-Plus 的魅力与高效率,值得注意的一点是:我们提供了强大的代码生成器,可以快速生成各类代码,真正的做到了即开即用。

6、代码生成器

https://mp.baomidou.com/guide/generator.html#%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B
代码文件夹:mp03
6.1 简介:

  1. MP提供了大量的自定义设置,生成的代码完全能够满足各类型的需求
  2. MP的代码生成器和Mybatis MBG 代码生成器:
    MP的代码生成器都是基于java代码来生成。MBG基于xml文件进行代码生成
    MyBatis的代码生成器可生成: 实体类、Mapper接口、Mapper映射文件
    MP的代码生成器可生成: 实体类(可以选择是否支持AR)、Mapper接口、Mapper映射文件、Service层、Controller层.
  3. 表及字段命名策略选择
    在MP中,我们建议数据库表名和表字段名采用驼峰命名方式,如果采用下划线命名方式请开启全局下划线开关,如果表名字段名命名方式不一致请注解指定,我们建议最好保持一致。这么做的原因是为了避免在对应实体类时产生的性能损耗,这样字段不用做映射就能直接和实体类对应。当然如果项目里不用考虑这点性能损耗,那么你采用下滑线也是没问题的,只需要在生成代码时配置dbColumnUnderline属性就可以

6.2 代码生成器的依赖
MyBatis-Plus 从 3.0.3 之后移除了代码生成器与模板引擎的默认依赖,需要手动添加相关依赖:
(1)用maven添加代码生成器依赖

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-generator</artifactId>
    <version>3.4.2</version>
</dependency>

(2)Maven添加生成器的模板引擎

<dependency>
    <groupId>org.apache.velocity</groupId>
    <artifactId>velocity-engine-core</artifactId>
    <version>2.3</version>
</dependency>

(3)加入slf4j,查看日志输出信息(不晓得为啥又换日志看框架)

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.7</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.7</version>
</dependency>

6.3 MP代码生成器示例代码

package com.atguigu.mp.test;
public class TestMP {
	/**
	 * 代码生成    示例代码
	 */
	@Test
	public void  testGenerator() {
		//1. 全局配置
		GlobalConfig config = new GlobalConfig();
		config.setActiveRecord(true) // 是否支持AR模式
			  .setAuthor("weiyunhui") // 作者
			 .setOutputDir("D:\\workspace_mp\\mp03\\src\\main\\java") // 生成路径
			  .setFileOverride(true)  // 文件覆盖
			  .setIdType(IdType.AUTO) // 主键策略
			  .setServiceName("%sService")  // 设置生成的service接口的名字的首字母是否为I
			  					   // IEmployeeService
 			  .setBaseResultMap(true)
 			  .setBaseColumnList(true);
		
		//2. 数据源配置
		DataSourceConfig  dsConfig  = new DataSourceConfig();
		dsConfig.setDbType(DbType.MYSQL)  // 设置数据库类型
				.setDriverName("com.mysql.jdbc.Driver")
				.setUrl("jdbc:mysql://localhost:3306/mp")
				.setUsername("root")
				.setPassword("1234");
		 
		//3. 策略配置
		StrategyConfig stConfig = new StrategyConfig();
		stConfig.setCapitalMode(true) //全局大写命名
				.setDbColumnUnderline(true)  // 指定表名 字段名是否使用下划线
				.setNaming(NamingStrategy.underline_to_camel) // 数据库表映射到实体的命名策略
				.setTablePrefix("tbl_")
				.setInclude("tbl_employee");  // 生成的表
		
		//4. 包名策略配置 
		PackageConfig pkConfig = new PackageConfig();
		pkConfig.setParent("com.atguigu.mp")
				.setMapper("mapper")
				.setService("service")
				.setController("controller")
				.setEntity("beans")
				.setXml("mapper");
		
		//5. 整合配置
		AutoGenerator  ag = new AutoGenerator();
		ag.setGlobalConfig(config)
		  .setDataSource(dsConfig)
		  .setStrategy(stConfig)
		  .setPackageInfo(pkConfig);
		
		//6. 执行
		ag.execute();
	}	
}

**说明:**这个生成器在按照MVC的规范生成代码,DAO(xxxmapper就在幕后了,封装在service层里面,service层出面增删改查)
在spring配置文件中,注入配置

<beans>
<!--  配置SqlSessionFactoryBean 
		Mybatis提供的: org.mybatis.spring.SqlSessionFactoryBean
		MP提供的:com.baomidou.mybatisplus.spring.MybatisSqlSessionFactoryBean
	 -->
	<bean id="sqlSessionFactoryBean" class="com.baomidou.mybatisplus.spring.MybatisSqlSessionFactoryBean">
		<!-- 数据源 -->
		<property name="dataSource" ref="dataSource"></property>
		<property name="configLocation" value="classpath:mybatis-config.xml"></property>
		<!-- 别名处理 -->
		<property name="typeAliasesPackage" value="com.atguigu.mp.beans"></property>		
		
		<!-- 注入全局MP策略配置 -->
		<property name="globalConfig" ref="globalConfiguration"></property>
		
	</bean>
	
	<!-- 定义MybatisPlus的全局策略配置-->
	<bean id ="globalConfiguration" class="com.baomidou.mybatisplus.entity.GlobalConfiguration">
		<!-- 在2.3版本以后,dbColumnUnderline 默认值就是true -->
		<property name="dbColumnUnderline" value="true"></property>
		
		<!-- 全局的主键策略 -->
		<property name="idType" value="0"></property>
		
		<!-- 全局的表前缀策略配置 -->
		<property name="tablePrefix" value="tbl_"></property>
	</bean>
</beans>

生成好代码以后,可以去controller里面试试,效果。

package com.atguigu.mp.controller;
/**
 * <p>
 *  前端控制器
 * </p>
 *
 * @author weiyunhui
 * @since 2018-06-21
 */
@Controller
@RequestMapping("/employee")
public class EmployeeController {
	@Autowired
	private EmployeeService employeeService; 
	public String  login() {		
	  employeeService.select
	}
}

7、插件扩展

代码:mp04
7.1 插件机制简介
(1) 插件机制:
Mybatis 通过插件(Interceptor) 可以做到拦截四大对象相关方法的执行,根据需求,完成相关数据的动态改变。
Executor
StatementHandler
ParameterHandler
ResultSetHandler
(2)插件原理
四大对象的每个对象在创建时,都会执行interceptorChain.pluginAll(),会经过每个插件对象的plugin()方法,目的是为当前的四大对象创建代理。代理对象就可以拦截到四大对象相关方法的执行,因为要执行四大对象的方法需要经过代理.
7.2 三大插件:分页插件,执行分析插件,性能分析插件,乐观锁插件
7.2.1 插件注册进spring
插件还是可以直接用spring的配置文件applicationContext.xml中进行配置 https://mp.baomidou.com/guide/page.html

<!-- spring xml 方式 -->
<bean id="sqlSessionFactoryBean" class="com.baomidou.mybatisplus.spring.MybatisSqlSessionFactoryBean">
		<!-- 数据源 -->
		<property name="dataSource" ref="dataSource"></property>
		<property name="configLocation" value="classpath:mybatis-config.xml"></property>
		<!-- 别名处理 -->
		<property name="typeAliasesPackage" value="com.atguigu.mp.beans"></property>		
		
		<!-- 注入全局MP策略配置 -->
		<property name="globalConfig" ref="globalConfiguration"></property>
		
		<!-- 插件注册 -->
		<property name="plugins">
			<list>
				<!-- 注册分页插件 -->
				<bean class="com.baomidou.mybatisplus.plugins.PaginationInterceptor"></bean>
				
				<!-- 注册执行分析插件 -->
				<bean class="com.baomidou.mybatisplus.plugins.SqlExplainInterceptor">
					<property name="stopProceed" value="true"></property>
				</bean>
				
				<!-- 注册性能分析插件 -->
				<bean class="com.baomidou.mybatisplus.plugins.PerformanceInterceptor">
					<property name="format" value="true"></property>
					<!-- <property name="maxTime" value="5"></property> -->
				</bean>
				
				<!-- 注册乐观锁插件 -->
				<bean class="com.baomidou.mybatisplus.plugins.OptimisticLockerInterceptor">
				</bean>
			</list>
		</property>
	</bean>

7.2.2 分页插件

package com.atguigu.mp.test;

public class TestMP {
	ApplicationContext ctx  = new ClassPathXmlApplicationContext("applicationContext.xml");
	EmployeeMapper employeeMapper = ctx.getBean("employeeMapper",EmployeeMapper.class);

/**
	 * 测试分页插件
	 */
	@Test
	public void testPage() {
		
		Page<Employee> page = new Page<>(1,1);
		List<Employee > emps = 
				employeeMapper.selectPage(page, null);
		System.out.println(emps);
		System.out.println("===============获取分页相关的一些信息======================");
		System.out.println("总条数:" +page.getTotal());
		System.out.println("当前页码: "+  page.getCurrent());
		System.out.println("总页码:" + page.getPages());
		System.out.println("每页显示的条数:" + page.getSize());
		System.out.println("是否有上一页: " + page.hasPrevious());
		System.out.println("是否有下一页: " + page.hasNext());
		//将查询的结果封装到page对象中
		page.setRecords(emps);
	}
}

7.2.3 执行分析插件

  1. com.baomidou.mybatisplus.plugins.SqlExplainInterceptor
  2. SQL执行分析拦截器,只支持MySQL5.6.3以上版本
  3. 该插件的作用是分析DELETE UPDATE语句,防止小白或者恶意进行DELETE UPDATE全表操作
  4. 只建议在开发环境中使用,不建议在生产环境使用
  5. 在插件的底层通过SQL语句分析命令:Explain 分析当前的SQL语句,根据结果集中的Extra列来断定当前是否全表操作。
package com.atguigu.mp.test;

public class TestMP {
	ApplicationContext ctx  = new ClassPathXmlApplicationContext("applicationContext.xml");
	EmployeeMapper employeeMapper = ctx.getBean("employeeMapper",EmployeeMapper.class);
/**
	 * 测试SQL执行分析插件
	 */
	@Test
	public void testSQLExplain() {
		employeeMapper.delete(null);  // 全表删除
	}
}

7.2.4 性能分析插件

  1. com.baomidou.mybatisplus.plugins.PerformanceInterceptor
  2. 性能分析拦截器,用于输出每条SQL语句及其执行时间
  3. SQL性能执行分析,开发环境使用,超过指定时间,停止运行。有助于发现问题
package com.atguigu.mp.test;

public class TestMP {
	ApplicationContext ctx  = new ClassPathXmlApplicationContext("applicationContext.xml");
	EmployeeMapper employeeMapper = ctx.getBean("employeeMapper",EmployeeMapper.class);
/**
	 * 测试 性能分析插件
	 */
	@Test
	public void testPerformance() {
		Employee employee = new Employee();
		employee.setLastName("玛利亚老师");
		employee.setEmail("mly@sina.com");
		employee.setGender("0");
		employee.setAge(22);
		
		employeeMapper.insert(employee);
		
	}
}

**说明:**配置好性能分析插件,控制台就可以看到该语句的性能分析了
7.2.5 乐观锁插件

  1. com.baomidou.mybatisplus.plugins.OptimisticLockerInterceptor
  2. 如果想实现如下需求: 当要更新一条记录的时候,希望这条记录没有被别人更新
  3. 乐观锁的实现原理:
    取出记录时,获取当前version 2
    更新时,带上这个version 2
    执行更新时,set version = yourVersion+1 where version = yourVersion
    如果version不对,就更新失败
  4. @Version 用于注解实体字段,必须要有。
package com.atguigu.mp.test;

public class TestMP {
	
	ApplicationContext ctx  = new ClassPathXmlApplicationContext("applicationContext.xml");
	
	EmployeeMapper employeeMapper = ctx.getBean("employeeMapper",EmployeeMapper.class);
	
	/**
	 * 测试 乐观锁插件
	 */
	@Test
	public void testOptimisticLocker() {
		//更新操作
		Employee employee = new Employee();
		employee.setId(15);
		employee.setLastName("TomAA");
		employee.setEmail("tomAA@sina.com");
		employee.setGender("1");
		employee.setAge(22);
		employee.setVersion(3);
		employeeMapper.updateById(employee);
	}
}

8、自定义全局操作

代码:mp05
就是让你自己写的sql语句和mp的默认sql语句(basemapper中的sql语句)一起启动,就不需要写映射文件了

根据MybatisPlus 的AutoSqlInjector可以自定义各种你想要的sql ,注入到全局中,相当于自定义Mybatisplus 自动注入的方法。

之前需要在xml中进行配置的SQL语句,现在通过扩展AutoSqlInjector 在加载mybatis环境时就注入。

8.1 AutoSqlInjector
自定义需要添加进全局的SQL语句

  1. 在Mapper接口中定义相关的CRUD方法
package com.atguigu.mp.mapper;
/**
除了基本的BaseMapper里面的sql语句对应的方法外,新增一个deleteAll()
 */
public interface EmployeeMapper extends BaseMapper<Employee> {
	int  deleteAll();
}
  1. 扩展AutoSqlInjector inject 方法,实现Mapper接口中方法要注入的SQL
package com.atguigu.mp.injector;
/**
 * 自定义全局操作
 */
public class MySqlInjector  extends AutoSqlInjector{
	/**
	 * 扩展inject 方法,完成自定义全局操作
	 */
	@Override
	public void inject(Configuration configuration, MapperBuilderAssistant builderAssistant, Class<?> mapperClass,
			Class<?> modelClass, TableInfo table) {
		//将EmployeeMapper中定义的deleteAll, 处理成对应的MappedStatement对象,加入到configuration对象中。
		
		//注入的SQL语句
		String sql = "delete from " +table.getTableName();
		//注入的方法名   一定要与EmployeeMapper接口中的方法名一致
		String method = "deleteAll" ;
		//构造SqlSource对象
		SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
		//构造一个删除的MappedStatement
		this.addDeleteMappedStatement(mapperClass, method, sqlSource);
	}
}

3) 在MP全局策略中,配置自定义注入器
进入spring配置文件applicationContext.xml中
总结:
全局操作 注入 全局策略配置 注入 sqlSessionFactoryBean

<!--  配置SqlSessionFactoryBean 
		Mybatis提供的: org.mybatis.spring.SqlSessionFactoryBean
		MP提供的:com.baomidou.mybatisplus.spring.MybatisSqlSessionFactoryBean
	 -->
	<bean id="sqlSessionFactoryBean" class="com.baomidou.mybatisplus.spring.MybatisSqlSessionFactoryBean">
		<!-- 数据源 -->
		<property name="dataSource" ref="dataSource"></property>
		<property name="configLocation" value="classpath:mybatis-config.xml"></property>
		<!-- 别名处理 -->
		<property name="typeAliasesPackage" value="com.atguigu.mp.beans"></property>		
		<!-- 注入全局MP策略配置 -->
		<property name="globalConfig" ref="globalConfiguration"></property>
  </bean>
  <!-- 定义MybatisPlus的全局策略配置-->
	<bean id ="globalConfiguration" class="com.baomidou.mybatisplus.entity.GlobalConfiguration">
		<!-- 在2.3版本以后,dbColumnUnderline 默认值就是true -->
		<property name="dbColumnUnderline" value="true"></property>
		<!-- Mysql 全局的主键策略 -->
		<property name="idType" value="0"></property> 
		<!-- 全局的表前缀策略配置 -->
		<property name="tablePrefix" value="tbl_"></property>
		
		<!--注入自定义全局操作 -->
		<property name="sqlInjector" ref="mySqlInjector"></property>
	</bean>
	<!-- 定义自定义注入器 -->
	<bean id="mySqlInjector" class="com.atguigu.mp.injector.MySqlInjector"></bean>

4)测试效果

package com.atguigu.mp.test;
public class TestMP {
	
	ApplicationContext ctx  = new ClassPathXmlApplicationContext("applicationContext.xml");
	
	EmployeeMapper employeeMapper = ctx.getBean("employeeMapper",EmployeeMapper.class);
	/**
	 * 测试自定义全局操作
	 */
	@Test
	public void  testMySqlInjector() {
		Integer result = employeeMapper.deleteAll();
		System.out.println("result: " +result );
	}
}

8.2 逻辑删除
使用别人给你写好的逻辑删除
假删除、逻辑删除: 并不会真正的从数据库中将数据删除掉,而是将当前被删除的这条数据中的一个逻辑删除字段置为删除状态.
tbl_user logic_flag = 1 —> -1

使用方法:

  1. com.baomidou.mybatisplus.mapper.LogicSqlInjector
  2. logicDeleteValue 逻辑删除全局值
  3. logicNotDeleteValue 逻辑未删除全局值
  4. 在POJO的逻辑删除字段添加@TableLogic注解
package com.atguigu.mp.beans;

public class User extends Parent {
	@TableId(type=IdType.INPUT)
	private Integer id  ;
	@TableField(fill=FieldFill.INSERT_UPDATE)
	private String name ;
	@TableLogic   // 逻辑删除属性
	private Integer logicFlag ;
	public Integer getId() {
		return id;
	}
	//get、get、toString()
}

spring配置文件中:
逻辑操作 注入 全局策略配置 注入 sqlSessionFactoryBean

<!--  配置SqlSessionFactoryBean 
		Mybatis提供的: org.mybatis.spring.SqlSessionFactoryBean
		MP提供的:com.baomidou.mybatisplus.spring.MybatisSqlSessionFactoryBean
	 -->
	<bean id="sqlSessionFactoryBean" class="com.baomidou.mybatisplus.spring.MybatisSqlSessionFactoryBean">
		<!-- 数据源 -->
		<property name="dataSource" ref="dataSource"></property>
		<property name="configLocation" value="classpath:mybatis-config.xml"></property>
		<!-- 别名处理 -->
		<property name="typeAliasesPackage" value="com.atguigu.mp.beans"></property>		
		<!-- 注入全局MP策略配置 -->
		<property name="globalConfig" ref="globalConfiguration"></property>
  </bean>
  <!-- 定义MybatisPlus的全局策略配置-->
	<bean id="globalConfiguration" class="com.baomidou.mybatisplus.entity.GlobalConfiguration">
		<!-- 在2.3版本以后,dbColumnUnderline 默认值就是true -->
		<property name="dbColumnUnderline" value="true"></property>
		<!-- Mysql 全局的主键策略 -->
		<property name="idType" value="0"></property> 
		<!-- 全局的表前缀策略配置 -->
		<property name="tablePrefix" value="tbl_"></property>
		
		<!-- 注入逻辑删除全局值 -->
	 	<property name="logicDeleteValue" value="-1"></property>
	 	<property name="logicNotDeleteValue" value="1"></property>
	</bean>
	<!-- 逻辑删除 -->
	<bean id="logicSqlInjector" class="com.baomidou.mybatisplus.mapper.LogicSqlInjector"></bean>
  1. 会在mp自带查询和更新方法的sql后面,追加『逻辑删除字段』=『LogicNotDeleteValue默认值』删除方法: deleteById()和其他delete方法, 底层SQL调用的是update tbl_xxx set 『逻辑删除字段』=『logicDeleteValue默认值』
package com.atguigu.mp.test;
public class TestMP {
	ApplicationContext ctx  = new ClassPathXmlApplicationContext("applicationContext.xml");
	EmployeeMapper employeeMapper = ctx.getBean("employeeMapper",EmployeeMapper.class);
	UserMapper userMapper = ctx.getBean("userMapper",UserMapper.class);
		/**
	 * 测试逻辑删除
	 */
	@Test
	public void testLogicDelete() {
		Integer result = userMapper.deleteById(1);
		System.out.println("result:" +result );
		User user = userMapper.selectById(1);
		System.out.println(user);
	}
}

逻辑删除序列号为1的行,其实物理上没删除,但是再用select语句,已经查不到了。

9、公共字段填充

比如空字段不想为null,我想都填充成 mojiji
9.1 元数据处理器接口简介
(1)com.baomidou.mybatisplus.mapper.MetaObjectHandler
(2)insertFill(MetaObject metaObject)
(3)updateFill(MetaObject metaObject)
(4)metaobject: 元对象. 是Mybatis提供的一个用于更加方便,更加优雅的访问对象的属性,给对象的属性设置值的一个对象. 还会用于包装对象. 支持对Object 、Map、Collection等对象进行包装
本质上metaObject获取对象的属性值或者是给对象的属性设置值,最终是要通过Reflector 获取到属性的对应方法的Invoker, 最终invoke.

9.2 开发步骤

  1. 注解填充字段@TableFile(fill = FieldFill.INSERT) 查看FieldFill
package com.atguigu.mp.beans;

public class User extends Parent {
	@TableId(type=IdType.INPUT)
	private Integer id  ;
	@TableField(fill=FieldFill.INSERT_UPDATE) //注解填充字段
	private String name ;
	}
	//get、get、toString()
}
  1. spring配置文件中自定义公共字段填充处理器
  2. MP全局注入自定义公共字段填充处理器
    填充处理器 注入 全局策略配置 注入 sqlSessionFactoryBean
<!--  配置SqlSessionFactoryBean 
		Mybatis提供的: org.mybatis.spring.SqlSessionFactoryBean
		MP提供的:com.baomidou.mybatisplus.spring.MybatisSqlSessionFactoryBean
	 -->
	<bean id="sqlSessionFactoryBean" class="com.baomidou.mybatisplus.spring.MybatisSqlSessionFactoryBean">
		<!-- 数据源 -->
		<property name="dataSource" ref="dataSource"></property>
		<property name="configLocation" value="classpath:mybatis-config.xml"></property>
		<!-- 别名处理 -->
		<property name="typeAliasesPackage" value="com.atguigu.mp.beans"></property>		
		<!-- 注入全局MP策略配置 -->
		<property name="globalConfig" ref="globalConfiguration"></property>
  </bean>
  <!-- 定义MybatisPlus的全局策略配置-->
	<bean id="globalConfiguration" class="com.baomidou.mybatisplus.entity.GlobalConfiguration">
		<!-- 在2.3版本以后,dbColumnUnderline 默认值就是true -->
		<property name="dbColumnUnderline" value="true"></property>
		<!-- Mysql 全局的主键策略 -->
		<property name="idType" value="0"></property> 
		<!-- 全局的表前缀策略配置 -->
		<property name="tablePrefix" value="tbl_"></property>
		<!-- 注入公共字段填充处理器 -->
	 	<property name="metaObjectHandler" ref="myMetaObjectHandler"></property>
	</bean>
	<!-- 公共字段填充 处理器 -->
	<bean id="myMetaObjectHandler" class="com.atguigu.mp.metaObjectHandler.MyMetaObjectHandler"> </bean>

编写myMetaObjectHandler类继承MetaObjectHandler,说清楚在使用更新或者删除方法时,填充的内容(官网已经更新成实现接口了,这里还是写教材的写法)

package com.atguigu.mp.metaObjectHandler;

import org.apache.ibatis.reflection.MetaObject;

import com.baomidou.mybatisplus.mapper.MetaObjectHandler;

/**
 * 自定义公共字段填充处理器
 */
public class MyMetaObjectHandler extends MetaObjectHandler {
	/**
	 * 插入操作 自动填充
	 */
	@Override
	public void insertFill(MetaObject metaObject) {
		//获取到需要被填充的字段的值
		Object fieldValue = getFieldValByName("name", metaObject);
		if(fieldValue == null) {
			System.out.println("*******插入操作 满足填充条件*********");
			setFieldValByName("name", "weiyunhui", metaObject);
		}
	}
	/**
	 * 修改操作 自动填充
	 */
	@Override
	public void updateFill(MetaObject metaObject) {
		Object fieldValue = getFieldValByName("name", metaObject);
		if(fieldValue == null) {
			System.out.println("*******修改操作 满足填充条件*********");
			setFieldValByName("name", "weiyh", metaObject);
		}
	}
}
package com.atguigu.mp.test;
public class TestMP {
	ApplicationContext ctx  = new ClassPathXmlApplicationContext("applicationContext.xml");
	EmployeeMapper employeeMapper = ctx.getBean("employeeMapper",EmployeeMapper.class);
	UserMapper userMapper = ctx.getBean("userMapper",UserMapper.class);
    /**
	 * 测试公共字段填充
	 */
	@Test
	public void testMetaObjectHandler() {
		User user = new User();
		//user.setName("Tom");
		user.setId(5);
		userMapper.updateById(user);
	}
}

特别注意:官网上面的重写方法也已经更新了,一定要结合源码看。下图是官网案例21-4-3

public class MyMetaObjectHandler implements MetaObjectHandler {

    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("start insert fill ....");
        this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0(推荐使用)
        // 或者
        this.strictUpdateFill(metaObject, "createTime", () -> LocalDateTime.now(), LocalDateTime.class); // 起始版本 3.3.3(推荐)
        // 或者
        this.fillStrategy(metaObject, "createTime", LocalDateTime.now()); // 也可以使用(3.3.0 该方法有bug)
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("start update fill ....");
        this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0(推荐)
        // 或者
        this.strictUpdateFill(metaObject, "updateTime", () -> LocalDateTime.now(), LocalDateTime.class); // 起始版本 3.3.3(推荐)
        // 或者
        this.fillStrategy(metaObject, "updateTime", LocalDateTime.now()); // 也可以使用(3.3.0 该方法有bug)
    }
}

this.strictUpdateFill(metaObject, “updateTime”, LocalDateTime.class, LocalDateTime.now());
第二个参数是实体类的字段属性,第三个是填充内容的类型,第四个是要填充的内容。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值