logo头像

分享技术,品味人生

NO Title!

瑞吉外卖-Day01

课程内容

  • 软件开发整体介绍

  • 瑞吉外卖项目介绍

  • 开发环境搭建

  • 后台登录功能开发

  • 后台退出功能开发

1. 软件开发整体介绍

作为一名软件开发工程师,我们需要了解在软件开发过程中的开发流程, 以及软件开发过程中涉及到的岗位角色,角色的分工、职责, 并了解软件开发中涉及到的三种软件环境。那么这一小节,我们将从 软件开发流程、角色分工、软件环境 三个方面,来整体上介绍一下软件开发。

1.1 软件开发流程

image-20210725231106363

1). 第1阶段: 需求分析

完成产品原型、需求规格说明书的编写。

产品原型,一般是通过网页(html)的形式展示当前的页面展示什么样的数据, 页面的布局是什么样子的,点击某个菜单,打开什么页面,点击某个按钮,出现什么效果,都可以通过产品原型看到。

需求规格说明书, 一般来说就是使用 Word 文档来描述当前项目有哪些功能,每一项功能的需求及业务流程是什么样的,都会在文档中描述。

2). 第2阶段: 设计

设计的内容包含 产品设计、UI界面设计、概要设计、详细设计、数据库设计。

在设计阶段,会出具相关的UI界面、及相关的设计文档。比如数据库设计,需要设计当前项目中涉及到哪些数据库,每一个数据库里面包含哪些表,这些表结构之间的关系是什么样的,表结构中包含哪些字段,字段类型都会在文档中描述清楚。

3). 第3阶段: 编码

编写项目代码、并完成单元测试。

作为软件开发工程师,我们主要的工作就是在该阶段, 对分配给我们的模块功能,进行编码实现。编码实现完毕后,进行单元测试,单元测试通过后再进入到下一阶段。

4). 第4阶段: 测试

在该阶段中主要由测试人员, 对部署在测试环境的项目进行功能测试, 并出具测试报告。

5). 第5阶段: 上线运维

在项目上线之前, 会由运维人员准备服务器上的软件环境安装、配置, 配置完毕后, 再将我们开发好的项目,部署在服务器上运行。

我们作为软件开发工程师, 我们主要的任务是在编码阶段, 但是在一些小的项目组当中, 也会涉及到数据库的设计、测试等方面的工作。

1.2 角色分工

学习了软件开发的流程之后, 我们还有必要了解一下在整个软件开发过程中涉及到的岗位角色,以及各个角色的职责分工。

image-20210725234015404

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

上述我们讲解的角色分工, 是在一个项目组中比较标准的角色分工, 但是在实际的项目中, 有一些项目组由于人员配置紧张, 可能并没有专门的架构师或测试人员, 这个时候可能需要有项目经理或者程序员兼任。

1.3 软件环境

image-20210725234046008

在我们日常的软件开发中,会涉及到软件开发中的三套环境, 那么这三套环境分别是: 开发环境、测试环境、生产环境。 接下来,我们分别介绍一下这三套环境的作用和特点。

1). 开发环境(development)

我们作为软件开发人员,在开发阶段使用的环境,就是开发环境,一般外部用户无法访问。

比如,我们在开发中使用的MySQL数据库和其他的一些常用软件,我们可以安装在本地, 也可以安装在一台专门的服务器中, 这些应用软件仅仅在软件开发过程中使用, 项目测试、上线时,我们不会使用这套环境了,这个环境就是开发环境。

2). 测试环境(testing)

当软件开发工程师,将项目的功能模块开发完毕,并且单元测试通过后,就需要将项目部署到测试服务器上,让测试人员对项目进行测试。那这台测试服务器就是专门给测试人员使用的环境, 也就是测试环境,用于项目测试,一般外部用户无法访问。

3). 生产环境(production)

当项目开发完毕,并且由测试人员测试通过之后,就可以上线项目,将项目部署到线上环境,并正式对外提供服务,这个线上环境也称之为生产环境。

 拓展知识:

准生产环境: 对于有的公司来说,项目功能开发好, 并测试通过以后,并不是直接就上生产环境。为了保证我们开发的项目在上线之后能够完全满足要求,就需要把项目部署在真实的环境中, 测试一下是否完全符合要求啊,这时候就诞生了准生产环境,你可以把他当做生产环境的克隆体,准生产环境的服务器配置, 安装的应用软件(JDK、Tomcat、数据库、中间件 …) 的版本都一样,这种环境也称为 “仿真环境”。

​ ps.由于项目的性质和类型不同,有的项目可能不需要这个环境

2. 瑞吉外卖项目介绍

在开发瑞吉外卖这个项目之前,我们需要全方位的来介绍一下当前我们学习的这个项目。接下来,我们将从以下的五个方面, 来介绍瑞吉外卖这个项目。

2.1 项目介绍

image-20210726000655646

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

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

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

2.2 产品原型

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

注意事项: 产品原型主要用于展示项目的功能,并不是最终的页面效果。

在课程资料的产品原型文件夹下,提供了两份产品原型。

image-20210726002509573

1). 管理端

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

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

2). 用户端

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

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

2.3 技术选型

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

image-20210726005437857

1). 用户层

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

2). 网关层

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

3). 应用层

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

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

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

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

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

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

4). 数据层

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

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

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

5). 工具

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

maven: 项目构建工具。

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

2.4 功能架构

image-20210726122825225

1). 移动端前台功能

手机号登录 , 微信登录 , 收件人地址管理 , 用户历史订单查询 , 菜品规格查询 , 购物车功能 , 下单 , 分类及菜品浏览。

2). 系统管理后台功能

员工登录/退出 , 员工信息管理 , 分类管理 , 菜品管理 , 套餐管理 , 菜品口味管理 , 订单管理 。

2.5 角色

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

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

3. 开发环境搭建

3.1 数据库环境搭建

3.1.1 创建数据库

可以通过以下两种方式中的任意一种, 来创建项目的数据库:

1).图形界面

image-20210726123903694

注意: 本项目数据库的字符串, 选择 utf8mb4

2).命令行

image-20210726123942443

3.1.2 数据库表导入

项目的数据库创建好了之后, 可以直接将 资料/数据模型/db_reggie.sql 直接导入到数据库中, 也可以通过两种方式实现:

1).图形界面

image-20210726124752975

2).命令行

image-20210726124818880

注意: 通过命令导入表结构时,注意sql文件不要放在中文目录中

3.1.3 数据库表介绍

数据库表导入之后, 接下来介绍一下本项目中所涉及到的表结构:

序号 表名 说明
1 employee 员工表
2 category 菜品和套餐分类表
3 dish 菜品表
4 setmeal 套餐表
5 setmeal_dish 套餐菜品关系表
6 dish_flavor 菜品口味关系表
7 user 用户表(C端)
8 address_book 地址簿表
9 shopping_cart 购物车表
10 orders 订单表
11 order_detail 订单明细表

上述的表结构, 我们目前先简单的结合页面原型了解一下, 大概有那些表, 每张表结构中存储什么样的数据, 有一个印象。对于具体的表结构, 以及表结构中的字段, 在讲解具体的功能开发时, 我们再详细介绍。

3.2 Maven项目搭建

3.2.1 创建maven项目

1). 在idea中创建maven project, 项目名称 reggie_take_out

image-20210726172842675

2). 检查项目编码

image-20210726173036263

3). 检查maven配置

image-20210726173116359

4). 检查JDK版本

image-20210726173237154

JDK的版本选择1.8;

3.2.2 搭建基础环境

1).在pom.xml中导入依赖

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

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

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <scope>compile</scope>
    </dependency>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.4.2</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.20</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.76</version>
    </dependency>
    <dependency>
        <groupId>commons-lang</groupId>
        <artifactId>commons-lang</artifactId>
        <version>2.6</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.1.23</version>
    </dependency>
</dependencies>

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

2).在工程的resources目录下创建application.yml文件,并引入配置

server:
  port: 8080
spring:
  application:
      #应用名称 , 可选
    name: reggie_take_out
  datasource:
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/reggie?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
      username: root
      password: root
