外卖平台及移动端开发(后台)

一、项目背景介绍

技术选型

img

功能架构

img

img

二、开发环境搭建

1、数据库环境搭建

  • 创建数据库
序号 表名 说明
1 category 菜品和套餐分类表
2 dish 菜品表
3 dish_flavor 菜品口味关系表
4 setmeal 套餐表
5 setmeal_dish 套餐菜品关系表
6 employee 员工表
7
8
9
10
11

2、maven项目搭建

2.1 创建maven项目

  • 检查项目文件编码(UTF-8)
  • 设置maven仓库
  • jdk配置(1.8)

2.2 导入pom文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<?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 http://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.4.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.itreggie</groupId>
<artifactId>reggie_take_out</artifactId>
<version>1.0-SNAPSHOT</version>

<properties><java.version>1.8</java.version></properties>

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

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

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

<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>

<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.23</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.4.5</version>
</plugin>
</plugins>
</build>
</project>

2.3 创建application.xml文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
server:
port: 8090
spring:
application:
name: reggie_take_out
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/reggie?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
username: root
password:
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

reggie:
path: D:\MyCode\reggie_take_out\img\

2.4 创建boot程序入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.itreggie.reggie;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Slf4j
@SpringBootApplication
@ServletComponentScan
@EnableTransactionManagement
public class ReggieApplication {
public static void main(String[] args) {
SpringApplication.run(ReggieApplication.class,args);
log.info("项目启动成功。。。");
}
}

3、导入前端文件

3.1 默认方式

​ 在Boot项目中,前台默认就只能访问 resource目录下的static和template文件夹下的文件;所以如果要使用这种方式,直接创建一个static目录就行,然后把这些前端资源放在这个static目录下就行;

3.2 自定义MVC支持

  1. 创建config文件包
  2. 创建WebMvcConfig;

记得在启动程序ReggieApplication加上@ServletComponentScan这个注解,否则这个配置类不会生效;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.itreggie.reggie.config;

import com.itreggie.reggie.common.JacksonObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

import java.util.List;

@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
/**
* 设置静态资源映射
* @param registry
*/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
log.info("开始静态资源映射。。。");
registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
}
}

三、后台登陆功能开发–employee

1、需求分析

​ 需求分析是通过产品原型来进行的,由产品经理负责。

2、代码开发

​ 前端页面访问地址:http://localhost:8090/backend/page/login/login.html

2.1 创建相关的包

img

2.2 实体类entity和Mapper

  1. 在entity内创建employee的实体类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.itreggie.reggie.entity;

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;

/**
* 员工实体类
*/
@Data
public class Employee implements Serializable {

private static final long serialVersionUID = 1L;

private Long id;

private String username;

private String name;

private String password;

private String phone;

private String sex;

private String idNumber;//身份证号码

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;

}

  1. 在Mapper中使用mybatis_plus生成employeeMapper
1
2
3
4
5
6
7
8
9
package com.itreggie.reggie.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itreggie.reggie.entity.Employee;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface EmpolyeeMapper extends BaseMapper<Employee> {
}

2.3 service

  1. 定义employeeService接口

    1
    2
    3
    4
    5
    6
    7
    package com.itreggie.reggie.service;

    import com.baomidou.mybatisplus.extension.service.IService;
    import com.itreggie.reggie.entity.Employee;

    public interface EmployeeService extends IService<Employee> {
    }
  2. 定义employeeServiceImpl实现类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    package com.itreggie.reggie.service.impl;

    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import com.itreggie.reggie.entity.Employee;
    import com.itreggie.reggie.mapper.EmpolyeeMapper;
    import com.itreggie.reggie.service.EmployeeService;
    import org.springframework.stereotype.Service;

    @Service
    public class EmployeeServiceImpl extends ServiceImpl<EmpolyeeMapper, Employee> implements EmployeeService {
    }

2.4 返回结果类R

​ 将共同使用的类存放进common包中,将返回结果类R放入common包中进行封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.itreggie.reggie.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;
}

}

2.5 controller

  • 处理业务逻辑

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

  • 编码处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package com.itreggie.reggie.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.itreggie.reggie.common.R;
import com.itreggie.reggie.entity.Employee;
import com.itreggie.reggie.service.EmployeeService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration;
import org.springframework.util.DigestUtils;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;

@Slf4j
@RestController
@RequestMapping("/employee")
public class EmployeeController {

@Autowired
private EmployeeService employeeService;

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

//1、将页面提交的密码password进行md5加密
String password = employee.getPassword();
password = DigestUtils.md5DigestAsHex(password.getBytes());

//2、根据页面提交的用户名username查询数据库
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Employee::getUsername,employee.getUsername());
Employee emp = employeeService.getOne(queryWrapper);

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

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

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

//6、登陆成功,将员工id存入Session并返回登陆成功结果
request.getSession().setAttribute("employee",emp.getId());
return R.success(emp);
}

3、功能测试

  • 使用debug启动项目
  • 浏览器F12打开管理员模式查看请求情况
  • 查看“响应”的“local storage”中,“key”和“value”是否正常显示了“userInfo”和employee表中信息

四、后台系统退出功能–employee

1、处理业务逻辑

  1. 在controller中创建对应的处理方法来接受前端的请求,请求方式为post;
  2. 清理session中的用户id
  3. 返回结果(前端页面会进行跳转到登录页面)
  4. 前端浏览器清除数据

2、编码处理

​ 后端:在employeeController中编写

1
2
3
4
5
6
7
8
9
/**
* 退出登陆
* @return
*/
@PostMapping("/logout")
public R<String> logout(HttpServletRequest request){
request.getSession().removeAttribute("employee");
return R.success("退出成功");
}

​ 前端:在”/backend/index.html”中编写方法

1
2
3
4
5
6
7
8
logout() {
logoutApi().then((res)=>{
if(res.code === 1){
localStorage.removeItem('userInfo')
window.location.href = '/backend/page/login/login.html'
}
})
},

3、功能测试

​ 尝试登陆再退出;

​ 查看浏览器中的数据是否会被清除(F12)。

五、员工管理模块–employee

1、完善登陆功能

  1. 处理业务逻辑:使用过滤器或者是拦截器,在拦截器或者是过滤器中判断用户是否已经完成了登陆,如果没有登陆则跳转到登陆页面

  2. 代码实现

    ①在Filter包中创建自定义过滤器LongCheckFilter

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    package com.itreggie.reggie.filter;

    import com.alibaba.fastjson.JSON;
    import com.itreggie.reggie.common.BaseContext;
    import com.itreggie.reggie.common.R;
    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;

    /**
    * 检查用户是否已经完成登录
    */
    @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 chain) throws IOException, ServletException {
    HttpServletRequest request = (HttpServletRequest) servletRequest;
    HttpServletResponse response = (HttpServletResponse) servletResponse;
    //1、获取本次请求的URI
    String requestURI = request.getRequestURI();

    log.info("拦截到请求:{}", requestURI);

    //1.1定义不需要请求的路径
    String[] urls = new String[]{
    "/employee/login",
    "/employee/logout",
    "/backend/**",
    "/front/**",
    "/common/**"
    };
    //2、判断本次请求是否需要处理
    boolean check = check(urls, requestURI);

    //3、如果不需要处理,则直接放行
    if (check) {
    log.info("本次请求{}不需要处理", requestURI);
    chain.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);

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

    log.info("用户未登陆");
    //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) {
    boolean match = PATH_MATCHER.match(url, requestURI);
    if (match) {
    return true;
    }
    }
    return false;
    }
    }

    ②在启动类加上注解@ServletComponentScan

image-20220905154241178

​ ③完善过滤器的处理逻辑

img

2、新增员工

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

2.1 处理业务逻辑

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

2.2 编码处理

