381 行
13 KiB
PHP
381 行
13 KiB
PHP
<?php
|
||
|
||
namespace addons\shopro_huifu\library\onboard;
|
||
|
||
use addons\shopro_huifu\library\HuifuConfig;
|
||
use addons\shopro_huifu\library\SdkLoader;
|
||
use think\Log;
|
||
|
||
class HuifuOnboardPicture
|
||
{
|
||
protected $sdk;
|
||
|
||
public function __construct(HuifuOnboardSdk $sdk = null)
|
||
{
|
||
$this->sdk = $sdk ?: new HuifuOnboardSdk();
|
||
}
|
||
|
||
/**
|
||
* @param string $fileType F02/F03/F13
|
||
* @param string $imageUrl 站点可访问图片 URL
|
||
*/
|
||
public function uploadByUrl($fileType, $imageUrl)
|
||
{
|
||
SdkLoader::requireRequest('V2SupplementaryPictureRequest.php');
|
||
$meta = $this->sdk->newReqMeta();
|
||
$request = new \BsPaySdk\request\V2SupplementaryPictureRequest();
|
||
$request->setReqSeqId($meta['req_seq_id']);
|
||
$request->setReqDate($meta['req_date']);
|
||
$request->setFileType($fileType);
|
||
|
||
list($sourcePath, $sourceTemp) = $this->resolveUploadFile($imageUrl);
|
||
list($uploadPath, $uploadTemp) = $this->normalizeImageForHuifu($sourcePath, $sourceTemp);
|
||
|
||
$filename = $this->buildUploadFilename($fileType, $uploadPath);
|
||
$mime = $this->detectMime($uploadPath);
|
||
$merchantHuifuId = $this->sdk->getPictureMerchantHuifuId();
|
||
|
||
$extend = $this->sdk->mergeSkillHuifuId([
|
||
'huifu_id' => $merchantHuifuId,
|
||
'picture' => $filename,
|
||
]);
|
||
$request->setExtendInfo($extend);
|
||
|
||
$uploadTag = new \CURLFile($uploadPath, $mime, $filename);
|
||
|
||
try {
|
||
$result = $this->sdk->getClient()->postRequest($request, $uploadTag);
|
||
return $this->parsePictureUploadResult($result, $fileType);
|
||
} finally {
|
||
if ($sourceTemp && is_file($sourcePath)) {
|
||
@unlink($sourcePath);
|
||
}
|
||
if ($uploadTemp && $uploadPath !== $sourcePath && is_file($uploadPath)) {
|
||
@unlink($uploadPath);
|
||
}
|
||
}
|
||
}
|
||
|
||
protected function parsePictureUploadResult($result, $fileType)
|
||
{
|
||
if (!$result || $result->isError()) {
|
||
$msg = \addons\shopro_huifu\library\HuifuClient::formatSdkError($result);
|
||
Log::error('[shopro_huifu] onboard picture sdk fail type=' . $fileType . ' ' . $msg);
|
||
throw new \RuntimeException('图片上传汇付失败:' . $msg);
|
||
}
|
||
|
||
$raw = $result->getRspDatas();
|
||
Log::info('[shopro_huifu] onboard picture resp type=' . $fileType . ' raw=' . json_encode($raw, JSON_UNESCAPED_UNICODE));
|
||
|
||
$layers = [];
|
||
if (is_array($raw)) {
|
||
$layers[] = $raw;
|
||
if (isset($raw['data'])) {
|
||
$layers[] = $this->decodeLayer($raw['data']);
|
||
}
|
||
}
|
||
|
||
foreach ($layers as $layer) {
|
||
if (!is_array($layer)) {
|
||
continue;
|
||
}
|
||
$this->assertPictureResponse($layer);
|
||
$fileId = $this->extractPictureFileId($layer);
|
||
if ($fileId !== '') {
|
||
return $fileId;
|
||
}
|
||
}
|
||
|
||
$desc = $this->pickResponseDesc($layers);
|
||
Log::error('[shopro_huifu] onboard picture no file_id type=' . $fileType . ' resp=' . json_encode($raw, JSON_UNESCAPED_UNICODE));
|
||
throw new \RuntimeException('图片上传汇付失败' . ($desc ? ':' . $desc : '') . ',请使用 jpg/png 格式且大小不超过 2MB');
|
||
}
|
||
|
||
protected function decodeLayer($value)
|
||
{
|
||
if (is_array($value)) {
|
||
return $value;
|
||
}
|
||
if (is_string($value) && $value !== '') {
|
||
$decoded = json_decode($value, true);
|
||
return is_array($decoded) ? $decoded : [];
|
||
}
|
||
return [];
|
||
}
|
||
|
||
protected function pickResponseDesc(array $layers)
|
||
{
|
||
foreach ($layers as $layer) {
|
||
if (!is_array($layer)) {
|
||
continue;
|
||
}
|
||
foreach (['sub_resp_desc', 'resp_desc'] as $key) {
|
||
if (!empty($layer[$key])) {
|
||
return (string) $layer[$key];
|
||
}
|
||
}
|
||
}
|
||
return '';
|
||
}
|
||
|
||
protected function assertPictureResponse(array $data)
|
||
{
|
||
$subCode = (string) ($data['sub_resp_code'] ?? '');
|
||
if ($subCode !== '' && $subCode !== '00000000') {
|
||
$desc = (string) ($data['sub_resp_desc'] ?? '业务失败');
|
||
throw new \RuntimeException('图片上传汇付失败:' . $desc . ' [' . $subCode . ']');
|
||
}
|
||
$respCode = (string) ($data['resp_code'] ?? '');
|
||
if ($respCode !== '' && !in_array($respCode, ['00000000', '10000'], true)) {
|
||
$desc = (string) ($data['resp_desc'] ?? '请求失败');
|
||
throw new \RuntimeException('图片上传汇付失败:' . $desc . ' [' . $respCode . ']');
|
||
}
|
||
}
|
||
|
||
protected function extractPictureFileId(array $data)
|
||
{
|
||
foreach (['file_id', 'jfile_id', 'fileId'] as $key) {
|
||
if (!empty($data[$key])) {
|
||
return trim((string) $data[$key]);
|
||
}
|
||
}
|
||
foreach ($data as $value) {
|
||
if (is_array($value)) {
|
||
$nested = $this->extractPictureFileId($value);
|
||
if ($nested !== '') {
|
||
return $nested;
|
||
}
|
||
} elseif (is_string($value) && $value !== '' && $value[0] === '{') {
|
||
$inner = json_decode($value, true);
|
||
if (is_array($inner)) {
|
||
$nested = $this->extractPictureFileId($inner);
|
||
if ($nested !== '') {
|
||
return $nested;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return '';
|
||
}
|
||
|
||
protected function buildUploadFilename($fileType, $localPath)
|
||
{
|
||
$map = [
|
||
'F02' => 'id_front',
|
||
'F03' => 'id_back',
|
||
'F12' => 'bank_card',
|
||
'F13' => 'bank_card',
|
||
];
|
||
$base = $map[$fileType] ?? 'upload';
|
||
$ext = strtolower(pathinfo($localPath, PATHINFO_EXTENSION));
|
||
if (!in_array($ext, ['jpg', 'jpeg', 'png'], true)) {
|
||
$ext = 'jpg';
|
||
}
|
||
if ($ext === 'jpeg') {
|
||
$ext = 'jpg';
|
||
}
|
||
return substr($base, 0, 20) . '.' . $ext;
|
||
}
|
||
|
||
protected function detectMime($localPath)
|
||
{
|
||
if (function_exists('mime_content_type')) {
|
||
$mime = mime_content_type($localPath);
|
||
if ($mime && stripos($mime, 'image/') === 0) {
|
||
return $mime;
|
||
}
|
||
}
|
||
$ext = strtolower(pathinfo($localPath, PATHINFO_EXTENSION));
|
||
if ($ext === 'png') {
|
||
return 'image/png';
|
||
}
|
||
return 'image/jpeg';
|
||
}
|
||
|
||
/**
|
||
* 转为汇付支持的 jpg/png,必要时压缩
|
||
* @return array [uploadPath, shouldDeleteUploadPath]
|
||
*/
|
||
protected function normalizeImageForHuifu($localPath, $isTemp)
|
||
{
|
||
if (!is_file($localPath)) {
|
||
throw new \RuntimeException('图片文件不存在');
|
||
}
|
||
if (filesize($localPath) > 2 * 1024 * 1024) {
|
||
$compressed = $this->compressImage($localPath);
|
||
if ($compressed) {
|
||
return [$compressed, true];
|
||
}
|
||
throw new \RuntimeException('图片超过 2MB,请压缩后重新上传');
|
||
}
|
||
|
||
$binary = file_get_contents($localPath);
|
||
if ($binary === false || $binary === '') {
|
||
throw new \RuntimeException('无法读取图片文件');
|
||
}
|
||
|
||
$isJpeg = strncmp($binary, "\xFF\xD8\xFF", 3) === 0;
|
||
$isPng = strncmp($binary, "\x89PNG\r\n\x1a\n", 8) === 0;
|
||
if ($isJpeg || $isPng) {
|
||
return [$localPath, $isTemp];
|
||
}
|
||
|
||
if (!function_exists('imagecreatefromstring') || !function_exists('imagejpeg')) {
|
||
throw new \RuntimeException('图片格式不支持,请上传 jpg 或 png 格式');
|
||
}
|
||
|
||
$img = @imagecreatefromstring($binary);
|
||
if (!$img) {
|
||
throw new \RuntimeException('图片格式不支持,请上传 jpg 或 png 格式');
|
||
}
|
||
$tmp = tempnam(sys_get_temp_dir(), 'hf_pic_conv_');
|
||
$out = $tmp . '.jpg';
|
||
@unlink($tmp);
|
||
if (!imagejpeg($img, $out, 85)) {
|
||
imagedestroy($img);
|
||
throw new \RuntimeException('图片格式转换失败');
|
||
}
|
||
imagedestroy($img);
|
||
if (filesize($out) > 2 * 1024 * 1024) {
|
||
@unlink($out);
|
||
throw new \RuntimeException('图片超过 2MB,请压缩后重新上传');
|
||
}
|
||
return [$out, true];
|
||
}
|
||
|
||
protected function compressImage($localPath)
|
||
{
|
||
if (!function_exists('imagecreatefromstring') || !function_exists('imagejpeg')) {
|
||
return null;
|
||
}
|
||
$binary = file_get_contents($localPath);
|
||
if ($binary === false) {
|
||
return null;
|
||
}
|
||
$img = @imagecreatefromstring($binary);
|
||
if (!$img) {
|
||
return null;
|
||
}
|
||
$width = imagesx($img);
|
||
$height = imagesy($img);
|
||
$maxSide = 1600;
|
||
if ($width > $maxSide || $height > $maxSide) {
|
||
$scale = min($maxSide / $width, $maxSide / $height);
|
||
$newW = (int) round($width * $scale);
|
||
$newH = (int) round($height * $scale);
|
||
$resized = imagecreatetruecolor($newW, $newH);
|
||
imagecopyresampled($resized, $img, 0, 0, 0, 0, $newW, $newH, $width, $height);
|
||
imagedestroy($img);
|
||
$img = $resized;
|
||
}
|
||
$tmp = tempnam(sys_get_temp_dir(), 'hf_pic_zip_');
|
||
$out = $tmp . '.jpg';
|
||
@unlink($tmp);
|
||
$ok = false;
|
||
for ($quality = 85; $quality >= 55; $quality -= 10) {
|
||
if (imagejpeg($img, $out, $quality) && filesize($out) <= 2 * 1024 * 1024) {
|
||
$ok = true;
|
||
break;
|
||
}
|
||
}
|
||
imagedestroy($img);
|
||
if (!$ok) {
|
||
@unlink($out);
|
||
return null;
|
||
}
|
||
return $out;
|
||
}
|
||
|
||
/**
|
||
* @return array [localPath, isTemp]
|
||
*/
|
||
protected function resolveUploadFile($imageUrl)
|
||
{
|
||
$localPath = $this->resolveLocalPath($imageUrl);
|
||
if ($localPath && is_file($localPath)) {
|
||
return [$localPath, false];
|
||
}
|
||
$absoluteUrl = $this->resolveAbsoluteUrl($imageUrl);
|
||
$content = $this->fetchImageBinary($absoluteUrl);
|
||
if ($content === '') {
|
||
throw new \RuntimeException('无法读取图片:' . $absoluteUrl);
|
||
}
|
||
if (strlen($content) > 2 * 1024 * 1024) {
|
||
throw new \RuntimeException('图片超过 2MB,请压缩后重新上传');
|
||
}
|
||
$ext = $this->guessImageExt($content, $absoluteUrl);
|
||
$tmp = tempnam(sys_get_temp_dir(), 'hf_pic_');
|
||
if ($tmp === false) {
|
||
throw new \RuntimeException('创建临时图片文件失败');
|
||
}
|
||
$tmpPath = $tmp . '.' . $ext;
|
||
@unlink($tmp);
|
||
if (file_put_contents($tmpPath, $content) === false) {
|
||
throw new \RuntimeException('写入临时图片失败');
|
||
}
|
||
return [$tmpPath, true];
|
||
}
|
||
|
||
protected function fetchImageBinary($url)
|
||
{
|
||
if (function_exists('curl_init')) {
|
||
$ch = curl_init($url);
|
||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
|
||
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||
$body = curl_exec($ch);
|
||
curl_close($ch);
|
||
if ($body !== false && $body !== '') {
|
||
return $body;
|
||
}
|
||
}
|
||
$body = @file_get_contents($url);
|
||
return $body !== false ? $body : '';
|
||
}
|
||
|
||
protected function guessImageExt($binary, $url)
|
||
{
|
||
if (strncmp($binary, "\x89PNG\r\n\x1a\n", 8) === 0) {
|
||
return 'png';
|
||
}
|
||
if (strncmp($binary, "\xFF\xD8\xFF", 3) === 0) {
|
||
return 'jpg';
|
||
}
|
||
$path = parse_url($url, PHP_URL_PATH);
|
||
$ext = strtolower(pathinfo((string) $path, PATHINFO_EXTENSION));
|
||
if (in_array($ext, ['jpg', 'jpeg', 'png'], true)) {
|
||
return $ext === 'jpeg' ? 'jpg' : $ext;
|
||
}
|
||
return 'jpg';
|
||
}
|
||
|
||
protected function resolveAbsoluteUrl($url)
|
||
{
|
||
$url = trim($url);
|
||
if (strpos($url, 'http') === 0) {
|
||
return $url;
|
||
}
|
||
$base = HuifuConfig::getInstance()->getSiteBaseUrl();
|
||
return rtrim($base, '/') . '/' . ltrim($url, '/');
|
||
}
|
||
|
||
protected function resolveLocalPath($url)
|
||
{
|
||
$path = parse_url($url, PHP_URL_PATH);
|
||
if (!$path) {
|
||
$path = $url;
|
||
}
|
||
if (strpos($path, '/uploads/') !== false) {
|
||
$relative = substr($path, strpos($path, '/uploads/'));
|
||
$local = ROOT_PATH . 'public' . str_replace('/', DS, $relative);
|
||
if (is_file($local)) {
|
||
return $local;
|
||
}
|
||
}
|
||
$try = ROOT_PATH . 'public' . str_replace('/', DS, $path);
|
||
return is_file($try) ? $try : null;
|
||
}
|
||
}
|