mybatis-plus:
  configuration:
    #在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射 address_book ---> AddressBook
    map-underscore-to-camel-case: true
    #日志输出
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      id-type: ASSIGN_ID

3).创建包 com.itheima.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("项目启动成功...");
    }
}

@Slf4j :

​ 是lombok中提供的注解, 用来通过slf4j记录日志。

当搭建完上述的基础环境之后, 就可以通过引导类, 启动该项目。

3.2.3 前端静态资源导入

我们作为服务端开发工程师, 我们课程学习的重心应该放在后端的业务代码上, 前端的页面我们只需要导入课程资料中的前端资源, 前端页面的代码我们只需要能看懂即可。

1). 导入静态资源

前端资源存放位置为 资料/前端资源 : image-20210726225948411

将上述两个目录中的静态资源文件, 导入到项目的resources目录下:

image-20210726230327313

2). 创建配置类WebMvcConfig,设置静态资源映射

用于在Springboot项目中, 默认静态资源的存放目录为 : “classpath:/resources/“, “classpath:/static/“, “classpath:/public/“ ; 而在我们的项目中静态资源存放在 backend, front 目录中, 那么这个时候要想访问到静态资源, 就需要设置静态资源映射。

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

3). 访问测试

http://localhost:8080/backend/index.html

image-20210726232859596

4. 后台系统登录功能

4.1 需求分析

1). 页面原型展示

image-20210726233540703

2). 登录页面成品展示

登录页面存放目录 /resources/backend/page/login/login.html

image-20210726233631409

3). 查看登录请求

通过浏览器调试工具(F12),可以发现,点击登录按钮时,页面会发送请求(请求地址为http://localhost:8080/employee/login)并提交参数 username和password, 请求参数为json格式数据 {“username”:”admin”,”password”:”123456”}。

image-20210726234439684

此时报404,是因为我们的后台系统还没有响应此请求的处理器,所以我们需要创建相关类来处理登录请求 ;

image-20210726234548093

4). 数据模型(employee表)

image-20210726234915737

5). 前端页面分析

image-20210727000040403

当点击 “登录” 按钮, 会触发Vue中定义的 handleLogin 方法:

image-20210727000329958

在上述的前端代码中, 大家可以看到, 发送登录的异步请求之后, 获取到响应结果, 在响应结果中至少包含三个属性: code、data、msg 。

由前端代码,我们也可以看到,在用户登录成功之后,服务端会返回用户信息,而前端是将这些用户信息,存储在客户端的 localStorage 中了。

localStorage.setItem('userInfo',JSON.stringify(res.data))

4.2 代码开发

4.2.1 基础准备工作

在进行登录功能的代码实现之前, 首先在我们的工程下创建包结构:

image-20210727001117783

1). 创建实体类Employee

该实体类主要用于和员工表 employee 进行映射。 该实体类, 也可以直接从资料( 资料/实体类 )中拷贝工程中。

所属包: com.itheima.reggie.entity

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

@Data
public class Employee implements Serializable {
    private static final long serialVersionUID = 1L;

    private Long id;

    private String username;

    private String name;

    private String password;

    private String phone;

    private String sex;

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

    private Integer status;

    private LocalDateTime createTime;

    private LocalDateTime updateTime;

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

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

2). 定义Mapper接口

在MybatisPlus中, 自定义的Mapper接口, 需要继承自 BaseMapper。

所属包: com.itheima.reggie.mapper

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

3).Service接口

本项目的Service接口, 在定义时需要继承自MybatisPlus提供的Service层接口 IService, 这样就可以直接调用 父接口的方法直接执行业务操作, 简化业务层代码实现。

所属包: com.itheima.reggie.service

public interface EmployeeService extends IService<Employee> {
}

4). Service实现类

所属包: com.itheima.reggie.service.impl

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

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

5). Controller基础代码

所属包: com.itheima.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;
    
}    

6). 导入通用结果类R

此类是一个通用结果类,服务端响应的所有结果最终都会包装成此种类型返回给前端页面。

所属包: com.itheima.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;
    }
}

A. 如果业务执行结果为成功, 构建R对象时, 只需要调用 success 方法; 如果需要返回数据传递 object 参数, 如果无需返回, 可以直接传递null。

