0%

瑞吉外卖项目实战

瑞吉外卖项目实战

第一章 项目准备

开发环境搭建

数据库环境搭建

创建数据库reggie

1
create database reggie character set utf8mb4;

运行sql文件,进行表结构的快速创建。

1
source sql文件绝对目录;

image-20230216100735742

maven环境搭建

创建项目reggie_take_out,检查maven、jre、jdk

maven使用本地仓库

image-20230216102831905

maven换源

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
<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">

<pluginGroups />
<proxies />
<servers />

<!-- maven自动下载的jar包,会存放到该目录下 -->
<localRepository>D:\maven\apache-maven-3.8.6\repository</localRepository>

<mirrors>
<mirror>
<id>alimaven</id>
<mirrorOf>central</mirrorOf>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/repositories/central/</url>
</mirror>
<mirror>
<id>alimaven</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<mirrorOf>central</mirrorOf>
</mirror>
<mirror>
<id>central</id>
<name>Maven Repository Switchboard</name>
<url>http://repo1.maven.org/maven2/</url>
<mirrorOf>central</mirrorOf>
</mirror>
<mirror>
<id>repo2</id>
<mirrorOf>central</mirrorOf>
<name>Human Readable Name for this Mirror.</name>
<url>http://repo2.maven.org/maven2/</url>
</mirror>
<mirror>
<id>ibiblio</id>
<mirrorOf>central</mirrorOf>
<name>Human Readable Name for this Mirror.</name>
<url>http://mirrors.ibiblio.org/pub/mirrors/maven2/</url>
</mirror>
<mirror>
<id>jboss-public-repository-group</id>
<mirrorOf>central</mirrorOf>
<name>JBoss Public Repository Group</name>
<url>http://repository.jboss.org/nexus/content/groups/public</url>
</mirror>
<mirror>
<id>google-maven-central</id>
<name>Google Maven Central</name>
<url>https://maven-central.storage.googleapis.com
</url>
<mirrorOf>central</mirrorOf>
</mirror>
<!-- 中央仓库在中国的镜像 -->
<mirror>
<id>maven.net.cn</id>
<name>oneof the central mirrors in china</name>
<url>http://maven.net.cn/content/groups/public/</url>
<mirrorOf>central</mirrorOf>
</mirror>
</mirrors>

</settings>

编写项目配置文件

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
<?xml version="1.0" encoding="UTF-8"?>
</img/Java/17ReggieProject 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.nuaa</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./img/Java/17ReggieProjectlombok</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>

<//img/Java/17ReggieProject>

配置spring配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
server:
port: 8080
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: qqy.2520
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

编写项目启动类

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

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
* @author xzt
* @version 1.0
* springboot的启动类
*/
@Slf4j // 记录日志信息
@SpringBootApplication
public class ReggieApplication {
public static void main(String[] args) {
SpringApplication.run(ReggieApplication.class, args);
log.info("项目启动成功!");
}
}

webmvc映射配置

编写一个配置类,实现搜索路径和静态资源的映射

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
package com.nuaa.reggie.config;

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

/**
* @author xzt
* @version 1.0
* springmvc配置文件
* 搜索路径与静态资源的映射配置文件
*/
@Slf4j
@Configuration // 注解,表示这是一个配置类
public class WebMvcConfig extends WebMvcConfigurationSupport {

/**
* 设置静态资源映射
* @param registry
*/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
log.info("开始静态资源映射....");
// 路径地址和静态资源的映射
registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
}
}

后台系统登录功能

导入员工实体类

1
2
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.nuaa.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; // 身份证号 对应数据库中的id_number,在配置文件中已经做了相应配置

private Integer status;

private LocalDateTime createTime;

private LocalDateTime updateTime;

@TableField(fill = FieldFill.INSERT)
private Long createUser;

@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateUser;

}

创建对应的mapper、service、controller

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.nuaa.reggie.mapper;

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

/**
* @author xzt
* @version 1.0
*/
@Mapper // 注解,表示是dao持久层
public interface EmployeeMapper extends BaseMapper<Employee> {
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.nuaa.reggie.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.nuaa.reggie.entity.Employee;
import org.springframework.stereotype.Service;

/**
* @author xzt
* @version 1.0
*/

public interface EmployeeService extends IService<Employee> {
//mybatis-plus 封装了很多方法,所以基本不用写了
}

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

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

/**
* @author xzt
* @version 1.0
*/
@Service // 注解,表示是service业务层
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements EmployeeService {
}

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
package com.nuaa.reggie.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.nuaa.reggie.common.R;
import com.nuaa.reggie.entity.Employee;
import com.nuaa.reggie.service.EmployeeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.util.DigestUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

/**
* @author xzt
* @version 1.0
*/
@Slf4j
@RestController
@RequestMapping("/employee")
public class EmployeeController {

@Autowired
private EmployeeService employeeService;
}

编写服务端返回结果类

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
package com.nuaa.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;
}

}

编写业务逻辑

image-20230216154255440

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
/**
* 员工登录
* @param request 用于获取session,保存session
* @param employee 将前端传递的用户名和密码封装到Employee中,需要名称完全一致
* @return
*/
@PostMapping("/login")
public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee) {

String username = employee.getUsername();
String password = employee.getPassword();

// 1.将密码进行md5加密处理
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);
}

后台系统退出功能

直接在controller中编写业务逻辑即可

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 员工推出
* @param request 用于清理session中的用户id
* @return 返回给页面的结果
*/
@PostMapping("/logout")
public R<String> logout(HttpServletRequest request) {
// 1.清理session中的用户id
request.getSession().removeAttribute("employee");
// 2.返回结果
return R.success("退出成功");
}

第二章 员工管理

完善登录功能

业务需求 :需要保证只有在登录后才可以访问主页面,未登录时则默认跳转至登录页面

具体实现方法 :使用 拦截器 或者 过滤器 ,在过滤器或者拦截器中判断用户是否已经完成登录,如果没有登录则跳转至登录页面。

本项目使用过滤器进行完善

image-20230216164520572

注意 :需要在springboot启动类中需要添加@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
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.nuaa.reggie.filter;

import com.alibaba.fastjson.JSON;
import com.nuaa.reggie.common.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.web.servlet.ServletComponentScan;
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;

/**
* @author xzt
* @version 1.0
* 检查用户是否已经完成登录
*/
@Slf4j
@WebFilter(filterName = "loginCheckFilter", urlPatterns = "/*")
public class LoginCheckFilter implements Filter {

// 路径匹配器,支持通配符
public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();

// 定义不需要请求的请求路径
private String[] urls = new String[]{
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**"
};

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;

// 1.获取本次请求的url
String requestURI = request.getRequestURI();
log.info("拦截到请求:{}",requestURI);
// 2.判断本次请求是否需要处理
boolean check = check(requestURI);
// 3.如果不需要处理,则直接放行
if (check) {
log.info("本次请求{}不需要处理",requestURI);
filterChain.doFilter(request, response);
return;
}
// 4.判断登录状态,如果已经登录,则直接放行
if (request.getSession().getAttribute("employee") != null) {
log.info("用户{}已登录",request.getSession().getAttribute("employee"));
filterChain.doFilter(request, response);
return;
}

log.info("用户未登录");
// 5.如果未登录则返回登录页面,通过输出流的方式向客户端页面响应数据
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
}

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

新增员工功能

image-20230216203213257

编写新增员工的业务逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@PostMapping
public R<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 R.success("新增员工成功");
}

上面的代码可能存在问题,加入新增的员工用户名如果重复,则会报异常,因此需要对异常进行处理。

编写全局异常处理⭐

通常有两种处理方式:

  • 在controller方法中加入try、catch进行异常捕获。

    1
    2
    3
    4
    5
    try{
    employeeService.save(employee);
    } catch (Exception e) {
    return R.error("新增员工失败");
    }
  • 使用异常处理器进行全局异常捕获

