refactor: 统一实体/限流AOP化/精简日志/文件归位
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
package com.youlai.boot.common.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 接口限流
|
||||
* <p>标注在 Controller 方法上,基于 Redis 计数窗口实现</p>
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface RateLimit {
|
||||
|
||||
int limit() default 0;
|
||||
|
||||
int window() default 0;
|
||||
|
||||
TimeUnit timeUnit() default TimeUnit.SECONDS;
|
||||
|
||||
String prefix() default "rate_limit:";
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
package com.youlai.boot.common.aspect;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.useragent.UserAgent;
|
||||
import cn.hutool.http.useragent.UserAgentUtil;
|
||||
import com.youlai.boot.common.annotation.Log;
|
||||
import com.youlai.boot.common.enums.ActionTypeEnum;
|
||||
import com.youlai.boot.common.enums.LogModuleEnum;
|
||||
import com.youlai.boot.common.util.IPUtils;
|
||||
import com.youlai.boot.framework.security.util.SecurityUtils;
|
||||
import com.youlai.boot.system.model.entity.SysLog;
|
||||
import com.youlai.boot.system.service.LogService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Pointcut;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 日志切面
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2.10.0
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class LogAspect {
|
||||
|
||||
private final LogService logService;
|
||||
|
||||
/**
|
||||
* 日志注解切点
|
||||
*/
|
||||
@Pointcut("@annotation(logAnnotation)")
|
||||
public void logPointCut(Log logAnnotation) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 环绕通知:记录操作日志
|
||||
*/
|
||||
@Around(value = "logPointCut(logAnnotation)", argNames = "pjp,logAnnotation")
|
||||
public Object around(ProceedingJoinPoint pjp, Log logAnnotation) throws Throwable {
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
// 在方法执行前获取用户信息,避免 logout 等操作清除 SecurityContext 后无法获取
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
String username = SecurityUtils.getUsername();
|
||||
Object result = null;
|
||||
Exception exception = null;
|
||||
|
||||
try {
|
||||
result = pjp.proceed();
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
exception = e;
|
||||
throw e;
|
||||
} finally {
|
||||
long executionTime = System.currentTimeMillis() - startTime;
|
||||
// fallback:登录等场景在 proceed() 前未认证,需在 proceed() 后获取
|
||||
if (userId == null) {
|
||||
userId = SecurityUtils.getUserId();
|
||||
username = SecurityUtils.getUsername();
|
||||
}
|
||||
try {
|
||||
saveLogAsync(logAnnotation, executionTime, exception, userId, username);
|
||||
} catch (Exception ex) {
|
||||
log.error("保存操作日志失败", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步保存日志
|
||||
*/
|
||||
@Async
|
||||
public void saveLogAsync(Log logAnnotation, long executionTime, Exception exception, Long userId, String username) {
|
||||
try {
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
HttpServletRequest request = attributes.getRequest();
|
||||
|
||||
// 解析 User-Agent
|
||||
String userAgentStr = request.getHeader("User-Agent");
|
||||
UserAgent userAgent = UserAgentUtil.parse(userAgentStr);
|
||||
|
||||
// 解析 IP 地区
|
||||
String ip = IPUtils.getIpAddr(request);
|
||||
String region = IPUtils.getRegion(ip);
|
||||
String province = null;
|
||||
String city = null;
|
||||
if (StrUtil.isNotBlank(region)) {
|
||||
String[] parts = region.split("\\|");
|
||||
if (parts.length >= 3) {
|
||||
province = StrUtil.blankToDefault(parts[2], null);
|
||||
city = StrUtil.blankToDefault(parts[3], null);
|
||||
}
|
||||
}
|
||||
|
||||
// 构建日志实体
|
||||
LogModuleEnum module = logAnnotation.module();
|
||||
ActionTypeEnum actionType = logAnnotation.value();
|
||||
String title = StrUtil.blankToDefault(logAnnotation.title(),
|
||||
module.getLabel() + "-" + actionType.getLabel());
|
||||
String content = logAnnotation.content();
|
||||
|
||||
SysLog logEntity = new SysLog();
|
||||
logEntity.setModule(module);
|
||||
logEntity.setActionType(actionType);
|
||||
logEntity.setTitle(title);
|
||||
logEntity.setContent(content);
|
||||
logEntity.setOperatorId(userId);
|
||||
logEntity.setOperatorName(username);
|
||||
logEntity.setRequestUri(request.getRequestURI());
|
||||
logEntity.setRequestMethod(request.getMethod());
|
||||
logEntity.setIp(ip);
|
||||
logEntity.setProvince(province);
|
||||
logEntity.setCity(city);
|
||||
logEntity.setDevice(userAgent.getOs().getName());
|
||||
logEntity.setOs(userAgent.getOs().getName());
|
||||
logEntity.setBrowser(userAgent.getBrowser().getName());
|
||||
logEntity.setStatus(exception == null ? 1 : 0);
|
||||
logEntity.setErrorMsg(exception != null ? exception.getMessage() : null);
|
||||
logEntity.setExecutionTime((int) executionTime);
|
||||
logEntity.setCreateTime(LocalDateTime.now());
|
||||
|
||||
logService.save(logEntity);
|
||||
} catch (Exception e) {
|
||||
log.error("保存操作日志异常: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ public interface RedisConstants {
|
||||
* 限流相关键
|
||||
*/
|
||||
interface RateLimiter {
|
||||
String IP = "rate_limiter:ip:{}"; // IP限流(示例:rate_limiter:ip:192.168.1.1)
|
||||
String API = "{}rate_limit:{}:{}"; // 接口限流(示例:login:rate_limit:token:/api/v1/auth/login)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -23,10 +23,4 @@ public interface SystemConstants {
|
||||
*/
|
||||
String ROOT_ROLE_CODE = "ROOT";
|
||||
|
||||
|
||||
/**
|
||||
* 系统配置 IP的QPS限流的KEY
|
||||
*/
|
||||
String SYSTEM_CONFIG_IP_QPS_LIMIT_KEY = "IP_QPS_THRESHOLD_LIMIT";
|
||||
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
package com.youlai.boot.common.enums;
|
||||
|
||||
/**
|
||||
* EasyCaptcha 验证码类型枚举
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2.5.1
|
||||
*/
|
||||
public enum CaptchaTypeEnum {
|
||||
|
||||
/**
|
||||
* 圆圈干扰验证码
|
||||
*/
|
||||
CIRCLE,
|
||||
/**
|
||||
* GIF验证码
|
||||
*/
|
||||
GIF,
|
||||
/**
|
||||
* 干扰线验证码
|
||||
*/
|
||||
LINE,
|
||||
/**
|
||||
* 扭曲干扰验证码
|
||||
*/
|
||||
SHEAR
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
package com.youlai.boot.common.result;
|
||||
|
||||
import cn.hutool.extra.servlet.JakartaServletUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* 响应写入器
|
||||
* <p>
|
||||
* 用于在过滤器、Security处理器等无法使用 @RestControllerAdvice 的场景中统一写入HTTP响应。
|
||||
* 支持写入成功响应和错误响应。
|
||||
* 此类为工具类,所有方法均为静态方法,禁止实例化。
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
public final class ResponseWriter {
|
||||
|
||||
/**
|
||||
* 私有构造函数,防止实例化
|
||||
*/
|
||||
private ResponseWriter() {
|
||||
throw new UnsupportedOperationException("工具类不允许实例化");
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入成功响应
|
||||
*
|
||||
* @param response HttpServletResponse
|
||||
* @param data 响应数据(可选)
|
||||
*/
|
||||
public static void writeSuccess(HttpServletResponse response, Object data) {
|
||||
writeResult(response, Result.success(data), HttpStatus.OK.value());
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入成功响应(无数据)
|
||||
*
|
||||
* @param response HttpServletResponse
|
||||
*/
|
||||
public static void writeSuccess(HttpServletResponse response) {
|
||||
writeSuccess(response, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入错误响应
|
||||
*
|
||||
* @param response HttpServletResponse
|
||||
* @param resultCode 响应结果码
|
||||
*/
|
||||
public static void writeError(HttpServletResponse response, ResultCode resultCode) {
|
||||
writeError(response, resultCode, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入错误响应(带自定义消息)
|
||||
*
|
||||
* @param response HttpServletResponse
|
||||
* @param resultCode 响应结果码
|
||||
* @param message 自定义消息(可选,为 null 时使用 resultCode 的默认消息)
|
||||
*/
|
||||
public static void writeError(HttpServletResponse response, ResultCode resultCode, String message) {
|
||||
Result<?> result = message == null
|
||||
? Result.failed(resultCode)
|
||||
: Result.failed(resultCode, message);
|
||||
|
||||
int httpStatus = mapHttpStatus(resultCode);
|
||||
writeResult(response, result, httpStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入响应结果(通用方法)
|
||||
*
|
||||
* @param response HttpServletResponse
|
||||
* @param result 响应结果对象
|
||||
* @param httpStatus HTTP状态码
|
||||
*/
|
||||
private static void writeResult(HttpServletResponse response, Result<?> result, int httpStatus) {
|
||||
try {
|
||||
// 设置HTTP状态码
|
||||
response.setStatus(httpStatus);
|
||||
|
||||
// 设置响应编码和内容类型
|
||||
response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
|
||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||
|
||||
// 写入响应
|
||||
JakartaServletUtil.write(response,
|
||||
JSONUtil.toJsonStr(result),
|
||||
MediaType.APPLICATION_JSON_VALUE
|
||||
);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("写入响应时发生未知异常: httpStatus={}, result={}", httpStatus, result, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据业务结果码映射HTTP状态码
|
||||
* 401: 未认证(token无效/过期)
|
||||
* 403: 权限不足
|
||||
* 400: 其他业务错误
|
||||
*
|
||||
* @param resultCode 业务结果码
|
||||
* @return HTTP状态码
|
||||
*/
|
||||
private static int mapHttpStatus(ResultCode resultCode) {
|
||||
return switch (resultCode) {
|
||||
case ACCESS_UNAUTHORIZED,
|
||||
ACCESS_TOKEN_INVALID,
|
||||
REFRESH_TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value();
|
||||
case ACCESS_PERMISSION_EXCEPTION -> HttpStatus.FORBIDDEN.value();
|
||||
default -> HttpStatus.BAD_REQUEST.value();
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user