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>
This commit is contained in:
@@ -204,7 +204,7 @@ var (
|
||||
{Name: "created_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
|
||||
{Name: "updated_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
|
||||
{Name: "deleted_at", Type: field.TypeTime, Nullable: true, SchemaType: map[string]string{"postgres": "timestamptz"}},
|
||||
{Name: "name", Type: field.TypeString, Unique: true, Size: 100},
|
||||
{Name: "name", Type: field.TypeString, Size: 100},
|
||||
{Name: "description", Type: field.TypeString, Nullable: true, SchemaType: map[string]string{"postgres": "text"}},
|
||||
{Name: "rate_multiplier", Type: field.TypeFloat64, Default: 1, SchemaType: map[string]string{"postgres": "decimal(10,4)"}},
|
||||
{Name: "is_exclusive", Type: field.TypeBool, Default: false},
|
||||
@@ -470,7 +470,7 @@ var (
|
||||
{Name: "created_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
|
||||
{Name: "updated_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
|
||||
{Name: "deleted_at", Type: field.TypeTime, Nullable: true, SchemaType: map[string]string{"postgres": "timestamptz"}},
|
||||
{Name: "email", Type: field.TypeString, Unique: true, Size: 255},
|
||||
{Name: "email", Type: field.TypeString, Size: 255},
|
||||
{Name: "password_hash", Type: field.TypeString, Size: 255},
|
||||
{Name: "role", Type: field.TypeString, Size: 20, Default: "user"},
|
||||
{Name: "balance", Type: field.TypeFloat64, Default: 0, SchemaType: map[string]string{"postgres": "decimal(20,8)"}},
|
||||
@@ -605,7 +605,7 @@ var (
|
||||
},
|
||||
{
|
||||
Name: "usersubscription_user_id_group_id",
|
||||
Unique: true,
|
||||
Unique: false,
|
||||
Columns: []*schema.Column{UserSubscriptionsColumns[16], UserSubscriptionsColumns[15]},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -33,10 +33,11 @@ func (Group) Mixin() []ent.Mixin {
|
||||
|
||||
func (Group) Fields() []ent.Field {
|
||||
return []ent.Field{
|
||||
// 唯一约束通过部分索引实现(WHERE deleted_at IS NULL),支持软删除后重用
|
||||
// 见迁移文件 016_soft_delete_partial_unique_indexes.sql
|
||||
field.String("name").
|
||||
MaxLen(100).
|
||||
NotEmpty().
|
||||
Unique(),
|
||||
NotEmpty(),
|
||||
field.String("description").
|
||||
Optional().
|
||||
Nillable().
|
||||
|
||||
@@ -33,10 +33,11 @@ func (User) Mixin() []ent.Mixin {
|
||||
|
||||
func (User) Fields() []ent.Field {
|
||||
return []ent.Field{
|
||||
// 唯一约束通过部分索引实现(WHERE deleted_at IS NULL),支持软删除后重用
|
||||
// 见迁移文件 016_soft_delete_partial_unique_indexes.sql
|
||||
field.String("email").
|
||||
MaxLen(255).
|
||||
NotEmpty().
|
||||
Unique(),
|
||||
NotEmpty(),
|
||||
field.String("password_hash").
|
||||
MaxLen(255).
|
||||
NotEmpty(),
|
||||
|
||||
@@ -109,7 +109,9 @@ func (UserSubscription) Indexes() []ent.Index {
|
||||
index.Fields("status"),
|
||||
index.Fields("expires_at"),
|
||||
index.Fields("assigned_by"),
|
||||
index.Fields("user_id", "group_id").Unique(),
|
||||
// 唯一约束通过部分索引实现(WHERE deleted_at IS NULL),支持软删除后重新订阅
|
||||
// 见迁移文件 016_soft_delete_partial_unique_indexes.sql
|
||||
index.Fields("user_id", "group_id"),
|
||||
index.Fields("deleted_at"),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user