diff --git a/src/main/java/com/yunhai/userbase/common/context/CallerAppContext.java b/src/main/java/com/yunhai/userbase/common/context/CallerAppContext.java
new file mode 100644
index 0000000..0450bf8
--- /dev/null
+++ b/src/main/java/com/yunhai/userbase/common/context/CallerAppContext.java
@@ -0,0 +1,27 @@
+package com.yunhai.userbase.common.context;
+
+/**
+ * 开放接口(oapi)调用方来源上下文:在 oapi 请求链路中保存"触发本次数据变更的订阅方 appKey",
+ * 供业务层发布 {@code DataSyncEvent} 时写入事件来源,推送分发器据此回推时排除来源应用自身
+ * (避免应用 A 自己调 oapi 写接口后又收到自己的回声)。
+ * 生命周期:由 {@code OapiCallerAppInterceptor} 在 {@code preHandle} 写入、
+ * {@code afterCompletion} 强制清除(防线程池复用泄漏)。管理后台 /api/* 写接口不走 oapi 拦截器,
+ * 此处为 null,表示"来源非订阅方",推送不对其做来源排除(全量投递)。
+ * 设计与 {@link UserContext} 同套路(ThreadLocal + remove)。
+ */
+public class CallerAppContext {
+
+ private static final ThreadLocal CALLER_APP_KEY = new ThreadLocal<>();
+
+ public static void setAppKey(Integer appKey) {
+ CALLER_APP_KEY.set(appKey);
+ }
+
+ public static Integer getAppKey() {
+ return CALLER_APP_KEY.get();
+ }
+
+ public static void clear() {
+ CALLER_APP_KEY.remove();
+ }
+}
diff --git a/src/main/java/com/yunhai/userbase/config/OapiCallerAppInterceptor.java b/src/main/java/com/yunhai/userbase/config/OapiCallerAppInterceptor.java
new file mode 100644
index 0000000..ed6f41b
--- /dev/null
+++ b/src/main/java/com/yunhai/userbase/config/OapiCallerAppInterceptor.java
@@ -0,0 +1,49 @@
+package com.yunhai.userbase.config;
+
+import com.yunhai.platform.appauth.AppAuthFilter;
+import com.yunhai.userbase.common.context.CallerAppContext;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * oapi 调用方来源注入拦截器:在 oapi 请求处理前,从 yh-app-auth 验签过滤器写入的
+ * {@link AppAuthFilter#APP_AUTH_ATTR}(调用方 appKey)解析出整型 appKey,
+ * 存入 {@link CallerAppContext};请求结束(无论成功/异常)在 {@code afterCompletion} 强制清除,
+ * 防止线程池复用导致 appKey 串号、进而误把其他订阅方排除。
+ * 配合 {@code PushDispatcher} 的来源排除逻辑,实现"应用自己触发的变更不回推给自己"。
+ * 管理后台 /api/* 不走此拦截器 → CallerAppContext 为 null → 推送对该来源不做排除(全量投递)。
+ */
+@Component
+public class OapiCallerAppInterceptor implements HandlerInterceptor {
+
+ @Override
+ public boolean preHandle(HttpServletRequest request,
+ HttpServletResponse response,
+ Object handler) {
+ Object attr = request.getAttribute(AppAuthFilter.APP_AUTH_ATTR);
+ if (attr == null) {
+ return true;
+ }
+ String appKeyStr = (attr instanceof String) ? (String) attr : String.valueOf(attr);
+ if (StringUtils.isBlank(appKeyStr)) {
+ return true;
+ }
+ try {
+ CallerAppContext.setAppKey(Integer.valueOf(appKeyStr.trim()));
+ } catch (NumberFormatException ignored) {
+ // 非法 appKey 格式:不设置来源,等同"非订阅方来源",推送全量投递
+ }
+ return true;
+ }
+
+ @Override
+ public void afterCompletion(HttpServletRequest request,
+ HttpServletResponse response,
+ Object handler, Exception ex) {
+ CallerAppContext.clear();
+ }
+}
diff --git a/src/main/java/com/yunhai/userbase/model/sysuser/SysUserGroupDeleteItem.java b/src/main/java/com/yunhai/userbase/model/sysuser/SysUserGroupDeleteItem.java
new file mode 100644
index 0000000..7645bfb
--- /dev/null
+++ b/src/main/java/com/yunhai/userbase/model/sysuser/SysUserGroupDeleteItem.java
@@ -0,0 +1,40 @@
+package com.yunhai.userbase.model.sysuser;
+
+/**
+ * 批量删除用户-组关系单项(与 BasePlatform 开放接口模型 {@code SysUserGroupDeleteItem} 字段对齐)。
+ * 每条记录携带自身所属学校范围 {@code schoolId},供 {@code SysUserService.deleteUsers} 逐条校验:
+ * groupId 必须属于 schoolId 子树且不能等于 schoolId(禁止跨校解绑 / 整校移除)。
+ */
+public class SysUserGroupDeleteItem {
+
+ /** 待解绑用户 ID */
+ private Integer userId;
+ /** 待解绑的组 ID(必须属于 schoolId 子树,且不能等于 schoolId) */
+ private Integer groupId;
+ /** 学校组 ID:限定该条解绑的学校范围 */
+ private Integer schoolId;
+
+ public Integer getUserId() {
+ return userId;
+ }
+
+ public void setUserId(Integer userId) {
+ this.userId = userId;
+ }
+
+ public Integer getGroupId() {
+ return groupId;
+ }
+
+ public void setGroupId(Integer groupId) {
+ this.groupId = groupId;
+ }
+
+ public Integer getSchoolId() {
+ return schoolId;
+ }
+
+ public void setSchoolId(Integer schoolId) {
+ this.schoolId = schoolId;
+ }
+}