苍穹外卖学习笔记(跳过微信支付)

苍穹外卖学习笔记

一. 项目概述、环境搭建

1. 软件开发整体流程

  1. 需求分析: 需求规格说明书、产品原型
  2. 设计: UI设计、数据库设计、接口设计
  3. 编码: 项目代码、单元测试
  4. 测试: 测试用例、测试报告
  5. 上线运维: 软件环境安装、配置

2. 技术选型

项目中使用到的技术框架和中间件

3. 开发环境搭建

3.1 前端环境搭建(nginx)

前端我们就不自己写代码了
直接打开nginx.exe运行,打开localhost:80端口访问前端页面

3.2 后端环境搭建

3.2.1 熟悉项目结构

  • 后端工程基于maven进行项目构建,并且进行分模块开发
  • 用idea打开sky-take-out工程

序号 名称 说明
1 sky-take-out maven父工程,统一管理依赖版本,聚合其他子模块
2 sky-common 子模块,存放公共类,例如:工具类、常量类、异常类等
3 sky-pojo 子模块,存放实体类、VO、DTO等
4 sky-server 子模块,后端服务,存放配置文件、Controller、Service、Mapper等

3.2.2 使用Git进行版本控制

Git学习笔记

3.2.2.1 创建本地仓库
  • 点击菜单-VCS-Create Git Repository-找到当前项目目录-点击OK
  • 将本地文件全部提交到本地仓库(commit)
3.2.2.2 创建远程仓库

打开Gitee,点击加号创建远程仓库

3.2.2.3 将本地文件推送到Git远程仓库

点击push,输入远程仓库url,输入账号密码,成功push

3.2.3 数据库环境搭建

记得在命令行中打开mysql连接:net start mysql
在navicat打开sql文件,运行创建数据库内容
navicat运行sql文件

3.2.4 前后端联调

nginx反向代理:就是将前端发送的动态请求由nginx转发到后端服务器

3.3 完善登录功能

使用md5加密算法将明文123456加密:e10adc3949ba59abbe56e057f20f883e

  • 数据库中修改管理员密码
  • java代码修改密码
1
password = DigestUtils.md5DigestAsHex(password.getBytes());

4. 导入接口文档

使用Apifox-导入-选择YApi-将json文件导入

5. Swagger

注解 说明
@Api 用在类上,例如Controller,表示对类的说明
@ApiModel 用在类上,例如entity、DTO、VO
@ApiModelProperty 用在属性上,描述属性信息
@ApiOperation 用在方法上,例如Controller的方法,说明方法的用途、作用

二. 员工管理、分类管理

1. 新增员工

1.1 员工的DTO类(EmployeeDTO)

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.sky.dto;
import lombok.Data;
import java.io.Serializable;

@Data
public class EmployeeDTO implements Serializable {
private Long id;
private String username;
private String name;
private String phone;
private String sex;
private String idNumber;
}

1.2 Controller层

在EmployeeController中创建新增员工方法,接收前端提交的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//package com.sky.controller.admin.EmployeeController;

/**
* 新增员工
* @param employeeDTO
* @return
*/
@PostMapping
@ApiOperation("新增员工")
public Result save(@RequestBody EmployeeDTO employeeDTO){
log.info("新增员工:{}",employeeDTO);
employeeService.save(employeeDTO);
return Result.success();
}

1.3 Service层

1.3.1 Service层接口

在EmployeeService接口中声明新增员工方法

1
2
3
4
5
6
7
// package com.sky.server.EmployeeService;

/**
* 新增员工
* @param employeeDTO
*/
void save(EmployeeDTO employeeDTO);

1.3.2 Service层实现类

在EmployeeServiceImpl中实现新增员工方法

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.sky.server.impl.EmployeeServiceImpl;

/**
* 新增员工
*
* @param employeeDTO
*/
public void save(EmployeeDTO employeeDTO) {
Employee employee = new Employee();

//对象属性拷贝
BeanUtils.copyProperties(employeeDTO, employee);

//设置账号的状态,默认正常状态 1表示正常 0表示锁定
employee.setStatus(StatusConstant.ENABLE);

//设置密码,默认密码123456
employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));

//设置当前记录的创建时间和修改时间
employee.setCreateTime(LocalDateTime.now());
employee.setUpdateTime(LocalDateTime.now());

//设置当前记录创建人id和修改人id
employee.setCreateUser(BaseContext.getCurrentId());
employee.setUpdateUser(BaseContext.getCurrentId());

employeeMapper.insert(employee);
}

1.4 Mapper层

在EmployeeMapper中声明insert方法

中添加方法

1
2
3
4
5
6
7
8
9
//package com.sky.EmployeeMapper;
/**
* 插入员工数据
* @param employee
*/
@Insert("insert into employee (name, username, password, phone, sex, id_number, create_time, update_time, create_user, update_user,status) " +
"values " +
"(#{name},#{username},#{password},#{phone},#{sex},#{idNumber},#{createTime},#{updateTime},#{createUser},#{updateUser},#{status})")
void insert(Employee employee);

1.5 处理SQL异常抛出

  • 问题:当录入的用户名已经存在时,抛出的异常后没有处理
  • 原因:username已经添加了唯一约束,不能重复
  • 解决方案:通过全局异常处理器处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//package com.sky.handler.GlobalExceptionHandler;

/**
* 处理SQL异常
* @param ex
* @return
*/
@ExceptionHandler
public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){
//Duplicate entry 'zhangsan' for key 'employee.idx_username'
String message = ex.getMessage();
if(message.contains("Duplicate entry")){
String[] split = message.split(" ");
String username = split[2];
String msg = username + MessageConstant.ALREADY_EXISTS;
return Result.error(msg);
}else{
return Result.error(MessageConstant.UNKNOWN_ERROR);
}
}
  • 然后定义添加相应的消息常量
1
2
//package com.sky.constant.MessageConstant;
public static final String ALREADY_EXISTS = "已存在";

1.6 ThreadLocal

  • ThreadLocal 并不是一个Thread,而是Thread的局部变量
  • ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。

常用方法

常用方法 作用
public void set(T value) 设置当前线程的线程局部变量的值
public T get() 返回当前线程所对应的线程局部变量的值
public void remove() 移除当前线程的线程局部变量

1.7 获取当前登录员工的ID

  • 问题:员工登录成功后会生成JWT令牌并响应给前端。后续请求中,前端会携带JWT令牌,通过JWT令牌可以解析出当前登录员工id:
  • 解决方案:通过ThreadLocal进行传递。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//package com.sky.interceptor.JwtTokenAdminInterceptor;

@Component
@Slf4j
public class JwtTokenAdminInterceptor implements HandlerInterceptor {
//.............................
//2、校验令牌
try {
//.................
Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
log.info("当前员工id:", empId);
/////将用户id存储到ThreadLocal////////
BaseContext.setCurrentId(empId);
////////////////////////////////////
//3、通过,放行
return true;
} catch (Exception ex) {
//......................
}
}
}

2. 员工分页查询

2.1 员工分页查询的DTO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.sky.dto;
import lombok.Data;
import java.io.Serializable;
@Data
public class EmployeePageQueryDTO implements Serializable {

//员工姓名
private String name;

//页码
private int page;

//每页显示记录数
private int pageSize;

}

2.2 Controller层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// package com.sky.controller.admin.EmployeeController;

/**
* 员工分页查询
* @param employeePageQueryDTO
* @return
*/
@GetMapping("/page")
@ApiOperation("员工分页查询")
public Result<PageResult> page(EmployeePageQueryDTO employeePageQueryDTO){
log.info("员工分页查询,参数为:{}", employeePageQueryDTO);
PageResult pageResult = employeeService.pageQuery(employeePageQueryDTO);//后续定义
return Result.success(pageResult);
}

2.3 Service层

2.3.1 Service层接口

在EmployeeService接口中声明pageQuery方法:

1
2
3
4
5
6
/**
* 分页查询
* @param employeePageQueryDTO
* @return
*/
PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO);

2.3.2 Service层实现类

在EmployeeServiceImpl中实现pageQuery方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 分页查询
*
* @param employeePageQueryDTO
* @return
*/
public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
// select * from employee limit 0,10
//开始分页查询
PageHelper.startPage(employeePageQueryDTO.getPage(), employeePageQueryDTO.getPageSize());

Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO);

long total = page.getTotal();
List<Employee> records = page.getResult();

return new PageResult(total, records);
}

2.4 Mapper层

在 EmployeeMapper 中声明 pageQuery 方法:

1
2
3
4
5
6
/**
* 分页查询
* @param employeePageQueryDTO
* @return
*/
Page<Employee> pageQuery(EmployeePageQueryDTO employeePageQueryDTO);

在 src/main/resources/mapper/EmployeeMapper.xml 中编写SQL:

1
2
3
4
5
6
7
8
9
<select id="pageQuery" resultType="com.sky.entity.Employee">
select * from employee
<where>
<if test="name != null and name != ''">
and name like concat('%',#{name},'%')
</if>
</where>
order by create_time desc
</select>

2.5 日期格式化

  • 问题:操作时间字段显示有问题。前端显示的时间格式不标准
  • 解决方案:在WebMvcConfiguration中扩展SpringMVC的消息转换器,统一对日期类型进行格式处理
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 扩展Spring MVC框架的消息转化器
* @param converters
*/
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器...");
//创建一个消息转换器对象
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
//需要为消息转换器设置一个对象转换器,对象转换器可以将Java对象序列化为json数据
converter.setObjectMapper(new JacksonObjectMapper());
//将自己的消息转化器加入容器中
converters.add(0,converter);
}

3. 启用禁用员工账号

3.1 Controller层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// package com.sky.controller.admin.EmployeeController;
/**
* 启用禁用员工账号
* @param status
* @param id
* @return
*/
@PostMapping("/status/{status}")
@ApiOperation("启用禁用员工账号")
public Result startOrStop(@PathVariable Integer status,Long id){
log.info("启用禁用员工账号:{},{}",status,id);
employeeService.startOrStop(status,id);//后绪步骤定义
return Result.success();
}

3.2 Service层

3.2.1 Service层接口

1
2
3
4
5
6
7
   //package com.sky.service.EmployeeService;
/**
* 启用禁用员工账号
* @param status
* @param id
*/
void startOrStop(Integer status, Long id);

3.2.2 Service层实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
   //package com.sky.service.impl.EmployeeServiceImpl;
/**
* 启用禁用员工账号
*
* @param status
* @param id
*/
public void startOrStop(Integer status, Long id) {
Employee employee = Employee.builder()
.status(status)
.id(id)
.build();

employeeMapper.update(employee);
}

3.3 Mapper层

1
2
3
4
5
6
   //package com.sky.mapper.EmployeeMapper;
/**
* 根据主键动态修改属性
* @param employee
*/
void update(Employee employee);

在 EmployeeMapper.xml 中编写SQL:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<update id="update" parameterType="Employee">
update employee
<set>
<if test="name != null">name = #{name},</if>
<if test="username != null">username = #{username},</if>
<if test="password != null">password = #{password},</if>
<if test="phone != null">phone = #{phone},</if>
<if test="sex != null">sex = #{sex},</if>
<if test="idNumber != null">id_Number = #{idNumber},</if>
<if test="updateTime != null">update_Time = #{updateTime},</if>
<if test="updateUser != null">update_User = #{updateUser},</if>
<if test="status != null">status = #{status},</if>
</set>
where id = #{id}
</update>

4. 编辑员工

4.1 回显员工信息功能

4.1.1 Controller层

在 EmployeeController 中创建 getById 方法:

1
2
3
4
5
6
7
8
9
10
11
/**
* 根据id查询员工信息
* @param id
* @return
*/
@GetMapping("/{id}")
@ApiOperation("根据id查询员工信息")
public Result<Employee> getById(@PathVariable Long id){
Employee employee = employeeService.getById(id);
return Result.success(employee);
}

4.1.2 Service层

4.1.2.1 Service层接口

在 EmployeeService 接口中声明 getById 方法:

1
2
3
4
5
6
/**
* 根据id查询员工
* @param id
* @return
*/
Employee getById(Long id);
4.1.2.2 Service层实现类

在 EmployeeServiceImpl 中实现 getById 方法:

1
2
3
4
5
6
7
8
9
10
11
12

/**
* 根据id查询员工
*
* @param id
* @return
*/
public Employee getById(Long id) {
Employee employee = employeeMapper.getById(id);
employee.setPassword("****");
return employee;
}

4.1.3 Mapper层

在 EmployeeMapper 接口中声明 getById 方法:

1
2
3
4
5
6
7
/**
* 根据id查询员工信息
* @param id
* @return
*/
@Select("select * from employee where id = #{id}")
Employee getById(Long id);

4.2 修改员工信息功能

4.2.1 Controller层

在 EmployeeController 中创建 update 方法:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 编辑员工信息
* @param employeeDTO
* @return
*/
@PutMapping
@ApiOperation("编辑员工信息")
public Result update(@RequestBody EmployeeDTO employeeDTO){
log.info("编辑员工信息:{}", employeeDTO);
employeeService.update(employeeDTO);
return Result.success();
}

4.2.2 Service层

4.2.2.1 Service层接口

在 EmployeeService 接口中声明 update 方法:

1
2
3
4
5
/**
* 编辑员工信息
* @param employeeDTO
*/
void update(EmployeeDTO employeeDTO);
4.2.2.2 Service层实现类

在 EmployeeServiceImpl 中实现 update 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 编辑员工信息
*
* @param employeeDTO
*/
public void update(EmployeeDTO employeeDTO) {
Employee employee = new Employee();
BeanUtils.copyProperties(employeeDTO, employee);

employee.setUpdateTime(LocalDateTime.now());
employee.setUpdateUser(BaseContext.getCurrentId());

employeeMapper.update(employee);
}

4.2.3 Mapper层

在实现启用禁用员工账号功能时,已实现employeeMapper.update(employee),在此不需写Mapper层代码。

5. 菜品分类和套餐分类

直接导入相应代码工程

三. 菜品管理

1. 公共字段自动填充

1.1 实现思路

实现步骤

  1. 自定义注解 AutoFill,用于标识需要进行公共字段自动填充的方法
  2. 自定义切面类 AutoFillAspect,统一拦截加入了 AutoFill 注解的方法,通过反射为公共字段赋值
  3. 在 Mapper 的方法上加入 AutoFill 注解

技术点:枚举、注解、AOP、反射

1.2 自定义注解 AutoFill

进入到sky-server模块,创建com.sky.annotation包。

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

import com.sky.enumeration.OperationType;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* 自定义注解,用于标识某个方法需要进行功能字段自动填充处理
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
//数据库操作类型:UPDATE INSERT
OperationType value();
}

1.3 自定义切面 AutoFillAspect

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

import com.sky.annotation.AutoFill;
import com.sky.constant.AutoFillConstant;
import com.sky.context.BaseContext;
import com.sky.enumeration.OperationType;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.time.LocalDateTime;

/**
* 自定义切面,实现公共字段自动填充处理逻辑
*/
@Aspect
@Component
@Slf4j
public class AutoFillAspect {

/**
* 切入点
*/
@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
public void autoFillPointCut(){}

/**
* 前置通知,在通知中进行公共字段的赋值
*/
@Before("autoFillPointCut()")
public void autoFill(JoinPoint joinPoint){
log.info("开始进行公共字段自动填充...");

//获取到当前被拦截的方法上的数据库操作类型
MethodSignature signature = (MethodSignature) joinPoint.getSignature();//方法签名对象
AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);//获得方法上的注解对象
OperationType operationType = autoFill.value();//获得数据库操作类型

//获取到当前被拦截的方法的参数--实体对象
Object[] args = joinPoint.getArgs();
if(args == null || args.length == 0){
return;
}

Object entity = args[0];

//准备赋值的数据
LocalDateTime now = LocalDateTime.now();
Long currentId = BaseContext.getCurrentId();

//根据当前不同的操作类型,为对应的属性通过反射来赋值
if(operationType == OperationType.INSERT){
//为4个公共字段赋值
try {
Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);

//通过反射为对象属性赋值
setCreateTime.invoke(entity,now);
setCreateUser.invoke(entity,currentId);
setUpdateTime.invoke(entity,now);
setUpdateUser.invoke(entity,currentId);
} catch (Exception e) {
e.printStackTrace();
}
}else if(operationType == OperationType.UPDATE){
//为2个公共字段赋值
try {
Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);

//通过反射为对象属性赋值
setUpdateTime.invoke(entity,now);
setUpdateUser.invoke(entity,currentId);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}

1.4 在Mapper接口的方法上加入 AutoFill 注解

CategoryMapper为例,分别在新增和修改方法添加@AutoFill()注解,也需要EmployeeMapper做相同操作;同时注释掉原有的新增和添加代码

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

@Mapper
public interface CategoryMapper {
/**
* 插入数据
* @param category
*/
@Insert("insert into category(type, name, sort, status, create_time, update_time, create_user, update_user)" +
" VALUES" +
" (#{type}, #{name}, #{sort}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})")
@AutoFill(value = OperationType.INSERT)
void insert(Category category);
/**
* 根据id修改分类
* @param category
*/
@AutoFill(value = OperationType.UPDATE)
void update(Category category);
}

2. 文件上传

  • 因为在新增菜品时,需要上传菜品对应的图片(文件),包括后绪其它功能也会使用到文件上传,故要实现通用的文件上传接口。
  • 在本项目选用阿里云的OSS服务进行文件存储。

2.1 定义OSS相关配置

在sky-server模块

  • application-dev.yml
