import type { ChargeMode, ListingPublishOptions, PublishDepositRecommendConfig, PublishOptionGroup, PublishQuantityItem, PublishRatioConfig, PublishSalePriceConfig, } from '@/features/listings/api/listingOptions' import type { DepositBreakdownItem, PublishForm, PublishPlatformPricing, } from '@/shared/types/publish' export const dailyLossOptions = [10, 20, 30, 40, 50] /** * 将金额四舍五入到角精度(0.1元) * 统一全项目金额处理规范 * @example roundMoney(12.34) -> 12.3 * @example roundMoney(12.36) -> 12.4 * @example roundMoney(12.35) -> 12.4 */ export function roundMoney(value: number) { return Math.round(value * 10) / 10 } /** * 将比例四舍五入到一位小数 */ export function roundRatio(value: number) { return Math.round(value * 10) / 10 } /** * 格式化数字为字符串(保留1位小数) */ export function formatNumber(value: number) { const rounded = Math.round(value * 10) / 10 return rounded.toFixed(1) } export function readUnitPrice(priceText: string) { const normalized = priceText.replace(/,/g, ',').trim() const fractionMatch = normalized.match(/(\d+(?:\.\d+)?)\s*元\s*\/\s*(\d+(?:\.\d+)?)/) if (fractionMatch) { const amount = Number(fractionMatch[1]) const count = Number(fractionMatch[2]) return count > 0 ? amount / count : 0 } const singleMatch = normalized.match(/(\d+(?:\.\d+)?)\s*元/) return singleMatch ? Number(singleMatch[1]) : 0 } export function calculateDailyLossRatioAdjustment(dailyLossMAmount: number) { return Math.min(Math.max(Math.floor((dailyLossMAmount - 10) / 10), 0), 4) } export function isGridCardQuantityItem(item: { key: string; label: string }) { return item.key === 'gridCard9' || item.label.includes('9格体验卡') } export function isQuantityItemDisabledForInsurance( item: { key: string; label: string }, seasonInsurance: string ) { return seasonInsurance === '3*3' && isGridCardQuantityItem(item) } export function calculateConsumablePrice(options: { quantityItems: PublishQuantityItem[] quantityValues: Record quantityModes: Record seasonInsurance: string }) { const total = options.quantityItems.reduce((sum, item) => { const quantity = Number(options.quantityValues[item.key] || 0) const mode = options.quantityModes[item.key] || '收费' if (isQuantityItemDisabledForInsurance(item, options.seasonInsurance)) return sum if (quantity <= 0 || mode !== '收费') return sum return sum + quantity * readUnitPrice(item.price) }, 0) return roundMoney(total) } export function calculateRecommendedDeposit(options: { depositRecommendConfig: PublishDepositRecommendConfig skinGroups: PublishOptionGroup[] selectedSkins: string[] }) { const baseAmount = Number(options.depositRecommendConfig.base_amount || 0) const skinAmount = options.depositRecommendConfig.skin_group_rules.reduce((sum, rule) => { const group = options.skinGroups.find(item => item.key === rule.group_key) if (!group) return sum const selectedCount = group.options.filter(skin => options.selectedSkins.includes(skin)).length return sum + selectedCount * Number(rule.amount_per_item || 0) }, 0) return roundMoney(baseAmount + skinAmount) } export function buildDepositBreakdownItems(options: { depositRecommendConfig: PublishDepositRecommendConfig skinGroups: PublishOptionGroup[] selectedSkins: string[] }): DepositBreakdownItem[] { const items: DepositBreakdownItem[] = [ { label: '基础押金', amount: Number(options.depositRecommendConfig.base_amount || 0), count: 1, }, ] for (const rule of options.depositRecommendConfig.skin_group_rules) { const group = options.skinGroups.find(item => item.key === rule.group_key) if (!group) continue const count = group.options.filter(skin => options.selectedSkins.includes(skin)).length if (count <= 0) continue items.push({ label: rule.label, amount: Number(rule.amount_per_item || 0) * count, count, }) } return items } export function calculateSellerReferenceRatio(options: { coinMAmount: number form: PublishForm ratioConfig: PublishRatioConfig skinGroups: PublishOptionGroup[] selectedSkins: string[] levelOptions: string[] dailyLossRatioAdjustment: number }) { const { coinMAmount, form, ratioConfig } = options if (coinMAmount <= 0 || !form.season_insurance || !form.stamina_level || !form.load_level) return 0 const baseRatio = getInsuranceBaseRatio(ratioConfig, form.season_insurance) if (baseRatio <= 0) return 0 return ( baseRatio + calculateConfigPenalty(ratioConfig, options) + getCoinCorrection(ratioConfig, coinMAmount) + options.dailyLossRatioAdjustment ) } export function readFinalSaleRatio( defaultRatio: number, acceleratedSaleRatio: number | '', maxAcceleratedSaleRatio: number ) { if (defaultRatio <= 0) return 0 if (!hasAcceleratedSaleRatioInput(acceleratedSaleRatio)) return defaultRatio const ratio = Number(acceleratedSaleRatio) if (!Number.isFinite(ratio) || ratio <= 0) return defaultRatio return roundRatio(Math.min(Math.max(ratio, defaultRatio), maxAcceleratedSaleRatio)) } export function hasAcceleratedSaleRatioInput(value: number | '' | null | undefined) { const ratio = Number(value) return ( value !== '' && value !== null && value !== undefined && Number.isFinite(ratio) && ratio > 0 ) } export function calculatePlatformPricing(options: { coinMAmount: number coinWanAmount: number sellerRatio: number sellerCoinBasePrice: number sellerTotalPrice: number consumablePrice: number salePriceConfig: PublishSalePriceConfig }): PublishPlatformPricing { if (options.sellerRatio <= 0 || options.sellerCoinBasePrice <= 0) return emptyPlatformPricing() const fixedRule = findSaleFixedMarkupRule(options.salePriceConfig, options.coinMAmount) if (fixedRule) { return buildPlatformPricing( roundMoney(options.sellerCoinBasePrice + Number(fixedRule.markup_amount || 0)), 'fixed_markup', options ) } const ratioRule = findSaleRatioAdjustmentRule(options.salePriceConfig, options.coinMAmount) const ratioSubtract = ratioRule ? Number(ratioRule.ratio_subtract || 0) : 0 const buyerRatio = options.sellerRatio - ratioSubtract if (buyerRatio > 0 && ratioRule) { return buildPlatformPricing( roundMoney(options.coinWanAmount / buyerRatio), 'ratio_subtract', options ) } return buildPlatformPricing(options.sellerCoinBasePrice, 'none', options) } export function emptyPlatformPricing(): PublishPlatformPricing { return { buyerCoinBasePrice: 0, buyerTotalPrice: 0, buyerRatio: 0, platformMarkupAmount: 0, ruleType: 'none', } } function buildPlatformPricing( buyerCoinBasePrice: number, ruleType: string, options: { coinWanAmount: number sellerTotalPrice: number consumablePrice: number } ): PublishPlatformPricing { const buyerTotalPrice = roundMoney(buyerCoinBasePrice + options.consumablePrice) return { buyerCoinBasePrice, buyerTotalPrice, buyerRatio: calculateEffectiveRatio(options.coinWanAmount, buyerCoinBasePrice), platformMarkupAmount: roundMoney(buyerTotalPrice - options.sellerTotalPrice), ruleType, } } function findSaleFixedMarkupRule(config: PublishSalePriceConfig, coinMAmount: number) { return [...config.fixed_markup_rules] .sort((a, b) => a.min_m - b.min_m) .find((item, index, rules) => isCoinInSaleRange(item, index, rules, coinMAmount, { includeLastMax: true }) ) } function findSaleRatioAdjustmentRule(config: PublishSalePriceConfig, coinMAmount: number) { return [...config.ratio_adjustment_rules] .sort((a, b) => a.min_m - b.min_m) .find((item, index, rules) => isCoinInSaleRange(item, index, rules, coinMAmount, { excludeFirstMin: true }) ) } function isCoinInSaleRange( item: { min_m: number; max_m: number }, index: number, rules: Array<{ min_m: number; max_m: number }>, coinMAmount: number, options: { includeLastMax?: boolean; excludeFirstMin?: boolean } = {} ) { const maxM = Number(item.max_m || 0) const minM = Number(item.min_m || 0) const minMatched = options.excludeFirstMin && index === 0 ? coinMAmount > minM : coinMAmount >= minM const isLastRule = index === rules.length - 1 const maxMatched = maxM <= 0 || coinMAmount < maxM || (options.includeLastMax && isLastRule && coinMAmount <= maxM) return minMatched && maxMatched } function calculateEffectiveRatio(coinWanAmount: number, price: number) { if (price <= 0) return 0 return roundRatio(coinWanAmount / price) } function getInsuranceBaseRatio( config: Pick, insurance: string ) { return config.insurance_base_ratios.find(item => item.insurance === insurance)?.ratio || 0 } function calculateConfigPenalty( config: Pick, options: { form: PublishForm skinGroups: PublishOptionGroup[] selectedSkins: string[] levelOptions: string[] } ) { return config.config_items.reduce((sum, item) => { return isRatioConfigItemMatched(item, options) ? sum : sum + Number(item.missing_penalty || 0) }, 0) } function isRatioConfigItemMatched( item: { kind: string; group_key?: string }, options: { form: PublishForm skinGroups: PublishOptionGroup[] selectedSkins: string[] levelOptions: string[] } ) { if (item.kind === 'skin_group') return hasSelectedSkinGroup(item.group_key || '', options) if (item.kind === 'max_stamina') return isMaxLevel(options.form.stamina_level, options.levelOptions) if (item.kind === 'max_load') return isMaxLevel(options.form.load_level, options.levelOptions) return false } function hasSelectedSkinGroup( groupKey: string, options: { skinGroups: PublishOptionGroup[] selectedSkins: string[] } ) { const group = options.skinGroups.find(item => item.key === groupKey) if (!group) return false return group.options.some(skin => options.selectedSkins.includes(skin)) } function isMaxLevel(value: string, levelOptions: string[]) { const currentLevel = readLevelNumber(value) const maxLevel = Math.max(...levelOptions.map(readLevelNumber).filter(Boolean)) if (currentLevel > 0 && maxLevel > 0) return currentLevel >= maxLevel return value === levelOptions[levelOptions.length - 1] } function readLevelNumber(value: string) { const match = value.match(/\d+/) return match ? Number(match[0]) : 0 } function getCoinCorrection( config: Pick, coinM: number ) { return ( [...config.coin_corrections] .sort((a, b) => b.threshold_m - a.threshold_m) .find(item => coinM > item.threshold_m)?.correction || 0 ) }