Files
aiops/backend/pkg/billingsync/sync.go
T
Logan.Li 0b785da8ae feat: 完善多云资产与腾讯云账单同步
- 接入 AWS、腾讯云、华为云和 Azure 区域及主机发现
- 接入腾讯云账单分页、限流与组件费用汇总
- 批量账单仅同步已支持厂商并优化云账号操作
2026-07-25 14:45:19 +08:00

145 lines
3.9 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.
// 云账单同步引擎 — 从云账号 AK 拉取日账单写入本地 // Logan.Li
package billingsync
import (
"fmt"
"time"
"ezops/cloud"
"ezops/model"
"gorm.io/gorm"
)
// Result 同步结果
type Result struct {
BillingDate string `json:"billing_date"`
Accounts int `json:"accounts"`
Created int `json:"created"`
Updated int `json:"updated"`
Skipped int `json:"skipped"`
Errors []string `json:"errors"`
}
// Yesterday 昨日日期 YYYY-MM-DD
func Yesterday() string {
return time.Now().AddDate(0, 0, -1).Format("2006-01-02")
}
// SyncDate 同步指定日期账单(全部或单个活跃云账号)
func SyncDate(db *gorm.DB, billingDate string, accountID *uint) Result {
result := Result{
BillingDate: billingDate,
Errors: []string{},
}
var accounts []model.CloudAccount
q := db.Where("status = ?", "active")
if accountID != nil {
q = q.Where("id = ?", *accountID)
} else {
// 批量同步仅处理已接入账单 API 的厂商,避免不支持厂商造成无效失败。 // Logan.Li
q = q.Where("provider IN ?", cloud.SupportedBillingProviders)
}
q.Find(&accounts)
result.Accounts = len(accounts)
if len(accounts) == 0 {
result.Errors = append(result.Errors, "没有可同步的活跃云账号")
return result
}
for _, account := range accounts {
created, updated, skipped, err := syncAccount(db, account, billingDate)
result.Created += created
result.Updated += updated
result.Skipped += skipped
if err != nil {
result.Errors = append(result.Errors, fmt.Sprintf("%s(%s): %s", account.Name, account.Provider, err.Error()))
}
}
return result
}
// SummaryMessage 生成执行摘要文案
func SummaryMessage(r Result) string {
msg := fmt.Sprintf("%s 新增 %d 条,更新 %d 条", r.BillingDate, r.Created, r.Updated)
if len(r.Errors) > 0 {
msg += fmt.Sprintf(";%d 个账号异常", len(r.Errors))
}
return msg
}
func syncAccount(db *gorm.DB, account model.CloudAccount, billingDate string) (created, updated, skipped int, err error) {
fetcher := cloud.NewBillingFetcher(account.Provider, account.AccessKey, account.SecretKey)
if fetcher == nil {
return 0, 0, 0, fmt.Errorf("暂不支持该云厂商账单同步")
}
records, err := fetcher.FetchDailyBill(billingDate)
if err != nil {
return 0, 0, 0, err
}
if len(records) == 0 {
return 0, 0, 1, fmt.Errorf("该日期暂无账单数据(可能尚未出账)")
}
accountID := account.ID
accountName := account.Name
for _, rec := range records {
var existing model.CloudBilling
findErr := db.Where(
"cloud_account_id = ? AND billing_date = ? AND provider = ? AND service = ? AND region = ?",
accountID, billingDate, rec.Provider, rec.Service, rec.Region,
).Take(&existing).Error
if findErr == gorm.ErrRecordNotFound {
row := model.CloudBilling{
CloudAccountID: &accountID,
AccountName: accountName,
Provider: rec.Provider,
BillingDate: billingDate,
Cost: rec.Cost,
Currency: rec.Currency,
Service: rec.Service,
Region: rec.Region,
Description: rec.Description,
}
if row.Currency == "" {
row.Currency = "CNY"
}
if db.Create(&row).Error == nil {
created++
} else {
skipped++
}
continue
}
if findErr != nil {
skipped++
continue
}
if db.Model(&existing).Updates(map[string]interface{}{
"cost": rec.Cost,
"currency": rec.Currency,
"description": rec.Description,
"account_name": accountName,
}).Error == nil {
updated++
} else {
skipped++
}
}
monthStart := billingDate[:7] + "-01"
var monthCost float64
db.Model(&model.CloudBilling{}).
Where("cloud_account_id = ? AND billing_date >= ?", accountID, monthStart).
Select("COALESCE(SUM(cost), 0)").Scan(&monthCost)
db.Model(&account).Update("monthly_cost", monthCost)
return created, updated, skipped, nil
}