苍穹外卖-day03
1.公共字段自动填充
需求分析和设计
❗:多个业务表中出现多个相同字段,这样会使程序中出现大量冗余代码,在日后如果需要修改表结构,则很不便利,需要逐一修改

这些字段只有在
insert,update操作时会去修改,所以只需要通过注解拦截这isnert,update操作,然后给他们自动填充
:解决办法:使用
AOP给指定方法进行拦截,自动修改公共字段,
:实现思路:
- 自定义注解
AutoFill注解, 用于标识需要进行公共字段自动填充的方法 - 自定义切面类
AutoFillAspect,统一拦截加入了AutoFill注解的方法,通过反射为公共字段赋值 - 在
Mapper的方法上加入AutoFill

代码实现
TODO
为什么使用前置通知,
定义注解类
annotation/AutoFill.java
//该注解只能出现在方法上
@Target(ElementType.METHOD)
//表示注解在运行时保留,可以通过反射读取。
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
//使用枚举,指定value的值的范围
OperationType value();
}
定义切面类
aspect/AutoAspect.java
@Aspect//定义切面类
@Component//纳入Bean管理
@Slf4j//记录日志
public class AutoAspect {
/**
* 切入点
*/
@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill))")
public void autoFillPointCut() {
}
/**
* 前置通知,为新增和修改操作的公共字段自动填充
*/
@Before("autoFillPointCut()")
public void autoFill(JoinPoint joinPoint) {
log.info("开始进行公共字段的自动填充");
//获取当前被拦截的方法上的数据库操作类型
OperationType operationType = ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(AutoFill.class).value();
//获取方法的参数
Object[] args = joinPoint.getArgs();
if (args == null || args.length == 0) {
log.error("方法参数为空,无法进行自动填充");
return;
}
//获取当前被拦截的方法的参数--实体对象 ,,这里获取的是方法上的第一个属性对象
Object entity = args[0];
//准备赋值的数据
Long currentId = BaseContext.getCurrentId();
LocalDateTime currentTime = LocalDateTime.now();
//反射获取当前类是否具备设置公共字段的方法
if (operationType == OperationType.INSERT) {
try {
//AutoFillConstant.SET_CREATE_TIME 是用提前创建好的常量避免出错
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, currentTime);
setUpdateTime.invoke(entity, currentTime);
setCreateUser.invoke(entity, currentId);
setUpdateUser.invoke(entity, currentId);
} catch (Exception e) {
log.error("反射设置公共字段失败:{}", e.getMessage());
}
} else if (operationType == OperationType.UPDATE) {
try {
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, currentTime);
setUpdateUser.invoke(entity, currentId);
} catch (Exception e) {
log.error("反射设置公共字段失败:{}", e.getMessage());
}
}
//根据当前不同的操作类,为对应的属性通过反射来赋值
}
}
给
Mapper层的,INSTER,UPDATE,操作加上AutoFill注解
/**
* 插入数据
* @param category
*/
@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})")
@AutoFill(value = OperationType.INSERT)
void insert(Category category);
//... ... 给所有相关方法加上注解
最后删除service层进行填充的冗余代码,还有使用builder构建的对象填充的冗余字段删除 。
功能测试 & 提交代码
测试成功!!!

2.新增菜品
需要分析和设计
接口设计 :
- 根据类型查询分类
- 文件上传
- 新增菜品

数据库设计:
- dish(菜品表)
- dish_flavor(菜品口味表)
逻辑外键:即不是数据库中真正的外键,而是靠java程序维护的外键

