SpringBoot实战——瑞吉外卖

屏幕截图 2023-05-21 221758

数据库创建:

屏幕截图 2023-05-21 203358

表功能:

address_book:地址表

category:菜品和套餐表

dish: 菜品表

dish_flavor: 菜品口味关系表

employee: 员工表

order_detail: 订单明细

orders:订单表

setmeal:套餐表

setmeal_dish:套餐菜品关系表

shopping_cart:购物车

user:用户表

项目创建

不必多言

pom配置:

这里其实版本号不一定是我的这个,各位可以自行在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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
<?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>

<!-- springboot组件-->
<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.Lf</groupId>
<artifactId>reggie_take_out</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- jdk版本-->
<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>
<!--web组件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>compile</scope>
</dependency>
<!--MP-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<!--Lombook-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
<!--fastjson-->
<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>
<!--druid-->
<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>

application.yml配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#端口号
server:
port: 81
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: 123456
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.lf.reggie;

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

/**
* @Author 林峰
* @Date
* @Version 1.0
*/
@Slf4j
@SpringBootApplication
public class ReggieApplication {
public static void main(String[] args) {
SpringApplication.run(ReggieApplication.class,args);
log.info("原神,启动!");
}
}

导入静态页面:

image-20230521231237689

本次实战不要求前端页面编写,所以直接导入,资料来源与黑马程序员

功能开发

登录功能:

登录页面:

image-20230521232141953

前端发送数据 -> controller接收 -> 调用service ->Mapper -> 数据库

流程还是老样子,不了解的可以看本网站springboot入门笔记

创建实体类

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.lf.reggie.domain;

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;

private LocalDateTime createTime;

private LocalDateTime updateTime;

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

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

}

mapper

1
2
3
4
5
6
7
8
9
10
11
12
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lf.reggie.domain.Employee;
import org.apache.ibatis.annotations.Mapper;

/**
* @Author 林峰
* @Date
* @Version 1.0
*/
@Mapper
public interface EmployeeMapper extends BaseMapper<Employee> {
}

service

1
2
3
4
5
6
7
8
9
10
import com.baomidou.mybatisplus.extension.service.IService;
import com.lf.reggie.domain.Employee;

/**
* @Author 林峰
* @Date
* @Version 1.0
*/
public interface EmployeeService extends IService<Employee> {
}

service实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.lf.reggie.domain.Employee;
import com.lf.reggie.mapper.EmployeeMapper;
import org.springframework.stereotype.Service;

/**
* @Author 林峰
* @Date
* @Version 1.0
*/
@Service
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements EmployeeService {
}

通用返回类型R

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
import lombok.Data;
import java.util.HashMap;
import java.util.Map;

//通用返回类型,
@Data
public class R<T> {

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

private String msg; //错误信息

private T data; //数据

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

//快速返回一个成功R对象
public static <T> R<T> success(T object) {
R<T> r = new R<T>();
r.data = object;
r.code = 1;
return r;
}
//快速返回一个错误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;
}

}

登录功能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
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
package com.lf.reggie.controller;



/**
* @Author 林峰
* @Date
* @Version 1.0
*
*/
@Slf4j
@RestController
@RequestMapping("/employee")
public class EmployeeController {

@Autowired
private EmployeeService employeeService;


//登录controller
@PostMapping("/login")
//因为前端发过来是Json数据,所以需要加 @RequestBody接收数据
public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee) {
/**
* @Description:
* @Params: [request, employee]
* request 之后用来获取session
* employee 前端传入的账号密码
* @Return com.lf.reggie.domain.R<com.lf.reggie.domain.Employee>
*/

// 1.前端提交的passowrd进行加密处理
String password = employee.getPassword();
password = DigestUtils.md5DigestAsHex(password.getBytes());
// 2.根据用户名查询数据库
// 首先需要创建条件构造器,详情见SpringBoot学习笔记(一),条件查询
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.上面都通过了,说明可以登录,把emp对象放入session,方便其他调用
HttpSession session = request.getSession();
session.setAttribute("employee", emp.getId());

return R.success(emp);
}

}

退出功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    @PostMapping("/logout")
//员工退出
public R<String> logout(HttpServletRequest request){
/**
* @Description:
* @Params: [request]
* request 用于获取sesssion
* @Return com.lf.reggie.domain.R<java.lang.String>
*/
// 从session里面移除之前存的登录信息
request.removeAttribute("employee");

return R.success("退出成功");
}

