【项目】瑞吉外卖

目录

业务开发Day1
业务开发Day2
业务开发Day3
业务开发Day4
业务开发Day5
业务开发Day6

业务开发Day1

01-本章内容介绍

项目效果展示

登录界面
请添加图片描述

登录成功界面
请添加图片描述

管理界面展示

  • 员工管理
  • 分类管理
  • 菜品管理
  • 套餐管理
  • 订单明细

请添加图片描述

前端开发使用HTML5技术(自适应屏幕大小功能)

请添加图片描述

目录

  • 软件开发整体介绍
  • 瑞吉外卖项目介绍
  • 开发环境搭建

02-软件开发整体介绍

  • 软件开发流程
  • 角色分工
  • 软件环境

软件开发流程

需求分析->设计->编码->测试->上线运维
请添加图片描述

角色分工

  • 项目经理
  • 产品经理
  • UI设计师
  • 架构师
  • 开发工程师
  • 测试工程师
  • 运维工程师
    请添加图片描述

软件环境

  • 开发环境
  • 测试环境
  • 生产环境

请添加图片描述

03-瑞吉外卖项目整体介绍

目录

  • 项目介绍
  • 产品原型展示
  • 技术选型
  • 功能架构
  • 角色
    请添加图片描述

项目介绍

本项目是专门为餐饮企业(餐厅、饭店)定制的一款软件产品,包括系统管理后台和移动端应用两部分。

  • 系统管理后台主要提功给餐饮企业内部员工使用(功能:对餐厅的菜品、套餐、订单等进行管理维护等)
  • 移动端应用主要提供给消费者使用(功能:在线浏览菜品、添加购物车、下单等)

请添加图片描述

产品原型展示

在这里插入图片描述

技术选型

  • 用户层
  • 网关层
  • 应用层
  • 数据层
  • 以及使用到的工具
    在这里插入图片描述

功能架构

  • 移动端前台
  • 系统管理后台
    请添加图片描述

角色

  • 后台系统管理员
  • 后台系统普通员工
  • C端用户
    在这里插入图片描述

04-开发环境搭建-数据库环境搭建

  1. 使用navicat(数据库可视化界面)创建对应的数据库,数据库名:reggie,字符集:utf8mb4
    在这里插入图片描述
  2. 操作步骤
  • 第一步:右键点击数据库再点击运行sql文件
  • 第二步:选择资料下载的位置,我的资料存储在D:\瑞吉外卖\资料\数据模型\db_reggie.sql,点击开始即可
  • 第三步:打开表,即可查看数据库中执行完sql文件的所有信息
    在这里插入图片描述

执行完sql文件对应的对象信息
在这里插入图片描述

05-开发环境搭建-maven项目环境搭建

创建maven项目

在这里插入图片描述

  • 点击next
    在这里插入图片描述
  • 填写好项目相关信息后,点击finish
    在这里插入图片描述

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

<groupId>com.itzq</groupId>
<artifactId>reggie_take_out</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- lookup parent from repository -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.5</version>
<relativePath/>
</parent>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</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>
<!-- mybatis依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
<!-- 将数据转为json对象 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
<!-- common-lang -->
<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>
<!-- druid数据库连接池 -->
<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.4.5</version>
</plugin>
</plugins>
</build>
</project>
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384

写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: root
password: root
mybatis-plus:
configuration:
##在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
##主键策略:ASSIGN_ID(雪花算法)
id-type: ASSIGN_ID
12345678910111213141516171819202122

主启动

在java下新建主启动类,带上包名
在这里插入图片描述

package com.itzq.reggie;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@Slf4j
@SpringBootApplication
public class ReggieApplication {
public static void main(String[] args) {
SpringApplication.run(ReggieApplication.class,args);
log.info("项目启动成功。。。");
}
}
1234567891011121314

将前端资源放入resources下

在磁盘中找到前端资源下的目录
在这里插入图片描述

将目录放入resources下
在这里插入图片描述

启动工程

项目启动成功
在这里插入图片描述

访问路径 http://localhost:8080/backend/index.html ,出现无法访问,默认情况下我们只能访问static、template静态目录下的静态资源,此时我们可以通过配置类的方式来设置静态资源映射
在这里插入图片描述

配置类设置静态资源映射

在这里插入图片描述

通过继承WebMvcConfigurationSupport,重写addResourceHandlers方法来实现我们想要的功能

package com.itzq.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/");
}
}
12345678910111213141516171819202122232425

重新启动工程!

浏览器访问地址:http://localhost:8080/backend/index.html,出现以下界面即配置成功,页面中请求出错是因为前端发送了一个ajax请求,而后端还没有写东西所以页面提示请求出错
在这里插入图片描述

06-后台系统登录功能-需求分析

需求分析

登录页面展示

访问路径:http://localhost:8080/backend/page/login/login.html
在这里插入图片描述

数据库的密码通过MD5加密了,它的明文密码为123456
在这里插入图片描述

输入正确的用户名和密码点击登录,按住f12,寻找到以下页面,可以看到请求路径跳转到地址为:localhost:8080/employee/login的页面,报404错误,因为后台系统还没有响应此请求的处理器,我们需要创建相关类来处理登录请求
在这里插入图片描述

07-后台系统登录功能-代码开发(创建controller,service,mapper,实体类

代码开发

创建controller,service,mapper,实体类

  1. 在reggie包下分别创建controller,service(在此包下再创建一个impl包),mapper,entity包
  2. 在entity包下创建Employee类
package com.itzq.reggie.entity;

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

@Data
public class Employee implements Serializable {

private static final long serialVersionUID = 1L;

private Long id;

private String username;

private String name;

private String password;

private String phone;

private String sex;

private String idNumber;

private Integer status;

private LocalDateTime createTime;

private LocalDateTime updateTime;

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

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

}
12345678910111213141516171819202122232425262728293031323334353637383940
  1. 在mapper包下创建EmployeeMapper接口,并继承BaseMapper,添加@Mapper注解在该接口上
package com.itzq.reggie.mapper;

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

@Mapper
public interface EmployeeMapper extends BaseMapper<Employee> {
}

12345678910
  1. 在service包下编写EmployeeService接口,并继承IService
package com.itzq.reggie.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.itzq.reggie.entity.Employee;

public interface EmployeeService extends IService<Employee> {
}

12345678
  1. 在Impl包下编写EmployeeServiceImpl类,并继承ServiceImpl类,实现EmployeeService接口
package com.itzq.reggie.service.Impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itzq.reggie.entity.Employee;
import com.itzq.reggie.mapper.EmployeeMapper;
import com.itzq.reggie.service.EmployeeService;
import org.springframework.stereotype.Service;

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

123456789101112
  1. 在controller包下编写EmployeeController类
package com.itzq.reggie.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

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

@Autowired
private EmployeeService employeeService;

}

1234567891011121314151617

08-后台系统登录功能-代码开发(导入通用返回结果类

导入通用结果类

在这里插入图片描述

在reggie包下创建一个子包为common,再在common包下创建一个R类

package com.itzq.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;
}

}

123456789101112131415161718192021222324252627282930313233343536373839404142

09-后台系统登录功能-代码开发(梳理登录方法处理逻辑

代码开发以及梳理

给EmployeeController类添加一个login方法

  • @RequestBody 主要用于接收前端传递给后端的json字符串(请求体中的数据)
  • HttpServletRequest request作用:如果登录成功,将员工对应的id存到session一份,这样想获取一份登录用户的信息就可以随时获取出来
package com.itzq.reggie.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.itzq.reggie.common.R;
import com.itzq.reggie.entity.Employee;
import com.itzq.reggie.service.EmployeeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.DigestUtils;
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
* @return
*/
//@RequestBody 主要用于接收前端传递给后端的json字符串(请求体中的数据)
//HttpServletRequest request作用:如果登录成功,将员工对应的id存到session一份,这样想获取一份登录用户的信息就可以随时获取出来
@PostMapping("/login")
public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee){
return null;
}
}

123456789101112131415161718192021222324252627282930313233343536373839

在controller中创建登录方法

  1. 将页面提交的密码进行MD5加密处理
  2. 根据页面提交的用户名username查询数据库
  3. 如果没有查询到则返回登录失败结果
  4. 密码比对,如果不一致则返回登录结果
  5. 查看员工状态,如果已为禁用状态,则返回员工已禁用结果
  6. 登录成功,将员工id存入session并返回登录成功结果
    在这里插入图片描述

10-后台系统登录功能-代码开发(实现登录处理逻辑

编写代码实现逻辑

在EmployeeController类的login方法中添加代码实现登录处理逻辑

package com.itzq.reggie.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.itzq.reggie.common.R;
import com.itzq.reggie.entity.Employee;
import com.itzq.reggie.service.EmployeeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.DigestUtils;
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
* @return
*/
//@RequestBody 主要用于接收前端传递给后端的json字符串(请求体中的数据)
//HttpServletRequest request作用:如果登录成功,将员工对应的id存到session一份,这样想获取一份登录用户的信息就可以随时获取出来
@PostMapping("/login")
public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee){
//1.将页面提交的密码进行MD5加密处理
String password = employee.getPassword();
password= DigestUtils.md5DigestAsHex(password.getBytes());

//2.根据页面提交的用户名username查询数据库
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Employee::getUsername,employee.getUsername());
Employee emp = employeeService.getOne(queryWrapper); //使用getOne:因为user_name字段有unique唯一约束,不会出现查询出多个结果

//3.如果没有查询到则返回登录失败结果
if (emp == null){
return R.error("登录失败"); //因为error为静态方法,所以可以在该类中直接调用
}

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

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

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

}

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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778

11-后台系统登录功能-代码开发(功能测试

测试

通过debug方式启动项目,在箭头指向位置添加断点
请添加图片描述
在浏览器上访问地址:http://localhost:8080/backend/page/login/login.html

  • 输入正确的用户名和密码点击登录
  • 在debug调试期间用时较长,前端在10s内得不到响应则会抛出异常,
  • 为了我们可以在后端进行长时间的调试,我们需要重新设置前端页面的响应超时时间找到request.js
    在这里插入图片描述

将timeout后面的数据多添加两个0,并且清除浏览器带有的缓存
在这里插入图片描述

清除浏览器缓存
在这里插入图片描述

测试

  1. 输入错误的username,页面返回登录失败的信息
  2. 输入错误的密码,页面返回登录失败的信息
  3. 在数据库中将员工的status改为0,表示该员工处于被禁用状态,页面返回登录失败的信息

12-后台系统退出功能_需求分析&代码开发&功能测试

功能测试

在这里插入图片描述

在EmployeeController类中添加logout方法

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

登录成功时
在这里插入图片描述

退出登录后
在这里插入图片描述

13-分析后台系统首页构成和效果展示方式

展示效果以及分析

在这里插入图片描述

在index.html页面下的menuList作为数据的准备

在这里插入图片描述

遍历menuList的代码块,里面各个标注对应的重要含义

  1. 遍历menuList
  2. v-if,根据提供的menuList可知并不满足条件
  3. 标签名:通过name属性定义
  4. 如果v-if不满足条件,则通过v-else
  5. 点击菜单,会执行menuHandle方法
    在这里插入图片描述

menuHandle方法最重要的是红框的语句
在这里插入图片描述

定义了一个iframe,用于展示另一个页面,这个页面从哪来?传给我什么数据,我就展示什么数据
在这里插入图片描述

那为什么登录成功后就是员工管理界面,因为在html中设置了初始值,当我们在点击菜单的时候,其实就是在切换url,展示一个新的界面
在html页面设置的初始界面
在这里插入图片描述

业务开发Day2

01-本章内容介绍

目录

在这里插入图片描述

针对员工这张表进行对数据的维护
在这里插入图片描述
点击添加员工后呈现的页面
在这里插入图片描述

在浏览器地址栏中输入地址:http://localhost:8080/backend/index.html ,在没有登录的情况下也可以进入管理界面,但最终我们想看到的效果是没有登录,就跳转到登录界面来,登录成功之后才能进入到管理界面
在这里插入图片描述

02-完善登录功能_问题分析并创建过滤器

问题分析

  • 前面我们已经完成了后台系统的员工登录功能开发,但是还存在一个问题:用户如果不登录,直接访问系统首页面,照样可以正常访问。
  • 这种设计并不合理,我们希望看到的效果应该是,只有登录成功后才可以访问系统中的页面,如果没有登录则跳转到登录页面。
  • 那么,具体应该怎么实现呢? 答案就是使用过滤器或者拦截器,在过滤器或者拦截器中判断用户是否已经完成登录,如果没有登录则跳转到登录页面。

代码实现

  1. 创建自定义过滤器LoginCheckFilter
package com.itzq.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;

@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
@Slf4j
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);
}
}

123456789101112131415161718192021222324
  1. 在启动类上加入注解@ServletComponentScan
package com.itzq.reggie;

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

@Slf4j
@SpringBootApplication
@ServletComponentScan
public class ReggieApplication {
public static void main(String[] args) {
SpringApplication.run(ReggieApplication.class,args);
log.info("项目启动成功。。。");
}
}
12345678910111213141516
  1. 完善过滤器的处理逻辑
    完善过滤器内容在下一章节

测试

在浏览器上访问:http://localhost:8080/backend/index.html ,控制台的呈现
在这里插入图片描述

03-完善登录功能_代码开发

处理逻辑

  1. 获取本次请求的URI
  2. .判断本次请求是否需要处理
  3. 如果不需要处理,则直接放行
  4. 判断登录状态,如果已登录,则直接放行
  5. 如果未登录则返回未登录结果
    在这里插入图片描述

完善LoginCheckFilter过滤器代码

package com.itzq.reggie.filter;

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

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

@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter implements Filter {

//路径匹配
public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();

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

//强转
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;

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

//定义不需要处理的请求
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;

}

public boolean check(String[] urls, String requestURI){
for (String url : urls) {
boolean match = PATH_MATCHER.match(url, requestURI);
if (match) {
//匹配
return true;
}
}
//不匹配
return false;
}
}

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172

可以在index.html页面找到红方框下的信息
请添加图片描述
寻找到request.js代码中的响应拦截器部分

  1. 服务端若传递的code=0,msg=NOTLOGIN
  2. 则返回到登录页面
    请添加图片描述

04-完善登录功能_功能测试

在LoginCheckFilter类中加入日志

作用:方便观察

package com.itzq.reggie.filter;

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

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

@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter implements Filter {

//路径匹配
public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();

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

//强转
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;

//1.获取本次请求的URI
String requestURI = request.getRequestURI();
log.info("拦截到的请求:{}",requestURI);

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

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

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

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

//5.如果未登录则返回未登录结果,通过输出流方式向客户端页面响应数据
log.info("用户未登录");
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
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;
}
}

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576

测试

  1. 启动项目
  2. 在浏览器的地址栏中输入:http://localhost:8080/backend/index.html ,会发现跳转到了登录界面
  3. 控制台打印日志
    在这里插入图片描述
  4. 在浏览器的地址栏中输入:http://localhost:8080/backend/page/login/login.html ,点击登录
  5. 控制台打印日志
    在这里插入图片描述

05-新增员工_需求分析和数据模型

需求分析

  • 新增员工,其实就是将我们新增页面录入的员工数据插入到employee表。
    在这里插入图片描述
  • 需要注意,employee表中对username字段加入了唯一约束,因为username是员工的登录账号,必须是唯一的
    在这里插入图片描述
  • employee表中的status字段已经设置了默认值1,表示状态正常。
    在这里插入图片描述

06-新增员工_梳理程序执行流程

在开发代码之前,需要梳理一下整个程序的执行过程:

  1. 页面发送ajax请求,将新增员工页面中输入的数据以json的形式提交到服务端
  2. 服务端Controller接收页面提交的数据并调用Service将数据进行保存
  3. Service调用Mapper操作数据库,保存数据
    在这里插入图片描述
    在这里插入图片描述

前端js代码对页面进行校验(validate)
在这里插入图片描述

07-新增员工_代码开发和功能测试

代码开发和功能测试

在EmployeeController类中添加save方法用于将前端传来的json数据保存到数据库中

 @PostMapping
public R<String> save(@RequestBody Employee employee){
log.info("新增员工的信息:{}",employee.toString());
return R.success("添加员工成功");
}
12345

启动项目后,输入员工信息,点击保存
请添加图片描述
保存后,可以看到控制台上打印的数据,表示可以接收到前端传递到服务端的数据
请添加图片描述

在save方法中新增业务逻辑类代码,实现将数据存入数据库操作

	@PostMapping
public R<String> save(HttpServletRequest request, @RequestBody Employee employee){
log.info("新增员工的信息:{}",employee.toString());
//设置初始密码,需要进行md5加密
employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));

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

//强转为long类型
Long empID = (Long)request.getSession().getAttribute("employee");

employee.setCreateUser(empID);
employee.setUpdateUser(empID);

employeeService.save(employee);
return R.success("添加员工成功");
}
123456789101112131415161718

重启项目,查看employee表中的内容
请添加图片描述
输入信息,点击保存
请添加图片描述
再次查看employee表中数据库内容,发现新增加了我们添加的员工信息请添加图片描述
再次添加相同的username(员工账号)
请添加图片描述
出现异常,因为在上面已经说过username(员工账号)的索引类型为unique,所以不能添加数据库中已存在的username
请添加图片描述

08-新增员工_编写全局异常处理器

全局异常处理器

前面的程序还存在一个问题,就是当我们在新增员工时输入的账号已经存在,由于employee表中对该字段加入了唯一约束,此时程序会抛出异常:
java.sql. SQLIntegrityConstraintViolationException: Duplicate entry ‘zhangsan’for key ‘idx_username

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

  1. 在Controller方法中加入try、catch进行异常捕获
  2. 使用异常处理器进行全局异常捕获(推荐使用第二种方式)

在common包下,建立GlobalExceptionHandler类,并添加exceptionHandler方法用来捕获异常,并返回结果

package com.itzq.reggie.common;

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 //java对象转为json格式的数据
@Slf4j
public class GlobalExceptionHandler {

@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R<String> exceptionHandler (SQLIntegrityConstraintViolationException exception){
log.error(exception.getMessage());
return R.error("failed");
}
}
12345678910111213141516171819202122232425

启动项目,输入数据库已存在的username员工信息请添加图片描述
页面上弹出方框中文字,表示方法成功执行请添加图片描述

09-新增员工_完善全局异常处理器并测试

完善全局异常处理器并测试

完善GlobalExceptionHandler类中的exceptionHandler方法

package com.itzq.reggie.common;

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 //java对象转为json格式的数据
@Slf4j
public class GlobalExceptionHandler {

@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R<String> exceptionHandler (SQLIntegrityConstraintViolationException exception){
log.error(exception.getMessage());
if (exception.getMessage().contains("Duplicate entry")){
String[] split = exception.getMessage().split(" ");
String msg = split[2] + "已存在";
return R.error(msg);
}
return R.error("unknown error");
}
}

12345678910111213141516171819202122232425262728293031

重启项目,输入数据库已存在的username员工信息
请添加图片描述
页面上弹出方框中文字,表示方法成功执行
请添加图片描述

10-新增员工_小节

总结

  1. 根据产品原型明确业务需求
  2. 重点分析数据的流转过程和数据格式
  3. 通过debug断点调试跟踪程序执行过程

11-员工信息分页查询_需求分析

需求分析

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

12-员工信息分页查询_梳理程序执行流程

程序执行流程

在开发代码之前,需要梳理一下整个程序的执行过程:

  1. 页面发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端
  2. 服务端Controller接收页面提交的数据并调用Service查询数据
  3. Service调用Mapper操作数据库,查询分页数据
  4. Controller将查询到的分页数据响应给页面
  5. 页面接收到分页数据并通过ElementUI的Table组件展示到页面上

13-员工信息分页查询_代码开发1

配置MP分页插件

package com.itzq.reggie.config;

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

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

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

123456789101112131415161718192021

分析前端代码

返回值携带records和total
在这里插入图片描述
点击员工管理,前端页面发送的请求
在这里插入图片描述
在搜索框中输入员工姓名,前端页面发送的请求
在这里插入图片描述

相关代码

在EmployeeController类中,编写page方法

    @GetMapping("/page")
public R<Page> page(int page,int pageSize,int name){
log.info("page = {}, pageSize = {}, name = {}",page,pageSize,name);
return null;
}

123456

点击员工管理,控制台输出的数据
在这里插入图片描述
在搜索框中添加数据,点击搜索,控制台输出的数据
在这里插入图片描述

14-员工信息分页查询_代码开发2

代码中添加功能

在page方法中添加代码实现分页查询功能

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

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

//添加排序添加
queryWrapper.orderByDesc(Employee::getUpdateTime);

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

return R.success(pageInfo);
}
123456789101112131415161718192021

15-员工信息分页查询_功能测试

测试

  1. 启动项目
  2. 登录管理界面 http://localhost:8080/backend/page/login/login.html
  3. 将会呈现以下界面
    在这里插入图片描述
    搜索框中输入:张
    在这里插入图片描述
    因为数据量有限,所以修改前端代码为每页两条数据,共两页
    在list.html修改前端代码
    在这里插入图片描述
    刷新后的页面
    在这里插入图片描述

16-员工信息分页查询_补充说明

说明

为什么后端传给页面的status数据为Integer类型,到页面展示效果的时候显示的是已禁用或者正常?
请添加图片描述
查看到前端list.html页面中的逻辑代码
请添加图片描述
由以上前端代码所表示的逻辑可知,当status为1时表示正常,为0时表示已禁用
请添加图片描述

17-启用、禁用员工账号_需求分析

需求分析

  1. 在员工管理列表页面,可以对某个员工账号进行启用或者禁用操作。账号禁用的员工不能登录系统,启用后的员工可以正常登录。
  2. 需要注意,只有管理员(admin用户)可以对其他普通用户进行启用、禁用操作,所以普通用户登录系统后启用、禁用按钮不显示。
  3. 管理员admin登录系统可以对所有员工账号进行启用、禁用操作。
  4. 如果某个员工账号状态为正常,则按钮显示为“禁用”,如果员工账号状态为已禁用,则按钮显示为“启用”
    在这里插入图片描述

18-启用、禁用员工账号_分析页面按钮动态显示效果

分析

页面中是怎样做到只有管理员(admin)才能看到启用,禁用按钮?

  • 非管理员看不见该按钮
    在这里插入图片描述
    从userInfo中取出username,并赋值给user模型数据
    在这里插入图片描述
    判断模型数据user的值是否等于admin,若等于则显示按钮
    在这里插入图片描述

19-启用、禁用员工账号_分析页面ajax请求发送过程

分析

在开发代码之前,需要梳理一下整个程序的执行过程:

  1. 页面发送ajax请求,将参数(id、status)提交到服务端
  2. 服务端Controller接收页面提交的数据并调用Service更新数据
  3. Service调用Mapper操作数据库

点击禁用或启用按钮会调用以下方法,该方法会调用enable0rDisableEmployee方法,该方法封装到一个js文件当中,来发送ajax请求
请添加图片描述
enable0rDisableEmployee方法
在这里插入图片描述

20-启用、禁用员工账号_代码开发和功能测试

代码开发和功能测试

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

在EmployeeController类中添加update方法

@PutMapping
public R<String> update(@RequestBody Employee employee){
log.info(employee.toString());
return null;
}

123456

启动项目,控制台上输出日志信息请添加图片描述
测试成功

完善update方法的代码逻辑

    @PutMapping
public R<String> update(HttpServletRequest request, @RequestBody Employee employee){
log.info(employee.toString());

Long empID = (Long)request.getSession().getAttribute("employee");
employee.setUpdateTime(LocalDateTime.now());
employee.setUpdateUser(empID);
employeeService.updateById(employee);
return R.success("员工修改信息成功");
}
12345678910

重启项目,点击禁用账号为zhangsi的信息员工信息
请添加图片描述
页面提示
请添加图片描述
查看数据表中的信息,发现status字段的值并没有发生改变
请添加图片描述
测试过程中没有报错,但是功能并没有实现,查看数据库中的数据也没有变化。观察控制台输出的SQL
在这里插入图片描述
SQL执行的结果是更新的数据行数为0,仔细观察id的值,和数据库中对应记录的id值并不相同

问题的原因

  • 即js对long型数据进行处理时丢失精度,导致提交的id和数据库中的id不一致。
    如何解决这个问题?
  • 我们可以在服务端给页面响应json数据时进行处理,将long型数据统一转为String字符串

21-启用、禁用员工账号_代码修复配置状态转换器

配置状态转换器

具体实现步骤:

  1. 提供对象转换器Jackson0bjectMapper,基于Jackson进行Java对象到json数据的转换(资料中已经提供,直接复制到项目中使用)
  2. 在WebMcConfig配置类中扩展Spring mvc的消息转换器,在此消息转换器中使用提供的对象转换器进行Java对象到json数据的转换

配置对象映射器JacksonObjectMapper ,继承ObjectMapper

package com.itzq.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);
}
}

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455

扩展mvc框架的消息转换器

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

//创建消息转换器对象
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
//设置对象转化器,底层使用jackson将java对象转为json
messageConverter.setObjectMapper(new JacksonObjectMapper());
//将上面的消息转换器对象追加到mvc框架的转换器集合当中(index设置为0,表示设置在第一个位置,避免被其它转换器接收,从而达不到想要的功能)
converters.add(0,messageConverter);

}
123456789101112131415

22-启用、禁用员工账号_再次测试

测试

启动项目,点击禁用张四
在这里插入图片描述
数据库中的status字段数据发生了改变,即成功
请添加图片描述

23-编辑员工信息_需求分析和梳理程序执行流程

需求分析

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

流程

在开发代码之前需要梳理一下操作过程和对应的程序的执行流程:

  1. 点击编辑按钮时,页面跳转到add.html,并在url中携带参数[员工id]
  2. 在add.html页面获取url中的参数[员工id]
  3. 发送ajax请求,请求服务端,同时提交员工id参数
  4. 服务端接收请求,根据员工id查询员工信息,将员工信息以json形式响应给页面
  5. 页面接收服务端响应的json数据,通过VUE的数据绑定进行员工信息回显
  6. 点击保存按钮,发送ajax请求,将页面中的员工信息以json方式提交给服务端
  7. 服务端接收员工信息,并进行处理,完成后给页面响应
  8. 页面接收到服务端响应信息后进行相应处理

24-编辑员工信息_页面效果分析和代码开发

页面分析

调用requestUrlParam方法获取id的值
在这里插入图片描述
requestUrlParam方法的具体实现
在这里插入图片描述
取出浏览器中id值,前端调用queryEmployeeById方法,向服务器发送ajax请求,查询包含该id的所有信息,若code为1,则表示在数据库中查询到该id所包含的员工信息,反之则为null
在这里插入图片描述

代码开发

在EmployeeController类中添加方法getById

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

log.info("根据id查询员工信息。。。");
Employee employee = employeeService.getById(id);
if (employee != null){
return R.success(employee);
}
return R.error("没有查询到该员工信息");
}
12345678910

25-编辑员工信息_功能测试

测试

启动项目,点击任意一个员工的编辑按钮,按住f12,观察浏览器发送的请求
在这里插入图片描述
页面数据回显成功
在这里插入图片描述
对性别(radio)的简单处理
因为在页面上的显示效果为男、女,而服务端传递过来的数据为1、0所以我们要将数据进行简单的处理
在这里插入图片描述

业务开发Day3

01-本章内容介绍

内容

  1. 公共字段自动填充
  2. 新增分类
  3. 分类信息分页查询
  4. 删除分类
  5. 修改分类

