feat: 支持账号导入与强制改密
- 新增账号导入模板下载、导入校验和默认密码重置标记 - 支持管理员重置密码并在登录后强制跳转修改密码 - 管理端与用户中心接入强密码校验和密码重置流程
This commit is contained in:
@@ -43,6 +43,12 @@ public class SysAccountBase extends DateEntity implements Serializable {
|
||||
@Column(comment = "密码")
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 是否需要重置密码
|
||||
*/
|
||||
@Column(comment = "是否需要重置密码")
|
||||
private Boolean passwordResetRequired;
|
||||
|
||||
/**
|
||||
* 账户类型
|
||||
*/
|
||||
@@ -149,6 +155,14 @@ public class SysAccountBase extends DateEntity implements Serializable {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public Boolean getPasswordResetRequired() {
|
||||
return passwordResetRequired;
|
||||
}
|
||||
|
||||
public void setPasswordResetRequired(Boolean passwordResetRequired) {
|
||||
this.passwordResetRequired = passwordResetRequired;
|
||||
}
|
||||
|
||||
public Integer getAccountType() {
|
||||
return accountType;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package tech.easyflow.system.entity.vo;
|
||||
|
||||
/**
|
||||
* 用户导入失败行。
|
||||
*/
|
||||
public class SysAccountImportErrorRowVo {
|
||||
|
||||
private Integer rowNumber;
|
||||
|
||||
private String deptCode;
|
||||
|
||||
private String loginName;
|
||||
|
||||
private String reason;
|
||||
|
||||
public Integer getRowNumber() {
|
||||
return rowNumber;
|
||||
}
|
||||
|
||||
public void setRowNumber(Integer rowNumber) {
|
||||
this.rowNumber = rowNumber;
|
||||
}
|
||||
|
||||
public String getDeptCode() {
|
||||
return deptCode;
|
||||
}
|
||||
|
||||
public void setDeptCode(String deptCode) {
|
||||
this.deptCode = deptCode;
|
||||
}
|
||||
|
||||
public String getLoginName() {
|
||||
return loginName;
|
||||
}
|
||||
|
||||
public void setLoginName(String loginName) {
|
||||
this.loginName = loginName;
|
||||
}
|
||||
|
||||
public String getReason() {
|
||||
return reason;
|
||||
}
|
||||
|
||||
public void setReason(String reason) {
|
||||
this.reason = reason;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package tech.easyflow.system.entity.vo;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户导入结果。
|
||||
*/
|
||||
public class SysAccountImportResultVo {
|
||||
|
||||
private int successCount = 0;
|
||||
|
||||
private int errorCount = 0;
|
||||
|
||||
private int totalCount = 0;
|
||||
|
||||
private List<SysAccountImportErrorRowVo> errorRows = new ArrayList<>();
|
||||
|
||||
public int getSuccessCount() {
|
||||
return successCount;
|
||||
}
|
||||
|
||||
public void setSuccessCount(int successCount) {
|
||||
this.successCount = successCount;
|
||||
}
|
||||
|
||||
public int getErrorCount() {
|
||||
return errorCount;
|
||||
}
|
||||
|
||||
public void setErrorCount(int errorCount) {
|
||||
this.errorCount = errorCount;
|
||||
}
|
||||
|
||||
public int getTotalCount() {
|
||||
return totalCount;
|
||||
}
|
||||
|
||||
public void setTotalCount(int totalCount) {
|
||||
this.totalCount = totalCount;
|
||||
}
|
||||
|
||||
public List<SysAccountImportErrorRowVo> getErrorRows() {
|
||||
return errorRows;
|
||||
}
|
||||
|
||||
public void setErrorRows(List<SysAccountImportErrorRowVo> errorRows) {
|
||||
this.errorRows = errorRows;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,13 @@
|
||||
package tech.easyflow.system.service;
|
||||
|
||||
import com.mybatisflex.core.service.IService;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.system.entity.SysAccount;
|
||||
import tech.easyflow.system.entity.vo.SysAccountImportResultVo;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* 用户表 服务层。
|
||||
@@ -14,4 +20,10 @@ public interface SysAccountService extends IService<SysAccount> {
|
||||
void syncRelations(SysAccount entity);
|
||||
|
||||
SysAccount getByUsername(String userKey);
|
||||
|
||||
void resetPassword(BigInteger accountId, BigInteger operatorId);
|
||||
|
||||
SysAccountImportResultVo importAccounts(MultipartFile file, LoginAccount loginAccount);
|
||||
|
||||
void writeImportTemplate(OutputStream outputStream);
|
||||
}
|
||||
|
||||
@@ -1,25 +1,54 @@
|
||||
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.FastExcel;
|
||||
import cn.idev.excel.context.AnalysisContext;
|
||||
import cn.idev.excel.metadata.data.ReadCellData;
|
||||
import cn.idev.excel.read.listener.ReadListener;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import tech.easyflow.common.cache.RedisLockExecutor;
|
||||
import tech.easyflow.common.constant.enums.EnumAccountType;
|
||||
import tech.easyflow.common.constant.enums.EnumDataStatus;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.util.StringUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.entity.SysAccount;
|
||||
import tech.easyflow.system.entity.SysAccountPosition;
|
||||
import tech.easyflow.system.entity.SysAccountRole;
|
||||
import tech.easyflow.system.entity.SysDept;
|
||||
import tech.easyflow.system.entity.SysPosition;
|
||||
import tech.easyflow.system.entity.SysRole;
|
||||
import tech.easyflow.system.entity.vo.SysAccountImportErrorRowVo;
|
||||
import tech.easyflow.system.entity.vo.SysAccountImportResultVo;
|
||||
import tech.easyflow.system.mapper.SysAccountMapper;
|
||||
import tech.easyflow.system.mapper.SysAccountPositionMapper;
|
||||
import tech.easyflow.system.mapper.SysAccountRoleMapper;
|
||||
import tech.easyflow.system.mapper.SysDeptMapper;
|
||||
import tech.easyflow.system.mapper.SysPositionMapper;
|
||||
import tech.easyflow.system.mapper.SysRoleMapper;
|
||||
import tech.easyflow.system.service.SysAccountService;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.time.Duration;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
@@ -34,6 +63,18 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
||||
private static final String ACCOUNT_RELATION_LOCK_KEY_PREFIX = "easyflow:lock:sys:account:relation:";
|
||||
private static final Duration LOCK_WAIT_TIMEOUT = Duration.ofSeconds(2);
|
||||
private static final Duration LOCK_LEASE_TIMEOUT = Duration.ofSeconds(10);
|
||||
private static final String DEFAULT_RESET_PASSWORD = "123456";
|
||||
private static final long MAX_IMPORT_FILE_SIZE_BYTES = 10L * 1024 * 1024;
|
||||
private static final int MAX_IMPORT_ROWS = 5000;
|
||||
private static final String IMPORT_HEAD_DEPT_CODE = "部门编码";
|
||||
private static final String IMPORT_HEAD_LOGIN_NAME = "登录账号";
|
||||
private static final String IMPORT_HEAD_NICKNAME = "昵称";
|
||||
private static final String IMPORT_HEAD_MOBILE = "手机号";
|
||||
private static final String IMPORT_HEAD_EMAIL = "邮箱";
|
||||
private static final String IMPORT_HEAD_STATUS = "状态";
|
||||
private static final String IMPORT_HEAD_ROLE_KEYS = "角色编码";
|
||||
private static final String IMPORT_HEAD_POSITION_CODES = "岗位编码";
|
||||
private static final String IMPORT_HEAD_REMARK = "备注";
|
||||
|
||||
@Resource
|
||||
private SysAccountRoleMapper sysAccountRoleMapper;
|
||||
@@ -42,7 +83,13 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
||||
@Resource
|
||||
private SysRoleMapper sysRoleMapper;
|
||||
@Resource
|
||||
private SysPositionMapper sysPositionMapper;
|
||||
@Resource
|
||||
private SysDeptMapper sysDeptMapper;
|
||||
@Resource
|
||||
private RedisLockExecutor redisLockExecutor;
|
||||
@Resource
|
||||
private PlatformTransactionManager transactionManager;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@@ -55,7 +102,6 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
||||
LOCK_WAIT_TIMEOUT,
|
||||
LOCK_LEASE_TIMEOUT,
|
||||
() -> {
|
||||
//sync roleIds
|
||||
List<BigInteger> roleIds = entity.getRoleIds();
|
||||
if (roleIds != null) {
|
||||
QueryWrapper delW = QueryWrapper.create();
|
||||
@@ -79,7 +125,6 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
||||
}
|
||||
}
|
||||
|
||||
//sync positionIds
|
||||
List<BigInteger> positionIds = entity.getPositionIds();
|
||||
if (positionIds != null) {
|
||||
QueryWrapper delW = QueryWrapper.create();
|
||||
@@ -112,4 +157,483 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
||||
w.eq(SysAccount::getLoginName, userKey);
|
||||
return getOne(w);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetPassword(BigInteger accountId, BigInteger operatorId) {
|
||||
SysAccount record = getById(accountId);
|
||||
if (record == null) {
|
||||
throw new BusinessException("用户不存在");
|
||||
}
|
||||
Integer accountType = record.getAccountType();
|
||||
if (EnumAccountType.SUPER_ADMIN.getCode().equals(accountType)) {
|
||||
throw new BusinessException("不能重置超级管理员密码");
|
||||
}
|
||||
if (EnumAccountType.TENANT_ADMIN.getCode().equals(accountType)) {
|
||||
throw new BusinessException("不能重置租户管理员密码");
|
||||
}
|
||||
SysAccount update = new SysAccount();
|
||||
update.setId(accountId);
|
||||
update.setPassword(BCrypt.hashpw(DEFAULT_RESET_PASSWORD));
|
||||
update.setPasswordResetRequired(true);
|
||||
update.setModified(new Date());
|
||||
update.setModifiedBy(operatorId);
|
||||
updateById(update);
|
||||
StpUtil.kickout(accountId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SysAccountImportResultVo importAccounts(MultipartFile file, LoginAccount loginAccount) {
|
||||
validateImportFile(file);
|
||||
List<SysAccountImportRow> rows = parseImportRows(file);
|
||||
SysAccountImportResultVo result = new SysAccountImportResultVo();
|
||||
result.setTotalCount(rows.size());
|
||||
if (rows.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
Map<String, SysDept> deptMap = buildDeptCodeMap();
|
||||
Map<String, SysRole> roleMap = buildRoleKeyMap();
|
||||
Map<String, SysPosition> positionMap = buildPositionCodeMap();
|
||||
for (SysAccountImportRow row : rows) {
|
||||
try {
|
||||
executeInRowTransaction(() -> importSingleRow(row, loginAccount, deptMap, roleMap, positionMap));
|
||||
result.setSuccessCount(result.getSuccessCount() + 1);
|
||||
} catch (Exception e) {
|
||||
result.setErrorCount(result.getErrorCount() + 1);
|
||||
appendImportError(result, row, extractImportErrorMessage(e));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeImportTemplate(OutputStream outputStream) {
|
||||
EasyExcel.write(outputStream)
|
||||
.head(buildImportHeadList())
|
||||
.sheet("模板")
|
||||
.doWrite(new ArrayList<>());
|
||||
}
|
||||
|
||||
private void executeInRowTransaction(Runnable action) {
|
||||
TransactionTemplate template = new TransactionTemplate(transactionManager);
|
||||
template.executeWithoutResult(status -> {
|
||||
try {
|
||||
action.run();
|
||||
} catch (RuntimeException e) {
|
||||
status.setRollbackOnly();
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void importSingleRow(SysAccountImportRow row,
|
||||
LoginAccount loginAccount,
|
||||
Map<String, SysDept> deptMap,
|
||||
Map<String, SysRole> roleMap,
|
||||
Map<String, SysPosition> positionMap) {
|
||||
String deptCode = trimToNull(row.getDeptCode());
|
||||
String loginName = trimToNull(row.getLoginName());
|
||||
String nickname = trimToNull(row.getNickname());
|
||||
if (deptCode == null) {
|
||||
throw new BusinessException("部门编码不能为空");
|
||||
}
|
||||
if (loginName == null) {
|
||||
throw new BusinessException("登录账号不能为空");
|
||||
}
|
||||
if (nickname == null) {
|
||||
throw new BusinessException("昵称不能为空");
|
||||
}
|
||||
SysDept dept = deptMap.get(deptCode);
|
||||
if (dept == null) {
|
||||
throw new BusinessException("部门编码不存在: " + deptCode);
|
||||
}
|
||||
ensureLoginNameNotExists(loginName);
|
||||
|
||||
List<BigInteger> roleIds = resolveRoleIds(row.getRoleKeys(), roleMap);
|
||||
List<BigInteger> positionIds = resolvePositionIds(row.getPositionCodes(), positionMap);
|
||||
|
||||
SysAccount entity = new SysAccount();
|
||||
entity.setDeptId(dept.getId());
|
||||
entity.setTenantId(loginAccount.getTenantId());
|
||||
entity.setLoginName(loginName);
|
||||
entity.setPassword(BCrypt.hashpw(DEFAULT_RESET_PASSWORD));
|
||||
entity.setPasswordResetRequired(true);
|
||||
entity.setAccountType(EnumAccountType.NORMAL.getCode());
|
||||
entity.setNickname(nickname);
|
||||
entity.setMobile(trimToNull(row.getMobile()));
|
||||
entity.setEmail(trimToNull(row.getEmail()));
|
||||
entity.setStatus(parseStatus(row.getStatus()));
|
||||
entity.setRemark(trimToNull(row.getRemark()));
|
||||
entity.setCreated(new Date());
|
||||
entity.setCreatedBy(loginAccount.getId());
|
||||
entity.setModified(new Date());
|
||||
entity.setModifiedBy(loginAccount.getId());
|
||||
entity.setRoleIds(roleIds);
|
||||
entity.setPositionIds(positionIds);
|
||||
save(entity);
|
||||
syncRelations(entity);
|
||||
}
|
||||
|
||||
private void ensureLoginNameNotExists(String loginName) {
|
||||
QueryWrapper wrapper = QueryWrapper.create();
|
||||
wrapper.eq(SysAccount::getLoginName, loginName);
|
||||
if (count(wrapper) > 0) {
|
||||
throw new BusinessException("登录账号已存在: " + loginName);
|
||||
}
|
||||
}
|
||||
|
||||
private Integer parseStatus(String rawStatus) {
|
||||
String status = trimToNull(rawStatus);
|
||||
if (status == null) {
|
||||
return EnumDataStatus.AVAILABLE.getCode();
|
||||
}
|
||||
if ("1".equals(status) || "已启用".equals(status) || "启用".equals(status)) {
|
||||
return EnumDataStatus.AVAILABLE.getCode();
|
||||
}
|
||||
if ("0".equals(status) || "未启用".equals(status) || "停用".equals(status) || "禁用".equals(status)) {
|
||||
return EnumDataStatus.UNAVAILABLE.getCode();
|
||||
}
|
||||
throw new BusinessException("状态不合法,仅支持 1/0 或 已启用/未启用");
|
||||
}
|
||||
|
||||
private List<BigInteger> resolveRoleIds(String roleKeysText, Map<String, SysRole> roleMap) {
|
||||
List<String> roleKeys = splitCodes(roleKeysText);
|
||||
if (roleKeys.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<BigInteger> roleIds = new ArrayList<>(roleKeys.size());
|
||||
for (String roleKey : roleKeys) {
|
||||
SysRole role = roleMap.get(roleKey);
|
||||
if (role == null) {
|
||||
throw new BusinessException("角色编码不存在: " + roleKey);
|
||||
}
|
||||
roleIds.add(role.getId());
|
||||
}
|
||||
return roleIds;
|
||||
}
|
||||
|
||||
private List<BigInteger> resolvePositionIds(String positionCodesText, Map<String, SysPosition> positionMap) {
|
||||
List<String> positionCodes = splitCodes(positionCodesText);
|
||||
if (positionCodes.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<BigInteger> positionIds = new ArrayList<>(positionCodes.size());
|
||||
for (String positionCode : positionCodes) {
|
||||
SysPosition position = positionMap.get(positionCode);
|
||||
if (position == null) {
|
||||
throw new BusinessException("岗位编码不存在: " + positionCode);
|
||||
}
|
||||
positionIds.add(position.getId());
|
||||
}
|
||||
return positionIds;
|
||||
}
|
||||
|
||||
private List<String> splitCodes(String rawCodes) {
|
||||
String codes = trimToNull(rawCodes);
|
||||
if (codes == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
String[] values = codes.split("[,,]");
|
||||
List<String> result = new ArrayList<>();
|
||||
Set<String> uniqueValues = new LinkedHashSet<>();
|
||||
for (String value : values) {
|
||||
String trimmed = trimToNull(value);
|
||||
if (trimmed != null && uniqueValues.add(trimmed)) {
|
||||
result.add(trimmed);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<String, SysDept> buildDeptCodeMap() {
|
||||
List<SysDept> deptList = sysDeptMapper.selectListByQuery(QueryWrapper.create());
|
||||
Map<String, SysDept> deptMap = new HashMap<>();
|
||||
for (SysDept dept : deptList) {
|
||||
String deptCode = trimToNull(dept.getDeptCode());
|
||||
if (deptCode != null) {
|
||||
deptMap.putIfAbsent(deptCode, dept);
|
||||
}
|
||||
}
|
||||
return deptMap;
|
||||
}
|
||||
|
||||
private Map<String, SysRole> buildRoleKeyMap() {
|
||||
List<SysRole> roleList = sysRoleMapper.selectListByQuery(QueryWrapper.create());
|
||||
Map<String, SysRole> roleMap = new HashMap<>();
|
||||
for (SysRole role : roleList) {
|
||||
String roleKey = trimToNull(role.getRoleKey());
|
||||
if (roleKey != null) {
|
||||
roleMap.putIfAbsent(roleKey, role);
|
||||
}
|
||||
}
|
||||
return roleMap;
|
||||
}
|
||||
|
||||
private Map<String, SysPosition> buildPositionCodeMap() {
|
||||
List<SysPosition> positionList = sysPositionMapper.selectListByQuery(QueryWrapper.create());
|
||||
Map<String, SysPosition> positionMap = new HashMap<>();
|
||||
for (SysPosition position : positionList) {
|
||||
String positionCode = trimToNull(position.getPositionCode());
|
||||
if (positionCode != null) {
|
||||
positionMap.putIfAbsent(positionCode, position);
|
||||
}
|
||||
}
|
||||
return positionMap;
|
||||
}
|
||||
|
||||
private List<SysAccountImportRow> parseImportRows(MultipartFile file) {
|
||||
try (InputStream inputStream = file.getInputStream()) {
|
||||
SysAccountExcelReadListener listener = new SysAccountExcelReadListener();
|
||||
FastExcel.read(inputStream, listener)
|
||||
.sheet()
|
||||
.doRead();
|
||||
return listener.getRows();
|
||||
} catch (BusinessException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new BusinessException("用户导入文件解析失败");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateImportFile(MultipartFile file) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new BusinessException("导入文件不能为空");
|
||||
}
|
||||
if (file.getSize() > MAX_IMPORT_FILE_SIZE_BYTES) {
|
||||
throw new BusinessException("导入文件大小不能超过10MB");
|
||||
}
|
||||
String fileName = file.getOriginalFilename();
|
||||
String lowerName = fileName == null ? "" : fileName.toLowerCase();
|
||||
if (!(lowerName.endsWith(".xlsx") || lowerName.endsWith(".xls"))) {
|
||||
throw new BusinessException("仅支持 xlsx/xls 文件");
|
||||
}
|
||||
}
|
||||
|
||||
private void appendImportError(SysAccountImportResultVo result, SysAccountImportRow row, String reason) {
|
||||
SysAccountImportErrorRowVo errorRow = new SysAccountImportErrorRowVo();
|
||||
errorRow.setRowNumber(row.getRowNumber());
|
||||
errorRow.setDeptCode(row.getDeptCode());
|
||||
errorRow.setLoginName(row.getLoginName());
|
||||
errorRow.setReason(reason);
|
||||
result.getErrorRows().add(errorRow);
|
||||
}
|
||||
|
||||
private String extractImportErrorMessage(Exception e) {
|
||||
if (e == null) {
|
||||
return "导入失败";
|
||||
}
|
||||
if (e.getCause() != null && StringUtil.hasText(e.getCause().getMessage())) {
|
||||
return e.getCause().getMessage();
|
||||
}
|
||||
if (StringUtil.hasText(e.getMessage())) {
|
||||
return e.getMessage();
|
||||
}
|
||||
return "导入失败";
|
||||
}
|
||||
|
||||
private List<List<String>> buildImportHeadList() {
|
||||
List<List<String>> headList = new ArrayList<>(9);
|
||||
headList.add(Collections.singletonList(IMPORT_HEAD_DEPT_CODE));
|
||||
headList.add(Collections.singletonList(IMPORT_HEAD_LOGIN_NAME));
|
||||
headList.add(Collections.singletonList(IMPORT_HEAD_NICKNAME));
|
||||
headList.add(Collections.singletonList(IMPORT_HEAD_MOBILE));
|
||||
headList.add(Collections.singletonList(IMPORT_HEAD_EMAIL));
|
||||
headList.add(Collections.singletonList(IMPORT_HEAD_STATUS));
|
||||
headList.add(Collections.singletonList(IMPORT_HEAD_ROLE_KEYS));
|
||||
headList.add(Collections.singletonList(IMPORT_HEAD_POSITION_CODES));
|
||||
headList.add(Collections.singletonList(IMPORT_HEAD_REMARK));
|
||||
return headList;
|
||||
}
|
||||
|
||||
private String trimToNull(String value) {
|
||||
if (!StringUtil.hasText(value)) {
|
||||
return null;
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
private static class SysAccountImportRow {
|
||||
private Integer rowNumber;
|
||||
private String deptCode;
|
||||
private String loginName;
|
||||
private String nickname;
|
||||
private String mobile;
|
||||
private String email;
|
||||
private String status;
|
||||
private String roleKeys;
|
||||
private String positionCodes;
|
||||
private String remark;
|
||||
|
||||
public Integer getRowNumber() {
|
||||
return rowNumber;
|
||||
}
|
||||
|
||||
public void setRowNumber(Integer rowNumber) {
|
||||
this.rowNumber = rowNumber;
|
||||
}
|
||||
|
||||
public String getDeptCode() {
|
||||
return deptCode;
|
||||
}
|
||||
|
||||
public void setDeptCode(String deptCode) {
|
||||
this.deptCode = deptCode;
|
||||
}
|
||||
|
||||
public String getLoginName() {
|
||||
return loginName;
|
||||
}
|
||||
|
||||
public void setLoginName(String loginName) {
|
||||
this.loginName = loginName;
|
||||
}
|
||||
|
||||
public String getNickname() {
|
||||
return nickname;
|
||||
}
|
||||
|
||||
public void setNickname(String nickname) {
|
||||
this.nickname = nickname;
|
||||
}
|
||||
|
||||
public String getMobile() {
|
||||
return mobile;
|
||||
}
|
||||
|
||||
public void setMobile(String mobile) {
|
||||
this.mobile = mobile;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getRoleKeys() {
|
||||
return roleKeys;
|
||||
}
|
||||
|
||||
public void setRoleKeys(String roleKeys) {
|
||||
this.roleKeys = roleKeys;
|
||||
}
|
||||
|
||||
public String getPositionCodes() {
|
||||
return positionCodes;
|
||||
}
|
||||
|
||||
public void setPositionCodes(String positionCodes) {
|
||||
this.positionCodes = positionCodes;
|
||||
}
|
||||
|
||||
public String getRemark() {
|
||||
return remark;
|
||||
}
|
||||
|
||||
public void setRemark(String remark) {
|
||||
this.remark = remark;
|
||||
}
|
||||
}
|
||||
|
||||
private class SysAccountExcelReadListener implements ReadListener<LinkedHashMap<Integer, Object>> {
|
||||
|
||||
private final Map<String, Integer> headIndex = new HashMap<>();
|
||||
private final List<SysAccountImportRow> rows = new ArrayList<>();
|
||||
private int sheetRowNo;
|
||||
|
||||
@Override
|
||||
public void invoke(LinkedHashMap<Integer, Object> data, AnalysisContext context) {
|
||||
sheetRowNo++;
|
||||
String deptCode = getCellValue(data, IMPORT_HEAD_DEPT_CODE);
|
||||
String loginName = getCellValue(data, IMPORT_HEAD_LOGIN_NAME);
|
||||
String nickname = getCellValue(data, IMPORT_HEAD_NICKNAME);
|
||||
String mobile = getCellValue(data, IMPORT_HEAD_MOBILE);
|
||||
String email = getCellValue(data, IMPORT_HEAD_EMAIL);
|
||||
String status = getCellValue(data, IMPORT_HEAD_STATUS);
|
||||
String roleKeys = getCellValue(data, IMPORT_HEAD_ROLE_KEYS);
|
||||
String positionCodes = getCellValue(data, IMPORT_HEAD_POSITION_CODES);
|
||||
String remark = getCellValue(data, IMPORT_HEAD_REMARK);
|
||||
if (!StringUtil.hasText(deptCode)
|
||||
&& !StringUtil.hasText(loginName)
|
||||
&& !StringUtil.hasText(nickname)
|
||||
&& !StringUtil.hasText(mobile)
|
||||
&& !StringUtil.hasText(email)
|
||||
&& !StringUtil.hasText(status)
|
||||
&& !StringUtil.hasText(roleKeys)
|
||||
&& !StringUtil.hasText(positionCodes)
|
||||
&& !StringUtil.hasText(remark)) {
|
||||
return;
|
||||
}
|
||||
if (rows.size() >= MAX_IMPORT_ROWS) {
|
||||
throw new BusinessException("单次最多导入5000个用户");
|
||||
}
|
||||
SysAccountImportRow row = new SysAccountImportRow();
|
||||
row.setRowNumber(sheetRowNo + 1);
|
||||
row.setDeptCode(deptCode);
|
||||
row.setLoginName(loginName);
|
||||
row.setNickname(nickname);
|
||||
row.setMobile(mobile);
|
||||
row.setEmail(email);
|
||||
row.setStatus(status);
|
||||
row.setRoleKeys(roleKeys);
|
||||
row.setPositionCodes(positionCodes);
|
||||
row.setRemark(remark);
|
||||
rows.add(row);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invokeHead(Map<Integer, ReadCellData<?>> headMap, AnalysisContext context) {
|
||||
for (Map.Entry<Integer, ReadCellData<?>> entry : headMap.entrySet()) {
|
||||
String headValue = entry.getValue() == null ? null : entry.getValue().getStringValue();
|
||||
String header = trimToNull(headValue);
|
||||
if (header != null) {
|
||||
headIndex.put(header, entry.getKey());
|
||||
}
|
||||
}
|
||||
List<String> requiredHeads = List.of(
|
||||
IMPORT_HEAD_DEPT_CODE,
|
||||
IMPORT_HEAD_LOGIN_NAME,
|
||||
IMPORT_HEAD_NICKNAME,
|
||||
IMPORT_HEAD_MOBILE,
|
||||
IMPORT_HEAD_EMAIL,
|
||||
IMPORT_HEAD_STATUS,
|
||||
IMPORT_HEAD_ROLE_KEYS,
|
||||
IMPORT_HEAD_POSITION_CODES,
|
||||
IMPORT_HEAD_REMARK
|
||||
);
|
||||
for (String requiredHead : requiredHeads) {
|
||||
if (!headIndex.containsKey(requiredHead)) {
|
||||
throw new BusinessException("导入模板表头不正确,必须包含:" + String.join("、", requiredHeads));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doAfterAllAnalysed(AnalysisContext context) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
public List<SysAccountImportRow> getRows() {
|
||||
return rows;
|
||||
}
|
||||
|
||||
private String getCellValue(Map<Integer, Object> row, String headName) {
|
||||
Integer index = headIndex.get(headName);
|
||||
if (index == null) {
|
||||
return null;
|
||||
}
|
||||
Object value = row.get(index);
|
||||
return value == null ? null : String.valueOf(value).trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package tech.easyflow.system.util;
|
||||
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 用户密码策略。
|
||||
*/
|
||||
public final class SysPasswordPolicy {
|
||||
|
||||
public static final String STRONG_PASSWORD_MESSAGE = "密码必须至少8位,且包含大写字母、小写字母、数字和特殊字符";
|
||||
|
||||
private static final Pattern STRONG_PASSWORD_PATTERN = Pattern.compile(
|
||||
"^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[^A-Za-z\\d]).{8,}$"
|
||||
);
|
||||
|
||||
private SysPasswordPolicy() {
|
||||
}
|
||||
|
||||
public static boolean isStrongPassword(String password) {
|
||||
return password != null && STRONG_PASSWORD_PATTERN.matcher(password).matches();
|
||||
}
|
||||
|
||||
public static void validateStrongPassword(String password) {
|
||||
if (!isStrongPassword(password)) {
|
||||
throw new BusinessException(STRONG_PASSWORD_MESSAGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package tech.easyflow.system.util;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
public class SysPasswordPolicyTest {
|
||||
|
||||
@Test
|
||||
public void shouldAcceptStrongPassword() {
|
||||
Assert.assertTrue(SysPasswordPolicy.isStrongPassword("Abcd1234!"));
|
||||
SysPasswordPolicy.validateStrongPassword("Abcd1234!");
|
||||
}
|
||||
|
||||
@Test(expected = BusinessException.class)
|
||||
public void shouldRejectWeakPassword() {
|
||||
Assert.assertFalse(SysPasswordPolicy.isStrongPassword("123456"));
|
||||
SysPasswordPolicy.validateStrongPassword("123456");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user