2025-04-17-Thu-T-苍穹外卖
1. 项目概述和环境搭建
1.1 软件开发整体介绍
1.1.1 软件开发流程
- 需求分析
- 需求规格说明书(PRD)
- 产品原型(静态网页)
- 设计
- UI设计
- 数据库设计
- 接口设计
- 编码
- 编写代码
- 单元测试
- 测试
- 测试用例
- 测试报告
- 上线运维
- 软件环境安装、配置
1.1.2 角色分工

1.1.3 软件环境
- 开发环境: 开发人员在开发阶段使用的环境,一般对外部用户无法访问
- 测试环境: 专门给测试人员使用的环境,用于测试项目,一般对外部用户无法访问
- 生产环境: 即线上环境,正式提供对外服务的环境
1.2 苍穹外卖项目介绍
1.2.1 项目介绍
-
定位: 专门为餐饮企业定制的一款软件产品
-
结构
- 管理端:商家使用
- 用户端:点餐用户使用
-
功能架构: 体现项目中业务功能模块

1.2.2 产品原型
静态的HTML页面,用于展示业务功能
1.2.3 技术选型
技术选型: 展示项目中使用到的技术框架和中间件等

1.3 开发环境搭建
整体结构

1.3.1 前端环境搭建

1.3.2 后端环境搭建
后端工程基于maven进行项目构建,并进行分模块开发

- entity:实体,通常和数据库中的表对应
- DTO: 数据传输对象,通常用于程序中各层之间传递数据
- VO:视图对象,为前端展示数据提供的对象
- POJO:普通Java对象,只有属性和对应的getter和setter
使用Git进行版本控制:
- 创建git本地仓库
- 创建git远程仓库
- 将本地文件push到git远程仓库
数据库环境搭建:

前后端联调



浏览器 --> Nginx--> 后端服务器
正向代理: 隐藏了客户端的真实地址(VPN)
反向代理:隐藏了服务端的真实地址
Nginx反向代理的好处:
- 提高访问速度:客户端访问nginx时,nginx中可以做缓存,如果是请求的同一个地址,就不用真正的去请求后端服务,而是直接请求nginx,nginx可以直接返回所需缓存数据
- 进行负载均衡:按照指定的方式分发给不同的服务器
- 保证后端服务器安全
反向代理:

负载均衡:


1.3.3 完善登录功能
问题:员工表中的密码是明文存储的,安全性太低
步骤:
- 将密码加密后进行存储,提高安全性
- 使用MD5加密方式对明文密码加密
- 修改数据库中的明文密码,改为MD5加密后的密文
- 修改java代码,前端提交的密码进行MD5加密后再与数据库中的密码比对

1.4 导入接口文档
1.4.1 前后端分离开发流程

1.4.2 操作步骤
本项目使用API fox
将接口的json文件导入APIFOX
1.5 Swagger
1.5.1 介绍
使用Swagger只需要按照它的规范去定义接口及接口相关的信息,就可以做到生成接口文档,以及在线接口调试界面。
Knife4j是为Java MVC框架集成Swagger生成API文档的增强解决方案
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.2</version>
</dependency>
1.5.2 使用方式
- 导入knife4j的maven坐标
- 在配置类中加入knife4j相关配置
/**
* 通过knife4j生成接口文档
* @return
*/
@Bean
public Docket docket() {
ApiInfo apiInfo = new ApiInfoBuilder()
.title("苍穹外卖项目接口文档")
.version("2.0")
.description("苍穹外卖项目接口文档")
.build();
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo)
.select()
.apis(RequestHandlerSelectors.basePackage("com.sky.controller"))
.paths(PathSelectors.any())
.build();
return docket;
}
- 设置静态资源映射,否则接口文档页面无法访问
/**
* 设置静态资源映射
* @param registry
*/
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
ApiFox导入的接口文档是设计阶段产出的,用于管理和维护接口
Swagger是在开发阶段使用的框架,帮助开发人员做后端的接口测试
1.5.3 Swagger常用注解
- @Api:用在类上,例如Controller,表示对类的说明
- @ApiModel:用在类上,例如entity,DTO,VO
- @ApiModelProperty:用在属性上,描述属性信息
- @ApiOperation:用在方法上,例如Controller的方法,说明方法的用途,作用
2. 员工管理和分类管理
2.1 新增员工
2.1.1 需求分析和设计
-
产品原型

-
接口设计

- 数据库设计

DROP TABLE IF EXISTS `employee`;
CREATE TABLE `employee` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(32) COLLATE utf8_bin NOT NULL COMMENT '姓名',
`username` varchar(32) COLLATE utf8_bin NOT NULL COMMENT '用户名',
`password` varchar(64) COLLATE utf8_bin NOT NULL COMMENT '密码',
`phone` varchar(11) COLLATE utf8_bin NOT NULL COMMENT '手机号',
`sex` varchar(2) COLLATE utf8_bin NOT NULL COMMENT '性别',
`id_number` varchar(18) COLLATE utf8_bin NOT NULL COMMENT '身份证号',
`status` int NOT NULL DEFAULT '1' COMMENT '状态 0:禁用,1:启用',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`create_user` bigint DEFAULT NULL COMMENT '创建人',
`update_user` bigint DEFAULT NULL COMMENT '修改人',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin COMMENT='员工信息';
AUTO_INCREMENT=2: 从2开始自增
2.1.2 代码开发


2.1.3 功能测试
功能测试方法
- 通过接口文档测试
- 通过前后端联调测试
注意: 由于开发阶段前端和后端是并行开发的,后端完成某个功能之后,此时前端对应的功能可能还没有完成,导致无法进行前后端联调测试。所以在开发阶段,后端测试主要以接口文档测试为主
2.1.4 代码完善
程序存在的问题:
- 录入的用户名如果已经存在,抛出的异常没有处理
- 添加全局异常方法,捕获sql异常
/**
* 处理sql异常
* @param ex
* @return
*/
@ExceptionHandler
public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){
//Duplicate entry 'lisi' for key 'employee.idx_username'
String message = ex.getMessage();
if(message.contains("Duplicate entry")){
String[] s = message.split(" ");
String username = s[2];
String msg = username + MessageConstant.ALREADY_EXISTS;
return Result.error(msg);
}else {
return Result.error(MessageConstant.UNKNOWN_ERROR);
}
}
- 新增员工时,创建人id和修改人id设置了固定值
- jwt原理

- 可以通过JWT令牌解析出当前用户的id,在interceptor解析的id传递到Service的save方法
- 使用ThreadLocal进行id值的传递
ThreadLocal并不是一个Thread,而是Thread的局部变量
ThreadLocal为每一个线程提供单独一份的存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问
ThreadLocal常用的方法:
- public void set(T value)
- public T get()
- public void remove()
// 1. 自定义Thread类
package com.sky.context;
public class BaseContext {
public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
public static void setCurrentId(Long id) {
threadLocal.set(id);
}
public static Long getCurrentId() {
return threadLocal.get();
}
public static void removeCurrentId() {
threadLocal.remove();
}
}
// 2. 在interceptor中添加id
//1、从请求头中获取令牌
String token = request.getHeader(jwtProperties.getAdminTokenName());
//2、校验令牌
try {
log.info("jwt校验:{}", token);
Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
log.info("当前员工id:", empId);
BaseContext.setCurrentId(empId); // 添加id到ThreadLocal
//3、通过,放行
return true;
} catch (Exception ex) {
//4、不通过,响应401状态码
response.setStatus(401);
return false;
}
// 3. 在service层取出id
employee.setCreateUser(BaseContext.getCurrentId());
employee.setUpdateUser(BaseContext.getCurrentId());
2.2 员工分页查询
2.2.1 需求分析和设计


2.2.2 代码开发


导入分页查询框架,方便使用
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
</dependency>
2.2.3 功能测试
查询
name = ""
page = 1 # 从1开始
pageSize = 10
2.2.4 代码完善
- 查询结果
{
"code": 1,
"msg": null,
"data": {
"total": 3,
"records": [
{
"id": 1,
"username": "admin",
"name": "管理员",
"password": "e10adc3949ba59abbe56e057f20f883e",
"phone": "13812312312",
"sex": "1",
"idNumber": "110101199001010047",
"status": 1,
"createTime": [
2022,
2,
15,
15,
51,
20
],
"updateTime": [
2022,
2,
17,
9,
16,
20
],
"createUser": 10,
"updateUser": 1
},
{
"id": 2,
"username": "fff",
"name": "FE",
"password": "e10adc3949ba59abbe56e057f20f883e",
"phone": "11122223333",
"sex": "男",
"idNumber": "232",
"status": 1,
"createTime": [
2025,
4,
18,
9,
30,
40
],
"updateTime": [
2025,
4,
18,
9,
30,
40
],
"createUser": 10,
"updateUser": 10
}
]
}
}
问题:上述结果中的创建/更新日期格式不满足前端要求
方式一:在属性上加注解,对日期进行格式化
@JsonFormat(pattern = "yyyy-MM-dd hh:mm:ss")
private LocalDataTime updateTime;
方式二:在WebMvcConfiguration中扩展Spring MVC的消息转换器,统一对日期类型进行格式化处理
/**
* 扩展mvc框架的消息转换器
* @param converters
*/
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("开始扩展消息转换器...");
// 创建一个消息转换器对象
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
//设置对象转换器,可以将Java对象转为json字符串
converter.setObjectMapper(new JacksonObjectMapper());
//将我们自己的转换器放入spring mvc框架的容器中
converters.add(0,converter);
}
2.3 启用禁用员工账号
2.3.1 需求分析和设计