​ 在employeeController中编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    /**
* 新增员工
* @param employee
* @return
*/
@PostMapping
public R<String> save(HttpServletRequest request,@RequestBody Employee employee){
log.info("新增员工,员工信息:{}",employee.toString());

//设置初始密码123456,需要进行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 R.success("新增员工成功");
}

2.3 功能测试

​ 登陆之后,点击添加,然后确认,然后去数据库看一下新增数据成功没,新增成功,那就表示代码可以执行;

2.4 补充–全局异常捕获

注意:因为我们把username设置为唯一索引,所以下次再新增用户的时候,就会从MySQL数据库抛出来一个异常;

2.4.1 解决方法:

img

2.4.2 编码处理

全局异常捕获GlobalExceptionHandler写在common包下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.itreggie.reggie.common;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import java.sql.SQLIntegrityConstraintViolationException;

/**
* 全局异常处理器
*/
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j
public 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("未知错误");
}

}
2.4.3 功能测试

​ 登陆后,添加一个一个已经存在账号名,看前端页面提示的是什么信息,以及看后台是否输出了报错日志;

img

2.4.4 总结

img

3、员工信息分页查询

3.1 处理业务逻辑

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

3.2 编码处理

  • 在config中配置Mybatis_plus的分页插件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    package com.itreggie.reggie.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;

    /**
    * 配置MP的分页插件
    */
    @Configuration
    public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
    MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
    mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
    return mybatisPlusInterceptor;
    }
    }

  • 在employeeController中编写

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    /**
    * 员工信息分页查询
    * @param page
    * @param pageSize
    * @param name
    * @return
    */
    @GetMapping("/page")
    public R<Page> page(int page,int pageSize,String name){
    log.info("page = {},pageSize = {},name = {}",page,pageSize,name);

    //构造分页构造器
    Page pageInfo = new Page(page,pageSize);

    //构造条件构造器
    LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper();
    //添加过滤条件
    queryWrapper.like(StringUtils.isNotEmpty(name),Employee::getName,name);
    //添加排序条件
    queryWrapper.orderByDesc(Employee::getUpdateTime);

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

    return R.success(pageInfo);
    }

3.3 功能测试

​ 分页的三个时机,①用户登录成功时,分页查询一次 ②用户使用条件查询的时候分页一次 ③跳转页面的时候分页查询一次

4、员工账号信息修改(禁用/启用)

4.1 需求分析

  • 只有管理员(admin用户)才可以对其他普通用户进行启用操作,禁用操作;
  • 如果某个员工账号的状态为正常,则按钮显示为’‘禁用’,如果员工账号状态为已禁用,则按钮显示为“启用”;
  • 普通员工登录系统后,启用,禁用按钮不显示。

4.2 处理业务逻辑

      1. 页面发送ajax请求,将参数(id,status)提交到服务端
      2. 服务端controller接收页面提交的数据并调用service更新数据
      3. service调用Mapper操作数据库

4.3 编码处理

​ 前端代码:实现**”管理员(admin用户)才可以对其他普通用户进行启用操作,禁用操作”**

img

注意:这里修改状态码要反着来,因为正常的用户你只能把它设置为禁用;已经禁用的账号你只能把它设置为正常

​ 后端代码:在employeeController中创建update方法,此方法是一个通用的修改员工信息的方法,因为status是employee中的一个属性;这里使用了动态SQL的功能,根据具体的数据修改对应的字段信息;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  /**
* 根据id修改员工信息
* @param employee
* @return
*/
@PutMapping
public R<String> update(HttpServletRequest request,@RequestBody Employee employee){
log.info(employee.toString());

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

Long empId = (Long) request.getSession().getAttribute("employee");
employee.setUpdateTime(LocalDateTime.now());
employee.setUpdateUser(empId);
employeeService.updateById(employee);

return R.success("员工信息修改成功");
}

4.4 功能测试

​ 测试的时候发现问题,修改员工的状态时,提示信息显示修改成功,但是数据库查验证的时候,发现员工的状态码没有变化。原因是:mybatis-plus对id使用了雪花算法,所以存入数据库中的id是19为长度,但是前端的js只能保证数据的前16位的数据的精度,对我们id后面三位数据进行了四舍五入,所以就出现了精度丢失;就会出现前端传过来的id和数据里面的id不匹配,就没办法正确的修改到我们想要的数据。
解决bug的方法:

- 关闭mybatis-plus的雪花算法来处理ID
- 使用自增ID的策略来往数据库添加ID (√)

4.5 补充–使用自定义消息转换器防止精度丢失

4.5.1 需求分析

既然js对long型的数据会进行精度丢失,那么我们就对数据进行转型,我们可以在服务端(Java端)给页面响应json格式的数据时进行处理,将long型的数据统一转换为string字符串;

4.5.2 处理业务逻辑

img

4.5.3 编码处理

1、在common包中自定义消息转换类JacksonObjectMapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package com.itreggie.reggie.common;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;

/**
* 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
* 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
* 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
*/
public class JacksonObjectMapper extends ObjectMapper {

public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";

public JacksonObjectMapper() {
super();
//收到未知属性时不报异常
this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);

//反序列化时,属性不存在的兼容处理
this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);


SimpleModule simpleModule = new SimpleModule()
.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))

.addSerializer(BigInteger.class, ToStringSerializer.instance)
.addSerializer(Long.class, ToStringSerializer.instance)
.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));

//注册功能模块 例如,可以添加自定义序列化器和反序列化器
this.registerModule(simpleModule);
}
}

2、 在WebMvcConfig 配置类中扩展springmvc的消息转换器,在消息转换器中使用spring提供的对象转换器进行Java对象到json数据的转换;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.itreggie.reggie.config;

import com.itreggie.reggie.common.JacksonObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

import java.util.List;

@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
/**
* 设置静态资源映射
* @param registry
*/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
log.info("开始静态资源映射。。。");
registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
}

/**
* 扩展MVC框架的消息转换器
* @param converters the list of configured converters to extend
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器。。。");
//创建消息转换器对象
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();

//设置对象转换器,底层使用jackson将java对象转为json
messageConverter.setObjectMapper(new JacksonObjectMapper());

//将上面的消息转换器对象追加到MVC框架的转换器集合中
converters.add(0,messageConverter);
}
}

4.5.4 功能测试

​ 启动程序,使用f12查看服务器响应到浏览器的用户id是不是变成了字符串,和数据库中是否相对应。发现对应,即消息转换器配置成功;再去测试启用与禁用员工账号这个功能,检查数据库修改情况。

5、编辑员工信息

5.1 处理业务逻辑

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

5.2 编码处理

​ 后端代码:在employeeController中实现getById方法实现数据回显

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 根据id查询员工信息
* @param id
* @return
*/
@GetMapping("/{id}")
public R<Employee> getById(@PathVariable Long id){
log.info("查询员工信息。。。");
Employee employee = employeeService.getById(id);
if (employee != null){
return R.success(employee);
}
return R.error("没有查询到对应员工信息");
}

​ 修改回显数据后,点击保存,会发送一个update的请求给后端,前面我们已经写了这个update的controller,所以只需要在前端跳转发请求就行;这样就实现了方法的复用,减少了代码量

5.3 功能测试

​ 测试编辑,看能不能数据回显,可不可以修改成功,修改后数据库的数据有没有跟着变化。

5.4 补充–公共字段填充

5.4.1 需求分析

img

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

​ 将相关的注解加在需要Mybatis_plus自动填充的字段上

1
2
3
4
5
6
7
8
9
10
11
@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;

​ 在common中设置处理类MyMetaObjectHandler,在此类中为公共字段赋值,并实现接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.itreggie.reggie.common;

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;

/**
* 自定义源数据对象处理器
*/
@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
/**
* 插入操作,自动填充
* @param metaObject 元对象
*/
@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());
}