使用全局异常捕获

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
package com.nuaa.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;

/**
* @author xzt
* @version 1.0
* 全局异常处理,底层是基于代理
*/
@ControllerAdvice(annotations = {RestController.class, Controller.class}) // 扫描有这两种注解的controller类
@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("未知错误");
}
}

员工信息分页查询

代码开发流程

image-20230217095651425

配置mybatis-plus分页插件⭐

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

/**
* @author xzt
* @version 1.0
* 配置mybatis-plus分页插件
*/
@Configuration
public class MybatisPlusConfig {

@Bean // 生成一个Bean对象,并交给spring管理。
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
}

完成员工信息分页查询的业务逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 分页查询员工信息
* @param page 页码
* @param pageSize 每页显示的数量
* @param name 员工名字,用于模糊查询
* @return 返回类型为R<Page>,因为页面接收的json数据中有records和total两个,所以用Page
*/
@GetMapping("/page")
public R<Page> getEmployee(int page, int pageSize, String name) {
log.info("page = {}, pageSize = {}, name = {}", page, pageSize, name);
// 构造分页构造器
Page<Employee> 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);
}

启用/禁用员工账号

代码开发

image-20230217153830469

代码实现

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

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

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

存在的问题

数据库中的id值是19位的long型数据,发送给前端页面时,前端js损失了精度,只保证了前16位数字,所以修改时获取的id和数据库中的不一致。

解决办法是,服务端给页面响应数据时,将 long型的数据统一转为string类型。

image-20230217155244810

配置对象映射器⭐

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
package com.nuaa.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);
}
}

扩展消息转换器

在项目启动时就会调用

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 扩展mvc框架的消息转换器
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
// 创建消息转换器对象
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
// 设置对象转换器,底层使用Jackson将java对象转为json
messageConverter.setObjectMapper(new JacksonObjectMapper());
// 将上面的消息转换器对象追加到mvc框架的转换器集合中
converters.add(0, messageConverter);
}

编辑员工信息

根据员工 id查询员工信息

1
2
3
4
5
6
7
8
9
10
11
12
13
 /**
* 根据id查询员工信息
* @param id 页面传过来的员工id
* @return
*/
@GetMapping("/{id}") // 路径中/id的需要这样接收且需要@PathVariable关键字
public R<Employee> getById(@PathVariable Long id) {
log.info("根据id查询员工信息");
Employee employee = employeeService.getById(id);
if (employee != null)
return R.success(employee);
return R.error("没有查询到对应员工信息");
}

第三章 分类管理

公共字段自动填充

公共字段

当若干表中存在相同字段,例如创建时间、创建人、修改时间、修改人等字段,则将这些字段称为公共字段,因此需要将这些公共字段在某个地方统一处理,来简化开发。可以使用mybatis-plus提供的公共字段自动填充功能。

实现步骤

image-20230217192302097

步骤1:在相应的实体类中添加@TableField注解

1
2
3
4
5
6
7
8
9
10
11
12
// 相应实体类的对应属性添加
@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:自定义元数据对象处理器

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.nuaa.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;

/**
* @author xzt
* @version 1.0
* 自定义的元数据对象处理器
*/
@Component // 注解作用:把普通的pojo实例化到spring容器中
@Slf4j
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", new Long(1));
metaObject.setValue("updateUser", new Long(1));
}


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

metaObject.setValue("updateTime", LocalDateTime.now());

metaObject.setValue("updateUser", new Long(1));
}
}

功能完善⭐

上述代码中updateUsercreateUser两个字段的值是写死的,但是在实际中,需要获取当前执行操作的用户id。因此需要将这部分功能进行完善。

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

image-20230217195711831

ThreadLocal介绍

image-20230217200030934

具体实现方法 :在LoginCheckFilterdoFilter方法中获取当前登录用户的id,并调用ThreadLocalset方法来设置当前线程局部变量的值(用户id),然后在MyMetaObjectHandlerupdateFill方法中调用ThreadLocalget方法来获得当前线程所对应的线程局部变量的值。

实现步骤

image-20230217200638084

步骤1:编写工具类

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

/**
* @author xzt
* @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:绑定当前线程的用户id

image-20230217201225326

步骤3:取出绑定的线程id

image-20230217201320877

新增分类

导入实体类

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
package com.nuaa.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 lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
import java.time.LocalDateTime;

/**
* 分类
*/
@Data
public class Category implements Serializable {

private static final long serialVersionUID = 1L;

private Long id;


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


//分类名称
private String name;


//顺序
private Integer sort;


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


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


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


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


//是否删除
private Integer isDeleted;

}

创建mapper、service、controller

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.nuaa.reggie.mapper;

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

/**
* @author xzt
* @version 1.0
*/
@Mapper
public interface CategoryMapper extends BaseMapper<Category> {
}
1
2
3
4
5
6
7
8
9
10
11
12
package com.nuaa.reggie.service;

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

/**
* @author xzt
* @version 1.0
*/
public interface CategoryService extends IService<Category> {
}

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

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.nuaa.reggie.entity.Category;
import com.nuaa.reggie.mapper.CategoryMapper;
import com.nuaa.reggie.service.CategoryService;
import org.springframework.stereotype.Service;

/**
* @author xzt
* @version 1.0
*/
@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.nuaa.reggie.controller;

import com.nuaa.reggie.service.CategoryService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* @author xzt
* @version 1.0
*/

@Slf4j
@RestController
@RequestMapping("/category")
public class CategoryController {

@Autowired
private CategoryService categoryService;
}

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 新增分类
* @param category 页面输入的分类信息
* @return
*/
@PostMapping
public R<String> add(@RequestBody Category category){
log.info("category: {}", category);
// 如果name字段重复,则会抛异常,会跳转至 GlobalExceptionHandler 函数中执行异常处理。
// 其中创建时间,更新时间,创建人,更新人,会因为实体类中的对应字段加了注解,使用公共字段自动填充了,所以不用处理。
categoryService.save(category);
return R.success("新增分类成功");
}

分类信息分页查询

和之前员工信息分页查询类似

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 分类信息分页查询
* @param page 页码
* @param pageSize 每页条数
* @return
*/
@GetMapping("/page")
public R<Page> getCategory(int page, int pageSize) {
log.info("page: {}, pageInfo: {}", page, pageSize);
// 构造分页构造器
Page<Category> pageInfo = new Page(page, pageSize);
// 构造条件构造器
LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
// 绑定条件
queryWrapper.orderByAsc(Category::getSort); // 升序
// 执行查询
categoryService.page(pageInfo, queryWrapper);
return R.success(pageInfo);
}

删除分类

需求分析

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

代码实现

1
2
3
4
5
6
7
8
9
10
11
/**
* 根据id删除分类信息
* @param id 页面传过来的id
* @return
*/
@DeleteMapping
public R<String> delete(Long id) {
log.info("删除的id是: {}", id);
categoryService.removeById(id);
return R.success("删除成功");
}

代码完善⭐

image-20230219162851125

  1. 创建两个实体类:菜品Dish,套餐Detmeal
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
package com.nuaa.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
package com.nuaa.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;
}
  1. 实现对应的mapper、service、controller接口或者类
  2. 代码完善

首先在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
/**
* 根据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<>();
// 添加查询条件,根据分类id进行查询
setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId, id);
int count2 = setmealService.count(setmealLambdaQueryWrapper);
if(count2 > 0) {
// 已经关联套餐,抛出一个业务异常。
throw new CustomException("当前分类下已经关联套餐,不能删除");
}
super.removeById(id);
}

然后进行异常处理,先自定义一个异常处理函数

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

/**
* @author xzt
* @version 1.0
* 自定义业务异常类
*/
public class CustomException extends RuntimeException{
public CustomException(String message) {
super(message);
}
}

再在全局的异常处理函数GlobalExceptionHandler中加入方法,主要是为了将异常信息显示在页面上。

1
2
3
4
5
6
7
8
9
10
/**
* custom自定义异常处理方法,主要是将异常提示信息显示在页面
* @return
*/
@ExceptionHandler(CustomException.class) //声明处理异常的类型
public R<String> exceptionHandler(CustomException ex) {
log.error(ex.getMessage());

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

修改分类

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 修改分类信息
* @param category 页面传递的修改后的分类信息
* @return
*/
@PutMapping
public R<String> update(@RequestBody Category category) {
log.info("修改分类信息:{}", category);
// 不需要进行更新时间或者更新人字段设置了。
categoryService.updateById(category);
return R.success("修改成功");
}

第四章 菜品管理

文件上传下载⭐⭐

文件上传❗

页面上传组件设置

image-20230220153722730

服务端接收文件配置

image-20230220154146798

文件下载

image-20230220154336114

代码实现

resources/application.yml中添加配置。

1
2
3
# 配置文件保存路径
reggie:
path: D:\

实现代码接收并保存

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
package com.nuaa.reggie.controller;

import com.nuaa.reggie.common.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
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 java.io.File;
import java.io.IOException;
import java.util.UUID;

/**
* @author xzt
* @version 1.0
* 文件上传和下载
*/
@RestController
@RequestMapping("/common")
@Slf4j
public class CommonController {

@Value("${reggie.path}")
private String basePath; // 文件上传目录,配置再resources/application.yml中

/**
* 文件上传
* @param file 必须和前端input框的name属性名一致
* @return
*/
@PostMapping("/upload")
public R<String> upload(MultipartFile file) {
// 当前file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会删除
log.info(file.toString());

// 获得原始文件名
String originalFilename = file.getOriginalFilename();
// 截取文件名后缀
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();
}

}
}

新增菜品

导入DishFlavor菜品口味实体类。创建对应的mapperservicecontroller类。

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.nuaa.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.time.LocalDateTime;

/**
菜品口味
*/
@Data
public class DishFlavor implements Serializable {

private static final long serialVersionUID = 1L;

private Long id;


//菜品id
private Long dishId;


//口味名称
private String name;


//口味数据list
private String value;


@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;

}

交互过程

image-20230220191812865

步骤1:先完成菜品分类信息的显示

image-20230220192639017

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 根据条件查询分类数据
* @param category 页面传过来的分类信息
* @return
*/
@GetMapping("/list")
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> categories = categoryService.list(queryWrapper);
return R.success(categories);
}

步骤2:图片上传下载,之前已经完成。

步骤3:菜品信息保存

因为前端传递的信息包含数据库中两个表,所以需要重新导入一个实体类DishDto

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

import com.nuaa.reggie.entity.Dish;
import com.nuaa.reggie.entity.DishFlavor;
import lombok.Data;

import java.util.ArrayList;
import java.util.List;

/**
* @author xzt
* @version 1.0
*/
@Data
public class DishDto extends Dish {
private List<DishFlavor> flavors = new ArrayList<>();

private String categoryName;

private Integer copies;
}

因为需要操作两张表,所以需要开启事务支持 ⭐,首先需要在启动类中添加@EnableTransactionManagement注解, 开启事务支持。然后在DishServiceImpl 中实现具体的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 新增菜品,同时保存对应得口味数据
* @param dishDto
*/
@Transactional // 操作两张表,需要绑定事务
public void saveWithFlavor(DishDto dishDto) {
// 保存菜品得基本信息到菜品表
this.save(dishDto);
Long dishId = dishDto.getId(); // 菜品id

List<DishFlavor> flavors = dishDto.getFlavors();
// 给每一项绑定dishid. Lambda表达式
flavors.stream().map((item) -> {
item.setDishId(dishId);
return item;
}).collect(Collectors.toList());

// 保存菜品口味数据到菜品口味表dishFlavor
dishFlavorService.saveBatch(dishDto.getFlavors());
}

菜品信息分页查询

image-20230220204149489

代码实现⭐

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
/**
* 菜品信息分页查询
* @param page 页码
* @param pageSize 每页条数
* @param name 菜品名称
* @return
*/
@GetMapping("/page")
public R<Page> getDish(int page, int pageSize, String name) {
// 构造分页构造器
Page<Dish> pageInfo = new Page<>(page, pageSize);
Page<DishDto> dishDtoPage = new Page<>();
// 构造条件构造器
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
// 添加过滤条件
queryWrapper.like(name != null, DishDto::getName, name);
// 添加排序条件
queryWrapper.orderByDesc(Dish::getUpdateTime);

// 执行分页查询
dishService.page(pageInfo, queryWrapper);

// 对象的拷贝,将pageInfo中除了records的所有属性拷贝至dishDtoPage中
BeanUtils.copyProperties(pageInfo, dishDtoPage, "records");

List<Dish> records = pageInfo.getRecords();
List<DishDto> list = records.stream().map((item) ->{
DishDto dishDto = new DishDto();

// 将item中的属性拷贝至dishDto中
BeanUtils.copyProperties(item, dishDto);

Long categoryId = item.getCategoryId(); // 分类id
// 根据id查询分类对象
Category category = categoryService.getById(categoryId);
String categoryName = category.getName();
dishDto.setCategoryName(categoryName);
return dishDto;
}).collect(Collectors.toList());

dishDtoPage.setRecords(list);

return R.success(dishDtoPage);
}

修改菜品

image-20230221100533035

菜品信息回显

需要查询两张表:dishdish_flavor两张表,所以在DishService中实现这个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 根据id查询对应的菜品信息和口味信息
* @param id 菜品id
* @return
*/
@Override
public DishDto geByIdWithFlavor(Long id) {
// 查询菜品基本信息
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> dishFlavors = dishFlavorService.list(queryWrapper);
dishDto.setFlavors(dishFlavors);

return dishDto;
}

实现对应controller层代码

1
2
3
4
5
6
7
8
9
10
/**
* 根据菜品id查询对应的菜品信息
* @param id 菜品id
* @return
*/
@GetMapping("/{id}")
public R<DishDto> getById(@PathVariable Long id) {
DishDto dishDto = dishService.geByIdWithFlavor(id);
return R.success(dishDto);
}

菜品信息修改

同样是修改两个表,同时还需要开启事务@Transactional

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
/**
* 更新菜品信息,同时更新对应的口味数据,需要操作两张表,dish、dishflavor
* @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();

// 给每一项绑定dishid. Lambda表达式
flavors.stream().map((item) -> {
item.setDishId(dishDto.getId());
return item;
}).collect(Collectors.toList());
dishFlavorService.saveBatch(flavors);
}

controller层代码实现

1
2
3
4
5
6
7
8
9
10
/**
* 修改菜品
* @param dishDto
* @return
*/
@PutMapping
public R<String> update(@RequestBody DishDto dishDto){
dishService.updateWithFlavor(dishDto);
return R.success("菜品信息修改成功");
}

第五章 套餐管理

新增套餐

需要两张表setmealsetmeal_dish

image-20230221110857196

实体类

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
package com.nuaa.reggie.entity;

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

