固化七月迭代审计治理进展以隔离线上热修
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 参数类型错误未通过。
This commit is contained in:
171
internal/query/integration/overview.go
Normal file
171
internal/query/integration/overview.go
Normal file
@@ -0,0 +1,171 @@
|
||||
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, "聚合外部交互趋势失败")
|
||||
}
|
||||
Reference in New Issue
Block a user