2.3.2 代码开发
- Controller
//对于查询类的controller,Result一般需要加泛型,对于非查询类的一般不用
@PostMapping("/status/{status}")
@ApiOperation("员工状态禁用启用")
public Result updateStatus(@PathVariable Integer status, Long id){
log.info("启用警用员工账号:{},{}", status, id);
employeeService.updateStatus(status,id);
return Result.success();
}
- Service
@Override
public void updateStatus(Integer status, Long id) {
Employee employee = Employee
.builder()
.status(status)
.id(id)
.build();
//修改时间
employee.setUpdateTime(LocalDateTime.now());
//修改人id
employee.setUpdateUser(BaseContext.getCurrentId());
employeeMapper.update(employee);
}
- Mapper
/**
* 根据主键动态修改属性
* @param employee
*/
void update(Employee employee);
- XML
<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="status != null">
status = #{status},
</if>
<if test="updateUser != null">
update_user = #{updateUser},
</if>
<if test="updateTime != null">
update_time = #{updateTime}
</if>
</set>
where id = #{id}
</update>
xml中的parameterType没有使用全类名,是因为yaml配置文件中已经添加了mapper的类扫描路径(当然,如果使用全类名也可以让其生效)
mybatis: #mapper配置文件 mapper-locations: classpath:mapper/*.xml
2.4 编辑员工
2.4.1 需求分析和设计

- 需要回显员工数据
- 对员工数据进行修改
编辑员工功能涉及到两个接口:
- 根据id查询员工信息
- 编辑员工信息


2.4.2 代码开发
- 添加EmployeeVO
package com.sky.vo;
import io.swagger.annotations.ApiModel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(description = "员工回显数据格式")
public class EmployeeVO implements Serializable {
private Long id;
private String username;
private String name;
private String phone;
private String sex;
private String idNumber;
}
类型 目的 生命周期 是否含业务逻辑 典型应用层 Entity 持久化数据 整个业务处理过程 是 领域层 DTO 跨层/跨系统数据传输 单次请求过程 否 控制器↔服务层 VO 前端展示 单次响应过程 仅展示逻辑 控制器→前端
- controller
@GetMapping("/{id}")
@ApiOperation("根据id查询员工信息")
public Result<EmployeeVO> getById(@PathVariable Long id){
log.info("查询员工信息:{}",id);
EmployeeVO employee = employeeService.getById(id);
return Result.success(employee);
}
/**
* 更新员工信息
* @param employeeDTO
* @return
*/
@PutMapping
@ApiOperation("编辑员工信息")
public Result updateUserInfo(@RequestBody EmployeeDTO employeeDTO){
log.info("编辑员工信息:{}", employeeDTO);
employeeService.updateUserInfo(employeeDTO);
return Result.success();
}
- service
@Override
public EmployeeVO getById(Long id) {
Employee employee = employeeMapper.getById(id);
EmployeeVO employeeVO = new EmployeeVO();
BeanUtils.copyProperties(employee,employeeVO);
return employeeVO;
}
@Override
public void updateUserInfo(EmployeeDTO employeeDTO) {
Employee employee = new Employee();
BeanUtils.copyProperties(employeeDTO,employee);
employee.setUpdateTime(LocalDateTime.now());
employee.setUpdateUser(BaseContext.getCurrentId());
employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));
employee.setStatus(StatusConstant.ENABLE);
employeeMapper.update(employee);
}
- mapper
/**
* 根据主键动态修改属性
* @param employee
*/
void update(Employee employee);
/**
* 通过id查询Employee
* @param id
* @return
*/
@Select("select * from employee where id = #{id}")
Employee getById(Long id);
2.5 导入分类模块功能代码
2.5.1 需求分析和设计
- 产品原型

- 业务规则
- 分类名称必须是唯一的
- 分类按照类型可以分为菜品分类和套餐分类
- 新添加的分类状态默认为“禁用”
- 接口设计
- 新增分类
- 分类分页查询
- 根据id删除分类
- 修改分类
- 启用禁用分类
- 根据类型查询分类
2.5.2 代码导入
3. 菜品管理
3.1 公共字段自动填充
3.1.1 问题分析
对于公共字段的赋值,存在许多冗余代码。后期对其修改维护,会有大量重复操作,不太方便。

3.1.2 实现思路

3.1.3 代码开发
- 自定义OperationType枚举类: 只有UPDATE和INSERT操作是因为对于公共字段而言,查询和删除操作基本不存在冗余。
package com.sky.enumeration;
/**
* 数据库操作类型
*/
public enum OperationType {
/**
* 更新操作
*/
UPDATE,
/**
* 插入操作
*/
INSERT
}
- 自定义注解AutoFile
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();
}
- 编写Aspect
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.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MemberSignature;
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(){
//前置通知,在执行这些添加了AutoFill注解的方法执行之前进行公共字段的赋值
}
@Before("autoFillPointCut()")
public void autoFill(JoinPoint joinPoint){
log.info("开始进行公共字段的填充...");
//1. 获取到当前被拦截的方法上的数据库操作类型
MethodSignature signature = (MethodSignature) joinPoint.getSignature(); // 方法签名对象
AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class); // 获得方法上的注解对象
OperationType operationType = autoFill.value(); // 获取数据库操作类型
//2. 获取到当前被拦截方法的参数--实体对象 Employee Category等
Object[] args = joinPoint.getArgs();
if (args == null || args.length == 0){
return;
}
Object entity = args[0];
//3. 准备赋值的数据 -- 时间和当前用户Id
LocalDateTime now = LocalDateTime.now();
Long currentId = BaseContext.getCurrentId();
//4. 根据当前不同操作类型,为对应的属性通过反射进行赋值
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.setAccessible(true); // 当set方法是public就不需要使用setAccessible(true)这个操作
// setCreateUser.setAccessible(true);
// setUpdateTime.setAccessible(true);
// setUpdateUser.setAccessible(true);
setCreateTime.invoke(entity,now);
setUpdateTime.invoke(entity,now);
setCreateUser.invoke(entity,currentId);
setUpdateUser.invoke(entity,currentId);
} catch (Exception e) {
throw new RuntimeException(e);
}
}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) {
throw new RuntimeException(e);
}
}
}
}
- 使用案例
/**
* 根据主键动态修改属性
* @param employee
*/
@AutoFill(OperationType.UPDATE)
void update(Employee employee);
3.2 新增菜品
3.2.1 需求分析和设计
- 产品原型

- 业务规则

- 接口设计




- 数据库设计

