current_promotion_price 全链路(超详细版)
1. 业务定义(一句话)
⚡⚡⚡ 购物车商品匹配商家后台活动规则,计算出总优惠金额,按商品价格占比均摊到每个商品行。
2. 后台配置到前台计算的关系
2.1 商家后台可控项(配置映射表)
| 后台配置项 | 典型存储/来源 | 前台读取点 | 对计算的直接影响 |
|---|---|---|---|
| 活动类型(满减/满件打折/第X件Y折/满件一口价/满X免Y) | promotion.type | getCartPromotion | 决定调用哪个计算函数 |
| 活动时间(起止时间) | promotion.starts_at/ends_at | getCartPromotion | 时间外活动不参与计算 |
| 商品范围(全场/指定商品/指定专辑) | promotion.product_range + promotion_range 表 | calGeneralDiscount | 决定哪些商品能匹配此活动 |
| 活动规则(满XX元减YY/满XX件YY折等) | promotion.rule_param['rule'] | discountPrice | 满额满件门槛与折扣值 |
| 封顶配置(单次/上不封顶) | promotion.rule_param['allocation_limit'] | discountPrice | 上不封顶时可叠加多次 |
| 互斥商品(一个商品只能属一个活动) | PromotionHandlerService::unsetProductsByMutexVariantUniqKey | getCartPromotion | 同一商品被先命中活动排除后命中活动 |
| 满件一口价排序方式(价格从低到高/高到低) | rule_param['promotion_xy_product_price_sort'] | calSinceDiscount | 第X件Y折中商品顺序决定哪些商品先享受折扣 |
| 会员价优先(minmaxoffer) | cart['has_maxoffer_discount'] + cart['has_maxoffer'] | handlerOrderSurchrge | 若会员折扣比活动优惠更大,以会员价为准 |
2.2 全链路时序图(配置 → 计算 → 落库)
flowchart TD A[商家配置活动规则] --> B[PromotionService.getAllCartPromotionByCache 获取所有活动] C[购物车 items] --> D[unsetProductsByMutexVariantUniqKey 过滤互斥商品] D --> B B --> E{"遍历每个活动:时间是否在有效期?"} E -->|否| Z[跳过] E -->|是| F[getCartPromotionRange 获取活动商品范围] F --> G{活动类型判断} G -->|第X件Y折| H[calSinceDiscount] G -->|满件一口价| I[calFixedPriceDiscount] G -->|满X免Y| J[calBuyXFeeY] G -->|其他活动| K[calGeneralDiscount] H --> L[handlerOrderSurchrge 处理会员价优先] I --> L J --> L K --> L L --> M["$return['promotion_price'] = Σdiscount + Σdiy_offers.discount"] M --> N[写入 cart.promotion_price]
3. 核心入口与数据结构
3.1 核心方法链路
CartService.php:550
└─ PromotionHandlerService::getCartPromotion($cart)
│
├─ step1: getAllCartPromotionByCache() 获取所有在有效期活动
├─ step2: 构建 not_ranges(互斥过滤) 同一商品只能用于一个活动
├─ step3: 按类型分发计算
│ ├─ calSinceDiscount() type=5 第X件Y折
│ ├─ calFixedPriceDiscount() type=6 满件一口价
│ ├─ calBuyXFeeY() type=7 满X免Y
│ └─ calGeneralDiscount() type=1/2/3/4 其他全场或部分活动
└─ handlerOrderSurchrge() 会员价优先兜底逻辑
3.2 活动类型 → 计算方法映射
| type 常量值 | 含义 | 计算方法 | 计费基数 |
|---|---|---|---|
| 1 | 满额减元(满XX元减YY元) | calGeneralDiscount → discountPrice | 商品总价(unit_price × quantity) |
| 2 | 上不封顶(满XX件YY折,可叠加) | calGeneralDiscount → discountPrice | 商品总价 |
| 3 | 满件减元(满XX件减YY元) | calGeneralDiscount → discountPrice | 商品总件数 |
| 4 | 满额打折(满XX元YY折) | calGeneralDiscount → discountPrice | 商品总价 |
| 5 | 第X件Y折 | calSinceDiscount | 按商品件数逐层匹配阶梯 |
| 6 | 满件一口价 | calFixedPriceDiscount | 找满足件数的最低价商品组合 |
| 7 | 满X免Y | calBuyXFeeY | 按商品件数计算可免数量 |
4. 完整计算逻辑
4.1 step1:获取活动列表
$allPromotion = (new PromotionService())->getAllCartPromotionByCache();
foreach ($allPromotion as $promotion) {
if ($startTime <= time() && $endTime >= time()) {
$promotion['ranges'] = (new PromotionService())
->getCartPromotionRangeByCache($promotion['id']);
$promotionList[] = $promotion;
}
}关键行为:
- 只取在有效期的活动(
starts_at ≤ now ≤ ends_at) - 无限期活动(
ends_at = -1)视为永久有效 - 同时读取每个活动的商品范围(
ranges,含product_id和collection_id)
4.2 step2:互斥过滤(同一商品只属一个活动)
// 按 product_range_sort 升序:指定商品 > 指定专辑 > 全场
array_multisort(array_column($promotionList, 'product_range_sort'), SORT_ASC);
// 遍历构建 not_ranges
foreach ($promotionList as $key => $item) {
$item['not_ranges']['product'] = array_filter($productIds); //已被占用的商品ID
$item['not_ranges']['collection'] = array_filter($collectionIds); //已被占用的专辑ID
// 若商品A已在 $productIds 中,则在当前活动的 not_ranges 中标记
}优先级:指定商品活动 > 指定专辑活动 > 全场活动
即:商品A 先被「指定商品A活动」占用后,不会再参与「指定专辑X活动」或「全场活动」。
4.3 step3:按类型分发计算
4.3.1 calGeneralDiscount(type 1/2/3/4)
适用:满额减元、满件打折、满额打折、上不封顶
第一步:过滤商品范围
根据 product_range 分三种情况:
| product_range | 含义 | 过滤逻辑 |
|---|---|---|
0 (STATUS_ALL) | 全场 | 排除已被其他活动占用的商品/专辑 |
1 (PRODUCT_RANGE_NOT_ALL) | 指定商品 | 只保留在 ranges 中且不在 not_ranges 中的商品 |
2 (PRODUCT_RANGE_COLLECTION) | 指定专辑 | 只保留商品 collection_ids 与 ranges.collection_id 有交集的商品 |
第二步:计算优惠金额
调用 PromotionService::discountPrice(),规则:
// 遍历配置的阶梯规则,找满足条件的最高阶梯
foreach ($ruleParam['rule'] as $val) {
$condition = $val['ge']; // 门槛值
$discountValue = $val['value']; // 折扣值
// 满额类:用总价判断
if (in_array($type, [满额减元, 满额打折])) {
if ($cartTotal < $condition) break;
}
// 满件类:用总件数判断
if (in_array($type, [满件打折, 满件减元])) {
if ($cartCount < $condition) break;
}
if ($allocationLimit == 上不封顶) {
// 可叠加次数 = floor(总价或总件数 / 门槛)
$discount = 折扣值 × floor(基数 / 门槛);
} else {
// 单次:取当前阶梯的折扣值
$discount = 折扣值; // 或 总价 × 折扣比例
}
}满额减元 type=1 示例:满100减10,上不封顶,买200元 → 减20
满件打折 type=4 示例:满3件8折,买5件 → 80% × 总价
第三步:均摊到商品
$discount = $discount * -1; // 转为负数
foreach ($productList as $key => $product) {
$productPrice = $product['unit_price'] * $product['quantity'];
$product['promotion_price'] = $productPrice / $price * $discount * -1;
// 即:商品优惠 = (商品行原价 / 购物车总价) × 总优惠
$productList[$key] = $product;
}
$promotion['discount'] = $discount * -1; // 负数⚠️ 注意:若 isOriginalPrice = true(满足会员价优先条件),计费基数用 original_price 而非 unit_price。
4.3.2 calSinceDiscount(type=5 第X件Y折)
特点:按商品件数逐层匹配阶梯折扣,每层可独立生效。
两种模式:
模式A:每层都生效(topPromotion = 0,默认)
$productInc = 0;
foreach ($productList as $key => $product) {
for ($i = 0; $i < $quantity; $i++) {
$product['quantity'] = 1;
$productInc += 1;
foreach ($ruleParams['rule'] as $rule) {
if ($productInc >= $condition && !in_array($condition, $hasHandlerCondition)) {
$discountPrice = ($product['unit_price'] * $value / 100) * -1;
$product['promotion_price'] += $discountPrice; // 叠加
$promotion['discount'] += $discountPrice; // 累加
$hasHandlerCondition[] = $condition; // 该层已处理
}
}
}
}示例:活动「满3件8折、满5件7折」,购买5件,按价格从低到高排:
- 件1-3:8折优惠(每件都享)
- 件4-5:7折优惠(替换3件的8折?不,每件独立叠加)
模式B:顶层生效(topPromotion = 1)
$productCount = Σ(quantity);
foreach ($ruleParams['rule'] as $inc => $rule) {
if ($condition <= $productCount && $nextCondition > $productCount) {
$promotion['discount'] = ($商品总价 × 折扣值 / 100) * -1;
// 只有最顶层折扣生效
}
}⚠️ 排序控制:通过 ruleParam['promotion_xy_product_price_sort'] 配置 asc(低到高)或 desc(高到低),决定哪些商品先被计算。
4.3.3 calFixedPriceDiscount(type=6 满件一口价)
核心思想:从购物车中选出一组商品,以固定总价成交。
// 商品按价格升序排列(便宜的先选)
// 遍历阶梯规则,找满足件数的最低价组合
foreach ($ruleParams['rule'] as $rule) {
if ($totalQuantity >= $rule['ge']) {
// 取 $rule['ge'] 个商品,检查是否比一口价高
$productPricesSlice = array_slice($productPrices, 0, $rule['ge']);
if (array_sum($productPricesSlice) > $ruleFixedPrice) {
$totalDiscount += (原价和 - 一口价);
if (allocationLimit != 上不封顶) break; // 单次只取最低组合
}
}
}示例:满3件一口价¥30,购物车有4件:价格[10, 15, 20, 25]
- 取前3件(10+15+20=45)→ 优惠45-30=15
上不封顶时:取完一组后,剩余商品继续匹配下一阶梯
4.3.4 calBuyXFeeY(type=7 满X免Y)
核心思想:买够X件,最便宜的Y件免单。
// 商品按价格升序(便宜的先免)
$totalFreeNumber = intval($discountValue * floor($totalQuantity / $condition));
// 循环:从低到高逐商品累加免单数量
foreach ($productList as $key => $product) {
$currentProductFreeNumber = min($quantity, $needFreeNumber);
$promotionPrice = -1 * ($product['price'] * $currentProductFreeNumber);
$productList[$key]['promotion_price'] = $promotionPrice;
}示例:满3件免1件,购买4件:价格[10, 15, 20, 25]
- 免单数量 = 1 × floor(4/3) = 1
- 最便宜的1件(10元)免单 → 优惠 -10
4.4 handlerOrderSurchrge(会员价优先兜底)
当同时存在会员折扣(has_maxoffer)和「第X件Y折」活动(type=5)时:
if ($isMinmaxoffer && $max > 0) {
// 1. 把所有活动优惠 discount 清零
foreach ($promotionList as $key => &$value) {
$value['discount'] = 0;
}
// 2. 以会员折扣优先:取 max(原价 - 上限价, 活动优惠最大值)
$firstTypeKey = array_search(type=5, array_column($promotionList, 'type'));
$promotionList[$firstTypeKey]['discount'] = max(($originalPrice - $max), max($discounts)) * -1;
// 3. 商品单价恢复为 original_price(会员价)
foreach ($cart['items'] as $key => $item) {
$cart['items'][$key]['price'] = $item['original_price'];
$cart['items'][$key]['unit_price'] = $item['original_price'];
$cart['items'][$key]['final_price'] = $item['original_price'];
$cart['items'][$key]['final_line_price'] = $item['original_line_price'];
}
}触发条件:会员折扣比活动优惠更大时,用会员价替换活动价。
5. 字段输出结构
5.1 cart.promotion_price 计算
// CartService.php:552
$return['promotion_price'] = price_format(
array_sum(array_column($promotion, 'discount'))
+ array_sum(array_column($return['diy_offers'] ?? [], 'discount'))
);promotion为数组,每个活动一项,含discount(负数)diy_offers为自定义活动优惠(由 DiyOfferService 计算)- 最终
promotion_price为负数(优惠为正数时用绝对值)
5.2 cart.items 每个商品行的 promotion_price
每个活动返回 promotion['product_list'],其中每个商品有 promotion_price(负数):
$product['promotion_price']; // 该商品在此活动中的优惠额(负数)同一商品若匹配多个活动,各活动的优惠额会叠加(同一个商品行可能有多个来源的 promotion_price)。
5.3 输出字段一览
| 字段 | 类型 | 含义 | 示例 |
|---|---|---|---|
cart.promotion_price | float | 总优惠金额(负数) | -15.00 |
cart.promotion | array | 活动详情列表 | [{"type":5,"discount":-5,...}] |
cart.promotion[].discount | float | 该活动总优惠(负数) | -10.00 |
cart.promotion[].type | int | 活动类型(1-7) | 5 |
cart.promotion[].product_list | array | 参与此活动的商品列表 | [{product_id:1,promotion_price:-3}] |
cart.promotion[].product_list[].promotion_price | float | 该商品在此活动中的优惠(负数) | -3.00 |
cart.promotion[].possible_products | array | 符合活动条件的商品(未参与互斥过滤前) | - |
cart.promotion[].ranges | array | 活动配置的商品/专辑范围 | [{"product_id":1}] |
cart.promotion[].rule_param | array | 活动规则JSON(解码后) | {"rule":[{"ge":3,"value":10}]} |
6. 优惠互斥与优先级规则
6.1 商品/专辑互斥
- 同一商品不能同时用于多个活动
- 同一专辑不能同时用于多个活动
- 按
product_range_sort升序处理:范围越小优先级越高
6.2 活动类型之间是否互斥?
同类型活动:互斥(通过 not_ranges 实现)
不同类型活动:可叠加,例如:
- 满100减10(type=1)+ 第X件Y折(type=5)可以同时生效
- 满X免Y(type=7)+ 满件一口价(type=6)可同时生效
6.3 与其他价格字段的互斥
| 组合 | 行为 |
|---|---|
| promotion + coupon | promotion 先算,coupon 后算;coupon 的 REPLACE_WITH_PROMOTION 模式会跳过 promotion 直接抵扣 |
| promotion + membership_discount | 会员折扣可能触发 handlerOrderSurchrge 兜底替换 promotion 结果 |
| promotion + diy_offers | 最后合并:promotion_price = Σpromotion.discount + Σdiy_offers.discount |
7. 边界场景说明
7.1 无任何活动
getAllCartPromotionByCache()返回空 →promotion_price = 0- 循环直接跳过,不做任何计算
7.2 活动商品不在购物车
calGeneralDiscount过滤后productList为空 →discount = 0,该活动不参与求和
7.3 互斥导致所有活动失效
- 商品被前面更高优先级活动占满 →
not_ranges覆盖全部商品 → 该活动 discount = 0
7.4 优惠金额大于商品总额
if ($discount > $price) {
$discount = $price; // 优惠不超商品总价
}7.5 满件一口价中商品部分参与
商品A买5件参与满3件一口价:
- 只有前3件参与活动享受一口价,后2件按原价
- 优惠按商品原价占比均摊给参与活动的商品
7.6 第X件Y折 + 上不封顶叠加
买10件,满3件8折、满5件7折(均为上不封顶):
- 每3件享一次8折:3×8折 + 3×8折 + 剩余4件(不满足任何阶梯?或按最后一层?)
- 取决于规则配置和
topPromotion标志
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
└─ promotion_price 在此处被扣减
⚡⚡⚡ promotion_price 是负数,在 total_price 公式中做加法(减去负数即增加)。
9. 关键代码索引
| 逻辑 | 文件:行号 |
|---|---|
| 入口:CartService 调用 | CartService.php:550-552 |
| 入口:PromotionHandlerService.getCartPromotion | PromotionHandlerService.php:17-150 |
| 互斥商品过滤 | PromotionHandlerService.php:573-593 |
| 第X件Y折计算 | PromotionHandlerService.php:158-287 |
| 满件一口价计算 | PromotionHandlerService.php:594-697 |
| 满X免Y计算 | PromotionHandlerService.php:702-812 |
| 全场/部分活动计算 | PromotionHandlerService.php:295-485 |
| 会员价优先兜底 | PromotionHandlerService.php:892-939 |
| 优惠金额计算(discountPrice) | PromotionService.php:957-1003 |
| 活动类型常量定义 | PromotionModel.php:12-18 |