B. 如果业务执行结果为失败, 构建R对象时, 只需要调用error 方法, 传递错误提示信息即可。

4.2.2 登录逻辑分析

image-20210727003101031

处理逻辑如下:

①. 将页面提交的密码password进行md5加密处理, 得到加密后的字符串

②. 根据页面提交的用户名username查询数据库中员工数据信息

③. 如果没有查询到, 则返回登录失败结果

④. 密码比对,如果不一致, 则返回登录失败结果

⑤. 查看员工状态,如果为已禁用状态,则返回员工已禁用结果

⑥. 登录成功,将员工id存入Session, 并返回登录成功结果

4.2.3 代码实现

技术点说明:

A. 由于需求分析时, 我们看到前端发起的请求为post请求, 所以服务端需要使用注解 @PostMapping

B. 由于前端传递的请求参数为json格式的数据, 这里使用Employee对象接收, 但是将json格式数据封装到实体类中, 在形参前需要加注解@RequestBody

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

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

    //2、根据页面提交的用户名username查询数据库
    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、查看员工状态,如果为已禁用状态,则返回员工已禁用结果
    if(emp.getStatus() == 0){
        return R.error("账号已禁用");
    }

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

4.3 功能测试

代码实现完毕后, 启动项目, 访问url: http://localhost:8080/backend/page/login/login.html , 进行登录测试。

在测试过程中, 可以通过debug断点调试的方式来跟踪程序的执行过程,并且可以查看程序运行时各个对象的具体赋值情况。而且需要注意, 在测试过程中,需要将所有的情况都覆盖到。

1). 问题说明

当我们在进行debug端点调试时, 前端可能会出现如下问题: 前端页面的控制台报出错误-超时;

image-20210727004455855

2). 解决方案

前端进行异步请求时, 默认超时10000ms , 可以将该值调大一些。

image-20210727004706639

==由于修改了JS文件,需要手动清理一下浏览器缓存,避免缓存影响,JS不能及时生效。==

5. 后台系统退出功能

5.1 需求分析

在后台管理系统中,管理员或者员工,登录进入系统之后,页面跳转到后台系统首页面(backend/index.html),此时会在系统的右上角显示当前登录用户的姓名。

如果员工需要退出系统,直接点击右侧的退出按钮即可退出系统,退出系统后页面应跳转回登录页面。

1). 退出页面展示

image-20210727005437531

2).前端页面分析

image-20210727010054851

点击 image-20210727010150207 将会调用一个js方法logout, 在logout的方法中执行如下逻辑:

image-20210727010422199

A. 发起post请求, 调用服务端接口 /employee/logout 执行退出操作 ;

B. 删除客户端 localStorage 中存储的用户登录信息, 跳转至登录页面 ;

5.2 代码实现

需要在Controller中创建对应的处理方法, 接收页面发送的POST请求 /employee/logout ,具体的处理逻辑:

A. 清理Session中的用户id

B. 返回结果

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

5.3 功能测试

1). 代码实现完毕后, 重启服务, 访问登录界面 http://localhost:8080/backend/page/login/login.html ;

2). 登录完成之后, 进入到系统首页 backend/index.html, 点击右上角 image-20210727011020244 按钮 执行退出操作, 完成后看看是否可以跳转到登录页面 , 并检查localStorage。

image-20210727011215186

image-20210727011259297

项目结构

image-20220905094207927

平台核心配置

pom配置

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.iyyxx</groupId>
    <artifactId>reggie_take_out</artifactId>
    <version>1.0-SNAPSHOT</version>
    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>

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

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

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

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

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

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

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

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

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

    </dependencies>

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

application.yml配置

server:
  port: 8080
spring:
  application:
    name: reggie_take_out
  datasource:
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://192.168.20.164:3306/reggie?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
      username: root
      password: 123456
mybatis-plus:
  configuration:
    #在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      id-type: ASSIGN_ID

MVC模板

entity 实体

与表结构一一对应,这里用了lombok简化代码,对公共字段做了填充绑定

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

mapper 数据库操作

mapper操作,默认继承Mybatis PlusBaseMapper,即可有常用增删改查、分页等功能!

