二刷苍穹外卖 day03
公共字段自动填充
通过AOP切面编程,实现功能增强,完成对公共字段的自动填充
实现思路:对以下四个字段进行统一处理
实现步骤:
1).自定义注解AutoFill,用于表示需要进行公共字段自动填充的方法
2)自定义切面类AutoFillAspect,统一拦截加入了AutoFill注解的方法,通过反射为公共字段赋值
3).在Mapper的方法上加入AutoFill注解
代码实现
自定义注解AutoFill
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill { OperationType value();
}
@Target(ElementType.MethOD)表明这个注解只能应用在方法上
@Retention(RetentionPolicy.RUNTIME)表明该注解在运行时依然存在,可通过反射获取
OperationType value();
定义了该注解有一个名为value
的属性,类型为OperationType
,使用该注解时需为value
属性赋值。
自定义切面AutoFillAspect
/*** 切入点*/@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")public void autoFillPointCut(){}/*** 前置通知,在通知中进行公共字段的赋值*/@Before("autoFillPointCut()")public void autoFill(JoinPoint joinPoint){/重要//可先进行调试,是否能进入该方法 提前在mapper方法添加AutoFill注解log.info("开始进行公共字段自动填充...");}
execution(* com.sky.mapper.*.*(..))
表示匹配com.sky.mapper
包下所有类的所有方法;&& @annotation(com.sky.annotation.AutoFill)
表示被com.sky.annotation.AutoFill
注解标记的方法才会被匹配。整体意思是,当com.sky.mapper
包下的方法被AutoFill
注解标记时,该切入点匹配。
public void autoFillPointCut() { }
是切入点方法,它本身没有实际的方法体,只是作为切入点的标识。
总结:
AutoFill是“标记”,而AutoFillAspect是"实现逻辑"
AutoFill用于标记目标方法(@Target(ElementType.METHOD)声明注解只能标注在方法上)
通过OperationType value()声明注解需要携带一个OperationType类型的参数
AutoFillAspect是一个切面类:
通过AOP的切点@Pointcut筛选出对应包下能被@AutoFill注解标记的方法
通过注解的操作类型自动填充字段
通过这种方式,只需要在Mapper中确定是插入操作还是更新操作即可统一管理公共字段的赋值逻辑,开发者只需在 Mapper
方法上声明 @AutoFill(OperationType.INSERT)
或 @AutoFill(OperationType.UPDATE)
,切面会自动完成公共字段的赋值,无需手动编写重复代码。
AOP
面向切面编程,在Spring AOP中可以认为是面向方法编程
在不修改目标方法源代码的前提下,底层通过动态代理机制实现对目标方法的编程,动态代理是目前面向方法编程最主流的实现技术
文件上传
定义OSS相关配置
读取OSS配置
@ConfigurationProperties(prefix = "sky.alioss")
:这是 Spring Boot 提供的注解,用于将配置文件中以sky.alioss
为前缀的属性值绑定到该类的对应字段上。比如在application.properties
或application.yml
等配置文件中,sky.alioss.endpoint
的值会被绑定到类中的endpoint
字段。
生成OSS工具类对象
定义文件上传接口
package com.sky.controller.admin; import com.sky.constant.MessageConstant;
import com.sky.result.Result;
import com.sky.utils.AliOssUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import java.io.IOException;
import java.util.UUID; @RestController
@RequestMapping("/admin/common")
@Api(tags = "通用接口")
@Slf4j
public class CommonController { @Autowired private AliOssUtil aliOssUtil; @PostMapping("/upload") @ApiOperation("文件上传") 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); }
}
String originalFilename = file.getOriginalFilename();
:获取文件的原始文件名,并将其存储在originalFilename
变量中。String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
:通过substring
方法从原始文件名中截取文件后缀。lastIndexOf(".")
用于找到文件名中最后一个点号(.)的位置,以此来确定后缀的起始位置,截取的后缀存储在extension
变量中。String objectName = UUID.randomUUID().toString() + extension;
:生成一个唯一的通用唯一识别码(UUID),并将其与文件后缀拼接起来,作为上传文件的名称存储在objectName
变量中。这样做可以确保上传文件的名称具有唯一性,避免文件名冲突。String filePath = aliOssUtil.upload(file.getBytes(), objectName);
:调用aliOssUtil
工具类中的upload
方法,将文件的字节数组(file.getBytes()
)和生成的上传文件名(objectName
)作为参数传入,该方法返回文件上传后的请求路径,并存储在filePath
变量中。这一步实现了将文件上传到指定的存储位置(可能是阿里云对象存储 OSS,由aliOssUtil
工具类决定)。
新增菜品
service层
@Override @Transactionalpublic void saveWithFlavor(DishDTO dishDTO) { Dish dish = new Dish(); BeanUtils.copyProperties(dishDTO, dish); dishMapper.insert(dish); Long dishId = dish.getId(); List<DishFlavor> flavors = dishDTO.getFlavors(); if (flavors != null && flavors.size() > 0) { /** * 如果直接调用 flavors.size() 而不先检查 flavors 是否为 null, * 当 flavors 是 null 时会抛出 NullPointerException(空指针异常), * 导致程序崩溃。 */
// for (DishFlavor flavor : flavors) {
// flavor.setDishId(dishId);
// } flavors.forEach(dishFlavor -> { dishFlavor.setDishId(dishId); }); dishFlavorMapper.insertBatch(flavors); } }
Mapper
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.DishMapper"><insert id="insert" useGeneratedKeys="true" keyProperty="id">insert into dish (name, category_id, price, image, description, create_time, update_time, create_user,update_user, status)values (#{name}, #{categoryId}, #{price}, #{image}, #{description}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser}, #{status})</insert>
</mapper>
useGeneratedKeys=“true” 表示使用数据库自动生成的主键值。当插入数据时,如果数据库支持自动生成主键(如 MySQL 的自增主键),设置此属性为 true 后,MyBatis 会在插入操作执行完毕后,获取数据库生成的主键值。
keyProperty=“id” 表示将获取到的自动生成的主键值,赋值给 Java 对象中的 id 属性。也就是说,假设在 Java 代码中有一个 JavaBean 对象,其中包含 id 这个属性,插入操作完成后,数据库生成的主键值会自动填充到该 JavaBean 对象的 id 属性中。
菜品分页查询
service层
/*** 菜品分页查询** @param dishPageQueryDTO* @return*/public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) {PageHelper.startPage(dishPageQueryDTO.getPage(), dishPageQueryDTO.getPageSize());Page<DishVO> page = dishMapper.pageQuery(dishPageQueryDTO);//后绪步骤实现return new PageResult(page.getTotal(), page.getResult());}
page.getResult()实际上返回的是List集合,DishVO
是单个菜品数据的 “数据模板”,而page.getResult()
是当前页所有菜品数据的 “容器”(列表),两者共同完成了 “分页查询结果的结构化封装”。
删除菜品
Service
@Autowiredprivate SetmealDishMapper setmealDishMapper;/*** 菜品批量删除** @param ids*/@Transactional//事务public void deleteBatch(List<Long> ids) {//判断当前菜品是否能够删除---是否存在起售中的菜品??for (Long id : ids) {Dish dish = dishMapper.getById(id);//后绪步骤实现if (dish.getStatus() == StatusConstant.ENABLE) {//当前菜品处于起售中,不能删除throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);}}//判断当前菜品是否能够删除---是否被套餐关联了??List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(ids);if (setmealIds != null && setmealIds.size() > 0) {//当前菜品被套餐关联了,不能删除throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);}//删除菜品表中的菜品数据for (Long id : ids) {dishMapper.deleteById(id);//后绪步骤实现//删除菜品关联的口味数据dishFlavorMapper.deleteByDishId(id);//后绪步骤实现}}
逻辑:查询所有菜品,是否存在启售?没有启售的再去查询这些菜品有没有被套餐关联,没有才能逐个删除
Mapper
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.SetmealDishMapper"><select id="getSetmealIdsByDishIds" 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>
</mapper>
resultType="java.lang.Long"
定义了单条记录的映射类型,而 MyBatis 会自动将多条记录的Long
对象封装为List<Long>
,因此接口方法返回List<Long>
是合理且 MyBatis 支持的
修改菜品
service
/*** 根据id修改菜品基本信息和对应的口味信息** @param dishDTO*/public void updateWithFlavor(DishDTO dishDTO) {Dish dish = new Dish();BeanUtils.copyProperties(dishDTO, dish);//修改菜品表基本信息dishMapper.update(dish);//删除原有的口味数据dishFlavorMapper.deleteByDishId(dishDTO.getId());//重新插入口味数据List<DishFlavor> flavors = dishDTO.getFlavors();if (flavors != null && flavors.size() > 0) {flavors.forEach(dishFlavor -> {dishFlavor.setDishId(dishDTO.getId());});//向口味表插入n条数据dishFlavorMapper.insertBatch(flavors);}}
先删除原有口味
再插入口味数据
总结
本节主要涉及公共字段自动填充、文件上传以及联表增删改查功能
1.AOP切面编程是什么?
在Spring AOP中可视作面向方法编程
2.@AutoFill的作用是什么?
自定义注解,用于标记需要填充公共字段的Mapper方法,通过在方法上标注并制定操作类型,AOP 切面会拦截并自动完成字段赋值,避免重复代码的书写
3.AOP切面AutoFillAspect如何识别需要自动填充的方法
通过@Pointcut定义切入点表达式:execution(* com.sky.mapper.*.*(..)) && @annotation(AutoFill)
,表示拦截com.sky.mapper
包下所有被@AutoFill
注解标记的方法,从而触发公共字段填充逻辑。
4.useGeneratedKeys="true"的作用
用于获取数据库自动生成的主键,设置后,MyBatis会在插入操作完成后将生成的主键赋值给keyProperty制定的对象方便后续使用
5.OSS 配置类如何与 application.yml 中的参数绑定?
通过@ConfigurationProperties(prefix = "sky.alioss")
注解,Spring 会自动将配置文件中以sky.alioss
为前缀的参数(如endpoint
、accessKeyId
)绑定到配置类的对应字段上。
6.在pageQuery分页查询方法中,Page和page.getResult()是什么含义?
page.getResult()返回的实际上是List集合,DishVo是单个菜品数据,而page.getResult()会返回当前页所有菜品数据以容器(list)的形式实现