瑞吉外卖项目-包含扩展功能-5W字详细图文讲解-附源码(后台部分)

1,267

移动端部分的文章:juejin.cn/spost/72426…

项目搭建

学生作者:吃饱饱坏蜀黍

springboot版本:2.7.12

jdk版本:20

开发编辑器工具:IDEA 2023.1.2

数据库:Mysql(社区版)8.0.32

数据库管理工具:Navicat Premium 16

操作系统:window10

链接:pan.baidu.com/s/19YTUCBtg… 提取码:arqf

pom.xml

​ 创建springboot项目,构建maven项目,导入所需的jar包,所使用到的jar包下面都有注解。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.12</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.HuaiShuShu</groupId>
    <artifactId>RuiJiWaiMai</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <java.version>20</java.version>
    </properties>

    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--Mysql较新版本的驱动-->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!--lombok工具包-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!--mybatispuls的jar包-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.3.1</version>
        </dependency>

        <!--操作json的jar包-->
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>2.0.32</version>
        </dependency>

        <!--通用的语言包-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.12.0</version>
        </dependency>

        <!--druid数据源管理-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.18</version>
        </dependency>

        <!--热部署包-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>

        <!--mybatispuls的代码生成器-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.5.1</version>
        </dependency>
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>2.3.31</version>
        </dependency>


    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

sql文件

​ sql文件教长,我就不在这里放置了,需要的小伙伴可以通过黑马的小程序获取,也可以通过我的网盘地址获取。

yml文件

​ 在yml文件中配置对应的属性:

server:
  port: 80

spring:
  application:
    #定义应用名称(默认为工程的名称,可选)
    name: RuiJiWaiMai
  datasource:
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/ruijiwaimai?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
      username: root
      password: root
  devtools:
    restart:
      # 设置不参与热部署的文件或文件夹
      exclude: static/**,public/**,config/application.yml
      # 设置热部署的开启状态
      enabled: true

mybatis-plus:
  #配置类型别名所对应的包
  type-aliases-package: com.atguigu.pojo
  configuration:
    #在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      id-type: ASSIGN_ID

mybatis-plus代码生成器

​ 在test测试文件夹下创建一个mybatis-plus代码生成器的类运行以下代码,快速生成整个项目的结构。

package com.huaishushu;

import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.OutputFile;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;

import java.util.Collections;


/**
 * @Author ChiBaoBaoHuaiShuShu
 * @Date 2023/4/19 13:25
 * @PackageName:com.atguigu
 * @Description: 代码生成器
 * @Version 1.0
 */

public class FastAutoGeneratorTest {
    public static void main(String[] args) {
        FastAutoGenerator.create("jdbc:mysql://127.0.0.1:3306/ruijiwaimai? characterEncoding=utf-8&userSSL=false", "root", "root")
                .globalConfig(builder -> {
                    builder.author("ChiBaoBaoHuaiShuShu") // 设置作者
                             //.enableSwagger()// 开启 swagger 模式
                            .fileOverride() // 覆盖已生成文件
                            .outputDir("D://GongZuo//IDEA//XianMu//RuiJiWaiMai-springboot-MybatisPlus//RuiJiWaiMai//src//main/java"); // 指定输出目录
                })
                .packageConfig(builder -> {
                    builder.parent("com.huaishushu") // 设置父包名
                            //.moduleName("") // 设置父包模块名
                            .pathInfo(Collections.singletonMap(OutputFile.mapperXml, "D://GongZuo//IDEA//XianMu//RuiJiWaiMai-springboot-MybatisPlus//RuiJiWaiMai//src//main/resources//mapper"));// 设置mapperXml生成路径
                })
                .strategyConfig(builder -> {
                    builder.addInclude("address_book") // 设置需要生成的表名
                            .addInclude("category")
                            .addInclude("dish")
                            .addInclude("dish_flavor")
                            .addInclude("employee")
                            .addInclude("order_detail")
                            .addInclude("orders")
                            .addInclude("setmeal")
                            .addInclude("setmeal_dish")
                            .addInclude("shopping_cart")
                            .addInclude("user")
                            .addTablePrefix("t_", "c_"); // 设置过滤表前缀
                })
                .templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker 引擎模板,默认的是Velocity引擎模板
                .execute();
    }

}

项目结构

​ 最后则可以获得对应的项目整体结构如下图:

day1_项目结果

​ 其中的common文件和config文件时笔者第二天的时候才创建的,这个笔记也是第二天的时候才开始记录的所以就懒的去掉啦,而且我所用的jar包都是比较新版本的了和yml文件中的属性配置也有一些自己改的地方,大家可以和黑马的资料对比一下。

​ 还有就是我们用MP的代码生成器生成的项目结构中,我们需要把Controller层中的所有类中的@Controller注解替换成@RestController注解,这是因为我们和前端约定好了返回一个R类型的实体类。

R实体类

package com.huaishushu.common;

import lombok.Data;

import java.util.HashMap;
import java.util.Map;

/**
 * 通用返回结果,服务端响应的数据最终都会封装成此对象
 * @param <T>
 */
@Data
public class R<T> {

    private Integer code; //编码:1成功,0和其它数字为失败

    private String msg; //错误信息

    private T data; //数据

    private Map map = new HashMap(); //动态数据

    public static <T> R<T> success(T object) {
        R<T> r = new R<T>();
        r.data = object;
        r.code = 1;
        return r;
    }

    public static <T> R<T> error(String msg) {
        R r = new R();
        r.msg = msg;
        r.code = 0;
        return r;
    }

    public R<T> add(String key, Object value) {
        this.map.put(key, value);
        return this;
    }

}

​ 前端的代码文件下载大家可以去b站黑马瑞吉外卖的视频下获取,也可以通过我的网盘地址下载,我的就没黑马那里的那么全了,我只下载了资料,没下载视频,所以需要视频的小伙伴可以去黑马那边下载,下载好后的同学把前端的resources->static文件夹下即可。

前端代码文件的位置

温馨提示

​ 后面的过程中,因为mybatispuls太长了,后期我可能会用MP来代指它,所以小伙伴们不要误会了。

业务开发

个人看法

​ 在业务开发这块的时候,黑马的视频中老师很多都是直接在Controller层控制层中直接调用了MP提供的方法来完成的业务的开发,这是我认为不太合理的地方,因为我们在学校学习MVC结构的时候我们知道Controller层是指进行调用接口返回数据或参数和前端做交互,所以后面很多业务模块的开发中,我很多都是采用了MVC的结构来编写。

温馨提示

​ 我们使用黑马资料中的sql文件生成数据库和表后,在构建项目结构的时候,不管小伙伴使用的是MP的代码生成器还是黑马资料内的实体类,我们都应该去检查一下数据库中的字段和实体类中的属性是否对应,以及有的模块开发中会涉及到一些逻辑删除,这时候我们可以看一下数据库中是否有用来逻辑删除的字段,这个项目中使用的是is_delete来充当逻辑删除的字段。

​ 因此我们要注意数据库中是否有这个字段,我在开发过程中就发现有的表是缺少这个字段的,但是业务开发中又需要逻辑删除,而且实体类中也有is_delete这个属性,所以可以判断是黑马资料的sql文件有缺失漏洞,所以我们应该检查表字段和实体类属性,并及时补充上去,并且因为需要使用到逻辑删除,所以应该在实体类的isDelete属性上添加@TableLogic注解。

​ 如果是数据库缺少了is_delete字段,则我们为其缺少的表添加上该字段即可。

逻辑删除的字段

员工模块

员工登录

代码开发

4)在Controller中创建登录方法 处理逻辑如下: 1、将页面提交的密码password进行md5加密处理2、根据页面提交的用户名username查询数据库 3、如果没有查询到则返回登录失败结果 4、密码比对,如果不一致则返回登录失败结果 5、查看员工状态,如果为已禁用状态,则返回员工已禁用结果6、登录成功,将员工id存入Session并返回登录成功结果

后台登录功能开发

​ 因此我们需要先在EmployeeController类中定义对应的登录方法login,并根据对应的前台传递回来的employee的json数据进行封装赋值给login方法的参数,并且在参数中定义HttpServletRequest对象来获取Sesion对象并将用户信息共享到会话层中。

Controller层

/**
 * <p>
 * 员工信息 前端控制器
 * </p>
 *
 * @author ChiBaoBaoHuaiShuShu
 * @since 2023-05-28
 */
@Slf4j
@RestController
@RequestMapping("/employee")
public class EmployeeController {

    @Autowired
    IEmployeeService employeeService;

    /**
     * 员工登录
     * @param request
     * @param employee
     * @return
     */
    @PostMapping("/login")
    public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee) {

        String password = employee.getPassword();
        //使用DigestUtils工具类进行md5加密处理
        password = DigestUtils.md5DigestAsHex(password.getBytes());
        //根据用户名进行查询
        Employee emp = employeeService.login(employee.getUsername());

        if (emp == null) {
            return R.error("登录失败,账号或密码错误!!!");
        }
        if (!emp.getPassword().equals(password)) {
            return R.error("登录失败,账号或密码错误!!!");
        }
        //查询员工状态,如果为已禁用状态,则放回员工已禁用结果
        if (emp.getStatus() == 0) {
            return R.error("账号已禁用");
        }

        request.getSession().setAttribute( "employee", emp.getId());
        return R.success(emp);
    }
}

​ 在通过IEmployeeService对象调用的方法来获取Employee对象时,我的获取方式有些不同,因为我认为Controller层不因和数据库直接进行交互和业务操作,因此此处我在对应的service层中定义了对应的login方法,其中传递的参数为String类型的username,返回类型是Employee类型的对象。

service接口

/**
 * <p>
 * 员工信息 服务类
 * </p>
 *
 * @author ChiBaoBaoHuaiShuShu
 * @since 2023-05-28
 */
public interface IEmployeeService extends IService<Employee> {

    /**
     * 根据用户的username查询是否存在
     * @param username
     * @return
     */
    Employee login(String username);

}

service实现类

/**
 * <p>
 * 员工信息 服务实现类
 * </p>
 *
 * @author ChiBaoBaoHuaiShuShu
 * @since 2023-05-28
 */
@Service
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements IEmployeeService {

    @Autowired
    EmployeeMapper employeeMapper;

    /**
     * 根据用户的username查询是否存在
     * @param username
     * @return
     */
    @Override
    public Employee login(String username) {
        LambdaQueryWrapper<Employee> lqw = new LambdaQueryWrapper<>();
        lqw.eq(Employee::getUsername, username);
        Employee employee = employeeMapper.selectOne(lqw);
        return employee;
    }
    
}

​ 这样Controller层调用方法则可获取到Employee或者null,之后则按照前面的逻辑进行判断并返回对应的信息即可。

员工退出

​ 员工退出则相对简单很多,删除保存在Session对象中的Employee就可以了。

/**
     * 员工退出
     * @param request
     * @return
     */
    @PostMapping("/logout")
    public R<String> logout(HttpServletRequest request) {
        //清理Session中保存的当前登录员工的id
        request.getSession().removeAttribute("employee");
        return R.success("退出成功");
    }

完善登录功能

​ 前面我们已经完成了后台系统的员工登录功能开发,但是还存在一个问题:用户如果不登录,直接访问系统首页面,照样可以正常访问。 ​ 这种设计并不合理,我们希望看到的效果应该是,只有登录成功后才可以访问系统中的页面,如果没有登录则跳转到登录页面 ​ 那么,具体应该怎么实现呢? 答案就是使用过滤器或者拦截器,在过滤器或者拦截器中判断用户是否已经完成登录,如果没有登录则跳转到登录页面

代码实现

实现步骤 1、创建自定义过滤器LoginCheckFilter 2、在启动类上加入注解@ServletComponentScan 3、完善过滤器的处理逻辑

完善登录功能的逻辑实现

​ 这块和视频中的一样,选择的是过滤器来实现的登录功能,因为登录和退出的请求以及访问静态资源的请求是不应该拦截的,因此需要在配置不拦截的请求路径,并且定义一个check方法来把请求逐一对比,并且定义了AntPathMatcher对象来辅助路径的比对,具体功能如下面所示:

/**
 * @Author HuaiShuShu
 * @Date 2023/5/28 21:56
 * @PackageName:com.huaishushu.filter
 * @Description: TODO
 * @Version 1.0
 */
@Slf4j
@WebFilter(filterName = "LoginCheckFilter",urlPatterns = "/*")
public class LoginCheckFilter implements Filter {
    //路径匹配器,支持通配符
    public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        //1、获取本次请求的URI
        String requestURI = request.getRequestURI();
        //定义不需要处理的请求路径
        String[] urls = new String[]{
                "/employee/login",
                "/employee/logout",
                "/backend/**",
                "/front/**"
        };
        //2、判断本次请求是否需要处理
        boolean check = check(urls, requestURI);
        // 3、如果不需要处理,则直接放行
        if (check) {
            filterChain.doFilter(request,response);
            return;
        }
        // 4、判断登录状态,如果已登录,则直接放行
        if (request.getSession().getAttribute("employee") != null) {
            filterChain.doFilter(request,response);
            return;
        }
        // 5、如果未登录则返回未登录结果,通过输出流方式想客户端页面响应数据
        response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
        return;
    }

    /**
     * 路径匹配,检查本次请求是否需要放行
     * @param urls
     * @param requestURI
     * @return
     */
    public boolean check(String[] urls,String requestURI) {
        for (String url : urls) {
            //逐一进行匹配,如果匹配上了则直接返回true
            boolean match = PATH_MATCHER.match(url, requestURI);
            if (match) {
                return true;
            }
        }
        return false;
    }

}

