组用户功能修订
ci-userbase-service / build-and-push (push) Failing after 1m45s

This commit is contained in:
chenxing
2026-08-12 19:16:22 +08:00
parent f55c865ab1
commit a5ced786b4
28 changed files with 1488 additions and 202 deletions
@@ -1,12 +1,14 @@
package com.yunhai.userbase.service.impl;
import com.yunhai.platform.utils.exception.DException;
import com.yunhai.userbase.entity.SysGroup;
import com.yunhai.userbase.enums.IEnum;
import com.yunhai.userbase.enums.StatusEnum;
import com.yunhai.userbase.enums.SysRoleEnum;
import com.yunhai.userbase.enums.UserTypeEnum;
import com.yunhai.userbase.entity.SysUser;
import com.yunhai.userbase.entity.SysUserGroupRel;
import com.yunhai.userbase.model.sysuser.BatchImportResult;
import com.yunhai.userbase.model.sysuser.SysUserQuery;
import com.yunhai.userbase.model.sysuser.SysUserRequest;
import com.yunhai.userbase.model.sysuser.SysUserVo;
@@ -16,6 +18,11 @@ import com.yunhai.userbase.repository.SysUserGroupRelRepository;
import com.yunhai.userbase.repository.SysUserRepository;
import com.yunhai.userbase.service.ISysGroupService;
import com.yunhai.userbase.service.ISysUserService;
import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
import com.yunhai.userbase.model.sysuser.ExcelSysUser;
import com.yunhai.userbase.utils.EasyPoiUtils;
import com.yunhai.userbase.utils.ValidatorUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
@@ -26,16 +33,28 @@ import org.springframework.data.jpa.domain.Specification;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.*;
import org.springframework.web.multipart.MultipartFile;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.JoinType;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.persistence.criteria.Subquery;
import javax.transaction.Transactional;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.time.ZoneId;
import java.util.*;
import java.util.stream.Collectors;
import org.springframework.transaction.annotation.Transactional;
import javax.servlet.http.HttpServletResponse;
/**
* @author chenx
@@ -73,6 +92,18 @@ public class SysUserService implements ISysUserService {
@Override
@Transactional
public void add(SysUserRequest request) {
// 单条新增走完整校验(含密码强度)
createUser(request, true);
}
/**
* 创建用户并写入组关联。
*
* @param enforcePasswordStrength 是否强制密码强度校验:
* - 单条新增 = true(前端已交互式校验);
* - 批量导入 = false(默认密码 Vzuk4Dp3 不含符号,按业务约定直接放行)。
*/
private void createUser(SysUserRequest request, boolean enforcePasswordStrength) {
if (sysUserRepository.countByAccount(request.getAccount()) > 0) {
throw new DException("账号已存在");
}
@@ -80,7 +111,9 @@ public class SysUserService implements ISysUserService {
throw new DException("所属分组不存在");
}
validateProfile(request);
String encodedPassword = validateAndEncodePassword(request.getPassword(), request.getConfirmPassword());
String encodedPassword = enforcePasswordStrength
? validateAndEncodePassword(request.getPassword(), request.getConfirmPassword())
: passwordEncoder.encode(request.getPassword());
SysUser entity = new SysUser();
entity.setAccount(request.getAccount());
@@ -89,6 +122,8 @@ public class SysUserService implements ISysUserService {
entity.setPhone(request.getPhone());
entity.setBirthday(request.getBirthday());
entity.setIdcard(request.getIdcard());
entity.setEmail(request.getEmail());
entity.setGender(request.getGender());
entity.setProfilePicture(request.getProfilePicture());
entity.setRoles(request.getRoles());
// 用户类型推导:超级管理员账号固定为 1;其余含 ADMIN(7) 角色 -> 2 学校管理员;默认 0 普通用户
@@ -126,6 +161,8 @@ public class SysUserService implements ISysUserService {
entity.setPhone(request.getPhone());
entity.setBirthday(request.getBirthday());
entity.setIdcard(request.getIdcard());
entity.setEmail(request.getEmail());
entity.setGender(request.getGender());
entity.setProfilePicture(request.getProfilePicture());
entity.setRoles(request.getRoles());
// 用户类型推导:仅当本次传入了角色时才重算,避免编辑时未传角色把 type 误重置为 0
@@ -154,6 +191,38 @@ public class SysUserService implements ISysUserService {
sysUserRepository.deleteById(id);
}
@Override
@Transactional
public void deleteFromGroup(Integer userId, Integer groupId) {
batchDeleteFromGroup(Collections.singletonList(userId), groupId);
}
@Override
@Transactional
public void batchDeleteFromGroup(List<Integer> userIds, Integer groupId) {
if (userIds == null || userIds.isEmpty()) {
throw new DException("待删除用户不能为空");
}
if (groupId == null) {
throw new DException("组ID不能为空");
}
List<SysUser> users = sysUserRepository.findAllById(userIds);
if (users.size() != userIds.size()) {
throw new DException("存在无效用户");
}
for (Integer userId : userIds) {
// 删除当前组关联
sysUserGroupRelRepository.deleteByUserIdAndGroupId(userId, groupId);
// 若用户已无其它组关联,则级联删除用户本身及其角色绑定
if (sysUserGroupRelRepository.countByUserId(userId) == 0) {
sysUserAppRoleRelRepository.deleteByUserIdBulk(userId);
sysUserRepository.deleteById(userId);
}
}
}
@Override
@Transactional
public void changeStatus(Integer id, boolean disabled) {
@@ -164,29 +233,396 @@ public class SysUserService implements ISysUserService {
sysUserRepository.save(entity);
}
@Override
@Transactional
public void batchChangeStatus(List<Integer> userIds, boolean disabled) {
if (userIds == null || userIds.isEmpty()) {
throw new DException("待设置用户不能为空");
}
List<SysUser> users = sysUserRepository.findAllById(userIds);
if (users.size() != userIds.size()) {
throw new DException("存在无效用户");
}
StatusEnum targetStatus = disabled ? StatusEnum.DISABLED : StatusEnum.NORMAL;
for (SysUser user : users) {
user.setDisabled(targetStatus);
}
sysUserRepository.saveAll(users);
}
@Override
@Transactional
public void changeSortNo(Integer userId, Integer groupId, Integer sortNo) {
if (!sysUserRepository.existsById(userId)) {
throw new DException("系统用户不存在");
}
// 排序号按组维度存储,必须落到具体的 (userId, groupId) 关联行
SysUserGroupRel rel = sysUserGroupRelRepository.findByUserIdAndGroupId(userId, groupId)
.orElseThrow(() -> new DException("用户不在该组,无法设置排序号"));
rel.setSortNo(sortNo);
sysUserGroupRelRepository.save(rel);
}
@Override
public Page<SysUserVo> list(SysUserQuery query, Pageable pageable) {
Specification<SysUser> spec = buildSpecification(query);
return sysUserRepository.findAll(spec, pageable).map(this::convertToVo);
Page<SysUser> page = sysUserRepository.findAll(spec, pageable);
// 把当前组维度的 sortNo 带到 Vo(仅在具体组上下文有意义;根节点/未选组时为 null)
if (query.getGroupId() != null) {
Map<Integer, Integer> sortNoMap = resolveUserSortNoInGroup(
page.getContent().stream().map(SysUser::getId).collect(Collectors.toList()),
query.getGroupId());
return page.map(u -> convertToVo(u, sortNoMap.get(u.getId())));
}
return page.map(u -> convertToVo(u, null));
}
@Override
public List<Integer> findGroupIdsByUserId(Integer userId) {
return sysUserGroupRelRepository.findByUserId(userId).stream()
.map(SysUserGroupRel::getGroupId)
.collect(Collectors.toList());
}
@Override
@Transactional
public void changeGroups(List<Integer> userIds, List<Integer> groupIds) {
if (userIds == null || userIds.isEmpty()) {
throw new DException("待调整用户不能为空");
}
if (groupIds == null || groupIds.isEmpty()) {
throw new DException("请至少保留一个组");
}
List<SysUser> users = sysUserRepository.findAllById(userIds);
if (users.size() != userIds.size()) {
throw new DException("存在无效用户");
}
List<SysGroup> groups = sysGroupRepository.findAllById(groupIds);
if (groups.size() != groupIds.size()) {
throw new DException("存在无效分组");
}
LocalDateTime now = LocalDateTime.now();
for (Integer userId : userIds) {
sysUserGroupRelRepository.deleteByUserId(userId);
List<SysUserGroupRel> rels = new ArrayList<>(groupIds.size());
for (int i = 0; i < groupIds.size(); i++) {
SysUserGroupRel rel = new SysUserGroupRel();
rel.setUserId(userId);
rel.setGroupId(groupIds.get(i));
rel.setIsMain(i == 0 ? IS_MAIN : 0);
rel.setCreateTime(now);
rels.add(rel);
}
sysUserGroupRelRepository.saveAll(rels);
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public BatchImportResult batchImport(MultipartFile file) {
BatchImportResult result = new BatchImportResult();
if (file == null || file.isEmpty()) {
throw new DException("上传文件不能为空");
}
Workbook workbook = null;
try {
workbook = WorkbookFactory.create(file.getInputStream());
Sheet sheet = workbook.getSheetAt(0);
int lastRowNum = sheet.getLastRowNum();
for (int i = 1; i <= lastRowNum; i++) {
Row row = sheet.getRow(i);
if (row == null || isBlankRow(row)) {
continue;
}
try {
SysUserRequest request = parseImportRow(row);
createUser(request, false);
result.addSuccess();
} catch (DException e) {
result.addFail(i + 1, e.getMessage());
} catch (Exception e) {
result.addFail(i + 1, "系统异常:" + e.getMessage());
}
}
// 仅统计实际参与解析的数据行(跳过空行),使 总数 = 成功 + 失败
result.setTotal(result.getSuccess() + result.getFail());
} catch (Exception e) {
throw new DException("文件读取失败:" + e.getMessage());
} finally {
if (workbook != null) {
try {
workbook.close();
} catch (IOException ignored) {
// 关闭资源失败不阻断主流程
}
}
}
return result;
}
// ===================== 导入模板下载 =====================
/**
* 导入/导出共用的列定义(顺序即 Excel 列顺序)。<br>
* 导入模板生成与(后续)导出复用同一套,避免两边列顺序/文案漂移。
*/
private static final List<ImportColumnDef> IMPORT_COLUMN_DEFS = Arrays.asList(
new ImportColumnDef("groupName", "组名\n(不必填,供与id对应参考用)", 36, false, null),
new ImportColumnDef("groupId", "组id(必须)", 14, true, null),
new ImportColumnDef("account", "用户名(必须)", 20, true, null),
new ImportColumnDef("password", "密码\n(可选,\n默认Vzuk4Dp3)", 18, false, ""),
new ImportColumnDef("realName", "姓名(必须)", 16, true, null),
new ImportColumnDef("idcard", "身份证号(可选)", 22, false, null),
new ImportColumnDef("phone", "手机号(可选)", 16, false, null),
new ImportColumnDef("email", "email(可选)", 24, false, null),
new ImportColumnDef("nature", "用户性质\n(可选,默认普通用户\n组管理员2,\n普通用户0,\n导入不接受别的代码)", 24, false, "0"),
new ImportColumnDef("identity", "用户身份\n(1教师 2教职工 3学生 4家长)", 24, false, null),
new ImportColumnDef("gender", "性别(可选)", 12, false, null),
new ImportColumnDef("birthday", "生日(yyyy-MM-dd)(可选)", 22, false, null)
);
/** 组织树虚拟根 parentId(与 SysGroupService 约定一致)。 */
private static final int ROOT_PARENT_ID = 0;
/**
* 下载用户导入模板:以指定组(含)为根,将其自身及所有子孙组平铺为数据行,
* 仅填充「组名(路径)」与「组id」两列,其余列为空供用户填写。<br>
* 生成真实 .xlsx(POI XSSF),可被后端 BatchImport 正确解析(HTML 表格伪装 .xls 无法被 POI 解析)。
*/
@Override
public void downloadImportTemplate(Integer groupId, HttpServletResponse response) throws IOException {
List<GroupPath> groups = buildImportGroupPaths(groupId);
XSSFWorkbook workbook = new XSSFWorkbook();
try {
XSSFSheet sheet = workbook.createSheet("用户导入模板");
// 表头样式:加粗、居中、自动换行、细边框
XSSFCellStyle headerStyle = workbook.createCellStyle();
XSSFFont headerFont = workbook.createFont();
headerFont.setBold(true);
headerStyle.setFont(headerFont);
headerStyle.setWrapText(true);
headerStyle.setVerticalAlignment(VerticalAlignment.CENTER);
headerStyle.setAlignment(HorizontalAlignment.CENTER);
headerStyle.setBorderTop(BorderStyle.THIN);
headerStyle.setBorderBottom(BorderStyle.THIN);
headerStyle.setBorderLeft(BorderStyle.THIN);
headerStyle.setBorderRight(BorderStyle.THIN);
XSSFRow headerRow = sheet.createRow(0);
for (int i = 0; i < IMPORT_COLUMN_DEFS.size(); i++) {
ImportColumnDef col = IMPORT_COLUMN_DEFS.get(i);
XSSFCell cell = headerRow.createCell(i);
cell.setCellValue(col.getTitle());
cell.setCellStyle(headerStyle);
sheet.setColumnWidth(i, col.getWidth() * 256);
}
headerRow.setHeightInPoints(64);
// 数据行:每个组一行,仅填 组名(路径) + 组id
int r = 1;
for (GroupPath g : groups) {
XSSFRow row = sheet.createRow(r++);
row.createCell(IDX_GROUP_NAME).setCellValue(g.getPath());
row.createCell(IDX_GROUP_ID).setCellValue(String.valueOf(g.getId()));
}
String filename = URLEncoder.encode("用户导入模板.xlsx", "UTF-8");
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-Disposition",
"attachment; filename=\"" + filename + "\"; filename*=UTF-8''" + filename);
try (OutputStream out = response.getOutputStream()) {
workbook.write(out);
}
} finally {
workbook.close();
}
}
// ===================== 导出 =====================
/**
* 导出组下所有用户(含子组)为 Excel:与导入模板 {@code IMPORT_COLUMN_DEFS} 同构,末尾追加「用户id」列。<br>
* - 组名 / 组id:取每个用户实际所属的主组(isMain=1),与导入模板的「参考」列保持一致;<br>
* - 密码:数据库存的是 BCrypt 密文、不可逆,导出留空(再导入时退化为默认 Vzuk4Dp3);<br>
* - 用户性质 nature:取 type 的 key(0普通 / 1超级管理员 / 2学校管理员);<br>
* - 用户身份 identity:取 roles 数组第一个元素(系统角色枚举 key:1管理员 2老师 3校领导 4局领导 5学生 6家长 7工作号)。
*/
@Override
public void exportUsers(Integer groupId, Integer withSub, String name, HttpServletResponse response) throws IOException {
// 1. 解析组范围(含子孙)并查询范围内全部用户(不分页)
SysUserQuery query = new SysUserQuery();
query.setGroupId(groupId);
query.setWithSub(withSub);
query.setName(name);
List<SysUser> users = sysUserRepository.findAll(buildSpecification(query));
// 2. 批量取每个用户的主组(isMain=1),用于填充「组名 / 组id」参考列
List<Integer> userIds = users.stream().map(SysUser::getId).collect(Collectors.toList());
Map<Integer, Integer> userMainGroup = resolveUserMainGroup(userIds);
// 3. 批量取组名
Map<Integer, String> groupNameMap = sysGroupRepository.findAllById(userMainGroup.values())
.stream().collect(Collectors.toMap(SysGroup::getId, SysGroup::getName));
// 4. 组装导出行
List<ExcelSysUser> rows = new ArrayList<>(users.size());
for (SysUser u : users) {
ExcelSysUser row = new ExcelSysUser();
Integer gid = userMainGroup.get(u.getId());
row.setGroupId(gid == null ? "" : String.valueOf(gid));
row.setGroupName(gid != null ? groupNameMap.get(gid) : null);
row.setAccount(u.getAccount());
row.setPassword(""); // 密文不可逆,导出留空
row.setRealName(u.getRealName());
row.setIdcard(u.getIdcard());
row.setPhone(u.getPhone());
row.setEmail(u.getEmail());
row.setNature(u.getType() == null ? null : u.getType().getKey());
row.setIdentity(u.getRoles() == null || u.getRoles().isEmpty() ? null : u.getRoles().get(0));
row.setGender(u.getGender());
row.setBirthday(u.getBirthday() == null ? "" : u.getBirthday().toString());
row.setUserId(u.getId());
rows.add(row);
}
// 5. 写出(HSSF .xls,与 EasyPoiUtils.downLoadExcel 的 .xls 兜底一致)
ExportParams exportParams = new ExportParams("组用户导出", "组用户");
exportParams.setType(ExcelType.HSSF);
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, ExcelSysUser.class, rows);
EasyPoiUtils.downLoadExcel("组用户导出", response, workbook);
}
/**
* 批量解析 用户 -> 主组(groupId) 映射:优先取 isMain=1 的组,否则取第一个关联组。
*/
private Map<Integer, Integer> resolveUserMainGroup(List<Integer> userIds) {
Map<Integer, Integer> mainGroup = new HashMap<>();
if (userIds.isEmpty()) {
return mainGroup;
}
List<SysUserGroupRel> rels = sysUserGroupRelRepository.findByUserIdIn(userIds);
Map<Integer, List<SysUserGroupRel>> byUser = rels.stream()
.collect(Collectors.groupingBy(SysUserGroupRel::getUserId));
for (Map.Entry<Integer, List<SysUserGroupRel>> e : byUser.entrySet()) {
Integer gid = e.getValue().stream()
.filter(r -> r.getIsMain() != null && r.getIsMain() == IS_MAIN)
.map(SysUserGroupRel::getGroupId)
.findFirst()
.orElse(e.getValue().get(0).getGroupId());
mainGroup.put(e.getKey(), gid);
}
return mainGroup;
}
/**
* 收集导入模板所需的「组路径 + 组id」列表:以指定组为根做先序 DFS,
* 路径用「/」拼接(如 上海鸿育初级中学/教师组/校长室)。
*/
private List<GroupPath> buildImportGroupPaths(Integer groupId) {
List<SysGroup> all = sysGroupRepository.findAll();
Map<Integer, SysGroup> byId = new HashMap<>();
Map<Integer, List<SysGroup>> childrenMap = new HashMap<>();
for (SysGroup g : all) {
byId.put(g.getId(), g);
Integer key = (g.getParentId() == null) ? ROOT_PARENT_ID : g.getParentId();
childrenMap.computeIfAbsent(key, k -> new ArrayList<>()).add(g);
}
// 同层级按 sort 升序、再按 id 升序,保证模板顺序稳定且与树展示一致
Comparator<SysGroup> order = (a, b) -> {
int sa = a.getSort() == null ? 999 : a.getSort();
int sb = b.getSort() == null ? 999 : b.getSort();
if (sa != sb) {
return Integer.compare(sa, sb);
}
return Integer.compare(a.getId(), b.getId());
};
for (List<SysGroup> siblings : childrenMap.values()) {
siblings.sort(order);
}
List<GroupPath> result = new ArrayList<>();
List<SysGroup> roots;
if (groupId != null && byId.containsKey(groupId)) {
roots = Collections.singletonList(byId.get(groupId));
} else {
roots = childrenMap.getOrDefault(ROOT_PARENT_ID, new ArrayList<>());
}
for (SysGroup root : roots) {
dfsGroup(root, "", childrenMap, result);
}
return result;
}
private void dfsGroup(SysGroup node, String parentPath,
Map<Integer, List<SysGroup>> childrenMap, List<GroupPath> out) {
String path = parentPath.isEmpty() ? node.getName() : parentPath + "/" + node.getName();
GroupPath gp = new GroupPath();
gp.setId(node.getId());
gp.setPath(path);
out.add(gp);
List<SysGroup> children = childrenMap.get(node.getId());
if (children != null) {
for (SysGroup child : children) {
dfsGroup(child, path, childrenMap, out);
}
}
}
/** 导入/导出列定义(轻量 DTO)。 */
@lombok.Data
private static class ImportColumnDef {
private final String key;
private final String title;
private final int width;
private final boolean required;
private final String sample;
}
/** 模板所需的组路径节点。 */
@lombok.Data
private static class GroupPath {
private Integer id;
private String path;
}
// ===================== 私有工具 =====================
/**
* 组装用户列表查询条件:组范围(含子组)+ 关键字(账号/姓名/身份证模糊、手机号精确)+ 启用状态。<br>
* 排序按「是否选中具体组」分治:<br>
* - 选中具体组(groupId != null):JOIN 关联表,按组内 sort_no 正序(NULLS LAST),其次 user_id 正序;
* 含子组时同一用户可能命中多组,启用 distinct 去重(同时保证导出不去重重复行)。<br>
* - 根节点 / 未选组(groupId == null):不 JOIN 关联表,直接按主键 id 正序(聚簇索引顺序,无 filesort),
* 平台总用户量不影响查询性能。
*/
private Specification<SysUser> buildSpecification(SysUserQuery query) {
return (root, cbQuery, cb) -> {
List<Predicate> predicates = new ArrayList<>();
// 组范围过滤:用户ID 属于目标组集合下的关联记录
if (query.getGroupId() != null) {
// 具体组上下文:JOIN 关联表以按组内 sort_no 排序
List<Integer> groupIds = resolveGroupIds(query.getGroupId(), query.getWithSub());
Subquery<Integer> sub = cbQuery.subquery(Integer.class);
Root<SysUserGroupRel> relRoot = sub.from(SysUserGroupRel.class);
sub.select(relRoot.get("userId"));
sub.where(relRoot.get("groupId").in(groupIds));
predicates.add(root.get("id").in(sub));
Join<SysUser, SysUserGroupRel> rel = root.join("userGroupRelList", JoinType.INNER);
predicates.add(rel.get("groupId").in(groupIds));
cbQuery.distinct(true);
// 排序规则:已设排序号者排最前(NULLS LAST),其次 sort_no 升序,最后 user_id 升序兜底
// 注:Hibernate 5.6 的 AST 解析器不支持把 IS NULL 表达式直接放进 ORDER BY,
// 改用 CASE WHEN sort_no IS NULL THEN 1 ELSE 0 END(产出普通整数,语义等价 NULLS LAST)。
// rel.get("sortNo") 返回 Path<Object>,直接作为 Expression<?> 传入 orderBy 即可。
cbQuery.orderBy(
cb.asc(cb.selectCase().when(cb.isNull(rel.get("sortNo")), 1).otherwise(0)),
cb.asc(rel.get("sortNo")),
cb.asc(root.get("id"))
);
} else {
// 根节点 / 未选组:无组上下文,按主键 id 升序(聚簇索引顺序,不产生 filesort)
cbQuery.orderBy(cb.asc(root.get("id")));
}
// 关键字:账号/姓名/身份证模糊,手机号精确
@@ -208,6 +644,21 @@ public class SysUserService implements ISysUserService {
};
}
/**
* 批量取「用户在某组下的排序号」,用于列表 Vo 携带当前组维度 sortNo(仅具体组上下文调用)。
*/
private Map<Integer, Integer> resolveUserSortNoInGroup(List<Integer> userIds, Integer groupId) {
Map<Integer, Integer> sortNoMap = new HashMap<>();
if (userIds.isEmpty()) {
return sortNoMap;
}
List<SysUserGroupRel> rels = sysUserGroupRelRepository.findByUserIdInAndGroupId(userIds, groupId);
for (SysUserGroupRel rel : rels) {
sortNoMap.put(rel.getUserId(), rel.getSortNo());
}
return sortNoMap;
}
/** 依据 withSub 决定组范围:1=含所有子孙组,否则仅当前组。 */
private List<Integer> resolveGroupIds(Integer groupId, Integer withSub) {
if (withSub != null && withSub == 1) {
@@ -243,12 +694,18 @@ public class SysUserService implements ISysUserService {
}
private SysUserVo convertToVo(SysUser entity) {
return convertToVo(entity, null);
}
private SysUserVo convertToVo(SysUser entity, Integer sortNo) {
SysUserVo vo = new SysUserVo();
BeanUtils.copyProperties(entity, vo);
// disabled 为枚举,需要单独映射为 key(与 GroupTypeEnum 的 type 处理一致)
vo.setDisabled(entity.getDisabled() == null ? null : entity.getDisabled().getKey());
// type 为枚举,同样映射为 key
vo.setType(entity.getType() == null ? null : entity.getType().getKey());
// 当前组维度的排序号;根节点 / 未选组时为 null
vo.setSortNo(sortNo);
return vo;
}
@@ -276,4 +733,191 @@ public class SysUserService implements ISysUserService {
}
return statusEnum;
}
// ===================== 批量导入私有工具 =====================
private static final int IDX_GROUP_NAME = 0;
private static final int IDX_GROUP_ID = 1;
private static final int IDX_ACCOUNT = 2;
private static final int IDX_PASSWORD = 3;
private static final int IDX_REAL_NAME = 4;
private static final int IDX_IDCARD = 5;
private static final int IDX_PHONE = 6;
private static final int IDX_EMAIL = 7;
private static final int IDX_NATURE = 8;
private static final int IDX_IDENTITY = 9;
private static final int IDX_GENDER = 10;
private static final int IDX_BIRTHDAY = 11;
/**
* 解析 Excel 一行为 SysUserRequest。
* 列顺序与前端导入模板严格一致:
* 0组名 1组id 2用户名 3密码 4姓名 5身份证 6手机 7email 8用户性质 9用户身份 10性别 11生日。
*/
private SysUserRequest parseImportRow(Row row) {
SysUserRequest request = new SysUserRequest();
Integer groupId = parseInteger(getCellString(row, IDX_GROUP_ID));
if (groupId == null) {
throw new DException("组id不能为空");
}
if (!sysGroupRepository.existsById(groupId)) {
throw new DException("组id【" + groupId + "】不存在");
}
request.setGroupId(groupId);
String account = getCellString(row, IDX_ACCOUNT);
if (StringUtils.isBlank(account)) {
throw new DException("用户名不能为空");
}
request.setAccount(account.trim());
String password = getCellString(row, IDX_PASSWORD);
if (StringUtils.isBlank(password)) {
password = "Vzuk4Dp3";
}
request.setPassword(password);
request.setConfirmPassword(password);
String realName = getCellString(row, IDX_REAL_NAME);
if (StringUtils.isBlank(realName)) {
throw new DException("姓名不能为空");
}
request.setRealName(realName.trim());
request.setIdcard(getCellString(row, IDX_IDCARD));
request.setPhone(getCellString(row, IDX_PHONE));
request.setEmail(getCellString(row, IDX_EMAIL));
Integer nature = parseInteger(getCellString(row, IDX_NATURE));
Integer identity = parseInteger(getCellString(row, IDX_IDENTITY));
request.setRoles(buildImportRoles(nature, identity));
request.setGender(parseGender(getCellString(row, IDX_GENDER)));
request.setBirthday(parseBirthday(row.getCell(IDX_BIRTHDAY)));
validateProfile(request);
return request;
}
/**
* 根据模板中的用户性质与用户身份构建 roles 列表。
* - 用户性质:0普通用户(不加管理员),2组管理员(加 ADMIN=7);其他值视为非法。
* - 用户身份:1教师->TEACHER(1),2教职工->SCHOOL_LEADER(2),3学生->STUDENT(3),4家长->PARENT(4)。
*/
private List<Integer> buildImportRoles(Integer nature, Integer identity) {
Set<Integer> roleSet = new HashSet<>();
if (nature != null) {
if (nature == 2) {
roleSet.add(SysRoleEnum.ADMIN.getKey());
} else if (nature != 0) {
throw new DException("用户性质只能为 0 或 2");
}
}
if (identity != null) {
switch (identity) {
case 1:
roleSet.add(SysRoleEnum.TEACHER.getKey());
break;
case 2:
roleSet.add(SysRoleEnum.SCHOOL_LEADER.getKey());
break;
case 3:
roleSet.add(SysRoleEnum.STUDENT.getKey());
break;
case 4:
roleSet.add(SysRoleEnum.PARENT.getKey());
break;
default:
throw new DException("用户身份只能为 1/2/3/4");
}
}
return new ArrayList<>(roleSet);
}
private Integer parseGender(String value) {
if (StringUtils.isBlank(value)) {
return null;
}
String v = value.trim();
if ("1".equals(v) || "男".equals(v)) {
return 1;
}
if ("0".equals(v) || "女".equals(v)) {
return 0;
}
throw new DException("性别只能为 0/女 或 1/男");
}
private LocalDate parseBirthday(Cell cell) {
if (cell == null) {
return null;
}
if (cell.getCellType() == CellType.NUMERIC && DateUtil.isCellDateFormatted(cell)) {
return cell.getDateCellValue().toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
}
String value = getCellString(cell);
if (StringUtils.isBlank(value)) {
return null;
}
try {
return LocalDate.parse(value.trim());
} catch (Exception e) {
throw new DException("生日格式错误,请使用 yyyy-MM-dd");
}
}
private boolean isBlankRow(Row row) {
for (int i = 0; i <= IDX_BIRTHDAY; i++) {
if (StringUtils.isNotBlank(getCellString(row, i))) {
return false;
}
}
return true;
}
private String getCellString(Row row, int cellIndex) {
Cell cell = row.getCell(cellIndex);
if (cell == null) {
return "";
}
return getCellString(cell);
}
private String getCellString(Cell cell) {
switch (cell.getCellType()) {
case STRING:
return StringUtils.trim(cell.getStringCellValue());
case NUMERIC:
if (DateUtil.isCellDateFormatted(cell)) {
return cell.getDateCellValue().toInstant().atZone(ZoneId.systemDefault()).toLocalDate().toString();
}
double numeric = cell.getNumericCellValue();
if (numeric == Math.floor(numeric)) {
return String.valueOf((long) numeric);
}
return String.valueOf(numeric);
case BOOLEAN:
return String.valueOf(cell.getBooleanCellValue());
case FORMULA:
try {
return String.valueOf(cell.getNumericCellValue());
} catch (Exception e) {
return cell.getStringCellValue();
}
default:
return "";
}
}
private Integer parseInteger(String value) {
if (StringUtils.isBlank(value)) {
return null;
}
try {
return Integer.valueOf(value.trim());
} catch (NumberFormatException e) {
return null;
}
}
}