feat: 增强奖池与钱包管理功能
更新 AdminJackpotPoolUpdateController 校验规则,禁止传入 current_amount。 优化 AdminRiskPoolManualStatusController:更新奖池状态后同步 Redis 状态。 在 TransferOrderReconcileController 中新增 completeCredit 方法,用于处理卡住的转账订单对账。 调整 TransferOrderListController:优化转账订单处理条件。 在 TicketItemsIndexController 中实现支持时区的日期筛选,提升日期处理准确性。 扩展 JackpotPool 模型,新增 adjustments 关联关系。 改进票据与钱包相关服务中的错误处理和事务管理。
This commit is contained in:
@@ -135,28 +135,44 @@ final class LotteryTransferService
|
||||
);
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($player, $currencyCode, $amountMinor, $order, $main, $transferNo, $idempotentKey): void {
|
||||
$wallet = $this->lockLotteryWallet($player, $currencyCode);
|
||||
$this->postLotteryWalletMovement(
|
||||
wallet: $wallet,
|
||||
bizType: self::BIZ_TRANSFER_IN,
|
||||
direction: self::TXN_DIR_IN,
|
||||
amountMinor: $amountMinor,
|
||||
bizNo: $transferNo,
|
||||
externalRefNo: $main->externalRefNo,
|
||||
idempotentKey: $idempotentKey,
|
||||
remark: null,
|
||||
deltaSign: 1,
|
||||
);
|
||||
try {
|
||||
DB::transaction(function () use ($player, $currencyCode, $amountMinor, $order, $main, $transferNo, $idempotentKey): void {
|
||||
$wallet = $this->lockLotteryWallet($player, $currencyCode);
|
||||
$this->postLotteryWalletMovement(
|
||||
wallet: $wallet,
|
||||
bizType: self::BIZ_TRANSFER_IN,
|
||||
direction: self::TXN_DIR_IN,
|
||||
amountMinor: $amountMinor,
|
||||
bizNo: $transferNo,
|
||||
externalRefNo: $main->externalRefNo,
|
||||
idempotentKey: $idempotentKey,
|
||||
remark: null,
|
||||
deltaSign: 1,
|
||||
);
|
||||
|
||||
$order->forceFill([
|
||||
'status' => self::ST_SUCCESS,
|
||||
'external_ref_no' => $main->externalRefNo,
|
||||
'external_request_payload' => $main->requestPayload,
|
||||
'external_response_payload' => $main->responsePayload,
|
||||
'finished_at' => now(),
|
||||
])->save();
|
||||
});
|
||||
$order->forceFill([
|
||||
'status' => self::ST_SUCCESS,
|
||||
'external_ref_no' => $main->externalRefNo,
|
||||
'external_request_payload' => $main->requestPayload,
|
||||
'external_response_payload' => $main->responsePayload,
|
||||
'finished_at' => now(),
|
||||
])->save();
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
$order->refresh();
|
||||
if ($order->status === self::ST_PROCESSING) {
|
||||
$order->forceFill([
|
||||
'status' => self::ST_PENDING_RECONCILE,
|
||||
'external_ref_no' => $main->externalRefNo,
|
||||
'external_request_payload' => $main->requestPayload,
|
||||
'external_response_payload' => $main->responsePayload,
|
||||
'fail_reason' => 'lottery_credit_failed',
|
||||
'finished_at' => null,
|
||||
])->save();
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return $this->successPayload(
|
||||
TransferOrder::query()->where('transfer_no', $transferNo)->firstOrFail(),
|
||||
@@ -328,6 +344,12 @@ final class LotteryTransferService
|
||||
return;
|
||||
}
|
||||
|
||||
if ($action === 'complete_credit') {
|
||||
DB::transaction(fn (): mixed => $this->completeStuckTransferInCredit($order, $remark));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($action === 'manually_process') {
|
||||
DB::transaction(fn (): mixed => $this->doManuallyProcess($order, $remark));
|
||||
|
||||
@@ -388,6 +410,79 @@ final class LotteryTransferService
|
||||
])->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* 主站已扣款但彩票侧入账失败时,人工/对账补完成转入。
|
||||
*/
|
||||
private function completeStuckTransferInCredit(TransferOrder $order, string $remark): void
|
||||
{
|
||||
/** @var TransferOrder $locked */
|
||||
$locked = TransferOrder::query()->whereKey($order->id)->lockForUpdate()->firstOrFail();
|
||||
|
||||
if ($locked->direction !== self::DIR_IN) {
|
||||
throw new WalletOperationException(
|
||||
'invalid_reconcile_action',
|
||||
ErrorCode::WalletExternalRejected->value,
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
if ($locked->status === self::ST_SUCCESS) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($locked->status !== self::ST_PENDING_RECONCILE) {
|
||||
throw new WalletOperationException(
|
||||
'order_not_pending_reconcile',
|
||||
ErrorCode::WalletExternalRejected->value,
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
if (! $this->isEligibleForCompleteCredit($locked)) {
|
||||
throw new WalletOperationException(
|
||||
'complete_credit_not_eligible',
|
||||
ErrorCode::WalletExternalRejected->value,
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
$idempotentKey = (string) $locked->idempotent_key;
|
||||
if (WalletTxn::query()
|
||||
->where('idempotent_key', $idempotentKey)
|
||||
->where('biz_type', self::BIZ_TRANSFER_IN)
|
||||
->where('status', self::TXN_POSTED)
|
||||
->exists()) {
|
||||
$locked->forceFill([
|
||||
'status' => self::ST_SUCCESS,
|
||||
'finished_at' => now(),
|
||||
])->save();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$player = Player::query()->whereKey($locked->player_id)->firstOrFail();
|
||||
$currencyCode = (string) $locked->currency_code;
|
||||
$wallet = $this->lockLotteryWallet($player, $currencyCode);
|
||||
|
||||
$this->postLotteryWalletMovement(
|
||||
wallet: $wallet,
|
||||
bizType: self::BIZ_TRANSFER_IN,
|
||||
direction: self::TXN_DIR_IN,
|
||||
amountMinor: (int) $locked->amount,
|
||||
bizNo: $locked->transfer_no,
|
||||
externalRefNo: $locked->external_ref_no,
|
||||
idempotentKey: $idempotentKey,
|
||||
remark: $remark ?: 'complete_stuck_transfer_in',
|
||||
deltaSign: 1,
|
||||
);
|
||||
|
||||
$locked->forceFill([
|
||||
'status' => self::ST_SUCCESS,
|
||||
'fail_reason' => null,
|
||||
'finished_at' => now(),
|
||||
])->save();
|
||||
}
|
||||
|
||||
private function doManuallyProcess(TransferOrder $order, string $remark): void
|
||||
{
|
||||
/** @var TransferOrder $locked */
|
||||
@@ -406,6 +501,14 @@ final class LotteryTransferService
|
||||
return;
|
||||
}
|
||||
|
||||
if ($locked->direction === self::DIR_OUT && $locked->status === self::ST_PENDING_RECONCILE) {
|
||||
throw new WalletOperationException(
|
||||
'manually_process_requires_reverse_for_transfer_out',
|
||||
ErrorCode::WalletExternalRejected->value,
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
$locked->forceFill([
|
||||
'status' => self::ST_MANUALLY_PROCESSED,
|
||||
'fail_reason' => 'manually_processed: '.($remark ?: 'admin_manual'),
|
||||
@@ -413,6 +516,13 @@ final class LotteryTransferService
|
||||
])->save();
|
||||
}
|
||||
|
||||
/** 仅主站已扣款(有 external_ref_no)且彩票入账失败时可补完成转入。 */
|
||||
private function isEligibleForCompleteCredit(TransferOrder $order): bool
|
||||
{
|
||||
return $order->fail_reason === 'lottery_credit_failed'
|
||||
&& trim((string) $order->external_ref_no) !== '';
|
||||
}
|
||||
|
||||
private function lockLotteryWalletById(int $playerId, string $currencyCode): PlayerWallet
|
||||
{
|
||||
$wallet = PlayerWallet::query()
|
||||
@@ -699,7 +809,8 @@ final class LotteryTransferService
|
||||
bool $requireBalance = false,
|
||||
): array {
|
||||
$before = (int) $wallet->balance;
|
||||
if ($requireBalance && $deltaSign < 0 && $before < $amountMinor) {
|
||||
$available = $before - (int) $wallet->frozen_balance;
|
||||
if ($requireBalance && $deltaSign < 0 && $available < $amountMinor) {
|
||||
throw new WalletOperationException(
|
||||
'insufficient_balance',
|
||||
ErrorCode::WalletInsufficientBalance->value,
|
||||
|
||||
Reference in New Issue
Block a user