springboot|一个SpringBoot单体项目--》外卖平台项目之后台管理端基础功能开发


外卖平台

  • 一 、项目基础实现
    • 1.1、搭建项目之项目引入静态资源
    • 1.2、项目功能实现 --> 后台用户
      • 1.2.1 -->后台用户登录逻辑的实现
        • 1.2.1.1 前后端交互流程
        • 1.2.1.2 流程说明
      • 1.2.2-->后台用户登出逻辑的实现
      • 1.2.2.1 请求流程分析
      • 1.2.3-->后台新增员工逻辑的实现
        • 1.2.3.1 程序执行流程
        • 1.2.3.2 代码实现流程
        • 1.2.3.2 该功能实现注意事项
      • 1.2.4-->后台员工信息的分页查询逻辑的实现
        • 1.2.4.1 程序执行流程
        • 1.2.4.2 代码实现
      • 1.2.5-->后台启用/禁用员工逻辑的实现
        • 1.2.5.1 展示效果
        • 1.2.5.2 程序执行流程
        • 1.2.5.3 代码实现后出现的问题
      • 1.2.6-->后台编辑员工逻辑的实现
        • 1.2.6.1 程序执行流程
          • 1.2.6.2 根据ID查询员工
          • 1.2.6.3 修改员工
    • 1.3 关于实现MP提供的公共字段填充
      • 1.3.1 基于ThreadLocal封装的工具类
      • 1.3.2 自动填充类
    • 1.4 项目后台功能实现 --> 分类管理
      • 1.4.1 新增分类
        • 1.4.1.1 程序执行过程
        • 1.4.1.1 代码实现
      • 1.4.2 分类信息的分页查询
        • 1.4.2.1 程序执行过程
        • 1.4.2.2 代码实现
      • 1.4.3 删除分类
        • 1.4.3.1 执行流程
        • 1.4.3.2 代码实现
      • 1.4.4 修改分类
        • 1.4.4.1 分析
        • 1.4.4.1 实现
    • 1.5 项目后台功能实现 --> 菜品管理
      • 1.5.1 关于菜品管理 需要上传菜品图片 ==》涉及文件上传与下载
        • 上传逻辑:
        • 下载逻辑:
        • 具体实现:
      • 1.5.2 菜品新增
        • 1.5.2.1 交互流程
          • 各种类型的实体模型
        • 1.5.2.2 代码实现
      • 1.5.3 菜品分页查询
        • 1.5.3.1 需求分析
        • 1.5.3.2 前端和服务端交互过程
        • 1.5.3.3 代码实现
      • 1.5.3 菜品修改
        • 1.5.3.1 需求分析
        • 1.5.3.2 交互流程
        • 1.5.3.3 功能实现
          • 根据ID查询菜品信息
          • 修改菜品信息
    • 1.6 项目后台功能实现 --> 套餐管理
      • 1.6.1 --> 新增套餐
        • 1.6.1.1 需求分析
        • 1.6.1.2 数据模型
        • 1.6.1.3 前端页面与服务端的交互过程
        • 1.6.1.4 代码开发
          • 根据分类查询菜品
          • 保存套餐
      • 1.6.2 --> 套餐的分页查询
        • 1.6.2.1 前端页面和服务端的交互过程
        • 1.6.2.2 代码开发
          • 基本信息查询
      • 1.6.3 --> 删除套餐
        • 1.6.3.1 --> 需求分析
        • 1.6.3.2 --> 交互过程
        • 1.6.3.3 --> 代码实现

一 、项目基础实现 1.1、搭建项目之项目引入静态资源
关于SpringBoot一体化开发,可以将静态资源(html/css/js等)放在resource文件下,然后编写一个SpringMvc的WebMvcConfig(需要继承一个WebMvcConfigurationSupport,重写一个addResoucehandlers(增加来源处理器)方法)进行响应配置代码的编写; demo:

import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; @Slf4j @Configuration public class WebMvcConfig extends WebMvcConfigurationSupport { /** * 设置静态资源映射 * @param registry */ @Override protected void addResourceHandlers(ResourceHandlerRegistry registry) { log.info("开始进行静态资源映射..."); registry.addResourceHandler("/backend/**(访问谁)").addResourceLocations("classpath:/backend/(映射到哪里去)"); registry.addResourceHandler("/front/** 访问谁)").addResourceLocations("classpath:/front/"(映射到哪里去)); } }

1.2、项目功能实现 --> 后台用户 1.2.1 -->后台用户登录逻辑的实现
1.2.1.1 前后端交互流程 springboot|一个SpringBoot单体项目--》外卖平台项目之后台管理端基础功能开发
文章图片

1.2.1.2 流程说明
前端发送一个post请求,请求体中携带本次请求应该带有的参数,在后台,经过javaweb提供的过滤器(使用它,需要在启动类中加入@ServletConponentScan),进行访问路径的过滤,然后放行到员工的cotroller层,在cotroller进行账号是否存在,密码是否正确,以及该账号的状态是否已被禁用,然后同一个返回一个结果给前端控制器,前端控制器将数据给前端页面; 注意:在登录成功后,要记录该登录用户到session域对象中,以备之后对用户信息的回显,判断用户登录状态,登出用户的操作;

1.2.2–>后台用户登出逻辑的实现
1.2.2.1 请求流程分析
前端发一个post请求到后端,后端清理session中保存的用户id,返回结果