3.2.2 代码开发
- 文件上传接口开发
图片 --> 浏览器 ---> 后端服务 ---> 对象存储服务器
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")
@Slf4j
@Api(tags = "通用接口")
public class CommonController {
@Autowired
private AliOssUtil aliOssUtil;
@ApiOperation("文件上传")
@PostMapping("/upload")
public Result<String> upload(MultipartFile file) {
log.info("文件上传:{}", file.getOriginalFilename());
// 使用原文件名可能有重名的情况,所以使用UUID
try {
String originalFilename = file.getOriginalFilename();
if(originalFilename == null || originalFilename.isEmpty() ){
return Result.error("文件名缺失");
}
//截取原文件名的后缀 .png .jpg
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.getMessage()) ;
// throw new RuntimeException(e);
}
return Result.error(MessageConstant.UPLOAD_FAILED);
}
}
- 新增菜品接口开发
- controller
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.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/admin/dish")
@Api(tags = "菜品相关接口")
@Slf4j
public class DishController {
@Autowired
private DishService dishService;
@PostMapping
@ApiOperation("添加菜品")
public Result addDish(@RequestBody DishDTO dishDTO) {
dishService.addDish(dishDTO);
return Result.success();
}
@GetMapping("/page")
@ApiOperation("菜品分页查询接口")
public Result<PageResult> pageQuery(DishPageQueryDTO dishPageQueryDTO) {
PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);
return Result.success(pageResult);
}
}
- service
package com.sky.service.impl;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.sky.dto.DishDTO;
import com.sky.dto.DishPageQueryDTO;
import com.sky.entity.Dish;
import com.sky.entity.DishFlavor;
import com.sky.mapper.DishFlavorMapper;
import com.sky.mapper.DishMapper;
import com.sky.result.PageResult;
import com.sky.service.DishService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class DishServiceImpl implements DishService {
@Autowired
private DishMapper dishMapper;
@Autowired
private DishFlavorMapper dishFlavorMapper;
/**
* 添加菜品
* 添加菜品涉及到 菜品口味的添加,所以需要保证这个方法的原子性,因此需要使用事务注解
* @param dishDTO
*/
@Transactional //启动类上事务管理已开启,但是此处还需要标明此方法的事务
@Override
public void addDish(DishDTO dishDTO) {
// 1. 向菜品表插入1条数据
Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO, dish);
dishMapper.insert(dish);
// 2. 向菜品口味表插入n条数据
List<DishFlavor> flavors = dishDTO.getFlavors();
// 批量插入Flavor
if (flavors != null && !flavors.isEmpty()) {
for (DishFlavor f : flavors) {
f.setDishId(dish.getId());
}
dishFlavorMapper.insertBatch(flavors);
}
}
@Override
public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) {
PageHelper.startPage(dishPageQueryDTO.getPage(),dishPageQueryDTO.getPageSize());// 这里会动态地将limit关键字以及后面的参数拼接到sql语句中
Page<Dish> page = dishMapper.pageQuery(dishPageQueryDTO);
return new PageResult(page.getTotal(),page.getResult());
}
}
- mapper
package com.sky.mapper;
import com.github.pagehelper.Page;
import com.sky.annotation.AutoFill;
import com.sky.dto.DishPageQueryDTO;
import com.sky.entity.Dish;
import com.sky.enumeration.OperationType;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface DishMapper {
/**
* 根据分类id查询菜品数量
* @param categoryId
* @return
*/
@Select("select count(id) from dish where category_id = #{categoryId}")
Integer countByCategoryId(Long categoryId);
@AutoFill(OperationType.INSERT)
void insert(Dish dish);
Page<Dish> pageQuery(DishPageQueryDTO dishPageQueryDTO);
}
- xml
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
/*userGenerateKeys: 使用本次插入数据后生成的主键, keyProperty: 将主键赋值给Mapper层参数里的属性id(如果是对象,则是对象中的属性)*/
insert into dish (name, category_id, price, image, description, status, create_time, update_time, create_user, update_user) VALUES
(#{name},#{categoryId},#{price},#{image},#{description},#{status}, #{createTime}, #{updateTime}, #{createUser},#{updateUser})
</insert>
3.3 菜品分页查询
3.3.1 需求分析和设计
- 产品原型

- 业务规则
- 根据页码显示菜品信息
- 每页展示10条数据
- 分页查询时,可以根据“菜品名称”,“菜品分类”,"售卖状态"进行查询
3.3.2 代码开发
- controller、service、mapper同员工分页查询
- xml
<?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">
/*userGenerateKeys: 使用本次插入数据后生成的主键, keyProperty: 将主键赋值给Mapper层参数里的属性id(如果是对象,则是对象中的属性)*/
insert into dish (name, category_id, price, image, description, status, create_time, update_time, create_user, update_user) VALUES
(#{name},#{categoryId},#{price},#{image},#{description},#{status}, #{createTime}, #{updateTime}, #{createUser},#{updateUser})
</insert>
<select id="pageQuery" resultType="com.sky.vo.DishVO">
select d.*, c.name as category_name from dish d left outer join category c on d.category_id = c.id
<where>
<if test="name != null and name != '' ">
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>
</mapper>
3.4 删除菜品
3.4.1 需求分析和设计
- 产品原型

- 业务规则
- 可以一次删除一个菜品,也可以批量删除 (一个批量删除的接口即可)
- 启售中的菜品不能删除
- 被套餐关联的菜品不能删除
- 删除菜品后,关联的口味数据也需要删除掉
- 接口设计

- 数据库设计

3.4.2 代码开发
- controller
/**
* 菜品的批量删除
* @param ids
* @return
*/
@DeleteMapping
@ApiOperation("菜品批量删除")
public Result deleteDish(@RequestParam List<Long> ids) {
log.info("菜品批量删除:{}", ids);
dishService.deleteBatch(ids);
return Result.success();
}
- service
@Override
@Transactional
public void deleteBatch(List<Long> ids) {
// 1. 判断菜品是否能够删除 -- 是否存在启售中的菜品
for (Long id : ids) {
Dish dish = dishMapper.getById(id);
if(dish.getStatus() == StatusConstant.ENABLE){
throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
}
}
// 2. 判断是否能够删除 -- 是否与套餐关联
List<Long> setMealIds = setmealDishMapper.getSetmealDishIdsBySetmealIds(ids);
if (setMealIds != null && !setMealIds.isEmpty()) {
throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
}
// 3. 删除菜品数据
dishMapper.deleteByIds(ids);
// 4. 删除菜品关联的口味数据
dishFlavorMapper.deleteByDishIds(ids);
}
- mapper
void deleteByIds(List<Long> ids);
- xml
<delete id="deleteByIds">
delete from dish where id in
/* open="(" close=")"*/
(
<foreach collection="ids" item="dishId" separator=",">
#{dishId}
</foreach>
)
</delete>
3.5 修改菜品
-
需求分析和设计

-
代码开发
-
controller
@PutMapping()
@ApiOperation("更新菜品信息")
public Result updateDish(@RequestBody DishDTO dishDTO) {
log.info("更新菜品信息:{}",dishDTO);
dishService.updateDish(dishDTO);
return Result.success();
}
@GetMapping("/{id}")
@ApiOperation("根据菜品id查询菜品")
public Result<DishVO> getDish(@PathVariable Long id) {
log.info("当前菜品id:{}",id);
DishVO dish = dishService.getDishById(id);
return Result.success(dish);
}
- service
@Override
@Transactional
public void updateDish(DishDTO dishDTO) {
Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO, dish);
dishMapper.update(dish);
dishFlavorMapper.deleteByDishId(dish.getId());
if(dishDTO.getFlavors() != null && !dishDTO.getFlavors().isEmpty()){
dishFlavorMapper.insertBatch(dishDTO.getFlavors());
}
}
@Override
public DishVO getDishById(Long id) {
Dish byId = dishMapper.getById(id);
DishVO dishVO = new DishVO();
BeanUtils.copyProperties(byId, dishVO);
dishVO.setFlavors(dishFlavorMapper.getByDishId(id));
return dishVO;
}
- mapper
@Select("select * from dish where id = #{id}")
Dish getById(Long id);
void deleteByIds(List<Long> ids);
@AutoFill(OperationType.UPDATE)
void update(Dish dish);
//flavor
@Delete("delete from dish_flavor where dish_id = #{dishId}")
void deleteByDishId(Long dishId);
@Select("select * from dish_flavor where dish_id = #{dishId}")
List<DishFlavor> getByDishId(Long dishId);
- xml
<!-- dish -->
<update id="update">
update dish
<set>
<if test="name != null and name != ''">
name = #{name},
</if>
<if test="categoryId != null">
category_id = #{categoryId},
</if>
<if test="price != null">
price = #{price},
</if>
<if test="image != null and image != ''">
image = #{image},
</if>
<if test="description != null and description !=''">
description = #{description},
</if>
<if test="status != null">
status = #{status},
</if>
<if test="updateUser != null">
update_user = #{updateUser},
</if>
<if test="updateTime != null">
update_time = #{updateTime}
</if>
</set>
where id = #{id}
</update>
<!-- flavor -->
<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>
<delete id="deleteByDishIds">
delete from dish_flavor where dish_id in
<foreach collection="dishIds" item="dishId" separator="," open="(" close=")">
#{dishId}
</foreach>
</delete>
4. 店铺营业状态设置
4.1 Redis入门
4.1.1 Redis简介
Redis是一个基于内存的key-value结构数据库
- 基于内存存储,读写性能高
- 适合存储热点数据(热点商品、咨询、新闻)
- 企业广泛应用
4.1.2 Redis下载与安装
Redis 安装包分为Windows版和Linux版
- Windows版下载地址:https://github.com/microsoftarchive/redis/releases
- Linux版下载地址:https://download.redis.io/releases

4.1.3 Redis服务启动与停止
4.2 Redis数据类型
Redis存储的是key-value结构的数据,其中key是字符串类型,value有5种常用的数据类型
- 字符串string
- 哈希hash
- 列表list
- 集合set
- 有序集合sorted set / zset

4.3 Redis常用命令
- 字符串操作命令
SET key value # 设置指定key的值
GET key # 获取指定key的值
SETEX key seconds value # 设置指定key的值,并把key的过期时间设置为seconds秒
SETNX key value # 只有在key不存在时设置key的值
- 哈希操作命令
Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象
HSET key field value # 将哈希表key中的字段field的值设置为value
HGET key field # 获取哈希表key中字段field的值
HDEL key field # 删除哈希表key中的field字段
HKEYS key # 获取哈希表key中的所有字段
HVALS key # 获取哈希表key中的所有字段的值
- 列表操作命令
Redis列表是简单的字符串列表,安装插入顺序排序
LPUSH key value1 value2 value3 ... # 将一个或者多个值插入到列表头部
LRANGE key satrt stop # 获取列表指定范围内的元素 LRANGE key 0 -1 # get all
RPOP key # 移除并获取列表最后一个元素
LLEN key # 获取列表长度
- 集合操作命令
Redis set是string类型的无序集合。集合成员是唯一的,集合中不能出现重复的数据
SADD key member1 member2 ... # 向集合添加一个或多个成员
SMEMBERS key # 返回集合中的所有成员
SCARD key # 获取集合的成员数
SINTER key1 key2 ... # 返回给定集合的交集
SUNION key1 key2 ... # 返回给定集合的并集
SREM key member1 member2 # 删除集合中的一个或多个成员
- 有序集合操作命令
Redis 有序集合是string类型元素的集合,且不允许有重复成员。每个元素都会关联一个double类型的分数
ZADD key score1 member1 score2 member2 ... # 向有序集合添加一个或多个成员
ZRANGE key start stop [WITHSCORES] # 通过索引区间返回有序集合中指定区间内的成员
ZINCRBT key increment member # 有序集合中对指定成员的分数加上增量increment
ZREM key member [memeber...] # 移除有序集合中的一个或多个成员
- 通用命令
KEYS pattern # 查找所有符合给定模式的key key * (查询所有)
EXISTS key # 检查给定key是否存在
TYPE key # 返回key所存储的值的类型
DEL key # 该命令用于在key存在时删除key
4.4 在Java中操作Redis
4.4.1 Redis的java客户端
Redis java客户端有很多,常用的有:
- Jedis
- Lettuce
- Spring Data Redis
Spring Data Redis是Spring的一部分,对Redis底层开发包进行了高度封装
在Spring项目中,可以使用Spring Data Redis来简化操作
4.4.2 Spring Data Redis使用方式
操作步骤:
-
导入Spring Data Redis的maven坐标
-
配置Redis数据源
-
编写配置类,创建RedisTemplate对象
-
通过RedisTemplate对象操作Redis
-
导入Spring Data Redis的maven坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 配置Redis数据源
sky:
datasource:
redis:
host: localhost
port: 6379
password: 123456
database: 0 # 默认为0,redis默认生成 16个数据库,编号为0 - 15
对于 Spring Boot 2.x:
spring.redis.database=0
spring.redis.host=localhost
spring.redis.port=16379
spring.redis.password=mypass
spring.redis.timeout=60000
对于 Spring Boot 3.x,我们需要设置以下属性:
spring.data.redis.database=0
spring.data.redis.host=localhost
spring.data.redis.port=16379
spring.data.redis.password=mypass
spring.data.redis.timeout=60000
- 编写配置类,创建RedisTemplate对象
/**
* Redis使用的配置类
*/
@Configuration
@Slf4j
public class RedisConfiguration {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
log.info("开始创建Redis Template 对象");
RedisTemplate redisTemplate = new RedisTemplate();
// 1. 设置redis连接工厂对象,此工厂对象已由spring框架创建(因为导入了redis的maven坐标)
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 2. 设置redis key的序列化器
redisTemplate.setKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
}
- 通过RedisTemplate对象操作Redis
@Test
public void testRedisTemplate(){
System.out.println(redisTemplate);
ValueOperations valueOperations = redisTemplate.opsForValue();
valueOperations.set("spring", "jjjaaavvvaaa");
HashOperations hashOperations = redisTemplate.opsForHash();
ListOperations listOperations = redisTemplate.opsForList();
SetOperations setOperations = redisTemplate.opsForSet();
ZSetOperations zSetOperations = redisTemplate.opsForZSet();
}
4.5 店铺营业状态设置
4.5.1 需求分析和设计
- 产品原型

- 接口设计
- 设置营业状态
- 管理端查询营业状态
/admin - 用户端查询营业状态
/user

营业状态数据的存储方式:基于Redis的字符串来进行存储
约定: 1表示营业,0表示打烊
4.5.2 代码开发
- configuration
@Bean
public Docket docketAdmin() {
log.info("准备生成管理端接口文档...");
ApiInfo apiInfo = new ApiInfoBuilder()
.title("苍穹外卖项目管理端接口文档")
.version("2.0")
.description("这是苍穹外卖项目管理端接口文档")
.build();
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.groupName("管理端接口")
.apiInfo(apiInfo)
.select()
.apis(RequestHandlerSelectors.basePackage("com.sky.controller.admin"))
.paths(PathSelectors.any())
.build();
return docket;
}
/**
* 通过knife4j生成用户端接口文档
* @return
*/
@Bean
public Docket docketUser() {
log.info("准备生成接口文档...");
ApiInfo apiInfo = new ApiInfoBuilder()
.title("苍穹外卖项目用户端接口文档")
.version("2.0")
.description("这是苍穹外卖项目用户端接口文档")
.build();
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.groupName("用户端接口")
.apiInfo(apiInfo)
.select()
.apis(RequestHandlerSelectors.basePackage("com.sky.controller.user"))
.paths(PathSelectors.any())
.build();
return docket;
}
- controller
package com.sky.controller.admin;
import com.sky.constant.StatusConstant;
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("adminShopController")
@RequestMapping("/admin/shop")
@Slf4j
@Api(tags = "店铺相关接口")
public class ShopController {
@Autowired
private RedisTemplate redisTemplate;
@PutMapping("/{status}")
@ApiOperation("设置店铺营业状态")
public Result setStatus(@PathVariable Integer status){
log.info("设置店铺营业状态:{}",status);
redisTemplate.opsForValue().set(StatusConstant.SHOP_STATUS, status);
return Result.success();
}
@GetMapping("/status")
@ApiOperation("管理端查询店铺营业状态")
public Result<Integer> getStatus(){
Integer status = (Integer) redisTemplate.opsForValue().get(StatusConstant.SHOP_STATUS);
log.info("查询店铺营业状态:{}",StatusConstant.SHOP_STATUS);
return Result.success(status);
}
}
5. 微信登陆、商品浏览
5.1 HttpClient
5.1.1 介绍
HttpClient是Apache Jakarta Common下的子项目,可以用来提供高效的、最新的、功能丰富的支持HTTP协议的客户端编程工具包,并且它支持HTTP协议最新的版本和建议。
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
- 核心API
- HttpClient
- HttpClients
- CloseableHttpClient
- HttpGet
- HttpPost
- 发送请求步骤:
- 创建HttpClient对象
- 创建Http请求对象
- 调用HttpClient的excute方法发送请求
HttpClient作用: 可以在java程序中通过编码的方式发送Http请求
5.1.2 入门案例
由于阿里云oss的sdk已经引入httpclient的maven坐标,所以不必再引入
package com.sky.test.server.httpclient;
import com.alibaba.fastjson.JSONObject;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.IOException;
@SpringBootTest
public class HttpClientTest {
/**
* GET 请求
*/
@Test
public void testGET() throws IOException {
//1. 创建HttpClient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
//2. 创建请求对象
HttpGet httpGet = new HttpGet("http://localhost:8080/user/shop/status");
//3. 发送请求
CloseableHttpResponse response = httpClient.execute(httpGet);
// 4.获取服务端返回的状态码
int statusCode = response.getStatusLine().getStatusCode();
System.out.println(statusCode);
// 5. 获取服务端返回的数据
HttpEntity entity = response.getEntity();
System.out.println("返回数据:" + EntityUtils.toString(entity));
// 6. 关闭资源
response.close();
httpClient.close();
}
@Test
public void testPost() throws IOException {
//1. 创建HttpClient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
//2. 创建请求对象
HttpPost httpPost = new HttpPost("http://localhost:8080/admin/employee/login");
JSONObject jsonObject = new JSONObject();
jsonObject.put("username", "admin");
jsonObject.put("password", "123456");
StringEntity stringEntity = new StringEntity(jsonObject.toString());
//指定编码方式和数据格式
stringEntity.setContentType("application/json");
stringEntity.setContentEncoding("UTF-8");
httpPost.setEntity(stringEntity);
//3. 发送请求
CloseableHttpResponse response = httpClient.execute(httpPost);
// 4.获取服务端返回的状态码
int statusCode = response.getStatusLine().getStatusCode();
System.out.println(statusCode);
// 5. 获取服务端返回的数据
HttpEntity entity = response.getEntity();
System.out.println("返回数据:" + EntityUtils.toString(entity));
// 6. 关闭资源
response.close();
httpClient.close();
}
}
5.2 微信小程序开发

5.2.1 准备工作
- 注册小程序
- 完善小程序信息
- 下载开发者工具
5.2.2 入门案例
- 了解微信小程序目录结构
小程序包含一个描述整体程序的app和多个描述各自页面的page
一个小程序主题部分由三个文件组成,必须放到项目的根目录,如下
app.js: 小程序逻辑
app.json: 小程序公共配置
app.wxss: 小程序公共样式表
一个小程序由4个文件组成:
- js:页面逻辑
- wxml:页面结构
- json:页面配置
- wxss:页面样式表
- 编写小程序代码
- 编译小程序
5.3 微信登陆
5.3.1 需求分析和设计



5.4 导入商品浏览功能代码
5.4.1 需求分析和设计
-
产品原型
-
接口设计
- 查询分类
- 根据分类id查询菜品
- 根据分类id查询套餐
- 根据套餐id查询包含的菜品
6. 缓存菜品、购物车
6.1 缓存菜品
- 问题说明: 用户端展示的菜品通过查询数据库获得,如果用户端的访问量增加,那么查询数据库的访问压力也会增加
- 实现思路:通过redis来缓存菜品数据,减少数据库查询操作


- 代码开发
@GetMapping("/list")
@ApiOperation("根据分类id查询菜品")
public Result<List<DishVO>> list(Long categoryId) {
// 构造redis中的key
String key = "dish_" + categoryId;
// 1. 查询redis中是否存在菜品数据
List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key);
if (list != null && list.size() > 0){
// 2. 如果存在,直接返回
return Result.success(list);
}
// 3. 如果不存在,查询数据库,将查询到的数据放入redis
Dish dish = new Dish();
dish.setCategoryId(categoryId);
dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品
list = dishService.listWithFlavor(dish);
redisTemplate.opsForValue().set(key, list);
return Result.success(list);
}
6.2 缓存套餐
6.2.1 Spring Cache
Spring Cache是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存的功能。
Spring Cache提供了一层抽象,底层可以切换不同的缓存实现,例如:
-
EHCache
-
Caffein
-
Redis
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
常用注解:
@EnableCaching: 开启缓存注解功能,通常加在启动类上@Cacheable: 在方法执行前先查c询缓存中是否有数据,如果有,则直接返回。没有则将数据放入缓存中再返回@CachePut(cacheNames = "userCache", key = "#user.id"): 将方法的返回值放入缓存中@CacheEvict:将一条或多条数据从缓存中删除
6.2.2 实现思路
- 导入Spring Cache和Redis相关的maven坐标
- 在启动类上加入@EnableCaching注解,开启缓存注解功能
- 在用户端接口SetmealController的list方法上加入@Cacheable注解
- 在管理端接口SetmealController的save、delete、update等方法上加上CacheEvict注解
6.2.3 代码开发
- 启动类开启
@EnableCaching
@SpringBootApplication
@EnableTransactionManagement //开启注解方式的事务管理
@Slf4j
@EnableCaching
public class SkyApplication {
public static void main(String[] args) {
SpringApplication.run(SkyApplication.class, args);
log.info("server started");
}
}
- Controller使用查询
@Cacheable(cacheNames = "setMealCache", key = "#categoryId") //
/**
* 条件查询
*
* @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);
}
- Controller更新时删除缓存
@CacheEvict(cacheNames = "setMealCache", key = "#setmealDTO.categoryId")
@PostMapping
@ApiOperation("新增套餐")
@CacheEvict(cacheNames = "setMealCache", key = "#setmealDTO.categoryId")
public Result addSetMeal(@RequestBody SetmealDTO setmealDTO) {
log.info("新增套餐:{}", setmealDTO);
setMealService.addSetMeal(setmealDTO);
return Result.success();
}
@GetMapping("/{id}")
@ApiOperation("根据id查询套餐")
public Result<SetmealVO> getSetmealById(@PathVariable Long id) {
log.info("根据id获取套餐信息:{}",id);
SetmealVO setmealVO = setMealService.getSetmealById(id);
return Result.success(setmealVO);
}
@PutMapping
@ApiOperation("修改套餐")
@CacheEvict(cacheNames = "SetMealCache", allEntries = true)
public Result updateSetmeal(@RequestBody SetmealDTO setmealDTO) {
log.info("修改套餐{}",setmealDTO);
setMealService.update(setmealDTO);
return Result.success();
}
@PostMapping("/status/{status}")
@ApiOperation("套餐起售、停售")
@CacheEvict(cacheNames = "SetMealCache", allEntries = true)
public Result updateStatus(Long id, @PathVariable Integer status) {
log.info("套餐起售、停售:id{},status:{}",id,status);
setMealService.updateStatus(id,status);
return Result.success();
}
@DeleteMapping()
@ApiOperation("根据套餐id批量删除套餐接口")
@CacheEvict(cacheNames = "SetMealCache", allEntries = true)
public Result deleteSetmealByIds(@RequestParam List<Long> ids) {
log.info("根据套餐id批量删除套餐:{}",ids);
setMealService.deleteSetmealByIds(ids);
return Result.success();
}
6.3 添加购物车
6.3.1 需求分析和设计

- 接口设计


6.3.2 代码开发
@Autowired
private ShoppingCartMapper shoppingCartMapper;
@Autowired
private DishMapper dishMapper;
@Autowired
private SetmealMapper setmealMapper;
@Override
public void addShoppingCart(ShoppingCartDTO shoppingCartDTO) {
// 判断当前用户加入到购物车中的商品是否已经存在
ShoppingCart shoppingCart = new ShoppingCart();
BeanUtils.copyProperties(shoppingCartDTO, shoppingCart);
Long currentId = BaseContext.getCurrentId();
shoppingCart.setUserId(currentId);
List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);
if(list != null && list.size() > 0) {
ShoppingCart cart = list.get(0);
cart.setNumber(cart.getNumber() + 1); // update shopping_cart set number = ? where id = ?
shoppingCartMapper.updateNumberById(cart);
}else{
// 如果不存在,插入一条购物车数据
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{
Long setmealId = shoppingCartDTO.getSetmealId();
Setmeal setMeal = setmealMapper.getById(setmealId);
shoppingCart.setName(setMeal.getName());
shoppingCart.setImage(setMeal.getImage());
shoppingCart.setAmount(setMeal.getPrice());
}
// 如果存在,只需要将数量+1
// 如果不存在,则需要插入一条购物车数据
shoppingCart.setNumber(1);
shoppingCart.setCreateTime(LocalDateTime.now());
shoppingCartMapper.insert(shoppingCart);
}
}
@Override
public List<ShoppingCart> list(Long userId) {
return shoppingCartMapper.list(ShoppingCart.builder().userId(userId).build());
}
6.4 清空购物车
@Delete("delete from shopping_cart where user_id = #{userId}")
7. 用户下单、订单支付
7.1 用户下单
-
需求分析

-
数据库设计



- controller
@Autowired
private OrderService orderService;
@PostMapping("/submit")
@ApiOperation("用户端订单提交")
public Result<OrderSubmitVO> submitOrder(@RequestBody OrdersSubmitDTO ordersSubmitDTO) {
log.info("用户下单,参数为:{}", ordersSubmitDTO);
OrderSubmitVO orderSubmitVO = orderService.submitOrder(ordersSubmitDTO);
return Result.success(orderSubmitVO);
}
- service
package com.sky.service.impl;
import com.sky.constant.MessageConstant;
import com.sky.context.BaseContext;
import com.sky.dto.OrdersSubmitDTO;
import com.sky.entity.AddressBook;
import com.sky.entity.OrderDetail;
import com.sky.entity.Orders;
import com.sky.entity.ShoppingCart;
import com.sky.exception.AddressBookBusinessException;
import com.sky.mapper.AddressBookMapper;
import com.sky.mapper.OrderDetailMapper;
import com.sky.mapper.OrderMapper;
import com.sky.mapper.ShoppingCartMapper;
import com.sky.service.OrderService;
import com.sky.vo.OrderSubmitVO;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private OrderDetailMapper orderDetailMapper;
@Autowired
private AddressBookMapper addressBookMapper;
@Autowired
private ShoppingCartMapper shoppingCartMapper;
@Override
@Transactional
public OrderSubmitVO submitOrder(OrdersSubmitDTO ordersSubmitDTO) {
// 处理各种业务异常(地址簿为空、购物车为空)
AddressBook addressBook = addressBookMapper.getById(ordersSubmitDTO.getAddressBookId());
if(addressBook == null){
// 抛出业务异常
throw new AddressBookBusinessException(MessageConstant.ADDRESS_BOOK_IS_NULL);
}
List<ShoppingCart> shoppingCartList = shoppingCartMapper.list(ShoppingCart.builder().userId(BaseContext.getCurrentId()).build());
if(shoppingCartList == null || shoppingCartList.isEmpty()){
throw new AddressBookBusinessException(MessageConstant.SHOPPING_CART_IS_NULL);
}
// 1. 向订单表插入一条订单数据
Orders orders = new Orders();
BeanUtils.copyProperties(ordersSubmitDTO, orders);
orders.setOrderTime(LocalDateTime.now());
orders.setPayStatus(Orders.UN_PAID);
orders.setStatus(Orders.PENDING_PAYMENT);
orders.setNumber(String.valueOf(System.currentTimeMillis()));
orders.setPhone(addressBook.getPhone());
orders.setConsignee(addressBook.getConsignee());
orders.setUserId(BaseContext.getCurrentId());
orderMapper.insertOrder(orders);
//2. 向订单明细表插入多条数据
List<OrderDetail> orderDetails = new ArrayList<>();
for(ShoppingCart shoppingCart : shoppingCartList){
OrderDetail orderDetail = new OrderDetail();
BeanUtils.copyProperties(shoppingCart, orderDetail);
orderDetail.setOrderId(orders.getId());
orderDetails.add(orderDetail);
}
orderDetailMapper.insertBatch(orderDetails);
//3. 清空当前用户的购物车数据
shoppingCartMapper.deleteByUserId(BaseContext.getCurrentId());
//4. 封装VO返回结果
return OrderSubmitVO.builder()
.id(orders.getId())
.orderTime(orders.getOrderTime())
.orderNumber(orders.getNumber())
.orderAmount(orders.getAmount())
.build();
}
}
7.2 订单支付
7.2.1 微信支付介绍



7.2.2 微信支付准备工作
- 内网穿透
- 下载cpolar:cpolar - secure introspectable tunnels to localhost
- 连接账户:
cpolar.exe authtoken ZGY2N2M3ZTUtNjZmMC00OTVmLWEwMDAtOGQzNDQ4OWZkZWU4 - 运行:
cpolar.exe http 80
8. 订单状态定时处理、来单提醒和客户催单
8.1 Spring Task
8.1.1 介绍
Spring Task是Spring框架提供的任务调度工具,可以按照约定的时间自动执行某个代码逻辑
-
应用场景:
- 信用卡每月还款提醒
- 在线支付应用处理未支付订单
- 某个纪念日为用户发送通知
-
使用步骤
- 启动类加
@EnableScheduling - 方法上加
@Scheduled(cron = "0/5 * * * * ?")(类上需要加上@Component注解)
- 启动类加
8.1.2 cron表达式
cron表达式可以理解为就是一个字符串,通过cron表达式可以定义任务的触发时间
构成规则:分成6或7个域,用空格分隔开,每个域代表一个含义
每个域的含义:秒 分 时 日 月 周 年(年可选)
2025年5月21日8点56分20秒
| 秒 | 分 | 时 | 日 | 月 | 周 | 年 |
|---|---|---|---|---|---|---|
| 20 | 56 | 08 | 21 | 5 | ?(日和周只能有一个出现) | 2022 |
| , - * / | , - * / | , - * / | , - * / L(每月最后一天) W(每月几号最近的工作日) | , - * / | , - * / L(1L本月最后一个星期1) #(3#4第三周的星期四) | , - * / |
cron表达式在线生成器: https://cron.qqe2.com
8.1.3 入门案例
- Spring Task 使用步骤
- 导入maven坐标 spring-context (spring boot默认已导入)
- 启动类添加注解@EnableScheduling开启任务调度
- 自定义定时任务类(需要加上
@Component注解)
8.2 订单状态定时处理
-
需求分析
- 下单后未支付,订单一直处于“待支付”状态
- 用户收货后管理端没有点击完成,订单一直处于“派送中”状态
-
实现思路
- 通过定时任务每分钟检查一次是否存在支付超时订单,如果存在则修改订单状态为“已取消”
-
task
package com.sky.task;
import com.sky.constant.MessageConstant;
import com.sky.entity.Orders;
import com.sky.mapper.OrderMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.List;
/**
* 定时任务类,定时处理订单状态
*/
@Component
@Slf4j
public class OrderTask {
@Autowired
OrderMapper orderMapper;
/**
* 处理超时订单的方法
*/
@Scheduled(cron = "*/10 * * * * ?")
public void processTimeOutOrder(){
log.info("定时处理超时订单:{}", LocalDateTime.now());
// select * from orders where status = ? and order_time < (当前时间-15分钟)
List<Orders> orderList = orderMapper.getByStatusAndOrderTimeLT(Orders.PENDING_PAYMENT, LocalDateTime.now().plusMinutes(-15));
if(orderList != null && orderList.size() > 0){
for (Orders orders : orderList) {
orders.setStatus(Orders.CANCELLED);
orders.setCancelTime(LocalDateTime.now());
orders.setCancelReason(MessageConstant.ORDER_OVERTIME);
}
orderMapper.batchUpdate(orderList);
}
}
/**
* 处理未处理的派送中订单
*/
@Scheduled(cron = "0 0 1 * * ?") // 每天凌晨1点
public void processDeliveryOrder(){
log.info("定时处理处于派送中的订单:{}", LocalDateTime.now());
List<Orders> orderList = orderMapper.getByStatusAndOrderTimeLT(Orders.DELIVERY_IN_PROGRESS, LocalDateTime.now().plusHours(-5));
if(orderList != null && orderList.size() > 0){
for (Orders orders : orderList) {
orders.setStatus(Orders.CANCELLED);
orders.setDeliveryTime(LocalDateTime.now());
}
orderMapper.batchUpdate(orderList);
}
}
}
- mapper
<update id="batchUpdate" >
update orders
<set>
<if test="list[0].cancelReason != null and list[0].cancelReason!='' ">
cancel_reason=#{list[0].cancelReason},
</if>
<if test="list[0].rejectionReason != null and list[0].rejectionReason!='' ">
rejection_reason=#{list[0].rejectionReason},
</if>
<if test="list[0].cancelTime != null">
cancel_time=#{list[0].cancelTime},
</if>
<if test="list[0].payStatus != null">
pay_status=#{list[0].payStatus},
</if>
<if test="list[0].payMethod != null">
pay_method=#{list[0].payMethod},
</if>
<if test="list[0].checkoutTime != null">
checkout_time=#{list[0].checkoutTime},
</if>
<if test="list[0].status != null">
status = #{list[0].status},
</if>
<if test="list[0].deliveryTime != null">
delivery_time = #{list[0].deliveryTime}
</if>
</set>
where id in
<foreach collection="orderList" item="order" separator="," open="(" close=")">
#{order.id}
</foreach>
8.3 WebSocket
WebSocket是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工通信----浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输
-
单工通信只支持信号在一个方向上传输(正向或反向),任何时候不能改变信号的传输方向。
-
半双工通信允许信号在两个方向上传输,但某一时刻只允许信号在一个信道上单向传输。
-
全双工通信允许数据同时在两个方向上传输,即有两个信道,因此允许同时进行双向传输。

