Bootstrap

黑马瑞吉外卖项目简单学习

项目介绍

黑马的公开课程:瑞吉外卖 通过一个小项目入门java开发,这是我目前看过最好的java开发入门课程,不是开发!不会细抠,能看懂大致代码和前后端数据交互流程即可

在这里插入图片描述

  • 开发流程

    • 需求调研
    • UI设计
    • 架构设计、技术选型
    • 代码实现
    • 编写用例
    • 环境搭建,项目上线
  • 后台功能

    • 员工管理:新增员工,编辑禁用
    • 分类管理:新增分类,修改删除分类
    • 菜品管理:新增菜品,修改删除查询
    • 订单明细:查询订单,查看配送
  • 前端H5页面功能

    • 菜品分类展示
    • 选择规格添加购物车
    • 订单支付
    • 个人信息页面

在这里插入图片描述

  • 技术选型

    • 技术选型

    • 用户层:H5、vue.js、elementUI、微信小程序

    • 网关层:Nginx

    • 应用层:SpingBoot、SpingMVC、SpingSession、Sping、Swagger、Lombok(Lombok是一个Java库,能自动插入编辑器并构建工具,简化Java开发)

    • 数据层:MySQL、Mybatis(MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作)、Mybatis Plus、Redis

在这里插入图片描述

  • 角色与权限
    • 后台管理员:拥有后台系统的所有操作权限
    • 后台普通员工:可以登录后台对菜品、套餐、订单进行管理
    • C端用户:可以登录移动端,执行购物个人管理操作

环境准备

新建数据库,字符集使用utf8mb4,和utf8的区别是其使用四个字节,能存储更多字符,比如生僻字、表情符号等

在这里插入图片描述

表结构
在这里插入图片描述

address_book:地址表
category:菜品和套餐分类
dish:菜品表
dish_flavor:菜品口味关系表
employee:员工表
order_detail:订单明细表
orders:订单表
setmeal:套餐类
setmeal_dish:套餐菜品关系表
shopping_cart:购物车表
user:用户表
pom.xml: 项目依赖包配置文件
resources/application.yml: 应用的一些配置
server port 应用端口 
spring application name 应用名称 
datasource 数据库连接相关配置 
mybatis-plus 映射关系 将表和类对应

启动类 :启动类就相当于程序的入口点
在这里插入图片描述

@Slf4jlombok 提供的注解,用于简化日志输出代码

private final Logger logger = LoggerFactory.getLogger(当前类名.class); ---> @Slf4j

几个常用的lombok注解

@Data: 注解在类上, 提供类所有属性的getting和setting方法,和equals、canEqual、hanshCode、toString方法
@Setter: 注解再属性上, 为属性提供setting方法
@Getter: 注解再类上, 为属性提供getting方法
@SneakyThrows: 无需再签名处显示抛出异常
@Log4j: 注解在类上, 为类提供一个属性名为log的log4j对象
@Slf4j: 同上
@NoArgsConstructor: 注解在类上, 为类提供一个无参构造方法
@AllArgsConstructor: 注解在类上, 为类提供一个全参的构造方法

下面是几个SpringBoot提供的注解

@SpringBootApplication注解一般放在项目的一个启动类上,用来把启动类注入到容器中,用来定义容器扫描的范围,用来加载classpath环境中一些bean
使用@ServletComponentScan注解后,ServletFilterListener可以直接通过@WebServlet@WebFilter@WebListener注解自动注册,无需其他代码
使用注解 @EnableTransactionManagement 开启事务支持

注解相当于代码的简化,使用一个注解符@表示一段代码的缩写,很方便!

resources/backend 和 resources/front: 存储静态页面资源,html、css、js

WebMvcConfig:配置类,通过注解@Configuration标识,设置静态资源映射目录,这是因为项目静态资源目录有默认值,我们需要修改为自己的目录

registry.addResourceHandler("/backend/").addResourceLocations("classpath:/backend/"); 
//classpath 指 resources 目录
registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");

目录结构梳理,我们可以通过具体代码再去体会层次关系

