user.go 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. package schema
  2. import (
  3. "fmt"
  4. "github.com/Wei-Shaw/sub2api/ent/schema/mixins"
  5. "github.com/Wei-Shaw/sub2api/internal/domain"
  6. "entgo.io/ent"
  7. "entgo.io/ent/dialect"
  8. "entgo.io/ent/dialect/entsql"
  9. "entgo.io/ent/schema"
  10. "entgo.io/ent/schema/edge"
  11. "entgo.io/ent/schema/field"
  12. "entgo.io/ent/schema/index"
  13. )
  14. // User holds the schema definition for the User entity.
  15. type User struct {
  16. ent.Schema
  17. }
  18. func (User) Annotations() []schema.Annotation {
  19. return []schema.Annotation{
  20. entsql.Annotation{Table: "users"},
  21. }
  22. }
  23. func (User) Mixin() []ent.Mixin {
  24. return []ent.Mixin{
  25. mixins.TimeMixin{},
  26. mixins.SoftDeleteMixin{},
  27. }
  28. }
  29. func (User) Fields() []ent.Field {
  30. return []ent.Field{
  31. // 唯一约束通过部分索引实现(WHERE deleted_at IS NULL),支持软删除后重用
  32. // 见迁移文件 016_soft_delete_partial_unique_indexes.sql
  33. field.String("email").
  34. MaxLen(255).
  35. NotEmpty(),
  36. field.String("password_hash").
  37. MaxLen(255).
  38. NotEmpty(),
  39. field.String("role").
  40. MaxLen(20).
  41. Default(domain.RoleUser),
  42. field.Float("balance").
  43. SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}).
  44. Default(0),
  45. field.Float("frozen_balance").
  46. SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}).
  47. Default(0),
  48. field.Int("concurrency").
  49. Default(5),
  50. field.String("status").
  51. MaxLen(20).
  52. Default(domain.StatusActive),
  53. // Optional profile fields (added later; default '' in DB migration)
  54. field.String("username").
  55. MaxLen(100).
  56. Default(""),
  57. // wechat field migrated to user_attribute_values (see migration 019)
  58. field.String("notes").
  59. SchemaType(map[string]string{dialect.Postgres: "text"}).
  60. Default(""),
  61. // TOTP 双因素认证字段
  62. field.String("totp_secret_encrypted").
  63. SchemaType(map[string]string{dialect.Postgres: "text"}).
  64. Optional().
  65. Nillable(),
  66. field.Bool("totp_enabled").
  67. Default(false),
  68. field.Time("totp_enabled_at").
  69. Optional().
  70. Nillable(),
  71. field.String("signup_source").
  72. Validate(func(value string) error {
  73. switch value {
  74. case "email", "linuxdo", "wechat", "oidc", "github", "google", "dingtalk":
  75. return nil
  76. default:
  77. return fmt.Errorf("must be one of email, linuxdo, wechat, oidc, github, google, dingtalk")
  78. }
  79. }).
  80. Default("email"),
  81. field.Time("last_login_at").
  82. Optional().
  83. Nillable().
  84. SchemaType(map[string]string{dialect.Postgres: "timestamptz"}),
  85. field.Time("last_active_at").
  86. Optional().
  87. Nillable().
  88. SchemaType(map[string]string{dialect.Postgres: "timestamptz"}),
  89. // 公开分组访问限制:为 false 时用户可绑定任意非专属分组(默认行为),
  90. // 为 true 时仅可绑定 user_allowed_groups 中列出的公开分组。
  91. field.Bool("restrict_public_groups").
  92. Default(false),
  93. // 余额不足通知
  94. field.Bool("balance_notify_enabled").
  95. Default(true),
  96. field.String("balance_notify_threshold_type").
  97. Default("fixed"), // "fixed" | "percentage"
  98. field.Float("balance_notify_threshold").
  99. SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}).
  100. Optional().
  101. Nillable(),
  102. field.String("balance_notify_extra_emails").
  103. SchemaType(map[string]string{dialect.Postgres: "text"}).
  104. Default("[]"),
  105. field.Float("total_recharged").
  106. SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}).
  107. Default(0),
  108. // 用户级每分钟请求数上限(0 = 不限制)。仅当所在分组未设置 rpm_limit 时作为兜底生效。
  109. field.Int("rpm_limit").
  110. Default(0),
  111. }
  112. }
  113. func (User) Edges() []ent.Edge {
  114. return []ent.Edge{
  115. edge.To("api_keys", APIKey.Type),
  116. edge.To("redeem_codes", RedeemCode.Type),
  117. edge.To("subscriptions", UserSubscription.Type),
  118. edge.To("assigned_subscriptions", UserSubscription.Type),
  119. edge.To("announcement_reads", AnnouncementRead.Type),
  120. edge.To("allowed_groups", Group.Type).
  121. Through("user_allowed_groups", UserAllowedGroup.Type),
  122. edge.To("usage_logs", UsageLog.Type),
  123. edge.To("attribute_values", UserAttributeValue.Type),
  124. edge.To("promo_code_usages", PromoCodeUsage.Type),
  125. edge.To("payment_orders", PaymentOrder.Type),
  126. edge.To("auth_identities", AuthIdentity.Type).
  127. Annotations(entsql.OnDelete(entsql.Cascade)),
  128. edge.To("pending_auth_sessions", PendingAuthSession.Type),
  129. edge.To("platform_quotas", UserPlatformQuota.Type),
  130. }
  131. }
  132. func (User) Indexes() []ent.Index {
  133. return []ent.Index{
  134. // email 字段已在 Fields() 中声明 Unique(),无需重复索引
  135. index.Fields("status"),
  136. index.Fields("deleted_at"),
  137. }
  138. }