/**
* 更新操作,自动填充
* @param metaObject 元对象
*/
@Override
public void updateFill(MetaObject metaObject) {
log.info("公共字段自动填充[update]...");
log.info(metaObject.toString());

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

metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("updateUser", BaseContext.getCurrentId());
}
}

5.4.4 **功能完善–ThreadLocal动态获取登陆用户id

​ 1. 需求分析:

​ 完成公共字段自动填充功能的代码开发后,仍有一个问题没有解决,就是自动填充createUser和UpdateTime时设置的用户id不是动态的。为此使用ThreadLocal来解决此问题。

  1. http请求逻辑分析

    image-20220905211255841

  2. ThreadLocal简介

    image-20220905211315278

  3. 处理业务逻辑

    • 编写BaseContext工具类,基于ThreadLocal封装的工具类
    • 在LoginCheckFilter的doFilter方法中调用BaseContext来设置当前登录用户的id
    • 在MyMetaObjectHandler的方法中调用BaseContext获取用户登录的id
    • 使用ThreadLocal动态获取并存储员工id
  4. 编码实现

    在common中定义基于ThreadLocal封装工具类BaseContext,用于保存和获取当前登录用户的id

    注意:要先把数据设置进threadLocal中,才能获取到

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    package com.itreggie.reggie.common;

    /**
    * 基于ThreadLocal封装工具类,用户保存和获取当前登陆用户id
    */
    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这个过滤器中添加有关登录用户id的代码(添加/保存)

