瑞吉外卖项目功能全实现及完全代码解析

目录

1.项目简介

1.1 项目来源

1.2 项目简介 

1.3 项目使用

1.31.管理端

1.32.用户端

1.4 技术选型

2.项目版本1.0代码

2.1 依赖pom.xml

2.2 配置application.yml

2.3 项目启动类ReggieApplication

2.4 filter包

 2.41过滤器LoginCheckFilter

2.5 common包

2.51用户线程类BaseContext

2.52自定义业务异常类CustomException

2.53全局异常处理类GlobalExceptionHandler

2.54自定义元数据对象处理器类MyMetaObjectHandler

2.55自定义结果返回类R

2.6 config包

2.61对象映射器JacksonObjectMapper

2.62配置Mybatis-plus的分页插件MybatisPlusConfig

2.63静态资源映射WebMvcConfig

2.7 util包

2.71随机生成验证码工具类ValidateCodeUtils

2.8 entity包

2.81地址簿AddressBookController

2.82分类Category

2.83菜品Dish

2.84菜品口味DishFlavor

2.85员工实体类Employee

2.86订单明细OrderDetail

2.87订单Orders

2.88套餐Setmeal

2.89套餐菜品关系SetmealDish

2.810购物车ShoppingCart

2.811用户信息User

2.9 dto包

2,91DishDto

2.92OrderDto

2.93SetmealDto

2.10 mapper包、service包、controller包

2.101文件上传和下载

2.102员工管理

2.103分类管理

2.104菜品管理

2.105套餐管理

2.106移动端登录

2.107地址管理

2.108订单管理

2.109购物车管理

3.项目改进2.0代码

3.1Linux项目部署

3.2项目优化

3.21环境准备

3.22缓存邮箱验证码

3.23查询菜品缓存

3.24清理菜品缓存

3.25SpringCache技术

3.26缓存套餐数据

3.3数据库读写分离

3.31目前数据库存在的困难

3.32MySQL主从复制简介与服务器的克隆

3.33主库配置

3.34从库配置

3.35项目读写分离

4.工程结构

5.代码下载


1.项目简介

1.1 项目来源

瑞吉外卖项目实战导学_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV13a411q753?p=1&vd_source=f2391424d1c9c217b2fb52874a510ad1

1.2 项目简介 

瑞吉外卖项目功能全实现及完全代码解析_第1张图片 本项目(瑞吉外卖)是专门为餐饮企业(餐厅、饭店)定制的一款软件产品,包括 系统管理后台 和 移动端应用 两部分。其中系统管理后台主要提供给餐饮企业内部员工使用,可以对餐厅的分类、菜品、套餐、订单、员工等进行管理维护。移动端应用主要提供给消费者使用,可以在线浏览菜品、添加购物车、下单等。

本项目共分为3期进行开发:

阶段 功能实现
第一期 主要实现基本需求,其中移动端应用通过H5实现,用户可以通过手机浏览器访问
第二期 主要针对移动端应用进行改进,使用微信小程序实现,用户使用起来更加方便
第三期 主要针对系统进行优化升级,提高系统的访问性能

1.3 项目使用

1.31.管理端

餐饮企业内部员工使用。 主要功能有:

模块 描述
登录/退出 内部员工必须登录后,才可以访问系统管理后台
员工管理 管理员可以在系统后台对员工信息进行管理,包含查询、新增、编辑、禁用等功能
分类管理 主要对当前餐厅经营的 菜品分类 或 套餐分类 进行管理维护, 包含查询、新增、修改、删除等功能
菜品管理 主要维护各个分类下的菜品信息,包含查询、新增、修改、删除、启售、停售等功能
套餐管理 主要维护当前餐厅中的套餐信息,包含查询、新增、修改、删除、启售、停售等功能
订单明细 主要维护用户在移动端下的订单信息,包含查询、取消、派送、完成,以及订单报表下载等功能

1.32.用户端

移动端应用主要提供给消费者使用。主要功能有:

模块 描述
登录/退出 在移动端, 用户也需要登录后使用APP进行点餐
点餐-菜单 在点餐界面需要展示出菜品分类/套餐分类, 并根据当前选择的分类加载其中的菜品信息, 供用户查询选择
点餐-购物车 用户选中的菜品就会加入用户的购物车, 主要包含 查询购物车、加入购物车、删除购物车、清空购物车等功能
订单支付 用户选完菜品/套餐后, 可以对购物车菜品进行结算支付, 这时就需要进行订单的支付
个人信息 在个人中心页面中会展示当前用户的基本信息, 用户可以管理收货地址, 也可以查询历史订单数据

1.4 技术选型

关于本项目的技术选型, 我们将会从 用户层、网关层、应用层、数据层 这几个方面进行介绍,而对于我们服务端开发工程师来说,在项目开发过程中,我们主要关注应用层及数据层技术的应用。

瑞吉外卖项目功能全实现及完全代码解析_第2张图片

1.41.用户层

本项目中在构建系统管理后台的前端页面,我们会用到H5、Vue.js、ElementUI等技术。而在构建移动端应用时,我们会使用到微信小程序。

1.42.网关层

Nginx是一个服务器,主要用来作为Http服务器,部署静态资源,访问性能高。在Nginx中还有两个比较重要的作用: 反向代理和负载均衡, 在进行项目部署时,要实现Tomcat的负载均衡,就可以通过Nginx来实现。

1.43.应用层

SpringBoot: 快速构建Spring项目, 采用 "约定优于配置" 的思想, 简化Spring项目的配置开发。

Spring: 统一管理项目中的各种资源(bean), 在web开发的各层中都会用到。

SpringMVC:SpringMVC是spring框架的一个模块,springmvc和spring无需通过中间整合层进行整合,可以无缝集成。

SpringSession: 主要解决在集群环境下的Session共享问题。

lombok:能以简单的注解形式来简化java代码,提高开发人员的开发效率。例如开发中经常需要写的javabean,都需要花时间去添加相应的getter/setter,也许还要去写构造器、equals等方法。

Swagger: 可以自动的帮助开发人员生成接口文档,并对接口进行测试。

1.44.数据层

MySQL: 关系型数据库, 本项目的核心业务数据都会采用MySQL进行存储。

MybatisPlus: 本项目持久层将会使用MybatisPlus来简化开发, 基本的单表增删改查直接调用框架提供的方法即可。

Redis: 基于key-value格式存储的内存数据库, 访问速度快, 经常使用它做缓存(降低数据库访问压力, 提供访问效率), 在后面的性能优化中会使用。

1.45.工具

Git: 版本控制工具, 在团队协作中, 使用该工具对项目中的代码进行管理。

Maven: 项目构建工具。

Junit:单元测试工具,开发人员功能实现完毕后,需要通过junit对功能进行单元测试。

1.46.用户角色

在瑞吉外卖这个项目中,存在以下三种用户,这三种用户对应三个角色: 后台系统管理员、后台系统普通员工、C端(移动端)用户。

角色 权限操作
后台系统管理员 登录后台管理系统,拥有后台系统中的所有操作权限
后台系统普通员工 登录后台管理系统,对菜品、套餐、订单等进行管理 (不包含员工管理)
C端用户 登录移动端应用,可以浏览菜品、添加购物车、设置地址、在线下单等

2.项目版本1.0代码

2.1 依赖pom.xml




  4.0.0

  
    org.springframework.boot
    spring-boot-starter-parent
    2.4.5
     
  

  com.hjg
  reggie_take_out
  1.0-SNAPSHOT
  
  war

  
    1.8
  

  

    
      org.springframework.boot
      spring-boot-starter
    

    
      org.springframework.boot
      spring-boot-starter-test
      test
    

    
      org.springframework.boot
      spring-boot-starter-web
      compile
    

    
      com.baomidou
      mybatis-plus-boot-starter
      3.4.2
    

    
      org.projectlombok
      lombok
      1.18.20
    

    
      com.alibaba
      fastjson
      1.2.76
    

    
      commons-lang
      commons-lang
      2.6
    

    
      mysql
      mysql-connector-java
      runtime
    

    
      com.alibaba
      druid-spring-boot-starter
      1.1.23
    

    
    
      org.springframework.boot
      spring-boot-starter-mail
    
    
    
      org.springframework.boot
      spring-boot-starter-thymeleaf
    

  

  
    
      
        org.springframework.boot
        spring-boot-maven-plugin
        2.4.5
      
    
  

2.2 配置application.yml

server:
  port: 8080

spring:
  application:
    # 应用的名称
    name: reggie_take_out
  datasource:
    # druid数据库连接
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/reggie?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
      username: root
      password: ****
  # 邮箱配置
  mail:
    host: smtp.qq.com  # 发送邮件的服务器地址
    username: **********@qq.com # 开启 IMAP/SMTP服务 的qq邮箱的账号
    password: **********  # 开启 IMAP/SMTP服务 获得的授权码,而不是qq邮箱的登录密码
    default-encoding: UTF-8

mybatis-plus:
  configuration:
    #在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射 address_book ---> AddressBook
    map-underscore-to-camel-case: true
    #日志输出
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      #该配置完成后,每个模型类的主键ID策略都将成为assign_id
      id-type: ASSIGN_ID

#文件图片等保存路径,这样写方便修改
reggie:
  path: E:\reggie_img\

2.3 项目启动类ReggieApplication

package com.hjg.reggie;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.transaction.annotation.EnableTransactionManagement;

/*
* @Slf4j:开启日志
* @SpringBootApplication:它会自动为我们扫描配置,最终成功启动项目
* @ServletComponentScan:扫描组件,如WebFilter登录过滤器等
* */
@Slf4j
@SpringBootApplication
@ServletComponentScan
@EnableTransactionManagement //开启对事物管理的支持
public class ReggieApplication {
    public static void main(String[] args) {
        SpringApplication.run(ReggieApplication.class,args);
        log.info("项目启动成功...");
    }
}

2.4 filter包

 2.41过滤器LoginCheckFilter

package com.hjg.reggie.filter;

import com.alibaba.fastjson.JSON;
import com.hjg.reggie.common.BaseContext;
import com.hjg.reggie.common.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.AntPathMatcher;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 检查用户是否已经完成登录
 */
@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter implements Filter {
    //路径匹配器,支持通配符
    public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();

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

        //1、获取本次请求的URI
        String requestURI = request.getRequestURI();// /backend/index.html

        log.info("拦截到请求:{}",requestURI);

        //定义不需要处理的请求路径
        String[] urls = new String[]{
                "/employee/login",//登录不拦截
                "/employee/logout",//退出登录不拦截
                "/backend/**",//backend的静态资源不拦截
                "/front/**",//front的静态资源不拦截
                "/user/sendMsg",//移动端发送短信不拦截
                "/user/login"//移动端登录不拦截
        };

        //2、判断本次请求是否需要处理
        boolean check = check(urls, requestURI);

        //3、如果不需要处理,则直接放行
        if(check){
            log.info("本次请求{}不需要处理",requestURI);
            filterChain.doFilter(request,response);
            return;
        }

        //4-1、判断后端登录状态,如果已登录,则直接放行
        if(request.getSession().getAttribute("employee") != null){
            log.info("用户后端已登录,用户id为:{}",request.getSession().getAttribute("employee"));

            //将用户id放入线程
            long empId = (long) request.getSession().getAttribute("employee");
            BaseContext.setCurrentId(empId);

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

        //4-2、判断移动端登录状态,如果已登录,则直接放行
        if(request.getSession().getAttribute("user") != null){
            log.info("用户移动端已登录,用户id为:{}",request.getSession().getAttribute("user"));

            Long userId = (Long) request.getSession().getAttribute("user");
            BaseContext.setCurrentId(userId);

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

        //5、如果未登录则返回未登录结果,通过输出流方式向客户端页面响应数据
        //未登录的跳转页面在前端的request.js中实现,将R对象转成JSON后传给这个js文件
        log.info("用户未登录");
        response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
        return;

    }

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

2.5 common包

2.51用户线程类BaseContext

package com.hjg.reggie.common;

/*
 * 1.ThreadLocal为每个线程提供单独一份存储空间
 * 具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问当前线程对应的值
 *
 * 2.在LoginCheckFilter的doFilter方法中获取当前登录用户id,并调用ThreadLocal的set方法来设置当前线程的线程局部变量的值(用户id),
 * 然后在MyMetaObjectHandler的updateFill方法中调用ThreadLocal的get方法来获得当前线程所对应的线程局部变量的值(用户id)。
 * 如果在后续的操作中, 我们需要在Controller / Service中要使用当前登录用户的ID, 可以直接从ThreadLocal直接获取。
 * */

/**
 * 基于ThreadLocal封装工具类,用户保存和获取当前登录用户id
 */
public class BaseContext {
    private static ThreadLocal threadLocal = new ThreadLocal<>();
    /**
     * 设置值
     * @param id
     */
    public static void setCurrentId(Long id){
        threadLocal.set(id);
    }
    /**
     * 获取值
     * @return
     */
    public static Long getCurrentId(){
        return threadLocal.get();
    }
}

2.52自定义业务异常类CustomException

package com.hjg.reggie.common;

/*
* 自定义业务异常
* 继承了运行时异常
* 1.查询当前分类是否关联了套餐或者菜品,如果已经关联不允许删除,抛出这个业务异常
*   该异常将在全局异常处理器中被捕获,并获取它携带的的信息
* */
public class CustomException extends RuntimeException{
    public CustomException(String message){
        super(message);
    }
}

2.53全局异常处理类GlobalExceptionHandler

package com.hjg.reggie.common;

/*
* 在 employee 表结构中,我们针对于username字段,建立了唯一索引,添加重复的username数据时,违背该约束,就会报错。
* 但是此时前端提示的信息并不具体,用户并不知道是因为什么原因造成的该异常,我们需要给用户提示详细的错误信息。
* 因此设置此全局异常处理器
* */

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import java.sql.SQLIntegrityConstraintViolationException;

/*
* ControllerAdvice这个代表拦截所有带RestController和Controller的类
* ResponseBody将结果封装成json数据并返回
* */
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 异常处理方法
     * 目的:禁止重复添加具有唯一约束字段的数据(如:相同username的员工用户,相同sort的菜品等)
     * @return
     */
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public R exceptionHandler(SQLIntegrityConstraintViolationException ex){
        log.error(ex.getMessage());
        //如果异常信息包括这个关键字,说明了已经违反了username的唯一约束
        //IDEA抛出异常信息例子:Duplicate entry 'zhangsan' for key 'employee.idx_username'
        if(ex.getMessage().contains("Duplicate entry")){
            String[] split = ex.getMessage().split(" ");
            String msg = split[2] + "已存在";
            return R.error(msg);
        }
        //如果产生其他异常信息,则输出"未知错误"
        return R.error("未知错误");
    }

    /**
     * 异常处理方法
     * 这是自己写的异常,捕获这个异常,并收到它携带的信息
     * @return
     */
    @ExceptionHandler(CustomException.class)
    public R exceptionHandler(CustomException ex){
        log.error(ex.getMessage());
        return R.error(ex.getMessage());
    }
}

2.54自定义元数据对象处理器类MyMetaObjectHandler

package com.hjg.reggie.common;

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

/**
 * 自定义元数据对象处理器
 */
@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {
    /**
     * 插入操作,自动填充,注意这两个方法必须同时包含createTime和updateTime才可使用
     * @param metaObject
     */
    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("公共字段自动填充[insert]...");
        log.info(metaObject.toString());

        metaObject.setValue("createTime", LocalDateTime.now());
        metaObject.setValue("updateTime",LocalDateTime.now());
        /*
        我们在自动填充createUser和updateUser时设置的用户id是固定值
        因此我们需要完善,改造成动态获取当前登录用户的id
        MyMetaObjectHandler类中是不能直接获得HttpSession对象的,所以我们需要通过其他方式来获取登录用户id
        */
        metaObject.setValue("createUser",BaseContext.getCurrentId());
        metaObject.setValue("updateUser",BaseContext.getCurrentId());
    }

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

        //在线程中设置id,以便MyMetaObjectHandler自动填充器使用
        long id = Thread.currentThread().getId();
        log.info("线程id为:{}",id);

        metaObject.setValue("updateTime",LocalDateTime.now());
        metaObject.setValue("updateUser",BaseContext.getCurrentId());
    }
}

