软件开发
编写公共代码
定义状态码
对执⾏业务处理逻辑过程中可能出现的成功与失败状态做针对性描述(根据需求分析阶段可以遇见的问题提前做出定义),⽤枚举定义状态码,先定义⼀部分,业务中遇到新的问题再添加
定义状态码如下
状态码 | 类型 | 描述 |
0 | SUCCESS | 操作成功 |
1000 | FAILED | 操作失败 |
1001 | FAILED_UNAUTHORIZED | 未授权 |
1002 | FAILED_PARAMS_VALIDATE | 参数校验失败 |
1003 | FAILED_FORBIDDEN | 禁⽌访问 |
1004 | FAILED_CREATE | 新增失败 |
1005 | FAILED_NOT_EXISTS | 资源不存在 |
1101 | FAILED_USER_EXISTS | ⽤⼾已存在 |
1102 | FAILED_USER_NOT_EXISTS | ⽤⼾不存在 |
1103 | FAILED_LOGIN | ⽤⼾名或密码错误 |
1104 | FAILED_USER_BANNED | 您已被禁⾔, 请联系管理员, 并重新登录. |
1105 | FAILED_TWO_PWD_NOT_S AME | 两次输⼊的密码不⼀致 |
2000 | ERROR_SERVICES | 两次输⼊的密码不⼀致 |
2001 | ERROR_IS_NULL | IS NULL. |
• 在 org.xiaobai.forum.common 包下创建枚举类型命名为ResultCode
这里有个很好用的东西,alt+鼠标左键,然后往下移动,就可以列编辑
具体的代码
package org.xiaobai.forum.common;
public enum ResultCode {
SUCCESS(0, "操作成功"),
FAILED(1000, "操作失败"),
FAILED_UNAUTHORIZED(1001, "未授权"),
FAILED_PARAMS_VALIDATE(1002, "参数校验失败"),
FAILED_FORBIDDEN(1003, "禁止访问"),
FAILED_CREATE(1004, "新增失败"),
FAILED_NOT_EXISTS(1005, "资源不存在"),
FAILED_USER_EXISTS(1101, "用户已存在"),
FAILED_USER_NOT_EXISTS(1102, "用户不存在"),
FAILED_LOGIN(1103, "⽤⼾名或密码错误"),
FAILED_USER_BANNED(1104, "您已被禁⾔, 请联系管理员, 并重新登录."),
FAILED_TWO_PWD_NOT_SAME(1105, "两次输⼊的密码不⼀致"),
ERROR_SERVICES(2000, "两次输⼊的密码不⼀致");
int code;
String message;
// 构造方法
ResultCode(int code, String message) {
this.code = code;
this.message = message;
}
@Override
public String toString() {
return "code = " + code + ", message = " + message + ". ";
}
// get 和 set 方法
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
定义返回结果
系统实现前后端分离,统⼀返回JSON格式的字符串,需要定义⼀个类,其中包含状态码,描述信息,返回的结果数据
• 在org.xiaobai.forum.forum.common包下创建AppResult类
具体代码, 此时还没规定传过来的数据是json
package org.xiaobai.forum.common;
public class AppResult<T> {
int code;
String message;
T data;
// 提供构造方法
public AppResult(int code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
public AppResult(int code, String message) {
this(code,message,null);
}
//对外提供静态方法方便调用
// 成功
public static AppResult success(){
// 返回一个AppResult 对象
return new AppResult(ResultCode.SUCCESS.getCode(),ResultCode.FAILED.getMessage());
}
// 成功自定义信息, 注册成功, 登录成功
public static AppResult success(String message){
return new AppResult(ResultCode.SUCCESS.getCode(),message);
}
// 成功自定义数据
public static <T> AppResult<T> success(T data){
return new AppResult<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(),data);
}
// 成功自定义信息和数据
public static <T> AppResult<T> success(String message,T data){
return new AppResult<>(ResultCode.SUCCESS.getCode(), message,data);
}
// 失败
// 失败直接返回写死的的错误码和信息
public static AppResult failed(){
return new AppResult(ResultCode.FAILED.getCode() ,ResultCode.FAILED.getMessage());
}
// 失败返回写死的错误码和自定义信息
public static AppResult failed(String message){
return new AppResult(ResultCode.FAILED.getCode(),message);
}
// 失败返回ResultCode
public static AppResult failed(ResultCode resultCode){
return new AppResult(resultCode.getCode(),resultCode.getMessage());
}
// 生成get和set方法
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
注意:
java 枚举本质上是一个类的实例,因此可以像普通对象一样调用其方法和访问属性。
自定义异常
创建⼀个异常类,加⼊状态码与状态描述属性
在org.xiaobai..forum.exception包下创建ApplicationException
主要的功能
自定义异常的代码
package org.xiaobai.forum.exception;
import org.xiaobai.forum.common.AppResult;
// 自定义异常
public class ApplicationException extends RuntimeException{
// 自定义错误
// 在异常中持有一个错误信息对象
protected AppResult errorResult;
public AppResult getErrorResult() {
return errorResult;
}
// 构造方法
// 自己写的
public ApplicationException (AppResult errorResult){
// 传给父类
// 打印ApplicationException, 而不是Runtime...
super(errorResult.getMessage());
this.errorResult = errorResult;
}
// 父类是RuntimeException, 我们重写它的方法
public ApplicationException(String message) {
super(message);
}
public ApplicationException(String message, Throwable cause) {
super(message, cause);
}
public ApplicationException(Throwable cause) {
super(cause);
}
}
全局处理异常
使⽤@ControllerAdvice(类) + @ExceptionHandler(方法) 注解实现统⼀异常处理
补充: @ControllerAdvice 表⽰控制器通知类, @ExceptionHandler 是异常处理器,两个结合表⽰当出现异常的时候执⾏某个通知,也就是执⾏某个⽅法事件
在org.xiaobai.forum.exception包下创建GlobalExceptionHandler
接⼝返回为数据时, 需要加 @ResponseBody 注解
具体代码
package org.xiaobai.forum.exception;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.xiaobai.forum.common.AppResult;
import org.xiaobai.forum.common.ResultCode;
// 全局异常处理
@Slf4j
@ControllerAdvice // 控制器通知类
public class GlobalExceptionHandler {
//处理异常
@ResponseBody
@ExceptionHandler(ApplicationException.class)// 指定要处理的是哪个异常,异常处理器
public AppResult applicationExceptionHandler(ApplicationException e) {
// TODO 自定义的异常
// 打印异常信息
e.printStackTrace();// 生产的时候要删除
// 打印日志
log.error(e.getMessage());
// 如果不为空就返回AppResult
if (e.getErrorResult() != null) {
return e.getErrorResult();
}
// e.getMessage()非空校验, 最保底的一个返回
if (e.getMessage() == null || e.getMessage().equals("")) {
// ERROR_SERVICES (2000, "服务器内部错误"),
return AppResult.failed(ResultCode.ERROR_SERVICES);
}
// 为空就返回具体的异常信息
return AppResult.failed(e.getMessage());
}
// TODO 兜底的异常
@ResponseBody
@ExceptionHandler(Exception.class)//指定处理的是哪个异常
public AppResult exceptionHandler(Exception e) {
// 打印异常信息
e.printStackTrace();
// 打印日志
log.error((e.getMessage()));
// e.getMessage()非空校验, 最保底的一个返回
if (e.getMessage() == null || e.getMessage().equals("")) {
// ERROR_SERVICES (2000, "服务器内部错误"),
return AppResult.failed(ResultCode.ERROR_SERVICES);
}
// 返回异常信息
return AppResult.failed(e.getMessage());
}
}
测试异常处理
代码
package org.xiaobai.forum.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.xiaobai.forum.common.AppResult;
import org.xiaobai.forum.exception.ApplicationException;
//返回的是数据不是url
@RestController
@RequestMapping("/test")
public class TestController {
@RequestMapping("/exception")
public AppResult testException () throws Exception {
throw new Exception("这是一个Exception");
}
@RequestMapping("appException")
public String testApplicationException(){
throw new ApplicationException("这是一个自定义的ApplicationException");
}
}
结果
登录拦截器(现在先不实现)
实现API 自动生成
单个进行输入网址测试接口不利于管理, 十分的零散
使⽤Springfox Swagger⽣成API,并导⼊Postman,完成API单元测试
Swagger 简介:Swagger是⼀套API定义的规范,按照这套规范的要求去定义接⼝及接⼝相关信息,再通过可以解析这套规范⼯具,就可以⽣成各种格式的接⼝⽂档,以及在线接⼝调试⻚⾯,通过⾃动⽂档的⽅式,解决了接⼝⽂档更新不及时的问题。
Springfox 简介:是对Swagger规范解析并⽣成⽂档的⼀个实现。
Springfox ⽂档:Springfox Reference Documentation
但是因为Springfox 很久每更新了,不支持spring boot 3.x 因此我们就使用另一个: Springdoc OpenAPI
Springfox Reference Documentation
swagger: 可以自动生成接口文档的东西,其他俩个是基于它来实现的
- Swagger 是一个广泛的 API 文档规范和工具集,核心是 OpenAPI 规范。
- Springfox Swagger 是 Swagger 的一个实现,专为 Spring 项目生成 Swagger 2 规范的 API 文档。
- Springdoc OpenAPI 是 Spring 的现代化库,专注于生成符合 OpenAPI 3.0 规范的 API 文档,逐渐取代了 Springfox。
使用: Springdoc OpenAPI 具体看这个博客: https://blog.csdn.net/2201_75880772/article/details/147875485?sharetype=blogdetail&sharerId=147875485&sharerefer=PC&sharesource=2201_75880772&sharefrom=mp_from_link
1> pom.xml来引入相关依赖
注意Spring Boot 版本不能太高我此时的版本是3.2.2
springdoc依赖
<!-- 加入 springdoc 依赖 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.5.0</version>
</dependency>
<repositories>
<!--阿里云镜像-->
<repository>
<id>alimaven</id>
<name>aliyun maven</name>
<url>https://maven.aliyun.com/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
2> 配置yml
server:
port: 8080 (你项目的端口号)springdoc:
api-docs:
enabled: true # 开启OpenApi接口
path: /v3/api-docs # 自定义路径,默认为 "/v3/api-docs"
swagger-ui:
enabled: true # 开启swagger界面,依赖OpenApi,需要OpenApi同时开启
path: /swagger-ui/index.html # 自定义路径,默认为"/swagger-ui/index.html"
3> 配置swagger
在config 包下编写SwaggerConfig
package org.xiaobai.forum.config;
import io.swagger.v3.oas.models.ExternalDocumentation;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Author HHHY
* @ClassName
* @Date: 2024/4/2 14:21
* @Description: Swagger 配置
*/
@Configuration
public class SwaggerConfig {
@Bean
public OpenAPI springShopOpenAPI() {
return new OpenAPI()
.info(new Info().title("Spring Boot 中使用 Swagger UI 构建 RESTful API")
.contact(new Contact())
.description("百草中医药信息管理平台提供的 RESTful API")
.version("v1.0.0")
.license(new License().name("Apache 2.0").url("http://springdoc.org")))
.externalDocs(new ExternalDocumentation()
.description("外部文档")
.url("https://springshop.wiki.github.org/docs"));
}
}
4> 访问: http://localhost:8080/swagger-ui/swagger-ui/index.html#/
就有以下成功页面显示
如果觉得不行可以看看这个佬的博客:Spring Boot 3.x 引入springdoc-openapi (内置Swagger UI、webmvc-api)_springdoc-openapi-starter-webmvc-ui-CSDN博客
创建工具类(MD5加密工具类)
工具类类型
1> 注册和登录都会对用户密码进行加密, 因此可以对外提供一个加密的工具类
2> 生成随机字符串(盐值), 我们也要提供一个工具类
3> String类型做非空校验, 也要提供一个工具类
创建MD5加密工具类
1> pom.xml中导⼊依赖,SpringBoot已经对这个包做了版本管理,所以这⾥不⽤指定版本号
<!-- 编码解码加密⼯具包-->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
2> 在 org.xiaobai.forum.utils包下创建MD5Util类
密码加密过程
1) 原密码
2) 生成扰动字符
3) 原密码(明文)进行MD5加密 => 密文1
4) 密文1 + 扰动字符 = 密文2
5) 对密文2 进行MD5加密
代码如下:
package org.xiaobai.forum.utils;
import org.apache.commons.codec.digest.DigestUtils;
public class MD5Util {
/**
* @param str 明文
* @return 密文
*/
public static String md5(String str){
return DigestUtils.md5Hex(str);
}
/**
*
* @param str 密码明文
* @param salt 扰动字符
* @return 密文
*/
public static String md5Salt(String str, String salt){
// ((明文)md5加密+扰动字符)md5加密
return md5(md5(str)+salt);
}
}
生成UUID工具类
生成随机字符串(盐值) 36位字符
org.xiaobai.forum.utils包下创建UUIDUtil类
package org.xiaobai.forum.utils;
import java.util.UUID;
public class StringUtil {
/**
* 生成一个36位的UUID
* @return
*/
public static String UUID_36(){
return UUID.randomUUID().toString();
}
/**
* 生成一个32位的UUID
* @return
*/
public static String UUID_32(){
return UUID.randomUUID().toString().replace("-","");
}
}
创建字符串工具类
String类型做非空校验
org.xiaobai.forum.utils包下创建StringUtil类
package org.xiaobai.forum.utils;
public class StringUtil {
/**
* 判断字符串是为空
* @param value
* @return true 空 <br/> false 非空
*/
public static boolean isEmpty(String value){
return value == null || value.length() == 0;
}
}
实现业务功能
业务实现过程中主要的包和⽬录及主要功能:
• model 包:实体对象 根据数据库生成, 还有一些和业务相关的属性
• dao 包:数据库访问 调用mapper, 和数据库进行交互
• services 包:业务处理相关的接⼝与实现,所有业务都在Services中实现 1. 定义接口 2. 主要处理业务逻辑, 可能会涉及到事务操作, 遇到异常的时候, 统一抛出ApplicationException
• controller 包:提供URL映射,⽤来接收参数并做校验,调⽤Service中的业务代码,返回执⾏结果 用来接收请求, 包括参数, 并且对参数做校验, 然后传给service层进行业务处理, 然后进行返回
• src/main/resources/mapper ⽬录:Mybaits映射⽂件,配置数据库实体与类之间的映射关系
定义SQL
• src/main/resources/static ⽬录:前端资源
注册
顺序图
有⻚⾯跳转的流程我们提供了UML顺序图,后⾯逻辑相对简单的接⼝,我们在做之前只列出实现逻辑帮⼤家理清思路即可
参数要求
• 注册时需要⽤⼾提交的参数列表
前三个参数是和数据库交互要关注的参数
第四个参数是在Controller层就对密码和确认密码进行校验, 不通过就不传给service直接返回错误
参数名 | 描述 | 类型 | 默认值 | 条件 |
username | ⽤⼾名 | String | 必须 | |
nickname | 昵称 | String | 与⽤⼾名相同 | 必须 |
password | 密码 | String | 必须 | |
passwordRepeat | 确认密码 | String | 必须,与密码相同 |
接口规范
// 请求
POST /user/register HTTP/1.1
Content-Type: application/x-www-form-urlencoded
username=user222&nickname=user222&password=123456&passwordRepeat=123456
// 响应
HTTP/1.1 200
Content-Type: application/json
{"code":0,"message":"成功","data":null}
注册功能实现后端代码
具体的实现步骤
实现过程: Mapper -> Dao -> Service(进行单元测试) -> Controller(也需要进行单元测试)
1. 定义SQL, 按照用户名查询用户信息
2. DAO 中对应类, 添加SQL中定义的方法
3. 进行Service中业务方法的编写
4. 进行单元测试
5. 编写Controller中的代码
6. 测试Controller中对外提供的API
1. 定义SQL, 按用户名查询用户信息
• src/main/resources/mapper⽬录下创建extension
• 由于src/main/resources/mapper ⽬录下是⾃动⽣成的映射⽂件,为防⽌后⾯修改数据再次⾃动成时把我们写的SQL给覆盖掉,新建⼀个扩展⽬录⽤来存放我们业务的SQL
• 在extension⽬录下新建 UserExtMapper.xml
a. 注意namespace表⽰命名空间,指定要与 UserMapper, xml中的namespace相同不同的映射⽂件指定了相同的namespace后,定义的所有⽤id或name标识的结果集映射都可以
b. 统⼀⽤org.xiaobai.forum.dao.UserMapper, 也就是UserMapper的完全限定名(包名+类名)
c. 不同的映射⽂件指定了相同的namespace后,定义的所有⽤id或name标识的结果集映射都可以不同⽂件中共享
• 代码如下:
<?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="org.xiaobai.forum.dao.UserMapper">
<!--
1. 注意namespace表⽰命名空间,要与 UserMapper.xml中的namespace相同
2. 统⼀⽤org.xiaobai.forum.dao.UserMapper, 也就是UserMapper的完全限定名(包名+类名)
3. 不同的映射⽂件指定了相同的namespace后,定义的所有⽤id或name标识的结果集映射都可以
在不同的⽂件中共享
-->
<!-- 根据用户名查询用户信息-->
<select id="selectByUserName" resultMap="BaseResultMap" parameterType="java.lang.String">
select
<include refid="Base_Column_List"></include>
form t_user
where
deleteState = 0
and
username = #{username,jdbcType=VARCHAR}
</select>
</mapper>
2. DAO 中对应类, 添加SQL中定义的方法
•org.xiaobai.forum.dao包下的UserMapper中添加⽅法声明
• 注意⽅法名要与UserExtMapper.xml中SQL标签的id相同
@Param里面的参数是SQL的参数名, 后面的参数是java代码里面使用的变量名
3. 进行Service中业务方法的编写
•org.xiaobai.forum.service包下IUserService接⼝
实现接口
org.xiaobai.forum.service.impl包下创建UserServiceImpl类并实现IUserService接口,实现逻辑参考代码中的注释(impl下面是接口实现类)
具体的步骤:
1) 进行非空校验
2) 按用户名查询信息.(要判断用户是否存在)
3) 新增用户, 设置默认值
4) 新增用户, 写入数据库
具体代码:
package org.xiaobai.forum.service.impl;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.xiaobai.forum.common.AppResult;
import org.xiaobai.forum.common.ResultCode;
import org.xiaobai.forum.dao.UserMapper;
import org.xiaobai.forum.exception.ApplicationException;
import org.xiaobai.forum.model.User;
import org.xiaobai.forum.service.IUserService;
import org.xiaobai.forum.utils.StringUtil;
import java.util.Date;
@Slf4j
@Service
public class UserServiceImpl implements IUserService {
// 注入 mapper
@Resource
private UserMapper userMapper;
@Override
public void createNormalUser(User user) {
// 1. 进行非空校验
// 对非空的必填选项进行非空校验
if(user == null || StringUtil.isEmpty(user.getUsername())
|| StringUtil.isEmpty(user.getNickname())|| StringUtil.isEmpty(user.getPassword())
|| StringUtil.isEmpty(user.getSalt())){
// 打印日志: 参数校验失败
log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
// 统一抛出 ApplicationException 自定义异常
throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
}
// 2. 按用户名查询用户信息
User existsUser = userMapper.selectByUserName(user.getUsername());
//1> 判断用户是否存在
// 不存在说明是第一次注册
if(existsUser!=null){
// 打印日志
log.info(ResultCode.FAILED_USER_EXISTS.toString());
// 抛出异常
throw new ApplicationException(AppResult.failed(ResultCode.FAILED_USER_EXISTS));
}
// 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);
// 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. 进行单元测试
步骤
运行结果
5. 编写Controller中的代码
接收请求, 包括参数, 并且对参数做校验, 然后传给service层进行业务处理, 然后进行返回
org.xiaobai.forum.controller包下创建UserController类,实现逻辑参考代码中的注释
1> 对用户名, 昵称, 密码, 确认密码进行非空校验
2> 校验俩次输入的密码进行校验是否一致
3> 创建User, 对必传值进行设置
4> 对密码进行MD5加密处理.1) 生成盐. 2) 生成密码的密文(md5加密) 3) 设置密码和盐
5> 调用service 层, 把创建好的对象传过去
6> 返回成功
package org.xiaobai.forum.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.xiaobai.forum.common.AppResult;
import org.xiaobai.forum.common.ResultCode;
import org.xiaobai.forum.model.User;
import org.xiaobai.forum.service.impl.UserServiceImpl;
import org.xiaobai.forum.utils.MD5Util;
import org.xiaobai.forum.utils.UUIDUtil;
// 对Controller进行API接口的描述
@Tag(name = "用户接口",description = "和用户相关的接口, 登录, 注册...")
// 日志注解
@Slf4j
// 这是一个返回数据的Controller
@RestController
// 路由映射, 一级路径
@RequestMapping("/user")
public class UserController {
@Resource
UserServiceImpl userService;
@Operation(summary = "用户注册")
@PostMapping("/register") // 直接使用api注解来实现非空校验
public AppResult register(@Parameter(description = "用户名")@RequestParam("username")@NonNull String username,
@Parameter(description = "昵称")@RequestParam("nickname")@NonNull String nickname,
@Parameter(description = "密码")@RequestParam("password")@NonNull String password,
@Parameter(description = "确认密码")@RequestParam("passwordRepeat")@NonNull String passwordRepeat){
// 校验密码和重复密码是否相同
if(!password.equals(passwordRepeat)){
// 俩次输入的密码不一致
log.warn(ResultCode.FAILED_TWO_PWD_NOT_SAME.toString());
// 返回错误信息
return AppResult.failed(ResultCode.FAILED_TWO_PWD_NOT_SAME);
}
// 准备要插入的数据(必传值进行设置)
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();
}
}
注意:
关于非空校验
传统方式, 但是此时我用的是SpringDoc API里面的注解
关于SpringDoc API 的注解进行非空校验
6. 测试Controller中对外提供的API
我们此时测试成功, 其他情况有机会再测
注册功能实现前端代码
1> 前端技术介绍
HTML, CSS, JavaScript 最基础的前端技术
参考⽹站:https://developer.mozilla.org/zh-CN
jQuery 对原生JS进行了封装, 主要使用AJAX,DOM元素的操作相关方法
选择器
1) 标签选择器 $("div")
2) ID 选择器 $("#myDiv")
3) 类选择器 $(".myClass")
官⽹:https://jquery.com
HTML, CSS, JavaScript, jQuery相关中⽂资料⽹上有很多,可⾃⾏搜索
Bootstrap 定义了HTML 元素的样式, 和页面布局
官⽹: https://getbootstrap.com Github: https://github.com/twbs 中⽂站:https://www.bootcss.com
Tabler 对Bootstrap美化后的元素进行排版和组合, 形成一个个可以直接使用的组件, 比如: 表单, 列表, 图片墙...
Github: https://github.com/tabler/tabler
官⽹: https://tabler.io
图标 丰富多彩的图标, 可以根据需要选择
https://tabler-icons.io/
toast 用于提示信息的JS插件(类似于JAVA中的依赖, 提供一些好用的功能)
官⽹: https://kamranahmed.info/toast
2> 集成前端代码
把前端所有的资源放在src/main/resource/static 下(资源绑定自己下载)
测试: 进入http://127.0.0.1:8080/sign-up.html#
后续我们编写前端代码, 我们使用vscode作为开发工具
1) 导入相关的依赖 CSS
2) 引入JS
3) 处理业务逻辑
注意: jQuery提供的所有功能都是方法, 因此调用后必须加()
对用户名, 昵称,密码, 重复密码进参数校验
构造发送的数据
构造ajax: 根据后端的响应结果来构造ajax
具体前端代码
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<link rel="shortcut icon" href="/favicon.ico">
<title>比特论坛 - 用户注册</title>
<!-- 导入CSS -->
<link href="./dist/css/tabler.min.css?1674944402" rel="stylesheet" />
<link rel="stylesheet" href="./dist/css/jquery.toast.css">
<!-- 设置字体 -->
<!-- <style>
@import url('https://rsms.me/inter/inter.css');
:root {
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
}
body {
font-feature-settings: "cv03", "cv04", "cv11";
}
</style> -->
</head>
<body class="d-flex flex-column">
<!-- 正文 -->
<div class="page page-center">
<div class="container container-tight py-4">
<div class="text-center mb-4">
<img src="./image/bit-forum-logo01.png" height="50" alt="">
</div>
<form id="signUpForm" class="card card-md" autocomplete="off" novalidate>
<div class="card-body">
<h2 class="text-center mb-4">用户注册</h2>
<!-- 用户名 -->
<div class="mb-3">
<label class="form-label required">用户名</label>
<input type="text" class="form-control " placeholder="请输入用户名" name="username" id="username">
<div class="invalid-feedback">用户名不能为空</div>
</div>
<!-- 昵称 -->
<div class="mb-3">
<label class="form-label required">昵称</label>
<input type="text" class="form-control" placeholder="请输入昵称" name="nickname" id="nickname">
<div class="invalid-feedback">昵称不能为空</div>
</div>
<!-- 密码 -->
<div class="mb-3">
<label class="form-label required">密码</label>
<div class="input-group input-group-flat">
<input type="password" class="form-control" placeholder="请输入密码" autocomplete="off" name="password"
id="password">
<span class="input-group-text">
<a href="javascript:void(0);" class="link-secondary" id="password_a" title="显示密码"
data-bs-toggle="tooltip"><!-- Download SVG icon from http://tabler-icons.io/i/eye -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 12m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
<path
d="M22 12c-2.667 4.667 -6 7 -10 7s-7.333 -2.333 -10 -7c2.667 -4.667 6 -7 10 -7s7.333 2.333 10 7" />
</svg>
</a>
</span>
<div class="invalid-feedback">密码不能为空</div>
</div>
</div>
<!-- 确认密码 -->
<div class="mb-3">
<label class="form-label required">确认密码</label>
<div class="input-group input-group-flat">
<input type="password" class="form-control" placeholder="再次输入密码" autocomplete="off" name="passwordRepeat"
id="passwordRepeat">
<span class="input-group-text">
<a href="javascript:void(0);" class="link-secondary" id="passwordRepeat_a" title="显示密码"
data-bs-toggle="tooltip"><!-- Download SVG icon from http://tabler-icons.io/i/eye -->
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 12m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
<path
d="M22 12c-2.667 4.667 -6 7 -10 7s-7.333 -2.333 -10 -7c2.667 -4.667 6 -7 10 -7s7.333 2.333 10 7" />
</svg>
</a>
</span>
<div class="invalid-feedback">请检查确认密码</div>
</div>
</div>
<div class="mb-3">
<label class="form-check">
<input type="checkbox" class="form-check-input" id="policy" />
<span class="form-check-label">同意 <a href="#" tabindex="-1">比特论坛使用条款和隐私政策</a>.</span>
</label>
</div>
<div class="form-footer">
<button type="button" class="btn btn-primary w-100" id="submit">注册</button>
</div>
</div>
</form>
<div class="text-center text-muted mt-3">
我已有一个账户? <a href="./sign-in.html" tabindex="-1">登录</a>
</div>
</div>
</div>
</body>
<!-- 导入JS -->
<script src="./dist/js/tabler.min.js"></script>
<script src="./dist/js/jquery-3.6.3.min.js"></script>
<script src="./dist/js/jquery.toast.js"></script>
<script>
// 当前页加载成功后执行的方法
$(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;
}
// 构造数据
let postData = {
username: $('#username').val(),
nickname: $('#nickname').val(),
password: $('#password').val(),
passwordRepeat: $('#passwordRepeat').val()
};
// 发送AJAX请求
// contentType = application/x-www-form-urlencoded
// 成功后跳转到 sign-in.html
$.ajax({
type: 'post',
url: 'user/register',
contentType: 'application/x-www-form-urlencoded',
data: postData,
// 回调方法
success: function (respData) {
// 判断返回的状态码
if (respData.code == 0) {
// 跳转到登录页面
location.assign('/sign-in.html');
} else {
// 提示信息
$.toast({
heading: '警告',
text: respData.message,
icon: 'warning'
});
}
},
error: function () {
// 提示信息
$.toast({
heading: '错误',
text: '访问出现问题,请与管理员联系.',
icon: 'error'
});
}
});
});
// 表单元单独检验
$('#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');
}
});
});
</script>
</html>
登录
顺序图
参数要求
参数名 | 描述 | 类型 | 条件 |
username | ⽤⼾名 | String | 必须 |
password | 密码 | String | 必须 |
接口规范
// 请求
POST /user/login HTTP/1.1
Content-Type: application/x-www-form-urlencoded
username=bitboy&password=123456
// 响应
HTTP/1.1 200
Content-Type: application/json
{"code":0,"message":"成功","data":null}
登录功能实现后端代码
1. 在Mapper.xml中编写SQL语句
2. 在Mapper.java中定义方法
1.2步在注册的时候已经写过了
3. 定义Service接口
定义一个login(username,password)
4. 实现Service接口
实现步骤
1) 对用户名和密码进行非空校验
2) 调用mapper层的根据用户名查询, 返回用户信息
3) 对查询到的用户进行非空校验(null 说明没查到)
4) 对密码进行MD5校验, 看经过MD5算法得到的密码和数据库的密码是否一致
5) 登录成功,打印日志
具体代码
/**
* @param username 用户名
* @return 根据用户名来进行查询, 查询是否登录的用户在数据库存在, 存在久返回查询结果
*/
@Override
public User selectByUserName(String username) {
// 非空校验
if (StringUtil.isEmpty(username)) {
// 打印日志
// 参数校验失败
log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
// 抛出异常
throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
}
// 调用mapper, 返回查询结果
return userMapper.selectByUserName(username);
}
/**
* 进行登录
*
* @param username 用户名
* @param password 密码
* @return 登录成功返回登录用户
*/
@Override
public User login(String username, String password) {
// 1. 非空校验
if (StringUtil.isEmpty(username) || StringUtil.isEmpty(password)) {
// 打印日志
// 参数校验失败
log.warn(ResultCode.FAILED_PARAMS_VALIDATE.toString());
// 抛出异常
throw new ApplicationException(AppResult.failed(ResultCode.FAILED_PARAMS_VALIDATE));
}
// 2. 按用户名进行查询
User user = selectByUserName(username);
// 3. 对查询的结果进行非空校验
if (user == null) {
// 说明没有查到
// 参数校验失败
log.warn(ResultCode.FAILED_LOGIN.toString() + ", username" + username);
// 抛出异常
throw new ApplicationException(AppResult.failed(ResultCode.FAILED_LOGIN));
}
// 4. 对密码进行非空校验: MD5(MD5(当前输入的密码)+随机字符(盐))
String encryptPassword = MD5Util.md5Salt(password,user.getSalt());
// 5. 用密文和数据库中存的用户密码进行比较
if(!encryptPassword.equals(user.getPassword())){
// 参数校验失败
log.warn(ResultCode.FAILED_LOGIN.toString() + ", 密码错误, username" + username);
// 抛出异常
throw new ApplicationException(AppResult.failed(ResultCode.FAILED_LOGIN));
}
// 打印成功日志
log.info("登录成功 username = " + username);
// 登录成功 返回用户信息
return user;
}
5. 单元测试
编写测试代码,进行测试
注意: 使用@Transaction 会在测试后进行事务的回滚, 不会污染数据库中的数据
6. Controller实现方法并对外提供API接口
在UserController中实现登录⽅法
步骤
1) 使用注解进行非空校验
2) 调用Service中的登录方法, 返回User对象
3) 登录成功就把User对象设置到Session作用域中
4) 返回结果
具体代码
/**
* 用户登录
* @param request 获取http请求, 设置session
* @param username 用户名
* @param password 密码
* @return 返回登录成功/失败
*/
@Operation(summary = "用户登录")
@PostMapping("/login") //1. 使用注解进行非空校验
public AppResult login (HttpServletRequest request,
@Parameter(description = "用户名") @RequestParam("username") @NonNull String username,
@Parameter(description = "密码") @RequestParam("password") @NonNull String password){
// 2. 调用Service中的登录方法, 返回User对象
User user = userService.login(username,password);
if(user == null){
// 打印日志
log.warn(ResultCode.FAILED_LOGIN.toString());
// 返回结果
return AppResult.failed(ResultCode.FAILED_LOGIN);
}
// 3. 如果登录成功就把User对象设置到Session作业域中
HttpSession session = request.getSession(true);// 没有session就创建新的session
session.setAttribute(AppConfig.USER_SESSION,user);
// 4. 返回结果
return AppResult.success("登录成功");
}
注意: 要设置为true , 这样没有session对象的时候就会新建一个
7. 测试API接口
启动项目, 进入这个url: http://localhost:8080/swagger-ui/swagger-ui/index.html#/
登录功能实现前端代码
编写前端代码
1) 为相关的控件写一个id
2) 编写ajax请求
具体代码, 太多了只放ajax的
// 构造数据
let postData = {
username : usernameEl.val(),
password : passwordEl.val()
}
// 发送AJAX请求,成功后跳转到index.html
$.ajax({
type: 'post',
url : 'user/login',
contentType : 'application/x-www-form-urlencoded',
data : postData,
success : function (respData) {
if (respData.code == 0) {
location.assign('/index.html');
} else {
// 提示信息
$.toast({
heading: '警告',
text: respData.message,
icon: 'warning'
});
}
},
error : function () {
// 提示信息
$.toast({
heading: '错误',
text: '访问出现问题,请与管理员联系.',
icon: 'error'
});
}
});
进行测试:
此时我只放一个错误的