API接口-初版

This commit is contained in:
2026-04-16 17:38:21 +08:00
parent 7b39a2a505
commit 545a818094
14 changed files with 1163 additions and 5 deletions

View File

@@ -21,3 +21,6 @@ DATABASE_PREFIX =
# 缓存config/cache.php # 缓存config/cache.php
CACHE_DRIVER = file CACHE_DRIVER = file
# 移动端接口鉴权(/api/v1/authToken
AUTH_TOKEN_SECRET = 564d14asdasd113e46542asd6das1a2a

View File

@@ -10,6 +10,7 @@ use app\common\facade\Token;
use app\common\model\UserScoreLog; use app\common\model\UserScoreLog;
use app\common\model\UserMoneyLog; use app\common\model\UserMoneyLog;
use app\common\controller\Frontend; use app\common\controller\Frontend;
use app\common\facade\Token as TokenFacade;
use support\validation\Validator; use support\validation\Validator;
use support\validation\ValidationException; use support\validation\ValidationException;
use Webman\Http\Request; use Webman\Http\Request;
@@ -20,6 +21,51 @@ class Account extends Frontend
protected array $noNeedLogin = ['retrievePassword']; protected array $noNeedLogin = ['retrievePassword'];
protected array $noNeedPermission = ['verification', 'changeBind']; protected array $noNeedPermission = ['verification', 'changeBind'];
public function userProfile(Request $request): Response
{
$response = $this->initializeFrontend($request);
if ($response !== null) {
return $response;
}
$authToken = trim((string) $request->header('auth-token', ''));
if ($authToken === '') {
return $this->mobileResult(1101, 'Missing auth-token');
}
$tokenData = TokenFacade::get($authToken);
$type = $tokenData['type'] ?? '';
$expireTime = $tokenData['expire_time'] ?? 0;
if ($type !== 'auth-token' || !is_numeric($expireTime) || $expireTime < time()) {
return $this->mobileResult(1101, 'auth-token is invalid or expired');
}
$user = $this->auth->getUser();
$payload = [
'code' => 1,
'message' => __('ok'),
'data' => [
'id' => $user->id,
'username' => $user->username,
'head_image' => $user->avatar ?? '',
'coin' => $user->coin,
'current_streak' => $user->current_streak ?? 0,
'channel_id' => $user->channel_id,
'risk_flags' => $user->risk_flags ?? 0,
],
];
return \response(json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), 200, ['Content-Type' => 'application/json']);
}
private function mobileResult(int $code, string $message, array $data = []): Response
{
$payload = [
'code' => $code,
'message' => __($message),
'data' => $data,
];
return \response(json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), 200, ['Content-Type' => 'application/json']);
}
public function overview(Request $request): Response public function overview(Request $request): Response
{ {
$response = $this->initializeFrontend($request); $response = $this->initializeFrontend($request);

138
app/api/controller/Auth.php Normal file
View File

@@ -0,0 +1,138 @@
<?php
declare(strict_types=1);
namespace app\api\controller;
use app\common\facade\Token;
use app\common\library\Auth as UserAuth;
use app\common\model\User;
use ba\Random;
use support\think\Db;
use Webman\Http\Request;
use support\Response;
class Auth extends MobileBase
{
protected array $noNeedLogin = ['userRegister', 'userLogin', 'tokenRefresh'];
public function userRegister(Request $request): Response
{
$response = $this->initializeMobile($request);
if ($response !== null) {
return $response;
}
$account = trim((string) $request->post('account', ''));
$accountType = trim((string) $request->post('account_type', ''));
$password = (string) $request->post('password', '');
$inviteCode = trim((string) $request->post('invite_code', ''));
if ($account === '' || $accountType === '' || $password === '') {
return $this->mobileError(1001, 'Missing parameters');
}
if ($accountType !== 'phone' && $accountType !== 'email') {
return $this->mobileError(1003, 'Invalid parameter value');
}
$username = $account;
$mobile = '';
$email = '';
if ($accountType === 'phone') {
$mobile = $account;
}
if ($accountType === 'email') {
$email = $account;
}
$extend = [];
if ($inviteCode !== '') {
$inviterAdmin = Db::name('admin')->field(['id', 'channel_id'])->where('invite_code', $inviteCode)->find();
if (!$inviterAdmin) {
return $this->mobileError(2002, 'Invite code does not exist');
}
$extend['register_invite_code'] = $inviteCode;
$extend['admin_id'] = $inviterAdmin['id'];
$extend['channel_id'] = $inviterAdmin['channel_id'] ?? null;
}
$registered = $this->auth->register($username, $password, $mobile, $email, 1, $extend);
if (!$registered) {
return $this->mobileError(2000, (string) $this->auth->getError());
}
$loggedIn = $this->auth->login($username, $password, true);
if (!$loggedIn) {
return $this->mobileError(2000, 'Registered successfully but login failed');
}
$userInfo = $this->auth->getUserInfo();
return $this->mobileSuccess([
'user_id' => $userInfo['id'] ?? null,
'access_token' => $userInfo['token'] ?? '',
'expires_in' => config('buildadmin.user_token_keep_time', 259200),
'profile' => [
'username' => $userInfo['username'] ?? '',
'coin' => $userInfo['coin'] ?? '0.0000',
'channel_id' => $userInfo['channel_id'] ?? null,
],
]);
}
public function userLogin(Request $request): Response
{
$response = $this->initializeMobile($request);
if ($response !== null) {
return $response;
}
$account = trim((string) $request->post('account', ''));
$password = (string) $request->post('password', '');
if ($account === '' || $password === '') {
return $this->mobileError(1001, 'Missing parameters');
}
$ok = $this->auth->login($account, $password, true);
if (!$ok) {
return $this->mobileError(1101, 'Incorrect account or password');
}
$userInfo = $this->auth->getUserInfo();
return $this->mobileSuccess([
'access_token' => $userInfo['token'] ?? '',
'refresh_token' => $userInfo['refresh_token'] ?? '',
'expires_in' => config('buildadmin.user_token_keep_time', 259200),
'user' => [
'id' => $userInfo['id'] ?? null,
'username' => $userInfo['username'] ?? '',
'coin' => $userInfo['coin'] ?? '0.0000',
'risk_flags' => $userInfo['risk_flags'] ?? 0,
],
]);
}
public function tokenRefresh(Request $request): Response
{
$response = $this->initializeMobile($request);
if ($response !== null) {
return $response;
}
$refreshToken = trim((string) $request->post('refresh_token', ''));
if ($refreshToken === '') {
return $this->mobileError(1001, 'Missing parameters');
}
$tokenData = Token::get($refreshToken);
if (!$tokenData || $tokenData['type'] !== UserAuth::TOKEN_TYPE . '-refresh' || $tokenData['expire_time'] < time()) {
return $this->mobileError(1101, 'Login status has expired');
}
$newToken = Random::uuid();
Token::set($newToken, UserAuth::TOKEN_TYPE, $tokenData['user_id'], config('buildadmin.user_token_keep_time', 259200));
return $this->mobileSuccess([
'access_token' => $newToken,
'expires_in' => config('buildadmin.user_token_keep_time', 259200),
]);
}
}

View File

@@ -0,0 +1,173 @@
<?php
declare(strict_types=1);
namespace app\api\controller;
use app\common\model\DepositOrder;
use app\common\model\WithdrawOrder;
use Webman\Http\Request;
use support\Response;
class Finance extends MobileBase
{
public function depositCreate(Request $request): Response
{
$response = $this->initializeMobile($request);
if ($response !== null) {
return $response;
}
$payAmountFiat = (string) $request->post('pay_amount_fiat', '');
$fiatCurrency = trim((string) $request->post('fiat_currency', ''));
$channel = trim((string) $request->post('channel', ''));
$idempotencyKey = trim((string) $request->post('idempotency_key', ''));
if ($payAmountFiat === '' || $fiatCurrency === '' || $channel === '' || $idempotencyKey === '') {
return $this->mobileError(1001, 'Missing parameters');
}
$orderNo = 'DP' . date('YmdHis') . substr(str_replace('.', '', uniqid('', true)), -6);
$coinAmount = $payAmountFiat;
DepositOrder::create([
'order_no' => $orderNo,
'user_id' => $this->auth->id,
'fiat_currency' => $fiatCurrency,
'fiat_amount' => $payAmountFiat,
'fx_rate' => '1.00000000',
'coin_amount' => $coinAmount,
'gateway' => $channel,
'status' => 0,
'create_time' => time(),
'update_time' => time(),
]);
return $this->mobileSuccess([
'order_no' => $orderNo,
'coin_amount' => $coinAmount,
'pay_url' => '',
'status' => 'pending',
]);
}
public function depositDetail(Request $request): Response
{
$response = $this->initializeMobile($request);
if ($response !== null) {
return $response;
}
$orderNo = trim((string) $request->get('order_no', ''));
if ($orderNo === '') {
return $this->mobileError(1001, 'Missing parameters');
}
$order = DepositOrder::where('order_no', $orderNo)->where('user_id', $this->auth->id)->find();
if (!$order) {
return $this->mobileError(2003, 'Order does not exist');
}
return $this->mobileSuccess([
'order_no' => $order->order_no,
'status' => $this->mapDepositStatus($order->status),
'coin_amount' => $order->coin_amount,
'create_time' => $order->create_time,
'finish_time' => $order->paid_at,
]);
}
public function withdrawCreate(Request $request): Response
{
$response = $this->initializeMobile($request);
if ($response !== null) {
return $response;
}
$withdrawCoin = (string) $request->post('withdraw_coin', '');
$receiveAccount = trim((string) $request->post('receive_account', ''));
$receiveType = trim((string) $request->post('receive_type', ''));
$idempotencyKey = trim((string) $request->post('idempotency_key', ''));
if ($withdrawCoin === '' || $receiveAccount === '' || $receiveType === '' || $idempotencyKey === '') {
return $this->mobileError(1001, 'Missing parameters');
}
$user = $this->auth->getUser();
if (bccomp((string) $user->coin, $withdrawCoin, 4) < 0) {
return $this->mobileError(2001, 'Insufficient balance');
}
$orderNo = 'WD' . date('YmdHis') . substr(str_replace('.', '', uniqid('', true)), -6);
$feeCoin = bcmul($withdrawCoin, '0.005', 4);
$actualArrivalCoin = bcsub($withdrawCoin, $feeCoin, 4);
WithdrawOrder::create([
'order_no' => $orderNo,
'user_id' => $user->id,
'apply_amount' => $withdrawCoin,
'fee_amount' => $feeCoin,
'actual_amount' => $actualArrivalCoin,
'fiat_currency' => '',
'need_audit' => 1,
'audit_status' => 0,
'reject_reason' => '',
'create_time' => time(),
'update_time' => time(),
]);
return $this->mobileSuccess([
'order_no' => $orderNo,
'status' => 'pending_review',
'fee_coin' => $feeCoin,
'actual_arrival_coin' => $actualArrivalCoin,
'risk_review_required' => true,
]);
}
public function withdrawDetail(Request $request): Response
{
$response = $this->initializeMobile($request);
if ($response !== null) {
return $response;
}
$orderNo = trim((string) $request->get('order_no', ''));
if ($orderNo === '') {
return $this->mobileError(1001, 'Missing parameters');
}
$order = WithdrawOrder::where('order_no', $orderNo)->where('user_id', $this->auth->id)->find();
if (!$order) {
return $this->mobileError(2003, 'Order does not exist');
}
return $this->mobileSuccess([
'order_no' => $order->order_no,
'status' => $this->mapWithdrawStatus($order->audit_status),
'withdraw_coin' => $order->apply_amount,
'fee_coin' => $order->fee_amount,
'reject_reason' => $order->reject_reason === '' ? null : $order->reject_reason,
'create_time' => $order->create_time,
]);
}
private function mapDepositStatus($status): string
{
if ($this->intValue($status) === 1) {
return 'paid';
}
if ($this->intValue($status) === 2 || $this->intValue($status) === 3) {
return 'failed';
}
return 'pending';
}
private function mapWithdrawStatus($auditStatus): string
{
if ($this->intValue($auditStatus) === 1) {
return 'approved';
}
if ($this->intValue($auditStatus) === 2) {
return 'rejected';
}
return 'pending_review';
}
private function intValue($value): int
{
$result = filter_var($value, FILTER_VALIDATE_INT);
if ($result === false) {
return 0;
}
return $result;
}
}

320
app/api/controller/Game.php Normal file
View File

@@ -0,0 +1,320 @@
<?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 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' => [
'max_select_count' => $this->intValue($this->getConfigValue('max_select_count', '5')),
'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->get('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,
]);
}
public function betPlace(Request $request): Response
{
$response = $this->initializeMobile($request);
if ($response !== null) {
return $response;
}
$periodNo = trim((string) $request->post('period_no', ''));
$numbers = $request->post('numbers', []);
$betAmount = (string) $request->post('bet_amount', '');
$idempotencyKey = trim((string) $request->post('idempotency_key', ''));
if ($periodNo === '' || !is_array($numbers) || $betAmount === '' || $idempotencyKey === '') {
return $this->mobileError(1001, 'Missing parameters');
}
if (count($numbers) < 1) {
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();
$pickCount = count($numbers);
$totalAmount = bcmul($betAmount, (string) $pickCount, 4);
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,
'unit_amount' => $betAmount,
'pick_count' => $pickCount,
'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();
} 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 betRebet(Request $request): Response
{
$response = $this->initializeMobile($request);
if ($response !== null) {
return $response;
}
return $this->mobileError(3001, 'Current process does not allow this operation');
}
public function autoBetCreate(Request $request): Response
{
$response = $this->initializeMobile($request);
if ($response !== null) {
return $response;
}
return $this->mobileError(3001, 'Current process does not allow this operation');
}
public function autoBetStop(Request $request): Response
{
$response = $this->initializeMobile($request);
if ($response !== null) {
return $response;
}
return $this->mobileError(3001, 'Current process does not allow this operation');
}
public function betMyOrders(Request $request): Response
{
$response = $this->initializeMobile($request);
if ($response !== null) {
return $response;
}
$page = $this->intValue($request->get('page', 1));
$pageSize = $this->intValue($request->get('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' => $item->unit_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(),
],
]);
}
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';
}
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;
}
}

