// Package mixins 提供 Ent schema 的可复用混入组件。 // 包括时间戳混入、软删除混入等通用功能。 package mixins import ( "context" "fmt" "time" "entgo.io/ent" "entgo.io/ent/dialect" "entgo.io/ent/dialect/sql" "entgo.io/ent/schema/field" "entgo.io/ent/schema/mixin" dbent "github.com/Wei-Shaw/sub2api/ent" "github.com/Wei-Shaw/sub2api/ent/intercept" ) // SoftDeleteMixin 实现基于 deleted_at 时间戳的软删除功能。 // // 软删除特性: // - 删除操作不会真正删除数据库记录,而是设置 deleted_at 时间戳 // - 所有查询默认自动过滤 deleted_at IS NULL,只返回"未删除"的记录 // - 通过 SkipSoftDelete(ctx) 可以绕过软删除过滤器,查询或真正删除记录 // // 实现原理: // - 使用 Ent 的 Interceptor 拦截所有查询,自动添加 deleted_at IS NULL 条件 // - 使用 Ent 的 Hook 拦截删除操作,将 DELETE 转换为 UPDATE SET deleted_at = NOW() // // 使用示例: // // func (User) Mixin() []ent.Mixin { // return []ent.Mixin{ // mixins.SoftDeleteMixin{}, // } // } type SoftDeleteMixin struct { mixin.Schema } // Fields 定义软删除所需的字段。 // deleted_at 字段: // - 类型为 TIMESTAMPTZ,精确记录删除时间 // - Optional 和 Nillable 确保新记录时该字段为 NULL // - NULL 表示记录未被删除,非 NULL 表示已软删除 func (SoftDeleteMixin) Fields() []ent.Field { return []ent.Field{ field.Time("deleted_at"). Optional(). Nillable(). SchemaType(map[string]string{ dialect.Postgres: "timestamptz", }), } } // softDeleteKey 是用于在 context 中标记跳过软删除的键类型。 // 使用空结构体作为键可以避免与其他包的键冲突。 type softDeleteKey struct{} // SkipSoftDelete 返回一个新的 context,用于跳过软删除的拦截器和变更器。 // // 使用场景: // - 查询已软删除的记录(如管理员查看回收站) // - 执行真正的物理删除(如彻底清理数据) // - 恢复软删除的记录 // // 示例: // // // 查询包含已删除记录的所有用户 // users, err := client.User.Query().All(mixins.SkipSoftDelete(ctx)) // // // 真正删除记录 // client.User.DeleteOneID(id).Exec(mixins.SkipSoftDelete(ctx)) func SkipSoftDelete(parent context.Context) context.Context { return context.WithValue(parent, softDeleteKey{}, true) } // Interceptors 返回查询拦截器列表。 // 拦截器会自动为所有查询添加 deleted_at IS NULL 条件, // 确保软删除的记录不会出现在普通查询结果中。 func (d SoftDeleteMixin) Interceptors() []ent.Interceptor { return []ent.Interceptor{ intercept.TraverseFunc(func(ctx context.Context, q intercept.Query) error { // 检查是否需要跳过软删除过滤 if skip, _ := ctx.Value(softDeleteKey{}).(bool); skip { return nil } // 为查询添加 deleted_at IS NULL 条件 d.applyPredicate(q) return nil }), } } // Hooks 返回变更钩子列表。 // 钩子会拦截 DELETE 操作,将其转换为 UPDATE SET deleted_at = NOW()。 // 这样删除操作实际上只是标记记录为已删除,而不是真正删除。 func (d SoftDeleteMixin) Hooks() []ent.Hook { return []ent.Hook{ func(next ent.Mutator) ent.Mutator { return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) { // 只处理删除操作 if m.Op() != ent.OpDelete && m.Op() != ent.OpDeleteOne { return next.Mutate(ctx, m) } // 检查是否需要执行真正的删除 if skip, _ := ctx.Value(softDeleteKey{}).(bool); skip { return next.Mutate(ctx, m) } // 类型断言,获取 mutation 的扩展接口 mx, ok := m.(interface { SetOp(ent.Op) SetDeletedAt(time.Time) WhereP(...func(*sql.Selector)) Client() *dbent.Client }) if !ok { return nil, fmt.Errorf("unexpected mutation type %T", m) } // 添加软删除过滤条件,确保不会影响已删除的记录 d.applyPredicate(mx) // 将 DELETE 操作转换为 UPDATE 操作 mx.SetOp(ent.OpUpdate) // 设置删除时间为当前时间 mx.SetDeletedAt(time.Now()) return mx.Client().Mutate(ctx, m) }) }, } } // applyPredicate 为查询添加 deleted_at IS NULL 条件。 // 这是软删除过滤的核心实现。 func (d SoftDeleteMixin) applyPredicate(w interface{ WhereP(...func(*sql.Selector)) }) { w.WhereP( sql.FieldIsNull(d.Fields()[0].Descriptor().Name), ) }