1)该博客内容为B站UP主的免费课程,想直接学习该视频的可以点击:传送门。这位老师讲解得很细致。
2)该课程的文档和源码的下载链接为:传送门。
3)本文博客的markdown资源的下载链接为:传送门。

目录:

  • 1 系统概述与环境搭建
    • 1.1 系统开发及运行环境
    • 1.2 项目分析
    • 1.3 搭建项目
      • 1.3.1 项目结构
      • 1.3.2 创建Spring Initializer 项目
      • 1.3.3 创建数据库
    • 1.4 测试连接
    • 1.5 问题与解决
  • 2 用户注册
    • 2.1 用户-创建数据表
    • 2.2 用户-创建实体类
    • 2.3 用户-注册-持久层
      • 2.3.1 规划需要执行的SQL语句
      • 2.3.2 设计接口和抽象方法
      • 2.3.3 配置SQL映射
      • 2.3.4 编写单元测试
    • 2.4 用户-注册-业务层
      • 2.4.1 业务的定位
      • 2.4.2 规划异常
      • 2.4.3 接口与抽象方法
      • 2.4.4 实现抽象方法
      • 2.4.5 编写单元测试
      • 2.4.6 密码加密介绍
    • 2.5 用户-注册-控制层
      • 2.5.1 创建响应结果类
      • 2.5.2 设计请求
      • 2.5.3 处理请求
      • 2.5.4 控制器层的调整
        • 1) @ExceptionHandler 注解
        • 2) 将控制层的异常处理代码解耦
    • 2.6 用户-注册-前端页面
      • 2.6.1 AJAX 介绍
      • 2.6.2 前端页面
  • 3 用户登录
    • 3.1 用户-登录-持久层
      • 3.1.1 规划需要执行的SQL语句
      • 3.1.2 接口与抽象方法
      • 3.1.3 配置SQL映射
    • 3.2 用户-登录-业务层
      • 3.2.1 规划异常
      • 3.2.2 接口与抽象方法
      • 3.2.3 实现抽象方法
    • 3.3 用户-登录-控制器
      • 3.3.1 处理异常
      • 3.3.2 设计请求
      • 3.3.3 处理请求
    • 3.4 用户-登录-前端页面
    • 3.5 用户-登录- Session会话
      • 3.5.1 当前登录方式可能产生的问题
      • 3.5.2 何为 Session
      • 3.5.3 使用
    • 3.6 用户-登录- 拦截器
      • 3.6.1 当前登录方式可能产生的问题
      • 3.6.2 何为拦截器
      • 3.6.3 使用
  • 4 修改密码
    • 4.1 用户-修改密码-持久层
      • 4.1.1 规划需要执行的SQL语句
      • 4.1.2 接口与抽象方法
      • 4.1.3 配置SQL映射
      • 4.1.4 单元测试
    • 4.2 用户-修改密码-业务层
      • 4.2.1 规划异常
      • 4.2.2 接口与抽象方法
      • 4.2.3 实现抽象方法
      • 4.2.4 单元测试
    • 4.3 用户-修改密码-控制层
      • 4.3.1 处理异常
      • 4.3.2 设计请求
      • 4.3.3 处理请求
    • 4.4 用户-修改密码-前端页面
  • 5 个人资料修改
    • 5.1 用户-修改密码-持久层
      • 5.1.1 规划需要执行的SQL语句
      • 5.1.2 接口与抽象方法
      • 5.1.3 配置SQL映射
      • 5.1.4 单元测试
    • 5.2 用户-修改密码-业务层
      • 5.2.1 规划异常
      • 5.2.2 接口与抽象方法
      • 5.2.3 实现抽象方法
      • 5.2.4 单元测试
    • 5.3 用户-个人资料-控制器
      • 5.3.1 处理异常
      • 5.3.2 设计请求
      • 5.3.3 处理请求
    • 5.4 用户-个人资料-前端页面
  • 6 用户头像上传
    • 6.1 用户-头像上传-持久层
      • 6.1.1 规划需要执行的SQL语句
      • 6.1.2 接口与抽象方法
      • 6.1.3 配置SQL映射
      • 6.1.4 单元测试
    • 6.2 用户-头像上传-业务层
      • 6.2.1 规划异常
      • 6.2.2 接口与抽象方法
      • 6.2.3 实现抽象方法
      • 6.2.4 单元测试
    • 6.3 用户-头像上传-控制层
      • 6.3.1 处理异常
      • 6.3.2 设计请求
      • 6.3.3 处理请求
    • 6.4 用户-上传头像-前端页面
    • 6.5 用户-上传头像-设置上传文件大小
    • 6.6 用户-上传头像-前端页面BUG解决
      • 6.6.1 上传后显示头像
      • 6.6.2 登录后显示头像
      • 6.6.3 显示最新头像

1 系统概述与环境搭建

1.1 系统开发及运行环境

电脑商城系统开发所需的环境及相关软件进行介绍。

1.操作系统:Windows 10

2.Java开发包:JDK 8

3.项目管理工具:Maven 3.6.3

4.项目开发工具:IntelliJ IDEA 2020.3.2 x64

5.数据库:MariaDB-10.3.7-winx64

6.浏览器:Google Chrome

7.服务器架构:Spring Boot 2.4.7 + MyBatis 2.1.4 + AJAX

1.2 项目分析

  1. 在开发某个项目之前,应先分析这个项目中可能涉及的数据。本项目中涉及的数据:用户、商品、商品类别、收藏、订单、购物车、收货地址。
  2. 然后确定这些数据的开发顺序。设计开发顺序的原则是:先开发基础、简单或熟悉的数据。以上需要处理的数据的开发流程是:用户-收货地址-商品类别-商品-收藏-购物车-订单。
  3. 然后分析该数据涉及的功能。以用户数据为例,需要开发的功能有:登录、注册、修改密码、修改资料、上传头像。
  4. 然后确定这些功能的开发顺序。原则上,应先做基础功能,并遵循增查删改的顺序来开发。则用户相关功能的开发顺序应该是:注册-登录-修改密码-修改个人资料-上传头像。
  5. 在开发某个具体的功能时,应遵循开发顺序:持久层 - 业务层 - 控制器 - 前端页面。
    • 持久层开发:依据前端页面的设置规划相关的 SQL 语句,以及进行配置;
    • 业务层开发:核心功能控制、业务操作以及异常的处理;
    • 控制层开发:接受请求、处理响应;
  6. 在实际开发中,应先创建该项目的数据库,当每次处理一种新的数据时,应先创建该数据在数据库中的数据表,然后在项目中创建该数据表对应的实体类。

1.3 搭建项目

1.3.1 项目结构

名称 路径
项目名称 com.sharm.store
资源文件 resources 文件夹下(static、templates)
单元测试 test.com.sharm.store

1.3.2 创建Spring Initializer 项目

本质上 Spring Initializer 是一个 Web 应用程序,它提供了一个基本的项目结构,能够帮助开发者快速构建一个基础的 Spring Boot 项目。在创建 Spring Initializer 类型的项目时需在计算机连网的状态下进行创建。

1. 首先确保计算机上安装了 JDK、IDEA、MySQL 等开发需要使用的软件,并在IDEA中配置了Maven 3.6.3项目管理工具。

2.在IDEA欢迎界面,点击【New Project】按钮创建项目,左侧选择【Spring Initializr】选项进行Spring Boot项目快速构建。

3.给项目添加Spring Web、MyBatis Framework、MySQL Driver的依赖。点击【Next】按钮完成项目创建。

4.首次创建完Spring Initializr项目时,解析项目依赖需消耗一定时间(Resolving dependencies of store…)。

1.3.3 创建数据库

使用 navicat 创建数据库。第一种方法为图形界面法:

或者使用 状态栏 - 工具 - 命令行界面 打开 navicat 的命令行。

CREATE DATABASE store character SET utf8;

1.4 测试连接

1 启动 SpringBoot 主类,是否有对应的 Spring 图形输出

找到项目的入口类(被 @SpringBootApplication 注解修饰),然后运行启动类;启动过程如果控制台输出Spring图形则表示启动成功。

2 在单元测试类中测试数据库的连接是否可以正常地加载

@SpringBootTest
class StoreApplicationTests {// 自动装配@Autowiredprivate DataSource dataSource;/*** 数据库连接池:Hikari 是 Spring 目前默认的数据库连接池。* @throws SQLException*/@Testvoid getConnection() throws SQLException {System.out.println(dataSource.getConnection());}
}

如果控制面板出现HikariProxyConnection@2049602706 wrapping com.mysql.cj.jdbc.ConnectionImpl@6326c5ec,则数据库连接成功。

3 测试项目的静态资源是否可以正常的加载

把这些静态资源复制到项目目录结构的 static 目录下,同时在访问时,static 就表示根目录,不需要在 url上显示的写出来。

由于 IDEA 对于 JS 代码的兼容性较差,所以可能存在编写了 JS 代码但无法正常加载的情况,解决方法有:

  1. IDEA 的缓存清理;
  2. Maven 右侧工具栏 Lifecycle 的 clear 清理,然后 install 安装;
  3. IDEA 工具栏的 rebuild 重构;
  4. 重启 IDEA 或者操作系统。

1.5 问题与解决

1 启动项目时提示:“配置数据源失败:'url’属性未指定,无法配置内嵌的数据源”,且有如下的错误提示。

Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.

解决以上操作提示的方法:在 resources 文件夹下的 application.properties 文件中添加数据源的配置。

spring.datasource.url=jdbc:mysql://localhost:3306/store?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=123456

2 用户注册

2.1 用户-创建数据表

首先规划 SQL 语句,然后确定 Java 代码的编写;

1.使用use命令先选中store数据库。

USE store;

2.在store数据库中创建t_user用户数据表。

