813 lines
28 KiB
PHP
813 lines
28 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Extend\RegulatoryPlatform;
|
|
|
|
use App\Constants\HttpEnumCode;
|
|
use App\Exception\BusinessException;
|
|
use App\Utils\Log;
|
|
use GuzzleHttp\Client;
|
|
use GuzzleHttp\Exception\GuzzleException;
|
|
use Hyperf\Context\ApplicationContext;
|
|
use Hyperf\Di\Annotation\Inject;
|
|
use Hyperf\Redis\Redis;
|
|
use Psr\Container\ContainerExceptionInterface;
|
|
use Psr\Container\ContainerInterface;
|
|
use Psr\Container\NotFoundExceptionInterface;
|
|
|
|
class regulatoryPlatform
|
|
{
|
|
protected const ACCESS_TOKEN_CACHE_KEY = 'regulatory_platform_access_token';
|
|
|
|
protected const REFRESH_TOKEN_CACHE_KEY = 'regulatory_platform_refresh_token';
|
|
|
|
protected const EXPIRES_AT_CACHE_KEY = 'regulatory_platform_access_token_expires_at';
|
|
|
|
protected const DEFAULT_ACCESS_TOKEN_TTL = 7200;
|
|
|
|
protected const DEFAULT_REFRESH_TOKEN_TTL = 2592000;
|
|
|
|
protected const ACCESS_TOKEN_BUFFER = 60;
|
|
|
|
protected const HTTP_TIMEOUT = 30;
|
|
|
|
protected const HTTP_CONNECT_TIMEOUT = 10;
|
|
|
|
#[Inject]
|
|
protected ContainerInterface $container;
|
|
|
|
#[Inject]
|
|
protected Client $client;
|
|
|
|
#[Inject]
|
|
protected Redis $redis;
|
|
|
|
protected array $config;
|
|
|
|
protected ?string $api_url;
|
|
|
|
protected ?string $client_id;
|
|
|
|
protected ?string $client_secret;
|
|
|
|
protected RegulatoryPlatformProtocol $protocol;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->config = (array) \Hyperf\Config\config('regulatory_platform', []);
|
|
$this->container = ApplicationContext::getContainer();
|
|
$this->client = $this->container->get(Client::class);
|
|
$this->redis = $this->container->get(Redis::class);
|
|
$this->protocol = RegulatoryPlatformProtocol::fromConfig($this->config);
|
|
$this->client_id = $this->protocol->getClientId();
|
|
$this->client_secret = $this->protocol->getClientSecret();
|
|
$this->api_url = $this->resolveConfigValue([
|
|
'api_url',
|
|
'v2.api_url',
|
|
'v2.data_upload_base_url',
|
|
'data_upload_base_url',
|
|
'data_api_url',
|
|
'base_url',
|
|
]);
|
|
}
|
|
|
|
public function protocol(): RegulatoryPlatformProtocol
|
|
{
|
|
return $this->protocol;
|
|
}
|
|
|
|
public function buildV2RequestEnvelope(
|
|
array $payload,
|
|
bool $includeClientId = false,
|
|
?string $clientId = null,
|
|
?string $clientSecret = null
|
|
): array {
|
|
return $this->protocol->buildRequestEnvelope($payload, $includeClientId, $clientId, $clientSecret);
|
|
}
|
|
|
|
public function decodeV2ResponseEnvelope(
|
|
array $response,
|
|
bool $verifySignature = false,
|
|
?string $clientId = null,
|
|
?string $clientSecret = null
|
|
): array {
|
|
return $this->protocol->decodeResponseEnvelope($response, $verifySignature, $clientId, $clientSecret);
|
|
}
|
|
|
|
public function getAccessToken(bool $forceRefresh = false): string
|
|
{
|
|
if (! $forceRefresh) {
|
|
$cachedAccessToken = $this->getCachedAccessToken();
|
|
if (! empty($cachedAccessToken)) {
|
|
return $cachedAccessToken;
|
|
}
|
|
}
|
|
|
|
$refreshToken = $this->getCachedRefreshToken();
|
|
if (! empty($refreshToken)) {
|
|
try {
|
|
$tokenPayload = $this->refreshAccessToken($refreshToken);
|
|
|
|
return $tokenPayload['accessToken'];
|
|
} catch (\Throwable $throwable) {
|
|
Log::getInstance('regulatoryPlatform-token', 'regulatory_platform_token')->warning($throwable->getMessage());
|
|
$this->clearCachedRefreshToken();
|
|
}
|
|
}
|
|
|
|
$tokenPayload = $this->requestAccessToken();
|
|
|
|
return $tokenPayload['accessToken'];
|
|
}
|
|
|
|
public function refreshAccessToken(?string $refreshToken = null): array
|
|
{
|
|
$refreshToken ??= $this->getCachedRefreshToken();
|
|
if (empty($refreshToken)) {
|
|
throw new BusinessException('Regulatory platform refresh token cache is empty');
|
|
}
|
|
|
|
return $this->requestToken(
|
|
$this->buildAuthEndpoint('refresh'),
|
|
[
|
|
'grantType' => 'refresh_token',
|
|
'refreshToken' => $refreshToken,
|
|
]
|
|
);
|
|
}
|
|
|
|
public function clearTokenCache(): void
|
|
{
|
|
$this->redis->del(self::ACCESS_TOKEN_CACHE_KEY);
|
|
$this->redis->del(self::REFRESH_TOKEN_CACHE_KEY);
|
|
$this->redis->del(self::EXPIRES_AT_CACHE_KEY);
|
|
}
|
|
|
|
public function requestV2Decoded(
|
|
string $url,
|
|
array $payload,
|
|
bool $includeClientId = false,
|
|
bool $verifySignature = false
|
|
): array {
|
|
try {
|
|
$envelope = $this->buildV2RequestEnvelope($payload, $includeClientId);
|
|
$this->logBusinessRequest('requestV2Decoded', $url, [
|
|
'payload' => $payload,
|
|
'includeClientId' => $includeClientId,
|
|
'verifySignature' => $verifySignature,
|
|
'requestEnvelope' => $envelope,
|
|
]);
|
|
$response = $this->httpRequestV2($url, ['json' => $envelope]);
|
|
$this->assertV2SuccessResponse($response);
|
|
|
|
$decoded = $this->decodeV2ResponseEnvelope($response, $verifySignature);
|
|
$this->logBusinessResponse('requestV2Decoded', $url, [
|
|
'verifySignature' => $verifySignature,
|
|
'response' => $response,
|
|
'decodedResponse' => $decoded,
|
|
]);
|
|
|
|
return $decoded;
|
|
} catch (GuzzleException $e) {
|
|
$this->logBusinessException('requestV2Decoded', $url, $e, [
|
|
'payload' => $payload,
|
|
'includeClientId' => $includeClientId,
|
|
'verifySignature' => $verifySignature,
|
|
]);
|
|
throw new BusinessException($e->getMessage());
|
|
} catch (\Throwable $throwable) {
|
|
$this->logBusinessException('requestV2Decoded', $url, $throwable, [
|
|
'payload' => $payload,
|
|
'includeClientId' => $includeClientId,
|
|
'verifySignature' => $verifySignature,
|
|
]);
|
|
throw $throwable;
|
|
}
|
|
}
|
|
|
|
public function requestV2Raw(string $url, array $payload, bool $includeClientId = false): array
|
|
{
|
|
try {
|
|
$envelope = $this->buildV2RequestEnvelope($payload, $includeClientId);
|
|
$this->logBusinessRequest('requestV2Raw', $url, [
|
|
'payload' => $payload,
|
|
'includeClientId' => $includeClientId,
|
|
'requestEnvelope' => $envelope,
|
|
]);
|
|
$response = $this->httpRequestV2($url, ['json' => $envelope]);
|
|
$this->assertV2SuccessResponse($response);
|
|
$this->logBusinessResponse('requestV2Raw', $url, [
|
|
'response' => $response,
|
|
]);
|
|
|
|
return $response;
|
|
} catch (GuzzleException $e) {
|
|
$this->logBusinessException('requestV2Raw', $url, $e, [
|
|
'payload' => $payload,
|
|
'includeClientId' => $includeClientId,
|
|
]);
|
|
throw new BusinessException($e->getMessage());
|
|
} catch (\Throwable $throwable) {
|
|
$this->logBusinessException('requestV2Raw', $url, $throwable, [
|
|
'payload' => $payload,
|
|
'includeClientId' => $includeClientId,
|
|
]);
|
|
throw $throwable;
|
|
}
|
|
}
|
|
|
|
public function extractV2UploadErrors(array $decodedResponse): array
|
|
{
|
|
$rows = $decodedResponse['data']['list'] ?? [];
|
|
if (! is_array($rows)) {
|
|
return [];
|
|
}
|
|
|
|
$errors = [];
|
|
foreach ($rows as $row) {
|
|
if (! is_array($row)) {
|
|
continue;
|
|
}
|
|
|
|
$rowErrors = $row['errors'] ?? [];
|
|
if (! is_array($rowErrors) || $rowErrors === []) {
|
|
continue;
|
|
}
|
|
|
|
$errors[] = [
|
|
'tableName' => isset($row['tableName']) && is_string($row['tableName']) ? $row['tableName'] : '',
|
|
'errors' => $rowErrors,
|
|
];
|
|
}
|
|
|
|
return $errors;
|
|
}
|
|
|
|
public function hasV2UploadErrors(array $decodedResponse): bool
|
|
{
|
|
return $this->extractV2UploadErrors($decodedResponse) !== [];
|
|
}
|
|
|
|
/**
|
|
* @throws ContainerExceptionInterface
|
|
* @throws NotFoundExceptionInterface
|
|
* 上报网络咨询(网络门诊)服务
|
|
*/
|
|
public function uploadConsult(array $arg): array
|
|
{
|
|
return $this->uploadData('api_upload_consult', $arg);
|
|
}
|
|
|
|
/**
|
|
* @throws ContainerExceptionInterface
|
|
* @throws NotFoundExceptionInterface
|
|
* 上报网络复诊服务
|
|
*/
|
|
public function uploadFurtherConsult(array $arg): array
|
|
{
|
|
return $this->uploadData('api_upload_further_consult', $arg);
|
|
}
|
|
|
|
/**
|
|
* @throws ContainerExceptionInterface
|
|
* @throws NotFoundExceptionInterface
|
|
* 上报电子处方服务
|
|
*/
|
|
public function uploadRecipe(array $arg): array
|
|
{
|
|
return $this->uploadData('api_upload_recipe', $arg);
|
|
}
|
|
|
|
/**
|
|
* @throws ContainerExceptionInterface
|
|
* @throws NotFoundExceptionInterface
|
|
* 上报药品处方明细
|
|
*/
|
|
public function uploadRecipeDetail(array $arg): array
|
|
{
|
|
return $this->uploadData('api_upload_recipe_detail_yp', $arg);
|
|
}
|
|
|
|
protected function requestAccessToken(): array
|
|
{
|
|
return $this->requestToken(
|
|
$this->buildAuthEndpoint('access'),
|
|
[
|
|
'grantType' => 'client_credentials',
|
|
]
|
|
);
|
|
}
|
|
|
|
protected function requestToken(string $url, array $payload): array
|
|
{
|
|
$decoded = $this->requestV2Decoded($url, $payload, true, false);
|
|
$tokenPayload = $this->extractTokenPayload($decoded['decoded']);
|
|
$this->cacheTokenPayload($tokenPayload);
|
|
|
|
return $tokenPayload;
|
|
}
|
|
|
|
protected function extractTokenPayload(array $decoded): array
|
|
{
|
|
$result = $decoded['result'] ?? null;
|
|
if (! is_array($result)) {
|
|
throw new BusinessException('Regulatory platform token response missing result');
|
|
}
|
|
|
|
$accessToken = $result['accessToken'] ?? '';
|
|
if (! is_string($accessToken) || $accessToken === '') {
|
|
throw new BusinessException('Regulatory platform token response missing accessToken');
|
|
}
|
|
|
|
$refreshToken = $result['refreshToken'] ?? $this->getCachedRefreshToken();
|
|
$expiresIn = (int) ($result['expiresIn'] ?? self::DEFAULT_ACCESS_TOKEN_TTL);
|
|
if ($expiresIn <= 0) {
|
|
$expiresIn = self::DEFAULT_ACCESS_TOKEN_TTL;
|
|
}
|
|
|
|
return [
|
|
'accessToken' => $accessToken,
|
|
'refreshToken' => is_string($refreshToken) ? $refreshToken : '',
|
|
'expiresIn' => $expiresIn,
|
|
'tokenType' => isset($result['tokenType']) && is_string($result['tokenType']) ? $result['tokenType'] : '',
|
|
];
|
|
}
|
|
|
|
protected function uploadData(string $tableName, array $rows): array
|
|
{
|
|
$accessToken = $this->getAccessToken();
|
|
$payload = [
|
|
$tableName => $this->normalizeUploadRows($rows),
|
|
];
|
|
$envelope = $this->buildV2RequestEnvelope($payload, false);
|
|
$response = $this->httpRequestV2($this->buildDataUploadRequestUrl(), [
|
|
'query' => $this->buildClientCredentialQuery(),
|
|
'headers' => [
|
|
'du-token' => $accessToken,
|
|
],
|
|
'json' => $envelope,
|
|
]);
|
|
$this->assertV2SuccessResponse($response);
|
|
|
|
$decodedResponse = $this->decodeV2ResponseEnvelope($response, false);
|
|
$decodedPayload = $decodedResponse['decoded'];
|
|
if ($this->hasV2UploadErrors($decodedPayload)) {
|
|
throw new BusinessException($this->formatV2UploadErrors($this->extractV2UploadErrors($decodedPayload)));
|
|
}
|
|
|
|
return $decodedPayload;
|
|
}
|
|
|
|
protected function cacheTokenPayload(array $tokenPayload): void
|
|
{
|
|
$expiresIn = (int) ($tokenPayload['expiresIn'] ?? self::DEFAULT_ACCESS_TOKEN_TTL);
|
|
if ($expiresIn <= 0) {
|
|
$expiresIn = self::DEFAULT_ACCESS_TOKEN_TTL;
|
|
}
|
|
|
|
$accessTokenTtl = max($expiresIn - self::ACCESS_TOKEN_BUFFER, 1);
|
|
$refreshTokenTtl = max($expiresIn, self::DEFAULT_REFRESH_TOKEN_TTL);
|
|
|
|
$this->redis->setex(self::ACCESS_TOKEN_CACHE_KEY, $accessTokenTtl, $tokenPayload['accessToken']);
|
|
$this->redis->setex(self::EXPIRES_AT_CACHE_KEY, $refreshTokenTtl, (string) (time() + $accessTokenTtl));
|
|
|
|
if (! empty($tokenPayload['refreshToken'])) {
|
|
$this->redis->setex(self::REFRESH_TOKEN_CACHE_KEY, $refreshTokenTtl, $tokenPayload['refreshToken']);
|
|
}
|
|
}
|
|
|
|
protected function getCachedAccessToken(): ?string
|
|
{
|
|
$token = $this->redis->get(self::ACCESS_TOKEN_CACHE_KEY);
|
|
|
|
return is_string($token) && $token !== '' ? $token : null;
|
|
}
|
|
|
|
protected function getCachedRefreshToken(): ?string
|
|
{
|
|
$token = $this->redis->get(self::REFRESH_TOKEN_CACHE_KEY);
|
|
|
|
return is_string($token) && $token !== '' ? $token : null;
|
|
}
|
|
|
|
protected function clearCachedRefreshToken(): void
|
|
{
|
|
$this->redis->del(self::REFRESH_TOKEN_CACHE_KEY);
|
|
}
|
|
|
|
protected function httpRequest(string $path, array $arg = []): array
|
|
{
|
|
$body = $this->sendJsonRequest($path, $arg, 'regulatoryPlatform-httpRequest', false);
|
|
|
|
if (isset($body['code'])) {
|
|
if (isset($body['message'])) {
|
|
throw new BusinessException((string) $body['message']);
|
|
}
|
|
|
|
throw new BusinessException(HttpEnumCode::getMessage(HttpEnumCode::SERVER_ERROR));
|
|
}
|
|
|
|
return $body;
|
|
}
|
|
|
|
protected function httpRequestV2(string $path, array $arg = []): array
|
|
{
|
|
return $this->sendJsonRequest($path, $arg, 'regulatoryPlatform-httpRequestV2', true);
|
|
}
|
|
|
|
protected function assertV2SuccessResponse(array $response): void
|
|
{
|
|
if (isset($response['code']) && (int) $response['code'] !== 0) {
|
|
throw new BusinessException($this->extractV2ErrorMessage($response));
|
|
}
|
|
|
|
if (isset($response['success']) && $response['success'] !== true) {
|
|
throw new BusinessException($this->extractV2ErrorMessage($response));
|
|
}
|
|
|
|
if (isset($response['serviceSuccess']) && $response['serviceSuccess'] !== true) {
|
|
throw new BusinessException($this->extractV2ErrorMessage($response));
|
|
}
|
|
}
|
|
|
|
protected function extractV2ErrorMessage(array $response): string
|
|
{
|
|
if (! empty($response['message']) && is_string($response['message'])) {
|
|
return $response['message'];
|
|
}
|
|
|
|
if (! empty($response['errors']) && is_array($response['errors'])) {
|
|
$first = $response['errors'][0] ?? null;
|
|
if (is_string($first) && $first !== '') {
|
|
return $first;
|
|
}
|
|
if (is_array($first)) {
|
|
$json = json_encode($first, JSON_UNESCAPED_UNICODE);
|
|
if ($json !== false) {
|
|
return $json;
|
|
}
|
|
}
|
|
}
|
|
|
|
return HttpEnumCode::getMessage(HttpEnumCode::SERVER_ERROR);
|
|
}
|
|
|
|
protected function buildAuthEndpoint(string $type): string
|
|
{
|
|
if (empty($this->api_url)) {
|
|
throw new BusinessException('Regulatory platform api_url is not configured');
|
|
}
|
|
|
|
if ($type === 'access') {
|
|
return $this->joinUrl($this->api_url, '/auth-api/oauth2/accessToken');
|
|
}
|
|
|
|
return $this->joinUrl($this->api_url, '/auth-api/oauth2/refreshToken');
|
|
}
|
|
|
|
protected function buildDataUploadEndpoint(): string
|
|
{
|
|
if (empty($this->api_url)) {
|
|
throw new BusinessException('Regulatory platform api_url is not configured');
|
|
}
|
|
|
|
return $this->joinUrl($this->api_url, '/du-api/v1/dataUpload');
|
|
}
|
|
|
|
protected function buildDataUploadRequestUrl(): string
|
|
{
|
|
return $this->buildDataUploadEndpoint();
|
|
}
|
|
|
|
protected function sendJsonRequest(string $path, array $arg, string $logChannel, bool $sanitize): array
|
|
{
|
|
$option = [
|
|
'verify' => false,
|
|
'timeout' => self::HTTP_TIMEOUT,
|
|
'connect_timeout' => self::HTTP_CONNECT_TIMEOUT,
|
|
'http_errors' => false,
|
|
];
|
|
if (! empty($option)) {
|
|
$arg = array_merge($arg, $option);
|
|
}
|
|
|
|
$requestLog = $sanitize ? $this->sanitizeLogContext($arg) : $arg;
|
|
$requestStartAt = microtime(true);
|
|
Log::getInstance($logChannel)->info(json_encode([
|
|
'phase' => 'request',
|
|
'url' => $path,
|
|
'request' => $requestLog,
|
|
], JSON_UNESCAPED_UNICODE));
|
|
|
|
$httpLogChannel = 'regulatoryPlatform-http-detail';
|
|
$requestDetail = $this->buildHttpRequestLogContext($path, $arg, $sanitize);
|
|
Log::getInstance($httpLogChannel, 'regulatory_platform_http_detail')->info(json_encode([
|
|
'phase' => 'request',
|
|
'url' => $path,
|
|
'request' => $requestDetail,
|
|
], JSON_UNESCAPED_UNICODE));
|
|
|
|
try {
|
|
$response = $this->client->post($path, $arg);
|
|
$statusCode = $response->getStatusCode();
|
|
$bodyText = (string) $response->getBody();
|
|
$body = json_decode($bodyText, true);
|
|
$durationMs = $this->toDurationMs($requestStartAt);
|
|
|
|
$responseLog = $sanitize
|
|
? $this->sanitizeLogContext(is_array($body) ? $body : ['raw' => $bodyText])
|
|
: (is_array($body) ? $body : ['raw' => $bodyText]);
|
|
Log::getInstance($logChannel)->info(json_encode([
|
|
'phase' => 'response',
|
|
'status' => $statusCode,
|
|
'duration_ms' => $durationMs,
|
|
'response' => $responseLog,
|
|
], JSON_UNESCAPED_UNICODE));
|
|
|
|
Log::getInstance($httpLogChannel, 'regulatory_platform_http_detail')->info(json_encode([
|
|
'phase' => 'response',
|
|
'url' => $path,
|
|
'status' => $statusCode,
|
|
'duration_ms' => $durationMs,
|
|
'response' => $this->buildHttpResponseLogContext($response, $bodyText, $body, $sanitize),
|
|
], JSON_UNESCAPED_UNICODE));
|
|
|
|
if ($statusCode !== 200) {
|
|
$message = is_array($body) ? $this->extractV2ErrorMessage($body) : $bodyText;
|
|
throw new BusinessException(sprintf('Regulatory platform HTTP request failed[%s]: %s', $statusCode, $message));
|
|
}
|
|
|
|
if (! is_array($body) || $body === []) {
|
|
throw new BusinessException(HttpEnumCode::getMessage(HttpEnumCode::SERVER_ERROR));
|
|
}
|
|
|
|
return $body;
|
|
} catch (\Throwable $throwable) {
|
|
Log::getInstance($httpLogChannel, 'regulatory_platform_http_detail')->error(json_encode([
|
|
'phase' => 'exception',
|
|
'url' => $path,
|
|
'duration_ms' => $this->toDurationMs($requestStartAt),
|
|
'request' => $requestDetail,
|
|
'exception' => [
|
|
'message' => $throwable->getMessage(),
|
|
'file' => $throwable->getFile(),
|
|
'line' => $throwable->getLine(),
|
|
],
|
|
], JSON_UNESCAPED_UNICODE));
|
|
|
|
throw $throwable;
|
|
}
|
|
}
|
|
|
|
protected function buildHttpRequestLogContext(string $url, array $arg, bool $sanitize): array
|
|
{
|
|
$query = $arg['query'] ?? [];
|
|
$headers = $arg['headers'] ?? [];
|
|
$json = $arg['json'] ?? null;
|
|
$formParams = $arg['form_params'] ?? null;
|
|
|
|
return [
|
|
'method' => 'POST',
|
|
'url' => $this->appendQueryString($url, is_array($query) ? $query : []),
|
|
'headers' => $sanitize ? $this->sanitizeLogContext(is_array($headers) ? $headers : []) : $headers,
|
|
'query' => $sanitize ? $this->sanitizeLogContext(is_array($query) ? $query : []) : $query,
|
|
'json' => $sanitize ? $this->sanitizeLogContext($json) : $json,
|
|
'form_params' => $sanitize ? $this->sanitizeLogContext($formParams) : $formParams,
|
|
'timeout' => $arg['timeout'] ?? null,
|
|
'connect_timeout' => $arg['connect_timeout'] ?? null,
|
|
'verify' => $arg['verify'] ?? null,
|
|
];
|
|
}
|
|
|
|
protected function buildHttpResponseLogContext(object $response, string $bodyText, mixed $body, bool $sanitize): array
|
|
{
|
|
$headers = [];
|
|
foreach ($response->getHeaders() as $key => $value) {
|
|
$headers[$key] = is_array($value) ? implode('; ', $value) : $value;
|
|
}
|
|
|
|
$normalizedBody = is_array($body) ? $body : ['raw' => $bodyText];
|
|
|
|
return [
|
|
'headers' => $sanitize ? $this->sanitizeLogContext($headers) : $headers,
|
|
'body' => $sanitize ? $this->sanitizeLogContext($normalizedBody) : $normalizedBody,
|
|
];
|
|
}
|
|
|
|
protected function appendQueryString(string $url, array $query): string
|
|
{
|
|
if ($query === []) {
|
|
return $url;
|
|
}
|
|
|
|
$queryString = http_build_query($query);
|
|
if ($queryString === '') {
|
|
return $url;
|
|
}
|
|
|
|
return str_contains($url, '?') ? $url . '&' . $queryString : $url . '?' . $queryString;
|
|
}
|
|
|
|
protected function toDurationMs(float $requestStartAt): int
|
|
{
|
|
return (int) round((microtime(true) - $requestStartAt) * 1000);
|
|
}
|
|
|
|
protected function logBusinessRequest(string $action, string $url, array $context): void
|
|
{
|
|
Log::getInstance('regulatoryPlatform-business', 'regulatory_platform_business')->info(json_encode([
|
|
'phase' => 'request',
|
|
'action' => $action,
|
|
'url' => $url,
|
|
'context' => $this->sanitizeLogContext($context),
|
|
], JSON_UNESCAPED_UNICODE));
|
|
}
|
|
|
|
protected function logBusinessResponse(string $action, string $url, array $context): void
|
|
{
|
|
Log::getInstance('regulatoryPlatform-business', 'regulatory_platform_business')->info(json_encode([
|
|
'phase' => 'response',
|
|
'action' => $action,
|
|
'url' => $url,
|
|
'context' => $this->sanitizeLogContext($context),
|
|
], JSON_UNESCAPED_UNICODE));
|
|
}
|
|
|
|
protected function logBusinessException(string $action, string $url, \Throwable $throwable, array $context = []): void
|
|
{
|
|
Log::getInstance('regulatoryPlatform-business', 'regulatory_platform_business')->error(json_encode([
|
|
'phase' => 'exception',
|
|
'action' => $action,
|
|
'url' => $url,
|
|
'context' => $this->sanitizeLogContext($context),
|
|
'exception' => [
|
|
'message' => $throwable->getMessage(),
|
|
'file' => $throwable->getFile(),
|
|
'line' => $throwable->getLine(),
|
|
],
|
|
], JSON_UNESCAPED_UNICODE));
|
|
}
|
|
|
|
protected function sanitizeLogContext(mixed $payload): mixed
|
|
{
|
|
if (! is_array($payload)) {
|
|
if (is_string($payload) && strlen($payload) > 160) {
|
|
return substr($payload, 0, 24) . '...' . substr($payload, -24);
|
|
}
|
|
|
|
return $payload;
|
|
}
|
|
|
|
$masked = [];
|
|
foreach ($payload as $key => $value) {
|
|
if (is_array($value)) {
|
|
$masked[$key] = $this->sanitizeLogContext($value);
|
|
continue;
|
|
}
|
|
|
|
$keyString = strtolower((string) $key);
|
|
if (in_array($keyString, ['sign', 'data', 'accesstoken', 'refreshtoken', 'client_secret', 'key'], true)) {
|
|
if (is_string($value)) {
|
|
$masked[$key] = [
|
|
'len' => strlen($value),
|
|
'preview' => strlen($value) > 16 ? substr($value, 0, 8) . '...' . substr($value, -8) : $value,
|
|
];
|
|
} else {
|
|
$masked[$key] = '[masked]';
|
|
}
|
|
continue;
|
|
}
|
|
|
|
$masked[$key] = $value;
|
|
}
|
|
|
|
return $masked;
|
|
}
|
|
|
|
protected function normalizeUploadRows(array $rows): array
|
|
{
|
|
$normalized = [];
|
|
foreach ($rows as $row) {
|
|
if (! is_array($row)) {
|
|
continue;
|
|
}
|
|
|
|
unset($row['accessToken'], $row['clientId']);
|
|
if (! isset($row['client_id']) || ! is_string($row['client_id']) || trim($row['client_id']) === '') {
|
|
$row['client_id'] = (string) $this->client_id;
|
|
}
|
|
|
|
if (array_key_exists('patientSex', $row)) {
|
|
$row['patientSex'] = $this->normalizePatientSex(
|
|
$row['patientSex'],
|
|
isset($row['patientIdcardType']) ? (int) $row['patientIdcardType'] : null,
|
|
isset($row['patientIdcardNum']) && is_scalar($row['patientIdcardNum']) ? (string) $row['patientIdcardNum'] : null
|
|
);
|
|
}
|
|
|
|
$normalized[] = $row;
|
|
}
|
|
|
|
return $normalized;
|
|
}
|
|
|
|
protected function normalizePatientSex(mixed $patientSex, ?int $patientIdcardType = null, ?string $patientIdcardNum = null): int
|
|
{
|
|
$inferredSex = $this->inferPatientSexFromIdCard($patientIdcardType, $patientIdcardNum);
|
|
if ($inferredSex !== null) {
|
|
return $inferredSex;
|
|
}
|
|
|
|
$patientSex = (int) $patientSex;
|
|
|
|
return in_array($patientSex, [1, 2], true) ? $patientSex : 0;
|
|
}
|
|
|
|
protected function inferPatientSexFromIdCard(?int $patientIdcardType, ?string $patientIdcardNum): ?int
|
|
{
|
|
if ($patientIdcardType !== 1 || empty($patientIdcardNum)) {
|
|
return null;
|
|
}
|
|
|
|
$patientIdcardNum = strtoupper(trim($patientIdcardNum));
|
|
if (preg_match('/^\d{17}[\dX]$/', $patientIdcardNum) === 1) {
|
|
return ((int) $patientIdcardNum[16]) % 2 === 1 ? 1 : 2;
|
|
}
|
|
|
|
if (preg_match('/^\d{15}$/', $patientIdcardNum) === 1) {
|
|
return ((int) $patientIdcardNum[14]) % 2 === 1 ? 1 : 2;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
protected function formatV2UploadErrors(array $errors): string
|
|
{
|
|
$messages = [];
|
|
foreach ($errors as $error) {
|
|
$tableName = isset($error['tableName']) && is_string($error['tableName']) ? $error['tableName'] : '';
|
|
$rowErrors = $error['errors'] ?? [];
|
|
if (! is_array($rowErrors) || $rowErrors === []) {
|
|
continue;
|
|
}
|
|
|
|
$message = implode('; ', array_map(static function (mixed $item): string {
|
|
if (is_string($item)) {
|
|
return $item;
|
|
}
|
|
|
|
$json = json_encode($item, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
|
|
return $json === false ? '[invalid error payload]' : $json;
|
|
}, $rowErrors));
|
|
|
|
$messages[] = $tableName !== '' ? sprintf('%s: %s', $tableName, $message) : $message;
|
|
}
|
|
|
|
if ($messages === []) {
|
|
return 'Regulatory platform upload validation failed';
|
|
}
|
|
|
|
return implode(' | ', $messages);
|
|
}
|
|
|
|
protected function buildClientCredentialQuery(): array
|
|
{
|
|
return array_filter([
|
|
'client_id' => $this->client_id,
|
|
'client_secret' => $this->client_secret,
|
|
], static fn (?string $value): bool => is_string($value) && $value !== '');
|
|
}
|
|
|
|
protected function joinUrl(string $baseUrl, string $path): string
|
|
{
|
|
return rtrim($baseUrl, '/') . '/' . ltrim($path, '/');
|
|
}
|
|
|
|
protected function resolveConfigValue(array $paths): ?string
|
|
{
|
|
foreach ($paths as $path) {
|
|
$value = $this->getByPath($this->config, $path);
|
|
if ($value !== null && $value !== '') {
|
|
return (string) $value;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
protected function getByPath(array $config, string $path): mixed
|
|
{
|
|
$current = $config;
|
|
foreach (explode('.', $path) as $segment) {
|
|
if (! is_array($current) || ! array_key_exists($segment, $current)) {
|
|
return null;
|
|
}
|
|
$current = $current[$segment];
|
|
}
|
|
|
|
return $current;
|
|
}
|
|
}
|