1
2
3
4
5
6
7
8
9
10
//4、判断登陆状态,如果已登陆,则直接放行
if (request.getSession().getAttribute("employee") != null) {
log.info("用户已登陆,用户id为:{}", request.getSession().getAttribute("employee"));

Long empId = (Long) request.getSession().getAttribute("employee");
BaseContext.setCurrentId(empId);

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

​ 再将处理器MyMetaObjectHandler中的静态id改为动态获取

1
2
metaObject.setValue("createUser", BaseContext.getCurrentId());
metaObject.setValue("updateUser", BaseContext.getCurrentId());
  1. 功能测试

    通过F12查看http请求信息,看看信息能否正常传输,若能则修改成功,实现了createUser和createTime的动态更新及存储。

六、菜品分类管理–category

1、新增分类

1.1 处理业务逻辑

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

1.2 编码实现

1.2.1 数据库表category结构

img

1.2.2 创建Mapper
1
2
3
4
5
6
7
8
9
10
package com.itreggie.reggie.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itreggie.reggie.entity.Category;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface CategoryMapper extends BaseMapper<Category> {
}

1.2.3 创建service
1
2
3
4
5
6
7
8
9
package com.itreggie.reggie.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.itreggie.reggie.entity.Category;

public interface CategoryService extends IService<Category> {

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.itreggie.reggie.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itreggie.reggie.common.CustomException;
import com.itreggie.reggie.entity.Category;
import com.itreggie.reggie.entity.Dish;
import com.itreggie.reggie.entity.Setmeal;
import com.itreggie.reggie.mapper.CategoryMapper;
import com.itreggie.reggie.service.CategoryService;
import com.itreggie.reggie.service.DishService;
import com.itreggie.reggie.service.SetmealService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {

}

1.2.4 编写controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.itreggie.reggie.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.itreggie.reggie.common.R;
import com.itreggie.reggie.entity.Category;
import com.itreggie.reggie.service.CategoryService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Locale;

/**
* 分类管理
*/
@RestController
@RequestMapping("/category")
@Slf4j
public class CategoryController {
@Autowired
private CategoryService categoryService;

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

1.3 功能测试

​ 登录后,点击添加新增菜品分类,看是否成功,数据库的数据是否变化。


2、菜品类分页查询

2.1 处理业务逻辑

1. 页面发送ajax请求,将分页查询参数(page,pageSize)提交到服务端
1. 服务端controller接收页面提交的数据并调用service查询数据
1. service调用Mapper 操作数据库,查询分页数据
1. controller将查询到的分页数据响应给页面
1. 页面接收到分页数据并通过elementUI的table组件展示到页面上

2.2 编码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 分页查询
* @param page
* @param pageSize
* @return
*/
@GetMapping("/page")
public R<Page> page(int page,int pageSize){
//分页构造器
Page<Category> pageInfo = new Page<>(page,pageSize);

//条件构造器
LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
//添加排序条件,根据sort进行排序
queryWrapper.orderByAsc(Category::getSort);

//进行分页查询
categoryService.page(pageInfo,queryWrapper);

return R.success(pageInfo);
}

2.3 功能测试

​ 刷新页面,重新登录,查看页面是否展示数据库信息。

3、删除分类

需要删除的数据可能与其他表关联,所以删除之前要先判断该数据是否与其他表中的数据关联;

3.1 编码实现

1
2
3
4
5
6
7
8
9
10
/**
* 根据id来删除分类的数据
* @param id
* @return
*/
@DeleteMapping()
public R<String> delete(@RequestParam("ids") Long ids){ //注意这里前端传过来的数据是ids
categoryService.removeById(ids);
return R.success("分类信息删除成功");
}

3.2 功能完善

img

3.2.1 创建实体类dish和setmeal的Mapper

​ 1、在entity中创建实体类dish和setmeal。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package com.itreggie.reggie.entity;

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
菜品
*/
@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;

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package com.itreggie.reggie.entity;

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
* 套餐
*/
@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;
}

​ 2、创建Mapper

1
2
3
4
5
6
7
8
9
10
package com.itreggie.reggie.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itreggie.reggie.entity.Dish;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface DishMapper extends BaseMapper<Dish> {
}

1
2
3
4
5
6
7
8
9
10
package com.itreggie.reggie.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itreggie.reggie.entity.Setmeal;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface SetmealMapper extends BaseMapper<Setmeal> {
}

3.2.2 创建service及实现类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.itreggie.reggie.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.itreggie.reggie.dto.DishDto;
import com.itreggie.reggie.entity.Dish;

import java.util.List;

public interface DishService extends IService<Dish> {
//新增菜品,同时插入菜品对应的口味数据,需要操作两张表:dish,dish_flavor
public void saveWithFlavor(DishDto dishDto);

//根据id查询菜品信息和对应的口味信息
public DishDto getByIdWithFlavor(Long id);

//更新菜品信息,同时更新口味信息
public void updateWithFlavor(DishDto dishDto);

//实现菜品的批量删除
public void deleteByIds(List<Long> ids);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.itreggie.reggie.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.itreggie.reggie.dto.SetmealDto;
import com.itreggie.reggie.entity.Setmeal;

import java.util.List;

public interface SetmealService extends IService<Setmeal> {
/**
* 新增套餐,同时保存菜品和套餐的关联关系
* @param setmealDto
*/
public void saveWithDish(SetmealDto setmealDto);

/**
* 删除套餐,同时删除套餐和菜品的关联数据
* @param ids
*/
public void removeWithDish(List<Long> ids);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.itreggie.reggie.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itreggie.reggie.common.CustomException;
import com.itreggie.reggie.dto.DishDto;
import com.itreggie.reggie.entity.Dish;
import com.itreggie.reggie.entity.DishFlavor;
import com.itreggie.reggie.mapper.DishMapper;
import com.itreggie.reggie.service.DishFlavorService;
import com.itreggie.reggie.service.DishService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.stream.Collectors;

@Service
@Slf4j
public class DishServiceImpl extends ServiceImpl<DishMapper, Dish> implements DishService {

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.itreggie.reggie.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itreggie.reggie.common.CustomException;
import com.itreggie.reggie.dto.SetmealDto;
import com.itreggie.reggie.entity.Setmeal;
import com.itreggie.reggie.entity.SetmealDish;
import com.itreggie.reggie.mapper.SetmealMapper;
import com.itreggie.reggie.service.SetmealDishService;
import com.itreggie.reggie.service.SetmealService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.stream.Collectors;

@Service
@Slf4j
public class SetmealServiceImpl extends ServiceImpl<SetmealMapper, Setmeal> implements SetmealService {

}

3.2.3 添加自定义的service方法

​ 在categoryService接口中添加,并重写实现类方法;

1
public void remove(Long id);

​ 由于增加了判定(菜品是否有关联),所以要自定义业务异常,在common中创建CustomException

1
2
3
4
5
6
7
8
9
10
11
package com.itreggie.reggie.common;

/**
* 自定义业务异常
*/
public class CustomException extends RuntimeException{
public CustomException(String message){
super(message);
}
}

​ 然后在外面前面写的GlobalExceptionHandler全局异常捕获器中添加该异常,这样就可以把相关的异常信息显示给前端操作的人员看见;

1
2
3
4
5
6
7
8
9
10
/**
* 异常处理方法
* @return
*/
@ExceptionHandler(CustomException.class)
public R<String> exceptionHandler(CustomException ex){
log.error(ex.getMessage());//报错记得打日志
//这里拿到的message是业务类抛出的异常信息,我们把它显示到前端
return R.error(ex.getMessage());
}

​ 在categoryServiceImpl中实现添加了关联判定的remove方法;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package com.itreggie.reggie.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itreggie.reggie.common.CustomException;
import com.itreggie.reggie.entity.Category;
import com.itreggie.reggie.entity.Dish;
import com.itreggie.reggie.entity.Setmeal;
import com.itreggie.reggie.mapper.CategoryMapper;
import com.itreggie.reggie.service.CategoryService;
import com.itreggie.reggie.service.DishService;
import com.itreggie.reggie.service.SetmealService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {
@Autowired
private DishService dishService;
@Autowired
private SetmealService setmealService;

/**
* 根据id删除分类,删除之前需要进行判断
* @param id
*/
@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<>();
setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId,id);
int count2 = setmealService.count(setmealLambdaQueryWrapper);
//查询当前分类是否关联套餐,如果已经关联,抛出业务异常
if (count2 > 0){
//说明已经关联套餐,抛出业务异常
throw new CustomException("当前分类项关联了套餐,删除失败");
}

//正常删除分类
super.removeById(id);
}
}

3.2.4 调用remove方法

​ 最后在categoryController中调用刚刚实现的remove方法,将之前的removeById方法取代。

1
2
3
4
5
6
7
8
9
10
11
12
13
    /**
* 根据id删除分类
* @param ids
* @return
*/
@DeleteMapping
public R<String> delete(Long ids){
log.info("删除分类,id为:{}",ids);

// categoryService.removeById(ids);
categoryService.remove(ids);
return R.success("分页信息删除成功");
}

3.3 功能测试

​ 自行添加测试数据测试就行;记得一定要测试一下删除有相关联的数据,看会不会删除和在前端提示异常信息。

4、修改分类

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

前端已经完成了编辑的数据回显,这样可以减少对数据库的操作,所以我们就不需要去数据库查询数据了。

img

4.1 编码实现

​ 在categoryController中实现update方法

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 根据id修改分类信息
* @param category
* @return
*/
@PutMapping
public R<String> update(@RequestBody Category category){
log.info("修改分类信息:{}",category);

categoryService.updateById(category);

return R.success("修改分类信息成功");
}

​ 记得在category实体类中加上公共字段的值设置(公共字段填充),前面已经配置好了。

image-20220906122809632

七、菜品管理的业务功能–dish&dish_flavor

1、*文件的上传和下载–commonController

1.1 整体介绍

​ 文件上传

img

img

img

​ 文件下载

img

1.2 前端代码实现

img

img

1.3 后端代码实现

​ 在application.yml文件中配置上传图片的储存位置

1
2
reggie:
path: D:\MyCode\reggie_take_out\img\

​ 在controller包下创建commonController类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
package com.itreggie.reggie.controller;

import com.itreggie.reggie.common.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.UUID;

/**
* 文件上传和下载
*/
@RestController
@RequestMapping("/common")
@Slf4j
public class CommonController {
@Value("${reggie.path}")
private String basePath;

/**
* 文件上传
* @param file
* @return
*/
@PostMapping("/upload")
public R<String> upload(MultipartFile file){
//file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会自动消失
log.info(file.toString());

//原始文件名
String originalFilename = file.getOriginalFilename();
//截取文件词根eg:.jpg
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));

//使用UUID重新生成文件名,防止文件名称重复造成文件覆盖
String fileName = UUID.randomUUID().toString() + suffix;

//创建一个目录对象
File dir = new File(basePath);
//判断当前目录是否存在
if (!dir.exists()){
//目录不存在,创建一个目录
dir.mkdirs();
}

try {
file.transferTo(new File(basePath+fileName));
} catch (IOException e) {
throw new RuntimeException(e);
}
return R.success(fileName);
}

/**
* 文件下载
* @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) {
e.printStackTrace();
}

}
}

​ 注意:这里上传的文件的文件名要和这个地方的一样,接收文件的参数的名不能随便定义,要和下面的name的值一致

img

2、新增菜品

2.1 数据模型

img

img

2.2 *处理业务逻辑

    1. 页面(backend/page/food/add.html)发送ajax请求,请求服务端获取菜品分类数据并展示到下拉框
    1. 页面发送请求进行图片上传,请求服务端将图片保存到服务器
    1. 页面发送请求进行图片下载,将上传的图片进行回显
    1. 点击保存按钮,发送ajax请求,将菜品相关数据以json形式提交到服务端

2.3 编码处理一

2.3.1 创建相关的Mapper和service层
1
2
3
4
5
6
7
8
9
10
package com.itreggie.reggie.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itreggie.reggie.entity.DishFlavor;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface DishFlavorMapper extends BaseMapper<DishFlavor> {
}

1
2
3
4
5
6
7
8
package com.itreggie.reggie.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.itreggie.reggie.entity.DishFlavor;

public interface DishFlavorService extends IService<DishFlavor> {
}

1
2
3
4
5
6
7
8
9
10
11
12
package com.itreggie.reggie.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itreggie.reggie.entity.DishFlavor;
import com.itreggie.reggie.mapper.DishFlavorMapper;
import com.itreggie.reggie.service.DishFlavorService;
import org.springframework.stereotype.Service;

@Service
public class DishFlavorServiceImpl extends ServiceImpl<DishFlavorMapper, DishFlavor> implements DishFlavorService {
}

2.3.2 获取和返回菜品分类列表–CategoryController

​ 前端的主要代码为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 获取菜品分类列表
const getCategoryList = (params) => {
return $axios({
url: '/category/list',
method: 'get',
params
})
}

if (res.code === 1) {
this.dishList = res.data //这里就相当于把所有的category对象的数据赋值给dishList
}

这是菜品分类和数据双向绑定的前端代码: 我们返回的是一个集合,
</el-form-item>
<el-form-item
label="菜品分类:"
prop="categoryId"
>
<el-select
v-model="ruleForm.categoryId"
placeholder="请选择菜品分类"
>
<el-option v-for="(item,index) in dishList" :key="index" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>

​ 在CategoryController中书写后端查询代码,不过这里的返回值和参数接收值可能和自己想的有点不一样。。。这个的返回值和参数值 值得多思考一下; 这里之所以返回list集合,是因为这个要展示的数据是引用类型的数据集,集合可以存放任意类型的数据;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 根据条件查询分类数据
* @param category
* @return
*/
@GetMapping("/list")
//这个接口接收到参数其实就是一个前端传过来的type,这里之所以使用Category这个类来接受前端的数据,是为了以后方便
//因为这个Category类里面包含了type这个数据,返回的数据多了,你自己用啥取啥就行
public R<List<Category>> list(Category category){
//条件构造器
LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper();
//添加条件
queryWrapper.eq(category.getType() != null,Category::getType,category.getType());
//添加排序条件
queryWrapper.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime);

List<Category> list = categoryService.list(queryWrapper);

return R.success(list);
}
2.3.3 功能测试一

​ 在浏览器中通过F12检查返回数据,确认页面数据能正常输送。

img

2.4 编码处理二

2.4.1 接收页面提交的数据

​ 点击保存按钮的时候,把前端的json数据提交到后台,后台接收数据,对数据进行处理;要与两张表打交道,一个是dish一个是dish_flavor表;

先用前端页面向后端发一次请求,看看前端具体的请求是什么,我们好写controller;然后再看前端提交携带的参数是什么,我们好选择用什么类型的数据来接收!!!

​ 从上图传出来的data数据可以看出参数比较复杂,可以使用两种方式进行封装:

    - 创建与这些数据对应的实体类(dto)【√】
    - 使用map进行接收
2.4.2 **创建DishDto实体类封装参数

img

​ 创建dto包,并创建DishDto实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.itreggie.reggie.dto;

import com.itreggie.reggie.entity.Dish;
import com.itreggie.reggie.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;
}

