f
This commit is contained in:
162
src/utils/promotionPricing.js
Normal file
162
src/utils/promotionPricing.js
Normal file
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* 推广定价计算工具
|
||||
* 统一管理价格计算逻辑,供 Promote.vue 和 PriceInputPopup.vue 共享
|
||||
*/
|
||||
|
||||
/**
|
||||
* 安全截断小数位数(四舍五入)
|
||||
* 用于前端显示和发送给后端的价格格式化
|
||||
* @param {number} num - 要格式化的数值
|
||||
* @param {number} decimals - 保留小数位数,默认2位
|
||||
* @returns {string} 格式化后的字符串,如 "12.34"
|
||||
*/
|
||||
export function safeTruncate(num, decimals = 2) {
|
||||
if (Number.isNaN(num) || !Number.isFinite(num)) {
|
||||
return '0.00'
|
||||
}
|
||||
|
||||
const factor = 10 ** decimals
|
||||
const scaled = Math.round(num * factor)
|
||||
return (scaled / factor).toFixed(decimals)
|
||||
}
|
||||
|
||||
/**
|
||||
* 将元转为分(整数),避免浮点运算精度问题
|
||||
* @param {number} num - 元为单位的数值
|
||||
* @returns {number} 分为单位的整数
|
||||
*/
|
||||
function toTruncatedCents(num) {
|
||||
if (Number.isNaN(num) || !Number.isFinite(num)) {
|
||||
return 0
|
||||
}
|
||||
return Math.round(num * 100)
|
||||
}
|
||||
|
||||
/**
|
||||
* 将分转回元,返回固定2位小数字符串
|
||||
* @param {number} cents - 分为单位的整数
|
||||
* @returns {string} 元为单位的字符串,如 "12.34"
|
||||
*/
|
||||
function centsToFixed(cents) {
|
||||
return (cents / 100).toFixed(2)
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算提价成本
|
||||
* 当客户查询价超过价格阈值时,超出部分按费率收取
|
||||
* @param {number} price - 客户查询价
|
||||
* @param {object} config - 产品配置
|
||||
* @returns {number} 提价成本(元)
|
||||
*/
|
||||
function calculateRaiseCost(price, config) {
|
||||
const priceThreshold = Number(config.price_threshold) || 0
|
||||
const priceFeeRate = Number(config.price_fee_rate) || 0
|
||||
|
||||
if (priceThreshold <= 0 || priceFeeRate <= 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
if (price <= priceThreshold) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return (price - priceThreshold) * priceFeeRate
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算推广定价(核心函数)
|
||||
* 所有金额先转为分(整数)再计算,避免浮点精度问题
|
||||
*
|
||||
* @param {number|string} priceInput - 客户查询价
|
||||
* @param {object} config - 产品配置,包含以下字段:
|
||||
* - actual_base_price: 实际底价(含等级加成)
|
||||
* - price_threshold: 价格阈值(超过此价格收取提价费用)
|
||||
* - price_fee_rate: 提价费率(0-1之间的小数)
|
||||
* @returns {{ costPrice: string, baseCost: string, raiseCost: string, promotionRevenue: string }}
|
||||
* 各项价格均为 "xx.xx" 格式的字符串
|
||||
*/
|
||||
export function calculatePromotionPricing(priceInput, config) {
|
||||
if (!config) {
|
||||
return {
|
||||
costPrice: '0.00',
|
||||
baseCost: '0.00',
|
||||
raiseCost: '0.00',
|
||||
promotionRevenue: '0.00',
|
||||
}
|
||||
}
|
||||
|
||||
const price = Number(priceInput)
|
||||
if (!Number.isFinite(price)) {
|
||||
return {
|
||||
costPrice: '0.00',
|
||||
baseCost: '0.00',
|
||||
raiseCost: '0.00',
|
||||
promotionRevenue: '0.00',
|
||||
}
|
||||
}
|
||||
|
||||
const baseCost = Number(config.actual_base_price) || 0
|
||||
const raiseCost = calculateRaiseCost(price, config)
|
||||
const totalCost = baseCost + raiseCost
|
||||
|
||||
// 转为分计算,避免浮点精度问题
|
||||
const priceCents = toTruncatedCents(price)
|
||||
const baseCostCents = toTruncatedCents(baseCost)
|
||||
const raiseCostCents = toTruncatedCents(raiseCost)
|
||||
const totalCostCents = toTruncatedCents(totalCost)
|
||||
const revenueCents = Math.max(0, priceCents - totalCostCents)
|
||||
|
||||
return {
|
||||
costPrice: centsToFixed(totalCostCents),
|
||||
baseCost: centsToFixed(baseCostCents),
|
||||
raiseCost: centsToFixed(raiseCostCents),
|
||||
promotionRevenue: centsToFixed(revenueCents),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证价格输入
|
||||
* @param {number|string} currentPrice - 当前输入的价格
|
||||
* @param {object} productConfig - 产品配置
|
||||
* @param {number} defaultPrice - 默认价格(用于无效输入时的回退)
|
||||
* @returns {{ newPrice: number, message: string }}
|
||||
*/
|
||||
export function validatePrice(currentPrice, productConfig, defaultPrice) {
|
||||
if (!productConfig) {
|
||||
return { newPrice: Number(defaultPrice || 0), message: '产品配置未就绪,请稍后再试' }
|
||||
}
|
||||
|
||||
const min = Number(productConfig.price_range_min) || 0
|
||||
const max = Number(productConfig.price_range_max) || Infinity
|
||||
let newPrice = Number(currentPrice)
|
||||
let message = ''
|
||||
|
||||
// 处理无效输入
|
||||
if (Number.isNaN(newPrice)) {
|
||||
newPrice = Number(defaultPrice || 0)
|
||||
return { newPrice, message: '输入无效,请输入价格' }
|
||||
}
|
||||
|
||||
// 处理小数位数(兼容科学计数法)
|
||||
try {
|
||||
const priceString = newPrice.toString()
|
||||
const [_, decimalPart = ''] = priceString.split('.')
|
||||
if (decimalPart.length > 2) {
|
||||
newPrice = Number.parseFloat(safeTruncate(newPrice))
|
||||
message = '价格已自动格式化为两位小数'
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('价格格式化异常:', e)
|
||||
}
|
||||
|
||||
// 范围校验
|
||||
if (newPrice < min) {
|
||||
message = `价格不能低于 ${min} 元`
|
||||
newPrice = min
|
||||
} else if (newPrice > max) {
|
||||
message = `价格不能高于 ${max} 元`
|
||||
newPrice = max
|
||||
}
|
||||
|
||||
return { newPrice, message }
|
||||
}
|
||||
Reference in New Issue
Block a user