Files
lgp-admin-plus-api/app/Service/common/upload/QiniuStorageService.php
2026-08-19 08:16:49 +08:00

224 lines
7.7 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Service\common\upload;
use App\BaseApp\BaseNotAuthService;
use App\Service\common\oss\OssStorageInterface;
use App\Service\common\UtilsService;
/**
* 七牛云上传;优先使用官方 SDK未安装时给出明确提示
*/
class QiniuStorageService extends BaseNotAuthService implements OssStorageInterface
{
use ObjectKeyNormalizeTrait;
protected array $config = [
'access_key' => '',
'secret_key' => '',
'bucket' => '',
'domain' => '',
'path_prefix' => '',
];
/** 同一次请求里复用,避免每页列举都去 UC 查一遍区域 */
private ?\Qiniu\Storage\BucketManager $cachedBucketMgr = null;
private string $cachedBucketMgrKey = '';
/**
* 注入解密后的运行时配置
*/
public function withConfig(array $config): static
{
$this->config = array_merge($this->config, $config);
$this->cachedBucketMgr = null;
$this->cachedBucketMgrKey = '';
return $this;
}
public function uploadImage($filePath, string $key): bool|array
{
return $this->uploadFile($filePath, $key);
}
public function uploadVideo($filePath, string $key): bool|array
{
return $this->uploadFile($filePath, $key);
}
public function deleteImage($key): bool
{
return $this->deleteFile($key);
}
public function deleteVideo($key): bool
{
return $this->deleteFile($key);
}
/**
* 列举空间对象BucketManager::listFilesmarker 分页)
*
* 七牛只在还有下一页时才回 marker所以 marker 为空即等于列完了。
* 列举结果的 key 必须用服务端原值,不能再套 path_prefix
* 否则根目录历史文件会被改写成 uploads/b_xxx.jpg。
*/
public function listObjects(string $prefix = '', string $marker = '', int $limit = 100): array
{
$bucketMgr = $this->bucketManager();
$bucket = (string) ($this->config['bucket'] ?? '');
$listPrefix = $this->scopedListPrefix($prefix);
[$ret, $err] = $bucketMgr->listFiles(
$bucket,
$listPrefix !== '' ? $listPrefix : null,
$marker !== '' ? $marker : null,
$this->boundedLimit($limit)
);
if ($err !== null) {
UtilsService::getInstance()->errorThrow('七牛云列举对象失败:' . $this->errorText($err));
}
$ret = is_array($ret) ? $ret : [];
$rawItems = $ret['items'] ?? $ret['Items'] ?? [];
// 个别 SDK 版本成功时直接回文件数组,而不是 {items: [...]}
if ($rawItems === [] && isset($ret[0]) && is_array($ret[0])) {
$rawItems = $ret;
}
$items = [];
foreach ((array) $rawItems as $row) {
if (!is_array($row)) {
continue;
}
$key = $this->normalizeListedKey((string) ($row['key'] ?? ''));
if ($key === '' || str_ends_with($key, '/')) {
continue;
}
$items[] = [
'key' => $key,
'size' => (int) ($row['fsize'] ?? 0),
'hash' => (string) ($row['hash'] ?? ''),
// putTime 的单位是 100 纳秒,当秒用会得到五亿年后的时间戳
'last_modified' => intdiv((int) ($row['putTime'] ?? 0), 10000000),
'url' => $this->publicUrlOf($key),
];
}
$next = (string) ($ret['marker'] ?? '');
return [
'items' => $items,
'next_marker' => $next,
'finished' => $next === '',
];
}
public function deleteObject(string $key): bool
{
return $this->deleteFile($this->normalizeListedKey($key));
}
/**
* 上传到七牛;无 SDK 时抛业务异常引导安装或改本地
*/
private function uploadFile($filePath, string $key): bool|array
{
if (!class_exists(\Qiniu\Auth::class)) {
UtilsService::getInstance()->errorThrow('未安装 qiniu/php-sdk请改用本地存储或安装依赖');
}
$accessKey = (string) ($this->config['access_key'] ?? '');
$secretKey = (string) ($this->config['secret_key'] ?? '');
$bucket = (string) ($this->config['bucket'] ?? '');
$domain = rtrim((string) ($this->config['domain'] ?? ''), '/');
if ($accessKey === '' || $secretKey === '' || $bucket === '') {
UtilsService::getInstance()->errorThrow('七牛云配置不完整');
}
$prefix = trim((string) ($this->config['path_prefix'] ?? ''), '/');
if ($prefix !== '' && !str_starts_with($key, $prefix . '/')) {
$key = $prefix . '/' . ltrim($key, '/');
}
$path = is_object($filePath) && method_exists($filePath, 'getRealPath')
? $filePath->getRealPath()
: (string) $filePath;
$auth = new \Qiniu\Auth($accessKey, $secretKey);
$token = $auth->uploadToken($bucket);
$uploadMgr = new \Qiniu\Storage\UploadManager();
list($ret, $err) = $uploadMgr->putFile($token, $key, $path);
if ($err !== null) {
return false;
}
return [
'key' => $ret['key'],
'url' => $domain . '/' . $ret['key'],
];
}
private function deleteFile(string $key): bool
{
$err = $this->bucketManager()->delete((string) $this->config['bucket'], $key);
return $err === null;
}
/**
* 构造 BucketManager顺手把「没装 SDK / 没填配置」两种情况前置拦掉
*
* 必须走 HTTPSSDK 默认 HTTP部分机房出网只放行 443列举会直接空失败。
*/
private function bucketManager(): \Qiniu\Storage\BucketManager
{
if (!class_exists(\Qiniu\Auth::class)) {
UtilsService::getInstance()->errorThrow('未安装 qiniu/php-sdk请改用本地存储或安装依赖');
}
$accessKey = trim((string) ($this->config['access_key'] ?? ''));
$secretKey = trim((string) ($this->config['secret_key'] ?? ''));
$bucket = trim((string) ($this->config['bucket'] ?? ''));
$missing = [];
if ($accessKey === '') {
$missing[] = 'AccessKey';
}
if ($secretKey === '') {
$missing[] = 'SecretKey';
}
if ($bucket === '') {
$missing[] = 'Bucket';
}
if ($missing !== []) {
UtilsService::getInstance()->errorThrow(
'七牛云配置不完整,缺少:' . implode('、', $missing) . '。请到「系统配置 → 存储」重新填写后保存'
);
}
$cacheKey = $accessKey . "\0" . $bucket;
if ($this->cachedBucketMgr !== null && $this->cachedBucketMgrKey === $cacheKey) {
return $this->cachedBucketMgr;
}
$sdkConfig = new \Qiniu\Config();
$sdkConfig->useHTTPS = true;
$this->cachedBucketMgr = new \Qiniu\Storage\BucketManager(
new \Qiniu\Auth($accessKey, $secretKey),
$sdkConfig
);
$this->cachedBucketMgrKey = $cacheKey;
return $this->cachedBucketMgr;
}
/**
* SDK 的错误对象没有统一契约,转成人能看懂的一行字
*/
private function errorText(mixed $err): string
{
if (is_object($err) && method_exists($err, 'message')) {
$text = trim((string) $err->message());
if ($text !== '') {
return $text;
}
}
if (is_object($err) && method_exists($err, 'code')) {
return 'HTTP ' . $err->code();
}
if (is_object($err) || is_array($err)) {
return (string) json_encode($err, JSON_UNESCAPED_UNICODE);
}
return (string) $err;
}
}