1
2
3
4
sky:
alioss:
endpoint: oss-cn-hangzhou.aliyuncs.com
bucket-name: sky-take-out
  • application.yml
1
2
3
4
5
6
7
spring:
profiles:
active: dev #设置环境
sky:
alioss:
endpoint: ${sky.alioss.endpoint}
bucket-name: ${sky.alioss.bucket-name}

2.2 OSS工具类对象

在sky-server模块

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

import com.sky.properties.AliOssProperties;
import com.sky.utils.AliOssUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* 配置类,用于创建AliOssUtil对象
*/
@Configuration
@Slf4j
public class OssConfiguration {

@Bean
@ConditionalOnMissingBean
public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties){
log.info("开始创建阿里云文件上传工具类对象:{}",aliOssProperties);
return new AliOssUtil(aliOssProperties.getEndpoint(),
aliOssProperties.getAccessKeyId(),
aliOssProperties.getAccessKeySecret(),
aliOssProperties.getBucketName());
}
}

2.3 定义文件上传接口

在sky-server模块中定义接口

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

import com.sky.constant.MessageConstant;
import com.sky.result.Result;
import com.sky.utils.AliOssUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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.IOException;
import java.util.UUID;

/**
* 通用接口
*/
@RestController
@RequestMapping("/admin/common")
@Api(tags = "通用接口")
@Slf4j
public class CommonController {

@Autowired
private AliOssUtil aliOssUtil;

/**
* 文件上传
* @param file
* @return
*/
@PostMapping("/upload")
@ApiOperation("文件上传")
public Result<String> upload(MultipartFile file){
log.info("文件上传:{}",file);

try {
//原始文件名
String originalFilename = file.getOriginalFilename();
//截取原始文件名的后缀
String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
//构造新文件名称
String objectName = UUID.randomUUID().toString() + extension;

//文件的请求路径
String filePath = aliOssUtil.upload(file.getBytes(), objectName);
return Result.success(filePath);
} catch (IOException e) {
log.error("文件上传失败:{}", e);
}
return Result.error(MessageConstant.UPLOAD_FAILED);
}
}

3. 新增菜品

3.1 Controller层

进入到sky-server模块

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.sky.controller.admin;

import com.sky.dto.DishDTO;
import com.sky.dto.DishPageQueryDTO;
import com.sky.entity.Dish;
import com.sky.result.PageResult;
import com.sky.result.Result;
import com.sky.service.DishService;
import com.sky.vo.DishVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Set;

/**
* 菜品管理
*/
@RestController
@RequestMapping("/admin/dish")
@Api(tags = "菜品相关接口")
@Slf4j
public class DishController {

@Autowired
private DishService dishService;

/**
* 新增菜品
*
* @param dishDTO
* @return
*/
@PostMapping
@ApiOperation("新增菜品")
public Result save(@RequestBody DishDTO dishDTO) {
log.info("新增菜品:{}", dishDTO);
dishService.saveWithFlavor(dishDTO);//后绪步骤开发
return Result.success();
}
}

3.2 Service层

3.2.1 Service层接口

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

import com.sky.dto.DishDTO;
import com.sky.entity.Dish;

public interface DishService {

/**
* 新增菜品和对应的口味
*
* @param dishDTO
*/
public void saveWithFlavor(DishDTO dishDTO);

}

3.2.2 Service层实现类

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


@Service
@Slf4j
public class DishServiceImpl implements DishService {

@Autowired
private DishMapper dishMapper;
@Autowired
private DishFlavorMapper dishFlavorMapper;

/**
* 新增菜品和对应的口味
*
* @param dishDTO
*/
@Transactional
public void saveWithFlavor(DishDTO dishDTO) {

Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO, dish);

//向菜品表插入1条数据
dishMapper.insert(dish);//后绪步骤实现

//获取insert语句生成的主键值
Long dishId = dish.getId();

List<DishFlavor> flavors = dishDTO.getFlavors();
if (flavors != null && flavors.size() > 0) {
flavors.forEach(dishFlavor -> {
dishFlavor.setDishId(dishId);
});
//向口味表插入n条数据
dishFlavorMapper.insertBatch(flavors);//后绪步骤实现
}
}

}

3.3 Mapper层

3.3.1 DishMapper.java

1
2
3
4
5
6
7
/**
* 插入菜品数据
*
* @param dish
*/
@AutoFill(value = OperationType.INSERT)
void insert(Dish dish);

在/resources/mapper中创建DishMapper.xml

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.DishMapper">

<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into dish (name, category_id, price, image, description, create_time, update_time, create_user,update_user, status)
values (#{name}, #{categoryId}, #{price}, #{image}, #{description}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser}, #{status})
</insert>
</mapper>

3.3.2 DishFlavorMapper.java

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

import com.sky.entity.DishFlavor;
import java.util.List;

@Mapper
public interface DishFlavorMapper {
/**
* 批量插入口味数据
* @param flavors
*/
void insertBatch(List<DishFlavor> flavors);

}

在/resources/mapper中创建DishFlavorMapper.xml

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.DishFlavorMapper">
<insert id="insertBatch">
insert into dish_flavor (dish_id, name, value) VALUES
<foreach collection="flavors" item="df" separator=",">
(#{df.dishId},#{df.name},#{df.value})
</foreach>
</insert>
</mapper>

4. 菜品分页查询

4.1 Controller层

根据接口定义创建DishController的page分页查询方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 菜品分页查询
*
* @param dishPageQueryDTO
* @return
*/
@GetMapping("/page")
@ApiOperation("菜品分页查询")
public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO) {
log.info("菜品分页查询:{}", dishPageQueryDTO);
PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);//后绪步骤定义
return Result.success(pageResult);
}

4.2 Service层

4.2.1 Service层接口

1
2
3
4
5
6
7
/**
* 菜品分页查询
*
* @param dishPageQueryDTO
* @return
*/
PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO);

4.2.2 Service层实现类

在 DishServiceImpl 中实现分页查询方法:

1
2
3
4
5
6
7
8
9
10
11
/**
* 菜品分页查询
*
* @param dishPageQueryDTO
* @return
*/
public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) {
PageHelper.startPage(dishPageQueryDTO.getPage(), dishPageQueryDTO.getPageSize());
Page<DishVO> page = dishMapper.pageQuery(dishPageQueryDTO);//后绪步骤实现
return new PageResult(page.getTotal(), page.getResult());
}

4.3 Mapper层

在 DishMapper 接口中声明 pageQuery 方法:

1
2
3
4
5
6
7
/**
* 菜品分页查询
*
* @param dishPageQueryDTO
* @return
*/
Page<DishVO> pageQuery(DishPageQueryDTO dishPageQueryDTO);

在 DishMapper.xml 中编写SQL:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<select id="pageQuery" resultType="com.sky.vo.DishVO">
select d.* , c.name as categoryName from dish d left outer join category c on d.category_id = c.id
<where>
<if test="name != null">
and d.name like concat('%',#{name},'%')
</if>
<if test="categoryId != null">
and d.category_id = #{categoryId}
</if>
<if test="status != null">
and d.status = #{status}
</if>
</where>
order by d.create_time desc
</select>

5. 删除菜品

5.1 Controller层

根据删除菜品的接口定义在DishController中创建方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 菜品批量删除
*
* @param ids
* @return
*/
@DeleteMapping
@ApiOperation("菜品批量删除")
public Result delete(@RequestParam List<Long> ids) {
log.info("菜品批量删除:{}", ids);
dishService.deleteBatch(ids);
return Result.success();
}

5.2 Service层

5.2.1 Service层接口

在DishService接口中声明deleteBatch方法:

1
2
3
4
5
6
/**
* 菜品批量删除
*
* @param ids
*/
void deleteBatch(List<Long> ids);

5.2.2 Service层实现类

在DishServiceImpl中实现deleteBatch方法:

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
   @Autowired
private SetmealDishMapper setmealDishMapper;
/**
* 菜品批量删除
*
* @param ids
*/
@Transactional//事务
public void deleteBatch(List<Long> ids) {
//判断当前菜品是否能够删除---是否存在起售中的菜品??
for (Long id : ids) {
Dish dish = dishMapper.getById(id);//后绪步骤实现
if (dish.getStatus() == StatusConstant.ENABLE) {
//当前菜品处于起售中,不能删除
throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
}
}

//判断当前菜品是否能够删除---是否被套餐关联了??
List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(ids);
if (setmealIds != null && setmealIds.size() > 0) {
//当前菜品被套餐关联了,不能删除
throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
}

//删除菜品表中的菜品数据
for (Long id : ids) {
dishMapper.deleteById(id);
//删除菜品关联的口味数据
dishFlavorMapper.deleteByDishId(id);
}
}

5.3 Mapper层

在DishMapper中声明getById方法,并配置SQL:

1
2
3
4
5
6
7
8
/**
* 根据主键查询菜品
*
* @param id
* @return
*/
@Select("select * from dish where id = #{id}")
Dish getById(Long id);

创建SetmealDishMapper,声明getSetmealIdsByDishIds方法,并在xml文件中编写SQL:

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

import com.sky.entity.SetmealDish;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;

@Mapper
public interface SetmealDishMapper {
/**
* 根据菜品id查询对应的套餐id
*
* @param dishIds
* @return
*/
//select setmeal_id from setmeal_dish where dish_id in (1,2,3,4)
List<Long> getSetmealIdsByDishIds(List<Long> dishIds);
}

SetmealDishMapper.xml

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.SetmealDishMapper">
<select id="getSetmealIdsByDishIds" resultType="java.lang.Long">
select setmeal_id from setmeal_dish where dish_id in
<foreach collection="dishIds" item="dishId" separator="," open="(" close=")">
#{dishId}
</foreach>
</select>
</mapper>

在DishMapper.java中声明deleteById方法并配置SQL:

1
2
3
4
5
6
7
/**
* 根据主键删除菜品数据
*
* @param id
*/
@Delete("delete from dish where id = #{id}")
void deleteById(Long id);

在DishFlavorMapper中声明deleteByDishId方法并配置SQL:

1
2
3
4
5
6
/**
* 根据菜品id删除对应的口味数据
* @param dishId
*/
@Delete("delete from dish_flavor where dish_id = #{dishId}")
void deleteByDishId(Long dishId);

6. 修改菜品

6.1 Controller层

根据修改菜品的接口定义在DishController中创建方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 修改菜品
*
* @param dishDTO
* @return
*/
@PutMapping
@ApiOperation("修改菜品")
public Result update(@RequestBody DishDTO dishDTO) {
log.info("修改菜品:{}", dishDTO);
dishService.updateWithFlavor(dishDTO);
return Result.success();
}

6.2 Service层

6.2.1 Service层接口

在DishService接口中声明updateWithFlavor方法:

1
2
3
4
5
6
/**
* 根据id修改菜品基本信息和对应的口味信息
*
* @param dishDTO
*/
void updateWithFlavor(DishDTO dishDTO);

6.2.2 Service层实现类

在DishServiceImpl中实现updateWithFlavor方法:

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
/**
* 根据id修改菜品基本信息和对应的口味信息
*
* @param dishDTO
*/
public void updateWithFlavor(DishDTO dishDTO) {
Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO, dish);

//修改菜品表基本信息
dishMapper.update(dish);

//删除原有的口味数据
dishFlavorMapper.deleteByDishId(dishDTO.getId());

//重新插入口味数据
List<DishFlavor> flavors = dishDTO.getFlavors();
if (flavors != null && flavors.size() > 0) {
flavors.forEach(dishFlavor -> {
dishFlavor.setDishId(dishDTO.getId());
});
//向口味表插入n条数据
dishFlavorMapper.insertBatch(flavors);
}
}

6.3 Mapper层

在DishMapper中,声明update方法:

1
2
3
4
5
6
7
/**
* 根据id动态修改菜品数据
*
* @param dish
*/
@AutoFill(value = OperationType.UPDATE)
void update(Dish dish);

并在DishMapper.xml文件中编写SQL:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<update id="update">
update dish
<set>
<if test="name != null">name = #{name},</if>
<if test="categoryId != null">category_id = #{categoryId},</if>
<if test="price != null">price = #{price},</if>
<if test="image != null">image = #{image},</if>
<if test="description != null">description = #{description},</if>
<if test="status != null">status = #{status},</if>
<if test="updateTime != null">update_time = #{updateTime},</if>
<if test="updateUser != null">update_user = #{updateUser},</if>
</set>
where id = #{id}
</update>

7. 菜品起售停售功能

7.1 Controller层

DishController

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 菜品起售停售
* @param status
* @param id
* @return
*/
@PostMapping("/status/{status}")
@ApiOperation("菜品起售停售")
public Result<String> startOrStop(@PathVariable Integer status, Long id){
dishService.startOrStop(status,id);
return Result.success();
}

7.2 Service层

7.2.1 DishService接口

1
2
3
4
5
6
/**
* 菜品起售停售
* @param status
* @param id
*/
void startOrStop(Integer status, Long id);

7.2.2 DishServiceImpl实现类

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
@Autowire
private SetmealMapper setmealMapper;
/**
* 菜品起售停售
*
* @param status
* @param id
*/
@Transactional
public void startOrStop(Integer status, Long id) {
Dish dish = Dish.builder()
.id(id)
.status(status)
.build();
dishMapper.update(dish);

if (status == StatusConstant.DISABLE) {
// 如果是停售操作,还需要将包含当前菜品的套餐也停售
List<Long> dishIds = new ArrayList<>();
dishIds.add(id);
// select setmeal_id from setmeal_dish where dish_id in (?,?,?)
List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(dishIds);
if (setmealIds != null && setmealIds.size() > 0) {
for (Long setmealId : setmealIds) {
Setmeal setmeal = Setmeal.builder()
.id(setmealId)
.status(StatusConstant.DISABLE)
.build();
setmealMapper.update(setmeal);
}
}
}
}

7.3 Mapper层

7.3.1 SetmealMapper

1
2
3
4
5
6
7
/**
* 根据id修改套餐
*
* @param setmeal
*/
@AutoFill(OperationType.UPDATE)
void update(Setmeal setmeal);

7.3.2 SetmealMapper.xml

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
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.SetmealMapper">

<update id="update" parameterType="Setmeal">
update setmeal
<set>
<if test="name != null">
name = #{name},
</if>
<if test="categoryId != null">
category_id = #{categoryId},
</if>
<if test="price != null">
price = #{price},
</if>
<if test="status != null">
status = #{status},
</if>
<if test="description != null">
description = #{description},
</if>
<if test="image != null">
image = #{image},
</if>
<if test="updateTime != null">
update_time = #{updateTime},
</if>
<if test="updateUser != null">
update_user = #{updateUser}
</if>
</set>
where id = #{id}
</update>

</mapper>

四. 套餐管理

1. 新增套餐

1.1 Controller层

DishController

1
2
3
4
5
6
7
8
9
10
11
/**
* 根据分类id查询菜品
* @param categoryId
* @return
*/
@GetMapping("/list")
@ApiOperation("根据分类id查询菜品")
public Result<List<Dish>> list(Long categoryId){
List<Dish> list = dishService.list(categoryId);
return Result.success(list);
}

1.2 Service层

1.2.1 Service层接口

DishService

1
2
3
4
5
6
/**
* 根据分类id查询菜品
* @param categoryId
* @return
*/
List<Dish> list(Long categoryId);

1.2.2 Service层实现类

DishServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 根据分类id查询菜品
* @param categoryId
* @return
*/
public List<Dish> list(Long categoryId) {
Dish dish = Dish.builder()
.categoryId(categoryId)
.status(StatusConstant.ENABLE)
.build();
return dishMapper.list(dish);
}

1.3 Mapper层

1.3.1 DishMapper

1
2
3
4
5
6
/**
* 动态条件查询菜品
* @param dish
* @return
*/
List<Dish> list(Dish dish);