-
应用场景
- 视频弹幕
- 网页聊天
- 体育实况更新
- 股票基金报价实时更新
-
入门案例
实现步骤:
- 使用websocket.html页面作为WebSocket客户端
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>WebSocket Demo</title>
</head>
<body>
<input id="text" type="text" />
<button onclick="send()">发送消息</button>
<button onclick="closeWebSocket()">关闭连接</button>
<div id="message">
</div>
</body>
<script type="text/javascript">
var websocket = null;
var clientId = Math.random().toString(36).substr(2);
//判断当前浏览器是否支持WebSocket
if('WebSocket' in window){
//连接WebSocket节点
websocket = new WebSocket("ws://localhost:8080/ws/"+clientId);
}
else{
alert('Not support websocket')
}
//连接发生错误的回调方法
websocket.onerror = function(){
setMessageInnerHTML("error");
};
//连接成功建立的回调方法
websocket.onopen = function(){
setMessageInnerHTML("连接成功");
}
//接收到消息的回调方法
websocket.onmessage = function(event){
setMessageInnerHTML(event.data);
}
//连接关闭的回调方法
websocket.onclose = function(){
setMessageInnerHTML("close");
}
//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function(){
websocket.close();
}
//将消息显示在网页上
function setMessageInnerHTML(innerHTML){
document.getElementById('message').innerHTML += innerHTML + '<br/>';
}
//发送消息
function send(){
var message = document.getElementById('text').value;
websocket.send(message);
}
//关闭连接
function closeWebSocket() {
websocket.close();
}
</script>
</html>
- 导入WebSocket的maven坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
-
导入WebSocket服务端组件WebSocketServer, 用于和客户端通信
-
导入配置类WebSocketConfiguration,注册WebSocket服务端组件
-
导入定时任务类WebSocketTask,定时向客户端推送数据
8.4 来单提醒
设计:
- 通过WebSocket实现管理端页面和服务端保持长连接状态
- 当客户支付后,调用WebSocket的相关API实现服务端向客户端推送消息
- 客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报
- 约定服务端发送给客户端浏览器的数据格式为JSON,字段包括:type、orderId、content
- type:消息类型,1为来单提醒、2为客户催单
- orderId:为订单id
- content:为消息内容
8.5 客户催单
- 需求分析
- 用户在小程序中点击催单按钮,需要第一时间通知外卖商家,通知形式有:
- 语音播报
- 弹出提示框
- 用户在小程序中点击催单按钮,需要第一时间通知外卖商家,通知形式有:
9.数据统计-图形报表
9.1 Apache Echarts
9.1.1 介绍
Apache Echarts是一款基于JavaScript的数据可视化图标库,提供直观、生动、可交互、可个性化定制的数据可视化图标
官网地址: https://echarts.apache.org/zh/index.html
9.1.2 入门案例
使用Echarts,重点在于研究当前图标所需的数据格式,通常是需要后端提供符合格式要求的动态数据,然后响应前端来展示图标
9.2 营业额统计
9.2.1 需求分析和设计


