1.修复自动创建下一期bug
This commit is contained in:
@@ -6,6 +6,7 @@ namespace app\common\service;
|
|||||||
|
|
||||||
use app\common\library\game\StreakWinReward;
|
use app\common\library\game\StreakWinReward;
|
||||||
use support\Log;
|
use support\Log;
|
||||||
|
use support\Redis;
|
||||||
use support\think\Db;
|
use support\think\Db;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
@@ -28,6 +29,9 @@ final class GameBetSettleService
|
|||||||
|
|
||||||
public const TOPIC_JACKPOT_HIT = 'jackpot.hit';
|
public const TOPIC_JACKPOT_HIT = 'jackpot.hit';
|
||||||
|
|
||||||
|
/** 每期结算推送去重(避免事务重试 / recover 重复推 user.streak、wallet.changed) */
|
||||||
|
private const SETTLE_NOTIFY_DEDUP_PREFIX = 'dfw:v1:settle:notify:';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 对指定期次按开奖号码结算所有「待开奖」注单;同一注单幂等(仅 status=1 会更新)。
|
* 对指定期次按开奖号码结算所有「待开奖」注单;同一注单幂等(仅 status=1 会更新)。
|
||||||
*
|
*
|
||||||
@@ -41,7 +45,13 @@ final class GameBetSettleService
|
|||||||
public static function settleBetsForDraw(int $recordId, int $resultNumber): array
|
public static function settleBetsForDraw(int $recordId, int $resultNumber): array
|
||||||
{
|
{
|
||||||
if ($recordId <= 0 || $resultNumber < 1) {
|
if ($recordId <= 0 || $resultNumber < 1) {
|
||||||
return ['jackpot_hits' => [], 'bet_wins' => []];
|
return [
|
||||||
|
'jackpot_hits' => [],
|
||||||
|
'bet_wins' => [],
|
||||||
|
'user_streak_events' => [],
|
||||||
|
'wallet_events' => [],
|
||||||
|
'settled_order_count' => 0,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
$now = time();
|
$now = time();
|
||||||
@@ -65,6 +75,14 @@ final class GameBetSettleService
|
|||||||
/** @var array<int, array{user_id: int, period_id: int, period_no: string, result_number: int, total_win: string, balance_after: string, is_jackpot: bool, bets: list<array{bet_id: int, win_amount: string}>}> */
|
/** @var array<int, array{user_id: int, period_id: int, period_no: string, result_number: int, total_win: string, balance_after: string, is_jackpot: bool, bets: list<array{bet_id: int, win_amount: string}>}> */
|
||||||
$winByUser = [];
|
$winByUser = [];
|
||||||
|
|
||||||
|
/** @var list<array{user_id: int, current_streak: int, extra: array<string, mixed>}> */
|
||||||
|
$userStreakEvents = [];
|
||||||
|
|
||||||
|
/** @var list<array<string, mixed>> */
|
||||||
|
$walletEvents = [];
|
||||||
|
|
||||||
|
$settledOrderCount = 0;
|
||||||
|
|
||||||
foreach ($bets as $bet) {
|
foreach ($bets as $bet) {
|
||||||
$betId = (int) ($bet['id'] ?? 0);
|
$betId = (int) ($bet['id'] ?? 0);
|
||||||
if ($betId <= 0) {
|
if ($betId <= 0) {
|
||||||
@@ -98,6 +116,8 @@ final class GameBetSettleService
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$settledOrderCount++;
|
||||||
|
|
||||||
self::creditUserBetFlow($bet, $now);
|
self::creditUserBetFlow($bet, $now);
|
||||||
|
|
||||||
if ($userId > 0) {
|
if ($userId > 0) {
|
||||||
@@ -115,9 +135,13 @@ final class GameBetSettleService
|
|||||||
|
|
||||||
$balanceAfter = (string) (Db::name('user')->where('id', $userId)->value('coin') ?? '0');
|
$balanceAfter = (string) (Db::name('user')->where('id', $userId)->value('coin') ?? '0');
|
||||||
if (!$needReview && bccomp($win, '0', 2) > 0) {
|
if (!$needReview && bccomp($win, '0', 2) > 0) {
|
||||||
$paid = self::creditUserPayout($bet, $betId, $win, $now, null, '压注派彩', $resultNumber);
|
$paid = self::creditUserPayout($bet, $betId, $win, $now, null, '压注派彩', $resultNumber, false);
|
||||||
if ($paid !== null) {
|
if (is_array($paid)) {
|
||||||
$balanceAfter = $paid;
|
$balanceAfter = (string) ($paid['balance_after'] ?? $balanceAfter);
|
||||||
|
$walletPayload = $paid['wallet_payload'] ?? null;
|
||||||
|
if (is_array($walletPayload)) {
|
||||||
|
$walletEvents[] = $walletPayload;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,14 +206,17 @@ final class GameBetSettleService
|
|||||||
]);
|
]);
|
||||||
GameHotDataCoordinator::afterUserCommitted($userId);
|
GameHotDataCoordinator::afterUserCommitted($userId);
|
||||||
$periodNo = isset($aggregateByUser[$userId]['period_no']) ? (string) $aggregateByUser[$userId]['period_no'] : '';
|
$periodNo = isset($aggregateByUser[$userId]['period_no']) ? (string) $aggregateByUser[$userId]['period_no'] : '';
|
||||||
GameWebSocketPayloadHelper::publishUserStreak($userId, $next, [
|
$userStreakEvents[] = [
|
||||||
// 明确标记本期结算结果,客户端可直接判断“当前用户是否中奖”。
|
'user_id' => $userId,
|
||||||
'is_win' => $hadWin,
|
'current_streak' => $next,
|
||||||
'period_id' => $recordId,
|
'extra' => [
|
||||||
'period_no' => $periodNo,
|
'is_win' => $hadWin,
|
||||||
'result_number' => $resultNumber,
|
'period_id' => $recordId,
|
||||||
'settled_at' => $now,
|
'period_no' => $periodNo,
|
||||||
]);
|
'result_number' => $resultNumber,
|
||||||
|
'settled_at' => $now,
|
||||||
|
],
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 兜底:若已判定本期中奖(is_win=true),但聚合中奖事件意外缺失,补一条 bet.win,保证客户端可感知中奖。
|
// 兜底:若已判定本期中奖(is_win=true),但聚合中奖事件意外缺失,补一条 bet.win,保证客户端可感知中奖。
|
||||||
@@ -247,7 +274,13 @@ final class GameBetSettleService
|
|||||||
|
|
||||||
$betWins = array_values($winByUser);
|
$betWins = array_values($winByUser);
|
||||||
|
|
||||||
return ['jackpot_hits' => $jackpotHits, 'bet_wins' => $betWins];
|
return [
|
||||||
|
'jackpot_hits' => $jackpotHits,
|
||||||
|
'bet_wins' => $betWins,
|
||||||
|
'user_streak_events' => $userStreakEvents,
|
||||||
|
'wallet_events' => $walletEvents,
|
||||||
|
'settled_order_count' => $settledOrderCount,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -295,18 +328,78 @@ final class GameBetSettleService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 结算提交后统一推送:先 bet.win(全员中奖),再 jackpot.hit(仅大奖档)。
|
* 结算提交后统一推送:user.streak / wallet.changed / bet.win / jackpot.hit(每期仅推一次)。
|
||||||
*
|
*
|
||||||
* @param array{jackpot_hits?: list<array<string, mixed>>, bet_wins?: list<array<string, mixed>>} $settleOut
|
* @param array{
|
||||||
|
* jackpot_hits?: list<array<string, mixed>>,
|
||||||
|
* bet_wins?: list<array<string, mixed>>,
|
||||||
|
* user_streak_events?: list<array{user_id: int, current_streak: int, extra?: array<string, mixed>}>,
|
||||||
|
* wallet_events?: list<array<string, mixed>>,
|
||||||
|
* settled_order_count?: int
|
||||||
|
* } $settleOut
|
||||||
*/
|
*/
|
||||||
public static function publishSettlementWinsAfterCommit(array $settleOut, int $periodId, string $periodNo, int $resultNumber): void
|
public static function publishSettlementWinsAfterCommit(array $settleOut, int $periodId, string $periodNo, int $resultNumber): void
|
||||||
{
|
{
|
||||||
|
$settledCount = filter_var($settleOut['settled_order_count'] ?? 0, FILTER_VALIDATE_INT);
|
||||||
$betWins = is_array($settleOut['bet_wins'] ?? null) ? $settleOut['bet_wins'] : [];
|
$betWins = is_array($settleOut['bet_wins'] ?? null) ? $settleOut['bet_wins'] : [];
|
||||||
|
$hasWins = $betWins !== [];
|
||||||
|
$hasStreak = is_array($settleOut['user_streak_events'] ?? null) && $settleOut['user_streak_events'] !== [];
|
||||||
|
$hasWallet = is_array($settleOut['wallet_events'] ?? null) && $settleOut['wallet_events'] !== [];
|
||||||
|
if ($settledCount === false || $settledCount <= 0) {
|
||||||
|
if (!$hasWins && !$hasStreak && !$hasWallet) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!self::markSettlementNotifyOnce($periodId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$streakEvents = is_array($settleOut['user_streak_events'] ?? null) ? $settleOut['user_streak_events'] : [];
|
||||||
|
foreach ($streakEvents as $row) {
|
||||||
|
if (!is_array($row)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$userId = filter_var($row['user_id'] ?? 0, FILTER_VALIDATE_INT);
|
||||||
|
if ($userId === false || $userId <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$streak = filter_var($row['current_streak'] ?? 0, FILTER_VALIDATE_INT);
|
||||||
|
$extra = is_array($row['extra'] ?? null) ? $row['extra'] : [];
|
||||||
|
GameWebSocketPayloadHelper::publishUserStreak($userId, $streak === false ? 0 : $streak, $extra);
|
||||||
|
}
|
||||||
|
|
||||||
|
$walletEvents = is_array($settleOut['wallet_events'] ?? null) ? $settleOut['wallet_events'] : [];
|
||||||
|
foreach ($walletEvents as $payload) {
|
||||||
|
if (!is_array($payload) || empty($payload['user_id'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$userId = filter_var($payload['user_id'], FILTER_VALIDATE_INT);
|
||||||
|
if ($userId === false || $userId <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
GameWebSocketEventBus::publish('wallet.changed', GameWebSocketPayloadHelper::mergeUserStreakInto($payload, $userId));
|
||||||
|
}
|
||||||
|
|
||||||
$jackpotHits = is_array($settleOut['jackpot_hits'] ?? null) ? $settleOut['jackpot_hits'] : [];
|
$jackpotHits = is_array($settleOut['jackpot_hits'] ?? null) ? $settleOut['jackpot_hits'] : [];
|
||||||
self::publishBetWinsAfterCommit($betWins);
|
self::publishBetWinsAfterCommit($betWins);
|
||||||
self::publishJackpotHitsAfterCommit($jackpotHits, $periodId, $periodNo, $resultNumber);
|
self::publishJackpotHitsAfterCommit($jackpotHits, $periodId, $periodNo, $resultNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static function markSettlementNotifyOnce(int $periodId): bool
|
||||||
|
{
|
||||||
|
if ($periodId <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$key = self::SETTLE_NOTIFY_DEDUP_PREFIX . $periodId;
|
||||||
|
try {
|
||||||
|
$ok = Redis::set($key, '1', ['nx', 'ex' => 86400]);
|
||||||
|
|
||||||
|
return $ok === true || $ok === 'OK';
|
||||||
|
} catch (Throwable) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 批量读取用户展示名:nickname 优先;空则 fallback 到 username;仍空则返回空串(调用方自行兜底)。
|
* 批量读取用户展示名:nickname 优先;空则 fallback 到 username;仍空则返回空串(调用方自行兜底)。
|
||||||
*
|
*
|
||||||
@@ -376,7 +469,19 @@ final class GameBetSettleService
|
|||||||
$now = time();
|
$now = time();
|
||||||
$balanceAfter = null;
|
$balanceAfter = null;
|
||||||
if (bccomp($winAmount, '0', 2) > 0) {
|
if (bccomp($winAmount, '0', 2) > 0) {
|
||||||
$balanceAfter = self::creditUserPayout($row, $playRecordId, $winAmount, $now, $operatorAdminId > 0 ? $operatorAdminId : null, '大奖审核通过派彩');
|
$paid = self::creditUserPayout(
|
||||||
|
$row,
|
||||||
|
$playRecordId,
|
||||||
|
$winAmount,
|
||||||
|
$now,
|
||||||
|
$operatorAdminId > 0 ? $operatorAdminId : null,
|
||||||
|
'大奖审核通过派彩',
|
||||||
|
null,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
if (is_array($paid)) {
|
||||||
|
$balanceAfter = (string) ($paid['balance_after'] ?? '0');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$reviewRemark = trim($remark);
|
$reviewRemark = trim($remark);
|
||||||
if ($reviewRemark === '') {
|
if ($reviewRemark === '') {
|
||||||
@@ -474,8 +579,10 @@ final class GameBetSettleService
|
|||||||
}
|
}
|
||||||
Db::startTrans();
|
Db::startTrans();
|
||||||
try {
|
try {
|
||||||
self::settleBetsForDraw($rid, $rn);
|
$settleOut = self::settleBetsForDraw($rid, $rn);
|
||||||
Db::commit();
|
Db::commit();
|
||||||
|
$periodNo = (string) Db::name('game_record')->where('id', $rid)->value('period_no');
|
||||||
|
self::publishSettlementWinsAfterCommit($settleOut, $rid, $periodNo, $rn);
|
||||||
$count++;
|
$count++;
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
Db::rollback();
|
Db::rollback();
|
||||||
@@ -540,10 +647,18 @@ final class GameBetSettleService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return string|null 派彩后余额;已幂等入账过时返回当前余额;失败或未执行派彩返回 null
|
* @return array{balance_after: string, wallet_payload: array<string, mixed>}|null
|
||||||
*/
|
*/
|
||||||
private static function creditUserPayout(array $bet, int $betId, string $winAmount, int $now, ?int $operatorAdminId, string $remark, ?int $resultNumber = null): ?string
|
private static function creditUserPayout(
|
||||||
{
|
array $bet,
|
||||||
|
int $betId,
|
||||||
|
string $winAmount,
|
||||||
|
int $now,
|
||||||
|
?int $operatorAdminId,
|
||||||
|
string $remark,
|
||||||
|
?int $resultNumber = null,
|
||||||
|
bool $emitWalletEvent = true
|
||||||
|
): ?array {
|
||||||
$userId = (int) ($bet['user_id'] ?? 0);
|
$userId = (int) ($bet['user_id'] ?? 0);
|
||||||
if ($userId <= 0) {
|
if ($userId <= 0) {
|
||||||
return null;
|
return null;
|
||||||
@@ -552,8 +667,25 @@ final class GameBetSettleService
|
|||||||
$idem = 'payout_bet_' . $betId;
|
$idem = 'payout_bet_' . $betId;
|
||||||
if (Db::name('user_wallet_record')->where('idempotency_key', $idem)->value('id')) {
|
if (Db::name('user_wallet_record')->where('idempotency_key', $idem)->value('id')) {
|
||||||
$coin = Db::name('user')->where('id', $userId)->value('coin');
|
$coin = Db::name('user')->where('id', $userId)->value('coin');
|
||||||
|
$balanceAfter = (string) ($coin ?? '0');
|
||||||
|
$walletPayload = [
|
||||||
|
'user_id' => $userId,
|
||||||
|
'balance_after' => $balanceAfter,
|
||||||
|
'biz_type' => 'payout',
|
||||||
|
'ref_id' => $betId,
|
||||||
|
'amount' => $winAmount,
|
||||||
|
'period_no' => (string) ($bet['period_no'] ?? ''),
|
||||||
|
'period_id' => isset($bet['period_id']) && is_numeric($bet['period_id']) ? (int) $bet['period_id'] : 0,
|
||||||
|
'changed_at' => $now,
|
||||||
|
];
|
||||||
|
if ($resultNumber !== null && $resultNumber > 0) {
|
||||||
|
$walletPayload['result_number'] = $resultNumber;
|
||||||
|
}
|
||||||
|
|
||||||
return (string) ($coin ?? '0');
|
return [
|
||||||
|
'balance_after' => $balanceAfter,
|
||||||
|
'wallet_payload' => $walletPayload,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
$user = Db::name('user')->where('id', $userId)->find();
|
$user = Db::name('user')->where('id', $userId)->find();
|
||||||
@@ -598,9 +730,14 @@ final class GameBetSettleService
|
|||||||
if ($resultNumber !== null && $resultNumber > 0) {
|
if ($resultNumber !== null && $resultNumber > 0) {
|
||||||
$walletPayload['result_number'] = $resultNumber;
|
$walletPayload['result_number'] = $resultNumber;
|
||||||
}
|
}
|
||||||
GameWebSocketEventBus::publish('wallet.changed', GameWebSocketPayloadHelper::mergeUserStreakInto($walletPayload, $userId));
|
if ($emitWalletEvent) {
|
||||||
|
GameWebSocketEventBus::publish('wallet.changed', GameWebSocketPayloadHelper::mergeUserStreakInto($walletPayload, $userId));
|
||||||
|
}
|
||||||
|
|
||||||
return $after;
|
return [
|
||||||
|
'balance_after' => $after,
|
||||||
|
'wallet_payload' => $walletPayload,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function jackpotMaxAmount(): string
|
private static function jackpotMaxAmount(): string
|
||||||
|
|||||||
@@ -99,12 +99,25 @@ final class GameLiveService
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$pendingCount = (int) Db::name('bet_order')
|
||||||
|
->where('period_id', $recordId)
|
||||||
|
->where('status', GameBetSettleService::PLAY_STATUS_PENDING_DRAW)
|
||||||
|
->count();
|
||||||
|
|
||||||
$now = time();
|
$now = time();
|
||||||
$payoutUntil = isset($row['payout_until']) ? (int) $row['payout_until'] : 0;
|
$payoutUntil = isset($row['payout_until']) ? (int) $row['payout_until'] : 0;
|
||||||
$settleOut = ['jackpot_hits' => [], 'bet_wins' => []];
|
$settleOut = [
|
||||||
|
'jackpot_hits' => [],
|
||||||
|
'bet_wins' => [],
|
||||||
|
'user_streak_events' => [],
|
||||||
|
'wallet_events' => [],
|
||||||
|
'settled_order_count' => 0,
|
||||||
|
];
|
||||||
Db::startTrans();
|
Db::startTrans();
|
||||||
try {
|
try {
|
||||||
$settleOut = GameBetSettleService::settleBetsForDraw($recordId, $resultNumber);
|
if ($pendingCount > 0) {
|
||||||
|
$settleOut = GameBetSettleService::settleBetsForDraw($recordId, $resultNumber);
|
||||||
|
}
|
||||||
if ($status === 2) {
|
if ($status === 2) {
|
||||||
if ($payoutUntil <= 0) {
|
if ($payoutUntil <= 0) {
|
||||||
$payoutUntil = $now + self::getPayoutGraceSeconds();
|
$payoutUntil = $now + self::getPayoutGraceSeconds();
|
||||||
@@ -743,12 +756,14 @@ final class GameLiveService
|
|||||||
public static function tickAutoDraw(): void
|
public static function tickAutoDraw(): void
|
||||||
{
|
{
|
||||||
if (!GameRecordService::isAutoCreateEnabled()) {
|
if (!GameRecordService::isAutoCreateEnabled()) {
|
||||||
$openCount = (int) Db::name('game_record')->whereIn('status', [0, 1])->count();
|
$record = Db::name('game_record')
|
||||||
if ($openCount <= 0) {
|
->whereIn('status', [0, 1])
|
||||||
return;
|
->order('id', 'asc')
|
||||||
}
|
->find();
|
||||||
|
$record = is_array($record) ? $record : null;
|
||||||
|
} else {
|
||||||
|
$record = self::resolveRecordForAutoDraw();
|
||||||
}
|
}
|
||||||
$record = self::resolveRecordForAutoDraw();
|
|
||||||
if (!$record || !in_array((int) $record['status'], [0, 1], true)) {
|
if (!$record || !in_array((int) $record['status'], [0, 1], true)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -804,7 +819,7 @@ final class GameLiveService
|
|||||||
}
|
}
|
||||||
$reason = (string) __('Open period closed after payout: game is in maintenance');
|
$reason = (string) __('Open period closed after payout: game is in maintenance');
|
||||||
$rows = Db::name('game_record')
|
$rows = Db::name('game_record')
|
||||||
->whereIn('status', [0, 1])
|
->whereIn('status', [0, 1, 2])
|
||||||
->order('id', 'asc')
|
->order('id', 'asc')
|
||||||
->select()
|
->select()
|
||||||
->toArray();
|
->toArray();
|
||||||
@@ -813,6 +828,18 @@ final class GameLiveService
|
|||||||
if ($rid === false || $rid <= 0) {
|
if ($rid === false || $rid <= 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
$st = (int) ($row['status'] ?? -1);
|
||||||
|
if ($st === 2) {
|
||||||
|
Db::name('game_record')->where('id', $rid)->update([
|
||||||
|
'status' => 5,
|
||||||
|
'void_reason' => $reason,
|
||||||
|
'pending_draw_number' => null,
|
||||||
|
'payout_until' => null,
|
||||||
|
'update_time' => time(),
|
||||||
|
]);
|
||||||
|
GameHotDataCoordinator::afterGameRecordCommitted($rid);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
self::voidOpenPeriodInternal($rid, $reason);
|
self::voidOpenPeriodInternal($rid, $reason);
|
||||||
}
|
}
|
||||||
GameHotDataRedis::gameRecordRefreshAggregateCaches();
|
GameHotDataRedis::gameRecordRefreshAggregateCaches();
|
||||||
|
|||||||
Reference in New Issue
Block a user