current_coupon_price 全链路(超详细版)

1. 业务定义(一句话)

⚡⚡⚡ 买家输入券码,系统校验券的可用性(有效期/次数/范围/门槛)后计算抵扣额,以负数写入 current_coupon_price;若券配置为”替换活动”模式,同时清空活动优惠。

2. 后台配置到前台计算的关系

2.1 商家后台可控项(配置映射表)

后台配置项典型存储/来源前台读取点对计算的直接影响
券码(coupon_code)o_coupon.coupon_codegetCouponPlan券的唯一索引
券名(coupon_name)o_coupon.coupon_namegetCouponPlan仅展示
适用范围(全场/指定商品/指定专辑)o_coupon.product_range + coupon_rangegetCouponPlan决定哪些商品参与计算
使用条件(满件/满额)o_coupon.param.condition.type/valuegetCouponPlan门槛不满足则券不可用
优惠类型(折扣率/固定金额)o_coupon.param.discount.type/valuegetCouponPlan折扣券 vs 固定券公式分支
与活动关系o_coupon.use_with_promotioncheckCouponUseWithPromotionStatus0=不叠加/1=叠加/2=替换活动
使用次数限制o_coupon.usage_limitgetCouponPlan次数耗尽则不可用
每人限用一次o_coupon.once_per_customercheckCouponHandler邮箱维度校验
券有效期o_coupon.starts_at/ends_atgetCouponPlan时间外不可用

2.2 全链路时序图(配置 → 计算 → 落库)

flowchart TD
A[商家配置优惠券] --> B[CouponService.getCouponPlan]
C[买家输入 coupon_code] --> B
D[购物车商品/价格/件数] --> B
B --> E{可用性校验:有效期/次数/范围/门槛}
E -->|不可用| Z[返回错误 msg,不写 coupon_price]
E -->|可用| F[计算 coupon_price]
F --> G{use_with_promotion?}
G -->|0 不叠加| H[活动优惠商品中剔除券适用商品后再算活动]
G -->|1 叠加| I[活动与券并行,两者都生效]
G -->|2 替换活动| J[清空 promotion_price,清零活动]
H --> K["写 current_coupon_price(负数)"]
I --> K
J --> K
K --> L[renew 重算 total_price]

3. 核心入口与数据结构

3.1 核心方法链路

CartService.php:554-557
  ├─ calPreCouponPrice()          从 cookie 中取出预存券码,计算券价
  │     └─ useCouponHandler()
  │           ├─ CouponService::getCouponPlan()   校验+计算券抵扣额
  │           └─ CouponService::useCoupon()         写库存/计数(已下单场景)
  └─ checkCouponUseWithPromotionStatus()   处理券与活动的互斥/替换关系
        └─ CouponHandlerService::checkCouponUseWithPromotionStatus()

3.2 券的 use_with_promotion 三种策略

配置值常量名含义对活动的影响
0NOT_USE_WITH_PROMOTION不叠加券适用商品从活动范围中剔除,券与活动作用于不同商品
1USE_WITH_PROMOTION叠加券与活动同时生效,可同时享用
2REPLACE_WITH_PROMOTION替换活动清空 promotion_price,券抵扣额取代活动优惠

⚠️ 注意:策略 2(替换活动)只在 action != 'useCoupon' 时生效(防止用户使用券时误判活动失效);action == 'useCoupon' 时活动优惠保持原样。

4. 完整计算逻辑

4.1 step1:券基础校验(getCouponPlan)

$couponInfo = (new CouponModel())->getCouponInfoByCode($this->storeId, $couponCode);
// 1. 券是否存在
if ($couponInfo->isEmpty()) {
    return ['code' => -1, 'msg' => 'coupon code not found'];
}
// 2. 次数是否耗尽
if ($couponInfo->usage_limit == 0) {
    return ['code' => -1, 'msg' => 'coupon used up'];
}

4.2 step2:适用范围过滤

根据 product_range 分三种情况:

product_range含义过滤逻辑
0 (PRODUCT_RANGE_ALL)全场所有商品参与
1 (PRODUCT_RANGE_NOT_ALL)指定商品只保留 coupon_range.product_id 与购物车商品ID交集
2 (PRODUCT_RANGE_COLLECTION)指定专辑商品 collection_idscoupon_range.collection_id 取交集

4.3 step3:门槛校验

// 门槛条件 type
if ($rule['condition']['type'] == COUPON_DISCOUNT_TYPE_NUM) {
    // 满件门槛
    if ($totalNum < $rule['condition']['value']) {
        return ['code' => -1, 'msg' => '不满足件数条件'];
    }
} else if ($rule['condition']['type'] == COUPON_DISCOUNT_TYPE_AMOUNT) {
    // 满额门槛
    if ($totalPrice < $rule['condition']['value']) {
        return ['code' => -1, 'msg' => '不满足金额条件'];
    }
    // 最高金额限制(max_value)
    if ($totalPrice > $rule['condition']['max_value']) {
        return ['code' => -1, 'msg' => '超过最高金额限制'];
    }
}

4.4 step4:计算抵扣额

// 折扣券:总金额 × 折扣率
if ($discountType == COUPON_DISCOUNT_OFF) {
    $return['price'] = ($totalPrice * floatval($rule['discount']['value'])) / 100;
}
// 固定金额券:面额与总金额取 min(不能超商品总价)
} else if ($discountType == COUPON_DISCOUNT_AMOUNT) {
    $return['price'] = min($rule['discount']['value'], $totalPrice);
}

公式对照

折扣类型计算公式示例(总金额=100)
折扣券(type=1)totalPrice × 折扣率8折券 → 100 × 0.8 = 80 优惠
固定金额券(type=2)min(面额, totalPrice)20元券 → min(20, 100) = 20 优惠

