【智能協同云圖庫】智能協同云圖庫第二彈:用戶管理系統后端設計與接口開發

在這里插入圖片描述

在這里插入圖片描述

在這里插入圖片描述


用戶管理系統


一、需求分析


對于用戶模塊,通常要具有下列功能:

image-20250625163902847


二、方案設計


(一)庫表設計


實現用戶模塊的難度不大,在方案設計階段,我們需要確認以下內容:

  1. 庫表設計
  2. 用戶登錄流程
  3. 如何對用戶權限進行控制

1. 核心設計

用戶表的核心是用戶登錄憑證(賬號密碼)和個人信息,SQL如下:

-- 創建數據庫
create database if not exists yu_picture;-- 切換庫
use yu_picture;-- 用戶表
-- 用戶表
create table if not exists user
(id           bigint auto_increment comment 'id' primary key,userAccount  varchar(256)                           not null comment '賬號',userPassword varchar(512)                           not null comment '密碼',userName     varchar(256)                           null comment '用戶昵稱',userAvatar   varchar(1024)                          null comment '用戶頭像',userProfile  varchar(512)                           null comment '用戶簡介',userRole     varchar(256) default 'user'            not null comment '用戶角色:user/admin',editTime     datetime     default current_timestamp not null comment '編輯時間',createTime   datetime     default current_timestamp not null comment '創建時間',updateTime   datetime     default current_timestamp not null on update current_timestamp comment '更新時間',isDelete     tinyint      default 0                 not null comment '是否刪除',unique key uk_userAccount (userAccount),index idx_userName (userName)
) comment '用戶' collate = utf8mb4_unicode_ci;

注意事項:

  1. editTimeupdateTime 的區別:editTime 表示用戶編輯個人信息的時間(需要業務代碼來更新),而 updateTime 表示這條用戶記錄任何字段發生修改的時間(由數據庫自動更新)。
  2. 給唯一值添加唯一鍵(唯一索引),比如賬號 userAccount,利用數據庫天然防重復,同時可以增加查詢效率。
  3. 給經常用于查詢的字段添加索引,比如用戶昵稱 userName,可以增加查詢效率。
  4. 建議:養成好習慣,將庫表設計 SQL 保存到項目的目錄中,比如新建 sql/create_table.sql 文件,這樣其他開發者就能更快地了解項目。image-20250623171350250

在當前情況下,我們創建的 SQL 文件無法通過 Ctrl+Alt+L 快捷鍵進行格式化,因為 IDEA 在未連接數據庫之前,無法識別應使用 MySQL 或其他數據庫的特定格式來格式化 SQL 語句。

image-20250623172830043


按照下面的操作,配置數據源,即可格式化代碼:

image-20250623175131373


執行 sql 語句:

image-20250623175722162


2. 擴展設計

(1)會員功能擴展

如果要實現會員功能,可以對表進行如下擴展:

  • userRole 字段新增枚舉值 vip,表示會員用戶,可根據該值判斷用戶權限。
  • 新增會員過期時間字段,可用于記錄會員有效期。
  • 新增會員兌換碼字段,可用于記錄會員的開通方式。
  • 新增會員編號字段,可便于定位用戶并提供額外服務,并增加會員歸屬感。
    對應的 SQL 如下:
vipExpireTime datetime     null comment '會員過期時間',
vipCode       varchar(128) null comment '會員兌換碼',
vipNumber     bigint       null comment '會員編號'

(2)用戶邀請功能擴展

如果要實現用戶邀請功能,可以對表進行如下擴展:

  • 新增 shareCode 分享碼字段,用于記錄每個用戶的唯一邀請標識,可拼接到邀請網址后面,比如 面試鴨 - 程序員求職面試刷題神器,高頻編程題目免費刷。
  • 新增 inviteUser 字段,用于記錄該用戶被哪個用戶邀請了,可通過這個字段查詢某用戶邀請的用戶列表。

對應的 SQL 如下:

shareCode     varchar(20)  DEFAULT NULL COMMENT '分享碼',
inviteUser    bigint       DEFAULT NULL COMMENT '邀請用戶 id'

(二)用戶登錄流程


  1. 建立初始會話:前端與服務器建立連接后,服務器會為該客戶端創建一個初始的匿名 Session,并將其狀態保存下來。這個 Session 的 ID 會作為唯一標識,返回給前端。

  2. 登錄成功,更新會話信息:當用戶在前端輸入正確的賬號密碼并提交到后端驗證成功后,后端會更新該用戶的 Session,將用戶的登錄信息(如用戶 ID、用戶名等)保存到與該 Session 關聯的存儲中。同時,服務器會生成一個 Set-Cookie 的響應頭,指示前端保存該用戶的 Session ID。

  3. 前端保存 Cookie:前端接收到后端的響應后,瀏覽器會自動根據 Set-Cookie 指令,將 Session ID 存儲到瀏覽器的 Cookie 中,與該域名綁定。

  4. 帶 Cookie 的后續請求:當前端再次向相同域名的服務器發送請求時,瀏覽器會自動在請求頭中附帶之前保存的 Cookie,其中包含 Session ID。

  5. 后端驗證會話:服務器接收到請求后,從請求頭中提取 Session ID,找到對應的 Session 數據。

  6. 獲取會話中存儲的信息:后端通過該 Session 獲取之前存儲的用戶信息(如登錄名、權限等),從而識別用戶身份并執行相應的業務邏輯。

mermaid


(三)用戶權限控制


可以將接口分為以下 4 種權限:

  1. 未登錄也可以使用
  2. 登錄用戶才能使用
  3. 未登錄也可以使用,但登錄用戶能進行更多操作(比如登錄后查看全文)
  4. 僅管理員才能使用

1. 傳統的權限控制方法

傳統的權限控制方法,是在每個接口內單獨編寫邏輯:先獲取到當前登錄用戶信息,然后判斷用戶的權限是否符合要求。

這種方法最靈活,但會寫很多重復的代碼,且其他開發者無法一眼得知接口所需要的權限。


2. 推薦的權限控制方法

權限校驗其實是一個比較通用的業務需求,一般會通過 Spring AOP 切面 + 自定義權限校驗注解 實現統一的接口攔截和權限校驗。如果有特殊的權限校驗邏輯,再單獨在接口中編碼。

如果需要更復雜、更靈活的權限控制,可以引入以下專門的權限管理框架:

  • Shiro
  • Spring Security
  • Sa-Token

💡 建議:選擇合適的權限管理框架,可以大大簡化權限控制的實現。


三、后端開發


(一)數據訪問層代碼生成


連接數據庫:首先利用 IDEA 連接 MySQL 數據庫。

image-20250623182307239


執行 SQL 腳本:創建數據庫表。

image-20250623175722162


生成代碼:數據訪問層的代碼一般包括實體類、MyBatis 的 Mapper 類和 XML 等。

相比手動編寫,建議使用 MyBatisX 代碼生成插件(可在 idea 中裝這個插件),可以快速得到這些文件。

選中數據庫的表,右鍵選擇 MybatisX 生成器。

image-20250623182519078


按照下圖進行配置。

image-20250623192704281


查看生成的代碼:生成的代碼包括實體類、Mapper、Service 等。

image-20250623192829639


Git 托管:

從當前情況可以看出,生成的文件字體顯示為紅色,這通常意味著該文件尚未提交到 Git 版本控制系統中。接下來,我們需要先將代碼提交到Git中。

image-20250623193208335


Git 回滾:

通過使用 Git,如果我們在后續開發過程中發現某些代碼不再需要,或者需要撤銷某些更改,可以利用 Git 的回滾機制,輕松恢復到之前的代碼版本:

image-20250623193413722


展開所有子目錄:

image-20250623194952158


移動代碼:將生成的代碼移動到項目對應的位置,例如:

  • User 移動到 model.entity 包。image-20250623195550988
  • Mapper 移動到 mapper 包。
  • Service 移動到 service 包。image-20250623201747946

為了增加數據爬取的難度,我們可以調整數據庫中字段的主鍵生成策略:

image-20250623201355870

  • 將字段的主鍵類型從自動遞增(IdType.AUTO)更改為手動指定(IdType.ASSIGN_ID):image-20250623195928682

  • 這樣的修改,可以使得主鍵的生成,不再遵循簡單的遞增規則,從而提高數據抓取的復雜性。


@TableLogic 是 MyBatis 框架中用于標識邏輯刪除的注解:

image-20250623200540581

通過使用 @TableLogic 注解:

  • 在指示 MyBatis 在執行插入和更新操作時,會忽略該字段;
  • 在查詢操作時,MyBatis 會根據配置自動添加條件,來排除那些被標記為已刪除的記錄。

這樣,開發者就無需在每次查詢時手動添加過濾條件,從而簡化了代碼并提高了開發效率。


如果我們不理解一些 mybatis-plus 的注解,可以仔細閱讀官方文檔:

image-20250623201256558


修改包名:移動之后,注意修改 UserMapper.xml 等文件的包名,確保路徑正確。

image-20250624114446604


(二)數據模型開發


1. 實體類

生成的代碼可能無法完全滿足需求,例如數據庫實體類,我們可以手動更改其字段配置,指定主鍵策略和邏輯刪除:

  • 主鍵策略:默認是連續生成的,容易被爬蟲抓取,因此更換策略為 ASSIGN_ID(雪花算法生成)。
  • 邏輯刪除:數據刪除時默認為徹底刪除記錄,如果出現誤刪,將難以恢復。因此采用邏輯刪除,通過修改 isDelete 字段為 1 表示已失效的數據。

image-20250624114711804

@TableName(value = "user")
@Data
public class User implements Serializable {/*** id(要指定主鍵策略)*/@TableId(type = IdType.ASSIGN_ID)private Long id;// ...(其他字段省略)/*** 是否刪除(邏輯刪除)*/@TableLogicprivate Integer isDelete;
}

使用框架時的建議:在使用框架的過程中,如果有任何疑問,都可以在官方文檔中查閱。例如,了解 MyBatis Plus 的主鍵生成注解,可以參考以下鏈接:
MyBatis Plus 注解配置


2. 枚舉類

對于用戶角色這樣值的數量有限的、可枚舉的字段,最好定義一個枚舉類,便于在項目中獲取值、減少枚舉值輸入錯誤的情況。

image-20250624114948038


model.enums 包下新建 UserRoleEnum

image-20250624114745497

@Getter
public enum UserRoleEnum {USER("用戶", "user"),ADMIN("管理員", "admin");private final String text;private final String value;UserRoleEnum(String text, String value) {this.text = text;this.value = value;}/*** 根據 value 獲取對應的枚舉實例** @param value 要查找的枚舉實例的 value* @return 匹配的枚舉實例,如果未找到則返回 null*/public static UserRoleEnum getEnumByValue(String value) {if (ObjUtil.isEmpty(value)) {// 使用 Hutool 工具類 ObjUtil 檢查 value 是否為空return null;}for (UserRoleEnum anEnum : UserRoleEnum.values()) {// values() 方法是枚舉類型提供的,用于獲取枚舉類中定義的所有枚舉常量if (anEnum.value.equals(value)) {return anEnum;}}return null;}
}

優化建議:如果枚舉值特別多,可以使用 Map 緩存所有枚舉值來加速查找,而不是遍歷列表。

@Getter
public enum UserRoleEnum {USER("用戶", "user"),ADMIN("管理員", "admin");private final String text;private final String value;// 定義一個靜態的Map來存儲枚舉值和枚舉實例的對應關系private static final Map<String, UserRoleEnum> CACHE = new HashMap<>();// 構造函數UserRoleEnum(String text, String value) {this.text = text;this.value = value;// 在構造函數中將當前枚舉實例添加到CACHE中CACHE.put(this.value, this);// way1}// 將枚舉元素放入哈希表,有兩個方法: way1, way2 , 二選一即可// way2// 靜態初始化塊,在類加載時執行static {// 遍歷UserRoleEnum的所有實例for (UserRoleEnum role : UserRoleEnum.values()) {// 將每個實例的value作為鍵,實例本身作為值,存入CACHE Map中CACHE.put(role.value, role);}}/*** 根據 value 獲取對應的枚舉實例** @param value 要查找的枚舉實例的 value* @return 匹配的枚舉實例,如果未找到則返回 null*/public static UserRoleEnum getEnumByValue(String value) {if (ObjUtil.isEmpty(value)) {// 使用 Hutool 工具類 ObjUtil 檢查 value 是否為空return null;}// 從CACHE Map中獲取枚舉實例return CACHE.get(value);}
}

我們暫時先繼續使用列表獲取枚舉類;


各功能接口的開發


用戶注冊


1. 數據模型


model.dto.user 包下新建用于接收請求參數的類 UserRegisterRequest

image-20250624120337471

/*** 用于用戶注冊的實體類*/@Data
public class UserRegisterRequest implements Serializable {private static final long serialVersionUID = 6055996074332767695L;/*** 賬號*/private String userAccount;/*** 密碼*/private String userPassword;/*** 確認密碼*/private String checkPassword;
}6055996074332767695L

  1. 序列化的目的:

    • 在Java中,實現Serializable接口允許對象轉換為字節序列。
    • 這種轉換使得對象可以被寫入文件通過網絡發送到另一臺機器上,從而實現對象的持久化和網絡傳輸
  2. 實現Serializable接口:

    • 實現 Serializable 接口,是啟用對象序列化的簡單方法。
    • 一旦對象實現了Serializable接口,就可以使用 Java 的序列化機制來轉換對象狀態
  3. 序列化ID(serialVersionUID):

    • 實現Serializable接口后,通常需要為類提供一個唯一的序列化 ID

    • 安裝插件GenerateSerialversionUID,使用該插件為類生成唯一序列化 ID

      image-20250624122505452

    • 這個ID用于驗證序列化對象的版本兼容性,確保在反序列化過程中,發送方和接收方的類版本是兼容的

  4. 序列化ID的更新:

