公共字段自动填充
问题分析
业务表中的公共字段:
而针对于这些字段,我们的赋值方式为:
在新增数据时, 将createTime、updateTime 设置为当前时间, createUser、updateUser设置为当前登录用户ID。
在更新数据时, 将updateTime 设置为当前时间, updateUser设置为当前登录用户ID。问题:代码冗余、不便于后期维护
实现思路
有四个公共字段,需要在新增/更新中进行赋值操作, 具体情况如下:
实现步骤:
- 自定义注解 AutoFill,用于标识需要进行公共字段自动填充的方法
- 自定义切面类 AutoFillAspect,统一拦截加入了 AutoFill 注解的方法,通过反射为公共字段赋值
- 在 Mapper 的方法上加入 AutoFill 注解
技术点:枚举、注解、AOP、反射
java--反射(reflection)_反射 reflection java-CSDN博客
代码开发
•自定义注解 AutoFill
package com.sky.annotation;
import com.sky.enumeration.OperationType;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义注解,用于标识某个方法需要进行功能字段自行填充
* @author 石头
* @version 1.0
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
//数据库操作类型:UPDATE INSERT
OperationType value();
}
Java中自定义注解的使用详解_java自定义注解使用-CSDN博客
自定义切面 AutoFillAspect
package com.sky.aspect;
import com.sky.annotation.AutoFill;
import com.sky.constant.AutoFillConstant;
import com.sky.context.BaseContext;
import com.sky.enumeration.OperationType;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
/**
* 自定义切面类,实现公告字段自动填充处理逻辑
*/
@Aspect
@Component
@Slf4j
public class AutoFillAspect {
/**
* 切入点
*/
@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
public void autoFillPointCut() {}
/**
* 通知 自动填充公共字段
* @param joinPoint
*/
@Before("autoFillPointCut()")
public void autoFill(JoinPoint joinPoint) {
log.info("公共字段自动填充...");
//获取到当前被拦截方法上的数据库操作类型
MethodSignature signature = (MethodSignature)joinPoint.getSignature();//获得方法签名对象
//获得方法上的注解
AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);
//获得注解中的操作类型
OperationType operationType = autoFill.value();
//获取到当前被拦截的方法的参数---实体对象
Object[] args = joinPoint.getArgs();//获取当前目标方法的参数
if (args == null || args.length == 0) { //防止空指针
return;
}
//实体对象
Object entity = args[0];
//准备赋值的数据
LocalDateTime time = LocalDateTime.now();
Long empId = BaseContext.getCurrentId();
//根据当前不同的操作类型,为对应的属性通过反射赋值
if (operationType == OperationType.INSERT) {
//当前执行的是insert操作,为4个字段赋值
try {
//获得set方法对象----Method
Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
//通过反射调用目标对象的方法
setCreateTime.invoke(entity, time);
setUpdateTime.invoke(entity, time);
setCreateUser.invoke(entity, empId);
setUpdateUser.invoke(entity, empId);
} catch (Exception ex) {
log.error("公共字段自动填充失败:{}", ex.getMessage());
}
}else {
//当前执行的是update操作,为2个字段赋值
try {
//获得set方法对象----Method
Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
//通过反射调用目标对象的方法
setUpdateTime.invoke(entity, time);
setUpdateUser.invoke(entity, empId);
} catch (Exception ex) {
log.error("公共字段自动填充失败:{}", ex.getMessage());
}
}
}
}
•在Mapper接口的方法上加入 AutoFill 注解
/**
* 插入数据
* @param category
*/
@AutoFill(OperationType.INSERT)
@Insert("insert into category(type, name, sort, status, create_time, update_time, create_user, update_user)" +
" VALUES" +
" (#{type}, #{name}, #{sort}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})")
void insert(Category category);
/**
* 根据id修改分类
* @param category
*/
@AutoFill(OperationType.UPDATE)
void update(Category category);
•将业务层为公共字段赋值的代码注释掉
功能测试
通过观察控制台输出的SQL来确定公共字段填充是否完成
新增菜品
需求分析和设计
产品原型
业务规则:
- 菜品名称必须是唯一的
- 菜品必须属于某个分类下,不能单独存在
- 新增菜品时可以根据情况选择菜品的口味
- 每个菜品必须对应一张图片
接口设计:
• 根据类型查询分类(已完成)• 文件上传• 新增菜品
•根据类型查询分类
•文件上传
•新增菜品
数据库设计(dish菜品表和dish_flavor口味表):
代码开发
开发文件上传接口:
开发文件上传接口:
开发新增菜品接口:
@Data
public class DishDTO implements Serializable {
private Long id;
//菜品名称
private String name;
//菜品分类id
private Long categoryId;
//菜品价格
private BigDecimal price;
//图片
private String image;
//描述信息
private String description;
//0 停售 1 起售
private Integer status;
//口味
private List<DishFlavor> flavors = new ArrayList<>();
}
开发新增菜品接口:
<!--
useGeneratedKeys:true 表示获取主键值
keyProperty="id" 表示将主键值赋给id属性
-->
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into dish
(status, name, category_id, price, image, description, create_time, update_time, create_user,update_user)
values
(#{status}, #{name}, #{categoryId}, #{price}, #{image}, #{description}, #{createTime}, #{updateTime},#{createUser}, #{updateUser})
</insert>
<insert id="insertBatch">
insert into dish_flavor(dish_id, name, value) values
<foreach collection="flavors" item="dishFlavor" separator=",">
(#{dishFlavor.dishId},#{dishFlavor.name},#{dishFlavor.value})
</foreach>
</insert>
功能测试
可以通过接口文档进行测试,也可以进行前后端联调测试
菜品分页查询
需求分析和设计
产品原型:
业务规则:
- 根据页码展示菜品信息
- 每页展示10条数据
- 分页查询时可以根据需要输入菜品名称、菜品分类、菜品状态进行查询
接口设计:
代码开发
根据菜品分页查询接口定义设计对应的DTO:
@Data
public class DishPageQueryDTO implements Serializable {
private int page;
private int pageSize;
//菜品名称
private String name;
//分类id
private Integer categoryId;
//状态 0表示禁用 1表示启用
private Integer status;
}
根据菜品分页查询接口定义设计对应的VO:
public class DishVO implements Serializable {
private Long id;
//菜品名称
private String name;
//菜品分类id
private Long categoryId;
//菜品价格
private BigDecimal price;
//图片
private String image;
//描述信息
private String description;
//0 停售 1 起售
private Integer status;
//更新时间
private LocalDateTime updateTime;
//分类名称
private String categoryName;
//菜品关联的口味
private List<DishFlavor> flavors = new ArrayList<>();
}
根据接口定义创建DishController的page分页查询方法:
如果前端是通过 URL 的查询字符串(query string)或路径变量(path variables)来传递数据的(比如 GET 请求中的参数),那么在后端就不需要使用 @RequestBody
在 DishService 中扩展分页查询方法:
在 DishServiceImpl 中实现分页查询方法:
在 DishMapper 接口中声明 pageQuery 方法:
在 DishMapper.xml 中编写SQL:
功能测试
可以通过接口文档进行测试,最后完成前后端联调测试即可
删除菜品
需求分析和设计
产品原型:
业务规则:
- 可以一次删除一个菜品,也可以批量删除菜品
- 起售中的菜品不能删除
- 被套餐关联的菜品不能删除
- 删除菜品后,关联的口味数据也需要删除掉
接口设计:
数据库设计:
代码开发
创建SetmealDishMapper,声明getSetmealIdsByDishIds方法,并在xml文件中编写SQL:
在DishMapper.xml中声明deleteById方法并配置SQL:
在DishFlavorMapper中声明deleteByDishId方法并配置SQL:
功能测试
通过Swagger接口文档进行测试,通过后再前后端联调测试即可
修改菜品
需求分析和设计
产品原型:
接口设计:
• 根据 id 查询菜品• 根据类型查询分类(已实现)• 文件上传(已实现)• 修改菜品
根据id查询菜品
修改菜品
代码开发
根据id查询菜品 接口开发:
根据id查询菜品 接口开发:
修改菜品 接口开发:
功能测试
通过Swagger接口文档进行测试,通过后再前后端联调测试即可