2.55自定义结果返回类R

设计表现层返回结果的模型类,用于后端与前端进行数据格式统一,也称为前后端数据协议 

package com.hjg.reggie.common;

import lombok.Data;
import java.util.HashMap;
import java.util.Map;
/*
* 通用返回结果,服务端响应的数据最终都会封装成此对象
* */
@Data
public class R {

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

    private String msg; //错误信息

    private T data; //数据

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

    public static  R success(T object) {
        R r = new R();
        r.data = object;
        r.code = 1;
        return r;
    }

    public static  R error(String msg) {
        R r = new R();
        r.msg = msg;
        r.code = 0;
        return r;
    }

    public R add(String key, Object value) {
        this.map.put(key, value);
        return this;
    }

}

2.6 config包

2.61对象映射器JacksonObjectMapper

package com.hjg.reggie.config;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;

/**
 * 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
 * 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
 * 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
 */
/*
* 解决问题:1.雪花算法生成的id过长,js处理时丢失精度,导致id和数据库中的不匹配,从而更新失败问题
* 2.改变时间的格式等
* */
public class JacksonObjectMapper extends ObjectMapper {

    public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
    public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
    public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";

    public JacksonObjectMapper() {
        super();
        //收到未知属性时不报异常
        this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);

        //反序列化时,属性不存在的兼容处理
        this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);


        SimpleModule simpleModule = new SimpleModule()
                .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))

                .addSerializer(BigInteger.class, ToStringSerializer.instance)
                //将雪花算法等过长的long类型数据转化为string类型
                .addSerializer(Long.class, ToStringSerializer.instance)
                //改变日期时间的格式
                .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));

        //注册功能模块 例如,可以添加自定义序列化器和反序列化器
        this.registerModule(simpleModule);
    }
}

2.62配置Mybatis-plus的分页插件MybatisPlusConfig

package com.hjg.reggie.config;

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 配置Mybatis-plus的分页插件
 */
@Configuration
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return mybatisPlusInterceptor;
    }
}

2.63静态资源映射WebMvcConfig

package com.hjg.reggie.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

import java.util.List;

@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
    /**
     * 设置静态资源映射
     * 使启动项目后可以访问到HTML等静态资源
     * @param registry
     */
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        log.info("开始进行静态资源映射...");
        registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
        registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
    }

    /**
     * 扩展mvc框架的消息转换器
     * @param converters
     */
    @Override
    protected void extendMessageConverters(List> converters) {
        log.info("扩展消息转换器...");
        //创建消息转换器对象
        MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
        //设置对象转换器,底层使用Jackson将Java对象转为json
        messageConverter.setObjectMapper(new JacksonObjectMapper());
        //将上面的消息转换器对象追加到mvc框架的转换器集合中
        //在启动时添加自己写的基于jackson将Java对象转为json的对象映射器JacksonObjectMapper
        converters.add(0,messageConverter);
    }
}

2.7 util包

2.71随机生成验证码工具类ValidateCodeUtils

package com.hjg.reggie.utils;

import java.util.Random;

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

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

2.8 entity包

2.81地址簿AddressBookController

package com.hjg.reggie.entity;

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

/**
 * 地址簿
 */
@Data
public class AddressBook implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;


    //用户id
    private Long userId;


    //收货人
    private String consignee;


    //手机号
    private String phone;


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


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


    //省级名称
    private String provinceName;


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


    //市级名称
    private String cityName;


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


    //区级名称
    private String districtName;


    //详细地址
    private String detail;


    //标签
    private String label;

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

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


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


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


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


    //是否删除
    //逻辑删除,value为正常数据的值,delval为删除数据的值
    @TableLogic(value="0",delval="1")
    private Integer isDeleted;
}

2.82分类Category

package com.hjg.reggie.entity;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
import java.time.LocalDateTime;

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

    private static final long serialVersionUID = 1L;

    private Long id;


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


    //分类名称
    private String name;


    //顺序
    private Integer sort;


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


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


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


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


    //是否删除
    //逻辑删除,value为正常数据的值,delval为删除数据的值
    @TableLogic(value="0",delval="1")
    private Integer isDeleted;

}

2.83菜品Dish

package com.hjg.reggie.entity;

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

/**
 菜品
 */
@Data
public class Dish implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;


    //菜品名称
    private String name;


    //菜品分类id
    private Long categoryId;


    //菜品价格
    private BigDecimal price;


    //商品码
    private String code;


    //图片
    private String image;


    //描述信息
    private String description;


    //0 停售 1 起售
    private Integer status;


    //顺序
    private Integer sort;


    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;


    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;


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


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


    //是否删除
    //逻辑删除,value为正常数据的值,delval为删除数据的值
    @TableLogic(value="0",delval="1")
    private Integer isDeleted;

}

2.84菜品口味DishFlavor

package com.hjg.reggie.entity;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;

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

    private static final long serialVersionUID = 1L;

    private Long id;


    //菜品id
    private Long dishId;


    //口味名称
    private String name;


    //口味数据list
    private String value;


    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;


    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;


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


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


    //是否删除
    //逻辑删除,value为正常数据的值,delval为删除数据的值
    @TableLogic(value="0",delval="1")
    private Integer isDeleted;

}

2.85员工实体类Employee

package com.hjg.reggie.entity;

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

/*
* 员工实体类
* */
@Data
public class Employee implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;

    private String username;

    private String name;

    private String password;

    private String phone;

    private String sex;

    private String idNumber;//驼峰命名法 ---> 映射的字段名为 id_number

    private Integer status;

    /*
    * 为什么要完成字段自动填充?
    * 因为每次添加修改操作都要设置更新人员id和更新时间等,很麻烦
    *
    * 实现步骤:
        1、在实体类的属性上加入@TableField注解,指定自动填充的策略。
        2、按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现MetaObjectHandler接口。
    * */
    @TableField(fill = FieldFill.INSERT)//完成字段自动填充,插入时填充该属性值
    private LocalDateTime createTime;

    @TableField(fill = FieldFill.INSERT_UPDATE)//完成字段自动填充,插入和更新时填充该属性值
    private LocalDateTime updateTime;

    @TableField(fill = FieldFill.INSERT)//完成字段自动填充,插入时填充该属性值
    private Long createUser;

    @TableField(fill = FieldFill.INSERT_UPDATE)//完成字段自动填充,插入和更新时填充该属性值
    private Long updateUser;

}

2.86订单明细OrderDetail

package com.hjg.reggie.entity;

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

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

    private static final long serialVersionUID = 1L;

    private Long id;

    //名称
    private String name;

    //订单id
    private Long orderId;


    //菜品id
    private Long dishId;


    //套餐id
    private Long setmealId;


    //口味
    private String dishFlavor;


    //数量
    private Integer number;

    //金额
    private BigDecimal amount;

    //图片
    private String image;
}

2.87订单Orders

package com.hjg.reggie.entity;

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

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

    private static final long serialVersionUID = 1L;

    private Long id;

    //订单号
    private String number;

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


    //下单用户id
    private Long userId;

    //地址id
    private Long addressBookId;


    //下单时间
    private LocalDateTime orderTime;


    //结账时间
    private LocalDateTime checkoutTime;


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


    //实收金额
    private BigDecimal amount;

    //备注
    private String remark;

    //用户名
    private String userName;

    //手机号
    private String phone;

    //地址
    private String address;

    //收货人
    private String consignee;
}

2.88套餐Setmeal

package com.hjg.reggie.entity;

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

/**
 * 套餐
 */
@Data
public class Setmeal implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;


    //分类id
    private Long categoryId;


    //套餐名称
    private String name;


    //套餐价格
    private BigDecimal price;


    //状态 0:停用 1:启用
    private Integer status;


    //编码
    private String code;


    //描述信息
    private String description;


    //图片
    private String image;


    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;


    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;


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


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


    //是否删除
    //逻辑删除,value为正常数据的值,delval为删除数据的值
    @TableLogic(value="0",delval="1")
    private Integer isDeleted;
}

2.89套餐菜品关系SetmealDish

package com.hjg.reggie.entity;

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

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

    private static final long serialVersionUID = 1L;

    private Long id;


    //套餐id
    private Long setmealId;


    //菜品id
    private Long dishId;


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

    //菜品原价
    private BigDecimal price;

    //份数
    private Integer copies;


    //排序
    private Integer sort;


    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;


    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;


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


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


    //是否删除
    //逻辑删除,value为正常数据的值,delval为删除数据的值
    @TableLogic(value="0",delval="1")
    private Integer isDeleted;
}

2.810购物车ShoppingCart

package com.hjg.reggie.entity;

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

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

    private static final long serialVersionUID = 1L;

    private Long id;

    //名称
    private String name;

    //用户id
    private Long userId;

    //菜品id
    private Long dishId;

    //套餐id
    private Long setmealId;

    //口味
    private String dishFlavor;

    //数量
    private Integer number;

    //金额
    private BigDecimal amount;

    //图片
    private String image;

    private LocalDateTime createTime;
}

2.811用户信息User

package com.hjg.reggie.entity;

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

    private static final long serialVersionUID = 1L;

    private Long id;


    //姓名
    private String name;


    //手机号
    private String phone;


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


    //身份证号
    private String idNumber;


    //头像
    private String avatar;


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

2.9 dto包

2,91DishDto

package com.hjg.reggie.dto;

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

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

/*
* | 实体模型 | 描述                                                         |
  | --------| ------------------------------------------------------------ |
  | DTO     | Data Transfer Object(数据传输对象),一般用于展示层与服务层之间的数据传输。 |
  | Entity  | 最常用实体类,基本和数据表一一对应,一个实体类对应一张表。   |
  | VO      | Value Object(值对象), 主要用于封装前端页面展示的数据对象,用一个VO对象来封装整个页面展示所需要的对象数据 |
  | PO      | Persistant Object(持久层对象), 是ORM(Objevt Relational Mapping)框架中Entity,PO属性和数据库中表的字段形成一一对应关系 |
* */
//将对多表查询的数据封装成dto对象
@Data
public class DishDto extends Dish {
    private List flavors = new ArrayList<>();//菜品对应的口味数据

    private String categoryName;//菜品分类名称

    private Integer copies;
}

2.92OrderDto

package com.hjg.reggie.dto;

import com.hjg.reggie.entity.OrderDetail;
import com.hjg.reggie.entity.Orders;
import lombok.Data;

import java.util.List;

@Data
public class OrderDto extends Orders {

    private List orderDetails;
}

2.93SetmealDto

package com.hjg.reggie.dto;

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

@Data
public class SetmealDto extends Setmeal {

    private List setmealDishes;//套餐关联的菜品集合

    private String categoryName;//分类名称
}

2.10 mapper包、service包、controller包

2.101文件上传和下载

CommonController
package com.hjg.reggie.controller;

import com.hjg.reggie.common.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
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 javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.UUID;

/**
 * 文件上传和下载
 */
@RestController
@RequestMapping("/common")
@Slf4j
public class CommonController {
    //获取配置中写的文件保存路径
    @Value("${reggie.path}")
    private String basePath;

    /**
     * 文件上传
     * @param file
     * @return
     */
    @PostMapping("/upload")
    public R upload(MultipartFile file){
        //MultipartFile的名字必须叫file,和前端保持一致,不能随意改
        //file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会删除
        log.info(file.toString());

        //原始文件名
        String originalFilename = file.getOriginalFilename();//abc.jpg
        String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));

        //使用UUID重新生成文件名,防止文件名称重复造成文件覆盖,和文件后缀拼接起来形成新的文件
        String fileName = UUID.randomUUID().toString() + suffix;

        //创建一个目录对象
        File dir = new File(basePath);
        //判断当前目录是否存在
        if(!dir.exists()){
            //目录不存在,需要创建
            dir.mkdirs();
        }

        try {
            //将临时文件转存到指定位置
            file.transferTo(new File(basePath + fileName));
        } catch (IOException e) {
            e.printStackTrace();
        }
        return R.success(fileName);
    }

    /**
     * 文件下载,此操作是将图片文件上传成功后,读取文件名字,进行图片在页面的回显
     * 逻辑:上传到后台服务器中,从服务器回显图片到前端
     * @param name
     * @param response
     */
    @GetMapping("/download")
    public void download(String name, HttpServletResponse response) throws IOException {
        FileInputStream fileInputStream=null;
        ServletOutputStream outputStream=null;

        try {
            //输入流,通过输入流读取文件内容
            fileInputStream = new FileInputStream(new File(basePath + name));

            //输出流,通过输出流将文件写回浏览器
            outputStream = response.getOutputStream();

            response.setContentType("image/jpeg");

            int len = 0;
            byte[] bytes = new byte[1024];
            while ((len = fileInputStream.read(bytes)) != -1){
                outputStream.write(bytes,0,len);
                outputStream.flush();
            }

        } catch (Exception e) {
            e.printStackTrace();
        }finally{
            //关闭资源
            outputStream.close();
            fileInputStream.close();
        }
    }
}