9.2.2 代码开发
- controller
@RestController
@Slf4j
@RequestMapping("/admin/report")
@Api(tags = "数据统计相关接口")
public class ReportController {
@Autowired
private ReportService reportService;
@GetMapping("/turnoverStatistics")
@ApiOperation("获取营业额数据")
public Result<TurnoverReportVO> turnoverStatistics(
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end) {
log.info("营业额数据统计:start = {}, end = {}", begin, end);
return Result.success(reportService.getTurnoverStatistics(begin,end));
}
}
- service
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.lang3.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.*;
@Service
@Slf4j
public class ReportServiceImpl implements ReportService {
@Autowired
private OrderMapper orderMapper;
@Override
public TurnoverReportVO getTurnoverStatistics(LocalDate begin, LocalDate end) {
// 当前集合用于存放begin到end范围内的每天的日期
List<LocalDate> dateList = new ArrayList<>();
dateList.add(begin);
while(!begin.equals(end)) {
begin = begin.plusDays(1);
dateList.add(begin);
}
List<Double> turnoverList = new ArrayList<>();
for(LocalDate date : dateList) {
LocalDateTime dayStart = LocalDateTime.of(date, LocalTime.MIN);
LocalDateTime dayEnd = LocalDateTime.of(date, LocalTime.MAX);
Map<String, Object> map = new HashMap<>();
map.put("dayStart", dayStart.toString());
map.put("dayEnd", dayEnd.toString());
map.put("status", Orders.COMPLETED);
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();
}
}
- mapper
<select id="sumByMap" resultType="java.lang.Double" parameterType="Map">
select sum(amount) from orders
<where>
<if test="dayStart != null">
and delivery_time > #{dayStart}
</if>
<if test="dayEnd != null">
and delivery_time < #{dayEnd}
</if>
<if test="status != null">
and status = #{status}
</if>
</where>
</select>
9.3 用户统计
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);
}
// select sum(id) from user where create_time > ? and create_time < ?
List<Integer> newUserList = new ArrayList<>();
// select sum(id) from user where create_time < ?
List<Integer> totalUserList = new ArrayList<>();
for(LocalDate date : dateList) {
LocalDateTime dayStart = LocalDateTime.of(date, LocalTime.MIN);
LocalDateTime dayEnd = LocalDateTime.of(date, LocalTime.MAX);
Map<String, Object> map = new HashMap<>();
map.put("dayEnd", dayEnd.toString());
Integer totalUser = userMapper.countByMap(map);
map.put("dayStart", dayStart.toString());
Integer newUser = userMapper.countByMap(map);
newUserList.add(newUser);
totalUserList.add(totalUser);
}
return UserReportVO.builder()
.dateList(StringUtils.join(dateList,","))
.newUserList(StringUtils.join(newUserList,","))
.totalUserList(StringUtils.join(totalUserList,","))
.build();
}
<select id="countByMap" resultType="java.lang.Integer">
select count(id) from user
<where>
<if test="dayStart != null">
and create_time > #{dayStart}
</if>
<if test="dayEnd != null">
and create_time < #{dayEnd}
</if>
</where>
</select>
9.4 订单统计
<select id="countByMap" resultType="java.lang.Integer">
select count(id) from orders
<where>
<if test="dayStart != null">
and order_time > #{dayStart}
</if>
<if test="dayEnd != null">
and order_time < #{dayEnd}
</if>
<if test="status != null">
and status = #{status}
</if>
</where>
</select>
9.5 销量排名TOP10
<select id="getTop10" resultType="com.sky.vo.SalesTop10ReportVO">
SELECT
GROUP_CONCAT(name ORDER BY sn DESC SEPARATOR ',') AS nameList,
GROUP_CONCAT(sn ORDER BY sn DESC SEPARATOR ',') AS numberList
FROM (
SELECT
ANY_VALUE(d.name) as name,
SUM(d.number) as sn
FROM
order_detail as d
INNER JOIN
orders as o
ON d.order_id = o.id
<where>
<if test="status != null">
and o.status = #{status}
</if>
<if test="dayStart != null">
and o.order_time > #{dayStart}
</if>
<if test="dayEnd != null">
and o.order_time < #{dayEnd}
</if>
</where>
GROUP BY
d.dish_id
ORDER BY
sn DESC
LIMIT 10
) AS top10
</select>
10 导出Excel报表
10.1 Apache POI
- 介绍
Apache POI 是一个处理Microsoft office各种文件格式的开源项目。可以使用POI在Java程序中对各种office文件进行读写操作。POI通常用于处理Excel文件
应用场景:
· 银行网银系统导出交易明细
· 各种业务系统导出Excel报表
· 批量导入业务数据
- 入门案例
- 导入maven坐标
<properties>
<poi>3.16</poi>
</properties>
<!-- poi -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>${poi}</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>${poi}</version>
</dependency>
10.2 导入运营数据
10.2.1 分析设计

