init: UserBaseService (backend + deploy + scripts)
ci-userbase-service / build-and-push (push) Successful in 40s

This commit is contained in:
chenxing
2026-08-10 14:50:22 +08:00
commit 4ec19486e0
234 changed files with 15604 additions and 0 deletions
@@ -0,0 +1,138 @@
package com.yunhai.userbase.service.impl;
import com.yunhai.platform.utils.exception.DException;
import com.yunhai.userbase.entity.AppCategory;
import com.yunhai.userbase.model.appauthz.AppAuthzAppRow;
import com.yunhai.userbase.model.appauthz.AppAuthzCategoryVo;
import com.yunhai.userbase.model.appcategory.AppCategoryQuery;
import com.yunhai.userbase.model.appcategory.AppCategoryRequest;
import com.yunhai.userbase.model.appcategory.AppCategoryVo;
import com.yunhai.userbase.repository.AppCategoryRepository;
import com.yunhai.userbase.repository.ApplicationInfoRepository;
import com.yunhai.userbase.service.IAppCategoryService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import javax.transaction.Transactional;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @author chenx
* @date 2026/7/15 21:30
* @description
*/
@Service
public class AppCategoryService implements IAppCategoryService {
@Autowired
private AppCategoryRepository appCategoryRepository;
@Autowired
private ApplicationInfoRepository applicationInfoRepository;
@Override
@Transactional
public void add(AppCategoryRequest request) {
// 重名校验:新增类别名称不允许重复
if (appCategoryRepository.existsByName(request.getName())) {
throw new DException("应用类型已存在");
}
AppCategory entity = new AppCategory();
entity.setName(request.getName());
if (request.getSortNo() == null) {
Integer maxSortNo = appCategoryRepository.findMaxSortNo();
entity.setSortNo(maxSortNo == null ? 1 : maxSortNo + 1);
} else {
entity.setSortNo(request.getSortNo());
}
appCategoryRepository.save(entity);
}
@Override
@Transactional
public void edit(Integer id, AppCategoryRequest request) {
AppCategory entity = appCategoryRepository.findById(id)
.orElseThrow(() -> new RuntimeException("应用类别不存在"));
// 重名校验:编辑时排除自身
if (appCategoryRepository.existsByNameAndIdNot(request.getName(), id)) {
throw new DException("应用类型已存在");
}
entity.setName(request.getName());
if (request.getSortNo() != null) {
entity.setSortNo(request.getSortNo());
}
appCategoryRepository.save(entity);
}
@Override
@Transactional
public void delete(Integer id) {
if (applicationInfoRepository.countByCategoryId(id) > 0) {
throw new RuntimeException("该类别下存在应用,无法删除");
}
appCategoryRepository.deleteById(id);
}
@Override
public List<AppCategoryVo> list(AppCategoryQuery query) {
List<AppCategory> list;
if (StringUtils.hasText(query.getName())) {
list = appCategoryRepository.findByNameContainingOrderBySortNoAsc(query.getName());
} else {
list = appCategoryRepository.findAllByOrderBySortNoAsc();
}
return list.stream().map(this::convertToVo).collect(Collectors.toList());
}
@Override
@Transactional
public void sort(List<Integer> ids) {
for (int i = 0; i < ids.size(); i++) {
if (!appCategoryRepository.existsById(ids.get(i))) {
throw new RuntimeException("类别不存在: " + ids.get(i));
}
appCategoryRepository.updateSortNoById(ids.get(i), i + 1);
}
}
@Override
public List<AppAuthzCategoryVo> listWithApps() {
List<AppCategory> categories = appCategoryRepository.findAllByOrderBySortNoAsc();
List<AppAuthzAppRow> rows = applicationInfoRepository.findAllAuthzRows();
// categoryId -> 该类型下应用列表(查询已按 sortNo、id 升序,插入顺序即排序)
Map<Integer, List<AppAuthzCategoryVo.AppItem>> appMap = new HashMap<>();
for (AppAuthzAppRow row : rows) {
AppAuthzCategoryVo.AppItem item = new AppAuthzCategoryVo.AppItem();
item.setId(row.getId());
item.setName(row.getName());
appMap.computeIfAbsent(row.getCategoryId(), k -> new ArrayList<>()).add(item);
}
List<AppAuthzCategoryVo> result = new ArrayList<>();
for (AppCategory category : categories) {
AppAuthzCategoryVo vo = new AppAuthzCategoryVo();
vo.setId(category.getId());
vo.setName(category.getName());
vo.setApp(appMap.getOrDefault(category.getId(), new ArrayList<>()));
result.add(vo);
}
return result;
}
private AppCategoryVo convertToVo(AppCategory entity) {
AppCategoryVo vo = new AppCategoryVo();
BeanUtils.copyProperties(entity, vo);
return vo;
}
}
@@ -0,0 +1,277 @@
package com.yunhai.userbase.service.impl;
import com.bestsch.utils.DException;
import com.yunhai.userbase.entity.AppModule;
import com.yunhai.userbase.model.appmodule.AppModuleQuery;
import com.yunhai.userbase.model.appmodule.AppModuleRequest;
import com.yunhai.userbase.model.appmodule.AppModuleVo;
import com.yunhai.userbase.repository.AppModuleRepository;
import com.yunhai.userbase.repository.ApplicationInfoRepository;
import com.yunhai.userbase.repository.SysAppRoleModuleRelRepository;
import com.yunhai.userbase.repository.SysGroupModulePermRepository;
import com.yunhai.userbase.service.IAppModuleService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.transaction.Transactional;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* @author chenx
* @date 2026/7/22
* @description 应用模块服务:树形模块的增删改查与层级校验(逻辑外键,最深5层)。<br>
*/
@Service
public class AppModuleService implements IAppModuleService {
/** 模块最大层级(根模块=1,最深5层)。<br> */
private static final int MAX_DEPTH = 5;
@Autowired
private AppModuleRepository appModuleRepository;
@Autowired
private ApplicationInfoRepository applicationInfoRepository;
@Autowired
private SysGroupModulePermRepository sysGroupModulePermRepository;
@Autowired
private SysAppRoleModuleRelRepository sysAppRoleModuleRelRepository;
@Override
@Transactional
public void add(AppModuleRequest request) {
Objects.requireNonNull(request, "模块新增入参不能为空");
if (!applicationInfoRepository.existsById(request.getAppId())) {
throw new DException("所属应用不存在");
}
if (appModuleRepository.existsByAppIdAndCustomId(request.getAppId(), request.getCustomId())) {
throw new DException("自定义ID在该应用下已存在");
}
AppModule entity = new AppModule();
BeanUtils.copyProperties(request, entity);
entity.setId(null);
entity.setLevel(resolveLevel(request.getAppId(), request.getParentId()));
// sortNo 默认为空(由前端编辑时带入),不自动递增
appModuleRepository.save(entity);
}
@Override
@Transactional
public void edit(Integer id, AppModuleRequest request) {
Objects.requireNonNull(id, "模块ID不能为空");
Objects.requireNonNull(request, "模块编辑入参不能为空");
AppModule entity = appModuleRepository.findById(id)
.orElseThrow(() -> new RuntimeException("模块不存在: " + id));
if (appModuleRepository.existsByAppIdAndCustomIdAndIdNot(entity.getAppId(), request.getCustomId(), id)) {
throw new DException("自定义ID在该应用下已存在");
}
// 可编辑字段:name/customId/path/sortNo;id、appId 不可改
entity.setName(request.getName());
entity.setCustomId(request.getCustomId());
entity.setPath(request.getPath());
entity.setSortNo(request.getSortNo());
// parentId 变更即移动子树:校验层级上限与环,并级联刷新后代层级
if (!Objects.equals(request.getParentId(), entity.getParentId())) {
moveSubtree(entity, request.getParentId());
} else {
appModuleRepository.save(entity);
}
}
@Override
@Transactional
public void delete(Integer id) {
Objects.requireNonNull(id, "模块ID不能为空");
if (!appModuleRepository.existsById(id)) {
throw new RuntimeException("模块不存在: " + id);
}
if (appModuleRepository.countByParentId(id) > 0) {
throw new DException("该模块下存在子模块,无法删除");
}
// AI-FIX: 删除模块时级联清理各组对该模块的权限记录(逻辑外键无物理 FK,避免残留孤儿数据)
sysGroupModulePermRepository.deleteByModuleIdInBulk(Collections.singletonList(id));
// AI-FIX(角色授权): 删除模块时级联清理各业务角色对该模块的授权(逻辑外键无物理 FK)
sysAppRoleModuleRelRepository.deleteByModuleIdInBulk(Collections.singletonList(id));
appModuleRepository.deleteById(id);
}
@Override
public List<AppModuleVo> list(AppModuleQuery query) {
// 载入该应用下全部模块(忽略 parentId/name,树在后处理阶段组装)
List<AppModule> all = appModuleRepository.findModules(query.getAppId(), null, null);
if (all.isEmpty()) {
return Collections.emptyList();
}
// parentId -> 子节点列表
Map<Integer, List<AppModule>> childrenMap = new HashMap<Integer, List<AppModule>>();
for (AppModule m : all) {
Integer key = (m.getParentId() == null) ? 0 : m.getParentId();
childrenMap.computeIfAbsent(key, k -> new ArrayList<AppModule>()).add(m);
}
// 每个兄弟组按 sortNo(空视为999) 升序,再按 id 升序
Comparator<AppModule> siblingOrder = (a, b) -> {
int sa = a.getSortNo() == null ? 999 : a.getSortNo();
int sb = b.getSortNo() == null ? 999 : b.getSortNo();
if (sa != sb) {
return Integer.compare(sa, sb);
}
return Integer.compare(a.getId(), b.getId());
};
for (List<AppModule> siblings : childrenMap.values()) {
siblings.sort(siblingOrder);
}
// 组装树:默认顶层为 level-1(parentId=null);传入 parentId 则以该模块为根返回其完整子树
List<AppModuleVo> tree;
if (query.getParentId() != null) {
AppModule root = all.stream()
.filter(m -> Objects.equals(m.getId(), query.getParentId()))
.findFirst()
.orElseThrow(() -> new RuntimeException("父模块不存在: " + query.getParentId()));
tree = Collections.singletonList(buildNode(root, childrenMap));
} else {
List<AppModule> roots = childrenMap.getOrDefault(0, Collections.emptyList());
tree = new ArrayList<AppModuleVo>();
for (AppModule r : roots) {
tree.add(buildNode(r, childrenMap));
}
}
// 名称过滤:保留名称匹配或含匹配后代的分支
String name = query.getName();
if (name != null && !name.trim().isEmpty()) {
tree = pruneByKeyword(tree, name.trim());
}
return tree;
}
/**
* 递归组装节点及其子模块。<br>
*/
private AppModuleVo buildNode(AppModule node, Map<Integer, List<AppModule>> childrenMap) {
AppModuleVo vo = convertToVo(node);
List<AppModule> children = childrenMap.get(node.getId());
if (children != null) {
for (AppModule child : children) {
vo.getChildren().add(buildNode(child, childrenMap));
}
}
return vo;
}
/**
* 按关键字剪枝:仅保留自身或后代命中的分支。<br>
*/
private List<AppModuleVo> pruneByKeyword(List<AppModuleVo> nodes, String keyword) {
List<AppModuleVo> result = new ArrayList<AppModuleVo>();
for (AppModuleVo n : nodes) {
boolean selfMatch = n.getName() != null && n.getName().contains(keyword);
List<AppModuleVo> keptChildren = pruneByKeyword(n.getChildren(), keyword);
if (selfMatch || !keptChildren.isEmpty()) {
n.setChildren(keptChildren);
result.add(n);
}
}
return result;
}
/**
* 计算新建模块层级。<br>
* 根模块(parentId=null)=1;子模块=父模块层级+1,超过 MAX_DEPTH 抛异常。
*/
private Integer resolveLevel(Integer appId, Integer parentId) {
if (parentId == null) {
return 1;
}
AppModule parent = appModuleRepository.findById(parentId)
.orElseThrow(() -> new DException("父模块不存在"));
if (!Objects.equals(parent.getAppId(), appId)) {
throw new DException("父模块不属于该应用");
}
int childLevel = parent.getLevel() + 1;
if (childLevel > MAX_DEPTH) {
throw new DException("模块层级不能超过" + MAX_DEPTH + "层");
}
return childLevel;
}
/**
* 移动子树:校验环与层级上限,刷新自身及后代层级后批量保存。<br>
* 依赖同一事务内持久化上下文的对象同一性(all 中即 entity)。
*/
private void moveSubtree(AppModule entity, Integer newParentId) {
List<AppModule> all = appModuleRepository.findModules(entity.getAppId(), null, null);
Map<Integer, AppModule> idMap = new HashMap<Integer, AppModule>();
for (AppModule m : all) {
idMap.put(m.getId(), m);
}
// 环检测:新父节点不能是自身或自身的后代
if (newParentId != null) {
if (Objects.equals(newParentId, entity.getId())) {
throw new DException("不能将模块移动到自身下");
}
Integer ancestor = newParentId;
while (ancestor != null) {
AppModule a = idMap.get(ancestor);
if (a == null) {
throw new DException("父模块不存在");
}
if (Objects.equals(a.getId(), entity.getId())) {
throw new DException("不能将模块移动到其子模块下");
}
ancestor = a.getParentId();
}
}
int newLevel = (newParentId == null) ? 1 : idMap.get(newParentId).getLevel() + 1;
int delta = newLevel - entity.getLevel();
int maxRelative = maxDescendantRelativeDepth(entity.getId(), all);
if (newLevel + maxRelative > MAX_DEPTH) {
throw new DException("移动后模块层级超过" + MAX_DEPTH + "层");
}
entity.setLevel(newLevel);
entity.setParentId(newParentId);
shiftDescendantLevels(entity.getId(), delta, all);
appModuleRepository.saveAll(all);
}
/**
* 计算指定节点下后代的最大相对深度(直接子节点=1)。<br>
*/
private int maxDescendantRelativeDepth(Integer rootId, List<AppModule> all) {
int max = 0;
for (AppModule m : all) {
if (Objects.equals(m.getParentId(), rootId)) {
int d = 1 + maxDescendantRelativeDepth(m.getId(), all);
if (d > max) {
max = d;
}
}
}
return max;
}
/**
* 将指定节点所有后代的 level 增加 delta。<br>
*/
private void shiftDescendantLevels(Integer rootId, int delta, List<AppModule> all) {
for (AppModule m : all) {
if (Objects.equals(m.getParentId(), rootId)) {
m.setLevel(m.getLevel() + delta);
shiftDescendantLevels(m.getId(), delta, all);
}
}
}
private AppModuleVo convertToVo(AppModule entity) {
AppModuleVo vo = new AppModuleVo();
BeanUtils.copyProperties(entity, vo);
return vo;
}
}
@@ -0,0 +1,499 @@
package com.yunhai.userbase.service.impl;
import com.bestsch.utils.DException;
import com.yunhai.userbase.entity.AppModule;
import com.yunhai.userbase.entity.ApplicationInfo;
import com.yunhai.userbase.entity.SysAppRole;
import com.yunhai.userbase.entity.SysAppRoleModuleRel;
import com.yunhai.userbase.entity.SysUser;
import com.yunhai.userbase.entity.SysUserAppRoleRel;
import com.yunhai.userbase.model.approle.AppRoleAuthzAppVo;
import com.yunhai.userbase.model.approle.AppRoleAuthzModuleVo;
import com.yunhai.userbase.model.approle.AppRoleModuleAssignRequest;
import com.yunhai.userbase.model.approle.AppRoleUserBindRequest;
import com.yunhai.userbase.model.approle.AppRoleUserVo;
import com.yunhai.userbase.model.approle.SysAppRoleQuery;
import com.yunhai.userbase.model.approle.SysAppRoleRequest;
import com.yunhai.userbase.model.approle.SysAppRoleVo;
import com.yunhai.userbase.model.approle.UserAppRoleBindRequest;
import com.yunhai.userbase.repository.AppModuleRepository;
import com.yunhai.userbase.repository.ApplicationInfoRepository;
import com.yunhai.userbase.repository.SysAppRoleModuleRelRepository;
import com.yunhai.userbase.repository.SysAppRoleRepository;
import com.yunhai.userbase.repository.SysUserAppRoleRelRepository;
import com.yunhai.userbase.repository.SysUserRepository;
import com.yunhai.userbase.service.IAppRoleService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.transaction.Transactional;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* @author chenx
* @date 2026/7/27
* @description 应用业务角色服务实现。<br>
* 角色创建:普通角色(allAppFlag=false,appId 必填,同应用下重名校验)/ 全应用角色(allAppFlag=true,appId 为空,全应用角色间重名校验)。<br>
* 模块授权:sys_app_role_module_rel 全量覆盖式保存(bulk 先删后插);普通角色仅能授权本应用模块,全应用角色可跨应用。<br>
* 用户绑定:sys_user_app_role_rel 支持角色维度 / 用户维度两个方向的全量覆盖保存。<br>
*/
@Service
public class AppRoleService implements IAppRoleService {
@Autowired
private SysAppRoleRepository sysAppRoleRepository;
@Autowired
private SysAppRoleModuleRelRepository sysAppRoleModuleRelRepository;
@Autowired
private SysUserAppRoleRelRepository sysUserAppRoleRelRepository;
@Autowired
private ApplicationInfoRepository applicationInfoRepository;
@Autowired
private AppModuleRepository appModuleRepository;
@Autowired
private SysUserRepository sysUserRepository;
// ===================== 角色 CRUD =====================
@Override
public Page<SysAppRoleVo> list(SysAppRoleQuery query, Pageable pageable) {
Objects.requireNonNull(query, "角色查询条件不能为空");
Objects.requireNonNull(pageable, "分页参数不能为空");
Page<SysAppRole> page = sysAppRoleRepository.findAll(select(query), pageable);
// 批量取应用名称,避免逐条查询
Set<Integer> appIds = new LinkedHashSet<>();
for (SysAppRole role : page.getContent()) {
if (role.getAppId() != null) {
appIds.add(role.getAppId());
}
}
Map<Integer, String> appNameMap = buildAppNameMap(appIds);
List<SysAppRoleVo> content = new ArrayList<SysAppRoleVo>();
for (SysAppRole role : page.getContent()) {
content.add(convertToVo(role, appNameMap));
}
return new PageImpl<>(content, pageable, page.getTotalElements());
}
@Override
@Transactional
public void add(SysAppRoleRequest request) {
Objects.requireNonNull(request, "角色新增入参不能为空");
boolean allAppFlag = Boolean.TRUE.equals(request.getAllAppFlag());
checkRoleShape(allAppFlag, request.getAppId());
// 重名校验:全应用角色之间唯一;普通角色同应用下唯一
if (allAppFlag) {
if (sysAppRoleRepository.existsByNameAndAllAppFlagTrue(request.getName())) {
throw new DException("全应用角色名称已存在");
}
} else {
if (sysAppRoleRepository.existsByNameAndAppId(request.getName(), request.getAppId())) {
throw new DException("该应用下角色名称已存在");
}
}
SysAppRole entity = new SysAppRole();
entity.setName(request.getName());
entity.setAllAppFlag(allAppFlag);
// 全应用角色强制 appId 为空,防止脏数据
entity.setAppId(allAppFlag ? null : request.getAppId());
entity.setRemark(request.getRemark());
entity.setDisabled(Boolean.TRUE.equals(request.getDisabled()));
sysAppRoleRepository.save(entity);
}
@Override
@Transactional
public void edit(Integer id, SysAppRoleRequest request) {
Objects.requireNonNull(id, "角色ID不能为空");
Objects.requireNonNull(request, "角色编辑入参不能为空");
SysAppRole entity = sysAppRoleRepository.findById(id)
.orElseThrow(() -> new RuntimeException("角色不存在: " + id));
// 角色类型与所属应用不允许修改:修改会导致已授权模块越界(普通角色授权范围与 appId 绑定)
if (!Objects.equals(entity.getAllAppFlag(), Boolean.TRUE.equals(request.getAllAppFlag()))
|| !Objects.equals(entity.getAppId(), request.getAppId())) {
throw new DException("角色类型与所属应用不允许修改,请删除后重建");
}
// 重名校验(排除自身)
if (Boolean.TRUE.equals(entity.getAllAppFlag())) {
if (sysAppRoleRepository.existsByNameAndAllAppFlagTrueAndIdNot(request.getName(), id)) {
throw new DException("全应用角色名称已存在");
}
} else {
if (sysAppRoleRepository.existsByNameAndAppIdAndIdNot(request.getName(), entity.getAppId(), id)) {
throw new DException("该应用下角色名称已存在");
}
}
entity.setName(request.getName());
entity.setRemark(request.getRemark());
if (request.getDisabled() != null) {
entity.setDisabled(request.getDisabled());
}
sysAppRoleRepository.save(entity);
}
@Override
@Transactional
public void delete(Integer id) {
Objects.requireNonNull(id, "角色ID不能为空");
if (!sysAppRoleRepository.existsById(id)) {
throw new RuntimeException("角色不存在: " + id);
}
// 逻辑外键级联:先清模块授权与用户绑定,再删角色本体
sysAppRoleModuleRelRepository.deleteByRoleIdBulk(id);
sysUserAppRoleRelRepository.deleteByRoleIdBulk(id);
sysAppRoleRepository.deleteById(id);
}
// ===================== 角色模块授权 =====================
@Override
public List<AppRoleAuthzAppVo> findRoleModuleTree(Integer roleId) {
Objects.requireNonNull(roleId, "角色ID不能为空");
SysAppRole role = sysAppRoleRepository.findById(roleId)
.orElseThrow(() -> new RuntimeException("角色不存在: " + roleId));
// 授权范围:普通角色仅本应用;全应用角色为系统全部应用
List<ApplicationInfo> apps;
if (Boolean.TRUE.equals(role.getAllAppFlag())) {
apps = applicationInfoRepository.findAll();
} else {
ApplicationInfo app = applicationInfoRepository.findById(role.getAppId()).orElse(null);
if (app == null) {
throw new DException("角色所属应用不存在: " + role.getAppId());
}
apps = Collections.singletonList(app);
}
apps.sort(appOrder());
List<Integer> appIds = new ArrayList<>();
for (ApplicationInfo app : apps) {
appIds.add(app.getId());
}
// 应用下全部模块,按 appId 分桶
Map<Integer, List<AppModule>> appModuleMap = new HashMap<>();
for (AppModule m : appModuleRepository.findByAppIdIn(appIds)) {
appModuleMap.computeIfAbsent(m.getAppId(), k -> new ArrayList<>()).add(m);
}
// 已授权模块集合,供 checked 回显
Set<Integer> checkedModuleIds = new HashSet<>();
for (SysAppRoleModuleRel rel : sysAppRoleModuleRelRepository.findByRoleId(roleId)) {
checkedModuleIds.add(rel.getModuleId());
}
List<AppRoleAuthzAppVo> result = new ArrayList<>();
for (ApplicationInfo app : apps) {
AppRoleAuthzAppVo appVo = new AppRoleAuthzAppVo();
BeanUtils.copyProperties(app, appVo);
List<AppModule> modules = appModuleMap.get(app.getId());
if (modules != null) {
appVo.setModules(buildModuleTree(modules, checkedModuleIds));
}
result.add(appVo);
}
return result;
}
@Override
@Transactional
public void saveRoleModules(AppRoleModuleAssignRequest request) {
Objects.requireNonNull(request, "角色模块授权入参不能为空");
Integer roleId = request.getRoleId();
SysAppRole role = sysAppRoleRepository.findById(roleId)
.orElseThrow(() -> new RuntimeException("角色不存在: " + roleId));
// 去重并校验模块均存在
List<Integer> moduleIds = new ArrayList<>(new LinkedHashSet<>(
request.getModuleIds() == null ? Collections.emptyList() : request.getModuleIds()));
if (!moduleIds.isEmpty()) {
Map<Integer, AppModule> moduleMap = new HashMap<>();
for (AppModule m : appModuleRepository.findAllById(moduleIds)) {
moduleMap.put(m.getId(), m);
}
for (Integer moduleId : moduleIds) {
AppModule m = moduleMap.get(moduleId);
if (m == null) {
throw new DException("模块不存在: " + moduleId);
}
// 越权防护:普通角色只能授权其所属应用下的模块;全应用角色可跨应用
if (!Boolean.TRUE.equals(role.getAllAppFlag()) && !Objects.equals(m.getAppId(), role.getAppId())) {
throw new DException("模块 " + moduleId + " 不属于该角色所属应用,普通角色不能跨应用授权");
}
}
}
// 全量覆盖:bulk 先删旧记录(立即发 SQL,避免 flush 顺序触发唯一键冲突),再批量插入
sysAppRoleModuleRelRepository.deleteByRoleIdBulk(roleId);
List<SysAppRoleModuleRel> batch = new ArrayList<>();
for (Integer moduleId : moduleIds) {
SysAppRoleModuleRel rel = new SysAppRoleModuleRel();
rel.setRoleId(roleId);
rel.setModuleId(moduleId);
batch.add(rel);
}
if (!batch.isEmpty()) {
sysAppRoleModuleRelRepository.saveAll(batch);
}
}
// ===================== 用户角色绑定 =====================
@Override
public List<AppRoleUserVo> findRoleUsers(Integer roleId) {
Objects.requireNonNull(roleId, "角色ID不能为空");
if (!sysAppRoleRepository.existsById(roleId)) {
throw new DException("角色不存在: " + roleId);
}
List<SysUserAppRoleRel> rels = sysUserAppRoleRelRepository.findByRoleId(roleId);
if (rels.isEmpty()) {
return Collections.emptyList();
}
Set<Integer> userIds = new LinkedHashSet<>();
for (SysUserAppRoleRel rel : rels) {
userIds.add(rel.getUserId());
}
Map<Integer, SysUser> userMap = new HashMap<>();
for (SysUser user : sysUserRepository.findAllById(userIds)) {
userMap.put(user.getId(), user);
}
List<AppRoleUserVo> result = new ArrayList<>();
for (SysUserAppRoleRel rel : rels) {
SysUser user = userMap.get(rel.getUserId());
if (user == null) {
continue;
}
AppRoleUserVo vo = new AppRoleUserVo();
vo.setId(user.getId());
vo.setAccount(user.getAccount());
vo.setRealName(user.getRealName());
result.add(vo);
}
return result;
}
@Override
@Transactional
public void saveRoleUsers(AppRoleUserBindRequest request) {
Objects.requireNonNull(request, "角色绑定用户入参不能为空");
Integer roleId = request.getRoleId();
if (!sysAppRoleRepository.existsById(roleId)) {
throw new DException("角色不存在: " + roleId);
}
// 去重并校验用户均存在
List<Integer> userIds = new ArrayList<>(new LinkedHashSet<>(
request.getUserIds() == null ? Collections.emptyList() : request.getUserIds()));
for (Integer userId : userIds) {
if (!sysUserRepository.existsById(userId)) {
throw new DException("用户不存在: " + userId);
}
}
// 全量覆盖:bulk 先删旧绑定,再批量插入
sysUserAppRoleRelRepository.deleteByRoleIdBulk(roleId);
List<SysUserAppRoleRel> batch = new ArrayList<>();
for (Integer userId : userIds) {
SysUserAppRoleRel rel = new SysUserAppRoleRel();
rel.setUserId(userId);
rel.setRoleId(roleId);
batch.add(rel);
}
if (!batch.isEmpty()) {
sysUserAppRoleRelRepository.saveAll(batch);
}
}
@Override
@Transactional
public void saveUserRoles(UserAppRoleBindRequest request) {
Objects.requireNonNull(request, "用户绑定角色入参不能为空");
Integer userId = request.getUserId();
if (!sysUserRepository.existsById(userId)) {
throw new DException("用户不存在: " + userId);
}
// 去重并校验角色均存在
List<Integer> roleIds = new ArrayList<>(new LinkedHashSet<>(
request.getRoleIds() == null ? Collections.emptyList() : request.getRoleIds()));
for (Integer roleId : roleIds) {
if (!sysAppRoleRepository.existsById(roleId)) {
throw new DException("角色不存在: " + roleId);
}
}
// 全量覆盖:bulk 先删旧绑定,再批量插入
sysUserAppRoleRelRepository.deleteByUserIdBulk(userId);
List<SysUserAppRoleRel> batch = new ArrayList<>();
for (Integer roleId : roleIds) {
SysUserAppRoleRel rel = new SysUserAppRoleRel();
rel.setUserId(userId);
rel.setRoleId(roleId);
batch.add(rel);
}
if (!batch.isEmpty()) {
sysUserAppRoleRelRepository.saveAll(batch);
}
}
@Override
@Transactional
public void removeRoleUser(Integer roleId, Integer userId) {
Objects.requireNonNull(roleId, "角色ID不能为空");
Objects.requireNonNull(userId, "用户ID不能为空");
if (!sysAppRoleRepository.existsById(roleId)) {
throw new DException("角色不存在: " + roleId);
}
if (!sysUserRepository.existsById(userId)) {
throw new DException("用户不存在: " + userId);
}
sysUserAppRoleRelRepository.deleteByRoleIdAndUserId(roleId, userId);
}
// ===================== 私有工具 =====================
/**
* 校验角色形态:普通角色 appId 必填且应用存在;全应用角色 appId 必须为空。<br>
*/
private void checkRoleShape(boolean allAppFlag, Integer appId) {
if (allAppFlag) {
if (appId != null) {
throw new DException("全应用角色不能指定所属应用");
}
} else {
if (appId == null) {
throw new DException("普通角色必须指定所属应用");
}
if (!applicationInfoRepository.existsById(appId)) {
throw new DException("所属应用不存在: " + appId);
}
}
}
/**
* 构建角色列表的动态查询条件(Tier 1 Specification)。<br>
* name 模糊匹配;appId/allAppFlag/disabled 精确匹配。<br>
*/
private Specification<SysAppRole> select(SysAppRoleQuery query) {
Specification<SysAppRole> specification = new Specification<SysAppRole>() {
@Override
public Predicate toPredicate(Root<SysAppRole> root, CriteriaQuery<?> cq, CriteriaBuilder cb) {
List<Predicate> predicates = new ArrayList<Predicate>();
if (query != null) {
if (StringUtils.isNotEmpty(query.getName())) {
predicates.add(cb.like(root.get("name"), "%" + query.getName() + "%"));
}
if (query.getAppId() != null) {
predicates.add(cb.equal(root.get("appId"), query.getAppId()));
}
if (query.getAllAppFlag() != null) {
predicates.add(cb.equal(root.get("allAppFlag"), query.getAllAppFlag()));
}
if (query.getDisabled() != null) {
predicates.add(cb.equal(root.get("disabled"), query.getDisabled()));
}
}
return cq.where(predicates.toArray(new Predicate[predicates.size()])).getRestriction();
}
};
return specification;
}
/**
* 批量查询应用名称映射(角色列表回填 appName 用)。<br>
*/
private Map<Integer, String> buildAppNameMap(Set<Integer> appIds) {
Map<Integer, String> map = new HashMap<>();
if (appIds.isEmpty()) {
return map;
}
for (ApplicationInfo app : applicationInfoRepository.findAllById(appIds)) {
map.put(app.getId(), app.getName());
}
return map;
}
private SysAppRoleVo convertToVo(SysAppRole entity, Map<Integer, String> appNameMap) {
SysAppRoleVo vo = new SysAppRoleVo();
BeanUtils.copyProperties(entity, vo);
if (entity.getAppId() != null) {
vo.setAppName(appNameMap.get(entity.getAppId()));
}
return vo;
}
/**
* 组装单个应用下的模块树(沿用 GroupAuthzService 的组装方式:childrenMap + sortNo 排序),并回显 checked。<br>
*/
private List<AppRoleAuthzModuleVo> buildModuleTree(List<AppModule> modules, Set<Integer> checkedModuleIds) {
// parentId -> 子节点列表(null 视为 0)
Map<Integer, List<AppModule>> childrenMap = new HashMap<>();
for (AppModule m : modules) {
Integer key = (m.getParentId() == null) ? 0 : m.getParentId();
childrenMap.computeIfAbsent(key, k -> new ArrayList<>()).add(m);
}
// 每个兄弟组按 sortNo(空视为999) 升序,再按 id 升序
Comparator<AppModule> siblingOrder = (a, b) -> {
int sa = a.getSortNo() == null ? 999 : a.getSortNo();
int sb = b.getSortNo() == null ? 999 : b.getSortNo();
if (sa != sb) {
return Integer.compare(sa, sb);
}
return Integer.compare(a.getId(), b.getId());
};
for (List<AppModule> siblings : childrenMap.values()) {
siblings.sort(siblingOrder);
}
List<AppModule> roots = childrenMap.getOrDefault(0, Collections.emptyList());
List<AppRoleAuthzModuleVo> tree = new ArrayList<>();
for (AppModule root : roots) {
tree.add(buildModuleNode(root, childrenMap, checkedModuleIds));
}
return tree;
}
/**
* 递归组装模块节点,并按角色授权记录回显 checked。<br>
*/
private AppRoleAuthzModuleVo buildModuleNode(AppModule node,
Map<Integer, List<AppModule>> childrenMap,
Set<Integer> checkedModuleIds) {
AppRoleAuthzModuleVo vo = new AppRoleAuthzModuleVo();
BeanUtils.copyProperties(node, vo);
if (checkedModuleIds.contains(node.getId())) {
vo.setChecked(Boolean.TRUE);
}
List<AppModule> children = childrenMap.get(node.getId());
if (children != null) {
for (AppModule child : children) {
vo.getChildren().add(buildModuleNode(child, childrenMap, checkedModuleIds));
}
}
return vo;
}
/** 应用排序:sortNo 升序(空视为999),再按 id 升序(与应用列表展示一致)。 */
private Comparator<ApplicationInfo> appOrder() {
return (a, b) -> {
int sa = a.getSortNo() == null ? 999 : a.getSortNo();
int sb = b.getSortNo() == null ? 999 : b.getSortNo();
if (sa != sb) {
return Integer.compare(sa, sb);
}
return Integer.compare(a.getId(), b.getId());
};
}
}
@@ -0,0 +1,259 @@
package com.yunhai.userbase.service.impl;
import com.yunhai.userbase.entity.AppModule;
import com.yunhai.userbase.entity.ApplicationInfo;
import com.yunhai.userbase.entity.SysAppRole;
import com.yunhai.userbase.repository.AppModuleRepository;
import com.yunhai.userbase.repository.ApplicationInfoRepository;
import com.yunhai.userbase.repository.SysAppRoleModuleRelRepository;
import com.yunhai.userbase.repository.SysAppRoleRepository;
import com.yunhai.userbase.repository.SysGroupAppRelRepository;
import com.yunhai.userbase.repository.SysGroupModulePermRepository;
import com.yunhai.userbase.repository.SysUserAppRoleRelRepository;
import com.yunhai.userbase.service.IApplicationInfoService;
import com.yunhai.userbase.service.SignKeyCacheService;
import com.bestsch.utils.DException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import com.yunhai.userbase.model.application.ApplicationInfoQuery;
import com.yunhai.userbase.model.application.ApplicationInfoRequest;
import com.yunhai.userbase.model.application.ApplicationInfoSortVo;
import com.yunhai.userbase.model.application.ApplicationInfoVo;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.transaction.Transactional;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
@Service
public class ApplicationInfoService implements IApplicationInfoService {
@Autowired
private ApplicationInfoRepository applicationInfoRepository;
@Autowired
private AppModuleRepository appModuleRepository;
@Autowired
private SysGroupAppRelRepository sysGroupAppRelRepository;
@Autowired
private SysGroupModulePermRepository sysGroupModulePermRepository;
@Autowired
private SysAppRoleRepository sysAppRoleRepository;
@Autowired
private SysAppRoleModuleRelRepository sysAppRoleModuleRelRepository;
@Autowired
private SysUserAppRoleRelRepository sysUserAppRoleRelRepository;
@Autowired
private SignKeyCacheService signKeyCacheService;
/**
* 查询缓存:sync = true 防击穿;底层优先 Redis,Redis 故障时自动降级 Caffeine。
*/
@Override
@Cacheable(value = "appInfo", key = "#id", sync = true)
public ApplicationInfo getById(Integer id) {
return applicationInfoRepository.findById(id).orElse(null);
}
/**
* 更新缓存:方法返回即写入缓存(与 @Cacheable 同一 cacheName/key,保证一致)。
*/
@Override
@CachePut(value = "appInfo", key = "#info.id")
public ApplicationInfo save(ApplicationInfo info) {
return applicationInfoRepository.save(info);
}
/**
* 删除缓存:删除数据时同步清除对应缓存项。
*/
@Override
@CacheEvict(value = "appInfo", key = "#id")
public void deleteById(Integer id) {
applicationInfoRepository.deleteById(id);
}
@Override
@Transactional
public void add(ApplicationInfoRequest request) {
Objects.requireNonNull(request, "应用新增入参不能为空");
// 重名校验:新增应用名称不允许重复
if (applicationInfoRepository.existsByName(request.getName())) {
throw new DException("应用名称已存在");
}
ApplicationInfo entity = new ApplicationInfo();
BeanUtils.copyProperties(request, entity);
entity.setId(null);
// appKey:整型,从 100000 起,取数据库现有最大 appKey + 1
Integer maxAppKey = applicationInfoRepository.findMaxAppKey();
Integer appKey = (maxAppKey == null) ? 100000 : maxAppKey + 1;
entity.setAppKey(appKey);
// appSecret:32 位 UUID(去除连字符)
entity.setAppSecret(UUID.randomUUID().toString().replace("-", ""));
ApplicationInfo saved = applicationInfoRepository.save(entity);
signKeyCacheService.syncOne(saved);
}
@Override
@Transactional
public void edit(Integer id, ApplicationInfoRequest request) {
Objects.requireNonNull(id, "应用ID不能为空");
Objects.requireNonNull(request, "应用编辑入参不能为空");
// 重名校验:编辑时排除自身
if (applicationInfoRepository.existsByNameAndIdNot(request.getName(), id)) {
throw new DException("应用名称已存在");
}
ApplicationInfo entity = applicationInfoRepository.findById(id)
.orElseThrow(() -> new RuntimeException("应用不存在: " + id));
BeanUtils.copyProperties(request, entity, "id", "appKey", "appSecret");
ApplicationInfo saved = applicationInfoRepository.save(entity);
signKeyCacheService.syncOne(saved);
}
@Override
@Transactional
// AI-FIX: 删除应用同时级联删除其下全部模块(逻辑外键 appId,无物理 FK,需手动清理),并清除 appInfo 缓存
@CacheEvict(value = "appInfo", key = "#id")
public void delete(Integer id) {
Objects.requireNonNull(id, "应用ID不能为空");
ApplicationInfo existing = applicationInfoRepository.findById(id)
.orElseThrow(() -> new RuntimeException("应用不存在: " + id));
Integer appKey = existing.getAppKey();
// AI-FIX: 删除应用前级联清理组权限关联(逻辑外键无物理 FK):先清该应用模块的组权限记录,再清组开通记录
List<AppModule> modules = appModuleRepository.findByAppIdIn(java.util.Collections.singletonList(id));
if (!modules.isEmpty()) {
List<Integer> moduleIds = new ArrayList<Integer>();
for (AppModule m : modules) {
moduleIds.add(m.getId());
}
sysGroupModulePermRepository.deleteByModuleIdInBulk(moduleIds);
}
sysGroupAppRelRepository.deleteByAppIdBulk(id);
// AI-FIX(角色授权): 删除应用前级联清理该应用下的业务角色及其授权关系(逻辑外键无物理 FK)
// 仅清理「普通角色」(appId=本应用);全应用角色(appId=null)不受应用删除影响,保留不动
List<SysAppRole> appRoles = sysAppRoleRepository.findByAppId(id);
if (!appRoles.isEmpty()) {
List<Integer> roleIds = new ArrayList<Integer>();
for (SysAppRole r : appRoles) {
roleIds.add(r.getId());
}
sysAppRoleModuleRelRepository.deleteByRoleIdInBulk(roleIds);
sysUserAppRoleRelRepository.deleteByRoleIdInBulk(roleIds);
sysAppRoleRepository.deleteAll(appRoles);
}
// 先删模块(整棵子树随 appId 一并清除),再删应用,避免残留孤儿数据
appModuleRepository.deleteByAppId(id);
applicationInfoRepository.deleteById(id);
signKeyCacheService.removeOne(appKey);
}
@Override
public Page<ApplicationInfoVo> list(ApplicationInfoQuery query, Pageable pageable) {
Objects.requireNonNull(query, "应用查询条件不能为空");
Objects.requireNonNull(pageable, "分页参数不能为空");
Page<ApplicationInfo> page = applicationInfoRepository.findAll(select(query), pageable);
List<ApplicationInfoVo> content = new ArrayList<ApplicationInfoVo>();
for (ApplicationInfo entity : page.getContent()) {
content.add(convertToVo(entity));
}
return new PageImpl<>(content, pageable, page.getTotalElements());
}
/**
* 构建应用列表的动态查询条件。<br>
* name 关键词:按应用名称模糊匹配,或按 appKey(appId)精确匹配;categoryId:按应用类别精确匹配。<br>
*
* @param query 前端传入的查询条件
* @return 动态查询 Specification
*/
private Specification select(ApplicationInfoQuery query) {
Specification<ApplicationInfo> specification = new Specification<ApplicationInfo>() {
@Override
public Predicate toPredicate(Root<ApplicationInfo> root, CriteriaQuery<?> cq, CriteriaBuilder cb) {
List<Predicate> predicates = new ArrayList<Predicate>();
if (query != null) {
if (StringUtils.isNotEmpty(query.getName())) {
String keyword = query.getName();
Predicate byName = cb.like(root.get("name"), "%" + keyword + "%");
// 关键词为纯数字时,同时按 appKey(appId)精确匹配
if (keyword.matches("\\d+")) {
predicates.add(cb.or(byName, cb.equal(root.get("appKey"), Integer.parseInt(keyword))));
} else {
predicates.add(byName);
}
}
if (query.getCategoryId() != null) {
predicates.add(cb.equal(root.get("categoryId"), query.getCategoryId()));
}
}
return cq.where(predicates.toArray(new Predicate[predicates.size()])).getRestriction();
}
};
return specification;
}
@Override
@Transactional
// AI-FIX: 重置密钥后必须清除 appInfo 缓存,否则控制器随后 getById 命中旧缓存、返回旧密钥
@CacheEvict(value = "appInfo", key = "#id")
public void resetAppSecret(Integer id) {
Objects.requireNonNull(id, "应用ID不能为空");
ApplicationInfo entity = applicationInfoRepository.findById(id)
.orElseThrow(() -> new RuntimeException("应用不存在: " + id));
entity.setAppSecret(UUID.randomUUID().toString().replace("-", ""));
ApplicationInfo saved = applicationInfoRepository.save(entity);
signKeyCacheService.syncOne(saved);
}
@Override
@Transactional
public void sort(List<Integer> ids) {
if (ids == null || ids.isEmpty()) {
throw new DException("排序应用ID列表不能为空");
}
Integer categoryId = null;
for (int i = 0; i < ids.size(); i++) {
Integer id = ids.get(i);
ApplicationInfo entity = applicationInfoRepository.findById(id)
.orElseThrow(() -> new DException("应用不存在: " + id));
// 额外校验:排序必须限定在同一个应用类型内,避免跨类型串号导致排序错乱
if (categoryId == null) {
categoryId = entity.getCategoryId();
} else if (!Objects.equals(categoryId, entity.getCategoryId())) {
throw new DException("排序应用不属于同一应用类型,请检查入参");
}
applicationInfoRepository.updateSortNoById(id, i + 1);
}
}
private ApplicationInfoVo convertToVo(ApplicationInfo entity) {
ApplicationInfoVo vo = new ApplicationInfoVo();
BeanUtils.copyProperties(entity, vo);
return vo;
}
@Override
public List<ApplicationInfoSortVo> findListByCategoryId(Integer categoryId) {
return applicationInfoRepository.findSortVoByCategoryId(categoryId);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,403 @@
package com.yunhai.userbase.service.impl;
import com.bestsch.utils.DException;
import com.yunhai.userbase.entity.SysGroup;
import com.yunhai.userbase.enums.GroupTypeEnum;
import com.yunhai.userbase.enums.IEnum;
import com.yunhai.userbase.model.sysgroup.SysGroupBatchRequest;
import com.yunhai.userbase.model.sysgroup.SysGroupMoveRequest;
import com.yunhai.userbase.model.sysgroup.SysGroupQuery;
import com.yunhai.userbase.model.sysgroup.SysGroupRequest;
import com.yunhai.userbase.model.sysgroup.SysGroupSortRequest;
import com.yunhai.userbase.model.sysgroup.SysGroupVo;
import com.yunhai.userbase.repository.SysGroupAppRelRepository;
import com.yunhai.userbase.repository.SysGroupModulePermRepository;
import com.yunhai.userbase.repository.SysGroupRepository;
import com.yunhai.userbase.repository.SysUserGroupRelRepository;
import com.yunhai.userbase.service.ISysGroupService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import javax.transaction.Transactional;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @author chenx
* @date 2026/7/20 16:00
* @description 系统组服务实现:组织架构树(仅 parentId 逻辑外键,不使用 path;后续可用 MySQL8 WITH 优化递归查询)。<br>
*/
@Service
public class SysGroupService implements ISysGroupService {
/** 虚拟父节点:根节点(教育云中心)的 parentId,表示「无上级」。 */
private static final int ROOT_PARENT_ID = 0;
@Autowired
private SysGroupRepository sysGroupRepository;
@Autowired
private SysUserGroupRelRepository sysUserGroupRelRepository;
@Autowired
private SysGroupAppRelRepository sysGroupAppRelRepository;
@Autowired
private SysGroupModulePermRepository sysGroupModulePermRepository;
// ===================== 基础增删改(含根节点保护) =====================
@Override
@Transactional
public void add(SysGroupRequest request) {
Integer parentId = request.getParentId();
// 根节点由系统自动维护,不允许手动新增一个 pid=0 的组
if (parentId == null || parentId == ROOT_PARENT_ID) {
throw new DException("根节点由系统自动创建,不可手动添加");
}
if (!sysGroupRepository.existsById(parentId)) {
throw new DException("父组不存在");
}
GroupTypeEnum typeEnum = resolveType(request.getType());
SysGroup entity = new SysGroup();
entity.setName(request.getName());
entity.setParentId(parentId);
entity.setType(typeEnum);
entity.setSort(nextSort(parentId));
sysGroupRepository.save(entity);
}
@Override
@Transactional
public void edit(Integer id, SysGroupRequest request) {
SysGroup entity = sysGroupRepository.findById(id)
.orElseThrow(() -> new DException("系统组不存在"));
// 根节点不可编辑
if (isRoot(entity)) {
throw new DException("根节点不可编辑");
}
entity.setName(request.getName());
if (request.getType() != null) {
entity.setType(resolveType(request.getType()));
}
if (request.getSort() != null) {
entity.setSort(request.getSort());
}
// 注:移动请走 move 接口,edit 不改 parentId,避免与移动逻辑混淆
sysGroupRepository.save(entity);
}
@Override
@Transactional
public void delete(Integer id) {
SysGroup entity = sysGroupRepository.findById(id)
.orElseThrow(() -> new DException("系统组不存在"));
// 根节点不可删除
if (isRoot(entity)) {
throw new DException("根节点不可删除");
}
if (sysGroupRepository.countByParentId(id) > 0) {
throw new DException("该组下存在子组,无法删除");
}
if (sysUserGroupRelRepository.countByGroupId(id) > 0) {
throw new DException("有用户,不能删除组");
}
// AI-FIX: 删除组时级联清理其应用开通记录与模块权限记录(逻辑外键无物理 FK,避免残留孤儿数据)
sysGroupAppRelRepository.deleteByGroupIdBulk(id);
sysGroupModulePermRepository.deleteByGroupIdBulk(id);
sysGroupRepository.deleteById(id);
}
@Override
public List<SysGroupVo> list(SysGroupQuery query) {
List<SysGroup> list;
if (StringUtils.hasText(query.getName())) {
list = sysGroupRepository.findByNameContainingOrderBySortAsc(query.getName());
} else if (query.getParentId() != null) {
list = sysGroupRepository.findByParentIdOrderBySortAsc(query.getParentId());
} else {
list = sysGroupRepository.findAllByOrderBySortAsc();
}
return list.stream().map(this::convertToVo).collect(Collectors.toList());
}
// ===================== 组织架构树 =====================
@Override
public List<SysGroupVo> getTree() {
// 根节点由 SysGroupRootInitializer 在启动时幂等创建,此处直接读取组装
List<SysGroup> all = sysGroupRepository.findAll();
if (all.isEmpty()) {
return new ArrayList<>();
}
Map<Integer, List<SysGroup>> childrenMap = buildSortedChildrenMap(all);
// 从根(pid=0)开始组装树
List<SysGroup> roots = childrenMap.getOrDefault(ROOT_PARENT_ID, new ArrayList<>());
return roots.stream().map(r -> buildNode(r, childrenMap)).collect(Collectors.toList());
}
@Override
public List<SysGroupVo> getSchoolTree() {
List<SysGroup> all = sysGroupRepository.findAll();
if (all.isEmpty()) {
return new ArrayList<>();
}
Map<Integer, List<SysGroup>> childrenMap = buildSortedChildrenMap(all);
List<SysGroup> roots = childrenMap.getOrDefault(ROOT_PARENT_ID, new ArrayList<>());
List<SysGroupVo> result = new ArrayList<>();
for (SysGroup root : roots) {
SysGroupVo vo = buildSchoolNode(root, childrenMap);
if (vo == null) {
// 根节点始终保留(即使其下没有任何学校组),children 为空
vo = convertToVo(root);
}
result.add(vo);
}
return result;
}
/**
* 递归组装「学校组」剪枝节点。<br>
* 学校组:直接作为叶子返回(不再向下递归,其下所有组不显示);<br>
* 非学校组:递归剪枝后仍有子节点才保留,否则返回 null(整枝剪掉)。<br>
*/
private SysGroupVo buildSchoolNode(SysGroup node, Map<Integer, List<SysGroup>> childrenMap) {
if (node.getType() == GroupTypeEnum.SCHOOL_GROUP) {
return convertToVo(node);
}
List<SysGroup> children = childrenMap.get(node.getId());
if (children == null) {
return null;
}
SysGroupVo vo = convertToVo(node);
for (SysGroup child : children) {
SysGroupVo childVo = buildSchoolNode(child, childrenMap);
if (childVo != null) {
vo.getChildren().add(childVo);
}
}
return vo.getChildren().isEmpty() ? null : vo;
}
/**
* 构建 parentId -> 直接子节点列表 的映射(null 视为 0),同层级按 sort 升序、再按 id 升序。<br>
*/
private Map<Integer, List<SysGroup>> buildSortedChildrenMap(List<SysGroup> all) {
Map<Integer, List<SysGroup>> childrenMap = new HashMap<>();
for (SysGroup g : all) {
Integer key = (g.getParentId() == null) ? ROOT_PARENT_ID : g.getParentId();
childrenMap.computeIfAbsent(key, k -> new ArrayList<>()).add(g);
}
Comparator<SysGroup> siblingOrder = (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(siblingOrder);
}
return childrenMap;
}
private SysGroupVo buildNode(SysGroup node, Map<Integer, List<SysGroup>> childrenMap) {
SysGroupVo vo = convertToVo(node);
List<SysGroup> children = childrenMap.get(node.getId());
if (children != null) {
for (SysGroup child : children) {
vo.getChildren().add(buildNode(child, childrenMap));
}
}
return vo;
}
// ===================== 批量新增 =====================
@Override
@Transactional
public void batchAdd(SysGroupBatchRequest request) {
Integer pid = request.getPid();
if (pid == null || !sysGroupRepository.existsById(pid)) {
throw new DException("父组不存在");
}
GroupTypeEnum typeEnum = resolveType(request.getType());
List<SysGroupBatchRequest.BatchItem> items = request.getItems();
if (items == null || items.isEmpty()) {
throw new DException("批量新增的子组列表不能为空");
}
// 追加到当前父组子节点之后
int base = nextSort(pid);
List<SysGroup> batch = new ArrayList<>();
for (int i = 0; i < items.size(); i++) {
String name = items.get(i).getName();
if (name == null || name.trim().isEmpty()) {
throw new DException("第 " + (i + 1) + " 个子组名称为空");
}
SysGroup g = new SysGroup();
g.setName(name.trim());
g.setParentId(pid);
g.setType(typeEnum);
g.setSort(base + i);
batch.add(g);
}
sysGroupRepository.saveAll(batch);
}
// ===================== 子组排序 =====================
@Override
@Transactional
public void sortChildren(SysGroupSortRequest request) {
Integer pid = request.getPid();
List<Integer> ids = request.getIds();
if (pid == null || !sysGroupRepository.existsById(pid)) {
throw new DException("父组不存在");
}
if (ids == null || ids.isEmpty()) {
throw new DException("子节点ID列表不能为空");
}
if (new HashSet<>(ids).size() != ids.size()) {
throw new DException("子节点ID列表存在重复");
}
// 取出 pid 的直接子节点,校验 ids 全是其直接子节点
List<SysGroup> children = sysGroupRepository.findByParentIdOrderBySortAsc(pid);
Set<Integer> childIdSet = children.stream().map(SysGroup::getId).collect(Collectors.toSet());
for (Integer id : ids) {
if (!childIdSet.contains(id)) {
throw new DException("ids 中存在不属于该父组的直接子节点");
}
}
// 按传入顺序落库排序(1 起)
Map<Integer, SysGroup> childMap = children.stream()
.collect(Collectors.toMap(SysGroup::getId, c -> c, (a, b) -> a));
for (int i = 0; i < ids.size(); i++) {
childMap.get(ids.get(i)).setSort(i + 1);
}
sysGroupRepository.saveAll(children);
}
// ===================== 移动组 =====================
@Override
@Transactional
public void move(SysGroupMoveRequest request) {
Integer moveId = request.getMoveId();
Integer pid = request.getPid();
if (moveId == null || !sysGroupRepository.existsById(moveId)) {
throw new DException("待移动节点不存在");
}
if (pid == null || pid == ROOT_PARENT_ID || !sysGroupRepository.existsById(pid)) {
throw new DException("目标父节点不存在");
}
SysGroup moveEntity = sysGroupRepository.findById(moveId).get();
// 根节点不可移动
if (isRoot(moveEntity)) {
throw new DException("根节点不可移动");
}
if (Objects.equals(pid, moveId)) {
throw new DException("不能移动到自身下");
}
// 环检测:目标父节点不能是待移动节点的后代(含自身)
Map<Integer, SysGroup> allMap = sysGroupRepository.findAll().stream()
.collect(Collectors.toMap(SysGroup::getId, g -> g, (a, b) -> a));
if (isDescendantOf(pid, moveId, allMap)) {
throw new DException("不能移动到自身子节点下");
}
// 移动后追加到目标父节点的子节点末尾
moveEntity.setParentId(pid);
moveEntity.setSort(nextSort(pid));
sysGroupRepository.save(moveEntity);
}
/**
* 判断 ancestorCandidate 是否为 nodeId 的后代(沿 parentId 向上回溯)。<br>
*/
private boolean isDescendantOf(Integer ancestorCandidate, Integer nodeId, Map<Integer, SysGroup> allMap) {
Integer cursor = ancestorCandidate;
while (cursor != null && cursor != ROOT_PARENT_ID) {
if (Objects.equals(cursor, nodeId)) {
return true;
}
SysGroup g = allMap.get(cursor);
if (g == null) {
return false;
}
cursor = g.getParentId();
}
return false;
}
// ===================== 子树聚合 =====================
@Override
public List<Integer> listSelfAndDescendantIds(Integer groupId) {
List<Integer> result = new ArrayList<>();
if (groupId == null) {
return result;
}
result.add(groupId);
// parentId -> 直接子节点ID列表
Map<Integer, List<Integer>> childrenMap = new HashMap<>();
for (SysGroup g : sysGroupRepository.findAll()) {
Integer parent = (g.getParentId() == null) ? ROOT_PARENT_ID : g.getParentId();
childrenMap.computeIfAbsent(parent, k -> new ArrayList<>()).add(g.getId());
}
// 广度优先向下收集所有子孙
List<Integer> frontier = new ArrayList<>();
frontier.add(groupId);
while (!frontier.isEmpty()) {
List<Integer> next = new ArrayList<>();
for (Integer id : frontier) {
List<Integer> children = childrenMap.get(id);
if (children != null) {
result.addAll(children);
next.addAll(children);
}
}
frontier = next;
}
return result;
}
// ===================== 私有工具 =====================
/** pid 下已有子节点的最大 sort +1;无子节点返回 1。 */
private int nextSort(Integer pid) {
Integer max = sysGroupRepository.findMaxSortByParentId(pid);
return (max == null ? 0 : max) + 1;
}
/** 校验并返回组类型枚举;非法类型抛异常。 */
private GroupTypeEnum resolveType(Integer type) {
GroupTypeEnum typeEnum = IEnum.getByKey(GroupTypeEnum.class, type);
if (typeEnum == null) {
throw new DException("组类型不存在");
}
return typeEnum;
}
/** 是否为系统根节点(唯一的 pid=0 节点)。 */
private boolean isRoot(SysGroup entity) {
return entity.getParentId() != null && entity.getParentId() == ROOT_PARENT_ID;
}
private SysGroupVo convertToVo(SysGroup entity) {
SysGroupVo vo = new SysGroupVo();
BeanUtils.copyProperties(entity, vo);
vo.setType(entity.getType() == null ? null : entity.getType().getKey());
return vo;
}
}
@@ -0,0 +1,181 @@
package com.yunhai.userbase.service.impl;
import com.yunhai.userbase.entity.SysOperationLog;
import com.yunhai.userbase.model.BasePageQuery;
import com.yunhai.userbase.model.BasePageResult;
import com.yunhai.userbase.model.oplog.SysOperationLogListVo;
import com.yunhai.userbase.model.oplog.SysOperationLogQuery;
import com.yunhai.userbase.repository.SysOperationLogRepository;
import com.yunhai.userbase.service.ISysOperationLogService;
import com.yunhai.userbase.utils.GzipUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
/**
* 系统操作日志业务实现。<br>
*
* @author chenx
* @date 2026/7/28
*/
@Slf4j
@Service
public class SysOperationLogServiceImpl implements ISysOperationLogService {
/** 一般日志(level=3)保留时长:一年(12 个月)。 */
private static final int GENERAL_RETENTION_MONTHS = 12;
/** 重要(level=1)/次要(level=2)日志保留时长:三年(36 个月)。 */
private static final int IMPORTANT_RETENTION_MONTHS = 36;
@Autowired
private SysOperationLogRepository sysOperationLogRepository;
@PersistenceContext
private EntityManager em;
@Override
@Async
public void saveAsync(SysOperationLog entity) {
try {
sysOperationLogRepository.save(entity);
} catch (Exception e) {
// 日志落库失败绝不抛回主流程,仅记录
log.error("操作日志异步落库失败, uri={}, method={}",
entity.getRequestUri(), entity.getRequestMethod(), e);
}
}
@Override
public BasePageResult<SysOperationLogListVo> pageQuery(SysOperationLogQuery query,
BasePageQuery pageQuery) {
int pageIndex = Math.max(pageQuery.getPageIndex(), 1);
int pageSize = Math.max(pageQuery.getPageSize(), 1);
CriteriaBuilder cb = em.getCriteriaBuilder();
// 列表投影:显式选择字段,不包含 response_data,避免加载 MEDIUMBLOB
CriteriaQuery<SysOperationLogListVo> cq = cb.createQuery(SysOperationLogListVo.class);
Root<SysOperationLog> root = cq.from(SysOperationLog.class);
cq.select(cb.construct(SysOperationLogListVo.class,
root.get("id"), root.get("logLevel"), root.get("userId"), root.get("username"),
root.get("requestIp"), root.get("requestUri"), root.get("requestMethod"),
root.get("operationType"), root.get("classMethod"), root.get("requestParams"),
root.get("statusCode"), root.get("costTime"), root.get("userAgent"),
root.get("errorMsg"), root.get("createTime")));
List<Predicate> predicates = buildPredicates(cb, root, query);
cq.where(predicates.toArray(new Predicate[0]));
cq.orderBy(cb.desc(root.get("createTime")));
// 总数查询
CriteriaQuery<Long> countCq = cb.createQuery(Long.class);
Root<SysOperationLog> countRoot = countCq.from(SysOperationLog.class);
countCq.select(cb.count(countRoot));
countCq.where(buildPredicates(cb, countRoot, query).toArray(new Predicate[0]));
long total = em.createQuery(countCq).getSingleResult();
TypedQuery<SysOperationLogListVo> typed = em.createQuery(cq);
typed.setFirstResult((pageIndex - 1) * pageSize);
typed.setMaxResults(pageSize);
List<SysOperationLogListVo> content = typed.getResultList();
int totalPages = (int) Math.ceil((double) total / pageSize);
boolean hasNext = pageIndex < totalPages;
return new BasePageResult<>(content, (int) total, pageIndex, pageSize, totalPages, hasNext);
}
@Override
public String getResponseData(Long id) {
SysOperationLog entity = sysOperationLogRepository.findById(id).orElse(null);
if (entity == null || entity.getResponseData() == null) {
return null;
}
return GzipUtil.decompress(entity.getResponseData());
}
@Override
@Transactional
public long cleanupExpired() {
LocalDateTime now = LocalDateTime.now();
LocalDateTime generalDeadline = now.minusMonths(GENERAL_RETENTION_MONTHS);
LocalDateTime importantDeadline = now.minusMonths(IMPORTANT_RETENTION_MONTHS);
// 一般日志(level=3)保留一年(12个月);重要(level=1)/次要(level=2)保留三年(36个月)
long deleted = sysOperationLogRepository.deleteByLogLevelAndCreateTimeBefore(3, generalDeadline)
+ sysOperationLogRepository.deleteByLogLevelAndCreateTimeBefore(2, importantDeadline)
+ sysOperationLogRepository.deleteByLogLevelAndCreateTimeBefore(1, importantDeadline);
log.info("操作日志清理完成, 删除{}条(一般截止={}, 重要/次要截止={})",
deleted, generalDeadline, importantDeadline);
return deleted;
}
/**
* 构建动态查询条件。<br>
* 模糊字段使用 LIKE,时间使用范围,其余等值匹配。<br>
*
* @param cb 条件构建器
* @param root 根路径
* @param query 查询条件
* @return 谓词列表
*/
private List<Predicate> buildPredicates(CriteriaBuilder cb, Root<SysOperationLog> root,
SysOperationLogQuery query) {
List<Predicate> predicates = new ArrayList<Predicate>();
if (query == null) {
return predicates;
}
if (query.getLogLevel() != null) {
predicates.add(cb.equal(root.get("logLevel"), query.getLogLevel()));
}
if (query.getUserId() != null) {
predicates.add(cb.equal(root.get("userId"), query.getUserId()));
}
if (StringUtils.isNotBlank(query.getUsername())) {
predicates.add(cb.like(root.get("username"), "%" + query.getUsername() + "%"));
}
if (StringUtils.isNotBlank(query.getRequestUri())) {
predicates.add(cb.like(root.get("requestUri"), "%" + query.getRequestUri() + "%"));
}
if (StringUtils.isNotBlank(query.getRequestIp())) {
predicates.add(cb.like(root.get("requestIp"), "%" + query.getRequestIp() + "%"));
}
if (StringUtils.isNotBlank(query.getOperationType())) {
predicates.add(cb.equal(root.get("operationType"), query.getOperationType()));
}
if (query.getStatusCode() != null) {
predicates.add(cb.equal(root.get("statusCode"), query.getStatusCode()));
}
if (query.getSuccess() != null) {
if (Boolean.TRUE.equals(query.getSuccess())) {
predicates.add(cb.equal(root.get("statusCode"), 200));
} else {
predicates.add(cb.notEqual(root.get("statusCode"), 200));
}
}
if (StringUtils.isNotBlank(query.getKeyword())) {
String kw = "%" + query.getKeyword().trim() + "%";
predicates.add(cb.or(
cb.like(root.get("requestUri"), kw),
cb.like(root.get("username"), kw)));
}
if (query.getStartTime() != null) {
predicates.add(cb.greaterThanOrEqualTo(root.get("createTime"), query.getStartTime()));
}
if (query.getEndTime() != null) {
predicates.add(cb.lessThanOrEqualTo(root.get("createTime"), query.getEndTime()));
}
return predicates;
}
}
@@ -0,0 +1,279 @@
package com.yunhai.userbase.service.impl;
import com.bestsch.utils.DException;
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.SysUserQuery;
import com.yunhai.userbase.model.sysuser.SysUserRequest;
import com.yunhai.userbase.model.sysuser.SysUserVo;
import com.yunhai.userbase.repository.SysGroupRepository;
import com.yunhai.userbase.repository.SysUserAppRoleRelRepository;
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 com.yunhai.userbase.utils.ValidatorUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.persistence.criteria.Subquery;
import javax.transaction.Transactional;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* @author chenx
* @date 2026/7/20 16:14
* @description 系统用户服务实现
*/
@Service
public class SysUserService implements ISysUserService {
/** 主归属标识:1=本职主学校/部门 */
private static final int IS_MAIN = 1;
/** 系统内置超级管理员账号(与 sys_user_init.sql 种子账号保持一致) */
private static final Set<String> SUPER_ADMIN_ACCOUNTS =
new HashSet<>(Arrays.asList("yunhaisch", "yunhaisch2", "yunhaisch3"));
@Autowired
private SysUserRepository sysUserRepository;
@Autowired
private SysUserGroupRelRepository sysUserGroupRelRepository;
@Autowired
private SysUserAppRoleRelRepository sysUserAppRoleRelRepository;
@Autowired
private SysGroupRepository sysGroupRepository;
@Autowired
private ISysGroupService sysGroupService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
@Transactional
public void add(SysUserRequest request) {
if (sysUserRepository.countByAccount(request.getAccount()) > 0) {
throw new DException("账号已存在");
}
if (!sysGroupRepository.existsById(request.getGroupId())) {
throw new DException("所属分组不存在");
}
validateProfile(request);
String encodedPassword = validateAndEncodePassword(request.getPassword(), request.getConfirmPassword());
SysUser entity = new SysUser();
entity.setAccount(request.getAccount());
entity.setPassword(encodedPassword);
entity.setRealName(request.getRealName());
entity.setPhone(request.getPhone());
entity.setBirthday(request.getBirthday());
entity.setIdcard(request.getIdcard());
entity.setProfilePicture(request.getProfilePicture());
entity.setRoles(request.getRoles());
// 用户类型推导:超级管理员账号固定为 1;其余含 ADMIN(7) 角色 -> 2 学校管理员;默认 0 普通用户
entity.setType(resolveUserType(entity.getAccount(), entity.getRoles()));
// disabled 用枚举存储:不传时默认 NORMAL(正常/未禁用)
entity.setDisabled(request.getDisabled() != null ? resolveStatus(request.getDisabled()) : StatusEnum.NORMAL);
entity.setExpired(request.getExpired() != null ? request.getExpired() : false);
sysUserRepository.save(entity);
// 写入组用户关联
SysUserGroupRel rel = new SysUserGroupRel();
rel.setUserId(entity.getId());
rel.setGroupId(request.getGroupId());
rel.setIsMain(IS_MAIN);
rel.setCreateTime(LocalDateTime.now());
sysUserGroupRelRepository.save(rel);
}
@Override
@Transactional
public void edit(Integer id, SysUserRequest request) {
if (sysUserRepository.countByAccountAndIdNot(request.getAccount(), id) > 0) {
throw new DException("账号已被其他用户使用");
}
SysUser entity = sysUserRepository.findById(id)
.orElseThrow(() -> new DException("系统用户不存在"));
validateProfile(request);
entity.setAccount(request.getAccount());
// 密码非空才修改:需两次一致 + 弱密码校验 + BCrypt 加密
if (StringUtils.isNotBlank(request.getPassword())) {
entity.setPassword(validateAndEncodePassword(request.getPassword(), request.getConfirmPassword()));
}
entity.setRealName(request.getRealName());
entity.setPhone(request.getPhone());
entity.setBirthday(request.getBirthday());
entity.setIdcard(request.getIdcard());
entity.setProfilePicture(request.getProfilePicture());
entity.setRoles(request.getRoles());
// 用户类型推导:仅当本次传入了角色时才重算,避免编辑时未传角色把 type 误重置为 0
if (request.getRoles() != null) {
entity.setType(resolveUserType(entity.getAccount(), entity.getRoles()));
}
if (request.getDisabled() != null) {
entity.setDisabled(resolveStatus(request.getDisabled()));
}
if (request.getExpired() != null) {
entity.setExpired(request.getExpired());
}
sysUserRepository.save(entity);
}
@Override
@Transactional
public void delete(Integer id) {
if (!sysUserRepository.existsById(id)) {
throw new DException("系统用户不存在");
}
// 先清理组用户关联、用户-角色绑定,再删除用户
sysUserGroupRelRepository.deleteByUserId(id);
// AI-FIX(角色授权): 删除用户时级联清理用户-业务角色绑定(逻辑外键无物理 FK,避免残留孤儿数据)
sysUserAppRoleRelRepository.deleteByUserIdBulk(id);
sysUserRepository.deleteById(id);
}
@Override
@Transactional
public void changeStatus(Integer id, boolean disabled) {
SysUser entity = sysUserRepository.findById(id)
.orElseThrow(() -> new DException("系统用户不存在"));
// 实体 disabled 字段为 StatusEnum(0=正常/未禁用,1=禁用):boolean 直接映射枚举
entity.setDisabled(disabled ? StatusEnum.DISABLED : StatusEnum.NORMAL);
sysUserRepository.save(entity);
}
@Override
public Page<SysUserVo> list(SysUserQuery query, Pageable pageable) {
Specification<SysUser> spec = buildSpecification(query);
return sysUserRepository.findAll(spec, pageable).map(this::convertToVo);
}
// ===================== 私有工具 =====================
/**
* 组装用户列表查询条件:组范围(含子组)+ 关键字(账号/姓名/身份证模糊、手机号精确)+ 启用状态。<br>
*/
private Specification<SysUser> buildSpecification(SysUserQuery query) {
return (root, cbQuery, cb) -> {
List<Predicate> predicates = new ArrayList<>();
// 组范围过滤:用户ID 属于目标组集合下的关联记录
if (query.getGroupId() != null) {
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));
}
// 关键字:账号/姓名/身份证模糊,手机号精确
if (StringUtils.isNotBlank(query.getName())) {
String keyword = query.getName().trim();
String like = "%" + keyword + "%";
Predicate byAccount = cb.like(root.get("account"), like);
Predicate byRealName = cb.like(root.get("realName"), like);
Predicate byIdcard = cb.like(root.get("idcard"), like);
Predicate byPhone = cb.equal(root.get("phone"), keyword);
predicates.add(cb.or(byAccount, byRealName, byIdcard, byPhone));
}
if (query.getDisabled() != null) {
// 实体 disabled 为 StatusEnum,查询条件需用枚举值比较(按 key 解析)
predicates.add(cb.equal(root.get("disabled"), IEnum.getByKey(StatusEnum.class, query.getDisabled())));
}
return cb.and(predicates.toArray(new Predicate[0]));
};
}
/** 依据 withSub 决定组范围:1=含所有子孙组,否则仅当前组。 */
private List<Integer> resolveGroupIds(Integer groupId, Integer withSub) {
if (withSub != null && withSub == 1) {
return sysGroupService.listSelfAndDescendantIds(groupId);
}
List<Integer> single = new ArrayList<>();
single.add(groupId);
return single;
}
/** 校验手机号与身份证(有值才校验)。 */
private void validateProfile(SysUserRequest request) {
if (StringUtils.isNotBlank(request.getPhone()) && !ValidatorUtil.isPhone(request.getPhone())) {
throw new DException("手机号格式不正确");
}
if (StringUtils.isNotBlank(request.getIdcard()) && !ValidatorUtil.isIdCard(request.getIdcard())) {
throw new DException("身份证号不合法");
}
}
/**
* 校验两次密码一致性与弱密码规则,通过后返回 BCrypt 密文。<br>
*/
private String validateAndEncodePassword(String password, String confirmPassword) {
if (!StringUtils.equals(password, confirmPassword)) {
throw new DException("两次输入的密码不一致");
}
String weakReason = ValidatorUtil.checkPasswordStrength(password);
if (weakReason != null) {
throw new DException(weakReason);
}
return passwordEncoder.encode(password);
}
private SysUserVo convertToVo(SysUser entity) {
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());
return vo;
}
/**
* 推导用户类型(见 {@link UserTypeEnum}):
* - 内置超级管理员账号 -> 1 超级管理员;
* - 其余账号若拥有 ADMIN(7) 角色 -> 2 学校管理员;
* - 其他 -> 0 普通用户。
*/
private UserTypeEnum resolveUserType(String account, List<Integer> roles) {
if (account != null && SUPER_ADMIN_ACCOUNTS.contains(account)) {
return UserTypeEnum.SUPER_ADMIN;
}
if (roles != null && roles.contains(SysRoleEnum.ADMIN.getKey())) {
return UserTypeEnum.SCHOOL_ADMIN;
}
return UserTypeEnum.NORMAL;
}
/** 校验并返回用户状态枚举;非法状态抛异常。 */
private StatusEnum resolveStatus(Integer status) {
StatusEnum statusEnum = IEnum.getByKey(StatusEnum.class, status);
if (statusEnum == null) {
throw new DException("用户状态不存在");
}
return statusEnum;
}
}