fix(upload): 改回PUT预签名URL方式,前端用readFile+uni.request PUT上传

- 后端恢复PUT预签名URL生成,移除POST Object的policy/signature逻辑
- 前端改用uni.getFileSystemManager().readFile读取二进制数据
- 再通过uni.request PUT方式直传COS(uni.uploadFile只支持POST)
- 参考验证过的CosUploadService实现
This commit is contained in:
zpc 2026-02-20 23:46:01 +08:00
parent 66df292628
commit 4bba7f8e53
3 changed files with 58 additions and 86 deletions

View File

@ -16,12 +16,12 @@ public interface IUploadConfigService
}
/// <summary>
/// 预签名上传信息COS POST Object 方式)
/// 预签名上传信息COS PUT 预签名URL方式)
/// </summary>
public class PresignedUploadInfo
{
/// <summary>
/// COS上传地址POST Object 目标URL
/// 预签名上传URLPUT方式直传
/// </summary>
public string UploadUrl { get; set; } = string.Empty;
@ -30,31 +30,6 @@ public class PresignedUploadInfo
/// </summary>
public string FileUrl { get; set; } = string.Empty;
/// <summary>
/// COS对象Key文件路径
/// </summary>
public string Key { get; set; } = string.Empty;
/// <summary>
/// Base64编码的上传策略
/// </summary>
public string Policy { get; set; } = string.Empty;
/// <summary>
/// SecretId
/// </summary>
public string SecretId { get; set; } = string.Empty;
/// <summary>
/// 密钥有效时间范围Unix时间戳格式: startTime;endTime
/// </summary>
public string KeyTime { get; set; } = string.Empty;
/// <summary>
/// HMAC-SHA1签名
/// </summary>
public string Signature { get; set; } = string.Empty;
/// <summary>
/// URL过期时间
/// </summary>

View File

@ -11,7 +11,7 @@ namespace MiAssessment.Core.Services;
/// <summary>
/// 上传配置服务实现
/// 从Admin库读取COS配置生成POST Object签名供小程序直传
/// 从Admin库读取COS配置生成PUT预签名URL供小程序直传
/// </summary>
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
};
}
/// <summary>
/// 生成POST Object的policy JSON
/// 参考: https://cloud.tencent.com/document/product/436/14690
/// 生成腾讯云COS预签名URLPUT方式
/// </summary>
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}";
}
/// <summary>

View File

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