学习资料

✧ Spring Boot 官方文档:https://docs.spring.io/spring-boot/docs/current/reference/html/

✧ 视频地址:黑马程序员Java项目实战《瑞吉外卖》,轻松掌握springboot + mybatis plus开发核心技术的真java实战项目_哔哩哔哩_bilibili

项目中的资料下载链接:(从黑马公众号获取到的最初状态的源码

        因为该项目简单易上手,比较合适新手作为第一个实战项目。所以为了尽可能的规范,该篇的代码基本上与视频导师中命名与开发代码一致。尽可能的规范代码。

        该篇只是到实现了后台管理功能与移动端的功能的需求。基本上(除QQ邮箱验证登录)是黑马程序员吉瑞外卖上课的笔记。

这里只分享该篇文章需要用到的资料代码

链接:https://pan.baidu.com/s/1uQKb8Wt3MRU204pBJsU1XQ 
提取码:6660

后期会将我所作的笔记思维导图分享给大家以便大家复习。

目录

学习资料

01-项目背景介绍

产品原型展示

角色即功能

02-软件开发流程

软件开发流程

角色分工

03-后台管理与移动端基础开发

基础配置

创建MAVEN工程

后端系统

登录功能

创建项目架构与基本类

退出登录功能

新增员工

员工信息分页功能

 启用/禁用员工账号

对象转换器JacksonObjectMapper

员工信息编辑

公共字段自动填充

新增分类

菜品分类管理分页查询

菜品分类删除

菜品分类修改

文件的上传下载

新增菜品

菜品管理分页查询

修改菜品

套餐管理

客户端系统

 QQ邮箱验证登录

导入用户地址簿

菜品展示

购物车

用户下单

项目优化


01-项目背景介绍

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

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

第一期主要实现基本需求,其中移动端应用通过H5实现,用户可以通过手机浏览器访问。

第二期主要针对移动端应用进行改进,使用微信小程序实现,用户使用起来更加方便。

第三期主要针对系统进行优化升级,提高系统的访问性能。

产品原型展示

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

课程资料中已经提供了产品原型:

产品原型只是指项目有这个功能,而不是最终的项目效果。本项目可以在资料-->产品原型  中查看

技术选型

功能结构

角色即功能

        后台系统管理员:登录后台管理系统、拥有后台系统中的所有操作权限

        后台系统普通员工:登录后台管理系统,对菜品、套餐、订单等进行管理

        C端用户:登录移动端应用,可以浏览菜品、添加购物车、设置地址、在线下单

02-软件开发流程

软件开发流程

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

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

        编码:项目代码、单元测试

        测试:测试用例、测试报告

        上线运难:软件环境安装、配置

角色分工

        项目经理:对整个项目负责,任务分配、把控进度

        产品经理:进行需求调研,输出需求调研文档、产品原型等

        UI设计师:根据产品原型输出界面效果图

        架构师:项目整体架构设计、技术选型等

        开发工程师:代码实现

        测试工程师:编写测试用例,输出测试报告

        运维工程师:软件环境搭建、项目上线

03-后台管理与移动端基础开发

基础配置

链接MySQL本地数据库:

MySQL -hlocalhost -u账号 -p密码

创建数据库reggie字符为utf8mb4:

mysql> create database reggie character set utf8mb4;

运行sql文件在1 瑞吉外卖项目-->资料-->数据模型-->db_reggie.sql;

使用客户端图形化界面运行或者使用命令行运行(命令行如下):

mysql> source {路径}

创建MAVEN工程

        更换个人的MAVEN、检查JDK1.8(Setting-->Build-->Build Tools-->Maven-->Runner、Project Settings-->project--> SDK)

<?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>

    <groupId>com.lxxd.reggie</groupId>
    <artifactId>reggie_lxxd</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <java.version>1.8</java.version>
    </properties>
    <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>

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

    </dependencies>

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

创建application.yml文件导入以下配置

server:
  port: 8080
spring:
  application:
    # 应用的名称
    name: reggie_take_out
  datasource:
    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: ${username}
      password: ${password}
mybatis-plus:
  configuration:
    #在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      id-type: ASSIGN_ID

编写启动Main类

@SpringBootApplication
@Slf4j //开启日志
@ServletComponentScan
public class Main {
    public static void main(String[] args) {
        SpringApplication.run(Main.class,args);
        log.info("项目启动成功");
    }
}

即可启动成功!!!

打开项目资料中的前端资源文件backend、front导入到resources下如图:

        默认情况下resources映射static、templates其中static中放静态页面,而templates中放动态页面。未放该文件夹下,所以需要添加文件的映射;

config.WebMvcConfig

@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
    /**
     * 设置静态资源映射
     * **/
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry){
        log.info("开启静态资源映射.....");
        registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
        registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
    }
}
 

        在启动类中加入@ServletComponentScan使配置类生效

        即可访问:http://127.0.0.1:8080/backend/page/login/login.html 访问到该网址,项目环境搭建完成。

后端系统

登录功能

登录功能网站:http://localhost:8080/backend/page/login/login.html

对应后台管理员表:employee

    登录功能处理逻辑如下:
     1、将页面提交的密码password进行 MD5 加密处理
     2、根据页面提交的用户名username查询数据库
     3、如果没有查询到数据,则返回登录失败的结果
     4、进行密码比对,如果不一致,则返回登录失败的结果
     5、查看员工状态,如果为 已禁用状态,则返回被禁用的结果信息
     6、登录成功,将员工id 存入Session并返回登录成功的结果

创建项目架构与基本类

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;

    private Integer status;

    private LocalDateTime createTime;

    private LocalDateTime updateTime;

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

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

}
 
 

mapper

@Mapper
public interface EmployeeMapper extends BaseMapper<Employee> {


}
 

service

public interface EmployeeService extends IService<Employee> {
}

Service.impl

@Service
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements EmployeeService {
}
 

controller

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

    @Autowired
    private EmployeeService employeeService;
}
 
 

common

封装返回结果类R;

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

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

}
 
 

⚝  请求URL地址:http://127.0.0.1:8080/employee/login

⚝  请求方式:post  

⚝  参数:username , password

controller.EmployeeController


/**
 * 员工登录
 * */
