feat: API 访问日志系统(拦截器 + 统计分析 + 管理页面)
- V6: api_access_logs 表(method/path/email/IP/status/耗时/错误) - ApiLogInterceptor: 记录所有 /api/v1/** 请求,order(0) 最优先 - ApiLogController: 分页查询 + 按邮箱/路径统计(24h 窗口) - 前端 ApiLogs.vue: 筛选/统计面板/日志列表/分页 - 导航新增「访问日志」菜单项
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package com.par.core.config;
|
||||
|
||||
import com.par.core.interceptor.AnonymousStatInterceptor;
|
||||
import com.par.core.interceptor.ApiLogInterceptor;
|
||||
import com.par.core.interceptor.HmacAuthInterceptor;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -16,9 +17,15 @@ public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
private final HmacAuthInterceptor hmacAuthInterceptor;
|
||||
private final AnonymousStatInterceptor anonymousStatInterceptor;
|
||||
private final ApiLogInterceptor apiLogInterceptor;
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
// API 日志拦截器:记录所有 API 请求(最优先)
|
||||
registry.addInterceptor(apiLogInterceptor)
|
||||
.addPathPatterns("/api/v1/**")
|
||||
.order(0);
|
||||
|
||||
// 匿名统计拦截器:拦截所有读取端点
|
||||
registry.addInterceptor(anonymousStatInterceptor)
|
||||
.addPathPatterns("/api/v1/sites/**", "/api/v1/configs/**", "/api/v1/manifest");
|
||||
|
||||
50
par-core/src/main/java/com/par/core/entity/ApiAccessLog.java
Normal file
50
par-core/src/main/java/com/par/core/entity/ApiAccessLog.java
Normal file
@@ -0,0 +1,50 @@
|
||||
package com.par.core.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* API 访问日志实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("api_access_logs")
|
||||
public class ApiAccessLog {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
@TableField("method")
|
||||
private String method;
|
||||
|
||||
@TableField("path")
|
||||
private String path;
|
||||
|
||||
@TableField("email")
|
||||
private String email;
|
||||
|
||||
@TableField("client_ip")
|
||||
private String clientIp;
|
||||
|
||||
@TableField("user_agent")
|
||||
private String userAgent;
|
||||
|
||||
@TableField("status_code")
|
||||
private Integer statusCode;
|
||||
|
||||
@TableField("response_ms")
|
||||
private Integer responseMs;
|
||||
|
||||
@TableField("success")
|
||||
private Boolean success;
|
||||
|
||||
@TableField("error_message")
|
||||
private String errorMessage;
|
||||
|
||||
@TableField("psk_version")
|
||||
private String pskVersion;
|
||||
|
||||
@TableField(value = "created_at", fill = FieldFill.INSERT)
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.par.core.interceptor;
|
||||
|
||||
import com.par.core.entity.ApiAccessLog;
|
||||
import com.par.core.mapper.ApiAccessLogMapper;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
/**
|
||||
* API 访问日志拦截器
|
||||
* 记录所有 API 请求(成功和失败),异步插入忽略写入错误
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class ApiLogInterceptor implements HandlerInterceptor {
|
||||
|
||||
private final ApiAccessLogMapper mapper;
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
||||
request.setAttribute("_log_start", System.currentTimeMillis());
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
|
||||
Object handler, Exception ex) {
|
||||
long elapsed = System.currentTimeMillis() - (long) request.getAttribute("_log_start");
|
||||
String email = request.getHeader("X-Email");
|
||||
String pskVer = (String) request.getAttribute("pskVersion");
|
||||
|
||||
ApiAccessLog log = new ApiAccessLog();
|
||||
log.setMethod(request.getMethod());
|
||||
log.setPath(request.getRequestURI());
|
||||
log.setEmail(email);
|
||||
log.setClientIp(request.getRemoteAddr());
|
||||
log.setUserAgent(request.getHeader("User-Agent"));
|
||||
log.setStatusCode(response.getStatus());
|
||||
log.setResponseMs((int) elapsed);
|
||||
log.setSuccess(response.getStatus() < 400 && ex == null);
|
||||
log.setErrorMessage(ex != null ? ex.getMessage() : null);
|
||||
log.setPskVersion(pskVer);
|
||||
|
||||
try { mapper.insert(log); } catch (Exception e) {
|
||||
// 日志写入失败不影响正常请求
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.par.core.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.par.core.entity.ApiAccessLog;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* API 访问日志 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface ApiAccessLogMapper extends BaseMapper<ApiAccessLog> {
|
||||
|
||||
@Select("SELECT email, COUNT(*) AS cnt, SUM(success) AS ok, " +
|
||||
"SUM(CASE WHEN success=0 THEN 1 ELSE 0 END) AS err " +
|
||||
"FROM api_access_logs WHERE created_at >= #{since} GROUP BY email ORDER BY cnt DESC")
|
||||
List<Map<String, Object>> statsByEmail(@Param("since") String since);
|
||||
|
||||
@Select("SELECT path, COUNT(*) AS cnt, AVG(response_ms) AS avg_ms " +
|
||||
"FROM api_access_logs WHERE created_at >= #{since} GROUP BY path ORDER BY cnt DESC")
|
||||
List<Map<String, Object>> statsByPath(@Param("since") String since);
|
||||
|
||||
@Select("SELECT DATE(created_at) AS dt, COUNT(*) AS cnt, SUM(success) AS ok " +
|
||||
"FROM api_access_logs WHERE created_at >= #{since} GROUP BY dt ORDER BY dt")
|
||||
List<Map<String, Object>> statsByDay(@Param("since") String since);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
-- PAR Flyway V6: API 访问日志表
|
||||
CREATE TABLE api_access_logs (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
method VARCHAR(10) NOT NULL COMMENT '请求方法',
|
||||
path VARCHAR(500) NOT NULL COMMENT '请求路径',
|
||||
email VARCHAR(255) COMMENT 'X-Email 请求头',
|
||||
client_ip VARCHAR(45) COMMENT '客户端 IP',
|
||||
user_agent VARCHAR(500) COMMENT 'User-Agent',
|
||||
status_code INT NOT NULL COMMENT 'HTTP 状态码',
|
||||
response_ms INT NOT NULL COMMENT '响应耗时(毫秒)',
|
||||
success TINYINT(1) NOT NULL COMMENT '是否成功',
|
||||
error_message VARCHAR(500) COMMENT '错误信息',
|
||||
psk_version VARCHAR(20) COMMENT 'PSK 版本号',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '请求时间',
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_email (email),
|
||||
KEY idx_path (path(100)),
|
||||
KEY idx_created (created_at),
|
||||
KEY idx_success (success)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='API 访问日志表';
|
||||
Reference in New Issue
Block a user