4.5 step5:写入 cart 字段

// CouponHandlerService.php:166-170
$cart['coupon_price']           = $couponPrice * -1;  // 转负数
$cart['coupon_price_currency']  = currencyExchange($couponPrice * -1, $cart['currency']);
$cart['coupon_code']            = $preCouponCode;
$cart['coupon_id']              = $couponInfo['id'] ?? 0;
$cart['customer_email']         = $preCustomerEmail ?? '';

⚡⚡⚡ 最终 coupon_price负数(优惠为正,显示时取绝对值)。

4.6 step6:替换活动逻辑(checkCouponUseWithPromotionStatus)

当券 use_with_promotion == REPLACE_WITH_PROMOTION(值=2)时:

if ($couponInfo['use_with_promotion'] == CouponModel::REPLACE_WITH_PROMOTION) {
    $action      = $this->request->action();
    $controller  = $this->request->controller();
 
    if (($action != 'useCoupon') || ($controller == 'OrderSinglePage')) {
        $cart['promotion']                 = [];        // 清空活动列表
        $cart['promotion_price']           = 0;         // 活动优惠清零
        $cart['promotion_price_currency']  = 0;
        $cart['disable_promotion_update']   = true;      // 阻止后续重新计算活动
 
        $orderId = $cart['order']['id'] ?? 0;
        if ($orderId) {
            (new OrderModel())->resetCurrentPromotionPrice($orderId); // 落库也清零
        }
    }
 
    if ($action == 'useCoupon') {
        $cart['tag_refresh_promotion_price'] = false; // useCoupon 时活动保持原样
    }
}

⚠️ 重要边界:在用户使用券的操作(useCoupon)中,故意不触发替换逻辑——这是为了避免误报”活动已失效”。活动优惠在 useCoupon 期间保持原样,仅在后续展示页面时才显示替换效果。

5. 券与活动的叠加/互斥/替换关系详解

5.1 use_with_promotion = 0(不叠加)

券与活动作用于不同商品

// 从活动商品中剔除券适用商品
if ($coupon['use_with_promotion'] == NOT_USE_WITH_PROMOTION) {
    $paramTwo = [];
    foreach ($param as $v) {
        if (!in_array($v['product_id'], $coupon['coupon_ids'])) {
            $paramTwo[] = $v;  // 只保留不在券范围内的商品
        }
    }
    // 用剔除后的商品列表重新算活动优惠
    $return[] = (new PromotionService())->getPromotionPlan($paramTwo);
}

示例:商品A参与活动,商品B参与券(不叠加)→ 商品A享活动,商品B享券抵扣

5.2 use_with_promotion = 1(叠加)

// 券和活动各自独立计算,最终合并
$return[] = $coupon;   // 券优惠
$return[] = (new PromotionService())->getPromotionPlan($param); // 活动优惠

示例:活动减30 + 券减20 → 总优惠50

5.3 use_with_promotion = 2(替换活动)

活动优惠被清零,只剩券抵扣:

示例:活动减30,券减20且配置为替换活动 → 活动清零,券减20(但实付比活动还少——需运营端注意)

6. 字段输出结构

字段类型含义示例
cart.coupon_pricefloat总优惠金额(负数)-20.00
cart.coupon_price_currencyfloat换算主货币后的券优惠(负数)-20.00
cart.coupon_codestring券码SAVE20
cart.coupon_idint券ID103
cart.customer_emailstring使用券的顾客邮箱a@b.com
cart.promotionarray活动列表(被替换时为空)[]
cart.promotion_pricefloat活动优惠(被替换时为0)0
cart.disable_promotion_updatebool是否禁止后续重算活动true

7. 边界场景说明

7.1 无券码或券码为空

  • calPreCouponPrice 直接返回原 cart,不写任何 coupon 字段
  • coupon_price 保持上一次计算的值(或0)

7.2 券面额大于商品总价

固定金额券:min(face_value, totalPrice) → 优惠不超过商品总价

7.3 券范围商品不在购物车

  • getCouponPlan 计算 couponIds 时,只取购物车中与券范围有交集的商品
  • 若交集为空,返回 code=-1 + “没有优惠商品” msg

7.4 券门槛设置了最高金额限制

若商品总价超过 max_value,视为不满足门槛,券不可用(见 4.3 step3)

7.5 使用替换型券后再使用普通券

  • checkCouponUseWithPromotionStatus 在每次 cart 刷新时都会执行
  • 若新券仍为替换型,再次清空活动;若是叠加型或不叠加型,活动在 renew 重算时恢复

7.6 已下单订单换券

通过 OrderService::renew 重算时,checkCouponUseWithPromotionStatus 再次执行,确保落库值一致

8. 与总价 total_price 的关系

total_price 的组成(9字段):
  subtotal_price        = Σ(final_line_price)          商品行小计
  discount_price        = 整单折扣(负数)
  refund_price          = 退款口径独立
  current_total_price   = subtotal + shipping           仅展示
  total_price           = subtotal - promotion - coupon + shipping + tax + tip + payment + insurance
                        └─ coupon_price 在此处被扣减(负数)

⚡⚡⚡ coupon_price 是负数,在 total_price 公式中做加法(减去负数即增加)。

9. 关键代码索引

逻辑文件:行号
入口:CartService 调用券计算CartService.php:554-557
预存券码计算(cookie 取码)CouponHandlerService.php:142-174
券使用主逻辑(含订单/购物车分支)CouponHandlerService.php:26-115
券基础校验(有效期/次数/范围/门槛/抵扣额)CouponService.php:819-970
券与活动关系处理(替换/叠加/不叠加)CouponHandlerService.php:567-619
不叠加时活动商品过滤CouponService.php:793-806
券类型常量定义CouponModel.php:17-23