10.2.2 代码开发
package com.sky.service.impl;
import com.sky.entity.Orders;
import com.sky.mapper.OrderDetailMapper;
import com.sky.mapper.OrderMapper;
import com.sky.mapper.UserMapper;
import com.sky.service.ReportService;
import com.sky.service.WorkspaceService;
import com.sky.vo.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.*;
@Service
@Slf4j
public class ReportServiceImpl implements ReportService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private UserMapper userMapper;
@Autowired
private OrderDetailMapper orderDetailMapper;
@Autowired
private WorkspaceService workspaceService;
@Override
public TurnoverReportVO getTurnoverStatistics(LocalDate begin, LocalDate end) {
// 当前集合用于存放begin到end范围内的每天的日期
List<LocalDate> dateList = new ArrayList<>();
dateList.add(begin);
while(!begin.equals(end)) {
begin = begin.plusDays(1);
dateList.add(begin);
}
List<Double> turnoverList = new ArrayList<>();
for(LocalDate date : dateList) {
LocalDateTime dayStart = LocalDateTime.of(date, LocalTime.MIN);
LocalDateTime dayEnd = LocalDateTime.of(date, LocalTime.MAX);
Map<String, Object> map = new HashMap<>();
map.put("dayStart", dayStart.toString());
map.put("dayEnd", dayEnd.toString());
map.put("status", Orders.COMPLETED);
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();
}
@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);
}
// select sum(id) from user where create_time > ? and create_time < ?
List<Integer> newUserList = new ArrayList<>();
// select sum(id) from user where create_time < ?
List<Integer> totalUserList = new ArrayList<>();
for(LocalDate date : dateList) {
LocalDateTime dayStart = LocalDateTime.of(date, LocalTime.MIN);
LocalDateTime dayEnd = LocalDateTime.of(date, LocalTime.MAX);
Map<String, Object> map = new HashMap<>();
map.put("dayEnd", dayEnd.toString());
Integer totalUser = userMapper.countByMap(map);
map.put("dayStart", dayStart.toString());
Integer newUser = userMapper.countByMap(map);
newUserList.add(newUser);
totalUserList.add(totalUser);
}
return UserReportVO.builder()
.dateList(StringUtils.join(dateList,","))
.newUserList(StringUtils.join(newUserList,","))
.totalUserList(StringUtils.join(totalUserList,","))
.build();
}
@Override
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> newOrderList = new ArrayList<>();
List<Integer> validOrderList = new ArrayList<>();
for(LocalDate date : dateList) {
// 查询订单总数 select count(id) from orders where order_time < ? and order_time > ?
LocalDateTime dayStart = LocalDateTime.of(date, LocalTime.MIN);
LocalDateTime dayEnd = LocalDateTime.of(date, LocalTime.MAX);
Integer orderCount = getOrderCount(dayStart, dayEnd, null);
//查询有效订单数 select count(id) from orders where order_time > ? and order_time < ? and status = completed
Integer validOrderCount = getOrderCount(dayStart, dayEnd, Orders.COMPLETED);
newOrderList.add(orderCount);
validOrderList.add(validOrderCount);
}
//计算时间区间内订单总数
Integer totalOrderCount = newOrderList.stream().reduce(Integer::sum).get();
Integer validOrderCount = validOrderList.stream().reduce(Integer::sum).get();
return OrderReportVO.builder()
.dateList(StringUtils.join(dateList,","))
.validOrderCountList(StringUtils.join(validOrderList,","))
.orderCountList(StringUtils.join(newOrderList,","))
.totalOrderCount(totalOrderCount)
.validOrderCount(totalOrderCount)
.orderCompletionRate(totalOrderCount == 0 ? 0.0 : validOrderCount.doubleValue() / totalOrderCount.doubleValue())
.build();
}
@Override
public SalesTop10ReportVO getTop10(LocalDate begin, LocalDate end) {
LocalDateTime dayStart = LocalDateTime.of(begin, LocalTime.MIN);
LocalDateTime dayEnd = LocalDateTime.of(end, LocalTime.MAX);
List<String> top10NameList = new ArrayList<>();
List<Integer> top10CountList = new ArrayList<>();
// select d.dish_id, ANY_VALUE(name) as name, sum(d.number) as sn from order_detail as d left join orders as o group by d.dish_id having o.status = #{status} order by sn DESC;
return orderDetailMapper.getTop10(dayStart,dayEnd,Orders.COMPLETED);
}
@Override
public void reportBusinessData(HttpServletResponse response) {
// 1. 查询数据库,获取运营数据
LocalDate startDate = LocalDate.now().minusDays(30);
LocalDate endDate = LocalDate.now().minusDays(1);
BusinessDataVO businessData = workspaceService.getBusinessData(LocalDateTime.of(startDate, LocalTime.MIN), LocalDateTime.of(endDate, LocalTime.MAX));
// 2. 通过POI写入数据
InputStream in = this.getClass().getClassLoader().getResourceAsStream("template/运营数据报表模板.xlsx");
ServletOutputStream outputStream = null;
XSSFWorkbook sheets = null;
try {
sheets = new XSSFWorkbook(in);
// 填充数据
XSSFSheet sheet1 = sheets.getSheet("Sheet1");
sheet1.getRow(1).getCell(1).setCellValue("时间: " + startDate + " -- " + endDate);
sheet1.getRow(3).getCell(2).setCellValue(businessData.getTurnover());
sheet1.getRow(3).getCell(4).setCellValue(businessData.getOrderCompletionRate());
sheet1.getRow(3).getCell(6).setCellValue(businessData.getNewUsers());
sheet1.getRow(4).getCell(2).setCellValue(businessData.getValidOrderCount());
sheet1.getRow(4).getCell(4).setCellValue(businessData.getUnitPrice());
//sheet1.getRow(7).getCell(2)
for (int i = 0; i < 30; i++) {
LocalDate localDate = startDate.plusDays(i);
BusinessDataVO dayData = workspaceService.getBusinessData(LocalDateTime.of(localDate, LocalTime.MIN), LocalDateTime.of(localDate, LocalTime.MAX));
sheet1.getRow(7 + i).getCell(1).setCellValue(localDate.toString());
sheet1.getRow(7 + i).getCell(2).setCellValue(dayData.getTurnover());
sheet1.getRow(7 + i).getCell(3).setCellValue(dayData.getValidOrderCount());
sheet1.getRow(7 + i).getCell(4).setCellValue(dayData.getOrderCompletionRate());
sheet1.getRow(7 + i).getCell(5).setCellValue(dayData.getUnitPrice());
sheet1.getRow(7 + i).getCell(6).setCellValue(dayData.getNewUsers());
}
// 3. 通过输出流,传输到客户端
outputStream = response.getOutputStream();
sheets.write(outputStream);
outputStream.close();
sheets.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private Integer getOrderCount(LocalDateTime begin, LocalDateTime end, Integer status) {
Map<String, Object> map = new HashMap<>();
map.put("dayStart", begin);
map.put("dayEnd", end);
map.put("status", status);
return orderMapper.countByMap(map);
}
}
11. 前端部分
11.1 vue基础回顾
11.1.1 基于脚手架创建前端工程

使用Vue CLI创建前端工程:
- 方式一:vue create 项目名称
npm i @vue/cli -g
vue create vue-demo-1
- 方式二:vue ui
vue ui

修改前端服务端口号


11.1.2 vue基本使用方式
vue组件
vue的组件文件以.vue结尾,每个组件由三部分组成:
结构 样式 逻辑

<template>
<div id="app"> <!--只能有一个根元素,如果有与此平级的div,则会报错-->
<img alt="Vue logo" src="./assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'App',
components: {
HelloWorld
}
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
文本插值
作用: 用来绑定data方法返回的对象属性
用法: {{}}
<template>
<div>
<h1>
{{age > 60 ? name : "青年"}}
</h1>
</div>
</template>
<script>
export default{
data() {
return {name: "老年", age: 70}
}
}
</script>
属性绑定
作用:为标签的属性绑定data方法中返回的属性
用法: v-bind:xxx,简写为:xxx
<template>
<div class="hello">
<div><input type="text" :value="age"></div>
<div><img :src="src"></div>
</div>
</template>
<script>
export default {
data() {
return {age: 50, name: "张三", src:"https://img-s.msn.cn/tenant/amp/entityid/BB1msKEx?w=0&h=0&q=60&m=6&f=jpg&u=t"};
},
}
事件绑定
作用:为元素绑定对应的事件
用法:v-on:xxx简写为@xxx
<template>
<div class="hello">
<div>
<input type="button" value="Save" @click="save"/>
</div>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
methods: {
save() {
alert(this.name)
}
}
}
</script>
双向绑定
作用:表单输入项和data方法中的属性进行绑定,任意一方改变都会同步给另一方 (输入框和data中的任意地方改变都会影响另一边)
用法: v-model
<template>
<div class="hello">
<h2>{{ name}}</h2>
<input type="text" v-model="name">
<div>
<input type="button" value="点击改变" @click="change"/>
</div>
</div>
</template>
<script>
export default {
data() {
return { name: "张三"};
},
methods: {
change() {
this.name = "李四"
}
}
}
</script>
条件渲染
作用: 根据表达式的值来动态渲染页面元素
用法: v-if,v-else,v-else-if
<template>
<div class="hello">
<div>
<div v-if="sex == 1"> 男 </div>
<div v-else-if="sex == 0"> 女</div>
<div v-else> 未知</div>
</div>
</div>
</template>
<script>
export default {
data() {
return { sex: 0};
}
</script>
axios (异步)
Axios是一个基于promise的网络请求库,作用与浏览器和node.js中
安装命令:npm install axios
导入命令: import axios from 'axios'

为了解决跨域问题,可以在vue.config.js文件中配置代理:
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
devServer:{
port:7070,
proxy: {
// 对前端的请求,前缀是/api的进行代理
'/api': {
target: "http://localhost:8080",
pathRewrite: {
// 将api请求前缀去掉 ==> 将/api替换为空字符串
"^/api": ''
}
}
}
},
})
<template>
<div>
<input type="button" value='get方式请求' @click="getMethod"/>
</div>
</template>
<script>
import axios from 'axios'
export default {
methods: {
getMethod(){
axios.get("/api/admin/shop/status",{
headers:{
token: "eyJhbGciOiJIUzI1NiJ9.eyJlbXBJZCI6MSwiZXhwIjoxNzQ4NjI1MjUwfQ.2UbrxjgMvSqiottavZ0M8r4zBOc8qNzh2Rr_fhFp7dw"
}
}).then(res => {
console.log(res.data),
console.log(res.data.code)
})
}
}
}
</script>
另一种axios请求方式
getMethod2(){
axios({
//默认使用get
mehtod: 'post',
url:'/api/admin/shop/status',
data: {
data01: 1,
data02: "2"
},
headers:{
token: "eyJhbGciOiJIUzI1NiJ9.eyJlbXBJZCI6MSwiZXhwIjoxNzQ4NjI1MjUwfQ.2UbrxjgMvSqiottavZ0M8r4zBOc8qNzh2Rr_fhFp7dw"
}
}).then(res => {
console.log(res.data),
console.log(res.data.code)
})
}
11.2 Vue-Router
11.2.1 Vue-Router 介绍
vue属于单页面应用,所谓的路由,就是根据浏览器路径不同,用不同的视图组件替换这个页面内容
使用vue ui然后手动配置或者使用npm install vue-router
11.2.2 路由配置
- 路由组成
VueRouter:路由器,根据路由请求在路由视图中动态渲染对应的视图组件<router-link>: 路由链接组件,浏览器会解析成<a>router-view: 路由视图组件, 用来展示与路由路径匹配的组件

