AI代码生成工具

This commit is contained in:
李琦
2026-08-10 18:32:06 +08:00
parent ab417bd0f2
commit cab69b4193
26 changed files with 1891 additions and 263 deletions

View File

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

View 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 => '其他',
};
}
}

View 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,
];
}
}

View 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),
};
}
}

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

View 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;
/**
* 阿里云 OSSREST PutObjectAuthorization 签名)
*/
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];
}
}

View File

@@ -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);

View 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简易 PUTAuthorization 签名 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];
}
}

View File

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

View 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 PUTendpoint 必填(如 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];
}
}