/**
* 套餐菜品关系
*/
@Data
public class SetmealDish implements Serializable {

private static final long serialVersionUID = 1L;

private Long id;


//套餐id
private Long setmealId;


//菜品id
private Long dishId;


//菜品名称 (冗余字段)
private String name;

//菜品原价
private BigDecimal price;

//份数
private Integer copies;


//排序
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
package com.nuaa.reggie.dto;

import com.nuaa.reggie.entity.Setmeal;
import com.nuaa.reggie.entity.SetmealDish;
import lombok.Data;
import java.util.List;

@Data
public class SetmealDto extends Setmeal {

private List<SetmealDish> setmealDishes;

private String categoryName;
}

对应的mapper、service

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.nuaa.reggie.mapper;

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

/**
* @author xzt
* @version 1.0
*/
@Mapper
public interface SetmealDishMapper extends BaseMapper<SetmealDish> {
}
1
2
3
4
5
6
7
8
9
10
11
package com.nuaa.reggie.service;

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

/**
* @author xzt
* @version 1.0
*/
public interface SetmealDishService extends IService<SetmealDish> {
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.nuaa.reggie.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.nuaa.reggie.entity.SetmealDish;
import com.nuaa.reggie.mapper.SetmealDishMapper;
import com.nuaa.reggie.service.SetmealDishService;
import org.springframework.stereotype.Service;

/**
* @author xzt
* @version 1.0
*/
@Service
public class SetmealDishServiceImpl extends ServiceImpl<SetmealDishMapper, SetmealDish> implements SetmealDishService {
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.nuaa.reggie.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* @author xzt
* @version 1.0
*/
@RestController
@RequestMapping("/setmeal")
@Slf4j
public class SetmealController {
@Autowired
private SetmealService setmealService;

@Autowired
private SetmealDishService setmealDishService;
}

代码开发

image-20230221112539556

步骤1:/category/list?type=2CategoryController中已经实现了对应的功能。

步骤2:/category/list?type=1CategoryController中已经实现了对应的功能。

步骤3:/dish/list?categoryId=请求菜品分类查询对应的菜品数据并展示到菜品中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 根据条件查询对应的菜品数据
* @param dish
* @return
*/
@GetMapping("/list")
public R<List<Dish>> list(Dish dish) {
// 添加条件构造器
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Dish::getStatus, 1);
queryWrapper.eq(dish.getCategoryId() != null, Dish::getCategoryId, dish.getCategoryId());
queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);

// 进行查询
List<Dish> list = dishService.list(queryWrapper);
return R.success(list);
}

image-20230221113625532

步骤4:/common/upload已经在CommonController实现了

步骤5:/common/download已经在CommonController实现。

步骤6:/setmealSetmealController中实现,因为需要同时向两个表中添加数据,所以需要现在SetmealService中实现方法。然后再controller中调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 添加套餐信息
* @param setmealDto
*/
@Override
public void saveWithDish(SetmealDto setmealDto) {
// 保存套餐的基本信息,操作setmeal,执行insert操作
this.save(setmealDto);
List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
setmealDishes.stream().map((item) ->{
item.setSetmealId(setmealDto.getId());
return item;
}).collect(Collectors.toList());
// 保存套餐和菜品的关联信息,操作setmeal_dish,执行insert操作
setmealDishService.saveBatch(setmealDishes);
}
1
2
3
4
5
6
7
8
9
10
11
/**
* 新增套餐
* @param setmealDto
* @return
*/
@PostMapping
public R<String> save(@RequestBody SetmealDto setmealDto){
log.info("套餐信息:{}", setmealDto);
setmealService.saveWithDish(setmealDto);
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
34
35
36
37
38
39
40
41
42
43
/**
* 套餐信息分页查询
* @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> setmealDtoPage = new Page<>();
// 构造条件构造器
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
// 添加过滤条件
queryWrapper.like(name != null, Setmeal::getName, name);
// 添加排序条件
queryWrapper.orderByDesc(Setmeal::getUpdateTime);

// 执行分页查询
setmealService.page(pageInfo, queryWrapper);
// 对象的拷贝
BeanUtils.copyProperties(pageInfo, setmealDtoPage, "records");

List<Setmeal> records = pageInfo.getRecords();
List<SetmealDto> list = records.stream().map((item) -> {
SetmealDto setmealDto = new SetmealDto();
// 将item中的属性拷贝至setmealDto中
BeanUtils.copyProperties(item, setmealDto);

Long categoryId = item.getCategoryId();
// 根据id查询分类对象
Category category = categoryService.getById(categoryId);
if(category != null) {
String categoryName = category.getName();
setmealDto.setCategoryName(categoryName);
}
return setmealDto;
}).collect(Collectors.toList());

setmealDtoPage.setRecords(list);
return R.success(setmealDtoPage);
}

删除套餐

需要删除两个表中的数据,所以需要现在service中实现具体的方法。

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
/**
* 删除套餐,同时需要删除和套餐关联的菜品关系
* @param ids
*/
@Override
@Transactional
public void removeWithDish(List<Long> ids) {
// 查询套餐状态,确定是否可以删除
// select count(*) from setmeal where id in (1, 2, 3, ....) and status = 1
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
// delete from setmeal_dish where setmeal_id in (1, 2, 3);
LambdaQueryWrapper<SetmealDish> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.in(SetmealDish::getSetmealId, ids);
setmealDishService.remove(lambdaQueryWrapper);
}

controller层代码实现

1
2
3
4
5
6
7
8
9
10
/**
* 删除套餐
* @param ids
* @return
*/
@DeleteMapping
public R<String> delete(@RequestParam List<Long> ids) {
setmealService.removeWithDish(ids);
return R.success("套餐删除成功");
}

第六章 手机验证码登录

短信发送

服务开启

image-20230221205010704

本项目使用阿里云验证码服务

image-20230221210219226

代码开发

步骤1:导入maven坐标

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.5.16</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
<version>2.1.0</version>
</dependency>

步骤2:调入API

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
package com.nuaa.reggie.utils;

import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.profile.DefaultProfile;

/**
* 短信发送工具类
*/
public class SMSUtils {

/**
* 发送短信
* @param signName 签名
* @param templateCode 模板
* @param phoneNumbers 手机号
* @param param 参数
*/
public static void sendMessage(String signName, String templateCode,String phoneNumbers,String param){
DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", "", "");
IAcsClient client = new DefaultAcsClient(profile);

SendSmsRequest request = new SendSmsRequest();
request.setSysRegionId("cn-hangzhou");
request.setPhoneNumbers(phoneNumbers);
request.setSignName(signName);
request.setTemplateCode(templateCode);
request.setTemplateParam("{\"code\":\""+param+"\"}");
try {
SendSmsResponse response = client.getAcsResponse(request);
System.out.println("短信发送成功");
}catch (ClientException e) {
e.printStackTrace();
}
}

}

业务功能开发

image-20230221211919842

交互过程梳理

image-20230222100818456

代码开发

前期准备

image-20230222100924434

导入user表对应的实体类User

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.nuaa.reggie.entity;

import lombok.Data;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;
import java.io.Serializable;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
/**
* 用户信息
*/
@Data
public class User implements Serializable {

private static final long serialVersionUID = 1L;

private Long id;


//姓名
private String name;


//手机号
private String phone;


//性别 0 女 1 男
private String sex;


//身份证号
private String idNumber;


//头像
private String avatar;


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

创建对应的mapper、service、controller类

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.nuaa.reggie.mapper;

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

/**
* @author xzt
* @version 1.0
*/
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
1
2
3
4
5
6
7
8
9
10
11
12
package com.nuaa.reggie.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.nuaa.reggie.entity.User;
import com.nuaa.reggie.mapper.UserMapper;

/**
* @author xzt
* @version 1.0
*/
public interface UserService extends IService<User> {
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.nuaa.reggie.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.nuaa.reggie.entity.User;
import com.nuaa.reggie.mapper.UserMapper;
import com.nuaa.reggie.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

/**
* @author xzt
* @version 1.0
*/
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.nuaa.reggie.controller;

import com.nuaa.reggie.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
* @author xzt
* @version 1.0
*/
@RestController
@Slf4j
@RequestMapping("/user")
public class UserController {

@Autowired
private UserService userService;
}

导入工具类:短信发送工具类SMSUtils、随机生成验证码工具类ValidateCodeUtils

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
package com.nuaa.reggie.utils;

import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.profile.DefaultProfile;

/**
* 短信发送工具类
*/
public class SMSUtils {

/**
* 发送短信
* @param signName 签名
* @param templateCode 模板
* @param phoneNumbers 手机号
* @param param 参数
*/
public static void sendMessage(String signName, String templateCode,String phoneNumbers,String param){
DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", "LTAI5tCmkkEhE21W4tvjS1dd", "xU2cXGMutcASkZjR185QMmORRpiqSA");
IAcsClient client = new DefaultAcsClient(profile);

SendSmsRequest request = new SendSmsRequest();
request.setSysRegionId("cn-hangzhou");
request.setPhoneNumbers(phoneNumbers);
request.setSignName(signName);
request.setTemplateCode(templateCode);
request.setTemplateParam("{\"code\":\""+param+"\"}");
try {
SendSmsResponse response = client.getAcsResponse(request);
System.out.println("短信发送成功");
}catch (ClientException e) {
e.printStackTrace();
}
}

}
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.nuaa.reggie.utils;

import java.util.Random;

/**
* 随机生成验证码工具类
*/
public class ValidateCodeUtils {
/**
* 随机生成验证码
* @param length 长度为4位或者6位
* @return
*/
public static Integer generateValidateCode(int length){
Integer code =null;
if(length == 4){
code = new Random().nextInt(9999);//生成随机数,最大为9999
if(code < 1000){
code = code + 1000;//保证随机数为4位数字
}
}else if(length == 6){
code = new Random().nextInt(999999);//生成随机数,最大为999999
if(code < 100000){
code = code + 100000;//保证随机数为6位数字
}
}else{
throw new RuntimeException("只能生成4位或6位数字验证码");
}
return code;
}

/**
* 随机生成指定长度字符串验证码
* @param length 长度
* @return
*/
public static String generateValidateCode4String(int length){
Random rdm = new Random();
String hash1 = Integer.toHexString(rdm.nextInt());
String capstr = hash1.substring(0, length);
return capstr;
}
}

修改LoginCheckFilter

1
2
3
4
5
6
7
8
9
10
// 定义不需要请求的请求路径
private String[] urls = new String[]{
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**",
"/common/**",
"/user/sendMsg",
"/user/login"
};

doFilter函数中添加用户登录状态判断

1
2
3
4
5
6
7
8
9
// 4-2.判断普通用户登录状态,如果已经登录,则直接放行
if (request.getSession().getAttribute("user") != null) {
log.info("用户{}已登录",request.getSession().getAttribute("user"));
Long userId = (Long) request.getSession().getAttribute("user");
BaseContext.setCurrentId(userId);

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

UserController中添加发送验证码消息业务代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 发送手机短信验证码
* @param user 接受页面传过来的手机号
* @return
*/
@PostMapping("/sendMsg")
public R<String> sendMsg(@RequestBody User user, HttpSession session){
// 获取手机号
String phone = user.getPhone();
if(StringUtils.isNotEmpty(phone)) {
// 生成随机的四位验证码
String code = ValidateCodeUtils.generateValidateCode(4).toString();
log.info("code: {}", code);
// 调用阿里云提供的短信服务API完成发送短信
// SMSUtils.sendMessage("瑞吉外卖", "SMS_270920139", phone, code);
// 讲生成的验证码保存在session中
session.setAttribute(phone, code);
return R.success("手机验证码发送成功");
}

return R.error("手机验证码发送失败");
}

实现对应的用户登录功能,需要判断数据库中是否有该用户

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
/**
* 用户登录,需要判断数据库中是否有该用户
* @param map
* @param session
* @return
*/
@PostMapping("/login")
public R<User> login(@RequestBody Map map, HttpSession session) {
log.info("map: {}", map.toString());
// 获取手机号
String phone = map.get("phone").toString();
// 获取验证码
String code = map.get("code").toString();
// 从session中获取保存的验证码
Object codeInSession = session.getAttribute(phone);
// 进行验证码比对,页面提交的验证码和session中保存的验证码比对
if (codeInSession != null && codeInSession.equals(code)) {
// 如果能够比对成功,说明登录成功
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getPhone, phone);
User user = userService.getOne(queryWrapper);
if(user == null) {
// 判断当前手机号是否为新用户,如果是新用户则自动完成注册
user = new User();
user.setPhone(phone);
user.setStatus(1);
userService.save(user);
}
session.setAttribute("user", user.getId());
return R.success(user);
}
return R.error("登录失败");
}

第七章 移动端菜品管理

导入用户地址簿相关功能代码

需求分析

地址簿,指的是移动端消费者用户的地址信息,用户登录成功后可以维护自己的地址信息。同一个用户可以有多个地址信息,但是只能有一个默认地址

image

数据模型

用户的地址信息会存储在address_book表,即地址簿表中。具体表结构如下:

image-20230222162355588

导入功能代码

功能代码清单:

  • 实体类AddressBook(直接从课程资料中导入即可)

    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
    package com.nuaa.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 AddressBook implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;


    //用户id
    private Long userId;


    //收货人
    private String consignee;


    //手机号
    private String phone;


    //性别 0 女 1 男
    private String sex;


    //省级区划编号
    private String provinceCode;


    //省级名称
    private String provinceName;


    //市级区划编号
    private String cityCode;


    //市级名称
    private String cityName;


    //区级区划编号
    private String districtCode;


    //区级名称
    private String districtName;


    //详细地址
    private String detail;


    //标签
    private String label;

    //是否默认 0 否 1是
    private Integer isDefault;

    //创建时间
    @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;
    }
  • Mapper接口AddressBookMapper

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    package com.nuaa.reggie.mapper;

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

    /**
    * @author xzt
    * @version 1.0
    */
    @Mapper
    public interface AddressBookMapper extends BaseMapper<AddressBook> {
    }
  • 业务层接口AddressBookService

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

    import com.baomidou.mybatisplus.extension.service.IService;
    import com.nuaa.reggie.entity.AddressBook;

    /**
    * @author xzt
    * @version 1.0
    */
    public interface AddressBookService extends IService<AddressBook> {
    }
  • 业务层实现类AddressBookServicelmpl

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

    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import com.nuaa.reggie.entity.AddressBook;
    import com.nuaa.reggie.mapper.AddressBookMapper;
    import com.nuaa.reggie.service.AddressBookService;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.stereotype.Service;

    /**
    * @author xzt
    * @version 1.0
    */
    @Service
    @Slf4j
    public class AddressBookServiceImpl extends ServiceImpl<AddressBookMapper, AddressBook> implements AddressBookService {
    }
  • 控制层AddressBookController(直接从课程资料中导入即可)

    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
    97
    98
    99
    100
    101
    102
    103
    104
    package com.nuaa.reggie.controller;

    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
    import com.nuaa.reggie.common.BaseContext;
    import com.nuaa.reggie.common.R;
    import com.nuaa.reggie.entity.AddressBook;
    import com.nuaa.reggie.service.AddressBookService;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.*;

    import java.util.List;

    /**
    * @author xzt
    * @version 1.0
    */
    @Slf4j
    @RestController
    @RequestMapping("/addressBook")
    public class AddressBookController {

    @Autowired
    private AddressBookService addressBookService;

    /**
    * 新增
    */
    @PostMapping
    public R<AddressBook> save(@RequestBody AddressBook addressBook) {
    addressBook.setUserId(BaseContext.getCurrentId());
    log.info("addressBook:{}", addressBook);
    addressBookService.save(addressBook);
    return R.success(addressBook);
    }

    /**
    * 设置默认地址
    */
    @PutMapping("default")
    public R<AddressBook> setDefault(@RequestBody AddressBook addressBook) {
    log.info("addressBook:{}", addressBook);
    LambdaUpdateWrapper<AddressBook> wrapper = new LambdaUpdateWrapper<>();
    wrapper.eq(AddressBook::getUserId, BaseContext.getCurrentId());
    wrapper.set(AddressBook::getIsDefault, 0);
    //SQL:update address_book set is_default = 0 where user_id = ?
    addressBookService.update(wrapper);

    addressBook.setIsDefault(1);
    //SQL:update address_book set is_default = 1 where id = ?
    addressBookService.updateById(addressBook);
    return R.success(addressBook);
    }

    /**
    * 根据id查询地址
    */
    @GetMapping("/{id}")
    public R get(@PathVariable Long id) {
    AddressBook addressBook = addressBookService.getById(id);
    if (addressBook != null) {
    return R.success(addressBook);
    } else {
    return R.error("没有找到该对象");
    }
    }

    /**
    * 查询默认地址
    */
    @GetMapping("default")
    public R<AddressBook> getDefault() {
    LambdaQueryWrapper<AddressBook> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(AddressBook::getUserId, BaseContext.getCurrentId());
    queryWrapper.eq(AddressBook::getIsDefault, 1);

    //SQL:select * from address_book where user_id = ? and is_default = 1
    AddressBook addressBook = addressBookService.getOne(queryWrapper);

    if (null == addressBook) {
    return R.error("没有找到该对象");
    } else {
    return R.success(addressBook);
    }
    }

    /**
    * 查询指定用户的全部地址
    */
    @GetMapping("/list")
    public R<List<AddressBook>> list(AddressBook addressBook) {
    addressBook.setUserId(BaseContext.getCurrentId());
    log.info("addressBook:{}", addressBook);

    //条件构造器
    LambdaQueryWrapper<AddressBook> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(null != addressBook.getUserId(), AddressBook::getUserId, addressBook.getUserId());
    queryWrapper.orderByDesc(AddressBook::getUpdateTime);

    //SQL:select * from address_book where user_id = ? order by update_time desc
    return R.success(addressBookService.list(queryWrapper));
    }
    }

功能测试

image-20230222164901344

菜品展示

需求分析

用户登录成功后跳转到系统首页,在首页需要根据分类来展示菜品和套餐。如果菜品设置了口味信息需要展示 [选择规格] 按钮,否则显示 [+] 按钮。

代码开发

梳理交互过程

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

​ 1、页面(front/index.html)发送ajax请求,获取分类数据(菜品分类和套餐分类)

​ 2、页面发送ajax请求,获取第一个分类下的菜品或者套餐

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

注意:首页加载完成后,还发送了一次ajax请求用于加载购物车数据,此处可以将这次请求的地址暂时修改一下,从静态json文件获取数据,等后续开发购物车功能时再修改回来,如下:

1
2
3
4
5
6
7
8
9
//获取购物车内商品的集合
function cartListApi(data) {
return $axios({
// 'url': '/shoppingCart/list',
'url': '/front/cartData.json',
'method': 'get',
params:{...data}
})
}

cartData.json:

1
{"code":1,"msg":null,"data":[],"map":{}}

改造DishController中的list方法

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
/**
* 根据条件查询对应的菜品数据 --- 改进版
* @param dish
* @return
*/
@GetMapping("/list")
public R<List<DishDto>> list(Dish dish) {
// 添加条件构造器
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Dish::getStatus, 1);
queryWrapper.eq(dish.getCategoryId() != null, Dish::getCategoryId, dish.getCategoryId());
queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
// 进行查询
List<Dish> list = dishService.list(queryWrapper);

// 新增内容,需要加上口味信息
List<DishDto> dishDtoList = list.stream().map((item) -> {
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(item, dishDto);
Long categoryId = item.getCategoryId();
// 根据id查询分类对象,绑定分类名称
Category category = categoryService.getById(categoryId);
if(category != null) {
String categoryName = category.getName();
dishDto.setCategoryName(categoryName);
}

// 绑定口味
Long dishId = item.getId();
// 根据id查询口味对象
LambdaQueryWrapper<DishFlavor> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(DishFlavor::getDishId, dishId);
List<DishFlavor> dishFlavorList = dishFlavorService.list(lambdaQueryWrapper);
dishDto.setFlavors(dishFlavorList);
return dishDto;
}).collect(Collectors.toList());

return R.success(dishDtoList);
}

SetmealController里添加list方法显示套餐信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 根据分类id查询套餐信息
* @param setmeal
* @return
*/
@GetMapping("/list")
public R<List<Setmeal>> list(Setmeal setmeal) {
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(setmeal.getCategoryId() != null, Setmeal::getCategoryId, setmeal.getCategoryId());
queryWrapper.eq(setmeal.getStatus() != null, Setmeal::getStatus, setmeal.getStatus());
queryWrapper.orderByDesc(Setmeal::getUpdateTime);

List<Setmeal> list = setmealService.list(queryWrapper);
return R.success(list);
}

运行效果

image-20230222172335894

购物车

需求分析

移动端用户可以将菜品或者套餐添加到购物车。对于菜品来说,如果设置了口味信息,则需要选择规格后才能加入购物车;对于套餐来说,可以直接点击 [+] 将当前套餐加入购物车。在购物车中可以修改菜品和套餐的数量,也可以清空购物车。

image

数据模型

image-20230222183832340

代码开发

梳理交互过程

在开发代码之前,需要梳理一下购物车操作时前端页面和服务端的交互过程:

  1. 点击 [加入购物车] 或者 [+] 按钮,页面发送ajax请求,请求服务端,将菜品或者套餐添加到购物车
  2. 点击购物车图标,页面发送ajax请求,请求服务端查询购物车中的菜品和套餐
  3. 点击清空购物车按钮,页面发送ajax请求,请求服务端来执行清空购物车操作

开发购物车功能,其实就是在服务端编写代码去处理前端页面发送的这3次请求即可。

准备工作

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

  • 实体类ShoppingCart(直接从课程资料中导入即可)

    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.nuaa.reggie.entity;

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

    /**
    * 购物车
    */
    @Data
    public class ShoppingCart implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;

    //名称
    private String name;

    //用户id
    private Long userId;

    //菜品id
    private Long dishId;

    //套餐id
    private Long setmealId;

    //口味
    private String dishFlavor;

    //数量
    private Integer number;

    //金额
    private BigDecimal amount;

    //图片
    private String image;

    private LocalDateTime createTime;
    }
  • Mapper接口ShoppingCartMapper

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    package com.nuaa.reggie.mapper;

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

