This commit is contained in:
@@ -53,12 +53,6 @@ public class SysUserController {
|
||||
sysUserService.edit(id, request);
|
||||
}
|
||||
|
||||
@OperationLog("删除用户")
|
||||
@PostMapping("Delete/{id}")
|
||||
public void delete(@PathVariable Integer id) {
|
||||
sysUserService.delete(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将用户从指定组中移除;若该用户无其它组关联则级联删除用户。
|
||||
*/
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
package com.yunhai.userbase.controller.oapi;
|
||||
|
||||
import com.yunhai.platform.utils.model.BoaGroup;
|
||||
import com.yunhai.userbase.common.validation.ValidationGroups;
|
||||
import com.yunhai.userbase.config.Config;
|
||||
import com.yunhai.userbase.model.sysgroup.SysGroupBatchRequest;
|
||||
import com.yunhai.userbase.model.sysgroup.SysGroupRequest;
|
||||
import com.yunhai.userbase.service.ISysGroupService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
@@ -60,4 +67,38 @@ public class GroupController {
|
||||
@RequestParam(value = "withSub", required = false) Boolean withSub) {
|
||||
return sysGroupService.getGroupList(ids, withSub);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增组:返回创建后的组节点(含 id)。<br>
|
||||
* 层级类型约束、根节点保护等复用后台逻辑;鉴权由 OpenApiAuthEnforceConfiguration 在过滤器层强制签名。
|
||||
*/
|
||||
@PostMapping("add")
|
||||
public BoaGroup add(@RequestBody @Validated(ValidationGroups.Create.class) SysGroupRequest request) {
|
||||
return sysGroupService.add(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑组(不含移动,移动请走 group/move 后台接口):返回更新后的组节点(含 id)。
|
||||
*/
|
||||
@PostMapping("edit/{id}")
|
||||
public BoaGroup edit(@PathVariable Integer id,
|
||||
@RequestBody @Validated(ValidationGroups.Update.class) SysGroupRequest request) {
|
||||
return sysGroupService.edit(id, request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除组:组下存在子组或用户时拒绝删除(根节点不可删)。无返回值。
|
||||
*/
|
||||
@PostMapping("delete/{id}")
|
||||
public void delete(@PathVariable Integer id) {
|
||||
sysGroupService.delete(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量新增子组:请求体 { pid, type, items:[{name},...] },返回创建后的组节点列表(均含 id)。
|
||||
*/
|
||||
@PostMapping("batchAdd")
|
||||
public List<BoaGroup> batchAdd(@RequestBody @Validated SysGroupBatchRequest request) {
|
||||
return sysGroupService.batchAdd(request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,20 +5,29 @@ import com.yunhai.platform.utils.exception.DException;
|
||||
import com.yunhai.platform.utils.model.BoaGroup;
|
||||
import com.yunhai.platform.utils.model.BoaModule;
|
||||
import com.yunhai.platform.utils.model.BoaUser;
|
||||
import com.yunhai.userbase.common.validation.ValidationGroups;
|
||||
import com.yunhai.userbase.config.Config;
|
||||
import com.yunhai.userbase.entity.ApplicationInfo;
|
||||
import com.yunhai.userbase.model.sysuser.SysUserMoveRequest;
|
||||
import com.yunhai.userbase.model.sysuser.SysUserRequest;
|
||||
import com.yunhai.userbase.repository.ApplicationInfoRepository;
|
||||
import com.yunhai.userbase.service.IGroupAuthzService;
|
||||
import com.yunhai.userbase.service.ISysUserService;
|
||||
import com.yunhai.userbase.service.impl.UserCenterService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@@ -41,6 +50,9 @@ public class UserController {
|
||||
@Autowired
|
||||
private UserCenterService userCenterService;
|
||||
|
||||
@Autowired
|
||||
private ISysUserService sysUserService;
|
||||
|
||||
@Autowired
|
||||
private IGroupAuthzService groupAuthzService;
|
||||
|
||||
@@ -130,28 +142,61 @@ public class UserController {
|
||||
return app.getId();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 按「schId 为空时」解析出用户主学校/主组作为模块查询的上下文组ID。<br>
|
||||
* 保留方法(逻辑已下沉到 Service.resolveEffectiveGroupId 复用),不再被 modules() 使用。
|
||||
* 新增用户:走完整校验(账号唯一/手机号/身份证/密码强度),BCrypt 加密并写入组关联;
|
||||
* 返回创建后的用户(含 id 与组织链)。<br>
|
||||
* 鉴权由 OpenApiAuthEnforceConfiguration 在过滤器层强制签名;此处不记操作日志(避免明文密码写入 request_params)。
|
||||
*/
|
||||
@Deprecated
|
||||
private Integer resolveEffectiveGroupId(Integer userId, Integer schId) {
|
||||
if (schId != null) {
|
||||
return schId;
|
||||
@PostMapping("add")
|
||||
public BoaUser add(@RequestBody @Validated(ValidationGroups.Create.class) SysUserRequest request) {
|
||||
Integer id = sysUserService.add(request);
|
||||
return userCenterService.getUserInfoWithDeptById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑用户:密码为空时不修改密码;返回更新后的用户(含 id 与组织链)。
|
||||
*/
|
||||
@PostMapping("edit/{id}")
|
||||
public BoaUser edit(@PathVariable Integer id,
|
||||
@RequestBody @Validated(ValidationGroups.Update.class) SysUserRequest request) {
|
||||
sysUserService.edit(id, request);
|
||||
return userCenterService.getUserInfoWithDeptById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户(按组解绑,语义同管理端「从组中删除用户」):仅解除该用户与指定组的关联;<br>
|
||||
* 仅当用户已无任何组关联时,才级联删除 sys_user 及其角色绑定。<br>
|
||||
* 这样多校用户只会被退出指定学校,而不会被整体误删。<br>
|
||||
* 若用户仅归属这一个组,调用后该用户将被真正删除(组关联清空即触发级联删)。
|
||||
*/
|
||||
@PostMapping("delete/{id}/{groupId}")
|
||||
public void delete(@PathVariable Integer id, @PathVariable Integer groupId) {
|
||||
sysUserService.deleteFromGroup(id, groupId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量新增用户:逐条走完整校验(含密码强度,每条需携带 password + confirmPassword);
|
||||
* 返回创建后的用户列表(顺序与入参一致,均含 id 与组织链)。
|
||||
*/
|
||||
@PostMapping("batchAdd")
|
||||
public List<BoaUser> batchAdd(@RequestBody List<SysUserRequest> requests) {
|
||||
List<Integer> ids = sysUserService.batchAdd(requests);
|
||||
List<BoaUser> result = new ArrayList<>();
|
||||
for (Integer id : ids) {
|
||||
result.add(userCenterService.getUserInfoWithDeptById(id));
|
||||
}
|
||||
BoaUser boa = userCenterService.getUserInfoWithDeptById(userId);
|
||||
if (boa == null) {
|
||||
return null;
|
||||
}
|
||||
BoaGroup school = boa.getSchool();
|
||||
if (school != null && school.getId() != null) {
|
||||
return school.getId();
|
||||
}
|
||||
List<BoaGroup> groups = boa.getGroups();
|
||||
if (groups != null && !groups.isEmpty()) {
|
||||
BoaGroup main = groups.get(0);
|
||||
return main == null ? null : main.getId();
|
||||
}
|
||||
return null;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动用户到指定组:仅允许在同一学校(组)范围内移动——用户当前必须已在该学校子树内,
|
||||
* 且目标组也必须在该学校子树内,否则拒绝(禁止跨校移动)。<br>
|
||||
* 语义同后台「调整组」——覆盖式把用户组归属替换为目标组(目标设为主组)。返回移动后的用户(含新的组织链)。
|
||||
*/
|
||||
@PostMapping("moveToGroup")
|
||||
public BoaUser moveToGroup(@RequestBody @Validated SysUserMoveRequest request) {
|
||||
sysUserService.moveUserWithinSchool(request.getUserId(), request.getGroupId(), request.getSchoolId());
|
||||
return userCenterService.getUserInfoWithDeptById(request.getUserId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,12 +37,24 @@ public class SysUserCurrentRole {
|
||||
private Integer userId;
|
||||
|
||||
/**
|
||||
* 用户切换选中的学校ID(根级group)
|
||||
* 用户切换选中的学校ID(根级group,数值即学校组 sys_group.id;区域/学段/根直属=0或null)
|
||||
*/
|
||||
private Integer schoolId;
|
||||
/**
|
||||
* 用户切换后生效角色ID
|
||||
* 用户切换后生效角色ID(SysRoleEnum.key)
|
||||
*/
|
||||
private Integer roleId;
|
||||
|
||||
/**
|
||||
* 最近一次切换时间(服务端 now() 写入)
|
||||
*/
|
||||
@Column(name = "switch_time")
|
||||
private LocalDateTime switchTime;
|
||||
|
||||
/**
|
||||
* 学校名冗余(避免每次 join sys_group 取 name)
|
||||
*/
|
||||
@Column(name = "school_name", length = 100)
|
||||
private String schoolName;
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.yunhai.userbase.service;
|
||||
|
||||
import com.yunhai.platform.utils.model.BoaModule;
|
||||
import com.yunhai.userbase.model.groupauthz.*;
|
||||
|
||||
import java.util.List;
|
||||
@@ -130,7 +131,7 @@ public interface IGroupAuthzService {
|
||||
* @param userId 用户ID(必填)
|
||||
* @return 该应用下用户有权限的模块(扁平化;无权限时返回空列表)
|
||||
*/
|
||||
List<com.yunhai.platform.utils.model.BoaModule> findUserModulesForApp(Integer appId, Integer schId, Integer userId);
|
||||
List<BoaModule> findUserModulesForApp(Integer appId, Integer schId, Integer userId);
|
||||
|
||||
// ===================== 新增:树节点式增量授权(供左侧树 + 右侧 appAuthzList 的勾选交互) =====================
|
||||
|
||||
|
||||
@@ -17,9 +17,9 @@ import java.util.List;
|
||||
*/
|
||||
public interface ISysGroupService {
|
||||
|
||||
void add(SysGroupRequest request);
|
||||
BoaGroup add(SysGroupRequest request);
|
||||
|
||||
void edit(Integer id, SysGroupRequest request);
|
||||
BoaGroup edit(Integer id, SysGroupRequest request);
|
||||
|
||||
void delete(Integer id);
|
||||
|
||||
@@ -36,7 +36,7 @@ public interface ISysGroupService {
|
||||
List<SysGroupVo> getSchoolTree();
|
||||
|
||||
/** 在指定父组下按规则批量生成子组。 */
|
||||
void batchAdd(SysGroupBatchRequest request);
|
||||
List<BoaGroup> batchAdd(SysGroupBatchRequest request);
|
||||
|
||||
/** 对 pid 的直接子节点按给定顺序重排并落库(ids 必须全是直接子节点)。 */
|
||||
void sortChildren(SysGroupSortRequest request);
|
||||
|
||||
@@ -25,8 +25,10 @@ public interface ISysUserService {
|
||||
/** 编辑用户;密码为空时不修改密码。 */
|
||||
void edit(Integer id, SysUserRequest request);
|
||||
|
||||
/** 删除用户:同时清理其组用户关联。 */
|
||||
void delete(Integer id);
|
||||
/**
|
||||
* 批量新增用户:逐条走完整校验(含密码强度),返回创建后的用户ID列表(顺序与入参一致)。
|
||||
*/
|
||||
List<Integer> batchAdd(List<SysUserRequest> requests);
|
||||
|
||||
/**
|
||||
* 把用户从指定组中移除;若移除后该用户已无其它组关联,则级联删除用户本身。
|
||||
@@ -87,6 +89,19 @@ public interface ISysUserService {
|
||||
*/
|
||||
void changeGroups(List<Integer> userIds, List<Integer> groupIds);
|
||||
|
||||
/**
|
||||
* 开放接口专用:在学校(组)范围内移动用户。<br>
|
||||
* 语义同 {@link #changeGroups}(覆盖式把用户组归属替换为目标组,目标设为主组),但额外强制:<br>
|
||||
* ① 用户当前必须已在该学校子树内(至少一个组归属落在 schoolId 子树);<br>
|
||||
* ② 目标 groupId 必须也在该学校子树内。<br>
|
||||
* 任意一条不满足即抛异常,禁止跨校移动。
|
||||
*
|
||||
* @param userId 待移动用户ID
|
||||
* @param groupId 目标组ID(移动后的主归属组)
|
||||
* @param schoolId 学校组ID(限定移动范围,仅允许同校内移动)
|
||||
*/
|
||||
void moveUserWithinSchool(Integer userId, Integer groupId, Integer schoolId);
|
||||
|
||||
/**
|
||||
* 调整用户在某组内的排序号(sort_no)。<br>
|
||||
* sortNo 为 null 时清除排序号,列表回退到 user_id 升序兜底。
|
||||
|
||||
@@ -28,6 +28,7 @@ import javax.transaction.Transactional;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@@ -47,6 +48,63 @@ public class SysGroupService implements ISysGroupService {
|
||||
/** 虚拟父节点:根节点(教育云中心)的 parentId,表示「无上级」。 */
|
||||
private static final int ROOT_PARENT_ID = 0;
|
||||
|
||||
/** 开放接口/oapi 写操作无登录态时使用的审计人哨兵值(非真实用户ID),用于兜底 create_user/update_user 的 NOT NULL 约束。 */
|
||||
private static final int SYSTEM_USER_ID = 0;
|
||||
|
||||
/**
|
||||
* 层级类型约束:父类型 -> 允许直接创建的子类型集合。
|
||||
* key 为 {@code null} 表示「根节点(教育云中心)」。
|
||||
* 集合为空 = 该类型禁止创建任何子组(班级组叶子节点、学段组占位等由空集自然拒绝)。
|
||||
* <p>规则来源:用户中心组层级约束(根-区域-学段-学校-教师|学生-年级-班级)。</p>
|
||||
*/
|
||||
private static final Map<GroupTypeEnum, Set<GroupTypeEnum>> ALLOWED_CHILDREN = new HashMap<>();
|
||||
static {
|
||||
ALLOWED_CHILDREN.put(null, EnumSet.of(
|
||||
GroupTypeEnum.SCHOOL_GROUP, GroupTypeEnum.NORMAL_GROUP, GroupTypeEnum.AREA_GROUP));
|
||||
// 区域组:只能挂在根节点下(由根节点的白名单约束),其下可挂 学段组/学校组/普通组
|
||||
ALLOWED_CHILDREN.put(GroupTypeEnum.AREA_GROUP, EnumSet.of(
|
||||
GroupTypeEnum.STAGE_GROUP, GroupTypeEnum.SCHOOL_GROUP, GroupTypeEnum.NORMAL_GROUP));
|
||||
// 学段组(高中/初中/小学):学校组的上级,可含 学校组/普通组
|
||||
ALLOWED_CHILDREN.put(GroupTypeEnum.STAGE_GROUP, EnumSet.of(
|
||||
GroupTypeEnum.SCHOOL_GROUP, GroupTypeEnum.NORMAL_GROUP));
|
||||
// 学校组:可含 教师组/学生组/普通组
|
||||
ALLOWED_CHILDREN.put(GroupTypeEnum.SCHOOL_GROUP, EnumSet.of(
|
||||
GroupTypeEnum.TEACHER_GROUP, GroupTypeEnum.STUDENT_GROUP, GroupTypeEnum.NORMAL_GROUP));
|
||||
// 教师组:维持原规范,仅含 普通组
|
||||
ALLOWED_CHILDREN.put(GroupTypeEnum.TEACHER_GROUP, EnumSet.of(GroupTypeEnum.NORMAL_GROUP));
|
||||
// 学生组:可含 年级组/学段组/普通组(学段组既可在区域组下、也可在学生组下)
|
||||
ALLOWED_CHILDREN.put(GroupTypeEnum.STUDENT_GROUP, EnumSet.of(
|
||||
GroupTypeEnum.GRADE_GROUP, GroupTypeEnum.STAGE_GROUP, GroupTypeEnum.NORMAL_GROUP));
|
||||
// 年级组:可含 班级组/普通组
|
||||
ALLOWED_CHILDREN.put(GroupTypeEnum.GRADE_GROUP, EnumSet.of(
|
||||
GroupTypeEnum.CLASS_GROUP, GroupTypeEnum.NORMAL_GROUP));
|
||||
// 班级组:叶子节点,禁止创建任何子组(⛔ 强约束,空集自然拒绝)
|
||||
ALLOWED_CHILDREN.put(GroupTypeEnum.CLASS_GROUP, EnumSet.noneOf(GroupTypeEnum.class));
|
||||
// 普通组:可含 学校组/教师组/学段组/普通组(年级组/班级组/学生组 受限,区域组受「只能挂根」硬约束排除)
|
||||
ALLOWED_CHILDREN.put(GroupTypeEnum.NORMAL_GROUP, EnumSet.of(
|
||||
GroupTypeEnum.SCHOOL_GROUP, GroupTypeEnum.TEACHER_GROUP,
|
||||
GroupTypeEnum.STAGE_GROUP, GroupTypeEnum.NORMAL_GROUP));
|
||||
}
|
||||
|
||||
/** 层级类型约束校验:parentType 为 null 表示根节点;非法组合抛 {@link DException}。 */
|
||||
private void validateParentChild(GroupTypeEnum parentType, GroupTypeEnum childType) {
|
||||
Set<GroupTypeEnum> allowed = ALLOWED_CHILDREN.get(parentType);
|
||||
if (allowed == null) {
|
||||
allowed = Collections.emptySet();
|
||||
}
|
||||
if (allowed.contains(childType)) {
|
||||
return;
|
||||
}
|
||||
String parentName = (parentType == null) ? "根节点" : parentType.getValue();
|
||||
if (childType == GroupTypeEnum.CLASS_GROUP) {
|
||||
throw new DException(parentName + "下不允许创建班级组(班级组为叶子节点,禁止创建子组)");
|
||||
}
|
||||
if (childType == GroupTypeEnum.AREA_GROUP) {
|
||||
throw new DException("区域组只能建立在根节点下");
|
||||
}
|
||||
throw new DException(parentName + "下不允许创建" + childType.getValue());
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private SysGroupRepository sysGroupRepository;
|
||||
|
||||
@@ -66,29 +124,35 @@ public class SysGroupService implements ISysGroupService {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void add(SysGroupRequest request) {
|
||||
public BoaGroup 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("父组不存在");
|
||||
// 允许在根节点(教育云中心)下建组:parentId 为 null 或 0 视为根
|
||||
GroupTypeEnum parentType = null; // null 表示根节点
|
||||
if (parentId != null && parentId != ROOT_PARENT_ID) {
|
||||
SysGroup parent = sysGroupRepository.findById(parentId)
|
||||
.orElseThrow(() -> new DException("父组不存在"));
|
||||
parentType = parent.getType();
|
||||
}
|
||||
GroupTypeEnum typeEnum = resolveType(request.getType());
|
||||
// 层级类型约束:当前父组是否允许创建该子类型
|
||||
validateParentChild(parentType, typeEnum);
|
||||
|
||||
int realPid = (parentId == null) ? ROOT_PARENT_ID : parentId;
|
||||
SysGroup entity = new SysGroup();
|
||||
entity.setName(request.getName());
|
||||
entity.setParentId(parentId);
|
||||
entity.setParentId(realPid);
|
||||
entity.setType(typeEnum);
|
||||
entity.setSort(nextSort(parentId));
|
||||
entity.setSort(nextSort(realPid));
|
||||
entity.setCreateUser(SYSTEM_USER_ID);
|
||||
entity.setUpdateUser(SYSTEM_USER_ID);
|
||||
SysGroup saved = sysGroupRepository.save(entity);
|
||||
publishGroupEvent(PushInterfaceType.ADD_GROUP, saved.getId());
|
||||
return convertToBoaGroup(saved);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void edit(Integer id, SysGroupRequest request) {
|
||||
public BoaGroup edit(Integer id, SysGroupRequest request) {
|
||||
SysGroup entity = sysGroupRepository.findById(id)
|
||||
.orElseThrow(() -> new DException("系统组不存在"));
|
||||
// 根节点不可编辑
|
||||
@@ -97,14 +161,39 @@ public class SysGroupService implements ISysGroupService {
|
||||
}
|
||||
entity.setName(request.getName());
|
||||
if (request.getType() != null) {
|
||||
entity.setType(resolveType(request.getType()));
|
||||
GroupTypeEnum newType = resolveType(request.getType());
|
||||
if (!Objects.equals(newType, entity.getType())) {
|
||||
// 1) 新类型须被当前父允许(根节点 parentId=0,parentType 视为 null)
|
||||
GroupTypeEnum parentType = null;
|
||||
if (entity.getParentId() != null && entity.getParentId() != ROOT_PARENT_ID) {
|
||||
SysGroup parent = sysGroupRepository.findById(entity.getParentId()).orElse(null);
|
||||
parentType = (parent != null) ? parent.getType() : null;
|
||||
}
|
||||
validateParentChild(parentType, newType);
|
||||
// 2) 新类型的允许子类型须覆盖现有直接子类型,避免把「含子组的组」改成叶子/不兼容类型
|
||||
Set<GroupTypeEnum> newAllowed = ALLOWED_CHILDREN.get(newType);
|
||||
if (newAllowed == null) {
|
||||
newAllowed = Collections.emptySet();
|
||||
}
|
||||
List<SysGroup> children = sysGroupRepository.findByParentIdOrderBySortAsc(entity.getId());
|
||||
for (SysGroup child : children) {
|
||||
GroupTypeEnum ct = child.getType();
|
||||
if (ct != null && !newAllowed.contains(ct)) {
|
||||
throw new DException("该组下已存在「" + ct.getValue() + "」,不可变更为「"
|
||||
+ newType.getValue() + "」(" + newType.getValue() + "不允许包含" + ct.getValue() + ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
entity.setType(newType);
|
||||
}
|
||||
if (request.getSort() != null) {
|
||||
entity.setSort(request.getSort());
|
||||
}
|
||||
// 注:移动请走 move 接口,edit 不改 parentId,避免与移动逻辑混淆
|
||||
entity.setUpdateUser(SYSTEM_USER_ID);
|
||||
sysGroupRepository.save(entity);
|
||||
publishGroupEvent(PushInterfaceType.UPDATE_GROUP, id);
|
||||
return convertToBoaGroup(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -309,18 +398,28 @@ public class SysGroupService implements ISysGroupService {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void batchAdd(SysGroupBatchRequest request) {
|
||||
public List<BoaGroup> batchAdd(SysGroupBatchRequest request) {
|
||||
Integer pid = request.getPid();
|
||||
if (pid == null || !sysGroupRepository.existsById(pid)) {
|
||||
throw new DException("父组不存在");
|
||||
GroupTypeEnum parentType = null; // null 表示根节点
|
||||
int realPid;
|
||||
if (pid != null && pid != ROOT_PARENT_ID) {
|
||||
SysGroup parent = sysGroupRepository.findById(pid)
|
||||
.orElseThrow(() -> new DException("父组不存在"));
|
||||
parentType = parent.getType();
|
||||
realPid = pid;
|
||||
} else {
|
||||
// 允许在根节点下批量建组
|
||||
realPid = ROOT_PARENT_ID;
|
||||
}
|
||||
GroupTypeEnum typeEnum = resolveType(request.getType());
|
||||
// 层级类型约束
|
||||
validateParentChild(parentType, typeEnum);
|
||||
List<SysGroupBatchRequest.BatchItem> items = request.getItems();
|
||||
if (items == null || items.isEmpty()) {
|
||||
throw new DException("批量新增的子组列表不能为空");
|
||||
}
|
||||
// 追加到当前父组子节点之后
|
||||
int base = nextSort(pid);
|
||||
int base = nextSort(realPid);
|
||||
|
||||
List<SysGroup> batch = new ArrayList<>();
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
@@ -330,14 +429,17 @@ public class SysGroupService implements ISysGroupService {
|
||||
}
|
||||
SysGroup g = new SysGroup();
|
||||
g.setName(name.trim());
|
||||
g.setParentId(pid);
|
||||
g.setParentId(realPid);
|
||||
g.setType(typeEnum);
|
||||
g.setSort(base + i);
|
||||
g.setCreateUser(SYSTEM_USER_ID);
|
||||
g.setUpdateUser(SYSTEM_USER_ID);
|
||||
batch.add(g);
|
||||
}
|
||||
sysGroupRepository.saveAll(batch);
|
||||
publishGroupEvent(PushInterfaceType.ADD_GROUP,
|
||||
batch.stream().map(SysGroup::getId).collect(Collectors.toList()));
|
||||
return batch.stream().map(this::convertToBoaGroup).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// ===================== 子组排序 =====================
|
||||
@@ -383,9 +485,16 @@ public class SysGroupService implements ISysGroupService {
|
||||
if (moveId == null || !sysGroupRepository.existsById(moveId)) {
|
||||
throw new DException("待移动节点不存在");
|
||||
}
|
||||
if (pid == null || pid == ROOT_PARENT_ID || !sysGroupRepository.existsById(pid)) {
|
||||
if (pid == null) {
|
||||
throw new DException("目标父节点不存在");
|
||||
}
|
||||
// 目标父为根节点时 pid=0;其余情况校验目标父存在并取其类型
|
||||
GroupTypeEnum parentType = null; // null 表示根节点
|
||||
if (pid != ROOT_PARENT_ID) {
|
||||
SysGroup target = sysGroupRepository.findById(pid)
|
||||
.orElseThrow(() -> new DException("目标父节点不存在"));
|
||||
parentType = target.getType();
|
||||
}
|
||||
SysGroup moveEntity = sysGroupRepository.findById(moveId).get();
|
||||
// 根节点不可移动
|
||||
if (isRoot(moveEntity)) {
|
||||
@@ -400,6 +509,8 @@ public class SysGroupService implements ISysGroupService {
|
||||
if (isDescendantOf(pid, moveId, allMap)) {
|
||||
throw new DException("不能移动到自身子节点下");
|
||||
}
|
||||
// 层级类型约束:移动后目标父类型须允许该节点类型
|
||||
validateParentChild(parentType, moveEntity.getType());
|
||||
// 移动后追加到目标父节点的子节点末尾
|
||||
moveEntity.setParentId(pid);
|
||||
moveEntity.setSort(nextSort(pid));
|
||||
|
||||
@@ -69,6 +69,9 @@ public class SysUserService implements ISysUserService {
|
||||
/** 主归属标识:1=本职主学校/部门 */
|
||||
private static final int IS_MAIN = 1;
|
||||
|
||||
/** 开放接口/oapi 写操作无登录态时使用的审计人哨兵值(非真实用户ID),用于兜底 create_user/update_user 的 NOT NULL 约束。 */
|
||||
private static final int SYSTEM_USER_ID = 0;
|
||||
|
||||
/** 系统内置超级管理员账号(与 sys_user_init.sql 种子账号保持一致) */
|
||||
private static final Set<String> SUPER_ADMIN_ACCOUNTS =
|
||||
new HashSet<>(Arrays.asList("yunhaisch", "yunhaisch2", "yunhaisch3"));
|
||||
@@ -136,6 +139,9 @@ public class SysUserService implements ISysUserService {
|
||||
// disabled 用枚举存储:不传时默认 NORMAL(正常/未禁用)
|
||||
entity.setDisabled(request.getDisabled() != null ? resolveStatus(request.getDisabled()) : StatusEnum.NORMAL);
|
||||
entity.setExpired(request.getExpired() != null ? request.getExpired() : false);
|
||||
// oapi 写操作无登录态时,用系统哨兵值兜底 create_user/update_user(NOT NULL 约束)
|
||||
entity.setCreateUser(SYSTEM_USER_ID);
|
||||
entity.setUpdateUser(SYSTEM_USER_ID);
|
||||
SysUser saved = sysUserRepository.save(entity);
|
||||
publishUserEvent(PushInterfaceType.ADD_USER, saved.getId());
|
||||
|
||||
@@ -183,22 +189,23 @@ public class SysUserService implements ISysUserService {
|
||||
if (request.getExpired() != null) {
|
||||
entity.setExpired(request.getExpired());
|
||||
}
|
||||
entity.setUpdateUser(SYSTEM_USER_ID);
|
||||
sysUserRepository.save(entity);
|
||||
publishUserEvent(PushInterfaceType.UPDATE_USER, id);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void delete(Integer id) {
|
||||
if (!sysUserRepository.existsById(id)) {
|
||||
throw new DException("系统用户不存在");
|
||||
public List<Integer> batchAdd(List<SysUserRequest> requests) {
|
||||
if (requests == null || requests.isEmpty()) {
|
||||
throw new DException("批量新增用户列表不能为空");
|
||||
}
|
||||
// 先清理组用户关联、用户-角色绑定,再删除用户
|
||||
sysUserGroupRelRepository.deleteByUserId(id);
|
||||
// AI-FIX(角色授权): 删除用户时级联清理用户-业务角色绑定(逻辑外键无物理 FK,避免残留孤儿数据)
|
||||
sysUserAppRoleRelRepository.deleteByUserIdBulk(id);
|
||||
sysUserRepository.deleteById(id);
|
||||
publishUserEvent(PushInterfaceType.DELETE_USER, id);
|
||||
// 批量新增与单条一致:强制密码强度校验(每条需携带 password + confirmPassword)
|
||||
List<Integer> ids = new ArrayList<>();
|
||||
for (SysUserRequest request : requests) {
|
||||
ids.add(createUser(request, true));
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -384,6 +391,46 @@ public class SysUserService implements ISysUserService {
|
||||
publishUserEvent(PushInterfaceType.UPDATE_USER, userIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void moveUserWithinSchool(Integer userId, Integer groupId, Integer schoolId) {
|
||||
if (userId == null) {
|
||||
throw new DException("用户ID不能为空");
|
||||
}
|
||||
if (groupId == null) {
|
||||
throw new DException("目标组ID不能为空");
|
||||
}
|
||||
if (schoolId == null) {
|
||||
throw new DException("学校ID不能为空");
|
||||
}
|
||||
if (!sysUserRepository.existsById(userId)) {
|
||||
throw new DException("系统用户不存在");
|
||||
}
|
||||
if (!sysGroupRepository.existsById(groupId)) {
|
||||
throw new DException("目标分组不存在");
|
||||
}
|
||||
if (!sysGroupRepository.existsById(schoolId)) {
|
||||
throw new DException("学校分组不存在");
|
||||
}
|
||||
|
||||
// 学校子树范围(含学校自身):用户与目标组都必须落在此范围内,否则视为跨校移动
|
||||
List<Integer> schoolScope = sysGroupService.listSelfAndDescendantIds(schoolId);
|
||||
|
||||
// ① 用户当前必须已在该学校内(至少一个组归属落在 schoolScope)
|
||||
List<Integer> userGroupIds = findGroupIdsByUserId(userId);
|
||||
boolean userInSchool = userGroupIds.stream().anyMatch(schoolScope::contains);
|
||||
if (!userInSchool) {
|
||||
throw new DException("用户不在该学校范围内,禁止跨校移动");
|
||||
}
|
||||
// ② 目标组必须也在该学校内
|
||||
if (!schoolScope.contains(groupId)) {
|
||||
throw new DException("目标组不属于该学校范围,禁止跨校移动");
|
||||
}
|
||||
|
||||
// 复用覆盖式改组逻辑(目标设为主组)
|
||||
changeGroups(Collections.singletonList(userId), Collections.singletonList(groupId));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public BatchImportResult batchImport(MultipartFile file) {
|
||||
|
||||
@@ -4,17 +4,22 @@ import com.yunhai.platform.utils.model.BoaGroup;
|
||||
import com.yunhai.platform.utils.model.BoaUser;
|
||||
import com.yunhai.userbase.entity.SysGroup;
|
||||
import com.yunhai.userbase.entity.SysUser;
|
||||
import com.yunhai.userbase.entity.SysUserCurrentRole;
|
||||
import com.yunhai.userbase.entity.SysUserGroupRel;
|
||||
import com.yunhai.userbase.entity.SysUserRoleRel;
|
||||
import com.yunhai.userbase.enums.GroupTypeEnum;
|
||||
import com.yunhai.userbase.enums.StatusEnum;
|
||||
import com.yunhai.userbase.repository.SysGroupRepository;
|
||||
import com.yunhai.userbase.repository.SysUserGroupRelRepository;
|
||||
import com.yunhai.userbase.repository.SysUserRepository;
|
||||
import com.yunhai.userbase.repository.SysUserRoleRelRepository;
|
||||
import com.yunhai.userbase.repository.SysUserCurrentRoleRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
@@ -38,6 +43,10 @@ public class UserCenterService {
|
||||
private SysUserGroupRelRepository sysUserGroupRelRepository;
|
||||
@Autowired
|
||||
private SysGroupRepository sysGroupRepository;
|
||||
@Autowired
|
||||
private SysUserRoleRelRepository sysUserRoleRelRepository;
|
||||
@Autowired
|
||||
private SysUserCurrentRoleRepository sysUserCurrentRoleRepository;
|
||||
|
||||
public BoaUser getUserInfoWithDeptByAccount(String account) {
|
||||
if (account == null) {
|
||||
@@ -77,11 +86,17 @@ public class UserCenterService {
|
||||
boa.setType(user.getType() != null ? user.getType().getKey() : null);
|
||||
// 状态语义对齐:本表 disabled(NORMAL=0 正常 / DISABLED=1 禁用) -> BoaUser(1 正常 / 0 禁用)
|
||||
boa.setStatus(user.getDisabled() == StatusEnum.NORMAL ? 1 : 0);
|
||||
boa.setRoleIds(user.getRoles());
|
||||
// 角色列表:从 sys_user_role_rel(平台身份角色权威源)取正常态 roleId 去重,不再读 sys_user.roles
|
||||
List<Integer> roleIds = sysUserRoleRelRepository
|
||||
.findDistinctRoleIdsByUserIdAndStatus(user.getId(), StatusEnum.NORMAL);
|
||||
boa.setRoleIds(roleIds);
|
||||
|
||||
List<BoaGroup> groups = buildGroups(user.getId());
|
||||
boa.setGroups(groups);
|
||||
// 部门/教育局:仍从组织链推导(与主组/区域组语义绑定)
|
||||
deriveOrg(groups, boa);
|
||||
// 学校/当前激活身份/可选学校:从身份关联表推导(school 语义=当前激活学校)
|
||||
deriveIdentity(user.getId(), boa);
|
||||
return boa;
|
||||
}
|
||||
|
||||
@@ -142,4 +157,50 @@ public class UserCenterService {
|
||||
// 部门:主组(链的近端第一个);若主组本身为学校,则部门即学校
|
||||
boa.setDept(groups.get(0));
|
||||
}
|
||||
|
||||
/**
|
||||
* 从身份关联推导当前激活身份与可选学校。school 字段语义=当前激活学校:<br>
|
||||
* ① 优先读 {@code sys_user_current_role}(用户主动切换后的状态);<br>
|
||||
* ② 无切换记录时兜底取 {@code sys_user_role_rel.is_main=1} 的整条记录(保证是合法存在的身份,
|
||||
* 避免「主组+主角色」组合出 B 学校老师的非法身份);<br>
|
||||
* ③ schools:用户在全部学校下的可选身份去重 schoolId(>0)集合。
|
||||
*/
|
||||
private void deriveIdentity(Integer userId, BoaUser boa) {
|
||||
Integer currentSchoolId = null;
|
||||
String currentSchoolName = null;
|
||||
|
||||
SysUserCurrentRole current = sysUserCurrentRoleRepository.findByUserId(userId).orElse(null);
|
||||
if (current != null && current.getRoleId() != null) {
|
||||
currentSchoolId = current.getSchoolId();
|
||||
currentSchoolName = current.getSchoolName();
|
||||
} else {
|
||||
// 兜底:直接取主身份整条记录(isMain=1),而不是「主组+主角色」组合
|
||||
SysUserRoleRel mainRel = sysUserRoleRelRepository
|
||||
.findByUserIdAndIsMain(userId, 1).orElse(null);
|
||||
if (mainRel != null) {
|
||||
currentSchoolId = mainRel.getSchoolId();
|
||||
if (currentSchoolId != null && currentSchoolId > 0) {
|
||||
SysGroup sch = sysGroupRepository.findById(currentSchoolId).orElse(null);
|
||||
currentSchoolName = sch != null ? sch.getName() : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (currentSchoolId != null && currentSchoolId > 0) {
|
||||
boa.setSchool(toBoaGroup(currentSchoolId, currentSchoolName));
|
||||
}
|
||||
}
|
||||
|
||||
/** 组装一个 BoaGroup(仅填充 id 与 name;name 为 null 时回源查 sys_group) */
|
||||
private BoaGroup toBoaGroup(Integer groupId, String fallbackName) {
|
||||
BoaGroup bg = new BoaGroup();
|
||||
bg.setId(groupId);
|
||||
if (fallbackName != null) {
|
||||
bg.setName(fallbackName);
|
||||
} else {
|
||||
SysGroup g = sysGroupRepository.findById(groupId).orElse(null);
|
||||
bg.setName(g != null ? g.getName() : null);
|
||||
}
|
||||
return bg;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user