代码开发
1.文件上传
1.1接口设计
开发文件上传接口:
关于阿里云oss创建参照黑马程序员2023新版JavaWeb视频p148
什么是OSS:
即:(Object Storage Service)一个阿里云对象存储服务,用户可以通过网络随时存储和调用包括文本,图片,音频,视频等各种文件
- 完成注册
- 配置yaml文件
在aplication-dev设置oss属性,用于个人开发者,便于后期切换生产环境,并在aplication配置中进行变量引用,
oss服务器地址(endpoint),我也找不到在根据所选地域直接找,我选的是华东1(杭州)
https://help.aliyun.com/zh/oss/user-guide/regions-and-endpoints 进入连接查找
oss服务器地址:oss-cn-hangzhou.aliyuncs.com
❗:切记使用外网Endpoint,开始因为使用了内网Endpoiont,导致图片上传不到OSS
- 配置文件
#application.yml
sky:
alioss:
endpoint: ${sky.alioss.endpoint}
bucket-name: ${sky.alioss.bucket-name}
access-key-id: ${sky.alioss.access-key-id}
access-key-secret: ${sky.alioss.access-key-secret}
#aplication-dev.yml
sky:
alioss:
bucket-name: [你自己的oss服务器名字]
endpoint: [你自己的oss服务器地址]
access-key-id: [AccessKey ID]
access-key-secret: [AccessKey Secret]
❗ yml配置oss endpoint的时候记得把前面的https://去掉。
此外:初始工程提供了
com.sky.properties.AliOssProperties这一工具类, 用于将文件上传到 OSS,并返回文件的访问 URL。
config/OssConfiguration在server模块配置类部分进行注入配置
@Bean注解用于标注一个方法,表示该方法将返回一个由 Spring 管理的 bean 实例。详细介绍
1.2代码实现
/**
* 阿里云OSS配置类
* @author Han
* @since
*/
@Component
@Slf4j
public class OssConfiguration {
public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties){
log.info("开始初始化阿里云OSS配置...");
return new AliOssUtil(aliOssProperties.getEndpoint(),aliOssProperties.getAccessKeyId(),aliOssProperties.getAccessKeySecret(),aliOssProperties.getBucketName());
}
}
CommonController文件上传
@RestController
@RequestMapping("/admin/common")
@Slf4j
//Controller
public class CommonController {
@Autowired
private AliOssUtil aliOssUtil;
/**
* 文件上传
* @return
*/
@PostMapping("/upload")
@ApiOperation("文件上传")
public Result<String> upload(MultipartFile file){
log.info("文件上传:{}", file.getOriginalFilename());
try{
//获取原始文件名后缀
String extension = file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf("."));
//用uuid作为文件名,防止生成的临时文件重复
String objectName = UUID.randomUUID().toString() + extension;
//上传文件
String filePath = aliOssUtil.upload(file.getBytes(), objectName);
log.info("图片上传成功:{}",filePath);
return Result.success(filePath);
} catch (IOException e) {
log.error("文件上传失败:{}",e.getMessage());
}
return Result.error(MessageConstant.UPLOAD_FAILED);
}
}
测试成功!
2.新增菜品接口代码开发
2.1接口设计:
使用DishDTO接受前端传给后端的数据

