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; + } +}