商品优惠券
-¥{{ 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-pGqJduz)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 @@
+
+
+
+
+
+
+
+ {{ item.label }}
+
+
+
+
+
+
+ {{ item.label }}
+
+
+ 天
+
+
+
+
+
+
+
+
+
+ 取消
+ {{ isedit ? "更新" : "提交" }}
+
+
+
+
+
+
\ 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+
+
+
+
+
+
+
发放总数量{{ summary.totalNum || 0 }}
+
+ {{ summary.totalNum || 0 }}
+
+
+
+
已使用优惠券
+
+ {{ summary.usedNum || 0 }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ scope.row.nickName }}
+
+
+
+
+ {{ scope.row.phone }}
+
+
+
+
+
+
+
+
+ {{ item.couponName }}
+ {{ item.num }}张
+
+
+
+
+
+
+
+ {{ item.couponName }}
+ {{ item.num }}张
+
+
+
+
+
+
+
+
+
+
+
\ 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
+
+
+
+
+
+
+
+ 添加
+
+
+
+
+
+ {{ returnUserType(scope.row.userType) }}
+
+
+
+
+
+ {{ item.title }}
+ {{ item.num }} 张
+
+
+
+
+
+ {{ returnDeliverDate(scope.row) }}
+
+
+
+
+
+ 编辑
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+ 开启后,将会在用户生日当天,发送祝福短信
+
+
+
+
+ 亲爱的{用户昵称},[店铺名称]祝您生日快乐!感谢您一直的陪伴。为您准备了{num}张超值优惠券,已经放入账户,愿我们的礼物能为您增添一份喜悦。
+
+
+
+
+
+
+ 保存
+
+ 取消
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
-
-
-
-
-
+
+
-
+
-
+ 用户名称/手机号
+
@@ -92,6 +96,7 @@ const query = reactive({
page: 1,
size: 10,
key: "",
+ dateTime: "",
});
const tableData = reactive({
From 1329891ded860ee39b605baad31e789833d589ec Mon Sep 17 00:00:00 2001
From: YeMingfei666 <1619116647@qq.com>
Date: Fri, 17 Oct 2025 11:28:24 +0800
Subject: [PATCH 9/9] =?UTF-8?q?fix:=20=E7=94=9F=E6=97=A5=E6=9C=89=E7=A4=BC?=
=?UTF-8?q?=E4=BF=AE=E6=94=B9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../marketing_center/birthdayGift/components/dialog-plans.vue | 4 ++--
src/views/marketing_center/birthdayGift/index.vue | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/views/marketing_center/birthdayGift/components/dialog-plans.vue b/src/views/marketing_center/birthdayGift/components/dialog-plans.vue
index 301853c..1f07d48 100644
--- a/src/views/marketing_center/birthdayGift/components/dialog-plans.vue
+++ b/src/views/marketing_center/birthdayGift/components/dialog-plans.vue
@@ -49,7 +49,7 @@ const show = ref(false);
// 发放时间选项
const deliverDates = ref([
- { value: "mouth", label: "生日前一天" },
+ { value: "mouth", label: "当月1号" },
{ value: "day", label: "当天" },
{ value: "advance", label: "提前" },
]);
@@ -95,7 +95,7 @@ function submit() {
} else {
//提前多少天
submitData.deliverDate = "day";
- submitData.deliverTime = submitData.deliverTime * -1;
+ submitData.deliverTime = Math.abs(submitData.deliverTime);
}
emits("submitSuccess", submitData, dataIndex);
diff --git a/src/views/marketing_center/birthdayGift/index.vue b/src/views/marketing_center/birthdayGift/index.vue
index d0cc785..d688213 100644
--- a/src/views/marketing_center/birthdayGift/index.vue
+++ b/src/views/marketing_center/birthdayGift/index.vue
@@ -125,7 +125,7 @@ function editPlan(row, index) {
}
function returnDeliverDate(row) {
if (row.deliverDate === "advance") {
- return "提前" + row.deliverTime + "天";
+ return "提前" + Math.abs(row.deliverTime) + "天";
}
if (row.deliverDate === "day") {
if (row.deliverTime != 0) {
@@ -134,7 +134,7 @@ function returnDeliverDate(row) {
return "当天";
}
if (row.deliverDate === "mouth") {
- return "生日前一天";
+ return "当月1号";
}
}
function deletePlan(row) {