gitee源码地址

系统概述与环境搭建

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

2 项目分析

1.在开发某个项目之前,应先分析这个项目中可能涉及哪些种类的数据。本项目中涉及的数据:用户、商品、商品类别、收藏、订单、购物车、收货地址。

2.关于数据,还应该要确定这些数据的开发顺序。设计开发顺序的原则是:先开发基础、简单或熟悉的数据。以上需要处理的数据的开发流程是:用户-收货地址-商品类别-商品-收藏-购物车-订单。

3.在开发每种数据的相关功能时,先分析该数据涉及哪些功能。在本项目中以用户数据为例,需要开发的功能有:登录、注册、修改密码、修改资料、上传头像。

4.然后,在确定这些功能的开发顺序。原则上,应先做基础功能,并遵循增查删改的顺序来开发。则用户相关功能的开发顺序应该是:注册-登录-修改密码-修改个人资料-上传头像。

5.在实际开发中,应先创建该项目的数据库,当每次处理一种新的数据时,应先创建该数据在数据库中的数据表,然后在项目中创建该数据表对应的实体类。

6.在开发某个具体的功能时,应遵循开发顺序:持久层-业务层-控制器-前端页面。

3 创建数据库

1.首先确保计算机上安装了MariaDB-10.3.7-winx64数据库,将来在数据库中创建与项目相关的表。

2.创建电脑商城项目对应的后台数据库系统store。

1
CREATE DATABASE store character SET utf8;

4 创建Spring Initializr项目

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

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

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

3.将Group设置为com.cy,Artifact设置为store,其余选项使用默认值。单击【Next】进入Spring Boot场景依赖选择界面。

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

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

5 配置并运行项目

5.1 运行项目

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

1
2
3
4
5
6
7
8
9
10
package com.cy.store;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class StoreApplication {
public static void main(String[] args) {
SpringApplication.run(StoreApplication.class, args);
}
}

5.2 配置项目

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

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

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

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

3.为了便于查询JSON数据,隐藏没有值的属性,减少流量的消耗,服务器不应该向客户端响应为NULL的属性。可以在属性或类之前添加@JsonInclude(value=Include.NON_NULL),也可以在application.properties中添加全局的配置。

1
2
# 服务器向客户端不响应为null的属性
spring.jackson.default-property-inclusion=NON_NULL

4.SpringBoot项目的默认访问名称是“/”,如果需要修改可以手动在配置文件中指定SpringBoot 2.x访问项目路径的项目名。不建议修改。

1
server.servlet.context-path=/store

5.重新启动项目,则不在提示配置数据源失败的问题。

用户注册

1、创建数据库表(store)

1.选中数据库:

1
use store

2.创建t_user表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
create table `t_user` (
`uid` int (11),
`username` varchar (60),
`password` char (96),
`salt` char (108),
`phone` varchar (60),
`email` varchar (90),
`gender` int (11),
`avatar` varchar (150),
`is_delete` int (11),
`created_user` varchar (60),
`created_time` datetime ,
`modified_user` varchar (60),
`modified_time` datetime
);

2、创建用户的实体类

1.通过表的结构提取出表的公共字段,放在一个实体类的基类中(BaseEntity)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package com.amh.entity;

import java.io.Serializable;
import java.util.Date;
import java.util.Objects;
//作为实体类的基类
public class BaseEntity implements Serializable {
private String createdUser;
private Date createdTime;
private String modifiedUser;
private Date modifiedTime;

public String getCreatedUser() {
return createdUser;
}

public void setCreatedUser(String createdUser) {
this.createdUser = createdUser;
}

public Date getCreatedTime() {
return createdTime;
}

public void setCreatedTime(Date createdTime) {
this.createdTime = createdTime;
}

public String getModifiedUser() {
return modifiedUser;
}

public void setModifiedUser(String modifiedUser) {
this.modifiedUser = modifiedUser;
}

public Date getModifiedTime() {
return modifiedTime;
}

public void setModifiedTime(Date modifiedTime) {
this.modifiedTime = modifiedTime;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BaseEntity that = (BaseEntity) o;
return Objects.equals(createdUser, that.createdUser) && Objects.equals(createdTime, that.createdTime) && Objects.equals(modifiedUser, that.modifiedUser) && Objects.equals(modifiedTime, that.modifiedTime);
}

@Override
public int hashCode() {
return Objects.hash(createdUser, createdTime, modifiedUser, modifiedTime);
}

@Override
public String toString() {
return "BaseEntity{" +
"createdUser='" + createdUser + '\'' +
", createdTime=" + createdTime +
", modifiedUser='" + modifiedUser + '\'' +
", modifiedTime=" + modifiedTime +
'}';
}
}

2.创建用户的实体类(User),继承BaseEntity类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
package com.amh.entity;

import java.io.Serializable;
import java.util.Objects;

//用户的实体类:SpringBoot约定大于配置
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;

public Integer getUid() {
return uid;
}

public void setUid(Integer uid) {
this.uid = uid;
}

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public String getSalt() {
return salt;
}

public void setSalt(String salt) {
this.salt = salt;
}

public String getPhone() {
return phone;
}

public void setPhone(String phone) {
this.phone = phone;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}

public Integer getGender() {
return gender;
}

public void setGender(Integer gender) {
this.gender = gender;
}

public String getAvatar() {
return avatar;
}

public void setAvatar(String avatar) {
this.avatar = avatar;
}

public Integer getIsDelete() {
return isDelete;
}

public void setIsDelete(Integer isDelete) {
this.isDelete = isDelete;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
User user = (User) o;
return Objects.equals(uid, user.uid) && Objects.equals(username, user.username) && Objects.equals(password, user.password) && Objects.equals(salt, user.salt) && Objects.equals(phone, user.phone) && Objects.equals(email, user.email) && Objects.equals(gender, user.gender) && Objects.equals(avatar, user.avatar) && Objects.equals(isDelete, user.isDelete);
}

@Override
public int hashCode() {
return Objects.hash(super.hashCode(), uid, username, password, salt, phone, email, gender, avatar, isDelete);
}

@Override
public String toString() {
return "User{" +
"uid=" + uid +
", username='" + username + '\'' +
", password='" + password + '\'' +
", salt='" + salt + '\'' +
", phone='" + phone + '\'' +
", email='" + email + '\'' +
", gender=" + gender +
", avatar='" + avatar + '\'' +
", isDelete=" + isDelete +
'}';
}
}

3、注册-持久层

通过Mybatis来操作数据库,在做,mybatis开发的流程

3.1 规划需要执行的SQL语句

1.用户的注册功能,相当于在做数据的插入操作

1
insert into t_user (username , password) values(值列表)

2.在用户注册时,首先查询数据库是否存在该用户名

1
select username from t_user where username = ?

3.2 设计接口的抽象方法

1.定义Mapper接口,在项目的目录结构下创建一个mapper包,在这个包下在根据不同的功能模块来创建mapper接口,创建一个UserMapper的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.amh.mapper;

import com.amh.entity.User;
import org.apache.ibatis.annotations.Mapper;

//用户模块的持久层的接口
public interface UserMapper {
/**
* 插入用户的数据
* @param user 用户的数据
* @return 受影响的行数
*/
Integer insert(User user);


/**
* 根据用户名查询用户的数据
* @param username 用户名
* @return 如果找到对应名称 返回该用户数据 ,如果没有找到则返回null值
*/
User findByUsername(String username);
}

2.在启动类配置mapper接口文件的位置

1
2
//MapperScan注解 指定当前项目中的Mapper接口路径的位置
@MapperScan("com.amh.mapper")

3.3编写映射

1.定义xml映射文件,在对应的接口进行关联。所有的映射文件需要放置在resources目录下,(为了方便管理)在这个目录下创建一个mapper文件夹,然后在这个文件夹在存放Mapper的映射文件

2.创建接口对应的映射文件,遵循和接口的名称保持一致即可。创建一个UserMapper.xml文件。

1
2
3
4
5
6
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<!--namespace属性:用于指定当前的映射文件和哪个接口进行映射。需要指定接口的文件路径-->
<mapper namespace="com.amh.mapper.UserMapper">

</mapper>

2.配置接口中的方法对应上sql语句上,需要借助标签来完成,insert\update\delete\select,对应的是数据库的增删改查操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<!--namespace属性:用于指定当前的映射文件和哪个接口进行映射。需要指定接口的文件路径-->
<mapper namespace="com.amh.mapper.UserMapper">

<!-- id属性:表示映射的接口中的方法的名称,直接在标签的内容部来编写Sql语句-->
<!--
useGeneratedKeys属性,表示开启某个字段的值递增(主键设置为自增)
keyProperty属性:指定表中的哪个字段作为主键递增
-->
<insert id="insert" useGeneratedKeys="true" keyProperty="uid">
INSERT INTO stores.t_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>


<!--
id属性: 给这个映射负责分配一个唯一的id值,对应的就是 resultMap=“id属性的值” 属性的取值
type属性:取值是一个类,表示数据库中的查询结果与java中的哪个实体类进行结果集映射
-->
<resultMap id="User" type="com.amh.entity.User">
<!--将表中字段和实体类属性不同的字段,进行匹配指定,名称一致的字段可以省略不写-->
<!--
column属性:表示数据库的字段
property:表示实体类的属性
-->
<!-- 在定义映射规则时,主键不可以省略的-->
<id column="uid" property="uid"></id>
<result column="isDelete" property="isDelete"></result>
<result column="createdUser" property="createdUser"></result>
<result column="createdTime" property="createdTime"></result>
<result column="modifiedUser" property="modifiedUser"></result>
<result column="modifiedTime" property="modifiedTime"></result>
</resultMap>

