From f76dff67d4ca6b341a840b937137e482d7c3b063 Mon Sep 17 00:00:00 2001 From: YeMingfei666 <1619116647@qq.com> Date: Fri, 19 Sep 2025 18:32:25 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E6=94=B9=E8=AE=A2=E5=8D=95?= =?UTF-8?q?=E8=AE=A1=E7=AE=97=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + src/api/account/coupon.ts | 4 +- src/api/market/coupon.ts | 16 + src/components/coupon/coup-lists.vue | 85 ++ src/store/modules/carts.ts | 836 +++++++++--------- src/utils/goods.ts | 561 ++++++++---- src/utils/test.ts | 6 +- .../components/headerCard.vue | 2 +- .../super_vip/components/dialog-plans.vue | 5 +- .../marketing_center/super_vip/index.vue | 200 +++-- .../tool/Instead/components/carts/item.vue | 4 +- .../tool/Instead/components/carts/list.vue | 3 - src/views/tool/Instead/components/order.vue | 134 ++- .../Instead/components/popup-coupon-back.vue | 456 ++++++++++ .../tool/Instead/components/popup-coupon.vue | 11 +- src/views/tool/Instead/index.vue | 6 + .../user/list/components/give-coupon.vue | 113 +++ .../list/components/user-coupon-dialog.vue | 46 +- src/views/user/list/config/content.ts | 1 + src/views/user/list/index.vue | 11 +- 20 files changed, 1800 insertions(+), 701 deletions(-) create mode 100644 src/components/coupon/coup-lists.vue create mode 100644 src/views/tool/Instead/components/popup-coupon-back.vue create mode 100644 src/views/user/list/components/give-coupon.vue diff --git a/package.json b/package.json index a57b7a2..4d34149 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@wangeditor-next/editor-for-vue": "^5.1.14", "ali-oss": "^6.22.0", "axios": "^1.7.9", + "bignumber.js": "^9.3.1", "bwip-js": "^4.5.1", "codemirror": "^5.65.18", "codemirror-editor-vue3": "^2.8.0", diff --git a/src/api/account/coupon.ts b/src/api/account/coupon.ts index c79df0f..1bc848c 100644 --- a/src/api/account/coupon.ts +++ b/src/api/account/coupon.ts @@ -1,6 +1,6 @@ import request from "@/utils/request"; -import { Account_BaseUrl } from "@/api/config"; -const baseURL = Account_BaseUrl + "/admin/coupon"; +import { Market_BaseUrl } from "@/api/config"; +const baseURL = Market_BaseUrl + "/admin/coupon"; const API = { getList(params: getListRequest) { return request({ diff --git a/src/api/market/coupon.ts b/src/api/market/coupon.ts index fddb4f4..15f2d06 100644 --- a/src/api/market/coupon.ts +++ b/src/api/market/coupon.ts @@ -17,6 +17,22 @@ const API = { params }); }, + // 删除用户优惠券 + delete(params: any) { + return request({ + url: `${baseURL}/deleteRecord`, + method: "delete", + params, + }); + }, + //优惠券发放 + giveCoupon(data: any) { + return request({ + url: `${baseURL}/grant`, + method: "post", + data + }); + }, } export default API; diff --git a/src/components/coupon/coup-lists.vue b/src/components/coupon/coup-lists.vue new file mode 100644 index 0000000..1a6e156 --- /dev/null +++ b/src/components/coupon/coup-lists.vue @@ -0,0 +1,85 @@ + + \ No newline at end of file diff --git a/src/store/modules/carts.ts b/src/store/modules/carts.ts index e9643a2..c0f0109 100644 --- a/src/store/modules/carts.ts +++ b/src/store/modules/carts.ts @@ -2,49 +2,121 @@ import { store } from "@/store"; import WebSocketManager, { type ApifoxModel, msgType } from "@/utils/websocket"; import orderApi from "@/api/order/order"; import { useUserStoreHook } from "@/store/modules/user"; -import { customTruncateToTwoDecimals } from '@/utils/tools' import productApi from "@/api/product/index"; -import yskUtils from 'ysk-utils' -const { OrderPriceCalculator } = yskUtils -console.log(OrderPriceCalculator) + +// 导入工具库及相关类型 +import { + OrderPriceCalculator, + BaseCartItem, + BackendCoupon, + ActivityConfig, + OrderExtraConfig, MerchantReductionConfig, MerchantReductionType, + GoodsType +} from "@/utils/goods"; + const shopUser = useUserStoreHook(); export interface CartsState { id: string | number; [property: string]: any; } +interface SeatFeeConfig { + pricePerPerson: number; + personCount: number; + isEnabled: boolean; +} +interface PointDeductionRule { + pointsPerYuan: number; + maxDeductionAmount: number; +} export const useCartsStore = defineStore("carts", () => { - //选择用户 + // ------------------------------ 1. 移到内部的工具函数(核心修复) ------------------------------ + // 适配工具库 BaseCartItem 接口的商品数据转换函数(原外部函数,现在内部) + const convertToBaseCartItem = (item: any): BaseCartItem => { + const skuData = item.skuData + ? { + id: item.skuData.id || item.sku_id, + salePrice: item.skuData.salePrice || 0, + memberPrice: item.skuData.memberPrice || 0 + } + : undefined; + + let productType: GoodsType = GoodsType.NORMAL; + switch (item.product_type) { + case 'weight': productType = GoodsType.WEIGHT; break; + case 'gift': productType = GoodsType.GIFT; break; + case 'package': productType = GoodsType.PACKAGE; break; + default: productType = GoodsType.NORMAL; + } + + return { + id: item.id, + product_id: item.product_id, + salePrice: item.salePrice || 0, + number: item.number || 0, + product_type: productType, + is_temporary: !!item.is_temporary, + is_gift: !!item.is_gift, + returnNum: item.returnNum || 0, + memberPrice: item.memberPrice || 0, + discountSaleAmount: item.discount_sale_amount || 0, + packFee: item.packFee || 0, + packNumber: item.pack_number || 0, + activityInfo: item.activityInfo + ? { + type: item.activityInfo.type, + discountRate: OrderPriceCalculator.formatDiscountRate(item.activityInfo.discountRate), + vipPriceShare: !!item.activityInfo.vipPriceShare + } + : undefined, + skuData + }; + }; + + // 合并所有商品列表(原外部函数,现在内部,可直接访问 list/giftList/oldOrder) + const getAllGoodsList = (): BaseCartItem[] => { + const currentGoods = (list.value || []).map(convertToBaseCartItem); + const giftGoods = (giftList.value || []).map(convertToBaseCartItem); + + // 扁平化历史订单商品 + const oldOrderGoods = Object.values(oldOrder.value.detailMap || {}) + .flat() + .map(convertToBaseCartItem); + + return [...currentGoods, ...giftGoods, ...oldOrderGoods]; + }; + + // ------------------------------ 2. Store 内部原有响应式变量 ------------------------------ + // 选择用户 const vipUser = ref<{ id?: string | number, isVip?: boolean }>({}); function changeUser(user: any) { vipUser.value = user; + userPoints.value = 0; } - //就餐类型 dine-in take-out - let dinnerType = ref('dine-in'); + // 就餐类型 + let dinnerType = ref<'dine-in' | 'take-out'>('dine-in'); - - - //是否启用会员价 + // 是否启用会员价 const useVipPrice = computed(() => { - return (shopUser.userInfo.isMemberPrice && vipUser.value.id && vipUser.value.isVip) ? true : false - }) + return (shopUser.userInfo.isMemberPrice && vipUser.value.id && vipUser.value.isVip) ? true : false; + }); - //台桌id - // const table_code = useStorage('Instead_table_code', ''); - const table_code = ref('') + // 台桌id + const table_code = ref(''); - //购物车是否初始化连接加载完成 - const isLinkFinshed = ref(false) - //当前购物车数据 + // 购物车是否初始化连接加载完成 + const isLinkFinshed = ref(false); + + // 当前购物车数据(现在 getAllGoodsList 能直接访问) const list = useStorage("carts", []); - //历史订单数据 + // 历史订单数据(现在 getAllGoodsList 能直接访问) const oldOrder = useStorage("Instead_olold_order", { detailMap: [], originAmount: 0 }); - //代客下单页面商品缓存 + // 代客下单页面商品缓存 const goods = useStorage("Instead_goods", []); async function getGoods(query: any) { const res = await productApi.getPage({ @@ -54,224 +126,212 @@ export const useCartsStore = defineStore("carts", () => { ...query, }); goods.value = res.records; - setGoodsMap(goods.value) + setGoodsMap(goods.value); } function setGoodsMap(goods: any[]) { for (let item of goods) { goodsMap[item.id] = item; } - } - //赠菜 + // 赠菜(现在 getAllGoodsList 能直接访问) const giftList = useStorage("giftList", []); let goodsMap: { [key: string]: any } = useStorage('Instead_goods_map', {}); - //当前选中cart - let selListIndex = ref(-1); - //当前选中商品是否是赠菜 + // ------------------------------ 3. 原有计算属性和业务逻辑(无需修改,正常调用 getAllGoodsList) ------------------------------ + // 当前选中cart相关状态 + let selListIndex = ref(-1); const isSelGift = ref(false); - //当前选中是否是历史订单 const isOldOrder = ref(false); - //选中历史订单中的第几次下单 const selPlaceNum = ref(-1); - const defaultCart = { - id: '', - number: 0, - } - //购物车是否为空 + const defaultCart = { id: '', number: 0 }; + + // 当前购物车是否为空 const isEmpty = computed(() => { return list.value.length === 0 && giftList.value.length === 0 - }) - //当前购物车选中数据 + }); + + // 当前购物车选中数据 const selCart = computed(() => { if (isOldOrder.value && selPlaceNum.value >= 0) { - return oldOrder.value.detailMap[selPlaceNum.value][selListIndex.value] + return oldOrder.value.detailMap[selPlaceNum.value][selListIndex.value] || defaultCart; } if (isSelGift.value) { - return giftList.value[selListIndex.value] || defaultCart + return giftList.value[selListIndex.value] || defaultCart; } - return list.value[selListIndex.value] || defaultCart - }) - //当前购物车选中对应商品数据 + return list.value[selListIndex.value] || defaultCart; + }); + + // 当前购物车选中对应商品数据 const selGoods = computed(() => { - return goodsMap[selCart.value.product_id] - }) - //当前选中购物车数据是否是可选套餐商品 + return goodsMap[selCart.value.product_id]; + }); + + // 当前选中购物车数据是否是可选套餐商品 const isCanSelectGroup = computed(() => { - if (!selGoods.value) { - return false - } - return selGoods.value.type == 'package' && selGoods.value.groupType == 1 - }) - function returnOneGoodsTotalMoney(cur: any) { - const memberPrice = cur.memberPrice || cur.salePrice - const total = cur.number * (useVipPrice.value ? memberPrice : cur.salePrice) - if (cur.type == 'weight') { - return customTruncateToTwoDecimals(total) - } else { - return total - } + if (!selGoods.value) return false; + return selGoods.value.type === 'package' && selGoods.value.groupType === 1; + }); + // 初始配置:默认无减免(固定金额 0 元) + const merchantReductionConfig = ref({ + type: MerchantReductionType.FIXED_AMOUNT, + fixedAmount: 0 + }); + + // 暴露外部修改方法:设置固定金额减免 + function setMerchantFixedReduction(amount: number) { + merchantReductionConfig.value = { + type: MerchantReductionType.FIXED_AMOUNT, + fixedAmount: Math.max(0, amount) // 确保金额非负 + }; } - //赠菜总价 + + // 暴露外部修改方法:设置比例折扣减免(传入折扣率,如 90 代表 9 折) + function setMerchantDiscountReduction(discountRate: number) { + merchantReductionConfig.value = { + type: MerchantReductionType.DISCOUNT_RATE, + discountRate: Math.max(0, Math.min(100, discountRate)) // 确保折扣率在 0-100% 之间 + }; + } + + // 暴露外部方法:清空商家减免 + function clearMerchantReduction() { + merchantReductionConfig.value = { + type: MerchantReductionType.FIXED_AMOUNT, + fixedAmount: 0 + }; + } + + const seatFeeConfig = ref({ + pricePerPerson: shopUser.userInfo.tableFee || 0, + personCount: 0, + isEnabled: !shopUser.userInfo.isTableFee + }) + const pointDeductionRule = ref({ + pointsPerYuan: 100, + maxDeductionAmount: Infinity + }) + const userPoints = ref(0); + // 订单额外配置(现在依赖响应式的 merchantReduction) + const orderExtraConfig = computed(() => ({ + // 引用扩展后的商家减免配置 + merchantReduction: merchantReductionConfig.value, + additionalFee: 0, + pointDeductionRule: pointDeductionRule.value, + seatFeeConfig: seatFeeConfig.value, + currentStoreId: shopUser.userInfo.shopId?.toString() || '', + userPoints: userPoints.value, + isMember: useVipPrice.value, + memberDiscountRate: shopUser.userInfo.memberDiscountRate || 1 + })); + + // 营销活动列表 + const activityList = computed(() => { + return []; + }); + + // 优惠券列表 + const backendCoupons = computed(() => { + return []; + }); + + // 商品加入购物车顺序 + const cartOrder = ref>({}); + + // 订单费用汇总(调用内部的 getAllGoodsList) + const orderCostSummary = computed(() => { + const allGoods = getAllGoodsList(); + const costSummary = OrderPriceCalculator.calculateOrderCostSummary( + allGoods, + dinnerType.value, + backendCoupons.value, + activityList.value, + orderExtraConfig.value, + cartOrder.value, + new Date() + ); + return costSummary; + }); + + + // 赠菜总价(调用内部的 getAllGoodsList) const giftMoney = computed(() => { - let oldGiftMoney = 0 - for (let i in oldOrder.value.detailMap) { - oldGiftMoney += oldOrder.value.detailMap[i].filter((v: any) => v.isGift).reduce((prve: number, cur: any) => { - const memberPrice = cur.memberPrice || cur.salePrice - return prve + cur.number * (useVipPrice.value ? memberPrice : cur.salePrice) - }, 0) - } - const nowTotal = giftList.value.reduce((acc: number, cur: any) => { - const memberPrice = cur.memberPrice || cur.salePrice - return acc + cur.number * (useVipPrice.value ? memberPrice : cur.salePrice) - }, 0) - return (nowTotal + oldGiftMoney) - }) - //返回打包数量(称重商品打包数量最大为1) - function returnCartPackNumber(cur: any) { - console.log(cur) - let pack_number = (dinnerType.value == 'take-out' ? cur.number : cur.pack_number * 1) - pack_number = (cur.product_type == 'weight' && pack_number > 1) ? 1 : pack_number; - return pack_number * 1 - } - //打包数量 + const allGoods = getAllGoodsList(); + const giftGoods = allGoods.filter(OrderPriceCalculator.isGiftGoods); + return OrderPriceCalculator.calcGoodsOriginalAmount(giftGoods); + }); + + // 打包数量(调用内部的 getAllGoodsList) const packNum = computed(() => { - const nowCartNumber = list.value.reduce((acc: number, cur: any) => { - return acc + returnCartPackNumber(cur) - }, 0) - const giftNumber = giftList.value.reduce((acc: number, cur: any) => { - return acc + returnCartPackNumber(cur) - }, 0) - let oldNumber = 0 - for (let i in oldOrder.value.detailMap) { - oldNumber += oldOrder.value.detailMap[i].reduce((prve: number, cur: any) => { - return prve + returnCartPackNumber(cur) - }, 0) - } - return nowCartNumber + giftNumber + oldNumber - }) + const allGoods = getAllGoodsList(); + return allGoods.reduce((total, goods) => { + const packNumber = OrderPriceCalculator.isWeightGoods(goods) + ? 1 + : (dinnerType.value === 'take-out' ? goods.number : goods.packNumber || 0); + return total + Math.max(0, packNumber); + }, 0); + }); - //打包费 + // 打包费 const packFee = computed(() => { - const nowPackFee = list.value.reduce((acc: number, cur: any) => { - return acc + (cur.packFee || 0) * returnCartPackNumber(cur) - }, 0) - const giftPackFee = giftList.value.reduce((acc: number, cur: any) => { - return acc + (cur.packFee || 0) * returnCartPackNumber(cur) - }, 0) - let oldPackfee = 0; - for (let i in oldOrder.value.detailMap) { - oldPackfee += oldOrder.value.detailMap[i].reduce((prve: number, cur: any) => { - return prve + (cur.packFee || 0) * returnCartPackNumber(cur) - }, 0) - } - return nowPackFee + giftPackFee + oldPackfee - }) - //会员优惠 + return orderCostSummary.value.packFee; + }); + + // 会员优惠(调用内部的 getAllGoodsList) const vipDiscount = computed(() => { - if (!useVipPrice.value) { - return 0 - } - const listTotal = list.value.reduce((acc: number, cur: any) => { - const n = (cur.salePrice * 1 - cur.memberPrice * 1) * cur.number - return acc + (n <= 0 ? 0 : n) - }, 0) - const giftTotal = giftList.value.reduce((acc: number, cur: any) => { - const n = (cur.salePrice * 1 - cur.memberPrice * 1) * cur.number - return acc + (n <= 0 ? 0 : n) - }, 0) - let oldTotal = 0; - for (let i in oldOrder.value.detailMap) { - oldTotal += oldOrder.value.detailMap[i].reduce((prve: number, cur: any) => { - const n = (cur.salePrice * 1 - cur.memberPrice * 1) * cur.number - return prve + (n <= 0 ? 0 : n) - }, 0) - } - return listTotal + giftTotal + oldTotal - }) - //单品改价优惠 + const allGoods = getAllGoodsList(); + const nonMemberRealAmount = OrderPriceCalculator.calcGoodsRealAmount( + allGoods, + { isMember: false, memberDiscountRate: 1 }, + activityList.value + ); + return OrderPriceCalculator.truncateToTwoDecimals( + nonMemberRealAmount - orderCostSummary.value.goodsOriginalAmount + ); + }); + + // 单品改价优惠(调用内部的 getAllGoodsList) const singleDiscount = computed(() => { - const listTotal = list.value.reduce((acc: number, cur: any) => { - if (cur.discount_sale_amount * 1 <= 0) { - return acc + 0 - } - const originPrice = useVipPrice.value ? (cur.memberPrice || cur.salePrice) : cur.salePrice - const n = (originPrice * 1 - cur.discount_sale_amount * 1) * cur.number - return acc + (n <= 0 ? 0 : n) - }, 0) - return listTotal - }) + const allGoods = getAllGoodsList(); + const noDiscountGoods = allGoods.map(goods => ({ ...goods, discountSaleAmount: 0 })); + const noDiscountRealAmount = OrderPriceCalculator.calcGoodsRealAmount( + noDiscountGoods, + { isMember: orderExtraConfig.value.isMember, memberDiscountRate: orderExtraConfig.value.memberDiscountRate }, + activityList.value + ); + return OrderPriceCalculator.truncateToTwoDecimals( + noDiscountRealAmount - orderCostSummary.value.goodsOriginalAmount + ); + }); - - // 优惠 + // 已优惠文案 const yiyouhui = computed(() => { - const youhui = giftMoney.value * 1 + vipDiscount.value * 1 + singleDiscount.value * 1 - if (youhui > 0) { - return '已优惠¥' + youhui.toFixed(2) - } - return '' - }) + const totalDiscount = giftMoney.value + vipDiscount.value + singleDiscount.value; + return totalDiscount > 0 + ? `已优惠¥${OrderPriceCalculator.truncateToTwoDecimals(totalDiscount).toFixed(2)}` + : ''; + }); - //历史订单价格 - const oldOrderMoney = computed(() => { - let total = 0 - for (let i in oldOrder.value.detailMap) { - total += oldOrder.value.detailMap[i].reduce((prve: number, cur: any) => { - if (cur.isGift) { - return prve + 0 - } - const discount_sale_amount = cur.discount_sale_amount * 1 || 0 - const memberPrice = cur.skuData ? (cur.skuData.memberPrice || cur.skuData.salePrice) : 0 - const price = (discount_sale_amount || cur.salePrice || 0) - const number = (cur.number - cur.returnNum) - return prve + (number <= 0 ? 0 : number) * (discount_sale_amount || (useVipPrice.value ? memberPrice : price)) - }, 0) - } - return total - }) - - - //支付总价 - const payMoney = computed(() => { - const money = list.value.reduce((acc: number, cur: any) => { - const discount_sale_amount = cur.discount_sale_amount * 1 || 0 - const memberPrice = cur.skuData ? (cur.skuData.memberPrice || cur.skuData.salePrice) : 0 - const price = (cur.discount_sale_amount * 1 || cur.salePrice || 0) - return acc + cur.number * (discount_sale_amount || (useVipPrice.value ? memberPrice : price)) - }, 0) - return (money + packFee.value + oldOrderMoney.value * 1).toFixed(2) - }) - //只算商品的总价 + // 只算商品的总价 const goodsTotal = computed(() => { - const money = list.value.reduce((acc: number, cur: any) => { - const discount_sale_amount = cur.discount_sale_amount * 1 || 0 - const memberPrice = cur.skuData ? (cur.skuData.memberPrice || cur.skuData.salePrice) : 0 - const price = (cur.discount_sale_amount * 1 || cur.salePrice || 0) - return acc + cur.number * (discount_sale_amount || (useVipPrice.value ? memberPrice : price)) - }, 0) - return (money + oldOrderMoney.value * 1).toFixed(2) - }) - //总计数量 + return orderCostSummary.value.goodsOriginalAmount.toFixed(2); + }); + + // 总计数量(调用内部的 getAllGoodsList) const totalNumber = computed(() => { - const cartNumber = list.value.reduce((acc: number, cur: any) => { - return acc + cur.number * 1 - }, 0) - const giftNumber = list.value.reduce((acc: number, cur: any) => { - return acc + cur.number * 1 - }, 0) - let oldNumber = 0 - - for (let i in oldOrder.value.detailMap) { - oldNumber += oldOrder.value.detailMap[i].reduce((prve: number, cur: any) => { - return prve + cur.number * 1 - }, 0) - } - return cartNumber + giftNumber + oldNumber - }) + const allGoods = getAllGoodsList(); + return allGoods.reduce((total, goods) => { + return total + Math.max(0, goods.number - (goods.returnNum || 0)); + }, 0); + }); + // 支付总价 + const payMoney = computed(() => { + return orderCostSummary.value.finalPayAmount.toFixed(2); + }); + // ------------------------------ 4. 原有业务逻辑(增删改查、WebSocket等,保持不变) ------------------------------ function changeNumber(step: number, item: CartsState) { if (item.number * 1 + step <= 0) { del(item); @@ -279,11 +339,11 @@ export const useCartsStore = defineStore("carts", () => { } const newNumber = item.number * 1 + step * 1; let pack_number = newNumber < item.pack_number ? (item.pack_number * 1 + step * 1) : item.pack_number; - if (dinnerType.value == 'take-out') { - pack_number = newNumber + if (dinnerType.value === 'take-out') { + pack_number = newNumber; } - if (item.product_type == 'weight') { - pack_number = 1 + if (item.product_type === 'weight') { + pack_number = 1; } update({ ...item, number: newNumber, pack_number }); } @@ -319,7 +379,6 @@ export const useCartsStore = defineStore("carts", () => { } } - const basic_msg = { number: 1, is_gift: 0, @@ -333,38 +392,30 @@ export const useCartsStore = defineStore("carts", () => { remark: "", sku_id: '', product_type: '' - } - //当前购物车直接添加 + }; + function cartsPush(data: any) { - sendMessage('add', { - ...basic_msg, - ...data - }); + sendMessage('add', { ...basic_msg, ...data }); } function add(data: any) { - const msg = { - ...basic_msg, - ...data - } + const msg = { ...basic_msg, ...data }; const hasCart = list.value.find((cartItem) => { return cartItem.product_id == msg.product_id && cartItem.sku_id == msg.sku_id; }); if (hasCart) { - update({ ...hasCart, ...msg, number: hasCart.number * 1 + msg.number * 1 }) + update({ ...hasCart, ...msg, number: hasCart.number * 1 + msg.number * 1 }); } else { - console.log(msg); sendMessage('add', msg); } } - // 换桌 function changeTable(newVal: string | undefined) { table_code.value = `${newVal}`; - $initParams.table_code = newVal - concocatSocket() + $initParams.table_code = newVal; + concocatSocket(); } - //转桌 + function rotTable(newVal: string | number, cart_id = []) { sendMessage('rottable', { new_table_code: newVal, @@ -373,37 +424,34 @@ export const useCartsStore = defineStore("carts", () => { }); } - function del(data: any) { sendMessage('del', { id: data.id }); } function update(data: any) { - console.log(data); const suitNum = data.skuData ? (data.skuData.suitNum || 1) : 1; if (data.number * 1 < suitNum * 1) { return sendMessage('del', data); } - const pack_number = dinnerType.value == 'take-out' ? data.number : data.pack_number + const pack_number = dinnerType.value === 'take-out' ? data.number : data.pack_number; sendMessage('edit', { ...data, pack_number }); } + function updateTag(key: string, val: any, cart: CartsState = selCart.value) { - const skuData = cart.skuData || { suitNum: 1 } + const skuData = cart.skuData || { suitNum: 1 }; if (cart.number * 1 < skuData.suitNum * 1) { return sendMessage('del', cart); } - console.log(key, val) - if (key == 'discount_sale_amount' && val * 1 <= 0) { - return ElMessage.error('价格不能为0!') + if (key === 'discount_sale_amount' && val * 1 <= 0) { + return ElMessage.error('价格不能为0!'); } - const msg = { ...cart, [key]: val } - if (key == 'number' && dinnerType.value == 'take-out') { - msg.pack_number == val + const msg = { ...cart, [key]: val }; + if (key === 'number' && dinnerType.value === 'take-out') { + msg.pack_number = val; } - sendMessage('edit', msg); } - // 更改全部商品打包状态 + function changePack(is_pack: number | string) { if (!isEmpty.value) { sendMessage('batch', { is_pack }); @@ -413,88 +461,77 @@ export const useCartsStore = defineStore("carts", () => { function clear() { sendMessage('cleanup', {}); } + function dataReset() { selListIndex.value = -1; - selPlaceNum.value = 1 - isOldOrder.value = false - isSelGift.value = false + selPlaceNum.value = -1; + isOldOrder.value = false; + isSelGift.value = false; list.value = []; giftList.value = []; - oldOrder.value = { - detailMap: [], - originAmount: 0 - } - vipUser.value = {} + oldOrder.value = { detailMap: [], originAmount: 0 }; + vipUser.value = {}; + cartOrder.value = {}; } function nowCartsClear() { - if (selPlaceNum.value == 1) { - selListIndex.value = -1; - } + selListIndex.value = -1; + isOldOrder.value = false; list.value = []; giftList.value = []; + cartOrder.value = {}; + userPoints.value = 0; + + } - // 寻找套餐商品sku interface GroupSnap { goods: { [key: string]: any }[]; } function findInGroupSnapSku(groupSnap: GroupSnap[], sku_id: string | number) { for (let i in groupSnap) { - const sku = groupSnap[i].goods.find(v => v.sku_id == sku_id) - if (sku) { - return sku - } + const sku = groupSnap[i].goods.find(v => v.sku_id == sku_id); + if (sku) return sku; } } - //获取历史订单 async function getOldOrder(table_code: string | number) { - const res = await orderApi.getHistoryList({ tableCode: table_code }) - if (res) { - setOldOrder(res) - } + const res = await orderApi.getHistoryList({ tableCode: table_code }); + if (res) setOldOrder(res); } function getProductDetails(v: { product_id: string, sku_id: string }) { - const goods = goodsMap[v.product_id] - if (!goods) { - return undefined - } - let skuData = undefined; - skuData = goods?.skuList.find((sku: { id: string, salePrice: number }) => sku.id == v.sku_id); + const goods = goodsMap[v.product_id]; + console.log('getProductDetails', goods) + if (!goods) return undefined; - // if (goods.type == 'package') { - // //套餐商品 - // const SnapSku = findInGroupSnapSku(goods.groupSnap, v.sku_id) - // skuData = { ...SnapSku, salePrice: SnapSku ? SnapSku.price : 0 } - // } else { - // skuData = goods?.skuList.find((sku: { id: string, salePrice: number }) => sku.id == v.sku_id); - // } - skuData = goods?.skuList.find((sku: { id: string, salePrice: number }) => sku.id == v.sku_id); + let skuData = goods?.skuList.find((sku: { id: string, salePrice: number }) => sku.id == v.sku_id); + if (!skuData && goods.type == 'package') { + const SnapSku = findInGroupSnapSku(goods.groupSnap, v.sku_id); + skuData = { ...SnapSku, salePrice: SnapSku ? SnapSku.price : 0 }; + } if (skuData) { return { - salePrice: skuData ? skuData.salePrice : 0, - memberPrice: skuData ? skuData.memberPrice : 0, + salePrice: skuData.salePrice || 0, + memberPrice: skuData.memberPrice || 0, coverImg: goods.coverImg, name: goods.name, specInfo: skuData.specInfo, packFee: goods.packFee || 0, type: goods.type, skuData - } - } else { - return undefined + }; } + return undefined; } function returnDetailMap(data: any) { - const newData: { [key: string]: any } = {} + const newData: { [key: string]: any } = {}; for (let i in data) { newData[i] = data[i].map((v: any) => { - const skuData = getProductDetails({ product_id: v.productId, sku_id: v.skuId }) + const skuData = getProductDetails({ product_id: v.productId, sku_id: v.skuId }); return { ...v, ...skuData, @@ -512,195 +549,166 @@ export const useCartsStore = defineStore("carts", () => { sku_name: v.skuName, sku_id: v.skuId, product_type: v.productType - } - }) + }; + }); } - return newData + return newData; } function setOldOrder(data: any) { oldOrder.value = { ...data, detailMap: returnDetailMap(data.detailMap) - } + }; } - let $initParams = {} as ApifoxModel - - - /** - * - * @param initParams 购物车初始化参数 - * @param $goodsMap 商品id对应的map - * @param oldOrder 历史订单数据 - */ - - + let $initParams = {} as ApifoxModel; async function init(initParams: ApifoxModel, $oldOrder: any | undefined) { - // 商品id对应的数据map - await getGoods({}) + await getGoods({}); + if ($oldOrder) setOldOrder($oldOrder); + else oldOrder.value = { detailMap: [] }; - if ($oldOrder) { - setOldOrder($oldOrder) - } else { - oldOrder.value = { detailMap: [] } - } - - // console.log('oldOrder.detailMap', oldOrder.value.detailMap) - // const cache_table_code = localStorage.getItem('cache_table_code'); - // const randomTableCode = cache_table_code ? cache_table_code : ('APC' + (1000 + Math.floor(Math.random() * 9000))) if (initParams) { - initParams.table_code = initParams.table_code ? initParams.table_code : '' - table_code.value = initParams.table_code + initParams.table_code = initParams.table_code || ''; + table_code.value = initParams.table_code; $initParams = initParams; } - console.log($initParams) - // localStorage.setItem('cache_table_code', table_code.value); - concocatSocket($initParams) + concocatSocket($initParams); } function concocatSocket(initParams = $initParams) { - console.log("初始化参数", initParams); WebSocketManager.subscribeToTopic(initParams, (msg) => { console.log("收到消息:", msg); - console.log([...list.value, ...giftList.value]) - if (msg.hasOwnProperty('status') && msg.status != 1) { - return ElMessage.error(msg.message || '操作失败') + if (msg.hasOwnProperty('status') && msg.status !== 1) { + return ElMessage.error(msg.message || '操作失败'); } - if (msg && msg.data) { + if (msg?.data) { if (Array.isArray(msg.data) && msg.data.length && msg.data[0].table_code) { - table_code.value = msg.data[0].table_code + table_code.value = msg.data[0].table_code; + } else if (msg.data.table_code) { + table_code.value = table_code.value || msg.data.table_code; + } else if (msg.table_code) { + table_code.value = table_code.value || msg.table_code; } - if (msg.data.table_code) { - table_code.value = table_code.value ? table_code.value : msg.data.table_code - } - if (msg.table_code) { - table_code.value = table_code.value ? table_code.value : msg.table_code - - } - } - - // 初始化 if (msg.operate_type === "manage_init") { - isLinkFinshed.value = true - // 设置单价 + isLinkFinshed.value = true; list.value = msg.data.filter((v: Record) => { - if (v.is_temporary) { - return v - } - const skuData = getProductDetails({ product_id: v.product_id, sku_id: v.sku_id }) + if (v.is_temporary) return v; + const skuData = getProductDetails({ product_id: v.product_id, sku_id: v.sku_id }); if (skuData) { - (Object.keys(skuData) as (keyof typeof skuData)[]).forEach((key) => { + Object.keys(skuData).forEach(key => { v[key] = skuData[key]; }); } else { - del({ id: v.id }) - return false + del({ id: v.id }); + return false; } - return !v.is_gift - }) + return !v.is_gift; + }); giftList.value = msg.data.filter((v: Record) => { - if (v.is_temporary) { - return v && v.is_gift - } - const skuData = getProductDetails({ product_id: v.product_id, sku_id: v.sku_id }) + if (v.is_temporary) return v && v.is_gift; + const skuData = getProductDetails({ product_id: v.product_id, sku_id: v.sku_id }); if (skuData) { - (Object.keys(skuData) as (keyof typeof skuData)[]).forEach((key) => { + Object.keys(skuData).forEach(key => { v[key] = skuData[key]; }); } else { - del({ id: v.id }) - return false + del({ id: v.id }); + return false; } - return v.is_gift - }) - } - //广播 - if (msg.type === "bc") { - msg.operate_type = 'manage_' + msg.operate_type + return v.is_gift; + }); + cartOrder.value = msg.data.reduce((obj: Record, item: any) => { + obj[item.id] = new Date(item.create_time).getTime(); + return obj; + }, {}); } + if (msg.operate_type === "manage_add") { if (list.value.find(v => v.id == msg.data.id)) { - return ElMessage.warning(msg.message || '该商品已存在') + return ElMessage.warning(msg.message || '该商品已存在'); } - const skuData = getProductDetails({ product_id: msg.data.product_id, sku_id: msg.data.sku_id }) - const newGoods = { ...skuData, ...msg.data } + const skuData = getProductDetails({ product_id: msg.data.product_id, sku_id: msg.data.sku_id }); + console.log('skuData', skuData); + const newGoods = { ...skuData, ...msg.data }; console.log('newGoods', newGoods) - list.value.push(newGoods) - return ElMessage.success(msg.message || '添加成功') - + list.value.push(newGoods); + return ElMessage.success(msg.message || '添加成功'); } + if (msg.operate_type === "manage_edit") { - const newCart = msg.data - const index = list.value.findIndex((item) => item.id === newCart.id) - const giftIndex = giftList.value.findIndex((item) => item.id === newCart.id) - const cartItem = list.value[index] || { is_gift: false }; - const giftItem = giftList.value[giftIndex]; + const newCart = msg.data; + const listIndex = list.value.findIndex(item => item.id === newCart.id); + const giftIndex = giftList.value.findIndex(item => item.id === newCart.id); - if (giftItem) { - //操作赠菜 - if (!newCart.is_gift) { - giftList.value.splice(giftIndex, 1) - list.value.push({ ...giftItem, ...newCart }) - selListIndex.value = -1 - } else { - giftList.value[giftIndex] = { ...giftItem, ...newCart } - } - } - if (cartItem) { - //操作非赠菜 - if (newCart.is_gift) { - list.value.splice(index, 1) - giftList.value.push({ ...cartItem, ...newCart }) - selListIndex.value = -1 - } else { - list.value[index] = { ...cartItem, ...newCart } - } - } - ElMessage.success(msg.message || '修改成功') - } - if (msg.operate_type === "manage_del") { - const cartId = Array.isArray(msg.data) ? msg.data[0].id : msg.data.id - const listIndex = list.value.findIndex((item) => item.id == cartId) - if (listIndex > -1) { - list.value.splice(listIndex, 1) - } - const giftIndex = giftList.value.findIndex((item) => item.id == cartId) if (giftIndex > -1) { - giftList.value.splice(giftIndex, 1) + if (!newCart.is_gift) { + giftList.value.splice(giftIndex, 1); + list.value.push({ ...giftList.value[giftIndex], ...newCart }); + cartOrder.value[newCart.id] = new Date().getTime(); + } else { + giftList.value[giftIndex] = { ...giftList.value[giftIndex], ...newCart }; + } } - return ElMessage.success(msg.message || '删除成功') + if (listIndex > -1) { + if (newCart.is_gift) { + list.value.splice(listIndex, 1); + giftList.value.push({ ...list.value[listIndex], ...newCart }); + delete cartOrder.value[newCart.id]; + } else { + list.value[listIndex] = { ...list.value[listIndex], ...newCart }; + } + } + ElMessage.success(msg.message || '修改成功'); } + + if (msg.operate_type === "manage_del") { + const cartId = Array.isArray(msg.data) ? msg.data[0].id : msg.data.id; + const listIndex = list.value.findIndex(item => item.id == cartId); + const giftIndex = giftList.value.findIndex(item => item.id == cartId); + + if (listIndex > -1) list.value.splice(listIndex, 1); + if (giftIndex > -1) giftList.value.splice(giftIndex, 1); + delete cartOrder.value[cartId]; + return ElMessage.success(msg.message || '删除成功'); + } + if (msg.operate_type === "manage_cleanup") { - nowCartsClear() - getOldOrder(msg.data.table_code) + nowCartsClear(); + getOldOrder(msg.data.table_code); } + if (msg.operate_type === "batch") { - concocatSocket({ ...$initParams, table_code: table_code.value }) + concocatSocket({ ...$initParams, table_code: table_code.value }); } + if (msg.operate_type === "product_update") { - console.log('商品更新') - init($initParams, oldOrder.value) + init($initParams, oldOrder.value); + } + + if (msg.type === "bc") { + msg.operate_type = 'manage_' + msg.operate_type; + concocatSocket(initParams); } }); } function disconnect() { - sendMessage('disconnect', undefined) + sendMessage('disconnect', undefined); } - const delArr = ['skuData', 'coverImg', 'specInfo', 'placeNum', 'update_time', 'create_time', 'packFee', 'memberPrice', 'type'] + const delArr = ['skuData', 'coverImg', 'specInfo', 'placeNum', 'update_time', 'create_time', 'packFee', 'memberPrice', 'type']; function sendMessage(operate_type: msgType, message: any) { - const msg = { ...message, operate_type: operate_type, table_code: table_code.value } + const msg = { ...message, operate_type, table_code: table_code.value }; for (let i in delArr) { - delete msg[delArr[i]] + delete msg[delArr[i]]; } - console.log('send msg', msg) WebSocketManager.sendMessage(msg); } + return { disconnect, dinnerType, @@ -714,7 +722,8 @@ export const useCartsStore = defineStore("carts", () => { dataReset, useVipPrice, changeUser, - packNum, packFee, + packNum, + packFee, isOldOrder, oldOrder, isCanSelectGroup, @@ -728,17 +737,32 @@ export const useCartsStore = defineStore("carts", () => { del, update, init, - changeNumber, isEmpty, - selCart, totalNumber, - changeSelCart, payMoney, - clear, yiyouhui, giftList, + changeNumber, + isEmpty, + selCart, + totalNumber, + changeSelCart, + payMoney, + clear, + yiyouhui, + giftList, changeTable, rotTable, getGoods, - setGoodsMap + setGoodsMap, + orderCostSummary, + // 暴露商家减免配置(供外部读取当前状态) + merchantReductionConfig, + // 暴露修改方法(供外部设置两种减免形式) + setMerchantFixedReduction, + setMerchantDiscountReduction, + clearMerchantReduction, + seatFeeConfig, + pointDeductionRule, + userPoints }; }); export function useCartsStoreHook() { return useCartsStore(store); -} +} \ No newline at end of file diff --git a/src/utils/goods.ts b/src/utils/goods.ts index e5f736f..7170b42 100644 --- a/src/utils/goods.ts +++ b/src/utils/goods.ts @@ -1,7 +1,15 @@ +import { BigNumber } from 'bignumber.js'; + +// 配置BigNumber精度 +BigNumber.set({ + DECIMAL_PLACES: 2, + ROUNDING_MODE: BigNumber.ROUND_DOWN // 向下取整,符合业务需求 +}); + /** * 购物车订单价格计算公共库 * 功能:覆盖订单全链路费用计算(商品价格、优惠券、积分、餐位费等),支持策略扩展 - * 小数处理:统一舍去小数点后两位(如 10.129 → 10.12,15.998 → 15.99) + * 小数处理:使用bignumber.js确保精度,统一舍去小数点后两位(如 10.129 → 10.12,15.998 → 15.99) * 扩展设计:优惠券/营销活动采用策略模式,新增类型仅需扩展策略,无需修改核心逻辑 * 关键规则: * - 所有优惠券均支持指定门槛商品(后端foods字段:空字符串=全部商品,ID字符串=指定商品ID) @@ -22,17 +30,20 @@ export enum GoodsType { PACKAGE = 'package'// 打包商品(如套餐/预打包商品,按普通商品逻辑处理,可扩展特殊规则) } -/** 优惠券计算结果类型 */ +/** 优惠券计算结果类型(新增细分字段) */ interface CouponResult { deductionAmount: number; // 抵扣金额 excludedProductIds: string[]; // 不适用商品ID列表(注意:是商品ID,非购物车ID) usedCoupon: Coupon | undefined; // 实际使用的优惠券 + productCouponDeduction: number; // 新增:商品优惠券抵扣(兑换券等) + fullCouponDeduction: number; // 新增:满减优惠券抵扣 } -/** 兑换券计算结果类型 */ +/** 兑换券计算结果类型(新增细分字段) */ interface ExchangeCalculationResult { deductionAmount: number; excludedProductIds: string[]; // 不适用商品ID列表(商品ID) + productCouponDeduction: number; // 新增:兑换券属于商品券,同步记录 } /** 优惠券类型枚举 */ @@ -193,10 +204,23 @@ export interface SeatFeeConfig { personCount: number; // 用餐人数(默认1) isEnabled: boolean; // 是否启用餐位费(默认false) } +/** 商家减免类型枚举 */ +export enum MerchantReductionType { + FIXED_AMOUNT = 'fixed_amount', // 固定金额减免(如直接减 10 元) + DISCOUNT_RATE = 'discount_rate' // 比例折扣减免(如打 9 折,即减免 10%) +} +/** 商家减免配置(新增,替代原单一金额字段) */ +export interface MerchantReductionConfig { + type: MerchantReductionType; // 减免类型(二选一) + fixedAmount?: number; // 固定减免金额(元,仅 FIXED_AMOUNT 生效,≥0) + discountRate?: number; // 折扣率(%,仅 DISCOUNT_RATE 生效,0-100,如 90 代表 9 折) +} /** 订单额外费用配置 */ export interface OrderExtraConfig { - merchantReduction: number; // 商家减免金额(元,默认0) + // merchantReduction: number; // 商家减免金额(元,默认0) + // 替换原单一金额字段,支持两种减免形式 + merchantReduction: MerchantReductionConfig; additionalFee: number; // 附加费(元,如余额充值、券包,默认0) pointDeductionRule: PointDeductionRule; // 积分抵扣规则 seatFeeConfig: SeatFeeConfig; // 餐位费配置 @@ -206,19 +230,27 @@ export interface OrderExtraConfig { 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; // 实际使用的积分 + goodsRealAmount: number; // 商品真实原价总和 + goodsOriginalAmount: number; // 商品原价总和 + goodsDiscountAmount: number; // 商品折扣金额 + couponDeductionAmount: number; // 优惠券总抵扣 + productCouponDeduction: number; // 商品优惠券抵扣 + fullCouponDeduction: number; // 满减优惠券抵扣 + pointDeductionAmount: number; // 积分抵扣金额 + seatFee: number; // 餐位费 + packFee: number; // 打包费 + // 新增:商家减免明细 + merchantReduction: { + type: MerchantReductionType; // 实际使用的减免类型 + originalConfig: MerchantReductionConfig; // 原始配置(便于前端展示) + actualAmount: number; // 实际减免金额(计算后的值,≥0) + }; + additionalFee: number; // 附加费 + finalPayAmount: number; // 最终实付金额 + couponUsed?: Coupon; // 实际使用的优惠券 + pointUsed: number; // 实际使用的积分 } // ============================ 2. 基础工具函数(核心修正:所有商品ID匹配用product_id) ============================ @@ -265,9 +297,8 @@ export function convertBackendCouponToToolCoupon( vipPriceShare: backendCoupon.vipPriceShare === 1, useType: backendCoupon.useType ? backendCoupon.useType.split(',') : [], isValid: isCouponInValidPeriod(backendCoupon, currentTime), - applicableProductIds: applicableProductIds + applicableProductIds: applicableProductIds, }; - // 5. 按券类型补充专属字段 switch (couponType) { case CouponType.FULL_REDUCTION: @@ -336,9 +367,6 @@ function isCouponAvailable( // 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; @@ -377,6 +405,7 @@ function isCouponInValidPeriod(backendCoupon: BackendCoupon, currentTime: Date): 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; } @@ -501,12 +530,13 @@ export function formatDiscountRate(backendDiscountRate?: number): number { } /** - * 统一小数处理:舍去小数点后两位(如 10.129 → 10.12,15.998 → 15.99) + * 统一小数处理:舍去小数点后两位以后的数字(解决浮点数精度偏差) + * 如 10.129 → 10.12,1.01 → 1.01,1.0100000001 → 1.01 * @param num 待处理数字 * @returns 处理后保留两位小数的数字 */ -export function truncateToTwoDecimals(num: number): number { - return Math.floor(num * 100) / 100; +export function truncateToTwoDecimals(num: number | string): number { + return new BigNumber(num).decimalPlaces(2, BigNumber.ROUND_DOWN).toNumber(); } /** @@ -539,7 +569,7 @@ export function calcMemberPrice( isMember: boolean, memberDiscountRate?: number ): number { - if (!isMember) return goods.salePrice; + if (!isMember) return truncateToTwoDecimals(goods.salePrice); // 优先级:SKU会员价 > 商品会员价 > 商品原价(无会员价时用会员折扣) const basePrice = goods.skuData?.memberPrice @@ -547,9 +577,13 @@ export function calcMemberPrice( ?? goods.salePrice; // 仅当无SKU会员价、无商品会员价时,才应用会员折扣率 - return memberDiscountRate && !goods.skuData?.memberPrice && !goods.memberPrice - ? truncateToTwoDecimals(basePrice * memberDiscountRate) - : basePrice; + if (memberDiscountRate && !goods.skuData?.memberPrice && !goods.memberPrice) { + return truncateToTwoDecimals( + new BigNumber(basePrice).multipliedBy(memberDiscountRate).toNumber() + ); + } + + return truncateToTwoDecimals(basePrice); } /** @@ -631,37 +665,39 @@ export function calcCouponThresholdAmount( 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; + let total = new BigNumber(0); - // 1. 基础金额:默认用商品原价(SKU原价优先) - const basePrice = goods.skuData?.salePrice ?? goods.salePrice; - let itemAmount = basePrice * availableNum; + for (const goods of eligibleGoods) { + const availableNum = Math.max(0, goods.number - (goods.returnNum || 0)); + if (availableNum <= 0) continue; - // 2. 处理「与会员价/会员折扣同享」规则:若开启,用会员价计算 - if (coupon.vipPriceShare) { - const memberPrice = calcMemberPrice(goods, config.isMember, config.memberDiscountRate); - itemAmount = memberPrice * availableNum; + // 1. 基础金额:默认用商品原价(SKU原价优先) + const basePrice = new BigNumber(goods.skuData?.salePrice ?? goods.salePrice); + let itemAmount = basePrice.multipliedBy(availableNum); + + // 2. 处理「与会员价/会员折扣同享」规则:若开启,用会员价计算 + if (coupon.vipPriceShare) { + const memberPrice = new BigNumber(calcMemberPrice(goods, config.isMember, config.memberDiscountRate)); + itemAmount = memberPrice.multipliedBy(availableNum); + } + + // 3. 处理「与限时折扣同享」规则:若开启,叠加限时折扣(按商品ID匹配活动) + if (coupon.discountShare) { + const activity = goods.activityInfo + ?? activities.find(act => + act.type === ActivityType.TIME_LIMIT_DISCOUNT + && (act.applicableProductIds || []).includes(String(goods.product_id)) // 核心修正:用商品ID匹配活动 + ); + + if (activity) { + itemAmount = itemAmount.multipliedBy(activity.discountRate); // 叠加限时折扣 } + } - // 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匹配活动 - ); + total = total.plus(itemAmount); + } - if (activity) { - itemAmount = itemAmount * activity.discountRate; // 叠加限时折扣 - } - } - - return total + itemAmount; - }, 0) - ); + return truncateToTwoDecimals(total.toNumber()); } // ============================ 3. 商品核心价格计算(核心修正:按商品ID匹配营销活动) ============================ @@ -685,25 +721,30 @@ export function calcSingleGoodsRealPrice( } // 2. 优先级2:会员价(含会员折扣率,SKU会员价优先) - const memberPrice = calcMemberPrice(goods, isMember, memberDiscountRate); + const memberPrice = new BigNumber(calcMemberPrice(goods, isMember, memberDiscountRate)); // 3. 优先级3:营销活动折扣(如限时折扣,需按商品ID匹配活动) const isActivityApplicable = activity ? (activity.applicableProductIds || []).includes(String(goods.product_id)) // 核心修正:用商品ID匹配活动 : false; if (!activity || !isActivityApplicable) { - return memberPrice; + return memberPrice.toNumber(); } // 处理活动与会员的同享/不同享逻辑 if (activity.vipPriceShare) { // 同享:会员价基础上叠加工活动折扣 - return truncateToTwoDecimals(memberPrice * activity.discountRate); + return truncateToTwoDecimals( + memberPrice.multipliedBy(activity.discountRate).toNumber() + ); } else { // 不同享:取会员价和活动价的最小值(活动价用SKU原价计算) - const basePriceForActivity = goods.skuData?.salePrice ?? goods.salePrice; - const activityPrice = truncateToTwoDecimals(basePriceForActivity * activity.discountRate); - return Math.min(memberPrice, activityPrice); + const basePriceForActivity = new BigNumber(goods.skuData?.salePrice ?? goods.salePrice); + const activityPrice = basePriceForActivity.multipliedBy(activity.discountRate); + + return truncateToTwoDecimals( + memberPrice.isLessThanOrEqualTo(activityPrice) ? memberPrice.toNumber() : activityPrice.toNumber() + ); } } @@ -713,13 +754,15 @@ export function calcSingleGoodsRealPrice( * @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) - ); + let total = new BigNumber(0); + + 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原价优先 + total = total.plus(basePrice.multipliedBy(availableNum)); + } + + return truncateToTwoDecimals(total.toNumber()); } /** @@ -734,21 +777,23 @@ export function calcGoodsRealAmount( 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; + let total = new BigNumber(0); - // 匹配商品参与的营销活动(按商品ID匹配,优先商品自身配置) - const activity = goods.activityInfo - ?? activities.find(act => - (act.applicableProductIds || []).includes(String(goods.product_id)) // 核心修正:用商品ID匹配活动 - ); + for (const goods of goodsList) { + const availableNum = Math.max(0, goods.number - (goods.returnNum || 0)); + if (availableNum <= 0) continue; - const realPrice = calcSingleGoodsRealPrice(goods, { ...config, activity }); - return total + realPrice * availableNum; - }, 0) - ); + // 匹配商品参与的营销活动(按商品ID匹配,优先商品自身配置) + const activity = goods.activityInfo + ?? activities.find(act => + (act.applicableProductIds || []).includes(String(goods.product_id)) // 核心修正:用商品ID匹配活动 + ); + + const realPrice = new BigNumber(calcSingleGoodsRealPrice(goods, { ...config, activity })); + total = total.plus(realPrice.multipliedBy(availableNum)); + } + + return truncateToTwoDecimals(total.toNumber()); } /** @@ -761,7 +806,11 @@ export function calcGoodsDiscountAmount( goodsOriginalAmount: number, goodsRealAmount: number ): number { - return truncateToTwoDecimals(Math.max(0, goodsOriginalAmount - goodsRealAmount)); + const original = new BigNumber(goodsOriginalAmount); + const real = new BigNumber(goodsRealAmount); + const discount = original.minus(real); + + return truncateToTwoDecimals(Math.max(0, discount.toNumber())); } // ============================ 4. 优惠券抵扣计算(策略模式,核心修正:按商品ID匹配) ============================ @@ -778,6 +827,8 @@ interface CouponCalculationStrategy { ): { deductionAmount: number; excludedProductIds: string[]; // 排除的商品ID列表(商品ID) + productCouponDeduction?: number; // 新增:当前券属于商品券时的抵扣金额 + fullCouponDeduction?: number; // 新增:当前券属于满减券时的抵扣金额 }; } @@ -787,35 +838,48 @@ class FullReductionStrategy implements CouponCalculationStrategy { coupon: FullReductionCoupon, goodsList: BaseCartItem[], config: any - ): { deductionAmount: number; excludedProductIds: string[] } { + ): { deductionAmount: number; excludedProductIds: string[]; fullCouponDeduction: number } { + // 1. 基础校验:优惠券不可用/门店不匹配 → 抵扣0 if (!coupon.available || !isStoreMatchByList(coupon.useShops, config.currentStoreId)) { - return { deductionAmount: 0, excludedProductIds: [] }; + return { deductionAmount: 0, excludedProductIds: [], fullCouponDeduction: 0 }; } // 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: [] }; + return { deductionAmount: 0, excludedProductIds: [], fullCouponDeduction: 0 }; } // 4. 按同享规则计算门槛金额(按商品ID匹配活动) - const thresholdAmount = calcCouponThresholdAmount( + const thresholdAmount = new BigNumber(calcCouponThresholdAmount( thresholdGoods, coupon, { isMember: config.isMember, memberDiscountRate: config.memberDiscountRate }, config.activities - ); + )); // 5. 满门槛则抵扣,否则0(不超过最大减免) - if (thresholdAmount < coupon.fullAmount) { - return { deductionAmount: 0, excludedProductIds: [] }; + if (thresholdAmount.isLessThan(coupon.fullAmount)) { + return { deductionAmount: 0, excludedProductIds: [], fullCouponDeduction: 0 }; } + const maxReduction = coupon.maxDiscountAmount ?? coupon.discountAmount; - const deductionAmount = truncateToTwoDecimals(Math.min(coupon.discountAmount, maxReduction)); - return { deductionAmount, excludedProductIds: [] }; + const deductionAmount = truncateToTwoDecimals( + new BigNumber(coupon.discountAmount).isLessThan(maxReduction) + ? coupon.discountAmount + : maxReduction + ); + + // 满减券计入满减优惠券抵扣 + return { + deductionAmount, + excludedProductIds: [], + fullCouponDeduction: deductionAmount + }; } } @@ -825,10 +889,10 @@ class DiscountStrategy implements CouponCalculationStrategy { coupon: DiscountCoupon, goodsList: BaseCartItem[], config: any - ): { deductionAmount: number; excludedProductIds: string[] } { + ): { deductionAmount: number; excludedProductIds: string[]; productCouponDeduction: number } { // 1. 基础校验:优惠券不可用/门店不匹配 → 抵扣0 if (!coupon.available || !isStoreMatchByList(coupon.useShops, config.currentStoreId)) { - return { deductionAmount: 0, excludedProductIds: [] }; + return { deductionAmount: 0, excludedProductIds: [], productCouponDeduction: 0 }; } // 2. 第一步:排除临时菜/赠菜/已抵扣商品(基础合格商品,按商品ID排除) @@ -836,21 +900,31 @@ class DiscountStrategy implements CouponCalculationStrategy { // 3. 第二步:按商品ID筛选门槛商品(匹配applicableProductIds) const thresholdGoods = filterThresholdGoods(baseEligibleGoods, coupon.applicableProductIds); if (thresholdGoods.length === 0) { - return { deductionAmount: 0, excludedProductIds: [] }; + return { deductionAmount: 0, excludedProductIds: [], productCouponDeduction: 0 }; } // 4. 按同享规则计算折扣基数(按商品ID匹配活动) - const discountBaseAmount = calcCouponThresholdAmount( + const discountBaseAmount = new BigNumber(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: [] }; + const discountAmount = discountBaseAmount.multipliedBy(new BigNumber(1).minus(coupon.discountRate)); + const deductionAmount = truncateToTwoDecimals( + discountAmount.isLessThan(coupon.maxDiscountAmount) + ? discountAmount.toNumber() + : coupon.maxDiscountAmount + ); + + // 折扣券计入商品优惠券抵扣(可根据业务调整归类) + return { + deductionAmount, + excludedProductIds: [], + productCouponDeduction: deductionAmount + }; } } @@ -860,13 +934,13 @@ class SecondHalfPriceStrategy implements CouponCalculationStrategy { coupon: SecondHalfPriceCoupon, goodsList: BaseCartItem[], config: any - ): { deductionAmount: number; excludedProductIds: string[] } { + ): { deductionAmount: number; excludedProductIds: string[]; productCouponDeduction: number } { // 1. 基础校验:优惠券不可用/门店不匹配 → 抵扣0 if (!coupon.available || !isStoreMatchByList(coupon.useShops, config.currentStoreId)) { - return { deductionAmount: 0, excludedProductIds: [] }; + return { deductionAmount: 0, excludedProductIds: [], productCouponDeduction: 0 }; } - let totalDeduction = 0; + let totalDeduction = new BigNumber(0); const excludedProductIds: string[] = []; // 存储排除的商品ID(商品ID) // 2. 第一步:排除临时菜/赠菜/已抵扣商品(基础合格商品,按商品ID排除) @@ -874,7 +948,7 @@ class SecondHalfPriceStrategy implements CouponCalculationStrategy { // 3. 第二步:按商品ID筛选门槛商品(匹配applicableProductIds) const thresholdGoods = filterThresholdGoods(baseEligibleGoods, coupon.applicableProductIds); if (thresholdGoods.length === 0) { - return { deductionAmount: 0, excludedProductIds: [] }; + return { deductionAmount: 0, excludedProductIds: [], productCouponDeduction: 0 }; } // 按商品ID分组(避免同商品多次处理,用商品ID作为分组key) @@ -905,20 +979,23 @@ class SecondHalfPriceStrategy implements CouponCalculationStrategy { const activity = config.activities.find((act: ActivityConfig) => (act.applicableProductIds || []).includes(productIdStr) // 按商品ID匹配活动 ); - const realPrice = calcSingleGoodsRealPrice(sampleGood, { + const realPrice = new BigNumber(calcSingleGoodsRealPrice(sampleGood, { isMember: config.isMember, memberDiscountRate: config.memberDiscountRate, activity - }); + })); // 累计抵扣金额并标记已优惠商品(记录商品ID) - totalDeduction += realPrice * 0.5 * discountCount; + totalDeduction = totalDeduction.plus(realPrice.multipliedBy(0.5).multipliedBy(discountCount)); excludedProductIds.push(productIdStr); } + const deductionAmount = truncateToTwoDecimals(totalDeduction.toNumber()); + // 第二件半价券计入商品优惠券抵扣 return { - deductionAmount: truncateToTwoDecimals(totalDeduction), - excludedProductIds + deductionAmount, + excludedProductIds, + productCouponDeduction: deductionAmount }; } } @@ -929,13 +1006,13 @@ class BuyOneGetOneStrategy implements CouponCalculationStrategy { coupon: BuyOneGetOneCoupon, goodsList: BaseCartItem[], config: any - ): { deductionAmount: number; excludedProductIds: string[] } { + ): { deductionAmount: number; excludedProductIds: string[]; productCouponDeduction: number } { // 1. 基础校验:优惠券不可用/门店不匹配 → 抵扣0 if (!coupon.available || !isStoreMatchByList(coupon.useShops, config.currentStoreId)) { - return { deductionAmount: 0, excludedProductIds: [] }; + return { deductionAmount: 0, excludedProductIds: [], productCouponDeduction: 0 }; } - let totalDeduction = 0; + let totalDeduction = new BigNumber(0); const excludedProductIds: string[] = []; // 存储排除的商品ID(商品ID) // 2. 第一步:排除临时菜/赠菜/已抵扣商品(基础合格商品,按商品ID排除) @@ -943,7 +1020,7 @@ class BuyOneGetOneStrategy implements CouponCalculationStrategy { // 3. 第二步:按商品ID筛选门槛商品(匹配applicableProductIds) const thresholdGoods = filterThresholdGoods(baseEligibleGoods, coupon.applicableProductIds); if (thresholdGoods.length === 0) { - return { deductionAmount: 0, excludedProductIds: [] }; + return { deductionAmount: 0, excludedProductIds: [], productCouponDeduction: 0 }; } // 按商品ID分组(用商品ID作为分组key) @@ -971,20 +1048,23 @@ class BuyOneGetOneStrategy implements CouponCalculationStrategy { const activity = config.activities.find((act: ActivityConfig) => (act.applicableProductIds || []).includes(productIdStr) // 按商品ID匹配活动 ); - const realPrice = calcSingleGoodsRealPrice(sampleGood, { + const realPrice = new BigNumber(calcSingleGoodsRealPrice(sampleGood, { isMember: config.isMember, memberDiscountRate: config.memberDiscountRate, activity - }); + })); // 累计抵扣金额(送1件=减免1件价格)并标记商品ID - totalDeduction += realPrice * 1 * discountCount; + totalDeduction = totalDeduction.plus(realPrice.multipliedBy(1).multipliedBy(discountCount)); excludedProductIds.push(productIdStr); } + const deductionAmount = truncateToTwoDecimals(totalDeduction.toNumber()); + // 买一送一券计入商品优惠券抵扣 return { - deductionAmount: truncateToTwoDecimals(totalDeduction), - excludedProductIds + deductionAmount, + excludedProductIds, + productCouponDeduction: deductionAmount }; } } @@ -995,10 +1075,10 @@ class ExchangeCouponStrategy implements CouponCalculationStrategy { coupon: ExchangeCoupon, goodsList: BaseCartItem[], config: any - ): { deductionAmount: number; excludedProductIds: string[] } { + ): { deductionAmount: number; excludedProductIds: string[]; productCouponDeduction: number } { // 1. 基础校验:优惠券不可用/门店不匹配 → 抵扣0 if (!coupon.available || !isStoreMatchByList(coupon.useShops, config.currentStoreId)) { - return { deductionAmount: 0, excludedProductIds: [] }; + return { deductionAmount: 0, excludedProductIds: [], productCouponDeduction: 0 }; } // 2. 第一步:排除临时菜/赠菜/已抵扣商品(基础合格商品,按商品ID排除) @@ -1006,13 +1086,13 @@ class ExchangeCouponStrategy implements CouponCalculationStrategy { // 3. 第二步:按商品ID筛选门槛商品(匹配applicableProductIds) const thresholdGoods = filterThresholdGoods(baseEligibleGoods, coupon.applicableProductIds); if (thresholdGoods.length === 0) { - return { deductionAmount: 0, excludedProductIds: [] }; + return { deductionAmount: 0, excludedProductIds: [], productCouponDeduction: 0 }; } // 按规则排序商品(按价格/数量/加入顺序) const sortedGoods = sortGoodsForCoupon(thresholdGoods, coupon.sortRule, config.cartOrder); let remainingCount = coupon.deductCount; - let totalDeduction = 0; + let totalDeduction = new BigNumber(0); const excludedProductIds: string[] = []; // 存储排除的商品ID(商品ID) // 计算兑换抵扣金额(按商品ID累计,避免重复抵扣) @@ -1030,22 +1110,25 @@ class ExchangeCouponStrategy implements CouponCalculationStrategy { const activity = config.activities.find((act: ActivityConfig) => (act.applicableProductIds || []).includes(productIdStr) // 按商品ID匹配活动 ); - const realPrice = calcSingleGoodsRealPrice(goods, { + const realPrice = new BigNumber(calcSingleGoodsRealPrice(goods, { isMember: config.isMember, memberDiscountRate: config.memberDiscountRate, activity - }); + })); // 累计抵扣金额并标记商品 - totalDeduction += realPrice * deductCount; + totalDeduction = totalDeduction.plus(realPrice.multipliedBy(deductCount)); excludedProductIds.push(productIdStr); usedProductIds.add(productIdStr); remainingCount -= deductCount; } + const deductionAmount = truncateToTwoDecimals(totalDeduction.toNumber()); + // 商品兑换券计入商品优惠券抵扣 return { - deductionAmount: truncateToTwoDecimals(totalDeduction), - excludedProductIds + deductionAmount, + excludedProductIds, + productCouponDeduction: deductionAmount }; } } @@ -1082,11 +1165,11 @@ function getCouponStrategy(couponType: CouponType): CouponCalculationStrategy { } /** - * 计算优惠券抵扣金额(处理互斥逻辑,选择最优优惠券,按商品ID排除) + * 计算优惠券抵扣金额(处理互斥逻辑,选择最优优惠券,按商品ID排除,新增细分统计) * @param backendCoupons 后端优惠券列表 * @param goodsList 商品列表 * @param config 订单配置(含就餐类型) - * @returns 最优优惠券的抵扣结果 + * @returns 最优优惠券的抵扣结果(含商品券/满减券细分) */ export function calcCouponDeduction( backendCoupons: BackendCoupon[], @@ -1099,6 +1182,8 @@ export function calcCouponDeduction( } ): { deductionAmount: number; + productCouponDeduction: number; // 新增:商品优惠券抵扣(兑换券/折扣券/买一送一等) + fullCouponDeduction: number; // 新增:满减优惠券抵扣 usedCoupon?: Coupon; excludedProductIds: string[]; // 排除的商品ID列表(商品ID) } { @@ -1111,20 +1196,26 @@ export function calcCouponDeduction( config.currentTime )) .filter(Boolean) as Coupon[]; - if (toolCoupons.length === 0) { - return { deductionAmount: 0, excludedProductIds: [] }; + return { + deductionAmount: 0, + productCouponDeduction: 0, + fullCouponDeduction: 0, + excludedProductIds: [] + }; } // 2. 优惠券互斥逻辑:兑换券与其他券互斥,优先选最优 const exchangeCoupons = toolCoupons.filter(c => c.type === CouponType.EXCHANGE); const nonExchangeCoupons = toolCoupons.filter(c => c.type !== CouponType.EXCHANGE); - // 3. 计算非兑换券最优抵扣(传递已抵扣商品ID,避免重复) + // 3. 计算非兑换券最优抵扣(传递已抵扣商品ID,避免重复,统计细分字段) let nonExchangeResult: CouponResult = { deductionAmount: 0, excludedProductIds: [], - usedCoupon: undefined + usedCoupon: undefined, + productCouponDeduction: 0, + fullCouponDeduction: 0 }; if (nonExchangeCoupons.length > 0) { nonExchangeResult = nonExchangeCoupons.reduce((best, coupon) => { @@ -1134,15 +1225,26 @@ export function calcCouponDeduction( excludedProductIds: best.excludedProductIds // 传递已排除的商品ID(商品ID) }); const currentResult: CouponResult = { - ...result, - usedCoupon: coupon + deductionAmount: result.deductionAmount, + excludedProductIds: result.excludedProductIds, + usedCoupon: coupon, + // 按策略返回的字段赋值细分抵扣 + productCouponDeduction: result.productCouponDeduction || 0, + fullCouponDeduction: result.fullCouponDeduction || 0 }; - return currentResult.deductionAmount > best.deductionAmount ? currentResult : best; + // 按总抵扣金额选择最优 + return new BigNumber(currentResult.deductionAmount).isGreaterThan(best.deductionAmount) + ? currentResult + : best; }, nonExchangeResult); } - // 4. 计算兑换券抵扣(排除非兑换券已抵扣的商品ID) - let exchangeResult: ExchangeCalculationResult = { deductionAmount: 0, excludedProductIds: [] }; + // 4. 计算兑换券抵扣(排除非兑换券已抵扣的商品ID,统计商品券细分) + let exchangeResult: ExchangeCalculationResult = { + deductionAmount: 0, + excludedProductIds: [], + productCouponDeduction: 0 + }; if (exchangeCoupons.length > 0) { exchangeResult = exchangeCoupons.reduce((best, coupon) => { const strategy = getCouponStrategy(coupon.type); @@ -1150,14 +1252,25 @@ export function calcCouponDeduction( ...config, excludedProductIds: [...nonExchangeResult.excludedProductIds, ...best.excludedProductIds] // 合并排除的商品ID }); - return result.deductionAmount > best.deductionAmount ? result : best; + return new BigNumber(result.deductionAmount).isGreaterThan(best.deductionAmount) + ? { + deductionAmount: result.deductionAmount, + excludedProductIds: result.excludedProductIds, + productCouponDeduction: result.productCouponDeduction || 0 // 兑换券属于商品券 + } + : best; }, exchangeResult); } // 5. 汇总结果:兑换券与非兑换券不可同时使用,取抵扣金额大的 - const isExchangeBetter = exchangeResult.deductionAmount > nonExchangeResult.deductionAmount; + const exchangeBn = new BigNumber(exchangeResult.deductionAmount); + const nonExchangeBn = new BigNumber(nonExchangeResult.deductionAmount); + const isExchangeBetter = exchangeBn.isGreaterThan(nonExchangeBn); + return { deductionAmount: truncateToTwoDecimals(isExchangeBetter ? exchangeResult.deductionAmount : nonExchangeResult.deductionAmount), + productCouponDeduction: isExchangeBetter ? exchangeResult.productCouponDeduction : nonExchangeResult.productCouponDeduction, + fullCouponDeduction: isExchangeBetter ? 0 : nonExchangeResult.fullCouponDeduction, // 兑换券与满减券互斥,满减券抵扣置0 usedCoupon: isExchangeBetter ? undefined : nonExchangeResult.usedCoupon, excludedProductIds: isExchangeBetter ? exchangeResult.excludedProductIds : nonExchangeResult.excludedProductIds }; @@ -1174,22 +1287,24 @@ 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; + let total = new BigNumber(0); - // 计算单个商品打包数量(外卖全打包,堂食按配置,称重商品≤1) - let packNum = dinnerType === 'take-out' - ? availableNum - : (goods.packNumber || 0); - if (goods.product_type === GoodsType.WEIGHT) { - packNum = Math.min(packNum, 1); - } + for (const goods of goodsList) { + const availableNum = Math.max(0, goods.number - (goods.returnNum || 0)); + if (availableNum === 0) continue; - return total + (goods.packFee || 0) * packNum; - }, 0) - ); + // 计算单个商品打包数量(外卖全打包,堂食按配置,称重商品≤1) + let packNum = dinnerType === 'take-out' + ? availableNum + : (goods.packNumber || 0); + if (goods.product_type === GoodsType.WEIGHT) { + packNum = Math.min(packNum, 1); + } + + total = total.plus(new BigNumber(goods.packFee || 0).multipliedBy(packNum)); + } + + return truncateToTwoDecimals(total.toNumber()); } /** @@ -1198,9 +1313,11 @@ export function calcTotalPackFee( * @returns 餐位费(元,未启用则0) */ export function calcSeatFee(config: SeatFeeConfig): number { - if (!config.isEnabled) return 0; + if (!config.isEnabled || config.personCount == 0) return 0; const personCount = Math.max(1, config.personCount); // 至少1人 - return truncateToTwoDecimals(config.pricePerPerson * personCount); + return truncateToTwoDecimals( + new BigNumber(config.pricePerPerson).multipliedBy(personCount).toNumber() + ); } /** @@ -1222,23 +1339,30 @@ export function calcPointDeduction( return { deductionAmount: 0, usedPoints: 0 }; } + const userPointsBn = new BigNumber(userPoints); + const pointsPerYuanBn = new BigNumber(rule.pointsPerYuan); + const maxLimitBn = new BigNumber(maxDeductionLimit); + // 最大可抵扣金额(积分可抵金额 vs 规则最大 vs 订单上限) - const maxDeductByPoints = truncateToTwoDecimals(userPoints / rule.pointsPerYuan); - const maxDeductAmount = Math.min( - maxDeductByPoints, - rule.maxDeductionAmount ?? Infinity, - maxDeductionLimit - ); + const maxDeductByPoints = userPointsBn.dividedBy(pointsPerYuanBn); + const maxDeductAmount = maxDeductByPoints + .isLessThan(rule.maxDeductionAmount ?? Infinity) + ? maxDeductByPoints + : new BigNumber(rule.maxDeductionAmount || Infinity) + .isLessThan(maxLimitBn) + ? maxDeductByPoints + : maxLimitBn; // 实际使用积分 = 抵扣金额 * 积分兑换比例 - const usedPoints = truncateToTwoDecimals(maxDeductAmount * rule.pointsPerYuan); + const usedPoints = maxDeductAmount.multipliedBy(pointsPerYuanBn); + return { - deductionAmount: maxDeductAmount, - usedPoints: Math.min(usedPoints, userPoints) // 避免积分超扣 + deductionAmount: truncateToTwoDecimals(maxDeductAmount.toNumber()), + usedPoints: truncateToTwoDecimals(Math.min(usedPoints.toNumber(), userPoints)) // 避免积分超扣 }; } -// ============================ 6. 订单总费用汇总与实付金额计算(核心入口,逻辑不变) ============================ +// ============================ 6. 订单总费用汇总与实付金额计算(核心入口,补充细分字段) ============================ /** * 计算订单所有费用子项并汇总(核心入口函数) * @param goodsList 购物车商品列表 @@ -1248,7 +1372,7 @@ export function calcPointDeduction( * @param config 订单额外配置(会员、积分、餐位费等) * @param cartOrder 商品加入购物车顺序(key=购物车ID,value=时间戳) * @param currentTime 当前时间(用于优惠券有效期判断) - * @returns 订单费用汇总(含所有子项和实付金额) + * @returns 订单费用汇总(含所有子项和实付金额,新增商品券/满减券细分) */ export function calculateOrderCostSummary( goodsList: BaseCartItem[], @@ -1259,7 +1383,7 @@ export function calculateOrderCostSummary( cartOrder: Record = {}, currentTime: Date = new Date() ): OrderCostSummary { - // 1. 基础费用计算 + // 1. 基础费用计算(原有逻辑不变) const goodsOriginalAmount = calcGoodsOriginalAmount(goodsList); const goodsRealAmount = calcGoodsRealAmount(goodsList, { isMember: config.isMember, @@ -1267,8 +1391,14 @@ export function calculateOrderCostSummary( }, activities); const goodsDiscountAmount = calcGoodsDiscountAmount(goodsOriginalAmount, goodsRealAmount); - // 2. 优惠券抵扣(传递就餐类型用于可用性判断) - const { deductionAmount: couponDeductionAmount, usedCoupon, excludedProductIds } = calcCouponDeduction( + // 2. 优惠券抵扣(原有逻辑不变) + const { + deductionAmount: couponDeductionAmount, + usedCoupon, + excludedProductIds, + productCouponDeduction, + fullCouponDeduction + } = calcCouponDeduction( backendCoupons, goodsList, { @@ -1282,47 +1412,103 @@ export function calculateOrderCostSummary( } ); - // 3. 其他费用计算 + // 3. 其他费用计算(原有逻辑不变) const packFee = calcTotalPackFee(goodsList, dinnerType); const seatFee = calcSeatFee(config.seatFeeConfig); - // 积分抵扣上限:商品实际价 - 优惠券抵扣(避免负抵扣) + const additionalFee = Math.max(0, config.additionalFee); + + // 4. 积分抵扣(原有逻辑不变,先于商家减免计算) const maxPointDeductionLimit = Math.max(0, goodsRealAmount - couponDeductionAmount); - const { deductionAmount: pointDeductionAmount, usedPoints } = calcPointDeduction( + 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); + // ============================ 新增:商家减免计算(支持两种形式) ============================ + // 商家减免计算时机:在商品折扣、优惠券、积分抵扣之后(避免过度减免) + const merchantReductionConfig = config.merchantReduction; + let merchantReductionActualAmount = 0; + // 计算商家减免的可抵扣上限:商品实际金额 - 优惠券 - 积分(避免减免后为负) + const maxMerchantReductionLimit = new BigNumber(goodsRealAmount) + .minus(couponDeductionAmount) + .minus(pointDeductionAmount) + .isGreaterThan(0) + ? new BigNumber(goodsRealAmount) + .minus(couponDeductionAmount) + .minus(pointDeductionAmount) + : new BigNumber(0); + + switch (merchantReductionConfig.type) { + case MerchantReductionType.FIXED_AMOUNT: + // 固定金额减免:取配置金额与上限的最小值,且不小于0 + const fixedAmount = new BigNumber(merchantReductionConfig.fixedAmount || 0); + merchantReductionActualAmount = fixedAmount + .isLessThanOrEqualTo(maxMerchantReductionLimit) + ? fixedAmount.toNumber() + : maxMerchantReductionLimit.toNumber(); + merchantReductionActualAmount = Math.max(0, merchantReductionActualAmount); + break; + + case MerchantReductionType.DISCOUNT_RATE: + // 比例折扣减免:先校验折扣率(0-100%),再按比例计算减免金额 + const validDiscountRate = Math.max(0, Math.min(100, merchantReductionConfig.discountRate || 0)); + // 折扣率转小数(如 90% → 0.9),减免金额 = 可抵扣上限 * (1 - 折扣率) + merchantReductionActualAmount = maxMerchantReductionLimit + .multipliedBy(new BigNumber(1).minus(validDiscountRate / 100)) + .toNumber(); + // 确保减免金额不超过上限且非负 + merchantReductionActualAmount = Math.min( + merchantReductionActualAmount, + maxMerchantReductionLimit.toNumber() + ); + break; + } + + // 5. 最终实付金额计算(整合所有费用) + const finalPayAmount = new BigNumber(goodsOriginalAmount) // 商品原价总和 + .minus(goodsDiscountAmount) // 减去商品折扣 + .minus(couponDeductionAmount) // 减去优惠券抵扣 + .minus(pointDeductionAmount) // 减去积分抵扣 + .minus(merchantReductionActualAmount) // 减去商家实际减免金额 + .plus(seatFee) // 加上餐位费(不参与减免) + .plus(packFee) // 加上打包费(不参与减免) + .plus(additionalFee); // 加上附加费 + + const finalPayAmountNonNegative = Math.max(0, finalPayAmount.toNumber()); + + // 6. 返回完整费用汇总(包含商家减免明细) return { + goodsRealAmount, goodsOriginalAmount, goodsDiscountAmount, couponDeductionAmount, + productCouponDeduction: truncateToTwoDecimals(productCouponDeduction || 0), + fullCouponDeduction: truncateToTwoDecimals(fullCouponDeduction || 0), pointDeductionAmount, seatFee, packFee, - merchantReductionAmount, + // 商家减免明细(含类型、原始配置、实际金额) + merchantReduction: { + type: merchantReductionConfig.type, + originalConfig: merchantReductionConfig, + actualAmount: truncateToTwoDecimals(merchantReductionActualAmount) + }, additionalFee, - finalPayAmount: finalPayAmountNonNegative, + finalPayAmount: truncateToTwoDecimals(finalPayAmountNonNegative), couponUsed: usedCoupon, pointUsed: usedPoints }; } +export function isWeightGoods(goods: BaseCartItem): boolean { + return goods.product_type === GoodsType.WEIGHT; +} + // ============================ 7. 对外暴露工具库 ============================ export const OrderPriceCalculator = { // 基础工具 @@ -1331,6 +1517,7 @@ export const OrderPriceCalculator = { isGiftGoods, formatDiscountRate, filterThresholdGoods, + isWeightGoods, // 优惠券转换 convertBackendCouponToToolCoupon, // 商品价格计算 diff --git a/src/utils/test.ts b/src/utils/test.ts index d2a5b93..33d6b52 100644 --- a/src/utils/test.ts +++ b/src/utils/test.ts @@ -161,6 +161,7 @@ const testBackendCoupons: BackendCoupon[] = [ { id: 1, title: "满减券", + status: 1, couponType: 1, fullAmount: 2, discountAmount: 1, @@ -172,6 +173,9 @@ const testBackendCoupons: BackendCoupon[] = [ validEndTime: '2025-09-30 16:00:00', useDays: '周一,周二,周三,周四,周五', useTimeType: 'all', + shopId: 101, + createTime: '2025-09-16 13:30:50', + validDays: 2 }, ]; const testOrderConfig = { @@ -192,7 +196,7 @@ const testOrderConfig = { additionalFee: 0, }; const testActivities: ActivityConfig[] = []; -const testCurrentTime = new Date("2024-06-01 12:00:00"); +const testCurrentTime = new Date(); // 调用函数(此时类型完全匹配) const result = OrderPriceCalculator.calculateOrderCostSummary( diff --git a/src/views/marketing_center/components/headerCard.vue b/src/views/marketing_center/components/headerCard.vue index 0233eea..ce69abe 100644 --- a/src/views/marketing_center/components/headerCard.vue +++ b/src/views/marketing_center/components/headerCard.vue @@ -10,7 +10,7 @@
- +
diff --git a/src/views/marketing_center/super_vip/components/dialog-plans.vue b/src/views/marketing_center/super_vip/components/dialog-plans.vue index 3a1cd0f..8ccc00a 100644 --- a/src/views/marketing_center/super_vip/components/dialog-plans.vue +++ b/src/views/marketing_center/super_vip/components/dialog-plans.vue @@ -8,7 +8,7 @@ - + @@ -117,6 +117,9 @@ function submit() { ElMessage.error("请选择会员周期"); return; } + if (!form.value.circleUnit) { + ElMessage.error("请选择会员周期单位"); + } const ispass = form.value.couponList.every((item) => item.num && item.coupon.id); if (!ispass) { ElMessage.error("请选择优惠券并输入数量"); diff --git a/src/views/marketing_center/super_vip/index.vue b/src/views/marketing_center/super_vip/index.vue index f9a15f5..6ac74cd 100644 --- a/src/views/marketing_center/super_vip/index.vue +++ b/src/views/marketing_center/super_vip/index.vue @@ -5,7 +5,7 @@ intro="用户会员管理设置" icon="super_vip" showSwitch - v-model:isOpen="isOpenSuperVip" + v-model:isOpen="basicForm.isOpen" > @@ -68,6 +68,10 @@ style="margin-left: 20px" v-if="item.label != '绑定手机号' && item.checked" v-model="item.value" + :step="item.step" + :precision="item.precision || 0" + :step-strictly="item.stepStrictly || false" + :min="item.min || 0" /> @@ -96,8 +100,8 @@ - 所有支付方式 - 仅余额支付 + + @@ -113,23 +117,31 @@

升级规则

-
- 每消费1元获得 - - 成长值 -
-
- 每充值1元获得 - - 成长值 +
+
+
+ 每消费1元获得 + + 成长值 +
+
+ 每充值1元获得 + + 成长值 +
+
+ + + *两个条件必选有一条是大于0的数值 +
@@ -152,6 +164,7 @@ style="margin-top: 20px" @tab-remove="removeLevel" @tab-add="addLevel" + @tab-change="levelTabChange" editable > - + - - + - - + @@ -210,8 +229,11 @@ style="margin-top: 10px" class="color-666 flex" > - 每消耗 - 每消费 +
- + - +
已禁用 -->
- +
赠送积分 - v.checked) + .map((v) => { + return { + code: v.code, + value: v.value, + }; + }); + if ( + basicForm.openType == "CONDITION" && + (!data.conditionList || data.conditionList.length <= 0) + ) { + return ElMessage.error("请选择成为会员条件"); + } + if (basicForm.costReward <= 0 && basicForm.rechargeReward <= 0) { + return ElMessage.error("获取成长值升级规则两个条件必选有一条是大于0的数值"); + } data.conditionList = conditionLists.value .filter((v) => v.checked) .map((v) => { @@ -404,11 +471,6 @@ const levels = ref([]); // 当前选中的会员等级 const selectedLevel = ref(null); -// 优惠券列表 -const couponList = ref([ - { id: 1, name: "满100减10" }, - { id: 2, name: "满200减30" }, -]); let activeLevelId = ref(null); // 添加会员等级 function addLevel() { @@ -425,15 +487,16 @@ function addLevel() { const newLevel = { name, experienceValue: 0, - discount: 1, + discount: 100, logo: "", costRewardPoints: 1, - isCostRewardPoints: 1, + isCostRewardPoints: 0, isCycleReward: 0, cycleTime: 1, cycleUnit: "月", cycleRewardPoints: 1, cycleRewardCouponList: [], + remark: "", }; console.log(newLevel); levels.value.push(newLevel); @@ -476,10 +539,36 @@ async function removeLevel(index) { } // 保存会员等级 async function saveLevel(level) { - const isPass = level.cycleRewardCouponList.every((item) => item.num && item.coupon.id); - if (!isPass) { - ElMessage.error("请选择优惠券并输入数量"); - return; + if (level.isCycleReward) { + if (level.cycleRewardCouponList && level.cycleRewardCouponList.length) { + const isPass = (level.cycleRewardCouponList || []).every( + (item) => item.num && item.coupon.id + ); + if (!isPass) { + ElMessage.error("请选择优惠券并输入数量"); + return; + } + } + if ( + (!level.cycleRewardCouponList || level.cycleRewardCouponList.length <= 0) && + level.cycleRewardPoints <= 0 + ) { + ElMessage.error("赠送积分和送优惠券必须填充一项"); + return; + } + } + + if (level.name.trim() === "") { + return ElMessage.error("请输入会员标题"); + } + if (level.experienceValue === "") { + return ElMessage.error("请输入所需成长值"); + } + if (level.discount === "") { + return ElMessage.error("请输入会员折扣"); + } + if (level.remark.trim() === "") { + return ElMessage.error("请输入等级说明"); } const res = level.id ? await memberApi.levelEdit(level) : await memberApi.levelAdd(level); if (res) { @@ -509,6 +598,8 @@ async function init() { }); memberApi.getConfig().then((res) => { Object.assign(basicForm, res); + res.conditionList = res.conditionList || []; + res.configList = res.configList || []; conditionLists.value = conditionLists.value.map((v) => { const findItem = res.conditionList.find((cond) => cond.code == v.code); if (findItem) { @@ -538,10 +629,19 @@ function totalCount(arr) { return total + item.num * 1; }, 0); } +// +function levelExperienceValueMin(index, level) { + if (index == 0) { + return 0; + } + return levels.value[index - 1].experienceValue + 1; +} //返回 function close() { router.back(); } +// +function levelTabChange(index) {} \ No newline at end of file diff --git a/src/views/tool/Instead/components/popup-coupon.vue b/src/views/tool/Instead/components/popup-coupon.vue index cce0b3a..bb1b7f6 100644 --- a/src/views/tool/Instead/components/popup-coupon.vue +++ b/src/views/tool/Instead/components/popup-coupon.vue @@ -44,7 +44,7 @@ - + v.type == 1 && orderPrice.value * 1 >= v.fullAmount * 1 ); quans.value.productCoupon = res - .filter((v) => v.type == 2 && canDikouGoodsArr.find((goods) => v.proId == goods.productId)) + .filter((v) => { + //是否抵扣全部商品 + const isDikouAll = v.useFoods.length === 0; + //是否包含抵扣商品 + const isDikou = v.useFoods.some((v) => v.id == goods.value.id); + return v.type == 2 && (isDikouAll || isDikou); + }) .map((v) => { const findGoods = goodsArr.find((goods) => goods.productId == v.proId); return { diff --git a/src/views/tool/Instead/index.vue b/src/views/tool/Instead/index.vue index 13004a7..a93fd50 100644 --- a/src/views/tool/Instead/index.vue +++ b/src/views/tool/Instead/index.vue @@ -882,6 +882,12 @@ watch( } } ); +watch( + () => perpole.value, + (newval) => { + carts.seatFeeConfig.personCount = newval; + } +);