reggie:
    common:用来封装一些常用的公共方法
    config:配置类
    controller:请求转发,接收页面过来的参数,传给service处理,接到返回值,再次传给页面
    dto:系统、服务之间交互传输用,是做表示层展示给用户的
    entity:持久化,与数据库对应,一般是用于orm对象关系映射,类似于mvc的model层,这里一个表对应一个类
    filter:过滤器实际上就是对web资源进行拦截,做一些处理后再交给下一个过滤器或servlet处理
    mapper:数据存储对象 相当于dao层,直接执行sql语句,接口提供给service层
    service:业务逻辑层接口,处理完将结果传递给mapper
      impl:service实现类
    utils:工具类
    ReggieApplication:项目启动类
resources:静态资源目录
    backend:后台所需静态资源目录
    front:用户C端所需静态资源目录
    application.yml:应用配置文件
test:
    UploadFileTest:上传测试类
pom.xml:项目依赖配置文件

在这里插入图片描述

后台登录功能

controller

访问后台登陆界面:/backend/page/login/login.html

在这里插入图片描述

在这里插入图片描述

输入账号密码,请求的是emploee/login路径,全局搜索,找到位于controller下的EmployeeController

@RequestMapping("/employee")	

这是用来配置路由的,也就是访问某一路径,交由哪个类,哪个方法来处理

@GetMapping     处理get请求
@PostMapping    处理post请求
@PutMapping     处理put请求
@DeleteMapping  处理delete请求

例如此例,访问emploee/login,就是访问该类配置了@PostMapping("/login")注解的方法,其他同理

@RequestMapping("/employee")	
@PostMapping("/login")
    public R<Employee> login(...){...}

entity

entity目录下,为每个表都映射了一个类,类的内容也是定了表的字段

在这里插入图片描述

举例,如employee表,定义的变量都是字段值

在这里插入图片描述

实体类的封装可以让我们不需要写SQL语句,直接对字段进行操作,字段都封装成属性了

// entity/Employee
@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;

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

}

这个定义有什么用呢,implements Serializable是实现一个序列化的接口,但是Serializable是一个语义级别的空接口,所以只是标识一个类的对象可以被序列化,实现了该接口的类可以被ObjectOutputStream转换为字节流,同时也可以通过ObjectInputStream 再将其解析为对象

@Data实现了get()set()toString()等方法,注解的作用应该可以理解了,提高代码的简洁性

@TableField(fill = FieldFill.INSERT_UPDATE),该注解用于标识非主键的字段,将数据库列与 JavaBean 中的属性进行映射

mapper

mapper目录下定义了每个表的对应的接口,虽然没有什么代码,但是它继承了BaseMapper,里面基本的增删改查方法都有了,<Employee>是标识泛型的

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

关于extendsinterfaceimplement的区别

extends: 继承,继承一个父类的所有功能
interface: 是创建接口的语法,在其中定义某些函数,但没有具体实现
implement: 是使用接口的语法,继承了接口定义的函数并实现

service

service目录定义一个接口,继承IService,也是数据库的操作

// service/EmployeeService
public interface EmployeeService extends IService<Employee> {
}

IServiceBaseMapper的增删改查有什么区别?

IService接口只是对BaseMapper的进一步封装,例如批量操作

impl目录编写对应的实现类

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

这里面提到的一些接口的定义、实现、注解都是按照我们使用的框架提供的规范编写的,帮助我们简化代码,规范流程,具体自己去看一下spingbootmybatis什么的

common/R

通用返回结果,服务端响应的数据都会封装成此对象返回给前端页面

// common/R
@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) {...}

    public static <T> R<T> error(String msg) {...}

    public R<T> add(String key, Object value) {...}
}

我们在前端的js代码中翻看,其中api接口基本都是返回异步请求的数据

$axios是基于promiseajax的一种封装,简而言之,ajax可以实现局部刷新功能

它会去请求对应的url,使用设定的请求方式,put是上传,data是传递的参数,简而言之,是前端去请求后端的方法