<!-- select语句在执行额时候,查询的结果是多个对象,或一个对象 -->
<!--
resultType:表示查询的结果集类型,只需要指定对应的映射类的类型,(实体类属性和数据库字段一致)
resultMap: 当表的字段和实体类属性不一致时,自定义查询结果集映射规则
-->
<select id="findByUsername" resultMap="User">
SELECT username
from stores.t_user
WHERE username = #{username}
</select>

</mapper>

3.将mapper文件的位置注册到项目的配置文件中。

1
2
mybatis:
mapper-locations: classpath:mapper/*.xml

4.单元测试:每个独立的层编写完毕后需要编写单元测试的方法,来测试当前的功能。在test包结构下创建mapper包,在这个包下再创建持久层的功能测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.amh.mapper;

import com.amh.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的实例类型
* 需要导入junit坐标
*/
@RunWith(SpringRunner.class)
public class UserMapperTests {
//idea 有检测功能,接口不能直接创建Bean的(动态代理技术来解决)
@Autowired(required = false)//解决爆红问题
//可能会爆红,但是不影响操作
private UserMapper userMapper;

/**
* 单元测试方法:就可以独立运行,不需要启动整个项目,可以做单元测试,提升了代码的测试效率
* 1.必须被@Test注解修饰
* 2.返回值类型必须是void
* 3.方法参数列表不指定任何类型
* 4.方法的访问修饰符必须是public
*/

@Test
public void insert(){
User user=new User();
user.setUsername("amh");
user.setPassword("123");

Integer insert = userMapper.insert(user);
System.out.println(insert);
}

@Test
public void findByUsername(){
System.out.println(userMapper.findByUsername("amh"));
}

}

4、注册-业务层

4.1规划异常

1.RuntimeException异常,在用户操作所产生的异常我们可以作为这个异常的子类,然后再去定义具体的异常类型来继承这个异常。创建一个业务层异常的基类(ServiceException),让这个异常去继承RuntimeException异常。异常机制的建立

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.amh.service.ex;

/** 业务层异常的基类:throws new ServiceException("业务层产生未知的异常")*/
public class ServiceException extends RuntimeException{


public ServiceException() {
super();
}

public ServiceException(String message) {
super(message);
}

public ServiceException(String message, Throwable cause) {
super(message, cause);
}

public ServiceException(Throwable cause) {
super(cause);
}

protected ServiceException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}

根据业务层不同的功能来详细定义具体的异常类型,统一去继承ServiceException异常类

2.用户在进行注册时候可能会产生用户名被占用的错误,抛出一个异常:UsernameDuplicatedException异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.amh.service.ex;

//用户名被占用的异常
public class UsernameDuplicatedException extends ServiceException{

public UsernameDuplicatedException() {
super();
}

public UsernameDuplicatedException(String message) {
super(message);
}

public UsernameDuplicatedException(String message, Throwable cause) {
super(message, cause);
}

public UsernameDuplicatedException(Throwable cause) {
super(cause);
}

protected UsernameDuplicatedException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}

3.正在执行数据插入操作时,服务器,数据库宕机。处于正在执行插入的过程中所产生的异常 InsertException异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.amh.service.ex;

public class InsertException extends ServiceException{
public InsertException() {
super();
}

public InsertException(String message) {
super(message);
}

public InsertException(String message, Throwable cause) {
super(message, cause);
}

public InsertException(Throwable cause) {
super(cause);
}

protected InsertException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}

4.2 设计接口和抽象方法

在service包下创建一个IUserService接口

1
2
3
4
5
6
7
8
9
10
11
12
package com.amh.service;

import com.amh.entity.User;

//用户模块业务层接口
public interface IUserService {
/**
* 用户注册方法
* @param user 用户的数据对象
*/
void reg(User user);
}

2.创建一个实现类UserServiceImpl类,需要实现这个接口,并且实现抽象方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
package com.amh.service.impl;

import com.amh.entity.User;
import com.amh.mapper.UserMapper;
import com.amh.service.IUserService;
import com.amh.service.ex.InsertException;
import com.amh.service.ex.UsernameDuplicatedException;
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 // @Service注解:将当前类的对象交给Spring管理,自动创建对象以及对象的维护
public class UserServiceImpl implements IUserService {

@Autowired
private UserMapper userMapper;

@Override
public void reg(User user) {
//调用findByUsername(username) 判断用户是否被注册过
User result = userMapper.findByUsername(user.getUsername());
//判断结果是否 不为null , 则抛出用户名被占用的异常
if (result != null) {
//抛出异常
throw new UsernameDuplicatedException("用户名被占用");
}
//密码的加密处理 md5算法的形式:略
//(串 + password + 串) ------md5算法进行加密 ,连续加载3次
//盐值 + password + 盐值 ------盐值就是一个随机字符串
String oldPassword = user.getPassword();
//获取盐值(随机生成一个盐值)
String salt = UUID.randomUUID().toString().toUpperCase();
//将盐值补全到数据库中(作用于,解密)
user.setSalt(salt);
//将密码和盐值作为一个整体进行加密处理(可以忽略原有密码的强度提升了数据的安全性)
String md5Password = gitMD5Password(oldPassword, salt);
//将加密之后的密码重新补全设置到user对象中
user.setPassword(md5Password);

//补全数据: is_delete设置为0
user.setIsDelete(0);
//补全数据:4个日志字段信息
user.setCreatedUser(user.getUsername());
user.setModifiedUser(user.getUsername());
Date date = new Date();
user.setCreatedTime(date);
user.setModifiedTime(date);
//执行注册业务功能的实现(rows==1)
Integer rows = userMapper.insert(user);
if (rows != 1) {
throw new InsertException("在用户注册时,发生了未知的异常");
}
}

/**
* 定义一个md5算法的加密处理
*
* @param password 用户注册的密码
* @param salt 盐值
* @return
*/
private String gitMD5Password(String password, String salt) {
for (int i = 0; i < 3; i++) {
//md5加密算法方法的调用(进行三次加密)
password = DigestUtils.md5DigestAsHex((salt + password + salt).getBytes()).toUpperCase();
}
//返回加密之后的密码
return password;
}
}

3.在单元测试包下创建一个UserServiceTest类,在这个类中添加单元测试的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package com.amh.service;

import com.amh.entity.User;
import com.amh.mapper.UserMapper;
import com.amh.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的实例类型
* 需要导入junit坐标
*/
@RunWith(SpringRunner.class)
public class UserServiceTest {
//idea 有检测功能,接口不能直接创建Bean的(动态代理技术来解决)
//可能会爆红,但是不影响操作
@Autowired(required = false)//解决爆红问题
private IUserService IUserService;

/**
* 单元测试方法:就可以独立运行,不需要启动整个项目,可以做单元测试,提升了代码的测试效率
* 1.必须被@Test注解修饰
* 2.返回值类型必须是void
* 3.方法参数列表不指定任何类型
* 4.方法的访问修饰符必须是public
*/

@Test
public void reg(){
try {
User user=new User();
user.setUsername("amh");
user.setPassword("123");
IUserService.reg(user);
System.out.println("OK");
} catch (ServiceException e) {
//获取类对象,在获取类对象的名称
System.out.println(e.getClass().getSimpleName());
//获取异常的具体描述信息
System.out.println(e.getMessage());
}
}
}

5、注册-控制层

5.1 创建响应

状态码、状态信息的描述、数据、这部分功能封装一个类中,将这类作为方法的返回值,返回给前端浏览器。

1
2
3
4
5
6
7
8
9
10
11
/**
* Json格式的数据进行响应
*/
public class JsonResult<E> implements Serializable {
/** 状态码*/
private Integer state;
/** 描述信息*/
private String message;
/** E 表示任何的数据类型----数据*/
private E data;
}

5.2 设计请求

依据当前的业务功能模块进行请求的设计。

1
2
3
4
请求路径:/users/reg
请求参数:User user
请求类型:POST
响应结果:JsonResult<void>

5.3 处理请求

1.创建一个控制层对应的类UserController类,依赖于业务层的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.amh.controlle;

import com.amh.entity.User;
import com.amh.service.IUserService;
import com.amh.service.ex.InsertException;
import com.amh.service.ex.UsernameDuplicatedException;
import com.amh.util.JsonResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

//@Controller
@RestController // @Controller + @ResponseBody
@RequestMapping("users")
public class UserController {

@Autowired
private IUserService userService;

@RequestMapping("reg")
// @ResponseBody //表示此方法的响应结果以JSON格式进行数据的响应给到前台
public JsonResult<Void> reg(User user) {
//创建响应结果对象
JsonResult<Void> result = new JsonResult<>();
try {
userService.reg(user);
result.setState(200);
result.setMessage("用户注册成功");
} catch (UsernameDuplicatedException e) {
result.setState(4000);
result.setMessage("用户名被占用");
} catch (InsertException e) {
result.setState(5000);
result.setMessage("注册时产生未知的错误");
}
return result;
}

}

5.4 控制层优化设计

在控制器抽离一个父类,在这个父类中统一的去处理关于异常的相关操作,编写BaseController类,统一处理异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.amh.controlle;

import com.amh.service.ex.InsertException;
import com.amh.service.ex.ServiceException;
import com.amh.service.ex.UsernameDuplicatedException;
import com.amh.util.JsonResult;
import org.springframework.web.bind.annotation.ExceptionHandler;

//控制器类的基类
public class BaseController {
//操作成功的状态码
public static final int OK = 200;

//请求处理方法,这个方法的返回值就是需要传递给前端的数据
//自动将异常对象传递给此方法的参数列表上
// 当项目中产生了异常,被统一拦截到此方法中,这个方法此时就充当的是请求处理方法。返回值直接返回给到前端
@ExceptionHandler(ServiceException.class) //统一处理抛出的异常
public JsonResult<Void> handleException(Throwable e) {
JsonResult<Void> result = new JsonResult<>();
if (e instanceof UsernameDuplicatedException) {
result.setState(4000);
result.setMessage("用户名被占用");
} else if (e instanceof InsertException) {
result.setState(5000);
result.setMessage("注册时产生未知的错误");
}
return result;
}
}

优化后的UserController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package com.amh.controlle;

import com.amh.entity.User;
import com.amh.service.IUserService;
import com.amh.service.ex.InsertException;
import com.amh.service.ex.UsernameDuplicatedException;
import com.amh.util.JsonResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

//@Controller
@RestController // @Controller + @ResponseBody
@RequestMapping("users")
public class UserController extends BaseController {

@Autowired
private IUserService userService;

@RequestMapping("reg")
// @ResponseBody //表示此方法的响应结果以JSON格式进行数据的响应给到前台
public JsonResult<Void> reg(User user) {
userService.reg(user);
return new JsonResult<>(OK);
}

/*
@RequestMapping("reg")
// @ResponseBody //表示此方法的响应结果以JSON格式进行数据的响应给到前台
public JsonResult<Void> reg(User user) {
//创建响应结果对象
JsonResult<Void> result = new JsonResult<>();
try {
userService.reg(user);
result.setState(200);
result.setMessage("用户注册成功");
} catch (UsernameDuplicatedException e) {
result.setState(4000);
result.setMessage("用户名被占用");
} catch (InsertException e) {
result.setState(5000);
result.setMessage("注册时产生未知的错误");
}
return result;
}
*/
}

6、注册-前端页面

1.在register页面中编写发送请求的方法,点击事件来完成,选选中对应的按键($(“选择器”)),再去添加点击的事件,$.ajax()函数发送异步请求。

2.JQuery封装了一个函数,称之为 $.ajax()函数,通过对象调用ajax()函数,可以异步加载相关的请求。

3.ajax()使用方法,需要传递一个方法体作为方法的参数来使用,一对大括号称之为方法体,ajax接收多个参数,参数与参数之间要求使用“ , ”进行分割,每一组参数之间使用“ : ”进行分割,参数的组成部分一个是参数的名称(不能随意的定义),是参数的值,参数的值要求是用字符串来标识,语法结构:

1
2
3
4
5
6
7
8
9
10
11
12
$.ajax({
url: "",
type: "",
data: "",
dataType: "",
success: function(){

},
error: function(){

}
})

4.ajax()函数参数的含义:

参数 功能描述
url 标识请求的地址(url地址),不能包含参数列表部分的内容。例如:url:”localhost:8080/users/reg”
type 请求类型(GET和POST悄悄地类型)。例如:type:”POST”
data 向指定的请求url地址 提交数据。例如:data:”username=amh&password=123456”
dataType 提交的数据的类型,数据类型一般指定为JSON类型。例如:dataType:”json”
success 当服务器正常响应客户端时,会自动调用success参数的方法,并且将服务器返回的数据以参数的形式传递给这个方法的参数上
error 当服务器未正常响应客户端时,会自动调用error参数的方法,并且将服务器返回的数据以参数的形式传递给这个方法的参数上

5.js代码可以独立声明在一个js的文件里或者声明在英国script标签中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<script type="text/javascript">
//1.监听注册按键是否被点击
$("#btn-reg").click(function () {
//2.发送ajax()的异步请求来完成用户的注册功能
$.ajax({
url:"/users/reg",
type:"POST",
//自动检测表单中的控件,并获取它们的值,
//自动拼接成---> username=amh&password=123456
data:$("#form-reg").serialize(),//获取表单数据(并序列化)
dataType:"JSON",
success:function (json) {
if(json.state==200){
alert("注册成功");
}else {
alert("注册失败");
}
},
error:function (xhr){
alert("注册时产生未知的错!"+xhr.status)
}
});
});
</script>

6.当js代码无法正常被服务器解析执行,体现在点击页面中的按钮没有任何的响应,解决方案:

  • 在项目的maven下clear清理项目,install重新部署
  • 在项目的file选项下,清理缓存
  • 重新的去构建项目:build选项下,rebuild选项
  • 重启idea
  • 重启电脑

用户登录

当用户输入用户名和密码将数据提交给后台数据库进行查询,如果存在对应的用户名和密码,则表示登录成功,登录成功之后跳转到系统的主页就是index.html页面,跳转在前端使用jQuery来完成。

1.登录—持久层

1.1 规划需要执行的sql语句

依据用户提交的用户名和密码做select查询。密码的比较在业务层执行。

1
select * from t_user where username = ?

说明:如果在分析过程中发现某个功能模块已经被开发完成,所有就可以省略当前的开发步骤,这个分析过程不能够省略。

1.2 接口设计和方法

不用重复开发,单元测试也是无需单独执行了。

2.登录—业务层

2.1 规划异常

1.用户名对应密码错误。密码匹配失败的异常:PasswordNotMatchException异常,运行时异常,业务异常。

2.用户名没有被找到,抛出异常:UsernameNotFoundException,运行时异常,业务异常。

3.异常的编写

  • 业务层异常需要继承ServiceException异常类
  • 在具体的异常类中定义构造方法(可以使用快捷键来生成,有5个构造方法)。

2.2 设计业务层接口和抽象方法

1.直接在IUseService接口中编写抽象方法,login(Spring username,String password)。将当前登录成功的用户数据以当前用户对象的形式进行返回,状态管理:可以将数据保存在Cookie或者session中,可以避免重复度很高的数据多次频繁操作数据进行获取(=用户名、用户id—存放在session中,用户的头像–Cookie中)。

1
2
3
4
5
6
7
/**
* 用户登录功能
* @param username 用户名
* @param password 密码
* @return 当前匹配的用户数据,如果没有返回null值
*/
User login(String username,String password);

2.需要在实现类中实现父类接口的抽象方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Override
public User login(String username, String password) {
//根据用户名称查询用户数据是否存在,如果不存在抛出异常
User result = userMapper.findByUsername(username);
if(result==null){
throw new UserNotFoundException("用户数据不存在");
}
//检测用户的密码是否匹配
//1.先获取数据库中的加密后的密码
String oldPassword = result.getPassword();
//2.和用户传递过来的密码进行比较
//2.1 先获取盐值:该用户注册时所自动生成的盐值
String salt = result.getSalt();
//2.2 将用户的密码按照相同的md5算法的规则进行加密
String newMd5Password = gitMD5Password(password, salt);
//3.将密码进行比较
if(!newMd5Password.equals(oldPassword)){
throw new PasswordNotMatchException("用户密码错误");
}

//判断is_delete字段的值是否为1(0:未删除,1:已删除)
if(result.getIsDelete()==1){
throw new UserNotFoundException("用户数据不存在");
}

//封装你想要传给前端的数据,提升了系统的性能
User user=new User();
user.setUid(result.getUid());
user.setUsername(result.getUsername());
user.setAvatar(result.getAvatar());

// 将当前的用户数据返回,返回的数据是为了辅助其他页面做数据的展示使用
return user;
}

3.在测试类中测试业务层登录的方法是否可以执行通过。

1
2
3
4
5
6
@Test
public void login(){
User user=IUserService.login("amh","123");
System.out.println(user);

}

3.登录—控制器

3.1 处理异常

业务层抛出的异常是什么,需要在统一异常处理类中统一捕获和处理,如果也曾抛出的异常类型已经在统一异常处理类中曾经处理过,则不需要重复添加。

1
2
3
4
5
6
7
else if (e instanceof UserNotFoundException) {
result.setState(5001);
result.setMessage("用户数据不存在");
}else if (e instanceof PasswordNotMatchException) {
result.setState(5002);
result.setMessage("用户名的密码错误");
}

3.2 设计请求

1
2
3
4
请求路径:/users/login
请求参数:Spring username,Spring password,HttpSession session
请求类型:POST
响应结果:JsonResult<User>

3.3 处理请求

在UserController类中编写请求处理的方法

1
2
3
4
5
@RequestMapping("login")
public JsonResult<User> login(String username,String password){
User data = userService.login(username, password);
return new JsonResult<User>(OK,data);
}

4.登录—前端页面

1.在login.html页面中依据前面所设置的请求来发ajax请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<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("登录成功")
//跳转到系统的主页index.html
//相对路径来确定跳转的页面
location.href="index.html"
}else {
alert("登录失败")
}
},
error: function(xhr){
alert("登录时产生未知的异常"+xhr.message)
}
})
})
</script>

2.访问页面进行用户的登录操作。

用户会话session

session对象主要存储在服务器端,可以用于保存服务器的临时数据的对象,所保存的数据可以在整个项目中都可以通过访问来获取,把session的数据看做一个共享的数据,首次登录的时候所获取的用户的数据,转移到session对象即可。session。getAttrbute(“key”),可以将获取session中的数据这种行为进行封装,封装在BaseControllerl类中。

1.封装session对象中数据的获取(封装父类中),数据的设置(当用户登录成功后进行数据的设置,设置到全局的session对象)。

2.在父类中封装两个数据:获取uid和获取username对应的两个方法,用户头像暂时不考虑,将来封装Cookie中来使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 获取session对象的uid
* @param session session 对象
* @return 当前登录的用户uid的值
*/
protected final Integer getuidFromSession(HttpSession session){
return Integer.valueOf(session.getAttribute("uid").toString());
}

/**
* 获取当前登录用户的username
* @param session session 对象
* @return 当前登录用户的用户名
*
* 在实现类中重写父类的toString(),不是句柄信息的输出
*/
protected final String getUsernameFromSession(HttpSession session){
return session.getAttribute("username").toString();
}

