Files
hr_data_analyzer/controllers/statistics.go
T
2026-05-04 16:20:46 +08:00

815 lines
26 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package controllers
import (
"errors"
"hr_receiver/config"
"hr_receiver/models"
"net/http"
"sort"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type StatisticsController struct {
DB *gorm.DB
}
func NewStatisticsController() *StatisticsController {
return &StatisticsController{DB: config.DB}
}
// --- 请求参数 ---
type analysisRecordListParams struct {
PageNum int `form:"pageNum,default=1"`
PageSize int `form:"pageSize,default=10"`
RegionID uint32 `form:"regionId"`
StartTime int64 `form:"startTime"`
EndTime int64 `form:"endTime"`
}
// --- 查询接口 ---
// @Summary 获取AI分析记录列表
// @Description 分页查询AI分析记录支持按区域和时间范围筛选
// @Tags 统计管理
// @Produce json
// @Param pageNum query int false "页码(默认1)"
// @Param pageSize query int false "每页数量(默认10,最大100)"
// @Param regionId query int false "区域ID"
// @Param startTime query int false "开始时间(毫秒时间戳)"
// @Param endTime query int false "结束时间(毫秒时间戳)"
// @Security BearerAuth
// @Success 200 {object} SwagAPIResponse "查询成功"
// @Router /admin/statistics/ai-analysis-records [get]
func (sc *StatisticsController) ListAIAnalysisRecords(c *gin.Context) {
var params analysisRecordListParams
if err := c.ShouldBindQuery(&params); err != nil {
writeError(c, http.StatusBadRequest, err.Error())
return
}
if params.PageNum < 1 {
params.PageNum = 1
}
if params.PageSize < 1 || params.PageSize > 100 {
params.PageSize = 10
}
offset := (params.PageNum - 1) * params.PageSize
query := sc.DB.Model(&models.AIAnalysisRecord{})
if params.RegionID > 0 {
query = query.Where("region_id = ?", params.RegionID)
}
if params.StartTime > 0 {
query = query.Where("upload_time >= ?", params.StartTime)
}
if params.EndTime > 0 {
query = query.Where("upload_time <= ?", params.EndTime)
}
var total int64
if err := query.Count(&total).Error; err != nil {
writeError(c, http.StatusInternalServerError, "failed to count records")
return
}
var records []models.AIAnalysisRecord
if err := query.Order("upload_time DESC").Offset(offset).Limit(params.PageSize).Find(&records).Error; err != nil {
writeError(c, http.StatusInternalServerError, "failed to query records")
return
}
writeSuccess(c, http.StatusOK, "query success", gin.H{
"list": records,
"pagination": gin.H{
"currentPage": params.PageNum,
"pageSize": params.PageSize,
"totalList": total,
"totalPage": int((total + int64(params.PageSize) - 1) / int64(params.PageSize)),
},
})
}
// --- 删除接口 ---
// @Summary 删除AI分析记录
// @Description 删除指定的AI分析记录
// @Tags 统计管理
// @Produce json
// @Param id path int true "记录ID"
// @Security BearerAuth
// @Success 200 {object} SwagAPIResponse "删除成功"
// @Failure 404 {object} SwagAPIResponse "记录不存在"
// @Router /admin/statistics/ai-analysis-records/{id} [delete]
func (sc *StatisticsController) DeleteAIAnalysisRecord(c *gin.Context) {
id := strings.TrimSpace(c.Param("id"))
if id == "" {
writeError(c, http.StatusBadRequest, "id is required")
return
}
var record models.AIAnalysisRecord
if err := sc.DB.First(&record, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
writeError(c, http.StatusNotFound, "record not found")
return
}
writeError(c, http.StatusInternalServerError, "failed to query record")
return
}
if err := sc.DB.Delete(&record).Error; err != nil {
writeError(c, http.StatusInternalServerError, "failed to delete record")
return
}
writeSuccess(c, http.StatusOK, "delete success", nil)
}
// --- 统计接口 ---
type regionStatisticsItem struct {
RegionID uint32 `json:"regionId"`
KindergartenName string `json:"kindergartenName"`
Count int64 `json:"count"`
TotalInputTokens int64 `json:"totalInputTokens"`
TotalOutputTokens int64 `json:"totalOutputTokens"`
TotalCacheHitTokens int64 `json:"totalCacheHitTokens"`
TotalCacheMissTokens int64 `json:"totalCacheMissTokens"`
TotalInputSizeBytes int64 `json:"totalInputSizeBytes"`
TotalOutputSizeBytes int64 `json:"totalOutputSizeBytes"`
TotalDurationMs int64 `json:"totalDurationMs"`
AvgDurationMs float64 `json:"avgDurationMs"`
AvgTotalCost float64 `json:"avgTotalCost"`
TotalOriginalFileSize int64 `json:"totalOriginalFileSize"`
TotalCompressedSize int64 `json:"totalCompressedSize"`
TotalCost float64 `json:"totalCost"`
TotalInputCost float64 `json:"totalInputCost"`
TotalOutputCost float64 `json:"totalOutputCost"`
TotalCacheHitCost float64 `json:"totalCacheHitCost"`
TotalCacheMissCost float64 `json:"totalCacheMissCost"`
AnalysisTypeCounts map[string]int64 `json:"analysisTypeCounts"`
SourceTypeCounts map[string]int64 `json:"sourceTypeCounts"`
FirstUsedAt *time.Time `json:"firstUsedAt"`
LastUsedAt *time.Time `json:"lastUsedAt"`
}
// @Summary AI分析区域统计
// @Description 按区域统计AI分析的使用情况包括调用次数、Token消耗、费用等
// @Tags 统计管理
// @Produce json
// @Param regionId query int false "区域ID"
// @Param startTime query int false "开始时间(毫秒时间戳)"
// @Param endTime query int false "结束时间(毫秒时间戳)"
// @Security BearerAuth
// @Success 200 {object} SwagAPIResponse "查询成功"
// @Router /admin/statistics/ai-analysis [get]
func (sc *StatisticsController) StatisticsByRegion(c *gin.Context) {
regionIDStr := c.Query("regionId")
startTimeStr := c.Query("startTime")
endTimeStr := c.Query("endTime")
query := sc.DB.Model(&models.AIAnalysisRecord{})
if regionIDStr != "" {
if regionID, err := strconv.ParseUint(regionIDStr, 10, 32); err == nil {
query = query.Where("region_id = ?", uint32(regionID))
}
}
if startTimeStr != "" {
if startTime, err := strconv.ParseInt(startTimeStr, 10, 64); err == nil {
query = query.Where("upload_time >= ?", startTime)
}
}
if endTimeStr != "" {
if endTime, err := strconv.ParseInt(endTimeStr, 10, 64); err == nil {
query = query.Where("upload_time <= ?", endTime)
}
}
type rawStats struct {
RegionID *uint32
Count int64
TotalInputTokens int64
TotalOutputTokens int64
TotalCacheHitTokens int64
TotalCacheMissTokens int64
TotalInputSizeBytes int64
TotalOutputSizeBytes int64
TotalDurationMs int64
TotalOriginalFileSize int64
TotalCompressedSize int64
TotalCost float64
TotalInputCost float64
TotalOutputCost float64
TotalCacheHitCost float64
TotalCacheMissCost float64
FirstUsedAt *int64
LastUsedAt *int64
}
var rawResults []rawStats
err := query.Select(`
region_id,
COUNT(*) as count,
COALESCE(SUM(input_tokens), 0) as total_input_tokens,
COALESCE(SUM(output_tokens), 0) as total_output_tokens,
COALESCE(SUM(cache_hit_tokens), 0) as total_cache_hit_tokens,
COALESCE(SUM(cache_miss_tokens), 0) as total_cache_miss_tokens,
COALESCE(SUM(input_size_bytes), 0) as total_input_size_bytes,
COALESCE(SUM(output_size_bytes), 0) as total_output_size_bytes,
COALESCE(SUM(duration_ms), 0) as total_duration_ms,
COALESCE(SUM(original_file_size), 0) as total_original_file_size,
COALESCE(SUM(compressed_content_size), 0) as total_compressed_size,
COALESCE(SUM(total_cost), 0) as total_cost,
COALESCE(SUM((cost_json::jsonb->>'cacheHitCost')::float8 + (cost_json::jsonb->>'cacheMissCost')::float8), 0) as total_input_cost,
COALESCE(SUM((cost_json::jsonb->>'outputCost')::float8), 0) as total_output_cost,
COALESCE(SUM((cost_json::jsonb->>'cacheHitCost')::float8), 0) as total_cache_hit_cost,
COALESCE(SUM((cost_json::jsonb->>'cacheMissCost')::float8), 0) as total_cache_miss_cost,
MIN(upload_time) as first_used_at,
MAX(upload_time) as last_used_at
`).Group("region_id").Scan(&rawResults).Error
if err != nil {
writeError(c, http.StatusInternalServerError, "failed to query statistics")
return
}
type analysisTypeCount struct {
RegionID *uint32
AnalysisType string
Count int64
}
var analysisTypeResults []analysisTypeCount
if err := query.Select("region_id, analysis_type, COUNT(*) as count").Group("region_id, analysis_type").Scan(&analysisTypeResults).Error; err != nil {
writeError(c, http.StatusInternalServerError, "failed to query analysis type statistics")
return
}
type sourceTypeCount struct {
RegionID *uint32
SourceType string
Count int64
}
var sourceTypeResults []sourceTypeCount
if err := query.Select("region_id, source_type, COUNT(*) as count").Group("region_id, source_type").Scan(&sourceTypeResults).Error; err != nil {
writeError(c, http.StatusInternalServerError, "failed to query source type statistics")
return
}
analysisTypeMap := make(map[uint32]map[string]int64)
for _, r := range analysisTypeResults {
regionID := uint32(0)
if r.RegionID != nil {
regionID = *r.RegionID
}
if analysisTypeMap[regionID] == nil {
analysisTypeMap[regionID] = make(map[string]int64)
}
analysisTypeMap[regionID][r.AnalysisType] = r.Count
}
sourceTypeMap := make(map[uint32]map[string]int64)
for _, r := range sourceTypeResults {
regionID := uint32(0)
if r.RegionID != nil {
regionID = *r.RegionID
}
if sourceTypeMap[regionID] == nil {
sourceTypeMap[regionID] = make(map[string]int64)
}
sourceTypeMap[regionID][r.SourceType] = r.Count
}
// 收集所有 regionId 查询幼儿园名称
regionIDs := make([]uint32, 0, len(rawResults))
for _, r := range rawResults {
if r.RegionID != nil && *r.RegionID > 0 {
regionIDs = append(regionIDs, *r.RegionID)
}
}
kindergartenMap := make(map[uint32]string)
if len(regionIDs) > 0 {
var kindergartens []models.Kindergarten
if err := sc.DB.Where("region_id IN ?", regionIDs).Find(&kindergartens).Error; err == nil {
for _, k := range kindergartens {
kindergartenMap[k.RegionID] = k.Name
}
}
}
overall := regionStatisticsItem{
AnalysisTypeCounts: make(map[string]int64),
SourceTypeCounts: make(map[string]int64),
}
regions := make(map[string]regionStatisticsItem, len(rawResults))
for _, r := range rawResults {
regionID := uint32(0)
if r.RegionID != nil {
regionID = *r.RegionID
}
avgDuration := float64(0)
if r.Count > 0 {
avgDuration = float64(r.TotalDurationMs) / float64(r.Count)
}
avgTotalCost := float64(0)
if r.Count > 0 {
avgTotalCost = r.TotalCost / float64(r.Count)
}
kgName := ""
if regionID > 0 {
kgName = kindergartenMap[regionID]
}
var firstUsedAt, lastUsedAt *time.Time
if r.FirstUsedAt != nil {
t := time.UnixMilli(*r.FirstUsedAt)
firstUsedAt = &t
}
if r.LastUsedAt != nil {
t := time.UnixMilli(*r.LastUsedAt)
lastUsedAt = &t
}
item := regionStatisticsItem{
RegionID: regionID,
KindergartenName: kgName,
Count: r.Count,
TotalInputTokens: r.TotalInputTokens,
TotalOutputTokens: r.TotalOutputTokens,
TotalCacheHitTokens: r.TotalCacheHitTokens,
TotalCacheMissTokens: r.TotalCacheMissTokens,
TotalInputSizeBytes: r.TotalInputSizeBytes,
TotalOutputSizeBytes: r.TotalOutputSizeBytes,
TotalDurationMs: r.TotalDurationMs,
AvgDurationMs: avgDuration,
AvgTotalCost: avgTotalCost,
TotalOriginalFileSize: r.TotalOriginalFileSize,
TotalCompressedSize: r.TotalCompressedSize,
TotalCost: r.TotalCost,
TotalInputCost: r.TotalInputCost,
TotalOutputCost: r.TotalOutputCost,
TotalCacheHitCost: r.TotalCacheHitCost,
TotalCacheMissCost: r.TotalCacheMissCost,
AnalysisTypeCounts: analysisTypeMap[regionID],
SourceTypeCounts: sourceTypeMap[regionID],
FirstUsedAt: firstUsedAt,
LastUsedAt: lastUsedAt,
}
regions[strconv.FormatUint(uint64(regionID), 10)] = item
overall.Count += r.Count
overall.TotalInputTokens += r.TotalInputTokens
overall.TotalOutputTokens += r.TotalOutputTokens
overall.TotalCacheHitTokens += r.TotalCacheHitTokens
overall.TotalCacheMissTokens += r.TotalCacheMissTokens
overall.TotalInputSizeBytes += r.TotalInputSizeBytes
overall.TotalOutputSizeBytes += r.TotalOutputSizeBytes
overall.TotalDurationMs += r.TotalDurationMs
overall.TotalOriginalFileSize += r.TotalOriginalFileSize
overall.TotalCompressedSize += r.TotalCompressedSize
overall.TotalCost += r.TotalCost
overall.TotalInputCost += r.TotalInputCost
overall.TotalOutputCost += r.TotalOutputCost
overall.TotalCacheHitCost += r.TotalCacheHitCost
overall.TotalCacheMissCost += r.TotalCacheMissCost
if firstUsedAt != nil {
if overall.FirstUsedAt == nil || firstUsedAt.Before(*overall.FirstUsedAt) {
overall.FirstUsedAt = firstUsedAt
}
}
if lastUsedAt != nil {
if overall.LastUsedAt == nil || lastUsedAt.After(*overall.LastUsedAt) {
overall.LastUsedAt = lastUsedAt
}
}
}
for _, r := range analysisTypeResults {
overall.AnalysisTypeCounts[r.AnalysisType] += r.Count
}
for _, r := range sourceTypeResults {
overall.SourceTypeCounts[r.SourceType] += r.Count
}
if overall.Count > 0 {
overall.AvgDurationMs = float64(overall.TotalDurationMs) / float64(overall.Count)
overall.AvgTotalCost = overall.TotalCost / float64(overall.Count)
}
writeSuccess(c, http.StatusOK, "query success", gin.H{
"overall": overall,
"regions": regions,
})
}
type trainingSessionRegionStatisticsItem struct {
RegionID uint32 `json:"regionId"`
KindergartenName string `json:"kindergartenName"`
Count int64 `json:"count"`
StartedCount int64 `json:"startedCount"`
EndedCount int64 `json:"endedCount"`
CompletedCount int64 `json:"completedCount"`
InProgressCount int64 `json:"inProgressCount"`
TotalDurationMs int64 `json:"totalDurationMs"`
AvgDurationMs float64 `json:"avgDurationMs"`
EventTypeCounts map[string]int64 `json:"eventTypeCounts"`
AppNameCounts map[string]int64 `json:"appNameCounts"`
FlavorTypeCounts map[string]int64 `json:"flavorTypeCounts"`
FirstPublishedAt *time.Time `json:"firstPublishedAt"`
LastPublishedAt *time.Time `json:"lastPublishedAt"`
}
// @Summary 训练会话区域统计
// @Description 按区域统计MQTT训练会话情况包括开始、结束、完成、进行中的会话数
// @Tags 统计管理
// @Produce json
// @Param regionId query int false "区域ID"
// @Param flavorType query string false "类型筛选"
// @Param startTime query int false "开始时间(毫秒时间戳)"
// @Param endTime query int false "结束时间(毫秒时间戳)"
// @Security BearerAuth
// @Success 200 {object} SwagAPIResponse "查询成功"
// @Router /admin/statistics/mqtt-training-sessions [get]
func (sc *StatisticsController) TrainingSessionStatisticsByRegion(c *gin.Context) {
regionIDStr := c.Query("regionId")
flavorType := strings.TrimSpace(c.Query("flavorType"))
startTimeStr := c.Query("startTime")
endTimeStr := c.Query("endTime")
query := sc.DB.Model(&models.MqttTrainingSessionRecord{})
if regionIDStr != "" {
if regionID, err := strconv.ParseUint(regionIDStr, 10, 32); err == nil {
query = query.Where("region_id = ?", uint32(regionID))
}
}
if flavorType != "" {
query = query.Where("flavor_type = ?", flavorType)
}
if startTimeStr != "" {
if startTime, err := strconv.ParseInt(startTimeStr, 10, 64); err == nil {
query = query.Where("published_at >= ?", startTime)
}
}
if endTimeStr != "" {
if endTime, err := strconv.ParseInt(endTimeStr, 10, 64); err == nil {
query = query.Where("published_at <= ?", endTime)
}
}
type rawTrainingStats struct {
RegionID *uint32
Count int64
StartedCount int64
EndedCount int64
CompletedCount int64
InProgressCount int64
TotalDurationMs int64
FirstPublishedAt *int64
LastPublishedAt *int64
}
var rawResults []rawTrainingStats
err := query.Select(`
region_id,
COUNT(*) as count,
COALESCE(SUM(CASE WHEN started_at IS NOT NULL THEN 1 ELSE 0 END), 0) as started_count,
COALESCE(SUM(CASE WHEN ended_at IS NOT NULL THEN 1 ELSE 0 END), 0) as ended_count,
COALESCE(SUM(CASE WHEN started_at IS NOT NULL AND ended_at IS NOT NULL THEN 1 ELSE 0 END), 0) as completed_count,
COALESCE(SUM(CASE WHEN started_at IS NOT NULL AND ended_at IS NULL THEN 1 ELSE 0 END), 0) as in_progress_count,
COALESCE(SUM(CASE WHEN started_at IS NOT NULL AND ended_at IS NOT NULL AND ended_at >= started_at THEN ended_at - started_at ELSE 0 END), 0) as total_duration_ms,
MIN(published_at) as first_published_at,
MAX(published_at) as last_published_at
`).Group("region_id").Scan(&rawResults).Error
if err != nil {
writeError(c, http.StatusInternalServerError, "failed to query training session statistics")
return
}
type trainingEventTypeCount struct {
RegionID *uint32
EventType string
Count int64
}
var eventTypeResults []trainingEventTypeCount
if err := query.Select("region_id, event_type, COUNT(*) as count").Group("region_id, event_type").Scan(&eventTypeResults).Error; err != nil {
writeError(c, http.StatusInternalServerError, "failed to query training session event type statistics")
return
}
type trainingAppNameCount struct {
RegionID *uint32
AppName string
Count int64
}
var appNameResults []trainingAppNameCount
if err := query.Select("region_id, app_name, COUNT(*) as count").Group("region_id, app_name").Scan(&appNameResults).Error; err != nil {
writeError(c, http.StatusInternalServerError, "failed to query training session app name statistics")
return
}
type trainingFlavorTypeCount struct {
RegionID *uint32
FlavorType string
Count int64
}
var flavorTypeResults []trainingFlavorTypeCount
if err := query.Select("region_id, flavor_type, COUNT(*) as count").Group("region_id, flavor_type").Scan(&flavorTypeResults).Error; err != nil {
writeError(c, http.StatusInternalServerError, "failed to query training session flavor type statistics")
return
}
eventTypeMap := make(map[uint32]map[string]int64)
for _, r := range eventTypeResults {
regionID := uint32(0)
if r.RegionID != nil {
regionID = *r.RegionID
}
if eventTypeMap[regionID] == nil {
eventTypeMap[regionID] = make(map[string]int64)
}
eventTypeMap[regionID][r.EventType] = r.Count
}
appNameMap := make(map[uint32]map[string]int64)
for _, r := range appNameResults {
regionID := uint32(0)
if r.RegionID != nil {
regionID = *r.RegionID
}
if appNameMap[regionID] == nil {
appNameMap[regionID] = make(map[string]int64)
}
appNameMap[regionID][r.AppName] = r.Count
}
flavorTypeMap := make(map[uint32]map[string]int64)
for _, r := range flavorTypeResults {
regionID := uint32(0)
if r.RegionID != nil {
regionID = *r.RegionID
}
if flavorTypeMap[regionID] == nil {
flavorTypeMap[regionID] = make(map[string]int64)
}
flavorTypeMap[regionID][r.FlavorType] = r.Count
}
regionIDs := make([]uint32, 0, len(rawResults))
for _, r := range rawResults {
if r.RegionID != nil && *r.RegionID > 0 {
regionIDs = append(regionIDs, *r.RegionID)
}
}
kindergartenMap := make(map[uint32]string)
if len(regionIDs) > 0 {
var kindergartens []models.Kindergarten
if err := sc.DB.Where("region_id IN ?", regionIDs).Find(&kindergartens).Error; err == nil {
for _, k := range kindergartens {
kindergartenMap[k.RegionID] = k.Name
}
}
}
overall := trainingSessionRegionStatisticsItem{
EventTypeCounts: make(map[string]int64),
AppNameCounts: make(map[string]int64),
FlavorTypeCounts: make(map[string]int64),
}
regions := make(map[string]trainingSessionRegionStatisticsItem, len(rawResults))
for _, r := range rawResults {
regionID := uint32(0)
if r.RegionID != nil {
regionID = *r.RegionID
}
avgDuration := float64(0)
if r.CompletedCount > 0 {
avgDuration = float64(r.TotalDurationMs) / float64(r.CompletedCount)
}
kgName := ""
if regionID > 0 {
kgName = kindergartenMap[regionID]
}
var firstPublishedAt, lastPublishedAt *time.Time
if r.FirstPublishedAt != nil {
t := time.UnixMilli(*r.FirstPublishedAt)
firstPublishedAt = &t
}
if r.LastPublishedAt != nil {
t := time.UnixMilli(*r.LastPublishedAt)
lastPublishedAt = &t
}
item := trainingSessionRegionStatisticsItem{
RegionID: regionID,
KindergartenName: kgName,
Count: r.Count,
StartedCount: r.StartedCount,
EndedCount: r.EndedCount,
CompletedCount: r.CompletedCount,
InProgressCount: r.InProgressCount,
TotalDurationMs: r.TotalDurationMs,
AvgDurationMs: avgDuration,
EventTypeCounts: eventTypeMap[regionID],
AppNameCounts: appNameMap[regionID],
FlavorTypeCounts: flavorTypeMap[regionID],
FirstPublishedAt: firstPublishedAt,
LastPublishedAt: lastPublishedAt,
}
regions[strconv.FormatUint(uint64(regionID), 10)] = item
overall.Count += r.Count
overall.StartedCount += r.StartedCount
overall.EndedCount += r.EndedCount
overall.CompletedCount += r.CompletedCount
overall.InProgressCount += r.InProgressCount
overall.TotalDurationMs += r.TotalDurationMs
if firstPublishedAt != nil {
if overall.FirstPublishedAt == nil || firstPublishedAt.Before(*overall.FirstPublishedAt) {
overall.FirstPublishedAt = firstPublishedAt
}
}
if lastPublishedAt != nil {
if overall.LastPublishedAt == nil || lastPublishedAt.After(*overall.LastPublishedAt) {
overall.LastPublishedAt = lastPublishedAt
}
}
}
for _, r := range eventTypeResults {
overall.EventTypeCounts[r.EventType] += r.Count
}
for _, r := range appNameResults {
overall.AppNameCounts[r.AppName] += r.Count
}
for _, r := range flavorTypeResults {
overall.FlavorTypeCounts[r.FlavorType] += r.Count
}
if overall.CompletedCount > 0 {
overall.AvgDurationMs = float64(overall.TotalDurationMs) / float64(overall.CompletedCount)
}
writeSuccess(c, http.StatusOK, "query success", gin.H{
"overall": overall,
"regions": regions,
})
}
// @Summary AI分析时间线统计
// @Description 按日期统计AI分析的使用情况趋势含总体和分区域数据
// @Tags 统计管理
// @Produce json
// @Param regionId query int false "区域ID"
// @Param startTime query int false "开始时间(毫秒时间戳)"
// @Param endTime query int false "结束时间(毫秒时间戳)"
// @Security BearerAuth
// @Success 200 {object} SwagAPIResponse "查询成功"
// @Router /admin/statistics/ai-analysis-timeline [get]
func (sc *StatisticsController) TimelineStatistics(c *gin.Context) {
regionIDStr := c.Query("regionId")
startTimeStr := c.Query("startTime")
endTimeStr := c.Query("endTime")
query := sc.DB.Model(&models.AIAnalysisRecord{})
if regionIDStr != "" {
if regionID, err := strconv.ParseUint(regionIDStr, 10, 32); err == nil {
query = query.Where("region_id = ?", uint32(regionID))
}
}
if startTimeStr != "" {
if startTime, err := strconv.ParseInt(startTimeStr, 10, 64); err == nil {
query = query.Where("upload_time >= ?", startTime)
}
}
if endTimeStr != "" {
if endTime, err := strconv.ParseInt(endTimeStr, 10, 64); err == nil {
query = query.Where("upload_time <= ?", endTime)
}
}
type timelineItem struct {
Date string `json:"date"`
Count int64 `json:"count"`
InputTokens int64 `json:"inputTokens"`
OutputTokens int64 `json:"outputTokens"`
CacheHitTokens int64 `json:"cacheHitTokens"`
CacheMissTokens int64 `json:"cacheMissTokens"`
TotalCost float64 `json:"totalCost"`
}
type rawRegionTimeline struct {
RegionID *uint32
Date string
Count int64
InputTokens int64
OutputTokens int64
CacheHitTokens int64
CacheMissTokens int64
TotalCost float64
}
var rawResults []rawRegionTimeline
err := query.Select(`
region_id,
DATE(TO_TIMESTAMP(upload_time / 1000.0)) as date,
COUNT(*) as count,
COALESCE(SUM(input_tokens), 0) as input_tokens,
COALESCE(SUM(output_tokens), 0) as output_tokens,
COALESCE(SUM(cache_hit_tokens), 0) as cache_hit_tokens,
COALESCE(SUM(cache_miss_tokens), 0) as cache_miss_tokens,
COALESCE(SUM(total_cost), 0) as total_cost
`).Group("region_id, DATE(TO_TIMESTAMP(upload_time / 1000.0))").Order("region_id, date ASC").Scan(&rawResults).Error
if err != nil {
writeError(c, http.StatusInternalServerError, "failed to query timeline statistics")
return
}
overallMap := make(map[string]*timelineItem)
regionItemsMap := make(map[string][]timelineItem)
regionIDs := make([]uint32, 0)
regionIDSet := make(map[uint32]struct{})
for _, r := range rawResults {
if overallMap[r.Date] == nil {
overallMap[r.Date] = &timelineItem{Date: r.Date}
}
overallMap[r.Date].Count += r.Count
overallMap[r.Date].InputTokens += r.InputTokens
overallMap[r.Date].OutputTokens += r.OutputTokens
overallMap[r.Date].CacheHitTokens += r.CacheHitTokens
overallMap[r.Date].CacheMissTokens += r.CacheMissTokens
overallMap[r.Date].TotalCost += r.TotalCost
regionID := uint32(0)
if r.RegionID != nil {
regionID = *r.RegionID
}
regionIDStr := strconv.FormatUint(uint64(regionID), 10)
regionItemsMap[regionIDStr] = append(regionItemsMap[regionIDStr], timelineItem{
Date: r.Date,
Count: r.Count,
InputTokens: r.InputTokens,
OutputTokens: r.OutputTokens,
CacheHitTokens: r.CacheHitTokens,
CacheMissTokens: r.CacheMissTokens,
TotalCost: r.TotalCost,
})
if _, ok := regionIDSet[regionID]; !ok && regionID > 0 {
regionIDSet[regionID] = struct{}{}
regionIDs = append(regionIDs, regionID)
}
}
// 查询幼儿园名称
kindergartenMap := make(map[uint32]string)
if len(regionIDs) > 0 {
var kindergartens []models.Kindergarten
if err := sc.DB.Where("region_id IN ?", regionIDs).Find(&kindergartens).Error; err == nil {
for _, k := range kindergartens {
kindergartenMap[k.RegionID] = k.Name
}
}
}
type regionTimeline struct {
Name string `json:"name"`
Items []timelineItem `json:"items"`
}
regionsMap := make(map[string]regionTimeline)
for regionIDStr, items := range regionItemsMap {
name := ""
if regionID, err := strconv.ParseUint(regionIDStr, 10, 32); err == nil && regionID > 0 {
name = kindergartenMap[uint32(regionID)]
}
regionsMap[regionIDStr] = regionTimeline{
Name: name,
Items: items,
}
}
var overall []timelineItem
for _, item := range overallMap {
overall = append(overall, *item)
}
sort.Slice(overall, func(i, j int) bool {
return overall[i].Date < overall[j].Date
})
writeSuccess(c, http.StatusOK, "query success", gin.H{
"overall": overall,
"regions": regionsMap,
})
}