View File

@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace app\api\controller;
use app\common\controller\Frontend;
use app\common\facade\Token;
use support\Response;
use Webman\Http\Request;
use function response;
abstract class MobileBase extends Frontend
{
protected array $noNeedPermission = ['*'];
protected array $noNeedAuthToken = [];
/**
* 移动端统一初始化:
* - 校验请求头 auth-token
* - 再走会员中心 Frontend 初始化(登录态/权限等)
*/
protected function initializeMobile(Request $request): ?Response
{
$this->setRequest($request);
$path = trim($request->path(), '/');
$parts = explode('/', $path);
$action = $parts[array_key_last($parts)] ?? '';
$needAuthToken = !action_in_arr($this->noNeedAuthToken, $action);
if ($needAuthToken) {
$authToken = trim((string) $request->header('auth-token', ''));
if ($authToken === '') {
return $this->mobileError(1101, 'Missing auth-token');
}
$tokenData = Token::get($authToken);
$type = $tokenData['type'] ?? '';
$expireTime = $tokenData['expire_time'] ?? 0;
if ($type !== 'auth-token' || !is_numeric($expireTime) || $expireTime < time()) {
return $this->mobileError(1101, 'auth-token is invalid or expired');
}
}
return $this->initializeFrontend($request);
}
protected function mobileSuccess(array $data = [], string $message = 'ok'): Response
{
if ($message === '') {
$message = __('ok');
} else {
$message = __($message);
}
$payload = [
'code' => 1,
'message' => $message,
'data' => $data,
];
return response(json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), 200, ['Content-Type' => 'application/json']);
}
protected function mobileError(int $code, string $message, array $data = []): Response
{
$payload = [
'code' => $code,
'message' => __($message),
'data' => $data,
];
return response(json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), 200, ['Content-Type' => 'application/json']);
}
}

View File

@@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace app\api\controller;
use app\common\model\OperationNotice;
use app\common\model\UserNoticeRead;
use Webman\Http\Request;
use support\Response;
class Notice extends MobileBase
{
public function noticeList(Request $request): Response
{
$response = $this->initializeMobile($request);
if ($response !== null) {
return $response;
}
$page = $this->intValue($request->get('page', 1), 1);
$pageSize = $this->intValue($request->get('page_size', 20), 20);
$paginate = OperationNotice::where('status', 1)->order('id', 'desc')->paginate([
'page' => $page,
'list_rows' => $pageSize,
]);
$noticeIds = [];
foreach ($paginate->items() as $row) {
$noticeIds[] = $row->id;
}
$readRows = [];
if ($noticeIds !== []) {
$readRows = UserNoticeRead::where('user_id', $this->auth->id)->whereIn('notice_id', $noticeIds)->column('notice_id');
}
$readMap = array_flip($readRows);
$list = [];
foreach ($paginate->items() as $row) {
$list[] = [
'notice_id' => $row->id,
'title' => $row->title,
'notice_type' => $this->intValue($row->notice_type, 0) === 1 ? 'popout' : 'silent',
'is_read' => isset($readMap[$row->id]),
'publish_time' => $row->publish_at,
];
}
return $this->mobileSuccess(['list' => $list]);
}
public function noticeDetail(Request $request): Response
{
$response = $this->initializeMobile($request);
if ($response !== null) {
return $response;
}
$id = $this->intValue($request->get('id', 0), 0);
if ($id < 1) {
return $this->mobileError(1001, 'Missing parameters');
}
$notice = OperationNotice::where('id', $id)->where('status', 1)->find();
if (!$notice) {
return $this->mobileError(2004, 'Notice does not exist');
}
return $this->mobileSuccess([
'notice_id' => $notice->id,
'title' => $notice->title,
'content' => $notice->content,
'notice_type' => $this->intValue($notice->notice_type, 0) === 1 ? 'popout' : 'silent',
'must_confirm' => $this->intValue($notice->notice_type, 0) === 1,
'publish_time' => $notice->publish_at,
]);
}
public function noticeConfirm(Request $request): Response
{
$response = $this->initializeMobile($request);
if ($response !== null) {
return $response;
}
$noticeId = $this->intValue($request->post('notice_id', 0), 0);
if ($noticeId < 1) {
return $this->mobileError(1001, 'Missing parameters');
}
$notice = OperationNotice::where('id', $noticeId)->where('status', 1)->find();
if (!$notice) {
return $this->mobileError(2004, 'Notice does not exist');
}
$exists = UserNoticeRead::where('user_id', $this->auth->id)->where('notice_id', $noticeId)->find();
$now = time();
if ($exists) {
$exists->save([
'confirmed' => 1,
'read_at' => $now,
]);
} else {
UserNoticeRead::create([
'user_id' => $this->auth->id,
'notice_id' => $noticeId,
'confirmed' => 1,
'read_at' => $now,
'create_time' => $now,
]);
}
return $this->mobileSuccess([
'notice_id' => $noticeId,
'confirmed' => true,
'confirm_time' => $now,
]);
}
private function intValue($value, int $default): int
{
$result = filter_var($value, FILTER_VALIDATE_INT);
if ($result === false) {
return $default;
}
return $result;
}
}