// backend/js/
const editOrderDetail = (params) => {  
  return $axios({    
    url: '/order',   
    method: 'put',    
    data: { ...params }  
  })

我们看登录html页面中的js代码,使用await获取后端异步返回的数据,它就可以调用像是codemsg等属性和方法

// backend/page/login/login.html
if (valid) {
    this.loading = true
    let res = await loginApi(this.loginForm)
    if (String(res.code) === '1') {//1表示登录成功
        localStorage.setItem('userInfo',JSON.stringify(res.data))
        window.location.href= '/backend/index.html'
    } else {
        this.$message.error(res.msg)
        this.loading = false

R.java中,这两个方法就是标识登录状态的,在js中进行res.code) === '1'的判断

// common/R
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;
}

登录功能实现

因为后台提供的是管理功能,使用者是内部人员,普通员工和管理员,所以除了简单的账号密码校验,还有一个账号权限的区分,在这里暂时没有体现,还有就是账号的状态,员工账号是否允许正常使用,也是要考虑的问题

// controller/EmployeeController
// 引入了两个封装好的实体类,数据封装类&员工类
import com.itheima.reggie.common.R;
import com.itheima.reggie.entity.Employee;

@PostMapping("/login")
// HttpServletRequest request对象用来获取session
// @RequestBody将前端发送过来的数据封装成employee对象,数据的键值要和类定义的变量名相同才能实现封装
public R<Employee> login(HttpServletRequest request,@RequestBody Employee employee){

//1、将页面提交的密码password进行md5加密处理,前端发送的数据被封装成对象,所以可以直接使用get方法获取
String password = employee.getPassword();
password = DigestUtils.md5DigestAsHex(password.getBytes());

//2、根据页面提交的用户名username查询数据库,因为用户名设置了唯一unique的属性,可以使用getOne方法
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Employee::getUsername,employee.getUsername());
Employee emp = employeeService.getOne(queryWrapper);

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

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

//5、查看员工状态,如果为已禁用状态,则返回员工已禁用结果,0禁用/1正常,根据status字段判断
if(emp.getStatus() == 0){
    return R.error("账号已禁用");
}

//6、登录成功,将员工id存入Session并返回登录成功结果,emp.getId(),emp就是数据库查询结果返回的对象
//session可以说是一个全局数组,它的变量在每个地方都可以访问
request.getSession().setAttribute("employee",emp.getId());
		return R.success(emp);
}

R对象的封装如下,登录失败可以传参返回提示和状态

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

总结:

前端:提供静态资源页面,登录接口通过js进行了简单的位数和是否为空的校验,然后将提交的数据发送给后端

后端:数据进行封装处理,前端传输的数据经过查询判断用户密码是否正确,并将结果存储到session中方便前后端调用

跳转实现:前端通过判断R.success函数返回的r.code = 1来判断登录成功,使用location进行跳转,还将后端返回的数据进行本地缓存

登录后,可以看到本地的缓存了这些数据,spring框架将后端返回的数据封装成了json也就是键值对,我们打开employee表就可以一一对应这些字段

在这里插入图片描述

后台退出功能

登录后,进入到index.html页面,在右上角有当前用户的名称

在这里插入图片描述

这是通过{{ userInfo.name }}实现的,而这个userInfo就是上一步登录后保存到客户端的变量,点击退出请求logout方法

// backend/index.html
<div class="avatar-wrapper">{{ userInfo.name }}</div>
// 点击退出图标就会调用logout函数
<img src="images/icons/[email protected]" class="outLogin" alt="退出" @click="logout" />
// 获取 localStorage 的 userinfo 值
const userInfo = window.localStorage.getItem('userInfo')
// logout函数首先通过logoutapi请求employee/logout controller接口,判断是否为登录状态,是的话将userinfo信息清除,然后重定向到登录页面
logout() {
    // logoutApi()会请求logout方法,返回code参数
    logoutApi().then((res)=>{
    if(res.code === 1){
        localStorage.removeItem('userInfo')
        window.location.href = '/backend/page/login/login.html'
}})}

后端除了将res.code的值返回给前端(前端没有保存),还会将

