sub2api/backend/ent/schema/user_platform_quota.go
DaydreamCoding 6b39b344d8 feat(quota): 用户 × 平台 USD 配额
为用户在 anthropic/openai/gemini/antigravity 四个平台上提供日/周/月
三个窗口的 USD 配额管控。配额语义:未设置=不限制,0=禁用,>0=美元上限。

两层模型:
- 配置层:系统默认配额,以及 email/linuxdo/oidc/wechat/github/google/
  dingtalk 七个鉴权来源的默认配额,存于 settings,以嵌套 JSON 整体读写
  (系统 1 个 key + 每个来源 1 个 key),整体替换语义。
- 运行时层:user_platform_quota 表按用户记录实际配额,与配置层解耦。

后端:新增 ent schema 与 140_user_platform_quotas.sql 迁移、repository
与 service 端口、计费链路集成、管理端与用户端读写接口。
前端:管理端设置页配额编辑、用户配额管理 Modal、用户 Dashboard 展示、
中英文案。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 10:49:20 +08:00

114 lines
3.1 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.

package schema
import (
"fmt"
"entgo.io/ent"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/entsql"
"entgo.io/ent/schema"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
"entgo.io/ent/schema/index"
"github.com/Wei-Shaw/sub2api/ent/schema/mixins"
)
// UserPlatformQuota holds the schema definition for per-user per-platform quota.
type UserPlatformQuota struct {
ent.Schema
}
func (UserPlatformQuota) Annotations() []schema.Annotation {
return []schema.Annotation{
entsql.Annotation{Table: "user_platform_quotas"},
}
}
func (UserPlatformQuota) Mixin() []ent.Mixin {
return []ent.Mixin{
mixins.TimeMixin{},
mixins.SoftDeleteMixin{},
}
}
func (UserPlatformQuota) Fields() []ent.Field {
return []ent.Field{
field.Int64("user_id"),
field.String("platform").
MaxLen(32).
NotEmpty().
Validate(func(s string) error {
// 注意:平台列表的单一权威源为 service.AllowedQuotaPlatforms
// 此处为 ent 构建期约束,需与 service.AllowedQuotaPlatforms 保持同步。
switch s {
case "anthropic", "openai", "gemini", "antigravity":
return nil
default:
return fmt.Errorf("platform %q is not allowed", s)
}
}),
// 日 / 周 / 月 USD 上限:
// nil / not set → 无限额(完全放行)
// 0 → 完全禁用(任何请求都会被拒绝,因为 usage >= 0 恒成立)
// > 0 → USD 限额上限
field.Float("daily_limit_usd").
Optional().
Nillable().
SchemaType(map[string]string{dialect.Postgres: "decimal(20,10)"}),
field.Float("weekly_limit_usd").
Optional().
Nillable().
SchemaType(map[string]string{dialect.Postgres: "decimal(20,10)"}),
field.Float("monthly_limit_usd").
Optional().
Nillable().
SchemaType(map[string]string{dialect.Postgres: "decimal(20,10)"}),
// 当前窗口已用量USDpreflight 时与 limit 比较)
field.Float("daily_usage_usd").
Default(0).
SchemaType(map[string]string{dialect.Postgres: "decimal(20,10)"}),
field.Float("weekly_usage_usd").
Default(0).
SchemaType(map[string]string{dialect.Postgres: "decimal(20,10)"}),
field.Float("monthly_usage_usd").
Default(0).
SchemaType(map[string]string{dialect.Postgres: "decimal(20,10)"}),
// 窗口起点NULL = 首次还未初始化,由 InitWindowStarts 用 COALESCE 兜底)
field.Time("daily_window_start").
Optional().
Nillable().
SchemaType(map[string]string{dialect.Postgres: "timestamptz"}),
field.Time("weekly_window_start").
Optional().
Nillable().
SchemaType(map[string]string{dialect.Postgres: "timestamptz"}),
field.Time("monthly_window_start").
Optional().
Nillable().
SchemaType(map[string]string{dialect.Postgres: "timestamptz"}),
}
}
func (UserPlatformQuota) Edges() []ent.Edge {
return []ent.Edge{
edge.From("user", User.Type).
Ref("platform_quotas").
Field("user_id").
Unique().
Required(),
}
}
func (UserPlatformQuota) Indexes() []ent.Index {
return []ent.Index{
// 软删除友好:只对未删记录唯一
index.Fields("user_id", "platform").
Unique().
Annotations(entsql.IndexWhere("deleted_at IS NULL")),
index.Fields("user_id"),
}
}