瑞吉外卖项目功能全实现及完全代码解析_第3张图片

2.102员工管理

EmployeeMapper
package com.hjg.reggie.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.hjg.reggie.entity.Employee;
import org.apache.ibatis.annotations.Mapper;
/*
* 此操作Mybatis-plus提供
* */
@Mapper
public interface EmployeeMapper extends BaseMapper {
}
EmployeeService
package com.hjg.reggie.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.hjg.reggie.entity.Employee;
/*
 * 此操作Mybatis-plus提供
 * */
public interface EmployeeService extends IService {
}
EmployeeServiceImpl
package com.hjg.reggie.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hjg.reggie.entity.Employee;
import com.hjg.reggie.mapper.EmployeeMapper;
import com.hjg.reggie.service.EmployeeService;
import org.springframework.stereotype.Service;
/*
 * 此操作Mybatis-plus提供
 * */
@Service
public class EmployeeServiceImpl extends ServiceImpl implements EmployeeService {
}
EmployeeController
package com.hjg.reggie.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hjg.reggie.common.R;
import com.hjg.reggie.entity.Employee;
import com.hjg.reggie.service.EmployeeService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.DigestUtils;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;

@Slf4j
@RestController
@RequestMapping("/employee")
public class EmployeeController {

    @Autowired
    private EmployeeService employeeService;

    /**
     * 员工登录
     * @param request
     * @param employee
     * @return
     */
    //登录时传的数据为post类型的json对象,@ResponseBody响应浏览器json数据
    @PostMapping("/login")
    public R login(HttpServletRequest request, @RequestBody Employee employee){
        //1、将页面提交的密码password进行md5加密处理
        String password = employee.getPassword();
        password = DigestUtils.md5DigestAsHex(password.getBytes());

        //2、根据页面提交的用户名username查询数据库(Mybatis-plus操作)
        LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Employee::getUsername,employee.getUsername());
        Employee emp = employeeService.getOne(queryWrapper);

        //3、如果没有查询到则返回登录失败结果
        if(emp == null){
            return R.error("登录失败");
        }

        //4、密码比对,如果不一致则返回登录失败结果
        if(!emp.getPassword().equals(password)){
            return R.error("登录失败");
        }

        //5、查看员工状态,如果为已禁用状态,则返回员工已禁用结果
        if(emp.getStatus() == 0){
            return R.error("账号已禁用");
        }

        //6、登录成功,将员工id存入Session并返回登录成功结果
        request.getSession().setAttribute("employee",emp.getId());
        return R.success(emp);
    }

    /**
     * 员工退出
     * @param request
     * @return
     */
    @PostMapping("/logout")
    public R logout(HttpServletRequest request){
        //清理Session中保存的当前登录员工的id
        request.getSession().removeAttribute("employee");
        return R.success("退出成功");
    }

    /**
     * 新增员工
     * 添加页面为post类型的地址/employee,这个也为:/employee,因此这个就不用再写了
     * @param employee
     * @return
     */
    @PostMapping
    public R save(HttpServletRequest request,@RequestBody Employee employee){
        log.info("新增员工,员工信息:{}",employee.toString());

        //设置初始密码123456,需要进行md5加密处理
        employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));

        /*设置了自动填充MyMetaObjectHandler,它会帮助我们填充更新人员id和时间
        employee.setCreateTime(LocalDateTime.now());
        employee.setUpdateTime(LocalDateTime.now());
        */

        //获得当前登录用户的id,即获取进行添加用户这个操作的人的id
        //Long empId = (Long) request.getSession().getAttribute("employee");

        /*employee.setCreateUser(empId);
        employee.setUpdateUser(empId);*/

        //用Mybatis-plus的方法
        employeeService.save(employee);

        return R.success("新增员工成功");
    }

    /**
     * 员工信息分页查询
     * @param page 当前查询页码
     * @param pageSize 每页展示记录数
     * @param name 员工姓名 - 可选参数
     * 页面传的数据名称和所设置名称相同,因此可以一一对应的接收到分页查询提交的的数据
     * @return
     */
    @GetMapping("/page")
    public R page(int page, int pageSize, String name){
        log.info("page = {},pageSize = {},name = {}" ,page,pageSize,name);
        //构造分页构造器
        Page pageInfo = new Page(page,pageSize);

        //构造条件构造器
        LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper();
        //添加过滤条件,如果name不等于空,才添加查询条件
        queryWrapper.like(StringUtils.isNotEmpty(name),Employee::getName,name);
        //添加排序条件,通过更新时间排序
        queryWrapper.orderByDesc(Employee::getUpdateTime);

        //执行查询
        employeeService.page(pageInfo,queryWrapper);
        return R.success(pageInfo);
    }

    /**
     * 根据id修改员工信息
     * @param employee
     * @return
     */
    @PutMapping
    public R update(HttpServletRequest request,@RequestBody Employee employee){
        log.info(employee.toString());

        long id = Thread.currentThread().getId();
        log.info("线程id为:{}",id);

        //Long empId = (Long)request.getSession().getAttribute("employee");

        /*employee.setUpdateTime(LocalDateTime.now());
        employee.setUpdateUser(empId);*/

        employeeService.updateById(employee);

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

    /**
     * 根据id查询员工信息
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    public R getById(@PathVariable Long id){
        /*
         * 编辑用户信息逻辑思路:
         * 1.先通过id获取需要编辑的用户的id,并跳转到add.html
         * 2.获取id后发送请求到后端,传到后端并进行id查询操作
         * 3.后端查询完毕进行数据模型ruleForm的赋值,从而达到数据的回显的效果
         * 4.修改后数据再次传给后端
         * 5.后端进行数据修改操作,调用通用的update方法(上面那个),并显示修改结果
         * */
        log.info("根据id查询员工信息...");
        Employee employee = employeeService.getById(id);
        if(employee != null){
            return R.success(employee);
        }
        return R.error("没有查询到对应员工信息");
    }
}

瑞吉外卖项目功能全实现及完全代码解析_第4张图片

2.103分类管理

CategoryMapper
package com.hjg.reggie.mapper;

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

/*
* 此操作Mybatis-plus提供
* */
@Mapper
public interface CategoryMapper extends BaseMapper {
}
CategoryService
package com.hjg.reggie.service;

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

/*
 * 此操作Mybatis-plus提供,并且可以自己扩展操作
 * */
public interface CategoryService extends IService {

    //根据ID删除分类
    public void remove(Long id);
}
CategoryServiceImpl
package com.hjg.reggie.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hjg.reggie.common.CustomException;
import com.hjg.reggie.entity.Category;
import com.hjg.reggie.entity.Dish;
import com.hjg.reggie.entity.Setmeal;
import com.hjg.reggie.mapper.CategoryMapper;
import com.hjg.reggie.service.CategoryService;
import com.hjg.reggie.service.DishService;
import com.hjg.reggie.service.SetmealService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/*
 * 此操作Mybatis-plus提供
 * */
@Service
public class CategoryServiceImpl extends ServiceImpl implements CategoryService {
    @Autowired
    private DishService dishService;
    @Autowired
    private SetmealService setmealService;

    /**
     * 根据id删除分类,删除之前需要进行判断
     * @param id
     */
    @Override
    public void remove(Long id) {
        //添加查询条件,根据分类id进行查询菜品数据
        LambdaQueryWrapper dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
        dishLambdaQueryWrapper.eq(Dish::getCategoryId,id);
        int count1 = dishService.count(dishLambdaQueryWrapper);
        //如果已经关联,抛出一个业务异常
        if(count1 > 0){
            throw new CustomException("当前分类下关联了菜品,不能删除");//已经关联菜品,抛出一个业务异常
        }

        //查询当前分类是否关联了套餐,如果已经关联,抛出一个业务异常
        LambdaQueryWrapper setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
        setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId,id);
        int count2 = setmealService.count(setmealLambdaQueryWrapper);
        if(count2 > 0){
            throw new CustomException("当前分类下关联了套餐,不能删除");//已经关联套餐,抛出一个业务异常
        }

        //正常删除分类
        super.removeById(id);
    }
}
CategoryController
package com.hjg.reggie.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hjg.reggie.common.R;
import com.hjg.reggie.entity.Category;
import com.hjg.reggie.service.CategoryService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/*
* 分类管理
* */

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

    @Autowired
    private CategoryService categoryService;

    /**
     * 新增分类
     * @param category
     * @return
     */
    @PostMapping
    public R save(@RequestBody Category category){
        log.info("category:{}",category);
        categoryService.save(category);
        return R.success("新增分类成功");
    }

    /**
     * 分页查询
     * @param page
     * @param pageSize
     * @return
     */
    @GetMapping("/page")
    public R page(int page, int pageSize){
        //分页构造器
        Page pageInfo = new Page<>(page,pageSize);
        //条件构造器
        LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
        //添加排序条件,根据sort进行排序
        queryWrapper.orderByAsc(Category::getSort);

        //分页查询
        categoryService.page(pageInfo,queryWrapper);
        return R.success(pageInfo);
    }

    /**
     * 根据id删除分类
     * @param ids
     * @return
     */
    @DeleteMapping
    public R delete(Long ids){
        log.info("删除分类,id为:{}",ids);

        //categoryService.removeById(ids);
        //此为自己定义修改的删除方法
        categoryService.remove(ids);

        return R.success("分类信息删除成功");
    }

    /**
     * 根据id修改分类信息
     * @param category
     * @return
     */
    @PutMapping
    public R update(@RequestBody Category category){
        log.info("修改分类信息:{}",category);
        categoryService.updateById(category);
        return R.success("修改分类信息成功");
    }

    /**
     * 根据条件查询分类数据
     * @param category
     * @return
     */
    @GetMapping("/list")
    public R> list(Category category){
        //条件构造器
        LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
        //添加条件
        queryWrapper.eq(category.getType() != null,Category::getType,category.getType());
        //添加排序条件
        queryWrapper.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime);

        List list = categoryService.list(queryWrapper);
        return R.success(list);
    }
}

瑞吉外卖项目功能全实现及完全代码解析_第5张图片

2.104菜品管理

DishMapper
package com.hjg.reggie.mapper;

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

@Mapper
public interface DishMapper extends BaseMapper {
}
DishService
package com.hjg.reggie.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.hjg.reggie.dto.DishDto;
import com.hjg.reggie.entity.Dish;

import java.util.List;

public interface DishService extends IService {

    //新增菜品,同时插入菜品对应的口味数据,需要操作两张表:dish、dish_flavor
    public void saveWithFlavor(DishDto dishDto);

    //根据id查询菜品信息和对应的口味信息
    public DishDto getByIdWithFlavor(Long id);

    //更新菜品信息,同时更新对应的口味信息
    public void updateWithFlavor(DishDto dishDto);

    //根据传过来的id批量或者是单个的删除菜品,并判断是否是启售的
    public void deleteByIds(List ids);

    //菜品批量删除和单个删除,删除时用到deleteByIds方法删除菜品
    public boolean deleteInSetmeal(List ids);
}
DishServiceImpl
package com.hjg.reggie.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hjg.reggie.common.CustomException;
import com.hjg.reggie.dto.DishDto;
import com.hjg.reggie.entity.Dish;
import com.hjg.reggie.entity.DishFlavor;
import com.hjg.reggie.entity.Setmeal;
import com.hjg.reggie.entity.SetmealDish;
import com.hjg.reggie.mapper.DishMapper;
import com.hjg.reggie.service.*;
import lombok.extern.slf4j.Slf4j;
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.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

@Service
@Slf4j
public class DishServiceImpl extends ServiceImpl implements DishService {

    @Autowired
    private DishFlavorService dishFlavorService;

    @Autowired
    private SetmealService setmealService;

    @Autowired
    private SetmealDishService setmealDishService;

    /**
     * 新增菜品,同时保存对应的口味数据
     * @param dishDto
     */
    /* 注意:
     * 由于在 saveWithFlavor 方法中,进行了两次数据库的保存操作,
     * 操作了两张表,那么为了保证数据的一致性,我们需要在方法上加上注解 @Transactional来控制事务。
     *
     * Service层方法上加的注解@Transactional要想生效,
     * 需要在引导类ReggieApplication上加上注解 @EnableTransactionManagement,开启对事务的支持。
     */
    @Transactional
    public void saveWithFlavor(DishDto dishDto) {
        //保存菜品的基本信息到菜品表dish
        this.save(dishDto);

        Long dishId = dishDto.getId();//菜品id
        //菜品口味
        List flavors = dishDto.getFlavors();
        /*
        * 将每个DishFlavor元素设置dishId,并重新赋予给自己
        * 因为新增时,flavor只有name和value属性,没有相应的菜品id,自己设置
        * */
        flavors = flavors.stream().map((item) -> {
            item.setDishId(dishId);
            return item;
        }).collect(Collectors.toList());

        //保存菜品口味数据到菜品口味表dish_flavor
        dishFlavorService.saveBatch(flavors);
    }

    /**
     * 根据id查询菜品信息和对应的口味信息
     * @param id
     * @return
     */
    public DishDto getByIdWithFlavor(Long id) {
        //查询菜品基本信息,从dish表查询
        Dish dish = this.getById(id);

        DishDto dishDto = new DishDto();
        BeanUtils.copyProperties(dish,dishDto);

        //查询当前菜品对应的口味信息,从dish_flavor表查询
        //在该页面本来存在的口味具有dish_id,但如果新增的口味就没有,因此仍全部重新赋予dish_id
        LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(DishFlavor::getDishId,dish.getId());
        List flavors = dishFlavorService.list(queryWrapper);
        dishDto.setFlavors(flavors);

        return dishDto;
    }

    @Transactional
    public void updateWithFlavor(DishDto dishDto) {
        //更新dish表基本信息
        this.updateById(dishDto);

        //清理当前菜品对应口味数据---dish_flavor表的delete操作
        LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper();
        queryWrapper.eq(DishFlavor::getDishId,dishDto.getId());

        dishFlavorService.remove(queryWrapper);

        //添加当前提交过来的口味数据---dish_flavor表的insert操作
        List flavors = dishDto.getFlavors();

        flavors = flavors.stream().map((item) -> {
            item.setDishId(dishDto.getId());
            return item;
        }).collect(Collectors.toList());

        dishFlavorService.saveBatch(flavors);
    }

