fix(notify): remove percentage threshold from balance notification
Balance low notification only supports fixed USD amount threshold. Percentage threshold is a quota concept, not applicable to balance. Reverted threshold_type from admin settings, user profile, and all backend/frontend layers. DB fields (balance_notify_threshold_type, total_recharged) retained for potential future quota use.
This commit is contained in:
@@ -177,7 +177,6 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
|
||||
EnableCCHSigning: settings.EnableCCHSigning,
|
||||
WebSearchEmulationEnabled: settings.WebSearchEmulationEnabled,
|
||||
BalanceLowNotifyEnabled: settings.BalanceLowNotifyEnabled,
|
||||
BalanceLowNotifyThresholdType: settings.BalanceLowNotifyThresholdType,
|
||||
BalanceLowNotifyThreshold: settings.BalanceLowNotifyThreshold,
|
||||
AccountQuotaNotifyEmails: settings.AccountQuotaNotifyEmails,
|
||||
PaymentEnabled: paymentCfg.Enabled,
|
||||
@@ -310,10 +309,9 @@ type UpdateSettingsRequest struct {
|
||||
EnableCCHSigning *bool `json:"enable_cch_signing"`
|
||||
|
||||
// Balance low notification
|
||||
BalanceLowNotifyEnabled *bool `json:"balance_low_notify_enabled"`
|
||||
BalanceLowNotifyThresholdType *string `json:"balance_low_notify_threshold_type"`
|
||||
BalanceLowNotifyThreshold *float64 `json:"balance_low_notify_threshold"`
|
||||
AccountQuotaNotifyEmails *[]string `json:"account_quota_notify_emails"`
|
||||
BalanceLowNotifyEnabled *bool `json:"balance_low_notify_enabled"`
|
||||
BalanceLowNotifyThreshold *float64 `json:"balance_low_notify_threshold"`
|
||||
AccountQuotaNotifyEmails *[]string `json:"account_quota_notify_emails"`
|
||||
|
||||
// Payment configuration (integrated into settings, full replace)
|
||||
PaymentEnabled *bool `json:"payment_enabled"`
|
||||
@@ -898,12 +896,6 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
}
|
||||
return previousSettings.BalanceLowNotifyEnabled
|
||||
}(),
|
||||
BalanceLowNotifyThresholdType: func() string {
|
||||
if req.BalanceLowNotifyThresholdType != nil {
|
||||
return *req.BalanceLowNotifyThresholdType
|
||||
}
|
||||
return previousSettings.BalanceLowNotifyThresholdType
|
||||
}(),
|
||||
BalanceLowNotifyThreshold: func() float64 {
|
||||
if req.BalanceLowNotifyThreshold != nil {
|
||||
return *req.BalanceLowNotifyThreshold
|
||||
@@ -1063,7 +1055,6 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
||||
EnableMetadataPassthrough: updatedSettings.EnableMetadataPassthrough,
|
||||
EnableCCHSigning: updatedSettings.EnableCCHSigning,
|
||||
BalanceLowNotifyEnabled: updatedSettings.BalanceLowNotifyEnabled,
|
||||
BalanceLowNotifyThresholdType: updatedSettings.BalanceLowNotifyThresholdType,
|
||||
BalanceLowNotifyThreshold: updatedSettings.BalanceLowNotifyThreshold,
|
||||
AccountQuotaNotifyEmails: updatedSettings.AccountQuotaNotifyEmails,
|
||||
PaymentEnabled: updatedPaymentCfg.Enabled,
|
||||
|
||||
@@ -150,10 +150,9 @@ type SystemSettings struct {
|
||||
PaymentCancelRateLimitMode string `json:"payment_cancel_rate_limit_window_mode"`
|
||||
|
||||
// Balance low notification
|
||||
BalanceLowNotifyEnabled bool `json:"balance_low_notify_enabled"`
|
||||
BalanceLowNotifyThresholdType string `json:"balance_low_notify_threshold_type"`
|
||||
BalanceLowNotifyThreshold float64 `json:"balance_low_notify_threshold"`
|
||||
AccountQuotaNotifyEmails []string `json:"account_quota_notify_emails"`
|
||||
BalanceLowNotifyEnabled bool `json:"balance_low_notify_enabled"`
|
||||
BalanceLowNotifyThreshold float64 `json:"balance_low_notify_threshold"`
|
||||
AccountQuotaNotifyEmails []string `json:"account_quota_notify_emails"`
|
||||
}
|
||||
|
||||
type DefaultSubscriptionSetting struct {
|
||||
|
||||
@@ -33,10 +33,9 @@ type ChangePasswordRequest struct {
|
||||
|
||||
// UpdateProfileRequest represents the update profile request payload
|
||||
type UpdateProfileRequest struct {
|
||||
Username *string `json:"username"`
|
||||
BalanceNotifyEnabled *bool `json:"balance_notify_enabled"`
|
||||
BalanceNotifyThresholdType *string `json:"balance_notify_threshold_type"`
|
||||
BalanceNotifyThreshold *float64 `json:"balance_notify_threshold"`
|
||||
Username *string `json:"username"`
|
||||
BalanceNotifyEnabled *bool `json:"balance_notify_enabled"`
|
||||
BalanceNotifyThreshold *float64 `json:"balance_notify_threshold"`
|
||||
}
|
||||
|
||||
// GetProfile handles getting user profile
|
||||
@@ -101,10 +100,9 @@ func (h *UserHandler) UpdateProfile(c *gin.Context) {
|
||||
}
|
||||
|
||||
svcReq := service.UpdateProfileRequest{
|
||||
Username: req.Username,
|
||||
BalanceNotifyEnabled: req.BalanceNotifyEnabled,
|
||||
BalanceNotifyThresholdType: req.BalanceNotifyThresholdType,
|
||||
BalanceNotifyThreshold: req.BalanceNotifyThreshold,
|
||||
Username: req.Username,
|
||||
BalanceNotifyEnabled: req.BalanceNotifyEnabled,
|
||||
BalanceNotifyThreshold: req.BalanceNotifyThreshold,
|
||||
}
|
||||
updatedUser, err := h.userService.UpdateProfile(c.Request.Context(), subject.UserID, svcReq)
|
||||
if err != nil {
|
||||
|
||||
@@ -51,12 +51,16 @@ func (s *BalanceNotifyService) CheckBalanceAfterDeduction(ctx context.Context, u
|
||||
return
|
||||
}
|
||||
|
||||
globalEnabled, globalThresholdType, globalThresholdValue := s.getBalanceNotifyConfig(ctx)
|
||||
globalEnabled, globalThreshold := s.getBalanceNotifyConfig(ctx)
|
||||
if !globalEnabled {
|
||||
return
|
||||
}
|
||||
|
||||
threshold := s.resolveEffectiveThreshold(user, globalThresholdType, globalThresholdValue)
|
||||
// User custom threshold overrides system default
|
||||
threshold := globalThreshold
|
||||
if user.BalanceNotifyThreshold != nil {
|
||||
threshold = *user.BalanceNotifyThreshold
|
||||
}
|
||||
if threshold <= 0 {
|
||||
return
|
||||
}
|
||||
@@ -76,30 +80,6 @@ func (s *BalanceNotifyService) CheckBalanceAfterDeduction(ctx context.Context, u
|
||||
}
|
||||
}
|
||||
|
||||
// resolveEffectiveThreshold computes the actual USD threshold based on type and user settings.
|
||||
// When user sets a custom threshold, their type is used independently (defaults to "fixed" if unset).
|
||||
func (s *BalanceNotifyService) resolveEffectiveThreshold(user *User, globalType string, globalValue float64) float64 {
|
||||
if user.BalanceNotifyThreshold != nil {
|
||||
thresholdType := user.BalanceNotifyThresholdType
|
||||
if thresholdType == "" {
|
||||
thresholdType = ThresholdTypeFixed // user custom value defaults to fixed, not inherited
|
||||
}
|
||||
return computeThreshold(thresholdType, *user.BalanceNotifyThreshold, user.TotalRecharged)
|
||||
}
|
||||
return computeThreshold(globalType, globalValue, user.TotalRecharged)
|
||||
}
|
||||
|
||||
// computeThreshold converts a threshold value to USD based on type.
|
||||
func computeThreshold(thresholdType string, value, totalRecharged float64) float64 {
|
||||
if thresholdType == ThresholdTypePercentage {
|
||||
if totalRecharged <= 0 {
|
||||
return 0 // no recharge history → skip percentage check
|
||||
}
|
||||
return totalRecharged * value / 100
|
||||
}
|
||||
return value // fixed USD amount
|
||||
}
|
||||
|
||||
// quotaDim describes one quota dimension for notification checking.
|
||||
type quotaDim struct {
|
||||
name string
|
||||
@@ -154,21 +134,13 @@ func (s *BalanceNotifyService) asyncSendQuotaAlert(adminEmails []string, account
|
||||
}
|
||||
|
||||
// getBalanceNotifyConfig reads global balance notification settings.
|
||||
func (s *BalanceNotifyService) getBalanceNotifyConfig(ctx context.Context) (enabled bool, thresholdType string, threshold float64) {
|
||||
keys := []string{
|
||||
SettingKeyBalanceLowNotifyEnabled,
|
||||
SettingKeyBalanceLowNotifyThresholdType,
|
||||
SettingKeyBalanceLowNotifyThreshold,
|
||||
}
|
||||
func (s *BalanceNotifyService) getBalanceNotifyConfig(ctx context.Context) (enabled bool, threshold float64) {
|
||||
keys := []string{SettingKeyBalanceLowNotifyEnabled, SettingKeyBalanceLowNotifyThreshold}
|
||||
settings, err := s.settingRepo.GetMultiple(ctx, keys)
|
||||
if err != nil {
|
||||
return false, ThresholdTypeFixed, 0
|
||||
return false, 0
|
||||
}
|
||||
enabled = settings[SettingKeyBalanceLowNotifyEnabled] == "true"
|
||||
thresholdType = settings[SettingKeyBalanceLowNotifyThresholdType]
|
||||
if thresholdType == "" {
|
||||
thresholdType = ThresholdTypeFixed
|
||||
}
|
||||
if v := settings[SettingKeyBalanceLowNotifyThreshold]; v != "" {
|
||||
if f, err := strconv.ParseFloat(v, 64); err == nil {
|
||||
threshold = f
|
||||
|
||||
@@ -251,13 +251,8 @@ const (
|
||||
SettingKeyEnableCCHSigning = "enable_cch_signing"
|
||||
|
||||
// Balance Low Notification
|
||||
SettingKeyBalanceLowNotifyEnabled = "balance_low_notify_enabled" // 全局开关
|
||||
SettingKeyBalanceLowNotifyThresholdType = "balance_low_notify_threshold_type" // "fixed" | "percentage"
|
||||
SettingKeyBalanceLowNotifyThreshold = "balance_low_notify_threshold" // 默认阈值(USD 或百分比)
|
||||
|
||||
// Threshold type constants
|
||||
ThresholdTypeFixed = "fixed"
|
||||
ThresholdTypePercentage = "percentage"
|
||||
SettingKeyBalanceLowNotifyEnabled = "balance_low_notify_enabled" // 全局开关
|
||||
SettingKeyBalanceLowNotifyThreshold = "balance_low_notify_threshold" // 默认阈值(USD)
|
||||
|
||||
// Account Quota Notification
|
||||
SettingKeyAccountQuotaNotifyEmails = "account_quota_notify_emails" // 管理员通知邮箱列表(JSON 数组)
|
||||
|
||||
@@ -608,11 +608,6 @@ func (s *SettingService) UpdateSettings(ctx context.Context, settings *SystemSet
|
||||
|
||||
// Balance low notification
|
||||
updates[SettingKeyBalanceLowNotifyEnabled] = strconv.FormatBool(settings.BalanceLowNotifyEnabled)
|
||||
thresholdType := settings.BalanceLowNotifyThresholdType
|
||||
if thresholdType != ThresholdTypeFixed && thresholdType != ThresholdTypePercentage {
|
||||
thresholdType = ThresholdTypeFixed
|
||||
}
|
||||
updates[SettingKeyBalanceLowNotifyThresholdType] = thresholdType
|
||||
updates[SettingKeyBalanceLowNotifyThreshold] = strconv.FormatFloat(settings.BalanceLowNotifyThreshold, 'f', 8, 64)
|
||||
accountQuotaNotifyEmailsJSON, err := json.Marshal(settings.AccountQuotaNotifyEmails)
|
||||
if err != nil {
|
||||
@@ -1252,10 +1247,6 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
|
||||
|
||||
// Balance low notification
|
||||
result.BalanceLowNotifyEnabled = settings[SettingKeyBalanceLowNotifyEnabled] == "true"
|
||||
result.BalanceLowNotifyThresholdType = settings[SettingKeyBalanceLowNotifyThresholdType]
|
||||
if result.BalanceLowNotifyThresholdType == "" {
|
||||
result.BalanceLowNotifyThresholdType = ThresholdTypeFixed
|
||||
}
|
||||
if v, err := strconv.ParseFloat(settings[SettingKeyBalanceLowNotifyThreshold], 64); err == nil && v >= 0 {
|
||||
result.BalanceLowNotifyThreshold = v
|
||||
}
|
||||
|
||||
@@ -108,9 +108,8 @@ type SystemSettings struct {
|
||||
EnableCCHSigning bool // 是否对 billing header cch 进行签名(默认 false)
|
||||
|
||||
// Balance low notification
|
||||
BalanceLowNotifyEnabled bool
|
||||
BalanceLowNotifyThresholdType string // "fixed" (default) | "percentage"
|
||||
BalanceLowNotifyThreshold float64
|
||||
BalanceLowNotifyEnabled bool
|
||||
BalanceLowNotifyThreshold float64
|
||||
|
||||
// Account quota notification
|
||||
AccountQuotaNotifyEmails []string
|
||||
|
||||
@@ -62,12 +62,11 @@ type UserRepository interface {
|
||||
|
||||
// UpdateProfileRequest 更新用户资料请求
|
||||
type UpdateProfileRequest struct {
|
||||
Email *string `json:"email"`
|
||||
Username *string `json:"username"`
|
||||
Concurrency *int `json:"concurrency"`
|
||||
BalanceNotifyEnabled *bool `json:"balance_notify_enabled"`
|
||||
BalanceNotifyThresholdType *string `json:"balance_notify_threshold_type"`
|
||||
BalanceNotifyThreshold *float64 `json:"balance_notify_threshold"`
|
||||
Email *string `json:"email"`
|
||||
Username *string `json:"username"`
|
||||
Concurrency *int `json:"concurrency"`
|
||||
BalanceNotifyEnabled *bool `json:"balance_notify_enabled"`
|
||||
BalanceNotifyThreshold *float64 `json:"balance_notify_threshold"`
|
||||
}
|
||||
|
||||
// ChangePasswordRequest 修改密码请求
|
||||
@@ -144,11 +143,6 @@ func (s *UserService) UpdateProfile(ctx context.Context, userID int64, req Updat
|
||||
if req.BalanceNotifyEnabled != nil {
|
||||
user.BalanceNotifyEnabled = *req.BalanceNotifyEnabled
|
||||
}
|
||||
if req.BalanceNotifyThresholdType != nil {
|
||||
if *req.BalanceNotifyThresholdType == ThresholdTypeFixed || *req.BalanceNotifyThresholdType == ThresholdTypePercentage {
|
||||
user.BalanceNotifyThresholdType = *req.BalanceNotifyThresholdType
|
||||
}
|
||||
}
|
||||
if req.BalanceNotifyThreshold != nil {
|
||||
if *req.BalanceNotifyThreshold <= 0 {
|
||||
user.BalanceNotifyThreshold = nil // clear to system default
|
||||
|
||||
@@ -137,7 +137,6 @@ export interface SystemSettings {
|
||||
|
||||
// Balance & quota notification
|
||||
balance_low_notify_enabled: boolean
|
||||
balance_low_notify_threshold_type: 'fixed' | 'percentage'
|
||||
balance_low_notify_threshold: number
|
||||
account_quota_notify_emails: string[]
|
||||
}
|
||||
@@ -241,7 +240,6 @@ export interface UpdateSettingsRequest {
|
||||
payment_cancel_rate_limit_window_mode?: string
|
||||
// Balance & quota notification
|
||||
balance_low_notify_enabled?: boolean
|
||||
balance_low_notify_threshold_type?: 'fixed' | 'percentage'
|
||||
balance_low_notify_threshold?: number
|
||||
account_quota_notify_emails?: string[]
|
||||
}
|
||||
|
||||
@@ -4633,12 +4633,8 @@ export default {
|
||||
title: 'Balance Low Notification',
|
||||
description: 'Send email notification when user balance falls below threshold',
|
||||
enabled: 'Enable Balance Low Notification',
|
||||
thresholdType: 'Threshold Type',
|
||||
typeFixed: 'Fixed Amount',
|
||||
typePercentage: 'Percentage of Recharged',
|
||||
threshold: 'Default Threshold',
|
||||
thresholdHint: 'Used when user has not set a custom value',
|
||||
percentageHint: 'Notify when balance falls below this percentage of total recharged amount',
|
||||
thresholdPlaceholder: 'Enter amount',
|
||||
},
|
||||
quotaNotify: {
|
||||
|
||||
@@ -4797,12 +4797,8 @@ export default {
|
||||
title: '余额不足提醒',
|
||||
description: '当用户余额低于阈值时发送邮件提醒',
|
||||
enabled: '启用余额不足提醒',
|
||||
thresholdType: '阈值类型',
|
||||
typeFixed: '固定金额',
|
||||
typePercentage: '充值百分比',
|
||||
threshold: '提醒阈值',
|
||||
threshold: '默认提醒阈值',
|
||||
thresholdHint: '用户未自定义时使用此值',
|
||||
percentageHint: '当余额低于累计充值额的此百分比时提醒',
|
||||
thresholdPlaceholder: '输入金额',
|
||||
},
|
||||
quotaNotify: {
|
||||
|
||||
@@ -2675,43 +2675,13 @@
|
||||
<label class="mb-0 block text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('admin.settings.balanceNotify.enabled') }}</label>
|
||||
<Toggle v-model="form.balance_low_notify_enabled" />
|
||||
</div>
|
||||
<div v-if="form.balance_low_notify_enabled" class="space-y-3">
|
||||
<!-- Threshold type selector -->
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('admin.settings.balanceNotify.thresholdType') }}</label>
|
||||
<div class="flex gap-4">
|
||||
<label class="flex items-center gap-1.5 text-sm cursor-pointer">
|
||||
<input type="radio" v-model="form.balance_low_notify_threshold_type" value="fixed" class="accent-primary-500" />
|
||||
{{ t('admin.settings.balanceNotify.typeFixed') }}
|
||||
</label>
|
||||
<label class="flex items-center gap-1.5 text-sm cursor-pointer">
|
||||
<input type="radio" v-model="form.balance_low_notify_threshold_type" value="percentage" class="accent-primary-500" />
|
||||
{{ t('admin.settings.balanceNotify.typePercentage') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Threshold value -->
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('admin.settings.balanceNotify.threshold') }}</label>
|
||||
<div class="relative">
|
||||
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400">
|
||||
{{ form.balance_low_notify_threshold_type === 'percentage' ? '%' : '$' }}
|
||||
</span>
|
||||
<input
|
||||
v-model.number="form.balance_low_notify_threshold"
|
||||
type="number"
|
||||
:min="0"
|
||||
:max="form.balance_low_notify_threshold_type === 'percentage' ? 100 : undefined"
|
||||
:step="form.balance_low_notify_threshold_type === 'percentage' ? 1 : 0.01"
|
||||
class="input pl-7"
|
||||
/>
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ form.balance_low_notify_threshold_type === 'percentage'
|
||||
? t('admin.settings.balanceNotify.percentageHint')
|
||||
: t('admin.settings.balanceNotify.thresholdHint') }}
|
||||
</p>
|
||||
<div v-if="form.balance_low_notify_enabled">
|
||||
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('admin.settings.balanceNotify.threshold') }}</label>
|
||||
<div class="relative">
|
||||
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400">$</span>
|
||||
<input v-model.number="form.balance_low_notify_threshold" type="number" min="0" step="0.01" class="input pl-7" />
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">{{ t('admin.settings.balanceNotify.thresholdHint') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3026,7 +2996,6 @@ const form = reactive<SettingsForm>({
|
||||
enable_cch_signing: false,
|
||||
// Balance & quota notification
|
||||
balance_low_notify_enabled: false,
|
||||
balance_low_notify_threshold_type: 'fixed' as 'fixed' | 'percentage',
|
||||
balance_low_notify_threshold: 0,
|
||||
account_quota_notify_emails: [] as string[]
|
||||
})
|
||||
@@ -3591,7 +3560,6 @@ async function saveSettings() {
|
||||
payment_cancel_rate_limit_window_mode: form.payment_cancel_rate_limit_window_mode,
|
||||
// Balance & quota notification
|
||||
balance_low_notify_enabled: form.balance_low_notify_enabled,
|
||||
balance_low_notify_threshold_type: form.balance_low_notify_threshold_type,
|
||||
balance_low_notify_threshold: Number(form.balance_low_notify_threshold) || 0,
|
||||
account_quota_notify_emails: (form.account_quota_notify_emails || []).filter((e: string) => e.trim() !== ''),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user