用户注册

创建数据库

用户表

image-20230713225827901

创建用户的实体类

实体类的包名规定:com.bang.store.pojo

数据表公共字段抽取

数据库设计过程中,多个业务数据表均存在以下公共字段,则可以将公共字段抽象成一个新的基类com.bang.store.BasePojo,供各个数据库表使用

1
2
3
4
created_user VARCHAR(20) COMMENT '日志-创建人',
created_time DATETIME COMMENT '日志-创建时间',
modified_user VARCHAR(20) COMMENT '日志-最后修改执行人',
modified_time DATETIME COMMENT '日志-最后修改时间',

java基类

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.bang.store.pojo;

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

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

@Data
@NoArgsConstructor
@AllArgsConstructor
public class BasePojo implements Serializable {
private String createdUser;
private Date createdTime;
private String modifiedUser;
private Date modifiedTime;

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

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

补充说明

  • 数据库字段中的日期类型对应于java中的java.util.Date

  • Lombok的注解@Date里面包含了get、set、toString、equals、hashCode等方法

  • Lombokequals好像不能比较属性,所以最好对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
32
33
34
35
36
37
38
39
40
package com.bang.store.pojo;

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

import java.util.Objects;


/**
* 用户表实体类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User extends BasePojo{
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;

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

持久层

数据访问层,主要是通过mybatis操作数据库

需要执行的SQL语句的设计

用户注册,通常需要输入用户名密码这两个字段,相当于数据库的插入过程

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

用户注册时,还应该判断当前用户名是否被注册过了,如果用户名已存在,则无法进行注册,需要用户重新输入用户名,这个过程本质是相当于一条查询语句

1
select * from t_user where username=?;

设计接口和抽象方法

Mapper接口方法

整个项目存在多个Mapper接口,需要在当前包目录结构下创建一个mapper包,在该下根据功能模块创建对应的Mapper接口。

  1. 创建UserMapper接口,实现与3.1中两条SQL语句对应的接口
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.bang.store.mapper;

import com.bang.store.pojo.User;

/**
* 用户模块的Mapper接口
*/
public interface UserMapper {
/**
* 将用户数据插入数据库
* @param user :用户数据
* @return 受影响行数,根据受影响函数返回当前操作是否执行成功
*/
Integer insert(User user);

/**
* 根据用户名查询用户数据
* @param username
* @return 查找到对应用户数据,返回数据,否则返回null值
*/
User findByUsername(String username);
}

  1. 在项目启动类中添加注解@MapperScan,指明Mapper接口路径,避免在每个接口类中都要添加注解@Mapper

    1
    2
    //指明当前项目中mapper接口的路径,项目启动会自动加载对应接口文件
    @MapperScan("com.bang.store.mapper")

编写映射

Mybatis的映射文件xxMapper.xml的编写,与对应的接口相关联

映射文件都需要放置在resources文件夹下,为便于管理,可以新建一个mapper文件夹,在该文件夹下再创建不同功能模块的映射文件,注意各个映射文件的命名要与对应接口的命名保持一致

创建UserMapper.xml文件

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
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">

<!--namespace属性,与当前映射文件对应接口的全类名 -->
<mapper namespace="com.bang.store.mapper.UserMapper">


<!-- 自定义映射规则 -->
<!--
ResultMap:
id属性: 映射规则名称,唯一标识
type属性: 数据库表映射对应的java实体类全类名
-->
<resultMap id="UserPojoMap" type="com.bang.store.pojo.User">
<!--
将表的字段与Java实体类属性字段一一对应,名称匹配的可以省略
id标签:主键映射关系,必须保留
result: 其他字段
column: 数据库字段名
property: 实体类属性名
-->
<id property="uid" column="uid"/>
<result property="idDelete" column="is_delete"/>
<result property="createUser" column="create_user"/>
<result property="createTime" column="create_time"/>
<result property="modifiedUser" column="modified_user"/>
<result property="modifiedTime" column="modified_time"/>
</resultMap>


<!-- id属性值需要与对应接口对应方法名一致 -->
<!-- useGeneratedKeys="true" keyProperty="uid" 开启某个字段作为主键并值自动递增,并指明字段名称 -->
<insert id="insert" useGeneratedKeys="true" keyProperty="uid" parameterType="com.bang.store.pojo.User">
insert into 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>

<!--
resultType="com.bang.store.pojo.User" 指明返回值类型,该类中各个字段应该与数据库各个字段名称保持一致
resultMap: 字段名称不一致时,自定义结果集的映射规则
-->
<select id="findByUsername" parameterType="String" resultMap="UserPojoMap">
select * from t_user where username=#{username};
</select>
</mapper>

Mybatis配置文件

Spring里面会有一个Mybatis Config.xml文件,在配置文件里会指定mapper映射文件的位置,SpringBoot里可以省去这个文件

但是需要在配置文件application.yml里要指明

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

单元测试

每一个部分功能模块编写完成之后。都需要进行专门的功能测试

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.bang.store.mapper;
import com.bang.store.pojo.User;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import javax.annotation.Resource;

@SpringBootTest
@RunWith(SpringRunner.class)
//@MapperScan("com.bang.store.mapper")
public class UserMapperTest {
@Resource
private UserMapper userMapper;
@Test
public void insert(){
User user = new User();
user.setUsername("李白");
user.setPassword("li123456");

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

@Test
public void findByUsernameTest(){
User user = userMapper.findByUsername("张飞");
System.out.println(user);
}
}

业务层

业务层与实际应用场景相挂钩

常见包目录结构