1.2.3–>后台新增员工逻辑的实现
1.2.3.1 程序执行流程
A. 点击"保存"按钮, 页面发送ajax请求,将新增员工页面中输入的数据以json的形式提交到服务端, 请求方式POST, 请求路径 /employee B. 服务端Controller接收页面提交的数据并调用Service将数据进行保存 C. Service调用Mapper操作数据库,保存数据

1.2.3.2 代码实现流程
A. 在新增员工时, 按钮页面原型中的需求描述, 需要给员工设置初始默认密码 123456, 并对密码进行MD5加密。 B. 在组装员工信息时, 还需要封装创建时间、修改时间,创建人、修改人信息(从session中获取当前登录用户)。

1.2.3.2 该功能实现注意事项
因为在数据库中对用户名进行了唯一的约束,假设,前台提交的增加用户的用户名,在数据库中已经存在,便会抛出一个Duplicate entry异常;显然,在spring中如果让它抛出了,就不得劲了,此时我们便使用Spring提供的全局异常处理机制,也就是说,所有的异常处理都交给spring,spring对异常进行处理;

import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import java.sql.SQLIntegrityConstraintViolationException; /** * 全局异常处理 */ @ControllerAdvice(annotations = {RestController.class, Controller.class}) @ResponseBody @Slf4j public classGlobalExceptionHandler {/** * 异常处理方法 * @return */ @ExceptionHandler(SQLIntegrityConstraintViolationException.class) public R exceptionHandler(SQLIntegrityConstraintViolationException ex){ log.error(ex.getMessage()); if(ex.getMessage().contains("Duplicate entry")){ String[] split = ex.getMessage().split(" "); String msg = split[2] + "已存在"; return R.error(msg); } return R.error("未知错误"); } }

注解说明:
@ControllerAdvice : 指定拦截那些类型的控制器;
? @ResponseBody: 将方法的返回值 R 对象转换为json格式的数据, 响应给页面;
1.2.4–>后台员工信息的分页查询逻辑的实现
1.2.4.1 程序执行流程 A. 点击菜单,打开员工管理页面时,执行查询:
1). 页面发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端
2). 服务端Controller接收页面提交的数据, 并组装条件调用Service查询数据
3). Service调用Mapper操作数据库,查询分页数据
4). Controller将查询到的分页数据, 响应给前端页面
5). 页面接收到分页数据, 并通过ElementUI的Table组件展示到页面上
B. 搜索栏输入员工姓名,回车,执行查询:
1). 页面发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端
2). 服务端Controller接收页面提交的数据, 并组装条件调用Service查询数据
3). Service调用Mapper操作数据库,查询分页数据
4). Controller将查询到的分页数据, 响应给前端页面
5). 页面接收到分页数据, 并通过ElementUI的Table组件展示到页面上
最终发送给服务端的请求为 : GET请求 , 请求链接 /employee/page?page=1&pageSize=10&name=xxx
1.2.4.2 代码实现
1. 分页查询==》本项目dao层框架使用的是mybatis+mybatispuls==〉在dao层框架中提供了分页插件,如果要使用,只需要进行相应配置与导入即可; 2. 配置分页插件步骤:创建一个mybatisPlus的配置文件,要加上@Configration注解,然后编写一个返回返回值为mybatisPlusInterceptor(mybatisplus的拦截器)的方法;在方法中new一个mybatisPlusInterceptor,然后调用该对象中addInnerInterceptor(增加内部拦截器方法),该方法的概述为分页内部拦截器: demo:

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * 配置MP的分页插件 */ @Configuration public class MybatisPlusConfig {@Bean public MybatisPlusInterceptor mybatisPlusInterceptor(){ MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor()); return mybatisPlusInterceptor; } }

3. cotroller中代码实现逻辑 1. 构造分页构造器;new 一个page<>对象 2. 构造条件构造器 3. 添加过滤条件 4. 添加排序条件 5. 执行查询

1.2.5–>后台启用/禁用员工逻辑的实现
1.2.5.1 展示效果
员工的启用\禁用操作中,在前台两者的操作只能是管理员进行,也就是说只有管理员(admin用户)可以对其他普通用户进行启用、禁用操作,所以普通用户登录系统后启用、禁用按钮不显示,这是在前台代码中对登录的用户进行了一个判断;

1.2.5.2 程序执行流程
1). 页面发送ajax请求,将参数(id、status)提交到服务端 2). 服务端Controller接收页面提交的数据并调用Service更新数据 3). Service调用Mapper操作数据库 注意:启用、禁用员工账号,本质上就是一个更新操作,也就是对status状态字段进行操作。在Controller中创建update方法,此方法是一个通用的修改员工信息的方法。

1.2.5.3 代码实现后出现的问题
关于后台查询出来的员工对象的id值为一个19位的Long类型,但是前端的Long类型只能处理16位,此时需要我们将后端发给前台的long类型的数据,提前转为String类型,而当前台提交过来也会将String再转为Long 具体实现: 该功能是由于在SpringMVC中, 将Controller方法返回值转换为json对象, 是通过jackson来实现的, 涉及到SpringMVC中的一个消息转换器MappingJackson2HttpMessageConverter, 解决方法是对这个消息转换器进行扩展 实现步骤: 1). 提供对象转换器JacksonObjectMapper,基于Jackson进行Java对象到json数据的转换