02-公共字段自动填充-内容分析

问题分析

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

表中公共字段
在这里插入图片描述

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

答案就是使用**Mybatis Plus**提供的公共字段自动填充功能。
1

03-公共字段自动填充-代码实现并测试

代码实现

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

实现步骤:

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

在公共属性上添加@TableField注解

  1. @TableField(fill = FieldFill.INSERT),表示插入时填充字段
  2. @TableField(fill = FieldFill.INSERT_UPDATE),表示插入和更新时填充字段
    在这里插入图片描述

在common包下,自定义元数据对象处理器(测试版)

package com.itzq.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;

@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {

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

@Override
public void updateFill(MetaObject metaObject) {
log.info("公共字段自动填充【update】");
log.info(metaObject.toString());
}
}
1234567891011121314151617181920212223

在该行添加断点,debug方式启动项目
在这里插入图片描述

来到修改员工界面,不需要修改数据,点击保存
在这里插入图片描述

发现前端传到服务端的数据,updateUser与updateTime数据在controller中的对应方法中被修改
在这里插入图片描述

注释掉save方法的部分代码
在这里插入图片描述

修改MyMetaObjectHandler类中部分代码

  • 先将createUser,updateUser属性的初始值设置为1,后面会讲解如何获取当前user
package com.itzq.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 {

@Override
public void insertFill(MetaObject metaObject) {
log.info("公共字段自动填充【insert】");
log.info(metaObject.toString());
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】");
log.info(metaObject.toString());
}
}

123456789101112131415161718192021222324252627282930

在insertFill方法上添加断点
在这里插入图片描述

debug方式重启项目

来到添加员工页面,输入数据,点击保存
在这里插入图片描述

断点走到insertFill方法末尾时,将初始为null的四个公共属性赋值成功
在这里插入图片描述

修改员工信息时也需要填充

对应代码

	@Override
public void updateFill(MetaObject metaObject) {
log.info("公共字段自动填充【update】");
log.info(metaObject.toString());
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("updateUser", new Long(1));
}
1234567

注释掉处理公共属性的代码
在这里插入图片描述

在该行添加断点,并以debug方式重启项目
在这里插入图片描述
在这里插入图片描述

断点在updateFill方法执行之前
在这里插入图片描述

断点在updateFill方法执行之后,发现数据被修改,表示成功
在这里插入图片描述

04-公共字段自动填充-功能完善

功能完善

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

  1. LoginCheckFilter的doFilter方法
  2. EmployeeController的update方法
  3. MyMetaObjectHandler的updateFill方法

可以在上面的三个方法中分别加入下面代码(获取当前线程id) :
\4. long id = Thread.currentThread().getId();
\5. log.info(“MyMetaObjectHandler线程id为:{}”,id);

LoginCheckFilter的doFilter方法
请添加图片描述

EmployeeController的update方法
在这里插入图片描述

MyMetaObjectHandler的updateFill方法
在这里插入图片描述

启动项目,在修改员工的页面中,点击保存
在这里插入图片描述

控制台上显示三条日志信息,表明类中的方法都属于相同的一个线程
在这里插入图片描述

什么是ThreadLocal?

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

ThreadLocal常用方法:

  1. public void set(T value) 设置当前线程的线程局部变量的值
  2. public T get() 返回当前线程所对应的线程局部变量的值

流程

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

代码实现

在common包下添加BaseContext类
作用:基于ThreadLocal封装工具类,用于保护和获取当前用户id

package com.itzq.reggie.common;

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

123456789101112131415

在LoginCheckFilter类中添加代码
在这里插入图片描述

在MyMetaObjectHandler类中,修改部分代码
在这里插入图片描述

重启项目,登录一个新的员工用户

在这里插入图片描述

来到新增员工界面,输入相应的信息,点击保存
在这里插入图片描述

发现以下字段添加成功,同理根据代码逻辑推断,更新用户操作会更新表中的updateTime,updateUser字段
在这里插入图片描述

05-新增分类-需求分析&数据模型&代码开发&功能测试

需求分析

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

可以在后台系统的分类管理页面分别添加菜品分类和套餐分类,如下:

新增菜品分类
在这里插入图片描述

新增套餐分类
在这里插入图片描述

数据模型

新增分类,其实就是将我们新增窗口录入的分类数据插入到category表,表结构如下:
在这里插入图片描述

代码分析

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

  1. 实体类Category(直接从课程资料中导入即可)
  2. Mapper接口CategoryMapper
  3. 业务层接口CategoryService
  4. 业务层实现类CategoryServicelmpl
  5. 控制层CategoryController
    在这里插入图片描述

实体类Category

package com.itzq.reggie.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 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;


}

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657

Mapper接口CategoryMapper

package com.itzq.reggie.mapper;

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

@Mapper
public interface CategoryMapper extends BaseMapper<Category> {
}

12345678910

业务层接口CategoryService

package com.itzq.reggie.service;

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

public interface CategoryService extends IService<Category> {
}

12345678

业务层实现类CategoryServicelmpl

package com.itzq.reggie.service.Impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itzq.reggie.entity.Category;
import com.itzq.reggie.mapper.CategoryMapper;
import com.itzq.reggie.service.CategoryService;
import org.springframework.stereotype.Service;

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

123456789101112

控制层CategoryController

package com.itzq.reggie.controller;

import com.itzq.reggie.common.R;
import com.itzq.reggie.entity.Category;
import com.itzq.reggie.service.CategoryService;
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;

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

@Autowired
private CategoryService categoryService;

}

12345678910111213141516171819202122

在开发代码之前,需要梳理一下整个程序的执行过程:

  1. 页面(backend/page/category/list.html)发送ajax请求,将新增分类窗口输入的数据以json形式提交到服务端
  2. 服务端Controller接收页面提交的数据并调用Service将数据进行保存
  3. Service调用Mapper操作数据库,保存数据

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

新增菜品分类
在这里插入图片描述

新增套餐分类
在这里插入图片描述

在控制层CategoryController中添加逻辑代码

package com.itzq.reggie.controller;

import com.itzq.reggie.common.R;
import com.itzq.reggie.entity.Category;
import com.itzq.reggie.service.CategoryService;
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;

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

@Autowired
private CategoryService categoryService;

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

123456789101112131415161718192021222324252627282930313233

功能测试

测试前需注意,category表中的name字段索引类型为unique
在这里插入图片描述

初始阶段数据库表
在这里插入图片描述

启动项目,在新增菜品分类页面输入数据,点击保存,若提交相同分类名称(name字段),则会经过我们之前写的全局异常处理器类,报相关的错误
在这里插入图片描述

将页面提交的数据保存到了数据库
在这里插入图片描述

06-分类信息分页查询-需求分析&代码开发&功能测试

需求分析

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

代码开发

在开发代码之前,需要梳理一下整个程序的执行过程:

  1. 页面发送ajax请求,将分页查询参数(page、pageSize)提交到服务端
  2. 服务端Controller接收页面提交的数据并调用Service查询数据
  3. Service调用Mapper操作数据库,查询分页数据
  4. Controler将查询到的分页数据响应给页面
  5. 页面接收到分页数据并通过ElementUI的Table组件展示到页面上

在CategoryController类上添加page方法

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

功能测试

点击分类管理,客户端发送请求给服务端,服务端接收数据,进行分页处理后,返回数据给页面,页面通过相应组件处理
在这里插入图片描述

页面返回的数据type数据为int类型,为什么页面展示出来的是文字?

因为返回的数据在前端做了相应的判断处理,再回显到页面
在这里插入图片描述

07-删除分类-需求分析&代码开发&功能测试

需求分析

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

代码开发

在开发代码之前,需要梳理一下整个程序的执行过程:
1、页面发送ajax请求,将参数(id)提交到服务端
2、服务端Controller接收页面提交的数据并调用Service删除数据
3、Service调用Mapper操作数据库

在CategoryController类上添加delete方法

    /**
* 删除分类信息
* @param id
* @return
*/
@DeleteMapping
public R<String> delete(Long id){
log.info("将要删除的分类id:{}",id);

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

需要注意教程文档中给出的url的id参数为ids,需要在category.js中将参数改为id,以便于与后端接收数据做好相应的映射处理,若还是不能正常访问,清除浏览器缓存
在这里插入图片描述

功能测试

启动项目,点击删除按钮,弹出一个对话框,并点击确认,页面会重新发送分页请求,在页面将看不见该行数据,查看数据库也没有该行数据显示,删除数据成功
请添加图片描述

08-删除分类-功能完善

功能完善

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

要完善分类删除功能,需要先准备基础的类和接口︰
1、实体类Dish和Setmeal(从课程资料中复制即可)
2、Mapper接口DishMapper和setmealMapper
3、Service接口DishService和SetmealService
4、Service实现类DishServicelmpl和SetmealServicelmpl

实体类Dish和Setmeal

Dish

package com.itzq.reggie.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.math.BigDecimal;
import java.time.LocalDateTime;

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

private static final long serialVersionUID = 1L;

private Long id;


//菜品名称
private String name;


//菜品分类id
private Long categoryId;


//菜品价格
private BigDecimal price;


//商品码
private String code;


//图片
private String image;


//描述信息
private String description;


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


//顺序
private Integer sort;


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


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


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


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


//是否删除
private Integer isDeleted;

}

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475

Setmeal

package com.itzq.reggie.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.math.BigDecimal;
import java.time.LocalDateTime;

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

private static final long serialVersionUID = 1L;

private Long id;


//分类id
private Long categoryId;


//套餐名称
private String name;


//套餐价格
private BigDecimal price;


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


//编码
private String code;


//描述信息
private String description;


//图片
private String image;


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


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


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


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


//是否删除
private Integer isDeleted;
}

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970

Mapper接口DishMapper和SetmealMapper

DishMapper

package com.itzq.reggie.mapper;

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

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

12345678910

SetmealMapper

package com.itzq.reggie.mapper;

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

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

12345678910

Service接口DishService和SetmealService

DishService

package com.itzq.reggie.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.itzq.reggie.entity.Dish;

public interface DishService extends IService<Dish> {
}

12345678

SetmealService

package com.itzq.reggie.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.itzq.reggie.entity.Setmeal;

public interface SetmealService extends IService<Setmeal> {
}

12345678

Service实现类DishServicelmpl和SetmealServicelmpl

DishServicelmpl

package com.itzq.reggie.service.Impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itzq.reggie.entity.Dish;
import com.itzq.reggie.mapper.DishMapper;
import com.itzq.reggie.service.DishService;
import org.springframework.stereotype.Service;

@Service
public class DishServicelmpl extends ServiceImpl<DishMapper, Dish> implements DishService {
}

123456789101112

SetmealServicelmpl

package com.itzq.reggie.service.Impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itzq.reggie.entity.Setmeal;
import com.itzq.reggie.mapper.SetmealMapper;
import com.itzq.reggie.service.SetmealService;
import org.springframework.stereotype.Service;

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

123456789101112

在common包下添加CustomException 类

package com.itzq.reggie.common;

public class CustomException extends RuntimeException{
public CustomException(String message){
super(message);
}
}

12345678

在GlobalExceptionHandler类,添加exceptionHandler方法用于处理CustomException异常