    • 如果類的實現發生了變化,如添加或刪除字段,應該更新序列化ID
    • 更新序列化 ID ,可以防止因類結構變化而導致的反序列化錯誤

開發建議:在 Java 接口開發中,為每個接口定義一個專門的類,來接收請求參數,可以提高代碼的可讀性和維護性,便于對參數進行統一驗證和擴展,同時減少接口方法參數過多導致的復雜性,有助于在復雜場景下更清晰地管理和傳遞數據。


2. 服務開發


service 包的 UserService 中增加方法聲明:

image-20250624123123897

public interface UserService extends IService<User> {/*** 用戶注冊** @param userAccount   用戶賬戶* @param userPassword  用戶密碼* @param checkPassword 校驗密碼* @return 新用戶 id*/long userRegister(String userAccount, String userPassword, String checkPassword);
}

UserServiceImpl 中增加實現代碼,注意補充一些校驗條件:

image-20250624123428923

@Service// (8) 在 MyBatis-Plus 框架中,ServiceImpl 類為服務層提供了基礎實現,它包含了對數據庫進行 CRUD(創建、讀取、更新、刪除)操作的方法。
public class UserServiceImpl extends ServiceImpl<UserMapper, User>implements UserService {@Overridepublic long userRegister(String userAccount, String userPassword, String checkPassword) {// (5) 先寫步驟 1、2、3、4, 后續可以利用 AI 根據注釋生成相應代碼// (1) 校驗參數if(StrUtil.hasBlank(userAccount, userPassword, checkPassword)){// (6) StrUtil.hasBlank 校驗傳入的參數是否為空, 拋出自定義異常throw new BusinessException(ErrorCode.PARAMS_ERROR, "參數為空");}if (userAccount.length() < 4) {throw new BusinessException(ErrorCode.PARAMS_ERROR, "用戶賬號過短");}if (userPassword.length() < 8 || checkPassword.length() < 8) {throw new BusinessException(ErrorCode.PARAMS_ERROR, "用戶密碼過短");}if (!userPassword.equals(checkPassword)) {throw new BusinessException(ErrorCode.PARAMS_ERROR, "兩次輸入的密碼不一致");}// (2) 檢查用戶賬號是否存于數據庫中// (7) 構造查詢條件QueryWrapper<User> queryWrapper = new QueryWrapper<>();queryWrapper.eq("userAccount",userAccount);// (9) 校驗當前賬戶是否是未注冊賬戶Long count = this.baseMapper.selectCount(queryWrapper);// this 指的是 UserServiceImpl 的一個對象// baseMapper 是 ServiceImpl 類中的一個屬性,它是一個泛型參數,通常對應于與實體類相關的 Mapper 接口// 調用 selectCount 方法時,它的作用是執行一個數據庫查詢,計算與提供的查詢條件相匹配的記錄數量// (10) 當前是注冊賬戶, 所以 count > 0 表示該用戶已存在, 注冊失敗if(count > 0){throw new BusinessException(ErrorCode.PARAMS_ERROR, "賬號重復");}// (3) 密碼使用加鹽加密String encryptPassword = getEncryptPassword(userPassword);// (4) 插入數據到數據庫中User user = new User();user.setUserAccount(userAccount);user.setUserPassword(encryptPassword);user.setUserName("無名");user.setUserRole(UserRoleEnum.USER.getValue());// (14) 保存用戶信息到數據庫中boolean saveResult = this.save(user);// save() 的作用是將 user 對象保存到數據庫中, 是 ServiceImpl 接口內置的方法// save() 返回一個 boolean 類型的結果,表示保存操作是否成功// (15) 保存失敗, 說明是系統內部的問題if(!saveResult){throw new BusinessException(ErrorCode.SYSTEM_ERROR, "注冊失敗, 數據庫錯誤");}// (16)// this.save(user) , mybatis 框架利用主鍵回填// 在調用 save() 時, 自動生成 user 的主鍵 id, 因此在這里可以直接獲取 idreturn user.getId();}/*** (11) 獲取加密后的密碼* @param userPassword 傳入用戶輸入的密碼* @return 經過加密算法加密后的密碼*/@Overridepublic String getEncryptPassword(String userPassword){// (12) 加鹽, 混淆密碼final String SALT = "yupi";// (13) 返回加密后的結果return DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());// (鹽值 + 密碼) 的字符串拼接結果, 轉為字節數組// 再利用 spring 內置的加密工具類, 對字節數組使用 md5 算法進行單向加密}
}

3. 接口開發


controller 包中新建 UserController,新增用戶注冊接口:

image-20250624132649436

@RestController
// @RestController 會把所有接口的返回結果轉為 JSON 格式@RequestMapping("/user")
public class UserController {// (4) 注入 service 對象@Resourceprivate UserService userService;@PostMapping("/register")public BaseResponse<Long> userRegister(@RequestBody UserRegisterRequest userRegisterRequest){// (1) 使用 @RequestBody 注解,后端能夠接收前端以 JSON 格式, 發送的對象參數// (2) 傳入對象為空, 使用自定義工具類拋異常ThrowUtils.throwIf(userRegisterRequest == null, ErrorCode.PARAMS_ERROR);// (3) 安裝插件 Generate All Getter And Setter, 下面三行代碼只需要輸入 userRegisterRequest.allgetString userAccount = userRegisterRequest.getUserAccount();String userPassword = userRegisterRequest.getUserPassword();String checkPassword = userRegisterRequest.getCheckPassword();// (5) 封裝返回結果long result = userService.userRegister(userAccount, userPassword, checkPassword);return ResultUtils.success(result);}
}

4. 測試接口


每開發完一個接口,都可以使用 Swagger 接口文檔 來測試,測試可以采取 Debug 模式,同樣可以滿足測試接口功能:

image-20250624142845320


💡 測試注意事項:測試時要尤其注意邊界值和特殊值,不能只測試正常的情況,如:

image-20250624151445303


用戶登錄


1. 數據模型


model.dto 包下新建用于接收請求參數的類 UserLoginRequest

image-20250624151845969

@Data
public class UserLoginRequest implements Serializable {private static final long serialVersionUID = 4110612674161298294L;/*** 賬號*/private String userAccount;/*** 密碼*/private String userPassword;
}

model.vo 包下新建用于返回響應參數的類 LoginUserVO,表示脫敏后的數據:

  1. 復制 model.entity包下的 User 類,粘貼到 model.vo
  2. 重構UserLoginUserVO
  3. 刪除不必要的字段(如 Password, isdelete)
  4. 刪除注解(返回給前端的數據不需要這些注解)

image-20250624153504626

/*** 已登錄用戶視圖脫敏后的數據*/@Data
public class LoginUserVO implements Serializable {/*** id*/private Long id;/*** 賬號*/private String userAccount;/*** 用戶昵稱*/private String userName;/*** 用戶頭像*/private String userAvatar;/*** 用戶簡介*/private String userProfile;/*** 用戶角色:user/admin*/private String userRole;/*** 編輯時間*/private Date editTime;/*** 創建時間*/private Date createTime;/*** 更新時間*/private Date updateTime;private static final long serialVersionUID = 1L;
}

2. 服務開發


service 包的 UserService 中增加方法聲明:

image-20250624153056258

/*** 用戶登錄** @param userAccount  用戶賬戶* @param userPassword 用戶密碼* @param request      HTTP 請求對象* @return 脫敏后的用戶信息*/
LoginUserVO userLogin(String userAccount, String userPassword, HttpServletRequest request);

image-20250624153032041


UserServiceImpl 中增加實現代碼:

image-20250624160530937


注意補充一些校驗條件。在用戶登錄成功后,將用戶信息存儲在當前的 Session 中。代碼如下:

/*** 用戶登錄* @param userAccount  用戶賬戶* @param userPassword 用戶密碼* @param request      HTTP 請求對象* @return 脫敏后的用戶信息*/
@Override
public LoginUserVO userLogin(String userAccount, String userPassword, HttpServletRequest request) {// 1. 校驗if (StrUtil.hasBlank(userAccount, userPassword)) {throw new BusinessException(ErrorCode.PARAMS_ERROR, "參數為空");}if (userAccount.length() < 4) {throw new BusinessException(ErrorCode.PARAMS_ERROR, "賬號錯誤");}if (userPassword.length() < 8) {throw new BusinessException(ErrorCode.PARAMS_ERROR, "密碼錯誤");}// 2. 對用戶傳遞的秘密進行加密String encryptPassword = getEncryptPassword(userPassword);// 3. 查詢用戶是否存在于數據庫QueryWrapper<User> queryWrapper = new QueryWrapper<>();queryWrapper.eq("userAccount", userAccount);queryWrapper.eq("userPassword",encryptPassword); // 用加密密碼比較// 5. selectOne() 保證查出來的結果只有一條數據User user = this.baseMapper.selectOne(queryWrapper);// 注意, 我們的數據庫表對 userAccount 字段設置為唯一索引,// 根據上面的構造條件, 只會查詢出一條結果, 所以不需要使用事務// 6. 查詢結果為空, 表示賬戶不存在if(user == null){log.info("user login failed, userAccount cannot match userPassword");throw new BusinessException(ErrorCode.PARAMS_ERROR, "用戶不存在或者密碼錯誤");}// 4. 保留用戶的登錄態request.getSession().setAttribute("USER_LOGIN_STATE", user);// "USER_LOGIN_STATE" 是寫死的, 會造成硬編碼, 后續需要拓展常量 constant// 7. 將查詢的 user 轉為 LoginUserVOreturn this.getLoginUserVO(user);
}/*** 8. 獲取脫敏后的登錄用戶信息* @param user 查詢數據庫得到的結果* @return 對查詢結果脫敏后, 得到的結果*/
@Override
public LoginUserVO getLoginUserVO(User user) {if(user == null){return null;}LoginUserVO loginUserVO = new LoginUserVO();BeanUtil.copyProperties(user,loginUserVO);// hutool 工具類, 拷貝源對象的屬性, 對給目標對象已有且對應的屬性進行賦值return loginUserVO;
}

注意事項:

  • 由于注冊用戶時存入數據庫的密碼是加密后的,查詢用戶信息時,也要對用戶輸入的密碼進行同樣算法的加密,才能與數據庫中的信息對應上。
  • 可以將上述的 Session 理解為一個 Map,可以給 Map 設置 keyvalue。每個不同的 SessionID 對應的 Session 存儲都是不同的,不用擔心會污染。
  • 所以上述代碼中,給 Session 設置了固定的 keyUSER_LOGIN_STATE),可以將這個 key 值提取為常量,便于后續獲取。

constant 包下新建 UserConstant 接口類,統一聲明用戶相關的常量:

image-20250624163835234

UserConstant 設計為接口類是合適的,因為接口中的字段默認是 public static final 的,這意味著它們是常量,且只能被賦值一次。這種特性使得接口成為定義常量的理想選擇。

/*** 用戶常量*/
public interface UserConstant {/*** 用戶登錄態鍵*/String USER_LOGIN_STATE = "user_login";// region 權限/*** 默認角色*/String DEFAULT_ROLE = "user";/*** 管理員角色*/String ADMIN_ROLE = "admin";// endregion
}

修改登錄態寫死的部分:

image-20250624163801482


3. 接口開發


UserController 中新增用戶登錄接口:

@PostMapping("/login")
public BaseResponse<LoginUserVO> userLogin(@RequestBody UserLoginRequest userLoginRequest, HttpServletRequest request) {// 1 登錄接口的參數, 比校驗接口多了一個 HttpServletRequest requestThrowUtils.throwIf(userLoginRequest == null, ErrorCode.PARAMS_ERROR);String userAccount = userLoginRequest.getUserAccount();String userPassword = userLoginRequest.getUserPassword();LoginUserVO loginUserVO = userService.userLogin(userAccount, userPassword, request);return ResultUtils.success(loginUserVO);
}

4. 測試接口


重新運行程序,測試接口:

image-20250624164805016


image-20250624164840977


獲取當前登錄用戶


可以從 request 請求對象對應的 Session 中直接獲取到之前保存的登錄用戶信息,無需其他請求參數。


1. 服務開發


service 包的 UserService 中增加方法聲明:

image-20250624173107134

/*** 獲取當前登錄用戶** @param request HTTP 請求對象* @return 用戶信息*/
User getLoginUser(HttpServletRequest request);

UserServiceImpl 中增加實現代碼:

image-20250624173119366

此處為了保證獲取到的數據始終是最新的,先從 Session 中獲取登錄用戶的 id,然后從數據庫中查詢最新的結果。代碼如下:

@Override
public User getLoginUser(HttpServletRequest request) {// 1. 先判斷是否已經登錄Object userObj = request.getSession().getAttribute(UserConstant.USER_LOGIN_STATE);// 2. 根據 Sesssion 獲取對象后, 先把 Object 轉為 UserUser currentUser = (User) userObj;// 3. 根據對象判斷當前是否登錄if(currentUser == null || currentUser.getId() == null){// 4. 多判斷一個 id 是否為空, 后續前端需要根據 id 來判斷用戶是否登錄throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR);}// 5. 從數據庫中查詢信息, 如果追求性能, 直接返回 currentUser 對象(利用緩存, 不推薦)Long userId = currentUser.getId();// 6. 根據 id 從數據庫中查詢, 查詢的結果重新賦值給 currentUsercurrentUser = this.getById(userId);// 7. 再次校驗, 當前用戶信息在數據庫中查不到, 可能該用戶被管理員刪除if(currentUser == null){throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR);}// 8. 返回數據庫查詢結果return currentUser;
}

2. 接口開發


UserController 中新增獲取當前登錄用戶接口:

image-20250624173144586

// 獲取當前登錄用戶信息@GetMapping("/get/login")
public BaseResponse<LoginUserVO> getLoginUser(HttpServletRequest request) {// 1. 調用 service , 獲取用戶信息User loginUser = userService.getLoginUser(request);// 2. Controller 接口可以被前端直接調用, 因此要對 Service 拿到的信息進行脫敏LoginUserVO loginUserVO = userService.getLoginUserVO(loginUser);// 3. 統一返回結果, 封裝脫敏后的數據, 返回給前端return ResultUtils.success(loginUserVO);
}

注意事項:上述代碼直接將數據庫查到的所有信息都返回給了前端(包括密碼),可能存在信息泄露的安全風險。因此,需要對返回結果進行脫敏處理。


用戶注銷


用戶注銷可以從 request 請求對象對應的 Session 中直接獲取到之前保存的登錄用戶信息,來完成注銷,無需其他請求參數。


1. 服務開發


service 包的 UserService 中增加方法聲明:

image-20250624173107134

/*** 用戶注銷** @param request HTTP 請求對象* @return 是否注銷成功*/
boolean userLogout(HttpServletRequest request);

UserServiceImpl 中增加實現代碼,從 Session 中移除掉當前用戶的登錄態即可:

image-20250624173119366

@Override
public boolean userLogout(HttpServletRequest request) {// 先判斷是否已登錄Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE);if (userObj == null) {throw new BusinessException(ErrorCode.OPERATION_ERROR, "未登錄");}// 移除登錄態request.getSession().removeAttribute(USER_LOGIN_STATE);return true;
}

2. 接口開發


UserController 中新增用戶注銷接口:

image-20250624173144586

@PostMapping("/logout")
public BaseResponse<Boolean> userLogout(HttpServletRequest request) {ThrowUtils.throwIf(request == null, ErrorCode.PARAMS_ERROR);boolean result = userService.userLogout(request);return ResultUtils.success(result);
}

用戶權限控制


在本節教程的方案設計中提到:權限校驗是一個比較通用的業務需求,通常通過 Spring AOP 切面 + 自定義權限校驗注解 實現統一的接口攔截和權限校驗。

如果有特殊的權限校驗邏輯,再單獨在接口中編碼。


1. 權限校驗注解


首先編寫權限校驗注解,將其放在 annotation 包下:

image-20250624173953812

下面的代碼定義了一個名為 AuthCheck 的自定義注解,用于在運行時進行權限檢查:

@Target(ElementType.METHOD)          // 注解的生效范圍: 定義為方法注解
@Retention(RetentionPolicy.RUNTIME)  // 注解的生效時機: 運行時生效
public @interface AuthCheck {/*** 必須有某個角色, 也就是說, */String mustRole() default "";
}

注解 AuthCheck 包含一個元素 mustRole()

