This commit is contained in:
2026-05-13 14:43:38 +08:00
parent 92efdae9eb
commit c9102f2d51
18 changed files with 679 additions and 838 deletions

View 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 }
}