黑马瑞吉外卖项目笔记

1、环境搭建

1、数据库搭建

1、创建数据库

create database ruijiDB;

2、执行sql脚本

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jU037pHh-1659354030972)(D:\学习笔记\黑马瑞吉外卖\images\Snipaste_2022-07-30_11-30-16.png)]

3、数据表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GvSBlKXB-1659354030974)(D:\学习笔记\黑马瑞吉外卖\images\2592691-20220510123310731-63671494.png)]

2、springboot环境创建

1、导入依赖


<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.0modelVersion>
    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.7.2version>
        <relativePath/> 
    parent>
    <groupId>com.fangroupId>
    <artifactId>ruiji_take_outartifactId>
    <version>0.0.1-SNAPSHOTversion>
    <name>ruiji_take_outname>
    <description>ruiji_take_outdescription>
    <properties>
        <java.version>1.8java.version>
    properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starterartifactId>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
            <scope>compilescope>
        dependency>

        <dependency>
            <groupId>com.baomidougroupId>
            <artifactId>mybatis-plus-boot-starterartifactId>
            <version>3.4.2version>
        dependency>

        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <version>1.18.20version>
        dependency>

        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>fastjsonartifactId>
            <version>1.2.76version>
        dependency>

        <dependency>
            <groupId>commons-langgroupId>
            <artifactId>commons-langartifactId>
            <version>2.6version>
        dependency>

        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <scope>runtimescope>
        dependency>

        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>druid-spring-boot-starterartifactId>
            <version>1.1.23version>
        dependency>
    dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-maven-pluginartifactId>
            plugin>
        plugins>
    build>

project>

2、导入配置文件

server:
  port: 8080
spring:
  application:
    #应用名称 可选
    name: ruiji_take_out
    # 数据源配置
  datasource:
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/ruijiDB?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
      username: root
      password: 123456
      # mybatis—plus 配置
mybatis-plus:
  configuration:
    #在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      id-type: ASSIGN_ID

3、配置Application启动类

import lombok.extern.java.Log;
import lombok.extern.slf4j.Slf4j;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@Slf4j //lombok提供的日志注解
@SpringBootApplication
@MapperScan("com.fan.ruiji_take_out.mapper") //配置接口扫描
public class RuijiTakeOutApplication {

    public static void main(String[] args) {
        SpringApplication.run(RuijiTakeOutApplication.class, args);
        log.info("项目启动成功!");
    }

}

启动项目,访问http://localhost:8080/backend/index.html

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qUTRm94j-1659354030975)(D:\学习笔记\黑马瑞吉外卖\images\Snipaste_2022-07-30_11-37-07.png)]

访问成功!

4、使用mybatis-x插件从数据库导入实体类、mapper接口、service接口和实现类、mapper XML文件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lzdIJqag-1659354030976)(D:\学习笔记\黑马瑞吉外卖\images\Snipaste_2022-07-30_11-40-22.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tcgezt4w-1659354030976)(D:\学习笔记\黑马瑞吉外卖\images\Snipaste_2022-07-30_11-40-57.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U17YFOKE-1659354030977)(D:\学习笔记\黑马瑞吉外卖\images\Snipaste_2022-07-30_11-41-06.png)]

5、配置前端静态资源路径

创建一个config包,在包下创建WebMvcConfig类,并配置静态资源映射关系

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

/**
 * @title: WebMvcConfug
 * @Author lfan
 * @Date: 2022/7/30 9:22
 * @Version 1.0
 */
@Slf4j
@Configuration
public class WebMvcConfug extends WebMvcConfigurationSupport {
    /**
     * 设置静态资源映射
     */
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        log.info("开始静态资源映射");
        registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
        registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");

    }
}

2、后台登录功能开发

1、导入返回结果类 放在common包下



import lombok.Data;
import java.util.HashMap;
import java.util.Map;

@Data
public class Result<T> {

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

    private String msg; //错误信息

    private T data; //数据

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

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

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

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

}

2、在controller包下创建EmployeeController类,并创建登录方法

员工登录处理逻辑

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y9YKJKvj-1659354030978)(D:\学习笔记\黑马瑞吉外卖\images\2592691-20220510123427122-1922929796.png)]

package com.fan.ruiji_take_out.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.api.R;
import com.fan.ruiji_take_out.common.Result;
import com.fan.ruiji_take_out.entity.Employee;
import com.fan.ruiji_take_out.service.EmployeeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.DigestUtils;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;

