532 lines
20 KiB
PHP
532 lines
20 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
|
||
namespace app\api\controller\v1;
|
||
|
||
use app\api\logic\UserLogic;
|
||
use app\api\util\ReturnCode;
|
||
use app\dice\model\game\DiceGame;
|
||
use app\dice\model\player\DicePlayer;
|
||
use app\dice\model\play_record\DicePlayRecord;
|
||
use app\dice\model\player_wallet_record\DicePlayerWalletRecord;
|
||
use app\dice\model\player_ticket_record\DicePlayerTicketRecord;
|
||
use support\think\Db;
|
||
use app\api\controller\BaseController;
|
||
use support\Request;
|
||
use support\Response;
|
||
use app\api\cache\UserCache;
|
||
|
||
/**
|
||
* 平台 v1 游戏接口
|
||
* 请求头:auth-token
|
||
*/
|
||
class GameController extends BaseController
|
||
{
|
||
/** 游戏记录 / 钱包流水 / 中奖券记录等拉取类接口的条数上限(与对接文档一致) */
|
||
private const PLAYER_PULL_RECORD_MAX_LIMIT = 2000;
|
||
|
||
private const PLAYER_PULL_RECORD_DEFAULT_LIMIT = 20;
|
||
|
||
/** 拉取类流水仅允许查询「当前时间起向前」的最大天数(含起止) */
|
||
private const PLAYER_PULL_RECORD_MAX_RANGE_DAYS = 7;
|
||
|
||
private const GAME_PUBLIC_FIELDS = [
|
||
'provider',
|
||
'provider_code',
|
||
'game_code',
|
||
'game_key',
|
||
'game_type',
|
||
'logo',
|
||
'game_url',
|
||
'hall_url',
|
||
'status',
|
||
'sort',
|
||
];
|
||
|
||
/** getPlayerInfo 仅查询需返回的列,减轻 IO(敏感/内部字段不入库查询) */
|
||
private const PLAYER_INFO_DB_FIELDS = [
|
||
'id', 'username', 'phone', 'uid', 'name', 'status', 'coin', 'is_up', 'admin_id',
|
||
'total_ticket_count', 'paid_ticket_count', 'free_ticket_count', 'free_ticket',
|
||
'total_win_coin',
|
||
'create_time', 'update_time',
|
||
];
|
||
|
||
/**
|
||
* 获取游戏列表
|
||
* POST 参数:lang(可选,zh/en,默认 zh)
|
||
* 当前返回启用中的游戏列表
|
||
*/
|
||
public function getGameList(Request $request): Response
|
||
{
|
||
$lang = $this->resolveLang($request->post('lang', 'zh'));
|
||
$games = $this->buildPublicGameList($lang, $this->agentDeptId($request));
|
||
return $this->success([
|
||
'game_list' => $games,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 获取游戏大厅信息(脱敏)
|
||
* POST 参数:lang(可选,zh/en,默认 zh)
|
||
*/
|
||
public function getGameHall(Request $request): Response
|
||
{
|
||
$lang = $this->resolveLang($request->post('lang', 'zh'));
|
||
$games = $this->buildPublicGameList($lang, $this->agentDeptId($request));
|
||
$hallUrl = '';
|
||
if (!empty($games)) {
|
||
$hallUrl = $games[0]['hall_url'] ?? '';
|
||
}
|
||
return $this->success([
|
||
'provider' => 'Dicey Fun',
|
||
'provider_code' => 'DF',
|
||
'hall_url' => $hallUrl,
|
||
'game_list' => $games,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 获取游戏地址
|
||
* 根据 username 创建登录 token(JWT),拼接游戏地址返回
|
||
*/
|
||
public function getGameUrl(Request $request): Response
|
||
{
|
||
$username = trim((string) ($request->post('username', '')));
|
||
$password = trim((string) ($request->post('password', '123456')));
|
||
$time = trim((string) ($request->post('time', '')));
|
||
|
||
if ($username === '') {
|
||
return $this->fail('username is required', ReturnCode::PARAMS_ERROR);
|
||
}
|
||
if ($password === '') {
|
||
$password = '123456';
|
||
}
|
||
if ($time === '') {
|
||
$time = (string) time();
|
||
}
|
||
|
||
$deptId = $this->agentDeptId($request);
|
||
$adminId = $this->agentAdminId($request);
|
||
$adminIdsInTopDept = UserLogic::getAdminIdsByAgentIdTopDept(trim((string) ($request->agent_id ?? '')));
|
||
|
||
$lang = trim((string) ($request->post('lang', 'zh')));
|
||
$lang = in_array($lang, ['en', 'zh'], true) ? $lang : 'zh';
|
||
|
||
try {
|
||
$logic = new UserLogic();
|
||
$result = $logic->loginByUsername($username, $password, $lang, 0.0, $time, $adminId, $adminIdsInTopDept, $deptId);
|
||
} catch (\plugin\saiadmin\exception\ApiException $e) {
|
||
return $this->fail($e->getMessage(), ReturnCode::PARAMS_ERROR);
|
||
}
|
||
|
||
$gameUrlBase = rtrim(config('api.game_url', 'dice-game.h55555game.top'), '/');
|
||
$tokenInUrl = str_replace('%3D', '=', urlencode($result['token']));
|
||
$url = $gameUrlBase . '/?token=' . $tokenInUrl . '&lang=' . $lang;
|
||
|
||
return $this->success([
|
||
'url' => $url,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 获取用户信息
|
||
* 参数:username(POST JSON / 表单 / Query 均可,input 合并读取降低偶发空参)
|
||
* 返回 DicePlayer 中非敏感信息;短期 Redis 快照 + 窄字段查询降低延迟
|
||
*/
|
||
public function getPlayerInfo(Request $request): Response
|
||
{
|
||
$usernameRaw = $request->input('username', '');
|
||
$username = is_string($usernameRaw) ? trim($usernameRaw) : '';
|
||
$deptId = $this->agentDeptId($request);
|
||
|
||
if ($username === '') {
|
||
return $this->fail('username is required', ReturnCode::PARAMS_ERROR);
|
||
}
|
||
|
||
$cached = UserCache::getPlayerInfoSnapshotByUsername($this->scopedUsername($deptId, $username));
|
||
if ($cached !== null) {
|
||
return $this->success($cached);
|
||
}
|
||
|
||
try {
|
||
$logic = new UserLogic();
|
||
$player = $logic->findOrCreatePlayerByUsername(
|
||
$username,
|
||
$this->agentAdminId($request),
|
||
$deptId > 0 ? $deptId : null
|
||
);
|
||
} catch (\plugin\saiadmin\exception\ApiException $e) {
|
||
return $this->fail($e->getMessage(), ReturnCode::PARAMS_ERROR);
|
||
}
|
||
|
||
$player = DicePlayer::field(self::PLAYER_INFO_DB_FIELDS)->where('id', (int) $player->id)->find();
|
||
if (!$player) {
|
||
return $this->fail('User not found', ReturnCode::PARAMS_ERROR);
|
||
}
|
||
|
||
$hidden = ['password', 'lottery_config_id', 't1_weight', 't2_weight', 't3_weight', 't4_weight', 't5_weight', 'delete_time'];
|
||
$info = $player->hidden($hidden)->toArray();
|
||
UserCache::setPlayerInfoSnapshotByUsername($this->scopedUsername($deptId, $username), $info);
|
||
|
||
return $this->success($info);
|
||
}
|
||
|
||
/**
|
||
* 解析拉取类流水接口的 limit(与 getPlayerGameRecord / getPlayerWalletRecord / getPlayerTicketRecord 共用)
|
||
*/
|
||
private function resolvePullRecordLimit(Request $request): int
|
||
{
|
||
$limit = (int) $request->post('limit', self::PLAYER_PULL_RECORD_DEFAULT_LIMIT);
|
||
if ($limit < 1 || $limit > self::PLAYER_PULL_RECORD_MAX_LIMIT) {
|
||
$limit = self::PLAYER_PULL_RECORD_DEFAULT_LIMIT;
|
||
}
|
||
|
||
return $limit;
|
||
}
|
||
|
||
/**
|
||
* 将 start_create_time / end_create_time 规范为「最近 7 天内」且跨度不超过 7 天的闭区间。
|
||
*
|
||
* @return array{ok: true, start: string, end: string}|array{ok: false, message: string}
|
||
*/
|
||
private function resolvePullRecordTimeWindow(string $startRaw, string $endRaw): array
|
||
{
|
||
$nowTs = time();
|
||
$weekAgoTs = strtotime('-' . self::PLAYER_PULL_RECORD_MAX_RANGE_DAYS . ' days', $nowTs);
|
||
if ($weekAgoTs === false) {
|
||
$weekAgoTs = $nowTs - self::PLAYER_PULL_RECORD_MAX_RANGE_DAYS * 86400;
|
||
}
|
||
$nowStr = date('Y-m-d H:i:s', $nowTs);
|
||
$weekAgoStr = date('Y-m-d H:i:s', $weekAgoTs);
|
||
|
||
if ($startRaw === '' && $endRaw === '') {
|
||
return ['ok' => true, 'start' => $weekAgoStr, 'end' => $nowStr];
|
||
}
|
||
|
||
$startTs = $startRaw === '' ? null : strtotime($startRaw);
|
||
$endTs = $endRaw === '' ? null : strtotime($endRaw);
|
||
|
||
if ($startRaw !== '' && $startTs === false) {
|
||
return ['ok' => false, 'message' => 'Invalid start_create_time'];
|
||
}
|
||
if ($endRaw !== '' && $endTs === false) {
|
||
return ['ok' => false, 'message' => 'Invalid end_create_time'];
|
||
}
|
||
|
||
if ($startRaw === '' && $endRaw !== '' && $endTs !== null) {
|
||
$endTs = min($endTs, $nowTs);
|
||
if ($endTs < $weekAgoTs) {
|
||
return ['ok' => false, 'message' => 'end_create_time must be within the last 7 days'];
|
||
}
|
||
$startTs = strtotime('-' . self::PLAYER_PULL_RECORD_MAX_RANGE_DAYS . ' days', $endTs);
|
||
if ($startTs === false) {
|
||
$startTs = $endTs - self::PLAYER_PULL_RECORD_MAX_RANGE_DAYS * 86400;
|
||
}
|
||
if ($startTs < $weekAgoTs) {
|
||
$startTs = $weekAgoTs;
|
||
}
|
||
|
||
return ['ok' => true, 'start' => date('Y-m-d H:i:s', $startTs), 'end' => date('Y-m-d H:i:s', $endTs)];
|
||
}
|
||
|
||
if ($startRaw !== '' && $startTs !== null && $endRaw === '') {
|
||
if ($startTs > $nowTs) {
|
||
return ['ok' => false, 'message' => 'start_create_time cannot be in the future'];
|
||
}
|
||
if ($startTs < $weekAgoTs) {
|
||
return ['ok' => false, 'message' => 'start_create_time cannot be earlier than 7 days ago'];
|
||
}
|
||
$endTs = strtotime('+' . self::PLAYER_PULL_RECORD_MAX_RANGE_DAYS . ' days', $startTs);
|
||
if ($endTs === false) {
|
||
$endTs = $startTs + self::PLAYER_PULL_RECORD_MAX_RANGE_DAYS * 86400;
|
||
}
|
||
$endTs = min($endTs, $nowTs);
|
||
|
||
return ['ok' => true, 'start' => date('Y-m-d H:i:s', $startTs), 'end' => date('Y-m-d H:i:s', $endTs)];
|
||
}
|
||
|
||
if ($startTs !== null && $endTs !== null) {
|
||
if ($endTs > $nowTs) {
|
||
$endTs = $nowTs;
|
||
}
|
||
if ($startTs > $endTs) {
|
||
return ['ok' => false, 'message' => 'start_create_time cannot be after end_create_time'];
|
||
}
|
||
if ($startTs < $weekAgoTs) {
|
||
return ['ok' => false, 'message' => 'start_create_time cannot be earlier than 7 days ago'];
|
||
}
|
||
if ($endTs < $weekAgoTs) {
|
||
return ['ok' => false, 'message' => 'end_create_time must be within the last 7 days'];
|
||
}
|
||
$maxEndForStart = strtotime('+' . self::PLAYER_PULL_RECORD_MAX_RANGE_DAYS . ' days', $startTs);
|
||
if ($maxEndForStart === false) {
|
||
$maxEndForStart = $startTs + self::PLAYER_PULL_RECORD_MAX_RANGE_DAYS * 86400;
|
||
}
|
||
if ($endTs > $maxEndForStart) {
|
||
return ['ok' => false, 'message' => 'Time range cannot exceed 7 days'];
|
||
}
|
||
|
||
return ['ok' => true, 'start' => date('Y-m-d H:i:s', $startTs), 'end' => date('Y-m-d H:i:s', $endTs)];
|
||
}
|
||
|
||
return ['ok' => false, 'message' => 'Invalid time parameters'];
|
||
}
|
||
|
||
/**
|
||
* 获取游戏记录
|
||
* POST 参数:username(非必填), start_create_time, end_create_time(可选,须在「最近 7 天」内且跨度不超过 7 天;均不传则默认最近 7 天), limit(非必填,返回条数上限)
|
||
* 返回 DicePlayRecord 中非敏感信息
|
||
*/
|
||
public function getPlayerGameRecord(Request $request): Response
|
||
{
|
||
$username = trim((string) ($request->post('username', '')));
|
||
$deptId = $this->agentDeptId($request);
|
||
$startCreateTime = trim((string) ($request->post('start_create_time', '')));
|
||
$endCreateTime = trim((string) ($request->post('end_create_time', '')));
|
||
$window = $this->resolvePullRecordTimeWindow($startCreateTime, $endCreateTime);
|
||
if (!$window['ok']) {
|
||
return $this->fail($window['message'], ReturnCode::PARAMS_ERROR);
|
||
}
|
||
$limit = $this->resolvePullRecordLimit($request);
|
||
|
||
$query = DicePlayRecord::where('dept_id', $deptId)->order('id', 'desc');
|
||
|
||
if ($username !== '') {
|
||
$player = $this->findPlayerByUsername($username, $deptId);
|
||
if (!$player) {
|
||
return $this->success([]);
|
||
}
|
||
$query->where('player_id', (int) $player->id);
|
||
}
|
||
|
||
$query->where('create_time', '>=', $window['start']);
|
||
$query->where('create_time', '<=', $window['end']);
|
||
|
||
$list = $query->limit($limit)->select()->toArray();
|
||
$playerIds = array_unique(array_column($list, 'player_id'));
|
||
if (!empty($playerIds)) {
|
||
$players = DicePlayer::whereIn('id', $playerIds)->where('dept_id', $deptId)->field('id,username,phone')->select()->toArray();
|
||
$playerMap = [];
|
||
foreach ($players as $p) {
|
||
$playerMap[(int) ($p['id'] ?? 0)] = $p;
|
||
}
|
||
foreach ($list as &$item) {
|
||
$item['dice_player'] = $playerMap[(int) ($item['player_id'] ?? 0)] ?? null;
|
||
}
|
||
}
|
||
|
||
return $this->success($list);
|
||
}
|
||
|
||
/**
|
||
* 获取钱包流水
|
||
* POST 参数:username(非必填), start_create_time, end_create_time(可选,规则同 getPlayerGameRecord), limit(非必填)
|
||
* 返回 DicePlayerWalletRecord 中非敏感信息
|
||
*/
|
||
public function getPlayerWalletRecord(Request $request): Response
|
||
{
|
||
$username = trim((string) ($request->post('username', '')));
|
||
$deptId = $this->agentDeptId($request);
|
||
$startCreateTime = trim((string) ($request->post('start_create_time', '')));
|
||
$endCreateTime = trim((string) ($request->post('end_create_time', '')));
|
||
$window = $this->resolvePullRecordTimeWindow($startCreateTime, $endCreateTime);
|
||
if (!$window['ok']) {
|
||
return $this->fail($window['message'], ReturnCode::PARAMS_ERROR);
|
||
}
|
||
$limit = $this->resolvePullRecordLimit($request);
|
||
|
||
$query = DicePlayerWalletRecord::where('dept_id', $deptId)->order('id', 'desc');
|
||
|
||
if ($username !== '') {
|
||
$player = $this->findPlayerByUsername($username, $deptId);
|
||
if (!$player) {
|
||
return $this->success([]);
|
||
}
|
||
$query->where('player_id', (int) $player->id);
|
||
}
|
||
|
||
$query->where('create_time', '>=', $window['start']);
|
||
$query->where('create_time', '<=', $window['end']);
|
||
|
||
$list = $query->with(['dicePlayer' => function ($q) {
|
||
$q->field('id,username,phone');
|
||
}])->limit($limit)->select()->toArray();
|
||
|
||
return $this->success($list);
|
||
}
|
||
|
||
/**
|
||
* 获取用户中奖券获取记录
|
||
* POST 参数:username(非必填), start_create_time, end_create_time(可选,规则同 getPlayerGameRecord), limit(非必填)
|
||
* 返回 DicePlayerTicketRecord 中非敏感信息
|
||
*/
|
||
public function getPlayerTicketRecord(Request $request): Response
|
||
{
|
||
$username = trim((string) ($request->post('username', '')));
|
||
$deptId = $this->agentDeptId($request);
|
||
$startCreateTime = trim((string) ($request->post('start_create_time', '')));
|
||
$endCreateTime = trim((string) ($request->post('end_create_time', '')));
|
||
$window = $this->resolvePullRecordTimeWindow($startCreateTime, $endCreateTime);
|
||
if (!$window['ok']) {
|
||
return $this->fail($window['message'], ReturnCode::PARAMS_ERROR);
|
||
}
|
||
$limit = $this->resolvePullRecordLimit($request);
|
||
|
||
$query = DicePlayerTicketRecord::where('dept_id', $deptId)->order('id', 'desc');
|
||
|
||
if ($username !== '') {
|
||
$player = $this->findPlayerByUsername($username, $deptId);
|
||
if (!$player) {
|
||
return $this->success([]);
|
||
}
|
||
$query->where('player_id', (int) $player->id);
|
||
}
|
||
|
||
$query->where('create_time', '>=', $window['start']);
|
||
$query->where('create_time', '<=', $window['end']);
|
||
|
||
$list = $query->with(['dicePlayer' => function ($q) {
|
||
$q->field('id,username,phone');
|
||
}])->limit($limit)->select()->toArray();
|
||
|
||
return $this->success($list);
|
||
}
|
||
|
||
/**
|
||
* 钱包转入转出
|
||
* POST 参数:username(必填), coin(转入>0 或 转出<0)
|
||
* 创建 DicePlayerWalletRecord,type: 0=充值(coin>0), 1=提现(coin<0)
|
||
* 返回创建的记录
|
||
*/
|
||
public function setPlayerWallet(Request $request): Response
|
||
{
|
||
$username = trim((string) ($request->post('username', '')));
|
||
$deptId = $this->agentDeptId($request);
|
||
$coin = $request->post('coin');
|
||
|
||
if ($username === '') {
|
||
return $this->fail('username is required', ReturnCode::PARAMS_ERROR);
|
||
}
|
||
if ($coin === null || $coin === '') {
|
||
return $this->fail('coin is required', ReturnCode::PARAMS_ERROR);
|
||
}
|
||
|
||
$coinVal = (float) $coin;
|
||
if ($coinVal === 0.0) {
|
||
return $this->fail('coin cannot be 0', ReturnCode::PARAMS_ERROR);
|
||
}
|
||
|
||
$player = $this->findPlayerByUsername($username, $deptId);
|
||
if (!$player) {
|
||
return $this->fail('User not found', ReturnCode::PARAMS_ERROR);
|
||
}
|
||
|
||
$walletBefore = (float) ($player->coin ?? 0);
|
||
$walletAfter = $walletBefore + $coinVal;
|
||
|
||
if ($coinVal < 0 && $walletBefore < -$coinVal) {
|
||
return $this->fail('Insufficient balance to transfer', ReturnCode::BUSINESS_ERROR);
|
||
}
|
||
|
||
$type = $coinVal > 0 ? 0 : 1;
|
||
$remark = $coinVal > 0 ? '充值' : '提现';
|
||
|
||
try {
|
||
Db::startTrans();
|
||
$player->coin = $walletAfter;
|
||
$player->save();
|
||
|
||
$adminId = ($player->admin_id ?? null) ? (int) $player->admin_id : null;
|
||
$record = DicePlayerWalletRecord::create([
|
||
'dept_id' => $deptId,
|
||
'player_id' => (int) $player->id,
|
||
'admin_id' => $adminId,
|
||
'coin' => $coinVal,
|
||
'type' => $type,
|
||
'wallet_before' => $walletBefore,
|
||
'wallet_after' => $walletAfter,
|
||
'total_ticket_count' => 0,
|
||
'paid_ticket_count' => 0,
|
||
'free_ticket_count' => 0,
|
||
'remark' => $remark,
|
||
'user_id' => 0,
|
||
]);
|
||
Db::commit();
|
||
} catch (\Throwable $e) {
|
||
Db::rollback();
|
||
return $this->fail('Operation failed: ' . $e->getMessage(), ReturnCode::SERVER_ERROR);
|
||
}
|
||
|
||
// 出于安全:删除该玩家相关缓存,后续 API 调用按需重建
|
||
UserCache::deleteUser($player->id);
|
||
if ($player->username !== '') {
|
||
UserCache::deletePlayerByUsername($player->username);
|
||
UserCache::deletePlayerByUsername($this->scopedUsername($deptId, (string) $player->username));
|
||
}
|
||
|
||
$recordArr = $record->toArray();
|
||
$recordArr['dice_player'] = ['id' => (int) $player->id, 'username' => $player->username ?? '', 'phone' => $player->phone ?? ''];
|
||
return $this->success($recordArr);
|
||
}
|
||
|
||
private function resolveLang($lang): string
|
||
{
|
||
if (!is_string($lang)) {
|
||
return 'zh';
|
||
}
|
||
$langValue = strtolower(trim($lang));
|
||
if (!in_array($langValue, ['zh', 'en'], true)) {
|
||
return 'zh';
|
||
}
|
||
return $langValue;
|
||
}
|
||
|
||
private function buildPublicGameList(string $lang, int $deptId): array
|
||
{
|
||
$rows = DiceGame::where('status', 1)
|
||
->where('dept_id', $deptId)
|
||
->order('sort', 'asc')
|
||
->order('id', 'asc')
|
||
->field(array_merge(self::GAME_PUBLIC_FIELDS, ['game_name', 'game_name_en']))
|
||
->select()
|
||
->toArray();
|
||
if (empty($rows)) {
|
||
return [];
|
||
}
|
||
|
||
$games = [];
|
||
foreach ($rows as $row) {
|
||
$game = [];
|
||
foreach (self::GAME_PUBLIC_FIELDS as $fieldName) {
|
||
$game[$fieldName] = $row[$fieldName] ?? '';
|
||
}
|
||
$gameNameEn = $row['game_name_en'] ?? '';
|
||
$game['game_name'] = $lang === 'en' && $gameNameEn !== '' ? $gameNameEn : ($row['game_name'] ?? '');
|
||
$games[] = $game;
|
||
}
|
||
return $games;
|
||
}
|
||
|
||
private function agentDeptId(Request $request): int
|
||
{
|
||
return (int) ($request->agent_dept_id ?? 0);
|
||
}
|
||
|
||
private function agentAdminId(Request $request): ?int
|
||
{
|
||
$adminId = (int) ($request->agent_admin_id ?? 0);
|
||
return $adminId > 0 ? $adminId : null;
|
||
}
|
||
|
||
private function scopedUsername(int $deptId, string $username): string
|
||
{
|
||
return $deptId . ':' . $username;
|
||
}
|
||
|
||
private function findPlayerByUsername(string $username, int $deptId): ?DicePlayer
|
||
{
|
||
$player = DicePlayer::where('username', $username)->where('dept_id', $deptId)->find();
|
||
return $player ?: null;
|
||
}
|
||
}
|