@ExceptionHandler(CustomException.class)
public R<String> exceptionHandler (CustomException exception){
log.error(exception.getMessage());

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

在CategoryService 接口中定义remove方法

package com.itzq.reggie.service;

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

public interface CategoryService extends IService<Category> {
void remove(Long id);
}

123456789

在CategoryServiceImpl 实现类中实现接口定义的remove方法,并为该方法添加所需要的逻辑代码

  • 逻辑:查看当前要删除的分类id是否与菜品或套餐相关联,若与其中一个关联,则抛出异常
package com.itzq.reggie.service.Impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itzq.reggie.common.CustomException;
import com.itzq.reggie.entity.Category;
import com.itzq.reggie.entity.Dish;
import com.itzq.reggie.entity.Setmeal;
import com.itzq.reggie.mapper.CategoryMapper;
import com.itzq.reggie.service.CategoryService;
import com.itzq.reggie.service.DishService;
import com.itzq.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
DishService dishService;

@Autowired
SetmealService setmealService;

/**
* 根据id删除分类,删除之前需要进行判断
* @param id
*/
@Override
public void remove(Long id) {
LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
//添加dish查询条件,根据分类id进行查询
dishLambdaQueryWrapper.eq(Dish::getCategoryId, id);
int count1 = dishService.count(dishLambdaQueryWrapper);

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

LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
//添加dish查询条件,根据分类id进行查询
int count2 = setmealService.count(setmealLambdaQueryWrapper);

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

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

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455

重启项目,点击删除按钮
请添加图片描述

内容提示
在这里插入图片描述

同理,若当前分类关联了套餐,也会有相应的提示,可以自己操作检验

09-修改分类-需求分析&分析页面回显效果&代码开发&功能测试

需求分析

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

分析页面回显效果

点击修改按钮,触发点击事件editHandle,并传递该行的数据
在这里插入图片描述

在editHandle函数中,将传来的参数赋值给classData下的name和sort数据
在这里插入图片描述
在这里插入图片描述

v-model实现数据的双向绑定
在这里插入图片描述

代码开发

在CategoryController类中添加update方法,请求方式为put请求

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

功能测试

点击修改按钮
在这里插入图片描述

填入想要修改的信息后,点击确定
在这里插入图片描述

页面显示
在这里插入图片描述

数据库中内容
在这里插入图片描述
修改成功!

业务开发Day4

01-本章内容介绍

目录

  1. 文件上传下载
  2. 新增菜品
  3. 菜品信息分页查询
  4. 修改菜品

需要实现页面的功能
在这里插入图片描述

02-文件上传下载_文件上传下载介绍

内容

  1. 文件上传介绍
  2. 文件下载介绍

文件上传介绍

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

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

  1. method=“post” 采用post方式提交数据
  2. enctype=“multipart/form-data” 采用multipart格式上传文件
  3. type=“file” 使用input的file控件上传

举例:

<form method="post" action="/common/upload" enctype="multipart/form-data">
<input name="myFile" type="file"/>
<input type="submit" value="提交" />
<form>
1234

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

例如ElementUI中提供的upload上传组件:
在这里插入图片描述

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

  • commons-fileupload
  • commons-io

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

文件下载介绍

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

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

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

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

03-文件上传下载_文件上传代码实现1

文件上传代码实现1

文件上传,页面端可以使用ElementUI提供的上传组件。
可以直接使用资料中提供的上传页面,位置:资料/文件上传下载页面/upload.html

upload.html-前端上传文件页面代码

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>文件上传</title>
<!-- 引入样式 -->
<link rel="stylesheet" href="../../plugins/element-ui/index.css" />
<link rel="stylesheet" href="../../styles/common.css" />
<link rel="stylesheet" href="../../styles/page.css" />
</head>
<body>
<div class="addBrand-container" id="food-add-app">
<div class="container">
<el-upload class="avatar-uploader"
action="/common/upload"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeUpload"
ref="upload">
<img v-if="imageUrl" :src="imageUrl" class="avatar"></img>
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
</div>
</div>
<!-- 开发环境版本,包含了有帮助的命令行警告 -->
<script src="../../plugins/vue/vue.js"></script>
<!-- 引入组件库 -->
<script src="../../plugins/element-ui/index.js"></script>
<!-- 引入axios -->
<script src="../../plugins/axios/axios.min.js"></script>
<script src="../../js/index.js"></script>
<script>
new Vue({
el: '##food-add-app',
data() {
return {
imageUrl: ''
}
},
methods: {
handleAvatarSuccess (response, file, fileList) {
this.imageUrl = `/common/download?name=${response.data}`
},
beforeUpload (file) {
if(file){
const suffix = file.name.split('.')[1]
const size = file.size / 1024 / 1024 < 2
if(['png','jpeg','jpg'].indexOf(suffix) < 0){
this.$message.error('上传图片只支持 png、jpeg、jpg 格式!')
this.$refs.upload.clearFiles()
return false
}
if(!size){
this.$message.error('上传文件大小不能超过 2MB!')
return false
}
return file
}
}
}
})
</script>
</body>
</html>
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566

放置upload.html代码的位置(需创建一个demo文件夹)
在这里插入图片描述

在controller包下创建*CommonController类*,代码内容如下:

package com.itzq.reggie.controller;

import com.itzq.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 {

@PostMapping("/upload")
public R<String> upload(MultipartFile file){
//file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件删除
log.info(file.toString());
return null;
}
}
123456789101112131415161718192021

注意:

  • MultipartFile是spring类型,代表HTML中form data方式上传的文件,包含二进制数据+文件名称
  • MultipartFile后面的参数名必须为file,因为需要和前端页面的name保持一致,否则不会生效
    在这里插入图片描述

启动项目,在浏览器地址栏输入:http://localhost:8080/backend/page/demo/upload.html

  • 点击上传图片,并上传一个符合文件上传格式,符合文件上传大小的图片
    在这里插入图片描述
  • 后端返回给前端的msg数据为NOTLOGIN,可知被filter过滤器拦截,返还还未登录的信息
  • 因此我们需要先在页面上登录,登录后会在服务端的内存中存储session对象,session的作用域为一次会话范围内
  • 在浏览器地址栏中输入:http://localhost:8080/backend/page/demo/upload.html,即可避免被filter过滤器拦截

被filter拦截
img

登录后可正常上传文件,在此处添加断点,以debug方式启动项目
在这里插入图片描述

来到文件上传页面,点击上传文件,进入断点模式,查看文件的存储位置
在这里插入图片描述

在磁盘中寻找到文件,发现该文件为临时文件(TMP文件),所以需要转存到指定位置,否则本次请求完成后临时文件删除
在这里插入图片描述

放行后发现临时文件消失
在这里插入图片描述

04-文件上传下载_文件上传代码实现2

文件上传代码实现2

在开发上传文件代码前,先再LoginCheckFilter类的urls数组中添加 “/common/“**
作用:避免每次上传文件时都需要进行登录操作
在这里插入图片描述

将临时文件存储存储到指定位置

package com.itzq.reggie.controller;

import com.itzq.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;

import java.io.File;
import java.io.IOException;

@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) {
e.printStackTrace();
}
return null;
}
}
12345678910111213141516171819202122232425262728293031

启动项目,浏览器地址栏输入地址:http://localhost:8080/backend/page/demo/upload.html

  • 上传图片,查看指定存储文件的位置是否有上传的文件
    在这里插入图片描述

文件转存的位置改为动态可配置的,通过配置文件的方式指定
在这里插入图片描述

  • 使用 @Value(“${reggie.path}”)读取到配置文件中的动态转存位置
  • 使用uuid方式重新生成文件名,避免文件名重复造成文件覆盖
  • 通过获取原文件名来截取文件后缀
package com.itzq.reggie.controller;

import com.itzq.reggie.common.R;
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("${reggie.path}")
private String basePath;

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



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

重启项目,上传图片,在动态指定的位置上发现上传的文件
在这里插入图片描述

指定的目录或许不存在于磁盘中,所以我们要为程序添加逻辑代码

若目录不存在于磁盘中,则需要创建该目录
在这里插入图片描述
具体代码

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

测试,
将配置文件中的目录信息修改为本地磁盘中不存在的目录
在这里插入图片描述

重启项目,上传图片,发现创建了一个新的目录,并将文件放入该目录下
在这里插入图片描述

服务端需返回文件名给前端,便于后续开发使用
在这里插入图片描述

05-文件上传下载_文件下载代码实现&测试

文件下载代码实现

前端处理

前端页面ElementUI的upload组件会在上传完图片后,触发img组件发送请求,服务端以流的形式(输出流)将文件写回浏览器,在浏览器中展示图片
在这里插入图片描述

定义前端发送回显图片请求的地址
在这里插入图片描述

在CommonController类中添加download方法

  1. 通过输入流读取文件内容
  2. 通过输出流将文件写回浏览器,在浏览器展示图片
  3. 关闭输入输出流,释放资源
@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");

int len = 0;
byte[] bytes = new byte[1024];
while ((len = fileInputStream.read(bytes)) != -1){
//向response缓冲区中写入字节,再由Tomcat服务器将字节内容组成Http响应返回给浏览器。
outputStream.write(bytes,0,len);
//所储存的数据全部清空
outputStream.flush();
}

//关闭流
fileInputStream.close();
outputStream.close();

} catch (IOException e) {
e.printStackTrace();
}
}
123456789101112131415161718192021222324252627282930

测试

启动项目,上传图片,图片回显到页面
在这里插入图片描述

06-新增菜品_需求分析&数据模型

需求分析

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

数据模型

dish表
在这里插入图片描述

dish_flavor表
在这里插入图片描述

新增菜品分类,会将前端传过来的数据保存在这两张表中

07-新增菜品_代码开发_查询分类数据

代码开发-准备工作

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

  1. 实体类DishFlavor(直接从课程资料中导入即可,Dish实体前面课程中已经导入过了)
  2. Mapper接口DishFlavorMapper
  3. 业务层接口DishFlavorService
  4. 业务层实现类DishFlavorServicelmpl
  5. 控制层DishController

实体类DishFlavor

package com.itzq.reggie.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;

}

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354

Mapper接口DishFlavorMapper

package com.itzq.reggie.mapper;

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

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

12345678910

业务层接口DishFlavorService

package com.itzq.reggie.service;

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

public interface DishFlavorService extends IService<DishFlavor> {
}

12345678

业务层实现类DishFlavorServicelmpl

package com.itzq.reggie.service.Impl;

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

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

123456789101112

控制层DishController

package com.itzq.reggie.controller;

import com.itzq.reggie.service.DishFlavorService;
import com.itzq.reggie.service.DishService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/dish")
@Slf4j
public class DishController {
@Autowired
private DishService dishService;

@Autowired
private DishFlavorService dishFlavorService;

}

123456789101112131415161718192021

代码开发-梳理交互过程

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

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

添加菜品页面展示
在这里插入图片描述

08-新增菜品_代码开发_查询分类数据

前端分析

一个vue实例被创建后会调用钩子函数,执行其中的方法
在这里插入图片描述

来到getDishList方法,执行其中getCategoryList方法
在这里插入图片描述

执行getCategoryList方法向服务端发送ajax请求,请求方式为get
在这里插入图片描述

在CategoryController类中,添加list方法,具体代码如下:

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

}
1234567891011121314

启动项目,进入菜品管理,点击菜品分类下拉框,成功获得数据
在这里插入图片描述

09-新增菜品_代码开发_接收页面提交的数据

接收图片文件

在本章节02-05,我们已经将图片的上传下载准备完毕

测试

添加一张图片,并回显图片
在这里插入图片描述

注意事项

  1. 价格在前端已被处理,在点击提交按钮后,先执行前端的submitForm方法,并将price做相应的处理(在页面中单位为元,在数据库中存储的单位为分),再通过ajax请求向后端提供相应的json数据
    在这里插入图片描述
  2. 因为Dish实体类不满足接收flavor参数,即需要导入DishDto,用于封装页面提交的数据
    在这里插入图片描述

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

  • 在reggie包下,创建一个新包为dto
  • 在该包下创建DishDto 数据传输类
package com.itzq.reggie.dto;


import com.itzq.reggie.entity.Dish;
import com.itzq.reggie.entity.DishFlavor;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;

@Data
public class DishDto extends Dish {

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

private String categoryName;

private Integer copies;
}

12345678910111213141516171819

代码开发

在DishController类中添加save方法

  • 代码逻辑:测试是否可以正常的接收前端传过来的json数据
  • 注意:因为前端传来的是json数据,所以我们需要在参数前添加*@RequestBody*注解
  • 具体代码如下:
@PostMapping
public R<String> save(@RequestBody DishDto dishDto){
log.info("接收的dishDto数据:{}",dishDto.toString());
return null;
}
12345

测试

debug方式启动项目,在标记的行处添加断点,用于查看数据
在这里插入图片描述

在添加菜品页面输入数据,点击保存
在这里插入图片描述

来到断点处,查看到数据准确无误的提交到服务端
在这里插入图片描述

10-新增菜品_代码开发_保存数据到菜品表和菜品口味表

分析

  • 在保存数据到菜品表和菜品口味表的过程中,我们需要对保存到菜品口味表的数据做相应的处理
  • 取出dishDto的dishId,通过stream流对每一组flavor的dishId赋值
  • 保存菜品口味到菜品数据表

代码开发

在DishServicelmpl类中添加如下代码

package com.itzq.reggie.service.Impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itzq.reggie.dto.DishDto;
import com.itzq.reggie.entity.Dish;
import com.itzq.reggie.entity.DishFlavor;
import com.itzq.reggie.mapper.DishMapper;
import com.itzq.reggie.service.DishFlavorService;
import com.itzq.reggie.service.DishService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

@Service
@Transactional
public class DishServicelmpl extends ServiceImpl<DishMapper, Dish> implements DishService {

@Autowired
DishFlavorService dishFlavorService;

@Override
public void saveWithFlavor(DishDto dishDto) {

//保存菜品的基本信息到菜品表
super.save(dishDto);
//获取菜品id
Long dishId = dishDto.getId();
//获取菜品口味
List<DishFlavor> flavors = dishDto.getFlavors();

//将每条flavor的dishId赋上值
flavors = flavors.stream().map((item) -> {
item.setDishId(dishId);
return item;
}).collect(Collectors.toList());

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

}
}

123456789101112131415161718192021222324252627282930313233343536373839404142434445

在ReggieApplication主启动类上,添加注解:@EnableTransactionManagement
在这里插入图片描述

11-新增菜品_代码开发_功能测试

功能测试

前提:在DishController类的save方法中添加代码

    @PostMapping
public R<String> save(@RequestBody DishDto dishDto){
log.info("接收的dishDto数据:{}",dishDto.toString());

//保存数据到数据库
dishService.saveWithFlavor(dishDto);
return R.success("新增菜品成功");
}
12345678

重启项目,进入添加菜品页面,输入数据,点击保存
在这里插入图片描述

dish表中添加数据成功
在这里插入图片描述

dish_flavor表中添加数据成功,并成功为每组flavor数据附上dishId
在这里插入图片描述

12-菜品信息分页查询_需求分析

需求分析

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

图片和菜品分类比较特殊

  • 图片列:会用到文件的下载功能
  • 菜品分类列:只保存了菜品的category_id,需通过查找category_id所对应的菜品分类名称,从而回显数据
    在这里插入图片描述

13-菜品信息分页查询_代码开发1

代码开发-梳理交互过程

在开发代码之前,需要梳理一下菜品分页查询时前端页面和服务端的交互过程:

  1. 页面(backend/page/food/list.html)发送ajax请求,将分页查询参数(page、pageSize、name),提交到服务端,获取分页数据
  2. 页面发送请求,请求服务端进行图片下载,用于页面图片展示

开发菜品信息分页查询功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。

代码

在DishController下,添加page方法,进行分页查询

    @GetMapping("/page")
public R<Page> page(int page, int pageSize, String name){

//构造分页构造器对象
Page<Dish> pageInfo = new Page<>();
//条件构造器
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
//添加条件
queryWrapper.like(name != null,Dish::getName,name);
queryWrapper.orderByDesc(Dish::getUpdateTime);
//执行分页查询
dishService.page(pageInfo,queryWrapper);

return R.success(pageInfo);
}
123456789101112131415

测试

  • 为什么只有宫保鸡丁有图片的展示效果,因为这是在刚刚添加菜品的时候添加的数据
  • 保证了在服务端存在对应图片名的信息,而其他是菜品是直接从sql文件导入,服务端不一定有对应的图片名
  • 为什么菜品分类中没有数据?
  • 因为服务端传给前端的菜品分类数据不满足前端的要求,所以在页面中不能回显菜品分类数据
    在这里插入图片描述

