CRMEB/backend/app/services/order/OrderComputedServices.php
吴昊天 5defa5ef2e feat(订单): 下单未选券时自动领取并使用整单最优优惠券
新增 OrderComputedServices::autoMatchCoupon:候选=已持有未使用+可领取(领取窗口/
库存/限领/会员券身份与手动领取同口径),整单门槛按购物车适用子集价格计算,折扣相同
优先已持有;confirm 进入确认页、computed 价格重算、create 提交下单三处挂钩,预览即
真实领取,create 后走既有扣减与订单事务内核销链路,失败降级为无券不阻断下单。
顺带修复品类券分支引用不存在的 StoreCategoryServices(改用 StoreCategoryDao)。
2026-10-21 16:54:08 +08:00

811 lines
40 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
// +----------------------------------------------------------------------
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
// +----------------------------------------------------------------------
// | Author: CRMEB Team <admin@crmeb.com>
// +----------------------------------------------------------------------
namespace app\services\order;
use crmeb\basic\BaseService;
use app\dao\order\OrderCenterDao;
use app\services\pay\PayServices;
use app\services\driver\pay\PayChannelServices;
use app\services\product\product\StoreCategoryServices;
use app\services\user\member\MemberCardServices;
use app\services\user\UserBillServices;
use app\services\user\UserServices;
use crmeb\exceptions\ApiException;
use app\services\user\UserAddressServices;
use app\services\activity\coupon\StoreCouponUserServices;
use app\services\activity\coupon\StoreCouponIssueServices;
use app\dao\activity\coupon\StoreCouponUserDao;
use think\facade\Log;
/**
* 订单计算金额
* Class OrderComputedServices
* @package app\services\order
*/
class OrderComputedServices extends BaseService
{
/**
* @var OrderCenterDao
*/
protected $dao;
/**
* 额外参数
* @var array
*/
protected $paramData = [];
/**
* OrderComputedServices constructor.
* @param OrderCenterDao $dao
*/
public function __construct(OrderCenterDao $dao)
{
$this->dao = $dao;
}
/**
* 设置额外参数
* @param array $paramData
* @return $this
*/
public function setParamData(array $paramData)
{
$this->paramData = $paramData;
return $this;
}
/**
* 计算订单金额
* @param int $uid
* @param string $key
* @param array $cartGroup
* @param int $addressId
* @param string $payType
* @param bool $useIntegral
* @param int $couponId
* @param bool $is_create
* @param int $shipping_type
* @return array
*/
public function computedOrder(int $uid, array $userInfo = [], array $cartGroup = [], int $addressId = 0, string $payType = '', bool $useIntegral = false, int $couponId = 0, bool $isCreate = false, int $shippingType = 1, int $is_gift = 0)
{
$systemPayType = PayServices::getPayType();
// 线下支付启停统一从 eb_pay_channel 读取,不再读取 system_config
if (!app()->make(PayChannelServices::class)->getChannelStatus('offline')) unset($systemPayType['offline']);
if (!$userInfo) {
/** @var UserServices $userServices */
$userServices = app()->make(UserServices::class);
$userInfo = $userServices->getUserInfo($uid);
if (!$userInfo) {
throw new ApiException('用户不存在');
}
}
$cartInfo = $cartGroup['cartInfo'];
$priceGroup = $cartGroup['priceGroup'];
$other = $cartGroup['other'];
$payPrice = (float)$priceGroup['totalPrice'];
$addr = $cartGroup['addr'] ?? [];
$postage = $priceGroup;
if (!$addr || $addr['id'] != $addressId) {
/** @var UserAddressServices $addressServices */
$addressServices = app()->make(UserAddressServices::class);
$addr = $addressServices->getAddress($addressId) ?? [];
if ($addr) {
$addr = $addr->toArray();
}
//改变地址重新计算邮费
$postage = [];
}
$combinationId = $this->paramData['combinationId'] ?? 0;
$seckillId = $this->paramData['seckill_id'] ?? 0;
$bargainId = $this->paramData['bargainId'] ?? 0;
$isActivity = $combinationId || $seckillId || $bargainId;
$pricingPolicy = $other['pricing_policy'] ?? [];
if (!$isActivity) {
//促销优惠段(价格链条位[20]):营销插件通过 OrderPromotionCompute 事件写入优惠(见 applyPromotion)
if (($pricingPolicy['promotion'] ?? true) === true) {
[$payPrice, $promotionPrice, $promotionDetail] = $this->applyPromotion($uid, $userInfo, $cartInfo, (string)$payPrice, $isCreate);
}
//使用优惠劵
if (($pricingPolicy['coupon'] ?? true) === true) {
[$payPrice, $couponPrice] = $this->useCouponId($couponId, $uid, $cartInfo, $payPrice, $isCreate);
}
//使用积分
$pricingPolicy = $other['pricing_policy'] ?? [];
[$payPrice, $deductionPrice, $usedIntegral, $SurplusIntegral] = $this->useIntegral(
$useIntegral && (($pricingPolicy['integral'] ?? true) === true),
$userInfo, $payPrice, $other
);
}
//计算邮费
[$payPrice, $payPostage, $storePostageDiscount, $storeFreePostage, $isStoreFreePostage] = $this->computedPayPostage($shippingType, $payType, $cartInfo, $addr, $payPrice, $postage, $other, $userInfo, $is_gift);
//赠送商品计算
$giftPrice = $priceGroup['giftPrice'] ?? 0;
$payPrice = bcadd($payPrice, $giftPrice, 2);
$result = [
'total_price' => $priceGroup['totalPrice'],
'gift_price' => $giftPrice,
'level_price' => $priceGroup['levelPrice'] ?? 0,
'member_price' => $priceGroup['memberPrice'] ?? 0,
//促销优惠段(价格链条位[20])优惠总额与明细,营销插件经 OrderPromotionCompute 写回
'promotion_price' => $promotionPrice ?? 0,
'promotion_detail' => $promotionDetail ?? [],
'pay_price' => $payPrice > 0 ? $payPrice : 0,
'pay_postage' => $payPostage,
'coupon_price' => $couponPrice ?? 0,
'deduction_price' => $deductionPrice ?? 0,
'usedIntegral' => $usedIntegral ?? 0,
'SurplusIntegral' => $SurplusIntegral ?? 0,
'storePostageDiscount' => $storePostageDiscount ?? 0,
'isStoreFreePostage' => $isStoreFreePostage ?? false,
'storeFreePostage' => $storeFreePostage ?? 0
];
$this->paramData = [];
return $result;
}
/**
* 应用促销优惠段(价格链条位[20],优先级见 PriceChainPriority::PROMOTION)
*
* 中台不实现具体营销玩法,仅提供 OrderPromotionCompute 事件扩展缝口:
* 交易营销类插件(如满减、满赠、加价购)通过模块 event.php 监听该事件,
* 以引用方式向 $event['promotion_price'] 累加写回优惠总额、向 $event['detail']
* 写回明细(key 为插件标识)。插件未安装时无监听器,本段为空操作。
*
* @param int $uid 用户编号
* @param mixed $userInfo 用户信息(数组或 Model)
* @param array $cartInfo 购物车信息
* @param string $totalPrice 商品总价(未叠加任何优惠)
* @param bool $isCreate 是否创建订单态(false 为结算预览态)
* @return array [叠加促销后的应付金额, 促销优惠总额, 促销明细]
*/
protected function applyPromotion(int $uid, $userInfo, array $cartInfo, string $totalPrice, bool $isCreate): array
{
//用户信息统一转为数组,保证插件侧数据契约一致
if (is_object($userInfo) && method_exists($userInfo, 'toArray')) {
$userInfo = $userInfo->toArray();
}
$event = [
'uid' => $uid,
'user_info' => $userInfo,
'cart_info' => $cartInfo,
'total_price' => $totalPrice,
'is_create' => $isCreate,
//===== 以下为扩展方写回区域(引用传递),中台只读 =====
//促销优惠总额:数值型,多个营销活动各自累加后写回
'promotion_price' => 0,
//促销明细:['插件标识' => ['title' => '名称', 'price' => 减免额, ...自定义字段]]
'detail' => [],
];
event('OrderPromotionCompute', [&$event]);
//边界保护:优惠金额不得为负,且不得超过商品总价
$promotionPrice = min(max((float)($event['promotion_price'] ?? 0), 0), (float)$totalPrice);
$payPrice = bcsub($totalPrice, (string)$promotionPrice, 2);
return [$payPrice, $promotionPrice, is_array($event['detail']) ? $event['detail'] : []];
}
/**
* 使用优惠卷
* @param int $couponId
* @param int $uid
* @param $cartInfo
* @param $payPrice
* @param bool $is_create
*/
public function useCouponId(int $couponId, int $uid, $cartInfo, $payPrice, bool $isCreate)
{
//使用优惠劵
$res1 = true;
if ($couponId) {
/** @var StoreCouponUserServices $couponServices */
$couponServices = app()->make(StoreCouponUserServices::class);
$couponInfo = $couponServices->getOne([['id', '=', $couponId], ['uid', '=', $uid], ['is_fail', '=', 0], ['status', '=', 0], ['start_time', '<', time()], ['end_time', '>', time()]], '*', ['issue']);
if (!$couponInfo) {
throw new ApiException('选择的优惠劵无效');
}
$type = $couponInfo['applicable_type'] ?? 0;
$flag = false;
$price = 0;
$count = 0;
switch ($type) {
case 0:
case 3:
foreach ($cartInfo as $cart) {
$price = bcadd($price, bcmul((string)$cart['truePrice'], (string)$cart['cart_num'], 2), 2);
$count++;
}
break;
case 1://品类券
/** @var StoreCategoryServices $storeCategoryServices */
$storeCategoryServices = app()->make(StoreCategoryServices::class);
$coupon_category = explode(',', (string)$couponInfo['category_id']);
$category_ids = $storeCategoryServices->getAllById($coupon_category);
if ($category_ids) {
$cateIds = array_column($category_ids, 'id');
foreach ($cartInfo as $cart) {
if (isset($cart['productInfo']['cate_id']) && array_intersect(explode(',', $cart['productInfo']['cate_id']), $cateIds)) {
$price = bcadd($price, bcmul((string)$cart['truePrice'], (string)$cart['cart_num'], 2), 2);
$count++;
}
}
}
break;
case 2:
foreach ($cartInfo as $cart) {
if (isset($cart['product_id']) && in_array($cart['product_id'], explode(',', $couponInfo['product_id']))) {
$price = bcadd($price, bcmul((string)$cart['truePrice'], (string)$cart['cart_num'], 2), 2);
$count++;
}
}
break;
}
if ($count && $couponInfo['use_min_price'] <= $price) {
$flag = true;
}
if (!$flag) {
throw new ApiException('不满足优惠劵的使用条件');
}
if ($isCreate) {
$res1 = $couponServices->useCoupon($couponId);
}
$couponPrice = $couponInfo['coupon_price'] > $price ? $price : $couponInfo['coupon_price'];
$payPrice = (float)bcsub((string)$payPrice, (string)$couponPrice, 2);
} else {
$couponPrice = 0;
}
if (!$res1) {
throw new ApiException('使用优惠劵失败');
}
return [$payPrice, $couponPrice];
}
/**
* 未显式选券时自动匹配整单最优优惠券
* 候选 = 已持有未使用 + 可领取(与手动领取同口径:领取窗口/库存/限领次数/付费会员身份);
* 折扣相同优先用已持有(无领取副作用);整单门槛按购物车适用子集价格计算(与显式用券同口径)
* @param int $uid
* @param array $cartInfo 购物车条目(须含 truePrice/cart_num/product_id/productInfo.cate_id)
* @param bool $claim true=真实下单(可领取券先领取并返回用户券id);false=预下单试算(可领取券返回伪券信息,不产生副作用)
* @return array 已持有/已领取:['coupon_user_id' => int];可领取试算:['pseudo_coupon' => 发券行+applicable_type+discount];无可用券:[]
*/
public function autoMatchCoupon(int $uid, array $cartInfo, bool $claim = false): array
{
try {
if (!$uid || empty($cartInfo)) {
return [];
}
$now = time();
$items = [];
foreach ($cartInfo as $cart) {
if (is_object($cart)) $cart = $cart->toArray();
$items[] = [
'product_id' => (int)($cart['product_id'] ?? 0),
'cate_ids' => array_map('intval', explode(',', (string)($cart['productInfo']['cate_id'] ?? ''))),
'price' => bcmul((string)($cart['truePrice'] ?? '0'), (string)($cart['cart_num'] ?? '0'), 2),
];
}
$isMember = (int)(app()->make(UserServices::class)->value(['uid' => $uid], 'is_money_level') ?? 0) > 0;
/** @var StoreCouponUserDao $couponUserDao */
$couponUserDao = app()->make(StoreCouponUserDao::class);
// 候选1:已持有未使用(有效性条件与显式用券一致;start_time 用 <= 兼容当秒领取后立即下单)
$heldBest = null;
$heldList = $couponUserDao->selectList(
[['uid', '=', $uid], ['status', '=', 0], ['is_fail', '=', 0], ['start_time', '<=', $now], ['end_time', '>', $now]],
'*', 0, 0, '', ['issue']
);
foreach ($heldList as $coupon) {
if (is_object($coupon)) $coupon = $coupon->toArray();
[$subset, $count] = $this->couponMatchedSubset($coupon, $items);
if (!$count || bccomp((string)($coupon['use_min_price'] ?? '0'), $subset, 2) > 0) {
continue;
}
$discount = bccomp((string)$coupon['coupon_price'], $subset, 2) > 0 ? $subset : (string)$coupon['coupon_price'];
if (!$heldBest || bccomp($discount, $heldBest['discount'], 2) > 0
|| (bccomp($discount, $heldBest['discount'], 2) === 0 && (int)$coupon['end_time'] < (int)$heldBest['end_time'])) {
$heldBest = ['coupon_user_id' => (int)$coupon['id'], 'discount' => $discount, 'end_time' => (int)$coupon['end_time']];
}
}
// 候选2:可领取(receive_type 1/4、领取窗口内、有库存;使用窗口与限领次数逐张过滤)
$issueBest = null;
$issueList = app()->make(StoreCouponIssueServices::class)->getPcIssueCouponList($uid, [], 0);
foreach ($issueList as $issue) {
if (($issue['receive_type'] ?? 0) == 4 && !$isMember) {
continue;
}
if (!empty($issue['start_use_time']) && ((int)$issue['start_use_time'] > $now || (int)$issue['end_use_time'] < $now)) {
continue;
}
if (!$couponUserDao->getUserCouponCanUse($uid, (int)$issue['id'], (int)($issue['receive_limit'] ?? 0))) {
continue;
}
$candidate = array_merge($issue, ['applicable_type' => $issue['type'] ?? 0]);
[$subset, $count] = $this->couponMatchedSubset($candidate, $items);
if (!$count || bccomp((string)($issue['use_min_price'] ?? '0'), $subset, 2) > 0) {
continue;
}
$discount = bccomp((string)$issue['coupon_price'], $subset, 2) > 0 ? $subset : (string)$issue['coupon_price'];
if (!$issueBest || bccomp($discount, $issueBest['discount'], 2) > 0
|| (bccomp($discount, $issueBest['discount'], 2) === 0 && (int)($issue['end_use_time'] ?? 0) < (int)$issueBest['end_time'])) {
$issueBest = ['cid' => (int)$issue['id'], 'discount' => $discount, 'end_time' => (int)($issue['end_use_time'] ?? 0), 'row' => $candidate];
}
}
// 择优:折扣相同优先用已持有(无领取副作用)
if ($heldBest && (!$issueBest || bccomp($heldBest['discount'], $issueBest['discount'], 2) >= 0)) {
return ['coupon_user_id' => $heldBest['coupon_user_id']];
}
if ($issueBest) {
if (!$claim) {
return ['pseudo_coupon' => array_merge($issueBest['row'], ['discount' => $issueBest['discount']])];
}
// 真实下单:复用手动领取服务(库存/限领/会员身份校验内聚其中),领取后返回用户券id,由调用方走既有核销链路
$user = app()->make(UserServices::class)->getUserInfo($uid);
app()->make(StoreCouponIssueServices::class)->issueUserCoupon($issueBest['cid'], $user, true);
$ids = $couponUserDao->getColumn(['uid' => $uid, 'cid' => $issueBest['cid'], 'status' => 0], 'id');
if (empty($ids)) {
return [];
}
return ['coupon_user_id' => (int)max($ids)];
}
return [];
} catch (\Throwable $e) {
// 自动匹配/领取失败降级为无券,不阻断下单
Log::error('[OrderComputed] 自动匹配优惠券失败: ' . $e->getMessage());
return [];
}
}
/**
* 计算优惠券对购物车条目的适用子集金额与命中条数
* 口径与显式用券一致:applicable_type 0/3 全部、1 品类(含子分类展开)、2 商品
* @param array $coupon 含 applicable_type/category_id/product_id 的券信息(用户券或发券行)
* @param array $items 商品条目(product_id/cate_ids/price)
* @return array [subsetPrice, matchedCount]
*/
protected function couponMatchedSubset(array $coupon, array $items): array
{
$subset = '0';
$count = 0;
$type = (int)($coupon['applicable_type'] ?? 0);
if (in_array($type, [0, 3], true)) {
foreach ($items as $item) {
$subset = bcadd($subset, $item['price'], 2);
$count++;
}
return [$subset, $count];
}
if ($type === 1) {
$couponCategory = array_values(array_filter(explode(',', (string)($coupon['category_id'] ?? ''))));
$cateIds = [];
if ($couponCategory) {
$categoryIds = app()->make(\app\dao\product\product\StoreCategoryDao::class)->getAllById($couponCategory);
$cateIds = $categoryIds ? array_map('intval', array_column($categoryIds, 'id')) : [];
}
if (!$cateIds) {
return [$subset, 0];
}
foreach ($items as $item) {
if ($item['cate_ids'] && array_intersect($item['cate_ids'], $cateIds)) {
$subset = bcadd($subset, $item['price'], 2);
$count++;
}
}
return [$subset, $count];
}
if ($type === 2) {
$productIds = array_map('intval', array_filter(explode(',', (string)($coupon['product_id'] ?? ''))));
if (!$productIds) {
return [$subset, 0];
}
foreach ($items as $item) {
if (in_array($item['product_id'], $productIds, true)) {
$subset = bcadd($subset, $item['price'], 2);
$count++;
}
}
}
return [$subset, $count];
}
/**
* 使用积分
* @param $useIntegral
* @param $userInfo
* @param $payPrice
* @param $other
* @return array
*/
public function useIntegral(bool $useIntegral, $userInfo, string $payPrice, array $other)
{
/** @var UserBillServices $userBillServices */
$userBillServices = app()->make(UserBillServices::class);
// 可用积分
$usable = bcsub((string)$userInfo['integral'], (string)$userBillServices->getBillSum(['uid' => $userInfo['uid'], 'is_frozen' => 1]), 0);
$SurplusIntegral = $usable;
$integralRatio = (float)($other['integralRatio'] ?? sys_config('integral_ratio', 0));
if ($useIntegral && $userInfo['integral'] > 0 && $integralRatio > 0) {
//积分抵扣上限
$integralMaxNum = sys_config('integral_max_num', 200);
if ($integralMaxNum > 0 && $usable > $integralMaxNum) {
$integral = $integralMaxNum;
} else {
$integral = $usable;
}
$deductionPrice = (float)bcmul((string)$integral, (string)$integralRatio, 2);
if ($deductionPrice < $payPrice) {
$payPrice = bcsub((string)$payPrice, (string)$deductionPrice, 2);
$usedIntegral = $integral;
} else {
$deductionPrice = $payPrice;
$usedIntegral = (int)ceil(bcdiv((string)$payPrice, (string)$integralRatio, 2));
$payPrice = 0;
}
$deductionPrice = $deductionPrice > 0 ? $deductionPrice : 0;
$usedIntegral = $usedIntegral > 0 ? $usedIntegral : 0;
$SurplusIntegral = (int)bcsub((string)$usable, $usedIntegral, 0);
} else {
$deductionPrice = 0;
$usedIntegral = 0;
}
if ($payPrice <= 0) $payPrice = 0;
return [$payPrice, $deductionPrice, $usedIntegral, $SurplusIntegral];
}
/**
* 计算邮费
* @param int $shipping_type
* @param string $payType
* @param array $cartInfo
* @param array $addr
* @param string $payPrice
* @param array $other
* @return array
*/
public function computedPayPostage(int $shipping_type, string $payType, array $cartInfo, array $addr, string $payPrice, array $postage = [], array $other = [], $userInfo = [], $is_gift = 0)
{
$storePostageDiscount = 0;
$storeFreePostage = $postage['storeFreePostage'] ?? 0;
$isStoreFreePostage = false;
if ($is_gift == 1) {
$shipping_type = 0;
$addr = [];
}
if (!$storeFreePostage) {
$storeFreePostage = floatval(sys_config('store_free_postage')) ?: 0;//满额包邮金额
}
if (!$addr && !isset($addr['id']) || !$cartInfo) {
$payPostage = 0;
} else {
//$shipping_type = 1 快递发货 $shipping_type = 2 门店自提
if ($shipping_type == 2) {
$store_self_mention = sys_config('store_self_mention') ?? 0;
if (!$store_self_mention) $shipping_type = 1;
}
//门店自提 || (线下支付 && 线下支付包邮) 没有邮费支付
if ($shipping_type === 2 || ($payType == 'offline' && ((isset($other['offlinePostage']) && $other['offlinePostage']) || sys_config('offline_postage')) == 1)) {
$payPostage = 0;
} else {
if (!$postage || !isset($postage['storePostage']) || !isset($postage['storePostageDiscount'])) {
$postage = $this->getOrderPriceGroup($storeFreePostage, $cartInfo, $addr, $userInfo);
}
$payPostage = $postage['storePostage'];
$storePostageDiscount = $postage['storePostageDiscount'];
$isStoreFreePostage = $postage['isStoreFreePostage'] ?? false;
$payPrice = (float)bcadd((string)$payPrice, (string)$payPostage, 2);
}
}
return [$payPrice, $payPostage, $storePostageDiscount, $storeFreePostage, $isStoreFreePostage];
}
/**
* 运费计算,总金额计算
* @param $cartInfo
* @param $addr
* @param array $userInfo
* @return array
*/
public function getOrderPriceGroup($storeFreePostage, $cartInfo, $addr, $userInfo = [], $shipping_type = 1, $is_gift = 0)
{
$storePostage = 0;
$storePostageDiscount = 0;
$giftPrice = 0;
$isStoreFreePostage = false;//是否满额包邮
$sumPrice = $this->getOrderSumPrice($cartInfo, 'sum_price');//获取订单原总金额
$totalPrice = $this->getOrderSumPrice($cartInfo, 'truePrice');//获取订单svip、用户等级优惠之后总金额
$costPrice = $this->getOrderSumPrice($cartInfo, 'costPrice');//获取订单成本价
$vipPrice = $this->getOrderSumPrice($cartInfo, 'vip_truePrice');//获取订单等级和付费会员总优惠金额
$levelPrice = $this->getOrderSumPrice($cartInfo, 'level');//获取会员等级优惠
$memberPrice = $this->getOrderSumPrice($cartInfo, 'member');//获取付费会员优惠
// 判断商品包邮和固定运费
// 直接读取 freight_config.express 节点(新双节点格式)
// freight=1/2/3 作为下游运费模板计算的中间态保留,从 express.type 推导
foreach ($cartInfo as $key => &$item) {
$item['postage_price'] = 0;
// 解析运费配置
$freightConfig = $item['productInfo']['freight_config'] ?? [];
// 兼容 JSON 字符串存储的历史数据
if (is_string($freightConfig)) {
$decodedFc = json_decode($freightConfig, true);
$freightConfig = is_array($decodedFc) ? $decodedFc : [];
}
if (!empty($freightConfig) && is_array($freightConfig)) {
// 读取 express 节点
$expressConfig = $freightConfig['express'] ?? ['type' => 'template'];
$expressType = (string)($expressConfig['type'] ?? 'template');
// 映射为 freight 中间态:free=1(包邮) / fixed=2(固定运费) / template=3(运费模板)
$item['productInfo']['freight'] = $expressType === 'template' ? 3 : ($expressType === 'fixed' ? 2 : 1);
$item['productInfo']['postage'] = (float)($expressConfig['postage'] ?? 0);
$item['productInfo']['temp_id'] = (int)($expressConfig['temp_id'] ?? 0);
}
// 兜底:旧购物车快照可能同时缺失 freight_config 与 freight 字段,默认按包邮处理,避免下游 Undefined array key 报错
$item['productInfo']['freight'] = $item['productInfo']['freight'] ?? 1;
$item['productInfo']['postage'] = $item['productInfo']['postage'] ?? 0;
$item['productInfo']['temp_id'] = $item['productInfo']['temp_id'] ?? 0;
if ($shipping_type == 1) {
if ($item['productInfo']['freight'] == 2) {
// 固定运费
$item['postage_price'] = bcmul((string)$item['productInfo']['postage'], (string)$item['cart_num'], 2);
$item['origin_postage_price'] = bcmul((string)$item['productInfo']['postage'], (string)$item['cart_num'], 2);
$storePostage = bcadd((string)$storePostage, (string)$item['postage_price'], 2);
} elseif ($item['productInfo']['freight'] == 1) {
// 包邮
$item['postage_price'] = 0;
}
if ($sumPrice >= $storeFreePostage) {
$item['postage_price'] = $item['origin_postage_price'] = $storePostage = 0;
}
}
if ($is_gift == 1) {
$item['postage_price'] = $item['origin_postage_price'] = $storePostage = 0;
$giftPrice = bcadd((string)$giftPrice, (string)$item['productInfo']['gift_price'], 2);
$addr = [];
}
}
$postageArr = [];
if (isset($cartInfo[0]['productInfo']['is_virtual']) && $cartInfo[0]['productInfo']['is_virtual'] == 1) {
$storePostage = 0;
} elseif ($cartInfo && $addr) {
// $storeFreePostage > 0 时检查是否满额包邮,为 0 时表示未配置满额包邮,直接按运费模板计算
if ($storeFreePostage > 0 && $sumPrice >= $storeFreePostage) {//如果总价大于等于满额包邮 邮费等于0
$isStoreFreePostage = true;
$storePostage = 0;
} else {
//按照运费模板计算每个运费模板下商品的件数/重量/体积以及总金额 按照首重倒序排列
// freight=1 包邮 → 运费为0,跳过
// freight=2 固定运费 → 已在前面计算过,跳过
// freight=3 运费模板 → 需要计算
// 注意:freight 字段已从 freight_config.express.type 映射而来
$cityId = $addr['city_id'] ?? 0;
$tempIds = [1];
foreach ($cartInfo as $key_c => $item_c) {
if (isset($item_c['productInfo']['freight']) && $item_c['productInfo']['freight'] == 3) {
$tempIds[] = $item_c['productInfo']['temp_id'];
}
}
$tempIds = array_unique($tempIds);
// 直接调用 app/ 层运费模板服务
/** @var \app\services\shipping\ShippingTemplatesServices $shippingServices */
$shippingServices = app()->make(\app\services\shipping\ShippingTemplatesServices::class);
$temp = $shippingServices->getShippingColumn(['id' => $tempIds], 'type,appoint', 'id');
/** @var \app\services\shipping\ShippingTemplatesRegionServices $regionServices */
$regionServices = app()->make(\app\services\shipping\ShippingTemplatesRegionServices::class);
$regions = $regionServices->getTempRegionList($tempIds, [$cityId, 0], 'temp_id,first,first_price,continue,continue_price', 'temp_id');
$temp_num = [];
foreach ($cartInfo as $cart) {
// freight=1 包邮 → 运费为0,跳过
// freight=2 固定运费 → 已在前面计算过,跳过
// freight=3 运费模板 → 计算运费
// 注意:freight 字段已从 freight_config.type 映射而来
if (isset($cart['productInfo']['freight']) && $cart['productInfo']['freight'] != 3) {
continue;
}
$tempId = $cart['productInfo']['temp_id'] ?? 1;
$type = $temp[$tempId]['type'] ?? $temp[1]['type'];
if ($type == 1) {
$num = $cart['cart_num'];
} elseif ($type == 2) {
$num = $cart['cart_num'] * $cart['productInfo']['attrInfo']['weight'];
} else {
$num = $cart['cart_num'] * $cart['productInfo']['attrInfo']['volume'];
}
$region = $regions[$tempId] ?? ($regions[1] ?? []);
if (!$region) continue;
if (!isset($temp_num[$tempId])) {
$temp_num[$tempId] = [
'number' => $num,
'type' => $type,
'price' => bcmul($cart['cart_num'], $cart['truePrice'], 2),
'first' => $region['first'],
'first_price' => $region['first_price'],
'continue' => $region['continue'],
'continue_price' => $region['continue_price'],
'temp_id' => $tempId
];
} else {
$temp_num[$tempId]['number'] += $num;
$temp_num[$tempId]['price'] += bcmul($cart['cart_num'], $cart['truePrice'], 2);
}
}
// 直接调用 app/ 层运费模板包邮服务
/** @var \app\services\shipping\ShippingTemplatesFreeServices $freeServices */
$freeServices = app()->make(\app\services\shipping\ShippingTemplatesFreeServices::class);
$freeList = $freeServices->isFreeList($tempIds, $addr['city_id'], 0, 'temp_id,number,price', 'temp_id');
if ($freeList) {
foreach ($temp_num as $k => $v) {
if (isset($temp[$v['temp_id']]['appoint']) && $temp[$v['temp_id']]['appoint'] && isset($freeList[$v['temp_id']])) {
$free = $freeList[$v['temp_id']];
$condition = $free['number'] <= $v['number'];
if ($free['price'] <= $v['price'] && $condition) {
unset($temp_num[$k]);
}
}
}
}
//首件运费最大值
$maxFirstPrice = $temp_num ? max(array_column($temp_num, 'first_price')) : 0;
//初始运费为0
$storePostage_arr = [];
$i = 0;
//循环运费数组
foreach ($temp_num as $fk => $fv) {
//找到首件运费等于最大值
if ($fv['first_price'] == $maxFirstPrice) {
//每次循环设置初始值
$tempArr = $temp_num;
$Postage = 0;
//计算首件运费
if ($fv['number'] <= $fv['first']) {
$Postage = bcadd($Postage, $fv['first_price'], 2);
} else {
if ($fv['continue'] <= 0) {
$Postage = $Postage;
} else {
$Postage = bcadd(bcadd($Postage, $fv['first_price'], 2), bcmul(ceil(bcdiv(bcsub($fv['number'], $fv['first'], 2), $fv['continue'] ?? 0, 2)), $fv['continue_price'], 4), 2);
}
}
$postageArr[$i]['data'][$fk] = $Postage;
//删除计算过的首件数据
unset($tempArr[$fk]);
//循环计算剩余运费
foreach ($tempArr as $ck => $cv) {
if ($cv['continue'] <= 0) {
$Postage = $Postage;
} else {
$one_postage = bcmul(ceil(bcdiv($cv['number'], $cv['continue'] ?? 0, 2)), $cv['continue_price'], 2);
$Postage = bcadd($Postage, $one_postage, 2);
$postageArr[$i]['data'][$ck] = $one_postage;
}
}
$postageArr[$i]['sum'] = $Postage;
$storePostage_arr[] = $Postage;
}
}
if (count($storePostage_arr)) {
$maxStorePostage = max($storePostage_arr);
//获取运费计算中的最大值
$storePostage = bcadd((string)$storePostage, (string)$maxStorePostage, 2);
}
}
}
//会员邮费享受折扣
if ($storePostage) {
$express_rule_number = 100;
if (!$userInfo) {
/** @var UserServices $userService */
$userService = app()->make(UserServices::class);
$userInfo = $userService->getUserInfo($addr['uid']);
}
if ($userInfo && isset($userInfo['is_money_level']) && $userInfo['is_money_level'] > 0) {
//看是否开启会员折扣奖励
/** @var MemberCardServices $memberCardService */
$memberCardService = app()->make(MemberCardServices::class);
$express_rule_number = $memberCardService->isOpenMemberCard('express');
$express_rule_number = $express_rule_number <= 0 ? 0 : $express_rule_number;
}
$discountRate = bcdiv($express_rule_number, 100, 4);
$truePostageArr = [];
foreach ($postageArr as $postitem) {
if ($postitem['sum'] == ($maxStorePostage ?? 0)) {
$truePostageArr = $postitem['data'];
break;
}
}
$cartAlready = [];
foreach ($cartInfo as &$item) {
// freight=1 包邮, freight=2 固定运费 → 跳过运费模板计算
// freight=3 运费模板 → 需要计算
if (isset($item['productInfo']['freight']) && in_array($item['productInfo']['freight'], [1, 2])) {
if ($item['productInfo']['freight'] == 2) {
// 固定运费应用折扣
$item['postage_price'] = sprintf("%.2f", bcmul($item['postage_price'], $discountRate, 6));
}
continue;
}
$tempId = $item['productInfo']['temp_id'] ?? 0;
$tempPostage = $truePostageArr[$tempId] ?? 0;
$tempNumber = $temp_num[$tempId]['number'] ?? 0;
if (!$tempId || !$tempPostage) continue;
$type = $temp_num[$tempId]['type'];
if ($type == 1) {
$num = $item['cart_num'];
} elseif ($type == 2) {
$num = $item['cart_num'] * $item['productInfo']['attrInfo']['weight'];
} else {
$num = $item['cart_num'] * $item['productInfo']['attrInfo']['volume'];
}
if ((($cartAlready[$tempId]['number'] ?? 0) + $num) >= $tempNumber) {
$price = isset($cartAlready[$tempId]['price']) ? bcsub((string)$tempPostage, (string)$cartAlready[$tempId]['price'], 6) : $tempPostage;
} else {
$price = bcmul((string)$tempPostage, bcdiv((string)$num, (string)$tempNumber, 6), 6);
}
$cartAlready[$tempId]['number'] = bcadd((string)($cartAlready[$tempId]['number'] ?? 0), (string)$num, 4);
$cartAlready[$tempId]['price'] = bcadd((string)($cartAlready[$tempId]['price'] ?? 0.00), (string)$price, 4);
if ($express_rule_number && $express_rule_number < 100) {
$price = bcmul($price, $discountRate, 4);
}
$item['postage_price'] = sprintf("%.2f", $price);
}
if ($express_rule_number && $express_rule_number < 100) {
$storePostageDiscount = $storePostage;
$storePostage = bcmul($storePostage, bcdiv($express_rule_number, 100, 4), 2);
$storePostageDiscount = bcsub($storePostageDiscount, $storePostage, 2);
} else {
$storePostageDiscount = 0;
$storePostage = $storePostage;
}
}
return compact('storePostage', 'storeFreePostage', 'isStoreFreePostage', 'sumPrice', 'totalPrice', 'costPrice', 'vipPrice', 'storePostageDiscount', 'cartInfo', 'levelPrice', 'memberPrice', 'giftPrice');
}
/**
* 获取某个字段总金额
* @param $cartInfo
* @param string $key
* @param bool $is_unit
* @return int|string
*/
public function getOrderSumPrice($cartInfo, $key = 'truePrice', $is_unit = true)
{
$SumPrice = 0;
foreach ($cartInfo as $cart) {
if (isset($cart['cart_info'])) $cart = $cart['cart_info'];
if ($is_unit) {
if ($key == 'level' || $key == 'member') {
if ($cart['price_type'] == $key) {
$SumPrice = bcadd($SumPrice, bcmul($cart['cart_num'], $cart['vip_truePrice'], 2), 2);
}
} else {
$SumPrice = bcadd($SumPrice, bcmul($cart['cart_num'], $cart[$key], 2), 2);
}
} else {
$SumPrice = bcadd($SumPrice, $cart[$key], 2);
}
}
return $SumPrice;
}
}