- 路由链接组件和路由视图组件
<template>
<div id="app">
<!-- 路由链接组件 -->
<nav>
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</nav>
<!-- 路由视图组件展示的位置 -->
<router-view/>
</div>
</template>
- 路由表
import Vue from 'vue'
import VueRouter from 'vue-router'
import HomeView from '../views/HomeView.vue'
Vue.use(VueRouter)
// 维护路由表,某个路由路径对应哪个视图组件
const routes = [
{
path: '/',
name: 'home',
component: HomeView // 静态加载
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited. 懒加载
component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
}
]
const router = new VueRouter({
routes
})
export default router
-
路由配置
- 路由路径和视图对应关系配置
./router/index.js <router-link>:./App.vue<router-view>:./App.vue
- 路由路径和视图对应关系配置
-
<router-link>路由跳转方式:- 标签式:
./App.vue - 编程式:
./App.vue
<template> <div id="app"> <!-- 路由链接组件 --> <nav> <!-- 标签式路由跳转 --> <router-link to="/">Home</router-link> | <router-link to="/about">About</router-link> <!-- 编程式路由跳转 --> <input type="button" value="编程式跳转" @click="jump"/> </nav> <!-- 路由视图组件展示的位置 --> <router-view/> </div> </template> <script> export default { methods: { jump() { this.$router.push("/") } } } </script>- 重定向
{ path: '/404', component: () => import("../views/404View.vue") }, { path: '*', redirect: "/404" } - 标签式:
11.2.3 嵌套路由
嵌套路由: 组件内要切换内容,就需要用到嵌套路由(子路由)

