3. Ent Schema 设计:定义 User、Task 等核心模型
在异步任务认证系统中,Schema 是数据层的契约。Ent 通过 Go 代码定义 Schema,自动生成类型安全的 CRUD 与迁移脚本。本章聚焦 User、Task、Credential 三个核心模型的字段设计、注解策略与索引约束。
3.1 核心模型概览
系统包含三个主要实体,其职责与关系如下表所示:
| 模型 | 职责 | 关联关系 |
|---|---|---|
| User | 系统用户,包含认证信息与状态 | 拥有多个 Credential 和 Task |
| Credential | 第三方平台凭证(如 OAuth2 token) | 属于一个 User |
| Task | 异步任务定义,包含类型、状态与重试策略 | 属于一个 User,可选关联 Credential |
设计原则:每个模型只承载与其生命周期一致的数据。Credential 独立于 User 存储,便于轮换与过期管理;Task 与 Credential 的关联为可选,支持匿名或系统级任务。
3.2 User Schema 详解
User 模型是认证体系的锚点,字段设计需兼顾登录、权限与审计需求。
// ent/schema/user.go
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/field"
"entgo.io/ent/schema/index"
"entgo.io/ent/schema/edge"
)
type User struct {
ent.Schema
}
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("username").
Unique().
MinLen(3).
MaxLen(32).
Comment("登录用户名,全局唯一"),
field.String("email").
Unique().
Comment("邮箱,用于找回密码与通知"),
field.String("password_hash").
Sensitive().
Comment("bcrypt 哈希后的密码"),
field.Enum("status").
Values("active", "inactive", "suspended").
Default("active").
Comment("用户状态"),
field.Time("created_at").
Default(now).
Immutable(),
field.Time("updated_at").
Default(now).
UpdateDefault(now),
}
}
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("credentials", Credential.Type),
edge.To("tasks", Task.Type),
}
}
func (User) Indexes() []ent.Index {
return []ent.Index{
index.Fields("email", "status").
Unique().
Comment("防止同一邮箱重复激活"),
}
}
关键注解:
Sensitive()标记密码字段,Ent 自动在日志与序列化中脱敏。Immutable()确保创建时间创建后不可修改。UpdateDefault(now)使更新时间在每次记录变更时自动刷新。
3.3 Credential Schema:凭证生命周期管理
第三方凭证具有时效性,Schema 需记录令牌、刷新令牌与过期时间。
// ent/schema/credential.go
type Credential struct {
ent.Schema
}
func (Credential) Fields() []ent.Field {
return []ent.Field{
field.String("platform").
Comment("平台标识,如 github、google"),
field.String("access_token").
Sensitive().
MaxLen(2048).
Comment("OAuth2 访问令牌"),
field.String("refresh_token").
Sensitive().
MaxLen(2048).
Optional().
Nillable().
Comment("刷新令牌,可选"),
field.Time("expires_at").
Optional().
Nillable().
Comment("令牌过期时间"),
field.Time("created_at").
Default(now).
Immutable(),
}
}
func (Credential) Edges() []ent.Edge {
return []ent.Edge{
edge.From("owner", User.Type).
Ref("credentials").
Unique().
Required(),
}
}
func (Credential) Indexes() []ent.Index {
return []ent.Index{
index.Fields("platform", "expires_at").
Comment("按平台与过期时间筛选过期凭证"),
}
}
避坑提示:refresh_token 使用
Optional().Nillable() 而非 Default(""),避免空字符串与 null 语义混淆。过期时间使用 Nillable() 支持永不过期的内部令牌。
3.4 Task Schema:异步任务核心
Task 是系统的执行单元,字段需覆盖任务类型、状态机、重试策略与上下文。
// ent/schema/task.go
type Task struct {
ent.Schema
}
func (Task) Fields() []ent.Field {
return []ent.Field{
field.String("type").
Comment("任务类型,如 send_email、sync_data"),
field.Enum("status").
Values("pending", "running", "success", "failed", "cancelled").
Default("pending").
Comment("任务状态"),
field.JSON("payload").
Comment("任务输入参数,JSON 格式"),
field.JSON("result").
Optional().
Comment("任务执行结果,JSON 格式"),
field.Int("retry_count").
Default(0).
Comment("已重试次数"),
field.Int("max_retries").
Default(3).
Comment("最大重试次数"),
field.Time("scheduled_at").
Optional().
Nillable().
Comment("计划执行时间,为空则立即执行"),
field.Time("started_at").
Optional().
Nillable().
Comment("实际开始时间"),
field.Time("completed_at").
Optional().
Nillable().
Comment("完成时间"),
field.Time("created_at").
Default(now).
Immutable(),
}
}
func (Task) Edges() []ent.Edge {
return []ent.Edge{
edge.From("owner", User.Type).
Ref("tasks").
Unique().
Required(),
edge.From("credential", Credential.Type).
Ref("tasks").
Unique().
Optional(),
}
}
func (Task) Indexes() []ent.Index {
return []ent.Index{
index.Fields("status", "scheduled_at").
Comment("调度器按状态与计划时间拉取任务"),
index.Fields("type", "status").
Comment("按类型与状态统计任务分布"),
}
}
3.5 模型关系与索引策略
下图展示了三个模型之间的关联关系与关键索引路径:
3.6 字段类型与注解对照
Ent 提供丰富的字段类型与注解,以下为本章使用的核心组合:
| 字段类型 | 适用场景 | 常用注解 |
|---|---|---|
field.String |
用户名、邮箱、令牌 | Unique()、Sensitive()、MinLen() |
field.Enum |
状态字段(status) | Values()、Default() |
field.JSON |
任务 payload / result | Optional() |
field.Time |
时间戳字段 | Default()、Immutable()、UpdateDefault() |
field.Int |
重试计数、最大重试 | Default()、Positive() |
3.7 索引与唯一约束最佳实践
索引设计直接影响查询性能与数据完整性。以下是本系统的索引策略总结:
- 唯一索引:
username、email各自独立唯一,确保登录标识不冲突。 - 联合唯一索引:
email + status防止同一邮箱存在多个激活账户。 - 复合索引:
status + scheduled_at是任务调度器的核心查询路径,必须覆盖。 - 过滤索引:
platform + expires_at用于定时清理过期凭证,减少全表扫描。 - 避免冗余:不要为
created_at单独建索引,除非有范围查询硬需求。
迁移建议:在生产环境添加索引时,使用 Ent 的
Atlas 迁移模式,生成增量 SQL 而非全量重建。对于大表,考虑 CONCURRENTLY 选项避免锁表。
3.8 Schema 设计检查清单
完成 Schema 定义后,逐项核对以下要点:
- 所有敏感字段(密码、令牌)已标记
Sensitive()。 - 时间戳字段正确使用
Immutable()与UpdateDefault()。 - 枚举状态值覆盖完整生命周期,无遗漏分支。
- JSON 字段明确标注
Optional(),避免非空约束导致写入失败。 - 索引覆盖所有高频查询 WHERE 条件,且复合索引遵循最左前缀原则。
- 边缘(Edge)关系方向正确,
Required()与Optional()语义准确。
完成上述 Schema 定义后,运行 go generate ./ent 即可生成类型安全的客户端代码,为后续的认证流程与异步任务调度奠定数据基础。