@PostMapping("/login")
public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee){
  

    //1、将界面提交的密码password进行md5加密处理
    String password = employee.getPassword();
    password = DigestUtils.md5DigestAsHex(password.getBytes());  //解析成md5加密

    //2.根据页面提交用户名username查询数据库
    LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(Employee::getUsername,employee.getUsername());
    Employee emp = employeeService.getOne(queryWrapper);   //获取数据库中唯一的数据,就是在索引中有Unique字段的数据

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

访问地址:http://127.0.0.1:8080/backend/page/login/login.html

可用账号:admin    ;默认密码:123456  进行登录测试;

F12-->Application-->Storage-->Local Storage中的userInfo可以查看已登录的id以及信息;

退出登录功能

当实现退出功能时:

1、我们应该清理session中的id  

2、并跳转界面来实现退出

登录后当我们点击右上角的退出按钮时,将会触发以下请求ajax请求:

⚝  请求URL地址:http://localhost:8080/employee/logout

⚝  请求方式:post

⚝  参数:无

//  退出功能实现
//  1、LocalStorage 清理Session中的用户id
//  2、返回结果
@PostMapping("/logout")
public R<String> logout(HttpServletRequest request){
    request.getSession().removeAttribute("employee");
    return R.success("退出成功");
}
 

此时可以查看到F12-->Application-->Storage-->Local Storage中的userInfo消失

完善登录功能 

        项目缺陷:如果用户不进行登陆,直接访问系统的首页,照样可以正常访问,这种设计是不合理的,我们希望看到的效果是只有完成了登陆后才可以访问系统中的页面,如果没有登陆则跳转到登陆页面;

前端拦截器如果msg返回为“NOTLOGIN”则跳回login.html界面

        使用过滤器或者是拦截器,在拦截器或者是过滤器中判断用户是否已经完成了登陆,这里使用过滤器。

创建过滤器:LoginCheckFilter

filter



/*
*检查用户是否已经完成登录
* **/
@WebFilter(filterName = "loginCheckFileter",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; //强转为HttpServletRequest
        HttpServletResponse response = (HttpServletResponse)servletResponse;
        log.info("拦截到请求:{}",request.getRequestURI());

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

        /*直接放行的URL*/
        String[] urls = new String[]{
                "/employee/login",
                "/employee/logout",
                "/backend/**",
                "/front/**",

        };


        // 2、判断本次请求是否需要处理
        boolean check = isMatch(urls, requestURI);
        // 3、如果不需要处理,直接放行
        if(check){
            log.info("本次请求{}不需要处理",requestURI);
            filterChain.doFilter(request,response);
            return;
        }

        // 4、判断登录状态(session是否含有employee的登录信息),如果已经登录,则直接放行
        Long empId = (Long) request.getSession().getAttribute("employee");
        if (empId != null) {
            log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("employee"));
        
            filterChain.doFilter(request,response);
            return;
        }


        //5.如果未登录则返回未登录结果,通过输出流的方式向客户端页面响应JSON格式数据
        response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
        return;


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

在启动类加上注解@ServletComponentScan

        此时访问:http://127.0.0.1:8080/backend/index.html 如果查看到该页面跳转至登录界面并且后台日志会打印拦截信息则过滤器配置成功!

新增员工

        将数据插入到employee表中,需要注意的是表中的username字段加入了唯一约束,因为username是员工登录账号必须是唯一的。

程序的执行过程:

1、页面发送ajax请求,将新增员工页面中输入的数据以json的形式提交到服务端

2、服务端Controller接收页面提交的数据并调用Service将数据进行保存

3、Service调用Mapper操作数据库,保存数据

F12-->Network-->employee中的信息可以查看到请求地址与请求方式以及携带参数等信息。

 

⚝  新增员工请求URL地址:http://localhost:8080/employee  

⚝  请求方式:post

⚝  参数:name,phone,sex,idNumber,username

controller.EmployeeController

//新增员工
@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());

    //获的当前登录用户的id
    Long empId= (Long) request.getSession().getAttribute("employee");
    employee.setCreateUser(empId);
    employee.setUpdateUser(empId);
    employeeService.save(employee);

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

        此时到界面中添加员工可以看见,添加数据库没有username的员工信息已经可以成功保存数据库并调整界面。(跳转到的界面出错不用管,因为还没有弄员工页面的展示)

完善新增功能

        我们在Mysql中username设置为唯一索引,而代码中并没有对username是否重复进行判断。新增用户username在数据库中已经存在的字段的时候,就会出现异常,这个异常是MySQL数据库抛出来的。因此我们需要配置异常处理器专门来处理这个异常。

添加重复的账号则会报异常:java.sql.SQLIntegrityConstraintViolationException: Duplicate entry 'lxxd' for key 'employee.idx_username'

配置全局异常处理器GlobalExceptionHandler :

filter

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(annotations = {RestController.class, Controller.class})  //只要类上加了这两个处理器的就会被拦截
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {
    /**
     * 进行异常处理 SQLIntegrityConstraintViolationException
     * */
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
        log.error(ex.getMessage());

        //判断异常信息里面是否含有该关键字,做进一步处理

        if(ex.getMessage().contains("Duplicate entry")){
            String[] split = ex.getMessage().split(" ");  //以空格分割
            String msg = split[2] + "已存在";   
            return R.error(msg);
        }
        return R.error("未知错误!");

    }

}
 
 

       

        此时我们在登录后,添加一个重复的账号时可以看到。前端输出的错误信息,以及后端我们打印出的报错日志;

员工信息分页功能

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

程序的执行过程:

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

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

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

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

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

配置MP的分页插件

config.MybatisPlusConfig

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;

/*
 *   配置MP的分页插件
 * **/
@Configuration
public class MybatisPlusConfig {


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


}
 
 

当我们登录进index.html界面后。前端主动往后端发送请求获取列表数据。请求如下

默认请求发送中的page=1,pageSize=10

⚝  员工列表请求URL:http://localhost:8080/employee/page?page=1&pageSize=10

⚝  请求方式:GET

⚝  参数:page,pageSize

我们在查看搜索框中以搜索123时

 

 地址相同而携带的参数不同。刚进入页面时默认发的请求中并未携带name参数;只有使用搜索框搜索时才携带name参数。

⚝  请求URL:http://localhost:8080/employee/page

⚝  请求方式:GET

⚝  参数:page,pageSize,name

controller.EmployeeController

//分页查询
@GetMapping("/page")
public R<Page> page(int page, int pageSize,String name){
    log.info("page = {},pageSize = {} ,name={}",page,pageSize,name);
    //构造分页构造器
    Page pageInfo = new Page(page,pageSize);

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

    //条件过滤条件 name不为null,才会 比较 getUsername方法和前端传入的name是否匹配 的过滤条件
    queryWrapper.like(StringUtils.isNotEmpty(name),Employee::getUsername,name);

    //添加排序条件  根据用户的时间升序分页展示
    queryWrapper.orderByDesc(Employee::getUpdateTime);

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


    //执行查询
    return R.success(pageInfo);
}
 

此时登录后我们可以正常查看员工的分页功能。并且能正常的使用搜索框搜索员工。

 启用/禁用员工账号

        在员工管理列表页面中,可以对某个员工账号进行启用或者是禁用操作。账号禁用的员工不能登陆系统,启用后的员工可以正常登陆;(后端传status由前端负责判定)

        只有管理员(admin用户)才可以对其他普通用户进行启用操作,禁用操作,所以普通用户登录系统后启用,禁用按钮不显示;(前端负责判定)

        并且如果某个员工账号的状态为正常,则按钮显示为’‘禁用’,如果员工账号状态为已禁用,则按钮显示为“启用”。(前端负责判定)

程序的执行过程:

1、页面发送ajax请求,将参数(id、status)提交到服务端

2、服务端Controller接收页面提交的数据并调用Service更新数据

3、Service调用Mapper操作数据库

账号正常:status=1;账号禁用:status=0;

⚝  启用/禁用请求URL:http://localhost:8080/employee    

⚝  请求方式:post        

⚝  参数:id ,status

controller.EmployeeController

@PutMapping
 public R<String> update1(HttpServletRequest request,@RequestBody Employee employee){

     log.info(employee.toString());

     Long empId = (Long) request.getSession().getAttribute("employee"); //获取浏览器info字段的id
     employee.setUpdateTime(LocalDateTime.now());
     employee.setUpdateUser(empId);
     employeeService.updateById(employee);
     return R.success("员工信息修改成功");
 }
 
 

        修改员工的状态,提示信息显示修改成功,但是我们去数据库查验证的时候,但数据库中的status字段未能发生变化。

        js精度只能到16位,后面两位为四舍五入。而代码中的长度为19位。所以我们可以看到最后的两位数为0。导致数据库根据id查询修改时未能发现该条数据。修改失败

解决方法

对象转换器JacksonObjectMapper

1)、提供对象转换器JacksonObjectMapper,基于Jackson进行Java对象到json数据的转换(资料中已经提供,直接复制到项目中使用)

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

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

在config.webMvcConfig中扩展我们的mvc框架的消息转换器

config.WebMavcConfig


    //  扩展SpringMvc的消息转换器
    @Override
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        log.info("自定义消息转化器 被调用!");
        // 创建消息转换器对象
        MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
        // 设置对象转换器,底层使用JackSON 将Java对象转化为JSON
        messageConverter.setObjectMapper(new JacksonObjectMapper());

        // 将上面的消息转换器对象追加到SpringMVC的 转换器容器 的第一个位置,优先采用下标为 0 位置的消息转换器
        converters.add(0,messageConverter);
    }

        启用与禁用员工账号这个功能,操作更新成功,并且数据库修改成功,页面的禁用和启动按钮也能正常转换;

员工信息编辑

        在员工管理中点击编辑按钮,跳转编辑界面并回显员工信息并进行修改,点击保存按钮完成编辑操作。当浏览器请求用户时会携带请求的id参数我们根据id参数返回对应的数据信息。返回类型的Json数据。