package com.itheima.reggie.common; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer; import java.math.BigInteger; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.format.DateTimeFormatter; import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES; /** * 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象 * 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象] * 从Java对象生成JSON的过程称为 [序列化Java对象到JSON] */ public class JacksonObjectMapper extends ObjectMapper {public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd"; public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss"; public JacksonObjectMapper() { super(); //收到未知属性时不报异常 this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false); //反序列化时,属性不存在的兼容处理 this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); SimpleModule simpleModule = new SimpleModule() .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT))) .addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT))) .addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT))).addSerializer(BigInteger.class, ToStringSerializer.instance) .addSerializer(Long.class, ToStringSerializer.instance) //long ==》String .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT))) .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT))) .addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT))); //注册功能模块 例如,可以添加自定义序列化器和反序列化器 this.registerModule(simpleModule); } }

2). 在WebMvcConfig配置类中扩展Spring mvc的消息转换器,在此消息转换器中使用提供的对象转换器进行Java对象到json数据的转换

/** * 扩展mvc框架的消息转换器 * @param converters */ @Override protected void extendMessageConverters(List> converters) { log.info("扩展消息转换器..."); //创建消息转换器对象 MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter(); //设置对象转换器,底层使用Jackson将Java对象转为json messageConverter.setObjectMapper(new JacksonObjectMapper()); //将上面的消息转换器对象追加到mvc框架的转换器集合中 converters.add(0,messageConverter); }

1.2.6–>后台编辑员工逻辑的实现
1.2.6.1 程序执行流程
1). 点击编辑按钮时,页面跳转到add.html,并在url中携带参数[员工id] 2). 在add.html页面获取url中的参数[员工id] 3). 发送ajax请求,请求服务端,同时提交员工id参数 4). 服务端接收请求,根据员工id查询员工信息,将员工信息以json形式响应给页面 5). 页面接收服务端响应的json数据,通过VUE的数据绑定进行员工信息回显 6). 点击保存按钮,发送ajax请求,将页面中的员工信息以json方式提交给服务端 7). 服务端接收员工信息,并进行处理,完成后给页面响应 8). 页面接收到服务端响应信息后进行相应处理

1.2.6.2 根据ID查询员工 代码实现:
/** * 根据id查询员工信息 * @param id * @return */ @GetMapping("/{id}") public R getById(@PathVariable Long id){ log.info("根据id查询员工信息..."); Employee employee = employeeService.getById(id); if(employee != null){ return R.success(employee); } return R.error("没有查询到对应员工信息"); }

1.2.6.3 修改员工 代码实现:
/** * 根据id修改员工信息 * @param employee * @return */ @PutMapping public R update(HttpServletRequest request,@RequestBody Employee employee){ log.info(employee.toString()); Long empId = (Long)request.getSession().getAttribute("employee"); employee.setUpdateTime(LocalDateTime.now()); employee.setUpdateUser(empId); employeeService.updateById(employee); return R.success("员工信息修改成功"); }

1.3 关于实现MP提供的公共字段填充 分析:我们可以从数据库中各个表中观察得知,每一张数据表都含有createUser、createTi me、updateUser、updateTime这些公共字段,而在本项目的技术选型中,选用mybatis以及它的最好搭档mybatisplus进行dao层的框架的搭建,而在mybatisplus中提供了公共字段自动填充的功能,通过在实体类中需要填充的字段加上@TableField注解以及编写一个自定义元数据类去实现一个MetaObjectHandler接口,实现其中的方法,该类需要交给spring容器进行管理;在通过元数据对象.setValue去设置公共字段填充内容;
关于自动填充创建/更新用户ID,子啊自定义元数据类中在通过session与获取userID的自动填充,显然是不现实的,接下来想另外一个方法去进行登录用户的信息的保存与提取—》通过实现得知,当一个请求来到后端—〉首先经过了过滤器,然后经过cotroller ,然后经过元数据处理类,最后在去service,dao层,而这一条数据处理链,都是由一个线程来处理的,也就是说,客户端发送的每次http请求,对应的在服务端都会分配一个新的线程来处理,在处理过程中涉及到下面类中的方法都属于相同的一个线程:所以我们可以用这个线程先保存信息,通过thread中threadlocal局部表量,去设置/获取参数;
ThreadLocal常用方法:
A. public void set(T value) : 设置当前线程的线程局部变量的值
B. public T get() : 返回当前线程所对应的线程局部变量的值
C. public void remove() : 删除当前线程所对应的线程局部变量的值
1.3.1 基于ThreadLocal封装的工具类
作用:设置线程中数据与获取线程中数据
实现:
package cn.zkwf.takeout.common; import cn.zkwf.takeout.entity.Employee; /** * @description:线程数据共享工具类 * @author: Sw_Ljb * @PACKAGE_NAME:cn.zkwf.takeout.common * @time: 2022/7/21 下午12:53 * @version: 1.0 */public class ThreadContext {//1、直接new出来这个对象 范性T是参数类型 private static ThreadLocal threadLocal = new ThreadLocal(); /** * 设置参数值 * @param empId */ public static void setParameter(Long empId ){ threadLocal.set(empId); }/** * 得到参数 * @return */ public static Long getParameter(){ return threadLocal.get(); }}

1.3.2 自动填充类
package cn.zkwf.takeout.common; import cn.zkwf.takeout.entity.Employee; import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.reflection.MetaObject; import org.springframework.stereotype.Component; import java.time.LocalDateTime; /** * @description: 元数据处理类 * @author: Sw_Ljb * @PACKAGE_NAME:cn.zkwf.takeout.common * @time: 2022/7/21 下午12:46 * @version: 1.0 */ @Component @Slf4j public class MyMetaObjectHandler implements MetaObjectHandler {@Override public void insertFill(MetaObject metaObject) { log.info("MP进入插入时自动注入元素信息"); //1、设置自动注入时间 metaObject.setValue("createTime", LocalDateTime.now()); metaObject.setValue("updateTime",LocalDateTime.now()); //2、设置自动注入操作人员 因为session不能在这使用 又由于这一条执行链是同一个线程 因此使用localthread进行参数共享 //2.1 得到线程中的值 Long empID = ThreadContext.getParameter(); metaObject.setValue("createUser",empID); metaObject.setValue("updateUser",empID); }@Override public void updateFill(MetaObject metaObject) { log.info("MP进入更新数据时自动注入元素信息"); //1、设置更新自动注入时间 metaObject.setValue("updateTime",LocalDateTime.now()); //2、设置自动注入操作人员 因为session不能在这使用 又由于这一条执行链是同一个线程 因此使用localthread进行参数共享 //2.1 得到线程中的值 Long empID = ThreadContext.getParameter(); metaObject.setValue("updateUser",empID); } }

1.4 项目后台功能实现 --> 分类管理 1.4.1 新增分类
1.4.1.1 程序执行过程 1). 在页面(backend/page/category/list.html)的新增分类表单中填写数据,点击 “确定” 发送ajax请求,将新增分类窗口输入的数据以json形式提交到服务端
2). 服务端Controller接收页面提交的数据并调用Service将数据进行保存
3). Service调用Mapper操作数据库,保存数据
1.4.1.1 代码实现
1. 基础环境的搭建(实体类+mapper(继承basemapper<实体类>)+service(interface IService<实体类>)(serviceimpl(extends ServiceImpl<实体类mapper,实体类> interface service))+controller); 2. 代码实现流程:前端将数据转为json传递给后端,后端在controller中使用@RequestBody注解 使用实体类去接受,然后直接调用service直接操作;

