feat(积分): 收货发放购买赠送积分并按来源分族退款冲正

- 下单快照 extra_info.items 写入商品级 give_integral,收货按数量聚合发放,旧订单缺快照回查商品配置兜底
- 赠送流水区分下单族(order_give_integral)/购买族(pay_give_integral),退款冲正按两族占比拆分落库 order_gain_refund/product_gain_refund,幂等标记分族查重
This commit is contained in:
吴昊天 2026-10-12 15:04:58 +08:00
parent 6da86d4e6d
commit 4668f0a1ea
6 changed files with 114 additions and 21 deletions

View File

@ -2101,6 +2101,8 @@ class CenterOrderController
'store_name' => $item['productInfo']['store_name'] ?? '',
'price' => $item['productInfo']['price'] ?? '0.00',
'true_price' => $item['truePrice'] ?? '0.00',
// 商品级购买送积分快照(eb_product_center.give_integral),收货时按数量聚合发放
'give_integral' => (float)($item['productInfo']['give_integral'] ?? 0),
'cart_num' => $item['cart_num'] ?? 1,
'product_type' => $item['productInfo']['product_type'] ?? '',
'attr_info' => $item['productInfo']['attrInfo'] ?? [],

View File

@ -32,7 +32,7 @@ class UserBillDao extends BaseDao
{
return $this->getModel()->where('link_id', $orderId)->where('uid', $uid)
->where('category', 'integral')->where('status', 1)
->whereIn('type', ['gain', 'product_gain', 'order_give_integral', 'pay_give_integral', 'order_deduction', 'integral_refund'])
->whereIn('type', ['gain', 'product_gain', 'order_give_integral', 'pay_give_integral', 'order_deduction', 'integral_refund', 'order_gain_refund', 'product_gain_refund'])
->field('type, pm, SUM(number) AS number')->group('type, pm')->select()->toArray();
}

View File

