外卖项目

瑞吉外卖项目

软件开发流程

1、需求分析

产品原型、需求规格说明书

2、设计

产品文档、UI界面设计、概要设计、详细设计、数据库设计

3、编码

项目代码、单元测试

4、测试

测试用例、设施报告

5、上线运维

软件环境安装、配置

角色分工

  • 项目经理:对整个项目负责,任务分配,把控进度
  • 产品经理:进行需求调研,输出需求调研文档、产品原型等
  • UI设计师:根据产品原型输出界面效果图
  • 架构师:项目整体架构设计,技术选型等
  • 开发工程师:代码实现
  • 测试工程师:编写测试用例,输出测试报告
  • 运维工程师:软件环境搭建、项目上线

软件环境

  • 开发环境(development):开发人员在开发阶段使用的环境,一般外部用户无法访问
  • 测试环境(testing):专门给测试人员使用的环境,用于测试项目,一般外部用户无法访问
  • 生产环境(production):即线上环境,正式提供对外服务的环境

瑞吉外卖项目介绍

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

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

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

产品原型

就是一款产品成型之前的一个简单的框架,就是将页面的排版布局展现出来,使产品的初步构思有一个可视化的展示。通过原型展示,可以更加直观的了解项目的需求和提供的功能。

导入pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <groupId>com.os467</groupId>
    <artifactId>reggie_take_out</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <scope>compile</scope>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.2</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.20</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.76</version>
        </dependency>

        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.23</version>
        </dependency>

    </dependencies>


    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.3.5.RELEASE</version>
            </plugin>
        </plugins>
    </build>




</project>

配置springboot启动类

package com.os467.reggie;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ReggieApplication {

    public static void main(String[] args) {

        SpringApplication.run(ReggieApplication.class,args);

    }

}

Slf4j

Simple Logging Facade for Java(SLF4J)作为各种日志框架(例如Java.util.Logging、logback、log4j)的简单外观或抽象

首先slf4j可以理解为规则的制定者,是一个抽象层,定义了日志相关的接口。log4j,logback,JDK Logging都是slf4j的实现层,只是出处不同,当然使用起来也就各有千秋

引用文章

为什么加了@slf4j注解 就可以直接使用log了呢?

如果不使用注解,那我们使用log,需要这样定义

static Logger logger = LoggerFactory.getLogger("Update的日志")

其实在编译完成生成字节码文件的时候idea就已经帮我们生成了这段代码

这就得益于lombok插件

其实这就是因为Lombok项目是一种自动接通你的编辑器和构建工具的一个Java库

可以理解为有了@slf4j,编辑器里就会给你加log的智能提示,但这并不代表log对象已生成

Lombok

Lombok官网

Lombok项目是一个java库,它可以自动插入到编辑器和构建工具中,增强java的性能,不需要再写getter、setter或equals方法,只要有一个注解,你的类就有一个功能齐全的构建器、自动记录变量等等

@Data

整合了Getter、Setter、ToString、EqualsAndHashCode、RequiredArgsConstructor注解

@Getter

快速构建Getter方法

@Setter

快速构建Setter方法

@ToString

快速将当前对象转换成字符串类型,便于log

@EqualsAndHashCode

快速进行相等判断

@NonNull

判断变量(对象)是否为空

https://blog.csdn.net/weixin_45433031/article/details/121846207

配置核心配置类

方法1 实现WebMvcConfigurer接口

WebMvcConfigurer 接口其实是Spring内部的一种配置方式,我们需要去实现这个接口

(接口中的default关键字规定了接口方法有方法体并且能被实现类继承和重写)

package com.os467.reggie.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Slf4j
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    /**
     * 设置静态资源映射
     * @param registry
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {

        log.info("开始进行静态资源映射");

        //将前端拦截到的资源路径映射到本地服务器静态资源
        registry.addResourceHandler("/backend/**").addResourceLocations("classpath:backend/");

        registry.addResourceHandler("/front/**").addResourceLocations("classpath:front/");
    }

    
}

方法2 继承WebMvcConfigurationSupport类

由于服务器无法访问后端的backend和front资源,我们需要配置一个WebMvcConfig配置类,我们需要让这个类去继承WebMvcConfigurationSupport这个类

这个类为我们提供了一个资源拦截器方法

我们想自定义静态资源映射目录的话,只需重写addResourceHandlers方法即可

SpringBoot拦截器addResourceHandlers()

addResourceLocations() 需要映射到的后端资源地址

package com.os467.reggie.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {

    /**
     * 设置静态资源映射
     * @param registry
     */
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {

        log.info("开始进行静态资源映射");

        //将前端拦截到的资源路径映射到本地服务器静态资源
        registry.addResourceHandler("/backend/**").addResourceLocations("classpath:backend/");

        registry.addResourceHandler("/front/**").addResourceLocations("classpath:front/");
        
    }
}

我们在持久层接口上加Mapper标签

让EmployeeMapper去继承MyBatisPlus提供的BaseMapper接口,需要提供一个实体类泛型

package com.os467.reggie.mapper;

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

@Mapper
public interface EmployeeMapper extends BaseMapper<Employee> {



}

表现层

@RequestBody注解 使得接收到的值指定为json串,必须是post方式提交

@PostMapping(“/login”)注解

等价于@RequestMapping(value = “/user/login”,method = RequestMethod.POST)

package com.os467.reggie.controller;

import com.os467.reggie.common.R;
import com.os467.reggie.entity.Employee;
import com.os467.reggie.service.EmployeeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

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

    //注入业务层实例
    @Autowired
    private EmployeeService employeeService;

    /**
     * 员工登入
     * @param request
     * @param employee @RequestBody 注解使得接收到的值指定为json串
     * @return
     */
    @PostMapping("/login")
    public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee){


        return null;
    }

}

返回结果类

本次工程内封装的返回结果类,用于给前端响应数据

package com.os467.reggie.common;

import lombok.Data;
import java.util.HashMap;
import java.util.Map;

/**
 * 通用返回结果类,服务端响应的数据最终都会封装成此对象
 * @param <T>
 */
@Data
public class R<T> {

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

    private String msg; //错误信息

    private T data; //数据

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

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

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

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

}

功能实现

1.1用户登录后端校验

需要对前端传来的用户密码进行md5加密

在后端查询是否有此用户名(用户名在数据库中设置了不能重)

进行密码校验

需要将用户数据存入到session域中

/**
 * 员工登入
 * @param request
 * @param employee @RequestBody 注解使得接收到的值指定为json串
 * @return
 */
@PostMapping("/login")
public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee){

    //1、将页面提交的密码password进行md5加密处理
    String password = employee.getPassword();

    password = DigestUtils.md5DigestAsHex(password.getBytes());

    //2、根据页面提交的用户名username查询数据库
    String username = employee.getUsername();

    LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();

    queryWrapper.eq(Employee::getUsername, username);

    Employee anEmployee = employeeService.getOne(queryWrapper);

    //3、如果没有查询到则返回登录失败结果
    if (anEmployee == null){

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

    }

    //4、密码比对,如果不一致则返回登陆失败结果
    if (!anEmployee.getPassword().equals(password)){

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

    }

    //5、查看员工状态,如果已禁用状态,则返回员工已禁用结果
    if (anEmployee.getStatus() == 0){

        return  R.error("账号已禁用");

    }

    //6、登录成功,将员工id存入Session域中并返回登入成功结果

    request.getSession().setAttribute("employee",employee.getId());

    return R.success(anEmployee);
}

1.2用户注销

/**
 * 员工注销方法
 * 1、清理Session中的用户id
 * 2、返回结果
 * @return
 */
