AI代码生成工具
This commit is contained in:
@@ -3,87 +3,72 @@
|
||||
namespace App\Service\common;
|
||||
|
||||
use App\BaseApp\BaseService;
|
||||
use App\Models\ProjectModel;
|
||||
use App\Models\RoleModel;
|
||||
use App\Models\AdminModel;
|
||||
use App\Service\common\upload\LocalhostStorageService;
|
||||
use App\Service\common\upload\QiniuStorageService;
|
||||
use App\Service\common\oss\OssRuntimeConfigService;
|
||||
use App\Service\common\oss\OssStorageFactory;
|
||||
use App\Service\common\oss\OssStorageInterface;
|
||||
use App\Service\FileService;
|
||||
use Exception;
|
||||
use Illuminate\Support\Str;
|
||||
use Psr\Container\ContainerExceptionInterface;
|
||||
use Psr\Container\NotFoundExceptionInterface;
|
||||
|
||||
/**
|
||||
* 统一上传入口:按数据库启用的 OSS 配置,经工厂分发到对应存储实现
|
||||
*/
|
||||
class UploadService extends BaseService
|
||||
{
|
||||
private $uploadService;
|
||||
private OssStorageInterface $uploadService;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->model = RoleModel::class;
|
||||
|
||||
switch (env('ECS')) {
|
||||
case 'aliyun':
|
||||
// $this->uploadService = AliyunStorageService::getInstance();
|
||||
break;
|
||||
case 'qcloud':
|
||||
// $this->uploadService = QcloudStorageService::getInstance();
|
||||
break;
|
||||
case 'qiniu':
|
||||
$this->uploadService = QiniuStorageService::getInstance();
|
||||
break;
|
||||
default:
|
||||
$this->uploadService = LocalhostStorageService::getInstance();
|
||||
break;
|
||||
try {
|
||||
$config = OssRuntimeConfigService::getInstance()->getActiveConfig();
|
||||
$this->uploadService = OssStorageFactory::getInstance()->make($config);
|
||||
} catch (\Throwable $e) {
|
||||
// 配置异常时回退本地,避免整站上传不可用
|
||||
$this->uploadService = OssStorageFactory::getInstance()->make([
|
||||
'driver' => 'local',
|
||||
'path_prefix' => 'uploads',
|
||||
'domain' => '',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传OSS图片
|
||||
* @param $file
|
||||
* @return array|bool
|
||||
* @throws Exception
|
||||
* 上传图片到当前启用的存储
|
||||
*
|
||||
* @param mixed $file 上传文件对象
|
||||
*/
|
||||
public function uploadImage($file): array|bool
|
||||
{
|
||||
// 获取文件后缀
|
||||
$ext = $file->getClientOriginalExtension();
|
||||
$key = 'spa/image/'. date('Ymd') . '/' . 'cc_upload_'. Str::random(). uniqid() . '.' . $ext;
|
||||
$key = 'spa/image/' . date('Ymd') . '/' . 'cc_upload_' . Str::random() . uniqid() . '.' . $ext;
|
||||
$result = $this->uploadService->uploadVideo($file, $key);
|
||||
// $result = [
|
||||
// 'key' => $key,
|
||||
// 'url' => 'https://img2.baidu.com/it/u=1629222614,1358629025&fm=253&fmt=auto&app=138&f=JPEG?w=889&h=500&a='. rand(1111111, 9999999)
|
||||
// ];
|
||||
if (!$result) {
|
||||
$this->utils->errorThrow('图片上传失败');
|
||||
}
|
||||
FileService::getInstance()->create([
|
||||
'user_id' => $this->userId,
|
||||
'url' => $result['url'],
|
||||
]);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传OSS视频
|
||||
* @param $file
|
||||
* @return array|bool
|
||||
* @throws Exception
|
||||
* 上传视频到当前启用的存储
|
||||
*
|
||||
* @param mixed $file 上传文件对象
|
||||
*/
|
||||
public function uploadVideo($file): array|bool
|
||||
{
|
||||
// 获取文件后缀
|
||||
$ext = $file->getClientOriginalExtension();
|
||||
$key = 'spa/video/'. date('Ymd') . '/' . 'cc_upload_'. Str::random(). uniqid() . '.' . $ext;
|
||||
$key = 'spa/video/' . date('Ymd') . '/' . 'cc_upload_' . Str::random() . uniqid() . '.' . $ext;
|
||||
$result = $this->uploadService->uploadVideo($file, $key);
|
||||
// $result = [
|
||||
// 'key' => $key,
|
||||
// 'url' => 'http://d-jy.nailaoyun.cn//storage/video//20250327/2cc1abf9bd69410ce7c69660daf54260.mp4'
|
||||
// ];
|
||||
if (!$result) {
|
||||
$this->utils->errorThrow('视频上传失败');
|
||||
}
|
||||
FileService::getInstance()->create([
|
||||
'user_id' => $this->userId,
|
||||
'url' => $result['url'],
|
||||
]);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
57
app/Service/common/UserAgentService.php
Normal file
57
app/Service/common/UserAgentService.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\common;
|
||||
|
||||
use App\BaseApp\BaseNotAuthService;
|
||||
|
||||
/**
|
||||
* User-Agent 解析:登录日志 / 操作日志共用
|
||||
*/
|
||||
class UserAgentService extends BaseNotAuthService
|
||||
{
|
||||
/**
|
||||
* 从 UA 推断操作系统名称
|
||||
* 为什么允许空参:中间件/登录处直接取当前请求头,避免到处传 UA
|
||||
*
|
||||
* @param string|null $ua 为空时取当前请求 User-Agent
|
||||
*/
|
||||
public function parseEquipment(?string $ua = null): string
|
||||
{
|
||||
$ua = $ua ?? (string) request()->header('User-Agent', '');
|
||||
if ($ua === '') {
|
||||
return '未知';
|
||||
}
|
||||
return match (true) {
|
||||
str_contains($ua, 'Windows NT 10') => 'Windows 10/11',
|
||||
str_contains($ua, 'Windows') => 'Windows',
|
||||
str_contains($ua, 'Macintosh') || str_contains($ua, 'Mac OS') => 'macOS',
|
||||
str_contains($ua, 'Android') => 'Android',
|
||||
str_contains($ua, 'iPhone') || str_contains($ua, 'iPad') => 'iOS',
|
||||
str_contains($ua, 'Linux') => 'Linux',
|
||||
default => '其他',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 UA 推断浏览器名称
|
||||
* Edge 需先于 Chrome 匹配,避免被 Chrome/ 抢先
|
||||
*
|
||||
* @param string|null $ua 为空时取当前请求 User-Agent
|
||||
*/
|
||||
public function parseBrowser(?string $ua = null): string
|
||||
{
|
||||
$ua = $ua ?? (string) request()->header('User-Agent', '');
|
||||
if ($ua === '') {
|
||||
return '未知';
|
||||
}
|
||||
return match (true) {
|
||||
str_contains($ua, 'Edg/') => 'Edge',
|
||||
str_contains($ua, 'OPR/') || str_contains($ua, 'Opera') => 'Opera',
|
||||
str_contains($ua, 'Chrome/') && !str_contains($ua, 'Edg/') => 'Chrome',
|
||||
str_contains($ua, 'Firefox/') => 'Firefox',
|
||||
str_contains($ua, 'Safari/') && !str_contains($ua, 'Chrome/') => 'Safari',
|
||||
str_contains($ua, 'MSIE') || str_contains($ua, 'Trident/') => 'IE',
|
||||
default => '其他',
|
||||
};
|
||||
}
|
||||
}
|
||||
97
app/Service/common/oss/OssRuntimeConfigService.php
Normal file
97
app/Service/common/oss/OssRuntimeConfigService.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\common\oss;
|
||||
|
||||
use App\BaseApp\BaseService;
|
||||
use App\Models\oss\OssConfigModel;
|
||||
use App\Service\common\FieldEncryptService;
|
||||
use App\Service\SystemConfigService;
|
||||
|
||||
/**
|
||||
* OSS 运行时配置:解析当前启用的存储配置并解密密钥,供上传工厂使用
|
||||
*/
|
||||
class OssRuntimeConfigService extends BaseService
|
||||
{
|
||||
public const CFG_ACTIVE_ID = 'oss_active_config_id';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// 上传链路可能在无登录上下文触发,不强制鉴权
|
||||
$this->isAuth = false;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前启用的明文配置
|
||||
* 为什么要解密:上传客户端需要明文 access/secret;库内仅存 AES 密文
|
||||
*
|
||||
* @return array{
|
||||
* id:int,driver:string,name:string,access_key:string,secret_key:string,
|
||||
* endpoint:string,region:string,bucket:string,domain:string,
|
||||
* path_prefix:string,extra_json:mixed
|
||||
* }
|
||||
*/
|
||||
public function getActiveConfig(): array
|
||||
{
|
||||
$sys = SystemConfigService::getInstance();
|
||||
$activeId = (int) $sys->getValue(self::CFG_ACTIVE_ID, '0');
|
||||
$row = null;
|
||||
if ($activeId > 0) {
|
||||
$row = OssConfigModel::where('id', $activeId)
|
||||
->where('deleted_at', 0)
|
||||
->where('status', 1)
|
||||
->first();
|
||||
}
|
||||
if (!$row) {
|
||||
// 回落 is_active 标记,避免仅写了表未写 system_config 时上传失败
|
||||
$row = OssConfigModel::where('is_active', 1)
|
||||
->where('deleted_at', 0)
|
||||
->where('status', 1)
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
}
|
||||
if (!$row) {
|
||||
// 最终回落本地默认行或内存默认
|
||||
$row = OssConfigModel::where('driver', 'local')
|
||||
->where('deleted_at', 0)
|
||||
->where('status', 1)
|
||||
->orderByDesc('is_active')
|
||||
->orderBy('id')
|
||||
->first();
|
||||
}
|
||||
if (!$row) {
|
||||
return [
|
||||
'id' => 0,
|
||||
'driver' => 'local',
|
||||
'name' => '本地存储',
|
||||
'access_key' => '',
|
||||
'secret_key' => '',
|
||||
'endpoint' => '',
|
||||
'region' => '',
|
||||
'bucket' => '',
|
||||
'domain' => '',
|
||||
'path_prefix' => 'uploads',
|
||||
'extra_json' => null,
|
||||
];
|
||||
}
|
||||
$enc = FieldEncryptService::getInstance();
|
||||
$extra = $row->extra_json;
|
||||
if (is_string($extra) && $extra !== '') {
|
||||
$decoded = json_decode($extra, true);
|
||||
$extra = is_array($decoded) ? $decoded : $extra;
|
||||
}
|
||||
return [
|
||||
'id' => (int) $row->id,
|
||||
'driver' => (string) $row->driver,
|
||||
'name' => (string) $row->name,
|
||||
'access_key' => $enc->decryptFromStorage((string) ($row->access_key ?? ''), true),
|
||||
'secret_key' => $enc->decryptFromStorage((string) ($row->secret_key ?? ''), true),
|
||||
'endpoint' => (string) ($row->endpoint ?? ''),
|
||||
'region' => (string) ($row->region ?? ''),
|
||||
'bucket' => (string) ($row->bucket ?? ''),
|
||||
'domain' => (string) ($row->domain ?? ''),
|
||||
'path_prefix' => (string) ($row->path_prefix ?? ''),
|
||||
'extra_json' => $extra,
|
||||
];
|
||||
}
|
||||
}
|
||||
47
app/Service/common/oss/OssStorageFactory.php
Normal file
47
app/Service/common/oss/OssStorageFactory.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\common\oss;
|
||||
|
||||
use App\BaseApp\BaseNotAuthService;
|
||||
use App\Enum\OssDriverEnum;
|
||||
use App\Service\common\upload\AliyunStorageService;
|
||||
use App\Service\common\upload\LocalhostStorageService;
|
||||
use App\Service\common\upload\QcloudStorageService;
|
||||
use App\Service\common\upload\QiniuStorageService;
|
||||
use App\Service\common\upload\S3CompatibleStorageService;
|
||||
use App\Service\common\UtilsService;
|
||||
|
||||
/**
|
||||
* OSS 存储工厂:按 OssDriverEnum 分发到对应实现(工厂 + 策略)
|
||||
* 覆盖:local / aliyun / qcloud / qiniu / huawei / aws / minio / baidu
|
||||
*/
|
||||
class OssStorageFactory extends BaseNotAuthService
|
||||
{
|
||||
/**
|
||||
* 根据明文运行时配置构建上传客户端
|
||||
* 为什么集中在工厂:UploadService 不关心具体 SDK/签名差异,只拿接口用
|
||||
*
|
||||
* @param array $config getActiveConfig() 返回值(含 driver)
|
||||
*/
|
||||
public function make(array $config): OssStorageInterface
|
||||
{
|
||||
$driverCode = strtolower(trim((string) ($config['driver'] ?? OssDriverEnum::Local->value)));
|
||||
$driver = OssDriverEnum::tryFrom($driverCode);
|
||||
if ($driver === null) {
|
||||
UtilsService::getInstance()->errorThrow(
|
||||
'不支持的存储驱动:' . $driverCode . ',请改用本地存储或完善配置'
|
||||
);
|
||||
}
|
||||
// 按驱动枚举创建策略实现;S3 兼容族共用一套 SigV4 客户端
|
||||
return match ($driver) {
|
||||
OssDriverEnum::Local => LocalhostStorageService::getInstance()->withConfig($config),
|
||||
OssDriverEnum::Aliyun => AliyunStorageService::getInstance()->withConfig($config),
|
||||
OssDriverEnum::Qcloud => QcloudStorageService::getInstance()->withConfig($config),
|
||||
OssDriverEnum::Qiniu => QiniuStorageService::getInstance()->withConfig($config),
|
||||
OssDriverEnum::Huawei,
|
||||
OssDriverEnum::Aws,
|
||||
OssDriverEnum::Minio,
|
||||
OssDriverEnum::Baidu => S3CompatibleStorageService::getInstance()->withConfig($config),
|
||||
};
|
||||
}
|
||||
}
|
||||
34
app/Service/common/oss/OssStorageInterface.php
Normal file
34
app/Service/common/oss/OssStorageInterface.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\common\oss;
|
||||
|
||||
/**
|
||||
* OSS 存储驱动统一契约:工厂按 driver 返回实现类,UploadService 只依赖本接口
|
||||
*/
|
||||
interface OssStorageInterface
|
||||
{
|
||||
/**
|
||||
* 注入本请求的运行时配置(密钥已解密)
|
||||
*
|
||||
* @param array $config OssRuntimeConfigService::getActiveConfig()
|
||||
*/
|
||||
public function withConfig(array $config): static;
|
||||
|
||||
/**
|
||||
* 上传图片对象
|
||||
*
|
||||
* @param mixed $filePath 本地路径或 UploadedFile
|
||||
* @param string $key 对象键
|
||||
* @return array{key:string,url:string}|false
|
||||
*/
|
||||
public function uploadImage($filePath, string $key): bool|array;
|
||||
|
||||
/**
|
||||
* 上传视频/通用文件对象
|
||||
*
|
||||
* @param mixed $filePath 本地路径或 UploadedFile
|
||||
* @param string $key 对象键
|
||||
* @return array{key:string,url:string}|false
|
||||
*/
|
||||
public function uploadVideo($filePath, string $key): bool|array;
|
||||
}
|
||||
76
app/Service/common/upload/AliyunStorageService.php
Normal file
76
app/Service/common/upload/AliyunStorageService.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\common\upload;
|
||||
|
||||
use App\BaseApp\BaseNotAuthService;
|
||||
use App\Service\common\oss\OssStorageInterface;
|
||||
use App\Service\common\UtilsService;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
/**
|
||||
* 阿里云 OSS:REST PutObject(Authorization 签名)
|
||||
*/
|
||||
class AliyunStorageService extends BaseNotAuthService implements OssStorageInterface
|
||||
{
|
||||
protected array $config = [];
|
||||
|
||||
public function withConfig(array $config): static
|
||||
{
|
||||
$this->config = $config;
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 OSS V1 签名上传对象
|
||||
*/
|
||||
private function uploadFile($filePath, string $key): bool|array
|
||||
{
|
||||
$accessKey = (string) ($this->config['access_key'] ?? '');
|
||||
$secretKey = (string) ($this->config['secret_key'] ?? '');
|
||||
$bucket = (string) ($this->config['bucket'] ?? '');
|
||||
$endpoint = (string) ($this->config['endpoint'] ?? '');
|
||||
$domain = rtrim((string) ($this->config['domain'] ?? ''), '/');
|
||||
if ($accessKey === '' || $secretKey === '' || $bucket === '' || $endpoint === '') {
|
||||
UtilsService::getInstance()->errorThrow('阿里云 OSS 配置不完整(需要 AccessKey/Secret/Bucket/Endpoint)');
|
||||
}
|
||||
$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;
|
||||
$content = file_get_contents($path);
|
||||
$contentType = 'application/octet-stream';
|
||||
$date = gmdate('D, d M Y H:i:s \G\M\T');
|
||||
$resource = '/' . $bucket . '/' . $key;
|
||||
$stringToSign = "PUT\n\n{$contentType}\n{$date}\n{$resource}";
|
||||
$signature = base64_encode(hash_hmac('sha1', $stringToSign, $secretKey, true));
|
||||
$host = preg_replace('#^https?://#', '', rtrim($endpoint, '/'));
|
||||
// 支持传入 oss-cn-xxx.aliyuncs.com 或带 bucket 的域名
|
||||
if (!str_starts_with($host, $bucket . '.')) {
|
||||
$host = $bucket . '.' . $host;
|
||||
}
|
||||
$url = 'https://' . $host . '/' . $key;
|
||||
$response = Http::withHeaders([
|
||||
'Date' => $date,
|
||||
'Content-Type' => $contentType,
|
||||
'Authorization' => 'OSS ' . $accessKey . ':' . $signature,
|
||||
])->withBody($content, $contentType)->put($url);
|
||||
if (!$response->successful()) {
|
||||
UtilsService::getInstance()->errorThrow('阿里云上传失败:' . $response->body());
|
||||
}
|
||||
$publicUrl = $domain !== '' ? ($domain . '/' . $key) : $url;
|
||||
return ['key' => $key, 'url' => $publicUrl];
|
||||
}
|
||||
}
|
||||
@@ -2,105 +2,72 @@
|
||||
|
||||
namespace App\Service\common\upload;
|
||||
|
||||
use App\BaseApp\BaseService;
|
||||
use App\BaseApp\BaseNotAuthService;
|
||||
use App\Service\common\oss\OssStorageInterface;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class LocalhostStorageService extends BaseService
|
||||
/**
|
||||
* 本地磁盘上传;支持 domain / path_prefix 覆盖
|
||||
*/
|
||||
class LocalhostStorageService extends BaseNotAuthService implements OssStorageInterface
|
||||
{
|
||||
/**
|
||||
* @var mixed 单例实例,确保该类只有一个全局实例
|
||||
*/
|
||||
protected static mixed $_instance;
|
||||
protected array $config = [
|
||||
'domain' => '',
|
||||
'path_prefix' => '',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
/**
|
||||
* 注入运行时配置(链式)
|
||||
*/
|
||||
public function withConfig(array $config): static
|
||||
{
|
||||
parent::__construct();
|
||||
$this->config = array_merge($this->config, $config);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取实例
|
||||
* @return null|static
|
||||
*/
|
||||
public static function getInstance(): null|static
|
||||
{
|
||||
$name = get_called_class();
|
||||
if (!isset(self::$_instance[$name])) {
|
||||
self::$_instance[$name] = new static();
|
||||
}
|
||||
|
||||
return self::$_instance[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传图片
|
||||
*
|
||||
* @param string $filePath 文件本地路径
|
||||
* @param string $key 上传到本地存储的文件名
|
||||
* @return array|bool
|
||||
*/
|
||||
public function uploadImage($filePath, $key): bool|array
|
||||
public function uploadImage($filePath, string $key): bool|array
|
||||
{
|
||||
return $this->uploadFile($filePath, $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传视频
|
||||
*
|
||||
* @param string $filePath 文件本地路径
|
||||
* @param string $key 上传到本地存储的文件名
|
||||
* @return array|bool
|
||||
*/
|
||||
public function uploadVideo($filePath, $key): bool|array
|
||||
public function uploadVideo($filePath, string $key): bool|array
|
||||
{
|
||||
return $this->uploadFile($filePath, $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除图片
|
||||
*
|
||||
* @param string $key 本地存储的文件名
|
||||
* @return bool
|
||||
*/
|
||||
public function deleteImage($key): bool
|
||||
{
|
||||
return $this->deleteFile($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除视频
|
||||
*
|
||||
* @param string $key 本地存储的文件名
|
||||
* @return bool
|
||||
*/
|
||||
public function deleteVideo($key): bool
|
||||
{
|
||||
return $this->deleteFile($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
*
|
||||
* @param string $filePath 文件本地路径
|
||||
* @param string $key 上传到本地存储的文件名
|
||||
* @return array|bool
|
||||
* 写入本地 storage;兼容 UploadedFile 与路径字符串
|
||||
*/
|
||||
private function uploadFile(string $filePath, string $key): bool|array
|
||||
private function uploadFile(mixed $filePath, string $key): bool|array
|
||||
{
|
||||
if (Storage::put($key, file_get_contents($filePath))) {
|
||||
return [
|
||||
'key' => $key,
|
||||
'url' => asset('/storage/' . $key)
|
||||
];
|
||||
$prefix = trim((string) ($this->config['path_prefix'] ?? ''), '/');
|
||||
if ($prefix !== '' && !str_starts_with($key, $prefix . '/')) {
|
||||
$key = $prefix . '/' . ltrim($key, '/');
|
||||
}
|
||||
return false;
|
||||
$path = is_object($filePath) && method_exists($filePath, 'getRealPath')
|
||||
? $filePath->getRealPath()
|
||||
: (string) $filePath;
|
||||
if (!Storage::put($key, file_get_contents($path))) {
|
||||
return false;
|
||||
}
|
||||
$domain = rtrim((string) ($this->config['domain'] ?? ''), '/');
|
||||
$url = $domain !== '' ? ($domain . '/' . $key) : asset('/storage/' . $key);
|
||||
return [
|
||||
'key' => $key,
|
||||
'url' => $url,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
*
|
||||
* @param string $key 本地存储的文件名
|
||||
* @return bool
|
||||
*/
|
||||
private function deleteFile(string $key): bool
|
||||
{
|
||||
return Storage::delete($key);
|
||||
|
||||
81
app/Service/common/upload/QcloudStorageService.php
Normal file
81
app/Service/common/upload/QcloudStorageService.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\common\upload;
|
||||
|
||||
use App\BaseApp\BaseNotAuthService;
|
||||
use App\Service\common\oss\OssStorageInterface;
|
||||
use App\Service\common\UtilsService;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
/**
|
||||
* 腾讯云 COS:简易 PUT(Authorization 签名 v5)
|
||||
*/
|
||||
class QcloudStorageService extends BaseNotAuthService implements OssStorageInterface
|
||||
{
|
||||
protected array $config = [];
|
||||
|
||||
public function withConfig(array $config): static
|
||||
{
|
||||
$this->config = $config;
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* COS 对象上传(Sign Algorithm=sha1)
|
||||
*/
|
||||
private function uploadFile($filePath, string $key): bool|array
|
||||
{
|
||||
$secretId = (string) ($this->config['access_key'] ?? '');
|
||||
$secretKey = (string) ($this->config['secret_key'] ?? '');
|
||||
$bucket = (string) ($this->config['bucket'] ?? '');
|
||||
$region = (string) ($this->config['region'] ?? '');
|
||||
$domain = rtrim((string) ($this->config['domain'] ?? ''), '/');
|
||||
if ($secretId === '' || $secretKey === '' || $bucket === '' || $region === '') {
|
||||
UtilsService::getInstance()->errorThrow('腾讯云 COS 配置不完整(需要 SecretId/SecretKey/Bucket/Region)');
|
||||
}
|
||||
$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;
|
||||
$content = file_get_contents($path);
|
||||
$host = $bucket . '.cos.' . $region . '.myqcloud.com';
|
||||
$urlPath = '/' . ltrim($key, '/');
|
||||
$now = time();
|
||||
$keyTime = $now . ';' . ($now + 600);
|
||||
$signKey = hash_hmac('sha1', $keyTime, $secretKey);
|
||||
$httpString = strtolower('put') . "\n" . $urlPath . "\n\nhost=" . strtolower($host) . "\n";
|
||||
$stringToSign = "sha1\n{$keyTime}\n" . sha1($httpString) . "\n";
|
||||
$signature = hash_hmac('sha1', $stringToSign, $signKey);
|
||||
$authorization = 'q-sign-algorithm=sha1'
|
||||
. '&q-ak=' . $secretId
|
||||
. '&q-sign-time=' . $keyTime
|
||||
. '&q-key-time=' . $keyTime
|
||||
. '&q-header-list=host'
|
||||
. '&q-url-param-list='
|
||||
. '&q-signature=' . $signature;
|
||||
$url = 'https://' . $host . $urlPath;
|
||||
$response = Http::withHeaders([
|
||||
'Host' => $host,
|
||||
'Authorization' => $authorization,
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
])->withBody($content, 'application/octet-stream')->put($url);
|
||||
if (!$response->successful()) {
|
||||
UtilsService::getInstance()->errorThrow('腾讯云上传失败:' . $response->body());
|
||||
}
|
||||
$publicUrl = $domain !== '' ? ($domain . $urlPath) : $url;
|
||||
return ['key' => $key, 'url' => $publicUrl];
|
||||
}
|
||||
}
|
||||
@@ -2,131 +2,95 @@
|
||||
|
||||
namespace App\Service\common\upload;
|
||||
|
||||
use App\BaseApp\BaseService;
|
||||
use Exception;
|
||||
use Qiniu\Auth;
|
||||
use Qiniu\Storage\BucketManager;
|
||||
use Qiniu\Storage\UploadManager;
|
||||
use App\BaseApp\BaseNotAuthService;
|
||||
use App\Service\common\oss\OssStorageInterface;
|
||||
use App\Service\common\UtilsService;
|
||||
|
||||
class QiniuStorageService extends BaseService
|
||||
/**
|
||||
* 七牛云上传;优先使用官方 SDK,未安装时给出明确提示
|
||||
*/
|
||||
class QiniuStorageService extends BaseNotAuthService implements OssStorageInterface
|
||||
{
|
||||
/**
|
||||
* @var mixed 单例实例,确保该类只有一个全局实例
|
||||
*/
|
||||
protected static mixed $_instance;
|
||||
private $accessKey;
|
||||
private $secretKey;
|
||||
private $bucket;
|
||||
private $domain;
|
||||
protected array $config = [
|
||||
'access_key' => '',
|
||||
'secret_key' => '',
|
||||
'bucket' => '',
|
||||
'domain' => '',
|
||||
'path_prefix' => '',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
/**
|
||||
* 注入解密后的运行时配置
|
||||
*/
|
||||
public function withConfig(array $config): static
|
||||
{
|
||||
parent::__construct();
|
||||
$this->accessKey = config('nl.oss.qiniu.access_key');
|
||||
$this->secretKey = config('nl.oss.qiniu.secret_key');
|
||||
$this->bucket = config('nl.oss.qiniu.bucket');
|
||||
$this->domain = config('nl.oss.qiniu.domain');
|
||||
$this->config = array_merge($this->config, $config);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取实例
|
||||
* @return null|static
|
||||
*/
|
||||
public static function getInstance(): null|static
|
||||
{
|
||||
$name = get_called_class();
|
||||
if (!isset(self::$_instance[$name])) {
|
||||
self::$_instance[$name] = new static();
|
||||
}
|
||||
|
||||
return self::$_instance[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传图片
|
||||
*
|
||||
* @param string $filePath 文件本地路径
|
||||
* @param string $key 上传到七牛云的文件名
|
||||
* @return array|bool
|
||||
* @throws Exception
|
||||
*/
|
||||
public function uploadImage($filePath, $key): bool|array
|
||||
public function uploadImage($filePath, string $key): bool|array
|
||||
{
|
||||
return $this->uploadFile($filePath, $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传视频
|
||||
*
|
||||
* @param string $filePath 文件本地路径
|
||||
* @param string $key 上传到七牛云的文件名
|
||||
* @return array|bool
|
||||
*/
|
||||
public function uploadVideo($filePath, $key): bool|array
|
||||
public function uploadVideo($filePath, string $key): bool|array
|
||||
{
|
||||
return $this->uploadFile($filePath, $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除图片
|
||||
*
|
||||
* @param string $key 七牛云存储的文件名
|
||||
* @return bool
|
||||
*/
|
||||
public function deleteImage($key): bool
|
||||
{
|
||||
return $this->deleteFile($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除视频
|
||||
*
|
||||
* @param string $key 七牛云存储的文件名
|
||||
* @return bool
|
||||
*/
|
||||
public function deleteVideo($key): bool
|
||||
{
|
||||
return $this->deleteFile($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
*
|
||||
* @param string $filePath 文件本地路径
|
||||
* @param string $key 上传到七牛云的文件名
|
||||
* @return array|bool
|
||||
* @throws Exception
|
||||
* 上传到七牛;无 SDK 时抛业务异常引导安装或改本地
|
||||
*/
|
||||
private function uploadFile(string $filePath, string $key): bool|array
|
||||
private function uploadFile($filePath, string $key): bool|array
|
||||
{
|
||||
$auth = new Auth($this->accessKey, $this->secretKey);
|
||||
$token = $auth->uploadToken($this->bucket);
|
||||
$uploadMgr = new UploadManager();
|
||||
|
||||
list($ret, $err) = $uploadMgr->putFile($token, $key, $filePath);
|
||||
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;
|
||||
} else {
|
||||
return [
|
||||
'key' => $ret['key'],
|
||||
'url' => $this->domain . '/' . $ret['key']
|
||||
];
|
||||
}
|
||||
return [
|
||||
'key' => $ret['key'],
|
||||
'url' => $domain . '/' . $ret['key'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
*
|
||||
* @param string $key 七牛云存储的文件名
|
||||
* @return bool
|
||||
*/
|
||||
private function deleteFile(string $key): bool
|
||||
{
|
||||
$auth = new Auth($this->accessKey, $this->secretKey);
|
||||
$bucketMgr = new BucketManager($auth);
|
||||
|
||||
$err = $bucketMgr->delete($this->bucket, $key);
|
||||
if (!class_exists(\Qiniu\Auth::class)) {
|
||||
return false;
|
||||
}
|
||||
$auth = new \Qiniu\Auth($this->config['access_key'], $this->config['secret_key']);
|
||||
$bucketMgr = new \Qiniu\Storage\BucketManager($auth);
|
||||
$err = $bucketMgr->delete($this->config['bucket'], $key);
|
||||
return $err === null;
|
||||
}
|
||||
}
|
||||
|
||||
90
app/Service/common/upload/S3CompatibleStorageService.php
Normal file
90
app/Service/common/upload/S3CompatibleStorageService.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\common\upload;
|
||||
|
||||
use App\BaseApp\BaseNotAuthService;
|
||||
use App\Service\common\oss\OssStorageInterface;
|
||||
use App\Service\common\UtilsService;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
/**
|
||||
* S3 兼容上传(AWS / MinIO / 华为 OBS / 百度 BOS)
|
||||
* 使用 AWS Signature Version 4 的 PUT Object
|
||||
*/
|
||||
class S3CompatibleStorageService extends BaseNotAuthService implements OssStorageInterface
|
||||
{
|
||||
protected array $config = [];
|
||||
|
||||
public function withConfig(array $config): static
|
||||
{
|
||||
$this->config = $config;
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* SigV4 PUT;endpoint 必填(如 https://s3.amazonaws.com 或 MinIO 地址)
|
||||
*/
|
||||
private function uploadFile($filePath, string $key): bool|array
|
||||
{
|
||||
$accessKey = (string) ($this->config['access_key'] ?? '');
|
||||
$secretKey = (string) ($this->config['secret_key'] ?? '');
|
||||
$bucket = (string) ($this->config['bucket'] ?? '');
|
||||
$region = (string) ($this->config['region'] ?? 'us-east-1');
|
||||
$endpoint = rtrim((string) ($this->config['endpoint'] ?? ''), '/');
|
||||
$domain = rtrim((string) ($this->config['domain'] ?? ''), '/');
|
||||
$driver = (string) ($this->config['driver'] ?? 'aws');
|
||||
if ($accessKey === '' || $secretKey === '' || $bucket === '' || $endpoint === '') {
|
||||
UtilsService::getInstance()->errorThrow(strtoupper($driver) . ' 配置不完整(需要 AccessKey/Secret/Bucket/Endpoint)');
|
||||
}
|
||||
$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;
|
||||
$payload = file_get_contents($path);
|
||||
$host = parse_url($endpoint, PHP_URL_HOST) ?: preg_replace('#^https?://#', '', $endpoint);
|
||||
// path-style: endpoint/bucket/key
|
||||
$canonicalUri = '/' . rawurlencode($bucket) . '/' . str_replace('%2F', '/', rawurlencode($key));
|
||||
// 简化:多数 MinIO/OBS 接受 path-style
|
||||
$url = $endpoint . '/' . $bucket . '/' . $key;
|
||||
$amzDate = gmdate('Ymd\THis\Z');
|
||||
$dateStamp = gmdate('Ymd');
|
||||
$payloadHash = hash('sha256', $payload);
|
||||
$canonicalHeaders = "host:{$host}\nx-amz-content-sha256:{$payloadHash}\nx-amz-date:{$amzDate}\n";
|
||||
$signedHeaders = 'host;x-amz-content-sha256;x-amz-date';
|
||||
$canonicalRequest = "PUT\n{$canonicalUri}\n\n{$canonicalHeaders}\n{$signedHeaders}\n{$payloadHash}";
|
||||
$service = $driver === 'huawei' ? 's3' : 's3';
|
||||
$credentialScope = "{$dateStamp}/{$region}/{$service}/aws4_request";
|
||||
$stringToSign = "AWS4-HMAC-SHA256\n{$amzDate}\n{$credentialScope}\n" . hash('sha256', $canonicalRequest);
|
||||
$kDate = hash_hmac('sha256', $dateStamp, 'AWS4' . $secretKey, true);
|
||||
$kRegion = hash_hmac('sha256', $region, $kDate, true);
|
||||
$kService = hash_hmac('sha256', $service, $kRegion, true);
|
||||
$kSigning = hash_hmac('sha256', 'aws4_request', $kService, true);
|
||||
$signature = hash_hmac('sha256', $stringToSign, $kSigning);
|
||||
$authorization = "AWS4-HMAC-SHA256 Credential={$accessKey}/{$credentialScope}, SignedHeaders={$signedHeaders}, Signature={$signature}";
|
||||
$response = Http::withHeaders([
|
||||
'Authorization' => $authorization,
|
||||
'x-amz-content-sha256' => $payloadHash,
|
||||
'x-amz-date' => $amzDate,
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Host' => $host,
|
||||
])->withBody($payload, 'application/octet-stream')->put($url);
|
||||
if (!$response->successful()) {
|
||||
UtilsService::getInstance()->errorThrow($driver . ' 上传失败:' . $response->body());
|
||||
}
|
||||
$publicUrl = $domain !== '' ? ($domain . '/' . $key) : $url;
|
||||
return ['key' => $key, 'url' => $publicUrl];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user