1.3.2 DishMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<select id="list" resultType="Dish" parameterType="Dish">
select * from dish
<where>
<if test="name != null">
and name like concat('%',#{name},'%')
</if>
<if test="categoryId != null">
and category_id = #{categoryId}
</if>
<if test="status != null">
and status = #{status}
</if>
</where>
order by create_time desc
</select>

1.4 SetmealController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 套餐管理
*/
@RestController
@RequestMapping("/admin/setmeal")
@Api(tags = "套餐相关接口")
@Slf4j
public class SetmealController {

@Autowired
private SetmealService setmealService;

/**
* 新增套餐
* @param setmealDTO
* @return
*/
@PostMapping
@ApiOperation("新增套餐")
public Result save(@RequestBody SetmealDTO setmealDTO) {
setmealService.saveWithDish(setmealDTO);
return Result.success();
}
}

1.5 Service层

1.5.1 SetmealService接口

1
2
3
4
5
6
7
8
public interface SetmealService {

/**
* 新增套餐,同时需要保存套餐和菜品的关联关系
* @param setmealDTO
*/
void saveWithDish(SetmealDTO setmealDTO);
}

1.5.2 SetmealServiceImpl实现类

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
/**
* 套餐业务实现
*/
@Service
@Slf4j
public class SetmealServiceImpl implements SetmealService {

@Autowired
private SetmealMapper setmealMapper;
@Autowired
private SetmealDishMapper setmealDishMapper;
@Autowired
private DishMapper dishMapper;

/**
* 新增套餐,同时需要保存套餐和菜品的关联关系
* @param setmealDTO
*/
@Transactional
public void saveWithDish(SetmealDTO setmealDTO) {
Setmeal setmeal = new Setmeal();
BeanUtils.copyProperties(setmealDTO, setmeal);

//向套餐表插入数据
setmealMapper.insert(setmeal);

//获取生成的套餐id
Long setmealId = setmeal.getId();

List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
setmealDishes.forEach(setmealDish -> {
setmealDish.setSetmealId(setmealId);
});

//保存套餐和菜品的关联关系
setmealDishMapper.insertBatch(setmealDishes);
}
}

1.6 Mapper层

1.6.1 SetmealMapper

1
2
3
4
5
6
/**
* 新增套餐
* @param setmeal
*/
@AutoFill(OperationType.INSERT)
void insert(Setmeal setmeal);

1.6.2 SetmealMapper.xml

1
2
3
4
5
6
<insert id="insert" parameterType="Setmeal" useGeneratedKeys="true" keyProperty="id">
insert into setmeal
(category_id, name, price, status, description, image, create_time, update_time, create_user, update_user)
values (#{categoryId}, #{name}, #{price}, #{status}, #{description}, #{image}, #{createTime}, #{updateTime},
#{createUser}, #{updateUser})
</insert>

1.6.3 SetmealDishMapper

1
2
3
4
5
/**
* 批量保存套餐和菜品的关联关系
* @param setmealDishes
*/
void insertBatch(List<SetmealDish> setmealDishes);

1.6.4 SetmealDishMapper.xml

1
2
3
4
5
6
7
8
<insert id="insertBatch" parameterType="list">
insert into setmeal_dish
(setmeal_id,dish_id,name,price,copies)
values
<foreach collection="setmealDishes" item="sd" separator=",">
(#{sd.setmealId},#{sd.dishId},#{sd.name},#{sd.price},#{sd.copies})
</foreach>
</insert>

2. 套餐分页查询

2.1 SetmealController

1
2
3
4
5
6
7
8
9
10
11
/**
* 分页查询
* @param setmealPageQueryDTO
* @return
*/
@GetMapping("/page")
@ApiOperation("分页查询")
public Result<PageResult> page(SetmealPageQueryDTO setmealPageQueryDTO) {
PageResult pageResult = setmealService.pageQuery(setmealPageQueryDTO);
return Result.success(pageResult);
}

2.2 SetmealService

1
2
3
4
5
6
/**
* 分页查询
* @param setmealPageQueryDTO
* @return
*/
PageResult pageQuery(SetmealPageQueryDTO setmealPageQueryDTO);

2.3 SetmealServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 分页查询
* @param setmealPageQueryDTO
* @return
*/
public PageResult pageQuery(SetmealPageQueryDTO setmealPageQueryDTO) {
int pageNum = setmealPageQueryDTO.getPage();
int pageSize = setmealPageQueryDTO.getPageSize();

PageHelper.startPage(pageNum, pageSize);
Page<SetmealVO> page = setmealMapper.pageQuery(setmealPageQueryDTO);
return new PageResult(page.getTotal(), page.getResult());
}

2.4 SetmealMapper

1
2
3
4
5
6
/**
* 分页查询
* @param setmealPageQueryDTO
* @return
*/
Page<SetmealVO> pageQuery(SetmealPageQueryDTO setmealPageQueryDTO);

2.5 SetmealMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<select id="pageQuery" resultType="com.sky.vo.SetmealVO">
select
s.*,c.name categoryName
from
setmeal s
left join
category c
on
s.category_id = c.id
<where>
<if test="name != null">
and s.name like concat('%',#{name},'%')
</if>
<if test="status != null">
and s.status = #{status}
</if>
<if test="categoryId != null">
and s.category_id = #{categoryId}
</if>
</where>
order by s.create_time desc
</select>

3. 删除套餐

3.1 SetmealController

1
2
3
4
5
6
7
8
9
10
11
/**
* 批量删除套餐
* @param ids
* @return
*/
@DeleteMapping
@ApiOperation("批量删除套餐")
public Result delete(@RequestParam List<Long> ids){
setmealService.deleteBatch(ids);
return Result.success();
}

3.2 SetmealService

1
2
3
4
5
/**
* 批量删除套餐
* @param ids
*/
void deleteBatch(List<Long> ids);

3.3 SetmealServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 批量删除套餐
* @param ids
*/
@Transactional
public void deleteBatch(List<Long> ids) {
ids.forEach(id -> {
Setmeal setmeal = setmealMapper.getById(id);
if(StatusConstant.ENABLE == setmeal.getStatus()){
//起售中的套餐不能删除
throw new DeletionNotAllowedException(MessageConstant.SETMEAL_ON_SALE);
}
});

ids.forEach(setmealId -> {
//删除套餐表中的数据
setmealMapper.deleteById(setmealId);
//删除套餐菜品关系表中的数据
setmealDishMapper.deleteBySetmealId(setmealId);
});
}

3.4 SetmealMapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 根据id查询套餐
* @param id
* @return
*/
@Select("select * from setmeal where id = #{id}")
Setmeal getById(Long id);

/**
* 根据id删除套餐
* @param setmealId
*/
@Delete("delete from setmeal where id = #{id}")
void deleteById(Long setmealId);

3.5 SetmealDishMapper

1
2
3
4
5
6
/**
* 根据套餐id删除套餐和菜品的关联关系
* @param setmealId
*/
@Delete("delete from setmeal_dish where setmeal_id = #{setmealId}")
void deleteBySetmealId(Long setmealId);

4. 修改套餐

4.1 SetmealController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 根据id查询套餐,用于修改页面回显数据
*
* @param id
* @return
*/
@GetMapping("/{id}")
@ApiOperation("根据id查询套餐")
public Result<SetmealVO> getById(@PathVariable Long id) {
SetmealVO setmealVO = setmealService.getByIdWithDish(id);
return Result.success(setmealVO);
}

/**
* 修改套餐
*
* @param setmealDTO
* @return
*/
@PutMapping
@ApiOperation("修改套餐")
public Result update(@RequestBody SetmealDTO setmealDTO) {
setmealService.update(setmealDTO);
return Result.success();
}

4.2 SetmealService

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 根据id查询套餐和关联的菜品数据
* @param id
* @return
*/
SetmealVO getByIdWithDish(Long id);

/**
* 修改套餐
* @param setmealDTO
*/
void update(SetmealDTO setmealDTO);

4.3 SetmealServiceImpl

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
/**
* 根据id查询套餐和套餐菜品关系
*
* @param id
* @return
*/
public SetmealVO getByIdWithDish(Long id) {
Setmeal setmeal = setmealMapper.getById(id);
List<SetmealDish> setmealDishes = setmealDishMapper.getBySetmealId(id);

SetmealVO setmealVO = new SetmealVO();
BeanUtils.copyProperties(setmeal, setmealVO);
setmealVO.setSetmealDishes(setmealDishes);

return setmealVO;
}

/**
* 修改套餐
*
* @param setmealDTO
*/
@Transactional
public void update(SetmealDTO setmealDTO) {
Setmeal setmeal = new Setmeal();
BeanUtils.copyProperties(setmealDTO, setmeal);

//1、修改套餐表,执行update
setmealMapper.update(setmeal);

//套餐id
Long setmealId = setmealDTO.getId();

//2、删除套餐和菜品的关联关系,操作setmeal_dish表,执行delete
setmealDishMapper.deleteBySetmealId(setmealId);

List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
setmealDishes.forEach(setmealDish -> {
setmealDish.setSetmealId(setmealId);
});
//3、重新插入套餐和菜品的关联关系,操作setmeal_dish表,执行insert
setmealDishMapper.insertBatch(setmealDishes);
}

4.4 SetmealDishMapper

1
2
3
4
5
6
7
/**
* 根据套餐id查询套餐和菜品的关联关系
* @param setmealId
* @return
*/
@Select("select * from setmeal_dish where setmeal_id = #{setmealId}")
List<SetmealDish> getBySetmealId(Long setmealId);

5. 起售停售套餐

5.1 SetmealController

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 套餐起售停售
* @param status
* @param id
* @return
*/
@PostMapping("/status/{status}")
@ApiOperation("套餐起售停售")
public Result startOrStop(@PathVariable Integer status, Long id) {
setmealService.startOrStop(status, id);
return Result.success();
}

5.2 SetmealService

1
2
3
4
5
6
/**
* 套餐起售、停售
* @param status
* @param id
*/
void startOrStop(Integer status, Long id);

5.3 SetmealServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 套餐起售、停售
* @param status
* @param id
*/
public void startOrStop(Integer status, Long id) {
//起售套餐时,判断套餐内是否有停售菜品,有停售菜品提示"套餐内包含未启售菜品,无法启售"
if(status == StatusConstant.ENABLE){
//select a.* from dish a left join setmeal_dish b on a.id = b.dish_id where b.setmeal_id = ?
List<Dish> dishList = dishMapper.getBySetmealId(id);
if(dishList != null && dishList.size() > 0){
dishList.forEach(dish -> {
if(StatusConstant.DISABLE == dish.getStatus()){
throw new SetmealEnableFailedException(MessageConstant.SETMEAL_ENABLE_FAILED);
}
});
}
}

Setmeal setmeal = Setmeal.builder()
.id(id)
.status(status)
.build();
setmealMapper.update(setmeal);
}

5.4 DishMapper

1
2
3
4
5
6
7
/**
* 根据套餐id查询菜品
* @param setmealId
* @return
*/
@Select("select a.* from dish a left join setmeal_dish b on a.id = b.dish_id where b.setmeal_id = #{setmealId}")
List<Dish> getBySetmealId(Long setmealId);

五. Redis

1. Redis简介

Redis是一个基于内存的key-value结构数据库。Redis 是互联网技术领域使用最为广泛的存储中间件

主要特点:

  • 基于内存存储,读写性能高
  • 适合存储热点数据(热点商品、资讯、新闻)
  • 企业应用广泛

启动Redis:在Redis安装目录cmd以下命令
redis-server.exe redis.windows.conf

2. Redis数据类型

2.1 五种常用数据类型介绍

Redis存储的是key-value结构的数据,其中key是字符串类型,value有5种常用的数据类型:

  • 字符串 string
  • 哈希 hash
  • 列表 list
  • 集合 set
  • 有序集合 sorted set / zset

2.2 各种数据类型特点

解释说明:

  • 字符串(string):普通字符串,Redis中最简单的数据类型
  • 哈希(hash):也叫散列,类似于Java中的HashMap结构
  • 列表(list):按照插入顺序排序,可以有重复元素,类似于Java中的LinkedList
  • 集合(set):无序集合,没有重复元素,类似于Java中的HashSet
  • 有序集合(sorted set/zset):集合中每个元素关联一个分数(score),根据分数升序排序,没有重复元素

3. Redis常用命令

3.1 字符串操作命令

Redis 中字符串类型常用命令:

  • SET key value 设置指定key的值
  • GET key 获取指定key的值
  • SETEX key seconds value 设置指定key的值,并将 key 的过期时间设为 seconds 秒
  • SETNX key value 只有在 key 不存在时设置 key 的值

3.2 哈希操作命令

Redis hash 是一个string类型的 field 和 value 的映射表,hash特别适合用于存储对象,常用命令:

  • HSET key field value 将哈希表 key 中的字段 field 的值设为 value
  • HGET key field 获取存储在哈希表中指定字段的值
  • HDEL key field 删除存储在哈希表中的指定字段
  • HKEYS key 获取哈希表中所有字段
  • HVALS key 获取哈希表中所有值

3.3 列表操作命令

Redis 列表是简单的字符串列表,按照插入顺序排序,常用命令:

  • LPUSH key value1 [value2] 将一个或多个值插入到列表头部
  • LRANGE key start stop 获取列表指定范围内的元素
  • RPOP key 移除并获取列表最后一个元素
  • LLEN key 获取列表长度
  • BRPOP key1 [key2 ] timeout 移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超 时或发现可弹出元素为止

3.4 集合操作命令

Redis set 是string类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据,常用命令:

  • SADD key member1 [member2] 向集合添加一个或多个成员
  • SMEMBERS key 返回集合中的所有成员
  • SCARD key 获取集合的成员数
  • SINTER key1 [key2] 返回给定所有集合的交集
  • SUNION key1 [key2] 返回所有给定集合的并集
  • SREM key member1 [member2] 移除集合中一个或多个成员

3.5 有序集合操作命令

Redis有序集合是string类型元素的集合,且不允许有重复成员。每个元素都会关联一个double类型的分数。常用命令:

常用命令:

  • ZADD key score1 member1 [score2 member2] 向有序集合添加一个或多个成员
  • ZRANGE key start stop [WITHSCORES] 通过索引区间返回有序集合中指定区间内的成员
  • ZINCRBY key increment member 有序集合中对指定成员的分数加上增量 increment
  • ZREM key member [member …] 移除有序集合中的一个或多个成员

3.6 通用命令

Redis的通用命令是不分数据类型的,都可以使用的命令:

  • KEYS pattern 查找所有符合给定模式( pattern)的 key
  • EXISTS key 检查给定 key 是否存在
  • TYPE key 返回 key 所储存的值的类型
  • DEL key 该命令用于在 key 存在是删除 key

4. 在java中操作Redis

4.1 Spring Data Redis

Spring 对 Redis 客户端进行了整合,提供了 Spring Data Redis,我们重点学习Spring Data Redis

Spring Data Redis中提供了一个高度封装的类:RedisTemplate,对相关api进行了归类封装,将同一类型操作封装为operation接口,具体分类如下:

  • ValueOperations:string数据操作
  • SetOperations:set类型数据操作
  • ZSetOperations:zset类型数据操作
  • HashOperations:hash类型的数据操作
  • ListOperations:list类型的数据操作

4.2 通过RedisTemplate对象操作Redis

在test下新建测试类

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

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.*;

@SpringBootTest
public class SpringDataRedisTest {
@Autowired
private RedisTemplate redisTemplate;

@Test
public void testRedisTemplate(){
System.out.println(redisTemplate);
//string数据操作
ValueOperations valueOperations = redisTemplate.opsForValue();
//hash类型的数据操作
HashOperations hashOperations = redisTemplate.opsForHash();
//list类型的数据操作
ListOperations listOperations = redisTemplate.opsForList();
//set类型数据操作
SetOperations setOperations = redisTemplate.opsForSet();
//zset类型数据操作
ZSetOperations zSetOperations = redisTemplate.opsForZSet();
}
}

4.3 操作常见类型数据

1). 操作字符串类型数据

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 操作字符串类型的数据
*/
@Test
public void testString(){
// set get setex setnx
redisTemplate.opsForValue().set("name","小明");
String city = (String) redisTemplate.opsForValue().get("name");
System.out.println(city);
redisTemplate.opsForValue().set("code","1234",3, TimeUnit.MINUTES);
redisTemplate.opsForValue().setIfAbsent("lock","1");
redisTemplate.opsForValue().setIfAbsent("lock","2");
}

2). 操作哈希类型数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 操作哈希类型的数据
*/
@Test
public void testHash(){
//hset hget hdel hkeys hvals
HashOperations hashOperations = redisTemplate.opsForHash();

hashOperations.put("100","name","tom");
hashOperations.put("100","age","20");

String name = (String) hashOperations.get("100", "name");
System.out.println(name);

Set keys = hashOperations.keys("100");
System.out.println(keys);

List values = hashOperations.values("100");
System.out.println(values);

hashOperations.delete("100","age");
}

3). 操作列表类型数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 操作列表类型的数据
*/
@Test
public void testList(){
//lpush lrange rpop llen
ListOperations listOperations = redisTemplate.opsForList();

listOperations.leftPushAll("mylist","a","b","c");
listOperations.leftPush("mylist","d");

List mylist = listOperations.range("mylist", 0, -1);
System.out.println(mylist);

listOperations.rightPop("mylist");

Long size = listOperations.size("mylist");
System.out.println(size);
}

4). 操作集合类型数据

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
/**
* 操作集合类型的数据
*/
@Test
public void testSet(){
//sadd smembers scard sinter sunion srem
SetOperations setOperations = redisTemplate.opsForSet();

setOperations.add("set1","a","b","c","d");
setOperations.add("set2","a","b","x","y");

Set members = setOperations.members("set1");
System.out.println(members);

Long size = setOperations.size("set1");
System.out.println(size);

Set intersect = setOperations.intersect("set1", "set2");
System.out.println(intersect);

Set union = setOperations.union("set1", "set2");
System.out.println(union);

setOperations.remove("set1","a","b");
}

5). 操作有序集合类型数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 操作有序集合类型的数据
*/
@Test
public void testZset(){
//zadd zrange zincrby zrem
ZSetOperations zSetOperations = redisTemplate.opsForZSet();

zSetOperations.add("zset1","a",10);
zSetOperations.add("zset1","b",12);
zSetOperations.add("zset1","c",9);

Set zset1 = zSetOperations.range("zset1", 0, -1);
System.out.println(zset1);

zSetOperations.incrementScore("zset1","c",10);

zSetOperations.remove("zset1","a","b");
}

6). 通用命令操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 通用命令操作
*/
@Test
public void testCommon(){
//keys exists type del
Set keys = redisTemplate.keys("*");
System.out.println(keys);

Boolean name = redisTemplate.hasKey("name");
Boolean set1 = redisTemplate.hasKey("set1");

for (Object key : keys) {
DataType type = redisTemplate.type(key);
System.out.println(type.name());
}

redisTemplate.delete("mylist");
}

六. 店铺营业状态

营业状态分为营业中打烊中

1. 设置营业状态

在sky-server模块中,创建ShopController.java

根据接口定义创建ShopController的setStatus设置营业状态方法:

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

