Compare commits

..
2 Commits
Author SHA1 Message Date
chenxing 0b0be6b8e5 Merge remote-tracking branch 'origin/main' (仓库 .git 修复)
ci-userbase-service / build-and-push (push) Successful in 48s
2026-08-21 10:46:58 +08:00
chenxing f182fe44ce 本地工作区快照:恢复 .git 仓库(合并远端 main 前基线) 2026-08-21 10:46:47 +08:00
4 changed files with 92 additions and 18 deletions
@@ -4,6 +4,7 @@ import com.yunhai.platform.utils.model.BoaGroup;
import com.yunhai.userbase.common.validation.ValidationGroups; import com.yunhai.userbase.common.validation.ValidationGroups;
import com.yunhai.userbase.config.Config; import com.yunhai.userbase.config.Config;
import com.yunhai.userbase.model.sysgroup.SysGroupBatchRequest; import com.yunhai.userbase.model.sysgroup.SysGroupBatchRequest;
import com.yunhai.userbase.model.sysgroup.SysGroupMoveRequest;
import com.yunhai.userbase.model.sysgroup.SysGroupRequest; import com.yunhai.userbase.model.sysgroup.SysGroupRequest;
import com.yunhai.userbase.service.ISysGroupService; import com.yunhai.userbase.service.ISysGroupService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -102,4 +103,14 @@ public class GroupController {
public List<BoaGroup> batchAdd(@RequestBody @Validated SysGroupBatchRequest request) { public List<BoaGroup> batchAdd(@RequestBody @Validated SysGroupBatchRequest request) {
return sysGroupService.batchAdd(request); return sysGroupService.batchAdd(request);
} }
/**
* 移动组(学校限定):仅允许在同一学校(组)子树内移动——moveId、pid 都必须落在 schoolId 子树内。<br>
* 语义复用后台 Move(环检测/类型约束/重排),额外叠加学校边界。鉴权由 OpenApiAuthEnforceConfiguration 在过滤器层强制签名。
*/
@PostMapping("move/{schoolId}")
public void move(@PathVariable Integer schoolId,
@RequestBody @Validated SysGroupMoveRequest request) {
sysGroupService.move(request, schoolId);
}
} }
@@ -35,6 +35,14 @@ public interface ISysGroupService {
*/ */
void delete(Integer id, Integer schoolId); void delete(Integer id, Integer schoolId);
/**
* 开放接口通用:校验学校锚点 schoolId 合法——非空、存在、且必须为学校组(SCHOOL_GROUP)。<br>
* 供 delete/move/用户组关系等带 schoolId 边界的开放接口统一调用。
*
* @param schoolId 学校组ID(限定范围)
*/
void assertSchoolId(Integer schoolId);
List<SysGroupVo> list(SysGroupQuery query); List<SysGroupVo> list(SysGroupQuery query);
/** 获取组织架构树;表为空时自动创建根节点「教育云中心」(pid=0)。 */ /** 获取组织架构树;表为空时自动创建根节点「教育云中心」(pid=0)。 */
@@ -56,6 +64,18 @@ public interface ISysGroupService {
/** 将 moveId 移动到 pid 下(环检测)。 */ /** 将 moveId 移动到 pid 下(环检测)。 */
void move(SysGroupMoveRequest request); void move(SysGroupMoveRequest request);
/**
* 开放接口专用:在学校(组)范围内移动组。<br>
* 与 {@link #move(SysGroupMoveRequest)} 移动逻辑一致,但额外强制边界:<br>
* ① moveId 不能等于 schoolId(禁止移动学校根组);<br>
* ② pid 不能是平台根(0)(禁止移出学校范围);<br>
* ③ moveId 与 pid 都必须属于 schoolId 子树(禁止跨校移动)。<br>
*
* @param request 待移动节点 moveId + 目标父节点 pid
* @param schoolId 学校组ID(限定移动范围)
*/
void move(SysGroupMoveRequest request, Integer schoolId);
/** /**
* 收集指定组自身及其所有子孙组的ID(沿 parentId 向下遍历)。<br> * 收集指定组自身及其所有子孙组的ID(沿 parentId 向下遍历)。<br>
* 用于「查询该节点及其子组下所有用户」等子树聚合场景。<br> * 用于「查询该节点及其子组下所有用户」等子树聚合场景。<br>
@@ -216,6 +216,22 @@ public class SysGroupService implements ISysGroupService {
deleteInternal(id); deleteInternal(id);
} }
/**
* 公共学校锚点校验:schoolId 非空、存在、且必须为学校组(SCHOOL_GROUP)。<br>
* 供本服务 delete/move 及 {@code SysUserService} 等带 schoolId 边界的开放接口统一调用。
*/
@Override
public void assertSchoolId(Integer schoolId) {
if (schoolId == null) {
throw new DException("学校ID不能为空");
}
SysGroup school = sysGroupRepository.findById(schoolId)
.orElseThrow(() -> new DException("学校分组不存在"));
if (school.getType() != GroupTypeEnum.SCHOOL_GROUP) {
throw new DException("学校ID必须为学校分组");
}
}
private void deleteInternal(Integer id) { private void deleteInternal(Integer id) {
SysGroup entity = sysGroupRepository.findById(id) SysGroup entity = sysGroupRepository.findById(id)
.orElseThrow(() -> new DException("系统组不存在")); .orElseThrow(() -> new DException("系统组不存在"));
@@ -243,15 +259,11 @@ public class SysGroupService implements ISysGroupService {
* @param schoolId 学校组ID(限定范围) * @param schoolId 学校组ID(限定范围)
*/ */
private void assertWithinSchool(Integer groupId, Integer schoolId) { private void assertWithinSchool(Integer groupId, Integer schoolId) {
if (schoolId == null) { // 公共锚点校验:schoolId 非空、存在、必须为学校组
throw new DException("学校ID不能为空"); assertSchoolId(schoolId);
}
if (groupId == null) { if (groupId == null) {
throw new DException("组ID不能为空"); throw new DException("组ID不能为空");
} }
if (!sysGroupRepository.existsById(schoolId)) {
throw new DException("学校分组不存在");
}
if (groupId.equals(schoolId)) { if (groupId.equals(schoolId)) {
throw new DException("禁止删除学校根组,请指定具体子组"); throw new DException("禁止删除学校根组,请指定具体子组");
} }
@@ -562,6 +574,45 @@ public class SysGroupService implements ISysGroupService {
publishGroupEvent(PushInterfaceType.UPDATE_GROUP, moveId); publishGroupEvent(PushInterfaceType.UPDATE_GROUP, moveId);
} }
@Override
@Transactional
public void move(SysGroupMoveRequest request, Integer schoolId) {
// 开放接口边界:moveId/pid 必须落在 schoolId 子树内(禁止跨校/移学校根组/移出平台根)
assertMoveWithinSchool(request.getMoveId(), request.getPid(), schoolId);
move(request);
}
/**
* 学校边界校验:确保待移动节点与目标父节点都属于指定学校范围,且不越界。
*
* @param moveId 待移动节点ID
* @param pid 目标父节点ID
* @param schoolId 学校组ID(限定移动范围)
*/
private void assertMoveWithinSchool(Integer moveId, Integer pid, Integer schoolId) {
// 公共锚点校验:schoolId 非空、存在、必须为学校组
assertSchoolId(schoolId);
if (moveId == null) {
throw new DException("待移动节点ID不能为空");
}
if (pid == null) {
throw new DException("目标父节点ID不能为空");
}
if (moveId.equals(schoolId)) {
throw new DException("禁止移动学校根组,请指定具体子组");
}
if (pid == ROOT_PARENT_ID) {
throw new DException("禁止移动到学校范围之外(根节点)");
}
List<Integer> schoolScope = listSelfAndDescendantIds(schoolId);
if (!schoolScope.contains(moveId)) {
throw new DException("待移动节点不属于该学校范围,禁止跨校移动");
}
if (!schoolScope.contains(pid)) {
throw new DException("目标父节点不属于该学校范围,禁止跨校移动");
}
}
/** /**
* 判断 ancestorCandidate 是否为 nodeId 的后代(沿 parentId 向上回溯)。<br> * 判断 ancestorCandidate 是否为 nodeId 的后代(沿 parentId 向上回溯)。<br>
*/ */
@@ -279,15 +279,11 @@ public class SysUserService implements ISysUserService {
* @param schoolId 学校组ID(限定范围) * @param schoolId 学校组ID(限定范围)
*/ */
private void assertGroupWithinSchool(Integer groupId, Integer schoolId) { private void assertGroupWithinSchool(Integer groupId, Integer schoolId) {
if (schoolId == null) { // 公共锚点校验:schoolId 非空、存在、必须为学校组(复用 SysGroupService.assertSchoolId)
throw new DException("学校ID不能为空"); sysGroupService.assertSchoolId(schoolId);
}
if (groupId == null) { if (groupId == null) {
throw new DException("组ID不能为空"); throw new DException("组ID不能为空");
} }
if (!sysGroupRepository.existsById(schoolId)) {
throw new DException("学校分组不存在");
}
// 拿学校根组当 groupId = 把用户从整所学校移除,禁止 // 拿学校根组当 groupId = 把用户从整所学校移除,禁止
if (groupId.equals(schoolId)) { if (groupId.equals(schoolId)) {
throw new DException("禁止对学校根组执行该操作,请指定具体子组"); throw new DException("禁止对学校根组执行该操作,请指定具体子组");
@@ -454,18 +450,14 @@ public class SysUserService implements ISysUserService {
if (groupId == null) { if (groupId == null) {
throw new DException("目标组ID不能为空"); throw new DException("目标组ID不能为空");
} }
if (schoolId == null) { // 公共锚点校验:schoolId 非空、存在、必须为学校组(复用 SysGroupService.assertSchoolId)
throw new DException("学校ID不能为空"); sysGroupService.assertSchoolId(schoolId);
}
if (!sysUserRepository.existsById(userId)) { if (!sysUserRepository.existsById(userId)) {
throw new DException("系统用户不存在"); throw new DException("系统用户不存在");
} }
if (!sysGroupRepository.existsById(groupId)) { if (!sysGroupRepository.existsById(groupId)) {
throw new DException("目标分组不存在"); throw new DException("目标分组不存在");
} }
if (!sysGroupRepository.existsById(schoolId)) {
throw new DException("学校分组不存在");
}
// 学校子树范围(含学校自身):用户与目标组都必须落在此范围内,否则视为跨校移动 // 学校子树范围(含学校自身):用户与目标组都必须落在此范围内,否则视为跨校移动
List<Integer> schoolScope = sysGroupService.listSelfAndDescendantIds(schoolId); List<Integer> schoolScope = sysGroupService.listSelfAndDescendantIds(schoolId);