3.在登录的方法中将数据封装在session对象中,服务本身自动创建有session对象,已经是一个全局我的session对象,SpringBoot直接使用session对象,直接将HttpSession类型的对象作为请求处理方法的参数,会自动将全局的session对象注入到请求处理方法的session形参上

1
2
3
4
5
6
7
8
9
10
11
12
13
@RequestMapping("login")
public JsonResult<User> login(String username,String password,HttpSession session){
User data = userService.login(username, password);
session.setAttribute("uid",data.getUid());
session.setAttribute("username",data.getUsername());

//获取session中绑定的数据
System.out.println(getuidFromSession(session));
System.out.println(getUsernameFromSession(session));


return new JsonResult<User>(OK,data);
}

拦截器

拦截器:首先将所有的请求统一拦截到拦截器中,可以在拦截器中定义过滤的规则,如果不满足系统的设置的过滤规则,统一的处理是:重新去打开login.html页面(重定向和转发),推荐使用重定向。

在SpringBoot项目中拦截器的定义和使用。SpringBoot是依靠SpringMVC来完成的。SpringMVC提供了一个HandlerInterceptor接口,用于表示定义一个拦截器,首先自定义个类,在让这个类实现这个接口。

1.首先自定义一个类,在这个类实现这个HandlerInterceptor接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.amh.interceptor;

import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class LoginInterceptor implements HandlerInterceptor {

/**
* 检测全局session对象中是否有uid数据,如果有则放行,如果没有重定向到登录页面
* @param request 请求对象
* @param response 响应对象
* @param handler 处理器(url+Controller:映射)
* @return 如果返回值为true 表示放行当前的请求,如果为false则表示连接当前的请求
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {

//HttpServletRequest对象来获取session对象
Object obj = request.getSession().getAttribute("uid");
if(obj==null){
//说明用户没有登录过系统,则重定向到login.html页面
response.sendRedirect("/web/login.html");
return false;
}
//请求放行
return true;
}
}

2.注册过滤器:添加白名单(哪些资源可以在未登录的情况下访问:login.html\register.html\login\reg\index.html\product.html)、

添加黑名单(在用户登录的状态才可以访问的页面资源)。

3.注册过滤器的技术:借助WebMvcConfigure接口,可以将用户定义的拦截器进行注册,才可以保证拦截器能够生效和使用。定义一个类,然后让这个类是实现WebMvcConfigure接口。配置信息,建议存放在项目的config包的结构下。

1
2
3
//将自定义拦截器进行注册
default void addInterceptors(InterceptorRegistry registry) {
}

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.amh.config;

import com.amh.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 //加载当前的拦截器并进行注册
public class LoginInterceptorConfigurer implements WebMvcConfigurer {
//配置拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
//创建自定义的拦截器对象
HandlerInterceptor interceptor=new LoginInterceptor();

//配置白名单 存放在一个List集合
List<String> patterns=new ArrayList<>();
patterns.add("/bootstrap3/**");
patterns.add("/css/**");
patterns.add("/images/**");
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("/index.html");
patterns.add("/");

//完成拦截器的注册
registry.addInterceptor(interceptor)
.addPathPatterns("/**")
//白名单
.excludePathPatterns(patterns);//表示要拦截的url是什么
}
}

4.提示重定向次数过多,login.html页面无法打开,将浏览器的Cookie请求清除,在将浏览器初始化

源码解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface HandlerInterceptor {
//在调用所有处理请求的方法之前,被自动调用执行的方法
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return true;
}

// 在ModelAndView对象返回之后被调用的方法
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
}

//在整个请求所有关联的资源被执行完毕最后所执行的方法
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
}
}

修改密码

需要用户提交原始密码和新密码,再根据当前登录的用户进行信息的修改操作。

1.修改密码-持久层

1.1 规划需要执行的sql语句

根据用户的uid修改用户password值

1
update t_user set password=?,modified_user=?.modified_time=? where uid=?

根据uid查询用户的数据。在修改密码之前。首先要保证当前这用户的数据存在,检测是否被标记为已删除,检测输入的原始密码是否正确。

1
select * from t_user where uid=?

1.2 设计接口和抽象方法

UserMapper接口,将以上两个方法抽象定义出来,将来映射到sql语句上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 /**
* 根据用户的uid来修改用户密码
* @param uid uid 用户id
* @param password 用户输入的新密码
* @param modifiedUser 表示修改的执行者
* @param modifiedTime 表示修改数据的时间
* @return 返回值为受影响的行数
*/
Integer updatePasswordByUid(Integer uid,
Spring password,
String modifiedUser,
Date modifiedTime);

/**
* 根据用户id查询用户的数据
* @param uid 用户id
* @return 找到返回对象,反之返回null值
*/
User findByUid(Integer uid);

1.3 SQL的映射

配置到映射文件UserMapper.xml中

1
2
3
4
5
6
7
8
9
10
11
<update id="updatePasswordByUid">
update stores.t_user
set password=#{password},
modified_user=#{modifiedUser},
modified_time=#{modifiedTime}
where uid = #{uid}
</update>

<select id="findByUid" resultMap="User">
select * from stores..t_user where uid=#{uid}
</select>

做单元测试功能测试

1
2
3
4
5
6
7
8
9
10
11
@Test
public void updatePasswordByUid(){
Integer i = userMapper.updatePasswordByUid(3, "321", "系统管理员", new Date());
System.out.println(i);
}

@Test
public void findByUid(){
User byUid = userMapper.findByUid(3);
System.out.println(byUid);
}

2. 修改密码—业务层

2.1规划异常

1.用户的原密码错误,is_delete==1、uid找不到,归结在用户没有发现的异常

2.update在更新的时候,可能产生未知的异常,UpdateException。

2.2 设计接口的抽象方法

执行用户修改吗密码的核心方法。

1
2
3
4
void changePassword(Integer uid,
String username,
String olePassword,
String newPassword);

在实现类中实现当前的抽象方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
public void changePassword(Integer uid, String username, String olePassword, String newPassword) {
User result = userMapper.findByUid(uid);
if(result==null || result.getIsDelete()==1){
throw new UserNotFoundException("用户数据不存在");
}
//原始密码和数据库中的密码进行比较
String oldMd5Password = gitMD5Password(olePassword, result.getSalt());
if(!result.getPassword().equals(oldMd5Password)){
throw new PasswordNotMatchException("密码错误");
}
//将新密码设置到数据库中,将新的密码进行加密
String newMd5Password = gitMD5Password(newPassword, result.getSalt());
Integer rows = userMapper.updatePasswordByUid(uid, newMd5Password, username, new Date());
if(rows==1){
throw new UpdateException("更新数据时,产生位置的异常");
}
}

在单元测试类中编写测试方法

1
2
3
4
@Test
public void changePassword(){
IUserService.changePassword(4,"text02","123","321");
}

3. 修改密码-控制层

3.1 处理异常

UpdateException需要配置在统一的异常处理的方法中。

1
2
3
4
else if (e instanceof UpdateException) {
result.setState(5001);
result.setMessage(e.getMessage());
}

3.2 设计请求

1
2
3
4
/users/change_password
post
String oldPassword,String newPassword,HttpSession session //需要和表单中的name属性值保持一致
JsonResult<Void>

3.3 处理请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 更新密码
* @param oldPassword
* @param newPassword
* @param session 获取登录的用户id 和 用户名
* @return
*/
@RequestMapping("change_password")
public JsonResult<Void> changePassword(String oldPassword,
String newPassword,
HttpSession session){
Integer uid = getuidFromSession(session);
String username = getUsernameFromSession(session);
userService.changePassword(uid,username,oldPassword,newPassword);
return new JsonResult<>(OK,"更新成功");
}

4. 修改密码-前端页面

password.html 中添加ajax请求的数据,不在手动去编写ajax结构,直接复制,然后在微调修改参数即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<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(JSON.message);
}else {
alert(JSON.message);
}
},
error: function(xhr){
alert("修改密码时产生未知的异常"+xhr.message)
}
})
})
</script>

个人资料

1. 个人资料-持久层

1.根据用户信息的SQL语句

1
update t_user set phone=?,email=?,gender=?,modified_user=?,modified_time=? where uid=?

2.根据用户名查询用户的数据。

1
select * from t_user where uid=?

查询用户的数据不需要再重复开发。

1.2 接口与抽象方法

更新用户的信息方法的定义。

1
2
3
4
5
6
 /**
* 更新用户的数据信息
* @param user 用户的数据
* @return 返回值为受影响的行数
*/
Integer updateInfoByUid(User user);

1.3 抽象方法的映射

在UserMapper.xml文件中进行映射的编写。

1
2
3
4
5
6
7
8
9
10
11
12
<update id="updateInfoByUid">
update stores.t_user
set
<!--if表示条件判断标签,test接收的是一个返回值为boolean类型的条件
如果test 条件的结果为true 则执行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}
where uid = #{uid}
</update>

在测试类中完成功能测试

1
2
3
4
5
6
7
8
9
@Test
public void updateInfoByUid(){
User user = new User();
user.setUid(4);
user.setPhone("13619284567");
user.setEmail("289028@qq.com");
user.setGender(1);
userMapper.updateInfoByUid(user);
}

2. 个人资料-业务层

2.1 异常规划

1.设计两个功能:

  • 当打开页面是获取用户的信息并且填充到对应的文本框中
  • 检测用户是否点击了修改按钮,如果检测到则执行修改用户信息的操作。

2.打开页面的时候可能找不到用户的数据,点击删除按钮之前需要再次的去检测用户我的数据是否存在。

