1.修复关闭自动创建下一局后还自动创建下一期
This commit is contained in:
@@ -144,6 +144,8 @@ class Live extends Backend
|
|||||||
GameRecordService::setAutoCreateEnabled($enabled);
|
GameRecordService::setAutoCreateEnabled($enabled);
|
||||||
if ($enabled) {
|
if ($enabled) {
|
||||||
GameRecordService::bootstrapPeriodWhenRuntimeEnabled();
|
GameRecordService::bootstrapPeriodWhenRuntimeEnabled();
|
||||||
|
} else {
|
||||||
|
GameLiveService::voidOrphanActiveRoundsOnRuntimeDisabled();
|
||||||
}
|
}
|
||||||
return $this->success('', GameLiveService::buildSnapshot(null));
|
return $this->success('', GameLiveService::buildSnapshot(null));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class Game extends MobileBase
|
|||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
$periodRow = GameHotDataRedis::gameRecordLatest();
|
$periodRow = $this->resolveMobilePeriodRow();
|
||||||
$now = time();
|
$now = time();
|
||||||
$startAt = $periodRow ? $this->intValue($periodRow['period_start_at'] ?? 0) : $now;
|
$startAt = $periodRow ? $this->intValue($periodRow['period_start_at'] ?? 0) : $now;
|
||||||
$lockAt = $startAt + 20;
|
$lockAt = $startAt + 20;
|
||||||
@@ -57,6 +57,7 @@ class Game extends MobileBase
|
|||||||
'server_time' => $now,
|
'server_time' => $now,
|
||||||
'runtime_enabled' => GameRecordService::getConfigBool(GameRecordService::KEY_AUTO_CREATE),
|
'runtime_enabled' => GameRecordService::getConfigBool(GameRecordService::KEY_AUTO_CREATE),
|
||||||
'period' => [
|
'period' => [
|
||||||
|
'period_id' => $periodRow ? $this->intValue($periodRow['id'] ?? 0) : 0,
|
||||||
'period_no' => (string) ($periodRow['period_no'] ?? ''),
|
'period_no' => (string) ($periodRow['period_no'] ?? ''),
|
||||||
'status' => $this->mapPeriodStatus($periodRow['status'] ?? null),
|
'status' => $this->mapPeriodStatus($periodRow['status'] ?? null),
|
||||||
'countdown' => $countdown,
|
'countdown' => $countdown,
|
||||||
@@ -110,7 +111,7 @@ class Game extends MobileBase
|
|||||||
if ($response !== null) {
|
if ($response !== null) {
|
||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
$periodRow = GameHotDataRedis::gameRecordLatest();
|
$periodRow = $this->resolveMobilePeriodRow();
|
||||||
if (!$periodRow) {
|
if (!$periodRow) {
|
||||||
return $this->mobileError(2002, 'Game period does not exist');
|
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);
|
$startAt = $this->intValue($periodRow['period_start_at'] ?? 0);
|
||||||
return $this->mobileSuccess([
|
return $this->mobileSuccess([
|
||||||
'runtime_enabled' => GameRecordService::getConfigBool(GameRecordService::KEY_AUTO_CREATE),
|
'runtime_enabled' => GameRecordService::getConfigBool(GameRecordService::KEY_AUTO_CREATE),
|
||||||
'period_id' => $periodRow['id'],
|
'period_id' => $this->intValue($periodRow['id'] ?? 0),
|
||||||
'period_no' => $periodRow['period_no'],
|
'period_no' => $periodRow['period_no'],
|
||||||
'status' => $this->mapPeriodStatus($periodRow['status'] ?? null),
|
'status' => $this->mapPeriodStatus($periodRow['status'] ?? null),
|
||||||
'countdown' => max(0, ($startAt + 30) - $now),
|
'countdown' => max(0, ($startAt + 30) - $now),
|
||||||
@@ -193,6 +194,14 @@ class Game extends MobileBase
|
|||||||
if ($this->intValue($period->status) !== 0) {
|
if ($this->intValue($period->status) !== 0) {
|
||||||
return $this->mobileError(3002, 'Betting is closed');
|
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;
|
$userIdRaw = $this->auth->id ?? null;
|
||||||
$userId = filter_var($userIdRaw, FILTER_VALIDATE_INT);
|
$userId = filter_var($userIdRaw, FILTER_VALIDATE_INT);
|
||||||
@@ -529,6 +538,16 @@ class Game extends MobileBase
|
|||||||
return (string) $value;
|
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
|
private function intValue($value): int
|
||||||
{
|
{
|
||||||
$result = filter_var($value, FILTER_VALIDATE_INT);
|
$result = filter_var($value, FILTER_VALIDATE_INT);
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ final class GameLiveService
|
|||||||
'payout_until' => null,
|
'payout_until' => null,
|
||||||
'update_time' => $now,
|
'update_time' => $now,
|
||||||
]);
|
]);
|
||||||
GameRecordService::createNextRecordAfterDraw();
|
$newPeriodNo = GameRecordService::createNextRecordAfterDraw();
|
||||||
Db::commit();
|
Db::commit();
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
Db::rollback();
|
Db::rollback();
|
||||||
@@ -259,6 +259,11 @@ final class GameLiveService
|
|||||||
GameRecordStatService::refreshForRecordId($recordId);
|
GameRecordStatService::refreshForRecordId($recordId);
|
||||||
} catch (Throwable) {
|
} catch (Throwable) {
|
||||||
}
|
}
|
||||||
|
if (!GameRecordService::isLiveRuntimeEnabled()) {
|
||||||
|
self::voidRemainingOpenRoundsOnMaintenanceAfterFinalize();
|
||||||
|
} elseif (is_string($newPeriodNo ?? null) && $newPeriodNo !== '') {
|
||||||
|
self::publishImmediateBettingTickAfterFinalize();
|
||||||
|
}
|
||||||
self::publishSnapshot(null);
|
self::publishSnapshot(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -689,6 +694,7 @@ final class GameLiveService
|
|||||||
if ($resultNumber === false || $resultNumber < 1) {
|
if ($resultNumber === false || $resultNumber < 1) {
|
||||||
$resultNumber = null;
|
$resultNumber = null;
|
||||||
}
|
}
|
||||||
|
$newPeriodNo = null;
|
||||||
Db::startTrans();
|
Db::startTrans();
|
||||||
try {
|
try {
|
||||||
Db::name('game_record')->where('id', $id)->update([
|
Db::name('game_record')->where('id', $id)->update([
|
||||||
@@ -696,9 +702,7 @@ final class GameLiveService
|
|||||||
'payout_until' => null,
|
'payout_until' => null,
|
||||||
'update_time' => time(),
|
'update_time' => time(),
|
||||||
]);
|
]);
|
||||||
if (GameRecordService::isLiveRuntimeEnabled()) {
|
$newPeriodNo = GameRecordService::createNextRecordAfterDraw();
|
||||||
GameRecordService::createNextRecordRowIfNoActive();
|
|
||||||
}
|
|
||||||
Db::commit();
|
Db::commit();
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
Db::rollback();
|
Db::rollback();
|
||||||
@@ -710,7 +714,11 @@ final class GameLiveService
|
|||||||
GameRecordStatService::refreshForRecordId($id);
|
GameRecordStatService::refreshForRecordId($id);
|
||||||
self::publishPublicPeriodFinished($id, $periodNo, $resultNumber);
|
self::publishPublicPeriodFinished($id, $periodNo, $resultNumber);
|
||||||
self::publishSnapshot(null);
|
self::publishSnapshot(null);
|
||||||
self::publishImmediateBettingTickAfterFinalize();
|
if (!GameRecordService::isLiveRuntimeEnabled()) {
|
||||||
|
self::voidRemainingOpenRoundsOnMaintenanceAfterFinalize();
|
||||||
|
} elseif (is_string($newPeriodNo ?? null) && $newPeriodNo !== '') {
|
||||||
|
self::publishImmediateBettingTickAfterFinalize();
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
GameHotDataLock::release(GameHotDataLock::TYPE_GAME_RECORD, (string) $id, $lock['token'], $lock['redis_lock']);
|
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);
|
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 为下注/封盘):待开奖注单退款,本期置为已作废,并关闭运行开关。
|
* 作废当前期(仅 status 为下注/封盘):待开奖注单退款,本期置为已作废,并关闭运行开关。
|
||||||
*
|
*
|
||||||
@@ -768,50 +896,22 @@ final class GameLiveService
|
|||||||
return ['ok' => false, 'msg' => __('Current period cannot be voided')];
|
return ['ok' => false, 'msg' => __('Current period cannot be voided')];
|
||||||
}
|
}
|
||||||
$rid = (int) $record['id'];
|
$rid = (int) $record['id'];
|
||||||
$lock = GameHotDataLock::tryAcquireWithWait(GameHotDataLock::TYPE_GAME_RECORD, (string) $rid, 3000);
|
$internal = self::voidOpenPeriodInternal($rid, $reason);
|
||||||
if (!$lock['acquired']) {
|
if (!($internal['ok'] ?? false)) {
|
||||||
return ['ok' => false, 'msg' => __('Another operation is in progress for this period; please try again later')];
|
$errMsg = $internal['msg'] ?? null;
|
||||||
|
return ['ok' => false, 'msg' => is_string($errMsg) ? $errMsg : __('Void failed')];
|
||||||
}
|
}
|
||||||
$refundedUserIds = [];
|
GameRecordService::setAutoCreateEnabled(false);
|
||||||
try {
|
GameHotDataCoordinator::afterGameConfigKeyCommitted(GameRecordService::KEY_AUTO_CREATE);
|
||||||
$now = time();
|
self::publishSnapshot(null);
|
||||||
$refund = ['user_ids' => [], 'order_count' => 0, 'total_amount' => '0.00', 'order_ids' => []];
|
$refund = $internal['refund'] ?? null;
|
||||||
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);
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'ok' => true,
|
'ok' => true,
|
||||||
'msg' => __('Period voided'),
|
'msg' => __('Period voided'),
|
||||||
'record' => self::reloadRecord($rid),
|
'record' => self::reloadRecord($rid),
|
||||||
'refund' => $refund,
|
'refund' => is_array($refund) ? $refund : null,
|
||||||
];
|
];
|
||||||
} finally {
|
|
||||||
GameHotDataLock::release(GameHotDataLock::TYPE_GAME_RECORD, (string) $rid, $lock['token'], $lock['redis_lock']);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user