增加出售设置

This commit is contained in:
yml2213
2026-05-23 20:43:01 +08:00
parent 9cec71ab1a
commit 9f15609afd
8 changed files with 303 additions and 8 deletions
@@ -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"`
}
@@ -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},
},
},
}
}
@@ -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
}
}
+19
View File
@@ -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 <= 币数:待定
+66
View File
@@ -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<T> {
@@ -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<ListingPublishOptio
fire_level_min: readPositiveInteger(options?.fire_level_min, 38),
price_config: normalizePriceConfig(options?.price_config),
ratio_config: normalizeRatioConfig(options?.ratio_config),
sale_price_config: normalizeSalePriceConfig(options?.sale_price_config),
}
}
@@ -207,6 +230,18 @@ function normalizeRatioConfig(value?: unknown): PublishRatioConfig {
}
}
function normalizeSalePriceConfig(value?: unknown): PublishSalePriceConfig {
const row = isRecord(value) ? value : {}
return {
fixed_markup_rules: normalizeSaleFixedMarkupRules(
Array.isArray(row.fixed_markup_rules) ? row.fixed_markup_rules : [],
),
ratio_adjustment_rules: normalizeSaleRatioAdjustmentRules(
Array.isArray(row.ratio_adjustment_rules) ? row.ratio_adjustment_rules : [],
),
}
}
function normalizeInsuranceBaseRatios(values: unknown[]): PublishInsuranceBaseRatio[] {
return values
.map((item) => {
@@ -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
@@ -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) {
<strong>{{ publishStats.regionCount }}</strong>
<span>地区</span>
</div>
<div class="publish-stat">
<strong>{{ publishStats.saleRuleCount }}</strong>
<span>出售规则</span>
</div>
</div>
</section>
@@ -642,6 +673,53 @@ function readError(error: unknown, fallback: string) {
</div>
<div class="editor-block">
<div class="editor-block-title">
<strong>出售加价规则</strong>
</div>
<div class="editor-block-title subtle-title">
<span>90M 及以下固定加价出售价格 = 回收价格 + 固定加价</span>
<el-button size="small" @click="addSaleFixedMarkupRule">添加固定加价</el-button>
</div>
<el-table :data="publishOptionsDraft.sale_price_config.fixed_markup_rules" size="small" border>
<el-table-column label="最小 M" min-width="120">
<template #default="{ row }"><el-input-number v-model="row.min_m" :min="0" :step="10" /></template>
</el-table-column>
<el-table-column label="最大 M" min-width="120">
<template #default="{ row }"><el-input-number v-model="row.max_m" :min="0" :step="10" /></template>
</el-table-column>
<el-table-column label="加价金额/元" min-width="140">
<template #default="{ row }"><el-input-number v-model="row.markup_amount" :min="0" :step="1" /></template>
</el-table-column>
<el-table-column label="操作" width="90">
<template #default="{ $index }">
<el-button size="small" type="danger" plain @click="removeSaleFixedMarkupRule($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="editor-block-title subtle-title">
<span>90M 以上比例调整出售比例 = 回收比例 - 比例扣减最大 M 0 表示无上限</span>
<el-button size="small" @click="addSaleRatioAdjustmentRule">添加比例调整</el-button>
</div>
<el-table :data="publishOptionsDraft.sale_price_config.ratio_adjustment_rules" size="small" border>
<el-table-column label="最小 M" min-width="120">
<template #default="{ row }"><el-input-number v-model="row.min_m" :min="0" :step="10" /></template>
</el-table-column>
<el-table-column label="最大 M" min-width="120">
<template #default="{ row }"><el-input-number v-model="row.max_m" :min="0" :step="10" /></template>
</el-table-column>
<el-table-column label="比例扣减" min-width="130">
<template #default="{ row }"><el-input-number v-model="row.ratio_subtract" :min="0" :step="0.5" /></template>
</el-table-column>
<el-table-column label="操作" width="90">
<template #default="{ $index }">
<el-button size="small" type="danger" plain @click="removeSaleRatioAdjustmentRule($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="editor-block">
<div class="editor-block-title">
<strong>皮肤分类</strong>
@@ -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;
}
@@ -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) {
<div class="result-grid">
<van-field
:model-value="calculatedRatioText"
label="比例"
label="出售比例"
readonly
:placeholder="priceConfig.ratio_description"
class="publish-field result-field"
>
<template #label>
<span class="hint-label" :data-hint="priceConfig.ratio_description" tabindex="0">比例</span>
<span class="hint-label" :data-hint="priceConfig.ratio_description" tabindex="0">出售比例</span>
</template>
</van-field>
<van-field
:model-value="calculatedSaleRuleText"
label="出售规则"
readonly
class="publish-field result-field muted-result-field"
/>
<van-field
:model-value="calculatedCoinBasePrice ? `¥${calculatedCoinBasePrice}` : ''"
label="纯币基础价"
@@ -958,7 +1043,7 @@ function readError(error: unknown, fallback: string) {
/>
</div>
<p class="price-breakdown-hint">
发布总价 = 纯币基础价 + 额外消耗品只有标记为收费的消耗品会计入价格
纯币基础价先按回收比例计算再套出售加价规则发布总价 = 纯币基础价 + 额外消耗品
</p>
<van-field