85
app/api/controller/V1.php Normal file
View File

@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace app\api\controller;
use app\common\controller\Api;
use app\common\facade\Token;
use ba\Random;
use Webman\Http\Request;
use support\Response;
use function response;
class V1 extends Api
{
public function authToken(Request $request): Response
{
$responseInit = $this->initializeApi($request);
if ($responseInit !== null) {
return $responseInit;
}
$secret = trim((string) $request->get('secret', ''));
$timestampRaw = $request->get('timestamp', '');
$deviceId = trim((string) $request->get('device_id', ''));
$signature = trim((string) $request->get('signature', ''));
if ($secret === '' || $timestampRaw === '' || $deviceId === '' || $signature === '') {
return $this->mobileResult(1001, 'Missing parameters');
}
$serverSecret = (string) env('AUTH_TOKEN_SECRET', '');
if ($serverSecret === '' || !hash_equals($serverSecret, $secret)) {
return $this->mobileResult(1103, 'Invalid secret');
}
$timestamp = filter_var($timestampRaw, FILTER_VALIDATE_INT);
if ($timestamp === false) {
return $this->mobileResult(1002, 'Invalid parameter format');
}
$now = time();
$skew = abs($now - $timestamp);
if ($skew > 300) {
return $this->mobileResult(3001, 'Invalid timestamp');
}
$params = [
'device_id' => $deviceId,
'secret' => $secret,
'timestamp' => (string) $timestamp,
];
ksort($params);
$pairs = [];
foreach ($params as $k => $v) {
$pairs[] = $k . '=' . $v;
}
$plain = implode('&', $pairs);
$expected = strtoupper(md5($plain));
if (!hash_equals($expected, $signature)) {
return $this->mobileResult(1103, 'Invalid signature');
}
$token = Random::uuid();
$expire = 60 * 60 * 24;
Token::set($token, 'auth-token', 0, $expire);
return $this->mobileResult(1, 'ok', [
'auth_token' => $token,
'expires_in' => $expire,
'server_time' => $now,
]);
}
private function mobileResult(int $code, string $message, array $data = []): Response
{
$payload = [
'code' => $code,
'message' => __($message),
'data' => $data,
];
return response(json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), 200, ['Content-Type' => 'application/json']);
}
}

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace app\api\controller;
use app\common\model\UserWalletRecord;
use Webman\Http\Request;
use support\Response;
class Wallet extends MobileBase
{
public function balanceSummary(Request $request): Response
{
$response = $this->initializeMobile($request);
if ($response !== null) {
return $response;
}
$user = $this->auth->getUser();
return $this->mobileSuccess([
'coin_balance' => $user->coin,
'frozen_balance' => '0.0000',
'total_deposit_coin' => $user->total_deposit_coin ?? '0.0000',
'total_valid_bet_coin' => $user->total_valid_bet_coin ?? '0.0000',
'withdrawable_balance' => $user->coin,
]);
}
public function recordList(Request $request): Response
{
$response = $this->initializeMobile($request);
if ($response !== null) {
return $response;
}
$type = trim((string) $request->get('type', 'all'));
$page = $this->intValue($request->get('page', 1), 1);
$pageSize = $this->intValue($request->get('page_size', 20), 20);
$query = UserWalletRecord::where('user_id', $this->auth->id)->order('id', 'desc');
if ($type !== '' && $type !== 'all') {
$query->where('biz_type', $type);
}
$paginate = $query->paginate([
'page' => $page,
'list_rows' => $pageSize,
]);
$list = [];
foreach ($paginate->items() as $row) {
$list[] = [
'record_id' => $row->id,
'biz_type' => $row->biz_type,
'direction' => $row->direction,
'amount' => $row->amount,
'balance_before' => $row->balance_before,
'balance_after' => $row->balance_after,
'ref_type' => $row->ref_type,
'ref_id' => $row->ref_id,
'create_time' => $row->create_time,
];
}
return $this->mobileSuccess([
'list' => $list,
'pagination' => [
'page' => $paginate->currentPage(),
'page_size' => $paginate->listRows(),
'total' => $paginate->total(),
],
]);
}
private function intValue($value, int $default): int
{
$result = filter_var($value, FILTER_VALIDATE_INT);
if ($result === false) {
return $default;
}
return $result;
}
}

