From 866688b92f0c92a98d822cf371d987f5171470d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=AD=90=E9=BB=98?= <925456043@qq.com> Date: Mon, 3 Aug 2026 14:48:39 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=BB=9F=E4=B8=80=E8=B4=A6=E5=8F=B7?= =?UTF-8?q?=E8=A7=92=E8=89=B2=E6=A0=A1=E9=AA=8C=E4=B8=8E=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E9=A6=96=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 创建与导入账号时强制校验启用角色 - 按启用角色返回默认首页并过滤禁用角色 --- .../system/SysAccountController.java | 79 ++++++- .../system/vo/SysAccountProfileVo.java | 76 +++++++ .../system/SysAccountControllerTest.java | 153 ++++++++++++- .../system/service/SysRoleService.java | 6 + .../service/impl/SysAccountServiceImpl.java | 56 +++-- .../service/impl/SysRoleServiceImpl.java | 9 +- .../impl/SysAccountServiceImplTest.java | 201 ++++++++++++++++++ .../service/impl/SysRoleServiceImplTest.java | 118 ++++++++++ .../system/sysAccount/SysAccountModal.vue | 41 +++- 9 files changed, 697 insertions(+), 42 deletions(-) create mode 100644 easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/vo/SysAccountProfileVo.java create mode 100644 easyflow-modules/easyflow-module-system/src/test/java/tech/easyflow/system/service/impl/SysRoleServiceImplTest.java diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/SysAccountController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/SysAccountController.java index 2ae99304..4e15a543 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/SysAccountController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/SysAccountController.java @@ -8,6 +8,7 @@ import com.mybatisflex.core.paginate.Page; import com.mybatisflex.core.query.QueryWrapper; import jakarta.servlet.http.HttpServletResponse; import org.springframework.dao.DuplicateKeyException; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -16,6 +17,7 @@ import org.springframework.web.multipart.MultipartFile; import tech.easyflow.auth.entity.EncryptedCredentialDTO; import tech.easyflow.auth.service.AuthCredentialKeyService; import tech.easyflow.auth.service.AuthService; +import tech.easyflow.common.constant.Constants; import tech.easyflow.common.constant.enums.EnumAccountType; import tech.easyflow.common.constant.enums.EnumDataStatus; import tech.easyflow.common.domain.Result; @@ -25,10 +27,13 @@ import tech.easyflow.common.util.StringUtil; import tech.easyflow.common.web.controller.BaseCurdController; import tech.easyflow.common.web.jsonbody.JsonBody; import tech.easyflow.log.annotation.LogRecord; +import tech.easyflow.admin.controller.system.vo.SysAccountProfileVo; import tech.easyflow.system.entity.SysAccount; +import tech.easyflow.system.entity.SysRole; import tech.easyflow.system.entity.vo.SysAccountBatchActionResultVo; import tech.easyflow.system.entity.vo.SysAccountImportResultVo; import tech.easyflow.system.service.SysAccountService; +import tech.easyflow.system.service.SysRoleService; import tech.easyflow.system.util.SysPasswordPolicy; import javax.annotation.Resource; @@ -36,9 +41,13 @@ import java.io.Serializable; import java.math.BigInteger; import java.net.URLEncoder; import java.util.Collection; +import java.util.ArrayList; import java.util.Date; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; /** * 用户表 控制层。 @@ -49,13 +58,27 @@ import java.util.Map; @RestController("sysAccountController") @RequestMapping("/api/v1/sysAccount") public class SysAccountController extends BaseCurdController { + private static final String SUPER_ADMIN_HOME_PATH = "/dashboard/workspace"; + private static final String USER_HOME_PATH = "/ai/agent-chat"; + private final AuthCredentialKeyService credentialKeyService; + private final SysRoleService sysRoleService; @Resource private AuthService authService; - public SysAccountController(SysAccountService service, AuthCredentialKeyService credentialKeyService) { + /** + * 创建用户管理控制器。 + * + * @param service 用户服务 + * @param credentialKeyService 凭证密钥服务 + * @param sysRoleService 角色服务 + */ + public SysAccountController(SysAccountService service, + AuthCredentialKeyService credentialKeyService, + SysRoleService sysRoleService) { super(service); this.credentialKeyService = credentialKeyService; + this.sysRoleService = sysRoleService; } @Override @@ -76,6 +99,10 @@ public class SysAccountController extends BaseCurdController 0) { return Result.fail(1, "用户名已存在"); } + Result roleValidation = validateCreateRoles(entity); + if (roleValidation != null) { + return roleValidation; + } String password = decryptInitialPassword(entity.getPasswordCredential()); if (!StringUtil.hasText(password)) { return Result.fail(1, "密码不能为空"); @@ -144,11 +171,24 @@ public class SysAccountController extends BaseCurdController myProfile() { + public Result myProfile() { LoginAccount account = SaTokenUtil.getLoginAccount(); SysAccount sysAccount = service.getById(account.getId()); - return Result.ok(sysAccount); + List roles = sysRoleService.getRolesByAccountId(account.getId()).stream() + .map(SysRole::getRoleKey) + .filter(StringUtil::hasText) + .distinct() + .collect(Collectors.toList()); + String homePath = roles.contains(Constants.SUPER_ADMIN_ROLE_CODE) + ? SUPER_ADMIN_HOME_PATH + : USER_HOME_PATH; + return Result.ok(SysAccountProfileVo.from(sysAccount, roles, homePath)); } @PostMapping("/updateProfile") @@ -262,8 +302,12 @@ public class SysAccountController extends BaseCurdController save(@JsonBody SysAccount entity) { try { return super.save(entity); @@ -271,4 +315,33 @@ public class SysAccountController extends BaseCurdController validateCreateRoles(SysAccount entity) { + List roleIds = entity == null ? null : entity.getRoleIds(); + Set uniqueRoleIds = new LinkedHashSet<>(); + if (roleIds != null) { + roleIds.stream() + .filter(java.util.Objects::nonNull) + .forEach(uniqueRoleIds::add); + } + if (uniqueRoleIds.isEmpty()) { + return Result.fail(1, "角色不能为空"); + } + + List roles = sysRoleService.listByIds(uniqueRoleIds); + boolean valid = roles.size() == uniqueRoleIds.size() + && roles.stream().allMatch(role -> + EnumDataStatus.AVAILABLE.getCode().equals(role.getStatus())); + if (!valid) { + return Result.fail(1, "角色不存在或已禁用"); + } + entity.setRoleIds(new ArrayList<>(uniqueRoleIds)); + return null; + } } diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/vo/SysAccountProfileVo.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/vo/SysAccountProfileVo.java new file mode 100644 index 00000000..d12e2e0b --- /dev/null +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/system/vo/SysAccountProfileVo.java @@ -0,0 +1,76 @@ +package tech.easyflow.admin.controller.system.vo; + +import cn.hutool.core.bean.BeanUtil; +import tech.easyflow.system.entity.SysAccount; + +import java.util.ArrayList; +import java.util.List; + +/** + * 管理端当前账号资料视图。 + */ +public class SysAccountProfileVo extends SysAccount { + + private String homePath; + private List roles = new ArrayList<>(); + + /** + * 创建空的当前账号资料视图。 + */ + public SysAccountProfileVo() { + } + + /** + * 根据账号实体和角色信息创建资料视图。 + * + * @param account 账号实体 + * @param roles 角色标识列表 + * @param homePath 默认首页 + * @return 当前账号资料视图 + */ + public static SysAccountProfileVo from(SysAccount account, List roles, String homePath) { + SysAccountProfileVo profile = new SysAccountProfileVo(); + if (account != null) { + BeanUtil.copyProperties(account, profile); + } + profile.setRoles(roles); + profile.setHomePath(homePath); + return profile; + } + + /** + * 获取默认首页。 + * + * @return 默认首页 + */ + public String getHomePath() { + return homePath; + } + + /** + * 设置默认首页。 + * + * @param homePath 默认首页 + */ + public void setHomePath(String homePath) { + this.homePath = homePath; + } + + /** + * 获取角色标识列表。 + * + * @return 角色标识列表 + */ + public List getRoles() { + return roles; + } + + /** + * 设置角色标识列表。 + * + * @param roles 角色标识列表 + */ + public void setRoles(List roles) { + this.roles = roles == null ? new ArrayList<>() : new ArrayList<>(roles); + } +} diff --git a/easyflow-api/easyflow-api-admin/src/test/java/tech/easyflow/admin/controller/system/SysAccountControllerTest.java b/easyflow-api/easyflow-api-admin/src/test/java/tech/easyflow/admin/controller/system/SysAccountControllerTest.java index 3c2963db..470c4dfe 100644 --- a/easyflow-api/easyflow-api-admin/src/test/java/tech/easyflow/admin/controller/system/SysAccountControllerTest.java +++ b/easyflow-api/easyflow-api-admin/src/test/java/tech/easyflow/admin/controller/system/SysAccountControllerTest.java @@ -2,22 +2,28 @@ package tech.easyflow.admin.controller.system; import com.alibaba.fastjson2.JSONObject; import com.mybatisflex.core.query.QueryWrapper; -import org.mockito.ArgumentCaptor; import org.mockito.MockedStatic; import org.testng.annotations.Test; import tech.easyflow.auth.service.AuthCredentialKeyService; import tech.easyflow.common.domain.Result; import tech.easyflow.common.entity.LoginAccount; import tech.easyflow.common.satoken.util.SaTokenUtil; +import tech.easyflow.admin.controller.system.vo.SysAccountProfileVo; import tech.easyflow.system.entity.SysAccount; +import tech.easyflow.system.entity.SysRole; import tech.easyflow.system.service.SysAccountService; +import tech.easyflow.system.service.SysRoleService; import java.math.BigInteger; +import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyCollection; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.testng.Assert.assertEquals; @@ -28,7 +34,7 @@ import static org.testng.Assert.assertEquals; public class SysAccountControllerTest { /** - * 验证创建用户时保留表单选择的部门,而不是替换为操作者部门。 + * 验证创建用户时保留表单选择的部门,不被操作者部门覆盖。 */ @Test public void saveShouldKeepSubmittedDepartment() { @@ -38,13 +44,28 @@ public class SysAccountControllerTest { BigInteger operatorId = BigInteger.valueOf(400); SysAccountService accountService = mock(SysAccountService.class); AuthCredentialKeyService credentialKeyService = mock(AuthCredentialKeyService.class); - SysAccountController controller = new SysAccountController(accountService, credentialKeyService); + SysRoleService roleService = mock(SysRoleService.class); + SysAccountController controller = new SysAccountController( + accountService, + credentialKeyService, + roleService + ); SysAccount entity = createAccount(selectedDeptId); LoginAccount loginAccount = createLoginAccount(operatorId, tenantId, operatorDeptId); + AtomicReference savedDeptId = new AtomicReference<>(); + AtomicReference savedTenantId = new AtomicReference<>(); + AtomicReference savedCreatedBy = new AtomicReference<>(); + AtomicReference savedModifiedBy = new AtomicReference<>(); when(accountService.count(any(QueryWrapper.class))).thenReturn(0L); + when(roleService.listByIds(anyCollection())).thenReturn(List.of(enabledRole(BigInteger.ONE, "user"))); when(accountService.save(any(SysAccount.class))).thenAnswer(invocation -> { - invocation.getArgument(0, SysAccount.class).setId(BigInteger.valueOf(500)); + SysAccount savedAccount = invocation.getArgument(0, SysAccount.class); + savedDeptId.set(savedAccount.getDeptId()); + savedTenantId.set(savedAccount.getTenantId()); + savedCreatedBy.set(savedAccount.getCreatedBy()); + savedModifiedBy.set(savedAccount.getModifiedBy()); + savedAccount.setId(BigInteger.valueOf(500)); return true; }); when(credentialKeyService.decryptPayload(any())) @@ -58,13 +79,107 @@ public class SysAccountControllerTest { assertEquals(result.getErrorCode(), 0); } - ArgumentCaptor accountCaptor = ArgumentCaptor.forClass(SysAccount.class); - verify(accountService).save(accountCaptor.capture()); - SysAccount savedAccount = accountCaptor.getValue(); - assertEquals(savedAccount.getDeptId(), selectedDeptId); - assertEquals(savedAccount.getTenantId(), tenantId); - assertEquals(savedAccount.getCreatedBy(), operatorId); - assertEquals(savedAccount.getModifiedBy(), operatorId); + verify(accountService).save(any(SysAccount.class)); + assertEquals(savedDeptId.get(), selectedDeptId); + assertEquals(savedTenantId.get(), tenantId); + assertEquals(savedCreatedBy.get(), operatorId); + assertEquals(savedModifiedBy.get(), operatorId); + } + + /** + * 验证创建用户时拒绝空角色。 + */ + @Test + public void saveShouldRejectEmptyRoles() { + SysAccountService accountService = mock(SysAccountService.class); + AuthCredentialKeyService credentialKeyService = mock(AuthCredentialKeyService.class); + SysRoleService roleService = mock(SysRoleService.class); + SysAccountController controller = new SysAccountController( + accountService, + credentialKeyService, + roleService + ); + SysAccount entity = createAccount(BigInteger.valueOf(200)); + entity.setRoleIds(List.of()); + LoginAccount loginAccount = createLoginAccount( + BigInteger.valueOf(400), + BigInteger.valueOf(300), + BigInteger.valueOf(100) + ); + when(accountService.count(any(QueryWrapper.class))).thenReturn(0L); + + try (MockedStatic saToken = mockStatic(SaTokenUtil.class)) { + saToken.when(SaTokenUtil::getLoginAccount).thenReturn(loginAccount); + + Result result = controller.save(entity); + + assertEquals(result.getErrorCode(), 1); + assertEquals(result.getMessage(), "角色不能为空"); + } + + verify(accountService, never()).save(any(SysAccount.class)); + } + + /** + * 验证超级管理员默认进入工作台并返回角色标识。 + */ + @Test + public void myProfileShouldReturnSuperAdminHomePath() { + BigInteger accountId = BigInteger.ONE; + SysAccountService accountService = mock(SysAccountService.class); + AuthCredentialKeyService credentialKeyService = mock(AuthCredentialKeyService.class); + SysRoleService roleService = mock(SysRoleService.class); + SysAccountController controller = new SysAccountController( + accountService, + credentialKeyService, + roleService + ); + SysAccount account = new SysAccount(); + account.setId(accountId); + when(accountService.getById(accountId)).thenReturn(account); + when(roleService.getRolesByAccountId(accountId)) + .thenReturn(List.of(enabledRole(BigInteger.ONE, "super_admin"))); + + try (MockedStatic saToken = mockStatic(SaTokenUtil.class)) { + saToken.when(SaTokenUtil::getLoginAccount) + .thenReturn(createLoginAccount(accountId, BigInteger.ZERO, BigInteger.ZERO)); + + Result result = controller.myProfile(); + + assertEquals(result.getData().getHomePath(), "/dashboard/workspace"); + assertEquals(result.getData().getRoles(), List.of("super_admin")); + } + } + + /** + * 验证普通账号默认进入智能体聊天页。 + */ + @Test + public void myProfileShouldReturnAgentChatHomePathForRegularUser() { + BigInteger accountId = BigInteger.valueOf(20); + SysAccountService accountService = mock(SysAccountService.class); + AuthCredentialKeyService credentialKeyService = mock(AuthCredentialKeyService.class); + SysRoleService roleService = mock(SysRoleService.class); + SysAccountController controller = new SysAccountController( + accountService, + credentialKeyService, + roleService + ); + SysAccount account = new SysAccount(); + account.setId(accountId); + when(accountService.getById(accountId)).thenReturn(account); + when(roleService.getRolesByAccountId(accountId)) + .thenReturn(List.of(enabledRole(BigInteger.TWO, "operator"))); + + try (MockedStatic saToken = mockStatic(SaTokenUtil.class)) { + saToken.when(SaTokenUtil::getLoginAccount) + .thenReturn(createLoginAccount(accountId, BigInteger.ZERO, BigInteger.ZERO)); + + Result result = controller.myProfile(); + + assertEquals(result.getData().getHomePath(), "/ai/agent-chat"); + assertEquals(result.getData().getRoles(), List.of("operator")); + } } /** @@ -79,9 +194,25 @@ public class SysAccountControllerTest { account.setLoginName("department_test_user"); account.setNickname("部门测试用户"); account.setPasswordCredential(Map.of("keyId", "test-key")); + account.setRoleIds(List.of(BigInteger.ONE)); return account; } + /** + * 构造启用角色。 + * + * @param id 角色 ID + * @param roleKey 角色标识 + * @return 启用角色 + */ + private SysRole enabledRole(BigInteger id, String roleKey) { + SysRole role = new SysRole(); + role.setId(id); + role.setRoleKey(roleKey); + role.setStatus(1); + return role; + } + /** * 构造当前登录账号。 * diff --git a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/SysRoleService.java b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/SysRoleService.java index f674f554..8988cafe 100644 --- a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/SysRoleService.java +++ b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/SysRoleService.java @@ -16,6 +16,12 @@ public interface SysRoleService extends IService { void saveRoleMenu(BigInteger roleId, List keys); + /** + * 查询账号当前关联的启用角色。 + * + * @param accountId 账号 ID + * @return 启用角色列表 + */ List getRolesByAccountId(BigInteger accountId); void saveRole(SysRole sysRole); diff --git a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysAccountServiceImpl.java b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysAccountServiceImpl.java index 28cf4e1b..67fc3f16 100644 --- a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysAccountServiceImpl.java +++ b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysAccountServiceImpl.java @@ -80,7 +80,9 @@ public class SysAccountServiceImpl extends ServiceImpl roleLookup = buildImportNameLookup( - sysRoleMapper.selectListByQuery(QueryWrapper.create()), + sysRoleMapper.selectListByQuery( + QueryWrapper.create().eq(SysRole::getStatus, EnumDataStatus.AVAILABLE.getCode()) + ), SysRole::getRoleName ); ImportNameLookup positionLookup = buildImportNameLookup( @@ -388,6 +392,7 @@ public class SysAccountServiceImpl extends ServiceImpl roleNames = splitCodes(row.getRoleNames()); if (deptName == null) { addImportDetail(details, IMPORT_FIELD_DEPT_NAME, row.getDeptName(), "部门名称不能为空"); @@ -400,6 +405,9 @@ public class SysAccountServiceImpl extends ServiceImpl roleIds = resolveResourceIds( - row.getRoleNames(), + roleNames, roleLookup, SysRole::getId, IMPORT_FIELD_ROLE_NAME, details ); List positionIds = resolveResourceIds( - row.getPositionNames(), + splitCodes(row.getPositionNames()), positionLookup, SysPosition::getId, IMPORT_FIELD_POSITION_NAME, @@ -487,7 +495,7 @@ public class SysAccountServiceImpl extends ServiceImpl List resolveResourceIds( - String rawNames, + List names, ImportNameLookup lookup, Function idExtractor, String fieldName, List details) { - List names = splitCodes(rawNames); if (names.isEmpty()) { return Collections.emptyList(); } @@ -693,8 +700,8 @@ public class SysAccountServiceImpl extends ServiceImpl> buildImportGuideRows() { List> rows = new ArrayList<>(); rows.add(List.of("填写规则", "请按名称填写部门、角色、岗位。")); - rows.add(List.of("必填字段", "部门名称*、登录账号*、昵称*")); - rows.add(List.of("可选字段", "手机号、邮箱、状态、角色名称、岗位名称、备注")); + rows.add(List.of("必填字段", "部门名称*、登录账号*、昵称*、角色名称*")); + rows.add(List.of("可选字段", "手机号、邮箱、状态、岗位名称、备注")); rows.add(List.of("状态可选值", "可留空,或填写 1/0/已启用/启用/未启用/停用/禁用")); rows.add(List.of("多值分隔", "角色名称、岗位名称支持使用英文逗号,或中文逗号,分隔多个名称")); rows.add(List.of( @@ -853,7 +860,7 @@ public class SysAccountServiceImpl extends ServiceImpl requiredHeads = List.of( IMPORT_HEAD_DEPT_NAME, IMPORT_HEAD_LOGIN_NAME, - IMPORT_HEAD_NICKNAME + IMPORT_HEAD_NICKNAME, + IMPORT_HEAD_ROLE_NAMES ); for (String requiredHead : requiredHeads) { + if (IMPORT_HEAD_ROLE_NAMES.equals(requiredHead) + && headIndex.containsKey(IMPORT_HEAD_ROLE_NAMES_LEGACY)) { + continue; + } if (!headIndex.containsKey(requiredHead)) { throw new BusinessException("导入模板表头不正确,必须包含:" + String.join("、", requiredHeads)); } @@ -914,13 +926,23 @@ public class SysAccountServiceImpl extends ServiceImpl row, String headName) { - Integer index = headIndex.get(headName); - if (index == null) { - return null; + /** + * 按候选表头顺序读取单元格文本。 + * + * @param row 当前数据行 + * @param headNames 候选表头 + * @return 单元格文本,无匹配表头时返回 {@code null} + */ + private String getCellValue(Map row, String... headNames) { + for (String headName : headNames) { + Integer index = headIndex.get(headName); + if (index == null) { + continue; + } + Object value = row.get(index); + return value == null ? null : String.valueOf(value).trim(); } - Object value = row.get(index); - return value == null ? null : String.valueOf(value).trim(); + return null; } } diff --git a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysRoleServiceImpl.java b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysRoleServiceImpl.java index 691f5a2a..83b40079 100644 --- a/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysRoleServiceImpl.java +++ b/easyflow-modules/easyflow-module-system/src/main/java/tech/easyflow/system/service/impl/SysRoleServiceImpl.java @@ -7,6 +7,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import tech.easyflow.common.cache.RedisLockExecutor; import tech.easyflow.common.constant.enums.EnumDataScope; +import tech.easyflow.common.constant.enums.EnumDataStatus; import tech.easyflow.system.entity.SysAccountRole; import tech.easyflow.system.entity.SysRole; import tech.easyflow.system.entity.SysRoleDept; @@ -80,6 +81,9 @@ public class SysRoleServiceImpl extends ServiceImpl impl }); } + /** + * {@inheritDoc} + */ @Override public List getRolesByAccountId(BigInteger accountId) { // 查询用户对应角色id集合 @@ -89,7 +93,10 @@ public class SysRoleServiceImpl extends ServiceImpl impl if (CollectionUtil.isEmpty(roleIds)) { return new ArrayList<>(); } - return listByIds(roleIds); + return listByIds(roleIds).stream() + .filter(role -> role != null + && EnumDataStatus.AVAILABLE.getCode().equals(role.getStatus())) + .collect(Collectors.toList()); } @Override diff --git a/easyflow-modules/easyflow-module-system/src/test/java/tech/easyflow/system/service/impl/SysAccountServiceImplTest.java b/easyflow-modules/easyflow-module-system/src/test/java/tech/easyflow/system/service/impl/SysAccountServiceImplTest.java index c2833b67..03bab512 100644 --- a/easyflow-modules/easyflow-module-system/src/test/java/tech/easyflow/system/service/impl/SysAccountServiceImplTest.java +++ b/easyflow-modules/easyflow-module-system/src/test/java/tech/easyflow/system/service/impl/SysAccountServiceImplTest.java @@ -2,28 +2,55 @@ package tech.easyflow.system.service.impl; import cn.dev33.satoken.stp.StpUtil; import cn.hutool.crypto.digest.BCrypt; +import cn.idev.excel.EasyExcel; +import cn.idev.excel.ExcelWriter; +import cn.idev.excel.FastExcel; +import cn.idev.excel.context.AnalysisContext; +import cn.idev.excel.metadata.data.ReadCellData; +import cn.idev.excel.read.listener.ReadListener; +import cn.idev.excel.write.metadata.WriteSheet; import com.mybatisflex.core.query.QueryWrapper; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.MockedStatic; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionStatus; +import org.springframework.web.multipart.MultipartFile; import tech.easyflow.common.constant.enums.EnumAccountType; +import tech.easyflow.common.entity.LoginAccount; +import tech.easyflow.common.web.exceptions.BusinessException; import tech.easyflow.system.config.AccountSecurityProperties; import tech.easyflow.system.entity.SysAccount; +import tech.easyflow.system.entity.SysDept; +import tech.easyflow.system.entity.vo.SysAccountImportErrorDetailVo; +import tech.easyflow.system.entity.vo.SysAccountImportResultVo; +import tech.easyflow.system.mapper.SysDeptMapper; +import tech.easyflow.system.mapper.SysPositionMapper; +import tech.easyflow.system.mapper.SysRoleMapper; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.lang.reflect.Field; import java.math.BigInteger; +import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; /** * {@link SysAccountServiceImpl} 测试。 @@ -97,6 +124,180 @@ public class SysAccountServiceImplTest { } } + /** + * 验证下载模板明确标记角色为必填字段。 + * + * @throws Exception 注入测试配置失败 + */ + @Test + public void shouldMarkRoleAsRequiredInImportTemplate() throws Exception { + SysAccountServiceImpl service = new SysAccountServiceImpl(); + AccountSecurityProperties properties = new AccountSecurityProperties(); + properties.setDefaultResetPassword("Template123!"); + setField(service, "accountSecurityProperties", properties); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + service.writeImportTemplate(outputStream); + + List headers = readFirstSheetHeaders(outputStream.toByteArray()); + assertTrue(headers.contains("角色名称*")); + } + + /** + * 验证缺少角色列时直接返回模板表头错误。 + * + * @throws Exception 构造上传文件失败 + */ + @Test + public void shouldRejectImportTemplateWithoutRequiredRoleHeader() throws Exception { + SysAccountServiceImpl service = new SysAccountServiceImpl(); + byte[] workbook = createImportWorkbook( + List.of( + List.of("部门名称*"), + List.of("登录账号*"), + List.of("昵称*") + ), + List.of(List.of("研发部", "missing-role", "缺少角色")) + ); + + BusinessException exception = assertThrows( + BusinessException.class, + () -> service.importAccounts(mockMultipartFile(workbook), new LoginAccount()) + ); + + assertTrue(exception.getMessage().contains("角色名称*")); + } + + /** + * 验证角色为空或仅包含分隔符时返回明确的行级必填错误。 + * + * @throws Exception 注入测试依赖或构造上传文件失败 + */ + @Test + public void shouldRejectImportRowsWithoutEffectiveRoles() throws Exception { + SysAccountServiceImpl service = spy(new SysAccountServiceImpl()); + SysDeptMapper deptMapper = mock(SysDeptMapper.class); + SysRoleMapper roleMapper = mock(SysRoleMapper.class); + SysPositionMapper positionMapper = mock(SysPositionMapper.class); + PlatformTransactionManager transactionManager = mock(PlatformTransactionManager.class); + TransactionStatus transactionStatus = mock(TransactionStatus.class); + SysDept dept = new SysDept(); + dept.setId(BigInteger.ONE); + dept.setDeptName("研发部"); + + when(deptMapper.selectListByQuery(any(QueryWrapper.class))).thenReturn(List.of(dept)); + when(roleMapper.selectListByQuery(any(QueryWrapper.class))).thenReturn(List.of()); + when(positionMapper.selectListByQuery(any(QueryWrapper.class))).thenReturn(List.of()); + when(transactionManager.getTransaction(any(TransactionDefinition.class))).thenReturn(transactionStatus); + doReturn(0L).when(service).count(any(QueryWrapper.class)); + setField(service, "sysDeptMapper", deptMapper); + setField(service, "sysRoleMapper", roleMapper); + setField(service, "sysPositionMapper", positionMapper); + setField(service, "transactionManager", transactionManager); + + byte[] workbook = createImportWorkbook( + List.of( + List.of("部门名称*"), + List.of("登录账号*"), + List.of("昵称*"), + List.of("角色名称") + ), + List.of( + List.of("研发部", "empty-role", "空角色", ""), + List.of("研发部", "delimiter-role", "分隔符角色", ", , ") + ) + ); + LoginAccount loginAccount = new LoginAccount(); + loginAccount.setId(BigInteger.TEN); + SysAccountImportResultVo result = service.importAccounts( + mockMultipartFile(workbook), + loginAccount + ); + + assertEquals(2, result.getTotalCount()); + assertEquals(0, result.getSuccessCount()); + assertEquals(2, result.getErrorCount()); + result.getErrorRows().forEach(errorRow -> { + List details = errorRow.getDetails(); + assertTrue(details.stream().anyMatch(detail -> + "角色名称".equals(detail.getFieldName()) + && "角色名称不能为空".equals(detail.getReason()) + )); + }); + verify(service, never()).save(any(SysAccount.class)); + } + + /** + * 创建账号导入测试工作簿。 + * + * @param headers 表头 + * @param rows 数据行 + * @return 工作簿字节 + */ + private byte[] createImportWorkbook(List> headers, List> rows) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ExcelWriter excelWriter = EasyExcel.write(outputStream).build(); + try { + WriteSheet sheet = EasyExcel.writerSheet("模板") + .head(headers) + .build(); + excelWriter.write(rows, sheet); + } finally { + excelWriter.finish(); + } + return outputStream.toByteArray(); + } + + /** + * 读取工作簿首个工作表的表头。 + * + * @param workbook 工作簿字节 + * @return 表头文本 + */ + private List readFirstSheetHeaders(byte[] workbook) { + List headers = new ArrayList<>(); + ReadListener> listener = + new ReadListener>() { + @Override + public void invoke(LinkedHashMap data, AnalysisContext context) { + // 模板没有数据行,仅检查表头。 + } + + @Override + public void invokeHead(Map> headMap, AnalysisContext context) { + headMap.values().stream() + .map(ReadCellData::getStringValue) + .filter(value -> value != null && !value.isBlank()) + .forEach(headers::add); + } + + @Override + public void doAfterAllAnalysed(AnalysisContext context) { + // 无需汇总数据行。 + } + }; + FastExcel.read(new ByteArrayInputStream(workbook), listener) + .sheet() + .doRead(); + return headers; + } + + /** + * 构造账号导入上传文件。 + * + * @param workbook 工作簿字节 + * @return 上传文件 + * @throws Exception 模拟输入流失败 + */ + private MultipartFile mockMultipartFile(byte[] workbook) throws Exception { + MultipartFile file = mock(MultipartFile.class); + when(file.isEmpty()).thenReturn(workbook.length == 0); + when(file.getSize()).thenReturn((long) workbook.length); + when(file.getOriginalFilename()).thenReturn("users.xlsx"); + when(file.getInputStream()).thenAnswer(invocation -> new ByteArrayInputStream(workbook)); + return file; + } + /** * 设置测试对象的私有字段。 * diff --git a/easyflow-modules/easyflow-module-system/src/test/java/tech/easyflow/system/service/impl/SysRoleServiceImplTest.java b/easyflow-modules/easyflow-module-system/src/test/java/tech/easyflow/system/service/impl/SysRoleServiceImplTest.java new file mode 100644 index 00000000..f8bcc2f2 --- /dev/null +++ b/easyflow-modules/easyflow-module-system/src/test/java/tech/easyflow/system/service/impl/SysRoleServiceImplTest.java @@ -0,0 +1,118 @@ +package tech.easyflow.system.service.impl; + +import com.mybatisflex.core.query.QueryWrapper; +import org.junit.Test; +import tech.easyflow.common.constant.enums.EnumDataStatus; +import tech.easyflow.system.entity.SysAccountRole; +import tech.easyflow.system.entity.SysRole; +import tech.easyflow.system.service.SysAccountRoleService; + +import java.io.Serializable; +import java.lang.reflect.Field; +import java.math.BigInteger; +import java.util.Collection; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * {@link SysRoleServiceImpl} 角色状态过滤测试。 + */ +public class SysRoleServiceImplTest { + + /** + * 验证账号角色查询只返回启用角色。 + * + * @throws Exception 注入测试依赖失败 + */ + @Test + public void getRolesByAccountIdShouldExcludeUnavailableRoles() throws Exception { + BigInteger accountId = BigInteger.valueOf(10); + BigInteger availableRoleId = BigInteger.valueOf(20); + BigInteger unavailableRoleId = BigInteger.valueOf(30); + SysAccountRoleService accountRoleService = mock(SysAccountRoleService.class); + SysAccountRole availableRelation = accountRole(accountId, availableRoleId); + SysAccountRole unavailableRelation = accountRole(accountId, unavailableRoleId); + when(accountRoleService.list(any(QueryWrapper.class))) + .thenReturn(List.of(availableRelation, unavailableRelation)); + + SysRole availableRole = role(availableRoleId, EnumDataStatus.AVAILABLE.getCode()); + SysRole unavailableRole = role(unavailableRoleId, EnumDataStatus.UNAVAILABLE.getCode()); + TestSysRoleService service = new TestSysRoleService(List.of(availableRole, unavailableRole)); + setField(service, "sysAccountRoleService", accountRoleService); + + List roles = service.getRolesByAccountId(accountId); + + assertEquals(List.of(availableRole), roles); + } + + /** + * 构造账号角色关系。 + * + * @param accountId 账号 ID + * @param roleId 角色 ID + * @return 账号角色关系 + */ + private SysAccountRole accountRole(BigInteger accountId, BigInteger roleId) { + SysAccountRole relation = new SysAccountRole(); + relation.setAccountId(accountId); + relation.setRoleId(roleId); + return relation; + } + + /** + * 构造角色。 + * + * @param roleId 角色 ID + * @param status 角色状态 + * @return 角色 + */ + private SysRole role(BigInteger roleId, Integer status) { + SysRole role = new SysRole(); + role.setId(roleId); + role.setStatus(status); + return role; + } + + /** + * 注入私有字段。 + * + * @param target 目标对象 + * @param fieldName 字段名称 + * @param value 字段值 + * @throws Exception 字段不存在或无法写入 + */ + private void setField(Object target, String fieldName, Object value) throws Exception { + Field field = SysRoleServiceImpl.class.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(target, value); + } + + /** + * 提供固定角色集合的测试服务。 + */ + private static final class TestSysRoleService extends SysRoleServiceImpl { + + private final List roles; + + /** + * 创建测试服务。 + * + * @param roles 固定角色集合 + */ + private TestSysRoleService(List roles) { + this.roles = roles; + } + + /** + * {@inheritDoc} + */ + @Override + public List listByIds(Collection ids) { + return roles; + } + } +} diff --git a/easyflow-ui-admin/app/src/views/system/sysAccount/SysAccountModal.vue b/easyflow-ui-admin/app/src/views/system/sysAccount/SysAccountModal.vue index 589e63a1..d0d46893 100644 --- a/easyflow-ui-admin/app/src/views/system/sysAccount/SysAccountModal.vue +++ b/easyflow-ui-admin/app/src/views/system/sysAccount/SysAccountModal.vue @@ -1,19 +1,20 @@