mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-09-30 00:06:38 +08:00
init
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
package com.admin.common.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 权限控制注解
|
||||
* 用于标记需要管理员权限的方法(role_id = 0)
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface RequireRole {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.admin.common.aop;
|
||||
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@Target({ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface LogAnnotation {}
|
||||
@@ -0,0 +1,193 @@
|
||||
package com.admin.common.aop;
|
||||
|
||||
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import com.admin.common.utils.JwtUtil;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.admin.common.utils.HttpContextUtils;
|
||||
import com.admin.common.utils.IpUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.annotation.*;
|
||||
import org.aspectj.lang.reflect.CodeSignature;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
@Aspect
|
||||
@Slf4j
|
||||
public class LogAspect {
|
||||
|
||||
@Pointcut("@annotation(com.admin.common.aop.LogAnnotation)")
|
||||
public void pt() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回后通知(@AfterReturning):在某连接点(joinpoint)
|
||||
* 正常完成后执行的通知:例如,一个方法没有抛出任何异常,正常返回
|
||||
* 方法执行完毕之后
|
||||
* 注意在这里不能使用ProceedingJoinPoint
|
||||
* 不然会报错ProceedingJoinPoint is only supported for around advice
|
||||
* crmAspect()指向需要控制的方法
|
||||
* returning 注解返回值
|
||||
*
|
||||
* @param joinPoint
|
||||
* @param returnValue 返回值
|
||||
* @throws Exception
|
||||
*/
|
||||
@AfterReturning(value = "pt()", returning = "returnValue")
|
||||
public void log(JoinPoint joinPoint, Object returnValue) throws Throwable {
|
||||
// 获取请求信息
|
||||
HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
|
||||
|
||||
// 获取请求方法类型(POST/GET等)
|
||||
String requestMethod = request.getMethod();
|
||||
|
||||
// 获取用户ID
|
||||
String authorization = request.getHeader("Authorization") + "";
|
||||
Object user_id = "未登录"; // 请求用户的id
|
||||
if (!authorization.equals("null")) {
|
||||
user_id = JwtUtil.getUserIdFromToken(authorization);
|
||||
}
|
||||
|
||||
// 获取请求IP
|
||||
String ipAddr = IpUtils.getIpAddr(request);
|
||||
|
||||
// 获取方法签名信息
|
||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||
Method method = signature.getMethod();
|
||||
|
||||
// 获取控制器方法名
|
||||
String className = joinPoint.getTarget().getClass().getName();
|
||||
String methodName = signature.getName();
|
||||
String controllerMethod = className + "." + methodName;
|
||||
|
||||
|
||||
// 获取请求参数
|
||||
String requestParams = getRequestParams(joinPoint);
|
||||
|
||||
// 获取返回参数
|
||||
String responseParams = returnValue != null ? JSON.toJSONString(returnValue) : "无返回值";
|
||||
|
||||
// 合并为一条完整的日志信息
|
||||
String logMessage = String.format(
|
||||
"【请求日志】用户ID:[%s], IP地址:[%s], 请求方式:[%s], 控制器方法:[%s], 请求参数:[%s], 返回参数:[%s]", user_id, ipAddr, requestMethod, controllerMethod, requestParams, responseParams
|
||||
);
|
||||
|
||||
// 打印单条完整日志
|
||||
log.info(logMessage);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 抛出异常后通知(@AfterThrowing):方法抛出异常退出时执行的通知
|
||||
* 注意在这里不能使用ProceedingJoinPoint
|
||||
* 不然会报错ProceedingJoinPoint is only supported for around advice
|
||||
* throwing注解为错误信息
|
||||
*
|
||||
* @param joinPoint
|
||||
* @param ex
|
||||
*/
|
||||
@AfterThrowing(value = "pt()", throwing = "ex")
|
||||
public void recordLog(JoinPoint joinPoint, Exception ex) {
|
||||
try {
|
||||
// 获取请求信息
|
||||
HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
|
||||
|
||||
// 获取请求方法类型(POST/GET等)
|
||||
String requestMethod = request.getMethod();
|
||||
|
||||
// 获取用户ID
|
||||
String authorization = request.getHeader("Authorization") + "";
|
||||
Object user_id = "未登录"; // 请求用户的id
|
||||
if (!authorization.equals("null")) {
|
||||
user_id = JwtUtil.getUserIdFromToken(authorization);
|
||||
}
|
||||
|
||||
// 获取请求IP
|
||||
String ipAddr = IpUtils.getIpAddr(request);
|
||||
|
||||
// 获取方法签名信息
|
||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||
Method method = signature.getMethod();
|
||||
|
||||
// 获取控制器方法名
|
||||
String className = joinPoint.getTarget().getClass().getName();
|
||||
String methodName = signature.getName();
|
||||
String controllerMethod = className + "." + methodName;
|
||||
|
||||
|
||||
|
||||
// 获取请求参数
|
||||
String requestParams = getRequestParams(joinPoint);
|
||||
|
||||
// 获取异常信息
|
||||
String exceptionMsg = ex != null ? ex.getMessage() : "未知异常";
|
||||
|
||||
// 合并为一条完整的异常日志信息
|
||||
String errorMessage = String.format(
|
||||
"【异常日志】用户ID:[%s], IP地址:[%s], 请求方式:[%s], 控制器方法:[%s], 请求参数:[%s], 异常信息:[%s]", user_id, ipAddr, requestMethod, controllerMethod, requestParams, exceptionMsg
|
||||
);
|
||||
|
||||
// 打印单条完整异常日志
|
||||
log.error(errorMessage, ex);
|
||||
} catch (Exception e) {
|
||||
log.error("记录异常日志时出错: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求参数
|
||||
*/
|
||||
private String getRequestParams(JoinPoint joinPoint) {
|
||||
try {
|
||||
Object[] args = joinPoint.getArgs();
|
||||
if (args.length == 0) {
|
||||
return "无参数";
|
||||
} else if (args[0] != null && args[0].toString().contains("SecurityContextHolderAwareRequestWrapper")) {
|
||||
return JSON.toJSONString(Arrays.toString(ArrayUtil.remove(args, 0)));
|
||||
} else {
|
||||
// 检查是否只有一个参数且已经是JSON字符串格式
|
||||
if (args.length == 1 && args[0] != null) {
|
||||
// 如果参数本身就是字符串且是JSON格式,直接返回
|
||||
if (args[0] instanceof String && ((String) args[0]).startsWith("{") && ((String) args[0]).endsWith("}")) {
|
||||
return (String) args[0];
|
||||
}
|
||||
|
||||
// 如果参数是普通对象,直接序列化
|
||||
try {
|
||||
return JSON.toJSONString(args[0]);
|
||||
} catch (Exception e) {
|
||||
// 如果序列化失败,再尝试使用参数名映射
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
String[] names = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
|
||||
if (names != null) {
|
||||
map.put(names[0], args[0]);
|
||||
return JSON.toJSONString(map);
|
||||
}
|
||||
return JSON.toJSONString(args[0]);
|
||||
}
|
||||
} else {
|
||||
// 多个参数时,使用参数名映射
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
String[] names = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
|
||||
if (names != null) {
|
||||
for (int i = 0; i < names.length; i++) {
|
||||
map.put(names[i], args[i]);
|
||||
}
|
||||
}
|
||||
return JSON.toJSONString(map);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return "获取参数失败: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.admin.common.aop;
|
||||
|
||||
import com.admin.common.annotation.RequireRole;
|
||||
import com.admin.common.lang.R;
|
||||
import com.admin.common.utils.JwtUtil;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* 权限控制切面
|
||||
* 处理 @RequireRole 注解,检查管理员权限(role_id = 0)
|
||||
* 注意:JWT拦截器已经验证了token的有效性,这里只需要检查权限
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
public class RoleAspect {
|
||||
|
||||
@Around("@annotation(requireRole)")
|
||||
public Object checkRole(ProceedingJoinPoint joinPoint, RequireRole requireRole) throws Throwable {
|
||||
// 获取当前请求
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes == null) {
|
||||
return R.err(500, "无法获取请求信息");
|
||||
}
|
||||
|
||||
HttpServletRequest request = attributes.getRequest();
|
||||
String token = request.getHeader("Authorization");
|
||||
|
||||
// JWT拦截器已经验证过token存在且有效,这里直接获取role_id
|
||||
Integer roleId = JwtUtil.getRoleIdFromToken(token);
|
||||
if (roleId == null) {
|
||||
return R.err(401, "无法获取用户权限信息");
|
||||
}
|
||||
|
||||
// 检查是否为管理员(role_id = 0)
|
||||
if (roleId != 0) {
|
||||
return R.err(403, "权限不足,仅管理员可操作");
|
||||
}
|
||||
|
||||
// 权限检查通过,执行原方法
|
||||
return joinPoint.proceed();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
@Data
|
||||
public class ChangePasswordDto {
|
||||
|
||||
@NotBlank(message = "当前密码不能为空")
|
||||
private String currentPassword;
|
||||
|
||||
@NotBlank(message = "新密码不能为空")
|
||||
private String newPassword;
|
||||
|
||||
@NotBlank(message = "确认密码不能为空")
|
||||
private String confirmPassword;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class FlowDto {
|
||||
// [{n=41_tcp, t=cc, u=73225, d=35043}, {n=41_tcp, t=conn, u=35043, d=73225}]
|
||||
// 转发id_类型
|
||||
private String n;
|
||||
|
||||
// 是请求还是接收
|
||||
private String t;
|
||||
|
||||
// 上传流量
|
||||
private Long u;
|
||||
|
||||
// 下载流量
|
||||
private Long d;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
@Data
|
||||
public class ForwardDto {
|
||||
|
||||
@NotBlank(message = "转发名称不能为空")
|
||||
private String name;
|
||||
|
||||
@NotNull(message = "隧道ID不能为空")
|
||||
private Integer tunnelId;
|
||||
|
||||
@NotBlank(message = "远程地址不能为空")
|
||||
private String remoteAddr;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
@Data
|
||||
public class ForwardUpdateDto {
|
||||
|
||||
@NotNull(message = "ID不能为空")
|
||||
private Long id;
|
||||
|
||||
@NotNull(message = "用户ID不能为空")
|
||||
private Integer userId;
|
||||
|
||||
@NotBlank(message = "转发名称不能为空")
|
||||
private String name;
|
||||
|
||||
@NotNull(message = "隧道ID不能为空")
|
||||
private Integer tunnelId;
|
||||
|
||||
@NotBlank(message = "远程地址不能为空")
|
||||
private String remoteAddr;
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 转发信息及关联隧道信息DTO
|
||||
* </p>
|
||||
*
|
||||
* @author QAQ
|
||||
* @since 2025-06-03
|
||||
*/
|
||||
@Data
|
||||
public class ForwardWithTunnelDto {
|
||||
|
||||
/**
|
||||
* 转发记录ID
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
|
||||
/**
|
||||
* 转发名称
|
||||
*/
|
||||
private String name;
|
||||
|
||||
|
||||
/**
|
||||
* 入口端口
|
||||
*/
|
||||
private Integer inPort;
|
||||
|
||||
/**
|
||||
* 远程地址
|
||||
*/
|
||||
private String remoteAddr;
|
||||
|
||||
/**
|
||||
* 转发状态
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private Long createdTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private Long updatedTime;
|
||||
|
||||
// 以下为隧道相关字段
|
||||
|
||||
/**
|
||||
* 隧道名称
|
||||
*/
|
||||
private String tunnelName;
|
||||
|
||||
/**
|
||||
* 入口IP
|
||||
*/
|
||||
private String inIp;
|
||||
|
||||
private String userName;
|
||||
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Integer userId;
|
||||
/**
|
||||
* 隧道ID
|
||||
*/
|
||||
private Integer tunnelId;
|
||||
|
||||
/**
|
||||
* 入站流量(字节)
|
||||
*/
|
||||
private Long inFlow;
|
||||
|
||||
/**
|
||||
* 出站流量(字节)
|
||||
*/
|
||||
private Long outFlow;
|
||||
|
||||
// /**
|
||||
// * 入口端口开始
|
||||
// */
|
||||
// private Integer inPortSta;
|
||||
//
|
||||
// /**
|
||||
// * 入口端口结束
|
||||
// */
|
||||
// private Integer inPortEnd;
|
||||
//
|
||||
// /**
|
||||
// * 出口IP
|
||||
// */
|
||||
// private String outIp;
|
||||
//
|
||||
// /**
|
||||
// * 出口端口开始
|
||||
// */
|
||||
// private Integer outIpSta;
|
||||
//
|
||||
// /**
|
||||
// * 出口端口结束
|
||||
// */
|
||||
// private Integer outIpEnd;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class GostDto {
|
||||
private Integer code;
|
||||
|
||||
private String msg;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
@Data
|
||||
public class LoginDto {
|
||||
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
private String username;
|
||||
|
||||
@NotBlank(message = "密码不能为空")
|
||||
private String password;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Max;
|
||||
import javax.validation.constraints.Min;
|
||||
|
||||
@Data
|
||||
public class NodeDto {
|
||||
|
||||
@NotBlank(message = "节点名称不能为空")
|
||||
private String name;
|
||||
|
||||
@NotNull(message = "控制端口不能为空")
|
||||
@Min(value = 1, message = "端口号必须在1-65535之间")
|
||||
@Max(value = 65535, message = "端口号必须在1-65535之间")
|
||||
private Integer port;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
@Data
|
||||
public class NodeUpdateDto {
|
||||
|
||||
@NotNull(message = "节点ID不能为空")
|
||||
private Long id;
|
||||
|
||||
@NotBlank(message = "节点名称不能为空")
|
||||
private String name;
|
||||
|
||||
@NotBlank(message = "节点IP不能为空")
|
||||
private String ip;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class PageDto {
|
||||
|
||||
/**
|
||||
* 当前页码,默认为1
|
||||
*/
|
||||
private Long current = 1L;
|
||||
|
||||
/**
|
||||
* 每页显示条数,默认为10
|
||||
*/
|
||||
private Long size = 10L;
|
||||
|
||||
/**
|
||||
* 搜索关键字(可选)
|
||||
*/
|
||||
private String keyword;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Min;
|
||||
|
||||
@Data
|
||||
public class SpeedLimitDto {
|
||||
|
||||
@NotBlank(message = "限速规则名称不能为空")
|
||||
private String name;
|
||||
|
||||
@NotNull(message = "速度限制不能为空")
|
||||
@Min(value = 1, message = "速度限制必须大于0")
|
||||
private Integer speed;
|
||||
|
||||
@NotNull(message = "隧道ID不能为空")
|
||||
private Long tunnelId;
|
||||
|
||||
@NotBlank(message = "隧道名称不能为空")
|
||||
private String tunnelName;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Min;
|
||||
|
||||
@Data
|
||||
public class SpeedLimitUpdateDto {
|
||||
|
||||
@NotNull(message = "ID不能为空")
|
||||
private Long id;
|
||||
|
||||
@NotBlank(message = "限速规则名称不能为空")
|
||||
private String name;
|
||||
|
||||
@NotNull(message = "速度限制不能为空")
|
||||
@Min(value = 1, message = "速度限制必须大于0")
|
||||
private Integer speed;
|
||||
|
||||
@NotNull(message = "隧道ID不能为空")
|
||||
private Long tunnelId;
|
||||
|
||||
@NotBlank(message = "隧道名称不能为空")
|
||||
private String tunnelName;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 系统信息DTO
|
||||
* 对应Go客户端上报的系统信息结构
|
||||
*/
|
||||
@Data
|
||||
public class SystemInfoDto {
|
||||
|
||||
/**
|
||||
* 主机IP地址
|
||||
*/
|
||||
@JsonProperty("host_ip")
|
||||
private String hostIp;
|
||||
|
||||
/**
|
||||
* 开机时间(秒)
|
||||
*/
|
||||
@JsonProperty("uptime")
|
||||
private Long uptime;
|
||||
|
||||
/**
|
||||
* 接收字节数
|
||||
*/
|
||||
@JsonProperty("bytes_received")
|
||||
private Long bytesReceived;
|
||||
|
||||
/**
|
||||
* 发送字节数
|
||||
*/
|
||||
@JsonProperty("bytes_transmitted")
|
||||
private Long bytesTransmitted;
|
||||
|
||||
/**
|
||||
* CPU使用率(百分比)
|
||||
*/
|
||||
@JsonProperty("cpu_usage")
|
||||
private Double cpuUsage;
|
||||
|
||||
/**
|
||||
* 内存使用率(百分比)
|
||||
*/
|
||||
@JsonProperty("memory_usage")
|
||||
private Double memoryUsage;
|
||||
|
||||
/**
|
||||
* 上报时间戳
|
||||
*/
|
||||
private Long timestamp;
|
||||
|
||||
public SystemInfoDto() {
|
||||
this.timestamp = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.Max;
|
||||
|
||||
@Data
|
||||
public class TunnelDto {
|
||||
|
||||
@NotBlank(message = "隧道名称不能为空")
|
||||
private String name;
|
||||
|
||||
@NotNull(message = "入口节点不能为空")
|
||||
private Long inNodeId;
|
||||
|
||||
@NotNull(message = "入口端口开始不能为空")
|
||||
@Min(value = 1, message = "入口端口开始必须大于0")
|
||||
@Max(value = 65535, message = "入口端口开始不能超过65535")
|
||||
private Integer inPortSta;
|
||||
|
||||
@NotNull(message = "入口端口结束不能为空")
|
||||
@Min(value = 1, message = "入口端口结束必须大于0")
|
||||
@Max(value = 65535, message = "入口端口结束不能超过65535")
|
||||
private Integer inPortEnd;
|
||||
|
||||
// 出口节点ID,当type=1时可以为空,会自动设置为入口节点ID
|
||||
private Long outNodeId;
|
||||
|
||||
// 出口端口开始,当type=1时可以为空,会自动设置为入口端口
|
||||
private Integer outIpSta;
|
||||
|
||||
// 出口端口结束,当type=1时可以为空,会自动设置为入口端口
|
||||
private Integer outIpEnd;
|
||||
|
||||
@NotNull(message = "隧道类型不能为空")
|
||||
private Integer type;
|
||||
|
||||
@NotNull(message = "流量计算类型不能为空")
|
||||
private Integer flow;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class TunnelListDto {
|
||||
|
||||
private Integer id;
|
||||
|
||||
private String name;
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Min;
|
||||
|
||||
@Data
|
||||
public class UserDto {
|
||||
|
||||
@NotBlank(message = "姓名不能为空")
|
||||
private String name;
|
||||
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
private String user;
|
||||
|
||||
@NotBlank(message = "密码不能为空")
|
||||
private String pwd;
|
||||
|
||||
@NotNull(message = "流量不能为空")
|
||||
@Min(value = 0, message = "流量不能小于0")
|
||||
private Long flow;
|
||||
|
||||
@NotNull(message = "转发数量不能为空")
|
||||
@Min(value = 0, message = "转发数量不能小于0")
|
||||
private Integer num;
|
||||
|
||||
@NotNull(message = "过期时间不能为空")
|
||||
private Long expTime;
|
||||
|
||||
@NotNull(message = "流量重置时间不能为空")
|
||||
private Long flowResetTime;
|
||||
|
||||
private Integer status;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户套餐信息DTO
|
||||
*/
|
||||
@Data
|
||||
public class UserPackageDto {
|
||||
|
||||
/**
|
||||
* 用户基本信息
|
||||
*/
|
||||
private UserInfoDto userInfo;
|
||||
|
||||
/**
|
||||
* 用户隧道权限列表
|
||||
*/
|
||||
private List<UserTunnelDetailDto> tunnelPermissions;
|
||||
|
||||
/**
|
||||
* 用户转发列表
|
||||
*/
|
||||
private List<UserForwardDetailDto> forwards;
|
||||
|
||||
/**
|
||||
* 用户基本信息
|
||||
*/
|
||||
@Data
|
||||
public static class UserInfoDto {
|
||||
private Long id;
|
||||
private String name;
|
||||
private String user;
|
||||
private Integer status;
|
||||
private Long flow; // 总流量配额(GB)
|
||||
private Long inFlow; // 已用入站流量(字节)
|
||||
private Long outFlow; // 已用出站流量(字节)
|
||||
private Integer num; // 转发数量配额
|
||||
private Long expTime; // 过期时间
|
||||
private Long flowResetTime; // 流量重置时间
|
||||
private Long createdTime;
|
||||
private Long updatedTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户隧道权限详情
|
||||
*/
|
||||
@Data
|
||||
public static class UserTunnelDetailDto {
|
||||
private Integer id;
|
||||
private Integer userId;
|
||||
private Integer tunnelId;
|
||||
private String tunnelName;
|
||||
private Integer tunnelFlow; // 隧道流量计算类型(1-单向,2-双向)
|
||||
private Long flow; // 隧道流量配额(GB)
|
||||
private Long inFlow; // 隧道已用入站流量(字节)
|
||||
private Long outFlow; // 隧道已用出站流量(字节)
|
||||
private Integer num; // 隧道转发数量配额
|
||||
private Long flowResetTime; // 流量重置时间
|
||||
private Long expTime; // 隧道权限过期时间
|
||||
private Integer speedId;
|
||||
private String speedLimitName;
|
||||
private Integer speed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户转发详情
|
||||
*/
|
||||
@Data
|
||||
public static class UserForwardDetailDto {
|
||||
private Long id;
|
||||
private String name;
|
||||
private Integer tunnelId;
|
||||
private String tunnelName;
|
||||
private String inIp;
|
||||
private Integer inPort;
|
||||
private String remoteAddr;
|
||||
private Long inFlow; // 转发入站流量(字节)
|
||||
private Long outFlow; // 转发出站流量(字节)
|
||||
private Integer status;
|
||||
private Long createdTime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Min;
|
||||
|
||||
@Data
|
||||
public class UserTunnelDto {
|
||||
|
||||
@NotNull(message = "用户ID不能为空")
|
||||
private Integer userId;
|
||||
|
||||
@NotNull(message = "隧道ID不能为空")
|
||||
private Integer tunnelId;
|
||||
|
||||
@NotNull(message = "流量限制不能为空")
|
||||
@Min(value = 0, message = "流量限制不能小于0")
|
||||
private Long flow;
|
||||
|
||||
@NotNull(message = "转发数量不能为空")
|
||||
@Min(value = 0, message = "转发数量不能小于0")
|
||||
private Integer num;
|
||||
|
||||
/**
|
||||
* 流量重置时间(时间戳)
|
||||
*/
|
||||
@NotNull(message = "流量重置时间不能为空")
|
||||
private Long flowResetTime;
|
||||
|
||||
/**
|
||||
* 到期时间(时间戳)
|
||||
*/
|
||||
@NotNull(message = "到期时间不能为空")
|
||||
private Long expTime;
|
||||
|
||||
/**
|
||||
* 限速规则ID(可选,null表示不限速)
|
||||
*/
|
||||
private Integer speedId;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
@Data
|
||||
public class UserTunnelQueryDto {
|
||||
|
||||
@NotNull
|
||||
private Integer userId;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Min;
|
||||
|
||||
@Data
|
||||
public class UserTunnelUpdateDto {
|
||||
|
||||
@NotNull(message = "用户隧道权限ID不能为空")
|
||||
private Integer id;
|
||||
|
||||
@NotNull(message = "流量限制不能为空")
|
||||
@Min(value = 0, message = "流量限制不能小于0")
|
||||
private Long flow;
|
||||
|
||||
@NotNull(message = "转发数量不能为空")
|
||||
@Min(value = 0, message = "转发数量不能小于0")
|
||||
private Integer num;
|
||||
|
||||
/**
|
||||
* 流量重置时间(时间戳)
|
||||
*/
|
||||
@NotNull(message = "流量重置时间不能为空")
|
||||
private Long flowResetTime;
|
||||
|
||||
/**
|
||||
* 到期时间(时间戳)
|
||||
*/
|
||||
@NotNull(message = "到期时间不能为空")
|
||||
private Long expTime;
|
||||
|
||||
/**
|
||||
* 限速规则ID(可选,null表示不限速)
|
||||
*/
|
||||
private Integer speedId;
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 用户隧道权限及隧道详细信息DTO
|
||||
* </p>
|
||||
*
|
||||
* @author QAQ
|
||||
* @since 2025-06-03
|
||||
*/
|
||||
@Data
|
||||
public class UserTunnelWithDetailDto {
|
||||
|
||||
/**
|
||||
* 用户隧道权限记录ID
|
||||
*/
|
||||
private Integer id;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Integer userId;
|
||||
|
||||
/**
|
||||
* 隧道ID
|
||||
*/
|
||||
private Integer tunnelId;
|
||||
|
||||
/**
|
||||
* 流量限制
|
||||
*/
|
||||
private Integer flow;
|
||||
|
||||
/**
|
||||
* 转发数量限制
|
||||
*/
|
||||
private Integer num;
|
||||
|
||||
/**
|
||||
* 流量重置时间(时间戳)
|
||||
*/
|
||||
private Long flowResetTime;
|
||||
|
||||
/**
|
||||
* 到期时间(时间戳)
|
||||
*/
|
||||
private Long expTime;
|
||||
|
||||
/**
|
||||
* 限速规则ID
|
||||
*/
|
||||
private Integer speedId;
|
||||
|
||||
/**
|
||||
* 限速规则名称
|
||||
*/
|
||||
private String speedLimitName;
|
||||
|
||||
/**
|
||||
* 限速值
|
||||
*/
|
||||
private Integer speed;
|
||||
|
||||
/**
|
||||
* 隧道名称
|
||||
*/
|
||||
private String tunnelName;
|
||||
|
||||
/**
|
||||
* 隧道流量计算类型(1-单向,2-双向)
|
||||
*/
|
||||
private Integer tunnelFlow;
|
||||
|
||||
/**
|
||||
* 入站流量(字节)
|
||||
*/
|
||||
private Long inFlow;
|
||||
|
||||
/**
|
||||
* 出站流量(字节)
|
||||
*/
|
||||
private Long outFlow;
|
||||
|
||||
// /**
|
||||
// * 入口IP
|
||||
// */
|
||||
// private String inIp;
|
||||
//
|
||||
// /**
|
||||
// * 入口端口开始
|
||||
// */
|
||||
// private Integer inPortSta;
|
||||
//
|
||||
// /**
|
||||
// * 入口端口结束
|
||||
// */
|
||||
// private Integer inPortEnd;
|
||||
//
|
||||
// /**
|
||||
// * 出口IP
|
||||
// */
|
||||
// private String outIp;
|
||||
//
|
||||
// /**
|
||||
// * 出口端口开始
|
||||
// */
|
||||
// private Integer outIpSta;
|
||||
//
|
||||
// /**
|
||||
// * 出口端口结束
|
||||
// */
|
||||
// private Integer outIpEnd;
|
||||
//
|
||||
// /**
|
||||
// * 隧道类型(1-端口转发,2-隧道转发)
|
||||
// */
|
||||
// private Integer type;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.admin.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Min;
|
||||
|
||||
@Data
|
||||
public class UserUpdateDto {
|
||||
|
||||
@NotNull(message = "用户ID不能为空")
|
||||
private Long id;
|
||||
|
||||
@NotBlank(message = "姓名不能为空")
|
||||
private String name;
|
||||
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
private String user;
|
||||
|
||||
private String pwd; // 更新时密码可选
|
||||
|
||||
@NotNull(message = "流量不能为空")
|
||||
@Min(value = 0, message = "流量不能小于0")
|
||||
private Long flow;
|
||||
|
||||
@NotNull(message = "转发数量不能为空")
|
||||
@Min(value = 0, message = "转发数量不能小于0")
|
||||
private Integer num;
|
||||
|
||||
@NotNull(message = "过期时间不能为空")
|
||||
private Long expTime;
|
||||
|
||||
@NotNull(message = "流量重置时间不能为空")
|
||||
private Long flowResetTime;
|
||||
|
||||
private Integer status;
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package com.admin.common.exception;
|
||||
|
||||
import com.admin.common.lang.R;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.catalina.connector.ClientAbortException;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.validation.ObjectError;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
@Slf4j
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
//
|
||||
|
||||
// 实体校验异常捕获
|
||||
//@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
@ExceptionHandler(value = MethodArgumentNotValidException.class)
|
||||
public R MethodArgumentNotValidException(MethodArgumentNotValidException e) {
|
||||
BindingResult result = e.getBindingResult();
|
||||
ObjectError objectError = result.getAllErrors().stream().findFirst().get();
|
||||
log.error("实体校验异常:----------------{}", objectError.getDefaultMessage());
|
||||
return R.err(500, objectError.getDefaultMessage());
|
||||
}
|
||||
|
||||
// 未授权异常捕获
|
||||
@ExceptionHandler(value = UnauthorizedException.class)
|
||||
public R handleUnauthorizedException(UnauthorizedException e) {
|
||||
log.error("未授权异常:----------------{}", e.getMessage());
|
||||
return R.err(401, e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(value = Exception.class)
|
||||
public R Exception(Exception e){
|
||||
if (e instanceof ClientAbortException) {
|
||||
return null; // 不需要返回值
|
||||
}
|
||||
log.error("异常:----------------{}", e.getMessage());
|
||||
return R.err(-2, "异常错误");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.admin.common.exception;
|
||||
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.web.client.ResponseErrorHandler;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class HttpErrorHandler implements ResponseErrorHandler {
|
||||
@Override
|
||||
public boolean hasError(ClientHttpResponse response) throws IOException {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleError(ClientHttpResponse clientHttpResponse) throws IOException {
|
||||
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package com.admin.common.exception;
|
||||
|
||||
/**
|
||||
* 未授权异常类
|
||||
*/
|
||||
public class UnauthorizedException extends RuntimeException {
|
||||
|
||||
public UnauthorizedException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public UnauthorizedException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.admin.common.interceptor;
|
||||
|
||||
|
||||
import com.admin.common.exception.UnauthorizedException;
|
||||
import com.admin.common.utils.JwtUtil;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* JWT拦截器,验证用户是否登录
|
||||
*/
|
||||
public class JwtInterceptor implements HandlerInterceptor {
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
||||
String token = request.getHeader("Authorization");
|
||||
|
||||
if (!StringUtils.hasText(token)) {
|
||||
throw new UnauthorizedException("未登录或token已过期");
|
||||
}
|
||||
|
||||
|
||||
if (!JwtUtil.validateToken(token)) {
|
||||
throw new UnauthorizedException("无效的token或token已过期");
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.admin.common.lang;
|
||||
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class R {
|
||||
|
||||
private int code = 0;
|
||||
private String msg = "操作成功";
|
||||
private long ts = System.currentTimeMillis();
|
||||
private Object data;
|
||||
|
||||
|
||||
|
||||
public static R ok(Object data){
|
||||
R m = new R();
|
||||
m.setData(data);
|
||||
return m;
|
||||
}
|
||||
|
||||
public static R ok(){
|
||||
return new R();
|
||||
}
|
||||
|
||||
public static R err(int code, String msg){
|
||||
R m = new R();
|
||||
m.setCode(code);
|
||||
m.setMsg(msg);
|
||||
return m;
|
||||
}
|
||||
|
||||
public static R err(String msg){
|
||||
R m = new R();
|
||||
m.setCode(-1);
|
||||
m.setMsg(msg);
|
||||
return m;
|
||||
}
|
||||
|
||||
public static R err(){
|
||||
R m = new R();
|
||||
m.setCode(-1);
|
||||
m.setMsg("请求失败");
|
||||
return m;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,559 @@
|
||||
package com.admin.common.task;
|
||||
|
||||
|
||||
import com.admin.mapper.UserMapper;
|
||||
import com.admin.mapper.ForwardMapper;
|
||||
import com.admin.mapper.UserTunnelMapper;
|
||||
import com.admin.mapper.TunnelMapper;
|
||||
import com.admin.mapper.NodeMapper;
|
||||
import com.admin.service.UserService;
|
||||
import com.admin.service.UserTunnelService;
|
||||
import com.admin.entity.Forward;
|
||||
import com.admin.entity.Tunnel;
|
||||
import com.admin.entity.Node;
|
||||
import com.admin.entity.UserTunnel;
|
||||
import com.admin.common.dto.GostDto;
|
||||
import com.admin.common.utils.GostUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.DelayQueue;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
public class DelayQueueManager implements CommandLineRunner {
|
||||
|
||||
private final DelayQueue<DelayTask> delayQueue = new DelayQueue<>();
|
||||
|
||||
@Autowired
|
||||
UserMapper userMapper;
|
||||
|
||||
@Resource
|
||||
ForwardMapper forwardMapper;
|
||||
|
||||
@Resource
|
||||
UserTunnelMapper userTunnelMapper;
|
||||
|
||||
@Resource
|
||||
TunnelMapper tunnelMapper;
|
||||
|
||||
@Resource
|
||||
NodeMapper nodeMapper;
|
||||
|
||||
/**
|
||||
* 加入到延时队列中
|
||||
*
|
||||
* @param task
|
||||
*/
|
||||
public void put(DelayTask task) {
|
||||
log.info("加入延时任务:{}", task);
|
||||
delayQueue.put(task);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消延时任务
|
||||
*
|
||||
* @param task
|
||||
* @return
|
||||
*/
|
||||
public boolean remove(DelayTask task) {
|
||||
log.info("取消延时任务:{}", task);
|
||||
return delayQueue.remove(task);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消延时任务
|
||||
*
|
||||
* @param taskid
|
||||
* @return
|
||||
*/
|
||||
public boolean remove(String taskid) {
|
||||
return remove(new DelayTask(new TaskBase(taskid), 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消延时任务
|
||||
*
|
||||
* @param taskid
|
||||
* @return
|
||||
*/
|
||||
public boolean remove_a(String taskid) {
|
||||
return remove(new DelayTask(new TaskBase(taskid), 0));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(String... args) throws Exception {
|
||||
log.info("初始化延时队列");
|
||||
Executors.newSingleThreadExecutor().execute(new Thread(this::excuteThread));
|
||||
|
||||
// 初始化用户账号到期延时任务
|
||||
initUserExpirationTasks();
|
||||
|
||||
// 初始化用户隧道到期延时任务
|
||||
initUserTunnelExpirationTasks();
|
||||
}
|
||||
|
||||
/**
|
||||
* 延时任务执行线程
|
||||
*/
|
||||
private void excuteThread() {
|
||||
while (true) {
|
||||
try {
|
||||
DelayTask task = delayQueue.take();
|
||||
processTask(task);
|
||||
} catch (InterruptedException e) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 内部执行延时任务
|
||||
*
|
||||
* @param task
|
||||
*/
|
||||
private void processTask(DelayTask task) {
|
||||
log.info("执行延时任务:{}", task.getData().toString());
|
||||
TaskBase data = task.getData();
|
||||
switch (data.getType()){
|
||||
case "1": // 账号到期延迟任务
|
||||
handleUserExpiration(data.getData());
|
||||
break;
|
||||
case "2": // 隧道到期延迟任务
|
||||
handleUserTunnelExpiration(data.getData());
|
||||
break;
|
||||
default:
|
||||
log.error("未知延时任务类型:{}", data.getType());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理用户账号到期
|
||||
*
|
||||
* @param userId 用户ID
|
||||
*/
|
||||
private void handleUserExpiration(String userId) {
|
||||
try {
|
||||
log.info("处理用户账号到期,用户ID:{}", userId);
|
||||
Long userIdLong = Long.parseLong(userId);
|
||||
|
||||
// 获取用户信息
|
||||
com.admin.entity.User user = userMapper.selectById(userIdLong);
|
||||
if (user == null) {
|
||||
log.warn("用户不存在,用户ID:{}", userId);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查用户是否确实已过期
|
||||
if (user.getExpTime() != null && user.getExpTime() > System.currentTimeMillis()) {
|
||||
log.info("用户未过期,无需处理,用户ID:{},过期时间:{}", userId, user.getExpTime());
|
||||
return;
|
||||
}
|
||||
|
||||
// 禁用用户账号
|
||||
user.setStatus(0); // 设置为禁用状态
|
||||
user.setUpdatedTime(System.currentTimeMillis());
|
||||
|
||||
int i = userMapper.updateById(user);
|
||||
if (i != 0) {
|
||||
log.info("用户账号已禁用,用户ID:{}", userId);
|
||||
|
||||
// 清理用户相关的活跃连接和服务
|
||||
cleanupUserServices(userIdLong);
|
||||
} else {
|
||||
log.error("禁用用户账号失败,用户ID:{}", userId);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理用户账号到期异常,用户ID:{},错误:{}", userId, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理用户相关服务
|
||||
*
|
||||
* @param userId 用户ID
|
||||
*/
|
||||
private void cleanupUserServices(Long userId) {
|
||||
try {
|
||||
log.info("暂停用户相关转发服务,用户ID:{}", userId);
|
||||
|
||||
// 获取用户的所有转发
|
||||
QueryWrapper<Forward> forwardQuery = new QueryWrapper<>();
|
||||
forwardQuery.eq("user_id", userId);
|
||||
List<Forward> userForwards = forwardMapper.selectList(forwardQuery);
|
||||
|
||||
log.info("找到用户转发数量:{},用户ID:{}", userForwards.size(), userId);
|
||||
|
||||
for (Forward forward : userForwards) {
|
||||
try {
|
||||
// 暂停转发服务
|
||||
pauseForwardService(forward, userId);
|
||||
} catch (Exception e) {
|
||||
log.error("暂停转发服务失败,转发ID:{},用户ID:{},错误:{}", forward.getId(), userId, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("清理用户服务失败,用户ID:{},错误:{}", userId, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停转发服务
|
||||
*
|
||||
* @param forward 转发对象
|
||||
* @param userId 用户ID
|
||||
*/
|
||||
private void pauseForwardService(Forward forward, Long userId) {
|
||||
try {
|
||||
Tunnel tunnel = tunnelMapper.selectById(forward.getTunnelId());
|
||||
if (tunnel == null) {
|
||||
log.warn("隧道不存在,跳过暂停,转发ID:{},隧道ID:{}", forward.getId(), forward.getTunnelId());
|
||||
return;
|
||||
}
|
||||
|
||||
Node inNode = nodeMapper.selectById(tunnel.getInNodeId());
|
||||
if (inNode == null) {
|
||||
log.warn("入口节点不存在,跳过暂停,转发ID:{},节点ID:{}", forward.getId(), tunnel.getInNodeId());
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取用户隧道关系
|
||||
UserTunnel userTunnel = getUserTunnelRelation(userId, tunnel.getId());
|
||||
if (userTunnel == null) {
|
||||
log.warn("用户隧道关系不存在,跳过暂停,用户ID:{},隧道ID:{}", userId, tunnel.getId());
|
||||
return;
|
||||
}
|
||||
|
||||
String serviceName = buildServiceName(forward.getId(), userId, userTunnel.getId());
|
||||
String nodeAddress = buildNodeAddress(inNode);
|
||||
|
||||
// 暂停主服务
|
||||
GostDto result = GostUtil.PauseService(nodeAddress, serviceName, inNode.getSecret());
|
||||
|
||||
// 隧道转发需要同时暂停远端服务
|
||||
if (tunnel.getType() == 2) { // TUNNEL_TYPE_TUNNEL_FORWARD
|
||||
Node outNode = nodeMapper.selectById(tunnel.getOutNodeId());
|
||||
if (outNode != null) {
|
||||
String outNodeAddress = buildNodeAddress(outNode);
|
||||
GostDto remoteResult = GostUtil.PauseRemoteService(outNodeAddress, serviceName, outNode.getSecret());
|
||||
if (!"OK".equals(remoteResult.getMsg())) {
|
||||
log.warn("暂停远端服务失败,转发ID:{},用户ID:{},服务名:{},结果:{}",
|
||||
forward.getId(), userId, serviceName, remoteResult.getMsg());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( "OK".equals(result.getMsg())) {
|
||||
forward.setStatus(0);
|
||||
forwardMapper.updateById(forward);
|
||||
log.info("成功暂停转发服务,转发ID:{},用户ID:{},服务名:{}", forward.getId(), userId, serviceName);
|
||||
} else {
|
||||
log.warn("暂停转发服务失败,转发ID:{},用户ID:{},服务名:{},结果:{}",
|
||||
forward.getId(), userId, serviceName, result.getMsg());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("暂停转发服务异常,转发ID:{},用户ID:{},错误:{}", forward.getId(), userId, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户隧道关系
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param tunnelId 隧道ID
|
||||
* @return 用户隧道关系对象
|
||||
*/
|
||||
private UserTunnel getUserTunnelRelation(Long userId, Long tunnelId) {
|
||||
try {
|
||||
QueryWrapper<UserTunnel> query = new QueryWrapper<>();
|
||||
query.eq("user_id", userId).eq("tunnel_id", tunnelId);
|
||||
return userTunnelMapper.selectOne(query);
|
||||
} catch (Exception e) {
|
||||
log.error("获取用户隧道关系失败,用户ID:{},隧道ID:{},错误:{}", userId, tunnelId, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建服务名称
|
||||
*
|
||||
* @param forwardId 转发ID
|
||||
* @param userId 用户ID
|
||||
* @param userTunnelId 用户隧道ID
|
||||
* @return 服务名称
|
||||
*/
|
||||
private String buildServiceName(Long forwardId, Long userId, Integer userTunnelId) {
|
||||
return forwardId + "_" + userId + "_" + userTunnelId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建节点地址
|
||||
*
|
||||
* @param node 节点对象
|
||||
* @return 节点地址字符串
|
||||
*/
|
||||
private String buildNodeAddress(Node node) {
|
||||
return node.getIp() + ":" + node.getPort();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化用户账号到期延时任务
|
||||
* 查询所有非管理员的正常用户,为有到期时间且未过期的用户创建延时任务
|
||||
*/
|
||||
private void initUserExpirationTasks() {
|
||||
try {
|
||||
log.info("开始初始化用户账号到期延时任务");
|
||||
QueryWrapper<com.admin.entity.User> userQuery = new QueryWrapper<>();
|
||||
userQuery.ne("role_id", 0) // 排除管理员用户
|
||||
.eq("status", 1) // 只查询启用状态的用户
|
||||
.isNotNull("exp_time") // 只查询有到期时间的用户
|
||||
.orderBy(true, true, "exp_time"); // 按到期时间排序
|
||||
|
||||
List<com.admin.entity.User> users = userMapper.selectList(userQuery);
|
||||
|
||||
|
||||
|
||||
for (com.admin.entity.User user : users) {
|
||||
scheduleUserExpirationTask(user);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("初始化用户账号到期延时任务失败:{}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安排用户到期延时任务
|
||||
*
|
||||
* @param user 用户对象
|
||||
*/
|
||||
private void scheduleUserExpirationTask(com.admin.entity.User user) {
|
||||
try {
|
||||
if (user.getExpTime() != null && user.getExpTime() > System.currentTimeMillis()) {
|
||||
// 创建延时任务
|
||||
TaskBase taskBase = new TaskBase(user.getId().toString());
|
||||
taskBase.setType("1"); // 账号到期延迟任务
|
||||
|
||||
long delayTime = user.getExpTime() - System.currentTimeMillis();
|
||||
DelayTask delayTask = new DelayTask(taskBase, delayTime);
|
||||
|
||||
put(delayTask);
|
||||
|
||||
log.debug("已添加用户到期延时任务,用户ID:{},到期时间:{},剩余时间:{}ms",
|
||||
user.getId(), user.getExpTime(), delayTime);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("添加用户到期延时任务失败,用户ID:{},错误:{}", user.getId(), e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理用户隧道到期
|
||||
*
|
||||
* @param userTunnelId 用户隧道ID
|
||||
*/
|
||||
private void handleUserTunnelExpiration(String userTunnelId) {
|
||||
try {
|
||||
log.info("处理用户隧道到期,用户隧道ID:{}", userTunnelId);
|
||||
Integer userTunnelIdInt = Integer.parseInt(userTunnelId);
|
||||
|
||||
// 获取用户隧道信息
|
||||
UserTunnel userTunnel = userTunnelMapper.selectById(userTunnelIdInt);
|
||||
if (userTunnel == null) {
|
||||
log.warn("用户隧道不存在,用户隧道ID:{}", userTunnelId);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查用户隧道是否确实已过期
|
||||
if (userTunnel.getExpTime() != null && userTunnel.getExpTime() > System.currentTimeMillis()) {
|
||||
log.info("用户隧道未过期,无需处理,用户隧道ID:{},过期时间:{}", userTunnelId, userTunnel.getExpTime());
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("用户隧道已过期,开始处理,用户隧道ID:{},用户ID:{},隧道ID:{}",
|
||||
userTunnelId, userTunnel.getUserId(), userTunnel.getTunnelId());
|
||||
|
||||
// 暂停该用户在该隧道上的所有转发服务
|
||||
cleanupUserTunnelServices(userTunnel);
|
||||
|
||||
// 禁用过期的用户隧道权限(设置status为0)
|
||||
userTunnel.setStatus(0);
|
||||
int updateResult = userTunnelMapper.updateById(userTunnel);
|
||||
if (updateResult > 0) {
|
||||
log.info("已禁用过期的用户隧道权限,用户隧道ID:{}", userTunnelId);
|
||||
} else {
|
||||
log.error("禁用过期用户隧道权限失败,用户隧道ID:{}", userTunnelId);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理用户隧道到期异常,用户隧道ID:{},错误:{}", userTunnelId, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理用户隧道相关服务
|
||||
*
|
||||
* @param userTunnel 用户隧道对象
|
||||
*/
|
||||
private void cleanupUserTunnelServices(UserTunnel userTunnel) {
|
||||
try {
|
||||
log.info("暂停用户隧道相关转发服务,用户ID:{},隧道ID:{}", userTunnel.getUserId(), userTunnel.getTunnelId());
|
||||
|
||||
// 获取该用户在该隧道上的所有转发
|
||||
QueryWrapper<Forward> forwardQuery = new QueryWrapper<>();
|
||||
forwardQuery.eq("user_id", userTunnel.getUserId())
|
||||
.eq("tunnel_id", userTunnel.getTunnelId());
|
||||
List<Forward> userTunnelForwards = forwardMapper.selectList(forwardQuery);
|
||||
|
||||
log.info("找到用户隧道转发数量:{},用户ID:{},隧道ID:{}",
|
||||
userTunnelForwards.size(), userTunnel.getUserId(), userTunnel.getTunnelId());
|
||||
|
||||
for (Forward forward : userTunnelForwards) {
|
||||
try {
|
||||
// 暂停转发服务
|
||||
pauseUserTunnelForwardService(forward, userTunnel);
|
||||
} catch (Exception e) {
|
||||
log.error("暂停用户隧道转发服务失败,转发ID:{},用户ID:{},隧道ID:{},错误:{}",
|
||||
forward.getId(), userTunnel.getUserId(), userTunnel.getTunnelId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("清理用户隧道服务失败,用户ID:{},隧道ID:{},错误:{}",
|
||||
userTunnel.getUserId(), userTunnel.getTunnelId(), e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停用户隧道转发服务
|
||||
*
|
||||
* @param forward 转发对象
|
||||
* @param userTunnel 用户隧道对象
|
||||
*/
|
||||
private void pauseUserTunnelForwardService(Forward forward, UserTunnel userTunnel) {
|
||||
try {
|
||||
Tunnel tunnel = tunnelMapper.selectById(forward.getTunnelId());
|
||||
if (tunnel == null) {
|
||||
log.warn("隧道不存在,跳过暂停,转发ID:{},隧道ID:{}", forward.getId(), forward.getTunnelId());
|
||||
return;
|
||||
}
|
||||
|
||||
Node inNode = nodeMapper.selectById(tunnel.getInNodeId());
|
||||
if (inNode == null) {
|
||||
log.warn("入口节点不存在,跳过暂停,转发ID:{},节点ID:{}", forward.getId(), tunnel.getInNodeId());
|
||||
return;
|
||||
}
|
||||
|
||||
String serviceName = buildServiceName(forward.getId(), Long.valueOf(userTunnel.getUserId()), userTunnel.getId());
|
||||
String nodeAddress = buildNodeAddress(inNode);
|
||||
|
||||
// 暂停服务
|
||||
GostDto result = GostUtil.PauseService(nodeAddress, serviceName, inNode.getSecret());
|
||||
|
||||
// 隧道转发需要同时暂停远端服务
|
||||
if (tunnel.getType() == 2) { // TUNNEL_TYPE_TUNNEL_FORWARD
|
||||
Node outNode = nodeMapper.selectById(tunnel.getOutNodeId());
|
||||
if (outNode != null) {
|
||||
String outNodeAddress = buildNodeAddress(outNode);
|
||||
GostDto remoteResult = GostUtil.PauseRemoteService(outNodeAddress, serviceName, outNode.getSecret());
|
||||
if (!"OK".equals(remoteResult.getMsg())) {
|
||||
log.warn("暂停远端服务失败,转发ID:{},用户ID:{},隧道ID:{},服务名:{},结果:{}",
|
||||
forward.getId(), userTunnel.getUserId(), userTunnel.getTunnelId(), serviceName, remoteResult.getMsg());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ("OK".equals(result.getMsg())) {
|
||||
forward.setStatus(0);
|
||||
forwardMapper.updateById(forward);
|
||||
log.info("成功暂停用户隧道转发服务,转发ID:{},用户ID:{},隧道ID:{},服务名:{}",
|
||||
forward.getId(), userTunnel.getUserId(), userTunnel.getTunnelId(), serviceName);
|
||||
} else {
|
||||
log.warn("暂停用户隧道转发服务失败,转发ID:{},用户ID:{},隧道ID:{},服务名:{},结果:{}",
|
||||
forward.getId(), userTunnel.getUserId(), userTunnel.getTunnelId(), serviceName, result.getMsg());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("暂停用户隧道转发服务异常,转发ID:{},用户ID:{},隧道ID:{},错误:{}",
|
||||
forward.getId(), userTunnel.getUserId(), userTunnel.getTunnelId(), e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化用户隧道到期延时任务
|
||||
* 查询所有有到期时间且未过期的用户隧道权限,为其创建延时任务
|
||||
*/
|
||||
private void initUserTunnelExpirationTasks() {
|
||||
try {
|
||||
log.info("开始初始化用户隧道到期延时任务");
|
||||
QueryWrapper<UserTunnel> userTunnelQuery = new QueryWrapper<>();
|
||||
userTunnelQuery.eq("status", 1); // 按到期时间排序
|
||||
|
||||
List<UserTunnel> userTunnels = userTunnelMapper.selectList(userTunnelQuery);
|
||||
|
||||
int taskCount = 0;
|
||||
|
||||
for (UserTunnel userTunnel : userTunnels) {
|
||||
scheduleUserTunnelExpirationTask(userTunnel);
|
||||
}
|
||||
|
||||
log.info("完成初始化用户隧道到期延时任务,总计查询:{},添加任务:{}", userTunnels.size(), taskCount);
|
||||
} catch (Exception e) {
|
||||
log.error("初始化用户隧道到期延时任务失败:{}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安排用户隧道到期延时任务
|
||||
*
|
||||
* @param userTunnel 用户隧道对象
|
||||
*/
|
||||
private void scheduleUserTunnelExpirationTask(UserTunnel userTunnel) {
|
||||
// 创建延时任务
|
||||
TaskBase taskBase = new TaskBase(userTunnel.getId().toString());
|
||||
taskBase.setType("2"); // 隧道到期延迟任务
|
||||
|
||||
long delayTime = userTunnel.getExpTime() - System.currentTimeMillis();
|
||||
DelayTask delayTask = new DelayTask(taskBase, delayTime);
|
||||
|
||||
put(delayTask);
|
||||
|
||||
log.debug("已添加用户隧道到期延时任务,用户隧道ID:{},用户ID:{},隧道ID:{},到期时间:{},剩余时间:{}ms",
|
||||
userTunnel.getId(), userTunnel.getUserId(), userTunnel.getTunnelId(),
|
||||
userTunnel.getExpTime(), delayTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加用户隧道到期延时任务(公共方法,供其他服务调用)
|
||||
*
|
||||
* @param userTunnel 用户隧道对象
|
||||
*/
|
||||
public void addUserTunnelExpirationTask(UserTunnel userTunnel) {
|
||||
scheduleUserTunnelExpirationTask(userTunnel);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除用户隧道到期延时任务(公共方法,供其他服务调用)
|
||||
*
|
||||
* @param userTunnelId 用户隧道ID
|
||||
*/
|
||||
public void removeUserTunnelExpirationTask(Integer userTunnelId) {
|
||||
String taskId = userTunnelId.toString();
|
||||
boolean removed = remove(taskId);
|
||||
if (removed) {
|
||||
log.info("已移除用户隧道到期延时任务,用户隧道ID:{}", userTunnelId);
|
||||
} else {
|
||||
log.debug("未找到需要移除的用户隧道到期延时任务,用户隧道ID:{}", userTunnelId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.admin.common.task;
|
||||
|
||||
import java.util.concurrent.Delayed;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
/**
|
||||
* 延时任务
|
||||
*/
|
||||
public class DelayTask implements Delayed {
|
||||
//任务参数
|
||||
final private TaskBase data;
|
||||
//任务的延时时间,单位毫秒
|
||||
final private long expire;
|
||||
|
||||
/**
|
||||
* 构造延时任务
|
||||
*
|
||||
* @param data 业务数据
|
||||
* @param expire 任务延时时间(ms)
|
||||
*/
|
||||
public DelayTask(TaskBase data, long expire) {
|
||||
super();
|
||||
this.data = data;
|
||||
this.expire = expire + System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public TaskBase getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public long getExpire() {
|
||||
return expire;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof DelayTask) {
|
||||
return this.data.getData().equals(((DelayTask) obj).getData().getData());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "{" + "data:" + data.toString() + "," + "延时时间:"+expire+"}";
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getDelay(TimeUnit unit) {
|
||||
return unit.convert(this.expire - System.currentTimeMillis(), unit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Delayed o) {
|
||||
long delta = getDelay(TimeUnit.NANOSECONDS) - o.getDelay(TimeUnit.NANOSECONDS);
|
||||
return (int) delta;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package com.admin.common.task;
|
||||
|
||||
import com.admin.entity.User;
|
||||
import com.admin.entity.UserTunnel;
|
||||
import com.admin.service.UserService;
|
||||
import com.admin.service.UserTunnelService;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@EnableScheduling
|
||||
public class ResetFlowAsync {
|
||||
|
||||
@Resource
|
||||
UserService userService;
|
||||
|
||||
@Resource
|
||||
UserTunnelService userTunnelService;
|
||||
|
||||
/**
|
||||
* 每天0点执行流量重置任务
|
||||
* 查询出用户和隧道的重置流量日期是今天的数据,将上下流量重置为0
|
||||
* 考虑当月是29、30天,但是选择是31的这种边界情况
|
||||
*
|
||||
* 并发安全说明:
|
||||
* - 使用setSql()进行原子SQL更新,只更新流量字段(in_flow, out_flow)
|
||||
* - 不会影响DelayQueueManager的到期任务对status等其他字段的更新
|
||||
* - 避免了并发修改导致的数据覆盖问题
|
||||
*/
|
||||
@Scheduled(cron = "0 0 0 * * ?")
|
||||
public void reset_flow(){
|
||||
log.info("开始执行流量重置任务");
|
||||
|
||||
try {
|
||||
// 获取当前日期信息
|
||||
LocalDate today = LocalDate.now();
|
||||
int currentDay = today.getDayOfMonth(); // 当前是几号
|
||||
int lastDayOfMonth = today.lengthOfMonth(); // 当月最后一天
|
||||
|
||||
log.info("当前日期: {}, 当月第{}天, 当月最后一天: {}", today, currentDay, lastDayOfMonth);
|
||||
|
||||
// 重置用户流量
|
||||
resetUserFlow(currentDay, lastDayOfMonth);
|
||||
|
||||
// 重置用户隧道流量
|
||||
resetUserTunnelFlow(currentDay, lastDayOfMonth);
|
||||
|
||||
log.info("流量重置任务执行完成");
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("流量重置任务执行失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置用户流量
|
||||
* @param currentDay 当前日期(几号)
|
||||
* @param lastDayOfMonth 当月最后一天
|
||||
*/
|
||||
private void resetUserFlow(int currentDay, int lastDayOfMonth) {
|
||||
try {
|
||||
// flowResetTime字段存储的是1-31的数字,表示每月第几号重置
|
||||
// 构建查询条件:重置日期等于今天,或者重置日期大于当月最大天数且今天是月末
|
||||
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
|
||||
if (currentDay == lastDayOfMonth) {
|
||||
// 如果今天是月末,查询重置日期等于今天或者大于当月最大天数的记录
|
||||
// 例如:当月30天,但用户设置31号重置,则在30号执行重置
|
||||
queryWrapper.and(wrapper -> wrapper.eq("flow_reset_time", currentDay)
|
||||
.or().gt("flow_reset_time", lastDayOfMonth));
|
||||
} else {
|
||||
// 否则只查询重置日期等于今天的记录
|
||||
queryWrapper.eq("flow_reset_time", currentDay);
|
||||
}
|
||||
|
||||
// 查询需要重置的用户
|
||||
List<User> usersToReset = userService.list(queryWrapper);
|
||||
|
||||
if (usersToReset.isEmpty()) {
|
||||
log.info("没有需要重置流量的用户");
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("找到{}个需要重置流量的用户", usersToReset.size());
|
||||
|
||||
// 批量重置用户流量 - 使用SQL原子操作避免与到期任务的并发冲突
|
||||
for (User user : usersToReset) {
|
||||
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
|
||||
updateWrapper.eq("id", user.getId())
|
||||
.setSql("in_flow = 0, out_flow = 0"); // 使用SQL原子操作,只更新流量字段
|
||||
|
||||
boolean success = userService.update(null, updateWrapper);
|
||||
if (success) {
|
||||
log.info("用户[ID: {}, 用户名: {}]流量重置成功,重置日期: 每月{}号",
|
||||
user.getId(), user.getUser(), user.getFlowResetTime());
|
||||
} else {
|
||||
log.error("用户[ID: {}, 用户名: {}]流量重置失败", user.getId(), user.getUser());
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("重置用户流量失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置用户隧道流量
|
||||
* @param currentDay 当前日期(几号)
|
||||
* @param lastDayOfMonth 当月最后一天
|
||||
*/
|
||||
private void resetUserTunnelFlow(int currentDay, int lastDayOfMonth) {
|
||||
try {
|
||||
// flowResetTime字段存储的是1-31的数字,表示每月第几号重置
|
||||
// 构建查询条件:重置日期等于今天,或者重置日期大于当月最大天数且今天是月末
|
||||
QueryWrapper<UserTunnel> queryWrapper = new QueryWrapper<>();
|
||||
if (currentDay == lastDayOfMonth) {
|
||||
// 如果今天是月末,查询重置日期等于今天或者大于当月最大天数的记录
|
||||
// 例如:当月30天,但用户设置31号重置,则在30号执行重置
|
||||
queryWrapper.and(wrapper -> wrapper.eq("flow_reset_time", currentDay)
|
||||
.or().gt("flow_reset_time", lastDayOfMonth));
|
||||
} else {
|
||||
// 否则只查询重置日期等于今天的记录
|
||||
queryWrapper.eq("flow_reset_time", currentDay);
|
||||
}
|
||||
|
||||
// 查询需要重置的用户隧道
|
||||
List<UserTunnel> userTunnelsToReset = userTunnelService.list(queryWrapper);
|
||||
|
||||
if (userTunnelsToReset.isEmpty()) {
|
||||
log.info("没有需要重置流量的用户隧道");
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("找到{}个需要重置流量的用户隧道", userTunnelsToReset.size());
|
||||
|
||||
// 批量重置用户隧道流量 - 使用SQL原子操作避免与到期任务的并发冲突
|
||||
for (UserTunnel userTunnel : userTunnelsToReset) {
|
||||
UpdateWrapper<UserTunnel> updateWrapper = new UpdateWrapper<>();
|
||||
updateWrapper.eq("id", userTunnel.getId())
|
||||
.setSql("in_flow = 0, out_flow = 0"); // 使用SQL原子操作,只更新流量字段
|
||||
|
||||
boolean success = userTunnelService.update(null, updateWrapper);
|
||||
if (success) {
|
||||
log.info("用户隧道[ID: {}, 用户ID: {}, 隧道ID: {}]流量重置成功,重置日期: 每月{}号",
|
||||
userTunnel.getId(), userTunnel.getUserId(), userTunnel.getTunnelId(), userTunnel.getFlowResetTime());
|
||||
} else {
|
||||
log.error("用户隧道[ID: {}, 用户ID: {}, 隧道ID: {}]流量重置失败",
|
||||
userTunnel.getId(), userTunnel.getUserId(), userTunnel.getTunnelId());
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("重置用户隧道流量失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.admin.common.task;
|
||||
|
||||
|
||||
import com.admin.common.dto.GostDto;
|
||||
import com.admin.common.utils.GostUtil;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class SaveConfigAsync {
|
||||
|
||||
|
||||
@Async
|
||||
public void run(String addr, String secret){
|
||||
try {
|
||||
GostUtil.SaveConfig(addr, secret);
|
||||
}catch (Exception e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.admin.common.task;
|
||||
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class TaskBase {
|
||||
private String data;
|
||||
private String type;
|
||||
|
||||
public TaskBase(String data) {
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,393 @@
|
||||
package com.admin.common.utils;
|
||||
|
||||
import com.admin.common.dto.GostDto;
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import org.aspectj.apache.bcel.generic.RET;
|
||||
|
||||
public class GostUtil {
|
||||
|
||||
private static final String API_BASE_URL = "/api/config/";
|
||||
private static final String LIMITERS_ENDPOINT = "limiters";
|
||||
private static final String SERVICES_ENDPOINT = "services";
|
||||
private static final String CHAINS_ENDPOINT = "chains";
|
||||
|
||||
|
||||
public static GostDto SaveConfig(String addr, String secret) {
|
||||
JSONObject data = new JSONObject();
|
||||
data.put("format", "json");
|
||||
String url = "http://" + addr + "/api/config?format=json";
|
||||
return HttpUtils.post(url, data, secret);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 添加限流器配置
|
||||
*
|
||||
* @param addr 服务器地址
|
||||
* @param name 限流器名称
|
||||
* @param speed 限速值(MB)
|
||||
* @param secret 认证密钥
|
||||
* @return 请求结果
|
||||
*/
|
||||
public static GostDto AddLimiters(String addr, Long name, String speed, String secret) {
|
||||
JSONObject data = createLimiterData(name, speed);
|
||||
String url = buildUrl(addr, LIMITERS_ENDPOINT);
|
||||
return HttpUtils.post(url, data, secret);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新限流器配置
|
||||
*
|
||||
* @param addr 服务器地址
|
||||
* @param name 限流器名称
|
||||
* @param speed 限速值(MB)
|
||||
* @param secret 认证密钥
|
||||
* @return 请求结果
|
||||
*/
|
||||
public static GostDto UpdateLimiters(String addr, Long name, String speed, String secret) {
|
||||
JSONObject data = createLimiterData(name, speed);
|
||||
String url = buildUrl(addr, LIMITERS_ENDPOINT + "/" + name);
|
||||
return HttpUtils.put(url, data, secret);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除限流器配置
|
||||
*
|
||||
* @param addr 服务器地址
|
||||
* @param name 限流器名称
|
||||
* @param secret 认证密钥
|
||||
* @return 请求结果
|
||||
*/
|
||||
public static GostDto DeleteLimiters(String addr, Long name, String secret) {
|
||||
String url = buildUrl(addr, LIMITERS_ENDPOINT + "/" + name);
|
||||
return HttpUtils.delete(url, secret);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建限流器数据
|
||||
*/
|
||||
private static JSONObject createLimiterData(Long name, String speed) {
|
||||
JSONObject data = new JSONObject();
|
||||
data.put("name", name.toString());
|
||||
JSONArray limits = new JSONArray();
|
||||
limits.add("$ " + speed + "MB " + speed + "MB");
|
||||
data.put("limits", limits);
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 添加服务配置(支持端口转发和隧道转发)
|
||||
*
|
||||
* @param addr 服务器地址
|
||||
* @param name 服务名称
|
||||
* @param in_port 监听端口
|
||||
* @param limiter 限流器ID
|
||||
* @param remoteAddr 远程地址(端口转发时使用)
|
||||
* @param secret 认证密钥
|
||||
* @param fow_type 转发类型:1=端口转发,2=隧道转发
|
||||
* @return 请求结果
|
||||
*/
|
||||
public static GostDto AddService(String addr, String name, Integer in_port, Integer limiter, String remoteAddr, String secret, Integer fow_type) {
|
||||
JSONArray services = new JSONArray();
|
||||
String[] protocols = {"tcp", "udp"};
|
||||
for (String protocol : protocols) {
|
||||
JSONObject service = createServiceConfig(name, in_port, limiter, remoteAddr, protocol, fow_type);
|
||||
services.add(service);
|
||||
}
|
||||
String url = buildUrl(addr, SERVICES_ENDPOINT + "/batch");
|
||||
return HttpUtils.post(url, services, secret);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新服务配置(批量更新TCP和UDP服务)
|
||||
*
|
||||
* @param addr 服务器地址
|
||||
* @param name 服务名称
|
||||
* @param in_port 监听端口
|
||||
* @param limiter 限流器ID
|
||||
* @param remoteAddr 远程地址(端口转发时使用)
|
||||
* @param secret 认证密钥
|
||||
* @param fow_type 转发类型:1=端口转发,2=隧道转发
|
||||
* @return 请求结果
|
||||
*/
|
||||
public static GostDto UpdateService(String addr, String name, Integer in_port, Integer limiter, String remoteAddr, String secret, Integer fow_type) {
|
||||
JSONArray services = new JSONArray();
|
||||
String[] protocols = {"tcp", "udp"};
|
||||
for (String protocol : protocols) {
|
||||
JSONObject service = createServiceConfig(name, in_port, limiter, remoteAddr, protocol, fow_type);
|
||||
services.add(service);
|
||||
}
|
||||
String url = buildUrl(addr, SERVICES_ENDPOINT + "/batch");
|
||||
return HttpUtils.put(url, services, secret);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除服务配置(批量删除TCP和UDP服务)
|
||||
*
|
||||
* @param addr 服务器地址
|
||||
* @param name 服务名称
|
||||
* @param secret 认证密钥
|
||||
* @return 请求结果
|
||||
*/
|
||||
public static GostDto DeleteService(String addr, String name, String secret) {
|
||||
JSONObject data = new JSONObject();
|
||||
JSONArray services = new JSONArray();
|
||||
services.add(name + "_tcp");
|
||||
services.add(name + "_udp");
|
||||
data.put("services", services);
|
||||
|
||||
String url = buildUrl(addr, SERVICES_ENDPOINT + "/batch");
|
||||
return HttpUtils.delete(url, data, secret);
|
||||
}
|
||||
|
||||
|
||||
public static GostDto PauseService(String addr, String name, String secret) {
|
||||
JSONObject data = new JSONObject();
|
||||
JSONArray services = new JSONArray();
|
||||
services.add(name + "_tcp");
|
||||
services.add(name + "_udp");
|
||||
data.put("services", services);
|
||||
String url = buildUrl(addr, SERVICES_ENDPOINT + "/batch/pause");
|
||||
return HttpUtils.post(url, data, secret);
|
||||
}
|
||||
|
||||
public static GostDto ResumeService(String addr, String name, String secret) {
|
||||
JSONObject data = new JSONObject();
|
||||
JSONArray services = new JSONArray();
|
||||
services.add(name + "_tcp");
|
||||
services.add(name + "_udp");
|
||||
data.put("services", services);
|
||||
String url = buildUrl(addr, SERVICES_ENDPOINT + "/batch/resume");
|
||||
return HttpUtils.post(url, data, secret);
|
||||
}
|
||||
|
||||
public static GostDto PauseRemoteService(String addr, String name, String secret) {
|
||||
JSONObject data = new JSONObject();
|
||||
JSONArray services = new JSONArray();
|
||||
services.add(name + "_tls");
|
||||
data.put("services", services);
|
||||
String url = buildUrl(addr, SERVICES_ENDPOINT + "/batch/pause");
|
||||
return HttpUtils.post(url, data, secret);
|
||||
}
|
||||
|
||||
public static GostDto ResumeRemoteService(String addr, String name, String secret) {
|
||||
JSONObject data = new JSONObject();
|
||||
JSONArray services = new JSONArray();
|
||||
services.add(name + "_tls");
|
||||
data.put("services", services);
|
||||
String url = buildUrl(addr, SERVICES_ENDPOINT + "/batch/resume");
|
||||
return HttpUtils.post(url, data, secret);
|
||||
}
|
||||
|
||||
public static GostDto AddChains(String addr, String name, String remoteAddr, String secret) {
|
||||
JSONObject dialer = new JSONObject();
|
||||
dialer.put("type", "tls");
|
||||
|
||||
JSONObject connector = new JSONObject();
|
||||
connector.put("type", "relay");
|
||||
|
||||
JSONObject node = new JSONObject();
|
||||
node.put("name", "node-" + name);
|
||||
node.put("addr", remoteAddr);
|
||||
node.put("connector", connector);
|
||||
node.put("dialer", dialer);
|
||||
|
||||
JSONArray nodes = new JSONArray();
|
||||
nodes.add(node);
|
||||
|
||||
JSONObject hop = new JSONObject();
|
||||
hop.put("name", "hop-" + name);
|
||||
hop.put("nodes", nodes);
|
||||
|
||||
JSONArray hops = new JSONArray();
|
||||
hops.add(hop);
|
||||
|
||||
JSONObject data = new JSONObject();
|
||||
data.put("name", name + "_chains");
|
||||
data.put("hops", hops);
|
||||
|
||||
String url = buildUrl(addr, CHAINS_ENDPOINT);
|
||||
return HttpUtils.post(url, data, secret);
|
||||
}
|
||||
|
||||
public static GostDto UpdateChains(String addr, String name, String remoteAddr, String secret) {
|
||||
JSONObject dialer = new JSONObject();
|
||||
dialer.put("type", "tls");
|
||||
|
||||
JSONObject connector = new JSONObject();
|
||||
connector.put("type", "relay");
|
||||
|
||||
JSONObject node = new JSONObject();
|
||||
node.put("name", "node-" + name);
|
||||
node.put("addr", remoteAddr);
|
||||
node.put("connector", connector);
|
||||
node.put("dialer", dialer);
|
||||
|
||||
JSONArray nodes = new JSONArray();
|
||||
nodes.add(node);
|
||||
|
||||
JSONObject hop = new JSONObject();
|
||||
hop.put("name", "hop-" + name);
|
||||
hop.put("nodes", nodes);
|
||||
|
||||
JSONArray hops = new JSONArray();
|
||||
hops.add(hop);
|
||||
|
||||
JSONObject data = new JSONObject();
|
||||
data.put("name", name + "_chains");
|
||||
data.put("hops", hops);
|
||||
|
||||
String url = buildUrl(addr, CHAINS_ENDPOINT + "/" + name + "_chains");
|
||||
return HttpUtils.put(url, data, secret);
|
||||
}
|
||||
|
||||
|
||||
public static GostDto DeleteChains(String addr, String name, String secret) {
|
||||
String url = buildUrl(addr, CHAINS_ENDPOINT + "/" + name + "_chains");
|
||||
return HttpUtils.delete(url, secret);
|
||||
}
|
||||
|
||||
public static GostDto AddRemoteService(String addr, String name, Integer out_port, String remoteAddr, String secret) {
|
||||
JSONObject data = new JSONObject();
|
||||
data.put("name", name + "_tls");
|
||||
data.put("addr", ":" + out_port);
|
||||
JSONObject handler = new JSONObject();
|
||||
handler.put("type", "relay");
|
||||
data.put("handler", handler);
|
||||
JSONObject listener = new JSONObject();
|
||||
listener.put("type", "tls");
|
||||
data.put("listener", listener);
|
||||
JSONObject forwarder = new JSONObject();
|
||||
JSONArray nodes = new JSONArray();
|
||||
JSONObject node = new JSONObject();
|
||||
node.put("name", name + "_node");
|
||||
node.put("addr", remoteAddr);
|
||||
nodes.add(node);
|
||||
forwarder.put("nodes", nodes);
|
||||
data.put("forwarder", forwarder);
|
||||
String url = buildUrl(addr, SERVICES_ENDPOINT);
|
||||
return HttpUtils.post(url, data, secret);
|
||||
}
|
||||
|
||||
|
||||
public static GostDto UpdateRemoteService(String addr, String name, Integer out_port, String remoteAddr, String secret) {
|
||||
JSONObject data = new JSONObject();
|
||||
data.put("name", name + "_tls");
|
||||
data.put("addr", ":" + out_port);
|
||||
JSONObject handler = new JSONObject();
|
||||
handler.put("type", "relay");
|
||||
data.put("handler", handler);
|
||||
JSONObject listener = new JSONObject();
|
||||
listener.put("type", "tls");
|
||||
data.put("listener", listener);
|
||||
JSONObject forwarder = new JSONObject();
|
||||
JSONArray nodes = new JSONArray();
|
||||
JSONObject node = new JSONObject();
|
||||
node.put("name", name + "_node");
|
||||
node.put("addr", remoteAddr);
|
||||
nodes.add(node);
|
||||
forwarder.put("nodes", nodes);
|
||||
data.put("forwarder", forwarder);
|
||||
String url = buildUrl(addr, SERVICES_ENDPOINT + "/" + name + "_tls");
|
||||
return HttpUtils.put(url, data, secret);
|
||||
}
|
||||
|
||||
|
||||
public static GostDto DeleteRemoteService(String addr, String name, String secret) {
|
||||
String url = buildUrl(addr, SERVICES_ENDPOINT + "/" + name + "_tls");
|
||||
return HttpUtils.delete(url, secret);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 创建单个服务配置
|
||||
*/
|
||||
private static JSONObject createServiceConfig(String name, Integer in_port, Integer limiter, String remoteAddr, String protocol, Integer fow_type) {
|
||||
JSONObject service = new JSONObject();
|
||||
service.put("name", name + "_" + protocol);
|
||||
service.put("addr", ":" + in_port);
|
||||
|
||||
// 添加限流器配置
|
||||
if (limiter != null) {
|
||||
service.put("limiter", limiter.toString());
|
||||
}
|
||||
|
||||
// 配置处理器
|
||||
JSONObject handler = createHandler(protocol, name, fow_type);
|
||||
service.put("handler", handler);
|
||||
|
||||
// 配置监听器
|
||||
JSONObject listener = createListener(protocol);
|
||||
service.put("listener", listener);
|
||||
|
||||
// 端口转发需要配置转发器
|
||||
if (isPortForwarding(fow_type)) {
|
||||
JSONObject forwarder = createForwarder(protocol, remoteAddr);
|
||||
service.put("forwarder", forwarder);
|
||||
}
|
||||
|
||||
return service;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建处理器配置
|
||||
*/
|
||||
private static JSONObject createHandler(String protocol, String name, Integer fow_type) {
|
||||
JSONObject handler = new JSONObject();
|
||||
handler.put("type", protocol);
|
||||
|
||||
// 隧道转发需要添加链配置
|
||||
if (isTunnelForwarding(fow_type)) {
|
||||
handler.put("chain", name + "_chains");
|
||||
}
|
||||
|
||||
return handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建监听器配置
|
||||
*/
|
||||
private static JSONObject createListener(String protocol) {
|
||||
JSONObject listener = new JSONObject();
|
||||
listener.put("type", protocol);
|
||||
return listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建转发器配置
|
||||
*/
|
||||
private static JSONObject createForwarder(String protocol, String remoteAddr) {
|
||||
JSONObject forwarder = new JSONObject();
|
||||
JSONArray nodes = new JSONArray();
|
||||
JSONObject node = new JSONObject();
|
||||
node.put("name", protocol);
|
||||
node.put("addr", remoteAddr);
|
||||
nodes.add(node);
|
||||
forwarder.put("nodes", nodes);
|
||||
return forwarder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为端口转发
|
||||
*/
|
||||
private static boolean isPortForwarding(Integer fow_type) {
|
||||
return fow_type != null && fow_type == 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为隧道转发
|
||||
*/
|
||||
private static boolean isTunnelForwarding(Integer fow_type) {
|
||||
return fow_type != null && fow_type != 1;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 构建API URL
|
||||
*/
|
||||
private static String buildUrl(String addr, String endpoint) {
|
||||
return "http://" + addr + API_BASE_URL + endpoint;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.admin.common.utils;
|
||||
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
public class HttpContextUtils {
|
||||
|
||||
public static HttpServletRequest getHttpServletRequest(){
|
||||
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
package com.admin.common.utils;
|
||||
|
||||
import com.admin.common.dto.GostDto;
|
||||
import com.admin.common.task.SaveConfigAsync;
|
||||
import com.admin.config.RestTemplateConfig;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import lombok.SneakyThrows;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.NameValuePair;
|
||||
import org.apache.http.client.config.RequestConfig;
|
||||
import org.apache.http.client.entity.UrlEncodedFormEntity;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.client.utils.URIBuilder;
|
||||
import org.apache.http.entity.ContentType;
|
||||
import org.apache.http.entity.StringEntity;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
import org.apache.http.message.BasicNameValuePair;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.ResponseErrorHandler;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* HTTP请求工具类
|
||||
* 支持GET和POST请求,支持表单和JSON格式的请求体
|
||||
*/
|
||||
@Component
|
||||
public class HttpUtils implements ApplicationContextAware {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(HttpUtils.class);
|
||||
|
||||
// 10秒超时配置
|
||||
private static final int TIMEOUT_SECONDS = 10;
|
||||
private static final int TIMEOUT_MILLISECONDS = TIMEOUT_SECONDS * 1000;
|
||||
|
||||
private static ApplicationContext applicationContext;
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext context) {
|
||||
HttpUtils.applicationContext = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取SaveConfigAsync Bean
|
||||
*/
|
||||
private static SaveConfigAsync getSaveConfigAsync() {
|
||||
try {
|
||||
return applicationContext.getBean(SaveConfigAsync.class);
|
||||
} catch (Exception e) {
|
||||
logger.warn("无法获取SaveConfigAsync Bean: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从URL中提取IP和端口
|
||||
*/
|
||||
private static String extractIpAndPortFromUrl(String url) {
|
||||
try {
|
||||
URI uri = URI.create(url);
|
||||
String host = uri.getHost();
|
||||
int port = uri.getPort();
|
||||
if (port == -1) {
|
||||
port = uri.getScheme().equals("https") ? 443 : 80;
|
||||
}
|
||||
return host + ":" + port;
|
||||
} catch (Exception e) {
|
||||
logger.warn("无法从URL提取IP和端口: {}", url);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步保存配置
|
||||
*/
|
||||
private static void asyncSaveConfig(String url, String secret) {
|
||||
try {
|
||||
SaveConfigAsync saveConfigAsync = getSaveConfigAsync();
|
||||
if (saveConfigAsync != null) {
|
||||
String ipAndPort = extractIpAndPortFromUrl(url);
|
||||
saveConfigAsync.run(ipAndPort, secret);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("异步保存配置失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义错误处理器,不抛出异常
|
||||
*/
|
||||
private static class NoOpResponseErrorHandler implements ResponseErrorHandler {
|
||||
@Override
|
||||
public boolean hasError(ClientHttpResponse response) throws IOException {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleError(ClientHttpResponse response) throws IOException {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建带超时配置的RestTemplate
|
||||
*/
|
||||
private static RestTemplate createRestTemplateWithTimeout() {
|
||||
// 创建基础的RequestFactory
|
||||
org.springframework.http.client.SimpleClientHttpRequestFactory factory =
|
||||
new org.springframework.http.client.SimpleClientHttpRequestFactory();
|
||||
|
||||
// 设置连接超时和读取超时
|
||||
factory.setConnectTimeout(TIMEOUT_MILLISECONDS);
|
||||
factory.setReadTimeout(TIMEOUT_MILLISECONDS);
|
||||
|
||||
// 创建RestTemplate
|
||||
RestTemplate restTemplate = new RestTemplate(factory);
|
||||
restTemplate.setErrorHandler(new NoOpResponseErrorHandler());
|
||||
|
||||
return restTemplate;
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public static GostDto post(String url, Object requestBody, String secret) {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
|
||||
String auth = secret + ":" + secret;
|
||||
String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8));
|
||||
headers.set("Authorization", "Basic " + encodedAuth);
|
||||
HttpEntity<Object> entity = new HttpEntity<>(requestBody, headers);
|
||||
RestTemplate restTemplate = createRestTemplateWithTimeout();
|
||||
try {
|
||||
ResponseEntity<GostDto> response = restTemplate.postForEntity(url, entity, GostDto.class);
|
||||
GostDto body = response.getBody();
|
||||
if (body.getMsg() != null && body.getMsg().contains("exists")) {
|
||||
body.setMsg("OK");
|
||||
}
|
||||
|
||||
if (!url.contains("/api/config?format=json")) {
|
||||
asyncSaveConfig(url, secret);
|
||||
}
|
||||
|
||||
return body;
|
||||
} catch (Exception e) {
|
||||
GostDto gostDto = new GostDto();
|
||||
gostDto.setCode(500);
|
||||
gostDto.setMsg("请求失败");
|
||||
return gostDto;
|
||||
}
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public static GostDto put(String url, Object requestBody, String secret) {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
|
||||
String auth = secret + ":" + secret;
|
||||
String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8));
|
||||
headers.set("Authorization", "Basic " + encodedAuth);
|
||||
HttpEntity<Object> entity = new HttpEntity<>(requestBody, headers);
|
||||
RestTemplate restTemplate = createRestTemplateWithTimeout();
|
||||
try {
|
||||
ResponseEntity<GostDto> response = restTemplate.exchange(
|
||||
url,
|
||||
HttpMethod.PUT,
|
||||
entity,
|
||||
GostDto.class
|
||||
);
|
||||
GostDto body = response.getBody();
|
||||
asyncSaveConfig(url, secret);
|
||||
return body;
|
||||
} catch (Exception e) {
|
||||
GostDto gostDto = new GostDto();
|
||||
gostDto.setCode(500);
|
||||
gostDto.setMsg("请求失败");
|
||||
return gostDto;
|
||||
}
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public static GostDto delete(String url, String secret) {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
|
||||
|
||||
// Basic Auth
|
||||
String auth = secret + ":" + secret;
|
||||
String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8));
|
||||
headers.set("Authorization", "Basic " + encodedAuth);
|
||||
|
||||
HttpEntity<Void> entity = new HttpEntity<>(headers);
|
||||
RestTemplate restTemplate = createRestTemplateWithTimeout();
|
||||
|
||||
try {
|
||||
ResponseEntity<GostDto> response = restTemplate.exchange(
|
||||
url,
|
||||
HttpMethod.DELETE,
|
||||
entity,
|
||||
GostDto.class
|
||||
);
|
||||
GostDto body = response.getBody();
|
||||
if (body != null && body.getMsg() != null && body.getMsg().contains("not found")) {
|
||||
body.setMsg("OK");
|
||||
}
|
||||
asyncSaveConfig(url, secret);
|
||||
return body;
|
||||
} catch (Exception e) {
|
||||
GostDto gostDto = new GostDto();
|
||||
gostDto.setCode(500);
|
||||
gostDto.setMsg("请求失败");
|
||||
return gostDto;
|
||||
}
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public static GostDto delete(String url, JSONObject data, String secret) {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
|
||||
|
||||
// Basic Auth
|
||||
String auth = secret + ":" + secret;
|
||||
String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8));
|
||||
headers.set("Authorization", "Basic " + encodedAuth);
|
||||
|
||||
HttpEntity<JSONObject> entity = new HttpEntity<>(data, headers);
|
||||
RestTemplate restTemplate = createRestTemplateWithTimeout();
|
||||
|
||||
try {
|
||||
ResponseEntity<GostDto> response = restTemplate.exchange(
|
||||
url,
|
||||
HttpMethod.DELETE,
|
||||
entity,
|
||||
GostDto.class
|
||||
);
|
||||
GostDto body = response.getBody();
|
||||
if (body != null && body.getMsg() != null && body.getMsg().contains("not found")) {
|
||||
body.setMsg("OK");
|
||||
}
|
||||
asyncSaveConfig(url, secret);
|
||||
return body;
|
||||
} catch (Exception e) {
|
||||
GostDto gostDto = new GostDto();
|
||||
gostDto.setCode(500);
|
||||
gostDto.setMsg("请求失败");
|
||||
return gostDto;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.admin.common.utils;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
public class IpUtils {
|
||||
|
||||
public static String getIpAddr(HttpServletRequest request) {
|
||||
String ipAddress = null;
|
||||
try {
|
||||
ipAddress = request.getHeader("x-forwarded-for");
|
||||
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
|
||||
ipAddress = request.getHeader("Proxy-Client-IP");
|
||||
}
|
||||
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
|
||||
ipAddress = request.getHeader("WL-Proxy-Client-IP");
|
||||
}
|
||||
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
|
||||
ipAddress = request.getRemoteAddr();
|
||||
if (ipAddress.equals("127.0.0.1")) {
|
||||
// 根据网卡取本机配置的IP
|
||||
InetAddress inet = null;
|
||||
try {
|
||||
inet = InetAddress.getLocalHost();
|
||||
} catch (UnknownHostException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
ipAddress = inet.getHostAddress();
|
||||
}
|
||||
}
|
||||
// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
|
||||
if (ipAddress != null && ipAddress.length() > 15) {
|
||||
// "***.***.***.***".length()
|
||||
// = 15
|
||||
if (ipAddress.indexOf(",") > 0) {
|
||||
ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
ipAddress="";
|
||||
}
|
||||
return ipAddress;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
package com.admin.common.utils;
|
||||
|
||||
import com.admin.entity.User;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import lombok.SneakyThrows;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* JWT工具类,不使用第三方库实现
|
||||
*/
|
||||
@Component
|
||||
public class JwtUtil {
|
||||
|
||||
@Value("${jwt-secret}")
|
||||
private String secretKey;
|
||||
|
||||
private static String SECRET_KEY;
|
||||
|
||||
// token有效期,7天
|
||||
private static final long EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000;
|
||||
// 算法
|
||||
private static final String ALGORITHM = "HmacSHA256";
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
SECRET_KEY = this.secretKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成JWT Token
|
||||
*
|
||||
* @param user 用户信息
|
||||
* @return 生成的JWT Token
|
||||
*/
|
||||
public static String generateToken(User user) {
|
||||
try {
|
||||
long nowMillis = System.currentTimeMillis();
|
||||
Date now = new Date(nowMillis);
|
||||
Date expireDate = new Date(nowMillis + EXPIRE_TIME);
|
||||
|
||||
// Header
|
||||
Map<String, Object> header = new HashMap<>();
|
||||
header.put("alg", ALGORITHM);
|
||||
header.put("typ", "JWT");
|
||||
String headerJson = JSON.toJSONString(header);
|
||||
String encodedHeader = Base64.getUrlEncoder().withoutPadding()
|
||||
.encodeToString(headerJson.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// Payload
|
||||
Map<String, Object> payload = new HashMap<>();
|
||||
payload.put("sub", user.getId().toString());
|
||||
payload.put("iat", now.getTime() / 1000); // 发布时间
|
||||
payload.put("exp", expireDate.getTime() / 1000); // 过期时间
|
||||
payload.put("user", user.getUser());
|
||||
payload.put("name", user.getName());
|
||||
payload.put("role_id", user.getRoleId());
|
||||
|
||||
String payloadJson = JSON.toJSONString(payload);
|
||||
String encodedPayload = Base64.getUrlEncoder().withoutPadding()
|
||||
.encodeToString(payloadJson.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// Signature
|
||||
String signature = calculateSignature(encodedHeader, encodedPayload);
|
||||
|
||||
// Token
|
||||
return encodedHeader + "." + encodedPayload + "." + signature;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("JWT token generation failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证JWT Token
|
||||
*
|
||||
* @param token JWT Token
|
||||
* @return 验证是否通过
|
||||
*/
|
||||
public static boolean validateToken(String token) {
|
||||
try {
|
||||
if (token == null || token.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String[] parts = token.split("\\.");
|
||||
if (parts.length != 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String encodedHeader = parts[0];
|
||||
String encodedPayload = parts[1];
|
||||
String signature = parts[2];
|
||||
|
||||
// 验证签名
|
||||
String expectedSignature = calculateSignature(encodedHeader, encodedPayload);
|
||||
if (!expectedSignature.equals(signature)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 验证过期时间
|
||||
String decodedPayload = new String(Base64.getUrlDecoder().decode(encodedPayload), StandardCharsets.UTF_8);
|
||||
Map<String, Object> payload = JSON.parseObject(decodedPayload, Map.class);
|
||||
long exp = Long.parseLong(payload.get("exp").toString());
|
||||
long now = System.currentTimeMillis() / 1000;
|
||||
|
||||
return exp > now;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从JWT Token中获取用户ID
|
||||
*
|
||||
* @param token JWT Token
|
||||
* @return 用户ID
|
||||
*/
|
||||
public static Long getUserIdFromToken(String token) {
|
||||
String[] parts = token.split("\\.");
|
||||
String encodedPayload = parts[1];
|
||||
String decodedPayload = new String(Base64.getUrlDecoder().decode(encodedPayload), StandardCharsets.UTF_8);
|
||||
Map<String, Object> payload = JSON.parseObject(decodedPayload, Map.class);
|
||||
return Long.parseLong(payload.get("sub").toString());
|
||||
}
|
||||
|
||||
|
||||
public static Integer getUserIdFromToken() {
|
||||
String token = HttpContextUtils.getHttpServletRequest().getHeader("Authorization");
|
||||
String[] parts = token.split("\\.");
|
||||
String encodedPayload = parts[1];
|
||||
String decodedPayload = new String(Base64.getUrlDecoder().decode(encodedPayload), StandardCharsets.UTF_8);
|
||||
Map<String, Object> payload = JSON.parseObject(decodedPayload, Map.class);
|
||||
return Integer.parseInt(payload.get("sub").toString());
|
||||
}
|
||||
|
||||
public static String getNameFromToken() {
|
||||
String token = HttpContextUtils.getHttpServletRequest().getHeader("Authorization");
|
||||
String[] parts = token.split("\\.");
|
||||
String encodedPayload = parts[1];
|
||||
String decodedPayload = new String(Base64.getUrlDecoder().decode(encodedPayload), StandardCharsets.UTF_8);
|
||||
Map<String, Object> payload = JSON.parseObject(decodedPayload, Map.class);
|
||||
return payload.get("name").toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从JWT Token中获取用户角色ID
|
||||
*
|
||||
* @param token JWT Token
|
||||
* @return 角色ID
|
||||
*/
|
||||
public static Integer getRoleIdFromToken(String token) {
|
||||
String[] parts = token.split("\\.");
|
||||
String encodedPayload = parts[1];
|
||||
String decodedPayload = new String(Base64.getUrlDecoder().decode(encodedPayload), StandardCharsets.UTF_8);
|
||||
Map<String, Object> payload = JSON.parseObject(decodedPayload, Map.class);
|
||||
return Integer.parseInt(payload.get("role_id").toString());
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public static Integer getRoleIdFromToken() {
|
||||
String token = HttpContextUtils.getHttpServletRequest().getHeader("Authorization");
|
||||
if (token == null || token.isEmpty()) throw new Exception();
|
||||
String[] parts = token.split("\\.");
|
||||
String encodedPayload = parts[1];
|
||||
String decodedPayload = new String(Base64.getUrlDecoder().decode(encodedPayload), StandardCharsets.UTF_8);
|
||||
Map<String, Object> payload = JSON.parseObject(decodedPayload, Map.class);
|
||||
return Integer.parseInt(payload.get("role_id").toString());
|
||||
}
|
||||
/**
|
||||
* 计算签名
|
||||
*
|
||||
* @param encodedHeader 编码后的头部
|
||||
* @param encodedPayload 编码后的负载
|
||||
* @return 签名
|
||||
* @throws Exception 签名计算异常
|
||||
*/
|
||||
private static String calculateSignature(String encodedHeader, String encodedPayload) throws Exception {
|
||||
String content = encodedHeader + "." + encodedPayload;
|
||||
Mac hmac = Mac.getInstance(ALGORITHM);
|
||||
SecretKeySpec secretKeySpec = new SecretKeySpec(SECRET_KEY.getBytes(StandardCharsets.UTF_8), ALGORITHM);
|
||||
hmac.init(secretKeySpec);
|
||||
byte[] signatureBytes = hmac.doFinal(content.getBytes(StandardCharsets.UTF_8));
|
||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(signatureBytes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package com.admin.common.utils;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* MD5工具类
|
||||
*/
|
||||
public class Md5Util {
|
||||
|
||||
private static final String MD5_ALGORITHM = "MD5";
|
||||
private static final String DEFAULT_SALT = "admin_salt_2024";
|
||||
|
||||
/**
|
||||
* 基础MD5加密
|
||||
*
|
||||
* @param input 待加密字符串
|
||||
* @return MD5加密后的字符串(32位小写)
|
||||
*/
|
||||
public static String md5(String input) {
|
||||
if (input == null || input.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance(MD5_ALGORITHM);
|
||||
byte[] digest = md.digest(input.getBytes());
|
||||
return bytesToHex(digest);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException("MD5算法不可用", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* MD5加密(使用默认盐值)
|
||||
*
|
||||
* @param input 待加密字符串
|
||||
* @return MD5加密后的字符串
|
||||
*/
|
||||
public static String md5WithSalt(String input) {
|
||||
return md5WithSalt(input, DEFAULT_SALT);
|
||||
}
|
||||
|
||||
/**
|
||||
* MD5加密(使用自定义盐值)
|
||||
*
|
||||
* @param input 待加密字符串
|
||||
* @param salt 盐值
|
||||
* @return MD5加密后的字符串
|
||||
*/
|
||||
public static String md5WithSalt(String input, String salt) {
|
||||
if (input == null || input.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (salt == null) {
|
||||
salt = DEFAULT_SALT;
|
||||
}
|
||||
|
||||
return md5(input + salt);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机盐值
|
||||
*
|
||||
* @param length 盐值长度
|
||||
* @return 随机盐值
|
||||
*/
|
||||
public static String generateSalt(int length) {
|
||||
SecureRandom random = new SecureRandom();
|
||||
byte[] salt = new byte[length];
|
||||
random.nextBytes(salt);
|
||||
return Base64.getEncoder().encodeToString(salt);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成默认长度(16字节)的随机盐值
|
||||
*
|
||||
* @return 随机盐值
|
||||
*/
|
||||
public static String generateSalt() {
|
||||
return generateSalt(16);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证密码
|
||||
*
|
||||
* @param password 原始密码
|
||||
* @param hashedPassword 已加密的密码
|
||||
* @return 是否匹配
|
||||
*/
|
||||
public static boolean verify(String password, String hashedPassword) {
|
||||
if (password == null || hashedPassword == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String encrypted = md5WithSalt(password);
|
||||
return encrypted.equals(hashedPassword);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证密码(使用自定义盐值)
|
||||
*
|
||||
* @param password 原始密码
|
||||
* @param salt 盐值
|
||||
* @param hashedPassword 已加密的密码
|
||||
* @return 是否匹配
|
||||
*/
|
||||
public static boolean verify(String password, String salt, String hashedPassword) {
|
||||
if (password == null || hashedPassword == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String encrypted = md5WithSalt(password, salt);
|
||||
return encrypted.equals(hashedPassword);
|
||||
}
|
||||
|
||||
/**
|
||||
* 多次MD5加密
|
||||
*
|
||||
* @param input 待加密字符串
|
||||
* @param times 加密次数
|
||||
* @return 加密后的字符串
|
||||
*/
|
||||
public static String md5Multiple(String input, int times) {
|
||||
if (input == null || input.isEmpty() || times <= 0) {
|
||||
return input;
|
||||
}
|
||||
|
||||
String result = input;
|
||||
for (int i = 0; i < times; i++) {
|
||||
result = md5(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 字节数组转十六进制字符串
|
||||
*
|
||||
* @param bytes 字节数组
|
||||
* @return 十六进制字符串
|
||||
*/
|
||||
private static String bytesToHex(byte[] bytes) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (byte b : bytes) {
|
||||
result.append(String.format("%02x", b));
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件的MD5值
|
||||
*
|
||||
* @param bytes 文件字节数组
|
||||
* @return MD5值
|
||||
*/
|
||||
public static String getFileMd5(byte[] bytes) {
|
||||
if (bytes == null || bytes.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance(MD5_ALGORITHM);
|
||||
byte[] digest = md.digest(bytes);
|
||||
return bytesToHex(digest);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException("MD5算法不可用", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package com.admin.common.utils;
|
||||
|
||||
|
||||
import com.admin.entity.Node;
|
||||
import com.admin.service.NodeService;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.web.socket.CloseStatus;
|
||||
import org.springframework.web.socket.TextMessage;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
import org.springframework.web.socket.handler.TextWebSocketHandler;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
|
||||
@Slf4j
|
||||
public class WebSocketServer extends TextWebSocketHandler {
|
||||
|
||||
@Resource
|
||||
NodeService nodeService;
|
||||
|
||||
// 存储所有活跃的 WebSocket 连接
|
||||
private static final CopyOnWriteArraySet<WebSocketSession> activeSessions = new CopyOnWriteArraySet<>();
|
||||
|
||||
// 为每个session提供锁对象,防止并发发送消息
|
||||
private static final ConcurrentHashMap<String, Object> sessionLocks = new ConcurrentHashMap<>();
|
||||
|
||||
//接受客户端消息
|
||||
@Override
|
||||
public void handleTextMessage(WebSocketSession session, TextMessage message) {
|
||||
try {
|
||||
if (StringUtils.isNoneBlank(message.getPayload())) {
|
||||
//log.info("收到消息: {}", message.getPayload());
|
||||
|
||||
String id = session.getAttributes().get("id").toString();
|
||||
String type = session.getAttributes().get("type").toString();
|
||||
|
||||
// 先发送确认消息
|
||||
sendToUser(session, "ok");
|
||||
|
||||
// 如果是节点类型,转发消息给其他会话
|
||||
if (Objects.equals(type, "1")) {
|
||||
JSONObject jsonObject = new JSONObject();
|
||||
jsonObject.put("id", id);
|
||||
jsonObject.put("type", "info");
|
||||
jsonObject.put("data", message.getPayload());
|
||||
String broadcastMessage = jsonObject.toJSONString();
|
||||
|
||||
// 异步处理广播消息,避免阻塞当前线程
|
||||
for (WebSocketSession targetSession : activeSessions) {
|
||||
if (targetSession != null && targetSession.isOpen() && !targetSession.equals(session)) {
|
||||
sendToUser(targetSession, broadcastMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("处理WebSocket消息时发生异常: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// 建立连接
|
||||
@Override
|
||||
public void afterConnectionEstablished(WebSocketSession session) {
|
||||
try {
|
||||
String id = session.getAttributes().get("id").toString();
|
||||
String type = session.getAttributes().get("type").toString();
|
||||
if (!Objects.equals(type, "1")) {
|
||||
activeSessions.add(session);
|
||||
}else {
|
||||
Node byId = nodeService.getById(id);
|
||||
if (byId != null) {
|
||||
byId.setStatus(1);
|
||||
nodeService.updateById(byId);
|
||||
JSONObject res = new JSONObject();
|
||||
res.put("id", id);
|
||||
res.put("type", "status");
|
||||
res.put("data", 1);
|
||||
broadcastMessage(res.toJSONString());
|
||||
}
|
||||
}
|
||||
log.info("WebSocket 连接建立成功 - id: {}, type: {}, 当前连接数: {}", id, type, activeSessions.size());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("建立连接时发生异常: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// 连接关闭后
|
||||
@Override
|
||||
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
|
||||
try {
|
||||
String id = session.getAttributes().get("id").toString();
|
||||
String type = session.getAttributes().get("type").toString();
|
||||
String sessionId = session.getId();
|
||||
|
||||
if (!Objects.equals(type, "1")) {
|
||||
activeSessions.remove(session);
|
||||
}else {
|
||||
Node byId = nodeService.getById(id);
|
||||
if (byId != null) {
|
||||
byId.setStatus(0);
|
||||
nodeService.updateById(byId);
|
||||
JSONObject res = new JSONObject();
|
||||
res.put("id", id);
|
||||
res.put("type", "status");
|
||||
res.put("data", 0);
|
||||
broadcastMessage(res.toJSONString());
|
||||
}
|
||||
}
|
||||
|
||||
// 清理session锁对象
|
||||
sessionLocks.remove(sessionId);
|
||||
|
||||
log.info("WebSocket 连接关闭 - id: {}, sessionId: {}, 关闭状态: {}, 当前连接数: {}",
|
||||
id, sessionId, status, activeSessions.size());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("关闭连接时发生异常: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// 点对点发送消息
|
||||
@SneakyThrows
|
||||
public static void sendToUser(WebSocketSession socketSession, String message) {
|
||||
if (socketSession != null && socketSession.isOpen()) {
|
||||
String sessionId = socketSession.getId();
|
||||
Object lock = sessionLocks.computeIfAbsent(sessionId, k -> new Object());
|
||||
|
||||
synchronized (lock) {
|
||||
try {
|
||||
if (socketSession.isOpen()) {
|
||||
socketSession.sendMessage(new TextMessage(message));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("发送WebSocket消息失败 [sessionId={}]: {}", sessionId, e.getMessage());
|
||||
activeSessions.remove(socketSession);
|
||||
sessionLocks.remove(sessionId);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
activeSessions.remove(socketSession);
|
||||
if (socketSession != null) {
|
||||
sessionLocks.remove(socketSession.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 广播消息
|
||||
public static void broadcastMessage(String message) {
|
||||
for (WebSocketSession session : activeSessions) {
|
||||
sendToUser(session, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user