CREATE TABLE t_user (uid INT AUTO_INCREMENT COMMENT '用户id',username VARCHAR(20) NOT NULL UNIQUE COMMENT '用户名',password CHAR(32) NOT NULL COMMENT '密码',salt CHAR(36) COMMENT '盐值',phone VARCHAR(20) COMMENT '电话号码',email VARCHAR(30) COMMENT '电子邮箱',gender INT COMMENT '性别:0-女,1-男',avatar VARCHAR(50) COMMENT '头像',is_delete INT COMMENT '是否删除:0-未删除,1-已删除',created_user VARCHAR(20) COMMENT '日志-创建人',created_time DATETIME COMMENT '日志-创建时间',modified_user VARCHAR(20) COMMENT '日志-最后修改执行人',modified_time DATETIME COMMENT '日志-最后修改时间',PRIMARY KEY (uid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

2.2 用户-创建实体类

数据库中的表要有与之相对应的实体类,这样才可以将表中的数据拿出来放到实体类中。

1.项目中许多实体类都会有日志相关的四个属性,所以在创建实体类之前,应先创建这些实体类的基类,将4个日志属性声明在基类中。在com.sharm.store.entity包下创建BaseEntity类,作为实体类的基类。

package com.sharm.store.entity;
import java.io.Serializable;
import java.util.Date;
import java.util.Objects;/*** 1. 只要是实体类就需要满足一定的约束,比如实现 Serializable 的接口* 2. 传统的 SSM 需要在实体类上加上 @Component 修饰,而 SpringBoot 发现这都是多余的,SpringBoot 的约定大于配置,所以无需写上该注解;*/
public class BaseEntity implements Serializable {/*** 在敲对应的属性时,可以先将 sql 的字段复制过去,然后删除后面的修饰,加上前面的修饰符。* 能 CV 就别手敲。*/private String createdUser;private Date createdTime;private String modifiedUser;private Date modifiedTime;// Generate: Getter and Setter、toString()
}

2.创建com.sharm.store.entity.User用户数据的实体类,继承自BaseEntity类,在类中声明与数据表中对应的属性。

package com.sharm.store.entity;import java.io.Serializable;
import java.util.Date;
import java.util.Objects;public class User extends BaseEntity implements Serializable {private Integer uid;private String username;private String password;private String salt;private String phone;private String email;private Integer gender;private String avatar;private Integer isDelete;//Generate: Getter and Setter、Generate hashCode() and equals()、toString()
}

2.3 用户-注册-持久层

2.3.1 规划需要执行的SQL语句

1.用户注册的本质是向用户表中插入数据,需要执行的SQL语句大致是:

INSERT INTO t_user (除了uid以外的字段列表) VALUES (匹配的值列表)

2.由于数据表中用户名字段被设计为UNIQUE,在执行插入数据之前,还应该检查该用户名是否已经被注册,因此需要有“根据用户名查询用户数据”的功能。需要执行的SQL语句大致是:

SELECT * FROM t_user WHERE username=?

2.3.2 设计接口和抽象方法

1.创建com.sharm.store.mapper.UserMapper接口,并在接口中添加抽象方法。

package com.sharm.store.mapper;
import com.sharm.store.entity.User;/** 处理用户数据操作的持久层接口 */
public interface UserMapper {/*** 插入用户数据:本来我们只需要插入几个字段,但多个字段毕竟麻烦,所以这里直接把 User 类型输入* @param user 用户数据* @return 新增数据的 ID*/Integer insert(User user);/*** 根据用户名查询用户数据* @param username 用户名* @return 匹配的用户数据,如果没有匹配的数据,则返回null*/User findByUsername(String username);
}

2.由于这是项目中第一次创建持久层接口,所以需要我们告诉 SpringBoot 我们的 Mapper 接口的位置;

MyBatis与Spring整合后需要实现实体和数据表的映射关系。实现实体和数据表的映射关系可以在Mapper接口上添加@Mapper注解。但建议以后直接在SpringBoot启动类中加@MapperScan(“mapper包”) 注解,这样会比较方便,不需要对每个Mapper都添加@Mapper注解。

@SpringBootApplication
// MapperScan注解指定当前项目中的Mapper接口路径的位置,在项目启动时会自动加载所有的接口
@MapperScan("com.sharm.store.mapper")
public class StoreApplication {public static void main(String[] args) {SpringApplication.run(StoreApplication.class, args);}
}

2.3.3 配置SQL映射

1.在src/main/resources下创建mapper文件夹,并在该文件夹下创建UserMapper.xml映射文件,进行以上两个抽象方法的映射配置。

1)Mapper 的配置文件的框架在 Mybatis 官网就可以拿到;

2)这样写最大的好处就是将 SQL 语句和 Java 代码进行了分离,实现了解耦。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- namespace 属性:用于指定当前的映射文件和哪一个接口进行映射,需要表注指定接口的完整路径-->
<mapper namespace="com.sharm.store.mapper.UserMapper"><resultMap id="UserEntityMap" type="com.sharm.store.entity.User"><!-- 只需要将表中的字段和类的属性不相同的字段进行指定匹配,名称一致的字段可以省略不写 --><!-- 在定义映射规则时,主键不可以省略 --><id column="uid" property="uid"/><result column="is_delete" property="isDelete"/><result column="created_user" property="createdUser"/><result column="created_time" property="createdTime"/><result column="modified_user" property="modifiedUser"/><result column="modified_time" property="modifiedTime"/></resultMap><!--  id 属性:表示映射的接口中的方法,通过 namespace 和 id 确定一个方法useGeneratedKeys 属性:表示开启某个字段的值递增keyProperty 属性:确认该递增的字段--><!-- 双击剪切,然后井号、大括号。最后部分不能有逗号,不能有分号。同时 value 中要修改为驼峰命名法;--><insert id="insert" useGeneratedKeys="true" keyProperty="uid">INSERT INTOt_user (username, password, salt, phone, email, gender, avatar, is_delete, created_user, created_time, modified_user, modified_time)VALUES(#{username}, #{password}, #{salt}, #{phone}, #{email}, #{gender}, #{avatar}, #{isDelete}, #{createdUser}, #{createdTime}, #{modifiedUser}, #{modifiedTime})</insert><!-- select 在查询的时候,结果可以是一个对象,也可以是多个对象--><!-- 其中查询结果的字段如果和实体类的字段都相同,比如 phone = phone,则可以直接使用 resultType --><!-- 若查询结果的字段和实体类的字段不都相同,比如 created_time != createdTime,则使用 resultMap --><select id="findByUsername" resultMap="UserEntityMap">SELECT*FROMt_userWHEREusername = #{username}</select>
</mapper>

2.由于这是项目中第一次使用SQL映射,所以需要在application.properties中添加mybatis.mapper-locations属性的配置,以指定XML文件的位置。

mybatis.mapper-locations=classpath:mapper/*.xml

2.3.4 编写单元测试

完成后及时执行单元测试,检查以上开发的功能是否可正确运行。在src/test/java下创建com.sharm.store.mapper.UserMapperTests单元测试类。

package com.sharm.store.mapper;
import com.sharm.store.entity.User;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;// 测试时必须导入这两个注解
// @SpringBootTest 表示标注当前的类是一个测试类,不会随项目一块打包
@SpringBootTest
// @RunWith 表示可以启动这个单元测试类。其中需要传递 SpringRunner.class 这个参数
@RunWith(SpringRunner.class)
public class UserMapperTests {// 在测试类中声明持久层对象,通过自动装配来注入值@Autowiredprivate UserMapper userMapper;/*** 单元测试方法:可以单独运行,不需要启动整个项目,提高了代码的测试效率* 条件:* 1. 该方法必须被 @Test 注解修饰* 2. 返回值必须时 void* 3. 方法的参数列表不指定任何类型* 4. 方法的访问修饰符必须是 public*/@Testpublic void insert() {User user = new User();user.setUsername("user01");user.setPassword("123456");Integer rows = userMapper.insert(user);System.out.println("rows=" + rows);}@Testpublic void findByUsername() {String username = "user01";User result = userMapper.findByUsername(username);System.out.println(result);}
}

1 如果在自动装配 userMapper 对象时出现 “Could not autowire. No beans of ‘UserMapper’ type found”错,无法进行自动装配。解决方案是,将Autowiring for bean class选项下的Severity设置为Warning即可。

2.4 用户-注册-业务层

2.4.1 业务的定位

1.业务:一套完整的数据处理过程,通常表现为用户认为的一个功能,但是在开发时对应多项数据操作。在项目中,通过业务控制每个“功能”(例如注册、登录等)的处理流程和相关逻辑。

2.流程:先做什么,再做什么。例如:注册时,需要先判断用户名是否被占用,再决定是否完成注册。

3.逻辑:能干什么,不能干什么。例如:注册时,如果用户名被占用,则不允许注册;反之,则允许注册。

4.业务的主要作用是保障数据安全和数据的完整性、有效性。

2.4.2 规划异常

运用异常的方式来作为解决方案,这是自己之前没想到的。

1.关于异常

异常的处理方式和处理原则:捕获处理(try…catch…finally),声明抛出(throw/throws)。如果当前方法适合处理,则捕获处理;如果当前方法不适合处理,则声明抛出。

2.异常规划

1.为了便于统一管理自定义异常,应先创建com.sharm.store.service.ex.ServiceException自定义异常的基类异常,继承自RuntimeException类,并从父类生成子类的五个构造方法。

package com.sharm.store.service.ex;/** 业务异常的基类 */
public class ServiceException extends RuntimeException {// Override Methods...
}

2.当用户进行注册时,可能会因为用户名被占用而导致无法正常注册,此时需要抛出用户名被占用的异常,因此可以设计一个用户名重复的com.sharm.store.service.ex.UsernameDuplicateException异常类,继承自ServiceException类,并从父类生成子类的五个构造方法。

package com.sharm.store.service.ex;/** 用户名重复的异常 */
public class UsernameDuplicateException extends ServiceException {// Override Methods...
}

3.在用户进行注册时,会执行数据库的INSERT操作,该操作也是有可能失败的。则创建com.sharm.store.service.ex.InsertException异常类,继承自ServiceException类,并从父类生成子类的五个构造方法。

package com.sharm.store.service.ex;/** 插入数据的异常 */
public class InsertException extends ServiceException {// Override Methods...
}

4.所有的自定义异常,都应是RuntimeException的子孙类异常。项目中目前异常的继承结构是见下。

RuntimeException-- ServiceException-- UsernameDuplicateException-- InsertException

2.4.3 接口与抽象方法

1.先创建com.sharm.store.service.IUserService业务层接口,并在接口中添加抽象方法。

package com.sharm.store.service;
import com.sharm.store.entity.User;/** 处理用户数据的业务层接口 */
// 接口的命名规则为:I + 数据名 + 层名
public interface IUserService {/*** 用户注册* @param user 用户数据*/void reg(User user);
}

2.创建业务层接口目的是为了解耦。关于业务层的抽象方法的设计原则。

1.仅以操作成功为前提来设计返回值类型,不考虑操作失败的情况;
2.方法中使用抛出异常的方式来表示操作失败;
2.方法名称可以自定义,通常与用户操作的功能相关;
3.方法的参数列表根据执行的具体业务功能来确定,需要哪些数据就设计哪些数据。通常情况下,参数需要足以调用持久层对应的相关功能,同时还要满足参数是客户端可以传递给控制器的.

2.4.4 实现抽象方法

1.UserServiceImpl类需要重写IUserService接口中的抽象方法,实现步骤大致是:

// 根据参数user对象获取注册的用户名
// 调用持久层的User findByUsername(String username)方法,根据用户名查询用户数据
// 判断查询结果是否不为null
// 是:表示用户名已被占用,则抛出UsernameDuplicateException异常// 创建当前时间对象
// 补全数据:加密后的密码
// 补全数据:盐值
// 补全数据:isDelete(0)
// 补全数据:4项日志属性// 表示用户名没有被占用,则允许注册
// 调用持久层Integer insert(User user)方法,执行注册并获取返回值(受影响的行数)
// 判断受影响的行数是否不为1
// 是:插入数据时出现某种错误,则抛出InsertException异常

2.实现抽象方法

