@@ -1,856 +0,0 @@
/**
* 购物车订单价格计算公共库
* 功能:覆盖订单全链路费用计算(商品价格、优惠券、积分、餐位费等),支持策略扩展
* 小数处理:统一舍去小数点后两位(如 10.129 → 10.12, 15.998 → 15.99)
* 扩展设计:优惠券/营销活动采用策略模式,新增类型仅需扩展策略,无需修改核心逻辑
*/
// ============================ 1. 基础类型定义(扩展可复用) ============================
/** 商品类型枚举 */
export enum GoodsType {
NORMAL = 'normal' , // 普通商品
WEIGHT = 'weight' , // 称重商品
GIFT = 'gift' , // 赠菜(继承普通商品逻辑,标记用)
}
// 定义计算结果类型(核心修正)
interface CouponResult {
deductionAmount : number ; // 抵扣金额
excludedProductIds : string [ ] ; // 不适用商品ID列表( 修正为 string[],而非 never[])
usedCoupon : Coupon | undefined ; // 允许为优惠券类型或 undefined
}
interface ExchangeCalculationResult {
deductionAmount : number ;
excludedProductIds : string [ ] ;
}
/** 优惠券类型枚举 */
export enum CouponType {
FULL_REDUCTION = 'full_reduction' , // 满减券
DISCOUNT = 'discount' , // 折扣券
SECOND_HALF = 'second_half' , // 第二件半价券
BUY_ONE_GET_ONE = 'buy_one_get_one' , // 买一送一券
EXCHANGE = 'exchange' , // 商品兑换券
}
/** 营销活动类型枚举 */
export enum ActivityType {
TIME_LIMIT_DISCOUNT = 'time_limit_discount' , // 限时折扣
}
/** 基础购物车商品项(含普通/称重/临时/赠菜) */
export interface BaseCartItem {
id : string | number ;
salePrice : number ; // 商品原价(元)
number : number ; // 商品数量
productType : GoodsType ; // 商品类型
isTemporary? : boolean ; // 是否临时菜( 默认false)
isGift? : boolean ; // 是否赠菜( 默认false)
returnNum? : number ; // 退货数量( 历史订单用, 默认0)
memberPrice? : number ; // 商品会员价(元,优先级:会员价 > 会员折扣)
discountSaleAmount? : number ; // 商家改价后单价(元,优先级最高)
packFee? : number ; // 单份打包费( 元, 默认0)
packNumber? : number ; // 堂食打包数量( 默认0)
activityInfo ? : { // 商品参与的营销活动(如限时折扣)
type : ActivityType ;
discountRate : number ; // 折扣率( 如0.8=8折)
shareWithMember : boolean ; // 是否与会员优惠同享( 默认false)
} ;
skuData ? : { // SKU扩展数据( 可选)
memberPrice? : number ;
salePrice? : number ;
} ;
}
/** 基础优惠券接口 */
interface BaseCoupon {
id : string | number ;
type : CouponType ; // 优惠券类型
name : string ; // 优惠券名称
available : boolean ; // 是否可用(当日剩余数量>0)
applicableStoreIds : string [ ] ; // 适用门店ID( 当前门店需在列)
shareWithActivity : boolean ; // 是否与营销活动同享( 默认false)
shareWithMember : boolean ; // 是否与会员优惠同享( 默认false)
}
/** 满减券( 示例: 满100减20) */
export interface FullReductionCoupon extends BaseCoupon {
type : CouponType . FULL_REDUCTION ;
fullAmount : number ; // 满减门槛(元)
reductionAmount : number ; // 减免金额(元)
maxReductionAmount? : number ; // 最大减免金额( 元, 默认等于reductionAmount)
}
/** 折扣券( 示例: 9折, 最大减50) */
export interface DiscountCoupon extends BaseCoupon {
type : CouponType . DISCOUNT ;
discountRate : number ; // 折扣率( 如0.9=9折, 需>0且<1)
maxReductionAmount : number ; // 最大减免金额(元,避免折扣过大)
}
/** 第二件半价券(限指定商品) */
export interface SecondHalfPriceCoupon extends BaseCoupon {
type : CouponType . SECOND_HALF ;
applicableProductIds : string [ ] ; // 适用商品ID
maxUseCountPerOrder? : number ; // 每单最大使用次数(默认不限,按商品数量算)
}
/** 买一送一券(限指定商品) */
export interface BuyOneGetOneCoupon extends BaseCoupon {
type : CouponType . BUY_ONE_GET_ONE ;
applicableProductIds : string [ ] ; // 适用商品ID
maxUseCountPerOrder? : number ; // 每单最大使用次数(默认不限,按商品数量算)
}
/** 商品兑换券(抵扣指定商品金额) */
export interface ExchangeCoupon extends BaseCoupon {
type : CouponType . EXCHANGE ;
applicableProductIds : string [ ] ; // 适用商品ID
deductCount : number ; // 可抵扣商品件数( 如1=抵扣1件)
sortRule : 'low_price_first' | 'high_price_first' ; // 商品排序规则
}
/** 所有优惠券类型联合 */
export type Coupon = FullReductionCoupon | DiscountCoupon | SecondHalfPriceCoupon | BuyOneGetOneCoupon | ExchangeCoupon ;
/** 营销活动配置(如限时折扣) */
export interface ActivityConfig {
type : ActivityType ;
applicableProductIds? : string [ ] ; // 适用商品ID
discountRate : number ; // 折扣率( 如0.8=8折)
shareWithMember : boolean ; // 是否与会员优惠同享
}
/** 积分抵扣规则 */
export interface PointDeductionRule {
pointsPerYuan : number ; // X积分=1元( 如100=100积分抵1元)
maxDeductionAmount? : number ; // 最大抵扣金额(元,默认不限)
}
/** 餐位费配置 */
export interface SeatFeeConfig {
pricePerPerson : number ; // 每人餐位费(元)
personCount : number ; // 用餐人数( 默认1)
isEnabled : boolean ; // 是否启用餐位费( 默认false)
}
/** 订单额外费用配置 */
export interface OrderExtraConfig {
merchantReduction : number ; // 商家减免金额( 元, 默认0)
additionalFee : number ; // 附加费( 元, 如余额充值、券包, 默认0)
pointDeductionRule : PointDeductionRule ; // 积分抵扣规则
seatFeeConfig : SeatFeeConfig ; // 餐位费配置
currentStoreId : string ; // 当前门店ID( 用于验证优惠券适用门店)
userPoints : number ; // 用户当前积分(用于积分抵扣)
isMember : boolean ; // 用户是否会员(用于会员优惠)
memberDiscountRate? : number ; // 会员折扣率( 如0.95=95折, 无会员价时用)
}
/** 订单费用汇总(所有子项清晰展示,方便调用方使用) */
export interface OrderCostSummary {
goodsOriginalAmount : number ; // 商品原价总和
goodsDiscountAmount : number ; // 商品折扣金额(原价-实际价)
couponDeductionAmount : number ; // 优惠券抵扣金额
pointDeductionAmount : number ; // 积分抵扣金额
seatFee : number ; // 餐位费
packFee : number ; // 打包费
merchantReductionAmount : number ; // 商家减免金额
additionalFee : number ; // 附加费
finalPayAmount : number ; // 最终实付金额
// 辅助信息
couponUsed? : Coupon ; // 实际使用的优惠券(互斥时选最优)
pointUsed : number ; // 实际使用的积分
}
// ============================ 2. 基础工具函数(通用能力,无业务耦合) ============================
/**
* 统一小数处理:舍去小数点后两位(如 10.129 → 10.12, 15.998 → 15.99)
* @param num 待处理数字
* @returns 处理后保留两位小数的数字
*/
export function truncateToTwoDecimals ( num : number ) : number {
return Math . floor ( num * 100 ) / 100 ;
}
/**
* 判断商品是否为临时菜(临时菜不参与优惠券门槛和折扣计算)
* @param goods 商品项
* @returns 是否临时菜
*/
export function isTemporaryGoods ( goods : BaseCartItem ) : boolean {
return ! ! goods . isTemporary ;
}
/**
* 判断商品是否为赠菜(赠菜不计入优惠券活动,但计打包费)
* @param goods 商品项
* @returns 是否赠菜
*/
export function isGiftGoods ( goods : BaseCartItem ) : boolean {
return ! ! goods . isGift ;
}
/**
* 计算单个商品的会员价(优先级:商品会员价 > 会员折扣率)
* @param goods 商品项
* @param isMember 是否会员
* @param memberDiscountRate 会员折扣率( 如0.95=95折)
* @returns 会员价(元)
*/
export function calcMemberPrice (
goods : BaseCartItem ,
isMember : boolean ,
memberDiscountRate? : number
) : number {
if ( ! isMember ) return goods . salePrice ;
// 优先级:商品会员价 > SKU会员价 > 会员折扣价 > 原价
const basePrice = goods . memberPrice
? ? goods . skuData ? . memberPrice
? ? goods . salePrice ;
return memberDiscountRate && ! goods . memberPrice && ! goods . skuData ? . memberPrice
? truncateToTwoDecimals ( basePrice * memberDiscountRate )
: basePrice ;
}
/**
* 过滤可参与优惠券计算的商品(排除临时菜、赠菜、已用兑换券的商品)
* @param goodsList 商品列表
* @param excludedProductIds 需排除的商品ID( 如兑换券已抵扣的商品)
* @returns 可参与优惠券计算的商品列表
*/
export function filterCouponEligibleGoods (
goodsList : BaseCartItem [ ] ,
excludedProductIds : string [ ] = [ ]
) : BaseCartItem [ ] {
return goodsList . filter ( goods = >
! isTemporaryGoods ( goods )
&& ! isGiftGoods ( goods )
&& ! excludedProductIds . includes ( String ( goods . id ) )
) ;
}
/**
* 商品排序(用于商品兑换券:按价格/数量/加入顺序排序)
* @param goodsList 商品列表
* @param sortRule 排序规则( low_price_first/high_price_first)
* @param cartOrder 商品加入购物车的顺序( key=商品ID, value=加入时间戳)
* @returns 排序后的商品列表
*/
export function sortGoodsForCoupon (
goodsList : BaseCartItem [ ] ,
sortRule : 'low_price_first' | 'high_price_first' ,
cartOrder : Record < string , number > = { }
) : BaseCartItem [ ] {
return [ . . . goodsList ] . sort ( ( a , b ) = > {
// 1. 按价格排序
const priceA = a . salePrice ;
const priceB = b . salePrice ;
if ( priceA !== priceB ) {
return sortRule === 'low_price_first' ? priceA - priceB : priceB - priceA ;
}
// 2. 同价格按数量排序(降序,多的优先)
if ( a . number !== b . number ) {
return b . number - a . number ;
}
// 3. 同价格同数量按加入购物车顺序(早的优先)
const orderA = cartOrder [ String ( a . id ) ] ? ? Infinity ;
const orderB = cartOrder [ String ( b . id ) ] ? ? Infinity ;
return orderA - orderB ;
} ) ;
}
// ============================ 3. 商品核心价格计算(含折扣、会员优惠) ============================
/**
* 计算单个商品的实际单价(整合商家改价、会员优惠、营销活动折扣)
* @param goods 商品项
* @param config 订单额外配置(含会员、活动信息)
* @returns 单个商品实际单价(元)
*/
export function calcSingleGoodsRealPrice (
goods : BaseCartItem ,
config : Pick < OrderExtraConfig , 'isMember' | 'memberDiscountRate' > & {
activity? : ActivityConfig ; // 商品参与的营销活动(如限时折扣)
}
) : number {
const { isMember , memberDiscountRate , activity } = config ;
// 1. 优先级1: 商家改价( 改价后单价>0才生效)
if ( goods . discountSaleAmount && goods . discountSaleAmount > 0 ) {
return truncateToTwoDecimals ( goods . discountSaleAmount ) ;
}
// 2. 优先级2: 会员价( 含会员折扣率)
const memberPrice = calcMemberPrice ( goods , isMember , memberDiscountRate ) ;
// 3. 优先级3: 营销活动折扣( 如限时折扣)
if ( ! activity || ( activity . applicableProductIds && ! activity . applicableProductIds . includes ( String ( goods . id ) ) ) ) {
return memberPrice ;
}
// 处理活动与会员的同享/不同享逻辑
if ( activity . shareWithMember ) {
// 同享:会员价基础上叠加工活动折扣
return truncateToTwoDecimals ( memberPrice * activity . discountRate ) ;
} else {
// 不同享:取会员价和活动价的最小值(最大折扣)
const activityPrice = truncateToTwoDecimals ( goods . salePrice * activity . discountRate ) ;
return Math . min ( memberPrice , activityPrice ) ;
}
}
/**
* 计算商品原价总和(所有商品:原价*数量,含临时菜、赠菜)
* @param goodsList 商品列表
* @returns 商品原价总和(元)
*/
export function calcGoodsOriginalAmount ( goodsList : BaseCartItem [ ] ) : number {
return truncateToTwoDecimals (
goodsList . reduce ( ( total , goods ) = > {
const availableNum = Math . max ( 0 , goods . number - ( goods . returnNum || 0 ) ) ;
return total + goods . salePrice * availableNum ;
} , 0 )
) ;
}
/**
* 计算商品实际总价(所有商品:实际单价*数量,含临时菜、赠菜)
* @param goodsList 商品列表
* @param config 订单额外配置(含会员、活动信息)
* @param activities 全局营销活动列表(如限时折扣)
* @returns 商品实际总价(元)
*/
export function calcGoodsRealAmount (
goodsList : BaseCartItem [ ] ,
config : Pick < OrderExtraConfig , 'isMember' | 'memberDiscountRate' > ,
activities : ActivityConfig [ ] = [ ]
) : number {
return truncateToTwoDecimals (
goodsList . reduce ( ( total , goods ) = > {
const availableNum = Math . max ( 0 , goods . number - ( goods . returnNum || 0 ) ) ;
if ( availableNum <= 0 ) return total ;
// 匹配商品参与的营销活动(优先商品自身配置,无则匹配全局活动)
const activity = goods . activityInfo
? ? activities . find ( act = > ( act . applicableProductIds || [ ] ) . includes ( String ( goods . id ) ) ) ;
const realPrice = calcSingleGoodsRealPrice ( goods , { . . . config , activity } ) ;
return total + realPrice * availableNum ;
} , 0 )
) ;
}
/**
* 计算商品折扣总金额(商品原价总和 - 商品实际总价)
* @param goodsOriginalAmount 商品原价总和
* @param goodsRealAmount 商品实际总价
* @returns 商品折扣总金额( 元, ≥0)
*/
export function calcGoodsDiscountAmount (
goodsOriginalAmount : number ,
goodsRealAmount : number
) : number {
return truncateToTwoDecimals ( Math . max ( 0 , goodsOriginalAmount - goodsRealAmount ) ) ;
}
// ============================ 4. 优惠券抵扣计算(策略模式,易扩展) ============================
/** 优惠券计算策略接口(新增优惠券类型时,实现此接口即可) */
interface CouponCalculationStrategy {
/**
* 计算优惠券抵扣金额
* @param coupon 优惠券信息
* @param goodsList 商品列表
* @param config 订单配置(门店、会员、活动等)
* @returns 抵扣金额 + 额外信息( 如排除的商品ID)
*/
calculate (
coupon : Coupon ,
goodsList : BaseCartItem [ ] ,
config : Pick < OrderExtraConfig , 'currentStoreId' | 'isMember' | 'memberDiscountRate' > & {
activities : ActivityConfig [ ] ; // 营销活动列表
cartOrder : Record < string , number > ; // 商品加入购物车顺序
goodsList? : BaseCartItem [ ] ; // 可选的过滤后商品列表(仅兑换券策略用)
}
) : {
deductionAmount : number ; // 抵扣金额(元)
excludedProductIds : string [ ] ; // 需排除的商品ID( 如兑换券抵扣的商品)
} ;
}
/** 满减券计算策略 */
class FullReductionStrategy implements CouponCalculationStrategy {
calculate (
coupon : FullReductionCoupon ,
goodsList : BaseCartItem [ ] ,
config : any
) : { deductionAmount : number ; excludedProductIds : string [ ] } {
// 1. 验证基础条件(适用门店、可用状态)
if ( ! coupon . available || ! coupon . applicableStoreIds . includes ( config . currentStoreId ) ) {
return { deductionAmount : 0 , excludedProductIds : [ ] } ;
}
// 2. 计算优惠券门槛(排除临时菜、赠菜)
const eligibleGoods = filterCouponEligibleGoods ( goodsList ) ;
const eligibleAmount = calcGoodsRealAmount ( eligibleGoods , {
isMember : config.isMember ,
memberDiscountRate : config.memberDiscountRate
} , config . activities ) ;
// 3. 满足门槛则抵扣, 否则0
if ( eligibleAmount < coupon . fullAmount ) {
return { deductionAmount : 0 , excludedProductIds : [ ] } ;
}
// 4. 计算实际抵扣金额(不超过最大减免)
const maxReduction = coupon . maxReductionAmount ? ? coupon . reductionAmount ;
const deductionAmount = truncateToTwoDecimals ( Math . min ( coupon . reductionAmount , maxReduction ) ) ;
return { deductionAmount , excludedProductIds : [ ] } ;
}
}
/** 折扣券计算策略 */
class DiscountStrategy implements CouponCalculationStrategy {
calculate (
coupon : DiscountCoupon ,
goodsList : BaseCartItem [ ] ,
config : any
) : { deductionAmount : number ; excludedProductIds : string [ ] } {
// 1. 验证基础条件
if ( ! coupon . available || ! coupon . applicableStoreIds . includes ( config . currentStoreId ) ) {
return { deductionAmount : 0 , excludedProductIds : [ ] } ;
}
// 2. 计算可折扣金额(排除临时菜、赠菜)
const eligibleGoods = filterCouponEligibleGoods ( goodsList ) ;
const eligibleAmount = calcGoodsRealAmount ( eligibleGoods , {
isMember : config.isMember ,
memberDiscountRate : config.memberDiscountRate
} , config . activities ) ;
// 3. 计算折扣金额(不超过最大减免)
const discountAmount = truncateToTwoDecimals ( eligibleAmount * ( 1 - coupon . discountRate ) ) ;
const deductionAmount = Math . min ( discountAmount , coupon . maxReductionAmount ) ;
return { deductionAmount : truncateToTwoDecimals ( deductionAmount ) , excludedProductIds : [ ] } ;
}
}
/** 第二件半价券计算策略 */
class SecondHalfPriceStrategy implements CouponCalculationStrategy {
calculate (
coupon : SecondHalfPriceCoupon ,
goodsList : BaseCartItem [ ] ,
config : any
) : { deductionAmount : number ; excludedProductIds : string [ ] } {
// 1. 验证基础条件
if ( ! coupon . available || ! coupon . applicableStoreIds . includes ( config . currentStoreId ) ) {
return { deductionAmount : 0 , excludedProductIds : [ ] } ;
}
let totalDeduction = 0 ;
// 2. 遍历适用商品,计算每类商品的优惠
for ( const productId of coupon . applicableProductIds ) {
const goods = goodsList . find ( g = > String ( g . id ) === productId && ! isGiftGoods ( g ) && ! isTemporaryGoods ( g ) ) ;
if ( ! goods ) continue ;
const availableNum = Math . max ( 0 , goods . number - ( goods . returnNum || 0 ) ) ;
if ( availableNum < 2 ) continue ; // 至少2件才享受优惠
// 3. 计算单类商品优惠: 每2件减免0.5件的实际价( 超过2件部分按原价)
const realPrice = calcSingleGoodsRealPrice ( goods , {
isMember : config.isMember ,
memberDiscountRate : config.memberDiscountRate ,
activity : config.activities.find ( ( act : { applicableProductIds : string | string [ ] ; } ) = > act . applicableProductIds . includes ( productId ) )
} ) ;
const discountCount = Math . floor ( availableNum / 2 ) ; // 优惠次数( 每2件1次)
totalDeduction += realPrice * 0.5 * discountCount ;
}
return {
deductionAmount : truncateToTwoDecimals ( totalDeduction ) ,
excludedProductIds : [ ]
} ;
}
}
/** 买一送一券计算策略 */
class BuyOneGetOneStrategy implements CouponCalculationStrategy {
calculate (
coupon : BuyOneGetOneCoupon ,
goodsList : BaseCartItem [ ] ,
config : any
) : { deductionAmount : number ; excludedProductIds : string [ ] } {
// 1. 验证基础条件
if ( ! coupon . available || ! coupon . applicableStoreIds . includes ( config . currentStoreId ) ) {
return { deductionAmount : 0 , excludedProductIds : [ ] } ;
}
let totalDeduction = 0 ;
// 2. 遍历适用商品,计算每类商品的优惠
for ( const productId of coupon . applicableProductIds ) {
const goods = goodsList . find ( g = > String ( g . id ) === productId && ! isGiftGoods ( g ) && ! isTemporaryGoods ( g ) ) ;
if ( ! goods ) continue ;
const availableNum = Math . max ( 0 , goods . number - ( goods . returnNum || 0 ) ) ;
if ( availableNum < 2 ) continue ; // 至少2件才享受优惠
// 3. 计算单类商品优惠: 每2件减免1件的实际价
const realPrice = calcSingleGoodsRealPrice ( goods , {
isMember : config.isMember ,
memberDiscountRate : config.memberDiscountRate ,
activity : config.activities.find ( ( act : { applicableProductIds : string | string [ ] ; } ) = > act . applicableProductIds . includes ( productId ) )
} ) ;
const discountCount = Math . floor ( availableNum / 2 ) ; // 优惠次数
totalDeduction += realPrice * 1 * discountCount ;
}
return {
deductionAmount : truncateToTwoDecimals ( totalDeduction ) ,
excludedProductIds : [ ]
} ;
}
}
/** 商品兑换券计算策略 */
class ExchangeCouponStrategy implements CouponCalculationStrategy {
calculate (
coupon : ExchangeCoupon ,
goodsList : BaseCartItem [ ] ,
config : any
) : { deductionAmount : number ; excludedProductIds : string [ ] } {
// 1. 验证基础条件
if ( ! coupon . available || ! coupon . applicableStoreIds . includes ( config . currentStoreId ) ) {
return { deductionAmount : 0 , excludedProductIds : [ ] } ;
}
// 2. 筛选适用商品(排除临时菜、赠菜)
const eligibleGoods = goodsList . filter ( goods = >
coupon . applicableProductIds . includes ( String ( goods . id ) )
&& ! isTemporaryGoods ( goods )
&& ! isGiftGoods ( goods )
) ;
if ( eligibleGoods . length === 0 ) {
return { deductionAmount : 0 , excludedProductIds : [ ] } ;
}
// 3. 按规则排序商品
const sortedGoods = sortGoodsForCoupon ( eligibleGoods , coupon . sortRule , config . cartOrder ) ;
// 4. 计算抵扣金额(按可抵扣件数,累计商品实际价)
let remainingCount = coupon . deductCount ;
let totalDeduction = 0 ;
const excludedProductIds : string [ ] = [ ] ;
for ( const goods of sortedGoods ) {
if ( remainingCount <= 0 ) break ;
const availableNum = Math . max ( 0 , goods . number - ( goods . returnNum || 0 ) ) ;
if ( availableNum === 0 ) continue ;
// 计算当前商品可抵扣的件数
const deductCount = Math . min ( availableNum , remainingCount ) ;
const realPrice = calcSingleGoodsRealPrice ( goods , {
isMember : config.isMember ,
memberDiscountRate : config.memberDiscountRate ,
activity : config.activities.find ( ( act : { applicableProductIds : string | string [ ] ; } ) = > act . applicableProductIds . includes ( String ( goods . id ) ) )
} ) ;
// 累计抵扣金额和排除商品ID
totalDeduction += realPrice * deductCount ;
excludedProductIds . push ( String ( goods . id ) ) ;
remainingCount -= deductCount ;
}
return {
deductionAmount : truncateToTwoDecimals ( totalDeduction ) ,
excludedProductIds
} ;
}
}
/**
* 优惠券计算策略工厂(根据优惠券类型获取对应策略,易扩展)
* @param couponType 优惠券类型
* @returns 对应的计算策略实例
*/
function getCouponStrategy ( couponType : CouponType ) : CouponCalculationStrategy {
switch ( couponType ) {
case CouponType.FULL_REDUCTION :
return new FullReductionStrategy ( ) ;
case CouponType.DISCOUNT :
return new DiscountStrategy ( ) ;
case CouponType.SECOND_HALF :
return new SecondHalfPriceStrategy ( ) ;
case CouponType.BUY_ONE_GET_ONE :
return new BuyOneGetOneStrategy ( ) ;
case CouponType.EXCHANGE :
return new ExchangeCouponStrategy ( ) ;
default :
throw new Error ( ` 不支持的优惠券类型: ${ couponType } ` ) ;
}
}
/**
* 计算优惠券抵扣金额(处理互斥逻辑,选择最优优惠券)
* @param coupons 用户选择的优惠券列表(可能多选,需处理互斥)
* @param goodsList 商品列表
* @param config 订单配置
* @returns 最优优惠券的抵扣结果
*/
export function calcCouponDeduction (
coupons : Coupon [ ] ,
goodsList : BaseCartItem [ ] ,
config : Pick < OrderExtraConfig , 'currentStoreId' | 'isMember' | 'memberDiscountRate' > & {
activities : ActivityConfig [ ] ;
cartOrder : Record < string , number > ; // 商品加入购物车顺序(用于兑换券排序)
}
) : {
deductionAmount : number ;
usedCoupon? : Coupon ;
excludedProductIds : string [ ] ;
} {
if ( coupons . length === 0 ) {
return { deductionAmount : 0 , excludedProductIds : [ ] } ;
}
// 1. 处理优惠券互斥逻辑(满减/折扣/第二件半价/买一送一 互斥;兑换券可单独用或同享)
const exchangeCoupons = coupons . filter ( c = > c . type === CouponType . EXCHANGE ) ;
const nonExchangeCoupons = coupons . filter ( c = > c . type !== CouponType . EXCHANGE ) ;
// 2. 计算非兑换券的最优抵扣(互斥,选最大)
let nonExchangeResult :CouponResult = {
deductionAmount : 0 ,
excludedProductIds : [ ] ,
usedCoupon : undefined
} ;
if ( nonExchangeCoupons . length > 0 ) {
nonExchangeResult = nonExchangeCoupons . reduce ( ( best , coupon ) = > {
const strategy = getCouponStrategy ( coupon . type ) ;
// 确保 calculate 返回的 result 包含 deductionAmount 和 excludedProductIds
const result : Omit < CouponResult , 'usedCoupon' > = strategy . calculate ( coupon , goodsList , config ) ;
// 合并结果,补充 usedCoupon
const currentResult : CouponResult = {
. . . result ,
usedCoupon : coupon
} ;
// 比较抵扣金额,返回更优结果
return currentResult . deductionAmount > best . deductionAmount
? currentResult
: best ;
} , nonExchangeResult ) ; // 初始值类型现在与回调返回值一致
}
// 3. 计算兑换券抵扣(可与非兑换券同享,需排除兑换券已抵扣的商品)
let exchangeResult :ExchangeCalculationResult = { deductionAmount : 0 , excludedProductIds : [ ] } ;
if ( exchangeCoupons . length > 0 ) {
exchangeResult = exchangeCoupons . reduce ( ( best , coupon ) = > {
const strategy = getCouponStrategy ( coupon . type ) ;
const result = strategy . calculate ( coupon , goodsList , {
. . . config ,
// 排除已抵扣的商品
goodsList : goodsList.filter ( g = > ! best . excludedProductIds . includes ( String ( g . id ) ) )
} ) ;
return result . deductionAmount > best . deductionAmount ? result : best ;
} , exchangeResult ) ;
}
// 4. 汇总结果(兑换券可与非兑换券同享,总抵扣=两者之和)
return {
deductionAmount : truncateToTwoDecimals ( nonExchangeResult . deductionAmount + exchangeResult . deductionAmount ) ,
usedCoupon : nonExchangeResult.usedCoupon ,
excludedProductIds : [ . . . nonExchangeResult . excludedProductIds , . . . exchangeResult . excludedProductIds ]
} ;
}
// ============================ 5. 其他费用计算(打包费、餐位费、积分抵扣) ============================
/**
* 计算总打包费( 赠菜也计算, 称重商品打包数量≤1)
* @param goodsList 商品列表
* @param dinnerType 就餐类型( 堂食dine-in/外卖take-out)
* @returns 总打包费(元)
*/
export function calcTotalPackFee (
goodsList : BaseCartItem [ ] ,
dinnerType : 'dine-in' | 'take-out'
) : number {
return truncateToTwoDecimals (
goodsList . reduce ( ( total , goods ) = > {
const availableNum = Math . max ( 0 , goods . number - ( goods . returnNum || 0 ) ) ;
if ( availableNum === 0 ) return total ;
// 计算单个商品打包数量( 外卖全打包, 堂食按配置, 称重商品≤1)
let packNum = dinnerType === 'take-out'
? availableNum
: ( goods . packNumber || 0 ) ;
if ( goods . productType === GoodsType . WEIGHT ) {
packNum = Math . min ( packNum , 1 ) ;
}
return total + ( goods . packFee || 0 ) * packNum ;
} , 0 )
) ;
}
/**
* 计算餐位费(按人数,不参与营销活动)
* @param config 餐位费配置
* @returns 餐位费( 元, 未启用则0)
*/
export function calcSeatFee ( config : SeatFeeConfig ) : number {
if ( ! config . isEnabled ) return 0 ;
const personCount = Math . max ( 1 , config . personCount ) ; // 至少1人
return truncateToTwoDecimals ( config . pricePerPerson * personCount ) ;
}
/**
* 计算积分抵扣金额( 按X积分=1元, 不超过最大抵扣和用户积分)
* @param userPoints 用户当前积分
* @param rule 积分抵扣规则
* @param maxDeductionLimit 最大抵扣上限(通常为订单金额,避免超扣)
* @returns 积分抵扣金额 + 实际使用积分
*/
export function calcPointDeduction (
userPoints : number ,
rule : PointDeductionRule ,
maxDeductionLimit : number
) : {
deductionAmount : number ;
usedPoints : number ;
} {
if ( rule . pointsPerYuan <= 0 || userPoints <= 0 ) {
return { deductionAmount : 0 , usedPoints : 0 } ;
}
// 最大可抵扣金额(积分可抵金额 vs 规则最大 vs 订单上限)
const maxDeductByPoints = truncateToTwoDecimals ( userPoints / rule . pointsPerYuan ) ;
const maxDeductAmount = Math . min (
maxDeductByPoints ,
rule . maxDeductionAmount ? ? Infinity ,
maxDeductionLimit
) ;
// 实际使用积分 = 抵扣金额 * 积分兑换比例
const usedPoints = truncateToTwoDecimals ( maxDeductAmount * rule . pointsPerYuan ) ;
return {
deductionAmount : maxDeductAmount ,
usedPoints : Math.min ( usedPoints , userPoints ) // 避免积分超扣
} ;
}
// ============================ 6. 订单总费用汇总与实付金额计算(最终入口) ============================
/**
* 计算订单所有费用子项并汇总(核心入口函数)
* @param goodsList 购物车商品列表
* @param dinnerType 就餐类型
* @param coupons 用户选择的优惠券列表
* @param activities 全局营销活动列表
* @param config 订单额外配置(会员、积分、餐位费等)
* @param cartOrder 商品加入购物车顺序( key=商品ID, value=时间戳)
* @returns 订单费用汇总(含所有子项和实付金额)
*/
export function calculateOrderCostSummary (
goodsList : BaseCartItem [ ] ,
dinnerType : 'dine-in' | 'take-out' ,
coupons : Coupon [ ] = [ ] ,
activities : ActivityConfig [ ] = [ ] ,
config : OrderExtraConfig ,
cartOrder : Record < string , number > = { }
) : OrderCostSummary {
// 1. 基础费用:商品原价、实际价、折扣金额
const goodsOriginalAmount = calcGoodsOriginalAmount ( goodsList ) ;
const goodsRealAmount = calcGoodsRealAmount ( goodsList , {
isMember : config.isMember ,
memberDiscountRate : config.memberDiscountRate
} , activities ) ;
const goodsDiscountAmount = calcGoodsDiscountAmount ( goodsOriginalAmount , goodsRealAmount ) ;
// 2. 优惠券抵扣(处理互斥和同享)
const { deductionAmount : couponDeductionAmount , usedCoupon , excludedProductIds } = calcCouponDeduction (
coupons ,
goodsList ,
{
currentStoreId : config.currentStoreId ,
isMember : config.isMember ,
memberDiscountRate : config.memberDiscountRate ,
activities ,
cartOrder
}
) ;
// 3. 其他固定费用:打包费、餐位费
const packFee = calcTotalPackFee ( goodsList , dinnerType ) ;
const seatFee = calcSeatFee ( config . seatFeeConfig ) ;
// 4. 积分抵扣(最大抵扣上限=商品实际价-优惠券抵扣,避免负金额)
const maxPointDeductionLimit = Math . max ( 0 , goodsRealAmount - couponDeductionAmount ) ;
const { deductionAmount : pointDeductionAmount , usedPoints } = calcPointDeduction (
config . userPoints ,
config . pointDeductionRule ,
maxPointDeductionLimit
) ;
// 5. 商家减免和附加费
const merchantReductionAmount = Math . max ( 0 , config . merchantReduction ) ; // 减免不能为负
const additionalFee = Math . max ( 0 , config . additionalFee ) ; // 附加费不能为负
// 6. 计算最终实付金额(按用户公式:实付=原价-折扣-优惠券-积分+餐位费+打包费-商家减免+附加费)
const finalPayAmount = truncateToTwoDecimals (
goodsOriginalAmount
- goodsDiscountAmount
- couponDeductionAmount
- pointDeductionAmount
+ seatFee
+ packFee
- merchantReductionAmount
+ additionalFee
) ;
// 确保实付金额≥0
const finalPayAmountNonNegative = Math . max ( 0 , finalPayAmount ) ;
// 返回完整费用汇总
return {
goodsOriginalAmount ,
goodsDiscountAmount ,
couponDeductionAmount ,
pointDeductionAmount ,
seatFee ,
packFee ,
merchantReductionAmount ,
additionalFee ,
finalPayAmount : finalPayAmountNonNegative ,
couponUsed : usedCoupon ,
pointUsed : usedPoints
} ;
}
// ============================ 7. 对外暴露工具库(按模块组织,方便调用) ============================
export const OrderPriceCalculator = {
// 基础工具
truncateToTwoDecimals ,
isTemporaryGoods ,
isGiftGoods ,
// 商品价格计算
calcGoodsOriginalAmount ,
calcGoodsRealAmount ,
calcGoodsDiscountAmount ,
// 优惠券计算
calcCouponDeduction ,
// 其他费用计算
calcTotalPackFee ,
calcSeatFee ,
calcPointDeduction ,
// 核心入口
calculateOrderCostSummary ,
// 类型导出(方便外部使用类型定义)
Types : {
GoodsType ,
CouponType ,
ActivityType ,
}
} ;
export default OrderPriceCalculator ;