package com.iyyxx.reggie.mapper;

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

/**
 * @className: EmployeeMapper
 * @description: TODO 类描述
 * @author: eric 4575252@gmail.com
 * @date: 2022/9/2/0002 12:10:53
 **/
@Mapper
public interface EmployeeMapper extends BaseMapper<Employee> {
}

service 服务调用

service接口,默认继承mybatis plusiservice,并传入实体类,即可统一CRUD接口!

package com.iyyxx.reggie.service;

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

/**
 * @className: EmployeeService
 * @description: TODO 类描述
 * @author: eric 4575252@gmail.com
 * @date: 2022/9/2/0002 12:09:26
 **/
public interface EmployeeService extends IService<Employee> {
}

service实现,默认继承mybatis plusserviceImpl,并传入实体类和mapper,即可统一CRUD接口!

package com.iyyxx.reggie.service.impl;

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

/**
 * @className: EmployeeServiceImpl
 * @description: TODO 类描述
 * @author: eric 4575252@gmail.com
 * @date: 2022/9/2/0002 12:13:34
 **/
@Service
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements EmployeeService {

}

controller web响应控制

  • 控制器首先需要RestController的注解,通过类和方法的mapping实现web访问入口
  • 注入所需的service接口
  • 返回数据类型为R,其中单体返回用实体,状态返回用String,分页返回用Page
  • spring处理http常规的get、post两种mapping,还扩展了put、del,方便区分CRUD操作
package com.iyyxx.reggie.controller;

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

import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;

/**
 * @className: EmployeeController
 * @description: TODO 类描述
 * @author: eric 4575252@gmail.com
 * @date: 2022/9/2/0002 12:06:50
 **/
@Slf4j
@RestController
@RequestMapping("/employee")
public class EmployeeController {
    @Autowired
    private EmployeeService employeeService;

    /***
     * @param: request
     * @param: employee
     * @description: TODO
     * @return: com.iyyxx.reggie.common.R<com.iyyxx.reggie.entity.Employee>
     * @author: eric 4575252@gmail.com
     * @date: 2022-09-2, 周五, 13:41
     */
    @PostMapping("/login")
    public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee){

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

        //2、根据页面提交的用户名username查询数据库
        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、查看员工状态,如果为已禁用状态,则返回员工已禁用结果
        if(emp.getStatus() == 0)
            return R.error("用户已停用");

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

    @PostMapping("/logout")
    public R<String> logout(HttpServletRequest request){
        request.getSession().removeAttribute("employee");
        return R.success("退出成功");
    }



    @PostMapping
    public R<String> save(HttpServletRequest request, @RequestBody Employee employee){

        //初始密码123456
        employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));

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

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

        employeeService.save(employee);
        return R.success("新增用户成功");
    }

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

后端mvc代码

entity

设计结构

  • 实体类,与物理表一比一
  • 类没有特别继承关系,需要实现Serializable接口
  • 公共字段,id,添加用户、添加时间、更新用户、更新时间
  • 数据类型主要有,String、Long、Integer、LocalDateTime
  • 使用@Data注解,简化getset代码,lombok.Data

dto

表单交互的中间对象,如组合套餐等过程对象。

mapper

  • 继承BaseMapper类,并传入对应的entity,即可拥有单表增删改查等所有操作! com.baomidou.mybatisplus.core.mapper.BaseMapper
  • 使用@Mapper注解,org.apache.ibatis.annotations.Mapper

controller

设计结构

  • 一般在com.company.project下添加controller包,统一存放所有控制类
  • 类仅默认集成Object,无其他特殊继承和接口实现
  • 需要RestController注解!org.springframework.web.bind.annotation.RestController
  • 类上面一般存放分类映射注解,采用@RequestMapping(“/xxxx”)
  • 类中方法对CRUD和分页有5种不同的映射注解,分别如下:
    • 新增,常用@PostMapping(“/submit”), 这是符合注解,等同于RequestMapping+method=post
    • 修改,常用@PutMapping
    • 查询单个实体,常用@GetMapping
    • 删除单个实体,常用@DeleteMapping
    • 分页查询,同单个查询,区别在于返回类型为Page,请求参数采用多个,如页码,页行记录数,查询关键字
  • 两个特殊方法参数注解,@RequestBody,@PathVariable,分别用于表单提交映射和restful风格

