fix: 优惠券方法更新

This commit is contained in:
YeMingfei666 2025-10-14 11:40:25 +08:00
parent cd351be6d0
commit 93e290fbaf
4 changed files with 84 additions and 390 deletions

View File

@ -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);

View File

@ -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<number, CouponType> = {
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);
}
/**
* IDuseShopType返回对应数组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% 900.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,

View File

@ -31,10 +31,14 @@
<!-- 打包费 -->
<template v-if="carts.packNum > 0">
<div class="cart-title"><span>打包费</span></div>
<extra-fee name="打包费" :number="carts.packNum" :price="carts.packFee"></extra-fee>
<extra-fee
name="打包费"
:number="carts.packNum"
:price="carts.orderCostSummary.packFee"
></extra-fee>
</template>
<!-- 餐位费 -->
<template v-if="perpole >= 1">
<template v-if="perpole >= 1 && carts.dinnerType == 'dine-in'">
<div class="cart-title"><span>餐位费</span></div>
<extra-fee name="餐位费" :number="perpole" :price="canWeiFee"></extra-fee>
</template>

View File

@ -253,11 +253,7 @@ function refGuaZhangConfirm(guazhangRen) {
function refGuaZhangShow() {
refGuaZhang.value.open();
}
//
let goodsArr = [];
//
let $goodsPayPriceMap = {};
const refCoupon = ref();
let quansSelArr = ref([]);
function openCoupon() {
@ -276,7 +272,6 @@ function refCouponConfirm(e, goodsList) {
title: v.name,
};
});
goodsArr = goodsList;
console.log("refCouponConfirm", e);
carts.setCoupons(e);
@ -291,7 +286,6 @@ function delQuan(row) {
}
carts.setCoupons(quansSelArr.value);
}
function couponChange(data) {}
//
const refDiscount = ref();
@ -313,25 +307,40 @@ function discountConfirm(e) {
usePointsNumber.value = 0;
}
//
// 使bignumber.js
function returnMerchantReductionDiscount() {
const {
goodsOriginalAmount, //
goodsDiscountAmount, //
couponDeductionAmount, //
pointDeductionAmount, //
merchantReductionActualAmount, //
seatFee, //
packFee, //
goodsDiscountAmount, //
couponDeductionAmount, //
pointDeductionAmount, //
seatFee, //
packFee, //
additionalFee,
} = carts.orderCostSummary;
const total =
goodsOriginalAmount - //
goodsDiscountAmount - //
couponDeductionAmount; //
return total <= 0 ? 0 : total;
// BigNumber
const originalAmount = new BigNumber(goodsOriginalAmount);
const discountAmount = new BigNumber(goodsDiscountAmount);
const couponAmount = new BigNumber(couponDeductionAmount);
const pointAmount = new BigNumber(pointDeductionAmount);
const seat = new BigNumber(seatFee);
const pack = new BigNumber(packFee);
const additional = new BigNumber(additionalFee);
//
const total = originalAmount
.minus(discountAmount) //
.minus(couponAmount) //
.minus(pointAmount) //
.plus(seat) //
.plus(pack) //
.plus(additional); //
// 0
return total.lte(0) ? 0 : total.toNumber();
}
function discountShow(e) {
refDiscount.value.open({
// amount: carts.goodsTotal - productCouponDiscountAmount.value,
@ -396,7 +405,11 @@ const pointsRes = ref({ usable: true, maxUsablePoints: 0, minDeductionPoints: 0
const usePointsNumber = ref(0);
//
const scoreMaxMoney = computed(() => {
return carts.orderCostSummary.finalPayAmount + carts.orderCostSummary.pointDeductionAmount;
return (
carts.orderCostSummary.finalPayAmount +
carts.orderCostSummary.pointDeductionAmount -
carts.orderCostSummary.merchantReduction.actualAmount
);
});
watch(
() => scoreMaxMoney.value,
@ -433,14 +446,7 @@ const emits = defineEmits(["chooseUser", "paysuccess"]);
function chooseUser() {
emits("chooseUser");
}
const coupDiscount = computed(() => {
const total =
props.orderInfo.fullCouponDiscountAmount * 1 + props.orderInfo.productCouponDiscountAmount * 1;
if (total <= 0) {
return 0;
}
return total.toFixed(2);
});
const score = reactive({
list: [],
sel: -1,
@ -495,7 +501,7 @@ function returnPayParams() {
orderId: props.orderInfo.id,
// discountRatio: (checkOrderPay.discount / 100).toFixed(2),
discountRatio: 0,
seatNum: props.perpole * 1,
seatNum: carts.dinnerType == "dine-in" ? props.perpole * 1 : 0,
originAmount: carts.orderCostSummary.goodsRealAmount,
discountAmount: discountAmount.value,
productCouponDiscountAmount: carts.orderCostSummary.productCouponDeduction,