This commit is contained in:
qaq
2025-06-17 12:17:33 +08:00
parent 4a6b2c8e0f
commit 9fa968d07c
648 changed files with 64968 additions and 1621 deletions
@@ -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;
}
@@ -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 {
}
}
@@ -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);
}
}
}