    @Transactional
    public void deleteByIds(List ids) {
        //构造条件查询器
        LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
        //先查询该菜品是否在售卖,如果是则抛出业务异常
        queryWrapper.in(ids!=null,Dish::getId,ids);
        List list = this.list(queryWrapper);
        for (Dish dish : list) {
            Integer status = dish.getStatus();
            //如果不是在售卖,则可以删除
            if (status == 0){
                this.removeById(dish.getId());
            }else {
                //此时应该回滚,因为可能前面的删除了,但是后面的是正在售卖
                throw new CustomException("删除菜品中有正在售卖菜品,无法全部删除");
            }
        }
    }

    @Transactional
    public boolean deleteInSetmeal(List ids) {
        boolean flag=true;

        //1.根据菜品id在stemeal_dish表中查出哪些套餐包含该菜品
        LambdaQueryWrapper setmealDishLambdaQueryWrapper = new LambdaQueryWrapper<>();
        setmealDishLambdaQueryWrapper.in(SetmealDish::getDishId,ids);
        List SetmealDishList = setmealDishService.list(setmealDishLambdaQueryWrapper);
        //2.如果菜品没有关联套餐,直接删除就行  其实下面这个逻辑可以抽离出来,这里我就不抽离了
        if (SetmealDishList.size() == 0){
            //这个deleteByIds中已经做了菜品启售不能删除的判断力
            this.deleteByIds(ids);
            LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.in(DishFlavor::getDishId,ids);
            dishFlavorService.remove(queryWrapper);
            return flag;
        }

        //3.如果菜品有关联套餐,并且该套餐正在售卖,那么不能删除
        //3.1得到与删除菜品关联的套餐id
        ArrayList Setmeal_idList = new ArrayList<>();
        for (SetmealDish setmealDish : SetmealDishList) {
            Long setmealId = setmealDish.getSetmealId();
            Setmeal_idList.add(setmealId);
        }
        //3.2查询出与删除菜品相关联的套餐
        LambdaQueryWrapper setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
        setmealLambdaQueryWrapper.in(Setmeal::getId,Setmeal_idList);
        List setmealList = setmealService.list(setmealLambdaQueryWrapper);
        //3.3对拿到的所有套餐进行遍历,然后拿到套餐的售卖状态,如果有套餐正在售卖那么删除失败
        for (Setmeal setmeal : setmealList) {
            Integer status = setmeal.getStatus();
            if (status == 1){
                flag=false;
            }
        }

        //3.4要删除的菜品关联的套餐没有在售,可以删除
        //3.5这下面的代码并不一定会执行,因为如果前面的for循环中出现status == 1,那么下面的代码就不会再执行
        this.deleteByIds(ids);
        LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.in(DishFlavor::getDishId,ids);
        dishFlavorService.remove(queryWrapper);

        return flag;
    }

}
DishFlavorMapper
package com.hjg.reggie.mapper;

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

@Mapper
public interface DishFlavorMapper extends BaseMapper {
}
DishFlavorService
package com.hjg.reggie.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.hjg.reggie.entity.DishFlavor;

public interface DishFlavorService extends IService {
}
DishFlavorServiceImpl
package com.hjg.reggie.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hjg.reggie.entity.DishFlavor;
import com.hjg.reggie.mapper.DishFlavorMapper;
import com.hjg.reggie.service.DishFlavorService;
import org.springframework.stereotype.Service;

@Service
public class DishFlavorServiceImpl extends ServiceImpl implements DishFlavorService {
}
DishController
package com.hjg.reggie.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hjg.reggie.common.R;
import com.hjg.reggie.dto.DishDto;
import com.hjg.reggie.entity.*;
import com.hjg.reggie.service.CategoryService;
import com.hjg.reggie.service.DishFlavorService;
import com.hjg.reggie.service.DishService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.stream.Collectors;

/**
 * 菜品管理
 */
@RestController
@RequestMapping("/dish")
@Slf4j
public class DishController {
    @Autowired
    private DishService dishService;

    @Autowired
    private CategoryService categoryService;

    @Autowired
    private DishFlavorService dishFlavorService;

