Files
miniapi/app/service/PassportOcrService.php
gaofeng 81ade70944 提交
2026-06-17 09:23:18 +08:00

234 lines
7.5 KiB
PHP

<?php
namespace app\service;
use Throwable;
class PassportOcrService
{
private const PASSPORT_ENDPOINT = '/ocr/passport/base64';
private const IDCARD_ENDPOINT = '/ocr/idcard/base64';
private const VISA_ENDPOINT = '/ocr/visa-number/base64';
public function passport($fileData): array
{
return $this->request(self::PASSPORT_ENDPOINT, $fileData);
}
public function identification($fileData): array
{
return $this->request(self::IDCARD_ENDPOINT, $fileData);
}
public function visa($fileData): array
{
return $this->request(self::VISA_ENDPOINT, $fileData);
}
private function request(string $endpoint, $fileData): array
{
if (!is_string($fileData) || $fileData === '') {
return $this->error('Unable to read uploaded file');
}
$config = $this->loadConfig();
if ($config['base_url'] === '' || $config['app_id'] === '' || $config['secret'] === '') {
return $this->error('OCR config missing: app.OCR_BASE_URL, app.OCR_APP_ID and app.OCR_SECRET are required');
}
try {
$payload = $this->buildPayload($fileData, $config);
} catch (Throwable $e) {
return $this->error('Unable to build OCR payload: ' . $e->getMessage());
}
$timestamp = (string) time();
$signature = hash_hmac('sha256', $config['app_id'] . "\n" . $timestamp, $config['secret']);
$headers = [
'Content-Type: application/json',
'Accept: application/json',
'X-App-Id: ' . $config['app_id'],
'X-Timestamp: ' . $timestamp,
'X-Signature: ' . $signature,
];
if ($config['origin'] !== '') {
$headers[] = 'Origin: ' . $config['origin'];
}
$ch = curl_init($config['base_url'] . $endpoint);
if ($ch === false) {
return $this->error('Unable to initialize cURL');
}
try {
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE),
CURLOPT_TIMEOUT => $config['timeout'],
CURLOPT_CONNECTTIMEOUT => $config['connect_timeout'],
]);
if (!$config['verify_ssl']) {
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
}
$response = curl_exec($ch);
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) {
return $this->error('OCR request failed: ' . curl_error($ch));
}
$decoded = json_decode((string) $response, true);
if (!is_array($decoded)) {
return $this->error('OCR response is not valid JSON', [
'http_code' => $httpCode,
'response' => substr((string) $response, 0, 300),
]);
}
return $decoded;
} finally {
curl_close($ch);
}
}
private function buildPayload(string $fileData, array $config): array
{
$mimeType = $this->detectMimeType($fileData);
$extension = $this->extensionForMimeType($mimeType);
return [
'filename' => 'upload.' . $extension,
'content_type' => $mimeType,
'image_base64' => 'data:' . $mimeType . ';base64,' . base64_encode($fileData),
'auto_rotate' => $config['auto_rotate'],
'return_debug' => $config['return_debug'],
];
}
private function loadConfig(): array
{
$config = config('ocr') ?: [];
return [
'base_url' => rtrim((string) (config('app.OCR_BASE_URL') ?? ''), '/'),
'app_id' => (string) (config('app.OCR_APP_ID') ?? ''),
'secret' => (string) (config('app.OCR_SECRET') ?? ''),
'origin' => $this->resolveOrigin((string) (config('app.OCR_ORIGIN') ?? ($config['origin'] ?? ''))),
'timeout' => max(1, (int) ($config['timeout'] ?? 60)),
'connect_timeout' => max(1, (int) ($config['connect_timeout'] ?? 10)),
'auto_rotate' => $this->boolValue($config['auto_rotate'] ?? true),
'return_debug' => $this->boolValue($config['return_debug'] ?? false),
'verify_ssl' => $this->boolValue($config['verify_ssl'] ?? true),
];
}
private function resolveOrigin(string $configuredOrigin): string
{
$configuredOrigin = trim($configuredOrigin);
if ($configuredOrigin !== '') {
return rtrim($configuredOrigin, '/');
}
$requestOrigin = trim((string) ($_SERVER['HTTP_ORIGIN'] ?? ''));
if ($this->isValidOrigin($requestOrigin)) {
return rtrim($requestOrigin, '/');
}
$host = trim((string) ($_SERVER['HTTP_HOST'] ?? ''));
if ($host === '' || !preg_match('/^[A-Za-z0-9.-]+(?::\d+)?$/', $host)) {
return '';
}
return $this->currentScheme() . '://' . $host;
}
private function isValidOrigin(string $origin): bool
{
if ($origin === '') {
return false;
}
$parts = parse_url($origin);
if (!is_array($parts)) {
return false;
}
$scheme = strtolower((string) ($parts['scheme'] ?? ''));
return in_array($scheme, ['http', 'https'], true)
&& !empty($parts['host'])
&& empty($parts['path'])
&& empty($parts['query'])
&& empty($parts['fragment']);
}
private function currentScheme(): string
{
$forwardedProto = trim((string) ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? ''));
if ($forwardedProto !== '') {
$forwardedProto = strtolower(trim(explode(',', $forwardedProto)[0]));
if (in_array($forwardedProto, ['http', 'https'], true)) {
return $forwardedProto;
}
}
$requestScheme = strtolower((string) ($_SERVER['REQUEST_SCHEME'] ?? ''));
if (in_array($requestScheme, ['http', 'https'], true)) {
return $requestScheme;
}
$https = strtolower((string) ($_SERVER['HTTPS'] ?? ''));
return $https !== '' && $https !== 'off' ? 'https' : 'http';
}
private function detectMimeType(string $fileData): string
{
if (!class_exists(\finfo::class)) {
return 'image/jpeg';
}
$finfo = new \finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->buffer($fileData);
if (is_string($mimeType) && $mimeType !== '' && $mimeType !== 'application/octet-stream') {
return $mimeType;
}
return 'image/jpeg';
}
private function extensionForMimeType(string $mimeType): string
{
return match ($mimeType) {
'image/png' => 'png',
'image/gif' => 'gif',
'image/webp' => 'webp',
'image/bmp', 'image/x-ms-bmp' => 'bmp',
default => 'jpg',
};
}
private function boolValue($value): bool
{
$parsed = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
return $parsed ?? (bool) $value;
}
private function error(string $message, array $data = []): array
{
$result = [
'code' => 0,
'msg' => $message,
];
if ($data !== []) {
$result['data'] = $data;
}
return $result;
}
}