1.修复自动创建下一期bug
This commit is contained in:
@@ -68,6 +68,23 @@ final class GameHotDataLock
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员强制操作前释放可能残留的互斥锁(仅删 key,不校验 token)。
|
||||
*/
|
||||
public static function forceRelease(string $type, string $resourceKey): void
|
||||
{
|
||||
if ($resourceKey === '') {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$client = Redis::connection()->client();
|
||||
if (is_object($client) && method_exists($client, 'del')) {
|
||||
$client->del(self::lockKey($type, $resourceKey));
|
||||
}
|
||||
} catch (Throwable) {
|
||||
}
|
||||
}
|
||||
|
||||
public static function release(string $type, string $resourceKey, ?string $token, bool $redisLock): void
|
||||
{
|
||||
if ($resourceKey === '' || !$redisLock || $token === null || $token === '') {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 封盘至本期完全结束前均展示赔付预估(含已开奖/派彩中),供后台实时对局页保留表格数据。
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user