Merge PR #105: fix(数据层): 修复软删除与唯一约束冲突问题 和 添加 model 参数必填验证
This commit is contained in:
@@ -204,7 +204,7 @@ var (
|
|||||||
{Name: "created_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
|
{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: "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: "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: "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: "rate_multiplier", Type: field.TypeFloat64, Default: 1, SchemaType: map[string]string{"postgres": "decimal(10,4)"}},
|
||||||
{Name: "is_exclusive", Type: field.TypeBool, Default: false},
|
{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: "created_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
|
||||||
{Name: "updated_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: "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: "password_hash", Type: field.TypeString, Size: 255},
|
||||||
{Name: "role", Type: field.TypeString, Size: 20, Default: "user"},
|
{Name: "role", Type: field.TypeString, Size: 20, Default: "user"},
|
||||||
{Name: "balance", Type: field.TypeFloat64, Default: 0, SchemaType: map[string]string{"postgres": "decimal(20,8)"}},
|
{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",
|
Name: "usersubscription_user_id_group_id",
|
||||||
Unique: true,
|
Unique: false,
|
||||||
Columns: []*schema.Column{UserSubscriptionsColumns[16], UserSubscriptionsColumns[15]},
|
Columns: []*schema.Column{UserSubscriptionsColumns[16], UserSubscriptionsColumns[15]},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -33,10 +33,11 @@ func (Group) Mixin() []ent.Mixin {
|
|||||||
|
|
||||||
func (Group) Fields() []ent.Field {
|
func (Group) Fields() []ent.Field {
|
||||||
return []ent.Field{
|
return []ent.Field{
|
||||||
|
// 唯一约束通过部分索引实现(WHERE deleted_at IS NULL),支持软删除后重用
|
||||||
|
// 见迁移文件 016_soft_delete_partial_unique_indexes.sql
|
||||||
field.String("name").
|
field.String("name").
|
||||||
MaxLen(100).
|
MaxLen(100).
|
||||||
NotEmpty().
|
NotEmpty(),
|
||||||
Unique(),
|
|
||||||
field.String("description").
|
field.String("description").
|
||||||
Optional().
|
Optional().
|
||||||
Nillable().
|
Nillable().
|
||||||
|
|||||||
@@ -33,10 +33,11 @@ func (User) Mixin() []ent.Mixin {
|
|||||||
|
|
||||||
func (User) Fields() []ent.Field {
|
func (User) Fields() []ent.Field {
|
||||||
return []ent.Field{
|
return []ent.Field{
|
||||||
|
// 唯一约束通过部分索引实现(WHERE deleted_at IS NULL),支持软删除后重用
|
||||||
|
// 见迁移文件 016_soft_delete_partial_unique_indexes.sql
|
||||||
field.String("email").
|
field.String("email").
|
||||||
MaxLen(255).
|
MaxLen(255).
|
||||||
NotEmpty().
|
NotEmpty(),
|
||||||
Unique(),
|
|
||||||
field.String("password_hash").
|
field.String("password_hash").
|
||||||
MaxLen(255).
|
MaxLen(255).
|
||||||
NotEmpty(),
|
NotEmpty(),
|
||||||
|
|||||||
@@ -109,7 +109,9 @@ func (UserSubscription) Indexes() []ent.Index {
|
|||||||
index.Fields("status"),
|
index.Fields("status"),
|
||||||
index.Fields("expires_at"),
|
index.Fields("expires_at"),
|
||||||
index.Fields("assigned_by"),
|
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"),
|
index.Fields("deleted_at"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,6 +88,12 @@ func (h *GatewayHandler) Messages(c *gin.Context) {
|
|||||||
reqModel := parsedReq.Model
|
reqModel := parsedReq.Model
|
||||||
reqStream := parsedReq.Stream
|
reqStream := parsedReq.Stream
|
||||||
|
|
||||||
|
// 验证 model 必填
|
||||||
|
if reqModel == "" {
|
||||||
|
h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "model is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Track if we've started streaming (for error handling)
|
// Track if we've started streaming (for error handling)
|
||||||
streamStarted := false
|
streamStarted := false
|
||||||
|
|
||||||
@@ -517,6 +523,12 @@ func (h *GatewayHandler) CountTokens(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 验证 model 必填
|
||||||
|
if parsedReq.Model == "" {
|
||||||
|
h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "model is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 获取订阅信息(可能为nil)
|
// 获取订阅信息(可能为nil)
|
||||||
subscription, _ := middleware2.GetSubscriptionFromContext(c)
|
subscription, _ := middleware2.GetSubscriptionFromContext(c)
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,12 @@ func (h *OpenAIGatewayHandler) Responses(c *gin.Context) {
|
|||||||
reqModel, _ := reqBody["model"].(string)
|
reqModel, _ := reqBody["model"].(string)
|
||||||
reqStream, _ := reqBody["stream"].(bool)
|
reqStream, _ := reqBody["stream"].(bool)
|
||||||
|
|
||||||
|
// 验证 model 必填
|
||||||
|
if reqModel == "" {
|
||||||
|
h.errorResponse(c, http.StatusBadRequest, "invalid_request_error", "model is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// For non-Codex CLI requests, set default instructions
|
// For non-Codex CLI requests, set default instructions
|
||||||
userAgent := c.GetHeader("User-Agent")
|
userAgent := c.GetHeader("User-Agent")
|
||||||
if !openai.IsCodexCLIRequest(userAgent) {
|
if !openai.IsCodexCLIRequest(userAgent) {
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
-- 016_soft_delete_partial_unique_indexes.sql
|
||||||
|
-- 修复软删除 + 唯一约束冲突问题
|
||||||
|
-- 将普通唯一约束替换为部分唯一索引(WHERE deleted_at IS NULL)
|
||||||
|
-- 这样软删除的记录不会占用唯一约束位置,允许删后重建同名/同邮箱/同订阅关系
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- 1. users 表: email 字段
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- 删除旧的唯一约束(可能的命名方式)
|
||||||
|
ALTER TABLE users DROP CONSTRAINT IF EXISTS users_email_key;
|
||||||
|
DROP INDEX IF EXISTS users_email_key;
|
||||||
|
DROP INDEX IF EXISTS user_email_key;
|
||||||
|
|
||||||
|
-- 创建部分唯一索引:只对未删除的记录建立唯一约束
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS users_email_unique_active
|
||||||
|
ON users(email)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- 2. groups 表: name 字段
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- 删除旧的唯一约束
|
||||||
|
ALTER TABLE groups DROP CONSTRAINT IF EXISTS groups_name_key;
|
||||||
|
DROP INDEX IF EXISTS groups_name_key;
|
||||||
|
DROP INDEX IF EXISTS group_name_key;
|
||||||
|
|
||||||
|
-- 创建部分唯一索引
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS groups_name_unique_active
|
||||||
|
ON groups(name)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- 3. user_subscriptions 表: (user_id, group_id) 组合字段
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- 删除旧的唯一约束/索引
|
||||||
|
ALTER TABLE user_subscriptions DROP CONSTRAINT IF EXISTS user_subscriptions_user_id_group_id_key;
|
||||||
|
DROP INDEX IF EXISTS user_subscriptions_user_id_group_id_key;
|
||||||
|
DROP INDEX IF EXISTS usersubscription_user_id_group_id;
|
||||||
|
|
||||||
|
-- 创建部分唯一索引
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS user_subscriptions_user_group_unique_active
|
||||||
|
ON user_subscriptions(user_id, group_id)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- 注意: api_keys 表的 key 字段保留普通唯一约束
|
||||||
|
-- API Key 即使软删除后也不应该重复使用(安全考虑)
|
||||||
|
-- ============================================================================
|
||||||
Reference in New Issue
Block a user