Files
junhong_cmp_fiber/internal/query/integration/overview.go
break b3499adfca 固化七月迭代审计治理进展以隔离线上热修
Constraint: 切换 main 前必须保存当前七月分支全部项目进展,套餐生效提案仅属于 Iteration/7-11。

Rejected: 将七月套餐修复直接移植到 main | 两个分支的可靠投递架构不同。

Confidence: medium

Scope-risk: broad

Directive: 不得将本提交整体 cherry-pick 到 main;main 套餐热修必须基于其纯 Asynq 代码独立实施。

Tested: git diff --check;openspec validate fix-package-activation-starvation --strict。

Not-tested: 按用户要求未运行自动化测试;go build ./... 因当前审计改造中的 Enterprise 模型字面量和 role.recordFailure 参数类型错误未通过。
2026-08-03 09:47:22 +08:00

172 lines
6.5 KiB
Go

package integration
import (
"context"
"time"
"gorm.io/gorm"
"github.com/break/junhong_cmp_fiber/internal/model"
"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"`
AnomalyCount int64 `json:"anomaly_count"`
UnknownCount int64 `json:"unknown_count"`
StalePendingCount int64 `json:"stale_pending_count"`
StateChangedCount int64 `json:"state_changed_count"`
AverageDurationMS float64 `json:"average_duration_ms"`
P95DurationMS float64 `json:"p95_duration_ms"`
Results []ResultCount `json:"results"`
Providers []NamedCount `json:"providers"`
Directions []NamedCount `json:"directions"`
Trend []TrendPoint `json:"trend"`
}
// ResultCount 是原始结果及其派生类别计数。
type ResultCount struct {
Code string `json:"code"`
Name string `json:"name"`
Category string `json:"category"`
Count int64 `json:"count"`
}
// NamedCount 是稳定编码、中文名称和数量。
type NamedCount struct {
Code string `json:"code"`
Name string `json:"name"`
Count int64 `json:"count"`
}
// TrendPoint 是固定时间桶内的结果类别趋势。
type TrendPoint struct {
BucketAt time.Time `json:"bucket_at"`
Total int64 `json:"total"`
Succeeded int64 `json:"succeeded"`
Processing int64 `json:"processing"`
Indeterminate int64 `json:"indeterminate"`
Failed int64 `json:"failed"`
NotSent int64 `json:"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"
}
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{}}
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, "聚合外部交互趋势失败")
}