diff --git a/backend/internal/modules/systemconfig/dto.go b/backend/internal/modules/systemconfig/dto.go index f4c0cb6..b3da7fb 100644 --- a/backend/internal/modules/systemconfig/dto.go +++ b/backend/internal/modules/systemconfig/dto.go @@ -121,7 +121,9 @@ type HomeBannerHotspot struct { type PublishOptionsDTO struct { // SchemaVersion 发布选项结构版本;缺省 0 代表未版本化的历史配置。 + // UnlockDemulanEnabled 发布页是否展示「是否解锁德穆兰」选项。 SchemaVersion int `json:"schema_version,omitempty"` + UnlockDemulanEnabled bool `json:"unlock_demulan_enabled"` NextSeasonPassEnabled bool `json:"next_season_pass_enabled"` ServerOptions []string `json:"server_options"` FaceOptions []string `json:"face_options"` diff --git a/backend/internal/modules/systemconfig/publish_options.go b/backend/internal/modules/systemconfig/publish_options.go index eb8d1f0..732e31c 100644 --- a/backend/internal/modules/systemconfig/publish_options.go +++ b/backend/internal/modules/systemconfig/publish_options.go @@ -3,8 +3,10 @@ package systemconfig import "encoding/json" const ( + // publishOptionsSchemaVersion 发布选项结构版本;v2 新增「是否解锁德穆兰」开关(默认关闭), + // 并把「下赛季是否购买通行证」默认打开。 + publishOptionsSchemaVersion = 2 publishOptionsConfigKey = "listing.publish_options" - publishOptionsSchemaVersion = 1 ) func defaultPublishOptionsConfigValue() string { @@ -18,7 +20,8 @@ func defaultPublishOptionsConfigValue() string { func DefaultPublishOptions() PublishOptionsDTO { return PublishOptionsDTO{ SchemaVersion: publishOptionsSchemaVersion, - NextSeasonPassEnabled: false, + UnlockDemulanEnabled: false, + NextSeasonPassEnabled: true, ServerOptions: []string{"QQ", "微信", "Steam"}, FaceOptions: []string{"是", "否"}, RankOptions: []string{"青铜", "白银", "黄金", "铂金", "钻石", "黑鹰", "三角洲巅峰"}, diff --git a/backend/internal/modules/systemconfig/publish_options_test.go b/backend/internal/modules/systemconfig/publish_options_test.go index 4683733..6d321ce 100644 --- a/backend/internal/modules/systemconfig/publish_options_test.go +++ b/backend/internal/modules/systemconfig/publish_options_test.go @@ -106,6 +106,53 @@ func TestNormalizePublishOptionsValuePreservesCurrentSchema(t *testing.T) { } } +func TestNormalizePublishOptionsValueMigratesSchemaV1Switches(t *testing.T) { + legacy := `{"schema_version":1,"next_season_pass_enabled":false,"server_options":["QQ"]}` + + normalized, err := normalizePublishOptionsValue(legacy) + if err != nil { + t.Fatalf("规范化失败: %v", err) + } + var got PublishOptionsDTO + if err := json.Unmarshal([]byte(normalized), &got); err != nil { + t.Fatalf("解析规范化结果失败: %v", err) + } + if got.SchemaVersion != publishOptionsSchemaVersion { + t.Fatalf("schema_version = %d, want %d", got.SchemaVersion, publishOptionsSchemaVersion) + } + if got.UnlockDemulanEnabled { + t.Fatal("v1 升级后「是否解锁德穆兰」应默认关闭") + } + if !got.NextSeasonPassEnabled { + t.Fatal("v1 升级后「下赛季是否购买通行证」应默认打开") + } +} + +func TestNormalizePublishOptionsValueKeepsSchemaV2Switches(t *testing.T) { + options := DefaultPublishOptions() + options.UnlockDemulanEnabled = true + options.NextSeasonPassEnabled = false + raw, err := json.Marshal(options) + if err != nil { + t.Fatalf("构造配置失败: %v", err) + } + + normalized, err := normalizePublishOptionsValue(string(raw)) + if err != nil { + t.Fatalf("规范化失败: %v", err) + } + var got PublishOptionsDTO + if err := json.Unmarshal([]byte(normalized), &got); err != nil { + t.Fatalf("解析规范化结果失败: %v", err) + } + if !got.UnlockDemulanEnabled { + t.Fatal("v2 配置中后台手动开启的德穆兰开关不应被覆盖") + } + if got.NextSeasonPassEnabled { + t.Fatal("v2 配置中后台手动关闭的通行证开关不应被覆盖") + } +} + func TestNormalizePublishOptionsValueRepairsMojibake(t *testing.T) { options := DefaultPublishOptions() raw, err := json.Marshal(options) diff --git a/backend/internal/modules/systemconfig/repository.go b/backend/internal/modules/systemconfig/repository.go index 90fe249..0fbac1b 100644 --- a/backend/internal/modules/systemconfig/repository.go +++ b/backend/internal/modules/systemconfig/repository.go @@ -330,6 +330,7 @@ func normalizePublishOptionsValue(value string) (string, error) { if err := json.Unmarshal([]byte(value), &fields); err != nil { return "", err } + originalVersion := options.SchemaVersion if options.SchemaVersion < publishOptionsSchemaVersion { if hasLegacyPublishQuantityItem(options.QuantityItems) { options = DefaultPublishOptions() @@ -342,6 +343,12 @@ func normalizePublishOptionsValue(value string) (string, error) { } else { normalizePublishOptions(&options) } + // v2 一次性迁移:新增「是否解锁德穆兰」开关并默认关闭,同时打开「下赛季是否购买通行证」。 + // 已经是 v2 的配置不再覆盖,保留后台手动调整的结果。 + if originalVersion < 2 { + options.UnlockDemulanEnabled = false + options.NextSeasonPassEnabled = true + } options.SchemaVersion = publishOptionsSchemaVersion raw, err := json.Marshal(options) if err != nil { diff --git a/frontend/src/features/admin/components/PublishOptionsDialog.vue b/frontend/src/features/admin/components/PublishOptionsDialog.vue index ae08aca..9832746 100644 --- a/frontend/src/features/admin/components/PublishOptionsDialog.vue +++ b/frontend/src/features/admin/components/PublishOptionsDialog.vue @@ -294,6 +294,13 @@ async function handleSave() {
发布规则
+ + + ): ListingPublishOptions { return { + unlock_demulan_enabled: options?.unlock_demulan_enabled === true, next_season_pass_enabled: options?.next_season_pass_enabled === true, server_options: normalizeStringList(options?.server_options), face_options: normalizeStringList(options?.face_options), diff --git a/frontend/src/features/seller/composables/usePublishForm.ts b/frontend/src/features/seller/composables/usePublishForm.ts index be30210..9e0ca73 100644 --- a/frontend/src/features/seller/composables/usePublishForm.ts +++ b/frontend/src/features/seller/composables/usePublishForm.ts @@ -84,6 +84,7 @@ export function usePublishForm(options: UsePublishFormOptions) { const submitButtonText = computed(() => (isEditMode.value ? '提交修改' : '立即发布')) const submitLoadingText = computed(() => (isEditMode.value ? '提交中...' : '发布中...')) const showBanRecordRiskHint = false + const unlockDemulanEnabled = computed(() => publishOptions.value.unlock_demulan_enabled) const nextSeasonPassEnabled = computed(() => publishOptions.value.next_season_pass_enabled) const pricing = usePricingCalculator({ @@ -734,7 +735,9 @@ export function usePublishForm(options: UsePublishFormOptions) { if (pricing.banRecordOptions.value.length && !form.ban_record) return '请选择封禁记录' if (!form.can_change_game_name) return '请选择是否可以修改游戏名' if (!form.all_hero) return '请选择是否有全英雄' - if (!form.unlock_demulan) return '请选择是否解锁德穆兰' + if (unlockDemulanEnabled.value && !form.unlock_demulan) { + return '请选择是否解锁德穆兰' + } if (nextSeasonPassEnabled.value && !form.next_season_pass) { return '请选择下赛季是否购买通行证' } @@ -934,6 +937,7 @@ export function usePublishForm(options: UsePublishFormOptions) { passwordAndDeviceConfirmed, canPublishAfterAgreements, showBanRecordRiskHint, + unlockDemulanEnabled, nextSeasonPassEnabled, isScanOnlyServer, effectiveLoginMethodOptions, diff --git a/frontend/src/features/seller/views/MobileSellerListingCreateView.vue b/frontend/src/features/seller/views/MobileSellerListingCreateView.vue index 479c160..bcadb5a 100644 --- a/frontend/src/features/seller/views/MobileSellerListingCreateView.vue +++ b/frontend/src/features/seller/views/MobileSellerListingCreateView.vue @@ -27,6 +27,7 @@ const { passwordAndDeviceConfirmed, canPublishAfterAgreements, showBanRecordRiskHint, + unlockDemulanEnabled, nextSeasonPassEnabled, fileInput, activeUploadKey, @@ -444,7 +445,12 @@ const screenshotGuides: Record = { - +