軟件開發
編寫公共代碼
定義狀態碼
對執?業務處理邏輯過程中可能出現的成功與失敗狀態做針對性描述(根據需求分析階段可以遇見的問題提前做出定義),?枚舉定義狀態碼,先定義?部分,業務中遇到新的問題再添加
定義狀態碼如下
狀態碼 | 類型 | 描述 |
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;}@Overridepublic 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);}// 失敗返回ResultCodepublic 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());// 如果不為空就返回AppResultif (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 {@Beanpublic 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_userwheredeleteState = 0andusername = #{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@Resourceprivate UserMapper userMapper;@Overridepublic 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 {@ResourceUserServiceImpl 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" /><pathd="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" /><pathd="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 根據用戶名來進行查詢, 查詢是否登錄的用戶在數據庫存在, 存在久返回查詢結果*/@Overridepublic 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 登錄成功返回登錄用戶*/@Overridepublic 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就創建新的sessionsession.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'});}});
進行測試:?
此時我只放一個錯誤的