编写开放接口,组/用户,移动组等功能
ci-userbase-service / build-and-push (push) Successful in 2m17s

This commit is contained in:
chenxing
2026-08-18 21:46:06 +08:00
parent 7a1dc29ff7
commit 52b29c5082
10 changed files with 388 additions and 61 deletions
@@ -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;
}
}