新增员工

新增员工按钮

​ 在数据库中的employee表中我们对username做了唯一约束,这是为了保证账号的唯一性,而咱们的新增员工就是向username中添加信息。

代码开发

在开发代码之前,需要梳理一下整个程序的执行过程: 1、页面发送ajax请求,将新增员工页面中输入的数据以json的形式提交到服务端

2、服务端Controller接收页面提交的数据并调用Service将数据进行保存

3、Service调用Mapper操作数据库,保存数据

新增员工_页面的请求载体

​ 业务以及逻辑分析可知,当前台在添加员工的页面中输入并点保存后,后发送对应的请求给服务器传达给后端,因此我们需要接收请求中的数据,并将其存入数据库中。

​ 因此需要定义在Controller层中定义添加员工的方法save,代码如下所示:

Controller层

/**
     * 员工信息分页查询
     * @param page
     * @param pageSize
     * @param name
     * @return
     */
    @GetMapping("/page")
    public R<Page> page(Integer page, Integer pageSize, String name) {
        Page<Employee> pageInfo = employeeService.page(page, pageSize, name);
        return R.success(pageInfo);
    }

service接口

/**
     * 新增员工信息
     * @param employee
     * @return
     */
    boolean save(HttpServletRequest request, Employee employee);

service实现类

/**
     * 新增员工信息
     * @param employee
     * @return
     */
    @Override
    public boolean save(HttpServletRequest request, Employee employee) {
        //设置初始密码并进行md5加密处理
        employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));
        employee.setCreateTime(LocalDateTime.now());
        employee.setUpdateTime(LocalDateTime.now());
        //获取当前登录用户的id
        Long empId = (Long) request.getSession().getAttribute("employee");
        employee.setCreateUser(empId);
        employee.setUpdateUser(empId);
        if (employeeMapper.insert(employee) > 0) {
            return true;
        }
        return false;
    }

​ 其中的employeeService.save()的方法是mybatisplus中封装好的保存方法,我们直接待用即可,这样我们在添加员工页面输入好数据后点击保存即可添加数据到数据库了。

​ 出现的问题,因为数据库中employee表中的username添加了唯一约束,因此当我们在重复添加相同的username的员工信息时,则会在IDEA编译器的控制台中出现报错。

​ 因此我们需要配置全局异常来捕获异常,并返回给前端。

​ 我们在common文件夹下新建一个GlobalExceptionHandler类来全局异常处理,其中代码如下:

/**
 * @Author HuaiShuShu
 * @Date 2023/5/29 0:06
 * @PackageName:com.huaishushu.common
 * @Description: 全局异常处理
 * @Version 1.0
 */
//开启异常处理,annotations属性可以配置需要拦截的位置。
@ControllerAdvice(annotations = {RestController.class, Controller.class})
//添加@ResponseBody,因为之后捕获异常以后需要返回json形式的数据
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 异常处理方法
     *
     * @return
     */
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public R<String> 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("未知错误!!!");
    }

}

​ 其中的SQLIntegrityConstraintViolationException.class为数据库异常类,将其配置给@ExceptionHandler注解可以只捕获这个类型的异常信息并返回,之后的这处String[] split = ex.getMessage().split(" ");String msg = split[2] + "已存在....";是因为报错的提示信息为

动态截取异常信息

​ 使用split方法后,其处于索引2的位置,因此才使用这个的。 ​

员工信息分页查询

代码开发

​ 在开发代码之前,需要梳理一下整个程序的执行过程:页面发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端12、服务端Controller接收页面提交的数据并调用Service查询数据。

  1. Service调用Mapper操作数据库,查询分页数据
  2. Controller将查询到的分页数据响应给页面
  3. 页面接收到分页数据并通过ElementUl的Table组件展示到页面上

分页查询的请求

​ 通过这边的代码开发分析,我们知道需要在Controller层定义一个对应的方法来接收浏览器发送url请求并接收里面的这三个参数page、pageSize、name,那么我们可以在EmployeeController类中定义一个分页方法public R page(Integer page, Integer pageSize, String name)来完成这项功能。

​ 同时我们还需要在Config中定义MybatisPlus的配置类,来加载咱们的分页组件,这个是MybatisPlus内部就已经封装好的分页查询,具体的实现源码感兴趣的小伙伴可以去MybatisPlus的官网了解。

​ 因此,我们在config文件夹下创建一个叫MybatisPlusConfig的类来加载MybatisPlus的一些插件。

MybatisPlusConfig

package com.huaishushu.config;

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;

/**
 * @Author HuaiShuShu
 * @Date 2023/5/29 19:18
 * @PackageName:com.huaishushu.config
 * @Description: 配置MP的分页插件
 * @Version 1.0
 */
@Configuration
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        //分页插件
        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return mybatisPlusInterceptor;
    }

}

Controller层

/**
     * 员工信息分页查询
     * @param page
     * @param pageSize
     * @param name
     * @return
     */
    @GetMapping("/page")
    public R<Page> page(Integer page, Integer pageSize, String name) {
        Page<Employee> pageInfo = employeeService.page(page, pageSize, name);
        return R.success(pageInfo);
    }

service接口

/**
     * 员工信息分页查询
     * @param employee
     * @return
     */
    @PutMapping
    public R<String> update(@RequestBody Employee employee) {
        return null;
    }

service实现类

/**
     * 员工信息分页查询
     * @param page
     * @param pageSize
     * @param name
     * @return
     */
    @Override
    public Page<Employee> page(Integer page, Integer pageSize, String name) {
        //构建分页构造器
        Page pageInfo = new Page(page, pageSize);
        LambdaQueryWrapper<Employee> lqw = new LambdaQueryWrapper<>();
        lqw.like(StringUtils.isNoneEmpty(name), Employee::getName, name);
        lqw.orderByDesc(Employee::getUpdateTime);
        //此处不返回是因为查询好后的值是直接赋值给了上面的分页构造器了pageInfo
        employeeMapper.selectPage(pageInfo, lqw);
        return pageInfo;
    }

​ 查询出来的信息,咱们按时间排序了一下。

问题:这处为什么不使用@RequestParam或@PathVariable来进行参数绑定呢,这样到时不是更好看到是什么和什么参数绑定了吗?

​ 答:方法中的page、pageSize、name是通过url中参数的形式传递回来的,并且参数中不一定会有name,所以方法中参数绑定接收这块不需要使用@RequestParam注解,不然添加上该注解后因为该注解属性中required的值默认是true,导致必须传递回来对应的参数,而@PathVariable注解绑定参数的形式是通过url请求中占位符的方式来绑定参数的,所以二者都不可行!!!

(笔者在写这块功能的时候就在想为什么不用@RequestParam或@PathVariable来绑定参数,然后测试之后浏览器一直报404而疑惑,后来查询资料后才解惑,还是基础不够扎实)

启用/禁用员工账号

需求分析

​ 在员工管理列表页面,可以对某个员工账号进行启用或禁用操作。账号禁用的员工不能登录系统,启用后的员工可以正常登录。

​ 需要注意,只有管理员(admin用户)可以对其他普通用户进行启用、禁用操作,所以普通用户登录系统后启用、禁用按钮不显示。

​ 在管理员权限的用户中,我们可以看到员工信息编辑栏中又禁用以及编辑按钮,而普通用户没有,这是因为前端static/backend/page/member/list.html从浏览器中获取了我们之前员工登录时保存在浏览器中的userInfo的信息,并做判断。

前端list获取浏览器userna

​ userInfo在浏览器中的位置。

userInfo在浏览器中的位置png

​ 做判断是否显示启用或禁用按钮,然后是admin用户则显示启用/禁用按钮。

代码开发

在开发代码之前,需要梳理一下整个程序的执行过程

1、页面发送ajax请求,将参数(id、status)提交到服务端

2、服务端Controller接收页面提交的数据并调用Service更新数据

3、Service调用Mapper操作数据库

启用禁用url请求

​ 更改启用/禁用本质就是一个更新修改操作,所以只需要对数据库中employee表的响应用户的status进行更新修改罢了,那么是修改操作那么请求方式一般都是PUT,而我们这个项目中的前端也确实发送的请求是PUT的请求,所以咱们Controller层中的方法应该使用@PutMapping注解了,而且其传递的参数一个是id一个是status参数则我们可以使用Employee对象来接收参数。

Controller层

/**
     * 启用/禁用员工账号
     * @param employee
     * @return
     */
    @PutMapping
    public R<String> update(@RequestBody Employee employee) {
        if (employeeService.update(employee)) {
            if (employee.getStatus() == 1) {
                return R.success("启用成功");
            } else {
                return R.success("禁用成功");
            }
        }
        return R.error("操作失败");
    }

service接口

/**
     * 启用/禁用员工账号
     * @param employee
     * @return
     */
    boolean update(Employee employee);

service实现类

 /**
     * 启用/禁用员工账号
     *
     * @param employee
     * @return
     */
    @Override
    public boolean update(HttpServletRequest resource, Employee employee) {
        //通过session获取当前登录账号的id
        Long empId = (Long) resource.getSession().getAttribute("employee");
        employee.setUpdateTime(LocalDateTime.now());
        employee.setUpdateUser(empId);

        //根据id修改即可
        if (employeeMapper.updateById(employee) > 0) {
            return true;
        }
        return false;
    }

​ service实现类这边的update方法这块调用的是mP的updateById方法根据id来修改就可以了。

出现问题

​ 当我们认为写好的时候运行操作后,会发现IDEA控制台显示一下sql语句,其结果显示0,说明我们没有成功对数据库进行更新修改。

启用禁用出错

​ 出现这种的原因是因为js保存long类型的数据出现了精度保存的误差问题,表中数据和浏览器传递回来的id数据明显不同。

js的id输出与保存不一致

解决方法

​ 将后端传递给前端的数据转换成string字符串类型的数据,这样传递的后js就能够正常保存。

具体实现步骤:

  1. 提供对象转换器Jackson0bjectMapper,基于]ackson进行Java对象到json数据的转换 (资料中已经提供,直接复制到项目中使用)
  2. 在WebMvcConfig配置类中扩展Spring mvc的消息转换器,在此消息转换器中使用提供的对象转换器进行lava对象到ison数据的转换/放水
package com.huaishushu.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)
                .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);
    }
}

​ 以上类的对象转换器,我们将其复制粘贴到common文件夹下即可,

编辑员工信息

代码开发

在开发代码之前需要梳理一下操作过程和对应的程序的执行流程:

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

注意:

add.html页面为公共页面,新增员工和编辑员工都是在此页面操作

​ 我们在页面中点击编辑按钮后会跳转到add这个页面,并且所发送的url请求中是get类型还携带了id。

add页面携带id

​ 然后在页面也发送了下面的这个get请求,来获取员工的信息

点击编辑后显示员工信息

​ 那么我们只需要在Controller层添加一个getByid的方法即可来接收id来查询员工信息。

/**
     * 根据id查询员工信息
     * 其时搭配编辑员工信息使用的
     * @param id
     * @return
     */
    @GetMapping("{id}")
    public R<Employee> getById(@PathVariable("id") Long id) {
        Employee employee = employeeService.getById(id);
        log.info("根据id查询到的员工信息:"+employee);
        if (employee != null) {
            return R.success(employee);
        }
        //这种情况只有在页面刷新过慢时会出现,或出现线程安全时出现
        return R.error(null);
    }

编辑显示了员工信息

公共字段自动填充(元数据处理器)

代码实现

Mvbatis Plus公共字段自动填充,也就是在插入或者更新的时候为指定字段赋予指定的值,使用它的好处就是可以统一对这些字段进行处理,避免了重复代码。

实现步骤:

  1. 在实体类的属性上加入@TableField注解,指定自动填充的策略
  2. 按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现MetaObjectHandler接口

​ 在Employee这个实体类上对应的属性上添加@TableField注解,而该注解时MP为我们提供的一个公共字段填充的字段,使用它需要编写元数据对象处理器并实现MetaObjectHandler接口。

@Data
public class Employee implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 主键
     */
    private Long id;

    /**
     * 姓名
     */
    private String name;

    /**
     * 用户名
     */
    private String username;

    /**
     * 密码
     */
    private String password;

    /**
     * 手机号
     */
    private String phone;

    /**
     * 性别
     */
    private String sex;

    /**
     * 身份证号
     */
    private String idNumber;

    /**
     * 状态 0:禁用,1:正常
     */
    private Integer status;

    /**
     * 创建时间
     */
    @TableField(fill = FieldFill.INSERT) //插入时填充字段
    private LocalDateTime createTime;

    /**
     * 更新时间
     */
    @TableField(fill = FieldFill.INSERT_UPDATE) //插入和更新时填充字段
    private LocalDateTime updateTime;

    /**
     * 创建人
     */
    @TableField(fill = FieldFill.INSERT) //插入时填充字段
    private Long createUser;

    /**
     * 修改人
     */
    @TableField(fill = FieldFill.INSERT_UPDATE) //插入和更新时填充字段
    private Long updateUser;
    
}

