package integration import ( "context" "time" "gorm.io/gorm" "github.com/break/junhong_cmp_fiber/internal/model" retentionquery "github.com/break/junhong_cmp_fiber/internal/query/retention" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" ) // OverviewFilter 定义外部交互总览的受控筛选和时间粒度。 type OverviewFilter struct { ListFilter Bucket string } // Overview 是外部交互固定维度聚合结果。 type Overview struct { Total int64 `json:"total" description:"外部交互总数"` AnomalyCount int64 `json:"anomaly_count" description:"failed或indeterminate类别的异常交互数"` UnknownCount int64 `json:"unknown_count" description:"原始结果为unknown的交互数"` StalePendingCount int64 `json:"stale_pending_count" description:"超过既定阈值仍为pending的交互数"` StateChangedCount int64 `json:"state_changed_count" description:"改变本地业务状态的交互数"` AverageDurationMS float64 `json:"average_duration_ms" description:"平均交互耗时,单位毫秒"` P95DurationMS float64 `json:"p95_duration_ms" description:"P95交互耗时,单位毫秒"` Results []ResultCount `json:"results" description:"原始结果分布,code/name/category分别为稳定编码、中文名和派生类别"` Providers []NamedCount `json:"providers" description:"提供方分布,code为provider枚举,name为中文展示名"` Directions []NamedCount `json:"directions" description:"方向分布,code为inbound或outbound,name为中文展示名"` Trend []TrendPoint `json:"trend" description:"五类派生结果的时间趋势"` Retention retentionquery.Info `json:"retention" description:"在线查询留存边界"` } // ResultCount 是原始结果及其派生类别计数。 type ResultCount struct { Code string `json:"code" enum:"pending,success,failed,unknown,not_found,invalid_payload,conflict,ignored,merged,rate_limited,completed,cancelled" description:"外部交互原始结果稳定编码"` Name string `json:"name" description:"code对应的中文展示名称"` Category string `json:"category" enum:"processing,succeeded,indeterminate,failed,not_sent" description:"由code派生的固定结果类别"` Count int64 `json:"count" description:"该原始结果的交互数量"` } // NamedCount 是稳定编码、中文名称和数量。 type NamedCount struct { Code string `json:"code" description:"当前聚合维度的稳定编码,枚举域由所属数组字段说明"` Name string `json:"name" description:"code对应的中文展示名称"` Count int64 `json:"count" description:"该编码的交互数量"` } // TrendPoint 是固定时间桶内的结果类别趋势。 type TrendPoint struct { BucketAt time.Time `json:"bucket_at" description:"时间桶起点"` Total int64 `json:"total" description:"桶内外部交互总数"` Succeeded int64 `json:"succeeded" description:"桶内succeeded类别数量"` Processing int64 `json:"processing" description:"桶内processing类别数量"` Indeterminate int64 `json:"indeterminate" description:"桶内indeterminate类别数量"` Failed int64 `json:"failed" description:"桶内failed类别数量"` NotSent int64 `json:"not_sent" description:"桶内not_sent类别数量"` } // Overview 查询指定时间范围的固定维度外部交互总览。 func (q *Query) Overview(ctx context.Context, filter OverviewFilter) (*Overview, error) { if err := q.authorize(ctx); err != nil { return nil, err } if filter.Bucket == "" { filter.Bucket = "hour" } var retention retentionquery.Info var err error filter.ListFilter, retention, err = q.normalizeOnlineFilter(ctx, filter.ListFilter) if err != nil { return nil, err } if !validFilter(filter.ListFilter) || (filter.Bucket != "hour" && filter.Bucket != "day") { return nil, errors.New(errors.CodeInvalidParam) } base := applyFilters(q.db.WithContext(ctx).Model(&model.IntegrationLog{}), filter.ListFilter) result := &Overview{Results: []ResultCount{}, Providers: []NamedCount{}, Directions: []NamedCount{}, Trend: []TrendPoint{}, Retention: retention} if err := loadOverviewMetrics(base, result); err != nil { return nil, err } if err := loadResultCounts(base, result); err != nil { return nil, err } if err := loadNamedCounts(base, "provider", result); err != nil { return nil, err } if err := loadNamedCounts(base, "direction", result); err != nil { return nil, err } if err := loadTrend(base, filter.Bucket, result); err != nil { return nil, err } return result, nil } func loadOverviewMetrics(query *gorm.DB, result *Overview) error { failed := categoryResults(constants.IntegrationResultCategoryFailed) var row struct { Total, AnomalyCount, UnknownCount, StalePendingCount, StateChangedCount int64 AverageDurationMS, P95DurationMS float64 } err := query.Select(`COUNT(*) AS total, COUNT(*) FILTER (WHERE result IN ? OR result = ?) AS anomaly_count, COUNT(*) FILTER (WHERE result = ?) AS unknown_count, COUNT(*) FILTER (WHERE result = ? AND created_at < ?) AS stale_pending_count, COUNT(*) FILTER (WHERE state_changed) AS state_changed_count, COALESCE(AVG(duration_ms), 0)::float8 AS average_duration_ms, COALESCE(percentile_cont(0.95) WITHIN GROUP (ORDER BY duration_ms), 0)::float8 AS p95_duration_ms`, failed, constants.IntegrationResultUnknown, constants.IntegrationResultUnknown, constants.IntegrationResultPending, time.Now().UTC().Add(-constants.IntegrationPendingStaleAfter)).Scan(&row).Error if err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "聚合外部交互总览失败") } result.Total, result.AnomalyCount, result.UnknownCount = row.Total, row.AnomalyCount, row.UnknownCount result.StalePendingCount, result.StateChangedCount = row.StalePendingCount, row.StateChangedCount result.AverageDurationMS, result.P95DurationMS = row.AverageDurationMS, row.P95DurationMS return nil } func loadResultCounts(query *gorm.DB, result *Overview) error { var rows []struct { Code string Count int64 } if err := query.Select("result AS code, COUNT(*) AS count").Group("result").Order("result ASC").Scan(&rows).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "聚合外部交互结果分布失败") } for _, row := range rows { result.Results = append(result.Results, ResultCount{Code: row.Code, Name: constants.IntegrationResultName(row.Code), Category: constants.IntegrationResultCategory(row.Code), Count: row.Count}) } return nil } func loadNamedCounts(query *gorm.DB, column string, result *Overview) error { var rows []struct { Code string Count int64 } if err := query.Select(column + " AS code, COUNT(*) AS count").Group(column).Order(column + " ASC").Scan(&rows).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "聚合外部交互维度分布失败") } items := make([]NamedCount, 0, len(rows)) for _, row := range rows { name := constants.IntegrationProviderName(row.Code) if column == "direction" { name = constants.IntegrationDirectionName(row.Code) } items = append(items, NamedCount{Code: row.Code, Name: name, Count: row.Count}) } if column == "provider" { result.Providers = items } else { result.Directions = items } return nil } func loadTrend(query *gorm.DB, bucket string, result *Overview) error { failed, notSent := categoryResults(constants.IntegrationResultCategoryFailed), categoryResults(constants.IntegrationResultCategoryNotSent) return wrapTrendError(query.Select(`date_trunc(?, created_at) AS bucket_at, COUNT(*) AS total, COUNT(*) FILTER (WHERE result = ?) AS succeeded, COUNT(*) FILTER (WHERE result = ?) AS processing, COUNT(*) FILTER (WHERE result = ?) AS indeterminate, COUNT(*) FILTER (WHERE result IN ?) AS failed, COUNT(*) FILTER (WHERE result IN ?) AS not_sent`, bucket, constants.IntegrationResultSuccess, constants.IntegrationResultPending, constants.IntegrationResultUnknown, failed, notSent). Group("bucket_at").Order("bucket_at ASC").Scan(&result.Trend).Error) } func wrapTrendError(err error) error { if err == nil { return nil } return errors.Wrap(errors.CodeDatabaseError, err, "聚合外部交互趋势失败") }