import com.sky.result.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController("adminShopController")
@RequestMapping("/admin/shop")
@Api(tags = "店铺相关接口")
@Slf4j
public class ShopController {

public static final String KEY = "SHOP_STATUS";

@Autowired
private RedisTemplate redisTemplate;

/**
* 设置店铺的营业状态
* @param status
* @return
*/
@PutMapping("/{status}")
@ApiOperation("设置店铺的营业状态")
public Result setStatus(@PathVariable Integer status){
log.info("设置店铺的营业状态为:{}",status == 1 ? "营业中" : "打烊中");
redisTemplate.opsForValue().set(KEY,status);
return Result.success();
}
}

2. 管理端查询营业状态

根据接口定义创建ShopController的getStatus查询营业状态方法:

1
2
3
4
5
6
7
8
9
10
11
/**
* 获取店铺的营业状态
* @return
*/
@GetMapping("/status")
@ApiOperation("获取店铺的营业状态")
public Result<Integer> getStatus(){
Integer status = (Integer) redisTemplate.opsForValue().get(KEY);
log.info("获取到店铺的营业状态为:{}",status == 1 ? "营业中" : "打烊中");
return Result.success(status);
}

3. 用户端查询营业状态

创建com.sky.controller.user包,在该包下创建ShopController.java

根据接口定义创建ShopController的getStatus查询营业状态方法:

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
package com.sky.controller.user;

import com.sky.result.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;

@RestController("userShopController")
@RequestMapping("/user/shop")
@Api(tags = "店铺相关接口")
@Slf4j
public class ShopController {

public static final String KEY = "SHOP_STATUS";

@Autowired
private RedisTemplate redisTemplate;

/**
* 获取店铺的营业状态
* @return
*/
@GetMapping("/status")
@ApiOperation("获取店铺的营业状态")
public Result<Integer> getStatus(){
Integer status = (Integer) redisTemplate.opsForValue().get(KEY);
log.info("获取到店铺的营业状态为:{}",status == 1 ? "营业中" : "打烊中");
return Result.success(status);
}
}

七. HttpClient

HttpClient 是Apache Jakarta Common 下的子项目,可以用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本和建议。

HttpClient作用:

  • 发送HTTP请求
  • 接收响应数据

HttpClient的核心API:

  • HttpClient:Http客户端对象类型,使用该类型对象可发起Http请求。
  • HttpClients:可认为是构建器,可创建HttpClient对象。
  • CloseableHttpClient:实现类,实现了HttpClient接口。
  • HttpGet:Get方式请求类型。
  • HttpPost:Post方式请求类型。

HttpClient发送请求步骤:

  • 创建HttpClient对象
  • 创建Http请求对象
  • 调用HttpClient的execute方法发送请求

八. 微信登录

1. 定义相关配置

1.1 配置微信登录所需配置项

application-dev.yml

1
2
3
4
sky:
wechat:
appid: wxffb3637a228223b8
secret: 84311df9199ecacdf4f12d27b6b9522d

application.yml

1
2
3
4
sky:
wechat:
appid: ${sky.wechat.appid}
secret: ${sky.wechat.secret}

1.2 配置为微信用户生成jwt令牌时使用的配置项

application.yml

1
2
3
4
5
6
7
8
9
10
11
sky:
jwt:
# 设置jwt签名加密时使用的秘钥
admin-secret-key: itcast
# 设置jwt过期时间
admin-ttl: 7200000
# 设置前端传递过来的令牌名称
admin-token-name: token
user-secret-key: itheima
user-ttl: 7200000
user-token-name: authentication

2. DTO设计

根据传入参数设计DTO类:

image-20221205183625049

在sky-pojo模块,UserLoginDTO.java已定义

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

import lombok.Data;

import java.io.Serializable;

/**
* C端用户登录
*/
@Data
public class UserLoginDTO implements Serializable {

private String code;

}

3. VO设计

根据返回数据设计VO类:

image-20221205183923272

在sky-pojo模块,UserLoginVO.java已定义

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

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserLoginVO implements Serializable {

private Long id;
private String openid;
private String token;

}

4. Controller层

根据接口定义创建UserController的login方法:

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
package com.sky.controller.user;

import com.sky.constant.JwtClaimsConstant;
import com.sky.dto.UserLoginDTO;
import com.sky.entity.User;
import com.sky.properties.JwtProperties;
import com.sky.result.Result;
import com.sky.service.UserService;
import com.sky.utils.JwtUtil;
import com.sky.vo.UserLoginVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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 java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/user/user")
@Api(tags = "C端用户相关接口")
@Slf4j
public class UserController {

@Autowired
private UserService userService;
@Autowired
private JwtProperties jwtProperties;

/**
* 微信登录
* @param userLoginDTO
* @return
*/
@PostMapping("/login")
@ApiOperation("微信登录")
public Result<UserLoginVO> login(@RequestBody UserLoginDTO userLoginDTO){
log.info("微信用户登录:{}",userLoginDTO.getCode());

//微信登录
User user = userService.wxLogin(userLoginDTO);//后绪步骤实现

//为微信用户生成jwt令牌
Map<String, Object> claims = new HashMap<>();
claims.put(JwtClaimsConstant.USER_ID,user.getId());
String token = JwtUtil.createJWT(jwtProperties.getUserSecretKey(), jwtProperties.getUserTtl(), claims);

UserLoginVO userLoginVO = UserLoginVO.builder()
.id(user.getId())
.openid(user.getOpenid())
.token(token)
.build();
return Result.success(userLoginVO);
}
}

5. Service层

5.1 Service层接口

创建UserService接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.sky.service;

import com.sky.dto.UserLoginDTO;
import com.sky.entity.User;

public interface UserService {

/**
* 微信登录
* @param userLoginDTO
* @return
*/
User wxLogin(UserLoginDTO userLoginDTO);
}

5.2 Service层实现类

**创建UserServiceImpl实现类:**实现获取微信用户的openid和微信登录功能

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
package com.sky.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.sky.constant.MessageConstant;
import com.sky.dto.UserLoginDTO;
import com.sky.entity.User;
import com.sky.exception.LoginFailedException;
import com.sky.mapper.UserMapper;
import com.sky.properties.WeChatProperties;
import com.sky.service.UserService;
import com.sky.utils.HttpClientUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

@Service
@Slf4j
public class UserServiceImpl implements UserService {

//微信服务接口地址
public static final String WX_LOGIN = "https://api.weixin.qq.com/sns/jscode2session";

@Autowired
private WeChatProperties weChatProperties;
@Autowired
private UserMapper userMapper;

/**
* 微信登录
* @param userLoginDTO
* @return
*/
public User wxLogin(UserLoginDTO userLoginDTO) {
String openid = getOpenid(userLoginDTO.getCode());

//判断openid是否为空,如果为空表示登录失败,抛出业务异常
if(openid == null){
throw new LoginFailedException(MessageConstant.LOGIN_FAILED);
}

//判断当前用户是否为新用户
User user = userMapper.getByOpenid(openid);

//如果是新用户,自动完成注册
if(user == null){
user = User.builder()
.openid(openid)
.createTime(LocalDateTime.now())
.build();
userMapper.insert(user);//后绪步骤实现
}

//返回这个用户对象
return user;
}

/**
* 调用微信接口服务,获取微信用户的openid
* @param code
* @return
*/
private String getOpenid(String code){
//调用微信接口服务,获得当前微信用户的openid
Map<String, String> map = new HashMap<>();
map.put("appid",weChatProperties.getAppid());
map.put("secret",weChatProperties.getSecret());
map.put("js_code",code);
map.put("grant_type","authorization_code");
String json = HttpClientUtil.doGet(WX_LOGIN, map);

JSONObject jsonObject = JSON.parseObject(json);
String openid = jsonObject.getString("openid");
return openid;
}
}

6. Mapper层

创建UserMapper接口:

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.sky.mapper;

import com.sky.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface UserMapper {

/**
* 根据openid查询用户
* @param openid
* @return
*/
@Select("select * from user where openid = #{openid}")
User getByOpenid(String openid);

/**
* 插入数据
* @param user
*/
void insert(User user);
}

创建UserMapper.xml映射文件:

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.UserMapper">

<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into user (openid, name, phone, sex, id_number, avatar, create_time)
values (#{openid}, #{name}, #{phone}, #{sex}, #{idNumber}, #{avatar}, #{createTime})
</insert>

</mapper>

7. 编写拦截器

**编写拦截器JwtTokenUserInterceptor:**统一拦截用户端发送的请求并进行jwt校验

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.sky.interceptor;

import com.sky.constant.JwtClaimsConstant;
import com.sky.context.BaseContext;
import com.sky.properties.JwtProperties;
import com.sky.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
* jwt令牌校验的拦截器
*/
@Component
@Slf4j
public class JwtTokenUserInterceptor implements HandlerInterceptor {

@Autowired
private JwtProperties jwtProperties;

/**
* 校验jwt
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判断当前拦截到的是Controller的方法还是其他资源
if (!(handler instanceof HandlerMethod)) {
//当前拦截到的不是动态方法,直接放行
return true;
}

//1、从请求头中获取令牌
String token = request.getHeader(jwtProperties.getUserTokenName());

//2、校验令牌
try {
log.info("jwt校验:{}", token);
Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token);
Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString());
log.info("当前用户的id:", userId);
BaseContext.setCurrentId(userId);
//3、通过,放行
return true;
} catch (Exception ex) {
//4、不通过,响应401状态码
response.setStatus(401);
return false;
}
}
}

在WebMvcConfiguration配置类中注册拦截器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Autowired
private JwtTokenUserInterceptor jwtTokenUserInterceptor;
/**
* 注册自定义拦截器
* @param registry
*/
protected void addInterceptors(InterceptorRegistry registry) {
log.info("开始注册自定义拦截器...");
//.........

registry.addInterceptor(jwtTokenUserInterceptor)
.addPathPatterns("/user/**")
.excludePathPatterns("/user/user/login")
.excludePathPatterns("/user/shop/status");
}

九. 商品浏览功能

这里导入相关代码

1. Mapper层

在SetmealMapper.java中添加list和getDishItemBySetmealId两个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 动态条件查询套餐
* @param setmeal
* @return
*/
List<Setmeal> list(Setmeal setmeal);

/**
* 根据套餐id查询菜品选项
* @param setmealId
* @return
*/
@Select("select sd.name, sd.copies, d.image, d.description " +
"from setmeal_dish sd left join dish d on sd.dish_id = d.id " +
"where sd.setmeal_id = #{setmealId}")
List<DishItemVO> getDishItemBySetmealId(Long setmealId);

创建SetmealMapper.xml文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.SetmealMapper">

<select id="list" parameterType="Setmeal" resultType="Setmeal">
select * from setmeal
<where>
<if test="name != null">
and name like concat('%',#{name},'%')
</if>
<if test="categoryId != null">
and category_id = #{categoryId}
</if>
<if test="status != null">
and status = #{status}
</if>
</where>
</select>
</mapper>

2. Service层

创建SetmealService.java

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

import com.sky.dto.SetmealDTO;
import com.sky.dto.SetmealPageQueryDTO;
import com.sky.entity.Setmeal;
import com.sky.result.PageResult;
import com.sky.vo.DishItemVO;
import com.sky.vo.SetmealVO;
import java.util.List;

public interface SetmealService {

/**
* 条件查询
* @param setmeal
* @return
*/
List<Setmeal> list(Setmeal setmeal);

/**
* 根据id查询菜品选项
* @param id
* @return
*/
List<DishItemVO> getDishItemById(Long id);

}

创建SetmealServiceImpl.java

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.sky.service.impl;

import com.sky.entity.Setmeal;
import com.sky.mapper.DishMapper;
import com.sky.mapper.SetmealDishMapper;
import com.sky.mapper.SetmealMapper;
import com.sky.service.SetmealService;
import com.sky.vo.DishItemVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
* 套餐业务实现
*/
@Service
@Slf4j
public class SetmealServiceImpl implements SetmealService {

@Autowired
private SetmealMapper setmealMapper;
@Autowired
private SetmealDishMapper setmealDishMapper;
@Autowired
private DishMapper dishMapper;

/**
* 条件查询
* @param setmeal
* @return
*/
public List<Setmeal> list(Setmeal setmeal) {
List<Setmeal> list = setmealMapper.list(setmeal);
return list;
}

/**
* 根据id查询菜品选项
* @param id
* @return
*/
public List<DishItemVO> getDishItemById(Long id) {
return setmealMapper.getDishItemBySetmealId(id);
}
}

在DishService.java中添加listWithFlavor方法定义

1
2
3
4
5
6
/**
* 条件查询菜品和口味
* @param dish
* @return
*/
List<DishVO> listWithFlavor(Dish dish);

在DishServiceImpl.java中实现listWithFlavor方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 条件查询菜品和口味
* @param dish
* @return
*/
public List<DishVO> listWithFlavor(Dish dish) {
List<Dish> dishList = dishMapper.list(dish);

List<DishVO> dishVOList = new ArrayList<>();

for (Dish d : dishList) {
DishVO dishVO = new DishVO();
BeanUtils.copyProperties(d,dishVO);

//根据菜品id查询对应的口味
List<DishFlavor> flavors = dishFlavorMapper.getByDishId(d.getId());

dishVO.setFlavors(flavors);
dishVOList.add(dishVO);
}

return dishVOList;
}

3. Controller层

创建DishController.java

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.sky.controller.user;

import com.sky.constant.StatusConstant;
import com.sky.entity.Dish;
import com.sky.result.Result;
import com.sky.service.DishService;
import com.sky.vo.DishVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;

@RestController("userDishController")
@RequestMapping("/user/dish")
@Slf4j
@Api(tags = "C端-菜品浏览接口")
public class DishController {
@Autowired
private DishService dishService;

/**
* 根据分类id查询菜品
*
* @param categoryId
* @return
*/
@GetMapping("/list")
@ApiOperation("根据分类id查询菜品")
public Result<List<DishVO>> list(Long categoryId) {
Dish dish = new Dish();
dish.setCategoryId(categoryId);
dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品

List<DishVO> list = dishService.listWithFlavor(dish);

return Result.success(list);
}

}

创建CategoryController.java

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
package com.sky.controller.user;

import com.sky.entity.Category;
import com.sky.result.Result;
import com.sky.service.CategoryService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;

@RestController("userCategoryController")
@RequestMapping("/user/category")
@Api(tags = "C端-分类接口")
public class CategoryController {

@Autowired
private CategoryService categoryService;

/**
* 查询分类
* @param type
* @return
*/
@GetMapping("/list")
@ApiOperation("查询分类")
public Result<List<Category>> list(Integer type) {
List<Category> list = categoryService.list(type);
return Result.success(list);
}
}

创建SetmealController.java

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.sky.controller.user;

import com.sky.constant.StatusConstant;
import com.sky.entity.Setmeal;
import com.sky.result.Result;
import com.sky.service.SetmealService;
import com.sky.vo.DishItemVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;

@RestController("userSetmealController")
@RequestMapping("/user/setmeal")
@Api(tags = "C端-套餐浏览接口")
public class SetmealController {
@Autowired
private SetmealService setmealService;

/**
* 条件查询
*
* @param categoryId
* @return
*/
@GetMapping("/list")
@ApiOperation("根据分类id查询套餐")
public Result<List<Setmeal>> list(Long categoryId) {
Setmeal setmeal = new Setmeal();
setmeal.setCategoryId(categoryId);
setmeal.setStatus(StatusConstant.ENABLE);

List<Setmeal> list = setmealService.list(setmeal);
return Result.success(list);
}

/**
* 根据套餐id查询包含的菜品列表
*
* @param id
* @return
*/
@GetMapping("/dish/{id}")
@ApiOperation("根据套餐id查询包含的菜品列表")
public Result<List<DishItemVO>> dishList(@PathVariable("id") Long id) {
List<DishItemVO> list = setmealService.getDishItemById(id);
return Result.success(list);
}
}

十. 缓存菜品和套餐

1. 缓存菜品

1.1 问题分析

用户端小程序展示的菜品数据都是通过查询数据库获得,如果用户端访问量比较大,数据库访问压力随之增大。通过Redis来缓存菜品数据,减少数据库查询操作

  • 每个分类下的菜品保存一份缓存数据
  • 数据库中菜品数据有变更时清理缓存数据

为了保证数据库和Redis中的数据保持一致,修改管理端接口 DishController 的相关方法,加入清理缓存逻辑。

需要改造的方法:

  • 新增菜品
  • 修改菜品
  • 批量删除菜品
  • 起售、停售菜品

1.2 用户端接口DishController

修改用户端接口 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
@Autowired
private RedisTemplate redisTemplate;
/**
* 根据分类id查询菜品
*
* @param categoryId
* @return
*/
@GetMapping("/list")
@ApiOperation("根据分类id查询菜品")
public Result<List<DishVO>> list(Long categoryId) {

//构造redis中的key,规则:dish_分类id
String key = "dish_" + categoryId;

//查询redis中是否存在菜品数据
List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key);
if(list != null && list.size() > 0){
//如果存在,直接返回,无须查询数据库
return Result.success(list);
}
////////////////////////////////////////////////////////
Dish dish = new Dish();
dish.setCategoryId(categoryId);
dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品

//如果不存在,查询数据库,将查询到的数据放入redis中
list = dishService.listWithFlavor(dish);
////////////////////////////////////////////////////////
redisTemplate.opsForValue().set(key, list);

return Result.success(list);
}

1.3 管理端接口DishController

1.3.1 抽取清理缓存的方法

在管理端DishController中添加

1
2
3
4
5
6
7
8
9
10
@Autowired
private RedisTemplate redisTemplate;
/**
* 清理缓存数据
* @param pattern
*/
private void cleanCache(String pattern){
Set keys = redisTemplate.keys(pattern);
redisTemplate.delete(keys);
}

1.3.2 新增菜品优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 新增菜品
*
* @param dishDTO
* @return
*/
@PostMapping
@ApiOperation("新增菜品")
public Result save(@RequestBody DishDTO dishDTO) {
log.info("新增菜品:{}", dishDTO);
dishService.saveWithFlavor(dishDTO);

//清理缓存数据
String key = "dish_" + dishDTO.getCategoryId();
cleanCache(key);
return Result.success();
}

1.3.3 菜品批量删除优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 菜品批量删除
*
* @param ids
* @return
*/
@DeleteMapping
@ApiOperation("菜品批量删除")
public Result delete(@RequestParam List<Long> ids) {
log.info("菜品批量删除:{}", ids);
dishService.deleteBatch(ids);

//将所有的菜品缓存数据清理掉,所有以dish_开头的key
cleanCache("dish_*");

return Result.success();
}

1.3.4 修改菜品优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 修改菜品
*
* @param dishDTO
* @return
*/
@PutMapping
@ApiOperation("修改菜品")
public Result update(@RequestBody DishDTO dishDTO) {
log.info("修改菜品:{}", dishDTO);
dishService.updateWithFlavor(dishDTO);

//将所有的菜品缓存数据清理掉,所有以dish_开头的key
cleanCache("dish_*");

return Result.success();
}

1.3.5 菜品起售停售优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 菜品起售停售
*
* @param status
* @param id
* @return
*/
@PostMapping("/status/{status}")
@ApiOperation("菜品起售停售")
public Result<String> startOrStop(@PathVariable Integer status, Long id) {
dishService.startOrStop(status, id);

//将所有的菜品缓存数据清理掉,所有以dish_开头的key
cleanCache("dish_*");

return Result.success();
}

2. 缓存套餐

2.1 Spring Cache

Spring Cache 是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能。

2.1.1 常用注解

在SpringCache中提供了很多缓存操作的注解,常见的是以下的几个:

注解 说明
@EnableCaching 开启缓存注解功能,通常加在启动类上
@Cacheable 在方法执行前先查询缓存中是否有数据,如果有数据,则直接返回缓存数据;如果没有缓存数据,调用方法并将方法返回值放到缓存中
@CachePut 将方法的返回值放到缓存中
@CacheEvict 将一条或多条数据从缓存中删除

2.1.2 @EnableCaching

启动类上加@EnableCaching:

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

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

@Slf4j
@SpringBootApplication
@EnableCaching//开启缓存注解功能
public class CacheDemoApplication {
public static void main(String[] args) {
SpringApplication.run(CacheDemoApplication.class,args);
log.info("项目启动成功...");
}
}

2.1.3 @CachePut注解

@CachePut 说明:

  • ​作用: 将方法返回值,放入缓存
  • ​value: 缓存的名称, 每个缓存名称下面可以有很多key
  • ​key: 缓存的key ———-> 支持Spring的表达式语言SPEL语法

在save方法上加注解@CachePut

当前UserController的save方法是用来保存用户信息的,我们希望在该用户信息保存到数据库的同时,也往缓存中缓存一份数据,我们可以在save方法上加上注解 @CachePut,用法如下:

1
2
3
4
5
6
7
8
9
10
11
/**
* CachePut:将方法返回值放入缓存
* value:缓存的名称,每个缓存名称下面可以有多个key
* key:缓存的key
*/
@PostMapping
@CachePut(value = "userCache", key = "#user.id")//key的生成:userCache::1
public User save(@RequestBody User user){
userMapper.insert(user);
return user;
}

**说明:**key的写法如下

#user.id : #user指的是方法形参的名称, id指的是user的id属性 , 也就是使用user的id属性作为key ;

2.1.4 @Cacheable注解

@Cacheable 说明:

  • ​作用: 在方法执行前,spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放到缓存中
  • ​value: 缓存的名称,每个缓存名称下面可以有多个key
  • ​key: 缓存的key ———-> 支持Spring的表达式语言SPEL语法

在getById上加注解@Cacheable

1
2
3
4
5
6
7
8
9
10
11
/**
* Cacheable:在方法执行前spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据, *调用方法并将方法返回值放到缓存中
* value:缓存的名称,每个缓存名称下面可以有多个key
* key:缓存的key
*/
@GetMapping
@Cacheable(cacheNames = "userCache",key="#id")
public User getById(Long id){
User user = userMapper.getById(id);
return user;
}

2.1.5 @CacheEvict注解

@CacheEvict 说明:

  • 作用: 清理指定缓存
  • value: 缓存的名称,每个缓存名称下面可以有多个key
  • key: 缓存的key ———-> 支持Spring的表达式语言SPEL语法

在 delete 方法上加注解@CacheEvict

1
2
3
4
5
6
7
8
9
10
11
@DeleteMapping
@CacheEvict(cacheNames = "userCache",key = "#id")//删除某个key对应的缓存数据
public void deleteById(Long id){
userMapper.deleteById(id);
}

@DeleteMapping("/delAll")
@CacheEvict(cacheNames = "userCache",allEntries = true)//删除userCache下所有的缓存数据
public void deleteAll(){
userMapper.deleteAll();
}

2.2 实现思路

实现步骤:

1). 导入Spring Cache和Redis相关maven坐标

2). 在启动类上加入@EnableCaching注解,开启缓存注解功能

3). 在用户端接口SetmealController的 list 方法上加入@Cacheable注解

4). 在管理端接口SetmealController的 save、delete、update、startOrStop等方法上加入CacheEvict注解

2.3 代码开发

2.3.1 @EnableCaching

在启动类上加入@EnableCaching注解,开启缓存注解功能

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

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@SpringBootApplication
@EnableTransactionManagement //开启注解方式的事务管理
@Slf4j
@EnableCaching
public class SkyApplication {
public static void main(String[] args) {
SpringApplication.run(SkyApplication.class, args);
log.info("server started");
}
}

2.3.2 用户端接口

在用户端接口SetmealController的 list 方法上加入@Cacheable注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 条件查询
*
* @param categoryId
* @return
*/
@GetMapping("/list")
@ApiOperation("根据分类id查询套餐")
@Cacheable(cacheNames = "setmealCache",key = "#categoryId") //key: setmealCache::100
public Result<List<Setmeal>> list(Long categoryId) {
Setmeal setmeal = new Setmeal();
setmeal.setCategoryId(categoryId);
setmeal.setStatus(StatusConstant.ENABLE);

List<Setmeal> list = setmealService.list(setmeal);
return Result.success(list);
}

2.3.3 管理端接口

在管理端接口SetmealController的 save、delete、update、startOrStop等方法上加入CacheEvict注解

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
/**
* 新增套餐
*
* @param setmealDTO
* @return
*/
@PostMapping
@ApiOperation("新增套餐")
@CacheEvict(cacheNames = "setmealCache",key = "#setmealDTO.categoryId")//key: setmealCache::100
public Result save(@RequestBody SetmealDTO setmealDTO) {
setmealService.saveWithDish(setmealDTO);
return Result.success();
}
/**
* 批量删除套餐
*
* @param ids
* @return
*/
@DeleteMapping
@ApiOperation("批量删除套餐")
@CacheEvict(cacheNames = "setmealCache",allEntries = true)
public Result delete(@RequestParam List<Long> ids) {
setmealService.deleteBatch(ids);
return Result.success();
}
/**
* 修改套餐
*
* @param setmealDTO
* @return
*/
@PutMapping
@ApiOperation("修改套餐")
@CacheEvict(cacheNames = "setmealCache",allEntries = true)
public Result update(@RequestBody SetmealDTO setmealDTO) {
setmealService.update(setmealDTO);
return Result.success();
}

/**
* 套餐起售停售
*
* @param status
* @param id
* @return
*/
@PostMapping("/status/{status}")
@ApiOperation("套餐起售停售")
@CacheEvict(cacheNames = "setmealCache",allEntries = true)
public Result startOrStop(@PathVariable Integer status, Long id) {
setmealService.startOrStop(status, id);
return Result.success();
}

十一. 购物车管理

1. 添加购物车

1.1 Controller层

根据添加购物车接口创建ShoppingCartController:

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
package com.sky.controller.user;


import com.sky.dto.ShoppingCartDTO;
import com.sky.result.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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;

/**
* 购物车
*/
@RestController
@RequestMapping("/user/shoppingCart")
@Slf4j
@Api(tags = "C端-购物车接口")
public class ShoppingCartController {

@Autowired
private ShoppingCartService shoppingCartService;

/**
* 添加购物车
* @param shoppingCartDTO
* @return
*/
@PostMapping("/add")
@ApiOperation("添加购物车")
public Result<String> add(@RequestBody ShoppingCartDTO shoppingCartDTO){
log.info("添加购物车:{}", shoppingCartDTO);
shoppingCartService.addShoppingCart(shoppingCartDTO);//后绪步骤实现
return Result.success();
}
}

1.2 Service层

1.2.1 Service层接口

创建ShoppingCartService接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.sky.service;

import com.sky.dto.ShoppingCartDTO;
import com.sky.entity.ShoppingCart;
import java.util.List;

public interface ShoppingCartService {

/**
* 添加购物车
* @param shoppingCartDTO
*/
void addShoppingCart(ShoppingCartDTO shoppingCartDTO);
}

1.2.2 Service层实现类

创建ShoppingCartServiceImpl实现类,并实现add方法:

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


import com.sky.context.BaseContext;
import com.sky.dto.ShoppingCartDTO;
import com.sky.entity.Dish;
import com.sky.entity.Setmeal;
import com.sky.entity.ShoppingCart;
import com.sky.mapper.DishMapper;
import com.sky.mapper.SetmealMapper;
import com.sky.service.ShoppingCartService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;

@Service
public class ShoppingCartServiceImpl implements ShoppingCartService {

@Autowired
private ShoppingCartMapper shoppingCartMapper;
@Autowired
private DishMapper dishMapper;
@Autowired
private SetmealMapper setmealMapper;

/**
* 添加购物车
*
* @param shoppingCartDTO
*/
public void addShoppingCart(ShoppingCartDTO shoppingCartDTO) {
ShoppingCart shoppingCart = new ShoppingCart();
BeanUtils.copyProperties(shoppingCartDTO, shoppingCart);
//只能查询自己的购物车数据
shoppingCart.setUserId(BaseContext.getCurrentId());

//判断当前商品是否在购物车中
List<ShoppingCart> shoppingCartList = shoppingCartMapper.list(shoppingCart);

if (shoppingCartList != null && shoppingCartList.size() == 1) {
//如果已经存在,就更新数量,数量加1
shoppingCart = shoppingCartList.get(0);
shoppingCart.setNumber(shoppingCart.getNumber() + 1);
shoppingCartMapper.updateNumberById(shoppingCart);
} else {
//如果不存在,插入数据,数量就是1

//判断当前添加到购物车的是菜品还是套餐
Long dishId = shoppingCartDTO.getDishId();
if (dishId != null) {
//添加到购物车的是菜品
Dish dish = dishMapper.getById(dishId);
shoppingCart.setName(dish.getName());
shoppingCart.setImage(dish.getImage());
shoppingCart.setAmount(dish.getPrice());
} else {
//添加到购物车的是套餐
Setmeal setmeal = setmealMapper.getById(shoppingCartDTO.getSetmealId());
shoppingCart.setName(setmeal.getName());
shoppingCart.setImage(setmeal.getImage());
shoppingCart.setAmount(setmeal.getPrice());
}
shoppingCart.setNumber(1);
shoppingCart.setCreateTime(LocalDateTime.now());
shoppingCartMapper.insert(shoppingCart);
}
}
}

1.3 Mapper层

创建ShoppingCartMapper接口:

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

import com.sky.entity.ShoppingCart;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Update;
import java.util.List;

@Mapper
public interface ShoppingCartMapper {
/**
* 条件查询
*
* @param shoppingCart
* @return
*/
List<ShoppingCart> list(ShoppingCart shoppingCart);

/**
* 更新商品数量
*
* @param shoppingCart
*/
@Update("update shopping_cart set number = #{number} where id = #{id}")
void updateNumberById(ShoppingCart shoppingCart);

/**
* 插入购物车数据
*
* @param shoppingCart
*/
@Insert("insert into shopping_cart (name, user_id, dish_id, setmeal_id, dish_flavor, number, amount, image, create_time) " +
" values (#{name},#{userId},#{dishId},#{setmealId},#{dishFlavor},#{number},#{amount},#{image},#{createTime})")
void insert(ShoppingCart shoppingCart);

}

创建ShoppingCartMapper.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.ShoppingCartMapper">
<select id="list" parameterType="ShoppingCart" resultType="ShoppingCart">
select * from shopping_cart
<where>
<if test="userId != null">
and user_id = #{userId}
</if>
<if test="dishId != null">
and dish_id = #{dishId}
</if>
<if test="setmealId != null">
and setmeal_id = #{setmealId}
</if>
<if test="dishFlavor != null">
and dish_flavor = #{dishFlavor}
</if>
</where>
order by create_time desc
</select>
</mapper>

2. 查看购物车

2.1 Controller层

在ShoppingCartController中创建查看购物车的方法:

1
2
3
4
5
6
7
8
9
/**
* 查看购物车
* @return
*/
@GetMapping("/list")
@ApiOperation("查看购物车")
public Result<List<ShoppingCart>> list(){
return Result.success(shoppingCartService.showShoppingCart());
}

2.2 Service层接口

在ShoppingCartService接口中声明查看购物车的方法:

1
2
3
4
5
/**
* 查看购物车
* @return
*/
List<ShoppingCart> showShoppingCart();

2.3 Service层实现类

在ShoppingCartServiceImpl中实现查看购物车的方法:

1
2
3
4
5
6
7
8
9
10
/**
* 查看购物车
* @return
*/
public List<ShoppingCart> showShoppingCart() {
return shoppingCartMapper.list(ShoppingCart.
builder().
userId(BaseContext.getCurrentId()).
build());
}

3. 清空购物车

3.1 Controller层

在ShoppingCartController中创建清空购物车的方法:

1
2
3
4
5
6
7
8
9
10
/**
* 清空购物车商品
* @return
*/
@DeleteMapping("/clean")
@ApiOperation("清空购物车商品")
public Result<String> clean(){
shoppingCartService.cleanShoppingCart();
return Result.success();
}

3.2 Service层接口

在ShoppingCartService接口中声明清空购物车的方法:

1
2
3
4
/**
* 清空购物车商品
*/
void cleanShoppingCart();

3.3 Service层实现类

在ShoppingCartServiceImpl中实现清空购物车的方法:

1
2
3
4
5
6
/**
* 清空购物车商品
*/
public void cleanShoppingCart() {
shoppingCartMapper.deleteByUserId(BaseContext.getCurrentId());
}

3.4 Mapper层

在ShoppingCartMapper接口中创建删除购物车数据的方法:

1
2
3
4
5
6
7
/**
* 根据用户id删除购物车数据
*
* @param userId
*/
@Delete("delete from shopping_cart where user_id = #{userId}")
void deleteByUserId(Long userId);

4. 删除购物车中单个商品

4.1 ShoppingCartController

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 删除购物车中一个商品
* @param shoppingCartDTO
* @return
*/
@PostMapping("/sub")
@ApiOperation("删除购物车中一个商品")
public Result sub(@RequestBody ShoppingCartDTO shoppingCartDTO){
log.info("删除购物车中一个商品,商品:{}", shoppingCartDTO);
shoppingCartService.subShoppingCart(shoppingCartDTO);
return Result.success();
}

4.2 ShoppingCartService

1
2
3
4
5
/**
* 删除购物车中一个商品
* @param shoppingCartDTO
*/
void subShoppingCart(ShoppingCartDTO shoppingCartDTO);

4.3 ShoppingCartServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 删除购物车中一个商品
* @param shoppingCartDTO
*/
public void subShoppingCart(ShoppingCartDTO shoppingCartDTO) {
ShoppingCart shoppingCart = new ShoppingCart();
BeanUtils.copyProperties(shoppingCartDTO,shoppingCart);
//设置查询条件,查询当前登录用户的购物车数据
shoppingCart.setUserId(BaseContext.getCurrentId());

List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);

if(list != null && list.size() > 0){
shoppingCart = list.get(0);

Integer number = shoppingCart.getNumber();
if(number == 1){
//当前商品在购物车中的份数为1,直接删除当前记录
shoppingCartMapper.deleteById(shoppingCart.getId());
}else {
//当前商品在购物车中的份数不为1,修改份数即可
shoppingCart.setNumber(shoppingCart.getNumber() - 1);
shoppingCartMapper.updateNumberById(shoppingCart);
}
}
}

4.4 ShoppingCartMapper

1
2
3
4
5
6
/**
* 根据id删除购物车数据
* @param id
*/
@Delete("delete from shopping_cart where id = #{id}")
void deleteById(Long id);

十二. 用户下单和订单支付

1. 用户下单

1.1 Controller层

创建OrderController并提供用户下单方法:

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.sky.controller.user;

