苍穹外卖-day03

YangeIT大约 28 分钟苍穹外卖文件上传AOP环绕通知反射事务

苍穹外卖-day03

课程内容

  • 公共字段自动填充
  • 新增菜品
  • 菜品分页查询
  • 删除菜品
  • 修改菜品

功能实现:🎯 菜品管理

管理端原型open in new window

用户端原型open in new window

菜品管理效果图:

image-20221121142133307

1. 公共字段自动填充 🍐 ✏️ 🚩

1.1 问题分析 🍐

在上一章节我们已经完成了后台系统的员工管理功能菜品分类功能的开发,在新增员工或者新增菜品分类时需要设置创建时间、创建人、修改时间、修改人 等字段,在编辑员工或者编辑菜品分类时需要设置修改时间、修改人等字段。这些字段属于公共字段 ,也就是也就是在我们的系统中很多表中都会有这些字段,如下:

公共字段

序号字段名含义数据类型
1create_time创建时间datetime
2create_user创建人idbigint
3update_time修改时间datetime
4update_user修改人idbigint

而针对于这些字段,我们的赋值方式为:

  1. 在新增数据时, 将createTime、updateTime 设置为当前时间, createUser、updateUser设置为当前登录用户ID。
  2. 在更新数据时, 将updateTime 设置为当前时间, updateUser设置为当前登录用户ID。

其中updateUser和createUser通过ThreadLocal来传递值

当前的赋值方式

  1. 在每一个业务方法中进行赋值操作,非常繁琐
点击查看代码

新增员工方法:

	/**
     * 新增员工
     *
     * @param employeeDTO
     */
    public void save(EmployeeDTO employeeDTO) {
        //.......................
		//////////////////////////////////////////
        //设置当前记录的创建时间和修改时间
        employee.setCreateTime(LocalDateTime.now());
        employee.setUpdateTime(LocalDateTime.now());

        //设置当前记录创建人id和修改人id
        employee.setCreateUser(BaseContext.getCurrentId());//目前写个假数据,后期修改
        employee.setUpdateUser(BaseContext.getCurrentId());
		///////////////////////////////////////////////
        employeeMapper.insert(employee);
    }

编辑员工方法:

	/**
     * 编辑员工信息
     *
     * @param employeeDTO
     */
    public void update(EmployeeDTO employeeDTO) {
       //........................................
	   ///////////////////////////////////////////////
        employee.setUpdateTime(LocalDateTime.now());
        employee.setUpdateUser(BaseContext.getCurrentId());
       ///////////////////////////////////////////////////

        employeeMapper.update(employee);
    }

新增菜品分类方法:

	/**
     * 新增分类
     * @param categoryDTO
     */
    public void save(CategoryDTO categoryDTO) {
       //....................................
       //////////////////////////////////////////
        //设置创建时间、修改时间、创建人、修改人
        category.setCreateTime(LocalDateTime.now());
        category.setUpdateTime(LocalDateTime.now());
        category.setCreateUser(BaseContext.getCurrentId());
        category.setUpdateUser(BaseContext.getCurrentId());
        ///////////////////////////////////////////////////

        categoryMapper.insert(category);
    }

修改菜品分类方法:

	/**
     * 修改分类
     * @param categoryDTO
     */
    public void update(CategoryDTO categoryDTO) {
        //....................................
        
		//////////////////////////////////////////////
        //设置修改时间、修改人
        category.setUpdateTime(LocalDateTime.now());
        category.setUpdateUser(BaseContext.getCurrentId());
        //////////////////////////////////////////////////

        categoryMapper.update(category);
    }

统一处理方式

使用AOP切面编程,实现功能增强,来完成公共字段自动填充功能

1.2 实现思路

  1. 在实现公共字段自动填充,也就是在插入或者更新 的时候为指定字段赋予指定的值,使用它的好处就是可以统一对这些字段进行处理,避免了重复代码。在上述的问题分析中,我们提到有四个公共字段,需要在新增/更新中进行赋值操作, 具体情况如下:
序号字段名含义数据类型操作类型
1create_time创建时间datetimeinsert(插入)
2create_user创建人idbigintinsert (插入)
3update_time修改时间datetimeinsert、update (插入或更新)
4update_user修改人idbigintinsert、update (插入或更新)

实现步骤

  1. 自定义注解 AutoFill,用于标识需要进行公共字段自动填充的方法
  2. 自定义切面类 AutoFillAspect,统一拦截加入了 AutoFill注解的方法,通过反射为公共字段赋值
  3. Mapper的方法 上加入 AutoFill 注解,并注释业务层赋值时间和用户Id的代码