1.4.2 分类信息的分页查询
1.4.2.1 程序执行过程 1). 页面发送ajax请求,将分页查询参数(page、pageSize)提交到服务端
2). 服务端Controller接收页面提交的数据并调用Service查询数据
3). Service调用Mapper操作数据库,查询分页数据
4). Controller将查询到的分页数据响应给页面
5). 页面接收到分页数据并通过ElementUI的Table组件展示到页面上
1.4.2.2 代码实现
/** * 分页查询 * @param page * @param pageSize * @return */ @GetMapping("/page") public R page(int page,int pageSize){ //分页构造器 Page pageInfo = new Page<>(page,pageSize); //条件构造器 LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); //添加排序条件,根据sort进行排序 queryWrapper.orderByAsc(Category::getSort); //分页查询 categoryService.page(pageInfo,queryWrapper); return R.success(pageInfo); }

1.4.3 删除分类
1.4.3.1 执行流程 1). 点击删除,页面发送ajax请求,将参数(id)提交到服务端
2). 服务端Controller接收页面提交的数据并调用Service删除数据
3). Service调用Mapper操作数据库
1.4.3.2 代码实现 分析:在执行删除分类操作时,删除逻辑不能时直接将分类,而是应该先检查当前分类下是否关联了菜品或者套餐,然后将这个分类的id拿着,去菜品或者套餐中查询是否含有该分类的数据,如果有抛一个自定义异常,如果没有,则可以删除该分类;
实现步骤:
  1. 创建自定义异常
package cn.zkwf.takeout.exception; /** * @description: * @author: Sw_Ljb * @PACKAGE_NAME:cn.zkwf.takeout.exception * @time: 2022/7/21 下午2:05 * @version: 1.0 */public class CostomExcetion extends RuntimeException{public CostomExcetion(String message) { super(message); } }

  1. 在全局异常处理器中将自定义异常加入
    全局异常处理器:
