[游戏管理]游戏实时对局
This commit is contained in:
58
app/admin/controller/game/Live.php
Normal file
58
app/admin/controller/game/Live.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace app\admin\controller\game;
|
||||
|
||||
use app\common\controller\Backend;
|
||||
use app\common\service\GameLiveService;
|
||||
use support\Response;
|
||||
use Webman\Http\Request as WebmanRequest;
|
||||
|
||||
/**
|
||||
* 游戏实时对局
|
||||
*/
|
||||
class Live extends Backend
|
||||
{
|
||||
protected ?object $model = null;
|
||||
|
||||
protected function initController(WebmanRequest $request): ?Response
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function _index(): Response
|
||||
{
|
||||
$recordIdRaw = $this->request ? $this->request->get('record_id') : null;
|
||||
$recordId = is_numeric((string) $recordIdRaw) ? (int) $recordIdRaw : null;
|
||||
return $this->success('', GameLiveService::buildSnapshot($recordId));
|
||||
}
|
||||
|
||||
public function snapshot(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
$recordIdRaw = $request->get('record_id');
|
||||
$recordId = is_numeric((string) $recordIdRaw) ? (int) $recordIdRaw : null;
|
||||
return $this->success('', GameLiveService::buildSnapshot($recordId));
|
||||
}
|
||||
|
||||
public function pushConfig(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$ws = (string) config('plugin.webman.push.app.websocket');
|
||||
$ws = str_replace('websocket://', 'ws://', $ws);
|
||||
$ws = str_replace('0.0.0.0', '127.0.0.1', $ws);
|
||||
|
||||
return $this->success('', [
|
||||
'url' => $ws,
|
||||
'app_key' => (string) config('plugin.webman.push.app.app_key'),
|
||||
'channel' => 'game-live',
|
||||
'event' => 'bet-updated',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace app\admin\controller\game;
|
||||
|
||||
use app\common\controller\Backend;
|
||||
use app\common\service\GamePeriodService;
|
||||
use app\common\service\GameRecordService;
|
||||
use support\Response;
|
||||
use Throwable;
|
||||
use Webman\Http\Request as WebmanRequest;
|
||||
@@ -29,7 +29,7 @@ class Period extends Backend
|
||||
|
||||
protected function initController(WebmanRequest $request): ?Response
|
||||
{
|
||||
$this->model = new \app\common\model\GamePeriod();
|
||||
$this->model = new \app\common\model\GameRecord();
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ class Period extends Backend
|
||||
}
|
||||
$method = $request->method();
|
||||
if ($method === 'GET') {
|
||||
return $this->success('', GamePeriodService::getPeriodSettings());
|
||||
return $this->success('', GameRecordService::getRecordSettings());
|
||||
}
|
||||
if ($method === 'POST') {
|
||||
$data = $request->post();
|
||||
@@ -52,7 +52,7 @@ class Period extends Backend
|
||||
return $this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
try {
|
||||
GamePeriodService::savePeriodSettings($data);
|
||||
GameRecordService::saveRecordSettings($data);
|
||||
} catch (Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
@@ -73,7 +73,7 @@ class Period extends Backend
|
||||
if ($request->method() !== 'POST') {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
$result = GamePeriodService::createNextPeriodForManual();
|
||||
$result = GameRecordService::createNextRecordForManual();
|
||||
if ($result['ok']) {
|
||||
return $this->success($result['msg'], ['period_no' => $result['period_no'] ?? '']);
|
||||
}
|
||||
|
||||
117
app/admin/controller/game/Record.php
Normal file
117
app/admin/controller/game/Record.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace app\admin\controller\game;
|
||||
|
||||
use app\common\controller\Backend;
|
||||
use app\common\service\GameRecordService;
|
||||
use support\Response;
|
||||
use Throwable;
|
||||
use Webman\Http\Request as WebmanRequest;
|
||||
|
||||
/**
|
||||
* 游戏对局记录
|
||||
*/
|
||||
class Record extends Backend
|
||||
{
|
||||
protected ?object $model = null;
|
||||
|
||||
protected string|array $preExcludeFields = ['id', 'create_time', 'update_time'];
|
||||
|
||||
protected string|array $quickSearchField = ['id', 'period_no'];
|
||||
|
||||
protected string|array $defaultSortField = ['id' => 'desc'];
|
||||
|
||||
protected string|array $orderGuarantee = ['id' => 'desc'];
|
||||
|
||||
protected bool $modelValidate = true;
|
||||
|
||||
protected bool $modelSceneValidate = true;
|
||||
|
||||
protected function initController(WebmanRequest $request): ?Response
|
||||
{
|
||||
$this->model = new \app\common\model\GameRecord();
|
||||
return null;
|
||||
}
|
||||
|
||||
public function recordSettings(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
if ($request->method() === 'GET') {
|
||||
return $this->success('', GameRecordService::getRecordSettings());
|
||||
}
|
||||
if ($request->method() === 'POST') {
|
||||
$data = $request->post();
|
||||
if (!is_array($data)) {
|
||||
return $this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
try {
|
||||
GameRecordService::saveRecordSettings($data);
|
||||
} catch (Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
return $this->success(__('Saved successfully'));
|
||||
}
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
|
||||
public function createNextManual(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
if ($request->method() !== 'POST') {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
$result = GameRecordService::createNextRecordForManual();
|
||||
if ($result['ok']) {
|
||||
return $this->success($result['msg'], ['period_no' => $result['period_no'] ?? '']);
|
||||
}
|
||||
return $this->error($result['msg']);
|
||||
}
|
||||
|
||||
protected function _add(): Response
|
||||
{
|
||||
if ($this->request && $this->request->method() === 'POST') {
|
||||
$data = $this->request->post();
|
||||
if (!is_array($data)) {
|
||||
return $this->error(__('Parameter %s can not be empty', ['']));
|
||||
}
|
||||
$data = $this->applyInputFilter($data);
|
||||
$data = $this->excludeFields($data);
|
||||
if (!isset($data['period_start_at']) || $data['period_start_at'] === '' || $data['period_start_at'] === null) {
|
||||
$data['period_start_at'] = time();
|
||||
}
|
||||
if ($this->dataLimit && $this->dataLimitFieldAutoFill) {
|
||||
$data[$this->dataLimitField] = $this->auth->id;
|
||||
}
|
||||
$result = false;
|
||||
$this->model->startTrans();
|
||||
try {
|
||||
if ($this->modelValidate) {
|
||||
$validate = str_replace("\\model\\", "\\validate\\", get_class($this->model));
|
||||
if (class_exists($validate)) {
|
||||
$validate = new $validate();
|
||||
if ($this->modelSceneValidate) {
|
||||
$validate->scene('add');
|
||||
}
|
||||
$validate->check($data);
|
||||
}
|
||||
}
|
||||
$result = $this->model->save($data);
|
||||
$this->model->commit();
|
||||
} catch (Throwable $e) {
|
||||
$this->model->rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
if ($result !== false) {
|
||||
return $this->success(__('Added successfully'));
|
||||
}
|
||||
return $this->error(__('No rows were added'));
|
||||
}
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ class BetOrder extends Backend
|
||||
|
||||
protected string|array $orderGuarantee = ['id' => 'desc'];
|
||||
|
||||
protected array $withJoinTable = ['user', 'channel', 'gamePeriod'];
|
||||
protected array $withJoinTable = ['user', 'channel', 'gameRecord'];
|
||||
|
||||
protected function initController(WebmanRequest $request): ?Response
|
||||
{
|
||||
@@ -89,7 +89,7 @@ class BetOrder extends Backend
|
||||
->visible([
|
||||
'user' => ['username', 'phone'],
|
||||
'channel' => ['name'],
|
||||
'gamePeriod' => ['period_no', 'status'],
|
||||
'gameRecord' => ['period_no', 'status'],
|
||||
])
|
||||
->alias($alias)
|
||||
->where($where)
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use app\common\service\GameLiveService;
|
||||
use support\think\Model;
|
||||
use Throwable;
|
||||
|
||||
class BetOrder extends Model
|
||||
{
|
||||
@@ -34,9 +36,18 @@ class BetOrder extends Model
|
||||
return $this->belongsTo(Channel::class, 'channel_id', 'id');
|
||||
}
|
||||
|
||||
public function gamePeriod(): \think\model\relation\BelongsTo
|
||||
protected static function onAfterInsert($model): void
|
||||
{
|
||||
return $this->belongsTo(GamePeriod::class, 'period_id', 'id');
|
||||
try {
|
||||
$periodId = isset($model['period_id']) ? (int) $model['period_id'] : null;
|
||||
GameLiveService::publishSnapshot($periodId);
|
||||
} catch (Throwable) {
|
||||
}
|
||||
}
|
||||
|
||||
public function gameRecord(): \think\model\relation\BelongsTo
|
||||
{
|
||||
return $this->belongsTo(GameRecord::class, 'period_id', 'id');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ use support\think\Model;
|
||||
|
||||
class GamePeriod extends Model
|
||||
{
|
||||
protected $name = 'game_period';
|
||||
protected $name = 'game_record';
|
||||
|
||||
protected $autoWriteTimestamp = true;
|
||||
|
||||
|
||||
39
app/common/model/GameRecord.php
Normal file
39
app/common/model/GameRecord.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\think\Model;
|
||||
|
||||
class GameRecord extends Model
|
||||
{
|
||||
protected $name = 'game_record';
|
||||
|
||||
protected $autoWriteTimestamp = true;
|
||||
|
||||
protected $type = [
|
||||
'create_time' => 'integer',
|
||||
'update_time' => 'integer',
|
||||
'period_start_at' => 'integer',
|
||||
'status' => 'integer',
|
||||
'draw_mode' => 'integer',
|
||||
'preset_number' => 'integer',
|
||||
'result_number' => 'integer',
|
||||
];
|
||||
|
||||
public function setPeriodStartAtAttr($value, $data = [])
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return 0;
|
||||
}
|
||||
if (is_int($value)) {
|
||||
return $value;
|
||||
}
|
||||
if (is_string($value)) {
|
||||
$t = strtotime($value);
|
||||
if ($t !== false) {
|
||||
return $t;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
120
app/common/service/GameLiveService.php
Normal file
120
app/common/service/GameLiveService.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use support\think\Db;
|
||||
use Throwable;
|
||||
use Webman\Push\Api;
|
||||
|
||||
final class GameLiveService
|
||||
{
|
||||
private const BASE_ODDS = 33;
|
||||
private const CHANNEL = 'game-live';
|
||||
private const EVENT = 'bet-updated';
|
||||
|
||||
public static function buildSnapshot(?int $recordId = null): array
|
||||
{
|
||||
$record = self::resolveRecord($recordId);
|
||||
if (!$record) {
|
||||
return [
|
||||
'record' => null,
|
||||
'bets' => [],
|
||||
'candidate_numbers' => [],
|
||||
'ai_default_number' => null,
|
||||
'server_time' => time(),
|
||||
];
|
||||
}
|
||||
|
||||
$bets = Db::name('bet_order')
|
||||
->where('period_id', (int) $record['id'])
|
||||
->order('id', 'desc')
|
||||
->limit(200)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'record' => $record,
|
||||
'bets' => array_map(static function (array $row): array {
|
||||
return [
|
||||
'id' => (int) $row['id'],
|
||||
'user_id' => (int) $row['user_id'],
|
||||
'period_no' => (string) $row['period_no'],
|
||||
'pick_numbers' => $row['pick_numbers'],
|
||||
'unit_amount' => (string) $row['unit_amount'],
|
||||
'total_amount' => (string) $row['total_amount'],
|
||||
'streak_at_bet' => (int) $row['streak_at_bet'],
|
||||
'create_time' => (int) $row['create_time'],
|
||||
];
|
||||
}, $bets),
|
||||
'candidate_numbers' => $candidates,
|
||||
'ai_default_number' => $bestNumber,
|
||||
'server_time' => time(),
|
||||
];
|
||||
}
|
||||
|
||||
public static function publishSnapshot(?int $recordId = null): void
|
||||
{
|
||||
try {
|
||||
$payload = self::buildSnapshot($recordId);
|
||||
$api = new Api(
|
||||
str_replace('0.0.0.0', '127.0.0.1', (string) config('plugin.webman.push.app.api')),
|
||||
(string) config('plugin.webman.push.app.app_key'),
|
||||
(string) config('plugin.webman.push.app.app_secret')
|
||||
);
|
||||
$api->trigger(self::CHANNEL, self::EVENT, $payload);
|
||||
} catch (Throwable) {
|
||||
}
|
||||
}
|
||||
|
||||
private static function resolveRecord(?int $recordId): ?array
|
||||
{
|
||||
if ($recordId !== null && $recordId > 0) {
|
||||
$row = Db::name('game_record')->where('id', $recordId)->find();
|
||||
if ($row) {
|
||||
return $row;
|
||||
}
|
||||
}
|
||||
return Db::name('game_record')->whereIn('status', [0, 1, 2, 3])->order('id', 'desc')->find();
|
||||
}
|
||||
|
||||
private static function estimateLossForNumber(array $bets, int $number): string
|
||||
{
|
||||
$payout = '0.0000';
|
||||
foreach ($bets as $bet) {
|
||||
$pickNumbers = $bet['pick_numbers'];
|
||||
if (is_string($pickNumbers)) {
|
||||
$decoded = json_decode($pickNumbers, true);
|
||||
$pickNumbers = is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
if (!is_array($pickNumbers)) {
|
||||
$pickNumbers = [];
|
||||
}
|
||||
if (!in_array($number, array_map('intval', $pickNumbers), true)) {
|
||||
continue;
|
||||
}
|
||||
$unit = (string) ($bet['unit_amount'] ?? '0');
|
||||
$streak = (int) ($bet['streak_at_bet'] ?? 0);
|
||||
$odds = (string) (($streak + 1) * self::BASE_ODDS);
|
||||
$orderPayout = bcmul($unit, $odds, 4);
|
||||
$payout = bcadd($payout, $orderPayout, 4);
|
||||
}
|
||||
return $payout;
|
||||
}
|
||||
}
|
||||
@@ -4,145 +4,41 @@ declare(strict_types=1);
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use support\think\Db;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* 全局期号:创建与 game_config 开关(自动新建下一期)
|
||||
* 兼容层:保留旧类名,内部转发到 GameRecordService
|
||||
*/
|
||||
final class GamePeriodService
|
||||
{
|
||||
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];
|
||||
public const KEY_AUTO_CREATE = GameRecordService::KEY_AUTO_CREATE;
|
||||
public const KEY_MANUAL_CREATE = GameRecordService::KEY_MANUAL_CREATE;
|
||||
|
||||
public static function getConfigBool(string $key): bool
|
||||
{
|
||||
$row = Db::name('game_config')->where('config_key', $key)->find();
|
||||
if (!$row) {
|
||||
return false;
|
||||
}
|
||||
$v = $row['config_value'] ?? '';
|
||||
return $v === '1' || $v === 1;
|
||||
return GameRecordService::getConfigBool($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{period_auto_create_enabled: int, period_manual_create_enabled: int}
|
||||
*/
|
||||
public static function getPeriodSettings(): 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,
|
||||
];
|
||||
return GameRecordService::getRecordSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{period_auto_create_enabled?: int|string, period_manual_create_enabled?: int|string} $data
|
||||
*/
|
||||
public static function savePeriodSettings(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);
|
||||
GameRecordService::saveRecordSettings($data);
|
||||
}
|
||||
|
||||
public static function hasActivePeriod(): bool
|
||||
{
|
||||
$count = Db::name('game_period')->whereIn('status', self::ACTIVE_STATUSES)->count();
|
||||
return $count > 0;
|
||||
return GameRecordService::hasActiveRecord();
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动开奖任务调用:开启自动创建且无进行中期号时插入新期
|
||||
*/
|
||||
public static function tickAutoCreate(): void
|
||||
{
|
||||
if (!self::getConfigBool(self::KEY_AUTO_CREATE)) {
|
||||
return;
|
||||
}
|
||||
if (self::hasActivePeriod()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
self::createNextPeriodRow();
|
||||
} catch (Throwable) {
|
||||
// 并发下可能重复,忽略
|
||||
}
|
||||
GameRecordService::tickAutoCreate();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{ok: bool, msg: string, period_no?: string}
|
||||
*/
|
||||
public static function createNextPeriodForManual(): array
|
||||
{
|
||||
if (!self::getConfigBool(self::KEY_MANUAL_CREATE)) {
|
||||
return ['ok' => false, 'msg' => '未开启「手动创建下一期」开关'];
|
||||
}
|
||||
if (self::hasActivePeriod()) {
|
||||
return ['ok' => false, 'msg' => '存在未结束期号,无法新建'];
|
||||
}
|
||||
try {
|
||||
$periodNo = self::createNextPeriodRow();
|
||||
return ['ok' => true, 'msg' => '已创建新期', 'period_no' => $periodNo];
|
||||
} catch (Throwable $e) {
|
||||
return ['ok' => false, 'msg' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
*/
|
||||
private static function createNextPeriodRow(): string
|
||||
{
|
||||
$periodNo = self::generatePeriodNo();
|
||||
$now = time();
|
||||
Db::name('game_period')->insert([
|
||||
'period_no' => $periodNo,
|
||||
'period_start_at' => $now,
|
||||
'status' => 0,
|
||||
'draw_mode' => 0,
|
||||
'void_reason' => '',
|
||||
'create_time' => $now,
|
||||
'update_time' => $now,
|
||||
]);
|
||||
return $periodNo;
|
||||
}
|
||||
|
||||
private static function generatePeriodNo(): string
|
||||
{
|
||||
return date('Ymd-His') . '-' . substr(bin2hex(random_bytes(4)), 0, 8);
|
||||
}
|
||||
|
||||
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,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
Db::name('game_config')->insert([
|
||||
'config_key' => $key,
|
||||
'config_value' => $value,
|
||||
'value_type' => $valueType,
|
||||
'remark' => $remark,
|
||||
'create_time' => $now,
|
||||
'update_time' => $now,
|
||||
]);
|
||||
return GameRecordService::createNextRecordForManual();
|
||||
}
|
||||
}
|
||||
|
||||
128
app/common/service/GameRecordService.php
Normal file
128
app/common/service/GameRecordService.php
Normal file
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
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];
|
||||
|
||||
public static function getConfigBool(string $key): bool
|
||||
{
|
||||
$row = Db::name('game_config')->where('config_key', $key)->find();
|
||||
if (!$row) {
|
||||
return false;
|
||||
}
|
||||
$v = $row['config_value'] ?? '';
|
||||
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::getConfigBool(self::KEY_AUTO_CREATE)) {
|
||||
return;
|
||||
}
|
||||
if (self::hasActiveRecord()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
self::createNextRecordRow();
|
||||
} catch (Throwable) {
|
||||
}
|
||||
}
|
||||
|
||||
public static function createNextRecordForManual(): array
|
||||
{
|
||||
if (!self::getConfigBool(self::KEY_MANUAL_CREATE)) {
|
||||
return ['ok' => false, 'msg' => '未开启「手动创建下一局」开关'];
|
||||
}
|
||||
if (self::hasActiveRecord()) {
|
||||
return ['ok' => false, 'msg' => '存在未结束对局,无法新建'];
|
||||
}
|
||||
try {
|
||||
$periodNo = self::createNextRecordRow();
|
||||
return ['ok' => true, 'msg' => '已创建新对局', 'period_no' => $periodNo];
|
||||
} catch (Throwable $e) {
|
||||
return ['ok' => false, 'msg' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
private static function createNextRecordRow(): string
|
||||
{
|
||||
$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,
|
||||
]);
|
||||
return $periodNo;
|
||||
}
|
||||
|
||||
private static function generatePeriodNo(): string
|
||||
{
|
||||
return date('Ymd-His') . '-' . substr(bin2hex(random_bytes(4)), 0, 8);
|
||||
}
|
||||
|
||||
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,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
Db::name('game_config')->insert([
|
||||
'config_key' => $key,
|
||||
'config_value' => $value,
|
||||
'value_type' => $valueType,
|
||||
'remark' => $remark,
|
||||
'create_time' => $now,
|
||||
'update_time' => $now,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ class GamePeriod extends Validate
|
||||
protected $failException = true;
|
||||
|
||||
protected $rule = [
|
||||
'period_no' => 'require|max:64|unique:game_period',
|
||||
'period_no' => 'require|max:64|unique:game_record',
|
||||
'period_start_at' => 'integer',
|
||||
'status' => 'require|in:0,1,2,3,4,5',
|
||||
'draw_mode' => 'in:0,1',
|
||||
|
||||
26
app/common/validate/GameRecord.php
Normal file
26
app/common/validate/GameRecord.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class GameRecord extends Validate
|
||||
{
|
||||
protected $failException = true;
|
||||
|
||||
protected $rule = [
|
||||
'period_no' => 'require|max:64|unique:game_record',
|
||||
'period_start_at' => 'integer',
|
||||
'status' => 'require|in:0,1,2,3,4,5',
|
||||
'draw_mode' => 'in:0,1',
|
||||
'preset_number' => 'between:1,36',
|
||||
'result_number' => 'between:1,36',
|
||||
];
|
||||
|
||||
protected $scene = [
|
||||
'add' => ['period_no', 'period_start_at', 'status', 'draw_mode', 'preset_number', 'result_number'],
|
||||
'edit' => ['period_start_at', 'status', 'draw_mode', 'preset_number', 'result_number'],
|
||||
];
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace app\process;
|
||||
|
||||
use app\common\service\GamePeriodService;
|
||||
use app\common\service\GameRecordService;
|
||||
use Workerman\Timer;
|
||||
|
||||
/**
|
||||
@@ -13,7 +13,7 @@ class GamePeriodAutoTicker
|
||||
public function onWorkerStart(): void
|
||||
{
|
||||
Timer::add(15, static function (): void {
|
||||
GamePeriodService::tickAutoCreate();
|
||||
GameRecordService::tickAutoCreate();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user