feat: 增强玩家管理功能,集成接入站点权限控制
在多个玩家相关控制器中引入 AdminSiteScope,确保管理员在执行操作前具备相应的接入站点权限。更新 Player 相关请求以支持 site_code 参数,增强权限验证逻辑,确保系统安全性与灵活性。同时,新增 AdminUser 模型方法以获取可访问的站点 ID 列表,优化权限管理。
This commit is contained in:
117
app/Services/Integration/IntegrationSiteService.php
Normal file
117
app/Services/Integration/IntegrationSiteService.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Integration;
|
||||
|
||||
use App\Models\AdminSite;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
final class IntegrationSiteService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PartnerSiteConfigResolver $configResolver,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
* @return array{site: AdminSite, secrets: array{sso_jwt_secret: string, wallet_api_key: string}}
|
||||
*/
|
||||
public function create(array $data): array
|
||||
{
|
||||
$secrets = $this->generateSecrets();
|
||||
|
||||
$site = DB::transaction(function () use ($data, $secrets): AdminSite {
|
||||
return AdminSite::query()->create([
|
||||
'code' => (string) $data['code'],
|
||||
'name' => (string) $data['name'],
|
||||
'currency_code' => (string) ($data['currency_code'] ?? 'NPR'),
|
||||
'status' => (int) ($data['status'] ?? 1),
|
||||
'is_default' => false,
|
||||
'wallet_api_url' => $this->nullableTrim($data['wallet_api_url'] ?? null),
|
||||
'wallet_debit_path' => (string) ($data['wallet_debit_path'] ?? '/wallet/debit-for-lottery'),
|
||||
'wallet_credit_path' => (string) ($data['wallet_credit_path'] ?? '/wallet/credit-from-lottery'),
|
||||
'wallet_balance_path' => (string) ($data['wallet_balance_path'] ?? '/wallet/balance'),
|
||||
'wallet_timeout_seconds' => max(1, (int) ($data['wallet_timeout_seconds'] ?? 10)),
|
||||
'iframe_allowed_origins' => $data['iframe_allowed_origins'] ?? null,
|
||||
'lottery_h5_base_url' => $this->nullableTrim($data['lottery_h5_base_url'] ?? null),
|
||||
'notes' => $this->nullableTrim($data['notes'] ?? null),
|
||||
'sso_jwt_secret_encrypted' => encrypt($secrets['sso_jwt_secret']),
|
||||
'wallet_api_key_encrypted' => encrypt($secrets['wallet_api_key']),
|
||||
]);
|
||||
});
|
||||
|
||||
$this->configResolver->forgetCache((string) $site->code);
|
||||
|
||||
return ['site' => $site->fresh(), 'secrets' => $secrets];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public function update(AdminSite $site, array $data): AdminSite
|
||||
{
|
||||
$site->fill([
|
||||
'name' => (string) $data['name'],
|
||||
'currency_code' => (string) ($data['currency_code'] ?? $site->currency_code),
|
||||
'status' => (int) ($data['status'] ?? $site->status),
|
||||
'wallet_api_url' => array_key_exists('wallet_api_url', $data)
|
||||
? $this->nullableTrim($data['wallet_api_url'])
|
||||
: $site->wallet_api_url,
|
||||
'wallet_debit_path' => (string) ($data['wallet_debit_path'] ?? $site->wallet_debit_path),
|
||||
'wallet_credit_path' => (string) ($data['wallet_credit_path'] ?? $site->wallet_credit_path),
|
||||
'wallet_balance_path' => (string) ($data['wallet_balance_path'] ?? $site->wallet_balance_path),
|
||||
'wallet_timeout_seconds' => max(1, (int) ($data['wallet_timeout_seconds'] ?? $site->wallet_timeout_seconds)),
|
||||
'iframe_allowed_origins' => $data['iframe_allowed_origins'] ?? $site->iframe_allowed_origins,
|
||||
'lottery_h5_base_url' => array_key_exists('lottery_h5_base_url', $data)
|
||||
? $this->nullableTrim($data['lottery_h5_base_url'])
|
||||
: $site->lottery_h5_base_url,
|
||||
'notes' => array_key_exists('notes', $data)
|
||||
? $this->nullableTrim($data['notes'])
|
||||
: $site->notes,
|
||||
]);
|
||||
$site->save();
|
||||
|
||||
$this->configResolver->forgetCache((string) $site->code);
|
||||
|
||||
return $site->fresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{site: AdminSite, secrets: array{sso_jwt_secret: string, wallet_api_key: string}}
|
||||
*/
|
||||
public function rotateSecrets(AdminSite $site): array
|
||||
{
|
||||
$secrets = $this->generateSecrets();
|
||||
|
||||
$site->forceFill([
|
||||
'sso_jwt_secret_encrypted' => encrypt($secrets['sso_jwt_secret']),
|
||||
'wallet_api_key_encrypted' => encrypt($secrets['wallet_api_key']),
|
||||
])->save();
|
||||
|
||||
$this->configResolver->forgetCache((string) $site->code);
|
||||
|
||||
return ['site' => $site->fresh(), 'secrets' => $secrets];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{sso_jwt_secret: string, wallet_api_key: string}
|
||||
*/
|
||||
private function generateSecrets(): array
|
||||
{
|
||||
return [
|
||||
'sso_jwt_secret' => Str::random(48),
|
||||
'wallet_api_key' => Str::random(40),
|
||||
];
|
||||
}
|
||||
|
||||
private function nullableTrim(mixed $value): ?string
|
||||
{
|
||||
if (! is_string($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$trimmed = trim($value);
|
||||
|
||||
return $trimmed === '' ? null : $trimmed;
|
||||
}
|
||||
}
|
||||
82
app/Services/Integration/PartnerSiteConfig.php
Normal file
82
app/Services/Integration/PartnerSiteConfig.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Integration;
|
||||
|
||||
/**
|
||||
* 运行时主站接入配置(由库表或 legacy env 解析而来)。
|
||||
*/
|
||||
final readonly class PartnerSiteConfig
|
||||
{
|
||||
public const SOURCE_DATABASE = 'database';
|
||||
|
||||
public const SOURCE_LEGACY_ENV = 'legacy_env';
|
||||
|
||||
public function __construct(
|
||||
public string $siteCode,
|
||||
public bool $enabled,
|
||||
public ?string $walletApiUrl,
|
||||
public string $walletDebitPath,
|
||||
public string $walletCreditPath,
|
||||
public string $walletBalancePath,
|
||||
public ?string $ssoJwtSecret,
|
||||
public ?string $walletApiKey,
|
||||
public int $walletTimeoutSeconds,
|
||||
public string $source,
|
||||
) {}
|
||||
|
||||
public function hasWalletApi(): bool
|
||||
{
|
||||
return is_string($this->walletApiUrl) && trim($this->walletApiUrl) !== '';
|
||||
}
|
||||
|
||||
public function hasSsoSecret(): bool
|
||||
{
|
||||
return is_string($this->ssoJwtSecret) && $this->ssoJwtSecret !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 可安全写入 Cache 的数组形态(避免 readonly 对象序列化产生 __PHP_Incomplete_Class)。
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toCacheArray(): array
|
||||
{
|
||||
return [
|
||||
'site_code' => $this->siteCode,
|
||||
'enabled' => $this->enabled,
|
||||
'wallet_api_url' => $this->walletApiUrl,
|
||||
'wallet_debit_path' => $this->walletDebitPath,
|
||||
'wallet_credit_path' => $this->walletCreditPath,
|
||||
'wallet_balance_path' => $this->walletBalancePath,
|
||||
'sso_jwt_secret' => $this->ssoJwtSecret,
|
||||
'wallet_api_key' => $this->walletApiKey,
|
||||
'wallet_timeout_seconds' => $this->walletTimeoutSeconds,
|
||||
'source' => $this->source,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public static function fromCacheArray(array $data): self
|
||||
{
|
||||
return new self(
|
||||
siteCode: (string) ($data['site_code'] ?? ''),
|
||||
enabled: (bool) ($data['enabled'] ?? false),
|
||||
walletApiUrl: isset($data['wallet_api_url']) && is_string($data['wallet_api_url']) && $data['wallet_api_url'] !== ''
|
||||
? $data['wallet_api_url']
|
||||
: null,
|
||||
walletDebitPath: (string) ($data['wallet_debit_path'] ?? '/wallet/debit-for-lottery'),
|
||||
walletCreditPath: (string) ($data['wallet_credit_path'] ?? '/wallet/credit-from-lottery'),
|
||||
walletBalancePath: (string) ($data['wallet_balance_path'] ?? '/wallet/balance'),
|
||||
ssoJwtSecret: isset($data['sso_jwt_secret']) && is_string($data['sso_jwt_secret']) && $data['sso_jwt_secret'] !== ''
|
||||
? $data['sso_jwt_secret']
|
||||
: null,
|
||||
walletApiKey: isset($data['wallet_api_key']) && is_string($data['wallet_api_key']) && $data['wallet_api_key'] !== ''
|
||||
? $data['wallet_api_key']
|
||||
: null,
|
||||
walletTimeoutSeconds: max(1, (int) ($data['wallet_timeout_seconds'] ?? 10)),
|
||||
source: (string) ($data['source'] ?? self::SOURCE_DATABASE),
|
||||
);
|
||||
}
|
||||
}
|
||||
164
app/Services/Integration/PartnerSiteConfigResolver.php
Normal file
164
app/Services/Integration/PartnerSiteConfigResolver.php
Normal file
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Integration;
|
||||
|
||||
use App\Models\AdminSite;
|
||||
use App\Models\Player;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* 按 {@see site_code} 解析主站对接配置;未命中库表时回退全局 MAIN_SITE_* env(并写 warning 日志)。
|
||||
*/
|
||||
final class PartnerSiteConfigResolver
|
||||
{
|
||||
private const CACHE_PREFIX = 'partner_site_config:';
|
||||
|
||||
private const CACHE_TTL_SECONDS = 60;
|
||||
|
||||
public function resolveForPlayer(Player $player): PartnerSiteConfig
|
||||
{
|
||||
return $this->resolveBySiteCode((string) $player->site_code);
|
||||
}
|
||||
|
||||
public function resolveBySiteCode(string $siteCode): PartnerSiteConfig
|
||||
{
|
||||
$siteCode = trim($siteCode);
|
||||
if ($siteCode === '') {
|
||||
return $this->legacyFallbackConfig($siteCode);
|
||||
}
|
||||
|
||||
$cacheKey = self::CACHE_PREFIX.$siteCode;
|
||||
|
||||
/** @var array<string, mixed> $cached */
|
||||
$cached = Cache::remember($cacheKey, self::CACHE_TTL_SECONDS, function () use ($siteCode): array {
|
||||
$site = AdminSite::query()->where('code', $siteCode)->first();
|
||||
$config = $site !== null
|
||||
? $this->fromAdminSite($site)
|
||||
: $this->legacyFallbackConfig($siteCode);
|
||||
|
||||
return $config->toCacheArray();
|
||||
});
|
||||
|
||||
return PartnerSiteConfig::fromCacheArray($cached);
|
||||
}
|
||||
|
||||
public function forgetCache(string $siteCode): void
|
||||
{
|
||||
Cache::forget(self::CACHE_PREFIX.trim($siteCode));
|
||||
}
|
||||
|
||||
/**
|
||||
* 从未验签的 JWT 中读取 site_code(仅用于选取验签密钥)。
|
||||
*/
|
||||
public function peekSiteCodeFromJwt(string $jwt): ?string
|
||||
{
|
||||
$jwt = trim($jwt);
|
||||
$parts = explode('.', $jwt);
|
||||
if (count($parts) !== 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$payloadJson = $this->base64UrlDecode($parts[1]);
|
||||
if ($payloadJson === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$payload = json_decode($payloadJson, true);
|
||||
if (! is_array($payload)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$siteKey = (string) config('lottery.player_auth.jwt.claim_site_code', 'site_code');
|
||||
$siteCode = $payload[$siteKey] ?? null;
|
||||
|
||||
return is_string($siteCode) && $siteCode !== '' ? $siteCode : null;
|
||||
}
|
||||
|
||||
private function fromAdminSite(AdminSite $site): PartnerSiteConfig
|
||||
{
|
||||
return new PartnerSiteConfig(
|
||||
siteCode: (string) $site->code,
|
||||
enabled: $site->isEnabled(),
|
||||
walletApiUrl: is_string($site->wallet_api_url) && $site->wallet_api_url !== ''
|
||||
? rtrim($site->wallet_api_url, '/')
|
||||
: null,
|
||||
walletDebitPath: (string) ($site->wallet_debit_path ?: '/wallet/debit-for-lottery'),
|
||||
walletCreditPath: (string) ($site->wallet_credit_path ?: '/wallet/credit-from-lottery'),
|
||||
walletBalancePath: (string) ($site->wallet_balance_path ?: '/wallet/balance'),
|
||||
ssoJwtSecret: $site->decryptedSsoJwtSecret(),
|
||||
walletApiKey: $site->decryptedWalletApiKey(),
|
||||
walletTimeoutSeconds: max(1, (int) ($site->wallet_timeout_seconds ?? 10)),
|
||||
source: PartnerSiteConfig::SOURCE_DATABASE,
|
||||
);
|
||||
}
|
||||
|
||||
private function legacyFallbackConfig(string $siteCode): PartnerSiteConfig
|
||||
{
|
||||
$defaultCode = (string) config('lottery.integration.default_site_code', 'default_site');
|
||||
$legacyCodes = array_filter([
|
||||
$defaultCode,
|
||||
(string) config('lottery.integration.legacy_env_site_code', ''),
|
||||
]);
|
||||
|
||||
$sso = config('lottery.main_site.sso_jwt_secret');
|
||||
$walletUrl = config('lottery.main_site.wallet_api_url');
|
||||
$walletKey = config('lottery.main_site.wallet_api_key');
|
||||
|
||||
$hasLegacy = (is_string($sso) && $sso !== '')
|
||||
|| (is_string($walletUrl) && trim((string) $walletUrl) !== '');
|
||||
|
||||
if ($hasLegacy && (
|
||||
$siteCode === ''
|
||||
|| in_array($siteCode, $legacyCodes, true)
|
||||
|| app()->environment(['local', 'testing'])
|
||||
)) {
|
||||
if ($siteCode !== '') {
|
||||
Log::warning('partner_site_config.legacy_env_fallback', [
|
||||
'site_code' => $siteCode,
|
||||
'hint' => 'Configure admin_sites row for this site_code',
|
||||
]);
|
||||
}
|
||||
|
||||
return new PartnerSiteConfig(
|
||||
siteCode: $siteCode !== '' ? $siteCode : $defaultCode,
|
||||
enabled: true,
|
||||
walletApiUrl: is_string($walletUrl) && trim($walletUrl) !== ''
|
||||
? rtrim(trim($walletUrl), '/')
|
||||
: null,
|
||||
walletDebitPath: (string) config('lottery.main_site.wallet_debit_path', '/wallet/debit-for-lottery'),
|
||||
walletCreditPath: (string) config('lottery.main_site.wallet_credit_path', '/wallet/credit-from-lottery'),
|
||||
walletBalancePath: (string) config('lottery.main_site.wallet_balance_path', '/wallet/balance'),
|
||||
ssoJwtSecret: is_string($sso) && $sso !== '' ? $sso : null,
|
||||
walletApiKey: is_string($walletKey) && $walletKey !== '' ? $walletKey : null,
|
||||
walletTimeoutSeconds: max(1, (int) config('lottery.main_site.wallet_timeout', 10)),
|
||||
source: PartnerSiteConfig::SOURCE_LEGACY_ENV,
|
||||
);
|
||||
}
|
||||
|
||||
return new PartnerSiteConfig(
|
||||
siteCode: $siteCode,
|
||||
enabled: false,
|
||||
walletApiUrl: null,
|
||||
walletDebitPath: '/wallet/debit-for-lottery',
|
||||
walletCreditPath: '/wallet/credit-from-lottery',
|
||||
walletBalancePath: '/wallet/balance',
|
||||
ssoJwtSecret: null,
|
||||
walletApiKey: null,
|
||||
walletTimeoutSeconds: 10,
|
||||
source: PartnerSiteConfig::SOURCE_DATABASE,
|
||||
);
|
||||
}
|
||||
|
||||
private function base64UrlDecode(string $segment): ?string
|
||||
{
|
||||
$remainder = strlen($segment) % 4;
|
||||
if ($remainder > 0) {
|
||||
$segment .= str_repeat('=', 4 - $remainder);
|
||||
}
|
||||
|
||||
$decoded = base64_decode(strtr($segment, '-_', '+/'), true);
|
||||
|
||||
return $decoded === false ? null : $decoded;
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ use Illuminate\Http\Request;
|
||||
use App\Support\PlayerTokenAesUnwrap;
|
||||
use Illuminate\Database\QueryException;
|
||||
use App\Exceptions\PlayerAuthenticationException;
|
||||
use App\Services\Integration\PartnerSiteConfigResolver;
|
||||
|
||||
/**
|
||||
* 从请求头解析玩家身份,返回已落库的 {@see Player}。
|
||||
@@ -36,6 +37,10 @@ final class PlayerTokenResolver
|
||||
/** players.status:与迁移注释一致 */
|
||||
private const PLAYER_STATUS_ACTIVE = 0;
|
||||
|
||||
public function __construct(
|
||||
private readonly PartnerSiteConfigResolver $partnerSiteConfigResolver,
|
||||
) {}
|
||||
|
||||
public function resolve(Request $request): Player
|
||||
{
|
||||
$header = $request->header('Authorization', '');
|
||||
@@ -56,17 +61,27 @@ final class PlayerTokenResolver
|
||||
if ($this->devBypassAllowed() && str_starts_with($token, 'dev:')) {
|
||||
$player = $this->resolveDevToken($token);
|
||||
} else {
|
||||
// 与 .env 中 MAIN_SITE_SSO_JWT_SECRET 一致,用于 firebase/php-jwt 验签
|
||||
$secret = config('lottery.main_site.sso_jwt_secret');
|
||||
$jwtPlain = $this->unwrapOpaqueToJwtString($token);
|
||||
$siteCode = $this->partnerSiteConfigResolver->peekSiteCodeFromJwt($jwtPlain);
|
||||
if ($siteCode === null) {
|
||||
throw new PlayerAuthenticationException('JWT 缺少站点标识', ErrorCode::PlayerTokenInvalid->value);
|
||||
}
|
||||
|
||||
$siteConfig = $this->partnerSiteConfigResolver->resolveBySiteCode($siteCode);
|
||||
if (! $siteConfig->enabled) {
|
||||
throw new PlayerAuthenticationException('站点已停用', ErrorCode::PlayerAccountSuspended->value, 403);
|
||||
}
|
||||
|
||||
$secret = $siteConfig->ssoJwtSecret;
|
||||
if (! is_string($secret) || $secret === '') {
|
||||
throw new PlayerAuthenticationException(
|
||||
'SSO 未配置(MAIN_SITE_SSO_JWT_SECRET)',
|
||||
'SSO 未配置(站点 '.$siteCode.')',
|
||||
ErrorCode::PlayerSsoSecretNotConfigured->value,
|
||||
503,
|
||||
);
|
||||
}
|
||||
|
||||
$player = $this->resolveJwtOrAesWrappedJwt($token, $secret);
|
||||
$player = $this->resolveJwt($jwtPlain, $secret);
|
||||
}
|
||||
|
||||
$this->assertPlayerActive($player);
|
||||
|
||||
@@ -4,58 +4,156 @@ namespace App\Services\Wallet;
|
||||
|
||||
use App\Models\Player;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use App\Services\Integration\PartnerSiteConfigResolver;
|
||||
|
||||
/**
|
||||
* 查询主站钱包余额(供玩家端余额接口填充 main_balance)。
|
||||
*/
|
||||
final class HttpMainSiteWalletBalanceClient
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PartnerSiteConfigResolver $partnerSiteConfigResolver,
|
||||
) {}
|
||||
|
||||
public function fetch(Player $player, string $currencyCode): ?int
|
||||
{
|
||||
$base = rtrim((string) config('lottery.main_site.wallet_api_url'), '/');
|
||||
if ($base === '') {
|
||||
return null;
|
||||
$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,
|
||||
);
|
||||
}
|
||||
|
||||
$path = (string) config('lottery.main_site.wallet_balance_path', '/wallet/balance');
|
||||
if (! $config->hasWalletApi()) {
|
||||
return new MainSiteWalletBalanceProbeResult(
|
||||
success: false,
|
||||
mainBalanceMinor: null,
|
||||
currencyCode: $currencyCode,
|
||||
requestUrl: '',
|
||||
httpStatus: null,
|
||||
message: '未配置主站钱包 API URL',
|
||||
responseBody: null,
|
||||
);
|
||||
}
|
||||
|
||||
$base = rtrim((string) $config->walletApiUrl, '/');
|
||||
$path = $config->walletBalancePath;
|
||||
$url = $base.'/'.ltrim($path, '/');
|
||||
$timeout = (int) config('lottery.main_site.wallet_timeout', 10);
|
||||
$apiKey = config('lottery.main_site.wallet_api_key');
|
||||
$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, [
|
||||
'site_code' => $player->site_code,
|
||||
'site_player_id' => $player->site_player_id,
|
||||
'currency_code' => $currencyCode,
|
||||
]);
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
->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()) {
|
||||
return null;
|
||||
$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,
|
||||
);
|
||||
}
|
||||
|
||||
$payload = $response->json();
|
||||
if (! is_array($payload)) {
|
||||
return null;
|
||||
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 null;
|
||||
return new MainSiteWalletBalanceProbeResult(
|
||||
success: false,
|
||||
mainBalanceMinor: null,
|
||||
currencyCode: $currencyCode,
|
||||
requestUrl: $url.'?'.http_build_query($query),
|
||||
httpStatus: $httpStatus,
|
||||
message: '响应缺少 main_balance 数值',
|
||||
responseBody: $preview,
|
||||
);
|
||||
}
|
||||
|
||||
return max(0, (int) $raw);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,24 +5,32 @@ namespace App\Services\Wallet;
|
||||
use App\Models\Player;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use GuzzleHttp\Exception\ConnectException;
|
||||
use App\Services\Integration\PartnerSiteConfigResolver;
|
||||
|
||||
/**
|
||||
* 通过 HTTP 调用主站钱包 API(路径见 config lottery.main_site.wallet_*_path)。
|
||||
*/
|
||||
final class HttpMainSiteWalletGateway implements MainSiteWalletGateway
|
||||
{
|
||||
public function __construct(
|
||||
private readonly PartnerSiteConfigResolver $partnerSiteConfigResolver,
|
||||
) {}
|
||||
|
||||
public function debitMainForLotteryDeposit(
|
||||
Player $player,
|
||||
string $currencyCode,
|
||||
int $amountMinor,
|
||||
string $idempotentKey,
|
||||
): MainSiteWalletResult {
|
||||
$config = $this->partnerSiteConfigResolver->resolveForPlayer($player);
|
||||
|
||||
return $this->post(
|
||||
(string) config('lottery.main_site.wallet_debit_path'),
|
||||
$config->walletDebitPath,
|
||||
$player,
|
||||
$currencyCode,
|
||||
$amountMinor,
|
||||
$idempotentKey,
|
||||
$config,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,12 +40,15 @@ final class HttpMainSiteWalletGateway implements MainSiteWalletGateway
|
||||
int $amountMinor,
|
||||
string $idempotentKey,
|
||||
): MainSiteWalletResult {
|
||||
$config = $this->partnerSiteConfigResolver->resolveForPlayer($player);
|
||||
|
||||
return $this->post(
|
||||
(string) config('lottery.main_site.wallet_credit_path'),
|
||||
$config->walletCreditPath,
|
||||
$player,
|
||||
$currencyCode,
|
||||
$amountMinor,
|
||||
$idempotentKey,
|
||||
$config,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -47,11 +58,24 @@ final class HttpMainSiteWalletGateway implements MainSiteWalletGateway
|
||||
string $currencyCode,
|
||||
int $amountMinor,
|
||||
string $idempotentKey,
|
||||
\App\Services\Integration\PartnerSiteConfig $config,
|
||||
): MainSiteWalletResult {
|
||||
$base = rtrim((string) config('lottery.main_site.wallet_api_url'), '/');
|
||||
if (! $config->hasWalletApi()) {
|
||||
return MainSiteWalletResult::success(null, ['stub' => true, 'reason' => 'wallet_api_not_configured'], [
|
||||
'site_code' => $player->site_code,
|
||||
'site_player_id' => $player->site_player_id,
|
||||
'player_id' => $player->id,
|
||||
'currency_code' => $currencyCode,
|
||||
'amount_minor' => $amountMinor,
|
||||
'idempotent_key' => $idempotentKey,
|
||||
'_meta' => ['stub' => true],
|
||||
]);
|
||||
}
|
||||
|
||||
$base = rtrim((string) $config->walletApiUrl, '/');
|
||||
$url = $base.'/'.ltrim($path, '/');
|
||||
$timeout = (int) config('lottery.main_site.wallet_timeout', 10);
|
||||
$apiKey = config('lottery.main_site.wallet_api_key');
|
||||
$timeout = $config->walletTimeoutSeconds;
|
||||
$apiKey = $config->walletApiKey;
|
||||
|
||||
$requestBody = [
|
||||
'site_code' => $player->site_code,
|
||||
|
||||
35
app/Services/Wallet/MainSiteWalletBalanceProbeResult.php
Normal file
35
app/Services/Wallet/MainSiteWalletBalanceProbeResult.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Wallet;
|
||||
|
||||
/**
|
||||
* 主站 balance 探测结果(后台联调检测用,含诊断信息)。
|
||||
*/
|
||||
final readonly class MainSiteWalletBalanceProbeResult
|
||||
{
|
||||
public function __construct(
|
||||
public bool $success,
|
||||
public ?int $mainBalanceMinor,
|
||||
public string $currencyCode,
|
||||
public string $requestUrl,
|
||||
public ?int $httpStatus,
|
||||
public ?string $message,
|
||||
public ?array $responseBody,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'success' => $this->success,
|
||||
'main_balance_minor' => $this->mainBalanceMinor,
|
||||
'currency_code' => $this->currencyCode,
|
||||
'request_url' => $this->requestUrl,
|
||||
'http_status' => $this->httpStatus,
|
||||
'message' => $this->message,
|
||||
'response_preview' => $this->responseBody,
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user