程序的执行流程:

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

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

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

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

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

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

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

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

保存则会调用前面已经写过的新增员工方法(save)。

使用@PathVariable 用于绑定 url 中的占位符。例如:请求 url 中 /delete/{id},这个{id}就是 url 占位符。(相当于他可以获得路径上指定{*}里面的字符串)

⚝  请求URL: http://localhost:8080/employee/{id}    

⚝  请求方式:GET

⚝  参数:id

//根据id查询员工信息
@GetMapping("/{id}")
public R<Employee> qq(@PathVariable Long id){
    log.info("根据id查询员工信息:id = {}"+String.valueOf(id));
    Employee employee =employeeService.getById(id);
    if(employee != null){
        return R.success(employee);
    }
    return R.error("没有查到对应员工信息");
}

        此时我们点击员工信息保存时就可以看见数据能够正常的回显到我们的编辑界面。并且我们修改新的信息后能够正常保存到数据库中。

公共字段自动填充

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

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

实现步骤:
        1、在实体类的属性上加入@TableField注解,指定自动填充的策略
        2、按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需MetaObjectHandler接口

在需要插入的字段是添加@TableField注解;如下

entity.Employee


@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、实现接口MetaObjectHandler

设置一个MetaObjectHandler类为公共字段提供赋值如下

common

(只能获取到时间的插入填充,未能获取到插入人信息的自动填充)

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

import java.time.LocalDateTime;

@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {

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

        /**给相应的属性赋值**/
        metaObject.setValue("createTime", LocalDateTime.now());
        metaObject.setValue("updateTime", LocalDateTime.now());

        metaObject.setValue("createUser",new Long(1));
        metaObject.setValue("updateUser",new Long(1));
    }


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

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

    }
}
 
 

        将EmployeeController里面更新插入时间与更新插入人的信息字段屏蔽。保证用的是MetaObjectHandler接口实现时间与插入人的更新。找到以下语句并屏蔽。如下:

controller.EmployeeController

//employee.setCreateTime(LocalDateTime.now()); 
//employee.setUpdateTime(LocalDateTime.now()); 

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

//  employee.setUpdateTime(LocalDateTime.now());

此时我们可以看到 create_user字段与update_user字段与我们前面写的一致存入的是个“1”;

3、完善公共字段自动填充获取插入人信息

使用ThreadLocal方法

什么是ThreadLocal?
        ThreadLocat并不是一个Thread,而是Thread的局部变量。当使用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的Filter方法中调用BaseContext来设置当前登录用户的id

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

common

public class BaseContext {
    public static  ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    /**
     * 设置值
     * */
    public static void setCurrentId(Long id){
        threadLocal.set(id);
    }

    /**
     * 获取值
     * */
    public static Long getCurrentId(){
        return threadLocal.get();
    }

}
 

在过滤器Filter.LoginCheckFilter中设置threadLocal值、来设置当前登录用户的id


...............................
 
// 4、判断登录状态(session是否含有employee的登录信息),如果已经登录,则直接放行
Long empId = (Long) request.getSession().getAttribute("employee");
if (empId != null) {
    log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("employee"));

******************************************
    BaseContext.setCurrentId(empId);

******************************************

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

就可以在common.MyMetaObjectHandler通过get设置值、来获取当前登录用户的id

   @Override
    public void insertFill(MetaObject metaObject) {
        log.info("公共字段自动填充[insert]....");

        /**给相应的属性赋值**/
        metaObject.setValue("createTime", LocalDateTime.now());
        metaObject.setValue("updateTime", LocalDateTime.now());

**************************************************
        metaObject.setValue("createUser",BaseContext.getCurrentId());
        metaObject.setValue("updateUser",BaseContext.getCurrentId());
**************************************************
    }

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

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

    }

        注意这里不要用admin账号进行测试,因为admin账号的id本来就是“1”,看不出来是否成功更新了update_user字段。所以这里我使用了一个id不为1的账号编辑了另一个员工账号。可以看到update_user字段成功修改。

新增分类

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

新增菜单对应的表:Category

准备工作

Mapper

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
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;


    //是否删除
    private Integer isDeleted;

}
 
 

service

import com.baomidou.mybatisplus.extension.service.IService;
import com.lxxd.object.entity.Category;

public interface CategoryService extends IService<Category> {
}
 
 

service.Impl

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

CategoryController

mport org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

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

    @Autowired
    private CategoryService categoryService;

}
 

新增菜品分类业务代码编写

程序的执行过程:

1、页面(backend/page/category/list.html)发送ajax请求,将新增分类窗口输入的数据以json形式提交到服务端

2、服务端Controller接收页面提交的数据并调用Service将数据进行保存

3、Service调用Mapper操作数据库,保存数据

可以看到新增菜品分类和新增套餐分类请求的服务端地址和提交的json数据结构相同,所以服务端只需要提供一个方法统一处理即可。

新增菜品、套餐分类:

⚝ 请求URL:http://127.0.0.1:8080/category    

⚝ 请求方式:POST                          

⚝ 参数:name, type, sort                                  

⚝ 参数说明:type=1为菜品,type=2为套餐

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

        此时添加菜品可以看到菜品添加成功。并存储到数据库中。

菜品分类管理分页查询

        将菜品的分类以分页的形式显示在页面中。

程序的执行过程:

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

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

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

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

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

⚝ 请求URl:http://127.0.0.1:8080/category/page

⚝ 请求方式:GET            

⚝ 参数:page ,pageSize

//分页信息分页查询
@GetMapping("/page")
public R<Page> page(int page,int pageSize){
    //构造分页构造器
    Page<Category> pageInfo = new Page(page,pageSize);

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



    //添加排序条件  根据Sort进行排序
    queryWrapper.orderByAsc(Category::getSort);

    //执行查询
    categoryService.page(pageInfo,queryWrapper);

    //执行查询
    return R.success(pageInfo);
}
 
 

        此时我们登录后,点击分类管理就可以看到我们的菜品分类信息了。

菜品分类删除

        在分类管理列表页面,可以对某个分类进行删除操作。需要注意的是当分类关联了菜品或者套餐时,此分类不允许删除。

⚝ 请求URL: http://127.0.0.1:8080/category        

⚝ 请求方式:DELETE                                            

⚝ 参数:ids

初步删除代码实现(该代码只是直接移除菜品分类。并没有对分类关联了菜品或者套餐进行判定后在删除)

@DeleteMapping
public R<String> delete(Long ids){
    log.info("删除分类 ,id为 :{}",ids);
    categoryService.removeById(ids);

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

完善删除功能

        我们要对关联的菜品表与套餐表进行判定是否关联,使用我们要导入对应的菜品表(Dish)与套餐表(Setmeal)

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

1、实体类Dish和Setmeal (从课程资料中复制即可)

2、Mapper接口DishMapper和SetmealMapper

3、Service接口DishService和SetmealService

4、Servicec类DishServicelmpl和SetmealServicelmpl

导入实体类Dish和Setmeal并创建对应的接口:如下

entity


/**
 菜品
 */
@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;


    //是否删除
    private Integer isDeleted;

}

Mapper

@Mapper
public interface DishMapper extends BaseMapper<Dish> {
}
 

service

public interface DishService extends IService<Dish> {


}

DishServiceImpl

@Service
@Slf4j
public class DishServiceImpl extends ServiceImpl<DishMapper, Dish> implements DishService {

}

entity

@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;


    //是否删除
    private Integer isDeleted;
}

mapper

@Mapper
public interface SetmealMapper extends BaseMapper<Setmeal> {
}

service

public interface SetmealService extends IService<Setmeal> {


}

 SetmealServiceImpl

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

        CategoryService中自定义remove方法并编写对应的方法实现类来对我们来实现我们业务的逻辑处理。

service.CategoryService

public void remove(Long ids);

CategoryServiceImpl


@Autowired
private SetmealService setmealService;