​ 通过DishDto中的flavors参数封装数据

img

2.4.3 实现菜品新增功能–dishController
后端代码

​ 在DishService中新增方法saveWithFlavor:

1
2
//新增菜品,同时插入菜品对应的口味数据,需要操作两张表:dish,dish_flavor
public void saveWithFlavor(DishDto dishDto);

​ 在DishServiceImpl中实现方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Autowired
private DishFlavorService dishFlavorService;

//新增菜品,同时保存对应的口味数据
@Override
@Transactional//涉及到对多张表的数据进行操作,需要加事务,需要事务生效,需要在启动类加上事务注解生效
public void saveWithFlavor(DishDto dishDto) {
//保存菜品基本信息到菜品表dish
this.save(dishDto);

Long dishId = dishDto.getId();//菜品id

List<DishFlavor> flavors = dishDto.getFlavors();//菜品口味
flavors = flavors.stream().map((item)->{//对集合进行赋值
item.setDishId(dishId);
return item;
}).collect(Collectors.toList());

//保存菜品口味到菜品口味表dish_flavor
dishFlavorService.saveBatch(flavors);
}

​ 在启动类ReggieApplication中开启事务: 加上这个注解 @EnableTransactionManagement(上面已经完成)

​ 在controller包中创建DishController并实现新增菜品的save方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.itreggie.reggie.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.itreggie.reggie.common.R;
import com.itreggie.reggie.dto.DishDto;
import com.itreggie.reggie.entity.Category;
import com.itreggie.reggie.entity.Dish;
import com.itreggie.reggie.entity.DishFlavor;
import com.itreggie.reggie.service.CategoryService;
import com.itreggie.reggie.service.DishFlavorService;
import com.itreggie.reggie.service.DishService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/dish")
@Slf4j
public class DishController {
@Autowired
private CategoryService categoryService;
@Autowired
private DishService dishService;
@Autowired
private DishFlavorService dishFlavorService;

@PostMapping
public R<String> save(@RequestBody DishDto dishDto){
log.info(dishDto.toString());

dishService.saveWithFlavor(dishDto);

return R.success("新增菜品成功");
}

}

前端代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<el-button
type="primary"
@click="submitForm('ruleForm')"
>
保存
</el-button>

let params = {...this.ruleForm}
// params.flavors = this.dishFlavors
params.status = this.ruleForm ? 1 : 0
params.price *= 100 //存到数据库的时候是以分为单位,所以这里x100
params.categoryId = this.ruleForm.categoryId
params.flavors = this.dishFlavors.map(obj => ({ ...obj, value: JSON.stringify(obj.value) }))