@PostMapping("/logout")
public R<String> logout(HttpServletRequest request){

    //清除Session域中保存的当前登录员工的id
    request.getSession().removeAttribute("employee");

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

☆完善登录功能(过滤器)

如果用户不登陆,直接访问系统页面,照样可以正常访问

这种设计并不合理,我们希望看到的效果应该是,只有登录成功后才可以访问系统中的页面,如果没有登录则跳转登录页面

具体如何实现?

使用过滤器或者拦截器,在过滤器或拦截器中判断用户是否已经无参登录,如果没有登录则跳转登录页面

步骤

1、创建自定义过滤器LoginCheckFilter

2、在启动类上加入注解@ServletCompontentScan

3、完善过滤器的处理逻辑

创建过滤器

首先在springboot启动类上加**@ServletComponentScan注解**

  • 开启扫描包后
  • Servlet、Filter、Listener 可以直接通过 @WebServlet、@WebFilter、@WebListener 注解自动注册

@WebFilter注解

在servlet3.0以后,我们可以不用在web.xml文件内配置Filter,只需要加上**@WebFilter注解**就可以实现,以达到简化配置的目的

该注解用来声明servlet过滤器,将会在部署时被容器处理,容器将根据具体的属性配置将相应的类部署为过滤器

filterName属性

指定过滤器的名字

urlPatterns属性

指定一组过滤器的URL匹配模式

package com.os467.reggie.Filter;

import lombok.extern.slf4j.Slf4j;

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

/**
 * 检查用户是否完成登录
 */
@Slf4j
@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
public class LoginCheckFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

        //需要先向下转型
        HttpServletRequest request = (HttpServletRequest) servletRequest;

        HttpServletResponse response = (HttpServletResponse) servletResponse;

        //日志
        log.info("拦截到请求:{}",request.getRequestURI());

        filterChain.doFilter(request,response);

    }

}

过滤器具体的处理逻辑如下:

1、获取本次请求的URI

2、判断本次请求是否需要处理

3、如果不需要处理,则直接放行

4、判断登录状态,如果已登录,则直接放行

5、如果未登录则返回未登录结果

  • 过滤器的作用就是之一就是在用户的请求到达servlet之前,拦截下来做预处理
  • 处理之后便执行chain.doFilter(request, response)这个方法,如果还有别的过滤器,那么将处理好的请求传给下个过滤器,依此类推
  • 当所有的过滤器都把这个请求处理好了之后,再将处理完的请求发给servlet
  • 如果就这一个过滤器,那么就将处理好的请求直接发给servlet

AntPathMatcher: 路径匹配器,支持通配符

package com.os467.reggie.Filter;

import com.alibaba.fastjson.JSON;
import com.os467.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;

/**
 * 检查用户是否完成登录
 */
@Slf4j
@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
public class LoginCheckFilter implements Filter {

    //路径匹配器,支持通配符
    private 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();

        //定义不要被处理的请求路径
        String[] urls = new String[]{
                "/employee/login",
                "/employee/logout",
                "backend/**",
                "/front/**"
        };

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

        //3、如果不需要处理,则直接放行
        if (check){
            filterChain.doFilter(request,response);
            return;
        }

        //4、判断登录状态,如果已登录,则直接放行
       if(request.getSession().getAttribute("employee") != null){
           filterChain.doFilter(request,response);
           return;
       }

        //5、如果未登录则返回未登录结果,通过输出流的方式向客户端来响应数据
        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;
    }

}

1.3新增员工

为数据库新增员工

/**
 * 新增员工
 * @param employee
 * @return
 */
@PostMapping
public R<String> save(HttpServletRequest request,@RequestBody Employee employee){
    log.info("新增员工,员工信息:{}",employee.toString());

    //设置初始密码123456,并且md5加密
    employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));

    //设置创建和更新时间
    employee.setCreateTime(LocalDateTime.now());
    employee.setUpdateTime(LocalDateTime.now());

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

    //设置账户的创建更新人id
    employee.setCreateUser(empId);
    employee.setUpdateUser(empId);

    employeeService.save(employee);

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

☆全局异常捕获(AOP)

前面的程序还存在一个问题,就是当我们在新增员工时输入的账号已经存在,由于employee表中对该字段加入了唯一约束,此时程序会抛出异常

此时需要我们的程序进行异常捕获,通常由两种处理方式:

1、在Controller方法中加入try、catch进行异常捕获(不推荐)

2、使用异常处理器进行全局异常捕获

第二种方法其实就是通过AOP底层动态代理来对目标方法的增强(异常通知)

@ControllerAdvice注解

annotations属性: 被其中存放的注解修饰的方法会被拦截,传入注解字节码数组

@ExceptionHandler注解

传入异常类的字节码,修饰方法(异常通知的内容),指定异常的处理类型

package com.os467.reggie.common;

import lombok.extern.slf4j.Slf4j;
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(annotations = {RestController.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 异常处理方法
     * @return
     */
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public R<String> exceptionHandler(SQLIntegrityConstraintViolationException exception){

        log.error(exception.getMessage());

        //根据异常提示定位到具体异常处理
        if (exception.getMessage().contains(("Duplicate entry"))){

            String msg = exception.getMessage().split(" ")[2] + "已存在";

            return R.error(msg);
        }

        return R.error("未知错误");
    }
}

总结

1、根据产品原型明确业务需求

2、重点分析数据的流转过程和数据格式

3、通过debug断点调试跟踪程序执行过程

2.1员工信息的分页查询

系统中的员工很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据

代码开发

1、页面发送ajax请求,将分页查询参数(page,pageSize,name)提交到服务器

2、服务器Controller接收页面提交的数据并调用Service查询数据

3、Service调用Mapper操作数据库,查询分页数据

4、Controller将查询到的分页数据响应给页面

5、页面接收到分页数据并通过ElementUI的Table组件展示到页面上

/**
 * 员工信息的分页查询
 * @param page
 * @param pageSize
 * @param name
 * @return
 */
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name){

    log.info("page = {},pageSize={},name={}",page,pageSize,name);

    //构造分页构造器
    Page pageConstructor = new Page(page,pageSize);

    //构造条件构造器,需要提供泛型
    LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper();

    //添加过滤条件
    queryWrapper.like(StringUtils.isNotBlank(name),Employee::getUsername,name);

    //排序条件
    queryWrapper.orderByDesc(Employee::getUpdateTime);

    //执行查询
    employeeService.page(pageConstructor,queryWrapper);

    return R.success(pageConstructor);
}

2.2启用/禁用员工账号

需求分析

在员工管理列表页面,可以对某个员工账号进行启用或者禁用操作,账号禁用的员工不能登录系统,启用后的员工可以正常登录

需要注意,只有管理员(admin用户)可以对其他普通用户进行启用、禁用操作,所以普通用户登录系统后启用、禁用按钮不显示

管理员admin登录系统可以对所有员工账号进行启用,禁用操作

如果某个员工账号状态为正常,则按钮显示“禁用”,如果员工账号状态为已禁用,则按钮显示为“启用”

普通员工登录系统后,启用、禁用按钮不显示

代码开发

启用、禁用员工账号,本质上就是一个更新操作,也就是对status状态字段进行操作,在Controller中创建update方法,此方法时一个通用的修改员工信息的方法

☆JS处理Long类型精度丢失

代码修复

通过观察控制台输出的SQL发现页面传递过来的员工id的值和数据库中的id值不一致,这是怎么回事?

  • 原来 JS 内置有 32 位整数,而 number 类型的安全整数是 53 位。如果超过 53 位,则精度会丢失。正如现在后台传来一个 64 位的 Long 类型整数,因此超过了 53 位,所以后台返回的值和前台获取的值会不一样

  • 页面中js处理long型数字只能精确到前16位,所以最终通过ajax请求提交给服务端的时候id的后三位被四舍五入了

