mirror of
https://github.com/crmeb/CRMEB.git
synced 2026-10-28 21:40:50 +00:00
feat(订单): 下单未选券时自动领取并使用整单最优优惠券
新增 OrderComputedServices::autoMatchCoupon:候选=已持有未使用+可领取(领取窗口/ 库存/限领/会员券身份与手动领取同口径),整单门槛按购物车适用子集价格计算,折扣相同 优先已持有;confirm 进入确认页、computed 价格重算、create 提交下单三处挂钩,预览即 真实领取,create 后走既有扣减与订单事务内核销链路,失败降级为无券不阻断下单。 顺带修复品类券分支引用不存在的 StoreCategoryServices(改用 StoreCategoryDao)。
This commit is contained in:
parent
8022bd9864
commit
5defa5ef2e
@ -97,8 +97,8 @@ class CenterOrderController
|
||||
// 待发货/待收货/待使用:均为进行中状态,产品类型与发货状态过滤交由 list 方法处理
|
||||
'pending_delivery' => OrderCenter::STATUS_PROCESSING,
|
||||
'pending_use' => OrderCenter::STATUS_PROCESSING,
|
||||
// 待评价:按已完成状态筛选(与前端原 getStatusMap 逻辑一致)
|
||||
'pending_comment' => OrderCenter::STATUS_COMPLETED,
|
||||
// 待评价:履约完成待评价(评价后流转 completed;与统计接口 grouped.pending_comment 口径一致)
|
||||
'pending_comment' => OrderCenter::STATUS_PENDING_REVIEW,
|
||||
// 售后:退款中 + 已退款
|
||||
'after_sale' => OrderCenter::STATUS_REFUNDING . ',' . OrderCenter::STATUS_REFUNDED,
|
||||
];
|
||||
@ -227,6 +227,9 @@ class CenterOrderController
|
||||
$integralRatio = sys_config('integral_ratio', 0);
|
||||
$integralOpen = (float) $integralRatio > 0 || $usableIntegral > 0;
|
||||
|
||||
// 活动订单不走优惠券自动匹配,标记前置(优惠券列表构建时使用)
|
||||
$isActivityOrder = $activityType !== 'normal' && $activityId > 0;
|
||||
|
||||
// 5. 获取用户可用优惠券
|
||||
$couponList = [];
|
||||
try {
|
||||
@ -237,6 +240,30 @@ class CenterOrderController
|
||||
} catch (\Throwable $e) {
|
||||
// 优惠券列表获取失败不影响主流程
|
||||
}
|
||||
// 5.1 用户尚无可用券时,自动领取整单最优优惠券并入列表(预览即真实领取,与 computed/create 自动用券同口径)
|
||||
if (!$isActivityOrder && empty($couponList)) {
|
||||
try {
|
||||
$autoCoupon = app()->make(OrderComputedServices::class)->autoMatchCoupon($uid, $cartInfo, true);
|
||||
if (!empty($autoCoupon['coupon_user_id'])) {
|
||||
$autoRow = $couponServices->get((int)$autoCoupon['coupon_user_id'], ['*'], ['issue']);
|
||||
if ($autoRow) {
|
||||
$autoRow = $autoRow->toArray();
|
||||
// 行结构与 getUsableCouponList 对齐(前端优惠券选择器字段)
|
||||
$autoRow['title'] = $autoRow['coupon_title'];
|
||||
$autoRow['type'] = $autoRow['applicable_type'] ?? 0;
|
||||
$autoRow['use_min_price'] = floatval($autoRow['use_min_price'] ?? 0);
|
||||
$autoRow['coupon_price'] = floatval($autoRow['coupon_price'] ?? 0);
|
||||
$autoRow['start_time'] = $autoRow['start_time'] ? date('Y/m/d', $autoRow['start_time']) : date('Y/m/d', $autoRow['add_time']);
|
||||
$autoRow['add_time'] = date('Y/m/d', (int)$autoRow['add_time']);
|
||||
$autoRow['end_time'] = date('Y/m/d', (int)$autoRow['end_time']);
|
||||
$autoRow['is_use'] = 0;
|
||||
$couponList[] = $autoRow;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// 自动领券失败不影响确认页主流程
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 获取商品详情扩展数据(自定义表单、商品参数)
|
||||
$detailExtend = $handler->getDetailExtend($productId);
|
||||
@ -259,7 +286,6 @@ class CenterOrderController
|
||||
$activityExtraInfo = [];
|
||||
$freePayType = '';
|
||||
$payRedirectUrl = '';
|
||||
$isActivityOrder = $activityType !== 'normal' && $activityId > 0;
|
||||
// 活动订单信息
|
||||
if ($isActivityOrder) {
|
||||
$activityAllowedPayTypes = $tradeModeHandler->getAllowedPayTypes($activityId);
|
||||
@ -743,8 +769,19 @@ class CenterOrderController
|
||||
];
|
||||
}
|
||||
|
||||
// 用户未选券时自动领取并使用整单最优优惠券(折扣对应真实领取的券,与商品详情到手价口径一致;失败降级为无券)
|
||||
if (!$couponId && !$isActivityOrder && ($pricingPolicy['coupon'] ?? true)) {
|
||||
/** @var OrderComputedServices $computedServices */
|
||||
$computedServices = app()->make(OrderComputedServices::class);
|
||||
$autoCoupon = $computedServices->autoMatchCoupon($uid, $cartInfo, true);
|
||||
if (!empty($autoCoupon['coupon_user_id'])) {
|
||||
$couponId = (int)$autoCoupon['coupon_user_id'];
|
||||
}
|
||||
}
|
||||
|
||||
// 手动校验并计算优惠券金额(不依赖 useCouponId 的 getOne 查询)
|
||||
$couponPrice = 0;
|
||||
$couponTitle = '';
|
||||
if ($couponId) {
|
||||
/** @var \app\services\activity\coupon\StoreCouponUserServices $couponServices */
|
||||
$couponServices = app()->make(\app\services\activity\coupon\StoreCouponUserServices::class);
|
||||
@ -753,6 +790,7 @@ class CenterOrderController
|
||||
if (!$couponInfo) {
|
||||
return app('json')->fail('选择的优惠劵无效');
|
||||
}
|
||||
$couponTitle = (string)$couponInfo['coupon_title'];
|
||||
// 使用 getData 获取原始值,避免 Model 访问器转换(status→"未使用"、type→"后台发放"等)
|
||||
$rawStatus = $couponInfo->getData('status');
|
||||
$rawIsFail = $couponInfo->getData('is_fail');
|
||||
@ -782,12 +820,11 @@ class CenterOrderController
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
/** @var \app\services\activity\coupon\StoreCategoryServices $storeCategoryServices */
|
||||
$storeCategoryServices = app()->make(\app\services\activity\coupon\StoreCategoryServices::class);
|
||||
$couponCategory = explode(',', (string)$couponInfo['category_id']);
|
||||
$categoryIds = $storeCategoryServices->getAllById($couponCategory);
|
||||
if ($categoryIds) {
|
||||
$cateIds = array_column($categoryIds, 'id');
|
||||
$storeCategoryServices = app()->make(\app\dao\product\product\StoreCategoryDao::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);
|
||||
@ -841,6 +878,9 @@ class CenterOrderController
|
||||
// 修正返回字段:total_price 用原始总价,coupon_price 用计算值
|
||||
$result['total_price'] = $originalTotalPrice;
|
||||
$result['coupon_price'] = $couponPrice;
|
||||
// 自动用券回显:前端据此展示已选中的优惠券
|
||||
$result['coupon_id'] = $couponId;
|
||||
$result['coupon_title'] = $couponTitle;
|
||||
|
||||
// 自动领取发生在确认接口返回之后时,重新查询可用券并返回给前端。
|
||||
// 否则金额和标题已经更新,但优惠券选择弹窗仍使用确认阶段的旧列表。
|
||||
@ -1137,6 +1177,21 @@ class CenterOrderController
|
||||
// 记录实时原始总价(优惠券抵扣不影响 total_price 记录)
|
||||
$originalTotalPrice = $priceData['totalPrice'] ?? 0;
|
||||
|
||||
// 用户未选券时自动匹配整单最优优惠券(可领取的先领取,失败降级为无券;活动订单与关闭券能力的商品类型不参与)
|
||||
if (!$couponId && !$isActivityOrder) {
|
||||
$couponPolicyEnabled = true;
|
||||
$autoHandler = $registry->get($productType);
|
||||
if (method_exists($autoHandler, 'getPricingPolicy')) {
|
||||
$couponPolicyEnabled = ($autoHandler->getPricingPolicy()['coupon'] ?? true) === true;
|
||||
}
|
||||
if ($couponPolicyEnabled) {
|
||||
$autoCoupon = app()->make(OrderComputedServices::class)->autoMatchCoupon($uid, $cartInfo, true);
|
||||
if (!empty($autoCoupon['coupon_user_id'])) {
|
||||
$couponId = (int)$autoCoupon['coupon_user_id'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1.5 手动校验并计算优惠券金额
|
||||
// 活动订单默认不支持优惠券
|
||||
if ($couponId && !$isActivityOrder) {
|
||||
@ -1160,8 +1215,7 @@ class CenterOrderController
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
/** @var \app\services\activity\coupon\StoreCategoryServices $storeCategoryServices */
|
||||
$storeCategoryServices = app()->make(\app\services\activity\coupon\StoreCategoryServices::class);
|
||||
$storeCategoryServices = app()->make(\app\dao\product\product\StoreCategoryDao::class);
|
||||
$couponCategory = explode(',', (string)$couponInfo['category_id']);
|
||||
$categoryIds = $storeCategoryServices->getAllById($couponCategory);
|
||||
if ($categoryIds) {
|
||||
|
||||
@ -22,6 +22,9 @@ 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;
|
||||
|
||||
/**
|
||||
* 订单计算金额
|
||||
@ -269,6 +272,157 @@ class OrderComputedServices extends BaseService
|
||||
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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user