diff --git a/ChangeLog.md b/ChangeLog.md index 52dce1c..d22b279 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,3 +1,6 @@ +### v1.3.1 +1. 添加API访问日志 + ### v1.3.0 1. 新增 Telegram Bot 2. 新增扩容 diff --git a/db/connection.go b/db/connection.go index 89fccce..d7af929 100644 --- a/db/connection.go +++ b/db/connection.go @@ -83,6 +83,9 @@ func InitDB() error { &entity.TaskItem{}, &entity.File{}, &entity.TelegramChannel{}, + &entity.APIAccessLog{}, + &entity.APIAccessLogStats{}, + &entity.APIAccessLogSummary{}, ) if err != nil { utils.Fatal("数据库迁移失败: %v", err) diff --git a/db/converter/api_access_log_converter.go b/db/converter/api_access_log_converter.go new file mode 100644 index 0000000..8fe6b0e --- /dev/null +++ b/db/converter/api_access_log_converter.go @@ -0,0 +1,66 @@ +package converter + +import ( + "github.com/ctwj/urldb/db/dto" + "github.com/ctwj/urldb/db/entity" +) + +// ToAPIAccessLogResponse 将APIAccessLog实体转换为APIAccessLogResponse +func ToAPIAccessLogResponse(log *entity.APIAccessLog) dto.APIAccessLogResponse { + return dto.APIAccessLogResponse{ + ID: log.ID, + IP: log.IP, + UserAgent: log.UserAgent, + Endpoint: log.Endpoint, + Method: log.Method, + RequestParams: log.RequestParams, + ResponseStatus: log.ResponseStatus, + ResponseData: log.ResponseData, + ProcessCount: log.ProcessCount, + ErrorMessage: log.ErrorMessage, + ProcessingTime: log.ProcessingTime, + CreatedAt: log.CreatedAt, + } +} + +// ToAPIAccessLogResponseList 将APIAccessLog实体列表转换为APIAccessLogResponse列表 +func ToAPIAccessLogResponseList(logs []entity.APIAccessLog) []dto.APIAccessLogResponse { + responses := make([]dto.APIAccessLogResponse, len(logs)) + for i, log := range logs { + responses[i] = ToAPIAccessLogResponse(&log) + } + return responses +} + +// ToAPIAccessLogSummaryResponse 将APIAccessLogSummary实体转换为APIAccessLogSummaryResponse +func ToAPIAccessLogSummaryResponse(summary *entity.APIAccessLogSummary) dto.APIAccessLogSummaryResponse { + return dto.APIAccessLogSummaryResponse{ + TotalRequests: summary.TotalRequests, + TodayRequests: summary.TodayRequests, + WeekRequests: summary.WeekRequests, + MonthRequests: summary.MonthRequests, + ErrorRequests: summary.ErrorRequests, + UniqueIPs: summary.UniqueIPs, + } +} + +// ToAPIAccessLogStatsResponse 将APIAccessLogStats实体转换为APIAccessLogStatsResponse +func ToAPIAccessLogStatsResponse(stat entity.APIAccessLogStats) dto.APIAccessLogStatsResponse { + return dto.APIAccessLogStatsResponse{ + Endpoint: stat.Endpoint, + Method: stat.Method, + RequestCount: stat.RequestCount, + ErrorCount: stat.ErrorCount, + AvgProcessTime: stat.AvgProcessTime, + LastAccess: stat.LastAccess, + } +} + +// ToAPIAccessLogStatsResponseList 将APIAccessLogStats实体列表转换为APIAccessLogStatsResponse列表 +func ToAPIAccessLogStatsResponseList(stats []entity.APIAccessLogStats) []dto.APIAccessLogStatsResponse { + responses := make([]dto.APIAccessLogStatsResponse, len(stats)) + for i, stat := range stats { + responses[i] = ToAPIAccessLogStatsResponse(stat) + } + return responses +} diff --git a/db/dto/api_access_log.go b/db/dto/api_access_log.go new file mode 100644 index 0000000..3e7bfd0 --- /dev/null +++ b/db/dto/api_access_log.go @@ -0,0 +1,55 @@ +package dto + +import "time" + +// APIAccessLogResponse API访问日志响应 +type APIAccessLogResponse struct { + ID uint `json:"id"` + IP string `json:"ip"` + UserAgent string `json:"user_agent"` + Endpoint string `json:"endpoint"` + Method string `json:"method"` + RequestParams string `json:"request_params"` + ResponseStatus int `json:"response_status"` + ResponseData string `json:"response_data"` + ProcessCount int `json:"process_count"` + ErrorMessage string `json:"error_message"` + ProcessingTime int64 `json:"processing_time"` + CreatedAt time.Time `json:"created_at"` +} + +// APIAccessLogSummaryResponse API访问日志汇总响应 +type APIAccessLogSummaryResponse struct { + TotalRequests int64 `json:"total_requests"` + TodayRequests int64 `json:"today_requests"` + WeekRequests int64 `json:"week_requests"` + MonthRequests int64 `json:"month_requests"` + ErrorRequests int64 `json:"error_requests"` + UniqueIPs int64 `json:"unique_ips"` +} + +// APIAccessLogStatsResponse 按端点统计响应 +type APIAccessLogStatsResponse struct { + Endpoint string `json:"endpoint"` + Method string `json:"method"` + RequestCount int64 `json:"request_count"` + ErrorCount int64 `json:"error_count"` + AvgProcessTime int64 `json:"avg_process_time"` + LastAccess time.Time `json:"last_access"` +} + +// APIAccessLogListResponse API访问日志列表响应 +type APIAccessLogListResponse struct { + Data []APIAccessLogResponse `json:"data"` + Total int64 `json:"total"` +} + +// APIAccessLogFilterRequest API访问日志过滤请求 +type APIAccessLogFilterRequest struct { + StartDate string `json:"start_date,omitempty"` + EndDate string `json:"end_date,omitempty"` + Endpoint string `json:"endpoint,omitempty"` + IP string `json:"ip,omitempty"` + Page int `json:"page,omitempty" default:"1"` + PageSize int `json:"page_size,omitempty" default:"20"` +} diff --git a/db/entity/api_access_log.go b/db/entity/api_access_log.go new file mode 100644 index 0000000..2156f42 --- /dev/null +++ b/db/entity/api_access_log.go @@ -0,0 +1,50 @@ +package entity + +import ( + "time" + + "gorm.io/gorm" +) + +// APIAccessLog API访问日志模型 +type APIAccessLog struct { + ID uint `json:"id" gorm:"primaryKey;autoIncrement"` + IP string `json:"ip" gorm:"size:45;not null;comment:客户端IP地址"` + UserAgent string `json:"user_agent" gorm:"size:500;comment:用户代理"` + Endpoint string `json:"endpoint" gorm:"size:255;not null;comment:访问的接口路径"` + Method string `json:"method" gorm:"size:10;not null;comment:HTTP方法"` + RequestParams string `json:"request_params" gorm:"type:text;comment:查询参数(JSON格式)"` + ResponseStatus int `json:"response_status" gorm:"default:200;comment:响应状态码"` + ResponseData string `json:"response_data" gorm:"type:text;comment:响应数据(JSON格式)"` + ProcessCount int `json:"process_count" gorm:"default:0;comment:处理数量(查询结果数或添加的数量)"` + ErrorMessage string `json:"error_message" gorm:"size:500;comment:错误消息"` + ProcessingTime int64 `json:"processing_time" gorm:"comment:处理时间(毫秒)"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"` +} + +// TableName 指定表名 +func (APIAccessLog) TableName() string { + return "api_access_logs" +} + +// APIAccessLogSummary API访问日志汇总统计 +type APIAccessLogSummary struct { + TotalRequests int64 `json:"total_requests"` + TodayRequests int64 `json:"today_requests"` + WeekRequests int64 `json:"week_requests"` + MonthRequests int64 `json:"month_requests"` + ErrorRequests int64 `json:"error_requests"` + UniqueIPs int64 `json:"unique_ips"` +} + +// APIAccessLogStats 按端点统计 +type APIAccessLogStats struct { + Endpoint string `json:"endpoint"` + Method string `json:"method"` + RequestCount int64 `json:"request_count"` + ErrorCount int64 `json:"error_count"` + AvgProcessTime int64 `json:"avg_process_time"` + LastAccess time.Time `json:"last_access"` +} diff --git a/db/repo/api_access_log_repository.go b/db/repo/api_access_log_repository.go new file mode 100644 index 0000000..7816ef5 --- /dev/null +++ b/db/repo/api_access_log_repository.go @@ -0,0 +1,169 @@ +package repo + +import ( + "encoding/json" + "time" + + "github.com/ctwj/urldb/db/entity" + "github.com/ctwj/urldb/utils" + + "gorm.io/gorm" +) + +// APIAccessLogRepository API访问日志Repository接口 +type APIAccessLogRepository interface { + BaseRepository[entity.APIAccessLog] + RecordAccess(ip, userAgent, endpoint, method string, requestParams interface{}, responseStatus int, responseData interface{}, processCount int, errorMessage string, processingTime int64) error + GetSummary() (*entity.APIAccessLogSummary, error) + GetStatsByEndpoint() ([]entity.APIAccessLogStats, error) + FindWithFilters(page, limit int, startDate, endDate *time.Time, endpoint, ip string) ([]entity.APIAccessLog, int64, error) + ClearOldLogs(days int) error +} + +// APIAccessLogRepositoryImpl API访问日志Repository实现 +type APIAccessLogRepositoryImpl struct { + BaseRepositoryImpl[entity.APIAccessLog] +} + +// NewAPIAccessLogRepository 创建API访问日志Repository +func NewAPIAccessLogRepository(db *gorm.DB) APIAccessLogRepository { + return &APIAccessLogRepositoryImpl{ + BaseRepositoryImpl: BaseRepositoryImpl[entity.APIAccessLog]{db: db}, + } +} + +// RecordAccess 记录API访问 +func (r *APIAccessLogRepositoryImpl) RecordAccess(ip, userAgent, endpoint, method string, requestParams interface{}, responseStatus int, responseData interface{}, processCount int, errorMessage string, processingTime int64) error { + log := entity.APIAccessLog{ + IP: ip, + UserAgent: userAgent, + Endpoint: endpoint, + Method: method, + ResponseStatus: responseStatus, + ProcessCount: processCount, + ErrorMessage: errorMessage, + ProcessingTime: processingTime, + } + + // 序列化请求参数 + if requestParams != nil { + if paramsJSON, err := json.Marshal(requestParams); err == nil { + log.RequestParams = string(paramsJSON) + } + } + + // 序列化响应数据(限制大小,避免存储大量数据) + if responseData != nil { + if dataJSON, err := json.Marshal(responseData); err == nil { + // 限制响应数据长度,避免存储过多数据 + dataStr := string(dataJSON) + if len(dataStr) > 2000 { + dataStr = dataStr[:2000] + "..." + } + log.ResponseData = dataStr + } + } + + return r.db.Create(&log).Error +} + +// GetSummary 获取访问日志汇总 +func (r *APIAccessLogRepositoryImpl) GetSummary() (*entity.APIAccessLogSummary, error) { + var summary entity.APIAccessLogSummary + now := utils.GetCurrentTime() + todayStr := now.Format(utils.TimeFormatDate) + weekStart := now.AddDate(0, 0, -int(now.Weekday())+1).Format(utils.TimeFormatDate) + monthStart := now.Format("2006-01") + "-01" + + // 总请求数 + if err := r.db.Model(&entity.APIAccessLog{}).Count(&summary.TotalRequests).Error; err != nil { + return nil, err + } + + // 今日请求数 + if err := r.db.Model(&entity.APIAccessLog{}).Where("DATE(created_at) = ?", todayStr).Count(&summary.TodayRequests).Error; err != nil { + return nil, err + } + + // 本周请求数 + if err := r.db.Model(&entity.APIAccessLog{}).Where("created_at >= ?", weekStart).Count(&summary.WeekRequests).Error; err != nil { + return nil, err + } + + // 本月请求数 + if err := r.db.Model(&entity.APIAccessLog{}).Where("created_at >= ?", monthStart).Count(&summary.MonthRequests).Error; err != nil { + return nil, err + } + + // 错误请求数 + if err := r.db.Model(&entity.APIAccessLog{}).Where("response_status >= 400").Count(&summary.ErrorRequests).Error; err != nil { + return nil, err + } + + // 唯一IP数 + if err := r.db.Model(&entity.APIAccessLog{}).Distinct("ip").Count(&summary.UniqueIPs).Error; err != nil { + return nil, err + } + + return &summary, nil +} + +// GetStatsByEndpoint 按端点获取统计 +func (r *APIAccessLogRepositoryImpl) GetStatsByEndpoint() ([]entity.APIAccessLogStats, error) { + var stats []entity.APIAccessLogStats + + query := ` + SELECT + endpoint, + method, + COUNT(*) as request_count, + SUM(CASE WHEN response_status >= 400 THEN 1 ELSE 0 END) as error_count, + AVG(processing_time) as avg_process_time, + MAX(created_at) as last_access + FROM api_access_logs + WHERE deleted_at IS NULL + GROUP BY endpoint, method + ORDER BY request_count DESC + ` + + err := r.db.Raw(query).Scan(&stats).Error + return stats, err +} + +// FindWithFilters 带过滤条件的分页查找访问日志 +func (r *APIAccessLogRepositoryImpl) FindWithFilters(page, limit int, startDate, endDate *time.Time, endpoint, ip string) ([]entity.APIAccessLog, int64, error) { + var logs []entity.APIAccessLog + var total int64 + + offset := (page - 1) * limit + query := r.db.Model(&entity.APIAccessLog{}) + + // 添加过滤条件 + if startDate != nil { + query = query.Where("created_at >= ?", *startDate) + } + if endDate != nil { + query = query.Where("created_at <= ?", *endDate) + } + if endpoint != "" { + query = query.Where("endpoint LIKE ?", "%"+endpoint+"%") + } + if ip != "" { + query = query.Where("ip = ?", ip) + } + + // 获取总数 + if err := query.Count(&total).Error; err != nil { + return nil, 0, err + } + + // 获取分页数据,按创建时间倒序排列 + err := query.Order("created_at DESC").Offset(offset).Limit(limit).Find(&logs).Error + return logs, total, err +} + +// ClearOldLogs 清理旧日志 +func (r *APIAccessLogRepositoryImpl) ClearOldLogs(days int) error { + cutoffDate := utils.GetCurrentTime().AddDate(0, 0, -days) + return r.db.Where("created_at < ?", cutoffDate).Delete(&entity.APIAccessLog{}).Error +} diff --git a/db/repo/manager.go b/db/repo/manager.go index 60aee04..2aeb145 100644 --- a/db/repo/manager.go +++ b/db/repo/manager.go @@ -21,6 +21,7 @@ type RepositoryManager struct { TaskItemRepository TaskItemRepository FileRepository FileRepository TelegramChannelRepository TelegramChannelRepository + APIAccessLogRepository APIAccessLogRepository } // NewRepositoryManager 创建Repository管理器 @@ -41,5 +42,6 @@ func NewRepositoryManager(db *gorm.DB) *RepositoryManager { TaskItemRepository: NewTaskItemRepository(db), FileRepository: NewFileRepository(db), TelegramChannelRepository: NewTelegramChannelRepository(db), + APIAccessLogRepository: NewAPIAccessLogRepository(db), } } diff --git a/handlers/api_access_log_handler.go b/handlers/api_access_log_handler.go new file mode 100644 index 0000000..34fb6aa --- /dev/null +++ b/handlers/api_access_log_handler.go @@ -0,0 +1,100 @@ +package handlers + +import ( + "net/http" + "strconv" + "time" + + "github.com/ctwj/urldb/db/converter" + + "github.com/gin-gonic/gin" +) + +// GetAPIAccessLogs 获取API访问日志 +func GetAPIAccessLogs(c *gin.Context) { + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) + startDateStr := c.Query("start_date") + endDateStr := c.Query("end_date") + endpoint := c.Query("endpoint") + ip := c.Query("ip") + + var startDate, endDate *time.Time + + if startDateStr != "" { + if parsed, err := time.Parse("2006-01-02", startDateStr); err == nil { + startDate = &parsed + } + } + + if endDateStr != "" { + if parsed, err := time.Parse("2006-01-02", endDateStr); err == nil { + // 设置为当天结束时间 + endOfDay := parsed.Add(24*time.Hour - time.Second) + endDate = &endOfDay + } + } + + // 获取分页数据 + logs, total, err := repoManager.APIAccessLogRepository.FindWithFilters(page, pageSize, startDate, endDate, endpoint, ip) + if err != nil { + ErrorResponse(c, "获取API访问日志失败: "+err.Error(), http.StatusInternalServerError) + return + } + + response := converter.ToAPIAccessLogResponseList(logs) + + SuccessResponse(c, gin.H{ + "data": response, + "total": int(total), + "page": page, + "limit": pageSize, + }) +} + +// GetAPIAccessLogSummary 获取API访问日志汇总 +func GetAPIAccessLogSummary(c *gin.Context) { + summary, err := repoManager.APIAccessLogRepository.GetSummary() + if err != nil { + ErrorResponse(c, "获取API访问日志汇总失败: "+err.Error(), 500) + return + } + + response := converter.ToAPIAccessLogSummaryResponse(summary) + SuccessResponse(c, response) +} + +// GetAPIAccessLogStats 获取API访问日志统计 +func GetAPIAccessLogStats(c *gin.Context) { + stats, err := repoManager.APIAccessLogRepository.GetStatsByEndpoint() + if err != nil { + ErrorResponse(c, "获取API访问日志统计失败: "+err.Error(), http.StatusInternalServerError) + return + } + + response := converter.ToAPIAccessLogStatsResponseList(stats) + SuccessResponse(c, response) +} + +// ClearAPIAccessLogs 清理API访问日志 +func ClearAPIAccessLogs(c *gin.Context) { + daysStr := c.Query("days") + if daysStr == "" { + ErrorResponse(c, "请提供要清理的天数参数", http.StatusBadRequest) + return + } + + days, err := strconv.Atoi(daysStr) + if err != nil || days < 1 { + ErrorResponse(c, "无效的天数参数", http.StatusBadRequest) + return + } + + err = repoManager.APIAccessLogRepository.ClearOldLogs(days) + if err != nil { + ErrorResponse(c, "清理API访问日志失败: "+err.Error(), http.StatusInternalServerError) + return + } + + SuccessResponse(c, gin.H{"message": "API访问日志清理成功"}) +} diff --git a/handlers/public_api_handler.go b/handlers/public_api_handler.go index a82556c..c11936a 100644 --- a/handlers/public_api_handler.go +++ b/handlers/public_api_handler.go @@ -3,6 +3,7 @@ package handlers import ( "strconv" "strings" + "time" "github.com/ctwj/urldb/db/dto" "github.com/ctwj/urldb/db/entity" @@ -69,6 +70,53 @@ func (h *PublicAPIHandler) filterForbiddenWords(resources []entity.Resource) ([] return filteredResources, uniqueForbiddenWords } +// logAPIAccess 记录API访问日志 +func (h *PublicAPIHandler) logAPIAccess(c *gin.Context, startTime time.Time, processCount int, responseData interface{}, errorMessage string) { + endpoint := c.Request.URL.Path + method := c.Request.Method + ip := c.ClientIP() + userAgent := c.GetHeader("User-Agent") + + // 计算处理时间 + processingTime := time.Since(startTime).Milliseconds() + + // 获取查询参数 + var requestParams interface{} + if method == "GET" { + requestParams = c.Request.URL.Query() + } else { + // 对于POST请求,尝试从上下文中获取请求体(如果之前已解析) + if req, exists := c.Get("request_body"); exists { + requestParams = req + } + } + + // 异步记录日志,避免影响API响应时间 + go func() { + defer func() { + if r := recover(); r != nil { + utils.Error("记录API访问日志时发生panic: %v", r) + } + }() + + err := repoManager.APIAccessLogRepository.RecordAccess( + ip, + userAgent, + endpoint, + method, + requestParams, + c.Writer.Status(), + responseData, + processCount, + errorMessage, + processingTime, + ) + if err != nil { + utils.Error("记录API访问日志失败: %v", err) + } + }() +} + // AddBatchResources godoc // @Summary 批量添加资源 // @Description 通过公开API批量添加多个资源到待处理列表 @@ -83,12 +131,18 @@ func (h *PublicAPIHandler) filterForbiddenWords(resources []entity.Resource) ([] // @Failure 500 {object} map[string]interface{} "服务器内部错误" // @Router /api/public/resources/batch-add [post] func (h *PublicAPIHandler) AddBatchResources(c *gin.Context) { + startTime := time.Now() + var req dto.BatchReadyResourceRequest if err := c.ShouldBindJSON(&req); err != nil { + h.logAPIAccess(c, startTime, 0, nil, "请求参数错误: "+err.Error()) ErrorResponse(c, "请求参数错误: "+err.Error(), 400) return } + // 存储请求体用于日志记录 + c.Set("request_body", req) + if len(req.Resources) == 0 { ErrorResponse(c, "资源列表不能为空", 400) return @@ -125,6 +179,7 @@ func (h *PublicAPIHandler) AddBatchResources(c *gin.Context) { // 生成 key(每组同一个 key) key, err := repoManager.ReadyResourceRepository.GenerateUniqueKey() if err != nil { + h.logAPIAccess(c, startTime, len(createdResources), nil, "生成资源组标识失败: "+err.Error()) ErrorResponse(c, "生成资源组标识失败: "+err.Error(), 500) return } @@ -156,10 +211,12 @@ func (h *PublicAPIHandler) AddBatchResources(c *gin.Context) { } } - SuccessResponse(c, gin.H{ + responseData := gin.H{ "created_count": len(createdResources), "created_ids": createdResources, - }) + } + h.logAPIAccess(c, startTime, len(createdResources), responseData, "") + SuccessResponse(c, responseData) } // SearchResources godoc @@ -179,6 +236,8 @@ func (h *PublicAPIHandler) AddBatchResources(c *gin.Context) { // @Failure 500 {object} map[string]interface{} "服务器内部错误" // @Router /api/public/resources/search [get] func (h *PublicAPIHandler) SearchResources(c *gin.Context) { + startTime := time.Now() + // 获取查询参数 keyword := c.Query("keyword") tag := c.Query("tag") @@ -276,6 +335,7 @@ func (h *PublicAPIHandler) SearchResources(c *gin.Context) { // 执行数据库搜索 resources, total, err = repoManager.ResourceRepository.SearchWithFilters(params) if err != nil { + h.logAPIAccess(c, startTime, 0, nil, "搜索失败: "+err.Error()) ErrorResponse(c, "搜索失败: "+err.Error(), 500) return } @@ -320,6 +380,7 @@ func (h *PublicAPIHandler) SearchResources(c *gin.Context) { "limit": pageSize, } + h.logAPIAccess(c, startTime, len(resourceResponses), responseData, "") SuccessResponse(c, responseData) } @@ -337,6 +398,8 @@ func (h *PublicAPIHandler) SearchResources(c *gin.Context) { // @Failure 500 {object} map[string]interface{} "服务器内部错误" // @Router /api/public/hot-dramas [get] func (h *PublicAPIHandler) GetHotDramas(c *gin.Context) { + startTime := time.Now() + pageStr := c.DefaultQuery("page", "1") pageSizeStr := c.DefaultQuery("page_size", "20") @@ -353,6 +416,7 @@ func (h *PublicAPIHandler) GetHotDramas(c *gin.Context) { // 获取热门剧 hotDramas, total, err := repoManager.HotDramaRepository.FindAll(page, pageSize) if err != nil { + h.logAPIAccess(c, startTime, 0, nil, "获取热门剧失败: "+err.Error()) ErrorResponse(c, "获取热门剧失败: "+err.Error(), 500) return } @@ -376,10 +440,12 @@ func (h *PublicAPIHandler) GetHotDramas(c *gin.Context) { }) } - SuccessResponse(c, gin.H{ + responseData := gin.H{ "hot_dramas": hotDramaResponses, "total": total, "page": page, "page_size": pageSize, - }) + } + h.logAPIAccess(c, startTime, len(hotDramaResponses), responseData, "") + SuccessResponse(c, responseData) } diff --git a/main.go b/main.go index 9485f23..12e962f 100644 --- a/main.go +++ b/main.go @@ -272,6 +272,12 @@ func main() { api.POST("/search-stats/record", handlers.RecordSearch) api.GET("/search-stats/summary", handlers.GetSearchStatsSummary) + // API访问日志路由 + api.GET("/api-access-logs", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetAPIAccessLogs) + api.GET("/api-access-logs/summary", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetAPIAccessLogSummary) + api.GET("/api-access-logs/stats", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetAPIAccessLogStats) + api.DELETE("/api-access-logs", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.ClearAPIAccessLogs) + // 系统配置路由 api.GET("/system/config", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetSystemConfig) api.POST("/system/config", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.UpdateSystemConfig) diff --git a/web/composables/useApi.ts b/web/composables/useApi.ts index e2f504d..3bdd2ba 100644 --- a/web/composables/useApi.ts +++ b/web/composables/useApi.ts @@ -326,4 +326,18 @@ export const useMeilisearchApi = () => { getSyncProgress, debugGetAllDocuments } +} + +// API访问日志管理API +export const useApiAccessLogApi = () => { + const getApiAccessLogs = (params?: any) => useApiFetch('/api/api-access-logs', { params }).then(parseApiResponse) + const getApiAccessLogSummary = () => useApiFetch('/api/api-access-logs/summary').then(parseApiResponse) + const getApiAccessLogStats = () => useApiFetch('/api/api-access-logs/stats').then(parseApiResponse) + const clearApiAccessLogs = (days: number) => useApiFetch('/api/api-access-logs', { method: 'DELETE', body: { days } }).then(parseApiResponse) + return { + getApiAccessLogs, + getApiAccessLogSummary, + getApiAccessLogStats, + clearApiAccessLogs + } } \ No newline at end of file diff --git a/web/layouts/admin.vue b/web/layouts/admin.vue index 01e6a9d..13035a5 100644 --- a/web/layouts/admin.vue +++ b/web/layouts/admin.vue @@ -398,13 +398,13 @@ const userMenuItems = computed(() => [ { to: '/admin/accounts', icon: 'fas fa-user-shield', - label: '账号管理', + label: '平台账号', type: 'link' }, { - to: '/admin/system-config', - icon: 'fas fa-cog', - label: '系统配置', + to: '/admin/api-access-logs', + icon: 'fas fa-history', + label: 'API访问日志', type: 'link' }, { diff --git a/web/pages/admin/api-access-logs.vue b/web/pages/admin/api-access-logs.vue new file mode 100644 index 0000000..ca7b1a3 --- /dev/null +++ b/web/pages/admin/api-access-logs.vue @@ -0,0 +1,387 @@ + + + + + \ No newline at end of file