feat: 优化用户导入失败反馈与模板说明
- 支持按字段返回用户导入失败明细 - 补充导入模板填写说明与名称匹配规则 - 优化管理端导入失败弹窗与错误展示
This commit is contained in:
@@ -0,0 +1,67 @@
|
|||||||
|
package tech.easyflow.system.entity.vo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户导入失败明细。
|
||||||
|
*/
|
||||||
|
public class SysAccountImportErrorDetailVo {
|
||||||
|
|
||||||
|
private String fieldName;
|
||||||
|
|
||||||
|
private String fieldValue;
|
||||||
|
|
||||||
|
private String reason;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取问题字段名。
|
||||||
|
*
|
||||||
|
* @return 字段名
|
||||||
|
*/
|
||||||
|
public String getFieldName() {
|
||||||
|
return fieldName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置问题字段名。
|
||||||
|
*
|
||||||
|
* @param fieldName 字段名
|
||||||
|
*/
|
||||||
|
public void setFieldName(String fieldName) {
|
||||||
|
this.fieldName = fieldName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户填写值。
|
||||||
|
*
|
||||||
|
* @return 用户填写值
|
||||||
|
*/
|
||||||
|
public String getFieldValue() {
|
||||||
|
return fieldValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置用户填写值。
|
||||||
|
*
|
||||||
|
* @param fieldValue 用户填写值
|
||||||
|
*/
|
||||||
|
public void setFieldValue(String fieldValue) {
|
||||||
|
this.fieldValue = fieldValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取失败原因。
|
||||||
|
*
|
||||||
|
* @return 失败原因
|
||||||
|
*/
|
||||||
|
public String getReason() {
|
||||||
|
return reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置失败原因。
|
||||||
|
*
|
||||||
|
* @param reason 失败原因
|
||||||
|
*/
|
||||||
|
public void setReason(String reason) {
|
||||||
|
this.reason = reason;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
package tech.easyflow.system.entity.vo;
|
package tech.easyflow.system.entity.vo;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户导入失败行。
|
* 用户导入失败行。
|
||||||
*/
|
*/
|
||||||
@@ -7,41 +10,141 @@ public class SysAccountImportErrorRowVo {
|
|||||||
|
|
||||||
private Integer rowNumber;
|
private Integer rowNumber;
|
||||||
|
|
||||||
private String deptCode;
|
private String deptName;
|
||||||
|
|
||||||
private String loginName;
|
private String loginName;
|
||||||
|
|
||||||
private String reason;
|
private String nickname;
|
||||||
|
|
||||||
|
private String roleNames;
|
||||||
|
|
||||||
|
private String positionNames;
|
||||||
|
|
||||||
|
private List<SysAccountImportErrorDetailVo> details = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取行号。
|
||||||
|
*
|
||||||
|
* @return 行号
|
||||||
|
*/
|
||||||
public Integer getRowNumber() {
|
public Integer getRowNumber() {
|
||||||
return rowNumber;
|
return rowNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置行号。
|
||||||
|
*
|
||||||
|
* @param rowNumber 行号
|
||||||
|
*/
|
||||||
public void setRowNumber(Integer rowNumber) {
|
public void setRowNumber(Integer rowNumber) {
|
||||||
this.rowNumber = rowNumber;
|
this.rowNumber = rowNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getDeptCode() {
|
/**
|
||||||
return deptCode;
|
* 获取部门名称。
|
||||||
|
*
|
||||||
|
* @return 部门名称
|
||||||
|
*/
|
||||||
|
public String getDeptName() {
|
||||||
|
return deptName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDeptCode(String deptCode) {
|
/**
|
||||||
this.deptCode = deptCode;
|
* 设置部门名称。
|
||||||
|
*
|
||||||
|
* @param deptName 部门名称
|
||||||
|
*/
|
||||||
|
public void setDeptName(String deptName) {
|
||||||
|
this.deptName = deptName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取登录账号。
|
||||||
|
*
|
||||||
|
* @return 登录账号
|
||||||
|
*/
|
||||||
public String getLoginName() {
|
public String getLoginName() {
|
||||||
return loginName;
|
return loginName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置登录账号。
|
||||||
|
*
|
||||||
|
* @param loginName 登录账号
|
||||||
|
*/
|
||||||
public void setLoginName(String loginName) {
|
public void setLoginName(String loginName) {
|
||||||
this.loginName = loginName;
|
this.loginName = loginName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getReason() {
|
/**
|
||||||
return reason;
|
* 获取昵称。
|
||||||
|
*
|
||||||
|
* @return 昵称
|
||||||
|
*/
|
||||||
|
public String getNickname() {
|
||||||
|
return nickname;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setReason(String reason) {
|
/**
|
||||||
this.reason = reason;
|
* 设置昵称。
|
||||||
|
*
|
||||||
|
* @param nickname 昵称
|
||||||
|
*/
|
||||||
|
public void setNickname(String nickname) {
|
||||||
|
this.nickname = nickname;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取角色名称集合文本。
|
||||||
|
*
|
||||||
|
* @return 角色名称集合文本
|
||||||
|
*/
|
||||||
|
public String getRoleNames() {
|
||||||
|
return roleNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置角色名称集合文本。
|
||||||
|
*
|
||||||
|
* @param roleNames 角色名称集合文本
|
||||||
|
*/
|
||||||
|
public void setRoleNames(String roleNames) {
|
||||||
|
this.roleNames = roleNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取岗位名称集合文本。
|
||||||
|
*
|
||||||
|
* @return 岗位名称集合文本
|
||||||
|
*/
|
||||||
|
public String getPositionNames() {
|
||||||
|
return positionNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置岗位名称集合文本。
|
||||||
|
*
|
||||||
|
* @param positionNames 岗位名称集合文本
|
||||||
|
*/
|
||||||
|
public void setPositionNames(String positionNames) {
|
||||||
|
this.positionNames = positionNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取失败明细。
|
||||||
|
*
|
||||||
|
* @return 失败明细
|
||||||
|
*/
|
||||||
|
public List<SysAccountImportErrorDetailVo> getDetails() {
|
||||||
|
return details;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置失败明细。
|
||||||
|
*
|
||||||
|
* @param details 失败明细
|
||||||
|
*/
|
||||||
|
public void setDetails(List<SysAccountImportErrorDetailVo> details) {
|
||||||
|
this.details = details;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ 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.EasyExcel;
|
||||||
|
import cn.idev.excel.ExcelWriter;
|
||||||
import cn.idev.excel.FastExcel;
|
import cn.idev.excel.FastExcel;
|
||||||
import cn.idev.excel.context.AnalysisContext;
|
import cn.idev.excel.context.AnalysisContext;
|
||||||
import cn.idev.excel.metadata.data.ReadCellData;
|
import cn.idev.excel.metadata.data.ReadCellData;
|
||||||
import cn.idev.excel.read.listener.ReadListener;
|
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 com.mybatisflex.spring.service.impl.ServiceImpl;
|
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -28,6 +30,7 @@ import tech.easyflow.system.entity.SysPosition;
|
|||||||
import tech.easyflow.system.entity.SysRole;
|
import tech.easyflow.system.entity.SysRole;
|
||||||
import tech.easyflow.system.entity.vo.SysAccountBatchActionErrorItemVo;
|
import tech.easyflow.system.entity.vo.SysAccountBatchActionErrorItemVo;
|
||||||
import tech.easyflow.system.entity.vo.SysAccountBatchActionResultVo;
|
import tech.easyflow.system.entity.vo.SysAccountBatchActionResultVo;
|
||||||
|
import tech.easyflow.system.entity.vo.SysAccountImportErrorDetailVo;
|
||||||
import tech.easyflow.system.entity.vo.SysAccountImportErrorRowVo;
|
import tech.easyflow.system.entity.vo.SysAccountImportErrorRowVo;
|
||||||
import tech.easyflow.system.entity.vo.SysAccountImportResultVo;
|
import tech.easyflow.system.entity.vo.SysAccountImportResultVo;
|
||||||
import tech.easyflow.system.mapper.SysAccountMapper;
|
import tech.easyflow.system.mapper.SysAccountMapper;
|
||||||
@@ -53,6 +56,7 @@ 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.Set;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户表 服务层实现。
|
* 用户表 服务层实现。
|
||||||
@@ -69,15 +73,27 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
|||||||
private static final String DEFAULT_RESET_PASSWORD = "123456";
|
private static final String DEFAULT_RESET_PASSWORD = "123456";
|
||||||
private static final long MAX_IMPORT_FILE_SIZE_BYTES = 10L * 1024 * 1024;
|
private static final long MAX_IMPORT_FILE_SIZE_BYTES = 10L * 1024 * 1024;
|
||||||
private static final int MAX_IMPORT_ROWS = 5000;
|
private static final int MAX_IMPORT_ROWS = 5000;
|
||||||
private static final String IMPORT_HEAD_DEPT_CODE = "部门编码";
|
private static final String IMPORT_HEAD_DEPT_NAME = "部门名称*";
|
||||||
private static final String IMPORT_HEAD_LOGIN_NAME = "登录账号";
|
private static final String IMPORT_HEAD_LOGIN_NAME = "登录账号*";
|
||||||
private static final String IMPORT_HEAD_NICKNAME = "昵称";
|
private static final String IMPORT_HEAD_NICKNAME = "昵称*";
|
||||||
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_KEYS = "角色编码";
|
private static final String IMPORT_HEAD_ROLE_NAMES = "角色名称";
|
||||||
private static final String IMPORT_HEAD_POSITION_CODES = "岗位编码";
|
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_CONTENT = "内容";
|
||||||
|
private static final String IMPORT_FIELD_DEPT_NAME = "部门名称";
|
||||||
|
private static final String IMPORT_FIELD_LOGIN_NAME = "登录账号";
|
||||||
|
private static final String IMPORT_FIELD_NICKNAME = "昵称";
|
||||||
|
private static final String IMPORT_FIELD_MOBILE = "手机号";
|
||||||
|
private static final String IMPORT_FIELD_EMAIL = "邮箱";
|
||||||
|
private static final String IMPORT_FIELD_STATUS = "状态";
|
||||||
|
private static final String IMPORT_FIELD_ROLE_NAME = "角色名称";
|
||||||
|
private static final String IMPORT_FIELD_POSITION_NAME = "岗位名称";
|
||||||
|
private static final String IMPORT_FIELD_SYSTEM = "系统";
|
||||||
|
private static final String IMPORT_ERROR_DUPLICATED_RESOURCE = "存在重名,请先在管理端处理";
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private SysAccountRoleMapper sysAccountRoleMapper;
|
private SysAccountRoleMapper sysAccountRoleMapper;
|
||||||
@@ -244,16 +260,25 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, SysDept> deptMap = buildDeptCodeMap();
|
ImportNameLookup<SysDept> deptLookup = buildImportNameLookup(
|
||||||
Map<String, SysRole> roleMap = buildRoleKeyMap();
|
sysDeptMapper.selectListByQuery(QueryWrapper.create()),
|
||||||
Map<String, SysPosition> positionMap = buildPositionCodeMap();
|
SysDept::getDeptName
|
||||||
|
);
|
||||||
|
ImportNameLookup<SysRole> roleLookup = buildImportNameLookup(
|
||||||
|
sysRoleMapper.selectListByQuery(QueryWrapper.create()),
|
||||||
|
SysRole::getRoleName
|
||||||
|
);
|
||||||
|
ImportNameLookup<SysPosition> positionLookup = buildImportNameLookup(
|
||||||
|
sysPositionMapper.selectListByQuery(QueryWrapper.create()),
|
||||||
|
SysPosition::getPositionName
|
||||||
|
);
|
||||||
for (SysAccountImportRow row : rows) {
|
for (SysAccountImportRow row : rows) {
|
||||||
try {
|
try {
|
||||||
executeInRowTransaction(() -> importSingleRow(row, loginAccount, deptMap, roleMap, positionMap));
|
executeInRowTransaction(() -> importSingleRow(row, loginAccount, deptLookup, roleLookup, positionLookup));
|
||||||
result.setSuccessCount(result.getSuccessCount() + 1);
|
result.setSuccessCount(result.getSuccessCount() + 1);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
result.setErrorCount(result.getErrorCount() + 1);
|
result.setErrorCount(result.getErrorCount() + 1);
|
||||||
appendImportError(result, row, extractImportErrorMessage(e));
|
appendImportError(result, row, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
@@ -261,10 +286,20 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void writeImportTemplate(OutputStream outputStream) {
|
public void writeImportTemplate(OutputStream outputStream) {
|
||||||
EasyExcel.write(outputStream)
|
ExcelWriter excelWriter = EasyExcel.write(outputStream).build();
|
||||||
.head(buildImportHeadList())
|
try {
|
||||||
.sheet("模板")
|
WriteSheet templateSheet = EasyExcel.writerSheet("模板")
|
||||||
.doWrite(new ArrayList<>());
|
.head(buildImportHeadList())
|
||||||
|
.build();
|
||||||
|
excelWriter.write(new ArrayList<>(), templateSheet);
|
||||||
|
|
||||||
|
WriteSheet guideSheet = EasyExcel.writerSheet("填写说明")
|
||||||
|
.head(buildImportGuideHeadList())
|
||||||
|
.build();
|
||||||
|
excelWriter.write(buildImportGuideRows(), guideSheet);
|
||||||
|
} finally {
|
||||||
|
excelWriter.finish();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void executeInRowTransaction(Runnable action) {
|
private void executeInRowTransaction(Runnable action) {
|
||||||
@@ -281,29 +316,54 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
|||||||
|
|
||||||
private void importSingleRow(SysAccountImportRow row,
|
private void importSingleRow(SysAccountImportRow row,
|
||||||
LoginAccount loginAccount,
|
LoginAccount loginAccount,
|
||||||
Map<String, SysDept> deptMap,
|
ImportNameLookup<SysDept> deptLookup,
|
||||||
Map<String, SysRole> roleMap,
|
ImportNameLookup<SysRole> roleLookup,
|
||||||
Map<String, SysPosition> positionMap) {
|
ImportNameLookup<SysPosition> positionLookup) {
|
||||||
String deptCode = trimToNull(row.getDeptCode());
|
List<SysAccountImportErrorDetailVo> details = new ArrayList<>();
|
||||||
|
String deptName = trimToNull(row.getDeptName());
|
||||||
String loginName = trimToNull(row.getLoginName());
|
String loginName = trimToNull(row.getLoginName());
|
||||||
String nickname = trimToNull(row.getNickname());
|
String nickname = trimToNull(row.getNickname());
|
||||||
if (deptCode == null) {
|
String mobile = trimToNull(row.getMobile());
|
||||||
throw new BusinessException("部门编码不能为空");
|
String email = trimToNull(row.getEmail());
|
||||||
|
|
||||||
|
if (deptName == null) {
|
||||||
|
addImportDetail(details, IMPORT_FIELD_DEPT_NAME, row.getDeptName(), "部门名称不能为空");
|
||||||
}
|
}
|
||||||
if (loginName == null) {
|
if (loginName == null) {
|
||||||
throw new BusinessException("登录账号不能为空");
|
addImportDetail(details, IMPORT_FIELD_LOGIN_NAME, row.getLoginName(), "登录账号不能为空");
|
||||||
|
} else if (isLoginNameExists(loginName)) {
|
||||||
|
addImportDetail(details, IMPORT_FIELD_LOGIN_NAME, row.getLoginName(), "登录账号已存在");
|
||||||
}
|
}
|
||||||
if (nickname == null) {
|
if (nickname == null) {
|
||||||
throw new BusinessException("昵称不能为空");
|
addImportDetail(details, IMPORT_FIELD_NICKNAME, row.getNickname(), "昵称不能为空");
|
||||||
}
|
}
|
||||||
SysDept dept = deptMap.get(deptCode);
|
if (mobile != null && !StringUtil.isMobileNumber(mobile)) {
|
||||||
if (dept == null) {
|
addImportDetail(details, IMPORT_FIELD_MOBILE, row.getMobile(), "手机号格式不正确");
|
||||||
throw new BusinessException("部门编码不存在: " + deptCode);
|
}
|
||||||
|
if (email != null && !StringUtil.isEmail(email)) {
|
||||||
|
addImportDetail(details, IMPORT_FIELD_EMAIL, row.getEmail(), "邮箱格式不正确");
|
||||||
}
|
}
|
||||||
ensureLoginNameNotExists(loginName);
|
|
||||||
|
|
||||||
List<BigInteger> roleIds = resolveRoleIds(row.getRoleKeys(), roleMap);
|
SysDept dept = resolveSingleResource(deptName, deptLookup, IMPORT_FIELD_DEPT_NAME, details);
|
||||||
List<BigInteger> positionIds = resolvePositionIds(row.getPositionCodes(), positionMap);
|
Integer status = parseStatus(row.getStatus(), details);
|
||||||
|
List<BigInteger> roleIds = resolveResourceIds(
|
||||||
|
row.getRoleNames(),
|
||||||
|
roleLookup,
|
||||||
|
SysRole::getId,
|
||||||
|
IMPORT_FIELD_ROLE_NAME,
|
||||||
|
details
|
||||||
|
);
|
||||||
|
List<BigInteger> positionIds = resolveResourceIds(
|
||||||
|
row.getPositionNames(),
|
||||||
|
positionLookup,
|
||||||
|
SysPosition::getId,
|
||||||
|
IMPORT_FIELD_POSITION_NAME,
|
||||||
|
details
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!details.isEmpty()) {
|
||||||
|
throw new ImportRowValidationException(details);
|
||||||
|
}
|
||||||
|
|
||||||
SysAccount entity = new SysAccount();
|
SysAccount entity = new SysAccount();
|
||||||
entity.setDeptId(dept.getId());
|
entity.setDeptId(dept.getId());
|
||||||
@@ -313,9 +373,9 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
|||||||
entity.setPasswordResetRequired(true);
|
entity.setPasswordResetRequired(true);
|
||||||
entity.setAccountType(EnumAccountType.NORMAL.getCode());
|
entity.setAccountType(EnumAccountType.NORMAL.getCode());
|
||||||
entity.setNickname(nickname);
|
entity.setNickname(nickname);
|
||||||
entity.setMobile(trimToNull(row.getMobile()));
|
entity.setMobile(mobile);
|
||||||
entity.setEmail(trimToNull(row.getEmail()));
|
entity.setEmail(email);
|
||||||
entity.setStatus(parseStatus(row.getStatus()));
|
entity.setStatus(status);
|
||||||
entity.setRemark(trimToNull(row.getRemark()));
|
entity.setRemark(trimToNull(row.getRemark()));
|
||||||
entity.setCreated(new Date());
|
entity.setCreated(new Date());
|
||||||
entity.setCreatedBy(loginAccount.getId());
|
entity.setCreatedBy(loginAccount.getId());
|
||||||
@@ -327,15 +387,26 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
|||||||
syncRelations(entity);
|
syncRelations(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ensureLoginNameNotExists(String loginName) {
|
/**
|
||||||
|
* 校验登录账号是否已存在。
|
||||||
|
*
|
||||||
|
* @param loginName 登录账号
|
||||||
|
* @return 是否已存在
|
||||||
|
*/
|
||||||
|
private boolean isLoginNameExists(String loginName) {
|
||||||
QueryWrapper wrapper = QueryWrapper.create();
|
QueryWrapper wrapper = QueryWrapper.create();
|
||||||
wrapper.eq(SysAccount::getLoginName, loginName);
|
wrapper.eq(SysAccount::getLoginName, loginName);
|
||||||
if (count(wrapper) > 0) {
|
return count(wrapper) > 0;
|
||||||
throw new BusinessException("登录账号已存在: " + loginName);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Integer parseStatus(String rawStatus) {
|
/**
|
||||||
|
* 解析导入状态列。
|
||||||
|
*
|
||||||
|
* @param rawStatus 原始状态文本
|
||||||
|
* @param details 错误明细收集器
|
||||||
|
* @return 解析后的状态编码
|
||||||
|
*/
|
||||||
|
private Integer parseStatus(String rawStatus, List<SysAccountImportErrorDetailVo> details) {
|
||||||
String status = trimToNull(rawStatus);
|
String status = trimToNull(rawStatus);
|
||||||
if (status == null) {
|
if (status == null) {
|
||||||
return EnumDataStatus.AVAILABLE.getCode();
|
return EnumDataStatus.AVAILABLE.getCode();
|
||||||
@@ -346,39 +417,38 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
|||||||
if ("0".equals(status) || "未启用".equals(status) || "停用".equals(status) || "禁用".equals(status)) {
|
if ("0".equals(status) || "未启用".equals(status) || "停用".equals(status) || "禁用".equals(status)) {
|
||||||
return EnumDataStatus.UNAVAILABLE.getCode();
|
return EnumDataStatus.UNAVAILABLE.getCode();
|
||||||
}
|
}
|
||||||
throw new BusinessException("状态不合法,仅支持 1/0 或 已启用/未启用");
|
addImportDetail(details, IMPORT_FIELD_STATUS, rawStatus, "状态不合法,仅支持 1/0/已启用/启用/未启用/停用/禁用");
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<BigInteger> resolveRoleIds(String roleKeysText, Map<String, SysRole> roleMap) {
|
/**
|
||||||
List<String> roleKeys = splitCodes(roleKeysText);
|
* 解析名称列表并映射为主键集合。
|
||||||
if (roleKeys.isEmpty()) {
|
*
|
||||||
|
* @param rawNames 原始名称文本
|
||||||
|
* @param lookup 名称查找表
|
||||||
|
* @param fieldName 字段名称
|
||||||
|
* @param details 错误明细收集器
|
||||||
|
* @param <T> 资源类型
|
||||||
|
* @return 匹配到的主键集合
|
||||||
|
*/
|
||||||
|
private <T> List<BigInteger> resolveResourceIds(
|
||||||
|
String rawNames,
|
||||||
|
ImportNameLookup<T> lookup,
|
||||||
|
Function<T, BigInteger> idExtractor,
|
||||||
|
String fieldName,
|
||||||
|
List<SysAccountImportErrorDetailVo> details) {
|
||||||
|
List<String> names = splitCodes(rawNames);
|
||||||
|
if (names.isEmpty()) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
List<BigInteger> roleIds = new ArrayList<>(roleKeys.size());
|
List<BigInteger> ids = new ArrayList<>(names.size());
|
||||||
for (String roleKey : roleKeys) {
|
for (String name : names) {
|
||||||
SysRole role = roleMap.get(roleKey);
|
T resource = resolveSingleResource(name, lookup, fieldName, details);
|
||||||
if (role == null) {
|
if (resource != null) {
|
||||||
throw new BusinessException("角色编码不存在: " + roleKey);
|
ids.add(idExtractor.apply(resource));
|
||||||
}
|
}
|
||||||
roleIds.add(role.getId());
|
|
||||||
}
|
}
|
||||||
return roleIds;
|
return ids;
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
private List<String> splitCodes(String rawCodes) {
|
||||||
@@ -398,40 +468,32 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, SysDept> buildDeptCodeMap() {
|
/**
|
||||||
List<SysDept> deptList = sysDeptMapper.selectListByQuery(QueryWrapper.create());
|
* 构建导入名称查找表,并标记重名数据。
|
||||||
Map<String, SysDept> deptMap = new HashMap<>();
|
*
|
||||||
for (SysDept dept : deptList) {
|
* @param resources 资源集合
|
||||||
String deptCode = trimToNull(dept.getDeptCode());
|
* @param nameExtractor 名称提取器
|
||||||
if (deptCode != null) {
|
* @param <T> 资源类型
|
||||||
deptMap.putIfAbsent(deptCode, dept);
|
* @return 名称查找表
|
||||||
|
*/
|
||||||
|
private <T> ImportNameLookup<T> buildImportNameLookup(List<T> resources, Function<T, String> nameExtractor) {
|
||||||
|
Map<String, T> uniqueMap = new HashMap<>();
|
||||||
|
Set<String> duplicateNames = new LinkedHashSet<>();
|
||||||
|
for (T resource : resources) {
|
||||||
|
String name = trimToNull(nameExtractor.apply(resource));
|
||||||
|
if (name == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (uniqueMap.containsKey(name)) {
|
||||||
|
duplicateNames.add(name);
|
||||||
|
uniqueMap.remove(name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!duplicateNames.contains(name)) {
|
||||||
|
uniqueMap.put(name, resource);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return deptMap;
|
return new ImportNameLookup<>(uniqueMap, duplicateNames);
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
private List<SysAccountImportRow> parseImportRows(MultipartFile file) {
|
||||||
@@ -462,12 +524,21 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void appendImportError(SysAccountImportResultVo result, SysAccountImportRow row, String reason) {
|
private void appendImportError(SysAccountImportResultVo result, SysAccountImportRow row, Exception exception) {
|
||||||
SysAccountImportErrorRowVo errorRow = new SysAccountImportErrorRowVo();
|
SysAccountImportErrorRowVo errorRow = new SysAccountImportErrorRowVo();
|
||||||
errorRow.setRowNumber(row.getRowNumber());
|
errorRow.setRowNumber(row.getRowNumber());
|
||||||
errorRow.setDeptCode(row.getDeptCode());
|
errorRow.setDeptName(row.getDeptName());
|
||||||
errorRow.setLoginName(row.getLoginName());
|
errorRow.setLoginName(row.getLoginName());
|
||||||
errorRow.setReason(reason);
|
errorRow.setNickname(row.getNickname());
|
||||||
|
errorRow.setRoleNames(row.getRoleNames());
|
||||||
|
errorRow.setPositionNames(row.getPositionNames());
|
||||||
|
if (exception instanceof ImportRowValidationException validationException) {
|
||||||
|
errorRow.setDetails(validationException.getDetails());
|
||||||
|
} else {
|
||||||
|
List<SysAccountImportErrorDetailVo> details = new ArrayList<>(1);
|
||||||
|
addImportDetail(details, IMPORT_FIELD_SYSTEM, null, extractImportErrorMessage(exception));
|
||||||
|
errorRow.setDetails(details);
|
||||||
|
}
|
||||||
result.getErrorRows().add(errorRow);
|
result.getErrorRows().add(errorRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -537,18 +608,69 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
|||||||
|
|
||||||
private List<List<String>> buildImportHeadList() {
|
private List<List<String>> buildImportHeadList() {
|
||||||
List<List<String>> headList = new ArrayList<>(9);
|
List<List<String>> headList = new ArrayList<>(9);
|
||||||
headList.add(Collections.singletonList(IMPORT_HEAD_DEPT_CODE));
|
headList.add(Collections.singletonList(IMPORT_HEAD_DEPT_NAME));
|
||||||
headList.add(Collections.singletonList(IMPORT_HEAD_LOGIN_NAME));
|
headList.add(Collections.singletonList(IMPORT_HEAD_LOGIN_NAME));
|
||||||
headList.add(Collections.singletonList(IMPORT_HEAD_NICKNAME));
|
headList.add(Collections.singletonList(IMPORT_HEAD_NICKNAME));
|
||||||
headList.add(Collections.singletonList(IMPORT_HEAD_MOBILE));
|
headList.add(Collections.singletonList(IMPORT_HEAD_MOBILE));
|
||||||
headList.add(Collections.singletonList(IMPORT_HEAD_EMAIL));
|
headList.add(Collections.singletonList(IMPORT_HEAD_EMAIL));
|
||||||
headList.add(Collections.singletonList(IMPORT_HEAD_STATUS));
|
headList.add(Collections.singletonList(IMPORT_HEAD_STATUS));
|
||||||
headList.add(Collections.singletonList(IMPORT_HEAD_ROLE_KEYS));
|
headList.add(Collections.singletonList(IMPORT_HEAD_ROLE_NAMES));
|
||||||
headList.add(Collections.singletonList(IMPORT_HEAD_POSITION_CODES));
|
headList.add(Collections.singletonList(IMPORT_HEAD_POSITION_NAMES));
|
||||||
headList.add(Collections.singletonList(IMPORT_HEAD_REMARK));
|
headList.add(Collections.singletonList(IMPORT_HEAD_REMARK));
|
||||||
return headList;
|
return headList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<List<String>> buildImportGuideHeadList() {
|
||||||
|
List<List<String>> headList = new ArrayList<>(2);
|
||||||
|
headList.add(Collections.singletonList(IMPORT_GUIDE_HEAD_ITEM));
|
||||||
|
headList.add(Collections.singletonList(IMPORT_GUIDE_HEAD_CONTENT));
|
||||||
|
return headList;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<List<String>> buildImportGuideRows() {
|
||||||
|
List<List<String>> rows = new ArrayList<>();
|
||||||
|
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("导入后初始密码", "导入成功的账号默认密码为 123456,首次登录需要修改密码"));
|
||||||
|
rows.add(List.of("示例行", "市场部 | zhangsan | 张三 | 13800138000 | zhangsan@example.com | 已启用 | 普通员工,审批专员 | 产品经理 | 示例导入"));
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addImportDetail(List<SysAccountImportErrorDetailVo> details,
|
||||||
|
String fieldName,
|
||||||
|
String fieldValue,
|
||||||
|
String reason) {
|
||||||
|
SysAccountImportErrorDetailVo detail = new SysAccountImportErrorDetailVo();
|
||||||
|
detail.setFieldName(fieldName);
|
||||||
|
detail.setFieldValue(trimToNull(fieldValue));
|
||||||
|
detail.setReason(reason);
|
||||||
|
details.add(detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> T resolveSingleResource(
|
||||||
|
String rawName,
|
||||||
|
ImportNameLookup<T> lookup,
|
||||||
|
String fieldName,
|
||||||
|
List<SysAccountImportErrorDetailVo> details) {
|
||||||
|
String name = trimToNull(rawName);
|
||||||
|
if (name == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (lookup.getDuplicateNames().contains(name)) {
|
||||||
|
addImportDetail(details, fieldName, rawName, fieldName + IMPORT_ERROR_DUPLICATED_RESOURCE);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
T resource = lookup.getUniqueMap().get(name);
|
||||||
|
if (resource == null) {
|
||||||
|
addImportDetail(details, fieldName, rawName, fieldName + "不存在");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return resource;
|
||||||
|
}
|
||||||
|
|
||||||
private String trimToNull(String value) {
|
private String trimToNull(String value) {
|
||||||
if (!StringUtil.hasText(value)) {
|
if (!StringUtil.hasText(value)) {
|
||||||
return null;
|
return null;
|
||||||
@@ -558,14 +680,14 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
|||||||
|
|
||||||
private static class SysAccountImportRow {
|
private static class SysAccountImportRow {
|
||||||
private Integer rowNumber;
|
private Integer rowNumber;
|
||||||
private String deptCode;
|
private String deptName;
|
||||||
private String loginName;
|
private String loginName;
|
||||||
private String nickname;
|
private String nickname;
|
||||||
private String mobile;
|
private String mobile;
|
||||||
private String email;
|
private String email;
|
||||||
private String status;
|
private String status;
|
||||||
private String roleKeys;
|
private String roleNames;
|
||||||
private String positionCodes;
|
private String positionNames;
|
||||||
private String remark;
|
private String remark;
|
||||||
|
|
||||||
public Integer getRowNumber() {
|
public Integer getRowNumber() {
|
||||||
@@ -576,12 +698,12 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
|||||||
this.rowNumber = rowNumber;
|
this.rowNumber = rowNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getDeptCode() {
|
public String getDeptName() {
|
||||||
return deptCode;
|
return deptName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDeptCode(String deptCode) {
|
public void setDeptName(String deptName) {
|
||||||
this.deptCode = deptCode;
|
this.deptName = deptName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getLoginName() {
|
public String getLoginName() {
|
||||||
@@ -624,20 +746,20 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
|||||||
this.status = status;
|
this.status = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getRoleKeys() {
|
public String getRoleNames() {
|
||||||
return roleKeys;
|
return roleNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setRoleKeys(String roleKeys) {
|
public void setRoleNames(String roleNames) {
|
||||||
this.roleKeys = roleKeys;
|
this.roleNames = roleNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getPositionCodes() {
|
public String getPositionNames() {
|
||||||
return positionCodes;
|
return positionNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setPositionCodes(String positionCodes) {
|
public void setPositionNames(String positionNames) {
|
||||||
this.positionCodes = positionCodes;
|
this.positionNames = positionNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getRemark() {
|
public String getRemark() {
|
||||||
@@ -658,23 +780,23 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
|||||||
@Override
|
@Override
|
||||||
public void invoke(LinkedHashMap<Integer, Object> data, AnalysisContext context) {
|
public void invoke(LinkedHashMap<Integer, Object> data, AnalysisContext context) {
|
||||||
sheetRowNo++;
|
sheetRowNo++;
|
||||||
String deptCode = getCellValue(data, IMPORT_HEAD_DEPT_CODE);
|
String deptName = getCellValue(data, IMPORT_HEAD_DEPT_NAME);
|
||||||
String loginName = getCellValue(data, IMPORT_HEAD_LOGIN_NAME);
|
String loginName = getCellValue(data, IMPORT_HEAD_LOGIN_NAME);
|
||||||
String nickname = getCellValue(data, IMPORT_HEAD_NICKNAME);
|
String nickname = getCellValue(data, IMPORT_HEAD_NICKNAME);
|
||||||
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 roleKeys = getCellValue(data, IMPORT_HEAD_ROLE_KEYS);
|
String roleNames = getCellValue(data, IMPORT_HEAD_ROLE_NAMES);
|
||||||
String positionCodes = getCellValue(data, IMPORT_HEAD_POSITION_CODES);
|
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(deptCode)
|
if (!StringUtil.hasText(deptName)
|
||||||
&& !StringUtil.hasText(loginName)
|
&& !StringUtil.hasText(loginName)
|
||||||
&& !StringUtil.hasText(nickname)
|
&& !StringUtil.hasText(nickname)
|
||||||
&& !StringUtil.hasText(mobile)
|
&& !StringUtil.hasText(mobile)
|
||||||
&& !StringUtil.hasText(email)
|
&& !StringUtil.hasText(email)
|
||||||
&& !StringUtil.hasText(status)
|
&& !StringUtil.hasText(status)
|
||||||
&& !StringUtil.hasText(roleKeys)
|
&& !StringUtil.hasText(roleNames)
|
||||||
&& !StringUtil.hasText(positionCodes)
|
&& !StringUtil.hasText(positionNames)
|
||||||
&& !StringUtil.hasText(remark)) {
|
&& !StringUtil.hasText(remark)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -683,14 +805,14 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
|||||||
}
|
}
|
||||||
SysAccountImportRow row = new SysAccountImportRow();
|
SysAccountImportRow row = new SysAccountImportRow();
|
||||||
row.setRowNumber(sheetRowNo + 1);
|
row.setRowNumber(sheetRowNo + 1);
|
||||||
row.setDeptCode(deptCode);
|
row.setDeptName(deptName);
|
||||||
row.setLoginName(loginName);
|
row.setLoginName(loginName);
|
||||||
row.setNickname(nickname);
|
row.setNickname(nickname);
|
||||||
row.setMobile(mobile);
|
row.setMobile(mobile);
|
||||||
row.setEmail(email);
|
row.setEmail(email);
|
||||||
row.setStatus(status);
|
row.setStatus(status);
|
||||||
row.setRoleKeys(roleKeys);
|
row.setRoleNames(roleNames);
|
||||||
row.setPositionCodes(positionCodes);
|
row.setPositionNames(positionNames);
|
||||||
row.setRemark(remark);
|
row.setRemark(remark);
|
||||||
rows.add(row);
|
rows.add(row);
|
||||||
}
|
}
|
||||||
@@ -705,15 +827,9 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
List<String> requiredHeads = List.of(
|
List<String> requiredHeads = List.of(
|
||||||
IMPORT_HEAD_DEPT_CODE,
|
IMPORT_HEAD_DEPT_NAME,
|
||||||
IMPORT_HEAD_LOGIN_NAME,
|
IMPORT_HEAD_LOGIN_NAME,
|
||||||
IMPORT_HEAD_NICKNAME,
|
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) {
|
for (String requiredHead : requiredHeads) {
|
||||||
if (!headIndex.containsKey(requiredHead)) {
|
if (!headIndex.containsKey(requiredHead)) {
|
||||||
@@ -740,4 +856,35 @@ public class SysAccountServiceImpl extends ServiceImpl<SysAccountMapper, SysAcco
|
|||||||
return value == null ? null : String.valueOf(value).trim();
|
return value == null ? null : String.valueOf(value).trim();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class ImportNameLookup<T> {
|
||||||
|
private final Map<String, T> uniqueMap;
|
||||||
|
private final Set<String> duplicateNames;
|
||||||
|
|
||||||
|
private ImportNameLookup(Map<String, T> uniqueMap, Set<String> duplicateNames) {
|
||||||
|
this.uniqueMap = uniqueMap;
|
||||||
|
this.duplicateNames = duplicateNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, T> getUniqueMap() {
|
||||||
|
return uniqueMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<String> getDuplicateNames() {
|
||||||
|
return duplicateNames;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ImportRowValidationException extends RuntimeException {
|
||||||
|
private final List<SysAccountImportErrorDetailVo> details;
|
||||||
|
|
||||||
|
private ImportRowValidationException(List<SysAccountImportErrorDetailVo> details) {
|
||||||
|
super("导入数据校验失败");
|
||||||
|
this.details = details;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<SysAccountImportErrorDetailVo> getDetails() {
|
||||||
|
return details;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,15 +45,23 @@
|
|||||||
"batchResetPasswordAllFailed": "Batch password reset failed",
|
"batchResetPasswordAllFailed": "Batch password reset failed",
|
||||||
"importTitle": "Import Users",
|
"importTitle": "Import Users",
|
||||||
"importUploadTitle": "Drag the Excel file here, or click to select a file",
|
"importUploadTitle": "Drag the Excel file here, or click to select a file",
|
||||||
"importUploadDesc": "Only .xlsx / .xls files are supported. Import only creates users and duplicate accounts will fail.",
|
"importUploadDesc": "Only .xlsx / .xls files are supported. Fill department, role, and position by name. Headers marked with * are required.",
|
||||||
"importSelectFileRequired": "Please select a file to import",
|
"importSelectFileRequired": "Please select a file to import",
|
||||||
"downloadTemplate": "Download Template",
|
"downloadTemplate": "Download Template",
|
||||||
"importFinished": "User import completed",
|
"importFinished": "User import completed",
|
||||||
|
"importPartialSuccess": "Import completed. {successCount} succeeded and {errorCount} failed. See the details below.",
|
||||||
|
"importAllFailed": "Import failed. See the details below.",
|
||||||
"importResultTitle": "Import Result",
|
"importResultTitle": "Import Result",
|
||||||
"importTotalCount": "Total",
|
"importTotalCount": "Total",
|
||||||
"importSuccessCount": "Success",
|
"importSuccessCount": "Success",
|
||||||
"importErrorCount": "Failed",
|
"importErrorCount": "Failed",
|
||||||
"importRowNumber": "Row",
|
"importRowNumber": "Row",
|
||||||
"importDeptCode": "Dept Code",
|
"importDeptCode": "Dept Code",
|
||||||
"importReason": "Reason"
|
"importFieldName": "Field",
|
||||||
|
"importFieldValue": "Value",
|
||||||
|
"importReason": "Reason",
|
||||||
|
"importGuideTitle": "Instructions",
|
||||||
|
"importGuideNameRule": "Enter department, role, and position by name.",
|
||||||
|
"importGuideRequired": "Only department name, login name, and nickname are required. Position, mobile, and email are optional.",
|
||||||
|
"importGuideMultiValue": "Role and position accept multiple names separated by commas."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,15 +46,23 @@
|
|||||||
"batchResetPasswordAllFailed": "批量重置密码失败",
|
"batchResetPasswordAllFailed": "批量重置密码失败",
|
||||||
"importTitle": "导入用户",
|
"importTitle": "导入用户",
|
||||||
"importUploadTitle": "拖拽 Excel 文件到此处,或点击选择文件",
|
"importUploadTitle": "拖拽 Excel 文件到此处,或点击选择文件",
|
||||||
"importUploadDesc": "仅支持 .xlsx / .xls,导入只新增用户,重复账号会报错",
|
"importUploadDesc": "仅支持 .xlsx / .xls。请按名称填写部门、角色、岗位,模板中的 * 为必填项。",
|
||||||
"importSelectFileRequired": "请先选择要导入的文件",
|
"importSelectFileRequired": "请先选择要导入的文件",
|
||||||
"downloadTemplate": "下载导入模板",
|
"downloadTemplate": "下载导入模板",
|
||||||
"importFinished": "用户导入完成",
|
"importFinished": "用户导入完成",
|
||||||
|
"importPartialSuccess": "导入完成,成功 {successCount} 条,失败 {errorCount} 条,请查看下方明细",
|
||||||
|
"importAllFailed": "导入失败,请查看下方明细",
|
||||||
"importResultTitle": "导入结果",
|
"importResultTitle": "导入结果",
|
||||||
"importTotalCount": "总条数",
|
"importTotalCount": "总条数",
|
||||||
"importSuccessCount": "成功数",
|
"importSuccessCount": "成功数",
|
||||||
"importErrorCount": "失败数",
|
"importErrorCount": "失败数",
|
||||||
"importRowNumber": "行号",
|
"importRowNumber": "行号",
|
||||||
"importDeptCode": "部门编码",
|
"importDeptCode": "部门编码",
|
||||||
"importReason": "失败原因"
|
"importFieldName": "问题字段",
|
||||||
|
"importFieldValue": "填写内容",
|
||||||
|
"importReason": "失败原因",
|
||||||
|
"importGuideTitle": "填写说明",
|
||||||
|
"importGuideNameRule": "部门、角色、岗位都填写名称。",
|
||||||
|
"importGuideRequired": "仅部门名称、登录账号、昵称为必填;岗位、手机号、邮箱为非必填。",
|
||||||
|
"importGuideMultiValue": "角色和岗位支持多个名称,使用英文逗号或中文逗号分隔。"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,29 @@ import {
|
|||||||
import { api } from '#/api/request';
|
import { api } from '#/api/request';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
interface ImportErrorDetail {
|
||||||
|
fieldName?: string;
|
||||||
|
fieldValue?: null | string;
|
||||||
|
reason?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportErrorRow {
|
||||||
|
rowNumber?: number;
|
||||||
|
deptName?: string;
|
||||||
|
loginName?: string;
|
||||||
|
nickname?: string;
|
||||||
|
roleNames?: string;
|
||||||
|
positionNames?: string;
|
||||||
|
details?: ImportErrorDetail[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportResult {
|
||||||
|
totalCount?: number;
|
||||||
|
successCount?: number;
|
||||||
|
errorCount?: number;
|
||||||
|
errorRows?: ImportErrorRow[];
|
||||||
|
}
|
||||||
|
|
||||||
const emit = defineEmits(['reload']);
|
const emit = defineEmits(['reload']);
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
@@ -37,10 +60,22 @@ const fileList = ref<any[]>([]);
|
|||||||
const currentFile = ref<File | null>(null);
|
const currentFile = ref<File | null>(null);
|
||||||
const submitLoading = ref(false);
|
const submitLoading = ref(false);
|
||||||
const downloadLoading = ref(false);
|
const downloadLoading = ref(false);
|
||||||
const importResult = ref<any>(null);
|
const importResult = ref<ImportResult | null>(null);
|
||||||
|
|
||||||
const hasErrors = computed(() => (importResult.value?.errorCount || 0) > 0);
|
const hasErrors = computed(() => (importResult.value?.errorCount || 0) > 0);
|
||||||
|
const hasSuccess = computed(() => (importResult.value?.successCount || 0) > 0);
|
||||||
const selectedFileName = computed(() => currentFile.value?.name || '');
|
const selectedFileName = computed(() => currentFile.value?.name || '');
|
||||||
|
const errorDetailRows = computed(() => {
|
||||||
|
return (importResult.value?.errorRows || []).flatMap((row) =>
|
||||||
|
(row.details || []).map((detail) => ({
|
||||||
|
rowNumber: row.rowNumber,
|
||||||
|
loginName: row.loginName,
|
||||||
|
fieldName: detail.fieldName,
|
||||||
|
fieldValue: detail.fieldValue,
|
||||||
|
reason: detail.reason,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
function openDialog() {
|
function openDialog() {
|
||||||
dialogVisible.value = true;
|
dialogVisible.value = true;
|
||||||
@@ -102,8 +137,20 @@ async function handleImport() {
|
|||||||
});
|
});
|
||||||
if (res.errorCode === 0) {
|
if (res.errorCode === 0) {
|
||||||
importResult.value = res.data;
|
importResult.value = res.data;
|
||||||
ElMessage.success($t('sysAccount.importFinished'));
|
const successCount = res.data?.successCount || 0;
|
||||||
emit('reload');
|
const errorCount = res.data?.errorCount || 0;
|
||||||
|
if (errorCount === 0) {
|
||||||
|
ElMessage.success($t('sysAccount.importFinished'));
|
||||||
|
} else if (successCount === 0) {
|
||||||
|
ElMessage.error($t('sysAccount.importAllFailed'));
|
||||||
|
} else {
|
||||||
|
ElMessage.warning(
|
||||||
|
$t('sysAccount.importPartialSuccess', { successCount, errorCount }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (successCount > 0) {
|
||||||
|
emit('reload');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
submitLoading.value = false;
|
submitLoading.value = false;
|
||||||
@@ -149,6 +196,15 @@ async function handleImport() {
|
|||||||
</div>
|
</div>
|
||||||
</ElUpload>
|
</ElUpload>
|
||||||
|
|
||||||
|
<div class="import-guide-card">
|
||||||
|
<div class="guide-title">{{ $t('sysAccount.importGuideTitle') }}</div>
|
||||||
|
<div class="guide-text">{{ $t('sysAccount.importGuideNameRule') }}</div>
|
||||||
|
<div class="guide-text">{{ $t('sysAccount.importGuideRequired') }}</div>
|
||||||
|
<div class="guide-text">
|
||||||
|
{{ $t('sysAccount.importGuideMultiValue') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="selectedFileName" class="selected-file-card">
|
<div v-if="selectedFileName" class="selected-file-card">
|
||||||
<div class="selected-file-main">
|
<div class="selected-file-main">
|
||||||
<ElIcon class="selected-file-icon"><Document /></ElIcon>
|
<ElIcon class="selected-file-icon"><Document /></ElIcon>
|
||||||
@@ -183,6 +239,22 @@ async function handleImport() {
|
|||||||
{{ $t('sysAccount.importResultTitle') }}
|
{{ $t('sysAccount.importResultTitle') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="result-summary">
|
||||||
|
<span v-if="hasErrors && hasSuccess">
|
||||||
|
{{
|
||||||
|
$t('sysAccount.importPartialSuccess', {
|
||||||
|
successCount: importResult?.successCount || 0,
|
||||||
|
errorCount: importResult?.errorCount || 0,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
<span v-else-if="hasErrors">
|
||||||
|
{{ $t('sysAccount.importAllFailed') }}
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
{{ $t('sysAccount.importFinished') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="result-stats">
|
<div class="result-stats">
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
@@ -211,7 +283,7 @@ async function handleImport() {
|
|||||||
|
|
||||||
<ElTable
|
<ElTable
|
||||||
v-if="hasErrors"
|
v-if="hasErrors"
|
||||||
:data="importResult.errorRows || []"
|
:data="errorDetailRows"
|
||||||
size="small"
|
size="small"
|
||||||
class="result-error-table"
|
class="result-error-table"
|
||||||
>
|
>
|
||||||
@@ -221,23 +293,36 @@ async function handleImport() {
|
|||||||
width="96"
|
width="96"
|
||||||
/>
|
/>
|
||||||
<ElTableColumn
|
<ElTableColumn
|
||||||
prop="deptCode"
|
prop="loginName"
|
||||||
:label="$t('sysAccount.importDeptCode')"
|
:label="$t('sysAccount.loginName')"
|
||||||
min-width="140"
|
min-width="140"
|
||||||
show-overflow-tooltip
|
show-overflow-tooltip
|
||||||
/>
|
/>
|
||||||
<ElTableColumn
|
<ElTableColumn
|
||||||
prop="loginName"
|
prop="fieldName"
|
||||||
:label="$t('sysAccount.loginName')"
|
:label="$t('sysAccount.importFieldName')"
|
||||||
min-width="160"
|
min-width="140"
|
||||||
show-overflow-tooltip
|
show-overflow-tooltip
|
||||||
/>
|
/>
|
||||||
|
<ElTableColumn
|
||||||
|
prop="fieldValue"
|
||||||
|
:label="$t('sysAccount.importFieldValue')"
|
||||||
|
min-width="180"
|
||||||
|
show-overflow-tooltip
|
||||||
|
>
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.fieldValue || '-' }}
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
<ElTableColumn
|
<ElTableColumn
|
||||||
prop="reason"
|
prop="reason"
|
||||||
:label="$t('sysAccount.importReason')"
|
:label="$t('sysAccount.importReason')"
|
||||||
min-width="260"
|
min-width="320"
|
||||||
show-overflow-tooltip
|
>
|
||||||
/>
|
<template #default="{ row }">
|
||||||
|
<div class="error-reason-cell">{{ row.reason }}</div>
|
||||||
|
</template>
|
||||||
|
</ElTableColumn>
|
||||||
</ElTable>
|
</ElTable>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -292,6 +377,26 @@ async function handleImport() {
|
|||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.import-guide-card {
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: hsl(var(--surface-subtle) / 94%);
|
||||||
|
border: 1px solid hsl(var(--border) / 72%);
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-title {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-text {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
.selected-file-main {
|
.selected-file-main {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
@@ -329,6 +434,13 @@ async function handleImport() {
|
|||||||
color: var(--el-text-color-primary);
|
color: var(--el-text-color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.result-summary {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
.result-stats {
|
.result-stats {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
@@ -362,6 +474,12 @@ async function handleImport() {
|
|||||||
color: var(--el-color-danger);
|
color: var(--el-color-danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.error-reason-cell {
|
||||||
|
word-break: normal;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
.dialog-footer {
|
.dialog-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FormInstance } from 'element-plus';
|
import type { FormInstance } from 'element-plus';
|
||||||
|
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
import { EasyFlowFormModal, EasyFlowInputPassword } from '@easyflow/common-ui';
|
import { EasyFlowFormModal, EasyFlowInputPassword } from '@easyflow/common-ui';
|
||||||
|
|
||||||
@@ -29,6 +29,7 @@ function createDefaultEntity() {
|
|||||||
deptId: '',
|
deptId: '',
|
||||||
loginName: '',
|
loginName: '',
|
||||||
password: '',
|
password: '',
|
||||||
|
confirmPassword: '',
|
||||||
accountType: '',
|
accountType: '',
|
||||||
nickname: '',
|
nickname: '',
|
||||||
mobile: '',
|
mobile: '',
|
||||||
@@ -54,6 +55,21 @@ const validateStrongPassword = (_rule: any, value: string, callback: any) => {
|
|||||||
}
|
}
|
||||||
callback();
|
callback();
|
||||||
};
|
};
|
||||||
|
const validateConfirmPassword = (_rule: any, value: string, callback: any) => {
|
||||||
|
if (!isAdd.value) {
|
||||||
|
callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!value) {
|
||||||
|
callback(new Error($t('sysAccount.repeatPwd')));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value !== entity.value.password) {
|
||||||
|
callback(new Error($t('sysAccount.notSamePwd')));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
};
|
||||||
const btnLoading = ref(false);
|
const btnLoading = ref(false);
|
||||||
const rules = ref({
|
const rules = ref({
|
||||||
deptId: [
|
deptId: [
|
||||||
@@ -68,6 +84,9 @@ const rules = ref({
|
|||||||
password: [
|
password: [
|
||||||
{ required: true, validator: validateStrongPassword, trigger: 'blur' },
|
{ required: true, validator: validateStrongPassword, trigger: 'blur' },
|
||||||
],
|
],
|
||||||
|
confirmPassword: [
|
||||||
|
{ required: true, validator: validateConfirmPassword, trigger: 'blur' },
|
||||||
|
],
|
||||||
status: [
|
status: [
|
||||||
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
{ required: true, message: $t('message.required'), trigger: 'blur' },
|
||||||
],
|
],
|
||||||
@@ -91,10 +110,11 @@ function save() {
|
|||||||
saveForm.value?.validate((valid) => {
|
saveForm.value?.validate((valid) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
btnLoading.value = true;
|
btnLoading.value = true;
|
||||||
|
const { confirmPassword: _confirmPassword, ...payload } = entity.value;
|
||||||
api
|
api
|
||||||
.post(
|
.post(
|
||||||
isAdd.value ? 'api/v1/sysAccount/save' : 'api/v1/sysAccount/update',
|
isAdd.value ? 'api/v1/sysAccount/save' : 'api/v1/sysAccount/update',
|
||||||
entity.value,
|
payload,
|
||||||
)
|
)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
btnLoading.value = false;
|
btnLoading.value = false;
|
||||||
@@ -116,6 +136,16 @@ function closeDialog() {
|
|||||||
entity.value = createDefaultEntity();
|
entity.value = createDefaultEntity();
|
||||||
dialogVisible.value = false;
|
dialogVisible.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => entity.value.password,
|
||||||
|
() => {
|
||||||
|
if (!dialogVisible.value || !isAdd.value || !entity.value.confirmPassword) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
saveForm.value?.validateField('confirmPassword').catch(() => {});
|
||||||
|
},
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -159,6 +189,16 @@ function closeDialog() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
<ElFormItem
|
||||||
|
v-if="isAdd"
|
||||||
|
prop="confirmPassword"
|
||||||
|
:label="$t('sysAccount.confirmPwd')"
|
||||||
|
>
|
||||||
|
<EasyFlowInputPassword
|
||||||
|
v-model="entity.confirmPassword"
|
||||||
|
:placeholder="$t('sysAccount.repeatPwd')"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
<ElFormItem prop="nickname" :label="$t('sysAccount.nickname')">
|
<ElFormItem prop="nickname" :label="$t('sysAccount.nickname')">
|
||||||
<ElInput v-model.trim="entity.nickname" />
|
<ElInput v-model.trim="entity.nickname" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
|||||||
Reference in New Issue
Block a user