[积分商城]优化对接API

This commit is contained in:
2026-03-30 11:47:32 +08:00
parent b30ef21780
commit 4a42899bfe
55 changed files with 835 additions and 1241 deletions

View File

@@ -75,6 +75,9 @@ class Common extends Api
if ($refreshToken['type'] == UserAuth::TOKEN_TYPE . '-refresh') {
Token::set($newToken, UserAuth::TOKEN_TYPE, $refreshToken['user_id'], (int)config('buildadmin.user_token_keep_time', 259200));
}
if ($refreshToken['type'] == UserAuth::TOKEN_TYPE_MALL_USER . '-refresh') {
Token::set($newToken, UserAuth::TOKEN_TYPE_MALL_USER, $refreshToken['user_id'], (int)config('buildadmin.user_token_keep_time', 259200));
}
return $this->success('', [
'type' => $refreshToken['type'],

View File

@@ -4,9 +4,14 @@ declare(strict_types=1);
namespace app\api\controller\v1;
use ba\Random;
use Throwable;
use app\common\controller\Api;
use app\common\facade\Token;
use app\common\library\Auth as UserAuth;
use app\common\library\AgentJwt;
use app\common\model\ChannelManage;
use app\common\model\MallPlayxUserAsset;
use app\admin\model\Admin;
use Webman\Http\Request;
use support\Response;
@@ -26,6 +31,11 @@ class Auth extends Api
*/
protected int $timeTolerance = 300;
/**
* 临时登录 token 有效期(秒)
*/
protected int $tempTokenExpire = 86400;
/**
* 获取鉴权 TokenGET 请求)
* 参数仅从 Query 读取signature、secret、agent_id、time
@@ -47,7 +57,7 @@ class Auth extends Api
return $this->error(__('Parameter signature/secret/agent_id/time can not be empty'));
}
$timestamp = (int) $time;
$timestamp = intval($time);
if ($timestamp <= 0) {
return $this->error(__('Invalid timestamp'));
}
@@ -62,7 +72,7 @@ class Auth extends Api
return $this->error(__('Agent not found'));
}
$channelId = (int) ($admin->channel_id ?? 0);
$channelId = intval($admin->channel_id ?? 0);
if ($channelId <= 0) {
return $this->error(__('Agent not found'));
}
@@ -81,7 +91,7 @@ class Auth extends Api
return $this->error(__('Invalid signature'));
}
$expire = (int) config('buildadmin.agent_auth.token_expire', 86400);
$expire = intval(config('buildadmin.agent_auth.token_expire', 86400));
$payload = [
'agent_id' => $agentId,
'channel_id' => $channel->id,
@@ -93,4 +103,52 @@ class Auth extends Api
'authtoken' => $authtoken,
]);
}
/**
* H5 临时登录GET/POST
* 参数username
* 写入或复用 mall_playx_user_asset签发 muser 类型 tokenuser_id 为资产表主键)
*/
public function temLogin(Request $request): Response
{
$response = $this->initializeApi($request);
if ($response !== null) {
return $response;
}
$enabled = config('buildadmin.agent_auth.temp_login_enable', false);
if (!$enabled) {
return $this->error(__('Temp login is disabled'));
}
$username = trim(strval($request->get('username', $request->post('username', ''))));
if ($username === '') {
return $this->error(__('Parameter username can not be empty'));
}
try {
$asset = MallPlayxUserAsset::ensureForUsername($username);
} catch (Throwable $e) {
return $this->error($e->getMessage());
}
$token = Random::uuid();
$refreshToken = Random::uuid();
$expire = config('buildadmin.agent_auth.temp_login_expire', $this->tempTokenExpire);
$assetId = intval($asset->getKey());
Token::set($token, UserAuth::TOKEN_TYPE_MALL_USER, $assetId, $expire);
Token::set($refreshToken, UserAuth::TOKEN_TYPE_MALL_USER . '-refresh', $assetId, 2592000);
return $this->success('', [
'userInfo' => [
'id' => $assetId,
'username' => strval($asset->username ?? ''),
'nickname' => strval($asset->username ?? ''),
'playx_user_id' => strval($asset->playx_user_id ?? ''),
'token' => $token,
'refresh_token' => $refreshToken,
'expires_in' => $expire,
],
]);
}
}

View File

@@ -4,7 +4,10 @@ declare(strict_types=1);
namespace app\api\controller\v1;
use ba\Random;
use app\common\controller\Api;
use app\common\facade\Token;
use app\common\library\Auth as UserAuth;
use app\common\model\MallItem;
use app\common\model\MallPlayxClaimLog;
use app\common\model\MallPlayxDailyPush;
@@ -21,28 +24,125 @@ use support\Response;
class Playx extends Api
{
/**
* 从请求解析 PlayX 会话用户ID优先 session_id其次 user_id
* 从请求解析 mall_playx_user_asset.idmuser token、session、user_id 均指向资产表主键或 playx_user_id
*/
private function resolveUserIdFromRequest(Request $request): ?string
private function resolvePlayxAssetIdFromRequest(Request $request): ?int
{
$sessionId = strval($request->post('session_id', $request->get('session_id', '')));
if ($sessionId !== '') {
$session = MallPlayxSession::where('session_id', $sessionId)->find();
if (!$session) {
return null;
if ($session) {
$expireTime = intval($session->expire_time ?? 0);
if ($expireTime > time()) {
$asset = MallPlayxUserAsset::where('playx_user_id', strval($session->user_id ?? ''))->find();
if ($asset) {
return intval($asset->getKey());
}
}
}
$expireTime = intval($session->expire_time ?? 0);
if ($expireTime <= time()) {
return null;
$assetId = $this->resolveAssetIdByToken($sessionId);
if ($assetId !== null) {
return $assetId;
}
return strval($session->user_id ?? '');
}
$token = strval($request->post('token', $request->get('token', '')));
if ($token === '') {
$token = get_auth_token(['ba', 'token'], $request);
}
if ($token !== '') {
return $this->resolveAssetIdByToken($token);
}
$userId = strval($request->post('user_id', $request->get('user_id', '')));
if ($userId === '') {
return null;
}
return $userId;
if (ctype_digit($userId)) {
return intval($userId);
}
$asset = MallPlayxUserAsset::where('playx_user_id', $userId)->find();
if ($asset) {
return intval($asset->getKey());
}
return null;
}
private function resolveAssetIdByToken(string $token): ?int
{
$tokenData = Token::get($token);
$tokenType = strval($tokenData['type'] ?? '');
$isMemberOrMall = $tokenType === UserAuth::TOKEN_TYPE || $tokenType === UserAuth::TOKEN_TYPE_MALL_USER;
if (!empty($tokenData)
&& $isMemberOrMall
&& intval($tokenData['expire_time'] ?? 0) > time()
&& intval($tokenData['user_id'] ?? 0) > 0
) {
return intval($tokenData['user_id']);
}
return null;
}
private function buildTempPhone(): ?string
{
for ($i = 0; $i < 8; $i++) {
$candidate = '13' . str_pad(strval(mt_rand(0, 999999999)), 9, '0', STR_PAD_LEFT);
if (!MallPlayxUserAsset::where('phone', $candidate)->find()) {
return $candidate;
}
}
return null;
}
private function ensureAssetForPlayx(string $playxUserId, string $username): ?MallPlayxUserAsset
{
$asset = MallPlayxUserAsset::where('playx_user_id', $playxUserId)->find();
if ($asset) {
return $asset;
}
$effectiveUsername = trim($username);
if ($effectiveUsername === '') {
$effectiveUsername = 'playx_' . $playxUserId;
}
$byName = MallPlayxUserAsset::where('username', $effectiveUsername)->find();
if ($byName) {
$byName->playx_user_id = $playxUserId;
$byName->save();
return $byName;
}
$phone = $this->buildTempPhone();
if ($phone === null) {
return null;
}
$pwd = hash_password(Random::build('alnum', 16));
$now = time();
return MallPlayxUserAsset::create([
'playx_user_id' => $playxUserId,
'username' => $effectiveUsername,
'phone' => $phone,
'password' => $pwd,
'admin_id' => 0,
'locked_points' => 0,
'available_points' => 0,
'today_limit' => 0,
'today_claimed' => 0,
'today_limit_date' => null,
'create_time' => $now,
'update_time' => $now,
]);
}
private function getAssetById(int $assetId): ?MallPlayxUserAsset
{
return MallPlayxUserAsset::where('id', $assetId)->find();
}
/**
@@ -66,11 +166,11 @@ class Playx extends Api
$requestId = $body['request_id'] ?? '';
$date = $body['date'] ?? '';
$userId = $body['user_id'] ?? '';
$playxUserId = strval($body['user_id'] ?? '');
$yesterdayWinLossNet = $body['yesterday_win_loss_net'] ?? 0;
$yesterdayTotalDeposit = $body['yesterday_total_deposit'] ?? 0;
if ($requestId === '' || $date === '' || $userId === '') {
if ($requestId === '' || $date === '' || $playxUserId === '') {
return $this->error(__('Missing required fields: request_id, date, user_id'));
}
@@ -80,29 +180,29 @@ class Playx extends Api
$ts = $request->header('X-Timestamp', '');
$rid = $request->header('X-Request-Id', '');
if ($sig === '' || $ts === '' || $rid === '') {
return $this->error('INVALID_SIGNATURE', null, 0, ['statusCode' => 401]);
return $this->error(__('Invalid signature'), null, 0, ['statusCode' => 401]);
}
$canonical = $ts . "\n" . $rid . "\nPOST\n/api/v1/playx/daily-push\n" . hash('sha256', json_encode($body));
$expected = hash_hmac('sha256', $canonical, $secret);
if (!hash_equals($expected, $sig)) {
return $this->error('INVALID_SIGNATURE', null, 0, ['statusCode' => 401]);
return $this->error(__('Invalid signature'), null, 0, ['statusCode' => 401]);
}
}
$exists = MallPlayxDailyPush::where('user_id', $userId)->where('date', $date)->find();
$exists = MallPlayxDailyPush::where('user_id', $playxUserId)->where('date', $date)->find();
if ($exists) {
return $this->success('', [
'request_id' => $requestId,
'accepted' => true,
'deduped' => true,
'message' => 'duplicate input',
'message' => __('Duplicate input'),
]);
}
Db::startTrans();
try {
MallPlayxDailyPush::create([
'user_id' => $userId,
'user_id' => $playxUserId,
'date' => $date,
'username' => $body['username'] ?? '',
'yesterday_win_loss_net' => $yesterdayWinLossNet,
@@ -121,30 +221,23 @@ class Playx extends Api
}
$todayLimit = intval(round(floatval($yesterdayTotalDeposit) * $unlockRatio));
$asset = MallPlayxUserAsset::where('user_id', $userId)->find();
$todayLimitDate = $date;
if ($asset) {
if ($asset->today_limit_date !== $todayLimitDate) {
$asset->today_claimed = 0;
$asset->today_limit_date = $todayLimitDate;
}
$asset->locked_points += $newLocked;
$asset->today_limit = $todayLimit;
$asset->username = $body['username'] ?? $asset->username;
$asset->save();
} else {
MallPlayxUserAsset::create([
'user_id' => $userId,
'username' => $body['username'] ?? '',
'locked_points' => $newLocked,
'available_points' => 0,
'today_limit' => $todayLimit,
'today_claimed' => 0,
'today_limit_date' => $todayLimitDate,
'create_time' => time(),
'update_time' => time(),
]);
$asset = $this->ensureAssetForPlayx($playxUserId, strval($body['username'] ?? ''));
if (!$asset) {
throw new \RuntimeException(__('Failed to map playx user to mall user'));
}
$todayLimitDate = $date;
if ($asset->today_limit_date !== $todayLimitDate) {
$asset->today_claimed = 0;
$asset->today_limit_date = $todayLimitDate;
}
$asset->locked_points = intval($asset->locked_points ?? 0) + $newLocked;
$asset->today_limit = $todayLimit;
$asset->playx_user_id = $playxUserId;
$uname = trim(strval($body['username'] ?? ''));
if ($uname !== '') {
$asset->username = $uname;
}
$asset->save();
Db::commit();
} catch (\Throwable $e) {
@@ -156,13 +249,13 @@ class Playx extends Api
'request_id' => $requestId,
'accepted' => true,
'deduped' => false,
'message' => 'ok',
'message' => __('Ok'),
]);
}
/**
* Token 验证 - 接收前端 token调用 PlayX 验证(占位,待 PlayX 提供 API
* POST /api/v1/playx/verify-token
* Token 验证 - POST /api/v1/playx/verify-token
* 配置 playx.verify_token_local_only=true 时仅本地校验 token不请求 PlayX
*/
public function verifyToken(Request $request): Response
{
@@ -171,15 +264,19 @@ class Playx extends Api
return $response;
}
$token = $request->post('token', $request->post('session', ''));
$token = strval($request->post('token', $request->post('session', $request->get('token', ''))));
if ($token === '') {
return $this->error('INVALID_TOKEN', null, 0, ['statusCode' => 401]);
return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]);
}
if (config('playx.verify_token_local_only', false)) {
return $this->verifyTokenLocal($token);
}
$baseUrl = config('playx.api.base_url', '');
$verifyUrl = config('playx.api.token_verify_url', '/api/v1/auth/verify-token');
if ($baseUrl === '') {
return $this->error('PlayX API not configured');
return $this->error(__('PlayX API not configured'));
}
try {
@@ -196,7 +293,10 @@ class Playx extends Api
$code = $res->getStatusCode();
$data = json_decode(strval($res->getBody()), true);
if ($code !== 200 || empty($data['user_id'])) {
return $this->error($data['message'] ?? 'INVALID_TOKEN', null, 0, ['statusCode' => 401]);
$remoteMsg = $data['message'] ?? '';
$msg = is_string($remoteMsg) && $remoteMsg !== '' ? $remoteMsg : __('Invalid token');
return $this->error($msg, null, 0, ['statusCode' => 401]);
}
$userId = strval($data['user_id']);
@@ -231,6 +331,53 @@ class Playx extends Api
}
}
/**
* 本地校验 temLogin 等写入的商城 token类型 muser写入 mall_playx_session
*/
private function verifyTokenLocal(string $token): Response
{
$tokenData = Token::get($token);
if (empty($tokenData) || (isset($tokenData['expire_time']) && intval($tokenData['expire_time']) <= time())) {
return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]);
}
$tokenType = strval($tokenData['type'] ?? '');
if ($tokenType !== UserAuth::TOKEN_TYPE_MALL_USER) {
return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]);
}
$assetId = intval($tokenData['user_id'] ?? 0);
if ($assetId <= 0) {
return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]);
}
$asset = MallPlayxUserAsset::where('id', $assetId)->find();
if (!$asset) {
return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]);
}
$playxUserId = strval($asset->playx_user_id ?? '');
if ($playxUserId === '') {
$playxUserId = strval($assetId);
}
$expireAt = time() + intval(config('playx.session_expire_seconds', 3600));
$sessionId = bin2hex(random_bytes(16));
MallPlayxSession::create([
'session_id' => $sessionId,
'user_id' => $playxUserId,
'username' => strval($asset->username ?? ''),
'expire_time' => $expireAt,
'create_time' => time(),
'update_time' => time(),
]);
return $this->success('', [
'session_id' => $sessionId,
'user_id' => $playxUserId,
'username' => strval($asset->username ?? ''),
'token_expire_at' => date('c', $expireAt),
]);
}
/**
* 用户资产
* GET /api/v1/playx/assets?user_id=xxx
@@ -242,12 +389,12 @@ class Playx extends Api
return $response;
}
$userId = $this->resolveUserIdFromRequest($request);
if ($userId === null) {
return $this->error('INVALID_TOKEN', null, 0, ['statusCode' => 401]);
$assetId = $this->resolvePlayxAssetIdFromRequest($request);
if ($assetId === null) {
return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]);
}
$asset = MallPlayxUserAsset::where('user_id', $userId)->find();
$asset = $this->getAssetById($assetId);
if (!$asset) {
return $this->success('', [
'locked_points' => 0,
@@ -282,22 +429,22 @@ class Playx extends Api
}
$claimRequestId = strval($request->post('claim_request_id', ''));
$userId = $this->resolveUserIdFromRequest($request);
if ($claimRequestId === '' || $userId === null) {
$assetId = $this->resolvePlayxAssetIdFromRequest($request);
if ($claimRequestId === '' || $assetId === null) {
return $this->error(__('claim_request_id and user_id/session_id required'));
}
$asset = $this->getAssetById($assetId);
if (!$asset || strval($asset->playx_user_id ?? '') === '') {
return $this->error(__('User asset not found'));
}
$playxUserId = strval($asset->playx_user_id);
$exists = MallPlayxClaimLog::where('claim_request_id', $claimRequestId)->find();
if ($exists) {
$asset = MallPlayxUserAsset::where('user_id', $userId)->find();
return $this->success('', $this->formatAsset($asset));
}
$asset = MallPlayxUserAsset::where('user_id', $userId)->find();
if (!$asset) {
return $this->error(__('User asset not found'));
}
$todayLimitDate = date('Y-m-d');
if ($asset->today_limit_date !== $todayLimitDate) {
$asset->today_claimed = 0;
@@ -315,7 +462,7 @@ class Playx extends Api
try {
MallPlayxClaimLog::create([
'claim_request_id' => $claimRequestId,
'user_id' => $userId,
'user_id' => $playxUserId,
'claimed_amount' => $canClaim,
'create_time' => time(),
]);
@@ -395,12 +542,16 @@ class Playx extends Api
return $response;
}
$userId = $this->resolveUserIdFromRequest($request);
if ($userId === null) {
return $this->error('INVALID_TOKEN', null, 0, ['statusCode' => 401]);
$assetId = $this->resolvePlayxAssetIdFromRequest($request);
if ($assetId === null) {
return $this->error(__('Invalid token'), null, 0, ['statusCode' => 401]);
}
$asset = $this->getAssetById($assetId);
if (!$asset || strval($asset->playx_user_id ?? '') === '') {
return $this->success('', ['list' => []]);
}
$list = MallPlayxOrder::where('user_id', $userId)
$list = MallPlayxOrder::where('user_id', strval($asset->playx_user_id))
->with(['mallItem'])
->order('id', 'desc')
->limit(100)
@@ -438,8 +589,8 @@ class Playx extends Api
}
$itemId = intval($request->post('item_id', 0));
$userId = $this->resolveUserIdFromRequest($request);
if ($itemId <= 0 || $userId === null) {
$assetId = $this->resolvePlayxAssetIdFromRequest($request);
if ($itemId <= 0 || $assetId === null) {
return $this->error(__('item_id and user_id/session_id required'));
}
@@ -448,10 +599,11 @@ class Playx extends Api
return $this->error(__('Item not found or not available'));
}
$asset = MallPlayxUserAsset::where('user_id', $userId)->find();
if (!$asset || $asset->available_points < $item->score) {
$asset = $this->getAssetById($assetId);
if (!$asset || strval($asset->playx_user_id ?? '') === '' || $asset->available_points < $item->score) {
return $this->error(__('Insufficient points'));
}
$playxUserId = strval($asset->playx_user_id);
$multiplier = intval($item->multiplier ?? 0);
if ($multiplier <= 0) {
@@ -466,7 +618,7 @@ class Playx extends Api
$orderNo = 'BONUS_ORD' . date('YmdHis') . mt_rand(1000, 9999);
$order = MallPlayxOrder::create([
'user_id' => $userId,
'user_id' => $playxUserId,
'type' => MallPlayxOrder::TYPE_BONUS,
'status' => MallPlayxOrder::STATUS_PENDING,
'mall_item_id' => $item->id,
@@ -487,7 +639,7 @@ class Playx extends Api
$baseUrl = config('playx.api.base_url', '');
if ($baseUrl !== '') {
$this->callPlayxBonusGrant($order, $item, $userId);
$this->callPlayxBonusGrant($order, $item, $playxUserId);
}
return $this->success(__('Redeem submitted, please wait about 10 minutes'), [
@@ -504,11 +656,11 @@ class Playx extends Api
}
$itemId = intval($request->post('item_id', 0));
$userId = $this->resolveUserIdFromRequest($request);
$assetId = $this->resolvePlayxAssetIdFromRequest($request);
$receiverName = $request->post('receiver_name', '');
$receiverPhone = $request->post('receiver_phone', '');
$receiverAddress = $request->post('receiver_address', '');
if ($itemId <= 0 || $userId === null || $receiverName === '' || $receiverPhone === '' || $receiverAddress === '') {
if ($itemId <= 0 || $assetId === null || $receiverName === '' || $receiverPhone === '' || $receiverAddress === '') {
return $this->error(__('Missing required fields'));
}
@@ -520,10 +672,11 @@ class Playx extends Api
return $this->error(__('Out of stock'));
}
$asset = MallPlayxUserAsset::where('user_id', $userId)->find();
if (!$asset || $asset->available_points < $item->score) {
$asset = $this->getAssetById($assetId);
if (!$asset || strval($asset->playx_user_id ?? '') === '' || $asset->available_points < $item->score) {
return $this->error(__('Insufficient points'));
}
$playxUserId = strval($asset->playx_user_id);
Db::startTrans();
try {
@@ -531,7 +684,7 @@ class Playx extends Api
$asset->save();
MallPlayxOrder::create([
'user_id' => $userId,
'user_id' => $playxUserId,
'type' => MallPlayxOrder::TYPE_PHYSICAL,
'status' => MallPlayxOrder::STATUS_PENDING,
'mall_item_id' => $item->id,
@@ -565,8 +718,8 @@ class Playx extends Api
}
$itemId = intval($request->post('item_id', 0));
$userId = $this->resolveUserIdFromRequest($request);
if ($itemId <= 0 || $userId === null) {
$assetId = $this->resolvePlayxAssetIdFromRequest($request);
if ($itemId <= 0 || $assetId === null) {
return $this->error(__('item_id and user_id/session_id required'));
}
@@ -575,10 +728,11 @@ class Playx extends Api
return $this->error(__('Item not found or not available'));
}
$asset = MallPlayxUserAsset::where('user_id', $userId)->find();
if (!$asset || $asset->available_points < $item->score) {
$asset = $this->getAssetById($assetId);
if (!$asset || strval($asset->playx_user_id ?? '') === '' || $asset->available_points < $item->score) {
return $this->error(__('Insufficient points'));
}
$playxUserId = strval($asset->playx_user_id);
$multiplier = intval($item->multiplier ?? 0);
if ($multiplier <= 0) {
@@ -593,7 +747,7 @@ class Playx extends Api
$orderNo = 'WITHDRAW_ORD' . date('YmdHis') . mt_rand(1000, 9999);
$order = MallPlayxOrder::create([
'user_id' => $userId,
'user_id' => $playxUserId,
'type' => MallPlayxOrder::TYPE_WITHDRAW,
'status' => MallPlayxOrder::STATUS_PENDING,
'mall_item_id' => $item->id,
@@ -614,7 +768,7 @@ class Playx extends Api
$baseUrl = config('playx.api.base_url', '');
if ($baseUrl !== '') {
$this->callPlayxBalanceCredit($order, $userId);
$this->callPlayxBalanceCredit($order, $playxUserId);
}
return $this->success(__('Withdraw submitted, please wait about 10 minutes'), [