@Autowired
private DishService dishService;



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

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

    //查询当前分类是否关联了菜品,如果已经关联,抛出一个业务异常
    if(count1 > 0){
        //已经关联菜品,抛出一个业务异常
    }



    //查询当前分类是否关联了套餐,如果已经关联,抛出一个业务异常
    LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
    //添加查询条件,根据分类id进行查询
    setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId,ids);
    int count2 = setmealService.count(setmealLambdaQueryWrapper);
    if(count2 > 0){
        //已经关联套餐,抛出一个业务异常
    }

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

        当我们查询当前分类是否关联了菜品或套餐时,如果已经关联。我们抛出一个业务异常来提示删除失败。

创建自定义异常类

common

/**
 * 自定义异常类
 * */
public class CustomException extends RuntimeException{
    public CustomException(String message){
        super(message);
    }
}

调用自定义业务异常

CategoryServicelmpl

........................................
//查询当前分类是否关联了菜品,如果已经关联,抛出一个业务异常
if(count1 > 0){
    //已经关联菜品,抛出一个业务异常
****************************************
    throw new CustomException("当前分类下关联了菜品,不能删除");
******************************************
}
.......
.....................................
if(count2 > 0){
    //已经关联套餐,抛出一个业务异常
*************************************************
    throw new CustomException("当前分类下关联了套餐,不能删除");
******************************************************
}
 
.....................................

在前面配置的全局异常处理器中捕获改异常并处理异常

common.GlobalExceptionHandler

 /**
     * 菜品异常处理  自定义CustomException
     * */
    @ExceptionHandler(CustomException.class)
    public R<String> exceptionHandler1(CustomException ex){
        log.error(ex.getMessage());


        return R.error(ex.getMessage());

    } 

将CategoryController里的方法修改成写的业务代码

controller.CategoryController

   @DeleteMapping
    public R<String> delete(Long ids){
        log.info("删除分类 ,id为 :{}",ids);
****************************************
        categoryService.remove(ids);
****************************************
        return R.success("删除成功");
    }

        删除功能完善完成,此时在分类管理中,已关联菜品或者已关联套餐的分类已经不能被删除。

菜品分类修改

        在分类管理列表页面点击修改按钮,弹出修改窗口,在修改窗口回显分类信息并进行修改,最后点击确定按钮完成修改操作。

这里是由前端做了数据回显功能,后端不在做数据回显功能

前端已写  如图:

⚝ 请求URL: http://127.0.0.1:8080/category      

⚝ 请求方式:PUT        

⚝ 参数:id, name, sort

controller.CategoryController

//根据id修改菜品信息
@PutMapping
public R<String> update(@RequestBody Category category){
    log.info("修改分类信息");
    //修改数据  注意:updatetime与updateUser需要在实体类中添加@TableField使其自动填充
    categoryService.updateById(category);

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

然后我们就可以启动测试菜品分类的修改信息功能,并查看数据库中的信息被同步修改。

文件的上传下载

文件的上传介绍

文件上传下载黑马程序www.itheima.文件上传介绍文件上传,也称为upload,是指将本地图片、视频、音频等文件上传到服务器上,可以供其他用户浏览或下载的过程。文件上传在项目中应用非常广泛,我们经常发微博、发微信朋友圈都用到了文件上传功能。文件上传时,对页面的form表单有如下要求:

method="post"    采用post方式提交数据.

enctype="multipart/form-data"   采用multipart格式上传文件

type="file"   使用input的file控件上传

举例:

<form method="post" action="/common/upload" enctype="multipart/form-data">

<input name="myFile" type="/>

<input type="submit" value="提交" />

</form>

服务端接收介绍

文件上传介绍服务端要接收客户端页面上传的文件,通常都会使用Apache的两个组:
commons-fileupload
commons-io
Spring框架在spring-web包中对文件上传进行了封装,大大简化了服务端代码,我们只需要在Controller的方法中声明一个MultipartFile类型的参数即可接收上传的文件,例如:

文件下载介绍

文件下载介绍文件下载,也称为download,是指将文件从服务器传输到本地计算机的过程。
通过浏览器进行文件下载,通常有两种表现形式:
    以附件形式下载,弹出保存对话框,将文件保存到指定磁盘目录.
    直接在浏览器中打开通
 
过浏览器进行文件下载,本质上就是服务端将文件以流的形式写回浏览器的过程。

文件上传

       

 

        将文件上传下载界面文件夹中的upload.html导入到工程目录resources/backend/page/demo下如图:

        这是一个测试界面,为了方便我们测试我们在过滤器中配置拦截界面不拦截此界面,以便方便我们测试文件上传与下载功能。

filter.LoginCheckFilter

/*直接放行的URL*/
String[] urls = new String[]{
     ..................
.......................
..................
********************
        "/common/**"
*******************
};
 

⚝ 请求URL:http://127.0.0.1:8080/common/upload                

⚝ 请求方式:POST      

⚝ 参数:file        

controller

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

    @PostMapping("/upload")
    public R<String> upload(MultipartFile file){
        //file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会被删除
        log.info(file.toString());
        try {
            file.transferTo(new File("D:\\hello.jpg"));   //文件的目录
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return null;
    }
}

此时我们访问网站:http://127.0.0.1:8080/backend/page/demo/upload.html

上传一张照片后可以看到我们在对应的D盘目录下就会产生一张hello.jpg照片。

完善功能

        在application.yml文件中指定好保存图片的目录。

lxxd:
  path: E:\img\

完善controller.CommonController

controller

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.util.UUID;

@RestController
@RequestMapping("/common")
@Slf4j
public class CommonController {
    /**
     * 文件上传
     * **/
    @Value("${lxxd.path}")
    private String basePath;

    @PostMapping("/upload")
    public R<String> upload(MultipartFile file){
        //file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会被删除
        //upload方法名中的参数名 必须是file(文件上传表单的 中name属性值必须是file,name="file")
        log.info("上传的文件为: "+file.toString());

        //原始文件名
        String originalFilename = file.getOriginalFilename();  //abc.jpg
        //截取原始文件名的后缀(使用UUID+原始文件名的后缀以防上传文件名重复)
        String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));  // suffix = .jpg  截取是带点的后缀

        // 使用UUID重新生成文件名,防止文件名重复,造成后面上传的文件覆盖前面上传的文件
        String fileName = UUID.randomUUID().toString()+suffix; //随机生成的30多位+后缀

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

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

    }
}
 
 

文件下载

⚝ 请求URL:http://127.0.0.1:8080/common/download

⚝ 请求方式:GET                                                                

⚝ 参数:name    

controller.CommonController