- 安装:
npm i element-ui -Svue3使用element-ui-plus - 导入
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.config.productionTip = false
// 全局使用ElementUI
Vue.use(ElementUI)
new Vue({
router,
render: h => h(App)
}).$mount('#app')
-
提供子视图

<template> <div> 这是P1 </div> </template> -
路由配置
{
path: '/c',
component: () => import("../views/container/containerView.vue"),
children: [ // 嵌套子路由
{
path: '/c/p1',
component: () => import("../views/container/P1View.vue")
},
{
path: '/c/p2',
component: () => import("../views/container/P2View.vue")
},
{
path: '/c/p3',
component: () => import("../views/container/P3View.vue")
}
]
}
- 布局容器中添加
<router-view>和<router-link>
<template>
<el-container>
<el-header>Header</el-header>
<el-container>
<el-aside width="200px">
<router-link to="/c/p1"> P1 View</router-link> <br>
<router-link to="/c/p2"> p2 view</router-link> <br>
<router-link to='/c/p3'> p3 view</router-link>
</el-aside>
<el-main>
<!-- P1 P2 P3 替换的位置 -->
<router-view/>
</el-main>
</el-container>
</el-container>
</template>
11.3 状态管理vuex
11.3.1 vuex介绍
- vuex是一个专门为Vue.js应用程序开发的状态管理库
- vuex可以在多个组件之间共享数据,并且共享的数据是响应式的,即数据的变更能及时渲染到模板
- vuex采用集中式存储管理所有组件的状态
安装: npm install vuex@next --save
核心概念
state:状态对象,集中定义各个组件共享的数据mutations: 类似一个事件,用于修改共享数据,要求必须是同步函数actions: 类似于mutation,可以包含异步操作,通过调用mutation来改变共享数据
11.3.2 使用方式
创建带有vuex功能的脚手架工程
定义共享数据./store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
//共享数据
state: {
name: "未登录游客"
},
getters: {
},
//修改共享数据时只能通过mutation实现,必须是同步操作
mutations: {
},
//通过action可以调用到mutation,action中可以进行异步操作
actions: {
},
modules: {
}
})
使用
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<h1>{{$store.state.name}}</h1>
</div>
</template>
在mutations中定义函数,修改共享数据
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
//共享数据
state: {
name: "未登录游客"
},
getters: {
},
//修改共享数据时只能通过mutation实现,必须是同步操作
mutations: {
setName(state,name){
state.name = name
}
},
//通过action可以调用到mutation,action中可以进行异步操作
actions: {
},
modules: {
}
})
使用
<template>
<div id="app">
<input type="button" value="通过mutations修改state" @click="handleUpdate"/>
</div>
</template>
<script>
export default {
name: "App",
methods: {
handleUpdate(){
// mutation中的函数不能直接调用,必须使用如下方式调用
this.$store.commit('setName', "新名字,李四")
}
}
}
</script>
在actions中定义函数,用于调用mutation
安装axios:npm install axios
定义actions函数:
import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'
Vue.use(Vuex)
export default new Vuex.Store({
//共享数据
state: {
name: "未登录游客"
},
getters: {
},
//修改共享数据时只能通过mutation实现,必须是同步操作
mutations: {
setName(state,name){
state.name = name
}
},
//通过action可以调用到mutation,action中可以进行异步操作
actions: {
setNameByAxios(context){
axios({
url: '/api/admin/employee/login',
method: 'post',
data: {
username: "admin",
password: '123456'
}
}).then(res => {
if(res.data.code == 1){
//异步请求后,需要修改共享数据
// actions中调用mutation定义的函数setName
context.commit('setName', res.data.data.name)
}
})
}
},
modules: {
}
})
App.vue中使用
<template>
<div id="app">
<input type='button' value="员工登录换名" @click="handleLogin"/>
</div>
</template>
<script>
export default {
name: "App",
methods: {
handleLogin(){
// 调用actions当中的函数 setNameByAxios
this.$store.dispatch('setNameByAxios')
}
}
}
</script>
11.4 TypeScript
11.4.1 TypeScript介绍
TypeScript(TS)是微软推出的开源语言
TypeScript是JavaScript的超集(JS有的TS都有)
TypeScript = Type + JavaScript (在JS基础上增加了类型支持)
// JavaScript, 没有明确类型
let age = 18
//TypeScript,有明确类型,即number 数值类型
let age:number = 18
TypeScript 文件扩展名为ts
TypeScript可以编译成标准的JavaScript,并且在编译时进行类型检查
- 安装typescript:
npm install -g typescript - 查看TS版本:
tsc -v - 编译TS为JS:
tsc hello.ts - 运行编译后的TS:
node hello.js


11.4.2 TypeScript常用类型

类型标注的位置
- 标注变量:
let msg:string = "hello" - 标注参数:
oneMethod(name:string){
console.log(name)
}
- 标注返回值
// 定义m函数,参数为string,返回类型为string
const m = (name:string):string =>{
return "返回类型为String" + name
}
字符串、数值、boolean
// string
let uesrname: string = "admin"
// number
let age: number = 20
//boolean
let isTrue: boolean = true
console.log(uesrname)
console.log(age)
console.log(isTrue)
字面量类型
function printText(greet:string, name:"tom"| "lily"| "fei" | "jerry"):string {
return greet + " : " + name
}
console.log(printText("hello" ,"tom"))
interface类型(类似于结构体)
interface Cat {
name: string,
age: number
}
const cat1:Cat = {name: "小白", age: 3}
在interface中定义属性时,可以使用?标注可选的属性
interface Cat {
name: string,
age?: number // 表示属性赋值时,age属性可有可无
}
class类
- 一般用法
class User {
name: string; // 属性
constructor(name: string){ // 构造方法
this.name = name;
}
study() { // 普通方法
console.log('[${this.name}]正在学习')
}
}
const u = new User("Tom")
console.log(u.name)
u.study()
- 实现接口
interface Animal{
name: string,
eat(): void
}
class Bird implements Animal{
name: string
constructor(name: string){
this.name = name
}
eat(): void {
console.log(this.name + "eat bug")
}
}
const b = new Bird("鹦鹉")
b.eat();
- 类的继承
class Parrot extends Bird{
say():void {
console.log("我是鹦鹉:" + this.name)
}
}
const p = new Parrot("小小鹦")
p.eat();
p.say();
11.5 前端项目
11.5.1 前端环境搭建
技术选型
- node.js
- vue
- ElementUI
- axios
- vuex
- vue-router
- typescript
熟悉前端代码结构



浙公网安备 33010602011771号