前言
该篇博客用于记录苍穹外卖Day03的学习,内容包括公共字段自动填充、菜品管理的开发
注意:可以根据代码上的注释加以理解,很多思路我都用注释写出来了
公共字段填充
如图所示,无论是员工管理、分类管理,还是后面我们要实现的菜品管理等,都有create_time、create_user、update_time、update_user这几个公共字段,每次都要在service层重复编写相同的代码来将这些值设置进去,易造成代码的冗余,而且当数据发生变更时,要一个一个修改,也不易于维护。所以这些公共字段我们要实现它的自动填充。
实现方法:
- 自定义注解AutoFill,用于表示需要进行公共字段自动填充的方法(Insert Update)
//自定义注解,用于表示某个方法需要进行公共字段自动填充功能
@Target(ElementType.METHOD)//作用在方法上
@Retention(RetentionPolicy.RUNTIME)//设置生命周期->运行时使用
public @interface AutoFill {
//数据库操作类型:UPDATE INSERT
OperationType value();
}
- 自定义切面类AutoFillAspect,统一拦截加入了AutoFill注解的方法,通过反射为公共字段赋值
//自定义切面类,实现公共字段自动填充的逻辑
@Aspect
@Slf4j
@Component
public class AutoFillAspect {
//切入点,定义作用范围
//匹配拦截mapper包下的所以类所有方法中 && 加了AutoFill注解的方法
@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
public void autoFillPointCut(){
}
//通过反射进行公共字段赋值
@Before("autoFillPointCut()")
public void autoFill(JoinPoint joinPoint){
log.info("开始进行公共字段自动填充");
//1.获取当前被拦截的方法上的数据库操作类型(insert update)
MethodSignature signature= (MethodSignature) joinPoint.getSignature();//方法签名对象
AutoFill autoFill=signature.getMethod().getAnnotation(AutoFill.class);//获得方法上的注解对象
OperationType operationType=autoFill.value();//获得数据库操作类型
//2.获取当前被拦截的方法的参数--实体对象args
Object[] args= joinPoint.getArgs();
if(args==null||args.length==0){
return;
}
//因为都是默认将要操作的实体对象放在第一个,所以是args[0]
Object entity=args[0];
//3.准备赋值的数据
LocalDateTime now = LocalDateTime.now();//当前时间
Long currentId = BaseContext.getCurrentId();当前操作人id
//4.根据当前不同的操作类型,为对应的属性通过反射来赋值
if(operationType==OperationType.INSERT){
//为四个公共字段赋值
try {
//获取setCreateTime()、setCreateUser()、setUpdateTime()、setUpdateUser()方法
/*
* AutoFillConstant.SET_CREATE_TIME-> "setCreateTime"
AutoFillConstant.SET_CREATE_USER->"setCreateUser"
AutoFillConstant.SET_UPDATE_TIME->"setUpdateTime"
AutoFillConstant.SET_UPDATE_USER->"setUpdateUser"
* */
Method setCreateTime=entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME,LocalDateTime.class);
Method setCreateUser=entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER,Long.class);
Method setUpdateTime=entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME,LocalDateTime.class);
Method setUpdateUser=entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER,Long.class);
//通过反射为对象属性赋值
//调用方法为对象属性赋值
setCreateTime.invoke(entity,now);
setCreateUser.invoke(entity,currentId);
setUpdateTime.invoke(entity,now);
setUpdateUser.invoke(entity,currentId);
} catch (Exception e) {
e.printStackTrace();
}
} else if (operationType==OperationType.UPDATE) {
//为两个公共字段赋值
try {
//获取setUpdateTime()、setUpdateUser()方法
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,now);
setUpdateUser.invoke(entity,currentId);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
- 在mapper层的方法上加上AutoFill(插入语句即第一个注解,更新语句即第二个注解)
@AutoFill(value = OperationType.INSERT)
@AutoFill(value = OperationType.UPDATE)
这样,那些公共属性就不需要我们自己再去手动赋值了
注意:自动填充只是不需要我们在service层手动去设置值,mapper层的SQL语句还是要写将自动填充的值保存到数据库中的逻辑的。相当于我们只是获得了要自动填充的值,这个值我们还是要手动存到数据库的。
菜品管理
文件上传阿里云
要使用阿里云对象存储服务,必须先在application.yml中配置好相关配置,如下所示:
(具体的配置信息我们配置在application-dev-yml中)
alioss:
endpoint: ${sky.alioss.endpoint}
access-key-id: ${sky.alioss.access-key-id}
access-key-secret: ${sky.alioss.access-key-secret}
bucket-name: ${sky.alioss.bucket-name}
这里的access-key-id、access-key-secret是要你要去阿里云OSS服务申请一个AccessKey(记得保存下来),然后就可以获得access-key-id、access-key-secret的值,接下来你就可以去创建一个Bucket,创建时名称可以随便取,读写权限建议设置为公共读,bucket-name就是你创建Bucket时的名称,endpoint是根据你的创建Bucket时的地域决定的(在你的Bucket的概览中即可找到,如下)。
注意:AccessKey我们只需要申请一次,再创建新的项目时需要申请新的Bucket,access-key-id、access-key-secret用原来申请的AccessKey的就好。
下面来分析一下上传文件的具体过程:
-
在application.yml中先配置好配置项,具体的配置信息写在application-dev.yml中
-
接着在AliOssProperties这个类中通过@ConfigurationProperties(prefix = “sky.alioss”)这个注解将配置文件中配置好的四个配置信息内容注入到AliOssProperties类中对应的四个属性内
-
在工具类AliOssUtils中有四个属性对应着配置信息但这四个属性暂时没有内容
-
我们就需要通过配置类(OssConfiguration)的方式将AliOssUtils的四个空的属性赋上值(因为AliOssProperties中有这四个属性的值,我们调用AliOssProperties的get方法将值赋给AliOssUtils创建出AliOssUtils对象)并注入到IOC容器内
-
最后我们就可以在controller层注入AliOssUtils对象并调用它的upload()方法上传文件了
@RestController
@RequestMapping("/admin/common")
@Api(tags = "通用接口")
@Slf4j
public class CommonController {
@Autowired
private AliOssUtil aliOssUtil;
@PostMapping("/upload")
@ApiOperation("文件上传")
//MultipartFile file中因为前端提交的叫做file,所以这里的参数名是file
public Result<String> upload(MultipartFile file){
log.info("文件上传:{}",file);
try {
//原始文件名
String originalFilename= file.getOriginalFilename();
//截取原始文件名后缀
String extension=originalFilename.substring(originalFilename.lastIndexOf("."));
//构造新文件名名称
String objectName= UUID.randomUUID().toString()+extension;
//文件请求路径
String filePath=aliOssUtil.upload(file.getBytes(), objectName);
return Result.success(filePath);
} catch (IOException e) {
log.error("文件上传失败:{}",e);
}
return Result.error(MessageConstant.UPLOAD_FAILED);
}
}
注意:可能会遇到图片不显示的问题,问题可以能是return了错误的结果,或者bucket设置为了私有的。
新增菜品接口
在新增菜品接口,涉及到一个多表的插入操作。在新增一个菜品的时候,由于每个菜品有不同的口味,所以在该接口,我们会插入一条菜品数据以及多条这个菜品的口味数据。
重点分析一下实现类DishServiceImpl中的代码:
//新增菜品和对应的口味
@Transactional//事务注解,菜品和口味插入要么全部成功要么全部失败
public void saveWithFlavor(DishDTO dishDTO) {
Dish dish=new Dish();
//将dishDto的属性赋给dish
BeanUtils.copyProperties(dishDTO,dish);
//向菜品表插1条数据
dishMapper.insert(dish);
//获取insert语句生成的主键值(获取菜品id)。因为是刚插入的数据,直接这样获取不到,需要在XML映射文件的SQL语句中做一些操作
Long dishId=dish.getId();
List<DishFlavor> flavors = dishDTO.getFlavors();
if(flavors!=null&&flavors.size()>0){
for (DishFlavor flavor : flavors) {
//设置口味关联的菜品的id
flavor.setDishId(dishId);
}
//向口味表插多条数据
dishFlavorMapper.insertBatch(flavors);
}
}
- @Transactional注解表示我们插入菜品数据和口味数据的操作要么同时成功,要么同时失败
- 其次在成功插入菜品之后,我们需要获得插入的菜品的id(dish.getId()),将这个id设置进口味,让该菜品与其口味关联起来,但这个方法需要在插入菜品后将这个id返回,我们才能通过dish.getId()获取到这个id,我们需要在SQL语句中加入以下才能成功将id返回
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
...
</insert>
useGeneratedKeys="true"表示我们需要获得插入这条数据后生成的主键值(id),keyProperty="id"表示将这个主键值赋给id这个属性
- 最后我们通过遍历的方式将菜品id设置进口味,然后将多条口味数据插入到数据库中
菜品分页查询
这个接口的SQL语句涉及到一个多表查询的知识点,我们来分析一下。这里要求我们查的是菜品的所有信息以及它所属的分类(分类名),故SQL语句如下:
select
d.*,c.name as categoryName # 指定查询dish表所有的数据和category表的name,起别名categoryName有利于数据的封装
from
dish d # 给dish表起别名d
left outer join # 左外连接,将dish表和category表连接起来
category c # 给category表起别名c
on d.category_id=c.id # 描述连接关系
完整的SQL语句为:
<select id="pageQuery" resultType="com.sky.vo.DishVO">
select d.*,c.name as categoryName from dish d left outer join category c on d.category_id=c.id
<where>
<if test="name!=null">
and d.name like concat('%',#{name},'%') # 对应根据菜品数据搜索
</if>
<if test="categoryId!=null">
and d.category_id=#{categoryId} # 根据菜品分类查询
</if>
<if test="status!=null">
and d.status=#{status} # 对应根据菜品状态搜索
</if>
</where>
order by d.create_time desc
</select>
删除菜品
在删除菜品接口,我们既可以一个一个删除,也可以批量删除,所以在该接口,我们controller层只需传进一个id的集合,即可覆盖一个一个删除和批量删除两种情况。但有两种类型的菜品我们不能删除1.起售中的菜品不能删除 2.被套餐关联的菜品不能删除(下面有重点分析)。所以我们需要在实现类中判断以下菜品能不能删:
@Override
@Transactional
public void deleteByIds(List<Long> ids) {
//判断菜品是否能删除
//1.起售中的菜品不能删除
for (Long id : ids) {
Dish dish=dishMapper.getById(id);
if(dish.getStatus()== StatusConstant.ENABLE){
//菜品处于起售,不能删
throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
}
}
//2.被套餐关联的菜品不能删除
List<Long> setMealIds=setMealDishMapper.getSetMealIdByDishId(ids);
//如果根据菜品id查询出了套餐id,则关联了,不能删
if (setMealIds!=null&&setMealIds.size()>0){
//又被套餐关联的菜品,不能删
throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
}
//两种删除方法
//1.for循环一个一个删除菜品表中的菜品数据
for (Long id : ids) {
dishMapper.deleteById(id);
//删除菜品关联的口味数据
dishFlavorMapper.deleteByDishId(id);
}
//2.动态SQL批量删除菜品数据
dishMapper.deleteByIds(ids);
}
这里重点分析一下被套餐关联的菜品不能删除的实现方法。我们的数据库中有三张表,一张是菜品表,另一个是套餐表,还有一个表示套餐与菜品之间关系的setmeal_dish表,这个表中有两个字段,一个是dish_id,一个是setmael_id,分别与菜品表中的菜品id和套餐表中的套餐id对应,我们只需要获得菜品的id(dish_id),使用这个dish_id去setmeal_dish表中查套餐的id(setmeal_id),查到了说明菜品关联了套餐,不能删。
在SetmealDishMapper中:
//做一些菜品表和套餐表间的多表操作
@Mapper
public interface SetmealDishMapper {
//根据菜品id查询套餐id,查出来的套餐id可能有多个,故使用集合
List<Long> getSetMealIdByDishId(List<Long> dishIds);
}
在映射文件中:
<select id="getSetMealIdByDishId" resultType="java.lang.Long">
select setmeal_id from setmeal_dish where dish_id in
<foreach collection="dishIds" item="dishId" separator="," open="(" close=")">
#{dishId}
</foreach>
</select>
修改菜品
在修改菜品接口,我们需要分别修改菜品的信息以及菜品口味的信息,另外在修改口味的操作中,我们再使用update语句来修改的话面临的情况比较多会比较麻烦,这里可以直接先把原来的口味数据删除再插入新的口味数据,这样也变相达到了一个修改口味数据的操作,所以在DishServiceImpl实现类中:
@Override
public void updateWithFlavor(DishDTO dishDTO) {
Dish dish=new Dish();
BeanUtils.copyProperties(dishDTO,dish);
//修改菜品表
dishMapper.update(dish);
//修改口味表
//1.先删除原有的口味信息
dishFlavorMapper.deleteByDishId(dishDTO.getId());
//2.重新插入口味信息
List<DishFlavor> flavors=dishDTO.getFlavors();
if(flavors!=null&&flavors.size()>0){
for (DishFlavor flavor : flavors) {
//遍历插入关联菜品的id
flavor.setDishId(dishDTO.getId());
}
}
dishFlavorMapper.insertBatch(flavors);
}
那么苍穹外卖第三天的学习就分享完了,感谢您的停留。