228 lines
7.4 KiB
PHP
228 lines
7.4 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace app\common\service;
|
||
|
||
use app\common\library\game\GamePeriodNo;
|
||
use support\think\Db;
|
||
use Throwable;
|
||
|
||
final class GameRecordService
|
||
{
|
||
public const KEY_AUTO_CREATE = 'period_auto_create_enabled';
|
||
|
||
public const KEY_MANUAL_CREATE = 'period_manual_create_enabled';
|
||
|
||
private const ACTIVE_STATUSES = [0, 1, 2, 3];
|
||
private const AUTO_CREATE_LOCK_KEY = 'auto-create-next-record';
|
||
|
||
public static function getConfigBool(string $key): bool
|
||
{
|
||
$row = GameHotDataRedis::gameConfigRow($key);
|
||
if (!$row) {
|
||
return false;
|
||
}
|
||
$v = $row['config_value'] ?? '';
|
||
return self::truthyConfigValue($v);
|
||
}
|
||
|
||
/**
|
||
* 是否允许自动创建下一局(读库,避免 Redis 缓存仍为 1 时误开新期)。
|
||
*/
|
||
public static function isAutoCreateEnabled(): bool
|
||
{
|
||
return self::getConfigBoolFromDb(self::KEY_AUTO_CREATE);
|
||
}
|
||
|
||
private static function getConfigBoolFromDb(string $key): bool
|
||
{
|
||
if ($key === '') {
|
||
return false;
|
||
}
|
||
$row = Db::name('game_config')->where('config_key', $key)->find();
|
||
if (!$row) {
|
||
return false;
|
||
}
|
||
return self::truthyConfigValue($row['config_value'] ?? '');
|
||
}
|
||
|
||
private static function truthyConfigValue(mixed $v): bool
|
||
{
|
||
return $v === '1' || $v === 1;
|
||
}
|
||
|
||
public static function getRecordSettings(): array
|
||
{
|
||
return [
|
||
'period_auto_create_enabled' => self::getConfigBool(self::KEY_AUTO_CREATE) ? 1 : 0,
|
||
'period_manual_create_enabled' => self::getConfigBool(self::KEY_MANUAL_CREATE) ? 1 : 0,
|
||
];
|
||
}
|
||
|
||
public static function saveRecordSettings(array $data): void
|
||
{
|
||
$now = time();
|
||
$auto = self::truthyConfigInput($data['period_auto_create_enabled'] ?? null) ? '1' : '0';
|
||
$manual = self::truthyConfigInput($data['period_manual_create_enabled'] ?? null) ? '1' : '0';
|
||
self::upsertConfig(self::KEY_AUTO_CREATE, $auto, 'int', '是否允许定时任务自动创建下一局(全局仅一局)', $now);
|
||
self::upsertConfig(self::KEY_MANUAL_CREATE, $manual, 'int', '是否允许后台手动创建下一局', $now);
|
||
}
|
||
|
||
public static function hasActiveRecord(): bool
|
||
{
|
||
$count = Db::name('game_record')->whereIn('status', self::ACTIVE_STATUSES)->count();
|
||
return $count > 0;
|
||
}
|
||
|
||
public static function tickAutoCreate(): void
|
||
{
|
||
if (!self::isAutoCreateEnabled()) {
|
||
return;
|
||
}
|
||
try {
|
||
self::createNextRecordRowIfNoActive();
|
||
} catch (Throwable) {
|
||
}
|
||
}
|
||
|
||
public static function createNextRecordForManual(): array
|
||
{
|
||
if (!self::getConfigBool(self::KEY_MANUAL_CREATE)) {
|
||
return ['ok' => false, 'msg' => __('Manual create next round is disabled')];
|
||
}
|
||
if (self::hasActiveRecord()) {
|
||
return ['ok' => false, 'msg' => __('There is an unfinished round; cannot create a new one')];
|
||
}
|
||
try {
|
||
$periodNo = self::createNextRecordRow();
|
||
return ['ok' => true, 'msg' => __('New round created'), 'period_no' => $periodNo];
|
||
} catch (Throwable $e) {
|
||
return ['ok' => false, 'msg' => $e->getMessage()];
|
||
}
|
||
}
|
||
|
||
public static function createNextRecordAfterDraw(): ?string
|
||
{
|
||
return self::createNextRecordRowIfNoActive();
|
||
}
|
||
|
||
/**
|
||
* 实时对局页「自动创建下一局」开关(兼容旧命名 runtime)。
|
||
*/
|
||
public static function isLiveRuntimeEnabled(): bool
|
||
{
|
||
return self::isAutoCreateEnabled();
|
||
}
|
||
|
||
public static function setLiveRuntimeEnabled(bool $enabled): void
|
||
{
|
||
self::setAutoCreateEnabled($enabled);
|
||
}
|
||
|
||
/**
|
||
* 重新开启游戏时:若无进行中/未结清对局,则立即创建新一期(与定时任务「无局时自动创建」语义一致,供开关打开时立刻开局)。
|
||
*/
|
||
public static function bootstrapPeriodWhenRuntimeEnabled(): void
|
||
{
|
||
if (!self::isAutoCreateEnabled()) {
|
||
return;
|
||
}
|
||
try {
|
||
self::createNextRecordRowIfNoActive();
|
||
} catch (Throwable) {
|
||
}
|
||
}
|
||
|
||
public static function setAutoCreateEnabled(bool $enabled): void
|
||
{
|
||
$now = time();
|
||
$v = $enabled ? '1' : '0';
|
||
GameHotDataRedis::gameConfigForget(self::KEY_AUTO_CREATE);
|
||
self::upsertConfig(self::KEY_AUTO_CREATE, $v, 'int', '是否允许自动创建下一局(全局仅一局)', $now);
|
||
}
|
||
|
||
/**
|
||
* 派彩结单后插入新一期(调用方须已保证无其它进行中局)。
|
||
*/
|
||
public static function createNextRecordRow(): string
|
||
{
|
||
$created = self::createNextRecordRowIfNoActive(false);
|
||
if ($created === null) {
|
||
throw new \RuntimeException((string) __('There is an unfinished round; cannot create a new one'));
|
||
}
|
||
|
||
return $created;
|
||
}
|
||
|
||
/**
|
||
* 幂等插入下一期:有进行中局则不插入,返回 null。
|
||
*
|
||
* @param bool $requireAutoCreate true=须开启自动开局(定时/派彩后);false=手动创建下一期
|
||
*/
|
||
public static function createNextRecordRowIfNoActive(bool $requireAutoCreate = true): ?string
|
||
{
|
||
if ($requireAutoCreate && !self::isAutoCreateEnabled()) {
|
||
return null;
|
||
}
|
||
$lock = GameHotDataLock::tryAcquireWithWait(GameHotDataLock::TYPE_GAME_RECORD, self::AUTO_CREATE_LOCK_KEY, 1500);
|
||
if (!$lock['acquired']) {
|
||
return null;
|
||
}
|
||
try {
|
||
if (self::hasActiveRecord()) {
|
||
return null;
|
||
}
|
||
$periodNo = self::generatePeriodNo();
|
||
$now = time();
|
||
Db::name('game_record')->insert([
|
||
'period_no' => $periodNo,
|
||
'period_start_at' => $now,
|
||
'status' => 0,
|
||
'draw_mode' => 0,
|
||
'void_reason' => '',
|
||
'create_time' => $now,
|
||
'update_time' => $now,
|
||
]);
|
||
GameHotDataCoordinator::afterGameRecordCommitted(null);
|
||
return $periodNo;
|
||
} finally {
|
||
GameHotDataLock::release(GameHotDataLock::TYPE_GAME_RECORD, self::AUTO_CREATE_LOCK_KEY, $lock['token'], $lock['redis_lock']);
|
||
}
|
||
}
|
||
|
||
private static function generatePeriodNo(): string
|
||
{
|
||
return GamePeriodNo::generateNext();
|
||
}
|
||
|
||
private static function truthyConfigInput(mixed $v): bool
|
||
{
|
||
return $v === 1 || $v === '1' || $v === true;
|
||
}
|
||
|
||
private static function upsertConfig(string $key, string $value, string $valueType, string $remark, int $now): void
|
||
{
|
||
$exists = Db::name('game_config')->where('config_key', $key)->find();
|
||
if ($exists) {
|
||
Db::name('game_config')->where('config_key', $key)->update([
|
||
'config_value' => $value,
|
||
'value_type' => $valueType,
|
||
'remark' => $remark,
|
||
'update_time' => $now,
|
||
]);
|
||
GameHotDataCoordinator::afterGameConfigKeyCommitted($key);
|
||
return;
|
||
}
|
||
Db::name('game_config')->insert([
|
||
'config_key' => $key,
|
||
'config_value' => $value,
|
||
'value_type' => $valueType,
|
||
'remark' => $remark,
|
||
'create_time' => $now,
|
||
'update_time' => $now,
|
||
]);
|
||
GameHotDataCoordinator::afterGameConfigKeyCommitted($key);
|
||
}
|
||
}
|