    /**
     * 新增菜品
     * @param dishDto
     * @return
     */
    @PostMapping
    public R save(@RequestBody DishDto dishDto){
        log.info(dishDto.toString());

        dishService.saveWithFlavor(dishDto);

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

    /**
     * 菜品信息分页查询
     * @param page
     * @param pageSize
     * @param name
     * @return
     */
    @GetMapping("/page")
    public R page(int page, int pageSize, String name){
        //构造分页构造器对象
        Page pageInfo = new Page<>(page,pageSize);
        Page dishDtoPage = new Page<>();

        //条件构造器
        LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
        //添加过滤条件
        queryWrapper.like(name != null,Dish::getName,name);
        //添加排序条件
        queryWrapper.orderByDesc(Dish::getUpdateTime);

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

        //对象拷贝
        //页面数据只传了分类id,没显示分类名称,要显示还需以下操作
        //1.1这个操作时数据拷贝,并且不拷贝records对象,records单独操作,有dish对象变为dishDto对象
        //1.2排除records对象,自己写records。records是page对象的属性,存放查询到的数据
        BeanUtils.copyProperties(pageInfo,dishDtoPage,"records");
        //获取List保存的数据集合
        List records = pageInfo.getRecords();
        List list = records.stream().map((item) -> {

            DishDto dishDto = new DishDto();
            BeanUtils.copyProperties(item,dishDto);
            Long categoryId = item.getCategoryId();//分类id
            //根据id查询分类对象
            Category category = categoryService.getById(categoryId);

            if(category != null){
                String categoryName = category.getName();
                dishDto.setCategoryName(categoryName);
            }
            return dishDto;
        }).collect(Collectors.toList());
        dishDtoPage.setRecords(list);

        return R.success(dishDtoPage);
    }

    /**
     * 根据id查询菜品信息和对应的口味信息
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    public R get(@PathVariable Long id){
        DishDto dishDto = dishService.getByIdWithFlavor(id);
        return R.success(dishDto);
    }

    /**
     * 修改菜品
     * @param dishDto
     * @return
     */
    @PutMapping
    public R update(@RequestBody DishDto dishDto){
        log.info(dishDto.toString());
        dishService.updateWithFlavor(dishDto);
        return R.success("修改菜品成功");
    }

    /*
    * 对菜品进行停售或者是启售
    * 包括单个菜品和多个菜品一起解决
    */
    @PostMapping("/status/{status}")
    public R status(@PathVariable("status") Integer status,@RequestParam List ids){
        LambdaQueryWrapper queryWrapper=new LambdaQueryWrapper<>();
        queryWrapper.in(ids!=null,Dish::getId,ids);
        List list = dishService.list(queryWrapper);

        for (Dish dish:list) {
            if(dish!=null){
                dish.setStatus(status);
                dishService.updateById(dish);
            }
        }
        return R.success("售卖状态修改成功");
    }

    /**
     * 菜品批量删除和单个删除
     * 1.判断要删除的菜品在不在售卖的套餐中,如果在那不能删除
     * 2.要先判断要删除的菜品是否在售卖,如果在售卖也不能删除
     * @return
     */
    @DeleteMapping
    public R delete(@RequestParam("ids") List ids){
        boolean deleteInSetmeal = dishService.deleteInSetmeal(ids);
        if(deleteInSetmeal){
            return R.success("菜品删除成功");
        }
        else{
            return R.error("删除的菜品中有关联在售套餐,删除失败!");
        }
    }

    /*
    * 1.根据套餐id,查询相应套餐所对应的菜品
    * 2.根据name,模糊查询所有的正在启售的菜品
    */
    /*@GetMapping("/list")
    public R> list(Dish dish){
        //构造查询条件
        LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(dish.getCategoryId() != null ,Dish::getCategoryId,dish.getCategoryId());
        queryWrapper.like(dish.getName()!=null,Dish::getName,dish.getName());
        //添加条件,查询状态为1(启售状态)的菜品
        queryWrapper.eq(Dish::getStatus,1);
        //添加排序条件
        queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);

        List list = dishService.list(queryWrapper);

        return R.success(list);
    }*/

    //改造上面这个分类中查询菜品的操作。使它在移动端中也能使用,并显示出更多的比如口味等信息
    @GetMapping("/list")
    public R> list(Dish dish){
        //构造查询条件
        LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(dish.getCategoryId() != null ,Dish::getCategoryId,dish.getCategoryId());
        //添加条件,查询状态为1(起售状态)的菜品
        queryWrapper.eq(Dish::getStatus,1);
        //添加排序条件
        queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);

        List list = dishService.list(queryWrapper);

        List dishDtoList = list.stream().map((item) -> {
            DishDto dishDto = new DishDto();
            BeanUtils.copyProperties(item,dishDto);

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

            //当前菜品的id
            Long dishId = item.getId();
            LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>();
            lambdaQueryWrapper.eq(DishFlavor::getDishId,dishId);
            //SQL:select * from dish_flavor where dish_id = ?
            List dishFlavorList = dishFlavorService.list(lambdaQueryWrapper);
            dishDto.setFlavors(dishFlavorList);

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

        return R.success(dishDtoList);
    }

}

瑞吉外卖项目功能全实现及完全代码解析_第6张图片

2.105套餐管理

SetmealMapper
package com.hjg.reggie.mapper;

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

@Mapper
public interface SetmealMapper extends BaseMapper {
}
SetmealService
package com.hjg.reggie.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.hjg.reggie.dto.SetmealDto;
import com.hjg.reggie.entity.Setmeal;

import java.util.List;

public interface SetmealService extends IService {
    /**
     * 新增套餐,同时需要保存套餐和菜品的关联关系
     * @param setmealDto
     */
    public void saveWithDish(SetmealDto setmealDto);

    /**
     * 根据套餐id查询套餐中的菜品
     */
    public SetmealDto getByIdWithDish(long id);

    /**
     * 修改菜品
     */
    public void updateWithDish(SetmealDto setmealDto);

    /**
     * 套餐批量删除和单个删除
     */
    public void deleteWithDish(List ids);
}
SetmealServiceImpl
package com.hjg.reggie.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hjg.reggie.common.CustomException;
import com.hjg.reggie.dto.SetmealDto;
import com.hjg.reggie.entity.Setmeal;
import com.hjg.reggie.entity.SetmealDish;
import com.hjg.reggie.mapper.SetmealMapper;
import com.hjg.reggie.service.SetmealDishService;
import com.hjg.reggie.service.SetmealService;
import lombok.extern.slf4j.Slf4j;
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;
import java.util.stream.Collectors;

@Service
@Slf4j
public class SetmealServiceImpl extends ServiceImpl implements SetmealService {

    @Autowired
    private SetmealDishService setmealDishService;

    /**
     * 新增套餐,同时需要保存套餐和菜品的关联关系
     * @param setmealDto
     */
    @Transactional
    public void saveWithDish(SetmealDto setmealDto) {
        //保存套餐的基本信息,操作setmeal,执行insert操作
        this.save(setmealDto);

        List setmealDishes = setmealDto.getSetmealDishes();
        //stream流在高数据量下的效率比foreach高,所以使用它
        setmealDishes.stream().map((item) -> {
            //将套餐产品表setmeal_dish数据插入,其中表的套餐id设为SetmealDto数据传入后台后,通过雪花算法随机产生的
            item.setSetmealId(setmealDto.getId());
            return item;
        }).collect(Collectors.toList());

        //保存套餐和菜品的关联信息,操作setmeal_dish,执行insert操作
        setmealDishService.saveBatch(setmealDishes);
    }

    /**
     * 根据套餐id查询套餐中的菜品
     */
    public SetmealDto getByIdWithDish(long id) {
        Setmeal setmeal = this.getById(id);
        SetmealDto setmealDto=new SetmealDto();
        BeanUtils.copyProperties(setmeal,setmealDto);

        LambdaQueryWrapper queryWrapper=new LambdaQueryWrapper<>();
        queryWrapper.eq(SetmealDish::getSetmealId,setmeal.getId());
        List setmealDishList = setmealDishService.list(queryWrapper);
        setmealDto.setSetmealDishes(setmealDishList);

        return setmealDto;
    }

    /**
     * 修改菜品
     * @param setmealDto
     */
    @Transactional
    public void updateWithDish(SetmealDto setmealDto) {
        //更新套餐表的套餐
        this.updateById(setmealDto);

        //查询并删除旧的套餐中的菜品
        LambdaQueryWrapper queryWrapper=new LambdaQueryWrapper<>();
        queryWrapper.eq(SetmealDish::getSetmealId,setmealDto.getId());
        setmealDishService.remove(queryWrapper);

        //更新传过来的新的菜品,并将其赋予套餐的id
        List setmealDishes = setmealDto.getSetmealDishes();
        setmealDishes=setmealDishes.stream().map((item)->{
            item.setSetmealId(setmealDto.getId());
            return item;
        }).collect(Collectors.toList());

        //将新传过来的套餐中的菜品其保存到setmeal_dish表中
        setmealDishService.saveBatch(setmealDishes);
    }

    /**
     * 套餐批量删除和单个删除
     */
    @Transactional
    public void deleteWithDish(List ids) {
        //查询套餐状态,确定是否可用删除
        LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper();
        queryWrapper.in(Setmeal::getId,ids);
        queryWrapper.eq(Setmeal::getStatus,1);

        //查询选中的并且在启售中的套餐数量
        int count = this.count(queryWrapper);
        if(count > 0){
            //count>0,说明有启售的套餐,全部不允许删除
            //如果不能删除,抛出一个业务异常
            throw new CustomException("套餐正在售卖中,不能删除");
        }

        //如果可以删除,先删除套餐表中的数据---setmeal
        this.removeByIds(ids);

        LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.in(SetmealDish::getSetmealId,ids);
        //删除关系表中的数据----setmeal_dish
        setmealDishService.remove(lambdaQueryWrapper);
    }

}
SetmealDishMapper
package com.hjg.reggie.mapper;

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

@Mapper
public interface SetmealDishMapper extends BaseMapper {
}
SetmealDishService
package com.hjg.reggie.service;

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

public interface SetmealDishService extends IService {
}
SetmealDishServiceImpl
package com.hjg.reggie.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hjg.reggie.entity.SetmealDish;
import com.hjg.reggie.mapper.SetmealDishMapper;
import com.hjg.reggie.service.SetmealDishService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class SetmealDishServiceImpl extends ServiceImpl implements SetmealDishService {
}
SetmealController
package com.hjg.reggie.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hjg.reggie.common.R;
import com.hjg.reggie.dto.DishDto;
import com.hjg.reggie.dto.SetmealDto;
import com.hjg.reggie.entity.Category;
import com.hjg.reggie.entity.Dish;
import com.hjg.reggie.entity.Setmeal;
import com.hjg.reggie.entity.SetmealDish;
import com.hjg.reggie.service.CategoryService;
import com.hjg.reggie.service.DishService;
import com.hjg.reggie.service.SetmealDishService;
import com.hjg.reggie.service.SetmealService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.stream.Collectors;

/**
 * 套餐管理
 */
@RestController
@RequestMapping("/setmeal")
@Slf4j
public class SetmealController {
    @Autowired
    private SetmealService setmealService;
    @Autowired
    private SetmealDishService setmealDishService;
    @Autowired
    private CategoryService categoryService;
    @Autowired
    private DishService dishService;

    /**
     * 新增套餐,同时需要保存套餐和菜品的关联关系
     * @param setmealDto
     */
    @PostMapping
    public R save(@RequestBody SetmealDto setmealDto){
        setmealService.saveWithDish(setmealDto);
        return R.success("新增套餐成功");
    }

    /**
     * 套餐信息分页查询
     * @param page
     * @param pageSize
     * @param name
     * @return
     */
    @GetMapping("/page")
    public R page(int page, int pageSize, String name){
        Page setmealPage=new Page<>(page,pageSize);
        Page setmealDtoPage=new Page<>();

        //条件构造器,添加过滤条件,添加排序条件
        LambdaQueryWrapper queryWrapper=new LambdaQueryWrapper<>();
        queryWrapper.like(name!=null,Setmeal::getName,name);
        queryWrapper.orderByDesc(Setmeal::getUpdateTime);

        //执行分页查询
        setmealService.page(setmealPage,queryWrapper);

        //对象拷贝
        //页面数据只传了分类id,没显示分类名称,要显示还需以下操作
        //排除records对象,自己写records。records是page对象的属性,存放查询到的数据
        BeanUtils.copyProperties(setmealPage,setmealDtoPage,"records");

        List setmealList=setmealPage.getRecords();
        List setmealDtoList=setmealList.stream().map((item)->{
            SetmealDto setmealDto=new SetmealDto();
            BeanUtils.copyProperties(item,setmealDto);

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

            return setmealDto;
        }).collect(Collectors.toList());
        setmealDtoPage.setRecords(setmealDtoList);

        return R.success(setmealDtoPage);
    }

    /**
     * 根据套餐id查询套餐中的菜品
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    public R get(@PathVariable Long id){
        SetmealDto setmealDto = setmealService.getByIdWithDish(id);
        return R.success(setmealDto);
    }

    /**
     * 修改套餐
     */
    @PutMapping
    public R update(@RequestBody SetmealDto setmealDto){
        setmealService.updateWithDish(setmealDto);
        return R.success("修改套餐成功");
    }

    /**
     * 套餐批量删除和单个删除
     * 1.要先判断要删除的菜品是否在售卖,如果在售卖不能删除套餐
     * 2.如果可以删除套餐,则将setmeal_dish表中保存的信息一并删除
     * @return
     */
    @DeleteMapping
    public R delete(@RequestParam("ids") List ids){
        setmealService.deleteWithDish(ids);
        return R.success("删除套餐成功");
    }

    /**
     * 套餐批量启售停售和单个启售停售
     * 请求 URL: http://localhost:8080/setmeal/status/1?ids=1415580119015145474,1579663716218183682
     */
    @PostMapping("/status/{status}")
    public R status(@PathVariable("status") Integer status,@RequestParam List ids){
        LambdaQueryWrapper queryWrapper=new LambdaQueryWrapper<>();
        queryWrapper.in(ids!=null,Setmeal::getId,ids);
        List setmealList = setmealService.list(queryWrapper);

        for (Setmeal item:setmealList) {
            if(item!=null){
                item.setStatus(status);
                setmealService.updateById(item);
            }
        }
        return R.success("售卖状态修改成功");
    }

    /**
     * 根据条件查询套餐数据
     * @param setmeal
     * @return
     * 请求 URL: http://localhost:8080/dish/list?categoryId=1397844263642378242&status=1
     */
    @GetMapping("/list")
    public R> list(Setmeal setmeal){
        //移动端的前端传过来套餐分类id,将其包装成Setmeal对象
        //移动端的前端传过来的数据携带了status=1的数据,封装查询后保证展示的都是启售的套餐
        LambdaQueryWrapper queryWrapper=new LambdaQueryWrapper<>();
        queryWrapper.eq(setmeal.getCategoryId()!=null,Setmeal::getCategoryId,setmeal.getCategoryId());
        queryWrapper.eq(setmeal.getStatus()!=null,Setmeal::getStatus,setmeal.getStatus());
        queryWrapper.orderByDesc(Setmeal::getUpdateTime);

        List setmealList = setmealService.list(queryWrapper);
        return R.success(setmealList);
    }


    /**
     * 移动端点击套餐图片查看套餐具体内容
     * 这里返回的是dto 对象,因为前端需要copies这个属性
     * 前端主要要展示的信息是:套餐中菜品的基本信息,图片,菜品描述,以及菜品的份数
     * @param SetmealId
     * @return
     */
    @GetMapping("/dish/{id}")
    public R> dish(@PathVariable("id") Long SetmealId){
        LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(SetmealDish::getSetmealId,SetmealId);
        //获取套餐里面的所有菜品,这个就是SetmealDish表里面的数据
        List SetmealDish = setmealDishService.list(queryWrapper);

        List dishDtos = SetmealDish.stream().map((setmealDish) -> {
            DishDto dishDto = new DishDto();
            //通过setmealDish表中的菜品id去dish表中查询菜品,从而获取菜品的各类数据
            Long dishId = setmealDish.getDishId();
            Dish dish = dishService.getById(dishId);
            BeanUtils.copyProperties(dish, dishDto);

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

        return R.success(dishDtos);
    }
}

瑞吉外卖项目功能全实现及完全代码解析_第7张图片

2.106移动端登录

瑞吉外卖项目功能全实现及完全代码解析_第8张图片
记得开启这个邮箱授权服务,这样才能用它发送验证码。

前端登录页面改进

A.由手机验证码登录改为邮箱验证码登录

B.加入验证码获取后60秒倒计时功能 




    
    
    
    
    菩提阁
    
    
    
    
    
    
    
    
    
    
    


登录
{{second}}s 获取验证码
邮箱格式不正确,请重新输入
登录
UserMapper 
package com.hjg.reggie.mapper;

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

@Mapper
public interface UserMapper extends BaseMapper {
}
UserService
package com.hjg.reggie.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.hjg.reggie.entity.User;

public interface UserService extends IService {
    //发送邮件
    void sendMsg(String to,String subject,String text);
}
UserServiceImpl
package com.hjg.reggie.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hjg.reggie.entity.User;
import com.hjg.reggie.mapper.UserMapper;
import com.hjg.reggie.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl extends ServiceImpl implements UserService {
    //把yml配置的邮箱号赋值到from
    @Value("${spring.mail.username}")
    private String from;
    //发送邮件需要的对象
    @Autowired
    private JavaMailSender javaMailSender;
    //邮件发送人
    @Override
    public void sendMsg(String to, String subject, String text) {
        //发送简单邮件,简单邮件不包括附件等别的
        SimpleMailMessage message = new SimpleMailMessage();
        message.setFrom(from);
        message.setTo(to);
        message.setSubject(subject);
        message.setText(text);
        //发送邮件
        javaMailSender.send(message);
    }
}
UserController
package com.hjg.reggie.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.hjg.reggie.common.R;
import com.hjg.reggie.entity.User;
import com.hjg.reggie.service.UserService;
import com.hjg.reggie.utils.ValidateCodeUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.Map;

@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
    @Autowired
    private UserService userService;

    //获取验证码
    @PostMapping("/sendMsg")
    public R sendMsg(HttpSession session, @RequestBody User user){
        //获取邮箱号
        //相当于发送短信定义的String to
        String email = user.getPhone();
        String subject = "瑞吉外卖";
        //StringUtils.isNotEmpty字符串非空判断
        if (StringUtils.isNotEmpty(email)) {
            //发送一个四位数的验证码,把验证码变成String类型
            String code = ValidateCodeUtils.generateValidateCode(4).toString();
            String text = "【瑞吉外卖】您好,您的登录验证码为:" + code + ",请尽快登录,如非本人操作,请忽略此邮件。";
            log.info("验证码为:" + code);
            //发送短信
            userService.sendMsg(email,subject,text);
            //将验证码保存到session当中
            //将邮箱作为key,将code最为value保存到session中,,因此邮箱和验证码可以一一对应
            session.setAttribute(email,code);
            return R.success("验证码发送成功");
        }
        return R.error("验证码发送异常,请重新发送");
    }

    //登录
    @PostMapping("/login")
    //Map存JSON数据
    public R login(HttpSession session,@RequestBody Map map){
        //获取邮箱,用户输入的,这个phone就是输入的邮箱
        String phone = map.get("phone").toString();
        //获取验证码,用户输入的,这个code就是生成的验证码
        String code = map.get("code").toString();
        /**
         * 获取session中保存的验证码
         * 登录的邮箱作为session的key值,将code最为value
         * 因此邮箱和验证码可以一一对应,保证邮箱验证码数据一致完整性
         * */
        Object sessionCode = session.getAttribute(phone);
        //将session的验证码和用户输入的验证码进行比对
        if (sessionCode != null && sessionCode.equals(code)) {
            //要是User数据库没有这个邮箱则自动注册,先看看输入的邮箱是否存在数据库
            LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(User::getPhone,phone);
            //获得唯一的用户,因为邮箱是唯一的
            User user = userService.getOne(queryWrapper);
            //要是User数据库没有这个邮箱则自动注册
            if (user == null) {
                user = new User();
                user.setPhone(phone);
                user.setStatus(1);
                //取邮箱的前五位为用户名
                user.setName("用户"+phone.substring(0,5));
                userService.save(user);
            }
            //不保存这个用户名就登不上去,因为过滤器需要得到这个user才能放行,程序才知道你登录了
            session.setAttribute("user", user.getId());
            return R.success(user);
        }
        return R.error("登录失败");
    }

    /**
     * 退出功能
     * ①在controller中创建对应的处理方法来接受前端的请求,请求方式为post;
     * ②清理session中的用户id
     * ③返回结果(前端页面会进行跳转到登录页面)
     * @return
     */
    @PostMapping("/logout")
    public R logout(HttpServletRequest request){
        //清理session中的用户id
        request.getSession().removeAttribute("user");
        return R.success("退出成功");
    }
}

 瑞吉外卖项目功能全实现及完全代码解析_第9张图片

2.107地址管理

AddressBookMapper
package com.hjg.reggie.mapper;

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

@Mapper
public interface AddressBookMapper extends BaseMapper {
}
AddressBookService
package com.hjg.reggie.service;

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

public interface AddressBookService extends IService {
}
AddressBookServiceImpl
package com.hjg.reggie.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hjg.reggie.entity.AddressBook;
import com.hjg.reggie.mapper.AddressBookMapper;
import com.hjg.reggie.service.AddressBookService;
import org.springframework.stereotype.Service;

@Service
public class AddressBookServiceImpl extends ServiceImpl implements AddressBookService {
}
AddressBookController
package com.hjg.reggie.controller;

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

import java.util.List;

/**
 * 地址簿管理
 */
@Slf4j
@RestController
@RequestMapping("/addressBook")
public class AddressBookController {
    @Autowired
    private AddressBookService addressBookService;

    /**
     * 新增
     */
    @PostMapping
    public R save(@RequestBody AddressBook addressBook) {
        //BaseContext.getCurrentId()从线程中获取保存的登录用户id
        //不能从session中获取,可能一台电脑登录多个用户session冲突
        addressBook.setUserId(BaseContext.getCurrentId());
        log.info("addressBook:{}", addressBook);

        //如果用户新增第一条地址,直接将其设为默认地址
        LambdaQueryWrapper queryWrapper=new LambdaQueryWrapper<>();
        queryWrapper.eq(AddressBook::getUserId,addressBook.getUserId());
        int count = addressBookService.count(queryWrapper);
        if(count==0){
            //将第一条地址设为默认
            addressBook.setIsDefault(1);
            addressBookService.save(addressBook);
        }
        else{
            addressBookService.save(addressBook);
        }

        return R.success(addressBook);
    }

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

        //2.将前端传过来的地址唯一修改为默认地址(1)
        addressBook.setIsDefault(1);
        //SQL:update address_book set is_default = 1 where id = ?
        //3.进行该默认地址的更新操作
        addressBookService.updateById(addressBook);
        return R.success(addressBook);
    }

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

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

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

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

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

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

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

    /**
     * 根据地址id删除用户地址
     * @param id
     * @return
     */
    @DeleteMapping
    public R delete(@RequestParam("ids") Long id){

        if (id == null){
            return R.error("请求异常");
        }
        LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
        //通过查询用户id和要删除的地址id,删除地址
        queryWrapper.eq(AddressBook::getId,id).eq(AddressBook::getUserId,BaseContext.getCurrentId());
        addressBookService.remove(queryWrapper);

        return R.success("删除地址成功");
    }

    /**
     * 修改收货地址
     * @param addressBook
     * @return
     */
    @PutMapping
    public R update(@RequestBody AddressBook addressBook){

        if (addressBook == null){
            return R.error("请求异常");
        }
        addressBookService.updateById(addressBook);

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

瑞吉外卖项目功能全实现及完全代码解析_第10张图片

2.108订单管理

OrderMapper
package com.hjg.reggie.mapper;

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

@Mapper
public interface OrderMapper extends BaseMapper {
}
OrderService
package com.hjg.reggie.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.hjg.reggie.entity.Orders;

public interface OrderService extends IService {

    /**
     * 用户下单
     * @param orders
     */
    public void submit(Orders orders);
}
OrderServiceImpl
package com.hjg.reggie.service.impl;

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

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

@Service
@Slf4j
public class OrderServiceImpl extends ServiceImpl implements OrderService {

    @Autowired
    private ShoppingCartService shoppingCartService;

    @Autowired
    private UserService userService;

    @Autowired
    private AddressBookService addressBookService;

    @Autowired
    private OrderDetailService orderDetailService;

    /**
     * 用户下单
     * @param orders
     * 请求 URL: http://localhost:8080/order/submit
     * 负载:{remark: "多加辣椒!!!", payMethod: 1, addressBookId: "1580164651377868802"}
     */
    @Transactional
    public void submit(Orders orders) {
        //获得当前用户id
        Long userId = BaseContext.getCurrentId();

        //查询当前用户的购物车数据
        LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(ShoppingCart::getUserId,userId);
        List shoppingCarts = shoppingCartService.list(wrapper);

        if(shoppingCarts == null || shoppingCarts.size() == 0){
            throw new CustomException("购物车为空,不能下单");
        }

        //查询用户数据
        User user = userService.getById(userId);

        //查询地址数据
        Long addressBookId = orders.getAddressBookId();
        AddressBook addressBook = addressBookService.getById(addressBookId);
        if(addressBook == null){
            throw new CustomException("用户地址信息有误,不能下单");
        }

        //IdWorker是mybatis-plus提供的一个ID生成工具,可以生成一个全局唯一的长整形ID。
        long orderId = IdWorker.getId();//订单号

        //AtomicInteger是一个Java concurrent包提供的一个原子类,通过这个类可以对Integer进行一些原子操作。
        //原子整型数,保证线程安全
        AtomicInteger amount = new AtomicInteger(0);

        //组装订单明细信息
        List orderDetails = shoppingCarts.stream().map((item) -> {
            OrderDetail orderDetail = new OrderDetail();
            orderDetail.setOrderId(orderId);
            orderDetail.setNumber(item.getNumber());
            orderDetail.setDishFlavor(item.getDishFlavor());
            orderDetail.setDishId(item.getDishId());
            orderDetail.setSetmealId(item.getSetmealId());
            orderDetail.setName(item.getName());
            orderDetail.setImage(item.getImage());
            orderDetail.setAmount(item.getAmount());
            //这个是累加金额操作,累加每个菜品或者套餐乘于相应的份数
            amount.addAndGet(item.getAmount().multiply(new BigDecimal(item.getNumber())).intValue());
            return orderDetail;
        }).collect(Collectors.toList());

        //组装订单数据
        orders.setId(orderId);
        orders.setOrderTime(LocalDateTime.now());
        orders.setCheckoutTime(LocalDateTime.now());
        orders.setStatus(2);
        orders.setAmount(new BigDecimal(amount.get()));//总金额,math的API类BigDecimal,用来对超过16位有效位的数进行精确的运算。
        orders.setUserId(userId);
        //注意:orders里面的number是订单号的意思,不是数量
        orders.setNumber(String.valueOf(orderId));
        orders.setUserName(user.getName());
        orders.setConsignee(addressBook.getConsignee());
        orders.setPhone(addressBook.getPhone());
        orders.setAddress((addressBook.getProvinceName() == null ? "" : addressBook.getProvinceName())
                + (addressBook.getCityName() == null ? "" : addressBook.getCityName())
                + (addressBook.getDistrictName() == null ? "" : addressBook.getDistrictName())
                + (addressBook.getDetail() == null ? "" : addressBook.getDetail()));
        //向订单表插入数据,一条数据
        this.save(orders);

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

        //清空购物车数据
        shoppingCartService.remove(wrapper);
    }
}
OrderDetailMapper
package com.hjg.reggie.mapper;

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

@Mapper
public interface OrderDetailMapper extends BaseMapper {
}
OrderDetailService
package com.hjg.reggie.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.hjg.reggie.entity.OrderDetail;

public interface OrderDetailService extends IService {
}
OrderDetailServiceImpl
package com.hjg.reggie.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hjg.reggie.entity.OrderDetail;
import com.hjg.reggie.mapper.OrderDetailMapper;
import com.hjg.reggie.service.OrderDetailService;
import org.springframework.stereotype.Service;

@Service
public class OrderDetailServiceImpl extends ServiceImpl implements OrderDetailService {
}
OrderController
package com.hjg.reggie.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hjg.reggie.common.BaseContext;
import com.hjg.reggie.common.R;
import com.hjg.reggie.dto.OrderDto;
import com.hjg.reggie.entity.OrderDetail;
import com.hjg.reggie.entity.Orders;
import com.hjg.reggie.entity.ShoppingCart;
import com.hjg.reggie.service.OrderDetailService;
import com.hjg.reggie.service.OrderService;
import com.hjg.reggie.service.ShoppingCartService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * 订单
 */
@Slf4j
@RestController
@RequestMapping("/order")
public class OrderController {
    @Autowired
    private OrderService orderService;

    @Autowired
    private OrderDetailService orderDetailService;

    @Autowired
    private ShoppingCartService shoppingCartService;

    /**
     * 用户下单
     * @param orders
     * @return
     * 请求 URL: http://localhost:8080/order/submit
     * 负载:{remark: "多加辣椒!!!", payMethod: 1, addressBookId: "1580164651377868802"}
     */
    @PostMapping("/submit")
    public R submit(@RequestBody Orders orders){
        log.info("订单数据:{}",orders);
        orderService.submit(orders);
        return R.success("下单成功");
    }

    /**
     * 后台查询订单明细
     * @param page
     * @param pageSize
     * @param number
     * @param beginTime
     * @param endTime
     * @return
     */
    @GetMapping("/page")
    public R page(int page, int pageSize, String number, String beginTime, String endTime){
        //分页构造器对象
        Page pageInfo = new Page<>(page,pageSize);
        //构造条件查询对象
        LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();

        //添加查询条件  动态sql  字符串使用StringUtils.isNotEmpty这个方法来判断
        //这里使用了范围查询的动态SQL,这里是重点!!!
        queryWrapper.like(number!=null,Orders::getNumber,number)
                .gt(StringUtils.isNotEmpty(beginTime),Orders::getOrderTime,beginTime)
                .lt(StringUtils.isNotEmpty(endTime),Orders::getOrderTime,endTime);

        orderService.page(pageInfo,queryWrapper);
        return R.success(pageInfo);
    }

    //抽离的一个方法,通过订单id查询订单明细,得到一个订单明细的集合
    //这里抽离出来是为了避免在stream中遍历的时候直接使用构造条件来查询导致eq叠加,从而导致后面查询的数据都是null
    public List getOrderDetailListByOrderId(Long orderId){
        LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(OrderDetail::getOrderId, orderId);
        List orderDetailList = orderDetailService.list(queryWrapper);
        return orderDetailList;
    }

    /**
     * 移动端展示自己的订单分页查询
     * @param page
     * @param pageSize
     * @return
     * 遇到的坑:原来分页对象中的records集合存储的对象是分页泛型中的对象,里面有分页泛型对象的数据
     * 开始的时候我以为前端只传过来了分页数据,其他所有的数据都要从本地线程存储的用户id开始查询,
     * 结果就出现了一个用户id查询到 n个订单对象,然后又使用 n个订单对象又去查询 m 个订单明细对象,
     * 结果就出现了评论区老哥出现的bug(嵌套显示数据....)
     * 正确方法:直接从分页对象中获取订单id就行,问题大大简化了......
     */
    @GetMapping("/userPage")
    public R page(int page, int pageSize){
        //分页构造器对象
        Page pageInfo = new Page<>(page,pageSize);
        Page pageDto = new Page<>(page,pageSize);
        //构造条件查询对象
        LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Orders::getUserId, BaseContext.getCurrentId());
        //这里是直接把当前用户分页的全部结果查询出来,要添加用户id作为查询条件,否则会出现用户可以查询到其他用户的订单情况
        //添加排序条件,根据更新时间降序排列
        queryWrapper.orderByDesc(Orders::getOrderTime);
        orderService.page(pageInfo,queryWrapper);

        //通过OrderId查询对应的OrderDetail
        LambdaQueryWrapper queryWrapper2 = new LambdaQueryWrapper<>();

        //对OrderDto进行需要的属性赋值
        List records = pageInfo.getRecords();
        List orderDtoList = records.stream().map((item) ->{
            OrderDto orderDto = new OrderDto();
            //此时的orderDto对象里面orderDetails属性还是空 下面准备为它赋值
            Long orderId = item.getId();//获取订单id
            List orderDetailList = this.getOrderDetailListByOrderId(orderId);
            BeanUtils.copyProperties(item,orderDto);
            //对orderDto进行OrderDetails属性的赋值
            orderDto.setOrderDetails(orderDetailList);
            return orderDto;
        }).collect(Collectors.toList());

        //使用dto的分页有点难度.....需要重点掌握
        BeanUtils.copyProperties(pageInfo,pageDto,"records");
        pageDto.setRecords(orderDtoList);
        return R.success(pageDto);
    }

    /**
     * 订单派送,订单完成操作
     * 注:后端操作
     * @param orders
     * @return
     */
    @PutMapping
    public R dispatch(@RequestBody Orders orders){
        LambdaQueryWrapper queryWrapper=new LambdaQueryWrapper<>();
        queryWrapper.eq(orders.getId()!=null,Orders::getId,orders.getId());
        Orders one = orderService.getOne(queryWrapper);

        one.setStatus(orders.getStatus());
        orderService.updateById(one);
        return R.success(one);
    }

    //移动端点击再来一单
    /**
     * 前端点击再来一单是直接跳转到购物车的,所以为了避免数据有问题,再跳转之前我们需要把购物车的数据给清除
     * ①通过orderId获取订单明细
     * ②把订单明细的数据的数据塞到购物车表中,不过在此之前要先把购物车表中的数据给清除(清除的是当前登录用户的购物车表中的数据),
     * 不然就会导致再来一单的数据有问题;
     * (这样可能会影响用户体验,但是对于外卖来说,用户体验的影响不是很大,电商项目就不能这么干了)
     */
    @PostMapping("/again")
    public R againSubmit(@RequestBody Map map){
        //获取再来一单的订单id
        String ids = map.get("id");
        long id = Long.parseLong(ids);

        LambdaQueryWrapper queryWrapper=new LambdaQueryWrapper<>();
        queryWrapper.eq(OrderDetail::getOrderId,id);
        //获取所有该订单中的菜品
        List orderDetailList = orderDetailService.list(queryWrapper);

        //通过用户id把原来的购物车给清空,这里的clean方法是视频中讲过的,建议抽取到service中,那么这里就可以直接调用了
        shoppingCartService.clean();

        //获取用户id
        Long userId = BaseContext.getCurrentId();
        //因为菜品详细表和购物车表内容很像,所有容易相互赋值
        List shoppingCartList=orderDetailList.stream().map((item)->{
            ShoppingCart shoppingCart=new ShoppingCart();
            shoppingCart.setUserId(userId);
            shoppingCart.setImage(item.getImage());
            Long dishId = item.getDishId();
            Long setmealId = item.getSetmealId();
            if (dishId != null) {
                //如果是菜品那就添加菜品的查询条件
                shoppingCart.setDishId(dishId);
            } else {
                //添加到购物车的是套餐
                shoppingCart.setSetmealId(setmealId);
            }
            shoppingCart.setName(item.getName());
            shoppingCart.setDishFlavor(item.getDishFlavor());
            shoppingCart.setNumber(item.getNumber());
            shoppingCart.setAmount(item.getAmount());
            shoppingCart.setCreateTime(LocalDateTime.now());

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

        //把携带数据的购物车批量插入购物车表  这个批量保存的方法要使用熟练!!!
        shoppingCartService.saveBatch(shoppingCartList);

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

瑞吉外卖项目功能全实现及完全代码解析_第11张图片

 瑞吉外卖项目功能全实现及完全代码解析_第12张图片

2.109购物车管理

ShoppingCartMapper
package com.hjg.reggie.mapper;

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

@Mapper
public interface ShoppingCartMapper extends BaseMapper {
}
ShoppingCartService
package com.hjg.reggie.service;

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

public interface ShoppingCartService extends IService {

    /**
     * 添加菜品套餐到购物车
     */
    public ShoppingCart add(ShoppingCart shoppingCart);

    /**
     * 减少菜品套餐到购物车
     */
    public ShoppingCart sub(ShoppingCart shoppingCart);

    /**
     * 清空购物车
     */
    public void clean();
}
ShoppingCartServiceImpl
package com.hjg.reggie.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hjg.reggie.common.BaseContext;
import com.hjg.reggie.common.CustomException;
import com.hjg.reggie.common.R;
import com.hjg.reggie.entity.ShoppingCart;
import com.hjg.reggie.mapper.ShoppingCartMapper;
import com.hjg.reggie.service.ShoppingCartService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;

@Service
public class ShoppingCartServiceImpl extends ServiceImpl implements ShoppingCartService {
    @Autowired
    private ShoppingCartService shoppingCartService;

    /**
     * 添加购物车
     * @param shoppingCart
     * @return
     * 请求 URL: http://localhost:8080/shoppingCart/add
     * 负载:{"amount":138,"dishFlavor":"常温,不要蒜,微辣","dishId":"1397851370462687234",
     *       "name":"邵阳猪血丸子","image":"2a50628e-7758-4c51-9fbb-d37c61cdacad.jpg"}
     */
    @Transactional
    public ShoppingCart add(ShoppingCart shoppingCart) {
        //设置用户id,指定当前是哪个用户的购物车数据
        Long currentId = BaseContext.getCurrentId();
        shoppingCart.setUserId(currentId);

        Long dishId = shoppingCart.getDishId();

        LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(ShoppingCart::getUserId,currentId);

        if(dishId != null){
            //添加到购物车的是菜品
            queryWrapper.eq(ShoppingCart::getDishId,dishId);
        }else{
            //添加到购物车的是套餐
            queryWrapper.eq(ShoppingCart::getSetmealId,shoppingCart.getSetmealId());
        }

        //查询当前菜品或者套餐是否在购物车中
        //SQL:select * from shopping_cart where user_id = ? and dish_id/setmeal_id = ?
        ShoppingCart cartServiceOne = shoppingCartService.getOne(queryWrapper);

        if(cartServiceOne != null){
            //如果已经存在,就在原来数量基础上加一
            Integer number = cartServiceOne.getNumber();
            cartServiceOne.setNumber(number + 1);
            shoppingCartService.updateById(cartServiceOne);
        }else{
            //如果不存在,则添加到购物车,数量默认就是一
            shoppingCart.setNumber(1);
            //注意这个不能使用自动填充,因为这个实体类只有createTime,没有updateTime
            shoppingCart.setCreateTime(LocalDateTime.now());
            shoppingCartService.save(shoppingCart);
            cartServiceOne = shoppingCart;
        }
        return cartServiceOne;
    }

    /**
     * 减少菜品套餐到购物车
     */
    @Transactional
    public ShoppingCart sub(ShoppingCart shoppingCart) {
        LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();

        Long dishId = shoppingCart.getDishId();
        //代表数量减少的是菜品数量
        if (dishId != null){
            //通过dishId查出购物车对象
            queryWrapper.eq(ShoppingCart::getDishId,dishId);
            //这里必须要加两个条件,否则会出现用户互相修改对方与自己购物车中相同套餐或者是菜品的数量
            queryWrapper.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());
            ShoppingCart cart1 = shoppingCartService.getOne(queryWrapper);
            cart1.setNumber(cart1.getNumber()-1);
            Integer LatestNumber = cart1.getNumber();
            if (LatestNumber > 0){
                //对数据进行更新操作
                shoppingCartService.updateById(cart1);
            }else if(LatestNumber == 0){
                //如果购物车的菜品数量减为0,那么就把菜品从购物车删除
                shoppingCartService.removeById(cart1.getId());
            }else if (LatestNumber < 0){
                throw new CustomException("操作异常");
            }

            return cart1;
        }

        Long setmealId = shoppingCart.getSetmealId();
        //代表是套餐数量减少
        if (setmealId != null){
            queryWrapper.eq(ShoppingCart::getSetmealId,setmealId).eq(ShoppingCart::getUserId, BaseContext.getCurrentId());
            ShoppingCart cart2 = shoppingCartService.getOne(queryWrapper);
            cart2.setNumber(cart2.getNumber()-1);
            Integer LatestNumber = cart2.getNumber();
            if (LatestNumber > 0){
                //对数据进行更新操作
                shoppingCartService.updateById(cart2);
            }else if(LatestNumber == 0){
                //如果购物车的套餐数量减为0,那么就把套餐从购物车删除
                shoppingCartService.removeById(cart2.getId());
            }else if (LatestNumber < 0){
                throw new CustomException("操作异常");
            }
            return cart2;
        }
        //如果两个if判断都进不去
        throw new CustomException("操作异常");
    }

    /**
     * 清空购物车
     */
    public void clean() {
        LambdaQueryWrapper queryWrapper=new LambdaQueryWrapper<>();
        queryWrapper.eq(ShoppingCart::getUserId, BaseContext.getCurrentId());

        shoppingCartService.remove(queryWrapper);
    }

}
ShoppingCartController
package com.hjg.reggie.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.hjg.reggie.common.BaseContext;
import com.hjg.reggie.common.R;
import com.hjg.reggie.entity.ShoppingCart;
import com.hjg.reggie.service.ShoppingCartService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * 购物车
 */
@Slf4j
@RestController
@RequestMapping("/shoppingCart")
public class ShoppingCartController {
    @Autowired
    private ShoppingCartService shoppingCartService;

    /**
     * 添加购物车
     * @param shoppingCart
     * @return
     * 请求 URL: http://localhost:8080/shoppingCart/add
     * 负载:{"amount":138,"dishFlavor":"常温,不要蒜,微辣","dishId":"1397851370462687234",
     *       "name":"邵阳猪血丸子","image":"2a50628e-7758-4c51-9fbb-d37c61cdacad.jpg"}
     */
    @PostMapping("/add")
    public R add(@RequestBody ShoppingCart shoppingCart){
        log.info("购物车数据:{}",shoppingCart);
        ShoppingCart cart = shoppingCartService.add(shoppingCart);
        return R.success(cart);
    }

    /**
     * 客户端的套餐或者是菜品数量减少设置
     * 没必要设置返回值
     * @param shoppingCart
     */
    @PostMapping("/sub")
    public R sub(@RequestBody ShoppingCart shoppingCart){
        log.info("购物车数据:{}",shoppingCart);
        ShoppingCart cart = shoppingCartService.sub(shoppingCart);
        return R.success(cart);
    }

    /**
     * 查看购物车
     * @return
     */
    @GetMapping("/list")
    public R> list(){
        log.info("查看购物车...");

        LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(ShoppingCart::getUserId, BaseContext.getCurrentId());
        queryWrapper.orderByAsc(ShoppingCart::getCreateTime);

        List list = shoppingCartService.list(queryWrapper);

        return R.success(list);
    }

    /**
     * 清空购物车
     * @return
     */
    @DeleteMapping("/clean")
    public R clean(){
        log.info("清空购物车...");
        shoppingCartService.clean();
        return R.success("清空购物车成功");
    }

}

瑞吉外卖项目功能全实现及完全代码解析_第13张图片


3.项目改进2.0代码

3.1Linux项目部署

Linux常用命令操作及springboot项目的部署_不知迷踪的博客-CSDN博客https://blog.csdn.net/weixin_59798969/article/details/127414055?spm=1001.2014.3001.5502

3.2项目优化

3.21环境准备

A.在项目的pom.xml文件中导入spring data redis的maven坐标


    org.springframework.boot
    spring-boot-starter-data-redis

B.在项目的application.yml中加入redis相关配置

spring:
    redis:
        # 加入redis相关配置
        host: localhost # 本地IP或者是虚拟机IP
        port: 6379
        # password: 1234 连接密码,没设置就关闭
        database: 0

注意: 引入上述依赖时,需要注意yml文件前面的缩进,上述配置应该配置在spring层级下面。

C.编写Redis的配置类RedisConfig,定义RedisTemplate

package com.hjg.reggie.config;

import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig extends CachingConfigurerSupport {
    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate redisTemplate = new RedisTemplate<>();
        //默认的Key序列化器为:JdkSerializationRedisSerializer
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setConnectionFactory(connectionFactory);
        return redisTemplate;
    }
}

解释说明:

1). 在SpringBoot工程启动时, 会加载一个自动配置类 RedisAutoConfiguration, 在里面已经声明了RedisTemplate这个bean

瑞吉外卖项目功能全实现及完全代码解析_第14张图片

上述框架默认声明的RedisTemplate用的key和value的序列化方式是默认的 JdkSerializationRedisSerializer,如果key采用这种方式序列化,最终我们在测试时通过redis的图形化界面查询不是很方便,如下形式:

2). 如果使用我们自定义的RedisTemplate, key的序列化方式使用的是StringRedisSerializer, 也就是字符串形式, 最终效果如下:

3). 定义了两个bean会不会出现冲突呢? 答案是不会, 因为源码如下:

瑞吉外卖项目功能全实现及完全代码解析_第15张图片

3.22缓存邮箱验证码

思路分析

前面我们已经实现了移动端邮箱验证码登录,随机生成的验证码我们是保存在HttpSession中的。但是在我们实际的业务场景中,一般验证码都是需要设置过期时间的,如果存在HttpSession中就无法设置过期时间,此时我们就需要对这一块的功能进行优化。

现在需要改造为将验证码缓存在Redis中,具体的实现思路如下:

  • 1). 在服务端UserController中注入RedisTemplate对象,用于操作Redis;
  • 2). 在服务端UserController的sendMsg方法中,将随机生成的验证码缓存到Redis中,并设置有效期为5分钟;
  • 3). 在服务端UserController的login方法中,从Redis中获取缓存的验证码,如果登录成功则删除Redis中的验证码;

