菜品表: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
菜品口味表:dish_flavor
字段名
数据类型
说明
备注
id
bigint
主键
自增
dish_id
bigint
菜品id
逻辑外键
name
varchar(32)
口味名称
value
varchar(255)
口味值
套餐菜品表:setmeal_dish
字段名
数据类型
说明
备注
id
bigint
主键
自增
setmeal_id
varchar(32)
套餐id
逻辑外键
dish_id
bigint
菜品id
逻辑外键
name
varchar(32)
口味名称
price
decimal(10,2)
菜品价格
copies
INT
菜品份数
1.公共字段自动填充 在上一章节我们已经完成了后台系统的员工管理功能和菜品分类功能的开发,在新增员工或者新增菜品分类时需要设置创建时间、创建人、修改时间、修改人等字段,在编辑员工或者编辑菜品分类时需要设置修改时间、修改人等字段。这些字段属于公共字段,也就是也就是在我们的系统中很多表中都会有这些字段,如下:
序号
字段名
含义
数据类型
1
create_time
创建时间
datetime
2
create_user
创建人id
bigint
3
update_time
修改时间
datetime
4
update_user
修改人id
bigint
而针对于这些字段,我们的赋值方式为:
在新增 数据时, 将createTime、updateTime 设置为当前 时间, createUser、updateUser 设置为当前 登录用户ID。
在更新 数据时, 将updateTime 设置为当前 时间, updateUser 设置为当前 登录用户ID。
目前,在我们的项目中处理这些字段都是在每一个业务方法中进行赋值操作。
1.1 实现思路 在实现公共字段自动填充,也就是在插入或者更新的时候为指定字段赋予指定的值,使用它的好处就是可以统一对这些字段进行处理,避免了重复代码。在上述的问题分析中,我们提到有四个公共字段,需要在新增/更新中进行赋值操作, 具体情况如下:
序号
字段名
含义
数据类型
操作类型
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.2 代码开发 1.2.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 package com.sky.enumeration;public enum OperationType { UPDATE, INSERT }
1.2.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 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.3 在Mapper接口的方法上加入 AutoFill 注解 分别在新增和修改方法添加 @AutoFill() 注解,同时,将新增和修改方法中的公共字段赋值的代码注释。
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: 注册oss自己获取 access-key-secret: 注册oss自己获取 bucket-name: 注册oss自己申请设置
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 新增菜品 Impl类
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 @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); } }
在/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 >
在/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 >
3.菜品分页查询 在 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 >
4.删除菜品 业务规则:
可以一次删除一个 菜品,也可以批量 删除菜品
起售中 的菜品不能 删除
被套餐关联 的菜品不能 删除
删除菜品后,关联的口味数据 也需要删除掉注意 :删除一个菜品和批量删除菜品共用 一个接口,故ids可包含多个菜品id,之间用逗号分隔。
在 Impl 类中实现 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); } }
创建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 >
5. 修改菜品 5.1 根据id查询菜品 在 Impl 类中实现 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; }
5.2 修改菜品 在 Impl 类中实现 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); } }