14-菜品信息分页查询_代码开发2

分析

在前端页面发现菜品分类对应的prop属性名为categoryName
在这里插入图片描述

但我们在响应的数据当中并没有发现categoryName字段
在这里插入图片描述

  • 页面需要什么数据,服务端就应该返还什么样的数据,所以Dish对象不满足该页面要求
  • 在之前我们创建了DishDto类,发现类中的属性名正好和前端的属性名对应
    在这里插入图片描述

代码

修改DishController中的page方法

 	@GetMapping("/page")
public R<Page> page(int page, int pageSize, String name){

//构造分页构造器对象
Page<Dish> pageInfo = new Page<>();
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");

//获取原records数据
List<Dish> records = pageInfo.getRecords();

List<DishDto> list = records.stream().map((item) -> {
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(item,dishDto);
Long categoryId = item.getCategoryId(); //分类id
Category category = categoryService.getById(categoryId);
String categoryName = category.getName();
dishDto.setCategoryName(categoryName);
return dishDto;
}).collect(Collectors.toList());

dishDtoPage.setRecords(list);

return R.success(dishDtoPage);
}
12345678910111213141516171819202122232425262728293031323334

15-菜品信息分页查询_功能测试

功能测试

前提

我们需要在分页方法中添加判空条件,若查询的数据为空,经过判断后跳过部分代码,就不会爆空指针异常
在这里插入图片描述

启动项目,点击菜品管理,可以看见页面展现的菜品分类信息
在这里插入图片描述

16-修改菜品_需求分析&梳理交互过程

需求分析

  • 在菜品管理列表页面点击修改按钮,跳转到修改菜品页面
  • 在修改页面回显菜品相关信息并进行修改
  • 最后点击确定按钮完成修改操作
    在这里插入图片描述

代码开发-梳理交互过程

在开发代码之前,需要梳理一下修改菜品时前端页面( add.html)和服务端的交互过程:

  1. 页面发送ajax请求,请求服务端获取分类数据,用于菜品分类下拉框中数据展示(已完成)
  2. 页面发送ajax请求,请求服务端,根据id查询当前菜品信息,用于菜品信息回显
  3. 页面发送请求,请求服务端进行图片下载,用于页图片回显(已完成)
  4. 点击保存按钮,页面发送ajax请求,将修改后的菜品相关数据以json形式提交到服务端

开发修改菜品功能,其实就是在服务端编写代码去处理前端页面发送的这4次请求即可。

17-修改菜品_代码开发_根据id查询对应的菜品和口味信息

代码

在DishService接口中添加方法*getByIdWithFlavor*
在这里插入图片描述

在DishServicelmpl中实现*getByIdWithFlavor*方法,并添加逻辑代码

  • 根据服务端接收的id,查询菜品的基本信息-dish
  • 创建dishDto对象,并将查询到的dish对象属性赋值给dishDto
  • 根据查询到的dish对象,可以取出对应的菜品id,再通过等值条件查询,查询到DishFlavor数据信息
  • 将查询到的flavor数据信息使用set方法赋值给dishDto对象
  • 返回dishDto对象
    @Override
public DishDto getByIdWithFlavor(Long id) {
//通过id查询菜品基本信息
Dish dish = super.getById(id);

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

//对象拷贝
BeanUtils.copyProperties(dish,dishDto);

//条件查询flavor
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DishFlavor::getDishId,dish.getId());
List<DishFlavor> list = dishFlavorService.list(queryWrapper);

//将查询到的flavor赋值到dto对象中
dishDto.setFlavors(list);

return dishDto;
}
123456789101112131415161718192021

在DishController中添加get方法,实现添加在DishServicelmpl中的逻辑代码,返回查询到的数据信息

@GetMapping("/{id}")
public R<DishDto> get(@PathVariable Long id){
//查询
DishDto dishDto = dishService.getByIdWithFlavor(id);

return R.success(dishDto);
}
1234567

18-修改菜品_代码开发_测试数据回显

测试数据回显

在DishController的get方法中加入断点
在这里插入图片描述

进入菜品管理,点击修改菜品按钮,程序跳转到断点处,查询回显的dishDto数据是否成功
在这里插入图片描述

页面回显成功
在这里插入图片描述

19-修改菜品_代码开发_修改菜品信息和口味信息

分析前端页面发送的请求

代码

在DishService接口中添加updateWithFlavor方法
在这里插入图片描述

DishServicelmpl类中实现DishService定义的方法,并添加代码逻辑

  • 根据id修改菜品的基本信息
  • 通过dish_id,删除菜品的flavor
  • 获取前端提交的flavor数据
  • 为条flavor的dishId属性赋值
  • 将数据批量保存到dish_flavor数据库
 @Override
public void updateWithFlavor(DishDto dishDto) {
//根据id修改菜品的基本信息
super.updateById(dishDto);

//通过dish_id,删除菜品的flavor
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DishFlavor::getDishId,dishDto.getId());
dishFlavorService.remove(queryWrapper);

//获取前端提交的flavor数据
List<DishFlavor> flavors = dishDto.getFlavors();

//将每条flavor的dishId赋值
flavors = flavors.stream().map((item) -> {
item.setDishId(dishDto.getId());
return item;
}).collect(Collectors.toList());


//将数据批量保存到dish_flavor数据库
dishFlavorService.saveBatch(flavors);
}
1234567891011121314151617181920212223

在DishController类中添加方法update,并调用updateWithFlavor方法实现表中数据的修改

    @PutMapping
public R<String> update(@RequestBody DishDto dishDto){

log.info("接收的dishDto数据:{}",dishDto.toString());

//更新数据库中的数据
dishService.updateWithFlavor(dishDto);

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

20-修改菜品_功能测试

功能测试

启动项目,来到菜品管理界面

  • 要修改菜品的初始值
    在这里插入图片描述
  • 点击修改,输入想要修改的信息,点击保存
    在这里插入图片描述
  • 跳转到菜品管理界面,修改菜品信息成功
    在这里插入图片描述

业务开发Day5

01-本章内容介绍

效果展示

  • 套餐管理界面
    在这里插入图片描述
  • 新增套餐界面
    在这里插入图片描述
  • 客户端展示
    在这里插入图片描述

目录

  1. 新增套餐
  2. 套餐信息分页查询
  3. 删除套餐

02-新增套餐_需求分析&数据模型

需求分析

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

数据模型

  • 新增套餐,其实就是将新增页面录入的套餐信息插入到setmeal表,还需要向setmeal_dish表插入套餐和菜品关联数据
  • 所以在新增套餐时,涉及到两个表:
  1. setmeal—-套餐表
  2. setmeal_dish—-套餐菜品关系表

setmeal
在这里插入图片描述

setmeal_dish
在这里插入图片描述

03-新增套餐_代码开发_准备工作&梳理交互过程

代码开发-准备工作

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

  1. 实体类SetmealDish(直接从课程资料中导入即可,Setmeal实体前面课程中已经导入过了)
  2. DTO SetmealDto (直接从课程资料中导入即可)
  3. Mapper接口SetmealDishMapper
  4. 业务层接口SetmealDishService
  5. 业务层实现类SetmealDishservicelmpl
  6. 控制层SetmealController

SetmealDish—实体类

package com.itzq.reggie.entity;

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

/**
* 套餐菜品关系
*/
@Data
public class 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;
}

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162

DTO SetmealDto—数据传输对象

package com.itzq.reggie.dto;


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

@Data
public class SetmealDto extends Setmeal {

private List<SetmealDish> setmealDishes;

private String categoryName;
}

12345678910111213141516

SetmealDishMapper接口

package com.itzq.reggie.mapper;

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

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


1234567891011

SetmealDishService接口

package com.itzq.reggie.service;

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

public interface SetmealDishService extends IService<SetmealDish> {
}

12345678

SetmealDishservicelmpl实现类

package com.itzq.reggie.service.Impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itzq.reggie.entity.SetmealDish;
import com.itzq.reggie.mapper.SetmealDishMapper;
import com.itzq.reggie.service.SetmealDishService;
import org.springframework.stereotype.Service;

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

123456789101112

SetmealController控制层

package com.itzq.reggie.controller;

import com.itzq.reggie.service.SetmealDishService;
import com.itzq.reggie.service.SetmealService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/setmeal")
@Slf4j
public class SetmealController {
@Autowired
private SetmealService setmealService;

@Autowired
private SetmealDishService setmealDishService;

}
1234567891011121314151617181920

代码开发-梳理交互过程

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

  1. 页面(backend/page/combo/add.html)发送ajax请求,请求服务端获取套餐分类数据并展示到下拉框中(已完成)
  2. 页面发送ajax请求,请求服务端,获取菜品分类数据并展示到添加菜品窗口中
  3. 页面发送ajax请求,请求服务端,根据菜品分类查询对应的菜品数据并展示到添加菜品窗口中
  4. 页面发送请求进行图片上传,请求服务端将图片保存到服务器(已完成)
  5. 页面发送请求进行图片下载,将上传的图片进行回显(已完成)
  6. 点击保存按钮,发送ajax请求,将套餐相关数据以json形式提交到服务端

开发新增套餐功能,其实就是在服务端编写代码去处理前端页面发送的这6次请求即可

04-新增套餐_代码开发_根据分类查询菜品

前端分析

启动项目,进入套餐管理,点击新建套餐,会发现页面发送的请求未被服务端接收
在这里插入图片描述

爆系统接口异常,服务端未定义查询菜品的方法
在这里插入图片描述

相关代码

在DishController类中,添加list方法
注意:需要添加额外的查询条件,只查询status为1的数据,表示该菜品为起售状态,才能被加入套餐中,供用户选择

    @GetMapping("/list")
public R<List<Dish>> list(Dish dish){

//构造查询条件
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(dish.getCategoryId() != null, Dish::getCategoryId, dish.getCategoryId());
//添加条件,查询状态为1(1为起售,0为停售)的菜品
queryWrapper.eq(Dish::getStatus,1);

List<Dish> list = dishService.list(queryWrapper);
//添加排序条件
return R.success(list);
}
12345678910111213

重启项目,发现查询数据成功,并回显到前端页面
img

05-新增套餐_代码开发_服务端接收页面提交的数据

前端分析

启动项目,来到添加套餐页面,输入数据,点击保存
在这里插入图片描述

查看前端页面发送的请求,请求方式
在这里插入图片描述

前端页面传输json数据给服务端
在这里插入图片描述

相关代码(测试版)

在SetmealController类中添加save方法

@PostMapping
public R<String> save(@RequestBody SetmealDto setmealDto){
log.info("数据传输对象setmealDto:{}",setmealDto.toString());
return null;
}
12345

添加断点在这里插入图片描述

debug方式重启项目,来到添加套餐页面,输入数据,点击保存
在这里插入图片描述

跳转到服务端,查看是否接收到客服端提交的数据,发现数据成功接收
在这里插入图片描述

06-新增套餐_代码开发_保存数据到对应表

相关代码

在SetmealService接口,添加saveWithDish方法
在这里插入图片描述

实现类SetmealServicelmpl,实现接口添加的方法,并向方法中添加代码逻辑

  • 保存套餐的基本信息
  • 保存套餐和菜品的关联信息
    @Override
@Transactional
public void saveWithDish(SetmealDto setmealDto) {
//保存套餐的基本信息,操作setmeal,执行insert操作
save(setmealDto);

List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();

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

//保存套餐和菜品的关联信息
setmealDishService.saveBatch(setmealDishes);
}
12345678910111213141516

在SetmealController控制层的save方法中,调用saveWithDish方法,将数据保存至数据库

    @PostMapping
public R<String> save(@RequestBody SetmealDto setmealDto){
log.info("数据传输对象setmealDto:{}",setmealDto.toString());
setmealService.saveWithDish(setmealDto);
return R.success("新增套餐成功");
}
123456

07-新增套餐_代码开发_功能测试

功能测试

来到新增套餐页面,输入数据,点击保存
在这里插入图片描述

setmeal_dish表—数据插入成功
在这里插入图片描述

setmeal—数据插入成功
在这里插入图片描述

08-套餐信息分页查询_需求分析&梳理交互过程

需求分析

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

梳理交互过程

在开发代码之前,需要梳理一下套餐分页查询时前端页面和服务端的交互过程:

  1. 页面(backend/page/combo/list.html)发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端,获取分页数据
  2. 页面发送请求,请求服务端进行图片下载,用于页面图片展示

开发套餐信息分页查询功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可

09-套餐信息分页查询_代码开发&功能测试

前端分析

点击套餐管理,前端页面发送ajax请求,请求方式:get
在这里插入图片描述

代码开发

SetmealController类中,添加list方法

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

//构造查询条件对象
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(name != null, Setmeal::getName, name);

//操作数据库
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);
//获取categoryId
Long categoryId = item.getCategoryId();
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);
}
1234567891011121314151617181920212223242526272829303132333435

注意
在套餐管理界面,套餐分类字段显示的是categoryId对应的中文,但在数据库里查询到的是categoryId,因此需要利用categoryId查询到categoryName,并赋值给数据传输对象SetmealDto

功能测试

启动项目,点击套餐管理,前端发送ajax请求,服务端接收前端发出的请求,并做相应的处理,向页面返回数据
在这里插入图片描述
数据成功回显到页面

10-删除套餐_需求分析&梳理交互过程

需求分析

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

梳理交互过程

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

  1. 删除单个套餐时,页面发送ajax请求,根据套餐id删除对应套餐
    在这里插入图片描述
  2. 删除多个套餐时,页面发送ajax请求,根据提交的多个套餐id删除对应套餐开发删除套餐功能
    在这里插入图片描述
    其实就是在服务端编写代码去处理前端页面发送的这2次请求即可

