soft_delete.go 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. // Package mixins 提供 Ent schema 的可复用混入组件。
  2. // 包括时间戳混入、软删除混入等通用功能。
  3. package mixins
  4. import (
  5. "context"
  6. "fmt"
  7. "reflect"
  8. "time"
  9. "entgo.io/ent"
  10. "entgo.io/ent/dialect"
  11. "entgo.io/ent/dialect/sql"
  12. "entgo.io/ent/schema/field"
  13. "entgo.io/ent/schema/mixin"
  14. "github.com/Wei-Shaw/sub2api/ent/intercept"
  15. )
  16. // SoftDeleteMixin 实现基于 deleted_at 时间戳的软删除功能。
  17. //
  18. // 软删除特性:
  19. // - 删除操作不会真正删除数据库记录,而是设置 deleted_at 时间戳
  20. // - 所有查询默认自动过滤 deleted_at IS NULL,只返回"未删除"的记录
  21. // - 通过 SkipSoftDelete(ctx) 可以绕过软删除过滤器,查询或真正删除记录
  22. //
  23. // 实现原理:
  24. // - 使用 Ent 的 Interceptor 拦截所有查询,自动添加 deleted_at IS NULL 条件
  25. // - 使用 Ent 的 Hook 拦截删除操作,将 DELETE 转换为 UPDATE SET deleted_at = NOW()
  26. //
  27. // 使用示例:
  28. //
  29. // func (User) Mixin() []ent.Mixin {
  30. // return []ent.Mixin{
  31. // mixins.SoftDeleteMixin{},
  32. // }
  33. // }
  34. type SoftDeleteMixin struct {
  35. mixin.Schema
  36. }
  37. // Fields 定义软删除所需的字段。
  38. // deleted_at 字段:
  39. // - 类型为 TIMESTAMPTZ,精确记录删除时间
  40. // - Optional 和 Nillable 确保新记录时该字段为 NULL
  41. // - NULL 表示记录未被删除,非 NULL 表示已软删除
  42. func (SoftDeleteMixin) Fields() []ent.Field {
  43. return []ent.Field{
  44. field.Time("deleted_at").
  45. Optional().
  46. Nillable().
  47. SchemaType(map[string]string{
  48. dialect.Postgres: "timestamptz",
  49. }),
  50. }
  51. }
  52. // softDeleteKey 是用于在 context 中标记跳过软删除的键类型。
  53. // 使用空结构体作为键可以避免与其他包的键冲突。
  54. type softDeleteKey struct{}
  55. // SkipSoftDelete 返回一个新的 context,用于跳过软删除的拦截器和变更器。
  56. //
  57. // 使用场景:
  58. // - 查询已软删除的记录(如管理员查看回收站)
  59. // - 执行真正的物理删除(如彻底清理数据)
  60. // - 恢复软删除的记录
  61. //
  62. // 示例:
  63. //
  64. // // 查询包含已删除记录的所有用户
  65. // users, err := client.User.Query().All(mixins.SkipSoftDelete(ctx))
  66. //
  67. // // 真正删除记录
  68. // client.User.DeleteOneID(id).Exec(mixins.SkipSoftDelete(ctx))
  69. func SkipSoftDelete(parent context.Context) context.Context {
  70. return context.WithValue(parent, softDeleteKey{}, true)
  71. }
  72. // Interceptors 返回查询拦截器列表。
  73. // 拦截器会自动为所有查询添加 deleted_at IS NULL 条件,
  74. // 确保软删除的记录不会出现在普通查询结果中。
  75. func (d SoftDeleteMixin) Interceptors() []ent.Interceptor {
  76. return []ent.Interceptor{
  77. intercept.TraverseFunc(func(ctx context.Context, q intercept.Query) error {
  78. // 检查是否需要跳过软删除过滤
  79. if skip, _ := ctx.Value(softDeleteKey{}).(bool); skip {
  80. return nil
  81. }
  82. // 为查询添加 deleted_at IS NULL 条件
  83. d.applyPredicate(q)
  84. return nil
  85. }),
  86. }
  87. }
  88. // Hooks 返回变更钩子列表。
  89. // 钩子会拦截 DELETE 操作,将其转换为 UPDATE SET deleted_at = NOW()。
  90. // 这样删除操作实际上只是标记记录为已删除,而不是真正删除。
  91. func (d SoftDeleteMixin) Hooks() []ent.Hook {
  92. return []ent.Hook{
  93. func(next ent.Mutator) ent.Mutator {
  94. return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
  95. // 只处理删除操作
  96. if m.Op() != ent.OpDelete && m.Op() != ent.OpDeleteOne {
  97. return next.Mutate(ctx, m)
  98. }
  99. // 检查是否需要执行真正的删除
  100. if skip, _ := ctx.Value(softDeleteKey{}).(bool); skip {
  101. return next.Mutate(ctx, m)
  102. }
  103. // 类型断言,获取 mutation 的扩展接口
  104. mx, ok := m.(interface {
  105. SetOp(ent.Op)
  106. SetDeletedAt(time.Time)
  107. WhereP(...func(*sql.Selector))
  108. })
  109. if !ok {
  110. return nil, fmt.Errorf("unexpected mutation type %T", m)
  111. }
  112. // 添加软删除过滤条件,确保不会影响已删除的记录
  113. d.applyPredicate(mx)
  114. // 将 DELETE 操作转换为 UPDATE 操作
  115. mx.SetOp(ent.OpUpdate)
  116. // 设置删除时间为当前时间
  117. mx.SetDeletedAt(time.Now())
  118. return mutateWithClient(ctx, m, next)
  119. })
  120. },
  121. }
  122. }
  123. // applyPredicate 为查询添加 deleted_at IS NULL 条件。
  124. // 这是软删除过滤的核心实现。
  125. func (d SoftDeleteMixin) applyPredicate(w interface{ WhereP(...func(*sql.Selector)) }) {
  126. w.WhereP(
  127. sql.FieldIsNull(d.Fields()[0].Descriptor().Name),
  128. )
  129. }
  130. func mutateWithClient(ctx context.Context, m ent.Mutation, fallback ent.Mutator) (ent.Value, error) {
  131. clientMethod := reflect.ValueOf(m).MethodByName("Client")
  132. if !clientMethod.IsValid() || clientMethod.Type().NumIn() != 0 || clientMethod.Type().NumOut() != 1 {
  133. return nil, fmt.Errorf("soft delete: mutation client method not found for %T", m)
  134. }
  135. client := clientMethod.Call(nil)[0]
  136. mutateMethod := client.MethodByName("Mutate")
  137. if !mutateMethod.IsValid() {
  138. return nil, fmt.Errorf("soft delete: mutation client missing Mutate for %T", m)
  139. }
  140. if mutateMethod.Type().NumIn() != 2 || mutateMethod.Type().NumOut() != 2 {
  141. return nil, fmt.Errorf("soft delete: mutation client signature mismatch for %T", m)
  142. }
  143. results := mutateMethod.Call([]reflect.Value{reflect.ValueOf(ctx), reflect.ValueOf(m)})
  144. value := results[0].Interface()
  145. var err error
  146. if !results[1].IsNil() {
  147. errValue := results[1].Interface()
  148. typedErr, ok := errValue.(error)
  149. if !ok {
  150. return nil, fmt.Errorf("soft delete: unexpected error type %T for %T", errValue, m)
  151. }
  152. err = typedErr
  153. }
  154. if err != nil {
  155. return nil, err
  156. }
  157. if value == nil {
  158. return nil, fmt.Errorf("soft delete: mutation client returned nil for %T", m)
  159. }
  160. v, ok := value.(ent.Value)
  161. if !ok {
  162. return nil, fmt.Errorf("soft delete: unexpected value type %T for %T", value, m)
  163. }
  164. return v, nil
  165. }