后台游戏对局实时显示-优化
This commit is contained in:
@@ -55,4 +55,44 @@ class Live extends Backend
|
||||
'event' => 'bet-updated',
|
||||
]);
|
||||
}
|
||||
|
||||
public function calculate(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
if ($request->method() !== 'POST') {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
$recordIdRaw = $request->post('record_id');
|
||||
$recordId = is_numeric((string) $recordIdRaw) ? (int) $recordIdRaw : null;
|
||||
$manualRaw = $request->post('manual_number');
|
||||
$manualNumber = is_numeric((string) $manualRaw) ? (int) $manualRaw : null;
|
||||
$res = GameLiveService::calculateResult($recordId, $manualNumber);
|
||||
if (!($res['ok'] ?? false)) {
|
||||
return $this->error((string) ($res['msg'] ?? '计算失败'));
|
||||
}
|
||||
return $this->success((string) $res['msg'], $res);
|
||||
}
|
||||
|
||||
public function draw(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
if ($request->method() !== 'POST') {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
$recordIdRaw = $request->post('record_id');
|
||||
$recordId = is_numeric((string) $recordIdRaw) ? (int) $recordIdRaw : null;
|
||||
$manualRaw = $request->post('manual_number');
|
||||
$manualNumber = is_numeric((string) $manualRaw) ? (int) $manualRaw : null;
|
||||
$res = GameLiveService::drawResult($recordId, $manualNumber);
|
||||
if (!($res['ok'] ?? false)) {
|
||||
return $this->error((string) ($res['msg'] ?? '开奖失败'));
|
||||
}
|
||||
return $this->success((string) $res['msg'], $res);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
@@ -33,85 +31,39 @@ class Record extends Backend
|
||||
return null;
|
||||
}
|
||||
|
||||
public function recordSettings(WebmanRequest $request): Response
|
||||
public function add(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
if ($request->method() === 'GET') {
|
||||
return $this->success('', GameRecordService::getRecordSettings());
|
||||
return $this->error('游戏对局记录由系统自动生成,禁止后台手工新增');
|
||||
}
|
||||
|
||||
public function edit(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
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('游戏对局记录不可编辑');
|
||||
}
|
||||
return $this->error(__('Parameter error'));
|
||||
$pk = $this->model->getPk();
|
||||
$id = $request->get($pk);
|
||||
$row = $this->model->find($id);
|
||||
if (!$row) {
|
||||
return $this->error(__('Record not found'));
|
||||
}
|
||||
return $this->success('', ['row' => $row]);
|
||||
}
|
||||
|
||||
public function createNextManual(WebmanRequest $request): Response
|
||||
public function del(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'));
|
||||
return $this->error('游戏对局记录不可删除');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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();
|
||||
|
||||
20
app/process/GameLiveTicker.php
Normal file
20
app/process/GameLiveTicker.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace app\process;
|
||||
|
||||
use app\common\service\GameLiveService;
|
||||
use Workerman\Timer;
|
||||
|
||||
/**
|
||||
* 实时对局:按单局时长自动开奖
|
||||
*/
|
||||
class GameLiveTicker
|
||||
{
|
||||
public function onWorkerStart(): void
|
||||
{
|
||||
Timer::add(1, static function (): void {
|
||||
GameLiveService::tickAutoDraw();
|
||||
GameLiveService::publishSnapshot(null);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user