From 93e290fbaf52103649e2af1a6890952a86c0e5af Mon Sep 17 00:00:00 2001 From: YeMingfei666 <1619116647@qq.com> Date: Tue, 14 Oct 2025 11:40:25 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BC=98=E6=83=A0=E5=88=B8=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/coupon-utils.js | 12 +- src/utils/goods.ts | 392 ++---------------- .../tool/Instead/components/carts/list.vue | 8 +- src/views/tool/Instead/components/order.vue | 62 +-- 4 files changed, 84 insertions(+), 390 deletions(-) diff --git a/src/utils/coupon-utils.js b/src/utils/coupon-utils.js index 9ce937a..7701ca4 100644 --- a/src/utils/coupon-utils.js +++ b/src/utils/coupon-utils.js @@ -181,7 +181,6 @@ export function returnCouponCanUse(args) { }; } } - // 商品兑换券,第二件半价和买一送一判断是否有可用商品 if ([2, 4, 5].includes(coupon.type)) { if (coupon.type == 2 && fullAmount < coupon.fullAmount) { @@ -190,6 +189,7 @@ export function returnCouponCanUse(args) { reason: `满${coupon.fullAmount}元可用,当前可参与金额${fullAmount}元`, }; } + // 没有符合条件的商品 if (isDikouAll && canDikouGoodsArr.length === 0) { return { @@ -217,8 +217,8 @@ export function returnCouponCanUse(args) { let canUse = false; if (isDikouAll) { canUse = canDikouGoodsArr.some((v) => v.num >= 2); - } else if (canCalcGoodsArr.length > 0) { - canUse = canCalcGoodsArr.some((v) => v.num >= 2); + } else if (canUseGoodsArr.length > 0) { + canUse = canUseGoodsArr.some((v) => v.num >= 2); } if (!canUse) { @@ -234,10 +234,9 @@ export function returnCouponCanUse(args) { let canUse = false; if (isDikouAll) { canUse = canDikouGoodsArr.some((v) => v.num >= 2); - } else if (canCalcGoodsArr.length > 0) { - canUse = canCalcGoodsArr.some((v) => v.num >= 2); + } else if (canUseGoodsArr.length > 0) { + canUse = canUseGoodsArr.some((v) => v.num >= 2); } - if (!canUse) { return { canUse: false, @@ -296,6 +295,7 @@ export function calcDiscountGoodsArrPrice(discountGoodsArr, discountNum, user, s * @param shopInfo 店铺信息 */ export function returnCouponDiscount(arr, coupon, user, goodsOrderPrice, selCoupon, shopInfo) { + arr = returnCanDikouGoods(arr, user, shopInfo); const canDikouGoodsArr = returnCanDikouGoodsArr(arr, selCoupon, user); if (coupon.type == 2) { return returnCouponProductDiscount(canDikouGoodsArr, coupon, user, shopInfo); diff --git a/src/utils/goods.ts b/src/utils/goods.ts index b9f436b..59146b7 100644 --- a/src/utils/goods.ts +++ b/src/utils/goods.ts @@ -242,6 +242,7 @@ export interface OrderExtraConfig { export interface OrderCostSummary { // 商品总件数 goodsTotal: number; + totalDiscountAmount: number, goodsRealAmount: number; // 商品真实原价总和 goodsOriginalAmount: number; // 商品原价总和 goodsDiscountAmount: number; // 商品折扣金额 @@ -263,333 +264,10 @@ export interface OrderCostSummary { pointUsed: number; // 实际使用的积分 newUserDiscount: number; // 新用户减免金额(元,默认0) dinnerType?: "dine-in" | "take-out"; // 就餐类型(堂食/自取/配送/快递) + config: OrderExtraConfig; // 订单额外费用配置 } -// ============================ 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.type === undefined) { - console.warn("优惠券必选字段缺失", backendCoupon); - return null; - } - // 2. 转换券类型:后端数字枚举 → 工具库字符串枚举 - const couponType = mapBackendCouponTypeToTool(backendCoupon.type); - if (!couponType) { - 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 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 - ), - useShops: getApplicableStoreIds(backendCoupon, currentStoreId), - discountShare: backendCoupon.discountShare === 1, - vipPriceShare: backendCoupon.vipPriceShare === 1, - useType: useType, - 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 === 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) @@ -1406,25 +1084,7 @@ function isStoreMatchByList( 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排除,新增细分统计) @@ -1502,15 +1162,18 @@ export function calcTotalPackFee( goodsList: BaseCartItem[], 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) { - const availableNum = Math.max(0, goods.number - (goods.returnNum || 0)); + const packNumber = goods.packNumber ? goods.packNumber * 1 : 0; + let availableNum = Math.max(0, goods.number - (goods.returnNum || 0)); + availableNum = Math.min(availableNum, packNumber); + if (availableNum === 0) continue; // 计算单个商品打包数量(外卖全打包,堂食按配置,称重商品≤1) - let packNum = dinnerType === "take-out" ? availableNum : 0; + let packNum = availableNum; if (goods.product_type === GoodsType.WEIGHT) { packNum = Math.min(packNum, 1); } @@ -1661,8 +1324,8 @@ export function calculateOrderCostSummary( const merchantReductionConfig = config.merchantReduction; let merchantReductionActualAmount = 0; - // 计算商家减免的可抵扣上限:商品实际金额 - 优惠券 - 积分(避免减免后为负) - const maxMerchantReductionLimit = new BigNumber(goodsRealAmount) + // 计算商家减免的可抵扣上限:商品实际金额 - 优惠券 - 积分(避免减免后为负) 再加上餐位费和打包费 + let maxMerchantReductionLimit = new BigNumber(goodsRealAmount) .minus(couponDeductionAmount) .minus(pointDeductionAmount) .isGreaterThan(0) @@ -1671,6 +1334,9 @@ export function calculateOrderCostSummary( .minus(pointDeductionAmount) : new BigNumber(0); + maxMerchantReductionLimit = maxMerchantReductionLimit.plus(seatFee).plus(packFee) + + switch (merchantReductionConfig.type) { case MerchantReductionType.FIXED_AMOUNT: // 固定金额减免:取配置金额与上限的最小值,且不小于0 @@ -1713,17 +1379,36 @@ export function calculateOrderCostSummary( .minus(couponDeductionAmount) // 减去优惠券抵扣 .minus(newUserDiscount) // 新客立减 .minus(pointDeductionAmount) // 减去积分抵扣 - .minus(merchantReductionActualAmount); // 减去商家实际减免金额 + // .minus(merchantReductionActualAmount); // 减去商家实际减免金额 // 确保折扣后金额不小于0,再加上后续费用 const nonNegativeAmount = discountedAmount.gt(0) ? discountedAmount : new BigNumber(0); const finalPayAmount = nonNegativeAmount .plus(seatFee) // 加上餐位费(不参与减免) .plus(packFee) // 加上打包费(不参与减免) - .plus(additionalFee); // 加上附加费 + .plus(additionalFee) // 加上附加费 + .minus(merchantReductionActualAmount); // 减去商家实际减免金额 + const finalPayAmountNonNegative = Math.max(0, finalPayAmount.toNumber()); + //计算全部的优惠金额 + const totalDiscountAmount = new BigNumber(goodsDiscountAmount) // 商品折扣 + .plus(couponDeductionAmount) // 减去优惠券抵扣 + .plus(newUserDiscount) // 新客立减 + .plus(pointDeductionAmount) // 减去积分抵扣 + .plus(merchantReductionActualAmount).toNumber(); // 减去商家实际减免金额 + + //最原始价格 + const originalAmount = new BigNumber(goodsRealAmount) // 商品真实原价总和 + .minus(goodsDiscountAmount) // 减去商品折扣 + .minus(couponDeductionAmount) // 减去优惠券抵扣 + .minus(newUserDiscount) // 新客立减 + .minus(pointDeductionAmount) // 减去积分抵扣 + .minus(merchantReductionActualAmount).toNumber(); // 减去商家实际减免金额 + + + // 6. 返回完整费用汇总(包含商家减免明细) return { // 商品总件数 @@ -1737,7 +1422,8 @@ export function calculateOrderCostSummary( pointDeductionAmount, seatFee, packFee, - + // 全部优惠金额 + totalDiscountAmount, // 商家减免明细(含类型、原始配置、实际金额) merchantReduction: { type: merchantReductionConfig.type, @@ -1750,6 +1436,7 @@ export function calculateOrderCostSummary( pointUsed: usedPoints, newUserDiscount, dinnerType, + config }; } @@ -1766,9 +1453,6 @@ export const OrderPriceCalculator = { formatDiscountRate, filterThresholdGoods, isWeightGoods, - // 优惠券转换 - convertBackendCouponToToolCoupon, - mapBackendCouponTypeToTool, // 商品价格计算 calcSingleGoodsRealPrice, calcGoodsOriginalAmount, diff --git a/src/views/tool/Instead/components/carts/list.vue b/src/views/tool/Instead/components/carts/list.vue index 2b9f02b..83187c9 100644 --- a/src/views/tool/Instead/components/carts/list.vue +++ b/src/views/tool/Instead/components/carts/list.vue @@ -31,10 +31,14 @@ -