注意

  • 观察删除单个套餐和批量删除套餐的请求信息可以发现,两种请求的地址和请求方式都是相同的
  • 不同的则是传递的id个数,所以在服务端可以提供一个方法来统一处理。

11-删除套餐_代码开发&功能测试

代码开发(测试)

在SetmealController中添加delete方法

    @DeleteMapping
public R<String> delete(@RequestParam List<Long> ids){
log.info("ids为:",ids);

return null;
}
123456

在delete方法上,添加断点
在这里插入图片描述

debug方式启动项目,来到套餐管理页面,点击删除按钮
在这里插入图片描述

跳转到服务端,查询ids可知服务端成功接收到前端传来的数据信息
在这里插入图片描述

代码开发(完善)

在SetmealService接口中添加removeWithDish方法
在这里插入图片描述

在SetmealServicelmpl实现类中实现对应接口中添加的方法

	@Override
@Transactional
public void removeWithDish(List<Long> ids) {
//select count(*) from setmeal where ids in(1,2,3) and status = 1
//查询套餐状态,确定是否可以删除
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(Setmeal::getId,ids);
queryWrapper.eq(Setmeal::getStatus,1);

int count = super.count(queryWrapper);

if (count > 0){
//如果不能删除,抛出一个业务异常
throw new CustomException("套餐正在售卖中,不能删除");
}

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

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

setmealDishService.remove(dishLambdaQueryWrapper);


}
12345678910111213141516171819202122232425262728

在SetmealController中完善代码—调用removeWithDish方法,实现套餐数据删除成功

@DeleteMapping
public R<String> delete(@RequestParam List<Long> ids){
log.info("ids为:",ids);
setmealService.removeWithDish(ids);
return R.success("套餐数据删除成功");

}

12345678

注意:将setmeal表中status字段值改为0—为停售状态,方便测试
在这里插入图片描述

重启项目,来到套餐管理界面,点击删除按钮
在这里插入图片描述

页面显示删除成功
在这里插入图片描述

setmeal表中该行已被删除
在这里插入图片描述

12-本章内容介绍

手机验证码登录

  • 点击获取验证码
  • 收到短信,并输入验证码
  • 点击登录,登录成功
    在这里插入图片描述

客户端登录成功页面
在这里插入图片描述

本章内容介绍

  1. 短信发送
  2. 手机验证码登录(基于阿里云讲解)

13-短信发送_短信服务介绍和阿里云短信服务介绍

短信服务介绍

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

常用短信服务:

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

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

应用场景:

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

阿里云短信服务介绍

打开浏览器,登录阿里云—网址:https://cn.aliyun.com/

点击产品,在搜索框中输入短信服务,并点击搜索
在这里插入图片描述

来到短信息服务界面
在这里插入图片描述

选择符合自己业务需求的短信套餐包
在这里插入图片描述

14-短信发送_阿里云短信服务

设置短信签名

开通短信服务之后,进入短信服务管理页面,选择国内消息菜单,我们需要在这里添加短信签名
在这里插入图片描述

什么是短信签名?

  • 短信签名是短信发送者的署名,表示发送方的身份
  • 我们要调用阿里云短信服务发送短信,签名是必不可少的部分

添加短信签名方式
注意:个人申请签名是有一定的难度的,所以我们只需要了解一下使用短信签名的具体流程
在这里插入图片描述

设置短信模板

切换到【模板管理】标签页:
在这里插入图片描述
短信模板包含短信发送内容、场景、变量信息

每一个被设置好的模板有一个短信模板详情,模板详情包含了模板的6条信息
在这里插入图片描述

添加模板,并且提交后审核通过
在这里插入图片描述

设置AccessKey

AccessKey 是访问阿里云 API 的密钥,具有账户的完全权限,我们要想在后面通过API调用阿里云短信服务的接口发送短信,那么就必须要设置AccessKey。

光标移动到用户头像上,在弹出的窗口中点击【AccessKey管理】︰
在这里插入图片描述

进入到AccessKey的管理界面之后,提示两个选项:

  1. 继续使用AccessKey
  2. 开始使用子用户AccessKey
    在这里插入图片描述

区别:

  1. 继续使用AccessKey
  • 如果选择的是该选项,我们创建的是阿里云账号的AccessKey,是具有账户的完全权限
  • 有了这个AccessKey之后,我们就可以通过API调用阿里云服务,不仅是短信服务,其他服务也可以调用
  • 相对来说,并不安全,当前的AccessKey泄露,会影响到当前账户的其他云服务。
  1. 开始使用子用户AccessKey
  • 可以创建一个子用户,这个子用户可以分配比较低的权限,比如仅分配短信发送的权限,不具备操作其他的服务的权限
  • 即使这个AccessKey泄漏了,也不会影响其他的云服务, 相对安全。

创建子用户AccessKey。

  1. 点击创建用户
    在这里插入图片描述
  2. 输入登录名称和显示名称(都是自定义),选择—Open API调用访问
    注意:在java代码当中使用这个用户,所以我们选择—Open API调用访问
    在这里插入图片描述
  3. 成功创建子用户AccessKey
  • AccessKey ID:用户名
  • AccessKey Secret:密码
  • 需要将这对用户名和密码保存起来,后面在我们的程序当中会使用
    在这里插入图片描述

权限管理

在新创建的子用户下点击添加权限
在这里插入图片描述

因为我们只需要使用短信服务,所以我们在搜索框输入sms,点击需要添加的权限
在这里插入图片描述

授权成功
在这里插入图片描述
表示当前我们只给该用户授予了两个权限,即使用户名和密码泄露,其他人也只能调用短信服务
授权成功之后就可以用代码的方式来调用短信服务

AccessKey泄露需要进行的处理

  • AccessKey泄露出去,别人就可以使用我们的 AccessKey来发送短信,我们就需要收回我们的 AccessKey
  • 我们可以禁用或者删除对应的 AccessKey
  • 操作之后,相当于这个 AccessKey就作废了
    在这里插入图片描述

15-短信发送_代码开发_参照官方文档封装发送短信工具类

参照官方文档

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

具体开发步骤:

  1. 导入maven坐标
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.5.16</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
<version>2.1.0</version>
</dependency>
12345678910
  1. 在reggie包下新建utils包,导入该工具类
package com.itzq.reggie.utils;

import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.profile.DefaultProfile;

/**
* 短信发送工具类
*/
public class SMSUtils {

/**
* 发送短信
* @param signName 签名
* @param templateCode 模板
* @param phoneNumbers 手机号
* @param param 参数
*/
public static void sendMessage(String signName, String templateCode,String phoneNumbers,String param){
DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", "", "");
IAcsClient client = new DefaultAcsClient(profile);

SendSmsRequest request = new SendSmsRequest();
request.setSysRegionId("cn-hangzhou");
request.setPhoneNumbers(phoneNumbers);
request.setSignName(signName);
request.setTemplateCode(templateCode);
request.setTemplateParam("{\"code\":\""+param+"\"}");
try {
SendSmsResponse response = client.getAcsResponse(request);
System.out.println("短信发送成功");
}catch (ClientException e) {
e.printStackTrace();
}
}

}

1234567891011121314151617181920212223242526272829303132333435363738394041

查看短信服务产品文档的java SDK,了解短信服务java SDK的使用方法以及示例
在这里插入图片描述

16-手机验证码登录_需求分析_数据模型

需求分析

为了方便用户登录,移动端通常都会提供通过手机验证码登录的功能

手机验证码登录的优点:

  • 方便快捷,无需注册,直接登录
  • 使用短信验证码作为登录凭证,无需记忆密码
  • 安全

登录流程:

  • 输入手机号 > 获取验证码 > 输入验证码 > 点击登录 > 登录成功

注意:通过手机验证码登录,手机号是区分不同用户的标识

用户登录端界面
在这里插入图片描述

数据模型

通过手机验证码登录时,涉及的表为user表,即用户表。结构如下:
在这里插入图片描述

注意:

  • 手机号是区分不同用户的标识,在用户登录的时候判断所输入的手机号是否存储在表中
  • 如果不在表中,说明该用户为一个新的用户,将该用户自动保在user表中

17-手机验证码登录_代码开发_梳理交互过程&修改LoginCheckFilter

梳理交互过程

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

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

开发手机验证码登录功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。

代码开发-准备工作

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

  • 实体类user (直接从课程资料中导入即可)
  • Mapper接口UserMapper
  • 业务层接口UserService
  • 业务层实现类UserServicelmpl
  • 控制层UserController
  • 工具类SMSutils、ValidateCodeutils(直接从课程资料中导入即可)

实体类user

package com.itzq.reggie.entity;

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

private static final long serialVersionUID = 1L;

private Long id;


//姓名
private String name;


//手机号
private String phone;


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


//身份证号
private String idNumber;


//头像
private String avatar;


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

1234567891011121314151617181920212223242526272829303132333435363738394041424344

Mapper接口UserMapper

package com.itzq.reggie.mapper;

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

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

12345678910

业务层接口UserService

package com.itzq.reggie.service;

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

public interface UserService extends IService<User> {
}

12345678

业务层实现类UserServicelmpl

package com.itzq.reggie.service.Impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itzq.reggie.entity.User;
import com.itzq.reggie.mapper.UserMapper;
import com.itzq.reggie.service.UserService;
import org.springframework.stereotype.Service;

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

123456789101112

控制层UserController

package com.itzq.reggie.controller;

import com.itzq.reggie.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

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

}

1234567891011121314151617

工具类SMSutils、ValidateCodeutils(直接从课程资料中导入即可)

  1. SMSutils类
package com.itzq.reggie.utils;

import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.profile.DefaultProfile;

/**
* 短信发送工具类
*/
public class SMSUtils {

/**
* 发送短信
* @param signName 签名
* @param templateCode 模板
* @param phoneNumbers 手机号
* @param param 参数
*/
public static void sendMessage(String signName, String templateCode,String phoneNumbers,String param){
DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", "", "");
IAcsClient client = new DefaultAcsClient(profile);

SendSmsRequest request = new SendSmsRequest();
request.setSysRegionId("cn-hangzhou");
request.setPhoneNumbers(phoneNumbers);
request.setSignName(signName);
request.setTemplateCode(templateCode);
request.setTemplateParam("{\"code\":\""+param+"\"}");
try {
SendSmsResponse response = client.getAcsResponse(request);
System.out.println("短信发送成功");
}catch (ClientException e) {
e.printStackTrace();
}
}

}

1234567891011121314151617181920212223242526272829303132333435363738394041
  1. ValidateCodeutils类
package com.itzq.reggie.utils;

import java.util.Random;

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

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

1234567891011121314151617181920212223242526272829303132333435363738394041424344

修改LoginCheckFilter

前面我们已经完成了LoginCheckFilter过滤器的开发,此过滤器用于检查用户的登录状态。我们在进行手机验证码登录时,发送的请求需要在此过滤器处理时直接放行。

在LoginCheckFilter类中的urls数组中,添加下面两条数据

  1. “/user/sendMsg”, //移动端发送短信
  2. “/user/login” //移动端登录
    在这里插入图片描述

启动项目,在浏览器中输入访问地址:http://localhost:8080/front/page/login.html
在这里插入图片描述

注意:
使用h5开发的,自适应手机屏幕的大小,在浏览器中,需使用浏览器的手机模式打开,下面为具体步骤:

  1. 按住F12,弹出对应的页面
  2. 点击红色方框所标注的位置,将页面切换至手机浏览模式
    在这里插入图片描述

成功显示用户登录页面
在这里插入图片描述

在LoginCheckFilter类下添加代码,判断用户是否登录

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

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

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

添加代码的位置
在这里插入图片描述

18-手机验证码登录_代码开发_发送验证码短信

注意

在给出的前端资源资料中,login.html是被后面章节修改过的,因此我们需要重新导入front目录
在这里插入图片描述

在给出的代码目录中,找到day05下的front目录,复制该目录,将项目中的front目录覆盖
在这里插入图片描述
修改完成后,访问前端页面可能出现问题,因此我们需要重启项目,删除浏览器中的数据
若还是不能解决,关闭idea,重新打开idea代码编辑器

前端分析

在用户登录界面中,输入电话号码,点击获取验证码,页面会发送一个ajax请求
请求地址:http://localhost:8080/user/sendMsg
请求方式:POST
在这里插入图片描述

代码开发

注意

  • 发送短信只需要调用封装的工具类中的方法即可
  • 使用手机号登录功能流程跑通,在测试中我们不用真正的发送短信,只需要将验证码信息,通过日志输出
  • 登录时,我们直接从控制台就可以看到生成的验证码(实际上也就是发送到我们手机上的验证码)

在UserController控制层中,添加sendMsg方法

	@PostMapping("/sendMsg")
public R<String> sendMsg(@RequestBody User user, HttpSession session){
//获取手机号
String phone = user.getPhone();

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

//调用阿里云提供的短信服务API完成短信发送
//SMSUtils.sendMessage("瑞吉外卖","",phone,code);

//需要将生成的验证码保存到session
session.setAttribute(phone,code);
return R.success("短信发送成功");
}
return R.error("短信发送失败");
}
12345678910111213141516171819

测试

  • 重启项目,在浏览器地址栏中输入地址:http://localhost:8080/front/page/login.html
  • 来到登录页面,输入手机号,点击获取验证码
    在这里插入图片描述
  • 后端获取到生成的验证码
    在这里插入图片描述
  • 在用户登录页面上,输入验证码,点击登录
    在这里插入图片描述
  • 报404错误,后端还没有处理该请求的代码
    在这里插入图片描述

19-手机验证码登录_代码开发_登录效验

前端分析

来到用户登录界面,按住F12,点击登录按钮,页面发送ajax请求,查看请求的地址以及方式
在这里插入图片描述

页面以json数据格式传输给服务端
在这里插入图片描述

代码开发

在UserController控制层类中,添加login方法,测试服务端是否可以接受前端提交的数据

	@PostMapping("/login")
