From 1c6739532a5ac6b0797038b810853b4989fab54a Mon Sep 17 00:00:00 2001 From: YeMingfei666 <1619116647@qq.com> Date: Wed, 15 Oct 2025 10:32:25 +0800 Subject: [PATCH 1/9] =?UTF-8?q?fix:=20=E5=B7=B2=E9=80=89=E5=88=B8=E5=88=B8?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E6=98=BE=E7=A4=BA=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/tool/Instead/components/order.vue | 25 +++++++++------------ 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/views/tool/Instead/components/order.vue b/src/views/tool/Instead/components/order.vue index 63b2f62..e46bcb2 100644 --- a/src/views/tool/Instead/components/order.vue +++ b/src/views/tool/Instead/components/order.vue @@ -88,7 +88,7 @@ @@ -166,6 +166,12 @@ }} +
+ 满减活动 + + -¥{{ carts.orderCostSummary.fullReduction.actualAmount }} + +
商品优惠券 -¥{{ productCouponDiscountAmount }} @@ -258,9 +264,7 @@ const refCoupon = ref(); let quansSelArr = ref([]); function openCoupon() { //商品订单金额 - const price = new BigNumber(carts.orderCostSummary.goodsOriginalAmount) - .minus(carts.orderCostSummary.goodsDiscountAmount) - .toFixed(2); + const price = carts.orderCostSummary.goodsRealAmount; refCoupon.value.open(price, props.orderInfo); } @@ -310,6 +314,7 @@ function discountConfirm(e) { // 计算商家减免前金额(使用bignumber.js确保精度) function returnMerchantReductionDiscount() { const { + fullReduction, //满减活动 goodsOriginalAmount, // 商品原价总和 goodsDiscountAmount, // 商品折扣 couponDeductionAmount, // 优惠券抵扣 @@ -330,6 +335,7 @@ function returnMerchantReductionDiscount() { // 按照原逻辑进行精确计算 const total = originalAmount + .minus(fullReduction.actualAmount) // 减去满减活动抵扣金额 .minus(discountAmount) // 减去商品折扣 .minus(couponAmount) // 减去优惠券抵扣 .minus(pointAmount) // 减去积分抵扣 @@ -368,15 +374,7 @@ const props = defineProps({ default: () => {}, }, }); -const seatAmount = computed(() => { - if (shopUser.userInfo.isTableFee) { - return "0.00"; - } - if (props.perpole >= 1) { - return (props.perpole * shopUser.userInfo.tableFee).toFixed(2); - } - return "0.00"; -}); + watch( () => props.user.id, (newval) => { @@ -673,7 +671,6 @@ const totalPrice = computed(() => { //应付金额 const currentpayMoney = computed(() => { return carts.orderCostSummary.finalPayAmount; - // return (totalMoney.value * 1 + carts.packFee * 1 + seatAmount.value * 1).toFixed(2); }); watch( () => currentpayMoney.value, From 8b1cb636511c55061eb8f4f200a3ac7c4c146f4a Mon Sep 17 00:00:00 2001 From: YeMingfei666 <1619116647@qq.com> Date: Wed, 15 Oct 2025 10:33:06 +0800 Subject: [PATCH 2/9] =?UTF-8?q?fix:=20=E5=A2=9E=E5=8A=A0=E6=B4=BB=E5=8A=A8?= =?UTF-8?q?=E6=BB=A1=E5=87=8F=E5=8A=9F=E8=83=BD=E6=B5=8B=E8=AF=95=EF=BC=8C?= =?UTF-8?q?=E5=A4=87=E4=BB=BD=E4=B9=8B=E5=89=8D=E7=9A=84=E7=BB=93=E7=AE=97?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/store/modules/carts.ts | 32 +- src/utils/goods copy.ts | 1477 ++++++++++++++++++++++++++++++++++++ src/utils/goods.ts | 985 ++++++++++-------------- 3 files changed, 1904 insertions(+), 590 deletions(-) create mode 100644 src/utils/goods copy.ts diff --git a/src/store/modules/carts.ts b/src/store/modules/carts.ts index abd190f..bc8a1ee 100644 --- a/src/store/modules/carts.ts +++ b/src/store/modules/carts.ts @@ -13,7 +13,7 @@ import { BackendCoupon, ActivityConfig, OrderExtraConfig, MerchantReductionConfig, MerchantReductionType, - GoodsType + GoodsType, } from "@/utils/goods"; const shopUser = useUserStoreHook(); @@ -217,6 +217,33 @@ export const useCartsStore = defineStore("carts", () => { }) //使用积分数量 const userPoints = ref(0); + const testFullReductionActivity = { + "id": 231, + "shopId": 26, + "status": 2, // 2=进行中 + "sort": 10, + "createTime": "2025-10-14 13:56:07", + "updateTime": "2025-10-14 14:41:02", // 最新修改 + "validStartTime": "2025-10-14", + "validEndTime": "2025-12-14", + "useType": "dine,pickup,deliv,express", // 支持所有就餐类型 + "useDays": "周一,周二,周三,周四,周五,周六,周日", // 全周期 + "useTimeType": "all", // 全时段 + "useStartTime": null, + "useEndTime": null, + "couponShare": 0, // 与优惠券不同享 + "discountShare": 0, // 与限时折扣不同享 + "vipPriceShare": 0, // 与会员价不同享 + "pointsShare": 0, // 与积分不同享 + "thresholds": [ // 多门槛(此处1个) + { + "activityId": 231, + "fullAmount": 1, // 满1元 + "discountAmount": 10 // 减10元 + } + ], + "isDel": false + }; // 订单额外配置(现在依赖响应式的 merchantReduction) const orderExtraConfig = computed(() => ({ // 引用扩展后的商家减免配置 @@ -227,7 +254,8 @@ export const useCartsStore = defineStore("carts", () => { currentStoreId: shopUser.userInfo.shopId?.toString() || '', userPoints: userPoints.value, isMember: useVipPrice.value, - memberDiscountRate: shopUser.userInfo.memberDiscountRate || 1 + memberDiscountRate: shopUser.userInfo.memberDiscountRate || 1, + fullReductionActivities: [testFullReductionActivity] })); // 营销活动列表 diff --git a/src/utils/goods copy.ts b/src/utils/goods copy.ts new file mode 100644 index 0000000..59146b7 --- /dev/null +++ b/src/utils/goods copy.ts @@ -0,0 +1,1477 @@ +import { BigNumber } from "bignumber.js"; + +// 配置BigNumber精度 +BigNumber.set({ + DECIMAL_PLACES: 2, + ROUNDING_MODE: BigNumber.ROUND_DOWN, // 向下取整,符合业务需求 +}); + +/** + * 购物车订单价格计算公共库 + * 功能:覆盖订单全链路费用计算(商品价格、优惠券、积分、餐位费等),支持策略扩展 + * 小数处理:使用bignumber.js确保精度,统一舍去小数点后两位(如 10.129 → 10.12,15.998 → 15.99) + * 扩展设计:优惠券/营销活动采用策略模式,新增类型仅需扩展策略,无需修改核心逻辑 + * 关键规则: + * - 所有优惠券均支持指定门槛商品(后端foods字段:空字符串=全部商品,ID字符串=指定商品ID) + * - 与限时折扣/会员价同享规则:开启则门槛计算含对应折扣,关闭则用原价/非会员价 + * 字段说明: + * - BaseCartItem.id:购物车项ID(唯一标识购物车中的条目) + * - BaseCartItem.product_id:商品ID(唯一标识商品,用于优惠券/活动匹配) + * - BaseCartItem.skuData.id:SKU ID(唯一标识商品规格) + */ + +// ============================ 1. 基础类型定义(核心修正:明确ID含义) ============================ +/** 商品类型枚举 */ +export enum GoodsType { + NORMAL = "normal", // 普通商品 + WEIGHT = "weight", // 称重商品 + GIFT = "gift", // 赠菜(继承普通商品逻辑,标记用) + EMPTY = "", // 空字符串类型(后端未返回时默认归类为普通商品) + PACKAGE = "package", // 打包商品(如套餐/预打包商品,按普通商品逻辑处理,可扩展特殊规则) +} + +/** 优惠券计算结果类型(新增细分字段) */ +interface CouponResult { + deductionAmount: number; // 抵扣金额 + excludedProductIds: string[]; // 不适用商品ID列表(注意:是商品ID,非购物车ID) + usedCoupon: Coupon | undefined; // 实际使用的优惠券 + productCouponDeduction: number; // 新增:商品优惠券抵扣(兑换券等) + fullCouponDeduction: number; // 新增:满减优惠券抵扣 +} + +/** 兑换券计算结果类型(新增细分字段) */ +interface ExchangeCalculationResult { + deductionAmount: number; + excludedProductIds: string[]; // 不适用商品ID列表(商品ID) + productCouponDeduction: number; // 新增:兑换券属于商品券,同步记录 +} + +/** 优惠券类型枚举 */ +export enum CouponType { + FULL_REDUCTION = "full_reduction", // 满减券 + DISCOUNT = "discount", // 折扣券 + SECOND_HALF = "second_half", // 第二件半价券 + BUY_ONE_GET_ONE = "buy_one_get_one", // 买一送一券 + EXCHANGE = "exchange", // 商品兑换券 +} + +/** 后端返回的优惠券原始字段类型 */ +export interface BackendCoupon { + id?: number; // 自增主键(int64) + shopId?: number; // 店铺ID(int64) + syncId?: number; // 同步Id(int64) + type?: number; // 优惠券类型:1-满减券,2-商品兑换券,3-折扣券,4-第二件半价券,5-消费送券,6-买一送一券,7-固定价格券,8-免配送费券 + name?: string; // 券名称 + useShopType?: string; // 可用门店类型:only-仅本店;all-所有门店,custom-指定门店 + useShops?: string; // 可用门店(逗号分隔字符串,如"1,2,3") + useType?: string; // 可使用类型:dine堂食/pickup自取/deliv配送/express快递 + validType?: string; // 有效期类型:fixed(固定时间),custom(自定义时间) + validDays?: number; // 有效期(天) + validStartTime?: string; // 有效期开始时间(如"2024-01-01 00:00:00") + validEndTime?: string; // 有效期结束时间 + daysToTakeEffect?: number; // 隔天生效 + useDays?: string; // 可用周期(如"周一,周二") + useTimeType?: string; // 可用时间段类型:all-全时段,custom-指定时段 + useStartTime?: string; // 可用开始时间(每日) + useEndTime?: string; // 可用结束时间(每日) + getType?: string; // 发放设置:不可自行领取/no,可领取/yes + getMode?: string; // 用户领取方式 + giveNum?: number; // 总发放数量,-10086为不限量 + getUserType?: string; // 可领取用户:全部/all,新用户一次/new,仅会员/vip + getLimit?: number; // 每人领取限量,-10086为不限量 + useLimit?: number; // 每人每日使用限量,-10086为不限量 + discountShare?: number; // 与限时折扣同享:0-否,1-是 + vipPriceShare?: number; // 与会员价同享:0-否,1-是 + ruleDetails?: string; // 附加规则说明 + status?: number; // 状态:0-禁用,1-启用 + useNum?: number; // 已使用数量 + leftNum?: number; // 剩余数量 + foods?: string; // 指定门槛商品(逗号分隔字符串,如"101,102",此处为商品ID) + fullAmount?: number; // 使用门槛:满多少金额(元) + discountAmount?: number; // 使用门槛:减多少金额(元) + discountRate?: number; // 折扣%(如90=9折) + maxDiscountAmount?: number; // 可抵扣最大金额(元) + useRule?: string; // 使用规则:price_asc-价格低到高,price_desc-高到低 + discountNum?: number; // 抵扣数量 + otherCouponShare?: number; // 与其它优惠共享:0-否,1-是 + createTime?: string; // 创建时间 + updateTime?: string; // 更新时间 +} + +/** 营销活动类型枚举 */ +export enum ActivityType { + TIME_LIMIT_DISCOUNT = "time_limit_discount", // 限时折扣 +} + +/** 基础购物车商品项(核心修正:新增product_id,明确各ID含义) */ +export interface BaseCartItem { + id: string | number; // 购物车ID(唯一标识购物车中的条目,如购物车项主键) + product_id: string | number; // 商品ID(唯一标识商品,用于优惠券/活动匹配,必选) + salePrice: number; // 商品原价(元) + number: number; // 商品数量 + product_type: GoodsType; // 商品类型 + is_temporary?: boolean; // 是否临时菜(默认false) + is_gift?: boolean; // 是否赠菜(默认false) + returnNum?: number; // 退货数量(历史订单用,默认0) + memberPrice?: number; // 商品会员价(元,优先级:商品会员价 > 会员折扣) + discountSaleAmount?: number; // 商家改价后单价(元,优先级最高) + packFee?: number; // 单份打包费(元,默认0) + packNumber?: number; // 堂食打包数量(默认0) + activityInfo?: { + // 商品参与的营销活动(如限时折扣) + type: ActivityType; + discountRate: number; // 折扣率(如0.8=8折) + vipPriceShare: boolean; // 是否与会员优惠同享(默认false) + }; + skuData?: { + // SKU扩展数据(可选) + id: string | number; // SKU ID(唯一标识商品规格,如颜色/尺寸) + memberPrice?: number; // SKU会员价 + salePrice?: number; // SKU原价 + }; +} + +/** 基础优惠券接口(所有券类型继承,包含统一门槛商品字段) */ +export interface BaseCoupon { + id: string | number; // 优惠券ID + type: CouponType; // 工具库字符串枚举(由后端couponType转换) + name: string; // 对应后端title + available: boolean; // 基于BackendCoupon字段计算的可用性 + useShopType?: string; // only-仅本店;all-所有门店,custom-指定门店 + useShops: string[]; // 可用门店ID列表 + discountShare: boolean; // 与限时折扣同享:0-否,1-是(后端字段转换为布尔值) + vipPriceShare: boolean; // 与会员价同享:0-否,1-是(后端字段转换为布尔值) + useType?: string[]; // 可使用类型:dine堂食/pickup自取/deliv配送/express快递 + isValid: boolean; // 是否在有效期内 + discountAmount?: number; // 减免金额 (满减券有) + fullAmount?: number; // 使用门槛:满多少金额 + maxDiscountAmount?: number; // 可抵扣最大金额 元 + applicableProductIds: string[]; // 门槛商品ID列表(空数组=全部商品,非空=指定商品ID) +} + +/** 满减券(适配后端字段) */ +export interface FullReductionCoupon extends BaseCoupon { + type: CouponType.FULL_REDUCTION; + fullAmount: number; // 对应后端fullAmount(满减门槛) + discountAmount: number; // 对应后端discountAmount(减免金额) + maxDiscountAmount?: number; // 对应后端maxDiscountAmount(最大减免) +} + +/** 折扣券(适配后端字段) */ +export interface DiscountCoupon extends BaseCoupon { + type: CouponType.DISCOUNT; + discountRate: number; // 后端discountRate(%)转小数(如90→0.9) + maxDiscountAmount: number; // 对应后端maxDiscountAmount(最大减免) +} + +/** 第二件半价券(适配后端字段) */ +export interface SecondHalfPriceCoupon extends BaseCoupon { + type: CouponType.SECOND_HALF; + maxUseCountPerOrder?: number; // 对应后端useLimit(-10086=不限) +} + +/** 买一送一券(适配后端字段) */ +export interface BuyOneGetOneCoupon extends BaseCoupon { + type: CouponType.BUY_ONE_GET_ONE; + maxUseCountPerOrder?: number; // 对应后端useLimit(-10086=不限) +} + +/** 商品兑换券(适配后端字段) */ +export interface ExchangeCoupon extends BaseCoupon { + type: CouponType.EXCHANGE; + deductCount: number; // 对应后端discountNum(抵扣数量) + sortRule: "low_price_first" | "high_price_first"; // 后端useRule转换 +} + +/** 所有优惠券类型联合 */ +export type Coupon = + | FullReductionCoupon + | DiscountCoupon + | SecondHalfPriceCoupon + | BuyOneGetOneCoupon + | ExchangeCoupon; + +/** 营销活动配置(如限时折扣,applicableProductIds为商品ID列表) */ +export interface ActivityConfig { + type: ActivityType; + applicableProductIds?: string[]; // 适用商品ID列表(与BaseCartItem.product_id匹配) + discountRate: number; // 折扣率(如0.8=8折) + vipPriceShare: boolean; // 是否与会员优惠同享 +} + +/** 积分抵扣规则 */ +export interface PointDeductionRule { + pointsPerYuan: number; // X积分=1元(如100=100积分抵1元) + maxDeductionAmount?: number; // 最大抵扣金额(元,默认不限) +} + +/** 餐位费配置 */ +export interface SeatFeeConfig { + pricePerPerson: number; // 每人餐位费(元) + personCount: number; // 用餐人数(默认1) + isEnabled: boolean; // 是否启用餐位费(默认false) +} +/** 商家减免类型枚举 */ +export 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: MerchantReductionConfig; + additionalFee: number; // 附加费(元,如余额充值、券包,默认0) + pointDeductionRule: PointDeductionRule; // 积分抵扣规则 + seatFeeConfig: SeatFeeConfig; // 餐位费配置 + currentStoreId: string; // 当前门店ID(用于验证优惠券适用门店) + userPoints: number; // 用户当前积分(用于积分抵扣) + isMember: boolean; // 用户是否会员(用于会员优惠) + memberDiscountRate?: number; // 会员折扣率(如0.95=95折,无会员价时用) + newUserDiscount?: number; // 新用户减免金额(元,默认0) +} + +/** 订单费用汇总(修改:补充商家减免类型和明细) */ +export interface OrderCostSummary { + // 商品总件数 + goodsTotal: number; + totalDiscountAmount: 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; // 实际使用的积分 + newUserDiscount: number; // 新用户减免金额(元,默认0) + dinnerType?: "dine-in" | "take-out"; // 就餐类型(堂食/自取/配送/快递) + config: OrderExtraConfig; // 订单额外费用配置 +} + + + +/** + * 折扣率格式化:后端discountRate(%)→ 工具库小数(如90→0.9) + */ +export function formatDiscountRate(backendDiscountRate?: number): number { + if (!backendDiscountRate || backendDiscountRate <= 0) return 1; // 默认无折扣(1=100%) + // 后端若为百分比(如90=9折),除以100;若已为小数(如0.9)直接返回 + return backendDiscountRate >= 1 + ? backendDiscountRate / 100 + : backendDiscountRate; +} + +/** + * 统一小数处理:舍去小数点后两位以后的数字(解决浮点数精度偏差) + * 如 10.129 → 10.12,1.01 → 1.01,1.0100000001 → 1.01 + * @param num 待处理数字 + * @returns 处理后保留两位小数的数字 + */ +export function truncateToTwoDecimals(num: number | string): number { + return new BigNumber(num).decimalPlaces(2, BigNumber.ROUND_DOWN).toNumber(); +} + +/** + * 判断商品是否为临时菜(临时菜不参与优惠券门槛和折扣计算) + * @param goods 商品项 + * @returns 是否临时菜 + */ +export function isTemporaryGoods(goods: BaseCartItem): boolean { + return !!goods.is_temporary; +} + +/** + * 判断商品是否为赠菜(赠菜不计入优惠券活动,但计打包费) + * @param goods 商品项 + * @returns 是否赠菜 + */ +export function isGiftGoods(goods: BaseCartItem): boolean { + return !!goods.is_gift; +} + +/** + * 计算单个商品的会员价(优先级:SKU会员价 > 商品会员价 > 会员折扣率) + * @param goods 商品项 + * @param isMember 是否会员 + * @param memberDiscountRate 会员折扣率(如0.95=95折) + * @returns 会员价(元) + */ +export function calcMemberPrice( + goods: BaseCartItem, + isMember: boolean, + memberDiscountRate?: number +): number { + if (!isMember) return truncateToTwoDecimals(goods.salePrice); + + // 优先级:SKU会员价 > 商品会员价 > 商品原价(无会员价时用会员折扣) + const basePrice = + goods.skuData?.memberPrice ?? goods.memberPrice ?? goods.salePrice; + + // 仅当无SKU会员价、无商品会员价时,才应用会员折扣率 + if (memberDiscountRate && !goods.skuData?.memberPrice && !goods.memberPrice) { + return truncateToTwoDecimals( + new BigNumber(basePrice).multipliedBy(memberDiscountRate).toNumber() + ); + } + + return truncateToTwoDecimals(basePrice); +} + +/** + * 过滤可参与优惠券计算的商品(排除临时菜、赠菜、已用兑换券的商品) + * @param goodsList 商品列表 + * @param excludedProductIds 需排除的商品ID列表(商品ID,非购物车ID) + * @returns 可参与优惠券计算的商品列表 + */ +export function filterCouponEligibleGoods( + goodsList: BaseCartItem[], + excludedProductIds: string[] = [] +): BaseCartItem[] { + return goodsList.filter( + (goods) => + !isTemporaryGoods(goods) && + !isGiftGoods(goods) && + !excludedProductIds.includes(String(goods.product_id)) // 核心修正:用商品ID排除 + ); +} + +/** + * 统一筛选门槛商品的工具函数(所有券类型复用,按商品ID匹配) + * @param baseEligibleGoods 基础合格商品(已排除临时菜/赠菜/已抵扣商品) + * @param applicableProductIds 优惠券指定的门槛商品ID数组 + * @returns 最终参与优惠券计算的商品列表 + */ +export function filterThresholdGoods( + baseEligibleGoods: BaseCartItem[], + applicableProductIds: string[] +): BaseCartItem[] { + // 空数组=全部基础合格商品;非空=仅商品ID匹配的商品(转字符串兼容类型) + return applicableProductIds.length === 0 + ? baseEligibleGoods + : baseEligibleGoods.filter((goods) => + applicableProductIds.includes(String(goods.product_id)) + ); // 核心修正:用商品ID匹配 +} + +/** + * 商品排序(用于商品兑换券:按价格/数量/加入顺序排序,按商品ID分组去重) + * @param goodsList 商品列表 + * @param sortRule 排序规则(low_price_first/high_price_first) + * @param cartOrder 商品加入购物车的顺序(key=购物车ID,value=加入时间戳) + * @returns 排序后的商品列表 + */ +export function sortGoodsForCoupon( + goodsList: BaseCartItem[], + sortRule: "low_price_first" | "high_price_first", + cartOrder: Record = {} +): BaseCartItem[] { + return [...goodsList].sort((a, b) => { + // 1. 按商品单价排序(优先级最高) + const priceA = a.skuData?.salePrice ?? a.salePrice; + const priceB = b.skuData?.salePrice ?? b.salePrice; + if (priceA !== priceB) { + return sortRule === "low_price_first" ? priceA - priceB : priceB - priceA; + } + + // 2. 同价格按商品数量排序(降序,多的优先) + if (a.number !== b.number) { + return b.number - a.number; + } + + // 3. 同价格同数量按加入购物车顺序(早的优先,用购物车ID匹配) + const orderA = cartOrder[String(a.id)] ?? Infinity; + const orderB = cartOrder[String(b.id)] ?? Infinity; + return orderA - orderB; + }); +} + +/** + * 计算优惠券门槛金额(根据同享规则,按商品ID匹配限时折扣) + * @param eligibleGoods 可参与优惠券的商品列表(已过滤临时菜/赠菜) + * @param coupon 优惠券(含discountShare/vipPriceShare配置) + * @param config 订单配置(会员信息) + * @param activities 全局营销活动(限时折扣,applicableProductIds为商品ID) + * @returns 满足优惠券门槛的金额基数 + */ +export function calcCouponThresholdAmount( + eligibleGoods: BaseCartItem[], + coupon: BaseCoupon, + config: Pick, + activities: ActivityConfig[] = [] +): number { + let total = new BigNumber(0); + + for (const goods of eligibleGoods) { + const availableNum = Math.max(0, goods.number - (goods.returnNum || 0)); + if (availableNum <= 0) continue; + + // 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); // 叠加限时折扣 + } + } + + total = total.plus(itemAmount); + } + + return truncateToTwoDecimals(total.toNumber()); +} + +// ============================ 3. 商品核心价格计算(核心修正:按商品ID匹配营销活动) ============================ +/** + * 计算单个商品的实际单价(整合商家改价、会员优惠、营销活动折扣,按商品ID匹配活动) + * @param goods 商品项 + * @param config 订单额外配置(含会员、活动信息) + * @returns 单个商品实际单价(元) + */ +export function calcSingleGoodsRealPrice( + goods: BaseCartItem, + config: Pick & { + activity?: ActivityConfig; // 商品参与的营销活动(如限时折扣,按商品ID匹配) + } +): number { + const { isMember, memberDiscountRate, activity } = config; + + //如果是增菜价格为0 + if (goods.is_gift) { + return 0; + } + + // 1. 优先级1:商家改价(改价后单价>0才生效) + if (goods.discountSaleAmount && goods.discountSaleAmount > 0) { + return truncateToTwoDecimals(goods.discountSaleAmount); + } + + // 2. 优先级2:会员价(含会员折扣率,SKU会员价优先) + const memberPrice = new BigNumber( + calcMemberPrice(goods, isMember, memberDiscountRate) + ); + + // 3. 优先级3:营销活动折扣(如限时折扣,需按商品ID匹配活动) + const isActivityApplicable = activity + ? (activity.applicableProductIds || []).includes(String(goods.product_id)) // 核心修正:用商品ID匹配活动 + : false; + if (!activity || !isActivityApplicable) { + return memberPrice.toNumber(); + } + + // 处理活动与会员的同享/不同享逻辑 + if (activity.vipPriceShare) { + // 同享:会员价基础上叠加工活动折扣 + return truncateToTwoDecimals( + memberPrice.multipliedBy(activity.discountRate).toNumber() + ); + } else { + // 不同享:取会员价和活动价的最小值(活动价用SKU原价计算) + const basePriceForActivity = new BigNumber( + goods.skuData?.salePrice ?? goods.salePrice + ); + const activityPrice = basePriceForActivity.multipliedBy( + activity.discountRate + ); + + return truncateToTwoDecimals( + memberPrice.isLessThanOrEqualTo(activityPrice) + ? memberPrice.toNumber() + : activityPrice.toNumber() + ); + } +} + +/** + * 计算商品原价总和(所有商品:原价*数量,含临时菜、赠菜,用SKU原价优先) + * @param goodsList 商品列表 + * @returns 商品原价总和(元) + */ +export function calcGoodsOriginalAmount(goodsList: BaseCartItem[]): number { + let total = new BigNumber(0); + + for (const goods of goodsList) { + const availableNum = Math.max(0, goods.number - (goods.returnNum || 0)); + let basePrice = new BigNumber(0); + if (goods.is_temporary) { + basePrice = new BigNumber(goods?.discountSaleAmount ?? 0); + } else if (goods.is_gift) { + basePrice = new BigNumber(0); + } + else { + basePrice = new BigNumber( + goods.skuData?.salePrice ?? goods.salePrice + ); // SKU原价优先 + } + + total = total.plus(basePrice.multipliedBy(availableNum)); + } + + return truncateToTwoDecimals(total.toNumber()); +} + +/** + * 计算商品实际总价(所有商品:实际单价*数量,含临时菜、赠菜,按商品ID匹配活动) + * @param goodsList 商品列表 + * @param config 订单额外配置(含会员、活动信息) + * @param activities 全局营销活动列表(如限时折扣,applicableProductIds为商品ID) + * @returns 商品实际总价(元) + */ +export function calcGoodsRealAmount( + goodsList: BaseCartItem[], + config: Pick, + activities: ActivityConfig[] = [] +): number { + let total = new BigNumber(0); + + for (const goods of goodsList) { + const availableNum = Math.max(0, goods.number - (goods.returnNum || 0)); + if (availableNum <= 0) continue; + + // 匹配商品参与的营销活动(按商品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()); +} + +/** + * 计算商品折扣总金额(商品原价总和 - 商品实际总价) + * @param goodsOriginalAmount 商品原价总和 + * @param goodsRealAmount 商品实际总价 + * @returns 商品折扣总金额(元,≥0) + */ +export function calcGoodsDiscountAmount( + goodsOriginalAmount: number, + goodsRealAmount: number +): number { + const original = new BigNumber(goodsOriginalAmount); + const real = new BigNumber(goodsRealAmount); + const discount = original.minus(real); + + return truncateToTwoDecimals(Math.max(0, discount.toNumber())); +} + +// ============================ 4. 优惠券抵扣计算(策略模式,核心修正:按商品ID匹配) ============================ +/** 优惠券计算策略接口(新增优惠券类型时,实现此接口即可) */ +interface CouponCalculationStrategy { + calculate( + coupon: Coupon, + goodsList: BaseCartItem[], + config: Pick< + OrderExtraConfig, + "currentStoreId" | "isMember" | "memberDiscountRate" + > & { + activities: ActivityConfig[]; + cartOrder: Record; + excludedProductIds?: string[]; // 需排除的商品ID列表(商品ID) + } + ): { + deductionAmount: number; + excludedProductIds: string[]; // 排除的商品ID列表(商品ID) + productCouponDeduction?: number; // 新增:当前券属于商品券时的抵扣金额 + fullCouponDeduction?: number; // 新增:当前券属于满减券时的抵扣金额 + }; +} + +/** 满减券计算策略(按商品ID筛选门槛商品) */ +class FullReductionStrategy implements CouponCalculationStrategy { + calculate( + coupon: FullReductionCoupon, + goodsList: BaseCartItem[], + config: any + ): { + deductionAmount: number; + excludedProductIds: string[]; + fullCouponDeduction: number; + } { + // 1. 基础校验:优惠券不可用/门店不匹配 → 抵扣0 + if ( + !coupon.available || + !isStoreMatchByList(coupon.useShops, config.currentStoreId) + ) { + 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: [], + fullCouponDeduction: 0, + }; + } + + // 4. 按同享规则计算门槛金额(按商品ID匹配活动) + const thresholdAmount = new BigNumber( + calcCouponThresholdAmount( + thresholdGoods, + coupon, + { + isMember: config.isMember, + memberDiscountRate: config.memberDiscountRate, + }, + config.activities + ) + ); + + // 5. 满门槛则抵扣,否则0(不超过最大减免) + if (thresholdAmount.isLessThan(coupon.fullAmount)) { + return { + deductionAmount: 0, + excludedProductIds: [], + fullCouponDeduction: 0, + }; + } + + const maxReduction = coupon.maxDiscountAmount ?? coupon.discountAmount; + const deductionAmount = truncateToTwoDecimals( + new BigNumber(coupon.discountAmount).isLessThan(maxReduction) + ? coupon.discountAmount + : maxReduction + ); + + // 满减券计入满减优惠券抵扣 + return { + deductionAmount, + excludedProductIds: [], + fullCouponDeduction: deductionAmount, + }; + } +} + +/** 折扣券计算策略(按商品ID筛选门槛商品) */ +class DiscountStrategy implements CouponCalculationStrategy { + calculate( + coupon: DiscountCoupon, + goodsList: BaseCartItem[], + config: any + ): { + deductionAmount: number; + excludedProductIds: string[]; + productCouponDeduction: number; + } { + // 1. 基础校验:优惠券不可用/门店不匹配 → 抵扣0 + if ( + !coupon.available || + !isStoreMatchByList(coupon.useShops, config.currentStoreId) + ) { + return { + deductionAmount: 0, + excludedProductIds: [], + productCouponDeduction: 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: [], + productCouponDeduction: 0, + }; + } + + // 4. 按同享规则计算折扣基数(按商品ID匹配活动) + const discountBaseAmount = new BigNumber( + calcCouponThresholdAmount( + thresholdGoods, + coupon, + { + isMember: config.isMember, + memberDiscountRate: config.memberDiscountRate, + }, + config.activities + ) + ); + + // 5. 计算折扣金额(不超过最大减免) + const discountAmount = discountBaseAmount.multipliedBy( + new BigNumber(1).minus(coupon.discountRate) + ); + const deductionAmount = truncateToTwoDecimals( + discountAmount.isLessThan(coupon.maxDiscountAmount) + ? discountAmount.toNumber() + : coupon.maxDiscountAmount + ); + + // 折扣券计入商品优惠券抵扣(可根据业务调整归类) + return { + deductionAmount, + excludedProductIds: [], + productCouponDeduction: deductionAmount, + }; + } +} + +/** 第二件半价券计算策略(按商品ID分组,筛选门槛商品) */ +class SecondHalfPriceStrategy implements CouponCalculationStrategy { + calculate( + coupon: SecondHalfPriceCoupon, + goodsList: BaseCartItem[], + config: any + ): { + deductionAmount: number; + excludedProductIds: string[]; + productCouponDeduction: number; + } { + // 1. 基础校验:优惠券不可用/门店不匹配 → 抵扣0 + if ( + !coupon.available || + !isStoreMatchByList(coupon.useShops, config.currentStoreId) + ) { + return { + deductionAmount: 0, + excludedProductIds: [], + productCouponDeduction: 0, + }; + } + + let totalDeduction = new BigNumber(0); + const excludedProductIds: string[] = []; // 存储排除的商品ID(商品ID) + + // 2. 第一步:排除临时菜/赠菜/已抵扣商品(基础合格商品,按商品ID排除) + const baseEligibleGoods = filterCouponEligibleGoods( + goodsList, + config.excludedProductIds || [] + ); + // 3. 第二步:按商品ID筛选门槛商品(匹配applicableProductIds) + const thresholdGoods = filterThresholdGoods( + baseEligibleGoods, + coupon.applicableProductIds + ); + if (thresholdGoods.length === 0) { + return { + deductionAmount: 0, + excludedProductIds: [], + productCouponDeduction: 0, + }; + } + + // 按商品ID分组(避免同商品多次处理,用商品ID作为分组key) + const goodsGroupByProductId = thresholdGoods.reduce((group, goods) => { + const productIdStr = String(goods.product_id); // 商品ID转字符串作为key + if (!group[productIdStr]) group[productIdStr] = []; + group[productIdStr].push(goods); + return group; + }, {} as Record); + + // 遍历每组商品计算半价优惠 + for (const [productIdStr, productGoods] of Object.entries( + goodsGroupByProductId + )) { + if (excludedProductIds.includes(productIdStr)) continue; // 按商品ID排除已处理商品 + + // 合并同商品数量(所有购物车项的数量总和) + const totalNum = productGoods.reduce( + (sum, g) => sum + (g.number - (g.returnNum || 0)), + 0 + ); + if (totalNum < 2) continue; // 至少2件才享受优惠 + + // 计算优惠次数(每2件1次,不超过最大使用次数) + const discountCount = Math.min( + Math.floor(totalNum / 2), + coupon.maxUseCountPerOrder || Infinity + ); + if (discountCount <= 0) continue; + + // 计算单件实际价格(取组内任意一个商品的配置,同商品规格一致) + const sampleGood = productGoods[0]; + const activity = config.activities.find( + (act: ActivityConfig) => + (act.applicableProductIds || []).includes(productIdStr) // 按商品ID匹配活动 + ); + const realPrice = new BigNumber( + calcSingleGoodsRealPrice(sampleGood, { + isMember: config.isMember, + memberDiscountRate: config.memberDiscountRate, + activity, + }) + ); + + // 累计抵扣金额并标记已优惠商品(记录商品ID) + totalDeduction = totalDeduction.plus( + realPrice.multipliedBy(0.5).multipliedBy(discountCount) + ); + excludedProductIds.push(productIdStr); + } + + const deductionAmount = truncateToTwoDecimals(totalDeduction.toNumber()); + // 第二件半价券计入商品优惠券抵扣 + return { + deductionAmount, + excludedProductIds, + productCouponDeduction: deductionAmount, + }; + } +} + +/** 买一送一券计算策略(按商品ID分组,筛选门槛商品) */ +class BuyOneGetOneStrategy implements CouponCalculationStrategy { + calculate( + coupon: BuyOneGetOneCoupon, + goodsList: BaseCartItem[], + config: any + ): { + deductionAmount: number; + excludedProductIds: string[]; + productCouponDeduction: number; + } { + // 1. 基础校验:优惠券不可用/门店不匹配 → 抵扣0 + if ( + !coupon.available || + !isStoreMatchByList(coupon.useShops, config.currentStoreId) + ) { + return { + deductionAmount: 0, + excludedProductIds: [], + productCouponDeduction: 0, + }; + } + + let totalDeduction = new BigNumber(0); + const excludedProductIds: string[] = []; // 存储排除的商品ID(商品ID) + + // 2. 第一步:排除临时菜/赠菜/已抵扣商品(基础合格商品,按商品ID排除) + const baseEligibleGoods = filterCouponEligibleGoods( + goodsList, + config.excludedProductIds || [] + ); + // 3. 第二步:按商品ID筛选门槛商品(匹配applicableProductIds) + const thresholdGoods = filterThresholdGoods( + baseEligibleGoods, + coupon.applicableProductIds + ); + if (thresholdGoods.length === 0) { + return { + deductionAmount: 0, + excludedProductIds: [], + productCouponDeduction: 0, + }; + } + + // 按商品ID分组(用商品ID作为分组key) + const goodsGroupByProductId = thresholdGoods.reduce((group, goods) => { + const productIdStr = String(goods.product_id); + if (!group[productIdStr]) group[productIdStr] = []; + group[productIdStr].push(goods); + return group; + }, {} as Record); + + // 遍历每组商品计算买一送一优惠 + for (const [productIdStr, productGoods] of Object.entries( + goodsGroupByProductId + )) { + if (excludedProductIds.includes(productIdStr)) continue; // 按商品ID排除已处理商品 + + // 合并同商品数量 + const totalNum = productGoods.reduce( + (sum, g) => sum + (g.number - (g.returnNum || 0)), + 0 + ); + if (totalNum < 2) continue; // 至少2件才享受优惠 + + // 计算优惠次数(每2件送1件) + const discountCount = Math.floor(totalNum / 2); + if (discountCount <= 0) continue; + + // 计算单件实际价格(按商品ID匹配活动) + const sampleGood = productGoods[0]; + const activity = config.activities.find( + (act: ActivityConfig) => + (act.applicableProductIds || []).includes(productIdStr) // 按商品ID匹配活动 + ); + const realPrice = new BigNumber( + calcSingleGoodsRealPrice(sampleGood, { + isMember: config.isMember, + memberDiscountRate: config.memberDiscountRate, + activity, + }) + ); + + // 累计抵扣金额(送1件=减免1件价格)并标记商品ID + totalDeduction = totalDeduction.plus( + realPrice.multipliedBy(1).multipliedBy(discountCount) + ); + excludedProductIds.push(productIdStr); + } + + const deductionAmount = truncateToTwoDecimals(totalDeduction.toNumber()); + // 买一送一券计入商品优惠券抵扣 + return { + deductionAmount, + excludedProductIds, + productCouponDeduction: deductionAmount, + }; + } +} + +/** 商品兑换券计算策略(按商品ID筛选门槛商品) */ +class ExchangeCouponStrategy implements CouponCalculationStrategy { + calculate( + coupon: ExchangeCoupon, + goodsList: BaseCartItem[], + config: any + ): { + deductionAmount: number; + excludedProductIds: string[]; + productCouponDeduction: number; + } { + // 1. 基础校验:优惠券不可用/门店不匹配 → 抵扣0 + if ( + !coupon.available || + !isStoreMatchByList(coupon.useShops, config.currentStoreId) + ) { + return { + deductionAmount: 0, + excludedProductIds: [], + productCouponDeduction: 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: [], + productCouponDeduction: 0, + }; + } + + // 按规则排序商品(按价格/数量/加入顺序) + const sortedGoods = sortGoodsForCoupon( + thresholdGoods, + coupon.sortRule, + config.cartOrder + ); + let remainingCount = coupon.deductCount; + let totalDeduction = new BigNumber(0); + const excludedProductIds: string[] = []; // 存储排除的商品ID(商品ID) + + // 计算兑换抵扣金额(按商品ID累计,避免重复抵扣) + const usedProductIds = new Set(); // 记录已使用的商品ID(避免同商品多次抵扣) + for (const goods of sortedGoods) { + if (remainingCount <= 0) break; + const productIdStr = String(goods.product_id); + if (usedProductIds.has(productIdStr)) continue; // 同商品仅抵扣一次 + + const availableNum = Math.max(0, goods.number - (goods.returnNum || 0)); + if (availableNum === 0) continue; + + // 计算当前商品可抵扣的件数(不超过剩余可抵扣数量) + const deductCount = Math.min(availableNum, remainingCount); + const activity = config.activities.find( + (act: ActivityConfig) => + (act.applicableProductIds || []).includes(productIdStr) // 按商品ID匹配活动 + ); + const realPrice = new BigNumber( + calcSingleGoodsRealPrice(goods, { + isMember: config.isMember, + memberDiscountRate: config.memberDiscountRate, + activity, + }) + ); + + // 累计抵扣金额并标记商品 + totalDeduction = totalDeduction.plus(realPrice.multipliedBy(deductCount)); + excludedProductIds.push(productIdStr); + usedProductIds.add(productIdStr); + remainingCount -= deductCount; + } + + const deductionAmount = truncateToTwoDecimals(totalDeduction.toNumber()); + // 商品兑换券计入商品优惠券抵扣 + return { + deductionAmount, + excludedProductIds, + productCouponDeduction: deductionAmount, + }; + } +} + +// ------------------------------ 策略辅助函数 ------------------------------ +/** + * 根据优惠券useShops列表判断门店是否匹配(适配BaseCoupon的useShops字段) + */ +function isStoreMatchByList( + useShops: string[], + currentStoreId: string +): boolean { + // 适用门店为空数组 → 无限制(所有门店适用) + if (useShops.length === 0) return true; + // 匹配当前门店ID(字符串比较,避免类型问题) + return useShops.includes(currentStoreId); +} + + + +/** + * 计算优惠券抵扣金额(处理互斥逻辑,选择最优优惠券,按商品ID排除,新增细分统计) + * @param backendCoupons 后端优惠券列表 + * @param goodsList 商品列表 + * @param config 订单配置(含就餐类型) + * @returns 最优优惠券的抵扣结果(含商品券/满减券细分) + */ +export function calcCouponDeduction( + backendCoupons: BackendCoupon[], + goodsList: BaseCartItem[], + config: Pick< + OrderExtraConfig, + "currentStoreId" | "isMember" | "memberDiscountRate" + > & { + activities: ActivityConfig[]; + cartOrder: Record; + dinnerType: "dine-in" | "take-out"; + currentTime?: Date; + } +): { + deductionAmount: number; + productCouponDeduction: number; // 新增:商品优惠券抵扣(兑换券/折扣券/买一送一等) + fullCouponDeduction: number; // 新增:满减优惠券抵扣 + usedCoupon?: Coupon; + excludedProductIds: string[]; // 排除的商品ID列表(商品ID) +} { + const goodsCoupon = backendCoupons.filter((v) => v.type == 2); + const discountCoupon = backendCoupons.filter((v) => v.type != 2); + + // 3. 计算非兑换券最优抵扣(传递已抵扣商品ID,避免重复,统计细分字段) + let nonExchangeResult: CouponResult = { + deductionAmount: discountCoupon.reduce((prve, cur): number => { + return prve + (cur.discountAmount || 0); + }, 0), + excludedProductIds: [], + usedCoupon: undefined, + productCouponDeduction: 0, + fullCouponDeduction: 0, + }; + + // 4. 计算兑换券抵扣(排除非兑换券已抵扣的商品ID,统计商品券细分) + let exchangeResult: ExchangeCalculationResult = { + deductionAmount: goodsCoupon.reduce((prve, cur): number => { + return prve + (cur.discountAmount || 0); + }, 0), + excludedProductIds: [], + productCouponDeduction: 0, + }; + + // 5. 汇总结果:兑换券与非兑换券不可同时使用,取抵扣金额大的 + const exchangeBn = new BigNumber(exchangeResult.deductionAmount); + const nonExchangeBn = new BigNumber(nonExchangeResult.deductionAmount); + const isExchangeBetter = exchangeBn.isGreaterThan(nonExchangeBn); + + return { + deductionAmount: exchangeBn.plus(nonExchangeBn).toNumber(), + productCouponDeduction: exchangeResult.deductionAmount, + fullCouponDeduction: nonExchangeResult.deductionAmount, // 兑换券与满减券互斥,满减券抵扣置0 + usedCoupon: isExchangeBetter ? undefined : nonExchangeResult.usedCoupon, + excludedProductIds: isExchangeBetter + ? exchangeResult.excludedProductIds + : nonExchangeResult.excludedProductIds, + }; +} + +// ============================ 5. 其他费用计算(无ID依赖,逻辑不变) ============================ +/** + * 计算总打包费(赠菜也计算,称重商品打包数量≤1) + * @param goodsList 商品列表 + * @param dinnerType 就餐类型(堂食dine-in/外卖take-out) + * @returns 总打包费(元) + */ +export function calcTotalPackFee( + goodsList: BaseCartItem[], + dinnerType: "dine-in" | "take-out" +): number { + // if (dinnerType !== "take-out") return 0; + let total = new BigNumber(0); + + for (const goods of goodsList) { + 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 = availableNum; + 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()); +} + +/** + * 计算餐位费(按人数,不参与营销活动) + * @param config 餐位费配置 + * @returns 餐位费(元,未启用则0) + */ +export function calcSeatFee(config: SeatFeeConfig): number { + if (!config.isEnabled || config.personCount == 0) return 0; + const personCount = Math.max(1, config.personCount); // 至少1人 + return truncateToTwoDecimals( + new BigNumber(config.pricePerPerson).multipliedBy(personCount).toNumber() + ); +} + +/** + * 计算积分抵扣金额(按X积分=1元,不超过最大抵扣和用户积分) + * @param userPoints 用户当前积分 + * @param rule 积分抵扣规则 + * @param maxDeductionLimit 最大抵扣上限(通常为订单金额,避免超扣) + * @returns 积分抵扣金额 + 实际使用积分 + */ +export function calcPointDeduction( + userPoints: number, + rule: PointDeductionRule, + maxDeductionLimit: number +): { + deductionAmount: number; + usedPoints: number; +} { + if (rule.pointsPerYuan <= 0 || userPoints <= 0) { + return { deductionAmount: 0, usedPoints: 0 }; + } + + const userPointsBn = new BigNumber(userPoints); + const pointsPerYuanBn = new BigNumber(rule.pointsPerYuan); + const maxLimitBn = new BigNumber(maxDeductionLimit); + + // 最大可抵扣金额(积分可抵金额 vs 规则最大 vs 订单上限) + const maxDeductByPoints = userPointsBn.dividedBy(pointsPerYuanBn); + const maxDeductAmount = maxDeductByPoints.isLessThan( + rule.maxDeductionAmount ?? Infinity + ) + ? maxDeductByPoints + : new BigNumber(rule.maxDeductionAmount || Infinity).isLessThan(maxLimitBn) + ? maxDeductByPoints + : maxLimitBn; + + // 实际使用积分 = 抵扣金额 * 积分兑换比例 + const usedPoints = maxDeductAmount.multipliedBy(pointsPerYuanBn); + + return { + deductionAmount: truncateToTwoDecimals(maxDeductAmount.toNumber()), + usedPoints: truncateToTwoDecimals( + Math.min(usedPoints.toNumber(), userPoints) + ), // 避免积分超扣 + }; +} + +// ============================ 6. 订单总费用汇总与实付金额计算(核心入口,补充细分字段) ============================ +/** + * 计算订单所有费用子项并汇总(核心入口函数) + * @param goodsList 购物车商品列表 + * @param dinnerType 就餐类型 + * @param backendCoupons 后端优惠券列表 + * @param activities 全局营销活动列表 + * @param config 订单额外配置(会员、积分、餐位费等) + * @param cartOrder 商品加入购物车顺序(key=购物车ID,value=时间戳) + * @param currentTime 当前时间(用于优惠券有效期判断) + * @returns 订单费用汇总(含所有子项和实付金额,新增商品券/满减券细分) + */ +export function calculateOrderCostSummary( + goodsList: BaseCartItem[], + dinnerType: "dine-in" | "take-out", + backendCoupons: BackendCoupon[] = [], + activities: ActivityConfig[] = [], + config: OrderExtraConfig, + cartOrder: Record = {}, + currentTime: Date = new Date() +): OrderCostSummary { + console.log("goodsList", goodsList); + // 1. 基础费用计算(原有逻辑不变) + const goodsOriginalAmount = calcGoodsOriginalAmount(goodsList); + // 商品总件数 + const goodsTotal = goodsList.reduce((total, goods) => { + return total + Math.max(0, goods.number - (goods.returnNum || 0)); + }, 0); + const goodsRealAmount = calcGoodsRealAmount( + goodsList, + { + isMember: config.isMember, + memberDiscountRate: config.memberDiscountRate, + }, + activities + ); + const goodsDiscountAmount = calcGoodsDiscountAmount( + goodsOriginalAmount, + goodsRealAmount + ); + + // 2. 优惠券抵扣(原有逻辑不变) + const { + deductionAmount: couponDeductionAmount, + usedCoupon, + excludedProductIds, + productCouponDeduction, + fullCouponDeduction, + } = calcCouponDeduction(backendCoupons, goodsList, { + currentStoreId: config.currentStoreId, + isMember: config.isMember, + memberDiscountRate: config.memberDiscountRate, + activities, + cartOrder, + dinnerType, + currentTime, + }); + + // 3. 其他费用计算(原有逻辑不变) + // 新客立减 + const newUserDiscount = config.newUserDiscount || 0; + const packFee = calcTotalPackFee(goodsList, dinnerType); + let seatFee = calcSeatFee(config.seatFeeConfig); + seatFee = dinnerType === "dine-in" ? seatFee : 0; // 外卖不收餐位费 + const additionalFee = Math.max(0, config.additionalFee); + + // 4. 积分抵扣(原有逻辑不变,先于商家减免计算) + const maxPointDeductionLimit = Math.max( + 0, + goodsRealAmount - couponDeductionAmount + ); + const { deductionAmount: pointDeductionAmount, usedPoints } = + calcPointDeduction( + config.userPoints, + config.pointDeductionRule, + maxPointDeductionLimit + ); + + // ============================ 新增:商家减免计算(支持两种形式) ============================ + // 商家减免计算时机:在商品折扣、优惠券、积分抵扣之后(避免过度减免) + const merchantReductionConfig = config.merchantReduction; + let merchantReductionActualAmount = 0; + + // 计算商家减免的可抵扣上限:商品实际金额 - 优惠券 - 积分(避免减免后为负) 再加上餐位费和打包费 + let maxMerchantReductionLimit = new BigNumber(goodsRealAmount) + .minus(couponDeductionAmount) + .minus(pointDeductionAmount) + .isGreaterThan(0) + ? new BigNumber(goodsRealAmount) + .minus(couponDeductionAmount) + .minus(pointDeductionAmount) + : new BigNumber(0); + + maxMerchantReductionLimit = maxMerchantReductionLimit.plus(seatFee).plus(packFee) + + + 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. 最终实付金额计算(整合所有费用) + // 先计算减去所有折扣后的金额,并确保最小值为0 + const discountedAmount = new BigNumber(goodsOriginalAmount) // 商品原价总和 + .minus(goodsDiscountAmount) // 减去商品折扣 + .minus(couponDeductionAmount) // 减去优惠券抵扣 + .minus(newUserDiscount) // 新客立减 + .minus(pointDeductionAmount) // 减去积分抵扣 + // .minus(merchantReductionActualAmount); // 减去商家实际减免金额 + + // 确保折扣后金额不小于0,再加上后续费用 + const nonNegativeAmount = discountedAmount.gt(0) ? discountedAmount : new BigNumber(0); + const finalPayAmount = nonNegativeAmount + .plus(seatFee) // 加上餐位费(不参与减免) + .plus(packFee) // 加上打包费(不参与减免) + .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 { + // 商品总件数 + goodsTotal, + goodsRealAmount, + goodsOriginalAmount, + goodsDiscountAmount, + couponDeductionAmount, + productCouponDeduction: truncateToTwoDecimals(productCouponDeduction || 0), + fullCouponDeduction: truncateToTwoDecimals(fullCouponDeduction || 0), + pointDeductionAmount, + seatFee, + packFee, + // 全部优惠金额 + totalDiscountAmount, + // 商家减免明细(含类型、原始配置、实际金额) + merchantReduction: { + type: merchantReductionConfig.type, + originalConfig: merchantReductionConfig, + actualAmount: truncateToTwoDecimals(merchantReductionActualAmount), + }, + additionalFee, + finalPayAmount: truncateToTwoDecimals(finalPayAmountNonNegative), + couponUsed: usedCoupon, + pointUsed: usedPoints, + newUserDiscount, + dinnerType, + config + }; +} + +export function isWeightGoods(goods: BaseCartItem): boolean { + return goods.product_type === GoodsType.WEIGHT; +} + +// ============================ 7. 对外暴露工具库 ============================ +export const OrderPriceCalculator = { + // 基础工具 + truncateToTwoDecimals, + isTemporaryGoods, + isGiftGoods, + formatDiscountRate, + filterThresholdGoods, + isWeightGoods, + // 商品价格计算 + calcSingleGoodsRealPrice, + calcGoodsOriginalAmount, + calcGoodsRealAmount, + calcGoodsDiscountAmount, + // 优惠券计算 + calcCouponDeduction, + // 其他费用计算 + calcTotalPackFee, + calcSeatFee, + calcPointDeduction, + // 核心入口 + calculateOrderCostSummary, + // 枚举导出 + Enums: { + GoodsType, + CouponType, + ActivityType, + }, +}; + +export default OrderPriceCalculator; diff --git a/src/utils/goods.ts b/src/utils/goods.ts index 59146b7..f8d7caf 100644 --- a/src/utils/goods.ts +++ b/src/utils/goods.ts @@ -236,6 +236,8 @@ export interface OrderExtraConfig { isMember: boolean; // 用户是否会员(用于会员优惠) memberDiscountRate?: number; // 会员折扣率(如0.95=95折,无会员价时用) newUserDiscount?: number; // 新用户减免金额(元,默认0) + fullReductionActivities: FullReductionActivity[]; // 当前店铺的满减活动列表(后端返回结构) + currentDinnerType: "dine" | "pickup" | "deliv" | "express"; // 当前就餐类型(匹配useType) } /** 订单费用汇总(修改:补充商家减免类型和明细) */ @@ -252,6 +254,7 @@ export interface OrderCostSummary { pointDeductionAmount: number; // 积分抵扣金额 seatFee: number; // 餐位费 packFee: number; // 打包费 + scoreMaxMoney: number; // 积分最大可抵扣金额 // 新增:商家减免明细 merchantReduction: { type: MerchantReductionType; // 实际使用的减免类型 @@ -265,10 +268,184 @@ export interface OrderCostSummary { newUserDiscount: number; // 新用户减免金额(元,默认0) dinnerType?: "dine-in" | "take-out"; // 就餐类型(堂食/自取/配送/快递) config: OrderExtraConfig; // 订单额外费用配置 + //满减活动 + fullReduction: { + usedActivity?: FullReductionActivity; // 实际使用的满减活动 + usedThreshold?: FullReductionThreshold; // 实际使用的满减阈值(多门槛中选最优) + actualAmount: number; // 满减实际减免金额(元) + }; +} + + +/** 满减活动阈值(单条满减规则:满X减Y)- 对应 MkDiscountThresholdInsertGroupDefaultGroup */ +export interface FullReductionThreshold { + activityId?: number; // 关联满减活动ID + fullAmount?: number; // 满多少金额(元,必填) + discountAmount?: number; // 减多少金额(元,必填) +} + +/** 满减活动主表 - 对应 Request 接口(后端真实字段) */ +export interface FullReductionActivity { + id?: number; // 自增主键(后端字段:id) + shopId?: number; // 店铺ID(后端字段:shopId) + status?: number; // 活动状态:1=未开始,2=进行中,3=已结束(后端字段:status) + sort?: number; // 排序值(越大优先级越高,后端字段:sort) + createTime?: string; // 创建时间(后端字段:createTime,格式如"2025-10-14 13:56:07") + updateTime?: string; // 最新修改时间(后端字段:updateTime,用于优先级排序) + validStartTime?: string; // 有效期开始时间(后端字段:validStartTime,格式如"2025-10-14") + validEndTime?: string; // 有效期结束时间(后端字段:validEndTime,格式如"2025-12-14") + useType?: string; // 可使用类型(后端字段:useType,如"dine,pickup,deliv,express") + useDays?: string; // 可用周期(后端字段:useDays,如"周一,周二,周三,周四,周五,周六,周日") + useTimeType?: string; // 可用时间段类型(后端字段:useTimeType,all=全时段,custom=指定时段) + useStartTime?: string; // 每日可用开始时间(后端字段:useStartTime,如"09:00:00",仅custom时有效) + useEndTime?: string; // 每日可用结束时间(后端字段:useEndTime,如"22:00:00",仅custom时有效) + couponShare?: number; // 与优惠券同享:0=否,1=是(后端字段:couponShare) + discountShare?: number; // 与限时折扣同享:0=否,1=是(后端字段:discountShare) + vipPriceShare?: number; // 与会员价同享:0=否,1=是(后端字段:vipPriceShare) + pointsShare?: number; // 与积分抵扣同享:0=否,1=是(后端字段:pointsShare) + thresholds?: FullReductionThreshold[]; // 满减阈值列表(多门槛,后端字段:thresholds) + isDel?: boolean; // 是否删除:0=否,1=是(后端字段:isDel,默认false) +} + +// ============================ 扩展:订单配置与费用汇总(加入后端满减类型) ============================ +/** 扩展订单额外配置:使用后端满减活动类型 */ +export interface OrderExtraConfig { + // ... 原有字段不变 ... + fullReductionActivities: FullReductionActivity[]; // 当前店铺的满减活动列表(后端返回结构) + currentDinnerType: "dine" | "pickup" | "deliv" | "express"; // 当前就餐类型(匹配useType) } +// 辅助枚举:星期映射(用于useDays校验) +const WEEKDAY_MAP = { + "周一": 1, + "周二": 2, + "周三": 3, + "周四": 4, + "周五": 5, + "周六": 6, + "周日": 0 // JS中getDay()返回0=周日 +}; + +/** + * 辅助:校验当前时间是否在活动的「每日可用时段」内 + * @param activity 满减活动 + * @param currentTime 当前时间 + * @returns 是否在时段内 + */ +function isInDailyTimeRange(activity: FullReductionActivity, currentTime: Date): boolean { + // 全时段无需校验 + if (activity.useTimeType === "all") return true; + // 无时段配置则不通过 + if (!activity.useStartTime || !activity.useEndTime) return false; + + const [startHour, startMinute] = activity.useStartTime.split(":").map(Number); + const [endHour, endMinute] = activity.useEndTime.split(":").map(Number); + const currentHour = currentTime.getHours(); + const currentMinute = currentTime.getMinutes(); + + // 转换为分钟数比较 + const startTotalMin = startHour * 60 + startMinute; + const endTotalMin = endHour * 60 + endMinute; + const currentTotalMin = currentHour * 60 + currentMinute; + + // 处理跨天场景(如23:00-02:00) + if (startTotalMin <= endTotalMin) { + return currentTotalMin >= startTotalMin && currentTotalMin <= endTotalMin; + } else { + return currentTotalMin >= startTotalMin || currentTotalMin <= endTotalMin; + } +} + +/** + * 辅助:校验当前时间是否在活动的「可用周期」内(如周一至周日) + * @param activity 满减活动 + * @param currentTime 当前时间 + * @returns 是否在周期内 + */ +function isInWeeklyCycle(activity: FullReductionActivity, currentTime: Date): boolean { + // 无周期配置则不通过 + if (!activity.useDays) return false; + const currentWeekday = currentTime.getDay(); // 0=周日,1=周一...6=周六 + const allowedWeekdays = activity.useDays.split(",").map(day => WEEKDAY_MAP[day as keyof typeof WEEKDAY_MAP]); + return allowedWeekdays.includes(currentWeekday); +} + +/** + * 辅助:校验当前就餐类型是否在活动的「可用类型」内(如堂食/自取) + * @param activity 满减活动 + * @param currentDinnerType 当前就餐类型 + * @returns 是否匹配 + */ +function isDinnerTypeMatch(activity: FullReductionActivity, currentDinnerType: string): boolean { + if (!activity.useType) return false; + const allowedTypes = activity.useType.split(","); + //满减活动的就餐类型和当前券类型字段值不一样,暂时返回true + return true +} +/** + * 筛选最优满减活动(对齐后端逻辑:状态→时间→周期→时段→就餐类型→排序→修改时间) + * @param activities 后端返回的满减活动列表 + * @param currentShopId 当前店铺ID + * @param currentDinnerType 当前就餐类型(dine/pickup等) + * @param currentTime 当前时间(默认当前时间) + * @returns 最优满减活动(无符合条件则返回undefined) + */ +export function filterOptimalFullReductionActivity( + activities: FullReductionActivity[], + currentShopId: number, + currentDinnerType: string, + currentTime: Date = new Date() +): FullReductionActivity | undefined { + if (!activities.length) return undefined; + console.log("原始满减活动列表:", activities); + // 第一步:基础筛选(未删除+当前店铺+活动进行中+就餐类型匹配) + const baseEligible = activities.filter(activity => { + return ( + activity.isDel !== true && // 未删除 + activity.shopId === currentShopId && // 当前店铺 + activity.status === 2 && // 状态=2(进行中) + isDinnerTypeMatch(activity, currentDinnerType) && // 就餐类型匹配 + activity.thresholds?.length // 至少有一个满减阈值 + ); + }); + + if (!baseEligible.length) return undefined; + + // 第二步:时间筛选(有效期内+周期内+时段内) + const timeEligible = baseEligible.filter(activity => { + // 1. 校验有效期(validStartTime ~ validEndTime) + if (!activity.validStartTime || !activity.validEndTime) return false; + const startDate = new Date(activity.validStartTime); + const endDate = new Date(activity.validEndTime); + // 处理有效期结束日期的23:59:59 + endDate.setHours(23, 59, 59, 999); + if (currentTime < startDate || currentTime > endDate) return false; + + // 2. 校验可用周期(如周一至周日) + if (!isInWeeklyCycle(activity, currentTime)) return false; + + // 3. 校验每日可用时段(如09:00-22:00) + if (!isInDailyTimeRange(activity, currentTime)) return false; + + return true; + }); + + if (!timeEligible.length) return undefined; + + // 第三步:按优先级排序(需求规则) + return timeEligible.sort((a, b) => { + // 1. 先比排序值:排序值大的优先 + if ((a.sort || 0) !== (b.sort || 0)) { + return (b.sort || 0) - (a.sort || 0); + } + // 2. 再比修改时间:最新修改的优先(时间戳降序) + const aUpdateTime = a.updateTime ? new Date(a.updateTime).getTime() : 0; + const bUpdateTime = b.updateTime ? new Date(b.updateTime).getTime() : 0; + return bUpdateTime - aUpdateTime; + })[0]; // 取最优活动 +} /** * 折扣率格式化:后端discountRate(%)→ 工具库小数(如90→0.9) */ @@ -599,476 +776,83 @@ export function calcGoodsDiscountAmount( return truncateToTwoDecimals(Math.max(0, discount.toNumber())); } +/** + * 从满减活动的多门槛中,选择最优阈值(满金额最小且减免金额最大) + * @param thresholds 满减阈值列表(多门槛) + * @param baseAmount 满减计算基数(新客立减后的金额) + * @param goodsOriginalAmount 商品原价总和(discountShare=0时用) + * @param goodsRealAmount 商品折扣后总和(discountShare=1时用) + * @param discountShare 与限时折扣同享:0=否,1=是 + * @returns 最优阈值(无符合条件则返回undefined) + */ +export function selectOptimalThreshold( + thresholds: FullReductionThreshold[] = [], + baseAmount: number, + goodsOriginalAmount: number, + goodsRealAmount: number, + discountShare: number = 0 +): FullReductionThreshold | undefined { + if (!thresholds.length) return undefined; -// ============================ 4. 优惠券抵扣计算(策略模式,核心修正:按商品ID匹配) ============================ -/** 优惠券计算策略接口(新增优惠券类型时,实现此接口即可) */ -interface CouponCalculationStrategy { - calculate( - coupon: Coupon, - goodsList: BaseCartItem[], - config: Pick< - OrderExtraConfig, - "currentStoreId" | "isMember" | "memberDiscountRate" - > & { - activities: ActivityConfig[]; - cartOrder: Record; - excludedProductIds?: string[]; // 需排除的商品ID列表(商品ID) + // 第一步:确定满减门槛基数(根据discountShare规则) + const thresholdBase = discountShare === 1 + ? new BigNumber(goodsRealAmount) // 与限时折扣同享→用折扣后金额 + : new BigNumber(goodsOriginalAmount); // 不同享→用原价 + + // 第二步:筛选「满金额≤基数」且「减免金额>0」的有效阈值 + const validThresholds = thresholds.filter(threshold => { + const fullAmount = new BigNumber(threshold.fullAmount || 0); + const discountAmount = new BigNumber(threshold.discountAmount || 0); + return fullAmount.isLessThanOrEqualTo(thresholdBase) && discountAmount.isGreaterThan(0); + }); + + if (!validThresholds.length) return undefined; + + // 第三步:选择最优阈值(优先级:1.满金额最小 → 2.减免金额最大) + return validThresholds.sort((a, b) => { + const aFull = new BigNumber(a.fullAmount || 0); + const bFull = new BigNumber(b.fullAmount || 0); + const aDiscount = new BigNumber(a.discountAmount || 0); + const bDiscount = new BigNumber(b.discountAmount || 0); + + // 先比满金额:越小越优先(满1减10 比 满100减20 更优) + if (!aFull.isEqualTo(bFull)) { + return aFull.comparedTo(bFull) || 0; // Ensure a number is always returned } - ): { - deductionAmount: number; - excludedProductIds: string[]; // 排除的商品ID列表(商品ID) - productCouponDeduction?: number; // 新增:当前券属于商品券时的抵扣金额 - fullCouponDeduction?: number; // 新增:当前券属于满减券时的抵扣金额 - }; + // 再比减免金额:越大越优先 + return bDiscount.comparedTo(aDiscount) || 0; // Ensure a number is always returned + })[0]; } -/** 满减券计算策略(按商品ID筛选门槛商品) */ -class FullReductionStrategy implements CouponCalculationStrategy { - calculate( - coupon: FullReductionCoupon, - goodsList: BaseCartItem[], - config: any - ): { - deductionAmount: number; - excludedProductIds: string[]; - fullCouponDeduction: number; - } { - // 1. 基础校验:优惠券不可用/门店不匹配 → 抵扣0 - if ( - !coupon.available || - !isStoreMatchByList(coupon.useShops, config.currentStoreId) - ) { - return { - deductionAmount: 0, - excludedProductIds: [], - fullCouponDeduction: 0, - }; - } +/** + * 计算满减实际减免金额(适配多门槛、同享规则) + * @param optimalActivity 最优满减活动 + * @param optimalThreshold 最优满减阈值 + * @param baseAmount 计算基数(新客立减后的金额) + * @returns 实际减免金额(元,未达门槛则0) + */ +export function calcFullReductionAmount( + baseAmount: number, + optimalActivity?: FullReductionActivity, + optimalThreshold?: FullReductionThreshold, +): number { + if (!optimalActivity || !optimalThreshold) return 0; - // 2. 第一步:排除临时菜/赠菜/已抵扣商品(基础合格商品,按商品ID排除) - const baseEligibleGoods = filterCouponEligibleGoods( - goodsList, - config.excludedProductIds || [] - ); - // 3. 第二步:按商品ID筛选门槛商品(匹配applicableProductIds) - const thresholdGoods = filterThresholdGoods( - baseEligibleGoods, - coupon.applicableProductIds - ); + const baseAmountBn = new BigNumber(baseAmount); + const discountAmountBn = new BigNumber(optimalThreshold.discountAmount || 0); - if (thresholdGoods.length === 0) { - return { - deductionAmount: 0, - excludedProductIds: [], - fullCouponDeduction: 0, - }; - } + // 1. 基数必须为正(避免减免后为负) + if (baseAmountBn.isLessThanOrEqualTo(0)) return 0; - // 4. 按同享规则计算门槛金额(按商品ID匹配活动) - const thresholdAmount = new BigNumber( - calcCouponThresholdAmount( - thresholdGoods, - coupon, - { - isMember: config.isMember, - memberDiscountRate: config.memberDiscountRate, - }, - config.activities - ) - ); + // 2. 减免金额不能超过基数(避免减成负数) + const maxReducible = baseAmountBn; + const actualReduction = discountAmountBn.isLessThanOrEqualTo(maxReducible) + ? discountAmountBn + : maxReducible; - // 5. 满门槛则抵扣,否则0(不超过最大减免) - if (thresholdAmount.isLessThan(coupon.fullAmount)) { - return { - deductionAmount: 0, - excludedProductIds: [], - fullCouponDeduction: 0, - }; - } - - const maxReduction = coupon.maxDiscountAmount ?? coupon.discountAmount; - const deductionAmount = truncateToTwoDecimals( - new BigNumber(coupon.discountAmount).isLessThan(maxReduction) - ? coupon.discountAmount - : maxReduction - ); - - // 满减券计入满减优惠券抵扣 - return { - deductionAmount, - excludedProductIds: [], - fullCouponDeduction: deductionAmount, - }; - } + return truncateToTwoDecimals(actualReduction.toNumber()); } -/** 折扣券计算策略(按商品ID筛选门槛商品) */ -class DiscountStrategy implements CouponCalculationStrategy { - calculate( - coupon: DiscountCoupon, - goodsList: BaseCartItem[], - config: any - ): { - deductionAmount: number; - excludedProductIds: string[]; - productCouponDeduction: number; - } { - // 1. 基础校验:优惠券不可用/门店不匹配 → 抵扣0 - if ( - !coupon.available || - !isStoreMatchByList(coupon.useShops, config.currentStoreId) - ) { - return { - deductionAmount: 0, - excludedProductIds: [], - productCouponDeduction: 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: [], - productCouponDeduction: 0, - }; - } - - // 4. 按同享规则计算折扣基数(按商品ID匹配活动) - const discountBaseAmount = new BigNumber( - calcCouponThresholdAmount( - thresholdGoods, - coupon, - { - isMember: config.isMember, - memberDiscountRate: config.memberDiscountRate, - }, - config.activities - ) - ); - - // 5. 计算折扣金额(不超过最大减免) - const discountAmount = discountBaseAmount.multipliedBy( - new BigNumber(1).minus(coupon.discountRate) - ); - const deductionAmount = truncateToTwoDecimals( - discountAmount.isLessThan(coupon.maxDiscountAmount) - ? discountAmount.toNumber() - : coupon.maxDiscountAmount - ); - - // 折扣券计入商品优惠券抵扣(可根据业务调整归类) - return { - deductionAmount, - excludedProductIds: [], - productCouponDeduction: deductionAmount, - }; - } -} - -/** 第二件半价券计算策略(按商品ID分组,筛选门槛商品) */ -class SecondHalfPriceStrategy implements CouponCalculationStrategy { - calculate( - coupon: SecondHalfPriceCoupon, - goodsList: BaseCartItem[], - config: any - ): { - deductionAmount: number; - excludedProductIds: string[]; - productCouponDeduction: number; - } { - // 1. 基础校验:优惠券不可用/门店不匹配 → 抵扣0 - if ( - !coupon.available || - !isStoreMatchByList(coupon.useShops, config.currentStoreId) - ) { - return { - deductionAmount: 0, - excludedProductIds: [], - productCouponDeduction: 0, - }; - } - - let totalDeduction = new BigNumber(0); - const excludedProductIds: string[] = []; // 存储排除的商品ID(商品ID) - - // 2. 第一步:排除临时菜/赠菜/已抵扣商品(基础合格商品,按商品ID排除) - const baseEligibleGoods = filterCouponEligibleGoods( - goodsList, - config.excludedProductIds || [] - ); - // 3. 第二步:按商品ID筛选门槛商品(匹配applicableProductIds) - const thresholdGoods = filterThresholdGoods( - baseEligibleGoods, - coupon.applicableProductIds - ); - if (thresholdGoods.length === 0) { - return { - deductionAmount: 0, - excludedProductIds: [], - productCouponDeduction: 0, - }; - } - - // 按商品ID分组(避免同商品多次处理,用商品ID作为分组key) - const goodsGroupByProductId = thresholdGoods.reduce((group, goods) => { - const productIdStr = String(goods.product_id); // 商品ID转字符串作为key - if (!group[productIdStr]) group[productIdStr] = []; - group[productIdStr].push(goods); - return group; - }, {} as Record); - - // 遍历每组商品计算半价优惠 - for (const [productIdStr, productGoods] of Object.entries( - goodsGroupByProductId - )) { - if (excludedProductIds.includes(productIdStr)) continue; // 按商品ID排除已处理商品 - - // 合并同商品数量(所有购物车项的数量总和) - const totalNum = productGoods.reduce( - (sum, g) => sum + (g.number - (g.returnNum || 0)), - 0 - ); - if (totalNum < 2) continue; // 至少2件才享受优惠 - - // 计算优惠次数(每2件1次,不超过最大使用次数) - const discountCount = Math.min( - Math.floor(totalNum / 2), - coupon.maxUseCountPerOrder || Infinity - ); - if (discountCount <= 0) continue; - - // 计算单件实际价格(取组内任意一个商品的配置,同商品规格一致) - const sampleGood = productGoods[0]; - const activity = config.activities.find( - (act: ActivityConfig) => - (act.applicableProductIds || []).includes(productIdStr) // 按商品ID匹配活动 - ); - const realPrice = new BigNumber( - calcSingleGoodsRealPrice(sampleGood, { - isMember: config.isMember, - memberDiscountRate: config.memberDiscountRate, - activity, - }) - ); - - // 累计抵扣金额并标记已优惠商品(记录商品ID) - totalDeduction = totalDeduction.plus( - realPrice.multipliedBy(0.5).multipliedBy(discountCount) - ); - excludedProductIds.push(productIdStr); - } - - const deductionAmount = truncateToTwoDecimals(totalDeduction.toNumber()); - // 第二件半价券计入商品优惠券抵扣 - return { - deductionAmount, - excludedProductIds, - productCouponDeduction: deductionAmount, - }; - } -} - -/** 买一送一券计算策略(按商品ID分组,筛选门槛商品) */ -class BuyOneGetOneStrategy implements CouponCalculationStrategy { - calculate( - coupon: BuyOneGetOneCoupon, - goodsList: BaseCartItem[], - config: any - ): { - deductionAmount: number; - excludedProductIds: string[]; - productCouponDeduction: number; - } { - // 1. 基础校验:优惠券不可用/门店不匹配 → 抵扣0 - if ( - !coupon.available || - !isStoreMatchByList(coupon.useShops, config.currentStoreId) - ) { - return { - deductionAmount: 0, - excludedProductIds: [], - productCouponDeduction: 0, - }; - } - - let totalDeduction = new BigNumber(0); - const excludedProductIds: string[] = []; // 存储排除的商品ID(商品ID) - - // 2. 第一步:排除临时菜/赠菜/已抵扣商品(基础合格商品,按商品ID排除) - const baseEligibleGoods = filterCouponEligibleGoods( - goodsList, - config.excludedProductIds || [] - ); - // 3. 第二步:按商品ID筛选门槛商品(匹配applicableProductIds) - const thresholdGoods = filterThresholdGoods( - baseEligibleGoods, - coupon.applicableProductIds - ); - if (thresholdGoods.length === 0) { - return { - deductionAmount: 0, - excludedProductIds: [], - productCouponDeduction: 0, - }; - } - - // 按商品ID分组(用商品ID作为分组key) - const goodsGroupByProductId = thresholdGoods.reduce((group, goods) => { - const productIdStr = String(goods.product_id); - if (!group[productIdStr]) group[productIdStr] = []; - group[productIdStr].push(goods); - return group; - }, {} as Record); - - // 遍历每组商品计算买一送一优惠 - for (const [productIdStr, productGoods] of Object.entries( - goodsGroupByProductId - )) { - if (excludedProductIds.includes(productIdStr)) continue; // 按商品ID排除已处理商品 - - // 合并同商品数量 - const totalNum = productGoods.reduce( - (sum, g) => sum + (g.number - (g.returnNum || 0)), - 0 - ); - if (totalNum < 2) continue; // 至少2件才享受优惠 - - // 计算优惠次数(每2件送1件) - const discountCount = Math.floor(totalNum / 2); - if (discountCount <= 0) continue; - - // 计算单件实际价格(按商品ID匹配活动) - const sampleGood = productGoods[0]; - const activity = config.activities.find( - (act: ActivityConfig) => - (act.applicableProductIds || []).includes(productIdStr) // 按商品ID匹配活动 - ); - const realPrice = new BigNumber( - calcSingleGoodsRealPrice(sampleGood, { - isMember: config.isMember, - memberDiscountRate: config.memberDiscountRate, - activity, - }) - ); - - // 累计抵扣金额(送1件=减免1件价格)并标记商品ID - totalDeduction = totalDeduction.plus( - realPrice.multipliedBy(1).multipliedBy(discountCount) - ); - excludedProductIds.push(productIdStr); - } - - const deductionAmount = truncateToTwoDecimals(totalDeduction.toNumber()); - // 买一送一券计入商品优惠券抵扣 - return { - deductionAmount, - excludedProductIds, - productCouponDeduction: deductionAmount, - }; - } -} - -/** 商品兑换券计算策略(按商品ID筛选门槛商品) */ -class ExchangeCouponStrategy implements CouponCalculationStrategy { - calculate( - coupon: ExchangeCoupon, - goodsList: BaseCartItem[], - config: any - ): { - deductionAmount: number; - excludedProductIds: string[]; - productCouponDeduction: number; - } { - // 1. 基础校验:优惠券不可用/门店不匹配 → 抵扣0 - if ( - !coupon.available || - !isStoreMatchByList(coupon.useShops, config.currentStoreId) - ) { - return { - deductionAmount: 0, - excludedProductIds: [], - productCouponDeduction: 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: [], - productCouponDeduction: 0, - }; - } - - // 按规则排序商品(按价格/数量/加入顺序) - const sortedGoods = sortGoodsForCoupon( - thresholdGoods, - coupon.sortRule, - config.cartOrder - ); - let remainingCount = coupon.deductCount; - let totalDeduction = new BigNumber(0); - const excludedProductIds: string[] = []; // 存储排除的商品ID(商品ID) - - // 计算兑换抵扣金额(按商品ID累计,避免重复抵扣) - const usedProductIds = new Set(); // 记录已使用的商品ID(避免同商品多次抵扣) - for (const goods of sortedGoods) { - if (remainingCount <= 0) break; - const productIdStr = String(goods.product_id); - if (usedProductIds.has(productIdStr)) continue; // 同商品仅抵扣一次 - - const availableNum = Math.max(0, goods.number - (goods.returnNum || 0)); - if (availableNum === 0) continue; - - // 计算当前商品可抵扣的件数(不超过剩余可抵扣数量) - const deductCount = Math.min(availableNum, remainingCount); - const activity = config.activities.find( - (act: ActivityConfig) => - (act.applicableProductIds || []).includes(productIdStr) // 按商品ID匹配活动 - ); - const realPrice = new BigNumber( - calcSingleGoodsRealPrice(goods, { - isMember: config.isMember, - memberDiscountRate: config.memberDiscountRate, - activity, - }) - ); - - // 累计抵扣金额并标记商品 - totalDeduction = totalDeduction.plus(realPrice.multipliedBy(deductCount)); - excludedProductIds.push(productIdStr); - usedProductIds.add(productIdStr); - remainingCount -= deductCount; - } - - const deductionAmount = truncateToTwoDecimals(totalDeduction.toNumber()); - // 商品兑换券计入商品优惠券抵扣 - return { - deductionAmount, - excludedProductIds, - productCouponDeduction: deductionAmount, - }; - } -} // ------------------------------ 策略辅助函数 ------------------------------ /** @@ -1255,183 +1039,205 @@ export function calcPointDeduction( */ export function calculateOrderCostSummary( goodsList: BaseCartItem[], - dinnerType: "dine-in" | "take-out", + dinnerType: "dine-in" | "take-out", // 前端就餐类型 backendCoupons: BackendCoupon[] = [], activities: ActivityConfig[] = [], - config: OrderExtraConfig, + config: OrderExtraConfig, // 含后端满减活动、currentDinnerType cartOrder: Record = {}, currentTime: Date = new Date() ): OrderCostSummary { - console.log("goodsList", goodsList); - // 1. 基础费用计算(原有逻辑不变) - const goodsOriginalAmount = calcGoodsOriginalAmount(goodsList); - // 商品总件数 - const goodsTotal = goodsList.reduce((total, goods) => { - return total + Math.max(0, goods.number - (goods.returnNum || 0)); - }, 0); - const goodsRealAmount = calcGoodsRealAmount( + // ------------------------------ 1. 基础费用计算 ------------------------------ + const goodsOriginalAmount = calcGoodsOriginalAmount(goodsList); // 商品原价总和 + const goodsRealAmount = calcGoodsRealAmount( // 商品折扣后总和 goodsList, - { - isMember: config.isMember, - memberDiscountRate: config.memberDiscountRate, - }, + { isMember: config.isMember, memberDiscountRate: config.memberDiscountRate }, activities ); - const goodsDiscountAmount = calcGoodsDiscountAmount( - goodsOriginalAmount, - goodsRealAmount + const goodsDiscountAmount = calcGoodsDiscountAmount(goodsOriginalAmount, goodsRealAmount); // 商品折扣金额 + const newUserDiscount = config.newUserDiscount || 0; // 新客立减 + + // ------------------------------ 2. 满减活动计算(核心步骤) ------------------------------ + let usedFullReductionActivity: FullReductionActivity | undefined; + let usedFullReductionThreshold: FullReductionThreshold | undefined; + let fullReductionAmount = 0; + + // 2.1 筛选最优满减活动(后端活动列表、当前店铺、就餐类型、时间) + usedFullReductionActivity = filterOptimalFullReductionActivity( + config.fullReductionActivities, + Number(config.currentStoreId), // 转换为数字(后端shopId是number) + config.currentDinnerType, // 后端useType匹配的就餐类型(如"dine") + currentTime ); - // 2. 优惠券抵扣(原有逻辑不变) - const { - deductionAmount: couponDeductionAmount, - usedCoupon, - excludedProductIds, - productCouponDeduction, - fullCouponDeduction, - } = calcCouponDeduction(backendCoupons, goodsList, { - currentStoreId: config.currentStoreId, - isMember: config.isMember, - memberDiscountRate: config.memberDiscountRate, - activities, - cartOrder, - dinnerType, - currentTime, - }); + // 2.2 计算满减基数(先扣新客立减) + const baseAfterNewUserDiscount = new BigNumber(goodsRealAmount) + .minus(newUserDiscount) + .isGreaterThan(0) + ? new BigNumber(goodsRealAmount).minus(newUserDiscount).toNumber() + : 0; - // 3. 其他费用计算(原有逻辑不变) - // 新客立减 - const newUserDiscount = config.newUserDiscount || 0; - const packFee = calcTotalPackFee(goodsList, dinnerType); - let seatFee = calcSeatFee(config.seatFeeConfig); - seatFee = dinnerType === "dine-in" ? seatFee : 0; // 外卖不收餐位费 - const additionalFee = Math.max(0, config.additionalFee); + // 2.3 选择最优满减阈值(多门槛场景) + if (usedFullReductionActivity) { + usedFullReductionThreshold = selectOptimalThreshold( + usedFullReductionActivity.thresholds, + baseAfterNewUserDiscount, + goodsOriginalAmount, + goodsRealAmount, + usedFullReductionActivity.discountShare || 0 // 与限时折扣同享规则 + ); - // 4. 积分抵扣(原有逻辑不变,先于商家减免计算) - const maxPointDeductionLimit = Math.max( - 0, - goodsRealAmount - couponDeductionAmount - ); - const { deductionAmount: pointDeductionAmount, usedPoints } = - calcPointDeduction( + // 2.4 计算满减实际减免金额 + fullReductionAmount = calcFullReductionAmount( + baseAfterNewUserDiscount, + usedFullReductionActivity, + usedFullReductionThreshold, + ); + } + + // ------------------------------ 3. 优惠券抵扣(适配满减同享规则) ------------------------------ + let couponDeductionAmount = 0; + let productCouponDeduction = 0; + let fullCouponDeduction = 0; + let usedCoupon: Coupon | undefined; + let excludedProductIds: string[] = []; + + // 若满减与优惠券同享(couponShare=1),才计算优惠券;否则优惠券抵扣为0 + if (usedFullReductionActivity?.couponShare === 1) { + const couponResult = calcCouponDeduction( // 原有优惠券计算函数 + backendCoupons, + goodsList, + { + currentStoreId: config.currentStoreId, + isMember: config.isMember, + memberDiscountRate: config.memberDiscountRate, + activities, + cartOrder, + dinnerType, + currentTime + } + ); + couponDeductionAmount = couponResult.deductionAmount; + productCouponDeduction = couponResult.productCouponDeduction; + fullCouponDeduction = couponResult.fullCouponDeduction; + usedCoupon = couponResult.usedCoupon; + excludedProductIds = couponResult.excludedProductIds; + } + + // ------------------------------ 4. 积分抵扣(适配满减同享规则) ------------------------------ + let pointDeductionAmount = 0; + let usedPoints = 0; + + // 计算积分抵扣基数(商品折扣后 - 新客立减 - 满减 - 优惠券) + const maxPointDeductionLimit = new BigNumber(goodsRealAmount) + .minus(newUserDiscount) + .minus(fullReductionAmount) + .minus(couponDeductionAmount) + .isGreaterThan(0) + ? new BigNumber(goodsRealAmount).minus(newUserDiscount).minus(fullReductionAmount).minus(couponDeductionAmount).toNumber() + : 0; + + // 若满减与积分同享(pointsShare=1),才计算积分;否则积分抵扣为0 + if ((usedFullReductionActivity?.pointsShare === 1) && maxPointDeductionLimit > 0) { + const pointResult = calcPointDeduction( config.userPoints, config.pointDeductionRule, maxPointDeductionLimit ); + pointDeductionAmount = pointResult.deductionAmount; + usedPoints = pointResult.usedPoints; + } - // ============================ 新增:商家减免计算(支持两种形式) ============================ - // 商家减免计算时机:在商品折扣、优惠券、积分抵扣之后(避免过度减免) + // ------------------------------ 5. 其他费用计算(原有逻辑不变) ------------------------------ + const packFee = calcTotalPackFee(goodsList, dinnerType); // 打包费 + let seatFee = calcSeatFee(config.seatFeeConfig); // 餐位费 + seatFee = dinnerType === "dine-in" ? seatFee : 0; // 外卖不收餐位费 + const additionalFee = Math.max(0, config.additionalFee); // 附加费 + + // 商家减免计算(原有逻辑不变) const merchantReductionConfig = config.merchantReduction; let merchantReductionActualAmount = 0; - - // 计算商家减免的可抵扣上限:商品实际金额 - 优惠券 - 积分(避免减免后为负) 再加上餐位费和打包费 - let maxMerchantReductionLimit = new BigNumber(goodsRealAmount) + const maxMerchantReductionLimit = new BigNumber(goodsRealAmount) + .minus(newUserDiscount) + .minus(fullReductionAmount) .minus(couponDeductionAmount) .minus(pointDeductionAmount) + .plus(seatFee) + .plus(packFee) .isGreaterThan(0) - ? new BigNumber(goodsRealAmount) - .minus(couponDeductionAmount) - .minus(pointDeductionAmount) - : new BigNumber(0); - - maxMerchantReductionLimit = maxMerchantReductionLimit.plus(seatFee).plus(packFee) - + ? new BigNumber(goodsRealAmount).minus(newUserDiscount).minus(fullReductionAmount).minus(couponDeductionAmount).minus(pointDeductionAmount).plus(seatFee).plus(packFee).toNumber() + : 0; switch (merchantReductionConfig.type) { case MerchantReductionType.FIXED_AMOUNT: - // 固定金额减免:取配置金额与上限的最小值,且不小于0 - const fixedAmount = new BigNumber( - merchantReductionConfig.fixedAmount || 0 - ); - merchantReductionActualAmount = fixedAmount.isLessThanOrEqualTo( + merchantReductionActualAmount = Math.min( + merchantReductionConfig.fixedAmount || 0, 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() - ); + const validRate = Math.max(0, Math.min(100, merchantReductionConfig.discountRate || 0)) / 100; + merchantReductionActualAmount = maxMerchantReductionLimit * (1 - validRate); break; } + merchantReductionActualAmount = Math.max(0, truncateToTwoDecimals(merchantReductionActualAmount)); - // 5. 最终实付金额计算(整合所有费用) - // 先计算减去所有折扣后的金额,并确保最小值为0 - const discountedAmount = new BigNumber(goodsOriginalAmount) // 商品原价总和 - .minus(goodsDiscountAmount) // 减去商品折扣 - .minus(couponDeductionAmount) // 减去优惠券抵扣 - .minus(newUserDiscount) // 新客立减 - .minus(pointDeductionAmount) // 减去积分抵扣 - // .minus(merchantReductionActualAmount); // 减去商家实际减免金额 + // ------------------------------ 6. 最终实付金额计算 ------------------------------ + const finalPayAmountBn = new BigNumber(goodsRealAmount) + .minus(newUserDiscount) + .minus(fullReductionAmount) + .minus(couponDeductionAmount) + .minus(pointDeductionAmount) + .minus(merchantReductionActualAmount) + .plus(seatFee) + .plus(packFee) + .plus(additionalFee); + const finalPayAmount = Math.max(0, truncateToTwoDecimals(finalPayAmountBn.toNumber())); - // 确保折扣后金额不小于0,再加上后续费用 - const nonNegativeAmount = discountedAmount.gt(0) ? discountedAmount : new BigNumber(0); - const finalPayAmount = nonNegativeAmount - .plus(seatFee) // 加上餐位费(不参与减免) - .plus(packFee) // 加上打包费(不参与减免) - .plus(additionalFee) // 加上附加费 - .minus(merchantReductionActualAmount); // 减去商家实际减免金额 + // ------------------------------ 7. 总优惠金额计算 ------------------------------ + const totalDiscountAmount = truncateToTwoDecimals( + new BigNumber(goodsDiscountAmount) + .plus(newUserDiscount) + .plus(fullReductionAmount) + .plus(couponDeductionAmount) + .plus(pointDeductionAmount) + .plus(merchantReductionActualAmount) + .toNumber() + ); + //积分可抵扣最大金额 最终支付金额+积分抵扣-商家减免 + const scoreMaxMoney = new BigNumber(finalPayAmount) + .plus(pointDeductionAmount) + .minus(merchantReductionActualAmount) + .toNumber() - 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. 返回完整费用汇总(包含商家减免明细) + // ------------------------------ 8. 返回完整结果 ------------------------------ return { - // 商品总件数 - goodsTotal, + goodsTotal: goodsList.reduce((sum, g) => sum + Math.max(0, g.number - (g.returnNum || 0)), 0), goodsRealAmount, goodsOriginalAmount, goodsDiscountAmount, couponDeductionAmount, - productCouponDeduction: truncateToTwoDecimals(productCouponDeduction || 0), - fullCouponDeduction: truncateToTwoDecimals(fullCouponDeduction || 0), + productCouponDeduction, + fullCouponDeduction, pointDeductionAmount, seatFee, packFee, - // 全部优惠金额 totalDiscountAmount, - // 商家减免明细(含类型、原始配置、实际金额) + //积分最大可抵扣金额 + scoreMaxMoney, + // 满减活动明细(后端字段) + fullReduction: { + usedActivity: usedFullReductionActivity, + usedThreshold: usedFullReductionThreshold, + actualAmount: truncateToTwoDecimals(fullReductionAmount) + }, merchantReduction: { type: merchantReductionConfig.type, originalConfig: merchantReductionConfig, - actualAmount: truncateToTwoDecimals(merchantReductionActualAmount), + actualAmount: merchantReductionActualAmount }, additionalFee, - finalPayAmount: truncateToTwoDecimals(finalPayAmountNonNegative), + finalPayAmount, couponUsed: usedCoupon, pointUsed: usedPoints, newUserDiscount, @@ -1458,6 +1264,8 @@ export const OrderPriceCalculator = { calcGoodsOriginalAmount, calcGoodsRealAmount, calcGoodsDiscountAmount, + //满减活动工具 + filterOptimalFullReductionActivity, // 优惠券计算 calcCouponDeduction, // 其他费用计算 @@ -1471,6 +1279,7 @@ export const OrderPriceCalculator = { GoodsType, CouponType, ActivityType, + WEEKDAY_MAP, }, }; From 6f64dafab7dd5a0b7f2b766887fa7c7578ab4b91 Mon Sep 17 00:00:00 2001 From: YeMingfei666 <1619116647@qq.com> Date: Wed, 15 Oct 2025 10:33:59 +0800 Subject: [PATCH 3/9] =?UTF-8?q?add:=20=E5=A2=9E=E5=8A=A0=E7=94=9F=E6=97=A5?= =?UTF-8?q?=E6=9C=89=E7=A4=BC=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/marketing_center/birthdayGift/index.vue | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/views/marketing_center/birthdayGift/index.vue diff --git a/src/views/marketing_center/birthdayGift/index.vue b/src/views/marketing_center/birthdayGift/index.vue new file mode 100644 index 0000000..fd13e7b --- /dev/null +++ b/src/views/marketing_center/birthdayGift/index.vue @@ -0,0 +1,3 @@ + \ No newline at end of file From bb47531a2bf68e1cbd0fb7f56f75af6765624a5f Mon Sep 17 00:00:00 2001 From: YeMingfei666 <1619116647@qq.com> Date: Wed, 15 Oct 2025 10:48:56 +0800 Subject: [PATCH 4/9] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E8=B4=AD=E7=89=A9?= =?UTF-8?q?=E8=BD=A6=E8=AE=A2=E5=8D=95=E8=AE=A1=E7=AE=97=E6=8A=A5=E9=94=99?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/store/modules/carts.ts | 15 +++++++++------ src/utils/goods.ts | 8 ++++---- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/store/modules/carts.ts b/src/store/modules/carts.ts index bc8a1ee..9f27c22 100644 --- a/src/store/modules/carts.ts +++ b/src/store/modules/carts.ts @@ -13,7 +13,7 @@ import { BackendCoupon, ActivityConfig, OrderExtraConfig, MerchantReductionConfig, MerchantReductionType, - GoodsType, + GoodsType, FullReductionActivity } from "@/utils/goods"; const shopUser = useUserStoreHook(); @@ -217,7 +217,7 @@ export const useCartsStore = defineStore("carts", () => { }) //使用积分数量 const userPoints = ref(0); - const testFullReductionActivity = { + const testFullReductionActivity: FullReductionActivity = { "id": 231, "shopId": 26, "status": 2, // 2=进行中 @@ -229,8 +229,8 @@ export const useCartsStore = defineStore("carts", () => { "useType": "dine,pickup,deliv,express", // 支持所有就餐类型 "useDays": "周一,周二,周三,周四,周五,周六,周日", // 全周期 "useTimeType": "all", // 全时段 - "useStartTime": null, - "useEndTime": null, + "useStartTime": '', + "useEndTime": '', "couponShare": 0, // 与优惠券不同享 "discountShare": 0, // 与限时折扣不同享 "vipPriceShare": 0, // 与会员价不同享 @@ -242,7 +242,7 @@ export const useCartsStore = defineStore("carts", () => { "discountAmount": 10 // 减10元 } ], - "isDel": false + "isDel": false, }; // 订单额外配置(现在依赖响应式的 merchantReduction) const orderExtraConfig = computed(() => ({ @@ -255,7 +255,9 @@ export const useCartsStore = defineStore("carts", () => { userPoints: userPoints.value, isMember: useVipPrice.value, memberDiscountRate: shopUser.userInfo.memberDiscountRate || 1, - fullReductionActivities: [testFullReductionActivity] + fullReductionActivities: [testFullReductionActivity], + currentDinnerType: dinnerType.value + })); // 营销活动列表 @@ -304,6 +306,7 @@ export const useCartsStore = defineStore("carts", () => { const orderCostSummary = computed(() => { allGoods.value = getAllGoodsList(); console.log(' allGoods.value', allGoods.value); + console.log(' orderExtraConfig.value', orderExtraConfig.value); const costSummary = OrderPriceCalculator.calculateOrderCostSummary( allGoods.value, dinnerType.value, diff --git a/src/utils/goods.ts b/src/utils/goods.ts index f8d7caf..38b9e00 100644 --- a/src/utils/goods.ts +++ b/src/utils/goods.ts @@ -237,7 +237,8 @@ export interface OrderExtraConfig { memberDiscountRate?: number; // 会员折扣率(如0.95=95折,无会员价时用) newUserDiscount?: number; // 新用户减免金额(元,默认0) fullReductionActivities: FullReductionActivity[]; // 当前店铺的满减活动列表(后端返回结构) - currentDinnerType: "dine" | "pickup" | "deliv" | "express"; // 当前就餐类型(匹配useType) + currentDinnerType: "dine-in" | "take-out" | "take-away" | "post"; // 当前就餐类型(匹配useType) + } /** 订单费用汇总(修改:补充商家减免类型和明细) */ @@ -312,7 +313,7 @@ export interface FullReductionActivity { export interface OrderExtraConfig { // ... 原有字段不变 ... fullReductionActivities: FullReductionActivity[]; // 当前店铺的满减活动列表(后端返回结构) - currentDinnerType: "dine" | "pickup" | "deliv" | "express"; // 当前就餐类型(匹配useType) + currentDinnerType: "dine-in" | "take-out" | "take-away" | "post"; // 当前就餐类型(匹配useType) } @@ -398,7 +399,7 @@ export function filterOptimalFullReductionActivity( currentDinnerType: string, currentTime: Date = new Date() ): FullReductionActivity | undefined { - if (!activities.length) return undefined; + if (!activities || !activities.length) return undefined; console.log("原始满减活动列表:", activities); // 第一步:基础筛选(未删除+当前店铺+活动进行中+就餐类型匹配) const baseEligible = activities.filter(activity => { @@ -1060,7 +1061,6 @@ export function calculateOrderCostSummary( let usedFullReductionActivity: FullReductionActivity | undefined; let usedFullReductionThreshold: FullReductionThreshold | undefined; let fullReductionAmount = 0; - // 2.1 筛选最优满减活动(后端活动列表、当前店铺、就餐类型、时间) usedFullReductionActivity = filterOptimalFullReductionActivity( config.fullReductionActivities, From 64b13d543f83b5bf65539c406cb021959a7c91e0 Mon Sep 17 00:00:00 2001 From: YeMingfei666 <1619116647@qq.com> Date: Wed, 15 Oct 2025 10:49:08 +0800 Subject: [PATCH 5/9] =?UTF-8?q?fix:=20=E5=88=A0=E9=99=A4=E6=97=A0=E7=94=A8?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/marketing_center/index.vue | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 src/views/marketing_center/index.vue diff --git a/src/views/marketing_center/index.vue b/src/views/marketing_center/index.vue deleted file mode 100644 index 7c2aa3f..0000000 --- a/src/views/marketing_center/index.vue +++ /dev/null @@ -1,3 +0,0 @@ - From 056992c8a0ad570bd36885a8ae485827071d1ec2 Mon Sep 17 00:00:00 2001 From: YeMingfei666 <1619116647@qq.com> Date: Wed, 15 Oct 2025 10:49:33 +0800 Subject: [PATCH 6/9] =?UTF-8?q?fix:=20=E5=A2=9E=E5=8A=A0=E7=94=9F=E6=97=A5?= =?UTF-8?q?=E6=9C=89=E7=A4=BC=E8=B7=B3=E8=BD=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/marketing_center/list.vue | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/views/marketing_center/list.vue b/src/views/marketing_center/list.vue index 1b6ff69..5c0870e 100644 --- a/src/views/marketing_center/list.vue +++ b/src/views/marketing_center/list.vue @@ -56,7 +56,12 @@ const menus = ref([ { name: "弹窗广告", icon: "tcgg", pathName: "", intro: "设置弹窗广告" }, { name: "超级会员", icon: "cjhy", pathName: "superVip", intro: "用户会员管理设置" }, { name: "新客立减", icon: "xklj", pathName: "newUserDiscount", intro: "首单下单减免金额" }, - { name: "智慧充值", icon: "zhcz", pathName: "wisdom_recharge", intro: "允许客户充值并使用余额支付" }, + { + name: "智慧充值", + icon: "zhcz", + pathName: "wisdom_recharge", + intro: "允许客户充值并使用余额支付", + }, { name: "分销", icon: "zhcz", pathName: "", intro: "允许客户充值并使用余额支付" }, { name: "消费返现", @@ -71,7 +76,7 @@ const menus = ref([ intro: "可设置用户下单成功后的群二维码", }, { name: "满减活动", icon: "mjhd", pathName: "", intro: "达到指定支付金额享受减价" }, - { name: "生日有礼", icon: "sryl", pathName: "", intro: "用户生日管理设置" }, + { name: "生日有礼", icon: "sryl", pathName: "birthdayGift", intro: "用户生日管理设置" }, { name: "点餐智能推荐", icon: "dczntj", From 36e14ed43406a02472d1e36dc1ed895c05affdc1 Mon Sep 17 00:00:00 2001 From: YeMingfei666 <1619116647@qq.com> Date: Wed, 15 Oct 2025 15:47:58 +0800 Subject: [PATCH 7/9] =?UTF-8?q?add:=20=E5=A2=9E=E5=8A=A0=E7=94=9F=E6=97=A5?= =?UTF-8?q?=E6=9C=89=E7=A4=BC=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/market/birthdayGift.ts | 35 +++ src/assets/applocation/birthdayGift.png | Bin 0 -> 3323 bytes .../birthdayGift/components/coup-lists.vue | 79 +++++++ .../birthdayGift/components/dialog-plans.vue | 132 +++++++++++ .../birthdayGift/components/record-lists.vue | 125 ++++++++++ .../marketing_center/birthdayGift/index.vue | 218 +++++++++++++++++- src/views/marketing_center/data.js | 17 ++ 7 files changed, 604 insertions(+), 2 deletions(-) create mode 100644 src/api/market/birthdayGift.ts create mode 100644 src/assets/applocation/birthdayGift.png create mode 100644 src/views/marketing_center/birthdayGift/components/coup-lists.vue create mode 100644 src/views/marketing_center/birthdayGift/components/dialog-plans.vue create mode 100644 src/views/marketing_center/birthdayGift/components/record-lists.vue create mode 100644 src/views/marketing_center/data.js diff --git a/src/api/market/birthdayGift.ts b/src/api/market/birthdayGift.ts new file mode 100644 index 0000000..edca022 --- /dev/null +++ b/src/api/market/birthdayGift.ts @@ -0,0 +1,35 @@ +import request from "@/utils/request"; +import { Market_BaseUrl } from "@/api/config"; +const baseURL = Market_BaseUrl + "/admin/birthdayGift"; +const API = { + getConfig(params: any) { + return request({ + url: `${baseURL}`, + method: "get", + params + }); + }, + editConfig(data: any) { + return request({ + url: `${baseURL}`, + method: "post", + data + }); + }, + getRecord(params: any) { + return request({ + url: `${baseURL}/record`, + method: "get", + params + }); + }, + getSummary(params: any) { + return request({ + url: `${baseURL}/summary`, + method: "get", + params + }); + }, +} +export default API; + diff --git a/src/assets/applocation/birthdayGift.png b/src/assets/applocation/birthdayGift.png new file mode 100644 index 0000000000000000000000000000000000000000..6abda3eb809c94e6c72faa09950d9db072a9bd1b GIT binary patch literal 3323 zcmVXwX4Fbc;qi#alHDzy`lSGX z`H2%+rle+BYDc5^R~Qa-g~y53TIN7Vup|;EHN)B)W)%Oz;lQv&8_h`ZzNUjJ4V9^i z|Aoter0zaet}zLdOXIBNu14`MTn==!{dOFCz%Ajn8@J};zytv3=}U-%;c}oC0H7v? z+icvL<3L*gaFO^K0S8(p01+@cSCcUSxi}M2dJI5FjLy|e3_vcXj0OaCKNa-aDKv|j8yV7n((WVD zwu9;Pi#tf*m6T4I1KY9lqV^KvX8h$+zb-D-%R}CDN^yru1^`6Pt9#ST zWHq3M-akTh03Z)}(>aSfR5Ad-#x>ie>JJHJr)>u$@??KooT()Rz=t)`+;wRX(1IXX z3ot)_P9hKSQlXLp0H(rlh%iit2$Kd8E+r~C0Q}6YIUzy*v{DO~GytW*?1uUn1JDqM z3j96>puh}nsQRJ;0D~H**!rlg8QaY6mKs+p>pet-G~U%$mUpy4{$e2;LiAn1f;q9j z^nf{5=H6(kW<7)h2BRCjuIMa5gh}IFL*+TRjkzpsQ=)8?6gdExmmj^!JX=F07&Ek| zR3Ba2UFy)rV_A7y2_i-s@9HbdJDfLjMoABimj-vIWV9410NC_r-4KmC5&O|J1D!rZ z2|;84Ad7Fm+HCmc(G4H*b*H&9@h?Z!i^u>#bYM{P4}AAw>4|?!OI|4)VZQda+4s{D zjvjwnnBm9(V9<~oEEdd1&AGDpC1d`pg`>qUO0?zL?bJ@{)Yiqqe?|sC=1{TW`|bUi z?}LhCFQToW>dUvDq5&Y^fz&BqLXjANh{1{hhyjS00}(eLuYeeUh&d2(^95f4T$tqo z?c%SR!~71Z>Q0ZA+>$n=!2>Ln503TTxjcn<(gtnOCd6r&zPPqlO_3JeEOE`ze@-E9 zA*i4LU}wSB!(C3Uk7M;L#2|vK6L7E-(Pt>gy0g_)FaQ|Heb;-I%~D`|CUfXNmQz-6 z=DQ%{1p}au*;uaeT_p^%(_z(h%$)vEA=@h$02sNtwoi9`|1s&8o22P0v(tj-rGLC7 zy|7za_8)21GuiXG^7pSXS#7o>@;<%Q?E4#~nXBqAL)qESdi}uQM_Dd%Rqe37$B6cy ze^>h07{AL5gY&IVm@d+z2tE`b0ChW;xq)$ihrx1nFq61Ug-7g5eI!1#;1=|d9^M74 z%)G?6?w5x5mMD{=eiCI6b-d3;`#)0ZGp1C2^S-$!Vy5y^38D_#ryk7Zqb(gL5tiG! zqYExpFaR>Uhye`BOz(1mbnj4)ISicl$lqJ9l%@lyqBv<@MzZ+T7G@ zO)L&q3OKK|VE!h8sixstV8)L!1^~k*UnMc?vem%A`S{L>fL@)XI|fPTw@&9ieo8v& zyS9##XXGUkzUH}=N_|P0>G5JEeqbVL$N!|Q?@LZ(5k~eOI{n2>x)q5DG59kQPJaim z|9w-#p#eY#S^kItHvr^(j|`V2dj)wN!AJ3c?VKLR28nT|1;h zIAYj`*zmdJo1!XX>ble93*#697|Qu+fQhu>z5FTpg+m2^uCnQIDrRQiWy_j0N81ld z+;H~04VnEjuQv_Hh)vq%O(vz)Z%e;UE}ri*bO7iu8WI~UX9eOA3}kk) z0@fIVNOMGffMf73?R`^gSD4yLe4{aU{syId$pDBd>(5J`oyP3FM+u!9z4?K3;Nx>E zb1u0f0U%okQ7zY+E)Eb+ye4hgD>2Kn)~-qebn9OQ%c<}%G?{si3kI5zCr%FX=1o*jN>dtK){jAesH5nv;Q_{8bEgC#T^i2ccb zJCAtgK0+V=WrxMHTWnfNXaMZAljXD(N`2vi3}%Ca{`Ozf&;PEJCgLB$8+O>b*YYFZ zU)&=hm)F?}xo@>npQZwYKYwO;^RBv|ue$e$CbX&ROr!bYWfE%Z)Ikz7X@F9<^Vsw` zpJZDhXk46>U48jJLj%Bdl$q<-LNHHM=lJd`wK7bsq_h>&RFLnlnb<)LxcYKEnep!8 zZmv8{Nk=?Eu57-~&;g*sFc7T+B1~E?VjnpAiG*&>?8}v}q4W(qJv$Tne`D&w287t; zOMF0g^|LA<7ti+@2>|(ax-sPe)_>2a)(&gzE376~5;ySJxY@K3g%Dv3a-0FkH@8Y+ zFY(WK{#yj7F@0!Eo@NWc;Tq|%??gMoPR9!;NZg=i6<|SqaJ}kSU9rF}+$=q78#v74 zBXCR^>>0!%Rzo)BfOk!yW(xq?3*=`Cr)abtG$06#$7w+v45yjVj&x{FrF5IAp={dz zI*7LkkZS=}PpH8VKw%@=WEvBjG`66r%cUpH;D^QrGtq!JBM=UP znk4`n(wMC<2zBz{XoiS2ckMD^lgl0rt06N(YX^17_dyY_ZFw`so zfHf=4P>0NBBmVvn2~7t1&pU{ffms0~9kU<83X6t>kAXZ8Hlg`rJ?)9vj|TMC0p0{d zfdRlQ!QPLS0S(Id01mr8}I zo%GZ=W*vnHqZ#qJEBR)b2M^ycGapg5)bga9mOI84+-OWadA$l~Ax07y!t| zAZJFt!`x>M8|+7Hq?~ZsWZ8g`^~`2wHQyF@YcE}5j(>=-f1hB(MJsw_lUZQRJkGw6 zZ@)6j2L=FU<)vo1WWZ5VRkef`<5WimP1<}@MF9fhKm4Q5xGMeqMLop;hZ@T2nO7zK z)lBBpQ3whQ0G2z46?EsQ2?nFzXEKZ#U2UdsHfZGeLmQ>p=JWOUcc%Fp=KRwGu&P>O zC814a))iDdFaUz;P)n;P1|VX@V*p|RBIZEE&BrSs1|VV%MBMy2tbmAn+Dw~|0f-pb z7=R)Gz;|~b;@-5p=1(S+kU8L5-pGq&#Jduz)runNOiS#>pA*S4nfXZp`gmF*ADcAh z{k8$GPP9LwwJOtNO{yX;)N~G%X-T`Is!FW9r|fv`Cwjzc+H4g6!r{PXcn +
+
+ + + + + + +
+ + 删除 + +
+
+
+
+ + + + 新增券 +
+
+
+ + \ No newline at end of file diff --git a/src/views/marketing_center/birthdayGift/components/dialog-plans.vue b/src/views/marketing_center/birthdayGift/components/dialog-plans.vue new file mode 100644 index 0000000..301853c --- /dev/null +++ b/src/views/marketing_center/birthdayGift/components/dialog-plans.vue @@ -0,0 +1,132 @@ + + + \ No newline at end of file diff --git a/src/views/marketing_center/birthdayGift/components/record-lists.vue b/src/views/marketing_center/birthdayGift/components/record-lists.vue new file mode 100644 index 0000000..6d32527 --- /dev/null +++ b/src/views/marketing_center/birthdayGift/components/record-lists.vue @@ -0,0 +1,125 @@ + + + + + \ No newline at end of file diff --git a/src/views/marketing_center/birthdayGift/index.vue b/src/views/marketing_center/birthdayGift/index.vue index fd13e7b..d0cc785 100644 --- a/src/views/marketing_center/birthdayGift/index.vue +++ b/src/views/marketing_center/birthdayGift/index.vue @@ -1,3 +1,217 @@ \ No newline at end of file +
+ + + + + + + + + +
+ + + + + diff --git a/src/views/marketing_center/data.js b/src/views/marketing_center/data.js new file mode 100644 index 0000000..378a87e --- /dev/null +++ b/src/views/marketing_center/data.js @@ -0,0 +1,17 @@ +export const userTypes = [ + { + label: "全部用户", + value: "all", + }, + { + label: "非会员用户", + value: "non_vip", + }, + { + label: "仅会员用户", + value: "vip", + }, +]; +export const returnUserType = (type) => { + return userTypes.find((item) => item.value === type)?.label; +}; From 46270cf7cc588b4da846396c94a4ecb69baab3b6 Mon Sep 17 00:00:00 2001 From: YeMingfei666 <1619116647@qq.com> Date: Wed, 15 Oct 2025 16:36:40 +0800 Subject: [PATCH 8/9] =?UTF-8?q?fix:=20=E7=94=9F=E6=97=A5=E6=9C=89=E7=A4=BC?= =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../birthdayGift/components/record-lists.vue | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/views/marketing_center/birthdayGift/components/record-lists.vue b/src/views/marketing_center/birthdayGift/components/record-lists.vue index 6d32527..fa5e408 100644 --- a/src/views/marketing_center/birthdayGift/components/record-lists.vue +++ b/src/views/marketing_center/birthdayGift/components/record-lists.vue @@ -1,18 +1,22 @@