FieldFill枚举中有的属性

public enum FieldFill {
    /**
     * 默认不处理
     */
    DEFAULT,
    /**
     * 插入时填充字段
     */
    INSERT,
    /**
     * 更新时填充字段
     */
    UPDATE,
    /**
     * 插入和更新时填充字段
     */
    INSERT_UPDATE
}

​ 元数据对象处理器MyMetaObjectHandler类创建在common文件夹下,里面关于id的获取先写死,因为我们还没有办法直接获取session。

/**
 * @Author HuaiShuShu
 * @Date 2023/5/30 23:06
 * @PackageName:com.huaishushu.common
 * @Description: 自定义元数据对象处理器
 * @Version 1.0
 */
@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {

    /**
     * 插入操作自动填充
     * @param metaObject 元对象
     */
    @Override
    public void insertFill(MetaObject metaObject) {
        metaObject.setValue("createTime", LocalDateTime.now());
        metaObject.setValue("updateTime", LocalDateTime.now());
        metaObject.setValue("createUser", new Long(1));
        metaObject.setValue("updateUser", new Long(1));
    }

    /**
     * 更新时自动填充
     * @param metaObject 元对象
     */
    @Override
    public void updateFill(MetaObject metaObject) {
        Long id = Thread.currentThread().threadId();
        log.info("线程:{}",id);
        metaObject.setValue("updateTime", LocalDateTime.now());
        metaObject.setValue("updateUser", new Long(1));
    }
}

功能完善

​ 前面我们已经完成了公共字段自动填充功能的代码开发,但是还有一个问题没有解决,就是我们在自动填充createuser和updateUser时设置的用户id是固定值,现在我们需要改造成动态获取当前登录用户的id。

​ 有的同学可能想到,用户登录成功后我们将用户id存入了Httpession中,现在我从Httpession中获取不就行了?注意,我们在MvMetaObiectHandler类中是不能获得HtoSession对象的,所以我们需要通过其他方式来获取登录用户id。

ThreadLocal

​ 可以使用ThreadLocal来解决此问题,它是JDK中提供的一个类

​ 在学习ThreadLocal之前,我们需要先确认一个事情,就是客户端发送的每次http请求,对应的在服务端都会分配一个新的线程来处理,在处理过程中涉及到下面类中的方法都属于相同的一个线程:

  1. LoginCheckFilter的doFilter方法
  2. EmployeeController的update方法
  3. MyMetaobjectHandler的updateFill方法

线程一致

线程一致1

​ 每个人的线程可能不一致,并且大家要注意看喔,因为我们没刷新一次就要发送一次http请求,这里面的线程就有可能会改变,所以大家要注意看线程的id。

什么是ThreadLocal?

​ ThreadLocal并不是一个Thread,而是Thread的局部变量。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。

ThreadLocal常用方法:

  • ​ public void set(T value) 设置当前线程的线程局部变量的值
  • ​ public T get() 返回当前线程所对应的线程局部变量的值

​ 我们可以在LoginCheckFilter的doFilter方法中获取当前登录用户id,并调用ThreadLocal的set方法来设置当前线程的线程局部变量的值 (用户id),然后在MyMetabiectHandler的updateFil方法中调用ThreadLocal的get方法来获得当前 线程所对应的线程局部变量的值 (用户id)。

实现步骤:

  1. 编写BaseContext工具类,基于ThreadLocal封装的工具类
  2. 在LoginCheckFilter的doFilter方法中调用BaseContext来设置当前登录用户的id
  3. 在MyMetaobjectHandler的方法中调用BaseContext获取登录用户的id

​ 在common文件夹下创建BaseContext工具类

public class BaseContext {

    private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    /**
     * 设置值
     * @param id
     */
    public static void setCurrentId(Long id) {
        threadLocal.set(id);
    }

    /**
     * 获取值
     * @return
     */
    public static Long getCurrentId() {
        return threadLocal.get();
    }


}

LoginCheckFilter过滤器类中的doFilter方法

public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        //1、获取本次请求的URI
        String requestURI = request.getRequestURI();
        log.info("拦截到请求:{}",requestURI);

        //定义不需要处理的请求路径
        String[] urls = new String[]{
                "/employee/login",
                "/employee/logout",
                "/backend/**",
                "/front/**"
        };
        //2、判断本次请求是否需要处理
        boolean check = check(urls, requestURI);
        // 3、如果不需要处理,则直接放行
        if (check) {
            filterChain.doFilter(request,response);
            return;
        }
        // 4、判断登录状态,如果已登录,则直接放行
        if (request.getSession().getAttribute("employee") != null) {
            log.info("用户已登录,登录id:" + request.getSession().getAttribute("employee"));
			
            //设置线程局部变量,供公共字段自动填充用
            Long empId = (Long) request.getSession().getAttribute("employee");
            BaseContext.setCurrentId(empId);

            filterChain.doFilter(request, response);
            return;
        }
        // 5、如果未登录则返回未登录结果,通过输出流方式想客户端页面响应数据
        response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
        return;
    }

​ 元数据对象处理器MyMetaObjectHandler中的id获取可以直接通过线程局部变量获取了。

/**
 * @Author HuaiShuShu
 * @Date 2023/5/30 23:06
 * @PackageName:com.huaishushu.common
 * @Description: 自定义元数据对象处理器
 * @Version 1.0
 */
@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {

    /**
     * 插入操作自动填充
     * @param metaObject 元对象
     */
    @Override
    public void insertFill(MetaObject metaObject) {
        metaObject.setValue("createTime", LocalDateTime.now());
        metaObject.setValue("updateTime", LocalDateTime.now());
        metaObject.setValue("createUser", BaseContext.getCurrentId());
        metaObject.setValue("updateUser", BaseContext.getCurrentId());
    }

    /**
     * 更新时自动填充
     * @param metaObject 元对象
     */
    @Override
    public void updateFill(MetaObject metaObject) {
        Long id = Thread.currentThread().threadId();
        log.info("线程:{}",id);
        metaObject.setValue("updateTime", LocalDateTime.now());
        metaObject.setValue("updateUser", BaseContext.getCurrentId());
    }
}

温馨提示

​ 其实在MyMetaObjectHandler类中直接自动装配一个session对象也是可以获取到id的,但是这样会有线程危险,不利于安全,在多用户环境下线程安全尤为重要。各位同僚也可以试一下其他的方法,但是也要一定要考虑线程安全的问题。

分类信息模块

温馨提示

​ 如果有的小伙伴在做这个模块的业务开发的时候遇到了数据库表里的数据无法给实体类赋值的话,注意是看实体类中是不是多了一个isDeleted属性,这个属性是用来做逻辑删除的,但是黑马提供的sql文件中并没有在表中生成这个字段,所以才会无法正常赋值,我们只需要在数据库中添加上is_deleted即可,并且要注意其默认赋值为0。

逻辑删除的字段转存失败,建议直接上传图片文件

​ 笔者因为在做的时候应该没遇到问题,所以当时没注意,后面写菜品信息删除的时候才注意到该问题,所以后续都有补充。

新增分类

​ 新增分类里的数据是存储在数据库中category表中,表的结构如下。

category表

​ 分类管理页面目前是没有数据的,因为我们还没实现对应的控制层的方法。

分类管理页面(无数据)

​ 有的小伙伴可能和博主是一样的,都是通过MP的代码生成器生成的项目结构,这时我们就需要在实体类上的公共字段补充上@TableField。

package com.huaishushu.entity;

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableLogic;
import lombok.Data;

import java.io.Serializable;
import java.time.LocalDateTime;

/**
 * <p>
 * 菜品及套餐分类
 * </p>
 *
 * @author ChiBaoBaoHuaiShuShu
 * @since 2023-05-28
 */
@Data
public class Category implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 主键
     */
    private Long id;

    /**
     * 类型   1 菜品分类 2 套餐分类
     */
    private Integer type;

    /**
     * 分类名称
     */
    private String name;

    /**
     * 顺序
     */
    private Integer sort;

    /**
     * 创建时间
     */
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    /**
     * 更新时间
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;

    /**
     * 创建人
     */
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;

    /**
     * 修改人
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;
}

代码开发

在开发代码之前,需要梳理一下整个程序的执行过程:

  1. 页面(backend/page/category/list.htm)发送ajax请求,将新增分类窗口输入的数据以ion形式提交到服务端
  2. 服务端Controller接收页面提交的数据并调用Service将数据进行保存
  3. Service调用Mapper操作数据库,保存数据

​ 可以看到新增菜品分类和新增套餐分类请求的服务端地址和提交的ison数据结构相同,只是type字段的信息不同而已,这个我们数据库中就有解释,“1”为分类,“2为套餐”,所以服务端只需要提供一个方法统一处理,并且它的url请求方式是post的,因此我们在Controller定义一个post的方法来接收即可。

分类添加的请求url

​ 我们在分类的前端代码中可以看到,其只用到了一个code,不需要发送实体类之类的,那么我们就只需要定义一个返回值是R类型的方法就可以了。

分类的用到的东西

Controller层

package com.huaishushu.controller;


import com.huaishushu.common.R;
import com.huaishushu.entity.Category;
import com.huaishushu.service.ICategoryService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;


/**
 * <p>
 * 菜品及套餐分类 前端控制器
 * </p>
 *
 * @author ChiBaoBaoHuaiShuShu
 * @since 2023-05-28
 */
@RestController
@RequestMapping("/category")
@Slf4j
public class CategoryController {

    @Autowired
    private ICategoryService categoryService;

    /**
     * 新增分类和套餐
     * @param category
     * @return
     */
    @PostMapping
    public R<String> save(@RequestBody Category category) {
        boolean flag = categoryService.save(category);
        log.info("新增是菜品还是套餐:{},是否成功:{}",category.getType(),flag);
        if (flag) {
            return R.success("新增分类成功");
        }
        return R.error("新增分类失败");
    }

}

Service接口

package com.huaishushu.service;

import com.huaishushu.entity.Category;
import com.baomidou.mybatisplus.extension.service.IService;

/**
 * <p>
 * 菜品及套餐分类 服务类
 * </p>
 *
 * @author ChiBaoBaoHuaiShuShu
 * @since 2023-05-28
 */
public interface ICategoryService extends IService<Category> {

    boolean save(Category category);

}

Service实现类

package com.huaishushu.service.impl;

import com.huaishushu.entity.Category;
import com.huaishushu.mapper.CategoryMapper;
import com.huaishushu.service.ICategoryService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * <p>
 * 菜品及套餐分类 服务实现类
 * </p>
 *
 * @author ChiBaoBaoHuaiShuShu
 * @since 2023-05-28
 */
@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements ICategoryService {

    @Autowired
    private CategoryMapper categoryMapper;

    @Override
    public boolean save(Category category) {
        return categoryMapper.insert(category) > 0;
    }
}

​ 之后咱们测试添加客家菜和老年套餐,可以看到显示分类添加成功,之后我们可以到数据库中查看一下,是否添加成功,从下图可以看到我们是添加成功了,并且type类型也是添加正确的,那么404报错是我们还没有实现分类的分页查询,所以才会显示404报错。

分类添加后的提示

分类添加后,数据库显示

温馨提示

分类信息分页查询

代码开发

在开发代码之前,需要梳理一下整个程序的执行过程

  1. 页面发送ajax请求,将分页查询参数(page、pageSize)提交到服务端
  2. 服务端Controller接收页面提交的数据并调用Service查询数据
  3. Service调用Mapper操作数据库,查询分页数据
  4. Controller将查询到的分页数据响应给页面
  5. 页面接收到分页数据并通过ElementUl的Table组件展示到页面上

分类信息分页url

​ 因此我们在Controller层页面中定义一个page方法并使用@GetMapping来接收前端的url请求。

Controller层

/**
     * 分类信息的分页查询
     * @param page
     * @param pageSize
     * @return
     */
    @GetMapping("/page")
    public R<Page> page(Integer page, Integer pageSize) {
        Page<Category> pageInfo = categoryService.page(page, pageSize);
        log.info("分类信息分页查询");
        return R.success(pageInfo);
    }

Service接口

/**
     * 分类信息的分页查询
     * @param page
     * @param pageSize
     * @return
     */
    Page<Category> page(Integer page, Integer pageSize);

Service实现类

/**
     * 分类信息的分页查询
     * @param page
     * @param pageSize
     * @return
     */
    @Override
    public Page<Category> page(Integer page, Integer pageSize) {
        //构建分页构造器
        Page pageInfo = new Page(page, pageSize);
        LambdaQueryWrapper<Category> lqw = new LambdaQueryWrapper<>();
        lqw.orderByDesc(Category::getUpdateTime);
        //此处不返回是因为查询好后的值是直接赋值给了上面的分页构造器了pageInfo
        categoryMapper.selectPage(pageInfo, lqw);
        return pageInfo;
    }

查询出来的信息,咱们按时间排序了一下。

删除分类

需求分析

