diff --git a/backend/ent/runtime/runtime.go b/backend/ent/runtime/runtime.go index da0accd7..0b254b3e 100644 --- a/backend/ent/runtime/runtime.go +++ b/backend/ent/runtime/runtime.go @@ -415,10 +415,6 @@ func init() { return nil } }() - // settingDescValue is the schema descriptor for value field. - settingDescValue := settingFields[1].Descriptor() - // setting.ValueValidator is a validator for the "value" field. It is called by the builders before save. - setting.ValueValidator = settingDescValue.Validators[0].(func(string) error) // settingDescUpdatedAt is the schema descriptor for updated_at field. settingDescUpdatedAt := settingFields[2].Descriptor() // setting.DefaultUpdatedAt holds the default value on creation for the updated_at field. diff --git a/backend/ent/schema/setting.go b/backend/ent/schema/setting.go index 3f896fab..0acfde59 100644 --- a/backend/ent/schema/setting.go +++ b/backend/ent/schema/setting.go @@ -36,7 +36,6 @@ func (Setting) Fields() []ent.Field { NotEmpty(). Unique(), field.String("value"). - NotEmpty(). SchemaType(map[string]string{ dialect.Postgres: "text", }), diff --git a/backend/ent/setting/setting.go b/backend/ent/setting/setting.go index feb86b87..79abe970 100644 --- a/backend/ent/setting/setting.go +++ b/backend/ent/setting/setting.go @@ -44,8 +44,6 @@ func ValidColumn(column string) bool { var ( // KeyValidator is a validator for the "key" field. It is called by the builders before save. KeyValidator func(string) error - // ValueValidator is a validator for the "value" field. It is called by the builders before save. - ValueValidator func(string) error // DefaultUpdatedAt holds the default value on creation for the "updated_at" field. DefaultUpdatedAt func() time.Time // UpdateDefaultUpdatedAt holds the default value on update for the "updated_at" field. diff --git a/backend/ent/setting_create.go b/backend/ent/setting_create.go index 66c1231e..553261e7 100644 --- a/backend/ent/setting_create.go +++ b/backend/ent/setting_create.go @@ -102,11 +102,6 @@ func (_c *SettingCreate) check() error { if _, ok := _c.mutation.Value(); !ok { return &ValidationError{Name: "value", err: errors.New(`ent: missing required field "Setting.value"`)} } - if v, ok := _c.mutation.Value(); ok { - if err := setting.ValueValidator(v); err != nil { - return &ValidationError{Name: "value", err: fmt.Errorf(`ent: validator failed for field "Setting.value": %w`, err)} - } - } if _, ok := _c.mutation.UpdatedAt(); !ok { return &ValidationError{Name: "updated_at", err: errors.New(`ent: missing required field "Setting.updated_at"`)} } diff --git a/backend/ent/setting_update.go b/backend/ent/setting_update.go index 007fa36e..42d016d6 100644 --- a/backend/ent/setting_update.go +++ b/backend/ent/setting_update.go @@ -110,11 +110,6 @@ func (_u *SettingUpdate) check() error { return &ValidationError{Name: "key", err: fmt.Errorf(`ent: validator failed for field "Setting.key": %w`, err)} } } - if v, ok := _u.mutation.Value(); ok { - if err := setting.ValueValidator(v); err != nil { - return &ValidationError{Name: "value", err: fmt.Errorf(`ent: validator failed for field "Setting.value": %w`, err)} - } - } return nil } @@ -254,11 +249,6 @@ func (_u *SettingUpdateOne) check() error { return &ValidationError{Name: "key", err: fmt.Errorf(`ent: validator failed for field "Setting.key": %w`, err)} } } - if v, ok := _u.mutation.Value(); ok { - if err := setting.ValueValidator(v); err != nil { - return &ValidationError{Name: "value", err: fmt.Errorf(`ent: validator failed for field "Setting.value": %w`, err)} - } - } return nil } diff --git a/backend/internal/repository/setting_repo_integration_test.go b/backend/internal/repository/setting_repo_integration_test.go index 784124f4..f91c0651 100644 --- a/backend/internal/repository/setting_repo_integration_test.go +++ b/backend/internal/repository/setting_repo_integration_test.go @@ -105,3 +105,59 @@ func (s *SettingRepoSuite) TestSetMultiple_Upsert() { s.Require().NoError(err) s.Require().Equal("new_val", got2) } + +// TestSet_EmptyValue 测试保存空字符串值 +// 这是一个回归测试,确保可选设置(如站点Logo、API端点地址等)可以保存为空字符串 +func (s *SettingRepoSuite) TestSet_EmptyValue() { + // 测试 Set 方法保存空值 + s.Require().NoError(s.repo.Set(s.ctx, "empty_key", ""), "Set with empty value should succeed") + + got, err := s.repo.GetValue(s.ctx, "empty_key") + s.Require().NoError(err, "GetValue for empty value") + s.Require().Equal("", got, "empty value should be preserved") +} + +// TestSetMultiple_WithEmptyValues 测试批量保存包含空字符串的设置 +// 模拟用户保存站点设置时部分字段为空的场景 +func (s *SettingRepoSuite) TestSetMultiple_WithEmptyValues() { + // 模拟保存站点设置,部分字段有值,部分字段为空 + settings := map[string]string{ + "site_name": "AICodex2API", + "site_subtitle": "Subscription to API", + "site_logo": "", // 用户未上传Logo + "api_base_url": "", // 用户未设置API地址 + "contact_info": "", // 用户未设置联系方式 + "doc_url": "", // 用户未设置文档链接 + } + + s.Require().NoError(s.repo.SetMultiple(s.ctx, settings), "SetMultiple with empty values should succeed") + + // 验证所有值都正确保存 + result, err := s.repo.GetMultiple(s.ctx, []string{"site_name", "site_subtitle", "site_logo", "api_base_url", "contact_info", "doc_url"}) + s.Require().NoError(err, "GetMultiple after SetMultiple with empty values") + + s.Require().Equal("AICodex2API", result["site_name"]) + s.Require().Equal("Subscription to API", result["site_subtitle"]) + s.Require().Equal("", result["site_logo"], "empty site_logo should be preserved") + s.Require().Equal("", result["api_base_url"], "empty api_base_url should be preserved") + s.Require().Equal("", result["contact_info"], "empty contact_info should be preserved") + s.Require().Equal("", result["doc_url"], "empty doc_url should be preserved") +} + +// TestSetMultiple_UpdateToEmpty 测试将已有值更新为空字符串 +// 确保用户可以清空之前设置的值 +func (s *SettingRepoSuite) TestSetMultiple_UpdateToEmpty() { + // 先设置非空值 + s.Require().NoError(s.repo.Set(s.ctx, "clearable_key", "initial_value")) + + got, err := s.repo.GetValue(s.ctx, "clearable_key") + s.Require().NoError(err) + s.Require().Equal("initial_value", got) + + // 更新为空值 + s.Require().NoError(s.repo.SetMultiple(s.ctx, map[string]string{"clearable_key": ""}), "Update to empty should succeed") + + got, err = s.repo.GetValue(s.ctx, "clearable_key") + s.Require().NoError(err) + s.Require().Equal("", got, "value should be updated to empty string") +} diff --git a/backend/migrations/015_fix_settings_unique_constraint.sql b/backend/migrations/015_fix_settings_unique_constraint.sql new file mode 100644 index 00000000..60f8fcad --- /dev/null +++ b/backend/migrations/015_fix_settings_unique_constraint.sql @@ -0,0 +1,19 @@ +-- 015_fix_settings_unique_constraint.sql +-- 修复 settings 表 key 字段缺失的唯一约束 +-- 此约束是 ON CONFLICT ("key") DO UPDATE 语句所必需的 + +-- 检查并添加唯一约束(如果不存在) +DO $$ +BEGIN + -- 检查是否已存在唯一约束 + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint + WHERE conrelid = 'settings'::regclass + AND contype = 'u' + AND conname = 'settings_key_key' + ) THEN + -- 添加唯一约束 + ALTER TABLE settings ADD CONSTRAINT settings_key_key UNIQUE (key); + END IF; +END +$$;