如何解决这个问题?

我们可以在服务端给页面响应json数据时进行处理,将long型数据统一转为String字符串

方法一

使用**@JsonSerialize注解**

该注解用于属性或者getter⽅法上,⽤于在序列化时嵌⼊开发者⾃定义的代码

@JsonSerialize(using = ToStringSerializer.class)

  • 通过fastjson序列化时,增加该字段的序列化类,该序列化类通过CODE查找名称,并序列化到前端,就不会丢失精度了
  • 使得Json序列化的时候将此属性转化为String类型

使用以下包

import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer
@JsonSerialize(using = ToStringSerializer.class)
private Long id;

方法二

1、提供对象转换器JacksonObjectMapper,基于jackson进行java对象到json数据的转换

2、在WebMvcConfig配置类中扩展SpringMVC的消息转换器,在此消息转换器中使用提供的对象转换器进行Java对象到Json数据的转换

直接使用JacksonObjectMapper类

package com.os467.reggie.common;

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

/**
 * 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
 * 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
 * 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
 */
public class JacksonObjectMapper extends ObjectMapper {

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

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

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


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

                .addSerializer(BigInteger.class, ToStringSerializer.instance)
                .addSerializer(Long.class, ToStringSerializer.instance)
                .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));

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

我们需要去扩展mvc框架的消息转换器

WebMvcConfig配置类中重写extendMessageConverters方法

消息转换器

添加自定义消息转换器不覆盖默认转换器

使用springboot中默认的Json消息转换器MappingJackson2HttpMessageConverter类创建转换器

这个类的主要实现逻辑是在AbstractJackson2HttpMessageConverter抽象类中实现的

这个列实现序列化与反序列化的最核心组件是ObjectMapper这个类

这些组件之间是如何协助,怎样才能合理的改写组件而实现自定义呢,这就需要了解起原理了

消息转换器创建和生效原理

CSDN博文引用

/**
 * 扩展mvc框架的消息转换器
 * @param converters
 */
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {

    //创建新的消息转换器
    MappingJackson2CborHttpMessageConverter messageConverter = new MappingJackson2CborHttpMessageConverter();

    //设置对象转换器,底层使用Jackson将java对象转成json
    messageConverter.setObjectMapper(new JacksonObjectMapper());

    //将上面的消息转换器对象追加到mvc框架的转换器集合中,放在最前面优先使用
    converters.add(0,messageConverter);

    super.extendMessageConverters(converters);
}

通用修改员工数据方法

/**
 * 根据id来修改员工信息
 * @param employee
 * @return
 */
@PutMapping
public R<String> update(HttpServletRequest request, @RequestBody Employee employee){

    log.info(employee+"");

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

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

    employeeService.updateById(employee);

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

2.3编辑员工信息

在员工管理列表页面点击编辑按钮,跳转到编辑页面,在编辑页面回显员工信息并进行修改,最后点击保存按钮完成编辑操作

代码开发

1、点击编辑按钮时,页面跳转到add.html,并在url中携带参数[员工id]

2、在add.html页面获取url中的参数[员工id]

3、发送ajax请求,请求服务端,同时提交员工id参数

4、服务端接收请求,根据员工id查询员工信息,将员工信息以json形式响应给页面

5、页面接收服务端响应的json数据,通过VUE的数据绑定进行员工信息回显

6、点击保存按钮,发送ajax请求,将页面中的员工信息以json方式提交给服务端

7、服务端接收员工信息,并进行处理,完成后给页面响应

8、页面接收到服务端响应信息后进行相应处理

@PathVariable注解

通过 @PathVariable 可以将 URL 中占位符参数绑定到控制器处理方法的入参中:URL 中的 {xxx} 占位符可以通过@PathVariable(“xxx“) 绑定到操作方法的入参中

@GetMapping("/{id}")
public R<Employee> getById(@PathVariable Long id){

    log.info("根据id来查询员工信息");

    Employee employee = employeeService.getById(id);

    return R.success(employee);
}

3.分类管理业务开发

☆公共字段自动填充

前面我们已经完成了后台系统的员工管理功能开发,在新增员工时需要设置创建时间、创建人、修改时间、修改人等字段,在编辑员工时需要设置修改时间和修改人等字段,这些字段属于公共字段,也就是很多表中都有这些字段

能不能对于这些公共字段在某个地方统一处理,来简化开发呢?

使用MybatisPlus提供的公共字段自动填充功能

也就是在插入或者更新的时候为指定字段赋予指定的值,使用它的好处就是可以统一对这些字段进行处理,避免了重复代码

实现步骤:

1、在实体类的属性上加入@TableField注解,指定自动填充的策略

2、按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,需要实现MetaObjectHandler接口

使用**@TableField注解**

@TableField(fill = FieldFill.INSERT)

指定填充策略

  • DEFAULT 默认不处理
  • INSERT 插入时填充字段
  • UPDATE 更新时填充字段
  • INSERT_UPDATE 插入和更新时填充字段

元数据对象处理器(填充策略的具体实现)

要实现一个MetaObjectHandler接口

package com.os467.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 {

    /**
     * 插入操作自动填充
     * @param metaObject
     */
    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("公共字段自动填充[insert]...");
        log.info(metaObject+"");

        metaObject.setValue("createTime", LocalDateTime.now());
        metaObject.setValue("updateTime", LocalDateTime.now());
        metaObject.setValue("createUser", new Long(1));
        metaObject.setValue("updateUser", new Long(1));
    }

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

        metaObject.setValue("updateTime", LocalDateTime.now());
        metaObject.setValue("updateUser", new Long(1));

    }
}

注意:当前我们设置createUser和updateUser为固定值,后面我们需要进行改造,改为动态获得当前登录用户的id

功能完善

前面我们已经完成了公共字段自动填充功能的代码开发,但是还有一个问题没有解决

  • 就是我们在自动填充createUser和updateUser时,设置的用户id是固定值,现在我们需要改造成动态获取当前登录用户的id

有的同学可能想到,用户登录成功后我们将用户id存入了HttpSession中,现在我从HttpSession中获取不就行了?

  • 注意:我们在MyMetaobjectHandler类中是不能获得HttpSession对象的,所以我们需要通过其他方式来获取登录用户id
  • 可以使用ThreadLocal来解决此问题,它是JDK中提供的一个类。

ThreadLocal类

在学习ThreadLocal之前,我们需要先确认一个事情,就是客户端发送的每次http请求,对应的在服务端都会分配一个新的线程来处理,在处理过程中涉及到下面类中的方法都属于相同的一个线程:

1、LoginCheckFilter的doFilter方法

2、EmployeeController的update方法

3、MyMetaObjectHandler的updateFill方法

可以在上面的三个方法中分别加入下面代码(获取当前线程id):

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

什么是ThreadLocal?

ThreadLocal并不是一个线程,而是线程的局部变量,当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,而不会影响其它线程所对应的副本,ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问

ThreadLocal常用方法:

public void set(T value) 设置当前线程的线程局部变量的值

public T get() 返回当前线程所对应的线程局部变量的值

我们可以在LoginCheckFilter的doFilter方法中获取当前登录用户id,并调用ThreadLocal的set方法来设置当前线程的线程局部变量的值(用户id),然后在MyMetaObjectHandler的updateFill方法中调用ThreadLocal的get方法来获得当前线程所对应的线程局部变量的值(用户id)。

实现步骤

1、编写BaseContext工具类,基于ThreadLocal封装的工具类

2、在LoginCheckFilter的doFilter方法中调用BaseContext来设置当前登录用户的id

3、在MyMetaObjectHandler的方法中调用BaseContext获取登录用户的id