  • mustRole() 一個字符串類型的元素,用于指定必須擁有的角色
  • 如果該元素不指定值(即默認值),則默認為空字符串 ""

使用這個注解的方法,需要確保調用者具有指定的角色,否則可能會拒絕訪問執行相應的權限檢查邏輯。這個注解可以用于實現基于角色的訪問控制(RBAC)


2. 權限校驗切面


編寫權限校驗 AOP,采用環繞通知,在 打上該注解的方法 執行前后進行一些額外的操作,比如校驗權限:

image-20250624194319719


代碼如下,放在 aop 包下:

image-20250624184537786

@Aspect     // 表示該類是一個切面類,用于定義橫切關注點
@Component  // 將該類聲明為 Spring 組件,使其成為 Spring 應用上下文中的一個 Bean
public class AuthInterceptor {@Resourceprivate UserService userService;/*** 執行權限攔截* @param joinPoint 切點參數,用于獲取被攔截方法的信息* @param authCheck 自定義的權限注解,用于指定權限要求* @return 攔截方法的執行結果* @throws Throwable 可能拋出的異常*/@Around("@annotation(authCheck)")  // 定義切點,指定只有被 @AuthCheck 注解標記的方法才會被攔截public Object doInterceptor(ProceedingJoinPoint joinPoint, AuthCheck authCheck) throws Throwable {// 1. 獲取必須具備的角色權限String mustRole = authCheck.mustRole();// 2. 從全局請求上下文中獲取當前請求RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();// 根據獲取全局請求上下文 RequestContextHolder, 獲取當前請求的各個屬性 currentRequestAttributes()// 3. 從請求上下文中獲取 HttpServletRequest 對象HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();// 需要把 RequestAttributes 類型的對象, 轉為子類 ServletRequestAttributes 類型的對象, 才可以調用 getRequest()// 4. 根據請求對象獲取當前登錄用戶的詳細信息User loginUser = userService.getLoginUser(request);// 5. 將角色權限字符串轉換為枚舉類型UserRoleEnum mustRoleEnum = UserRoleEnum.getEnumByValue(mustRole);// 6. 如果mustRoleEnum 為空,表示沒有指定必須具備的角色權限,直接放行if(mustRoleEnum == null){return joinPoint.proceed();}// 7. 獲取當前登錄用戶的角色權限(會員權限、管理員權限等特殊權限)UserRoleEnum userRoleEnum = UserRoleEnum.getEnumByValue(loginUser.getUserRole());// 8. 如果用戶角色權限為空,表示用戶沒有權限,拋出異常if (userRoleEnum == null) {throw new BusinessException(ErrorCode.NO_AUTH_ERROR);}// 9. 如果必須具備管理員權限,但當前登錄用戶不是管理員,則拋出異常if(UserRoleEnum.ADMIN.equals(mustRoleEnum) && !UserRoleEnum.ADMIN.equals(userRoleEnum)){throw new BusinessException(ErrorCode.NO_AUTH_ERROR);}// 10. 如果當前用戶是管理員,放行執行原方法return joinPoint.proceed();}
}

3. 使用注解


只要給方法添加了 @AuthCheck 注解,就必須要登錄,否則會拋出異常。可以設置 mustRole 為管理員,這樣僅管理員才能使用該接口:

@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)

對于不需要登錄就能使用的接口,不需要使用該注解。


4. 注解測試


我們測試以下權限校驗切面的功能,在注冊接口設置管理員權限的注解:

image-20250624194532322


重新啟動程序,當前用戶不是管理員,如果調用注冊接口,會產生如下效果:

image-20250624194805563


我們調用登錄接口登錄,再調用注冊接口:

image-20250624194926914


修改后端用戶權限:

image-20250624195029549


重新運行程序,再次調用注冊接口,繼續測試權限校驗切面的功能:

image-20250624195251674

賬戶重復,表示程序已經進入到切點內了,也就表示權限校驗注解已經開發完成了;


注冊接口在當前用戶的 userRole = admin 的情況下,可以正常訪問:

image-20250624195411473


用戶管理


用戶管理功能具體可以拆分為以下幾部分:

  • 管理員操作

    • 創建用戶

    • 根據 id 刪除用戶

    • 更新用戶

    • 分頁獲取用戶列表(需要脫敏)

    • 根據 id 獲取用戶(未脫敏)

  • 普通用戶操作

