current_coupon_price 全链路(超详细版)
1. 业务定义(一句话)
⚡⚡⚡ 买家输入券码,系统校验券的可用性(有效期/次数/范围/门槛)后计算抵扣额,以负数写入 current_coupon_price;若券配置为”替换活动”模式,同时清空活动优惠。
2. 后台配置到前台计算的关系
2.1 商家后台可控项(配置映射表)
| 后台配置项 | 典型存储/来源 | 前台读取点 | 对计算的直接影响 |
|---|---|---|---|
| 券码(coupon_code) | o_coupon.coupon_code | getCouponPlan | 券的唯一索引 |
| 券名(coupon_name) | o_coupon.coupon_name | getCouponPlan | 仅展示 |
| 适用范围(全场/指定商品/指定专辑) | o_coupon.product_range + coupon_range 表 | getCouponPlan | 决定哪些商品参与计算 |
| 使用条件(满件/满额) | o_coupon.param.condition.type/value | getCouponPlan | 门槛不满足则券不可用 |
| 优惠类型(折扣率/固定金额) | o_coupon.param.discount.type/value | getCouponPlan | 折扣券 vs 固定券公式分支 |
| 与活动关系 | o_coupon.use_with_promotion | checkCouponUseWithPromotionStatus | 0=不叠加/1=叠加/2=替换活动 |
| 使用次数限制 | o_coupon.usage_limit | getCouponPlan | 次数耗尽则不可用 |
| 每人限用一次 | o_coupon.once_per_customer | checkCouponHandler | 邮箱维度校验 |
| 券有效期 | o_coupon.starts_at/ends_at | getCouponPlan | 时间外不可用 |
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 三种策略
| 配置值 | 常量名 | 含义 | 对活动的影响 |
|---|---|---|---|
| 0 | NOT_USE_WITH_PROMOTION | 不叠加 | 券适用商品从活动范围中剔除,券与活动作用于不同商品 |
| 1 | USE_WITH_PROMOTION | 叠加 | 券与活动同时生效,可同时享用 |
| 2 | REPLACE_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_ids 与 coupon_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_price | float | 总优惠金额(负数) | -20.00 |
cart.coupon_price_currency | float | 换算主货币后的券优惠(负数) | -20.00 |
cart.coupon_code | string | 券码 | SAVE20 |
cart.coupon_id | int | 券ID | 103 |
cart.customer_email | string | 使用券的顾客邮箱 | a@b.com |
cart.promotion | array | 活动列表(被替换时为空) | [] |
cart.promotion_price | float | 活动优惠(被替换时为0) | 0 |
cart.disable_promotion_update | bool | 是否禁止后续重算活动 | 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 |