diff --git a/src/store/modules/carts.ts b/src/store/modules/carts.ts index f8538ba..abd190f 100644 --- a/src/store/modules/carts.ts +++ b/src/store/modules/carts.ts @@ -275,6 +275,7 @@ export const useCartsStore = defineStore("carts", () => { // 订单费用汇总(调用内部的 getAllGoodsList) const orderCostSummary = computed(() => { allGoods.value = getAllGoodsList(); + console.log(' allGoods.value', allGoods.value); const costSummary = OrderPriceCalculator.calculateOrderCostSummary( allGoods.value, dinnerType.value, @@ -284,6 +285,8 @@ export const useCartsStore = defineStore("carts", () => { cartOrder.value, new Date() ); + console.log('costSummary', costSummary); + return costSummary; }); @@ -671,10 +674,14 @@ export const useCartsStore = defineStore("carts", () => { } if (msg.operate_type === "manage_edit") { - const newCart = msg.data; + const skuData = getProductDetails({ product_id: msg.data.product_id, sku_id: msg.data.sku_id }); + console.log('skuData,', skuData); + const newCart = { ...skuData, ...msg.data }; + const listIndex = list.value.findIndex(item => item.id === newCart.id); const giftIndex = giftList.value.findIndex(item => item.id === newCart.id); + if (giftIndex > -1) { if (!newCart.is_gift) { giftList.value.splice(giftIndex, 1); diff --git a/src/utils/goods.ts b/src/utils/goods.ts index 2bdf643..0c50174 100644 --- a/src/utils/goods.ts +++ b/src/utils/goods.ts @@ -1,9 +1,9 @@ -import { BigNumber } from 'bignumber.js'; +import { BigNumber } from "bignumber.js"; // 配置BigNumber精度 BigNumber.set({ DECIMAL_PLACES: 2, - ROUNDING_MODE: BigNumber.ROUND_DOWN // 向下取整,符合业务需求 + ROUNDING_MODE: BigNumber.ROUND_DOWN, // 向下取整,符合业务需求 }); /** @@ -23,36 +23,36 @@ BigNumber.set({ // ============================ 1. 基础类型定义(核心修正:明确ID含义) ============================ /** 商品类型枚举 */ export enum GoodsType { - NORMAL = 'normal', // 普通商品 - WEIGHT = 'weight', // 称重商品 - GIFT = 'gift', // 赠菜(继承普通商品逻辑,标记用) - EMPTY = '', // 空字符串类型(后端未返回时默认归类为普通商品) - PACKAGE = 'package'// 打包商品(如套餐/预打包商品,按普通商品逻辑处理,可扩展特殊规则) + 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; // 新增:满减优惠券抵扣 + deductionAmount: number; // 抵扣金额 + excludedProductIds: string[]; // 不适用商品ID列表(注意:是商品ID,非购物车ID) + usedCoupon: Coupon | undefined; // 实际使用的优惠券 + productCouponDeduction: number; // 新增:商品优惠券抵扣(兑换券等) + fullCouponDeduction: number; // 新增:满减优惠券抵扣 } /** 兑换券计算结果类型(新增细分字段) */ interface ExchangeCalculationResult { deductionAmount: number; - excludedProductIds: string[]; // 不适用商品ID列表(商品ID) - productCouponDeduction: 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', // 商品兑换券 + FULL_REDUCTION = "full_reduction", // 满减券 + DISCOUNT = "discount", // 折扣券 + SECOND_HALF = "second_half", // 第二件半价券 + BUY_ONE_GET_ONE = "buy_one_get_one", // 买一送一券 + EXCHANGE = "exchange", // 商品兑换券 } /** 后端返回的优惠券原始字段类型 */ @@ -100,32 +100,34 @@ export interface BackendCoupon { /** 营销活动类型枚举 */ export enum ActivityType { - TIME_LIMIT_DISCOUNT = 'time_limit_discount', // 限时折扣 + 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?: { // 商品参与的营销活动(如限时折扣) + 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) + discountRate: number; // 折扣率(如0.8=8折) + vipPriceShare: boolean; // 是否与会员优惠同享(默认false) }; - skuData?: { // SKU扩展数据(可选) - id: string | number; // SKU ID(唯一标识商品规格,如颜色/尺寸) - memberPrice?: number; // SKU会员价 - salePrice?: number; // SKU原价 + skuData?: { + // SKU扩展数据(可选) + id: string | number; // SKU ID(唯一标识商品规格,如颜色/尺寸) + memberPrice?: number; // SKU会员价 + salePrice?: number; // SKU原价 }; } @@ -178,36 +180,41 @@ export interface BuyOneGetOneCoupon extends BaseCoupon { export interface ExchangeCoupon extends BaseCoupon { type: CouponType.EXCHANGE; deductCount: number; // 对应后端discountNum(抵扣数量) - sortRule: 'low_price_first' | 'high_price_first'; // 后端useRule转换 + sortRule: "low_price_first" | "high_price_first"; // 后端useRule转换 } /** 所有优惠券类型联合 */ -export type Coupon = FullReductionCoupon | DiscountCoupon | SecondHalfPriceCoupon | BuyOneGetOneCoupon | ExchangeCoupon; +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; // 是否与会员优惠同享 + 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; // 最大抵扣金额(元,默认不限) + pointsPerYuan: number; // X积分=1元(如100=100积分抵1元) + maxDeductionAmount?: number; // 最大抵扣金额(元,默认不限) } /** 餐位费配置 */ export interface SeatFeeConfig { - pricePerPerson: number; // 每人餐位费(元) - personCount: number; // 用餐人数(默认1) - isEnabled: boolean; // 是否启用餐位费(默认false) + pricePerPerson: number; // 每人餐位费(元) + personCount: number; // 用餐人数(默认1) + isEnabled: boolean; // 是否启用餐位费(默认false) } /** 商家减免类型枚举 */ export enum MerchantReductionType { - FIXED_AMOUNT = 'fixed_amount', // 固定金额减免(如直接减 10 元) - DISCOUNT_RATE = 'discount_rate' // 比例折扣减免(如打 9 折,即减免 10%) + FIXED_AMOUNT = "fixed_amount", // 固定金额减免(如直接减 10 元) + DISCOUNT_RATE = "discount_rate", // 比例折扣减免(如打 9 折,即减免 10%) } /** 商家减免配置(新增,替代原单一金额字段) */ @@ -221,18 +228,20 @@ export interface OrderExtraConfig { // merchantReduction: number; // 商家减免金额(元,默认0) // 替换原单一金额字段,支持两种减免形式 merchantReduction: MerchantReductionConfig; - additionalFee: number; // 附加费(元,如余额充值、券包,默认0) + additionalFee: number; // 附加费(元,如余额充值、券包,默认0) pointDeductionRule: PointDeductionRule; // 积分抵扣规则 - seatFeeConfig: SeatFeeConfig; // 餐位费配置 - currentStoreId: string; // 当前门店ID(用于验证优惠券适用门店) - userPoints: number; // 用户当前积分(用于积分抵扣) - isMember: boolean; // 用户是否会员(用于会员优惠) - memberDiscountRate?: number; // 会员折扣率(如0.95=95折,无会员价时用) + seatFeeConfig: SeatFeeConfig; // 餐位费配置 + currentStoreId: string; // 当前门店ID(用于验证优惠券适用门店) + userPoints: number; // 用户当前积分(用于积分抵扣) + isMember: boolean; // 用户是否会员(用于会员优惠) + memberDiscountRate?: number; // 会员折扣率(如0.95=95折,无会员价时用) newUserDiscount?: number; // 新用户减免金额(元,默认0) } /** 订单费用汇总(修改:补充商家减免类型和明细) */ export interface OrderCostSummary { + // 商品总件数 + goodsTotal: number; goodsRealAmount: number; // 商品真实原价总和 goodsOriginalAmount: number; // 商品原价总和 goodsDiscountAmount: number; // 商品折扣金额 @@ -253,8 +262,7 @@ export interface OrderCostSummary { couponUsed?: Coupon; // 实际使用的优惠券 pointUsed: number; // 实际使用的积分 newUserDiscount: number; // 新用户减免金额(元,默认0) - dinnerType?: 'dine-in' | 'take-out'; // 就餐类型(堂食/自取/配送/快递) - + dinnerType?: "dine-in" | "take-out"; // 就餐类型(堂食/自取/配送/快递) } // ============================ 2. 基础工具函数(核心修正:所有商品ID匹配用product_id) ============================ @@ -269,34 +277,48 @@ export interface OrderCostSummary { export function convertBackendCouponToToolCoupon( backendCoupon: BackendCoupon, currentStoreId: string, - dinnerType: 'dine-in' | 'take-out', + dinnerType: "dine-in" | "take-out", currentTime: Date = new Date() ): Coupon | null { // 1. 基础校验:必选字段缺失直接返回null if (!backendCoupon.id || backendCoupon.type === undefined) { - console.warn('优惠券必选字段缺失', backendCoupon); + console.warn("优惠券必选字段缺失", backendCoupon); return null; } // 2. 转换券类型:后端数字枚举 → 工具库字符串枚举 const couponType = mapBackendCouponTypeToTool(backendCoupon.type); if (!couponType) { - console.warn(`不支持的优惠券类型:${backendCoupon.type}(券ID:${backendCoupon.id})`); + console.warn( + `不支持的优惠券类型:${backendCoupon.type}(券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数组 + const applicableProductIds = + backendCoupon.foods === "" || !backendCoupon.foods + ? [] // 空字符串/undefined → 全部商品(按商品ID匹配) + : backendCoupon.foods.split(",").map((id) => id.trim()); // 逗号分隔 → 指定商品ID数组 - const useType = backendCoupon?.useType?.split(',')?.map(v => v.replace(/[\[\]]/g, '').replace(/""/g, '"').replace(/["']/g, '')) || []; + const useType = + backendCoupon?.useType?.split(",")?.map((v) => + v + .replace(/[\[\]]/g, "") + .replace(/""/g, '"') + .replace(/["']/g, "") + ) || []; // 4. 计算基础公共字段(含多维度可用性校验) const baseCoupon: BaseCoupon = { id: backendCoupon.id, type: couponType, - name: backendCoupon.name || '', - available: isCouponAvailable(backendCoupon, currentStoreId, dinnerType, currentTime), + name: backendCoupon.name || "", + available: isCouponAvailable( + backendCoupon, + currentStoreId, + dinnerType, + currentTime + ), useShops: getApplicableStoreIds(backendCoupon, currentStoreId), discountShare: backendCoupon.discountShare === 1, vipPriceShare: backendCoupon.vipPriceShare === 1, @@ -311,33 +333,42 @@ export function convertBackendCouponToToolCoupon( ...baseCoupon, fullAmount: backendCoupon.fullAmount || 0, discountAmount: backendCoupon.discountAmount || 0, - maxDiscountAmount: backendCoupon.maxDiscountAmount ?? Infinity + maxDiscountAmount: backendCoupon.maxDiscountAmount ?? Infinity, } as FullReductionCoupon; case CouponType.DISCOUNT: return { ...baseCoupon, discountRate: formatDiscountRate(backendCoupon.discountRate), - maxDiscountAmount: backendCoupon.maxDiscountAmount ?? Infinity + maxDiscountAmount: backendCoupon.maxDiscountAmount ?? Infinity, } as DiscountCoupon; case CouponType.SECOND_HALF: return { ...baseCoupon, - maxUseCountPerOrder: backendCoupon.useLimit === -10086 ? Infinity : backendCoupon.useLimit || 1 + 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 + 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' + sortRule: + backendCoupon.useRule === "price_asc" + ? "low_price_first" + : "high_price_first", } as ExchangeCoupon; default: @@ -349,13 +380,15 @@ export function convertBackendCouponToToolCoupon( /** * 后端优惠券类型(数字)→ 工具库优惠券类型(字符串枚举) */ -function mapBackendCouponTypeToTool(backendType: number): CouponType | undefined { +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-买一送一券 + 2: CouponType.EXCHANGE, // 2-商品兑换券 + 3: CouponType.DISCOUNT, // 3-折扣券 + 4: CouponType.SECOND_HALF, // 4-第二件半价券 + 6: CouponType.BUY_ONE_GET_ONE, // 6-买一送一券 }; return typeMap[backendType]; } @@ -366,7 +399,7 @@ function mapBackendCouponTypeToTool(backendType: number): CouponType | undefined function isCouponAvailable( backendCoupon: BackendCoupon, currentStoreId: string, - dinnerType: 'dine-in' | 'take-out', + dinnerType: "dine-in" | "take-out", currentTime: Date = new Date() ): boolean { // 1. 状态校验:必须启用(status=1) @@ -396,18 +429,22 @@ function isCouponAvailable( /** * 判断优惠券是否在有效期内(处理后端validType逻辑) */ -function isCouponInValidPeriod(backendCoupon: BackendCoupon, currentTime: Date): boolean { - const { validType, validStartTime, validEndTime, validDays, createTime } = backendCoupon; +function isCouponInValidPeriod( + backendCoupon: BackendCoupon, + currentTime: Date +): boolean { + const { validType, validStartTime, validEndTime, validDays, createTime } = + backendCoupon; // 固定时间有效期(validType=fixed):直接对比validStartTime和validEndTime - if (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) { + if (validType === "custom" && createTime && validDays) { const create = new Date(createTime); const end = new Date(create.getTime() + validDays * 24 * 60 * 60 * 1000); // 加N天 @@ -421,8 +458,12 @@ function isCouponInValidPeriod(backendCoupon: BackendCoupon, currentTime: Date): /** * 隔天生效校验:若设置了daysToTakeEffect,需超过生效时间(创建时间+N天的0点) */ -function isCouponEffectiveAfterDays(backendCoupon: BackendCoupon, currentTime: Date): boolean { - if (!backendCoupon.daysToTakeEffect || backendCoupon.daysToTakeEffect <= 0) return true; +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); @@ -436,17 +477,25 @@ function isCouponEffectiveAfterDays(backendCoupon: BackendCoupon, currentTime: D /** * 每日时段校验:当前时间需在useStartTime和useEndTime之间(仅比较时分秒,支持跨天) */ -function isCouponInDailyTimeRange(backendCoupon: BackendCoupon, currentTime: Date): boolean { +function isCouponInDailyTimeRange( + backendCoupon: BackendCoupon, + currentTime: Date +): boolean { // 全时段可用或未配置时段类型 → 直接通过 - if (backendCoupon.useTimeType === 'all' || !backendCoupon.useTimeType) return true; + if (backendCoupon.useTimeType === "all" || !backendCoupon.useTimeType) + return true; // 非自定义时段 → 默认可用(兼容未配置场景) - if (backendCoupon.useTimeType !== 'custom') 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 [startHours, startMinutes] = backendCoupon.useStartTime + .split(":") + .map(Number); + const [endHours, endMinutes] = backendCoupon.useEndTime + .split(":") + .map(Number); // 转换为当天分钟数(便于比较) const currentMinutes = currentTime.getHours() * 60 + currentTime.getMinutes(); @@ -455,44 +504,55 @@ function isCouponInDailyTimeRange(backendCoupon: BackendCoupon, currentTime: Dat // 处理跨天场景(如22:00-02:00) if (startTotalMinutes <= endTotalMinutes) { - return currentMinutes >= startTotalMinutes && currentMinutes <= endTotalMinutes; + return ( + currentMinutes >= startTotalMinutes && currentMinutes <= endTotalMinutes + ); } else { - return currentMinutes >= startTotalMinutes || currentMinutes <= endTotalMinutes; + return ( + currentMinutes >= startTotalMinutes || currentMinutes <= endTotalMinutes + ); } } /** * 每周周期校验:当前星期几需在useDays范围内(如"周一,周二") */ -function isCouponInWeekDays(backendCoupon: BackendCoupon, currentTime: Date): boolean { +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: '周六' + 0: "周七", + 1: "周一", + 2: "周二", + 3: "周三", + 4: "周四", + 5: "周五", + 6: "周六", }; - const currentWeekDay = weekDayMap[currentTime.getDay() as keyof typeof weekDayMap]; - return backendCoupon.useDays.split(',').includes(currentWeekDay); + const currentWeekDay = + weekDayMap[currentTime.getDay() as keyof typeof weekDayMap]; + return backendCoupon.useDays.split(",").includes(currentWeekDay); } /** * 门店匹配校验:根据useShopType判断当前门店是否适用 */ -function isStoreMatch(backendCoupon: BackendCoupon, currentStoreId: string): boolean { +function isStoreMatch( + backendCoupon: BackendCoupon, + currentStoreId: string +): boolean { const { useShopType, useShops, shopId } = backendCoupon; switch (useShopType) { - case 'all': // 所有门店适用 + case "all": // 所有门店适用 return true; - case 'custom': // 指定门店适用(useShops逗号分割,门店ID) - return useShops ? useShops.split(',').includes(currentStoreId) : false; - case 'only': // 仅本店适用(shopId为门店ID) + 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; @@ -502,23 +562,29 @@ function isStoreMatch(backendCoupon: BackendCoupon, currentStoreId: string): boo /** * 就餐类型匹配校验:当前就餐类型需在useType范围内(如"dine,pickup") */ -function isDinnerTypeMatch(backendCoupon: BackendCoupon, dinnerType: string): boolean { +function isDinnerTypeMatch( + backendCoupon: BackendCoupon, + dinnerType: string +): boolean { if (!backendCoupon.useType) return true; // 未配置 → 默认可用 - return backendCoupon.useType.split(',').includes(dinnerType); + return backendCoupon.useType.split(",").includes(dinnerType); } /** * 处理适用门店ID:根据useShopType返回对应数组(供BaseCoupon使用) */ -function getApplicableStoreIds(backendCoupon: BackendCoupon, currentStoreId: string): string[] { +function getApplicableStoreIds( + backendCoupon: BackendCoupon, + currentStoreId: string +): string[] { const { useShopType, useShops, shopId } = backendCoupon; switch (useShopType) { - case 'all': // 所有门店适用:返回空数组(工具库空数组表示无限制) + case "all": // 所有门店适用:返回空数组(工具库空数组表示无限制) return []; - case 'custom': // 指定门店适用:useShops逗号分割转数组(门店ID) - return useShops ? useShops.split(',').map(id => id.trim()) : []; - case 'only': // 仅当前店铺适用:返回shopId(转字符串,门店ID) + case "custom": // 指定门店适用:useShops逗号分割转数组(门店ID) + return useShops ? useShops.split(",").map((id) => id.trim()) : []; + case "only": // 仅当前店铺适用:返回shopId(转字符串,门店ID) return shopId ? [shopId.toString()] : []; default: // 未配置:默认仅当前门店适用 return [currentStoreId]; @@ -531,7 +597,9 @@ function getApplicableStoreIds(backendCoupon: BackendCoupon, currentStoreId: str 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; + return backendDiscountRate >= 1 + ? backendDiscountRate / 100 + : backendDiscountRate; } /** @@ -577,9 +645,8 @@ export function calcMemberPrice( if (!isMember) return truncateToTwoDecimals(goods.salePrice); // 优先级:SKU会员价 > 商品会员价 > 商品原价(无会员价时用会员折扣) - const basePrice = goods.skuData?.memberPrice - ?? goods.memberPrice - ?? goods.salePrice; + const basePrice = + goods.skuData?.memberPrice ?? goods.memberPrice ?? goods.salePrice; // 仅当无SKU会员价、无商品会员价时,才应用会员折扣率 if (memberDiscountRate && !goods.skuData?.memberPrice && !goods.memberPrice) { @@ -601,10 +668,11 @@ export function filterCouponEligibleGoods( goodsList: BaseCartItem[], excludedProductIds: string[] = [] ): BaseCartItem[] { - return goodsList.filter(goods => - !isTemporaryGoods(goods) - && !isGiftGoods(goods) - && !excludedProductIds.includes(String(goods.product_id)) // 核心修正:用商品ID排除 + return goodsList.filter( + (goods) => + !isTemporaryGoods(goods) && + !isGiftGoods(goods) && + !excludedProductIds.includes(String(goods.product_id)) // 核心修正:用商品ID排除 ); } @@ -621,7 +689,9 @@ export function filterThresholdGoods( // 空数组=全部基础合格商品;非空=仅商品ID匹配的商品(转字符串兼容类型) return applicableProductIds.length === 0 ? baseEligibleGoods - : baseEligibleGoods.filter(goods => applicableProductIds.includes(String(goods.product_id))); // 核心修正:用商品ID匹配 + : baseEligibleGoods.filter((goods) => + applicableProductIds.includes(String(goods.product_id)) + ); // 核心修正:用商品ID匹配 } /** @@ -633,7 +703,7 @@ export function filterThresholdGoods( */ export function sortGoodsForCoupon( goodsList: BaseCartItem[], - sortRule: 'low_price_first' | 'high_price_first', + sortRule: "low_price_first" | "high_price_first", cartOrder: Record = {} ): BaseCartItem[] { return [...goodsList].sort((a, b) => { @@ -641,7 +711,7 @@ export function sortGoodsForCoupon( 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; + return sortRule === "low_price_first" ? priceA - priceB : priceB - priceA; } // 2. 同价格按商品数量排序(降序,多的优先) @@ -667,7 +737,7 @@ export function sortGoodsForCoupon( export function calcCouponThresholdAmount( eligibleGoods: BaseCartItem[], coupon: BaseCoupon, - config: Pick, + config: Pick, activities: ActivityConfig[] = [] ): number { let total = new BigNumber(0); @@ -677,21 +747,27 @@ export function calcCouponThresholdAmount( if (availableNum <= 0) continue; // 1. 基础金额:默认用商品原价(SKU原价优先) - const basePrice = new BigNumber(goods.skuData?.salePrice ?? goods.salePrice); + 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)); + 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匹配活动 + const activity = + goods.activityInfo ?? + activities.find( + (act) => + act.type === ActivityType.TIME_LIMIT_DISCOUNT && + (act.applicableProductIds || []).includes(String(goods.product_id)) // 核心修正:用商品ID匹配活动 ); if (activity) { @@ -714,19 +790,26 @@ export function calcCouponThresholdAmount( */ export function calcSingleGoodsRealPrice( goods: BaseCartItem, - config: Pick & { + config: Pick & { 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)); + const memberPrice = new BigNumber( + calcMemberPrice(goods, isMember, memberDiscountRate) + ); // 3. 优先级3:营销活动折扣(如限时折扣,需按商品ID匹配活动) const isActivityApplicable = activity @@ -744,11 +827,17 @@ export function calcSingleGoodsRealPrice( ); } else { // 不同享:取会员价和活动价的最小值(活动价用SKU原价计算) - const basePriceForActivity = new BigNumber(goods.skuData?.salePrice ?? goods.salePrice); - const activityPrice = basePriceForActivity.multipliedBy(activity.discountRate); + const basePriceForActivity = new BigNumber( + goods.skuData?.salePrice ?? goods.salePrice + ); + const activityPrice = basePriceForActivity.multipliedBy( + activity.discountRate + ); return truncateToTwoDecimals( - memberPrice.isLessThanOrEqualTo(activityPrice) ? memberPrice.toNumber() : activityPrice.toNumber() + memberPrice.isLessThanOrEqualTo(activityPrice) + ? memberPrice.toNumber() + : activityPrice.toNumber() ); } } @@ -763,7 +852,18 @@ export function calcGoodsOriginalAmount(goodsList: BaseCartItem[]): number { 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原价优先 + 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)); } @@ -779,7 +879,7 @@ export function calcGoodsOriginalAmount(goodsList: BaseCartItem[]): number { */ export function calcGoodsRealAmount( goodsList: BaseCartItem[], - config: Pick, + config: Pick, activities: ActivityConfig[] = [] ): number { let total = new BigNumber(0); @@ -789,12 +889,16 @@ export function calcGoodsRealAmount( if (availableNum <= 0) continue; // 匹配商品参与的营销活动(按商品ID匹配,优先商品自身配置) - const activity = goods.activityInfo - ?? activities.find(act => - (act.applicableProductIds || []).includes(String(goods.product_id)) // 核心修正:用商品ID匹配活动 + const activity = + goods.activityInfo ?? + activities.find( + (act) => + (act.applicableProductIds || []).includes(String(goods.product_id)) // 核心修正:用商品ID匹配活动 ); - const realPrice = new BigNumber(calcSingleGoodsRealPrice(goods, { ...config, activity })); + const realPrice = new BigNumber( + calcSingleGoodsRealPrice(goods, { ...config, activity }) + ); total = total.plus(realPrice.multipliedBy(availableNum)); } @@ -824,7 +928,10 @@ interface CouponCalculationStrategy { calculate( coupon: Coupon, goodsList: BaseCartItem[], - config: Pick & { + config: Pick< + OrderExtraConfig, + "currentStoreId" | "isMember" | "memberDiscountRate" + > & { activities: ActivityConfig[]; cartOrder: Record; excludedProductIds?: string[]; // 需排除的商品ID列表(商品ID) @@ -833,7 +940,7 @@ interface CouponCalculationStrategy { deductionAmount: number; excludedProductIds: string[]; // 排除的商品ID列表(商品ID) productCouponDeduction?: number; // 新增:当前券属于商品券时的抵扣金额 - fullCouponDeduction?: number; // 新增:当前券属于满减券时的抵扣金额 + fullCouponDeduction?: number; // 新增:当前券属于满减券时的抵扣金额 }; } @@ -843,33 +950,62 @@ class FullReductionStrategy implements CouponCalculationStrategy { coupon: FullReductionCoupon, goodsList: BaseCartItem[], config: any - ): { deductionAmount: number; excludedProductIds: string[]; fullCouponDeduction: number } { - + ): { + deductionAmount: number; + excludedProductIds: string[]; + fullCouponDeduction: number; + } { // 1. 基础校验:优惠券不可用/门店不匹配 → 抵扣0 - if (!coupon.available || !isStoreMatchByList(coupon.useShops, config.currentStoreId)) { - return { deductionAmount: 0, excludedProductIds: [], fullCouponDeduction: 0 }; + if ( + !coupon.available || + !isStoreMatchByList(coupon.useShops, config.currentStoreId) + ) { + return { + deductionAmount: 0, + excludedProductIds: [], + fullCouponDeduction: 0, + }; } // 2. 第一步:排除临时菜/赠菜/已抵扣商品(基础合格商品,按商品ID排除) - const baseEligibleGoods = filterCouponEligibleGoods(goodsList, config.excludedProductIds || []); + const baseEligibleGoods = filterCouponEligibleGoods( + goodsList, + config.excludedProductIds || [] + ); // 3. 第二步:按商品ID筛选门槛商品(匹配applicableProductIds) - const thresholdGoods = filterThresholdGoods(baseEligibleGoods, coupon.applicableProductIds); + const thresholdGoods = filterThresholdGoods( + baseEligibleGoods, + coupon.applicableProductIds + ); if (thresholdGoods.length === 0) { - return { deductionAmount: 0, excludedProductIds: [], fullCouponDeduction: 0 }; + return { + deductionAmount: 0, + excludedProductIds: [], + fullCouponDeduction: 0, + }; } // 4. 按同享规则计算门槛金额(按商品ID匹配活动) - const thresholdAmount = new BigNumber(calcCouponThresholdAmount( - thresholdGoods, - coupon, - { isMember: config.isMember, memberDiscountRate: config.memberDiscountRate }, - config.activities - )); + const thresholdAmount = new BigNumber( + calcCouponThresholdAmount( + thresholdGoods, + coupon, + { + isMember: config.isMember, + memberDiscountRate: config.memberDiscountRate, + }, + config.activities + ) + ); // 5. 满门槛则抵扣,否则0(不超过最大减免) if (thresholdAmount.isLessThan(coupon.fullAmount)) { - return { deductionAmount: 0, excludedProductIds: [], fullCouponDeduction: 0 }; + return { + deductionAmount: 0, + excludedProductIds: [], + fullCouponDeduction: 0, + }; } const maxReduction = coupon.maxDiscountAmount ?? coupon.discountAmount; @@ -883,7 +1019,7 @@ class FullReductionStrategy implements CouponCalculationStrategy { return { deductionAmount, excludedProductIds: [], - fullCouponDeduction: deductionAmount + fullCouponDeduction: deductionAmount, }; } } @@ -894,30 +1030,58 @@ class DiscountStrategy implements CouponCalculationStrategy { coupon: DiscountCoupon, goodsList: BaseCartItem[], config: any - ): { deductionAmount: number; excludedProductIds: string[]; productCouponDeduction: number } { + ): { + deductionAmount: number; + excludedProductIds: string[]; + productCouponDeduction: number; + } { // 1. 基础校验:优惠券不可用/门店不匹配 → 抵扣0 - if (!coupon.available || !isStoreMatchByList(coupon.useShops, config.currentStoreId)) { - return { deductionAmount: 0, excludedProductIds: [], productCouponDeduction: 0 }; + if ( + !coupon.available || + !isStoreMatchByList(coupon.useShops, config.currentStoreId) + ) { + return { + deductionAmount: 0, + excludedProductIds: [], + productCouponDeduction: 0, + }; } // 2. 第一步:排除临时菜/赠菜/已抵扣商品(基础合格商品,按商品ID排除) - const baseEligibleGoods = filterCouponEligibleGoods(goodsList, config.excludedProductIds || []); + const baseEligibleGoods = filterCouponEligibleGoods( + goodsList, + config.excludedProductIds || [] + ); // 3. 第二步:按商品ID筛选门槛商品(匹配applicableProductIds) - const thresholdGoods = filterThresholdGoods(baseEligibleGoods, coupon.applicableProductIds); + const thresholdGoods = filterThresholdGoods( + baseEligibleGoods, + coupon.applicableProductIds + ); if (thresholdGoods.length === 0) { - return { deductionAmount: 0, excludedProductIds: [], productCouponDeduction: 0 }; + return { + deductionAmount: 0, + excludedProductIds: [], + productCouponDeduction: 0, + }; } // 4. 按同享规则计算折扣基数(按商品ID匹配活动) - const discountBaseAmount = new BigNumber(calcCouponThresholdAmount( - thresholdGoods, - coupon, - { isMember: config.isMember, memberDiscountRate: config.memberDiscountRate }, - config.activities - )); + const discountBaseAmount = new BigNumber( + calcCouponThresholdAmount( + thresholdGoods, + coupon, + { + isMember: config.isMember, + memberDiscountRate: config.memberDiscountRate, + }, + config.activities + ) + ); // 5. 计算折扣金额(不超过最大减免) - const discountAmount = discountBaseAmount.multipliedBy(new BigNumber(1).minus(coupon.discountRate)); + const discountAmount = discountBaseAmount.multipliedBy( + new BigNumber(1).minus(coupon.discountRate) + ); const deductionAmount = truncateToTwoDecimals( discountAmount.isLessThan(coupon.maxDiscountAmount) ? discountAmount.toNumber() @@ -928,7 +1092,7 @@ class DiscountStrategy implements CouponCalculationStrategy { return { deductionAmount, excludedProductIds: [], - productCouponDeduction: deductionAmount + productCouponDeduction: deductionAmount, }; } } @@ -939,21 +1103,42 @@ class SecondHalfPriceStrategy implements CouponCalculationStrategy { coupon: SecondHalfPriceCoupon, goodsList: BaseCartItem[], config: any - ): { deductionAmount: number; excludedProductIds: string[]; productCouponDeduction: number } { + ): { + deductionAmount: number; + excludedProductIds: string[]; + productCouponDeduction: number; + } { // 1. 基础校验:优惠券不可用/门店不匹配 → 抵扣0 - if (!coupon.available || !isStoreMatchByList(coupon.useShops, config.currentStoreId)) { - return { deductionAmount: 0, excludedProductIds: [], productCouponDeduction: 0 }; + if ( + !coupon.available || + !isStoreMatchByList(coupon.useShops, config.currentStoreId) + ) { + return { + deductionAmount: 0, + excludedProductIds: [], + productCouponDeduction: 0, + }; } let totalDeduction = new BigNumber(0); const excludedProductIds: string[] = []; // 存储排除的商品ID(商品ID) // 2. 第一步:排除临时菜/赠菜/已抵扣商品(基础合格商品,按商品ID排除) - const baseEligibleGoods = filterCouponEligibleGoods(goodsList, config.excludedProductIds || []); + const baseEligibleGoods = filterCouponEligibleGoods( + goodsList, + config.excludedProductIds || [] + ); // 3. 第二步:按商品ID筛选门槛商品(匹配applicableProductIds) - const thresholdGoods = filterThresholdGoods(baseEligibleGoods, coupon.applicableProductIds); + const thresholdGoods = filterThresholdGoods( + baseEligibleGoods, + coupon.applicableProductIds + ); if (thresholdGoods.length === 0) { - return { deductionAmount: 0, excludedProductIds: [], productCouponDeduction: 0 }; + return { + deductionAmount: 0, + excludedProductIds: [], + productCouponDeduction: 0, + }; } // 按商品ID分组(避免同商品多次处理,用商品ID作为分组key) @@ -965,11 +1150,16 @@ class SecondHalfPriceStrategy implements CouponCalculationStrategy { }, {} as Record); // 遍历每组商品计算半价优惠 - for (const [productIdStr, productGoods] of Object.entries(goodsGroupByProductId)) { + 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); + const totalNum = productGoods.reduce( + (sum, g) => sum + (g.number - (g.returnNum || 0)), + 0 + ); if (totalNum < 2) continue; // 至少2件才享受优惠 // 计算优惠次数(每2件1次,不超过最大使用次数) @@ -981,17 +1171,22 @@ class SecondHalfPriceStrategy implements CouponCalculationStrategy { // 计算单件实际价格(取组内任意一个商品的配置,同商品规格一致) const sampleGood = productGoods[0]; - const activity = config.activities.find((act: ActivityConfig) => - (act.applicableProductIds || []).includes(productIdStr) // 按商品ID匹配活动 + const activity = config.activities.find( + (act: ActivityConfig) => + (act.applicableProductIds || []).includes(productIdStr) // 按商品ID匹配活动 + ); + const realPrice = new BigNumber( + calcSingleGoodsRealPrice(sampleGood, { + isMember: config.isMember, + memberDiscountRate: config.memberDiscountRate, + activity, + }) ); - const realPrice = new BigNumber(calcSingleGoodsRealPrice(sampleGood, { - isMember: config.isMember, - memberDiscountRate: config.memberDiscountRate, - activity - })); // 累计抵扣金额并标记已优惠商品(记录商品ID) - totalDeduction = totalDeduction.plus(realPrice.multipliedBy(0.5).multipliedBy(discountCount)); + totalDeduction = totalDeduction.plus( + realPrice.multipliedBy(0.5).multipliedBy(discountCount) + ); excludedProductIds.push(productIdStr); } @@ -1000,7 +1195,7 @@ class SecondHalfPriceStrategy implements CouponCalculationStrategy { return { deductionAmount, excludedProductIds, - productCouponDeduction: deductionAmount + productCouponDeduction: deductionAmount, }; } } @@ -1011,21 +1206,42 @@ class BuyOneGetOneStrategy implements CouponCalculationStrategy { coupon: BuyOneGetOneCoupon, goodsList: BaseCartItem[], config: any - ): { deductionAmount: number; excludedProductIds: string[]; productCouponDeduction: number } { + ): { + deductionAmount: number; + excludedProductIds: string[]; + productCouponDeduction: number; + } { // 1. 基础校验:优惠券不可用/门店不匹配 → 抵扣0 - if (!coupon.available || !isStoreMatchByList(coupon.useShops, config.currentStoreId)) { - return { deductionAmount: 0, excludedProductIds: [], productCouponDeduction: 0 }; + if ( + !coupon.available || + !isStoreMatchByList(coupon.useShops, config.currentStoreId) + ) { + return { + deductionAmount: 0, + excludedProductIds: [], + productCouponDeduction: 0, + }; } let totalDeduction = new BigNumber(0); const excludedProductIds: string[] = []; // 存储排除的商品ID(商品ID) // 2. 第一步:排除临时菜/赠菜/已抵扣商品(基础合格商品,按商品ID排除) - const baseEligibleGoods = filterCouponEligibleGoods(goodsList, config.excludedProductIds || []); + const baseEligibleGoods = filterCouponEligibleGoods( + goodsList, + config.excludedProductIds || [] + ); // 3. 第二步:按商品ID筛选门槛商品(匹配applicableProductIds) - const thresholdGoods = filterThresholdGoods(baseEligibleGoods, coupon.applicableProductIds); + const thresholdGoods = filterThresholdGoods( + baseEligibleGoods, + coupon.applicableProductIds + ); if (thresholdGoods.length === 0) { - return { deductionAmount: 0, excludedProductIds: [], productCouponDeduction: 0 }; + return { + deductionAmount: 0, + excludedProductIds: [], + productCouponDeduction: 0, + }; } // 按商品ID分组(用商品ID作为分组key) @@ -1037,11 +1253,16 @@ class BuyOneGetOneStrategy implements CouponCalculationStrategy { }, {} as Record); // 遍历每组商品计算买一送一优惠 - for (const [productIdStr, productGoods] of Object.entries(goodsGroupByProductId)) { + 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); + const totalNum = productGoods.reduce( + (sum, g) => sum + (g.number - (g.returnNum || 0)), + 0 + ); if (totalNum < 2) continue; // 至少2件才享受优惠 // 计算优惠次数(每2件送1件) @@ -1050,17 +1271,22 @@ class BuyOneGetOneStrategy implements CouponCalculationStrategy { // 计算单件实际价格(按商品ID匹配活动) const sampleGood = productGoods[0]; - const activity = config.activities.find((act: ActivityConfig) => - (act.applicableProductIds || []).includes(productIdStr) // 按商品ID匹配活动 + const activity = config.activities.find( + (act: ActivityConfig) => + (act.applicableProductIds || []).includes(productIdStr) // 按商品ID匹配活动 + ); + const realPrice = new BigNumber( + calcSingleGoodsRealPrice(sampleGood, { + isMember: config.isMember, + memberDiscountRate: config.memberDiscountRate, + activity, + }) ); - const realPrice = new BigNumber(calcSingleGoodsRealPrice(sampleGood, { - isMember: config.isMember, - memberDiscountRate: config.memberDiscountRate, - activity - })); // 累计抵扣金额(送1件=减免1件价格)并标记商品ID - totalDeduction = totalDeduction.plus(realPrice.multipliedBy(1).multipliedBy(discountCount)); + totalDeduction = totalDeduction.plus( + realPrice.multipliedBy(1).multipliedBy(discountCount) + ); excludedProductIds.push(productIdStr); } @@ -1069,7 +1295,7 @@ class BuyOneGetOneStrategy implements CouponCalculationStrategy { return { deductionAmount, excludedProductIds, - productCouponDeduction: deductionAmount + productCouponDeduction: deductionAmount, }; } } @@ -1080,22 +1306,47 @@ class ExchangeCouponStrategy implements CouponCalculationStrategy { coupon: ExchangeCoupon, goodsList: BaseCartItem[], config: any - ): { deductionAmount: number; excludedProductIds: string[]; productCouponDeduction: number } { + ): { + deductionAmount: number; + excludedProductIds: string[]; + productCouponDeduction: number; + } { // 1. 基础校验:优惠券不可用/门店不匹配 → 抵扣0 - if (!coupon.available || !isStoreMatchByList(coupon.useShops, config.currentStoreId)) { - return { deductionAmount: 0, excludedProductIds: [], productCouponDeduction: 0 }; + if ( + !coupon.available || + !isStoreMatchByList(coupon.useShops, config.currentStoreId) + ) { + return { + deductionAmount: 0, + excludedProductIds: [], + productCouponDeduction: 0, + }; } // 2. 第一步:排除临时菜/赠菜/已抵扣商品(基础合格商品,按商品ID排除) - const baseEligibleGoods = filterCouponEligibleGoods(goodsList, config.excludedProductIds || []); + const baseEligibleGoods = filterCouponEligibleGoods( + goodsList, + config.excludedProductIds || [] + ); // 3. 第二步:按商品ID筛选门槛商品(匹配applicableProductIds) - const thresholdGoods = filterThresholdGoods(baseEligibleGoods, coupon.applicableProductIds); + const thresholdGoods = filterThresholdGoods( + baseEligibleGoods, + coupon.applicableProductIds + ); if (thresholdGoods.length === 0) { - return { deductionAmount: 0, excludedProductIds: [], productCouponDeduction: 0 }; + return { + deductionAmount: 0, + excludedProductIds: [], + productCouponDeduction: 0, + }; } // 按规则排序商品(按价格/数量/加入顺序) - const sortedGoods = sortGoodsForCoupon(thresholdGoods, coupon.sortRule, config.cartOrder); + const sortedGoods = sortGoodsForCoupon( + thresholdGoods, + coupon.sortRule, + config.cartOrder + ); let remainingCount = coupon.deductCount; let totalDeduction = new BigNumber(0); const excludedProductIds: string[] = []; // 存储排除的商品ID(商品ID) @@ -1112,14 +1363,17 @@ class ExchangeCouponStrategy implements CouponCalculationStrategy { // 计算当前商品可抵扣的件数(不超过剩余可抵扣数量) const deductCount = Math.min(availableNum, remainingCount); - const activity = config.activities.find((act: ActivityConfig) => - (act.applicableProductIds || []).includes(productIdStr) // 按商品ID匹配活动 + const activity = config.activities.find( + (act: ActivityConfig) => + (act.applicableProductIds || []).includes(productIdStr) // 按商品ID匹配活动 + ); + const realPrice = new BigNumber( + calcSingleGoodsRealPrice(goods, { + isMember: config.isMember, + memberDiscountRate: config.memberDiscountRate, + activity, + }) ); - const realPrice = new BigNumber(calcSingleGoodsRealPrice(goods, { - isMember: config.isMember, - memberDiscountRate: config.memberDiscountRate, - activity - })); // 累计抵扣金额并标记商品 totalDeduction = totalDeduction.plus(realPrice.multipliedBy(deductCount)); @@ -1133,7 +1387,7 @@ class ExchangeCouponStrategy implements CouponCalculationStrategy { return { deductionAmount, excludedProductIds, - productCouponDeduction: deductionAmount + productCouponDeduction: deductionAmount, }; } } @@ -1142,7 +1396,10 @@ class ExchangeCouponStrategy implements CouponCalculationStrategy { /** * 根据优惠券useShops列表判断门店是否匹配(适配BaseCoupon的useShops字段) */ -function isStoreMatchByList(useShops: string[], currentStoreId: string): boolean { +function isStoreMatchByList( + useShops: string[], + currentStoreId: string +): boolean { // 适用门店为空数组 → 无限制(所有门店适用) if (useShops.length === 0) return true; // 匹配当前门店ID(字符串比较,避免类型问题) @@ -1169,7 +1426,6 @@ function getCouponStrategy(couponType: CouponType): CouponCalculationStrategy { } } - /** * 计算优惠券抵扣金额(处理互斥逻辑,选择最优优惠券,按商品ID排除,新增细分统计) * @param backendCoupons 后端优惠券列表 @@ -1180,43 +1436,45 @@ function getCouponStrategy(couponType: CouponType): CouponCalculationStrategy { export function calcCouponDeduction( backendCoupons: BackendCoupon[], goodsList: BaseCartItem[], - config: Pick & { + config: Pick< + OrderExtraConfig, + "currentStoreId" | "isMember" | "memberDiscountRate" + > & { activities: ActivityConfig[]; cartOrder: Record; - dinnerType: 'dine-in' | 'take-out'; + dinnerType: "dine-in" | "take-out"; currentTime?: Date; } ): { deductionAmount: number; productCouponDeduction: number; // 新增:商品优惠券抵扣(兑换券/折扣券/买一送一等) - fullCouponDeduction: 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) + 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) + return prve + (cur.discountAmount || 0); }, 0), excludedProductIds: [], usedCoupon: undefined, productCouponDeduction: 0, - fullCouponDeduction: 0 + fullCouponDeduction: 0, }; // 4. 计算兑换券抵扣(排除非兑换券已抵扣的商品ID,统计商品券细分) let exchangeResult: ExchangeCalculationResult = { deductionAmount: goodsCoupon.reduce((prve, cur): number => { - return prve + (cur.discountAmount || 0) + return prve + (cur.discountAmount || 0); }, 0), excludedProductIds: [], - productCouponDeduction: 0 + productCouponDeduction: 0, }; - // 5. 汇总结果:兑换券与非兑换券不可同时使用,取抵扣金额大的 const exchangeBn = new BigNumber(exchangeResult.deductionAmount); const nonExchangeBn = new BigNumber(nonExchangeResult.deductionAmount); @@ -1227,7 +1485,9 @@ export function calcCouponDeduction( productCouponDeduction: exchangeResult.deductionAmount, fullCouponDeduction: nonExchangeResult.deductionAmount, // 兑换券与满减券互斥,满减券抵扣置0 usedCoupon: isExchangeBetter ? undefined : nonExchangeResult.usedCoupon, - excludedProductIds: isExchangeBetter ? exchangeResult.excludedProductIds : nonExchangeResult.excludedProductIds + excludedProductIds: isExchangeBetter + ? exchangeResult.excludedProductIds + : nonExchangeResult.excludedProductIds, }; } @@ -1240,9 +1500,9 @@ export function calcCouponDeduction( */ export function calcTotalPackFee( goodsList: BaseCartItem[], - dinnerType: 'dine-in' | 'take-out' + dinnerType: "dine-in" | "take-out" ): number { - if (dinnerType !== 'take-out') return 0; + if (dinnerType !== "take-out") return 0; let total = new BigNumber(0); for (const goods of goodsList) { @@ -1250,9 +1510,7 @@ export function calcTotalPackFee( if (availableNum === 0) continue; // 计算单个商品打包数量(外卖全打包,堂食按配置,称重商品≤1) - let packNum = dinnerType === 'take-out' - ? availableNum - : (0); + let packNum = dinnerType === "take-out" ? availableNum : 0; if (goods.product_type === GoodsType.WEIGHT) { packNum = Math.min(packNum, 1); } @@ -1301,11 +1559,11 @@ export function calcPointDeduction( // 最大可抵扣金额(积分可抵金额 vs 规则最大 vs 订单上限) const maxDeductByPoints = userPointsBn.dividedBy(pointsPerYuanBn); - const maxDeductAmount = maxDeductByPoints - .isLessThan(rule.maxDeductionAmount ?? Infinity) + const maxDeductAmount = maxDeductByPoints.isLessThan( + rule.maxDeductionAmount ?? Infinity + ) ? maxDeductByPoints - : new BigNumber(rule.maxDeductionAmount || Infinity) - .isLessThan(maxLimitBn) + : new BigNumber(rule.maxDeductionAmount || Infinity).isLessThan(maxLimitBn) ? maxDeductByPoints : maxLimitBn; @@ -1314,7 +1572,9 @@ export function calcPointDeduction( return { deductionAmount: truncateToTwoDecimals(maxDeductAmount.toNumber()), - usedPoints: truncateToTwoDecimals(Math.min(usedPoints.toNumber(), userPoints)) // 避免积分超扣 + usedPoints: truncateToTwoDecimals( + Math.min(usedPoints.toNumber(), userPoints) + ), // 避免积分超扣 }; } @@ -1332,20 +1592,32 @@ export function calcPointDeduction( */ export function calculateOrderCostSummary( goodsList: BaseCartItem[], - dinnerType: 'dine-in' | 'take-out', + dinnerType: "dine-in" | "take-out", backendCoupons: BackendCoupon[] = [], activities: ActivityConfig[] = [], config: OrderExtraConfig, cartOrder: Record = {}, currentTime: Date = new Date() ): OrderCostSummary { + console.log("goodsList", goodsList); // 1. 基础费用计算(原有逻辑不变) const goodsOriginalAmount = calcGoodsOriginalAmount(goodsList); - const goodsRealAmount = calcGoodsRealAmount(goodsList, { - isMember: config.isMember, - memberDiscountRate: config.memberDiscountRate - }, activities); - const goodsDiscountAmount = calcGoodsDiscountAmount(goodsOriginalAmount, goodsRealAmount); + // 商品总件数 + const goodsTotal = goodsList.reduce((total, goods) => { + return total + Math.max(0, goods.number - (goods.returnNum || 0)); + }, 0); + const goodsRealAmount = calcGoodsRealAmount( + goodsList, + { + isMember: config.isMember, + memberDiscountRate: config.memberDiscountRate, + }, + activities + ); + const goodsDiscountAmount = calcGoodsDiscountAmount( + goodsOriginalAmount, + goodsRealAmount + ); // 2. 优惠券抵扣(原有逻辑不变) const { @@ -1353,39 +1625,36 @@ export function calculateOrderCostSummary( usedCoupon, excludedProductIds, productCouponDeduction, - fullCouponDeduction - } = calcCouponDeduction( - backendCoupons, - goodsList, - { - currentStoreId: config.currentStoreId, - isMember: config.isMember, - memberDiscountRate: config.memberDiscountRate, - activities, - cartOrder, - dinnerType, - currentTime - } - ); + fullCouponDeduction, + } = calcCouponDeduction(backendCoupons, goodsList, { + currentStoreId: config.currentStoreId, + isMember: config.isMember, + memberDiscountRate: config.memberDiscountRate, + activities, + cartOrder, + dinnerType, + currentTime, + }); // 3. 其他费用计算(原有逻辑不变) // 新客立减 const newUserDiscount = config.newUserDiscount || 0; const packFee = calcTotalPackFee(goodsList, dinnerType); let seatFee = calcSeatFee(config.seatFeeConfig); - seatFee = dinnerType === 'dine-in' ? seatFee : 0; // 外卖不收餐位费 + seatFee = dinnerType === "dine-in" ? seatFee : 0; // 外卖不收餐位费 const additionalFee = Math.max(0, config.additionalFee); // 4. 积分抵扣(原有逻辑不变,先于商家减免计算) - const maxPointDeductionLimit = Math.max(0, goodsRealAmount - couponDeductionAmount); - const { - deductionAmount: pointDeductionAmount, - usedPoints - } = calcPointDeduction( - config.userPoints, - config.pointDeductionRule, - maxPointDeductionLimit + const maxPointDeductionLimit = Math.max( + 0, + goodsRealAmount - couponDeductionAmount ); + const { deductionAmount: pointDeductionAmount, usedPoints } = + calcPointDeduction( + config.userPoints, + config.pointDeductionRule, + maxPointDeductionLimit + ); // ============================ 新增:商家减免计算(支持两种形式) ============================ // 商家减免计算时机:在商品折扣、优惠券、积分抵扣之后(避免过度减免) @@ -1405,17 +1674,26 @@ export function calculateOrderCostSummary( switch (merchantReductionConfig.type) { case MerchantReductionType.FIXED_AMOUNT: // 固定金额减免:取配置金额与上限的最小值,且不小于0 - const fixedAmount = new BigNumber(merchantReductionConfig.fixedAmount || 0); - merchantReductionActualAmount = fixedAmount - .isLessThanOrEqualTo(maxMerchantReductionLimit) + const fixedAmount = new BigNumber( + merchantReductionConfig.fixedAmount || 0 + ); + merchantReductionActualAmount = fixedAmount.isLessThanOrEqualTo( + maxMerchantReductionLimit + ) ? fixedAmount.toNumber() : maxMerchantReductionLimit.toNumber(); - merchantReductionActualAmount = Math.max(0, merchantReductionActualAmount); + merchantReductionActualAmount = Math.max( + 0, + merchantReductionActualAmount + ); break; case MerchantReductionType.DISCOUNT_RATE: // 比例折扣减免:先校验折扣率(0-100%),再按比例计算减免金额 - const validDiscountRate = Math.max(0, Math.min(100, merchantReductionConfig.discountRate || 0)); + const validDiscountRate = Math.max( + 0, + Math.min(100, merchantReductionConfig.discountRate || 0) + ); // 折扣率转小数(如 90% → 0.9),减免金额 = 可抵扣上限 * (1 - 折扣率) merchantReductionActualAmount = maxMerchantReductionLimit .multipliedBy(new BigNumber(1).minus(validDiscountRate / 100)) @@ -1429,20 +1707,22 @@ export function calculateOrderCostSummary( } // 5. 最终实付金额计算(整合所有费用) - const finalPayAmount = new BigNumber(goodsOriginalAmount) // 商品原价总和 - .minus(goodsDiscountAmount) // 减去商品折扣 - .minus(couponDeductionAmount) // 减去优惠券抵扣 - .minus(newUserDiscount) // 新客立减 - .minus(pointDeductionAmount) // 减去积分抵扣 + const finalPayAmount = new BigNumber(goodsOriginalAmount) // 商品原价总和 + .minus(goodsDiscountAmount) // 减去商品折扣 + .minus(couponDeductionAmount) // 减去优惠券抵扣 + .minus(newUserDiscount) // 新客立减 + .minus(pointDeductionAmount) // 减去积分抵扣 .minus(merchantReductionActualAmount) // 减去商家实际减免金额 - .plus(seatFee) // 加上餐位费(不参与减免) - .plus(packFee) // 加上打包费(不参与减免) - .plus(additionalFee); // 加上附加费 + .plus(seatFee) // 加上餐位费(不参与减免) + .plus(packFee) // 加上打包费(不参与减免) + .plus(additionalFee); // 加上附加费 const finalPayAmountNonNegative = Math.max(0, finalPayAmount.toNumber()); // 6. 返回完整费用汇总(包含商家减免明细) return { + // 商品总件数 + goodsTotal, goodsRealAmount, goodsOriginalAmount, goodsDiscountAmount, @@ -1452,18 +1732,19 @@ export function calculateOrderCostSummary( pointDeductionAmount, seatFee, packFee, + // 商家减免明细(含类型、原始配置、实际金额) merchantReduction: { type: merchantReductionConfig.type, originalConfig: merchantReductionConfig, - actualAmount: truncateToTwoDecimals(merchantReductionActualAmount) + actualAmount: truncateToTwoDecimals(merchantReductionActualAmount), }, additionalFee, finalPayAmount: truncateToTwoDecimals(finalPayAmountNonNegative), couponUsed: usedCoupon, pointUsed: usedPoints, newUserDiscount, - dinnerType + dinnerType, }; } @@ -1501,7 +1782,7 @@ export const OrderPriceCalculator = { GoodsType, CouponType, ActivityType, - } + }, }; export default OrderPriceCalculator; diff --git a/src/views/tool/Instead/components/carts/item.vue b/src/views/tool/Instead/components/carts/item.vue index e046dbc..3b76247 100644 --- a/src/views/tool/Instead/components/carts/item.vue +++ b/src/views/tool/Instead/components/carts/item.vue @@ -212,6 +212,7 @@ const vipAllPrice = computed(() => { return n; }); const allPrice = computed(() => { + console.log("allPrice", props.item); if (props.item.discount_sale_amount * 1 > 0) { return props.item.discount_sale_amount * props.item.number; } diff --git a/src/views/tool/Instead/components/carts/list.vue b/src/views/tool/Instead/components/carts/list.vue index c79cb02..2b9f02b 100644 --- a/src/views/tool/Instead/components/carts/list.vue +++ b/src/views/tool/Instead/components/carts/list.vue @@ -144,7 +144,7 @@ 去结账 diff --git a/src/views/tool/Instead/components/order.vue b/src/views/tool/Instead/components/order.vue index b72370e..c14a7b9 100644 --- a/src/views/tool/Instead/components/order.vue +++ b/src/views/tool/Instead/components/order.vue @@ -93,19 +93,8 @@ @@ -408,12 +397,13 @@ watch( //002-获取订单可用积分及抵扣金额(支付页面使用) const pointsRes = ref({ usable: true, maxUsablePoints: 0, minDeductionPoints: 0 }); const usePointsNumber = ref(0); -const orderAmountOrderAmount = computed(() => { - return (carts.goodsTotal - checkOrderPay.discountAmount).toFixed(2); +//积分可抵扣最大金额 +const scoreMaxMoney = computed(() => { + return carts.orderCostSummary.finalPayAmount - carts.orderCostSummary.pointDeductionAmount; }); const pointsDiscountAmount = ref(0); watch( - () => orderAmountOrderAmount.value, + () => scoreMaxMoney.value, (newval) => { pointsInit(); } @@ -426,7 +416,7 @@ function returnPointsDiscountAmount() { } async function pointsInit() { - if (!props.user.id || score.sel == -1) { + if (!props.user.id) { return; } const res = await PointsApi.calcOrderUsablePoints({ @@ -435,6 +425,9 @@ async function pointsInit() { }); pointsRes.value = res; carts.pointDeductionRule.pointsPerYuan = res.equivalentPoints; + if (score.sel == -1) { + return; + } usePointsNumber.value = res.usable ? res.maxUsablePoints : 0; if (res.usable) { diff --git a/src/views/tool/Instead/components/popup-coupon.vue b/src/views/tool/Instead/components/popup-coupon.vue index 9fef9d7..a5f462b 100644 --- a/src/views/tool/Instead/components/popup-coupon.vue +++ b/src/views/tool/Instead/components/popup-coupon.vue @@ -360,6 +360,7 @@ async function getcoup() { function couponClick(checked, row) { couponSel.value = checked ? row : { id: "" }; + updateSelCoupon(); } const AllCouponPrice = computed(() => { return quansSelArr.value.reduce((pre, cur) => pre + cur.discountAmount, 0); @@ -370,6 +371,7 @@ const payPrice = computed(() => { }); function productCouponClick(checked, item) { goodsCouponSel.value = checked ? item : { id: "" }; + updateSelCoupon(); } //删除选中的优惠券 @@ -403,46 +405,51 @@ watch( getcoup(); } ); +function updateSelCoupon() { + const newval = quansSelArr.value; + if (newval.length >= 2) { + let goodsCoupon = newval.filter((v) => v.type == 2); + let otherCoupon = newval.filter((v) => v.type != 2); + goodsCoupon = goodsCoupon.map((v) => { + const discount = UTILS.returnCouponDiscount( + canDikouGoodsArr, + v, + props.user, + orderPrice.value, + [], + shopUser.userInfo + ); + return { + ...v, + discount, + discountAmount: discount ? discount.discountPrice : v.discountAmount, + }; + }); + otherCoupon = otherCoupon.map((v) => { + const discount = UTILS.returnCouponDiscount( + canDikouGoodsArr, + v, + props.user, + orderPrice.value, + goodsCoupon, + shopUser.userInfo + ); + return { + ...v, + discount, + discountAmount: discount ? discount.discountPrice : v.discountAmount, + }; + }); + couponSel.value = otherCoupon[0]; + goodsCouponSel.value = goodsCoupon[0]; + } +} + watch( () => quansSelArr.value, (newval) => { console.log("quansSelArr", newval); - if (newval.length >= 2) { - let goodsCoupon = newval.filter((v) => v.type == 2); - let otherCoupon = newval.filter((v) => v.type != 2); - goodsCoupon = goodsCoupon.map((v) => { - const discount = UTILS.returnCouponDiscount( - canDikouGoodsArr, - v, - props.user, - orderPrice.value, - [], - shopUser.userInfo - ); - return { - ...v, - discount, - discountAmount: discount ? discount.discountPrice : v.discountAmount, - }; - }); - otherCoupon = otherCoupon.map((v) => { - const discount = UTILS.returnCouponDiscount( - canDikouGoodsArr, - v, - props.user, - orderPrice.value, - goodsCoupon, - shopUser.userInfo - ); - return { - ...v, - discount, - discountAmount: discount ? discount.discountPrice : v.discountAmount, - }; - }); - couponSel.value = otherCoupon[0]; - goodsCouponSel.value = goodsCoupon[0]; - } + getcoup(); } );