  • service
    • ex:异常类
    • impl:接口文件对应的实现类
    • 接口文件

异常规划

结合用户使用具体场景,设计对应的异常

在实际项目中,异常要进行分级,各个具体问题要建立自定义的异常

比如:业务层的异常需定义一个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.bang.store.service.ex;

/**
* 业务层异常基类,继承自运行时异常
*/
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);
}
}

>
>

业务层根据应用场景定义对应具体的异常类,均需继承于ServivceException

用户在进行注册时可能会出现用户名被占用的情况,此时应抛出一个异常

定义名为UsernameOccupiedException来表明用户名占用错误

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.bang.store.service.ex;

/**
* 用户名被占用异常
*/
public class UsernameOccupiedException extends ServiceException{
public UsernameOccupiedException() {
super();
}

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

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

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

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

用户数据插入过程中,由于服务器宕机、数据库宕机所引发的异常

定义名为insertException来表明由于此类问题所引起的异常

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.bang.store.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);
}
}

接口设计

具体业务逻辑,接口命名建议参考IxxxService

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

import com.bang.store.pojo.User;

public interface IUserService {
/**
* 用户注册方法
* @param user 用户数据对象
*/
void reg(User user);
}

实现类

实现业务接口中的业务方法,命名建议xxxSericeImpl

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
package com.bang.store.service.impl;

import com.bang.store.mapper.UserMapper;
import com.bang.store.pojo.User;
import com.bang.store.service.IUserService;
import com.bang.store.service.ex.InsertException;
import com.bang.store.service.ex.UsernameOccupiedException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;

import javax.annotation.Resource;
import java.util.Date;
import java.util.UUID;

@Service
public class UserServiceImpl implements IUserService {
@Resource
private UserMapper userMapper;
@Override
public void reg(User user) {
//1.判断用户名是否存在
User isOccupied = userMapper.findByUsername(user.getUsername());
//用户名存在,则抛出对应的异常
if(isOccupied!=null){
throw new UsernameOccupiedException("用户名已经被占用");
}
//2.不存在,将用户信息存入数据库
//用户密码加密
//生成盐值,盐值+password+盐值: 送入MD5算法加密三次再存入数据库
//盐值也要存入数据库
String password = user.getPassword();
String salt = UUID.randomUUID().toString().toUpperCase();
password = getMD5password(password,salt);
user.setSalt(salt);
user.setPassword(password);


//用户类其他信息的补全操作
user.setIsDelete(0);
//创建和修改的用户名称
user.setCreatedUser(user.getUsername());
user.setModifiedUser(user.getUsername());
//创建和修改时间
Date date = new Date();
user.setCreatedTime(date);
user.setModifiedTime(date);
Integer rows = userMapper.insert(user);
//数据插入不一定会成功,此时需检测插入是否成功
if(rows!=1){
throw new InsertException("用户注册过程中产生了未知异常");
}
}

public String getMD5password(String password,String salt){
for(int i=0;i<3;i++){
password = DigestUtils.md5DigestAsHex((salt+password+salt).getBytes()).toUpperCase();
}
return password;
}
}

单元测试

创建对应的测试文件

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.bang.store.service;

import com.bang.store.pojo.User;
import com.bang.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
@RunWith(SpringRunner.class)
public class UserServiceTest {
@Autowired
private IUserService iUserService;
@Test
public void reg(){
try{
User user = new User();
user.setUsername("刘备");
user.setPassword("liu123456");
System.out.println(user);
iUserService.reg(user);
System.out.println("注册成功!!!");
}catch (ServiceException e){
System.out.println(e.getClass());
System.out.println(e.getMessage());
}

}
}

补充项

密码明文展示存在安全隐患,利用md5加密算法,对密码进行加密再存储在数据库

常见解决方案: 串+真实password+串 -》MD5算法加密,连续加载三次

串是指:盐值,一个随机的字符串

控制层

创建响应

所有的控制层的响应应该都包含:状态码、状态描述信息、数据

所以将其分装成一个类,将这个类作为返回值返回给前端浏览器

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.bang.store.utils;

import lombok.Data;

import java.io.Serializable;

