197 lines
5.7 KiB
Go
197 lines
5.7 KiB
Go
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
|
|
}
|