fix: 增强充值队列与日结全链路幂等防重机制,彻底杜绝重复发放与脏写
这个提交包含在:
@@ -23,43 +23,51 @@ class OrderPaid extends BaseJob
|
||||
*/
|
||||
public function paid(Job $job, $data)
|
||||
{
|
||||
$orderData = $data['order'] ?? null;
|
||||
$userData = $data['user'] ?? null;
|
||||
$orderId = is_array($orderData) ? ($orderData['id'] ?? 0) : ($orderData->id ?? 0);
|
||||
$userId = is_array($userData) ? ($userData['id'] ?? 0) : ($userData->id ?? 0);
|
||||
|
||||
try {
|
||||
$order = $data['order'];
|
||||
$user = $data['user'];
|
||||
if ($orderId) {
|
||||
Db::transaction(function () use ($orderId, $userId) {
|
||||
$order = Order::where('id', $orderId)->lock(true)->find();
|
||||
if (!$order) {
|
||||
return;
|
||||
}
|
||||
$user = User::get($userId ?: $order->user_id);
|
||||
$ext = is_array($order->ext) ? $order->ext : (json_decode($order->ext ?? '', true) ?: []);
|
||||
|
||||
$order = Order::where('id', $order['id'])->find();
|
||||
$user = User::get($user['id']);
|
||||
|
||||
// 数据库删订单的问题常见,这里被删的订单直接把队列移除
|
||||
if ($order) {
|
||||
Db::transaction(function () use ($order, $user, $data) {
|
||||
if ($order->type == 'recharge') {
|
||||
// 充值
|
||||
$ext = $order->ext;
|
||||
$rule = $ext['rule'] ?? [];
|
||||
$money = (isset($rule['money']) && $rule['money'] > 0) ? $rule['money'] : 0;
|
||||
$gift_type = $rule['gift_type'] ?? 'money';
|
||||
$gift = (isset($rule['gift']) && $rule['gift'] > 0) ? $rule['gift'] : 0;
|
||||
if ($money > 0) {
|
||||
// 增加余额
|
||||
WalletService::change($user, 'money', $money, 'order_recharge', [
|
||||
'order_id' => $order->id,
|
||||
'order_sn' => $order->order_sn,
|
||||
'order_type' => 'trade_order',
|
||||
]);
|
||||
}
|
||||
// 幂等防重:检查是否已经执行过充值入账
|
||||
if (empty($ext['recharge_processed']) && $order->status !== Order::STATUS_COMPLETED) {
|
||||
$rule = $ext['rule'] ?? [];
|
||||
$money = (isset($rule['money']) && $rule['money'] > 0) ? $rule['money'] : 0;
|
||||
$gift_type = $rule['gift_type'] ?? 'money';
|
||||
$gift = (isset($rule['gift']) && $rule['gift'] > 0) ? $rule['gift'] : 0;
|
||||
if ($money > 0) {
|
||||
WalletService::change($user, 'money', $money, 'order_recharge', [
|
||||
'order_id' => $order->id,
|
||||
'order_sn' => $order->order_sn,
|
||||
'order_type' => 'trade_order',
|
||||
]);
|
||||
}
|
||||
|
||||
if ($gift > 0) {
|
||||
// 充值赠送
|
||||
WalletService::change($user, $gift_type, $gift, 'recharge_gift', [
|
||||
'order_id' => $order->id,
|
||||
'order_sn' => $order->order_sn,
|
||||
'order_type' => 'trade_order',
|
||||
]);
|
||||
if ($gift > 0) {
|
||||
WalletService::change($user, $gift_type, $gift, 'recharge_gift', [
|
||||
'order_id' => $order->id,
|
||||
'order_sn' => $order->order_sn,
|
||||
'order_type' => 'trade_order',
|
||||
]);
|
||||
}
|
||||
|
||||
$ext['recharge_processed'] = time();
|
||||
$order->status = Order::STATUS_COMPLETED;
|
||||
$order->ext = $ext;
|
||||
$order->save();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$uploadshoppingInfo = new WechatMiniProgramShop(Wechat::miniProgram());
|
||||
|
||||
// 微信小程序,使用 微信支付, 并且存在微信发货管理权限时,才推送发货消息
|
||||
@@ -74,9 +82,36 @@ class OrderPaid extends BaseJob
|
||||
} catch (HttpResponseException $e) {
|
||||
$data = $e->getResponse()->getData();
|
||||
$message = $data ? ($data['msg'] ?? '') : $e->getMessage();
|
||||
format_log_error($e, 'OrderPaid.paid.HttpResponseException', $message);
|
||||
} catch (\Exception $e) {
|
||||
format_log_error($e, 'OrderPaid.paid');
|
||||
format_log_error($e, 'TradeOrderPaid.paid.HttpResponseException', $message);
|
||||
$this->handleFailure($job, $orderId, $message);
|
||||
} catch (\Throwable $e) {
|
||||
$message = $e->getMessage();
|
||||
format_log_error($e, 'TradeOrderPaid.paid', $message);
|
||||
$this->handleFailure($job, $orderId, $message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生产级失败与阶梯重试处理
|
||||
*/
|
||||
protected function handleFailure(Job $job, $orderId, $errorMessage)
|
||||
{
|
||||
$attempts = $job->attempts();
|
||||
$maxAttempts = 5;
|
||||
|
||||
if ($attempts >= $maxAttempts) {
|
||||
\think\Log::record("[TradeOrderPaid CRITICAL] 充值订单 ID:{$orderId} 处理连续失败 {$attempts} 次已达上限: {$errorMessage}", 'error');
|
||||
$this->failed([
|
||||
'job' => 'trade\OrderPaid@paid',
|
||||
'order_id' => $orderId,
|
||||
'error' => $errorMessage,
|
||||
'attempts' => $attempts,
|
||||
'failed_time' => time(),
|
||||
]);
|
||||
$job->delete();
|
||||
} else {
|
||||
$delay = min(60, $attempts * 5);
|
||||
$job->release($delay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,7 +375,7 @@ class DailySettleService
|
||||
$distributed = 0;
|
||||
$walletType = PartnerConfig::rewardWalletType();
|
||||
foreach ($userIds as $userId) {
|
||||
PartnerRewardService::write([
|
||||
$record = PartnerRewardService::write([
|
||||
'order_id' => $orderId,
|
||||
'user_id' => $userId,
|
||||
'role' => $role,
|
||||
@@ -390,7 +390,8 @@ class DailySettleService
|
||||
],
|
||||
'status' => 1,
|
||||
]);
|
||||
if ((float) $each > 0) {
|
||||
$isNew = ($record && !empty($record->_isNewCreated)) || PartnerRewardService::$lastWriteCreated;
|
||||
if ($isNew && (float) $each > 0) {
|
||||
Wallet::change($userId, $walletType, $each, $type, [
|
||||
'settle_date' => $settleDate,
|
||||
]);
|
||||
@@ -456,7 +457,7 @@ class DailySettleService
|
||||
$walletType = PartnerConfig::rewardWalletType();
|
||||
|
||||
foreach ($userIds as $userId) {
|
||||
PartnerRewardService::write([
|
||||
$record = PartnerRewardService::write([
|
||||
'order_id' => $virtualOrderId,
|
||||
'buyer_id' => 0,
|
||||
'user_id' => $userId,
|
||||
@@ -474,7 +475,8 @@ class DailySettleService
|
||||
],
|
||||
'status' => 1,
|
||||
]);
|
||||
if ((float) $each > 0) {
|
||||
$isNew = ($record && !empty($record->_isNewCreated)) || PartnerRewardService::$lastWriteCreated;
|
||||
if ($isNew && (float) $each > 0) {
|
||||
Wallet::change($userId, $walletType, $each, $type, [
|
||||
'minute_settle' => 1,
|
||||
'orders' => $orderIds,
|
||||
|
||||
@@ -11,6 +11,18 @@ class MadowService
|
||||
{
|
||||
public function share($settleDate, $gmv, $userIds)
|
||||
{
|
||||
// 幂等防重:检查该结算日期是否已分发过madow
|
||||
$existing = \think\Db::name('shopro_partner_reward')
|
||||
->where('settle_date', $settleDate)
|
||||
->where('reward_type', 'madow_share')
|
||||
->find();
|
||||
if ($existing) {
|
||||
$pool = (float) $gmv;
|
||||
$count = count($userIds);
|
||||
$each = $existing['madow'] ?? 0;
|
||||
return ['pool' => $pool, 'count' => $count, 'each' => $each];
|
||||
}
|
||||
|
||||
$count = count($userIds);
|
||||
$pool = (float) $gmv;
|
||||
if ($pool <= 0) {
|
||||
|
||||
@@ -294,10 +294,13 @@ class PartnerRewardService
|
||||
} catch (\Exception $e) {
|
||||
$msg = $e->getMessage();
|
||||
if (stripos($msg, 'Duplicate') !== false || stripos($msg, '1062') !== false) {
|
||||
$existing = RewardModel::where('order_id', $payload['order_id'])
|
||||
$existingQuery = RewardModel::where('order_id', $payload['order_id'])
|
||||
->where('user_id', $payload['user_id'])
|
||||
->where('reward_type', $payload['reward_type'])
|
||||
->find();
|
||||
->where('reward_type', $payload['reward_type']);
|
||||
if (!empty($payload['settle_date'])) {
|
||||
$existingQuery->where('settle_date', $payload['settle_date']);
|
||||
}
|
||||
$existing = $existingQuery->find();
|
||||
if ($existing) {
|
||||
$existing->_isNewCreated = false;
|
||||
}
|
||||
|
||||
@@ -253,6 +253,16 @@ class VoucherService
|
||||
|
||||
public function releaseByDate($settleDate)
|
||||
{
|
||||
// 幂等防重:检查该结算日期是否已释放过兑换券
|
||||
$existingCount = RewardModel::where('settle_date', $settleDate)
|
||||
->where('reward_type', 'voucher_release')
|
||||
->count();
|
||||
if ($existingCount > 0) {
|
||||
return (float) RewardModel::where('settle_date', $settleDate)
|
||||
->where('reward_type', 'voucher_release')
|
||||
->sum('amount');
|
||||
}
|
||||
|
||||
$plans = VoucherPlan::where('status', 1)
|
||||
->where('start_date', '<=', $settleDate)
|
||||
->select();
|
||||
|
||||
在新工单中引用
屏蔽一个用户