员工操作

过滤器

过滤器是为了防止用户不登录而是通过网页url直接访问页面,有安全风险

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
package com.lf.reggie.filter;



/**
* @Author 林峰
* @Date
* @Version 1.0
* 过滤器,检查是否登录
* 关于过滤器详情请见本网站学习笔记
*/
@WebFilter(filterName = "loginCheckFilter", urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter implements Filter {

// 路径匹配器
public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();

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

// 1.获取访问url
String requestURI = request.getRequestURI();
System.out.println("访问路径" + requestURI);

// 2.设置放行路径,其中的路径不需要处理
String[] urls = {"/employee/login", "/employee/logout", "/backend/**", "/front/**","/favicon.ico"};
// 3.判断是否放行
boolean b = checkUrl(urls, requestURI);
System.out.println(b);

if (b) {
filterChain.doFilter(request, response);
return;
}
// 4.判断用户是否登录
HttpSession session = request.getSession();
Object employee = session.getAttribute("employee");
// 5.成功则放行
if (employee != null) {
filterChain.doFilter(request, response);
return;
} else {
// 6.用户未登录,给前端返回错误信息

response.getWriter().write((JSON.toJSONString(R.error("NOTLOGIN"))));
return;
}


}

boolean checkUrl(String[] urls, String requestUrl) {
/**
* @Description:
* @Params: [urls, requestUrl]
* urls:放行路径
* requestUrl: 请求路径
* @Return boolean
* 判断是否放行
*/
for (String url :
urls) {
boolean match = PATH_MATCHER.match(url, requestUrl);
if (match) return true;
}
return false;

}
}

添加员工操作

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
@PostMapping
// 新增员工代码
public R<String> save(HttpServletRequest request,@RequestBody Employee employee){
/**
* @Description:
* @Params: [request, employee]
* request 获取session得到操作人信息,
* employee 前端传的员工信息
* @Return com.lf.reggie.domain.R<java.lang.String>
*/
log.info("新增员工信息{}",employee);
// md5加密密码,初始密码为123456
employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));

// 给我们新增的信息加点料: 创建时间,更新时间,创建人,更新人
employee.setCreateTime(LocalDateTime.now());
employee.setUpdateTime(LocalDateTime.now());

HttpSession session = request.getSession();
Long id = (Long) session.getAttribute("employee");

employee.setCreateUser(id);
employee.setUpdateUser(id);

// 调用方法保存数据库
employeeService.save(employee);
// System.out.println(employee);
return R.success("新增员工成功");
}

关于controller的访问url你得看前端html页面访问的url,根据它写我们后端的@Post/Getmapping

全局异常捕获

当我们重复添加员工信息,数据库就会给我们报错,因为我们的username字段是唯一的

这种情况我们可以选择利用try - catch结构来捕获异常,但这样我们要在每一个可能抛出异常的代码上写一遍,未免太过麻烦,我们就可以选择全局异常捕获

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
/**
* @Author 林峰
* @Date
* @Version 1.0
* 全局异常捕获
*/
//这个异常捕获类可以捕获加了哪些注解的异常
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {

@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
/*
捕获异常后的异常处理方法
*/
log.error(ex.getMessage());
if(ex.getMessage().contains("Duplicate entry")){
return R.error("员工已存在,添加失败");
}
return R.error("未知错误");

}
}

分页查询操作

要想实现分页查询,首先需要创建一个MP配置类,用来配置分页查询拦截器,具体实现如下,在我之前的学习笔记中也有相关介绍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @Author 林峰
* @Date
* @Version 1.0
* MP配置分页插件
*/
@Configuration
public class MybatisPlusConfig {

//拦截器
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
//配置分页拦截器
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
}

controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    //    分页查询
@GetMapping("/page")
public R<Page> page(int page, int pageSize, String name) {

// 分页构造器:
Page pageInfo = new Page(page, pageSize);
// 条件构造器:
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
// 加条件:
queryWrapper.like(StringUtils.isNotEmpty(name), Employee::getName, name);
// 排序条件:
queryWrapper.orderByDesc(Employee::getUpdateTime);

// 调用service执行查询,自动封装到pageInfo
employeeService.page(pageInfo, queryWrapper);

return R.success(pageInfo);
}

屏幕截图 2023-05-26 224332

用户禁用启用