/**
 * @title: EmployeeController
 * @Author lfan
 * @Date: 2022/7/30 9:57
 * @Version 1.0
 */
@Slf4j
@RestController
@RequestMapping("/employee")
public class EmployeeController {
    @Autowired
    private EmployeeService employeeService;

    /**
     * 员工登录
     *
     * @param request  用于登录成功保存数据
     * @param employee
     * @return
     */
    @PostMapping("/login")
    public Result<Employee> login(HttpServletRequest request, @RequestBody Employee employee) {

        //1、将页面提交的密码进行md5加密处理
        String password = employee.getPassword();
        password = DigestUtils.md5DigestAsHex(password.getBytes());
        //2、根据页面提交的用户名来查数据库
        QueryWrapper<Employee> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("username", employee.getUsername());
        Employee emp = employeeService.getOne(queryWrapper);

        //3、如果没有查询到则返回失败结果
        if (emp == null) {
            return Result.error("登录失败");
        }

        //4、比对密码,如果不一致则返回失败结果
        if (!emp.getPassword().equals(password)) {
            return Result.error("登录失败");
        }

        //5、查看员工状态,如果已禁用状态,则返回员工已禁用结果
        if (emp.getStatus() == 0) {
            return Result.error("账号已禁用!");
        }

        //6、登录成功,将用户id存入Session并返回成功结果

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

}

3、员工退出方法

/**
     * 员工退出
     *
     * @param request
     * @return
     */
    @PostMapping("/logout")
    public Result<String> logout(HttpServletRequest request) {
        //清除session中保存的员工id
        request.getSession().removeAttribute("employee");
        return Result.success("退出成功");
    }

    @GetMapping("/page")
  

4、配置过滤器 实现登录验证

过滤器具体的处理逻辑如下:

1、获取本次请求的URI

2、判断本次请求是否需要处理

3、如果不需要处理,则直接放行

4、判断登录状态,如果已登录,则直接放行

5、如果未登录则返回未登录结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RvOGHMVN-1659354030978)(D:\学习笔记\黑马瑞吉外卖\images\2592691-20220511151434661-434791688.png)]

在filter包下创建LoginCheckFilter类


import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.extension.api.R;
import com.fan.ruiji_take_out.common.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.AntPathMatcher;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @title: LoginCheckFilter
 * @Author lfan
 * @Date: 2022/7/30 13:58
 * @Version 1.0
 * 检查用户是否完成登录
 */
@WebFilter(filterName = "loginCheckFilter", urlPatterns = "/*")
@Slf4j
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){
            log.info("本次请求{}不需要处理",requestURI);
            filterChain.doFilter(request,response);
            return;
        }
//        4、判断登录状态,如果已登录,则直接放行
        if(request.getSession().getAttribute("employee") != null){
            log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("employee"));
            filterChain.doFilter(request,response);
            return;
        }

//        5、如果未登录则返回未登录结果,通过输出流向客户端页面响应数据
        response.getWriter().write(JSON.toJSONString(Result.error("NOTLOGIN")));


    }
    //路径匹配,检查本次请求是否需要放行
    public  boolean check(String[] urls, String requestURI){
        for (String url : urls) {
            boolean match = PATH_MATCHER.match(url, requestURI);
            if (match){
                return match;
            }
        }
        return false;
    }
}

public static final AntPathMatcher PATH_MATCHER=new AntPathMatcher();

该对象可以用来匹配通配符 比如 “/backend/**” 和 “/backend/index.html”

未登录的返回结果是根据前端的响应拦截器来设置的 ==> data.msg === ‘NOTLOGIN’

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PeTJJjwa-1659354030979)(D:\学习笔记\黑马瑞吉外卖\images\Snipaste_2022-07-30_14-41-18.png)]

3、员工管理

1、员工管理页面开发

1、新增员工

后台系统中可以管理员工信息,通过新增员工来添加后台系统用户。点击 添加员工 按钮跳转到新增页面,如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UHAi00cA-1659354030980)(D:\学习笔记\黑马瑞吉外卖\images\Snipaste_2022-07-30_15-38-37.png)]

新增员工,其实就是将我们新增页面录入的员工数据插入到employee表。需要注意,employee表中对username字段加入了唯一约束,因为username是员工的登录账号,必须是唯一

分析下该功能的执行流程

  • 页面发送ajax请求,将新增员工页面中输入的数据以json的形式提交到服务端
  • 服务端Controller接收页面提交的数据并调用Service将数据进行保存
  • Service调用Mapper操作数据库,保存数据

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DGiN8mED-1659354030980)(D:\学习笔记\黑马瑞吉外卖\images\2592691-20220511151450410-1062740598.png)]

代码编写

 /**
     * 新增员工
     * @param request
     * @param employee
     * @return
     */
    @PostMapping
    public Result<String> save(HttpServletRequest request,@RequestBody Employee employee){
        log.info("新增员工,员工信息:{}",employee.toString());
        //设置初始密码 需要进行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);
        employeeService.save(employee);
        return Result.success("新增员工成功");

    }

