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) {