feat(ops): 运维监控新增 OpenAI Token 请求统计表
- 新增管理端接口 /api/v1/admin/ops/dashboard/openai-token-stats,按模型聚合统计 gpt% 请求 - 支持 time_range=30m|1h|1d|15d|30d(默认 30d),支持 platform/group_id 过滤 - 支持分页(page/page_size)或 TopN(top_n)互斥查询 - 前端运维监控页新增统计表卡片,包含空态/错误态与分页/TopN 交互 - 补齐后端与前端测试
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -218,6 +219,115 @@ func (h *OpsHandler) GetDashboardErrorDistribution(c *gin.Context) {
|
||||
response.Success(c, data)
|
||||
}
|
||||
|
||||
// GetDashboardOpenAITokenStats returns OpenAI token efficiency stats grouped by model.
|
||||
// GET /api/v1/admin/ops/dashboard/openai-token-stats
|
||||
func (h *OpsHandler) GetDashboardOpenAITokenStats(c *gin.Context) {
|
||||
if h.opsService == nil {
|
||||
response.Error(c, http.StatusServiceUnavailable, "Ops service not available")
|
||||
return
|
||||
}
|
||||
if err := h.opsService.RequireMonitoringEnabled(c.Request.Context()); err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
filter, err := parseOpsOpenAITokenStatsFilter(c)
|
||||
if err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
data, err := h.opsService.GetOpenAITokenStats(c.Request.Context(), filter)
|
||||
if err != nil {
|
||||
response.ErrorFrom(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, data)
|
||||
}
|
||||
|
||||
func parseOpsOpenAITokenStatsFilter(c *gin.Context) (*service.OpsOpenAITokenStatsFilter, error) {
|
||||
if c == nil {
|
||||
return nil, fmt.Errorf("invalid request")
|
||||
}
|
||||
|
||||
timeRange := strings.TrimSpace(c.Query("time_range"))
|
||||
if timeRange == "" {
|
||||
timeRange = "30d"
|
||||
}
|
||||
dur, ok := parseOpsOpenAITokenStatsDuration(timeRange)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid time_range")
|
||||
}
|
||||
end := time.Now().UTC()
|
||||
start := end.Add(-dur)
|
||||
|
||||
filter := &service.OpsOpenAITokenStatsFilter{
|
||||
TimeRange: timeRange,
|
||||
StartTime: start,
|
||||
EndTime: end,
|
||||
Platform: strings.TrimSpace(c.Query("platform")),
|
||||
}
|
||||
|
||||
if v := strings.TrimSpace(c.Query("group_id")); v != "" {
|
||||
id, err := strconv.ParseInt(v, 10, 64)
|
||||
if err != nil || id <= 0 {
|
||||
return nil, fmt.Errorf("invalid group_id")
|
||||
}
|
||||
filter.GroupID = &id
|
||||
}
|
||||
|
||||
topNRaw := strings.TrimSpace(c.Query("top_n"))
|
||||
pageRaw := strings.TrimSpace(c.Query("page"))
|
||||
pageSizeRaw := strings.TrimSpace(c.Query("page_size"))
|
||||
if topNRaw != "" && (pageRaw != "" || pageSizeRaw != "") {
|
||||
return nil, fmt.Errorf("invalid query: top_n cannot be used with page/page_size")
|
||||
}
|
||||
|
||||
if topNRaw != "" {
|
||||
topN, err := strconv.Atoi(topNRaw)
|
||||
if err != nil || topN < 1 || topN > 100 {
|
||||
return nil, fmt.Errorf("invalid top_n")
|
||||
}
|
||||
filter.TopN = topN
|
||||
return filter, nil
|
||||
}
|
||||
|
||||
filter.Page = 1
|
||||
filter.PageSize = 20
|
||||
if pageRaw != "" {
|
||||
page, err := strconv.Atoi(pageRaw)
|
||||
if err != nil || page < 1 {
|
||||
return nil, fmt.Errorf("invalid page")
|
||||
}
|
||||
filter.Page = page
|
||||
}
|
||||
if pageSizeRaw != "" {
|
||||
pageSize, err := strconv.Atoi(pageSizeRaw)
|
||||
if err != nil || pageSize < 1 || pageSize > 100 {
|
||||
return nil, fmt.Errorf("invalid page_size")
|
||||
}
|
||||
filter.PageSize = pageSize
|
||||
}
|
||||
return filter, nil
|
||||
}
|
||||
|
||||
func parseOpsOpenAITokenStatsDuration(v string) (time.Duration, bool) {
|
||||
switch strings.TrimSpace(v) {
|
||||
case "30m":
|
||||
return 30 * time.Minute, true
|
||||
case "1h":
|
||||
return time.Hour, true
|
||||
case "1d":
|
||||
return 24 * time.Hour, true
|
||||
case "15d":
|
||||
return 15 * 24 * time.Hour, true
|
||||
case "30d":
|
||||
return 30 * 24 * time.Hour, true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func pickThroughputBucketSeconds(window time.Duration) int {
|
||||
// Keep buckets predictable and avoid huge responses.
|
||||
switch {
|
||||
|
||||
Reference in New Issue
Block a user