@ -68,10 +68,16 @@ class OrderRefundRewardServices extends BaseService
if (!$user) {
throw new \RuntimeException('退款权益关联用户不存在');
}
$earned = $returned = '0';
$earned = $returned = $earnedOrder = $earnedProduct = '0';
foreach ($this->billDao->getOrderIntegralRewards((int)$order['id'], $uid) as $bill) {
if ((int)$bill['pm'] === 1) {
$earned = bcadd($earned, (string)$bill['number'], 0);
// 下单赠送族 gain/order_give_integral,购买赠送族 product_gain/pay_give_integral
if (in_array($bill['type'], ['gain', 'order_give_integral'], true)) {
$earnedOrder = bcadd($earnedOrder, (string)$bill['number'], 0);
} else {
$earnedProduct = bcadd($earnedProduct, (string)$bill['number'], 0);
}
} else {
$returned = bcadd($returned, (string)$bill['number'], 0);
}
@ -79,9 +85,22 @@ class OrderRefundRewardServices extends BaseService
$integralTarget = $this->target($earned, $total, $paid, 0);
$processed = max((int)$returned, (int)($accounted['integral'] ?? 0));
$due = max(0, (int)bcsub($integralTarget, (string)$processed, 0));
$deduct = min($due, max(0, (int)$user['integral']));
if ($deduct > 0 && !$this->app->make(UserBillServices::class)->billIntegralIncome(
'integral_refund', $uid, $deduct, (int)$order['id'], '订单退款回收赠积分:' . $order['order_no']
// 按两族赠送占比拆分本次冲正额,分族落库(order_gain_refund / product_gain_refund)
$dueOrder = $earned > 0 ? (int)bcdiv(bcmul((string)$due, $earnedOrder, 2), $earned, 0) : 0;
$dueProduct = max(0, $due - $dueOrder);
$balance = max(0, (int)$user['integral']);
$deductOrder = min($dueOrder, $balance);
$balance -= $deductOrder;
$deductProduct = min($dueProduct, $balance);
$deduct = $deductOrder + $deductProduct;
$userBillServices = $this->app->make(UserBillServices::class);
if ($deductOrder > 0 && !$userBillServices->billIntegralIncome(
'order_gain_refund', $uid, $deductOrder, (int)$order['id'], '订单退款回收下单赠送积分:' . $order['order_no']
)) {
throw new \RuntimeException('退款赠积分冲正失败');
}
if ($deductProduct > 0 && !$userBillServices->billIntegralIncome(
'product_gain_refund', $uid, $deductProduct, (int)$order['id'], '订单退款回收购买商品赠送积分:' . $order['order_no']
)) {
throw new \RuntimeException('退款赠积分冲正失败');
}

View File

@ -13,6 +13,7 @@ namespace app\services\order;
use app\dao\order\OrderCenterDao;
use app\dao\product\CenterProductDao;
use crmeb\basic\BaseService;
use app\services\user\member\MemberCardServices;
use app\services\user\UserBillServices;
@ -266,10 +267,39 @@ class OrderTakeServices extends BaseService
}
/** @var UserBillServices $userBillServices */
$userBillServices = app()->make(UserBillServices::class);
// 检查是否有赠送积分可送(中台订单无 gain_integral 实体列,缺省 0——商品级赠送快照聚合待后续补全)
if ((int)($order['gain_integral'] ?? 0) > 0) {
// 购买商品送积分:中台订单无 gain_integral 实体列,从 extra_info.items 的商品级
// give_integral 快照按数量聚合(下单时由 buildOrderItemsForExtra 写入)
$extraInfo = $order['extra_info'] ?? [];
if (is_string($extraInfo)) {
$extraInfo = json_decode($extraInfo, true) ?: [];
}
$items = $extraInfo['items'] ?? [];
// 修复前下的旧订单快照无 give_integral 字段,回查中台商品当前配置兜底
$missingIds = [];
foreach ($items as $item) {
if (!array_key_exists('give_integral', $item) && (int)($item['product_id'] ?? 0) > 0) {
$missingIds[$item['product_id']] = true;
}
}
$productGiveMap = [];
if ($missingIds) {
/** @var CenterProductDao $centerProductDao */
$centerProductDao = app()->make(CenterProductDao::class);
$productGiveMap = $centerProductDao->getColumn([['id', 'in', array_keys($missingIds)]], 'give_integral', 'id') ?: [];
}
$gainIntegral = '0';
foreach ($items as $item) {
$give = array_key_exists('give_integral', $item)
? (float)($item['give_integral'] ?? 0)
: (float)($productGiveMap[$item['product_id'] ?? 0] ?? 0);
if ($give > 0) {
$num = max(1, (int)($item['cart_num'] ?? 1));
$gainIntegral = bcadd($gainIntegral, bcmul((string)$give, (string)$num, 0), 0);
}
}
if ((float)$gainIntegral > 0) {
// 购买商品送积分
$res2 = $userBillServices->billIntegralIncome('pay_give_integral', $order['uid'], $order['gain_integral'], $order['id']);
$res2 = $userBillServices->billIntegralIncome('pay_give_integral', $order['uid'], $gainIntegral, $order['id']);
} else {
$res2 = true;
}
@ -293,7 +323,7 @@ class OrderTakeServices extends BaseService
} else {
$res3 = true;
}
$give_integral = $order_integral + (int)($order['gain_integral'] ?? 0);
$give_integral = (int)$order_integral + (int)$gainIntegral;
if ($give_integral > 0 && $res2 && $res3) {
//触发积分变动事件(消息发送已移至 UserIntegralChangeListener;中台无 gain_integral 列,累计值以 bill 流水为准不再回写)
event('UserIntegralChange', [(int)$order['uid'], $order, $storeTitle, $give_integral, $order_integral]);

View File

@ -137,35 +137,61 @@ class RefundExecutorServices extends BaseService
/** @var UserBillServices $userBillServices */
$userBillServices = app()->make(UserBillServices::class);
// 操作1:收回订单赠送积分(下单赠送 gain + 商品赠送 product_gain,integral_refund 已回退过则幂等跳过)
// 操作1:分族收回订单赠送积分(下单赠送族 gain/order_give_integral、购买赠送族 product_gain/pay_give_integral)
// 发放链路经 billIntegralIncome 落库类型为 order_give_integral / pay_give_integral,须按实际落库类型统计
$orderGain = (int)$userBillServices->sum([
'category' => 'integral',
'type' => 'order_give_integral',
'link_id' => $orderId,
'uid' => $uid,
], 'number') + (int)$userBillServices->sum([
'category' => 'integral',
'type' => 'gain',
'link_id' => $orderId,
'uid' => $uid,
], 'number');
$productGain = (int)$userBillServices->sum([
'category' => 'integral',
'type' => 'pay_give_integral',
'link_id' => $orderId,
'uid' => $uid,
], 'number') + (int)$userBillServices->sum([
'category' => 'integral',
'type' => 'product_gain',
'link_id' => $orderId,
'uid' => $uid,
], 'number');
$giveIntegral = $orderGain + $productGain;
if ($giveIntegral > 0) {
// 幂等标记查实际落库 type:模板 'integral_refund' 落库 type 为 order_deduction
// (type=integral_refund 是模板 'order_integral_refund' 返还使用积分的流水,语义不同)
$count = (int)$userBillServices->count([
if ($orderGain > 0 || $productGain > 0) {
// 幂等标记按分族落库类型分别查重
$orderRefunded = (int)$userBillServices->count([
'category' => 'integral',
'type' => 'order_deduction',
'type' => 'order_gain_refund',
'link_id' => $orderId,
]);
if (!$count) {
// 当前积分不足时扣减全部,差额不追回
$deductAmount = min($currentIntegral, $giveIntegral);
$productRefunded = (int)$userBillServices->count([
'category' => 'integral',
'type' => 'product_gain_refund',
'link_id' => $orderId,
]);
$hasRefund = false;
// 顺序扣减:当前积分不足时扣减全部,差额不追回(总量与原合并扣减口径一致)
if (!$orderRefunded && $orderGain > 0) {
$deductAmount = min($currentIntegral, $orderGain);
if ($deductAmount > 0) {
$userBillServices->billIntegralIncome('integral_refund', $uid, $deductAmount, $orderId);
$currentIntegral = bcsub((string)$currentIntegral, (string)$deductAmount, 2);
$userBillServices->billIntegralIncome('order_gain_refund', $uid, $deductAmount, $orderId);
$currentIntegral = (int)bcsub((string)$currentIntegral, (string)$deductAmount, 0);
$hasRefund = true;
}
}
if (!$productRefunded && $productGain > 0) {
$deductAmount = min($currentIntegral, $productGain);
if ($deductAmount > 0) {
$userBillServices->billIntegralIncome('product_gain_refund', $uid, $deductAmount, $orderId);
$currentIntegral = (int)bcsub((string)$currentIntegral, (string)$deductAmount, 0);
$hasRefund = true;
}
}
if ($hasRefund) {
// 清除积分冻结
$userBillServices->update(['link_id' => $orderId], ['frozen_time' => 0]);
}

View File

@ -97,6 +97,22 @@ class UserBillServices extends BaseService
'status' => 1,
'pm' => 1
],
'order_gain_refund' => [
'title' => '退款返还下单赠送积分',
'category' => self::TYPE_INTEGRAL,
'type' => 'order_gain_refund',
'mark' => '订单退款,返还下单赠送积分{%num%}',
'status' => 1,
'pm' => 0
],
'product_gain_refund' => [
'title' => '退款返还购买商品赠送积分',
'category' => self::TYPE_INTEGRAL,
'type' => 'product_gain_refund',
'mark' => '订单退款,返还购买商品赠送积分{%num%}',
'status' => 1,
'pm' => 0
],
'pay_product_integral_back' => [
'title' => '商品退积分',
'category' => self::TYPE_INTEGRAL,