/** * 购物车订单价格计算公共库 * 功能:覆盖订单全链路费用计算(商品价格、优惠券、积分、餐位费等),支持策略扩展 * 小数处理:统一舍去小数点后两位(如 10.129 → 10.12,15.998 → 15.99) * 扩展设计:优惠券/营销活动采用策略模式,新增类型仅需扩展策略,无需修改核心逻辑 * 关键规则: * - 所有优惠券均支持指定门槛商品(后端foods字段:空字符串=全部商品,ID字符串=指定商品ID) * - 与限时折扣/会员价同享规则:开启则门槛计算含对应折扣,关闭则用原价/非会员价 * 字段说明: * - BaseCartItem.id:购物车项ID(唯一标识购物车中的条目) * - BaseCartItem.product_id:商品ID(唯一标识商品,用于优惠券/活动匹配) * - BaseCartItem.skuData.id:SKU 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; // 实际使用的优惠券 } /** 兑换券计算结果类型 */ interface ExchangeCalculationResult { deductionAmount: number; excludedProductIds: string[]; // 不适用商品ID列表(商品ID) } /** 优惠券类型枚举 */ 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; // 店铺ID(int64) syncId?: number; // 同步Id(int64) couponType?: number; // 优惠券类型:1-满减券,2-商品兑换券,3-折扣券,4-第二件半价券,5-消费送券,6-买一送一券,7-固定价格券,8-免配送费券 title?: 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 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. 基础工具函数(核心修正:所有商品ID匹配用product_id) ============================ /** * 后端优惠券转工具库Coupon的转换函数 * @param backendCoupon 后端返回的优惠券 * @param currentStoreId 当前门店ID(用于验证门店适用性) * @param dinnerType 就餐类型(用于验证使用场景) * @param currentTime 当前时间(默认取当前时间,用于有效期判断) * @returns 工具库 Coupon | null(不支持的券类型/无效券返回null) */ export function convertBackendCouponToToolCoupon( backendCoupon: BackendCoupon, currentStoreId: string, dinnerType: 'dine-in' | 'take-out', currentTime: Date = new Date() ): Coupon | null { // 1. 基础校验:必选字段缺失直接返回null if (!backendCoupon.id || backendCoupon.couponType === undefined || !backendCoupon.title) { console.warn('优惠券必选字段缺失', backendCoupon); return null; } // 2. 转换券类型:后端数字枚举 → 工具库字符串枚举 const couponType = mapBackendCouponTypeToTool(backendCoupon.couponType); if (!couponType) { console.warn(`不支持的优惠券类型:${backendCoupon.couponType}(券ID:${backendCoupon.id})`); return null; } // 3. 统一处理所有券类型的applicableProductIds(映射后端foods,此处为商品ID列表) const applicableProductIds = backendCoupon.foods === '' || !backendCoupon.foods ? [] // 空字符串/undefined → 全部商品(按商品ID匹配) : backendCoupon.foods.split(',').map(id => id.trim()); // 逗号分隔 → 指定商品ID数组 // 4. 计算基础公共字段(含多维度可用性校验) const baseCoupon: BaseCoupon = { id: backendCoupon.id, type: couponType, name: backendCoupon.title, available: isCouponAvailable(backendCoupon, currentStoreId, dinnerType, currentTime), useShops: getApplicableStoreIds(backendCoupon, currentStoreId), discountShare: backendCoupon.discountShare === 1, vipPriceShare: backendCoupon.vipPriceShare === 1, useType: backendCoupon.useType ? backendCoupon.useType.split(',') : [], isValid: isCouponInValidPeriod(backendCoupon, currentTime), applicableProductIds: applicableProductIds }; // 5. 按券类型补充专属字段 switch (couponType) { case CouponType.FULL_REDUCTION: return { ...baseCoupon, fullAmount: backendCoupon.fullAmount || 0, discountAmount: backendCoupon.discountAmount || 0, maxDiscountAmount: backendCoupon.maxDiscountAmount ?? Infinity } as FullReductionCoupon; case CouponType.DISCOUNT: return { ...baseCoupon, discountRate: formatDiscountRate(backendCoupon.discountRate), maxDiscountAmount: backendCoupon.maxDiscountAmount ?? Infinity } as DiscountCoupon; case CouponType.SECOND_HALF: return { ...baseCoupon, maxUseCountPerOrder: backendCoupon.useLimit === -10086 ? Infinity : backendCoupon.useLimit || 1 } as SecondHalfPriceCoupon; case CouponType.BUY_ONE_GET_ONE: return { ...baseCoupon, maxUseCountPerOrder: backendCoupon.useLimit === -10086 ? Infinity : backendCoupon.useLimit || 1 } as BuyOneGetOneCoupon; case CouponType.EXCHANGE: return { ...baseCoupon, deductCount: backendCoupon.discountNum || 1, sortRule: backendCoupon.useRule === 'price_asc' ? 'low_price_first' : 'high_price_first' } as ExchangeCoupon; default: return null; } } // ------------------------------ 转换辅助函数 ------------------------------ /** * 后端优惠券类型(数字)→ 工具库优惠券类型(字符串枚举) */ function mapBackendCouponTypeToTool(backendType: number): CouponType | undefined { const typeMap: Record = { 1: CouponType.FULL_REDUCTION, // 1-满减券 2: CouponType.EXCHANGE, // 2-商品兑换券 3: CouponType.DISCOUNT, // 3-折扣券 4: CouponType.SECOND_HALF, // 4-第二件半价券 6: CouponType.BUY_ONE_GET_ONE // 6-买一送一券 }; return typeMap[backendType]; } /** * 多维度判断优惠券是否可用:状态+库存+有效期+隔天生效+每日时段+每周周期+门店+就餐类型 */ function isCouponAvailable( backendCoupon: BackendCoupon, currentStoreId: string, dinnerType: 'dine-in' | 'take-out', currentTime: Date = new Date() ): boolean { // 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; // 4. 隔天生效校验:若设置了隔天生效,需超过生效时间 if (!isCouponEffectiveAfterDays(backendCoupon, currentTime)) return false; // 5. 每日时段校验:当前时间需在每日可用时段内(useTimeType=custom时生效) if (!isCouponInDailyTimeRange(backendCoupon, currentTime)) return false; // 6. 每周周期校验:当前星期几需在可用周期内(useDays非空时生效) if (!isCouponInWeekDays(backendCoupon, currentTime)) return false; // 7. 门店匹配校验:当前门店需在适用门店范围内 if (!isStoreMatch(backendCoupon, currentStoreId)) return false; // 8. 就餐类型校验:当前就餐类型需在可用类型范围内 if (!isDinnerTypeMatch(backendCoupon, dinnerType)) return false; return true; } /** * 判断优惠券是否在有效期内(处理后端validType逻辑) */ function isCouponInValidPeriod(backendCoupon: BackendCoupon, currentTime: Date): boolean { const { validType, validStartTime, validEndTime, validDays, createTime } = backendCoupon; // 固定时间有效期(validType=fixed):直接对比validStartTime和validEndTime if (validType === 'fixed' && validStartTime && validEndTime) { const start = new Date(validStartTime); const end = new Date(validEndTime); return currentTime >= start && currentTime <= end; } // 自定义天数有效期(validType=custom):创建时间+validDays天 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; } // 无有效期配置:默认视为无效 return false; } /** * 隔天生效校验:若设置了daysToTakeEffect,需超过生效时间(创建时间+N天的0点) */ function isCouponEffectiveAfterDays(backendCoupon: BackendCoupon, currentTime: Date): boolean { if (!backendCoupon.daysToTakeEffect || backendCoupon.daysToTakeEffect <= 0) return true; if (!backendCoupon.createTime) return false; const create = new Date(backendCoupon.createTime); const effectiveTime = new Date(create); effectiveTime.setDate(create.getDate() + backendCoupon.daysToTakeEffect); effectiveTime.setHours(0, 0, 0, 0); // 隔天0点生效 return currentTime >= effectiveTime; } /** * 每日时段校验:当前时间需在useStartTime和useEndTime之间(仅比较时分秒,支持跨天) */ function isCouponInDailyTimeRange(backendCoupon: BackendCoupon, currentTime: Date): boolean { // 全时段可用或未配置时段类型 → 直接通过 if (backendCoupon.useTimeType === 'all' || !backendCoupon.useTimeType) return true; // 非自定义时段 → 默认可用(兼容未配置场景) if (backendCoupon.useTimeType !== 'custom') return true; // 缺少时段配置 → 无效 if (!backendCoupon.useStartTime || !backendCoupon.useEndTime) return false; // 解析时分(如"10:30" → [10, 30]) const [startHours, startMinutes] = backendCoupon.useStartTime.split(':').map(Number); const [endHours, endMinutes] = backendCoupon.useEndTime.split(':').map(Number); // 转换为当天分钟数(便于比较) const currentMinutes = currentTime.getHours() * 60 + currentTime.getMinutes(); const startTotalMinutes = startHours * 60 + startMinutes; const endTotalMinutes = endHours * 60 + endMinutes; // 处理跨天场景(如22:00-02:00) if (startTotalMinutes <= endTotalMinutes) { return currentMinutes >= startTotalMinutes && currentMinutes <= endTotalMinutes; } else { return currentMinutes >= startTotalMinutes || currentMinutes <= endTotalMinutes; } } /** * 每周周期校验:当前星期几需在useDays范围内(如"周一,周二") */ function isCouponInWeekDays(backendCoupon: BackendCoupon, currentTime: Date): boolean { if (!backendCoupon.useDays) return true; // 未配置周期 → 默认可用 // 星期映射:getDay()返回0=周日,1=周一...6=周六 const weekDayMap = { 0: '周七', 1: '周一', 2: '周二', 3: '周三', 4: '周四', 5: '周五', 6: '周六' }; const currentWeekDay = weekDayMap[currentTime.getDay() as keyof typeof weekDayMap]; return backendCoupon.useDays.split(',').includes(currentWeekDay); } /** * 门店匹配校验:根据useShopType判断当前门店是否适用 */ function isStoreMatch(backendCoupon: BackendCoupon, currentStoreId: string): boolean { const { useShopType, useShops, shopId } = backendCoupon; switch (useShopType) { case 'all': // 所有门店适用 return true; case 'custom': // 指定门店适用(useShops逗号分割,门店ID) return useShops ? useShops.split(',').includes(currentStoreId) : false; case 'only': // 仅本店适用(shopId为门店ID) return shopId ? String(shopId) === currentStoreId : false; default: // 未配置 → 默认为仅本店 return shopId ? String(shopId) === currentStoreId : false; } } /** * 就餐类型匹配校验:当前就餐类型需在useType范围内(如"dine,pickup") */ function isDinnerTypeMatch(backendCoupon: BackendCoupon, dinnerType: string): boolean { if (!backendCoupon.useType) return true; // 未配置 → 默认可用 return backendCoupon.useType.split(',').includes(dinnerType); } /** * 处理适用门店ID:根据useShopType返回对应数组(供BaseCoupon使用) */ function getApplicableStoreIds(backendCoupon: BackendCoupon, currentStoreId: string): string[] { const { useShopType, useShops, shopId } = backendCoupon; switch (useShopType) { case 'all': // 所有门店适用:返回空数组(工具库空数组表示无限制) return []; case 'custom': // 指定门店适用:useShops逗号分割转数组(门店ID) return useShops ? useShops.split(',').map(id => id.trim()) : []; case 'only': // 仅当前店铺适用:返回shopId(转字符串,门店ID) return shopId ? [shopId.toString()] : []; default: // 未配置:默认仅当前门店适用 return [currentStoreId]; } } /** * 折扣率格式化:后端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.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.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 goods.salePrice; // 优先级:SKU会员价 > 商品会员价 > 商品原价(无会员价时用会员折扣) const basePrice = goods.skuData?.memberPrice ?? goods.memberPrice ?? goods.salePrice; // 仅当无SKU会员价、无商品会员价时,才应用会员折扣率 return memberDiscountRate && !goods.skuData?.memberPrice && !goods.memberPrice ? truncateToTwoDecimals(basePrice * memberDiscountRate) : 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=购物车ID,value=加入时间戳) * @returns 排序后的商品列表 */ export function sortGoodsForCoupon( goodsList: BaseCartItem[], sortRule: 'low_price_first' | 'high_price_first', cartOrder: Record = {} ): 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, activities: ActivityConfig[] = [] ): number { return truncateToTwoDecimals( eligibleGoods.reduce((total, goods) => { const availableNum = Math.max(0, goods.number - (goods.returnNum || 0)); if (availableNum <= 0) return total; // 1. 基础金额:默认用商品原价(SKU原价优先) const basePrice = goods.skuData?.salePrice ?? goods.salePrice; let itemAmount = basePrice * availableNum; // 2. 处理「与会员价/会员折扣同享」规则:若开启,用会员价计算 if (coupon.vipPriceShare) { const memberPrice = calcMemberPrice(goods, config.isMember, config.memberDiscountRate); itemAmount = memberPrice * 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 * activity.discountRate; // 叠加限时折扣 } } return total + itemAmount; }, 0) ); } // ============================ 3. 商品核心价格计算(核心修正:按商品ID匹配营销活动) ============================ /** * 计算单个商品的实际单价(整合商家改价、会员优惠、营销活动折扣,按商品ID匹配活动) * @param goods 商品项 * @param config 订单额外配置(含会员、活动信息) * @returns 单个商品实际单价(元) */ export function calcSingleGoodsRealPrice( goods: BaseCartItem, config: Pick & { activity?: ActivityConfig; // 商品参与的营销活动(如限时折扣,按商品ID匹配) } ): number { const { isMember, memberDiscountRate, activity } = config; // 1. 优先级1:商家改价(改价后单价>0才生效) if (goods.discountSaleAmount && goods.discountSaleAmount > 0) { return truncateToTwoDecimals(goods.discountSaleAmount); } // 2. 优先级2:会员价(含会员折扣率,SKU会员价优先) const memberPrice = calcMemberPrice(goods, isMember, memberDiscountRate); // 3. 优先级3:营销活动折扣(如限时折扣,需按商品ID匹配活动) const isActivityApplicable = activity ? (activity.applicableProductIds || []).includes(String(goods.product_id)) // 核心修正:用商品ID匹配活动 : false; if (!activity || !isActivityApplicable) { return memberPrice; } // 处理活动与会员的同享/不同享逻辑 if (activity.vipPriceShare) { // 同享:会员价基础上叠加工活动折扣 return truncateToTwoDecimals(memberPrice * activity.discountRate); } else { // 不同享:取会员价和活动价的最小值(活动价用SKU原价计算) const basePriceForActivity = goods.skuData?.salePrice ?? goods.salePrice; const activityPrice = truncateToTwoDecimals(basePriceForActivity * activity.discountRate); return Math.min(memberPrice, activityPrice); } } /** * 计算商品原价总和(所有商品:原价*数量,含临时菜、赠菜,用SKU原价优先) * @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)); const basePrice = goods.skuData?.salePrice ?? goods.salePrice; // SKU原价优先 return total + basePrice * availableNum; }, 0) ); } /** * 计算商品实际总价(所有商品:实际单价*数量,含临时菜、赠菜,按商品ID匹配活动) * @param goodsList 商品列表 * @param config 订单额外配置(含会员、活动信息) * @param activities 全局营销活动列表(如限时折扣,applicableProductIds为商品ID) * @returns 商品实际总价(元) */ export function calcGoodsRealAmount( goodsList: BaseCartItem[], config: Pick, activities: ActivityConfig[] = [] ): number { return truncateToTwoDecimals( goodsList.reduce((total, goods) => { const availableNum = Math.max(0, goods.number - (goods.returnNum || 0)); if (availableNum <= 0) return total; // 匹配商品参与的营销活动(按商品ID匹配,优先商品自身配置) const activity = goods.activityInfo ?? activities.find(act => (act.applicableProductIds || []).includes(String(goods.product_id)) // 核心修正:用商品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. 优惠券抵扣计算(策略模式,核心修正:按商品ID匹配) ============================ /** 优惠券计算策略接口(新增优惠券类型时,实现此接口即可) */ interface CouponCalculationStrategy { calculate( coupon: Coupon, goodsList: BaseCartItem[], config: Pick & { activities: ActivityConfig[]; cartOrder: Record; excludedProductIds?: string[]; // 需排除的商品ID列表(商品ID) } ): { deductionAmount: number; excludedProductIds: string[]; // 排除的商品ID列表(商品ID) }; } /** 满减券计算策略(按商品ID筛选门槛商品) */ class FullReductionStrategy implements CouponCalculationStrategy { calculate( coupon: FullReductionCoupon, goodsList: BaseCartItem[], config: any ): { deductionAmount: number; excludedProductIds: string[] } { // 1. 基础校验:优惠券不可用/门店不匹配 → 抵扣0 if (!coupon.available || !isStoreMatchByList(coupon.useShops, config.currentStoreId)) { return { deductionAmount: 0, excludedProductIds: [] }; } // 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: [] }; } // 4. 按同享规则计算门槛金额(按商品ID匹配活动) const thresholdAmount = calcCouponThresholdAmount( thresholdGoods, coupon, { isMember: config.isMember, memberDiscountRate: config.memberDiscountRate }, config.activities ); // 5. 满门槛则抵扣,否则0(不超过最大减免) if (thresholdAmount < coupon.fullAmount) { return { deductionAmount: 0, excludedProductIds: [] }; } const maxReduction = coupon.maxDiscountAmount ?? coupon.discountAmount; const deductionAmount = truncateToTwoDecimals(Math.min(coupon.discountAmount, maxReduction)); return { deductionAmount, excludedProductIds: [] }; } } /** 折扣券计算策略(按商品ID筛选门槛商品) */ class DiscountStrategy implements CouponCalculationStrategy { calculate( coupon: DiscountCoupon, goodsList: BaseCartItem[], config: any ): { deductionAmount: number; excludedProductIds: string[] } { // 1. 基础校验:优惠券不可用/门店不匹配 → 抵扣0 if (!coupon.available || !isStoreMatchByList(coupon.useShops, config.currentStoreId)) { return { deductionAmount: 0, excludedProductIds: [] }; } // 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: [] }; } // 4. 按同享规则计算折扣基数(按商品ID匹配活动) const discountBaseAmount = 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: [] }; } } /** 第二件半价券计算策略(按商品ID分组,筛选门槛商品) */ class SecondHalfPriceStrategy implements CouponCalculationStrategy { calculate( coupon: SecondHalfPriceCoupon, goodsList: BaseCartItem[], config: any ): { deductionAmount: number; excludedProductIds: string[] } { // 1. 基础校验:优惠券不可用/门店不匹配 → 抵扣0 if (!coupon.available || !isStoreMatchByList(coupon.useShops, config.currentStoreId)) { return { deductionAmount: 0, excludedProductIds: [] }; } let totalDeduction = 0; const excludedProductIds: string[] = []; // 存储排除的商品ID(商品ID) // 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: [] }; } // 按商品ID分组(避免同商品多次处理,用商品ID作为分组key) const goodsGroupByProductId = thresholdGoods.reduce((group, goods) => { const productIdStr = String(goods.product_id); // 商品ID转字符串作为key if (!group[productIdStr]) group[productIdStr] = []; group[productIdStr].push(goods); return group; }, {} as Record); // 遍历每组商品计算半价优惠 for (const [productIdStr, productGoods] of Object.entries(goodsGroupByProductId)) { if (excludedProductIds.includes(productIdStr)) continue; // 按商品ID排除已处理商品 // 合并同商品数量(所有购物车项的数量总和) const totalNum = productGoods.reduce((sum, g) => sum + (g.number - (g.returnNum || 0)), 0); if (totalNum < 2) continue; // 至少2件才享受优惠 // 计算优惠次数(每2件1次,不超过最大使用次数) const discountCount = Math.min( Math.floor(totalNum / 2), coupon.maxUseCountPerOrder || Infinity ); if (discountCount <= 0) continue; // 计算单件实际价格(取组内任意一个商品的配置,同商品规格一致) const sampleGood = productGoods[0]; const activity = config.activities.find((act: ActivityConfig) => (act.applicableProductIds || []).includes(productIdStr) // 按商品ID匹配活动 ); const realPrice = calcSingleGoodsRealPrice(sampleGood, { isMember: config.isMember, memberDiscountRate: config.memberDiscountRate, activity }); // 累计抵扣金额并标记已优惠商品(记录商品ID) totalDeduction += realPrice * 0.5 * discountCount; excludedProductIds.push(productIdStr); } return { deductionAmount: truncateToTwoDecimals(totalDeduction), excludedProductIds }; } } /** 买一送一券计算策略(按商品ID分组,筛选门槛商品) */ class BuyOneGetOneStrategy implements CouponCalculationStrategy { calculate( coupon: BuyOneGetOneCoupon, goodsList: BaseCartItem[], config: any ): { deductionAmount: number; excludedProductIds: string[] } { // 1. 基础校验:优惠券不可用/门店不匹配 → 抵扣0 if (!coupon.available || !isStoreMatchByList(coupon.useShops, config.currentStoreId)) { return { deductionAmount: 0, excludedProductIds: [] }; } let totalDeduction = 0; const excludedProductIds: string[] = []; // 存储排除的商品ID(商品ID) // 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: [] }; } // 按商品ID分组(用商品ID作为分组key) const goodsGroupByProductId = thresholdGoods.reduce((group, goods) => { const productIdStr = String(goods.product_id); if (!group[productIdStr]) group[productIdStr] = []; group[productIdStr].push(goods); return group; }, {} as Record); // 遍历每组商品计算买一送一优惠 for (const [productIdStr, productGoods] of Object.entries(goodsGroupByProductId)) { if (excludedProductIds.includes(productIdStr)) continue; // 按商品ID排除已处理商品 // 合并同商品数量 const totalNum = productGoods.reduce((sum, g) => sum + (g.number - (g.returnNum || 0)), 0); if (totalNum < 2) continue; // 至少2件才享受优惠 // 计算优惠次数(每2件送1件) const discountCount = Math.floor(totalNum / 2); if (discountCount <= 0) continue; // 计算单件实际价格(按商品ID匹配活动) const sampleGood = productGoods[0]; const activity = config.activities.find((act: ActivityConfig) => (act.applicableProductIds || []).includes(productIdStr) // 按商品ID匹配活动 ); const realPrice = calcSingleGoodsRealPrice(sampleGood, { isMember: config.isMember, memberDiscountRate: config.memberDiscountRate, activity }); // 累计抵扣金额(送1件=减免1件价格)并标记商品ID totalDeduction += realPrice * 1 * discountCount; excludedProductIds.push(productIdStr); } return { deductionAmount: truncateToTwoDecimals(totalDeduction), excludedProductIds }; } } /** 商品兑换券计算策略(按商品ID筛选门槛商品) */ class ExchangeCouponStrategy implements CouponCalculationStrategy { calculate( coupon: ExchangeCoupon, goodsList: BaseCartItem[], config: any ): { deductionAmount: number; excludedProductIds: string[] } { // 1. 基础校验:优惠券不可用/门店不匹配 → 抵扣0 if (!coupon.available || !isStoreMatchByList(coupon.useShops, config.currentStoreId)) { return { deductionAmount: 0, excludedProductIds: [] }; } // 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: [] }; } // 按规则排序商品(按价格/数量/加入顺序) const sortedGoods = sortGoodsForCoupon(thresholdGoods, coupon.sortRule, config.cartOrder); let remainingCount = coupon.deductCount; let totalDeduction = 0; const excludedProductIds: string[] = []; // 存储排除的商品ID(商品ID) // 计算兑换抵扣金额(按商品ID累计,避免重复抵扣) const usedProductIds = new Set(); // 记录已使用的商品ID(避免同商品多次抵扣) for (const goods of sortedGoods) { if (remainingCount <= 0) break; const productIdStr = String(goods.product_id); if (usedProductIds.has(productIdStr)) continue; // 同商品仅抵扣一次 const availableNum = Math.max(0, goods.number - (goods.returnNum || 0)); if (availableNum === 0) continue; // 计算当前商品可抵扣的件数(不超过剩余可抵扣数量) const deductCount = Math.min(availableNum, remainingCount); const activity = config.activities.find((act: ActivityConfig) => (act.applicableProductIds || []).includes(productIdStr) // 按商品ID匹配活动 ); const realPrice = calcSingleGoodsRealPrice(goods, { isMember: config.isMember, memberDiscountRate: config.memberDiscountRate, activity }); // 累计抵扣金额并标记商品 totalDeduction += realPrice * deductCount; excludedProductIds.push(productIdStr); usedProductIds.add(productIdStr); remainingCount -= deductCount; } return { deductionAmount: truncateToTwoDecimals(totalDeduction), excludedProductIds }; } } // ------------------------------ 策略辅助函数 ------------------------------ /** * 根据优惠券useShops列表判断门店是否匹配(适配BaseCoupon的useShops字段) */ function isStoreMatchByList(useShops: string[], currentStoreId: string): boolean { // 适用门店为空数组 → 无限制(所有门店适用) if (useShops.length === 0) return true; // 匹配当前门店ID(字符串比较,避免类型问题) return useShops.includes(currentStoreId); } /** * 优惠券计算策略工厂(根据优惠券类型获取对应策略,易扩展) */ 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}`); } } /** * 计算优惠券抵扣金额(处理互斥逻辑,选择最优优惠券,按商品ID排除) * @param backendCoupons 后端优惠券列表 * @param goodsList 商品列表 * @param config 订单配置(含就餐类型) * @returns 最优优惠券的抵扣结果 */ export function calcCouponDeduction( backendCoupons: BackendCoupon[], goodsList: BaseCartItem[], config: Pick & { activities: ActivityConfig[]; cartOrder: Record; dinnerType: 'dine-in' | 'take-out'; currentTime?: Date; } ): { deductionAmount: number; usedCoupon?: Coupon; excludedProductIds: string[]; // 排除的商品ID列表(商品ID) } { // 1. 后端优惠券转工具库Coupon(过滤无效/不支持的券) const toolCoupons = backendCoupons .map(coupon => convertBackendCouponToToolCoupon( coupon, config.currentStoreId, config.dinnerType, config.currentTime )) .filter(Boolean) as Coupon[]; if (toolCoupons.length === 0) { return { deductionAmount: 0, excludedProductIds: [] }; } // 2. 优惠券互斥逻辑:兑换券与其他券互斥,优先选最优 const exchangeCoupons = toolCoupons.filter(c => c.type === CouponType.EXCHANGE); const nonExchangeCoupons = toolCoupons.filter(c => c.type !== CouponType.EXCHANGE); // 3. 计算非兑换券最优抵扣(传递已抵扣商品ID,避免重复) let nonExchangeResult: CouponResult = { deductionAmount: 0, excludedProductIds: [], usedCoupon: undefined }; if (nonExchangeCoupons.length > 0) { nonExchangeResult = nonExchangeCoupons.reduce((best, coupon) => { const strategy = getCouponStrategy(coupon.type); const result = strategy.calculate(coupon, goodsList, { ...config, excludedProductIds: best.excludedProductIds // 传递已排除的商品ID(商品ID) }); const currentResult: CouponResult = { ...result, usedCoupon: coupon }; return currentResult.deductionAmount > best.deductionAmount ? currentResult : best; }, nonExchangeResult); } // 4. 计算兑换券抵扣(排除非兑换券已抵扣的商品ID) 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, excludedProductIds: [...nonExchangeResult.excludedProductIds, ...best.excludedProductIds] // 合并排除的商品ID }); return result.deductionAmount > best.deductionAmount ? result : best; }, exchangeResult); } // 5. 汇总结果:兑换券与非兑换券不可同时使用,取抵扣金额大的 const isExchangeBetter = exchangeResult.deductionAmount > nonExchangeResult.deductionAmount; return { deductionAmount: truncateToTwoDecimals(isExchangeBetter ? exchangeResult.deductionAmount : nonExchangeResult.deductionAmount), 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 { 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.product_type === 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 backendCoupons 后端优惠券列表 * @param activities 全局营销活动列表 * @param config 订单额外配置(会员、积分、餐位费等) * @param cartOrder 商品加入购物车顺序(key=购物车ID,value=时间戳) * @param currentTime 当前时间(用于优惠券有效期判断) * @returns 订单费用汇总(含所有子项和实付金额) */ export function calculateOrderCostSummary( goodsList: BaseCartItem[], dinnerType: 'dine-in' | 'take-out', backendCoupons: BackendCoupon[] = [], activities: ActivityConfig[] = [], config: OrderExtraConfig, cartOrder: Record = {}, 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); // 2. 优惠券抵扣(传递就餐类型用于可用性判断) const { deductionAmount: couponDeductionAmount, usedCoupon, excludedProductIds } = calcCouponDeduction( backendCoupons, goodsList, { currentStoreId: config.currentStoreId, isMember: config.isMember, memberDiscountRate: config.memberDiscountRate, activities, cartOrder, dinnerType, currentTime } ); // 3. 其他费用计算 const packFee = calcTotalPackFee(goodsList, dinnerType); const seatFee = calcSeatFee(config.seatFeeConfig); // 积分抵扣上限:商品实际价 - 优惠券抵扣(避免负抵扣) const maxPointDeductionLimit = Math.max(0, goodsRealAmount - couponDeductionAmount); 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); return { goodsOriginalAmount, goodsDiscountAmount, couponDeductionAmount, pointDeductionAmount, seatFee, packFee, merchantReductionAmount, additionalFee, finalPayAmount: finalPayAmountNonNegative, couponUsed: usedCoupon, pointUsed: usedPoints }; } // ============================ 7. 对外暴露工具库 ============================ export const OrderPriceCalculator = { // 基础工具 truncateToTwoDecimals, isTemporaryGoods, isGiftGoods, formatDiscountRate, filterThresholdGoods, // 优惠券转换 convertBackendCouponToToolCoupon, // 商品价格计算 calcSingleGoodsRealPrice, calcGoodsOriginalAmount, calcGoodsRealAmount, calcGoodsDiscountAmount, // 优惠券计算 calcCouponDeduction, // 其他费用计算 calcTotalPackFee, calcSeatFee, calcPointDeduction, // 核心入口 calculateOrderCostSummary, // 枚举导出 Enums: { GoodsType, CouponType, ActivityType, } }; export default OrderPriceCalculator;