diff --git a/backend/internal/modules/systemconfig/dto.go b/backend/internal/modules/systemconfig/dto.go index 12c4f05..8251ee5 100644 --- a/backend/internal/modules/systemconfig/dto.go +++ b/backend/internal/modules/systemconfig/dto.go @@ -52,6 +52,7 @@ type PublishOptionsDTO struct { FireLevelMin int `json:"fire_level_min"` PriceConfig PublishPriceConfig `json:"price_config"` RatioConfig PublishRatioConfig `json:"ratio_config"` + SalePriceConfig PublishSalePriceConfig `json:"sale_price_config"` } type PublishOptionGroup struct { @@ -103,3 +104,20 @@ type PublishCoinCorrection struct { ThresholdM float64 `json:"threshold_m"` Correction float64 `json:"correction"` } + +type PublishSalePriceConfig struct { + FixedMarkupRules []PublishSaleFixedMarkupRule `json:"fixed_markup_rules"` + RatioAdjustmentRules []PublishSaleRatioAdjustmentRule `json:"ratio_adjustment_rules"` +} + +type PublishSaleFixedMarkupRule struct { + MinM float64 `json:"min_m"` + MaxM float64 `json:"max_m"` + MarkupAmount float64 `json:"markup_amount"` +} + +type PublishSaleRatioAdjustmentRule struct { + MinM float64 `json:"min_m"` + MaxM float64 `json:"max_m,omitempty"` + RatioSubtract float64 `json:"ratio_subtract"` +} diff --git a/backend/internal/modules/systemconfig/publish_options.go b/backend/internal/modules/systemconfig/publish_options.go index 19d3fc5..29afd36 100644 --- a/backend/internal/modules/systemconfig/publish_options.go +++ b/backend/internal/modules/systemconfig/publish_options.go @@ -108,5 +108,22 @@ func DefaultPublishOptions() PublishOptionsDTO { {ThresholdM: 530, Correction: 5}, }, }, + SalePriceConfig: PublishSalePriceConfig{ + FixedMarkupRules: []PublishSaleFixedMarkupRule{ + {MinM: 10, MaxM: 30, MarkupAmount: 28}, + {MinM: 30, MaxM: 50, MarkupAmount: 31}, + {MinM: 50, MaxM: 70, MarkupAmount: 34}, + {MinM: 70, MaxM: 90, MarkupAmount: 38}, + }, + RatioAdjustmentRules: []PublishSaleRatioAdjustmentRule{ + {MinM: 90, MaxM: 150, RatioSubtract: 5}, + {MinM: 150, MaxM: 230, RatioSubtract: 4}, + {MinM: 230, MaxM: 310, RatioSubtract: 3.5}, + {MinM: 310, MaxM: 390, RatioSubtract: 3}, + {MinM: 390, MaxM: 470, RatioSubtract: 0}, + {MinM: 470, MaxM: 550, RatioSubtract: 0}, + {MinM: 550, RatioSubtract: 0}, + }, + }, } } diff --git a/backend/internal/modules/systemconfig/service.go b/backend/internal/modules/systemconfig/service.go index 76293b7..9c59e5a 100644 --- a/backend/internal/modules/systemconfig/service.go +++ b/backend/internal/modules/systemconfig/service.go @@ -214,4 +214,10 @@ func normalizePublishOptions(options *PublishOptionsDTO) { if len(options.RatioConfig.CoinCorrections) == 0 { options.RatioConfig.CoinCorrections = defaults.RatioConfig.CoinCorrections } + if len(options.SalePriceConfig.FixedMarkupRules) == 0 { + options.SalePriceConfig.FixedMarkupRules = defaults.SalePriceConfig.FixedMarkupRules + } + if len(options.SalePriceConfig.RatioAdjustmentRules) == 0 { + options.SalePriceConfig.RatioAdjustmentRules = defaults.SalePriceConfig.RatioAdjustmentRules + } } diff --git a/docs/出售加价规则.md b/docs/出售加价规则.md new file mode 100644 index 0000000..fcd0d00 --- /dev/null +++ b/docs/出售加价规则.md @@ -0,0 +1,19 @@ +当 10M <= 哈夫币 <= 90M: +出售价格 = 回收价格 + 固定加价 + +10M <= 币数 < 30M:+28元 +30M <= 币数 < 50M:+31元 +50M <= 币数 < 70M:+34元 +70M <= 币数 <= 90M:+38元 + +当 哈夫币 > 90M: +出售比例 = 回收比例 - 比例修正 +出售价格 = 哈夫币万数 / 出售比例 + +90M < 币数 < 150M:-5 +150M <= 币数 < 230M:-4 +230M <= 币数 < 310M:-3.5 +310M <= 币数 < 390M:-3 +390M <= 币数 < 470M:待定 +470M <= 币数 < 550M:待定 +550M <= 币数:待定 \ No newline at end of file diff --git a/frontend/src/api/listingOptions.ts b/frontend/src/api/listingOptions.ts index f2d7ee2..c8990d6 100644 --- a/frontend/src/api/listingOptions.ts +++ b/frontend/src/api/listingOptions.ts @@ -52,12 +52,29 @@ export interface PublishCoinCorrection { correction: number } +export interface PublishSaleFixedMarkupRule { + min_m: number + max_m: number + markup_amount: number +} + +export interface PublishSaleRatioAdjustmentRule { + min_m: number + max_m: number + ratio_subtract: number +} + export interface PublishRatioConfig { insurance_base_ratios: PublishInsuranceBaseRatio[] config_items: PublishRatioConfigItem[] coin_corrections: PublishCoinCorrection[] } +export interface PublishSalePriceConfig { + fixed_markup_rules: PublishSaleFixedMarkupRule[] + ratio_adjustment_rules: PublishSaleRatioAdjustmentRule[] +} + export interface ListingPublishOptions { server_options: string[] face_options: string[] @@ -74,6 +91,7 @@ export interface ListingPublishOptions { fire_level_min: number price_config: PublishPriceConfig ratio_config: PublishRatioConfig + sale_price_config: PublishSalePriceConfig } interface ApiResponse { @@ -106,6 +124,10 @@ export const emptyListingPublishOptions: ListingPublishOptions = { config_items: [], coin_corrections: [], }, + sale_price_config: { + fixed_markup_rules: [], + ratio_adjustment_rules: [], + }, } export async function fetchListingPublishOptions() { @@ -130,6 +152,7 @@ export function mergeListingPublishOptions(options?: Partial { @@ -246,6 +281,37 @@ function normalizeCoinCorrections(values: unknown[]): PublishCoinCorrection[] { .filter((item) => item.threshold_m >= 0 && item.correction > 0) } +function normalizeSaleFixedMarkupRules(values: unknown[]): PublishSaleFixedMarkupRule[] { + return values + .map((item) => { + const row = isRecord(item) ? item : {} + return { + min_m: readNumber(row.min_m), + max_m: readNumber(row.max_m), + markup_amount: readNumber(row.markup_amount), + } + }) + .filter((item) => item.min_m >= 0 && item.max_m >= item.min_m && item.markup_amount >= 0) +} + +function normalizeSaleRatioAdjustmentRules(values: unknown[]): PublishSaleRatioAdjustmentRule[] { + return values + .map((item) => { + const row = isRecord(item) ? item : {} + return { + min_m: readNumber(row.min_m), + max_m: readNumber(row.max_m), + ratio_subtract: readNumber(row.ratio_subtract), + } + }) + .filter( + (item) => + item.min_m >= 0 && + (item.max_m === 0 || item.max_m >= item.min_m) && + item.ratio_subtract >= 0, + ) +} + function readNumber(value: unknown) { const parsed = Number(value) return Number.isFinite(parsed) ? parsed : 0 diff --git a/frontend/src/views/admin/AdminSystemConfigsView.vue b/frontend/src/views/admin/AdminSystemConfigsView.vue index 652c260..03e5de3 100644 --- a/frontend/src/views/admin/AdminSystemConfigsView.vue +++ b/frontend/src/views/admin/AdminSystemConfigsView.vue @@ -54,6 +54,9 @@ const publishStats = computed(() => { resourceCount: options.quantity_items.length, screenshotCount: options.screenshot_slots.length, regionCount: options.region_options.length, + saleRuleCount: + options.sale_price_config.fixed_markup_rules.length + + options.sale_price_config.ratio_adjustment_rules.length, } }) const homeStats = computed(() => { @@ -281,6 +284,30 @@ function removeCoinCorrection(index: number) { publishOptionsDraft.value.ratio_config.coin_corrections.splice(index, 1) } +function addSaleFixedMarkupRule() { + publishOptionsDraft.value.sale_price_config.fixed_markup_rules.push({ + min_m: 0, + max_m: 0, + markup_amount: 0, + }) +} + +function removeSaleFixedMarkupRule(index: number) { + publishOptionsDraft.value.sale_price_config.fixed_markup_rules.splice(index, 1) +} + +function addSaleRatioAdjustmentRule() { + publishOptionsDraft.value.sale_price_config.ratio_adjustment_rules.push({ + min_m: 0, + max_m: 0, + ratio_subtract: 0, + }) +} + +function removeSaleRatioAdjustmentRule(index: number) { + publishOptionsDraft.value.sale_price_config.ratio_adjustment_rules.splice(index, 1) +} + function addHomeBanner() { homeBannersDraft.value.push({ eyebrow: '首页推荐', @@ -387,6 +414,10 @@ function readError(error: unknown, fallback: string) { {{ publishStats.regionCount }} 地区 +
+ {{ publishStats.saleRuleCount }} + 出售规则 +
@@ -642,6 +673,53 @@ function readError(error: unknown, fallback: string) { +
+
+ 出售加价规则 +
+
+ 90M 及以下固定加价:出售价格 = 回收价格 + 固定加价 + 添加固定加价 +
+ + + + + + + + + + + + + + + +
+ 90M 以上比例调整:出售比例 = 回收比例 - 比例扣减;最大 M 为 0 表示无上限 + 添加比例调整 +
+ + + + + + + + + + + + + + +
+
皮肤分类 diff --git a/frontend/src/views/mobile/MobileSellerListingCreateView.css b/frontend/src/views/mobile/MobileSellerListingCreateView.css index 4ec19dd..08e98a3 100644 --- a/frontend/src/views/mobile/MobileSellerListingCreateView.css +++ b/frontend/src/views/mobile/MobileSellerListingCreateView.css @@ -498,6 +498,12 @@ font-weight: 800; } +.muted-result-field :deep(.van-field__control) { + color: #5c6470; + font-size: 12px; + font-weight: 700; +} + .total-price-field :deep(.van-field__control) { font-size: 16px; } diff --git a/frontend/src/views/mobile/MobileSellerListingCreateView.vue b/frontend/src/views/mobile/MobileSellerListingCreateView.vue index 02db889..a1fd1ad 100644 --- a/frontend/src/views/mobile/MobileSellerListingCreateView.vue +++ b/frontend/src/views/mobile/MobileSellerListingCreateView.vue @@ -117,6 +117,7 @@ const fireLevelPlaceholder = computed( () => `等级低于${fireLevelMin.value}级的号无法发布` ); const ratioConfig = computed(() => publishOptions.value.ratio_config); +const salePriceConfig = computed(() => publishOptions.value.sale_price_config); const skinGroups = computed(() => publishOptions.value.skin_groups); const quantityItems = computed(() => publishOptions.value.quantity_items); const screenshotSlots = computed(() => publishOptions.value.screenshot_slots); @@ -127,10 +128,11 @@ const screenshotUrls = computed(() => ); const coinMAmount = computed(() => Number(form.haf_coin_amount || 0)); const coinWanAmount = computed(() => coinMAmount.value * 100); -const calculatedRatio = computed(() => calculatePublishRatio()); -const calculatedCoinBasePrice = computed(() => - calculatedRatio.value > 0 ? roundMoney(coinWanAmount.value / calculatedRatio.value) : 0 -); +const calculatedRecycleRatio = computed(() => calculatePublishRatio()); +const calculatedSalePricing = computed(() => calculateSalePricing()); +const calculatedRatio = computed(() => calculatedSalePricing.value.saleRatio); +const calculatedCoinBasePrice = computed(() => calculatedSalePricing.value.coinBasePrice); +const calculatedSaleRuleText = computed(() => calculatedSalePricing.value.ruleText); const calculatedConsumablePrice = computed(() => calculateConsumablePrice()); const calculatedFinalPrice = computed(() => calculatedRatio.value > 0 @@ -443,10 +445,14 @@ function buildAssetSummary() { fire_level: Number(form.fire_level), publish_ratio: calculatedRatio.value, price_breakdown: { + recycle_coin_base_price: calculatedSalePricing.value.recycleCoinBasePrice, coin_base_price: calculatedCoinBasePrice.value, consumable_price: calculatedConsumablePrice.value, final_price: calculatedFinalPrice.value, - ratio: calculatedRatio.value, + recycle_ratio: calculatedRecycleRatio.value, + sale_ratio: calculatedRatio.value, + sale_rule_type: calculatedSalePricing.value.ruleType, + sale_rule_text: calculatedSalePricing.value.ruleText, }, season_insurance: form.season_insurance, stamina_level: form.stamina_level, @@ -515,6 +521,79 @@ function calculatePublishRatio() { return baseRatio + calculateConfigPenalty() + getCoinCorrection(coinMAmount.value); } +function calculateSalePricing() { + const recycleRatio = calculatedRecycleRatio.value; + if (recycleRatio <= 0 || coinWanAmount.value <= 0) { + return emptySalePricing(); + } + const recycleCoinBasePrice = roundMoney(coinWanAmount.value / recycleRatio); + const fixedRule = findSaleFixedMarkupRule(); + if (fixedRule) { + const coinBasePrice = roundMoney(recycleCoinBasePrice + Number(fixedRule.markup_amount || 0)); + return { + recycleCoinBasePrice, + coinBasePrice, + saleRatio: calculateEffectiveRatio(coinBasePrice), + ruleType: "fixed_markup", + ruleText: `${formatSaleRange(fixedRule)} 加价 ${formatNumber(fixedRule.markup_amount)}元`, + }; + } + + const ratioRule = findSaleRatioAdjustmentRule(); + const ratioSubtract = ratioRule ? Number(ratioRule.ratio_subtract || 0) : 0; + const saleRatio = recycleRatio - ratioSubtract; + if (saleRatio <= 0) { + return emptySalePricing(recycleCoinBasePrice); + } + return { + recycleCoinBasePrice, + coinBasePrice: roundMoney(coinWanAmount.value / saleRatio), + saleRatio, + ruleType: ratioRule ? "ratio_subtract" : "none", + ruleText: ratioRule + ? `${formatSaleRange(ratioRule)} 出售比例=回收比例-${formatNumber(ratioSubtract)}` + : "未匹配出售加价规则,按回收比例计算", + }; +} + +function emptySalePricing(recycleCoinBasePrice = 0) { + return { + recycleCoinBasePrice, + coinBasePrice: 0, + saleRatio: 0, + ruleType: "none", + ruleText: "", + }; +} + +function findSaleFixedMarkupRule() { + return [...salePriceConfig.value.fixed_markup_rules] + .sort((a, b) => a.min_m - b.min_m) + .find((item) => isCoinInSaleRange(item)); +} + +function findSaleRatioAdjustmentRule() { + return [...salePriceConfig.value.ratio_adjustment_rules] + .sort((a, b) => a.min_m - b.min_m) + .find((item) => isCoinInSaleRange(item)); +} + +function isCoinInSaleRange(item: { min_m: number; max_m: number }) { + const maxM = Number(item.max_m || 0); + return coinMAmount.value >= Number(item.min_m || 0) && (maxM <= 0 || coinMAmount.value <= maxM); +} + +function calculateEffectiveRatio(price: number) { + if (price <= 0) return 0; + return Math.round((coinWanAmount.value / price) * 10) / 10; +} + +function formatSaleRange(item: { min_m: number; max_m: number }) { + const minM = formatNumber(Number(item.min_m || 0)); + const maxM = Number(item.max_m || 0); + return maxM > 0 ? `${minM}M-${formatNumber(maxM)}M` : `${minM}M以上`; +} + function getInsuranceBaseRatio(insurance: string) { return ratioConfig.value.insurance_base_ratios.find( (item) => item.insurance === insurance @@ -921,15 +1000,21 @@ function readError(error: unknown, fallback: string) {
+

- 发布总价 = 纯币基础价 + 额外消耗品;只有标记为“收费”的消耗品会计入价格。 + 纯币基础价先按回收比例计算,再套出售加价规则;发布总价 = 纯币基础价 + 额外消耗品。