要注意的问题是 由于员工 username 在数据库中设置了unique 唯一约束

所以在添加员工信息时,如果添加了相同的员工 会抛出异常

java.sql.SQLIntegrityConstraintViolationException: Duplicate entry 'heniang' for key 'idx_username'

此时需要进行异常处理 有两种方法

1、在Controller方法中加入try.catch进行异常捕获

2、使用异常处理器进行全局异常捕获(推荐)

第一种需要在每个controller中进行捕获,不推荐

第二种只需要创建一个额外的类进行捕获

在common包下创建GlobalExceptionHanler类 用来处理全局异常

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import java.sql.SQLIntegrityConstraintViolationException;

/**
 * @title: GlobalExceptionHandler
 * @Author lfan
 * @Date: 2022/7/30 15:26
 * @Version 1.0
 * 全局异常处理器
 */
@ControllerAdvice(annotations = {RestController.class, Controller.class}) //指定要拦截的类 根据类上的注解来指定
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 异常处理方法
     * @param ex
     * @return
     */
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class) //指定要捕获的具体异常类
    public Result<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
        log.info(ex.getMessage());
        if (ex.getMessage().contains("Duplicate entry")){//该错误违反唯一约束
            String[] split = ex.getMessage().split(" ");
            String msg = split[2] + "已存在";
            return Result.error(msg);
        }
        return Result.error("未知错误");
    }

}

2、员工信息分页查询

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x4kKO3b6-1659354030981)(D:\学习笔记\黑马瑞吉外卖\images\2592691-20220511151459085-440937171.png)]

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5DJIENjw-1659354030982)(D:\学习笔记\黑马瑞吉外卖\images\Snipaste_2022-07-30_16-01-38.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F5RHjCl6-1659354030982)(D:\学习笔记\黑马瑞吉外卖\images\Snipaste_2022-07-30_16-02-06.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cxLFyOFe-1659354030983)(D:\学习笔记\黑马瑞吉外卖\images\Snipaste_2022-07-30_16-01-47.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VguitK6F-1659354030983)(D:\学习笔记\黑马瑞吉外卖\images\Snipaste_2022-07-30_16-02-14.png)]

代码开发

配置MP的分页插件

在config包下创建MybatisPlusConfig类

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;

/**
 * @title: MybatisPlusConfig
 * @Author lfan
 * @Date: 2022/7/30 16:07
 * @Version 1.0
 * 设置mybatis-plus的配置
 */
@Configuration
public class MybatisPlusConfig  {
    /**
     * 配置MP的分页插件
     * @return
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        //添加分页插件
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return interceptor;
    }
}

分页查询员工

/**
     * 分页查询员工
     * @param page
     * @param pageSize
     * @param name
     * @return
     */
    @GetMapping("/page")
    public Result<Page> page(int page, int pageSize, String name){
        log.info("page = {}, pageSize = {}, name = {}",page,pageSize,name);

        //创建分页对象
        Page pageInfo = new Page(page, pageSize);
        //创建wrapper对象
        QueryWrapper<Employee> queryWrapper = new QueryWrapper<>();
        //添加过滤条件
        queryWrapper.like(!StringUtils.isEmpty(name),"name",name);
        //添加排序条件
        queryWrapper.orderByDesc("create_time");

        //执行查询
        pageInfo = employeeService.page(pageInfo,queryWrapper);

        return Result.success(pageInfo);

    }

功能展示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j1ZOAL2h-1659354030984)(D:\学习笔记\黑马瑞吉外卖\images\Snipaste_2022-07-30_16-27-51.png)]

3、启用/禁用员工账号

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

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

页面中是怎么做到只有管理员admin能够看到启用、禁用按钮的?

只有管理员登录才能显示启用和禁用按钮

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UbLRBYiz-1659354030984)(D:\学习笔记\黑马瑞吉外卖\images\2592691-20220511151516295-2052598661 (1)].png)

流程分析

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

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

3、Service调用Mapper操作数据库

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BeTckPGJ-1659354030985)(D:\学习笔记\黑马瑞吉外卖\images\Snipaste_2022-07-30_16-33-22.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IdoLKoRY-1659354030986)(D:\学习笔记\黑马瑞吉外卖\images\Snipaste_2022-07-30_16-33-30.png)]

页面中的ajax请求是如何发送的?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E1LMcYWO-1659354030986)(D:\学习笔记\黑马瑞吉外卖\images\2592691-20220511151529589-250427209.png)]

代码开发

根据id修改员工信息

/**
     * 修改员工信息
     * @param request
     * @param employee
     * @return
     */
    @PutMapping
    public Result<String> update(HttpServletRequest request, @RequestBody Employee employee){
        log.info("该员工的信息为: {}",employee.toString());
        Long empId = (Long) request.getSession().getAttribute("employee");
        employee.setUpdateUser(empId);
        employee.setUpdateTime(LocalDateTime.now());

        employeeService.updateById(employee);
        return Result.success("员工信息修改成功");
    }

测试过程中没有报错,但是功能并没有实现,查看数据库中的数据也没有变化。观察控制台输出的SQL:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hujrl97D-1659354030987)(D:\学习笔记\黑马瑞吉外卖\images\2592691-20220511151540882-307959154.png)]

SQL执行的结果是更新的数据行数为0,仔细观察id的值,和数据库中对应记录的id值并不相同

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q8cf0lNz-1659354030988)(D:\学习笔记\黑马瑞吉外卖\images\2592691-20220511151551563-64038564.png)]

分页查询时服务端响应给页面的数据中id的值为19位数字,类型为long

页面中js处理long型数字只能精确到前16位,所以最终通过ajax请求提交给服务端的时候id就改变了

前面我们已经发现了问题的原因,即js对long型数据进行处理时丢失精度,导致提交的id和数据库中的id不一致。

如何解决这个问题?

我们可以在服务端给页面响应json数据时进行处理将long型数据统一转为String字符串

具体实现代码:

在common包下导入JacksonObjectMapper类



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);
    }
}

然后在 WebMvcConfig类中扩展mvc的消息转换器

此消息转换器可以将 Java对象转为json数据

 /**
     * 扩展mvc框架的消息转换器
     * @param converters
     */
    @Override
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        log.info("扩展消息转换器...");
        //创建消息转换器对象
        MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
        //设置对象转换器,底层使用Jackson将Java对象转为json
        messageConverter.setObjectMapper(new JacksonObjectMapper());
        //将上面的消息转换器对象追加到mvc框架的转换器集合中  放到索引为0的位置(最前面) 可以优先执行自己设置的转换器
        converters.add(0,messageConverter);

    }

最后测试一下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OfhtekOx-1659354030990)(D:\学习笔记\黑马瑞吉外卖\images\Snipaste_2022-08-01_10-02-11.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w9Y7riFe-1659354030991)(D:\学习笔记\黑马瑞吉外卖\images\Snipaste_2022-08-01_10-02-41.png)]

禁用成功

4、编辑员工信息

执行流程

1、点击编辑按钮时,页面跳转到add.html,并在url中携带参数[员工id]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XZkFtXoX-1659354030992)(D:\学习笔记\黑马瑞吉外卖\images\Snipaste_2022-08-01_10-26-02.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H5vuNeo1-1659354030992)(D:\学习笔记\黑马瑞吉外卖\images\Snipaste_2022-08-01_10-28-07.png)]

2、在add.html页面获取url中的参数[员工id]

3、发送ajax请求,请求服务端,同时提交员工id参数

