You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
270 lines
9.9 KiB
270 lines
9.9 KiB
<?php
|
|
// +----------------------------------------------------------------------
|
|
// | Niucloud-admin 企业快速开发的多应用管理平台
|
|
// +----------------------------------------------------------------------
|
|
// | 官方网址:https://www.niucloud.com
|
|
// +----------------------------------------------------------------------
|
|
// | niucloud团队 版权所有 开源版本可自由商用
|
|
// +----------------------------------------------------------------------
|
|
// | Author: Niucloud Team
|
|
// +----------------------------------------------------------------------
|
|
|
|
namespace app\service\admin\upload;
|
|
|
|
use app\service\core\sys\CoreConfigService;
|
|
use core\base\BaseAdminService;
|
|
use core\exception\AdminException;
|
|
|
|
/**
|
|
* 对象存储直传服务类
|
|
*/
|
|
class DirectUploadService extends BaseAdminService
|
|
{
|
|
/**
|
|
* 获取上传凭证
|
|
* @param string $file_type 文件类型
|
|
* @param string $file_name 文件名
|
|
* @return array
|
|
*/
|
|
public function getUploadCredentials($file_type = 'image', $file_name = '')
|
|
{
|
|
// 获取存储配置
|
|
$config_data = (new CoreConfigService())->getConfig('STORAGE');
|
|
if (empty($config_data) || empty($config_data['value'])) {
|
|
throw new AdminException('存储配置未设置');
|
|
}
|
|
|
|
$storage_config = $config_data['value'];
|
|
if (is_string($storage_config)) {
|
|
$storage_config = json_decode($storage_config, true);
|
|
}
|
|
|
|
if (empty($storage_config)) {
|
|
throw new AdminException('存储配置格式错误');
|
|
}
|
|
|
|
$default_storage = $storage_config['default'] ?? 'local';
|
|
if ($default_storage == 'local') {
|
|
throw new AdminException('本地存储不支持直传');
|
|
}
|
|
|
|
if (!isset($storage_config[$default_storage])) {
|
|
throw new AdminException('存储配置错误');
|
|
}
|
|
|
|
$config = $storage_config[$default_storage];
|
|
|
|
// 生成文件路径
|
|
$file_path = $this->generateFilePath($file_type, $file_name);
|
|
|
|
// 根据不同存储类型获取凭证
|
|
switch ($default_storage) {
|
|
case 'tencent':
|
|
return $this->getTencentCredentials($config, $file_path, $file_type);
|
|
case 'aliyun':
|
|
return $this->getAliyunCredentials($config, $file_path, $file_type);
|
|
default:
|
|
throw new AdminException('不支持的存储类型');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取腾讯云COS直传凭证
|
|
* @param array $config 配置信息
|
|
* @param string $file_path 文件路径
|
|
* @param string $file_type 文件类型
|
|
* @return array
|
|
*/
|
|
private function getTencentCredentials($config, $file_path, $file_type)
|
|
{
|
|
// 检查必要的配置
|
|
$required_keys = ['bucket', 'region', 'access_key', 'secret_key'];
|
|
foreach ($required_keys as $key) {
|
|
if (empty($config[$key])) {
|
|
throw new AdminException("腾讯云配置缺少必要参数: {$key}");
|
|
}
|
|
}
|
|
|
|
// 使用UTC时间戳,确保与腾讯云服务器时间同步
|
|
$current_time = time();
|
|
$expired = $current_time + 3600; // 1小时过期
|
|
|
|
// 腾讯云COS要求:q-key-time和q-sign-time使用相同的时间范围
|
|
$key_time = $current_time . ';' . $expired;
|
|
$sign_time = $key_time; // POST上传时,sign-time通常与key-time相同
|
|
|
|
// 腾讯云COS POST表单策略 - 必须包含所有签名相关字段
|
|
$policy = [
|
|
'expiration' => gmdate('Y-m-d\TH:i:s.000\Z', $expired),
|
|
'conditions' => [
|
|
['starts-with', '$key', dirname($file_path) . '/'],
|
|
['eq', '$q-sign-algorithm', 'sha1'],
|
|
['eq', '$q-ak', $config['access_key']],
|
|
['eq', '$q-key-time', $key_time],
|
|
['eq', '$q-sign-time', $sign_time] // 必需字段
|
|
]
|
|
];
|
|
|
|
// 根据文件类型设置大小限制
|
|
$max_size = $this->getMaxFileSize($file_type);
|
|
if ($max_size > 0) {
|
|
$policy['conditions'][] = ['content-length-range', 0, $max_size];
|
|
}
|
|
|
|
// 重要:生成policy字符串时使用标准JSON格式
|
|
$policy_string = json_encode($policy, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
|
$policy_base64 = base64_encode($policy_string);
|
|
|
|
// 腾讯云COS POST签名计算 - 根据官方文档v4签名算法
|
|
// 1. 生成 SignKey = HMAC-SHA1(SecretKey, KeyTime)
|
|
$sign_key = hash_hmac('sha1', $key_time, $config['secret_key']);
|
|
|
|
// 2. 生成 StringToSign = SHA1(policy) - 注意:是policy字符串的SHA1,不是Base64
|
|
$string_to_sign = sha1($policy_string);
|
|
|
|
// 3. 生成 Signature = HMAC-SHA1(SignKey, StringToSign)
|
|
$signature = hash_hmac('sha1', $string_to_sign, $sign_key);
|
|
|
|
return [
|
|
'storage_type' => 'tencent',
|
|
'upload_url' => "https://{$config['bucket']}.cos.{$config['region']}.myqcloud.com",
|
|
'credentials' => [
|
|
'policy' => $policy_base64,
|
|
'q-sign-algorithm' => 'sha1',
|
|
'q-ak' => $config['access_key'],
|
|
'q-key-time' => $key_time,
|
|
'q-sign-time' => $sign_time,
|
|
'q-signature' => $signature,
|
|
'key' => $file_path,
|
|
],
|
|
'file_path' => $file_path,
|
|
'key' => $file_path,
|
|
'domain' => $config['domain'] ?? "https://{$config['bucket']}.cos.{$config['region']}.myqcloud.com",
|
|
'max_size' => $max_size,
|
|
'expired' => $expired,
|
|
// 调试信息(生产环境应移除)
|
|
'debug' => [
|
|
'policy_string' => $policy_string,
|
|
'string_to_sign' => $string_to_sign,
|
|
'sign_key' => $sign_key,
|
|
'current_time_utc' => gmdate('Y-m-d H:i:s', $current_time) . ' UTC',
|
|
'expired_time_utc' => gmdate('Y-m-d H:i:s', $expired) . ' UTC',
|
|
'timestamp_current' => $current_time,
|
|
'timestamp_expired' => $expired
|
|
]
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 获取阿里云OSS直传凭证
|
|
* @param array $config 配置信息
|
|
* @param string $file_path 文件路径
|
|
* @param string $file_type 文件类型
|
|
* @return array
|
|
*/
|
|
private function getAliyunCredentials($config, $file_path, $file_type)
|
|
{
|
|
// 检查必要的配置
|
|
$required_keys = ['bucket', 'endpoint', 'access_key', 'secret_key'];
|
|
foreach ($required_keys as $key) {
|
|
if (empty($config[$key])) {
|
|
throw new AdminException("阿里云配置缺少必要参数: {$key}");
|
|
}
|
|
}
|
|
|
|
$expired = time() + 3600; // 1小时过期
|
|
$dir = dirname($file_path) . '/';
|
|
|
|
// 生成策略
|
|
$policy = [
|
|
'expiration' => gmdate('Y-m-d\TH:i:s.000\Z', $expired),
|
|
'conditions' => [
|
|
['starts-with' => '$key', $dir],
|
|
]
|
|
];
|
|
|
|
// 根据文件类型设置大小限制
|
|
$max_size = $this->getMaxFileSize($file_type);
|
|
if ($max_size > 0) {
|
|
$policy['conditions'][] = ['content-length-range', 0, $max_size];
|
|
}
|
|
|
|
$policy_encoded = base64_encode(json_encode($policy));
|
|
$signature = base64_encode(hash_hmac('sha1', $policy_encoded, $config['secret_key'], true));
|
|
|
|
return [
|
|
'storage_type' => 'aliyun',
|
|
'upload_url' => "https://{$config['bucket']}.{$config['endpoint']}",
|
|
'credentials' => [
|
|
'OSSAccessKeyId' => $config['access_key'],
|
|
'policy' => $policy_encoded,
|
|
'signature' => $signature,
|
|
'key' => $file_path,
|
|
'success_action_status' => '200',
|
|
],
|
|
'file_path' => $file_path,
|
|
'domain' => $config['domain'] ?? "https://{$config['bucket']}.{$config['endpoint']}",
|
|
'max_size' => $max_size,
|
|
'expired' => $expired
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 生成文件路径
|
|
* @param string $file_type 文件类型
|
|
* @param string $file_name 文件名
|
|
* @return string
|
|
*/
|
|
private function generateFilePath($file_type, $file_name = '')
|
|
{
|
|
$type_dir_map = [
|
|
'image' => 'upload/attachment/image',
|
|
'video' => 'upload/attachment/video',
|
|
'document' => 'upload/attachment/document'
|
|
];
|
|
|
|
$dir = $type_dir_map[$file_type] ?? 'upload/attachment/file';
|
|
$date_dir = date('Y/m/d');
|
|
|
|
if (empty($file_name)) {
|
|
$file_name = uniqid() . '_${filename}'; // ${filename} 将被前端替换
|
|
}
|
|
|
|
return $dir . '/' . $date_dir . '/' . $file_name;
|
|
}
|
|
|
|
/**
|
|
* 根据文件类型获取最大文件大小
|
|
* @param string $file_type 文件类型
|
|
* @return int 字节数,0表示不限制
|
|
*/
|
|
private function getMaxFileSize($file_type)
|
|
{
|
|
$size_map = [
|
|
'image' => 10 * 1024 * 1024, // 10MB
|
|
'video' => 500 * 1024 * 1024, // 500MB
|
|
'document' => 50 * 1024 * 1024, // 50MB
|
|
];
|
|
|
|
return $size_map[$file_type] ?? 10 * 1024 * 1024;
|
|
}
|
|
|
|
/**
|
|
* 确认上传完成
|
|
* @param array $data 文件信息
|
|
* @return array
|
|
*/
|
|
public function confirmUpload($data)
|
|
{
|
|
// 这里可以将文件信息保存到附件表,供后续管理使用
|
|
// 暂时直接返回文件URL,实际项目中可以根据需要保存到数据库
|
|
|
|
return [
|
|
'url' => $data['file_url'],
|
|
'name' => $data['file_name'],
|
|
'size' => $data['file_size'],
|
|
'type' => $data['file_type'],
|
|
'storage_type' => $data['storage_type']
|
|
];
|
|
}
|
|
}
|