package com.sharm.store.service.impl;import com.sharm.store.entity.User;
import com.sharm.store.mapper.UserMapper;
import com.sharm.store.service.IUserService;
import com.sharm.store.service.ex.InsertException;
import com.sharm.store.service.ex.UsernameDuplicateException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;import java.util.Date;
import java.util.UUID;// 通过使用 @Service 注解,将当前类的对象交给 Spring 来管理,自动进行对象的创建以及维护
@Service
/** 处理用户数据的业务层实现类 */
public class UserServiceImpl implements IUserService {@Autowiredprivate UserMapper userMapper;@Overridepublic void reg(User user) {// 根据参数user对象获取注册的用户名String username = user.getUsername();// 调用持久层的User findByUsername(String username)方法,根据用户名查询用户数据User result = userMapper.findByUsername(username);// 判断查询结果是否不为nullif (result != null) {// 是:表示用户名已被占用,则抛出UsernameDuplicateException异常throw new UsernameDuplicateException("尝试注册的用户名[" + username + "]已经被占用");}// 创建当前时间对象Date now = new Date();// 补全数据:加密后的密码String salt = UUID.randomUUID().toString().toUpperCase();// (盐值 + 密码 + 盐值 ) - md5 算法进行加密,连续加载三次String md5Password = getMd5Password(user.getPassword(), salt);user.setPassword(md5Password);// 补全数据:盐值user.setSalt(salt);// 补全数据:isDelete(0)user.setIsDelete(0);// 补全数据:4项日志属性// 在注册的时候,创建时间和修改时间肯定是一样的user.setCreatedUser(username);user.setCreatedTime(now);user.setModifiedUser(username);user.setModifiedTime(now);// 表示用户名没有被占用,则允许注册// 调用持久层Integer insert(User user)方法,执行注册并获取返回值(受影响的行数)Integer rows = userMapper.insert(user);// 判断受影响的行数是否不为1if (rows != 1) {// 是:插入数据时出现某种错误,则抛出InsertException异常throw new InsertException("添加用户数据出现未知错误,请联系系统管理员");}}/*** 执行密码加密* @param password 原始密码* @param salt 盐值* @return 加密后的密文*/private String getMd5Password(String password, String salt) {/** 加密规则:* 1、无视原始密码的强度* 2、使用UUID作为盐值,在原始密码的左右两侧拼接* 3、循环加密3次*/for (int i = 0; i < 3; i++) {password = DigestUtils.md5DigestAsHex((salt + password + salt).getBytes()).toUpperCase();}return password;}
}

3.业务层的代码目录

2.4.5 编写单元测试

package com.sharm.store.service;import com.sharm.store.entity.User;
import com.sharm.store.service.ex.ServiceException;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;// 测试时必须导入这两个注解
// @SpringBootTest 表示标注当前的类是一个测试类,不会随项目一块打包
@SpringBootTest
// @RunWith 表示可以启动这个单元测试类。其中需要传递 SpringRunner.class 这个参数
@RunWith(SpringRunner.class)
public class UserServiceTests {// 在测试类中声明业务层对象,通过自动装配来注入值// 所以用的是接口?@Autowiredprivate IUserService iUserService;@Testpublic void reg() {try {User user1 = new User();user1.setUsername("sharm");user1.setPassword("123456");iUserService.reg(user1);} catch (ServiceException e) {System.out.println("注册失败!" + e.getClass().getSimpleName());System.out.println(e.getMessage());}}
}

2.4.6 密码加密介绍

密码加密可以有效的防止数据泄密后带来的账号安全问题。通常,程序员不需要考虑加密过程中使用的算法,因为已经存在非常多成熟的加密算法可以直接使用。但是所有的加密算法都不适用于对密码进行加密,因为加密算法都是可以进行逆向运算的。即:如果能够获取加密过程中所有的参数,就可以根据密文得到原文。

对密码进行加密时,需使用消息摘要算法。消息摘要算法的特点是:

1.原文相同时,使用相同的摘要算法得到的摘要数据一定相同;
2.使用相同的摘要算法进行运算,无论原文的长度是多少,得到的摘要数据长度是固定的;
3.如果摘要数据相同,则原文几乎相同,但也可能不同,可能性极低。

不同的原文,在一定的概率上能够得到相同的摘要数据,发生这种现象时称为碰撞。

以MD5算法为例,运算得到的结果是128位的二进制数。在密码的应用领域中,通常会限制密码长度的最小值和最大值,可是密码的种类是有限的,发生碰撞在概率上可以认为是不存在的。

常见的摘要算法有SHA(Secure Hash Argorithm)家族和MD(Message Digest)系列的算法。

关于MD5算法的破解主要来自两方面。一个是王小云教授的破解,学术上的破解其实是研究消息摘要算法的碰撞,也就是更快的找到两个不同的原文却对应相同的摘要,并不是假想中的“根据密文逆向运算得到原文”。另一个是所谓的“在线破解”,是使用数据库记录大量的原文与摘要的对应关系,当尝试“破解”时本质上是查询这个数据库,根据摘要查询原文。

为进一步保障密码安全,需满足以下加密规则:

1.要求用户使用安全强度更高的原始密码;
2.加盐;
3.多重加密;
4.综合以上所有应用方式。

2.5 用户-注册-控制层

2.5.1 创建响应结果类

创建com.sharm.store.util.JsonResult响应结果类:

package com.sharm.store.util;import java.io.Serializable;/*** 响应结果类* 又学到一点,那就是构造方法可以这么用* @param <E> 响应数据的类型*/
public class JsonResult<E> implements Serializable {/** 状态码 */private Integer state;/** 状态描述信息 */private String message;/** 响应的数据,因为不确定,所以需要使用泛型 */private E data;public JsonResult() {}public JsonResult(Integer state) {this.state = state;}/***  将异常的信息传递给 message,妙!* @param e 捕获的异常*/public JsonResult(Throwable e) {this.message = e.getMessage();}public JsonResult(Integer state, E data) {this.state = state;this.data = data;}public Integer getState() {return state;}public void setState(Integer state) {this.state = state;}public String getMessage() {return message;}public void setMessage(String message) {this.message = message;}public E getData() {return data;}public void setData(E data) {this.data = data;}
}

2.5.2 设计请求

设计用户提交的请求,并设计响应的方式:

请求路径:/users/reg
请求参数:User user
请求类型:POST
响应结果:JsonResult<Void>

2.5.3 处理请求

1.创建com.sharm.store.controller.UserController控制器类,在类的声明之前添加@RestController和@RequestMapping(“users”)注解,在类中添加IUserService业务对象并使用@Autowired注解修饰。

package com.sharm.store.controller;import com.sharm.store.entity.User;
import com.sharm.store.service.IUserService;
import com.sharm.store.service.ex.InsertException;
import com.sharm.store.service.ex.UsernameDuplicateException;
import com.sharm.store.util.JsonResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;// @RestController = @Controller + @ResponseBody : 便可以直接使用 JSON 返回
/** 处理用户相关请求的控制器类 */
@RestController
@RequestMapping("users")
public class UserController {@Autowiredprivate IUserService userService;
}

2.然后在类中添加处理请求的用户注册方法。

@RequestMapping("reg")
public JsonResult<Void> reg(User user) {// 创建返回值JsonResult<Void> result = new JsonResult<Void>();try {// 调用业务对象执行注册userService.reg(user);// 响应成功result.setState(200);} catch (UsernameDuplicateException e) {// 用户名被占用result.setState(4000);result.setMessage("用户名已经被占用");} catch (InsertException e) {// 插入数据异常result.setState(5000);result.setMessage("注册失败,请联系系统管理员");}// 所以它这边直接返回的是一个 Java 的对象。没有经过 JSON 的任何转变return result;
}

3.完成后启动项目,打开浏览器访问http://localhost:8080/users/reg?username=sharm&password=123456请求进行测试。

{state: 200,message: null,data: null
}

2.5.4 控制器层的调整

1) @ExceptionHandler 注解

**使用方式:**Spring的@ExceptionHandler用来统一处理方法抛出的异常,当对某一个方法加上@ExceptionHandler注解后,这个方法就会处理被@RequestMapping注解修饰的其它方法的异常;

注解的参数:@ExceptionHandler注解中可以添加参数,参数是某个异常类的class,代表这个方法只处理该异常类的异常。

注解方法的返回值:标识了@ExceptionHandler注解的方法,返回值类型和标识了@RequestMapping的方法是统一的。有些情况下我们会给标识了@RequestMapping的方法添加@ResponseBody,比如使用Ajax的场景,直接返回字符串,异常处理类也可以如此操作,添加@ResponseBody注解后,可以直接返回字符串。

参考文献:传送门

2) 将控制层的异常处理代码解耦

1.然后创建提供控制器类的基类com.sharm.store.controller.BaseController,在其中定义表示响应成功的状态码及统一处理异常的方法。

package com.sharm.store.controller;import com.sharm.store.service.ex.InsertException;
import com.sharm.store.service.ex.ServiceException;
import com.sharm.store.service.ex.UsernameDuplicateException;
import com.sharm.store.util.JsonResult;
import org.springframework.web.bind.annotation.ExceptionHandler;/** 控制器类的基类 */
public class BaseController {/** 操作成功的状态码 */public static final int OK = 200;/** @ExceptionHandler用于统一处理方法抛出的异常 */@ExceptionHandler(ServiceException.class)public JsonResult<Void> handleException(Throwable e){JsonResult<Void> result = new JsonResult<>(e);if (e instanceof UsernameDuplicateException) {result.setState(4000);} else if (e instanceof InsertException) {result.setState(5000);}return result;}
}

2.最后简化UserController控制器类中的用户注册reg()方法的代码。

package com.sharm.store.controller;import com.sharm.store.entity.User;
import com.sharm.store.service.IUserService;
import com.sharm.store.util.JsonResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("user")
public class UserController extends BaseController{@Autowiredprivate IUserService userService;@RequestMapping("reg")public JsonResult<Void> reg(User user) {userService.reg(user);return new JsonResult<Void>(OK);}
}

3.完成后启动项目,打开浏览器访问http://localhost:8080/users/reg?username=sharm&password=123456请求进行测试。

2.6 用户-注册-前端页面

2.6.1 AJAX 介绍

关于 AJAX 的详细介绍的博客,传送门

这边通过代码介绍一下 AJAX 在这个项目中的使用。

2.6.2 前端页面

1.将电脑商城前端资源页面pages文件夹下的静态资源:bootstrap3、css、images、js、web、index.html相关的资源复制到项目src/main/resources/static文件夹下。如图所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LHQrvZLr-1657522141282)(C:\Users\Sharm\Desktop\电脑商场项目的总结\2 用户注册登录.assets\2.png)]

2.在register.html页面中body标签内部的最后,添加script标签用于编写JavaScript程序。请求的url中需要添加项目的访问名称。

serialize() 方法通过序列化表单值,创建 URL 编码文本字符串。详细内容可以参考这篇博客:传送门

<script type="text/javascript">// 利用 JQuery 选择到 btn-reg 这个按钮,并且对其加上点击事件$("#btn-reg").click(function() {console.log($("#form-reg").serialize());// ajax 的固定写法$.ajax({// 目标地址url: "/users/reg",type: "POST",// 通过 $("#form-reg").serialize() 得到 username=sharm&password=123data: $("#form-reg").serialize(),dataType: "json",// 因为 JS 本身就是弱数据类型的语言,所以function 中变量的类型不定义也没事success: function(json) {if (json.state == 200) {alert("注册成功!");// location.href = "login.html";} else {alert("注册失败!" + json.message);}},// 对于 success、error 以及 success 中的判断,一个是外部是否成功,另一个则是内部的判断;error:function(xhr){alert("注册时产生未知的错误!"+xhr.status)}});});</script>

3.完成后启动项目,打开浏览器访问http://localhost:8080/web/register.html页面并进行注册。

注意:由于没有验证数据,即使没有填写用户名或密码,也可以注册成功。


3 用户登录

3.1 用户-登录-持久层

3.1.1 规划需要执行的SQL语句

用户登录功能需要执行的SQL语句是根据用户名查询用户数据,再在业务层判断密码是否正确。SQL语句大致是:

SELECT * FROM t_user WHERE username=?

说明:以上SQL语句对应的后台开发已经完成,无需再次开发。

说明:之所以不推荐SELECT * FROM t_user WHERE username=? AND password=?,一方面是因为如果使用这条SQL语句,无法判断登录失败的原因是该用户不存在,还是用户名和密码不匹配,需要再次验证;另一方面,可以实现代码的复用。

3.1.2 接口与抽象方法

说明:无需再次开发。

3.1.3 配置SQL映射

说明:无需再次开发。

在代码书写之前的分析过程中,如果发现某个功能模块已经开发完成,那么就可以省略当前的开发步骤,但是这个分析过程不能省略。

3.2 用户-登录-业务层

按照这三个步骤进行。

3.2.1 规划异常

1.如果用户名不存在则登录失败,抛出com.sharm.store.service.ex.UserNotFoundException异常。该异常为运行时异常,同时属于业务层异常。

package com.sharm.store.service.ex;/** 用户数据不存在的异常 */
public class UserNotFoundException extends ServiceException {// Override Methods...
}

2.如果用户的isDelete字段的值为1,则表示当前用户数据被标记为“已删除”,需进行登录失败操作同时抛出UserNotFoundException。该异常为运行时异常,同时属于业务层异常。

