diff --git a/backend/internal/modules/listing/repository.go b/backend/internal/modules/listing/repository.go
index 22c909d..79c066e 100644
--- a/backend/internal/modules/listing/repository.go
+++ b/backend/internal/modules/listing/repository.go
@@ -493,6 +493,9 @@ func publicListings(items []ListingDTO) []ListingDTO {
func applyPublicListingURLs(item *ListingDTO) {
item.ScreenshotURLS = publicScreenshotURLs(item.ID, item.ScreenshotURLS, item.Status, item.ReviewStatus)
+ if item.AssetSummary != nil {
+ delete(item.AssetSummary, "price_breakdown")
+ }
}
func (row listingRow) toDTO() ListingDTO {
diff --git a/frontend/src/api/listingOptions.ts b/frontend/src/api/listingOptions.ts
index 158cc8d..618c8fa 100644
--- a/frontend/src/api/listingOptions.ts
+++ b/frontend/src/api/listingOptions.ts
@@ -126,8 +126,21 @@ export const emptyListingPublishOptions: ListingPublishOptions = {
}
export const emptyListingSalePriceConfig: PublishSalePriceConfig = {
- fixed_markup_rules: [],
- ratio_adjustment_rules: [],
+ fixed_markup_rules: [
+ { min_m: 10, max_m: 30, markup_amount: 28 },
+ { min_m: 30, max_m: 50, markup_amount: 31 },
+ { min_m: 50, max_m: 70, markup_amount: 34 },
+ { min_m: 70, max_m: 90, markup_amount: 38 },
+ ],
+ ratio_adjustment_rules: [
+ { min_m: 90, max_m: 150, ratio_subtract: 5 },
+ { min_m: 150, max_m: 230, ratio_subtract: 4 },
+ { min_m: 230, max_m: 310, ratio_subtract: 3.5 },
+ { min_m: 310, max_m: 390, ratio_subtract: 3 },
+ { min_m: 390, max_m: 470, ratio_subtract: 0 },
+ { min_m: 470, max_m: 550, ratio_subtract: 0 },
+ { min_m: 550, max_m: 0, ratio_subtract: 0 },
+ ],
}
export async function fetchListingPublishOptions() {
@@ -240,7 +253,7 @@ function normalizeRatioConfig(value?: unknown): PublishRatioConfig {
function normalizeSalePriceConfig(value?: unknown): PublishSalePriceConfig {
const row = isRecord(value) ? value : {}
- return {
+ const config = {
fixed_markup_rules: normalizeSaleFixedMarkupRules(
Array.isArray(row.fixed_markup_rules) ? row.fixed_markup_rules : [],
),
@@ -248,6 +261,10 @@ function normalizeSalePriceConfig(value?: unknown): PublishSalePriceConfig {
Array.isArray(row.ratio_adjustment_rules) ? row.ratio_adjustment_rules : [],
),
}
+ if (config.fixed_markup_rules.length === 0 && config.ratio_adjustment_rules.length === 0) {
+ return JSON.parse(JSON.stringify(emptyListingSalePriceConfig)) as PublishSalePriceConfig
+ }
+ return config
}
function normalizeInsuranceBaseRatios(values: unknown[]): PublishInsuranceBaseRatio[] {
diff --git a/frontend/src/views/admin/AdminSystemConfigsView.vue b/frontend/src/views/admin/AdminSystemConfigsView.vue
index 0cfaa2b..b6e8143 100644
--- a/frontend/src/views/admin/AdminSystemConfigsView.vue
+++ b/frontend/src/views/admin/AdminSystemConfigsView.vue
@@ -463,7 +463,7 @@ function readError(error: unknown, fallback: string) {
Sale Pricing
内部出售定价规则
-
管理出售加价和比例扣减规则,仅用于平台内部定价计算,不在发布页展示。
+
管理出售专用比例计算规则,仅用于平台内部定价计算,不在发布页展示。
编辑出售规则
@@ -474,7 +474,7 @@ function readError(error: unknown, fallback: string) {
{{ salePriceStats.ratioCount }}
- 比例扣减
+ 比例修正
内部
@@ -843,8 +843,8 @@ function readError(error: unknown, fallback: string) {
- 90M 以上比例调整:出售比例 = 回收比例 - 比例扣减;最大 M 为 0 表示无上限
- 添加比例调整
+ 90M 以上比例修正:出售比例 = 回收比例 - 比例修正;最大 M 为 0 表示无上限
+ 添加比例修正
@@ -853,7 +853,7 @@ function readError(error: unknown, fallback: string) {
-
+
diff --git a/frontend/src/views/mobile/MobileSellerListingCreateView.css b/frontend/src/views/mobile/MobileSellerListingCreateView.css
index 4ec19dd..9ccffc5 100644
--- a/frontend/src/views/mobile/MobileSellerListingCreateView.css
+++ b/frontend/src/views/mobile/MobileSellerListingCreateView.css
@@ -498,6 +498,104 @@
font-weight: 800;
}
+.ratio-panel {
+ display: grid;
+ gap: 10px;
+ margin-bottom: 8px;
+}
+
+.ratio-reference {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ min-height: 48px;
+ padding: 10px 12px;
+ border-radius: 8px;
+ background: #f8fafc;
+}
+
+.ratio-reference span {
+ color: #30343a;
+ font-size: 13px;
+ font-weight: 700;
+}
+
+.ratio-reference strong {
+ color: #ff6a00;
+ font-size: 18px;
+ font-weight: 900;
+ line-height: 1;
+}
+
+.ratio-range {
+ margin: 0;
+ color: #858c96;
+ font-size: 11px;
+ font-weight: 600;
+ line-height: 1.5;
+}
+
+.ratio-mode-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 8px;
+}
+
+.ratio-mode-btn {
+ display: grid;
+ gap: 5px;
+ min-width: 0;
+ min-height: 74px;
+ padding: 12px 8px;
+ border: 1px solid #d7dce3;
+ border-radius: 8px;
+ background: #fff;
+ color: #30343a;
+ text-align: center;
+}
+
+.ratio-mode-btn strong,
+.ratio-mode-btn span {
+ min-width: 0;
+ overflow-wrap: anywhere;
+}
+
+.ratio-mode-btn strong {
+ font-size: 14px;
+ font-weight: 800;
+}
+
+.ratio-mode-btn span {
+ color: #858c96;
+ font-size: 12px;
+ font-weight: 700;
+ line-height: 1.25;
+}
+
+.ratio-mode-btn.active {
+ border-color: #ff6a00;
+ background: #fff7f0;
+ color: #ff6a00;
+}
+
+.ratio-mode-btn.active span {
+ color: #ff6a00;
+}
+
+.ratio-mode-btn:disabled {
+ cursor: not-allowed;
+ opacity: 0.58;
+}
+
+.ratio-input-field {
+ margin-bottom: 0;
+}
+
+.ratio-input-hint {
+ margin: -2px 0 0;
+}
+
.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 255f63d..6c41882 100644
--- a/frontend/src/views/mobile/MobileSellerListingCreateView.vue
+++ b/frontend/src/views/mobile/MobileSellerListingCreateView.vue
@@ -125,7 +125,6 @@ const fireLevelMin = computed(() => publishOptions.value.fire_level_min || 38);
const fireLevelPlaceholder = computed(
() => `等级低于${fireLevelMin.value}级的号无法发布`
);
-const ratioConfig = computed(() => publishOptions.value.ratio_config);
const skinGroups = computed(() => publishOptions.value.skin_groups);
const quantityItems = computed(() => publishOptions.value.quantity_items);
const screenshotSlots = computed(() => publishOptions.value.screenshot_slots);
@@ -140,16 +139,14 @@ const dailyLossMAmount = computed(() => Number(form.daily_loss_m || 10));
const dailyLossRatioAdjustment = computed(() =>
Math.min(Math.max(Math.floor((dailyLossMAmount.value - 10) / 10), 0), 4)
);
-const calculatedRecycleRatio = computed(() => calculatePublishRatio());
-const calculatedSalePricing = computed(() => calculateSalePricing());
-const calculatedDefaultSaleRatio = computed(() => calculatedSalePricing.value.saleRatio);
+const calculatedSellerReferenceRatio = computed(() => calculateSellerReferenceRatio());
+const calculatedDefaultSaleRatio = computed(() => calculatedSellerReferenceRatio.value);
const maxAcceleratedSaleRatio = computed(() =>
calculatedDefaultSaleRatio.value > 0 ? roundRatio(calculatedDefaultSaleRatio.value + 10) : 0
);
const calculatedRatio = computed(() => readFinalSaleRatio());
const calculatedCoinBasePrice = computed(() => {
if (calculatedRatio.value <= 0) return 0;
- if (!hasAcceleratedSaleRatioInput()) return calculatedSalePricing.value.coinBasePrice;
return roundMoney(coinWanAmount.value / calculatedRatio.value);
});
const acceleratedSaleRatioPlaceholder = computed(() => {
@@ -157,14 +154,23 @@ const acceleratedSaleRatioPlaceholder = computed(() => {
return `默认 ${formatNumber(calculatedDefaultSaleRatio.value)},最高 ${formatNumber(maxAcceleratedSaleRatio.value)}`;
});
const calculatedConsumablePrice = computed(() => calculateConsumablePrice());
-const calculatedFinalPrice = computed(() =>
+const calculatedSellerPrice = computed(() =>
calculatedRatio.value > 0
? roundMoney(calculatedCoinBasePrice.value + calculatedConsumablePrice.value)
: 0
);
+const calculatedPlatformPricing = computed(() => calculatePlatformPricing());
+const calculatedFinalPrice = computed(() => calculatedPlatformPricing.value.buyerTotalPrice);
const calculatedRatioText = computed(() =>
calculatedRatio.value > 0 ? `1:${formatNumber(calculatedRatio.value)}` : ""
);
+const calculatedDefaultSaleRatioText = computed(() =>
+ calculatedDefaultSaleRatio.value > 0 ? `1:${formatNumber(calculatedDefaultSaleRatio.value)}` : "--"
+);
+const saleRatioRangeText = computed(() => {
+ if (calculatedDefaultSaleRatio.value <= 0) return "完成基础信息后自动计算参考比例";
+ return `可设置 1:${formatNumber(calculatedDefaultSaleRatio.value)} ~ 1:${formatNumber(maxAcceleratedSaleRatio.value)}`;
+});
onMounted(() => {
restoreDraft();
@@ -403,6 +409,15 @@ function clampAcceleratedSaleRatioInput() {
form.accelerated_sale_ratio = roundRatio(Math.min(Math.max(ratio, minRatio), maxRatio));
}
+function useReferenceSaleRatio() {
+ form.accelerated_sale_ratio = "";
+}
+
+function useMaxAcceleratedSaleRatio() {
+ if (maxAcceleratedSaleRatio.value <= 0) return;
+ form.accelerated_sale_ratio = maxAcceleratedSaleRatio.value;
+}
+
async function handleSubmit() {
const error = validateForm();
if (error) {
@@ -505,19 +520,20 @@ function buildAssetSummary() {
secret_kd: form.secret_kd,
fire_level: Number(form.fire_level),
daily_loss_m: dailyLossMAmount.value,
- publish_ratio: calculatedRatio.value,
+ publish_ratio: calculatedPlatformPricing.value.buyerRatio,
price_breakdown: {
- recycle_coin_base_price: calculatedSalePricing.value.recycleCoinBasePrice,
- default_coin_base_price: calculatedSalePricing.value.coinBasePrice,
- coin_base_price: calculatedCoinBasePrice.value,
+ seller_reference_ratio: calculatedSellerReferenceRatio.value,
+ seller_ratio: calculatedRatio.value,
+ seller_coin_base_price: calculatedCoinBasePrice.value,
+ seller_total_price: calculatedSellerPrice.value,
consumable_price: calculatedConsumablePrice.value,
- final_price: calculatedFinalPrice.value,
- recycle_ratio: calculatedRecycleRatio.value,
daily_loss_ratio_adjustment: dailyLossRatioAdjustment.value,
- default_sale_ratio: calculatedDefaultSaleRatio.value,
- sale_ratio: calculatedRatio.value,
accelerated_sale_ratio: hasAcceleratedSaleRatioInput() ? Number(form.accelerated_sale_ratio) : calculatedDefaultSaleRatio.value,
- sale_rule_type: calculatedSalePricing.value.ruleType,
+ buyer_coin_base_price: calculatedPlatformPricing.value.buyerCoinBasePrice,
+ buyer_total_price: calculatedFinalPrice.value,
+ buyer_ratio: calculatedPlatformPricing.value.buyerRatio,
+ platform_markup_amount: calculatedPlatformPricing.value.platformMarkupAmount,
+ platform_rule_type: calculatedPlatformPricing.value.ruleType,
},
season_insurance: form.season_insurance,
stamina_level: form.stamina_level,
@@ -571,7 +587,7 @@ function readUnitPrice(priceText: string) {
return singleMatch ? Number(singleMatch[1]) : 0;
}
-function calculatePublishRatio() {
+function calculateSellerReferenceRatio() {
if (
coinMAmount.value <= 0 ||
!form.season_insurance ||
@@ -580,13 +596,13 @@ function calculatePublishRatio() {
) {
return 0;
}
- const baseRatio = getInsuranceBaseRatio(form.season_insurance);
+ const baseRatio = getInsuranceBaseRatio(publishOptions.value.ratio_config, form.season_insurance);
if (baseRatio <= 0) return 0;
return (
baseRatio +
- calculateConfigPenalty() +
- getCoinCorrection(coinMAmount.value) +
+ calculateConfigPenalty(publishOptions.value.ratio_config) +
+ getCoinCorrection(publishOptions.value.ratio_config, coinMAmount.value) +
dailyLossRatioAdjustment.value
);
}
@@ -604,42 +620,44 @@ function hasAcceleratedSaleRatioInput() {
return form.accelerated_sale_ratio !== "" && form.accelerated_sale_ratio !== null;
}
-function calculateSalePricing() {
- const recycleRatio = calculatedRecycleRatio.value;
- if (recycleRatio <= 0 || coinWanAmount.value <= 0) {
- return emptySalePricing();
+function calculatePlatformPricing() {
+ if (calculatedRatio.value <= 0 || calculatedCoinBasePrice.value <= 0) {
+ return emptyPlatformPricing();
}
- 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",
- };
+ return buildPlatformPricing(
+ roundMoney(calculatedCoinBasePrice.value + Number(fixedRule.markup_amount || 0)),
+ "fixed_markup",
+ );
}
const ratioRule = findSaleRatioAdjustmentRule();
const ratioSubtract = ratioRule ? Number(ratioRule.ratio_subtract || 0) : 0;
- const saleRatio = recycleRatio - ratioSubtract;
- if (saleRatio <= 0) {
- return emptySalePricing(recycleCoinBasePrice);
+ const buyerRatio = calculatedRatio.value - ratioSubtract;
+ if (buyerRatio > 0 && ratioRule) {
+ return buildPlatformPricing(roundMoney(coinWanAmount.value / buyerRatio), "ratio_subtract");
}
+ return buildPlatformPricing(calculatedCoinBasePrice.value, "none");
+}
+
+function buildPlatformPricing(buyerCoinBasePrice: number, ruleType: string) {
+ const buyerTotalPrice = roundMoney(buyerCoinBasePrice + calculatedConsumablePrice.value);
return {
- recycleCoinBasePrice,
- coinBasePrice: roundMoney(coinWanAmount.value / saleRatio),
- saleRatio,
- ruleType: ratioRule ? "ratio_subtract" : "none",
+ buyerCoinBasePrice,
+ buyerTotalPrice,
+ buyerRatio: calculateEffectiveRatio(buyerCoinBasePrice),
+ platformMarkupAmount: roundMoney(buyerTotalPrice - calculatedSellerPrice.value),
+ ruleType,
};
}
-function emptySalePricing(recycleCoinBasePrice = 0) {
+function emptyPlatformPricing() {
return {
- recycleCoinBasePrice,
- coinBasePrice: 0,
- saleRatio: 0,
+ buyerCoinBasePrice: 0,
+ buyerTotalPrice: 0,
+ buyerRatio: 0,
+ platformMarkupAmount: 0,
ruleType: "none",
};
}
@@ -647,18 +665,27 @@ function emptySalePricing(recycleCoinBasePrice = 0) {
function findSaleFixedMarkupRule() {
return [...salePriceConfig.value.fixed_markup_rules]
.sort((a, b) => a.min_m - b.min_m)
- .find((item) => isCoinInSaleRange(item));
+ .find((item, index, rules) => isCoinInSaleRange(item, index, rules, { includeLastMax: true }));
}
function findSaleRatioAdjustmentRule() {
return [...salePriceConfig.value.ratio_adjustment_rules]
.sort((a, b) => a.min_m - b.min_m)
- .find((item) => isCoinInSaleRange(item));
+ .find((item, index, rules) => isCoinInSaleRange(item, index, rules, { excludeFirstMin: true }));
}
-function isCoinInSaleRange(item: { min_m: number; max_m: number }) {
+function isCoinInSaleRange(
+ item: { min_m: number; max_m: number },
+ index: number,
+ rules: Array<{ min_m: number; max_m: number }>,
+ options: { includeLastMax?: boolean; excludeFirstMin?: boolean } = {},
+) {
const maxM = Number(item.max_m || 0);
- return coinMAmount.value >= Number(item.min_m || 0) && (maxM <= 0 || coinMAmount.value <= maxM);
+ const minM = Number(item.min_m || 0);
+ const minMatched = options.excludeFirstMin && index === 0 ? coinMAmount.value > minM : coinMAmount.value >= minM;
+ const isLastRule = index === rules.length - 1;
+ const maxMatched = maxM <= 0 || coinMAmount.value < maxM || (options.includeLastMax && isLastRule && coinMAmount.value <= maxM);
+ return minMatched && maxMatched;
}
function calculateEffectiveRatio(price: number) {
@@ -666,14 +693,14 @@ function calculateEffectiveRatio(price: number) {
return roundRatio(coinWanAmount.value / price);
}
-function getInsuranceBaseRatio(insurance: string) {
- return ratioConfig.value.insurance_base_ratios.find(
+function getInsuranceBaseRatio(config: { insurance_base_ratios: Array<{ insurance: string; ratio: number }> }, insurance: string) {
+ return config.insurance_base_ratios.find(
(item) => item.insurance === insurance
)?.ratio || 0;
}
-function calculateConfigPenalty() {
- return ratioConfig.value.config_items.reduce((sum, item) => {
+function calculateConfigPenalty(config: { config_items: Array<{ kind: string; group_key?: string; missing_penalty: number }> }) {
+ return config.config_items.reduce((sum, item) => {
return isRatioConfigItemMatched(item) ? sum : sum + Number(item.missing_penalty || 0);
}, 0);
}
@@ -703,8 +730,8 @@ function readLevelNumber(value: string) {
return match ? Number(match[0]) : 0;
}
-function getCoinCorrection(coinM: number) {
- return [...ratioConfig.value.coin_corrections]
+function getCoinCorrection(config: { coin_corrections: Array<{ threshold_m: number; correction: number }> }, coinM: number) {
+ return [...config.coin_corrections]
.sort((a, b) => b.threshold_m - a.threshold_m)
.find((item) => coinM > item.threshold_m)?.correction || 0;
}
@@ -1092,28 +1119,69 @@ function readError(error: unknown, fallback: string) {
比例调整:在默认10M/天,每增加10M,出租比例相应+1,请根据实际需求合理设置,感谢您的配合。
-
-
- 请根据实际需求合理设置,感谢您的配合。
-
+
+
+ 参考比例
+ {{ calculatedDefaultSaleRatioText }}
+
+
{{ saleRatioRangeText }},比例越高出租速度越快,自行选择。
+
+
+
+
+
+
+ 留空使用参考比例;填写后范围为参考比例到参考比例+10,请根据实际需求合理设置。
+
+
- 出售比例
+ 卖家比例
- 发布总价 = 纯币基础价 + 额外消耗品;加速比例仅影响纯币基础价。
+ 卖家价格 = 纯币基础价 + 额外消耗品;加速比例仅影响纯币基础价。
).seller_total_price)
+ if (Number.isFinite(price) && price > 0) return price
+ }
+ return Number(row.price_daily || row.price_hourly || 0)
}