A. 在UserController中注入RedisTemplate对象,用于操作Redis

@Autowired
private RedisTemplate redisTemplate;

B. 在UserController的sendMsg方法中,将生成的验证码保存到Redis

//需要将生成的验证码保存到Redis,设置过期时间
redisTemplate.opsForValue().set(phone, code, 5, TimeUnit.MINUTES);

C. 在UserController的login方法中,从Redis中获取生成的验证码,如果登录成功则删除Redis中缓存的验证码

//从Redis中获取缓存的验证码
Object codeInSession = redisTemplate.opsForValue().get(phone);
//从Redis中删除缓存的验证码
redisTemplate.delete(phone);

瑞吉外卖项目功能全实现及完全代码解析_第16张图片

注意:启动项目前必须开启Redis服务
UserController全代码:
package com.hjg.reggie.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.hjg.reggie.common.R;
import com.hjg.reggie.entity.User;
import com.hjg.reggie.service.UserService;
import com.hjg.reggie.utils.ValidateCodeUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
    @Autowired
    private UserService userService;

    @Autowired
    private RedisTemplate redisTemplate;

    //获取验证码
    @PostMapping("/sendMsg")
    public R sendMsg(HttpSession session, @RequestBody User user){
        //获取邮箱号
        //相当于发送短信定义的String to
        String email = user.getPhone();
        String subject = "瑞吉外卖";
        //StringUtils.isNotEmpty字符串非空判断
        if (StringUtils.isNotEmpty(email)) {
            //发送一个四位数的验证码,把验证码变成String类型
            String code = ValidateCodeUtils.generateValidateCode(4).toString();
            String text = "【瑞吉外卖】您好,您的登录验证码为:" + code + ",请尽快登录,验证码有效时间5分钟。如非本人操作,请忽略此邮件。";
            log.info("验证码为:" + code);
            //发送短信
            userService.sendMsg(email,subject,text);

            /*
            //将验证码保存到session当中
            //将邮箱作为key,将code最为value保存到session中,,因此邮箱和验证码可以一一对应
            session.setAttribute(email,code);
            */

            //需要将生成的验证码保存到Redis,设置过期时间
            redisTemplate.opsForValue().set(email, code, 5, TimeUnit.MINUTES);

            return R.success("验证码发送成功");
        }
        return R.error("验证码发送异常,请重新发送");
    }

    //登录
    @PostMapping("/login")
    //Map存JSON数据
    public R login(HttpSession session,@RequestBody Map map){
        //获取邮箱,用户输入的,这个phone就是输入的邮箱
        String phone = map.get("phone").toString();
        //获取验证码,用户输入的,这个code就是生成的验证码
        String code = map.get("code").toString();

        /**
         * 获取session中保存的验证码
         * 登录的邮箱作为session的key值,将code最为value
         * 因此邮箱和验证码可以一一对应,保证邮箱验证码数据一致完整性
         * */
        //Object sessionCode = session.getAttribute(phone);

        //改动:从Redis中获取缓存的验证码
        Object sessionCode = redisTemplate.opsForValue().get(phone);

        //将session的验证码和用户输入的验证码进行比对
        if (sessionCode != null && sessionCode.equals(code)) {
            //要是User数据库没有这个邮箱则自动注册,先看看输入的邮箱是否存在数据库
            LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(User::getPhone,phone);
            //获得唯一的用户,因为邮箱是唯一的
            User user = userService.getOne(queryWrapper);
            //要是User数据库没有这个邮箱则自动注册
            if (user == null) {
                user = new User();
                user.setPhone(phone);
                user.setStatus(1);
                //取邮箱的前五位为用户名
                user.setName("用户"+phone.substring(0,5));
                userService.save(user);
            }
            //不保存这个用户名就登不上去,因为过滤器需要得到这个user才能放行,程序才知道你登录了
            session.setAttribute("user", user.getId());

            //如果用户登陆成功,则删除Redis缓存中的验证码
            redisTemplate.delete(phone);

            return R.success(user);
        }
        return R.error("登录失败");
    }

    /**
     * 退出功能
     * ①在controller中创建对应的处理方法来接受前端的请求,请求方式为post;
     * ②清理session中的用户id
     * ③返回结果(前端页面会进行跳转到登录页面)
     * @return
     */
    @PostMapping("/logout")
    public R logout(HttpServletRequest request){
        //清理session中的用户id
        request.getSession().removeAttribute("user");
        return R.success("退出成功");
    }
}

