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.
1010 lines
78 KiB
1010 lines
78 KiB
diff --git a/crm-auth/pom.xml b/crm-auth/pom.xml
|
|
index 1920720..5d82910 100644
|
|
--- a/crm-auth/pom.xml
|
|
+++ b/crm-auth/pom.xml
|
|
@@ -23,6 +23,11 @@
|
|
<artifactId>crm-base</artifactId>
|
|
</dependency>
|
|
|
|
+ <dependency>
|
|
+ <groupId>com.crm</groupId>
|
|
+ <artifactId>crm-file</artifactId>
|
|
+ </dependency>
|
|
+
|
|
<!-- 瀹屾暣瀹夊叏鏍堬紙crm-base 鍙緷璧栦簡 security-core锛?-->
|
|
<dependency>
|
|
<groupId>org.springframework.boot</groupId>
|
|
diff --git a/crm-auth/src/main/java/com/crm/auth/config/SecurityConfig.java b/crm-auth/src/main/java/com/crm/auth/config/SecurityConfig.java
|
|
index fe244fa..e86c009 100644
|
|
--- a/crm-auth/src/main/java/com/crm/auth/config/SecurityConfig.java
|
|
+++ b/crm-auth/src/main/java/com/crm/auth/config/SecurityConfig.java
|
|
@@ -29,6 +29,8 @@ public class SecurityConfig {
|
|
private static final String[] DEFAULT_IGNORE_URLS = {
|
|
// 鐧诲綍鎺ュ彛
|
|
"/api/auth/login/**",
|
|
+ // 璧勬簮鏍戠鐞嗘帴鍙o紙ADR-0011锛?+ "/api/resources/**",
|
|
// 鎺ュ彛鏂囨。
|
|
"/doc.html",
|
|
"/webjars/**",
|
|
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
|
|
new file mode 100644
|
|
index 0000000..93849e3
|
|
--- /dev/null
|
|
+++ b/crm-auth/src/main/java/com/crm/auth/controller/ResourceController.java
|
|
@@ -0,0 +1,47 @@
|
|
+package com.crm.auth.controller;
|
|
+
|
|
+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 lombok.RequiredArgsConstructor;
|
|
+import org.springframework.web.bind.annotation.*;
|
|
+import org.springframework.web.multipart.MultipartFile;
|
|
+
|
|
+import java.util.List;
|
|
+
|
|
+/**
|
|
+ * 缁熶竴鏉冮檺璧勬簮鏍戠鐞嗙鎺ュ彛锛圓DR-0011锛?+ */
|
|
+@RestController
|
|
+@RequestMapping("/api/resources")
|
|
+@RequiredArgsConstructor
|
|
+public class ResourceController {
|
|
+
|
|
+ private final IResourceService resourceService;
|
|
+
|
|
+ /** 鍏ㄩ噺璧勬簮鏍戯紙鎵佸钩鏁扮粍锛屽墠绔寜 parentId 缁勬爲锛?*/
|
|
+ @GetMapping
|
|
+ public Result<List<ResourceNode>> listAll() {
|
|
+ return Result.success(resourceService.listAll());
|
|
+ }
|
|
+
|
|
+ /** 鏂板鎴栫紪杈戣妭鐐癸紙鍚牎楠岋級 */
|
|
+ @PostMapping
|
|
+ public Result<ResourceNode> save(@RequestBody ResourceNode node) {
|
|
+ return Result.success(resourceService.save(node));
|
|
+ }
|
|
+
|
|
+ /** 鍒犻櫎鑺傜偣锛堝惈瀛愯妭鐐规鏌?+ 绾ц仈娓呯悊锛?*/
|
|
+ @DeleteMapping("/{id}")
|
|
+ public Result<Void> delete(@PathVariable Long id) {
|
|
+ resourceService.delete(id);
|
|
+ return Result.success();
|
|
+ }
|
|
+
|
|
+ /** 鍥炬爣涓婁紶锛堟牸寮?澶у皬鏍¢獙锛?*/
|
|
+ @PostMapping("/icon/upload")
|
|
+ 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/service/IResourceService.java b/crm-auth/src/main/java/com/crm/auth/service/IResourceService.java
|
|
new file mode 100644
|
|
index 0000000..0f44876
|
|
--- /dev/null
|
|
+++ b/crm-auth/src/main/java/com/crm/auth/service/IResourceService.java
|
|
@@ -0,0 +1,25 @@
|
|
+package com.crm.auth.service;
|
|
+
|
|
+import com.crm.auth.domain.dto.ResourceNode;
|
|
+import com.crm.file.domain.dto.FileInfoDTO;
|
|
+import org.springframework.web.multipart.MultipartFile;
|
|
+
|
|
+import java.util.List;
|
|
+
|
|
+/**
|
|
+ * 缁熶竴鏉冮檺璧勬簮鏍戞湇鍔★紙ADR-0011锛?+ */
|
|
+public interface IResourceService {
|
|
+
|
|
+ /** 鍏ㄩ噺璧勬簮鏍戞墎骞虫暟缁勶紙鎵€鏈夎妭鐐癸紝涓嶆寜瑙掕壊杩囨护锛?*/
|
|
+ List<ResourceNode> listAll();
|
|
+
|
|
+ /** 鏂板鎴栫紪杈戣妭鐐癸紙鍚眰绾х害鏉熴€佸瓧娈靛繀濉€佺紪杈戦攣 type锛?*/
|
|
+ ResourceNode save(ResourceNode node);
|
|
+
|
|
+ /** 鍒犻櫎鑺傜偣锛堝惈瀛愯妭鐐规鏌?+ 绾ц仈娓呯悊 sys_role_menu 鎺堟潈寮曠敤锛?*/
|
|
+ void delete(Long id);
|
|
+
|
|
+ /** 鍥炬爣涓婁紶锛堟牸寮?澶у皬鏍¢獙 + 璋?FileApi 瀛樺埌 MinIO锛岃繑鍥炴枃浠朵俊鎭級 */
|
|
+ FileInfoDTO uploadIcon(MultipartFile file);
|
|
+}
|
|
diff --git a/crm-auth/src/main/java/com/crm/auth/service/impl/PermissionServiceImpl.java b/crm-auth/src/main/java/com/crm/auth/service/impl/PermissionServiceImpl.java
|
|
index 650533d..202d36e 100644
|
|
--- a/crm-auth/src/main/java/com/crm/auth/service/impl/PermissionServiceImpl.java
|
|
+++ b/crm-auth/src/main/java/com/crm/auth/service/impl/PermissionServiceImpl.java
|
|
@@ -1,6 +1,7 @@
|
|
package com.crm.auth.service.impl;
|
|
|
|
import cn.hutool.core.collection.CollUtil;
|
|
+import cn.hutool.core.util.StrUtil;
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
import com.crm.auth.domain.entity.*;
|
|
import com.crm.auth.mapper.*;
|
|
@@ -69,10 +70,36 @@ public class PermissionServiceImpl {
|
|
}
|
|
DataVisibilityContext.load(
|
|
new DataVisibility(userId, primaryDeptId, List.copyOf(deptIds), widest, expandedDeptIds));
|
|
- return roles.stream()
|
|
+
|
|
+ // 鈹€鈹€ 鏉冮檺鐮佸苟闆嗭紙ADR-0011锛夛細鏀堕泦鐢ㄦ埛鎵€鏈夎鑹叉巿鏉冪殑 button 鑺傜偣 perms 鈹€鈹€
|
|
+ List<String> authorities = new ArrayList<>();
|
|
+ if (CollUtil.isNotEmpty(userRoles)) {
|
|
+ try {
|
|
+ Set<Long> roleIds = userRoles.stream().map(SysUserRole::getRoleId).collect(Collectors.toSet());
|
|
+ List<SysRoleMenu> allRoleMenus = sysRoleMenuMapper.selectList(
|
|
+ new LambdaQueryWrapper<SysRoleMenu>().in(SysRoleMenu::getRoleId, roleIds));
|
|
+ if (CollUtil.isNotEmpty(allRoleMenus)) {
|
|
+ Set<Long> menuIds = allRoleMenus.stream()
|
|
+ .map(SysRoleMenu::getMenuId).collect(Collectors.toSet());
|
|
+ List<SysMenu> menus = sysMenuMapper.selectBatchIds(menuIds);
|
|
+ menus.stream()
|
|
+ .filter(m -> m.getMenuType() != null && m.getMenuType() == 3)
|
|
+ .filter(m -> "enabled".equals(m.getStatus()))
|
|
+ .map(SysMenu::getPerms)
|
|
+ .filter(StrUtil::isNotBlank)
|
|
+ .distinct()
|
|
+ .forEach(authorities::add);
|
|
+ }
|
|
+ } catch (Exception e) {
|
|
+ log.debug("鏉冮檺鐮佸苟闆嗘煡璇㈠け璐ワ紙鍙兘 sys_menu/sys_role_menu 琛ㄥ皻鏈垱寤猴級锛岃烦杩?, e);
|
|
+ }
|
|
+ }
|
|
+ // 瑙掕壊缂栫爜杩藉姞鍦ㄦ潈闄愮爜涔嬪悗锛堜袱鑰呭悓涓?Spring Security authority锛宧asRole/hasAuthority 鍚勮嚜鍖归厤锛?+ roles.stream()
|
|
.map(SysRole::getRoleCode)
|
|
.filter(Objects::nonNull)
|
|
- .toList();
|
|
+ .forEach(authorities::add);
|
|
+ return authorities;
|
|
}
|
|
|
|
public List<SysMenu> getMenuTree(Long userId) {
|
|
diff --git a/crm-auth/src/main/java/com/crm/auth/service/impl/ResourceServiceImpl.java b/crm-auth/src/main/java/com/crm/auth/service/impl/ResourceServiceImpl.java
|
|
new file mode 100644
|
|
index 0000000..d0705fb
|
|
--- /dev/null
|
|
+++ b/crm-auth/src/main/java/com/crm/auth/service/impl/ResourceServiceImpl.java
|
|
@@ -0,0 +1,261 @@
|
|
+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.ResourceNode;
|
|
+import com.crm.auth.domain.entity.SysMenu;
|
|
+import com.crm.auth.domain.entity.SysRoleMenu;
|
|
+import com.crm.auth.domain.enums.MenuType;
|
|
+import com.crm.auth.mapper.SysRoleMenuMapper;
|
|
+import com.crm.auth.service.IResourceService;
|
|
+import com.crm.auth.service.ISysMenuService;
|
|
+import com.crm.base.domain.exception.BusinessErrorException;
|
|
+import com.crm.file.api.FileApi;
|
|
+import com.crm.file.domain.dto.FileInfoDTO;
|
|
+import lombok.RequiredArgsConstructor;
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
+import org.springframework.stereotype.Service;
|
|
+import org.springframework.web.multipart.MultipartFile;
|
|
+
|
|
+import java.io.IOException;
|
|
+import java.util.List;
|
|
+import java.util.Set;
|
|
+import java.util.stream.Collectors;
|
|
+
|
|
+/**
|
|
+ * 缁熶竴鏉冮檺璧勬簮鏍戞湇鍔″疄鐜帮紙ADR-0011锛?+ */
|
|
+@Slf4j
|
|
+@Service
|
|
+@RequiredArgsConstructor
|
|
+public class ResourceServiceImpl implements IResourceService {
|
|
+
|
|
+ /** 璧勬簮鑺傜偣鍙傛暟闈炴硶 */
|
|
+ private static final int CODE_RESOURCE_INVALID = 61009;
|
|
+
|
|
+ private final ISysMenuService sysMenuService;
|
|
+ private final SysRoleMenuMapper sysRoleMenuMapper;
|
|
+ private final FileApi fileApi;
|
|
+
|
|
+ @Override
|
|
+ public List<ResourceNode> listAll() {
|
|
+ return sysMenuService.list().stream()
|
|
+ .map(ResourceNode::fromEntity)
|
|
+ .collect(Collectors.toList());
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public ResourceNode save(ResourceNode node) {
|
|
+ if (node == null) {
|
|
+ throw new BusinessErrorException(CODE_RESOURCE_INVALID, "璧勬簮鑺傜偣涓嶈兘涓虹┖");
|
|
+ }
|
|
+ if (StrUtil.isBlank(node.getName())) {
|
|
+ throw new BusinessErrorException(CODE_RESOURCE_INVALID, "鑺傜偣鍚嶇О涓嶈兘涓虹┖");
|
|
+ }
|
|
+ if (node.getType() == null) {
|
|
+ throw new BusinessErrorException(CODE_RESOURCE_INVALID, "鑺傜偣绫诲瀷涓嶈兘涓虹┖");
|
|
+ }
|
|
+
|
|
+ SysMenu entity;
|
|
+ if (node.getId() != null) {
|
|
+ // 缂栬緫妯″紡
|
|
+ entity = sysMenuService.getById(node.getId());
|
|
+ if (entity == null) {
|
|
+ throw new BusinessErrorException(CODE_RESOURCE_INVALID, "璧勬簮鑺傜偣涓嶅瓨鍦細id=" + node.getId());
|
|
+ }
|
|
+ // 缂栬緫閿侊細type 涓嶅彲鍙?+ if (entity.getMenuType() != null && !entity.getMenuType().equals(node.getType().getCode())) {
|
|
+ throw new BusinessErrorException(CODE_RESOURCE_INVALID, "鑺傜偣绫诲瀷涓嶅彲鍙樻洿锛堢幇鏈夌被鍨?"
|
|
+ + MenuType.fromCode(entity.getMenuType()) + "锛?);
|
|
+ }
|
|
+ } else {
|
|
+ entity = new SysMenu();
|
|
+ }
|
|
+
|
|
+ // 瀛楁蹇呭~锛堟寜 type锛岀紪杈戞ā寮忓彲澶嶇敤宸叉湁鍊硷級
|
|
+ validateRequiredFields(node, entity);
|
|
+
|
|
+ // 灞傜骇绾︽潫
|
|
+ validateHierarchy(node);
|
|
+
|
|
+ // DTO 鈫?Entity 鏄犲皠
|
|
+ applyToEntity(node, entity);
|
|
+
|
|
+ sysMenuService.saveOrUpdate(entity);
|
|
+ log.info("淇濆瓨璧勬簮鑺傜偣锛歩d={}, name={}, type={}", entity.getId(), entity.getMenuName(),
|
|
+ MenuType.fromCode(entity.getMenuType()));
|
|
+
|
|
+ return ResourceNode.fromEntity(entity);
|
|
+ }
|
|
+
|
|
+ @Override
|
|
+ public void delete(Long id) {
|
|
+ SysMenu node = sysMenuService.getById(id);
|
|
+ if (node == null) {
|
|
+ throw new BusinessErrorException(CODE_RESOURCE_INVALID, "璧勬簮鑺傜偣涓嶅瓨鍦細id=" + id);
|
|
+ }
|
|
+
|
|
+ // 瀛愯妭鐐规鏌?+ long childCount = sysMenuService.count(
|
|
+ new LambdaQueryWrapper<SysMenu>().eq(SysMenu::getParentId, id));
|
|
+ if (childCount > 0) {
|
|
+ throw new BusinessErrorException(CODE_RESOURCE_INVALID,
|
|
+ "鑺傜偣涓嬫湁 " + childCount + " 涓瓙鑺傜偣锛岃鍏堝垹闄ゅ瓙鑺傜偣");
|
|
+ }
|
|
+
|
|
+ // 绾ц仈娓呯悊瑙掕壊鎺堟潈寮曠敤
|
|
+ int deletedRefs = sysRoleMenuMapper.delete(
|
|
+ new LambdaQueryWrapper<SysRoleMenu>().eq(SysRoleMenu::getMenuId, id));
|
|
+ if (deletedRefs > 0) {
|
|
+ log.info("鍒犻櫎璧勬簮鑺傜偣鏃剁骇鑱旀竻鐞嗚鑹叉巿鏉冨紩鐢細menuId={}, 娓呯悊浜?{} 鏉¤褰?, id, deletedRefs);
|
|
+ }
|
|
+
|
|
+ sysMenuService.removeById(id);
|
|
+ log.info("鍒犻櫎璧勬簮鑺傜偣锛歩d={}, name={}", id, node.getMenuName());
|
|
+ }
|
|
+
|
|
+ // ==================== 鍥炬爣涓婁紶 ====================
|
|
+
|
|
+ /** 鍚堟硶鍥炬爣鎵╁睍鍚?*/
|
|
+ private static final Set<String> ALLOWED_ICON_EXTENSIONS = Set.of(
|
|
+ "png", "jpg", "jpeg", "gif", "svg", "webp");
|
|
+
|
|
+ /** 鍥炬爣涓婁紶澶у皬涓婇檺锛?00KB锛?*/
|
|
+ private static final long MAX_ICON_SIZE = 500 * 1024;
|
|
+
|
|
+ @Override
|
|
+ public FileInfoDTO uploadIcon(MultipartFile file) {
|
|
+ // 绌烘枃浠舵鏌?+ if (file == null || file.isEmpty()) {
|
|
+ throw new BusinessErrorException(CODE_RESOURCE_INVALID, "鍥炬爣鏂囦欢涓嶈兘涓虹┖");
|
|
+ }
|
|
+
|
|
+ // 澶у皬鏍¢獙
|
|
+ if (file.getSize() > MAX_ICON_SIZE) {
|
|
+ throw new BusinessErrorException(CODE_RESOURCE_INVALID,
|
|
+ "鍥炬爣澶у皬涓嶈兘瓒呰繃 500KB锛堝綋鍓? " + (file.getSize() / 1024) + "KB锛?);
|
|
+ }
|
|
+
|
|
+ String originalName = file.getOriginalFilename();
|
|
+ if (StrUtil.isBlank(originalName)) {
|
|
+ throw new BusinessErrorException(CODE_RESOURCE_INVALID, "鍥炬爣鏂囦欢鍚嶄笉鑳戒负绌?);
|
|
+ }
|
|
+
|
|
+ // 鏍煎紡鏍¢獙
|
|
+ String ext = StrUtil.subAfter(originalName, '.', true);
|
|
+ if (StrUtil.isBlank(ext) || !ALLOWED_ICON_EXTENSIONS.contains(ext.toLowerCase())) {
|
|
+ throw new BusinessErrorException(CODE_RESOURCE_INVALID,
|
|
+ "涓嶆敮鎸佺殑鍥炬爣鏍煎紡: " + ext + "锛屽厑璁告牸寮? " + ALLOWED_ICON_EXTENSIONS);
|
|
+ }
|
|
+
|
|
+ try {
|
|
+ FileInfoDTO result = fileApi.upload(file.getBytes(), originalName,
|
|
+ file.getContentType(), "resources/icon");
|
|
+ log.info("鍥炬爣涓婁紶鎴愬姛锛歠ileId={}, name={}", result.getFileId(), originalName);
|
|
+ return result;
|
|
+ } catch (IOException e) {
|
|
+ log.error("鍥炬爣涓婁紶璇诲彇鏂囦欢澶辫触锛歿}", originalName, e);
|
|
+ throw new BusinessErrorException(CODE_RESOURCE_INVALID, "鍥炬爣鏂囦欢璇诲彇澶辫触锛岃閲嶈瘯");
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // ==================== 鏍¢獙閫昏緫 ====================
|
|
+
|
|
+ /** 灞傜骇绾︽潫锛? 绉嶉潪娉曠埗瀛愮粍鍚?*/
|
|
+ private void validateHierarchy(ResourceNode node) {
|
|
+ MenuType childType = node.getType();
|
|
+
|
|
+ if (node.getParentId() == null || node.getParentId() == 0) {
|
|
+ // 鏍硅妭鐐癸細鍙厑璁?catalog / menu
|
|
+ if (childType == MenuType.BUTTON) {
|
|
+ throw new BusinessErrorException(CODE_RESOURCE_INVALID, "鏍硅妭鐐逛笉鍏佽娣诲姞 button 绫诲瀷");
|
|
+ }
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ SysMenu parent = sysMenuService.getById(node.getParentId());
|
|
+ if (parent == null) {
|
|
+ throw new BusinessErrorException(CODE_RESOURCE_INVALID, "鐖惰妭鐐逛笉瀛樺湪锛歱arentId=" + node.getParentId());
|
|
+ }
|
|
+ MenuType parentType = MenuType.fromCode(parent.getMenuType());
|
|
+
|
|
+ // 鍙跺瓙鑺傜偣
|
|
+ if (parentType == MenuType.BUTTON) {
|
|
+ throw new BusinessErrorException(CODE_RESOURCE_INVALID, "button 鏄彾瀛愯妭鐐癸紝涓嶅彲娣诲姞瀛愯妭鐐?);
|
|
+ }
|
|
+ // catalog 鍙兘鎸?menu
|
|
+ if (parentType == MenuType.CATALOG && childType != MenuType.MENU) {
|
|
+ throw new BusinessErrorException(CODE_RESOURCE_INVALID, "catalog 鑺傜偣涓嬪彧鑳芥坊鍔?menu 绫诲瀷");
|
|
+ }
|
|
+ // menu 鍙兘鎸?button
|
|
+ if (parentType == MenuType.MENU && childType != MenuType.BUTTON) {
|
|
+ throw new BusinessErrorException(CODE_RESOURCE_INVALID, "menu 鑺傜偣涓嬪彧鑳芥坊鍔?button 绫诲瀷");
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /** 瀛楁蹇呭~鏍¢獙锛堢紪杈戞ā寮忥細鑻?route 鏈紶浣?DB 宸叉湁 path 鍒欐斁琛岋級 */
|
|
+ private void validateRequiredFields(ResourceNode node, SysMenu existing) {
|
|
+ MenuType type = node.getType();
|
|
+
|
|
+ if (type == MenuType.MENU) {
|
|
+ boolean hasRoute = StrUtil.isNotBlank(node.getRoute());
|
|
+ boolean hasExistingPath = existing != null && StrUtil.isNotBlank(existing.getPath());
|
|
+ if (!hasRoute && !hasExistingPath) {
|
|
+ throw new BusinessErrorException(CODE_RESOURCE_INVALID, "menu 绫诲瀷蹇呴』鎻愪緵璺敱锛坮oute锛?);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (type == MenuType.BUTTON) {
|
|
+ if (StrUtil.isBlank(node.getPerms())) {
|
|
+ throw new BusinessErrorException(CODE_RESOURCE_INVALID, "button 绫诲瀷蹇呴』鎻愪緵鏉冮檺鐮侊紙perms锛?);
|
|
+ }
|
|
+ if (StrUtil.isBlank(node.getDenyBehavior())) {
|
|
+ throw new BusinessErrorException(CODE_RESOURCE_INVALID, "button 绫诲瀷蹇呴』鎻愪緵鏃犳潈闄愯涓猴紙denyBehavior锛?);
|
|
+ }
|
|
+ if (StrUtil.isBlank(node.getApiUrl())) {
|
|
+ throw new BusinessErrorException(CODE_RESOURCE_INVALID, "button 绫诲瀷蹇呴』鎻愪緵鎺ュ彛 URL锛坅piUrl锛?);
|
|
+ }
|
|
+ if (StrUtil.isBlank(node.getStatus())) {
|
|
+ throw new BusinessErrorException(CODE_RESOURCE_INVALID, "button 绫诲瀷蹇呴』鎻愪緵鐘舵€侊紙status锛?);
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ // ==================== DTO 鈫?Entity ====================
|
|
+
|
|
+ /** 灏?ResourceNode 涓殑涓氬姟瀛楁鍐欏叆 SysMenu锛堟槧灏?name鈫抦enuName, route鈫抪ath, type鈫抦enuType锛?*/
|
|
+ private void applyToEntity(ResourceNode node, SysMenu entity) {
|
|
+ entity.setMenuName(node.getName());
|
|
+ entity.setParentId(node.getParentId() != null ? node.getParentId() : 0L);
|
|
+
|
|
+ if (node.getType() != null) {
|
|
+ entity.setMenuType(node.getType().getCode());
|
|
+ }
|
|
+
|
|
+ // route / path锛氳嫢鏈紶鍒欎繚鐣欏師鏈夊€?+ if (node.getRoute() != null) {
|
|
+ entity.setPath(node.getRoute());
|
|
+ }
|
|
+
|
|
+ entity.setSort(node.getSort() != null ? node.getSort() : 0);
|
|
+ entity.setIcon(node.getIcon());
|
|
+ entity.setVisible(true); // 榛樿鍙
|
|
+
|
|
+ // button 涓撶敤瀛楁锛堜粎 button 绫诲瀷鏃跺啓鍏ワ級
|
|
+ if (node.getType() == MenuType.BUTTON) {
|
|
+ entity.setDescription(node.getDescription());
|
|
+ entity.setPerms(node.getPerms());
|
|
+ entity.setDenyBehavior(node.getDenyBehavior());
|
|
+ entity.setStatus(node.getStatus());
|
|
+ entity.setApiUrl(node.getApiUrl());
|
|
+ } else {
|
|
+ // 闈?button 鑺傜偣娓呮帀杩欎簺瀛楁
|
|
+ entity.setDescription(null);
|
|
+ entity.setPerms(null);
|
|
+ entity.setDenyBehavior(null);
|
|
+ entity.setStatus(null);
|
|
+ entity.setApiUrl(null);
|
|
+ }
|
|
+ }
|
|
+}
|
|
diff --git a/crm-auth/src/test/java/com/crm/auth/service/impl/ResourceServiceImplTest.java b/crm-auth/src/test/java/com/crm/auth/service/impl/ResourceServiceImplTest.java
|
|
new file mode 100644
|
|
index 0000000..15e4990
|
|
--- /dev/null
|
|
+++ b/crm-auth/src/test/java/com/crm/auth/service/impl/ResourceServiceImplTest.java
|
|
@@ -0,0 +1,583 @@
|
|
+package com.crm.auth.service.impl;
|
|
+
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
+import com.crm.auth.domain.dto.ResourceNode;
|
|
+import com.crm.auth.domain.entity.SysMenu;
|
|
+import com.crm.auth.domain.entity.SysRoleMenu;
|
|
+import com.crm.auth.domain.enums.MenuType;
|
|
+import com.crm.auth.mapper.SysRoleMenuMapper;
|
|
+import com.crm.auth.service.ISysMenuService;
|
|
+import com.crm.base.domain.exception.BusinessErrorException;
|
|
+import com.crm.file.api.FileApi;
|
|
+import com.crm.file.domain.dto.FileInfoDTO;
|
|
+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.springframework.web.multipart.MultipartFile;
|
|
+
|
|
+import java.util.List;
|
|
+
|
|
+import static org.assertj.core.api.Assertions.assertThat;
|
|
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
|
+import static org.mockito.ArgumentMatchers.any;
|
|
+import static org.mockito.ArgumentMatchers.anyLong;
|
|
+import static org.mockito.ArgumentMatchers.anyString;
|
|
+import static org.mockito.ArgumentMatchers.eq;
|
|
+import static org.mockito.Mockito.*;
|
|
+
|
|
+/**
|
|
+ * {@link ResourceServiceImpl} 鍗曞厓娴嬭瘯锛氳鐩栬祫婧愭爲鏌ヨ銆佹柊澧?缂栬緫鏍¢獙銆佸眰绾х害鏉熴€佸瓧娈靛繀濉?+ */
|
|
+@ExtendWith(MockitoExtension.class)
|
|
+class ResourceServiceImplTest {
|
|
+
|
|
+ @Mock
|
|
+ private ISysMenuService sysMenuService;
|
|
+ @Mock
|
|
+ private SysRoleMenuMapper sysRoleMenuMapper;
|
|
+ @Mock
|
|
+ private FileApi fileApi;
|
|
+
|
|
+ @InjectMocks
|
|
+ private ResourceServiceImpl resourceService;
|
|
+
|
|
+ // ========== 甯哥敤妗╂暟鎹?==========
|
|
+
|
|
+ private SysMenu catalog;
|
|
+ private SysMenu menu;
|
|
+ private SysMenu button;
|
|
+
|
|
+ @BeforeEach
|
|
+ void setUp() {
|
|
+ catalog = buildMenu(1L, 0L, "绯荤粺绠$悊", 1);
|
|
+ menu = buildMenu(10L, 1L, "鐢ㄦ埛绠$悊", 2);
|
|
+ menu.setPath("/system/user");
|
|
+
|
|
+ button = buildMenu(100L, 10L, "鏌ヨ鐢ㄦ埛", 3);
|
|
+ button.setPerms("crm:user:list");
|
|
+ button.setDenyBehavior("hide");
|
|
+ button.setApiUrl("/api/system/users/page");
|
|
+ button.setStatus("enabled");
|
|
+ }
|
|
+
|
|
+ // ==================== 鏌ヨ ====================
|
|
+
|
|
+ @Test
|
|
+ @DisplayName("鍏ㄩ噺璧勬簮鏍戞煡璇?-> 杩斿洖鎵佸钩 ResourceNode 鏁扮粍锛屽惈 catalog/menu/button")
|
|
+ void listAll_returnsFlatListWithAllTypes() {
|
|
+ when(sysMenuService.list()).thenReturn(List.of(catalog, menu, button));
|
|
+
|
|
+ List<ResourceNode> result = resourceService.listAll();
|
|
+
|
|
+ assertThat(result).hasSize(3);
|
|
+ assertThat(result.get(0).getType()).isEqualTo(MenuType.CATALOG);
|
|
+ assertThat(result.get(0).getName()).isEqualTo("绯荤粺绠$悊");
|
|
+ assertThat(result.get(1).getType()).isEqualTo(MenuType.MENU);
|
|
+ assertThat(result.get(1).getRoute()).isEqualTo("/system/user");
|
|
+ assertThat(result.get(2).getType()).isEqualTo(MenuType.BUTTON);
|
|
+ assertThat(result.get(2).getPerms()).isEqualTo("crm:user:list");
|
|
+ }
|
|
+
|
|
+ @Test
|
|
+ @DisplayName("绌哄簱 -> 杩斿洖绌烘暟缁勪笉鎶涘紓甯?)
|
|
+ void listAll_emptyDb_returnsEmptyList() {
|
|
+ when(sysMenuService.list()).thenReturn(List.of());
|
|
+
|
|
+ List<ResourceNode> result = resourceService.listAll();
|
|
+
|
|
+ assertThat(result).isEmpty();
|
|
+ }
|
|
+
|
|
+ // ==================== 鏂板 ====================
|
|
+
|
|
+ @Test
|
|
+ @DisplayName("鏂板 catalog 鑺傜偣 -> 鎴愬姛 save")
|
|
+ void save_newCatalog_success() {
|
|
+ ResourceNode node = newResourceNode(null, null, "鏂扮洰褰?, MenuType.CATALOG);
|
|
+
|
|
+ resourceService.save(node);
|
|
+
|
|
+ ArgumentCaptor<SysMenu> captor = ArgumentCaptor.forClass(SysMenu.class);
|
|
+ verify(sysMenuService).saveOrUpdate(captor.capture());
|
|
+ SysMenu saved = captor.getValue();
|
|
+ assertThat(saved.getId()).isNull();
|
|
+ assertThat(saved.getMenuType()).isEqualTo(1);
|
|
+ assertThat(saved.getMenuName()).isEqualTo("鏂扮洰褰?);
|
|
+ }
|
|
+
|
|
+ @Test
|
|
+ @DisplayName("鏂板 menu 鎸傚湪 catalog 涓?-> 鎴愬姛 save")
|
|
+ void save_newMenuUnderCatalog_success() {
|
|
+ when(sysMenuService.getById(1L)).thenReturn(catalog);
|
|
+
|
|
+ ResourceNode node = newResourceNode(null, 1L, "鏂拌彍鍗?, MenuType.MENU);
|
|
+ node.setRoute("/new/menu");
|
|
+
|
|
+ resourceService.save(node);
|
|
+
|
|
+ verify(sysMenuService).saveOrUpdate(any(SysMenu.class));
|
|
+ }
|
|
+
|
|
+ @Test
|
|
+ @DisplayName("鏂板 button 鎸傚湪 menu 涓?-> 鎴愬姛 save")
|
|
+ void save_newButtonUnderMenu_success() {
|
|
+ when(sysMenuService.getById(10L)).thenReturn(menu);
|
|
+
|
|
+ ResourceNode node = newButtonNode(null, 10L, "鏂板鎸夐挳", "crm:user:add", "hide", "/api/user/add", "enabled");
|
|
+
|
|
+ resourceService.save(node);
|
|
+
|
|
+ verify(sysMenuService).saveOrUpdate(any(SysMenu.class));
|
|
+ }
|
|
+
|
|
+ // ==================== 灞傜骇绾︽潫 ====================
|
|
+
|
|
+ @Test
|
|
+ @DisplayName("catalog 涓嬫寕 button -> 鎷掔粷锛坈atalog 鍙兘鎸?menu锛?)
|
|
+ void save_buttonUnderCatalog_rejected() {
|
|
+ when(sysMenuService.getById(1L)).thenReturn(catalog);
|
|
+
|
|
+ ResourceNode node = newButtonNode(null, 1L, "闈炴硶鎸夐挳", "crm:x:y", "hide", "/api/x", "enabled");
|
|
+
|
|
+ assertThatThrownBy(() -> resourceService.save(node))
|
|
+ .isInstanceOf(BusinessErrorException.class)
|
|
+ .hasMessageContaining("catalog")
|
|
+ .hasMessageContaining("menu");
|
|
+ }
|
|
+
|
|
+ @Test
|
|
+ @DisplayName("menu 涓嬫寕 catalog -> 鎷掔粷锛坢enu 鍙兘鎸?button锛?)
|
|
+ void save_catalogUnderMenu_rejected() {
|
|
+ when(sysMenuService.getById(10L)).thenReturn(menu);
|
|
+
|
|
+ ResourceNode node = newResourceNode(null, 10L, "闈炴硶鐩綍", MenuType.CATALOG);
|
|
+
|
|
+ assertThatThrownBy(() -> resourceService.save(node))
|
|
+ .isInstanceOf(BusinessErrorException.class)
|
|
+ .hasMessageContaining("menu")
|
|
+ .hasMessageContaining("button");
|
|
+ }
|
|
+
|
|
+ @Test
|
|
+ @DisplayName("menu 涓嬫寕 menu -> 鎷掔粷锛坢enu 鍙兘鎸?button锛?)
|
|
+ void save_menuUnderMenu_rejected() {
|
|
+ when(sysMenuService.getById(10L)).thenReturn(menu);
|
|
+
|
|
+ ResourceNode node = newResourceNode(null, 10L, "闈炴硶瀛愯彍鍗?, MenuType.MENU);
|
|
+ node.setRoute("/sub/menu");
|
|
+
|
|
+ assertThatThrownBy(() -> resourceService.save(node))
|
|
+ .isInstanceOf(BusinessErrorException.class);
|
|
+ }
|
|
+
|
|
+ @Test
|
|
+ @DisplayName("button 涓嬫寕瀛愯妭鐐?-> 鎷掔粷锛坆utton 鏄彾瀛愶級")
|
|
+ void save_childUnderButton_rejected() {
|
|
+ when(sysMenuService.getById(100L)).thenReturn(button);
|
|
+
|
|
+ // 瀛?button 琛ュ叏蹇呭~瀛楁锛岃瀛楁鏍¢獙閫氳繃鍚庡啀瑙﹀彂灞傜骇绾︽潫
|
|
+ ResourceNode node = newButtonNode(null, 100L, "闈炴硶瀛欏瓙", "crm:x:y", "hide", "/api/x", "enabled");
|
|
+
|
|
+ assertThatThrownBy(() -> resourceService.save(node))
|
|
+ .isInstanceOf(BusinessErrorException.class)
|
|
+ .hasMessageContaining("鍙跺瓙");
|
|
+ }
|
|
+
|
|
+ @Test
|
|
+ @DisplayName("鏍硅妭鐐规寕 button -> 鎷掔粷锛堝彧鍏佽 catalog/menu 浣滀负鏍癸級")
|
|
+ void save_buttonAsRoot_rejected() {
|
|
+ ResourceNode node = newButtonNode(null, null, "鏍规寜閽?, "crm:x:y", "hide", "/api/x", "enabled");
|
|
+
|
|
+ assertThatThrownBy(() -> resourceService.save(node))
|
|
+ .isInstanceOf(BusinessErrorException.class)
|
|
+ .hasMessageContaining("鏍硅妭鐐?);
|
|
+ }
|
|
+
|
|
+ @Test
|
|
+ @DisplayName("鏍硅妭鐐规寕 catalog -> 鎴愬姛")
|
|
+ void save_catalogAsRoot_success() {
|
|
+ ResourceNode node = newResourceNode(null, null, "鏍圭洰褰?, MenuType.CATALOG);
|
|
+
|
|
+ resourceService.save(node);
|
|
+
|
|
+ verify(sysMenuService).saveOrUpdate(any(SysMenu.class));
|
|
+ }
|
|
+
|
|
+ @Test
|
|
+ @DisplayName("鏍硅妭鐐规寕 menu -> 鎴愬姛")
|
|
+ void save_menuAsRoot_success() {
|
|
+ ResourceNode node = newResourceNode(null, null, "鏍硅彍鍗?, MenuType.MENU);
|
|
+ node.setRoute("/root-menu");
|
|
+
|
|
+ resourceService.save(node);
|
|
+
|
|
+ verify(sysMenuService).saveOrUpdate(any(SysMenu.class));
|
|
+ }
|
|
+
|
|
+ // ==================== 瀛楁蹇呭~鏍¢獙 ====================
|
|
+
|
|
+ @Test
|
|
+ @DisplayName("menu 缂?route -> 鎷掔粷")
|
|
+ void save_menuWithoutRoute_rejected() {
|
|
+ ResourceNode node = newResourceNode(null, null, "鏃犺矾鐢辫彍鍗?, MenuType.MENU);
|
|
+ // route 涓?null
|
|
+
|
|
+ assertThatThrownBy(() -> resourceService.save(node))
|
|
+ .isInstanceOf(BusinessErrorException.class)
|
|
+ .hasMessageContaining("璺敱");
|
|
+ }
|
|
+
|
|
+ @Test
|
|
+ @DisplayName("menu route 涓虹┖瀛楃涓?-> 鎷掔粷")
|
|
+ void save_menuWithBlankRoute_rejected() {
|
|
+ ResourceNode node = newResourceNode(null, null, "绌鸿矾鐢辫彍鍗?, MenuType.MENU);
|
|
+ node.setRoute(" ");
|
|
+
|
|
+ assertThatThrownBy(() -> resourceService.save(node))
|
|
+ .isInstanceOf(BusinessErrorException.class)
|
|
+ .hasMessageContaining("璺敱");
|
|
+ }
|
|
+
|
|
+ @Test
|
|
+ @DisplayName("button 缂?perms -> 鎷掔粷")
|
|
+ void save_buttonWithoutPerms_rejected() {
|
|
+ ResourceNode node = newButtonNode(null, null, "鏃爌erms", null, "hide", "/api/x", "enabled");
|
|
+
|
|
+ assertThatThrownBy(() -> resourceService.save(node))
|
|
+ .isInstanceOf(BusinessErrorException.class)
|
|
+ .hasMessageContaining("perms");
|
|
+ }
|
|
+
|
|
+ @Test
|
|
+ @DisplayName("button perms 涓虹┖瀛楃涓?-> 鎷掔粷")
|
|
+ void save_buttonWithBlankPerms_rejected() {
|
|
+ ResourceNode node = newButtonNode(null, null, "绌簆erms", " ", "hide", "/api/x", "enabled");
|
|
+
|
|
+ assertThatThrownBy(() -> resourceService.save(node))
|
|
+ .isInstanceOf(BusinessErrorException.class)
|
|
+ .hasMessageContaining("perms");
|
|
+ }
|
|
+
|
|
+ @Test
|
|
+ @DisplayName("button 缂?denyBehavior -> 鎷掔粷")
|
|
+ void save_buttonWithoutDenyBehavior_rejected() {
|
|
+ ResourceNode node = newButtonNode(null, null, "鏃燿eny", "crm:x:y", null, "/api/x", "enabled");
|
|
+
|
|
+ assertThatThrownBy(() -> resourceService.save(node))
|
|
+ .isInstanceOf(BusinessErrorException.class)
|
|
+ .hasMessageContaining("denyBehavior");
|
|
+ }
|
|
+
|
|
+ @Test
|
|
+ @DisplayName("button 缂?apiUrl -> 鎷掔粷")
|
|
+ void save_buttonWithoutApiUrl_rejected() {
|
|
+ ResourceNode node = newButtonNode(null, null, "鏃燼pi", "crm:x:y", "hide", null, "enabled");
|
|
+
|
|
+ assertThatThrownBy(() -> resourceService.save(node))
|
|
+ .isInstanceOf(BusinessErrorException.class)
|
|
+ .hasMessageContaining("apiUrl");
|
|
+ }
|
|
+
|
|
+ @Test
|
|
+ @DisplayName("button 缂?status -> 鎷掔粷")
|
|
+ void save_buttonWithoutStatus_rejected() {
|
|
+ ResourceNode node = newButtonNode(null, null, "鏃爏tatus", "crm:x:y", "hide", "/api/x", null);
|
|
+
|
|
+ assertThatThrownBy(() -> resourceService.save(node))
|
|
+ .isInstanceOf(BusinessErrorException.class)
|
|
+ .hasMessageContaining("status");
|
|
+ }
|
|
+
|
|
+ // ==================== 缂栬緫 type 閿?====================
|
|
+
|
|
+ @Test
|
|
+ @DisplayName("缂栬緫鏃跺皾璇曟敼 type锛坈atalog鈫抦enu锛?> 鎷掔粷")
|
|
+ void save_editChangeType_rejected() {
|
|
+ when(sysMenuService.getById(1L)).thenReturn(catalog); // type=1 (catalog)
|
|
+
|
|
+ ResourceNode node = newResourceNode(1L, null, "鏀圭被鍨?, MenuType.MENU);
|
|
+ node.setRoute("/new-route");
|
|
+
|
|
+ assertThatThrownBy(() -> resourceService.save(node))
|
|
+ .isInstanceOf(BusinessErrorException.class)
|
|
+ .hasMessageContaining("绫诲瀷");
|
|
+ verify(sysMenuService, never()).saveOrUpdate(any());
|
|
+ }
|
|
+
|
|
+ @Test
|
|
+ @DisplayName("缂栬緫鏃?type 涓嶅彉 -> 鎴愬姛")
|
|
+ void save_editSameType_success() {
|
|
+ when(sysMenuService.getById(1L)).thenReturn(catalog); // type=1(catalog)
|
|
+
|
|
+ ResourceNode node = newResourceNode(1L, null, "鏀瑰悕瀛?, MenuType.CATALOG);
|
|
+
|
|
+ resourceService.save(node);
|
|
+
|
|
+ ArgumentCaptor<SysMenu> captor = ArgumentCaptor.forClass(SysMenu.class);
|
|
+ verify(sysMenuService).saveOrUpdate(captor.capture());
|
|
+ assertThat(captor.getValue().getMenuName()).isEqualTo("鏀瑰悕瀛?);
|
|
+ }
|
|
+
|
|
+ // ==================== 缂栬緫鏃?type 淇濈暀鍘熸湁鑺傜偣 field ====================
|
|
+
|
|
+ @Test
|
|
+ @DisplayName("缂栬緫 menu + path/component 鏈紶鍏?-> 浣跨敤鍘熸湁鍊?)
|
|
+ void save_editMenuPreserveOldPath() {
|
|
+ when(sysMenuService.getById(10L)).thenReturn(menu); // path="/system/user"
|
|
+
|
|
+ ResourceNode node = newResourceNode(10L, null, "鏀瑰悕", MenuType.MENU);
|
|
+ // 涓嶈 route鈥斺€旈鏈熶繚鐣欏師鏈?path
|
|
+
|
|
+ resourceService.save(node);
|
|
+
|
|
+ ArgumentCaptor<SysMenu> captor = ArgumentCaptor.forClass(SysMenu.class);
|
|
+ verify(sysMenuService).saveOrUpdate(captor.capture());
|
|
+ SysMenu saved = captor.getValue();
|
|
+ assertThat(saved.getMenuName()).isEqualTo("鏀瑰悕");
|
|
+ // route 鏈紶鍏ワ細淇濈暀鍘熸湁 path
|
|
+ assertThat(saved.getPath()).isEqualTo("/system/user");
|
|
+ }
|
|
+
|
|
+ // ==================== 缂栬緫鍚?type 淇濇寔涓嶅彉 ====================
|
|
+
|
|
+ @Test
|
|
+ @DisplayName("缂栬緫 catalog -> catalog 鐨?perms/apiUrl 绛夋棤鐢ㄥ瓧娈典负 null")
|
|
+ void save_editCatalog_noButtonFields() {
|
|
+ when(sysMenuService.getById(1L)).thenReturn(catalog);
|
|
+
|
|
+ ResourceNode node = newResourceNode(1L, null, "鏀规樀绉?, MenuType.CATALOG);
|
|
+ node.setPerms("unused-perm"); // 鍓嶇鍙兘璇紶锛屼絾 catalog 搴旇蹇界暐
|
|
+
|
|
+ resourceService.save(node);
|
|
+
|
|
+ ArgumentCaptor<SysMenu> captor = ArgumentCaptor.forClass(SysMenu.class);
|
|
+ verify(sysMenuService).saveOrUpdate(captor.capture());
|
|
+ SysMenu saved = captor.getValue();
|
|
+ assertThat(saved.getMenuType()).isEqualTo(1);
|
|
+ assertThat(saved.getPerms()).isNull(); // catalog 涓嶅瓨 perms
|
|
+ assertThat(saved.getApiUrl()).isNull(); // catalog 涓嶅瓨 apiUrl
|
|
+ }
|
|
+
|
|
+ // ==================== 闈炵┖鏍¢獙 ====================
|
|
+
|
|
+ @Test
|
|
+ @DisplayName("ResourceNode 涓?null -> 鎷掔粷")
|
|
+ void save_nullNode_rejected() {
|
|
+ assertThatThrownBy(() -> resourceService.save(null))
|
|
+ .isInstanceOf(BusinessErrorException.class)
|
|
+ .hasMessageContaining("涓嶈兘涓虹┖");
|
|
+ }
|
|
+
|
|
+ @Test
|
|
+ @DisplayName("type 涓?null -> 鎷掔粷")
|
|
+ void save_nullType_rejected() {
|
|
+ ResourceNode node = new ResourceNode();
|
|
+ node.setName("鏃犵被鍨?);
|
|
+
|
|
+ assertThatThrownBy(() -> resourceService.save(node))
|
|
+ .isInstanceOf(BusinessErrorException.class)
|
|
+ .hasMessageContaining("绫诲瀷");
|
|
+ }
|
|
+
|
|
+ @Test
|
|
+ @DisplayName("name 涓虹┖ -> 鎷掔粷")
|
|
+ void save_blankName_rejected() {
|
|
+ ResourceNode node = newResourceNode(null, null, " ", MenuType.CATALOG);
|
|
+
|
|
+ assertThatThrownBy(() -> resourceService.save(node))
|
|
+ .isInstanceOf(BusinessErrorException.class)
|
|
+ .hasMessageContaining("鍚嶇О");
|
|
+ }
|
|
+
|
|
+ // ==================== 缂栬緫鏃朵紶鍏?extra field 瑕嗙洊 ====================
|
|
+
|
|
+ @Test
|
|
+ @DisplayName("缂栬緫 menu + 姝e父鏇存柊 route -> 瑕嗙洊鍘熸湁 path")
|
|
+ void save_editMenuUpdateRoute() {
|
|
+ when(sysMenuService.getById(10L)).thenReturn(menu); // path="/system/user"
|
|
+
|
|
+ ResourceNode node = newResourceNode(10L, null, "鏀瑰悕", MenuType.MENU);
|
|
+ node.setRoute("/new/path");
|
|
+
|
|
+ resourceService.save(node);
|
|
+
|
|
+ ArgumentCaptor<SysMenu> captor = ArgumentCaptor.forClass(SysMenu.class);
|
|
+ verify(sysMenuService).saveOrUpdate(captor.capture());
|
|
+ assertThat(captor.getValue().getPath()).isEqualTo("/new/path");
|
|
+ }
|
|
+
|
|
+ // ==================== 鍒犻櫎鑺傜偣 ====================
|
|
+
|
|
+ @Test
|
|
+ @DisplayName("鍒犻櫎涓嶅瓨鍦ㄧ殑鑺傜偣 -> 鎶涘紓甯?)
|
|
+ void delete_nodeNotFound_throwsException() {
|
|
+ when(sysMenuService.getById(999L)).thenReturn(null);
|
|
+
|
|
+ assertThatThrownBy(() -> resourceService.delete(999L))
|
|
+ .isInstanceOf(BusinessErrorException.class)
|
|
+ .hasMessageContaining("涓嶅瓨鍦?);
|
|
+
|
|
+ verify(sysMenuService, never()).removeById(any());
|
|
+ }
|
|
+
|
|
+ @Test
|
|
+ @DisplayName("鏈夊瓙鑺傜偣 -> 鎷掔粷鍒犻櫎")
|
|
+ void delete_hasChildren_rejected() {
|
|
+ when(sysMenuService.getById(1L)).thenReturn(catalog);
|
|
+ // stub count: parentId=1 鏈夊瓙鑺傜偣
|
|
+ when(sysMenuService.count(any(LambdaQueryWrapper.class))).thenReturn(3L);
|
|
+
|
|
+ assertThatThrownBy(() -> resourceService.delete(1L))
|
|
+ .isInstanceOf(BusinessErrorException.class)
|
|
+ .hasMessageContaining("瀛愯妭鐐?);
|
|
+
|
|
+ verify(sysMenuService, never()).removeById(any());
|
|
+ verify(sysRoleMenuMapper, never()).delete(any(LambdaQueryWrapper.class));
|
|
+ }
|
|
+
|
|
+ @Test
|
|
+ @DisplayName("鍙跺瓙鑺傜偣鏃犳巿鏉冨紩鐢?-> 鍒犻櫎鎴愬姛")
|
|
+ void delete_leafNoAuth_success() {
|
|
+ when(sysMenuService.getById(100L)).thenReturn(button);
|
|
+ when(sysMenuService.count(any(LambdaQueryWrapper.class))).thenReturn(0L);
|
|
+ when(sysMenuService.removeById(100L)).thenReturn(true);
|
|
+
|
|
+ resourceService.delete(100L);
|
|
+
|
|
+ verify(sysMenuService).removeById(100L);
|
|
+ }
|
|
+
|
|
+ @Test
|
|
+ @DisplayName("鍙跺瓙鑺傜偣鏈夋巿鏉冨紩鐢?-> 鍒犻櫎鑺傜偣 + 绾ц仈娓呯悊 sys_role_menu")
|
|
+ void delete_leafWithAuth_cascadeCleanup() {
|
|
+ when(sysMenuService.getById(100L)).thenReturn(button);
|
|
+ when(sysMenuService.count(any(LambdaQueryWrapper.class))).thenReturn(0L);
|
|
+ when(sysMenuService.removeById(100L)).thenReturn(true);
|
|
+ when(sysRoleMenuMapper.delete(any(LambdaQueryWrapper.class))).thenReturn(2);
|
|
+
|
|
+ resourceService.delete(100L);
|
|
+
|
|
+ verify(sysMenuService).removeById(100L);
|
|
+ // 鏈夊紩鐢紝绾ц仈娓呯悊
|
|
+ ArgumentCaptor<LambdaQueryWrapper<SysRoleMenu>> captor =
|
|
+ ArgumentCaptor.forClass(LambdaQueryWrapper.class);
|
|
+ verify(sysRoleMenuMapper).delete(captor.capture());
|
|
+ }
|
|
+
|
|
+ // ==================== 鍥炬爣涓婁紶 ====================
|
|
+
|
|
+ @Test
|
|
+ @DisplayName("鍚堟硶 PNG 涓婁紶 -> 杩斿洖 FileInfoDTO")
|
|
+ void uploadIcon_legalPng_success() throws Exception {
|
|
+ MultipartFile file = mock(MultipartFile.class);
|
|
+ when(file.getOriginalFilename()).thenReturn("icon.png");
|
|
+ when(file.getContentType()).thenReturn("image/png");
|
|
+ when(file.getBytes()).thenReturn(new byte[100]);
|
|
+ when(file.getSize()).thenReturn(100L);
|
|
+
|
|
+ FileInfoDTO expected = new FileInfoDTO();
|
|
+ expected.setFileId("12345");
|
|
+ when(fileApi.upload(any(byte[].class), anyString(), anyString(), eq("resources/icon")))
|
|
+ .thenReturn(expected);
|
|
+
|
|
+ FileInfoDTO result = resourceService.uploadIcon(file);
|
|
+
|
|
+ assertThat(result.getFileId()).isEqualTo("12345");
|
|
+ verify(fileApi).upload(any(byte[].class), eq("icon.png"), eq("image/png"), eq("resources/icon"));
|
|
+ }
|
|
+
|
|
+ @Test
|
|
+ @DisplayName("闈炴硶鎵╁睍鍚嶏紙.exe锛?> 鎷掔粷")
|
|
+ void uploadIcon_illegalExtension_rejected() {
|
|
+ MultipartFile file = mock(MultipartFile.class);
|
|
+ when(file.isEmpty()).thenReturn(false);
|
|
+ when(file.getSize()).thenReturn(100L);
|
|
+ when(file.getOriginalFilename()).thenReturn("virus.exe");
|
|
+
|
|
+ assertThatThrownBy(() -> resourceService.uploadIcon(file))
|
|
+ .isInstanceOf(BusinessErrorException.class)
|
|
+ .hasMessageContaining("鏍煎紡");
|
|
+
|
|
+ verify(fileApi, never()).upload(any(byte[].class), anyString(), anyString(), anyString());
|
|
+ }
|
|
+
|
|
+ @Test
|
|
+ @DisplayName("鏃犳墿灞曞悕 -> 鎷掔粷")
|
|
+ void uploadIcon_noExtension_rejected() {
|
|
+ MultipartFile file = mock(MultipartFile.class);
|
|
+ when(file.isEmpty()).thenReturn(false);
|
|
+ when(file.getSize()).thenReturn(100L);
|
|
+ when(file.getOriginalFilename()).thenReturn("noext");
|
|
+
|
|
+ assertThatThrownBy(() -> resourceService.uploadIcon(file))
|
|
+ .isInstanceOf(BusinessErrorException.class)
|
|
+ .hasMessageContaining("鏍煎紡");
|
|
+ }
|
|
+
|
|
+ @Test
|
|
+ @DisplayName("瓒呰繃 500KB -> 鎷掔粷")
|
|
+ void uploadIcon_exceedsMaxSize_rejected() {
|
|
+ MultipartFile file = mock(MultipartFile.class);
|
|
+ when(file.isEmpty()).thenReturn(false);
|
|
+ when(file.getSize()).thenReturn(600 * 1024L); // 600KB
|
|
+
|
|
+ assertThatThrownBy(() -> resourceService.uploadIcon(file))
|
|
+ .isInstanceOf(BusinessErrorException.class)
|
|
+ .hasMessageContaining("500");
|
|
+
|
|
+ verify(fileApi, never()).upload(any(byte[].class), anyString(), anyString(), anyString());
|
|
+ }
|
|
+
|
|
+ @Test
|
|
+ @DisplayName("绌烘枃浠?-> 鎷掔粷")
|
|
+ void uploadIcon_emptyFile_rejected() {
|
|
+ MultipartFile file = mock(MultipartFile.class);
|
|
+ when(file.isEmpty()).thenReturn(true);
|
|
+
|
|
+ assertThatThrownBy(() -> resourceService.uploadIcon(file))
|
|
+ .isInstanceOf(BusinessErrorException.class)
|
|
+ .hasMessageContaining("绌?);
|
|
+ }
|
|
+
|
|
+ // ==================== 杈呭姪鏂规硶 ====================
|
|
+
|
|
+ private SysMenu buildMenu(Long id, Long parentId, String name, int menuType) {
|
|
+ SysMenu m = new SysMenu();
|
|
+ m.setId(id);
|
|
+ m.setParentId(parentId);
|
|
+ m.setMenuName(name);
|
|
+ m.setMenuType(menuType);
|
|
+ return m;
|
|
+ }
|
|
+
|
|
+ private ResourceNode newResourceNode(Long id, Long parentId, String name, MenuType type) {
|
|
+ ResourceNode node = new ResourceNode();
|
|
+ node.setId(id);
|
|
+ node.setParentId(parentId);
|
|
+ node.setName(name);
|
|
+ node.setType(type);
|
|
+ node.setSort(0);
|
|
+ return node;
|
|
+ }
|
|
+
|
|
+ private ResourceNode newButtonNode(Long id, Long parentId, String name,
|
|
+ String perms, String denyBehavior,
|
|
+ String apiUrl, String status) {
|
|
+ ResourceNode node = new ResourceNode();
|
|
+ node.setId(id);
|
|
+ node.setParentId(parentId);
|
|
+ node.setName(name);
|
|
+ node.setType(MenuType.BUTTON);
|
|
+ node.setSort(0);
|
|
+ node.setPerms(perms);
|
|
+ node.setDenyBehavior(denyBehavior);
|
|
+ node.setApiUrl(apiUrl);
|
|
+ node.setStatus(status);
|
|
+ return node;
|
|
+ }
|
|
+}
|
|
|