From 4bba7f8e538a4c4b3c867f1e4a31856ee2e82ef9 Mon Sep 17 00:00:00 2001 From: zpc Date: Fri, 20 Feb 2026 23:46:01 +0800 Subject: [PATCH] =?UTF-8?q?fix(upload):=20=E6=94=B9=E5=9B=9EPUT=E9=A2=84?= =?UTF-8?q?=E7=AD=BE=E5=90=8DURL=E6=96=B9=E5=BC=8F=EF=BC=8C=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=E7=94=A8readFile+uni.request=20PUT=E4=B8=8A=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端恢复PUT预签名URL生成,移除POST Object的policy/signature逻辑 - 前端改用uni.getFileSystemManager().readFile读取二进制数据 - 再通过uni.request PUT方式直传COS(uni.uploadFile只支持POST) - 参考验证过的CosUploadService实现 --- .../Interfaces/IUploadConfigService.cs | 29 +-------- .../Services/UploadConfigService.cs | 62 +++++++++---------- uniapp/utils/upload.js | 53 ++++++++-------- 3 files changed, 58 insertions(+), 86 deletions(-) diff --git a/server/MiAssessment/src/MiAssessment.Core/Interfaces/IUploadConfigService.cs b/server/MiAssessment/src/MiAssessment.Core/Interfaces/IUploadConfigService.cs index 3009c59..e8af761 100644 --- a/server/MiAssessment/src/MiAssessment.Core/Interfaces/IUploadConfigService.cs +++ b/server/MiAssessment/src/MiAssessment.Core/Interfaces/IUploadConfigService.cs @@ -16,12 +16,12 @@ public interface IUploadConfigService } /// -/// 预签名上传信息(COS POST Object 方式) +/// 预签名上传信息(COS PUT 预签名URL方式) /// public class PresignedUploadInfo { /// - /// COS上传地址(POST Object 目标URL) + /// 预签名上传URL(PUT方式直传) /// public string UploadUrl { get; set; } = string.Empty; @@ -30,31 +30,6 @@ public class PresignedUploadInfo /// public string FileUrl { get; set; } = string.Empty; - /// - /// COS对象Key(文件路径) - /// - public string Key { get; set; } = string.Empty; - - /// - /// Base64编码的上传策略 - /// - public string Policy { get; set; } = string.Empty; - - /// - /// SecretId - /// - public string SecretId { get; set; } = string.Empty; - - /// - /// 密钥有效时间范围(Unix时间戳格式: startTime;endTime) - /// - public string KeyTime { get; set; } = string.Empty; - - /// - /// HMAC-SHA1签名 - /// - public string Signature { get; set; } = string.Empty; - /// /// URL过期时间(秒) /// diff --git a/server/MiAssessment/src/MiAssessment.Core/Services/UploadConfigService.cs b/server/MiAssessment/src/MiAssessment.Core/Services/UploadConfigService.cs index aa22b26..34d18d0 100644 --- a/server/MiAssessment/src/MiAssessment.Core/Services/UploadConfigService.cs +++ b/server/MiAssessment/src/MiAssessment.Core/Services/UploadConfigService.cs @@ -11,7 +11,7 @@ namespace MiAssessment.Core.Services; /// /// 上传配置服务实现 -/// 从Admin库读取COS配置,生成POST Object签名供小程序直传 +/// 从Admin库读取COS配置,生成PUT预签名URL供小程序直传 /// public class UploadConfigService : IUploadConfigService { @@ -60,53 +60,49 @@ public class UploadConfigService : IUploadConfigService var uniqueFileName = $"{timestamp}_{guid}{extension}"; var objectKey = $"{UploadBasePath}/{datePath}/{uniqueFileName}"; - // 生成POST Object签名信息 - var host = $"{setting.Bucket}.cos.{setting.Region}.myqcloud.com"; - var uploadUrl = $"https://{host}"; + // 生成PUT预签名URL + var presignedUrl = GeneratePresignedUrl(setting, objectKey, "PUT", contentType, DefaultExpiresInSeconds); var fileUrl = GenerateAccessUrl(setting.Domain!, objectKey); - var startTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); - var endTime = startTime + DefaultExpiresInSeconds; - var keyTime = $"{startTime};{endTime}"; - - // 生成policy - var policy = GeneratePostPolicy(objectKey, setting.AccessKeyId!, keyTime); - var policyBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(policy)); - - // 生成签名: SignKey -> StringToSign(SHA1 of raw policy) -> Signature - var signKey = HmacSha1(setting.AccessKeySecret!, keyTime); - var stringToSign = Sha1Hash(policy); - var signature = HmacSha1(signKey, stringToSign); - - _logger.LogInformation("生成POST Object签名成功: {ObjectKey}", objectKey); + _logger.LogInformation("生成预签名URL成功: {ObjectKey}", objectKey); return new PresignedUploadInfo { - UploadUrl = uploadUrl, + UploadUrl = presignedUrl, FileUrl = fileUrl, - Key = objectKey, - Policy = policyBase64, - SecretId = setting.AccessKeyId!, - KeyTime = keyTime, - Signature = signature, ExpiresIn = DefaultExpiresInSeconds }; } /// - /// 生成POST Object的policy JSON - /// 参考: https://cloud.tencent.com/document/product/436/14690 + /// 生成腾讯云COS预签名URL(PUT方式) /// - private static string GeneratePostPolicy(string objectKey, string secretId, string keyTime) + private static string GeneratePresignedUrl(UploadSetting setting, string objectKey, string httpMethod, string contentType, int expiresInSeconds) { - var expiration = DateTimeOffset.UtcNow.AddSeconds(DefaultExpiresInSeconds).ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); + var startTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + var endTime = startTime + expiresInSeconds; + var keyTime = $"{startTime};{endTime}"; - // 手动拼接JSON,因为COS要求key中包含连字符(q-sign-algorithm等) - var policy = $$""" - {"expiration":"{{expiration}}","conditions":[{"q-sign-algorithm":"sha1"},{"q-ak":"{{secretId}}"},{"q-sign-time":"{{keyTime}}"},{"key":"{{objectKey}}"}]} - """.Trim(); + var host = $"{setting.Bucket}.cos.{setting.Region}.myqcloud.com"; + var urlPath = $"/{objectKey}"; - return policy; + // 1. SignKey + var signKey = HmacSha1(setting.AccessKeySecret!, keyTime); + + // 2. HttpString + var httpString = $"{httpMethod.ToLowerInvariant()}\n{urlPath}\n\nhost={host.ToLowerInvariant()}\n"; + + // 3. StringToSign + var sha1HttpString = Sha1Hash(httpString); + var stringToSign = $"sha1\n{keyTime}\n{sha1HttpString}\n"; + + // 4. Signature + var signature = HmacSha1(signKey, stringToSign); + + // 5. Authorization + var authorization = $"q-sign-algorithm=sha1&q-ak={setting.AccessKeyId}&q-sign-time={keyTime}&q-key-time={keyTime}&q-header-list=host&q-url-param-list=&q-signature={signature}"; + + return $"https://{host}{urlPath}?{authorization}"; } /// diff --git a/uniapp/utils/upload.js b/uniapp/utils/upload.js index 6e6dbe2..b3782e9 100644 --- a/uniapp/utils/upload.js +++ b/uniapp/utils/upload.js @@ -1,7 +1,8 @@ /** * COS直传工具 - * 通过POST Object方式将文件直传到腾讯云COS - * 参考: https://cloud.tencent.com/document/product/436/14690 + * 通过PUT预签名URL将文件直传到腾讯云COS + * 使用 uni.getFileSystemManager().readFile 读取文件二进制数据 + * 再通过 uni.request PUT 方式上传(uni.uploadFile 只支持 POST) */ import { getPresignedUploadUrl } from '@/api/user.js' @@ -41,39 +42,39 @@ export async function chooseAndUploadImage(options = {}) { const mimeMap = { jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif', webp: 'image/webp' } const contentType = mimeMap[ext] || 'image/png' - // 2. 获取POST Object签名信息 + // 2. 获取预签名URL const presignedRes = await getPresignedUploadUrl(fileName, contentType) if (!presignedRes || presignedRes.code !== 0 || !presignedRes.data) { throw new Error(presignedRes?.message || '获取上传地址失败') } - const { uploadUrl, fileUrl, key, policy, secretId, keyTime, signature } = presignedRes.data + const { uploadUrl, fileUrl } = presignedRes.data - // 3. POST Object方式直传COS + // 3. 读取文件二进制数据,然后用PUT方式上传到COS await new Promise((resolve, reject) => { - uni.uploadFile({ - url: uploadUrl, + uni.getFileSystemManager().readFile({ filePath: tempFilePath, - name: 'file', - formData: { - 'key': key, - 'policy': policy, - 'q-sign-algorithm': 'sha1', - 'q-ak': secretId, - 'q-key-time': keyTime, - 'q-sign-time': keyTime, - 'q-signature': signature + success: (readRes) => { + // 使用PUT方法上传二进制数据 + uni.request({ + url: uploadUrl, + method: 'PUT', + data: readRes.data, + header: { + 'Content-Type': contentType + }, + success: (res) => { + if (res.statusCode === 200) { + resolve(res) + } else { + console.error('COS上传失败:', res.statusCode, res.data) + reject(new Error(`上传失败,状态码: ${res.statusCode}`)) + } + }, + fail: (err) => reject(new Error('上传COS失败: ' + (err.errMsg || '网络错误'))) + }) }, - success: (res) => { - // COS POST Object成功返回 200 或 204 - if (res.statusCode >= 200 && res.statusCode < 300) { - resolve(res) - } else { - console.error('COS上传失败:', res.statusCode, res.data) - reject(new Error(`上传失败,状态码: ${res.statusCode}`)) - } - }, - fail: (err) => reject(new Error(err.errMsg || '上传失败')) + fail: (err) => reject(new Error('读取文件失败: ' + err.errMsg)) }) })