​ 在分类管理列表页面,可以对某个分类进行删除操作。需要注意的是当分类关联了菜品或者套餐时,此分类不允许删除。

分类删除按钮

代码开发

在开发代码之前,需要梳理一下整个程序的执行过程:

  1. 页面发送ajax请求,将参数(id)提交到服务端
  2. 服务端Controller接收页面提交的数据并调用Service删除数据
  3. Service调用Mapper操作数据库

删除分类url

​ 那么我们在Controller层创建对应的方法来接收删除请求即可,这里我们先直接删除信息,下一个小模块我们的在完善这个删除功能,使其判断该分类是否有菜品绑定了这个分类。

Controller层

/**
     * 根据id删除分类信息
     * @param id
     * @return
     */
    @DeleteMapping
    public R<String> delete(Long id) {
        boolean flag = categoryService.delete(id);
        log.info("删除分类信息的id:{},是否删除成功:{}",id,flag);
        if (flag) {
            return R.success("删除分类信息成功");
        }
        return R.error("删除分类信息失败!!!");
    }

Service接口

/**
     * 根据id来删除分类信息
     * @param id
     * @return
     */
    boolean delete(Long id);

Service实现类

/**
     * 根据id来删除分类信息
     * @param id
     * @return
     */
    @Override
    public boolean delete(Long id) {
        return categoryMapper.deleteById(id) > 0;
    }

​ 这里有的小伙伴可能会遇到一些问题,我们可以从图中看到delete删除的url请求中传递id的参数名称是ids,但是老师视频中的url请求时id,所以这块我们可以去到static/backend/api/category.js中修改。

delete中url的id

删除功能完善

​ 前面我们已经实现了根据id删除分类的功能,但是并没有检查删除的分类是否关联了菜品或者套餐,所以我们需要进行功能完善。

要完善分类删除功能,需要先准备基础的类和接口:

  1. 实体类Dish和Setmeal (从课程资料中复制即可)
  2. Mapper接口DishMapper和SetmealMapper
  3. Service接口DishService和SetmealService
  4. Service实现类DishServicelmpl和SetmealServicelmpl

如果是MP代码生成器生成的项目结果,此处也要记得在实体类Dish和Setmeal类中为公共字段添加@TableField,如果是复制课程资料的则直接复制就行了

Dish实体类
package com.huaishushu.entity;

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;

import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
 * <p>
 * 菜品管理
 * </p>
 *
 * @author ChiBaoBaoHuaiShuShu
 * @since 2023-05-28
 */
@Data
public class Dish implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 主键
     */
    private Long id;

    /**
     * 菜品名称
     */
    private String name;

    /**
     * 菜品分类id
     */
    private Long categoryId;

    /**
     * 菜品价格
     */
    private BigDecimal price;

    /**
     * 商品码
     */
    private String code;

    /**
     * 图片
     */
    private String image;

    /**
     * 描述信息
     */
    private String description;

    /**
     * 0 停售 1 起售
     */
    private Integer status;

    /**
     * 顺序
     */
    private Integer sort;

    /**
     * 创建时间
     */
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    /**
     * 更新时间
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;

    /**
     * 创建人
     */
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;

    /**
     * 修改人
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;

    /**
     * 是否删除
     */
    private Integer isDeleted;


}

Setmeal实体类
package com.huaishushu.entity;

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;

import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
 * <p>
 * 套餐
 * </p>
 *
 * @author ChiBaoBaoHuaiShuShu
 * @since 2023-05-28
 */
@Data
public class Setmeal implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 主键
     */
    private Long id;

    /**
     * 菜品分类id
     */
    private Long categoryId;

    /**
     * 套餐名称
     */
    private String name;

    /**
     * 套餐价格
     */
    private BigDecimal price;

    /**
     * 状态 0:停用 1:启用
     */
    private Integer status;

    /**
     * 编码
     */
    private String code;

    /**
     * 描述信息
     */
    private String description;

    /**
     * 图片
     */
    private String image;

    /**
     * 创建时间
     */
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    /**
     * 更新时间
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;

    /**
     * 创建人
     */
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;

    /**
     * 修改人
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;

    /**
     * 是否删除
     */
    private Integer isDeleted;

}	

​ 咱们在CategoryServiceImpl的delete方法中,追加业务逻辑的判断:

  1. 判断该分类是否关联了菜品,如果关联了则抛出业务异常
  2. 判断该分类是否关联了套餐,如果关联了则抛出业务异常
  3. 如果都没关联,则正常删除

CategoryServiceImpl类:

/**
 * <p>
 * 菜品及套餐分类 服务实现类
 * </p>
 *
 * @author ChiBaoBaoHuaiShuShu
 * @since 2023-05-28
 */
@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements ICategoryService {

    @Autowired
    private CategoryMapper categoryMapper;

    @Autowired
    private DishServiceImpl dishService;

    @Autowired
    private SetmealServiceImpl setmealService;

    /**
     * 新增分类和套餐
     * @param category
     * @return
     */
    @Override
    public boolean save(Category category) {
        return categoryMapper.insert(category) > 0;
    }

    /**
     * 分类信息的分页查询
     * @param page
     * @param pageSize
     * @return
     */
    @Override
    public Page<Category> page(Integer page, Integer pageSize) {
        //构建分页构造器
        Page pageInfo = new Page(page, pageSize);
        LambdaQueryWrapper<Category> lqw = new LambdaQueryWrapper<>();
        lqw.orderByDesc(Category::getUpdateTime);
        //此处不返回是因为查询好后的值是直接赋值给了上面的分页构造器了pageInfo
        categoryMapper.selectPage(pageInfo, lqw);
        return pageInfo;
    }

    /**
     * 根据id来删除分类信息
     * 删除之前判断菜品是否绑定了分类,没绑定分类信息则删,绑定了的话不能删除
     * @param id
     * @return
     */
    @Override
    public boolean delete(Long id) {
        //查询当前分类是否关联了菜品,如果已经关联,抛出一个业务异常
        LambdaQueryWrapper<Dish>dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
        dishLambdaQueryWrapper.eq(Dish::getCategoryId,id);
        long count = dishService.count(dishLambdaQueryWrapper);
        if (count > 0) {
            // 抛出一个业务异常
            throw new CustomException("当前分类关联了菜品,不能删除!!!");
        }
        //查询当前分类是否关联了套餐,如果已经管理了抛出一个业务异常
        LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
        setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId,id);
        count = setmealService.count(setmealLambdaQueryWrapper);
        if (count > 0) {
            // 抛出一个业务异常
            throw new CustomException("当前分类关联了套餐,不能删除!!!");
        }
        //正常删除分类
        return categoryMapper.deleteById(id)>0;
    }
}

​ 其中咱们自定义了一个CustomException类来处理业务异常,其要继承RuntimeException接口,该接口是一个运行异常接口。

	在接口开发的过程中,为了程序的健壮性,经常要考虑到代码执行的异常,并给前端一个友好的展示,这里就用到的自定义异常,继承RuntimeException类。那么这个RuntimeException和普通的Exception有什么区别呢。
Exception: 非运行时异常,在项目运行之前必须处理掉。一般由程序员try catch 掉。
RuntimeException,运行时异常,在项目运行之后出错则直接中止运行,异常由JVM虚拟机处理。
       在接口的逻辑判断出现异常时,可能会影响后面代码。或者说绝对不容忍(允许)该代码块出错,那么我们就用RuntimeException,但是我们又不能因为系统挂掉,只在后台抛出异常而不给前端返回友好的提示吧,至少给前端返回出现异常的原因。因此接口的自定义异常作用就体现出来了。

CustomException类

/**
 * @Author HuaiShuShu
 * @Date 2023/5/31 17:10
 * @PackageName:com.huaishushu.common
 * @Description: 自定义业务异常
 * @Version 1.0
 */
//RuntimeException运行时异常
public class CustomException extends RuntimeException{

    public CustomException(String message) {
        super(message);
    }

}

​ 之后再在咱们之前定义的全局异常处理器GlobalExceptionHandler中添加咱们自定义的业务异常,使用@ExceptionHandler注解来绑定咱们自定义的异常

GlobalExceptionHandler类

/**
 * @Author HuaiShuShu
 * @Date 2023/5/29 0:06
 * @PackageName:com.huaishushu.common
 * @Description: 全局异常处理器
 * @Version 1.0
 */
//开启异常处理,annotations属性可以配置需要拦截的位置。
@ControllerAdvice(annotations = {RestController.class, Controller.class})
//添加@ResponseBody,因为之后捕获异常以后需要返回json形式的数据
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 异常处理方法
     * 处理这个异常类型的报错(SQLIntegrityConstraintViolationException)
     * @return
     */
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public R<String> 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("未知错误!!!");
    }

    /**
     * 异常处理方法
     * 处理自定义的异常 CustomException.class
     * @return
     */
    @ExceptionHandler(CustomException.class)
    public R<String> exceptionHandler(CustomException ex) {
        log.error(ex.getMessage());

        return R.error(ex.getMessage());
    }

}

​ 之后测试即可,咱们可以看到能够正常添加和删除刚添加的分类信息,而原本就有的分类信息无法删除,这是因为我们自己新添加的还没有绑定菜品和套餐。

修改分类

​ 这部分的修改功能和前面的员工信息的修改是一样的业务逻辑:

  1. 点击修改按钮后,回显当前行的分类信息。
  2. 在修改窗口完成修改分类信息后,点击确定按钮实现对分类信息的修改。

​ 但是当我们点击编辑按钮后,弹出的修改窗口中已经有当前行的数据回显了,这是因为我们的新增菜品分类、新增套餐分类,修改分类,这三个的窗口用的是同一套数据模版。

分类信息中窗口模版一样

​ 因此前段这块的代码就是把同一套数据模型修改了而已,修改按钮那块把当前行的数据传过去了,然后下面的函数进行了数据替换,所以就能够显现了(这块需要有一定前端基础的小伙伴看比较好)

分类修改数据回显

​ 当我们点击确定后,前端会发送分类修改的url请求,并且其携带了一个category实体类参数。

分类修改的url1

​ 那么我们接下来的做法和员工信息修改那块差不多了。

Controller层

/**
     * 根据id修改分类信息
     * @param category
     * @return
     */
    //@PutMapping
    public R<String> update(@RequestBody Category category) {
        log.info("修改分类信息:{}",category);
        boolean flag = categoryService.update(category);
        if (flag) {
            return R.success("修改分类信息成功");
        }
        return R.error("修改分类信息失败!!!");
    }

Service接口

/**
     * 根据id修改分类信息
     * @param category
     * @return
     */
    boolean update(Category category);

Service实现类

/**
     * 根据id修改分类信息
     * @param category
     * @return
     */
    @Override
    public boolean update(Category category) {
        return categoryMapper.updateById(category) > 0;
    }

菜品模块

文件上传下载

文件上传介绍

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

文件上传时,对页面的form表单有如下要求

  • method="post" 采用post方式提交数据
  • enctype="multipart/form-data 采用multipart格式上传文件
  • type="file" 使用input的file控件上传

服务端要接收客户端页面上传的文件,通常都会使用Apache的两个组件

  • commons-fileupload
  • commons-io

​ Spring框架在spring-web包中对文件上传进行了封装,大大简化了服务端代码,我们只需要在Controller的方法中声明一个MultipartFile类型的参数即可接收上传的文件,例如:

上传

文件下载介绍

​ 文件下载,也称为download,是指将文件从服务器传输到本地计算机的过程 通过浏览器进行文件下载,通常有两种表现形式:

  • 以附件形式下载,弹出保存对话框,将文件保存到指定磁盘目录
  • 直接在浏览器中打开

通过浏览器进行文件下载,本质上就是服务端将文件以流的形式写回浏览器的过程

文件上传代码实现

文件上传,页面端可以使用ElementUI提供的上传组件。

可以直接使用资料中提供的上传页面,位置: 资料/文件上传下载页面/upload.html

上组件

​ 将其复制放在page文件夹下并新建一个demo的文件夹下。

upload的位置

​ 通过查看它的代码,我们可以看到,上传文件的url请求路径是/common/upload,并且是post请求。

文件上传的url

​ 所以我们在Controller层下创建一个CommonController类,并通过它来创建接收上传文件url实现上传功能。

CommonController类

/**
 * @Author HuaiShuShu
 * @Date 2023/5/31 20:46
 * @PackageName:com.huaishushu.controller
 * @Description: 文件上传和下载
 * @Version 1.0
 */
@RestController
@RequestMapping("/common")
@Slf4j
public class CommonController {

    @Value("${ruiJiWaiMai.path}")
    private String basePath;