4、服务端接收请求,根据员工id查询员工信息,将员工信息以json形式响应给页面

 /**
     * 根据id获取用户信息
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    public Result<Employee> getById(@PathVariable String id){
        log.info("根据id获取对象");
        Employee emp = employeeService.getById(id);
        if (emp != null){
            return Result.success(emp);
        }
        else {
            return Result.error("没有查询到该用户信息");
        }
    }

5、页面接收服务端响应的json数据,通过VUE的数据绑定进行员工信息回显

6、点击保存按钮,发送ajax请求,将页面中的员工信息以json方式提交给服务端

7、服务端接收员工信息,并进行处理,完成后给页面响应

8、页面接收到服务端响应信息后进行相应处理

注意:add.html页面为公共页面,新增员工和编辑员工都是在此页面操作,所以该代码部分与之前添加员工代码对应,不需要重写。

/**
     * 修改员工信息(修改员工状态,编辑员工)
     * @param request
     * @param employee
     * @return
     */
    @PutMapping
    public Result<String> update(HttpServletRequest request, @RequestBody Employee employee){
        log.info("该员工的信息为: {}",employee.toString());
        Long empId = (Long) request.getSession().getAttribute("employee");
        employee.setUpdateUser(empId);
        employee.setUpdateTime(LocalDateTime.now());

        employeeService.updateById(employee);
        return Result.success("员工信息修改成功");
    }

修改员工状态,编辑员工进入的都是update方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FsOLGn3E-1659354030993)(D:\学习笔记\黑马瑞吉外卖\images\Snipaste_2022-08-01_10-37-27.png)]

2、公共字段自动填充

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZijtGZTN-1659354030994)(D:\学习笔记\黑马瑞吉外卖\images\Snipaste_2022-08-01_15-32-41.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WNeXsj5Z-1659354030995)(D:\学习笔记\黑马瑞吉外卖\images\2592691-20220512160148850-1381544179.png)]

可以使用MP的公共字段填充功能来简化开发

1、代码实现

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

实现步骤:

1、在实体类的属性上加入@TableField注解,指定自动填充的策略

  /**
     * 创建时间
     */
    @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;

2、按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现MetaObjectHandler接口

在common包下创建MyMetaObjectHandler类


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;

/**
 * @title: MyMetaObjectHandler
 * @Author lfan
 * @Date: 2022/8/1 15:25
 * @Version 1.0
 */
@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
    //插入时自动填充
    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("公共字段自动填充【insert】。。。");
        log.info(metaObject.toString());
        metaObject.setValue("createTime", LocalDateTime.now());
        metaObject.setValue("updateTime", LocalDateTime.now());
        
        //此处为临时设置数据
        metaObject.setValue("createUser", new Long(1));
        metaObject.setValue("updateUser", new Long(1));

    }

    //更新时自动填充
    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("公共字段自动填充【update】。。。");
        log.info(metaObject.toString());
        metaObject.setValue("updateTime", LocalDateTime.now());
        
        //此处为临时设置数据
        metaObject.setValue("updateUser", new Long(1));
    }
}

2、功能完善

前面我们已经完成了公共字段自动填充功能的代码开发,但是还有一个问题没有解决,就是我们在自动填充createUser和updateUser时设置的用户id是固定值,

现在我们需要改造成动态获取当前登录用户的id。

用户登录成功后我们将用户id存入了HttpSession中,现在我从HttpSession中获取不就行了?

注意,我们在MyMetaObjectHandler类中是不能获得HttpSession对象的,所以我们需要通过其他方式来获取登录用户id。

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

在学习ThreadLocal之前,我们需要先确认一个事情,就是客户端发送的每次http请求,对应的在服务端都会分配一个新的线程来处理,在处理

过程中涉及到下面类中的方法都属于相同的一个线程:

1、LoginCheckFilter的doFilter方法

2、EmployeeContraller的update方法

3、MyMetaObjectHandler的updateFill方法

可以在上面的三个方法中分别加入下面代码(获取当前线程id):

long id = Thread.currentThread().getId() ;
log.info("线程id:{}" ,id);

执行编辑员工功能进行验证,通过观察控制台输出可以发现,一次请求对应的线程id是相同的:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SkqgFa4M-1659354030996)(D:\学习笔记\黑马瑞吉外卖\images\2592691-20220512160210276-655169945.png)]

什么是ThreadLocal?

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

ThreadLocal常用方法:

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

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

实现步骤:

1、编写BaseContext工具类,基于ThreadLocal封装的工具类


/**
 * @title: BaseContext
 * @Author lfan
 * @Date: 2022/8/1 15:50
 * @Version 1.0
 * 基于ThreadLocal封装的工具类,用于保存和获取当前登录用户的id
 */
public class BaseContext {
    private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    public static void setCurrentId(Long id){
        threadLocal.set(id);
    }

    public static Long getCurrentId(){
        return threadLocal.get();
    }
}

2、在LogincheckFilter的doFilter方法中调用BaseContext来设置当前登录用户的id