    /**
    * @author xzt
    * @version 1.0
    */
    @Mapper
    public interface ShoppingCartMapper extends BaseMapper<ShoppingCart> {
    }
  • 业务层接口ShoppingCartService

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

    import com.baomidou.mybatisplus.extension.service.IService;
    import com.nuaa.reggie.entity.ShoppingCart;

    /**
    * @author xzt
    * @version 1.0
    */
    public interface ShoppingCartService extends IService<ShoppingCart> {
    }
  • 业务层实现类ShoppingCartServicelmpl

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

    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import com.nuaa.reggie.entity.ShoppingCart;
    import com.nuaa.reggie.mapper.ShoppingCartMapper;
    import com.nuaa.reggie.service.ShoppingCartService;
    import org.springframework.stereotype.Service;

    /**
    * @author xzt
    * @version 1.0
    */
    @Service
    public class ShoppingCartServicelmpl extends ServiceImpl<ShoppingCartMapper, ShoppingCart> implements ShoppingCartService {
    }
  • 控制层ShoppingCartController

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

    import com.nuaa.reggie.service.ShoppingCartService;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;

    /**
    * @author xzt
    * @version 1.0
    */
    @Slf4j
    @RestController
    @RequestMapping("/shoppingcart")
    public class ShoppingCartController {