if (this.actionType == 'add') {
delete params.id
addDish(params).then(res => {
if (res.code === 1) {
this.$message.success('菜品添加成功!')
if (!st) {
this.goBack()
} else { ....


// 新增接口
const addDish = (params) => {
return $axios({
url: '/dish',
method: 'post',
data: { ...params }
})
}
2.4.4 功能测试二

​ 点击保存按钮,查看页面是否弹出’’菜品添加成功’’提示框,数据库表数据是否有更新修改。

3、菜品信息分页查询

3.1 处理业务逻辑

​ 交互流程梳理

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

​ 开发菜品信息分页查询功能,实际在服务端处理前端页面发送的这2次请求即可。

3.2 编码处理

图片下载的请求前面已经写好了,前端也写好了相关的请求,所以只需要处理第一个请求;

3.2.1 DishController中实现page方法(需完善)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 菜品信息分页查询
* @param page
* @param pageSize
* @param name
* @return
*/
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name){

//构造一个分页构造器对象
Page<Dish> dishPage = new Page<>(page,pageSize);

//构造一个条件构造器
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
//添加过滤条件 注意判断是否为空 使用对name的模糊查询
queryWrapper.like(name != null,Dish::getName,name);
//添加排序条件 根据更新时间降序排
queryWrapper.orderByDesc(Dish::getUpdateTime);
//去数据库处理分页 和 查询
dishService.page(dishPage,queryWrapper);

//因为上面处理的数据没有分类的id,这样直接返回R.success(dishPage)虽然不会报错,但是前端展示的时候这个菜品分类这一数据就为空
return R.success(dishPage);
}

3.3 *功能完善

​ 通过引入DishDto,完善分页查询代码,解决菜品分类数据为空这一问题

​ DishDto代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.itreggie.reggie.dto;

import com.itreggie.reggie.entity.Dish;
import com.itreggie.reggie.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;
}

​ 改进page方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/**
* 菜品信息分页查询
* @param page
* @param pageSize
* @param name
* @return
*/
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name){

//构造一个分页构造器对象
Page<Dish> dishPage = new Page<>(page,pageSize);
Page<DishDto> dishDtoPage = new Page<>(page,pageSize);
//上面对dish泛型的数据已经赋值了,这里对DishDto我们可以把之前的数据拷贝过来进行赋值

//构造一个条件构造器
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
//添加过滤条件 注意判断是否为空 使用对name的模糊查询
queryWrapper.like(name != null,Dish::getName,name);
//添加排序条件 根据更新时间降序排
queryWrapper.orderByDesc(Dish::getUpdateTime);
//去数据库处理分页 和 查询
dishService.page(dishPage,queryWrapper);

//获取到dish的所有数据 records属性是分页插件中表示分页中所有的数据的一个集合
List<Dish> records = dishPage.getRecords();

List<DishDto> list = records.stream().map((item) ->{
//对实体类DishDto进行categoryName的设值

DishDto dishDto = new DishDto();
//这里的item相当于Dish 对dishDto进行除categoryName属性的拷贝
BeanUtils.copyProperties(item,dishDto);
//获取分类的id
Long categoryId = item.getCategoryId();
//通过分类id获取分类对象
Category category = categoryService.getById(categoryId);
if ( category != null){
//设置实体类DishDto的categoryName属性值
String categoryName = category.getName();
dishDto.setCategoryName(categoryName);
}
return dishDto;
}).collect(Collectors.toList());

//对象拷贝 使用框架自带的工具类,第三个参数是不拷贝到属性
BeanUtils.copyProperties(dishPage,dishDtoPage,"records");
dishDtoPage.setRecords(list);
//因为上面处理的数据没有分类的id,这样直接返回R.success(dishPage)虽然不会报错,但是前端展示的时候这个菜品分类这一数据就为空
//所以进行了上面的一系列操作
return R.success(dishDtoPage);
}

​ records的值: protected List records,实现:

img

3.4 功能测试

​ 刷新页面,重新登录,点击菜品分类按钮,呈现页面实现:

img

4、修改菜品

4.1 处理业务流程

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

开发修改菜品功能,实际在服务端处理以上前端页面发送的4次请求即可。其中,第一次交互的后端代码已经完成;菜品分类的信息做新增菜品的时候已经完成,这里前端发一个相关接口的请求就行;第三次交互图片的下载前面也已经完成,所以前端直接发生请求就行;所以只需要处理第二次和第四次请求

4.2 编码处理

4.2.1 实现菜品信息的回显

​ 在DishService层添加菜品信息回显的getWithFlavor方法:

1
2
//根据id查询菜品信息和对应的口味信息
public DishDto getByIdWithFlavor(Long id);

​ 在Impl中实现方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 根据id查询菜品信息和对应的口味信息
* @param id
* @return
*/
public DishDto getByIdWithFlavor(Long id) {
//查询菜品基本信息,从dish表查询
Dish dish = this.getById(id);

DishDto dishDto = new DishDto();
//对象拷贝
BeanUtils.copyProperties(dish,dishDto);

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

return dishDto;
}

​ DishController层的实现:

1
2
3
4
5
6
7
8
9
10
/**
* 根据id来查询菜品信息和对应的口味信息
* @param id
* @return
*/
@GetMapping("/{id}")
public R<DishDto> get(@PathVariable Long id){ //这里返回什么数据是要看前端需要什么数据,不能直接想当然的就返回Dish对象
DishDto dishDto = dishService.getByIdWithFlavor(id);
return R.success(dishDto);
}
4.2.2 **保存修改

​ 在DishService中添加保存修改设计两张表的数据的updateWithFlavor方法

1
2
//更新菜品信息,同时更新口味信息
public void updateWithFlavor(DishDto dishDto);

​ impl中的实现方法为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 修改数据库菜品信息
* @param dishDto
*/
@Override
@Transactional
public void updateWithFlavor(DishDto dishDto) {
//更新dish表基本信息
this.updateById(dishDto);

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

dishFlavorService.remove(queryWrapper);

//添加当前提交过来的口味数据--dish_flavor表的insert操作
List<DishFlavor> flavors = dishDto.getFlavors();

flavors = flavors.stream().map((item)->{
item.setDishId(dishDto.getId());
return item;
}).collect(Collectors.toList());

dishFlavorService.saveBatch(flavors);
}

4.3 功能测试

​ 点击页面修改按钮,修改数据后点击保存,查看页面数据回显情况

5、菜品的启售和停售

5.1 编码处理

​ 前端发过来的请求(使用的是post方式):http://localhost:8090/dish/status/1?ids=

​ 后端在DishController中通过status方法接收前端请求并进行修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 对菜品进行停售或者是起售
* @return
*/
@PostMapping("/status/{status}")
public R<String> status(@PathVariable("status") Integer status,Long ids){
//检查后端能否接收到前端的数据
log.info("status:{}",status);
log.info("ids:{}",ids);

Dish dish = dishService.getById(ids);
if (dish != null){
dish.setStatus(status);
dishService.updateById(dish);
return R.success("开始启售");
}
return R.error("售卖状态设置异常");
}

6、菜品的批量启售和批量停售

6.1 编码处理

​ 把上面对单个菜品的售卖状态的方法进行修改;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 对菜品批量或者是单个 进行停售或者是起售
* @return
*/
@PostMapping("/status/{status}")
//这个参数这里一定记得加注解才能获取到参数,否则这里非常容易出问题
public R<String> status(@PathVariable("status") Integer status,@RequestParam List<Long> ids){
//log.info("status:{}",status);
//log.info("ids:{}",ids);
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper();
queryWrapper.in(ids !=null,Dish::getId,ids);
//根据数据进行批量查询
List<Dish> list = dishService.list(queryWrapper);

//对单个菜品的代码进行修改,添加一个遍历条件
for (Dish dish : list) {
if (dish != null){
dish.setStatus(status);
dishService.updateById(dish);
}
}
return R.success("售卖状态修改成功");
}

注意:controller层的代码是不可以直接写业务的,建议把它抽离到service层,controller调用一下service的方法就行;

7、删除菜品(批量/单个)

7.1 编码处理

​ 在DishFlavor实体类中,在private Integer isDeleted;字段上加上@TableLogic注解,表示删除是逻辑删除,由mybatis-plus提供的;

1
2
3
@TableLogic
//是否删除
private Integer isDeleted;

​ 在DishService中添加相关的方法并在Impl中实现方法

1
public void deleteByIds(List<Long> ids);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 删除菜品
* @param ids
*/
@Override
@Transactional
public void deleteByIds(List<Long> ids) {
//构造条件查询器
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();

//添加判断,判断菜品是否已停售,未停售抛出业务异常
queryWrapper.in(ids != null, Dish::getId,ids);
List<Dish> list = this.list(queryWrapper);

for (Dish dish : list){
Integer status = dish.getStatus();
//如果不是在售卖,则可以删除
if (status == 0){
this.removeById(dish.getId());
}else {
throw new CustomException("删除菜品中有菜品未下架,删除失败");
}
}
}

​ 再在DishController中实现delete方法,调用deleteById方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 菜品删除
* @param ids
* @return
*/
@DeleteMapping
public R<String> delete(@RequestParam List<Long> ids){
//删除菜品
dishService.deleteByIds(ids);
//删除菜品口味关联信息
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(DishFlavor::getDishId,ids);

dishFlavorService.remove(queryWrapper);

return R.success("菜品删除成功");
}

7.2 代码优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
* 菜品批量删除和单个删除
* 1.判断要删除的菜品在不在售卖的套餐中,如果在那不能删除
* 2.要先判断要删除的菜品是否在售卖,如果在售卖也不能删除
* @return
*/

//遇到一个小问题,添加菜品后,然后再添加套餐,但是套餐可选择添加的菜品选项是没有刚刚添加的菜品的?
//原因:redis存储的数据没有过期,不知道为什么redis没有重新刷新缓存
// (与DishController中的@GetMapping("/list")中的缓存设置有关,目前不知道咋配置刷新缓存。。。。。
// 解决方案,把redis中的数据手动的重新加载一遍,或者是等待缓存过期后再添加相关的套餐,或者改造成使用spring catch
@DeleteMapping
public R<String> delete(@RequestParam("ids") List<Long> ids){
//根据菜品id在stemeal_dish表中查出哪些套餐包含该菜品
LambdaQueryWrapper<SetmealDish> setmealDishLambdaQueryWrapper = new LambdaQueryWrapper<>();
setmealDishLambdaQueryWrapper.in(SetmealDish::getDishId,ids);
List<SetmealDish> SetmealDishList = setmealDishService.list(setmealDishLambdaQueryWrapper);
//如果菜品没有关联套餐,直接删除就行 其实下面这个逻辑可以抽离出来,这里我就不抽离了
if (SetmealDishList.size() == 0){
//这个deleteByIds中已经做了菜品起售不能删除的判断
dishService.deleteByIds(ids);
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(DishFlavor::getDishId,ids);
dishFlavorService.remove(queryWrapper);
return R.success("菜品删除成功");
}

//如果菜品有关联套餐,并且该套餐正在售卖,那么不能删除
//得到与删除菜品关联的套餐id
ArrayList<Long> Setmeal_idList = new ArrayList<>();
for (SetmealDish setmealDish : SetmealDishList) {
Long setmealId = setmealDish.getSetmealId();
Setmeal_idList.add(setmealId);
}
//查询出与删除菜品相关联的套餐
LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
setmealLambdaQueryWrapper.in(Setmeal::getId,Setmeal_idList);
List<Setmeal> setmealList = setmealService.list(setmealLambdaQueryWrapper);
//对拿到的所有套餐进行遍历,然后拿到套餐的售卖状态,如果有套餐正在售卖那么删除失败
for (Setmeal setmeal : setmealList) {
Integer status = setmeal.getStatus();
if (status == 1){
return R.error("删除的菜品中有关联在售套餐,删除失败!");
}
}

//要删除的菜品关联的套餐没有在售,可以删除
//这下面的代码并不一定会执行,因为如果前面的for循环中出现status == 1,那么下面的代码就不会再执行
dishService.deleteByIds(ids);
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(DishFlavor::getDishId,ids);
dishFlavorService.remove(queryWrapper);
return R.success("菜品删除成功");
}

八、套餐管理–setmeal&setmeal_dish

1、数据模型

img

img

2、新增套餐

2.1 处理业务逻辑

 1. 页面(backend/page/combo/add.html)发送ajax请求,请求服务端**获取套餐分类**数据并展示到下拉框中
 2. 页面发送ajax请求,请求服务端获取**菜品分类**数据并展示到添加菜品窗口中
 3. 页面发送ajax请求,请求服务器,根据菜品分类查询对应的**菜品**数据并展示到添加菜品窗口中
 4. 页面发送请求进行**图片上传**,请求服务端将图片保存到服务器
 5. 页面发送请求进行**图片下载**,将上传的图像进行回显
 6. 点击保存按钮,发送ajax请求,将**套餐**相关数据以json形式提交到服务端

第一个交互前面已经完成:分类管理通过type的值来控制在前端展示的是 菜品分类(type=1) 或者是 套餐分类(type=2);

第二个交互在categorycontroller里面的list方法;

第四和第五个交互也已经实现,则还剩第三和第六个交互需要处理。

2.2 编码处理

2.2.1 创建类和接口的基本结构

​ 创建setmealDishMapper:

1
2
3
4
5
6
7
8
9
10
package com.itreggie.reggie.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.itreggie.reggie.entity.SetmealDish;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface SetmealDishMapper extends BaseMapper<SetmealDish> {
}

​ 创建setmealDishService:

1
2
3
4
5
6
7
8
package com.itreggie.reggie.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.itreggie.reggie.entity.SetmealDish;

public interface SetmealDishService extends IService<SetmealDish> {
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.itreggie.reggie.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itreggie.reggie.entity.SetmealDish;
import com.itreggie.reggie.mapper.SetmealDishMapper;
import com.itreggie.reggie.service.SetmealDishService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class SetmealDishServiceImpl extends ServiceImpl<SetmealDishMapper, SetmealDish> implements SetmealDishService {

}

2.2.2 添加菜品数据回显(交互三)–DishController

​ 由前端请求的地址可知菜品分类通过categoryID查询

img

​ 在DishController中实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 根据条件查询对应的菜品数据
* @param dish
* @return
*/
@GetMapping("/list")
public R<List<Dish>> list(Dish dish){ //会自动映射的
//这里可以传categoryId,但是为了代码通用性更强,这里直接使用dish类来接受(因为dish里面是有categoryId的),以后传dish的其他属性这里也可以使用
//构造查询条件
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(dish.getCategoryId() != null ,Dish::getCategoryId,dish.getCategoryId());
//添加条件,查询状态为1(起售状态)的菜品
queryWrapper.eq(Dish::getStatus,1);

//添加排序条件
queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);

List<Dish> list = dishService.list(queryWrapper);
return R.success(list);
}

​ 控制台输出的sql语句:

1
SELECT id,name,category_id,price,code,image,description,status,sort,create_time,update_time,create_user,update_user,is_deleted FROM dish WHERE (category_id = ? AND status = ?) ORDER BY sort ASC,update_time DESC
2.2.3 保存添加套餐(交互六)–SetmealController

根据前端传过来的数据我们可以在后端确定我们需要在后端使用什么来接受前端的参数;

img

img

​ 创建SetmealController控制类,上面的dishList,我们数据库并不需要这个数据,所以接收数据的实体类没有dishList这个属性也没有关系,前端传过来的数据都是自动映射到接收数据的实体类的属性上的,没有对应起来就不会映射。

​ 在SetmealController控制类中实现save方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package com.itreggie.reggie.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.itreggie.reggie.common.R;
import com.itreggie.reggie.dto.SetmealDto;
import com.itreggie.reggie.entity.Category;
import com.itreggie.reggie.entity.Setmeal;
import com.itreggie.reggie.entity.SetmealDish;
import com.itreggie.reggie.service.CategoryService;
import com.itreggie.reggie.service.SetmealDishService;
import com.itreggie.reggie.service.SetmealService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.stream.Collectors;

/**
* 套餐管理
*/
@RestController
@RequestMapping("/setmeal")
@Slf4j
public class SetmealController {
@Autowired
private SetmealService setmealService;
@Autowired
private SetmealDishService setmealDishService;
@Autowired
private CategoryService categoryService;

/**
* 新增套餐
* @param setmealDto
* @return
*/
@PostMapping
public R<String> save(@RequestBody SetmealDto setmealDto){
log.info("套餐信息:{}",setmealDto);

setmealService.saveWithDish(setmealDto);

return R.success("新增套餐成功");
}

}

​ 在SetmealService中添加自定义的方法并在Impl中实现:

1
2
3
4
5
/**
* 新增套餐,同时保存菜品和套餐的关联关系
* @param setmealDto
*/
public void saveWithDish(SetmealDto setmealDto);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Autowired
SetmealDishService setmealDishService;

/**
* 新增套餐,同时需要保存套餐和菜品的关联关系
* @param setmealDto
*/
@Transactional
@Override
public void saveWithDish(SetmealDto setmealDto) {
//保存套餐的基本信息,操作setmeal,执行insert
this.save(setmealDto);
log.info(setmealDto.toString()); //查看一下这个套餐的基本信息是什么

//保存套餐和菜品的关联信息,操作setmeal_dish ,执行insert操作
List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();

//注意上面拿到的setmealDishes是没有setmeanlId这个的值的,通过debug可以发现
setmealDishes.stream().map((item)->{
item.setSetmealId(setmealDto.getId());
return item; //这里返回的就是集合的泛型
}).collect(Collectors.toList());

setmealDishService.saveBatch(setmealDishes); //批量保存
}

3、套餐信息分页查询

3.1 处理业务逻辑

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

3.2 编码处理

​ 在SetmealController中实现page方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
* 套餐分页查询
* @param page
* @param pageSize
* @param name
* @return
*/
@GetMapping("/page")
public R<Page> page(int page, int pageSize, String name){

//分页构造器对象
Page<Setmeal> pageInfo = new Page<>(page,pageSize);
Page<SetmealDto> dtoPage = new Page<>(page,pageSize);

//构造条件查询对象
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
//添加查询条件,根据name进行like模糊查询
queryWrapper.like(name != null,Setmeal::getName,name);
//添加排序条件,根据更新时间降序排列
queryWrapper.orderByDesc(Setmeal::getUpdateTime);
setmealService.page(pageInfo,queryWrapper);

//对象的拷贝 注意这里要把分页数据的全集合records给忽略掉
BeanUtils.copyProperties(pageInfo,dtoPage,"records");
List<Setmeal> records = pageInfo.getRecords();

//对records对象进行处理然后封装好赋值给list
List<SetmealDto> list = records.stream().map((item)->{
SetmealDto setmealDto = new SetmealDto();

//对setmealDto进行除categoryName的属性进行拷贝(因为item里面没有categoryName)
BeanUtils.copyProperties(item,setmealDto);

//获取分类id 通过分类id获取分类对象 然后再通过分类对象获取分类名
Long categoryId = item.getCategoryId();

//根据分类id获取分类对象 判断是否为null
Category category = categoryService.getById(categoryId);

if (category != null){
String categoryName = category.getName();
setmealDto.setCategoryName(categoryName);
}
return setmealDto;
}).collect(Collectors.toList());
dtoPage.setRecords(list);

return R.success(dtoPage);
}

4、套餐的启售和停售(批量/单个)

​ 根据前面菜品模块自己实现的功能,我们可以知道,我们只需要写一个批量处理的方法就可以完成单个或者是批量套餐的启售,停售;

4.1 编码处理

4.1.1 SetmealService中添加updateSetmealStatusByIds方法并在Impl中实现
1
2
3
4
5
6
/**
* 根据套餐id修改售卖状态
* @param status
* @param ids
*/
void updateSetmealStatusById(Integer status,List<Long> ids);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 根据套餐id修改售卖状态
* @param status
* @param ids
*/
@Override
public void updateSetmealStatusById(Integer status, List<Long> ids) {
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper();
queryWrapper.in(ids !=null,Setmeal::getId,ids);
List<Setmeal> list = this.list(queryWrapper);

for (Setmeal setmeal : list) {
if (setmeal != null){
setmeal.setStatus(status);
this.updateById(setmeal);
}
}
}
4.1.2 SetmealController中实现status代码
1
2
3
4
5
6
7
8
9
10
/**
* 对菜品批量或者是单个 进行停售或者是起售
* @return
*/
@PostMapping("/status/{status}")
//这个参数这里一定记得加注解才能获取到参数,否则这里非常容易出问题
public R<String> status(@PathVariable("status") Integer status,@RequestParam List<Long> ids){
setmealService.updateSetmealStatusById(status,ids);
return R.success("售卖状态修改成功");
}

5、套餐信息修改

5.1 处理业务逻辑

1. 将页面的数据传到后端,在数据库中进行修改
1. 回显数据到页面

5.2 编码处理

5.2.1 setmealService添加getDate方法并在Impl中实现
1
2
3
4
5
/**
* 回显套餐数据:根据套餐id查询套餐
* @return
*/
SetmealDto getDate(Long id);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 回显套餐数据:根据套餐id查询套餐
* @return
*/
@Override
public SetmealDto getDate(Long id) {
Setmeal setmeal = this.getById(id);
SetmealDto setmealDto = new SetmealDto();
LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper();
//在关联表中查询,setmealdish
queryWrapper.eq(id!=null,SetmealDish::getSetmealId,id);

if (setmeal != null){
BeanUtils.copyProperties(setmeal,setmealDto);
List<SetmealDish> list = setmealDishService.list(queryWrapper);
setmealDto.setSetmealDishes(list);
return setmealDto;
}
return null;
}
5.2.2 SetmealController 中添加getDate方法实现数据回显
1
2
3
4
5
6
7
8
9
10
/**
* 回显套餐数据:根据套餐id查询套餐
* @return
*/
@GetMapping("/{id}")
public R<SetmealDto> getData(@PathVariable Long id){
SetmealDto setmealDto = setmealService.getDate(id);

return R.success(setmealDto);
}
5.2.3 完善已选菜品中的菜品并没有展示对应的菜品名丢失问题

​ 修改具体的前端代码:把backend/page/combo/add.html中的335行修改为下面的代码;

img

​ 出现菜品名丢失的原因是这里的item是代表dish对象,dish实体类是使用name作为菜品名称的

img

5.2.4 保存数据返回套餐页面

​ 根据前端传过来的数据和需要的返回值,我们就可以知道controller层方法的返回值和用什么参数来接收前端传给我们的数据;注意这个套餐里面的菜品也要保存修改:需要把setealDish保存到seteal_dish表中;

​ 为了不把问题复杂化,直接把相关的setmealDish内容移除然后再重新添加,这样就可以不用考虑dish重复的问题和存在修改遗漏的问题。

​ 在setmealController中实现方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@PutMapping
public R<String> edit(@RequestBody SetmealDto setmealDto){

if (setmealDto==null){
return R.error("请求异常");
}

if (setmealDto.getSetmealDishes()==null){
return R.error("套餐没有菜品,请添加套餐");
}
List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
Long setmealId = setmealDto.getId();

LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(SetmealDish::getSetmealId,setmealId);
setmealDishService.remove(queryWrapper);

//为setmeal_dish表填充相关的属性
for (SetmealDish setmealDish : setmealDishes) {
setmealDish.setSetmealId(setmealId);
}
//批量把setmealDish保存到setmeal_dish表
setmealDishService.saveBatch(setmealDishes);
setmealService.updateById(setmealDto);

return R.success("套餐修改成功");
}

6、删除套餐

6.1 处理业务逻辑

img

6.2 编码处理

​ 注意:对于正在售卖中的套餐不能删除,需要先停售再删除。

6.2.1 在setmealService中添加removeWithDish方法并在Impl中实现
1
2
3
4
5
/**
* 删除套餐,同时删除套餐和菜品的关联数据
* @param ids
*/
public void removeWithDish(List<Long> ids);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 删除套餐,同时删除套餐和菜品的关联数据
* @param ids
*/
@Transactional
public void removeWithDish(List<Long> ids) {
//查询套餐状态,确定是否可以删除
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(Setmeal::getId,ids);
queryWrapper.eq(Setmeal::getStatus,1);

int count = this.count(queryWrapper);
if (count > 0){
//如果不能删除,抛出业务异常
throw new CustomException("套餐正在售卖中,删除失败");
}

//如果可以删除,先删除套餐表中的数据--setmeal
this.removeByIds(ids);

//删除关系表中的数据--setmeal_dish
LambdaQueryWrapper<SetmealDish> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.in(SetmealDish::getSetmealId,ids);

setmealDishService.remove(lambdaQueryWrapper);
}
6.2 2 setmealController层调用removeWithDish方法
1
2
3
4
5
6
7
8
9
10
11
12
/**
* 删除套餐
* @param ids
* @return
*/
@DeleteMapping
public R<String> delete(@RequestParam List<Long> ids){
log.info("ids: {}",ids);

setmealService.removeWithDish(ids);
return R.success("套餐数据删除成功");
}

九、后台订单管理

1、按条件查看和展示客户订单

​ 实现移动端下单功能后,后台应实现客户订单的数据展示

1.1 编码处理

​ 在orderController中实现page方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 后台查询订单明细
* @param page
* @param pageSize
* @param number
* @param beginTime
* @param endTime
* @return
*/
@GetMapping("/page")
public R<Page> page(int page, int pageSize, String number,String beginTime,String endTime){
//分页构造器对象
Page<Orders> pageInfo = new Page<>(page,pageSize);
//构造条件查询对象
LambdaQueryWrapper<Orders> queryWrapper = new LambdaQueryWrapper<>();

//添加查询条件 动态sql 字符串使用StringUtils.isNotEmpty这个方法来判断
//这里使用了范围查询的动态SQL,这里是重点!!!
queryWrapper.like(number!=null,Orders::getNumber,number)
.gt(StringUtils.isNotEmpty(beginTime),Orders::getOrderTime,beginTime)
.lt(StringUtils.isNotEmpty(endTime),Orders::getOrderTime,endTime);

orderService.page(pageInfo,queryWrapper);
return R.success(pageInfo);
}

1.2 性能完善

​ 经过功能测试,可以看出展示页面的用户名无法展示,原因是用户名此项的前端代码绑定的是orders表中的user_name列,但用户注册时user_name并未填写,因此查询出来的用户名全为null。

手动修改数据库具体的值负担较大,所以这里使用用户下单的数据库中也有consignee来显示,但是数据库中的consignee是可以为null的,所以在后台代码中帮订单添加该属性的时候要判断是否null!然后就是去修改前端代码就行

​ 在backend/page/order/list.html中,修改第72行的userName改为consignee即可

img

2、后台订单状态修改

​ 后台订单明细中点击派送按钮:前端会发送下面的请求来:是json格式的数据;

2.1 编码处理

​ 在orderController中实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@PutMapping
public R<String> orderStatusChange(@RequestBody Map<String,String> map){

String id = map.get("id");
Long orderId = Long.parseLong(id);
Integer status = Integer.parseInt(map.get("status"));

if(orderId == null || status==null){
return R.error("传入信息不合法");
}
Orders orders = orderService.getById(orderId);
orders.setStatus(status);
orderService.updateById(orders);

return R.success("订单状态修改成功");

}