diff --git a/backend/api/handlers.go b/backend/api/handlers.go
index 8682dc5..d661583 100644
--- a/backend/api/handlers.go
+++ b/backend/api/handlers.go
@@ -140,6 +140,7 @@ func createRequestLog(apiKeyID uint, virtualModelName string, backendModel *mode
APIKeyID: apiKeyID,
VirtualModelName: virtualModelName,
BackendModelName: backendModel.Name,
+ ProviderName: backendModel.Provider.Name,
RequestTimestamp: requestTimestamp,
ResponseTimestamp: responseTimestamp,
RequestTokens: requestTokenCount,
@@ -172,3 +173,155 @@ func copyResponseHeaders(c *gin.Context, resp *http.Response) {
}
}
}
+
+// GetRequestLogsHandler 获取请求日志列表
+func (h *APIHandler) GetRequestLogsHandler(c *gin.Context) {
+ // 获取分页参数
+ page := 1
+ pageSize := 20
+ if pageParam := c.Query("page"); pageParam != "" {
+ fmt.Sscanf(pageParam, "%d", &page)
+ }
+ if pageSizeParam := c.Query("page_size"); pageSizeParam != "" {
+ fmt.Sscanf(pageSizeParam, "%d", &pageSize)
+ }
+ if page < 1 {
+ page = 1
+ }
+ if pageSize < 1 || pageSize > 100 {
+ pageSize = 20
+ }
+
+ // 构建查询
+ query := h.DB.Model(&models.RequestLog{})
+
+ // 按时间范围过滤
+ if startTime := c.Query("start_time"); startTime != "" {
+ if t, err := time.Parse(time.RFC3339, startTime); err == nil {
+ query = query.Where("request_timestamp >= ?", t)
+ }
+ }
+ if endTime := c.Query("end_time"); endTime != "" {
+ if t, err := time.Parse(time.RFC3339, endTime); err == nil {
+ query = query.Where("request_timestamp <= ?", t)
+ }
+ }
+
+ // 按虚拟模型名称过滤
+ if virtualModel := c.Query("virtual_model"); virtualModel != "" {
+ query = query.Where("virtual_model_name = ?", virtualModel)
+ }
+
+ // 按后端模型名称过滤
+ if backendModel := c.Query("backend_model"); backendModel != "" {
+ query = query.Where("backend_model_name = ?", backendModel)
+ }
+
+ // 获取总数
+ var total int64
+ if err := query.Count(&total).Error; err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to count logs"})
+ return
+ }
+
+ // 获取日志列表
+ var logs []models.RequestLog
+ offset := (page - 1) * pageSize
+ if err := query.Order("request_timestamp DESC").Limit(pageSize).Offset(offset).Find(&logs).Error; err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch logs"})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "logs": logs,
+ "total": total,
+ "page": page,
+ "page_size": pageSize,
+ "total_pages": (total + int64(pageSize) - 1) / int64(pageSize),
+ })
+}
+
+// GetRequestLogStatsHandler 获取请求日志统计信息
+func (h *APIHandler) GetRequestLogStatsHandler(c *gin.Context) {
+ // 获取时间范围参数
+ var startTime, endTime time.Time
+ var err error
+
+ if startTimeParam := c.Query("start_time"); startTimeParam != "" {
+ startTime, err = time.Parse(time.RFC3339, startTimeParam)
+ if err != nil {
+ // 默认最近7天
+ startTime = time.Now().AddDate(0, 0, -7)
+ }
+ } else {
+ startTime = time.Now().AddDate(0, 0, -7)
+ }
+
+ if endTimeParam := c.Query("end_time"); endTimeParam != "" {
+ endTime, err = time.Parse(time.RFC3339, endTimeParam)
+ if err != nil {
+ endTime = time.Now()
+ }
+ } else {
+ endTime = time.Now()
+ }
+
+ // 统计总请求数
+ var totalRequests int64
+ h.DB.Model(&models.RequestLog{}).
+ Where("request_timestamp BETWEEN ? AND ?", startTime, endTime).
+ Count(&totalRequests)
+
+ // 统计总成本
+ var totalCost float64
+ h.DB.Model(&models.RequestLog{}).
+ Where("request_timestamp BETWEEN ? AND ?", startTime, endTime).
+ Select("COALESCE(SUM(cost), 0)").
+ Scan(&totalCost)
+
+ // 统计总token数
+ var totalTokens struct {
+ RequestTokens int64
+ ResponseTokens int64
+ }
+ h.DB.Model(&models.RequestLog{}).
+ Where("request_timestamp BETWEEN ? AND ?", startTime, endTime).
+ Select("COALESCE(SUM(request_tokens), 0) as request_tokens, COALESCE(SUM(response_tokens), 0) as response_tokens").
+ Scan(&totalTokens)
+
+ // 按虚拟模型统计
+ type ModelStats struct {
+ ModelName string `json:"model_name"`
+ Count int64 `json:"count"`
+ TotalCost float64 `json:"total_cost"`
+ }
+ var virtualModelStats []ModelStats
+ h.DB.Model(&models.RequestLog{}).
+ Where("request_timestamp BETWEEN ? AND ?", startTime, endTime).
+ Select("virtual_model_name as model_name, COUNT(*) as count, COALESCE(SUM(cost), 0) as total_cost").
+ Group("virtual_model_name").
+ Order("count DESC").
+ Limit(10).
+ Scan(&virtualModelStats)
+
+ // 按后端模型统计
+ var backendModelStats []ModelStats
+ h.DB.Model(&models.RequestLog{}).
+ Where("request_timestamp BETWEEN ? AND ?", startTime, endTime).
+ Select("backend_model_name as model_name, COUNT(*) as count, COALESCE(SUM(cost), 0) as total_cost").
+ Group("backend_model_name").
+ Order("count DESC").
+ Limit(10).
+ Scan(&backendModelStats)
+
+ c.JSON(http.StatusOK, gin.H{
+ "start_time": startTime,
+ "end_time": endTime,
+ "total_requests": totalRequests,
+ "total_cost": totalCost,
+ "total_request_tokens": totalTokens.RequestTokens,
+ "total_response_tokens": totalTokens.ResponseTokens,
+ "virtual_model_stats": virtualModelStats,
+ "backend_model_stats": backendModelStats,
+ })
+}
diff --git a/backend/internal/models/schema.go b/backend/internal/models/schema.go
index 012cf3c..0a7064e 100644
--- a/backend/internal/models/schema.go
+++ b/backend/internal/models/schema.go
@@ -54,6 +54,7 @@ type BackendModel struct {
type RequestLog struct {
gorm.Model
APIKeyID uint `gorm:"index"` // API密钥ID
+ ProviderName string `gorm:"index"` // 服务商名称
VirtualModelName string `gorm:"index"` // 虚拟模型名称
BackendModelName string `gorm:"index"` // 后端模型名称
RequestTimestamp time.Time `gorm:"index;not null"` // 请求时间戳
diff --git a/backend/main.go b/backend/main.go
index 696ab94..0859d56 100644
--- a/backend/main.go
+++ b/backend/main.go
@@ -72,6 +72,10 @@ func main() {
api_.GET("/virtual-models/:id", handler.GetVirtualModelHandler)
api_.PUT("/virtual-models/:id", handler.UpdateVirtualModelHandler)
api_.DELETE("/virtual-models/:id", handler.DeleteVirtualModelHandler)
+
+ // Request Logs
+ api_.GET("/logs", handler.GetRequestLogsHandler)
+ api_.GET("/logs/stats", handler.GetRequestLogStatsHandler)
}
// 启动HTTP服务器
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 732da88..cb3a957 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -2,17 +2,26 @@ import React from 'react';
import PageLayout from './components/layout/PageLayout';
import ProvidersPage from './features/providers';
import VirtualModelsPage from './features/virtual-models';
+import LogsPage from './features/logs';
import './App.css';
function App() {
- return (
-