if(request.getSession().getAttribute("employee") != null){
            log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("employee"));
            //将当前用户id作为局部变量存放到当前线程中
            Long empId = (Long) request.getSession().getAttribute("employee");
            BaseContext.setCurrentId(empId);

            filterChain.doFilter(request,response);
            return;
        }
    

3、在MyMeta0bjectHandler的方法中调用BaseContext获取登录用户的id



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;

/**
 * @title: MyMetaObjectHandler
 * @Author lfan
 * @Date: 2022/8/1 15:25
 * @Version 1.0
 */
@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
    //插入时自动填充
    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("公共字段自动填充【insert】。。。");
        log.info(metaObject.toString());
        metaObject.setValue("createTime", LocalDateTime.now());
        metaObject.setValue("updateTime", LocalDateTime.now());
        
        metaObject.setValue("createUser", BaseContext.getCurrentId());
        metaObject.setValue("updateUser", BaseContext.getCurrentId());

    }

    //更新时自动填充
    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("公共字段自动填充【update】。。。");
        log.info(metaObject.toString());
        metaObject.setValue("updateTime", LocalDateTime.now());
        
        metaObject.setValue("updateUser", BaseContext.getCurrentId());


    }
}

4、分类管理

1、新增分类

·后台系统中可以管理分类信息,分类包括两种类型,分别是菜品分类和套餐分类。当我们在后台系统中添加菜品时需要选择一个菜品分类,当我们在后台系统中添加一个套餐时需要选择一个套餐分类,在移动端也会按照菜品分类和套餐分类来展示对应的菜品和套餐。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cZavG8D9-1659354030997)(D:\学习笔记\黑马瑞吉外卖\images\2592691-20220512160234249-1136711537.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pPoQ04Z4-1659354030997)(D:\学习笔记\黑马瑞吉外卖\images\2592691-20220512160238251-250847397.png)]

数据模型

新增分类,其实就是将我们新增窗口录入的分类数据插入到category表,表结构如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EGe1smIs-1659354030998)(D:\学习笔记\黑马瑞吉外卖\images\2592691-20220512160245889-1847592747.png)]

代码开发

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

1、页面(backend/page/category/list.html)发送ajax请求,将新增分类窗口输入的数据以json形式提交到服务端

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

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

可以看到新增菜品分类和新增套餐分类请求的服务端地址和提交的json数据结构相同,所以服务端只需要提供一个方法统一处理即可

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9SfYMcRV-1659354030999)(D:\学习笔记\黑马瑞吉外卖\images\2592691-20220512160259222-1818691866.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8TbSoAx3-1659354030999)(D:\学习笔记\黑马瑞吉外卖\images\2592691-20220512160302285-1976698036.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AvDIZuyX-1659354031000)(D:\学习笔记\黑马瑞吉外卖\images\2592691-20220512160307681-1150668982.png)]



import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fan.ruiji_take_out.common.Result;
import com.fan.ruiji_take_out.entity.Category;
import com.fan.ruiji_take_out.entity.Employee;
import com.fan.ruiji_take_out.service.CategoryService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;

/**
 * @title: CategoryController
 * @Author lfan
 * @Date: 2022/8/1 16:20
 * @Version 1.0
 */
@Slf4j
@RestController
@RequestMapping("/category")
public class CategoryController {
    @Autowired
    private CategoryService categoryService;

    /**
     * 新增分类
     * @param category
     * @return
     */
    @PostMapping
    public Result<String> save(@RequestBody Category category){
        log.info("category :{}",category);
        categoryService.save(category);
        return Result.success("新增分类成功");
    }

  

2、分类信息分页查询

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

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

1、页面发送ajax请求,将分页查询参数(page.pageSize)提交到服务端

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

3、Service调用Mapper操作数据库,查询分页数据

4、Controller将查询到的分页数据响应给页面

5、页面接收到分页数据并通过ElementUI的Table组件展示到页面上

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mPE0zcny-1659354031000)(D:\学习笔记\黑马瑞吉外卖\images\2592691-20220512160317974-925436217.png)]

/**
     * 分页查询分类
     * @param page
     * @param pageSize
     * @return
     */
    @GetMapping("/page")
    public Result<Page> page(int page, int pageSize){
        log.info("page = {}, pageSize = {}",page, pageSize);
        //创建分页对象
        Page pageInfo = new Page(page, pageSize);
        //创建wrapper对象
        QueryWrapper<Category> queryWrapper = new QueryWrapper<>();

        //添加排序条件
        queryWrapper.orderByDesc("sort");

        pageInfo = categoryService.page(pageInfo,queryWrapper);

        return Result.success(pageInfo);
    }