// controller/EmployeeController
@PostMapping("/logout")
public R<String> logout(HttpServletRequest request){
    // 清理Session中保存的当前登录员工的id,之前登录成功后存到session里了
    request.getSession().removeAttribute("employee");
    // 会返回r.code=1表示退出成功
    return R.success("退出成功");

关于权限校验

这时我们直接访问index.html页面,也可以直接访问,原因是没有权限校验,在渗透中就可以测试未授权访问

我们可以通过实现过滤器或拦截器来控制用户登录

// filter/LoginCheckFilter
@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
public class LoginCheckFilter implements Filter{
  // 定义不需要处理的请求路径
    String[] urls = new String[]{
        "/employee/login",
        "/employee/logout",
        // 静态资源的请求都不拦截,即使访问到index页面,也无法看到数据,也不能进行其他操作
        "/backend/**",
        "/front/**",
        "/common/**",
        "/user/sendMsg",
        "/user/login"
    }
  // 判断本次请求是否需要处理
  // 判断是否登录,未登录就跳转登录页面
  ...
  // 判断登录状态,如果已登录,则直接放行,通过判断查询结果是否为空,防止前端通过code值判断容易被篡改,employee的值就是用户id
    if(request.getSession().getAttribute("employee") != null){
        log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("employee"));

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

        filterChain.doFilter(request,response);
        return;
    }
  ...
}
// 在启动类加上注解,有了这个注解才会扫描过滤器将其创建成功
@ServletComponentScan

而前端也有对应的响应拦截器,为了前端进行判断以便做出不同响应,比如这里根据后端登录数据判断是否需要跳转登录