    • 根據 id 獲取用戶(脫敏)

1. 數據模型


每個操作都需要提供一個請求類,都放在 dto.user 包下;用戶管理功能,本質上就是對用戶信息進行增刪改查:

image-20250625124137748


用戶刪除請求

/*** 通用的刪除請求類(刪除操作都是根據用戶 id 進行刪除, 所以放在 common 進行復用)*/
@Data
public class DeleteRequest implements Serializable {/*** 數據 ID*/private Long id;private static final long serialVersionUID = 1L;
}

用戶創建請求

@Data
public class UserAddRequest implements Serializable {/*** 用戶昵稱*/private String userName;/*** 賬號*/private String userAccount;/*** 用戶頭像*/private String userAvatar;/*** 用戶簡介*/private String userProfile;/*** 用戶角色: user, admin*/private String userRole;private static final long serialVersionUID = 1L;
}

用戶更新請求

@Data
public class UserUpdateRequest implements Serializable {/*** id*/private Long id;/*** 用戶昵稱*/private String userName;/*** 用戶頭像*/private String userAvatar;/*** 簡介*/private String userProfile;/*** 用戶角色:user/admin*/private String userRole;private static final long serialVersionUID = 1L;
}

用戶查詢請求

需要繼承公共包中的 PageRequest 來支持分頁查詢:

@EqualsAndHashCode(callSuper = true)
@Data
public class UserQueryRequest extends PageRequest implements Serializable {/*** id*/private Long id;/*** 用戶昵稱*/private String userName;/*** 賬號*/private String userAccount;/*** 簡介*/private String userProfile;/*** 用戶角色:user/admin/ban*/private String userRole;private static final long serialVersionUID = 1L;
}@Data
public class PageRequest {/*** 當前頁號,默認值為 1*/private int current = 1;/*** 頁面大小,默認值為 10*/private int pageSize = 10;/*** 排序字段*/private String sortField;/*** 排序順序(默認降序)*/private String sortOrder = "descend";
}

用戶信息脫敏

由于要提供獲取用戶信息的接口,需要和獲取當前登錄用戶的接口一樣,對用戶信息進行脫敏

model.vo 包下新建 UserVO,表示脫敏后的用戶信息:

image-20250625124613198

@Data
public class UserVO implements Serializable {/*** id*/private Long id;/*** 賬號*/private String userAccount;/*** 用戶昵稱*/private String userName;/*** 用戶頭像*/private String userAvatar;/*** 用戶簡介*/private String userProfile;/*** 用戶角色:user/admin*/private String userRole;/*** 創建時間*/private Date createTime;private static final long serialVersionUID = 1L;
}

2. 服務開發


(1)獲取脫敏后的單個用戶信息

UserService 中編寫獲取脫敏后的單個用戶信息方法:

image-20250625131309697

/*** 獲取脫敏后的登錄用戶信息(普通用戶、管理員)** @param user 查詢數據庫得到的結果* @return 對查詢結果脫敏后, 得到的結果*/
UserVO getUserVO(User user);

image-20250625131840249

@Override
public UserVO getUserVO(User user) {if (user == null) {return null;}UserVO userVO = new UserVO();BeanUtils.copyProperties(user, userVO);return userVO;
}

(2)獲取脫敏后的用戶列表

UserService 中編寫獲取脫敏后的用戶列表方法:

image-20250625131857719

/*** 獲取脫敏后的登錄用戶信息列表(管理員)** @param userList 查詢數據庫, 得到的結果列表* @return 對查詢結果脫敏后, 得到的結果列表*/
List<User> getUserVOList(List<User> userList);

image-20250625131848334

@Override
public List<UserVO> getUserVOList(List<User> userList) {if(CollUtil.isEmpty(userList)){// CollUtil 是 Hutool 關于集合操作的工具類return new ArrayList<>();// 傳入的 userList 為空, 脫敏結果也是一個空數組}return userList.stream().map(this::getUserVO).collect(Collectors.toList());
}

image-20250625134209466


(3)獲取查詢條件

除了上述方法外,對于分頁查詢接口,需要根據用戶傳入的參數來構造 SQL 查詢。

由于使用了 MyBatis Plus 框架,無需手動拼接 SQL,而是通過構造 QueryWrapper 對象來生成 SQL 查詢。


可以在 UserService 中編寫一個方法,專門用于將查詢請求轉換為 QueryWrapper 對象:

image-20250625135045242

/*** 獲取查詢條件* @param userQueryRequest* @return*/
QueryWrapper<User> getQueryWrapper(UserQueryRequest userQueryRequest);

實現獲取查詢條件的接口方法:

image-20250625135220877

@Override
public QueryWrapper<User> getQueryWrapper(UserQueryRequest userQueryRequest) {if (userQueryRequest == null) {throw new BusinessException(ErrorCode.PARAMS_ERROR, "請求參數為空");}// 1. userQueryRequest.allgetLong id = userQueryRequest.getId();String userName = userQueryRequest.getUserName();String userAccount = userQueryRequest.getUserAccount();String userProfile = userQueryRequest.getUserProfile();String userRole = userQueryRequest.getUserRole();// 2. 下面兩個字段展示用不到// int current = userQueryRequest.getCurrent();// int pageSize = userQueryRequest.getPageSize();String sortField = userQueryRequest.getSortField();String sortOrder = userQueryRequest.getSortOrder();// 3. 將獲取到的值作為為請求參數來構造條件QueryWrapper<User> queryWrapper = new QueryWrapper<>();// 4. 傳入參數中, 獲取到的 id 不為空, 根據數據庫 id 字段, 查找傳入的 id 值queryWrapper.eq(ObjUtil.isNotEmpty(id), "id", id);queryWrapper.eq(StrUtil.isNotBlank(userRole), "userRole", userRole);// 5. 用到模糊查詢 like, 只要賬號, 用戶名....等字段內容部分相同, 就可以匹配出符合條件的結果queryWrapper.like(StrUtil.isNotBlank(userAccount), "userAccount", userAccount);queryWrapper.like(StrUtil.isNotBlank(userName), "userName", userName);queryWrapper.like(StrUtil.isNotBlank(userProfile), "userProfile", userProfile);// 6. 利用排序, 如果傳入參數中, 獲取到的排序規則參數不為空, 就按照升序排序 ascend 排序查找到的數據, 降序規則: descendqueryWrapper.orderBy(StrUtil.isNotEmpty(sortField), sortOrder.equals("ascend"), sortField);return queryWrapper;
}

補充說明,光標放在方法上,按ctrl + p 可以顯示方法的參數:

image-20250625140515217

orderBy() 的第二個參數,和我們自己傳入的參數sortOrder.equals("ascend") 是呼應的;


3. 接口開發


上述功能主要是常見的 CRUD(增刪改查) 操作,代碼實現相對簡單。需要注意以下幾點:

  1. 添加對應的權限注解。
  2. 做好參數校驗。

image-20250625142823137


(1)創建用戶

/*** 創建用戶* @param userAddRequest 創建用戶請求* @return 創建好用戶后, 在數據庫中存的該用戶的 id*/@PostMapping("/add")// 6. 該接口需要管理員權限
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Long> addUser(@RequestBody UserAddRequest userAddRequest) {// 1. 傳入參數為空, 調用自定義工具類, 拋出參數錯誤異常ThrowUtils.throwIf(userAddRequest == null, ErrorCode.PARAMS_ERROR);// 2. 把傳入的參數賦值給 User 對象User user = new User();BeanUtil.copyProperties(userAddRequest,user);// 3. 給 User 對象設置密碼默認值final String DEFAULT_PASSWORD = "12345678";String encryptPassword = userService.getEncryptPassword(DEFAULT_PASSWORD); // 對默認密碼加密user.setUserPassword(encryptPassword);// 4. 插入數據庫boolean result = userService.save(user);ThrowUtils.throwIf(result == false, ErrorCode.OPERATION_ERROR);  // 操作異常// 5. 統一返回結果 BaseResponse<Long>: user.getId() 對應 Long 類型  return ResultUtils.success(user.getId());
}

(2)根據 id 獲取用戶(僅管理員)

@GetMapping("/get")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<User> getUserById(long id) {ThrowUtils.throwIf(id <= 0, ErrorCode.PARAMS_ERROR);User user = userService.getById(id);ThrowUtils.throwIf(user == null, ErrorCode.NOT_FOUND_ERROR);return ResultUtils.success(user);
}

(3)根據 id 獲取包裝類

@GetMapping("/get/vo")
public BaseResponse<UserVO> getUserVOById(long id) {BaseResponse<User> response = getUserById(id);User user = response.getData();return ResultUtils.success(userService.getUserVO(user));
}

(4)刪除用戶

/*** 根據 id 刪除用戶, 僅管理員* @param deleteRequest* @return*/
@PostMapping("/delete")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Boolean> deleteUser(@RequestBody DeleteRequest deleteRequest) {if (deleteRequest == null || deleteRequest.getId() <= 0) {throw new BusinessException(ErrorCode.PARAMS_ERROR);}boolean b = userService.removeById(deleteRequest.getId());return ResultUtils.success(b);
}

(5)更新用戶

類比創建用戶接口:

  1. 參數校驗

    • 首先,對輸入的請求參數進行校驗。
  2. 創建用戶對象

    • 根據校驗通過的請求參數,創建一個新的 User 對象,并復制相關的屬性。
  3. 更新數據庫

    • 使用創建的 User 對象更新數據庫中的相應記錄。
  4. 處理更新結果

    • 根據數據庫更新操作的結果,采取相應的措施:
      • 如果更新失敗(例如,由于違反約束或技術問題),則拋出自定義異常
      • 如果更新成功,則封裝結果并返回給調用者。
@PostMapping("/update")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Boolean> updateUser(@RequestBody UserUpdateRequest userUpdateRequest) {if (userUpdateRequest == null || userUpdateRequest.getId() == null) {throw new BusinessException(ErrorCode.PARAMS_ERROR);}User user = new User();BeanUtils.copyProperties(userUpdateRequest, user);boolean result = userService.updateById(user);ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);return ResultUtils.success(true);
}

(6)分頁獲取用戶封裝列表(僅管理員)

注意事項