//文件下载
@GetMapping("/download")
public void download(String name, HttpServletResponse response){
    try {
        //输入流,通过输入流读取文件内容
        FileInputStream fileInputStream = new FileInputStream(new File(basePath+name));

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

        //设置一下为图片文件
        response.setContentType("image/jpeg");

        // 输入流读取到 内容放到 bytes数组中
        int len = 0;
        byte[] bytes = new byte[1024];
        while ((len = fileInputStream.read(bytes)) != -1){   //当为-1的时候输入流读取完成
            outputStream.write(bytes,0,len);          //写输入流到浏览器
            outputStream.flush();
        }

        //关闭资源
        outputStream.close();
        fileInputStream.close();
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

        此时可以在网站:http://127.0.0.1:8080/backend/page/demo/upload.html   中测试可以看到,图片上传后并回显到网页的图片展示框中。并且可以在上传图片存储的目录(E:\img\)内看到该图片。

新增菜品

        后台系统中可以管理菜品信息,通过新增功能来添加一个新的菜品,在添加菜品时需要选择当前菜品所属的菜品分类,并且需要上传菜品图片,在移动端会按照菜品分类来展示对应的菜品信息。

        新增菜品,其实就是将新增页面录入的菜品信息插入到dish表,如果添加了口味做法,还需要向dish_flavor表插入数据。所以在新增菜品时,涉及到两个表:

对应的菜品表:dish

对应的菜品口味表:dish_flavor

准备工作

在开发业务功能前,先将需要用到的类和接口基本结构创建好:

实体类DishFlavor (直接从课程资料中导入即可,

Dish实体前面课程中已经导入过了)

Mapper接口 DishFlavorMapper

业务层接口DishFlavorService

业务层实现类DishFlavorServicelmpl

控制层DishController

entity

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
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;


    //是否删除
    private Integer isDeleted;

}
 
 

mapper

@Mapper
public interface DishFlavorMapper extends BaseMapper<DishFlavor> {
}
 

service

public interface DishFlavorService extends IService<DishFlavor> {
}
 

DishFlavorServiceImpl

@Service
public class DishFlavorServiceImpl extends ServiceImpl<DishFlavorMapper, DishFlavor> implements DishFlavorService {
}

controller

@Slf4j
@RequestMapping("/dish")
@RestController
public class DishController {
    
   @Autowired
    private  CategoryService categoryService;
}
 

新增菜品时前端页面和服务端的交互过程:
    在开发代码之前,需要梳理干下新增菜品时前端页面和服务端的交互过程:
    1、页面(backend/page/food/add.html)发送ajax请求,请求服务端获取菜品分类数据并展示到下拉框中
    2、页面发送请求进行图片上传,请求服务端将图片保存到服务器
    3、页面发送请求进行图片下载,将上传的图片进行回显
    4、点击保存按钮,发送ajax请求,将菜品相关数据以json形式提交到服务端
    开发新增菜品功能,其实就是在服务端编写代码去处理前端页面发送的这4次请求即可。

获取菜品分类数据

⚝ 请求URL: http://127.0.0.1:8080/category/list                  

⚝ 请求方式: GET                                        

⚝ 参数:type=1

controller.CategoryController

// 根据条件查询分类数据
@GetMapping("/list")
public R<List<Category>> categoryList(Category category){
    //条件构造器
    LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
    //  条件只有当 category.getType()不为空
    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);
}
 

此时我们点击菜品管理,添加菜品。点击菜品分类就可以看到菜品分类的展示。

添加菜品保存功能

我们可以看到flavors中存储的是dish_flavors表所存储的数据表对应关系为:
dish表中  id=dish_id   dish_flavors表
涉及到多表存储需要用DTO先接收数据在分布存储

DTO介绍:

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

因为涉及到多表的存储所以需要使用DTO

dto


@Data
public class DishDto extends Dish {

    private List<DishFlavor> flavors = new ArrayList<>();

    private String categoryName;

    private Integer copies;
}
 

⚝ 请求URL: http://127.0.0.1:8080/dish        

⚝ 请求方式:POST    

⚝ 参数:name,price,code,image,description,flavors[],image,name,price,status

        DishService中创建saveWithFlavor方法并编写对应的方法实现类来对我们来实现我们业务的逻辑处理。

service.DishService

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

DishServiceImpl

 @Autowired
    private DishFlavorService dishFlavorService;

    @Transactional// 由于涉及到对dish、dish_flavor两张表的操作,应该使用 @Transactional 来标注事务
    @Override
    public void saveWithFlavor(DishDto dishDto) {
        //保持菜品的基本信息到菜品表dish
        this.save(dishDto);
        log.info("this = " + this);

        Long dishId = dishDto.getId();      //获取前端传过来的dishId

        // 菜品口味 (flavors 并不包含 dishId,故dish需要另外赋值)
        List<DishFlavor> flavors = dishDto.getFlavors();
        flavors.stream().map((flavor)->{
            flavor.setDishId(dishId);
            return flavor;
        }).collect(Collectors.toList());

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

controller.CategoryController

    //新增菜品
    @PostMapping
    public R<String> save(@RequestBody DishDto dishDto) {
        log.info(dishDto.toString());
        dishService.saveWithFlavor(dishDto);
        return R.success("新增菜品操作成功!");
    }
此时为了让@Transactional 生效,还需要在启动类添加@EnableTransactionManagement 来开启事务

Main

@SpringBootApplication
@Slf4j //开启日志
@ServletComponentScan
@EnableTransactionManagement  // 开启事务,DishServiceImpl的saveWithFlavor方法
public class Main {
    public static void main(String[] args) {
        SpringApplication.run(Main.class,args);
        log.info("项目启动成功");
    }
}

我们新增菜品后就可以在数据库对应的

菜品管理分页查询

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

程序执行过程:
1、页面(backend/page/food/list.html)发送ajax请求,将分页查询参数(page、pageSize,name)提交到服务端,获取分页数据
2、页面发送请求,请求服务端进行图片下载,用于页面图片展示开发菜品信息分页查询功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。

⚝ 请求URL:http://127.0.0.1:8080/dish/page    

⚝ 请求方式:GET                    

⚝ 参数page,pageSize,name     

controller.DishController

 
@Autowired
private CategoryService categoryService;
 
 
//菜品分页查询
@GetMapping("/page")
public R<Page> page(int page,int pageSize ,String name){

    //构造分页构造器对象
    Page<Dish> pageInfo = new Page<>(page,pageSize);
    Page<DishDto> dishDtoPage = new Page<>();

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

    //添加过滤条件
    queryWrapper.like(name !=null,Dish::getName,name);

    //添加排序条件
    queryWrapper.orderByDesc(Dish::getUpdateTime);

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

    //对象拷贝
    BeanUtils.copyProperties(pageInfo,dishDtoPage,"records"); //将pageInfo拷贝到dishDtoPage(Page里面有records方法)除去records方法不拷贝

    //获取Page 里面的Records字段
    List<Dish> records = pageInfo.getRecords();
    List<DishDto> list = records.stream().map((dish) -> {  // dish 为每个菜品对象
        DishDto dishDto = new DishDto();

        BeanUtils.copyProperties(dish,dishDto);

        Long categoryId = dish.getCategoryId();  // 菜品的分类id

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

        return dishDto;
    }).collect(Collectors.toList());
    //赋值
    dishDtoPage.setRecords(list);


    return R.success(dishDtoPage);
}
 

        此时登录后点击菜品管理,可以看到菜品管理中所有的菜品数据都以分页数据已经全部显示在页面上。

修改菜品

        在菜品管理列表页面点击修改按钮,跳转到修改菜品页面,在修改页面回显菜品相关信息并进行修改,最后点击确定按钮完成修改操作。

需要梳理一下修改菜品时前端页面(add.html)和服务端的交互过程:
1、页面发送ajax请求,请求服务端获取分类数据,用于菜品分类下拉框中数据展示( 前面实现了)
2、页面发送ajax请求,,请求服务端,根据id查询当前菜品信息,用于菜品信息回显
3、页面发送请求,请求服务端进行图片下载,用于页图片回显
4、点击保存按钮,页面发送ajax请求,将修改后的菜品相关数据以json形式提交到服务端
开发修改菜品功能,其实就是在服务端编写代码去处理前端页面发送的这4次请求即可。

修改菜品对应的菜品信息进行回显

⚝ 请求URL:http://127.0.0.1:8080/dish/{id}        

⚝ 请求方式:GET                                      

⚝ 参数:id

service.DishService

public DishDto getByIdWithFlavor(Long id);

DishServiceImpl

/**
 * 根据id查询菜品信息和对应口味信息
 * **/
public DishDto getByIdWithFlavor(Long id){
    // 从dish表中查询 菜品的基本信息
    Dish dish = this.getById(id);   //根据id查询
    DishDto dishDto = new DishDto();
    //拷贝
    BeanUtils.copyProperties(dish,dishDto);
    //查询当前菜品对应的口味信息,从dish_flavor表查询

    LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(DishFlavor::getDishId,dish.getId());

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


    return dishDto;
}

controller.DishController

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

}

保存修改完成后对对应的菜品信息进行保存

⚝ 请求URL: http://127.0.0.1:8080/dish      

⚝ 请求方式:PUT      

⚝ 参数:categoryId,categoryId,code,copies,createTime,createUser,description,flavors[],id,image,isDeleted,name,price,sort,status,updateUser

service.DishService

//保存更新菜品分类save
public void  updateWithflavor(DishDto dishDto);

DishServiceImpl

