Files
lotteryLaravel/app/Services/Wallet/HttpMainSiteWalletBalanceClient.php
kang fe0594beaa feat: 增强钱包 API URL 验证与配置
- 在 AdminIntegrationSiteStoreRequest 和 AdminIntegrationSiteUpdateRequest 中引入 WalletApiUrlRule,确保 wallet_api_url 字段符合 HTTPS 公开域名要求。
- 更新 HttpMainSiteWalletBalanceClient 和 HttpMainSiteWalletGateway,使用 WalletApiUrlSanitizer 进行 URL 规范化与验证,防止 SSRF 攻击。
- 新增测试用例,验证 wallet_api_url 的有效性,确保系统安全性与稳定性。
- 更新 .env.example 文件,添加 LOTTERY_RISK_POOL_USE_REDIS_LUA 配置项以支持 Redis Lua 原子扣减功能。
- 修改 package-lock.json 中的项目名称,确保一致性。
- 在 API 路由中新增 integration/runtime-origins 路由,提供运行时白名单功能。
2026-05-28 10:10:26 +08:00

173 lines
5.8 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
namespace App\Services\Wallet;
use App\Models\Player;
use Illuminate\Support\Facades\Http;
use App\Services\Integration\PartnerSiteConfigResolver;
use App\Support\Integration\WalletApiUrlSanitizer;
/**
* 查询主站钱包余额(供玩家端余额接口填充 main_balance
*/
final class HttpMainSiteWalletBalanceClient
{
public function __construct(
private readonly PartnerSiteConfigResolver $partnerSiteConfigResolver,
) {}
public function fetch(Player $player, string $currencyCode): ?int
{
$probe = $this->probe($player, $currencyCode);
return $probe->success ? $probe->mainBalanceMinor : null;
}
public function probe(Player $player, string $currencyCode): MainSiteWalletBalanceProbeResult
{
$config = $this->partnerSiteConfigResolver->resolveForPlayer($player);
$currencyCode = trim($currencyCode) !== '' ? trim($currencyCode) : 'NPR';
if (! $config->enabled) {
return new MainSiteWalletBalanceProbeResult(
success: false,
mainBalanceMinor: null,
currencyCode: $currencyCode,
requestUrl: '',
httpStatus: null,
message: '接入站点已停用或未配置',
responseBody: null,
);
}
if (! $config->hasWalletApi()) {
return new MainSiteWalletBalanceProbeResult(
success: false,
mainBalanceMinor: null,
currencyCode: $currencyCode,
requestUrl: '',
httpStatus: null,
message: '未配置主站钱包 API URL',
responseBody: null,
);
}
$base = WalletApiUrlSanitizer::normalizeAndValidate($config->walletApiUrl);
if ($base === null) {
return new MainSiteWalletBalanceProbeResult(
success: false,
mainBalanceMinor: null,
currencyCode: $currencyCode,
requestUrl: '',
httpStatus: null,
message: 'wallet_api_url 无效(拒绝以防 SSRF',
responseBody: null,
);
}
$path = $config->walletBalancePath;
$url = $base.'/'.ltrim($path, '/');
$timeout = $config->walletTimeoutSeconds;
$apiKey = $config->walletApiKey;
$headers = ['Accept' => 'application/json'];
if (is_string($apiKey) && $apiKey !== '') {
$headers['Authorization'] = 'Bearer '.$apiKey;
}
$query = [
'site_code' => $player->site_code,
'site_player_id' => $player->site_player_id,
'currency_code' => $currencyCode,
];
try {
$response = Http::withHeaders($headers)
->timeout($timeout)
->acceptJson()
->get($url, $query);
} catch (\Throwable $e) {
return new MainSiteWalletBalanceProbeResult(
success: false,
mainBalanceMinor: null,
currencyCode: $currencyCode,
requestUrl: $url.'?'.http_build_query($query),
httpStatus: null,
message: '请求失败: '.$e->getMessage(),
responseBody: null,
);
}
$httpStatus = $response->status();
$payload = $response->json();
$preview = is_array($payload) ? self::truncateResponsePreview($payload) : null;
if (! $response->successful()) {
$message = is_array($payload) && is_string($payload['message'] ?? null)
? (string) $payload['message']
: 'HTTP '.$httpStatus;
return new MainSiteWalletBalanceProbeResult(
success: false,
mainBalanceMinor: null,
currencyCode: $currencyCode,
requestUrl: $url.'?'.http_build_query($query),
httpStatus: $httpStatus,
message: $message,
responseBody: $preview,
);
}
if (! is_array($payload)) {
return new MainSiteWalletBalanceProbeResult(
success: false,
mainBalanceMinor: null,
currencyCode: $currencyCode,
requestUrl: $url.'?'.http_build_query($query),
httpStatus: $httpStatus,
message: '响应不是 JSON 对象',
responseBody: null,
);
}
$raw = data_get($payload, 'data.main_balance')
?? data_get($payload, 'main_balance');
if (! is_numeric($raw)) {
return new MainSiteWalletBalanceProbeResult(
success: false,
mainBalanceMinor: null,
currencyCode: $currencyCode,
requestUrl: $url.'?'.http_build_query($query),
httpStatus: $httpStatus,
message: '响应缺少 main_balance 数值',
responseBody: $preview,
);
}
return new MainSiteWalletBalanceProbeResult(
success: true,
mainBalanceMinor: max(0, (int) $raw),
currencyCode: (string) (data_get($payload, 'data.currency_code') ?? $currencyCode),
requestUrl: $url.'?'.http_build_query($query),
httpStatus: $httpStatus,
message: null,
responseBody: $preview,
);
}
/**
* @param array<string, mixed> $payload
* @return array<string, mixed>
*/
private static function truncateResponsePreview(array $payload): array
{
$json = json_encode($payload, JSON_UNESCAPED_UNICODE);
if (is_string($json) && strlen($json) > 512) {
return ['_truncated' => true, 'preview' => substr($json, 0, 512)];
}
return $payload;
}
}