diff --git a/src/main/java/com/yunhai/userbase/config/SignKeyInternalIpFilter.java b/src/main/java/com/yunhai/userbase/config/SignKeyInternalIpFilter.java
new file mode 100644
index 0000000..2359640
--- /dev/null
+++ b/src/main/java/com/yunhai/userbase/config/SignKeyInternalIpFilter.java
@@ -0,0 +1,124 @@
+package com.yunhai.userbase.config;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.util.CollectionUtils;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.List;
+
+/**
+ * 内部开放接口(/oapi/internal/*)来源 IP 白名单过滤器。
+ * 防止密钥兜底端点(/oapi/internal/sign-key/{appId})被公网直接枚举调用;
+ * 只允许内网 / 显式配置的白名单来源通过,未命中返回 403。
+ * 支持 X-Forwarded-For / X-Real-IP 等常见代理头解析,取最左侧真实来源 IP 做校验。
+ */
+public class SignKeyInternalIpFilter extends OncePerRequestFilter {
+
+ private static final Logger log = LoggerFactory.getLogger(SignKeyInternalIpFilter.class);
+
+ private final SignKeyInternalProperties properties;
+
+ public SignKeyInternalIpFilter(SignKeyInternalProperties properties) {
+ this.properties = properties;
+ }
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
+ throws ServletException, IOException {
+ if (!properties.isEnabled()) {
+ filterChain.doFilter(request, response);
+ return;
+ }
+ String sourceIp = resolveSourceIp(request);
+ List effectiveIps = properties.getEffectiveIps();
+ if (CollectionUtils.isEmpty(effectiveIps) || matchAny(sourceIp, effectiveIps)) {
+ filterChain.doFilter(request, response);
+ return;
+ }
+ log.warn("[SignKeyInternal] 拒绝非白名单来源访问内部接口: uri={}, sourceIp={}, whitelist={}",
+ request.getRequestURI(), sourceIp, effectiveIps);
+ response.setStatus(HttpServletResponse.SC_FORBIDDEN);
+ response.setContentType("application/json;charset=UTF-8");
+ response.getWriter().write("{\"code\":403,\"msg\":\"内部接口不允许从该来源访问\",\"data\":null}");
+ response.getWriter().flush();
+ }
+
+ private static String resolveSourceIp(HttpServletRequest request) {
+ String[] headers = new String[] {
+ "X-Forwarded-For",
+ "X-Real-IP",
+ "Proxy-Client-IP",
+ "WL-Proxy-Client-IP",
+ "HTTP_CLIENT_IP",
+ "HTTP_X_FORWARDED_FOR"
+ };
+ for (String header : headers) {
+ String raw = request.getHeader(header);
+ if (raw == null || raw.isEmpty() || "unknown".equalsIgnoreCase(raw)) {
+ continue;
+ }
+ // X-Forwarded-For 可能是 多个IP逗号分隔,取最左侧
+ String first = raw.split(",")[0].trim();
+ if (!first.isEmpty()) {
+ return first;
+ }
+ }
+ return request.getRemoteAddr();
+ }
+
+ private static boolean matchAny(String ip, List cidrs) {
+ for (String cidr : cidrs) {
+ if (matchCidr(ip, cidr)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean matchCidr(String ip, String cidr) {
+ if (ip == null || cidr == null) {
+ return false;
+ }
+ try {
+ int prefixLen;
+ String network;
+ if (cidr.contains("/")) {
+ String[] parts = cidr.split("/");
+ network = parts[0].trim();
+ prefixLen = Integer.parseInt(parts[1].trim());
+ } else {
+ network = cidr.trim();
+ // 判断地址族决定默认前缀长度
+ InetAddress na = InetAddress.getByName(network);
+ prefixLen = (na instanceof Inet6Address) ? 128 : 32;
+ }
+ InetAddress ipAddr = InetAddress.getByName(ip);
+ InetAddress netAddr = InetAddress.getByName(network);
+ if (ipAddr.getClass() != netAddr.getClass()) {
+ return false;
+ }
+ byte[] ipBytes = ipAddr.getAddress();
+ byte[] netBytes = netAddr.getAddress();
+ int bits = prefixLen;
+ for (int i = 0; i < ipBytes.length && bits > 0; i++) {
+ int mask = bits >= 8 ? 0xFF : (0xFF << (8 - bits));
+ if ((ipBytes[i] & mask) != (netBytes[i] & mask)) {
+ return false;
+ }
+ bits -= 8;
+ }
+ return true;
+ } catch (UnknownHostException | NumberFormatException ignored) {
+ return false;
+ }
+ }
+}
diff --git a/src/main/java/com/yunhai/userbase/config/SignKeyInternalProperties.java b/src/main/java/com/yunhai/userbase/config/SignKeyInternalProperties.java
new file mode 100644
index 0000000..f5eb76b
--- /dev/null
+++ b/src/main/java/com/yunhai/userbase/config/SignKeyInternalProperties.java
@@ -0,0 +1,55 @@
+package com.yunhai.userbase.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * 内部开放接口(/oapi/internal/*)访问白名单 IP 配置。
+ * 支持单 IP、CIDR(如 10.0.0.0/8、172.16.0.0/12、192.168.0.0/16、127.0.0.1/32)。
+ *
+ * 可在 Nacos 上配置,例如:
+ * yh:
+ * sign-key-internal:
+ * ips:
+ * - 10.0.0.0/8
+ * - 172.16.0.0/12
+ * - 192.168.0.0/16
+ * - 127.0.0.1/32
+ * - 172.20.10.5/32 # 某台固定机器
+ *
+ * 未配置时,默认允许 10/172.16-31/192.168 私有段 + 127.0.0.1(方便本地开发/内网部署)。
+ */
+@ConfigurationProperties(prefix = "yh.sign-key-internal")
+public class SignKeyInternalProperties {
+
+ /** 显式允许的来源 IP(CIDR 或 单 IP)。为空时使用默认私有段规则。 */
+ private List ips = new ArrayList<>();
+
+ /** 是否启用白名单校验;设置 false 可临时关闭(生产不建议)。 */
+ private boolean enabled = true;
+
+ public List getIps() { return ips; }
+
+ public void setIps(List ips) { this.ips = ips; }
+
+ public boolean isEnabled() { return enabled; }
+
+ public void setEnabled(boolean enabled) { this.enabled = enabled; }
+
+ /** 未配置 ips 时返回默认私有段 + localhost 规则。 */
+ public List getEffectiveIps() {
+ if (ips != null && !ips.isEmpty()) {
+ return ips;
+ }
+ return Arrays.asList(
+ "10.0.0.0/8",
+ "172.16.0.0/12",
+ "192.168.0.0/16",
+ "127.0.0.1/32",
+ "0:0:0:0:0:0:0:1/128"
+ );
+ }
+}
diff --git a/src/main/java/com/yunhai/userbase/model/sysuser/BatchChangeStatusRequest.java b/src/main/java/com/yunhai/userbase/model/sysuser/BatchChangeStatusRequest.java
new file mode 100644
index 0000000..28905f7
--- /dev/null
+++ b/src/main/java/com/yunhai/userbase/model/sysuser/BatchChangeStatusRequest.java
@@ -0,0 +1,24 @@
+package com.yunhai.userbase.model.sysuser;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+/**
+ * @author chenx
+ * @date 2026/8/12
+ * @description 批量设置用户状态请求
+ */
+@Data
+public class BatchChangeStatusRequest {
+
+ /** 待设置用户ID列表 */
+ @NotEmpty(message = "待设置用户不能为空")
+ private List userIds;
+
+ /** 是否禁用:true=禁用,false=启用 */
+ @NotNull(message = "状态不能为空")
+ private Boolean disabled;
+}
diff --git a/src/main/java/com/yunhai/userbase/model/sysuser/BatchDeleteRequest.java b/src/main/java/com/yunhai/userbase/model/sysuser/BatchDeleteRequest.java
new file mode 100644
index 0000000..5f1b4cb
--- /dev/null
+++ b/src/main/java/com/yunhai/userbase/model/sysuser/BatchDeleteRequest.java
@@ -0,0 +1,24 @@
+package com.yunhai.userbase.model.sysuser;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+/**
+ * @author chenx
+ * @date 2026/8/12
+ * @description 批量从当前组删除用户请求
+ */
+@Data
+public class BatchDeleteRequest {
+
+ /** 待删除用户ID列表 */
+ @NotEmpty(message = "待删除用户不能为空")
+ private List userIds;
+
+ /** 当前组ID */
+ @NotNull(message = "组ID不能为空")
+ private Integer groupId;
+}
diff --git a/src/main/java/com/yunhai/userbase/model/sysuser/ChangeSortNoRequest.java b/src/main/java/com/yunhai/userbase/model/sysuser/ChangeSortNoRequest.java
new file mode 100644
index 0000000..0631049
--- /dev/null
+++ b/src/main/java/com/yunhai/userbase/model/sysuser/ChangeSortNoRequest.java
@@ -0,0 +1,29 @@
+package com.yunhai.userbase.model.sysuser;
+
+import lombok.Data;
+
+import javax.validation.constraints.Max;
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotNull;
+
+/**
+ * @author chenx
+ * @date 2026/8/12
+ * @description 调整用户在某组内的排序号
+ */
+@Data
+public class ChangeSortNoRequest {
+
+ /** 所属分组(排序号按组维度存储,必须明确是哪个组) */
+ @NotNull(message = "所属分组不能为空")
+ private Integer groupId;
+
+ /**
+ * 组内排序号:1~999 的正整数;越小越靠前。
+ * 允许为 null —— 表示清除排序号,列表回退到 user_id 升序兜底。
+ * 注意:@Min/@Max 对 null 不生效,null 由业务层解释为「清除」。
+ */
+ @Min(1)
+ @Max(999)
+ private Integer sortNo;
+}