//保存更新菜品分类save
@Override
@Transactional  //开启事务保证数据一致性
public void updateWithflavor(DishDto dishDto) {
    // 更新dish表
    this.updateById(dishDto);

    // 删除当前菜品对应的口味数据,dish_flavor表的delete操作
    LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(DishFlavor::getDishId,dishDto.getId());
    dishFlavorService.remove(queryWrapper);

    // 添加 前端提交过来的口味数据,insert操作
    List<DishFlavor> flavors = dishDto.getFlavors();
    List<DishFlavor> flavorList = flavors.stream().map((item) -> {
        item.setDishId(dishDto.getId());
        return item;
    }).collect(Collectors.toList());

    dishFlavorService.saveBatch(flavorList);
}
 
 

controller.DishController

@PutMapping
public R<String> update(@RequestBody DishDto dishDto){
    log.info(dishDto.toString());

    dishService.updateWithflavor(dishDto);

    return R.success("修改菜品操作成功!");
}
 

        此时我们在菜品管理中并可以对菜品的信息进行修改。

套餐管理

新增套餐

        需求分析套餐就是菜品的集合。后台系统中可以管理套餐信息,通过新增套餐功能来添加一个新的套餐,在添加套餐时需要选择当前套餐所属的套餐
        分类和包含的菜品,并且需要上传套餐对应的图片,在移动端会按照套餐分类来展示对应的套餐。

套餐表:setmeal

套餐菜品关系表:setmeal_dish

准备工作

代码开发-准备工作
    在开发业务功能前,先将需要用到的类和接口基本结构创建好:
    实体类SetmealDish(直接从课程资料中导入即可,Setmeal实体前面课程中已经导入过了).
    DTO SetmealDto (直接从课程资料中导入即可).
    Mapper接口 SetmealDishMapper.
    业务层接口SetmealDishService
    业务层实现类SetmealDishServicelmpl
    控制层SetmealController.

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


    //是否删除
    private Integer isDeleted;
}
 
 

dto

@Data
public class SetmealDto extends Setmeal {

    private List<SetmealDish> setmealDishes;

    private String categoryName;
}

mapper

@Mapper
public interface SetmealDishMapper extends BaseMapper<SetmealDish> {
}
 

servier

public interface SetmealDishService extends IService<SetmealDish> {
}
 

SetmealDishServicelmpl

@Service
@Slf4j
public class SetmealDishServiceImpl extends ServiceImpl<SetmealDishMapper,SetmealDish> implements SetmealDishService {
}

controller

@RestController
@RequestMapping("/setmeal")
@Slf4j
public class SetmealController {

    @Autowired
    private SetmealService setmealService;

    @Autowired
    private SetmealDishService setmealDishService;

}
 

在新增套餐中我们首先应该请求数据库中的套餐分类请求,获得套餐分类数据 :

     交互过程在开发代码之前,需要梳理一下新增套餐时前端页面和服务端的程序的执行过程:
1、页面(backend/page/combo/add.html)发送ajax请求,请求服务端获取套餐分类数据并展示到下拉框中
2、页面发送ajax请求,请求服务端获取菜品分类数据并展示到添加菜品窗口中
3、页面发送ajax请求,请求服务端,根据菜品分类查询对应的菜品数据并展示到添加菜品窗口中
4、页面发送请求进行图片上传,请求服务端将图片保存到服务器
5、页面发送请求进行图片下载,将上传的图片进行回显
6、点击保存按钮,发送ajax请求,将套餐相关数据以json形式提交到服务端
    开发新增套餐功能,其实就是在服务端编写代码去处理前端页面发送的这6次请求即可。

套餐分类数据

⚝ 请求URL: http://127.0.0.1:8080/dish/list      

⚝ 请求方式:GET                    

⚝ 参数:categoryId

controller.DishController

// 根据条件(分类id)查询对应的菜品数据
@GetMapping("/list")
public R<List<Dish>> list(Dish dish) {

    //构造查询条件
    LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(dish.getCategoryId() != null,Dish::getCategoryId,dish.getCategoryId());
    //条件条件,查询状态是1 (Status=0代表禁售,Status=1代表正常)
    queryWrapper.eq(Dish::getStatus,1);

    //添加排序条件
    queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);   //根据getSort升序排,根据getUpdateTime降序排
    List<Dish> list = dishService.list(queryWrapper);

    return R.success(list);

}

保存套餐

⚝ 请求URL: http://127.0.0.1:8080/setmeal                                                  

⚝ 请求方式:POST                                          

⚝ 参数:categoryId,code,description,dishList: [],idType,image,name,price,setmealDishes: [{copies, dishId,, name: ,price: ,},…],status

service.SetmealService

public void saveWithDish(SetmealDto setmealDto);

SetmealServiceImpl



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

    List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
    setmealDishes.stream().map((item)->{
        item.setSetmealId(setmealDto.getId());
        return item;
    }).collect(Collectors.toList());

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

controller.SetmealController

  @PostMapping
    public R<String> save(@RequestBody SetmealDto setmealDto){
        log.info("套餐信息:{}",setmealDto);
        setmealService.saveWithDish(setmealDto);

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

    }

此时我们添加套餐时可以看到,数据库对应的setmeal表与setmealDish表产生对应的数据。

套餐管理的分页查询

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


 

在开发代码之前,需要梳理一下套餐分页查询时前端页面和服务端的交互过程:
1、页面(backend/page/combo/list.html)发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端,获取分页数据
2、页面发送请求,请求服务端进行图片下载,用于页面图片展示
开发套餐信息分页查询功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。

⚝ 请求URL: http://127.0.0.1:8080/setmeal/page      

⚝ 请求方式:GET                

⚝ 参数:page,pageSize,name

controller.SetmealController

   @Autowired
    private CategoryService categoryService;
@GetMapping("/page")
public R<Page> page(int page,int pageSize,String name){
    //分页构造器对象
    Page<Setmeal> pageInfo = new Page<>(page,pageSize);
    Page<SetmealDto> dtoPage = new Page<>(page,pageSize);

    LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
    //添加查询条件,根据name进行like模糊查询
    queryWrapper.like(name!= null ,Setmeal::getName,name);
    //添加排序条件,根据跟新时间降序排列
    queryWrapper.orderByDesc(Setmeal::getUpdateTime);

    setmealService.page(pageInfo,queryWrapper);
    //拷贝对象
    BeanUtils.copyProperties(pageInfo,dtoPage,"records");
    List<Setmeal> records = pageInfo.getRecords();

    List<SetmealDto> list=records.stream().map((item)->{
        SetmealDto setmealDto = new SetmealDto();
        //对象拷贝
        BeanUtils.copyProperties(item,setmealDto);
        //分类id
        Long categoryId = item.getCategoryId();
        //根据分类id查询分类对象
        Category category = categoryService.getById(categoryId);
        if(category != null){
            String categoryName = category.getName();
            setmealDto.setCategoryName(categoryName);
        }
        return setmealDto;
    }).collect(Collectors.toList());

    dtoPage.setRecords(list);
return R.success(dtoPage);

}

删除套餐

        在套餐管理列表页面点击删除按钮,可以删除对应的套餐信息。也可以通过复选框选择多个套餐,点击批量删除按钮一次删除多个套餐。注意,对于状态为售卖中的套餐不能删除,需要先停售,然后才能删除。

⚝ 请求URL:http://127.0.0.1:8080/setmeal                  

⚝ 请求方式:DELETE              

⚝ 参数:ids    

参数说明:ids:被删除的id之间用“,”间隔        

如:ids:1415580119015145474,1556280893128380418

service.SetmealService

/**
 * 删除套餐,同时需要删除套餐和菜品的关联数据
 * **/

public void removeWithDish(List<Long> ids);

SetmealServiceImpl

/**
 * 删除套餐,同时需要删除套餐和菜品的关联数据
 * **/
