1.修复关闭自动创建下一局后还自动创建下一期

This commit is contained in:
2026-05-26 16:10:06 +08:00
parent 8c3f3c4e2c
commit 66c002f522
3 changed files with 171 additions and 50 deletions

View File

@@ -144,6 +144,8 @@ class Live extends Backend
GameRecordService::setAutoCreateEnabled($enabled);
if ($enabled) {
GameRecordService::bootstrapPeriodWhenRuntimeEnabled();
} else {
GameLiveService::voidOrphanActiveRoundsOnRuntimeDisabled();
}
return $this->success('', GameLiveService::buildSnapshot(null));
}

View File

@@ -30,7 +30,7 @@ class Game extends MobileBase
return $response;
}
$periodRow = GameHotDataRedis::gameRecordLatest();
$periodRow = $this->resolveMobilePeriodRow();
$now = time();
$startAt = $periodRow ? $this->intValue($periodRow['period_start_at'] ?? 0) : $now;
$lockAt = $startAt + 20;
@@ -57,6 +57,7 @@ class Game extends MobileBase
'server_time' => $now,
'runtime_enabled' => GameRecordService::getConfigBool(GameRecordService::KEY_AUTO_CREATE),
'period' => [
'period_id' => $periodRow ? $this->intValue($periodRow['id'] ?? 0) : 0,
'period_no' => (string) ($periodRow['period_no'] ?? ''),
'status' => $this->mapPeriodStatus($periodRow['status'] ?? null),
'countdown' => $countdown,
@@ -110,7 +111,7 @@ class Game extends MobileBase
if ($response !== null) {
return $response;
}
$periodRow = GameHotDataRedis::gameRecordLatest();
$periodRow = $this->resolveMobilePeriodRow();
if (!$periodRow) {
return $this->mobileError(2002, 'Game period does not exist');
}
@@ -118,7 +119,7 @@ class Game extends MobileBase
$startAt = $this->intValue($periodRow['period_start_at'] ?? 0);
return $this->mobileSuccess([
'runtime_enabled' => GameRecordService::getConfigBool(GameRecordService::KEY_AUTO_CREATE),
'period_id' => $periodRow['id'],
'period_id' => $this->intValue($periodRow['id'] ?? 0),
'period_no' => $periodRow['period_no'],
'status' => $this->mapPeriodStatus($periodRow['status'] ?? null),
'countdown' => max(0, ($startAt + 30) - $now),
@@ -193,6 +194,14 @@ class Game extends MobileBase
if ($this->intValue($period->status) !== 0) {
return $this->mobileError(3002, 'Betting is closed');
}
$activeRow = GameHotDataRedis::gameRecordActive();
if ($activeRow !== null) {
$activeNo = trim((string) ($activeRow['period_no'] ?? ''));
$activeId = $this->intValue($activeRow['id'] ?? 0);
if ($activeNo !== '' && ($periodNo !== $activeNo || $this->intValue($period->id) !== $activeId)) {
return $this->mobileError(3004, 'Not the current period; please refresh period_no');
}
}
$userIdRaw = $this->auth->id ?? null;
$userId = filter_var($userIdRaw, FILTER_VALIDATE_INT);
@@ -529,6 +538,16 @@ class Game extends MobileBase
return (string) $value;
}
/**
* 移动端展示的当前期优先进行中局id 最大且 status∈0..3),避免 gameRecordLatest 指向已结束新局而客户端仍用旧 period_no 下注。
*
* @return array<string, mixed>|null
*/
private function resolveMobilePeriodRow(): ?array
{
return GameHotDataRedis::gameRecordActive() ?? GameHotDataRedis::gameRecordLatest();
}
private function intValue($value): int
{
$result = filter_var($value, FILTER_VALIDATE_INT);

View File

@@ -244,7 +244,7 @@ final class GameLiveService
'payout_until' => null,
'update_time' => $now,
]);
GameRecordService::createNextRecordAfterDraw();
$newPeriodNo = GameRecordService::createNextRecordAfterDraw();
Db::commit();
} catch (Throwable $e) {
Db::rollback();
@@ -259,6 +259,11 @@ final class GameLiveService
GameRecordStatService::refreshForRecordId($recordId);
} catch (Throwable) {
}
if (!GameRecordService::isLiveRuntimeEnabled()) {
self::voidRemainingOpenRoundsOnMaintenanceAfterFinalize();
} elseif (is_string($newPeriodNo ?? null) && $newPeriodNo !== '') {
self::publishImmediateBettingTickAfterFinalize();
}
self::publishSnapshot(null);
}
@@ -689,6 +694,7 @@ final class GameLiveService
if ($resultNumber === false || $resultNumber < 1) {
$resultNumber = null;
}
$newPeriodNo = null;
Db::startTrans();
try {
Db::name('game_record')->where('id', $id)->update([
@@ -696,9 +702,7 @@ final class GameLiveService
'payout_until' => null,
'update_time' => time(),
]);
if (GameRecordService::isLiveRuntimeEnabled()) {
GameRecordService::createNextRecordRowIfNoActive();
}
$newPeriodNo = GameRecordService::createNextRecordAfterDraw();
Db::commit();
} catch (Throwable $e) {
Db::rollback();
@@ -710,7 +714,11 @@ final class GameLiveService
GameRecordStatService::refreshForRecordId($id);
self::publishPublicPeriodFinished($id, $periodNo, $resultNumber);
self::publishSnapshot(null);
self::publishImmediateBettingTickAfterFinalize();
if (!GameRecordService::isLiveRuntimeEnabled()) {
self::voidRemainingOpenRoundsOnMaintenanceAfterFinalize();
} elseif (is_string($newPeriodNo ?? null) && $newPeriodNo !== '') {
self::publishImmediateBettingTickAfterFinalize();
}
} finally {
GameHotDataLock::release(GameHotDataLock::TYPE_GAME_RECORD, (string) $id, $lock['token'], $lock['redis_lock']);
}
@@ -737,6 +745,126 @@ final class GameLiveService
self::drawResult((int) $record['id'], null);
}
/**
* 关闭「自动创建下一局」时:作废除当前 draining 局外的其它下注/封盘期(历史重复开局遗留),避免被 tickAutoDraw 当成新一局继续跑。
*/
public static function voidOrphanActiveRoundsOnRuntimeDisabled(): void
{
if (GameRecordService::isLiveRuntimeEnabled()) {
return;
}
$canonical = GameHotDataRedis::gameRecordActive();
if (!$canonical) {
return;
}
$keepId = filter_var($canonical['id'] ?? 0, FILTER_VALIDATE_INT);
if ($keepId === false || $keepId <= 0) {
return;
}
$reason = (string) __('Orphan period closed: auto-create next round is disabled');
$rows = Db::name('game_record')
->whereIn('status', [0, 1])
->where('id', '<>', $keepId)
->order('id', 'asc')
->select()
->toArray();
foreach ($rows as $row) {
$rid = filter_var($row['id'] ?? 0, FILTER_VALIDATE_INT);
if ($rid === false || $rid <= 0) {
continue;
}
self::voidOpenPeriodInternal($rid, $reason);
}
GameHotDataRedis::gameRecordRefreshAggregateCaches();
self::publishSnapshot(null);
}
/**
* 维护模式:本期派彩结单后,清理仍停留在下注/封盘的遗留对局(不插入新期)。
*/
public static function voidRemainingOpenRoundsOnMaintenanceAfterFinalize(): void
{
if (GameRecordService::isLiveRuntimeEnabled()) {
return;
}
$reason = (string) __('Open period closed after payout: game is in maintenance');
$rows = Db::name('game_record')
->whereIn('status', [0, 1])
->order('id', 'asc')
->select()
->toArray();
foreach ($rows as $row) {
$rid = filter_var($row['id'] ?? 0, FILTER_VALIDATE_INT);
if ($rid === false || $rid <= 0) {
continue;
}
self::voidOpenPeriodInternal($rid, $reason);
}
GameHotDataRedis::gameRecordRefreshAggregateCaches();
}
/**
* 作废单期(仅 status 0/1不修改自动开局开关。
*
* @return array{ok: bool, msg?: string}
*/
private static function voidOpenPeriodInternal(int $recordId, string $voidReason): array
{
if ($recordId <= 0) {
return ['ok' => false, 'msg' => __('Parameter error')];
}
$reason = trim($voidReason);
if ($reason === '') {
return ['ok' => false, 'msg' => __('Void reason is required')];
}
$record = Db::name('game_record')->where('id', $recordId)->find();
if (!$record) {
return ['ok' => false, 'msg' => __('No active game in progress')];
}
$st = (int) ($record['status'] ?? -1);
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);
if (!$lock['acquired']) {
return ['ok' => false, 'msg' => __('Another operation is in progress for this period; please try again later')];
}
$refundedUserIds = [];
try {
$now = time();
Db::startTrans();
try {
$refund = self::refundPendingBetsSummaryForPeriodLocked($recordId, $now);
$refundedUserIds = $refund['user_ids'];
Db::name('game_record')->where('id', $recordId)->update([
'status' => 5,
'void_reason' => $reason,
'pending_draw_number' => null,
'payout_until' => null,
'ai_locked_number' => null,
'update_time' => $now,
]);
Db::commit();
} catch (Throwable $e) {
Db::rollback();
return ['ok' => false, 'msg' => __('Void failed') . ': ' . $e->getMessage()];
}
GameHotDataCoordinator::afterGameRecordCommitted($recordId);
foreach ($refundedUserIds as $uid) {
if ($uid > 0) {
GameHotDataCoordinator::afterUserCommitted($uid);
}
}
return [
'ok' => true,
'msg' => __('Period voided'),
'refund' => $refund,
];
} finally {
GameHotDataLock::release(GameHotDataLock::TYPE_GAME_RECORD, (string) $recordId, $lock['token'], $lock['redis_lock']);
}
}
/**
* 作废当前期(仅 status 为下注/封盘):待开奖注单退款,本期置为已作废,并关闭运行开关。
*
@@ -768,50 +896,22 @@ final class GameLiveService
return ['ok' => false, 'msg' => __('Current period cannot be voided')];
}
$rid = (int) $record['id'];
$lock = GameHotDataLock::tryAcquireWithWait(GameHotDataLock::TYPE_GAME_RECORD, (string) $rid, 3000);
if (!$lock['acquired']) {
return ['ok' => false, 'msg' => __('Another operation is in progress for this period; please try again later')];
$internal = self::voidOpenPeriodInternal($rid, $reason);
if (!($internal['ok'] ?? false)) {
$errMsg = $internal['msg'] ?? null;
return ['ok' => false, 'msg' => is_string($errMsg) ? $errMsg : __('Void failed')];
}
$refundedUserIds = [];
try {
$now = time();
$refund = ['user_ids' => [], 'order_count' => 0, 'total_amount' => '0.00', 'order_ids' => []];
Db::startTrans();
try {
$refund = self::refundPendingBetsSummaryForPeriodLocked($rid, $now);
$refundedUserIds = $refund['user_ids'];
Db::name('game_record')->where('id', $rid)->update([
'status' => 5,
'void_reason' => $reason,
'pending_draw_number' => null,
'payout_until' => null,
'ai_locked_number' => null,
'update_time' => $now,
]);
Db::commit();
} catch (Throwable $e) {
Db::rollback();
return ['ok' => false, 'msg' => __('Void failed') . ': ' . $e->getMessage()];
}
GameRecordService::setAutoCreateEnabled(false);
GameHotDataCoordinator::afterGameRecordCommitted($rid);
GameHotDataCoordinator::afterGameConfigKeyCommitted(GameRecordService::KEY_AUTO_CREATE);
foreach ($refundedUserIds as $uid) {
if ($uid > 0) {
GameHotDataCoordinator::afterUserCommitted($uid);
}
}
self::publishSnapshot(null);
GameRecordService::setAutoCreateEnabled(false);
GameHotDataCoordinator::afterGameConfigKeyCommitted(GameRecordService::KEY_AUTO_CREATE);
self::publishSnapshot(null);
$refund = $internal['refund'] ?? null;
return [
'ok' => true,
'msg' => __('Period voided'),
'record' => self::reloadRecord($rid),
'refund' => $refund,
];
} finally {
GameHotDataLock::release(GameHotDataLock::TYPE_GAME_RECORD, (string) $rid, $lock['token'], $lock['redis_lock']);
}
return [
'ok' => true,
'msg' => __('Period voided'),
'record' => self::reloadRecord($rid),
'refund' => is_array($refund) ? $refund : null,
];
}
/**