编写工具类

package com.os467.reggie.common;

/**
 * 基于ThreadLocal封装工具类,用户保存和获取当前登录用户id
 */
public class BaseContext {

    private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    public static void setCurrentId(Long id){
        threadLocal.set(id);
    }

    public static Long getCurrentId(){
        return threadLocal.get();
    }

}

在doFilter方法中获取id并且存储id值

/4、判断登录状态,如果已登录,则直接放行
if(request.getSession().getAttribute("employee") != null){

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

    BaseContext.setCurrentId(empId);

    log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("employee"));
    filterChain.doFilter(request,response);
    return;
}

在MyMetaObjectHandler类中获取需要的id值

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

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

}

3.1新增分类

需求分析

后台系统中可以管理分类信息,分类包括两种类型,分别是菜品分类和套餐分类。当我们在后台系统中添加菜品时需要选择一个菜品分类,当我们在后台系统中添加一个套餐时需要选择一个套餐分类,在移动端也会按照菜品分类和套餐分类来展示对应的菜品和套餐。

/**
 * 新增分类
 * @param category
 * @return
 */
@PostMapping
public R<String> save(@RequestBody Category category){

    log.info("category:{}",category);

    categoryService.save(category);

    return R.success("新增分类成功");
}

分页列表

/**
 * 分类分页
 * @param page
 * @param pageSize
 * @return
 */
@GetMapping("page")
public R<Page> page(int page,int pageSize){

    log.info("page = {},pageSize={},",page,pageSize);

    Page<Category> pageConstructor = new Page<>(page,pageSize);

    //条件构造器
    LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();

    //根据sort字段排序
    queryWrapper.orderByAsc(Category::getSort);

    categoryService.page(pageConstructor,queryWrapper);

    return R.success(pageConstructor);
}

3.2删除分类

删除分类

/**
 * 删除分类
 * @param ids
 * @return
 */
@DeleteMapping
public R<String> delete(Long ids){

    log.info("删除分类,id为{}",ids);

    categoryService.removeById(ids);

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

功能完善

前面我们已经实现了根据id删除分类的功能,但是并没有检查删除的分类是否关联了菜品或者套餐,所以我们需要进行功能完善

要完善分类删除功能,需要准备基础的类和接口:

实体类Dish和Setmeal

Mapper接口

Service实接口

Service实现类

根据条件删除分类,同时判断是否抛出异常

package com.os467.reggie.service.impl;

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

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

    @Autowired
    private SetmealService setmealService;

    @Autowired
    private DishService dishService;

    /**
     * 根据id来删除分类,删除之前进行判断
     * @param id
     */
    @Override
    public void remove(Long id) {

        LambdaQueryWrapper<Dish> queryWrapperDish = new LambdaQueryWrapper<>();
        //添加查询条件,根据分类id进行查询
        queryWrapperDish.eq(Dish::getCategoryId, id);

        //查询当前分类是否关联了菜品
        int count1 = dishService.count(queryWrapperDish);

        if (count1 > 0){

            //已经关联了菜品,抛出一个业务异常
            throw new CustomException("当前分类下关联了菜品,不能删除");

        }

        //查询当前的分类是否关联了相应的套餐
        LambdaQueryWrapper<Setmeal> queryWrapperSetmeal = new LambdaQueryWrapper<>();

        queryWrapperSetmeal.eq(Setmeal::getCategoryId,id);

        int count2 = setmealService.count(queryWrapperSetmeal);

        if (count2 > 0){

            //已经关联了套餐,抛出一个业务异常
            throw new CustomException("当前分类下关联了套餐,不能删除");

        }

        //正常删除分类
        super.removeById(id);

    }
}

自定义的异常

package com.os467.reggie.common;

public class CustomException extends RuntimeException {

    public CustomException(String msg){

        super(msg);

    }

}

在全局异常处理类中进行处理

/**
 * 异常处理方法
 * @return
 */
@ExceptionHandler(CustomException.class)
public R<String> exceptionHandler(CustomException exception){

    log.error(exception.getMessage());

    return R.error(exception.getMessage());
}

3.3修改分类

/**
 * 根据id修改分类信息
 * @param category
 * @return
 */
@PutMapping
public R<String> update(@RequestBody Category category){

    log.info("修改分类信息:{}",category);

    categoryService.updateById(category);

    return R.success("修改分类信息成功");
}

4.菜品管理业务开发

4.1文件上传上传下载

文件上传介绍

文件上传,也称为upload,是指将本地图片、视频、音频等文件上传到服务器上,可以供其它用户浏览或下载的过程,文件上传在项目中应用非常广泛,我们经常发微博、发微信朋友圈都用到了文件上传功能

文件上传时,对页面的form表单有如下要求(固定要求):

  • method="post" 采用post方式提交数据
  • enctype="multipart/form-data" 采用multipart格式上传文件
  • type="file" 使用input的file控件上传

目前一些前端组件库也提供了相应的上传组件,但是底层原理还是基于form表单的文件上传

例如ElementUI中提供的upload上传组件

服务端要接收客户端页面上传的文件,通常都会使用Apache的两个组件

  • commons-fileupload
  • commons-io

Spring框架在spring-web包中对文件上传进行了封装,大大简化了服务端代码,我们只需要在Controller的方法中声明一个MultipartFile类型的参数即可接收上传的文件,例如:

文件下载介绍

文件下载,也称为download,是指将文件从服务器传输到本地计算机的过程

通过浏览器进行文件下载,通常有两种表现形式:

  • 以附件形式下载,弹出保存对话框,将文件保存到指定磁盘目录
  • 直接在浏览器中打开

通过浏览器进行文件下载,本质上就是服务端将文件以流的形式写回浏览器的过程

文件上传代码实现

MultipartFile类:用于接收文件类型数据

创建一个CommonController来处理平常一些的请求

测试是否能接收到文件

package com.os467.reggie.controller;

import com.os467.reggie.common.R;
import lombok.extern.slf4j.Slf4j;
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;

/**
 * 文件的上传和下载
 */
@RestController
@RequestMapping("/common")
@Slf4j
public class CommonController {

    /**
     * 文件上传
     * @param file 参数名必须和前端name一致
     * @return
     */
    @PostMapping("/upload")
    public R<String> upload(MultipartFile file){
        log.info(file.toString());
        return null;
    }

}

在配置文件中指定文件存放路径

reggie.iconPath=D:\\pythonschool\\PY_lab\\JavaProject\\reggie_take_out\\src\\main\\resources\\localIcon

在表现层通过Value注解获取

@Value("${reggie.iconPath}")
private String basePath;

文件上传

/**
 * 文件上传
 * @param file 参数名必须和前端name一致
 * @return
 */
@PostMapping("/upload")
public R<String> upload(  MultipartFile file){
    //file是一个临时文件,需要转存到指定位置,否则本子请求完成后临时文件就会删除
    log.info(file.toString());

    //获取原始文件名
    String originalFilename = file.getOriginalFilename();

    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);
}

文件下载代码实现

getOutputStream()

该方法所获得的的字节流对象为ServletOutputStream类型, 由于ServletOutputStream是OutputStream的子类,它可以直接输出字节组中的二进制数据

/**
 * 文件下载
 * @param name
 * @param response
 */