@Override
@Transactional
public void removeWithDish(List<Long> ids) {

    //查询套餐状态,确定是否可用删除(套餐的 status=1,表示套餐正在售卖,不能删除,如果非要删除需要停售套餐)
    //select count(*) from setmeal where id in (1,2,3) and status=1;
    LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.in(Setmeal::getId,ids);
    queryWrapper.eq(Setmeal::getStatus,1);

    int count = this.count(queryWrapper);
    if(count >0){
        //如果不能删除,抛出一个业务异常
        throw  new CustomException("套餐正在售卖中,删除失败");
    }
    //如果可以删除,先删除套餐表中的数据--setmeal
    this.removeByIds(ids);

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

controller.SetmealController

//删除与批量删除套餐
@DeleteMapping
public R<String> delete(@RequestParam List<Long> ids){
    log.info("ids: {}",ids);
    setmealService.removeWithDish(ids);

    return R.success("套餐数据删除成功");
}

我们进入数据库手动将字段status=0使其停售。以便测试删除功能。

以上就基础版的吉瑞外卖后台管理系统内容!!!

客户端系统

 QQ邮箱验证登录

        黑马程序员吉瑞外卖项目中,视频上移动端用户使用的是手机验证码登录,当是由于使用验证码登录需要申请挺繁琐的一个过程。视频为(79-87)级。这里我们感觉黑马程序员吉瑞项目所学的验证码登录,自己仿照了一个qq邮箱验证登录。

准备工作

代码开发-准备工作在开发业务功能前,先将需要用到的类和接口基本结构创建好:
实体类User (直接从课程资料中导入即可)
Mapper接口 UserMapper.
业务层接口UserService.
业务层实现类 UserServicelmpl.
控制层UserController工具类SMSUtils,ValidateCodeUtils(直接从课程资料中导入即可)

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

Mapper

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserMapper  extends BaseMapper<User> {
}
 
 

service


public interface UserService extends IService<User> {
}

UserServiceImpl

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper,User> implements UserService {
}

controller

@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {

    @Autowired
    private UserService userService;

}

        打开QQ邮箱--> 设置 --> 账号 -- > 开启服务:POP3/SMTP服务 然后在生成授权码,并将授权码保存下来 如图:

导入邮箱发送的MVEAN

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

修改application.yml  注意填写邮箱地址与授权码)

spring:
  application:
    # 应用的名称
    name: reggie_lxxd
***********************************
  mail:
    username: {qq邮箱地址}
    password: {生成的授权码}
    host: smtp.qq.com
    properties:
      mail:
        smtp:
          ssl:
            enable: true
***********************************

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

front/page/login.html在改文件中手机的提示信息自行修改,这里将不带着你们修改了:

将正则表达式中的手机规则改为邮箱的规则

methods:{
    getCode(){
************************************
const regex = /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/;
******************************

我们在filter.LoginCheckfilter过滤器中对验证码的发送界面和登录界面进行放行

/*直接放行的URL*/
String[] urls = new String[]{
        "/employee/login",
        "/employee/logout",
        "/backend/**",
        "/front/**",
        "/common/**",
*************************************
        "/user/sendMsg", //移动端发送短信地址
        "/user/login"    //移动端登录
**********************************

};
 
 // 4-1、判断登录状态(session是否含有employee的登录信息),如果已经登录,则直接放行
Long empId = (Long) request.getSession().getAttribute("employee");
if (empId != null) {
    log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("employee"));
    BaseContext.setCurrentId(empId);
    filterChain.doFilter(request,response);
    return;
}
**************************************
 // 4-2、判断前端用户登录状态(session是否含有employee的登录信息),如果已经登录,则直接放行
Long userId = (Long) request.getSession().getAttribute("user");
if (userId != null) {
    log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("user"));
    BaseContext.setCurrentId(userId);
    filterChain.doFilter(request,response);
    return;
}
********************************************************************
//5.如果未登录则返回未登录结果,通过输出流的方式向客户端页面响应JSON格式数据
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
return;
 

service.UserService

public R<String> sendMsg(String email,String code);

UserserviceImpl

@Autowired
private JavaMailSenderImpl mailSender;  //报红但可以用

@Value("${spring.mail.username}")
private String username;   // 邮件发送人

@Override
public R<String> sendMsg(String email, String code) {

    //发送到邮箱
    SimpleMailMessage mailMessage = new SimpleMailMessage();
    mailMessage.setSubject("尊敬的用户您好:");  //头文件
    mailMessage.setText("你的验证码为:"+code+";请勿泄露");  //内容
    mailMessage.setTo(email);   //接收者
    mailMessage.setFrom(username);  //发送者
    mailSender.send(mailMessage);

    return R.error("验证码发送失败,请稍后重试");
}
 

验证码发送:

⚝ 请求URL:http://localhost:8080/user/sendMsg

⚝ 请求方式:POST

⚝ 参数:phone

controller

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

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


            //发送到邮箱
            userService.sendMsg(phone,code);

            //存在浏览器
            session.setAttribute(phone,code);
            return R.success("邮箱验证码发送成功");
        }

        return R.error("短信发送失败");
    }

点击登录

⚝ 请求URL:http://localhost:8080/user/login

⚝ 请求方式:POST

⚝ 参数:code,phone

controller


    /**
     * 移动端用户登录
     */
    @PostMapping("/login")
    public R<User> login(@RequestBody Map map, HttpSession session){
        log.info(map.toString());

        //获取qq邮箱
        String phone = map.get("phone").toString();

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

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

        //进行验证码的比对(页面提交的验证码和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){
                //判断当前手机号对应的用户是否为新用户,如果是新用户就自动完成注册
                user = new User();
                user.setPhone(phone);
                user.setStatus(1);
                userService.save(user);
            }
            session.setAttribute("user",user.getId());
            return R.success(user);
        }
        return R.error("登录失败11");
    }

我们访问网址http://localhost:8080/front/page/login.html

因为这是手机界面,我们在浏览器开发中需要将浏览器调为手机开发模式,F12-左上角的手机图标,如图

        此时我们邮箱可以成功获取到验证码并用该验证码成功登录,我们也可以通过我们的日志查看邮箱与验证码,如图

导入用户地址簿

        地址簿,指的是移动端消费者用户的地址信息,用户登录成功后可以维护自己的地址信息。同一个用户可以有多个地址信息,但是只能有一个默认地址

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


    //是否删除
    private Integer isDeleted;
}
 
 

mapper

@Mapper
public interface AddressBookMapper extends BaseMapper<AddressBook> {
}
 

service

public interface AddressBookService extends IService<AddressBook> {
}

AddressBookServiceImpl

@Service
public class AddressBookServiceImpl extends ServiceImpl<AddressBookMapper,AddressBook> implements AddressBookService {

}
 

controller


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

    @Autowired
    private AddressBookService addressBookService;

    /**
     * 新增
     * @return
     */
    @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));
    }
}
 
 

        此时我们已经可以成功的读取地址,并将地址回显到页面上。

菜品展示

        用户登录成功后跳转到系统首页,在首页需要根据分类来展示菜品和套餐。如果菜品设置了口味信息,需要展示选择规格按钮,否则显示+按钮。

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

1、页面(front/index.html)发送ajax请求,获取分类数据(菜品分类和套餐分类)

2、页面发送ajax请求,获取第一个分类下的菜品或者套餐开发菜品展示功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。

注意:首页加载完成后,还发送了一次ajax请求用于加载购物车数据,此处可以将这次请求的地址暂时修改一下,从静态json文件获取数据,等后续开发购物车功能时再修改回来,如下:

front/api/main.js

//获取购物车内商品的集合
function cartListApi(data) {
    return $axios({
        //'url': '/shoppingCart/list',
        'url': '/front/cartData.json',
        'method': 'get',
        params:{...data}
    })
}
 

普通菜品展示

        我们在请求菜品信息的时候返回发数据为R<List<Dish>>,但是我们在客户端菜品信息获取中,要连相关的口味信息一起获取。所以我们要重写list方法使其返回待遇口味信息数据的信息。

⚝ 请求URL:http://localhost:8080/dish/list          

⚝ 请求方式:GET                  

⚝ 参数:categoryId ,status

先屏蔽原先写的

controller.DishController

/*@GetMapping("/list")
public R<List<DishDto>> list(Dish dish){
..........................
.....................
}*/

重新写该方法

controller.DishController

    @Autowired
    private DishFlavorService dishFlavorService;