@Data
public class JsonResult<E> implements Serializable {
//状态码
private Integer state;
//状态描述信息
private String message;
//请求返回数据,数据类型位置,泛型
private E data;

public JsonResult() {
}

public JsonResult(Integer state) {
this.state = state;
}
//捕获异常,异常信息的传递
public JsonResult(Throwable e){
this.message=e.getMessage();
}

public JsonResult(Integer state, E data) {
this.state = state;
this.data = data;
}
}

设计请求

依据具体的业务设计对应的请求,常见请求应该包含以下四个主题:请求地址、请求参数、请求方法、请求响应

比如针对用户注册功能

1
2
3
4
请求地址: /user/reg
请求参数: User user
请求方法:Post
请求响应:JsonResult<void>

处理请求

在控制层创建对应的实现类,借助业务层的相关接口方法,对用户的请求做出对应逻辑的处理

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
package com.bang.store.controller;


import com.bang.store.pojo.User;
import com.bang.store.service.IUserService;
import com.bang.store.service.ex.InsertException;
import com.bang.store.service.ex.UsernameOccupiedException;
import com.bang.store.utils.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 {
@Autowired
private IUserService userService;

@RequestMapping("/reg")
public JsonResult<Void> reg(User user){
JsonResult<Void> result = new JsonResult<>();
try{
userService.reg(user);
result.setState(200);
result.setMessage("用户注册成功");
}catch (UsernameOccupiedException e){
result.setState(4000);
result.setMessage("用户名被占用");
}catch (InsertException e){
result.setState(5000);
result.setMessage("注册时未知异常");
}
return result;
}

}

控制层代码优化设计

观察5.3中控制层请求处理代码逻辑,发现异常捕获部分存在较大冗余,对于业务层而言,其代码确定之后,异常类型也就可以确定,所以控制层可能捕获的异常也就可能确定,可以将异常捕获处理这部分代码剥离出来,构建一个基础类,让控制层处理请求类继承至该类

这么设计的可行性:

  • Spring提供了ExceptionHandle注解,该注解可以当某种类型异常发生之后,会统一拦截到对应方法上进行处理

抽象出的基类

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.bang.store.controller;

import com.bang.store.service.ex.InsertException;
import com.bang.store.service.ex.ServiceException;
import com.bang.store.service.ex.UsernameOccupiedException;
import com.bang.store.utils.JsonResult;
import org.springframework.web.bind.annotation.ExceptionHandler;

public class BaseController {
public static final Integer OK=200;

//@ExceptionHandle注解: 项目中如果产生指定类型的异常,会统一拦截到此方法上进行处理
// 参数:对应拦截的异常类类型
@ExceptionHandler(ServiceException.class)
public JsonResult<Void> handleException(Throwable e){
JsonResult<Void> result = new JsonResult<>();
if(e instanceof UsernameOccupiedException){
result.setState(4000);
result.setMessage("用户名被占用");
} else if (e instanceof InsertException) {
result.setState(5000);
result.setMessage("注册时未知异常");
}
return result;
}
}

控制层注册逻辑代码简化版本

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


import com.bang.store.pojo.User;
import com.bang.store.service.IUserService;
import com.bang.store.service.ex.InsertException;
import com.bang.store.service.ex.UsernameOccupiedException;
import com.bang.store.utils.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{
@Autowired
private IUserService userService;
@RequestMapping("/reg")
public JsonResult<Void> reg(User user){
userService.reg(user);
return new JsonResult<>(OK,"用户注册成功");
}

}

前端页面的开发

  1. 在注册页面编写发送请求的方法,利用事件来完成。首先根据选择器的到对应的按钮对象(比如$(选择器)),对按钮对象绑定对应的点击事件

    image-20230718235451253

  2. 利用异步请求(ajax技术),对后端程序发送对应的请求

    ajax的使用方法,ajax函数会接受多个参数,参数类似于键值对的形式,用,分割

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

    }
    ,error:function(){

    }
    });
参数 参数功能
url 标识请求的地址,不包含参数列表 ,比如:url:http://localhost:8080/user/reg
type 请求方法类型(GET、POST) ,比如: type:”POST”
data 请求发送的数据,例如: data:”username=张飞&password=123456”
dataType 请求传送数据数据类型,一般为json类型,比如: dataType:”json”
success 服务器正常响应客户端时,会自动调用该参数对应的方法,并将服务器返回值自动装填到方法参数上
error 服务器不能够正常响应客户端时,会自动调用该参数对应的方法,并将服务器返回值自动装填到方法参数上
  1. 前端核心代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <script>
    //1.获取对应按钮对象,绑定点击事件
    $("#btn-reg").click(function () {
    //2.点击之后发送ajax异步请求
    $.ajax({
    url:"/user/reg",
    type:"POST",
    //动态获取对应表单数据,会自动拼接成:表单name1=val1&表单name2=val2&...
    data:$("#form-reg").serialize(),
    dataType:"json",
    //服务器返回数据
    success:function (data) {
    alert(data.message)
    },
    //xhr对象,status对应的状态码
    error:function (xhr) {
    alert("注册时产生未知错误"+ xhr.status)
    }
    });
    });
    </script>