news 2026/7/1 8:34:01

CRMEB Pro 订单源码解析:购物车结算、优惠分摊、库存预占到底怎么串?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CRMEB Pro 订单源码解析:购物车结算、优惠分摊、库存预占到底怎么串?

## 摘要

很多人做订单二开,第一反应是“把总价算出来就行了”。但 CRMEB Pro 的真实链路里,下单前要先把购物车里的商品整合成可结算数据,再根据用户等级、付费会员、渠道身份、活动商品、首单优惠、优惠券、积分、运费模板重新算一遍,最后才进入库存扣减和订单落库。

真正容易出问题的,不是单个计算函数,而是这些规则被拆散后顺序错了:先扣库存再校验优惠,先算优惠再补活动上下文,先取缓存再忽略地址变化,或者把活动商品和普通商品混在一个购物车里算。结果就是金额偏差、库存错扣、优惠券误用、活动商品被错误叠加。

本文基于 CRMEB Pro 当前项目真实实现,拆开购物车整合、优惠计算、库存校验和订单创建前的关键动作,看看下单前到底是哪一步在兜底,为什么这些步骤不能省。

本文涉及的真实目录:

```text
app/services/order/StoreCartServices.php
app/services/order/StoreOrderComputedServices.php
app/services/order/StoreOrderCreateServices.php
app/services/order/StoreOrderServices.php
app/services/activity/coupon/StoreCouponUserServices.php
app/services/product/product/StoreProductServices.php
app/services/user/level/SystemUserLevelServices.php
app/services/user/member/MemberCardServices.php
app/services/activity/seckill/StoreSeckillServices.php
app/services/activity/combination/StoreCombinationServices.php
app/services/activity/bargain/StoreBargainServices.php
app/services/activity/integral/StoreIntegralServices.php
app/services/activity/live/LiveRoomProductServices.php
```

## 一、先把购物车整合成“能结算的数据”

下单前,CRMEB Pro 不直接拿购物车表开算,而是先把购物车商品整合成统一结构。入口在:

```php
public function getUserProductCartListV1(int $uid, $cartIds, bool $new, array $addr = [], int $shipping_type = 1, int $coupon_id = 0, bool $isCart = false, int $isSendGift = 0)
```

这一步会先处理三件很关键的事:

```text
1. 补齐直播间、活动关系字段。
2. 检查购物车里是不是混了不能一起结算的商品。
3. 把商品价格、会员价、活动价、运费先整成统一结构。
```

比如直播商品不能和普通商品混算,代码里直接拦:

```php
if (!in_array(21, $types, true)) {
return;
}
if (count($types) > 1) {
throw new ValidateException('直播商品不能和普通商品一起结算');
}
```

这类限制很重要。因为一旦类型混了,后面的优惠券适用范围、运费模板、活动库存都会被串乱。

## 二、购物车里真正能结算的,是 `valid` 不是原始列表

`handleCartList()` 会把购物车原始数据加工成两组:

```text
valid
可以继续结算的商品。

invalid
不能结算、要提示用户处理的商品。
```

在这一步里,商品会被补齐这些字段:

```text
truePrice
vip_truePrice
price_type
channel_price
costPrice
trueStock
sum_price
```

然后根据用户身份和商品规则重新算价:

```php
[$truePrice, $vip_truePrice, $type] = $productServices->setLevelPrice(...)
$item['truePrice'] = $truePrice;
$item['vip_truePrice'] = $vip_truePrice;
$item['price_type'] = $type;
```

如果是渠道商,还会再走一层渠道价:

```php
[$truePrice, $channelPrice] = $productServices->setChannelPrice(...)
$item['truePrice'] = $truePrice;
$item['channel_price'] = $channelPrice;
$item['price_type'] = 'channel';
```

这说明购物车阶段并不只是“把商品列出来”,而是在帮后面的价格计算准备统一口径。

## 三、优惠券不是想用就能用,要按当前购物车重新判定

优惠券计算在 `computedProductPromotion()` 和 `useCouponId()` 里,先检查当前购物车和商品是否符合券的使用条件,再计算实际抵扣金额。

例如按商品分类、品牌或商品范围判断:

```php
switch ($type) {
case 0://全场券
case 1://品类券
case 2://商品券
case 3://品牌券
}
```

然后再判断门槛:

```php
if (!$count || $couponInfo['use_min_price'] > $price) {
return 0;
}
```

这里最容易踩坑的是:

```text
前端显示能用,不代表当前购物车还能用。
```

因为商品数量变化、活动叠加变化、地址变化后,优惠券门槛和可叠加范围可能已经变了。所以订单确认时必须重新算,而不是沿用旧的券状态。

## 四、订单价格组里,优惠、积分、邮费是一个整体

`StoreOrderComputedServices::getOrderPriceGroup()` 会把这几个核心量一起算:

```text
sumPrice
totalPrice
costPrice
totalIntegral
vipPrice
levelPrice
memberPrice
changePrice
channelPrice
storePostage
storePostageDiscount
```

这里有个容易误解的点:

```text
vipPrice 不是最终支付价,
它只是会员价和等级价相关的优惠统计口径。
```

真正的支付价是先算商品,再算优惠券,再算首单优惠,再算积分,最后加运费:

```php
if ($couponPrice < $payPrice) {
$payPrice = bcsub((string)$payPrice, (string)$couponPrice, 2);
}
if ($firstOrderPrice < $payPrice) {
$payPrice = bcsub((string)$payPrice, (string)$firstOrderPrice, 2);
}
[$payPrice, $deductionPrice, $usedIntegral, $SurplusIntegral] = $this->useIntegral(...);
$payPrice = (float)bcadd((string)$payPrice, (string)$payPostage, 2);
```

所以如果你二开新增一个“员工补贴”“渠道补贴”“团长补贴”,不要单独往前端加字段了事,必须放进这个价格组里统一参与计算。

## 五、库存不是提交订单时随便扣一下,而是按商品类型分流扣减

订单创建后,`decGoodsStock()` 会按商品类型走不同库存处理逻辑:

```php
switch ($type) {
case 0://普通
case 6://预售
case 8://抽奖
case 1://秒杀
case 2://砍价
case 3://拼团
case 4://积分
case 5://套餐
case 7://新人专享
case 21://直播
}
```

普通商品走商品库存扣减:

```php
$res5 = $res5 && $services->decProductStock($cart_num, (int)$cart['productInfo']['id'], $unique);
```

活动商品则走各自的活动库存服务:

```php
$res5 = $res5 && $seckillServices->decSeckillStock(...);
$res5 = $res5 && $pinkServices->decCombinationStock(...);
$res5 = $res5 && $storeIntegralServices->decIntegralStock(...);
```

这说明库存预占不是一个统一减库存函数硬扛,而是要按活动类型分流。否则你会把拼团库存和普通库存混在一起,后面售后和回滚根本对不上。

## 六、为什么要先算再扣:顺序错了,整单就会翻车

这一条很值钱:

```text
下单流程不是“先扣库存再看价格”,而是先统一结算,再在事务里扣库存、抵积分、写订单快照。
```

订单创建里实际顺序大致是:

```php
$order = $this->dao->save($orderInfo);
$couponServices->useCoupon(...);
$this->deductIntegral(...);
$this->decGoodsStock(...);
$cartServices->setCartInfo(...);
```

如果你把顺序乱改成:

```text
先扣库存
再判断优惠券
再算积分
```

那只要中间有一个校验失败,就会出现库存已经扣了、订单没建成、优惠券状态也不对的事故。

## 七、订单商品明细要保存快照,不要只存 id

`StoreOrderCartInfoServices::setCartInfo()` 会把购物车商品直接序列化成订单商品快照:

```php
'cart_info' => json_encode($cart),
'cart_num' => $cart['cart_num'],
'total_price' => $cart['total_price'] ?? 0,
'pay_price' => $cart['pay_price'] ?? 0,
'pay_postage' => $cart['postage_price'] ?? 0,
'coupon_price' => $cart['coupon_price'] ?? 0,
'promotions_price' => $cart['sum_promotions_price'] ?? 0,
'first_order_price' => $cart['first_order_price'] ?? 0,
```

这就是为什么历史订单能回看当时的商品名、SKU、优惠、运费和赠品信息。你如果只存商品 id,后面商品名改了、规格改了、活动结束了,订单详情页就会失真。

## 八、二开时最该守住的边界

如果你要改订单链路,优先守住这几个边界:

```text
1. 购物车只做整合,不直接承担最终结算责任。
2. 优惠券、积分、首单优惠必须在后端重算。
3. 库存扣减必须和商品类型绑定,不能一把梭。
4. 订单主表和订单商品表都要留快照。
5. 事务内先落单,再扣减和写明细,别乱换顺序。
```

## 注意事项

1. 订单、库存、优惠券、积分都属于高风险业务,改动前先确认影响范围。
2. 直播、拼团、秒杀、积分、套餐等活动商品不要和普通商品混用结算逻辑。
3. 不要把活动库存当普通库存扣减。
4. 订单商品快照一旦落库,后续不要反向污染历史单据。
5. 任何金额计算都优先用项目现有的 `bc*` 方式。

## 标签建议

#CRMEBPro #订单二开 #购物车 #优惠券 #库存预占 #源码解析 #价格分摊 #事务 #二开实战 #商城系统

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/1 8:31:12

sql语法 - WITH, ROW_NUMBER, 经典用法

全局编号&#xff08;不分组&#xff09; ROW_NUMBER() 是 SQL 中一个非常重要的窗口函数&#xff08;Window Function&#xff09;&#xff0c;用于为查询结果集中的每一行生成唯一的、连续的整数序号&#xff08;从 1 开始&#xff09;。 ROW_NUMBER() OVER ([PARTITION BY 分…

作者头像 李华
网站建设 2026/7/1 8:31:03

如何轻松重置JetBrains IDE试用期:专业开发者的完整解决方案

如何轻松重置JetBrains IDE试用期&#xff1a;专业开发者的完整解决方案 【免费下载链接】ide-eval-resetter 项目地址: https://gitcode.com/gh_mirrors/id/ide-eval-resetter JetBrains IDE试用期重置工具&#xff08;ide-eval-resetter&#xff09;是一款专门为开发…

作者头像 李华
网站建设 2026/7/1 8:29:10

Platinum-MD:终极免费工具,让经典MiniDisc重获新生

Platinum-MD&#xff1a;终极免费工具&#xff0c;让经典MiniDisc重获新生 【免费下载链接】platinum-md Minidisc NetMD Conversion and Upload 项目地址: https://gitcode.com/gh_mirrors/pl/platinum-md MiniDisc无损音乐管理工具Platinum-MD为音乐爱好者带来了革命性…

作者头像 李华
网站建设 2026/7/1 8:28:43

MATLAB 2022a实战:手把手教你搭建MSK通信链路并对比三种解调器性能

MATLAB 2022a实战&#xff1a;从零构建MSK通信链路与解调器性能深度评测引言&#xff1a;为什么选择MSK调制&#xff1f;在数字通信领域&#xff0c;调制技术决定了信号如何在有限带宽内高效传输。MSK&#xff08;最小频移键控&#xff09;作为一种特殊的连续相位频移键控&…

作者头像 李华