2.2 接口和抽象的方法

主要有两个功能的模块,对应的是两个抽象的方法的设计。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 根据用户的id 查询用户的数据
* @param uid 用户id
* @return 用户的数据
*/
User getByUid(Integer uid);

/**、
* 更新用户的数据操作
* @param uid 用户的id
* @param username 用户名
* @param user 用户对象的数据
*/
void changeInfo(Integer uid,String username,User user);

2.3 实现抽象方法

在UserSerivceImpl类中添加两个抽象方法的具体实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Override
public User getByUid(Integer uid) {
User result = userMapper.findByUid(uid);
if(result.getIsDelete()==1 || result==null){
throw new UserNotFoundException("用户数据不存在");
}
//只给前端传递需要的数据,减轻传输压力
User user = new User();
user.setUsername(result.getUsername());
user.setPhone(result.getPhone());
user.setEmail(result.getEmail());
user.setGender(result.getGender());
return user;
}

/**
* User对象中的数据 phone\email\gender,手动的将username\uid封装到User对象中
*/
@Override
public void changeInfo(Integer uid, String username, User user) {
User result = userMapper.findByUid(uid);
if(result.getIsDelete()==1 || result==null){
throw new UserNotFoundException("用户数据不存在");
}
user.setUid(uid);
user.setModifiedUser(username);
user.setModifiedTime(new Date());
Integer rows = userMapper.updateInfoByUid(user);
if(rows!=1){
throw new UpdateException("更新数据时,产生位置的异常");
}
}

在测试类中进行功能单元测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void getByUid(){
System.out.println(IUserService.getByUid(4));
}
@Test
public void changeInfo(){
User user=new User();
user.setPhone("13600000000");
user.setEmail("amh@qq.com");
user.setGender(0);
IUserService.changeInfo(4,"管理员",user);
}

3. 个人资料-控制层

3.1 处理异常

暂无

3.2 设计请求

1.设置一打开页面就发送当前用户数据的查询。

1
2
3
4
/users/get_by_uid
GET
HttpSession session
JsonResult<User>

2.点击修改按钮发送用户的数据修改操作请求的设计。

1
2
3
4
/user.change_info
POST
User user,HttpSession session
JsonResult<Void>

3.3处理请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RequestMapping("get_by_uid")
public JsonResult<User> getByUid(HttpSession session){
User data = userService.getByUid(getuidFromSession(session));
return new JsonResult<>(OK,data);
}

@RequestMapping("change_info")
public JsonResult<Void> changeInfo(User user,HttpSession session){
//user对象有四部分数据:username、phone、email,gender
// uid数据需要再次封装到user对象中
Integer uid = getuidFromSession(session);
String username = getUsernameFromSession(session);
userService.changeInfo(uid,username,user);
return new JsonResult<>(OK,"更新成功");
}

4. 个人资料-前端页面

1.在打开userdata.html页面自动发送ajax请求,查询到的数据填充到这个页面。

2.在检测到用户点击了修改按钮之后发送一个ajax请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<script type="text/javascript">
/**
* 一旦检测到当前页面被加载,就会触发ready方法
* $(document).ready(function(){
* //编写代码
* })
*/
$(document).ready(function () {
$.ajax({
url: "/users/get_by_uid",
type: "GET",
data: $("#form-change-info").serialize(),
dataType: "JSON",
success: function(JSON){
if(JSON.state==200){
$("#username").val(JSON.data.username);
$("#phone").val(JSON.data.phone);
$("#email").val(JSON.data.email);
let radio = JSON.data.gender ==0 ?
$("#gender-female") : $("#gender-male")
//prop()表示给每个元素添加属性及属性的值
radio.prop("checked","checked");
}else {
alert(JSON.message);
}
},
error: function(xhr){
alert("查询信息时产生未知的异常"+xhr.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(JSON.message);
//修改成功后重新加载页面
location.href="userdata.html";
}else {
alert(JSON.message);
}
},
error: function(xhr){
alert("更新个人数据时产生未知的异常"+xhr.message)
}
})
})
</script>

上传头像

1.上传头像-持久层

1.1 SQL语句的规划

将对象文件保存在操作系统上,然后在把这个文件路径记录下来,因为在记录路径的是非常便捷和方便的,如果要打开这个文件可以依据这个路径去找到这个文件。在数据库中需要保存这个文件的路径即可。将所有的静态资源(图片、文件、其他资源文件)放到某台电脑上,在把这个电脑作为一台单独的服务器使用。

对应的是一个更新用户avatar字段的sql语句。

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

1.2 设计接口和抽象方法

UserMapper接口中来定义个抽象方法用于修改用户的头像。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 /**
* @Param("SQL映射文件中#{}占位符变量名"):解决问题,大部分sql语句的占位符
* 和映射的接口方法参数名不一致时,需要将某个参数强行注入到某个占位符变量上时,
* 可以使用@Param这个注解来标注映射的关系
*
* 根据用户uid值来修改用户的头像
* @param uid
* @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);

在测试类中进行功能单元测试。

1
2
3
4
5
@Test
public void updateAvatarByUid() {
userMapper.updateAvatarByUid(4, "/upload/avatar.png",
"管理员", new Date());
}

2. 上传头像-业务层

2.1 规划异常

1.用户数据不存在,找不到对应的用户数据

2.更新的时候,各种未知的异常产生。

无需重复开发

2.2 设计接口和抽象方法

编写业务层的更新用户头像的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public void changeAvatar(Integer uid, String avatar, String username) {
User result = userMapper.findByUid(uid);
if(result.getIsDelete()==1 || result==null){
throw new UserNotFoundException("用户数据不存在");
}
Integer rows = userMapper.updateAvatarByUid(uid, avatar,
username, new Date());

if (rows != 1){
throw new UpdateException("更新头像时,产生未知的异常");
}

}

在测试类中进行功能单元测试。

1
2
3
4
@Test
public void changeAvatar() {
IUserService.changeAvatar(4,"/upload/test.png","小明");
}

3.上传头像-控制层

3.1 规划异常

1
2
3
4
5
6
7
8
9
文件异常的父类:
FileUploadException 泛指文件上传异常(父类)继承 RuntimeException

父类是:FileUploadException
FileEmptyException 文件为空的异常
FileSizeException 文件大小超出限制
FileTypeException 文件类型异常
FileUploadIOException 文件读写的异常
FileStateException 文件读写的异常

五个规则方法显示的声明出来,再去继承相关的父类

3.2 处理异常

在基类BaseContoller类中进行编写和统一处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
else if (e instanceof FileEmptyException) {
result.setState(6000);
result.setMessage(e.getMessage());
}else if (e instanceof FileSizeException) {
result.setState(6001);
result.setMessage(e.getMessage());
}else if (e instanceof FileTypeException) {
result.setState(6002);
result.setMessage(e.getMessage());
}else if (e instanceof FileStateException) {
result.setState(6003);
result.setMessage(e.getMessage());
}else if (e instanceof FileUploadIOException) {
result.setState(6004);
result.setMessage(e.getMessage());
}

在异常统一处理的参数列表上增加新的异常处理作为它的参数。

1
@ExceptionHandler({ServiceException.class,FileUploadException.class})

3.3 设计请求

1
2
3
4
/users/change_avatar
POST(get请求提交数据2KB)
HttpSession session, MultipartFile file
JsonResult<String>

3.4 实现请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
/**
* MultipartFile 接口是SpringMVC 提供的一个接口,这个接口为我们包装了
* 获取文件类型的数据(任何类型的file 都可以接收),SpringBoot它有整合了
* SpringMVC,只需要在处理请求的方法参数列表上声明一个参数类型为MultipartFile
* 的参数,然后SpringBoot 自动将文件数据赋值给这个参数
*
* @RequestParam 表示请求中的参数,将请求中的参数注入到请求处理方法的某个参数上,
* 如果名称不一致则可以使用 @RequestParam 注解进行标记和映射
*
* 类似于Mybatis中的 @param 注解
*
* @param session
* @param file
* @return
*/
//设置上传文件的最大值
public static final int AVATAR_MAX_SIZE = 10*1024*1024;

//限制上传文件的类型
public static final List<String> AVATAR_TYPE=new ArrayList<>();

static {
AVATAR_TYPE.add("images/jpeg");
AVATAR_TYPE.add("images/png");
AVATAR_TYPE.add("images/bmp");
AVATAR_TYPE.add("images/gif");
}

@RequestMapping("change_avatar")
public JsonResult<String> changeAvatar(HttpSession session,
@RequestParam("file") MultipartFile file){
//判断文件是否为空
if(file.isEmpty()){
throw new FileEmptyException("文件为空");
}
//判断文件大小
if(file.getSize()>AVATAR_MAX_SIZE){
throw new FileSizeException("文件超出限制");
}
// 判断文件的类型是否是我们规定的后缀类型
String contentType = file.getContentType();
//如果集合包含某个元素则返回true
if(!AVATAR_TYPE.contains(contentType)){
throw new FileTypeException("文件类型不支持");
}
//上传的文件.../upload/文件.png 获取 upload文件夹的真实路径 类似:D:\upload\text.png
String parent = "E:\\电脑商城\\store\\src\\main\\resources\\static\\upload\\";
// File 对象指向这个路径,File 是否存在
File dir = new File(parent);
if(!dir.exists()){ //检测目录是否存在
dir.mkdirs();//创建该目录
}
//获取这个文件的名称,UUID工具来生成一个新的字符串作为文件名
//例如:avatar01.png
String originalFilename = file.getOriginalFilename();
int index =originalFilename.lastIndexOf(".");
String suffix =originalFilename.substring(index);
//ADSHW55-55ESFS-FYSHJS.png
String filename= UUID.randomUUID().toString().toUpperCase()+suffix;

File dest =new File(dir,filename);//是一个空文件
//将file中数据写入到这个空文件中
try {
file.transferTo(dest);//将file文件中的数据写入到dest文件中
}catch (FileStateException e) {
throw new FileStateException("文件状态异常");
} catch (IOException e) {
throw new FileUploadIOException("文件读写异常");
}
Integer uid = getuidFromSession(session);
String username = getUsernameFromSession(session);
//返回头像的路径。upload/test.png
String avatar="/upload"+filename;
userService.changeAvatar(uid,avatar,username);
//返回用户头像的路径给前端页面,将来用于头像展示使用
return new JsonResult<>(OK,avatar);

}