测试

1). 访问前端工程,获取验证码

瑞吉外卖项目功能全实现及完全代码解析_第17张图片

2). 通过Redis的图形化界面工具查看Redis中的数据  

瑞吉外卖项目功能全实现及完全代码解析_第18张图片

登录后这个Redis数据会自动删除 

3.23查询菜品缓存

前面我们已经实现了移动端菜品查看功能,对应的服务端方法为DishController的list方法,此方法会根据前端提交的查询条件(categoryId)进行数据库查询操作。在高并发的情况下,频繁查询数据库会导致系统性能下降,服务端响应时间增长。现在需要对此方法进行缓存优化,提高系统的性能。

注意

在使用缓存过程中,要注意保证数据库中的数据和缓存中的数据一致,如果数据库中的数据发生变化,需要及时清理缓存数据。否则就会造成缓存数据与数据库数据不一致的情况

具体的实现思路如下:

  • 1). 改造DishController的list方法,先从Redis中获取分类对应的菜品数据,如果有则直接返回,无需查询数据库;如果没有则查询数据库,并将查询到的菜品数据存入Redis。
  • 2). 改造DishController的增加,修改,删除,启售停售等操作均要删除已缓存的Redis数据,加入清理缓存的逻辑。

查询菜品缓存

请求 URL:

http://localhost:8080/dish/list?categoryId=1578680783852396546&status=1

改造的方法 redis的数据类型 redis缓存的key redis缓存的value
list string dish分类Id状态 , 比如: dish_12323232323_1 List

1). 在DishController中注入RedisTemplate

@Autowired
private RedisTemplate redisTemplate;

2). 在list方法中,查询数据库之前,先查询缓存, 缓存中有数据, 直接返回

List dishDtoList = null;
//动态构造key
String key = "dish_" + dish.getCategoryId() + "_" + dish.getStatus();//dish_1397844391040167938_1
//先从redis中获取缓存数据
dishDtoList = (List) redisTemplate.opsForValue().get(key);
if(dishDtoList != null){
    //如果存在,直接返回,无需查询数据库
    return R.success(dishDtoList);
}

瑞吉外卖项目功能全实现及完全代码解析_第19张图片

3). 如果redis不存在,查询数据库,并将数据库查询结果,缓存在redis,并设置过期时间

//如果不存在,需要查询数据库,将查询到的菜品数据缓存到Redis
redisTemplate.opsForValue().set(key,dishDtoList,60, TimeUnit.MINUTES);

3.24清理菜品缓存

为了保证数据库中的数据和缓存中的数据一致,如果数据库中的数据发生变化,需要及时清理缓存数据。所以,我们需要在增加菜品,修改菜品,删除菜品,启售停售菜品时清空缓存数据。

1). 保存菜品,清空缓存

在保存菜品的方法save中,当菜品数据保存完毕之后,需要清空菜品的缓存。那么这里清理菜品缓存的方式存在两种:

A. 清理所有分类下的菜品缓存

//清理所有菜品的缓存数据
Set keys = redisTemplate.keys("dish_*"); //获取所有以dish_xxx开头的key
redisTemplate.delete(keys); //删除这些key

B. 清理当前添加菜品分类下的缓存

//清理某个分类下面的菜品缓存数据
String key = "dish_" + dishDto.getCategoryId() + "_1";
redisTemplate.delete(key);

此处, 我们推荐使用第二种清理的方式, 只清理当前菜品关联的分类下的菜品数据。 

瑞吉外卖项目功能全实现及完全代码解析_第20张图片

2). 更新菜品,清空缓存

在更新菜品的方法update中,当菜品数据更新完毕之后,需要清空菜品的缓存。这里清理缓存的方式和上述基本一致。

A. 清理所有分类下的菜品缓存

//清理所有菜品的缓存数据
Set keys = redisTemplate.keys("dish_*"); //获取所有以dish_xxx开头的key
redisTemplate.delete(keys); //删除这些key

B. 清理当前添加菜品分类下的缓存