1
2
3
4
5
6
7
8
9
10
11
12
13
//    修改员工信息
@PutMapping
public R<String> updata(HttpServletRequest request,@RequestBody Employee employee){
HttpSession session = request.getSession();
Long empID = (Long)session.getAttribute("employee");
// 更新时间,和操作人
employee.setUpdateTime(LocalDateTime.now());
employee.setUpdateUser(empID);
// 调用service
employeeService.updateById(employee);

return R.success("成功");
}

这样写会有点小问题,因为我们修改是按照前端传来的id来定位对哪一行修改的,但是前端传来的id如果本身太长,会造成失真,也就是id会显示不正确,不正确的id自然也不能正确找到要修改的数据,所以如果只写上面的controller是不够的,还需要配置新的json转换器

转换器(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
package com.lf.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);
}
}

然后我们需要设置配置类,将我们自定义的数据转换器加到mvc默认的数据转换器集合中:

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.lf.reggie.config;

import com.lf.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.WebMvcConfigurer;


import java.util.List;

/**
* @Author 林峰
* @Date
* @Version 1.0
*/
@Slf4j
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
/**
* @Description:
* @Params: 扩展消息转换器
* @Return void
*/

// 创建消息转换器对象
MappingJackson2HttpMessageConverter mjhmc = new MappingJackson2HttpMessageConverter();

// 设置对象转换器,把我们自定义好的转换器传进去
mjhmc.setObjectMapper(new JacksonObjectMapper());
// 将设置好的对象转换器加入到我们mvc框架的转换器集合中
converters.add(0,mjhmc);

}

}

这样才算大功告成

注意,这里你如果跟着课程走,他的配置类可能是继承了 WebMvcConfigurationSupport 这个类,然后你可能出现404错误,因为WebMvcConfigurationSupport存在时会自动无效化mvc的默认静态资源路径(我用的是implements WebMvcConfigurer 不覆盖默认),但实际上只要你把静态路径放到/resources/static下,这里是默认静态访问路径,根本就不需要自己用WebMvcConfigurationSupport配置,我不太理解为什么课程要自己配置,多了很多麻烦

屏幕截图 2023-05-29 195515

公共字段自动填充

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;

在我们的employee实体类上,可以看到有这么几条属性,上面的注解@TableField(fill = FieldFill.INSERT)&

@TableField(fill = FieldFill.INSERT_UPDATE) 是MP包为我们提供的可以对公共字段插入或更新时,统一进行操作,而不用我们单独设置每个对象的公共属性,例如更新时间,我们可以利用这个注解在进行数据库更新时,自动为更新后的数据加入更新时间

写我们自定义的字段补全类,实现接口:implements MetaObjectHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* @Author 林峰
* @Date
* @Version 1.0
* 自定义元对象数据处理器
*/
@Component
public class MyMetaObjecthandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime", LocalDateTime.now());
System.out.println(BaseContext.getCurrentId());
metaObject.setValue("createUser", BaseContext.getCurrentId());
metaObject.setValue("updateUser", BaseContext.getCurrentId());
}

@Override
public void updateFill(MetaObject metaObject) {
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("updateUser", BaseContext.getCurrentId());
}
}

这里注意,我们在写controller时可以利用request获得session进而获得共享的数据,但在普通java类中没法这么操作,但我们可以利用线程共享,且数据只能在同一线程中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* @Author 林峰
* @Date
* @Version 1.0
* 线程内共享数据
*/
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();
}
}

最后再在filter中加入已登录用户的id