4.上传头像-前端页面

在upload页面中编写上传头像的代码

说明:如果直接使用表单进行文件的上传,需要给表单显示的添加一个属性声明enctype=”multipart/form-data” 出来,不会将目标文件的数据结构做修改在上传,不同字符串

1
2
3
<form method="post"
action="/change_avatar"
enctype="multipart/form-data" class="form-horizontal" role="form">

并且按钮是type=”submit”

1
<input id="btn-change-avatar" type="submit" class="btn btn-primary" value="上传" />

(推荐) 如果使用Ajax来进行文件的上传,则不需要给表单添加以上数据 (推荐)

只需要给 form标签 和 按钮(type=”button”)加id值即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<script type="text/javascript">
$("#btn-change-avatar").click(function () {
$.ajax({
url: "/users/change_avatar",
type: "POST",
data: new FormData($("#form-change-avatar")[0]),
processData: false, //处理数据的形式,关闭处理数据
contentType: false, //提交数据的形式,关闭默认提交数据的形式
dataType: "JSON",
success: function(JSON){
if(JSON.state==200){
alert("头像修改成功");
//将服务端返回的头像地址设置到img标签的src属性上
//attr(属性,属性值):给某个数据设置某个值
$("#img-avatar").attr("src",JSON.data);
//将头像保存在cookie中(更新用户头像路径)
$.cookie("avatar",JSON.data,{expires: 7})
}else {
alert("头像修改失败");
}
},
error: function(xhr){
alert("更新个人数据时产生未知的异常"+xhr.message)
}
})
})
</script>

5. 解决Bug

5.1.更改默认的大小限制

SpringMVC默认为1MB网站可以进行上传,手动的去修改SpringMVC默认上传文件的大小。

方式1:直接可以在配置文件中配置:

1
2
3
4
5
#文件上传大小限制
servlet:
multipart:
max-file-size: 10MB
max-request-size: 15MB

方式2:需要采用java代码的进行来设置文件的上传大小的限制,主类中进行配置,可以定义一个方法,必须使用@Bean修饰符来修饰。在类的前面添加一个@Configration注解进行修饰类。

1
2
3
4
5
6
7
8
9
10
11
12
@Bean
public MultipartConfigElement getMultipartConfigElement(){
//创建一个配置的工厂类对象
MultipartConfigFactory factory = new MultipartConfigFactory();

//设置需要创建的对象的相关信息
factory.setMaxFileSize(DataSize.of(10, DataUnit.MEGABYTES));
factory.setMaxRequestSize(DataSize.of(15, DataUnit.MEGABYTES));

//通过工厂类来创建MultipartConfigElement对象
return factory.createMultipartConfig();
}

5.2 显示头像

在页面中通过ajax请求来提交文件,提交完成后返回json串,解析出data中数据,设置到img头像的标签的src属性上就可以了。

  • serialize():可以将表单中的数据自动拼接成key=value的结果进行提交给服务器,一般提交的是普通的控件类型中的数据(text\password\radio\checkbox)等等

  • FormData类:将表单中数据保持原有的结果进行数据的提交。

    1
    new FormData($("#from")[0]);//文件类型的数据可以使用FormData对象进行存储
  • ajax默认处理数据时按照字符串的形式进行处理,以及默认会采用字符串的形式进行提交数据。关闭这两个默认的功能

    image-20220420201107655

5.3 登录后显示头像

可以更新头像成功后,将服务器返回的头像路径保存到客户端的Cookie对象,然后每次检测到用户打开上传头像页面,在这个页面中通过ready()方法来自动检测去读取Cookie中头像并设置到src属性上。

1.设置cookie中的值:

导入cookie.js文件

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

调用cookie方法:

1
$.cookie(key,value,time) //单位:天

2.在login.html 页面先引入cookie.js文件

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

3.在登录时就在cookie中存入用户自身的avatar 路径

1
2
//将服务器返回头像设置到Cookie中
$.cookie("avatar",JSON.data.avatar,{expires: 7})

4.在updata.html 页面先引入cookie.js文件

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

5.在updata.html 页面通过ready()自动读取cookie中的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
<script type="text/javascript">
/**
* 一旦检测到当前页面被加载,就会触发ready方法
* $(document).ready(function(){
* //编写代码
* })
*/
//当页面被加载时,就把头像显示出来
$(document).ready(function(){
let avatar=$.cookie("avatar");
$("#img-avatar").attr("src",avatar);
});
</script>

5.4 解决上传图片不能显示的问题

spring boot图片上传回显问题—上传后重启项目或者多次刷新才显示图片

原因:因为对服务器的保护措施导致的,服务器不能对外部暴露真实的资源路径,需要配置虚拟路径映射访问。

解决方法:

  • 建立ImagesUploadConfig类,实现WebMvcConfigurer接口。

  • 重写public void addResourceHandlers(ResourceHandlerRegistry registry) 方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    package com.amh.config;

    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

    //处理器拦截器的注册
    @Configuration //加载当前的拦截器并进行注册
    public class ImagesUploadConfig implements WebMvcConfigurer {
    /**
    * 解决 解决SpringBoot图片上传需重启服务器才能显示问题
    * @param registry
    */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
    System.out.println("配置文件已经生效");
    registry.addResourceHandler("/upload/**").addResourceLocations("file:E:/电脑商城/store/src/main/resources/static/upload/");
    }
    }

    注意:

    • 这段代码意思就配置一个拦截器, 如果访问路径是addResourceHandler中的这个路径 那么就 映射到访问本地的addResourceLocations 的参数的这个路径上,这样就可以让别人访问服务器的本地文件了,比如本地图片或者本地音乐视频什么的。

    • addResourceLocations("file:E:/电脑商城/store/src/main/resources/static/upload/")里面的参数必须要写成"file:+路径"的形式,该路径一直到你上传图片的位置

    • 上传图片的位置不可以是static,要在static下重新创建一个文件夹upload来存放你的图片

    • 在浏览器访问图片的时候一定要加上upload,如:http://localhost:8188/upload/123.jpg

新增收获地址

