173 lines
5.8 KiB
PHP
173 lines
5.8 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Esign;
|
|
|
|
use JsonException;
|
|
use RuntimeException;
|
|
|
|
/**
|
|
* Reads the E签宝 settings that Laravel loaded from the private runtime/.env.
|
|
*
|
|
* Keeping this normalization and endpoint check in one place prevents a
|
|
* production credential from silently being sent to the sandbox endpoint (or
|
|
* the reverse) when a deployment omits ESIGN_BASE_URL.
|
|
*/
|
|
final class EsignConfiguration
|
|
{
|
|
private readonly string $environment;
|
|
|
|
private readonly string $baseUrl;
|
|
|
|
private readonly string $appId;
|
|
|
|
private readonly string $appSecret;
|
|
|
|
private readonly string $notifyUrl;
|
|
|
|
private readonly int $callbackToleranceSeconds;
|
|
|
|
private readonly int $timeout;
|
|
|
|
private readonly string $initiatorAccounts;
|
|
|
|
private readonly string $defaultInitiator;
|
|
|
|
public function __construct(
|
|
string $environment,
|
|
string $baseUrl,
|
|
string $appId,
|
|
string $appSecret,
|
|
string $notifyUrl,
|
|
int $callbackToleranceSeconds,
|
|
int $timeout,
|
|
string $initiatorAccounts = '{}',
|
|
string $defaultInitiator = '',
|
|
) {
|
|
$this->environment = strtolower(trim($environment));
|
|
$this->baseUrl = rtrim(trim($baseUrl), '/');
|
|
$this->appId = trim($appId);
|
|
$this->appSecret = trim($appSecret);
|
|
$this->notifyUrl = trim($notifyUrl);
|
|
$this->callbackToleranceSeconds = max(60, $callbackToleranceSeconds);
|
|
$this->timeout = max(5, $timeout);
|
|
$this->initiatorAccounts = trim($initiatorAccounts);
|
|
$this->defaultInitiator = trim($defaultInitiator);
|
|
}
|
|
|
|
public static function fromConfig(): self
|
|
{
|
|
return new self(
|
|
environment: strtolower(trim((string) config('services.esign.environment', 'sandbox'))),
|
|
baseUrl: rtrim(trim((string) config('services.esign.base_url')), '/'),
|
|
appId: trim((string) config('services.esign.app_id')),
|
|
appSecret: trim((string) config('services.esign.app_secret')),
|
|
notifyUrl: trim((string) config('services.esign.notify_url')),
|
|
callbackToleranceSeconds: max(60, (int) config('services.esign.callback_tolerance_seconds', 900)),
|
|
timeout: max(5, (int) config('services.esign.timeout', 20)),
|
|
initiatorAccounts: (string) config('services.esign.initiator_accounts', '{}'),
|
|
defaultInitiator: (string) config('services.esign.default_initiator', ''),
|
|
);
|
|
}
|
|
|
|
public function isConfigured(): bool
|
|
{
|
|
return $this->appId !== '' && $this->appSecret !== '';
|
|
}
|
|
|
|
public function assertReadyForApi(): void
|
|
{
|
|
if (! $this->isConfigured()) {
|
|
$label = $this->environment === 'production' ? '正式' : '沙箱';
|
|
|
|
throw new RuntimeException("E 签宝{$label}尚未配置 App ID 或 AppSecret。");
|
|
}
|
|
|
|
if (! in_array($this->environment, ['sandbox', 'production'], true)) {
|
|
throw new RuntimeException('E 签宝环境只能是 sandbox 或 production。');
|
|
}
|
|
|
|
if ($this->baseUrl === '' || filter_var($this->baseUrl, FILTER_VALIDATE_URL) === false) {
|
|
throw new RuntimeException('E 签宝 API 地址无效,请检查 ESIGN_BASE_URL。');
|
|
}
|
|
|
|
$scheme = strtolower((string) parse_url($this->baseUrl, PHP_URL_SCHEME));
|
|
$host = strtolower((string) parse_url($this->baseUrl, PHP_URL_HOST));
|
|
|
|
if ($scheme !== 'https') {
|
|
throw new RuntimeException('E 签宝 API 地址必须使用 HTTPS。');
|
|
}
|
|
|
|
if ($this->environment === 'production' && $host === 'smlopenapi.esign.cn') {
|
|
throw new RuntimeException('E 签宝正式环境不能使用沙箱 API 地址。');
|
|
}
|
|
|
|
if ($this->environment === 'sandbox' && $host === 'openapi.esign.cn') {
|
|
throw new RuntimeException('E 签宝沙箱环境不能使用正式 API 地址。');
|
|
}
|
|
}
|
|
|
|
public function environment(): string
|
|
{
|
|
return $this->environment;
|
|
}
|
|
|
|
/** Resolve the selected legal entity without changing credentials used by historical flows. */
|
|
public function signFlowInitiator(?string $organizationName = null): ?array
|
|
{
|
|
try {
|
|
$accounts = json_decode($this->initiatorAccounts ?: '{}', true, 32, JSON_THROW_ON_ERROR);
|
|
} catch (JsonException) {
|
|
throw new RuntimeException('E 签宝发起身份配置格式无效,请联系管理员。');
|
|
}
|
|
|
|
if (! is_array($accounts)) {
|
|
throw new RuntimeException('E 签宝发起身份配置格式无效,请联系管理员。');
|
|
}
|
|
if ($accounts === [] && $this->defaultInitiator === '') {
|
|
return null;
|
|
}
|
|
|
|
$name = trim((string) $organizationName) ?: $this->defaultInitiator;
|
|
$account = $accounts[$name] ?? null;
|
|
if (! is_array($account) || ! is_string($account['org_id'] ?? null) || ! is_string($account['psn_id'] ?? null)
|
|
|| trim($account['org_id']) === '' || trim($account['psn_id']) === '') {
|
|
throw new RuntimeException('所选签署主体未配置完整的 E 签宝发起企业和经办人身份,请联系管理员。');
|
|
}
|
|
|
|
return ['orgInitiator' => [
|
|
'orgId' => trim($account['org_id']),
|
|
'transactor' => ['psnId' => trim($account['psn_id'])],
|
|
]];
|
|
}
|
|
|
|
public function baseUrl(): string
|
|
{
|
|
return $this->baseUrl;
|
|
}
|
|
|
|
public function appId(): string
|
|
{
|
|
return $this->appId;
|
|
}
|
|
|
|
public function appSecret(): string
|
|
{
|
|
return $this->appSecret;
|
|
}
|
|
|
|
public function notifyUrl(): string
|
|
{
|
|
return $this->notifyUrl;
|
|
}
|
|
|
|
public function callbackToleranceSeconds(): int
|
|
{
|
|
return $this->callbackToleranceSeconds;
|
|
}
|
|
|
|
public function timeout(): int
|
|
{
|
|
return $this->timeout;
|
|
}
|
|
}
|