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> /// <summary>
/// 预签名上传信息COS POST Object 方式) /// 预签名上传信息COS PUT 预签名URL方式)
/// </summary> /// </summary>
public class PresignedUploadInfo public class PresignedUploadInfo
{ {
/// <summary> /// <summary>
/// COS上传地址POST Object 目标URL /// 预签名上传URLPUT方式直传
/// </summary> /// </summary>
public string UploadUrl { get; set; } = string.Empty; public string UploadUrl { get; set; } = string.Empty;
@ -30,31 +30,6 @@ public class PresignedUploadInfo
/// </summary> /// </summary>
public string FileUrl { get; set; } = string.Empty; 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> /// <summary>
/// URL过期时间 /// URL过期时间
/// </summary> /// </summary>

View File

@ -11,7 +11,7 @@ namespace MiAssessment.Core.Services;
/// <summary> /// <summary>
/// 上传配置服务实现 /// 上传配置服务实现
/// 从Admin库读取COS配置生成POST Object签名供小程序直传 /// 从Admin库读取COS配置生成PUT预签名URL供小程序直传
/// </summary> /// </summary>
public class UploadConfigService : IUploadConfigService public class UploadConfigService : IUploadConfigService
{ {
@ -60,53 +60,49 @@ public class UploadConfigService : IUploadConfigService
var uniqueFileName = $"{timestamp}_{guid}{extension}"; var uniqueFileName = $"{timestamp}_{guid}{extension}";
var objectKey = $"{UploadBasePath}/{datePath}/{uniqueFileName}"; var objectKey = $"{UploadBasePath}/{datePath}/{uniqueFileName}";
// 生成POST Object签名信息 // 生成PUT预签名URL
var host = $"{setting.Bucket}.cos.{setting.Region}.myqcloud.com"; var presignedUrl = GeneratePresignedUrl(setting, objectKey, "PUT", contentType, DefaultExpiresInSeconds);
var uploadUrl = $"https://{host}";
var fileUrl = GenerateAccessUrl(setting.Domain!, objectKey); var fileUrl = GenerateAccessUrl(setting.Domain!, objectKey);
var startTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); _logger.LogInformation("生成预签名URL成功: {ObjectKey}", objectKey);
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);
return new PresignedUploadInfo return new PresignedUploadInfo
{ {
UploadUrl = uploadUrl, UploadUrl = presignedUrl,
FileUrl = fileUrl, FileUrl = fileUrl,
Key = objectKey,
Policy = policyBase64,
SecretId = setting.AccessKeyId!,
KeyTime = keyTime,
Signature = signature,
ExpiresIn = DefaultExpiresInSeconds ExpiresIn = DefaultExpiresInSeconds
}; };
} }
/// <summary> /// <summary>
/// 生成POST Object的policy JSON /// 生成腾讯云COS预签名URLPUT方式
/// 参考: https://cloud.tencent.com/document/product/436/14690
/// </summary> /// </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 host = $"{setting.Bucket}.cos.{setting.Region}.myqcloud.com";
var policy = $$""" var urlPath = $"/{objectKey}";
{"expiration":"{{expiration}}","conditions":[{"q-sign-algorithm":"sha1"},{"q-ak":"{{secretId}}"},{"q-sign-time":"{{keyTime}}"},{"key":"{{objectKey}}"}]}
""".Trim();
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> /// <summary>

View File

@ -1,7 +1,8 @@
/** /**
* COS直传工具 * COS直传工具
* 通过POST Object方式将文件直传到腾讯云COS * 通过PUT预签名URL将文件直传到腾讯云COS
* 参考: https://cloud.tencent.com/document/product/436/14690 * 使用 uni.getFileSystemManager().readFile 读取文件二进制数据
* 再通过 uni.request PUT 方式上传uni.uploadFile 只支持 POST
*/ */
import { getPresignedUploadUrl } from '@/api/user.js' 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 mimeMap = { jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif', webp: 'image/webp' }
const contentType = mimeMap[ext] || 'image/png' const contentType = mimeMap[ext] || 'image/png'
// 2. 获取POST Object签名信息 // 2. 获取预签名URL
const presignedRes = await getPresignedUploadUrl(fileName, contentType) const presignedRes = await getPresignedUploadUrl(fileName, contentType)
if (!presignedRes || presignedRes.code !== 0 || !presignedRes.data) { if (!presignedRes || presignedRes.code !== 0 || !presignedRes.data) {
throw new Error(presignedRes?.message || '获取上传地址失败') 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) => { await new Promise((resolve, reject) => {
uni.uploadFile({ uni.getFileSystemManager().readFile({
url: uploadUrl,
filePath: tempFilePath, filePath: tempFilePath,
name: 'file', success: (readRes) => {
formData: { // 使用PUT方法上传二进制数据
'key': key, uni.request({
'policy': policy, url: uploadUrl,
'q-sign-algorithm': 'sha1', method: 'PUT',
'q-ak': secretId, data: readRes.data,
'q-key-time': keyTime, header: {
'q-sign-time': keyTime, 'Content-Type': contentType
'q-signature': signature },
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) => { fail: (err) => reject(new Error('读取文件失败: ' + err.errMsg))
// 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 || '上传失败'))
}) })
}) })