diff --git a/src/assets/icons/inventory-management.svg b/src/assets/icons/inventory-management.svg new file mode 100644 index 0000000..9534261 --- /dev/null +++ b/src/assets/icons/inventory-management.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/icons/sms_notification.svg b/src/assets/icons/sms_notification.svg new file mode 100644 index 0000000..0b4e8e2 --- /dev/null +++ b/src/assets/icons/sms_notification.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/utils/goods.ts b/src/utils/goods.ts index 38b9e00..71ab3f8 100644 --- a/src/utils/goods.ts +++ b/src/utils/goods.ts @@ -223,6 +223,17 @@ export interface MerchantReductionConfig { fixedAmount?: number; // 固定减免金额(元,仅 FIXED_AMOUNT 生效,≥0) discountRate?: number; // 折扣率(%,仅 DISCOUNT_RATE 生效,0-100,如 90 代表 9 折) } +/**商家霸王餐配置 */ +export interface FreeDineConfig { + enable: boolean; //是否开启 + rechargeThreshold: number; //订单满多少元可以使用 + rechargeTimes: number; //充值多少倍免单 + withCoupon: boolean; //与优惠券同享 + withPoints: boolean; //与积分同享 + useType?: string[]; //使用类型 dine-in店内 takeout 自取 post快递,takeaway外卖 + useShopType?: string; //all 全部 part部分 + shopIdList?: number[]; //可用门店id +} /** 订单额外费用配置 */ export interface OrderExtraConfig { // merchantReduction: number; // 商家减免金额(元,默认0) @@ -238,14 +249,15 @@ export interface OrderExtraConfig { newUserDiscount?: number; // 新用户减免金额(元,默认0) fullReductionActivities: FullReductionActivity[]; // 当前店铺的满减活动列表(后端返回结构) currentDinnerType: "dine-in" | "take-out" | "take-away" | "post"; // 当前就餐类型(匹配useType) - + isFreeDine?: boolean; //是否霸王餐 + freeDineConfig?: FreeDineConfig; } /** 订单费用汇总(修改:补充商家减免类型和明细) */ export interface OrderCostSummary { // 商品总件数 goodsTotal: number; - totalDiscountAmount: number, + totalDiscountAmount: number; goodsRealAmount: number; // 商品真实原价总和 goodsOriginalAmount: number; // 商品原价总和 goodsDiscountAmount: number; // 商品折扣金额 @@ -275,9 +287,10 @@ export interface OrderCostSummary { usedThreshold?: FullReductionThreshold; // 实际使用的满减阈值(多门槛中选最优) actualAmount: number; // 满减实际减免金额(元) }; + // 订单原支付金额 + orderOriginFinalPayAmount: number; //订单原金额(包含打包费+餐位费) } - /** 满减活动阈值(单条满减规则:满X减Y)- 对应 MkDiscountThresholdInsertGroupDefaultGroup */ export interface FullReductionThreshold { activityId?: number; // 关联满减活动ID @@ -308,25 +321,15 @@ export interface FullReductionActivity { isDel?: boolean; // 是否删除:0=否,1=是(后端字段:isDel,默认false) } -// ============================ 扩展:订单配置与费用汇总(加入后端满减类型) ============================ -/** 扩展订单额外配置:使用后端满减活动类型 */ -export interface OrderExtraConfig { - // ... 原有字段不变 ... - fullReductionActivities: FullReductionActivity[]; // 当前店铺的满减活动列表(后端返回结构) - currentDinnerType: "dine-in" | "take-out" | "take-away" | "post"; // 当前就餐类型(匹配useType) -} - - - // 辅助枚举:星期映射(用于useDays校验) const WEEKDAY_MAP = { - "周一": 1, - "周二": 2, - "周三": 3, - "周四": 4, - "周五": 5, - "周六": 6, - "周日": 0 // JS中getDay()返回0=周日 + 周一: 1, + 周二: 2, + 周三: 3, + 周四: 4, + 周五: 5, + 周六: 6, + 周日: 0, // JS中getDay()返回0=周日 }; /** @@ -335,7 +338,10 @@ const WEEKDAY_MAP = { * @param currentTime 当前时间 * @returns 是否在时段内 */ -function isInDailyTimeRange(activity: FullReductionActivity, currentTime: Date): boolean { +function isInDailyTimeRange( + activity: FullReductionActivity, + currentTime: Date +): boolean { // 全时段无需校验 if (activity.useTimeType === "all") return true; // 无时段配置则不通过 @@ -365,11 +371,16 @@ function isInDailyTimeRange(activity: FullReductionActivity, currentTime: Date): * @param currentTime 当前时间 * @returns 是否在周期内 */ -function isInWeeklyCycle(activity: FullReductionActivity, currentTime: Date): boolean { +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]); + const allowedWeekdays = activity.useDays + .split(",") + .map((day) => WEEKDAY_MAP[day as keyof typeof WEEKDAY_MAP]); return allowedWeekdays.includes(currentWeekday); } @@ -379,11 +390,14 @@ function isInWeeklyCycle(activity: FullReductionActivity, currentTime: Date): bo * @param currentDinnerType 当前就餐类型 * @returns 是否匹配 */ -function isDinnerTypeMatch(activity: FullReductionActivity, currentDinnerType: string): boolean { +function isDinnerTypeMatch( + activity: FullReductionActivity, + currentDinnerType: string +): boolean { if (!activity.useType) return false; const allowedTypes = activity.useType.split(","); //满减活动的就餐类型和当前券类型字段值不一样,暂时返回true - return true + return true; } /** * 筛选最优满减活动(对齐后端逻辑:状态→时间→周期→时段→就餐类型→排序→修改时间) @@ -400,22 +414,20 @@ export function filterOptimalFullReductionActivity( currentTime: Date = new Date() ): FullReductionActivity | undefined { if (!activities || !activities.length) return undefined; - console.log("原始满减活动列表:", activities); // 第一步:基础筛选(未删除+当前店铺+活动进行中+就餐类型匹配) - const baseEligible = activities.filter(activity => { + const baseEligible = activities.filter((activity) => { return ( activity.isDel !== true && // 未删除 - activity.shopId === currentShopId && // 当前店铺 - activity.status === 2 && // 状态=2(进行中) + // activity.shopId === currentShopId && // 当前店铺 + // activity.status === 2 && // 状态=2(进行中) isDinnerTypeMatch(activity, currentDinnerType) && // 就餐类型匹配 activity.thresholds?.length // 至少有一个满减阈值 ); }); - if (!baseEligible.length) return undefined; // 第二步:时间筛选(有效期内+周期内+时段内) - const timeEligible = baseEligible.filter(activity => { + const timeEligible = baseEligible.filter((activity) => { // 1. 校验有效期(validStartTime ~ validEndTime) if (!activity.validStartTime || !activity.validEndTime) return false; const startDate = new Date(activity.validStartTime); @@ -485,6 +497,13 @@ export function isTemporaryGoods(goods: BaseCartItem): boolean { export function isGiftGoods(goods: BaseCartItem): boolean { return !!goods.is_gift; } +/** + * 判断可用类型是否可用 + */ +export function useTypeCanUse(useType: string[]) { + const arr = ["all", "dine-in", "take-out", "take-away", "post"]; + return useType.some((item) => arr.includes(item)); +} /** * 计算单个商品的会员价(优先级:SKU会员价 > 商品会员价 > 会员折扣率) @@ -713,11 +732,8 @@ export function calcGoodsOriginalAmount(goodsList: BaseCartItem[]): number { 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原价优先 + } else { + basePrice = new BigNumber(goods.skuData?.salePrice ?? goods.salePrice); // SKU原价优先 } total = total.plus(basePrice.multipliedBy(availableNum)); @@ -796,33 +812,47 @@ export function selectOptimalThreshold( if (!thresholds.length) return undefined; // 第一步:确定满减门槛基数(根据discountShare规则) - const thresholdBase = discountShare === 1 - ? new BigNumber(goodsRealAmount) // 与限时折扣同享→用折扣后金额 - : new BigNumber(goodsOriginalAmount); // 不同享→用原价 + const thresholdBase = baseAmount; // 第二步:筛选「满金额≤基数」且「减免金额>0」的有效阈值 - const validThresholds = thresholds.filter(threshold => { + 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); + 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); + // const sortValidThresholds = 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 - } - // 再比减免金额:越大越优先 - return bDiscount.comparedTo(aDiscount) || 0; // Ensure a number is always returned - })[0]; + // // 先比满金额:越小越优先(满1减10 比 满100减20 更优) + // if (!aFull.isEqualTo(bFull)) { + // return aFull.comparedTo(bFull) || 0; // Ensure a number is always returned + // } + // // 再比减免金额:越大越优先 + // return bDiscount.comparedTo(aDiscount) || 0; // Ensure a number is always returned + // }) + // 找到抵扣金额最大的门槛项 + const maxDiscountThreshold = validThresholds.reduce( + (maxItem, currentItem) => { + // 处理空值,默认抵扣金额为0 + const maxDiscount = new BigNumber(maxItem?.discountAmount || 0); + const currentDiscount = new BigNumber(currentItem?.discountAmount || 0); + + // 比较当前项和已存最大项的抵扣金额,保留更大的 + return currentDiscount.gt(maxDiscount) ? currentItem : maxItem; + }, + validThresholds[0] || null + ); // 初始值为数组第一项(若数组为空则返回null) + console.log("maxDiscountThreshold", maxDiscountThreshold); + return maxDiscountThreshold; } /** @@ -835,7 +865,7 @@ export function selectOptimalThreshold( export function calcFullReductionAmount( baseAmount: number, optimalActivity?: FullReductionActivity, - optimalThreshold?: FullReductionThreshold, + optimalThreshold?: FullReductionThreshold ): number { if (!optimalActivity || !optimalThreshold) return 0; @@ -854,7 +884,6 @@ export function calcFullReductionAmount( return truncateToTwoDecimals(actualReduction.toNumber()); } - // ------------------------------ 策略辅助函数 ------------------------------ /** * 根据优惠券useShops列表判断门店是否匹配(适配BaseCoupon的useShops字段) @@ -869,8 +898,6 @@ function isStoreMatchByList( return useShops.includes(currentStoreId); } - - /** * 计算优惠券抵扣金额(处理互斥逻辑,选择最优优惠券,按商品ID排除,新增细分统计) * @param backendCoupons 后端优惠券列表 @@ -953,12 +980,15 @@ export function calcTotalPackFee( 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; + let packNum = Math.min(availableNum, packNumber); + if (dinnerType === "take-out") { + packNum = availableNum + } if (goods.product_type === GoodsType.WEIGHT) { packNum = Math.min(packNum, 1); } @@ -997,6 +1027,7 @@ export function calcPointDeduction( deductionAmount: number; usedPoints: number; } { + console.log("calcPointDeduction", userPoints, rule, maxDeductionLimit); if (rule.pointsPerYuan <= 0 || userPoints <= 0) { return { deductionAmount: 0, usedPoints: 0 }; } @@ -1047,14 +1078,24 @@ export function calculateOrderCostSummary( cartOrder: Record = {}, currentTime: Date = new Date() ): OrderCostSummary { + //是否使用霸王餐,霸王餐配置 + const { isFreeDine, freeDineConfig } = config; + // ------------------------------ 1. 基础费用计算 ------------------------------ const goodsOriginalAmount = calcGoodsOriginalAmount(goodsList); // 商品原价总和 - const goodsRealAmount = calcGoodsRealAmount( // 商品折扣后总和 + 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. 满减活动计算(核心步骤) ------------------------------ @@ -1069,12 +1110,21 @@ export function calculateOrderCostSummary( currentTime ); + // 其他费用计算(原有逻辑不变) ------------------------------ + const packFee = calcTotalPackFee(goodsList, dinnerType); // 打包费 + let seatFee = calcSeatFee(config.seatFeeConfig); // 餐位费 + seatFee = dinnerType === "dine-in" ? seatFee : 0; // 外卖不收餐位费 + const additionalFee = Math.max(0, config.additionalFee); // 附加费 + // 2.2 计算满减基数(先扣新客立减) - const baseAfterNewUserDiscount = new BigNumber(goodsRealAmount) + let baseAfterNewUserDiscount = new BigNumber(goodsRealAmount) .minus(newUserDiscount) - .isGreaterThan(0) - ? new BigNumber(goodsRealAmount).minus(newUserDiscount).toNumber() - : 0; + .plus(packFee) + .plus(seatFee) + .plus(additionalFee) + .toNumber(); + baseAfterNewUserDiscount = + baseAfterNewUserDiscount > 0 ? baseAfterNewUserDiscount : 0; // 2.3 选择最优满减阈值(多门槛场景) if (usedFullReductionActivity) { @@ -1090,10 +1140,9 @@ export function calculateOrderCostSummary( fullReductionAmount = calcFullReductionAmount( baseAfterNewUserDiscount, usedFullReductionActivity, - usedFullReductionThreshold, + usedFullReductionThreshold ); } - // ------------------------------ 3. 优惠券抵扣(适配满减同享规则) ------------------------------ let couponDeductionAmount = 0; let productCouponDeduction = 0; @@ -1101,57 +1150,85 @@ export function calculateOrderCostSummary( let usedCoupon: Coupon | undefined; let excludedProductIds: string[] = []; + 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; + // 若满减与优惠券同享(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; + if (usedFullReductionActivity && !usedFullReductionActivity.couponShare) { + couponDeductionAmount = 0; + productCouponDeduction = 0; + fullCouponDeduction = 0; + usedCoupon = undefined; + excludedProductIds = []; } // ------------------------------ 4. 积分抵扣(适配满减同享规则) ------------------------------ let pointDeductionAmount = 0; let usedPoints = 0; - // 计算积分抵扣基数(商品折扣后 - 新客立减 - 满减 - 优惠券) - const maxPointDeductionLimit = new BigNumber(goodsRealAmount) + // 计算积分抵扣基数(商品折扣后 - 新客立减 - 满减 - 优惠券 + 餐位费 + 打包费 + 附加费) + let maxPointDeductionLimit = new BigNumber(goodsRealAmount) .minus(newUserDiscount) .minus(fullReductionAmount) .minus(couponDeductionAmount) - .isGreaterThan(0) - ? new BigNumber(goodsRealAmount).minus(newUserDiscount).minus(fullReductionAmount).minus(couponDeductionAmount).toNumber() - : 0; + .plus(seatFee) + .plus(packFee) + .plus(additionalFee) + .toNumber(); + maxPointDeductionLimit = + maxPointDeductionLimit > 0 ? maxPointDeductionLimit : 0; - // 若满减与积分同享(pointsShare=1),才计算积分;否则积分抵扣为0 - if ((usedFullReductionActivity?.pointsShare === 1) && maxPointDeductionLimit > 0) { - const pointResult = calcPointDeduction( - config.userPoints, - config.pointDeductionRule, - maxPointDeductionLimit - ); - pointDeductionAmount = pointResult.deductionAmount; - usedPoints = pointResult.usedPoints; + + const pointResult = calcPointDeduction( + config.userPoints, + config.pointDeductionRule, + maxPointDeductionLimit + ); + console.log("积分抵扣结果:", pointResult); + + pointDeductionAmount = pointResult.deductionAmount; + usedPoints = pointResult.usedPoints; + // 若满减与积分不同享(pointsShare=1)积分抵扣为0 + if (usedFullReductionActivity && !usedFullReductionActivity.pointsShare) { + console.log("满减与积分不同享:积分抵扣为0"); + pointDeductionAmount = 0; + usedPoints = 0; } - // ------------------------------ 5. 其他费用计算(原有逻辑不变) ------------------------------ - const packFee = calcTotalPackFee(goodsList, dinnerType); // 打包费 - let seatFee = calcSeatFee(config.seatFeeConfig); // 餐位费 - seatFee = dinnerType === "dine-in" ? seatFee : 0; // 外卖不收餐位费 - const additionalFee = Math.max(0, config.additionalFee); // 附加费 + //使用霸王餐 + if (isFreeDine && freeDineConfig && freeDineConfig.enable) { + console.log("使用霸王餐"); + //不与优惠券同享 + if (!freeDineConfig.withCoupon) { + couponDeductionAmount = 0; + productCouponDeduction = 0; + fullCouponDeduction = 0; + usedCoupon = undefined; + excludedProductIds = []; + } + //不与积分同享 + if (!freeDineConfig.withPoints) { + pointDeductionAmount = 0; + usedPoints = 0; + } + } // 商家减免计算(原有逻辑不变) const merchantReductionConfig = config.merchantReduction; @@ -1164,7 +1241,14 @@ export function calculateOrderCostSummary( .plus(seatFee) .plus(packFee) .isGreaterThan(0) - ? new BigNumber(goodsRealAmount).minus(newUserDiscount).minus(fullReductionAmount).minus(couponDeductionAmount).minus(pointDeductionAmount).plus(seatFee).plus(packFee).toNumber() + ? new BigNumber(goodsRealAmount) + .minus(newUserDiscount) + .minus(fullReductionAmount) + .minus(couponDeductionAmount) + .minus(pointDeductionAmount) + .plus(seatFee) + .plus(packFee) + .toNumber() : 0; switch (merchantReductionConfig.type) { @@ -1175,11 +1259,17 @@ export function calculateOrderCostSummary( ); break; case MerchantReductionType.DISCOUNT_RATE: - const validRate = Math.max(0, Math.min(100, merchantReductionConfig.discountRate || 0)) / 100; - merchantReductionActualAmount = maxMerchantReductionLimit * (1 - validRate); + const validRate = + Math.max(0, Math.min(100, merchantReductionConfig.discountRate || 0)) / + 100; + merchantReductionActualAmount = + maxMerchantReductionLimit * (1 - validRate); break; } - merchantReductionActualAmount = Math.max(0, truncateToTwoDecimals(merchantReductionActualAmount)); + merchantReductionActualAmount = Math.max( + 0, + truncateToTwoDecimals(merchantReductionActualAmount) + ); // ------------------------------ 6. 最终实付金额计算 ------------------------------ const finalPayAmountBn = new BigNumber(goodsRealAmount) @@ -1191,7 +1281,17 @@ export function calculateOrderCostSummary( .plus(seatFee) .plus(packFee) .plus(additionalFee); - const finalPayAmount = Math.max(0, truncateToTwoDecimals(finalPayAmountBn.toNumber())); + let finalPayAmount = Math.max( + 0, + truncateToTwoDecimals(finalPayAmountBn.toNumber()) + ); + // ------------------------------ 使用霸王餐计算 ------------------------------ + let orderOriginFinalPayAmount = finalPayAmount; + if (isFreeDine && freeDineConfig && freeDineConfig.enable) { + finalPayAmount = BigNumber(finalPayAmount) + .times(freeDineConfig.rechargeTimes) + .toNumber(); + } // ------------------------------ 7. 总优惠金额计算 ------------------------------ const totalDiscountAmount = truncateToTwoDecimals( @@ -1207,12 +1307,14 @@ export function calculateOrderCostSummary( const scoreMaxMoney = new BigNumber(finalPayAmount) .plus(pointDeductionAmount) .minus(merchantReductionActualAmount) - .toNumber() - + .toNumber(); // ------------------------------ 8. 返回完整结果 ------------------------------ return { - goodsTotal: goodsList.reduce((sum, g) => sum + Math.max(0, g.number - (g.returnNum || 0)), 0), + goodsTotal: goodsList.reduce( + (sum, g) => sum + Math.max(0, g.number - (g.returnNum || 0)), + 0 + ), goodsRealAmount, goodsOriginalAmount, goodsDiscountAmount, @@ -1223,18 +1325,20 @@ export function calculateOrderCostSummary( seatFee, packFee, totalDiscountAmount, + //最终支付原金额 + orderOriginFinalPayAmount, //积分最大可抵扣金额 scoreMaxMoney, // 满减活动明细(后端字段) fullReduction: { usedActivity: usedFullReductionActivity, usedThreshold: usedFullReductionThreshold, - actualAmount: truncateToTwoDecimals(fullReductionAmount) + actualAmount: truncateToTwoDecimals(fullReductionAmount), }, merchantReduction: { type: merchantReductionConfig.type, originalConfig: merchantReductionConfig, - actualAmount: merchantReductionActualAmount + actualAmount: merchantReductionActualAmount, }, additionalFee, finalPayAmount, @@ -1242,7 +1346,7 @@ export function calculateOrderCostSummary( pointUsed: usedPoints, newUserDiscount, dinnerType, - config + config, }; }