    @Autowired
    private ShoppingCartService shoppingCartService;
    }

添加购物车

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
/**
* 添加购物车
* @param shoppingCart
* @return
*/
@PostMapping("/add")
public R<ShoppingCart> add(@RequestBody ShoppingCart shoppingCart) {
log.info("购物车数据:{}", shoppingCart);

// 设置用户id,指定当前是哪个用户的购物车数据
Long currentId = BaseContext.getCurrentId();
shoppingCart.setUserId(currentId);
// 查询当前添加的菜品是否已经在购物车中
Long dishId = shoppingCart.getDishId();
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId, currentId);
if(dishId != null) {
// 添加到购物车的是菜品
queryWrapper.eq(ShoppingCart::getDishId, dishId);
} else {
queryWrapper.eq(ShoppingCart::getSetmealId, shoppingCart.getSetmealId());
}
ShoppingCart cartServiceOne = shoppingCartService.getOne(queryWrapper);
// 如果已经存在,则需要增加number
if(cartServiceOne != null) {
Integer number = cartServiceOne.getNumber();
cartServiceOne.setNumber(number + 1);
shoppingCartService.updateById(cartServiceOne);
} else{
// 如果不存在,则添加到购物车,数量默认就是1
shoppingCart.setNumber(1);
shoppingCartService.save(shoppingCart);
cartServiceOne = shoppingCart;
}
return R.success(cartServiceOne);
}

