苍穹外卖-day03 课程内容
公共字段自动填充
新增菜品
菜品分页查询
删除菜品
修改菜品
功能实现: 菜品管理
菜品管理效果图:
1. 公共字段自动填充 1.1 问题分析 在上一章节我们已经完成了后台系统的员工管理功能 和菜品分类功能 的开发,在新增员工 或者新增菜品分类 时需要设置创建时间、创建人、修改时间、修改人等字段,在编辑员工 或者编辑菜品分类 时需要设置修改时间、修改人等字段。这些字段属于公共字段,也就是也就是在我们的系统中很多表中都会有这些字段,如下:
序号
字段名
含义
数据类型
1
create_time
创建时间
datetime
2
create_user
创建人id
bigint
3
update_time
修改时间
datetime
4
update_user
修改人id
bigint
而针对于这些字段,我们的赋值方式为:
1). 在新增数据时, 将createTime、updateTime 设置为当前时间, createUser、updateUser设置为当前登录用户ID。
2). 在更新数据时, 将updateTime 设置为当前时间, updateUser设置为当前登录用户ID。
目前,在我们的项目中处理这些字段都是在每一个业务方法中进行赋值操作,如下:
新增员工方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public void save (EmployeeDTO employeeDTO) { employee.setCreateTime(LocalDateTime.now()); employee.setUpdateTime(LocalDateTime.now()); employee.setCreateUser(BaseContext.getCurrentId()); employee.setUpdateUser(BaseContext.getCurrentId()); employeeMapper.insert(employee); }
编辑员工方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public void update (EmployeeDTO employeeDTO) { employee.setUpdateTime(LocalDateTime.now()); employee.setUpdateUser(BaseContext.getCurrentId()); employeeMapper.update(employee); }
新增菜品分类方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public void save (CategoryDTO categoryDTO) { category.setCreateTime(LocalDateTime.now()); category.setUpdateTime(LocalDateTime.now()); category.setCreateUser(BaseContext.getCurrentId()); category.setUpdateUser(BaseContext.getCurrentId()); categoryMapper.insert(category); }
修改菜品分类方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public void update (CategoryDTO categoryDTO) { category.setUpdateTime(LocalDateTime.now()); category.setUpdateUser(BaseContext.getCurrentId()); categoryMapper.update(category); }
如果都按照上述的操作方式来处理这些公共字段, 需要在每一个业务方法中进行操作, 编码相对冗余、繁琐,那能不能对于这些公共字段在某个地方统一处理,来简化开发呢?
答案是可以的,我们使用AOP切面编程,实现功能增强,来完成公共字段自动填充功能。
1.2 实现思路 在实现公共字段自动填充,也就是在插入或者更新的时候为指定字段赋予指定的值,使用它的好处就是可以统一对这些字段进行处理,避免了重复代码。在上述的问题分析中,我们提到有四个公共字段,需要在新增/更新中进行赋值操作, 具体情况如下:
序号
字段名
含义
数据类型
操作类型
1
create_time
创建时间
datetime
insert
2
create_user
创建人id
bigint
insert
3
update_time
修改时间
datetime
insert、update
4
update_user
修改人id
bigint
insert、update
实现步骤:
1). 自定义注解 AutoFill,用于标识需要进行公共字段自动填充的方法
2). 自定义切面类 AutoFillAspect,统一拦截加入了 AutoFill 注解的方法,通过反射为公共字段赋值
3). 在 Mapper 的方法上加入 AutoFill 注解
若要实现上述步骤,需掌握以下知识(之前课程内容都学过)
技术点: 枚举、注解、AOP、反射
1.3 代码开发 按照上一小节分析的实现步骤依次实现,共三步。
1.3.1 步骤一 自定义注解 AutoFill
进入到sky-server模块,创建com.sky.annotation包。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.sky.annotation;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 { OperationType value () ; }
其中OperationType已在sky-common模块中定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.sky.enumeration;public enum OperationType { UPDATE, INSERT }
1.3.2 步骤二 自定义切面 AutoFillAspect
在sky-server模块,创建com.sky.aspect包。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 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("开始进行公共字段自动填充..." ); } }
完善自定义切面 AutoFillAspect 的 autoFill 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 package com.sky.aspect;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 @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){ 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){ 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 做相同操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.sky.mapper;@Mapper public interface CategoryMapper { @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) ; @AutoFill(value = OperationType.UPDATE) void update (Category category) ; }
同时 ,将业务层为公共字段赋值的代码注释掉。
1). 将员工管理的新增和编辑方法中的公共字段赋值的代码注释。
2). 将菜品分类管理的新增和修改方法中的公共字段赋值的代码注释。
1.4 功能测试 以新增菜品分类 为例,进行测试
启动项目和Nginx
查看控制台
通过观察控制台输出的SQL来确定公共字段填充是否完成
查看表
category表中数据
其中create_time,update_time,create_user,update_user字段都已完成自动填充。
由于使用admin(id=1)用户登录进行菜品添加操作,故create_user,update_user都为1.
1.5 代码提交 点击提交:
提交过程中 ,出现提示:
继续push:
推送成功:
2. 新增菜品 2.1 需求分析与设计 2.1.1 产品原型 后台系统中可以管理菜品信息,通过 新增功能 来添加一个新的菜品,在添加菜品时需要选择当前菜品所属的菜品分类,并且需要上传菜品图片。
新增菜品原型:
当填写完表单信息, 点击”保存”按钮后, 会提交该表单的数据到服务端, 在服务端中需要接受数据, 然后将数据保存至数据库中。
业务规则:
菜品名称必须是唯一的
菜品必须属于某个分类下,不能单独存在
新增菜品时可以根据情况选择菜品的口味
每个菜品必须对应一张图片
2.1.2 接口设计 根据上述原型图先粗粒度 设计接口,共包含3个接口。
接口设计:
接下来细粒度 分析每个接口,明确每个接口的请求方式、请求路径、传入参数和返回值。
1. 根据类型查询分类
2. 文件上传
3. 新增菜品
2.1.3 表设计 通过原型图进行分析:
新增菜品,其实就是将新增页面录入的菜品信息插入到dish表,如果添加了口味做法,还需要向dish_flavor表插入数据。所以在新增菜品时,涉及到两个表:
表名
说明
dish
菜品表
dish_flavor
菜品口味表
1). 菜品表:dish
字段名
数据类型
说明
备注
id
bigint
主键
自增
name
varchar(32)
菜品名称
唯一
category_id
bigint
分类id
逻辑外键
price
decimal(10,2)
菜品价格
image
varchar(255)
图片路径
description
varchar(255)
菜品描述
status
int
售卖状态
1起售 0停售
create_time
datetime
创建时间
update_time
datetime
最后修改时间
create_user
bigint
创建人id
update_user
bigint
最后修改人id
2). 菜品口味表:dish_flavor
字段名
数据类型
说明
备注
id
bigint
主键
自增
dish_id
bigint
菜品id
逻辑外键
name
varchar(32)
口味名称
value
varchar(255)
口味值
2.2 代码开发 2.2.1 文件上传实现 因为在新增菜品时,需要上传菜品对应的图片(文件),包括后绪其它功能也会使用到文件上传,故要实现通用的文件上传接口。
文件上传,是指将本地图片、视频、音频等文件上传到服务器上,可以供其他用户浏览或下载的过程。文件上传在项目中应用非常广泛,我们经常发抖音、发朋友圈都用到了文件上传功能。
实现文件上传服务,需要有存储的支持,那么我们的解决方案将以下几种:
直接将图片保存到服务的硬盘(springmvc中的文件上传)
优点:开发便捷,成本低
缺点:扩容困难
使用分布式文件系统进行存储
优点:容易实现扩容
缺点:开发复杂度稍大(有成熟的产品可以使用,比如:FastDFS,MinIO)
使用第三方的存储服务(例如OSS)
优点:开发简单,拥有强大功能,免维护
缺点:付费
在本项目选用阿里云的OSS服务进行文件存储。(前面课程已学习过阿里云OSS,不再赘述)
实现步骤:
1). 定义OSS相关配置
在sky-server模块
application-dev.yml
1 2 3 4 5 6 sky: alioss: endpoint: oss-cn-hangzhou.aliyuncs.com access-key-id: LTAI5tPeFLzsPPT8gG3LPW64 access-key-secret: U6k1brOZ8gaOIXv3nXbulGTUzy6Pd7 bucket-name: sky-take-out
application.yml
1 2 3 4 5 6 7 8 9 10 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}
2). 读取OSS配置
在sky-common模块中,已定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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; }
3). 生成OSS工具类对象
在sky-server模块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package com.sky.config;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 { @Bean @ConditionalOnMissingBean public AliOssUtil aliOssUtil (AliOssProperties aliOssProperties) { log.info("开始创建阿里云文件上传工具类对象:{}" ,aliOssProperties); return new AliOssUtil (aliOssProperties.getEndpoint(), aliOssProperties.getAccessKeyId(), aliOssProperties.getAccessKeySecret(), aliOssProperties.getBucketName()); } }
其中,AliOssUtil.java已在sky-common模块中定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 package com.sky.utils;import com.aliyun.oss.ClientException;import com.aliyun.oss.OSS;import com.aliyun.oss.OSSClientBuilder;import com.aliyun.oss.OSSException;import lombok.AllArgsConstructor;import lombok.Data;import lombok.extern.slf4j.Slf4j;import java.io.ByteArrayInputStream;@Data @AllArgsConstructor @Slf4j public class AliOssUtil { private String endpoint; private String accessKeyId; private String accessKeySecret; private String bucketName; public String upload (byte [] bytes, String objectName) { OSS ossClient = new OSSClientBuilder ().build(endpoint, accessKeyId, accessKeySecret); try { 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(); } } StringBuilder stringBuilder = new StringBuilder ("https://" ); stringBuilder .append(bucketName) .append("." ) .append(endpoint) .append("/" ) .append(objectName); log.info("文件上传到:{}" , stringBuilder.toString()); return stringBuilder.toString(); } }
4). 定义文件上传接口
在sky-server模块中定义接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 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); } }
2.2.2 新增菜品实现 1). 设计DTO类
在sky-pojo模块中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package com.sky.dto;import com.sky.entity.DishFlavor;import lombok.Data;import java.io.Serializable;import java.math.BigDecimal;import java.util.ArrayList;import java.util.List;@Data public class DishDTO implements Serializable { private Long id; private String name; private Long categoryId; private BigDecimal price; private String image; private String description; private Integer status; private List<DishFlavor> flavors = new ArrayList <>(); }
2). Controller层
进入到sky-server模块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 package com.sky.controller.admin;import com.sky.dto.DishDTO;import com.sky.dto.DishPageQueryDTO;import com.sky.entity.Dish;import com.sky.result.PageResult;import com.sky.result.Result;import com.sky.service.DishService;import com.sky.vo.DishVO;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.*;import java.util.List;import java.util.Set;@RestController @RequestMapping("/admin/dish") @Api(tags = "菜品相关接口") @Slf4j public class DishController { @Autowired private DishService dishService; @PostMapping @ApiOperation("新增菜品") public Result save (@RequestBody DishDTO dishDTO) { log.info("新增菜品:{}" , dishDTO); dishService.saveWithFlavor(dishDTO); return Result.success(); } }
3). Service层接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.sky.service;import com.sky.dto.DishDTO;import com.sky.entity.Dish;public interface DishService { public void saveWithFlavor (DishDTO dishDTO) ; }
4). Service层实现类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 package com.sky.service.impl;@Service @Slf4j public class DishServiceImpl implements DishService { @Autowired private DishMapper dishMapper; @Autowired private DishFlavorMapper dishFlavorMapper; @Transactional public 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.forEach(dishFlavor -> { dishFlavor.setDishId(dishId); }); dishFlavorMapper.insertBatch(flavors); } } }
5). Mapper层
DishMapper.java中添加
1 2 3 4 5 6 7 @AutoFill(value = OperationType.INSERT) void insert (Dish dish) ;
在/resources/mapper中创建DishMapper.xml
1 2 3 4 5 6 7 8 9 10 11 <?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 >
DishFlavorMapper.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.sky.mapper;import com.sky.entity.DishFlavor;import java.util.List;@Mapper public interface DishFlavorMapper { void insertBatch (List<DishFlavor> flavors) ; }
在/resources/mapper中创建DishFlavorMapper.xml
1 2 3 4 5 6 7 8 9 10 11 <?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 功能测试 进入到菜品管理—>新建菜品
由于没有实现菜品查询功能,所以保存后,暂且在表中查看添加的数据。
dish表:
dish_flavor表:
测试成功。
2.4代码提交
后续步骤和上述功能代码提交一致,不再赘述。
3. 菜品分页查询 3.1 需求分析和设计 3.1.1 产品原型 系统中的菜品数据很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。
菜品分页原型:
在菜品列表展示时,除了菜品的基本信息(名称、售价、售卖状态、最后操作时间)外,还有两个字段略微特殊,第一个是图片字段 ,我们从数据库查询出来的仅仅是图片的名字,图片要想在表格中回显展示出来,就需要下载这个图片。第二个是菜品分类,这里展示的是分类名称,而不是分类ID,此时我们就需要根据菜品的分类ID,去分类表中查询分类信息,然后在页面展示。
业务规则:
根据页码展示菜品信息
每页展示10条数据
分页查询时可以根据需要输入菜品名称、菜品分类、菜品状态进行查询
3.1.2 接口设计 根据上述原型图,设计出相应的接口。
3.2 代码开发 3.2.1 设计DTO类 根据菜品分页查询接口定义设计对应的DTO:
在sky-pojo模块中,已定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.sky.dto;import lombok.Data;import java.io.Serializable;@Data public class DishPageQueryDTO implements Serializable { private int page; private int pageSize; private String name; private Integer categoryId; private Integer status; }
3.2.2 设计VO类 根据菜品分页查询接口定义设计对应的VO:
在sky-pojo模块中,已定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package com.sky.vo;import com.sky.entity.DishFlavor;import lombok.AllArgsConstructor;import lombok.Builder;import lombok.Data;import lombok.NoArgsConstructor;import java.io.Serializable;import java.math.BigDecimal;import java.time.LocalDateTime;import java.util.ArrayList;import java.util.List;@Data @Builder @NoArgsConstructor @AllArgsConstructor public class DishVO implements Serializable { private Long id; private String name; private Long categoryId; private BigDecimal price; private String image; private String description; private Integer status; private LocalDateTime updateTime; private String categoryName; private List<DishFlavor> flavors = new ArrayList <>(); }
3.2.3 Controller层 根据接口定义创建DishController的page分页查询方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 @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 中扩展分页查询方法:
1 2 3 4 5 6 7 PageResult pageQuery (DishPageQueryDTO dishPageQueryDTO) ;
3.2.5 Service层实现类 在 DishServiceImpl 中实现分页查询方法:
1 2 3 4 5 6 7 8 9 10 11 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 方法:
1 2 3 4 5 6 7 Page<DishVO> pageQuery (DishPageQueryDTO dishPageQueryDTO) ;
在 DishMapper.xml 中编写SQL:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <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,进入菜品分页查询接口
注意: 使用admin用户登录重新获取token,防止token失效。
点击发送:
3.3.2 前后端联调测试 启动nginx,访问 http://localhost
点击菜品管理
数据成功查出。
4. 删除菜品 4.1 需求分析和设计 4.1.1 产品原型 在菜品列表页面,每个菜品后面对应的操作分别为修改 、删除 、停售 ,可通过删除功能完成对菜品及相关的数据进行删除。
删除菜品原型:
业务规则:
可以一次删除一个菜品,也可以批量删除菜品
起售中的菜品不能删除
被套餐关联的菜品不能删除
删除菜品后,关联的口味数据也需要删除掉
4.1.2 接口设计 根据上述原型图,设计出相应的接口。
注意: 删除一个菜品和批量删除菜品共用一个接口,故ids可包含多个菜品id,之间用逗号分隔。
4.1.3 表设计 在进行删除菜品操作时,会涉及到以下三张表。
注意事项:
在dish表中删除菜品基本数据时,同时,也要把关联在dish_flavor表中的数据一块删除。
setmeal_dish表为菜品和套餐关联的中间表。
若删除的菜品数据关联着某个套餐,此时,删除失败。
若要删除套餐关联的菜品数据,先解除两者关联,再对菜品进行删除。
4.2 代码开发 4.1.2 Controller层 根据删除菜品的接口定义在DishController中创建方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 @DeleteMapping @ApiOperation("菜品批量删除") public Result delete (@RequestParam List<Long> ids) { log.info("菜品批量删除:{}" , ids); dishService.deleteBatch(ids); return Result.success(); }
4.2.2 Service层接口 在DishService接口中声明deleteBatch方法:
1 2 3 4 5 6 void deleteBatch (List<Long> ids) ;
4.2.3 Service层实现类 在DishServiceImpl中实现deleteBatch方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 @Autowired private SetmealDishMapper setmealDishMapper; @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:
1 2 3 4 5 6 7 8 @Select("select * from dish where id = #{id}") Dish getById (Long id) ;
创建SetmealDishMapper,声明getSetmealIdsByDishIds方法,并在xml文件中编写SQL:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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 { List<Long> getSetmealIdsByDishIds (List<Long> dishIds) ; }
SetmealDishMapper.xml
1 2 3 4 5 6 7 8 9 10 11 <?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:
1 2 3 4 5 6 7 @Delete("delete from dish where id = #{id}") void deleteById (Long id) ;
在DishFlavorMapper中声明deleteByDishId方法并配置SQL:
1 2 3 4 5 6 @Delete("delete from dish_flavor where dish_id = #{dishId}") void deleteByDishId (Long dishId) ;
4.3 功能测试 既可以通过Swagger接口文档进行测试,也可以通过前后端联调测试,接下来,我们直接使用前后端联调测试 。
进入到菜品列表查询页面
对测试菜品进行删除操作
同时,进到dish表和dish_flavor两个表查看测试菜品 的相关数据都已被成功删除。
再次,删除状态为启售的菜品
点击批量删除
删除失败,因为起售中的菜品不能删除。
4.4 代码提交
后续步骤和上述功能代码提交一致,不再赘述。
5. 修改菜品 5.1 需求分析和设计 5.1.1 产品原型 在菜品管理列表页面点击修改按钮,跳转到修改菜品页面,在修改页面回显菜品相关信息并进行修改,最后点击保存按钮完成修改操作。
修改菜品原型:
5.1.2 接口设计 通过对上述原型图进行分析,该页面共涉及4个接口。
接口:
根据id查询菜品
根据类型查询分类(已实现)
文件上传(已实现)
修改菜品
我们只需要实现根据id查询菜品 和修改菜品 两个接口,接下来,我们来重点分析这两个接口。
1). 根据id查询菜品
2). 修改菜品
注:因为是修改功能,请求方式可设置为PUT。
5.2 代码开发 5.2.1 根据id查询菜品实现 1). Controller层
根据id查询菜品的接口定义在DishController中创建方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 @GetMapping("/{id}") @ApiOperation("根据id查询菜品") public Result<DishVO> getById (@PathVariable Long id) { log.info("根据id查询菜品:{}" , id); DishVO dishVO = dishService.getByIdWithFlavor(id); return Result.success(dishVO); }
2). Service层接口
在DishService接口中声明getByIdWithFlavor方法:
1 2 3 4 5 6 7 DishVO getByIdWithFlavor (Long id) ;
3). Service层实现类
在DishServiceImpl中实现getByIdWithFlavor方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public DishVO getByIdWithFlavor (Long id) { Dish dish = dishMapper.getById(id); List<DishFlavor> dishFlavors = dishFlavorMapper.getByDishId(id); DishVO dishVO = new DishVO (); BeanUtils.copyProperties(dish, dishVO); dishVO.setFlavors(dishFlavors); return dishVO; }
4). Mapper层
在DishFlavorMapper中声明getByDishId方法,并配置SQL:
1 2 3 4 5 6 7 @Select("select * from dish_flavor where dish_id = #{dishId}") List<DishFlavor> getByDishId (Long dishId) ;
5.2.1 修改菜品实现 1). Controller层
根据修改菜品的接口定义在DishController中创建方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 @PutMapping @ApiOperation("修改菜品") public Result update (@RequestBody DishDTO dishDTO) { log.info("修改菜品:{}" , dishDTO); dishService.updateWithFlavor(dishDTO); return Result.success(); }
2). Service层接口
在DishService接口中声明updateWithFlavor方法:
1 2 3 4 5 6 void updateWithFlavor (DishDTO dishDTO) ;
3). Service层实现类
在DishServiceImpl中实现updateWithFlavor方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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()); }); dishFlavorMapper.insertBatch(flavors); } }
4). Mapper层
在DishMapper中,声明update方法:
1 2 3 4 5 6 7 @AutoFill(value = OperationType.UPDATE) void update (Dish dish) ;
并在DishMapper.xml文件中编写SQL:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <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方式启动项目,观察运行中步骤。
进入菜品列表查询页面,对第一个菜品的价格进行修改
点击修改,回显成功
菜品价格修改后,点击保存
修改成功
5.4 代码提交
后续步骤和上述功能代码提交一致,不再赘述。