diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2781344c..8f42b851 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
# Changelog
+## [0.2] - 2025-08-12
+- 提升多账号情况下资产采集的时效性
+- 回流采集异常日志、支持手动云账号触发采集任务
+- 前端交互和展示优化,提升使用体验
+- 其他bug修复
+
## [0.1] - 2025-05-01
- 项目初始化,基础功能上线,奠定平台架构基础。
- 云账号管理:支持云账号的创建与查询,便于多云环境统一纳管,提升账号安全性与可追溯性。
diff --git a/CHANGELOG_EN.md b/CHANGELOG_EN.md
index 24f26a57..71681531 100644
--- a/CHANGELOG_EN.md
+++ b/CHANGELOG_EN.md
@@ -1,5 +1,11 @@
# Changelog
+## [0.2] - 2025-08-12
+- Improve the timeliness of asset collection in the case of multiple accounts
+- Collection of abnormal logs and manual cloud account-triggered collection tasks
+- Optimized frontend interaction and display for better user experience
+- Other bug fixes
+
## [0.1] - 2025-05-01
- Project initialization, foundational features launched, establishing the platform architecture.
- Cloud Account Management: Supports creation and query of cloud accounts, enabling unified management in multi-cloud environments, enhancing account security and traceability.
diff --git a/app/api/pom.xml b/app/api/pom.xml
index bda9f727..dfe925b2 100644
--- a/app/api/pom.xml
+++ b/app/api/pom.xml
@@ -5,7 +5,7 @@
cloudrec
com.alipay
- 0.2.0-SNAPSHOT
+ 0.2.1-SNAPSHOT
../../pom.xml
api
@@ -18,7 +18,7 @@
com.alipay
application
- 0.2.0-SNAPSHOT
+ 0.2.1-SNAPSHOT
@@ -27,4 +27,24 @@
test
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.0.0-M7
+
+
+ true
+
+ **/*Test.java
+ **/*Tests.java
+
+ false
+
+
+
+
diff --git a/app/api/src/main/java/com/alipay/api/collector/CollectorApi.java b/app/api/src/main/java/com/alipay/api/collector/CollectorApi.java
index 0a13ebf2..43034f82 100644
--- a/app/api/src/main/java/com/alipay/api/collector/CollectorApi.java
+++ b/app/api/src/main/java/com/alipay/api/collector/CollectorApi.java
@@ -19,10 +19,7 @@
import com.alipay.application.service.collector.AgentService;
import com.alipay.application.service.collector.domain.TaskResp;
import com.alipay.application.service.resource.SaveResourceService;
-import com.alipay.application.share.request.collector.AcceptSupportResourceTypeRequest;
-import com.alipay.application.share.request.collector.LogRequest;
-import com.alipay.application.share.request.collector.QueryCloudAccountRequest;
-import com.alipay.application.share.request.collector.RunningFinishSignalRequest;
+import com.alipay.application.share.request.collector.*;
import com.alipay.application.share.request.resource.DataPushRequest;
import com.alipay.application.share.vo.ApiResponse;
import com.alipay.application.share.vo.collector.AgentCloudAccountVO;
@@ -73,13 +70,25 @@ public ApiResponse acceptResourceData(@Validated @RequestBody DataPushRe
}
@PostMapping("/acceptRunningFinishSignal")
- public void acceptRunningFinishSignal(@Validated @RequestBody RunningFinishSignalRequest req,
- BindingResult err) {
+ public ApiResponse acceptRunningFinishSignal(@Validated @RequestBody RunningFinishSignalRequest req,
+ BindingResult err) {
if (err.hasErrors()) {
throw new IllegalArgumentException(err.toString());
}
agentService.runningFinishSignal(req.getCloudAccountId(), req.getTaskId());
+ return ApiResponse.SUCCESS;
+ }
+
+ @PostMapping("/acceptRunningStartSignal")
+ public ApiResponse acceptRunningStartSignal(HttpServletRequest request, @Validated @RequestBody RunningStartSignalRequest req,
+ BindingResult err) {
+ if (err.hasErrors()) {
+ throw new IllegalArgumentException(err.toString());
+ }
+
+ agentService.runningStartSignal(request.getHeader(PERSISTENT_TOKEN), req.getCloudAccountId(), req.getCollectRecordInfo());
+ return ApiResponse.SUCCESS;
}
@@ -94,7 +103,7 @@ public void acceptRunningFinishSignal(@Validated @RequestBody RunningFinishSigna
public ApiResponse> listCloudAccount(HttpServletRequest request,
@RequestBody QueryCloudAccountRequest req) {
return agentService.queryCloudAccountList(request.getHeader(PERSISTENT_TOKEN),
- req.getRegistryValue(), req.getPlatform(), req.getSites(), req.getTaskIds());
+ req.getRegistryValue(), req.getPlatform(), req.getSites(), req.getTaskIds(), req.getFreeCloudAccountCount());
}
/**
diff --git a/app/api/src/main/java/com/alipay/api/config/filter/CachedBodyFilter.java b/app/api/src/main/java/com/alipay/api/config/filter/CachedBodyFilter.java
new file mode 100644
index 00000000..f9f09e71
--- /dev/null
+++ b/app/api/src/main/java/com/alipay/api/config/filter/CachedBodyFilter.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.alipay.api.config.filter;
+
+import com.alipay.application.service.system.utils.CachedBodyHttpServletRequest;
+import jakarta.servlet.*;
+import jakarta.servlet.http.HttpServletRequest;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+
+/**
+ * Filter to cache request body for multiple reads
+ * This filter wraps POST requests with JSON content type to allow
+ * multiple components to read the request body without conflicts
+ */
+@Component
+@Order(1) // Execute early in the filter chain
+public class CachedBodyFilter implements Filter {
+
+ private static final Logger logger = LoggerFactory.getLogger(CachedBodyFilter.class);
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+ throws IOException, ServletException {
+
+ if (request instanceof HttpServletRequest httpRequest) {
+ // Only wrap POST requests with JSON content type
+ if ("POST".equalsIgnoreCase(httpRequest.getMethod())) {
+ String contentType = httpRequest.getContentType();
+ if (contentType != null && contentType.toLowerCase().contains("application/json")) {
+ try {
+ // Wrap the request to cache the body
+ CachedBodyHttpServletRequest cachedRequest = new CachedBodyHttpServletRequest(httpRequest);
+ logger.debug("Wrapped POST request with JSON content type for caching");
+ chain.doFilter(cachedRequest, response);
+ return;
+ } catch (Exception e) {
+ logger.warn("Failed to wrap request for body caching: {}", e.getMessage());
+ // Fall through to process the original request
+ }
+ }
+ }
+ }
+
+ // For non-POST requests or requests without JSON content type, proceed normally
+ chain.doFilter(request, response);
+ }
+
+ @Override
+ public void init(FilterConfig filterConfig) {
+ logger.info("CachedBodyFilter initialized");
+ }
+
+ @Override
+ public void destroy() {
+ logger.info("CachedBodyFilter destroyed");
+ }
+}
\ No newline at end of file
diff --git a/app/api/src/main/java/com/alipay/api/config/filter/OpenApiInterceptor.java b/app/api/src/main/java/com/alipay/api/config/filter/OpenApiInterceptor.java
new file mode 100644
index 00000000..4efdea41
--- /dev/null
+++ b/app/api/src/main/java/com/alipay/api/config/filter/OpenApiInterceptor.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.alipay.api.config.filter;
+
+import com.alipay.api.config.filter.annotation.aop.OpenApi;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.jetbrains.annotations.NotNull;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpMethod;
+import org.springframework.stereotype.Component;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.AsyncHandlerInterceptor;
+
+/**
+ * 拦截器,用于检测方法上是否有@OpenApi注解,如果有则放行
+ */
+@Component
+public class OpenApiInterceptor implements AsyncHandlerInterceptor {
+
+ private static final Logger logger = LoggerFactory.getLogger(OpenApiInterceptor.class);
+
+ /**
+ * 用于标记请求是否为OpenApi请求的属性名
+ */
+ public static final String OPEN_API_REQUEST_ATTRIBUTE = "OPEN_API_REQUEST";
+
+ @Override
+ public boolean preHandle(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Object handler) {
+ // 对于OPTIONS请求直接放行
+ if (HttpMethod.OPTIONS.toString().equals(request.getMethod())) {
+ return true;
+ }
+
+ // 检查处理器是否为HandlerMethod类型
+ if (handler instanceof HandlerMethod handlerMethod) {
+ // 检查方法上是否有@OpenApi注解
+ if (handlerMethod.hasMethodAnnotation(OpenApi.class)) {
+ logger.debug("Detected @OpenApi annotation on method: {}", handlerMethod.getMethod().getName());
+ // 在请求属性中标记这是一个OpenApi请求
+ request.setAttribute(OPEN_API_REQUEST_ATTRIBUTE, Boolean.TRUE);
+ // 有@OpenApi注解的方法直接放行,认证由OpenApiAspect处理
+ return true;
+ }
+ }
+
+ // 没有@OpenApi注解的方法,交给下一个拦截器处理
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/app/api/src/main/java/com/alipay/api/config/filter/PermissionInterceptor.java b/app/api/src/main/java/com/alipay/api/config/filter/PermissionInterceptor.java
index c71dea43..f2517290 100644
--- a/app/api/src/main/java/com/alipay/api/config/filter/PermissionInterceptor.java
+++ b/app/api/src/main/java/com/alipay/api/config/filter/PermissionInterceptor.java
@@ -39,15 +39,26 @@
@Component
public class PermissionInterceptor implements AsyncHandlerInterceptor {
+ private static final Logger logger = LoggerFactory.getLogger(PermissionInterceptor.class);
+
@Resource
private UserMapper userMapper;
@Override
public boolean preHandle(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Object handler) {
+ // 对于OPTIONS请求直接放行
if (HttpMethod.OPTIONS.toString().equals(request.getMethod())) {
return true;
}
+ // 检查请求是否已被标记为OpenApi请求
+ Boolean isOpenApiRequest = (Boolean) request.getAttribute(OpenApiInterceptor.OPEN_API_REQUEST_ATTRIBUTE);
+ if (Boolean.TRUE.equals(isOpenApiRequest)) {
+ logger.info("Skipping permission check for OpenApi request");
+ return true;
+ }
+
+ // 非OpenApi请求,执行正常的权限验证
String token = request.getHeader("token");
if (StringUtils.isBlank(token) || "null".equals(token)) {
throw new UserNoLoginException("Login failed");
diff --git a/app/api/src/main/java/com/alipay/api/config/filter/WebMvcConfig.java b/app/api/src/main/java/com/alipay/api/config/filter/WebMvcConfig.java
index 88bce670..e347fc38 100644
--- a/app/api/src/main/java/com/alipay/api/config/filter/WebMvcConfig.java
+++ b/app/api/src/main/java/com/alipay/api/config/filter/WebMvcConfig.java
@@ -47,17 +47,25 @@ public class WebMvcConfig implements WebMvcConfigurer {
private LocaleChangeInterceptor localeChangeInterceptor;
@Resource
private LangContextInterceptor langContextInterceptor;
+ @Resource
+ private OpenApiInterceptor openApiInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
+ // 添加国际化拦截器
registry.addInterceptor(localeChangeInterceptor);
+ // 添加语言上下文拦截器
registry.addInterceptor(langContextInterceptor);
+ // 添加OpenApi拦截器,用于检测@OpenApi注解
+ registry.addInterceptor(openApiInterceptor).addPathPatterns("/api/**");
+ // 添加权限拦截器,但OpenApi拦截器已经处理过的请求将被放行
registry.addInterceptor(permissionInterceptor).addPathPatterns("/api/**")
.excludePathPatterns("/api/agent/**")
.excludePathPatterns("/api/user/queryUserInfo")
.excludePathPatterns("/api/user/login")
- .excludePathPatterns("/api/open/v1/**")
- .excludePathPatterns("/api/cloudAccount/acceptCloudAccount");
+ .excludePathPatterns("/api/user/register")
+ .excludePathPatterns("/api/tenant/checkInviteCode")
+ .excludePathPatterns("/api/open/v1/**");
}
@Override
diff --git a/app/api/src/main/java/com/alipay/api/config/filter/annotation/aop/OpenApiAspect.java b/app/api/src/main/java/com/alipay/api/config/filter/annotation/aop/OpenApiAspect.java
index bd2ab268..8841302e 100644
--- a/app/api/src/main/java/com/alipay/api/config/filter/annotation/aop/OpenApiAspect.java
+++ b/app/api/src/main/java/com/alipay/api/config/filter/annotation/aop/OpenApiAspect.java
@@ -16,16 +16,25 @@
*/
package com.alipay.api.config.filter.annotation.aop;
+import com.alipay.application.service.system.domain.User;
+import com.alipay.application.service.system.domain.repo.UserRepository;
import com.alipay.application.service.system.utils.DigestSignUtils;
import com.alipay.application.share.vo.ApiResponse;
import com.alipay.common.exception.OpenAipNoAuthException;
+import com.alipay.dao.context.UserInfoContext;
+import com.alipay.dao.context.UserInfoDTO;
+import com.alipay.dao.mapper.OpenApiAuthMapper;
+import com.alipay.dao.po.OpenApiAuthPO;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
@Aspect
@@ -34,31 +43,43 @@ public class OpenApiAspect {
@Resource
private DigestSignUtils digestSignUtils;
+
+ @Resource
+ private OpenApiAuthMapper openApiAuthMapper;
+
+ @Resource
+ private UserRepository userRepository;
@Pointcut("@annotation(com.alipay.api.config.filter.annotation.aop.OpenApi)")
- public void annotatedMethod() {
+ public void openApiPointcut() {
}
- @Before("@annotation(com.alipay.api.config.filter.annotation.aop.OpenApi)")
+ @Before("openApiPointcut()")
public void processRequest(JoinPoint joinPoint) {
- HttpServletRequest request = getRequestFromArgs(joinPoint.getArgs());
+ HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
ApiResponse response = digestSignUtils.isAuth(request);
if (response.getCode() != ApiResponse.SUCCESS_CODE) {
throw new OpenAipNoAuthException(response.getMsg());
}
-
-
- }
-
- private HttpServletRequest getRequestFromArgs(Object[] args) {
- for (Object arg : args) {
- if (arg instanceof HttpServletRequest) {
- return (HttpServletRequest) arg;
+
+ String accessKey = request.getHeader(DigestSignUtils.ACCESS_KEY_NAME);
+ OpenApiAuthPO openApiAuthPO = openApiAuthMapper.findByAccessKey(accessKey);
+ if (openApiAuthPO != null) {
+ String userId = openApiAuthPO.getUserId();
+ User user = userRepository.find(userId);
+ if (user != null) {
+ UserInfoDTO userInfoDTO = new UserInfoDTO();
+ userInfoDTO.setUid(user.getId());
+ userInfoDTO.setUserId(user.getUserId());
+ userInfoDTO.setUsername(user.getUsername());
+ userInfoDTO.setTenantId(user.getTenantId());
+ UserInfoContext.setCurrentUser(userInfoDTO);
}
}
- return null;
}
-
-
-
+
+ @After("openApiPointcut()")
+ public void clearUserInfo() {
+ UserInfoContext.clear();
+ }
}
diff --git a/app/api/src/main/java/com/alipay/api/config/filter/annotation/aop/RateLimit.java b/app/api/src/main/java/com/alipay/api/config/filter/annotation/aop/RateLimit.java
new file mode 100644
index 00000000..4e0bccd5
--- /dev/null
+++ b/app/api/src/main/java/com/alipay/api/config/filter/annotation/aop/RateLimit.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.alipay.api.config.filter.annotation.aop;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Rate limiting annotation for API endpoints
+ * Uses sliding window algorithm to control request frequency
+ *
+ * @author jietian
+ * @version 1.0
+ * @since 2024
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface RateLimit {
+
+ /**
+ * Maximum number of requests allowed per time window
+ * Default: 10 requests
+ */
+ int maxRequests() default 10;
+
+ /**
+ * Time window in seconds
+ * Default: 60 seconds (1 minute)
+ */
+ int timeWindowSeconds() default 60;
+
+ /**
+ * Rate limiting key strategy
+ * IP: limit by client IP address
+ * USER: limit by authenticated user ID
+ * GLOBAL: global rate limiting for all requests
+ * Default: IP
+ */
+ KeyStrategy keyStrategy() default KeyStrategy.IP;
+
+ /**
+ * Custom error message when rate limit is exceeded
+ * Default: "Too many requests, please try again later"
+ */
+ String message() default "Too many requests, please try again later";
+
+ /**
+ * Key strategy enumeration
+ */
+ enum KeyStrategy {
+ IP, // Rate limit by IP address
+ USER, // Rate limit by user ID
+ GLOBAL // Global rate limit
+ }
+}
\ No newline at end of file
diff --git a/app/api/src/main/java/com/alipay/api/config/filter/annotation/aop/RateLimitAspect.java b/app/api/src/main/java/com/alipay/api/config/filter/annotation/aop/RateLimitAspect.java
new file mode 100644
index 00000000..31c3e6ed
--- /dev/null
+++ b/app/api/src/main/java/com/alipay/api/config/filter/annotation/aop/RateLimitAspect.java
@@ -0,0 +1,184 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.alipay.api.config.filter.annotation.aop;
+
+import com.alipay.api.config.filter.service.RateLimitService;
+import com.alipay.application.share.vo.ApiResponse;
+import com.alipay.dao.context.UserInfoContext;
+import com.alipay.dao.context.UserInfoDTO;
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+/**
+ * Rate limiting aspect for intercepting methods annotated with @RateLimit
+ * Implements rate limiting logic using sliding window algorithm
+ *
+ * @author jietian
+ * @version 1.0
+ * @since 2024
+ */
+@Aspect
+@Component
+@Slf4j
+public class RateLimitAspect {
+
+ @Resource
+ private RateLimitService rateLimitService;
+
+ /**
+ * Around advice for rate limiting
+ * Intercepts method calls annotated with @RateLimit and applies rate limiting logic
+ *
+ * @param joinPoint the join point representing the intercepted method
+ * @param rateLimit the rate limit annotation
+ * @return the result of the method execution or rate limit error response
+ * @throws Throwable if method execution fails
+ */
+ @Around("@annotation(rateLimit)")
+ public Object rateLimit(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
+ try {
+ // Generate rate limiting key based on strategy
+ String rateLimitKey = generateRateLimitKey(rateLimit.keyStrategy(), joinPoint);
+
+ // Check if request is allowed
+ boolean allowed = rateLimitService.isAllowed(
+ rateLimitKey,
+ rateLimit.maxRequests(),
+ rateLimit.timeWindowSeconds()
+ );
+
+ if (!allowed) {
+ log.warn("Rate limit exceeded for key: {}, method: {}.{}",
+ rateLimitKey,
+ joinPoint.getTarget().getClass().getSimpleName(),
+ joinPoint.getSignature().getName());
+
+ // Return rate limit exceeded response
+ return createRateLimitResponse(rateLimit.message());
+ }
+
+ // Proceed with method execution
+ return joinPoint.proceed();
+
+ } catch (Exception e) {
+ log.error("Error in rate limiting aspect for method: {}.{}",
+ joinPoint.getTarget().getClass().getSimpleName(),
+ joinPoint.getSignature().getName(), e);
+
+ // In case of error, allow the request to proceed
+ return joinPoint.proceed();
+ }
+ }
+
+ /**
+ * Generate rate limiting key based on the specified strategy
+ *
+ * @param strategy the key generation strategy
+ * @param joinPoint the join point for method context
+ * @return the generated rate limiting key
+ */
+ private String generateRateLimitKey(RateLimit.KeyStrategy strategy, ProceedingJoinPoint joinPoint) {
+ String methodName = joinPoint.getTarget().getClass().getSimpleName() + "." + joinPoint.getSignature().getName();
+
+ switch (strategy) {
+ case IP:
+ return "rate_limit:ip:" + getClientIpAddress() + ":" + methodName;
+ case USER:
+ return "rate_limit:user:" + getCurrentUserId() + ":" + methodName;
+ case GLOBAL:
+ return "rate_limit:global:" + methodName;
+ default:
+ return "rate_limit:ip:" + getClientIpAddress() + ":" + methodName;
+ }
+ }
+
+ /**
+ * Get client IP address from HTTP request
+ *
+ * @return client IP address
+ */
+ private String getClientIpAddress() {
+ try {
+ ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
+ HttpServletRequest request = attributes.getRequest();
+
+ // Check for IP address in various headers (for proxy scenarios)
+ String ipAddress = request.getHeader("X-Forwarded-For");
+ if (StringUtils.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) {
+ ipAddress = request.getHeader("Proxy-Client-IP");
+ }
+ if (StringUtils.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) {
+ ipAddress = request.getHeader("WL-Proxy-Client-IP");
+ }
+ if (StringUtils.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) {
+ ipAddress = request.getHeader("HTTP_CLIENT_IP");
+ }
+ if (StringUtils.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) {
+ ipAddress = request.getHeader("HTTP_X_FORWARDED_FOR");
+ }
+ if (StringUtils.isBlank(ipAddress) || "unknown".equalsIgnoreCase(ipAddress)) {
+ ipAddress = request.getRemoteAddr();
+ }
+
+ // Handle multiple IPs in X-Forwarded-For header
+ if (StringUtils.isNotBlank(ipAddress) && ipAddress.contains(",")) {
+ ipAddress = ipAddress.split(",")[0].trim();
+ }
+
+ return StringUtils.isNotBlank(ipAddress) ? ipAddress : "unknown";
+ } catch (Exception e) {
+ log.warn("Failed to get client IP address", e);
+ return "unknown";
+ }
+ }
+
+ /**
+ * Get current authenticated user ID
+ *
+ * @return user ID or "anonymous" if not authenticated
+ */
+ private String getCurrentUserId() {
+ try {
+ UserInfoDTO userInfo = UserInfoContext.getCurrentUser();
+ if (userInfo != null && StringUtils.isNotBlank(userInfo.getUserId())) {
+ return userInfo.getUserId();
+ }
+ } catch (Exception e) {
+ log.debug("Failed to get current user ID", e);
+ }
+ return "anonymous";
+ }
+
+ /**
+ * Create rate limit exceeded response
+ *
+ * @param message custom error message
+ * @return API response indicating rate limit exceeded
+ */
+ private ApiResponse