類別管理–根據id查詢類別詳情–持久層
13.1. 規(guī)劃SQL語句
本次需要執(zhí)行的SQL語句大致是:
select * from pms_category where id=?
關于字段列表,應該包括:
id, name, parent_id, depth, keywords, sort, icon, enable, is_parent, is_display
13.2. 抽象方法(可能需要創(chuàng)建VO類)
在csmall-pojo的根包下的vo包下創(chuàng)建CategoryDetailsVO類,封裝以上設計的字段對應的屬性:
import lombok.Data;import java.io.Serializable;@Datapublic class CategoryDetailsVO implements Serializable { private Long id; private String name; private Long parentId; private Integer depth; private String keywords; private Integer sort; private String icon; private Integer enable; private Integer isParent; private Integer isDisplay;}
在CategoryMapper接口中添加:
CategoryDetailsVO getDetailsById(Long id);
13.3. 在XML中配置SQL
在CategoryMapper.xml中添加配置:
select from pms_category where id=#{id} id, name, parent_id, depth, keywords, sort, icon, enable, is_parent, is_display
13.4. 測試
@Test@Sql({“classpath:truncate.sql”, “classpath:insert_data.sql”})public void testGetDetailsByIdSuccessfully() { // 測試數據 Long id = 1L; // 斷言不會拋出異常 assertDoesNotThrow(() -> { // 執(zhí)行查詢 Object category = mapper.getDetailsById(id); // 斷言查詢結果不為null assertNotNull(category);});}@Test@Sql({“classpath:truncate.sql”})public void testGetDetailsByIdFailBecauseNotFound() { // 測試數據 Long id = -1L; // 斷言不會拋出異常 assertDoesNotThrow(() -> { // 執(zhí)行查詢 Object category = mapper.getDetailsById(id); // 斷言查詢結果為null assertNull(category);});}
14. 類別管理–根據id查詢類別詳情–業(yè)務邏輯層
14.1. 接口和抽象方法
在ICategoryService中添加:
CategoryDetailsVO getDetailsById(Long id);
14.2. 實現(xiàn)
在CategoryServiceImpl中執(zhí)行查詢并返回。
14.3. 測試
@Test@Sql({“classpath:truncate.sql”, “classpath:insert_data.sql”})public void testGetDetailsByIdSuccessfully() { // 測試數據 Long id = 1L; // 斷言不拋出異常 assertDoesNotThrow(() -> { service.getDetailsById(id); });}@Test@Sql({“classpath:truncate.sql”})public void testGetDetailsByIdFailBecauseNotFound() { // 測試數據 Long id = -1L; // 斷言拋出異常 assertThrows(ServiceException.class, () -> { service.getDetailsById(id); });}
15. 類別管理–根據id查詢類別詳情–控制器層
在CategoryController中添加:
@GetMapping(“/{id}”)public JsonResult getDetailsById(@PathVariable Long id) { CategoryDetailsVO category = categoryService.getDetailsById(id); return JsonResult.ok(category);}
在CategoryControllerTests中測試:
@Test@Sql({“classpath:truncate.sql”, “classpath:insert_data.sql”})public void testGetDetailsByIdSuccessfully() throws Exception { // 準備測試數據,注意:此次沒有提交必要的name屬性值 String id = “1”; // 請求路徑,不需要寫協(xié)議、服務器主機和端口號 String url = “/categories/” + id; // 執(zhí)行測試 // 以下代碼相對比較固定 mockMvc.perform( // 執(zhí)行發(fā)出請求 MockMvcRequestBuilders.get(url) // 根據請求方式決定調用的方法 .contentType(MediaType.APPLICATION_FORM_URLENCODED) // 請求數據的文檔類型,例如:application/json; charset=utf-8 .accept(MediaType.APPLICATION_JSON)) // 接收的響應結果的文檔類型,注意:perform()方法到此結束 .andExpect( // 預判結果,類似斷言 MockMvcResultMatchers .jsonPath(“state”) // 預判響應的JSON結果中將有名為state的屬性 .value(State.OK.getValue())) // 預判響應的JSON結果中名為state的屬性的值,注意:andExpect()方法到此結束 .andDo( // 需要執(zhí)行某任務 MockMvcResultHandlers.print()); // 打印日志}@Test@Sql({“classpath:truncate.sql”, “classpath:insert_data.sql”})public void testGetDetailsByIdFailBecauseNotFound() throws Exception { // 準備測試數據,注意:此次沒有提交必要的name屬性值 String id = “9999999999”; // 請求路徑,不需要寫協(xié)議、服務器主機和端口號 String url = “/categories/” + id; // 執(zhí)行測試 // 以下代碼相對比較固定 mockMvc.perform( // 執(zhí)行發(fā)出請求 MockMvcRequestBuilders.get(url) // 根據請求方式決定調用的方法 .contentType(MediaType.APPLICATION_FORM_URLENCODED) // 請求數據的文檔類型,例如:application/json; charset=utf-8 .accept(MediaType.APPLICATION_JSON)) // 接收的響應結果的文檔類型,注意:perform()方法到此結束 .andExpect( // 預判結果,類似斷言 MockMvcResultMatchers .jsonPath(“state”) // 預判響應的JSON結果中將有名為state的屬性 .value(State.ERR_CATEGORY_NOT_FOUND.getValue())) // 預判響應的JSON結果中名為state的屬性的值,注意:andExpect()方法到此結束 .andDo( // 需要執(zhí)行某任務 MockMvcResultHandlers.print()); // 打印日志}
管理員相關數據表
管理員及權限的管理,涉及的數據表有:
— 數據庫:mall_ams– 權限表:創(chuàng)建數據表drop table if exists ams_permission;create table ams_permission ( id bigint unsigned auto_increment, name varchar(50) default null comment ‘名稱’, value varchar(255) default null comment ‘值’, description varchar(255) default null comment ‘描述’, sort tinyint unsigned default 0 comment ‘自定義排序序號’, gmt_create datetime default null comment ‘數據創(chuàng)建時間’, gmt_modified datetime default null comment ‘數據最后修改時間’, primary key (id)) comment ‘權限表’ charset utf8mb4;– 權限表:插入測試數據insert into ams_permission (name, value, description) values(‘商品-商品管理-讀取’, ‘/pms/product/read’, ‘讀取商品數據,含列表、詳情、查詢等’),(‘商品-商品管理-編輯’, ‘/pms/product/update’, ‘修改商品數據’),(‘商品-商品管理-刪除’, ‘/pms/product/delete’, ‘刪除商品數據’),(‘后臺管理-管理員-讀取’, ‘/ams/admin/read’, ‘讀取管理員數據,含列表、詳情、查詢等’),(‘后臺管理-管理員-編輯’, ‘/ams/admin/update’, ‘編輯管理員數據’),(‘后臺管理-管理員-刪除’, ‘/ams/admin/delete’, ‘刪除管理員數據’);– 角色表:創(chuàng)建數據表drop table if exists ams_role;create table ams_role ( id bigint unsigned auto_increment, name varchar(50) default null comment ‘名稱’, description varchar(255) default null comment ‘描述’, sort tinyint unsigned default 0 comment ‘自定義排序序號’, gmt_create datetime default null comment ‘數據創(chuàng)建時間’, gmt_modified datetime default null comment ‘數據最后修改時間’, primary key (id)) comment ‘角色表’ charset utf8mb4;– 角色表:插入測試數據insert into ams_role (name) values (‘超級管理員’), (‘系統(tǒng)管理員’), (‘商品管理員’), (‘訂單管理員’);– 角色權限關聯(lián)表:創(chuàng)建數據表drop table if exists ams_role_permission;create table ams_role_permission ( id bigint unsigned auto_increment, role_id bigint unsigned default null comment ‘角色id’, permission_id bigint unsigned default null comment ‘權限id’, gmt_create datetime default null comment ‘數據創(chuàng)建時間’, gmt_modified datetime default null comment ‘數據最后修改時間’, primary key (id)) comment ‘角色權限關聯(lián)表’ charset utf8mb4;– 角色權限關聯(lián)表:插入測試數據insert into ams_role_permission (role_id, permission_id) values (1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (2, 1), (2, 2), (2, 3), (2, 4), (2, 5), (2, 6), (3, 1), (3, 2), (3, 3);– 管理員表:創(chuàng)建數據表drop table if exists ams_admin;create table ams_admin ( id bigint unsigned auto_increment, username varchar(50) default null unique comment ‘用戶名’, password char(64) default null comment ‘密碼(密文)’, nickname varchar(50) default null comment ‘昵稱’, avatar varchar(255) default null comment ‘頭像URL’, phone varchar(50) default null unique comment ‘手機號碼’, email varchar(50) default null unique comment ‘電子郵箱’, description varchar(255) default null comment ‘描述’, is_enable tinyint unsigned default 0 comment ‘是否啟用,1=啟用,0=未啟用’, last_login_ip varchar(50) default null comment ‘最后登錄IP地址(冗余)’, login_count int unsigned default 0 comment ‘累計登錄次數(冗余)’, gmt_last_login datetime default null comment ‘最后登錄時間(冗余)’, gmt_create datetime default null comment ‘數據創(chuàng)建時間’, gmt_modified datetime default null comment ‘數據最后修改時間’, primary key (id)) comment ‘管理員表’ charset utf8mb4;– 管理員表:插入測試數據insert into ams_admin (username, password, nickname, email, description, is_enable) values (‘root’, ‘1234’, ‘root’, ‘root@celinf.cn’, ‘最高管理員’, 1), (‘super_admin’, ‘1234’, ‘administrator’, ‘admin@celinf.cn’, ‘超級管理員’, 1), (‘nobody’, ‘1234’, ‘無名’, ‘liucs@celinf.cn’, null, 0);– 管理員角色關聯(lián)表:創(chuàng)建數據表drop table if exists ams_admin_role;create table ams_admin_role ( id bigint unsigned auto_increment, admin_id bigint unsigned default null comment ‘管理員id’, role_id bigint unsigned default null comment ‘角色id’, gmt_create datetime default null comment ‘數據創(chuàng)建時間’, gmt_modified datetime default null comment ‘數據最后修改時間’, primary key (id)) comment ‘管理員角色關聯(lián)表’ charset utf8mb4;– 管理員角色關聯(lián)表:插入測試數據insert into ams_admin_role (admin_id, role_id) values (1, 1), (1, 2), (1, 3), (2, 2), (2, 3), (2, 4), (3, 3);– 查詢示例:查詢id=1的管理員的權限select distinct ams_permission.value from ams_permissionleft join ams_role_permission on ams_role_permission.permission_id=ams_permission.idleft join ams_role on ams_role_permission.role_id=ams_role.idleft join ams_admin_role on ams_admin_role.role_id=ams_role.idleft join ams_admin on ams_admin_role.admin_id=ams_admin.idwhere ams_admin.id=1order by ams_permission.value;– 管理員登錄日志表:創(chuàng)建數據表drop table if exists ams_login_log;create table ams_login_log ( id bigint unsigned auto_increment, admin_id bigint unsigned default null comment ‘管理員id’, username varchar(50) default null comment ‘管理員用戶名(冗余)’, nickname varchar(50) default null comment ‘管理員昵稱(冗余)’, ip varchar(50) default null comment ‘登錄IP地址’, user_agent varchar(255) default null comment ‘瀏覽器內核’, gmt_login datetime default null comment ‘登錄時間’, gmt_create datetime default null comment ‘數據創(chuàng)建時間’, gmt_modified datetime default null comment ‘數據最后修改時間’, primary key (id)) comment ‘管理員登錄日志表’ charset utf8mb4;– 管理員登錄日志表:插入測試數據insert into ams_login_log (admin_id, username, nickname, ip, user_agent, gmt_login) values (1, ‘root’, ‘root’, ‘127.0.0.1’, ‘Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Safari/605.1.15’, DATE_SUB(NOW(), interval 1 day)), (2, ‘root’, ‘root’, ‘127.0.0.1’, ‘Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Safari/605.1.15’, DATE_SUB(NOW(), interval 12 hour)), (3, ‘root’, ‘root’, ‘127.0.0.1’, ‘Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Safari/605.1.15’, NOW());– 查看數據表結構desc ams_permission; desc ams_role; desc ams_role_permission; desc ams_admin; desc ams_admin_role; desc ams_login_log;
當某個管理員嘗試登錄時,必須實現(xiàn)”根據用戶名查詢此管理員的信息,至少包括id、密碼、權限“,需要執(zhí)行的SQL語句大致是:
— 管理員表 admin– 角色表 role– 管理員與角色關聯(lián)表 admin_role (admin_id, role_id)– 權限表 permission– 角色與權限關聯(lián)表 role_permission (role_id, permission_id)– 【根據用戶名查詢管理員,且必須查出對應的權限】select ams_admin.id, ams_admin.username, ams_admin.password, ams_admin.is_enable, ams_permission.valuefrom ams_adminleft join ams_admin_role on ams_admin.id = ams_admin_role.admin_idleft join ams_role_permission on ams_admin_role.role_id = ams_role_permission.role_idleft join ams_permission on ams_role_permission.permission_id = ams_permission.idwhere username=’root’;
接下來,在根項目中創(chuàng)建csmall-admin模塊(與csmall-product類似),并在其下創(chuàng)建csmall-admin-service和csmall-admin-webapi這2個子模塊(與csmall-product的2個子模塊類似),然后,盡量在csmall-admin-webapi中實現(xiàn)以上查詢功能:
public interface AdminMapper { AdminLoginVO findByUsername(String username);}
最后,關于Key的使用,通常建議使用冒號區(qū)分多層次,類似URL的設計方式,例如:
- 類別列表的Key:categories:list或categories
- 某個id(9000)對應的類別的Key:categories:item:9000
關于用戶身份認證與授權
Spring Security是用于解決認證與授權的框架。
在根項目下創(chuàng)建新的`csmall-passport`子模塊,最基礎的依賴項包括`spring-boot-starter-web`與`spring-boot-starter-security`(為避免默認存在的測試類出錯,應該保留測試的依賴項`spring-boot-starter-test`),完整的`csmall-passwort`的`pom.xml`為:
org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-security org.springframework.boot spring-boot-starter-test test
調整完成后,即可啟動項目,在啟動的日志中,可以看到類似以下內容:
Using generated security password: 2abb9119-b5bb-4de9-8584-9f893e4a5a92
Spring Security有默認登錄的賬號和密碼(以上提示的值),密碼是隨機的,每次啟動項目都會不同。
Spring Security默認要求所有的請求都是必須先登錄才允許的訪問,可以使用默認的用戶名`user`和自動生成的隨機密碼來登錄。在測試登錄時,在瀏覽器訪問當前主機的任意網址都可以(包括不存在的資源),會自動跳轉到登錄頁(是由Spring Security提供的,默認的URL是:http://localhost:8080/login),當登錄成功后,會自動跳轉到此前訪問的URL(跳轉登錄頁之前的URL),另外,還可以通過 http://localhost:8080/logout 退出登錄。
Spring Security的依賴項中包括了Bcrypt算法的工具類,Bcrypt是一款非常優(yōu)秀的密碼加密工具,適用于對需要存儲下來的密碼進行加密處理。
import org.junit.jupiter.api.Test;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;public class BcryptPasswordEncoderTests { private BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); @Test public void testEncode() { // 原文相同的情況,每次加密得到的密文都不同 for (int i = 0; i < 10; i++) { String rawPassword = "123456"; String encodedPassword = passwordEncoder.encode(rawPassword); System.out.println("rawPassword = " + rawPassword); System.out.println("encodedPassword = " + encodedPassword); } // rawPassword = 123456 // encodedPassword = $2a$10$HWuJ9WgPazrwg9.isaae4u7XdP7ohH7LetDwdlTWuPC4ZAvG.Uc7W // encodedPassword = $2a$10$rOwgZMpDvZ3Kn7CxHWiEbeC6bQMGtfX.VYc9DCzx9BxkWymX6FbrS } @Test public void testMatches() { String rawPassword = "123456"; String encodedPassword = "$2a$10$hI4wweFOGJ7FMduSmCjNBexbKFOjYMWl8hkug0n0k1LNR5vEyhhMW"; boolean matchResult = passwordEncoder.matches(rawPassword, encodedPassword); System.out.println("match result : " + matchResult); }}
如果要使得Spring Security能使用數據庫中的信息(數據庫中的用戶名與密碼)來驗證用戶身份(認證),首先,必須實現(xiàn)“根據用戶名查詢此用戶的登錄信息(應該包括權限信息)”的查詢功能,要實現(xiàn)此查詢,需要執(zhí)行的SQL語句大致是:
selectams_admin.id, ams_admin.username, ams_admin.password, ams_admin.is_enable, ams_permission.valuefrom ams_admin left join ams_admin_role on ams_admin.id = ams_admin_role.admin_id left join ams_role_permission on ams_admin_role.role_id = ams_role_permission.role_id left join ams_permission on ams_role_permission.permission_id = ams_permission.idwhere username=’root’;
要在當前模塊(`csmall-passport`)中實現(xiàn)此查詢功能,需要:
@Datapublic class AdminLoginVO implements Serializable { private Long id; private String username; private String password; private Integer isEnable; private List permissions;}
– [`csmall-passport`] 在`pom.xml`中添加對`csmall-pojo`的依賴
– [`csmall-passport`] 在`src/main/java`下的`cn.celinf.csmall.passport`包下創(chuàng)建`mapper.AdminMapper.java`接口
– [`csmall-passport`] 在接口中添加抽象方法:
AdminLoginVO getLoginInfoByUsername(String username);
– 在`src/main/resources`下創(chuàng)建`mapper`文件夾,并在此文件夾下粘貼得到`AdminMapper.xml`
– 在`AdminMapper.xml`中配置以上抽象方法映射的SQL查詢:
selectfrom ams_adminleft join ams_admin_roleon ams_admin.id = ams_admin_role.admin_idleft join ams_role_permissionon ams_admin_role.role_id = ams_role_permission.role_idleft join ams_permissionon ams_role_permission.permission_id = ams_permission.idwhere username=#{username}ams_admin.id,ams_admin.username,ams_admin.password,ams_admin.is_enable,ams_permission.value
– 完成后,還應該編寫并執(zhí)行測試
根據有效的用戶名查詢出的結果例如:
AdminLoginVO( id=1, username=root, password=1234, isEnable=1, permissions=[ /pms/product/read, /pms/product/update, /pms/product/delete, /ams/admin/read, /ams/admin/update, /ams/admin/delete ])
學習記錄,如有侵權請聯(lián)系刪除