查看购物车

把前端数据改回来

1
2
3
4
5
6
7
8
9
//获取购物车内商品的集合
function cartListApi(data) {
return $axios({
'url': '/shoppingCart/list',
// 'url': '/front/cartData.json',
'method': 'get',
params:{...data}
})
}

查看购物车业务代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 查看购物车
* @return
*/
@GetMapping("/list")
public R<List<ShoppingCart>> list() {
log.info("查看购物车");
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId, BaseContext.getCurrentId());
queryWrapper.orderByDesc(ShoppingCart::getCreateTime);
List<ShoppingCart> list = shoppingCartService.list(queryWrapper);
return R.success(list);
}

清空购物车

1
2
3
4
5
6
7
8
9
10
11
/**
* 清空购物车
* @return
*/
@DeleteMapping("/clean")
public R<String> clean() {
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId, BaseContext.getCurrentId());
shoppingCartService.remove(queryWrapper);
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
/**
* 减少购物车
* @param shoppingCart
* @return
*/
@PostMapping("/sub")
public R<ShoppingCart> sub(@RequestBody ShoppingCart shoppingCart) {
Long dishId = shoppingCart.getDishId();
Long setmealId = shoppingCart.getSetmealId();
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();

queryWrapper.eq(ShoppingCart::getUserId, BaseContext.getCurrentId());
if(dishId != null) {
// 操作的是菜品
queryWrapper.eq(ShoppingCart::getDishId, dishId);
} else {
// 操作的是套餐
queryWrapper.eq(ShoppingCart::getSetmealId, setmealId);
}
ShoppingCart one = shoppingCartService.getOne(queryWrapper);
Integer number = one.getNumber(); // 获得购物车中该菜品的数量
if(number == 1){
// 如果购物车中只有一个菜品,则直接删除
shoppingCartService.remove(queryWrapper);
} else {
// 如果有多个,则需要更新number即可
one.setNumber(number - 1);
shoppingCartService.updateById(one);
}
return R.success(one);
}

用户下单

需求分析

移动端用户将菜品或者套餐加入购物车后,可以点击购物车中的 【去结算】 按钮,页面跳转到订单确认页面,点击 【去支付】 按钮则完成下单操作。

数据模型

用户下单业务对应的数据表为orders表和order_detail表:

orders订单表

image-20230223153359488

order_detail订单明细表

image-20230223153430574

代码开发

梳理交互过程

在开发代码之前,需要梳理一下用户下单操作时前端页面和服务端的交互过程:

  1. 在购物车中点击 【去结算】 按钮,页面跳转到订单确认页面
  2. 在订单确认页面,发送ajax请求,请求服务端获取当前登录用户的默认地址
  3. 在订单确认页面,发送ajax请求,请求服务端获取当前登录用户的购物车数据
  4. 在订单确认页面点击 【去支付】 按钮,发送ajax请求,请求服务端完成下单操作

开发用户下单功能,其实就是在服务端编写代码去处理前端页面发送的请求即可。

准备工作

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

  • 实体类Orders、OrderDetail(直接从课程资料中导入即可)

    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
    package com.nuaa.reggie.entity;

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

    /**
    * 订单
    */
    @Data
    public class Orders implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;

    //订单号
    private String number;

    //订单状态 1待付款,2待派送,3已派送,4已完成,5已取消
    private Integer status;


    //下单用户id
    private Long userId;

    //地址id
    private Long addressBookId;


    //下单时间
    private LocalDateTime orderTime;


    //结账时间
    private LocalDateTime checkoutTime;


    //支付方式 1微信,2支付宝
    private Integer payMethod;


    //实收金额
    private BigDecimal amount;

    //备注
    private String remark;

    //用户名
    private String userName;

    //手机号
    private String phone;

    //地址
    private String address;

    //收货人
    private String consignee;
    }
    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.nuaa.reggie.entity;

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

    /**
    * 订单明细
    */
    @Data
    public class OrderDetail implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;

    //名称
    private String name;

    //订单id
    private Long orderId;


    //菜品id
    private Long dishId;


    //套餐id
    private Long setmealId;


    //口味
    private String dishFlavor;


    //数量
    private Integer number;

    //金额
    private BigDecimal amount;

    //图片
    private String image;
    }
  • Mapper接口OrderMapper、OrderDetailMapper

  • 业务层接口OrderService、OrderDetailService

  • 业务层实现类OrderServicelmpl、OrderDetailServicelmpl

  • 控制层OrderController、OrderDetailController

代码开发

OrderService添加submit方法用于用户下单

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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
package com.nuaa.reggie.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.nuaa.reggie.common.BaseContext;
import com.nuaa.reggie.common.CustomException;
import com.nuaa.reggie.entity.*;
import com.nuaa.reggie.mapper.OrderMapper;
import com.nuaa.reggie.service.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

