fix: 修改订单计算逻辑
This commit is contained in:
@@ -1,7 +1,15 @@
|
||||
import { BigNumber } from 'bignumber.js';
|
||||
|
||||
// 配置BigNumber精度
|
||||
BigNumber.set({
|
||||
DECIMAL_PLACES: 2,
|
||||
ROUNDING_MODE: BigNumber.ROUND_DOWN // 向下取整,符合业务需求
|
||||
});
|
||||
|
||||
/**
|
||||
* 购物车订单价格计算公共库
|
||||
* 功能:覆盖订单全链路费用计算(商品价格、优惠券、积分、餐位费等),支持策略扩展
|
||||
* 小数处理:统一舍去小数点后两位(如 10.129 → 10.12,15.998 → 15.99)
|
||||
* 小数处理:使用bignumber.js确保精度,统一舍去小数点后两位(如 10.129 → 10.12,15.998 → 15.99)
|
||||
* 扩展设计:优惠券/营销活动采用策略模式,新增类型仅需扩展策略,无需修改核心逻辑
|
||||
* 关键规则:
|
||||
* - 所有优惠券均支持指定门槛商品(后端foods字段:空字符串=全部商品,ID字符串=指定商品ID)
|
||||
@@ -22,17 +30,20 @@ export enum GoodsType {
|
||||
PACKAGE = 'package'// 打包商品(如套餐/预打包商品,按普通商品逻辑处理,可扩展特殊规则)
|
||||
}
|
||||
|
||||
/** 优惠券计算结果类型 */
|
||||
/** 优惠券计算结果类型(新增细分字段) */
|
||||
interface CouponResult {
|
||||
deductionAmount: number; // 抵扣金额
|
||||
excludedProductIds: string[]; // 不适用商品ID列表(注意:是商品ID,非购物车ID)
|
||||
usedCoupon: Coupon | undefined; // 实际使用的优惠券
|
||||
productCouponDeduction: number; // 新增:商品优惠券抵扣(兑换券等)
|
||||
fullCouponDeduction: number; // 新增:满减优惠券抵扣
|
||||
}
|
||||
|
||||
/** 兑换券计算结果类型 */
|
||||
/** 兑换券计算结果类型(新增细分字段) */
|
||||
interface ExchangeCalculationResult {
|
||||
deductionAmount: number;
|
||||
excludedProductIds: string[]; // 不适用商品ID列表(商品ID)
|
||||
productCouponDeduction: number; // 新增:兑换券属于商品券,同步记录
|
||||
}
|
||||
|
||||
/** 优惠券类型枚举 */
|
||||
@@ -193,10 +204,23 @@ export interface SeatFeeConfig {
|
||||
personCount: number; // 用餐人数(默认1)
|
||||
isEnabled: boolean; // 是否启用餐位费(默认false)
|
||||
}
|
||||
/** 商家减免类型枚举 */
|
||||
export enum MerchantReductionType {
|
||||
FIXED_AMOUNT = 'fixed_amount', // 固定金额减免(如直接减 10 元)
|
||||
DISCOUNT_RATE = 'discount_rate' // 比例折扣减免(如打 9 折,即减免 10%)
|
||||
}
|
||||
|
||||
/** 商家减免配置(新增,替代原单一金额字段) */
|
||||
export interface MerchantReductionConfig {
|
||||
type: MerchantReductionType; // 减免类型(二选一)
|
||||
fixedAmount?: number; // 固定减免金额(元,仅 FIXED_AMOUNT 生效,≥0)
|
||||
discountRate?: number; // 折扣率(%,仅 DISCOUNT_RATE 生效,0-100,如 90 代表 9 折)
|
||||
}
|
||||
/** 订单额外费用配置 */
|
||||
export interface OrderExtraConfig {
|
||||
merchantReduction: number; // 商家减免金额(元,默认0)
|
||||
// merchantReduction: number; // 商家减免金额(元,默认0)
|
||||
// 替换原单一金额字段,支持两种减免形式
|
||||
merchantReduction: MerchantReductionConfig;
|
||||
additionalFee: number; // 附加费(元,如余额充值、券包,默认0)
|
||||
pointDeductionRule: PointDeductionRule; // 积分抵扣规则
|
||||
seatFeeConfig: SeatFeeConfig; // 餐位费配置
|
||||
@@ -206,19 +230,27 @@ export interface OrderExtraConfig {
|
||||
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; // 实际使用的积分
|
||||
goodsRealAmount: number; // 商品真实原价总和
|
||||
goodsOriginalAmount: number; // 商品原价总和
|
||||
goodsDiscountAmount: number; // 商品折扣金额
|
||||
couponDeductionAmount: number; // 优惠券总抵扣
|
||||
productCouponDeduction: number; // 商品优惠券抵扣
|
||||
fullCouponDeduction: number; // 满减优惠券抵扣
|
||||
pointDeductionAmount: number; // 积分抵扣金额
|
||||
seatFee: number; // 餐位费
|
||||
packFee: number; // 打包费
|
||||
// 新增:商家减免明细
|
||||
merchantReduction: {
|
||||
type: MerchantReductionType; // 实际使用的减免类型
|
||||
originalConfig: MerchantReductionConfig; // 原始配置(便于前端展示)
|
||||
actualAmount: number; // 实际减免金额(计算后的值,≥0)
|
||||
};
|
||||
additionalFee: number; // 附加费
|
||||
finalPayAmount: number; // 最终实付金额
|
||||
couponUsed?: Coupon; // 实际使用的优惠券
|
||||
pointUsed: number; // 实际使用的积分
|
||||
}
|
||||
|
||||
// ============================ 2. 基础工具函数(核心修正:所有商品ID匹配用product_id) ============================
|
||||
@@ -265,9 +297,8 @@ export function convertBackendCouponToToolCoupon(
|
||||
vipPriceShare: backendCoupon.vipPriceShare === 1,
|
||||
useType: backendCoupon.useType ? backendCoupon.useType.split(',') : [],
|
||||
isValid: isCouponInValidPeriod(backendCoupon, currentTime),
|
||||
applicableProductIds: applicableProductIds
|
||||
applicableProductIds: applicableProductIds,
|
||||
};
|
||||
|
||||
// 5. 按券类型补充专属字段
|
||||
switch (couponType) {
|
||||
case CouponType.FULL_REDUCTION:
|
||||
@@ -336,9 +367,6 @@ function isCouponAvailable(
|
||||
// 1. 状态校验:必须启用(status=1)
|
||||
if (backendCoupon.status !== 1) return false;
|
||||
|
||||
// 2. 库存校验:剩余数量>0(-10086表示不限量)
|
||||
if (backendCoupon.leftNum !== -10086 && (backendCoupon.leftNum || 0) <= 0) return false;
|
||||
|
||||
// 3. 有效期校验:必须在有效期内
|
||||
if (!isCouponInValidPeriod(backendCoupon, currentTime)) return false;
|
||||
|
||||
@@ -377,6 +405,7 @@ function isCouponInValidPeriod(backendCoupon: BackendCoupon, currentTime: Date):
|
||||
if (validType === 'custom' && createTime && validDays) {
|
||||
const create = new Date(createTime);
|
||||
const end = new Date(create.getTime() + validDays * 24 * 60 * 60 * 1000); // 加N天
|
||||
|
||||
return currentTime <= end;
|
||||
}
|
||||
|
||||
@@ -501,12 +530,13 @@ export function formatDiscountRate(backendDiscountRate?: number): number {
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一小数处理:舍去小数点后两位(如 10.129 → 10.12,15.998 → 15.99)
|
||||
* 统一小数处理:舍去小数点后两位以后的数字(解决浮点数精度偏差)
|
||||
* 如 10.129 → 10.12,1.01 → 1.01,1.0100000001 → 1.01
|
||||
* @param num 待处理数字
|
||||
* @returns 处理后保留两位小数的数字
|
||||
*/
|
||||
export function truncateToTwoDecimals(num: number): number {
|
||||
return Math.floor(num * 100) / 100;
|
||||
export function truncateToTwoDecimals(num: number | string): number {
|
||||
return new BigNumber(num).decimalPlaces(2, BigNumber.ROUND_DOWN).toNumber();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -539,7 +569,7 @@ export function calcMemberPrice(
|
||||
isMember: boolean,
|
||||
memberDiscountRate?: number
|
||||
): number {
|
||||
if (!isMember) return goods.salePrice;
|
||||
if (!isMember) return truncateToTwoDecimals(goods.salePrice);
|
||||
|
||||
// 优先级:SKU会员价 > 商品会员价 > 商品原价(无会员价时用会员折扣)
|
||||
const basePrice = goods.skuData?.memberPrice
|
||||
@@ -547,9 +577,13 @@ export function calcMemberPrice(
|
||||
?? goods.salePrice;
|
||||
|
||||
// 仅当无SKU会员价、无商品会员价时,才应用会员折扣率
|
||||
return memberDiscountRate && !goods.skuData?.memberPrice && !goods.memberPrice
|
||||
? truncateToTwoDecimals(basePrice * memberDiscountRate)
|
||||
: basePrice;
|
||||
if (memberDiscountRate && !goods.skuData?.memberPrice && !goods.memberPrice) {
|
||||
return truncateToTwoDecimals(
|
||||
new BigNumber(basePrice).multipliedBy(memberDiscountRate).toNumber()
|
||||
);
|
||||
}
|
||||
|
||||
return truncateToTwoDecimals(basePrice);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -631,37 +665,39 @@ export function calcCouponThresholdAmount(
|
||||
config: Pick<OrderExtraConfig, 'isMember' | 'memberDiscountRate'>,
|
||||
activities: ActivityConfig[] = []
|
||||
): number {
|
||||
return truncateToTwoDecimals(
|
||||
eligibleGoods.reduce((total, goods) => {
|
||||
const availableNum = Math.max(0, goods.number - (goods.returnNum || 0));
|
||||
if (availableNum <= 0) return total;
|
||||
let total = new BigNumber(0);
|
||||
|
||||
// 1. 基础金额:默认用商品原价(SKU原价优先)
|
||||
const basePrice = goods.skuData?.salePrice ?? goods.salePrice;
|
||||
let itemAmount = basePrice * availableNum;
|
||||
for (const goods of eligibleGoods) {
|
||||
const availableNum = Math.max(0, goods.number - (goods.returnNum || 0));
|
||||
if (availableNum <= 0) continue;
|
||||
|
||||
// 2. 处理「与会员价/会员折扣同享」规则:若开启,用会员价计算
|
||||
if (coupon.vipPriceShare) {
|
||||
const memberPrice = calcMemberPrice(goods, config.isMember, config.memberDiscountRate);
|
||||
itemAmount = memberPrice * availableNum;
|
||||
// 1. 基础金额:默认用商品原价(SKU原价优先)
|
||||
const basePrice = new BigNumber(goods.skuData?.salePrice ?? goods.salePrice);
|
||||
let itemAmount = basePrice.multipliedBy(availableNum);
|
||||
|
||||
// 2. 处理「与会员价/会员折扣同享」规则:若开启,用会员价计算
|
||||
if (coupon.vipPriceShare) {
|
||||
const memberPrice = new BigNumber(calcMemberPrice(goods, config.isMember, config.memberDiscountRate));
|
||||
itemAmount = memberPrice.multipliedBy(availableNum);
|
||||
}
|
||||
|
||||
// 3. 处理「与限时折扣同享」规则:若开启,叠加限时折扣(按商品ID匹配活动)
|
||||
if (coupon.discountShare) {
|
||||
const activity = goods.activityInfo
|
||||
?? activities.find(act =>
|
||||
act.type === ActivityType.TIME_LIMIT_DISCOUNT
|
||||
&& (act.applicableProductIds || []).includes(String(goods.product_id)) // 核心修正:用商品ID匹配活动
|
||||
);
|
||||
|
||||
if (activity) {
|
||||
itemAmount = itemAmount.multipliedBy(activity.discountRate); // 叠加限时折扣
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 处理「与限时折扣同享」规则:若开启,叠加限时折扣(按商品ID匹配活动)
|
||||
if (coupon.discountShare) {
|
||||
const activity = goods.activityInfo
|
||||
?? activities.find(act =>
|
||||
act.type === ActivityType.TIME_LIMIT_DISCOUNT
|
||||
&& (act.applicableProductIds || []).includes(String(goods.product_id)) // 核心修正:用商品ID匹配活动
|
||||
);
|
||||
total = total.plus(itemAmount);
|
||||
}
|
||||
|
||||
if (activity) {
|
||||
itemAmount = itemAmount * activity.discountRate; // 叠加限时折扣
|
||||
}
|
||||
}
|
||||
|
||||
return total + itemAmount;
|
||||
}, 0)
|
||||
);
|
||||
return truncateToTwoDecimals(total.toNumber());
|
||||
}
|
||||
|
||||
// ============================ 3. 商品核心价格计算(核心修正:按商品ID匹配营销活动) ============================
|
||||
@@ -685,25 +721,30 @@ export function calcSingleGoodsRealPrice(
|
||||
}
|
||||
|
||||
// 2. 优先级2:会员价(含会员折扣率,SKU会员价优先)
|
||||
const memberPrice = calcMemberPrice(goods, isMember, memberDiscountRate);
|
||||
const memberPrice = new BigNumber(calcMemberPrice(goods, isMember, memberDiscountRate));
|
||||
|
||||
// 3. 优先级3:营销活动折扣(如限时折扣,需按商品ID匹配活动)
|
||||
const isActivityApplicable = activity
|
||||
? (activity.applicableProductIds || []).includes(String(goods.product_id)) // 核心修正:用商品ID匹配活动
|
||||
: false;
|
||||
if (!activity || !isActivityApplicable) {
|
||||
return memberPrice;
|
||||
return memberPrice.toNumber();
|
||||
}
|
||||
|
||||
// 处理活动与会员的同享/不同享逻辑
|
||||
if (activity.vipPriceShare) {
|
||||
// 同享:会员价基础上叠加工活动折扣
|
||||
return truncateToTwoDecimals(memberPrice * activity.discountRate);
|
||||
return truncateToTwoDecimals(
|
||||
memberPrice.multipliedBy(activity.discountRate).toNumber()
|
||||
);
|
||||
} else {
|
||||
// 不同享:取会员价和活动价的最小值(活动价用SKU原价计算)
|
||||
const basePriceForActivity = goods.skuData?.salePrice ?? goods.salePrice;
|
||||
const activityPrice = truncateToTwoDecimals(basePriceForActivity * activity.discountRate);
|
||||
return Math.min(memberPrice, activityPrice);
|
||||
const basePriceForActivity = new BigNumber(goods.skuData?.salePrice ?? goods.salePrice);
|
||||
const activityPrice = basePriceForActivity.multipliedBy(activity.discountRate);
|
||||
|
||||
return truncateToTwoDecimals(
|
||||
memberPrice.isLessThanOrEqualTo(activityPrice) ? memberPrice.toNumber() : activityPrice.toNumber()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -713,13 +754,15 @@ export function calcSingleGoodsRealPrice(
|
||||
* @returns 商品原价总和(元)
|
||||
*/
|
||||
export function calcGoodsOriginalAmount(goodsList: BaseCartItem[]): number {
|
||||
return truncateToTwoDecimals(
|
||||
goodsList.reduce((total, goods) => {
|
||||
const availableNum = Math.max(0, goods.number - (goods.returnNum || 0));
|
||||
const basePrice = goods.skuData?.salePrice ?? goods.salePrice; // SKU原价优先
|
||||
return total + basePrice * availableNum;
|
||||
}, 0)
|
||||
);
|
||||
let total = new BigNumber(0);
|
||||
|
||||
for (const goods of goodsList) {
|
||||
const availableNum = Math.max(0, goods.number - (goods.returnNum || 0));
|
||||
const basePrice = new BigNumber(goods.skuData?.salePrice ?? goods.salePrice); // SKU原价优先
|
||||
total = total.plus(basePrice.multipliedBy(availableNum));
|
||||
}
|
||||
|
||||
return truncateToTwoDecimals(total.toNumber());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -734,21 +777,23 @@ export function calcGoodsRealAmount(
|
||||
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;
|
||||
let total = new BigNumber(0);
|
||||
|
||||
// 匹配商品参与的营销活动(按商品ID匹配,优先商品自身配置)
|
||||
const activity = goods.activityInfo
|
||||
?? activities.find(act =>
|
||||
(act.applicableProductIds || []).includes(String(goods.product_id)) // 核心修正:用商品ID匹配活动
|
||||
);
|
||||
for (const goods of goodsList) {
|
||||
const availableNum = Math.max(0, goods.number - (goods.returnNum || 0));
|
||||
if (availableNum <= 0) continue;
|
||||
|
||||
const realPrice = calcSingleGoodsRealPrice(goods, { ...config, activity });
|
||||
return total + realPrice * availableNum;
|
||||
}, 0)
|
||||
);
|
||||
// 匹配商品参与的营销活动(按商品ID匹配,优先商品自身配置)
|
||||
const activity = goods.activityInfo
|
||||
?? activities.find(act =>
|
||||
(act.applicableProductIds || []).includes(String(goods.product_id)) // 核心修正:用商品ID匹配活动
|
||||
);
|
||||
|
||||
const realPrice = new BigNumber(calcSingleGoodsRealPrice(goods, { ...config, activity }));
|
||||
total = total.plus(realPrice.multipliedBy(availableNum));
|
||||
}
|
||||
|
||||
return truncateToTwoDecimals(total.toNumber());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -761,7 +806,11 @@ export function calcGoodsDiscountAmount(
|
||||
goodsOriginalAmount: number,
|
||||
goodsRealAmount: number
|
||||
): number {
|
||||
return truncateToTwoDecimals(Math.max(0, goodsOriginalAmount - goodsRealAmount));
|
||||
const original = new BigNumber(goodsOriginalAmount);
|
||||
const real = new BigNumber(goodsRealAmount);
|
||||
const discount = original.minus(real);
|
||||
|
||||
return truncateToTwoDecimals(Math.max(0, discount.toNumber()));
|
||||
}
|
||||
|
||||
// ============================ 4. 优惠券抵扣计算(策略模式,核心修正:按商品ID匹配) ============================
|
||||
@@ -778,6 +827,8 @@ interface CouponCalculationStrategy {
|
||||
): {
|
||||
deductionAmount: number;
|
||||
excludedProductIds: string[]; // 排除的商品ID列表(商品ID)
|
||||
productCouponDeduction?: number; // 新增:当前券属于商品券时的抵扣金额
|
||||
fullCouponDeduction?: number; // 新增:当前券属于满减券时的抵扣金额
|
||||
};
|
||||
}
|
||||
|
||||
@@ -787,35 +838,48 @@ class FullReductionStrategy implements CouponCalculationStrategy {
|
||||
coupon: FullReductionCoupon,
|
||||
goodsList: BaseCartItem[],
|
||||
config: any
|
||||
): { deductionAmount: number; excludedProductIds: string[] } {
|
||||
): { deductionAmount: number; excludedProductIds: string[]; fullCouponDeduction: number } {
|
||||
|
||||
// 1. 基础校验:优惠券不可用/门店不匹配 → 抵扣0
|
||||
if (!coupon.available || !isStoreMatchByList(coupon.useShops, config.currentStoreId)) {
|
||||
return { deductionAmount: 0, excludedProductIds: [] };
|
||||
return { deductionAmount: 0, excludedProductIds: [], fullCouponDeduction: 0 };
|
||||
}
|
||||
|
||||
// 2. 第一步:排除临时菜/赠菜/已抵扣商品(基础合格商品,按商品ID排除)
|
||||
const baseEligibleGoods = filterCouponEligibleGoods(goodsList, config.excludedProductIds || []);
|
||||
// 3. 第二步:按商品ID筛选门槛商品(匹配applicableProductIds)
|
||||
const thresholdGoods = filterThresholdGoods(baseEligibleGoods, coupon.applicableProductIds);
|
||||
|
||||
if (thresholdGoods.length === 0) {
|
||||
return { deductionAmount: 0, excludedProductIds: [] };
|
||||
return { deductionAmount: 0, excludedProductIds: [], fullCouponDeduction: 0 };
|
||||
}
|
||||
|
||||
// 4. 按同享规则计算门槛金额(按商品ID匹配活动)
|
||||
const thresholdAmount = calcCouponThresholdAmount(
|
||||
const thresholdAmount = new BigNumber(calcCouponThresholdAmount(
|
||||
thresholdGoods,
|
||||
coupon,
|
||||
{ isMember: config.isMember, memberDiscountRate: config.memberDiscountRate },
|
||||
config.activities
|
||||
);
|
||||
));
|
||||
|
||||
// 5. 满门槛则抵扣,否则0(不超过最大减免)
|
||||
if (thresholdAmount < coupon.fullAmount) {
|
||||
return { deductionAmount: 0, excludedProductIds: [] };
|
||||
if (thresholdAmount.isLessThan(coupon.fullAmount)) {
|
||||
return { deductionAmount: 0, excludedProductIds: [], fullCouponDeduction: 0 };
|
||||
}
|
||||
|
||||
const maxReduction = coupon.maxDiscountAmount ?? coupon.discountAmount;
|
||||
const deductionAmount = truncateToTwoDecimals(Math.min(coupon.discountAmount, maxReduction));
|
||||
return { deductionAmount, excludedProductIds: [] };
|
||||
const deductionAmount = truncateToTwoDecimals(
|
||||
new BigNumber(coupon.discountAmount).isLessThan(maxReduction)
|
||||
? coupon.discountAmount
|
||||
: maxReduction
|
||||
);
|
||||
|
||||
// 满减券计入满减优惠券抵扣
|
||||
return {
|
||||
deductionAmount,
|
||||
excludedProductIds: [],
|
||||
fullCouponDeduction: deductionAmount
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -825,10 +889,10 @@ class DiscountStrategy implements CouponCalculationStrategy {
|
||||
coupon: DiscountCoupon,
|
||||
goodsList: BaseCartItem[],
|
||||
config: any
|
||||
): { deductionAmount: number; excludedProductIds: string[] } {
|
||||
): { deductionAmount: number; excludedProductIds: string[]; productCouponDeduction: number } {
|
||||
// 1. 基础校验:优惠券不可用/门店不匹配 → 抵扣0
|
||||
if (!coupon.available || !isStoreMatchByList(coupon.useShops, config.currentStoreId)) {
|
||||
return { deductionAmount: 0, excludedProductIds: [] };
|
||||
return { deductionAmount: 0, excludedProductIds: [], productCouponDeduction: 0 };
|
||||
}
|
||||
|
||||
// 2. 第一步:排除临时菜/赠菜/已抵扣商品(基础合格商品,按商品ID排除)
|
||||
@@ -836,21 +900,31 @@ class DiscountStrategy implements CouponCalculationStrategy {
|
||||
// 3. 第二步:按商品ID筛选门槛商品(匹配applicableProductIds)
|
||||
const thresholdGoods = filterThresholdGoods(baseEligibleGoods, coupon.applicableProductIds);
|
||||
if (thresholdGoods.length === 0) {
|
||||
return { deductionAmount: 0, excludedProductIds: [] };
|
||||
return { deductionAmount: 0, excludedProductIds: [], productCouponDeduction: 0 };
|
||||
}
|
||||
|
||||
// 4. 按同享规则计算折扣基数(按商品ID匹配活动)
|
||||
const discountBaseAmount = calcCouponThresholdAmount(
|
||||
const discountBaseAmount = new BigNumber(calcCouponThresholdAmount(
|
||||
thresholdGoods,
|
||||
coupon,
|
||||
{ isMember: config.isMember, memberDiscountRate: config.memberDiscountRate },
|
||||
config.activities
|
||||
);
|
||||
));
|
||||
|
||||
// 5. 计算折扣金额(不超过最大减免)
|
||||
const discountAmount = truncateToTwoDecimals(discountBaseAmount * (1 - coupon.discountRate));
|
||||
const deductionAmount = truncateToTwoDecimals(Math.min(discountAmount, coupon.maxDiscountAmount));
|
||||
return { deductionAmount, excludedProductIds: [] };
|
||||
const discountAmount = discountBaseAmount.multipliedBy(new BigNumber(1).minus(coupon.discountRate));
|
||||
const deductionAmount = truncateToTwoDecimals(
|
||||
discountAmount.isLessThan(coupon.maxDiscountAmount)
|
||||
? discountAmount.toNumber()
|
||||
: coupon.maxDiscountAmount
|
||||
);
|
||||
|
||||
// 折扣券计入商品优惠券抵扣(可根据业务调整归类)
|
||||
return {
|
||||
deductionAmount,
|
||||
excludedProductIds: [],
|
||||
productCouponDeduction: deductionAmount
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -860,13 +934,13 @@ class SecondHalfPriceStrategy implements CouponCalculationStrategy {
|
||||
coupon: SecondHalfPriceCoupon,
|
||||
goodsList: BaseCartItem[],
|
||||
config: any
|
||||
): { deductionAmount: number; excludedProductIds: string[] } {
|
||||
): { deductionAmount: number; excludedProductIds: string[]; productCouponDeduction: number } {
|
||||
// 1. 基础校验:优惠券不可用/门店不匹配 → 抵扣0
|
||||
if (!coupon.available || !isStoreMatchByList(coupon.useShops, config.currentStoreId)) {
|
||||
return { deductionAmount: 0, excludedProductIds: [] };
|
||||
return { deductionAmount: 0, excludedProductIds: [], productCouponDeduction: 0 };
|
||||
}
|
||||
|
||||
let totalDeduction = 0;
|
||||
let totalDeduction = new BigNumber(0);
|
||||
const excludedProductIds: string[] = []; // 存储排除的商品ID(商品ID)
|
||||
|
||||
// 2. 第一步:排除临时菜/赠菜/已抵扣商品(基础合格商品,按商品ID排除)
|
||||
@@ -874,7 +948,7 @@ class SecondHalfPriceStrategy implements CouponCalculationStrategy {
|
||||
// 3. 第二步:按商品ID筛选门槛商品(匹配applicableProductIds)
|
||||
const thresholdGoods = filterThresholdGoods(baseEligibleGoods, coupon.applicableProductIds);
|
||||
if (thresholdGoods.length === 0) {
|
||||
return { deductionAmount: 0, excludedProductIds: [] };
|
||||
return { deductionAmount: 0, excludedProductIds: [], productCouponDeduction: 0 };
|
||||
}
|
||||
|
||||
// 按商品ID分组(避免同商品多次处理,用商品ID作为分组key)
|
||||
@@ -905,20 +979,23 @@ class SecondHalfPriceStrategy implements CouponCalculationStrategy {
|
||||
const activity = config.activities.find((act: ActivityConfig) =>
|
||||
(act.applicableProductIds || []).includes(productIdStr) // 按商品ID匹配活动
|
||||
);
|
||||
const realPrice = calcSingleGoodsRealPrice(sampleGood, {
|
||||
const realPrice = new BigNumber(calcSingleGoodsRealPrice(sampleGood, {
|
||||
isMember: config.isMember,
|
||||
memberDiscountRate: config.memberDiscountRate,
|
||||
activity
|
||||
});
|
||||
}));
|
||||
|
||||
// 累计抵扣金额并标记已优惠商品(记录商品ID)
|
||||
totalDeduction += realPrice * 0.5 * discountCount;
|
||||
totalDeduction = totalDeduction.plus(realPrice.multipliedBy(0.5).multipliedBy(discountCount));
|
||||
excludedProductIds.push(productIdStr);
|
||||
}
|
||||
|
||||
const deductionAmount = truncateToTwoDecimals(totalDeduction.toNumber());
|
||||
// 第二件半价券计入商品优惠券抵扣
|
||||
return {
|
||||
deductionAmount: truncateToTwoDecimals(totalDeduction),
|
||||
excludedProductIds
|
||||
deductionAmount,
|
||||
excludedProductIds,
|
||||
productCouponDeduction: deductionAmount
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -929,13 +1006,13 @@ class BuyOneGetOneStrategy implements CouponCalculationStrategy {
|
||||
coupon: BuyOneGetOneCoupon,
|
||||
goodsList: BaseCartItem[],
|
||||
config: any
|
||||
): { deductionAmount: number; excludedProductIds: string[] } {
|
||||
): { deductionAmount: number; excludedProductIds: string[]; productCouponDeduction: number } {
|
||||
// 1. 基础校验:优惠券不可用/门店不匹配 → 抵扣0
|
||||
if (!coupon.available || !isStoreMatchByList(coupon.useShops, config.currentStoreId)) {
|
||||
return { deductionAmount: 0, excludedProductIds: [] };
|
||||
return { deductionAmount: 0, excludedProductIds: [], productCouponDeduction: 0 };
|
||||
}
|
||||
|
||||
let totalDeduction = 0;
|
||||
let totalDeduction = new BigNumber(0);
|
||||
const excludedProductIds: string[] = []; // 存储排除的商品ID(商品ID)
|
||||
|
||||
// 2. 第一步:排除临时菜/赠菜/已抵扣商品(基础合格商品,按商品ID排除)
|
||||
@@ -943,7 +1020,7 @@ class BuyOneGetOneStrategy implements CouponCalculationStrategy {
|
||||
// 3. 第二步:按商品ID筛选门槛商品(匹配applicableProductIds)
|
||||
const thresholdGoods = filterThresholdGoods(baseEligibleGoods, coupon.applicableProductIds);
|
||||
if (thresholdGoods.length === 0) {
|
||||
return { deductionAmount: 0, excludedProductIds: [] };
|
||||
return { deductionAmount: 0, excludedProductIds: [], productCouponDeduction: 0 };
|
||||
}
|
||||
|
||||
// 按商品ID分组(用商品ID作为分组key)
|
||||
@@ -971,20 +1048,23 @@ class BuyOneGetOneStrategy implements CouponCalculationStrategy {
|
||||
const activity = config.activities.find((act: ActivityConfig) =>
|
||||
(act.applicableProductIds || []).includes(productIdStr) // 按商品ID匹配活动
|
||||
);
|
||||
const realPrice = calcSingleGoodsRealPrice(sampleGood, {
|
||||
const realPrice = new BigNumber(calcSingleGoodsRealPrice(sampleGood, {
|
||||
isMember: config.isMember,
|
||||
memberDiscountRate: config.memberDiscountRate,
|
||||
activity
|
||||
});
|
||||
}));
|
||||
|
||||
// 累计抵扣金额(送1件=减免1件价格)并标记商品ID
|
||||
totalDeduction += realPrice * 1 * discountCount;
|
||||
totalDeduction = totalDeduction.plus(realPrice.multipliedBy(1).multipliedBy(discountCount));
|
||||
excludedProductIds.push(productIdStr);
|
||||
}
|
||||
|
||||
const deductionAmount = truncateToTwoDecimals(totalDeduction.toNumber());
|
||||
// 买一送一券计入商品优惠券抵扣
|
||||
return {
|
||||
deductionAmount: truncateToTwoDecimals(totalDeduction),
|
||||
excludedProductIds
|
||||
deductionAmount,
|
||||
excludedProductIds,
|
||||
productCouponDeduction: deductionAmount
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -995,10 +1075,10 @@ class ExchangeCouponStrategy implements CouponCalculationStrategy {
|
||||
coupon: ExchangeCoupon,
|
||||
goodsList: BaseCartItem[],
|
||||
config: any
|
||||
): { deductionAmount: number; excludedProductIds: string[] } {
|
||||
): { deductionAmount: number; excludedProductIds: string[]; productCouponDeduction: number } {
|
||||
// 1. 基础校验:优惠券不可用/门店不匹配 → 抵扣0
|
||||
if (!coupon.available || !isStoreMatchByList(coupon.useShops, config.currentStoreId)) {
|
||||
return { deductionAmount: 0, excludedProductIds: [] };
|
||||
return { deductionAmount: 0, excludedProductIds: [], productCouponDeduction: 0 };
|
||||
}
|
||||
|
||||
// 2. 第一步:排除临时菜/赠菜/已抵扣商品(基础合格商品,按商品ID排除)
|
||||
@@ -1006,13 +1086,13 @@ class ExchangeCouponStrategy implements CouponCalculationStrategy {
|
||||
// 3. 第二步:按商品ID筛选门槛商品(匹配applicableProductIds)
|
||||
const thresholdGoods = filterThresholdGoods(baseEligibleGoods, coupon.applicableProductIds);
|
||||
if (thresholdGoods.length === 0) {
|
||||
return { deductionAmount: 0, excludedProductIds: [] };
|
||||
return { deductionAmount: 0, excludedProductIds: [], productCouponDeduction: 0 };
|
||||
}
|
||||
|
||||
// 按规则排序商品(按价格/数量/加入顺序)
|
||||
const sortedGoods = sortGoodsForCoupon(thresholdGoods, coupon.sortRule, config.cartOrder);
|
||||
let remainingCount = coupon.deductCount;
|
||||
let totalDeduction = 0;
|
||||
let totalDeduction = new BigNumber(0);
|
||||
const excludedProductIds: string[] = []; // 存储排除的商品ID(商品ID)
|
||||
|
||||
// 计算兑换抵扣金额(按商品ID累计,避免重复抵扣)
|
||||
@@ -1030,22 +1110,25 @@ class ExchangeCouponStrategy implements CouponCalculationStrategy {
|
||||
const activity = config.activities.find((act: ActivityConfig) =>
|
||||
(act.applicableProductIds || []).includes(productIdStr) // 按商品ID匹配活动
|
||||
);
|
||||
const realPrice = calcSingleGoodsRealPrice(goods, {
|
||||
const realPrice = new BigNumber(calcSingleGoodsRealPrice(goods, {
|
||||
isMember: config.isMember,
|
||||
memberDiscountRate: config.memberDiscountRate,
|
||||
activity
|
||||
});
|
||||
}));
|
||||
|
||||
// 累计抵扣金额并标记商品
|
||||
totalDeduction += realPrice * deductCount;
|
||||
totalDeduction = totalDeduction.plus(realPrice.multipliedBy(deductCount));
|
||||
excludedProductIds.push(productIdStr);
|
||||
usedProductIds.add(productIdStr);
|
||||
remainingCount -= deductCount;
|
||||
}
|
||||
|
||||
const deductionAmount = truncateToTwoDecimals(totalDeduction.toNumber());
|
||||
// 商品兑换券计入商品优惠券抵扣
|
||||
return {
|
||||
deductionAmount: truncateToTwoDecimals(totalDeduction),
|
||||
excludedProductIds
|
||||
deductionAmount,
|
||||
excludedProductIds,
|
||||
productCouponDeduction: deductionAmount
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1082,11 +1165,11 @@ function getCouponStrategy(couponType: CouponType): CouponCalculationStrategy {
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算优惠券抵扣金额(处理互斥逻辑,选择最优优惠券,按商品ID排除)
|
||||
* 计算优惠券抵扣金额(处理互斥逻辑,选择最优优惠券,按商品ID排除,新增细分统计)
|
||||
* @param backendCoupons 后端优惠券列表
|
||||
* @param goodsList 商品列表
|
||||
* @param config 订单配置(含就餐类型)
|
||||
* @returns 最优优惠券的抵扣结果
|
||||
* @returns 最优优惠券的抵扣结果(含商品券/满减券细分)
|
||||
*/
|
||||
export function calcCouponDeduction(
|
||||
backendCoupons: BackendCoupon[],
|
||||
@@ -1099,6 +1182,8 @@ export function calcCouponDeduction(
|
||||
}
|
||||
): {
|
||||
deductionAmount: number;
|
||||
productCouponDeduction: number; // 新增:商品优惠券抵扣(兑换券/折扣券/买一送一等)
|
||||
fullCouponDeduction: number; // 新增:满减优惠券抵扣
|
||||
usedCoupon?: Coupon;
|
||||
excludedProductIds: string[]; // 排除的商品ID列表(商品ID)
|
||||
} {
|
||||
@@ -1111,20 +1196,26 @@ export function calcCouponDeduction(
|
||||
config.currentTime
|
||||
))
|
||||
.filter(Boolean) as Coupon[];
|
||||
|
||||
if (toolCoupons.length === 0) {
|
||||
return { deductionAmount: 0, excludedProductIds: [] };
|
||||
return {
|
||||
deductionAmount: 0,
|
||||
productCouponDeduction: 0,
|
||||
fullCouponDeduction: 0,
|
||||
excludedProductIds: []
|
||||
};
|
||||
}
|
||||
|
||||
// 2. 优惠券互斥逻辑:兑换券与其他券互斥,优先选最优
|
||||
const exchangeCoupons = toolCoupons.filter(c => c.type === CouponType.EXCHANGE);
|
||||
const nonExchangeCoupons = toolCoupons.filter(c => c.type !== CouponType.EXCHANGE);
|
||||
|
||||
// 3. 计算非兑换券最优抵扣(传递已抵扣商品ID,避免重复)
|
||||
// 3. 计算非兑换券最优抵扣(传递已抵扣商品ID,避免重复,统计细分字段)
|
||||
let nonExchangeResult: CouponResult = {
|
||||
deductionAmount: 0,
|
||||
excludedProductIds: [],
|
||||
usedCoupon: undefined
|
||||
usedCoupon: undefined,
|
||||
productCouponDeduction: 0,
|
||||
fullCouponDeduction: 0
|
||||
};
|
||||
if (nonExchangeCoupons.length > 0) {
|
||||
nonExchangeResult = nonExchangeCoupons.reduce((best, coupon) => {
|
||||
@@ -1134,15 +1225,26 @@ export function calcCouponDeduction(
|
||||
excludedProductIds: best.excludedProductIds // 传递已排除的商品ID(商品ID)
|
||||
});
|
||||
const currentResult: CouponResult = {
|
||||
...result,
|
||||
usedCoupon: coupon
|
||||
deductionAmount: result.deductionAmount,
|
||||
excludedProductIds: result.excludedProductIds,
|
||||
usedCoupon: coupon,
|
||||
// 按策略返回的字段赋值细分抵扣
|
||||
productCouponDeduction: result.productCouponDeduction || 0,
|
||||
fullCouponDeduction: result.fullCouponDeduction || 0
|
||||
};
|
||||
return currentResult.deductionAmount > best.deductionAmount ? currentResult : best;
|
||||
// 按总抵扣金额选择最优
|
||||
return new BigNumber(currentResult.deductionAmount).isGreaterThan(best.deductionAmount)
|
||||
? currentResult
|
||||
: best;
|
||||
}, nonExchangeResult);
|
||||
}
|
||||
|
||||
// 4. 计算兑换券抵扣(排除非兑换券已抵扣的商品ID)
|
||||
let exchangeResult: ExchangeCalculationResult = { deductionAmount: 0, excludedProductIds: [] };
|
||||
// 4. 计算兑换券抵扣(排除非兑换券已抵扣的商品ID,统计商品券细分)
|
||||
let exchangeResult: ExchangeCalculationResult = {
|
||||
deductionAmount: 0,
|
||||
excludedProductIds: [],
|
||||
productCouponDeduction: 0
|
||||
};
|
||||
if (exchangeCoupons.length > 0) {
|
||||
exchangeResult = exchangeCoupons.reduce((best, coupon) => {
|
||||
const strategy = getCouponStrategy(coupon.type);
|
||||
@@ -1150,14 +1252,25 @@ export function calcCouponDeduction(
|
||||
...config,
|
||||
excludedProductIds: [...nonExchangeResult.excludedProductIds, ...best.excludedProductIds] // 合并排除的商品ID
|
||||
});
|
||||
return result.deductionAmount > best.deductionAmount ? result : best;
|
||||
return new BigNumber(result.deductionAmount).isGreaterThan(best.deductionAmount)
|
||||
? {
|
||||
deductionAmount: result.deductionAmount,
|
||||
excludedProductIds: result.excludedProductIds,
|
||||
productCouponDeduction: result.productCouponDeduction || 0 // 兑换券属于商品券
|
||||
}
|
||||
: best;
|
||||
}, exchangeResult);
|
||||
}
|
||||
|
||||
// 5. 汇总结果:兑换券与非兑换券不可同时使用,取抵扣金额大的
|
||||
const isExchangeBetter = exchangeResult.deductionAmount > nonExchangeResult.deductionAmount;
|
||||
const exchangeBn = new BigNumber(exchangeResult.deductionAmount);
|
||||
const nonExchangeBn = new BigNumber(nonExchangeResult.deductionAmount);
|
||||
const isExchangeBetter = exchangeBn.isGreaterThan(nonExchangeBn);
|
||||
|
||||
return {
|
||||
deductionAmount: truncateToTwoDecimals(isExchangeBetter ? exchangeResult.deductionAmount : nonExchangeResult.deductionAmount),
|
||||
productCouponDeduction: isExchangeBetter ? exchangeResult.productCouponDeduction : nonExchangeResult.productCouponDeduction,
|
||||
fullCouponDeduction: isExchangeBetter ? 0 : nonExchangeResult.fullCouponDeduction, // 兑换券与满减券互斥,满减券抵扣置0
|
||||
usedCoupon: isExchangeBetter ? undefined : nonExchangeResult.usedCoupon,
|
||||
excludedProductIds: isExchangeBetter ? exchangeResult.excludedProductIds : nonExchangeResult.excludedProductIds
|
||||
};
|
||||
@@ -1174,22 +1287,24 @@ 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;
|
||||
let total = new BigNumber(0);
|
||||
|
||||
// 计算单个商品打包数量(外卖全打包,堂食按配置,称重商品≤1)
|
||||
let packNum = dinnerType === 'take-out'
|
||||
? availableNum
|
||||
: (goods.packNumber || 0);
|
||||
if (goods.product_type === GoodsType.WEIGHT) {
|
||||
packNum = Math.min(packNum, 1);
|
||||
}
|
||||
for (const goods of goodsList) {
|
||||
const availableNum = Math.max(0, goods.number - (goods.returnNum || 0));
|
||||
if (availableNum === 0) continue;
|
||||
|
||||
return total + (goods.packFee || 0) * packNum;
|
||||
}, 0)
|
||||
);
|
||||
// 计算单个商品打包数量(外卖全打包,堂食按配置,称重商品≤1)
|
||||
let packNum = dinnerType === 'take-out'
|
||||
? availableNum
|
||||
: (goods.packNumber || 0);
|
||||
if (goods.product_type === GoodsType.WEIGHT) {
|
||||
packNum = Math.min(packNum, 1);
|
||||
}
|
||||
|
||||
total = total.plus(new BigNumber(goods.packFee || 0).multipliedBy(packNum));
|
||||
}
|
||||
|
||||
return truncateToTwoDecimals(total.toNumber());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1198,9 +1313,11 @@ export function calcTotalPackFee(
|
||||
* @returns 餐位费(元,未启用则0)
|
||||
*/
|
||||
export function calcSeatFee(config: SeatFeeConfig): number {
|
||||
if (!config.isEnabled) return 0;
|
||||
if (!config.isEnabled || config.personCount == 0) return 0;
|
||||
const personCount = Math.max(1, config.personCount); // 至少1人
|
||||
return truncateToTwoDecimals(config.pricePerPerson * personCount);
|
||||
return truncateToTwoDecimals(
|
||||
new BigNumber(config.pricePerPerson).multipliedBy(personCount).toNumber()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1222,23 +1339,30 @@ export function calcPointDeduction(
|
||||
return { deductionAmount: 0, usedPoints: 0 };
|
||||
}
|
||||
|
||||
const userPointsBn = new BigNumber(userPoints);
|
||||
const pointsPerYuanBn = new BigNumber(rule.pointsPerYuan);
|
||||
const maxLimitBn = new BigNumber(maxDeductionLimit);
|
||||
|
||||
// 最大可抵扣金额(积分可抵金额 vs 规则最大 vs 订单上限)
|
||||
const maxDeductByPoints = truncateToTwoDecimals(userPoints / rule.pointsPerYuan);
|
||||
const maxDeductAmount = Math.min(
|
||||
maxDeductByPoints,
|
||||
rule.maxDeductionAmount ?? Infinity,
|
||||
maxDeductionLimit
|
||||
);
|
||||
const maxDeductByPoints = userPointsBn.dividedBy(pointsPerYuanBn);
|
||||
const maxDeductAmount = maxDeductByPoints
|
||||
.isLessThan(rule.maxDeductionAmount ?? Infinity)
|
||||
? maxDeductByPoints
|
||||
: new BigNumber(rule.maxDeductionAmount || Infinity)
|
||||
.isLessThan(maxLimitBn)
|
||||
? maxDeductByPoints
|
||||
: maxLimitBn;
|
||||
|
||||
// 实际使用积分 = 抵扣金额 * 积分兑换比例
|
||||
const usedPoints = truncateToTwoDecimals(maxDeductAmount * rule.pointsPerYuan);
|
||||
const usedPoints = maxDeductAmount.multipliedBy(pointsPerYuanBn);
|
||||
|
||||
return {
|
||||
deductionAmount: maxDeductAmount,
|
||||
usedPoints: Math.min(usedPoints, userPoints) // 避免积分超扣
|
||||
deductionAmount: truncateToTwoDecimals(maxDeductAmount.toNumber()),
|
||||
usedPoints: truncateToTwoDecimals(Math.min(usedPoints.toNumber(), userPoints)) // 避免积分超扣
|
||||
};
|
||||
}
|
||||
|
||||
// ============================ 6. 订单总费用汇总与实付金额计算(核心入口,逻辑不变) ============================
|
||||
// ============================ 6. 订单总费用汇总与实付金额计算(核心入口,补充细分字段) ============================
|
||||
/**
|
||||
* 计算订单所有费用子项并汇总(核心入口函数)
|
||||
* @param goodsList 购物车商品列表
|
||||
@@ -1248,7 +1372,7 @@ export function calcPointDeduction(
|
||||
* @param config 订单额外配置(会员、积分、餐位费等)
|
||||
* @param cartOrder 商品加入购物车顺序(key=购物车ID,value=时间戳)
|
||||
* @param currentTime 当前时间(用于优惠券有效期判断)
|
||||
* @returns 订单费用汇总(含所有子项和实付金额)
|
||||
* @returns 订单费用汇总(含所有子项和实付金额,新增商品券/满减券细分)
|
||||
*/
|
||||
export function calculateOrderCostSummary(
|
||||
goodsList: BaseCartItem[],
|
||||
@@ -1259,7 +1383,7 @@ export function calculateOrderCostSummary(
|
||||
cartOrder: Record<string, number> = {},
|
||||
currentTime: Date = new Date()
|
||||
): OrderCostSummary {
|
||||
// 1. 基础费用计算
|
||||
// 1. 基础费用计算(原有逻辑不变)
|
||||
const goodsOriginalAmount = calcGoodsOriginalAmount(goodsList);
|
||||
const goodsRealAmount = calcGoodsRealAmount(goodsList, {
|
||||
isMember: config.isMember,
|
||||
@@ -1267,8 +1391,14 @@ export function calculateOrderCostSummary(
|
||||
}, activities);
|
||||
const goodsDiscountAmount = calcGoodsDiscountAmount(goodsOriginalAmount, goodsRealAmount);
|
||||
|
||||
// 2. 优惠券抵扣(传递就餐类型用于可用性判断)
|
||||
const { deductionAmount: couponDeductionAmount, usedCoupon, excludedProductIds } = calcCouponDeduction(
|
||||
// 2. 优惠券抵扣(原有逻辑不变)
|
||||
const {
|
||||
deductionAmount: couponDeductionAmount,
|
||||
usedCoupon,
|
||||
excludedProductIds,
|
||||
productCouponDeduction,
|
||||
fullCouponDeduction
|
||||
} = calcCouponDeduction(
|
||||
backendCoupons,
|
||||
goodsList,
|
||||
{
|
||||
@@ -1282,47 +1412,103 @@ export function calculateOrderCostSummary(
|
||||
}
|
||||
);
|
||||
|
||||
// 3. 其他费用计算
|
||||
// 3. 其他费用计算(原有逻辑不变)
|
||||
const packFee = calcTotalPackFee(goodsList, dinnerType);
|
||||
const seatFee = calcSeatFee(config.seatFeeConfig);
|
||||
// 积分抵扣上限:商品实际价 - 优惠券抵扣(避免负抵扣)
|
||||
const additionalFee = Math.max(0, config.additionalFee);
|
||||
|
||||
// 4. 积分抵扣(原有逻辑不变,先于商家减免计算)
|
||||
const maxPointDeductionLimit = Math.max(0, goodsRealAmount - couponDeductionAmount);
|
||||
const { deductionAmount: pointDeductionAmount, usedPoints } = calcPointDeduction(
|
||||
const {
|
||||
deductionAmount: pointDeductionAmount,
|
||||
usedPoints
|
||||
} = calcPointDeduction(
|
||||
config.userPoints,
|
||||
config.pointDeductionRule,
|
||||
maxPointDeductionLimit
|
||||
);
|
||||
const merchantReductionAmount = Math.max(0, config.merchantReduction);
|
||||
const additionalFee = Math.max(0, config.additionalFee);
|
||||
|
||||
// 4. 计算最终实付金额(确保非负)
|
||||
const finalPayAmount = truncateToTwoDecimals(
|
||||
goodsOriginalAmount
|
||||
- goodsDiscountAmount
|
||||
- couponDeductionAmount
|
||||
- pointDeductionAmount
|
||||
+ seatFee
|
||||
+ packFee
|
||||
- merchantReductionAmount
|
||||
+ additionalFee
|
||||
);
|
||||
const finalPayAmountNonNegative = Math.max(0, finalPayAmount);
|
||||
// ============================ 新增:商家减免计算(支持两种形式) ============================
|
||||
// 商家减免计算时机:在商品折扣、优惠券、积分抵扣之后(避免过度减免)
|
||||
const merchantReductionConfig = config.merchantReduction;
|
||||
let merchantReductionActualAmount = 0;
|
||||
|
||||
// 计算商家减免的可抵扣上限:商品实际金额 - 优惠券 - 积分(避免减免后为负)
|
||||
const maxMerchantReductionLimit = new BigNumber(goodsRealAmount)
|
||||
.minus(couponDeductionAmount)
|
||||
.minus(pointDeductionAmount)
|
||||
.isGreaterThan(0)
|
||||
? new BigNumber(goodsRealAmount)
|
||||
.minus(couponDeductionAmount)
|
||||
.minus(pointDeductionAmount)
|
||||
: new BigNumber(0);
|
||||
|
||||
switch (merchantReductionConfig.type) {
|
||||
case MerchantReductionType.FIXED_AMOUNT:
|
||||
// 固定金额减免:取配置金额与上限的最小值,且不小于0
|
||||
const fixedAmount = new BigNumber(merchantReductionConfig.fixedAmount || 0);
|
||||
merchantReductionActualAmount = fixedAmount
|
||||
.isLessThanOrEqualTo(maxMerchantReductionLimit)
|
||||
? fixedAmount.toNumber()
|
||||
: maxMerchantReductionLimit.toNumber();
|
||||
merchantReductionActualAmount = Math.max(0, merchantReductionActualAmount);
|
||||
break;
|
||||
|
||||
case MerchantReductionType.DISCOUNT_RATE:
|
||||
// 比例折扣减免:先校验折扣率(0-100%),再按比例计算减免金额
|
||||
const validDiscountRate = Math.max(0, Math.min(100, merchantReductionConfig.discountRate || 0));
|
||||
// 折扣率转小数(如 90% → 0.9),减免金额 = 可抵扣上限 * (1 - 折扣率)
|
||||
merchantReductionActualAmount = maxMerchantReductionLimit
|
||||
.multipliedBy(new BigNumber(1).minus(validDiscountRate / 100))
|
||||
.toNumber();
|
||||
// 确保减免金额不超过上限且非负
|
||||
merchantReductionActualAmount = Math.min(
|
||||
merchantReductionActualAmount,
|
||||
maxMerchantReductionLimit.toNumber()
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
// 5. 最终实付金额计算(整合所有费用)
|
||||
const finalPayAmount = new BigNumber(goodsOriginalAmount) // 商品原价总和
|
||||
.minus(goodsDiscountAmount) // 减去商品折扣
|
||||
.minus(couponDeductionAmount) // 减去优惠券抵扣
|
||||
.minus(pointDeductionAmount) // 减去积分抵扣
|
||||
.minus(merchantReductionActualAmount) // 减去商家实际减免金额
|
||||
.plus(seatFee) // 加上餐位费(不参与减免)
|
||||
.plus(packFee) // 加上打包费(不参与减免)
|
||||
.plus(additionalFee); // 加上附加费
|
||||
|
||||
const finalPayAmountNonNegative = Math.max(0, finalPayAmount.toNumber());
|
||||
|
||||
// 6. 返回完整费用汇总(包含商家减免明细)
|
||||
return {
|
||||
goodsRealAmount,
|
||||
goodsOriginalAmount,
|
||||
goodsDiscountAmount,
|
||||
couponDeductionAmount,
|
||||
productCouponDeduction: truncateToTwoDecimals(productCouponDeduction || 0),
|
||||
fullCouponDeduction: truncateToTwoDecimals(fullCouponDeduction || 0),
|
||||
pointDeductionAmount,
|
||||
seatFee,
|
||||
packFee,
|
||||
merchantReductionAmount,
|
||||
// 商家减免明细(含类型、原始配置、实际金额)
|
||||
merchantReduction: {
|
||||
type: merchantReductionConfig.type,
|
||||
originalConfig: merchantReductionConfig,
|
||||
actualAmount: truncateToTwoDecimals(merchantReductionActualAmount)
|
||||
},
|
||||
additionalFee,
|
||||
finalPayAmount: finalPayAmountNonNegative,
|
||||
finalPayAmount: truncateToTwoDecimals(finalPayAmountNonNegative),
|
||||
couponUsed: usedCoupon,
|
||||
pointUsed: usedPoints
|
||||
};
|
||||
}
|
||||
|
||||
export function isWeightGoods(goods: BaseCartItem): boolean {
|
||||
return goods.product_type === GoodsType.WEIGHT;
|
||||
}
|
||||
|
||||
// ============================ 7. 对外暴露工具库 ============================
|
||||
export const OrderPriceCalculator = {
|
||||
// 基础工具
|
||||
@@ -1331,6 +1517,7 @@ export const OrderPriceCalculator = {
|
||||
isGiftGoods,
|
||||
formatDiscountRate,
|
||||
filterThresholdGoods,
|
||||
isWeightGoods,
|
||||
// 优惠券转换
|
||||
convertBackendCouponToToolCoupon,
|
||||
// 商品价格计算
|
||||
|
||||
@@ -161,6 +161,7 @@ const testBackendCoupons: BackendCoupon[] = [
|
||||
{
|
||||
id: 1,
|
||||
title: "满减券",
|
||||
status: 1,
|
||||
couponType: 1,
|
||||
fullAmount: 2,
|
||||
discountAmount: 1,
|
||||
@@ -172,6 +173,9 @@ const testBackendCoupons: BackendCoupon[] = [
|
||||
validEndTime: '2025-09-30 16:00:00',
|
||||
useDays: '周一,周二,周三,周四,周五',
|
||||
useTimeType: 'all',
|
||||
shopId: 101,
|
||||
createTime: '2025-09-16 13:30:50',
|
||||
validDays: 2
|
||||
},
|
||||
];
|
||||
const testOrderConfig = {
|
||||
@@ -192,7 +196,7 @@ const testOrderConfig = {
|
||||
additionalFee: 0,
|
||||
};
|
||||
const testActivities: ActivityConfig[] = [];
|
||||
const testCurrentTime = new Date("2024-06-01 12:00:00");
|
||||
const testCurrentTime = new Date();
|
||||
|
||||
// 调用函数(此时类型完全匹配)
|
||||
const result = OrderPriceCalculator.calculateOrderCostSummary(
|
||||
|
||||
Reference in New Issue
Block a user