public R<String> login(@RequestBody Map map, HttpSession session){

log.info(map.toString());
return R.error("短信发送失败");
}

1234567

重启项目,来到用户登录界面,输入正确手机号,点击获取验证码,查看服务端日志打印出的验证码信息,输入验证码,点击登录,前端发送ajax请求
在这里插入图片描述

前端发送ajax请求,服务端通过日志打印出手机和验证码信息,接收数据成功
在这里插入图片描述

注意
在login方法中,接收数据的参数类型为Map类型,也可以重新定义一个UserDto(用户类数据传输对象)用来接收数据
在这里插入图片描述

完善用户登录代码

@PostMapping("/login")
public R<User> login(@RequestBody Map map, HttpSession session){
log.info(map.toString());
//获取手机号
String phone = map.get("phone").toString();
//获取验证码
String code = map.get("code").toString();
//从session中获取保存的验证码
String codeInSession = session.getAttribute(phone).toString();
//进行验证码的对比(页面提交的验证码和session中保存的验证码)
if (code != null && code.equals(codeInSession)){
//如果比对成功,则登录成功
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getPhone,phone);
User user = userService.getOne(queryWrapper);

if (user == null){
//判断当前手机号是否为新用户,如果是新用户则自动完成注入
user = new User();
user.setPhone(phone);
userService.save(user);
}
return R.success(user);
}

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

20-手机验证码登录_功能测试

测试

重启项目,进入用户登录界面,输入手机号,点击获取验证码
在这里插入图片描述

服务器控制台打印出验证码日志信息
在这里插入图片描述

在用户界面输入获取到的验证码,点击登录
在这里插入图片描述

页面跳转到用户登录界面

  • 经过分析得出:未将userId存储到session中
  • 导致前端发送请求时,进入filter过滤器,判断用户登录状态为未登录状态
  • 即跳转到登录界面
    在这里插入图片描述

在login方法中,添加代码
目的:将userId保存到session当中,前端页面发送请求到服务端,经过filter过滤器,判断用户状态为已登录状态,页面将不会跳转到用户登录界面
在这里插入图片描述

重启项目,在用户登录页面输入手机号,和获取到的验证码,点击登录
在这里插入图片描述

页面成功跳转到服务用户界面
在这里插入图片描述

user表中成功添加上测试的手机号码(未注册的手机号码)
在这里插入图片描述

业务开发Day6

01-本章内容介绍

目录

  1. 导入用户地址簿相关功能代码
  2. 菜品展示
  3. 购物车
  4. 下单

效果展示

在这里插入图片描述

02-导入用户地址簿相关代码

需求分析

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

页面展示

  • 新增收货地址页面
  • 地址管理页面
  • 编辑收货地址页面
    在这里插入图片描述

数据模型

用户的地址信息会存储在address_book表,即地址簿表中。具体表结构如下:
在这里插入图片描述

导入功能代码

功能代码清单:

  • 实体类AddressBook(直接从课程资料中导入即可)
  • Mapper接口AddressBookMapper
  • 业务层接口AddressBookService
  • 业务层实现类AddressBookServicelmpl
  • 控制层AddressBookController(直接从课程资料中导入即可)

实体类AddressBook

package com.itzq.reggie.entity;

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

/**
* 地址簿
*/
@Data
public class 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;
}

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293

Mapper接口AddressBookMapper

package com.itzq.reggie.mapper;

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

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

12345678910

业务层接口AddressBookService

package com.itzq.reggie.service;

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

public interface AddressBookService extends IService<AddressBook> {
}

12345678

业务层实现类AddressBookServicelmpl

package com.itzq.reggie.service.Impl;

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

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

123456789101112

控制层AddressBookController

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;

@RestController
@RequestMapping("/addressBook")
@Slf4j
public class AddressBookController {
@Autowired
private AddressBookService addressBookService;
123456789

完善地址管理页面

前端分析

点击方框里的图标,跳转到个人中心
在这里插入图片描述

在个人中心界面,点击地址管理
在这里插入图片描述

点击地址管理后,前端发送ajax请求,以下是该请求的地址和方式
在这里插入图片描述

新增代码

在AddressBookController控制层中,添加list方法
目的:查询指定用户的全部地址

    @GetMapping("/list")
public R<List<AddressBook>> list(AddressBook addressBook){
addressBook.setUserId(BaseContext.getCurrentId());
log.info("addressBook={}",addressBook);

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

List<AddressBook> list = addressBookService.list(queryWrapper);
return R.success(list);
}
12345678910111213

新增收货地址页面

前端分析

来到新增收货地址界面,输入相关信息,点击保存
在这里插入图片描述

前端发送ajax请求,以及请求的方式,服务端未响应,所以报404错误
在这里插入图片描述

新增代码

在AddressBookController控制层中,添加save方法
目的:将前端以json格式传输到后端的数据,保存到数据库中

	@PostMapping
public R<AddressBook> save(@RequestBody AddressBook addressBook){
addressBook.setUserId(BaseContext.getCurrentId());
log.info("addressBook={}",addressBook);

addressBookService.save(addressBook);

return R.success(addressBook);
}
123456789

测试-1

重启项目,来到新增收货地址页面,输入地址信息后,点击保存地址
在这里插入图片描述

页面跳转到地址管理界面,前端发送ajax请求—显示出该用户所有地址
在这里插入图片描述

测试成功

  • 查询指定用户的全部地址
  • 将新增地址保存到数据库

设置默认地址

前端分析

在地址管理界面,点击圆圈—将该地址设置为默认地址
在这里插入图片描述

点击后,前端发送ajax请求,以下为请求地址和请求方式
在这里插入图片描述

新增代码

在AddressBookController控制层中,添加getDefault方法
目的:设置默认地址

 @PutMapping("/default")
public R<AddressBook> getDefault(@RequestBody AddressBook addressBook){
addressBook.setUserId(BaseContext.getCurrentId());

//条件构造器
LambdaUpdateWrapper<AddressBook> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(addressBook.getUserId() != null,AddressBook::getUserId,addressBook.getUserId());
updateWrapper.set(AddressBook::getIsDefault,0);

//将与用户id所关联的所有地址的is_default字段更新为0
addressBookService.update(updateWrapper);

addressBook.setIsDefault(1);
//再将前端传递的地址id的is_default字段更新为1
addressBookService.updateById(addressBook);

return R.success(addressBook);
}
123456789101112131415161718

测试-2

重启项目,来到地址管理界面,点击设置为默认地址
在这里插入图片描述

测试成功

成功将该地址设置为默认地址
在这里插入图片描述

该用户address_book表中,该地址is_default字段从0变为1,表示该地址为该用户的默认地址
在这里插入图片描述

03-菜品展示_需求分析

需求分析

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

页面效果展示
在这里插入图片描述

04-菜品展示_代码开发_梳理交互过程

梳理交互过程

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

  1. 页面(front/index.html)发送ajax请求,获取分类数据(菜品分类和套餐分类)
  2. 页面发送ajax请求,获取第一个分类下的菜品或者套餐

开发菜品展示功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可

前端分析

注意:首页加载完成后,页面还发送了一次ajax请求用于加载购物车数据

  1. 前端页面初始化数据发送ajax请求—获取所有菜品和套餐分类
    在这里插入图片描述

该请求方法在此之前已存在—查询分类信息
在这里插入图片描述

  1. 前端页面初始化数据发送ajax请求— 获取购物车类商品集合
    在这里插入图片描述

注此处可以将这次请求的地址暂时修改一下,从静态json文件中获取数据,等后续开发购物车功能时再修改回来,如下:

在这里插入图片描述

在front包下添加json文件

  • json文件名:cartData.json
  • json文件代码:
{"code":1,"msg":null,"data":[],"map":{}}
1

为什么只有以上两个ajax请求同时接收到页面返回的数据后才能正常显示?

因为Promse.all在处理多个异步请求时,需要等待绑定的每个ajax请求返回数据以后才能正常显示
在这里插入图片描述

页面在初始化时,发送ajax请求,获取第一个分类下的菜品或套餐

下面是请求地址和请求方式—(由请求地址可知,该请求为获取第一个分类下的菜品信息)
在这里插入图片描述

该请求也是在此之前已存在—通过条件查询获取该种类下的所有菜品信息,并且查询的菜品必须为起售状态(status=1)
在这里插入图片描述

测试

  • 因修改过前端代码,所以需要清空浏览器的缓存数据(Ctrl+Shift+Delete),并重启项目
  • 若还是不能正常访问,重启idea代码编辑器

显示成功
在这里插入图片描述

注意:

  • 前端页面的需求:如果菜品设置了口味信息,需要展示选择规格按钮,否则显示+按钮
  • 但返回的Dish类型中未包含菜品口味信息
  • 所以需要修改部分代码让返回值既包含菜品的基本信息,也包含了口味的信息

05-菜品展示_代码开发_修改DishController的list方法并测试

修改DishController的list方法

  • 添加的SQL语句为:select * from dish_flavors where dish_id = ?—转化为代码形式
  • 查询出数据后,将查询的数据放入dishDto(dish的数据转换对象)对象中
  • 返回list集合,list中对象类型为:DishDto
@GetMapping("/list")
public R<List<DishDto>> list(Dish dish){

//构造查询条件
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(dish.getCategoryId() != null, Dish::getCategoryId, dish.getCategoryId());
//添加条件,查询状态为1(1为起售,0为停售)的菜品
queryWrapper.eq(Dish::getStatus,1);

List<Dish> list = dishService.list(queryWrapper);

List<DishDto> dishDtoList = list.stream().map((item) -> {
DishDto dishDto = new DishDto();
//对象拷贝(每一个list数据)
BeanUtils.copyProperties(item,dishDto);
Long categoryId = item.getCategoryId(); //分类id
//通过categoryId查询到category内容
Category category = categoryService.getById(categoryId);
//判空
if(category != null){
String categoryName = category.getName();
dishDto.setCategoryName(categoryName);
}

//获取当前菜品id
Long dishId = item.getId();

//构造条件构造器
LambdaQueryWrapper<DishFlavor> dishFlavorLambdaQueryWrapper= new LambdaQueryWrapper<>();
//添加查询条件
dishFlavorLambdaQueryWrapper.eq(dishId != null,DishFlavor::getDishId,dishId);
//select * from dish_flavors where dish_id = ?
List<DishFlavor> dishFlavors = dishFlavorService.list(dishFlavorLambdaQueryWrapper);

dishDto.setFlavors(dishFlavors);

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

return R.success(dishDtoList);
}
1234567891011121314151617181920212223242526272829303132333435363738394041

测试

启动项目,点击登录,来到客户端首页,若该菜品有相应的口味选择,前端页面展示选择规格按钮;反之,前端页面展示+
按钮

在这里插入图片描述

06-菜品展示_代码开发_创建SetmealController的list方法并测试

前端分析

点击套餐分类时,报404异常,因为还未在服务端做相应的映射地址

  • 点击套餐分类时的请求地址以及请求方式
  • 通过url方式传参,参数类型为key-value键值对形式
    在这里插入图片描述

创建SetmealController的list方法

在SetmealController控制层中,添加list方法
目的:通过套餐种类Id和套餐对应的状态查询出符合条件的套餐

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

测试

前端页面成功显示出儿童套餐种类包含的套餐
在这里插入图片描述

07-购物车_需求分析&数据模型&梳理交互过程&准备工作

需求分析

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

前端页面展示
在这里插入图片描述

数据模型

购物车对应的数据表为shopping_cart表,具体表结构如下:
在这里插入图片描述

梳理交互过程

在开发代码之前,需要梳理一下购物车操作时前端页面和服务端的交互过程:

  1. 点击加入购物车按钮或者*+*按钮,页面发送ajax请求,请求服务端,将菜品或者套餐添加到购物车
  2. 点击购物车图标,页面发送ajax请求,请求服务端查询购物车中的菜品和套餐
  3. 点击清空购物车按钮,页面发送ajax请求,请求服务端来执行清空购物车操作

开发购物车功能,其实就是在服务端编写代码去处理前端页面发送的这3次请求即可

准备工作

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

  • 实体类ShoppingCart(直接从课程资料中导入即可)
  • Mapper接口ShoppingCartMapper
  • 业务层接口ShoppingCartService
  • 业务层实现类ShoppingCartServicelmpl
  • 控制层ShoppingCartController

实体类ShoppingCart

package com.itzq.reggie.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;
}

1234567891011121314151617181920212223242526272829303132333435363738394041424344

Mapper接口ShoppingCartMapper

package com.itzq.reggie.mapper;

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

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

12345678910

业务层接口ShoppingCartService

package com.itzq.reggie.service;

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

public interface ShoppingCartService extends IService<ShoppingCart> {
}

12345678

业务层实现类ShoppingCartServicelmpl

package com.itzq.reggie.service.Impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itzq.reggie.entity.ShoppingCart;
import com.itzq.reggie.mapper.ShoppingCartMapper;
import com.itzq.reggie.service.ShoppingCartService;
import org.springframework.stereotype.Service;

@Service
public class ShoppingCartServicelmpl extends ServiceImpl<ShoppingCartMapper, ShoppingCart> implements ShoppingCartService {
}

123456789101112

控制层ShoppingCartController

package com.itzq.reggie.controller;

import com.itzq.reggie.service.ShoppingCartService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/shoppingCart")
@Slf4j
public class ShoppingCartController {
@Autowired
private ShoppingCartService shoppingCartService;
}

12345678910111213141516

08-购物车_代码开发_添加购物车

前端分析

启动项目,用户登录外卖系统,选择喜欢的菜品或套餐,若用户选择菜品并且该菜品有相应的口味则需要选择口味,点击加入购物车

  • 页面效果展示
  • 前端页面发送的请求地址以及请求方式
    在这里插入图片描述
  • 前端页面以json格式将数据提交给服务端
    在这里插入图片描述

代码

测试服务端是否能够接收前端页面提交的数据

在ShoppingCartController控制层,新增add方法(测试服务端是否可以成功接收前端页面提交的数据)

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

@PostMapping("/add")
public R<ShoppingCart> add(@RequestBody ShoppingCart shoppingCart){
log.info("shoppingCart={}",shoppingCart);

return null;
}
}
1234567891011121314

在指定位置添加断点,便于查看是否成功接收数据
在这里插入图片描述

debug方式启动项目,添加菜品或套餐到购物车
在这里插入图片描述

跳转到服务端,服务端成功接收到前端提交的数据
在这里插入图片描述

完善代码

在ShoppingCartController控制层中的add方法添加代码
目的:将用户添加到购物车的菜品或套餐信息保存到数据库中,若添加相同菜品或相同套餐,则只需要在shopping_cart表更新该菜品或者套餐的数量(number字段);反之,则将菜品或套餐直接保存数据库中

package com.itzq.reggie.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.itzq.reggie.common.BaseContext;
import com.itzq.reggie.common.R;
import com.itzq.reggie.entity.ShoppingCart;
import com.itzq.reggie.service.ShoppingCartService;
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;

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

@PostMapping("/add")
public R<ShoppingCart> add(@RequestBody ShoppingCart shoppingCart){
log.info("shoppingCart={}",shoppingCart);

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

//获取当前菜品id
Long dishId = shoppingCart.getDishId();
//条件构造器
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();

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

//查询当前菜品或者套餐是否在购物车中
ShoppingCart cartServiceOne = shoppingCartService.getOne(queryWrapper);

if (cartServiceOne != null){

//如果已存在就在当前的数量上加1
Integer number = cartServiceOne.getNumber();
cartServiceOne.setNumber(number + 1);
shoppingCartService.updateById(cartServiceOne);
}else {

//如果不存在,则添加到购物车,数量默认为1
shoppingCartService.save(shoppingCart);
cartServiceOne = shoppingCart;
}



return R.success(cartServiceOne);
}
}

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263

