diff --git a/app/admin/controller/game/Live.php b/app/admin/controller/game/Live.php new file mode 100644 index 0000000..6ec7553 --- /dev/null +++ b/app/admin/controller/game/Live.php @@ -0,0 +1,58 @@ +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', + ]); + } +} diff --git a/app/admin/controller/game/Period.php b/app/admin/controller/game/Period.php index 9dfca04..6524c8a 100644 --- a/app/admin/controller/game/Period.php +++ b/app/admin/controller/game/Period.php @@ -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'] ?? '']); } diff --git a/app/admin/controller/game/Record.php b/app/admin/controller/game/Record.php new file mode 100644 index 0000000..00e8c37 --- /dev/null +++ b/app/admin/controller/game/Record.php @@ -0,0 +1,117 @@ + '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')); + } +} diff --git a/app/admin/controller/order/BetOrder.php b/app/admin/controller/order/BetOrder.php index f583b4e..9461f1b 100644 --- a/app/admin/controller/order/BetOrder.php +++ b/app/admin/controller/order/BetOrder.php @@ -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) diff --git a/app/common/model/BetOrder.php b/app/common/model/BetOrder.php index bd6a832..eaba40a 100644 --- a/app/common/model/BetOrder.php +++ b/app/common/model/BetOrder.php @@ -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'); } } diff --git a/app/common/model/GamePeriod.php b/app/common/model/GamePeriod.php index eeb8488..4f907e7 100644 --- a/app/common/model/GamePeriod.php +++ b/app/common/model/GamePeriod.php @@ -6,7 +6,7 @@ use support\think\Model; class GamePeriod extends Model { - protected $name = 'game_period'; + protected $name = 'game_record'; protected $autoWriteTimestamp = true; diff --git a/app/common/model/GameRecord.php b/app/common/model/GameRecord.php new file mode 100644 index 0000000..d7be5ad --- /dev/null +++ b/app/common/model/GameRecord.php @@ -0,0 +1,39 @@ + '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; + } +} diff --git a/app/common/service/GameLiveService.php b/app/common/service/GameLiveService.php new file mode 100644 index 0000000..31436d2 --- /dev/null +++ b/app/common/service/GameLiveService.php @@ -0,0 +1,120 @@ + 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; + } +} diff --git a/app/common/service/GamePeriodService.php b/app/common/service/GamePeriodService.php index fd679cd..00bb553 100644 --- a/app/common/service/GamePeriodService.php +++ b/app/common/service/GamePeriodService.php @@ -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(); } } diff --git a/app/common/service/GameRecordService.php b/app/common/service/GameRecordService.php new file mode 100644 index 0000000..482a9ab --- /dev/null +++ b/app/common/service/GameRecordService.php @@ -0,0 +1,128 @@ +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, + ]); + } +} diff --git a/app/common/validate/GamePeriod.php b/app/common/validate/GamePeriod.php index 2847eb5..3d3732f 100644 --- a/app/common/validate/GamePeriod.php +++ b/app/common/validate/GamePeriod.php @@ -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', diff --git a/app/common/validate/GameRecord.php b/app/common/validate/GameRecord.php new file mode 100644 index 0000000..350ed7d --- /dev/null +++ b/app/common/validate/GameRecord.php @@ -0,0 +1,26 @@ + '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'], + ]; +} diff --git a/app/process/GamePeriodAutoTicker.php b/app/process/GamePeriodAutoTicker.php index 9fc089a..838838a 100644 --- a/app/process/GamePeriodAutoTicker.php +++ b/app/process/GamePeriodAutoTicker.php @@ -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(); }); } } diff --git a/composer.json b/composer.json index 2ca4933..4e78a6d 100644 --- a/composer.json +++ b/composer.json @@ -40,7 +40,8 @@ "nelexa/zip": "^4.0.0", "voku/anti-xss": "^4.1", "topthink/think-validate": "^3.0", - "ext-bcmath": "*" + "ext-bcmath": "*", + "webman/push": "^1.1" }, "suggest": { "ext-event": "For better performance. " diff --git a/config/plugin/webman/push/app.php b/config/plugin/webman/push/app.php new file mode 100644 index 0000000..303f438 --- /dev/null +++ b/config/plugin/webman/push/app.php @@ -0,0 +1,10 @@ + true, + 'websocket' => 'websocket://0.0.0.0:3131', + 'api' => 'http://0.0.0.0:3232', + 'app_key' => '6d0af5971ad191f2dc8a500885cb79c7', + 'app_secret' => 'c457f0be89cd48d481b37f16c0a97f5f', + 'channel_hook' => 'http://127.0.0.1:8787/plugin/webman/push/hook', + 'auth' => '/plugin/webman/push/auth' +]; \ No newline at end of file diff --git a/config/plugin/webman/push/process.php b/config/plugin/webman/push/process.php new file mode 100644 index 0000000..01c545d --- /dev/null +++ b/config/plugin/webman/push/process.php @@ -0,0 +1,21 @@ + [ + 'handler' => Server::class, + 'listen' => config('plugin.webman.push.app.websocket'), + 'count' => 1, // 必须是1 + 'reloadable' => false, // 执行reload不重启 + 'constructor' => [ + 'api_listen' => config('plugin.webman.push.app.api'), + 'app_info' => [ + config('plugin.webman.push.app.app_key') => [ + 'channel_hook' => config('plugin.webman.push.app.channel_hook'), + 'app_secret' => config('plugin.webman.push.app.app_secret'), + ], + ] + ] + ] +]; \ No newline at end of file diff --git a/config/plugin/webman/push/route.php b/config/plugin/webman/push/route.php new file mode 100644 index 0000000..ea54c37 --- /dev/null +++ b/config/plugin/webman/push/route.php @@ -0,0 +1,87 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +use support\Request; +use Webman\Route; +use Webman\Push\Api; + +/** + * 推送js客户端文件 + */ +Route::get('/plugin/webman/push/push.js', function (Request $request) { + return response()->file(base_path().'/vendor/webman/push/src/push.js'); +}); + +/** + * 私有频道鉴权,这里应该使用session辨别当前用户身份,然后确定该用户是否有权限监听channel_name + */ +Route::post(config('plugin.webman.push.app.auth'), function (Request $request) { + $pusher = new Api(str_replace('0.0.0.0', '127.0.0.1', config('plugin.webman.push.app.api')), config('plugin.webman.push.app.app_key'), config('plugin.webman.push.app.app_secret')); + $channel_name = $request->post('channel_name'); + $session = $request->session(); + // 这里应该通过session和channel_name判断当前用户是否有权限监听channel_name + $has_authority = true; + if ($has_authority) { + return response($pusher->socketAuth($channel_name, $request->post('socket_id'))); + } else { + return response('Forbidden', 403); + } +}); + +/** + * 当频道上线以及下线时触发的回调 + * 频道上线:是指某个频道从没有连接在线到有连接在线的事件 + * 频道下线:是指某个频道的所有连接都断开触发的事件 + */ +Route::post(parse_url(config('plugin.webman.push.app.channel_hook'), PHP_URL_PATH), function (Request $request) { + + // 没有x-pusher-signature头视为伪造请求 + if (!$webhook_signature = $request->header('x-pusher-signature')) { + return response('401 Not authenticated', 401); + } + + $body = $request->rawBody(); + + // 计算签名,$app_secret 是双方使用的密钥,是保密的,外部无从得知 + $expected_signature = hash_hmac('sha256', $body, config('plugin.webman.push.app.app_secret'), false); + + // 安全校验,如果签名不一致可能是伪造的请求,返回401状态码 + if ($webhook_signature !== $expected_signature) { + return response('401 Not authenticated', 401); + } + + // 这里存储这上线 下线的channel数据 + $payload = json_decode($body, true); + + $channels_online = $channels_offline = []; + + foreach ($payload['events'] as $event) { + if ($event['name'] === 'channel_added') { + $channels_online[] = $event['channel']; + } else if ($event['name'] === 'channel_removed') { + $channels_offline[] = $event['channel']; + } + } + + // 业务根据需要处理上下线的channel,例如将在线状态写入数据库,通知其它channel等 + // 上线的所有channel + echo 'online channels: ' . implode(',', $channels_online) . "\n"; + // 下线的所有channel + echo 'offline channels: ' . implode(',', $channels_offline) . "\n"; + + return 'OK'; +}); + + + diff --git a/web/src/lang/backend/en/game/betOrder.ts b/web/src/lang/backend/en/game/betOrder.ts index 4b0ed30..7dc7f43 100644 --- a/web/src/lang/backend/en/game/betOrder.ts +++ b/web/src/lang/backend/en/game/betOrder.ts @@ -22,8 +22,8 @@ export default { idempotency_key: 'Idempotency key', create_time: 'Created', update_time: 'Updated', - gamePeriod_period_no: 'Period (relation)', - gamePeriod_status: 'Period status', + gameRecord_period_no: 'Round (relation)', + gameRecord_status: 'Round status', user_username: 'Username', channel_name: 'Channel', } diff --git a/web/src/lang/backend/en/game/live.ts b/web/src/lang/backend/en/game/live.ts new file mode 100644 index 0000000..0f6f383 --- /dev/null +++ b/web/src/lang/backend/en/game/live.ts @@ -0,0 +1,14 @@ +export default { + tip: 'Listen to pushed bet stream in real time and show the AI default number (minimum estimated platform loss).', + current_record: 'Current round', + ai_default_number: 'AI default number', + candidate_title: 'Candidate payout estimates', + number: 'Number', + estimated_loss: 'Estimated payout', + bet_stream_title: 'Realtime bet stream', + bet_id: 'Bet ID', + user_id: 'Player ID', + pick_numbers: 'Pick numbers', + unit_amount: 'Unit amount', + streak_at_bet: 'Streak at bet', +} diff --git a/web/src/lang/backend/en/game/record.ts b/web/src/lang/backend/en/game/record.ts new file mode 100644 index 0000000..25c9304 --- /dev/null +++ b/web/src/lang/backend/en/game/record.ts @@ -0,0 +1,27 @@ +export default { + 'quick Search Fields': 'Round No. / ID', + id: 'ID', + period_no: 'Round No.', + period_start_at: 'Start time', + status: 'Status', + 'status 0': 'Betting open', + 'status 1': 'Closed', + 'status 2': 'Settling', + 'status 3': 'Paying', + 'status 4': 'Ended', + 'status 5': 'Void', + draw_mode: 'Draw mode', + 'draw_mode 0': 'Auto AI', + 'draw_mode 1': 'Manual preset', + preset_number: 'Preset number', + result_number: 'Result number', + void_reason: 'Void reason', + create_time: 'Created', + update_time: 'Updated', + section_auto: 'Auto draw & new round', + auto_create_label: 'Allow auto-create next round', + auto_create_tip: 'When enabled, ticker inserts next round if no active one exists', + manual_create_label: 'Allow manual create next round', + manual_create_tip: 'When enabled, button below can create next round manually', + btn_create_next: 'Create next round (manual)', +} diff --git a/web/src/lang/backend/en/order/betOrder.ts b/web/src/lang/backend/en/order/betOrder.ts index 3ad60bb..ead2c0a 100644 --- a/web/src/lang/backend/en/order/betOrder.ts +++ b/web/src/lang/backend/en/order/betOrder.ts @@ -22,14 +22,14 @@ idempotency_key: 'Idempotency key', create_time: 'Created', update_time: 'Updated', - gamePeriod_period_no: 'Period (relation)', - gamePeriod_status: 'Period status', - 'gamePeriod_status 0': 'Open for betting', - 'gamePeriod_status 1': 'Closed', - 'gamePeriod_status 2': 'Settling tickets', - 'gamePeriod_status 3': 'Paying out', - 'gamePeriod_status 4': 'Finished', - 'gamePeriod_status 5': 'Voided', + gameRecord_period_no: 'Round (relation)', + gameRecord_status: 'Round status', + 'gameRecord_status 0': 'Open for betting', + 'gameRecord_status 1': 'Closed', + 'gameRecord_status 2': 'Settling tickets', + 'gameRecord_status 3': 'Paying out', + 'gameRecord_status 4': 'Finished', + 'gameRecord_status 5': 'Voided', user_username: 'Username', channel_name: 'Channel', } diff --git a/web/src/lang/backend/zh-cn/game/betOrder.ts b/web/src/lang/backend/zh-cn/game/betOrder.ts index 2addd5b..8ffa7a7 100644 --- a/web/src/lang/backend/zh-cn/game/betOrder.ts +++ b/web/src/lang/backend/zh-cn/game/betOrder.ts @@ -22,8 +22,8 @@ export default { idempotency_key: '幂等键', create_time: '创建时间', update_time: '更新时间', - gamePeriod_period_no: '对局期号', - gamePeriod_status: '期状态', + gameRecord_period_no: '对局期号', + gameRecord_status: '期状态', user_username: '用户名', channel_name: '渠道', } diff --git a/web/src/lang/backend/zh-cn/game/live.ts b/web/src/lang/backend/zh-cn/game/live.ts new file mode 100644 index 0000000..68d414c --- /dev/null +++ b/web/src/lang/backend/zh-cn/game/live.ts @@ -0,0 +1,14 @@ +export default { + tip: '实时监听页面推送的压注记录,并展示AI默认最优开奖号码(平台预估亏损最少)', + current_record: '当前对局', + ai_default_number: 'AI默认开奖号码', + candidate_title: '候选号码赔付预估', + number: '号码', + estimated_loss: '预估赔付', + bet_stream_title: '实时压注记录', + bet_id: '注单ID', + user_id: '玩家ID', + pick_numbers: '压注号码', + unit_amount: '单号金额', + streak_at_bet: '下注时连胜', +} diff --git a/web/src/lang/backend/zh-cn/game/record.ts b/web/src/lang/backend/zh-cn/game/record.ts new file mode 100644 index 0000000..c657aba --- /dev/null +++ b/web/src/lang/backend/zh-cn/game/record.ts @@ -0,0 +1,27 @@ +export default { + 'quick Search Fields': '局号/ID', + id: 'ID', + period_no: '局号', + period_start_at: '开始时间', + status: '状态', + 'status 0': '下注开放', + 'status 1': '已封盘', + 'status 2': '算票中', + 'status 3': '派彩中', + 'status 4': '已结束', + 'status 5': '已作废', + draw_mode: '开奖方式', + 'draw_mode 0': '自动AI', + 'draw_mode 1': '手动预设', + preset_number: '预设号码', + result_number: '开奖号码', + void_reason: '作废原因', + create_time: '创建时间', + update_time: '更新时间', + section_auto: '自动开奖与新建对局', + auto_create_label: '允许自动创建下一局', + auto_create_tip: '开启后由后台定时任务在无进行中对局时自动插入新局', + manual_create_label: '允许手动创建下一局', + manual_create_tip: '开启后可在本页使用「手动创建下一局」按钮', + btn_create_next: '手动创建下一局', +} diff --git a/web/src/lang/backend/zh-cn/order/betOrder.ts b/web/src/lang/backend/zh-cn/order/betOrder.ts index 8cd04b8..832ccd2 100644 --- a/web/src/lang/backend/zh-cn/order/betOrder.ts +++ b/web/src/lang/backend/zh-cn/order/betOrder.ts @@ -22,14 +22,14 @@ idempotency_key: '幂等键', create_time: '创建时间', update_time: '更新时间', - gamePeriod_period_no: '对局期号', - gamePeriod_status: '期状态', - 'gamePeriod_status 0': '下注开放', - 'gamePeriod_status 1': '已封盘', - 'gamePeriod_status 2': '算票中', - 'gamePeriod_status 3': '派彩中', - 'gamePeriod_status 4': '已结束', - 'gamePeriod_status 5': '已作废', + gameRecord_period_no: '对局期号', + gameRecord_status: '期状态', + 'gameRecord_status 0': '下注开放', + 'gameRecord_status 1': '已封盘', + 'gameRecord_status 2': '算票中', + 'gameRecord_status 3': '派彩中', + 'gameRecord_status 4': '已结束', + 'gameRecord_status 5': '已作废', user_username: '用户名', channel_name: '渠道', } diff --git a/web/src/views/backend/game/live/index.vue b/web/src/views/backend/game/live/index.vue new file mode 100644 index 0000000..3c99903 --- /dev/null +++ b/web/src/views/backend/game/live/index.vue @@ -0,0 +1,140 @@ + + + + + diff --git a/web/src/views/backend/game/record/index.vue b/web/src/views/backend/game/record/index.vue new file mode 100644 index 0000000..769ee70 --- /dev/null +++ b/web/src/views/backend/game/record/index.vue @@ -0,0 +1,203 @@ + + + + + diff --git a/web/src/views/backend/game/record/popupForm.vue b/web/src/views/backend/game/record/popupForm.vue new file mode 100644 index 0000000..436589c --- /dev/null +++ b/web/src/views/backend/game/record/popupForm.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/web/src/views/backend/order/betOrder/index.vue b/web/src/views/backend/order/betOrder/index.vue index b3d84c3..07436b4 100644 --- a/web/src/views/backend/order/betOrder/index.vue +++ b/web/src/views/backend/order/betOrder/index.vue @@ -68,8 +68,8 @@ const baTable = new baTableClass( operator: 'LIKE', }, { - label: t('order.betOrder.gamePeriod_period_no'), - prop: 'gamePeriod.period_no', + label: t('order.betOrder.gameRecord_period_no'), + prop: 'gameRecord.period_no', align: 'center', minWidth: 160, operatorPlaceholder: t('Fuzzy query'), @@ -77,8 +77,8 @@ const baTable = new baTableClass( render: 'tags', }, { - label: t('order.betOrder.gamePeriod_status'), - prop: 'gamePeriod.status', + label: t('order.betOrder.gameRecord_status'), + prop: 'gameRecord.status', align: 'center', width: 100, operator: 'eq', @@ -93,12 +93,12 @@ const baTable = new baTableClass( '5': 'danger', }, replaceValue: { - '0': t('order.betOrder.gamePeriod_status 0'), - '1': t('order.betOrder.gamePeriod_status 1'), - '2': t('order.betOrder.gamePeriod_status 2'), - '3': t('order.betOrder.gamePeriod_status 3'), - '4': t('order.betOrder.gamePeriod_status 4'), - '5': t('order.betOrder.gamePeriod_status 5'), + '0': t('order.betOrder.gameRecord_status 0'), + '1': t('order.betOrder.gameRecord_status 1'), + '2': t('order.betOrder.gameRecord_status 2'), + '3': t('order.betOrder.gameRecord_status 3'), + '4': t('order.betOrder.gameRecord_status 4'), + '5': t('order.betOrder.gameRecord_status 5'), }, }, {