import com.sky.dto.OrdersPaymentDTO;
import com.sky.dto.OrdersSubmitDTO;
import com.sky.result.PageResult;
import com.sky.result.Result;
import com.sky.service.OrderService;
import com.sky.vo.OrderPaymentVO;
import com.sky.vo.OrderSubmitVO;
import com.sky.vo.OrderVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
* 订单
*/
@RestController("userOrderController")
@RequestMapping("/user/order")
@Slf4j
@Api(tags = "C端-订单接口")
public class OrderController {

@Autowired
private OrderService orderService;

/**
* 用户下单
*
* @param ordersSubmitDTO
* @return
*/
@PostMapping("/submit")
@ApiOperation("用户下单")
public Result<OrderSubmitVO> submit(@RequestBody OrdersSubmitDTO ordersSubmitDTO) {
log.info("用户下单:{}", ordersSubmitDTO);
OrderSubmitVO orderSubmitVO = orderService.submitOrder(ordersSubmitDTO);
return Result.success(orderSubmitVO);
}

}

1.2 Service层

1.2.1 Service接口

创建OrderService接口,并声明用户下单方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.sky.service;

import com.sky.dto.*;
import com.sky.vo.OrderSubmitVO;

public interface OrderService {

/**
* 用户下单
* @param ordersSubmitDTO
* @return
*/
OrderSubmitVO submitOrder(OrdersSubmitDTO ordersSubmitDTO);
}

1.2.2 Service层实现类

创建OrderServiceImpl实现OrderService接口:

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
package com.sky.service.impl;

/**
* 订单
*/
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private OrderDetailMapper orderDetailMapper;
@Autowired
private ShoppingCartMapper shoppingCartMapper;
@Autowired
private AddressBookMapper addressBookMapper;


/**
* 用户下单
*
* @param ordersSubmitDTO
* @return
*/
@Transactional
public OrderSubmitVO submitOrder(OrdersSubmitDTO ordersSubmitDTO) {
//异常情况的处理(收货地址为空、超出配送范围、购物车为空)
AddressBook addressBook = addressBookMapper.getById(ordersSubmitDTO.getAddressBookId());
if (addressBook == null) {
throw new AddressBookBusinessException(MessageConstant.ADDRESS_BOOK_IS_NULL);
}

Long userId = BaseContext.getCurrentId();
ShoppingCart shoppingCart = new ShoppingCart();
shoppingCart.setUserId(userId);

//查询当前用户的购物车数据
List<ShoppingCart> shoppingCartList = shoppingCartMapper.list(shoppingCart);
if (shoppingCartList == null || shoppingCartList.size() == 0) {
throw new ShoppingCartBusinessException(MessageConstant.SHOPPING_CART_IS_NULL);
}

//构造订单数据
Orders order = new Orders();
BeanUtils.copyProperties(ordersSubmitDTO,order);
order.setPhone(addressBook.getPhone());
order.setAddress(addressBook.getDetail());
order.setConsignee(addressBook.getConsignee());
order.setNumber(String.valueOf(System.currentTimeMillis()));
order.setUserId(userId);
order.setStatus(Orders.PENDING_PAYMENT);
order.setPayStatus(Orders.UN_PAID);
order.setOrderTime(LocalDateTime.now());

//向订单表插入1条数据
orderMapper.insert(order);

//订单明细数据
List<OrderDetail> orderDetailList = new ArrayList<>();
for (ShoppingCart cart : shoppingCartList) {
OrderDetail orderDetail = new OrderDetail();
BeanUtils.copyProperties(cart, orderDetail);
orderDetail.setOrderId(order.getId());
orderDetailList.add(orderDetail);
}

//向明细表插入n条数据
orderDetailMapper.insertBatch(orderDetailList);

//清理购物车中的数据
shoppingCartMapper.deleteByUserId(userId);

//封装返回结果
OrderSubmitVO orderSubmitVO = OrderSubmitVO.builder()
.id(order.getId())
.orderNumber(order.getNumber())
.orderAmount(order.getAmount())
.orderTime(order.getOrderTime())
.build();

return orderSubmitVO;
}

}

1.3 Mapper层

1.3.1 创建OrderMapper接口和对应的xml映射文件

OrderMapper.java

1
2
3
4
5
6
7
8
9
10
11
package com.sky.mapper;

@Mapper
public interface OrderMapper {
/**
* 插入订单数据
* @param order
*/
void insert(Orders order);
}

OrderMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.OrderMapper">

<insert id="insert" parameterType="Orders" useGeneratedKeys="true" keyProperty="id">
insert into orders
(number, status, user_id, address_book_id, order_time, checkout_time, pay_method, pay_status, amount, remark,
phone, address, consignee, estimated_delivery_time, delivery_status, pack_amount, tableware_number,
tableware_status)
values (#{number}, #{status}, #{userId}, #{addressBookId}, #{orderTime}, #{checkoutTime}, #{payMethod},
#{payStatus}, #{amount}, #{remark}, #{phone}, #{address}, #{consignee},
#{estimatedDeliveryTime}, #{deliveryStatus}, #{packAmount}, #{tablewareNumber}, #{tablewareStatus})
</insert>
</mapper>

1.3.2 创建OrderDetailMapper接口和对应的xml映射文件

OrderDetailMapper.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.sky.mapper;

import com.sky.entity.OrderDetail;
import java.util.List;

@Mapper
public interface OrderDetailMapper {

/**
* 批量插入订单明细数据
* @param orderDetails
*/
void insertBatch(List<OrderDetail> orderDetails);

}

OrderDetailMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.OrderDetailMapper">

<insert id="insertBatch" parameterType="list">
insert into order_detail
(name, order_id, dish_id, setmeal_id, dish_flavor, number, amount, image)
values
<foreach collection="orderDetails" item="od" separator=",">
(#{od.name},#{od.orderId},#{od.dishId},#{od.setmealId},#{od.dishFlavor},
#{od.number},#{od.amount},#{od.image})
</foreach>
</insert>

</mapper>

2. 订单支付

2.1 微信支付流程

2.2 代码导入

2.2.1 Mapper层

在OrderMapper.java中添加getByNumberAndUserId和update两个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 根据订单号和用户id查询订单
* @param orderNumber
* @param userId
*/
@Select("select * from orders where number = #{orderNumber} and user_id= #{userId}")
Orders getByNumberAndUserId(String orderNumber, Long userId);

/**
* 修改订单信息
* @param orders
*/
void update(Orders orders);

在OrderMapper.xml中添加

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
<update id="update" parameterType="com.sky.entity.Orders">
update orders
<set>
<if test="cancelReason != null and cancelReason!='' ">
cancel_reason=#{cancelReason},
</if>
<if test="rejectionReason != null and rejectionReason!='' ">
rejection_reason=#{rejectionReason},
</if>
<if test="cancelTime != null">
cancel_time=#{cancelTime},
</if>
<if test="payStatus != null">
pay_status=#{payStatus},
</if>
<if test="payMethod != null">
pay_method=#{payMethod},
</if>
<if test="checkoutTime != null">
checkout_time=#{checkoutTime},
</if>
<if test="status != null">
status = #{status},
</if>
<if test="deliveryTime != null">
delivery_time = #{deliveryTime}
</if>
</set>
where id = #{id}
</update>

2.2.2 Service层

在OrderService.java中添加payment和paySuccess两个方法定义

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 订单支付
* @param ordersPaymentDTO
* @return
*/
OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) throws Exception;

/**
* 支付成功,修改订单状态
* @param outTradeNo
*/
void paySuccess(String outTradeNo);

在OrderServiceImpl.java中实现payment和paySuccess两个方法

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
	@Autowired
private UserMapper userMapper;
@Autowired
private WeChatPayUtil weChatPayUtil;
/**
* 订单支付
*
* @param ordersPaymentDTO
* @return
*/
public OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) throws Exception {
// 当前登录用户id
Long userId = BaseContext.getCurrentId();
User user = userMapper.getById(userId);

//调用微信支付接口,生成预支付交易单
JSONObject jsonObject = weChatPayUtil.pay(
ordersPaymentDTO.getOrderNumber(), //商户订单号
new BigDecimal(0.01), //支付金额,单位 元
"苍穹外卖订单", //商品描述
user.getOpenid() //微信用户的openid
);

if (jsonObject.getString("code") != null && jsonObject.getString("code").equals("ORDERPAID")) {
throw new OrderBusinessException("该订单已支付");
}

OrderPaymentVO vo = jsonObject.toJavaObject(OrderPaymentVO.class);
vo.setPackageStr(jsonObject.getString("package"));

return vo;
}

/**
* 支付成功,修改订单状态
*
* @param outTradeNo
*/
public void paySuccess(String outTradeNo) {
// 当前登录用户id
Long userId = BaseContext.getCurrentId();

// 根据订单号查询当前用户的订单
Orders ordersDB = orderMapper.getByNumberAndUserId(outTradeNo, userId);

// 根据订单id更新订单的状态、支付方式、支付状态、结账时间
Orders orders = Orders.builder()
.id(ordersDB.getId())
.status(Orders.TO_BE_CONFIRMED)
.payStatus(Orders.PAID)
.checkoutTime(LocalDateTime.now())
.build();

orderMapper.update(orders);
}

2.2.3 Controller层

在OrderController.java中添加payment方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 订单支付
*
* @param ordersPaymentDTO
* @return
*/
@PutMapping("/payment")
@ApiOperation("订单支付")
public Result<OrderPaymentVO> payment(@RequestBody OrdersPaymentDTO ordersPaymentDTO) throws Exception {
log.info("订单支付:{}", ordersPaymentDTO);
OrderPaymentVO orderPaymentVO = orderService.payment(ordersPaymentDTO);
log.info("生成预支付交易单:{}", orderPaymentVO);
return Result.success(orderPaymentVO);
}

导入PayNotifyController.java

2.3 跳过微信支付

跳过微信支付

十三. 用户端历史订单模块

1. 历史订单查询

1.1 user/OrderController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 历史订单查询
*
* @param page
* @param pageSize
* @param status 订单状态 1待付款 2待接单 3已接单 4派送中 5已完成 6已取消
* @return
*/
@GetMapping("/historyOrders")
@ApiOperation("历史订单查询")
public Result<PageResult> page(int page, int pageSize, Integer status) {
PageResult pageResult = orderService.pageQuery4User(page, pageSize, status);
return Result.success(pageResult);
}

1.2 OrderService

1
2
3
4
5
6
7
8
/**
* 用户端订单分页查询
* @param page
* @param pageSize
* @param status
* @return
*/
PageResult pageQuery4User(int page, int pageSize, Integer status);

1.3 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
/**
* 用户端订单分页查询
*
* @param pageNum
* @param pageSize
* @param status
* @return
*/
public PageResult pageQuery4User(int pageNum, int pageSize, Integer status) {
// 设置分页
PageHelper.startPage(pageNum, pageSize);

OrdersPageQueryDTO ordersPageQueryDTO = new OrdersPageQueryDTO();
ordersPageQueryDTO.setUserId(BaseContext.getCurrentId());
ordersPageQueryDTO.setStatus(status);

// 分页条件查询
Page<Orders> page = orderMapper.pageQuery(ordersPageQueryDTO);

List<OrderVO> list = new ArrayList();

// 查询出订单明细,并封装入OrderVO进行响应
if (page != null && page.getTotal() > 0) {
for (Orders orders : page) {
Long orderId = orders.getId();// 订单id

// 查询订单明细
List<OrderDetail> orderDetails = orderDetailMapper.getByOrderId(orderId);

OrderVO orderVO = new OrderVO();
BeanUtils.copyProperties(orders, orderVO);
orderVO.setOrderDetailList(orderDetails);

list.add(orderVO);
}
}
return new PageResult(page.getTotal(), list);
}

1.4 OrderMapper

1
2
3
4
5
/**
* 分页条件查询并按下单时间排序
* @param ordersPageQueryDTO
*/
Page<Orders> pageQuery(OrdersPageQueryDTO ordersPageQueryDTO);