1.新增收货地址-数据表的创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
CREATE TABLE t_address (
aid INT AUTO_INCREMENT COMMENT '收货地址id',
uid INT COMMENT '归属的用户id',
name VARCHAR(20) COMMENT '收货人姓名',
province_name VARCHAR(15) COMMENT '省-名称',
province_code CHAR(6) COMMENT '省-行政代号',
city_name VARCHAR(15) COMMENT '市-名称',
city_code CHAR(6) COMMENT '市-行政代号',
area_name VARCHAR(15) COMMENT '区-名称',
area_code CHAR(6) COMMENT '区-行政代号',
zip CHAR(6) COMMENT '邮政编码',
address VARCHAR(50) COMMENT '详细地址',
phone VARCHAR(20) COMMENT '手机',
tel VARCHAR(20) COMMENT '固话',
tag VARCHAR(6) COMMENT '标签',
is_default INT COMMENT '是否默认:0-不默认,1-默认',
created_user VARCHAR(20) COMMENT '创建人',
created_time DATETIME COMMENT '创建时间',
modified_user VARCHAR(20) COMMENT '修改人',
modified_time DATETIME COMMENT '修改时间',
PRIMARY KEY (aid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

2 新增收货地址-创建实体类

创建com.amh.entity.Address新增收获地址的实体类,继承自BaseEntity类,在类中声明与数据表中对应的属性,添加Getters and Setters方法,基于唯一标识aid生成hashCode()和equals()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.amh.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

/** 收货地址数据的实体类 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Address extends BaseEntity implements Serializable {
private Integer aid;
private Integer uid;
private String name;
private String provinceName;
private String provinceCode;
private String cityName;
private String cityCode;
private String areaName;
private String areaCode;
private String zip;
private String address;
private String phone;
private String tel;
private String tag;
private Integer isDefault;

// Generate: Getter and Setter、Generate hashCode() and equals()、toString()
}

3 新增收货地址-持久层

3.1 各种功能的开发顺序

当前收货地址功能模块:列表的展示、修改、删除、设置默认、新增收获地址。开发顺序:新增收获地址-列表展示-设置默认收获地址-删除收货地址-修改收货地址。

3.2 规划需要执行的SQL语句

1.对应是插入语句

1
insert into t_address (除了aid外字段列表) values (字段值列表)

2.一个用户的收货地址规定最多只能有20条数据对应。在插入用户数据之前先做查询操作。收获地址逻辑控制方面的一个异常。

1
select count(*) t_address where uid=?

3.3 接口与抽象方法

1.创建一个接口AddressMapper,在这个接口中来定义上面两个SQL语句抽象方法定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.amh.mapper;

import com.amh.entity.Address;

/** 收货地址持久层的接口 */
public interface AddressMapper {
/**
* 插入用户的收货地址数据
* @param address 收货地址数据
* @return 受影响行数
*/
Integer insert(Address address);

/**
* 根据用户的id统计收货地址数量
* @param uid 用户的id
* @return 当前用户的收货地址总数
*/
Integer countByUid(Integer uid);
}

3.4 配置SQL映射

  1. 创建一个AddressMapper.xml映射文件,在这个文件中添加抽象方法的映射,映射到的sql语句上。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.amh.mapper.AddressMapper">

<resultMap id="AddressEntityMap" type="com.amh.entity.Address">
<id column="aid" property="aid"/>
<result column="province_code" property="provinceCode"/>
<result column="province_name" property="provinceName"/>
<result column="city_code" property="cityCode"/>
<result column="city_name" property="cityName"/>
<result column="area_code" property="areaCode"/>
<result column="area_name" property="areaName"/>
<result column="is_default" property="isDefault"/>
<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>

<!-- 插入收货地址数据:Integer insert(Address address) -->
<insert id="insert" useGeneratedKeys="true" keyProperty="aid">
INSERT INTO stores.t_address (uid, name, province_name, province_code, city_name, city_code, area_name, area_code, zip,
address, phone, tel, tag, is_default, created_user, created_time, modified_user,
modified_time)
VALUES (#{uid}, #{name}, #{provinceName}, #{provinceCode}, #{cityName}, #{cityCode}, #{areaName},
#{areaCode}, #{zip}, #{address}, #{phone}, #{tel}, #{tag}, #{isDefault}, #{createdUser},
#{createdTime}, #{modifiedUser}, #{modifiedTime})
</insert>

<!-- 统计某用户的收货地址数据的数量:Integer countByUid(Integer uid) -->
<select id="countByUid" resultType="java.lang.Integer">
SELECT COUNT(*)
FROM stores.t_address
WHERE uid = #{uid}
</select>
</mapper>

2.在test下的mapper文件夹下创建AddressMapperTests的测试类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@RunWith(SpringRunner.class)
public class AddressMapperTests {
//可能会爆红,但是不影响操作
@Autowired(required = false)//解决爆红问题
private AddressMapper addressMapper;

@Test
public void insert() {
Address address=new Address();
address.setUid(6);
address.setPhone("12345678911");
address.setName("安");
addressMapper.insert(address);
}

@Test
public void countByUid() {
Integer count = addressMapper.countByUid(6);
System.out.println(count);
}
}

4.新增收货地址-业务层

4.1 规划异常

如果用户是第一次插入用户的收货地址,规则:当前用户插入的地址是第一条时,需要将当前地址作为默认的收货地址。如果查询的总数为0,则将当前地址的is_default值设置为1。查询条件的结果为0不代表异常。

查询到的结果大于20了,这时候需要抛出业务控制的异常AddressCountLimitException异常。自行创建这个异常。

1
2
3
4
//收获地址总数 超出限制的异常(20条)
public class AddressCountLimitException extends ServiceException{
// ...
}

插入数据时产生未知的异常InsertException,不需要再重复的创建。

4.2 接口与抽象方法

1.创建一个IAddressService接口。在中定义业务的抽象方法。

1
2
3
4
5
6
7
8
9
10
package com.amh.service;

import com.amh.entity.Address;

//收获地址业务层接口
public interface IAddressService {

void addNewAddress(Integer uid, String username, Address address);

}

2.创建一个AddressServiceImpl实现类,去实现接口中抽象方法。

在配置文件中定义数据。

1
2
# Spring读取配置文件中数据:@Value("${user.address.max-count}")
user.address.max-count: 20

在实现类中显示业务控制。

4.3 实现抽象方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.amh.service.impl;

import com.amh.entity.Address;
import com.amh.mapper.AddressMapper;
import com.amh.service.IAddressService;
import com.amh.service.ex.AddressCountLimitException;
import com.amh.service.ex.InsertException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;

import java.util.Date;

//新增收货地址的实现类
public class AddressServiceImpl implements IAddressService {
@Autowired
private AddressMapper addressMapper;

@Value("${user.address.max-count}")
private Integer maxCount;

@Override
public void addNewAddress(Integer uid, String username, Address address) {
//调用收货地址统计的方法
Integer count = addressMapper.countByUid(uid);
if (count > maxCount) {
throw new AddressCountLimitException("用户收货地址超出上限");
}
//uid 、isDelete
address.setUid(uid);
Integer isDefault = count == 0 ? 1 : 0;//1表示默认,0表示不是默认
address.setIsDefault(isDefault);
//补全四项日志
address.setCreatedUser(username);
address.setCreatedTime(new Date());
address.setModifiedUser(username);
address.setModifiedTime(new Date());

//插入收货地址的方法
Integer rows = addressMapper.insert(address);
if(rows!=1){
throw new InsertException("插入用户的收货地址产生未知的异常");
}

}
}

3.测试业务层的功能是否正常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RunWith(SpringRunner.class)
public class AddressServiceTest {

@Autowired(required = false)//解决爆红问题
private IAddressService addressService;

@Test
public void addNewAddress() {
Address address=new Address();
address.setPhone("4567898945");
address.setName("梦赫");
addressService.addNewAddress(1,"安梦赫",address);
}
}

5.新增收货地址-控制层

5.1 处理异常

业务层抛出了收货地址总数超标的异常,在BaseController中进行处理。

1
2
3
4
else if (e instanceof AddressCountLimitException) {
result.setState(4003);
result.setMessage(e.getMessage());
}

5.2 设计请求

1
2
3
4
/addresses/add_new_address
post
Address address,HttpSession session
JsonResult<Void>

5.3 处理请求

在控制层创建AddressController来处理用户收货地址的请求和响应。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.amh.controlle;

import com.amh.entity.Address;
import com.amh.service.IAddressService;
import com.amh.util.JsonResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpSession;

@RestController
@RequestMapping("addresses")
public class AddressController extends BaseController{
@Autowired
private IAddressService addressService;

@RequestMapping("add_new_address")
public JsonResult<Void> addNewAddress(Address address, HttpSession session){
Integer uid = getuidFromSession(session);
String username = getUsernameFromSession(session);
addressService.addNewAddress(uid,username,address);
return new JsonResult<>(OK,"添加成功");
}
}

先登录用户,然后在访问 http://localhost:8080/addresses/add_new_address?name=Tom&phone=136192

6 新增收货地址-前端页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<script type="text/javascript">
//1.监听注册按键是否被点击
$("#btn-add-new-address").click(function () {
//2.发送ajax()的异步请求来完成用户的注册功能
$.ajax({
url:"/addresses/add_new_address",
type:"POST",
//自动检测表单中的控件,并获取它们的值,
//自动拼接成---> username=amh&password=123456
data:$("#form-add-new-address").serialize(),//获取表单数据(并序列化)
dataType:"JSON",
success:function (json) {
if(json.state==200){
//获取后端传来的 信息
alert(json.message);
}else {
//获取后端传来的 信息
alert(json.message);
}
},
error:function (xhr){
alert("新增收货地址产生未知的错!"+xhr.status)
}
});
});
</script>

获取省/市/区的列表

1.获取省/市/区的列表-数据表的创建

1
2
3
4
5
6
7
create table `t_dict_district` (
`id` int (11) NOT NULL AUTO_INCREMENT,
`parent` varchar (18) DEFAULT NULL,
`code` varchar (18) DEFAULT NULL,
`name` varchar (48) DEFAULT NULL,
PRIMARY KEY ('id')
); ENGINE=InnoDB DEFAULT CHARSET=utf8;

parent属性表示的是父区域代码号,省的父代码号是+86

2.获取省/市/区的列表-创建实体类

创建一个District的实体类。

1
2
3
4
5
6
7
8
9
10
11
12
/** 省/市/区数据的实体类 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class District implements Serializable {
private Integer id;
private String parent;
private String code;
private String name;

// Generate: Getter and Setter、Generate hashCode() and equals()、toString()
}

3.获取省/市/区的列表-持久层

3.1 规划需要执行的SQL语句

获取全国所有省/某省所有市/某市所有区的查询SQL语句大致是:

查询语句,根据父代号进行查询。

1
select * from t_dict_district where parent=? order by code ASC;

3.2 接口与抽象方法

创建DistrictMapper接口,添加抽象方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.amh.mapper;

import com.amh.entity.District;

import java.util.List;

/** 处理省/市/区数据的持久层接口 */
public interface DistrictMapper {
/**
* 根据用户的父代号查询区域的信息
* @param parent 父级代号,当获取某市所有区时,使用市的代号;当获取省所有市时,使用省的代号;当获取全国所有省时,使用"86"作为父级代号
* @return 某个区域下的所有列表
*/
List<District> findByParent(String parent);
}

3.3 配置SQL映射

1.创建DistrictMapper.xml,修改根节点的namespace属性的值为以上接口文件,并配置以上抽象方法的映射。

1
2
3
4
5
6
7
8
9
10
11
<!-- 获取全国所有省/某省所有市/某市所有区:List<District> findByParent(String parent) -->
<select id="findByParent" resultType="com.amh.entity.District">
SELECT
*
FROM
stores.t_dict_district
WHERE
parent=#{parent}
ORDER BY
code ASC
</select>

2.编写测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//@SpringBootTest:表示当前类是一个测试类,不会随项目一同打包
@SpringBootTest
/**
* @RunWith: 表示启动这个单元测试类(单元测试类是不能运行的),需要传递一个参数,必须是SpringRunner的实例类型
* 需要导入junit坐标
*/
@RunWith(SpringRunner.class)
public class DistrictMapperTests {

@Autowired(required = false)
private DistrictMapper districtMapper;

@Test
public void findByParent(){
String parent = "110100";
List<District> list = districtMapper.findByParent(parent);
for (District district : list) {
System.out.println(district);
}
}
}

4.获取省/市/区的列表-业务层

4.1 规划异常

说明:无异常。

4.2 接口与抽象方法

创建IDistrictService接口,并添加抽象方法。

1
2
3
4
5
6
7
8
public interface IDistrictService {
/**
* 根据用户的父代号查询区域的信息(省市区)
* @param parent 父代码
* @return 多个区域的信息
*/
List<District> getByParent(String parent);
}

4.3 实现抽象方法

1.创建DistrictServiceImpl类,实现IDistrictService接口,在类之前添加@Service注解,以及在类中添加持久层对象并使用@Autowired修饰。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
public class IDistrictServiceImpl implements IDistrictService {
@Autowired
DistrictMapper districtMapper;

@Override
public List<District> getByParent(String parent) {
List<District> list = districtMapper.findByParent(parent);
/**
* 在进行网络数据传输时,为了尽量避免无效数据的传递,可以将无效数据设置null,
* 可以节省流量,另一方面提升效率
* */
for (District district : list) {
district.setId(null);
district.setParent(null);
}
return list;
}
}

2.单元测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//@SpringBootTest:表示当前类是一个测试类,不会随项目一同打包
@SpringBootTest
/**
* @RunWith: 表示启动这个单元测试类(单元测试类是不能运行的),需要传递一个参数,必须是SpringRunner的实例类型
* 需要导入junit坐标
*/
@RunWith(SpringRunner.class)
public class DistrictServiceTests {
//idea 有检测功能,接口不能直接创建Bean的(动态代理技术来解决)
//可能会爆红,但是不影响操作
@Autowired(required = false)//解决爆红问题
private IDistrictService districtService;

@Test
public void getByParent() {

//86 表示中国,所有省份的父代码都是86
String parent = "86";
List<District> list = districtService.getByParent(parent);
System.out.println("count=" + list.size());
for (District item : list) {
System.out.println(item);
}
}
}

5.获取省/市/区的列表-控制层

5.1 处理异常

说明:无异常。

5.2 设计请求

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

请求路径:/districts/
请求参数:String parent
请求类型:GET
响应结果:JsonResult<List<District>>
是否拦截:否,需要在拦截器的配置中添加白名单

5.3 处理请求

创建DistrictController,继承自BaseController类,在类之前添加@RequestMapping(“districts”)和@RestController注解,并在类中添加业务层对象,对其使用@Autowired注解修饰。

在这个类中来编写处理请求的方法

1
2
3
4
5
6
7
@RequestMapping("districts")
@RestController
public class DistrictController extends BaseController {
@Autowired
private IDistrictService districtService;

}

2.在类中添加处理请求的方法getByParent(String parent)及方法的实现。

@GetMapping:是一个组合注解,等价于@RequestMapping(method={RequestMethod.GET}),它将HTTP的GET请求映射到特定的处理方法上。“/”表示方法将处理所有传入的URI请求。简化代码。

1
2
3
4
5
@GetMapping({"", "/"})
public JsonResult<List<District>> getByParent(String parent) {
List<District> data = districtService.getByParent(parent);
return new JsonResult<>(OK, data);
}

3.在拦截器LoginInterceptorConfigurer类的addInterceptors(InterceptorRegistry registry)方法中将“districts”请求添加为白名单。如果已经添加无需重复添加。

1
patterns.add("/districts/**");

4.完成后启动项目,打开浏览器(不需要登录),直接访问http://localhost:8080/districts?parent=86进行测试。

6.获取省/市/区的列表-前端页面

1.在addAddress.html页面中的head标签内导入的distpicker.data.js和distpicker.js文件注释掉。

1
2
3
4
<!--
<script type="text/javascript" src="../js/distpicker.data.js"></script>
<script type="text/javascript" src="../js/distpicker.js"></script>
-->

2.在新增收货地址表单中,给”选择省”控件添加name=”provinceCode”和id=”province-list”属性,给”选择市”添加name=”cityCode”和id=”city-list”属性,给”选择区”控件添加name=”areaCode”和id=”area-list”属性。以上属性如果已经添加无需重复添加。

3.运行前端看是否还可以正常保存数据(除了省市区之外)

获取省市区的名称

1. 获取省市区的名称-持久层

1.获取根据当前code来获取当前省市区的名称,对应就是一条查询语句。

1
select * from t_dist_district where code=?

2.在DistrictMapper接口定义出来。

1
String findNameByCode(String code);

3.在DistrictMapper.xml文件中添加抽象方法的映射。

1
2
3
<select id="findNameByCode" resultType="java.lang.String">
select * from stores.t_dict_district where code=#{code}
</select>

单元测试

1
2
3
4
5
@Test
public void findNameByCode(){
String nameByCode = districtMapper.findNameByCode("610000");
System.out.println(nameByCode);
}

2.获取省市区的名称-业务层

1.在业务层没有异常需要处理。

2.定义对应的业务层接口中的抽象方法。

1
2
3
4
5
6
/**
* 根据省/市/区的行政代号获取省/市/区的名称
* @param code 省/市/区的行政代号
* @return 匹配的省/市/区的名称,如果没有匹配的数据则返回null
*/
String getNameByCode(String code);

3.在子类中实现

1
2
3
4
@Override
public String getNameByCode(String code) {
return districtMapper.findNameByCode(code);
}

4.测试可以省略不写(超过8行以上的代码都要进行独立的测试)。

3.获取省市区的名称-业务层优化

1.添加地址层依赖于IDistrictService层。

1
2
3
//在添加用户的收货地址在业务层依赖于IDistrictService的业务层接口
@Autowired
private IDistrictService districtService;

2.在addNewAddress方法中将districtService接口中获取到的省市区数据转移到address对象,这个对象中就包括了所有的用户收货地址的数据。

1
2
3
4
5
6
7
//对address 对象中数据进行补全:省市区
String provinceName = districtService.getNameByCode(address.getProvinceCode());
String cityName = districtService.getNameByCode(address.getCityCode());
String areaName = districtService.getNameByCode(address.getAreaCode());
address.setProvinceName(provinceName);
address.setCityName(cityName);
address.setAreaName(areaName);

4 获取省市区-前端页面

1.addAddress.html页面中来编写对应的省市区展示,根据用户的不同选择来限制对应的标签中的内容。

2.编写相关的事件代码。

要理解下面代码,必须先了解数据表的数据之间的关系(以下代码并不难)

t_dict_district表

  • parent 父级代号,当获取全国所有省时,使用**”86”**作为父级代号

    1
    SELECT * FROM stores.t_dict_district WHERE parent="86" ORDER BY code ASC

    image-20220424190939970

  • 想要获取省份内(陕西省)的市区—->省的code字段的值 会充当 市区数据的parent字段

    1
    SELECT * FROM stores.t_dict_district WHERE parent="610000" ORDER BY code ASC

    image-20220424191317924

  • 想要获取市区内(西安市)的县区—>市区的code字段的值 会充当 县区数据的parent字段

    1
    SELECT * FROM stores.t_dict_district WHERE parent="610100" ORDER BY code ASC

    image-20220424191852983

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
<script type="text/javascript">
//option只会把value值发送到后端,value属性用于表示当前的这个区域的code值
let defaultOption="<option value='0'>---- 选择区 ----</option>"
$(document).ready(function () {
showProvinceList();
//追加数据(设置默认的“请选择”的值,作为控件的默认值)
$("#city-list").append(defaultOption);
$("#area-list").append(defaultOption);
});
/**
* change() 函数用于监听某个控件是否发生改变,一旦发生改变就会触发参数的函数。
* 需要传递一个function(){}
*/
$("#city-list").change(function () {
//先获取行政区父代码
let parent = $("#city-list").val();
console.log(parent);

//表示的是:清空select下拉列表中所有的option元素
$("#area-list").empty();
// 填充默认值 “请选择”
$("#area-list").append(defaultOption);

if(parent==0){
return;
}
$.ajax({
url:"/districts/",
type:"GET",
data: "parent="+parent,//获取表单数据(并序列化)
dataType:"JSON",
success:function (json) {
if(json.state==200){
let list=json.data;
for (let i=0;i<list.length;i++){
let opt="<option value='"+list[i].code+"'>"+list[i].name+"</option>"
$("#area-list").append(opt);
}
}else {
alert("县区信息加载数据失败");
}
}
});
})


/**
* change() 函数用于监听某个控件是否发生改变,一旦发生改变就会触发参数的函数。
* 需要传递一个function(){}
*/
$("#province-list").change(function () {
//先获取行政区父代码
let parent = $("#province-list").val();
console.log(parent);

//表示的是:清空select下拉列表中所有的option元素
$("#city-list").empty();
$("#area-list").empty();
// 填充默认值 “请选择”
$("#city-list").append(defaultOption);
$("#area-list").append(defaultOption);

if(parent==0){
return;
}
$.ajax({
url:"/districts/",
type:"GET",
data: "parent="+parent,//获取表单数据(并序列化)
dataType:"JSON",
success:function (json) {
if(json.state==200){
let list=json.data;
for (let i=0;i<list.length;i++){
let opt="<option value='"+list[i].code+"'>"+list[i].name+"</option>"
$("#city-list").append(opt);
}
}else {
alert("城市信息加载数据失败");
}
}
});
})

/** 省的下拉列表数据展示*/
function showProvinceList() {
$.ajax({
url:"/districts/",
type:"GET",
data: "parent=86",//获取表单数据(并序列化)
dataType:"JSON",
success:function (json) {
if(json.state==200){
let list=json.data;
for (let i=0;i<list.length;i++){
let opt="<option value='"+list[i].code+"'>"+list[i].name+"</option>"
$("#province-list").append(opt);
}
}else {
alert("省/直辖市信息加载数据失败");
}
}
});
}