测试

重启项目,选择菜品或套餐,加入到购物车中
在这里插入图片描述

shopping_cart表中添加上用户选择的菜品信息
在这里插入图片描述

点击加号或减号可以修改该菜品的数量
目前还未开发减号相关的代码,所以只有加号可以正常操作
在这里插入图片描述

shopping_cart表中的number字段值得到更新
在这里插入图片描述

注意:amount字段的值为该菜品价格,而与数量无关

09-购物车_代码开发_查看购物车&清空购物车

查看购物车

前端分析

查看购物车,前端页面发送ajax请求,以下是请求的方式和请求的地址
在这里插入图片描述

代码开发

在ShoppingCartController类中,添加list方法

	/**
* 查询用户购物车数据
* @return
*/
@GetMapping("list")
public R<List<ShoppingCart>> list(){
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());
queryWrapper.orderByDesc(ShoppingCart::getCreateTime);

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

return R.success(list);
}
1234567891011121314

测试

重启项目,选择所需菜品或套餐,若需选择口味,则在选择口味后点击加入购物车
在这里插入图片描述

点击加入购物车后,页面发送三个ajax请求到服务端

  • add— 》 将所选菜品或套餐添加到数据库表中
  • list— 》 查询用户的购物车信息(需要测试的方法)
  • download— 》 图片下载
    在这里插入图片描述

查询用户购物车信息成功,并在页面展示
在这里插入图片描述

清空购物车

前端分析

点击清空用户购物车,页面发送ajax请求,以下是请求的方式和请求的地址
在这里插入图片描述

代码开发

在ShoppingCartController类中,添加clean方法

	@DeleteMapping("/clean")
public R<String> clean(){
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());

//SQL:delete from shopping_cart where user_id = ?
shoppingCartService.remove(queryWrapper);

return R.success("成功清空购物车");
}
12345678910

测试

重启项目,来到购物车界面,点击清空按钮
在这里插入图片描述

执行成功
在这里插入图片描述

10-用户下单_需求分析&数据模型

需求分析

移动端用户将菜品或者套餐加入购物车后,可以点击购物车中的去结算按钮,页面跳转到订单确认页面,点击去支付按钮,完成下单操作

前端页面效果展示
在这里插入图片描述

数据模型

用户下单业务对应的数据表为orders表和order_detail表

  • orders:订单表,具体表结构如下:
    在这里插入图片描述
  • order_detail:订单明细表,具体表结构如下:
    在这里插入图片描述

11-用户下单_梳理交互过程&准备工作

梳理交互过程

在开发代码之前,需要梳理一下用户下单操作时前端页面和服务端的交互过程:

  1. 在购物车中点击去结算按钮,页面跳转到订单确认页面
  2. 在订单确认页面,发送ajax请求,请求服务端获取当前登录用户的默认地址
  3. 在订单确认页面,发送ajax请求,请求服务端获取当前登录用户的购物车数据
  4. 在订单确认页面点击去支付按钮,发送ajax请求,请求服务端完成下单操作

开发用户下单功能,其实就是在服务端编写代码去处理前端页面发送的请求即可

准备工作

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

  1. 实体类Orders、OrderDetail(直接从课程资料中导入即可)
  2. Mapper接口OrderMapper、OrderDetailMapper
  3. 业务层接口OrderService、OrderDetailService
  4. 业务层实现类OrderServicelmpl、OrderDetailServicelmpl
  5. 控制层OrderController、OrderDetailController

实体类

Orders

package com.itzq.reggie.entity;

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

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

private static final long serialVersionUID = 1L;

private Long id;

//订单号
private String number;

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


//下单用户id
private Long userId;

//地址id
private Long addressBookId;


//下单时间
private LocalDateTime orderTime;


//结账时间
private LocalDateTime checkoutTime;


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


//实收金额
private BigDecimal amount;

//备注
private String remark;

//用户名
private String userName;

//手机号
private String phone;

//地址
private String address;

//收货人
private String consignee;
}

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162

OrderDetail

package com.itzq.reggie.entity;

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

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

private static final long serialVersionUID = 1L;

private Long id;

//名称
private String name;

//订单id
private Long orderId;


//菜品id
private Long dishId;


//套餐id
private Long setmealId;


//口味
private String dishFlavor;


//数量
private Integer number;

//金额
private BigDecimal amount;

//图片
private String image;
}

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647

Mapper接口

OrderMapper

package com.itzq.reggie.mapper;

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

@Mapper
public interface OrderMapper extends BaseMapper<Orders> {
}

12345678910

OrderDetailMapper

package com.itzq.reggie.mapper;

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

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


1234567891011

业务层接口

OrderService

package com.itzq.reggie.service;

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

public interface OrderService extends IService<Orders> {
}

12345678

OrderDetailService

package com.itzq.reggie.service;

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

public interface OrderDetailService extends IService<OrderDetail> {
}

12345678

业务层实现类

OrderServicelmpl

package com.itzq.reggie.service.Impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itzq.reggie.entity.Orders;
import com.itzq.reggie.mapper.OrderMapper;
import com.itzq.reggie.service.OrderService;
import org.springframework.stereotype.Service;

@Service
public class OrderServicelmpl extends ServiceImpl<OrderMapper, Orders> implements OrderService {
}

123456789101112

OrderDetailServicelmpl

package com.itzq.reggie.service.Impl;

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

@Service
public class OrderDetailServicelmpl extends ServiceImpl<OrderDetailMapper, OrderDetail> implements OrderDetailService {
}

123456789101112

控制层

OrderController

package com.itzq.reggie.controller;

import com.itzq.reggie.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

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

}

1234567891011121314151617

OrderDetailController

package com.itzq.reggie.controller;

import com.itzq.reggie.service.OrderDetailService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/orderDetail")
@Slf4j
public class OrderDetailController {
@Autowired
private OrderDetailService orderDetailService;

}

1234567891011121314151617

12-用户下单_代码开发1

前端分析

启动项目,来到外卖初始界面,点击去结算按钮
在这里插入图片描述

页面跳转到确认订单界面,前端界面发送ajax请求,用于获取该用户的默认地址,发现请求失败,服务端没有对应的映射
在这里插入图片描述

在AddressBookController控制层,添加getDefault,请求方式为get请求

 @GetMapping("/default")
public R<AddressBook> getDefault(){
//当前用户id
Long currentId = BaseContext.getCurrentId();

//创建条件构造器
LambdaQueryWrapper<AddressBook> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(AddressBook::getUserId,currentId);
queryWrapper.eq(AddressBook::getIsDefault,1);

AddressBook addressBook = addressBookService.getOne(queryWrapper);

return R.success(addressBook);
}
1234567891011121314

重启项目,点击去结算按钮,跳转到确认订单页面,成功接收到该用户的默认地址
在这里插入图片描述

处理好确认订单页面后,点击去支付,系统报404错误—服务端还未添加相应的映射

以下展示了前端页面发送ajax请求的方式以及请求的地址
在这里插入图片描述

提交给服务端的数据格式为json数据
在这里插入图片描述

代码开发1

在业务层接口OrderService中添加submit方法

package com.itzq.reggie.service;

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

public interface OrderService extends IService<Orders> {
/**
* 用户下单
* @param orders
*/
void submit(Orders orders);
}

12345678910111213

在业务层实现类OrderServicelmpl中,实现业务层接口OrderService添加的submit方法

package com.itzq.reggie.service.Impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itzq.reggie.entity.Orders;
import com.itzq.reggie.mapper.OrderMapper;
import com.itzq.reggie.service.OrderService;
import org.springframework.stereotype.Service;

@Service
public class OrderServicelmpl extends ServiceImpl<OrderMapper, Orders> implements OrderService {

/**
* 用户下单
* @param orders
*/
@Override
public void submit(Orders orders) {
//获取当前用户id

//查询当前用户的购物车数据

//向订单表插入数据,一条数据

//向订单明细表插入数据,多条数据

//清空购物车数据
}
}

1234567891011121314151617181920212223242526272829

在OrderController类中,添加submit方法—通过调用orderService接口实现对数据库的操作

package com.itzq.reggie.controller;

import com.itzq.reggie.common.R;
import com.itzq.reggie.entity.Orders;
import com.itzq.reggie.service.OrderService;
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;

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

@PostMapping("/submit")
public R<String> submit(@RequestBody Orders orders){
log.info("orders={}",orders);

orderService.submit(orders);
return R.success("用户下单成功");
}
}

12345678910111213141516171819202122232425262728

13-用户下单_代码开发2

代码开发2

在业务层实现类OrderServicelmpl的submit方法中,添加逻辑代码

  • 获取当前用户id
  • 查询当前用户的购物车数据
  • 查询用户数据
  • 查询地址信息
	/**
* 用户下单
* @param orders
*/
@Override
@Transactional
public void submit(Orders orders) {
//获取当前用户id
Long currentId = BaseContext.getCurrentId();

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

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

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

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

//向订单表插入数据,一条数据

//向订单明细表插入数据,多条数据

//清空购物车数据
}
1234567891011121314151617181920212223242526272829303132333435

14-用户下单_代码开发3

代码开发3

提供完整的submit方法代码

  • 设置订单id
  • 向订单表设置属性
  • 向订单表插入数据,一条数据
  • 通过stream流,遍历购物车数据来获取的订单明细
  • 向订单明细表插入数据,多条数据
  • 清空购物车—通过用户id作为约束条件
package com.itzq.reggie.service.Impl;

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

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

@Service
public class OrderServicelmpl extends ServiceImpl<OrderMapper, Orders> implements OrderService {

@Autowired
private ShoppingCartService shoppingCartService;

@Autowired
private UserService userService;

@Autowired
private AddressBookService addressBookService;

@Autowired
private OrderDetailService orderDetailService;

/**
* 用户下单
* @param orders
*/
@Override
@Transactional
public void submit(Orders orders) {
//获取当前用户id
Long currentId = BaseContext.getCurrentId();

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

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

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

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

//设置订单id
long orderId = IdWorker.getId();

AtomicInteger amount = new AtomicInteger(0);

List<OrderDetail> orderDetailList= shoppingCarts.stream().map((item) -> {
OrderDetail orderDetail = new OrderDetail();
orderDetail.setOrderId(orderId);
orderDetail.setName(item.getName());
orderDetail.setImage(item.getImage());
orderDetail.setDishId(item.getDishId());
orderDetail.setSetmealId(item.getSetmealId());
orderDetail.setDishFlavor(item.getDishFlavor());
orderDetail.setNumber(item.getNumber());
orderDetail.setAmount(item.getAmount());
amount.addAndGet(item.getAmount().multiply(new BigDecimal(item.getNumber())).intValue());

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

//向订单表设置属性
orders.setId(orderId);
orders.setNumber(String.valueOf(orderId));
orders.setStatus(2);
orders.setUserId(currentId);
orders.setAddressBookId(addressBookId);
orders.setOrderTime(LocalDateTime.now());
orders.setCheckoutTime(LocalDateTime.now());
orders.setAmount(new BigDecimal(amount.get()));
orders.setPhone(addressBook.getPhone());
orders.setUserName(user.getName());
orders.setConsignee(addressBook.getConsignee());
orders.setAddress(
(addressBook.getProvinceName() == null ? "":addressBook.getProvinceName())+
(addressBook.getCityName() == null ? "":addressBook.getCityName())+
(addressBook.getDistrictName() == null ? "":addressBook.getDistrictName())+
(addressBook.getDetail() == null ? "":addressBook.getDetail())
);
//向订单表插入数据,一条数据
super.save(orders);

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

//清空购物车数据
shoppingCartService.remove(queryWrapper);

}
}

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114

15-用户下单_功能测试

功能测试

重启项目,登录用户,来到确定订单页面,点击去支付
在这里插入图片描述

支付成功
在这里插入图片描述

数据库中orders表的变化:
在这里插入图片描述

数据库中order_detail表的变化:
在这里插入图片描述

操作成功