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:
mediabot-pt
2026-06-30 11:32:50 +08:00
parent 1664033c53
commit 88728ce2c2
10 changed files with 331 additions and 0 deletions

View File

@@ -0,0 +1,62 @@
package com.par.api.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.par.core.dto.ApiResponse;
import com.par.core.entity.ApiAccessLog;
import com.par.core.mapper.ApiAccessLogMapper;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.Map;
/**
* API 访问日志控制器
*/
@RestController
@RequestMapping("/api/v1/admin/logs")
@RequiredArgsConstructor
public class ApiLogController {
private final ApiAccessLogMapper mapper;
@GetMapping
public ApiResponse<Map<String, Object>> list(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "50") int size,
@RequestParam(required = false) String email,
@RequestParam(required = false) String path,
@RequestParam(required = false) Boolean success,
HttpServletRequest request) {
// checkAdmin implicitly via interceptor
LambdaQueryWrapper<ApiAccessLog> q = new LambdaQueryWrapper<>();
if (email != null && !email.isBlank()) q.like(ApiAccessLog::getEmail, email);
if (path != null && !path.isBlank()) q.like(ApiAccessLog::getPath, path);
if (success != null) q.eq(ApiAccessLog::getSuccess, success);
q.orderByDesc(ApiAccessLog::getCreatedAt);
Page<ApiAccessLog> p = mapper.selectPage(new Page<>(page, size), q);
Map<String, Object> result = Map.of(
"records", p.getRecords(),
"total", p.getTotal(),
"page", p.getCurrent(),
"size", p.getSize()
);
return ApiResponse.success(result);
}
@GetMapping("/stats/email")
public ApiResponse<Object> statsByEmail(@RequestParam(defaultValue = "24") int hours) {
String since = LocalDateTime.now().minusHours(hours).toString();
return ApiResponse.success(mapper.statsByEmail(since));
}
@GetMapping("/stats/path")
public ApiResponse<Object> statsByPath(@RequestParam(defaultValue = "24") int hours) {
String since = LocalDateTime.now().minusHours(hours).toString();
return ApiResponse.success(mapper.statsByPath(since));
}
}