package cn.zkwf.takeout.common; import cn.zkwf.takeout.exception.CostomExcetion; import cn.zkwf.takeout.resulttype.R; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import java.sql.SQLIntegrityConstraintViolationException; /** * @description: * @author: Sw_Ljb * @PACKAGE_NAME:cn.zkwf.takeout.common * @time: 2022/7/20 下午1:53 * @version: 1.0 */ @Slf4j @ControllerAdvice(annotations = {RestController.class, Controller.class}) @ResponseBody public class GlobalExceptionHandler {@ExceptionHandler(SQLIntegrityConstraintViolationException.class) public RexceptionHandler(SQLIntegrityConstraintViolationException ex){ //1、打印日志 log.info("自定义全局异常处理正在运行!!"); //Duplicate entry 'zhangsan' for key 'employee.idx_username' if (ex.getMessage().contains("Duplicate entry ")){ //2、判断当前抛出的异常是否为Duplicate entry String[] strArray = ex.getMessage().split(" "); //3、截取异常信息中重点错误信息 String msg= strArray[2] +"已经存在了!!"; //4、设置返回信息 return R.error(msg); } //4、设置返回信息 return R.error("未知错误!"); }@ExceptionHandler(CostomExcetion.class) public RcostomerExceptionHandler(CostomExcetion ex){ //1、打印日志 log.info("自定义全局异常处理正在运行!!==>自定义异常"); return R.error(ex.getMessage()); }}

  1. 扩展分类service,实现该方法
    CategoryServiceImpl.remove():
@Autowired private DishService dishService; @Autowired private SetmealService setmealService; /** * 根据id删除分类,删除之前需要进行判断 * @param id */ @Override public void remove(Long id) { //添加查询条件,根据分类id进行查询菜品数据 LambdaQueryWrapper dishLambdaQueryWrapper = new LambdaQueryWrapper<>(); dishLambdaQueryWrapper.eq(Dish::getCategoryId,id); int count1 = dishService.count(dishLambdaQueryWrapper); //如果已经关联,抛出一个业务异常 if(count1 > 0){ throw new CustomException("当前分类下关联了菜品,不能删除"); //已经关联菜品,抛出一个业务异常 }//查询当前分类是否关联了套餐,如果已经关联,抛出一个业务异常 LambdaQueryWrapper setmealLambdaQueryWrapper = new LambdaQueryWrapper<>(); setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId,id); int count2 = setmealService.count(setmealLambdaQueryWrapper); if(count2 > 0){ throw new CustomException("当前分类下关联了套餐,不能删除"); //已经关联套餐,抛出一个业务异常 }//正常删除分类 super.removeById(id); }

1.4.4 修改分类
1.4.4.1 分析
当前端点"修改"按钮的时候,当信息被修改后,点击保存按钮,页面在发送一个请求,去修改分类信息

1.4.4.1 实现
回显:回显这一步的操作前端已经实现

springboot|一个SpringBoot单体项目--》外卖平台项目之后台管理端基础功能开发
文章图片

修改:
/** * 根据id修改分类信息 * @param category * @return */ @PutMapping public R update(@RequestBody Category category){ log.info("修改分类信息:{}",category); categoryService.updateById(category); return R.success("修改分类信息成功"); }

1.5 项目后台功能实现 --> 菜品管理 1.5.1 关于菜品管理 需要上传菜品图片 ==》涉及文件上传与下载
文件上传三要素:
表单属性 取值 说明
method post 必须选择post方式提交
enctype multipart/form-data 采用multipart格式上传文件
type file 使用input的file控件上传
文件下载两种表现形式
  1. 以附件形式下载,弹出保存对话框,将文件保存到指定磁盘目录
  2. 直接在浏览器中打开,通过浏览器进行文件下载,本质上就是服务端将文件以流的形式写回浏览器的过程。
上传逻辑: 1). 获取文件的原始文件名, 通过原始文件名获取文件后缀
2). 通过UUID重新声明文件名, 文件名称重复造成文件覆盖
3). 创建文件存放目录
4). 将上传的临时文件转存到指定位置
下载逻辑: 1). 定义输入流,通过输入流读取文件内容
2). 通过response对象,获取到输出流
3). 通过response对象设置响应数据格式(image/jpeg)
4). 通过输入流读取文件数据,然后通过上述的输出流写回浏览器
5). 关闭资源
具体实现:
package cn.zkwf.takeout.controller; import cn.zkwf.takeout.resulttype.R; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletResponse; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.UUID; /** * @description: * http://localhost:7070/common/upload * Request Method: POST * @author: Sw_Ljb * @PACKAGE_NAME:cn.zkwf.takeout.controller * @time: 2022/7/22 下午2:54 * @version: 1.0 */ @Slf4j @RestController @RequestMapping("/common") public class FileController {@Value(value = "https://www.it610.com/article/${serverpath.serverImpPath}") private String serverImpPath; //Content-Disposition: form-data; name="file"; filename="0a3b3288-3446-4420-bbff-f263d0c02d8e.jpg" @RequestMapping("/upload") public R upload(MultipartFile file){ log.info("上传路径{}:",serverImpPath); //更保险的做法 将上传过来的照片再次创建一个uuid String suffdex = file.getOriginalFilename().substring(file.getOriginalFilename().indexOf('.')); String imgName = UUID.randomUUID().toString()+suffdex; try { //上传路径/Users/mac/Desktop/fliedemo/takeoutServerImg保险期间对目录进行一次判断 File imgfile = new File(serverImpPath); if (!imgfile.exists()){ imgfile.mkdirs(); } file.transferTo(new File(serverImpPath+"/"+imgName)); } catch (IOException e) { e.printStackTrace(); } return R.success(imgName); } /** * 下载数据 * @param response * @param name */ @GetMapping("/download") public void download(HttpServletResponse response, String name){ log.info("下载文件{}",name); try { //输入流 FileInputStream fis = new FileInputStream(new File(serverImpPath+"/"+name)); log.info(serverImpPath+"/"+name); //输出流 ServletOutputStream outputStream = response.getOutputStream(); //先读出来 再响应给前台//设置响应类型 response.setContentType("image/jpeg"); //1、定义一个读出来存的字节数组 byte[] bytes = new byte[1024]; //2、定一个读多少 int len= 0; while ((len = fis.read(bytes))!=-1){ //3、写会前台 outputStream.write(bytes,0,len); outputStream.flush(); } //4、记得关流,释放资源 outputStream.close(); fis.close(); } catch (Exception e) { e.printStackTrace(); } } }

1.5.2 菜品新增
分析:新增菜品,其实就是将新增页面录入的菜品信息插入到dish表,如果添加了口味做法,还需要向dish_flavor表插入数据。所以在新增菜品时,涉及到两个表:dish/dish_flavor
1.5.2.1 交互流程 1). 点击新建菜品按钮, 访问页面(backend/page/food/add.html), 页面加载时发送ajax请求,请求服务端获取菜品分类数据并展示到下拉框中
2). 页面发送请求进行图片上传,请求服务端将图片保存到服务器(上传功能已实现)
3). 页面发送请求进行图片下载,将上传的图片进行回显(下载功能已实现)
4). 点击保存按钮,发送ajax请求,将菜品相关数据以json形式提交到服务端
对于前端传递到服务端的数据分析:
如果使用菜品类Dish来封装,只能封装菜品的基本属性,flavors属性是无法封装的。这个时候,我们需要自定义一个实体类,然后继承自 Dish,并对Dish的属性进行拓展,增加 flavors 集合属性(内部封装DishFlavor)。
各种类型的实体模型 springboot|一个SpringBoot单体项目--》外卖平台项目之后台管理端基础功能开发
文章图片

1.5.2.2 代码实现 页面传递的菜品口味信息,仅仅包含name 和 value属性,缺少一个非常重要的属性dishId, 所以在保存完菜品的基本信息后,我们需要获取到菜品ID,然后为菜品口味对象属性dishId赋值。
具体逻辑如下:
①. 保存菜品基本信息 ;
②. 获取保存的菜品ID ;
③. 获取菜品口味列表,遍历列表,为菜品口味对象属性dishId赋值;
④. 批量保存菜品口味列表;
注意:由于需要进行了两次数据库的保存操作,操作了两张表,那么为了保证数据的一致性,我们需要在方法上加上注解 @Transactional来控制事务。Service层方法上加的注解@Transactional要想生效,需要在引导类上加上注解 @EnableTransactionManagement, 开启对事务的支持。
代码实现:
@Autowired private DishFlavorService dishFlavorService; /** * 新增菜品,同时保存对应的口味数据 * @param dishDto */ @Transactional public void saveWithFlavor(DishDto dishDto) { //保存菜品的基本信息到菜品表dish this.save(dishDto); Long dishId = dishDto.getId(); //菜品id //菜品口味 List flavors = dishDto.getFlavors(); flavors = flavors.stream().map((item) -> { item.setDishId(dishId); return item; }).collect(Collectors.toList()); //保存菜品口味数据到菜品口味表dish_flavor dishFlavorService.saveBatch(flavors); }

1.5.3 菜品分页查询
1.5.3.1 需求分析 系统中的菜品数据很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。
springboot|一个SpringBoot单体项目--》外卖平台项目之后台管理端基础功能开发
文章图片

在菜品列表展示时,除了菜品的基本信息(名称、售价、售卖状态、更新时间)外,还有两个字段略微特殊,第一个是图片字段 ,我们从数据库查询出来的仅仅是图片的名字,图片要想在表格中回显展示出来,就需要下载这个图片。第二个是菜品分类,这里展示的是分类名称,而不是分类ID,此时我们就需要根据菜品的分类ID,去分类表中查询分类信息,然后在页面展示。
1.5.3.2 前端和服务端交互过程 1). 访问页面(backend/page/food/list.html)时,发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端,获取分页数据
2). 页面发送请求,请求服务端进行图片下载,用于页面图片展示
1.5.3.3 代码实现 分析:
实体类 Dish 中,仅仅包含 categoryId, 不包含 categoryName,这里我们可以返回DishDto对象,在该对象中我们可以拓展一个属性 categoryName,来封装菜品分类名称。
具体逻辑为:
1). 构造分页条件对象
2). 构建查询及排序条件
3). 执行分页条件查询
4). 遍历分页查询列表数据,根据分类ID查询分类信息,从而获取该菜品的分类名称
5). 封装数据并返回
实现:
/** * 菜品信息分页查询 * @param page * @param pageSize * @param name * @return */ @GetMapping("/page") public R page(int page,int pageSize,String name){ //构造分页构造器对象 Page pageInfo = new Page<>(page,pageSize); Page dishDtoPage = new Page<>(); //条件构造器 LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); //添加过滤条件 queryWrapper.like(name != null,Dish::getName,name); //添加排序条件 queryWrapper.orderByDesc(Dish::getUpdateTime); //执行分页查询 dishService.page(pageInfo,queryWrapper); //对象拷贝 BeanUtils.copyProperties(pageInfo,dishDtoPage,"records"); List records = pageInfo.getRecords(); List list = records.stream().map((item) -> {DishDto dishDto = new DishDto(); BeanUtils.copyProperties(item,dishDto); Long categoryId = item.getCategoryId(); //分类id //根据id查询分类对象 Category category = categoryService.getById(categoryId); if(category != null){ String categoryName = category.getName(); dishDto.setCategoryName(categoryName); } return dishDto; }).collect(Collectors.toList()); dishDtoPage.setRecords(list); return R.success(dishDtoPage); }

数据库查询菜品信息时,获取到的分页查询结果 Page 的泛型为 Dish,而我们最终需要给前端页面返回的类型为 DishDto,所以这个时候就要进行转换,基本属性我们可以直接通过属性拷贝的形式对Page中的属性进行复制,而对于结果列表 records属性,是需要进行特殊处理的(需要封装菜品分类名称);
1.5.3 菜品修改
1.5.3.1 需求分析 在菜品管理列表页面点击修改按钮,跳转到修改菜品页面,在修改页面回显菜品相关信息并进行修改,最后点击确定按钮完成修改操作。
1.5.3.2 交互流程 1). 点击菜品列表的中的修改按钮,携带菜品id跳转至add.html
2). 进入add.html,页面发送ajax请求,请求服务端获取分类数据,用于菜品分类下拉框中数据展示
3). add.html获取id, 发送ajax请求,请求服务端,根据id查询当前菜品信息,用于菜品信息回显
4). 页面发送请求,请求服务端进行图片下载,用于页图片回显
5). 点击保存按钮,页面发送ajax请求,将修改后的菜品相关数据以json形式提交到服务端
我们只需要在这里实现两个功能即可:
1). 根据ID查询菜品及菜品口味信息
2). 修改菜品及菜品口味信息
1.5.3.3 功能实现 根据ID查询菜品信息
具体逻辑为: A. 根据ID查询菜品的基本信息 B. 根据菜品的ID查询菜品口味列表数据 C. 组装数据并返回

