From f3656057d4ee9a6052651370012b388a46a42e05 Mon Sep 17 00:00:00 2001 From: chenxing Date: Wed, 19 Aug 2026 20:38:00 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8E=A8=E9=80=81=E8=AE=A2=E9=98=85=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E6=89=B9=E9=87=8F=E5=88=A0=E9=99=A4=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yunhai/userbase/config/DWebMvcConfig.java | 5 ++ .../controller/oapi/GroupController.java | 9 ++-- .../controller/oapi/UserController.java | 18 +++++-- .../yunhai/userbase/event/DataSyncEvent.java | 3 ++ .../userbase/service/ISysGroupService.java | 12 +++++ .../userbase/service/ISysUserService.java | 21 ++++++++ .../service/impl/SysGroupService.java | 40 ++++++++++++++ .../userbase/service/impl/SysUserService.java | 52 +++++++++++++++++++ .../userbase/service/push/PushDispatcher.java | 8 +++ 9 files changed, 160 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/yunhai/userbase/config/DWebMvcConfig.java b/src/main/java/com/yunhai/userbase/config/DWebMvcConfig.java index 5695ada..ebb16e9 100644 --- a/src/main/java/com/yunhai/userbase/config/DWebMvcConfig.java +++ b/src/main/java/com/yunhai/userbase/config/DWebMvcConfig.java @@ -12,6 +12,8 @@ public class DWebMvcConfig implements WebMvcConfigurer { @Autowired private BoaUserContextInterceptor boaUserContextInterceptor; + @Autowired + private OapiCallerAppInterceptor oapiCallerAppInterceptor; @Override public void addCorsMappings(CorsRegistry registry) @@ -25,6 +27,9 @@ public class DWebMvcConfig implements WebMvcConfigurer @Override public void addInterceptors(InterceptorRegistry registry) { + // oapi 调用方来源注入:仅命中 /oapi/**,供推送来源排除使用 + registry.addInterceptor(oapiCallerAppInterceptor) + .addPathPatterns("/oapi/**"); registry.addInterceptor(boaUserContextInterceptor) .addPathPatterns("/**") .excludePathPatterns("/login", "/logout", "/static/**", "/error", "/upload/**"); diff --git a/src/main/java/com/yunhai/userbase/controller/oapi/GroupController.java b/src/main/java/com/yunhai/userbase/controller/oapi/GroupController.java index 5cab5cb..86ff82f 100644 --- a/src/main/java/com/yunhai/userbase/controller/oapi/GroupController.java +++ b/src/main/java/com/yunhai/userbase/controller/oapi/GroupController.java @@ -87,11 +87,12 @@ public class GroupController { } /** - * 删除组:组下存在子组或用户时拒绝删除(根节点不可删)。无返回值。 + * 删除组:组下存在子组或用户时拒绝删除(根节点不可删)。
+ * 新增 {@code schoolId} 边界:服务端强制 id 属于该校子树且不等于 schoolId,避免删学校根组/跨校删组。 */ - @PostMapping("delete/{id}") - public void delete(@PathVariable Integer id) { - sysGroupService.delete(id); + @PostMapping("delete/{id}/{schoolId}") + public void delete(@PathVariable Integer id, @PathVariable Integer schoolId) { + sysGroupService.delete(id, schoolId); } /** diff --git a/src/main/java/com/yunhai/userbase/controller/oapi/UserController.java b/src/main/java/com/yunhai/userbase/controller/oapi/UserController.java index 1763a2e..c34b6ba 100644 --- a/src/main/java/com/yunhai/userbase/controller/oapi/UserController.java +++ b/src/main/java/com/yunhai/userbase/controller/oapi/UserController.java @@ -8,6 +8,7 @@ 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.SysUserGroupDeleteItem; import com.yunhai.userbase.model.sysuser.SysUserMoveRequest; import com.yunhai.userbase.model.sysuser.SysUserRequest; import com.yunhai.userbase.repository.ApplicationInfoRepository; @@ -168,11 +169,20 @@ public class UserController { * 删除用户(按组解绑,语义同管理端「从组中删除用户」):仅解除该用户与指定组的关联;
* 仅当用户已无任何组关联时,才级联删除 sys_user 及其角色绑定。
* 这样多校用户只会被退出指定学校,而不会被整体误删。
- * 若用户仅归属这一个组,调用后该用户将被真正删除(组关联清空即触发级联删)。 + * 新增 {@code schoolId} 边界:服务端强制 groupId 属于该校子树且不等于 schoolId,避免跨校/整校移除。 */ - @PostMapping("delete/{id}/{groupId}") - public void delete(@PathVariable Integer id, @PathVariable Integer groupId) { - sysUserService.deleteFromGroup(id, groupId); + @PostMapping("delete/{id}/{groupId}/{schoolId}") + public void delete(@PathVariable Integer id, @PathVariable Integer groupId, @PathVariable Integer schoolId) { + sysUserService.deleteFromGroup(id, groupId, schoolId); + } + + /** + * 批量删除用户-组关系:请求体为 {@code SysUserGroupDeleteItem} 数组,每条携带自身 (userId, groupId, schoolId);
+ * 服务端逐条校验学校边界后解绑,语义与单条 delete 一致。 + */ + @PostMapping("deleteUsers") + public void deleteUsers(@RequestBody List items) { + sysUserService.deleteUsers(items); } /** diff --git a/src/main/java/com/yunhai/userbase/event/DataSyncEvent.java b/src/main/java/com/yunhai/userbase/event/DataSyncEvent.java index c1d667f..267a2aa 100644 --- a/src/main/java/com/yunhai/userbase/event/DataSyncEvent.java +++ b/src/main/java/com/yunhai/userbase/event/DataSyncEvent.java @@ -24,4 +24,7 @@ public class DataSyncEvent { private List userIds; /** 组维度的关联事件(如删除用户=解除组关系)携带的组 id */ private Integer groupId; + /** 触发本次变更的 oapi 调用方 appKey;非 oapi 来源(如管理后台)为 null。 + * 推送分发器据此回推时排除来源应用自身,避免收到自己的回声。 */ + private Integer sourceAppKey; } diff --git a/src/main/java/com/yunhai/userbase/service/ISysGroupService.java b/src/main/java/com/yunhai/userbase/service/ISysGroupService.java index b148b71..0d55f3c 100644 --- a/src/main/java/com/yunhai/userbase/service/ISysGroupService.java +++ b/src/main/java/com/yunhai/userbase/service/ISysGroupService.java @@ -23,6 +23,18 @@ public interface ISysGroupService { void delete(Integer id); + /** + * 开放接口专用:在学校(组)范围内删除组。
+ * 与 {@link #delete(Integer)} 删除逻辑一致,但额外强制边界:
+ * ① id 不能等于 schoolId(禁止删学校根组);
+ * ② id 必须属于 schoolId 子树(禁止跨校删组)。
+ * 任意一条不满足即抛异常。 + * + * @param id 待删除组ID + * @param schoolId 学校组ID(限定删除范围) + */ + void delete(Integer id, Integer schoolId); + List list(SysGroupQuery query); /** 获取组织架构树;表为空时自动创建根节点「教育云中心」(pid=0)。 */ diff --git a/src/main/java/com/yunhai/userbase/service/ISysUserService.java b/src/main/java/com/yunhai/userbase/service/ISysUserService.java index 113f86a..6eafe45 100644 --- a/src/main/java/com/yunhai/userbase/service/ISysUserService.java +++ b/src/main/java/com/yunhai/userbase/service/ISysUserService.java @@ -1,6 +1,7 @@ package com.yunhai.userbase.service; import com.yunhai.userbase.model.sysuser.BatchImportResult; +import com.yunhai.userbase.model.sysuser.SysUserGroupDeleteItem; import com.yunhai.userbase.model.sysuser.SysUserQuery; import com.yunhai.userbase.model.sysuser.SysUserRequest; import com.yunhai.userbase.model.sysuser.SysUserVo; @@ -46,6 +47,26 @@ public interface ISysUserService { */ void batchDeleteFromGroup(List userIds, Integer groupId); + /** + * 开放接口专用:在学校(组)范围内按组解绑用户。
+ * 与 {@link #deleteFromGroup(Integer, Integer)} 语义一致,但额外强制边界:
+ * ① groupId 不能等于 schoolId(禁止对学校根组解绑,避免整校移除);
+ * ② groupId 必须属于 schoolId 子树(禁止跨校解绑)。
+ * 任意一条不满足即抛异常。 + * + * @param userId 用户ID + * @param groupId 当前组ID + * @param schoolId 学校组ID(限定解绑范围) + */ + void deleteFromGroup(Integer userId, Integer groupId, Integer schoolId); + + /** + * 开放接口专用:批量删除用户-组关系;每条携带自身 (userId, groupId, schoolId),逐条做上述学校边界校验后解绑。 + * + * @param items 待删除项列表({@link SysUserGroupDeleteItem}) + */ + void deleteUsers(List items); + /** * 启用/禁用用户。
* diff --git a/src/main/java/com/yunhai/userbase/service/impl/SysGroupService.java b/src/main/java/com/yunhai/userbase/service/impl/SysGroupService.java index 41e499a..ceb29b5 100644 --- a/src/main/java/com/yunhai/userbase/service/impl/SysGroupService.java +++ b/src/main/java/com/yunhai/userbase/service/impl/SysGroupService.java @@ -6,6 +6,7 @@ import com.yunhai.platform.utils.model.BoaGroup; import com.yunhai.userbase.enums.GroupTypeEnum; import com.yunhai.userbase.enums.IEnum; import com.yunhai.userbase.event.DataSyncEvent; +import com.yunhai.userbase.common.context.CallerAppContext; import com.yunhai.userbase.model.push.PushInterfaceType; import org.springframework.context.ApplicationEventPublisher; import com.yunhai.userbase.model.sysgroup.SysGroupBatchRequest; @@ -199,6 +200,18 @@ public class SysGroupService implements ISysGroupService { @Override @Transactional public void delete(Integer id) { + deleteInternal(id); + } + + @Override + @Transactional + public void delete(Integer id, Integer schoolId) { + // 开放接口边界:id 必须属于 schoolId 子树且不能等于 schoolId(禁止删学校根组/跨校删组) + assertWithinSchool(id, schoolId); + deleteInternal(id); + } + + private void deleteInternal(Integer id) { SysGroup entity = sysGroupRepository.findById(id) .orElseThrow(() -> new DException("系统组不存在")); // 根节点不可删除 @@ -218,6 +231,31 @@ public class SysGroupService implements ISysGroupService { publishGroupEvent(PushInterfaceType.DELETE_GROUP, id); } + /** + * 学校边界校验:确保待删除的组属于指定学校范围,且不是学校根组本身。 + * + * @param groupId 待删除组ID + * @param schoolId 学校组ID(限定范围) + */ + private void assertWithinSchool(Integer groupId, Integer schoolId) { + if (schoolId == null) { + throw new DException("学校ID不能为空"); + } + if (groupId == null) { + throw new DException("组ID不能为空"); + } + if (!sysGroupRepository.existsById(schoolId)) { + throw new DException("学校分组不存在"); + } + if (groupId.equals(schoolId)) { + throw new DException("禁止删除学校根组,请指定具体子组"); + } + List schoolScope = listSelfAndDescendantIds(schoolId); + if (!schoolScope.contains(groupId)) { + throw new DException("组不属于该学校范围,禁止跨校操作"); + } + } + @Override public List list(SysGroupQuery query) { List list; @@ -653,6 +691,7 @@ public class SysGroupService implements ISysGroupService { DataSyncEvent e = new DataSyncEvent(); e.setType(type); e.setGroupIds(Collections.singletonList(groupId)); + e.setSourceAppKey(CallerAppContext.getAppKey()); eventPublisher.publishEvent(e); } @@ -663,6 +702,7 @@ public class SysGroupService implements ISysGroupService { DataSyncEvent e = new DataSyncEvent(); e.setType(type); e.setGroupIds(groupIds); + e.setSourceAppKey(CallerAppContext.getAppKey()); eventPublisher.publishEvent(e); } } diff --git a/src/main/java/com/yunhai/userbase/service/impl/SysUserService.java b/src/main/java/com/yunhai/userbase/service/impl/SysUserService.java index c547268..da27f4d 100644 --- a/src/main/java/com/yunhai/userbase/service/impl/SysUserService.java +++ b/src/main/java/com/yunhai/userbase/service/impl/SysUserService.java @@ -9,6 +9,7 @@ import com.yunhai.userbase.enums.UserTypeEnum; import com.yunhai.userbase.entity.SysUser; import com.yunhai.userbase.entity.SysUserGroupRel; import com.yunhai.userbase.model.sysuser.BatchImportResult; +import com.yunhai.userbase.model.sysuser.SysUserGroupDeleteItem; import com.yunhai.userbase.model.sysuser.SysUserQuery; import com.yunhai.userbase.model.sysuser.SysUserRequest; import com.yunhai.userbase.model.sysuser.SysUserVo; @@ -19,6 +20,7 @@ import com.yunhai.userbase.repository.SysUserRepository; import com.yunhai.userbase.service.ISysGroupService; import com.yunhai.userbase.service.ISysUserService; import com.yunhai.userbase.event.DataSyncEvent; +import com.yunhai.userbase.common.context.CallerAppContext; import com.yunhai.userbase.model.push.PushInterfaceType; import org.springframework.context.ApplicationEventPublisher; import cn.afterturn.easypoi.excel.ExcelExportUtil; @@ -243,6 +245,53 @@ public class SysUserService implements ISysUserService { publishUserEvent(PushInterfaceType.DELETE_USER, userIds, groupId); } + @Override + @Transactional + public void deleteFromGroup(Integer userId, Integer groupId, Integer schoolId) { + // 开放接口边界:groupId 必须属于 schoolId 子树且不能等于 schoolId(禁止跨校/整校移除) + assertGroupWithinSchool(groupId, schoolId); + batchDeleteFromGroup(Collections.singletonList(userId), groupId); + } + + @Override + @Transactional + public void deleteUsers(List items) { + if (items == null || items.isEmpty()) { + throw new DException("批量删除用户-组关系列表不能为空"); + } + for (SysUserGroupDeleteItem item : items) { + deleteFromGroup(item.getUserId(), item.getGroupId(), item.getSchoolId()); + } + } + + /** + * 学校边界校验:确保待操作的组属于指定学校范围,且不是学校根组本身。
+ * 与 {@link #moveUserWithinSchool} 的「学校子树范围」判定一致,复用 {@code listSelfAndDescendantIds}。 + * + * @param groupId 待操作组ID(解绑/删除目标) + * @param schoolId 学校组ID(限定范围) + */ + private void assertGroupWithinSchool(Integer groupId, Integer schoolId) { + if (schoolId == null) { + throw new DException("学校ID不能为空"); + } + if (groupId == null) { + throw new DException("组ID不能为空"); + } + if (!sysGroupRepository.existsById(schoolId)) { + throw new DException("学校分组不存在"); + } + // 拿学校根组当 groupId = 把用户从整所学校移除,禁止 + if (groupId.equals(schoolId)) { + throw new DException("禁止对学校根组执行该操作,请指定具体子组"); + } + // groupId 必须落在 schoolId 子树(含子孙,不含学校根组自身——上面已拦截) + List schoolScope = sysGroupService.listSelfAndDescendantIds(schoolId); + if (!schoolScope.contains(groupId)) { + throw new DException("组不属于该学校范围,禁止跨校操作"); + } + } + @Override @Transactional public void changeStatus(Integer id, boolean disabled) { @@ -1008,6 +1057,7 @@ public class SysUserService implements ISysUserService { DataSyncEvent e = new DataSyncEvent(); e.setType(type); e.setUserIds(Collections.singletonList(userId)); + e.setSourceAppKey(CallerAppContext.getAppKey()); eventPublisher.publishEvent(e); } @@ -1018,6 +1068,7 @@ public class SysUserService implements ISysUserService { DataSyncEvent e = new DataSyncEvent(); e.setType(type); e.setUserIds(userIds); + e.setSourceAppKey(CallerAppContext.getAppKey()); eventPublisher.publishEvent(e); } @@ -1030,6 +1081,7 @@ public class SysUserService implements ISysUserService { e.setType(type); e.setUserIds(userIds); e.setGroupId(groupId); + e.setSourceAppKey(CallerAppContext.getAppKey()); eventPublisher.publishEvent(e); } } diff --git a/src/main/java/com/yunhai/userbase/service/push/PushDispatcher.java b/src/main/java/com/yunhai/userbase/service/push/PushDispatcher.java index 58515ce..3a56ddb 100644 --- a/src/main/java/com/yunhai/userbase/service/push/PushDispatcher.java +++ b/src/main/java/com/yunhai/userbase/service/push/PushDispatcher.java @@ -72,6 +72,10 @@ public class PushDispatcher { ? event.getGroupIds() : event.getUserIds(); Integer groupId = event.getGroupId(); for (ApplicationInfo app : applicationInfoRepository.findByPushEnabled(1)) { + // 来源排除:应用自己触发的变更(如 oapi 写接口)不回推给自己,避免收到自己的回声 + if (event.getSourceAppKey() != null && event.getSourceAppKey().equals(app.getAppKey())) { + continue; + } dispatchDeleteToOne(app, type, ids, groupId, "auto"); } return; @@ -88,6 +92,10 @@ public class PushDispatcher { users = sysUserRepository.findAll(); } for (ApplicationInfo app : applicationInfoRepository.findByPushEnabled(1)) { + // 来源排除:应用自己触发的变更(如 oapi 写接口)不回推给自己,避免收到自己的回声 + if (event.getSourceAppKey() != null && event.getSourceAppKey().equals(app.getAppKey())) { + continue; + } dispatchToOne(app, type, groups, users, "auto", false); } } catch (Exception e) {