注:下面这些新注解是在SpringMVC 4.3引入,属于新瓶装旧酒

GetMapping前后端对比

前端代码

this.$axios.get('/test', {
    params: {
        id: 1,
        name: 'aa'
    }
})
//或者
this.$axios.get('/test?id=1&name=aa')

后端代码

@ResponseBody
@GetMapping(value = "/test")
public void test(String id, String name) {
    System.out.println("id:" + id);
    System.out.println("name:" + name);
}

restful风格前端代码

this.$axios.get("/api/index/test/1")

restful风格后端代码

@ResponseBody
@GetMapping(value = "/test/{id}")
public void test(@PathVariable String id) {
    System.out.println("id:" + id);
}

PostMapping前后端对比

前端代码

this.$axios.post('/test', {
    data: {
        id: 1,
        name: 'aa'
    }
})

后端代码, 一定要使用@RequestBody注解,不然接收不到参数

@ResponseBody
@PostMapping(value = "/test")
public void test(@RequestBody Map<String, Object> map) {
    System.out.println(map.toString());
}

PutMapping前后端对比

前端代码

this.$axios.put('/test', {
    data: {
        id: 1,
        name: 'aa'
    }
})

后端代码,同post一定要使用@RequestBody注解

@ResponseBody
@PutMapping(value = "/test")
public void test(@RequestBody Map<String, Object> map) {
    System.out.println(map.toString());
}

DeleteMapping前后端对比

前端代码,类似GetMapping

this.$axios.delete('/test', {
    params: {
        id: 1
    }
})

//或者
this.$axios.delete('/test?id=1')

后端代码

@ResponseBody
@DeleteMapping(value = "/test")
public void test(String id) {
    System.out.println("id:" + id);
}

restful风格@PathVariable注解

this.$axios.delete("/api/index/test/1")

后端代码

@ResponseBody
@DeleteMapping(value = "/test/{id}")
public void test(@PathVariable String id) {
    System.out.println(id);
}

特殊注解

@ReponseBody, 主要用于异步消息返回,而非跳转,这边用到了异常信息回传的异步通讯场景

   @RequestMapping("/login")
  @ResponseBody
  public User login(User user){
    return user;
  }
  User字段:userName pwd
  那么在前台接收到的数据为:'{"userName":"xxx","pwd":"xxx"}'

  效果等同于如下代码:
  @RequestMapping("/login")
  public void login(User user, HttpServletResponse response){
    response.getWriter.write(JSONObject.fromObject(user).toString());
  }

service

接口部分

  • 继承IService接口,传入实体entity

实现部分

  • 继承ServiceImpl类,传入mapper、entity,并实现service接口,低代码!

后端特殊应用

全局异常处理

后端程序经常出现一些数据库操作异常、业务场景自定义异常需传递错误到前端辅助提醒的场景,根据异常类型做分类做统一捕获后传递是不错的处理办法。

这里主要有数据唯一值重复提交、自定义异常两种捕获方式,后续可以自由扩展

实现步骤:

  • 新增异常统一处理类,标注@ControllerAdvice注解,这个是控制器增强开关,绑定注解类型为RestController和RestController注解。
  • 异常处理类,标准@ResponseBody,这样可以对返回到前端的数据进行json格式转换并回传
    • 添加处理方法,参数为对应的异常类,并标注@ExceptionHandler注解传入异常类做绑定
    • 对异常信息进行截取、对比等操作,返回R的错误信息供前端展示。
package com.itheima.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
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 异常处理方法
     * @return
     */
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
        log.error(ex.getMessage());

        if(ex.getMessage().contains("Duplicate entry")){
            String[] split = ex.getMessage().split(" ");
            String msg = split[2] + "已存在";
            return R.error(msg);
        }

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

    /**
     * 异常处理方法,捕获自定义异常
     * @return
     */
    @ExceptionHandler(CustomException.class)
    public R<String> exceptionHandler(CustomException ex){
        log.error(ex.getMessage());

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

}
自定义异常类
package com.itheima.reggie.common;

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

