# 苍穹外卖 - day03# 课程内容公共字段自动填充 新增菜品 菜品分页查询 删除菜品 修改菜品 功能实现
菜品管理
# 公共字段自动填充# 问题分析在上一章节我们已经完成了后台系统的员工管理功能 和菜品分类功能 的开发,在新增员工 或者新增菜品分类 时需要设置创建时间、创建人、修改时间、修改人等字段,在编辑员工 或者编辑菜品分类 时需要设置修改时间、修改人等字段。这些字段属于公共字段,也就是也就是在我们的系统中很多表中都会有这些字段,如下:
序号 字段名 含义 数据类型 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 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、反射
# 代码开发按照上一小节分析的实现步骤依次实现,共三步。
# 步骤一 自定义注解 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 () ; }
@Target (ElementType.METHOD) 代表给方法注解
@Retention (RetentionPolicy.RUNTIME) 是 Java 中的一个元注解,它用于描述其他注解的生命周期。
这个注解的作用是指定该注解可以被保留到何时:
RetentionPolicy.SOURCE: 注解只在源码级别保留,在编译器编译时就会被忽略。 RetentionPolicy.CLASS: 注解会在 class 文件中存在,但会被 VM 忽略。 RetentionPolicy.RUNTIME: 注解会在运行时也保留,因此可以被反射机制读取。 对于大部分自定义注解来说,我们希望注解能在运行时被程序读取和处理,所以通常会使用 @Retention (RetentionPolicy.RUNTIME) 来修饰自定义注解。这样就可以在程序运行时通过反射 API 获取注解信息,并根据注解信息执行相应的逻辑。
OperationType value();
OperationType 被定义在 sky.common 下的枚举类型 enumeration 包下,并且
value()
就是一个注解属性。它的作用是:
当使用 @AutoFill
注解时,可以为 value()
属性赋值,例如 @AutoFill(OperationType.UPDATE)
或 @AutoFill(OperationType.INSERT)
。 在程序运行时,可以通过反射等方式获取 @AutoFill
注解的 value()
属性值,从而获取该注解的相关信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.sky.enumeration;public enum OperationType { UPDATE, INSERT }
# 步骤二 自定义切面 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 2 Object entity = args[0 ];这里是编码规范,在注解对应的mapper相应的方法中,第一个参数为实体,所以arg[0 ]为实体。
1 2 MethodSignature signature = (MethodSignature) joinPoint.getSignature();AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);
这段代码的作用是:
首先获取当前连接点 (方法执行) 的方法签名信息。 然后从方法签名中获取具体的 Method
对象。 最后从 Method
对象中获取方法上的 @AutoFill
注解实例。 # 步骤三 在 Mapper 接口的方法上加入 AutoFill 注解以 CategoryMapper 为例,分别在新增和修改方法添加 @Auto Fill () 注解,也需要 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). 将菜品分类管理的新增和修改方法中的公共字段赋值的代码注释。
# 新增菜品# 需求分析与设计# 产品原型后台系统中可以管理菜品信息,通过 新增功能 来添加一个新的菜品,在添加菜品时需要选择当前菜品所属的菜品分类,并且需要上传菜品图片。
当填写完表单信息,点击 "保存" 按钮后,会提交该表单的数据到服务端,在服务端中需要接受数据,然后将数据保存至数据库中。
业务规则:
菜品名称必须是唯一的 菜品必须属于某个分类下,不能单独存在 新增菜品时可以根据情况选择菜品的口味 每个菜品必须对应一张图片 # 接口设计根据上述原型图先粗粒度 设计接口,共包含 3 个接口。
接口设计:
接下来细粒度 分析每个接口,明确每个接口的请求方式、请求路径、传入参数和返回值。
1. 根据类型查询分类
2. 文件上传
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) 口味值
# 代码开发# 文件上传实现因为在新增菜品时,需要上传菜品对应的图片 (文件),包括后绪其它功能也会使用到文件上传,故要实现通用的文件上传接口。
文件上传,是指将本地图片、视频、音频等文件上传到服务器上,可以供其他用户浏览或下载的过程。文件上传在项目中应用非常广泛,我们经常发抖音、发朋友圈都用到了文件上传功能。
实现文件上传服务,需要有存储的支持,那么我们的解决方案将以下几种:
直接将图片保存到服务的硬盘(springmvc 中的文件上传)优点:开发便捷,成本低 缺点:扩容困难 使用分布式文件系统进行存储优点:容易实现扩容 缺点:开发复杂度稍大(有成熟的产品可以使用,比如:FastDFS,MinIO) 使用第三方的存储服务(例如 OSS)优点:开发简单,拥有强大功能,免维护 缺点:付费 在本项目选用阿里云的 OSS 服务进行文件存储。
实现步骤:
记得先在总工程 pom.xml 文件里导入依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <properties > ··· <aliyun.sdk.oss > 3.10.2</aliyun.sdk.oss > ··· </properties > <dependencyManagement > ··· <dependency > <groupId > com.aliyun.oss</groupId > <artifactId > aliyun-sdk-oss</artifactId > <version > ${aliyun.sdk.oss}</version > </dependency > ··· </dependencyManagement >
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); } }
# 新增菜品实现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 <?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 >
# 菜品分页查询# 需求分析和设计# 产品原型系统中的菜品数据很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据
菜品分页原型:
在菜品列表展示时,除了菜品的基本信息 (名称、售价、售卖状态、最后操作时间) 外,还有两个字段略微特殊,第一个是图片字段 ,我们从数据库查询出来的仅仅是图片的名字,图片要想在表格中回显展示出来,就需要下载这个图片。第二个是菜品分类,这里展示的是分类名称,而不是分类 ID,此时我们就需要根据菜品的分类 ID,去分类表中查询分类信息,然后在页面展示。
业务规则:
根据页码展示菜品信息 每页展示 10 条数据 分页查询时可以根据需要输入菜品名称、菜品分类、菜品状态进行查询 # 接口设计根据上述原型图,设计出相应的接口。
# 代码开发# 设计 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; }
# 设计 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 <>(); }
# 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); }
# Service 层接口在 DishService 中扩展分页查询方法:
1 2 3 4 5 6 7 PageResult pageQuery (DishPageQueryDTO dishPageQueryDTO) ;
# 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()); }
# 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 >
# 删除菜品# 需求分析和设计# 产品原型在菜品列表页面,每个菜品后面对应的操作分别为修改 、删除 、停售 ,可通过删除功能完成对菜品及相关的数据进行删除。
删除菜品原型:
业务规则:
可以一次删除一个菜品,也可以批量删除菜品 起售中的菜品不能删除 被套餐关联的菜品不能删除 删除菜品后,关联的口味数据也需要删除掉 # 接口设计根据上述原型图,设计出相应的接口。
注意:
删除一个菜品和批量删除菜品共用一个接口,故 ids 可包含多个菜品 id, 之间用逗号分隔。
# 表设计在进行删除菜品操作时,会涉及到以下三张表。
注意事项:
在 dish 表中删除菜品基本数据时,同时,也要把关联在 dish_flavor 表中的数据一块删除。 setmeal_dish 表为菜品和套餐关联的中间表。 若删除的菜品数据关联着某个套餐,此时,删除失败。 若要删除套餐关联的菜品数据,先解除两者关联,再对菜品进行删除。 # 代码开发# 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(); }
# Service 层接口在 DishService 接口中声明 deleteBatch 方法:
1 2 3 4 5 6 void deleteBatch (List<Long> ids) ;
# 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); } }
# 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 个接口。
接口:
根据 id 查询菜品 根据类型查询分类 (已实现) 文件上传 (已实现) 修改菜品 我们只需要实现根据 id 查询菜品 和修改菜品 两个接口,接下来,我们来重点分析这两个接口。
1). 根据 id 查询菜品
2). 修改菜品
注:因为是修改功能,请求方式可设置为 PUT。
# 代码开发# 根据 id 查询菜品实现# 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); }
# Service 层接口在 DishService 接口中声明 getByIdWithFlavor 方法:
1 2 3 4 5 6 7 DishVO getByIdWithFlavor (Long id) ;
# 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; }
# 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) ;
# 修改菜品实现# 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(); }
# Service 层接口在 DishService 接口中声明 updateWithFlavor 方法:
1 2 3 4 5 6 void updateWithFlavor (DishDTO dishDTO) ;
# 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); } }
# 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 >