package service import ( "context" "fmt" "time" "oci-portal/internal/model" ) // OverviewTenants 是租户数量与类别分布。 type OverviewTenants struct { Total int `json:"total"` Alive int `json:"alive"` Dead int `json:"dead"` ByType map[string]int `json:"byType"` } // OverviewCheck 是测活快照聚合;实例数为各配置默认区域之和, // CoveredConfigs/TotalConfigs 用于前端标注覆盖度。 type OverviewCheck struct { HasActiveTask bool `json:"hasActiveTask"` CoveredConfigs int `json:"coveredConfigs"` TotalConfigs int `json:"totalConfigs"` InstanceCount int `json:"instanceCount"` LastCheckedAt *time.Time `json:"lastCheckedAt"` } // OverviewCostDay 是一天的跨租户成本合计。 type OverviewCostDay struct { Day string `json:"day"` Amount float64 `json:"amount"` } // OverviewCost 是近 7 日成本快照聚合;金额跨配置直接相加, // Currency 取快照中出现的第一个币种(混合币种时以此为准展示)。 type OverviewCost struct { HasActiveTask bool `json:"hasActiveTask"` CoveredConfigs int `json:"coveredConfigs"` Currency string `json:"currency"` Total float64 `json:"total"` Days []OverviewCostDay `json:"days"` } // OverviewTasks 是后台任务数量统计。 type OverviewTasks struct { Total int `json:"total"` Active int `json:"active"` HealthCheck int `json:"healthCheck"` Cost int `json:"cost"` Snatch int `json:"snatch"` } // Overview 是总览页聚合数据。 type Overview struct { Tenants OverviewTenants `json:"tenants"` Check OverviewCheck `json:"check"` Cost OverviewCost `json:"cost"` Tasks OverviewTasks `json:"tasks"` } // Overview 聚合总览数据:租户分布、测活/成本快照与任务统计,全部读本地库不发云端请求。 func (s *OciConfigService) Overview(ctx context.Context) (*Overview, error) { var out Overview if err := s.overviewTenants(ctx, &out); err != nil { return nil, err } if err := s.overviewCheck(ctx, &out); err != nil { return nil, err } if err := s.overviewCost(ctx, &out); err != nil { return nil, err } if err := s.overviewTasks(ctx, &out); err != nil { return nil, err } return &out, nil } func (s *OciConfigService) overviewTenants(ctx context.Context, out *Overview) error { configs, err := s.List(ctx) if err != nil { return err } out.Tenants.Total = len(configs) out.Tenants.ByType = map[string]int{} for _, cfg := range configs { out.Tenants.ByType[cfg.AccountType]++ switch cfg.AliveStatus { case model.AliveStatusAlive: out.Tenants.Alive++ case model.AliveStatusDead: out.Tenants.Dead++ } } return nil } // hasActiveTask 判断是否存在指定类型且调度中的任务。 func (s *OciConfigService) hasActiveTask(ctx context.Context, taskType string) (bool, error) { var n int64 err := s.db.WithContext(ctx).Model(&model.Task{}). Where("type = ? AND status = ?", taskType, model.TaskStatusActive).Count(&n).Error if err != nil { return false, fmt.Errorf("count %s tasks: %w", taskType, err) } return n > 0, nil } func (s *OciConfigService) overviewCheck(ctx context.Context, out *Overview) error { has, err := s.hasActiveTask(ctx, model.TaskTypeHealthCheck) if err != nil { return err } out.Check.HasActiveTask = has out.Check.TotalConfigs = out.Tenants.Total var snaps []model.CheckSnapshot err = s.db.WithContext(ctx). Where("oci_config_id IN (?)", s.db.Model(&model.OciConfig{}).Select("id")). Find(&snaps).Error if err != nil { return fmt.Errorf("load check snapshots: %w", err) } out.Check.CoveredConfigs = len(snaps) for _, snap := range snaps { out.Check.InstanceCount += snap.InstanceCount if out.Check.LastCheckedAt == nil || snap.CheckedAt.After(*out.Check.LastCheckedAt) { t := snap.CheckedAt out.Check.LastCheckedAt = &t } } return nil } func (s *OciConfigService) overviewCost(ctx context.Context, out *Overview) error { has, err := s.hasActiveTask(ctx, model.TaskTypeCost) if err != nil { return err } out.Cost.HasActiveTask = has since := time.Now().UTC().AddDate(0, 0, -7).Format("2006-01-02") var snaps []model.CostSnapshot err = s.db.WithContext(ctx). Where("day >= ? AND oci_config_id IN (?)", since, s.db.Model(&model.OciConfig{}).Select("id")). Order("day").Find(&snaps).Error if err != nil { return fmt.Errorf("load cost snapshots: %w", err) } aggregateCostDays(&out.Cost, snaps) return nil } // aggregateCostDays 把逐配置逐日的快照聚合为跨租户的每日序列(snaps 须按 day 升序)。 func aggregateCostDays(cost *OverviewCost, snaps []model.CostSnapshot) { byDay := map[string]float64{} covered := map[uint]bool{} var order []string for _, snap := range snaps { if _, ok := byDay[snap.Day]; !ok { order = append(order, snap.Day) } byDay[snap.Day] += snap.Amount covered[snap.OciConfigID] = true if cost.Currency == "" { cost.Currency = snap.Currency } } cost.CoveredConfigs = len(covered) cost.Days = make([]OverviewCostDay, 0, len(order)) for _, day := range order { cost.Days = append(cost.Days, OverviewCostDay{Day: day, Amount: byDay[day]}) cost.Total += byDay[day] } } func (s *OciConfigService) overviewTasks(ctx context.Context, out *Overview) error { var tasks []model.Task if err := s.db.WithContext(ctx).Find(&tasks).Error; err != nil { return fmt.Errorf("list tasks: %w", err) } out.Tasks.Total = len(tasks) for _, t := range tasks { if t.Status == model.TaskStatusActive { out.Tasks.Active++ } switch t.Type { case model.TaskTypeHealthCheck: out.Tasks.HealthCheck++ case model.TaskTypeCost: out.Tasks.Cost++ case model.TaskTypeSnatch: out.Tasks.Snatch++ } } return nil }