640 lines
24 KiB
PHP
640 lines
24 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace app\api\controller;
|
||
|
||
use app\common\library\game\BetChips;
|
||
use app\common\library\game\GamePeriodNo;
|
||
use app\common\library\game\StreakWinReward;
|
||
use app\common\library\game\ZiHuaDictionary;
|
||
use app\common\model\BetOrder;
|
||
use app\common\model\GameRecord;
|
||
use app\common\model\UserWalletRecord;
|
||
use app\common\service\GameHotDataCoordinator;
|
||
use app\common\service\GameHotDataRedis;
|
||
use app\common\service\GameRecordService;
|
||
use app\common\service\GameWebSocketEventBus;
|
||
use app\common\service\GameWebSocketPayloadHelper;
|
||
use support\think\Db;
|
||
use Webman\Http\Request;
|
||
use support\Response;
|
||
|
||
class Game extends MobileBase
|
||
{
|
||
protected array $noNeedLogin = ['dictionaryList', 'periodHistory'];
|
||
|
||
public function lobbyInit(Request $request): Response
|
||
{
|
||
$response = $this->initializeMobile($request);
|
||
if ($response !== null) {
|
||
return $response;
|
||
}
|
||
|
||
$periodRow = $this->resolveMobilePeriodRow();
|
||
$now = time();
|
||
$startAt = $periodRow ? $this->intValue($periodRow['period_start_at'] ?? 0) : $now;
|
||
$lockAt = $startAt + 20;
|
||
$openAt = $startAt + 22;
|
||
$countdown = $periodRow ? max(0, ($startAt + 30) - $now) : 0;
|
||
|
||
$dictionaryConfig = GameHotDataRedis::gameConfigRow(ZiHuaDictionary::CONFIG_KEY) ?? [];
|
||
$dictionaryItems = ZiHuaDictionary::parseFromConfigValue($dictionaryConfig['config_value'] ?? null);
|
||
$items = [];
|
||
foreach ($dictionaryItems as $row) {
|
||
$items[] = [
|
||
'number' => $row['no'],
|
||
'name' => $row['name'],
|
||
'category' => $row['category'],
|
||
'icon' => '',
|
||
];
|
||
}
|
||
|
||
$user = $this->auth->getUser();
|
||
$currentStreakRaw = $user->current_streak ?? 0;
|
||
$currentStreakParsed = filter_var($currentStreakRaw, FILTER_VALIDATE_INT);
|
||
$currentStreak = $currentStreakParsed === false ? 0 : $currentStreakParsed;
|
||
return $this->mobileSuccess([
|
||
'server_time' => $now,
|
||
'runtime_enabled' => GameRecordService::getConfigBool(GameRecordService::KEY_AUTO_CREATE),
|
||
'period' => [
|
||
'period_id' => $periodRow ? $this->intValue($periodRow['id'] ?? 0) : 0,
|
||
'period_no' => GamePeriodNo::toDisplay(
|
||
(string) ($periodRow['period_no'] ?? ''),
|
||
$periodRow ? $this->intValue($periodRow['id'] ?? 0) : 0
|
||
),
|
||
'status' => $this->mapPeriodStatus($periodRow['status'] ?? null),
|
||
'countdown' => $countdown,
|
||
'lock_at' => $lockAt,
|
||
'open_at' => $openAt,
|
||
],
|
||
'bet_config' => array_merge(
|
||
[
|
||
'pick_max_number_count' => $this->getPickMaxNumberCount(),
|
||
'min_bet_per_number' => $this->getConfigValue('min_bet_per_number', '0.0100'),
|
||
'max_bet_per_number' => $this->getConfigValue('max_bet_per_number', '10000.0000'),
|
||
],
|
||
BetChips::lobbyChipsPayload()
|
||
),
|
||
'dictionary' => $items,
|
||
'user_snapshot' => array_merge(
|
||
[
|
||
'coin' => $user->coin,
|
||
],
|
||
StreakWinReward::playerBetOddsForCurrentStreak($currentStreak)
|
||
),
|
||
]);
|
||
}
|
||
|
||
public function dictionaryList(Request $request): Response
|
||
{
|
||
$response = $this->initializeMobile($request);
|
||
if ($response !== null) {
|
||
return $response;
|
||
}
|
||
$dictionaryConfig = GameHotDataRedis::gameConfigRow(ZiHuaDictionary::CONFIG_KEY) ?? [];
|
||
$dictionaryItems = ZiHuaDictionary::parseFromConfigValue($dictionaryConfig['config_value'] ?? null);
|
||
$items = [];
|
||
foreach ($dictionaryItems as $row) {
|
||
$items[] = [
|
||
'number' => $row['no'],
|
||
'name' => $row['name'],
|
||
'category' => $row['category'],
|
||
'icon' => '',
|
||
];
|
||
}
|
||
return $this->mobileSuccess([
|
||
'version' => (string) ($dictionaryConfig['update_time'] ?? '1'),
|
||
'items' => $items,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 获取最近开奖记录(默认最新 30 条),期号为纯数字。
|
||
*/
|
||
public function periodHistory(Request $request): Response
|
||
{
|
||
$response = $this->initializeMobile($request);
|
||
if ($response !== null) {
|
||
return $response;
|
||
}
|
||
$limit = $this->intValue($request->input('limit', 30));
|
||
if ($limit < 1) {
|
||
$limit = 30;
|
||
}
|
||
if ($limit > 100) {
|
||
$limit = 100;
|
||
}
|
||
$list = GameRecord::whereNotNull('result_number')->order('id', 'desc')->limit($limit)->select();
|
||
$rows = [];
|
||
foreach ($list as $item) {
|
||
$periodId = $this->intValue($item->id ?? 0);
|
||
$rows[] = [
|
||
'period_no' => GamePeriodNo::toDisplay((string) ($item->period_no ?? ''), $periodId),
|
||
'result_number' => $item->result_number,
|
||
'open_time' => $item->update_time,
|
||
];
|
||
}
|
||
|
||
return $this->mobileSuccess(['list' => $rows]);
|
||
}
|
||
|
||
public function periodCurrent(Request $request): Response
|
||
{
|
||
$response = $this->initializeMobile($request);
|
||
if ($response !== null) {
|
||
return $response;
|
||
}
|
||
$periodRow = $this->resolveMobilePeriodRow();
|
||
if (!$periodRow) {
|
||
return $this->mobileError(2002, 'Game period does not exist');
|
||
}
|
||
$now = time();
|
||
$startAt = $this->intValue($periodRow['period_start_at'] ?? 0);
|
||
return $this->mobileSuccess([
|
||
'runtime_enabled' => GameRecordService::getConfigBool(GameRecordService::KEY_AUTO_CREATE),
|
||
'period_id' => $this->intValue($periodRow['id'] ?? 0),
|
||
'period_no' => GamePeriodNo::toDisplay(
|
||
(string) ($periodRow['period_no'] ?? ''),
|
||
$this->intValue($periodRow['id'] ?? 0)
|
||
),
|
||
'status' => $this->mapPeriodStatus($periodRow['status'] ?? null),
|
||
'countdown' => max(0, ($startAt + 30) - $now),
|
||
'bet_close_in' => max(0, ($startAt + 20) - $now),
|
||
'result_number' => $periodRow['result_number'] ?? null,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 兼容旧路由:/api/game/betPlace
|
||
* 与 placeBet 一致:须传筹码标识 bet_id(1–6),单注金额取自后台 game_config 的 bet_chips。
|
||
*/
|
||
public function betPlace(Request $request): Response
|
||
{
|
||
return $this->placeBet($request);
|
||
}
|
||
|
||
/**
|
||
* 提交下注:入参为 period_no + numbers + bet_id(1–6,对应 lobbyInit.bet_config.chips 的键)+ idempotency_key。
|
||
*/
|
||
public function placeBet(Request $request): Response
|
||
{
|
||
$response = $this->initializeMobile($request);
|
||
if ($response !== null) {
|
||
return $response;
|
||
}
|
||
$periodNo = trim((string) $request->post('period_no', ''));
|
||
$numbersRaw = $request->post('numbers', '');
|
||
$idempotencyKey = trim((string) $request->post('idempotency_key', ''));
|
||
$chipPick = $this->resolveBetChipFromRequest($request);
|
||
if (isset($chipPick['error'])) {
|
||
return $chipPick['error'];
|
||
}
|
||
$betChipId = $chipPick['bet_id'];
|
||
$singleBetAmount = $chipPick['amount'];
|
||
if ($periodNo === '' || $idempotencyKey === '') {
|
||
return $this->mobileError(1001, 'Missing parameters');
|
||
}
|
||
if (!is_numeric($singleBetAmount) || bccomp($singleBetAmount, '0', 2) <= 0) {
|
||
return $this->mobileError(1003, 'Invalid parameter value');
|
||
}
|
||
$numbers = $this->parseBetNumbersFromRequest($numbersRaw);
|
||
if ($numbers === []) {
|
||
return $this->mobileError(1003, 'Invalid parameter value');
|
||
}
|
||
$maxSelect = $this->getPickMaxNumberCount();
|
||
if (count($numbers) > $maxSelect) {
|
||
return $this->mobileError(1003, 'Invalid parameter value');
|
||
}
|
||
$singleAmount = bcadd($singleBetAmount, '0', 2);
|
||
$minPer = trim((string) $this->getConfigValue('min_bet_per_number', '0.0100'));
|
||
$maxPer = trim((string) $this->getConfigValue('max_bet_per_number', '10000.0000'));
|
||
if (!is_numeric($minPer) || bccomp($minPer, '0', 4) <= 0) {
|
||
$minPer = '0.0100';
|
||
}
|
||
if (!is_numeric($maxPer) || bccomp($maxPer, $minPer, 4) < 0) {
|
||
$maxPer = '10000.0000';
|
||
}
|
||
if (bccomp($singleAmount, $minPer, 4) < 0 || bccomp($singleAmount, $maxPer, 4) > 0) {
|
||
return $this->mobileError(1003, 'Bet amount out of range');
|
||
}
|
||
$numberCount = (string) count($numbers);
|
||
$totalAmount = bcmul($singleAmount, $numberCount, 2);
|
||
|
||
if (!GameRecordService::getConfigBool(GameRecordService::KEY_AUTO_CREATE)) {
|
||
return $this->mobileError(3001, 'Game is paused');
|
||
}
|
||
$period = GameRecord::where('period_no', $periodNo)->find();
|
||
if (!$period && ctype_digit($periodNo)) {
|
||
$periodIdLookup = filter_var($periodNo, FILTER_VALIDATE_INT);
|
||
if ($periodIdLookup !== false && $periodIdLookup > 0) {
|
||
$period = GameRecord::find($periodIdLookup);
|
||
}
|
||
}
|
||
if (!$period) {
|
||
return $this->mobileError(2002, 'Game period does not exist');
|
||
}
|
||
if ($this->intValue($period->status) !== 0) {
|
||
return $this->mobileError(3002, 'Betting is closed');
|
||
}
|
||
$activeRow = GameHotDataRedis::gameRecordActive();
|
||
if ($activeRow !== null) {
|
||
$activeNo = trim((string) ($activeRow['period_no'] ?? ''));
|
||
$activeId = $this->intValue($activeRow['id'] ?? 0);
|
||
if ($activeNo !== '' && ($periodNo !== $activeNo || $this->intValue($period->id) !== $activeId)) {
|
||
return $this->mobileError(3004, 'Not the current period; please refresh period_no');
|
||
}
|
||
}
|
||
|
||
$userIdRaw = $this->auth->id ?? null;
|
||
$userId = filter_var($userIdRaw, FILTER_VALIDATE_INT);
|
||
if ($userId === false || $userId <= 0) {
|
||
return $this->mobileError(1001, 'Missing parameters');
|
||
}
|
||
|
||
$user = $this->auth->getUser();
|
||
if (bccomp((string) $user->coin, $totalAmount, 2) < 0) {
|
||
return $this->mobileError(2001, 'Insufficient balance');
|
||
}
|
||
|
||
$exists = BetOrder::where('idempotency_key', $idempotencyKey)->find();
|
||
if ($exists) {
|
||
return $this->mobileError(3003, 'Duplicate request');
|
||
}
|
||
|
||
$lock = GameHotDataRedis::userAdminMutationLockTry($userId);
|
||
if (!$lock['acquired']) {
|
||
return $this->mobileError(5000, (string) __('This user is being operated by another admin (wallet/concurrent save); please try again later'));
|
||
}
|
||
|
||
try {
|
||
$coinRow = Db::name('user')->where('id', $userId)->field(['coin', 'channel_id', 'current_streak'])->find();
|
||
if (!$coinRow) {
|
||
return $this->mobileError(5000, 'System is busy, please try again later');
|
||
}
|
||
$before = (string) ($coinRow['coin'] ?? '0');
|
||
if (bccomp($before, $totalAmount, 2) < 0) {
|
||
return $this->mobileError(2001, 'Insufficient balance');
|
||
}
|
||
|
||
$existsLocked = BetOrder::where('idempotency_key', $idempotencyKey)->find();
|
||
if ($existsLocked) {
|
||
return $this->mobileError(3003, 'Duplicate request');
|
||
}
|
||
|
||
$channelIdRaw = $coinRow['channel_id'] ?? null;
|
||
$channelId = filter_var($channelIdRaw, FILTER_VALIDATE_INT);
|
||
if ($channelId === false) {
|
||
$channelId = null;
|
||
}
|
||
|
||
$now = time();
|
||
$after = bcsub($before, $totalAmount, 2);
|
||
$orderNo = 'BO' . date('YmdHis') . substr(str_replace('.', '', uniqid('', true)), -6);
|
||
$streakAtBet = (int) ($coinRow['current_streak'] ?? 0);
|
||
|
||
Db::startTrans();
|
||
try {
|
||
$affected = Db::name('user')->where('id', $userId)->where('coin', $before)->update([
|
||
'coin' => $after,
|
||
'update_time' => $now,
|
||
]);
|
||
if ($affected !== 1) {
|
||
Db::rollback();
|
||
return $this->mobileError(5000, (string) __('Debit failed: user balance changed by another request; please refresh and retry'));
|
||
}
|
||
|
||
UserWalletRecord::create([
|
||
'user_id' => $userId,
|
||
'channel_id' => $channelId,
|
||
'biz_type' => 'bet',
|
||
'direction' => 2,
|
||
'amount' => $totalAmount,
|
||
'balance_before' => $before,
|
||
'balance_after' => $after,
|
||
'ref_type' => 'bet_order',
|
||
'remark' => '移动端下注',
|
||
'create_time' => $now,
|
||
]);
|
||
BetOrder::create([
|
||
'period_id' => $period->id,
|
||
'period_no' => $period->period_no,
|
||
'user_id' => $userId,
|
||
'channel_id' => $channelId,
|
||
'pick_numbers' => $numbers,
|
||
'total_amount' => $totalAmount,
|
||
'streak_at_bet' => $streakAtBet,
|
||
'is_auto' => 0,
|
||
'status' => 1,
|
||
'idempotency_key' => $idempotencyKey,
|
||
'create_time' => $now,
|
||
'update_time' => $now,
|
||
]);
|
||
Db::commit();
|
||
} catch (\Throwable $e) {
|
||
Db::rollback();
|
||
return $this->mobileError(5000, 'System is busy, please try again later', ['detail' => $e->getMessage()]);
|
||
}
|
||
|
||
GameHotDataCoordinator::afterUserCommitted($userId);
|
||
GameWebSocketEventBus::publish('bet.accepted', GameWebSocketPayloadHelper::mergeUserStreakInto([
|
||
'user_id' => $userId,
|
||
'period_no' => $period->period_no,
|
||
'numbers' => $numbers,
|
||
'bet_id' => $betChipId,
|
||
'single_bet_amount' => $singleAmount,
|
||
'numbers_count' => count($numbers),
|
||
'total_amount' => $totalAmount,
|
||
'balance_after' => $after,
|
||
'accepted_at' => time(),
|
||
], $userId, $streakAtBet));
|
||
GameWebSocketEventBus::publish('wallet.changed', GameWebSocketPayloadHelper::mergeUserStreakInto([
|
||
'user_id' => $userId,
|
||
'balance_after' => $after,
|
||
'biz_type' => 'bet',
|
||
'changed_at' => time(),
|
||
], $userId, $streakAtBet));
|
||
$periodIdForDisplay = filter_var($period->id ?? null, FILTER_VALIDATE_INT);
|
||
if ($periodIdForDisplay === false) {
|
||
$periodIdForDisplay = 0;
|
||
}
|
||
return $this->mobileSuccess([
|
||
'order_no' => $orderNo,
|
||
'period_no' => GamePeriodNo::toDisplay((string) ($period->period_no ?? ''), $periodIdForDisplay),
|
||
'status' => 'accepted',
|
||
'bet_id' => $betChipId,
|
||
'single_bet_amount' => $singleAmount,
|
||
'numbers_count' => count($numbers),
|
||
'locked_balance' => '0.00',
|
||
'balance_after' => $after,
|
||
'current_streak' => $streakAtBet,
|
||
]);
|
||
} finally {
|
||
GameHotDataRedis::userAdminMutationLockRelease($userId, $lock['token'], $lock['redis_lock']);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 自动托管(无推送模式):先落托管配置,实际执行仍由客户端轮询 current_status 驱动。
|
||
*/
|
||
public function autoSpin(Request $request): Response
|
||
{
|
||
$response = $this->initializeMobile($request);
|
||
if ($response !== null) {
|
||
return $response;
|
||
}
|
||
$action = trim((string) $request->post('action', 'start'));
|
||
if ($action === 'stop') {
|
||
return $this->mobileSuccess([
|
||
'status' => 'stopped',
|
||
'auto_mode' => false,
|
||
]);
|
||
}
|
||
|
||
$periodNo = trim((string) $request->post('period_no', ''));
|
||
$numbersRaw = $request->post('numbers', '');
|
||
$rounds = $this->intValue($request->post('rounds', 1));
|
||
$chipPick = $this->resolveBetChipFromRequest($request);
|
||
if (isset($chipPick['error'])) {
|
||
return $chipPick['error'];
|
||
}
|
||
$singleBetAmount = $chipPick['amount'];
|
||
if ($periodNo === '' || $rounds < 1) {
|
||
return $this->mobileError(1001, 'Missing parameters');
|
||
}
|
||
if (!is_numeric($singleBetAmount) || bccomp($singleBetAmount, '0', 2) <= 0) {
|
||
return $this->mobileError(1003, 'Invalid parameter value');
|
||
}
|
||
$numbers = $this->parseBetNumbersFromRequest($numbersRaw);
|
||
if ($numbers === []) {
|
||
return $this->mobileError(1003, 'Invalid parameter value');
|
||
}
|
||
$userIdValue = filter_var($this->auth->id ?? null, FILTER_VALIDATE_INT);
|
||
if ($userIdValue === false) {
|
||
$userIdValue = 0;
|
||
}
|
||
GameWebSocketEventBus::publish('auto.spin.progress', [
|
||
'user_id' => $userIdValue,
|
||
'period_no' => $periodNo,
|
||
'bet_id' => $chipPick['bet_id'],
|
||
'rounds' => $rounds,
|
||
'remaining_rounds' => $rounds,
|
||
'completed_rounds' => 0,
|
||
'status' => 'scheduled',
|
||
'server_time' => time(),
|
||
]);
|
||
return $this->mobileSuccess([
|
||
'status' => 'scheduled',
|
||
'auto_mode' => true,
|
||
'period_no' => $periodNo,
|
||
'numbers' => $numbers,
|
||
'bet_id' => $chipPick['bet_id'],
|
||
'single_bet_amount' => bcadd($singleBetAmount, '0', 2),
|
||
'rounds' => $rounds,
|
||
'remaining_rounds' => $rounds,
|
||
]);
|
||
}
|
||
|
||
public function betMyOrders(Request $request): Response
|
||
{
|
||
$response = $this->initializeMobile($request);
|
||
if ($response !== null) {
|
||
return $response;
|
||
}
|
||
$page = $this->intValue($request->input('page', 1));
|
||
$pageSize = $this->intValue($request->input('page_size', 20));
|
||
$paginate = BetOrder::where('user_id', $this->auth->id)->order('id', 'desc')->paginate([
|
||
'page' => $page,
|
||
'list_rows' => $pageSize,
|
||
]);
|
||
|
||
$periodIds = [];
|
||
foreach ($paginate->items() as $item) {
|
||
$periodIdValue = filter_var($item->period_id ?? null, FILTER_VALIDATE_INT);
|
||
if ($periodIdValue !== false && $periodIdValue > 0) {
|
||
$periodIds[] = $periodIdValue;
|
||
}
|
||
}
|
||
$periodIds = array_values(array_unique($periodIds));
|
||
$resultNumberByPeriodId = [];
|
||
if ($periodIds !== []) {
|
||
$periodRows = Db::name('game_record')
|
||
->whereIn('id', $periodIds)
|
||
->field(['id', 'result_number'])
|
||
->select()
|
||
->toArray();
|
||
foreach ($periodRows as $row) {
|
||
if (!is_array($row)) {
|
||
continue;
|
||
}
|
||
$pid = filter_var($row['id'] ?? null, FILTER_VALIDATE_INT);
|
||
if ($pid === false || $pid <= 0) {
|
||
continue;
|
||
}
|
||
$rn = filter_var($row['result_number'] ?? null, FILTER_VALIDATE_INT);
|
||
$resultNumberByPeriodId[$pid] = ($rn === false ? null : $rn);
|
||
}
|
||
}
|
||
|
||
$rows = [];
|
||
foreach ($paginate->items() as $item) {
|
||
$periodIdValue = filter_var($item->period_id ?? null, FILTER_VALIDATE_INT);
|
||
if ($periodIdValue === false) {
|
||
$periodIdValue = 0;
|
||
}
|
||
$rows[] = [
|
||
'order_no' => (string) $item->id,
|
||
'period_no' => GamePeriodNo::toDisplay((string) ($item->period_no ?? ''), $periodIdValue),
|
||
'numbers' => $item->pick_numbers ?? [],
|
||
// 整笔压注金额(本笔总扣款)
|
||
'bet_amount' => $item->total_amount,
|
||
'total_amount' => $item->total_amount,
|
||
'result_number' => $periodIdValue > 0 ? ($resultNumberByPeriodId[$periodIdValue] ?? null) : null,
|
||
'win_amount' => $item->win_amount,
|
||
'status' => (string) $item->status,
|
||
'create_time' => $item->create_time,
|
||
];
|
||
}
|
||
|
||
return $this->mobileSuccess([
|
||
'list' => $rows,
|
||
'pagination' => [
|
||
'page' => $paginate->currentPage(),
|
||
'page_size' => $paginate->listRows(),
|
||
'total' => $paginate->total(),
|
||
],
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 下注号码:`numbers` 为逗号分隔字符串(如 `1,8,16`);兼容旧版 JSON 数组。
|
||
*
|
||
* @return list<int>
|
||
*/
|
||
private function parseBetNumbersFromRequest($numbersRaw): array
|
||
{
|
||
if (is_array($numbersRaw)) {
|
||
$out = [];
|
||
foreach ($numbersRaw as $v) {
|
||
$n = filter_var($v, FILTER_VALIDATE_INT);
|
||
if ($n === false || $n < 1 || $n > 36) {
|
||
return [];
|
||
}
|
||
$out[] = $n;
|
||
}
|
||
$out = array_values(array_unique($out));
|
||
sort($out);
|
||
|
||
return $out;
|
||
}
|
||
$raw = trim((string) $numbersRaw);
|
||
if ($raw === '') {
|
||
return [];
|
||
}
|
||
$parts = preg_split('/\s*,\s*/', $raw);
|
||
$out = [];
|
||
foreach ($parts as $p) {
|
||
if ($p === '') {
|
||
continue;
|
||
}
|
||
$n = filter_var($p, FILTER_VALIDATE_INT);
|
||
if ($n === false || $n < 1 || $n > 36) {
|
||
return [];
|
||
}
|
||
$out[] = $n;
|
||
}
|
||
$out = array_values(array_unique($out));
|
||
sort($out);
|
||
|
||
return $out;
|
||
}
|
||
|
||
private function mapPeriodStatus($status): string
|
||
{
|
||
if ($this->intValue($status) === 0) {
|
||
return 'betting';
|
||
}
|
||
if ($this->intValue($status) === 1) {
|
||
return 'locked';
|
||
}
|
||
if ($this->intValue($status) === 2 || $this->intValue($status) === 3) {
|
||
return 'settling';
|
||
}
|
||
if ($this->intValue($status) === 5) {
|
||
return 'void';
|
||
}
|
||
return 'finished';
|
||
}
|
||
|
||
/**
|
||
* @return array{bet_id: int, amount: string}|array{error: Response}
|
||
*/
|
||
private function resolveBetChipFromRequest(Request $request): array
|
||
{
|
||
$betIdRaw = $request->post('bet_id', '');
|
||
if ($betIdRaw === '' || $betIdRaw === null) {
|
||
return ['error' => $this->mobileError(1001, 'Missing parameters')];
|
||
}
|
||
$betId = filter_var($betIdRaw, FILTER_VALIDATE_INT);
|
||
if ($betId === false || $betId < 1 || $betId > 6) {
|
||
return ['error' => $this->mobileError(1003, 'Invalid parameter value')];
|
||
}
|
||
$resolved = BetChips::resolveFromHotData();
|
||
$amount = BetChips::amountForBetId($betId, $resolved['map']);
|
||
if ($amount === null) {
|
||
return ['error' => $this->mobileError(1003, 'Invalid parameter value')];
|
||
}
|
||
|
||
return ['bet_id' => $betId, 'amount' => $amount];
|
||
}
|
||
|
||
/**
|
||
* 单注最多可选号码个数:`game_config.config_key = pick_max_number_count`
|
||
*/
|
||
private function getPickMaxNumberCount(): int
|
||
{
|
||
$v = $this->intValue($this->getConfigValue('pick_max_number_count', '10'));
|
||
if ($v < 1) {
|
||
return 1;
|
||
}
|
||
if ($v > 36) {
|
||
return 36;
|
||
}
|
||
|
||
return $v;
|
||
}
|
||
|
||
private function getConfigValue(string $key, string $default): string
|
||
{
|
||
$row = GameHotDataRedis::gameConfigRow($key);
|
||
if ($row === null) {
|
||
return $default;
|
||
}
|
||
$value = $row['config_value'] ?? null;
|
||
if ($value === null || $value === '') {
|
||
return $default;
|
||
}
|
||
return (string) $value;
|
||
}
|
||
|
||
/**
|
||
* 移动端展示的当前期:优先进行中局(id 最大且 status∈0..3),避免 gameRecordLatest 指向已结束新局而客户端仍用旧 period_no 下注。
|
||
*
|
||
* @return array<string, mixed>|null
|
||
*/
|
||
private function resolveMobilePeriodRow(): ?array
|
||
{
|
||
return GameHotDataRedis::gameRecordActive() ?? GameHotDataRedis::gameRecordLatest();
|
||
}
|
||
|
||
private function intValue($value): int
|
||
{
|
||
$result = filter_var($value, FILTER_VALIDATE_INT);
|
||
if ($result === false) {
|
||
return 0;
|
||
}
|
||
return $result;
|
||
}
|
||
}
|
||
|