cashier-ipad/lib/goods-1.0.47-back.ts

1276 lines
51 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { BigNumber } from "bignumber.js";
// 配置BigNumber精度
BigNumber.set({
DECIMAL_PLACES: 2,
ROUNDING_MODE: BigNumber.ROUND_DOWN, // 向下取整,符合业务需求
});
/**
* 购物车订单价格计算公共库
* 功能:覆盖订单全链路费用计算(商品价格、优惠券、积分、餐位费等),支持策略扩展
* 小数处理使用bignumber.js确保精度统一舍去小数点后两位如 10.129 → 10.1215.998 → 15.99
* 扩展设计:优惠券/营销活动采用策略模式,新增类型仅需扩展策略,无需修改核心逻辑
* 关键规则:
* - 所有优惠券均支持指定门槛商品后端foods字段空字符串=全部商品ID字符串=指定商品ID
* - 与限时折扣/会员价同享规则:开启则门槛计算含对应折扣,关闭则用原价/非会员价
* 字段说明:
* - BaseCartItem.id购物车项ID唯一标识购物车中的条目
* - BaseCartItem.product_id商品ID唯一标识商品用于优惠券/活动匹配)
* - BaseCartItem.skuData.idSKU ID唯一标识商品规格
*/
// ============================ 1. 基础类型定义核心修正明确ID含义 ============================
/** 商品类型枚举 */
export enum GoodsType {
NORMAL = "normal", // 普通商品
WEIGHT = "weight", // 称重商品
GIFT = "gift", // 赠菜(继承普通商品逻辑,标记用)
EMPTY = "", // 空字符串类型(后端未返回时默认归类为普通商品)
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; // 新增:兑换券属于商品券,同步记录
}
/** 优惠券类型枚举 */
export enum CouponType {
FULL_REDUCTION = "full_reduction", // 满减券
DISCOUNT = "discount", // 折扣券
SECOND_HALF = "second_half", // 第二件半价券
BUY_ONE_GET_ONE = "buy_one_get_one", // 买一送一券
EXCHANGE = "exchange", // 商品兑换券
}
/** 后端返回的优惠券原始字段类型 */
export interface BackendCoupon {
id?: number; // 自增主键int64
shopId?: number; // 店铺IDint64
syncId?: number; // 同步Idint64
type?: number; // 优惠券类型1-满减券2-商品兑换券3-折扣券4-第二件半价券5-消费送券6-买一送一券7-固定价格券8-免配送费券
name?: string; // 券名称
useShopType?: string; // 可用门店类型only-仅本店all-所有门店custom-指定门店
useShops?: string; // 可用门店(逗号分隔字符串,如"1,2,3"
useType?: string; // 可使用类型dine堂食/pickup自取/deliv配送/express快递
validType?: string; // 有效期类型fixed固定时间custom自定义时间
validDays?: number; // 有效期(天)
validStartTime?: string; // 有效期开始时间(如"2024-01-01 00:00:00"
validEndTime?: string; // 有效期结束时间
daysToTakeEffect?: number; // 隔天生效
useDays?: string; // 可用周期(如"周一,周二"
useTimeType?: string; // 可用时间段类型all-全时段custom-指定时段
useStartTime?: string; // 可用开始时间(每日)
useEndTime?: string; // 可用结束时间(每日)
getType?: string; // 发放设置:不可自行领取/no可领取/yes
getMode?: string; // 用户领取方式
giveNum?: number; // 总发放数量,-10086为不限量
getUserType?: string; // 可领取用户:全部/all新用户一次/new仅会员/vip
getLimit?: number; // 每人领取限量,-10086为不限量
useLimit?: number; // 每人每日使用限量,-10086为不限量
discountShare?: number; // 与限时折扣同享0-否1-是
vipPriceShare?: number; // 与会员价同享0-否1-是
ruleDetails?: string; // 附加规则说明
status?: number; // 状态0-禁用1-启用
useNum?: number; // 已使用数量
leftNum?: number; // 剩余数量
foods?: string; // 指定门槛商品(逗号分隔字符串,如"101,102"此处为商品ID
fullAmount?: number; // 使用门槛:满多少金额(元)
discountAmount?: number; // 使用门槛:减多少金额(元)
discountRate?: number; // 折扣%如90=9折
maxDiscountAmount?: number; // 可抵扣最大金额(元)
useRule?: string; // 使用规则price_asc-价格低到高price_desc-高到低
discountNum?: number; // 抵扣数量
otherCouponShare?: number; // 与其它优惠共享0-否1-是
createTime?: string; // 创建时间
updateTime?: string; // 更新时间
}
/** 营销活动类型枚举 */
export enum ActivityType {
TIME_LIMIT_DISCOUNT = "time_limit_discount", // 限时折扣
}
/** 基础购物车商品项核心修正新增product_id明确各ID含义 */
export interface BaseCartItem {
id: string | number; // 购物车ID唯一标识购物车中的条目如购物车项主键
product_id: string | number; // 商品ID唯一标识商品用于优惠券/活动匹配,必选)
salePrice: number; // 商品原价(元)
number: number; // 商品数量
product_type: GoodsType; // 商品类型
is_temporary?: boolean; // 是否临时菜默认false
is_gift?: boolean; // 是否赠菜默认false
returnNum?: number; // 退货数量历史订单用默认0
memberPrice?: number; // 商品会员价(元,优先级:商品会员价 > 会员折扣)
discountSaleAmount?: number; // 商家改价后单价(元,优先级最高)
packFee?: number; // 单份打包费默认0
packNumber?: number; // 堂食打包数量默认0
activityInfo?: {
// 商品参与的营销活动(如限时折扣)
type: ActivityType;
discountRate: number; // 折扣率如0.8=8折
vipPriceShare: boolean; // 是否与会员优惠同享默认false
};
skuData?: {
// SKU扩展数据可选
id: string | number; // SKU ID唯一标识商品规格如颜色/尺寸)
memberPrice?: number; // SKU会员价
salePrice?: number; // SKU原价
};
}
/** 基础优惠券接口(所有券类型继承,包含统一门槛商品字段) */
export interface BaseCoupon {
id: string | number; // 优惠券ID
type: CouponType; // 工具库字符串枚举由后端couponType转换
name: string; // 对应后端title
available: boolean; // 基于BackendCoupon字段计算的可用性
useShopType?: string; // only-仅本店all-所有门店custom-指定门店
useShops: string[]; // 可用门店ID列表
discountShare: boolean; // 与限时折扣同享0-否1-是(后端字段转换为布尔值)
vipPriceShare: boolean; // 与会员价同享0-否1-是(后端字段转换为布尔值)
useType?: string[]; // 可使用类型dine堂食/pickup自取/deliv配送/express快递
isValid: boolean; // 是否在有效期内
discountAmount?: number; // 减免金额 (满减券有)
fullAmount?: number; // 使用门槛:满多少金额
maxDiscountAmount?: number; // 可抵扣最大金额 元
applicableProductIds: string[]; // 门槛商品ID列表空数组=全部商品,非空=指定商品ID
}
/** 满减券(适配后端字段) */
export interface FullReductionCoupon extends BaseCoupon {
type: CouponType.FULL_REDUCTION;
fullAmount: number; // 对应后端fullAmount满减门槛
discountAmount: number; // 对应后端discountAmount减免金额
maxDiscountAmount?: number; // 对应后端maxDiscountAmount最大减免
}
/** 折扣券(适配后端字段) */
export interface DiscountCoupon extends BaseCoupon {
type: CouponType.DISCOUNT;
discountRate: number; // 后端discountRate%转小数如90→0.9
maxDiscountAmount: number; // 对应后端maxDiscountAmount最大减免
}
/** 第二件半价券(适配后端字段) */
export interface SecondHalfPriceCoupon extends BaseCoupon {
type: CouponType.SECOND_HALF;
maxUseCountPerOrder?: number; // 对应后端useLimit-10086=不限)
}
/** 买一送一券(适配后端字段) */
export interface BuyOneGetOneCoupon extends BaseCoupon {
type: CouponType.BUY_ONE_GET_ONE;
maxUseCountPerOrder?: number; // 对应后端useLimit-10086=不限)
}
/** 商品兑换券(适配后端字段) */
export interface ExchangeCoupon extends BaseCoupon {
type: CouponType.EXCHANGE;
deductCount: number; // 对应后端discountNum抵扣数量
sortRule: "low_price_first" | "high_price_first"; // 后端useRule转换
}
/** 所有优惠券类型联合 */
export type Coupon =
| FullReductionCoupon
| DiscountCoupon
| SecondHalfPriceCoupon
| BuyOneGetOneCoupon
| ExchangeCoupon;
/** 营销活动配置如限时折扣applicableProductIds为商品ID列表 */
export interface ActivityConfig {
type: ActivityType;
applicableProductIds?: string[]; // 适用商品ID列表与BaseCartItem.product_id匹配
discountRate: number; // 折扣率如0.8=8折
vipPriceShare: 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 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: MerchantReductionConfig;
additionalFee: number; // 附加费如余额充值、券包默认0
pointDeductionRule: PointDeductionRule; // 积分抵扣规则
seatFeeConfig: SeatFeeConfig; // 餐位费配置
currentStoreId: string; // 当前门店ID用于验证优惠券适用门店
userPoints: number; // 用户当前积分(用于积分抵扣)
isMember: boolean; // 用户是否会员(用于会员优惠)
memberDiscountRate?: number; // 会员折扣率如0.95=95折无会员价时用
newUserDiscount?: number; // 新用户减免金额默认0
}
/** 订单费用汇总(修改:补充商家减免类型和明细) */
export interface OrderCostSummary {
// 商品总件数
goodsTotal: number;
totalDiscountAmount: 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; // 实际使用的积分
newUserDiscount: number; // 新用户减免金额默认0
dinnerType?: "dine-in" | "take-out"; // 就餐类型(堂食/自取/配送/快递)
config: OrderExtraConfig; // 订单额外费用配置
//满减活动
fullReduction: {
usedActivity?: FullReductionActivity; // 实际使用的满减活动
usedThreshold?: FullReductionThreshold; // 实际使用的满减阈值(多门槛中选最优)
actualAmount: number; // 满减实际减免金额(元)
};
}
/** 满减活动阈值单条满减规则满X减Y- 对应 MkDiscountThresholdInsertGroupDefaultGroup */
export interface FullReductionThreshold {
activityId?: number; // 关联满减活动ID
fullAmount?: number; // 满多少金额(元,必填)
discountAmount?: number; // 减多少金额(元,必填)
}
/** 满减活动主表 - 对应 Request 接口(后端真实字段) */
export interface FullReductionActivity {
id?: number; // 自增主键后端字段id
shopId?: number; // 店铺ID后端字段shopId
status?: number; // 活动状态1=未开始2=进行中3=已结束后端字段status
sort?: number; // 排序值越大优先级越高后端字段sort
createTime?: string; // 创建时间后端字段createTime格式如"2025-10-14 13:56:07"
updateTime?: string; // 最新修改时间后端字段updateTime用于优先级排序
validStartTime?: string; // 有效期开始时间后端字段validStartTime格式如"2025-10-14"
validEndTime?: string; // 有效期结束时间后端字段validEndTime格式如"2025-12-14"
useType?: string; // 可使用类型后端字段useType如"dine,pickup,deliv,express"
useDays?: string; // 可用周期后端字段useDays如"周一,周二,周三,周四,周五,周六,周日"
useTimeType?: string; // 可用时间段类型后端字段useTimeTypeall=全时段custom=指定时段)
useStartTime?: string; // 每日可用开始时间后端字段useStartTime如"09:00:00"仅custom时有效
useEndTime?: string; // 每日可用结束时间后端字段useEndTime如"22:00:00"仅custom时有效
couponShare?: number; // 与优惠券同享0=否1=是后端字段couponShare
discountShare?: number; // 与限时折扣同享0=否1=是后端字段discountShare
vipPriceShare?: number; // 与会员价同享0=否1=是后端字段vipPriceShare
pointsShare?: number; // 与积分抵扣同享0=否1=是后端字段pointsShare
thresholds?: FullReductionThreshold[]; // 满减阈值列表多门槛后端字段thresholds
isDel?: boolean; // 是否删除0=否1=是后端字段isDel默认false
}
// ============================ 扩展:订单配置与费用汇总(加入后端满减类型) ============================
/** 扩展订单额外配置:使用后端满减活动类型 */
export interface OrderExtraConfig {
// ... 原有字段不变 ...
fullReductionActivities: FullReductionActivity[]; // 当前店铺的满减活动列表(后端返回结构)
currentDinnerType: "dine" | "pickup" | "deliv" | "express"; // 当前就餐类型匹配useType
}
// 辅助枚举星期映射用于useDays校验
const WEEKDAY_MAP = {
"周一": 1,
"周二": 2,
"周三": 3,
"周四": 4,
"周五": 5,
"周六": 6,
"周日": 0 // JS中getDay()返回0=周日
};
/**
* 辅助:校验当前时间是否在活动的「每日可用时段」内
* @param activity 满减活动
* @param currentTime 当前时间
* @returns 是否在时段内
*/
function isInDailyTimeRange(activity: FullReductionActivity, currentTime: Date): boolean {
// 全时段无需校验
if (activity.useTimeType === "all") return true;
// 无时段配置则不通过
if (!activity.useStartTime || !activity.useEndTime) return false;
const [startHour, startMinute] = activity.useStartTime.split(":").map(Number);
const [endHour, endMinute] = activity.useEndTime.split(":").map(Number);
const currentHour = currentTime.getHours();
const currentMinute = currentTime.getMinutes();
// 转换为分钟数比较
const startTotalMin = startHour * 60 + startMinute;
const endTotalMin = endHour * 60 + endMinute;
const currentTotalMin = currentHour * 60 + currentMinute;
// 处理跨天场景如23:00-02:00
if (startTotalMin <= endTotalMin) {
return currentTotalMin >= startTotalMin && currentTotalMin <= endTotalMin;
} else {
return currentTotalMin >= startTotalMin || currentTotalMin <= endTotalMin;
}
}
/**
* 辅助:校验当前时间是否在活动的「可用周期」内(如周一至周日)
* @param activity 满减活动
* @param currentTime 当前时间
* @returns 是否在周期内
*/
function isInWeeklyCycle(activity: FullReductionActivity, currentTime: Date): boolean {
// 无周期配置则不通过
if (!activity.useDays) return false;
const currentWeekday = currentTime.getDay(); // 0=周日1=周一...6=周六
const allowedWeekdays = activity.useDays.split(",").map(day => WEEKDAY_MAP[day as keyof typeof WEEKDAY_MAP]);
return allowedWeekdays.includes(currentWeekday);
}
/**
* 辅助:校验当前就餐类型是否在活动的「可用类型」内(如堂食/自取)
* @param activity 满减活动
* @param currentDinnerType 当前就餐类型
* @returns 是否匹配
*/
function isDinnerTypeMatch(activity: FullReductionActivity, currentDinnerType: string): boolean {
if (!activity.useType) return false;
const allowedTypes = activity.useType.split(",");
return allowedTypes.includes(currentDinnerType);
}
/**
* 筛选最优满减活动(对齐后端逻辑:状态→时间→周期→时段→就餐类型→排序→修改时间)
* @param activities 后端返回的满减活动列表
* @param currentShopId 当前店铺ID
* @param currentDinnerType 当前就餐类型dine/pickup等
* @param currentTime 当前时间(默认当前时间)
* @returns 最优满减活动无符合条件则返回undefined
*/
export function filterOptimalFullReductionActivity(
activities: FullReductionActivity[],
currentShopId: number,
currentDinnerType: string,
currentTime: Date = new Date()
): FullReductionActivity | undefined {
if (!activities.length) return undefined;
// 第一步:基础筛选(未删除+当前店铺+活动进行中+就餐类型匹配)
const baseEligible = activities.filter(activity => {
return (
activity.isDel !== true && // 未删除
activity.shopId === currentShopId && // 当前店铺
activity.status === 2 && // 状态=2进行中
isDinnerTypeMatch(activity, currentDinnerType) && // 就餐类型匹配
activity.thresholds?.length // 至少有一个满减阈值
);
});
if (!baseEligible.length) return undefined;
// 第二步:时间筛选(有效期内+周期内+时段内)
const timeEligible = baseEligible.filter(activity => {
// 1. 校验有效期validStartTime ~ validEndTime
if (!activity.validStartTime || !activity.validEndTime) return false;
const startDate = new Date(activity.validStartTime);
const endDate = new Date(activity.validEndTime);
// 处理有效期结束日期的23:59:59
endDate.setHours(23, 59, 59, 999);
if (currentTime < startDate || currentTime > endDate) return false;
// 2. 校验可用周期(如周一至周日)
if (!isInWeeklyCycle(activity, currentTime)) return false;
// 3. 校验每日可用时段如09:00-22:00
if (!isInDailyTimeRange(activity, currentTime)) return false;
return true;
});
if (!timeEligible.length) return undefined;
// 第三步:按优先级排序(需求规则)
return timeEligible.sort((a, b) => {
// 1. 先比排序值:排序值大的优先
if ((a.sort || 0) !== (b.sort || 0)) {
return (b.sort || 0) - (a.sort || 0);
}
// 2. 再比修改时间:最新修改的优先(时间戳降序)
const aUpdateTime = a.updateTime ? new Date(a.updateTime).getTime() : 0;
const bUpdateTime = b.updateTime ? new Date(b.updateTime).getTime() : 0;
return bUpdateTime - aUpdateTime;
})[0]; // 取最优活动
}
/**
* 折扣率格式化后端discountRate%)→ 工具库小数如90→0.9
*/
export function formatDiscountRate(backendDiscountRate?: number): number {
if (!backendDiscountRate || backendDiscountRate <= 0) return 1; // 默认无折扣1=100%
// 后端若为百分比如90=9折除以100若已为小数如0.9)直接返回
return backendDiscountRate >= 1
? backendDiscountRate / 100
: backendDiscountRate;
}
/**
* 统一小数处理:舍去小数点后两位以后的数字(解决浮点数精度偏差)
* 如 10.129 → 10.121.01 → 1.011.0100000001 → 1.01
* @param num 待处理数字
* @returns 处理后保留两位小数的数字
*/
export function truncateToTwoDecimals(num: number | string): number {
return new BigNumber(num).decimalPlaces(2, BigNumber.ROUND_DOWN).toNumber();
}
/**
* 判断商品是否为临时菜(临时菜不参与优惠券门槛和折扣计算)
* @param goods 商品项
* @returns 是否临时菜
*/
export function isTemporaryGoods(goods: BaseCartItem): boolean {
return !!goods.is_temporary;
}
/**
* 判断商品是否为赠菜(赠菜不计入优惠券活动,但计打包费)
* @param goods 商品项
* @returns 是否赠菜
*/
export function isGiftGoods(goods: BaseCartItem): boolean {
return !!goods.is_gift;
}
/**
* 计算单个商品的会员价优先级SKU会员价 > 商品会员价 > 会员折扣率)
* @param goods 商品项
* @param isMember 是否会员
* @param memberDiscountRate 会员折扣率如0.95=95折
* @returns 会员价(元)
*/
export function calcMemberPrice(
goods: BaseCartItem,
isMember: boolean,
memberDiscountRate?: number
): number {
if (!isMember) return truncateToTwoDecimals(goods.salePrice);
// 优先级SKU会员价 > 商品会员价 > 商品原价(无会员价时用会员折扣)
const basePrice =
goods.skuData?.memberPrice ?? goods.memberPrice ?? goods.salePrice;
// 仅当无SKU会员价、无商品会员价时才应用会员折扣率
if (memberDiscountRate && !goods.skuData?.memberPrice && !goods.memberPrice) {
return truncateToTwoDecimals(
new BigNumber(basePrice).multipliedBy(memberDiscountRate).toNumber()
);
}
return truncateToTwoDecimals(basePrice);
}
/**
* 过滤可参与优惠券计算的商品(排除临时菜、赠菜、已用兑换券的商品)
* @param goodsList 商品列表
* @param excludedProductIds 需排除的商品ID列表商品ID非购物车ID
* @returns 可参与优惠券计算的商品列表
*/
export function filterCouponEligibleGoods(
goodsList: BaseCartItem[],
excludedProductIds: string[] = []
): BaseCartItem[] {
return goodsList.filter(
(goods) =>
!isTemporaryGoods(goods) &&
!isGiftGoods(goods) &&
!excludedProductIds.includes(String(goods.product_id)) // 核心修正用商品ID排除
);
}
/**
* 统一筛选门槛商品的工具函数所有券类型复用按商品ID匹配
* @param baseEligibleGoods 基础合格商品(已排除临时菜/赠菜/已抵扣商品)
* @param applicableProductIds 优惠券指定的门槛商品ID数组
* @returns 最终参与优惠券计算的商品列表
*/
export function filterThresholdGoods(
baseEligibleGoods: BaseCartItem[],
applicableProductIds: string[]
): BaseCartItem[] {
// 空数组=全部基础合格商品;非空=仅商品ID匹配的商品转字符串兼容类型
return applicableProductIds.length === 0
? baseEligibleGoods
: baseEligibleGoods.filter((goods) =>
applicableProductIds.includes(String(goods.product_id))
); // 核心修正用商品ID匹配
}
/**
* 商品排序(用于商品兑换券:按价格/数量/加入顺序排序按商品ID分组去重
* @param goodsList 商品列表
* @param sortRule 排序规则low_price_first/high_price_first
* @param cartOrder 商品加入购物车的顺序key=购物车IDvalue=加入时间戳)
* @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.skuData?.salePrice ?? a.salePrice;
const priceB = b.skuData?.salePrice ?? 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. 同价格同数量按加入购物车顺序早的优先用购物车ID匹配
const orderA = cartOrder[String(a.id)] ?? Infinity;
const orderB = cartOrder[String(b.id)] ?? Infinity;
return orderA - orderB;
});
}
/**
* 计算优惠券门槛金额根据同享规则按商品ID匹配限时折扣
* @param eligibleGoods 可参与优惠券的商品列表(已过滤临时菜/赠菜)
* @param coupon 优惠券含discountShare/vipPriceShare配置
* @param config 订单配置(会员信息)
* @param activities 全局营销活动限时折扣applicableProductIds为商品ID
* @returns 满足优惠券门槛的金额基数
*/
export function calcCouponThresholdAmount(
eligibleGoods: BaseCartItem[],
coupon: BaseCoupon,
config: Pick<OrderExtraConfig, "isMember" | "memberDiscountRate">,
activities: ActivityConfig[] = []
): number {
let total = new BigNumber(0);
for (const goods of eligibleGoods) {
const availableNum = Math.max(0, goods.number - (goods.returnNum || 0));
if (availableNum <= 0) continue;
// 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); // 叠加限时折扣
}
}
total = total.plus(itemAmount);
}
return truncateToTwoDecimals(total.toNumber());
}
// ============================ 3. 商品核心价格计算核心修正按商品ID匹配营销活动 ============================
/**
* 计算单个商品的实际单价整合商家改价、会员优惠、营销活动折扣按商品ID匹配活动
* @param goods 商品项
* @param config 订单额外配置(含会员、活动信息)
* @returns 单个商品实际单价(元)
*/
export function calcSingleGoodsRealPrice(
goods: BaseCartItem,
config: Pick<OrderExtraConfig, "isMember" | "memberDiscountRate"> & {
activity?: ActivityConfig; // 商品参与的营销活动如限时折扣按商品ID匹配
}
): number {
const { isMember, memberDiscountRate, activity } = config;
//如果是增菜价格为0
if (goods.is_gift) {
return 0;
}
// 1. 优先级1商家改价改价后单价>0才生效
if (goods.discountSaleAmount && goods.discountSaleAmount > 0) {
return truncateToTwoDecimals(goods.discountSaleAmount);
}
// 2. 优先级2会员价含会员折扣率SKU会员价优先
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.toNumber();
}
// 处理活动与会员的同享/不同享逻辑
if (activity.vipPriceShare) {
// 同享:会员价基础上叠加工活动折扣
return truncateToTwoDecimals(
memberPrice.multipliedBy(activity.discountRate).toNumber()
);
} else {
// 不同享取会员价和活动价的最小值活动价用SKU原价计算
const basePriceForActivity = new BigNumber(
goods.skuData?.salePrice ?? goods.salePrice
);
const activityPrice = basePriceForActivity.multipliedBy(
activity.discountRate
);
return truncateToTwoDecimals(
memberPrice.isLessThanOrEqualTo(activityPrice)
? memberPrice.toNumber()
: activityPrice.toNumber()
);
}
}
/**
* 计算商品原价总和(所有商品:原价*数量含临时菜、赠菜用SKU原价优先
* @param goodsList 商品列表
* @returns 商品原价总和(元)
*/
export function calcGoodsOriginalAmount(goodsList: BaseCartItem[]): number {
let total = new BigNumber(0);
for (const goods of goodsList) {
const availableNum = Math.max(0, goods.number - (goods.returnNum || 0));
let basePrice = new BigNumber(0);
if (goods.is_temporary) {
basePrice = new BigNumber(goods?.discountSaleAmount ?? 0);
} else if (goods.is_gift) {
basePrice = new BigNumber(0);
}
else {
basePrice = new BigNumber(
goods.skuData?.salePrice ?? goods.salePrice
); // SKU原价优先
}
total = total.plus(basePrice.multipliedBy(availableNum));
}
return truncateToTwoDecimals(total.toNumber());
}
/**
* 计算商品实际总价(所有商品:实际单价*数量含临时菜、赠菜按商品ID匹配活动
* @param goodsList 商品列表
* @param config 订单额外配置(含会员、活动信息)
* @param activities 全局营销活动列表如限时折扣applicableProductIds为商品ID
* @returns 商品实际总价(元)
*/
export function calcGoodsRealAmount(
goodsList: BaseCartItem[],
config: Pick<OrderExtraConfig, "isMember" | "memberDiscountRate">,
activities: ActivityConfig[] = []
): number {
let total = new BigNumber(0);
for (const goods of goodsList) {
const availableNum = Math.max(0, goods.number - (goods.returnNum || 0));
if (availableNum <= 0) continue;
// 匹配商品参与的营销活动按商品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());
}
/**
* 计算商品折扣总金额(商品原价总和 - 商品实际总价)
* @param goodsOriginalAmount 商品原价总和
* @param goodsRealAmount 商品实际总价
* @returns 商品折扣总金额≥0
*/
export function calcGoodsDiscountAmount(
goodsOriginalAmount: number,
goodsRealAmount: number
): number {
const original = new BigNumber(goodsOriginalAmount);
const real = new BigNumber(goodsRealAmount);
const discount = original.minus(real);
return truncateToTwoDecimals(Math.max(0, discount.toNumber()));
}
/**
* 从满减活动的多门槛中,选择最优阈值(满金额最小且减免金额最大)
* @param thresholds 满减阈值列表(多门槛)
* @param baseAmount 满减计算基数(新客立减后的金额)
* @param goodsOriginalAmount 商品原价总和discountShare=0时用
* @param goodsRealAmount 商品折扣后总和discountShare=1时用
* @param discountShare 与限时折扣同享0=否1=是
* @returns 最优阈值无符合条件则返回undefined
*/
export function selectOptimalThreshold(
thresholds: FullReductionThreshold[] = [],
baseAmount: number,
goodsOriginalAmount: number,
goodsRealAmount: number,
discountShare: number = 0
): FullReductionThreshold | undefined {
if (!thresholds.length) return undefined;
// 第一步确定满减门槛基数根据discountShare规则
const thresholdBase = discountShare === 1
? new BigNumber(goodsRealAmount) // 与限时折扣同享→用折扣后金额
: new BigNumber(goodsOriginalAmount); // 不同享→用原价
// 第二步:筛选「满金额≤基数」且「减免金额>0」的有效阈值
const validThresholds = thresholds.filter(threshold => {
const fullAmount = new BigNumber(threshold.fullAmount || 0);
const discountAmount = new BigNumber(threshold.discountAmount || 0);
return fullAmount.isLessThanOrEqualTo(thresholdBase) && discountAmount.isGreaterThan(0);
});
if (!validThresholds.length) return undefined;
// 第三步选择最优阈值优先级1.满金额最小 → 2.减免金额最大)
return validThresholds.sort((a, b) => {
const aFull = new BigNumber(a.fullAmount || 0);
const bFull = new BigNumber(b.fullAmount || 0);
const aDiscount = new BigNumber(a.discountAmount || 0);
const bDiscount = new BigNumber(b.discountAmount || 0);
// 先比满金额越小越优先满1减10 比 满100减20 更优)
if (!aFull.isEqualTo(bFull)) {
return aFull.comparedTo(bFull) || 0; // Ensure a number is always returned
}
// 再比减免金额:越大越优先
return bDiscount.comparedTo(aDiscount) || 0; // Ensure a number is always returned
})[0];
}
/**
* 计算满减实际减免金额(适配多门槛、同享规则)
* @param optimalActivity 最优满减活动
* @param optimalThreshold 最优满减阈值
* @param baseAmount 计算基数(新客立减后的金额)
* @returns 实际减免金额未达门槛则0
*/
export function calcFullReductionAmount(
baseAmount: number,
optimalActivity?: FullReductionActivity,
optimalThreshold?: FullReductionThreshold,
): number {
if (!optimalActivity || !optimalThreshold) return 0;
const baseAmountBn = new BigNumber(baseAmount);
const discountAmountBn = new BigNumber(optimalThreshold.discountAmount || 0);
// 1. 基数必须为正(避免减免后为负)
if (baseAmountBn.isLessThanOrEqualTo(0)) return 0;
// 2. 减免金额不能超过基数(避免减成负数)
const maxReducible = baseAmountBn;
const actualReduction = discountAmountBn.isLessThanOrEqualTo(maxReducible)
? discountAmountBn
: maxReducible;
return truncateToTwoDecimals(actualReduction.toNumber());
}
// ------------------------------ 策略辅助函数 ------------------------------
/**
* 根据优惠券useShops列表判断门店是否匹配适配BaseCoupon的useShops字段
*/
function isStoreMatchByList(
useShops: string[],
currentStoreId: string
): boolean {
// 适用门店为空数组 → 无限制(所有门店适用)
if (useShops.length === 0) return true;
// 匹配当前门店ID字符串比较避免类型问题
return useShops.includes(currentStoreId);
}
/**
* 计算优惠券抵扣金额处理互斥逻辑选择最优优惠券按商品ID排除新增细分统计
* @param backendCoupons 后端优惠券列表
* @param goodsList 商品列表
* @param config 订单配置(含就餐类型)
* @returns 最优优惠券的抵扣结果(含商品券/满减券细分)
*/
export function calcCouponDeduction(
backendCoupons: BackendCoupon[],
goodsList: BaseCartItem[],
config: Pick<
OrderExtraConfig,
"currentStoreId" | "isMember" | "memberDiscountRate"
> & {
activities: ActivityConfig[];
cartOrder: Record<string, number>;
dinnerType: "dine-in" | "take-out";
currentTime?: Date;
}
): {
deductionAmount: number;
productCouponDeduction: number; // 新增:商品优惠券抵扣(兑换券/折扣券/买一送一等)
fullCouponDeduction: number; // 新增:满减优惠券抵扣
usedCoupon?: Coupon;
excludedProductIds: string[]; // 排除的商品ID列表商品ID
} {
const goodsCoupon = backendCoupons.filter((v) => v.type == 2);
const discountCoupon = backendCoupons.filter((v) => v.type != 2);
// 3. 计算非兑换券最优抵扣传递已抵扣商品ID避免重复统计细分字段
let nonExchangeResult: CouponResult = {
deductionAmount: discountCoupon.reduce((prve, cur): number => {
return prve + (cur.discountAmount || 0);
}, 0),
excludedProductIds: [],
usedCoupon: undefined,
productCouponDeduction: 0,
fullCouponDeduction: 0,
};
// 4. 计算兑换券抵扣排除非兑换券已抵扣的商品ID统计商品券细分
let exchangeResult: ExchangeCalculationResult = {
deductionAmount: goodsCoupon.reduce((prve, cur): number => {
return prve + (cur.discountAmount || 0);
}, 0),
excludedProductIds: [],
productCouponDeduction: 0,
};
// 5. 汇总结果:兑换券与非兑换券不可同时使用,取抵扣金额大的
const exchangeBn = new BigNumber(exchangeResult.deductionAmount);
const nonExchangeBn = new BigNumber(nonExchangeResult.deductionAmount);
const isExchangeBetter = exchangeBn.isGreaterThan(nonExchangeBn);
return {
deductionAmount: exchangeBn.plus(nonExchangeBn).toNumber(),
productCouponDeduction: exchangeResult.deductionAmount,
fullCouponDeduction: nonExchangeResult.deductionAmount, // 兑换券与满减券互斥满减券抵扣置0
usedCoupon: isExchangeBetter ? undefined : nonExchangeResult.usedCoupon,
excludedProductIds: isExchangeBetter
? exchangeResult.excludedProductIds
: nonExchangeResult.excludedProductIds,
};
}
// ============================ 5. 其他费用计算无ID依赖逻辑不变 ============================
/**
* 计算总打包费赠菜也计算称重商品打包数量≤1
* @param goodsList 商品列表
* @param dinnerType 就餐类型堂食dine-in/外卖take-out
* @returns 总打包费(元)
*/
export function calcTotalPackFee(
goodsList: BaseCartItem[],
dinnerType: "dine-in" | "take-out"
): number {
// if (dinnerType !== "take-out") return 0;
let total = new BigNumber(0);
for (const goods of goodsList) {
const packNumber = goods.packNumber ? goods.packNumber * 1 : 0;
let availableNum = Math.max(0, goods.number - (goods.returnNum || 0));
availableNum = Math.min(availableNum, packNumber);
if (availableNum === 0) continue;
// 计算单个商品打包数量外卖全打包堂食按配置称重商品≤1
let packNum = availableNum;
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());
}
/**
* 计算餐位费(按人数,不参与营销活动)
* @param config 餐位费配置
* @returns 餐位费未启用则0
*/
export function calcSeatFee(config: SeatFeeConfig): number {
if (!config.isEnabled || config.personCount == 0) return 0;
const personCount = Math.max(1, config.personCount); // 至少1人
return truncateToTwoDecimals(
new BigNumber(config.pricePerPerson).multipliedBy(personCount).toNumber()
);
}
/**
* 计算积分抵扣金额按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 };
}
const userPointsBn = new BigNumber(userPoints);
const pointsPerYuanBn = new BigNumber(rule.pointsPerYuan);
const maxLimitBn = new BigNumber(maxDeductionLimit);
// 最大可抵扣金额(积分可抵金额 vs 规则最大 vs 订单上限)
const maxDeductByPoints = userPointsBn.dividedBy(pointsPerYuanBn);
const maxDeductAmount = maxDeductByPoints.isLessThan(
rule.maxDeductionAmount ?? Infinity
)
? maxDeductByPoints
: new BigNumber(rule.maxDeductionAmount || Infinity).isLessThan(maxLimitBn)
? maxDeductByPoints
: maxLimitBn;
// 实际使用积分 = 抵扣金额 * 积分兑换比例
const usedPoints = maxDeductAmount.multipliedBy(pointsPerYuanBn);
return {
deductionAmount: truncateToTwoDecimals(maxDeductAmount.toNumber()),
usedPoints: truncateToTwoDecimals(
Math.min(usedPoints.toNumber(), userPoints)
), // 避免积分超扣
};
}
// ============================ 6. 订单总费用汇总与实付金额计算(核心入口,补充细分字段) ============================
/**
* 计算订单所有费用子项并汇总(核心入口函数)
* @param goodsList 购物车商品列表
* @param dinnerType 就餐类型
* @param backendCoupons 后端优惠券列表
* @param activities 全局营销活动列表
* @param config 订单额外配置(会员、积分、餐位费等)
* @param cartOrder 商品加入购物车顺序key=购物车IDvalue=时间戳)
* @param currentTime 当前时间(用于优惠券有效期判断)
* @returns 订单费用汇总(含所有子项和实付金额,新增商品券/满减券细分)
*/
export function calculateOrderCostSummary(
goodsList: BaseCartItem[],
dinnerType: "dine-in" | "take-out", // 前端就餐类型
backendCoupons: BackendCoupon[] = [],
activities: ActivityConfig[] = [],
config: OrderExtraConfig, // 含后端满减活动、currentDinnerType
cartOrder: Record<string, number> = {},
currentTime: Date = new Date()
): OrderCostSummary {
// ------------------------------ 1. 基础费用计算 ------------------------------
const goodsOriginalAmount = calcGoodsOriginalAmount(goodsList); // 商品原价总和
const goodsRealAmount = calcGoodsRealAmount( // 商品折扣后总和
goodsList,
{ isMember: config.isMember, memberDiscountRate: config.memberDiscountRate },
activities
);
const goodsDiscountAmount = calcGoodsDiscountAmount(goodsOriginalAmount, goodsRealAmount); // 商品折扣金额
const newUserDiscount = config.newUserDiscount || 0; // 新客立减
// ------------------------------ 2. 满减活动计算(核心步骤) ------------------------------
let usedFullReductionActivity: FullReductionActivity | undefined;
let usedFullReductionThreshold: FullReductionThreshold | undefined;
let fullReductionAmount = 0;
// 2.1 筛选最优满减活动(后端活动列表、当前店铺、就餐类型、时间)
usedFullReductionActivity = filterOptimalFullReductionActivity(
config.fullReductionActivities,
Number(config.currentStoreId), // 转换为数字后端shopId是number
config.currentDinnerType, // 后端useType匹配的就餐类型如"dine"
currentTime
);
// 2.2 计算满减基数(先扣新客立减)
const baseAfterNewUserDiscount = new BigNumber(goodsRealAmount)
.minus(newUserDiscount)
.isGreaterThan(0)
? new BigNumber(goodsRealAmount).minus(newUserDiscount).toNumber()
: 0;
// 2.3 选择最优满减阈值(多门槛场景)
if (usedFullReductionActivity) {
usedFullReductionThreshold = selectOptimalThreshold(
usedFullReductionActivity.thresholds,
baseAfterNewUserDiscount,
goodsOriginalAmount,
goodsRealAmount,
usedFullReductionActivity.discountShare || 0 // 与限时折扣同享规则
);
// 2.4 计算满减实际减免金额
fullReductionAmount = calcFullReductionAmount(
baseAfterNewUserDiscount,
usedFullReductionActivity,
usedFullReductionThreshold,
);
}
// ------------------------------ 3. 优惠券抵扣(适配满减同享规则) ------------------------------
let couponDeductionAmount = 0;
let productCouponDeduction = 0;
let fullCouponDeduction = 0;
let usedCoupon: Coupon | undefined;
let excludedProductIds: string[] = [];
// 若满减与优惠券同享couponShare=1才计算优惠券否则优惠券抵扣为0
if (usedFullReductionActivity?.couponShare === 1) {
const couponResult = calcCouponDeduction( // 原有优惠券计算函数
backendCoupons,
goodsList,
{
currentStoreId: config.currentStoreId,
isMember: config.isMember,
memberDiscountRate: config.memberDiscountRate,
activities,
cartOrder,
dinnerType,
currentTime
}
);
couponDeductionAmount = couponResult.deductionAmount;
productCouponDeduction = couponResult.productCouponDeduction;
fullCouponDeduction = couponResult.fullCouponDeduction;
usedCoupon = couponResult.usedCoupon;
excludedProductIds = couponResult.excludedProductIds;
}
// ------------------------------ 4. 积分抵扣(适配满减同享规则) ------------------------------
let pointDeductionAmount = 0;
let usedPoints = 0;
// 计算积分抵扣基数(商品折扣后 - 新客立减 - 满减 - 优惠券)
const maxPointDeductionLimit = new BigNumber(goodsRealAmount)
.minus(newUserDiscount)
.minus(fullReductionAmount)
.minus(couponDeductionAmount)
.isGreaterThan(0)
? new BigNumber(goodsRealAmount).minus(newUserDiscount).minus(fullReductionAmount).minus(couponDeductionAmount).toNumber()
: 0;
// 若满减与积分同享pointsShare=1才计算积分否则积分抵扣为0
if ((usedFullReductionActivity?.pointsShare === 1) && maxPointDeductionLimit > 0) {
const pointResult = calcPointDeduction(
config.userPoints,
config.pointDeductionRule,
maxPointDeductionLimit
);
pointDeductionAmount = pointResult.deductionAmount;
usedPoints = pointResult.usedPoints;
}
// ------------------------------ 5. 其他费用计算(原有逻辑不变) ------------------------------
const packFee = calcTotalPackFee(goodsList, dinnerType); // 打包费
let seatFee = calcSeatFee(config.seatFeeConfig); // 餐位费
seatFee = dinnerType === "dine-in" ? seatFee : 0; // 外卖不收餐位费
const additionalFee = Math.max(0, config.additionalFee); // 附加费
// 商家减免计算(原有逻辑不变)
const merchantReductionConfig = config.merchantReduction;
let merchantReductionActualAmount = 0;
const maxMerchantReductionLimit = new BigNumber(goodsRealAmount)
.minus(newUserDiscount)
.minus(fullReductionAmount)
.minus(couponDeductionAmount)
.minus(pointDeductionAmount)
.plus(seatFee)
.plus(packFee)
.isGreaterThan(0)
? new BigNumber(goodsRealAmount).minus(newUserDiscount).minus(fullReductionAmount).minus(couponDeductionAmount).minus(pointDeductionAmount).plus(seatFee).plus(packFee).toNumber()
: 0;
switch (merchantReductionConfig.type) {
case MerchantReductionType.FIXED_AMOUNT:
merchantReductionActualAmount = Math.min(
merchantReductionConfig.fixedAmount || 0,
maxMerchantReductionLimit
);
break;
case MerchantReductionType.DISCOUNT_RATE:
const validRate = Math.max(0, Math.min(100, merchantReductionConfig.discountRate || 0)) / 100;
merchantReductionActualAmount = maxMerchantReductionLimit * (1 - validRate);
break;
}
merchantReductionActualAmount = Math.max(0, truncateToTwoDecimals(merchantReductionActualAmount));
// ------------------------------ 6. 最终实付金额计算 ------------------------------
const finalPayAmountBn = new BigNumber(goodsRealAmount)
.minus(newUserDiscount)
.minus(fullReductionAmount)
.minus(couponDeductionAmount)
.minus(pointDeductionAmount)
.minus(merchantReductionActualAmount)
.plus(seatFee)
.plus(packFee)
.plus(additionalFee);
const finalPayAmount = Math.max(0, truncateToTwoDecimals(finalPayAmountBn.toNumber()));
// ------------------------------ 7. 总优惠金额计算 ------------------------------
const totalDiscountAmount = truncateToTwoDecimals(
new BigNumber(goodsDiscountAmount)
.plus(newUserDiscount)
.plus(fullReductionAmount)
.plus(couponDeductionAmount)
.plus(pointDeductionAmount)
.plus(merchantReductionActualAmount)
.toNumber()
);
// 积分可抵扣最大金额
// ------------------------------ 8. 返回完整结果 ------------------------------
return {
goodsTotal: goodsList.reduce((sum, g) => sum + Math.max(0, g.number - (g.returnNum || 0)), 0),
goodsRealAmount,
goodsOriginalAmount,
goodsDiscountAmount,
couponDeductionAmount,
productCouponDeduction,
fullCouponDeduction,
pointDeductionAmount,
seatFee,
packFee,
totalDiscountAmount,
// 满减活动明细(后端字段)
fullReduction: {
usedActivity: usedFullReductionActivity,
usedThreshold: usedFullReductionThreshold,
actualAmount: truncateToTwoDecimals(fullReductionAmount)
},
merchantReduction: {
type: merchantReductionConfig.type,
originalConfig: merchantReductionConfig,
actualAmount: merchantReductionActualAmount
},
additionalFee,
finalPayAmount,
couponUsed: usedCoupon,
pointUsed: usedPoints,
newUserDiscount,
dinnerType,
config
};
}
export function isWeightGoods(goods: BaseCartItem): boolean {
return goods.product_type === GoodsType.WEIGHT;
}
// ============================ 7. 对外暴露工具库 ============================
export const OrderPriceCalculator = {
// 基础工具
truncateToTwoDecimals,
isTemporaryGoods,
isGiftGoods,
formatDiscountRate,
filterThresholdGoods,
isWeightGoods,
// 商品价格计算
calcSingleGoodsRealPrice,
calcGoodsOriginalAmount,
calcGoodsRealAmount,
calcGoodsDiscountAmount,
//满减活动工具
filterOptimalFullReductionActivity,
// 优惠券计算
calcCouponDeduction,
// 其他费用计算
calcTotalPackFee,
calcSeatFee,
calcPointDeduction,
// 核心入口
calculateOrderCostSummary,
// 枚举导出
Enums: {
GoodsType,
CouponType,
ActivityType,
WEEKDAY_MAP,
},
};
export default OrderPriceCalculator;