fix: 统一账号角色校验与默认首页
- 创建与导入账号时强制校验启用角色 - 按启用角色返回默认首页并过滤禁用角色
This commit is contained in:
@@ -8,6 +8,7 @@ import com.mybatisflex.core.paginate.Page;
|
|||||||
import com.mybatisflex.core.query.QueryWrapper;
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.springframework.dao.DuplicateKeyException;
|
import org.springframework.dao.DuplicateKeyException;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
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.entity.EncryptedCredentialDTO;
|
||||||
import tech.easyflow.auth.service.AuthCredentialKeyService;
|
import tech.easyflow.auth.service.AuthCredentialKeyService;
|
||||||
import tech.easyflow.auth.service.AuthService;
|
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.EnumAccountType;
|
||||||
import tech.easyflow.common.constant.enums.EnumDataStatus;
|
import tech.easyflow.common.constant.enums.EnumDataStatus;
|
||||||
import tech.easyflow.common.domain.Result;
|
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.controller.BaseCurdController;
|
||||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||||
import tech.easyflow.log.annotation.LogRecord;
|
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.SysAccount;
|
||||||
|
import tech.easyflow.system.entity.SysRole;
|
||||||
import tech.easyflow.system.entity.vo.SysAccountBatchActionResultVo;
|
import tech.easyflow.system.entity.vo.SysAccountBatchActionResultVo;
|
||||||
import tech.easyflow.system.entity.vo.SysAccountImportResultVo;
|
import tech.easyflow.system.entity.vo.SysAccountImportResultVo;
|
||||||
import tech.easyflow.system.service.SysAccountService;
|
import tech.easyflow.system.service.SysAccountService;
|
||||||
|
import tech.easyflow.system.service.SysRoleService;
|
||||||
import tech.easyflow.system.util.SysPasswordPolicy;
|
import tech.easyflow.system.util.SysPasswordPolicy;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
@@ -36,9 +41,13 @@ import java.io.Serializable;
|
|||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户表 控制层。
|
* 用户表 控制层。
|
||||||
@@ -49,13 +58,27 @@ import java.util.Map;
|
|||||||
@RestController("sysAccountController")
|
@RestController("sysAccountController")
|
||||||
@RequestMapping("/api/v1/sysAccount")
|
@RequestMapping("/api/v1/sysAccount")
|
||||||
public class SysAccountController extends BaseCurdController<SysAccountService, SysAccount> {
|
public class SysAccountController extends BaseCurdController<SysAccountService, SysAccount> {
|
||||||
|
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 AuthCredentialKeyService credentialKeyService;
|
||||||
|
private final SysRoleService sysRoleService;
|
||||||
@Resource
|
@Resource
|
||||||
private AuthService authService;
|
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);
|
super(service);
|
||||||
this.credentialKeyService = credentialKeyService;
|
this.credentialKeyService = credentialKeyService;
|
||||||
|
this.sysRoleService = sysRoleService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -76,6 +99,10 @@ public class SysAccountController extends BaseCurdController<SysAccountService,
|
|||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
return Result.fail(1, "用户名已存在");
|
return Result.fail(1, "用户名已存在");
|
||||||
}
|
}
|
||||||
|
Result<?> roleValidation = validateCreateRoles(entity);
|
||||||
|
if (roleValidation != null) {
|
||||||
|
return roleValidation;
|
||||||
|
}
|
||||||
String password = decryptInitialPassword(entity.getPasswordCredential());
|
String password = decryptInitialPassword(entity.getPasswordCredential());
|
||||||
if (!StringUtil.hasText(password)) {
|
if (!StringUtil.hasText(password)) {
|
||||||
return Result.fail(1, "密码不能为空");
|
return Result.fail(1, "密码不能为空");
|
||||||
@@ -144,11 +171,24 @@ public class SysAccountController extends BaseCurdController<SysAccountService,
|
|||||||
return super.onRemoveBefore(ids);
|
return super.onRemoveBefore(ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前账号资料、角色标识与默认首页。
|
||||||
|
*
|
||||||
|
* @return 当前账号资料视图
|
||||||
|
*/
|
||||||
@GetMapping("/myProfile")
|
@GetMapping("/myProfile")
|
||||||
public Result<SysAccount> myProfile() {
|
public Result<SysAccountProfileVo> myProfile() {
|
||||||
LoginAccount account = SaTokenUtil.getLoginAccount();
|
LoginAccount account = SaTokenUtil.getLoginAccount();
|
||||||
SysAccount sysAccount = service.getById(account.getId());
|
SysAccount sysAccount = service.getById(account.getId());
|
||||||
return Result.ok(sysAccount);
|
List<String> 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")
|
@PostMapping("/updateProfile")
|
||||||
@@ -262,8 +302,12 @@ public class SysAccountController extends BaseCurdController<SysAccountService,
|
|||||||
service.writeImportTemplate(response.getOutputStream());
|
service.writeImportTemplate(response.getOutputStream());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
@PostMapping("save")
|
@PostMapping("save")
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public Result<?> save(@JsonBody SysAccount entity) {
|
public Result<?> save(@JsonBody SysAccount entity) {
|
||||||
try {
|
try {
|
||||||
return super.save(entity);
|
return super.save(entity);
|
||||||
@@ -271,4 +315,33 @@ public class SysAccountController extends BaseCurdController<SysAccountService,
|
|||||||
return Result.fail(1, "用户名已存在");
|
return Result.fail(1, "用户名已存在");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验创建账号时提交的角色,并归一化角色 ID。
|
||||||
|
*
|
||||||
|
* @param entity 待创建账号
|
||||||
|
* @return 校验失败结果,校验通过返回 null
|
||||||
|
*/
|
||||||
|
private Result<?> validateCreateRoles(SysAccount entity) {
|
||||||
|
List<BigInteger> roleIds = entity == null ? null : entity.getRoleIds();
|
||||||
|
Set<BigInteger> uniqueRoleIds = new LinkedHashSet<>();
|
||||||
|
if (roleIds != null) {
|
||||||
|
roleIds.stream()
|
||||||
|
.filter(java.util.Objects::nonNull)
|
||||||
|
.forEach(uniqueRoleIds::add);
|
||||||
|
}
|
||||||
|
if (uniqueRoleIds.isEmpty()) {
|
||||||
|
return Result.fail(1, "角色不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<SysRole> 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<String> roles = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建空的当前账号资料视图。
|
||||||
|
*/
|
||||||
|
public SysAccountProfileVo() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据账号实体和角色信息创建资料视图。
|
||||||
|
*
|
||||||
|
* @param account 账号实体
|
||||||
|
* @param roles 角色标识列表
|
||||||
|
* @param homePath 默认首页
|
||||||
|
* @return 当前账号资料视图
|
||||||
|
*/
|
||||||
|
public static SysAccountProfileVo from(SysAccount account, List<String> 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<String> getRoles() {
|
||||||
|
return roles;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置角色标识列表。
|
||||||
|
*
|
||||||
|
* @param roles 角色标识列表
|
||||||
|
*/
|
||||||
|
public void setRoles(List<String> roles) {
|
||||||
|
this.roles = roles == null ? new ArrayList<>() : new ArrayList<>(roles);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,22 +2,28 @@ package tech.easyflow.admin.controller.system;
|
|||||||
|
|
||||||
import com.alibaba.fastjson2.JSONObject;
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
import com.mybatisflex.core.query.QueryWrapper;
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
import org.mockito.ArgumentCaptor;
|
|
||||||
import org.mockito.MockedStatic;
|
import org.mockito.MockedStatic;
|
||||||
import org.testng.annotations.Test;
|
import org.testng.annotations.Test;
|
||||||
import tech.easyflow.auth.service.AuthCredentialKeyService;
|
import tech.easyflow.auth.service.AuthCredentialKeyService;
|
||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
import tech.easyflow.common.entity.LoginAccount;
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
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.SysAccount;
|
||||||
|
import tech.easyflow.system.entity.SysRole;
|
||||||
import tech.easyflow.system.service.SysAccountService;
|
import tech.easyflow.system.service.SysAccountService;
|
||||||
|
import tech.easyflow.system.service.SysRoleService;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyCollection;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.mockStatic;
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
import static org.testng.Assert.assertEquals;
|
import static org.testng.Assert.assertEquals;
|
||||||
@@ -28,7 +34,7 @@ import static org.testng.Assert.assertEquals;
|
|||||||
public class SysAccountControllerTest {
|
public class SysAccountControllerTest {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证创建用户时保留表单选择的部门,而不是替换为操作者部门。
|
* 验证创建用户时保留表单选择的部门,不被操作者部门覆盖。
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
public void saveShouldKeepSubmittedDepartment() {
|
public void saveShouldKeepSubmittedDepartment() {
|
||||||
@@ -38,13 +44,28 @@ public class SysAccountControllerTest {
|
|||||||
BigInteger operatorId = BigInteger.valueOf(400);
|
BigInteger operatorId = BigInteger.valueOf(400);
|
||||||
SysAccountService accountService = mock(SysAccountService.class);
|
SysAccountService accountService = mock(SysAccountService.class);
|
||||||
AuthCredentialKeyService credentialKeyService = mock(AuthCredentialKeyService.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);
|
SysAccount entity = createAccount(selectedDeptId);
|
||||||
LoginAccount loginAccount = createLoginAccount(operatorId, tenantId, operatorDeptId);
|
LoginAccount loginAccount = createLoginAccount(operatorId, tenantId, operatorDeptId);
|
||||||
|
AtomicReference<BigInteger> savedDeptId = new AtomicReference<>();
|
||||||
|
AtomicReference<BigInteger> savedTenantId = new AtomicReference<>();
|
||||||
|
AtomicReference<BigInteger> savedCreatedBy = new AtomicReference<>();
|
||||||
|
AtomicReference<BigInteger> savedModifiedBy = new AtomicReference<>();
|
||||||
|
|
||||||
when(accountService.count(any(QueryWrapper.class))).thenReturn(0L);
|
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 -> {
|
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;
|
return true;
|
||||||
});
|
});
|
||||||
when(credentialKeyService.decryptPayload(any()))
|
when(credentialKeyService.decryptPayload(any()))
|
||||||
@@ -58,13 +79,107 @@ public class SysAccountControllerTest {
|
|||||||
assertEquals(result.getErrorCode(), 0);
|
assertEquals(result.getErrorCode(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
ArgumentCaptor<SysAccount> accountCaptor = ArgumentCaptor.forClass(SysAccount.class);
|
verify(accountService).save(any(SysAccount.class));
|
||||||
verify(accountService).save(accountCaptor.capture());
|
assertEquals(savedDeptId.get(), selectedDeptId);
|
||||||
SysAccount savedAccount = accountCaptor.getValue();
|
assertEquals(savedTenantId.get(), tenantId);
|
||||||
assertEquals(savedAccount.getDeptId(), selectedDeptId);
|
assertEquals(savedCreatedBy.get(), operatorId);
|
||||||
assertEquals(savedAccount.getTenantId(), tenantId);
|
assertEquals(savedModifiedBy.get(), operatorId);
|
||||||
assertEquals(savedAccount.getCreatedBy(), operatorId);
|
}
|
||||||
assertEquals(savedAccount.getModifiedBy(), 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<SaTokenUtil> 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<SaTokenUtil> saToken = mockStatic(SaTokenUtil.class)) {
|
||||||
|
saToken.when(SaTokenUtil::getLoginAccount)
|
||||||
|
.thenReturn(createLoginAccount(accountId, BigInteger.ZERO, BigInteger.ZERO));
|
||||||
|
|
||||||
|
Result<SysAccountProfileVo> 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<SaTokenUtil> saToken = mockStatic(SaTokenUtil.class)) {
|
||||||
|
saToken.when(SaTokenUtil::getLoginAccount)
|
||||||
|
.thenReturn(createLoginAccount(accountId, BigInteger.ZERO, BigInteger.ZERO));
|
||||||
|
|
||||||
|
Result<SysAccountProfileVo> 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.setLoginName("department_test_user");
|
||||||
account.setNickname("部门测试用户");
|
account.setNickname("部门测试用户");
|
||||||
account.setPasswordCredential(Map.of("keyId", "test-key"));
|
account.setPasswordCredential(Map.of("keyId", "test-key"));
|
||||||
|
account.setRoleIds(List.of(BigInteger.ONE));
|
||||||
return account;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构造当前登录账号。
|
* 构造当前登录账号。
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -16,6 +16,12 @@ public interface SysRoleService extends IService<SysRole> {
|
|||||||
|
|
||||||
void saveRoleMenu(BigInteger roleId, List<String> keys);
|
void saveRoleMenu(BigInteger roleId, List<String> keys);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询账号当前关联的启用角色。
|
||||||
|
*
|
||||||
|
* @param accountId 账号 ID
|
||||||
|
* @return 启用角色列表
|
||||||
|
*/
|
||||||
List<SysRole> getRolesByAccountId(BigInteger accountId);
|
List<SysRole> getRolesByAccountId(BigInteger accountId);
|
||||||
|
|
||||||
void saveRole(SysRole sysRole);
|
void saveRole(SysRole sysRole);
|
||||||
|
|||||||
@@ -80,7 +80,9 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
|||||||
private static final String IMPORT_HEAD_MOBILE = "手机号";
|
private static final String IMPORT_HEAD_MOBILE = "手机号";
|
||||||
private static final String IMPORT_HEAD_EMAIL = "邮箱";
|
private static final String IMPORT_HEAD_EMAIL = "邮箱";
|
||||||
private static final String IMPORT_HEAD_STATUS = "状态";
|
private static final String IMPORT_HEAD_STATUS = "状态";
|
||||||
private static final String IMPORT_HEAD_ROLE_NAMES = "角色名称";
|
private static final String IMPORT_HEAD_ROLE_NAMES = "角色名称*";
|
||||||
|
/** 兼容已下载的旧版导入模板表头。 */
|
||||||
|
private static final String IMPORT_HEAD_ROLE_NAMES_LEGACY = "角色名称";
|
||||||
private static final String IMPORT_HEAD_POSITION_NAMES = "岗位名称";
|
private static final String IMPORT_HEAD_POSITION_NAMES = "岗位名称";
|
||||||
private static final String IMPORT_HEAD_REMARK = "备注";
|
private static final String IMPORT_HEAD_REMARK = "备注";
|
||||||
private static final String IMPORT_GUIDE_HEAD_ITEM = "说明项";
|
private static final String IMPORT_GUIDE_HEAD_ITEM = "说明项";
|
||||||
@@ -328,7 +330,9 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
|||||||
SysDept::getDeptName
|
SysDept::getDeptName
|
||||||
);
|
);
|
||||||
ImportNameLookup<SysRole> roleLookup = buildImportNameLookup(
|
ImportNameLookup<SysRole> roleLookup = buildImportNameLookup(
|
||||||
sysRoleMapper.selectListByQuery(QueryWrapper.create()),
|
sysRoleMapper.selectListByQuery(
|
||||||
|
QueryWrapper.create().eq(SysRole::getStatus, EnumDataStatus.AVAILABLE.getCode())
|
||||||
|
),
|
||||||
SysRole::getRoleName
|
SysRole::getRoleName
|
||||||
);
|
);
|
||||||
ImportNameLookup<SysPosition> positionLookup = buildImportNameLookup(
|
ImportNameLookup<SysPosition> positionLookup = buildImportNameLookup(
|
||||||
@@ -388,6 +392,7 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
|||||||
String nickname = trimToNull(row.getNickname());
|
String nickname = trimToNull(row.getNickname());
|
||||||
String mobile = trimToNull(row.getMobile());
|
String mobile = trimToNull(row.getMobile());
|
||||||
String email = trimToNull(row.getEmail());
|
String email = trimToNull(row.getEmail());
|
||||||
|
List<String> roleNames = splitCodes(row.getRoleNames());
|
||||||
|
|
||||||
if (deptName == null) {
|
if (deptName == null) {
|
||||||
addImportDetail(details, IMPORT_FIELD_DEPT_NAME, row.getDeptName(), "部门名称不能为空");
|
addImportDetail(details, IMPORT_FIELD_DEPT_NAME, row.getDeptName(), "部门名称不能为空");
|
||||||
@@ -400,6 +405,9 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
|||||||
if (nickname == null) {
|
if (nickname == null) {
|
||||||
addImportDetail(details, IMPORT_FIELD_NICKNAME, row.getNickname(), "昵称不能为空");
|
addImportDetail(details, IMPORT_FIELD_NICKNAME, row.getNickname(), "昵称不能为空");
|
||||||
}
|
}
|
||||||
|
if (roleNames.isEmpty()) {
|
||||||
|
addImportDetail(details, IMPORT_FIELD_ROLE_NAME, row.getRoleNames(), "角色名称不能为空");
|
||||||
|
}
|
||||||
if (mobile != null && !StringUtil.isMobileNumber(mobile)) {
|
if (mobile != null && !StringUtil.isMobileNumber(mobile)) {
|
||||||
addImportDetail(details, IMPORT_FIELD_MOBILE, row.getMobile(), "手机号格式不正确");
|
addImportDetail(details, IMPORT_FIELD_MOBILE, row.getMobile(), "手机号格式不正确");
|
||||||
}
|
}
|
||||||
@@ -410,14 +418,14 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
|||||||
SysDept dept = resolveSingleResource(deptName, deptLookup, IMPORT_FIELD_DEPT_NAME, details);
|
SysDept dept = resolveSingleResource(deptName, deptLookup, IMPORT_FIELD_DEPT_NAME, details);
|
||||||
Integer status = parseStatus(row.getStatus(), details);
|
Integer status = parseStatus(row.getStatus(), details);
|
||||||
List<BigInteger> roleIds = resolveResourceIds(
|
List<BigInteger> roleIds = resolveResourceIds(
|
||||||
row.getRoleNames(),
|
roleNames,
|
||||||
roleLookup,
|
roleLookup,
|
||||||
SysRole::getId,
|
SysRole::getId,
|
||||||
IMPORT_FIELD_ROLE_NAME,
|
IMPORT_FIELD_ROLE_NAME,
|
||||||
details
|
details
|
||||||
);
|
);
|
||||||
List<BigInteger> positionIds = resolveResourceIds(
|
List<BigInteger> positionIds = resolveResourceIds(
|
||||||
row.getPositionNames(),
|
splitCodes(row.getPositionNames()),
|
||||||
positionLookup,
|
positionLookup,
|
||||||
SysPosition::getId,
|
SysPosition::getId,
|
||||||
IMPORT_FIELD_POSITION_NAME,
|
IMPORT_FIELD_POSITION_NAME,
|
||||||
@@ -487,7 +495,7 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
|||||||
/**
|
/**
|
||||||
* 解析名称列表并映射为主键集合。
|
* 解析名称列表并映射为主键集合。
|
||||||
*
|
*
|
||||||
* @param rawNames 原始名称文本
|
* @param names 标准化后的名称列表
|
||||||
* @param lookup 名称查找表
|
* @param lookup 名称查找表
|
||||||
* @param fieldName 字段名称
|
* @param fieldName 字段名称
|
||||||
* @param details 错误明细收集器
|
* @param details 错误明细收集器
|
||||||
@@ -495,12 +503,11 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
|||||||
* @return 匹配到的主键集合
|
* @return 匹配到的主键集合
|
||||||
*/
|
*/
|
||||||
private <T> List<BigInteger> resolveResourceIds(
|
private <T> List<BigInteger> resolveResourceIds(
|
||||||
String rawNames,
|
List<String> names,
|
||||||
ImportNameLookup<T> lookup,
|
ImportNameLookup<T> lookup,
|
||||||
Function<T, BigInteger> idExtractor,
|
Function<T, BigInteger> idExtractor,
|
||||||
String fieldName,
|
String fieldName,
|
||||||
List<SysAccountImportErrorDetailVo> details) {
|
List<SysAccountImportErrorDetailVo> details) {
|
||||||
List<String> names = splitCodes(rawNames);
|
|
||||||
if (names.isEmpty()) {
|
if (names.isEmpty()) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
@@ -693,8 +700,8 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
|||||||
private List<List<String>> buildImportGuideRows() {
|
private List<List<String>> buildImportGuideRows() {
|
||||||
List<List<String>> rows = new ArrayList<>();
|
List<List<String>> 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("可选字段", "手机号、邮箱、状态、岗位名称、备注"));
|
||||||
rows.add(List.of("状态可选值", "可留空,或填写 1/0/已启用/启用/未启用/停用/禁用"));
|
rows.add(List.of("状态可选值", "可留空,或填写 1/0/已启用/启用/未启用/停用/禁用"));
|
||||||
rows.add(List.of("多值分隔", "角色名称、岗位名称支持使用英文逗号,或中文逗号,分隔多个名称"));
|
rows.add(List.of("多值分隔", "角色名称、岗位名称支持使用英文逗号,或中文逗号,分隔多个名称"));
|
||||||
rows.add(List.of(
|
rows.add(List.of(
|
||||||
@@ -853,7 +860,7 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
|||||||
String mobile = getCellValue(data, IMPORT_HEAD_MOBILE);
|
String mobile = getCellValue(data, IMPORT_HEAD_MOBILE);
|
||||||
String email = getCellValue(data, IMPORT_HEAD_EMAIL);
|
String email = getCellValue(data, IMPORT_HEAD_EMAIL);
|
||||||
String status = getCellValue(data, IMPORT_HEAD_STATUS);
|
String status = getCellValue(data, IMPORT_HEAD_STATUS);
|
||||||
String roleNames = getCellValue(data, IMPORT_HEAD_ROLE_NAMES);
|
String roleNames = getCellValue(data, IMPORT_HEAD_ROLE_NAMES, IMPORT_HEAD_ROLE_NAMES_LEGACY);
|
||||||
String positionNames = getCellValue(data, IMPORT_HEAD_POSITION_NAMES);
|
String positionNames = getCellValue(data, IMPORT_HEAD_POSITION_NAMES);
|
||||||
String remark = getCellValue(data, IMPORT_HEAD_REMARK);
|
String remark = getCellValue(data, IMPORT_HEAD_REMARK);
|
||||||
if (!StringUtil.hasText(deptName)
|
if (!StringUtil.hasText(deptName)
|
||||||
@@ -896,9 +903,14 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
|||||||
List<String> requiredHeads = List.of(
|
List<String> requiredHeads = List.of(
|
||||||
IMPORT_HEAD_DEPT_NAME,
|
IMPORT_HEAD_DEPT_NAME,
|
||||||
IMPORT_HEAD_LOGIN_NAME,
|
IMPORT_HEAD_LOGIN_NAME,
|
||||||
IMPORT_HEAD_NICKNAME
|
IMPORT_HEAD_NICKNAME,
|
||||||
|
IMPORT_HEAD_ROLE_NAMES
|
||||||
);
|
);
|
||||||
for (String requiredHead : requiredHeads) {
|
for (String requiredHead : requiredHeads) {
|
||||||
|
if (IMPORT_HEAD_ROLE_NAMES.equals(requiredHead)
|
||||||
|
&& headIndex.containsKey(IMPORT_HEAD_ROLE_NAMES_LEGACY)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (!headIndex.containsKey(requiredHead)) {
|
if (!headIndex.containsKey(requiredHead)) {
|
||||||
throw new BusinessException("导入模板表头不正确,必须包含:" + String.join("、", requiredHeads));
|
throw new BusinessException("导入模板表头不正确,必须包含:" + String.join("、", requiredHeads));
|
||||||
}
|
}
|
||||||
@@ -914,13 +926,23 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
|||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getCellValue(Map<Integer, Object> row, String headName) {
|
/**
|
||||||
Integer index = headIndex.get(headName);
|
* 按候选表头顺序读取单元格文本。
|
||||||
if (index == null) {
|
*
|
||||||
return null;
|
* @param row 当前数据行
|
||||||
|
* @param headNames 候选表头
|
||||||
|
* @return 单元格文本,无匹配表头时返回 {@code null}
|
||||||
|
*/
|
||||||
|
private String getCellValue(Map<Integer, Object> 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 null;
|
||||||
return value == null ? null : String.valueOf(value).trim();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import org.springframework.stereotype.Service;
|
|||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import tech.easyflow.common.cache.RedisLockExecutor;
|
import tech.easyflow.common.cache.RedisLockExecutor;
|
||||||
import tech.easyflow.common.constant.enums.EnumDataScope;
|
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.SysAccountRole;
|
||||||
import tech.easyflow.system.entity.SysRole;
|
import tech.easyflow.system.entity.SysRole;
|
||||||
import tech.easyflow.system.entity.SysRoleDept;
|
import tech.easyflow.system.entity.SysRoleDept;
|
||||||
@@ -80,6 +81,9 @@ public class SysRoleServiceImpl extends ServiceImpl<SysRoleMapper, SysRole> impl
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public List<SysRole> getRolesByAccountId(BigInteger accountId) {
|
public List<SysRole> getRolesByAccountId(BigInteger accountId) {
|
||||||
// 查询用户对应角色id集合
|
// 查询用户对应角色id集合
|
||||||
@@ -89,7 +93,10 @@ public class SysRoleServiceImpl extends ServiceImpl<SysRoleMapper, SysRole> impl
|
|||||||
if (CollectionUtil.isEmpty(roleIds)) {
|
if (CollectionUtil.isEmpty(roleIds)) {
|
||||||
return new ArrayList<>();
|
return new ArrayList<>();
|
||||||
}
|
}
|
||||||
return listByIds(roleIds);
|
return listByIds(roleIds).stream()
|
||||||
|
.filter(role -> role != null
|
||||||
|
&& EnumDataStatus.AVAILABLE.getCode().equals(role.getStatus()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -2,28 +2,55 @@ package tech.easyflow.system.service.impl;
|
|||||||
|
|
||||||
import cn.dev33.satoken.stp.StpUtil;
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
import cn.hutool.crypto.digest.BCrypt;
|
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 com.mybatisflex.core.query.QueryWrapper;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.mockito.ArgumentCaptor;
|
import org.mockito.ArgumentCaptor;
|
||||||
import org.mockito.MockedStatic;
|
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.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.config.AccountSecurityProperties;
|
||||||
import tech.easyflow.system.entity.SysAccount;
|
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.lang.reflect.Field;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.Mockito.doReturn;
|
import static org.mockito.Mockito.doReturn;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.mockStatic;
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.spy;
|
import static org.mockito.Mockito.spy;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link SysAccountServiceImpl} 测试。
|
* {@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<String> 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<SysAccountImportErrorDetailVo> 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<List<String>> headers, List<List<String>> 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<String> readFirstSheetHeaders(byte[] workbook) {
|
||||||
|
List<String> headers = new ArrayList<>();
|
||||||
|
ReadListener<LinkedHashMap<Integer, Object>> listener =
|
||||||
|
new ReadListener<LinkedHashMap<Integer, Object>>() {
|
||||||
|
@Override
|
||||||
|
public void invoke(LinkedHashMap<Integer, Object> data, AnalysisContext context) {
|
||||||
|
// 模板没有数据行,仅检查表头。
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void invokeHead(Map<Integer, ReadCellData<?>> 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;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置测试对象的私有字段。
|
* 设置测试对象的私有字段。
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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<SysRole> 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<SysRole> roles;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建测试服务。
|
||||||
|
*
|
||||||
|
* @param roles 固定角色集合
|
||||||
|
*/
|
||||||
|
private TestSysRoleService(List<SysRole> roles) {
|
||||||
|
this.roles = roles;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<SysRole> listByIds(Collection<? extends Serializable> ids) {
|
||||||
|
return roles;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,20 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type {FormInstance} from 'element-plus';
|
import type { FormInstance } from 'element-plus';
|
||||||
import {ElForm, ElFormItem, ElInput, ElMessage} from 'element-plus';
|
|
||||||
|
|
||||||
import {onMounted, ref, watch} from 'vue';
|
import { onMounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
import {EasyFlowFormModal, EasyFlowInputPassword} from '@easyflow/common-ui';
|
import { EasyFlowFormModal, EasyFlowInputPassword } from '@easyflow/common-ui';
|
||||||
|
|
||||||
import {getCredentialKeyApi} from '#/api';
|
import { ElForm, ElFormItem, ElInput, ElMessage } from 'element-plus';
|
||||||
import {api} from '#/api/request';
|
|
||||||
|
import { getCredentialKeyApi } from '#/api';
|
||||||
|
import { api } from '#/api/request';
|
||||||
import DictSelect from '#/components/dict/DictSelect.vue';
|
import DictSelect from '#/components/dict/DictSelect.vue';
|
||||||
// import Cropper from '#/components/upload/Cropper.vue';
|
// import Cropper from '#/components/upload/Cropper.vue';
|
||||||
import UploadAvatar from '#/components/upload/UploadAvatar.vue';
|
import UploadAvatar from '#/components/upload/UploadAvatar.vue';
|
||||||
import {$t} from '#/locales';
|
import { $t } from '#/locales';
|
||||||
import {encryptCredentialPayload} from '#/utils/credential-encryption';
|
import { encryptCredentialPayload } from '#/utils/credential-encryption';
|
||||||
import {isStrongPassword} from '#/utils/password-policy';
|
import { isStrongPassword } from '#/utils/password-policy';
|
||||||
|
|
||||||
const emit = defineEmits(['reload']);
|
const emit = defineEmits(['reload']);
|
||||||
// vue
|
// vue
|
||||||
@@ -71,6 +72,21 @@ const validateConfirmPassword = (_rule: any, value: string, callback: any) => {
|
|||||||
}
|
}
|
||||||
callback();
|
callback();
|
||||||
};
|
};
|
||||||
|
const validateRoles = (_rule: any, value: unknown, callback: any) => {
|
||||||
|
if (!isAdd.value) {
|
||||||
|
callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!Array.isArray(value) ||
|
||||||
|
value.filter((roleId) => roleId !== null && roleId !== undefined).length ===
|
||||||
|
0
|
||||||
|
) {
|
||||||
|
callback(new Error($t('message.required')));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
};
|
||||||
const btnLoading = ref(false);
|
const btnLoading = ref(false);
|
||||||
const rules = ref({
|
const rules = ref({
|
||||||
deptId: [
|
deptId: [
|
||||||
@@ -82,6 +98,7 @@ const rules = ref({
|
|||||||
nickname: [
|
nickname: [
|
||||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||||
],
|
],
|
||||||
|
roleIds: [{ validator: validateRoles, trigger: 'change' }],
|
||||||
password: [
|
password: [
|
||||||
{ required: true, validator: validateStrongPassword, trigger: 'blur' },
|
{ required: true, validator: validateStrongPassword, trigger: 'blur' },
|
||||||
],
|
],
|
||||||
@@ -222,7 +239,11 @@ watch(
|
|||||||
<ElFormItem prop="remark" :label="$t('sysAccount.remark')">
|
<ElFormItem prop="remark" :label="$t('sysAccount.remark')">
|
||||||
<ElInput v-model.trim="entity.remark" />
|
<ElInput v-model.trim="entity.remark" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem prop="roleIds" :label="$t('sysAccount.roleIds')">
|
<ElFormItem
|
||||||
|
prop="roleIds"
|
||||||
|
:label="$t('sysAccount.roleIds')"
|
||||||
|
:required="isAdd"
|
||||||
|
>
|
||||||
<DictSelect multiple v-model="entity.roleIds" dict-code="sysRole" />
|
<DictSelect multiple v-model="entity.roleIds" dict-code="sysRole" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem prop="positionIds" :label="$t('sysAccount.positionIds')">
|
<ElFormItem prop="positionIds" :label="$t('sysAccount.positionIds')">
|
||||||
|
|||||||
Reference in New Issue
Block a user