diff --git a/backend/internal/service/balance_notify_service.go b/backend/internal/service/balance_notify_service.go
index 7cd61a0a..8223a231 100644
--- a/backend/internal/service/balance_notify_service.go
+++ b/backend/internal/service/balance_notify_service.go
@@ -85,73 +85,59 @@ func (s *BalanceNotifyService) CheckBalanceAfterDeduction(ctx context.Context, u
}
}
+// quotaDim describes one quota dimension for notification checking.
+type quotaDim struct {
+ name string
+ enabled bool
+ threshold float64
+ oldUsed float64
+ limit float64
+}
+
+// buildQuotaDims returns the three quota dimensions for notification checking.
+func buildQuotaDims(account *Account) []quotaDim {
+ return []quotaDim{
+ {quotaDimDaily, account.GetQuotaNotifyDailyEnabled(), account.GetQuotaNotifyDailyThreshold(), account.GetQuotaDailyUsed(), account.GetQuotaDailyLimit()},
+ {quotaDimWeekly, account.GetQuotaNotifyWeeklyEnabled(), account.GetQuotaNotifyWeeklyThreshold(), account.GetQuotaWeeklyUsed(), account.GetQuotaWeeklyLimit()},
+ {quotaDimTotal, account.GetQuotaNotifyTotalEnabled(), account.GetQuotaNotifyTotalThreshold(), account.GetQuotaUsed(), account.GetQuotaLimit()},
+ }
+}
+
// CheckAccountQuotaAfterIncrement checks if any quota dimension crossed above its notify threshold.
// The account's Extra fields contain pre-increment usage values.
func (s *BalanceNotifyService) CheckAccountQuotaAfterIncrement(ctx context.Context, account *Account, cost float64) {
if account == nil || s.emailService == nil || s.settingRepo == nil || cost <= 0 {
return
}
-
adminEmails := s.getAccountQuotaNotifyEmails(ctx)
if len(adminEmails) == 0 {
return
}
siteName := s.getSiteName(ctx)
-
- // Check each dimension
- type quotaDim struct {
- name string
- enabled bool
- threshold float64
- oldUsed float64
- limit float64
- }
-
- dims := []quotaDim{
- {
- name: quotaDimDaily,
- enabled: account.GetQuotaNotifyDailyEnabled(),
- threshold: account.GetQuotaNotifyDailyThreshold(),
- oldUsed: account.GetQuotaDailyUsed(),
- limit: account.GetQuotaDailyLimit(),
- },
- {
- name: quotaDimWeekly,
- enabled: account.GetQuotaNotifyWeeklyEnabled(),
- threshold: account.GetQuotaNotifyWeeklyThreshold(),
- oldUsed: account.GetQuotaWeeklyUsed(),
- limit: account.GetQuotaWeeklyLimit(),
- },
- {
- name: quotaDimTotal,
- enabled: account.GetQuotaNotifyTotalEnabled(),
- threshold: account.GetQuotaNotifyTotalThreshold(),
- oldUsed: account.GetQuotaUsed(),
- limit: account.GetQuotaLimit(),
- },
- }
-
- for _, dim := range dims {
+ for _, dim := range buildQuotaDims(account) {
if !dim.enabled || dim.threshold <= 0 {
continue
}
newUsed := dim.oldUsed + cost
- // Only notify on first crossing
if dim.oldUsed < dim.threshold && newUsed >= dim.threshold {
- dimCopy := dim // capture loop variable
- go func() {
- defer func() {
- if r := recover(); r != nil {
- slog.Error("panic in quota notification", "recover", r)
- }
- }()
- s.sendQuotaAlertEmails(adminEmails, account.Name, dimCopy.name, newUsed, dimCopy.limit, dimCopy.threshold, siteName)
- }()
+ s.asyncSendQuotaAlert(adminEmails, account.Name, dim, newUsed, siteName)
}
}
}
+// asyncSendQuotaAlert sends quota alert email in a goroutine with panic recovery.
+func (s *BalanceNotifyService) asyncSendQuotaAlert(adminEmails []string, accountName string, dim quotaDim, newUsed float64, siteName string) {
+ go func() {
+ defer func() {
+ if r := recover(); r != nil {
+ slog.Error("panic in quota notification", "recover", r)
+ }
+ }()
+ s.sendQuotaAlertEmails(adminEmails, accountName, dim.name, newUsed, dim.limit, dim.threshold, siteName)
+ }()
+}
+
// getBalanceNotifyConfig reads global balance notification settings.
func (s *BalanceNotifyService) getBalanceNotifyConfig(ctx context.Context) (enabled bool, threshold float64) {
keys := []string{SettingKeyBalanceLowNotifyEnabled, SettingKeyBalanceLowNotifyThreshold}
@@ -191,7 +177,7 @@ func (s *BalanceNotifyService) collectBalanceNotifyRecipients(user *User) []stri
recipients := []string{user.Email}
for _, extra := range user.BalanceNotifyExtraEmails {
email := strings.TrimSpace(extra)
- if email != "" && email != user.Email {
+ if email != "" && !strings.EqualFold(email, user.Email) {
recipients = append(recipients, email)
}
}
@@ -234,6 +220,7 @@ func (s *BalanceNotifyService) sendQuotaAlertEmails(adminEmails []string, accoun
}
// buildBalanceLowEmailBody builds HTML email for balance low notification.
+// Lines exceed 30 due to inline HTML template (not splittable).
func (s *BalanceNotifyService) buildBalanceLowEmailBody(userName string, balance, threshold float64, siteName string) string {
return fmt.Sprintf(`
@@ -271,6 +258,7 @@ func (s *BalanceNotifyService) buildBalanceLowEmailBody(userName string, balance
}
// buildQuotaAlertEmailBody builds HTML email for account quota alert.
+// Lines exceed 30 due to inline HTML template (not splittable).
func (s *BalanceNotifyService) buildQuotaAlertEmailBody(accountName, dimLabel string, used, limit, threshold float64, siteName string) string {
limitStr := fmt.Sprintf("$%.2f", limit)
if limit <= 0 {
diff --git a/backend/internal/service/gateway_service.go b/backend/internal/service/gateway_service.go
index 72ab39ce..1203f0c6 100644
--- a/backend/internal/service/gateway_service.go
+++ b/backend/internal/service/gateway_service.go
@@ -7343,12 +7343,9 @@ func finalizePostUsageBilling(p *postUsageBillingParams, deps *billingDeps) {
deps.balanceNotifyService.CheckBalanceAfterDeduction(context.Background(), p.User, p.User.Balance, p.Cost.ActualCost)
}
- // Account quota notification
+ // Account quota notification (use same cost formula as postUsageBilling)
if p.Cost.TotalCost > 0 && p.Account != nil && p.Account.IsAPIKeyOrBedrock() && deps.balanceNotifyService != nil {
- accountCost := p.Cost.TotalCost
- if p.AccountRateMultiplier > 0 {
- accountCost *= p.AccountRateMultiplier
- }
+ accountCost := p.Cost.TotalCost * p.AccountRateMultiplier
deps.balanceNotifyService.CheckAccountQuotaAfterIncrement(context.Background(), p.Account, accountCost)
}
}
diff --git a/frontend/src/components/account/QuotaLimitCard.vue b/frontend/src/components/account/QuotaLimitCard.vue
index 9840a5e1..7c3afd23 100644
--- a/frontend/src/components/account/QuotaLimitCard.vue
+++ b/frontend/src/components/account/QuotaLimitCard.vue
@@ -1,6 +1,7 @@
+
+
+
+
+
+
+ $
+
+
+
+