Files
zyt/server/app/common/service/DirectUploadService.php
T
long 7d7a4aa4d2 oss 直传
分片上传
2026-05-08 10:49:16 +08:00

151 lines
4.9 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace app\common\service;
use app\common\enum\FileEnum;
use app\common\model\file\File;
use app\common\service\storage\engine\Qcloud as QcloudEngine;
use Exception;
/**
* 浏览器直传 OSS 服务
* - 目前仅支持腾讯云 COSqcloud)
* - 其他 driver 返回 fallback=true,由前端降级到老链路
*
* Class DirectUploadService
* @package app\common\service
*/
class DirectUploadService
{
/** 视频允许的扩展名(沿用 config/project.file_video */
public const TYPE_VIDEO = 'video';
/** 默认凭证有效期 30 分钟 */
public const DEFAULT_DURATION = 1800;
/** 允许扩展类型 → 大小上限(字节) */
private const MAX_SIZE = [
self::TYPE_VIDEO => 2 * 1024 * 1024 * 1024, // 2GB
];
/**
* @notes 签发临时凭证
* @param string $type
* @return array
* @throws Exception
*/
public static function issueCredentials(string $type): array
{
if (!isset(self::MAX_SIZE[$type])) {
throw new Exception('不支持的上传类型: ' . $type);
}
$default = ConfigService::get('storage', 'default', 'local');
if ($default !== 'qcloud') {
// 其他 driver 不支持直传,前端降级
return ['provider' => $default, 'fallback' => true];
}
$storageConfig = ConfigService::get('storage', 'qcloud');
if (empty($storageConfig['bucket']) || empty($storageConfig['region'])
|| empty($storageConfig['access_key']) || empty($storageConfig['secret_key'])) {
throw new Exception('腾讯云 COS 配置不完整');
}
$keyPrefix = self::buildKeyPrefix($type);
$engine = new QcloudEngine($storageConfig);
$sts = $engine->getStsCredentials($keyPrefix, self::MAX_SIZE[$type], self::DEFAULT_DURATION);
return [
'provider' => 'qcloud',
'fallback' => false,
'bucket' => $sts['bucket'],
'region' => $sts['region'],
'host' => $sts['host'],
'cdn_domain' => rtrim((string)($storageConfig['domain'] ?? ''), '/'),
'key_prefix' => $keyPrefix,
'max_size' => self::MAX_SIZE[$type],
'duration' => self::DEFAULT_DURATION,
'expired_time' => $sts['expiredTime'],
'start_time' => $sts['startTime'],
'credentials' => $sts['credentials'],
];
}
/**
* @notes 直传完成后的回执:HEAD 校验 + 写 file 表
* @param array $params {key, type, name, size, content_type, cid, admin_id}
* @return array {id, cid, type, name, uri, url}
* @throws Exception
*/
public static function confirm(array $params): array
{
$type = (string)($params['type'] ?? '');
if (!isset(self::MAX_SIZE[$type])) {
throw new Exception('不支持的上传类型');
}
$default = ConfigService::get('storage', 'default', 'local');
if ($default !== 'qcloud') {
throw new Exception('当前存储驱动不支持直传回执');
}
$key = ltrim((string)($params['key'] ?? ''), '/');
$allowedPrefix = self::buildKeyPrefix($type);
if ($key === '' || strpos($key, $allowedPrefix) !== 0) {
throw new Exception('对象 Key 非法');
}
$storageConfig = ConfigService::get('storage', 'qcloud');
$engine = new QcloudEngine($storageConfig);
$head = $engine->headObject($key);
if ($head === false) {
throw new Exception('对象未找到,请确认上传是否完成');
}
if ($head['size'] <= 0 || $head['size'] > self::MAX_SIZE[$type]) {
throw new Exception('文件大小超出限制');
}
$name = trim((string)($params['name'] ?? ''));
if ($name === '') {
$name = basename($key);
}
if (strlen($name) > 128) {
$name = substr($name, 0, 123) . substr($name, -5);
}
$file = File::create([
'cid' => (int)($params['cid'] ?? 0),
'type' => self::resolveFileType($type),
'name' => $name,
'uri' => $key,
'source' => FileEnum::SOURCE_ADMIN,
'source_id' => (int)($params['admin_id'] ?? 0),
'create_time' => time(),
]);
$url = FileService::getFileUrl($key);
return [
'id' => $file['id'],
'cid' => $file['cid'],
'type' => $file['type'],
'name' => $file['name'],
'uri' => $url,
'url' => $url,
];
}
private static function buildKeyPrefix(string $type): string
{
return 'uploads/' . $type . '/' . date('Ymd') . '/';
}
private static function resolveFileType(string $type): int
{
return match ($type) {
self::TYPE_VIDEO => FileEnum::VIDEO_TYPE,
default => FileEnum::FILE_TYPE,
};
}
}