若要实现上述步骤,需掌握以下知识(之前课程内容都学过)

技术点: 枚举、注解、AOP、反射

1.3 代码开发 ✏️

1.3.1 步骤一

自定义注解 AutoFill

进入到sky-server模块,创建com.sky.annotation包。

package com.sky.annotation;

/**
 * 自定义注解,用于标识某个方法需要进行功能字段自动填充处理
 */
@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.4 功能测试

新增菜品分类为例,进行测试

启动项目和Nginx

image-20221121154642757

查看控制台

通过观察控制台输出的SQL来确定公共字段填充是否完成

image-20221121154841887
image-20221121154841887

查看表

category表中数据

image-20221121155104152
image-20221121155104152

其中create_time,update_time,create_user,update_user字段都已完成自动填充。

由于使用admin(id=1)用户登录进行菜品添加操作,故create_user,update_user都为1.

1.5 代码提交 ⬆️

点击查看提交步骤

点击提交:

image-20221121155718603

提交过程中,出现提示:

image-20221121155824695

继续push:

image-20221121160002324

推送成功:

image-20221121160037460

2. 新增菜品 🚩

2.1 需求分析与设计

2.1.1 产品原型

后台系统中可以管理菜品信息,通过 新增功能 来添加一个新的菜品,在添加菜品时需要选择当前菜品所属的菜品分类,并且需要上传菜品图片

新增菜品原型:

image-20221121164131337

当填写完表单信息, 点击"保存"按钮后, 会提交该表单的数据到服务端, 在服务端中需要接受数据, 然后将数据保存至数据库中。

业务规则

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

2.1.2 接口设计

苍穹外卖-管理端接口open in new window

根据上述原型图先粗粒度设计接口,共包含3个接口。

1. 根据类型查询分类

image-20221121165033612 image-20221121165043619

2.1.3 表设计

通过原型图进行分析:

image-20221121165917874
  1. 新增菜品,其实就是将新增页面录入的菜品信息插入到dish表
  2. 如果添加了口味做法,还需要向dish_flavor表 插入数据。

所以在新增菜品时,涉及到两个表:

表名说明
dish菜品表
dish_flavor菜品口味表

1). 菜品表:dish

字段名数据类型说明备注
idbigint主键自增
namevarchar(32)菜品名称唯一
category_idbigint分类id逻辑外键
pricedecimal(10,2)菜品价格
imagevarchar(255)图片路径
descriptionvarchar(255)菜品描述
statusint售卖状态1起售 0停售
create_timedatetime创建时间
update_timedatetime最后修改时间
create_userbigint创建人id
update_userbigint最后修改人id

2.2 代码开发 ✏️

2.2.1 文件上传实现

因为在新增菜品时,需要上传菜品对应的图片(文件),包括后绪其它功能也会使用到文件上传,故要实现通用的文件上传接口。

文件上传,是指将本地图片、视频、音频等文件上传到服务器上,可以供其他用户浏览或下载的过程。文件上传在项目中应用非常广泛,我们经常发抖音、发朋友圈都用到了文件上传功能。

实现文件上传服务,需要有存储的支持,那么我们的解决方案将以下几种:

  1. 直接将图片保存到服务的硬盘(springmvc中的文件上传)
    1. 优点:开发便捷,成本低
    2. 缺点:扩容困难
  2. 使用分布式文件系统进行存储
    1. 优点:容易实现扩容
    2. 缺点:开发复杂度稍大(有成熟的产品可以使用,比如:FastDFS,MinIO)
  3. 使用第三方的存储服务(例如OSS)
    1. 优点:开发简单,拥有强大功能,免维护
    2. 缺点:付费

在本项目选用阿里云的OSS服务进行文件存储。(前面课程已学习过阿里云OSS,不再赘述)

image-20221121174942235

实现步骤:

  1. 定义OSS相关配置
  2. 读取OSS配置
  3. 生成OSS工具类对象
  4. 定义文件上传接口

1). 定义OSS相关配置

在sky-server模块

application-dev.yml

sky:
  alioss:
    endpoint: oss-cn-hangzhou.aliyuncs.com
    access-key-id: LTAI5tPeFLzsPPT8gG3LPW64
    access-key-secret: U6k1brOZ8gaOIXv3nXbulGTUzy6Pd7
    bucket-name: sky-take-out

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}

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.3 功能测试

进入到菜品管理--->新建菜品

image-20221121195440804

由于没有实现菜品查询功能,所以保存后,暂且在表中查看添加数据。

dish表:

image-20221121195737692

dish_flavor表:

image-20221121195902555

测试成功。

2.4代码提交 ⬆️

image-20221121200332933

后续步骤和上述功能代码提交一致,不再赘述。

3. 菜品分页查询 🚩

前言

产品原型

系统中的菜品数据很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。

菜品分页原型:

image-20221121201552489

在菜品列表展示时,除了菜品的基本信息(名称、售价、售卖状态、最后操作时间)外,还有两个字段略微特殊,第一个是图片字段 ,我们从数据库查询出来的仅仅是图片的名字,图片要想在表格中回显展示出来,就需要下载这个图片。第二个是菜品分类,这里展示的是分类名称,而不是分类ID,此时我们就需要根据菜品的分类ID,去分类表中查询分类信息,然后在页面展示。

业务规则

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

代码操作

在sky-pojo模块中,已定义

package com.sky.dto;

@Data
public class DishPageQueryDTO implements Serializable {
    private int page;
    private int pageSize;
    private String name;
    //分类id
    private Integer categoryId;
    //状态 0表示禁用 1表示启用
    private Integer status;

}

总结

课堂作业

  1. 分页查询的sql关键字有哪些?🎤

4. 删除菜品 🚩

删除菜品

产品原型

在菜品列表页面,每个菜品后面对应的操作分别为修改删除停售,可通过删除功能完成对菜品及相关的数据进行删除。

删除菜品原型:

image-20221121211236356

业务规则:

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

代码操作

Controller层

根据删除菜品的接口定义在DishController中创建方法:

	/**
     * 菜品批量删除
     *
     * @param ids
     * @return
     */
    @DeleteMapping
    @ApiOperation("菜品批量删除")
    public Result delete(@RequestParam List<Long> ids) {
        log.info("菜品批量删除:{}", ids);
        dishService.deleteBatch(ids);//后绪步骤实现
        return Result.success();
    }

总结

  • 在dish表中删除菜品基本数据时,同时,也要把关联在dish_flavor表中的数据一块删除。
  • setmeal_dish表为菜品和套餐关联的中间表。
  • 若删除的菜品数据关联着某个套餐,此时,删除失败。
  • 若要删除套餐关联的菜品数据,先解除两者关联,再对菜品进行删除。

课堂作业

  1. 为什么要在项目中使用自定义异常?🎤
  2. 使用什么类可以捕获这些异常?🎤

5. 修改菜品 🚩

前言

产品原型

在菜品管理列表页面点击修改按钮,跳转到修改菜品页面,在修改页面回显菜品相关信息并进行修改,最后点击保存按钮完成修改操作

修改菜品原型:

image-20221122130837173

代码开发

根据id查询菜品实现

1). Controller层

根据id查询菜品的接口定义在DishController中创建方法:

    /**
     * 根据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);
    }

修改菜品实现

1). Controller层

根据修改菜品的接口定义在DishController中创建方法:

	/**
     * 修改菜品
     *
     * @param dishDTO
     * @return
     */
    @PutMapping
    @ApiOperation("修改菜品")
    public Result update(@RequestBody DishDTO dishDTO) {
        log.info("修改菜品:{}", dishDTO);
        dishService.updateWithFlavor(dishDTO);
        return Result.success();
    }

总结

课堂作业

  1. 查询一对多数据分装,传统方式和collection方式都进行尝试 ,理解区别🎤

课后作业

🚩 1. 重点完成上述的课堂作业

  1. 晚自习第一节课的前30分钟,总结完毕之后,每个同学先必须梳理今日知识点 (记得写不知道的,以及感恩三件事);整理好的笔记可以发给组长,组长交给班长,意在培养大家总结的能力)

  2. 晚自习第一节课的后30分钟开始练习(记住:程序员是代码堆起来的):

  3. 剩余的时间:预习第二天的知识,预习的时候一定要注意:

  • 预习不是学习,不要死看第二天的视频(很容易出现看了白看,为了看视频而看视频)
  • 预习看第二天的笔记,把笔记中标注重要的知识,可以找到预习视频,先看一遍,如果不懂的 ,记住做好标注。

面试题

  1. AOP是什么?解决了什么问题?应用场景?
  2. AOP应用各个通知类型以及执行时间
  3. Aop中切面可以有多个吗?可以作用于同一个切入点方法吗?
  4. MyBatis之association和collection标签的区别,对应的应用场景
  5. 为什么要使用批量插入?相比循环插入有点有哪些?
  6. Mybatis的resultMap是什么意思?和resultType有什么区别?
  1. resultmap:resultMap如果查询出来的列名和pojo的属性名不一致,通过定义一个resultMap对列名和pojo属性名之间作一个映射关系。
  2. resulttype:resultType使用resultType进行输出映射,只有查询出来的列名和pojo中的属性名一致,该列才可以映射成功。