优化游戏实时对局页面

This commit is contained in:
2026-04-21 10:02:16 +08:00
parent 17eadddaa2
commit aad00e10f8
9 changed files with 622 additions and 41 deletions

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace app\common\service;
use app\common\library\game\StreakWinReward;
use app\common\model\UserWalletRecord;
use app\common\service\GameHotDataCoordinator;
use app\common\service\GameHotDataLock;
use support\think\Db;
@@ -98,6 +99,11 @@ final class GameLiveService
&& $elapsed >= $betSeconds
&& $elapsed < $periodSeconds;
$runtimeEnabled = GameRecordService::isLiveRuntimeEnabled();
$hasActiveRound = GameRecordService::hasActiveRecord();
/** 关服且已无进行中局:派彩结束后的「完整维护」态(仅此时展示维护中 UI */
$maintenanceUi = !$runtimeEnabled && !$hasActiveRound;
return [
'record' => $record,
'bets' => array_map(static function (array $row): array {
@@ -123,6 +129,9 @@ final class GameLiveService
'bet_remaining_seconds' => $betRemaining,
'payout_remaining_seconds' => $payoutRemaining,
'is_payout_phase' => $status === 3,
'runtime_enabled' => $runtimeEnabled,
'maintenance_ui' => $maintenanceUi,
/** 关闭游戏(维护)时仍允许完成当局、计算与预约开奖;仅阻止新用户下注与结束后自动开新期 */
'can_calculate' => $canCalculate,
'can_draw' => $canScheduleDraw,
'can_schedule_draw' => $canScheduleDraw,
@@ -135,6 +144,10 @@ final class GameLiveService
*/
private static function emptySnapshotPayload(): array
{
$runtimeEnabled = GameRecordService::isLiveRuntimeEnabled();
$hasActiveRound = GameRecordService::hasActiveRecord();
$maintenanceUi = !$runtimeEnabled && !$hasActiveRound;
return [
'record' => null,
'bets' => [],
@@ -150,6 +163,8 @@ final class GameLiveService
'bet_remaining_seconds' => 0,
'payout_remaining_seconds' => 0,
'is_payout_phase' => false,
'runtime_enabled' => $runtimeEnabled,
'maintenance_ui' => $maintenanceUi,
'can_calculate' => false,
'can_draw' => false,
'can_schedule_draw' => false,
@@ -449,6 +464,151 @@ final class GameLiveService
self::drawResult((int) $record['id'], null);
}
/**
* 作废当前期(仅 status 为下注/封盘):待开奖注单退款,本期置为已作废,并关闭运行开关。
*
* @return array{ok: bool, msg?: string, record?: array|null}
*/
public static function voidCurrentPeriod(?int $recordId, string $voidReason): array
{
$reason = trim($voidReason);
if ($reason === '') {
return ['ok' => false, 'msg' => __('Void reason is required')];
}
if (function_exists('mb_strlen')) {
if (mb_strlen($reason) > 255) {
return ['ok' => false, 'msg' => __('Void reason is too long')];
}
} elseif (strlen($reason) > 255) {
return ['ok' => false, 'msg' => __('Void reason is too long')];
}
if (strlen($reason) < 2) {
return ['ok' => false, 'msg' => __('Void reason is too short')];
}
$record = self::resolveRecord($recordId);
if (!$record) {
return ['ok' => false, 'msg' => __('No active game in progress')];
}
$st = (int) $record['status'];
if (!in_array($st, [0, 1], true)) {
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')];
}
$refundedUserIds = [];
try {
$now = time();
Db::startTrans();
try {
$refundedUserIds = self::refundPendingBetsForPeriodLocked($rid, $now);
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::setLiveRuntimeEnabled(false);
GameHotDataCoordinator::afterGameRecordCommitted($rid);
GameHotDataCoordinator::afterGameConfigKeyCommitted(GameRecordService::KEY_LIVE_RUNTIME);
foreach ($refundedUserIds as $uid) {
if ($uid > 0) {
GameHotDataCoordinator::afterUserCommitted($uid);
}
}
self::publishSnapshot(null);
return [
'ok' => true,
'msg' => __('Period voided'),
'record' => self::reloadRecord($rid),
];
} finally {
GameHotDataLock::release(GameHotDataLock::TYPE_GAME_RECORD, (string) $rid, $lock['token'], $lock['redis_lock']);
}
}
/**
* @return list<int>
*/
private static function refundPendingBetsForPeriodLocked(int $periodId, int $now): array
{
$userIdSet = [];
$bets = Db::name('bet_order')
->where('period_id', $periodId)
->where('status', 1)
->order('id', 'asc')
->select()
->toArray();
foreach ($bets as $bet) {
$betId = (int) ($bet['id'] ?? 0);
$userId = (int) ($bet['user_id'] ?? 0);
$totalRaw = $bet['total_amount'] ?? '0';
$total = is_string($totalRaw) ? $totalRaw : (string) $totalRaw;
if ($betId <= 0) {
continue;
}
if ($userId <= 0 || bccomp($total, '0', 4) <= 0) {
Db::name('bet_order')->where('id', $betId)->where('status', 1)->update([
'status' => 3,
'update_time' => $now,
]);
continue;
}
$before = (string) (Db::name('user')->where('id', $userId)->value('coin') ?? '0');
$after = bcadd($before, $total, 4);
$u = Db::name('user')->where('id', $userId)->where('coin', $before)->update([
'coin' => $after,
'update_time' => $now,
]);
if ($u !== 1) {
throw new \RuntimeException((string) __('Concurrent balance update; please retry'));
}
$bo = Db::name('bet_order')->where('id', $betId)->where('status', 1)->update([
'status' => 3,
'update_time' => $now,
]);
if ($bo !== 1) {
throw new \RuntimeException((string) __('Bet order state changed; please retry'));
}
$channelIdRaw = $bet['channel_id'] ?? null;
$channelId = filter_var($channelIdRaw, FILTER_VALIDATE_INT);
if ($channelId === false) {
$channelId = null;
}
UserWalletRecord::create([
'user_id' => $userId,
'channel_id' => $channelId,
'biz_type' => 'bet_void',
'direction' => 1,
'amount' => $total,
'balance_before' => $before,
'balance_after' => $after,
'ref_type' => 'bet_order',
'remark' => (string) __('Period void refund'),
'create_time' => $now,
]);
$userIdSet[$userId] = true;
}
$out = [];
foreach (array_keys($userIdSet) as $uid) {
$out[] = (int) $uid;
}
return $out;
}
public static function publishSnapshot(?int $recordId = null): void
{
try {
@@ -575,6 +735,9 @@ final class GameLiveService
*/
private static function mapPublicPeriodStatus(int $dbStatus, int $betCloseIn): string
{
if ($dbStatus === 5) {
return 'void';
}
if ($dbStatus === 0) {
return $betCloseIn > 0 ? 'betting' : 'locked';
}