Files
webman-buildadmin/app/api/controller/Game.php
zhenhui c184fa8a46 1.优化后台测试推送功能页面
2.优化开奖和实时对局页面
2026-04-18 17:16:13 +08:00

375 lines
13 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace app\api\controller;
use app\common\library\game\ZiHuaDictionary;
use app\common\model\BetOrder;
use app\common\model\GameConfig;
use app\common\model\GameRecord;
use app\common\model\UserWalletRecord;
use app\common\service\UserPushService;
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;
}
$period = GameRecord::order('id', 'desc')->find();
$now = time();
$startAt = $period ? $this->intValue($period->period_start_at) : $now;
$lockAt = $startAt + 20;
$openAt = $startAt + 22;
$countdown = $period ? max(0, ($startAt + 30) - $now) : 0;
$dictionaryConfig = GameConfig::where('config_key', ZiHuaDictionary::CONFIG_KEY)->find();
$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();
return $this->mobileSuccess([
'server_time' => $now,
'period' => [
'period_no' => $period->period_no ?? '',
'status' => $this->mapPeriodStatus($period->status ?? null),
'countdown' => $countdown,
'lock_at' => $lockAt,
'open_at' => $openAt,
],
'bet_config' => [
'pick_max_number_count' => $this->getPickMaxNumberCount(),
'chips' => ['1.0000', '5.0000', '10.0000', '25.0000', '50.0000', '100.0000'],
'single_number_max_bet' => $this->getConfigValue('single_number_max_bet', '500.0000'),
],
'dictionary' => $items,
'user_snapshot' => [
'coin' => $user->coin,
'current_streak' => $user->current_streak ?? 0,
],
]);
}
public function dictionaryList(Request $request): Response
{
$response = $this->initializeMobile($request);
if ($response !== null) {
return $response;
}
$dictionaryConfig = GameConfig::where('config_key', ZiHuaDictionary::CONFIG_KEY)->find();
$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,
]);
}
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;
}
$list = GameRecord::whereNotNull('result_number')->order('id', 'desc')->limit($limit)->select();
$rows = [];
foreach ($list as $item) {
$rows[] = [
'period_no' => $item->period_no,
'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;
}
$period = GameRecord::order('id', 'desc')->find();
if (!$period) {
return $this->mobileError(2002, 'Game period does not exist');
}
$now = time();
$startAt = $this->intValue($period->period_start_at);
return $this->mobileSuccess([
'period_id' => $period->id,
'period_no' => $period->period_no,
'status' => $this->mapPeriodStatus($period->status),
'countdown' => max(0, ($startAt + 30) - $now),
'bet_close_in' => max(0, ($startAt + 20) - $now),
'result_number' => $period->result_number,
]);
}
/**
* 提交下注入参极简——period_no + numbers + bet_amount整笔总金额 + idempotency_key。
*
* 下注判定:开奖号码 ∈ pick_numbers 即算中奖,赔付按整笔 total_amount × odds 计算
* odds 定义见 GameBetSettleService::BASE_ODDS 与 streak_at_bet
*/
public function betPlace(Request $request): Response
{
$response = $this->initializeMobile($request);
if ($response !== null) {
return $response;
}
$periodNo = trim((string) $request->post('period_no', ''));
$numbersRaw = $request->post('numbers', '');
$betAmount = trim((string) $request->post('bet_amount', ''));
$idempotencyKey = trim((string) $request->post('idempotency_key', ''));
if ($periodNo === '' || $betAmount === '' || $idempotencyKey === '') {
return $this->mobileError(1001, 'Missing parameters');
}
if (!is_numeric($betAmount) || bccomp($betAmount, '0', 4) <= 0) {
return $this->mobileError(1003, 'Invalid parameter value');
}
$totalAmount = bcadd($betAmount, '0', 4);
$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');
}
$period = GameRecord::where('period_no', $periodNo)->find();
if (!$period) {
return $this->mobileError(2002, 'Game period does not exist');
}
if ($this->intValue($period->status) !== 0) {
return $this->mobileError(3002, 'Betting is closed');
}
$user = $this->auth->getUser();
if (bccomp((string) $user->coin, $totalAmount, 4) < 0) {
return $this->mobileError(2001, 'Insufficient balance');
}
$exists = BetOrder::where('idempotency_key', $idempotencyKey)->find();
if ($exists) {
return $this->mobileError(3003, 'Duplicate request');
}
Db::startTrans();
try {
$before = (string) $user->coin;
$after = bcsub($before, $totalAmount, 4);
UserWalletRecord::create([
'user_id' => $user->id,
'channel_id' => $user->channel_id,
'biz_type' => 'bet',
'direction' => 2,
'amount' => $totalAmount,
'balance_before' => $before,
'balance_after' => $after,
'ref_type' => 'bet_order',
'remark' => '移动端下注',
'create_time' => time(),
]);
Db::name('user')->where('id', $user->id)->update(['coin' => $after, 'update_time' => time()]);
$orderNo = 'BO' . date('YmdHis') . substr(str_replace('.', '', uniqid('', true)), -6);
BetOrder::create([
'period_id' => $period->id,
'period_no' => $period->period_no,
'user_id' => $user->id,
'channel_id' => $user->channel_id,
'pick_numbers' => $numbers,
'total_amount' => $totalAmount,
'streak_at_bet' => $user->current_streak ?? 0,
'is_auto' => 0,
'status' => 1,
'idempotency_key' => $idempotencyKey,
'create_time' => time(),
'update_time' => time(),
]);
Db::commit();
UserPushService::publish((int) $user->id, UserPushService::EVT_BET_ACCEPTED, [
'order_no' => $orderNo,
'period_no' => (string) $period->period_no,
'status' => 'accepted',
'balance_after' => $after,
'total_amount' => $totalAmount,
'current_streak' => (int) ($user->current_streak ?? 0),
]);
} catch (\Throwable $e) {
Db::rollback();
return $this->mobileError(5000, 'System is busy, please try again later', ['detail' => $e->getMessage()]);
}
return $this->mobileSuccess([
'order_no' => $orderNo,
'period_no' => $period->period_no,
'status' => 'accepted',
'locked_balance' => '0.0000',
'balance_after' => $after,
'current_streak' => $user->current_streak ?? 0,
]);
}
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,
]);
$rows = [];
foreach ($paginate->items() as $item) {
$rows[] = [
'order_no' => (string) $item->id,
'period_no' => $item->period_no,
'numbers' => $item->pick_numbers ?? [],
// 整笔压注金额(与请求 bet_amount 语义一致)
'bet_amount' => $item->total_amount,
'total_amount' => $item->total_amount,
'result_number' => 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';
}
return 'finished';
}
/**
* 单注最多可选号码个数:`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
{
$value = GameConfig::where('config_key', $key)->value('config_value');
if ($value === null || $value === '') {
return $default;
}
return (string) $value;
}
private function intValue($value): int
{
$result = filter_var($value, FILTER_VALIDATE_INT);
if ($result === false) {
return 0;
}
return $result;
}
}