/**
* @author xzt
* @version 1.0
*/
@Slf4j
@Service
public class OrderServicelmpl extends ServiceImpl<OrderMapper, Orders> implements OrderService {
@Autowired
private ShoppingCartService shoppingCartService;

@Autowired
private OrderDetailService orderDetailService;

@Autowired
private UserService userService;

@Autowired
private AddressBookService addressBookService;

/**
* 用户提交订单
* @param orders
*/
@Override
@Transactional
public void submit(Orders orders) {
// 获取用户id
Long currentId = BaseContext.getCurrentId();
// 查询当前用户的购物车数据
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId, currentId);
List<ShoppingCart> shoppingCarts = shoppingCartService.list(queryWrapper);

if(shoppingCarts == null || shoppingCarts.size() == 0) {
throw new CustomException("购物车为空,不能下单");
}
// 查询用户数据
User user = userService.getById(currentId);
// 查询地址数据
Long addressBookId = orders.getAddressBookId();
AddressBook addressBook = addressBookService.getById(addressBookId);
if(addressBook == null) {
throw new CustomException("用户地址信息有误,不能下单");
}

long orderId = IdWorker.getId(); // 获取订单号Mybatis-plus提供

// 计算总金额,原子操作,保证在多线程的情况下也能计算正确
AtomicInteger amount = new AtomicInteger(0);

List<OrderDetail> orderDetails = shoppingCarts.stream().map((item) -> {
OrderDetail orderDetail = new OrderDetail();
orderDetail.setOrderId(orderId);
orderDetail.setNumber(item.getNumber());
orderDetail.setDishFlavor(item.getDishFlavor());
orderDetail.setDishId(item.getDishId());
orderDetail.setSetmealId(item.getSetmealId());
orderDetail.setName(item.getName());
orderDetail.setImage(item.getImage());
orderDetail.setAmount(item.getAmount());
amount.addAndGet(item.getAmount().multiply(new BigDecimal(item.getNumber())).intValue());
return orderDetail;
}).collect(Collectors.toList());


//向订单表中插入一条数据
orders.setNumber(String.valueOf(orderId)); // 设置订单号
orders.setId(orderId);
orders.setOrderTime(LocalDateTime.now());
orders.setCheckoutTime(LocalDateTime.now());
orders.setStatus(2); // 2带派送
orders.setAmount(new BigDecimal(amount.get()));//计算总金额
orders.setUserId(currentId);
orders.setUserName(user.getName());
orders.setConsignee(addressBook.getConsignee());
orders.setPhone(addressBook.getPhone());
orders.setAddress((addressBook.getProvinceName()==null?"":addressBook.getProvinceName())
+(addressBook.getCityName()==null?"":addressBook.getCityName())
+(addressBook.getDistrictName()==null?"":addressBook.getDistrictName())
+(addressBook.getDetail()==null?"":addressBook.getDetail()));

this.save(orders);

// 向订单明细表中插入多条数据
orderDetailService.saveBatch(orderDetails);

// 清空购物车数据
shoppingCartService.remove(queryWrapper);


}
}

OrderControllersubmit方法处理post请求实现上面的方法

1
2
3
4
5
6
7
8
9
10
11
/**
* 用户下单
* @param orders
* @return
*/
@PostMapping("/submit")
public R<String> submit(@RequestBody Orders orders) {
log.info("订单数据:{}", orders);
orderService.submit(orders);
return R.success("下单成功");
}

功能测试

image-20230223162621032

image-20230223162629059

功能补充

用户退出

1
2
3
4
5
6
7
8
9
10
11
/**
* 用户退出
* @param request
* @return
*/
@PostMapping("/loginout")
public R<String> logout(HttpServletRequest request) {
// 清理session中保存的用户id
request.getSession().removeAttribute("user");
return R.success("退出成功");
}

订单管理

导入OrderDto需手动添加private int sumNum;前端会计算数量

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

import com.nuaa.reggie.entity.OrderDetail;
import com.nuaa.reggie.entity.Orders;
import lombok.Data;
import java.util.List;

@Data
public class OrdersDto extends Orders {

private String userName;

private String phone;

private String address;

private String consignee;

private List<OrderDetail> orderDetails;

private int sumNum;

}

OrderController添加userPage方法

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
/**
* 分页查看订单信息
* @param page
* @param pageSize
* @return
*/
@GetMapping("/userPage")
public R<Page> getOrder(int page, int pageSize) {
// 构造分页构造器
Page<Orders> pageInfo = new Page<>(page, pageSize);
Page<OrdersDto> ordersDtoPage = new Page<>();

// 构造条件构造器
LambdaQueryWrapper<Orders> queryWrapper = new LambdaQueryWrapper<>();
// 绑定用户
queryWrapper.eq(Orders::getUserId, BaseContext.getCurrentId());
// 添加排序条件
queryWrapper.orderByDesc(Orders::getOrderTime);
// 分页查询
orderService.page(pageInfo, queryWrapper);

// 对象拷贝
BeanUtils.copyProperties(pageInfo, ordersDtoPage, "records");
List<Orders> records = pageInfo.getRecords();

List<OrdersDto> list = records.stream().map((item) -> {
OrdersDto ordersDto = new OrdersDto();
BeanUtils.copyProperties(item, ordersDto);
Long id = item.getId();
// 根据id 查订单详细信息
Orders orders = orderService.getById(id);
String number = orders.getNumber(); // 获取该订单的订单号

LambdaQueryWrapper<OrderDetail> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(OrderDetail::getOrderId, number);
List<OrderDetail> orderDetailList = orderDetailService.list(lambdaQueryWrapper);
ordersDto.setOrderDetails(orderDetailList);
return ordersDto;
}).collect(Collectors.toList());

ordersDtoPage.setRecords(list);
return R.success(ordersDtoPage);
}

image-20230223192144378

管理员端订单信息管理分页查询

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 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);
Page<OrdersDto> ordersDtoPage = new Page<>();

// 构造条件构造器
LambdaQueryWrapper<Orders> queryWrapper = new LambdaQueryWrapper<>();

// 根据number进行模糊查询
queryWrapper.like(!StringUtils.isEmpty(number), Orders::getNumber, number);
// 根据datetime进行时间范围查询
if(beginTime != null && endTime != null) {
queryWrapper.ge(Orders::getOrderTime, beginTime);
queryWrapper.le(Orders::getOrderTime, endTime);
}

// 添加排序条件
queryWrapper.orderByDesc(Orders::getOrderTime);

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

// 对象拷贝
BeanUtils.copyProperties(pageInfo, ordersDtoPage);

List<Orders> records = pageInfo.getRecords();

List<OrdersDto> list = records.stream().map((item) -> {
OrdersDto ordersDto = new OrdersDto();

BeanUtils.copyProperties(item, ordersDto);
String name = "用户" + item.getUserId();
ordersDto.setUserName(name);
return ordersDto;
}).collect(Collectors.toList());
ordersDtoPage.setRecords(list);

return R.success(ordersDtoPage);
}

image-20230223192021209

外卖订单派送

OrderController处理post请求修改status

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 派送/完成订单--修改订单状态
* @param orders
* @return
*/
@PutMapping
public R<String> send(@RequestBody Orders orders) {
Long id = orders.getId();
Integer status = orders.getStatus();

LambdaQueryWrapper<Orders> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Orders::getId, id);
Orders one = orderService.getOne(queryWrapper);
one.setStatus(status);
orderService.updateById(one);
return R.success("派送成功");
}

image-20230223192700658

image-20230223192731544image-20230223192846024

再来一单

用户可以通过该方法快速再下一单

image-20230223193001095

因为设计两个表,所以需要再OrderServiceImpl中实现

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
/**
* 再来一单
* @param orders
*/
@Override
@Transactional
public void again(Orders orders) {
// 获取订单id 并获取订单的详细信息
Long id = orders.getId();
Orders newOrders = this.getById(id);

// 设置订单号
long orderId = IdWorker.getId();
newOrders.setId(orderId);
// 设置订单号
newOrders.setNumber(String.valueOf(orderId));
// 设置下单时间
newOrders.setOrderTime(LocalDateTime.now());
newOrders.setCheckoutTime(LocalDateTime.now());
newOrders.setStatus(2); // 待派送

// 增加订单明细表
LambdaQueryWrapper<OrderDetail> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(OrderDetail::getOrderId, id);
List<OrderDetail> list = orderDetailService.list(queryWrapper);
list.stream().map((item) -> {
// 生成新的id
Long detailId = IdWorker.getId();
item.setOrderId(orderId);
item.setId(detailId);
return item;
}).collect(Collectors.toList());

// 向订单表中插入一条数据
this.save(newOrders);

// 向订单明细表中插入多条数据
orderDetailService.saveBatch(list);
}

OrderController中进行调用

1
2
3
4
5
6
7
8
9
10
11
/**
* 再来一单
* @param orders 只传递了订单编号
* @return
*/
@PostMapping("/again")
public R<String> again(@RequestBody Orders orders) {
log.info("再来一单:{}", orders);
orderService.again(orders);
return R.success("已成功下单");
}

image-20230223194942144

正在加载今日诗词....