后台游戏对局实时显示-优化

This commit is contained in:
2026-04-16 16:36:57 +08:00
parent c7149e7058
commit 015d1e4d5b
12 changed files with 499 additions and 212 deletions

View File

@@ -13,6 +13,9 @@ final class GameLiveService
private const BASE_ODDS = 33;
private const CHANNEL = 'game-live';
private const EVENT = 'bet-updated';
private const KEY_PERIOD_SECONDS = 'period_seconds';
private const KEY_BET_SECONDS = 'bet_seconds';
private const KEY_PICK_MAX_NUMBER_COUNT = 'pick_max_number_count';
public static function buildSnapshot(?int $recordId = null): array
{
@@ -23,10 +26,25 @@ final class GameLiveService
'bets' => [],
'candidate_numbers' => [],
'ai_default_number' => null,
'calc_number' => null,
'period_seconds' => self::getConfigInt(self::KEY_PERIOD_SECONDS, 30),
'bet_seconds' => self::getConfigInt(self::KEY_BET_SECONDS, 20),
'pick_max_number_count' => self::getPickMaxNumberCount(),
'remaining_seconds' => 0,
'bet_remaining_seconds' => 0,
'can_calculate' => false,
'can_draw' => false,
'server_time' => time(),
];
}
$periodSeconds = self::getConfigInt(self::KEY_PERIOD_SECONDS, 30);
$betSeconds = self::getConfigInt(self::KEY_BET_SECONDS, 20);
$pickMax = self::getPickMaxNumberCount();
$elapsed = max(0, time() - (int) $record['period_start_at']);
$remaining = max(0, $periodSeconds - $elapsed);
$betRemaining = max(0, $betSeconds - $elapsed);
$bets = Db::name('bet_order')
->where('period_id', (int) $record['id'])
->order('id', 'desc')
@@ -37,15 +55,19 @@ final class GameLiveService
$candidates = [];
$bestNumber = null;
$bestLoss = null;
for ($n = 1; $n <= 36; $n++) {
$loss = self::estimateLossForNumber($bets, $n);
$candidates[] = [
'number' => $n,
'estimated_loss' => $loss,
];
if ($bestLoss === null || bccomp((string) $loss, (string) $bestLoss, 4) < 0) {
$bestLoss = $loss;
$bestNumber = $n;
$status = (int) $record['status'];
$canCalculate = $elapsed >= $betSeconds && ($status === 0 || $status === 1);
if ($canCalculate) {
for ($n = 1; $n <= $pickMax; $n++) {
$loss = self::estimateLossForNumber($bets, $n);
$candidates[] = [
'number' => $n,
'estimated_loss' => $loss,
];
if ($bestLoss === null || bccomp((string) $loss, (string) $bestLoss, 4) < 0) {
$bestLoss = $loss;
$bestNumber = $n;
}
}
}
@@ -65,10 +87,134 @@ final class GameLiveService
}, $bets),
'candidate_numbers' => $candidates,
'ai_default_number' => $bestNumber,
'calc_number' => $bestNumber,
'period_seconds' => $periodSeconds,
'bet_seconds' => $betSeconds,
'pick_max_number_count' => $pickMax,
'remaining_seconds' => $remaining,
'bet_remaining_seconds' => $betRemaining,
'can_calculate' => $canCalculate,
'can_draw' => $canCalculate,
'server_time' => time(),
];
}
public static function calculateResult(?int $recordId, ?int $manualNumber = null): array
{
$record = self::resolveRecord($recordId);
if (!$record) {
return ['ok' => false, 'msg' => '未找到进行中的对局'];
}
if (!in_array((int) $record['status'], [0, 1], true)) {
return ['ok' => false, 'msg' => '当前对局状态不可计算'];
}
$periodSeconds = self::getConfigInt(self::KEY_PERIOD_SECONDS, 30);
$betSeconds = self::getConfigInt(self::KEY_BET_SECONDS, 20);
$elapsed = max(0, time() - (int) $record['period_start_at']);
if ($elapsed < $betSeconds) {
return ['ok' => false, 'msg' => '下注开放时长未结束,暂不可计算'];
}
if ((int) $record['status'] === 0) {
Db::name('game_record')->where('id', (int) $record['id'])->update([
'status' => 1,
'update_time' => time(),
]);
$record['status'] = 1;
}
$pickMax = self::getPickMaxNumberCount();
if ($manualNumber !== null && ($manualNumber < 1 || $manualNumber > $pickMax)) {
return ['ok' => false, 'msg' => '手动开奖号码超出允许范围'];
}
$bets = Db::name('bet_order')->where('period_id', (int) $record['id'])->select()->toArray();
$candidates = [];
$bestNumber = null;
$bestLoss = null;
for ($n = 1; $n <= $pickMax; $n++) {
$loss = self::estimateLossForNumber($bets, $n);
$candidates[] = ['number' => $n, 'estimated_loss' => $loss];
if ($bestLoss === null || bccomp((string) $loss, (string) $bestLoss, 4) < 0) {
$bestLoss = $loss;
$bestNumber = $n;
}
}
$finalNumber = $manualNumber ?? $bestNumber;
$finalLoss = '0.0000';
if ($finalNumber !== null) {
$finalLoss = self::estimateLossForNumber($bets, $finalNumber);
}
return [
'ok' => true,
'msg' => '计算完成',
'record' => $record,
'period_seconds' => $periodSeconds,
'bet_seconds' => $betSeconds,
'pick_max_number_count' => $pickMax,
'candidate_numbers' => $candidates,
'ai_default_number' => $bestNumber,
'final_number' => $finalNumber,
'final_estimated_loss' => $finalLoss,
];
}
public static function drawResult(?int $recordId, ?int $manualNumber = null): array
{
$calc = self::calculateResult($recordId, $manualNumber);
if (!($calc['ok'] ?? false)) {
return $calc;
}
$record = $calc['record'];
$finalNumber = (int) $calc['final_number'];
$now = time();
Db::startTrans();
try {
Db::name('game_record')->where('id', (int) $record['id'])->update([
'status' => 4,
'result_number' => $finalNumber,
'draw_mode' => $manualNumber === null ? 0 : 1,
'update_time' => $now,
]);
GameRecordService::createNextRecordAfterDraw();
Db::commit();
} catch (Throwable $e) {
Db::rollback();
return ['ok' => false, 'msg' => $e->getMessage()];
}
self::publishSnapshot(null);
return [
'ok' => true,
'msg' => '开奖完成',
'result_number' => $finalNumber,
'estimated_loss' => $calc['final_estimated_loss'],
];
}
public static function tickAutoDraw(): void
{
$record = self::resolveRecord(null);
if (!$record || !in_array((int) $record['status'], [0, 1], true)) {
return;
}
$betSeconds = self::getConfigInt(self::KEY_BET_SECONDS, 20);
$periodSeconds = self::getConfigInt(self::KEY_PERIOD_SECONDS, 30);
$elapsed = max(0, time() - (int) $record['period_start_at']);
if ($elapsed >= $betSeconds && (int) $record['status'] === 0) {
Db::name('game_record')->where('id', (int) $record['id'])->update([
'status' => 1,
'update_time' => time(),
]);
$record['status'] = 1;
}
if ($elapsed < $periodSeconds) {
return;
}
self::drawResult((int) $record['id'], null);
}
public static function publishSnapshot(?int $recordId = null): void
{
try {
@@ -94,6 +240,34 @@ final class GameLiveService
return Db::name('game_record')->whereIn('status', [0, 1, 2, 3])->order('id', 'desc')->find();
}
private static function getConfigInt(string $key, int $default): int
{
$row = Db::name('game_config')->where('config_key', $key)->find();
if (!$row) {
return $default;
}
$v = $row['config_value'] ?? null;
if ($v === null || $v === '') {
return $default;
}
if (!is_numeric((string) $v)) {
return $default;
}
return (int) $v;
}
private static function getPickMaxNumberCount(): int
{
$max = self::getConfigInt(self::KEY_PICK_MAX_NUMBER_COUNT, 36);
if ($max < 1) {
return 1;
}
if ($max > 36) {
return 36;
}
return $max;
}
private static function estimateLossForNumber(array $bets, int $number): string
{
$payout = '0.0000';

View File

@@ -78,6 +78,14 @@ final class GameRecordService
}
}
public static function createNextRecordAfterDraw(): ?string
{
if (self::hasActiveRecord()) {
return null;
}
return self::createNextRecordRow();
}
private static function createNextRecordRow(): string
{
$periodNo = self::generatePeriodNo();