一、业务功能实现过程中涉及到的包和目录及主要功能说明
二、注册功能实现
1.参数要求
参数名 | 描述 | 类型 | 默认值 | 条件 |
---|---|---|---|---|
username | 用户名 | String | 必须 | |
nickname | 昵称 | String | 与用户名相同 | 必须 |
password | 密码 | String | 必须 | |
passwordRepeat | 确认密码 | String | 必须,与密码相同 |
前三个是与数据库交互的过程中关注的参数,最后一个是在Controller层对密码与确认密码进行校验,不通过直接返回错误信息。
2.接口规范
#请求接口
请求方式:POST
请求路径:/user/register
版本:HTTP/1.1
Content-Type:application/x-www-form-urlencoded
请求参数格式
username=user111&nickname=user111&password=123456&passwordRepeat=123456
#响应接口
HTTP/1.1 200
Content-Type:application/json
{
"code":0,
"message":"成功",
"data":null
}
3.后端代码逻辑实现
(1)定义SQL,按用户名查询用户信息
之前我们通过MyBatis已经自动生成sql的一些语句,但是呢我去查看里面没有我想要的sql,所以此时我们就需要去自定义一个,这里要注意的是自定义的要单独写出来,然后去引用相应的调用代码即可,因为不自定义的话可能会造成对原来的覆盖。
在src/main/resources/mapper目录下创建extension目录来存放我们业务的SQL。
在extension目录下新建UserExtMapper.xml文件。
检查之前的yml配置文件中是不是有遍历mapper下的所有xml文件
然后回到我们的UserExtMapper.xml文件中,为了能够被映射到,我们直接复制自动生成的任意一个xml文件中的开头部分。
<?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.xiang.forum.dao.ArticleMapper"></mapper>
注意事项
- 注意namespace表示命名空间,指定要与UserMapper,xml中的namespace相同
- 统一用com.xiang.forum.dao.UserMapper,也就是UserMapper的完全限定名(包名+类名)
- 不同的映射文件指定了相同的namespace后,定义的所有用id或者name标识的结果集映射都可以不同文件中共享
开始编写具体查询,根据用户名查询用户信息
(2)Dao中对应类,添加SQL中定义的方法
UserMapper类中添加以下代码。其中@Param代表的是SQL传入的参数名,String username代表的是Java代码中使用的变量名。
User selectByUserName (@Param("username") String username);
(3)进行Service中业务方法的编写
在com.xiang.forum.service包下创建IUserService接口(通常接口以I开头)
实现Service接口
在com.xiang.forum.service.impl包下创建UserServiceImpl类,并实现IUserService接口
非空校验
打印日志及抛出异常
log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
// 抛出异常,统一抛出ApplicationException
throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
根据用户名查询用户信息
首先看用户是否存在
然后因为还有密码是需要进行加盐处理的,所以在这里也需要去判断以下盐值是否为空
新增用户流程,设置数据库中默认值
// 3.新增用户流程,设置数据库中默认值
user.setGender((byte) 2);
user.setArticleCount(0);
user.setIsAdmin((byte) 0);
user.setState((byte) 0);
user.setDeleteState((byte) 0);
// 当前日期
Date date = new Date();
user.setCreateTime(date);
user.setUpdateTime(date);
同时在User类中添加获取文章数量的方法
然后将用户注册信息写入数据库
// 4.将用户信息写入数据库
int row = userMapper.insertSelective(user);
if(row != 1) {
// 打印日志
log.info(ResultCode.FAILED_CREATE.toString());
// 抛出异常
throw new ApplicationException(AppResult.failed(ResultCode.FAILED_CREATE));
}
// 打印日志
log.info("新用户添加成功:username = " + user.getUsername());
(4)进行单元测试
快速生成单元测试,在要生成的类中右击,然后Generate——》Test——》Junit5选定好就可以生成了。
package com.xiang.forum.service.impl;
import com.xiang.forum.model.User;
import com.xiang.forum.service.IUserService;
import com.xiang.forum.uitls.MD5Util;
import com.xiang.forum.uitls.UUIDUtil;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
@SpringBootTest
class UserServiceImplTest {
@Resource
private IUserService userService;
@Test
void createNormalUser() {
// 构造User对象
User user = new User();
user.setUsername("小哈");
user.setNickname("小哈");
// 定义一个原始密码
String password = "123456";
// 生成盐
String salt = UUIDUtil.UUID_32();
// 生成密码的密文
String ciphertext = MD5Util.md5Salt(password,salt);
// 设置加密后的密码
user.setPassword(ciphertext);
// 设置盐
user.setSalt(salt);
// 调用service层方法(通过Resource注入UserServiceImpl userService)
userService.createNormalUser(user);
// 打印结果
System.out.println(user);
}
}
这里的话主要是我数据库那边出问题了,就是电话号码和邮箱的默认值设置的问题,所以这里我需要去UserServiceImpl设置默认的值。
插入相同的信息也是提示用户名已经存在了
(5)编写Controller中的代码
在com.xinag.forum.controller包下创建UserController类
添加相应的注解信息,这里我们使用了API注解,以下是对API常用注解的说明
- @Api:作用在Controller上,对控制器类的说明。tags=“说明该类的作用,可以在前台界面上看到的注解”
- @ApiModel:作用在响应的类上,对返回响应数据的说明
- @ApiModelProerty:作用在类的属性上,对属性的说明
- @ApiOperation:作用在具体方法上,对API接口的说明
- @ApiParam:作用在方法中的每一个参数上,对参数的属性进行说明
写好这些注解后,我们开始实际的方法逻辑实现,我们在前面知道Controller层是用来接收用户请求,包括参数,并对传来的参数进行校验,同时准备数据结果,针对传来的值得结果进行响应。
@ApiOperation("用户注册")
@PostMapping("/register")
public AppResult register(String username,String nickname,String password,String passwordRepeat) {
if(StringUtil.isEmpty(username) ||
StringUtil.isEmpty(nickname) ||
StringUtil.isEmpty(password) ||
StringUtil.isEmpty(passwordRepeat)) {
// 返回错误信息
return AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE);
}
return null;
}
这个是传统的校验方式,能够对代码每个参数进行校验,如果不通过,就给前端返回错误的响应信息。我们可以换用API注解得方式进行校验,通过对每一个参数进行注解,完成对应的功能。下面是改写后的。
public AppResult register(@ApiParam("用户名") @RequestParam("username") @NonNull String username,
@ApiParam("昵称") @RequestParam("nickname") @NonNull String nickname,
@ApiParam("密码") @RequestParam("password") @NonNull String password,
@ApiParam("确认密码") @RequestParam("passwordRepeat") @NonNull String passwordRepeat) {
return null;
}
参数说明
然后去校验输入密码和确认密码的密码是否一致
// 校验两次密码是否一致
if(!password.equals(passwordRepeat)) {
log.warn(ResultCode.FAILED_TWO_PWD_NOT_SAME.toString());
// 返回错误信息
return AppResult.failed(ResultCode.FAILED_TWO_PWD_NOT_SAME);
}
写完我们的注册方法验证后,就可以开始准备数据,从Service层中注入进来操作。
@Resource
private IUserService userService;
// 准备数据
User user = new User();
user.setUsername(username);
user.setNickname(nickname);
// 处理密码
// 1.生成盐
String salt = UUIDUtil.UUID_32();
// 2.生成密码的密文
String encryptPassword = MD5Util.md5Salt(password,salt);
// 3.设置密码和盐
user.setPassword(encryptPassword);
user.setSalt(salt);
// 调用Service层
userService.createNormalUser(user);
// 返回成功
return AppResult.success();
(6)测试Controller中对外提供的API
这里的话建议使用swagger-ui进行测试,测试通过之后就可以把接口提供出去,给使用者来调用。启动程序,打开浏览器输入swagger-ui访问地址进行访问
先查看一下我们的数据库有哪些数据
然后我们可以先校验一下密码重复情况
可以看到是可以校验的,然后在去测一下用户名重复的情况。
至此完成对密码一致,用户名重复的校验。我们插入新的数据进行查看。
可以看到是插入成功了的,并且密码也进行了加密。然后就可以去进行前端操作,让前端去调用接口。
(7)前端涉及的技术
- HTML、CSS、JavaScript
- jQuery
对原生JS进行封装,主要使用AJAX,DOM元素的操作相关的方法 - Bootstrap
定义了HTML元素的样式及页面布局 - Tabler
对Bootstarp美化后的元素进行排版和组合,形成一个个可以直接使用的组件,比如表单,列表,图片墙等 - 图标
(8)前端操作
首先将前端涉及的所有页面放在项目的static目录下
启动程序,去到浏览器访问
打开注册页面如下:
要做的事,对每个注册控件进行校验
$(function () {
// 获取表单并校验
$('#submit').click(function () {
let checkForm = true;
// 校验用户名
if (!$('#username').val()) {
$('#username').addClass('is-invalid');
checkForm = false;
}
// 校验昵称
if (!$('#nickname').val()) {
$('#nickname').addClass('is-invalid');
checkForm = false;
}
// 校验密码非空
if (!$('#password').val()) {
$('#password').addClass('is-invalid');
checkForm = false;
}
// 校验确认密码非空, 校验密码与重复密码是否相同
if (!$('#passwordRepeat').val() || $('#password').val() != $('#passwordRepeat').val()) {
$('#passwordRepeat').addClass('is-invalid');
checkForm = false;
}
// 检验政策是否勾选
if (!$('#policy').prop('checked')) {
$('#policy').addClass('is-invalid');
checkForm = false;
}
// 根据判断结果提交表单
if (!checkForm) {
return false;
}
// 构造数据
// 发送AJAX请求
// contentType = application/x-www-form-urlencoded
// 成功后跳转到 sign-in.html
$.ajax ({
});
});
// 表单元单独检验
$('#username, #nickname, #password').on('blur', function () {
if ($(this).val()) {
$(this).removeClass('is-invalid');
$(this).addClass('is-valid');
} else {
$(this).removeClass('is-valid');
$(this).addClass('is-invalid');
}
})
// 检验确认密码
$('#passwordRepeat').on('blur', function () {
if ($(this).val() && $(this).val() == $('#password').val()) {
$(this).removeClass('is-invalid');
$(this).addClass('is-valid');
} else {
$(this).removeClass('is-valid');
$(this).addClass('is-invalid');
}
})
// 校验政策是否勾选
$('#policy').on('change', function () {
if ($(this).prop('checked')) {
$(this).removeClass('is-invalid');
$(this).addClass('is-valid');
} else {
$(this).removeClass('is-valid');
$(this).addClass('is-invalid');
}
})
// 密码框右侧明文密文切换按钮
$('#passwordRepeat_a').click(function () {
if($('#passwordRepeat').attr('type') == 'password') {
$('#passwordRepeat').attr('type', 'text');
} else {
$('#passwordRepeat').attr('type', 'password');
}
});
$('#password_a').click(function () {
if($('#password').attr('type') == 'password') {
$('#password').attr('type', 'text');
} else {
$('#password').attr('type', 'password');
}
});
});
校验结果如下
然后就可以向后端发送数据啦,首先构造出要发送的数据,左边是传给后端的,尽量与数据库名字一致,右边是用户填入的值
// 构造数据
let postData = {
username:$('#username').val(),
nickname:$('#nickname').val(),
password:$('#password').val(),
passwordRepeat:$('#passwordRepeat').val()
}
构造好数据后,通过ajax发送数据给后端,进行校验,这里要注意的是我们在返回结果时候,可以使用https://kamranahmed.info/toast这个插件返回一些信息
$.ajax ({
type:'post',
url:'user/register',
// 表单提交方式,form表单
contentType:'application/x-www-form-urlencoded',
data:postData,
// 成功回调方法
success:function(respData) {
// 判断返回的状态码
if(respData.code == 0) {
// 利用https://kamranahmed.info/toast这个插件返回一些信息
// 提示信息
$.toast({
heading: 'Success',
text: '恭喜你注册成功,请登录!',
icon: 'success'
});
// 注册成功,跳转到登录页面
location.assign('/sign-in.html')
} else {
// 提示信息也是插件
$.toast({
heading: 'Warrning',
text: respData.message,
icon: 'warning'
});
}
},
error:function() {
$.toast({
heading: 'Error',
text: '访问出错,请与管理员联系',
icon: 'error'
});
}
});
});
注册成功后跳转到登陆页面,我们可以通过数据库去查询,是否注册成功
其次是我们返回的校验失败,利用插件提示的信息
三、登录功能实现
1.参数要求
登录时用户需要提交的参数列表,前端进行非空校验
参数名 | 描述 | 类型 | 默认值 | 条件 |
---|---|---|---|---|
username | 用户名 | String | 必须 | |
password | 密码 | String | 必须 |
2.接口规范
#请求接口
请求方式:POST
请求路径:/user/login
版本:HTTP/1.1
Content-Type:application/x-www-form-urlencoded
请求参数格式
username=user111&password=123456
#响应接口
HTTP/1.1 200
Content-Type:application/json
{
"code":0,
"message":"成功",
"data":null
}
3.后端代码实现
(1)在Mapper.xml中编写SQL语句
(2)在Mapper.java中定义方法
根据前端传过来的数据进行查询用户信息,(1)(2)已将在前面的注册部分已将实现了,所以我们可以直接去实现service接口
(3)定义Service接口
这里定义一个登录的接口 login(username,password)
在IUserService里面创建用户接口,根据用户名查询用户信息
会发现会报上面的错误,没关系,我们去重写此方法即可,接着再继续写登陆的接口,会发现也是会出现上述的提示,我们过去重写即可啦
@Override
public User selectByUserName(String username) {
return null;
}
@Override
public User login(String username, String password) {
return null;
}
然后我们就可以开始在这两个方法中编写我们具体的后端代码实现啦,我们可以通过swagger查看接口存不存在
(4)实现Service接口
首先对于根据用户名查询用户信息,我们也是要进行一个非空校验,打印一下我们的后台日志,然后抛出异常信息,最后返回查询的结果
@Override
public User selectByUserName(String username) {
// 1.非空校验
if(StringUtil.isEmpty(username)) {
// 2.打印日志,我们都打印参数校验失败这个参数
log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
// 3.抛出异常
throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
}
// 4.将我们的结果返回
return userMapper.selectByUserName(username);
}
然后校验用户登录,也是一样的原理,就是要注意的是我们的密码是经过加盐然后进行md5的。所以我们要对拿到的密码进行加密候再去与数据库中存在的用户密码进行比较,如果是一致的登陆成功,不一致,一样的打印对应的日志信息,抛出异常。具体可以查看代码中的注解如下。
@Override
public User login(String username, String password) {
// 1.非空校验
if(StringUtil.isEmpty(username) || StringUtil.isEmpty(password)) {
// 2.打印日志,我们都打印用户名或密码错误这个参数信息
log.warn(ResultCode.FAILED_LOGIN.toString());
// 3.抛出错误异常
throw new ApplicationException(AppResult.failed(ResultCode.FAILED_LOGIN));
}
// 4.按用户名查询用户信息
User user = selectByUserName(username);
// 5.对查询结果进行非空校验
if(user == null) {
// 打印日志信息
log.warn(ResultCode.FAILED_LOGIN.toString() + ", username = " + username);
// 抛出异常
throw new ApplicationException(AppResult.failed(ResultCode.FAILED_LOGIN));
}
// 6.对密码进行校验,给用户输入的密码加盐
String encryptPassword = MD5Util.md5Salt(password,user.getSalt());
// 7.用密文和数据库中的用户密码进行校验
if(!encryptPassword.equalsIgnoreCase(user.getPassword())) {
// 打印日志
log.warn(ResultCode.FAILED_LOGIN.toString() + ", 密码错误,username = " + username);
// 抛出异常
throw new ApplicationException(AppResult.failed(ResultCode.FAILED_LOGIN));
}
// 8.最后登录成功,返回用户信息
return user;
}
(5)单元测试
出现弹窗Error没关系,点击🆗就行
可以看到在测试接口类中已将生成了
然后进入到数据库中查看已有哪些数据
@Test
void selectByUserName() {
User user = userService.selectByUserName("haha");
System.out.println(user);
}
测试结果如下,因为没有数据所以返回空值
@Test
void selectByUserName() {
User user = userService.selectByUserName("小哈");
System.out.println(user);
}
测试结果如下,返回了该条记录
@Test
void login() {
User user = userService.login("xiaoha","123456");
System.out.println(user);
}
测试结果如下,发现抛出异常了,不存在该用户
@Test
void login() {
User user = userService.login("小哈","123456");
System.out.println(user);
}
测试结果如下,可以看到是能够查询到的,登录成功
我们可以去修改一下,打印一下日志输出登陆成功
// 8.最后登录成功,返回用户信息
log.warn(username + "登陆成功!");
新增点,就是如果我们在测试方法中,不想去污染原本的数据库,那么我们可以在测试方法中添加一个事务注解,然后当我们测试操作数据库的时候,就会进行回滚,不会影响原有的数据库
(6)Controller实现方法并对外提供API接口
service业务逻辑已经实现完成了,我们就可以去到UserController中去实现登录的方法
给参数注释信息,其次不要忘记给上不为空的注解
这部分要注意的就是我们需要将登录的用户信息存储在session中,所以我们需要在加一个请求参数request,通过在配置类新建一个全局配置类AppConfig,设置USER_SESSION,这样就可以在存储的时候将对象存储给它,最后将登陆成功的结果进行返回即可
// 步骤
// 1.调用Service层中的登录方法,返回User对象
User user = userService.login(username, password);
if(user == null) {
log.warn(ResultCode.FAILED_LOGIN.toString());
return AppResult.failed(ResultCode.FAILED_LOGIN);
}
// 2.如果登陆成功,我们需要把User对象信息存储到Serssion作用域中,所以这里还需要一个变量,request
HttpSession session = request.getSession(true);
// 这里我们可以去到全局配置中将我们的session设置,新建一个配置类
session.setAttribute(AppConfig.USER_SESSION,user);
// 3.将登录成功的结果返回
return AppResult.success();
(7)测试API接口
写好登陆方法后,我们就可以去测试一下看能不能成功。运行程序,同样的打开swagger查看调用接口的返回,也可以使用postman进行验证。
至此后端登录逻辑完成,现在可以开始引入前端页面进行操作了
(8)前端代码实现,完成前后端交互
首先将前端页面相关的进行引入,然后给需要传递的参数进行非空校验,通过ajax进行参数传给后端
前端对表单验证
$(function () {
// 获取控件
// 用户名
let usernameEl = $('#username');
let passwordEl = $('#password');
// 登录校验
$('#submit').click(function () {
let checkForm = true;
// 校验用户名
if (!usernameEl.val()) {
usernameEl.addClass('is-invalid');
checkForm = false;
}
// 校验密码
if (!passwordEl.val()) {
passwordEl.addClass('is-invalid');
checkForm = false;
}
// 根据判断结果提交表单
if (!checkForm) {
return false;
}
//
// 表单元单独检验
$('#username, #password').on('blur', function () {
if ($(this).val()) {
$(this).removeClass('is-invalid');
$(this).addClass('is-valid');
} else {
$(this).removeClass('is-valid');
$(this).addClass('is-invalid');
}
});
// 显示密码
$('#password_a').click(function () {
if(passwordEl.attr('type') == 'password') {
passwordEl.attr('type', 'text');
} else {
passwordEl.attr('type', 'password');
}
});
});
构造数据
// 构造数据
let postData = {
username:usernameEl.val(),
password:passwordEl.val()
};
发送ajax请求,成功后跳转到首页,这里与注册都是一样的逻辑
// 发送AJAX请求,成功后跳转到index.html
$.ajax({
type:'post',
url:'user/login',
contentType:'application/x-www-form-urlencoded',
data:postData,
success:function(respData){
if(respData.code == 0) {
$.toast({
heading: 'Success',
text: '恭喜你成功登录!',
icon: 'success'
});
location.assign('/index.html');
}else {
// 提示信息
$.toast({
heading: 'Warning',
text: respData.message,
icon: 'warning'
});
}
},
error:function() {
// 提示信息
$.toast({
heading: 'Error',
text: '访问失败!',
icon: 'error'
});
}
});
});
至此就完成了前端的传递数据功能,最后的结果如下