View File

@@ -12,6 +12,27 @@ return [
'Please login first' => 'Please login first', 'Please login first' => 'Please login first',
'You have no permission' => 'No permission to operate', 'You have no permission' => 'No permission to operate',
'Captcha error' => 'Captcha error!', 'Captcha error' => 'Captcha error!',
'ok' => 'ok',
'Missing parameters' => 'Missing parameters',
'Invalid parameter format' => 'Invalid parameter format',
'Invalid parameter value' => 'Invalid parameter value',
'Missing auth-token' => 'Missing auth-token',
'auth-token is invalid or expired' => 'auth-token is invalid or expired',
'Invalid secret' => 'Invalid secret',
'Invalid signature' => 'Invalid signature',
'Invalid timestamp' => 'Invalid timestamp',
'Invite code does not exist' => 'Invite code does not exist',
'Registered successfully but login failed' => 'Registered successfully but login failed',
'Incorrect account or password' => 'Incorrect account or password',
'Login status has expired' => 'Login status has expired',
'Game period does not exist' => 'Game period does not exist',
'Betting is closed' => 'Betting is closed',
'Insufficient balance' => 'Insufficient balance',
'Duplicate request' => 'Duplicate request',
'System is busy, please try again later' => 'System is busy, please try again later',
'Current process does not allow this operation' => 'Current process does not allow this operation',
'Order does not exist' => 'Order does not exist',
'Notice does not exist' => 'Notice does not exist',
// Member center account // Member center account
'Data updated successfully~' => 'Data updated successfully~', 'Data updated successfully~' => 'Data updated successfully~',
'Password has been changed~' => 'Password has been changed~', 'Password has been changed~' => 'Password has been changed~',

View File

@@ -44,6 +44,27 @@ return [
'Parameter error' => '参数错误!', 'Parameter error' => '参数错误!',
'Token expiration' => '登录态过期,请重新登录!', 'Token expiration' => '登录态过期,请重新登录!',
'Captcha error' => '验证码错误!', 'Captcha error' => '验证码错误!',
'ok' => '成功',
'Missing parameters' => '参数缺失',
'Invalid parameter format' => '参数格式错误',
'Invalid parameter value' => '参数取值非法',
'Missing auth-token' => '缺少 auth-token',
'auth-token is invalid or expired' => 'auth-token 无效或已过期',
'Invalid secret' => '密钥无效',
'Invalid signature' => '签名错误',
'Invalid timestamp' => '时间戳无效',
'Invite code does not exist' => '邀请码不存在',
'Registered successfully but login failed' => '注册成功但登录失败',
'Incorrect account or password' => '账号或密码错误',
'Login status has expired' => '登录状态已过期',
'Game period does not exist' => '对局不存在',
'Betting is closed' => '已封盘,禁止下注',
'Insufficient balance' => '余额不足',
'Duplicate request' => '重复请求(幂等冲突)',
'System is busy, please try again later' => '系统繁忙,请稍后重试',
'Current process does not allow this operation' => '当前流程不允许该操作',
'Order does not exist' => '订单不存在',
'Notice does not exist' => '公告不存在',
// 会员中心 account // 会员中心 account
'Data updated successfully~' => '资料更新成功~', 'Data updated successfully~' => '资料更新成功~',
'Password has been changed~' => '密码已修改~', 'Password has been changed~' => '密码已修改~',

View File

@@ -25,12 +25,20 @@ class LoadLangPack implements MiddlewareInterface
protected function loadLang(Request $request): void protected function loadLang(Request $request): void
{ {
// 优先从请求头 think-lang 获取前端选择的语言(与前端 axios 发送的 header 对应) // 优先从请求头 lang / think-lang 获取前端选择的语言
// 安装页等未发送 think-lang 时,回退到 Accept-Language 或配置默认值 // 支持lang=en / lang=zh / think-lang=en / think-lang=zh-cn
$headerLang = $request->header('think-lang'); // 未发送时回退到 Accept-Language 或配置默认值
$headerLang = $request->header('lang', '');
if ($headerLang === '') {
$headerLang = $request->header('think-lang', '');
}
$allowLangList = config('lang.allow_lang_list', ['zh-cn', 'en']); $allowLangList = config('lang.allow_lang_list', ['zh-cn', 'en']);
if ($headerLang && in_array(str_replace('_', '-', strtolower($headerLang)), $allowLangList)) { $normalizedHeaderLang = str_replace('_', '-', strtolower($headerLang));
$langSet = str_replace('_', '-', strtolower($headerLang)); if ($normalizedHeaderLang === 'zh') {
$normalizedHeaderLang = 'zh-cn';
}
if ($headerLang && in_array($normalizedHeaderLang, $allowLangList)) {
$langSet = $normalizedHeaderLang;
} else { } else {
$acceptLang = $request->header('accept-language', ''); $acceptLang = $request->header('accept-language', '');
if (preg_match('/^zh[-_]?cn|^zh/i', $acceptLang)) { if (preg_match('/^zh[-_]?cn|^zh/i', $acceptLang)) {

View File

@@ -68,6 +68,9 @@ Route::get('/install/index', function () use ($installLockFileForInstall, $insta
// api/index // api/index
Route::get('/api/index/index', [\app\api\controller\Index::class, 'index']); Route::get('/api/index/index', [\app\api\controller\Index::class, 'index']);
// api/v1
Route::get('/api/v1/authToken', [\app\api\controller\V1::class, 'authToken']);
// api/userGET 获取配置POST 登录/注册) // api/userGET 获取配置POST 登录/注册)
Route::add(['GET', 'POST'], '/api/user/checkIn', [\app\api\controller\User::class, 'checkIn']); Route::add(['GET', 'POST'], '/api/user/checkIn', [\app\api\controller\User::class, 'checkIn']);
Route::post('/api/user/logout', [\app\api\controller\User::class, 'logout']); Route::post('/api/user/logout', [\app\api\controller\User::class, 'logout']);
@@ -108,6 +111,35 @@ Route::post('/api/account/retrievePassword', [\app\api\controller\Account::class
// api/ems // api/ems
Route::post('/api/ems/send', [\app\api\controller\Ems::class, 'send']); Route::post('/api/ems/send', [\app\api\controller\Ems::class, 'send']);
// ==================== 移动端游戏接口36字花 ====================
Route::post('/api/auth/userRegister', [\app\api\controller\Auth::class, 'userRegister']);
Route::post('/api/auth/userLogin', [\app\api\controller\Auth::class, 'userLogin']);
Route::post('/api/auth/tokenRefresh', [\app\api\controller\Auth::class, 'tokenRefresh']);
Route::get('/api/account/userProfile', [\app\api\controller\Account::class, 'userProfile']);
Route::get('/api/game/lobbyInit', [\app\api\controller\Game::class, 'lobbyInit']);
Route::get('/api/game/dictionaryList', [\app\api\controller\Game::class, 'dictionaryList']);
Route::get('/api/game/periodHistory', [\app\api\controller\Game::class, 'periodHistory']);
Route::get('/api/game/periodCurrent', [\app\api\controller\Game::class, 'periodCurrent']);
Route::post('/api/game/betPlace', [\app\api\controller\Game::class, 'betPlace']);
Route::post('/api/game/betRebet', [\app\api\controller\Game::class, 'betRebet']);
Route::post('/api/game/autoBetCreate', [\app\api\controller\Game::class, 'autoBetCreate']);
Route::post('/api/game/autoBetStop', [\app\api\controller\Game::class, 'autoBetStop']);
Route::get('/api/game/betMyOrders', [\app\api\controller\Game::class, 'betMyOrders']);
Route::get('/api/wallet/balanceSummary', [\app\api\controller\Wallet::class, 'balanceSummary']);
Route::get('/api/wallet/recordList', [\app\api\controller\Wallet::class, 'recordList']);
Route::post('/api/finance/depositCreate', [\app\api\controller\Finance::class, 'depositCreate']);
Route::get('/api/finance/depositDetail', [\app\api\controller\Finance::class, 'depositDetail']);
Route::post('/api/finance/withdrawCreate', [\app\api\controller\Finance::class, 'withdrawCreate']);
Route::get('/api/finance/withdrawDetail', [\app\api\controller\Finance::class, 'withdrawDetail']);
Route::get('/api/notice/noticeList', [\app\api\controller\Notice::class, 'noticeList']);
Route::get('/api/notice/noticeDetail', [\app\api\controller\Notice::class, 'noticeDetail']);
Route::post('/api/notice/noticeConfirm', [\app\api\controller\Notice::class, 'noticeConfirm']);
// ==================== Admin 路由 ==================== // ==================== Admin 路由 ====================
// Admin 多为 JSON API前端可能用 GET 传参查列表、POST 提交表单,使用 any 确保兼容 // Admin 多为 JSON API前端可能用 GET 传参查列表、POST 提交表单,使用 any 确保兼容

View File

@@ -0,0 +1,36 @@
<?php
/**
* 执行方法
* php scripts/generate_auth_signature.php
* php scripts/generate_auth_signature.php 设备码 密钥 时间戳
* php scripts/generate_auth_signature.php 1 564d14asdasd113e46542asd6das1a2a 1776331077
*/
declare(strict_types=1);
$deviceId = $argv[1] ?? '1';
$secret = $argv[2] ?? ((string) getenv('AUTH_TOKEN_SECRET') ?: '564d14asdasd113e46542asd6das1a2a');
$timestamp = $argv[3] ?? (string) time();
$params = [
'device_id' => (string) $deviceId,
'secret' => (string) $secret,
'timestamp' => (string) $timestamp,
];
ksort($params);
$pairs = [];
foreach ($params as $key => $value) {
$pairs[] = $key . '=' . $value;
}
$plain = implode('&', $pairs);
$signature = strtoupper(md5($plain));
echo 'device_id: ' . $params['device_id'] . PHP_EOL;
echo 'secret: ' . $params['secret'] . PHP_EOL;
echo 'timestamp: ' . $params['timestamp'] . PHP_EOL;
echo 'signature: ' . $signature . PHP_EOL;
echo 'url: /api/v1/authToken?secret=' . rawurlencode($params['secret']) . '&timestamp=' . rawurlencode($params['timestamp']) . '&device_id=' . rawurlencode($params['device_id']) . '&signature=' . rawurlencode($signature) . PHP_EOL;