统一数据交互(后端返回)

这是外卖项目一个后端返回的接口数据结构案例,相当经典,特此记录

实现步骤:

  • 新增类R,并接收泛型,这里用了lombok的@Data标签,省去getset方法编写。

  • R对象的数据结构主要包括了这些数据类型

    • 返回代码,code,1成功,0失败
    • 消息书局,msg,存放错误描述,通常为一句话或关键词,仅在有异常、即code为1时写入。
    • 核心数据,data,泛型,通常用于存放一个实体或封装为Page的多行记录。
    • 动态数据,map,作为扩展保留,后期如有需要额外传递数据时采用kv结构获取即可。
  • R对象提供三个主要操作方法

    • 往前端传递成功的消息,静态方法,传递核心数据,默认返回代码为成功,即code=1
    • 往前端传递失败的消息,静态方法,传递消息数据,默认返回代码为失败,即code=0
    • add方法,非静态,暂时未用到,猜想作为后端内部流转的数据格式??或前端的补充?
package com.iyyxx.reggie.common;

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

@Data
public class R<T> {

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

    private String msg; //错误信息

    private T data; //数据

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

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

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

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

}

统一静态资源访问(mvc控制器)

springmvc 默认只能访问resources目录下的static、template两个文件夹,一般前后端程序结构都会有backend、front的静态文件文件夹,这里介绍SpringMVC控制器如何通过扩展增加访问权限。

实现步骤:

  • 新增mvc控制器,继承 WebMvcConfigurationSupport,并标注@Configuration注解

  • mvc控制器中,重写addResourceHandlers方法,调用registry实例,添加uri和classpath的绑定关系。

package com.itheima.reggie.config;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.itheima.reggie.common.JacksonObjectMapper;
import com.itheima.reggie.entity.Employee;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.List;

import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;

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

全局消息转换器(mvc控制器)

基于数字过长被截取、日期格式不易传输等问题,这里通过扩展后端消息转换器来对日期、长数字进行格式转换的案例。

实现步骤:

  • 新增自定义转换器,继承ObjectMapper

  • 自定义转换器的构造方法中,实例化SimpleModule对象,并使用SimpleModule对象添加时间、长数字的双向转换器,最后调用父类的注册模块提交SimpleModule对象。

  • 新增mvc控制器,继承 WebMvcConfigurationSupport,并标注@Configuration注解

  • mvc控制器中,重写extendMessageConverters方法,将自定义的转换器添加至队列顶部。

package com.itheima.reggie.config;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import com.itheima.reggie.common.JacksonObjectMapper;
import com.itheima.reggie.entity.Employee;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.List;

import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;

@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {

    /**
     * 扩展mvc框架的消息转换器
     * @param converters
     */
    @Override
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        log.info("扩展消息转换器...");
        //创建消息转换器对象
        MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
        //设置对象转换器,底层使用Jackson将Java对象转为json
        messageConverter.setObjectMapper(new JacksonObjectMapper());
        //将上面的消息转换器对象追加到mvc框架的转换器集合中
        converters.add(0,messageConverter);
    }
}
双向转化Json、Java实体

后端往前端通讯,将长数字、日期做文本和特定格式的转换

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

全局登录权限检测,基于Spring管理的过滤器

常规情况下,后端平台除了登录、退出、静态资源访问之外都需要严控是否已登录,这里用了传统过滤器,并交由Spring统一管理

实现步骤:

  • 添加一个过滤器,实现avax.servlet.Filter接口的doFilter方法。

  • 过滤器标注@WebFilter注解,填入filterName,urlPatterns,声明为过滤器,绑定请求路径

  • SpringBoot程序入口,增加@ServletComponentScan注解,开启Spring对Servlet的扫描和管理

package com.iyyxx.reggie.filter;

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

/**
 * @className: LoginCheckFilter
 * @description: TODO 类描述
 * @author: eric 4575252@gmail.com
 * @date: 2022/9/2/0002 13:53:44
 **/