3.如果密码错误则进行登录失败操作,同时抛出com.sharm.store.service.ex.PasswordNotMatchException异常。该异常为运行时异常,同时属于业务层异常。

package com.sharm.store.service.ex;/** 密码验证失败的异常 */
public class PasswordNotMatchException extends ServiceException {// Override Methods...
}

4.创建以上UserNotFoundException和PasswordNotMatchException异常类,以上异常类应继承自ServiceException类。

3.2.2 接口与抽象方法

在IUserService接口中添加登录功能的抽象方法。

/*** 用户登录* @param username 用户名* @param password 密码* @return 登录成功的用户数据*/
User login(String username, String password);

之所以使用 User,而不使用布尔类型来作为返回值,是因为,当登录成功后需要获取该用户的id,以便于后续识别该用户的身份,并且还需要获取该用户的用户名、头像等数据,用于显示在软件的界面中,同时还需使用可以封装用于id、用户名和头像的数据的类型来作为登录方法的返回值类型。所以这是一种很巧妙的方法。

3.2.3 实现抽象方法

1.在UserServiceImpl类中添加login(String username, String password)方法并分析业务逻辑

@Override
public User login(String username, String password) {// 调用userMapper的findByUsername()方法,根据参数username查询用户数据// 判断查询结果是否为null// 是:抛出UserNotFoundException异常// 判断查询结果中的isDelete是否为1// 是:抛出UserNotFoundException异常// 从查询结果中获取盐值// 调用getMd5Password()方法,将参数password和salt结合起来进行加密// 判断查询结果中的密码,与以上加密得到的密码是否不一致// 是:抛出PasswordNotMatchException异常// 创建新的User对象// 将查询结果中的uid、username、avatar封装到新的user对象中// 返回新的user对象return null;
}

2.login(String username, String password)方法中代码的具体实现。

@Override
public User login(String username, String password) {// 调用userMapper的findByUsername()方法,根据参数username查询用户数据User result = userMapper.findByUsername(username);// 判断该用户是否存在,判断查询结果是否为nullif(result == null){// 是:抛出UserNotFoundException异常throw new UserNotFoundException("用户数据不存在");}// 如果该用户存在,再判断该用户是否已经注销。判断查询结果中的isDelete是否为1if(result.getIsDelete() == 1){// 是:抛出UserNotFoundException异常throw new UserNotFoundException("用户数据不存在");}// 从查询结果中获取盐值String salt = result.getSalt();// 调用getMd5Password()方法,将参数password和salt结合起来进行加密String md5Password = getMd5Password(password, salt);// 判断查询结果中的密码,与以上加密得到的密码是否不一致if(!result.getPassword().equals(md5Password)){// 是:抛出PasswordNotMatchException异常throw new PasswordNotMatchException("密码验证失败的错误");}// 为啥要创建一个新的对象,不是多此一举吗?// 事实上并不是,这样做可以使得各个层之间的数据传输量变小,当数据的体量非常大时,那么提高的性能是非常大的,也就是空间换时间User user = new User();// 将查询结果中的uid、username、avatar封装到新的user对象中user.setUid(result.getUid());user.setUsername(result.getUsername());user.setAvatar(result.getAvatar());// 返回新的user对象return user;
}

3.完成后在UserServiceTests中编写并完成单元测试。

@Test
public void login(){try {User sharm = iUserService.login("sharm", "123456");System.out.println(sharm);} catch (ServiceException e) {System.out.println(e.getMessage());}
}

3.3 用户-登录-控制器

3.3.1 处理异常

处理用户登录功能时,在业务层抛出了UserNotFoundException和PasswordNotMatchException异常,而这两个异常均未被处理过。则应在BaseController类的处理异常的方法中,添加这两个分支进行处理。

