You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1277 lines
106 KiB

diff --git a/crm-auth/src/main/java/com/crm/auth/config/DataInitializer.java b/crm-auth/src/main/java/com/crm/auth/config/DataInitializer.java
index 189453e..a13cefb 100644
--- a/crm-auth/src/main/java/com/crm/auth/config/DataInitializer.java
+++ b/crm-auth/src/main/java/com/crm/auth/config/DataInitializer.java
@@ -1,15 +1,23 @@
package com.crm.auth.config;
-import com.crm.auth.domain.entity.*;
-import com.crm.auth.mapper.*;
-import com.crm.auth.service.ISysRoleService;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.crm.auth.domain.entity.SysMenu;
+import com.crm.auth.domain.entity.SysRole;
+import com.crm.auth.domain.entity.SysRoleMenu;
+import com.crm.auth.mapper.SysMenuMapper;
+import com.crm.auth.mapper.SysRoleMapper;
+import com.crm.auth.mapper.SysRoleMenuMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
+import java.util.List;
+
/**
- * 绯荤粺鏁版嵁鍒濆鍖栵細棣栨鍚姩鏃惰嫢鏉冮檺琛ㄤ负绌猴紝鎻掑叆榛樿绠$悊鍛樿鑹层€佽彍鍗曘€佽鑹?鐢ㄦ埛缁戝畾銆?+ * 绯荤粺鏁版嵁鍒濆鍖栵紙骞傜瓑锛夛細姣忔鍚姩閫愰」妫€鏌ワ紝鍙ˉ缂虹殑銆?+ * <p>鍒涘缓鍐呯疆绠$悊鍛樿鑹层€佺郴缁熺鐞嗚彍鍗曘€佽鑹茬鐞?button 鏉冮檺鐐癸紝
+ * 骞跺皢鍏ㄩ儴璧勬簮鍒嗛厤缁欑鐞嗗憳瑙掕壊锛圓DR-0012锛夈€?/p>
*/
@Slf4j
@Component
@@ -19,41 +27,87 @@ public class DataInitializer implements CommandLineRunner {
private final SysRoleMapper sysRoleMapper;
private final SysMenuMapper sysMenuMapper;
private final SysRoleMenuMapper sysRoleMenuMapper;
- private final SysUserRoleMapper sysUserRoleMapper;
@Override
public void run(String... args) {
- if (sysRoleMapper.selectCount(null) > 0) {
- log.info("鏉冮檺鏁版嵁宸插瓨鍦紝璺宠繃鍒濆鍖?);
- return;
+ log.info("鎵ц鏉冮檺鏁版嵁鍒濆鍖栨鏌?..");
+
+ // ---- 鍐呯疆瑙掕壊锛欰DMIN ----
+ SysRole adminRole = sysRoleMapper.selectOne(
+ new LambdaQueryWrapper<SysRole>().eq(SysRole::getRoleCode, "ROLE_ADMIN"));
+ if (adminRole == null) {
+ adminRole = new SysRole();
+ adminRole.setRoleName("绠$悊鍛?);
+ adminRole.setRoleCode("ROLE_ADMIN");
+ adminRole.setDataScope(4);
+ adminRole.setSort(0);
+ adminRole.setBuiltin(true);
+ sysRoleMapper.insert(adminRole);
+ log.info("鍒涘缓鍐呯疆瑙掕壊锛歿}", adminRole.getRoleCode());
+ } else if (!Boolean.TRUE.equals(adminRole.getBuiltin())) {
+ adminRole.setBuiltin(true);
+ sysRoleMapper.updateById(adminRole);
+ log.info("琛ヨ鍐呯疆瑙掕壊鏍囪锛歿}", adminRole.getRoleCode());
+ }
+
+ // ---- 涓€绾х洰褰曪細绯荤粺绠$悊 ----
+ SysMenu sysManage = findMenu("绯荤粺绠$悊", 0L);
+ if (sysManage == null) {
+ sysManage = insertMenu("绯荤粺绠$悊", 0L, 1, null, null, 0);
+ }
+
+ // ---- 浜岀骇鑿滃崟 ----
+ SysMenu roleManage = findMenu("瑙掕壊绠$悊", sysManage.getId());
+ if (roleManage == null) {
+ roleManage = insertMenu("瑙掕壊绠$悊", sysManage.getId(), 2,
+ "/system/role", "system/role/index", 1);
+ }
+ SysMenu menuManage = findMenu("鑿滃崟绠$悊", sysManage.getId());
+ if (menuManage == null) {
+ menuManage = insertMenu("鑿滃崟绠$悊", sysManage.getId(), 2,
+ "/system/menu", "system/menu/index", 2);
+ }
+ SysMenu deptManage = findMenu("閮ㄩ棬绠$悊", sysManage.getId());
+ if (deptManage == null) {
+ deptManage = insertMenu("閮ㄩ棬绠$悊", sysManage.getId(), 2,
+ "/system/dept", "system/dept/index", 3);
+ }
+
+ // ---- 瑙掕壊绠$悊 button 鏉冮檺鐐癸紙ADR-0012锛?----
+ insertButtonIfAbsent("瑙掕壊鍒楄〃", roleManage.getId(), "crm:role:list", "/api/roles/page", 1);
+ insertButtonIfAbsent("瑙掕壊璇︽儏", roleManage.getId(), "crm:role:detail", "/api/roles/detail", 2);
+ insertButtonIfAbsent("瑙掕壊淇濆瓨", roleManage.getId(), "crm:role:save", "/api/roles/saveOrUpdate", 3);
+ insertButtonIfAbsent("瑙掕壊鍒犻櫎", roleManage.getId(), "crm:role:delete", "/api/roles/delete", 4);
+ insertButtonIfAbsent("鏉冮檺鍒嗛厤", roleManage.getId(), "crm:role:assign", "/api/roles/assign-resources", 5);
+
+ // ---- 瑙掕壊-鑿滃崟缁戝畾锛堢鐞嗗憳鎷ユ湁鎵€鏈夎彍鍗曞拰鏉冮檺鐐癸級 ----
+ bindIfAbsent(adminRole.getId(), sysManage.getId());
+ bindIfAbsent(adminRole.getId(), roleManage.getId());
+ bindIfAbsent(adminRole.getId(), menuManage.getId());
+ bindIfAbsent(adminRole.getId(), deptManage.getId());
+ // 缁戝畾瑙掕壊绠$悊涓嬬殑鍏ㄩ儴 button 鏉冮檺鐐?+ List<SysMenu> roleButtons = sysMenuMapper.selectList(
+ new LambdaQueryWrapper<SysMenu>()
+ .eq(SysMenu::getParentId, roleManage.getId())
+ .eq(SysMenu::getMenuType, 3));
+ for (SysMenu btn : roleButtons) {
+ bindIfAbsent(adminRole.getId(), btn.getId());
}
- log.info("棣栨鍚姩锛屽垵濮嬪寲榛樿鏉冮檺鏁版嵁...");
-
- // ---- 瑙掕壊 ----
- SysRole adminRole = new SysRole();
- adminRole.setRoleName("绠$悊鍛?);
- adminRole.setRoleCode("ROLE_ADMIN");
- adminRole.setDataScope(4); // 鍏ㄩ儴鏁版嵁
- adminRole.setSort(0);
- sysRoleMapper.insert(adminRole);
-
- // ---- 鑿滃崟 ----
- // 涓€绾х洰褰?- SysMenu sysManage = menu("绯荤粺绠$悊", 0L, 1, null, null, 0);
- SysMenu roleManage = menu("瑙掕壊绠$悊", sysManage.getId(), 2, "/system/role", "system/role/index", 1);
- SysMenu menuManage = menu("鑿滃崟绠$悊", sysManage.getId(), 2, "/system/menu", "system/menu/index", 2);
- SysMenu deptManage = menu("閮ㄩ棬绠$悊", sysManage.getId(), 2, "/system/dept", "system/dept/index", 3);
-
- // ---- 瑙掕壊-鑿滃崟缁戝畾锛堢鐞嗗憳鎷ユ湁鎵€鏈夎彍鍗曪級 ----
- bind(adminRole.getId(), sysManage.getId());
- bind(adminRole.getId(), roleManage.getId());
- bind(adminRole.getId(), menuManage.getId());
- bind(adminRole.getId(), deptManage.getId());
-
- log.info("榛樿鏉冮檺鏁版嵁鍒濆鍖栧畬鎴?);
+
+ log.info("鏉冮檺鏁版嵁鍒濆鍖栨鏌ュ畬鎴?);
+ }
+
+ // ==================== 骞傜瓑杈呭姪鏂规硶 ====================
+
+ private SysMenu findMenu(String name, Long parentId) {
+ return sysMenuMapper.selectOne(
+ new LambdaQueryWrapper<SysMenu>()
+ .eq(SysMenu::getMenuName, name)
+ .eq(SysMenu::getParentId, parentId));
}
- private SysMenu menu(String name, Long parentId, int type, String path, String component, int sort) {
+ private SysMenu insertMenu(String name, Long parentId, int type,
+ String path, String component, int sort) {
SysMenu m = new SysMenu();
m.setParentId(parentId);
m.setMenuName(name);
@@ -63,10 +117,39 @@ public class DataInitializer implements CommandLineRunner {
m.setSort(sort);
m.setVisible(true);
sysMenuMapper.insert(m);
+ log.info("鍒涘缓鑿滃崟鑺傜偣锛歿} (type={})", name, type);
return m;
}
- private void bind(Long roleId, Long menuId) {
+ private void insertButtonIfAbsent(String name, Long parentId,
+ String perms, String apiUrl, int sort) {
+ Long count = sysMenuMapper.selectCount(
+ new LambdaQueryWrapper<SysMenu>().eq(SysMenu::getPerms, perms));
+ if (count > 0) {
+ return;
+ }
+ SysMenu btn = new SysMenu();
+ btn.setParentId(parentId);
+ btn.setMenuName(name);
+ btn.setMenuType(3);
+ btn.setSort(sort);
+ btn.setVisible(true);
+ btn.setPerms(perms);
+ btn.setDenyBehavior("hide");
+ btn.setApiUrl(apiUrl);
+ btn.setStatus("enabled");
+ sysMenuMapper.insert(btn);
+ log.info("鍒涘缓鏉冮檺鐐癸細{} -> {}", name, perms);
+ }
+
+ private void bindIfAbsent(Long roleId, Long menuId) {
+ Long count = sysRoleMenuMapper.selectCount(
+ new LambdaQueryWrapper<SysRoleMenu>()
+ .eq(SysRoleMenu::getRoleId, roleId)
+ .eq(SysRoleMenu::getMenuId, menuId));
+ if (count > 0) {
+ return;
+ }
SysRoleMenu rm = new SysRoleMenu();
rm.setRoleId(roleId);
rm.setMenuId(menuId);
diff --git a/crm-auth/src/main/java/com/crm/auth/constant/AuthConstants.java b/crm-auth/src/main/java/com/crm/auth/constant/AuthConstants.java
index 4f1f19c..4f4f17c 100644
--- a/crm-auth/src/main/java/com/crm/auth/constant/AuthConstants.java
+++ b/crm-auth/src/main/java/com/crm/auth/constant/AuthConstants.java
@@ -51,4 +51,13 @@ public interface AuthConstants {
/** 闈炴湰缁勭粐鎴愬憳锛堢粍缁囧噯鍏ユ牎楠屾湭閫氳繃锛?*/
int CODE_NOT_ORG_MEMBER = 61008;
+
+ /** 瑙掕壊鍙傛暟闈炴硶锛堢紪鐮佷负绌恒€佹暟鎹寖鍥磋秺鐣岀瓑锛?*/
+ int CODE_ROLE_INVALID = 61010;
+
+ /** 瑙掕壊缂栫爜宸插瓨鍦?*/
+ int CODE_ROLE_CODE_DUPLICATE = 61011;
+
+ /** 鍐呯疆瑙掕壊淇濇姢锛堢姝㈠垹闄ゆ垨鏀圭紪鐮侊級 */
+ int CODE_BUILTIN_ROLE_PROTECTED = 61012;
}
diff --git a/crm-auth/src/main/java/com/crm/auth/controller/AuthController.java b/crm-auth/src/main/java/com/crm/auth/controller/AuthController.java
index c14454f..34a50e7 100644
--- a/crm-auth/src/main/java/com/crm/auth/controller/AuthController.java
+++ b/crm-auth/src/main/java/com/crm/auth/controller/AuthController.java
@@ -27,20 +27,20 @@ public class AuthController {
private final IAuthService authService;
- @Operation(summary = "閽夐拤鎵爜鐧诲綍")
+ @Operation(summary = "閽夐拤鐧诲綍锛堟壂鐮?鍏嶇櫥閫氱敤锛? , tags = "鐧诲綍")
@PostMapping("/login/dingtalk")
public Result<LoginResultDTO> loginByDingTalk(@RequestParam String authCode) {
return Result.success(authService.login(IdentityTypeEnum.DINGTALK, authCode));
}
- @Operation(summary = "娉ㄩ攢")
+ @Operation(summary = "娉ㄩ攢" , tags = "鐧诲綍")
@PostMapping("/logout")
public Result<Void> logout(HttpServletRequest request) {
authService.logout(resolveToken(request));
return Result.success();
}
- @Operation(summary = "鑾峰彇褰撳墠鐧诲綍鐢ㄦ埛淇℃伅")
+ @Operation(summary = "鑾峰彇褰撳墠鐧诲綍鐢ㄦ埛淇℃伅" , tags = "鐧诲綍")
@GetMapping("/me")
public Result<UserInfoDTO> me() {
return Result.success(authService.getCurrentUserInfo());
diff --git a/crm-auth/src/main/java/com/crm/auth/controller/ResourceController.java b/crm-auth/src/main/java/com/crm/auth/controller/ResourceController.java
index ee26018..9951b59 100644
--- a/crm-auth/src/main/java/com/crm/auth/controller/ResourceController.java
+++ b/crm-auth/src/main/java/com/crm/auth/controller/ResourceController.java
@@ -4,6 +4,7 @@ import com.crm.auth.domain.dto.ResourceNode;
import com.crm.auth.service.IResourceService;
import com.crm.base.domain.result.Result;
import com.crm.file.domain.dto.FileInfoDTO;
+import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@@ -25,18 +26,21 @@ public class ResourceController {
/** 鍏ㄩ噺璧勬簮鏍戯紙鎵佸钩鏁扮粍锛屽墠绔寜 parentId 缁勬爲锛?*/
@GetMapping("/list")
+ @Operation(summary = "鑾峰彇鏉冮檺璧勬簮鏍? , tags = "鏉冮檺鐐圭鐞?)
public Result<List<ResourceNode>> listAll() {
return Result.success(resourceService.listAll());
}
/** 鏂板鎴栫紪杈戣妭鐐癸紙鍚牎楠岋級 */
@PostMapping("/saveOrUpdate")
+ @Operation(summary = "鏂板缂栬緫涓€绾ц彍鍗?浜岀骇鑿滃崟/鏉冮檺鐐? , tags = "鏉冮檺鐐圭鐞?)
public Result<ResourceNode> saveOrUpdate(ResourceNode node) {
return Result.success(resourceService.save(node));
}
/** 鍒犻櫎鑺傜偣锛堝惈瀛愯妭鐐规鏌?+ 绾ц仈娓呯悊锛?*/
@PostMapping("/delete")
+ @Operation(summary = "鍒犻櫎涓€绾ц彍鍗?浜岀骇鑿滃崟/鏉冮檺鐐? , tags = "鏉冮檺鐐圭鐞?)
public Result<Void> delete(@RequestParam Long id) {
resourceService.delete(id);
return Result.success();
@@ -44,6 +48,7 @@ public class ResourceController {
/** 鍥炬爣涓婁紶锛堟牸寮?澶у皬鏍¢獙锛?*/
@PostMapping("/icon/upload")
+ @Operation(summary = "涓婁紶鍥炬爣" , tags = "鏉冮檺鐐圭鐞?)
public Result<FileInfoDTO> uploadIcon(@RequestParam("file") MultipartFile file) {
return Result.success(resourceService.uploadIcon(file));
}
diff --git a/crm-auth/src/main/java/com/crm/auth/controller/RoleController.java b/crm-auth/src/main/java/com/crm/auth/controller/RoleController.java
new file mode 100644
index 0000000..fc5178d
--- /dev/null
+++ b/crm-auth/src/main/java/com/crm/auth/controller/RoleController.java
@@ -0,0 +1,97 @@
+package com.crm.auth.controller;
+
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.crm.auth.domain.dto.RoleDetailVO;
+import com.crm.auth.domain.entity.SysRole;
+import com.crm.auth.service.ISysRoleService;
+import com.crm.base.domain.result.PageResult;
+import com.crm.base.domain.result.Result;
+import io.swagger.v3.oas.annotations.Operation;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 瑙掕壊绠$悊绔帴鍙o紙ADR-0012锛?+ * <p>閬靛惊鍏ㄥ眬鎺ュ彛濂戠害锛氶潪涓ユ牸 RESTful锛屽啓鎿嶄綔 POST + 鍔ㄤ綔鍚庣紑锛岃〃鍗曞瓧娈垫敹鍙?/p>
+ * <p>鏉冮檺鎺у埗锛氱函 hasAuthority锛屾潈闄愮爜鐢辨暟鎹垵濮嬪寲鍣ㄧ瀛愬寲鍒版潈闄愯祫婧愭爲</p>
+ */
+@RestController
+@RequestMapping("/api/roles")
+@RequiredArgsConstructor
+public class RoleController {
+
+ private final ISysRoleService sysRoleService;
+
+ @PostMapping("/page")
+ @PreAuthorize("hasAuthority('crm:role:list')")
+ @Operation(summary = "鍒嗛〉鏌ヨ瑙掕壊", tags = "瑙掕壊绠$悊")
+ public Result<PageResult<SysRole>> page(
+ @RequestParam(defaultValue = "1") Long current,
+ @RequestParam(defaultValue = "10") Long size,
+ @RequestParam(required = false) String keyword) {
+ Page<SysRole> page = new Page<>(current, size);
+ sysRoleService.lambdaQuery()
+ .like(StrUtil.isNotBlank(keyword), SysRole::getRoleName, keyword)
+ .page(page);
+ return Result.success(new PageResult<>(page));
+ }
+
+ @PostMapping("/saveOrUpdate")
+ @PreAuthorize("hasAuthority('crm:role:save')")
+ @Operation(summary = "鏂板鎴栫紪杈戣鑹?, tags = "瑙掕壊绠$悊")
+ public Result<Void> saveOrUpdate(
+ @RequestParam(required = false) Long id,
+ @RequestParam String roleName,
+ @RequestParam String roleCode,
+ @RequestParam(defaultValue = "1") Integer dataScope,
+ @RequestParam(defaultValue = "0") Integer sort,
+ @RequestParam(required = false) String remark) {
+ SysRole role = new SysRole();
+ role.setId(id);
+ role.setRoleName(roleName);
+ role.setRoleCode(roleCode);
+ role.setDataScope(dataScope);
+ role.setSort(sort);
+ role.setRemark(remark);
+ sysRoleService.saveRole(role);
+ return Result.success();
+ }
+
+ @GetMapping("/detail")
+ @PreAuthorize("hasAuthority('crm:role:detail')")
+ @Operation(summary = "瑙掕壊璇︽儏锛堝惈鎺堟潈璧勬簮闆嗗悎锛?, tags = "瑙掕壊绠$悊")
+ public Result<RoleDetailVO> detail(@RequestParam Long roleId) {
+ return Result.success(sysRoleService.getRoleDetail(roleId));
+ }
+
+ @PostMapping("/assign-resources")
+ @PreAuthorize("hasAuthority('crm:role:assign')")
+ @Operation(summary = "鍒嗛厤瑙掕壊鏉冮檺璧勬簮锛堢鍏堣ˉ鍏?鍏ㄩ噺鏇挎崲锛?, tags = "瑙掕壊绠$悊")
+ public Result<Void> assignResources(
+ @RequestParam Long roleId,
+ @RequestParam String resourceIds) {
+ List<Long> menuIdList = StrUtil.isBlank(resourceIds)
+ ? Collections.emptyList()
+ : Arrays.stream(resourceIds.split(","))
+ .filter(StrUtil::isNotBlank)
+ .map(idStr -> Long.valueOf(idStr.trim()))
+ .collect(Collectors.toList());
+ sysRoleService.assignResources(roleId, menuIdList);
+ return Result.success();
+ }
+
+ @PostMapping("/delete")
+ @PreAuthorize("hasAuthority('crm:role:delete')")
+ @Operation(summary = "鍒犻櫎瑙掕壊锛堢骇鑱旀竻鐞嗗叧鑱旇〃锛?, tags = "瑙掕壊绠$悊")
+ public Result<Void> delete(@RequestParam Long id) {
+ sysRoleService.deleteRoleCascade(id);
+ return Result.success();
+ }
+}
diff --git a/crm-auth/src/main/java/com/crm/auth/controller/SystemController.java b/crm-auth/src/main/java/com/crm/auth/controller/SystemController.java
index 790bf34..5535bcc 100644
--- a/crm-auth/src/main/java/com/crm/auth/controller/SystemController.java
+++ b/crm-auth/src/main/java/com/crm/auth/controller/SystemController.java
@@ -6,12 +6,9 @@ import com.crm.auth.domain.dto.UserListDTO;
import com.crm.auth.domain.dto.UserStatsVO;
import com.crm.auth.domain.entity.SysDept;
import com.crm.auth.domain.entity.SysMenu;
-import com.crm.auth.domain.entity.SysRole;
import com.crm.auth.domain.param.UserPageParam;
import com.crm.auth.service.IAuthUserService;
import com.crm.auth.service.ISysDeptService;
-import com.crm.auth.service.ISysMenuService;
-import com.crm.auth.service.ISysRoleService;
import com.crm.auth.security.PermissionResolver;
import com.crm.base.domain.result.PageResult;
import com.crm.base.domain.result.Result;
@@ -29,8 +26,6 @@ import java.util.stream.Collectors;
@RequiredArgsConstructor
public class SystemController {
- private final ISysRoleService sysRoleService;
- private final ISysMenuService sysMenuService;
private final ISysDeptService sysDeptService;
private final IAuthUserService authUserService;
private final PermissionResolver permissionResolver;
@@ -43,62 +38,6 @@ public class SystemController {
return Result.success(permissionResolver.visibleMenuTree(userId));
}
- // ========== 瑙掕壊 ==========
-
- @PostMapping("/roles/page")
- public Result<PageResult<SysRole>> rolePage(
- @RequestParam(defaultValue = "1") Long current,
- @RequestParam(defaultValue = "10") Long size,
- @RequestParam(required = false) String keyword) {
- Page<SysRole> page = new Page<>(current, size);
- sysRoleService.lambdaQuery()
- .like(StrUtil.isNotBlank(keyword), SysRole::getRoleName, keyword)
- .page(page);
- return Result.success(new PageResult<>(page));
- }
-
- @PostMapping("/roles/save")
- public Result<Void> saveRole(
- @RequestParam(required = false) Long id,
- @RequestParam String roleName,
- @RequestParam String roleCode,
- @RequestParam(defaultValue = "1") Integer dataScope,
- @RequestParam(defaultValue = "0") Integer sort,
- @RequestParam(required = false) String remark) {
- SysRole role = new SysRole();
- role.setId(id);
- role.setRoleName(roleName);
- role.setRoleCode(roleCode);
- role.setDataScope(dataScope);
- role.setSort(sort);
- role.setRemark(remark);
- sysRoleService.saveOrUpdate(role);
- return Result.success();
- }
-
- @PostMapping("/roles/delete")
- public Result<Void> deleteRole(@RequestParam Long id) {
- sysRoleService.deleteRoleCascade(id);
- return Result.success();
- }
-
- @PostMapping("/roles/assign-menus")
- public Result<Void> assignRoleMenus(@RequestParam Long roleId, @RequestParam String menuIds) {
- List<Long> menuIdList = StrUtil.isBlank(menuIds)
- ? Collections.emptyList()
- : Arrays.stream(menuIds.split(","))
- .filter(StrUtil::isNotBlank)
- .map(idStr -> Long.valueOf(idStr.trim()))
- .collect(Collectors.toList());
- sysRoleService.assignMenus(roleId, menuIdList);
- return Result.success();
- }
-
- @GetMapping("/roles/menus")
- public Result<List<Long>> roleMenus(@RequestParam Long roleId) {
- return Result.success(sysMenuService.getMenuIdsByRoleId(roleId));
- }
-
// ========== 閮ㄩ棬 ==========
@GetMapping("/depts/tree")
diff --git a/crm-auth/src/main/java/com/crm/auth/domain/dto/RoleDetailVO.java b/crm-auth/src/main/java/com/crm/auth/domain/dto/RoleDetailVO.java
new file mode 100644
index 0000000..4365cbd
--- /dev/null
+++ b/crm-auth/src/main/java/com/crm/auth/domain/dto/RoleDetailVO.java
@@ -0,0 +1,60 @@
+package com.crm.auth.domain.dto;
+
+import com.crm.auth.domain.entity.SysRole;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 瑙掕壊璇︽儏 DTO锛圓DR-0012锛?+ * <p>瑙掕壊绠$悊璇︽儏鎺ュ彛鐨勮緭鍑鸿浇浣擄紱resourceIds 涓哄凡琛ュ叏绁栧厛鐨勫畬鏁存巿鏉冮泦鍚堬紙ADR-0005锛?/p>
+ */
+@Data
+public class RoleDetailVO implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ @Schema(description = "瑙掕壊 ID")
+ private Long id;
+
+ @Schema(description = "瑙掕壊鍚嶇О")
+ private String roleName;
+
+ @Schema(description = "瑙掕壊缂栫爜")
+ private String roleCode;
+
+ @Schema(description = "鏁版嵁鑼冨洿 1=鏈汉 2=鏈儴闂?3=鏈儴闂ㄥ強瀛愰儴闂?4=鍏ㄩ儴")
+ private Integer dataScope;
+
+ @Schema(description = "鎺掑簭")
+ private Integer sort;
+
+ @Schema(description = "澶囨敞")
+ private String remark;
+
+ @Schema(description = "鏄惁鍐呯疆瑙掕壊")
+ private Boolean builtin;
+
+ @Schema(description = "宸茶ˉ鍏ㄧ鍏堢殑瀹屾暣鎺堟潈闆嗗悎锛堝瓨鍌ㄦ€侊級")
+ private List<Long> resourceIds;
+
+ /** SysRole 鈫?RoleDetailVO 鍩烘湰瀛楁鏄犲皠锛堜笉鍚?resourceIds锛?*/
+ public static RoleDetailVO fromEntity(SysRole entity) {
+ if (entity == null) {
+ return null;
+ }
+ RoleDetailVO vo = new RoleDetailVO();
+ vo.setId(entity.getId());
+ vo.setRoleName(entity.getRoleName());
+ vo.setRoleCode(entity.getRoleCode());
+ vo.setDataScope(entity.getDataScope());
+ vo.setSort(entity.getSort());
+ vo.setRemark(entity.getRemark());
+ vo.setBuiltin(entity.getBuiltin());
+ return vo;
+ }
+}
diff --git a/crm-auth/src/main/java/com/crm/auth/domain/entity/SysRole.java b/crm-auth/src/main/java/com/crm/auth/domain/entity/SysRole.java
index 819e2f2..adb2d31 100644
--- a/crm-auth/src/main/java/com/crm/auth/domain/entity/SysRole.java
+++ b/crm-auth/src/main/java/com/crm/auth/domain/entity/SysRole.java
@@ -33,4 +33,8 @@ public class SysRole extends BaseEntity {
@Comment("澶囨敞")
@Column(columnDefinition = "varchar(200) comment '澶囨敞'")
private String remark;
+
+ @Comment("鍐呯疆瑙掕壊鏍囪锛宼rue=绯荤粺鍐呯疆涓嶅彲鍒犻櫎涓嶅彲鏀圭紪鐮?)
+ @Column(columnDefinition = "boolean default false")
+ private Boolean builtin;
}
diff --git a/crm-auth/src/main/java/com/crm/auth/service/ISysMenuService.java b/crm-auth/src/main/java/com/crm/auth/service/ISysMenuService.java
index af633c1..272a9c4 100644
--- a/crm-auth/src/main/java/com/crm/auth/service/ISysMenuService.java
+++ b/crm-auth/src/main/java/com/crm/auth/service/ISysMenuService.java
@@ -3,10 +3,17 @@ package com.crm.auth.service;
import com.crm.auth.domain.entity.SysMenu;
import com.crm.base.service.IBaseService;
-import java.util.List;
+import java.util.Collection;
+import java.util.Set;
public interface ISysMenuService extends IBaseService<SysMenu> {
- /** 鑾峰彇鏌愯鑹茬殑鑿滃崟ID鍒楄〃 */
- List<Long> getMenuIdsByRoleId(Long roleId);
+ /**
+ * 娌?parentId 鍚戜笂閬嶅巻锛岃ˉ鍏ㄧ粰瀹氳妭鐐归泦鍚堢殑鍏ㄩ儴绁栧厛 catalog/menu ID锛圓DR-0005锛夈€?+ * <p>鎵归噺鏌ュ叏琛ㄦ瀯寤?ID鈫抪arentId 鏄犲皠锛屽唴瀛橀亶鍘嗭紝閬垮厤閫愯妭鐐规煡 DB銆?/p>
+ *
+ * @param menuIds 鐢ㄦ埛鍕鹃€夌殑璧勬簮鑺傜偣 ID锛堝彲鑳戒笉鍚鍏堬級
+ * @return 鍖呭惈鍏ㄩ儴绁栧厛鐨勫畬鏁撮泦鍚堬紙杈撳叆 ID + 绁栧厛 ID锛夛紝瀛樺偍鎬?+ */
+ Set<Long> getAncestorIds(Collection<Long> menuIds);
}
diff --git a/crm-auth/src/main/java/com/crm/auth/service/ISysRoleService.java b/crm-auth/src/main/java/com/crm/auth/service/ISysRoleService.java
index 8735b8d..c50ee13 100644
--- a/crm-auth/src/main/java/com/crm/auth/service/ISysRoleService.java
+++ b/crm-auth/src/main/java/com/crm/auth/service/ISysRoleService.java
@@ -1,5 +1,6 @@
package com.crm.auth.service;
+import com.crm.auth.domain.dto.RoleDetailVO;
import com.crm.auth.domain.entity.SysRole;
import com.crm.base.service.IBaseService;
@@ -8,12 +9,29 @@ import java.util.List;
public interface ISysRoleService extends IBaseService<SysRole> {
/**
- * 鍒嗛厤瑙掕壊鐨勮彍鍗曟潈闄愶紙鍏ㄩ噺鏇挎崲锛屼簨鍔′繚鎶わ級
+ * 淇濆瓨瑙掕壊锛堝甫鏍¢獙锛歳oleCode 鍞竴銆佸唴缃鑹蹭繚鎶ゃ€乨ataScope 鑼冨洿銆佺璁?builtin锛? *
- * @param roleId 瑙掕壊 ID
- * @param menuIds 鑿滃崟 ID 鍒楄〃
+ * @param role 瑙掕壊淇℃伅锛坕d 涓虹┖鏃舵柊澧炪€侀潪绌烘椂缂栬緫锛?+ * @return 淇濆瓨鍚庣殑瑙掕壊锛堝惈 ID锛? */
- void assignMenus(Long roleId, List<Long> menuIds);
+ SysRole saveRole(SysRole role);
+
+ /**
+ * 鑾峰彇瑙掕壊璇︽儏锛堝熀鏈俊鎭?+ 宸叉巿鏉冭祫婧?ID 闆嗗悎锛?+ *
+ * @param roleId 瑙掕壊 ID
+ * @return RoleDetailVO锛屽惈 resourceIds锛堜粠 sys_role_menu 鏌ュ嚭鐨勫畬鏁存巿鏉冮泦鍚堬級
+ */
+ RoleDetailVO getRoleDetail(Long roleId);
+
+ /**
+ * 鍒嗛厤瑙掕壊鏉冮檺璧勬簮锛堢鍏堣ˉ鍏?+ 鍏ㄩ噺鏇挎崲锛屼簨鍔′繚鎶わ級
+ * <p>鍓嶇鍙紶鍕鹃€夌殑鍙跺瓙鑺傜偣锛屽悗绔皟鐢?ISysMenuService.getAncestorIds 琛ュ叏绁栧厛鍚庡叏閲忓啓鍏?sys_role_menu</p>
+ *
+ * @param roleId 瑙掕壊 ID
+ * @param resourceIds 鍓嶇鍕鹃€夌殑璧勬簮鑺傜偣 ID锛堝彲鑳戒笉鍚鍏堬級
+ */
+ void assignResources(Long roleId, List<Long> resourceIds);
/**
* 鍒犻櫎瑙掕壊骞剁骇鑱旀竻鐞嗚鑹?鑿滃崟鍏宠仈锛堜簨鍔′繚鎶わ級
diff --git a/crm-auth/src/main/java/com/crm/auth/service/impl/SysMenuServiceImpl.java b/crm-auth/src/main/java/com/crm/auth/service/impl/SysMenuServiceImpl.java
index 6cb99a6..db54cd2 100644
--- a/crm-auth/src/main/java/com/crm/auth/service/impl/SysMenuServiceImpl.java
+++ b/crm-auth/src/main/java/com/crm/auth/service/impl/SysMenuServiceImpl.java
@@ -1,32 +1,42 @@
package com.crm.auth.service.impl;
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.crm.auth.domain.entity.SysMenu;
-import com.crm.auth.domain.entity.SysRoleMenu;
import com.crm.auth.mapper.SysMenuMapper;
-import com.crm.auth.mapper.SysRoleMenuMapper;
import com.crm.auth.service.ISysMenuService;
import com.crm.base.service.impl.BaseServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
-import java.util.Collections;
-import java.util.List;
+import java.util.*;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class SysMenuServiceImpl extends BaseServiceImpl<SysMenuMapper, SysMenu> implements ISysMenuService {
- private final SysRoleMenuMapper sysRoleMenuMapper;
-
@Override
- public List<Long> getMenuIdsByRoleId(Long roleId) {
- List<SysRoleMenu> list = sysRoleMenuMapper.selectList(
- new LambdaQueryWrapper<SysRoleMenu>().eq(SysRoleMenu::getRoleId, roleId));
- if (list == null || list.isEmpty()) {
- return Collections.emptyList();
+ public Set<Long> getAncestorIds(Collection<Long> menuIds) {
+ if (menuIds == null || menuIds.isEmpty()) {
+ return Collections.emptySet();
+ }
+ // 鎵归噺鏌ュ叏琛紝鏋勫缓 ID鈫抪arentId 鏄犲皠
+ List<SysMenu> allMenus = list();
+ Map<Long, Long> parentIdMap = allMenus.stream()
+ .collect(Collectors.toMap(SysMenu::getId, SysMenu::getParentId, (a, b) -> a));
+
+ // 瀵规瘡涓緭鍏ヨ妭鐐癸紝娌?parentId 鍚戜笂閬嶅巻鏀堕泦绁栧厛
+ Set<Long> result = new LinkedHashSet<>(menuIds);
+ for (Long menuId : menuIds) {
+ Long current = menuId;
+ while (current != null) {
+ Long parent = parentIdMap.get(current);
+ if (parent == null || parent == 0L || result.contains(parent)) {
+ break;
+ }
+ result.add(parent);
+ current = parent;
+ }
}
- return list.stream().map(SysRoleMenu::getMenuId).collect(Collectors.toList());
+ return result;
}
}
diff --git a/crm-auth/src/main/java/com/crm/auth/service/impl/SysRoleServiceImpl.java b/crm-auth/src/main/java/com/crm/auth/service/impl/SysRoleServiceImpl.java
index f8423c8..95e2a87 100644
--- a/crm-auth/src/main/java/com/crm/auth/service/impl/SysRoleServiceImpl.java
+++ b/crm-auth/src/main/java/com/crm/auth/service/impl/SysRoleServiceImpl.java
@@ -1,47 +1,135 @@
package com.crm.auth.service.impl;
+import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.crm.auth.constant.AuthConstants;
+import com.crm.auth.domain.dto.RoleDetailVO;
import com.crm.auth.domain.entity.SysRole;
import com.crm.auth.domain.entity.SysRoleMenu;
+import com.crm.auth.domain.entity.SysUserRole;
import com.crm.auth.mapper.SysRoleMapper;
import com.crm.auth.mapper.SysRoleMenuMapper;
+import com.crm.auth.mapper.SysUserRoleMapper;
+import com.crm.auth.service.ISysMenuService;
import com.crm.auth.service.ISysRoleService;
+import com.crm.base.domain.exception.BusinessErrorException;
import com.crm.base.service.impl.BaseServiceImpl;
import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
+import java.util.Set;
+import static com.crm.auth.constant.AuthConstants.*;
+
+@Slf4j
@Service
@RequiredArgsConstructor
public class SysRoleServiceImpl extends BaseServiceImpl<SysRoleMapper, SysRole> implements ISysRoleService {
private final SysRoleMenuMapper sysRoleMenuMapper;
+ private final SysUserRoleMapper sysUserRoleMapper;
+ private final ISysMenuService sysMenuService;
+
+ @Override
+ public SysRole saveRole(SysRole role) {
+ // 鍩烘湰闈炵┖鏍¢獙
+ if (StrUtil.isBlank(role.getRoleName())) {
+ throw new BusinessErrorException(CODE_ROLE_INVALID, "瑙掕壊鍚嶇О涓嶈兘涓虹┖");
+ }
+ if (StrUtil.isBlank(role.getRoleCode())) {
+ throw new BusinessErrorException(CODE_ROLE_INVALID, "瑙掕壊缂栫爜涓嶈兘涓虹┖");
+ }
+
+ // dataScope 鑼冨洿鏍¢獙
+ if (role.getDataScope() != null && (role.getDataScope() < 1 || role.getDataScope() > 4)) {
+ throw new BusinessErrorException(CODE_ROLE_INVALID, "鏁版嵁鑼冨洿蹇呴』鍦?1-4 鑼冨洿鍐?);
+ }
+
+ // 鍐呯疆瑙掕壊淇濇姢锛氫笉鍏佽閫氳繃鎺ュ彛鍒涘缓鍐呯疆瑙掕壊锛堟棤璁烘柊寤鸿繕鏄紪杈戜笉瀛樺湪鐨勮褰曪級
+ SysRole existing = role.getId() != null ? baseMapper.selectById(role.getId()) : null;
+ if (Boolean.TRUE.equals(role.getBuiltin())
+ && (existing == null || !Boolean.TRUE.equals(existing.getBuiltin()))) {
+ throw new BusinessErrorException(CODE_BUILTIN_ROLE_PROTECTED, "涓嶅厑璁搁€氳繃鎺ュ彛鍒涘缓鍐呯疆瑙掕壊");
+ }
+ if (role.getId() == null) {
+ role.setBuiltin(false);
+ } else if (existing != null && Boolean.TRUE.equals(existing.getBuiltin())
+ && !existing.getRoleCode().equals(role.getRoleCode())) {
+ // 缂栬緫锛氬唴缃鑹茬姝㈡敼 roleCode
+ throw new BusinessErrorException(CODE_BUILTIN_ROLE_PROTECTED, "鍐呯疆瑙掕壊鐨勭紪鐮佷笉鍙慨鏀?);
+ }
+
+ // roleCode 鍞竴鎬ф牎楠?+ Long count = baseMapper.selectCount(
+ new LambdaQueryWrapper<SysRole>()
+ .eq(SysRole::getRoleCode, role.getRoleCode())
+ .ne(role.getId() != null, SysRole::getId, role.getId()));
+ if (count != null && count > 0) {
+ throw new BusinessErrorException(CODE_ROLE_CODE_DUPLICATE,
+ "瑙掕壊缂栫爜宸插瓨鍦細" + role.getRoleCode());
+ }
+
+ saveOrUpdate(role);
+ log.info("淇濆瓨瑙掕壊锛歩d={}, roleCode={}", role.getId(), role.getRoleCode());
+ return role;
+ }
+
+ @Override
+ public RoleDetailVO getRoleDetail(Long roleId) {
+ SysRole role = baseMapper.selectById(roleId);
+ if (role == null) {
+ throw new BusinessErrorException(CODE_ROLE_INVALID, "瑙掕壊涓嶅瓨鍦細" + roleId);
+ }
+ RoleDetailVO vo = RoleDetailVO.fromEntity(role);
+ // 鏌?sys_role_menu 寰楀埌瀹屾暣鎺堟潈闆嗗悎锛堝瓨鍌ㄦ€侊紝宸插惈绁栧厛锛?+ List<SysRoleMenu> bindings = sysRoleMenuMapper.selectList(
+ new LambdaQueryWrapper<SysRoleMenu>().eq(SysRoleMenu::getRoleId, roleId));
+ List<Long> resourceIds = bindings.stream()
+ .map(SysRoleMenu::getMenuId)
+ .toList();
+ vo.setResourceIds(resourceIds);
+ return vo;
+ }
@Override
@Transactional(rollbackFor = Exception.class)
- public void assignMenus(Long roleId, List<Long> menuIds) {
- // 鍏堝垹鍚庢彃锛堝叏閲忔浛鎹級
+ public void assignResources(Long roleId, List<Long> resourceIds) {
+ // 绁栧厛琛ュ叏锛氬墠绔彧浼犲嬀閫夌殑鍙跺瓙鑺傜偣锛屽悗绔ˉ鍏ㄥ叏閮ㄧ鍏?catalog/menu ID
+ Set<Long> completeIds = sysMenuService.getAncestorIds(resourceIds);
+ log.info("鍒嗛厤瑙掕壊璧勬簮锛歳oleId={}, 杈撳叆 {} 涓?-> 琛ュ叏鍚?{} 涓?, roleId,
+ resourceIds != null ? resourceIds.size() : 0, completeIds.size());
+
+ // 鍏堝垹鍚庢彃锛堝叏閲忔浛鎹?sys_role_menu锛? sysRoleMenuMapper.delete(
new LambdaQueryWrapper<SysRoleMenu>().eq(SysRoleMenu::getRoleId, roleId));
- if (menuIds != null && !menuIds.isEmpty()) {
- for (Long menuId : menuIds) {
- SysRoleMenu rm = new SysRoleMenu();
- rm.setRoleId(roleId);
- rm.setMenuId(menuId);
- sysRoleMenuMapper.insert(rm);
- }
+ for (Long menuId : completeIds) {
+ SysRoleMenu rm = new SysRoleMenu();
+ rm.setRoleId(roleId);
+ rm.setMenuId(menuId);
+ sysRoleMenuMapper.insert(rm);
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteRoleCascade(Long roleId) {
+ // 鍐呯疆瑙掕壊淇濇姢锛歜uiltin=true 鐨勮鑹叉嫆缁濆垹闄?+ SysRole role = baseMapper.selectById(roleId);
+ if (role != null && Boolean.TRUE.equals(role.getBuiltin())) {
+ throw new BusinessErrorException(CODE_BUILTIN_ROLE_PROTECTED, "鍐呯疆瑙掕壊涓嶅厑璁稿垹闄?);
+ }
// 绾ц仈娓呯悊瑙掕壊-鑿滃崟鍏宠仈
sysRoleMenuMapper.delete(
new LambdaQueryWrapper<SysRoleMenu>().eq(SysRoleMenu::getRoleId, roleId));
+ // 绾ц仈娓呯悊鐢ㄦ埛-瑙掕壊鍏宠仈
+ sysUserRoleMapper.delete(
+ new LambdaQueryWrapper<SysUserRole>()
+ .eq(SysUserRole::getRoleId, roleId));
// 鍒犻櫎瑙掕壊
removeById(roleId);
+ log.info("鍒犻櫎瑙掕壊鍙婄骇鑱斿叧鑱旓細roleId={}", roleId);
}
}
diff --git a/crm-auth/src/test/java/com/crm/auth/security/scope/DataScopeIntegrationTest.java b/crm-auth/src/test/java/com/crm/auth/security/scope/DataScopeIntegrationTest.java
index 565ced7..2301fad 100644
--- a/crm-auth/src/test/java/com/crm/auth/security/scope/DataScopeIntegrationTest.java
+++ b/crm-auth/src/test/java/com/crm/auth/security/scope/DataScopeIntegrationTest.java
@@ -460,7 +460,8 @@ class DataScopeIntegrationTest {
create table sys_role (
id bigint primary key, creator_id varchar(50), create_time datetime,
updater_id varchar(50), update_time datetime, deleted tinyint not null default 0,
- role_name varchar(50), role_code varchar(50), data_scope tinyint, sort int, remark varchar(200))
+ role_name varchar(50), role_code varchar(50), data_scope tinyint, sort int, remark varchar(200),
+ builtin boolean default false)
""",
"""
create table sys_user_role (
diff --git a/crm-auth/src/test/java/com/crm/auth/service/impl/SysMenuServiceImplTest.java b/crm-auth/src/test/java/com/crm/auth/service/impl/SysMenuServiceImplTest.java
new file mode 100644
index 0000000..805dea1
--- /dev/null
+++ b/crm-auth/src/test/java/com/crm/auth/service/impl/SysMenuServiceImplTest.java
@@ -0,0 +1,127 @@
+package com.crm.auth.service.impl;
+
+import com.crm.auth.domain.entity.SysMenu;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Spy;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.doReturn;
+
+/**
+ * {@link SysMenuServiceImpl} 鍗曞厓娴嬭瘯锛氳鐩栫鍏堣ˉ鍏ㄩ€昏緫锛圓DR-0005锛?+ */
+@ExtendWith(MockitoExtension.class)
+class SysMenuServiceImplTest {
+
+ @Spy
+ @InjectMocks
+ private SysMenuServiceImpl sysMenuService;
+
+ // ==================== 绁栧厛琛ュ叏 ====================
+
+ @Test
+ @DisplayName("鍗曢摼鍚戜笂閬嶅巻 -> 浼犲叆鍙跺瓙 button锛岃繑鍥?button+menu+catalog 瀹屾暣閾?)
+ void singleChainAncestorCompletion() {
+ SysMenu catalog = buildMenu(1L, 0L, "绯荤粺绠$悊", 1);
+ SysMenu menu = buildMenu(10L, 1L, "瑙掕壊绠$悊", 2);
+ SysMenu button = buildMenu(100L, 10L, "瑙掕壊鍒楄〃", 3);
+ doReturn(List.of(catalog, menu, button)).when(sysMenuService).list();
+
+ Set<Long> result = sysMenuService.getAncestorIds(Set.of(100L));
+
+ assertThat(result).containsExactlyInAnyOrder(100L, 10L, 1L);
+ }
+
+ @Test
+ @DisplayName("澶氶摼姹囧悎 -> 涓ゆ潯鐙珛閾剧殑鍙跺瓙锛岃繑鍥炰袱鏉¢摼鍏ㄩ儴绁栧厛鐨勫苟闆?)
+ void multipleChainsAncestorCompletion() {
+ // 閾?锛氱郴缁熺鐞?1) -> 瑙掕壊绠$悊(10) -> 瑙掕壊鍒楄〃(100)
+ // 閾?锛氬鎴风鐞?2) -> 瀹㈡埛鍒楄〃(20) -> 鏂板瀹㈡埛(200)
+ SysMenu catalog1 = buildMenu(1L, 0L, "绯荤粺绠$悊", 1);
+ SysMenu menu1 = buildMenu(10L, 1L, "瑙掕壊绠$悊", 2);
+ SysMenu button1 = buildMenu(100L, 10L, "瑙掕壊鍒楄〃", 3);
+
+ SysMenu catalog2 = buildMenu(2L, 0L, "瀹㈡埛绠$悊", 1);
+ SysMenu menu2 = buildMenu(20L, 2L, "瀹㈡埛鍒楄〃", 2);
+ SysMenu button2 = buildMenu(200L, 20L, "鏂板瀹㈡埛", 3);
+
+ doReturn(List.of(catalog1, menu1, button1, catalog2, menu2, button2))
+ .when(sysMenuService).list();
+
+ Set<Long> result = sysMenuService.getAncestorIds(Set.of(100L, 200L));
+
+ assertThat(result).containsExactlyInAnyOrder(100L, 10L, 1L, 200L, 20L, 2L);
+ }
+
+ @Test
+ @DisplayName("鏍硅妭鐐?-> parentId=0 鐨?catalog锛岃繑鍥炲畠鑷繁锛堟棤绁栧厛鍙ˉ锛?)
+ void rootNodeNoAncestor() {
+ SysMenu catalog = buildMenu(1L, 0L, "绯荤粺绠$悊", 1);
+ SysMenu menu = buildMenu(10L, 1L, "瑙掕壊绠$悊", 2);
+ doReturn(List.of(catalog, menu)).when(sysMenuService).list();
+
+ Set<Long> result = sysMenuService.getAncestorIds(Set.of(1L));
+
+ assertThat(result).containsExactly(1L);
+ }
+
+ @Test
+ @DisplayName("绌鸿緭鍏?-> 浼犲叆绌洪泦鍚堬紝杩斿洖绌洪泦鍚?)
+ void emptyInputReturnsEmpty() {
+ Set<Long> result = sysMenuService.getAncestorIds(Collections.emptySet());
+
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ @DisplayName("null 杈撳叆 -> 浼犲叆 null锛岃繑鍥炵┖闆嗗悎")
+ void nullInputReturnsEmpty() {
+ Set<Long> result = sysMenuService.getAncestorIds(null);
+
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ @DisplayName("宸插惈绁栧厛 -> 浼犲叆瀹屾暣闆嗗悎锛岃繑鍥炵浉鍚岄泦鍚堬紙骞傜瓑涓嶉噸澶嶏級")
+ void alreadyCompleteSetIsIdempotent() {
+ SysMenu catalog = buildMenu(1L, 0L, "绯荤粺绠$悊", 1);
+ SysMenu menu = buildMenu(10L, 1L, "瑙掕壊绠$悊", 2);
+ SysMenu button = buildMenu(100L, 10L, "瑙掕壊鍒楄〃", 3);
+ doReturn(List.of(catalog, menu, button)).when(sysMenuService).list();
+
+ Set<Long> result = sysMenuService.getAncestorIds(Set.of(100L, 10L, 1L));
+
+ assertThat(result).containsExactlyInAnyOrder(100L, 10L, 1L);
+ }
+
+ @Test
+ @DisplayName("杈撳叆鍚笉瀛樺湪浜?DB 鐨?ID -> 璇?ID 淇濈暀鍦ㄧ粨鏋滀腑锛屼絾涓嶄骇鐢熺鍏?)
+ void nonExistentIdKeptButNoAncestors() {
+ SysMenu catalog = buildMenu(1L, 0L, "绯荤粺绠$悊", 1);
+ SysMenu menu = buildMenu(10L, 1L, "瑙掕壊绠$悊", 2);
+ doReturn(List.of(catalog, menu)).when(sysMenuService).list();
+
+ Set<Long> result = sysMenuService.getAncestorIds(Set.of(10L, 999L));
+
+ assertThat(result).containsExactlyInAnyOrder(10L, 1L, 999L);
+ }
+
+ // ==================== 妗╂暟鎹?====================
+
+ private SysMenu buildMenu(Long id, Long parentId, String name, int type) {
+ SysMenu m = new SysMenu();
+ m.setId(id);
+ m.setParentId(parentId);
+ m.setMenuName(name);
+ m.setMenuType(type);
+ return m;
+ }
+}
diff --git a/crm-auth/src/test/java/com/crm/auth/service/impl/SysRoleServiceImplTest.java b/crm-auth/src/test/java/com/crm/auth/service/impl/SysRoleServiceImplTest.java
new file mode 100644
index 0000000..70c49b9
--- /dev/null
+++ b/crm-auth/src/test/java/com/crm/auth/service/impl/SysRoleServiceImplTest.java
@@ -0,0 +1,300 @@
+package com.crm.auth.service.impl;
+
+import com.crm.auth.domain.dto.RoleDetailVO;
+import com.crm.auth.domain.entity.SysRole;
+import com.crm.auth.domain.entity.SysRoleMenu;
+import com.crm.auth.domain.entity.SysUserRole;
+import com.crm.auth.mapper.SysRoleMapper;
+import com.crm.auth.mapper.SysRoleMenuMapper;
+import com.crm.auth.mapper.SysUserRoleMapper;
+import com.crm.auth.service.ISysMenuService;
+import com.crm.base.domain.exception.BusinessErrorException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.util.List;
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * {@link SysRoleServiceImpl} 鍗曞厓娴嬭瘯锛氳鐩?saveRole 鏍¢獙瑙勫垯锛圓DR-0012锛?+ */
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
+class SysRoleServiceImplTest {
+
+ @Mock
+ private SysRoleMapper sysRoleMapper;
+ @Mock
+ private SysRoleMenuMapper sysRoleMenuMapper;
+ @Mock
+ private SysUserRoleMapper sysUserRoleMapper;
+ @Mock
+ private ISysMenuService sysMenuService;
+
+ @InjectMocks
+ private SysRoleServiceImpl sysRoleService;
+
+ @BeforeEach
+ void setUp() {
+ // ServiceImpl 鐨?baseMapper 瀛楁鍦ㄧ埗绫伙紝@InjectMocks 鍙兘涓嶆敞鍏ワ紝鎵嬪姩璁剧疆
+ ReflectionTestUtils.setField(sysRoleService, "baseMapper", sysRoleMapper);
+ }
+
+ // ==================== roleCode 鍞竴鎬?====================
+
+ @Test
+ @DisplayName("鍒涘缓瑙掕壊 -> roleCode 宸插瓨鍦?-> 鎷掔粷")
+ void createRole_duplicateCode_rejected() {
+ SysRole role = buildRole(null, "閿€鍞?, "ROLE_SALES", 1);
+ when(sysRoleMapper.selectCount(any())).thenReturn(1L);
+
+ assertThatThrownBy(() -> sysRoleService.saveRole(role))
+ .isInstanceOf(BusinessErrorException.class)
+ .hasMessageContaining("瑙掕壊缂栫爜宸插瓨鍦?);
+ }
+
+ @Test
+ @DisplayName("缂栬緫瑙掕壊 -> roleCode 涓嶅彉锛堝悓 id锛?-> 閫氳繃")
+ void editRole_sameCodeSameId_passes() {
+ SysRole role = buildRole(1L, "閿€鍞?, "ROLE_SALES", 1);
+ when(sysRoleMapper.selectById(1L)).thenReturn(role);
+ when(sysRoleMapper.selectCount(any())).thenReturn(0L);
+ when(sysRoleMapper.updateById(any(SysRole.class))).thenReturn(1);
+
+ assertThatCode(() -> sysRoleService.saveRole(role))
+ .doesNotThrowAnyException();
+ }
+
+ // ==================== 鍐呯疆瑙掕壊淇濇姢 ====================
+
+ @Test
+ @DisplayName("缂栬緫鍐呯疆瑙掕壊 -> 淇敼 roleCode -> 鎷掔粷")
+ void editBuiltinRole_changeCode_rejected() {
+ SysRole existing = buildRole(1L, "绠$悊鍛?, "ROLE_ADMIN", 4);
+ existing.setBuiltin(true);
+ SysRole input = buildRole(1L, "绠$悊鍛?, "ROLE_SUPER", 4);
+ when(sysRoleMapper.selectById(1L)).thenReturn(existing);
+
+ assertThatThrownBy(() -> sysRoleService.saveRole(input))
+ .isInstanceOf(BusinessErrorException.class)
+ .hasMessageContaining("鍐呯疆瑙掕壊鐨勭紪鐮佷笉鍙慨鏀?);
+ }
+
+ @Test
+ @DisplayName("缂栬緫鍐呯疆瑙掕壊 -> roleCode 涓嶅彉 -> 閫氳繃")
+ void editBuiltinRole_sameCode_passes() {
+ SysRole existing = buildRole(1L, "绠$悊鍛?, "ROLE_ADMIN", 4);
+ existing.setBuiltin(true);
+ SysRole input = buildRole(1L, "绠$悊鍛樻敼", "ROLE_ADMIN", 4);
+ when(sysRoleMapper.selectById(1L)).thenReturn(existing);
+ when(sysRoleMapper.selectCount(any())).thenReturn(0L);
+ when(sysRoleMapper.updateById(any(SysRole.class))).thenReturn(1);
+
+ assertThatCode(() -> sysRoleService.saveRole(input))
+ .doesNotThrowAnyException();
+ }
+
+ @Test
+ @DisplayName("鍒涘缓瑙掕壊 -> 璁剧疆 builtin=true -> 鎷掔粷")
+ void createRole_setBuiltin_rejected() {
+ SysRole role = buildRole(null, "鍋囩鐞嗗憳", "ROLE_FAKE", 4);
+ role.setBuiltin(true);
+
+ assertThatThrownBy(() -> sysRoleService.saveRole(role))
+ .isInstanceOf(BusinessErrorException.class)
+ .hasMessageContaining("涓嶅厑璁搁€氳繃鎺ュ彛鍒涘缓鍐呯疆瑙掕壊");
+ }
+
+ @Test
+ @DisplayName("缂栬緫涓嶅瓨鍦ㄧ殑 id -> 璁剧疆 builtin=true -> 鎷掔粷锛堥槻姝?saveOrUpdate 鐨?insert 缁曡繃锛?)
+ void editNonExistentRole_setBuiltin_rejected() {
+ SysRole role = buildRole(999L, "鍋囩鐞嗗憳", "ROLE_FAKE", 4);
+ role.setBuiltin(true);
+ when(sysRoleMapper.selectById(999L)).thenReturn(null);
+
+ assertThatThrownBy(() -> sysRoleService.saveRole(role))
+ .isInstanceOf(BusinessErrorException.class)
+ .hasMessageContaining("涓嶅厑璁搁€氳繃鎺ュ彛鍒涘缓鍐呯疆瑙掕壊");
+ }
+
+ // ==================== dataScope 鑼冨洿鏍¢獙 ====================
+
+ @Test
+ @DisplayName("鍒涘缓瑙掕壊 -> dataScope=0锛堣秺鐣岋級 -> 鎷掔粷")
+ void createRole_dataScopeTooLow_rejected() {
+ SysRole role = buildRole(null, "閿€鍞?, "ROLE_SALES", 0);
+
+ assertThatThrownBy(() -> sysRoleService.saveRole(role))
+ .isInstanceOf(BusinessErrorException.class)
+ .hasMessageContaining("鏁版嵁鑼冨洿蹇呴』鍦?1-4 鑼冨洿鍐?);
+ }
+
+ @Test
+ @DisplayName("鍒涘缓瑙掕壊 -> dataScope=5锛堣秺鐣岋級 -> 鎷掔粷")
+ void createRole_dataScopeTooHigh_rejected() {
+ SysRole role = buildRole(null, "閿€鍞?, "ROLE_SALES", 5);
+
+ assertThatThrownBy(() -> sysRoleService.saveRole(role))
+ .isInstanceOf(BusinessErrorException.class)
+ .hasMessageContaining("鏁版嵁鑼冨洿蹇呴』鍦?1-4 鑼冨洿鍐?);
+ }
+
+ @Test
+ @DisplayName("鍒涘缓瑙掕壊 -> dataScope=3锛堝悎娉曪級 -> 閫氳繃")
+ void createRole_validDataScope_passes() {
+ SysRole role = buildRole(null, "閿€鍞?, "ROLE_SALES", 3);
+ when(sysRoleMapper.selectCount(any())).thenReturn(0L);
+ when(sysRoleMapper.insert(any(SysRole.class))).thenReturn(1);
+
+ assertThatCode(() -> sysRoleService.saveRole(role))
+ .doesNotThrowAnyException();
+ }
+
+ // ==================== 鍩烘湰闈炵┖鏍¢獙 ====================
+
+ @Test
+ @DisplayName("鍒涘缓瑙掕壊 -> roleName 涓虹┖ -> 鎷掔粷")
+ void createRole_blankName_rejected() {
+ SysRole role = buildRole(null, "", "ROLE_SALES", 1);
+
+ assertThatThrownBy(() -> sysRoleService.saveRole(role))
+ .isInstanceOf(BusinessErrorException.class)
+ .hasMessageContaining("瑙掕壊鍚嶇О涓嶈兘涓虹┖");
+ }
+
+ @Test
+ @DisplayName("鍒涘缓瑙掕壊 -> roleCode 涓虹┖ -> 鎷掔粷")
+ void createRole_blankCode_rejected() {
+ SysRole role = buildRole(null, "閿€鍞?, "", 1);
+
+ assertThatThrownBy(() -> sysRoleService.saveRole(role))
+ .isInstanceOf(BusinessErrorException.class)
+ .hasMessageContaining("瑙掕壊缂栫爜涓嶈兘涓虹┖");
+ }
+
+ // ==================== 鏉冮檺鍒嗛厤锛堢鍏堣ˉ鍏級 ====================
+
+ @Test
+ @DisplayName("assignResources -> 璋冪敤绁栧厛琛ュ叏 -> 鍐欏叆鐨?menuIds 鍖呭惈绁栧厛")
+ void assignResources_ancestorCompletion_writesAllIds() {
+ // 杈撳叆鍙紶鍙跺瓙鑺傜偣 100L锛岀鍏堣ˉ鍏ㄥ悗搴斿寘鍚?100L + 10L + 1L
+ when(sysMenuService.getAncestorIds(List.of(100L)))
+ .thenReturn(Set.of(100L, 10L, 1L));
+ when(sysRoleMenuMapper.delete(any())).thenReturn(1);
+ when(sysRoleMenuMapper.insert(any(SysRoleMenu.class))).thenReturn(1);
+
+ sysRoleService.assignResources(1L, List.of(100L));
+
+ // 鐢?ArgumentCaptor 鎹曡幏鍐欏叆鐨?SysRoleMenu锛岄獙璇?menuId 闆嗗悎鍖呭惈绁栧厛
+ ArgumentCaptor<SysRoleMenu> captor = ArgumentCaptor.forClass(SysRoleMenu.class);
+ verify(sysRoleMenuMapper, atLeastOnce()).insert(captor.capture());
+ List<Long> writtenMenuIds = captor.getAllValues().stream()
+ .map(SysRoleMenu::getMenuId).toList();
+ assertThat(writtenMenuIds).containsExactlyInAnyOrder(100L, 10L, 1L);
+ assertThat(captor.getAllValues()).allSatisfy(rm ->
+ assertThat(rm.getRoleId()).isEqualTo(1L));
+ }
+
+ @Test
+ @DisplayName("assignResources -> 鍏堝垹鍚庢彃鍏ㄩ噺鏇挎崲 sys_role_menu")
+ void assignResources_fullReplacement() {
+ when(sysMenuService.getAncestorIds(List.of(200L)))
+ .thenReturn(Set.of(200L));
+ when(sysRoleMenuMapper.delete(any())).thenReturn(1);
+ when(sysRoleMenuMapper.insert(any(SysRoleMenu.class))).thenReturn(1);
+
+ sysRoleService.assignResources(1L, List.of(200L));
+
+ // 楠岃瘉鍐欏叆鐨?menuId 闆嗗悎浠呬负琛ュ叏鍚庣殑 {200L}
+ ArgumentCaptor<SysRoleMenu> captor = ArgumentCaptor.forClass(SysRoleMenu.class);
+ verify(sysRoleMenuMapper, atLeastOnce()).insert(captor.capture());
+ assertThat(captor.getAllValues()).hasSize(1);
+ assertThat(captor.getAllValues().get(0).getMenuId()).isEqualTo(200L);
+ }
+
+ // ==================== 瑙掕壊璇︽儏 ====================
+
+ @Test
+ @DisplayName("getRoleDetail -> 杩斿洖 RoleDetailVO 鍚熀鏈俊鎭拰 resourceIds")
+ void getRoleDetail_returnsVO() {
+ SysRole role = buildRole(1L, "閿€鍞?, "ROLE_SALES", 1);
+ when(sysRoleMapper.selectById(1L)).thenReturn(role);
+ SysRoleMenu rm1 = new SysRoleMenu();
+ rm1.setMenuId(100L);
+ SysRoleMenu rm2 = new SysRoleMenu();
+ rm2.setMenuId(200L);
+ when(sysRoleMenuMapper.selectList(any())).thenReturn(List.of(rm1, rm2));
+
+ RoleDetailVO vo = sysRoleService.getRoleDetail(1L);
+
+ assertThat(vo).isNotNull();
+ assertThat(vo.getRoleName()).isEqualTo("閿€鍞?);
+ assertThat(vo.getRoleCode()).isEqualTo("ROLE_SALES");
+ assertThat(vo.getResourceIds()).containsExactly(100L, 200L);
+ }
+
+ @Test
+ @DisplayName("getRoleDetail -> 瑙掕壊涓嶅瓨鍦?-> 鎷掔粷")
+ void getRoleDetail_notFound_rejected() {
+ when(sysRoleMapper.selectById(999L)).thenReturn(null);
+
+ assertThatThrownBy(() -> sysRoleService.getRoleDetail(999L))
+ .isInstanceOf(BusinessErrorException.class)
+ .hasMessageContaining("瑙掕壊涓嶅瓨鍦?);
+ }
+
+ // ==================== 瑙掕壊鍒犻櫎锛堢骇鑱旀竻鐞嗭級 ====================
+
+ @Test
+ @DisplayName("鍒犻櫎鍐呯疆瑙掕壊 -> builtin=true -> 鎷掔粷")
+ void deleteBuiltinRole_rejected() {
+ SysRole role = buildRole(1L, "绠$悊鍛?, "ROLE_ADMIN", 4);
+ role.setBuiltin(true);
+ when(sysRoleMapper.selectById(1L)).thenReturn(role);
+
+ assertThatThrownBy(() -> sysRoleService.deleteRoleCascade(1L))
+ .isInstanceOf(BusinessErrorException.class)
+ .hasMessageContaining("鍐呯疆瑙掕壊涓嶅厑璁稿垹闄?);
+ }
+
+ @Test
+ @DisplayName("鍒犻櫎鏅€氳鑹?-> builtin=false -> 閫氳繃骞舵竻鐞?sys_role_menu + sys_user_role")
+ void deleteNormalRole_passes() {
+ SysRole role = buildRole(2L, "閿€鍞?, "ROLE_SALES", 1);
+ when(sysRoleMapper.selectById(2L)).thenReturn(role);
+ when(sysRoleMenuMapper.delete(any())).thenReturn(1);
+ when(sysUserRoleMapper.delete(any())).thenReturn(1);
+
+ assertThatCode(() -> sysRoleService.deleteRoleCascade(2L))
+ .doesNotThrowAnyException();
+ }
+
+ // ==================== 妗╂暟鎹?====================
+
+ private SysRole buildRole(Long id, String name, String code, int dataScope) {
+ SysRole role = new SysRole();
+ role.setId(id);
+ role.setRoleName(name);
+ role.setRoleCode(code);
+ role.setDataScope(dataScope);
+ role.setSort(0);
+ return role;
+ }
+}
diff --git a/crm-auth/src/test/java/com/crm/auth/service/impl/UserListIntegrationTest.java b/crm-auth/src/test/java/com/crm/auth/service/impl/UserListIntegrationTest.java
index 2e0c173..be66a5e 100644
--- a/crm-auth/src/test/java/com/crm/auth/service/impl/UserListIntegrationTest.java
+++ b/crm-auth/src/test/java/com/crm/auth/service/impl/UserListIntegrationTest.java
@@ -332,7 +332,8 @@ class UserListIntegrationTest {
create table sys_role (
id bigint primary key, creator_id varchar(50), create_time datetime,
updater_id varchar(50), update_time datetime, deleted tinyint not null default 0,
- role_name varchar(50), role_code varchar(50), data_scope tinyint, sort int, remark varchar(200))
+ role_name varchar(50), role_code varchar(50), data_scope tinyint, sort int, remark varchar(200),
+ builtin boolean default false)
""",
"""
create table sys_user_role (