【苍穹外卖】项目实战Day03
? 本文由 程序喵正在路上 原创,CSDN首发!
? 系列专栏:苍穹外卖项目实战
? 首发时间:2024年5月4日
? 欢迎关注?点赞?收藏?留言?
目录
- 公共字段自动填充
- 问题分析
- 解决思路
- 代码开发
- 功能测试
- 新增菜品
- 需求分析和设计
- 代码开发
- 功能测试
- 菜品分页查询
- 需求分析和设计
- 代码开发
- 功能测试
- 删除菜品
- 需求分析和设计
- 代码开发
- 代码优化
- 功能测试
- 修改菜品
- 需求分析和设计
- 代码开发
- 功能测试
- 菜品启售停售
- 需求分析和设计
- 代码开发
- 功能测试
公共字段自动填充
问题分析
在前面的业务表中,比如表 employee 和 category 中,我们可以发现一些公共字段,这些字段在后面的业务表中也同样存在:
这些公共字段在某些操作中都需要设置,而且设置的代码都是一样的
那么就出现了问题:代码冗余、不便于后期维护
- 工程中存在多段相同的代码
- 如果后期我们需要对业务表进行修改,涉及到这些公共字段,进行修改也是非常麻烦的
解决思路
我们发现,这些公共字段的设置只会出现在特定的操作中,比如 create_time 和 create_user 只会出现在 insert 操作中。
所以,我们的想法是将其统一处理。
具体思路:
- 自定义注解 AutoFill,用于标识需要进行公共字段自动填充的方法
- 自定义切面类 AutoFillAspect,统一拦截加入了 AutoFill 注解的方法,通过反射为公共字段赋值
- 在 Mapper 中需要处理公共字段的方法上加入 AutoFill 注解
- 涉及技术点:枚举、注解、AOP、反射
代码开发
创建一个专门用来存放自定义注解的包 annotation,在包下创建自定义注解 AutoFill
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;
@Target(ElementType.METHOD) //只作用于方法上
@Retention(RetentionPolicy.RUNTIME) //表示被该注解修饰的注解在运行时保留,即可以通过反射机制在运行时获取这些注解的信息
public @interface AutoFill {
//定义注解的属性为数据库操作类型: UPDATE INSERT
//OperationType在包com.sky.enumeration中
OperationType value();
}
创建一个专门用来存放切面类的包 aspect,在包下创建切面类 AutoFillAspect
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.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 //标记为Spring管理的Bean
@Slf4j //日志
public class AutoFillAspect {
//切入点
/**
* execution(* com.sky.mapper.*.*(..)): 切点表达式, 方法返回值为任意, 匹配com.sky.mapper包下所有类和接口,
* 参数为任意
*
* @annotation(com.sky.annotation.AutoFill): 注解匹配表达式, 拦截带有AutoFill注解的方法
*/
@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 now = LocalDateTime.now();
Long currentId = 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, now);
setUpdateTime.invoke(entity, now);
setCreateUser.invoke(entity, currentId);
setUpdateUser.invoke(entity, currentId);
} catch (Exception e) {
log.info("公共字段自动填充失败:{}", e.getMessage());
}
} else if (operationType == OperationType.UPDATE) {
//当前执行的是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, now);
setUpdateUser.invoke(entity, currentId);
} catch (Exception e) {
log.info("公共字段自动填充失败:{}", e.getMessage());
}
}
}
}
- 给 EmployeeMapper 和 CategoryMapper 中数据库操作类型为
insert
和update
的方法加上自定义的注解@AutoFill(value = OperationType.INSERT)
,类型记得别写错;然后,原先代码 Service 层中给这些公共字段赋值的代码就可以删掉了。
功能测试
直接通过前后端联调测试,通过观察控制台输出的SQL来确定公共字段填充是否完成;也可以直接在前端页面观察插入和修改操作后时间是否改变来确认。
新增菜品
需求分析和设计
产品原型:
业务规则:
- 菜品名称必须是唯一的
- 菜品必须属于某个分类下,不能单独存在
- 新增菜品时可以根据情况选择菜品的口味
- 每个菜品必须对应一张图片
接口设计:
根据类型查询分类(已开发)
文件上传
- 新增菜品
数据库设计(dish菜品表和dish_flavor口味表):
代码开发
开发文件上传接口
首先,我们需要配置阿里云OSS的相关信息,如果你忘了阿里云OSS的相关内容,可以查看这篇文章 ,请配置自己的OSS信息,图中配置信息无效:
在我们写这些配置的时候,会发现有提示信息,但是这些属性并不是框架自带,为什么会有提示信息呢?
其实这是因为项目的 com.sky.properties 包下有一个配置属性类 AliOssProperties
通过使用 @ConfigurationProperties
注解,可以将配置文件中以 sky.alioss
前缀开头的属性映射到该类的实例中。然后,可以在 Spring Boot 应用程序中注入 AliOssProperties
类的实例,以便在应用程序中使用阿里云 OSS 的相关配置信息,比如访问端点 (endpoint)、访问密钥 (accessKeyId 和 accessKeySecret) 以及存储桶名称 (bucketName) 等。
同时,项目中还有一个使用阿里云 OSS 必须有的工具类,它是由官方代码改造而来,实现的功能为上传文件到阿里云 OSS 并返回该文件的外部访问路径:
接下来,我们来开发文件上传接口,首先,我们要写一个配置类,用于配置阿里云对象存储服务(OSS)相关的 Bean,后面我们需要用到这个 Bean。
你可能会好奇为什么要写成一个配置类,好像普通的类就能实现相同的功能? 如果你不好奇,可以直接去看配置类的代码。
将类声明为配置类的一个主要目的是使其成为 Spring 容器的一部分,从而可以利用 Spring 的依赖注入和管理功能。这样做有几个好处:
- 依赖注入管理: 声明为配置类的 Bean 可以在应用程序的整个生命周期内由 Spring 容器进行管理。这意味着可以在应用程序的其他组件中轻松地注入和使用这些 Bean,而不需要手动创建对象。
- 组件化: 将相关的配置信息和对象创建逻辑放在一个配置类中有助于更好地组织代码结构,并将关注点分离。这样做可以使代码更易于维护和理解。
- 条件化装配: 使用 Spring 的条件化装配功能(如
@ConditionalOnMissingBean
)可以根据特定条件决定是否创建 Bean,从而实现更灵活的 Bean 创建和管理。 - 集成测试: 在单元测试和集成测试中,可以通过模拟和替换 Bean 来轻松测试应用程序的不同部分,而无需更改源代码。
虽然可以手动创建对象来使用,但使用 Spring 的依赖注入机制能够提供更好的可维护性、灵活性和测试性,因此在大多数情况下,将类声明为 Spring 的配置类是更好的选择。
import com.sky.properties.AliOssProperties;
import com.sky.utils.AliOssUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration //配置类
@Slf4j
public class OssConfiguration {
/**
* 通过spring管理对象
*
* @param aliOssProperties
* @return
*/
@Bean
@ConditionalOnMissingBean //只有当容器中不存在名为aliOssUtil的Bean时才创建该Bean, 防止重复创建
public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties) {
log.info("开始创建阿里云OSS工具类对象...");
return new AliOssUtil(aliOssProperties.getEndpoint(),
aliOssProperties.getAccessKeyId(),
aliOssProperties.getAccessKeySecret(),
aliOssProperties.getBucketName());
}
}
文件上传这个接口在其他模块中也有可能会使用到,所以我们将其写在一个通用的控制类 CommonController 中。
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.util.UUID;
/**
* 通用接口
*/
@RestController
@RequestMapping("/admin/common")
@Slf4j
@Api(tags = "通用接口")
public class CommonController {
@Autowired
private AliOssUtil aliOssUtil;
/**
* 文件上传
*
* @param file
* @return
*/
@PostMapping("/upload")
@ApiOperation("文件上传")
public Result<String> upload(MultipartFile file) {
log.info("文件上传:{}", file.getName());
String originalFilename = file.getOriginalFilename(); //原始文件名
String extension = originalFilename.substring(originalFilename.lastIndexOf(".")); //获取文件后缀
//生成一个独一无二的文件名
String newFileName = UUID.randomUUID().toString() + extension;
//将文件上传到阿里云
try {
String filePath = aliOssUtil.upload(file.getBytes(), newFileName);
return Result.success(filePath);
} catch (Exception e) {
log.error("文件上传失败:{}", e.getMessage());
}
return Result.error(MessageConstant.UPLOAD_FAILED);
}
}
ps:建议配置单个文件最大上传大小和单个请求最大上传大小:
spring:
servlet:
multipart:
max-file-size: 10MB #单个文件最大上传大小
max-request-size: 100MB #单个请求最大上传大小
开发新增菜品接口
根据新增菜品接口设计对应的DTO(项目中已开发):
新建 DishController,在其中创建新增菜品方法:
import com.sky.dto.DishDTO;
import com.sky.result.Result;
import com.sky.service.DishService;
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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/admin/dish")
@Api(tags = "菜品相关接口")
@Slf4j
public class DishController {
@Autowired
private DishService dishService;
/**
* 新增菜品
*
* @param dishDTO
* @return
*/
@PostMapping
@ApiOperation("新增菜品")
public Result<String> save(@RequestBody DishDTO dishDTO) {
log.info("新增菜品:{}", dishDTO);
dishService.saveWithFlavor(dishDTO);
return Result.success();
}
}
在 DishService 接口中声明新增菜品方法:
import com.sky.dto.DishDTO;
public interface DishService {
/**
* 新增菜品和对应的口味
* @param dishDTO
*/
void saveWithFlavor(DishDTO dishDTO);
}
在 DishServiceImpl 中实现新增菜品方法:
import com.sky.dto.DishDTO;
import com.sky.entity.Dish;
import com.sky.entity.DishFlavor;
import com.sky.mapper.DishFlavorMapper;
import com.sky.mapper.DishMapper;
import com.sky.service.DishService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class DishServiceImpl implements DishService {
@Autowired
private DishMapper dishMapper;
@Autowired
private DishFlavorMapper dishFlavorMapper;
/**
* 新增菜品和对应的口味
*
* @param dishDTO
*/
@Transactional //声明方法应该被包含在一个事务中执行
public void saveWithFlavor(DishDTO dishDTO) {
Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO, dish); //拷贝对象属性
//向菜品表dish中插入1条数据
dishMapper.insert(dish);
//获取菜品的主键值
Long dishId = dish.getId();
List<DishFlavor> flavors = dishDTO.getFlavors();
if (flavors != null && flavors.size() > 0) {
//向口味表dish_flavor插入n条数据
flavors.forEach(dishFlavor -> {
dishFlavor.setDishId(dishId);
});
//批量插入口味数据
dishFlavorMapper.insertBatch(flavors);
}
}
}
在 DishMapper 中声明 insert 方法:
/**
* 插入菜品数据
*
* @param dish
*/
@AutoFill(OperationType.INSERT)
void insert(Dish dish);
新建 DishMapper.xml,在其中编写对应的 SQL:
<?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">
<!--
useGeneratedKeys="true": 表示获取数据插入后所在的主键值
keyProperty="id": 表示将主键值赋给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>
</mapper>
新建 DishFlavorMapper,在其中声明 insertBatch 方法:
import com.sky.entity.DishFlavor;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface DishFlavorMapper {
/**
* 批量插入口味数据
*
* @param flavors
*/
void insertBatch(List<DishFlavor> flavors);
}
新建 DishFlavorMapper.xml,在其中编写对应的 SQL:
<?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.DishFlavorMapper">
<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>
</mapper>
功能测试
进行前后端联调测试,点击菜品管理,添加菜品,可以看到图片,说明文件上传接口开发成功:
填写一下其他信息,点击保存:
表中已经有了刚刚添加的数据,成功:
提交下代码到码云。
菜品分页查询
需求分析和设计
产品原型:
业务规则:
- 根据页码展示菜品信息
- 每页展示10条数据
- 分页查询时可以根据需要输入菜品名称、菜品分类、菜品状态进行查询
接口设计:
代码开发
根据菜品分页查询接口定义设计对应的 DTO(项目中已开发):
由于返回数据多了一项分类名称,所以我们需要根据菜品分页查询接口定义设计对应的 VO 用于封装返回数据(项目中已开发):
根据接口定义创建 DishController 的 page 分页查询方法:
/**
* 菜品分页查询
*
* @param dishPageQueryDTO
* @return
*/
@GetMapping("/page")
@ApiOperation("菜品分页查询")
public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO) {
log.info("菜品分页查询:{}", dishPageQueryDTO);
PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);
return Result.success(pageResult);
}
在 DishService 中扩展分页查询方法:
/**
* 菜品分页查询
*
* @param dishPageQueryDTO
* @return
*/
PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO);
在 DishServiceImpl 中实现分页查询方法:
/**
* 菜品分页查询
*
* @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());
}
在 DishMapper 接口中声明 pageQuery 方法:
/**
* 菜品分页查询
*
* @param dishPageQueryDTO
* @return
*/
Page<DishVO> pageQuery(DishPageQueryDTO dishPageQueryDTO);
在 DishMapper.xml 中编写 SQL:
<select id="pageQuery" resultType="com.sky.vo.DishVO">
select d.*, c.name as categoryName from dish d left 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>
功能测试
进行前后端联调测试,可以看到所有菜品,也可以输入条件查询,可以切换下一页,成功:
没有显示图片是因为图片访问地址已失效,后面可以修改。
删除菜品
需求分析和设计
产品原型:
业务规则:
- 可以一次删除一个菜品,也可以批量删除菜品
- 起售中的菜品不能删除
- 被套餐关联的菜品不能删除
- 删除菜品后,关联的口味数据也需要删除掉
接口设计:
数据库设计:
代码开发
根据删除菜品的接口定义在 DishController 中创建方法:
/**
* 菜品批量删除
*
* @param ids
* @return
*/
@DeleteMapping
@ApiOperation("菜品批量删除")
public Result delete(@RequestParam List<Long> ids) {
log.info("菜品批量删除:{}", ids);
dishService.deleteBatch(ids);
return Result.success();
}
在 DishService 接口中声明 deleteBatch 方法:
/**
* 菜品批量删除
*
* @param ids
*/
void deleteBatch(List<Long> ids);
在 DishServiceImpl 中实现 deleteBatch 方法:
/**
* 菜品批量删除
*
* @param ids
*/
@Transactional
public void deleteBatch(List<Long> ids) {
ids.forEach(id -> {
Dish dish = dishMapper.getById(id);
//判断当前要删除的菜品状态是否为起售
if (dish.getStatus() == StatusConstant.ENABLE) {
//抛出自定义异常并返回错误信息给前端
throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
}
});
//判断当前要删除的菜品是否关联了套餐
List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(ids); //根据菜品id查询关联的所有套餐id
if (setmealIds != null && setmealIds.size() > 0) {
throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
}
//删除菜品表中的数据
ids.forEach(id -> {
dishMapper.deleteById(id);
//删除菜品对应的口味数据
dishFlavorMapper.deleteByDishId(id);
});
}
在 DishMapper 中声明 getById 方法,并配置 SQL:
/**
* 根据id查询菜品数据
*
* @param id
* @return
*/
@Select("select * from dish where id = #{id}")
Dish getById(Long id);
创建 SetmealDishMapper,声明 getSetmealIdsByDishIds 方法,并在 xml 文件中编写 SQL:
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface SetmealDishMapper {
/**
* 根据菜品id查询关联的套餐id
*
* @param ids
* @return
*/
List<Long> getSetmealIdsByDishIds(List<Long> ids);
}
<?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="ids" separator="," open="(" close=")" item="dishId">
#{dishId}
</foreach>
</select>
</mapper>
在 DishMapper 中声明 deleteById 方法并配置 SQL:
/**
* 根据id删除菜品
*
* @param id
*/
@Delete("delete from dish where id = #{id}")
void deleteById(Long id);
在 DishFlavorMapper 中声明 deleteByDishId 方法并配置 SQL:
/**
* 根据菜品id删除口味数据
*
* @param dishId
*/
@Delete("delete from dish_flavor where dish_id = #{dishId}")
void deleteByDishId(Long dishId);
代码优化
//删除菜品表中的数据
ids.forEach(id -> {
dishMapper.deleteById(id);
//删除菜品对应的口味数据
dishFlavorMapper.deleteByDishId(id);
});
我们可以把 deleteBatch 的最后几行代码优化一下,原本的代码是遍历菜品 id 集合,然后一个一个去删除菜品和对应的口味数据,当数据量非常大时,SQL 语句就会非常多,会影响我们系统的效率,所以我们将其改进一下。
//改进:直接根据菜品id集合一次性删除菜品, 减少SQL语句
dishMapper.deleteByIds(ids);
//根据菜品id集合一次性删除关联的口味数据
dishFlavorMapper.deleteByDishIds(ids);
DishMapper.xml 的 SQL 语句:
<delete id="deleteByIds">
delete from dish where id in
<foreach collection="ids" separator="," open="(" close=")" item="id">
#{id}
</foreach>
</delete>
DishFlavorMapper.xml 的 SQL 语句:
<delete id="deleteByDishIds">
delete from dish_flavor where dish_id in
<foreach collection="dishIds" separator="," open="(" close=")" item="dishId">
#{dishId}
</foreach>
</delete>
功能测试
通过Swagger接口文档进行测试,通过后再前后端联调测试即可
修改菜品
需求分析和设计
产品原型:
接口设计:
根据id查询菜品
- 根据类型查询分类(已实现)
- 文件上传(已实现)
修改菜品
代码开发
根据 id 查询菜品接口开发
在 DishController 中创建 getById 方法:
/**
* 根据id查询菜品和关联的口味数据
*
* @param id
* @return
*/
@GetMapping("/{id}")
@ApiOperation("根据id查询菜品和关联的口味数据")
public Result<DishVO> getById(@PathVariable Long id) {
log.info("根据id查询菜品和关联的口味数据:{}", id);
return Result.success(dishService.getByIdWithFlavor(id));
}
在 DishService 接口中声明 getByIdWithFlavor 方法:
/**
* 根据id查询菜品和关联的口味数据
*
* @param id
* @return
*/
DishVO getByIdWithFlavor(Long id);
在 DishServiceImpl 中实现 getByIdWithFlavor 方法:
/**
* 根据id查询菜品和关联的口味数据
*
* @param id
* @return
*/
public DishVO getByIdWithFlavor(Long id) {
//查询菜品表
Dish dish = dishMapper.getById(id);
//查询菜品关联的口味
List<DishFlavor> dishFlavorList = dishFlavorMapper.getByDishId(id);
//封装成VO
DishVO dishVO = new DishVO();
BeanUtils.copyProperties(dish, dishVO);
dishVO.setFlavors(dishFlavorList);
return dishVO;
}
在 DishFlavorMapper 中声明 getByDishId 方法,并配置 SQL:
/**
* 根据菜品id查询对应的口味
*
* @param dishId
* @return
*/
@Select("select * from dish_flavor where dish_id = #{dishId}")
List<DishFlavor> getByDishId(Long dishId);
修改菜品接口开发
在 DishController 中创建 update 方法:
/**
* 修改菜品
*
* @param dishDTO
* @return
*/
@PutMapping
@ApiOperation("修改菜品")
public Result update(@RequestBody DishDTO dishDTO) {
log.info("修改菜品:{}", dishDTO);
dishService.updateWithFlavor(dishDTO);
return Result.success();
}
在 DishService 接口中声明 updateWithFlavor 方法:
/**
* 根据id修改菜品和关联的口味
* @param dishDTO
*/
void updateWithFlavor(DishDTO dishDTO);
在 DishServiceImpl 中实现 updateWithFlavor 方法:
/**
* 根据id修改菜品和关联的口味
*
* @param dishDTO
*/
@Transactional
public void updateWithFlavor(DishDTO dishDTO) {
Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO, dish);
//获取所要修改菜品的id
Long dishId = dishDTO.getId();
//修改菜品表dish
dishMapper.update(dish);
//删除当前菜品关联的口味数据
dishFlavorMapper.deleteByDishId(dishId);
//插入最新的口味数据
List<DishFlavor> flavors = dishDTO.getFlavors();
if (flavors != null && flavors.size() > 0) {
flavors.forEach(dishFlavor -> {
dishFlavor.setDishId(dishId);
});
//批量插入口味数据
dishFlavorMapper.insertBatch(flavors);
}
}
在 DishMapper 中声明 update 方法:
/**
* 根据菜品id修改菜品信息
*
* @param dish
*/
@AutoFill(OperationType.UPDATE)
void update(Dish dish);
在 DishMapper.xml 中配置 SQL:
<!-- 根据菜品id修改菜品信息-->
<update id="update">
update dish
<set>
<if test="name != null">
name = #{name},
</if>
<if test="categoryId != null">
category_id = #{categoryId},
</if>
<if test="price != null">
price = #{price},
</if>
<if test="image != null">
image = #{image},
</if>
<if test="description != null">
description = #{description},
</if>
<if test="status != null">
status = #{status},
</if>
<if test="updateTime != null">
update_time = #{updateTime},
</if>
<if test="updateUser != null">
update_user = #{updateUser}
</if>
</set>
where id = #{id}
</update>
功能测试
直接进行前后端联调比较方便,可以给没有图片的菜品修改图片:
菜品启售停售
需求分析和设计
产品原型:
业务规则:
- 菜品停售,则包含菜品的套餐同时停售
接口设计:
数据库设计:
涉及 dish、setmeal 和 setmeal_dish 三个表
代码开发
在 DishController 中创建 startOrStop 方法:
/**
* 菜品启售停售
*
* @param status
* @return
*/
@PostMapping("/status/{status}")
@ApiOperation("菜品启售停售")
public Result startOrStop(@PathVariable Integer status, Long id) {
log.info("菜品启售停售:{}", status);
dishService.startOrStop(status, id);
return Result.success();
}
在 DishService 中声明 startOrStop 方法:
/**
* 修改菜品状态
*
* @param status
* @param id
*/
void startOrStop(Integer status, Long id);
在 DishServiceImpl 中实现 startOrStop 方法:
/**
* 修改菜品状态
*
* @param status
* @param id
*/
@Transactional //涉及修改的操作加入事务管理比较好
public void startOrStop(Integer status, Long id) {
Dish dish = dishMapper.getById(id);//获取菜品对象
dish.setStatus(status);//设置菜品状态
dishMapper.update(dish);//更新菜品信息
//如果是停售操作, 则需要将菜品关联的套餐一并停售
if (status.equals(StatusConstant.DISABLE)) {
List<Long> dishIds = new ArrayList<>();
dishIds.add(id);
//根据菜品id查询关联的所有套餐id
List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(dishIds);
if (setmealIds != null && setmealIds.size() > 0) {
setmealIds.forEach(setmealId -> {
Setmeal setmeal = Setmeal.builder()
.id(setmealId)
.status(StatusConstant.DISABLE)
.build();
setmealMapper.update(setmeal); //根据套餐id修改套餐信息
});
}
}
}
在 SetmealMapper 中声明 update 方法:
/**
* 根据套餐id修改套餐信息
*
* @param setmeal
*/
@AutoFill(OperationType.UPDATE)
void update(Setmeal setmeal);
新建 SetmealMapper.xml,并在其中配置对应的 SQL:
<?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.SetmealMapper">
<!-- 根据套餐id修改套餐信息-->
<update id="update">
update setmeal
<set>
<if test="categoryId != null">
category_id = #{
categoryId},
</if>
<if test="name != null">
name = #{
name},
</if>
<if test="price != null">
price = #{
price},
</if>
<if test="status != null">
status = #{
status},
</if>
<if test="description != null">
description = #{
description},
</if>
<if test="image != null">
image = #{
image},
</if>
<if test="updateTime != null">
update_time = #{
updateTime},
</if>
<if test="updateUser != null">
update_user = #{
updateUser}
</if>
</set>
where id = #{
id}
</update>
</mapper>
功能测试
通过swagger接口文档和前后端联调进行功能测试
还没有评论,来说两句吧...