Merge branch 'ymf' of https://newgitea.sxczgkj.cn/czg_team/cashier-web into gyq
This commit is contained in:
856
src/lib/carts.ts
Normal file
856
src/lib/carts.ts
Normal file
@@ -0,0 +1,856 @@
|
||||
/**
|
||||
* 购物车订单价格计算公共库
|
||||
* 功能:覆盖订单全链路费用计算(商品价格、优惠券、积分、餐位费等),支持策略扩展
|
||||
* 小数处理:统一舍去小数点后两位(如 10.129 → 10.12,15.998 → 15.99)
|
||||
* 扩展设计:优惠券/营销活动采用策略模式,新增类型仅需扩展策略,无需修改核心逻辑
|
||||
*/
|
||||
|
||||
// ============================ 1. 基础类型定义(扩展可复用) ============================
|
||||
/** 商品类型枚举 */
|
||||
export enum GoodsType {
|
||||
NORMAL = 'normal', // 普通商品
|
||||
WEIGHT = 'weight', // 称重商品
|
||||
GIFT = 'gift', // 赠菜(继承普通商品逻辑,标记用)
|
||||
}
|
||||
// 定义计算结果类型(核心修正)
|
||||
interface CouponResult {
|
||||
deductionAmount: number; // 抵扣金额
|
||||
excludedProductIds: string[]; // 不适用商品ID列表(修正为 string[],而非 never[])
|
||||
usedCoupon: Coupon | undefined; // 允许为优惠券类型或 undefined
|
||||
}
|
||||
|
||||
interface ExchangeCalculationResult {
|
||||
deductionAmount: number;
|
||||
excludedProductIds: string[];
|
||||
}
|
||||
/** 优惠券类型枚举 */
|
||||
export enum CouponType {
|
||||
FULL_REDUCTION = 'full_reduction', // 满减券
|
||||
DISCOUNT = 'discount', // 折扣券
|
||||
SECOND_HALF = 'second_half', // 第二件半价券
|
||||
BUY_ONE_GET_ONE = 'buy_one_get_one', // 买一送一券
|
||||
EXCHANGE = 'exchange', // 商品兑换券
|
||||
}
|
||||
|
||||
/** 营销活动类型枚举 */
|
||||
export enum ActivityType {
|
||||
TIME_LIMIT_DISCOUNT = 'time_limit_discount', // 限时折扣
|
||||
}
|
||||
|
||||
/** 基础购物车商品项(含普通/称重/临时/赠菜) */
|
||||
export interface BaseCartItem {
|
||||
id: string | number;
|
||||
salePrice: number; // 商品原价(元)
|
||||
number: number; // 商品数量
|
||||
productType: GoodsType; // 商品类型
|
||||
isTemporary?: boolean; // 是否临时菜(默认false)
|
||||
isGift?: boolean; // 是否赠菜(默认false)
|
||||
returnNum?: number; // 退货数量(历史订单用,默认0)
|
||||
memberPrice?: number; // 商品会员价(元,优先级:会员价 > 会员折扣)
|
||||
discountSaleAmount?: number; // 商家改价后单价(元,优先级最高)
|
||||
packFee?: number; // 单份打包费(元,默认0)
|
||||
packNumber?: number; // 堂食打包数量(默认0)
|
||||
activityInfo?: { // 商品参与的营销活动(如限时折扣)
|
||||
type: ActivityType;
|
||||
discountRate: number; // 折扣率(如0.8=8折)
|
||||
shareWithMember: boolean; // 是否与会员优惠同享(默认false)
|
||||
};
|
||||
skuData?: { // SKU扩展数据(可选)
|
||||
memberPrice?: number;
|
||||
salePrice?: number;
|
||||
};
|
||||
}
|
||||
|
||||
/** 基础优惠券接口 */
|
||||
interface BaseCoupon {
|
||||
id: string | number;
|
||||
type: CouponType; // 优惠券类型
|
||||
name: string; // 优惠券名称
|
||||
available: boolean; // 是否可用(当日剩余数量>0)
|
||||
applicableStoreIds: string[]; // 适用门店ID(当前门店需在列)
|
||||
shareWithActivity: boolean; // 是否与营销活动同享(默认false)
|
||||
shareWithMember: boolean; // 是否与会员优惠同享(默认false)
|
||||
}
|
||||
|
||||
/** 满减券(示例:满100减20) */
|
||||
export interface FullReductionCoupon extends BaseCoupon {
|
||||
type: CouponType.FULL_REDUCTION;
|
||||
fullAmount: number; // 满减门槛(元)
|
||||
reductionAmount: number; // 减免金额(元)
|
||||
maxReductionAmount?: number; // 最大减免金额(元,默认等于reductionAmount)
|
||||
}
|
||||
|
||||
/** 折扣券(示例:9折,最大减50) */
|
||||
export interface DiscountCoupon extends BaseCoupon {
|
||||
type: CouponType.DISCOUNT;
|
||||
discountRate: number; // 折扣率(如0.9=9折,需>0且<1)
|
||||
maxReductionAmount: number; // 最大减免金额(元,避免折扣过大)
|
||||
}
|
||||
|
||||
/** 第二件半价券(限指定商品) */
|
||||
export interface SecondHalfPriceCoupon extends BaseCoupon {
|
||||
type: CouponType.SECOND_HALF;
|
||||
applicableProductIds: string[]; // 适用商品ID
|
||||
maxUseCountPerOrder?: number; // 每单最大使用次数(默认不限,按商品数量算)
|
||||
}
|
||||
|
||||
/** 买一送一券(限指定商品) */
|
||||
export interface BuyOneGetOneCoupon extends BaseCoupon {
|
||||
type: CouponType.BUY_ONE_GET_ONE;
|
||||
applicableProductIds: string[]; // 适用商品ID
|
||||
maxUseCountPerOrder?: number; // 每单最大使用次数(默认不限,按商品数量算)
|
||||
}
|
||||
|
||||
/** 商品兑换券(抵扣指定商品金额) */
|
||||
export interface ExchangeCoupon extends BaseCoupon {
|
||||
type: CouponType.EXCHANGE;
|
||||
applicableProductIds: string[]; // 适用商品ID
|
||||
deductCount: number; // 可抵扣商品件数(如1=抵扣1件)
|
||||
sortRule: 'low_price_first' | 'high_price_first'; // 商品排序规则
|
||||
}
|
||||
|
||||
/** 所有优惠券类型联合 */
|
||||
export type Coupon = FullReductionCoupon | DiscountCoupon | SecondHalfPriceCoupon | BuyOneGetOneCoupon | ExchangeCoupon;
|
||||
|
||||
/** 营销活动配置(如限时折扣) */
|
||||
export interface ActivityConfig {
|
||||
type: ActivityType;
|
||||
applicableProductIds?: string[]; // 适用商品ID
|
||||
discountRate: number; // 折扣率(如0.8=8折)
|
||||
shareWithMember: 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 interface OrderExtraConfig {
|
||||
merchantReduction: number; // 商家减免金额(元,默认0)
|
||||
additionalFee: number; // 附加费(元,如余额充值、券包,默认0)
|
||||
pointDeductionRule: PointDeductionRule; // 积分抵扣规则
|
||||
seatFeeConfig: SeatFeeConfig; // 餐位费配置
|
||||
currentStoreId: string; // 当前门店ID(用于验证优惠券适用门店)
|
||||
userPoints: number; // 用户当前积分(用于积分抵扣)
|
||||
isMember: boolean; // 用户是否会员(用于会员优惠)
|
||||
memberDiscountRate?: number; // 会员折扣率(如0.95=95折,无会员价时用)
|
||||
}
|
||||
|
||||
/** 订单费用汇总(所有子项清晰展示,方便调用方使用) */
|
||||
export interface OrderCostSummary {
|
||||
goodsOriginalAmount: number; // 商品原价总和
|
||||
goodsDiscountAmount: number; // 商品折扣金额(原价-实际价)
|
||||
couponDeductionAmount: number; // 优惠券抵扣金额
|
||||
pointDeductionAmount: number; // 积分抵扣金额
|
||||
seatFee: number; // 餐位费
|
||||
packFee: number; // 打包费
|
||||
merchantReductionAmount: number; // 商家减免金额
|
||||
additionalFee: number; // 附加费
|
||||
finalPayAmount: number; // 最终实付金额
|
||||
// 辅助信息
|
||||
couponUsed?: Coupon; // 实际使用的优惠券(互斥时选最优)
|
||||
pointUsed: number; // 实际使用的积分
|
||||
}
|
||||
|
||||
// ============================ 2. 基础工具函数(通用能力,无业务耦合) ============================
|
||||
/**
|
||||
* 统一小数处理:舍去小数点后两位(如 10.129 → 10.12,15.998 → 15.99)
|
||||
* @param num 待处理数字
|
||||
* @returns 处理后保留两位小数的数字
|
||||
*/
|
||||
export function truncateToTwoDecimals(num: number): number {
|
||||
return Math.floor(num * 100) / 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断商品是否为临时菜(临时菜不参与优惠券门槛和折扣计算)
|
||||
* @param goods 商品项
|
||||
* @returns 是否临时菜
|
||||
*/
|
||||
export function isTemporaryGoods(goods: BaseCartItem): boolean {
|
||||
return !!goods.isTemporary;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断商品是否为赠菜(赠菜不计入优惠券活动,但计打包费)
|
||||
* @param goods 商品项
|
||||
* @returns 是否赠菜
|
||||
*/
|
||||
export function isGiftGoods(goods: BaseCartItem): boolean {
|
||||
return !!goods.isGift;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算单个商品的会员价(优先级:商品会员价 > 会员折扣率)
|
||||
* @param goods 商品项
|
||||
* @param isMember 是否会员
|
||||
* @param memberDiscountRate 会员折扣率(如0.95=95折)
|
||||
* @returns 会员价(元)
|
||||
*/
|
||||
export function calcMemberPrice(
|
||||
goods: BaseCartItem,
|
||||
isMember: boolean,
|
||||
memberDiscountRate?: number
|
||||
): number {
|
||||
if (!isMember) return goods.salePrice;
|
||||
|
||||
// 优先级:商品会员价 > SKU会员价 > 会员折扣价 > 原价
|
||||
const basePrice = goods.memberPrice
|
||||
?? goods.skuData?.memberPrice
|
||||
?? goods.salePrice;
|
||||
|
||||
return memberDiscountRate && !goods.memberPrice && !goods.skuData?.memberPrice
|
||||
? truncateToTwoDecimals(basePrice * memberDiscountRate)
|
||||
: basePrice;
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤可参与优惠券计算的商品(排除临时菜、赠菜、已用兑换券的商品)
|
||||
* @param goodsList 商品列表
|
||||
* @param excludedProductIds 需排除的商品ID(如兑换券已抵扣的商品)
|
||||
* @returns 可参与优惠券计算的商品列表
|
||||
*/
|
||||
export function filterCouponEligibleGoods(
|
||||
goodsList: BaseCartItem[],
|
||||
excludedProductIds: string[] = []
|
||||
): BaseCartItem[] {
|
||||
return goodsList.filter(goods =>
|
||||
!isTemporaryGoods(goods)
|
||||
&& !isGiftGoods(goods)
|
||||
&& !excludedProductIds.includes(String(goods.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<string, number> = {}
|
||||
): BaseCartItem[] {
|
||||
return [...goodsList].sort((a, b) => {
|
||||
// 1. 按价格排序
|
||||
const priceA = a.salePrice;
|
||||
const priceB = 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. 同价格同数量按加入购物车顺序(早的优先)
|
||||
const orderA = cartOrder[String(a.id)] ?? Infinity;
|
||||
const orderB = cartOrder[String(b.id)] ?? Infinity;
|
||||
return orderA - orderB;
|
||||
});
|
||||
}
|
||||
|
||||
// ============================ 3. 商品核心价格计算(含折扣、会员优惠) ============================
|
||||
/**
|
||||
* 计算单个商品的实际单价(整合商家改价、会员优惠、营销活动折扣)
|
||||
* @param goods 商品项
|
||||
* @param config 订单额外配置(含会员、活动信息)
|
||||
* @returns 单个商品实际单价(元)
|
||||
*/
|
||||
export function calcSingleGoodsRealPrice(
|
||||
goods: BaseCartItem,
|
||||
config: Pick<OrderExtraConfig, 'isMember' | 'memberDiscountRate'> & {
|
||||
activity?: ActivityConfig; // 商品参与的营销活动(如限时折扣)
|
||||
}
|
||||
): number {
|
||||
const { isMember, memberDiscountRate, activity } = config;
|
||||
|
||||
// 1. 优先级1:商家改价(改价后单价>0才生效)
|
||||
if (goods.discountSaleAmount && goods.discountSaleAmount > 0) {
|
||||
return truncateToTwoDecimals(goods.discountSaleAmount);
|
||||
}
|
||||
|
||||
// 2. 优先级2:会员价(含会员折扣率)
|
||||
const memberPrice = calcMemberPrice(goods, isMember, memberDiscountRate);
|
||||
|
||||
// 3. 优先级3:营销活动折扣(如限时折扣)
|
||||
if (!activity || (activity.applicableProductIds && !activity.applicableProductIds.includes(String(goods.id)))) {
|
||||
return memberPrice;
|
||||
}
|
||||
|
||||
// 处理活动与会员的同享/不同享逻辑
|
||||
if (activity.shareWithMember) {
|
||||
// 同享:会员价基础上叠加工活动折扣
|
||||
return truncateToTwoDecimals(memberPrice * activity.discountRate);
|
||||
} else {
|
||||
// 不同享:取会员价和活动价的最小值(最大折扣)
|
||||
const activityPrice = truncateToTwoDecimals(goods.salePrice * activity.discountRate);
|
||||
return Math.min(memberPrice, activityPrice);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算商品原价总和(所有商品:原价*数量,含临时菜、赠菜)
|
||||
* @param goodsList 商品列表
|
||||
* @returns 商品原价总和(元)
|
||||
*/
|
||||
export function calcGoodsOriginalAmount(goodsList: BaseCartItem[]): number {
|
||||
return truncateToTwoDecimals(
|
||||
goodsList.reduce((total, goods) => {
|
||||
const availableNum = Math.max(0, goods.number - (goods.returnNum || 0));
|
||||
return total + goods.salePrice * availableNum;
|
||||
}, 0)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算商品实际总价(所有商品:实际单价*数量,含临时菜、赠菜)
|
||||
* @param goodsList 商品列表
|
||||
* @param config 订单额外配置(含会员、活动信息)
|
||||
* @param activities 全局营销活动列表(如限时折扣)
|
||||
* @returns 商品实际总价(元)
|
||||
*/
|
||||
export function calcGoodsRealAmount(
|
||||
goodsList: BaseCartItem[],
|
||||
config: Pick<OrderExtraConfig, 'isMember' | 'memberDiscountRate'>,
|
||||
activities: ActivityConfig[] = []
|
||||
): number {
|
||||
return truncateToTwoDecimals(
|
||||
goodsList.reduce((total, goods) => {
|
||||
const availableNum = Math.max(0, goods.number - (goods.returnNum || 0));
|
||||
if (availableNum <= 0) return total;
|
||||
|
||||
// 匹配商品参与的营销活动(优先商品自身配置,无则匹配全局活动)
|
||||
const activity = goods.activityInfo
|
||||
?? activities.find(act => (act.applicableProductIds || []).includes(String(goods.id)));
|
||||
|
||||
const realPrice = calcSingleGoodsRealPrice(goods, { ...config, activity });
|
||||
return total + realPrice * availableNum;
|
||||
}, 0)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算商品折扣总金额(商品原价总和 - 商品实际总价)
|
||||
* @param goodsOriginalAmount 商品原价总和
|
||||
* @param goodsRealAmount 商品实际总价
|
||||
* @returns 商品折扣总金额(元,≥0)
|
||||
*/
|
||||
export function calcGoodsDiscountAmount(
|
||||
goodsOriginalAmount: number,
|
||||
goodsRealAmount: number
|
||||
): number {
|
||||
return truncateToTwoDecimals(Math.max(0, goodsOriginalAmount - goodsRealAmount));
|
||||
}
|
||||
|
||||
// ============================ 4. 优惠券抵扣计算(策略模式,易扩展) ============================
|
||||
/** 优惠券计算策略接口(新增优惠券类型时,实现此接口即可) */
|
||||
interface CouponCalculationStrategy {
|
||||
/**
|
||||
* 计算优惠券抵扣金额
|
||||
* @param coupon 优惠券信息
|
||||
* @param goodsList 商品列表
|
||||
* @param config 订单配置(门店、会员、活动等)
|
||||
* @returns 抵扣金额 + 额外信息(如排除的商品ID)
|
||||
*/
|
||||
calculate(
|
||||
coupon: Coupon,
|
||||
goodsList: BaseCartItem[],
|
||||
config: Pick<OrderExtraConfig, 'currentStoreId' | 'isMember' | 'memberDiscountRate'> & {
|
||||
activities: ActivityConfig[]; // 营销活动列表
|
||||
cartOrder: Record<string, number>; // 商品加入购物车顺序
|
||||
goodsList?: BaseCartItem[]; // 可选的过滤后商品列表(仅兑换券策略用)
|
||||
}
|
||||
): {
|
||||
deductionAmount: number; // 抵扣金额(元)
|
||||
excludedProductIds: string[]; // 需排除的商品ID(如兑换券抵扣的商品)
|
||||
};
|
||||
}
|
||||
|
||||
/** 满减券计算策略 */
|
||||
class FullReductionStrategy implements CouponCalculationStrategy {
|
||||
calculate(
|
||||
coupon: FullReductionCoupon,
|
||||
goodsList: BaseCartItem[],
|
||||
config: any
|
||||
): { deductionAmount: number; excludedProductIds: string[] } {
|
||||
// 1. 验证基础条件(适用门店、可用状态)
|
||||
if (!coupon.available || !coupon.applicableStoreIds.includes(config.currentStoreId)) {
|
||||
return { deductionAmount: 0, excludedProductIds: [] };
|
||||
}
|
||||
|
||||
// 2. 计算优惠券门槛(排除临时菜、赠菜)
|
||||
const eligibleGoods = filterCouponEligibleGoods(goodsList);
|
||||
const eligibleAmount = calcGoodsRealAmount(eligibleGoods, {
|
||||
isMember: config.isMember,
|
||||
memberDiscountRate: config.memberDiscountRate
|
||||
}, config.activities);
|
||||
|
||||
// 3. 满足门槛则抵扣,否则0
|
||||
if (eligibleAmount < coupon.fullAmount) {
|
||||
return { deductionAmount: 0, excludedProductIds: [] };
|
||||
}
|
||||
|
||||
// 4. 计算实际抵扣金额(不超过最大减免)
|
||||
const maxReduction = coupon.maxReductionAmount ?? coupon.reductionAmount;
|
||||
const deductionAmount = truncateToTwoDecimals(Math.min(coupon.reductionAmount, maxReduction));
|
||||
return { deductionAmount, excludedProductIds: [] };
|
||||
}
|
||||
}
|
||||
|
||||
/** 折扣券计算策略 */
|
||||
class DiscountStrategy implements CouponCalculationStrategy {
|
||||
calculate(
|
||||
coupon: DiscountCoupon,
|
||||
goodsList: BaseCartItem[],
|
||||
config: any
|
||||
): { deductionAmount: number; excludedProductIds: string[] } {
|
||||
// 1. 验证基础条件
|
||||
if (!coupon.available || !coupon.applicableStoreIds.includes(config.currentStoreId)) {
|
||||
return { deductionAmount: 0, excludedProductIds: [] };
|
||||
}
|
||||
|
||||
// 2. 计算可折扣金额(排除临时菜、赠菜)
|
||||
const eligibleGoods = filterCouponEligibleGoods(goodsList);
|
||||
const eligibleAmount = calcGoodsRealAmount(eligibleGoods, {
|
||||
isMember: config.isMember,
|
||||
memberDiscountRate: config.memberDiscountRate
|
||||
}, config.activities);
|
||||
|
||||
// 3. 计算折扣金额(不超过最大减免)
|
||||
const discountAmount = truncateToTwoDecimals(eligibleAmount * (1 - coupon.discountRate));
|
||||
const deductionAmount = Math.min(discountAmount, coupon.maxReductionAmount);
|
||||
return { deductionAmount: truncateToTwoDecimals(deductionAmount), excludedProductIds: [] };
|
||||
}
|
||||
}
|
||||
|
||||
/** 第二件半价券计算策略 */
|
||||
class SecondHalfPriceStrategy implements CouponCalculationStrategy {
|
||||
calculate(
|
||||
coupon: SecondHalfPriceCoupon,
|
||||
goodsList: BaseCartItem[],
|
||||
config: any
|
||||
): { deductionAmount: number; excludedProductIds: string[] } {
|
||||
// 1. 验证基础条件
|
||||
if (!coupon.available || !coupon.applicableStoreIds.includes(config.currentStoreId)) {
|
||||
return { deductionAmount: 0, excludedProductIds: [] };
|
||||
}
|
||||
|
||||
let totalDeduction = 0;
|
||||
|
||||
// 2. 遍历适用商品,计算每类商品的优惠
|
||||
for (const productId of coupon.applicableProductIds) {
|
||||
const goods = goodsList.find(g => String(g.id) === productId && !isGiftGoods(g) && !isTemporaryGoods(g));
|
||||
if (!goods) continue;
|
||||
|
||||
const availableNum = Math.max(0, goods.number - (goods.returnNum || 0));
|
||||
if (availableNum < 2) continue; // 至少2件才享受优惠
|
||||
|
||||
// 3. 计算单类商品优惠:每2件减免0.5件的实际价(超过2件部分按原价)
|
||||
const realPrice = calcSingleGoodsRealPrice(goods, {
|
||||
isMember: config.isMember,
|
||||
memberDiscountRate: config.memberDiscountRate,
|
||||
activity: config.activities.find((act: { applicableProductIds: string | string[]; }) => act.applicableProductIds.includes(productId))
|
||||
});
|
||||
const discountCount = Math.floor(availableNum / 2); // 优惠次数(每2件1次)
|
||||
totalDeduction += realPrice * 0.5 * discountCount;
|
||||
}
|
||||
|
||||
return {
|
||||
deductionAmount: truncateToTwoDecimals(totalDeduction),
|
||||
excludedProductIds: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** 买一送一券计算策略 */
|
||||
class BuyOneGetOneStrategy implements CouponCalculationStrategy {
|
||||
calculate(
|
||||
coupon: BuyOneGetOneCoupon,
|
||||
goodsList: BaseCartItem[],
|
||||
config: any
|
||||
): { deductionAmount: number; excludedProductIds: string[] } {
|
||||
// 1. 验证基础条件
|
||||
if (!coupon.available || !coupon.applicableStoreIds.includes(config.currentStoreId)) {
|
||||
return { deductionAmount: 0, excludedProductIds: [] };
|
||||
}
|
||||
|
||||
let totalDeduction = 0;
|
||||
|
||||
// 2. 遍历适用商品,计算每类商品的优惠
|
||||
for (const productId of coupon.applicableProductIds) {
|
||||
const goods = goodsList.find(g => String(g.id) === productId && !isGiftGoods(g) && !isTemporaryGoods(g));
|
||||
if (!goods) continue;
|
||||
|
||||
const availableNum = Math.max(0, goods.number - (goods.returnNum || 0));
|
||||
if (availableNum < 2) continue; // 至少2件才享受优惠
|
||||
|
||||
// 3. 计算单类商品优惠:每2件减免1件的实际价
|
||||
const realPrice = calcSingleGoodsRealPrice(goods, {
|
||||
isMember: config.isMember,
|
||||
memberDiscountRate: config.memberDiscountRate,
|
||||
activity: config.activities.find((act: { applicableProductIds: string | string[]; }) => act.applicableProductIds.includes(productId))
|
||||
});
|
||||
const discountCount = Math.floor(availableNum / 2); // 优惠次数
|
||||
totalDeduction += realPrice * 1 * discountCount;
|
||||
}
|
||||
|
||||
return {
|
||||
deductionAmount: truncateToTwoDecimals(totalDeduction),
|
||||
excludedProductIds: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** 商品兑换券计算策略 */
|
||||
class ExchangeCouponStrategy implements CouponCalculationStrategy {
|
||||
calculate(
|
||||
coupon: ExchangeCoupon,
|
||||
goodsList: BaseCartItem[],
|
||||
config: any
|
||||
): { deductionAmount: number; excludedProductIds: string[] } {
|
||||
// 1. 验证基础条件
|
||||
if (!coupon.available || !coupon.applicableStoreIds.includes(config.currentStoreId)) {
|
||||
return { deductionAmount: 0, excludedProductIds: [] };
|
||||
}
|
||||
|
||||
// 2. 筛选适用商品(排除临时菜、赠菜)
|
||||
const eligibleGoods = goodsList.filter(goods =>
|
||||
coupon.applicableProductIds.includes(String(goods.id))
|
||||
&& !isTemporaryGoods(goods)
|
||||
&& !isGiftGoods(goods)
|
||||
);
|
||||
if (eligibleGoods.length === 0) {
|
||||
return { deductionAmount: 0, excludedProductIds: [] };
|
||||
}
|
||||
|
||||
// 3. 按规则排序商品
|
||||
const sortedGoods = sortGoodsForCoupon(eligibleGoods, coupon.sortRule, config.cartOrder);
|
||||
|
||||
// 4. 计算抵扣金额(按可抵扣件数,累计商品实际价)
|
||||
let remainingCount = coupon.deductCount;
|
||||
let totalDeduction = 0;
|
||||
const excludedProductIds: string[] = [];
|
||||
|
||||
for (const goods of sortedGoods) {
|
||||
if (remainingCount <= 0) break;
|
||||
|
||||
const availableNum = Math.max(0, goods.number - (goods.returnNum || 0));
|
||||
if (availableNum === 0) continue;
|
||||
|
||||
// 计算当前商品可抵扣的件数
|
||||
const deductCount = Math.min(availableNum, remainingCount);
|
||||
const realPrice = calcSingleGoodsRealPrice(goods, {
|
||||
isMember: config.isMember,
|
||||
memberDiscountRate: config.memberDiscountRate,
|
||||
activity: config.activities.find((act: { applicableProductIds: string | string[]; }) => act.applicableProductIds.includes(String(goods.id)))
|
||||
});
|
||||
|
||||
// 累计抵扣金额和排除商品ID
|
||||
totalDeduction += realPrice * deductCount;
|
||||
excludedProductIds.push(String(goods.id));
|
||||
remainingCount -= deductCount;
|
||||
}
|
||||
|
||||
return {
|
||||
deductionAmount: truncateToTwoDecimals(totalDeduction),
|
||||
excludedProductIds
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 优惠券计算策略工厂(根据优惠券类型获取对应策略,易扩展)
|
||||
* @param couponType 优惠券类型
|
||||
* @returns 对应的计算策略实例
|
||||
*/
|
||||
function getCouponStrategy(couponType: CouponType): CouponCalculationStrategy {
|
||||
switch (couponType) {
|
||||
case CouponType.FULL_REDUCTION:
|
||||
return new FullReductionStrategy();
|
||||
case CouponType.DISCOUNT:
|
||||
return new DiscountStrategy();
|
||||
case CouponType.SECOND_HALF:
|
||||
return new SecondHalfPriceStrategy();
|
||||
case CouponType.BUY_ONE_GET_ONE:
|
||||
return new BuyOneGetOneStrategy();
|
||||
case CouponType.EXCHANGE:
|
||||
return new ExchangeCouponStrategy();
|
||||
default:
|
||||
throw new Error(`不支持的优惠券类型:${couponType}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算优惠券抵扣金额(处理互斥逻辑,选择最优优惠券)
|
||||
* @param coupons 用户选择的优惠券列表(可能多选,需处理互斥)
|
||||
* @param goodsList 商品列表
|
||||
* @param config 订单配置
|
||||
* @returns 最优优惠券的抵扣结果
|
||||
*/
|
||||
export function calcCouponDeduction(
|
||||
coupons: Coupon[],
|
||||
goodsList: BaseCartItem[],
|
||||
config: Pick<OrderExtraConfig, 'currentStoreId' | 'isMember' | 'memberDiscountRate'> & {
|
||||
activities: ActivityConfig[];
|
||||
cartOrder: Record<string, number>; // 商品加入购物车顺序(用于兑换券排序)
|
||||
}
|
||||
): {
|
||||
deductionAmount: number;
|
||||
usedCoupon?: Coupon;
|
||||
excludedProductIds: string[];
|
||||
} {
|
||||
if (coupons.length === 0) {
|
||||
return { deductionAmount: 0, excludedProductIds: [] };
|
||||
}
|
||||
|
||||
// 1. 处理优惠券互斥逻辑(满减/折扣/第二件半价/买一送一 互斥;兑换券可单独用或同享)
|
||||
const exchangeCoupons = coupons.filter(c => c.type === CouponType.EXCHANGE);
|
||||
const nonExchangeCoupons = coupons.filter(c => c.type !== CouponType.EXCHANGE);
|
||||
|
||||
// 2. 计算非兑换券的最优抵扣(互斥,选最大)
|
||||
let nonExchangeResult:CouponResult = {
|
||||
deductionAmount: 0,
|
||||
excludedProductIds: [],
|
||||
usedCoupon: undefined
|
||||
};
|
||||
if (nonExchangeCoupons.length > 0) {
|
||||
nonExchangeResult = nonExchangeCoupons.reduce((best, coupon) => {
|
||||
const strategy = getCouponStrategy(coupon.type);
|
||||
// 确保 calculate 返回的 result 包含 deductionAmount 和 excludedProductIds
|
||||
const result: Omit<CouponResult, 'usedCoupon'> = strategy.calculate(coupon, goodsList, config);
|
||||
// 合并结果,补充 usedCoupon
|
||||
const currentResult: CouponResult = {
|
||||
...result,
|
||||
usedCoupon: coupon
|
||||
};
|
||||
// 比较抵扣金额,返回更优结果
|
||||
return currentResult.deductionAmount > best.deductionAmount
|
||||
? currentResult
|
||||
: best;
|
||||
}, nonExchangeResult); // 初始值类型现在与回调返回值一致
|
||||
}
|
||||
|
||||
// 3. 计算兑换券抵扣(可与非兑换券同享,需排除兑换券已抵扣的商品)
|
||||
let exchangeResult:ExchangeCalculationResult = { deductionAmount: 0, excludedProductIds: [] };
|
||||
if (exchangeCoupons.length > 0) {
|
||||
exchangeResult = exchangeCoupons.reduce((best, coupon) => {
|
||||
const strategy = getCouponStrategy(coupon.type);
|
||||
const result = strategy.calculate(coupon, goodsList, {
|
||||
...config,
|
||||
// 排除已抵扣的商品
|
||||
goodsList: goodsList.filter(g => !best.excludedProductIds.includes(String(g.id)))
|
||||
});
|
||||
return result.deductionAmount > best.deductionAmount ? result : best;
|
||||
}, exchangeResult);
|
||||
}
|
||||
|
||||
// 4. 汇总结果(兑换券可与非兑换券同享,总抵扣=两者之和)
|
||||
return {
|
||||
deductionAmount: truncateToTwoDecimals(nonExchangeResult.deductionAmount + exchangeResult.deductionAmount),
|
||||
usedCoupon: nonExchangeResult.usedCoupon,
|
||||
excludedProductIds: [...nonExchangeResult.excludedProductIds, ...exchangeResult.excludedProductIds]
|
||||
};
|
||||
}
|
||||
|
||||
// ============================ 5. 其他费用计算(打包费、餐位费、积分抵扣) ============================
|
||||
/**
|
||||
* 计算总打包费(赠菜也计算,称重商品打包数量≤1)
|
||||
* @param goodsList 商品列表
|
||||
* @param dinnerType 就餐类型(堂食dine-in/外卖take-out)
|
||||
* @returns 总打包费(元)
|
||||
*/
|
||||
export function calcTotalPackFee(
|
||||
goodsList: BaseCartItem[],
|
||||
dinnerType: 'dine-in' | 'take-out'
|
||||
): number {
|
||||
return truncateToTwoDecimals(
|
||||
goodsList.reduce((total, goods) => {
|
||||
const availableNum = Math.max(0, goods.number - (goods.returnNum || 0));
|
||||
if (availableNum === 0) return total;
|
||||
|
||||
// 计算单个商品打包数量(外卖全打包,堂食按配置,称重商品≤1)
|
||||
let packNum = dinnerType === 'take-out'
|
||||
? availableNum
|
||||
: (goods.packNumber || 0);
|
||||
if (goods.productType === GoodsType.WEIGHT) {
|
||||
packNum = Math.min(packNum, 1);
|
||||
}
|
||||
|
||||
return total + (goods.packFee || 0) * packNum;
|
||||
}, 0)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算餐位费(按人数,不参与营销活动)
|
||||
* @param config 餐位费配置
|
||||
* @returns 餐位费(元,未启用则0)
|
||||
*/
|
||||
export function calcSeatFee(config: SeatFeeConfig): number {
|
||||
if (!config.isEnabled) return 0;
|
||||
const personCount = Math.max(1, config.personCount); // 至少1人
|
||||
return truncateToTwoDecimals(config.pricePerPerson * personCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算积分抵扣金额(按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 };
|
||||
}
|
||||
|
||||
// 最大可抵扣金额(积分可抵金额 vs 规则最大 vs 订单上限)
|
||||
const maxDeductByPoints = truncateToTwoDecimals(userPoints / rule.pointsPerYuan);
|
||||
const maxDeductAmount = Math.min(
|
||||
maxDeductByPoints,
|
||||
rule.maxDeductionAmount ?? Infinity,
|
||||
maxDeductionLimit
|
||||
);
|
||||
|
||||
// 实际使用积分 = 抵扣金额 * 积分兑换比例
|
||||
const usedPoints = truncateToTwoDecimals(maxDeductAmount * rule.pointsPerYuan);
|
||||
return {
|
||||
deductionAmount: maxDeductAmount,
|
||||
usedPoints: Math.min(usedPoints, userPoints) // 避免积分超扣
|
||||
};
|
||||
}
|
||||
|
||||
// ============================ 6. 订单总费用汇总与实付金额计算(最终入口) ============================
|
||||
/**
|
||||
* 计算订单所有费用子项并汇总(核心入口函数)
|
||||
* @param goodsList 购物车商品列表
|
||||
* @param dinnerType 就餐类型
|
||||
* @param coupons 用户选择的优惠券列表
|
||||
* @param activities 全局营销活动列表
|
||||
* @param config 订单额外配置(会员、积分、餐位费等)
|
||||
* @param cartOrder 商品加入购物车顺序(key=商品ID,value=时间戳)
|
||||
* @returns 订单费用汇总(含所有子项和实付金额)
|
||||
*/
|
||||
export function calculateOrderCostSummary(
|
||||
goodsList: BaseCartItem[],
|
||||
dinnerType: 'dine-in' | 'take-out',
|
||||
coupons: Coupon[] = [],
|
||||
activities: ActivityConfig[] = [],
|
||||
config: OrderExtraConfig,
|
||||
cartOrder: Record<string, number> = {}
|
||||
): OrderCostSummary {
|
||||
// 1. 基础费用:商品原价、实际价、折扣金额
|
||||
const goodsOriginalAmount = calcGoodsOriginalAmount(goodsList);
|
||||
const goodsRealAmount = calcGoodsRealAmount(goodsList, {
|
||||
isMember: config.isMember,
|
||||
memberDiscountRate: config.memberDiscountRate
|
||||
}, activities);
|
||||
const goodsDiscountAmount = calcGoodsDiscountAmount(goodsOriginalAmount, goodsRealAmount);
|
||||
|
||||
// 2. 优惠券抵扣(处理互斥和同享)
|
||||
const { deductionAmount: couponDeductionAmount, usedCoupon, excludedProductIds } = calcCouponDeduction(
|
||||
coupons,
|
||||
goodsList,
|
||||
{
|
||||
currentStoreId: config.currentStoreId,
|
||||
isMember: config.isMember,
|
||||
memberDiscountRate: config.memberDiscountRate,
|
||||
activities,
|
||||
cartOrder
|
||||
}
|
||||
);
|
||||
|
||||
// 3. 其他固定费用:打包费、餐位费
|
||||
const packFee = calcTotalPackFee(goodsList, dinnerType);
|
||||
const seatFee = calcSeatFee(config.seatFeeConfig);
|
||||
|
||||
// 4. 积分抵扣(最大抵扣上限=商品实际价-优惠券抵扣,避免负金额)
|
||||
const maxPointDeductionLimit = Math.max(0, goodsRealAmount - couponDeductionAmount);
|
||||
const { deductionAmount: pointDeductionAmount, usedPoints } = calcPointDeduction(
|
||||
config.userPoints,
|
||||
config.pointDeductionRule,
|
||||
maxPointDeductionLimit
|
||||
);
|
||||
|
||||
// 5. 商家减免和附加费
|
||||
const merchantReductionAmount = Math.max(0, config.merchantReduction); // 减免不能为负
|
||||
const additionalFee = Math.max(0, config.additionalFee); // 附加费不能为负
|
||||
|
||||
// 6. 计算最终实付金额(按用户公式:实付=原价-折扣-优惠券-积分+餐位费+打包费-商家减免+附加费)
|
||||
const finalPayAmount = truncateToTwoDecimals(
|
||||
goodsOriginalAmount
|
||||
- goodsDiscountAmount
|
||||
- couponDeductionAmount
|
||||
- pointDeductionAmount
|
||||
+ seatFee
|
||||
+ packFee
|
||||
- merchantReductionAmount
|
||||
+ additionalFee
|
||||
);
|
||||
|
||||
// 确保实付金额≥0
|
||||
const finalPayAmountNonNegative = Math.max(0, finalPayAmount);
|
||||
|
||||
// 返回完整费用汇总
|
||||
return {
|
||||
goodsOriginalAmount,
|
||||
goodsDiscountAmount,
|
||||
couponDeductionAmount,
|
||||
pointDeductionAmount,
|
||||
seatFee,
|
||||
packFee,
|
||||
merchantReductionAmount,
|
||||
additionalFee,
|
||||
finalPayAmount: finalPayAmountNonNegative,
|
||||
couponUsed: usedCoupon,
|
||||
pointUsed: usedPoints
|
||||
};
|
||||
}
|
||||
|
||||
// ============================ 7. 对外暴露工具库(按模块组织,方便调用) ============================
|
||||
export const OrderPriceCalculator = {
|
||||
// 基础工具
|
||||
truncateToTwoDecimals,
|
||||
isTemporaryGoods,
|
||||
isGiftGoods,
|
||||
// 商品价格计算
|
||||
calcGoodsOriginalAmount,
|
||||
calcGoodsRealAmount,
|
||||
calcGoodsDiscountAmount,
|
||||
// 优惠券计算
|
||||
calcCouponDeduction,
|
||||
// 其他费用计算
|
||||
calcTotalPackFee,
|
||||
calcSeatFee,
|
||||
calcPointDeduction,
|
||||
// 核心入口
|
||||
calculateOrderCostSummary,
|
||||
// 类型导出(方便外部使用类型定义)
|
||||
Types: {
|
||||
GoodsType,
|
||||
CouponType,
|
||||
ActivityType,
|
||||
}
|
||||
};
|
||||
|
||||
export default OrderPriceCalculator;
|
||||
793
src/lib/coupon.ts
Normal file
793
src/lib/coupon.ts
Normal file
@@ -0,0 +1,793 @@
|
||||
import { BigNumber } from "bignumber.js";
|
||||
import _ from "lodash";
|
||||
|
||||
import {
|
||||
ShopInfo,couponCalcParams,
|
||||
BaseCartItem,TimeLimitDiscountConfig,CanDikouGoodsArrArgs,
|
||||
Coupon,
|
||||
ShopUserInfo,
|
||||
GoodsType,
|
||||
BackendCoupon,
|
||||
ExchangeCalculationResult,
|
||||
PointDeductionRule,
|
||||
OrderCostSummary,
|
||||
} from "./types";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 返回商品单价
|
||||
* @param goods 商品
|
||||
* @param user 用户信息
|
||||
* @param {Object} shopInfo
|
||||
*/
|
||||
export function returnGoodsPrice(
|
||||
goods: BaseCartItem,
|
||||
user: ShopUserInfo,
|
||||
shopInfo: ShopInfo,
|
||||
limitTimeDiscount: TimeLimitDiscountConfig | null | undefined
|
||||
) {
|
||||
if (!goods) {
|
||||
return 0;
|
||||
}
|
||||
//是否可以使用会员价
|
||||
const canUseVipPrice =
|
||||
user &&
|
||||
user.isVip &&
|
||||
user.isMemberPrice &&
|
||||
goods.memberPrice * 1 > 0 &&
|
||||
shopInfo &&
|
||||
shopInfo.isMemberPrice;
|
||||
// 商家改价
|
||||
if (goods.discount_sale_amount&&goods.discount_sale_amount * 1 > 0) {
|
||||
return goods.salePrice;
|
||||
}
|
||||
// 限时折扣
|
||||
if (limitTimeDiscount && limitTimeDiscount.id) {
|
||||
const canUseFoods = limitTimeDiscount.foods.split(",");
|
||||
const canUseLimit =
|
||||
limitTimeDiscount.foodType == 1 ||
|
||||
canUseFoods.includes(`${goods.productId}`);
|
||||
if (canUseLimit && limitTimeDiscount.discountPriority == "limit-time") {
|
||||
return new BigNumber(goods.salePrice)
|
||||
.times(limitTimeDiscount.discountRate / 100)
|
||||
.decimalPlaces(2, BigNumber.ROUND_UP)
|
||||
.toNumber();
|
||||
}
|
||||
|
||||
if (canUseLimit && limitTimeDiscount.discountPriority == "vip-price") {
|
||||
if (canUseVipPrice) {
|
||||
return goods.memberPrice;
|
||||
} else {
|
||||
return new BigNumber(goods.salePrice)
|
||||
.times(limitTimeDiscount.discountRate / 100)
|
||||
.decimalPlaces(2, BigNumber.ROUND_UP)
|
||||
.toNumber();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (canUseVipPrice) {
|
||||
return goods.memberPrice;
|
||||
}
|
||||
return goods.salePrice;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回商品分组
|
||||
* @param arr 商品列表
|
||||
*/
|
||||
export function returnGoodsGroupMap(arr: BaseCartItem[]) {
|
||||
let map: { [key: string]: BaseCartItem[] } = {};
|
||||
arr.forEach((v) => {
|
||||
const key = v.productId + "_" + v.skuId;
|
||||
if (!map[key]) {
|
||||
map[key] = [];
|
||||
}
|
||||
map[key].push(v);
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
interface CouponTypes {
|
||||
1: "满减券";
|
||||
2: "商品券";
|
||||
3: "折扣券";
|
||||
4: "第二件半价券";
|
||||
5: "消费送券";
|
||||
6: "买一送一券";
|
||||
7: "固定价格券";
|
||||
8: "免配送费券";
|
||||
}
|
||||
/**
|
||||
* 优惠券类型:1-满减券,2-商品兑换券,3-折扣券,4-第二件半价券,5-消费送券,6-买一送一券,7-固定价格券,8-免配送费券
|
||||
* @param coupon
|
||||
*/
|
||||
export function returnCoupType(coupon: Coupon) {
|
||||
const couponTypes: CouponTypes = {
|
||||
1: "满减券",
|
||||
2: "商品券",
|
||||
3: "折扣券",
|
||||
4: "第二件半价券",
|
||||
5: "消费送券",
|
||||
6: "买一送一券",
|
||||
7: "固定价格券",
|
||||
8: "免配送费券",
|
||||
};
|
||||
return couponTypes[coupon.type as keyof CouponTypes] || "未知类型";
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回商品券抵扣后的商品列表
|
||||
* @param canDikouGoodsArr 可抵扣商品列表
|
||||
* @param selCoupon 已选择的优惠券列表
|
||||
* @param user 用户信息
|
||||
*/
|
||||
export function returnCanDikouGoodsArr(args: CanDikouGoodsArrArgs) {
|
||||
const { canDikouGoodsArr, selCoupon, user, shopInfo, limitTimeDiscount } =
|
||||
args;
|
||||
const types = [2, 4, 6];
|
||||
// 收集已抵扣商品并关联对应的优惠券类型
|
||||
const goodsCouponGoods = selCoupon
|
||||
.filter((v) => types.includes(v.type))
|
||||
.reduce((prev: BaseCartItem[], cur) => {
|
||||
// 给每个抵扣商品添加所属优惠券类型
|
||||
if (cur && cur.discount) {
|
||||
const goodsWithType = cur.discount.hasDiscountGoodsArr.map((goods) => ({
|
||||
...goods,
|
||||
couponType: cur.type, // 记录该商品是被哪种类型的优惠券抵扣的
|
||||
}));
|
||||
prev.push(...goodsWithType);
|
||||
}
|
||||
return prev;
|
||||
}, []);
|
||||
const arr = _.cloneDeep(canDikouGoodsArr)
|
||||
.map((v) => {
|
||||
const findCart = goodsCouponGoods.find((carts) => carts.id == v.id);
|
||||
if (findCart) {
|
||||
// 根据优惠券类型判断扣减数量
|
||||
if ([4, 6].includes(findCart.couponType ?? 0)) {
|
||||
// 类型4(第二件半价)或6(买一送一),数量减2
|
||||
if(v.num){
|
||||
v.num -= 2;
|
||||
}
|
||||
} else {
|
||||
// 其他类型(如类型2商品券),按原逻辑扣减对应数量
|
||||
if(v.num){
|
||||
v.num -= findCart.num??0;
|
||||
}
|
||||
}
|
||||
}
|
||||
return v;
|
||||
})
|
||||
.filter((v) => {
|
||||
const canUseNum = (v.num ?? 0) - (v.returnNum || 0);
|
||||
if (canUseNum <= 0 || v.is_temporary || v.is_gift ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}); // 过滤掉数量<=0的商品,赠菜,临时菜
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回商品是否享用了会员价/会员折扣
|
||||
* @param {*} goods
|
||||
*/
|
||||
function returnGoodsIsUseVipPrice(
|
||||
shopInfo: ShopInfo,
|
||||
user: ShopUserInfo,
|
||||
goods: BaseCartItem
|
||||
) {
|
||||
if (goods.is_time_discount) {
|
||||
return false;
|
||||
}
|
||||
if (shopInfo.isMemberPrice != 1 || user.isVip != 1) {
|
||||
return false;
|
||||
}
|
||||
if (shopInfo.isMemberPrice == 1 && user.isVip == 1) {
|
||||
if (goods.memberPrice <= 0) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回可以计算抵扣金额的商品列表
|
||||
*/
|
||||
function returnCanCalcGoodsList(
|
||||
canCalcGoodsArr: BaseCartItem[],
|
||||
coupon: Coupon,
|
||||
shopInfo: ShopInfo,
|
||||
user: ShopUserInfo
|
||||
) {
|
||||
return canCalcGoodsArr.filter((goods) => {
|
||||
console.log("goods");
|
||||
console.log(goods);
|
||||
if (
|
||||
!coupon.discountShare &&
|
||||
(goods.is_time_discount || goods.isTimeDiscount)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
!coupon.vipPriceShare &&
|
||||
returnGoodsIsUseVipPrice(shopInfo, user, goods)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断优惠券是否可使用,并返回不可用原因
|
||||
*
|
||||
* @param {Object} args - 函数参数集合
|
||||
* @param {Array} args.canDikouGoodsArr - 可参与抵扣的商品列表
|
||||
* @param {Object} args.coupon - 优惠券信息对象
|
||||
* @param {boolean} args.coupon.use - 优惠券是否启用
|
||||
* @param {Array} args.coupon.useFoods - 优惠券适用的商品ID列表
|
||||
* @param {number} args.coupon.fullAmount - 优惠券使用门槛金额
|
||||
* @param {number} args.coupon.type - 优惠券类型
|
||||
* @param {number} args.goodsOrderPrice - 订单中所有商品的总金额
|
||||
* @param {Object} args.user - 用户信息对象
|
||||
* @param {Object} args.selCoupon - 已经选择的优惠券信息对象
|
||||
* @param {Object} args.shopInfo
|
||||
* @param {boolean} args.limitTimeDiscount - 限时折扣
|
||||
* @returns {Object} - { canUse: boolean, reason: string } 可用状态及不可用原因
|
||||
*/
|
||||
export function returnCouponCanUse(args: couponCalcParams) {
|
||||
let {
|
||||
canDikouGoodsArr,
|
||||
coupon,
|
||||
goodsOrderPrice,
|
||||
user,
|
||||
selCoupon,
|
||||
shopInfo,
|
||||
isMemberPrice,
|
||||
limitTimeDiscount,
|
||||
} = args;
|
||||
// 优惠券未启用
|
||||
if (!coupon.use) {
|
||||
return {
|
||||
canUse: false,
|
||||
reason: coupon.noUseRestrictions || "不在可用时间段内",
|
||||
};
|
||||
}
|
||||
if (
|
||||
limitTimeDiscount &&
|
||||
limitTimeDiscount.id &&
|
||||
limitTimeDiscount.foodType == 1 &&
|
||||
!coupon.discountShare
|
||||
) {
|
||||
return {
|
||||
canUse: false,
|
||||
reason: coupon.noUseRestrictions || "不可与限时折扣同享",
|
||||
};
|
||||
}
|
||||
|
||||
// 计算门槛金额
|
||||
let fullAmount = goodsOrderPrice;
|
||||
canDikouGoodsArr = returnCanDikouGoodsArr(args);
|
||||
//优惠券指定门槛商品列表
|
||||
let canCalcGoodsArr = [...canDikouGoodsArr];
|
||||
//部分商品参与门槛计算
|
||||
if (coupon.thresholdFoods.length) {
|
||||
canCalcGoodsArr = canDikouGoodsArr.filter((v) => {
|
||||
return coupon.thresholdFoods.find((food) => food.id == v.productId);
|
||||
});
|
||||
}
|
||||
canCalcGoodsArr = returnCanCalcGoodsList(
|
||||
canCalcGoodsArr,
|
||||
coupon,
|
||||
shopInfo,
|
||||
user
|
||||
);
|
||||
console.log("canCalcGoodsArr");
|
||||
console.log(canCalcGoodsArr);
|
||||
fullAmount = canCalcGoodsArr.reduce((pre, cur) => {
|
||||
return (
|
||||
pre + returnGoodsPrice(cur, user, shopInfo, limitTimeDiscount) * (cur.num||0)
|
||||
);
|
||||
}, 0);
|
||||
|
||||
// 是否全部商品可用
|
||||
const isDikouAll = coupon.useFoods.length === 0;
|
||||
// 订单可用商品列表
|
||||
let canUseGoodsArr: BaseCartItem[] = [];
|
||||
if (!isDikouAll) {
|
||||
canUseGoodsArr = canDikouGoodsArr.filter((v) => {
|
||||
return coupon.useFoods.find((food) => food.id == v.productId);
|
||||
});
|
||||
}
|
||||
// if (user.isVip && !coupon.vipPriceShare) {
|
||||
// return {
|
||||
// canUse: false,
|
||||
// reason: "非会员可用",
|
||||
// };
|
||||
// }
|
||||
if (selCoupon.length > 0 && !selCoupon[0].otherCouponShare) {
|
||||
return {
|
||||
canUse: false,
|
||||
reason: "当前选中的券不可与其他券同享",
|
||||
};
|
||||
}
|
||||
if (selCoupon.length > 0 && !coupon.otherCouponShare) {
|
||||
return {
|
||||
canUse: false,
|
||||
reason: "当前选中的券不可与其他券同享",
|
||||
};
|
||||
}
|
||||
// 满减券和折扣券计算门槛金额是否满足
|
||||
if ([1, 3].includes(coupon.type)) {
|
||||
if (canCalcGoodsArr.length <= 0) {
|
||||
return {
|
||||
canUse: false,
|
||||
reason: "没有可参与计算门槛的商品",
|
||||
};
|
||||
}
|
||||
// 不满足门槛金额
|
||||
if (fullAmount < (coupon.fullAmount || 0)) {
|
||||
return {
|
||||
canUse: false,
|
||||
reason: `满${coupon.fullAmount}元可用,当前可参与金额${fullAmount}元`,
|
||||
};
|
||||
}
|
||||
}
|
||||
// 商品兑换券,第二件半价和买一送一判断是否有可用商品
|
||||
if ([2, 4, 5].includes(coupon.type)) {
|
||||
// 没有符合条件的商品
|
||||
if (isDikouAll && canDikouGoodsArr.length === 0) {
|
||||
return {
|
||||
canUse: false,
|
||||
reason: "没有符合条件的商品",
|
||||
};
|
||||
}
|
||||
if (!isDikouAll && canUseGoodsArr.length === 0) {
|
||||
return {
|
||||
canUse: false,
|
||||
reason: "没有符合条件的商品",
|
||||
};
|
||||
}
|
||||
if (coupon.type == 2) {
|
||||
if (canCalcGoodsArr.length <= 0) {
|
||||
return {
|
||||
canUse: false,
|
||||
reason: "没有符合计算门槛条件的商品",
|
||||
};
|
||||
}
|
||||
if (fullAmount < (coupon.fullAmount || 0)) {
|
||||
return {
|
||||
canUse: false,
|
||||
reason: `满${coupon.fullAmount}元可用,当前可参与金额${fullAmount}元`,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
//商品兑换券是否达到门槛金额
|
||||
if (coupon.type == 2 && goodsOrderPrice < (coupon.fullAmount || 0)) {
|
||||
return {
|
||||
canUse: false,
|
||||
reason: `满${coupon.fullAmount}元可用,当前可参与金额${fullAmount}元`,
|
||||
};
|
||||
}
|
||||
|
||||
// 买一送一券特殊验证
|
||||
if (coupon.type === 6) {
|
||||
let canUse = false;
|
||||
if (isDikouAll) {
|
||||
canUse = canDikouGoodsArr.some((v) => (v.num || 0) >= 2);
|
||||
} else if (canUseGoodsArr.length > 0) {
|
||||
canUse = canUseGoodsArr.some((v) => (v.num || 0) >= 2);
|
||||
}
|
||||
|
||||
if (!canUse) {
|
||||
return {
|
||||
canUse: false,
|
||||
reason: "需要购买至少2件相同的商品才能使用",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 第二件半价券特殊验证
|
||||
if (coupon.type === 4) {
|
||||
let canUse = false;
|
||||
if (isDikouAll) {
|
||||
canUse = canDikouGoodsArr.some((v) => (v.num || 0) >= 2);
|
||||
} else if (canUseGoodsArr.length > 0) {
|
||||
canUse = canUseGoodsArr.some((v) => (v.num || 0) >= 2);
|
||||
}
|
||||
if (!canUse) {
|
||||
return {
|
||||
canUse: false,
|
||||
reason: "需要购买至少2件相同的商品才能使用",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 所有条件都满足
|
||||
return {
|
||||
canUse: true,
|
||||
reason: "",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算抵扣商品金额
|
||||
* @param discountGoodsArr 可抵扣商品列表
|
||||
* @param discountNum 抵扣数量
|
||||
* @param user 用户信息
|
||||
* @param {Object} shopInfo 店铺信息
|
||||
*/
|
||||
export function calcDiscountGoodsArrPrice(
|
||||
discountGoodsArr: BaseCartItem[],
|
||||
discountNum: number,
|
||||
user: ShopUserInfo,
|
||||
shopInfo: ShopInfo,
|
||||
limitTimeDiscount?: TimeLimitDiscountConfig | null | undefined
|
||||
) {
|
||||
let hasCountNum = 0;
|
||||
let discountPrice = 0;
|
||||
let hasDiscountGoodsArr = [];
|
||||
|
||||
for (let i = 0; i < discountGoodsArr.length; i++) {
|
||||
if (hasCountNum >= discountNum) {
|
||||
break;
|
||||
}
|
||||
const goods = discountGoodsArr[i];
|
||||
const shengyuNum = discountNum - hasCountNum;
|
||||
const num = Math.min((goods.num || 0), shengyuNum);
|
||||
const realPrice = returnGoodsPrice(
|
||||
goods,
|
||||
user,
|
||||
shopInfo,
|
||||
limitTimeDiscount
|
||||
);
|
||||
|
||||
discountPrice += realPrice * num;
|
||||
|
||||
hasCountNum += num;
|
||||
hasDiscountGoodsArr.push({
|
||||
...goods,
|
||||
num,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
discountPrice,
|
||||
hasDiscountGoodsArr,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算优惠券抵扣金额
|
||||
* @param arr 可抵扣商品列表
|
||||
* @param coupon 优惠券
|
||||
* @param user 用户信息
|
||||
* @param goodsOrderPrice 商品订单金额
|
||||
* @param selCoupon 已选择的优惠券列表
|
||||
* @param shopInfo 店铺信息
|
||||
* @param limitTimeDiscount 限时折扣
|
||||
*/
|
||||
export function returnCouponDiscount(
|
||||
arr: BaseCartItem[],
|
||||
coupon: Coupon,
|
||||
user: ShopUserInfo,
|
||||
goodsOrderPrice: number,
|
||||
selCoupon: Coupon[],
|
||||
shopInfo: ShopInfo,
|
||||
limitTimeDiscount?: TimeLimitDiscountConfig | null | undefined
|
||||
) {
|
||||
arr = returnCanDikouGoods(arr, user, shopInfo, limitTimeDiscount);
|
||||
const canDikouGoodsArr = returnCanDikouGoodsArr({
|
||||
canDikouGoodsArr: arr,
|
||||
selCoupon,
|
||||
user,
|
||||
shopInfo,
|
||||
limitTimeDiscount,
|
||||
});
|
||||
if (coupon.type == 2) {
|
||||
return returnCouponProductDiscount(
|
||||
canDikouGoodsArr,
|
||||
coupon,
|
||||
user,
|
||||
shopInfo,
|
||||
limitTimeDiscount
|
||||
);
|
||||
}
|
||||
if (coupon.type == 6) {
|
||||
const result = returnCouponBuyOneGiveOneDiscount(
|
||||
canDikouGoodsArr,
|
||||
coupon,
|
||||
user,
|
||||
shopInfo,
|
||||
limitTimeDiscount
|
||||
);
|
||||
return result;
|
||||
}
|
||||
if (coupon.type == 4) {
|
||||
return returnSecoendDiscount(
|
||||
canDikouGoodsArr,
|
||||
coupon,
|
||||
user,
|
||||
shopInfo,
|
||||
limitTimeDiscount
|
||||
);
|
||||
}
|
||||
if (coupon.type == 3) {
|
||||
return returnCouponZhekouDiscount(
|
||||
canDikouGoodsArr,
|
||||
coupon,
|
||||
user,
|
||||
goodsOrderPrice,
|
||||
selCoupon,
|
||||
limitTimeDiscount
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 折扣券抵扣金额
|
||||
* @param canDikouGoodsArr 可抵扣商品列表
|
||||
* @param coupon 优惠券
|
||||
* @param user 用户信息
|
||||
* @param goodsOrderPrice 商品订单金额
|
||||
* @param selCoupon 已选择的优惠券列表
|
||||
* @param limitTimeDiscount 限时折扣
|
||||
*/
|
||||
export function returnCouponZhekouDiscount(
|
||||
canDikouGoodsArr: BaseCartItem[],
|
||||
coupon: Coupon,
|
||||
user: ShopUserInfo,
|
||||
goodsOrderPrice: number,
|
||||
selCoupon: Coupon[],
|
||||
limitTimeDiscount?: TimeLimitDiscountConfig | null | undefined
|
||||
) {
|
||||
let { discountRate, maxDiscountAmount } = coupon;
|
||||
maxDiscountAmount = maxDiscountAmount || 0;
|
||||
// 计算商品优惠券折扣总和,使用BigNumber避免精度问题
|
||||
const goodsCouponDiscount = selCoupon
|
||||
.filter((v) => v.type == 2)
|
||||
.reduce((prve, cur) => {
|
||||
return new BigNumber(prve).plus(
|
||||
new BigNumber(cur?.discount?.discountPrice || 0)
|
||||
);
|
||||
}, new BigNumber(0));
|
||||
|
||||
// 将商品订单价格转换为BigNumber并减去优惠券折扣
|
||||
const adjustedGoodsOrderPrice = new BigNumber(goodsOrderPrice).minus(
|
||||
goodsCouponDiscount
|
||||
);
|
||||
|
||||
// 计算优惠比例:(100 - 折扣率) / 100
|
||||
const discountAmountRatio = new BigNumber(100)
|
||||
.minus(discountRate||0)
|
||||
.dividedBy(100);
|
||||
|
||||
// 计算折扣金额:调整后的商品订单金额 × 优惠比例
|
||||
let discountPrice = adjustedGoodsOrderPrice
|
||||
.times(discountAmountRatio)
|
||||
.decimalPlaces(2, BigNumber.ROUND_FLOOR)
|
||||
.toNumber();
|
||||
|
||||
// 应用最大折扣金额限制
|
||||
if (maxDiscountAmount !== 0) {
|
||||
discountPrice =
|
||||
discountPrice >= maxDiscountAmount ? maxDiscountAmount : discountPrice;
|
||||
}
|
||||
|
||||
return {
|
||||
discountPrice, // 折扣抵扣金额(即优惠的金额)
|
||||
hasDiscountGoodsArr: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 商品券抵扣金额
|
||||
* @param canDikouGoodsArr 可抵扣商品列表
|
||||
* @param coupon 优惠券
|
||||
* @param user 用户信息
|
||||
* @param shopInfo 店铺信息
|
||||
*/
|
||||
export function returnCouponProductDiscount(
|
||||
canDikouGoodsArr: BaseCartItem[],
|
||||
coupon: Coupon,
|
||||
user: ShopUserInfo,
|
||||
shopInfo: ShopInfo,
|
||||
limitTimeDiscount?: TimeLimitDiscountConfig | null | undefined
|
||||
) {
|
||||
let { useFoods, discountNum, useRule } = coupon;
|
||||
discountNum = discountNum || 0;
|
||||
//抵扣商品数组
|
||||
let discountGoodsArr = [];
|
||||
|
||||
//抵扣全部商品
|
||||
if (useFoods.length === 0) {
|
||||
if (useRule == "price_asc") {
|
||||
discountGoodsArr = canDikouGoodsArr.slice(discountNum * -1).reverse();
|
||||
} else {
|
||||
discountGoodsArr = canDikouGoodsArr.slice(0, discountNum);
|
||||
}
|
||||
} else {
|
||||
//抵扣选中商品
|
||||
const discountSelGoodsArr = canDikouGoodsArr.filter((v) =>
|
||||
useFoods.find((food) => food.id == v.productId)
|
||||
);
|
||||
if (useRule == "price_asc") {
|
||||
discountGoodsArr = discountSelGoodsArr.slice(discountNum * -1).reverse();
|
||||
} else {
|
||||
discountGoodsArr = discountSelGoodsArr.slice(0, discountNum);
|
||||
}
|
||||
}
|
||||
|
||||
const result = calcDiscountGoodsArrPrice(
|
||||
discountGoodsArr,
|
||||
discountNum,
|
||||
user,
|
||||
shopInfo,
|
||||
limitTimeDiscount
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
// 返回买一送一券抵扣详情
|
||||
/**
|
||||
* @param canDikouGoodsArr 可抵扣商品列表
|
||||
* @param coupon 优惠券
|
||||
* @param user 用户信息
|
||||
* @param shopInfo 店铺信息
|
||||
*/
|
||||
function returnCouponBuyOneGiveOneDiscount(
|
||||
canDikouGoodsArr: BaseCartItem[],
|
||||
coupon: Coupon,
|
||||
user: ShopUserInfo,
|
||||
shopInfo: ShopInfo,
|
||||
limitTimeDiscount?: TimeLimitDiscountConfig | null | undefined
|
||||
) {
|
||||
const { useFoods, useRule } = coupon;
|
||||
//抵扣商品
|
||||
let discountGoods = undefined;
|
||||
//符合买一送一条件的商品
|
||||
const canUseGoods = canDikouGoodsArr.filter((v) => (v.num || 0) >= 2);
|
||||
//抵扣全部商品
|
||||
if (useFoods.length === 0) {
|
||||
if (useRule == "price_asc") {
|
||||
discountGoods = canUseGoods[canUseGoods.length - 1];
|
||||
} else {
|
||||
discountGoods = canUseGoods[0];
|
||||
}
|
||||
} else {
|
||||
//符合抵扣条件的商品
|
||||
const canUseGoods1 = canUseGoods.filter((v) =>
|
||||
useFoods.find((food) => food.id == v.productId)
|
||||
);
|
||||
if (useRule == "price_asc") {
|
||||
discountGoods = canUseGoods1[canUseGoods1.length - 1];
|
||||
} else {
|
||||
discountGoods = canUseGoods1[0];
|
||||
}
|
||||
}
|
||||
let discountPrice = 0;
|
||||
let hasDiscountGoodsArr: BaseCartItem[] = [];
|
||||
if (discountGoods) {
|
||||
discountPrice = returnGoodsPrice(
|
||||
discountGoods,
|
||||
user,
|
||||
shopInfo,
|
||||
limitTimeDiscount
|
||||
);
|
||||
hasDiscountGoodsArr = [discountGoods];
|
||||
}
|
||||
return {
|
||||
discountPrice: discountPrice <= 0 ? 0 : discountPrice,
|
||||
hasDiscountGoodsArr,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回第二件半价券抵扣详情
|
||||
* @param canDikouGoodsArr 可抵扣商品列表
|
||||
* @param coupon 优惠券
|
||||
* @param user 用户信息
|
||||
* @param shopInfo 店铺信息
|
||||
*/
|
||||
function returnSecoendDiscount(
|
||||
canDikouGoodsArr: BaseCartItem[],
|
||||
coupon: Coupon,
|
||||
user: ShopUserInfo,
|
||||
shopInfo: ShopInfo,
|
||||
limitTimeDiscount?: TimeLimitDiscountConfig | null | undefined
|
||||
) {
|
||||
const { useFoods, useRule } = coupon;
|
||||
//抵扣商品
|
||||
let discountGoods = undefined;
|
||||
//符合条件的商品
|
||||
const canUseGoods = canDikouGoodsArr.filter((v) => (v.num || 0) >= 2);
|
||||
//抵扣全部商品
|
||||
if (useFoods.length === 0) {
|
||||
if (useRule == "price_asc") {
|
||||
discountGoods = canUseGoods[canUseGoods.length - 1];
|
||||
} else {
|
||||
discountGoods = canUseGoods[0];
|
||||
}
|
||||
} else {
|
||||
//符合抵扣条件的商品
|
||||
const canUseGoods1 = canUseGoods.filter((v) =>
|
||||
useFoods.find((food) => food.id == v.productId)
|
||||
);
|
||||
if (useRule == "price_asc") {
|
||||
discountGoods = canUseGoods1[canUseGoods1.length - 1];
|
||||
} else {
|
||||
discountGoods = canUseGoods1[0];
|
||||
}
|
||||
}
|
||||
let discountPrice = 0;
|
||||
let hasDiscountGoodsArr: BaseCartItem[] = [];
|
||||
if (discountGoods) {
|
||||
discountPrice = returnGoodsPrice(
|
||||
discountGoods,
|
||||
user,
|
||||
shopInfo,
|
||||
limitTimeDiscount
|
||||
);
|
||||
hasDiscountGoodsArr = [discountGoods];
|
||||
}
|
||||
//返回半价价格
|
||||
return {
|
||||
discountPrice:
|
||||
discountPrice <= 0
|
||||
? 0
|
||||
: new BigNumber(discountPrice).dividedBy(2).toNumber(),
|
||||
hasDiscountGoodsArr,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回可以抵扣优惠券的商品列表,过滤掉赠品、临时商品,价格从高到低排序
|
||||
* @param arr 商品列表
|
||||
* @param user 用户信息
|
||||
* @param shopInfo 店铺信息
|
||||
* @param limitTimeDiscount 限时折扣
|
||||
*/
|
||||
export function returnCanDikouGoods(
|
||||
arr: BaseCartItem[],
|
||||
user: ShopUserInfo,
|
||||
shopInfo: ShopInfo,
|
||||
limitTimeDiscount?: TimeLimitDiscountConfig | null | undefined
|
||||
) {
|
||||
const result = arr
|
||||
.filter((v) => {
|
||||
return !v.is_temporary && !v.is_gift ;
|
||||
})
|
||||
.filter((v) => {
|
||||
return (v.num || 0) > 0;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
return (
|
||||
returnGoodsPrice(b, user, shopInfo, limitTimeDiscount) -
|
||||
returnGoodsPrice(a, user, shopInfo, limitTimeDiscount)
|
||||
);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
export const utils = {
|
||||
returnGoodsPrice,
|
||||
returnGoodsGroupMap,
|
||||
returnCoupType,
|
||||
returnCanDikouGoods,
|
||||
returnCanDikouGoodsArr,
|
||||
returnCouponCanUse,
|
||||
calcDiscountGoodsArrPrice,
|
||||
returnCouponDiscount,
|
||||
returnCouponProductDiscount,
|
||||
returnCouponZhekouDiscount,
|
||||
};
|
||||
|
||||
export default utils;
|
||||
1275
src/lib/goods-1.0.47-back.ts
Normal file
1275
src/lib/goods-1.0.47-back.ts
Normal file
File diff suppressed because it is too large
Load Diff
1373
src/lib/goods.ts
Normal file
1373
src/lib/goods.ts
Normal file
File diff suppressed because it is too large
Load Diff
11
src/lib/index.ts
Normal file
11
src/lib/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export * from "./types";
|
||||
import OrderPriceCalculator from "./goods";
|
||||
import couponUtils from "./coupon";
|
||||
import limitUtils from "./limit";
|
||||
|
||||
export { OrderPriceCalculator, couponUtils, limitUtils };
|
||||
export default {
|
||||
OrderPriceCalculator,
|
||||
couponUtils,
|
||||
limitUtils,
|
||||
};
|
||||
216
src/lib/limit.ts
Normal file
216
src/lib/limit.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import BigNumber from "bignumber.js";
|
||||
|
||||
import _ from "lodash";
|
||||
|
||||
import {
|
||||
BaseCartItem,
|
||||
ShopUserInfo,
|
||||
ShopInfo,
|
||||
TimeLimitDiscountConfig,
|
||||
CanReturnMemberPriceArgs,
|
||||
returnPriceArgs,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
* 判断商品是否可以使用限时折扣
|
||||
* @param goods 商品对象
|
||||
* @param limitTimeDiscountRes 限时折扣配置
|
||||
* @param shopInfo 店铺信息
|
||||
* @param shopUserInfo 店铺用户信息
|
||||
* @param idKey 商品ID键名,默认"id"
|
||||
* @returns
|
||||
*/
|
||||
export function canUseLimitTimeDiscount(
|
||||
goods: BaseCartItem,
|
||||
limitTimeDiscountRes: TimeLimitDiscountConfig | null | undefined,
|
||||
shopInfo: ShopInfo,
|
||||
shopUserInfo: ShopUserInfo,
|
||||
idKey = "id" as keyof BaseCartItem
|
||||
) {
|
||||
shopInfo = shopInfo || {};
|
||||
shopUserInfo = shopUserInfo || {};
|
||||
if (!limitTimeDiscountRes || !limitTimeDiscountRes.id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const canUseFoods = (limitTimeDiscountRes.foods || "").split(",");
|
||||
|
||||
const goodsCanUse =
|
||||
limitTimeDiscountRes.foodType == 1 ||
|
||||
canUseFoods.includes(`${goods[idKey]}`);
|
||||
if (!goodsCanUse) {
|
||||
return false;
|
||||
}
|
||||
if (limitTimeDiscountRes.discountPriority == "limit-time") {
|
||||
return true;
|
||||
}
|
||||
if (limitTimeDiscountRes.discountPriority == "vip-price") {
|
||||
if (shopUserInfo.isVip != 1 || shopUserInfo.isMemberPrice != 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
shopUserInfo.isVip == 1 &&
|
||||
shopUserInfo.isMemberPrice == 1 &&
|
||||
goods.memberPrice * 1 <= 0
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回商品显示价格
|
||||
* @params {*} args 参数对象
|
||||
* @params {*} args.goods 商品对象
|
||||
* @params {*} args.shopInfo 店铺信息
|
||||
* @params {*} args.limitTimeDiscountRes 限时折扣信息
|
||||
* @params {*} args.shopUserInfo 店铺用户信息
|
||||
* @returns
|
||||
*/
|
||||
export function returnPrice(args: returnPriceArgs) {
|
||||
let {
|
||||
goods,
|
||||
shopInfo,
|
||||
limitTimeDiscountRes,
|
||||
shopUserInfo,
|
||||
idKey = "product_id",
|
||||
} = args;
|
||||
limitTimeDiscountRes = limitTimeDiscountRes || {
|
||||
foods: "",
|
||||
foodType: 2,
|
||||
discountPriority: "",
|
||||
discountRate: 0,
|
||||
id: 0,
|
||||
shopId: 0,
|
||||
useType: "",
|
||||
};
|
||||
const canUseFoods = (limitTimeDiscountRes.foods || "").split(",");
|
||||
const includesGoods =
|
||||
limitTimeDiscountRes.foodType == 1 ||
|
||||
canUseFoods.includes("" + goods[idKey]);
|
||||
shopInfo = shopInfo || {};
|
||||
shopUserInfo = shopUserInfo || {};
|
||||
if (
|
||||
shopUserInfo.isMemberPrice == 1 &&
|
||||
shopUserInfo.isVip == 1 &&
|
||||
shopInfo.isMemberPrice == 1
|
||||
) {
|
||||
const memberPrice = goods.memberPrice || goods.salePrice;
|
||||
|
||||
//是会员而且启用会员价
|
||||
if (limitTimeDiscountRes) {
|
||||
//使用限时折扣
|
||||
//限时折扣优先
|
||||
if (limitTimeDiscountRes.discountPriority == "limit-time") {
|
||||
if (includesGoods) {
|
||||
return returnLimitPrice({
|
||||
price: goods.salePrice,
|
||||
limitTimeDiscountRes,
|
||||
});
|
||||
} else {
|
||||
return memberPrice;
|
||||
}
|
||||
}
|
||||
if (
|
||||
limitTimeDiscountRes.discountPriority == "vip-price" &&
|
||||
includesGoods
|
||||
) {
|
||||
if (goods.memberPrice * 1 > 0) {
|
||||
//会员优先
|
||||
return memberPrice;
|
||||
} else {
|
||||
const price = returnLimitPrice({
|
||||
price: goods.salePrice,
|
||||
limitTimeDiscountRes,
|
||||
goods: goods,
|
||||
});
|
||||
|
||||
return price;
|
||||
}
|
||||
} else {
|
||||
return memberPrice;
|
||||
}
|
||||
} else {
|
||||
//是会员没有限时折扣
|
||||
return memberPrice;
|
||||
}
|
||||
} else {
|
||||
// console.log('不是会员或者没有启用会员价',goods,limitTimeDiscountRes);
|
||||
//不是会员或者没有启用会员价
|
||||
if (limitTimeDiscountRes && limitTimeDiscountRes.id && includesGoods) {
|
||||
const price = returnLimitPrice({
|
||||
price: goods.salePrice,
|
||||
limitTimeDiscountRes,
|
||||
goods: goods,
|
||||
});
|
||||
|
||||
return price;
|
||||
} else {
|
||||
return goods.salePrice;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface returnLimitPriceArgs {
|
||||
limitTimeDiscountRes: TimeLimitDiscountConfig | null | undefined;
|
||||
price: number;
|
||||
goods?: BaseCartItem;
|
||||
}
|
||||
/**
|
||||
* 返回限时折扣价格
|
||||
* @params {*} args 参数对象
|
||||
* @params {*} args.limitTimeDiscountRes 限时折扣信息
|
||||
* @params {*} args.price 商品价格
|
||||
* @param {*} args.goods 商品对象
|
||||
* @returns
|
||||
*/
|
||||
export function returnLimitPrice(args: returnLimitPriceArgs) {
|
||||
const { limitTimeDiscountRes, price, goods } = args;
|
||||
const discountRate = new BigNumber(
|
||||
limitTimeDiscountRes ? limitTimeDiscountRes.discountRate : 100
|
||||
).dividedBy(100);
|
||||
|
||||
const result = BigNumber(price)
|
||||
.times(discountRate)
|
||||
.decimalPlaces(2, BigNumber.ROUND_UP)
|
||||
.toNumber();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否返回会员价
|
||||
* @param {*} args 参数对象
|
||||
* @param {*} args.shopInfo 店铺信息
|
||||
* @param {*} args.shopUserInfo 店铺用户信息
|
||||
* @returns
|
||||
*/
|
||||
export function canReturnMemberPrice(args: CanReturnMemberPriceArgs) {
|
||||
const { shopInfo, shopUserInfo } = args;
|
||||
if (shopUserInfo.isMemberPrice == 1 && shopUserInfo.isVip == 1) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回会员价格
|
||||
* @param {*} goods
|
||||
* @returns
|
||||
*/
|
||||
export function returnMemberPrice(goods: BaseCartItem) {
|
||||
return goods.memberPrice || goods.salePrice;
|
||||
}
|
||||
|
||||
export const utils = {
|
||||
returnPrice,
|
||||
canUseLimitTimeDiscount,
|
||||
returnLimitPrice,
|
||||
canReturnMemberPrice,
|
||||
returnMemberPrice,
|
||||
};
|
||||
|
||||
export default utils;
|
||||
429
src/lib/types.ts
Normal file
429
src/lib/types.ts
Normal file
@@ -0,0 +1,429 @@
|
||||
/** 商品类型枚举 */
|
||||
export enum GoodsType {
|
||||
NORMAL = "normal", // 普通商品
|
||||
WEIGHT = "weight", // 称重商品
|
||||
GIFT = "gift", // 赠菜(继承普通商品逻辑,标记用)
|
||||
EMPTY = "", // 空字符串类型(后端未返回时默认归类为普通商品)
|
||||
PACKAGE = "package", // 打包商品(如套餐/预打包商品,按普通商品逻辑处理,可扩展特殊规则)
|
||||
}
|
||||
|
||||
/** 优惠券计算结果类型(新增细分字段) */
|
||||
export interface CouponResult {
|
||||
deductionAmount: number; // 抵扣金额
|
||||
excludedProductIds: string[]; // 不适用商品ID列表(注意:是商品ID,非购物车ID)
|
||||
usedCoupon: Coupon | undefined; // 实际使用的优惠券
|
||||
productCouponDeduction: number; // 新增:商品优惠券抵扣(兑换券等)
|
||||
fullCouponDeduction: number; // 新增:满减优惠券抵扣
|
||||
}
|
||||
|
||||
/** 兑换券计算结果类型(新增细分字段) */
|
||||
export interface ExchangeCalculationResult {
|
||||
deductionAmount: number;
|
||||
excludedProductIds: string[]; // 不适用商品ID列表(商品ID)
|
||||
productCouponDeduction: number; // 新增:兑换券属于商品券,同步记录
|
||||
}
|
||||
|
||||
export interface CouponTypes {
|
||||
1: "满减券";
|
||||
2: "商品券";
|
||||
3: "折扣券";
|
||||
4: "第二件半价券";
|
||||
5: "消费送券";
|
||||
6: "买一送一券";
|
||||
7: "固定价格券";
|
||||
8: "免配送费券";
|
||||
}
|
||||
|
||||
/** 优惠券类型枚举 */
|
||||
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(唯一标识商品,用于优惠券/活动匹配,必选)
|
||||
productId?: string | number; // 商品ID
|
||||
salePrice: number; // 商品原价(元)
|
||||
number: number; // 商品数量
|
||||
num?: number; // 商品数量
|
||||
isTimeDiscount?: boolean; // 是否限时折扣商品(默认false)
|
||||
is_time_discount?: boolean; // 是否限时折扣商品(默认false)
|
||||
product_type: GoodsType; // 商品类型
|
||||
is_temporary?: boolean; // 是否临时菜(默认false)
|
||||
isTemporary?: boolean; // 是否临时菜(默认false)
|
||||
is_gift?: boolean; // 是否赠菜(默认false)
|
||||
isGift?: boolean; // 是否赠菜(默认false)
|
||||
returnNum?: number; // 退货数量(历史订单用,默认0)
|
||||
memberPrice: number; // 商品会员价(元,优先级:商品会员价 > 会员折扣)
|
||||
discountSaleAmount?: number; // 商家改价后单价(元,优先级最高)
|
||||
discount_sale_amount?: 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原价
|
||||
};
|
||||
skuId?: string | number; // SKU ID(唯一标识商品规格,如颜色/尺寸)
|
||||
couponType?: number; // 优惠券类型:1-满减券,2-商品兑换券,3-折扣券,4-第二件半价券,5-消费送券,6-买一送一券,7-固定价格券,8-免配送费券
|
||||
}
|
||||
|
||||
export interface CouponFoods {
|
||||
id: string;
|
||||
name: string;
|
||||
images: string;
|
||||
}
|
||||
|
||||
/** 基础优惠券接口(所有券类型继承,包含统一门槛商品字段) */
|
||||
export interface BaseCoupon {
|
||||
otherCouponShare?: number; // 与其它优惠共享:0-否,1-是
|
||||
id: string | number; // 优惠券ID
|
||||
type: number; // 工具库字符串枚举(由后端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; // 可抵扣最大金额 元
|
||||
use: boolean;
|
||||
discountNum?: number; // 抵扣数量
|
||||
useRule?: string; // 使用规则:price_asc-价格低到高,price_desc-高到低
|
||||
discountRate?: number; // 折扣%(如90=9折)
|
||||
noUseRestrictions?: boolean; // 是不可用原因
|
||||
thresholdFoods: CouponFoods[]; // 门槛商品ID列表(空数组=全部商品,非空=指定商品ID)
|
||||
useFoods: CouponFoods[]; // 可用商品ID列表(空数组=全部商品,非空=指定商品ID)
|
||||
}
|
||||
export interface couponDiscount {
|
||||
discountPrice: number;
|
||||
hasDiscountGoodsArr: BaseCartItem[];
|
||||
}
|
||||
/** 满减券(适配后端字段) */
|
||||
export interface FullReductionCoupon extends BaseCoupon {
|
||||
fullAmount: number; // 对应后端fullAmount(满减门槛)
|
||||
discountAmount: number; // 对应后端discountAmount(减免金额)
|
||||
maxDiscountAmount?: number; // 对应后端maxDiscountAmount(最大减免)
|
||||
discount?: couponDiscount;
|
||||
}
|
||||
|
||||
/** 折扣券(适配后端字段) */
|
||||
export interface DiscountCoupon extends BaseCoupon {
|
||||
discountRate: number; // 后端discountRate(%)转小数(如90→0.9)
|
||||
maxDiscountAmount: number; // 对应后端maxDiscountAmount(最大减免)
|
||||
discount?: couponDiscount;
|
||||
}
|
||||
|
||||
/** 第二件半价券(适配后端字段) */
|
||||
export interface SecondHalfPriceCoupon extends BaseCoupon {
|
||||
maxUseCountPerOrder?: number; // 对应后端useLimit(-10086=不限)
|
||||
discount?: couponDiscount;
|
||||
}
|
||||
|
||||
/** 买一送一券(适配后端字段) */
|
||||
export interface BuyOneGetOneCoupon extends BaseCoupon {
|
||||
maxUseCountPerOrder?: number; // 对应后端useLimit(-10086=不限)
|
||||
discount?: couponDiscount;
|
||||
}
|
||||
|
||||
/** 商品兑换券(适配后端字段) */
|
||||
export interface ExchangeCoupon extends BaseCoupon {
|
||||
deductCount: number; // 对应后端discountNum(抵扣数量)
|
||||
sortRule: "low_price_first" | "high_price_first"; // 后端useRule转换
|
||||
discount?: couponDiscount;
|
||||
}
|
||||
|
||||
/** 所有优惠券类型联合 */
|
||||
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 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 TimeLimitDiscountConfig {
|
||||
/**
|
||||
* 折扣优先级 limit-time/vip-price
|
||||
*/
|
||||
discountPriority: string;
|
||||
/**
|
||||
* 折扣% 范围1-99
|
||||
*/
|
||||
discountRate: number;
|
||||
/**
|
||||
* 参与商品
|
||||
*/
|
||||
foods: string;
|
||||
/**
|
||||
* 参与商品 1全部 2部分
|
||||
*/
|
||||
foodType: number;
|
||||
/**
|
||||
* 自增主键
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* 店铺ID
|
||||
*/
|
||||
shopId: number;
|
||||
/**
|
||||
* 可使用类型:堂食 dine-in 外带 take-out 外卖 take-away 配送 post
|
||||
*/
|
||||
useType: string;
|
||||
[property: string]: any;
|
||||
}
|
||||
|
||||
//用户信息
|
||||
export interface ShopUserInfo {
|
||||
isVip: number | null; //是否会员
|
||||
discount: number | null; //用户折扣
|
||||
isMemberPrice: number | null; //会员折扣与会员价是否同时使用
|
||||
}
|
||||
/** 订单额外费用配置 */
|
||||
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)
|
||||
fullReductionActivities: FullReductionActivity[]; // 当前店铺的满减活动列表(后端返回结构)
|
||||
currentDinnerType: "dine-in" | "take-out" | "take-away" | "post"; // 当前就餐类型(匹配useType)
|
||||
isFreeDine?: boolean; //是否霸王餐
|
||||
freeDineConfig?: FreeDineConfig;
|
||||
limitTimeDiscount?: TimeLimitDiscountConfig; //限时折扣
|
||||
shopUserInfo: ShopUserInfo; // 用户信息
|
||||
}
|
||||
|
||||
/** 订单费用汇总(修改:补充商家减免类型和明细) */
|
||||
export interface OrderCostSummary {
|
||||
goodsList: BaseCartItem[];
|
||||
// 商品总件数
|
||||
goodsTotal: number;
|
||||
totalDiscountAmount: number;
|
||||
goodsRealAmount: number; // 商品真实原价总和
|
||||
goodsOriginalAmount: number; // 商品原价总和
|
||||
goodsDiscountAmount: number; // 商品折扣金额
|
||||
couponDeductionAmount: number; // 优惠券总抵扣
|
||||
productCouponDeduction: number; // 商品优惠券抵扣
|
||||
fullCouponDeduction: number; // 满减优惠券抵扣
|
||||
pointDeductionAmount: number; // 积分抵扣金额
|
||||
seatFee: number; // 餐位费
|
||||
packFee: number; // 打包费
|
||||
scoreMaxMoney: 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; // 订单额外费用配置
|
||||
//满减活动
|
||||
fullReduction: {
|
||||
usedFullReductionActivityFullAmount: number; // 计算出的满减活动的门槛金额
|
||||
usedActivity?: FullReductionActivity; // 实际使用的满减活动
|
||||
usedThreshold?: FullReductionThreshold; // 实际使用的满减阈值(多门槛中选最优)
|
||||
actualAmount: number; // 满减实际减免金额(元)
|
||||
};
|
||||
vipDiscountAmount: number; //会员折扣减免金额
|
||||
// 订单原支付金额
|
||||
orderOriginFinalPayAmount: 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)
|
||||
}
|
||||
|
||||
// 辅助枚举:星期映射(用于useDays校验)
|
||||
export const WEEKDAY_MAP = {
|
||||
周一: 1,
|
||||
周二: 2,
|
||||
周三: 3,
|
||||
周四: 4,
|
||||
周五: 5,
|
||||
周六: 6,
|
||||
周日: 0, // JS中getDay()返回0=周日
|
||||
};
|
||||
|
||||
export interface ShopInfo {
|
||||
isMemberPrice: number; // 是否开启会员价 1是开启
|
||||
[property: string]: any;
|
||||
}
|
||||
|
||||
export interface couponCalcParams {
|
||||
canDikouGoodsArr: BaseCartItem[];
|
||||
coupon: Coupon;
|
||||
user: ShopUserInfo;
|
||||
shopInfo: ShopInfo;
|
||||
selCoupon: Coupon[];
|
||||
goodsOrderPrice: number; //商品订单总价
|
||||
isMemberPrice: number; // 是否开启会员价 1是开启
|
||||
limitTimeDiscount?: TimeLimitDiscountConfig | null | undefined;
|
||||
}
|
||||
export interface CanDikouGoodsArrArgs {
|
||||
canDikouGoodsArr: BaseCartItem[];
|
||||
selCoupon: Coupon[];
|
||||
user: ShopUserInfo;
|
||||
shopInfo: ShopInfo;
|
||||
limitTimeDiscount?: TimeLimitDiscountConfig | null | undefined;
|
||||
}
|
||||
export interface returnPriceArgs {
|
||||
goods: BaseCartItem;
|
||||
selCoupon: Coupon[];
|
||||
user: ShopUserInfo;
|
||||
shopInfo: ShopInfo;
|
||||
shopUserInfo: ShopUserInfo;
|
||||
limitTimeDiscountRes?: TimeLimitDiscountConfig | null | undefined;
|
||||
idKey?: keyof BaseCartItem;
|
||||
}
|
||||
|
||||
|
||||
export interface CanReturnMemberPriceArgs {
|
||||
shopInfo?: ShopInfo;
|
||||
shopUserInfo: ShopUserInfo;
|
||||
}
|
||||
@@ -9,14 +9,7 @@ import * as UTILS from "@/utils/coupon-utils.js";
|
||||
import { BigNumber } from "bignumber.js";
|
||||
import _ from "lodash";
|
||||
// 导入工具库及相关类型
|
||||
// import {
|
||||
// OrderPriceCalculator,
|
||||
// BaseCartItem,
|
||||
// BackendCoupon,
|
||||
// ActivityConfig,
|
||||
// OrderExtraConfig, MerchantReductionConfig, MerchantReductionType,
|
||||
// GoodsType, FullReductionActivity
|
||||
// } from "@/utils/goods";
|
||||
|
||||
import {
|
||||
OrderPriceCalculator, limitUtils,
|
||||
BaseCartItem,
|
||||
@@ -25,6 +18,17 @@ import {
|
||||
OrderExtraConfig, MerchantReductionConfig, MerchantReductionType,
|
||||
GoodsType, FullReductionActivity
|
||||
} from "ysk-utils";
|
||||
|
||||
// import {
|
||||
// OrderPriceCalculator, limitUtils,
|
||||
// BaseCartItem,
|
||||
// BackendCoupon,
|
||||
// ActivityConfig,
|
||||
// OrderExtraConfig, MerchantReductionConfig, MerchantReductionType,
|
||||
// GoodsType, FullReductionActivity
|
||||
// } from '@/lib/index'
|
||||
|
||||
|
||||
console.log('OrderPriceCalculator', OrderPriceCalculator); // 不报错,能打印出类/对象
|
||||
// import yskUtils from 'ysk-utils';
|
||||
// const OrderPriceCalculator = yskUtils.OrderPriceCalculator
|
||||
|
||||
Reference in New Issue
Block a user