// backend/js/request.js
if (res.data.code === 0 && res.data.msg === 'NOTLOGIN') {// 返回登录页面
    console.log('---/backend/page/login/login.html---',code)
    localStorage.removeItem('userInfo')
    window.top.location.href = '/backend/page/login/login.html'
} else {
    return res.data

页面切换

登录后台,点击左侧菜单栏,网址栏的url并没有发生变化,都是backend/index.html,它请求了不同的list.html

page/food/list.htmlpage/category/list.html,这就是ajax的应用,部分刷新

在这里插入图片描述

// backend/index.html 在上方有遍历的实现 不在这里讨论
menuList: [
  {
    id: '2',
    name: '员工管理',
    url: 'page/member/list.html',
    icon: 'icon-member'
  },
  {
    id: '3',
    name: '分类管理',
    url: 'page/category/list.html',
    icon: 'icon-category'
  }
...
menuHandle(item, goBackFlag) {
    // iframe 用于在网页内显示网页
    this.iframeUrl = item.url
}
// iframe的设置,默认打开 iframeUrl: 'page/member/list.html'
<iframe
    id="cIframe"
    class="c_iframe"
    name="cIframe"
    :src="iframeUrl"
    width="100%"
    height="auto"
    frameborder="0"
    v-show="!loading"
></iframe>

page目录,是每个具体的list页面

员工管理

首先是添加员工的功能,请求add.html页面,很容易就明白是一个数据库的操作

在这里插入图片描述

member.lst中,点击添加员工按钮会调用addMemberHandle函数

@click="addMemberHandle('add')"
//调用函数,并请求add.html页面
addMemberHandle (st) {
    if (st === 'add'){
      // menuHandle主要的功能就是iframe.url参数设置,进行窗口切换
      window.parent.menuHandle({
        id: '2',
        url: '/backend/page/member/add.html',
        name: '添加员工'
      },true)
    } else {
      //修改员工同样是调用这个函数
      window.parent.menuHandle({
        id: '2',
        url: '/backend/page/member/add.html?id='+st,
        name: '修改员工'
      },true)
    }
  }

添加员工后,我们请求了employee接口,并传递了json格式的数据

// 点击保存,调用submitForm
@click="submitForm('ruleForm', false)"
// submitForm调用了addEmployee函数
addEmployee(params).then(res => {...}
// member.js提交参数给employeeController处理
function addEmployee (params) {
  return $axios({
    url: '/employee',
    method: 'post',
    data: { ...params }
  })
}

在这里插入图片描述

传进来的数据进行加密,然后传给Service层处理,再传递给mapper层,save()就是给我们提供的插入数据的方法

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

// save
default boolean save(T entity) {
  return SqlHelper.retBool(this.getBaseMapper().insert(entity));
}
// insert()插入实体
int insert(T entity);

全局异常处理

GlobalExceptionHandler通过拦截所有带有以下两个注解的类,来捕捉其中的异常,try catch的集中管理

@ControllerAdvice(annotations = {RestController.class, Controller.class})

员工信息分页查询

流程如下:

访问员工管理页面,会请求list.html,然后通过函数请求employee/page接口

async init () {
    const params = {
      page: this.page,
      pageSize: this.pageSize,
      name: this.input ? this.input : undefined
    }
    // getMemberList请求了EmployeeController接口的page方法
    await getMemberList(params).then(res => {
      if (String(res.code) === '1') {
        this.tableData = res.data.records || []
        this.counts = res.data.total
      }
    }).catch(err => {
      this.$message.error('请求出错了:' + err)
    })
  }

1.页面发送ajax请求,将分页查询参数提交到服务端,如page=2pageSize=2

在这里插入图片描述

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

很多东西框架都给封装好了,所以看起来会云里雾里不知其所以然,比如这里的page方法也都是继承的,它直接提供分页查询的功能,很多代码真的都是高度重复的,我们主要关注参数的处理流程就好了,就是这三个参数

@GetMapping("/page")
    public R<Page> page(int page,int pageSize,String name){
        //接收了三个参数,name是查询接口的参数
        log.info("page = {},pageSize = {},name = {}" ,page,pageSize,name);
        //构造分页构造器
        Page pageInfo = new Page(page,pageSize);
        //构造条件构造器
        LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper();
        //添加过滤条件,name参数不为空,传递为like语句的参数
        queryWrapper.like(StringUtils.isNotEmpty(name),Employee::getName,name);
        //添加排序条件
        queryWrapper.orderByDesc(Employee::getUpdateTime);
        //执行查询
        employeeService.page(pageInfo,queryWrapper);
        return R.success(pageInfo);
    }

3.返回封装略过,分页查询功能处理一个查询语句,其他都是对数据进行一些显示方面处理,不只是这里,这个项目很多的功能都是这样的,只是一个数据库操作,但是它也没有在代码中进行sql语句的拼接,就比较安全

启用禁用员工账号

禁用:直接将用户的状态修改为 0,交给employeeController处理,status是判断用户是否正常使用的参数,之前在实现登录功能的时候我们进行了判断

在这里插入图片描述

启用:将参数值修改为 1,它们都是使用PUT方式提交数据,那对应的方法就是employeeController中带有@PutMapping注解的update方法

在这里插入图片描述

需要注意的是,只有管理员有操作权限,在此处做了判断,如果不是管理员就不显示此功能,具体的信息获取还是之前的localStorage

<el-button
    type="text"
    size="small"
    class="blueBug"
    @click="addMemberHandle(scope.row.id)"
    :class="{notAdmin:user !== 'admin'}"
    >
      编辑
    </el-button>
    <el-button
    type="text"
    size="small"
    class="delBut non"
    @click="statusHandle(scope.row)"
    v-if="user === 'admin'"
    >
    {{ scope.row.status == '1' ? '禁用' : '启用' }}
</el-button>
// 获取username值并赋值给 user
created() {
    this.init()
    if(localStorage.getItem('userInfo') != null){
      //获取当前登录员工的账号,并赋值给模型数据user
      this.user = JSON.parse(localStorage.getItem('userInfo')).username
    }
  }

在请求时通过Cookie: JSESSIONID=xxxxxxxxxxxxxxxx来标识身份

在这里插入图片描述

编辑员工信息

首先是请求add.html页面,查询对应id的详细信息

@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("没有查询到对应员工信息");
    }

接下来和启用禁用账号是一个方法,是一个更新操作,很简单,只不过启用禁用功能只更新一个status值,这些都是mybatisPlus提供的封装好的方法

@PutMapping
    public R<String> update(HttpServletRequest request,@RequestBody Employee employee){
        log.info(employee.toString());
        long id = Thread.currentThread().getId();
        log.info("线程id为:{}",id);
        //employee是登录时保存的用户id
        employeeService.updateById(employee);
        return R.success("员工信息修改成功");
    }

在这个过程中,员工id会因为js精度的问题有所损失,所以要进行对象数据转换,在JacksonObjectMapper中提供对象转换器

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

WebMvcConfig中还有消息转换器的具体利用代码将对象进行转换,它是扩展了spring提供的默认转换器的功能

越权漏洞

员工管理

我们发现它是通过获取本地缓存来判断用户身份的,我们可以尝试修改进行越权测试

使用普通用户test登录

在这里插入图片描述

修改localStroage中的username参数为admin

在这里插入图片描述

刷新,可以发现能够看到启用禁用功能,那么能否使用这个功能呢?

在这里插入图片描述

我们尝试禁用user账户,确实是成功了

在这里插入图片描述

在这里插入图片描述

它只是通过cookie来判断用户是否登录,请求该接口时并没有对身份进行后端的验证,我们可以很容易伪造, 并不安全

编辑信息

在编辑信息时,我们传递了员工id用来查询对应数据

在这里插入图片描述

如果修改为管理员id,我们就可以查询管理员信息

在这里插入图片描述

修改时可以尝试修改密码加密后的md5值,举例子,从123456换成456789

在这里插入图片描述

这样我们就可以使用自定义的密码登录admin账号了

在这里插入图片描述

公共字段自动填充

在很多表中,都有创建时间、更新时间、创建人、修改人这几个字段,每次都要设置很不方便,所以使用mybatisplus提供的方法进行简化

实现步骤:

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

@TableField(fill = FieldFill.INSERT) //插入时填充字段
private LocalDateTime createTime;

@TableField(fill = FieldFill.INSERT_UPDATE) //插入和更新时填充字段
private LocalDateTime updateTime;

@TableField(fill = FieldFill.INSERT) //插入时填充字段
private Long createUser;

@TableField(fill = FieldFill.INSERT_UPDATE) //插入和更新时填充字段
private Long updateUser;

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

/**
 * 自定义元数据对象处理器
 */
@Component
@Slf4j
public class MyMetaObjecthandler implements MetaObjectHandler {...}

客户端发送的每次http请求,在服务器都会分配一个新的线程来处理

BaseContext工具类为了解决在上述MetaObjectHandler方法中无法获取用户id的问题,通过线程变量来获取具体值

新增分类

也是对数据库表的操作,对应category实体类,新增菜品分类和新增套餐分类两个功能对应一个方法,只不过通过type参数来区分

在这里插入图片描述

直接将传递的参数进行保存

在这里插入图片描述

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

分类信息查询

流程应该很熟了,请求的接口都在category.js中,这里是请求查询数据表,和之前的查询列表功能差不多,不多说

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

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

删除分类

通过id删除数据,使用自定义的方法

categoryService.remove(id);

在这里插入图片描述

service中声明

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

servoceImpl中实现

public class CategoryServiceImpl extends ServiceImpl<CategoryMapper,Category> implements CategoryService{
		// 使用@Autowired将实体注入就可以在此查询使用
    @Autowired
    private DishService dishService;
    @Autowired
    private SetmealService setmealService;

    @Override
    public void remove(Long id) {
        // 根据套餐id查询是否有关联菜品,如果有菜品属于这个分类,就不删除,抛出异常,如果没有菜品属于此分类,就正常删除
        ...
    }
}

因为是使用的提供的源码,这里还检查了是否关联了套餐分类,但是没有关联套餐也不能删除,原因是源码查询语句少了一个参数

// CategoryServiceImpl删除方法 检查是否关联套餐,将语句添加如下参数 
int count2 = setmealService.count();
int count2 = setmealService.count(setmealLambdaQueryWrapper);

在这里插入图片描述

修改分类

经过学习,很容易就知道是一个数据表更新操作

在这里插入图片描述

这是mybatisplus提供的方法,我们直接使用,就是更新功能

@PutMapping
    public R<String> update(@RequestBody Category category){
        // 输出日志
        log.info("修改分类信息:{}",category);
        // 更新数据
        categoryService.updateById(category);
        // 返回执行结果是否成功
        return R.success("修改分类信息成功");
    }

文件上传功能

上传使用form表单,要求使用post方式提交,采用enctype="multipart/form-data"格式上传,使用file控件上传type="file"

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

在这里使用elementUI前端框架提供的upload组件,本质是相同的

// 上传按钮
<el-upload
    class="avatar-uploader"
		// 请求后端方法
    action="/common/upload"
    :show-file-list="false"
    :on-success="handleAvatarSuccess"
    :on-change="onChange"
    ref="upload"
>
// 调用的js上传函数,对后缀名和文件大小做简单的前端校验
onChange (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
      }
  }

上传后,后端使用apache组件commons-fileuploadcommons-io接收

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

        // 原始文件名,通过最后一个 . 来分割判断后缀名,即文件类型
        String originalFilename = file.getOriginalFilename();//abc.jpg
        String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));

        // 使用UUID重新生成文件名,防止文件名称重复造成文件覆盖
        String fileName = UUID.randomUUID().toString() + suffix;//dfsdfdfd.jpg

        // 创建一个目录对象,用来存储临时文件,主要是修改保存路径的时候方便
        File dir = new File(basePath);
        // 判断当前目录是否存在
        if(!dir.exists()){
            // 目录不存在,需要创建
            dir.mkdirs();
        }

        try {
            // 将临时文件转存到指定位置
            file.transferTo(new File(basePath + fileName));
        } catch (IOException e) {
            e.printStackTrace();
        }
        return R.success(fileName);
    }