1.5 OrderMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<select id="pageQuery" resultType="Orders">
select * from orders
<where>
<if test="number != null and number!=''">
and number like concat('%',#{number},'%')
</if>
<if test="phone != null and phone!=''">
and phone like concat('%',#{phone},'%')
</if>
<if test="userId != null">
and user_id = #{userId}
</if>
<if test="status != null">
and status = #{status}
</if>
<if test="beginTime != null">
and order_time &gt;= #{beginTime}
</if>
<if test="endTime != null">
and order_time &lt;= #{endTime}
</if>
</where>
order by order_time desc
</select>

1.6 OrderDetailMapper

1
2
3
4
5
6
7
/**
* 根据订单id查询订单明细
* @param orderId
* @return
*/
@Select("select * from order_detail where order_id = #{orderId}")
List<OrderDetail> getByOrderId(Long orderId);

2. 查询订单详情

2.1 user/OrderController

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 查询订单详情
*
* @param id
* @return
*/
@GetMapping("/orderDetail/{id}")
@ApiOperation("查询订单详情")
public Result<OrderVO> details(@PathVariable("id") Long id) {
OrderVO orderVO = orderService.details(id);
return Result.success(orderVO);
}

2.2 OrderService

1
2
3
4
5
6
/**
* 查询订单详情
* @param id
* @return
*/
OrderVO details(Long id);

2.3 OrderServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 查询订单详情
*
* @param id
* @return
*/
public OrderVO details(Long id) {
// 根据id查询订单
Orders orders = orderMapper.getById(id);

// 查询该订单对应的菜品/套餐明细
List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(orders.getId());

// 将该订单及其详情封装到OrderVO并返回
OrderVO orderVO = new OrderVO();
BeanUtils.copyProperties(orders, orderVO);
orderVO.setOrderDetailList(orderDetailList);

return orderVO;
}

2.4 OrderMapper

1
2
3
4
5
6
/**
* 根据id查询订单
* @param id
*/
@Select("select * from orders where id=#{id}")
Orders getById(Long id);

3. 取消订单

3.1 user/OrderController

1
2
3
4
5
6
7
8
9
10
11
/**
* 用户取消订单
*
* @return
*/
@PutMapping("/cancel/{id}")
@ApiOperation("取消订单")
public Result cancel(@PathVariable("id") Long id) throws Exception {
orderService.userCancelById(id);
return Result.success();
}

3.2 OrderService

1
2
3
4
5
/**
* 用户取消订单
* @param id
*/
void userCancelById(Long id) throws Exception;

3.3 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
40
41
/**
* 用户取消订单
*
* @param id
*/
public void userCancelById(Long id) throws Exception {
// 根据id查询订单
Orders ordersDB = orderMapper.getById(id);

// 校验订单是否存在
if (ordersDB == null) {
throw new OrderBusinessException(MessageConstant.ORDER_NOT_FOUND);
}

//订单状态 1待付款 2待接单 3已接单 4派送中 5已完成 6已取消
if (ordersDB.getStatus() > 2) {
throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
}

Orders orders = new Orders();
orders.setId(ordersDB.getId());

// 订单处于待接单状态下取消,需要进行退款
if (ordersDB.getStatus().equals(Orders.TO_BE_CONFIRMED)) {
//调用微信支付退款接口
weChatPayUtil.refund(
ordersDB.getNumber(), //商户订单号
ordersDB.getNumber(), //商户退款单号
new BigDecimal(0.01),//退款金额,单位 元
new BigDecimal(0.01));//原订单金额

//支付状态修改为 退款
orders.setPayStatus(Orders.REFUND);
}

// 更新订单状态、取消原因、取消时间
orders.setStatus(Orders.CANCELLED);
orders.setCancelReason("用户取消");
orders.setCancelTime(LocalDateTime.now());
orderMapper.update(orders);
}

4. 再来一单

4.1 user/OrderController

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 再来一单
*
* @param id
* @return
*/
@PostMapping("/repetition/{id}")
@ApiOperation("再来一单")
public Result repetition(@PathVariable Long id) {
orderService.repetition(id);
return Result.success();
}

4.2 OrderService

1
2
3
4
5
6
/**
* 再来一单
*
* @param id
*/
void repetition(Long id);

4.3 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
/**
* 再来一单
*
* @param id
*/
public void repetition(Long id) {
// 查询当前用户id
Long userId = BaseContext.getCurrentId();

// 根据订单id查询当前订单详情
List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(id);

// 将订单详情对象转换为购物车对象
List<ShoppingCart> shoppingCartList = orderDetailList.stream().map(x -> {
ShoppingCart shoppingCart = new ShoppingCart();

// 将原订单详情里面的菜品信息重新复制到购物车对象中
BeanUtils.copyProperties(x, shoppingCart, "id");
shoppingCart.setUserId(userId);
shoppingCart.setCreateTime(LocalDateTime.now());

return shoppingCart;
}).collect(Collectors.toList());

// 将购物车对象批量添加到数据库
shoppingCartMapper.insertBatch(shoppingCartList);
}

4.4 ShoppingCartMapper

1
2
3
4
5
6
/**
* 批量插入购物车数据
*
* @param shoppingCartList
*/
void insertBatch(List<ShoppingCart> shoppingCartList);

4.5 ShoppingCartMapper.xml

1
2
3
4
5
6
7
8
<insert id="insertBatch" parameterType="list">
insert into shopping_cart
(name, image, user_id, dish_id, setmeal_id, dish_flavor, number, amount, create_time)
values
<foreach collection="shoppingCartList" item="sc" separator=",">
(#{sc.name},#{sc.image},#{sc.userId},#{sc.dishId},#{sc.setmealId},#{sc.dishFlavor},#{sc.number},#{sc.amount},#{sc.createTime})
</foreach>
</insert>

十四. 商家端订单管理模块

1. 订单搜索

1.1 admin/OrderController

在admin包下创建OrderController

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
/**
* 订单管理
*/
@RestController("adminOrderController")
@RequestMapping("/admin/order")
@Slf4j
@Api(tags = "订单管理接口")
public class OrderController {

@Autowired
private OrderService orderService;

/**
* 订单搜索
*
* @param ordersPageQueryDTO
* @return
*/
@GetMapping("/conditionSearch")
@ApiOperation("订单搜索")
public Result<PageResult> conditionSearch(OrdersPageQueryDTO ordersPageQueryDTO) {
PageResult pageResult = orderService.conditionSearch(ordersPageQueryDTO);
return Result.success(pageResult);
}
}

1.2 OrderService

1
2
3
4
5
6
/**
* 条件搜索订单
* @param ordersPageQueryDTO
* @return
*/
PageResult conditionSearch(OrdersPageQueryDTO ordersPageQueryDTO);

1.3 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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/**
* 订单搜索
*
* @param ordersPageQueryDTO
* @return
*/
public PageResult conditionSearch(OrdersPageQueryDTO ordersPageQueryDTO) {
PageHelper.startPage(ordersPageQueryDTO.getPage(), ordersPageQueryDTO.getPageSize());

Page<Orders> page = orderMapper.pageQuery(ordersPageQueryDTO);

// 部分订单状态,需要额外返回订单菜品信息,将Orders转化为OrderVO
List<OrderVO> orderVOList = getOrderVOList(page);

return new PageResult(page.getTotal(), orderVOList);
}

private List<OrderVO> getOrderVOList(Page<Orders> page) {
// 需要返回订单菜品信息,自定义OrderVO响应结果
List<OrderVO> orderVOList = new ArrayList<>();

List<Orders> ordersList = page.getResult();
if (!CollectionUtils.isEmpty(ordersList)) {
for (Orders orders : ordersList) {
// 将共同字段复制到OrderVO
OrderVO orderVO = new OrderVO();
BeanUtils.copyProperties(orders, orderVO);
String orderDishes = getOrderDishesStr(orders);

// 将订单菜品信息封装到orderVO中,并添加到orderVOList
orderVO.setOrderDishes(orderDishes);
orderVOList.add(orderVO);
}
}
return orderVOList;
}

/**
* 根据订单id获取菜品信息字符串
*
* @param orders
* @return
*/
private String getOrderDishesStr(Orders orders) {
// 查询订单菜品详情信息(订单中的菜品和数量)
List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(orders.getId());

// 将每一条订单菜品信息拼接为字符串(格式:宫保鸡丁*3;)
List<String> orderDishList = orderDetailList.stream().map(x -> {
String orderDish = x.getName() + "*" + x.getNumber() + ";";
return orderDish;
}).collect(Collectors.toList());

// 将该订单对应的所有菜品信息拼接在一起
return String.join("", orderDishList);
}

2. 各个状态的订单数量统计

2.1 admin/OrderController

1
2
3
4
5
6
7
8
9
10
11
/**
* 各个状态的订单数量统计
*
* @return
*/
@GetMapping("/statistics")
@ApiOperation("各个状态的订单数量统计")
public Result<OrderStatisticsVO> statistics() {
OrderStatisticsVO orderStatisticsVO = orderService.statistics();
return Result.success(orderStatisticsVO);
}

2.2 OrderService

1
2
3
4
5
/**
* 各个状态的订单数量统计
* @return
*/
OrderStatisticsVO statistics();

2.3 OrderServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 各个状态的订单数量统计
*
* @return
*/
public OrderStatisticsVO statistics() {
// 根据状态,分别查询出待接单、待派送、派送中的订单数量
Integer toBeConfirmed = orderMapper.countStatus(Orders.TO_BE_CONFIRMED);
Integer confirmed = orderMapper.countStatus(Orders.CONFIRMED);
Integer deliveryInProgress = orderMapper.countStatus(Orders.DELIVERY_IN_PROGRESS);

// 将查询出的数据封装到orderStatisticsVO中响应
OrderStatisticsVO orderStatisticsVO = new OrderStatisticsVO();
orderStatisticsVO.setToBeConfirmed(toBeConfirmed);
orderStatisticsVO.setConfirmed(confirmed);
orderStatisticsVO.setDeliveryInProgress(deliveryInProgress);
return orderStatisticsVO;
}

2.4 OrderMapper

1
2
3
4
5
6
/**
* 根据状态统计订单数量
* @param status
*/
@Select("select count(id) from orders where status = #{status}")
Integer countStatus(Integer status);

3. 查询订单详情

3.1 admin/OrderController

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 订单详情
*
* @param id
* @return
*/
@GetMapping("/details/{id}")
@ApiOperation("查询订单详情")
public Result<OrderVO> details(@PathVariable("id") Long id) {
OrderVO orderVO = orderService.details(id);
return Result.success(orderVO);
}

4. 接单

4.1 admin/OrderController

1
2
3
4
5
6
7
8
9
10
11
/**
* 接单
*
* @return
*/
@PutMapping("/confirm")
@ApiOperation("接单")
public Result confirm(@RequestBody OrdersConfirmDTO ordersConfirmDTO) {
orderService.confirm(ordersConfirmDTO);
return Result.success();
}

4.2 OrderService

1
2
3
4
5
6
/**
* 接单
*
* @param ordersConfirmDTO
*/
void confirm(OrdersConfirmDTO ordersConfirmDTO);

4.3 OrderServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 接单
*
* @param ordersConfirmDTO
*/
public void confirm(OrdersConfirmDTO ordersConfirmDTO) {
Orders orders = Orders.builder()
.id(ordersConfirmDTO.getId())
.status(Orders.CONFIRMED)
.build();

orderMapper.update(orders);
}

5. 拒单

5.1 admin/OrderController

1
2
3
4
5
6
7
8
9
10
11
/**
* 拒单
*
* @return
*/
@PutMapping("/rejection")
@ApiOperation("拒单")
public Result rejection(@RequestBody OrdersRejectionDTO ordersRejectionDTO) throws Exception {
orderService.rejection(ordersRejectionDTO);
return Result.success();
}

5.2 OrderService

1
2
3
4
5
6
/**
* 拒单
*
* @param ordersRejectionDTO
*/
void rejection(OrdersRejectionDTO ordersRejectionDTO) throws Exception;

5.3 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
/**
* 拒单
*
* @param ordersRejectionDTO
*/
public void rejection(OrdersRejectionDTO ordersRejectionDTO) throws Exception {
// 根据id查询订单
Orders ordersDB = orderMapper.getById(ordersRejectionDTO.getId());

// 订单只有存在且状态为2(待接单)才可以拒单
if (ordersDB == null || !ordersDB.getStatus().equals(Orders.TO_BE_CONFIRMED)) {
throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
}

//支付状态
Integer payStatus = ordersDB.getPayStatus();
if (payStatus == Orders.PAID) {
//用户已支付,需要退款
String refund = weChatPayUtil.refund(
ordersDB.getNumber(),
ordersDB.getNumber(),
new BigDecimal(0.01),
new BigDecimal(0.01));
log.info("申请退款:{}", refund);
}

// 拒单需要退款,根据订单id更新订单状态、拒单原因、取消时间
Orders orders = new Orders();
orders.setId(ordersDB.getId());
orders.setStatus(Orders.CANCELLED);
orders.setRejectionReason(ordersRejectionDTO.getRejectionReason());
orders.setCancelTime(LocalDateTime.now());

orderMapper.update(orders);
}

6. 取消订单

6.1 admin/OrderController

1
2
3
4
5
6
7
8
9
10
11
/**
* 取消订单
*
* @return
*/
@PutMapping("/cancel")
@ApiOperation("取消订单")
public Result cancel(@RequestBody OrdersCancelDTO ordersCancelDTO) throws Exception {
orderService.cancel(ordersCancelDTO);
return Result.success();
}

6.2 OrderService

1
2
3
4
5
6
/**
* 商家取消订单
*
* @param ordersCancelDTO
*/
void cancel(OrdersCancelDTO ordersCancelDTO) throws Exception;

6.3 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
/**
* 取消订单
*
* @param ordersCancelDTO
*/
public void cancel(OrdersCancelDTO ordersCancelDTO) throws Exception {
// 根据id查询订单
Orders ordersDB = orderMapper.getById(ordersCancelDTO.getId());

//支付状态
Integer payStatus = ordersDB.getPayStatus();
if (payStatus == 1) {
//用户已支付,需要退款
String refund = weChatPayUtil.refund(
ordersDB.getNumber(),
ordersDB.getNumber(),
new BigDecimal(0.01),
new BigDecimal(0.01));
log.info("申请退款:{}", refund);
}

// 管理端取消订单需要退款,根据订单id更新订单状态、取消原因、取消时间
Orders orders = new Orders();
orders.setId(ordersCancelDTO.getId());
orders.setStatus(Orders.CANCELLED);
orders.setCancelReason(ordersCancelDTO.getCancelReason());
orders.setCancelTime(LocalDateTime.now());
orderMapper.update(orders);
}

7. 派送订单

7.1 admin/OrderController

1
2
3
4
5
6
7
8
9
10
11
/**
* 派送订单
*
* @return
*/
@PutMapping("/delivery/{id}")
@ApiOperation("派送订单")
public Result delivery(@PathVariable("id") Long id) {
orderService.delivery(id);
return Result.success();
}

7.2 OrderService

1
2
3
4
5
6
/**
* 派送订单
*
* @param id
*/
void delivery(Long id);

7.3 OrderServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 派送订单
*
* @param id
*/
public void delivery(Long id) {
// 根据id查询订单
Orders ordersDB = orderMapper.getById(id);

// 校验订单是否存在,并且状态为3
if (ordersDB == null || !ordersDB.getStatus().equals(Orders.CONFIRMED)) {
throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
}

Orders orders = new Orders();
orders.setId(ordersDB.getId());
// 更新订单状态,状态转为派送中
orders.setStatus(Orders.DELIVERY_IN_PROGRESS);

orderMapper.update(orders);
}

8. 完成订单

8.1 admin/OrderController

1
2
3
4
5
6
7
8
9
10
11
/**
* 完成订单
*
* @return
*/
@PutMapping("/complete/{id}")
@ApiOperation("完成订单")
public Result complete(@PathVariable("id") Long id) {
orderService.complete(id);
return Result.success();
}

8.2 OrderService

1
2
3
4
5
6
/**
* 完成订单
*
* @param id
*/
void complete(Long id);

8.3 OrderServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 完成订单
*
* @param id
*/
public void complete(Long id) {
// 根据id查询订单
Orders ordersDB = orderMapper.getById(id);

// 校验订单是否存在,并且状态为4
if (ordersDB == null || !ordersDB.getStatus().equals(Orders.DELIVERY_IN_PROGRESS)) {
throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
}

Orders orders = new Orders();
orders.setId(ordersDB.getId());
// 更新订单状态,状态转为完成
orders.setStatus(Orders.COMPLETED);
orders.setDeliveryTime(LocalDateTime.now());

orderMapper.update(orders);
}

十五. Spring Task 和 订单状态定时处理

1. Spring Task

1.1 介绍

  • Spring Task 是Spring框架提供的任务调度工具,可以按照约定的时间自动执行某个代码逻辑。
  • 作用定时自动执行某段Java代码

1.2 cron表达式

cron表达式其实就是一个字符串,通过cron表达式可以定义任务触发的时间

**构成规则:**分为6或7个域,由空格分隔开,每个域代表一个含义

每个域的含义分别为:秒、分钟、小时、日、月、周、年(可选)

举例:

2022年10月12日上午9点整 对应的cron表达式为:0 0 9 12 10 ? 2022

1.3 Spring Task使用步骤

  1. 导入maven坐标 spring-context
  2. 启动类添加注解 @EnableScheduling 开启任务调度
  3. 自定义定时任务类

2. 订单状态定时处理

2.1 问题分析

  • 下单后未支付,订单一直处于**“待支付”**状态
  • 用户收货后管理端未点击完成按钮,订单一直处于**“派送中”**状态

对于上面两种情况需要通过定时任务来修改订单状态,具体逻辑为:

  • 通过定时任务每分钟检查一次是否存在支付超时订单(下单后超过15分钟仍未支付则判定为支付超时订单),如果存在则修改订单状态为“已取消”
  • 通过定时任务每天凌晨1点检查一次是否存在“派送中”的订单,如果存在则修改订单状态为“已完成”

2.2 自定义定时任务类OrderTask

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

/**
* 自定义定时任务,实现订单状态定时处理
*/
@Component
@Slf4j
public class OrderTask {

@Autowired
private OrderMapper orderMapper;

/**
* 处理支付超时订单
*/
@Scheduled(cron = "0 * * * * ?")
public void processTimeoutOrder(){
log.info("处理支付超时订单:{}", new Date());

LocalDateTime time = LocalDateTime.now().plusMinutes(-15);

// select * from orders where status = 1 and order_time < 当前时间-15分钟
List<Orders> ordersList = orderMapper.getByStatusAndOrdertimeLT(Orders.PENDING_PAYMENT, time);
if(ordersList != null && ordersList.size() > 0){
ordersList.forEach(order -> {
order.setStatus(Orders.CANCELLED);
order.setCancelReason("支付超时,自动取消");
order.setCancelTime(LocalDateTime.now());
orderMapper.update(order);
});
}
}

/**
* 处理“派送中”状态的订单
*/
@Scheduled(cron = "0 0 1 * * ?")
public void processDeliveryOrder(){
log.info("处理派送中订单:{}", new Date());
// select * from orders where status = 4 and order_time < 当前时间-1小时
LocalDateTime time = LocalDateTime.now().plusMinutes(-60);
List<Orders> ordersList = orderMapper.getByStatusAndOrdertimeLT(Orders.DELIVERY_IN_PROGRESS, time);

if(ordersList != null && ordersList.size() > 0){
ordersList.forEach(order -> {
order.setStatus(Orders.COMPLETED);
orderMapper.update(order);
});
}
}
}

2.3 在OrderMapper接口中扩展方法

1
2
3
4
5
6
7
/**
* 根据状态和下单时间查询订单
* @param status
* @param orderTime
*/
@Select("select * from orders where status = #{status} and order_time < #{orderTime}")
List<Orders> getByStatusAndOrdertimeLT(Integer status, LocalDateTime orderTime);

十六. WebSocket 和 用户来单催单提醒

1. WebSocket

WebSocket 是基于 TCP 的一种新的网络协议。它实现了浏览器与服务器全双工通信——浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接, 并进行双向数据传输。

HTTP协议和WebSocket协议对比:

  • HTTP是短连接
  • WebSocket是长连接
  • HTTP通信是单向的,基于请求响应模式
  • WebSocket支持双向通信
  • HTTP和WebSocket底层都是TCP连接

2. 用户来单提醒

2.1 实现思路

  • 通过WebSocket实现管理端页面和服务端保持长连接状态
  • 当客户支付后,调用WebSocket的相关API实现服务端向客户端推送消息
  • 客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报
  • 约定服务端发送给客户端浏览器的数据格式为JSON,字段包括:type,orderId,content
    • type 为消息类型,1为来单提醒 2为客户催单
    • orderId 为订单id
    • content 为消息内容

2.2 代码开发

在OrderServiceImpl中注入WebSocketServer对象,修改paySuccess方法,加入如下代码:

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
@Autowired
private WebSocketServer webSocketServer;
/**
* 支付成功,修改订单状态
*
* @param outTradeNo
*/
public void paySuccess(String outTradeNo) {
// 当前登录用户id
Long userId = BaseContext.getCurrentId();

// 根据订单号查询当前用户的订单
Orders ordersDB = orderMapper.getByNumberAndUserId(outTradeNo, userId);

// 根据订单id更新订单的状态、支付方式、支付状态、结账时间
Orders orders = Orders.builder()
.id(ordersDB.getId())
.status(Orders.TO_BE_CONFIRMED)
.payStatus(Orders.PAID)
.checkoutTime(LocalDateTime.now())
.build();

orderMapper.update(orders);
//////////////////////////////////////////////
Map map = new HashMap();
map.put("type", 1);//消息类型,1表示来单提醒
map.put("orderId", orders.getId());
map.put("content", "订单号:" + outTradeNo);

//通过WebSocket实现来单提醒,向客户端浏览器推送消息
webSocketServer.sendToAllClient(JSON.toJSONString(map));
///////////////////////////////////////////////////
}

3. 用户催单提醒

3.1 Controller层

根据用户催单的接口定义,在user/OrderController中创建催单方法:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 用户催单
*
* @param id
* @return
*/
@GetMapping("/reminder/{id}")
@ApiOperation("用户催单")
public Result reminder(@PathVariable("id") Long id) {
orderService.reminder(id);
return Result.success();
}

3.2 Service层接口

在OrderService接口中声明reminder方法:

1
2
3
4
5
/**
* 用户催单
* @param id
*/
void reminder(Long id);

3.3 Service层实现类

在OrderServiceImpl中实现reminder方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 用户催单
*
* @param id
*/
public void reminder(Long id) {
// 查询订单是否存在
Orders orders = orderMapper.getById(id);
if (orders == null) {
throw new OrderBusinessException(MessageConstant.ORDER_NOT_FOUND);
}

//基于WebSocket实现催单
Map map = new HashMap();
map.put("type", 2);//2代表用户催单
map.put("orderId", id);
map.put("content", "订单号:" + orders.getNumber());
webSocketServer.sendToAllClient(JSON.toJSONString(map));
}

十七. 数据统计功能

1. 营业额统计

1.1 Controller层

根据接口定义创建ReportController:

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
package com.sky.controller.admin;

import com.sky.result.Result;
import com.sky.service.ReportService;
import com.sky.vo.TurnoverReportVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDate;

/**
* 报表
*/
@RestController
@RequestMapping("/admin/report")
@Slf4j
@Api(tags = "统计报表相关接口")
public class ReportController {

@Autowired
private ReportService reportService;

/**
* 营业额数据统计
*
* @param begin
* @param end
* @return
*/
@GetMapping("/turnoverStatistics")
@ApiOperation("营业额数据统计")
public Result<TurnoverReportVO> turnoverStatistics(
@DateTimeFormat(pattern = "yyyy-MM-dd")
LocalDate begin,
@DateTimeFormat(pattern = "yyyy-MM-dd")
LocalDate end) {
return Result.success(reportService.getTurnover(begin, end));
}

}

1.2 Service层接口

创建ReportService接口,声明getTurnover方法:

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

import com.sky.vo.TurnoverReportVO;
import java.time.LocalDate;

public interface ReportService {

/**
* 根据时间区间统计营业额
* @param beginTime
* @param endTime
* @return
*/
TurnoverReportVO getTurnover(LocalDate beginTime, LocalDate endTime);
}

1.3 Service层实现类

创建ReportServiceImpl实现类,实现getTurnover方法:

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
package com.sky.service.impl;

import com.sky.entity.Orders;
import com.sky.mapper.OrderMapper;
import com.sky.service.ReportService;
import com.sky.vo.TurnoverReportVO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service
@Slf4j
public class ReportServiceImpl implements ReportService {

@Autowired
private OrderMapper orderMapper;

/**
* 根据时间区间统计营业额
* @param begin
* @param end
* @return
*/
public TurnoverReportVO getTurnover(LocalDate begin, LocalDate end) {
List<LocalDate> dateList = new ArrayList<>();
dateList.add(begin);

while (!begin.equals(end)){
begin = begin.plusDays(1);//日期计算,获得指定日期后1天的日期
dateList.add(begin);
}

List<Double> turnoverList = new ArrayList<>();
for (LocalDate date : dateList) {
LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);
LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);
Map map = new HashMap();
map.put("status", Orders.COMPLETED);
map.put("begin",beginTime);
map.put("end", endTime);
Double turnover = orderMapper.sumByMap(map);
turnover = turnover == null ? 0.0 : turnover;
turnoverList.add(turnover);
}

//数据封装
return TurnoverReportVO.builder()
.dateList(StringUtils.join(dateList,","))
.turnoverList(StringUtils.join(turnoverList,","))
.build();
}
}

1.4 Mapper层

在OrderMapper接口声明sumByMap方法:

1
2
3
4
5
/**
* 根据动态条件统计营业额
* @param map
*/
Double sumByMap(Map map);

在OrderMapper.xml文件中编写动态SQL:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<select id="sumByMap" resultType="java.lang.Double">
select sum(amount) from orders
<where>
<if test="status != null">
and status = #{status}
</if>
<if test="begin != null">
and order_time &gt;= #{begin}
</if>
<if test="end != null">
and order_time &lt;= #{end}
</if>
</where>
</select>

2. 用户统计

2.1 Controller层

根据接口定义,在ReportController中创建userStatistics方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 用户数据统计
* @param begin
* @param end
* @return
*/
@GetMapping("/userStatistics")
@ApiOperation("用户数据统计")
public Result<UserReportVO> userStatistics(
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end){

return Result.success(reportService.getUserStatistics(begin,end));
}

2.2 Service层接口

在ReportService接口中声明getUserStatistics方法:

1
2
3
4
5
6
7
/**
* 根据时间区间统计用户数量
* @param begin
* @param end
* @return
*/
UserReportVO getUserStatistics(LocalDate begin, LocalDate end);

2.3 Service层实现类

在ReportServiceImpl实现类中实现getUserStatistics方法:

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
@Override
public UserReportVO getUserStatistics(LocalDate begin, LocalDate end) {
List<LocalDate> dateList = new ArrayList<>();
dateList.add(begin);

while (!begin.equals(end)){
begin = begin.plusDays(1);
dateList.add(begin);
}
List<Integer> newUserList = new ArrayList<>(); //新增用户数
List<Integer> totalUserList = new ArrayList<>(); //总用户数

for (LocalDate date : dateList) {
LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);
LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);
//新增用户数量 select count(id) from user where create_time > ? and create_time < ?
Integer newUser = getUserCount(beginTime, endTime);
//总用户数量 select count(id) from user where create_time < ?
Integer totalUser = getUserCount(null, endTime);

newUserList.add(newUser);
totalUserList.add(totalUser);
}

