• 苍穹外卖项目笔记(4)——菜品管理


    菜品管理

    主要功能模块:新建菜品、修改菜品、启用禁用菜品、菜品的分页查询、删除菜品

    代码:GitHub - Echo0701/take-out

    1 公共字段自动填充

           公共字段指的是业务表中有一些相同的字段,比如创建人、创建时间、修改人、修改时间等,我们在维护这些数据的时候,都需要为这几个字段来赋值,为避免代码重复,引入公共字段自动填充的内容。

    1.1 问题分析

    业务表中存在公共字段,导致 Java 中出现冗余的代码,后期如果进行变更,不方便维护

    1.2 实现思路

    明确上述字段的操作时机

    • 自定义注解 AutoFill ,用于标识需要进行公共字段自动填充的方法
    • 自定义切面类 AutoFillAspect ,统一拦截加入了 AutoFill 注解的方法,通过反射为公共字段赋值 
    • 在  Mapper 的方法上加入 AutoFill 注解

    【技术点】枚举(标识当前操作的类型)、注解、AOP、反射

    1.3 代码开发

    AutoFill.java

    1. /**
    2. * 自定义注解,用于表示某个方法需要进行公共字段填充处理
    3. */
    4. @Target(ElementType.METHOD) //指定注解只能加在方法上面
    5. @Retention(RetentionPolicy.RUNTIME) //指明注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在,且可以在运行时通过反射获取到。这样的注解可以用来在运行时进行一些特殊的操作,例如动态生成代码、动态代理等
    6. public @interface AutoFill {
    7. //指定当前数据库的操作的类型:update insert
    8. OperationType value();
    9. }

    AutoFillAspect.java

    1. @Aspect
    2. @Component
    3. @Slf4j //记录日志
    4. public class AutoFillAspect {
    5. //定义切入点:哪些类的哪些方法来进行拦截
    6. /**
    7. * 切入点
    8. */
    9. @Pointcut("execution(* com.sky.mapper.*.*(..) && @annotation(com.sky.annotation.AutoFill)")
    10. public void autoFillPointCut() {
    11. }
    12. /**
    13. * 前置通知,在这个部分进行公共字段的赋值
    14. */
    15. //定义前置通知,因为要在 update 和 insert 之前为公共字段赋值
    16. //当匹配上切点表达式的时候就会执行我们这个通知的方法
    17. @Before("autoFillPointCut")
    18. public void autoFill(JoinPoint joinPoint) {
    19. log.info("开始进行公共字段的填充...");
    20. //1、获取到当前被拦截的方法上的数据库操作类型
    21. MethodSignature signature = (MethodSignature) joinPoint.getSignature(); //方法签名对象
    22. AutoFill autoFill= signature.getMethod().getAnnotation(AutoFill.class); //获得方法上的注解对象
    23. OperationType operationType = autoFill.value(); // 获得数据库操作类型
    24. //2、获取到当前被拦截的方法的参数--实体对象
    25. Object[] args= joinPoint.getArgs();
    26. if(args == null || args.length == 0) {
    27. return;
    28. }
    29. Object entity = args[0];
    30. //3、准备赋值的数据
    31. LocalDateTime now = LocalDateTime.now();
    32. Long currentId = BaseContext.getCurrentId();
    33. //4、根据当前的不同操作类型,为对应的属性通过反射来赋值
    34. if (operationType == OperationType.INSERT) {
    35. //为四个公共字段赋值
    36. try {
    37. //获得了四个公共字段的set方法
    38. // Method setCreateTime = entity.getClass().getDeclaredMethod("setCreateTime",LocalDateTime.class); 为了避免方法拼写错误,所以统一用方法常量,定义在common
    39. Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME,LocalDateTime.class);
    40. Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER,Long.class);
    41. Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME,LocalDateTime.class);
    42. Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER,Long.class);
    43. //通过反射来为对象属性赋值
    44. setCreateTime.invoke(entity,now);
    45. setCreateUser.invoke(entity,currentId);
    46. setUpdateTime.invoke(entity,now);
    47. setUpdateUser.invoke(entity,currentId);
    48. } catch (Exception e) {
    49. throw new RuntimeException(e);
    50. }
    51. } else if (operationType == OperationType.UPDATE) {
    52. //为两个公共字段赋值
    53. try {
    54. //获得了2个公共字段的set方法
    55. Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME,LocalDateTime.class);
    56. Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER,Long.class);
    57. //通过反射来为对象属性赋值
    58. setUpdateTime.invoke(entity,now);
    59. setUpdateUser.invoke(entity,currentId);
    60. } catch (Exception e) {
    61. throw new RuntimeException(e);
    62. }
    63. }
    64. }
    65. }

    2 新增菜品

    2.1 需求分析和设计

    产品原型

    业务规则

    • 菜品名称必须唯一
    • 菜品必须属于某个分类下,不能单独存在
    • 新增菜品时可以根据情况选择菜品的口味
    • 每个菜品必须对应一张图片

    接口设计 

    ① 根据类型查询分类

    ② 文件上传

    ③ 新增菜品

    数据库设计

    【逻辑外键】:数据库里并没有将这个外界关系真的给创建出来,而是通过我们的程序自己去维护这个字段,换句话说,数据库并不认为当前这个字段是一个外界,但在程序中会将其当作外键字段来处理

    2.2 代码开发

    2.2.1 文件上传

    利用阿里云OSS来存储对象,完成文件上传

    OssConfiguration.java

    1. package com.sky.config;
    2. import com.sky.properties.AliOssProperties;
    3. import com.sky.utils.AliOssUtil;
    4. import lombok.extern.slf4j.Slf4j;
    5. import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
    6. import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
    7. import org.springframework.context.annotation.Bean;
    8. import org.springframework.context.annotation.Configuration;
    9. /**
    10. * 配置类,用于创建 AliOssUtil 对象
    11. */
    12. @Configuration
    13. @Slf4j
    14. public class OssConfiguration {
    15. @Bean
    16. @ConditionalOnMissingBean //保证整个spring容器里只有一个util对象,当没有util的条件下才会创建这个bean
    17. public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties) {
    18. log.info("开始创建阿里云文件上传工具类对象:{}",aliOssProperties);
    19. return new AliOssUtil(aliOssProperties.getEndpoint(),
    20. aliOssProperties.getAccessKeyId(),
    21. aliOssProperties.getAccessKeySecret(),
    22. aliOssProperties.getBucketName());
    23. }
    24. }

     application-dev.yml

    1. sky:
    2. datasource:
    3. driver-class-name: com.mysql.cj.jdbc.Driver
    4. host: localhost
    5. port: 3306
    6. database: sky_take_out
    7. username: root
    8. password: root
    9. alioss:
    10. endpoint: https://oss-cn-hangzhou.aliyuncs.com
    11. access-key-id: LTAI5tLmKyefze8CGYs47HQt
    12. access-key-secret: MvO3k0AIAm4GSNujs0fLug4Chyu4sB
    13. bucket-name: cq-takeout-test

     application.yml

    1. sky:
    2. jwt:
    3. # 设置jwt签名加密时使用的秘钥
    4. admin-secret-key: itcast
    5. # 设置jwt过期时间
    6. admin-ttl: 7200000
    7. # 设置前端传递过来的令牌名称
    8. admin-token-name: token
    9. alioss:
    10. endpoint: ${sky.alioss.endpoint}
    11. access-key-id: ${sky.alioss.access-key-id}
    12. access-key-secret: ${sky.alioss.access-key-secret}
    13. bucket-name: ${sky.alioss.bucket-name}

    【代码逻辑】配置文件里配置了几个配置项,这几个配置项通过配置属性类来加载,然后又编写了配置类,通过这个配置类就可以创建出所需要的对象

    上传文件的通用控制类 CommonController.java

    1. /**
    2. * 通用接口
    3. */
    4. @RestController
    5. @RequestMapping("/admin/common")
    6. @Api(tags = "通用接口")
    7. @Slf4j
    8. public class CommonController {
    9. //这里的 file 要和前端接口请求的参数名相同
    10. private AliOssUtil aliOssUtil;
    11. /**
    12. * 文件上传
    13. * @param file
    14. * @return
    15. */
    16. @PostMapping("/upload")
    17. @ApiOperation("文件上传")
    18. public Result<String> upload(MultipartFile file) {
    19. log.info("文件上传:{}",file);
    20. try {
    21. //原始文件名
    22. String originalFilename = file.getOriginalFilename();
    23. //截取原始文件后缀名
    24. String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
    25. //构建新文件名称
    26. String objectName = UUID.randomUUID().toString() + extension;
    27. //文件的请求路径
    28. String filePath = aliOssUtil.upload(file.getBytes(), objectName);
    29. return Result.success(filePath);
    30. } catch (IOException e) {
    31. log.info("文件上传失败:{}", e);
    32. }
    33. return Result.error(MessageConstant.UPLOAD_FAILED);
    34. }
    35. }

    2.2.2 新增菜品 

     DishController.java

    1. /**
    2. * 菜品管理
    3. */
    4. @RestController
    5. @RequestMapping("/admin/dish")
    6. @Api(tags = "菜品相关接口")
    7. @Slf4j
    8. public class DishController {
    9. @Autowired
    10. private DishService dishService;
    11. /**
    12. * 新增菜品
    13. * @param dishDTO
    14. * @return
    15. */
    16. @PostMapping
    17. @ApiOperation("新增菜品")
    18. public Result save(@RequestBody DishDTO dishDTO) {
    19. log.info("新增菜品:{}",dishDTO);
    20. dishService.saveWithFlavor(dishDTO);
    21. return Result.success();
    22. }
    23. }

     DishServiceImpl.java

    1. @Service
    2. @Slf4j
    3. public class DishServiceImpl implements DishService {
    4. @Autowired
    5. private DishMapper dishMapper;
    6. private DishFlavorMapper dishFlavorMapper;
    7. /**
    8. * 新增菜品和口味
    9. * @param dishDTO
    10. */
    11. @Transactional
    12. public void saveWithFlavor(DishDTO dishDTO) {
    13. Dish dish = new Dish();
    14. BeanUtils.copyProperties(dishDTO,dish);
    15. //向菜品表插入一条数据
    16. dishMapper.insert(dish);
    17. //获取insert语句生成的主键值
    18. Long dishId = dish.getId();
    19. List<DishFlavor> flavors = dishDTO.getFlavors();
    20. if(flavors != null && flavors.size() > 0) {
    21. flavors.forEach(dishFlavor -> {
    22. dishFlavor.setDishId(dishId);
    23. });
    24. //向口味表插入n条数据
    25. dishFlavorMapper.insertBatch(flavors);
    26. }
    27. }
    28. }

     DishFlavorMapper.java

    1. @Mapper
    2. public interface DishFlavorMapper {
    3. /**
    4. * 批量插入口味数据
    5. * @param flavors
    6. */
    7. void insertBatch(List<DishFlavor> flavors);
    8. //因为要用到动态sql,所以写到映射文件里面去
    9. }

      DishMapper.java

    1. @Mapper
    2. public interface DishMapper {
    3. /**
    4. * 根据分类id查询菜品数量
    5. * @param categoryId
    6. * @return
    7. */
    8. @Select("select count(id) from dish where category_id = #{categoryId}")
    9. Integer countByCategoryId(Long categoryId);
    10. /**
    11. * 插入菜品数据
    12. * @param dish
    13. */
    14. @AutoFill(value = OperationType.INSERT)
    15. void insert(Dish dish);
    16. //这里插入的数据很多,所以写到映射文件里面去
    17. }

    3 菜品分页查询

    3.1 需求分析和设计

    产品原型

    业务规则

    • 根据页码展示菜品信息
    • 每页展示十条数据
    • 分页查询时可以根据需要输入菜品名称、菜品分类、菜品状态进行查询

    接口设计

    3.2 代码开发

    ① 根据菜品分页查询接口定义设计对应的DTO:

    ② 根据菜品分页查询接口定义设计对应的VO:

    【注】把这个vo转换成json数据传到前端,前端就可以正常去显示 

    DishController.java

    1. @GetMapping("/page")
    2. @ApiOperation("菜品分页查询")
    3. public Result page(DishPageQueryDTO dishPageQueryDTO){
    4. log.info("菜品分页查询:{}",dishPageQueryDTO);
    5. PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);
    6. return Result.success(pageResult);
    7. }

    DishServiceImpl.java

    1. /**
    2. * 菜品分页查询
    3. * @param dishPageQueryDTO
    4. * @return
    5. */
    6. public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) {
    7. //利用PageHelper插件来进行分页操作
    8. //开始分页
    9. PageHelper.startPage(dishPageQueryDTO.getPage(),dishPageQueryDTO.getPageSize());
    10. //开始查询
    11. Page<DishVO> page = dishMapper.pageQuery(dishPageQueryDTO);
    12. return new PageResult(page.getTotal(),page.getResult());
    13. }

    4 删除菜品

    4.1 需求分析和设计

    产品原型

    业务规则

    • 可以一次删除一个菜品,也可以批量删除菜品
    • 起售中的菜品不能删除
    • 被套餐关联的菜品不能删除
    • 删除菜品以后,关联的口味数据也需要删除掉 

    接口设计

    数据库设计

    【注】setmeal_dish表:套餐和菜品关系表 

    4.2 代码开发

    DishServiceImpl.java

    1. /**
    2. * 菜品批量删除
    3. * @param ids
    4. */
    5. //加上事务注解,保证事务的一致性
    6. @Transactional
    7. public void deleteBatch(List<Long> ids) {
    8. //判断当前菜品是否能够删除---是否存在起售中菜品?
    9. for (Long id : ids) {
    10. Dish dish = dishMapper.getById(id);
    11. if(dish.getStatus() == StatusConstant.ENABLE) {
    12. //当前菜品出于起售状态,不能删除
    13. throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
    14. }
    15. }
    16. //判断当前菜品是否能够删除---是否被套餐关联?
    17. List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(ids);
    18. if(setmealIds != null && setmealIds.size() > 0) {
    19. //存在套餐关联当前菜品,不能删除
    20. throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
    21. }
    22. //删除菜品表中的菜品数据
    23. for (Long id : ids) {
    24. dishMapper.deleteById(id);
    25. //删除菜品关联的口味数据
    26. dishFlavorMapper.deleteByDishId(id);
    27. }
    28. }

    DishController.java

    1. /**
    2. * 菜品批量删除
    3. * @return
    4. */
    5. @DeleteMapping
    6. @ApiOperation("菜品批量删除")
    7. public Result delete(@RequestParam List ids) {
    8. log.info("菜品批量删除:{}",ids);
    9. dishService.deleteBatch(ids);
    10. return Result.success();
    11. }

    5 修改菜品

    5.1 需求分析和设计

    产品原型

     接口设计

    • 根据 id 查询菜品
    • 根据类型查询分类(已实现)
    • 文件上传(已实现)
    • 修改菜品

    ① 根据 id 查询菜品

    ② 修改菜品 

    5.2 代码开发

    DishController.java

    1. /**
    2. * 根据 id 查询菜品
    3. * @param id
    4. * @return
    5. */
    6. @GetMapping("/{id}")
    7. @ApiOperation("根据 id 查询菜品")
    8. public Result getById(@PathVariable Long id) {
    9. log.info("根据id查询菜品:{}",id);
    10. DishVO dishVO = dishService.getByIdWithFlavor(id);
    11. return Result.success(dishVO);
    12. }
    13. /**
    14. * 修改菜品
    15. * @param dishDTO
    16. * @return
    17. */
    18. @PutMapping
    19. @ApiOperation("修改菜品")
    20. public Result update(@RequestBody DishDTO dishDTO) {
    21. log.info("修改菜品:{}",dishDTO);
    22. dishService.updateWithFlavor(dishDTO);
    23. return Result.success();
    24. }

    DishServiceImpl.java

    1. /**
    2. * 根据 id 查询菜品和对应的口味数据
    3. * @param id
    4. * @return
    5. */
    6. public DishVO getByIdWithFlavor(Long id) {
    7. //根据 id 查询菜品数据
    8. Dish dish = dishMapper.getById(id);
    9. //根据菜品 id 查询口味数据
    10. List<DishFlavor> dishFlavors = dishFlavorMapper.getByDishId(id);
    11. //将查询到的数据封装到VO
    12. DishVO dishVO = new DishVO();
    13. BeanUtils.copyProperties(dish,dishVO);
    14. dishVO.setFlavors(dishFlavors);
    15. return dishVO;
    16. }
    17. /**
    18. * 根据 id 修改菜品的基本信息和对应的口味信息
    19. * @param dishDTO
    20. */
    21. public void updateWithFlavor(DishDTO dishDTO) {
    22. //因为口味的修改比较复杂,可能是追加口味,也可能是删除口味,也可能是全部更改口味
    23. //所以这里的统一操作为先删除,再按照实际的需求进行插入操作
    24. Dish dish = new Dish();
    25. BeanUtils.copyProperties(dishDTO,dish);
    26. //修改菜品表的基本信息
    27. dishMapper.update(dish);
    28. //删除原有的口味数据
    29. dishFlavorMapper.deleteByDishId(dishDTO.getId());
    30. //重新插入口味数据
    31. List<DishFlavor> flavors = dishDTO.getFlavors();
    32. if(flavors != null && flavors.size() > 0) {
    33. flavors.forEach(dishFlavor -> {
    34. dishFlavor.setDishId(dishDTO.getId());
    35. });
    36. //向口味表插入n条数据
    37. dishFlavorMapper.insertBatch(flavors);
    38. }
    39. }

  • 相关阅读:
    jQuery
    分布式精讲系列 实现分布式服务应该具备哪些核心技术组件?
    java计算机毕业设计无人智慧药柜系统设计源码+lw文档+系统+数据库
    前端src中图片img标签资源的几种写法?
    java 二叉树的增删查
    Javaweb之HTML,CSS的详细解析
    【LabVIEW FPGA入门】NI 环境安装教程
    vue使用腾讯地图获取经纬度和逆解析获取详细地址
    Sqlserver并行等待CXPACKET、CXCONSUMER问题的解决思路和案例
    Python 框架学习 Django篇 (三) 链接数据库
  • 原文地址:https://blog.csdn.net/wohuishidalao/article/details/134473994