/** 控制器类的基类 */
public class BaseController {/** 操作成功的状态码 */public static final int OK = 200;/** @ExceptionHandler用于统一处理方法抛出的异常 */@ExceptionHandler(ServiceException.class)public JsonResult<Void> handleException(Throwable e){JsonResult<Void> result = new JsonResult<Void>(e);// 具体的异常可以从4000开始写,笼统的异常可以从5000开始写if (e instanceof UsernameDuplicateException) {result.setState(4000);} else if (e instanceof UserNotFoundException) {result.setState(4001);} else if (e instanceof PasswordNotMatchException) {result.setState(4002);} else if (e instanceof InsertException) {result.setState(5000);}return result;}
}

3.3.2 设计请求

设计用户提交的请求,并设计响应的方式:

请求路径:/users/login
请求参数:String username, String password
请求类型:POST
响应结果:JsonResult<User>

3.3.3 处理请求

1.在UserController类中添加处理登录请求的login(String username, String password)方法。

@RequestMapping("login")
public JsonResult<User> login(String username, String password) {// 调用业务对象的方法执行登录,并获取返回值// 将以上返回值和状态码OK封装到响应结果中并返回return null;
}

2.处理登录请求的login(String username, String password)方法代码具体实现。

// @RestController = @Controller + @ResponseBody : 便可以直接使用 JSON 返回
/** 处理用户相关请求的控制器类 */
@RestController
@RequestMapping("users")
public class UserController extends BaseController{@Autowiredprivate IUserService userService;// SpringBoot 就是通过这种约定大于配置的方式来进行编程的// 接受数据方式:该方法的参数列表设置为 pojo 来接受前端的数据,SpringBoot 会// 将前端的 url 地址中的参数名和 pojo 类的属性名进行比较,如果这两个名称相同,// 则将值注入到 pojo 类对应的属性中。@RequestMapping("reg")public JsonResult<Void> reg(User user) {userService.reg(user);return new JsonResult<Void>(OK);}// 接受数据方式:该方法的参数列表设置为非 pojo 来接受前端的数据,SpringBoot 会直接// 将请求的参数名和方法的参数名进行比较,如果名称相同则自动完成值的依赖注入@RequestMapping("login")public JsonResult<User> login(String username, String password) {// 调用业务对象的方法执行登录,并获取返回值User data = userService.login(username, password);// 将以上返回值和状态码OK封装到响应结果中并返回return new JsonResult<User>(OK, data);}
}

IDEA 复制当前行代码:把光标放在当前行的末尾,按下 Ctrl + C 即可复制当前行。

4.完成后启动项目,访问http://localhost:8080/users/login?username=sharm&password=123456请求进行登录。

3.4 用户-登录-前端页面

1.在login.html页面中body标签内部的最后,添加script标签用于编写JavaScript程序。

<!--作为后端工程师,我们只需要读懂表单和写一些简单的JS代码即可-->
<!--这个 AJAX 要写得非常熟练 -->
<!--这里的 url 必须以斜杠开头 -->
<script type="text/javascript">$("#btn-login").click(function() {$.ajax({url: "/users/login",type: "POST",data: $("#form-login").serialize(),dataType: "json",success: function(json) {if (json.state == 200) {alert("登录成功!");// 跳转到相同路径下location.href = "index.html";} else {alert("登录失败!" + json.message);}}});
});
</script>

2.完成后启动项目,打开浏览器访问http://localhost:8080/web/login.html页面并进行登录。

3.5 用户-登录- Session会话

3.5.1 当前登录方式可能产生的问题

由于登录后用户的信息是保存在变量中,这意味着,每次想要用户的信息,都需要重新登录一遍。

因此,我们需要一个全局变量来保存用户的信息,而 Session 便可以达到这样的目的。

3.5.2 何为 Session

为了弥补 HTTP 的无状态特征,也就是说 HTTP 协议自身不对请求和响应之间的通信状态进行保存,所以就出现了 Cookie 和 Session。其中 Session保存在服务器端,Cookie保存在客户端。

其中,Session 可以保存任何数据类型,而 Cookie 只可以保存 String 类型。

为了介绍这两种保存方式的使用,在本项目中,将用户名信息保存在 Session 中,将头像信息保存在 Cookie 中。

3.5.3 使用

对于 Session 中的数据,主要有两个操作,分别是存储和使用。

  • 存储发生在用户的首次登录过程中,当用户登录成功后,即将用户信息封装到 Session 对象中。 session.setAttribute()

  • 使用则根据具体场景来确定。 session.getAttribute()

在 SpringBoot 中,会自动帮我们生成一个全局的 Session 对象,通过将 HttpSession 类型的对象作为请求方法的参数,则会自动将全局的 Session 对象注入到请求处理的方法的 Session 形参中。

1.数据的设置只依赖于登录,因此将 Session 数据的存储部分放在 login 方法中;

@RequestMapping("login")
public JsonResult<User> login(String username, String password, HttpSession session) {// HttpSession 类型的对象作为请求方法的参数// 调用业务对象的方法执行登录,并获取返回值User data = userService.login(username, password);// 登录成功后,将uid和username存入到HttpSession中session.setAttribute("uid", data.getUid());session.setAttribute("username", data.getUsername());// 将以上返回值和状态码OK封装到响应结果中并返回return new JsonResult<User>(OK, data);
}

2.由于 Session 只在控制层使用,且控制层的类都继承了 BaseController,因此可以将 Session 数据的使用放在 BaseController 中。

/**
* 从HttpSession对象中获取uid
* HttpSession 是一个接口,既然 session 可以作为形参输入,那么说明肯定存在一个 HttpSession 的实现类。
* @param session HttpSession对象* @return 当前登录的用户的id
*/
protected final Integer getUidFromSession(HttpSession session) {return Integer.valueOf(session.getAttribute("uid").toString());
}/**
* 从HttpSession对象中获取用户名
* @param session HttpSession对象
* @return 当前登录的用户名
*/
protected final String getUsernameFromSession(HttpSession session) {return session.getAttribute("username").toString();
}

3.6 用户-登录- 拦截器

3.6.1 当前登录方式可能产生的问题

在实际项目中,我们需要保证有些页面需要用户登录之后才能访问,比如购物车.html等,而有些页面不需要页面登录也可以访问,比如主页.html。这个时候,我们便需要拦截器的帮助。

3.6.2 何为拦截器

拦截器的相关概念,传送门。

通过在拦截器中定义相关过滤规则,可以在 SpringMVC 的 DispatcherServlet 方法之前进行判断,只允许满足规则的请求进去。

SpringBoot 是依靠 SpringMVC 来完成拦截器功能的,通过定义一个实现 HandlerInterceptor 接口的类,来表示拦截器。

HandlerInterceptor 中存在三个重要的方法,如下:

方法 功能
preHandle() 该方法将在请求处理之前被调用。该方法的返回值是布尔值类型,当返回false时,表示请求结束,后续的Interceptor和Controller都不会再执行;当返回值true时,就会继续调用下一个Interceptor的preHandle方法,如果已经是最后一个Interceptor的时,就会调用当前请求的Controller方法。
postHandle() 该方法将在当前请求进行处理之后,也就是Controller方法调用之后执行,但是它会在DispatcherServlet进行视图返回渲染之前被调用,所以我们可以在这个方法中对Controller处理之后的ModelAndView对象进行操作。postHandle方法被调用的方向跟preHandle是相反的,也就是说先声明的Interceptor的postHandle方法反而会后执行。如果当前Interceptor的preHandle()方法返回值为false,则此方法不会被调用。
afterCompletion() 该方法将在整个当前请求结束之后,也就是在DispatcherServlet渲染了对应的视图之后执行。这个方法的主要作用是用于进行资源清理工作。如果当前Interceptor的preHandle()方法返回值为false,则此方法不会被调用。

3.6.3 使用

1.在com.sharm.store.interceptor定义一个实现了 HandlerInterceptor 接口的类 LoginInterceptor,即拦截器;

package com.sharm.store.interceptor;import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {if (request.getSession().getAttribute("uid") == null) {// 使用重定向response.sendRedirect("/web/login.html");return false;}return true;}
}

2.将拦截器注册到项目中。通过借助 WebMvcConfigure 接口,可以将用户自定义的拦截器进行注册,才能保证拦截器能够生效和使用。首先定义一个配置类,将这个类实现 WebMvcConfigure 接口,然后在这个类中实例化自定义的拦截器。

在com.sharm.store.config下创建LoginInterceptorConfigurer拦截器的配置类并实现org.springframework.web.servlet.config.annotation.WebMvcConfigurer接口,配置类需要添加@Configruation注解修饰。

package com.sharm.store.config;import com.sharm.store.interceptor.LoginInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;import java.util.ArrayList;
import java.util.List;/*** 拦截器的注册。一定要加上 @Configuration 注解修饰。*/
@Configuration
public class LoginInterceptorConfigurer implements WebMvcConfigurer {/***  配置拦截器* @param registry*/@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 创建自定义的拦截器对象HandlerInterceptor interceptor = new  LoginInterceptor();// 白名单List<String> patterns = new ArrayList<String>();// 静态资源不能过滤掉patterns.add("/bootstrap3/**");patterns.add("/css/**");patterns.add("/images/**");patterns.add("/js/**");patterns.add("/web/register.html");patterns.add("/web/login.html");patterns.add("/web/index.html");patterns.add("/web/product.html");patterns.add("/users/reg");patterns.add("/users/login");patterns.add("/districts/**");patterns.add("/products/**");// 通过注册工具添加拦截器registry.addInterceptor(interceptor).addPathPatterns("/**").excludePathPatterns(patterns);}
}

4 修改密码

4.1 用户-修改密码-持久层

4.1.1 规划需要执行的SQL语句

分析用户逻辑:从网页中可以看到,用户通过输入原密码和新密码来进行密码的修改。

规划底层逻辑:当用户输入原密码和新密码后,服务器首先判断该用户是否存在(尽管修改密码页面只有当用户登录后才能访问,但是可能存在用户登陆时,后台的管理员将该用户删除的情况),即 username 存在,但 is_delete 是否为1;然后判断原密码和新密码是否相同;最后再将新的密码保存到数据库中。

同时,为了跟踪数据的修改,我们还需要将修改人和修改时间也在表中更新。

1.用户修改密码时需要执行的SQL语句为:

UPDATE t_user SET password=?, modified_user=?, modified_time=? WHERE uid=?

2.检查用户信息需要执行的SQL语句为:

SELECT * FROM t_user WHERE uid=?

为什么不用用户名来做查询条件呢?

当不考虑管理员时,无论是 username 还是 uid 来作为查询条件都是可以的,因为该条数据在表中永不删除;

但当考虑管理员,n当新增一条数据时,自增的 uid 永远都不会相同,而用户名则可能相同,尽管用户在注销时,该数据仍然保存在数据库中,但管理员可以将该数据手动删除。

4.1.2 接口与抽象方法

在UserMapper接口中添加对应的两个抽象方法。

在Java代码中,通过利用@Param注解给对应的参数命名,那么在xml文件中,就可以通过将该参数传入到sql语句中得到结果。因此@Param(“参数名”)注解中的参数名需要和sql语句中的#{参数名}的参数名保持一致。

/**
* 根据uid更新用户的密码:增删改都会返回对应行数,所以我们可以将方法的返回值设置为Integer,不过 xml 中可以不需要弄一个返回值
* @param uid 用户的id
* @param password 新密码
* @param modifiedUser 最后修改执行人
* @param modifiedTime 最后修改时间
* @return 受影响的行数
*/
Integer updatePasswordByUid(@Param("uid") Integer uid,@Param("password") String password,@Param("modifiedUser") String modifiedUser,@Param("modifiedTime") Date modifiedTime);/*** 根据用户id查询用户数据* @param uid 用户id* @return 匹配的用户数据,如果没有匹配的用户数据,则返回null*/
User findByUid(Integer uid);

4.1.3 配置SQL映射

在UserMapper.xml中配置updatePasswordByUid()、findByUid()抽象方法的映射。

<!-- 根据uid更新用户的密码 -->
<update id="updatePasswordByUid">UPDATEt_user SETpassword = #{password},modified_user = #{modifiedUser},modified_time = #{modifiedTime} WHEREuid = #{uid}
</update><!-- 根据用户id查询用户数据 -->
<select id="findByUid" resultMap="UserEntityMap">SELECT*FROMt_userWHEREuid = #{uid}
</select>

4.1.4 单元测试

在UserMapperTests中编写并执行单元测试:

// 写测试类时可以将接口中的方法名复制过来,然后修改
@Test
public void updatePasswordByUid(){System.out.println(userMapper.findByUsername("sharm"));Date modifiedTime = new Date();userMapper.updatePasswordByUid(2,"zzm","sharm4" , modifiedTime);System.out.println(userMapper.findByUsername("sharm"));
}@Test
public void findByUid() {Integer uid = 2;User result = userMapper.findByUid(uid);System.out.println(result);
}

4.2 用户-修改密码-业务层

4.2.1 规划异常

1.用户在修改密码前,需要检查用户数据是否存在及是否被标记为“已删除”。如果检查不通过则应抛出UserNotFoundException异常;

2.用户修改密码时,可能会因为输入的原密码错误导致修改失败,则应抛出PasswordNotMatchException异常;

3.用户修改密码时,可能会出现新密码和原密码相同的情况,则应抛出 PasswordSameAsBeforeException异常;

4.更新数据时出现未知错误,则应抛出UpdateException异常;

同样,将这些异常继承自 ServiceException类。在 ex 包下新增对应异常。

4.2.2 接口与抽象方法

根据分析可知,为了修改密码,我们需要以下五个数据,分别是 uId、newPassword、oldPassword、modifiedUser、modifiedTime。其中 uId 和 modifiedUser 可以从 session 中拿到,modifiedTime 可以从 Date 对象中拿到。因此,该抽象方法只需要传入(Integer uid, String username, String oldPassword, String newPassword)四个形参即可。

/*** 修改密码* @param uid 当前登录的用户id* @param username 用户名* @param oldPassword 原密码* @param newPassword 新密码*/
void changePassword(Integer uid, String username, String oldPassword, String newPassword);

4.2.3 实现抽象方法

1.在UserServiceImpl类中实现changePassword()抽象方法。

public void changePassword(Integer uid, String username, String oldPassword, String newPassword) {// 调用userMapper的findByUid()方法,根据参数uid查询用户数据// 检查查询结果是否为null// 是:抛出UserNotFoundException异常// 检查查询结果中的isDelete是否为1// 是:抛出UserNotFoundException异常// 判断参数oldPassword、newPassword 是否相同// 是:抛出PPasswordSameAsBeforeException异常// 从查询结果中取出盐值// 将参数oldPassword结合盐值加密,得到oldMd5Password// 判断查询结果中的password与oldMd5Password是否不一致// 是:抛出PasswordNotMatchException异常// 将参数newPassword结合盐值加密,得到newMd5Password// 创建当前时间对象// 调用userMapper的updatePasswordByUid()更新密码,并获取返回值// 判断以上返回的受影响行数是否不为1// 是:抛了UpdateException异常
}

2.changePassword()方法的具体代码。

String中的equals与contentEquals方法,都可以用来比较String对象内容是否相同。

@Override
public void changePassword(Integer uid, String username, String oldPassword, String newPassword) {User user = userMapper.findByUid(uid);if(user == null || user.getIsDelete() == 1){throw new UserNotFoundException("用户不存在");}if(oldPassword.equals(newPassword)){throw new PasswordSameAsBeforeException("新密码不能和原密码相同");}String salt = user.getSalt();String oldMd5Password = getMd5Password(oldPassword, salt);if(!user.getPassword().equals(oldMd5Password)){throw new PasswordNotMatchException("输入的原密码错误");}String newMd5Password = getMd5Password(newPassword, salt);Integer rows = userMapper.updatePasswordByUid(uid, newMd5Password, username, new Date());// 判断以上返回的受影响行数是否不为1if (rows != 1) {// 是:抛出UpdateException异常throw new UpdateException("更新用户数据时出现未知错误,请联系系统管理员");}
}

4.2.4 单元测试

在UserServiceTests中编写并执行单元测试。

@Test
public void changePassword() {try {Integer uid = 5;String username = "jack";String oldPassword = "123456";String newPassword = "888888";iUserService.changePassword(uid, username, oldPassword, newPassword);System.out.println("密码修改成功!");} catch (ServiceException e) {System.out.println("密码修改失败!" + e.getClass().getSimpleName());System.out.println(e.getMessage());}
}

4.3 用户-修改密码-控制层

4.3.1 处理异常

在用户修改密码的业务中抛出了新的PasswordSameAsBeforeException和UpdateException异常,需要在BaseController类中进行处理。

/** @ExceptionHandler用于统一处理方法抛出的异常 */
@ExceptionHandler(ServiceException.class)
public JsonResult<Void> handleException(Throwable e){JsonResult<Void> result = new JsonResult<Void>(e);// 具体的异常可以从4000开始写,笼统的异常可以从5000开始写if (…………) {…………} else if (e instanceof PasswordSameAsBeforeException) {result.setState(4003);}else if (…………) {…………);}else if (e instanceof UpdateException) {result.setState(5001);}return result;
}

4.3.2 设计请求

设计用户提交的请求,并设计响应的方式。

请求路径:/users/change_password
请求参数(方法的形参):String oldPassword, String newPassword, HttpSession session
请求类型:POST
响应结果:JsonResult<Void>

4.3.3 处理请求

1.在UserController类中添加处理请求的changePassword(String oldPassword, String newPassword, HttpSession session)方法。

@RequestMapping("change_password")
public JsonResult<Void> changePassword(String oldPassword, String newPassword, HttpSession session) {// 调用session.getAttribute("")获取uid和username// 调用业务对象执行修改密码// 返回成功return null;
}

2.实现UserController控制器中的修改密码方法的代码。

@RequestMapping("change_password")
public JsonResult<Void> changePassword(String oldPassword, String newPassword, HttpSession session) {// 调用session.getAttribute("")获取uid和usernameInteger uid = getUidFromSession(session);String username = getUsernameFromSession(session);// 调用业务对象执行修改密码userService.changePassword(uid, username, oldPassword, newPassword);// 返回成功return new JsonResult<Void>(OK);
}

3.启动项目先登录,再访问http://localhost:8080/users/change_password?oldPassword=123456&newPassword=654321进行测试。

4.4 用户-修改密码-前端页面

1.在password.html页面中body标签内部的最后,添加script标签用于编写JavaScript程序。

<script type="text/javascript">$("#btn-change-password").click(function() {$.ajax({url: "/users/change_password",type: "POST",data: $("#form-change-password").serialize(),dataType: "json",success: function(json) {if (json.state == 200) {alert("修改成功!");} else {alert("修改失败!" + json.message);}}});});
</script>

2.启动项目先登录,再访问http://localhost:8080/web/password.html页面并进行修改密码。

问题一:如果无法正常将数据传递给后台,重启动系统和IDEA开发工具,登陆后便可修改密码。

问题二:在操作前端页面时用户进入修改密码页面,长时间停留在当前页面未进行任何操作,将导致登录信息过期。此时点击修改按钮时,仍会向/users/change_password发送请求,会被拦截器重定向到登录页面。由于整个过程是由$.ajax()函数采用异步的方式处理的,所以重定向也是由异步任务完成的,在页面中没有任何表现就会出现“用户登录信息超时后点击按钮没有任何反应”的问题。

解决方案:可以在password.html页面的$.ajax()中补充error属性的配置,该属性的值是一个回调函数。当服务器未正常响应状态码时,例如出现302、400、404、405、500等状态码时,将会调用该函数。

error: function (xhr) {alert("您的登录信息已经过期,请重新登录!HTTP响应码:" + xhr.status);location.href = "login.html";
}

问题三: 提交的参数和后端处理方法的形参名不一致怎么办?

如:提交数据为:http://localhost:8080/hello?username=sharm

处理方法:

@RequestMapping("/hello")
//@RequestParam("username") : username 提交的域的名称
public String hello(@RequestParam("username") String name){System.out.println(name);return "result";
}

5 个人资料修改

5.1 用户-修改密码-持久层

5.1.1 规划需要执行的SQL语句

分析用户逻辑

  1. 打开个人资料页面后,各个文本框会显示当前用户的信息;其中用户名是固定的,无法修改。
  2. 填写新的资料并且点击修改后,该用户的相关信息会进行更新;

规划底层逻辑:

1.打开个人资料页面后,各个信息会以 Ajax 的方式传到前端,执行的 SQL 语句为:

SELECT * FROM t_user WHERE uid=?

1.该查询功能已经实现,无需再次开发;

2.执行修改用户个人资料的SQL语句为:

UPDATE t_user SET phone=?, email=?, gender=?, modified_user=?, modified_time=? WHERE uid=?

5.1.2 接口与抽象方法

在UserMapper接口中添加updateInfoByUid(User user)方法。

/*** 根据uid更新用户资料* @param user 封装了用户id和新个人资料的对象* @return 受影响的行数*/
Integer updateInfoByUid(User user);

5.1.3 配置SQL映射

1.在UserMapper.xml中配置Integer updateInfoByUid(User user)抽象方法的映射。

<!-- 根据uid更新用户个人资料:Integer updateInfoByUid(User user) -->
<update id="updateInfoByUid">UPDATEt_user SET# 确保这三个字段不为空,如果为空,则默认不修改。test 为判断条件# 如果 if 中条件不满足,则不执行这个判断内的操作<if test="phone != null">phone = #{phone},</if><if test="email != null">email = #{email},</if><if test="gender != null">gender = #{gender},</if>modified_user = #{modifiedUser},modified_time = #{modifiedTime}WHEREuid = #{uid}
</update>

5.1.4 单元测试

在UserMapperTests中编写并执行单元测试。

@Test
public void updateInfoByUid(){// uId 封装在 User 对象中,不需要另外给定User user = new User();user.setUid(2);user.setPhone("12345");user.setEmail("share_me@126.com");user.setGender(0);user.setCreatedUser("sharm");user.setCreatedTime(new Date());Integer rows = userMapper.updateInfoByUid(user);System.out.println("受到影响的行数为" + rows);}

5.2 用户-修改密码-业务层

5.2.1 规划异常

用户修改个人资料由两个功能组成:

  • 打开页面时显示当前登录的用户的信息;
  • 点击修改按钮时更新用户的信息。

因此,可能出现的异常为:

  1. 打开该页面显示当前用户信息时,可能会存在用户数据不存在、用户已经被标记为“已删除”而无法显示正确的显示效果,或者由于 Session 失效,无法通过 uId 查询到相应用户,则抛出UserNotFoundException异常;
  2. 点击修改按钮更新时,可能会存在用户数据不存在、用户已经被标记为“已删除”而无法显示正确的显示效果,或者由于 Session 失效,无法通过 uId 查询到相应用户,则抛出UserNotFoundException异常;
  3. 点击修改按钮更新时,可能抛出 UpdateException 异常。

5.2.2 接口与抽象方法

与修改密码时将 findByUid() 放在 changePassword() 方法中不同,由于个人资料修改这一功能需要单独使用根据 uid 查找用户信息这一方法,因此需要将其独立出来。

在 IUserService 接口中添加两个抽象方法,分别对应以上两个功能。

/*** 获取当前登录的用户的信息* @param uid 当前登录的用户的id* @return 当前登录的用户的信息*/
User getByUid(Integer uid);/*** 修改用户资料* @param uid 当前登录的用户的id* @param username 当前登录的用户名* @param user 用户的新的数据*/
void changeInfo(Integer uid, String username, User user);

5.2.3 实现抽象方法

1.getByUid(Integer uid) 和 changeInfo(Integer uid, String modifiedUser, User user) 这两个抽象方法的流程思路。

@Override
public User getByUid(Integer uid) {// 调用userMapper的findByUid()方法,根据参数uid查询用户数据// 判断查询结果是否为null// 是:抛出UserNotFoundException异常// 判断查询结果中的isDelete是否为1// 是:抛出UserNotFoundException异常// 创建新的User对象// 将以上查询结果中的username/phone/email/gender封装到新User对象中// 返回新的User对象
}@Override
public void changeInfo(Integer uid, String modifiedUser, User user) {// 调用userMapper的findByUid()方法,根据参数uid查询用户数据// 判断查询结果是否为null// 是:抛出UserNotFoundException异常// 判断查询结果中的isDelete是否为1// 是:抛出UserNotFoundException异常// 向参数user中补全数据:uid// 向参数user中补全数据:modifiedUser(modifiedUser)// 向参数user中补全数据:modifiedTime(new Date())// 调用userMapper的updateInfoByUid(User user)方法执行修改,并获取返回值// 判断以上返回的受影响行数是否不为1// 是:抛出UpdateException异常
}

2.getByUid(Integer uid)和changeInfo(Integer uid, String username, User user)方法的具体代码实现。

@Override
public User getByUid(Integer uid) {// 调用userMapper的findByUid()方法,根据参数uid查询用户数据User result = userMapper.findByUid(uid);// 判断查询结果是否为nullif (result == null) {// 是:抛出UserNotFoundException异常throw new UserNotFoundException("用户数据不存在");}// 判断查询结果中的isDelete是否为1if (result.getIsDelete().equals(1)) {// 是:抛出UserNotFoundException异常throw new UserNotFoundException("用户数据不存在");}// 创建新的User对象User user = new User();// 将以上查询结果中的username/phone/email/gender封装到新User对象中user.setUsername(result.getUsername());user.setPhone(result.getPhone());user.setEmail(result.getEmail());user.setGender(result.getGender());// 返回新的User对象return user;
}@Override
public void changeInfo(Integer uid, String modifiedUser, User user) {// 调用userMapper的findByUid()方法,根据参数uid查询用户数据User result = userMapper.findByUid(uid);// 判断查询结果是否为nullif (result == null) {// 是:抛出UserNotFoundException异常throw new UserNotFoundException("用户数据不存在");}// 判断查询结果中的isDelete是否为1if (result.getIsDelete().equals(1)) {// 是:抛出UserNotFoundException异常throw new UserNotFoundException("用户数据不存在");}// 向参数user中补全数据:uiduser.setUid(uid);// 向参数user中补全数据:modifiedUser(modifiedUser)user.setModifiedUser(modifiedUser);// 向参数user中补全数据:modifiedTime(new Date())user.setModifiedTime(new Date());// 调用userMapper的updateInfoByUid(User user)方法执行修改,并获取返回值Integer rows = userMapper.updateInfoByUid(user);// 判断以上返回的受影响行数是否不为1if (rows != 1) {// 是:抛出UpdateException异常throw new UpdateException("更新用户数据时出现未知错误,请联系系统管理员");}
}

5.2.4 单元测试

@Test
public void getByUid() {try {Integer uid = 3;User user = iUserService.getByUid(uid);System.out.println(user);} catch (ServiceException e) {System.out.println(e.getClass().getSimpleName());System.out.println(e.getMessage());}
}@Test
public void changeInfo() {try {Integer uid = 2;String modifiedUser = "sharm";User user = new User();user.setPhone("15512328888");user.setEmail("admin03@126.cn");user.setGender(2);iUserService.changeInfo(uid, modifiedUser, user);System.out.println("OK.");} catch (ServiceException e) {System.out.println(e.getClass().getSimpleName());System.out.println(e.getMessage());}
}

5.3 用户-个人资料-控制器

5.3.1 处理异常

说明:由于捕获该功能的异常已经书写,因此这里不需要重复书写。

5.3.2 设计请求

1.设计用户提交显示当前登录的用户信息的请求,并设计响应的方式。

请求路径:/users/get_by_uid
请求参数:HttpSession session
请求类型:GET
响应结果:JsonResult<User>

2.设计用户提交执行修改用户信息的请求,并设计响应的方式。

请求路径:/users/change_info
请求参数:User user, HttpSession session
请求类型:POST
响应结果:JsonResult<Void>

5.3.3 处理请求

1.处理获取用户信息请求

1.在UserController类中添加处理请求的getByUid()方法。

@GetMapping("get_by_uid")
public JsonResult<User> getByUid(HttpSession session) {// 从HttpSession对象中获取uid// 调用业务对象执行获取数据// 响应成功和数据return null;
}

2.getByUid(HttpSession session)方法中具体代码实现为。

@GetMapping("get_by_uid")
public JsonResult<User> getByUid(HttpSession session) {// 从HttpSession对象中获取uidInteger uid = getUidFromSession(session);// 调用业务对象执行获取数据User data = userService.getByUid(uid);// 响应成功和数据return new JsonResult<User>(OK, data);
}

3.完成后启动项目,打开浏览器先登录,再访问http://localhost:8080/users/get_by_uid请求进行测试。

2.处理修改用户个人信息请求

1.在UserController类中添加处理请求的changeInfo(User user, HttpSession session)方法。

@RequestMapping("change_info")
public JsonResult<Void> changeInfo(User user, HttpSession session) {// 从HttpSession对象中获取uid和username// 调用业务对象执行修改用户资料// 响应成功return null;
}

2.changeInfo(User user, HttpSession session)方法中具体代码实现为。

@RequestMapping("change_info")
public JsonResult<Void> changeInfo(User user, HttpSession session) {// 从HttpSession对象中获取uid和usernameInteger uid = getUidFromSession(session);String modifiedUser = getUsernameFromSession(session);// 调用业务对象执行修改用户资料userService.changeInfo(uid, modifiedUser, user);// 响应成功return new JsonResult<Void>(OK);
}

3.完成后启动项目,打开浏览器先登录,再访问http://localhost:8080/users/change_info?phone=18758558336&email=share_me@126.com&gender=1进行测试。

5.4 用户-个人资料-前端页面

1.在userdata.html页面中body标签内部的最后,添加script标签用于编写JavaScript程序。

<script type="text/javascript">$(document).ready(function() {$.ajax({url: "/users/get_by_uid",type: "GET",dataType: "json",success: function(json) {if (json.state == 200) {// 浏览器的 console 输出console.log("username=" + json.data.username);console.log("phone=" + json.data.phone);console.log("email=" + json.data.email);console.log("gender=" + json.data.gender);$("#username").val(json.data.username);$("#phone").val(json.data.phone);$("#email").val(json.data.email);// 如果 radio 仅仅是一个引用,那是可以理解的var radio = (json.data.gender == 0 ? $("#gender-female") : $("#gender-male"));radio.prop("checked", "checked");} else {alert("获取用户信息失败!" + json.message);}}});});$("#btn-change-info").click(function() {$.ajax({url: "/users/change_info",type: "POST",data: $("#form-change-info").serialize(),dataType: "json",success: function(json) {if (json.state == 200) {alert("修改成功!");location.href = "login.html";} else {alert("修改失败!" + json.message);}},error: function(xhr) {alert("您的登录信息已经过期,请重新登录!HTTP响应码:" + xhr.status);location.href = "login.html";}});});
</script>

2.完成后启动项目,打开浏览器先登录,再访问http://localhost:8080/web/userdata.html页面并进行用户个人资料的修改测试。


6 用户头像上传

6.1 用户-头像上传-持久层

6.1.1 规划需要执行的SQL语句

长传文件在持久层的实际操作是:先将用户上传的文件保存到服务器端的某个位置,然后将保存文件的路径记录在数据库中。当后续需要使用该文件时,从数据库中读出文件的路径,即可实现在线访问该文件。

不需要将图片转化为字符串再保存到数据库中了,这种效率是很低的。相当于把整台服务器都看做是数据库。

因此,所执行的 SQL 语句为:

update t_user set avatar=?, modified_user=?, modified_time=? where uid=?

为啥操作数据库的语句有些是按照uid来操作,有些是按照username来操作,这就让人很迷惑。

6.1.2 接口与抽象方法

在UserMapper接口中添加updateAvatarByUid()抽象方法。

/*** 根据uid更新用户的头像* 该 @Param("SQL映射文件中#{}占位符的变量名")解决的问题是:当SQL语句的占位符和映射的接口方法参数名* 不一致时,需要将某个参数强行注入到某个占位符变量上时,可以使用@Param这个注解来解决* @param uid 用户的id* @param avatar 新头像的路径* @param modifiedUser 修改执行人* @param modifiedTime 修改时间* @return 受影响的行数*/
Integer updateAvatarByUid(@Param("uid") Integer uid,@Param("avatar") String avatar,@Param("modifiedUser") String modifiedUser,@Param("modifiedTime") Date modifiedTime);

6.1.3 配置SQL映射

在UserMapper.xml中配置updateAvatarByUid()抽象方法的映射。

<!-- 根据uid更新用户的头像 -->
<update id="updateAvatarByUid">UPDATEt_userSETavatar = #{avatar},modified_user = #{modifiedUser},modified_time = #{modifiedTime}WHEREuid = #{uid}
</update>

6.1.4 单元测试

在UserMapperTests中编写并执行单元测试。

@Test
public void updateAvatarByUidTest(){Integer uid = 6;String avatar =  "/upload/avatar.png";String modifiedUser = "超级管理员";Date modifiedTime = new Date();Integer rows = userMapper.updateAvatarByUid(uid, avatar, modifiedUser, modifiedTime);System.out.println("rows=" + rows);
}

6.2 用户-头像上传-业务层

6.2.1 规划异常

用户头像上传由一个功能组成:用户选择文件并点击上传按钮后,可以更新用户的信息。

因此,可能出现的异常为:

  1. 点击上传按钮更新时,可能会存在用户数据不存在、用户已经被标记为“已删除”而无法显示正确的显示效果,或者由于 Session 失效,无法通过 uId 查询到相应用户,则抛出UserNotFoundException异常;

  2. 点击修改按钮更新时,可能抛出 UpdateException 异常。

6.2.2 接口与抽象方法

通过观察持久层所需要的参数来决定接口中输入的参数,

update t_user set avatar=?, modified_user=?, modified_time=? where uid=?

因此我们需要在接口中输入:文件保存位置的字符串 avatar、用户ID uid、修改人 modified_user,而修改时间则可以通过Date对象获得。

故在IUserService中添加changeAvatar(Integer uid, String modified_user, String avatar)抽象方法。

/**
* 修改用户头像
* @param uid 当前登录的用户的id
* @param modified_user 修改人
* @param avatar 用户的新头像的路径
*/
void changeAvatar(Integer uid, String modified_user, String avatar);

6.2.3 实现抽象方法

在UserServiceImpl类中实现changeAvatar方法。

public void changeAvatar(Integer uid, String modified_user, String avatar) {// 调用userMapper的findByUid()方法,根据参数uid查询用户数据User result = userMapper.findByUid(uid);// 检查查询结果是否为null// 是:抛出UserNotFoundException// 检查查询结果中的isDelete是否为1// 是:抛出UserNotFoundExceptionif(result == null || result.getIsDelete().equals(1)){throw new UserNotFoundException("用户数据不存在");}// 创建当前时间对象Date nowTime = new Date();// 调用userMapper的updateAvatarByUid()方法执行更新,并获取返回值Integer rows = userMapper.updateAvatarByUid(uid, avatar, modified_user, nowTime);// 判断以上返回的受影响行数是否不为1if (rows != 1) {// 是:抛出UpdateExceptionthrow new UpdateException("更新用户数据时出现未知错误,请联系系统管理员");}
}

6.2.4 单元测试

在UserServiceTests类中进行单元测试。

@Test
public void changeAvatarTest() {try {Integer uid = 6;String modified_user = "头像管理员";String avatar = "/upload/avatar.png";iUserService.changeAvatar(uid, modified_user, avatar);System.out.println("OK.");} catch (ServiceException e) {System.out.println(e.getClass().getSimpleName());System.out.println(e.getMessage());}
}

6.3 用户-头像上传-控制层

6.3.1 处理异常

这里是我们第一次在控制层抛出异常,之所以这么操作,是因为这些异常并不是业务上的异常,可以直接在控制层来判断。

FileUploadException 泛指文件上传时的异常,继承自RuntimeException其中其还有几个子类异常,分别是:FileEmptyException:文件为空的异常FileSizeException:文件大小超出了限制值FileTypeException:文件类型超出了限制FileStateException:文件状态异常FileUploadIOException:文件时读写异常

首先,需要创建文件上传相关异常的基类,即在com.sharm.store.controller.ex包下创建FileUploadException类,并继承自RuntimeException类。

package com.sharm.store.service.ex;/** 文件上传相关异常的基类 */
public class FileUploadException extends RuntimeException {public FileUploadException() {super();}public FileUploadException(String message) {super(message);}public FileUploadException(String message, Throwable cause) {super(message, cause);}public FileUploadException(Throwable cause) {super(cause);}protected FileUploadException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {super(message, cause, enableSuppression, writableStackTrace);}
}

2.创建FileEmptyException异常类,并继承FileUploadException类。

package com.sharm.store.service.ex;/** 上传的文件为空的异常,例如没有选择上传的文件就提交了表单,或选择的文件是0字节的空文件 */
public class FileEmptyException extends FileUploadException {// Override Methods...
}

3.创建FileSizeException异常类,并继承FileUploadException类。

package com.sharm.store.service.ex;/** 上传的文件的大小超出了限制值 */
public class FileSizeException extends FileUploadException {// Override Methods...
}

4.创建FileTypeException异常类,并继承FileUploadException类。

package com.sharm.store.service.ex;/** 上传的文件类型超出了限制 */
public class FileTypeException extends FileUploadException {// Override Methods...
}

5.创建FileStateException异常类,并继承FileUploadException类。

package com.sharm.store.service.ex;/** 上传的文件状态异常 */
public class FileStateException extends FileUploadException {// Override Methods...
}

6.创建FileUploadIOException异常类,并继承FileUploadException类。

package com.sharm.store.service.ex;/** 上传文件时读写异常 */
public class FileUploadIOException extends FileUploadException {// Override Methods...
}

7.然后在BaseController的handleException()的@ExceptionHandler注解中添加FileUploadException.class异常的处理;最后在方法中处理这些异常。

// 在异常统一处理方法的参数列表上增加新的异常处理作为它的参数
@ExceptionHandler({ServiceException.class, FileUploadException.class})
public JsonResult<Void> handleException(Throwable e) {JsonResult<Void> result = new JsonResult<Void>(e);if (e instanceof UsernameDuplicateException) {result.setState(4000);} else if (e instanceof UserNotFoundException) {result.setState(4001);} else if (e instanceof PasswordNotMatchException) {result.setState(4002);} else if (e instanceof InsertException) {result.setState(5000);} else if (e instanceof UpdateException) {result.setState(5001);// 设置这些上传文件的异常码以6开头} else if (e instanceof FileEmptyException) {result.setState(6000);} else if (e instanceof FileSizeException) {result.setState(6001);} else if (e instanceof FileTypeException) {result.setState(6002);} else if (e instanceof FileStateException) {result.setState(6003);} else if (e instanceof FileUploadIOException) {result.setState(6004);}return result;
}

6.3.2 设计请求

设计用户提交的请求,并设计响应的方式:

请求路径:/users/change_avatar
请求参数:MultipartFile file, HttpSession session
请求类型:POST
响应结果:JsonResult<String>

SpringMVC 给我们引入了一个上传文件的接口 MultipartFile,这个接口为我们包装了获取文件类型的数据(任何类型的 file 都可以接受),只需要在处理请求的方法参数列表上声明一个参数类型为 MultipartFile 的参数,然后 SpringMVC 便会自动将传递给服务器的文件数据赋值给这个参数

6.3.3 处理请求

1.在UserController类中添加处理请求的changeAvatar(@RequestParam(“file”) MultipartFile file, HttpSession session)方法。

@PostMapping("change_avatar")
public JsonResult<String> changeAvatar(@RequestParam("file") MultipartFile file, HttpSession session) {// 判断上传的文件是否为空// 是:抛出异常// 判断上传的文件大小是否超出限制值// 是:抛出异常// 判断上传的文件类型是否超出限制// 是:抛出异常// 获取当前项目的绝对磁盘路径// 保存头像文件的文件夹// 保存的头像文件的文件名// 创建文件对象,表示保存的头像文件// 执行保存头像文件// 如果产生异常则抛出// 头像路径// 从Session中获取uid和username// 将头像写入到数据库中// 返回成功和头像路径return null;
}

2.changeAvatar(@RequestParam(“file”) MultipartFile file, HttpSession session)方法中具体代码实现为。

/** 头像文件大小的上限值(10MB) */
public static final int AVATAR_MAX_SIZE = 10 * 1024 * 1024;
/** 允许上传的头像的文件类型 */
public static final List<String> AVATAR_TYPES = new ArrayList<String>();/** 初始化允许上传的头像的文件类型,image单词不能加s */
static {AVATAR_TYPES.add("image/jpeg");AVATAR_TYPES.add("image/png");AVATAR_TYPES.add("image/bmp");AVATAR_TYPES.add("image/gif");
}@PostMapping("change_avatar")
public JsonResult<String> changeAvatar(@RequestParam("file") MultipartFile file, HttpSession session) {// 判断上传的文件是否为空if (file.isEmpty()) {// 是:抛出异常throw new FileEmptyException("上传的头像文件不允许为空");}// 判断上传的文件大小是否超出限制值if (file.getSize() > AVATAR_MAX_SIZE) { // getSize():返回文件的大小,以字节为单位// 是:抛出异常throw new FileSizeException("不允许上传超过" + (AVATAR_MAX_SIZE / 1024) + "KB的头像文件");}// 判断上传的文件类型是否超出限制String contentType = file.getContentType();// public boolean list.contains(Object o):当前列表若包含某元素,返回结果为true;若不包含该元素,返回结果为false。if (!AVATAR_TYPES.contains(contentType)) {// 是:抛出异常throw new FileTypeException("不支持使用该类型的文件作为头像,允许的文件类型:\n" + AVATAR_TYPES);}// 获取当前项目的绝对磁盘路径String parent = session.getServletContext().getRealPath("upload");// 保存头像文件的文件夹File dir = new File(parent);if (!dir.exists()) {dir.mkdirs();}// 保存的头像文件的文件名String suffix = "";String originalFilename = file.getOriginalFilename();int beginIndex = originalFilename.lastIndexOf(".");if (beginIndex > 0) {suffix = originalFilename.substring(beginIndex);}String filename = UUID.randomUUID().toString() + suffix;// 创建文件对象,表示保存的头像文件File dest = new File(dir, filename);// 执行保存头像文件try {// 将file文件中的数据写入到了dest中,前提时这两个文件的 后缀相同file.transferTo(dest);} catch (IllegalStateException e) {// 抛出异常throw new FileStateException("文件状态异常,可能文件已被移动或删除");} catch (IOException e) {// 抛出异常throw new FileUploadIOException("上传文件时读写错误,请稍后重尝试");}// 头像路径,保存一个相对路径String avatar = "/upload/" + filename;// 从Session中获取uid和usernameInteger uid = getUidFromSession(session);String username = getUsernameFromSession(session);// 将头像写入到数据库中userService.changeAvatar(uid, username, avatar);// 返回成功头像路径return new JsonResult<String>(OK, avatar);
}

6.4 用户-上传头像-前端页面

1.然后在upload.html页面中配置用户上传头像的form表单。

2.完成后启动项目,打开浏览器先登录,再访问http://localhost:8080/web/upload.html进行测试。

6.5 用户-上传头像-设置上传文件大小

1.SpringBoot中默认MultipartResolver的最大文件大小值为1M。如果上传的文件的大小超过1M,会抛FileSizeLimitExceededException异常。

2.如果需要调整上传的限制值,直接在启动类中添加getMultipartConfigElement()方法,并且在启动类之前添加@Configuration注解。

package com.cy.store;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.MultipartConfigFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.unit.DataSize;
import org.springframework.util.unit.DataUnit;
import javax.servlet.MultipartConfigElement;@Configuration
@SpringBootApplication
@MapperScan("com.cy.store.mapper")
public class StoreApplication {public static void main(String[] args) {SpringApplication.run(StoreApplication.class, args);}@Beanpublic MultipartConfigElement getMultipartConfigElement() {// 创建一个配置的工厂类对象MultipartConfigFactory factory = new MultipartConfigFactory();// 设置工厂需要创建的对象的相关信息// DataSize dataSize = DataSize.ofMegabytes(10);// 设置文件最大10M,DataUnit提供5中类型B,KB,MB,GB,TBfactory.setMaxFileSize(DataSize.of(10, DataUnit.MEGABYTES));factory.setMaxRequestSize(DataSize.of(10, DataUnit.MEGABYTES));// 设置总上传数据总大小10Mreturn factory.createMultipartConfig();}
}

3.除了以上编写方法配置上传的上限值以外,还可以通过在application.properties或application.yml中添加配置来实现。

(1) 低版本:1.X

spring.http.multipart.max-file-size=10MB
spring.http.multipart.max-request-size=10MB

(2) 高版本:2.X

#方式1
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=10MB
#方式2
spring.servlet.multipart.maxFileSize=10MB
spring.servlet.multipart.maxRequestSize=10MB

6.6 用户-上传头像-前端页面BUG解决

6.6.1 上传后显示头像

1.头像上传成功后,显示上传的头像。在upload.html页面中,是使用img标签来显示头像图片的。首先确定img标签是否添加有id="img-avatar"属性,便于后续访问该标签;而img标签是通过src属性来决定显示哪张图片的,所以修改src该属性的值即可设置需要显示的图片。修改表单添加id="form-change-avatar"属性。修改input标签,添加id="btn-change-avatar"和type="button"属性。

2.在upload.html页面中body标签内部的最后,添加script标签用于编写JavaScript程序。

  • processData:处理数据。默认情况下,processData的值是true,其代表以对象的形式上传的数据都会被转换为字符串的形式上传。而当上传文件的时候,则不需要把其转换为字符串,因此要改成false。
  • contentType:发送数据的格式。其代表的是前端发送数据的格式,默认值application/x-www-form-urlencoded。代表的是ajax的 data是以字符串的形式传递,使用这种传数据的格式,无法传输复杂的数据,比如多维数组、文件等。把contentType设置为false就会改掉之前默认的数据格式,在上传文件时就不会报错。
<script type="text/javascript">$("#btn-change-avatar").click(function() {$.ajax({url: "/users/change_avatar",type: "POST",data: new FormData($("#form-change-avatar")[0]),dataType: "JSON",processData: false, // processData处理数据contentType: false, // contentType发送数据的格式success: function(json) {if (json.state == 200) {$("#img-avatar").attr("src", json.data);} else {alert("修改失败!" + json.message);}},error: function(xhr) {alert("您的登录信息已经过期,请重新登录!HTTP响应码:" + xhr.status);location.href = "login.html";}});});
</script>

3.完成后启动项目,打开浏览器先登录,再访问http://localhost:8080/web/upload.html进行测试。

6.6.2 登录后显示头像

1.首先检查登录成功后是否返回了头像的数据。访问http://localhost:8080/users/login?username=sharm&password=321测试。

2.用户名、用户Id、用户头像等数据,属于常用数据,在客户端的许多页面都可能需要使用,如果每次都向服务器提交请求获取这些数据,是非常不合适的。可以在用户登录成功后,将这些数据存储在客户端本地,后续在客户端中需要显示这些数据时,直接从本地获取即可,无需再向服务器请求这些数据。在客户端本地存取数据时,可以使用Cookie技术。

3.设计思路:当用户登录成功后,将服务器返回的头像路径存储到本地的Cookie中,在打开“上传头像”页面时,从本地的Cookie中读取头像路径并显示即可。在登录login.html页面中,当登录成功后,将用户头像路径保存到Cookie中。

$("#btn-login").click(function() {$.ajax({url: "/users/login",type: "POST",data: $("#form-login").serialize(),dataType: "json",success: function(json) {if (json.state == 200) {alert("登录成功!");$.cookie("avatar", json.data.avatar, {expires: 7});console.log("cookie中的avatar=" + $.cookie("avatar"));location.href = "index.html";} else {alert("登录失败!" + json.message);}}});
});

语法:$.cookie(名称,值,[option])。[option]参数说明:

expires:有限日期,可以是一个整数或一个日期(单位天)。如果不设置这个值,默认情况下浏览器关闭之后此Cookie就会失效。

path:表示Cookie值保存的路径,默认与创建页路径一致。

domin:表示Cookie域名属性,默认与创建页域名一样。要注意跨域的概念,如果要主域名二级域名有效则要设置“.xxx.com”。

secrue:布尔类型的值,表示传输Cookie值时,是否需要一个安全协议。

4.在upload.html页面中,默认并没有引用jqueyr.cookie.js文件,因此无法识别$.cookie()函数;所以需要在upload.html页面head标签内添加jqueyr.cookie.js文件。

<script src="../bootstrap3/js/jquery.cookie.js" type="text/javascript" charset="utf-8"></script>

5.在打开页面时自动读取显示用户图像。获取Cookie中头像的路径,然后将获取到的头像路径设置给img标签的src属性以显示头像。在upload.html页面中的script标签的内部添加自动读取用户图像的jquery代码。

$(document).ready(function () {console.log("cookie中的avatar=" + $.cookie("avatar"));$("#img-avatar").attr("src", $.cookie("avatar"));
});

6.6.3 显示最新头像

以上代码表示“每次打开页面时,读取Cookie中的头像并显示”,如果此时重新上传用户头像,而Cookie中所保存的头像还是之前上传的头像路径值,无法显示最新的用户头像。所以当用户重新上传头像后,还应把新头像的路径更新到Cookie中。

1.在upload.html页面中,用户头像修改成功后,并将新的用户头像路径保存到Cookie中。

$.cookie("avatar", json.data, {expires: 7});

2.完成后启动项目,打开浏览器先登录,再访问http://localhost:8080/web/upload.html进行测试。

基于SpringBoot的项目:商城项目相关推荐

  1. SpringBoot+MyBatis 天猫商城项目,超级漂亮【附源码】

    大家好,我是小咖秀! 帮粉丝找了一个基于SpringBoot的天猫商城项目,快速部署运行,所用技术:MySQL,Druid,Log4j2,Maven,Echarts,Bootstrap ... 免费给 ...

  2. [转]2020年排名前20的基于SpringBoot搭建的开源项目,帮你快速进行项目搭建!

    △Hollis, 一个对Coding有着独特追求的人△ 这是Hollis的第 287 篇原创分享 作者 l Hollis 来源 l Hollis(ID:hollischuang) SpringBoot ...

  3. 2020年排名前20的基于SpringBoot搭建的开源项目,帮你快速进行项目搭建!

    △Hollis, 一个对Coding有着独特追求的人△ 这是Hollis的第 287 篇原创分享 作者 l Hollis 来源 l Hollis(ID:hollischuang) SpringBoot ...

  4. 基于springboot+mybatisplus+vue-科技项目评审及专家库管理系统

    基于springboot+mybatisplus+vue-科技项目评审及专家库管理系统 1.1 项目概述 开发语言:Java8 数据库:Mysql5 前端技术:echarts图表 渲染数据:thyme ...

  5. eclipse spring boot项目搭建_基于Spring-boot的kettle调度项目

    介绍 基于Spring-boot的kettle调度项目,参考了zhaxiaodong9860的代码并引用了其中的页面管理,后台代码自行参考了API进行工具化编写,方便使用 在原代码的基础上加入以下功能 ...

  6. 基于SpringBoot和MyBatisPlus的项目开发脚手架

    本博客主要功能是从创建Spring Boot项目开始讲解如何搭建一个较为通用的快速开发脚手架,方便在以后的开发中可以快速的应用,避免每次写都要去以前的项目里翻工具类和通用配置. 代码下载地址:基于Sp ...

  7. 毕业设计之基于springboot的开源商城系统

    一.介绍 本项目是一个基于springboot的开源商城系统 ,前后端分离. 二.功能模块 平台端 管理端 用户手机端 首页 会员 订单 商品 促销 店铺 运营 统计 设计 三.技术架构 前端 vue ...

  8. java基于springboot的网上商城购物系统

    项目介绍 随着社会的快速发展,计算机的影响是全面且深入的.人们生活水平的不断提高,日常生活中人们对网上商城购物系统方面的要求也在不断提高,购物的人数更是不断增加,使得网上商城购物系统的开发成为必需而且 ...

  9. (原创)基于springboot,vue宠物商城定制版v3.0

    (全新)基于springboot,vue宠物商城定制版v3.0 本人原创作品,用户前台.系统管理员后台项目完整,无任何bug. 每行代码都是本人自己写,我在代码上面都写有详细注释 开发工具:IDEA ...

  10. 基于Springboot 网上花店商城(源代码+数据库+报告) 010

    代码地址 https://gitee.com/ynwynwyn/SpringbootSchoolShop-public 基于Springboot 网上花店商城(源代码+数据库+报告) 一.系统介绍 包 ...

最新文章

  1. a[1]和1[a]——浅谈数组下标运算符
  2. AIO(Asynchronous IO)基本原理
  3. pytorch dataset_【小白学PyTorch】16.TF2读取图片的方法
  4. python 百分比数据_如何使用python计算数据列相对于另一列的百分比排名
  5. 如果你需要在 XHTML 中声明 DOCTYPE,必须使用到jsp:text动作元素
  6. es6=unicode码详解
  7. Property “pageNumber“ was accessed during render but is not defined on instance.
  8. matlab炮灰模型,非诚勿扰的数学分析
  9. python pop3_Python使用POP3和SMTP协议收发邮件
  10. Postman测试post接口
  11. c#中HttpWebRequest使用Proxy实现指定IP的域名请求
  12. Chrome\Edge 更新后访问网站变慢或加载不出来,其他浏览器正常
  13. WIN10 + Ubuntu18.04 双系统安装教程(新机)
  14. zabbix监控nginx状态页面
  15. 舒老师AK的hu测 T2. LX还在迷路(线段树+等差数列)
  16. Mysql访问日志记录
  17. 外部电源和锂电池自动切换电路
  18. ROS-Industrial 之 MoveIt —— 碰撞物体约束添加
  19. opengl的配置和太阳-地球-月球演示
  20. Linux内核进程,线程,进程组,会话组织模型以及进程管理

热门文章

  1. vim编辑器常用操作及快捷键
  2. 数据+AI技术,赋能企业业务和管理流程
  3. 简单理解光会产生折射的原因及折射定律的推导
  4. 软件设计师必考精华 - 计算机网络
  5. java metronic_基于Metronic的Bootstrap开发框架总览
  6. 计算机网络之数据链路层
  7. oracle websp,WebSP - 安全牛课堂 - 领先的信息安全在线教育平台
  8. Linux 内核参数说明
  9. STM32F4_时钟系统精讲
  10. 你早该这么玩Excel(读书笔记)_henry_dx_新浪博客