//清理某个分类下面的菜品缓存数据
String key = "dish_" + dishDto.getCategoryId() + "_1";
redisTemplate.delete(key);

瑞吉外卖项目功能全实现及完全代码解析_第21张图片

注意: 在这里我们推荐使用第一种方式进行清理,这样逻辑更加严谨。 因为对于修改操作,用户是可以修改菜品的分类的,如果用户修改了菜品的分类,那么原来分类下将少一个菜品,新的分类下将多一个菜品,这样的话,两个分类下的菜品列表数据都发生了变化。

3). 删除菜品,清空缓存

这个在DishServiceImpl

瑞吉外卖项目功能全实现及完全代码解析_第22张图片

4). 启售停售菜品,清空缓存

瑞吉外卖项目功能全实现及完全代码解析_第23张图片

3.25SpringCache技术

介绍

Spring Cache是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能,大大简化我们在业务中操作缓存的代码。

Spring Cache只是提供了一层抽象,底层可以切换不同的cache实现。具体就是通过CacheManager接口来统一不同的缓存技术。CacheManager是Spring提供的各种缓存技术抽象接口。

针对不同的缓存技术需要实现不同的CacheManager:

CacheManager 描述
EhCacheCacheManager 使用EhCache作为缓存技术
GuavaCacheManager 使用Google的GuavaCache作为缓存技术
RedisCacheManager 使用Redis作为缓存技术

注解

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

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

在spring boot项目中,使用缓存技术只需在项目中导入相关缓存技术的依赖包,并在启动类上使用@EnableCaching开启缓存支持即可。

例如,使用Redis作为缓存技术,只需要导入Spring data Redis的maven坐标即可。

3.26缓存套餐数据

实现思路

前面我们已经实现了移动端套餐查看功能,对应的服务端方法为SetmealController的list方法,此方法会根据前端提交的查询条件进行数据库查询操作。在高并发的情况下,频繁查询数据库会导致系统性能下降,服务端响应时间增长。现在需要对此方法进行缓存优化,提高系统的性能。

具体的实现思路如下

  • 1). 导入Spring Cache和Redis相关maven坐标
  • 2). 在application.yml中配置缓存数据的过期时间
  • 3). 在启动类上加入@EnableCaching注解,开启缓存注解功能
  • 4). 在SetmealController的list方法上加入@Cacheable注解
  • 5). 在SetmealController的增加,删除,修改,启售停售方法上加入CacheEvict注解

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


    org.springframework.boot
    spring-boot-starter-cache

备注: spring-boot-starter-data-redis 这个依赖前面已经引入了, 无需再次引入。  

A2.application.yml中设置缓存过期时间

spring:  
  cache:
    redis:
      time-to-live: 1800000 #设置缓存数据的过期时间

:还需要自己封装的返回对象R,继承一下序列化接口

瑞吉外卖项目功能全实现及完全代码解析_第24张图片

序列化后可以将返回的R对象保存到Redis中,R里面已经包含了所需要的信息,如菜品数据等。前端从Redis获取数据时也是获取的保存的R数据,并将其相应的赋值给前端的vue模型。

A3.启动类上加入@EnableCaching注解

瑞吉外卖项目功能全实现及完全代码解析_第25张图片

A4. 在SetmealController的list方法上加入@Cacheable注解

瑞吉外卖项目功能全实现及完全代码解析_第26张图片

/**
* 根据条件查询套餐数据
* @param setmeal
* @return
*/
@GetMapping("/list")
@Cacheable(value = "setmealCache",key = "#setmeal.categoryId + '_' + #setmeal.status")
public R> list(Setmeal setmeal){
    LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(setmeal.getCategoryId() != null,Setmeal::getCategoryId,setmeal.getCategoryId());
    queryWrapper.eq(setmeal.getStatus() != null,Setmeal::getStatus,setmeal.getStatus());
    queryWrapper.orderByDesc(Setmeal::getUpdateTime);

    List list = setmealService.list(queryWrapper);

    return R.success(list);
}

A5. 在SetmealController的增加,删除,修改,启售停售方法上加入CacheEvict注解

瑞吉外卖项目功能全实现及完全代码解析_第27张图片

3.3数据库读写分离

3.31目前数据库存在的困难

1). 存在的问题

在前面基础功能实现的过程中,我们后台管理系统及移动端的用户,在进行数据访问时,都是直接操作数据库MySQL的。结构如下图:

瑞吉外卖项目功能全实现及完全代码解析_第28张图片

而在当前,MySQL服务器只有一台,那么就可能会存在如下问题

  • 1. 读和写所有压力都由一台数据库承担,压力大
  • 2. 数据库服务器磁盘损坏则数据丢失,单点故障

2). 解决方案

为了解决上述提到的两个问题,我们可以准备两台MySQL,一台主(Master)服务器,一台从(Slave)服务器,主库的数据变更,需要同步到从库中(主从复制)。而用户在访问我们项目时,如果是写操作(insert、update、delete),则直接操作主库;如果是读(select)操作,则直接操作从库(在这种读写分离的结构中,从库是可以有多个的),这种结构我们称为读写分离

瑞吉外卖项目功能全实现及完全代码解析_第29张图片

今天我们就需要实现上述的架构,来解决业务开发中所存在的问题。  

3.32MySQL主从复制简介与服务器的克隆

MySQL数据库默认是支持主从复制的,不需要借助于其他的技术,我们只需要在数据库中简单的配置即可。接下来,我们就从以下的几个方面,来介绍一下主从复制:

1.介绍

MySQL主从复制是一个异步的复制过程,底层是基于MySQL数据库自带的二进制日志功能。就是一台或多台MySQL数据库(slave,即从库)从另一台MySQL数据库(master,即主库)进行日志的复制,然后再解析日志并应用到自身,最终实现 从库 的数据和 主库 的数据保持一致。MySQL主从复制是MySQL数据库自带功能,无需借助第三方工具。

二进制日志:

二进制日志(BINLOG)记录了所有的 DDL(数据定义语言)语句和 DML(数据操纵语言)语句,但是不包括数据查询语句。此日志对于灾难时的数据恢复起着极其重要的作用,MySQL的主从复制, 就是通过该binlog实现的。默认MySQL是未开启该日志的。

MySQL的主从复制原理: 

瑞吉外卖项目功能全实现及完全代码解析_第30张图片

MySQL复制过程分成三步:

  • 1). MySQL master 将数据变更写入二进制日志( binary log)
  • 2). slave将master的binary log拷贝到它的中继日志(relay log)
  • 3). slave重做中继日志中的事件,将数据变更反映它自己的数据

2.准备工作

提前准备两台服务器,并且在服务器中安装MySQL,服务器的信息如下:

数据库 IP 数据库版本
Master 192.168.199.129 5.7.25
Slave 192.168.199.130 5.7.25

并在两台服务器上做如下准备工作:

1). 防火墙开放3306端口号

firewall-cmd --zone=public --add-port=3306/tcp --permanent

firewall-cmd --zone=public --list-ports

2). 并将两台数据库服务器启动起来:

systemctl start mysqld

登录MySQL,验证是否正常启动


那么如何准备两台服务器呢?

A.可以按照以上步骤重新安装一台虚拟机

B.根据一台服务器克隆出另一台

我们使用第二种方法:

瑞吉外卖项目功能全实现及完全代码解析_第31张图片

克隆完成后还需要

A.修改IP

vim /etc/sysconfig/network-scripts/ifcfg-ens33

B. 修改MySQL的UUID

B1.使用 find / -iname "auto.cnf" 命令查找你数据库的auto.cnf 配置文件。

find / -iname "auto.cnf"

我的在这个目录下,大家的的也可能在其他目录,取决于你mysql放的位置。 

B2.对这个配置文件的uuid进行更改。

vim /var/lib/mysql/auto.cnf

瑞吉外卖项目功能全实现及完全代码解析_第32张图片

将其修改一下,改成与原服务器不同的UUID。 

B3.重新启动mysql

systemctl restart mysqld

3.33主库配置

服务器: 192.168.199.129

1). 修改Mysql数据库的配置文件/etc/m

vim /etc/my.cnf

在最下面增加配置:

log-bin=mysql-bin   #[必须]启用二进制日志
server-id=100       #[必须]服务器唯一ID(唯一即可)

2). 重启Mysql服务

执行指令:

systemctl restart mysqld

3). 创建数据同步的用户并授权

登录mysql,并执行如下指令,创建用户并授权:

GRANT REPLICATION SLAVE ON *.* to 'xiaoming'@'%' identified by 'Root@1234';

注:上面SQL的作用是创建一个用户xiaoming,密码为 Root@1234 ,并且给xiaoming用户授予REPLICATION SLAVE权限。常用于建立复制时所需要用到的用户权限,也就是slave必须被master授权具有该权限的用户,才能通过该用户复制。

4). 登录Mysql数据库,查看master同步状态

执行下面SQL,记录下结果中FilePosition的值

show master status;

注:上面SQL的作用是查看Master的状态,执行完此SQL后不要再执行任何操作。 

3.34从库配置

服务器: 192.168.199.130

1). 修改Mysql数据库的配置文件/etc/my.cnf

server-id=101 	#[必须]服务器唯一ID

2). 重启Mysql服务

systemctl restart mysqld

3). 登录Mysql数据库,设置主库地址及同步位置

change master to master_host='192.168.199.129',master_user='xiaoming',master_password='Root@1234',master_log_file='mysql-bin.000002',master_log_pos=154;

start slave;

参数说明:

A. master_host : 主库的IP地址

B. master_user : 访问主库进行主从复制的用户名(上面在主库创建的)

C. master_password : 访问主库进行主从复制的用户名对应的密码

D. master_log_file : 从哪个日志文件开始同步(上述查询master状态中展示的有)

E. master_log_pos : 从指定日志文件的哪个位置开始同步(上述查询master状态中展示的有

4). 查看从数据库的状态

然后通过状态信息中的 Slave_IO_running 和 Slave_SQL_running 可以看出主从同步是否就绪,如果这两个参数全为Yes,表示主从同步已经配置完成。

3.35项目读写分离

面对日益增加的系统访问量,数据库的吞吐量面临着巨大瓶颈。 对于同一时刻有大量并发读操作和较少写操作类型的应用系统来说,将数据库拆分为主库从库,主库负责处理事务性的增删改操作,从库负责处理查询操作,能够有效的避免由数据更新导致的行锁,使得整个系统的查询性能得到极大的改善。

通过读写分离,就可以降低单台数据库的访问压力, 提高访问效率,也可以避免单机故障。

1.数据库环境准备

1). 将自己本地的reggie数据库的数据导出SQL文件

瑞吉外卖项目功能全实现及完全代码解析_第33张图片

这样做的话,我们之前自己开发时,添加的测试数据都还在的,便于测试。  

2). 在主数据库master中,创建数据库reggie,并导入该SQL文件

master中创建数据库,会自动同步至slave从库

瑞吉外卖项目功能全实现及完全代码解析_第34张图片

3). 在master中导入sql文件

瑞吉外卖项目功能全实现及完全代码解析_第35张图片

注:MySQL8.0数据存储MySQL5.7小技巧

将导出的sql文件的内容复制出来到Word等文档编辑工具中,然后进行文本替换,最后将他们在MySQL5.7中运行即可。

utf8mb4替换为utf8
utf8mb4_0900_ai_ci替换为utf8_general_ci
utf8_croatian_ci替换为utf8_general_ci
utf8mb4_general_ci替换为utf8_general_ci

2.读写分离配置

1). 在项目的pom.xml增加依赖

Sharding-JDBC定位为轻量级Java框架,在Java的JDBC层提供的额外服务。 它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架。使用Sharding-JDBC可以在程序中轻松的实现数据库读写分离。

Sharding-JDBC具有以下几个特点:

  • 1). 适用于任何基于JDBC的ORM框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template或直接使用JDBC。
  • 2). 支持任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP, Druid, HikariCP等。
  • 3). 支持任意实现JDBC规范的数据库。目前支持MySQL,Oracle,SQLServer,PostgreSQL以及任何遵循SQL92标准的数据库。  

    org.apache.shardingsphere
    sharding-jdbc-spring-boot-starter
    4.0.0-RC1

2). 在项目的application.yml中配置数据源相关信息

spring:
  shardingsphere:
    datasource:
      names:
        master,slave
      # 主数据源
      master:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://192.168.199.129:3306/reggie?characterEncoding=utf-8&useSSL=false
        username: root
        password: root
      # 从数据源
      slave:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://192.168.199.130:3306/reggie?characterEncoding=utf-8&useSSL=false
        username: root
        password: root
    masterslave:
      # 读写分离配置
      load-balance-algorithm-type: round_robin #轮询
      # 最终的数据源名称
      name: dataSource
      # 主库数据源名称
      master-data-source-name: master
      # 从库数据源名称列表,多个逗号分隔
      slave-data-source-names: slave
    props:
      sql:
        show: true #开启SQL显示,默认false
  main:
    allow-bean-definition-overriding: true

配置解析:

瑞吉外卖项目功能全实现及完全代码解析_第36张图片

: 配置好以上操作后就不用管了,此框架会自动判断,如果是查询的SQL会使用salve数据源操作其所对应的库,如果是增删改操作的SQL则会使用master数据源操作其所对应的数据库。


 如果不配置该项,项目启动之后将会报错:

 报错信息表明,在声明 org.apache.shardingsphere.shardingjdbc.spring.boot 包下的SpringBootConfiguration中的dataSource这个bean时出错, 原因是有一个同名的 dataSource 的bean在com.alibaba.druid.spring.boot.autoconfigure包下的DruidDataSourceAutoConfigure类加载时已经声明了。

而我们需要用到的是 shardingjdbc包下的dataSource,所以我们需要配置上述属性,让后加载的覆盖先加载的。


3.功能测试

配置完毕之后,我们启动项目进行测试(注意别忘了启动Redis服务和配置主库从库的虚拟机服务器),直接访问系统管理后台的页面,然后执行相关业务操作,看控制台输出的日志信息即可。

查询操作:

瑞吉外卖项目功能全实现及完全代码解析_第37张图片

修改操作: 

瑞吉外卖项目功能全实现及完全代码解析_第38张图片

该项目读写分离设置成功。

4.工程结构

瑞吉外卖项目功能全实现及完全代码解析_第39张图片

5.代码下载

瑞吉外卖项目源码及数据库资源-Java文档类资源-CSDN文库https://download.csdn.net/download/weixin_59798969/86832050


可以点个免费的赞吗!!!   

你可能感兴趣的:(SSM,小程序,spring,mybatis,spring,boot,瑞吉外卖)