1
2
3
4
5
//        5.成功则放行
if (employee != null) {
// 将用户登录id存入线程
BaseContext.setCurrentId(employee);
filterChain.doFilter(request, response);

成功,现在我们不需要手动设置时间,操作人等,他会自动设置

菜品操作

实体类&mapper&服务接口&接口实现

实体类:

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
/**
* 分类
*/
@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:

1
2
3
4
5
6
7
8
/**
* @Author 林峰
* @Date
* @Version 1.0
*/
@Mapper
public interface CategoryMapper extends BaseMapper<Category> {
}

服务接口:

1
2
3
4
5
6
7
/**
* @Author 林峰
* @Date
* @Version 1.0
*/
public interface CategoryService extends IService<Category> {
}

接口实现:

1
2
3
4
5
6
7
8
9
/**
* @Author 林峰
* @Date
* @Version 1.0
*/

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

新增菜品

1
2
3
4
5
6
7
8
//    新增分类
@PostMapping
public R<String> save(@RequestBody Category category){
log.info("category:{}",category);

categoryService.save(category);
return R.success("新增成功");
}

image-20230530225627454

成功

分页操作

可以类比着之前的写

1
2
3
4
5
6
7
8
9
10
11
12
13
    //    分页查询
@GetMapping("/page")
public R<Page> page(int page,int pageSize){

Page<Category> p = new Page<>(page, pageSize);
// 条件构造器
LambdaQueryWrapper<Category> lambdaQueryWrapper = new LambdaQueryWrapper<>();

lambdaQueryWrapper.orderByDesc(Category::getSort);
categoryService.page(p,lambdaQueryWrapper);

return R.success(p);
}

image-20230530225337619

完成

删除分类

这个删除肯定不能直接remove这么简单了,按照业务需求,如果该分类下有东西,那我们就不能直接将其删除,所以在删除之前,我们应该查询该分类下有没有菜品(套餐)

service自定义删除方法

1
2
3
4
5
6
7
8
9
/**
* @Author 林峰
* @Date
* @Version 1.0
*/
public interface CategoryService extends IService<Category> {
// 按分类查询
public void remove(Long id);
}

实现类:

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
@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {

@Autowired
private DishService dishService;

@Autowired
private SetMealService setMealService;

@Override
public void remove(Long id) {
/**
* @Description:
* @Params: [id]
* 查询传进来的id
* @Return void
* 自定义一个查询,因为业务要求我们如果删除项和其他项有关联,就不能删除
*/
// 查询条件:看是否有关联菜品
LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
dishLambdaQueryWrapper.eq(Dish::getCategoryId,id);
// 如果数量大于0则说明有关联
int count1 = dishService.count(dishLambdaQueryWrapper);

if(count1 > 0){
// 抛异常
throw new CustomException("删除失败,分类包含菜品");
}

// 查询条件,看是否关联套餐
LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId,id);
// 数量大于0说明有关联
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
/**
* @Author 林峰
* @Date
* @Version 1.0
* 自定义删除异常
*/
public class CustomException extends RuntimeException {
public CustomException(String message){
super(message);
}
}

还需要用到我们之前为了捕获用户重复加入写的全局异常捕捉(加一个新的捕捉方法):

1
2
3
4
5
6
7
8
9
10
11
//    捕获关联删除异常
@ExceptionHandler(CustomException.class)
public R<String> exceptionHandler(CustomException ex){
/*
捕获异常后的异常处理方法
*/
log.error(ex.getMessage());

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

}

屏幕截图 2023-05-30 225252

完成

修改分类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//    修改分类信息
@PutMapping
public R<String> update(@RequestBody Category category){
/**
* @Description:
* @Params: [category]
* 前端传入的分类信息,包括id
* @Return com.lf.reggie.domain.R<java.lang.String>
*/
log.info("修改分类信息:{}",category);

categoryService.updateById(category);
return R.success("修改分类成功");
}

这个比较简单,不做太多说明

image-20230601221451747

image-20230601221513715

OK

文件上传

文件上传的后端处理其实在springboot中得到了极大简化,只需要用到MultipartFile

具体代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    @PostMapping("/upload")
public R<String> upload(MultipartFile file){
// 此处接收到的file是临时文件,需要将其转存
log.info(file.toString());

// 利用UUID重新生成文件名,防止文件名重复
String filename = UUID.randomUUID().toString();

try {
// 上传的文件转存
file.transferTo(new File(basepath + filename));
} catch (IOException e) {
R.error("上传失败");
}
return R.success(filename);
}

配置文件中的基础路径:

1
2
reggie:
path: D:\java\reggie_take_out\src\main\img\

image-20230601232242744

文件下载(上传回显)

实现了文件上传功能后,一般还会追加一个上传回显功能,能够让用户直接看到自己上传的图片出现在上传框内,还能让其他用户也看到上传的图片。

代码如下

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
    @GetMapping("/download")
public void download(String name, HttpServletResponse response){
/**
* @Description:
* @Params: [name, response]
* @Return void
* 文件下载
*/

try {
// 输入流,获取图片
FileInputStream fileInputStream = new FileInputStream(new File(basepath + name));
// 输出流,回显图片
ServletOutputStream outputStream = response.getOutputStream();

response.setContentType("image/jpeg");
byte[] bytes = new byte[1024];
int len = 0;
while((len = fileInputStream.read(bytes)) != -1){
// 回显
outputStream.write(bytes,0,len);
outputStream.flush();
}
outputStream.close();
fileInputStream.close();

} catch (Exception e) {
e.printStackTrace();
}

}

这里传入的name其实是我们上传后返回给页面的文件名,然后利用fileInputStream将文件下载到本地并回显,最后关闭流。

新增菜品

由于新增菜品涉及到对两张表(菜品,口味)进行修改,因此我们常规的菜品和口味实体类无法同时封装新增的菜品信息,因此我们需要用的一个数据传输对象(dto)来支持前端到后端的数据传输,类似于这样:

1
2
3
4
5
6
7
8
9
@Data
public class DishDto extends Dish {

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

private String categoryName;

private Integer copies;
}

有了这个整合了dish和dishflavor两个类的数据传输对象,我们能够将前端传入的数据完全封装

1
2
3
4
5
6
7
8
9
10
11
12
13
@PostMapping
public R<String> save(@RequestBody DishDto dishDto){
/**
* @Description:
* @Params: [dishDto]
* 新的dto,可以封装菜品信息
* @Return com.lf.reggie.domain.R<java.lang.String>
*/
log.info(dishDto.toString());
dishService.saveWithFlavor(dishDto);

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

记得加@RequestBody

然后我们需要新增一条新的业务方法,能够支持同时修改dish和flavor两张表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//    这里涉及了多张表,需要添加事务
@Transactional
public void saveWithFlavor(DishDto dishDto) {
/**
* @Description: 实现同时添加菜品和口味
* @Params: [dishDto]
* @Return void
*/
// 保存菜品
this.save(dishDto);

Long id = dishDto.getId();

List<DishFlavor> flavors = dishDto.getFlavors();
// 添加菜品id
for (DishFlavor item:flavors
) {
item.setDishId(id);

}
// 保存口味
dishFlavorService.saveBatch(flavors);
}

补充PO、VO、DAO、BO、DTO、POJO解释

PO

PO是“Persistent Object”的缩写,意为“持久化对象”。它通常用于表示数据库中的一条记录,即一组相关的数据。PO是由ORM(对象关系映射)框架生成或手动创建的Java对象,它们通常具有与数据库中的表相同的字段和数据类型。在Java开发中,PO常常被用作DAO(数据访问对象)层的数据模型,以及和数据库交互的对象。PO对象中的字段与数据库中的列相对应,每一行数据对应一个PO对象,PO对象中的字段值就是对应列的值。

VO

VO是“Value Object”的缩写,意为“值对象”。VO通常用于表示程序中的某个值或者一组相关的值,例如用户的姓名、年龄、地址等等。VO通常是一个不可变对象,也就是说,它的值在创建之后就不能再修改。在Java开发中,VO对象通常用于在不同层之间传递数据,例如在Controller层和Service层之间传递数据。VO对象和PO对象类似,但是它们的作用不同。VO通常是从PO对象中提取出来的一部分数据,用于展示和传递给前端界面。

DAO

DAO是“Data Access Object”的缩写,意为“数据访问对象”。DAO层是整个应用程序中与数据库交互的核心部分。DAO层负责将数据库中的数据转换成Java对象,并将Java对象的数据保存到数据库中。DAO层的主要作用是隔离上层业务逻辑和下层数据访问细节。在Java开发中,通常使用Hibernate等ORM框架来实现DAO层。DAO层的主要任务是实现数据的增删改查等基本操作,同时提供一些高级查询功能。

BO

BO是“Business Object”的缩写,意为“业务对象”。BO通常用于表示某个业务逻辑的实体或者模型。BO通常包含一些业务逻辑和方法,例如计算某些值、验证数据、调用其他服务等等。在Java开发中,BO对象通常由Service层或者Facade层来创建,并且它们通常包含一些业务逻辑的实现,以及对数据的操作。BO通常是针对具体的业务场景而设计的,它们是具有业务含义的实体。

DTO

DTO是“Data Transfer Object”的缩写,意为“数据传输对象”。DTO通常用于在不同层之间传输数据,例如在Controller层和Service层之间传输数据。DTO对象通常包含一些简单的数据结构,例如字符串、整数、布尔值等等。在Java开发中,DTO对象通常由Controller层或者Service层来创建,并且它们通常是不可变的。

POJO

POJO是“Plain Old Java Object”的缩写,意为“简单的Java对象”。POJO通常指的是一个没有任何限制、继承或实现特定接口的普通Java对象。POJO对象通常是一种轻量级的Java对象,没有任何框架或者注解的依赖。在Java开发中,POJO对象通常用于表示简单的数据模型或者数据传输对象。

修改菜品

首先页面信息的修改一定离不开的就是回显数据

image-20230914220847306

只有回显数据,才能看到我们即将修改的信息

回显的实现并不困难,从前端角度来说,只需要在页面加载完毕后自动向后端发送一次请求,调用后端的get方法获得商品数据,然后通过json发回前端进行显示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    @Transactional
public DishDto getByIdWithFlavor(Long id) {

DishDto dishDto = new DishDto();

Dish dish = dishService.getById(id);

// 拦截器
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();

queryWrapper.eq(DishFlavor::getDishId,id);

List<DishFlavor> flavors = dishFlavorService.list(queryWrapper);

BeanUtils.copyProperties(dish,dishDto);
dishDto.setFlavors(flavors);

return dishDto;
}

代码实现也都是之前学过的内容

修改的代码也没什么好说的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//  更新菜品信息
@Transactional
public void updateWithFlavor(DishDto dishdto) {
// 更新dish表
dishService.updateById(dishdto);

// 删除原先口味表里存在的口味数据
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DishFlavor::getDishId,dishdto.getId());

dishFlavorService.remove(queryWrapper);

// 重新添加口味信息
dishFlavorService.saveBatch(dishdto.getFlavors());
}

以上方法均新增在service接口

1
2
3
4
5
6
7
8
9
10
11
public interface DishService extends IService<Dish> {

// 新增方法,同时保存菜品和口味
public void saveWithFlavor(DishDto dishDto);

// 新增方法,查询菜品信息和口味
public DishDto getByIdWithFlavor(Long id);

// 新增方法,更新菜品信息和口味
public void updateWithFlavor(DishDto dishdto);
}

套餐

新增套餐

由于新增套餐设计到多表操作,所以先在接口中定义新的方法:

1
2
//    保存菜品和套餐
public void saveWithMeal(SetmealDto setmealDto);

并在接口实现类中手动实现该方法:

1
2
3
4
5
6
7
8
9
10
11
12
    @Override
public void saveWithMeal(SetmealDto setmealDto) {
// 保存套餐基本信息
this.save(setmealDto);
// 保存菜品和套餐关系
List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
for(SetmealDish i:setmealDishes){
i.setSetmealId(setmealDto.getId());
}

setMealDishService.saveBatch(setmealDishes);
}

这样在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
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name){
/**
* @Description:
* @Params: [page, pageSize] 页码和页大小
* @Return com.lf.reggie.domain.R<com.baomidou.mybatisplus.extension.plugins.pagination.Page>
*/

Page<Setmeal> pageInfo = new Page<>(page,pageSize);
Page<SetmealDto> dtopage = new Page<>();
LambdaQueryWrapper<Setmeal> lambdaQueryWrapper = new LambdaQueryWrapper<>();

lambdaQueryWrapper.like(name != null,Setmeal::getName,name);
lambdaQueryWrapper.orderByDesc(Setmeal::getUpdateTime);

setMealService.page(pageInfo,lambdaQueryWrapper);
BeanUtils.copyProperties(pageInfo,dtopage,"records");

List<Setmeal> pageInfoRecords = pageInfo.getRecords();

ArrayList<SetmealDto> records = new ArrayList<>();

for (Setmeal item :
pageInfoRecords) {
SetmealDto sd = new SetmealDto();
BeanUtils.copyProperties(item,sd);

sd.setCategoryName( categoryService.getById(item.getCategoryId()).getName());
records.add(sd);
}
dtopage.setRecords(records);
return R.success(dtopage);
}

分页操作我们之前已经进行过很多次了,具体操作不再赘述,但此处有一个细节,因为前端页面要求显示套餐分类,而普通的

Setmeal类中没有套餐分类属性,所以我们需要setmealdto。

效果展示:

image-20231001015601525

用户手机端操作

验证码登录功能

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
    @PostMapping("/sendMsg")
public R<String> sendMsg(@RequestBody User user, HttpSession session){
// 获取手机号
String phone = user.getPhone();

// 生成随机验证码
String code = ValidateCodeUtils.generateValidateCode(4).toString();
// 发送短信(由于发送短信需要额外申请互联网服务,需要花钱,此处仅作演示)
log.info("验证码{}",code);
// session 保存验证码
session.setAttribute(phone,code);
return R.success("短信发送成功!");
}

@PostMapping("/login")
public R<User> login(@RequestBody Map map,HttpSession session){

log.info(map.toString());

// 获取手机号
String phone = map.get("phone").toString();
// 获取验证码
String code = map.get("code").toString();
// 取session验证码
Object session_code = session.getAttribute(phone);
if(session_code != null && session_code.equals(code)){
LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();

lambdaQueryWrapper.eq(User::getPhone,phone);
User user = userService.getOne(lambdaQueryWrapper);
if(user != null){
// 虚晃
}else{
user = new User();
user.setPhone(phone);
user.setStatus(1);
userService.save(user);
}
session.setAttribute("user",user);
return R.success(user);
}
// 比对
// 另外判断是否新用户(自动注册)
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
37
38
39
40
41
42
43
44
package com.lf.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;
}
}