@GetMapping("/list")
public R<List<DishDto>> list(Dish dish){
    //构造查询条件
    LambdaQueryWrapper<Dish> 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<Dish> list = dishService.list(queryWrapper);

    List<DishDto> 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<DishFlavor> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(DishFlavor::getDishId,dishId);
        //SQL:select * from dish_flavor where dish_id = ?
        List<DishFlavor> dishFlavorList = dishFlavorService.list(lambdaQueryWrapper);  //口味的集合
        dishDto.setFlavors(dishFlavorList);
        return dishDto;
    }).collect(Collectors.toList());

    return R.success(dishDtoList);
 }

菜品套餐展示

⚝ 请求URL:http://localhost:8080/setmeal/list            

⚝ 请求方式:GET                  

⚝ 参数:categoryId ,status

controller.SetmealController

/**
 * 根据条件查询套餐数据
 * */

@GetMapping("/list")
public R<List<Setmeal>> list(Setmeal setmeal){
    LambdaQueryWrapper<Setmeal> 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<Setmeal> list = setmealService.list(queryWrapper);
    return R.success(list);


}
 

此时菜品就可以成功展示在界面上供客户选择。

购物车

        需求分析移动端用户可以将菜品或者套餐添加到购物车。对于菜品来说,如果设置了口味信息,则需要选择规格后才能加入购物车;对于套餐来说,可以直接点击+将当前套餐加入购物车。在购物车中可以修改菜品和套餐的数量,也可以清空购物车。

对应表:shopping_cart    

购物车
      代码开发-梳理交互过程在开发代码之前,需要梳理一下购物车操作时前端页面和服务端的交互过程:
1、点击加入购物车或者+按钮,页面发送ajax请求,请求服务端,将菜品或者套餐添加到购物车2、点击购物车图标,页面发送ajax请求,请求服务端查询购物车中的菜品和套餐
3、点击清空购物车按钮,页面发送ajax请求,请求服务端来执行清空购物车操作
开发购物车功能,其实就是在服务端编写代码去处理前端页面发送的这3次请求即可。

准备工作

 entity

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

mapper

@Mapper
public interface ShoppingCartMapper extends BaseMapper<ShoppingCart> {
}

service

public interface ShoppingCartService extends IService<ShoppingCart> {
}
 

ShoppingCartServiceImpl

@Service
public class ShoppingCartServiceImpl extends ServiceImpl<ShoppingCartMapper, ShoppingCart> implements ShoppingCartServicer {
}
 

controller

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

    @Autowired
    private ShoppingCartService shoppingCartService;
}

添加到购物车

⚝ 请求URL: http://localhost:8080/shoppingCart/add            

⚝ 请求方式:POST          

⚝ 如果为菜品:参数:amount,dishFlavor,dishId,image,name          

⚝ 如果为套餐:参数:amount,image,name,setmealId

controller.ShoppingCartController

@PostMapping("/add")
public R<ShoppingCart> add(@RequestBody ShoppingCart shoppingCart){
    log.info("购物车数据:{}",shoppingCart);

    //设置用户id,指定当前是哪个用户的购物车数据
    Long currentId = BaseContext.getCurrentId();
    shoppingCart.setUserId(currentId);

    Long dishId = shoppingCart.getDishId();

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

    queryWrapper.eq(ShoppingCart::getUserId,currentId);           //将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);
        shoppingCart.setCreateTime(LocalDateTime.now());
        shoppingCartService.save(shoppingCart);
        cartServiceOne = shoppingCart;
    }

    return R.success(cartServiceOne);
}
 

查看购物车

我们需要刚刚获取购物车内商品集合的ajax请求恢复  如下:

front/api/main.js

/获取购物车内商品的集合
function cartListApi(data) {
    return $axios({
        'url': '/shoppingCart/list',
        //'url': '/front/cartData.json',
        'method': 'get',
        params:{...data}
    })
}
 

⚝ 请求URL: http://localhost:8080/shoppingCart/list      

⚝ 请求方式:GET                                                          

⚝ 参数:无   (这里通过浏览器存储的用户id来查询数据库获取该用户下的订单数据)

controller.ShoppingCartController

/**
 * 查看购物车
 * **/

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

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

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

    return R.success(list);
}

清空购物车

⚝ 请求URL: http://localhost:8080/shoppingCart/clean          

⚝ 请求方式:DELETE                                                      

⚝ 参数:无

controller.ShoppingCartController

@DeleteMapping("/clean")
    public R<String> clean(){
        log.info("清空购物车");
        LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());
        shoppingCartService.remove(queryWrapper);
        return R.success("清空购物车成功");
    }

此时使用清空按钮可以看到数据库中对应的数据被跟着清除了。

用户下单

        移动端用户将菜品或者套餐加入购物车后,可以点击购物车中的去结算按钮,页面跳转到订单确认页面,点击去支付按钮则完成下单操作。这里由于真正的付款功能需要企业申请,个人很难申请,所以这里只是一个模拟。

z订单表:orders         

订单明细表:orders_detail

在开发代码之前,需要梳理一下用户下单操作时前端页面和服务端的交互过程:
    1、在购物车中点击去结算按钮,页面跳转到订单确认页面
    2、在订单确认页面,发送ajax请求,请求服务端获取当前登录用户的默认地址
    3、在订单确认页面,发送ajax请求,请求服务端获取当前登录用户的购物车数据
    4、在订单确认页面点击去支付按钮,发送ajax请求,请求服务端完成下单操作
 
开发用户下单功能,其实就是在服务端编写代码去处理前端页面发送的请求即可。

准备工作

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

mapper

@Mapper
public interface OrdersMapper extends BaseMapper<Orders> {
}
 
@Mapper
public interface OrderDetailMapper extends BaseMapper<OrderDetail> {

}

service

public interface OrdersService extends IService<Orders> {
}
public interface OrderDetailService extends IService<OrderDetail> {

}

service.impl

@Service
public class OrdersServiceImpl extends ServiceImpl<OrdersMapper, Orders> implements OrdersService {
}
 
 
@Service
public class OrderDetailServiceImpl extends ServiceImpl<OrderDetailMapper, OrderDetail> implements OrderDetailService {

}
 

controller

@Slf4j
@RestController
@RequestMapping("order")
public class OrderController {
    @Autowired
    private OrderService orderService;
    
}
 
 

⚝ 请求URL: http://localhost:8080/order/submit          

⚝ 请求方式:POST              

⚝ 参数:addressBookId,payMethod,remark

service.OrderService

public void submit(Orders orders);
 

OrderServiceImpl

@Autowired
private ShoppingCartService shoppingCartService;

@Autowired
private UserService userService;

@Autowired
private AddressBookService addressBookService;

@Autowired
private OrderDetailService orderDetailService;


@Transactional
public void submit(Orders orders) {
    //获得当前用户id
    Long userId = BaseContext.getCurrentId();

    //查询当前用户的购物车数据
    LambdaQueryWrapper<ShoppingCart> wrapper = new LambdaQueryWrapper<>();
    wrapper.eq(ShoppingCart::getUserId,userId);
    List<ShoppingCart> 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("用户地址信息有误,不能下单");
    }

    long orderId = IdWorker.getId();//订单号

    AtomicInteger amount = new AtomicInteger(0);

    //计算总金额,插入订单明显
    List<OrderDetail> 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()));//总金额
    orders.setUserId(userId);
    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);
}
 

controller.OrderController

@Autowired
private OrderService orderService;

@PostMapping("/submit")
public R<String> submit(@RequestBody Orders orders){
    log.info("订单数据:{}",orders);
    orderService.submit(orders);
    return R.success("下单成功");
}
 

此时我们点击去支付按钮后能正确的跳转到下单成功界面。

至此,移动端的基本开发也到此结束。

项目优化

使用Gitee管理项目的版本与redis作为缓存技术缓存后期会做补充

介绍一下前后端开发,了解一个项目的开发过程以及代码规范。重点注重后端

        这里写了份吉瑞外卖的思维导图笔记,方便大家复习。如果有需要可以私聊我。会在后面更新完这个吉瑞外卖项目后将笔记分享给大家

 

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