接口设计
| 接口(/admin) | 方法 | 功能 |
|---|---|---|
| /category/list(已完成) | 根据类型查询分类 | |
| /dish/list | get | 根据分类id查询菜品 |
| /upload(已完成) | 上传图片 | |
| /setmeal | post | 新增套餐 |
2.2代码开发
Controller层
@RestController
@RequestMapping("admin/dish")
@Slf4j
@Api(tags = "菜品相关接口")
public class DishController {
@Autowired
private DishService dishService;
@PostMapping
@ApiOperation("新增菜品")
public Result save(@RequestBody DishDTO dishDTO){
dishService.saveWithFlavor(dishDTO);
return Result.success();
}
}
service层
//菜品接口
public interface DishService {
/**
* 新增菜品和对应口味
* @param dishDTO
*/
public void saveWithFlavor(DishDTO dishDTO);
}
//菜品实现类
@Service
public class DishServiceImpl implements DishService {
@Autowired
private DishMapper dishMapper;
@Autowired
private DishFlavorMapper dishFlavorMapper;
/**
* 新增菜品和对应口味
* @param dishDTO
*/
@Override
public void saveWithFlavor(DishDTO dishDTO) {
Dish dish = new Dish();
//使用对象拷贝
BeanUtils.copyProperties(dishDTO,dish);
//掉用mapper,插入一个新菜品
//之前完成了公共字段填充,现在只需要在mapper层上加上注解即可
dishMapper.insert(dish);
//获取菜品id,上面调用的mapper动态sql已经指定了将自增主键返回到实体类的id属性
Long dishId = dish.getId();
/*在新增菜品的时候,还要连同口味一起新增,口味使用集合存储,使用动态sql批量进行插入*/
List<DishFlavor> flavors = dishDTO.getFlavors();
if (flavors != null && flavors.size() > 0 ) {
flavors.forEach(dishFlavor -> {
dishFlavor.setDishId(dishId);
});
}
dishFlavorMapper.insertBatch(flavors);
}
}
❕:(原子性)这里有两个数据库操作同时进行,所以要开始事务,以便数据统一
如果数据库
表dish已经插入数据库,而表dish_flavor没有插入数据库那么会造成数据不完整,不一致的问题关于事务详情:https://blog.csdn.net/MinggeQingchun/article/details/119579941
mapper层
<!--DishMapper.xml-->
<!--useGeneratedKeys="true":让 MyBatis 在插入数据后自动获取数据库生成的主键(如 自增id)。-->
<!-- keyProperty="id" 主要用于获取数据库自动生成的主键值,并将其回填到 Java 对象的 id 属性中。-->
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into dish (name, category_id, price, image, description, status, create_time, update_time, create_user, update_user)
values
(#{name},#{categoryId},#{price},#{image},#{description},#{status},#{createTime},#{updateTime},#{createUser},#{updateUser})
</insert>
<!--DishFlavorMapper.xml-->
<insert id="insertBatch">
insert into dish_flavor (dish_id, name, value) values
<foreach collection="flavors" item="df" index="index" separator=",">
(#{df.dishId},#{df.name},#{df.value})
</foreach>
</insert>
出现的问题mybatis中
传入字段顺序和数据库中字段顺序不一样,导致错误

功能测试&提交
测试成功,提交代码

3.菜品分页查询
需求分析和设计

- 接口设计

设计一个DTO用于接受前端传给后端的数据

- vo类设计
categoryName,来自另一个表因为查询到的数据来自两张表, 所以需要一个VO类进行统一封装然后返回给前端
将两个表中数据封装到一个VO对象中

代码开发
controller
/**
* 菜品分页查询
* @param dishPageQueryDTO
* @return
*/
@GetMapping("/page")
@ApiOperation(value = "菜品查询分页")
public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO){
log.info("菜品查询分页信息:{}",dishPageQueryDTO);
PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);
return Result.success(pageResult);
}
service
//接口
PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO);
//impl
/**
* 菜品分页查询
* @param dishPageQueryDTO
* @return
*/
@Override
public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) {
log.info("使用分页插件");
PageHelper.startPage(dishPageQueryDTO.getPage(),dishPageQueryDTO.getPageSize());
Page<DishVO> page = dishMapper.pageQuery(dishPageQueryDTO);
return new PageResult(page.getTotal(),page.getResult());
}
mapper
/**
* 菜品分页查询
* @param dishPageQueryDTO
* @return
*/
Page<DishVO> pageQuery(DishPageQueryDTO dishPageQueryDTO);
DishMapper.xml
<!--动态xml 实现分页查询-->
<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 name!=''">
and d.name like concat('%',#{name},'%')
</if>
<if test="categoryId!=null and categoryId!=''">
and d.category_id=#{categoryId}
</if>
<if test="status!=null and status!=''">
and d.status=#{status}
</if>
</where>
order by d.create_time desc
</select>
内连接,外链接,多表查询... ...参考https://developer.aliyun.com/article/1397957
功能测试&提交
前后端联调,测试成功

项目采用Springboot2.X.x不支持JDK21,由于当天完成学校作业把idea项目jdk设置为了21,导致项目不能正常运行
Caused by: org.springframework.core.NestedIOException: ASM ClassReader failed to parse class file - probably due to a new Java class file version that isn't supported yet: file [D:\Code\SkyTakeaway\后端初始工程\sky-take-out\sky-server\target\classes\com\sky\SkyApplication.class]; nested exception is java.lang.IllegalArgumentException: Unsupported class file major version 65
4.删除菜品
需求分析和设计
需求分析:
- 可以单独删除,
- 也可以批量删除
- 启售中菜品不能删除
- 被套餐关联的菜品不能删除
- 删除菜品后,关联的口味数据也要删除

接口设计:
只用一个批量删除的接口,替代单独删除

数据库设计:
代码开发
controller
/**
* 菜品批量删除
* @param ids
* @return
*/
@DeleteMapping
@ApiOperation("菜品批量删除")
// RequestParam注解自动讲前端传的字符串,转换为集合
public Result delete(@RequestParam List<Long> ids){
log.info("菜品批量删除:{}",ids);
dishService.deleteBatch(ids);
return Result.success();
}
service
/**
*
* 批量删除菜品
* @param ids
*/
public void deleteBatch(List<Long> ids){
log.info("开始批量删除ids={}",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.getSetmealIdsByDishIds(ids);
log.info("setmealIds={}",setmealIds);
if (setmealIds != null && setmealIds.size() > 0){
//关联了菜品信息不能删除
throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
}
for (Long id : ids) {
//删除菜品表中数据
dishMapper.deleteById(id);
//
dishFlavorMapper.deleteDishId(id);
}
}
TODO:日后优化这里的for循环,
这里的删除菜品表中数据,有待优化,现在如果要删除一个数据就会循环执行两个sql语句,有些占用资源,
------>
可以在mapper优化sql,让所有被删除id全部传入mapper层。使用sql的
in语法:检查某个字段的值是否匹配给定集合中的任意一个值
SELECT column1, column2, ... FROM table_name WHERE column_name IN (value1, value2, value3, ...);
mapper
//DishMapper
//将deleteById函数修改为
/**
* 根据id批量删除菜品
* @param ids
*/
void deleteByIds(List<Long> ids);
//-----------------------------------------
//DishFlavorMapper
//原来deleteByDishId函数改为
/**
* 根据批量菜品id删除口味数据
* @param dishIds
*/
void deleteByDishIds(List<Long> dishIds);
DishMapper
//DishMapper 动态xml
<delete id="deleteByIds">
delete from dish where id in
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
//-----------------------------------------
//DishFlavorMapper 动态xml
<delete id="deleteByDishIds">
delete from dish_flavor where dish_id in
<foreach collection="dishIds" item="dishId" open="(" separator="," close=")">
#{dishId}
</foreach>
</delete>
功能测试&提交
已成功

5.修改菜品
需求分析和设计
- 原型图
- 业务分析
1.回显数据,根据id查询数据;回显dish表中数据然后在回显口味表数据
2.修改数据,修改dish表数据,删除口味再,新增口味以达到和修改口味同样的修改操作
3.根据类型查询分类;(已完成)
4.文件上传;(已完成)
- 接口设计


代码开发
- 1.回显菜品及口味数据
controller
//DishController
/**
* 根据id查询菜品及对应口味数据
* @param id
* @return
*/
@GetMapping("/{id}")
@ApiOperation("根据id查询菜品")
public Result<DishVO> getById(@PathVariable Long id) {
log.info("根据id查询菜品:{}", id);
DishVO dishVO = dishService.getByIdWithFlavor(id);
return Result.success(dishVO);
}
service
//DisService接口
/**
* 根据id查询菜品及其对应口味数据
* @param id
* @return
*/
DishVO getByIdWithFlavor(Long id);
//impl
/**
* 根据id查询菜品及其对应口味数据
* @param id
* @return
*/
@Override
public DishVO getByIdWithFlavor(Long id) {
//获取菜品数据
Dish dish = dishMapper.getById(id);
//获取菜品口味数据
List<DishFlavor> flavors = dishFlavorMapper.getByDishId(id);
//将菜品和口味数据封装到VO对象中
DishVO dishVO = new DishVO();
BeanUtils.copyProperties(dish, dishVO);
dishVO.setFlavors(flavors);
return dishVO;
}
mapper(之前已经完成,可以直接用)
//DishMapper
/**
* 根据id查询菜品
* @param id
* @return
*/
@Select("select * from dish where id = #{id}")
Dish getById(Long id);
//DishFlavorMapper
/**
* 根据菜品id查询口味数据
* @param dishiId
* @return
*/
@Select("select * from dish_flavor where dish_id = #{dishId}")
List<DishFlavor> getByDishId(Long dishiId);
这里完成了修改菜品的第一步,回显菜品数据,前后端联调测试一下,成功!!!

下面再完成最后的修改功能
controller
/**
* 根据id修改菜品
* @param dishDTO
* @return
*/
@PutMapping
@ApiOperation("根据id修改菜品")
public Result update(@RequestBody DishDTO dishDTO) {
log.info("修改菜品:{}", dishDTO);
dishService.updateWithFlavor(dishDTO);
return Result.success();
}
service
//DishService接口
/**
* 根据id更新菜品及其对应口味数据
*
* @param dishDTO
*/
public void updateWithFlavor(DishDTO dishDTO);
//DishServiceImpl实现
/**
* 根据id更新菜品及其对应口味数据
*
* @param dishDTO
*/
@Transactional
@Override
public void updateWithFlavor(DishDTO dishDTO) {
//1.更新菜品基本数据
Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO, dish);
dishMapper.update(dish);
//更新菜品口味数据
//先删除原来的口味数据
dishFlavorMapper.deleteByDishId(dishDTO.getId());
//再插入新的口味数据
List<DishFlavor> flavors = dishDTO.getFlavors();
//新增的口味数据要重新设置菜品id
if(flavors != null && flavors.size() > 0){
flavors.forEach(flavor -> flavor.setDishId(dishDTO.getId()));
dishFlavorMapper.insertBatch(flavors);
}
}
@Transactional : 这里涉及到两张表数据的修改,为了确保数据的一致性,使用事务
什么情况使用事务:涉及到多张表的(插入,修改,删除)操作时应当使用事务
| 场景 | 是否需要事务? | 原因 |
|---|---|---|
| 多个表的增删改 | ✅ 需要 | 保证数据一致性 |
| 一个业务涉及多步 SQL 操作 | ✅ 需要 | 避免部分成功、部分失败 |
| 高并发操作(如库存扣减) | ✅ 需要 | 防止超卖、数据竞争 |
| 转账、扣款等资金交易 | ✅ 必须 | 资金安全,防止丢失 |
| 级联删除多个关联数据 | ✅ 需要 | 避免部分删除,数据残留 |
单个表的 SELECT 查询 |
❌ 不需要 | 读操作不影响数据 |
单个表的简单 INSERT/UPDATE/DELETE |
❌ 一般不需要 | 数据库本身保证原子性 |
| 日志记录等非关键操作 | ❌ 不需要 | 失败不会影响业务 |
事务是什么:即
A C I D
-
A(原子性,Atomicity)
事务中的所有操作要么全部执行成功,要么全部执行失败。事务是一个原子操作,不可分割。如果事务中的某个操作失败,所有之前的操作都会被回滚,确保数据的一致性。 -
C(一致性,Consistency)
事务执行前后,数据库的状态必须是一致的。事务的执行会使数据库从一个一致性状态转换到另一个一致性状态,确保数据库不会在错误状态下运行。 -
I(隔离性,Isolation)
不同事务之间的操作相互独立,互不干扰。事务的隔离性保证了一个事务的执行不会被其他事务影响,避免脏读、不可重复读、幻读等问题。不同的隔离级别(如 READ COMMITTED、REPEATABLE READ)提供不同程度的隔离。 -
D(持久性,Durability)
一旦事务提交,其结果将永久保存在数据库中。即使发生系统崩溃,已提交的事务所做的修改依然会保留在数据库中,不会丢失。
mapper
//DishMapper
/**
* 根据id更新菜品
*
* @param dish
*/
//使用之前的公共字段自动填充,修改时间,修改人
@AutoFill(OperationType.UPDATE)
void update(Dish dish);
//DishFlavorMapper
/**
* 根据菜品id删除口味
*
* @param id
*/
@Delete("delete from dish_flavor where dish_id = #{id}")
void deleteByDishId(Long id);
/**
* 批量插入口味
*
* @param flavors
*/
void insertBatch(List<DishFlavor> flavors);
⚠️提示:先删除,再插入,代替修改操作,这种方式可以提高性能,同时降低了sql的复杂度,保证数据的一致性
mybatis映射文件
<!--DishMapper-->
<!--根据菜品id 动态修改菜品-->
<update id="update">
update dish
<set>
<if test="name!=null and name!=''">
name=#{name},
</if>
<if test="categoryId!=null">
category_id=#{categoryId},
</if>
<if test="price!=null">
price=#{price},
</if>
<if test="image!=null and image!=''">
image=#{image},
</if>
<if test="description!=null and description!=''">
description=#{description},
</if>
<if test="updateTime!=null">
update_time=#{updateTime},
</if>
<if test="updateUser!=null and updateUser!=''">
update_user=#{updateUser},
</if>
<if test="status!=null">
status=#{status},
</if>
</set>
where id=#{id}
</update>
<!--DishFlavorMapper-->
<!--批量新增口味-->
<insert id="insertBatch">
insert into dish_flavor (dish_id, name, value) values
<foreach collection="flavors" item="df" index="index" separator=",">
(#{df.dishId},#{df.name},#{df.value})
</foreach>
</insert>
❗对于数据库update和delete操作一定要谨慎,一定要多检查where子句等细节,后续一定要学习数据库备份等相关安全操作,关于update和delete误操作的恢复办法
功能测试&提交
测试成功 、提交

这里应为其他原因使用ljdk21,忘记切换回springboot2.X.X项目对应的jdk17导致不能正常运行
TODO:为了避免出错先设置为jdk17,日后再升级springBoot3
springboot2 ——>springboot3 迁移指南https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.0-Migration-Guide




浙公网安备 33010602011771号