feat(notify): add balance low & account quota notification system

- User balance low notification: email alert when balance drops below
  configurable threshold (user email + verified extra emails)
- Account quota notification: broadcast email to admin-configured
  recipients when daily/weekly/total quota usage exceeds alert threshold
- Admin settings: global enable/disable, default threshold, quota
  notification email list (Email Settings tab)
- User profile: enable/disable, custom threshold, add/remove extra
  notification emails with verification code flow
- Account quota: per-dimension alert toggle and threshold in quota
  control card
- Trigger logic: first-crossing only (old >= threshold && new < threshold
  for balance; old < threshold && new >= threshold for quota), naturally
  prevents duplicate notifications without Redis dedup
This commit is contained in:
erio
2026-04-12 02:48:57 +08:00
parent 60b0fa81ec
commit b32d1a2c9f
47 changed files with 2375 additions and 121 deletions

View File

@@ -243,6 +243,61 @@ func (_u *UserUpdate) ClearTotpEnabledAt() *UserUpdate {
return _u
}
// SetBalanceNotifyEnabled sets the "balance_notify_enabled" field.
func (_u *UserUpdate) SetBalanceNotifyEnabled(v bool) *UserUpdate {
_u.mutation.SetBalanceNotifyEnabled(v)
return _u
}
// SetNillableBalanceNotifyEnabled sets the "balance_notify_enabled" field if the given value is not nil.
func (_u *UserUpdate) SetNillableBalanceNotifyEnabled(v *bool) *UserUpdate {
if v != nil {
_u.SetBalanceNotifyEnabled(*v)
}
return _u
}
// SetBalanceNotifyThreshold sets the "balance_notify_threshold" field.
func (_u *UserUpdate) SetBalanceNotifyThreshold(v float64) *UserUpdate {
_u.mutation.ResetBalanceNotifyThreshold()
_u.mutation.SetBalanceNotifyThreshold(v)
return _u
}
// SetNillableBalanceNotifyThreshold sets the "balance_notify_threshold" field if the given value is not nil.
func (_u *UserUpdate) SetNillableBalanceNotifyThreshold(v *float64) *UserUpdate {
if v != nil {
_u.SetBalanceNotifyThreshold(*v)
}
return _u
}
// AddBalanceNotifyThreshold adds value to the "balance_notify_threshold" field.
func (_u *UserUpdate) AddBalanceNotifyThreshold(v float64) *UserUpdate {
_u.mutation.AddBalanceNotifyThreshold(v)
return _u
}
// ClearBalanceNotifyThreshold clears the value of the "balance_notify_threshold" field.
func (_u *UserUpdate) ClearBalanceNotifyThreshold() *UserUpdate {
_u.mutation.ClearBalanceNotifyThreshold()
return _u
}
// SetBalanceNotifyExtraEmails sets the "balance_notify_extra_emails" field.
func (_u *UserUpdate) SetBalanceNotifyExtraEmails(v string) *UserUpdate {
_u.mutation.SetBalanceNotifyExtraEmails(v)
return _u
}
// SetNillableBalanceNotifyExtraEmails sets the "balance_notify_extra_emails" field if the given value is not nil.
func (_u *UserUpdate) SetNillableBalanceNotifyExtraEmails(v *string) *UserUpdate {
if v != nil {
_u.SetBalanceNotifyExtraEmails(*v)
}
return _u
}
// AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by IDs.
func (_u *UserUpdate) AddAPIKeyIDs(ids ...int64) *UserUpdate {
_u.mutation.AddAPIKeyIDs(ids...)
@@ -746,6 +801,21 @@ func (_u *UserUpdate) sqlSave(ctx context.Context) (_node int, err error) {
if _u.mutation.TotpEnabledAtCleared() {
_spec.ClearField(user.FieldTotpEnabledAt, field.TypeTime)
}
if value, ok := _u.mutation.BalanceNotifyEnabled(); ok {
_spec.SetField(user.FieldBalanceNotifyEnabled, field.TypeBool, value)
}
if value, ok := _u.mutation.BalanceNotifyThreshold(); ok {
_spec.SetField(user.FieldBalanceNotifyThreshold, field.TypeFloat64, value)
}
if value, ok := _u.mutation.AddedBalanceNotifyThreshold(); ok {
_spec.AddField(user.FieldBalanceNotifyThreshold, field.TypeFloat64, value)
}
if _u.mutation.BalanceNotifyThresholdCleared() {
_spec.ClearField(user.FieldBalanceNotifyThreshold, field.TypeFloat64)
}
if value, ok := _u.mutation.BalanceNotifyExtraEmails(); ok {
_spec.SetField(user.FieldBalanceNotifyExtraEmails, field.TypeString, value)
}
if _u.mutation.APIKeysCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,
@@ -1434,6 +1504,61 @@ func (_u *UserUpdateOne) ClearTotpEnabledAt() *UserUpdateOne {
return _u
}
// SetBalanceNotifyEnabled sets the "balance_notify_enabled" field.
func (_u *UserUpdateOne) SetBalanceNotifyEnabled(v bool) *UserUpdateOne {
_u.mutation.SetBalanceNotifyEnabled(v)
return _u
}
// SetNillableBalanceNotifyEnabled sets the "balance_notify_enabled" field if the given value is not nil.
func (_u *UserUpdateOne) SetNillableBalanceNotifyEnabled(v *bool) *UserUpdateOne {
if v != nil {
_u.SetBalanceNotifyEnabled(*v)
}
return _u
}
// SetBalanceNotifyThreshold sets the "balance_notify_threshold" field.
func (_u *UserUpdateOne) SetBalanceNotifyThreshold(v float64) *UserUpdateOne {
_u.mutation.ResetBalanceNotifyThreshold()
_u.mutation.SetBalanceNotifyThreshold(v)
return _u
}
// SetNillableBalanceNotifyThreshold sets the "balance_notify_threshold" field if the given value is not nil.
func (_u *UserUpdateOne) SetNillableBalanceNotifyThreshold(v *float64) *UserUpdateOne {
if v != nil {
_u.SetBalanceNotifyThreshold(*v)
}
return _u
}
// AddBalanceNotifyThreshold adds value to the "balance_notify_threshold" field.
func (_u *UserUpdateOne) AddBalanceNotifyThreshold(v float64) *UserUpdateOne {
_u.mutation.AddBalanceNotifyThreshold(v)
return _u
}
// ClearBalanceNotifyThreshold clears the value of the "balance_notify_threshold" field.
func (_u *UserUpdateOne) ClearBalanceNotifyThreshold() *UserUpdateOne {
_u.mutation.ClearBalanceNotifyThreshold()
return _u
}
// SetBalanceNotifyExtraEmails sets the "balance_notify_extra_emails" field.
func (_u *UserUpdateOne) SetBalanceNotifyExtraEmails(v string) *UserUpdateOne {
_u.mutation.SetBalanceNotifyExtraEmails(v)
return _u
}
// SetNillableBalanceNotifyExtraEmails sets the "balance_notify_extra_emails" field if the given value is not nil.
func (_u *UserUpdateOne) SetNillableBalanceNotifyExtraEmails(v *string) *UserUpdateOne {
if v != nil {
_u.SetBalanceNotifyExtraEmails(*v)
}
return _u
}
// AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by IDs.
func (_u *UserUpdateOne) AddAPIKeyIDs(ids ...int64) *UserUpdateOne {
_u.mutation.AddAPIKeyIDs(ids...)
@@ -1967,6 +2092,21 @@ func (_u *UserUpdateOne) sqlSave(ctx context.Context) (_node *User, err error) {
if _u.mutation.TotpEnabledAtCleared() {
_spec.ClearField(user.FieldTotpEnabledAt, field.TypeTime)
}
if value, ok := _u.mutation.BalanceNotifyEnabled(); ok {
_spec.SetField(user.FieldBalanceNotifyEnabled, field.TypeBool, value)
}
if value, ok := _u.mutation.BalanceNotifyThreshold(); ok {
_spec.SetField(user.FieldBalanceNotifyThreshold, field.TypeFloat64, value)
}
if value, ok := _u.mutation.AddedBalanceNotifyThreshold(); ok {
_spec.AddField(user.FieldBalanceNotifyThreshold, field.TypeFloat64, value)
}
if _u.mutation.BalanceNotifyThresholdCleared() {
_spec.ClearField(user.FieldBalanceNotifyThreshold, field.TypeFloat64)
}
if value, ok := _u.mutation.BalanceNotifyExtraEmails(); ok {
_spec.SetField(user.FieldBalanceNotifyExtraEmails, field.TypeString, value)
}
if _u.mutation.APIKeysCleared() {
edge := &sqlgraph.EdgeSpec{
Rel: sqlgraph.O2M,