@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[] skipUrls = {
                "/employee/login",
                "/employee/logout",
                "/backend/**",
                "/front/**"
        };

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

        //3、如果不需要处理,则直接放行
        log.info("判断URI是否跳过:{}", checkUriSkip);
        if(checkUriSkip){
            filterChain.doFilter(request,response);
            return;
        }

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

        //5、如果未登录则返回未登录结果
        log.info("未登录,退回登录页");
        response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
        return;
    }

    public boolean checkUriSkip(String[] skipURIs, String checkURI){
        for (String skipURI:skipURIs ) {
            if(PATH_MATCHER.match(skipURI, checkURI))
                return true;
        }
        return false;
    }
}

注意:这里用到AntPathMatcher 路径比较匹配器,支持通配符匹配,效率极高!

统一分页处理

分页的处理依赖前端axios的请求、数据处理elementui对数据的渲染后端主要依赖MybatisPlus的技术架构,这里主要介绍MP如何配置分页插件并被Spring管理及Controller如何使用

实现步骤:

  • 新增分页插件
    • 创建一个类,任意取名,比如MybatisPlusConfig,添加@Configuration注解
    • 类中添加一个方法,任意取名,比如,返回MybatisPlusInterceptor对象,添加@Bean注解
  • RestController中添加一个方法,
    • 一般常用@GetMapping,绑定/page请求,接收页码、行记录数、搜索关键字等信息
    • 创建分页构造器,新建Page对象
    • 创建条件构造器,新建LambdaQueryWrapper,用泛型,并添加搜索条件。
    • 条件构造器添加排序约束,可判断空,用方法引入的形式传入get方法及对应的参数。
    • 通过service的page方法查询,即传入分页构造器、条件构造器后Page对象被赋值。
    • 返回Page对象,本项目用R对象做了封装(补充)。
package com.iyyxx.reggie.controller;

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

import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;

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

    @Autowired
    private EmployeeService employeeService;


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

        //构造分页构造器
        Page 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);
    }

}
分页插件,mp3.4.2新语法
package com.itheima.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;
    }
}

统一公共字段处理

对于所有表都共有的字段,这里对新增和修改的操作做了统一填充,实体类也需要做双向绑定!

公共字段的处理是MybatisPlus提供的技术,这里主要介绍自定义元数据对象处理对新增、修改的统一处理,及实体类如何设置策略,形成双向绑定

实现步骤:

  • 创建一个类,实现MetaObjectHandler接口的insertFill和updateFill方法,并标注@Component注解,由spring管理
  • entity类中,对所需的字段设置@TabFiled的对应策略为insert、update、insert_update、default
package com.itheima.reggie.common;

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

/**
 * 自定义元数据对象处理器
 */
@Component
@Slf4j
public class MyMetaObjecthandler implements MetaObjectHandler {
    /**
     * 插入操作,自动填充
     * @param metaObject
     */
    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("公共字段自动填充[insert]...");
        log.info(metaObject.toString());

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

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

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

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




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

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

}
基于ThreadLocal做统一会话存取

客户端一次http请求,后端会有一个线程进行统一处理,哪怕是经过了多个不同的处理类,打印Thread.currentThread().getId() 都是一样的,jdk的lang包下提供ThreadLocal作为线程的处理类,并提供get、set方法。

在web会话处理中,有些场景如拦截器不易获取session对象,这时如果线程前置处理类预存数据,就可以方便的供后置类取得。

这里通过一个BaseContext类提供的静态存取接口事先跨线程的数据隔离和存取,因ThreadLocal本身并不存取数据,而是依赖ThreadLocalMap对每个线程单独提供,完成了隔离!

实现方法:

  • 创建一个BaseContext类
    • 提供静态私有化的threadLocal对象
    • 提供静态存取接口
package com.itheima.reggie.common;

/**
 * 基于ThreadLocal封装工具类,用户保存和获取当前登录用户id
 */
public class BaseContext {
    private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

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

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

==注意!==ThreadLocal是一个线程本地变量,每次取数要记得清空,不然容易出现内存泄漏!虽然threadlocal的threadlocalmap下的entity采用弱引用,即继承了WeakReference,当内存不足触发gc会被立刻回收,保险起见,显式的清空更安全!

前端axios代码

评论系统未开启,无法评论!