Files
aiops/backend/pkg/billingsync/sync.go
T
Logan.Li dd3770a66c fix: 恢复腾讯云账单同步并完善成本预测
- 接入腾讯云单日账单、全局限频和指数退避

- 修正月末费用预测并增加账期提示和费用归因

- 补充腾讯云限频及账单归因回归测试
2026-07-29 05:44:40 +08:00

145 lines
3.8 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
}