1.修复自动创建下一期bug

This commit is contained in:
2026-05-26 17:49:32 +08:00
parent 5f8f5aa4ca
commit 5d33b13c6f
3 changed files with 197 additions and 235 deletions

View File

@@ -572,10 +572,10 @@ final class GameLiveService
return ['ok' => false, 'msg' => __('Failed to save scheduled draw number; please try again')];
}
GameHotDataCoordinator::afterGameRecordCommitted($rid);
self::publishSnapshot(null);
} finally {
GameHotDataLock::release(GameHotDataLock::TYPE_GAME_RECORD, (string) $rid, $lock['token'], $lock['redis_lock']);
}
self::publishSnapshot(null);
return [
'ok' => true,
@@ -610,6 +610,11 @@ final class GameLiveService
if (!$lock['acquired']) {
return ['ok' => false, 'msg' => __('Another operation is in progress for this period; please try again later')];
}
/** @var null|callable(): void */
$notifyAfterLock = null;
$result = ['ok' => false, 'msg' => __('Game live: settlement error')];
try {
self::ensureAiLocked($rid);
@@ -620,102 +625,122 @@ final class GameLiveService
$payoutUntil = 0;
$periodNo = '';
$now = time();
$drawCommitted = false;
Db::startTrans();
try {
$record = self::loadRecordRowFromDb($rid, true);
if (!$record) {
Db::rollback();
return ['ok' => false, 'msg' => __('No active game in progress')];
$result = ['ok' => false, 'msg' => __('No active game in progress')];
} else {
$st = (int) ($record['status'] ?? -1);
$existingResult = filter_var($record['result_number'] ?? 0, FILTER_VALIDATE_INT);
if ($existingResult !== false && $existingResult >= 1 && $existingResult <= self::DRAW_NUMBER_MAX && $st >= 2) {
Db::commit();
$periodNo = is_string($record['period_no'] ?? null) ? (string) $record['period_no'] : '';
GameHotDataCoordinator::afterGameRecordCommitted($rid);
$notifyAfterLock = static function () use ($rid): void {
self::publishSnapshot($rid);
};
$result = [
'ok' => true,
'msg' => __('Draw completed; paying out'),
'result_number' => $existingResult,
'estimated_loss' => '0.00',
'payout_until' => (int) ($record['payout_until'] ?? 0),
];
} elseif (!in_array($st, [0, 1], true)) {
Db::rollback();
$result = ['ok' => false, 'msg' => __('Current game status does not allow drawing')];
} else {
$elapsedLocked = max(0, $now - (int) ($record['period_start_at'] ?? $now));
if ($elapsedLocked < $betSeconds || $elapsedLocked < $periodSeconds) {
Db::rollback();
$result = ['ok' => false, 'msg' => __('Period countdown has not ended; cannot draw yet')];
} else {
[$finalNumber, $drawMode] = self::resolveFinalDrawNumber($record, $manualNumber);
$bets = Db::name('bet_order')->where('period_id', $rid)->select()->toArray();
$finalLoss = self::estimateLossForNumber($bets, $finalNumber);
$payoutUntil = $now + self::getPayoutGraceSeconds();
$periodNo = is_string($record['period_no'] ?? null) ? (string) $record['period_no'] : '';
Db::name('game_record')->where('id', $rid)->update([
'status' => 3,
'result_number' => $finalNumber,
'draw_mode' => $drawMode,
'pending_draw_number' => null,
'payout_until' => $payoutUntil,
'update_time' => $now,
]);
$settleOut = GameBetSettleService::settleBetsForDraw($rid, $finalNumber);
Db::commit();
$drawCommitted = true;
}
}
}
$st = (int) ($record['status'] ?? -1);
$existingResult = filter_var($record['result_number'] ?? 0, FILTER_VALIDATE_INT);
if ($existingResult !== false && $existingResult >= 1 && $existingResult <= self::DRAW_NUMBER_MAX && $st >= 2) {
Db::commit();
$periodNo = is_string($record['period_no'] ?? null) ? (string) $record['period_no'] : '';
GameHotDataCoordinator::afterGameRecordCommitted($rid);
self::publishSnapshot($rid);
return [
'ok' => true,
'msg' => __('Draw completed; paying out'),
'result_number' => $existingResult,
'estimated_loss' => '0.00',
'payout_until' => (int) ($record['payout_until'] ?? 0),
];
}
if (!in_array($st, [0, 1], true)) {
Db::rollback();
return ['ok' => false, 'msg' => __('Current game status does not allow drawing')];
}
$elapsedLocked = max(0, $now - (int) ($record['period_start_at'] ?? $now));
if ($elapsedLocked < $betSeconds || $elapsedLocked < $periodSeconds) {
Db::rollback();
return ['ok' => false, 'msg' => __('Period countdown has not ended; cannot draw yet')];
}
[$finalNumber, $drawMode] = self::resolveFinalDrawNumber($record, $manualNumber);
$bets = Db::name('bet_order')->where('period_id', $rid)->select()->toArray();
$finalLoss = self::estimateLossForNumber($bets, $finalNumber);
$payoutUntil = $now + self::getPayoutGraceSeconds();
$periodNo = is_string($record['period_no'] ?? null) ? (string) $record['period_no'] : '';
Db::name('game_record')->where('id', $rid)->update([
'status' => 3,
'result_number' => $finalNumber,
'draw_mode' => $drawMode,
'pending_draw_number' => null,
'payout_until' => $payoutUntil,
'update_time' => $now,
]);
$settleOut = GameBetSettleService::settleBetsForDraw($rid, $finalNumber);
Db::commit();
} catch (Throwable $e) {
Db::rollback();
return ['ok' => false, 'msg' => __('Game live: settlement error') . ': ' . $e->getMessage()];
$result = ['ok' => false, 'msg' => __('Game live: settlement error') . ': ' . $e->getMessage()];
}
GameBetSettleService::publishSettlementWinsAfterCommit(
$settleOut,
$rid,
$periodNo,
$finalNumber
);
if ($drawCommitted) {
GameBetSettleService::publishSettlementWinsAfterCommit(
$settleOut,
$rid,
$periodNo,
$finalNumber
);
GameHotDataCoordinator::afterGameRecordCommitted($rid);
GameHotDataCoordinator::afterGameRecordCommitted($rid);
try {
GameRecordStatService::refreshForRecordId($rid);
} catch (Throwable) {
try {
GameRecordStatService::refreshForRecordId($rid);
} catch (Throwable) {
}
$notifyAfterLock = static function () use (
$rid,
$periodNo,
$finalNumber,
$drawMode,
$payoutUntil,
$now,
$settleOut
): void {
self::publishPublicPeriodOpened($periodNo, $finalNumber, $drawMode, $now);
self::publishPublicPeriodPayout($rid, $periodNo, $finalNumber, $payoutUntil, $now);
$jackpotHits = is_array($settleOut['jackpot_hits'] ?? null) ? $settleOut['jackpot_hits'] : [];
GameWebSocketEventBus::publish('admin.live.opened', [
'period_id' => $rid,
'period_no' => $periodNo,
'result_number' => $finalNumber,
'draw_mode' => $drawMode,
'payout_until' => $payoutUntil,
'jackpot_hits' => $jackpotHits,
'server_time' => $now,
]);
self::publishSnapshot(null);
};
$result = [
'ok' => true,
'msg' => __('Draw completed; paying out'),
'result_number' => $finalNumber,
'draw_mode' => $drawMode,
'estimated_loss' => $finalLoss,
'payout_until' => $payoutUntil,
];
}
self::publishPublicPeriodOpened($periodNo, $finalNumber, $drawMode, $now);
self::publishPublicPeriodPayout($rid, $periodNo, $finalNumber, $payoutUntil, $now);
$jackpotHits = is_array($settleOut['jackpot_hits'] ?? null) ? $settleOut['jackpot_hits'] : [];
GameWebSocketEventBus::publish('admin.live.opened', [
'period_id' => $rid,
'period_no' => $periodNo,
'result_number' => $finalNumber,
'draw_mode' => $drawMode,
'payout_until' => $payoutUntil,
'jackpot_hits' => $jackpotHits,
'server_time' => $now,
]);
self::publishSnapshot(null);
return [
'ok' => true,
'msg' => __('Draw completed; paying out'),
'result_number' => $finalNumber,
'draw_mode' => $drawMode,
'estimated_loss' => $finalLoss,
'payout_until' => $payoutUntil,
];
} finally {
GameHotDataLock::release(GameHotDataLock::TYPE_GAME_RECORD, (string) $rid, $lock['token'], $lock['redis_lock']);
}
if ($notifyAfterLock !== null) {
$notifyAfterLock();
}
return $result;
}
/**
@@ -739,6 +764,10 @@ final class GameLiveService
return;
}
/** @var null|callable(): void */
$notifyAfterLock = null;
try {
$periodNo = is_string($row['period_no'] ?? null) ? (string) $row['period_no'] : '';
$resultNumber = filter_var($row['result_number'] ?? 0, FILTER_VALIDATE_INT);
@@ -747,6 +776,7 @@ final class GameLiveService
}
$newPeriodNo = null;
$now = time();
$finalized = false;
Db::startTrans();
try {
$affected = Db::name('game_record')
@@ -759,16 +789,19 @@ final class GameLiveService
]);
if ($affected < 1) {
Db::rollback();
return;
} else {
Db::commit();
$finalized = true;
}
Db::commit();
} catch (Throwable $e) {
Db::rollback();
Log::warning('finalizePayoutGrace failed: ' . $e->getMessage(), ['record_id' => $id]);
}
if (!$finalized) {
return;
}
if (GameRecordService::isAutoCreateEnabled()) {
try {
$newPeriodNo = GameRecordService::createNextRecordAfterDraw();
@@ -781,27 +814,41 @@ final class GameLiveService
}
GameHotDataCoordinator::afterGameRecordCommitted($id);
GameRecordStatService::refreshForRecordId($id);
self::publishPublicPeriodFinished($id, $periodNo, $resultNumber);
GameWebSocketEventBus::publish('admin.live.finalized', [
'period_id' => $id,
'period_no' => $periodNo,
'result_number' => $resultNumber,
'runtime_enabled' => GameRecordService::isLiveRuntimeEnabled(),
'maintenance_ui' => !GameRecordService::isLiveRuntimeEnabled()
&& !GameRecordService::hasActiveRecord(),
'server_time' => $now,
]);
self::publishSnapshot(null);
if (!GameRecordService::isLiveRuntimeEnabled()) {
self::voidRemainingOpenRoundsOnMaintenanceAfterFinalize();
GameHotDataRedis::gameRecordRefreshAggregateCaches();
$runtimeEnabled = GameRecordService::isLiveRuntimeEnabled();
$notifyAfterLock = static function () use (
$id,
$periodNo,
$resultNumber,
$now,
$runtimeEnabled,
$newPeriodNo
): void {
self::publishPublicPeriodFinished($id, $periodNo, $resultNumber);
GameWebSocketEventBus::publish('admin.live.finalized', [
'period_id' => $id,
'period_no' => $periodNo,
'result_number' => $resultNumber,
'runtime_enabled' => $runtimeEnabled,
'maintenance_ui' => !$runtimeEnabled && !GameRecordService::hasActiveRecord(),
'server_time' => $now,
]);
self::publishSnapshot(null);
} elseif (is_string($newPeriodNo ?? null) && $newPeriodNo !== '') {
self::publishImmediateBettingTickAfterFinalize();
}
if (!$runtimeEnabled) {
self::voidRemainingOpenRoundsOnMaintenanceAfterFinalize();
GameHotDataRedis::gameRecordRefreshAggregateCaches();
self::publishSnapshot(null);
} elseif (is_string($newPeriodNo ?? null) && $newPeriodNo !== '') {
self::publishImmediateBettingTickAfterFinalize();
}
};
} finally {
GameHotDataLock::release(GameHotDataLock::TYPE_GAME_RECORD, (string) $id, $lock['token'], $lock['redis_lock']);
}
if ($notifyAfterLock !== null) {
$notifyAfterLock();
}
}
public static function tickAutoDraw(): void
@@ -919,7 +966,7 @@ final class GameLiveService
if (!in_array($st, [0, 1], true)) {
return ['ok' => false, 'msg' => __('Current period cannot be voided')];
}
$lock = GameHotDataLock::tryAcquireWithWait(GameHotDataLock::TYPE_GAME_RECORD, (string) $recordId, 3000);
$lock = self::acquireRecordLockForAdminMutation((string) $recordId, 3000);
if (!$lock['acquired']) {
return ['ok' => false, 'msg' => __('Another operation is in progress for this period; please try again later')];
}
@@ -1592,6 +1639,26 @@ final class GameLiveService
return $max;
}
/**
* 管理员作废等操作:等待锁失败时清除残留锁再试一次,避免 ticker/开奖持锁导致无法作废。
*
* @return array{acquired: bool, token: ?string, redis_lock: bool}
*/
private static function acquireRecordLockForAdminMutation(string $recordId, int $waitMs): array
{
if ($recordId === '') {
return ['acquired' => false, 'token' => null, 'redis_lock' => false];
}
$lock = GameHotDataLock::tryAcquireWithWait(GameHotDataLock::TYPE_GAME_RECORD, $recordId, $waitMs);
if ($lock['acquired']) {
return $lock;
}
GameHotDataLock::forceRelease(GameHotDataLock::TYPE_GAME_RECORD, $recordId);
Log::warning('admin record lock force-released before void', ['record_id' => $recordId]);
return GameHotDataLock::tryAcquireWithWait(GameHotDataLock::TYPE_GAME_RECORD, $recordId, 800);
}
/**
* 封盘至本期完全结束前均展示赔付预估(含已开奖/派彩中),供后台实时对局页保留表格数据。
*/