類別管理–根據(jù)id查詢類別詳情–持久層
13.1. 規(guī)劃SQL語(yǔ)句
本次需要執(zhí)行的SQL語(yǔ)句大致是:
select * from pms_category where id=?
關(guān)于字段列表,應(yīng)該包括:
id, name, parent_id, depth, keywords, sort, icon, enable, is_parent, is_display
13.2. 抽象方法(可能需要?jiǎng)?chuàng)建VO類)
在csmall-pojo的根包下的vo包下創(chuàng)建CategoryDetailsVO類,封裝以上設(shè)計(jì)的字段對(duì)應(yīng)的屬性:
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. 測(cè)試
@Test@Sql({“classpath:truncate.sql”, “classpath:insert_data.sql”})public void testGetDetailsByIdSuccessfully() { // 測(cè)試數(shù)據(jù) Long id = 1L; // 斷言不會(huì)拋出異常 assertDoesNotThrow(() -> { // 執(zhí)行查詢 Object category = mapper.getDetailsById(id); // 斷言查詢結(jié)果不為null assertNotNull(category);});}@Test@Sql({“classpath:truncate.sql”})public void testGetDetailsByIdFailBecauseNotFound() { // 測(cè)試數(shù)據(jù) Long id = -1L; // 斷言不會(huì)拋出異常 assertDoesNotThrow(() -> { // 執(zhí)行查詢 Object category = mapper.getDetailsById(id); // 斷言查詢結(jié)果為null assertNull(category);});}
14. 類別管理–根據(jù)id查詢類別詳情–業(yè)務(wù)邏輯層
14.1. 接口和抽象方法
在ICategoryService中添加:
CategoryDetailsVO getDetailsById(Long id);
14.2. 實(shí)現(xiàn)
在CategoryServiceImpl中執(zhí)行查詢并返回。
14.3. 測(cè)試
@Test@Sql({“classpath:truncate.sql”, “classpath:insert_data.sql”})public void testGetDetailsByIdSuccessfully() { // 測(cè)試數(shù)據(jù) Long id = 1L; // 斷言不拋出異常 assertDoesNotThrow(() -> { service.getDetailsById(id); });}@Test@Sql({“classpath:truncate.sql”})public void testGetDetailsByIdFailBecauseNotFound() { // 測(cè)試數(shù)據(jù) Long id = -1L; // 斷言拋出異常 assertThrows(ServiceException.class, () -> { service.getDetailsById(id); });}
15. 類別管理–根據(jù)id查詢類別詳情–控制器層
在CategoryController中添加:
@GetMapping(“/{id}”)public JsonResult getDetailsById(@PathVariable Long id) { CategoryDetailsVO category = categoryService.getDetailsById(id); return JsonResult.ok(category);}
在CategoryControllerTests中測(cè)試:
@Test@Sql({“classpath:truncate.sql”, “classpath:insert_data.sql”})public void testGetDetailsByIdSuccessfully() throws Exception { // 準(zhǔn)備測(cè)試數(shù)據(jù),注意:此次沒(méi)有提交必要的name屬性值 String id = “1”; // 請(qǐng)求路徑,不需要寫(xiě)協(xié)議、服務(wù)器主機(jī)和端口號(hào) String url = “/categories/” + id; // 執(zhí)行測(cè)試 // 以下代碼相對(duì)比較固定 mockMvc.perform( // 執(zhí)行發(fā)出請(qǐng)求 MockMvcRequestBuilders.get(url) // 根據(jù)請(qǐng)求方式?jīng)Q定調(diào)用的方法 .contentType(MediaType.APPLICATION_FORM_URLENCODED) // 請(qǐng)求數(shù)據(jù)的文檔類型,例如:application/json; charset=utf-8 .accept(MediaType.APPLICATION_JSON)) // 接收的響應(yīng)結(jié)果的文檔類型,注意:perform()方法到此結(jié)束 .andExpect( // 預(yù)判結(jié)果,類似斷言 MockMvcResultMatchers .jsonPath(“state”) // 預(yù)判響應(yīng)的JSON結(jié)果中將有名為state的屬性 .value(State.OK.getValue())) // 預(yù)判響應(yīng)的JSON結(jié)果中名為state的屬性的值,注意:andExpect()方法到此結(jié)束 .andDo( // 需要執(zhí)行某任務(wù) MockMvcResultHandlers.print()); // 打印日志}@Test@Sql({“classpath:truncate.sql”, “classpath:insert_data.sql”})public void testGetDetailsByIdFailBecauseNotFound() throws Exception { // 準(zhǔn)備測(cè)試數(shù)據(jù),注意:此次沒(méi)有提交必要的name屬性值 String id = “9999999999”; // 請(qǐng)求路徑,不需要寫(xiě)協(xié)議、服務(wù)器主機(jī)和端口號(hào) String url = “/categories/” + id; // 執(zhí)行測(cè)試 // 以下代碼相對(duì)比較固定 mockMvc.perform( // 執(zhí)行發(fā)出請(qǐng)求 MockMvcRequestBuilders.get(url) // 根據(jù)請(qǐng)求方式?jīng)Q定調(diào)用的方法 .contentType(MediaType.APPLICATION_FORM_URLENCODED) // 請(qǐng)求數(shù)據(jù)的文檔類型,例如:application/json; charset=utf-8 .accept(MediaType.APPLICATION_JSON)) // 接收的響應(yīng)結(jié)果的文檔類型,注意:perform()方法到此結(jié)束 .andExpect( // 預(yù)判結(jié)果,類似斷言 MockMvcResultMatchers .jsonPath(“state”) // 預(yù)判響應(yīng)的JSON結(jié)果中將有名為state的屬性 .value(State.ERR_CATEGORY_NOT_FOUND.getValue())) // 預(yù)判響應(yīng)的JSON結(jié)果中名為state的屬性的值,注意:andExpect()方法到此結(jié)束 .andDo( // 需要執(zhí)行某任務(wù) MockMvcResultHandlers.print()); // 打印日志}
管理員相關(guān)數(shù)據(jù)表
管理員及權(quán)限的管理,涉及的數(shù)據(jù)表有:
— 數(shù)據(jù)庫(kù):mall_ams– 權(quán)限表:創(chuàng)建數(shù)據(jù)表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 ‘自定義排序序號(hào)’, gmt_create datetime default null comment ‘數(shù)據(jù)創(chuàng)建時(shí)間’, gmt_modified datetime default null comment ‘數(shù)據(jù)最后修改時(shí)間’, primary key (id)) comment ‘權(quán)限表’ charset utf8mb4;– 權(quán)限表:插入測(cè)試數(shù)據(jù)insert into ams_permission (name, value, description) values(‘商品-商品管理-讀取’, ‘/pms/product/read’, ‘讀取商品數(shù)據(jù),含列表、詳情、查詢等’),(‘商品-商品管理-編輯’, ‘/pms/product/update’, ‘修改商品數(shù)據(jù)’),(‘商品-商品管理-刪除’, ‘/pms/product/delete’, ‘刪除商品數(shù)據(jù)’),(‘后臺(tái)管理-管理員-讀取’, ‘/ams/admin/read’, ‘讀取管理員數(shù)據(jù),含列表、詳情、查詢等’),(‘后臺(tái)管理-管理員-編輯’, ‘/ams/admin/update’, ‘編輯管理員數(shù)據(jù)’),(‘后臺(tái)管理-管理員-刪除’, ‘/ams/admin/delete’, ‘刪除管理員數(shù)據(jù)’);– 角色表:創(chuàng)建數(shù)據(jù)表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 ‘自定義排序序號(hào)’, gmt_create datetime default null comment ‘數(shù)據(jù)創(chuàng)建時(shí)間’, gmt_modified datetime default null comment ‘數(shù)據(jù)最后修改時(shí)間’, primary key (id)) comment ‘角色表’ charset utf8mb4;– 角色表:插入測(cè)試數(shù)據(jù)insert into ams_role (name) values (‘超級(jí)管理員’), (‘系統(tǒng)管理員’), (‘商品管理員’), (‘訂單管理員’);– 角色權(quán)限關(guān)聯(lián)表:創(chuàng)建數(shù)據(jù)表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 ‘權(quán)限id’, gmt_create datetime default null comment ‘數(shù)據(jù)創(chuàng)建時(shí)間’, gmt_modified datetime default null comment ‘數(shù)據(jù)最后修改時(shí)間’, primary key (id)) comment ‘角色權(quán)限關(guān)聯(lián)表’ charset utf8mb4;– 角色權(quán)限關(guān)聯(lián)表:插入測(cè)試數(shù)據(jù)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)建數(shù)據(jù)表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 ‘手機(jī)號(hào)碼’, 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 ‘累計(jì)登錄次數(shù)(冗余)’, gmt_last_login datetime default null comment ‘最后登錄時(shí)間(冗余)’, gmt_create datetime default null comment ‘數(shù)據(jù)創(chuàng)建時(shí)間’, gmt_modified datetime default null comment ‘數(shù)據(jù)最后修改時(shí)間’, primary key (id)) comment ‘管理員表’ charset utf8mb4;– 管理員表:插入測(cè)試數(shù)據(jù)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’, ‘超級(jí)管理員’, 1), (‘nobody’, ‘1234’, ‘無(wú)名’, ‘liucs@celinf.cn’, null, 0);– 管理員角色關(guān)聯(lián)表:創(chuàng)建數(shù)據(jù)表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 ‘數(shù)據(jù)創(chuàng)建時(shí)間’, gmt_modified datetime default null comment ‘數(shù)據(jù)最后修改時(shí)間’, primary key (id)) comment ‘管理員角色關(guān)聯(lián)表’ charset utf8mb4;– 管理員角色關(guān)聯(lián)表:插入測(cè)試數(shù)據(jù)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的管理員的權(quán)限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)建數(shù)據(jù)表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 ‘瀏覽器內(nèi)核’, gmt_login datetime default null comment ‘登錄時(shí)間’, gmt_create datetime default null comment ‘數(shù)據(jù)創(chuàng)建時(shí)間’, gmt_modified datetime default null comment ‘數(shù)據(jù)最后修改時(shí)間’, primary key (id)) comment ‘管理員登錄日志表’ charset utf8mb4;– 管理員登錄日志表:插入測(cè)試數(shù)據(jù)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());– 查看數(shù)據(jù)表結(jié)構(gòu)desc ams_permission; desc ams_role; desc ams_role_permission; desc ams_admin; desc ams_admin_role; desc ams_login_log;
當(dāng)某個(gè)管理員嘗試登錄時(shí),必須實(shí)現(xiàn)”根據(jù)用戶名查詢此管理員的信息,至少包括id、密碼、權(quán)限“,需要執(zhí)行的SQL語(yǔ)句大致是:
— 管理員表 admin– 角色表 role– 管理員與角色關(guān)聯(lián)表 admin_role (admin_id, role_id)– 權(quán)限表 permission– 角色與權(quán)限關(guān)聯(lián)表 role_permission (role_id, permission_id)– 【根據(jù)用戶名查詢管理員,且必須查出對(duì)應(yīng)的權(quán)限】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’;
接下來(lái),在根項(xiàng)目中創(chuàng)建csmall-admin模塊(與csmall-product類似),并在其下創(chuàng)建csmall-admin-service和csmall-admin-webapi這2個(gè)子模塊(與csmall-product的2個(gè)子模塊類似),然后,盡量在csmall-admin-webapi中實(shí)現(xiàn)以上查詢功能:
public interface AdminMapper { AdminLoginVO findByUsername(String username);}
最后,關(guān)于Key的使用,通常建議使用冒號(hào)區(qū)分多層次,類似URL的設(shè)計(jì)方式,例如:
- 類別列表的Key:categories:list或categories
- 某個(gè)id(9000)對(duì)應(yīng)的類別的Key:categories:item:9000
關(guān)于用戶身份認(rèn)證與授權(quán)
Spring Security是用于解決認(rèn)證與授權(quán)的框架。
在根項(xiàng)目下創(chuàng)建新的`csmall-passport`子模塊,最基礎(chǔ)的依賴項(xiàng)包括`spring-boot-starter-web`與`spring-boot-starter-security`(為避免默認(rèn)存在的測(cè)試類出錯(cuò),應(yīng)該保留測(cè)試的依賴項(xiàng)`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
調(diào)整完成后,即可啟動(dòng)項(xiàng)目,在啟動(dòng)的日志中,可以看到類似以下內(nèi)容:
Using generated security password: 2abb9119-b5bb-4de9-8584-9f893e4a5a92
Spring Security有默認(rèn)登錄的賬號(hào)和密碼(以上提示的值),密碼是隨機(jī)的,每次啟動(dòng)項(xiàng)目都會(huì)不同。
Spring Security默認(rèn)要求所有的請(qǐng)求都是必須先登錄才允許的訪問(wèn),可以使用默認(rèn)的用戶名`user`和自動(dòng)生成的隨機(jī)密碼來(lái)登錄。在測(cè)試登錄時(shí),在瀏覽器訪問(wèn)當(dāng)前主機(jī)的任意網(wǎng)址都可以(包括不存在的資源),會(huì)自動(dòng)跳轉(zhuǎn)到登錄頁(yè)(是由Spring Security提供的,默認(rèn)的URL是:http://localhost:8080/login),當(dāng)?shù)卿洺晒?,?huì)自動(dòng)跳轉(zhuǎn)到此前訪問(wèn)的URL(跳轉(zhuǎn)登錄頁(yè)之前的URL),另外,還可以通過(guò) http://localhost:8080/logout 退出登錄。
Spring Security的依賴項(xiàng)中包括了Bcrypt算法的工具類,Bcrypt是一款非常優(yōu)秀的密碼加密工具,適用于對(duì)需要存儲(chǔ)下來(lái)的密碼進(jìn)行加密處理。
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能使用數(shù)據(jù)庫(kù)中的信息(數(shù)據(jù)庫(kù)中的用戶名與密碼)來(lái)驗(yàn)證用戶身份(認(rèn)證),首先,必須實(shí)現(xiàn)“根據(jù)用戶名查詢此用戶的登錄信息(應(yīng)該包括權(quán)限信息)”的查詢功能,要實(shí)現(xiàn)此查詢,需要執(zhí)行的SQL語(yǔ)句大致是:
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’;
要在當(dāng)前模塊(`csmall-passport`)中實(shí)現(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`中添加對(duì)`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
– 完成后,還應(yīng)該編寫(xiě)并執(zhí)行測(cè)試
根據(jù)有效的用戶名查詢出的結(jié)果例如:
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 ])
學(xué)習(xí)記錄,如有侵權(quán)請(qǐng)聯(lián)系刪除