Files
aiops/backend/pkg/billingsync/sync.go
T
Logan.Li fd60361d93 chore: 统一本地服务器与远程仓库源码
- 合并 main 与 master 历史并以服务器结构为基线
- 保留迁移、数据保留、AWS 账单与部署安全能力
- 建立 SDD 规格和 CI 验证闭环
2026-08-05 20:03:25 +08:00

151 lines
4.0 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"
"strings"
"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"
}
row.Currency = strings.ToUpper(row.Currency)
if db.Create(&row).Error == nil {
created++
} else {
skipped++
}
continue
}
if findErr != nil {
skipped++
continue
}
currency := strings.ToUpper(rec.Currency)
if currency == "" {
currency = "CNY"
}
if db.Model(&existing).Updates(map[string]interface{}{
"cost": rec.Cost,
"currency": 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
}