    /**
     * 文件上传
     * @param file (该参数的名称应与前端的请求表单中的name对应)
     * @return
     */
    @PostMapping("/upload")
    //这里MultipartFile类型的参数名称不能随便起,应该和前端的请求表单中的name对应
    public R<String> upload(MultipartFile file) {
        //file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会删除

        //原始文件名
        String originalFilename = file.getOriginalFilename();
        //获取文件的后缀
        String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
        //使用UUID重新生成文件名,防止文件名重复造成文件覆盖
        String fileNmae = UUID.randomUUID().toString() + suffix;
        //创建一个目录对象
        File dir = new File(basePath);
        //判断当前目录是否存在
        if (!dir.exists()) {
            //目前不存在,需要创建
            dir.mkdir();
        }

        log.info(file.toString());
        try {
            //将临时文件转存到指定位置
            file.transferTo(new File(basePath + fileNmae));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return R.success(fileNmae);
    }
    
}

​ 并且在yml文件中通过自定义属性来动态为上传路径赋值,并通过@Value注解为其basePath,笔者是在项目中的images文件夹下新建了一个path文件夹来保存上传的文件。

#自定义一个属性,用来做文件上传的地址
ruiJiWaiMai:
  path: D:\GongZuo\IDEA\XianMu\RuiJiWaiMai-springboot-MybatisPlus\RuiJiWaiMai\src\main\resources\static\backend\images\path\

温馨提示

​ 我们上传好后可以去相应路径下查看是否上传成功,但发现上传后,但是在相应文件夹下却没有显示的时候,我们应该去先去登录账号先,因为我们前面配置的过滤器中需要判断我们是否已经登录了账号,没登录的话就会被过滤了。

​ 解决方法:

  • 可以先在登录页中登录账号后,在去测试上传功能。
  • 可以在过滤器中添加免过滤的路径,/demo/upload

文件下载代码实现

文件下载,页面端可以使用转存失败,建议直接上传图片文件标签展示下载的图片

下载实现代码

​ 文件下载我们在CommonController类中添加download方法,并设置@GetMapping("/download")来获取前端下载的url请求。

/**
     * 文件下载
     * @param name
     * @param response
     */
    @GetMapping("/download")
    public void download(String name, HttpServletResponse response) {
        try {
            //输入流,通过输入流读取文件内容
            FileInputStream fileInputStream = new FileInputStream(new File(basePath + name));

            //输出流,通过输出流将文件写回浏览器,在浏览器展示图片了
            ServletOutputStream outputStream = response.getOutputStream();

            //设置输出文件的类型
            response.setContentType("image/jpeg");

            int len = 0;
            byte[] bytes = new byte[1024];
            while ((len = fileInputStream.read(bytes)) != -1) {
                outputStream.write(bytes, 0, len);
                outputStream.flush();
            }

            //关闭资源
            outputStream.close();
            fileInputStream.close();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

​ 这个就是上传成功后,前端组件自动调用咱们刚刚写好的下载方法,下载好后并回显给前端组件框了。

图片下载回显

新增菜品

需求分析

​ 后台系统中可以管理菜品信息,通过新增功能来添加一个新的菜品,在添加菜品时需要选择当前菜品所属的菜品分类并且需要上传菜品图片,在移动端会按照菜品分类来展示对应的菜品信息。

新增菜品

数据模型

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

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

  • dish 菜品表
  • dish flavor 菜品口味表

代码开发-准备工作

在开发业务功能前,先将需要用到的类和接口基本结构创建好:

  • 实体类 DishFlavor(直接从课程资料中导入即可,Dish实体前面课程中已经导入过了)
  • Mapper接口 DishFlavorMapper
  • 业务层接口 DishFlavorService
  • 业务层实现类 DishFlavorServicelmpl
  • 控制层 DishController

​ 如果使用的是MP生成的表结构,那么我们需要在实体类 DishFlavor中为那几个公共字段添加@TableField注解,与前面员工实体类和分类实体类一样的。

​ 对于Controller层的话,因为视频里黑马的老师打对于菜品口味和菜品管理的Controller层的操作都放在了DishController类里,所以这里我们也按照视频中一样这样操作。

​ 对于MP代码生成器帮我们生成的DishFlavorController类,我们可以先留着,看看后期开发会不会用到,如果最后用不到在删掉也是可以的,目前是不影响开发的。

代码开发-梳理交互过程

在开发代码之前,需要梳理一下新增菜品时前端页面和服务端的交互过程:

  1. 页面(backend/page/food/add.html)发送ajax请求,请求服务端获取菜品分类数据并展示到下拉框中

    新增菜品,发送分类的url请求
  2. 页面发送请求进行图片上传,请求服务端将图片保存到服务器

  3. 页面发送请求进行图片下载,将上传的图片进行回显

  4. 点击保存按钮,发送ajax请求,将菜品相关数据以ison形式提交到服务端

​ 开发新增菜品功能,其实就是在服务端编写代码去处理前端页面发送的这4次请求即可,2和3这块的上传和回显,我们在前面的测试中就已经在CommonController类中已经完成了。

新增菜品中图片回显

分类下拉框实现

​ 我们来看分类下拉框这个业务功能的实现,它的前端代码逻辑,我们刚进入到添加菜品这个页面的时候它的vue钩子函数就会调用getDishList()方法,并把type=1这个参数给传递过去给food.js,然后发送了下面的这个查询分类信息的url请求给后端来获取分类信息中分类的数据。

新增菜品,发送分类的url请求转存失败,建议直接上传图片文件

添加菜品-分类下拉框前端代码

​ 因为它发送的请求是/category的请求,因此我们在CategoryController类来编写它的控制方法来获取请求并返回数据给前端。

Controller层

​ 这里因为前端传递的url请求中有type参数,这里我们可以在控制方法定义参数来接收它的时候,可以选择直接定义一个list(Integer type)来接收,也可以选择定义实体类参数list(Category category)来接收,这里我们选择实体类来接收,因为这样后期我们也可以传递其他的参数来查询分类信息,复用性较好。

/**
     * 根据条件来查询所有分类信息
     * @param category
     * @return
     */
    @GetMapping("list")
    public R<List<Category>> list(Category category) {
        List<Category> categoryList = categoryService.list(category);
        return R.success(categoryList);
    }

Service接口

/**
     * 根据条件来查询所有分类信息
     * @param category
     * @return
     */
    List<Category> list(Category category);

Service实现类

/**
     * 根据条件来查询所有分类信息
     * @param category
     * @return
     */
    @Override
    public List<Category> list(Category category) {
        LambdaQueryWrapper<Category> lqw = new LambdaQueryWrapper<>();
        lqw.eq(category.getType() != null, Category::getType, category.getType());
        //添加个排序,一个是按顺序升序排序,一个是按更新时间降序排序
        lqw.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime);
        List<Category> categoryList = categoryMapper.selectList(lqw);
        return categoryList;
    }

​ 之后测试,即可看到分类信息显示成功了。

菜品添加中,分类信息显示成功

菜品添加保存实现

​ 因为我们点击保存后可以看到发送的请求路径为http://localhost/dish,并且为post请求,所以我们在Controller层要定义的一个post的控制方法来接收它的参数来实现存储。

菜品添加发送的url参数

代码开发-导入DTO

导入DishDto (位置: 资料/dto) ,用于封装页面提交的数据

​ 因为我们可以从图中看到前端页面传回来的参数类型较多,我们无法只用Dish这个实体类来接收,里面有些参数不是Dish类里的属性,因此我们可以定义一个新的实体类来接收这些参数,这种实体类的名称为DTO(数据传输服务对象)。

package com.huaishushu.dto;


import com.huaishushu.entity.Dish;
import com.huaishushu.entity.DishFlavor;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;

/**
 * 数据传输服务对象,用于辅助我们来完成一些关于多对多表的业务操作
 */
@Data
public class DishDto extends Dish {

    private List<DishFlavor> flavors = new ArrayList<>();

    private String categoryName;

    private Integer copies;
}

​ 其中flavors是用来接收页面传输回来的口味集合的,而另外两个目前暂时还用不上,放着不用理它就行了。

Controller层

/**
 * <p>
 * 菜品管理 前端控制器
 * </p>
 *
 * @author ChiBaoBaoHuaiShuShu
 * @since 2023-05-28
 */
@RestController
@RequestMapping("/dish")
@Slf4j
public class DishController {

    @Autowired
    private IDishService dishService;

    @Autowired
    private IDishFlavorService dishFlavorService;

    /**
     * 新增菜品信息,操作的是两张表,还需要插入口味
     * @return
     */
    @PostMapping
    public R<String> save(@RequestBody DishDto dishDto) {
        dishService.saveWithFlavor(dishDto);
        return R.success("新增菜品成功");
    }
}

service接口

/**
 * <p>
 * 菜品管理 服务类
 * </p>
 *
 * @author ChiBaoBaoHuaiShuShu
 * @since 2023-05-28
 */
public interface IDishService extends IService<Dish> {

    /**
     * 新增菜品信息,操作的是两张表,还需要插入口味
     * @return
     */
    void saveWithFlavor(DishDto dishDto);

}

service实现类

/**
 * <p>
 * 菜品管理 服务实现类
 * </p>
 *
 * @author ChiBaoBaoHuaiShuShu
 * @since 2023-05-28
 */
@Service
public class DishServiceImpl extends ServiceImpl<DishMapper, Dish> implements IDishService {

    @Autowired
    DishMapper dishMapper;

    @Autowired
    IDishFlavorService dishFlavorService;

    /**
     * 新增菜品信息,操作的是两张表,还需要插入口味
     * @return
     */
    @Override
    @Transactional
    public void saveWithFlavor(DishDto dishDto) {
        //保存菜品的基本信息到菜品表dish
        this.save(dishDto);
        //菜品id
        Long dishId = dishDto.getId();
        //为所有的菜品口味添加菜品的ID
        for (DishFlavor dishFlavor : dishDto.getFlavors()) {
            dishFlavor.setDishId(dishId);
        }
        //保存菜品口味数据到菜品口味表dish_flavor
        dishFlavorService.saveBatch(dishDto.getFlavors());
    }
}

​ 在接口实现类中,因为前端传回来的菜品口味参数是没有菜品的参数的,因此我们需要遍历为其每个口味赋值菜品的id。

菜品信息分页查询

需求分析

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

菜品分页显示

​ 从图中我们可以看出,菜品里头显示有菜品的分类的名称,这个分类名称在外面的dish表中是没有存储的,我们是只保存有分类的id,所以菜品分页查询这块会有些复杂。

代码开发

代码开发-梳理交互过程

​ 在开发代码之前,需要梳理一下菜品分页查询时前端页面和服务端的交互过程:

  1. 页面(backend/page/food/list.html)发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端,获取分页数据,其中name是我们在菜品管理中用来进行模糊查询的,这块和员工分页查询业务是一样的。
  2. 页面发送请求,请求服务端进行图片下载,用于页面图片展示

​ 开发菜品信息分页查询功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。

​ 我们这个模块的所有的MVC层的方法都是在Dish所对应的Controller、service、serviceImpl中中完成的,那么这里有的小伙伴可能就会按照我们之写员工分页查询那样,这样写没有问题,但是因为dish表中只保存分类信息的id,并没有保存名称,所以页面中是不会显示分类名称的,所以我们这样写的话是只完成了一半。

@Override
    public Page<Dish> page(Integer page, Integer pageSize, String name) {
        //构建分页构造器
        Page pageInfo = new Page(page, pageSize);
        LambdaQueryWrapper<Dish> lqw = new LambdaQueryWrapper<>();
        lqw.like(StringUtils.isNoneEmpty(name), Dish::getName, name);
        lqw.orderByDesc(Dish::getUpdateTime);
        //此处不返回是因为查询好后的值是直接赋值给了上面的分页构造器了pageInfo
        dishMapper.selectPage(pageInfo, lqw);
        return pageInfo;
    }

菜品分类完成一半

​ 我们可以到static/backend/page/food/list.html的文件下找到菜品分类的分类名称的变量,是categoryName,但是后端传给前端的数据是没有这个变量的。

菜品分类名称的显示-前端名称

后端给前端的变量

Controller层

/**
     * 菜品信息分页查询
     * @param page
     * @param pageSize
     * @param name
     * @return
     */
    @GetMapping("/page")
    public R<Page> page(Integer page, Integer pageSize, String name) {
        Page<DishDto> dishDtoPage = dishService.page(page, pageSize, name);
        log.info("菜品信息分页查询:{}",dishDtoPage);
        return R.success(dishDtoPage);
    }

Service接口

/**
     * 菜品信息分页查询
     * @param page
     * @param pageSize
     * @param name
     * @return
     */
    Page<DishDto> page(Integer page, Integer pageSize, String name);

Service实现类

/**
     * 菜品信息分页查询
     * @param page
     * @param pageSize
     * @param name
     * @return
     */
    @Override
    public Page<DishDto> page(Integer page, Integer pageSize, String name) {
        
        Page<Dish> pageInfo = new Page(page, pageSize);
        LambdaQueryWrapper<Dish> lqw = new LambdaQueryWrapper<>();
        lqw.like(StringUtils.isNoneEmpty(name), Dish::getName, name);
        lqw.orderByDesc(Dish::getUpdateTime);
        //此处不返回是因为查询好后的值是直接赋值给了上面的分页构造器了pageInfo
        dishMapper.selectPage(pageInfo, lqw);

        //构建DishDto的分页构造器
        Page<DishDto> dishDtoPage = new Page<>();
        //对象拷贝,但不拷贝“records”
        BeanUtils.copyProperties(pageInfo, dishDtoPage, "records");

        List<Dish> records = pageInfo.getRecords();
        //这块用的是stream().map(字符流加lmabe)
        List<DishDto> dishDtoList = records.stream().map((itme) -> {
            DishDto dishDto = new DishDto();
            //因为是新new出来的,里面什么数据都没有,因此需要拷贝赋值
            BeanUtils.copyProperties(itme, dishDto);
            Long categoryId = itme.getCategoryId();
            Category category = categoryService.getById(categoryId);
            //此处是因为当我们自己添加或通过sql文件导入菜品时并没有设置分类,所以可能是会为空的
            if (category != null) {
                String categoryName = category.getName();
                dishDto.setCategoryName(categoryName);
            }
            return dishDto;
        }).collect(Collectors.toList());
        dishDtoPage.setRecords(dishDtoList);
        return dishDtoPage;
    }

​ 这块的Impl是实现类的业务逻辑实现对于不太熟练的小伙伴来说可能会有些懵,

​ 我们来梳理一下整体我们的目的,如果我们只是单纯的时候 Page来进行分页查询,会缺少categoryName分类名称,因此这里我们使用到了DishDto类这个数据传输服务对象,构造一个关于构建DishDto的分页构造器对象dishDtoPage,从之前我们的代码梳理的后端传递给前端的Page可以看到,分页的数据是封装在records集合里的,所以接下来外面的步骤是:

  1. 将除records集合的其他所有数据拷贝给dishDtoPage对象。
  2. 从pageInfoz中获取records的list集合并通过字节流加lambda表达式遍历它,它的每一个遍历对象变量为item,将每个dish对象的数据拷贝给dishdto(因为dishdto是我们新new出来的所以其里面是空的。)
  3. 之后通过item获取categoryId,之后我们在public Page page(Integer page, Integer pageSize, String name)方法外,新自动装配一个IcategoryService的对象,通过categoryService来通过id查询获取到Category对象。
  4. 因为我们是通过自己添加或通过sql文件导入菜品时并没有设置分类,所以可能是会为空的,所以做一次判断,如果不为空则通过Category对象来获取categoryName分类名称,之后在赋值给dishDto里头的categoryName。
  5. 之后收集其records的list集合遍历后的结果,并封装成一个list对象,并赋值给List dishDtoList。
  6. 最后在将dishDtoList这个list赋值给dishDtoPage的records,因为前面我们通过BeanUtils工具类拷贝pageInfo和dishDtoPage的时候是排除了records,所以dishDtoPage的records此时是为null的,当我们把records赋值给dishDtoPage时,它才是完整的一个分页类。

功能测试

​ 从第一步就可以看出传给前端page中是没有categoryName这个属性对象的。

菜品分页功能测试1

​ 当我们创建出dishDtoPage对象时,它还是空的,此时内部是并没有数据的。

菜品分页功能测试2f

​ 我们可以看到当我们运行完BeanUtils工具类为dishDtoPage赋值后,它的其他属性部分都是由数据了,只有records部分还是空的。

菜品分页功能测试3

​ 之后当我们使用steam字节流加lambda表达式来遍历records的list对象时,我们新建的DishDto对象是空的。

菜品分页功能测试4

​ 当我们将itme(其实就是dish对象)拷贝给dishDto后,它出了分类信息的名称没有以外,其他都是有数据了。

菜品分页功能测试5

​ 最后将收集到的list对象赋值给dishDtoList,并将其在赋值给dishDtoPage对象的records,这样我们就有一个完整的Page对象了

测试功能6

​ 之后将这个对象放回给前端后,那么前端就可以正常显示了。

菜品分类信息的功能测试7

问题-项目无法启动

​ 在这处业务开发中,有的小伙伴可能会遇到以下的报错提示,这里的报错信息显示:

	不鼓励依赖循环引用,默认情况下禁止使用循环引用。更新应用程序以删除 Bean 之间的依赖循环。作为最后的手段,可以通过将spring.main.allow-circular-references设置为true来自动打破循环。


菜品分页开发遇到的问题

​ 这主要是因为我们在前面的分类信息的service实现类中定义了一个自动装配dishService对象,这是一个bean,而我们在刚刚的菜品分页查询中又定义了一个自动装配categoryService对象,这也是一个bean,所以导致了bean的嵌套了,在新版的spring中这是不鼓励的。

菜品分类查询问题1

菜品分页问题2

解决方法

​ 前面的报错信息也提示我们了,所以我们在yml文件中以下属性即可

spring:
	main:
		allow-circular-references: true

问题-菜品图片显示

​ 有的小伙伴会发现我们的菜品展示中,只有我们后面自己手动添加的菜品有图片,通过sql文件生成的那些菜品没有图片,这个问题是因为我们的图片回显下载路径就是我们的图片上传路径时指定的位置,所以我们要将黑马提供给我们的图片资源也保存到一样的路径下,这样在菜品展示时才会从指定的位置中下载回来图片进行回显。

图片下载回显的自定义路径

图片保存的位置

菜品管理页面图片显示

修改菜品

需求分析

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

菜品修改需求分析

代码开发-梳理交互过程

在开发代码之前,需要梳理一下修改菜品时前端页面 (add.html)和服务端的交互过程:

  1. 页面发送aiax请求,请求服务端获取分类数据,用于菜品分类下拉框中数据展示
  2. 页面发送ajax请求,请求服务端,根据id查询当前菜品信息,用于菜品信息回显
  3. 页面发送请求,请求服务端进行图片下载,用于页图片回显
  4. 点击保存按钮,页面发送ajax请求,将修改后的菜品相关数据以ison形式提交到服务端

​ 开发修改菜品功能,其实就是在服务端编写代码去处理前端页面发送的这4次请求即可,因为我们前面已经完成了对于菜品分类下拉框的业务,以及图片的上传和下载回显模块,其实就想担当于已经完成了1和3,那么我们剩下只需要完成2和4即可。

请求2

​ 第二次请求,对于根据id来查询当前菜品信息,并回显,我们可以先看以下前端页面发送的url请求链接,从中我们可以看到其id是以请求路径的信息返回的,并不是以参数的形式,因此我们之后在Controller层中定义方法来接收的时候就需要在@GetMapping中定义参数来接收了。

菜品修改信息回显url

Controller层

/**
     * 根据id来查询菜品信息和对于的口味信息
     * @param id
     * @return
     */
    @GetMapping("{id}")
    public R<DishDto> getByIdWithFlavor(@PathVariable("id") Long id) {
        DishDto dishDto = dishService.getByIdWithFlavor(id);
        return R.success(dishDto);
    }

Service接口

/**
     * 根据id来查询菜品信息和对于的口味信息
     * @param id
     * @return
     */
    public DishDto getByIdWithFlavor(Long id);

Service实现类

/**
     * 根据id来查询菜品信息和对于的口味信息
     * @param id
     * @return
     */
    @Override
    public DishDto getByIdWithFlavor(Long id) {
        //查询菜品基本信息,从dish表查询
        Dish dish = this.getById(id);

        DishDto dishDto = new DishDto();
        BeanUtils.copyProperties(dish,dishDto);

        //查询当前菜品对应的口味信息,从dish_flavor表查询
        LambdaQueryWrapper<DishFlavor> lqw = new LambdaQueryWrapper<>();
        lqw.eq(DishFlavor::getDishId, dish.getId());
        List<DishFlavor> flavors = dishFlavorService.list(lqw);
        dishDto.setFlavors(flavors);

        return dishDto;
    }

​ 业务分析后我们可以知道,我们还需要回显菜品关联的口味信息,所以如果我们光用dish对象来直接查询数据库的话,不没法回显完整的菜品信息中中的口味信息的,因此这里我们使用的是dishDto对象来完成的,它的实现步骤为:

  1. 先查询菜品的基本信息,直接从dish表中查询
  2. 将查询到的菜品基本信息dish拷贝给dishDto对象
  3. 查询当前菜品对应的口味信息,从dish_flavor表查询
  4. 查询到后赋值给dishDto对象中的flavors属性即可
  5. 返回dishDto给前端

菜品修改信息回显成功

请求4

​ 当我们点击保存按钮后,页面发送的请求中,我们可以看到它的请求类型为PUT,并且参数的类型和我们前面添加菜品信息的格式是一样的,因此我们这块可以参考之前新增菜品信息的方法。

菜品信息修改保存的url

Controller层

/**
     * 修改菜品信息,操作的是两张表,还需要插入口味
     * @return
     */
    @PutMapping
    public R<String> update(@RequestBody DishDto dishDto) {
        dishService.updateWithFlavor(dishDto);
        return R.success("新增菜品成功");
    }

Service接口

/**
     * 修改菜品信息,操作的是两张表,还需要插入口味
     * dish、dish_flavor两张表
     * @return
     */
    void updateWithFlavor(DishDto dishDto);

Service实现类

/**
     * 修改菜品信息,操作的是两张表,还需要插入口味
     * dish、dish_flavor两张表
     * @return
     */
    @Override
    @Transactional
    public void updateWithFlavor(DishDto dishDto) {
        //更新菜品的基本信息到菜品表dish
        this.updateById(dishDto);

        //清理当前菜品对应口味数据--dish_flavor表的delete操作
        LambdaQueryWrapper<DishFlavor> lqw = new LambdaQueryWrapper<>();
        lqw.eq(DishFlavor::getDishId, dishDto.getId());
        dishFlavorService.remove(lqw);

        //菜品id
        Long dishId = dishDto.getId();
        //为所有的菜品口味添加菜品的ID
        for (DishFlavor dishFlavor : dishDto.getFlavors()) {
            dishFlavor.setDishId(dishId);
        }
        //更新菜品口味数据到菜品口味表dish_flavor
        dishFlavorService.saveBatch(dishDto.getFlavors());
    }

​ 在实现类中,因为其的功能和前面新增菜品类似,所以我们可以参考前面的方法,以下是我们的实现步骤:

  1. 先修改更新菜品的基本信息。
  2. 清理当前菜品对应的口味数据。
  3. 当我们重新保存口味信息时,遇到情况是和新增菜品时一样的,缺少菜品的id,所有循环遍历为其赋值。
  4. 重新为其添加菜品的口味信息,以达到口味信息修改的业务目的。

​ 后面更新那块有的小伙伴可能会想使用MP中的updateBatchById来批量修改,我们不能使用updateBatchById批量修改的原因是,前端不一定会发回来一样的口味,可能多一个或者少一个口味,因为批量根据id修改是根据集合中的数量修改的,如果少了一个口味,那么就少改了一个,从而数据库中保留了一个口味导致多了一个口味,或者发回来多了一个口味,导致无法修改成功。

业务逻辑漏洞

​ 我们后面这边更新口味信息这块是由逻辑漏洞的,比如我们删除过后,当我们重新添加口味信息的时候,因为口味信息中保存由创建时间和创建人的信息,这样我们删除后在重新添加口味信息,那么创建时间和创建人又变为当前的时间和当前的用户信息了,这是不合理的。

​ 这块需要看看黑马中的后续开发中是否会修补这部分的漏洞问题,或者笔者之后自己修改这部分的逻辑问题,因为笔者的这些笔记都是一边跟着做开发一边编写的,所以后续的内容还不清楚。

菜品状态的启用/禁用

需求分析

​ 在菜品管理列表页面点击禁用/启用按钮,或者批量选择菜品后,来完成对于菜品状态的修改,这块的设计不考虑菜品是否已在套餐中,因为也普遍存在一些菜品只做套餐售卖,不做单点,因此这块的业务逻辑就是正常的直接启售/禁售,无需考虑其他的。

菜品状态的确定框

代码开发-梳理交互过程

​ 当我们点击启用/禁用,或者批量选择后使用批量启用/禁用时,前端发送ajax给后端,后端根据前端的发送回来的情况,在具体操作,对于状态的修改有如下4种类型的ajax请求:

禁用:

  • 菜品信息禁用url1

  • 菜品信息禁用url2

启用:

  • 菜品启用url2

  • 菜品启用url1

​ 它们所发送的请求方式都是POST,传递的参数都是一样的,这是一个ids参数,这块有比较多种的接收方式,笔者这里使用的是字符串String去接收的,因为笔者前面使用list去接收的时候没有效果,还报错了,所以改了String,至于报错的原因是没有使用@RequestParam注解去标识list,所以导致没有正常接收参数报错,对于其前置路径都是一样,唯一不一样的就是禁用请求的后一个占位路径为0,而启用的则为1。

​ 因此这块我们就应该想到在Controller层的控制方法上的@POSTMapping中使用一个占位符来把它获取,之后做判断来进行是启用还是禁用操作。

Controller层

/**
     * 根据传递回来的status来判断是启用还是修改操作
     * 之后根据id来批量删除修改
     *
     * @param status
     * @param ids
     * @return
     */
    @PostMapping("/status/{status}")
    public R<String> updateStatusById(@PathVariable("status") Integer status, String ids) {
        //应为参数ids为String类型的参数,并且里面的id用 , 隔开,因此我们需要将其分割出来
        String[] split = ids.split(",");
        dishService.updateStatusById(status, split);
        return R.success("修改状态成功");
    }

Service接口

/**
     * 根据传递回来的status来判断是启用还是修改操作
     * 之后根据id来批量删除修改
     * @param status
     * @param ids
     * @return
     */
    void updateStatusById(Integer status, String[] ids);

Service实现类

/**
     * 根据传递回来的status来判断是启用还是修改操作
     * 之后根据id来批量删除修改
     * @param status
     * @param ids
     * @return
     */
    @Override
    @Transactional
    public void updateStatusById(Integer status, String[] ids) {
        //禁售业务操作
        if (status == 0) {
            for (String id : ids) {
                dishMapper.updateStatusById(0, Long.valueOf(id));
            }
        }
        //起售业务操作
        if (status == 1) {
            for (String id : ids) {
                dishMapper.updateStatusById(1, Long.valueOf(id));
            }
        }
    }

mapper接口

/**
     * 根据id修改菜品状态信息
     * @param status
     * @param id
     * @return
     */
    int updateStatusById(@Param("status") Integer status, @Param("id") Long id);

mapper.xml

<mapper namespace="com.huaishushu.mapper.DishMapper">

    <update id="updateStatusById">
        update dish
        set status = #{status,jdbcType=NUMERIC}
        where id = #{id,jdbcType=NUMERIC}
    </update>
</mapper>

​ 整体逻辑比较简单,我没有用MP我根据会比较麻烦,所以就使用了编写sql的方式来,这样反而很快。

删除菜品

需求分析

​ 在菜品管理列表页面点击删除按钮,或者批量选择菜品后,来完成对于菜品信息的删除,但是因为我们前面在新增菜品的时候也保存有对应的口味信息,因此当我们删除菜品信息的时候,相应的口味信息也应该跟着一起删除掉,并且还要判断当前的菜品是否为起售,或者当前菜品是否在套餐中,如果在的话那么无法删除。

删除菜品

代码开发-梳理交互过程

​ 当我们点击删除,或者批量选择后使用批量删除的时候,前端发送ajax给后端,其请求类型为DELETE类型,携带的参数类型与前面的启用和禁用是一样的string字符串类型的ids。

菜品删除url1

菜品删除url2

​ 此处405是因为我前面已经定义并完成了这块功能,我只是把@DeleteMapping给注释掉了,浏览器还没反应过来,我开了热部署。

Controller层

/**
     * 根据id删除菜品信息(也可批量删除)
     * @param ids (id字符串)
     * @return
     */
    @DeleteMapping
    public R<String> delete(String ids) {
        String[] split = ids.split(",");
        dishService.delete(split);
        return R.success("删除成功");
    }

Service接口

/**
     * 根据id删除菜品信息(也可批量删除)
     * @param ids (id数组)
     */
    void delete(String[] ids);

Service实现类

/**
     * 根据id删除菜品信息(也可批量删除)
     * @param ids (id数组)
     */
    @Override
    @Transactional
    public void delete(String[] ids) {
        for (String id : ids) {

            //进行业务判断,当前菜品是否在套餐中售卖,以及是否为起售状态,如果是则抛出业务异常
            Dish dish = dishMapper.selectById(id);
            LambdaQueryWrapper<SetmealDish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
            dishLambdaQueryWrapper.eq(SetmealDish::getDishId, id);
            long count = setmealDishService.count(dishLambdaQueryWrapper);
            if (dish.getStatus() == 1) {
                throw new CustomException("当前菜品正在起售,无法删除");
            }
            if (count > 0) {
                throw new CustomException("当前菜品在套餐中,无法删除");
            }

            //删除菜品的基本信息
            dishMapper.deleteById(id);
            //删除菜品对应的口味信息
            LambdaQueryWrapper<DishFlavor> lqw = new LambdaQueryWrapper<>();
            lqw.eq(DishFlavor::getDishId, id);
            dishFlavorService.remove(lqw);
        }
    }

​ 整体思路就是遍历id,并进行业务逻辑判断当前菜品是否符合业务逻辑,是否在起售,是否在套餐中,如果都不在,那么逐一删除当前菜品的基本信息以及关联的口味信息。

套餐模块

新增套餐

需求分析

​ 套餐就是菜品的集合。

​ 后台系统中可以管理套餐信息,通过新增套餐功能来添加一个新的套餐,在添加套餐时需要选择当前套餐所属的套餐分类和包含的菜品,并且需要上传套餐对应的图片,在移动端会按照套餐分类来展示对应的套餐。

新增套餐需求分析

新增套餐需求分析移动端

数据模型

​ 新增套餐,其实就是将新增页面录入的套餐信息插入到setmeal表,还需要向setmeal dish表插入套餐和菜品关联数据所以在新增套餐时,涉及到两个表: setmeal 套餐表

  • 套餐的表结构

setmeal dish 套餐菜品关系表

  • 套餐菜品的表结构

代码开发

在开发业务功能前,先将需要用到的类和接口基本结构创建好:

  • 实体类SetmealDish(直接从课程资料中导入即可,Setmeal实体前面课程中已经导入过了)
  • DTO SetmealDto (直接从课程资料中导入即可)
  • Mapper接口 SetmealDishMapper
  • 业务层接口 SetmealDishService
  • 业务层实现类 SetmealDishServicelmpl
  • 控制层 SetmealController

​ 如果是使用了MP代码生成器的小伙伴则导入SetmealDto,并且在相应的实体类上补充公共字段填充的注解@TableField,还有就是在实体类属性上isDeleted补充@TableLogic注解,这个是用来做逻辑删除的时候使用的。

​ 之后主要是在SetmealController来调用接口方法,而我们使用MP生成的多余的Controller等整个项目开发完后,如果实在没用,在删除即可。

代码开发-梳理交互过程

在开发代码之前,需要梳理一下新增套餐时前端页面和服务端的交互过程:

  1. 页面(backend/page/combo/add.html)发送ajax请求,请求服务端获取套餐分类数据并展示到下拉框中

    新增套餐的第一个url

  2. 页面发送aiax请求,请求服务端获取菜品分类数据并展示到添加菜品窗口中

    新增套餐的第二个交互过程

  3. 页面发送aiax请求,请求服务端,根据菜品分类查询对应的菜品数据并展示到添加菜品窗口中

  4. 页面发送请求进行图片上传,请求服务端将图片保存到服务器

  5. 页面发送请求进行图片下载,将上传的图片进行回显

  6. 点击保存按钮,发送ajax请求,将套餐相关数据以ison形式提交到服务端

​ 开发新增套餐功能,其实就是在服务端编写代码去处理前端页面发送的这6次请求即可,不过1、2、4、5的这四次url的控制方法我们都已经在前面的模块中完成了,1、2是在前面菜品的添加业务中完成的,图片也是,因此我们只需要完成2、6即可。

2、菜品信息显示

​ 对于我们的菜品信息显示,我们从页面发送的url中就可以看到,其是发送的分类信息的id来获取菜品的信息数据,因此我们只需要在DishController层定义相应的控制方法来获取参数并返回数据即可。

新增套餐菜品信息回显url

​ 因为它传递的参数是categoryId分类信息的id,而我们的Dish实体类中也有这个属性,因此在控制器方法中定义实体类来接收参数复用性会更好。

DishController

/**
     * 根据条件来查询菜品信息
     * @param dish
     */
    @GetMapping("/list")
    public R<List<Dish>> list(Dish dish) {
        List<Dish> dishList = dishService.listByCategoryId(dish);
        return R.success(dishList);
    }

IDishService接口

/**
     * 根据条件来查询菜品信息
     * @param dish
     */
    List<Dish> listByCategoryId(Dish dish);

IDishService实现类

/**
     * 根据条件来查询菜品信息
     * @param dish
     */
    @Override
    public List<Dish> listByCategoryId(Dish dish) {
        LambdaQueryWrapper<Dish> lqw = new LambdaQueryWrapper<>();
        lqw.eq(dish.getCategoryId() != null, Dish::getCategoryId, dish.getCategoryId());
        //查询状态为1的数据,就是起售的
        lqw.eq(Dish::getStatus, 1);
        //添加排序条件
        lqw.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
        List<Dish> dishList = dishMapper.selectList(lqw);
        return dishList;
    }

​ 经过测试,我们可以看到它已经可以根据不同的菜品分类来显示菜品信息。

套餐信息中菜品数据的显示

6、套餐信息的保存

​ 这块的套餐保存和我们前面的新增菜品信息时是一样的,都是对两张表进行操作,并且都是关联表中缺少一个id,因此我们模仿之前菜品信息新增的业务来写就可以了。

Controller层

/**
 * <p>
 * 套餐 前端控制器
 * </p>
 *
 * @author ChiBaoBaoHuaiShuShu
 * @since 2023-05-28
 */
@RestController
@RequestMapping("/setmeal")
@Slf4j
public class SetmealController {

    @Autowired
    private ISetmealService setmealService;

    @Autowired
    private ISetmealDishService setmealDishService;

    /**
     * 添加套餐信息
     * 操作两张表,setmeal_dish和setmeal表
     * @param setmealDto
     * @return
     */
    @PostMapping
    public R<String> saver(@RequestBody SetmealDto setmealDto) {
        log.info("新增套餐信息:{}",setmealDto);
        setmealService.saverWithDish(setmealDto);

        return R.success("添加套餐成功");
    }

}

Service接口

/**
 * <p>
 * 套餐 服务类
 * </p>
 *
 * @author ChiBaoBaoHuaiShuShu
 * @since 2023-05-28
 */
public interface ISetmealService extends IService<Setmeal> {

    /**
     * 添加套餐信息
     * 操作两张表,setmeal_dish和setmeal表
     * @param setmealDto
     * @return
     */
    void saverWithDish(SetmealDto setmealDto);
}

Service实现类

/**
 * <p>
 * 套餐 服务实现类
 * </p>
 *
 * @author ChiBaoBaoHuaiShuShu
 * @since 2023-05-28
 */
@Service
public class SetmealServiceImpl extends ServiceImpl<SetmealMapper, Setmeal> implements ISetmealService {

    @Autowired
    private SetmealMapper setmealMapper;

    @Autowired
    private ISetmealDishService setmealDishService;

    /**
     * 添加套餐信息
     * 操作两张表,setmeal_dish和setmeal表
     * @param setmealDto
     * @return
     */
    @Override
    @Transactional
    public void saverWithDish(SetmealDto setmealDto) {
        //保存基本的套餐信息setmeal
        setmealMapper.insert(setmealDto);
        //SetmealDishes中缺少setmealId,需要为其赋值
        Long setmealId = setmealDto.getId();
        for (SetmealDish setmealDish : setmealDto.getSetmealDishes()) {
            setmealDish.setSetmealId(String.valueOf(setmealId));
        }
        //保存套餐中所对于的菜品信息setmeal_dish
        setmealDishService.saveBatch(setmealDto.getSetmealDishes());

    }
}

功能测试

​ 填写好测试信息后,点击保存,可以看到url请求已经响应成功了。

新增套餐url相应成功

​ 查看数据库也可以看到,套餐的信息确实保存了,以及套餐对于的菜品信息也是。

新增套餐数据库保存成功1

新增套餐数据库保存成功2

套餐信息分页查询

需求分析

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

套餐分页查询的页面

代码开发-梳理交互过程

在开发代码之前,需要梳理一下套餐分页查询时前端页面和服务端的交互过程:

  1. 页面(backend/page/combo/list.html)发送ajax请求,将分页查询参数(page、pageSizename)提交到服务端,获取分页数据
  2. 页面发送请求,请求服务端进行图片下载,用于页面图片展示

​ 开发套餐信息分页查询功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。

​ 这块的代码开发和前面的菜品信息的分页查询是一样的,所以整体的业务逻辑也是一样的,所以我们仿照之前的菜品信息分页查询的模块,来自己尝试的来完成这块代码会比较好。

Controller层

/**
     * 套餐信息分页查询
     * @param page
     * @param pageSize
     * @param name
     * @return
     */
    @GetMapping("/page")
    public R<Page<SetmealDto>> page(Integer page,Integer pageSize,String name) {
        Page<SetmealDto> setmealDtoPage = setmealService.page(page, pageSize, name);
        log.info("菜品信息分页查询:{}",setmealDtoPage);
        return R.success(setmealDtoPage);
    }

Service接口

/**
     * 菜品信息分页查询
     * @param page
     * @param pageSize
     * @param name
     * @return
     */
    Page<SetmealDto> page(Integer page, Integer pageSize, String name);

Service实现类

/**
     * 菜品信息分页查询
     * @param page
     * @param pageSize
     * @param name
     * @return
     */
    @Override
    public Page<SetmealDto> page(Integer page, Integer pageSize, String name) {
        //构造Setmeal的分页构造器
        Page<Setmeal> setmealPage = new Page<>();
        LambdaQueryWrapper<Setmeal> lqw = new LambdaQueryWrapper<>();
        lqw.like(StringUtils.isNoneEmpty(name), Setmeal::getName, name);
        lqw.orderByDesc(Setmeal::getUpdateTime);
        setmealMapper.selectPage(setmealPage, lqw);

        //构建SetmealDto的分页构造器
        Page<SetmealDto> setmealDtoPage = new Page<>();
        //对象拷贝,但不拷贝“records”
        BeanUtils.copyProperties(setmealPage,setmealDtoPage,"records");

        List<Setmeal> records = setmealPage.getRecords();
        List<SetmealDto> setmealDtoList = records.stream().map(item -> {
            SetmealDto setmealDto = new SetmealDto();
            //对象拷贝
            BeanUtils.copyProperties(item, setmealDto);
            Long categoryId = item.getCategoryId();
            Category category = categoryService.getById(categoryId);
            //此处是因为当我们自己添加或通过sql文件导入菜品时并没有设置分类,所有可能是会为空的
            if (category != null) {
                String categoryName = category.getName();
                setmealDto.setCategoryName(categoryName);
            }
            return setmealDto;
        }).collect(Collectors.toList());
        setmealDtoPage.setRecords(setmealDtoList);
        return setmealDtoPage;
    }

​ 这块Service实现类的方法不理解的小伙伴,这时候应该停下来,去前面我们编写的菜品信息分页查询的模块,复习一下,那边我写有较为详细的逻辑思路。

功能测试

​ 断点我就不跑了,这边的效果还是和前面菜品信息分页查询时的一样,不理解的小伙伴可以去那个模块复习一下,感兴趣的小伙伴可以直接跑一下断点,效果应该是一样的。

套餐分页查询成功

修改套餐

需求分析

​ 在套餐管理列表页面点击修改按钮,跳转到修改套餐页面,在修改页面回显套餐相关信息并进行修改,最后点击确定按钮完成修改操作

套餐修改数据回显url1

代码开发-梳理交互过程

在开发代码之前,需要梳理一下修改套餐时前端页面 (add.html)和服务端的交互过程:

  1. 页面发送aiax请求,请求服务端获取分类数据,用于套餐分类下拉框中数据展示
  2. 页面发送ajax请求,请求服务端,根据id查询当前套餐信息,用于套餐信息回显
  3. 页面发送ajax请求,请求服务端获得菜品分类信息中所对应的菜品信息,用于套餐菜品中的添加菜品框的显示。
  4. 页面发送请求,请求服务端进行图片下载,用于页图片回显
  5. 点击保存按钮,页面发送ajax请求,将修改后的菜品相关数据以ison形式提交到服务端

​ 开发修改套餐功能,其实就是在服务端编写代码去处理前端页面发送的这5次请求即可,因为我们前面已经完成了对于套餐分类信息下拉框的业务,以及图片的上传和下载回显模块,并且我们也在套餐添加模块中完成了添加菜品框的信息回显功能。其实就想担当于已经完成了1、3、4,那么我们剩下只需要完成2和5即可。

请求2

​ 第二次请求,对于根据id来查询当前套餐信息,并回显,我们可以先看以下前端页面发送的url请求链接,从中我们可以看到其id是以请求路径的信息返回的,并不是以参数的形式,因此我们之后在Controller层中定义方法来接收的时候就需要在@GetMapping中定义参数来接收了。

套餐修改-id数据回显url

Controller层

/**
     * 根据套餐id来套餐的基本信息以及套餐所对应的菜品信息
     * @param setmealId
     * @return
     */
    @GetMapping("{setmealId}")
    public R<SetmealDto> getByIdWithDish(@PathVariable("setmealId") Long setmealId) {
        SetmealDto setmealDto = setmealService.getByIdWithDish(setmealId);
        return R.success(setmealDto);
    }

service接口

/**
     * 根据套餐id来套餐的基本信息以及套餐所对应的菜品信息
     * @param setmealId
     * @return
     */
    SetmealDto getByIdWithDish(Long setmealId);

servicea实现类

/**
     * 根据套餐id来套餐的基本信息以及套餐所对应的菜品信息
     * @param setmealId
     * @return
     */
    @Override
    public SetmealDto getByIdWithDish(Long setmealId) {
        //查询套餐的基本信息,从setmeala表中查询
        Setmeal setmeal = setmealMapper.selectById(setmealId);

        SetmealDto setmealDto = new SetmealDto();
        BeanUtils.copyProperties(setmeal, setmealDto);

        //查询当前套餐所对应的菜品信息,从setmeal_dish表查询
        LambdaQueryWrapper<SetmealDish> lqw = new LambdaQueryWrapper<>();
        lqw.eq(SetmealDish::getSetmealId, setmealId);
        List<SetmealDish> dishes = setmealDishService.list(lqw);
        setmealDto.setSetmealDishes(dishes);

        return setmealDto;
    }

​ 这块的实现类回显思路和我们前面修改菜品信息时的思路时一样的,都是先根据套餐id来查询套餐的基本信息先,然后在构建一个DTO对象,并把这些基本信息赋值给DTO对象,这样DTO对象就只差套餐所关联的菜品的集合了,那么我们在使用条件构造器来实现对其的查询获取菜品的集合并赋值给DTO对象中的集合属性即可。

请求4

​ 当我们点击保存按钮后,页面发送的请求中,我们可以看到它的请求类型为PUT,并且参数的类型和我们前面添加套餐信息的格式是一样的,因此我们这块可以参考之前新增套餐信息的方法,并且整体的业务逻辑思路是和前面的菜品信息修改是一样的。

修改套餐url

Controller层

/**
     * 修改套餐信息,操作的是两张表,还需要插入菜品信息
     * @param setmealDto
     * @return
     */
    //@PutMapping
    public R<String> update(@RequestBody SetmealDto setmealDto) {
        setmealService.updateWithDish(setmealDto);
        return R.success("修改套餐信息成功");
    }

Service接口

/**
     * 修改套餐信息,操作的是两张表,还需要插入菜品信息
     * @param setmealDto
     * @return
     */
    void updateWithDish(SetmealDto setmealDto);

Service实现类

/**
     * 修改套餐信息,操作的是两张表,还需要插入菜品信息
     * @param setmealDto
     * @return
     */
    @Override
    @Transactional
    public void updateWithDish(SetmealDto setmealDto) {
        //修改基本的套餐信息setmeal
        setmealMapper.updateById(setmealDto);

        //清理当前菜品对应口味数据--dish_flavor表的delete操作
        LambdaQueryWrapper<SetmealDish> lqw = new LambdaQueryWrapper<>();
        lqw.eq(SetmealDish::getSetmealId, setmealDto.getId());
        setmealDishService.remove(lqw);

        //SetmealDishes中缺少setmealId,需要为其赋值
        Long setmealId = setmealDto.getId();
        for (SetmealDish setmealDish : setmealDto.getSetmealDishes()) {
            setmealDish.setSetmealId(String.valueOf(setmealId));
        }
        //保存套餐中所对于的菜品信息setmeal_dish
        setmealDishService.saveBatch(setmealDto.getSetmealDishes());
    }

​ 在实现类中,因为其的功能和前面修改菜品和新增套餐类似,所以我们可以参考前面的方法,以下是我们的实现步骤:

  1. 先修改更新套餐的基本信息。

  2. 清理当前套餐对应的菜品数据。

  3. 当我们重新保存菜品信息时,遇到情况是和新增套餐时一样的,缺少套餐的id,所有循环遍历为其赋值。

  4. 重新为其添加套餐的菜品信息,以达到菜品信息修改的业务目的。

停售/起售

需求分析

​ 在套餐管理列表页面点击禁用/启用按钮,或者批量选择菜品后,来完成对于菜品状态的修改。

套餐管理页面-启售、禁售

代码开发-梳理交互过程

​ 当我们点击启用/禁用,或者批量选择后使用批量启用/禁用时,前端发送ajax给后端,后端根据前端的发送回来的情况,在具体操作,对于状态的修改有如下4种类型的ajax请求:

禁用:

  • 套餐停售url1

  • 套餐停售url2

启售:

  • 套餐启售url1

  • 套餐启售url2

​ 它们所发送的请求方式都是POST,传递的参数都是一样的,这是一个ids参数,这块有比较多种的接收方式,唯一不一样的就是禁用请求的后一个占位路径为0,而启用的则为1。

​ 因此这块我们就应该想到在Controller层的控制方法上的@POSTMapping中使用一个占位符来把它获取,之后做判断来进行是启用还是禁用操作。

整体的业务逻辑思路是和菜品信息的启用/禁用是一样的。

Controller层

/**
     * 根据传递回来的status来判断是启用还是修改操作
     * 之后根据id来批量删除修改
     * @param status
     * @param ids
     * @return
     */
    @PostMapping("/status/{status}")
    public R<String> updateStatusById(@PathVariable("status") Integer status, String ids) {
        //应为参数ids为String类型的参数,并且里面的id用 , 隔开,因此我们需要将其分割出来
        String[] split = ids.split(",");
        setmealService.updateStatusById(status, split);
        return R.success("修改状态成功");
    }

Service接口

/**
     * 根据传递回来的status来判断是启用还是修改操作
     * 之后根据id来批量修改
     * @param status
     * @param ids
     * @return
     */
    void updateStatusById(Integer status, String[] split);

Service实现类

/**
     * 根据传递回来的status来判断是启用还是修改操作
     * 之后根据id来批量修改
     * @param status
     * @param ids
     * @return
     */
    @Override
    @Transactional
    public void updateStatusById(Integer status, String[] ids) {
        //禁售业务操作
        if (status == 0) {
            for (String id : ids) {
                setmealMapper.updateStatusById(0, Long.valueOf(id));
            }
        }
        //起售业务操作
        if (status == 1) {
            for (String id : ids) {
                setmealMapper.updateStatusById(1, Long.valueOf(id));
            }
        }
    }

mapper接口

/**
     * 根据id修改套餐状态信息
     * @param status
     * @param id
     * @return
     */
    int updateStatusById(@Param("status") Integer status, @Param("id") Long id);

mapper.xml

<mapper namespace="com.huaishushu.mapper.SetmealMapper">

    <update id="updateStatusById">
        update setmeal
        set status = #{status,jdbcType=NUMERIC}
        where id = #{id,jdbcType=NUMERIC}
    </update>
</mapper>

​ 整体逻辑比较简单,我没有用MP我根据会比较麻烦,所以就使用了编写sql的方式来,这样反而很块。

删除套餐

需求分析

​ 在套餐管理列表页面点击删除按钮,可以删除对应的套餐信息。也可以通过复选框选择多个套餐,点击批量删除按钮一次删除多个套餐。注意,对于状态为售卖中的套餐不能删除,需要先停售,然后才能删除。

套餐删除需求分析

代码开发-梳理交互过程

在开发代码之前,需要梳理一下删除套餐时前端页面和服务端的交互过程

  1. 删除单个套餐时,页面发送aiax请求,根据套餐id删除对应套餐

    套餐删除url1

  2. 删除多个套餐时,页面发送ajax请求,根据提交的多个套餐id删除对应套餐

    套餐删除url2

​ 开发删除套餐功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。

​ 观察删除单个套餐和批量删除套餐的请求信息可以发现,两种请求的地址和请求方式都是相同的,不同的则是传递的id个数,所以在服务端可以提供一个方法来统一处理。

​ 其实这个模块的业务功能和前面的菜品信息的删除功能是一样的,整体流程业务逻辑基本相同的,因此我们可以仿照菜品信息删除的代码逻辑来完成这个模块的功能。

Controller层

/**
     * 根据id字符串来批量删除套餐信息
     * @param ids
     * @return
     */
    @DeleteMapping
    public R<String> delete(String ids) {
        log.info("删除的套餐id为:{}",ids);
        String[] split = ids.split(",");
        setmealService.delete(split);
        return R.success("删除套餐成功");
    }

Service接口

/**
     * 根据id数组来批量删除套餐信息
     * @param ids
     */
    void delete(String[] ids);

Service实现类

/**
     * 根据id数组来批量删除套餐信息
     * @param ids
     */
    @Override
    @Transactional
    public void delete(String[] ids) {
        for (String id : ids) {
            //判断当前套餐是否在售卖,如果在售卖则抛出一个业务异常
            Setmeal setmeal = setmealMapper.selectById(id);
            if (setmeal.getStatus() == 1) {
                throw new CustomException("套餐正在售卖,无法删除,售卖的套餐名称为:" + setmeal.getName());
            }
            //删除套餐的基本信息
            setmealMapper.deleteById(id);

            //删除套餐关联的菜品信息
            LambdaQueryWrapper<SetmealDish> lqw = new LambdaQueryWrapper<>();
            lqw.eq(SetmealDish::getSetmealId, id);
            setmealDishService.remove(lqw);
        }
    }

​ 整体的思路就是获取ids参数,并遍历id然后进行业务判断,当前套餐是否正在售卖,如果在售卖则抛出异常,如果没有售卖则进行删除。

可能遇到的问题

乱码问题

​ 有的小伙伴在项目开发过程中会遇到乱码问题,这个情况我们只需要在yml文件中添加字符编码的设置即可(UTF-8)

server:
  port: 80
  servlet:
    # 实现编码统一
    encoding:
      charset: utf-8
      enabled: true
      force: true

bean循环内嵌

​ 在这处业务开发中,有的小伙伴可能会遇到以下的报错提示,这里的报错信息显示:

	不鼓励依赖循环引用,默认情况下禁止使用循环引用。更新应用程序以删除 Bean 之间的依赖循环。作为最后的手段,可以通过将spring.main.allow-circular-references设置为true来自动打破循环。


解决方法:

​ 前面的报错信息也提示我们了,所以我们在yml文件中以下属性即可

spring:
	main:
		allow-circular-references: true