@GetMapping("/download")
public void download(String name, HttpServletResponse response){

    FileInputStream fileInputStream = null;

    ServletOutputStream outputStream = null;

    try {

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

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

        byte[] bytes = new byte[1024];

        //设置响应文件类型
        response.setContentType("image/jpeg");

        int read = 0;

        while ((read = fileInputStream.read(bytes)) != -1){

            outputStream.write(bytes,0,read);
            outputStream.flush();
        }


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

        try {
            if (fileInputStream != null){
                fileInputStream.close();
            }

            if (outputStream != null){
                outputStream.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }


}

4.2新增菜品

新增菜品,其实就是将新增页面录入的菜品信息插入到dish表

如果添加了口味做法,还需要向dish_flavor表插入数据

所以在新增菜品时,涉及到两个表:

  • dish 菜品表
  • dish_flavor 菜品口味表

交互过程

1、页面(backend/page/food/add.html)发送ajax请求,请求服务端获取菜品分类数据并展示到下拉框中

2、页面发送请求进行图片上传,请求服务端将图片保存到服务器

3、页面发送请求进行图片下载,将上传的图片进行回显

4、点击保存按钮,发送ajax请求,将菜品相关数据以json形式提交到服务端

4.2.1回显分类数据

/**
 * 根据条件来回显分类数据
 * @param category
 * @return
 */
@GetMapping("list")
public R<List<Category>> list(Category category){

    //条件构造器
    LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();

    //添加条件
    queryWrapper.eq(category.getType() != null,Category::getType,category.getType());

    //添加排序条件
    queryWrapper.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime);

    //查询
    List<Category> list = categoryService.list(queryWrapper);

    return R.success(list);
}

导入DishDto,用于封装页面提交的数据

DTO,全称为Data Transfer Object,即数据传输对象,一般用于展示与服务层之间的数据传输

Stream流的特性

相比forEach效率高,数据多会启用多线程

  • Stream流不是一种数据结构,不保存数据,它只是在原数据集上定义了一组操作

  • 这些操作是惰性的,即每当访问到流中的一个元素,才会在此元素上执行这一系列操作

  • Stream不保存数据,故每个Stream流只能使用一次

  • 关于应用在Stream流上的操作,可以分成两种:Intermediate(中间操作)和Terminal(终止操作)

  • 中间操作的返回结果都是Stream,故可以多个中间操作叠加

  • 终止操作用于返回我们最终需要的数据,只能有一个终止操作

Intermediate(中间操作):map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 skip、 parallel、 sequential、 unordered

Terminal(终止操作):forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、iterator

Short-circuiting(短路):anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit

开启事务

需要在启动类上添加注解

@EnableTransactionManagement

在新增菜品的方法上开启事务

@Transactional

4.2.2新增菜品

/**
 * 新增菜品
 * @param dishDto
 * @return
 */
@PostMapping
public R<String> save(@RequestBody DishDto dishDto){

    log.info("dishDto:{}",dishDto);

    dishService.saveWithFlavor(dishDto);

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

DishServiceImpl

/**
 * 新增菜品,同时保存对应的口味数据
 * @param dishDto
 */
@Override
@Transactional
public void saveWithFlavor(DishDto dishDto) {
    //保存菜品的基本信息到菜品表dish
    this.save(dishDto);

    Long id = dishDto.getId();

    //菜品口味
    List<DishFlavor> flavors = dishDto.getFlavors();

    //使用stream流遍历
    flavors = flavors.stream().map((item) ->{
       item.setDishId(id);
       return item;
    }).collect(Collectors.toList());

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

}

4.3菜品信息分页查询

再查询菜品信息的同时要查询到菜品分类名

我们使用DishDto来构造分页,通过拷贝工具将查询好的菜品信息复制给DishDto模型

spring提供的BeansUtil有拷贝功能(底层是反射)

/**
 * 菜品分页
 * @param page
 * @param pageSize
 * @return
 */
@GetMapping("page")
public R<Page> page(int page, int pageSize,String name){

    log.info("page = {},pageSize={},",page,pageSize);

    log.info("name:{}",name);

    //菜品分页构造器
    Page<Dish> dishPage = new Page<>(page,pageSize);

    //模型分页构造器
    Page<DishDto> dishDtoPage = new Page<>(page,pageSize);

    //条件构造器
    LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();

    //查询条件
    queryWrapper.like(StringUtils.isNotBlank(name),Dish::getName,name);

    //根据sort字段排序
    queryWrapper.orderByAsc(Dish::getSort);

    dishService.page(dishPage,queryWrapper);

    //对象拷贝
    BeanUtils.copyProperties(dishPage,dishDtoPage,"records");

    //获取菜品分页构造器内部查询到的菜品列表信息
    List<Dish> dishList = dishPage.getRecords();

    //创建模型列表
    List<DishDto> list = null;

    //遍历菜品数据流,修改成我们想要的泛型
    list = dishList.stream().map((item) -> {

        //创建模型对象
        DishDto dishDto = new DishDto();

        //将菜品对象信息拷贝到模型对象
        BeanUtils.copyProperties(item, dishDto);

        //获取到分类id
        Long categoryId = item.getCategoryId();

        //查询分类信息
        Category category = categoryService.getById(categoryId);

        //获取分类名
        String categoryName = category.getName();

        //为模型赋值分类名
        dishDto.setCategoryName(categoryName);

        return dishDto;

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

    //为模型分页赋值
    dishDtoPage.setRecords(list);

    return R.success(dishDtoPage);
}

4.4修改菜品

回显菜品信息以及对应口味信息

代码实现

/**
 * 根据id来查询菜品信息和对应的口味信息
 * @param id
 * @return
 */
@GetMapping("/{id}")
public R<DishDto> page(@PathVariable("id") Long id){

    DishDto dishDto = dishService.getByIdWithFlavor(id);

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

    DishDto dishDto = new DishDto();

    BeanUtils.copyProperties(dish,dishDto);

    //查询菜品对应的口味信息
    LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();

    queryWrapper.eq(DishFlavor::getDishId,dish.getId());

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

    dishDto.setFlavors(flavors);

    return dishDto;
}

提交修改

/**
 * 修改菜品信息
 * @return
 */
@PutMapping
public R<String> update(@RequestBody DishDto dishDto){

    log.info("dishDto:{}",dishDto);

    dishService.updateWithFlavor(dishDto);

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

}
/**
 * 修改菜品信息并且更新对应口味信息
 * @param dishDto
 */
@Override
@Transactional
public void updateWithFlavor(DishDto dishDto) {

    //更新菜品表信息
    this.updateById(dishDto);

    //清理当前菜品对应的口味数据---dish_flavor表的delete操作
    LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper();

    queryWrapper.eq(DishFlavor::getDishId,dishDto.getId());

    dishFlavorService.remove(queryWrapper);

    List<DishFlavor> flavors = dishDto.getFlavors();

    //使用stream流遍历
    flavors = flavors.stream().map((item) ->{
        item.setDishId(dishDto.getId());
        return item;
    }).collect(Collectors.toList());

    //添加当前提交过来的口味数据
    dishFlavorService.saveBatch(flavors);

}

5.套餐部分(独立完成)

package com.os467.reggie.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.os467.reggie.common.R;
import com.os467.reggie.dto.SetmealDto;
import com.os467.reggie.entity.Category;
import com.os467.reggie.entity.Dish;
import com.os467.reggie.entity.Setmeal;
import com.os467.reggie.entity.SetmealDish;
import com.os467.reggie.service.CategoryService;
import com.os467.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 CategoryService categoryService;

    /**
     * 获取套餐列表,并且显示套餐分类
     * @param page
     * @param pageSize
     * @param name
     * @return
     */
    @GetMapping("/page")
    public R<Page<SetmealDto>> page(Integer page,Integer pageSize,String name){

        //创建分页构造器
        Page<Setmeal> setmealPage = new Page<>(page,pageSize);

        LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();

        //根据条件模糊查询
        queryWrapper.like(StringUtils.isNotBlank(name),Setmeal::getName,name);

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

        //创建SetmealDto分页构造器
        Page<SetmealDto> setmealDtoPage = new Page<SetmealDto>();

        //拷贝分页构造器
        BeanUtils.copyProperties(setmealPage,setmealDtoPage,"records");

        //获取套餐记录
        List<Setmeal> setmealList = setmealPage.getRecords();

        //获取Dto格式的套餐信息
        List<SetmealDto> setmealDtoList = setmealList.stream().map((item) -> {

            SetmealDto setmealDto = new SetmealDto();

            //获取套餐id
            Long categoryId = item.getCategoryId();

            //获取该套餐信息
            Category category = categoryService.getById(categoryId);

            //拷贝套餐信息
            BeanUtils.copyProperties(item, setmealDto);

            //设置套餐名称
            setmealDto.setCategoryName(category.getName());

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

        //为分页构造器添加信息
        setmealDtoPage.setRecords(setmealDtoList);

        return R.success(setmealDtoPage);
    }

    /**
     * 添加套餐并且保存套餐对应菜品信息
     * @param setmealDto
     * @return
     */
    @PostMapping
    public R<String> save(@RequestBody SetmealDto setmealDto){

        //添加套餐
        setmealService.saveSetmealWithDish(setmealDto);

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

    /**
     * 删除套餐,并且删除套餐对应的菜品
     * @param ids
     * @return
     */
    @DeleteMapping
    public R<String> delete(String ids){

        setmealService.deleteSetmealWithDish(ids);

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

    @GetMapping("/{setmealId}")
    public R<SetmealDto> getSetmeal(@PathVariable("setmealId") Long setmealId){

        SetmealDto setmealDto = setmealService.getSetmeal(setmealId);

        return R.success(setmealDto);
    }

    /**
     * 起售、停售菜品
     * @param flag
     * @param ids
     * @return
     */
    @PostMapping("/status/{flag}")
    public R<String> stopSale(@PathVariable("flag") Integer flag,String ids){

        LambdaUpdateWrapper<Setmeal> updateWrapper = new LambdaUpdateWrapper<>();

        String[] idList = ids.split(",");

        updateWrapper.in(Setmeal::getId,idList);

        updateWrapper.set(Setmeal::getStatus,flag);

        setmealService.update(updateWrapper);

        return R.success("状态更新成功");
    }


    /**
     * 更新套餐数据
     * @param setmealDto
     * @return
     */
    @PutMapping
    public R<String> update(@RequestBody SetmealDto setmealDto){

        log.info("setmealDto:{}",setmealDto);

        setmealService.updateWithDish(setmealDto);

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

}
package com.os467.reggie.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.os467.reggie.dto.SetmealDto;
import com.os467.reggie.entity.Dish;
import com.os467.reggie.entity.Setmeal;
import com.os467.reggie.entity.SetmealDish;
import com.os467.reggie.mapper.SetmealMapper;
import com.os467.reggie.service.CategoryService;
import com.os467.reggie.service.DishService;
import com.os467.reggie.service.SetmealDishService;
import com.os467.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 java.util.List;
import java.util.stream.Collectors;

@Service
public class SetmealServiceImpl extends ServiceImpl<SetmealMapper, Setmeal> implements SetmealService {

    @Autowired
    private SetmealDishService setmealDishService;

    @Autowired
    private CategoryService categoryService;


    /**
     * 设置套餐,并且保存套餐菜品信息
     * @param setmealDto
     */
    @Override
    public void saveSetmealWithDish(SetmealDto setmealDto) {

        //获取套餐菜品关系列表
        List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();

        //保存套餐信息
        this.save(setmealDto);

        //获取套餐id
        Long setmealId = setmealDto.getId();

        //设置套餐id
        List<SetmealDish> setmealDishList = setmealDishes.stream().map((item) -> {

            item.setSetmealId(setmealId);

            return item;

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

        //保存套餐菜品信息
        setmealDishService.saveBatch(setmealDishList);

    }

    /**
     * 删除套餐,以及对应的菜品
     * @param ids
     */
    @Override
    public void deleteSetmealWithDish(String ids) {

        //获取需要删除的套餐id列表
        String[] idList = ids.split(",");

        LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();

        //需要删除id列表包含的菜套餐
        queryWrapper.in(Setmeal::getId,idList);

        //删除套餐
        this.remove(queryWrapper);

        //删除套餐对应的菜品
        LambdaQueryWrapper<SetmealDish> queryWrapperDish = new LambdaQueryWrapper<>();

        queryWrapperDish.in(SetmealDish::getSetmealId,idList);

        //删除套餐对应菜品
        setmealDishService.remove(queryWrapperDish);

    }

    /**
     * 回显套餐信息
     * @param setmealId
     */
    @Override
    public SetmealDto getSetmeal(Long setmealId) {

        //根据id查询出套餐信息
        Setmeal setmeal = this.getById(setmealId);

        Long categoryId = setmeal.getCategoryId();

        //获取套餐分类名
        String categoryName = categoryService.getById(categoryId).getName();

        //创建Dto模型
        SetmealDto setmealDto = new SetmealDto();

        //拷贝套餐信息
        BeanUtils.copyProperties(setmeal,setmealDto);

        //获取套餐对应菜品列表
        LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>();

        //查询条件
        queryWrapper.eq(SetmealDish::getSetmealId,setmealId);

        //查询出符合条件的套餐菜品表
        List<SetmealDish> setmealDishList = setmealDishService.list(queryWrapper);

        //存放套餐菜品列表
        setmealDto.setSetmealDishes(setmealDishList);

        //存放套餐名
        setmealDto.setCategoryName(categoryName);

        return setmealDto;
    }

    /**
     * 更新套餐信息以及套餐菜品
     * @param setmealDto
     */
    @Override
    public void updateWithDish(SetmealDto setmealDto) {

        this.updateById(setmealDto);

        Long setmealId = setmealDto.getId();

        LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>();

        queryWrapper.eq(SetmealDish::getSetmealId,setmealId);

        //清除相关菜品信息
        setmealDishService.remove(queryWrapper);

        //重新写入套餐id
        List<SetmealDish> setmealDishList = setmealDto.getSetmealDishes().stream().map((item) -> {

            item.setSetmealId(setmealId);

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

        //重新写入菜品信息
        setmealDishService.saveBatch(setmealDishList);

    }
}

6.手机验证码

6.1短信发送

短信服务介绍

目前市面上有很多第三方提供的短信服务,这些第三方短信服务会和各个运营商(移动、联通、电信)对接,我们只要注册成为会员并且按照提供的开发文档进行调用就可以发送短信,需要说明的是,这些短信服务一般都是收费服务

常用短信服务:

  • 阿里云
  • 华为云
  • 腾讯云
  • 京东
  • 梦网
  • 乐信

阿里云短信服务-介绍

阿里云短信服务(Short Message Service)是广大企业客户快速触达手机用户所优选使用的通信能力。调用API或用群发助手,即可发送验证码、通知类和营销类短信;国内验证短信秒级触达,到达率最高可达99%;国际/港澳台短信覆盖200多个国家和地区,安全稳定,广受出海企业选用。

应用场景:

  • 验证码
  • 短信通知
  • 推广短信

使用阿里云短信服务发送短信,可以参照官方提供的文档即可

具体开发步骤:

1、导入maven坐标

2、调用API

在开发代码之前,需要梳理一下登录时前端页面和服务端的交互过程:

1、在登录页面(front/page/login.html)输入手机号,点击【获取验证码】按钮,页面发送ajax请求,在服务端调用短信服务API给指定手机号发送验证码短信

2、在登录页面输入验证码,点击【登录】按钮,发送ajax请求,在服务端处理登录请求开发手机验证码登录功能,其实就是在服务端编

写代码去处理前端页面发送的这2次请求即可。

添加不需要处理的路径

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

判断移动端登录状态

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

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

    BaseContext.setCurrentId(userId);

    log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("user"));
    filterChain.doFilter(request,response);
    return;
}

手机验证码发送

/**
 * 发送手机短信验证码
 * @param user
 * @return
 */
@PostMapping("/sendMsg")
public R<String> sendMsg(@RequestBody User user, HttpSession session){
    //获取手机号
    String phone = user.getPhone();

    if (StringUtils.isNotBlank(phone)){

        //生成随机的四位的验证码
        String code = ValidateCodeUtils.generateValidateCode(4).toString();
        log.info("code={}",code);

        //调用阿里云提供的短信服务API提供短信服务
        //SMSUtils.sendMessage("短信验证码","",phone,code);

        //需要将生成的验证码保存到Session中
        session.setAttribute(phone,code);

        return R.success("手机验证码短信发送成功");

    }

    return R.error("手机验证码短信发送失败");

}

移动端登录验证

/**
 * 移动端用户登录
 * @param userInfo
 * @return
 */
@PostMapping("/login")
public R<User> login(@RequestBody Map userInfo, HttpSession session){

    log.info("Map:{}",userInfo);

    //获取手机号
    String phone = userInfo.get("phone").toString();

    //获取验证码
    String code = userInfo.get("code").toString();

    //从Session中获取保存的验证码
    Object codeInSession = session.getAttribute("code");

    log.info("前端:{},后端:{}",code,codeInSession);

    //验证码的比对(页面提交的验证码和Session中保存的验证码比对)
    if (codeInSession != null && codeInSession.equals(code)){
        //能够比对成功,则说明登录成功

        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();

        queryWrapper.eq(User::getPhone,phone);

        User user = userService.getOne(queryWrapper);

        //判断当前手机号对应的用户是否为新用户,如果是新用户就自动完成注册
        if (user == null){

            log.info("新用户注册");

            user = new User();

            user.setPhone(phone);

            userService.save(user);
        }

        session.setAttribute("user",user.getId());

        return R.success(user);

    }

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

6.2导入用户地址簿相关功能代码

package com.os467.reggie.controller;

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

import java.util.List;

/**
 * 地址簿管理
 */
@Slf4j
@RestController
@RequestMapping("/addressBook")
public class AddressBookController {

    @Autowired
    private AddressBookService addressBookService;

    /**
     * 新增
     */
    @PostMapping
    public R<AddressBook> save(@RequestBody AddressBook addressBook) {
        addressBook.setUserId(BaseContext.getCurrentId());
        log.info("addressBook:{}", addressBook);
        addressBookService.save(addressBook);
        return R.success(addressBook);
    }

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

        addressBook.setIsDefault(1);
        //SQL:update address_book set is_default = 1 where id = ?
        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<AddressBook> getDefault() {
        LambdaQueryWrapper<AddressBook> 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>> list(AddressBook addressBook) {
        addressBook.setUserId(BaseContext.getCurrentId());
        log.info("addressBook:{}", addressBook);

        //条件构造器
        LambdaQueryWrapper<AddressBook> 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));
    }

    /**
     * 删除地址
     * @param ids
     * @return
     */
    @DeleteMapping
    public R<String> deleteById(String ids){

        if (ids != null){

            addressBookService.removeById(ids);

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

        }

        return R.error("删除失败");

    }

    @PutMapping
    public R<String> update(@RequestBody AddressBook addressBook){

        //如果存在地址簿信息则修改
        if (addressBook != null){

            addressBookService.updateById(addressBook);

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

        return R.error("修改失败");
    }

}

6.3订单功能

package com.os467.reggie.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.os467.reggie.common.R;
import com.os467.reggie.dto.OrdersDto;
import com.os467.reggie.entity.*;
import com.os467.reggie.service.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpSession;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;

@RestController
@Slf4j
@RequestMapping("/order")
public class OrdersController {

    @Autowired
    private OrdersService ordersService;

    @Autowired
    private AddressBookService addressBookService;

    @Autowired
    private ShoppingCartService shoppingCartService;

    @Autowired
    private OrderDetailService orderDetailService;

    @Autowired
    private UserService userService;


    /**
     * 回显用户信息
     * @param page
     * @param pageSize
     * @return
     */
    @GetMapping("/userPage")
    public R<Page<User>> userPage(Integer page, Integer pageSize, HttpSession session){

        //创建分页构造器
        Page<User> userPage = new Page<>(page,pageSize);

        Long userId = (Long)session.getAttribute("user");

        LambdaQueryWrapper<Orders> ordersLambdaQueryWrapper = new LambdaQueryWrapper<>();

        return R.success(userPage);
    }

    /**
     * 回显用户信息
     * @param page
     * @param pageSize
     * @return
     */
    @GetMapping("/page")
    public R<Page<OrdersDto>> page(Integer page, Integer pageSize, HttpSession session){

        //创建分页构造器
        Page<Orders> orderPage = new Page<>(page,pageSize);

        //创建分页构造器
        Page<OrdersDto> orderDtoPage = new Page<>(page,pageSize);

        Long userId = (Long)session.getAttribute("user");

        LambdaQueryWrapper<Orders> ordersLambdaQueryWrapper = new LambdaQueryWrapper<>();

        ordersLambdaQueryWrapper.orderByDesc(Orders::getOrderTime);

        ordersService.page(orderPage,ordersLambdaQueryWrapper);

        //拷贝分页结构
        BeanUtils.copyProperties(orderPage,orderDtoPage,"records");

        List<OrdersDto> ordersDtoList = orderPage.getRecords().stream().map((item) -> {

            OrdersDto ordersDto = new OrdersDto();

            BeanUtils.copyProperties(item, ordersDto);

            //获取订单id
            Long ordersId = ordersDto.getId();

            LambdaQueryWrapper<OrderDetail> orderDetailLambdaQueryWrapper = new LambdaQueryWrapper<>();

            //与订单匹配的订单明细
            orderDetailLambdaQueryWrapper.eq(OrderDetail::getOrderId, ordersId);

            //获取订单明细列表
            List<OrderDetail> orderDetailList = orderDetailService.list(orderDetailLambdaQueryWrapper);

            ordersDto.setOrderDetails(orderDetailList);

            return ordersDto;

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

        //设置分页内数据
        orderDtoPage.setRecords(ordersDtoList);

        return R.success(orderDtoPage);
    }

    /**
     * 提交订单,清空购物车
     * @return
     */
    @PostMapping("/submit")
    public R<String> submit(@RequestBody Orders orders,HttpSession session){

        AddressBook addressBook = addressBookService.getById(orders.getAddressBookId());

        //获取当前用户id
        Long userId = (Long)session.getAttribute("user");

        LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();

        //根据用户id查询购物车
        queryWrapper.eq(ShoppingCart::getUserId,userId);

        //获取购物车金额
        List<ShoppingCart> shoppingCartList = shoppingCartService.list(queryWrapper);

        //累计金额
        BigDecimal amountSum = new BigDecimal(0);

        for (ShoppingCart shoppingCart : shoppingCartList) {

            amountSum.add(shoppingCart.getAmount().multiply(new BigDecimal(shoppingCart.getNumber())));
        }

        //设置时间
        orders.setOrderTime(LocalDateTime.now());
        orders.setCheckoutTime(LocalDateTime.now());
        //设置总金额
        orders.setAmount(amountSum);
        //设置用户id
        orders.setUserId(userId);
        orders.setConsignee(addressBook.getConsignee()+(addressBook.getSex().equals("1")?"先生":"女士"));
        orders.setPhone(addressBook.getPhone());
        orders.setAddress(addressBook.getDetail());

        //获取用户信息
        User user = userService.getById(userId);

        orders.setUserName(user.getName());

        int count = ordersService.count();

        if (count == 0){

            orders.setNumber("1");

        }else {

            LambdaQueryWrapper<Orders> ordersLambdaQueryWrapper = new LambdaQueryWrapper<>();

            ordersLambdaQueryWrapper.orderByDesc(Orders::getNumber);

            ordersLambdaQueryWrapper.last("limit 1");

            Orders one = ordersService.getOne(ordersLambdaQueryWrapper);

            //生成订单号
            orders.setNumber(Integer.parseInt(one.getNumber())+1+"");

        }

        ordersService.save(orders);

        List<OrderDetail> orderDetailList = new ArrayList<>();

        //保存订单明细
        for (ShoppingCart shoppingCart : shoppingCartList) {

            OrderDetail orderDetail = new OrderDetail();

            //保存订单id
            orderDetail.setOrderId(orders.getId());

            //保存订单明细数据
            orderDetail.setAmount(shoppingCart.getAmount());
            orderDetail.setNumber(shoppingCart.getNumber());
            orderDetail.setDishId(shoppingCart.getDishId());
            orderDetail.setSetmealId(shoppingCart.getSetmealId());
            orderDetail.setDishFlavor(shoppingCart.getDishFlavor());
            orderDetail.setImage(shoppingCart.getImage());
            orderDetail.setName(shoppingCart.getName());

            orderDetailList.add(orderDetail);

        }

        //保存订单明细
        orderDetailService.saveBatch(orderDetailList);

        //清除购物车记录
        LambdaQueryWrapper<ShoppingCart> shoppingCartLambdaQueryWrapper = new LambdaQueryWrapper<>();

        shoppingCartLambdaQueryWrapper.eq(ShoppingCart::getUserId,userId);

        shoppingCartService.remove(shoppingCartLambdaQueryWrapper);

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

}

6.4购物车功能

package com.os467.reggie.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.os467.reggie.common.R;
import com.os467.reggie.entity.DishFlavor;
import com.os467.reggie.entity.Setmeal;
import com.os467.reggie.entity.ShoppingCart;
import com.os467.reggie.service.DishFlavorService;
import com.os467.reggie.service.SetmealService;
import com.os467.reggie.service.ShoppingCartService;
import com.sun.org.apache.regexp.internal.RE;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpSession;
import java.util.List;

@RestController
@Slf4j
@RequestMapping("/shoppingCart")
public class ShoppingCartController {

    @Autowired
    private ShoppingCartService shoppingCartService;

    @Autowired
    private DishFlavorService dishFlavorService;

    /**
     * 向购物车中添加数据
     * @param shoppingCart
     * @param session
     * @return
     */
    @PostMapping("/add")
    public R<String> add(@RequestBody ShoppingCart shoppingCart, HttpSession session){

        log.info("ShoppingCart:{}",shoppingCart);

        Long userId = (Long)session.getAttribute("user");

        //向购物车中添加用户id
        shoppingCart.setUserId(userId);

        LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();

        //检查购物车里是否有相同的菜品或套餐
        queryWrapper.eq(ShoppingCart::getUserId,userId).
                eq(ShoppingCart::getDishId,shoppingCart.getDishId()).or().
                eq(ShoppingCart::getSetmealId,shoppingCart.getSetmealId());

        ShoppingCart one = shoppingCartService.getOne(queryWrapper);

        if (one != null && one.getNumber() >= 1){

            //更新数量
            one.setNumber(one.getNumber()+1);
            //更新口味
            one.setDishFlavor(shoppingCart.getDishFlavor());

            shoppingCartService.updateById(one);

            return R.success("添加成功");

        }else {

            return shoppingCartService.save(shoppingCart)?R.success("添加成功"):R.error("添加失败");

        }

    }

    /**
     * 根据用户id获取购物车列表
     * @return
     */
    @GetMapping("/list")
    public R<List<ShoppingCart>> list(HttpSession session){

        //获取用户id
        Long userId = (Long)session.getAttribute("user");

        LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();

        //查询该用户的购物车
        queryWrapper.eq(ShoppingCart::getUserId, userId);

        List<ShoppingCart> shoppingCartList = shoppingCartService.list(queryWrapper);

        if (shoppingCartList != null){

            return R.success(shoppingCartList);

        }

        return R.error("未找到数据");
    }

    /**
     * 根据id删除该用户购物车中的内容
     * @param shoppingCart
     * @return
     */
    @PostMapping("/sub")
    public R<String> sub(@RequestBody ShoppingCart shoppingCart,HttpSession session){

        //获取当前用户id
        Long userId = (Long)session.getAttribute("user");

        LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();

        queryWrapper.eq(shoppingCart.getDishId() != null,ShoppingCart::getDishId,shoppingCart.getDishId());
        queryWrapper.eq(shoppingCart.getSetmealId() != null,ShoppingCart::getSetmealId,shoppingCart.getSetmealId());
        queryWrapper.eq(userId != null,ShoppingCart::getUserId,userId);

        ShoppingCart one = shoppingCartService.getOne(queryWrapper);

        if (one.getNumber() == 1){
            shoppingCartService.removeById(one.getId());
        }else {

            //数量减一
            one.setNumber(one.getNumber()-1);

            shoppingCartService.updateById(one);

        }

        return R.success("更新成功");

    }

    /**
     * 根据当前用户id清空购物车
     * @param session
     * @return
     */
    @DeleteMapping("/clean")
    public R<String> cleanShoppingCart(HttpSession session){

        //获取当前用户id
        Long userId = (Long)session.getAttribute("user");

        LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();

        queryWrapper.eq(userId != null,ShoppingCart::getUserId,userId);

        return shoppingCartService.remove(queryWrapper)?R.success("清空成功"):R.error("清空失败");
    }

}

6.5地址簿功能

package com.os467.reggie.controller;

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

import java.util.List;

/**
 * 地址簿管理
 */
@Slf4j
@RestController
@RequestMapping("/addressBook")
public class AddressBookController {

    @Autowired
    private AddressBookService addressBookService;

    /**
     * 新增
     */
    @PostMapping
    public R<AddressBook> save(@RequestBody AddressBook addressBook) {
        addressBook.setUserId(BaseContext.getCurrentId());
        log.info("addressBook:{}", addressBook);
        addressBookService.save(addressBook);
        return R.success(addressBook);
    }

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

        addressBook.setIsDefault(1);
        //SQL:update address_book set is_default = 1 where id = ?
        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<AddressBook> getDefault() {
        LambdaQueryWrapper<AddressBook> 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>> list(AddressBook addressBook) {
        addressBook.setUserId(BaseContext.getCurrentId());
        log.info("addressBook:{}", addressBook);

        //条件构造器
        LambdaQueryWrapper<AddressBook> 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));
    }

    /**
     * 删除地址
     * @param ids
     * @return
     */
    @DeleteMapping
    public R<String> deleteById(String ids){

        if (ids != null){

            addressBookService.removeById(ids);

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

        }

        return R.error("删除失败");

    }

    @PutMapping
    public R<String> update(@RequestBody AddressBook addressBook){

        //如果存在地址簿信息则修改
        if (addressBook != null){

            addressBookService.updateById(addressBook);

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

        return R.error("修改失败");
    }

}

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以邮件至 1300452403@qq.com

文章标题:外卖项目

字数:15.5k

本文作者:Os467

发布时间:2022-08-03, 08:35:11

最后更新:2022-09-05, 00:06:43

原始链接:https://os467.github.io/2022/08/03/%E5%A4%96%E5%8D%96%E9%A1%B9%E7%9B%AE/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

×

喜欢就点赞,疼爱就打赏