简单说就是:前端收到发送验证码请求后,后端调用专门的api接通网络服务给前端传入的手机号发短信验证码(此处用log代替了),用户收到后,将验证码填入前端的验证码窗并点击登录,后端的登录方法接收到phone和code后先是根据phone从session(事先存入的)中取得对应code,然后进行对比,一致的话在进行之后的操作(判断是否新用户等)

说明:后面的功能基本上和之前的套路一致,笔记先不做了,之后看看有时间复习的时候做。

项目优化(redis)

环境搭建

pom:

1
2
3
4
5
<!--redis配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

application(redis连接信息):

1
2
3
4
5
redis:
host: *
port: 6379
password: *
database: 0

缓存验证码

之前的验证码是保存在session中的

优化一下,将其保存在redis中

  1. 获取验证码后,通过set存入redis
1
2
//        使用redis存储验证码,并设置有效期为5分钟
stringRedisTemplate.opsForValue().set(phone,code,5, TimeUnit.MINUTES);
  1. 登录时get获取验证码
1
2
////        取session验证码
String session_code = stringRedisTemplate.opsForValue().get(phone);
  1. 登陆成功删除
1
redisTemplate.delete(phone);

缓存菜品

当高并发模式下,每次调用菜品的list方法都要查询数据库,这样无疑增加了资源消耗,如果将

