mirror of
https://gitee.com/technical-laohu/mpay_v2_webman.git
synced 2026-04-27 12:34:28 +08:00
更新统一使用 PHPDoc + PSR-19 标准注释
This commit is contained in:
@@ -3,57 +3,69 @@
|
||||
namespace app\service\file\storage;
|
||||
|
||||
use app\common\constant\FileConstant;
|
||||
use app\exception\ResourceNotFoundException;
|
||||
use app\service\file\StorageConfigService;
|
||||
use support\Response;
|
||||
|
||||
/**
|
||||
* 文件存储驱动抽象基类。
|
||||
*
|
||||
* 提供文件存储驱动公共能力。
|
||||
*
|
||||
* @property-read StorageConfigService $storageConfigService 存储配置服务
|
||||
*/
|
||||
abstract class AbstractStorageDriver implements StorageDriverInterface
|
||||
{
|
||||
/**
|
||||
* 注入存储配置服务。
|
||||
*
|
||||
* @param StorageConfigService $storageConfigService 存储配置服务
|
||||
*/
|
||||
public function __construct(
|
||||
protected StorageConfigService $storageConfigService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 从资产数组中读取指定字段。
|
||||
*
|
||||
* @param array<string, mixed> $asset 文件资产数据
|
||||
* @param string $key 字段名
|
||||
* @param mixed $default 默认值
|
||||
* @return mixed 资产字段值
|
||||
*/
|
||||
protected function assetValue(array $asset, string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $asset[$key] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析本地存储文件的绝对路径。
|
||||
*
|
||||
* @param array $asset 文件资产数据
|
||||
* @return string 绝对路径
|
||||
*/
|
||||
protected function resolveLocalAbsolutePath(array $asset): string
|
||||
{
|
||||
$objectKey = trim((string) $this->assetValue($asset, 'object_key', ''));
|
||||
if ($objectKey === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$visibility = (int) $this->assetValue($asset, 'visibility', FileConstant::VISIBILITY_PRIVATE);
|
||||
$candidate = '';
|
||||
|
||||
if ($objectKey !== '') {
|
||||
$candidate = $this->storageConfigService->buildLocalAbsolutePath($visibility, $objectKey);
|
||||
if ($candidate !== '' && is_file($candidate)) {
|
||||
return $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (['url', 'public_url'] as $field) {
|
||||
$url = trim((string) $this->assetValue($asset, $field, ''));
|
||||
if ($url === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$parsedPath = (string) parse_url($url, PHP_URL_PATH);
|
||||
if ($parsedPath === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$candidate = public_path() . DIRECTORY_SEPARATOR . ltrim($parsedPath, '/');
|
||||
if (is_file($candidate)) {
|
||||
return $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return $candidate;
|
||||
return $this->storageConfigService->buildLocalAbsolutePath($visibility, $objectKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造字符串响应。
|
||||
*
|
||||
* @param string $body 响应内容
|
||||
* @param string $mimeType MIME 类型
|
||||
* @param int $status HTTP 状态码
|
||||
* @param array $headers 额外响应头
|
||||
* @return Response 响应对象
|
||||
*/
|
||||
protected function bodyResponse(string $body, string $mimeType = 'application/octet-stream', int $status = 200, array $headers = []): Response
|
||||
{
|
||||
$responseHeaders = array_merge([
|
||||
@@ -63,6 +75,14 @@ abstract class AbstractStorageDriver implements StorageDriverInterface
|
||||
return response($body, $status, $responseHeaders);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造文件下载响应。
|
||||
*
|
||||
* @param string $body 响应内容
|
||||
* @param string $downloadName 下载文件名
|
||||
* @param string $mimeType MIME 类型
|
||||
* @return Response 响应对象
|
||||
*/
|
||||
protected function downloadBodyResponse(string $body, string $downloadName, string $mimeType = 'application/octet-stream'): Response
|
||||
{
|
||||
$response = $this->bodyResponse($body, $mimeType, 200, [
|
||||
@@ -72,6 +92,14 @@ abstract class AbstractStorageDriver implements StorageDriverInterface
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据本地路径构造预览或下载响应。
|
||||
*
|
||||
* @param string $path 本地文件路径
|
||||
* @param string $downloadName 下载文件名
|
||||
* @param bool $attachment 是否下载附件
|
||||
* @return Response 响应对象
|
||||
*/
|
||||
protected function responseFromPath(string $path, string $downloadName = '', bool $attachment = false): Response
|
||||
{
|
||||
if ($attachment) {
|
||||
@@ -81,26 +109,46 @@ abstract class AbstractStorageDriver implements StorageDriverInterface
|
||||
return response()->file($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造本地文件预览响应。
|
||||
*
|
||||
* @param array $asset 文件资产数据
|
||||
* @return Response 响应对象
|
||||
* @throws ResourceNotFoundException
|
||||
*/
|
||||
protected function localPreviewResponse(array $asset): Response
|
||||
{
|
||||
$path = $this->resolveLocalAbsolutePath($asset);
|
||||
if ($path === '' || !is_file($path)) {
|
||||
return response('文件不存在', 404);
|
||||
throw new ResourceNotFoundException('文件不存在');
|
||||
}
|
||||
|
||||
return $this->responseFromPath($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造本地文件下载响应。
|
||||
*
|
||||
* @param array $asset 文件资产数据
|
||||
* @return Response 响应对象
|
||||
* @throws ResourceNotFoundException
|
||||
*/
|
||||
protected function localDownloadResponse(array $asset): Response
|
||||
{
|
||||
$path = $this->resolveLocalAbsolutePath($asset);
|
||||
if ($path === '' || !is_file($path)) {
|
||||
return response('文件不存在', 404);
|
||||
throw new ResourceNotFoundException('文件不存在');
|
||||
}
|
||||
|
||||
return $this->responseFromPath($path, (string) $this->assetValue($asset, 'original_name', basename($path)), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据文件场景返回目录前缀。
|
||||
*
|
||||
* @param int $scene 文件场景
|
||||
* @return string 目录前缀
|
||||
*/
|
||||
protected function scenePrefix(int $scene): string
|
||||
{
|
||||
return match ($scene) {
|
||||
|
||||
@@ -4,20 +4,36 @@ namespace app\service\file\storage;
|
||||
|
||||
use app\common\constant\FileConstant;
|
||||
use app\exception\BusinessStateException;
|
||||
use app\exception\ResourceNotFoundException;
|
||||
use Qcloud\Cos\Client as CosClient;
|
||||
use support\Response;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* 腾讯云 COS 文件存储驱动。
|
||||
*
|
||||
* 负责对象上传、删除、公开地址生成和对象内容响应。
|
||||
*/
|
||||
class CosStorageDriver extends AbstractStorageDriver
|
||||
{
|
||||
/**
|
||||
* 获取 COS 存储引擎标识。
|
||||
*
|
||||
* @return int 存储引擎常量
|
||||
*/
|
||||
public function engine(): int
|
||||
{
|
||||
return FileConstant::STORAGE_TENCENT_COS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将本地临时文件上传到 COS。
|
||||
*
|
||||
* @param string $sourcePath 待上传文件路径
|
||||
* @param array $context 上传上下文,包含 object_key、visibility 等信息
|
||||
* @return array 上传后的资产数据
|
||||
* @throws BusinessStateException
|
||||
*/
|
||||
public function storeFromPath(string $sourcePath, array $context): array
|
||||
{
|
||||
if (!is_file($sourcePath)) {
|
||||
@@ -33,6 +49,7 @@ class CosStorageDriver extends AbstractStorageDriver
|
||||
|
||||
$client = $this->client($config);
|
||||
$objectKey = (string) ($context['object_key'] ?? '');
|
||||
$visibility = (int) ($context['visibility'] ?? FileConstant::VISIBILITY_PRIVATE);
|
||||
$client->putObject([
|
||||
'Bucket' => (string) $config['bucket'],
|
||||
'Key' => $objectKey,
|
||||
@@ -40,23 +57,30 @@ class CosStorageDriver extends AbstractStorageDriver
|
||||
]);
|
||||
|
||||
$publicUrl = $this->publicUrl([
|
||||
'visibility' => $visibility,
|
||||
'object_key' => $objectKey,
|
||||
]);
|
||||
$visibility = (int) ($context['visibility'] ?? FileConstant::VISIBILITY_PRIVATE);
|
||||
|
||||
return [
|
||||
'storage_engine' => $this->engine(),
|
||||
'object_key' => $objectKey,
|
||||
'url' => $visibility === FileConstant::VISIBILITY_PUBLIC ? $publicUrl : '',
|
||||
'public_url' => $publicUrl,
|
||||
'public_url' => $visibility === FileConstant::VISIBILITY_PUBLIC ? $publicUrl : '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 COS 对象。
|
||||
*
|
||||
* @param array $asset 文件资产数据
|
||||
* @return bool 是否删除成功
|
||||
* @throws BusinessStateException
|
||||
*/
|
||||
public function delete(array $asset): bool
|
||||
{
|
||||
$config = $this->storageConfigService->cosConfig();
|
||||
if (trim((string) ($config['bucket'] ?? '')) === '') {
|
||||
return false;
|
||||
throw new BusinessStateException('腾讯云 COS 存储配置未完整');
|
||||
}
|
||||
|
||||
$objectKey = (string) ($asset['object_key'] ?? '');
|
||||
@@ -73,23 +97,41 @@ class CosStorageDriver extends AbstractStorageDriver
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造 COS 文件预览响应。
|
||||
*
|
||||
* @param array $asset 文件资产数据
|
||||
* @return Response 响应对象
|
||||
*/
|
||||
public function previewResponse(array $asset): Response
|
||||
{
|
||||
$url = $this->publicUrl($asset);
|
||||
if ($url !== '') {
|
||||
return redirect($url);
|
||||
}
|
||||
|
||||
return $this->responseFromObject($asset, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造 COS 文件下载响应。
|
||||
*
|
||||
* @param array $asset 文件资产数据
|
||||
* @return Response 响应对象
|
||||
*/
|
||||
public function downloadResponse(array $asset): Response
|
||||
{
|
||||
return $this->responseFromObject($asset, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 COS 公开访问地址。
|
||||
*
|
||||
* @param array $asset 文件资产数据
|
||||
* @return string 公共 URL
|
||||
*/
|
||||
public function publicUrl(array $asset): string
|
||||
{
|
||||
$visibility = (int) ($asset['visibility'] ?? FileConstant::VISIBILITY_PRIVATE);
|
||||
if ($visibility !== FileConstant::VISIBILITY_PUBLIC) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$publicUrl = trim((string) ($asset['url'] ?? $asset['public_url'] ?? ''));
|
||||
if ($publicUrl !== '') {
|
||||
return $publicUrl;
|
||||
@@ -115,11 +157,17 @@ class CosStorageDriver extends AbstractStorageDriver
|
||||
return 'https://' . $bucket . '.cos.' . $region . '.myqcloud.com/' . ltrim($objectKey, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 COS 临时访问地址。
|
||||
*
|
||||
* @param array $asset 文件资产数据
|
||||
* @return string 临时 URL
|
||||
*/
|
||||
public function temporaryUrl(array $asset): string
|
||||
{
|
||||
$config = $this->storageConfigService->cosConfig();
|
||||
if (trim((string) ($config['bucket'] ?? '')) === '' || trim((string) ($config['region'] ?? '')) === '') {
|
||||
return $this->publicUrl($asset);
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -134,10 +182,16 @@ class CosStorageDriver extends AbstractStorageDriver
|
||||
$objectKey
|
||||
);
|
||||
} catch (Throwable) {
|
||||
return $this->publicUrl($asset);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 COS 客户端。
|
||||
*
|
||||
* @param array $config 存储配置
|
||||
* @return CosClient COS 客户端
|
||||
*/
|
||||
private function client(array $config): CosClient
|
||||
{
|
||||
return new CosClient([
|
||||
@@ -149,13 +203,21 @@ class CosStorageDriver extends AbstractStorageDriver
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 COS 对象内容构造预览或下载响应。
|
||||
*
|
||||
* @param array $asset 文件资产数据
|
||||
* @param bool $attachment 是否下载附件
|
||||
* @return Response 响应对象
|
||||
* @throws ResourceNotFoundException
|
||||
*/
|
||||
private function responseFromObject(array $asset, bool $attachment): Response
|
||||
{
|
||||
$config = $this->storageConfigService->cosConfig();
|
||||
$bucket = trim((string) ($config['bucket'] ?? ''));
|
||||
$objectKey = (string) ($asset['object_key'] ?? '');
|
||||
if ($bucket === '' || $objectKey === '') {
|
||||
return response('文件不存在', 404);
|
||||
throw new ResourceNotFoundException('文件不存在');
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -171,7 +233,7 @@ class CosStorageDriver extends AbstractStorageDriver
|
||||
} elseif (is_object($result) && method_exists($result, '__toString')) {
|
||||
$body = (string) $result;
|
||||
} elseif (is_array($result)) {
|
||||
$body = (string) ($result['Body'] ?? $result['body'] ?? '');
|
||||
$body = (string) ($result['Body'] ?? $result['body'] ?? '');
|
||||
}
|
||||
|
||||
$mimeType = (string) ($asset['mime_type'] ?? 'application/octet-stream');
|
||||
@@ -182,7 +244,7 @@ class CosStorageDriver extends AbstractStorageDriver
|
||||
|
||||
return $this->bodyResponse($body, $mimeType);
|
||||
} catch (Throwable) {
|
||||
return response('文件不存在', 404);
|
||||
throw new ResourceNotFoundException('文件不存在');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,14 +8,29 @@ use support\Response;
|
||||
|
||||
/**
|
||||
* 本地文件存储驱动。
|
||||
*
|
||||
* 负责本地文件存储和响应构造。
|
||||
*/
|
||||
class LocalStorageDriver extends AbstractStorageDriver
|
||||
{
|
||||
/**
|
||||
* 获取本地存储引擎标识。
|
||||
*
|
||||
* @return int 存储引擎常量
|
||||
*/
|
||||
public function engine(): int
|
||||
{
|
||||
return FileConstant::STORAGE_LOCAL;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将临时文件写入本地存储目录。
|
||||
*
|
||||
* @param string $sourcePath 待上传文件路径
|
||||
* @param array $context 上传上下文,包含 object_key、visibility、public_url 等信息
|
||||
* @return array 上传后的资产数据
|
||||
* @throws BusinessStateException
|
||||
*/
|
||||
public function storeFromPath(string $sourcePath, array $context): array
|
||||
{
|
||||
if (!is_file($sourcePath)) {
|
||||
@@ -51,6 +66,13 @@ class LocalStorageDriver extends AbstractStorageDriver
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除本地文件。
|
||||
*
|
||||
* @param array $asset 文件资产数据
|
||||
* @return bool 是否删除成功
|
||||
* @throws BusinessStateException
|
||||
*/
|
||||
public function delete(array $asset): bool
|
||||
{
|
||||
$path = $this->resolveLocalAbsolutePath($asset);
|
||||
@@ -58,26 +80,43 @@ class LocalStorageDriver extends AbstractStorageDriver
|
||||
return true;
|
||||
}
|
||||
|
||||
return @unlink($path);
|
||||
if (@unlink($path)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new BusinessStateException('本地文件删除失败');
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造本地文件预览响应。
|
||||
*
|
||||
* @param array $asset 文件资产数据
|
||||
* @return Response 响应对象
|
||||
*/
|
||||
public function previewResponse(array $asset): Response
|
||||
{
|
||||
return $this->localPreviewResponse($asset);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造本地文件下载响应。
|
||||
*
|
||||
* @param array $asset 文件资产数据
|
||||
* @return Response 响应对象
|
||||
*/
|
||||
public function downloadResponse(array $asset): Response
|
||||
{
|
||||
return $this->localDownloadResponse($asset);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取本地公开访问地址。
|
||||
*
|
||||
* @param array $asset 文件资产数据
|
||||
* @return string 公共 URL
|
||||
*/
|
||||
public function publicUrl(array $asset): string
|
||||
{
|
||||
$url = trim((string) ($asset['url'] ?? $asset['public_url'] ?? ''));
|
||||
if ($url !== '') {
|
||||
return $url;
|
||||
}
|
||||
|
||||
$visibility = (int) ($asset['visibility'] ?? FileConstant::VISIBILITY_PRIVATE);
|
||||
if ($visibility !== FileConstant::VISIBILITY_PUBLIC) {
|
||||
return '';
|
||||
@@ -91,13 +130,14 @@ class LocalStorageDriver extends AbstractStorageDriver
|
||||
return $this->storageConfigService->buildLocalPublicUrl($objectKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取本地临时访问地址。
|
||||
*
|
||||
* @param array $asset 文件资产数据
|
||||
* @return string 临时 URL
|
||||
*/
|
||||
public function temporaryUrl(array $asset): string
|
||||
{
|
||||
$url = trim((string) ($asset['url'] ?? $asset['public_url'] ?? ''));
|
||||
if ($url !== '') {
|
||||
return $url;
|
||||
}
|
||||
|
||||
$visibility = (int) ($asset['visibility'] ?? FileConstant::VISIBILITY_PRIVATE);
|
||||
if ($visibility === FileConstant::VISIBILITY_PUBLIC) {
|
||||
return $this->publicUrl($asset);
|
||||
@@ -108,6 +148,12 @@ class LocalStorageDriver extends AbstractStorageDriver
|
||||
return $id > 0 ? '/adminapi/file-asset/' . $id . '/preview' : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保目标目录存在。
|
||||
*
|
||||
* @param string $directory 目录路径
|
||||
* @throws BusinessStateException
|
||||
*/
|
||||
private function ensureDirectory(string $directory): void
|
||||
{
|
||||
if (is_dir($directory)) {
|
||||
|
||||
@@ -4,20 +4,36 @@ namespace app\service\file\storage;
|
||||
|
||||
use app\common\constant\FileConstant;
|
||||
use app\exception\BusinessStateException;
|
||||
use app\exception\ResourceNotFoundException;
|
||||
use AlibabaCloud\Oss\V2 as Oss;
|
||||
use support\Response;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* 阿里云 OSS 文件存储驱动。
|
||||
*
|
||||
* 负责对象上传、删除、公开地址生成和预签名访问。
|
||||
*/
|
||||
class OssStorageDriver extends AbstractStorageDriver
|
||||
{
|
||||
/**
|
||||
* 获取 OSS 存储引擎标识。
|
||||
*
|
||||
* @return int 存储引擎常量
|
||||
*/
|
||||
public function engine(): int
|
||||
{
|
||||
return FileConstant::STORAGE_ALIYUN_OSS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将本地临时文件上传到 OSS。
|
||||
*
|
||||
* @param string $sourcePath 待上传文件路径
|
||||
* @param array $context 上传上下文,包含 object_key、visibility 等信息
|
||||
* @return array 上传后的资产数据
|
||||
* @throws BusinessStateException
|
||||
*/
|
||||
public function storeFromPath(string $sourcePath, array $context): array
|
||||
{
|
||||
if (!is_file($sourcePath)) {
|
||||
@@ -33,6 +49,9 @@ class OssStorageDriver extends AbstractStorageDriver
|
||||
|
||||
$client = $this->client($config);
|
||||
$objectKey = (string) ($context['object_key'] ?? '');
|
||||
$visibility = (int) ($context['visibility'] ?? FileConstant::VISIBILITY_PRIVATE);
|
||||
|
||||
/** @var Oss\Models\PutObjectRequest $request */
|
||||
$request = new Oss\Models\PutObjectRequest(
|
||||
bucket: (string) $config['bucket'],
|
||||
key: $objectKey
|
||||
@@ -42,23 +61,30 @@ class OssStorageDriver extends AbstractStorageDriver
|
||||
$client->putObject($request);
|
||||
|
||||
$publicUrl = $this->publicUrl([
|
||||
'visibility' => $visibility,
|
||||
'object_key' => $objectKey,
|
||||
]);
|
||||
$visibility = (int) ($context['visibility'] ?? FileConstant::VISIBILITY_PRIVATE);
|
||||
|
||||
return [
|
||||
'storage_engine' => $this->engine(),
|
||||
'object_key' => $objectKey,
|
||||
'url' => $visibility === FileConstant::VISIBILITY_PUBLIC ? $publicUrl : '',
|
||||
'public_url' => $publicUrl,
|
||||
'public_url' => $visibility === FileConstant::VISIBILITY_PUBLIC ? $publicUrl : '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 OSS 对象。
|
||||
*
|
||||
* @param array $asset 文件资产数据
|
||||
* @return bool 是否删除成功
|
||||
* @throws BusinessStateException
|
||||
*/
|
||||
public function delete(array $asset): bool
|
||||
{
|
||||
$config = $this->storageConfigService->ossConfig();
|
||||
if (trim((string) ($config['bucket'] ?? '')) === '') {
|
||||
return false;
|
||||
throw new BusinessStateException('阿里云 OSS 存储配置未完整');
|
||||
}
|
||||
|
||||
$objectKey = (string) ($asset['object_key'] ?? '');
|
||||
@@ -67,6 +93,8 @@ class OssStorageDriver extends AbstractStorageDriver
|
||||
}
|
||||
|
||||
$client = $this->client($config);
|
||||
|
||||
/** @var Oss\Models\DeleteObjectRequest $request */
|
||||
$request = new Oss\Models\DeleteObjectRequest(
|
||||
bucket: (string) $config['bucket'],
|
||||
key: $objectKey
|
||||
@@ -76,23 +104,41 @@ class OssStorageDriver extends AbstractStorageDriver
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造 OSS 文件预览响应。
|
||||
*
|
||||
* @param array $asset 文件资产数据
|
||||
* @return Response 响应对象
|
||||
*/
|
||||
public function previewResponse(array $asset): Response
|
||||
{
|
||||
$url = $this->publicUrl($asset);
|
||||
if ($url !== '') {
|
||||
return redirect($url);
|
||||
}
|
||||
|
||||
return $this->responseFromObject($asset, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造 OSS 文件下载响应。
|
||||
*
|
||||
* @param array $asset 文件资产数据
|
||||
* @return Response 响应对象
|
||||
*/
|
||||
public function downloadResponse(array $asset): Response
|
||||
{
|
||||
return $this->responseFromObject($asset, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 OSS 公开访问地址。
|
||||
*
|
||||
* @param array $asset 文件资产数据
|
||||
* @return string 公共 URL
|
||||
*/
|
||||
public function publicUrl(array $asset): string
|
||||
{
|
||||
$visibility = (int) ($asset['visibility'] ?? FileConstant::VISIBILITY_PRIVATE);
|
||||
if ($visibility !== FileConstant::VISIBILITY_PUBLIC) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$publicUrl = trim((string) ($asset['url'] ?? $asset['public_url'] ?? ''));
|
||||
if ($publicUrl !== '') {
|
||||
return $publicUrl;
|
||||
@@ -120,11 +166,17 @@ class OssStorageDriver extends AbstractStorageDriver
|
||||
return 'https://' . $bucket . '.' . ltrim($endpoint, '/') . '/' . ltrim($objectKey, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 OSS 预签名访问地址。
|
||||
*
|
||||
* @param array $asset 文件资产数据
|
||||
* @return string 临时 URL
|
||||
*/
|
||||
public function temporaryUrl(array $asset): string
|
||||
{
|
||||
$config = $this->storageConfigService->ossConfig();
|
||||
if (trim((string) ($config['bucket'] ?? '')) === '' || trim((string) ($config['region'] ?? '')) === '') {
|
||||
return $this->publicUrl($asset);
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -134,6 +186,7 @@ class OssStorageDriver extends AbstractStorageDriver
|
||||
return '';
|
||||
}
|
||||
|
||||
/** @var Oss\Models\GetObjectRequest $request */
|
||||
$request = new Oss\Models\GetObjectRequest(
|
||||
bucket: (string) $config['bucket'],
|
||||
key: $objectKey
|
||||
@@ -142,12 +195,19 @@ class OssStorageDriver extends AbstractStorageDriver
|
||||
|
||||
return (string) ($result->url ?? '');
|
||||
} catch (Throwable) {
|
||||
return $this->publicUrl($asset);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 OSS 客户端。
|
||||
*
|
||||
* @param array $config 存储配置
|
||||
* @return Oss\Client OSS 客户端
|
||||
*/
|
||||
private function client(array $config): Oss\Client
|
||||
{
|
||||
/** @var Oss\Credentials\StaticCredentialsProvider $provider */
|
||||
$provider = new Oss\Credentials\StaticCredentialsProvider(
|
||||
accessKeyId: (string) $config['access_key_id'],
|
||||
accessKeySecret: (string) $config['access_key_secret']
|
||||
@@ -165,17 +225,26 @@ class OssStorageDriver extends AbstractStorageDriver
|
||||
return new Oss\Client($cfg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 OSS 对象内容构造预览或下载响应。
|
||||
*
|
||||
* @param array $asset 文件资产数据
|
||||
* @param bool $attachment 是否下载附件
|
||||
* @return Response 响应对象
|
||||
* @throws ResourceNotFoundException
|
||||
*/
|
||||
private function responseFromObject(array $asset, bool $attachment): Response
|
||||
{
|
||||
$config = $this->storageConfigService->ossConfig();
|
||||
$bucket = trim((string) ($config['bucket'] ?? ''));
|
||||
$objectKey = (string) ($asset['object_key'] ?? '');
|
||||
if ($bucket === '' || $objectKey === '') {
|
||||
return response('文件不存在', 404);
|
||||
throw new ResourceNotFoundException('文件不存在');
|
||||
}
|
||||
|
||||
try {
|
||||
$client = $this->client($config);
|
||||
/** @var Oss\Models\GetObjectRequest $request */
|
||||
$request = new Oss\Models\GetObjectRequest(
|
||||
bucket: $bucket,
|
||||
key: $objectKey
|
||||
@@ -190,7 +259,7 @@ class OssStorageDriver extends AbstractStorageDriver
|
||||
|
||||
return $this->bodyResponse($body, $mimeType);
|
||||
} catch (Throwable) {
|
||||
return response('文件不存在', 404);
|
||||
throw new ResourceNotFoundException('文件不存在');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,48 +4,95 @@ namespace app\service\file\storage;
|
||||
|
||||
use app\common\constant\FileConstant;
|
||||
use app\exception\BusinessStateException;
|
||||
use app\exception\ResourceNotFoundException;
|
||||
use support\Response;
|
||||
|
||||
/**
|
||||
* 远程引用驱动。
|
||||
* 远程引用文件存储驱动。
|
||||
*
|
||||
* 仅保存原始远程 URL,不做本地落盘或对象存储复制。
|
||||
*/
|
||||
class RemoteUrlStorageDriver extends AbstractStorageDriver
|
||||
{
|
||||
/**
|
||||
* 获取远程引用引擎标识。
|
||||
*
|
||||
* @return int 存储引擎常量
|
||||
*/
|
||||
public function engine(): int
|
||||
{
|
||||
return FileConstant::STORAGE_REMOTE_URL;
|
||||
}
|
||||
|
||||
/**
|
||||
* 远程引用模式不支持直接上传。
|
||||
*
|
||||
* @param string $sourcePath 待上传文件路径
|
||||
* @param array $context 上传上下文
|
||||
* @return array 上传后的资产数据
|
||||
* @throws BusinessStateException
|
||||
*/
|
||||
public function storeFromPath(string $sourcePath, array $context): array
|
||||
{
|
||||
throw new BusinessStateException('远程引用模式不支持直接上传,请先下载后再入库');
|
||||
}
|
||||
|
||||
/**
|
||||
* 远程引用模式不需要真正删除对象。
|
||||
*
|
||||
* @param array $asset 文件资产数据
|
||||
* @return bool 是否删除成功
|
||||
*/
|
||||
public function delete(array $asset): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接跳转到源站地址进行预览。
|
||||
*
|
||||
* @param array $asset 文件资产数据
|
||||
* @return Response 响应对象
|
||||
* @throws ResourceNotFoundException
|
||||
*/
|
||||
public function previewResponse(array $asset): Response
|
||||
{
|
||||
$url = (string) ($asset['source_url'] ?? $asset['url'] ?? '');
|
||||
if ($url === '') {
|
||||
return response('文件不存在', 404);
|
||||
throw new ResourceNotFoundException('文件不存在');
|
||||
}
|
||||
|
||||
return redirect($url);
|
||||
}
|
||||
|
||||
/**
|
||||
* 远程引用文件的下载行为与预览保持一致。
|
||||
*
|
||||
* @param array $asset 文件资产数据
|
||||
* @return Response 响应对象
|
||||
*/
|
||||
public function downloadResponse(array $asset): Response
|
||||
{
|
||||
return $this->previewResponse($asset);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取原始远程地址。
|
||||
*
|
||||
* @param array $asset 文件资产数据
|
||||
* @return string 远程 URL
|
||||
*/
|
||||
public function publicUrl(array $asset): string
|
||||
{
|
||||
return (string) ($asset['source_url'] ?? $asset['url'] ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取原始远程地址。
|
||||
*
|
||||
* @param array $asset 文件资产数据
|
||||
* @return string 远程 URL
|
||||
*/
|
||||
public function temporaryUrl(array $asset): string
|
||||
{
|
||||
return (string) ($asset['source_url'] ?? $asset['url'] ?? '');
|
||||
|
||||
@@ -6,20 +6,64 @@ use support\Response;
|
||||
|
||||
/**
|
||||
* 文件存储驱动接口。
|
||||
*
|
||||
* 统一定义文件存储驱动能力。
|
||||
*/
|
||||
interface StorageDriverInterface
|
||||
{
|
||||
/**
|
||||
* 获取存储引擎标识。
|
||||
*
|
||||
* @return int 存储引擎常量
|
||||
*/
|
||||
public function engine(): int;
|
||||
|
||||
/**
|
||||
* 将本地临时文件写入存储后端。
|
||||
*
|
||||
* @param string $sourcePath 待上传的本地临时文件路径
|
||||
* @param array $context 上传上下文,通常包含 object_key、visibility 等信息
|
||||
* @return array 上传后的资产数据
|
||||
*/
|
||||
public function storeFromPath(string $sourcePath, array $context): array;
|
||||
|
||||
/**
|
||||
* 删除指定文件资产。
|
||||
*
|
||||
* @param array $asset 文件资产数据
|
||||
* @return bool 是否删除成功
|
||||
*/
|
||||
public function delete(array $asset): bool;
|
||||
|
||||
/**
|
||||
* 构造文件预览响应。
|
||||
*
|
||||
* @param array $asset 文件资产数据
|
||||
* @return Response 响应对象
|
||||
*/
|
||||
public function previewResponse(array $asset): Response;
|
||||
|
||||
/**
|
||||
* 构造文件下载响应。
|
||||
*
|
||||
* @param array $asset 文件资产数据
|
||||
* @return Response 响应对象
|
||||
*/
|
||||
public function downloadResponse(array $asset): Response;
|
||||
|
||||
/**
|
||||
* 获取公开访问地址。
|
||||
*
|
||||
* @param array $asset 文件资产数据
|
||||
* @return string 公开 URL
|
||||
*/
|
||||
public function publicUrl(array $asset): string;
|
||||
|
||||
/**
|
||||
* 获取临时访问地址。
|
||||
*
|
||||
* @param array $asset 文件资产数据
|
||||
* @return string 临时 URL
|
||||
*/
|
||||
public function temporaryUrl(array $asset): string;
|
||||
}
|
||||
|
||||
@@ -8,9 +8,27 @@ use support\Response;
|
||||
|
||||
/**
|
||||
* 文件存储驱动管理器。
|
||||
*
|
||||
* 负责根据存储引擎分发文件操作。
|
||||
*
|
||||
* @property StorageConfigService $storageConfigService 存储配置服务
|
||||
* @property LocalStorageDriver $localStorageDriver 本地存储驱动
|
||||
* @property OssStorageDriver $ossStorageDriver oss存储驱动
|
||||
* @property CosStorageDriver $cosStorageDriver cos存储驱动
|
||||
* @property RemoteUrlStorageDriver $remoteUrlStorageDriver remoteUrl存储驱动
|
||||
*/
|
||||
class StorageManager
|
||||
{
|
||||
/**
|
||||
* 构造方法。
|
||||
*
|
||||
* @param StorageConfigService $storageConfigService 存储配置服务
|
||||
* @param LocalStorageDriver $localStorageDriver 本地存储驱动
|
||||
* @param OssStorageDriver $ossStorageDriver oss存储驱动
|
||||
* @param CosStorageDriver $cosStorageDriver cos存储驱动
|
||||
* @param RemoteUrlStorageDriver $remoteUrlStorageDriver remoteUrl存储驱动
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(
|
||||
protected StorageConfigService $storageConfigService,
|
||||
protected LocalStorageDriver $localStorageDriver,
|
||||
@@ -20,6 +38,18 @@ class StorageManager
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建存储上下文。
|
||||
*
|
||||
* @param string $sourcePath 源文件路径
|
||||
* @param string $originalName 原始文件名
|
||||
* @param int|null $scene 场景
|
||||
* @param int|null $visibility 可见性
|
||||
* @param int|null $engine 存储引擎
|
||||
* @param string|null $sourceUrl 源地址
|
||||
* @param string $sourceType 来源类型
|
||||
* @return array 上下文数据
|
||||
*/
|
||||
public function buildContext(
|
||||
string $sourcePath,
|
||||
string $originalName,
|
||||
@@ -58,6 +88,18 @@ class StorageManager
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文件路径保存文件。
|
||||
*
|
||||
* @param string $sourcePath 源文件路径
|
||||
* @param string $originalName 原始文件名
|
||||
* @param int|null $scene 场景
|
||||
* @param int|null $visibility 可见性
|
||||
* @param int|null $engine 存储引擎
|
||||
* @param string|null $sourceUrl 源地址
|
||||
* @param string $sourceType 来源类型
|
||||
* @return array 保存结果
|
||||
*/
|
||||
public function storeFromPath(
|
||||
string $sourcePath,
|
||||
string $originalName,
|
||||
@@ -73,36 +115,72 @@ class StorageManager
|
||||
return array_merge($context, $driver->storeFromPath($sourcePath, $context));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除存储对象。
|
||||
*
|
||||
* @param array $asset 文件记录
|
||||
* @return bool 是否删除成功
|
||||
*/
|
||||
public function delete(array $asset): bool
|
||||
{
|
||||
return $this->resolveDriver((int) ($asset['storage_engine'] ?? FileConstant::STORAGE_LOCAL))
|
||||
->delete($asset);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预览响应。
|
||||
*
|
||||
* @param array $asset 文件记录
|
||||
* @return Response 响应对象
|
||||
*/
|
||||
public function previewResponse(array $asset): Response
|
||||
{
|
||||
return $this->resolveDriver((int) ($asset['storage_engine'] ?? FileConstant::STORAGE_LOCAL))
|
||||
->previewResponse($asset);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下载响应。
|
||||
*
|
||||
* @param array $asset 文件记录
|
||||
* @return Response 响应对象
|
||||
*/
|
||||
public function downloadResponse(array $asset): Response
|
||||
{
|
||||
return $this->resolveDriver((int) ($asset['storage_engine'] ?? FileConstant::STORAGE_LOCAL))
|
||||
->downloadResponse($asset);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取公开访问 URL。
|
||||
*
|
||||
* @param array $asset 文件记录
|
||||
* @return string 访问 URL
|
||||
*/
|
||||
public function publicUrl(array $asset): string
|
||||
{
|
||||
return $this->resolveDriver((int) ($asset['storage_engine'] ?? FileConstant::STORAGE_LOCAL))
|
||||
->publicUrl($asset);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取临时访问 URL。
|
||||
*
|
||||
* @param array $asset 文件记录
|
||||
* @return string 访问 URL
|
||||
*/
|
||||
public function temporaryUrl(array $asset): string
|
||||
{
|
||||
return $this->resolveDriver((int) ($asset['storage_engine'] ?? FileConstant::STORAGE_LOCAL))
|
||||
->temporaryUrl($asset);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析对应的存储驱动。
|
||||
*
|
||||
* @param int $engine 存储引擎
|
||||
* @return StorageDriverInterface 存储驱动
|
||||
*/
|
||||
public function resolveDriver(int $engine): StorageDriverInterface
|
||||
{
|
||||
return match ($engine) {
|
||||
@@ -114,6 +192,14 @@ class StorageManager
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 按存储引擎构建公开访问 URL。
|
||||
*
|
||||
* @param int $engine 存储引擎
|
||||
* @param int $visibility 可见性
|
||||
* @param string $objectKey 对象键
|
||||
* @return string 访问 URL
|
||||
*/
|
||||
private function buildPublicUrlByEngine(int $engine, int $visibility, string $objectKey): string
|
||||
{
|
||||
if ($engine === FileConstant::STORAGE_LOCAL && $visibility === FileConstant::VISIBILITY_PUBLIC) {
|
||||
@@ -123,6 +209,13 @@ class StorageManager
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 估算 MIME 类型。
|
||||
*
|
||||
* @param string $sourcePath 源文件路径
|
||||
* @param string $originalName 原始文件名
|
||||
* @return string MIME 类型
|
||||
*/
|
||||
private function guessMimeType(string $sourcePath, string $originalName): string
|
||||
{
|
||||
$mimeType = '';
|
||||
@@ -156,3 +249,5 @@ class StorageManager
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user