- 在多个控制器中将权限检查从 hasAdminPermission 更新为 hasPermissionCode,以增强权限管理的灵活性。 - 引入 AdminScopePolicy,优化基于代理节点的权限和数据过滤逻辑,确保管理员能够更精确地控制访问权限。 - 在请求验证中添加 agent_node_id 字段,确保 API 接口支持代理节点的相关操作。 - 更新 AdminUser 模型,新增 hasPermissionCode 方法,以支持更细粒度的权限检查。 - 优化审计日志记录逻辑,确保在处理请求时能够准确记录管理员的操作。
240 lines
7.6 KiB
PHP
240 lines
7.6 KiB
PHP
<?php
|
||
|
||
namespace App\Services\Wallet;
|
||
|
||
use App\Models\Player;
|
||
use Illuminate\Support\Facades\Http;
|
||
use GuzzleHttp\Exception\ConnectException;
|
||
use App\Services\Integration\PartnerSiteConfigResolver;
|
||
use App\Support\Integration\WalletApiUrlSanitizer;
|
||
|
||
/**
|
||
* 通过 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(
|
||
$config->walletDebitPath,
|
||
$player,
|
||
$currencyCode,
|
||
$amountMinor,
|
||
$idempotentKey,
|
||
$config,
|
||
);
|
||
}
|
||
|
||
public function creditMainForLotteryWithdraw(
|
||
Player $player,
|
||
string $currencyCode,
|
||
int $amountMinor,
|
||
string $idempotentKey,
|
||
): MainSiteWalletResult {
|
||
$config = $this->partnerSiteConfigResolver->resolveForPlayer($player);
|
||
|
||
return $this->post(
|
||
$config->walletCreditPath,
|
||
$player,
|
||
$currencyCode,
|
||
$amountMinor,
|
||
$idempotentKey,
|
||
$config,
|
||
);
|
||
}
|
||
|
||
public function refundMainForFailedLotteryDeposit(
|
||
Player $player,
|
||
string $currencyCode,
|
||
int $amountMinor,
|
||
string $idempotentKey,
|
||
): MainSiteWalletResult {
|
||
$config = $this->partnerSiteConfigResolver->resolveForPlayer($player);
|
||
|
||
return $this->post(
|
||
$config->walletCreditPath,
|
||
$player,
|
||
$currencyCode,
|
||
$amountMinor,
|
||
$idempotentKey,
|
||
$config,
|
||
);
|
||
}
|
||
|
||
private function post(
|
||
string $path,
|
||
Player $player,
|
||
string $currencyCode,
|
||
int $amountMinor,
|
||
string $idempotentKey,
|
||
\App\Services\Integration\PartnerSiteConfig $config,
|
||
): MainSiteWalletResult {
|
||
if (! $config->hasWalletApi()) {
|
||
$requestSnapshot = [
|
||
'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],
|
||
];
|
||
|
||
// 生产环境不允许把“主站未配置钱包 API”当作成功,从而避免资金链路失真。
|
||
if (app()->environment(['production'])) {
|
||
return MainSiteWalletResult::failure(
|
||
'wallet_api_not_configured',
|
||
['stub' => false, 'reason' => 'wallet_api_not_configured'],
|
||
false,
|
||
$requestSnapshot,
|
||
);
|
||
}
|
||
|
||
// 本地/测试环境允许 stub 成功,方便联调与自动化用例覆盖资金链路流程。
|
||
return MainSiteWalletResult::success(
|
||
null,
|
||
['stub' => true, 'reason' => 'wallet_api_not_configured'],
|
||
$requestSnapshot,
|
||
);
|
||
}
|
||
|
||
$base = WalletApiUrlSanitizer::normalizeAndValidate($config->walletApiUrl);
|
||
if ($base === null) {
|
||
return MainSiteWalletResult::failure(
|
||
'wallet_api_url_invalid',
|
||
['reason' => 'invalid_base_url'],
|
||
false,
|
||
[
|
||
'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, 'operation' => 'invalid_wallet_api_url'],
|
||
],
|
||
);
|
||
}
|
||
|
||
$requestBody = [
|
||
'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,
|
||
];
|
||
|
||
$requestSnapshot = array_merge($requestBody, [
|
||
'_meta' => [
|
||
'method' => 'POST',
|
||
'path' => '/'.ltrim($path, '/'),
|
||
],
|
||
]);
|
||
|
||
$url = $base.'/'.ltrim($path, '/');
|
||
$timeout = $config->walletTimeoutSeconds;
|
||
$apiKey = $config->walletApiKey;
|
||
|
||
if (app()->environment(['production'])
|
||
&& $config->source === \App\Services\Integration\PartnerSiteConfig::SOURCE_LEGACY_ENV
|
||
&& (! is_string($apiKey) || trim($apiKey) === '')
|
||
) {
|
||
return MainSiteWalletResult::failure(
|
||
'main_site_wallet_api_key_required',
|
||
['reason' => 'missing_main_site_wallet_api_key'],
|
||
false,
|
||
$requestSnapshot,
|
||
);
|
||
}
|
||
|
||
$headers = [];
|
||
if (is_string($apiKey) && $apiKey !== '') {
|
||
$headers['Authorization'] = 'Bearer '.$apiKey;
|
||
}
|
||
|
||
try {
|
||
$response = Http::withHeaders($headers)
|
||
->timeout($timeout)
|
||
->acceptJson()
|
||
->asJson()
|
||
->post($url, $requestBody);
|
||
} catch (\Throwable $e) {
|
||
return MainSiteWalletResult::failure(
|
||
$e->getMessage(),
|
||
null,
|
||
self::isUncertainTransportFailure($e),
|
||
$requestSnapshot,
|
||
);
|
||
}
|
||
|
||
$payload = $response->json();
|
||
if (! is_array($payload)) {
|
||
$payload = ['raw' => $response->body()];
|
||
}
|
||
|
||
$status = $response->status();
|
||
if ($status === 408 || $status === 504) {
|
||
return MainSiteWalletResult::failure(
|
||
'HTTP '.$status,
|
||
$payload,
|
||
true,
|
||
$requestSnapshot,
|
||
);
|
||
}
|
||
|
||
if (! $response->successful()) {
|
||
return MainSiteWalletResult::failure(
|
||
is_string(data_get($payload, 'message'))
|
||
? (string) data_get($payload, 'message')
|
||
: ('HTTP '.$status),
|
||
$payload,
|
||
false,
|
||
$requestSnapshot,
|
||
);
|
||
}
|
||
|
||
$ok = (bool) data_get($payload, 'success', true);
|
||
if (! $ok) {
|
||
return MainSiteWalletResult::failure(
|
||
is_string(data_get($payload, 'message'))
|
||
? (string) data_get($payload, 'message')
|
||
: 'main_site_rejected',
|
||
$payload,
|
||
false,
|
||
$requestSnapshot,
|
||
);
|
||
}
|
||
|
||
$ref = data_get($payload, 'external_ref_no')
|
||
?? data_get($payload, 'data.external_ref_no')
|
||
?? data_get($payload, 'ref');
|
||
|
||
return MainSiteWalletResult::success(is_string($ref) ? $ref : null, $payload, $requestSnapshot);
|
||
}
|
||
|
||
private static function isUncertainTransportFailure(\Throwable $e): bool
|
||
{
|
||
if ($e instanceof ConnectException) {
|
||
return true;
|
||
}
|
||
|
||
$msg = strtolower($e->getMessage());
|
||
|
||
return str_contains($msg, 'curl error 28')
|
||
|| str_contains($msg, 'connection timed out')
|
||
|| str_contains($msg, 'timed out')
|
||
|| str_contains($msg, 'operation timed out');
|
||
}
|
||
}
|