feat(rpm): RPM 限流模块优化

P0:
- rpm_override 嵌入 Auth Cache Snapshot,消除每请求 DB 查询 (snapshot v6→v7)
- 429 RPM 响应返回 Retry-After 头(当前分钟剩余秒数)

P1:
- ClearAll 按钮直连 DELETE API,带 loading 防重复
- 新增 GET /admin/users/:id/rpm-status 管理员 RPM 用量查询端点

优化:
- checkRPM 从级联互斥改为并行取最严,user.rpm_limit 作为全局硬上限始终生效
- Override/Group 变更后自动失效 auth cache
- fail-open 语义不变,Redis 故障不阻塞业务
This commit is contained in:
james-6-23
2026-04-23 03:33:52 +08:00
parent ef967d8f8a
commit dc5d42addc
79 changed files with 2831 additions and 140 deletions

View File

@@ -59,6 +59,8 @@ const (
FieldBalanceNotifyExtraEmails = "balance_notify_extra_emails"
// FieldTotalRecharged holds the string denoting the total_recharged field in the database.
FieldTotalRecharged = "total_recharged"
// FieldRpmLimit holds the string denoting the rpm_limit field in the database.
FieldRpmLimit = "rpm_limit"
// EdgeAPIKeys holds the string denoting the api_keys edge name in mutations.
EdgeAPIKeys = "api_keys"
// EdgeRedeemCodes holds the string denoting the redeem_codes edge name in mutations.
@@ -203,6 +205,7 @@ var Columns = []string{
FieldBalanceNotifyThreshold,
FieldBalanceNotifyExtraEmails,
FieldTotalRecharged,
FieldRpmLimit,
}
var (
@@ -271,6 +274,8 @@ var (
DefaultBalanceNotifyExtraEmails string
// DefaultTotalRecharged holds the default value on creation for the "total_recharged" field.
DefaultTotalRecharged float64
// DefaultRpmLimit holds the default value on creation for the "rpm_limit" field.
DefaultRpmLimit int
)
// OrderOption defines the ordering options for the User queries.
@@ -391,6 +396,11 @@ func ByTotalRecharged(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldTotalRecharged, opts...).ToFunc()
}
// ByRpmLimit orders the results by the rpm_limit field.
func ByRpmLimit(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldRpmLimit, opts...).ToFunc()
}
// ByAPIKeysCount orders the results by api_keys count.
func ByAPIKeysCount(opts ...sql.OrderTermOption) OrderOption {
return func(s *sql.Selector) {

View File

@@ -165,6 +165,11 @@ func TotalRecharged(v float64) predicate.User {
return predicate.User(sql.FieldEQ(FieldTotalRecharged, v))
}
// RpmLimit applies equality check predicate on the "rpm_limit" field. It's identical to RpmLimitEQ.
func RpmLimit(v int) predicate.User {
return predicate.User(sql.FieldEQ(FieldRpmLimit, v))
}
// CreatedAtEQ applies the EQ predicate on the "created_at" field.
func CreatedAtEQ(v time.Time) predicate.User {
return predicate.User(sql.FieldEQ(FieldCreatedAt, v))
@@ -1295,6 +1300,46 @@ func TotalRechargedLTE(v float64) predicate.User {
return predicate.User(sql.FieldLTE(FieldTotalRecharged, v))
}
// RpmLimitEQ applies the EQ predicate on the "rpm_limit" field.
func RpmLimitEQ(v int) predicate.User {
return predicate.User(sql.FieldEQ(FieldRpmLimit, v))
}
// RpmLimitNEQ applies the NEQ predicate on the "rpm_limit" field.
func RpmLimitNEQ(v int) predicate.User {
return predicate.User(sql.FieldNEQ(FieldRpmLimit, v))
}
// RpmLimitIn applies the In predicate on the "rpm_limit" field.
func RpmLimitIn(vs ...int) predicate.User {
return predicate.User(sql.FieldIn(FieldRpmLimit, vs...))
}
// RpmLimitNotIn applies the NotIn predicate on the "rpm_limit" field.
func RpmLimitNotIn(vs ...int) predicate.User {
return predicate.User(sql.FieldNotIn(FieldRpmLimit, vs...))
}
// RpmLimitGT applies the GT predicate on the "rpm_limit" field.
func RpmLimitGT(v int) predicate.User {
return predicate.User(sql.FieldGT(FieldRpmLimit, v))
}
// RpmLimitGTE applies the GTE predicate on the "rpm_limit" field.
func RpmLimitGTE(v int) predicate.User {
return predicate.User(sql.FieldGTE(FieldRpmLimit, v))
}
// RpmLimitLT applies the LT predicate on the "rpm_limit" field.
func RpmLimitLT(v int) predicate.User {
return predicate.User(sql.FieldLT(FieldRpmLimit, v))
}
// RpmLimitLTE applies the LTE predicate on the "rpm_limit" field.
func RpmLimitLTE(v int) predicate.User {
return predicate.User(sql.FieldLTE(FieldRpmLimit, v))
}
// HasAPIKeys applies the HasEdge predicate on the "api_keys" edge.
func HasAPIKeys() predicate.User {
return predicate.User(func(s *sql.Selector) {