// application.yml中配置路径
reggie:
  path: D:\img\

文件下载

上传后会调用handleAvatarSuccess回调函数,请求图片进行预览

// 检查是否登录,如果登录,上传后将在上传窗口预览图片
handleAvatarSuccess (response, file, fileList) {
    // 拼接down接口预览
    if(response.code === 0 && response.msg === '未登录'){
        window.top.location.href = '/backend/page/login/login.html'
    }else{
        this.imageUrl = `/common/download?name=${response.data}`
        this.ruleForm.image = response.data
    }
}

上传后请求下载图片

在这里插入图片描述

后端通过输入输出流读取传递文件

@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){
                outputStream.write(bytes,0,len);
                outputStream.flush();
            }
            //关闭资源
            outputStream.close();
            fileInputStream.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

新增菜品

首先查询了已有的菜品分类,方便进行下拉框的选择,将它归类

在这里插入图片描述

调用submitForm函数

<el-button
  type="primary"
  @click="submitForm('ruleForm')"
  >
    保存

submitForm函数也调用了几个函数,对应请求后端接口

submitForm(formName, st) {
    // 判断图片是否上传,存在返回路径,若不存在,编辑后重新提交
    if(!this.imageUrl){...}
    // 请求 /dish 接口,使用 post 方法
    addDish(params).then(res => {...}
    // 请求 /dish 接口,使用 put 方法
    editDish(params).then(res => {...}
}

json数据上传进行数据表的更新

在这里插入图片描述

// adddish后端实现
dishService.saveWithFlavor(dishDto);

菜品信息查询

和之前的详细信息查询一个思路,它请求接口传递参数查询数据库之后会请求所有需要的菜品图片

在这里插入图片描述

差不多的查询操作,就略过了,后面的数据库查询也不说了,大差不差

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

修改菜品

除了也有的查询菜品分类,在新增菜品时,它首先根据id查询了对应菜品的数据然后返回

在这里插入图片描述

返回后修改重新提交

在这里插入图片描述

// editdish后端实现
dishService.updateWithFlavor(dishDto);

新增套餐

查询已有套餐分类,只有商务套餐和儿童套餐

在这里插入图片描述

查询已有分类,这两个都是为了后续填选用的

在这里插入图片描述

请求了菜品列表,默认请求了第一个

在这里插入图片描述

点击不同菜品会请求不同分类

在这里插入图片描述

新增

在这里插入图片描述

套餐信息查询

同样的操作,不复述了

在这里插入图片描述

删除套餐

同理,根据id删除

在这里插入图片描述

短信发送

阿里云短信服务使用云服务提供的api接口进行短信发送,申请比较困难,看看视频和文档

顺便一提,阿里云有一个accesskey,它拥有账号完全的权限,如下,是键值对类型的,如果泄露也是很危险的 👉记一次阿里云主机泄露Access Key到Getshell

在这里插入图片描述

它可以创建用户,分组,并授予不同的权限进行操作

在这里插入图片描述

获取验证码流程如下

在这里插入图片描述

使用随机函数生成四位数字验证码,保存到服务端session,然后通过短信api发送给用户,用户输入返回通过session验证

// 点击获取验证码,调用getcode函数
<span @click='getCode'>获取验证码</span>
// getcode函数将手机号传递,并调用sendMsgApi
sendMsgApi({phone:this.form.phone})
// 请求后端接口,并传递手机号
function sendMsgApi(data) {
    return $axios({
        'url': '/user/sendMsg',
        'method': 'post',
        data
    })
}
// 调用后端 UserController
@PostMapping("/sendMsg")
    public R<String> sendMsg(@RequestBody User user, HttpSession session){
        //获取手机号
        String phone = user.getPhone();
        if(StringUtils.isNotEmpty(phone)){
            // 生成随机的 4 位验证码
            String code = ValidateCodeUtils.generateValidateCode(4).toString();
            log.info("code={}",code);
            // 将生成的验证码通过阿里云提供的短信服务 API 发送给用户
            SMSUtils.sendMessage("瑞吉外卖","",phone,code);
            // 将生成的验证码保存到 Session,用于后续验证
            session.setAttribute(phone,code);
            return R.success("手机验证码短信发送成功");
        }

验证

@PostMapping("/login")
    public R<User> login(@RequestBody Map map, HttpSession session){
    //获取手机号,这种取值方式不太了解
    String phone = map.get("phone").toString();
    //获取验证码
    String code = map.get("code").toString();
    //从Session中获取保存的验证码
    Object codeInSession = session.getAttribute(phone);
    //进行验证码的比对(页面提交的验证码和Session中保存的验证码比对)
    if(codeInSession != null && codeInSession.equals(code)){...}
    ...
        // 登录成功保存用户id,之前是设置employee属性,这里改成用户对象
        session.setAttribute("user",user.getId());
    ...}      

登录成功之后手机号已经显示,其他是默认的

在这里插入图片描述

用户地址

address_book表中管理

新增:将数据插入数据表

修改:将数据更新到数据表

设为默认:根据is_default参数来判断是否是默认地址

查询:根据用户id查询所有地址,is_default为默认地址

菜品展示

之前已经写过了category/list,和第一个菜品分类列表

在这里插入图片描述

购物车

添加购物车

在这里插入图片描述

@PostMapping("/add")
    public R<ShoppingCart> add(@RequestBody ShoppingCart shoppingCart){
        // 设置用户id,指定当前是哪个用户的购物车数据
        Long currentId = BaseContext.getCurrentId();
        shoppingCart.setUserId(currentId);
        Long dishId = shoppingCart.getDishId();
        LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(ShoppingCart::getUserId,currentId);   
				// 判断添加的是菜品还是套餐
        if(dishId != null){
            //添加到购物车的是菜品
            queryWrapper.eq(ShoppingCart::getDishId,dishId);
        }else{
            //添加到购物车的是套餐
            queryWrapper.eq(ShoppingCart::getSetmealId,shoppingCart.getSetmealId());
        }
        //查询当前菜品或者套餐是否在购物车中
        //SQL:select * from shopping_cart where user_id = ? and dish_id/setmeal_id = ?
        ShoppingCart cartServiceOne = shoppingCartService.getOne(queryWrapper);

        if(cartServiceOne != null){
            //如果已经存在,就在原来数量基础上加一
            Integer number = cartServiceOne.getNumber();
            cartServiceOne.setNumber(number + 1);
            shoppingCartService.updateById(cartServiceOne);
        }else{
            //如果不存在,则添加到购物车,数量默认就是一
            shoppingCart.setNumber(1);
            shoppingCart.setCreateTime(LocalDateTime.now());
            shoppingCartService.save(shoppingCart);
            cartServiceOne = shoppingCart;
        }
        return R.success(cartServiceOne);
    }

对应的是shopping_cart

在这里插入图片描述

如果是套餐数量减一,请求的是sub方法,源码中没有开发这部分,其他清空、查看就略过不说了

用户下单

首先查询地址列表并返回,然后查询购物车列表返回

在这里插入图片描述

支付,请求order/sumit,后面是对接支付接口,这里就自己去开发吧

在这里插入图片描述

很多细节的东西都在这里没有细讲,我也学到🤮了,自己体会

;