/** * 根据id查询菜品信息和对应的口味信息 * @param id * @return */ public DishDto getByIdWithFlavor(Long id) { //查询菜品基本信息,从dish表查询 Dish dish = this.getById(id); DishDto dishDto = new DishDto(); BeanUtils.copyProperties(dish,dishDto); //查询当前菜品对应的口味信息,从dish_flavor表查询 LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(DishFlavor::getDishId,dish.getId()); List flavors = dishFlavorService.list(queryWrapper); dishDto.setFlavors(flavors); return dishDto; }

修改菜品信息
实现步骤: 点击保存按钮,页面发送ajax请求,将修改后的菜品相关数据以json形式提交到服务端。在修改菜品信息时需要注意,除了要更新dish菜品表,还需要更新dish_flavor菜品口味表;在该方法中,我们既需要更新dish菜品基本信息表,还需要更新dish_flavor菜品口味表。而页面再操作时,关于菜品的口味,有修改,有新增,也有可能删除,我们应该如何更新菜品口味信息呢,其实,无论菜品口味信息如何变化,我们只需要保持一个原则: 先删除,后添加。

@Override @Transactional public void updateWithFlavor(DishDto dishDto) { //更新dish表基本信息 this.updateById(dishDto); //清理当前菜品对应口味数据---dish_flavor表的delete操作 LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper(); queryWrapper.eq(DishFlavor::getDishId,dishDto.getId()); dishFlavorService.remove(queryWrapper); //添加当前提交过来的口味数据---dish_flavor表的insert操作 List flavors = dishDto.getFlavors(); flavors = flavors.stream().map((item) -> { item.setDishId(dishDto.getId()); return item; }).collect(Collectors.toList()); dishFlavorService.saveBatch(flavors); }

1.6 项目后台功能实现 --> 套餐管理 1.6.1 --> 新增套餐
1.6.1.1 需求分析 后台系统中可以管理套餐信息,通过新增套餐功能来添加一个新的套餐,在添加套餐时需要选择当前套餐所属的套餐分类和包含的菜品,并且需要上传套餐对应的图片,在移动端会按照套餐分类来展示对应的套餐。
1.6.1.2 数据模型 新增套餐,其实就是将新增页面录入的套餐信息插入到setmeal表,还需要向setmeal_dish表插入套餐和菜品关联数据。所以在新增套餐时,涉及到两个表:
springboot|一个SpringBoot单体项目--》外卖平台项目之后台管理端基础功能开发
文章图片

1.6.1.3 前端页面与服务端的交互过程 1). 点击新建套餐按钮,访问页面(backend/page/combo/add.html),页面加载发送ajax请求,请求服务端获取套餐分类数据并展示到下拉框中
2). 访问页面(backend/page/combo/add.html),页面加载时发送ajax请求,请求服务端获取菜品分类数据并展示到添加菜品窗口中
3). 当点击添加菜品窗口左侧菜单的某一个分类, 页面发送ajax请求,请求服务端,根据菜品分类查询对应的菜品数据并展示到添加菜品窗口中
4). 页面发送请求进行图片上传,请求服务端将图片保存到服务器
5). 页面发送请求进行图片下载,将上传的图片进行回显
6). 点击保存按钮,发送ajax请求,将套餐相关数据以json形式提交到服务端
分析发送的请求有5个,分别是:
A. 根据传递的参数,查询套餐分类列表
B. 根据传递的参数,查询菜品分类列表
C. 图片上传
D. 图片下载展示
E. 根据菜品分类ID,查询菜品列表
F. 保存套餐信息
1.6.1.4 代码开发 根据分类查询菜品
/** * 根据条件查询对应的菜品数据 * @param dish * @return */ @GetMapping("/list") public R> list(Dish dish){ //构造查询条件 LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(dish.getCategoryId() != null ,Dish::getCategoryId,dish.getCategoryId()); //添加条件,查询状态为1(起售状态)的菜品 queryWrapper.eq(Dish::getStatus,1); //添加排序条件 queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime); List list = dishService.list(queryWrapper); return R.success(list); }

保存套餐 分析: 在进行套餐信息保存时,前端提交的数据,不仅包含套餐的基本信息,还包含套餐关联的菜品列表数据 setmealDishes。所以这个时候我们使用Setmeal就不能完成参数的封装了,我们需要在Setmeal的基本属性的基础上,再扩充一个属性 setmealDishes 来接收页面传递的套餐关联的菜品列表,而我们在准备工作中,导入进来的SetmealDto能够满足这个需求。
SetmealServiceImpl.saveWithDish():
具体逻辑:
A. 保存套餐基本信息
B. 获取套餐关联的菜品集合,并为集合中的每一个元素赋值套餐ID(setmealId)
C. 批量保存套餐关联的菜品集合
代码实现:
/** * 新增套餐,同时需要保存套餐和菜品的关联关系 * @param setmealDto */ @Transactional public void saveWithDish(SetmealDto setmealDto) { //保存套餐的基本信息,操作setmeal,执行insert操作 this.save(setmealDto); List setmealDishes = setmealDto.getSetmealDishes(); setmealDishes.stream().map((item) -> { item.setSetmealId(setmealDto.getId()); return item; }).collect(Collectors.toList()); //保存套餐和菜品的关联信息,操作setmeal_dish,执行insert操作 setmealDishService.saveBatch(setmealDishes); }

1.6.2 --> 套餐的分页查询
1.6.2.1 前端页面和服务端的交互过程 1). 访问页面(backend/page/combo/list.html),页面加载时,会自动发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端,获取分页数据
2). 在列表渲染展示时,页面发送请求,请求服务端进行图片下载,用于页面图片展示
1.6.2.2 代码开发 基本信息查询 基本逻辑:
1). 构建分页条件对象
2). 构建查询条件对象,如果传递了套餐名称,根据套餐名称模糊查询, 并对结果按修改时间降序排序
3). 执行分页查询
4). 由于查询套餐信息时, 只包含套餐的基本信息, 并不包含套餐的分类名称, 所以在这里查询到套餐的基本信息后, 还需要根据分类ID(categoryId), 查询套餐分类名称(categoryName),并最终将套餐的基本信息及分类名称信息封装到SetmealDto。组装数据并返回
代码实现:
/** * 套餐分页查询 * @param page * @param pageSize * @param name * @return */ @GetMapping("/page") public R page(int page,int pageSize,String name){ //分页构造器对象 Page pageInfo = new Page<>(page,pageSize); Page dtoPage = new Page<>(); LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); //添加查询条件,根据name进行like模糊查询 queryWrapper.like(name != null,Setmeal::getName,name); //添加排序条件,根据更新时间降序排列 queryWrapper.orderByDesc(Setmeal::getUpdateTime); setmealService.page(pageInfo,queryWrapper); //对象拷贝 BeanUtils.copyProperties(pageInfo,dtoPage,"records"); List records = pageInfo.getRecords(); List list = records.stream().map((item) -> { SetmealDto setmealDto = new SetmealDto(); //对象拷贝 BeanUtils.copyProperties(item,setmealDto); //分类id Long categoryId = item.getCategoryId(); //根据分类id查询分类对象 Category category = categoryService.getById(categoryId); if(category != null){ //分类名称 String categoryName = category.getName(); setmealDto.setCategoryName(categoryName); } return setmealDto; }).collect(Collectors.toList()); dtoPage.setRecords(list); return R.success(dtoPage); }

1.6.3 --> 删除套餐
1.6.3.1 --> 需求分析 在套餐管理列表页面,点击删除按钮,可以删除对应的套餐信息。也可以通过复选框选择多个套餐,点击批量删除按钮一次删除多个套餐。注意,对于状态为售卖中的套餐不能删除,需要先停售,然后才能删除
1.6.3.2 --> 交互过程 1). 点击删除, 删除单个套餐时,页面发送ajax请求,根据套餐id删除对应套餐
2). 删除多个套餐时,页面发送ajax请求,根据提交的多个套餐id删除对应套餐
分析:开发删除套餐功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可,一次请求为根据ID删除,一次请求为根据ID批量删除。观察删除单个套餐和批量删除套餐的请求信息可以发现,两种请求的地址和请求方式都是相同的,不同的则是传递的id个数,所以在服务端可以提供一个方法来统一处理。
1.6.3.3 --> 代码实现 【springboot|一个SpringBoot单体项目--》外卖平台项目之后台管理端基础功能开发】分析:删除套餐时, 我们不仅要删除套餐, 还要删除套餐与菜品的关联关系。
实现:SetmealServiceImpl.removeWithDish():
具体的逻辑:
A. 查询该批次套餐中是否存在售卖中的套餐, 如果存在, 不允许删除
B. 删除套餐数据
C. 删除套餐关联的菜品数据
/** * 删除套餐,同时需要删除套餐和菜品的关联数据 * @param ids */ @Transactional public void removeWithDish(List ids) { //select count(*) from setmeal where id in (1,2,3) and status = 1 //查询套餐状态,确定是否可用删除 LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper(); queryWrapper.in(Setmeal::getId,ids); queryWrapper.eq(Setmeal::getStatus,1); int count = this.count(queryWrapper); if(count > 0){ //如果不能删除,抛出一个业务异常 throw new CustomException("套餐正在售卖中,不能删除"); }//如果可以删除,先删除套餐表中的数据---setmeal this.removeByIds(ids); //delete from setmeal_dish where setmeal_id in (1,2,3) LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>(); lambdaQueryWrapper.in(SetmealDish::getSetmealId,ids); //删除关系表中的数据----setmeal_dish setmealDishService.remove(lambdaQueryWrapper); }

    推荐阅读