3、删除分类

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

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c2U5xF8D-1659354031001)(D:\学习笔记\黑马瑞吉外卖\images\2592691-20220512160327156-1521388521.png)]

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

3、Service调用Mapper操作数据库

  • 在CategoryService添加remove方法
public interface CategoryService extends IService<Category> {
    public void remove(Long id);
}
  • 在CategoryServicelmpl实现remove方法
@Service
public class CategoryServicelmpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {

    @Autowired
    private DishService dishService;

    @Autowired
    private SetmealService setmealService;

    @Override
    public void remove(Long id) {
        LambdaQueryWrapper<Dish> dishLambdaQueryWrapper=new LambdaQueryWrapper<>();
        //添加查询条件,根据分类id进行查询
        dishLambdaQueryWrapper.eq(Dish::getCategoryId,id);
        int count1 = dishService.count(dishLambdaQueryWrapper);

        //查询当前分类是否关联菜品,如果已经关联,抛出业务异常
        if(count1>0){
            //已经关联菜品,抛出业务异常
            throw new CustomException("已经关联菜品,不能删除");
        }

        //查询当前分类是否关联了套餐,如果已经关联,抛出业务异常
        LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper=new LambdaQueryWrapper<>();
        //添加查询条件,根据分类id进行查询
        setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId,id);
        int count2 = setmealService.count(setmealLambdaQueryWrapper);

        if(count2>0){
            //已经关联套餐,抛出业务异常
            throw new CustomException("已经关联套餐,不能删除");
        }
        //正常删除分类
        super.removeById(id);
    }
}
  • 定义异常类CustomException
public class CustomException extends RuntimeException{
    public CustomException(String message){
        super(message);
    }
}
  • 在全局异常处理器GlobalExceptionHandler添加
//进行异常处理方法
@ExceptionHandler(CustomException.class)
public R<String> exceptionHandler(CustomException ex){
    log.error(ex.getMessage());

    return R.error(ex.getMessage());
/**
     * 删除分类
     * @param ids
     * @return
     */
    @DeleteMapping
    public Result<String> delete(Long ids){
        log.info("要删除的id为 :{}",ids);
        categoryService.remove(ids);
        return Result.success("分类信息删除成功");
    }

4、修改分类

在分类管理列表页面点击修改按钮,弹出修改窗口,在修改窗口回显分类信息并进行修改,最后点击确定按钮完成修改操作

 /**
     * 修改分类
     * @param category
     * @return
     */
    @PutMapping
    public Result<String> update(@RequestBody Category category){
        log.info("该分类信息为 :{}",category);
        categoryService.updateById(category);
        return Result.success("分类修改成功");
    }

ambdaQueryWrapper=new LambdaQueryWrapper<>();
//添加查询条件,根据分类id进行查询
setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId,id);
int count2 = setmealService.count(setmealLambdaQueryWrapper);

    if(count2>0){
        //已经关联套餐,抛出业务异常
        throw new CustomException("已经关联套餐,不能删除");
    }
    //正常删除分类
    super.removeById(id);
}

}


- 定义异常类CustomException

```scala
public class CustomException extends RuntimeException{
    public CustomException(String message){
        super(message);
    }
}
  • 在全局异常处理器GlobalExceptionHandler添加
//进行异常处理方法
@ExceptionHandler(CustomException.class)
public R<String> exceptionHandler(CustomException ex){
    log.error(ex.getMessage());

    return R.error(ex.getMessage());
/**
     * 删除分类
     * @param ids
     * @return
     */
    @DeleteMapping
    public Result<String> delete(Long ids){
        log.info("要删除的id为 :{}",ids);
        categoryService.remove(ids);
        return Result.success("分类信息删除成功");
    }

4、修改分类

在分类管理列表页面点击修改按钮,弹出修改窗口,在修改窗口回显分类信息并进行修改,最后点击确定按钮完成修改操作

 /**
     * 修改分类
     * @param category
     * @return
     */
    @PutMapping
    public Result<String> update(@RequestBody Category category){
        log.info("该分类信息为 :{}",category);
        categoryService.updateById(category);
        return Result.success("分类修改成功");
    }

你可能感兴趣的:(mybatis,java,数据库)