'', '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::listFiles,marker 分页) * * 七牛只在还有下一页时才回 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 / 没填配置」两种情况前置拦掉 * * 必须走 HTTPS:SDK 默认 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; } }