查出的菜品放到缓存中,在查找前先找缓存,如果缓存中存在需要的,则直接调用,另外缓存中的数据在修改或增加的时候需要删除,为的是在下次查找时放入新数据。

1
2
3
4
5
//        构造一个key
String key = "key" + dish.getCategoryId() + "_" + dish.getStatus();

// 先从redis中查询数据
dishDtos = (List<DishDto>) redisTemplate.opsForValue().get(key);

SpringCache

这是一个靠注解就能实现缓存的框架(不用自己手动存入redis)

常用注解:
image-20231206201708239

@EnableCaching 放在启动类上,代表开启缓存注释功能

@Cacheable 有缓存直接返回,不调用方法(查找) 它还有个condition = “”,里面可以塞判断条件,比如在查找数据时在数据库找不到,这样缓存里面就会留下一个查询条件的key还有个为null的value,所以可以condition=’’#result != null’

@CachePut 放在方法上(一般是保存数据,把新增的数据加到缓存中)

使用方法:

1
@CachePut(value = "",key = "")

value: 缓存名称(相当于一个分类,每个分类下可以有多个key,比如用户缓存,下面可以有多个用户)

key:就是key值,用来查找value

这个key值可以通过一些特殊方法来取比如:#result 代表当前方法的返回值(比如返回一个user对象,你可以#result.id)#root通过这个你可以取到当前的方法名,比如(#root.methodName)还可以

#+参数名,然后用参数名里面的属性值 比如参数里面有个user(需要添加的新数据),你就可以这样写:key = #user.id,把用户id作为key

@CacheEvict 同理删除哪个,先指定name,再指定key(#user.id)(更新数据)

切换缓存产品

上一个说的springcache是spring自带的,他不需要引入除spring原始jar包之外的包,而我们如果想用redis作为我们的缓存服务产品(springCache如果项目重启,则缓存清空,redis可以保存本地一段时间),就需要引入专门的jar包

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>

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

然后可以在配置中设置过期时间

1
2
3
cache:
redis:
time-to-live: ***(单位ms)

注意:
用到cacheable将方法返回值缓存时,方法返回值需要能被序列化(一般返回的是自拟类的话就不可以,需要在类上继承 implements Serializable )