  1. 因為分頁查詢涉及到復雜的查詢,參數會比較復雜,POST GET 能發送的數據量更大
  2. BaseResponse<Page<UserVO>>中,要注意引入 Page 的包:image-20250625144809796
@PostMapping("/list/page/vo")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Page<UserVO>> listUserVOByPage(@RequestBody UserQueryRequest userQueryRequest){// 1. 校驗參數ThrowUtils.throwIf(userQueryRequest == null, ErrorCode.PARAMS_ERROR);// 2. 從參數中獲取當前頁碼、每頁顯示的記錄樹int current = userQueryRequest.getCurrent();    // 當前頁碼int pageSize = userQueryRequest.getPageSize();  // 每頁顯示的記錄數// 3. 構建分頁查詢條件并執行查詢,獲取分頁結果Page<User> userPage = userService.page(new Page<>(current, pageSize), // 創建分頁對象,包含當前頁碼和頁面大小userService.getQueryWrapper(userQueryRequest) // 根據請求參數構造查詢條件);// page(Page<current, pageSize> 對象, 查詢條件) 是 MyBatis-Plus 提供的一個方法, 用于執行分頁查詢// 4. 根據當前頁碼、頁面大小以及從數據庫查詢得到的總記錄數, 創建一個新的 Page<UserVO> 分頁對象, 用于后續存儲轉換后的 UserVO 列表Page<UserVO> userVOPage = new Page<>(current, pageSize, userPage.getTotal());// userPage.getTotal() 表示從數據庫查詢得到的總記錄數;// 這里從 userPage(一個 Page<User> 對象)獲取總記錄數, 因為 userPage 已經包含了這次查詢的分頁信息// 5. 對分頁結果進行脫敏List<UserVO> userVOList = userService.getUserVOList(userPage.getRecords());//  getRecords() 方法的作用是從一個分頁對象 userPage 中提取當前頁的記錄列表// 6. 將 userVOList 列表, 設置為 userVOPage 分頁對象中的當前頁記錄userVOPage.setRecords(userVOList);// setRecords() 用于設置分頁對象中, 當前頁的數據記錄列表return ResultUtils.success(userVOPage);
}

4.分頁功能修復


使用 Swagger 接口文檔 依次對上述接口進行測試,發現 listUserVOByPage 接口有一些問題!

image-20250625152555445


發送請求:

image-20250625152715782


分頁好像沒有生效,還是查出了全部數據。

image-20250625152842431

由于我們用的是 MyBatis Plus 來操作數據庫,所以需要通過 官方文檔 來查詢解決方案。


查閱后發現,原來必須要配置一個分頁插件。必須要注意,本項目使用的 v3.5.9 版本引入分頁插件的方式和之前不同!v3.5.9 版本后需要獨立安裝分頁插件依賴!!!

image-20250625153005552


pom.xml 中引入分頁插件依賴:

<!-- MyBatis Plus 分頁插件 -->
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-jsqlparser-4.9</artifactId>
</dependency>

image-20250625153844887

注意,這個插件適用于 JDK8,如果是其他版本的 JDK ,需要去官方文檔中引入其他版本的分頁插件;

光引入這一條,大概率是無法成功下載依賴的;


pom.xml 的依賴管理配置中補充 mybatis-plus-bom

<dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>${spring-boot.version}</version><type>pom</type><scope>import</scope></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-bom</artifactId><version>3.5.9</version><type>pom</type><scope>import</scope></dependency></dependencies>
</dependencyManagement>

image-20250625153615692


依賴下載成功后,根據官方文檔的提示,在 config 包下新建 MyBatis Plus 攔截器配置,添加分頁插件:

image-20250625154153227

@Configuration
@MapperScan("com.yupi.yupiturebackend.mapper")  
// 注意: 掃描路徑需要復制自己項目中 mapper 對應的包
public class MyBatisPlusConfig {/*** 攔截器配置** @return {@link MybatisPlusInterceptor}*/@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();// 分頁插件interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));return interceptor;}
}

重啟項目,重新調用登錄接口(項目使用本地 session,重啟需要重新登錄),再次調用分頁接口,這次就能正常完成分頁了:

image-20250625154603575


5. 數據精度修復


但是,在測試中,如果你打開 F12 控制臺,利用預覽來查看響應數據,就會發現另一個問題:

image-20250625155347955


id 的最后兩位好像都變成 0 了!

image-20250625155417253


但是在響應中、以及 Swagger 中查看,卻是正常的。

image-20250625155525922

這是由于前端 JS 的精度范圍有限,我們后端返回的 id 范圍過大,導致前端精度丟失,會影響前端頁面獲取到的數據結果。


為了解決這個問題,可以在后端 config 包下新建一個 全局 JSON 配置,將整個后端 Spring MVC 接口返回值的長整型數字轉換為字符串進行返回,從而集中解決問題。

image-20250625155812605

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import org.springframework.boot.jackson.JsonComponent;
import org.springframework.context.annotation.Bean;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;/*** Spring MVC Json 配置*/
@JsonComponent
public class JsonConfig {/*** 添加 Long 轉 json 精度丟失的配置*/@Beanpublic ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {ObjectMapper objectMapper = builder.createXmlMapper(false).build();SimpleModule module = new SimpleModule();module.addSerializer(Long.class, ToStringSerializer.instance);module.addSerializer(Long.TYPE, ToStringSerializer.instance);objectMapper.registerModule(module);return objectMapper;}
}

重啟項目進行測試,這次看到的 id 值就正常了。

image-20250625155944328

至此,用戶相關的后端接口開發完畢,大家可以按需完善上述代碼~~~


在這里插入圖片描述

在這里插入圖片描述

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/86929.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/86929.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/86929.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

閑庭信步使用SV搭建圖像測試平臺:第十三課——談談SV的數據類型

&#xff08;本系列只需要modelsim即可完成數字圖像的處理&#xff0c;每個工程都搭建了全自動化的仿真環境&#xff0c;只需要雙擊top_tb.bat文件就可以完成整個的仿真&#xff0c;大大降低了初學者的門檻&#xff01;&#xff01;&#xff01;&#xff01;如需要該系列的工程…

前端進階之路-從傳統前端到VUE-JS(第一期-VUE-JS環境配置)(Node-JS環境配置)(Node-JS/npm換源)

經過前面的傳統前端開發學習后&#xff0c;我們接下來進行前端的VUE-JS框架學習&#xff08;寫這篇文章的時候VUE-JS最新版是VUE3&#xff0c;所以默認為VUE3即可&#xff09; 首先&#xff0c;我們要配置Node-JS環境&#xff0c;雖然我們還不學習Node-JS但是Node-JS可以快速配…

Requests源碼分析:面試考察角度梳理

簡單描述執行流程 ?? Q:能簡單描述一下發送一個requests.get(url)請求時,在requests庫內部的主要執行流程嗎?(從調用get方法到收到響應) 入口委托: get() 方法內部調用 requests.request(GET, url)。Session 接管: request() 方法會獲取或隱式創建一個 Session 對象,并…

航天VR賦能,無人機總測實驗艙開啟高效新篇?

(一)沉浸式培訓體驗? 在傳統的無人機培訓中&#xff0c;操作人員主要通過理論學習和簡單的模擬操作來掌握技能。但這種方式存在很大局限性&#xff0c;難以讓操作人員真正感受無人機在復雜環境下的運行狀態。而航天 VR 技術引入到 VR 無人機總測實驗艙后&#xff0c;徹底改變了…

Kotlin 函數與 Lambda 表達式

今天繼續分享Kotlin學習內容。 目標&#xff1a;掌握函數定義、調用、參數傳遞&#xff0c;以及 Lambda 表達式的基礎用法 1. 函數&#xff1a;Kotlin 的代碼模塊化工具 定義&#xff1a;函數是可重復調用的代碼塊&#xff0c;用于封裝邏輯。 語法&#xff1a; fun 函數名(參…

[mcp-servers] docs | AI客戶端-MCP服務器-AI 架構

鏈接&#xff1a;https://github.com/punkpeye/awesome-mcp-servers 服務器調用 相關專欄&#xff1a;實現Json-Rpc docs&#xff1a;精選MCP服務器資源列表 本專欄為精選 模型上下文協議&#xff08;MCP&#xff09;服務器的列表。 MCP 是一種標準協議語言&#xff0c;允許*…

1688商品發布API:自動化上架與信息同步

一、1688商品發布API的核心功能與技術架構 1.1 API功能全景 1688商品發布API是1688開放平臺的核心組件之一&#xff0c;支持商品信息的自動化發布、編輯、上下架及庫存同步。其核心功能包括&#xff1a; 商品信息管理&#xff1a;支持商品標題、描述、價格、庫存、SKU&#…

如何在x86_64 Linux上部署Android Cuttlefish模擬器運行環境

0 軟硬件環境 x86_64服務器Ubuntu20.04 LTS參考&#xff1a;Cuttlefish 虛擬 Android 設備參考&#xff1a; 筆記&#xff1a;搭建 Cuttlefish 運行環境可以下載編好的android-cuttlefish&#xff1a;android-cuttlefish.tar.gz 1 系統采用Ubuntu20.04 LTS 2 搭建cuttlefish…

機器學習9——決策樹

決策樹 Intro 歸納學習&#xff08;Inductive Learning&#xff09;的目標&#xff1a;從訓練數據中學習一般規則&#xff0c;應用于未見過的數據。 決策樹是一個樹形結構&#xff0c;其中&#xff1a; 每個分支節點表示一個屬性上的選擇&#xff08;即決策條件&#xff09;。…

CppCon 2017 學習:The Asynchronous C++ Parallel Programming Model

清晰理解 Amdahl’s Law&#xff08;阿姆達爾定律&#xff09;&#xff0c;這是一條描述并行計算加速能力的核心定律。 定義公式&#xff1a; S 1 ( 1 ? P ) P N S \frac{1}{(1 - P) \frac{P}{N}} S(1?P)NP?1? S S S&#xff1a;加速比&#xff08;Speedup&#xff09…

60頁PPT實戰方案 | 大數據決策分析平臺建設全流程路徑圖

目錄 一、什么是大數據決策分析平臺&#xff1f; 二、為什么要做大數據決策分析平臺建設&#xff1f; 1. 數據已經成為“資源”&#xff0c;但多數組織還停留在“信息孤島” 2. 管理復雜度上升&#xff0c;傳統報表跟不上業務節奏 3. 外部環境不確定性高&#xff0c;倒逼企…

芯谷科技--降壓型DC-DC轉換器D4005

在現代電子設備中&#xff0c;電源管理芯片的性能直接關系到設備的穩定性和效率。D4005以其高效、穩定的性能和廣泛的應用范圍&#xff0c;成為眾多工程師在設計電源方案時的優選。 產品簡介 D4005 是一款高效降壓型 DC-DC 轉換器&#xff0c;具備固定 400KHz 開關頻率&#…

【51單片機節日彩燈控制器設計】2022-6-11

緣由單片機節日彩燈控制器設計-編程語言-CSDN問答 #include "reg52.h" sbit k0P1^2; sbit k1P1^3; sbit k2P1^4; sbit k3P1^5; bit k0,kk0; void main() {unsigned char Xd0;unsigned int ys0; while(1){if(k00&&Xd0){kk0;kP31;while(k00);}if(k10&&…

PyEcharts教程(010):天貓訂單數據可視化項目

文章目錄 1、讀取數據2、數據處理3、重復值查看4、缺失值查看5、PyEcharts可視化5.1 各個省份的訂單量5.2 時間序列分析5.3 每天訂單量統計可視化6、數據下載1、讀取數據 1??讀取數據: import pandas as pd from pyecharts import options as opts from pyecharts.charts …

Redis 持久化之 AOF 策略

1. 什么是 AOF AOF 是 append only file&#xff0c;AOF 文件中記錄了每次的操作指令&#xff0c;在啟動 Redis 時&#xff0c;會將 AOF 文件中的數據讀取出來以恢復數據。 2. 開啟 AOF Redis 默認關閉 AOF&#xff0c;可以通過將 Redis 配置文件中的 appendonly 設置為 ye…

實現OFD轉換PDF文件的實用方法

ODF格式的文件屬于國內新型的文件格式&#xff0c;一般應用在保密等級比較高的系統或者單位中&#xff0c;比如一般政務方面或者法律行業經常會用到這種類型的文件&#xff0c;但是有些時候我們把文件分享給別人的時候別人不一定能打開&#xff0c;這時候就需要把OFD文件轉換成…

JSON + 存儲過程:SaaS 架構下的統一接口與租戶定制之道

在多租戶 SaaS 系統中&#xff0c;不同客戶往往有差異化的業務邏輯、字段要求與流程規則。傳統“統一模型 配置參數”的開發模式&#xff0c;雖然具有可控性&#xff0c;但在高度動態、合作多樣化的場景下&#xff0c;逐漸暴露出擴展困難、上線周期長、定制成本高等問題。 隨…

各種常用的串口助手工具分享

記錄一篇常用串口工具的文章 工具的下載鏈接&#xff1a;https://download.csdn.net/download/m0_59415345/91204823?spm1001.2014.3001.5503 各工具的使用操作說明參考嵌入式hxydj博主的文章&#xff1a;https://blog.csdn.net/qq_20222919/article/details/117038284

AVL樹的簡潔寫法

文章目錄 零、寫在前面一、AVL 樹定義1.1 性質1.2 樹高的證明 二、AVL樹實現&#xff08;AVL樹實現名次樹&#xff09;2.1 節點定義2.2 左/右旋轉2.3 zig-zag / zag-zig 雙旋2.4 重平衡函數2.5 插入2.6 刪除2.7 排名查詢2.8 查前驅/后繼2.9 查第 k 小2.10 完整代碼 三、online …

紅外圖像增強(dde):基于“基礎層-細節層”分解的增強算法

1、引言 與可見光圖像相比&#xff0c;紅外熱成像捕捉的是物體表面的溫度分布&#xff0c;其原始數據&#xff08;通常為12位或14位&#xff09;包含了極寬的溫度動態范圍。然而&#xff0c;人眼能夠感知的灰度范圍以及顯示設備能夠展示的灰度級&#xff08;通常為8位&#xf…