Commit f6a66148 authored by lixuan's avatar lixuan

feat: 需求

parent 6147cdb2
Pipeline #147205 failed with stages
in 0 seconds
...@@ -14,6 +14,7 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; ...@@ -14,6 +14,7 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import com.ruoyi.common.config.RuoYiConfig; import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.constant.Constants; import com.ruoyi.common.constant.Constants;
import com.ruoyi.framework.interceptor.RepeatSubmitInterceptor; import com.ruoyi.framework.interceptor.RepeatSubmitInterceptor;
import com.ruoyi.framework.interceptor.impl.ApiCallStatsInterceptor;
/** /**
* 通用配置 * 通用配置
...@@ -26,6 +27,9 @@ public class ResourcesConfig implements WebMvcConfigurer ...@@ -26,6 +27,9 @@ public class ResourcesConfig implements WebMvcConfigurer
@Autowired @Autowired
private RepeatSubmitInterceptor repeatSubmitInterceptor; private RepeatSubmitInterceptor repeatSubmitInterceptor;
@Autowired
private ApiCallStatsInterceptor apiCallStatsInterceptor;
@Override @Override
public void addResourceHandlers(ResourceHandlerRegistry registry) public void addResourceHandlers(ResourceHandlerRegistry registry)
{ {
...@@ -46,6 +50,18 @@ public class ResourcesConfig implements WebMvcConfigurer ...@@ -46,6 +50,18 @@ public class ResourcesConfig implements WebMvcConfigurer
public void addInterceptors(InterceptorRegistry registry) public void addInterceptors(InterceptorRegistry registry)
{ {
registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**"); registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");
// 接口调用统计(记录哪些接口被调用、次数、耗时,用于后续重构分析)
registry.addInterceptor(apiCallStatsInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(
"/swagger-ui/**",
"/swagger-resources/**",
"/v2/api-docs",
"/v3/api-docs",
"/webjars/**",
"/profile/**",
"/favicon.ico",
"/error");
} }
/** /**
......
package com.ruoyi.framework.interceptor.impl;
import java.lang.reflect.Method;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.HandlerMapping;
import com.ruoyi.framework.manager.AsyncManager;
import com.ruoyi.framework.manager.factory.AsyncFactory;
/**
* 接口调用统计拦截器。
* <p>
* 目标:记录"哪些接口被调用 + 调用次数 + 耗时",用于后续重构分析。
* <p>
* - 只统计能匹配到 {@link HandlerMethod} 的请求(自动跳过 404、静态资源、CORS 预检等)。
* - 在 {@code preHandle} 记录开始时间到 request attribute。
* - 在 {@code afterCompletion} 计算耗时,异步写库。
* - 所有异常吞掉仅记 log,保证绝不影响主业务请求。
*
* @author ruoyi
*/
@Component
public class ApiCallStatsInterceptor implements HandlerInterceptor
{
private static final Logger log = LoggerFactory.getLogger(ApiCallStatsInterceptor.class);
/** 请求开始时间的 request attribute key */
private static final String START_TIME_ATTR = "__api_call_stats_start__";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
{
if (handler instanceof HandlerMethod)
{
request.setAttribute(START_TIME_ATTR, System.currentTimeMillis());
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
{
if (!(handler instanceof HandlerMethod))
{
return;
}
try
{
Object startAttr = request.getAttribute(START_TIME_ATTR);
long start = startAttr instanceof Long ? (Long) startAttr : System.currentTimeMillis();
long cost = System.currentTimeMillis() - start;
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
String handlerClass = method.getDeclaringClass().getName();
String handlerMethodName = method.getName();
String httpMethod = request.getMethod();
String urlPattern = resolveUrlPattern(request);
boolean isError = ex != null || response.getStatus() >= 500;
AsyncManager.me().execute(
AsyncFactory.recordApiCallStats(httpMethod, urlPattern, handlerClass, handlerMethodName,
cost, isError));
}
catch (Exception e)
{
log.warn("[ApiCallStats] record failed: {}", e.getMessage());
}
}
/**
* 优先使用 Spring 匹配到的 URL pattern(如 /xxx/{id}),
* 匹配不到则回退到原始 requestURI。
*/
private String resolveUrlPattern(HttpServletRequest request)
{
Object best = request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
if (best instanceof String && !((String) best).isEmpty())
{
return (String) best;
}
return request.getRequestURI();
}
}
...@@ -10,8 +10,10 @@ import com.ruoyi.common.utils.StringUtils; ...@@ -10,8 +10,10 @@ import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.ip.AddressUtils; import com.ruoyi.common.utils.ip.AddressUtils;
import com.ruoyi.common.utils.ip.IpUtils; import com.ruoyi.common.utils.ip.IpUtils;
import com.ruoyi.common.utils.spring.SpringUtils; import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.system.domain.monitor.ApiCallStats;
import com.ruoyi.system.domain.system.SysLogininfor; import com.ruoyi.system.domain.system.SysLogininfor;
import com.ruoyi.system.domain.system.SysOperLog; import com.ruoyi.system.domain.system.SysOperLog;
import com.ruoyi.system.mapper.monitor.ApiCallStatsMapper;
import com.ruoyi.system.service.system.ISysLogininforService; import com.ruoyi.system.service.system.ISysLogininforService;
import com.ruoyi.system.service.system.ISysOperLogService; import com.ruoyi.system.service.system.ISysOperLogService;
import eu.bitwalker.useragentutils.UserAgent; import eu.bitwalker.useragentutils.UserAgent;
...@@ -99,4 +101,38 @@ public class AsyncFactory ...@@ -99,4 +101,38 @@ public class AsyncFactory
} }
}; };
} }
/**
* 接口调用统计(按 http_method + url_pattern 聚合 upsert)。
* <p>仅用于记录"哪些接口被调用 + 调用次数 + 耗时",服务后续重构分析。
*/
public static TimerTask recordApiCallStats(final String httpMethod, final String urlPattern,
final String handlerClass, final String handlerMethod,
final long costMs, final boolean isError)
{
return new TimerTask()
{
@Override
public void run()
{
try
{
ApiCallStats stats = new ApiCallStats();
stats.setHttpMethod(httpMethod);
stats.setUrlPattern(urlPattern);
stats.setHandlerClass(handlerClass);
stats.setHandlerMethod(handlerMethod);
stats.setLastCostMs(costMs);
stats.setErrorCount(isError ? 1L : 0L);
SpringUtils.getBean(ApiCallStatsMapper.class).upsert(stats);
}
catch (Exception e)
{
// 统计失败不影响主流程
LoggerFactory.getLogger(AsyncFactory.class)
.warn("[ApiCallStats] async upsert failed: {}", e.getMessage());
}
}
};
}
} }
package com.ruoyi.system.domain.monitor;
import java.util.Date;
/**
* 接口调用统计(按 http_method + url_pattern 聚合)
*
* 用于后续重构分析:哪些接口还在用、调用频率、耗时分布。
*
* @author ruoyi
*/
public class ApiCallStats
{
private Long id;
/** 请求方式 GET/POST/PUT/DELETE */
private String httpMethod;
/** 接口路径模式(带占位符,如 /xxx/{id}) */
private String urlPattern;
/** 处理器类全限定名 */
private String handlerClass;
/** 处理器方法名 */
private String handlerMethod;
/** 累计调用次数 */
private Long callCount;
/** 累计耗时(ms) */
private Long totalCostMs;
/** 最大耗时(ms) */
private Long maxCostMs;
/** 最小耗时(ms) */
private Long minCostMs;
/** 最近一次耗时(ms) */
private Long lastCostMs;
/** 异常次数 */
private Long errorCount;
/** 首次调用时间 */
private Date firstCallTime;
/** 最近调用时间 */
private Date lastCallTime;
public Long getId()
{
return id;
}
public void setId(Long id)
{
this.id = id;
}
public String getHttpMethod()
{
return httpMethod;
}
public void setHttpMethod(String httpMethod)
{
this.httpMethod = httpMethod;
}
public String getUrlPattern()
{
return urlPattern;
}
public void setUrlPattern(String urlPattern)
{
this.urlPattern = urlPattern;
}
public String getHandlerClass()
{
return handlerClass;
}
public void setHandlerClass(String handlerClass)
{
this.handlerClass = handlerClass;
}
public String getHandlerMethod()
{
return handlerMethod;
}
public void setHandlerMethod(String handlerMethod)
{
this.handlerMethod = handlerMethod;
}
public Long getCallCount()
{
return callCount;
}
public void setCallCount(Long callCount)
{
this.callCount = callCount;
}
public Long getTotalCostMs()
{
return totalCostMs;
}
public void setTotalCostMs(Long totalCostMs)
{
this.totalCostMs = totalCostMs;
}
public Long getMaxCostMs()
{
return maxCostMs;
}
public void setMaxCostMs(Long maxCostMs)
{
this.maxCostMs = maxCostMs;
}
public Long getMinCostMs()
{
return minCostMs;
}
public void setMinCostMs(Long minCostMs)
{
this.minCostMs = minCostMs;
}
public Long getLastCostMs()
{
return lastCostMs;
}
public void setLastCostMs(Long lastCostMs)
{
this.lastCostMs = lastCostMs;
}
public Long getErrorCount()
{
return errorCount;
}
public void setErrorCount(Long errorCount)
{
this.errorCount = errorCount;
}
public Date getFirstCallTime()
{
return firstCallTime;
}
public void setFirstCallTime(Date firstCallTime)
{
this.firstCallTime = firstCallTime;
}
public Date getLastCallTime()
{
return lastCallTime;
}
public void setLastCallTime(Date lastCallTime)
{
this.lastCallTime = lastCallTime;
}
}
package com.ruoyi.system.mapper.monitor;
import com.ruoyi.system.domain.monitor.ApiCallStats;
/**
* 接口调用统计 数据层
*
* @author ruoyi
*/
public interface ApiCallStatsMapper
{
/**
* 新增或更新一条接口调用统计。
* <p>通过 (http_method, url_pattern) 唯一键做 upsert:
* 首次调用则插入,后续调用累加 call_count / total_cost_ms,
* 更新 max/min/last_cost_ms、error_count、last_call_time。
*/
void upsert(ApiCallStats stats);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.system.mapper.monitor.ApiCallStatsMapper">
<insert id="upsert" parameterType="ApiCallStats">
INSERT INTO sys_api_call_stats (
http_method, url_pattern, handler_class, handler_method,
call_count, total_cost_ms, max_cost_ms, min_cost_ms, last_cost_ms,
error_count, first_call_time, last_call_time
) VALUES (
#{httpMethod}, #{urlPattern}, #{handlerClass}, #{handlerMethod},
1, #{lastCostMs}, #{lastCostMs}, #{lastCostMs}, #{lastCostMs},
#{errorCount}, NOW(), NOW()
)
ON DUPLICATE KEY UPDATE
call_count = call_count + 1,
total_cost_ms = total_cost_ms + VALUES(last_cost_ms),
max_cost_ms = GREATEST(max_cost_ms, VALUES(last_cost_ms)),
min_cost_ms = LEAST(min_cost_ms, VALUES(last_cost_ms)),
last_cost_ms = VALUES(last_cost_ms),
error_count = error_count + VALUES(error_count),
handler_class = VALUES(handler_class),
handler_method = VALUES(handler_method),
last_call_time = NOW()
</insert>
</mapper>
-- 接口调用统计表(用于后续重构分析:哪些接口在用、调用频率、耗时)
-- 按 (http_method, url_pattern) 聚合,一个接口一行,upsert 更新
DROP TABLE IF EXISTS `sys_api_call_stats`;
CREATE TABLE `sys_api_call_stats` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`http_method` varchar(10) NOT NULL COMMENT '请求方式 GET/POST/PUT/DELETE/...',
`url_pattern` varchar(255) NOT NULL COMMENT '接口路径模式(带占位符,如 /xxx/{id})',
`handler_class` varchar(255) DEFAULT NULL COMMENT '处理器类全限定名',
`handler_method` varchar(128) DEFAULT NULL COMMENT '处理器方法名',
`call_count` bigint(20) NOT NULL DEFAULT '0' COMMENT '累计调用次数',
`total_cost_ms` bigint(20) NOT NULL DEFAULT '0' COMMENT '累计耗时(ms),用于算平均',
`max_cost_ms` bigint(20) NOT NULL DEFAULT '0' COMMENT '最大耗时(ms)',
`min_cost_ms` bigint(20) NOT NULL DEFAULT '0' COMMENT '最小耗时(ms)',
`last_cost_ms` bigint(20) NOT NULL DEFAULT '0' COMMENT '最近一次耗时(ms)',
`error_count` bigint(20) NOT NULL DEFAULT '0' COMMENT '异常调用次数',
`first_call_time` datetime DEFAULT NULL COMMENT '首次调用时间',
`last_call_time` datetime DEFAULT NULL COMMENT '最近调用时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_method_url` (`http_method`, `url_pattern`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='接口调用统计';
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment