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

📅 2026/7/1 8:34:09
CRMEB Pro 订单源码解析:购物车结算、优惠分摊、库存预占到底怎么串?
## 摘要很多人做订单二开第一反应是“把总价算出来就行了”。但 CRMEB Pro 的真实链路里下单前要先把购物车里的商品整合成可结算数据再根据用户等级、付费会员、渠道身份、活动商品、首单优惠、优惠券、积分、运费模板重新算一遍最后才进入库存扣减和订单落库。真正容易出问题的不是单个计算函数而是这些规则被拆散后顺序错了先扣库存再校验优惠先算优惠再补活动上下文先取缓存再忽略地址变化或者把活动商品和普通商品混在一个购物车里算。结果就是金额偏差、库存错扣、优惠券误用、活动商品被错误叠加。本文基于 CRMEB Pro 当前项目真实实现拆开购物车整合、优惠计算、库存校验和订单创建前的关键动作看看下单前到底是哪一步在兜底为什么这些步骤不能省。本文涉及的真实目录textapp/services/order/StoreCartServices.phpapp/services/order/StoreOrderComputedServices.phpapp/services/order/StoreOrderCreateServices.phpapp/services/order/StoreOrderServices.phpapp/services/activity/coupon/StoreCouponUserServices.phpapp/services/product/product/StoreProductServices.phpapp/services/user/level/SystemUserLevelServices.phpapp/services/user/member/MemberCardServices.phpapp/services/activity/seckill/StoreSeckillServices.phpapp/services/activity/combination/StoreCombinationServices.phpapp/services/activity/bargain/StoreBargainServices.phpapp/services/activity/integral/StoreIntegralServices.phpapp/services/activity/live/LiveRoomProductServices.php## 一、先把购物车整合成“能结算的数据”下单前CRMEB Pro 不直接拿购物车表开算而是先把购物车商品整合成统一结构。入口在phppublic function getUserProductCartListV1(int $uid, $cartIds, bool $new, array $addr [], int $shipping_type 1, int $coupon_id 0, bool $isCart false, int $isSendGift 0)这一步会先处理三件很关键的事text1. 补齐直播间、活动关系字段。2. 检查购物车里是不是混了不能一起结算的商品。3. 把商品价格、会员价、活动价、运费先整成统一结构。比如直播商品不能和普通商品混算代码里直接拦phpif (!in_array(21, $types, true)) {return;}if (count($types) 1) {throw new ValidateException(直播商品不能和普通商品一起结算);}这类限制很重要。因为一旦类型混了后面的优惠券适用范围、运费模板、活动库存都会被串乱。## 二、购物车里真正能结算的是 valid 不是原始列表handleCartList() 会把购物车原始数据加工成两组textvalid可以继续结算的商品。invalid不能结算、要提示用户处理的商品。在这一步里商品会被补齐这些字段texttruePricevip_truePriceprice_typechannel_pricecostPricetrueStocksum_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() 里先检查当前购物车和商品是否符合券的使用条件再计算实际抵扣金额。例如按商品分类、品牌或商品范围判断phpswitch ($type) {case 0://全场券case 1://品类券case 2://商品券case 3://品牌券}然后再判断门槛phpif (!$count || $couponInfo[use_min_price] $price) {return 0;}这里最容易踩坑的是text前端显示能用不代表当前购物车还能用。因为商品数量变化、活动叠加变化、地址变化后优惠券门槛和可叠加范围可能已经变了。所以订单确认时必须重新算而不是沿用旧的券状态。## 四、订单价格组里优惠、积分、邮费是一个整体StoreOrderComputedServices::getOrderPriceGroup() 会把这几个核心量一起算textsumPricetotalPricecostPricetotalIntegralvipPricelevelPricememberPricechangePricechannelPricestorePostagestorePostageDiscount这里有个容易误解的点textvipPrice 不是最终支付价它只是会员价和等级价相关的优惠统计口径。真正的支付价是先算商品再算优惠券再算首单优惠再算积分最后加运费phpif ($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() 会按商品类型走不同库存处理逻辑phpswitch ($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先扣库存再判断优惠券再算积分那只要中间有一个校验失败就会出现库存已经扣了、订单没建成、优惠券状态也不对的事故。## 七、订单商品明细要保存快照不要只存 idStoreOrderCartInfoServices::setCartInfo() 会把购物车商品直接序列化成订单商品快照phpcart_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后面商品名改了、规格改了、活动结束了订单详情页就会失真。## 八、二开时最该守住的边界如果你要改订单链路优先守住这几个边界text1. 购物车只做整合不直接承担最终结算责任。2. 优惠券、积分、首单优惠必须在后端重算。3. 库存扣减必须和商品类型绑定不能一把梭。4. 订单主表和订单商品表都要留快照。5. 事务内先落单再扣减和写明细别乱换顺序。## 注意事项1. 订单、库存、优惠券、积分都属于高风险业务改动前先确认影响范围。2. 直播、拼团、秒杀、积分、套餐等活动商品不要和普通商品混用结算逻辑。3. 不要把活动库存当普通库存扣减。4. 订单商品快照一旦落库后续不要反向污染历史单据。5. 任何金额计算都优先用项目现有的 bc* 方式。## 标签建议#CRMEBPro #订单二开 #购物车 #优惠券 #库存预占 #源码解析 #价格分摊 #事务 #二开实战 #商城系统