Files
xinghuoapi/backend/ent/schema/user_subscription.go
yangjianbo 59269dc1c1 fix(数据层): 修复软删除与唯一约束冲突问题
问题:软删除的记录仍占用唯一约束位置,导致删后无法重建同名/同邮箱/同订阅

修复方案:使用 PostgreSQL 部分唯一索引(WHERE deleted_at IS NULL)
- User.email: 移除字段级 Unique(),改用部分唯一索引
- Group.name: 移除字段级 Unique(),改用部分唯一索引
- UserSubscription.(user_id, group_id): 移除组合唯一索引,改用部分唯一索引
- ApiKey.key: 保留普通唯一约束(安全考虑,已删除的 Key 不应重用)

安全性:
- 应用层已有 ExistsByXxx 检查,自动过滤软删除记录
- 数据库层部分唯一索引提供最后一道防线

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 16:37:18 +08:00

118 lines
3.0 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 (
"time"
"github.com/Wei-Shaw/sub2api/ent/schema/mixins"
"github.com/Wei-Shaw/sub2api/internal/service"
"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"
)
// UserSubscription holds the schema definition for the UserSubscription entity.
type UserSubscription struct {
ent.Schema
}
func (UserSubscription) Annotations() []schema.Annotation {
return []schema.Annotation{
entsql.Annotation{Table: "user_subscriptions"},
}
}
func (UserSubscription) Mixin() []ent.Mixin {
return []ent.Mixin{
mixins.TimeMixin{},
mixins.SoftDeleteMixin{},
}
}
func (UserSubscription) Fields() []ent.Field {
return []ent.Field{
field.Int64("user_id"),
field.Int64("group_id"),
field.Time("starts_at").
SchemaType(map[string]string{dialect.Postgres: "timestamptz"}),
field.Time("expires_at").
SchemaType(map[string]string{dialect.Postgres: "timestamptz"}),
field.String("status").
MaxLen(20).
Default(service.SubscriptionStatusActive),
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"}),
field.Float("daily_usage_usd").
SchemaType(map[string]string{dialect.Postgres: "decimal(20,10)"}).
Default(0),
field.Float("weekly_usage_usd").
SchemaType(map[string]string{dialect.Postgres: "decimal(20,10)"}).
Default(0),
field.Float("monthly_usage_usd").
SchemaType(map[string]string{dialect.Postgres: "decimal(20,10)"}).
Default(0),
field.Int64("assigned_by").
Optional().
Nillable(),
field.Time("assigned_at").
Default(time.Now).
SchemaType(map[string]string{dialect.Postgres: "timestamptz"}),
field.String("notes").
Optional().
Nillable().
SchemaType(map[string]string{dialect.Postgres: "text"}),
}
}
func (UserSubscription) Edges() []ent.Edge {
return []ent.Edge{
edge.From("user", User.Type).
Ref("subscriptions").
Field("user_id").
Unique().
Required(),
edge.From("group", Group.Type).
Ref("subscriptions").
Field("group_id").
Unique().
Required(),
edge.From("assigned_by_user", User.Type).
Ref("assigned_subscriptions").
Field("assigned_by").
Unique(),
edge.To("usage_logs", UsageLog.Type),
}
}
func (UserSubscription) Indexes() []ent.Index {
return []ent.Index{
index.Fields("user_id"),
index.Fields("group_id"),
index.Fields("status"),
index.Fields("expires_at"),
index.Fields("assigned_by"),
// 唯一约束通过部分索引实现WHERE deleted_at IS NULL支持软删除后重新订阅
// 见迁移文件 016_soft_delete_partial_unique_indexes.sql
index.Fields("user_id", "group_id"),
index.Fields("deleted_at"),
}
}