文件
xionggeshop/addons/shopro_huifu/library/onboard/HuifuOnboardPicture.php

381 行
13 KiB
PHP

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<?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;
}
}