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进行版本控制:

  1. 创建git本地仓库
  2. 创建git远程仓库
  3. 将本地文件push到git远程仓库

数据库环境搭建:

前后端联调

浏览器 --> Nginx--> 后端服务器

正向代理: 隐藏了客户端的真实地址(VPN)

反向代理:隐藏了服务端的真实地址

Nginx反向代理的好处:

  • 提高访问速度:客户端访问nginx时,nginx中可以做缓存,如果是请求的同一个地址,就不用真正的去请求后端服务,而是直接请求nginx,nginx可以直接返回所需缓存数据
  • 进行负载均衡:按照指定的方式分发给不同的服务器
  • 保证后端服务器安全

反向代理:

负载均衡:

1.3.3 完善登录功能

问题:员工表中的密码是明文存储的,安全性太低

步骤:

  1. 将密码加密后进行存储,提高安全性
  2. 使用MD5加密方式对明文密码加密
  3. 修改数据库中的明文密码,改为MD5加密后的密文
  4. 修改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 使用方式

  1. 导入knife4j的maven坐标
  2. 在配置类中加入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;
    }
  1. 设置静态资源映射,否则接口文档页面无法访问
/**
 * 设置静态资源映射
 * @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 代码完善

程序存在的问题:

  1. 录入的用户名如果已经存在,抛出的异常没有处理
    • 添加全局异常方法,捕获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);
        }

    }
  1. 新增员工时,创建人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 需求分析和设计

  • 产品原型

  • 业务规则
    • 分类名称必须是唯一的
    • 分类按照类型可以分为菜品分类和套餐分类
    • 新添加的分类状态默认为“禁用”
  • 接口设计
    1. 新增分类
    2. 分类分页查询
    3. 根据id删除分类
    4. 修改分类
    5. 启用禁用分类
    6. 根据类型查询分类

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结构数据库

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

官网:https://redis.io

中文网:https://www.redis.net.cn

4.1.2 Redis下载与安装

Redis 安装包分为Windows版和Linux版

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使用方式

操作步骤:

  1. 导入Spring Data Redis的maven坐标

  2. 配置Redis数据源

  3. 编写配置类,创建RedisTemplate对象

  4. 通过RedisTemplate对象操作Redis

  5. 导入Spring Data Redis的maven坐标

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

  1. 配置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
  1. 编写配置类,创建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;


    }
}
  1. 通过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个文件组成:

  1. js:页面逻辑
  2. wxml:页面结构
  3. json:页面配置
  4. 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 微信支付介绍

产品介绍_JSAPI支付|微信支付商户文档中心

7.2.2 微信支付准备工作

  • 内网穿透
  1. 下载cpolar:cpolar - secure introspectable tunnels to localhost
  2. 连接账户:cpolar.exe authtoken ZGY2N2M3ZTUtNjZmMC00OTVmLWEwMDAtOGQzNDQ4OWZkZWU4
  3. 运行:cpolar.exe http 80

8. 订单状态定时处理、来单提醒和客户催单

8.1 Spring Task

8.1.1 介绍

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

  • 应用场景:

    • 信用卡每月还款提醒
    • 在线支付应用处理未支付订单
    • 某个纪念日为用户发送通知
  • 使用步骤

    1. 启动类加@EnableScheduling
    2. 方法上加@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 使用步骤
    1. 导入maven坐标 spring-context (spring boot默认已导入)
    2. 启动类添加注解@EnableScheduling开启任务调度
    3. 自定义定时任务类(需要加上@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的一种新的网络协议。它实现了浏览器与服务器全双工通信----浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输

  • 单工通信只支持信号在一个方向上传输(正向或反向),任何时候不能改变信号的传输方向。

  • 半双工通信允许信号在两个方向上传输,但某一时刻只允许信号在一个信道上单向传输。

  • 全双工通信允许数据同时在两个方向上传输,即有两个信道,因此允许同时进行双向传输。

  • 应用场景

    • 视频弹幕
    • 网页聊天
    • 体育实况更新
    • 股票基金报价实时更新
  • 入门案例

实现步骤:

  1. 使用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>

  1. 导入WebSocket的maven坐标
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
  1. 导入WebSocket服务端组件WebSocketServer, 用于和客户端通信

  2. 导入配置类WebSocketConfiguration,注册WebSocket服务端组件

  3. 导入定时任务类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 &lt; #{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 &lt; #{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 &gt; #{dayStart}
            </if>
            <if test="dayEnd != null">
                and order_time &lt; #{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 &gt; #{dayStart}
                    </if>
                    <if test="dayEnd != null">
                        and o.order_time &lt; #{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报表

· 批量导入业务数据

  • 入门案例
  1. 导入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 &lt; ? and  order_time &gt; ?
            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

  • 路由配置

    1. 路由路径和视图对应关系配置 ./router/index.js
    2. <router-link>: ./App.vue
    3. <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 嵌套路由

嵌套路由: 组件内要切换内容,就需要用到嵌套路由(子路由)

  1. 安装: npm i element-ui -S vue3使用element-ui-plus
  2. 导入
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')

  1. 提供子视图

    <template>
      <div> 这是P1 </div>
    </template>
    
  2. 路由配置

  {
    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")
      }
    ]

  }
  1. 布局容器中添加<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
熟悉前端代码结构

11.5.2 员工分页查询

11.5.3 启用/禁用员工账号

11.5.4 新增员工

11.5.5 修改员工

posted @ 2025-11-23 21:38  飞↑  阅读(4)  评论(0)    收藏  举报