Files
junhong_cmp_fiber/internal/query/integration/overview.go
break c64f3d8b80
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m31s
全局审计完成
2026-08-07 11:02:52 +08:00

180 lines
8.3 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 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或outboundname为中文展示名"`
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, "聚合外部交互趋势失败")
}