Day3 苍穹外卖项目 公共字段自动填充(AOP)、文件上传、新增菜品、菜品分页查询、删除菜品、修改菜品
目录
1.公共字段自动填充
1.1 问题分析
1.2 实现思路
1.3 代码开发
1.3.1 自定义注解 AutoFill
1.3.2 自定义切面类 AutoFillAspect.java
1.3.3 在Mapper接口的方法上加入 AutoFill 注解
1.4 功能测试
1.5 代码提交
2.新增菜品
2.1 需求分析与设计
2.1.1 产品原型
2.1.2 接口设计
2.1.3 表设计
2.2 代码开发
2.2.1 文件上传实现
2.2.1.1 定义OSS相关配置
2.2.1.2 读取OSS配置
2.2.1.3 生成OSS工具类对象
2.2.1.4 定义文件上传接口
2.2.2 新增菜品实现
2.2.2.1 设计DTO类
2.2.2.2 Controller层
2.2.2.3 Service层接口
2.2.2.4 Service层实现类
2.2.2.5 Mapper层
2.3 功能测试
2.4 代码提交
3.菜品分页查询
3.1 需求分析和设计
3.1.1 产品原型
3.1.2 接口设计
3.2 代码开发
3.2.1 设计DTO类
3.2.2 设计VO类
3.2.3 Controller层
3.2.4 Service层接口
3.2.5 Service层实现类
3.2.6 Mapper层
3.3 功能测试
3.3.1 接口文档测试
4.删除菜品
4.1 需求分析和设计
4.1.1 产品原型
4.1.2 接口设计
4.1.3 表设计
4.2 代码开发
4.1.2 Controller层
4.2.2 Service层接口
4.2.3 Service层实现类
4.2.4 Mapper层
4.3 功能测试
4.4 代码提交
5.修改菜品
5.1 需求分析和设计
5.1.1 产品原型
5.1.2 接口设计
5.2 代码开发
5.2.1 根据id查询菜品实现
5.2.1.1 Controller层
5.2.1.2 Service层接口
5.2.1.3 Service层实现类
5.2.1.4 Mapper层
5.2.2 修改菜品实现
5.2.2.1 Controller层
5.2.2.2 Service层接口
5.2.2.3 Service层实现类
5.2.2.4 Mapper层
5.3 功能测试
5.4 代码提交
1.公共字段自动填充
1.1 问题分析
后台系统的员工管理功能和分类管理功能的开发,在新增员工或者新增分类时需要设置创建时间、创建人、修改时间、修改人等字段,在编辑员工或者编辑菜品分类时需要设置修改时间、修改人等字段。这些字段属于公共字段,也就是也就是在我们的系统中很多表中都会有这些字段,如下:
而针对于这些字段,我们的赋值方式为:
1). 在新增数据时, 将createTime、updateTime 设置为当前时间, createUser、updateUser设置为当前登录用户ID。
2). 在更新数据时, 将updateTime 设置为当前时间, updateUser设置为当前登录用户ID。
如果都按照上述的操作方式来处理这些公共字段, 需要在每一个业务方法中进行操作, 编码相对冗余、繁琐,那能不能对于这些公共字段在某个地方统一处理,来简化开发呢?
答案:可以的,我们使用AOP切面编程,实现功能增强,来完成公共字段自动填充功能。
1.2 实现思路
在实现公共字段自动填充,也就是在插入或者更新的时候为指定字段赋予指定的值,使用它的好处就是可以统一对这些字段进行处理,避免了重复代码。
实现步骤:
1). 自定义注解 AutoFill,用于标识需要进行公共字段自动填充的方法。
2). 自定义切面类 AutoFillAspect,统一拦截加入了 AutoFill 注解的方法,通过反射为公共字段赋值
3). 在 Mapper 的方法上加入 AutoFill 注解。
技术点:枚举、注解、AOP、反射。
原理:切入点指定拦截的路径,拦截的是Mapper层的所有方法,并且方法上还需要有@AutoFill注解才会被拦截,切面类的通知设置为前置通知@Before,在执行Mapper层对应的方法会先执行前置通知里面的代码。因此公共字段可以在这里实现统一封装。
1.3 代码开发
1.3.1 自定义注解 AutoFill
进入到sky-server模块,创建com.sky.annotation包。
package com.sky.annotation;
/**
* 自定义注解,用于标识某个方法需要进行功能字段自动填充处理
*/
// ElementType.METHOD该注解只能在方法上使用
@Target(ElementType.METHOD)
// 生命周期
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
//数据库操作类型:UPDATE INSERT
OperationType value();
}
其中OperationType已在sky-common模块中定义枚举类型:
package com.sky.enumeration;
/**
* 数据库操作类型
*/
public enum OperationType {
/**
* 更新操作
*/
UPDATE,
/**
* 插入操作
*/
INSERT
}
1.3.2 自定义切面类 AutoFillAspect.java
在sky-server模块,创建com.sky.aspect包。
package com.sky.aspect;
/**
* 自定义切面,实现公共字段自动填充处理逻辑
*/
// 定义该类为切面类
@Aspect
// 该类交给Spring管理
@Component
@Slf4j
public class 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("开始进行公共字段自动填充...");
}
}
完善自定义切面 AutoFillAspect 的 autoFill 方法
package com.sky.aspect;
/**
* 自定义切面,实现公共字段自动填充处理逻辑
*/
@Aspect
@Component
@Slf4j
public class AutoFillAspect {
/**
* 切入点
*/
@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
public void autoFillPointCut(){}
/**
* 前置通知,在通知中进行公共字段的赋值
*/
@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){
//为4个公共字段赋值
try {
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){
//为2个公共字段赋值
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,now);
setUpdateUser.invoke(entity,currentId);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
1.3.3 在Mapper接口的方法上加入 AutoFill 注解
以CategoryMapper为例,分别在新增和修改方法添加@AutoFill()注解,也需要EmployeeMapper做相同操作:
package com.sky.mapper;
@Mapper
public interface CategoryMapper {
/**
* 插入数据
* @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);
/**
* 根据id修改分类
* @param category
*/
@AutoFill(value = OperationType.UPDATE)
void update(Category category);
}
同时,将业务层为公共字段赋值的代码注释掉。
1). 将员工管理的新增和编辑方法中的公共字段赋值的代码注释。
2). 将菜品分类管理的新增和修改方法中的公共字段赋值的代码注释。
1.4 功能测试
修改员工信息:
添加员工信息:
分类管理的测试也是类似的。
1.5 代码提交
2.新增菜品
2.1 需求分析与设计
2.1.1 产品原型
后台系统中可以管理菜品信息,通过 新增功能来添加一个新的菜品,在添加菜品时需要选择当前菜品所属的菜品分类,并且需要上传菜品图片。
新增菜品原型:
当填写完表单信息, 点击"保存"按钮后, 会提交该表单的数据到服务端, 在服务端中需要接受数据, 然后将数据保存至数据库中。
业务规则:
-
菜品名称必须是唯一的
-
菜品必须属于某个分类下,不能单独存在
-
新增菜品时可以根据情况选择菜品的口味
-
每个菜品必须对应一张图片
2.1.2 接口设计
根据上述原型图先粗粒度设计接口,共包含3个接口。
接口设计:
-
根据类型查询分类(已完成)(显示下拉框)
-
文件上传
-
新增菜品(有可能涉及到两个表 dish表和dish_flavor表)
-
注意事项:涉及到多张表要开启事务。而且还要注意要拿到刚添加菜品的主键,设置给dish_flavor表的dish_id属性。
@Transactional //dishServiceImpl的新增方法加上
@EnableTransactionManagement //开启注解方式的事务管理
-
接下来细粒度分析每个接口,明确每个接口的请求方式、请求路径、传入参数和返回值。
1. 根据类型查询分类(已完成)
2. 文件上传
3. 新增菜品
2.1.3 表设计
通过原型图进行分析:
dish表:
dish_flavor表:口味表
2.2 代码开发
2.2.1 文件上传实现
文件上传,是指将本地图片、视频、音频等文件上传到服务器上,可以供其他用户浏览或下载的过程。文件上传在项目中应用非常广泛,我们经常发抖音、发朋友圈都用到了文件上传功能。
实现文件上传服务,需要有存储的支持,那么我们的解决方案将以下几种:
-
直接将图片保存到服务的硬盘(springmvc中的文件上传)
-
优点:开发便捷,成本低
-
缺点:扩容困难
-
-
使用分布式文件系统进行存储
-
优点:容易实现扩容
-
缺点:开发复杂度稍大(有成熟的产品可以使用,比如:FastDFS,MinIO)
-
-
使用第三方的存储服务(例如OSS)
-
优点:开发简单,拥有强大功能,免维护
-
缺点:付费
-
在本项目选用阿里云的OSS服务进行文件存储。
访问aliyun官网:https://www.aliyun.com/
创建Bucket并且配置相应的信息就可以上传文件。
2.2.1.1 定义OSS相关配置
引入依赖:
<!-- https://mvnrepository.com/artifact/com.aliyun.oss/aliyun-sdk-oss -->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.17.4</version>
</dependency>
在sky-server模块
@ConfigurationProperties可以读取指定的配置文件内容:
application.yml
${ }这种配置方式更加灵活,可以通过设置不同的环境,读取不同的内容:
spring:
profiles:
active: dev #设置环境
sky:
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}
application-dev.yml
配置在开发环境下被读取的内容:
sky:
alioss:
endpoint: oss-cn-hangzhou.aliyuncs.com
access-key-id: LTAI5tPeFLzsPPT8gG3LPW64
access-key-secret: U6k1brOZ8gaOIXv3nXbulGTUzy6Pd7
bucket-name: sky-take-out
2.2.1.2 读取OSS配置
在sky-common模块中,已定义
package com.sky.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
//读取配置文件
@ConfigurationProperties(prefix = "sky.alioss")
@Data
public class AliOssProperties {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
}
2.2.1.3 生成OSS工具类对象
其中,AliOssUtil.java已在sky-common模块中定义
package com.sky.utils;
@Data
@AllArgsConstructor
@Slf4j
public class AliOssUtil {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
/**
* 文件上传
*
* @param bytes
* @param objectName
* @return
*/
public String upload(byte[] bytes, String objectName) {
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
try {
// 创建PutObject请求。
ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(bytes));
} catch (OSSException oe) {
System.out.println("Caught an OSSException, which means your request made it to OSS, "
+ "but was rejected with an error response for some reason.");
System.out.println("Error Message:" + oe.getErrorMessage());
System.out.println("Error Code:" + oe.getErrorCode());
System.out.println("Request ID:" + oe.getRequestId());
System.out.println("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
System.out.println("Caught an ClientException, which means the client encountered "
+ "a serious internal problem while trying to communicate with OSS, "
+ "such as not being able to access the network.");
System.out.println("Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
//文件访问路径规则 https://BucketName.Endpoint/ObjectName
StringBuilder stringBuilder = new StringBuilder("https://");
stringBuilder
.append(bucketName)
.append(".")
.append(endpoint)
.append("/")
.append(objectName);
log.info("文件上传到:{}", stringBuilder.toString());
return stringBuilder.toString();
}
}
在sky-server模块
这里使用了导入第三方bean的思想,扫描不到这个@Component注解,所以需要通过配置类的方式引入。
在 Spring 框架中,@Bean
方法的参数是通过自动装配(Autowiring)机制来注入的。因此,在 @Bean
方法中声明的参数会自动由 Spring 容器解析并注入,而不需要显式使用 @Autowired
注解。
package com.sky.config;
/**
* 配置类,用于创建AliOssUtil对象
*/
@Configuration
@Slf4j
public class OssConfiguration {
//引入第三方Bean的方式
@Bean
@ConditionalOnMissingBean
public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties){
log.info("开始创建阿里云文件上传工具类对象:{}",aliOssProperties);
return new AliOssUtil(aliOssProperties.getEndpoint(),
aliOssProperties.getAccessKeyId(),
aliOssProperties.getAccessKeySecret(),
aliOssProperties.getBucketName());
}
}
2.2.1.4 定义文件上传接口
在sky-server模块中定义接口
package com.sky.controller.admin;
/**
* 通用接口
*/
@RestController
@RequestMapping("/admin/common")
@Api(tags = "通用接口")
@Slf4j
public class CommonController {
@Autowired
private AliOssUtil aliOssUtil;
/**
* 文件上传
* @param file
* @return
*/
@PostMapping("/upload")
@ApiOperation("文件上传")
// 注意这里的file
public Result<String> upload(MultipartFile file){
log.info("文件上传:{}",file);
try {
//原始文件名
String originalFilename = file.getOriginalFilename();
//截取原始文件名的后缀 dfdfdf.png
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);
}
}
2.2.2 新增菜品实现
2.2.2.1 设计DTO类
在sky-pojo模块中
package com.sky.dto;
@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<>();
}
2.2.2.2 Controller层
进入到sky-server模块
package com.sky.controller.admin;
/**
* 菜品管理
*/
@RestController
@RequestMapping("/admin/dish")
@Api(tags = "菜品相关接口")
@Slf4j
public class DishController {
@Autowired
private DishService dishService;
/**
* 新增菜品
*
* @param dishDTO
* @return
*/
@PostMapping
@ApiOperation("新增菜品")
public Result save(@RequestBody DishDTO dishDTO) {
log.info("新增菜品:{}", dishDTO);
dishService.saveWithFlavor(dishDTO);//后绪步骤开发
return Result.success();
}
}
2.2.2.3 Service层接口
package com.sky.service;
public interface DishService {
/**
* 新增菜品和对应的口味
*
* @param dishDTO
*/
public void saveWithFlavor(DishDTO dishDTO);
}
2.2.2.4 Service层实现类
package com.sky.service.impl;
@Service
@Slf4j
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);
//向菜品表插入1条数据
dishMapper.insert(dish);//后绪步骤实现
//获取insert语句生成的主键值。这里需要配置xml文件才能拿到主键值
Long dishId = dish.getId();
List<DishFlavor> flavors = dishDTO.getFlavors();
if (flavors != null && flavors.size() > 0) {
flavors.forEach(dishFlavor -> {
dishFlavor.setDishId(dishId);
});
//向口味表插入n条数据
dishFlavorMapper.insertBatch(flavors);//后绪步骤实现
}
}
}
2.2.2.5 Mapper层
DishMapper.java中添加
/**
* 插入菜品数据
*
* @param dish
*/
@AutoFill(value = OperationType.INSERT)
void insert(Dish dish);
在/resources/mapper中创建DishMapper.xml
<?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>
注意事项:下面两个属性配合使用,可以实现类可以拿到刚插入数据库自增的菜品的主键。
1. useGeneratedKeys="true"
- 作用:MyBatis 会自动将数据库生成的主键值(通常是自增ID)返回给调用方法。
2. keyProperty="id"
- 含义:指定Java对象中哪个属性接收生成的主键值。
DishFlavorMapper.java
package com.sky.mapper;
import com.sky.entity.DishFlavor;
import java.util.List;
@Mapper
public interface DishFlavorMapper {
/**
* 批量插入口味数据
* @param flavors
*/
void insertBatch(List<DishFlavor> flavors);
}
在/resources/mapper中创建DishFlavorMapper.xml
<?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="df" separator=",">
(#{df.dishId},#{df.name},#{df.value})
</foreach>
</insert>
</mapper>
2.3 功能测试
进入到菜品管理--->新建菜品
插入成功:
2.4 代码提交
3.菜品分页查询
3.1 需求分析和设计
3.1.1 产品原型
系统中的菜品数据很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。
菜品分页原型:
注意事项:
在菜品列表展示时,有两个字段略微特殊,第一个是图片字段 ,我们从数据库查询出来的仅仅是图片的名字,图片要想在表格中回显展示出来,就需要下载这个图片。第二个是菜品分类,这里展示的是分类名称,而不是分类ID,此时我们就需要根据菜品的分类ID,去分类表中查询分类信息,然后在页面展示(涉及多表查询)。
业务规则:
-
根据页码展示菜品信息
-
每页展示10条数据
-
分页查询时可以根据需要输入菜品名称、菜品分类、菜品状态进行查询
3.1.2 接口设计
根据上述原型图,设计出相应的接口。
3.2 代码开发
3.2.1 设计DTO类
根据菜品分页查询接口定义设计对应的DTO:
在sky-pojo模块中,已定义
package com.sky.dto;
@Data
public class DishPageQueryDTO implements Serializable {
private int page;
private int pageSize;
private String name;
private Integer categoryId; //分类id
private Integer status; //状态 0表示禁用 1表示启用
}
3.2.2 设计VO类
根据菜品分页查询接口定义设计对应的VO:
在sky-pojo模块中,已定义
封装前端所需要的数据。
package com.sky.vo;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
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<>();
}
3.2.3 Controller层
根据接口定义创建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);
}
3.2.4 Service层接口
在 DishService 中扩展分页查询方法:
/**
* 菜品分页查询
*
* @param dishPageQueryDTO
* @return
*/
PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO);
3.2.5 Service层实现类
在 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());
}
3.2.6 Mapper层
在 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 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>
3.3 功能测试
3.3.1 接口文档测试
接口文档调试:
启动服务:访问http://localhost:8080/doc.html,进入菜品分页查询接口
前后端联调:
4.删除菜品
4.1 需求分析和设计
4.1.1 产品原型
在菜品列表页面,每个菜品后面对应的操作分别为修改、删除、停售,可通过删除功能完成对菜品及相关的数据进行删除。
删除菜品原型:
业务规则:
-
可以一次删除一个菜品,也可以批量删除菜品
-
起售中的菜品不能删除
-
被套餐关联的菜品不能删除
-
删除菜品后,关联的口味数据也需要删除掉
4.1.2 接口设计
根据上述原型图,设计出相应的接口。
注意事项:删除一个菜品和批量删除菜品共用一个接口,故ids可包含多个菜品id,之间用逗号分隔。
4.1.3 表设计
在进行删除菜品操作时,会涉及到以下三张表。
注意事项:
-
在dish表中删除菜品基本数据时,同时,也要把关联在dish_flavor表中的数据一块删除。
-
若菜品为起售状态就禁止删除。(刚添加的菜品必须跟分类关联,并且为停售状态)
-
若删除的菜品数据关联着某个套餐,此时,删除失败。
-
若要删除套餐关联的菜品数据,先解除两者关联,再对菜品进行删除。
4.2 代码开发
4.1.2 Controller层
根据删除菜品的接口定义在DishController中创建方法:
/**
* 菜品批量删除
*
* @param ids
* @return
*/
@DeleteMapping
@ApiOperation("菜品批量删除")
public Result delete(@RequestParam List<Long> ids) {
log.info("菜品批量删除:{}", ids);
dishService.deleteBatch(ids);//后绪步骤实现
return Result.success();
}
4.2.2 Service层接口
在DishService接口中声明deleteBatch方法:
/**
* 菜品批量删除
*
* @param ids
*/
void deleteBatch(List<Long> ids);
4.2.3 Service层实现类
在DishServiceImpl中实现deleteBatch方法:
@Autowired
private 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);//后绪步骤实现
}
}
4.2.4 Mapper层
在DishMapper中声明getById方法,并配置SQL:
/**
* 根据主键查询菜品
*
* @param id
* @return
*/
@Select("select * from dish where id = #{id}")
Dish getById(Long id);
创建SetmealDishMapper,声明getSetmealIdsByDishIds方法,并在xml文件中编写SQL:
package com.sky.mapper;
import com.sky.entity.SetmealDish;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface SetmealDishMapper {
/**
* 根据菜品id查询对应的套餐id
*
* @param dishIds
* @return
*/
//select setmeal_id from setmeal_dish where dish_id in (1,2,3,4)
List<Long> getSetmealIdsByDishIds(List<Long> dishIds);
}
SetmealDishMapper.xml
<?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>
在DishMapper.java中声明deleteById方法并配置SQL:
/**
* 根据主键删除菜品数据
*
* @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);
4.3 功能测试
前后端联调测试。
删除停售菜品:
删除起售菜品:
4.4 代码提交
5.修改菜品
5.1 需求分析和设计
5.1.1 产品原型
在菜品管理列表页面点击修改按钮,跳转到修改菜品页面,在修改页面回显菜品相关信息并进行修改,最后点击保存按钮完成修改操作。
修改菜品原型:
5.1.2 接口设计
通过对上述原型图进行分析,该页面共涉及4个接口。
接口:
-
根据id查询菜品(回显)
-
根据类型查询分类(已实现)
-
文件上传(已实现)
-
修改菜品
我们只需要实现根据id查询菜品和修改菜品两个接口,接下来,我们来重点分析这两个接口。
1). 根据id查询菜品
2). 修改菜品
注意事项:因为是修改功能,请求方式可设置为PUT。
5.2 代码开发
5.2.1 根据id查询菜品实现
5.2.1.1 Controller层
根据id查询菜品的接口定义在DishController中创建方法:
返回类型DishVO
/**
* 根据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);
}
5.2.1.2 Service层接口
在DishService接口中声明getByIdWithFlavor方法:
/**
* 根据id查询菜品和对应的口味数据
*
* @param id
* @return
*/
DishVO getByIdWithFlavor(Long id);
5.2.1.3 Service层实现类
在DishServiceImpl中实现getByIdWithFlavor方法:
/**
* 根据id查询菜品和对应的口味数据
*
* @param id
* @return
*/
public DishVO getByIdWithFlavor(Long id) {
//根据id查询菜品数据
Dish dish = dishMapper.getById(id);
//根据菜品id查询口味数据
List<DishFlavor> dishFlavors = dishFlavorMapper.getByDishId(id);//后绪步骤实现
//将查询到的数据封装到VO
DishVO dishVO = new DishVO();
BeanUtils.copyProperties(dish, dishVO);
dishVO.setFlavors(dishFlavors);
return dishVO;
}
5.2.1.4 Mapper层
在DishFlavorMapper中声明getByDishId方法,并配置SQL:
/**
* 根据菜品id查询对应的口味数据
* @param dishId
* @return
*/
@Select("select * from dish_flavor where dish_id = #{dishId}")
List<DishFlavor> getByDishId(Long dishId);
5.2.2 修改菜品实现
5.2.2.1 Controller层
根据修改菜品的接口定义在DishController中创建方法:
/**
* 修改菜品
*
* @param dishDTO
* @return
*/
@PutMapping
@ApiOperation("修改菜品")
public Result update(@RequestBody DishDTO dishDTO) {
log.info("修改菜品:{}", dishDTO);
dishService.updateWithFlavor(dishDTO);
return Result.success();
}
5.2.2.2 Service层接口
在DishService接口中声明updateWithFlavor方法:
/**
* 根据id修改菜品基本信息和对应的口味信息
*
* @param dishDTO
*/
void updateWithFlavor(DishDTO dishDTO);
5.2.2.3 Service层实现类
在DishServiceImpl中实现updateWithFlavor方法:
注意事项:修改口味信息,是先删除原有的口味信息,再重新添加的操作。在插入数据库之前
要设置好 dish_id 的值。
/**
* 根据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);
}
}
5.2.2.4 Mapper层
在DishMapper中,声明update方法:
/**
* 根据id动态修改菜品数据
*
* @param dish
*/
@AutoFill(value = OperationType.UPDATE)
void update(Dish dish);
并在DishMapper.xml文件中编写SQL:
<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>
5.3 功能测试
本次测试直接通过前后端联调测试 ,可使用Debug方式启动项目,观察运行中步骤。