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

This commit is contained in:
2026-05-26 18:30:19 +08:00
parent bdd66f7bd9
commit bb5ef82d49
7 changed files with 169 additions and 69 deletions

View File

@@ -641,60 +641,103 @@ final class GameLiveService
$now = time();
$drawCommitted = false;
Db::startTrans();
try {
$record = self::loadRecordRowFromDb($rid, true);
if (!$record) {
Db::rollback();
$result = ['ok' => false, 'msg' => __('No active game in progress')];
} else {
$txResult = self::withShortInnodbLockWait(3, static function () use (
$rid,
$betSeconds,
$periodSeconds,
$manualNumber,
$now
): array {
Db::startTrans();
try {
$record = self::loadRecordRowFromDb($rid, true);
if (!$record) {
Db::rollback();
return ['ok' => false, 'draw_committed' => false, 'result' => ['ok' => false, 'msg' => __('No active game in progress')]];
}
$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'] : '';
$postLockWork = static function () use ($rid): void {
GameHotDataCoordinator::afterGameRecordCommitted($rid);
self::publishSnapshot($rid, false);
};
$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,
]);
Db::commit();
$drawCommitted = true;
}
return [
'ok' => true,
'draw_committed' => false,
'existing_result' => $existingResult,
'record' => $record,
];
}
if (!in_array($st, [0, 1], true)) {
Db::rollback();
return ['ok' => false, 'draw_committed' => false, 'result' => ['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, 'draw_committed' => false, 'result' => ['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,
]);
Db::commit();
return [
'ok' => true,
'draw_committed' => true,
'final_number' => $finalNumber,
'draw_mode' => $drawMode,
'final_loss' => $finalLoss,
'payout_until' => $payoutUntil,
'period_no' => $periodNo,
];
} catch (Throwable $e) {
Db::rollback();
return [
'ok' => false,
'draw_committed' => false,
'result' => ['ok' => false, 'msg' => __('Game live: settlement error') . ': ' . $e->getMessage()],
];
}
} catch (Throwable $e) {
Db::rollback();
$result = ['ok' => false, 'msg' => __('Game live: settlement error') . ': ' . $e->getMessage()];
});
if (!($txResult['ok'] ?? false)) {
$result = is_array($txResult['result'] ?? null) ? $txResult['result'] : ['ok' => false, 'msg' => __('Game live: settlement error')];
} elseif (!empty($txResult['draw_committed'])) {
$drawCommitted = true;
$finalNumber = (int) ($txResult['final_number'] ?? 0);
$drawMode = (int) ($txResult['draw_mode'] ?? 0);
$finalLoss = is_string($txResult['final_loss'] ?? null) ? $txResult['final_loss'] : '0.00';
$payoutUntil = (int) ($txResult['payout_until'] ?? 0);
$periodNo = is_string($txResult['period_no'] ?? null) ? (string) $txResult['period_no'] : '';
} else {
$existingResult = (int) ($txResult['existing_result'] ?? 0);
$periodNo = is_string($txResult['record']['period_no'] ?? null) ? (string) $txResult['record']['period_no'] : '';
$postLockWork = static function () use ($rid): void {
GameHotDataCoordinator::afterGameRecordCommitted($rid);
self::publishSnapshot($rid, false);
};
$result = [
'ok' => true,
'msg' => __('Draw completed; paying out'),
'result_number' => $existingResult,
'estimated_loss' => '0.00',
'payout_until' => (int) ($txResult['record']['payout_until'] ?? 0),
];
}
if ($drawCommitted) {
@@ -882,34 +925,33 @@ final class GameLiveService
return;
}
$rid = (int) $record['id'];
if (GameHotDataRedis::isStaleOpenPeriodRecord($record, $periodSeconds)) {
GameHotDataLock::forceRelease(GameHotDataLock::TYPE_GAME_RECORD, (string) $rid);
$st = (int) ($record['status'] ?? 0);
$abnormalAfter = $periodSeconds + self::getPayoutGraceSeconds() + self::STARTUP_RECOVER_GRACE_SECONDS;
if ($elapsed > $abnormalAfter) {
$resultNumber = isset($record['result_number']) ? (int) $record['result_number'] : 0;
if ($resultNumber <= 0) {
GameHotDataLock::forceRelease(GameHotDataLock::TYPE_GAME_RECORD, (string) $rid);
self::markAbnormalAndRefundOnStartup($rid, $st);
}
return;
}
$out = self::drawResult($rid, null);
if ($out['ok'] ?? false) {
return;
}
$msg = is_string($out['msg'] ?? null) ? (string) $out['msg'] : '';
if (!str_contains($msg, 'Another operation is in progress')) {
if (
!str_contains($msg, 'Another operation is in progress')
&& !str_contains($msg, 'Lock wait timeout')
&& !str_contains($msg, '1205')
) {
Log::warning('tickAutoDraw: drawResult failed', [
'record_id' => $rid,
'period_no' => $record['period_no'] ?? '',
'msg' => $msg,
]);
return;
}
if (!GameHotDataRedis::isStaleOpenPeriodRecord($record, $periodSeconds)) {
return;
}
GameHotDataLock::forceRelease(GameHotDataLock::TYPE_GAME_RECORD, (string) $rid);
$retry = self::drawResult($rid, null);
if (!($retry['ok'] ?? false)) {
Log::warning('tickAutoDraw: drawResult failed after lock force-release', [
'record_id' => $rid,
'period_no' => $record['period_no'] ?? '',
'msg' => is_string($retry['msg'] ?? null) ? $retry['msg'] : '',
]);
}
}
@@ -1871,4 +1913,28 @@ final class GameLiveService
}
return $parsed;
}
/**
* 开奖事务使用较短行锁等待,避免 HTTP/定时任务被 InnoDB 默认 50s 锁等待拖死。
*
* @template T
* @param callable(): T $fn
* @return T
*/
private static function withShortInnodbLockWait(int $seconds, callable $fn): mixed
{
$seconds = max(1, min(50, $seconds));
try {
Db::execute('SET SESSION innodb_lock_wait_timeout = ' . $seconds);
} catch (Throwable) {
}
try {
return $fn();
} finally {
try {
Db::execute('SET SESSION innodb_lock_wait_timeout = 50');
} catch (Throwable) {
}
}
}
}