return UserReportVO.builder()
.dateList(StringUtils.join(dateList,","))
.newUserList(StringUtils.join(newUserList,","))
.totalUserList(StringUtils.join(totalUserList,","))
.build();
}

在ReportServiceImpl实现类中创建私有方法getUserCount:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 根据时间区间统计用户数量
* @param beginTime
* @param endTime
* @return
*/
private Integer getUserCount(LocalDateTime beginTime, LocalDateTime endTime) {
Map map = new HashMap();
map.put("begin",beginTime);
map.put("end", endTime);
return userMapper.countByMap(map);
}

2.4 Mapper层

在UserMapper接口中声明countByMap方法:

1
2
3
4
5
6
/**
* 根据动态条件统计用户数量
* @param map
* @return
*/
Integer countByMap(Map map);

在UserMapper.xml文件中编写动态SQL:

1
2
3
4
5
6
7
8
9
10
11
<select id="countByMap" resultType="java.lang.Integer">
select count(id) from user
<where>
<if test="begin != null">
and create_time &gt;= #{begin}
</if>
<if test="end != null">
and create_time &lt;= #{end}
</if>
</where>
</select>

3. 订单统计

3.1 Controller层

在ReportController中根据订单统计接口创建orderStatistics方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 订单数据统计
* @param begin
* @param end
* @return
*/
@GetMapping("/ordersStatistics")
@ApiOperation("用户数据统计")
public Result<OrderReportVO> orderStatistics(
@DateTimeFormat(pattern = "yyyy-MM-dd")
LocalDate begin,
@DateTimeFormat(pattern = "yyyy-MM-dd")
LocalDate end){

return Result.success(reportService.getOrderStatistics(begin,end));
}

3.2 Service层接口

在ReportService接口中声明getOrderStatistics方法:

1
2
3
4
5
6
7
/**
* 根据时间区间统计订单数量
* @param begin
* @param end
* @return
*/
OrderReportVO getOrderStatistics(LocalDate begin, LocalDate end);

3.3 Service层实现类

在ReportServiceImpl实现类中实现getOrderStatistics方法:

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
/**
* 根据时间区间统计订单数量
* @param begin
* @param end
* @return
*/
public OrderReportVO getOrderStatistics(LocalDate begin, LocalDate end){
List<LocalDate> dateList = new ArrayList<>();
dateList.add(begin);

while (!begin.equals(end)){
begin = begin.plusDays(1);
dateList.add(begin);
}
//每天订单总数集合
List<Integer> orderCountList = new ArrayList<>();
//每天有效订单数集合
List<Integer> validOrderCountList = new ArrayList<>();
for (LocalDate date : dateList) {
LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);
LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);
//查询每天的总订单数 select count(id) from orders where order_time > ? and order_time < ?
Integer orderCount = getOrderCount(beginTime, endTime, null);

//查询每天的有效订单数 select count(id) from orders where order_time > ? and order_time < ? and status = ?
Integer validOrderCount = getOrderCount(beginTime, endTime, Orders.COMPLETED);

orderCountList.add(orderCount);
validOrderCountList.add(validOrderCount);
}

//时间区间内的总订单数
Integer totalOrderCount = orderCountList.stream().reduce(Integer::sum).get();
//时间区间内的总有效订单数
Integer validOrderCount = validOrderCountList.stream().reduce(Integer::sum).get();
//订单完成率
Double orderCompletionRate = 0.0;
if(totalOrderCount != 0){
orderCompletionRate = validOrderCount.doubleValue() / totalOrderCount;
}
return OrderReportVO.builder()
.dateList(StringUtils.join(dateList, ","))
.orderCountList(StringUtils.join(orderCountList, ","))
.validOrderCountList(StringUtils.join(validOrderCountList, ","))
.totalOrderCount(totalOrderCount)
.validOrderCount(validOrderCount)
.orderCompletionRate(orderCompletionRate)
.build();

}

在ReportServiceImpl实现类中提供私有方法getOrderCount:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 根据时间区间统计指定状态的订单数量
* @param beginTime
* @param endTime
* @param status
* @return
*/
private Integer getOrderCount(LocalDateTime beginTime, LocalDateTime endTime, Integer status) {
Map map = new HashMap();
map.put("status", status);
map.put("begin",beginTime);
map.put("end", endTime);
return orderMapper.countByMap(map);
}

3.4 Mapper层

在OrderMapper接口中声明countByMap方法:

1
2
3
4
5
/**
*根据动态条件统计订单数量
* @param map
*/
Integer countByMap(Map map);

在OrderMapper.xml文件中编写动态SQL:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<select id="countByMap" resultType="java.lang.Integer">
select count(id) from orders
<where>
<if test="status != null">
and status = #{status}
</if>
<if test="begin != null">
and order_time &gt;= #{begin}
</if>
<if test="end != null">
and order_time &lt;= #{end}
</if>
</where>
</select>

4. 销量排名Top10

4.1 Controller层

在ReportController中根据销量排名接口创建top10方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 销量排名统计
* @param begin
* @param end
* @return
*/
@GetMapping("/top10")
@ApiOperation("销量排名统计")
public Result<SalesTop10ReportVO> top10(
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end){
return Result.success(reportService.getSalesTop10(begin,end));
}

4.2 Service层接口

在ReportService接口中声明getSalesTop10方法:

1
2
3
4
5
6
7
/**
* 查询指定时间区间内的销量排名top10
* @param begin
* @param end
* @return
*/
SalesTop10ReportVO getSalesTop10(LocalDate begin, LocalDate end);

4.3 Service层实现类

在ReportServiceImpl实现类中实现getSalesTop10方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 查询指定时间区间内的销量排名top10
* @param begin
* @param end
* @return
* */
public SalesTop10ReportVO getSalesTop10(LocalDate begin, LocalDate end){
LocalDateTime beginTime = LocalDateTime.of(begin, LocalTime.MIN);
LocalDateTime endTime = LocalDateTime.of(end, LocalTime.MAX);
List<GoodsSalesDTO> goodsSalesDTOList = orderMapper.getSalesTop10(beginTime, endTime);

String nameList = StringUtils.join(goodsSalesDTOList.stream().map(GoodsSalesDTO::getName).collect(Collectors.toList()),",");
String numberList = StringUtils.join(goodsSalesDTOList.stream().map(GoodsSalesDTO::getNumber).collect(Collectors.toList()),",");

return SalesTop10ReportVO.builder()
.nameList(nameList)
.numberList(numberList)
.build();
}

4.4 Mapper层

在OrderMapper接口中声明getSalesTop10方法:

1
2
3
4
5
6
/**
* 查询商品销量排名
* @param begin
* @param end
*/
List<GoodsSalesDTO> getSalesTop10(LocalDateTime begin, LocalDateTime end);

在OrderMapper.xml文件中编写动态SQL:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<select id="getSalesTop10" resultType="com.sky.dto.GoodsSalesDTO">
select od.name name,sum(od.number) number from order_detail od ,orders o
where od.order_id = o.id
and o.status = 5
<if test="begin != null">
and order_time &gt;= #{begin}
</if>
<if test="end != null">
and order_time &lt;= #{end}
</if>
group by name
order by number desc
limit 0, 10
</select>

十八. Apache POI 和 导出运营数据

1. Apache POI

一般情况下,POI 都是用于操作 Excel 文件。

1.1 将数据写入Excel文件

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

import org.apache.poi.xssf.usermodel.XSSFCell;
import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;

public class POITest {

/**
* 基于POI向Excel文件写入数据
* @throws Exception
*/
public static void write() throws Exception{
//在内存中创建一个Excel文件对象
XSSFWorkbook excel = new XSSFWorkbook();
//创建Sheet页
XSSFSheet sheet = excel.createSheet("itcast");

//在Sheet页中创建行,0表示第1行
XSSFRow row1 = sheet.createRow(0);
//创建单元格并在单元格中设置值,单元格编号也是从0开始,1表示第2个单元格
row1.createCell(1).setCellValue("姓名");
row1.createCell(2).setCellValue("城市");

XSSFRow row2 = sheet.createRow(1);
row2.createCell(1).setCellValue("张三");
row2.createCell(2).setCellValue("北京");

XSSFRow row3 = sheet.createRow(2);
row3.createCell(1).setCellValue("李四");
row3.createCell(2).setCellValue("上海");

FileOutputStream out = new FileOutputStream(new File("D:\\itcast.xlsx"));
//通过输出流将内存中的Excel文件写入到磁盘上
excel.write(out);

//关闭资源
out.flush();
out.close();
excel.close();
}
public static void main(String[] args) throws Exception {
write();
}
}

1.2 读取Excel文件中的数据

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

import org.apache.poi.xssf.usermodel.XSSFCell;
import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;

public class POITest {
/**
* 基于POI读取Excel文件
* @throws Exception
*/
public static void read() throws Exception{
FileInputStream in = new FileInputStream(new File("D:\\itcast.xlsx"));
//通过输入流读取指定的Excel文件
XSSFWorkbook excel = new XSSFWorkbook(in);
//获取Excel文件的第1个Sheet页
XSSFSheet sheet = excel.getSheetAt(0);

//获取Sheet页中的最后一行的行号
int lastRowNum = sheet.getLastRowNum();

for (int i = 0; i <= lastRowNum; i++) {
//获取Sheet页中的行
XSSFRow titleRow = sheet.getRow(i);
//获取行的第2个单元格
XSSFCell cell1 = titleRow.getCell(1);
//获取单元格中的文本内容
String cellValue1 = cell1.getStringCellValue();
//获取行的第3个单元格
XSSFCell cell2 = titleRow.getCell(2);
//获取单元格中的文本内容
String cellValue2 = cell2.getStringCellValue();

System.out.println(cellValue1 + " " +cellValue2);
}

//关闭资源
in.close();
excel.close();
}

public static void main(String[] args) throws Exception {
read();
}
}

2. 导出运营数据Excel报表

2.1 实现步骤

  1. 设计Excel模板文件
  2. 查询近30天的运营数据
  3. 将查询到的运营数据写入模板文件
  4. 通过输出流将Excel文件下载到客户端浏览器

2.2 Controller层

根据接口定义,在ReportController中创建export方法:

1
2
3
4
5
6
7
8
9
/**
* 导出运营数据报表
* @param response
*/
@GetMapping("/export")
@ApiOperation("导出运营数据报表")
public void export(HttpServletResponse response){
reportService.exportBusinessData(response);
}

2.3 Service层接口

在ReportService接口中声明导出运营数据报表的方法:

1
2
3
4
5
/**
* 导出近30天的运营数据报表
* @param response
**/
void exportBusinessData(HttpServletResponse response);

2.4 Service层实现类

在ReportServiceImpl实现类中实现导出运营数据报表的方法:

提前将资料中的运营数据报表模板.xlsx拷贝到项目的resources/template目录中

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
/**导出近30天的运营数据报表
* @param response
**/
public void exportBusinessData(HttpServletResponse response) {
LocalDate begin = LocalDate.now().minusDays(30);
LocalDate end = LocalDate.now().minusDays(1);
//查询概览运营数据,提供给Excel模板文件
BusinessDataVO businessData = workspaceService.getBusinessData(LocalDateTime.of(begin,LocalTime.MIN), LocalDateTime.of(end, LocalTime.MAX));
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("template/运营数据报表模板.xlsx");
try {
//基于提供好的模板文件创建一个新的Excel表格对象
XSSFWorkbook excel = new XSSFWorkbook(inputStream);
//获得Excel文件中的一个Sheet页
XSSFSheet sheet = excel.getSheet("Sheet1");

sheet.getRow(1).getCell(1).setCellValue(begin + "至" + end);
//获得第4行
XSSFRow row = sheet.getRow(3);
//获取单元格
row.getCell(2).setCellValue(businessData.getTurnover());
row.getCell(4).setCellValue(businessData.getOrderCompletionRate());
row.getCell(6).setCellValue(businessData.getNewUsers());
row = sheet.getRow(4);
row.getCell(2).setCellValue(businessData.getValidOrderCount());
row.getCell(4).setCellValue(businessData.getUnitPrice());
for (int i = 0; i < 30; i++) {
LocalDate date = begin.plusDays(i);
//准备明细数据
businessData = workspaceService.getBusinessData(LocalDateTime.of(date,LocalTime.MIN), LocalDateTime.of(date, LocalTime.MAX));
row = sheet.getRow(7 + i);
row.getCell(1).setCellValue(date.toString());
row.getCell(2).setCellValue(businessData.getTurnover());
row.getCell(3).setCellValue(businessData.getValidOrderCount());
row.getCell(4).setCellValue(businessData.getOrderCompletionRate());
row.getCell(5).setCellValue(businessData.getUnitPrice());
row.getCell(6).setCellValue(businessData.getNewUsers());
}
//通过输出流将文件下载到客户端浏览器中
ServletOutputStream out = response.getOutputStream();
excel.write(out);
//关闭资源
out.flush();
out.close();
excel.close();

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

END