From 786920c7e32872d4375b1249f8eb7a18c03d822a Mon Sep 17 00:00:00 2001 From: npc0-hue Date: Thu, 12 Mar 2026 11:42:02 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=A7=84=E8=8C=83=E5=8C=96=E5=A4=84?= =?UTF-8?q?=E7=90=86=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/AGENTS.md | 9 ++ admin/server/api/v1/gaia/model_provider.go | 94 ++++++------------- .../model/gaia/request/model_provider.go | 6 ++ admin/server/model/gaia/response/dashboard.go | 24 +++++ admin/server/service/gaia/dashboard.go | 53 ++++------- admin/server/service/gaia/model_provider.go | 22 +++++ 6 files changed, 112 insertions(+), 96 deletions(-) create mode 100644 admin/AGENTS.md diff --git a/admin/AGENTS.md b/admin/AGENTS.md new file mode 100644 index 000000000..5f0e6ed7f --- /dev/null +++ b/admin/AGENTS.md @@ -0,0 +1,9 @@ +# Admin (Go Backend) Agent Guide + +## Rules (must follow) + +### 禁止匿名 struct + +- **禁止在代码中出现匿名 struct**。不得使用 `var x []struct { ... }`、`var x struct { ... }` 或字面量 `struct { A int }{1}` 等匿名结构体。 +- 所有用于 GORM 查询扫描、缓存结构、API 请求/响应的结构体必须定义为**具名类型**,放在合适的 model 包(如 `model/gaia/request`、`model/gaia/response`)或当前包顶部,便于复用和规范约束。 +- 示例:用 `[]response.AppQuotaRankingRow` 替代 `[]struct { AppID string; TotalCost float64; ... }`;用 `response.AppQuotaRankingCache` 替代 `struct { List ...; Total int64 }`。 diff --git a/admin/server/api/v1/gaia/model_provider.go b/admin/server/api/v1/gaia/model_provider.go index 05d1cb4e1..dd6c6a4f3 100644 --- a/admin/server/api/v1/gaia/model_provider.go +++ b/admin/server/api/v1/gaia/model_provider.go @@ -2,13 +2,13 @@ package gaia import ( "encoding/json" - "fmt" "io" "net/http" "strings" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + gaiaReq "github.com/flipped-aurora/gin-vue-admin/server/model/gaia/request" "github.com/flipped-aurora/gin-vue-admin/server/service" "github.com/flipped-aurora/gin-vue-admin/server/utils" "github.com/gin-gonic/gin" @@ -31,7 +31,7 @@ func (m *ModelProviderApi) GetProviderList(c *gin.Context) { list, err := modelProviderService.GetProviderList() if err != nil { global.GVA_LOG.Error("获取提供商配置列表失败", zap.Error(err)) - response.FailWithMessage("获取失败: "+err.Error(), c) + response.FailWithMessage("获取失败:"+err.Error(), c) return } response.OkWithData(list, c) @@ -54,20 +54,20 @@ func (m *ModelProviderApi) UpdateProviderConfig(c *gin.Context) { } if err := c.ShouldBindJSON(&req); err != nil { - response.FailWithMessage("参数错误: "+err.Error(), c) + response.FailWithMessage("参数错误:"+err.Error(), c) return } if err := modelProviderService.UpdateProviderConfig(req.ProviderName, req.Enabled, req.Models); err != nil { global.GVA_LOG.Error("更新提供商配置失败", zap.String("provider", req.ProviderName), zap.Error(err)) - response.FailWithMessage("更新失败: "+err.Error(), c) + response.FailWithMessage("更新失败:"+err.Error(), c) return } response.OkWithMessage("更新成功", c) } -// GetModels 获取开启的模型列表(OpenAI格式) +// GetModels 获取开启的模型列表(OpenAI 格式,供第三方兼容调用;成功时返回裸 JSON,错误时与项目统一使用 response)。 // @Tags ModelProvider // @Summary 获取开启的模型列表 // @Security ApiKeyAuth @@ -79,14 +79,9 @@ func (m *ModelProviderApi) GetModels(c *gin.Context) { models, err := modelProviderService.GetEnabledModels() if err != nil { global.GVA_LOG.Error("获取模型列表失败", zap.Error(err)) - c.JSON(http.StatusInternalServerError, gin.H{ - "error": gin.H{ - "message": "获取模型列表失败: " + err.Error(), - }, - }) + response.FailWithMessage("获取失败:"+err.Error(), c) return } - c.JSON(http.StatusOK, models) } @@ -161,17 +156,15 @@ func (m *ModelProviderApi) Proxy(c *gin.Context) { func (m *ModelProviderApi) GetAvailableModels(c *gin.Context) { providerName := c.Query("provider_name") if providerName == "" { - response.FailWithMessage("参数错误: provider_name不能为空", c) + response.FailWithMessage("参数错误:provider_name不能为空", c) return } - models, err := modelProviderService.GetAvailableModelsFromDify(providerName) if err != nil { global.GVA_LOG.Error("获取可用模型失败", zap.String("provider", providerName), zap.Error(err)) - response.FailWithMessage("获取失败: "+err.Error(), c) + response.FailWithMessage("获取失败:"+err.Error(), c) return } - response.OkWithData(models, c) } @@ -187,14 +180,13 @@ func (m *ModelProviderApi) GetAvailableModels(c *gin.Context) { func (m *ModelProviderApi) TestProviderCredentials(c *gin.Context) { providerName := c.Query("provider_name") if providerName == "" { - response.FailWithMessage("参数错误: provider_name不能为空", c) + response.FailWithMessage("参数错误:provider_name不能为空", c) return } - creds, err := modelProviderService.GetDifyProviderCredentials(providerName) if err != nil { global.GVA_LOG.Error("获取提供商凭证失败", zap.String("provider", providerName), zap.Error(err)) - response.FailWithMessage("获取凭证失败: "+err.Error(), c) + response.FailWithMessage("获取凭证失败:"+err.Error(), c) return } @@ -215,7 +207,7 @@ func (m *ModelProviderApi) TestProviderCredentials(c *gin.Context) { response.OkWithData(result, c) } -// GetProxyLogs 获取代理日志 +// GetProxyLogs 获取代理日志(分页) // @Tags ModelProvider // @Summary 获取代理日志 // @Security ApiKeyAuth @@ -223,54 +215,30 @@ func (m *ModelProviderApi) TestProviderCredentials(c *gin.Context) { // @Produce application/json // @Param page query int false "页码" // @Param page_size query int false "每页数量" -// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "获取成功" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" // @Router /gaia/model-provider/logs [get] func (m *ModelProviderApi) GetProxyLogs(c *gin.Context) { - page := c.DefaultQuery("page", "1") - pageSize := c.DefaultQuery("page_size", "20") - - var pageInt, pageSizeInt int - if _, err := fmt.Sscanf(page, "%d", &pageInt); err != nil { - pageInt = 1 - } - if _, err := fmt.Sscanf(pageSize, "%d", &pageSizeInt); err != nil { - pageSizeInt = 20 - } - - if pageInt < 1 { - pageInt = 1 - } - if pageSizeInt < 1 || pageSizeInt > 100 { - pageSizeInt = 20 - } - - var logs []map[string]interface{} - var total int64 - - db := global.GVA_DB.Table("model_proxy_log") - - // 获取总数 - if err := db.Count(&total).Error; err != nil { - global.GVA_LOG.Error("获取日志总数失败", zap.Error(err)) - response.FailWithMessage("获取失败: "+err.Error(), c) + var req gaiaReq.GetProxyLogsReq + if err := c.ShouldBindQuery(&req); err != nil { + response.FailWithMessage("参数错误:"+err.Error(), c) return } - - // 分页查询 - offset := (pageInt - 1) * pageSizeInt - if err := db.Order("created_at DESC").Limit(pageSizeInt).Offset( - offset).Find(&logs).Error; err != nil { - global.GVA_LOG.Error("获取日志列表失败", zap.Error(err)) - response.FailWithMessage("获取失败: "+err.Error(), c) + if req.Page < 1 { + req.Page = 1 + } + if req.PageSize < 1 || req.PageSize > 100 { + req.PageSize = 20 + } + list, total, err := modelProviderService.GetProxyLogs(req) + if err != nil { + global.GVA_LOG.Error("获取代理日志失败", zap.Error(err)) + response.FailWithMessage("获取失败:"+err.Error(), c) return } - - result := map[string]interface{}{ - "list": logs, - "total": total, - "page": pageInt, - "page_size": pageSizeInt, - } - - response.OkWithData(result, c) + response.OkWithDetailed(response.PageResult{ + List: list, + Total: total, + Page: req.Page, + PageSize: req.PageSize, + }, "获取成功", c) } diff --git a/admin/server/model/gaia/request/model_provider.go b/admin/server/model/gaia/request/model_provider.go index 55b8eee94..0cefa3814 100644 --- a/admin/server/model/gaia/request/model_provider.go +++ b/admin/server/model/gaia/request/model_provider.go @@ -1,5 +1,11 @@ package request +// GetProxyLogsReq 代理日志分页请求 +type GetProxyLogsReq struct { + Page int `form:"page"` // 页码,从 1 开始 + PageSize int `form:"page_size"` // 每页条数,最大 100 +} + // ChatRequest 聊天请求(OpenAI 兼容) type ChatRequest struct { Model string `json:"model"` diff --git a/admin/server/model/gaia/response/dashboard.go b/admin/server/model/gaia/response/dashboard.go index 6023740f2..db846ed6d 100644 --- a/admin/server/model/gaia/response/dashboard.go +++ b/admin/server/model/gaia/response/dashboard.go @@ -1,5 +1,29 @@ package response +// AppQuotaRankingRow 应用配额排名查询单行(仅用于 service 层 GORM 查询扫描) +type AppQuotaRankingRow struct { + AppID string `gorm:"column:app_id"` + TotalCost float64 `gorm:"column:total_cost"` + MessageCost float64 `gorm:"column:message_cost"` + WorkflowCost float64 `gorm:"column:workflow_cost"` + RecordNum float64 `gorm:"column:record_num"` +} + +// AppQuotaRankingCache 应用配额排名缓存结构(List + Total) +type AppQuotaRankingCache struct { + List []GetAppQuotaRankingDataRes + Total int64 +} + +// AiImageQuotaRankingRow AI 图片使用量排名查询单行(仅用于 service 层 GORM 查询扫描) +type AiImageQuotaRankingRow struct { + Address string `gorm:"column:address"` + Path string `gorm:"column:path"` + TotalCost float64 `gorm:"column:total_cost"` + RecordNum int `gorm:"column:record_num"` + Model string `gorm:"column:model"` +} + // GetAccountQuotaRankingDataRes 获取账户配额排名数据的响应结构 type GetAccountQuotaRankingDataRes struct { Ranking int `json:"ranking"` // 排名 diff --git a/admin/server/service/gaia/dashboard.go b/admin/server/service/gaia/dashboard.go index c28bca3ef..7ea79f5b3 100644 --- a/admin/server/service/gaia/dashboard.go +++ b/admin/server/service/gaia/dashboard.go @@ -19,7 +19,8 @@ import ( type DashboardService struct{} // GetAccountQuotaRankingData 分页获取【账号】额度排名列表 -func (dashboardService *DashboardService) GetAccountQuotaRankingData(info gaiaReq.GetAccountQuotaRankingDataReq) (list []response.GetAccountQuotaRankingDataRes, total int64, err error) { +func (s *DashboardService) GetAccountQuotaRankingData(info gaiaReq.GetAccountQuotaRankingDataReq) ( + list []response.GetAccountQuotaRankingDataRes, total int64, err error) { limit := info.PageSize offset := info.PageSize * (info.Page - 1) @@ -79,15 +80,13 @@ func (dashboardService *DashboardService) GetAccountQuotaRankingData(info gaiaRe } // GetAppQuotaRankingData 分页获取【应用】配额排名数据 -func (dashboardService *DashboardService) GetAppQuotaRankingData(info gaiaReq.GetAppQuotaRankingDataReq) (list []response.GetAppQuotaRankingDataRes, total int64, err error) { +func (s *DashboardService) GetAppQuotaRankingData(info gaiaReq.GetAppQuotaRankingDataReq) ( + list []response.GetAppQuotaRankingDataRes, total int64, err error) { cacheKey := fmt.Sprintf("app_token_quota_ranking:%d:%d", info.Page, info.PageSize) - var cachedResult struct { - List []response.GetAppQuotaRankingDataRes - Total int64 - } + var cachedResult response.AppQuotaRankingCache - if found, err := dashboardService.getCachedResult(cacheKey, &cachedResult); err == nil && found { + if found, err := s.getCachedResult(cacheKey, &cachedResult); err == nil && found { return cachedResult.List, cachedResult.Total, nil } @@ -140,13 +139,7 @@ func (dashboardService *DashboardService) GetAppQuotaRankingData(info gaiaReq.Ge } // 执行查询 - var results []struct { - AppID string `gorm:"column:app_id"` - TotalCost float64 `gorm:"column:total_cost"` - MessageCost float64 `gorm:"column:message_cost"` - WorkflowCost float64 `gorm:"column:workflow_cost"` - RecordNum float64 `gorm:"column:record_num"` - } + var results []response.AppQuotaRankingRow err = query.Find(&results).Error if err != nil { @@ -269,12 +262,8 @@ func (dashboardService *DashboardService) GetAppQuotaRankingData(info gaiaReq.Ge } // 在返回结果之前,缓存结果 - result := struct { - List []response.GetAppQuotaRankingDataRes - Total int64 - }{list, total} - - if err := dashboardService.cacheResult(cacheKey, result, 24*time.Hour); err != nil { + cachePayload := response.AppQuotaRankingCache{List: list, Total: total} + if err := s.cacheResult(cacheKey, cachePayload, 24*time.Hour); err != nil { global.GVA_LOG.Error("Failed to cache result", zap.Error(err)) } @@ -282,7 +271,8 @@ func (dashboardService *DashboardService) GetAppQuotaRankingData(info gaiaReq.Ge } // GetAppTokenQuotaRankingData 分页获取【应用密钥】配额排名数据列表 -func (dashboardService *DashboardService) GetAppTokenQuotaRankingData(info gaiaReq.GetAppTokenQuotaRankingDataReq) (list []response.GetAppTokenQuotaRankingDataRes, total int64, err error) { +func (s *DashboardService) GetAppTokenQuotaRankingData(info gaiaReq.GetAppTokenQuotaRankingDataReq) ( + list []response.GetAppTokenQuotaRankingDataRes, total int64, err error) { limit := info.PageSize offset := info.PageSize * (info.Page - 1) @@ -365,9 +355,11 @@ func (dashboardService *DashboardService) GetAppTokenQuotaRankingData(info gaiaR } // GetAppTokenDailyQuotaData 获取每天密钥花费数据列表 -func (dashboardService *DashboardService) GetAppTokenDailyQuotaData(info gaiaReq.GetAppTokenDailyQuotaDataReq) (list []response.GetAppTokenDailyQuotaDataRes, err error) { +func (s *DashboardService) GetAppTokenDailyQuotaData(info gaiaReq.GetAppTokenDailyQuotaDataReq) ( + list []response.GetAppTokenDailyQuotaDataRes, err error) { - db := global.GVA_DB.Select("DATE(stat_at) as stat_at, SUM(day_used_quota) as day_used_quota").Model(&gaia.ApiTokenMoneyDailyStatExtend{}).Order("stat_at desc").Group("DATE(stat_at)") + db := global.GVA_DB.Select("DATE(stat_at) as stat_at, SUM(day_used_quota) as day_used_quota").Model( + &gaia.ApiTokenMoneyDailyStatExtend{}).Order("stat_at desc").Group("DATE(stat_at)") var apiTokenMoneyDailyStatExtends []gaia.ApiTokenMoneyDailyStatExtend if info.AppId != "" { @@ -397,7 +389,8 @@ func (dashboardService *DashboardService) GetAppTokenDailyQuotaData(info gaiaReq } // GetAiImageQuotaRankingData 获取【AI图片】使用量排名数据列表 -func (dashboardService *DashboardService) GetAiImageQuotaRankingData(info gaiaReq.GetAiImageQuotaRankingDataReq) (list []response.GetAiImageQuotaRankingRes, err error) { +func (s *DashboardService) GetAiImageQuotaRankingData(info gaiaReq.GetAiImageQuotaRankingDataReq) ( + list []response.GetAiImageQuotaRankingRes, err error) { limit := info.PageSize offset := info.PageSize * (info.Page - 1) @@ -427,13 +420,7 @@ func (dashboardService *DashboardService) GetAiImageQuotaRankingData(info gaiaRe db = db.Limit(limit).Offset(offset) } - var results []struct { - Address string - Path string - TotalCost float64 - RecordNum int - Model string - } + var results []response.AiImageQuotaRankingRow err = db.Find(&results).Error if err != nil { @@ -456,7 +443,7 @@ func (dashboardService *DashboardService) GetAiImageQuotaRankingData(info gaiaRe return list, nil } -func (dashboardService *DashboardService) cacheResult(key string, data interface{}, expiration time.Duration) error { +func (s *DashboardService) cacheResult(key string, data interface{}, expiration time.Duration) error { jsonData, err := json.Marshal(data) if err != nil { @@ -465,7 +452,7 @@ func (dashboardService *DashboardService) cacheResult(key string, data interface return global.GVA_REDIS.Set(context.Background(), key, jsonData, expiration).Err() } -func (dashboardService *DashboardService) getCachedResult(key string, result interface{}) (bool, error) { +func (s *DashboardService) getCachedResult(key string, result interface{}) (bool, error) { data, err := global.GVA_REDIS.Get(context.Background(), key).Bytes() if err != nil { if errors.Is(err, redis.Nil) { diff --git a/admin/server/service/gaia/model_provider.go b/admin/server/service/gaia/model_provider.go index 89d9cbdf5..ff80d35d0 100644 --- a/admin/server/service/gaia/model_provider.go +++ b/admin/server/service/gaia/model_provider.go @@ -1346,6 +1346,28 @@ func (s *ModelProviderService) ProxyRequest( return err } +// GetProxyLogs 分页查询代理日志(model_proxy_log 表)。 +func (s *ModelProviderService) GetProxyLogs(info gaiaRequest.GetProxyLogsReq) (list []map[string]interface{}, total int64, err error) { + page, pageSize := info.Page, info.PageSize + if page < 1 { + page = 1 + } + if pageSize < 1 || pageSize > 100 { + pageSize = 20 + } + db := global.GVA_DB.Table("model_proxy_log") + if err = db.Count(&total).Error; err != nil { + err = fmt.Errorf("查询日志总数失败:%w", err) + return + } + offset := (page - 1) * pageSize + if err = db.Order("created_at DESC").Limit(pageSize).Offset(offset).Find(&list).Error; err != nil { + err = fmt.Errorf("查询日志列表失败:%w", err) + return + } + return list, total, nil +} + // isProviderEnabled 检查该提供商是否已启用(未校验具体模型列表,用于通用代理)。 func (s *ModelProviderService) isProviderEnabled(providerName string) bool { var config gaia.ModelProviderConfig