图片上传改造方案梳理.md
14.6 KB
图片上传改造方案梳理
文档日期:2025年1月
改造目标:将图片上传流程改为"先上传到服务器本地,再上传到OSS"
一、当前实现(备份方法)
1.1 当前流程
标准文件上传(Uploader_bak):
- 验证文件类型
- 生成文件路径和文件名
- 直接上传到OSS(
UploadFileByType_bak) - 获取OSS访问URL
- 返回结果
Base64图片上传(UploadBase64Image_bak):
- 解析Base64数据
- 验证图片格式
- 生成文件路径和文件名
- 直接上传到OSS
- 获取OSS访问URL
- 返回结果
问题:
- ❌ 如果OSS上传失败,没有本地备份
- ❌ 如果OSS服务不可用,无法提供服务
- ❌ 无法进行本地验证和预览
二、改造方案
2.1 改造后的流程
标准文件上传(Uploader):
- 验证文件类型
- 生成文件路径和文件名
- 先上传到服务器本地
- 从服务器本地上传到OSS
- OSS上传成功 → 删除本地文件
- OSS上传失败 → 保留本地文件
- 获取访问URL(OSS成功用OSS URL,失败用本地URL)
- 返回结果
Base64图片上传(UploadBase64Image):
- 解析Base64数据
- 验证图片格式
- 生成文件路径和文件名
- 先保存到服务器本地
- 从服务器本地上传到OSS
- OSS上传成功 → 删除本地文件
- OSS上传失败 → 保留本地文件
- 获取访问URL(OSS成功用OSS URL,失败用本地URL)
- 返回结果
三、详细改造逻辑
3.1 Uploader 方法改造逻辑
改造前:
// 直接上传到OSS
await UploadFileByType(file, uploadFilePath, _fileName, forceStoreType);
改造后:
// 1. 先上传到服务器本地
var localFilePath = GetPathByType(type); // 获取本地存储路径
var localFileName = _fileName;
var localFullPath = Path.Combine(localFilePath, localFileName);
// 确保目录存在
if (!Directory.Exists(localFilePath))
{
Directory.CreateDirectory(localFilePath);
}
// 保存到本地
using (var localStream = File.Create(localFullPath))
{
await file.CopyToAsync(localStream);
}
// 2. 从服务器本地上传到OSS
bool ossUploadSuccess = false;
string fileUrl;
try
{
if (type == "annexpic" || forceStoreType == "aliyun-oss")
{
// 读取本地文件并上传到OSS
var bucketName = KeyVariable.BucketName;
var ossPath = $"{uploadFilePath.TrimEnd('/').TrimEnd('\\')}/{_fileName}";
using (var localFileStream = File.OpenRead(localFullPath))
{
await _oSSServiceFactory.Create("aliyun").PutObjectAsync(bucketName, ossPath, localFileStream);
}
ossUploadSuccess = true;
// 3. OSS上传成功,删除本地文件
if (File.Exists(localFullPath))
{
File.Delete(localFullPath);
}
// 获取OSS访问URL
fileUrl = await GetOSSAccessUrl(uploadFilePath, _fileName);
}
else
{
// 非OSS类型,使用本地URL
fileUrl = string.Format("/api/File/Image/{0}/{1}", type, _fileName);
ossUploadSuccess = true; // 本地存储视为成功
}
}
catch (Exception ossEx)
{
// 4. OSS上传失败,保留本地文件
ossUploadSuccess = false;
// 使用本地URL作为降级方案
fileUrl = string.Format("/api/File/Image/{0}/{1}", type, _fileName);
// 记录错误日志(可选)
// _logger.LogError(ossEx, "OSS上传失败,使用本地文件作为降级方案");
}
3.2 UploadBase64Image 方法改造逻辑
改造前:
// 直接上传到OSS
using (var stream = new MemoryStream(imageData))
{
await _oSSServiceFactory.Create("aliyun").PutObjectAsync(bucketName, ossPath, stream);
}
改造后:
// 1. 先保存到服务器本地
var localFilePath = GetPathByType(imageType);
var localFileName = fileName;
var localFullPath = Path.Combine(localFilePath, localFileName);
// 确保目录存在
if (!Directory.Exists(localFilePath))
{
Directory.CreateDirectory(localFilePath);
}
// 保存Base64数据到本地
await File.WriteAllBytesAsync(localFullPath, imageData);
// 2. 从服务器本地上传到OSS
bool ossUploadSuccess = false;
string accessUrl;
try
{
var bucketName = KeyVariable.BucketName;
var ossPath = $"{uploadFilePath.TrimEnd('/').TrimEnd('\\')}/{fileName}";
// 读取本地文件并上传到OSS
using (var localFileStream = File.OpenRead(localFullPath))
{
await _oSSServiceFactory.Create("aliyun").PutObjectAsync(bucketName, ossPath, localFileStream);
}
ossUploadSuccess = true;
// 3. OSS上传成功,删除本地文件
if (File.Exists(localFullPath))
{
File.Delete(localFullPath);
}
// 获取OSS访问URL
accessUrl = await GetOSSAccessUrl(uploadFilePath, fileName);
}
catch (Exception ossEx)
{
// 4. OSS上传失败,保留本地文件
ossUploadSuccess = false;
// 使用本地URL作为降级方案
accessUrl = string.Format("/api/File/Image/{0}/{1}", imageType, fileName);
// 记录错误日志(可选)
// _logger.LogError(ossEx, "OSS上传失败,使用本地文件作为降级方案");
}
3.3 UploadFileByType 方法改造逻辑
说明:UploadFileByType 方法需要改造,但改造方式取决于调用场景:
方案A:保持方法签名不变,内部实现改造
- 优点:调用方不需要修改
- 缺点:方法职责不清晰(既要处理本地又要处理OSS)
方案B:创建新方法 UploadFileToLocalThenOSS
- 优点:职责清晰,不影响现有逻辑
- 缺点:需要修改调用方
推荐方案B:创建新方法,保留原方法作为备份
/// <summary>
/// 先上传到本地,再上传到OSS(改造后的方法)
/// </summary>
[NonAction]
public async Task<(bool OssSuccess, string LocalPath, string OssPath)> UploadFileToLocalThenOSS(
IFormFile file,
string localFilePath,
string ossFilePath,
string fileName,
string forceStoreType = null)
{
var localFullPath = Path.Combine(localFilePath, fileName);
bool ossUploadSuccess = false;
string ossPath = null;
try
{
// 1. 先保存到本地
if (!Directory.Exists(localFilePath))
{
Directory.CreateDirectory(localFilePath);
}
using (var localStream = File.Create(localFullPath))
{
await file.CopyToAsync(localStream);
}
// 2. 判断是否需要上传到OSS
var fileStoreType = !string.IsNullOrEmpty(forceStoreType) ? forceStoreType : KeyVariable.FileStoreType;
if (fileStoreType == "aliyun-oss")
{
// 3. 从本地文件上传到OSS
var bucketName = KeyVariable.BucketName;
ossPath = $"{ossFilePath.TrimEnd('/').TrimEnd('\\')}/{fileName}";
using (var localFileStream = File.OpenRead(localFullPath))
{
await _oSSServiceFactory.Create("aliyun").PutObjectAsync(bucketName, ossPath, localFileStream);
}
ossUploadSuccess = true;
// 4. OSS上传成功,删除本地文件
if (File.Exists(localFullPath))
{
File.Delete(localFullPath);
}
}
else
{
// 非OSS类型,本地存储视为成功
ossUploadSuccess = true;
}
return (ossUploadSuccess, localFullPath, ossPath);
}
catch (Exception ex)
{
// OSS上传失败,保留本地文件
ossUploadSuccess = false;
// 记录错误日志
// _logger.LogError(ex, $"文件上传到OSS失败,保留本地文件: {localFullPath}");
return (ossUploadSuccess, localFullPath, null);
}
}
四、改造要点总结
4.1 关键步骤
先保存到本地
- 使用
File.Create或File.WriteAllBytesAsync保存文件 - 确保目录存在(
Directory.CreateDirectory)
- 使用
从本地上传到OSS
- 使用
File.OpenRead读取本地文件 - 使用
PutObjectAsync上传到OSS
- 使用
OSS上传成功处理
- 删除本地文件(
File.Delete) - 返回OSS访问URL
- 删除本地文件(
OSS上传失败处理
- 保留本地文件
- 返回本地访问URL(降级方案)
- 记录错误日志(可选)
4.2 异常处理
异常场景:
- 本地保存失败:直接抛出异常,不继续OSS上传
- OSS上传失败:捕获异常,保留本地文件,返回本地URL
- OSS上传成功但删除本地文件失败:记录警告日志,但不影响返回结果
异常处理代码结构:
try
{
// 1. 保存到本地
// ...
// 2. 上传到OSS
try
{
// OSS上传逻辑
// ...
// 3. 删除本地文件
if (File.Exists(localFullPath))
{
File.Delete(localFullPath);
}
}
catch (Exception ossEx)
{
// OSS上传失败,保留本地文件
// 返回本地URL
}
}
catch (Exception localEx)
{
// 本地保存失败,抛出异常
throw NCCException.Oh($"文件保存到本地失败: {localEx.Message}", localEx);
}
4.3 文件路径处理
本地路径:
- 使用
GetPathByType(type)获取本地存储路径 - 使用
Path.Combine(localFilePath, fileName)组合完整路径 - 确保使用系统路径分隔符
OSS路径:
- 使用正斜杠
/,不使用Path.Combine - 格式:
{uploadFilePath}/{fileName}
路径示例:
// 本地路径(Windows: D:\Files\annexpic\20250123_123.jpg)
var localPath = Path.Combine("D:\\Files\\annexpic", "20250123_123.jpg");
// OSS路径(2025/01/23/20250123_123.jpg)
var ossPath = "2025/01/23/20250123_123.jpg";
4.4 URL返回逻辑
OSS上传成功:
- 返回OSS访问URL(带签名)
- 使用
GetOSSAccessUrl方法生成
OSS上传失败:
- 返回本地访问URL
- 格式:
/api/File/Image/{type}/{fileName} - 通过
GetImg方法提供访问
五、改造影响分析
5.1 优点
✅ 数据安全:
- 本地有备份,即使OSS失败也能提供服务
- 可以定期清理本地文件
✅ 可维护性:
- 可以本地验证文件是否正确
- 便于调试和问题排查
✅ 降级方案:
- OSS服务不可用时,自动降级到本地存储
- 不影响业务连续性
5.2 缺点
⚠️ 存储空间:
- 需要额外的本地存储空间
- 如果OSS上传失败,本地文件会累积
⚠️ 性能影响:
- 需要两次写入(本地 + OSS)
- 可能略微增加上传时间
⚠️ 清理机制:
- 需要定期清理OSS上传失败后保留的本地文件
- 需要监控本地存储空间
5.3 风险点
本地存储空间不足:
- 风险:如果本地磁盘空间不足,可能导致上传失败
- 缓解:定期清理、监控磁盘空间
OSS上传失败后本地文件累积:
- 风险:如果OSS长期不可用,本地文件会大量累积
- 缓解:定期清理机制、监控本地文件数量
并发问题:
- 风险:多线程同时操作同一文件
- 缓解:文件名使用唯一ID,避免冲突
六、改造实施步骤
6.1 第一步:接口备份 ✅
- ✅ 已完成:
Uploader_bak、UploadBase64Image_bak、UploadFileByType_bak
6.2 第二步:梳理逻辑 ✅
- ✅ 已完成:本文档
6.3 第三步:创建新方法(可选)
- 创建
UploadFileToLocalThenOSS方法 - 或直接改造
UploadFileByType方法
6.4 第四步:改造 Uploader 方法
- 修改
Uploader方法,使用新的上传逻辑 - 测试标准文件上传功能
6.5 第五步:改造 UploadBase64Image 方法
- 修改
UploadBase64Image方法,使用新的上传逻辑 - 测试Base64图片上传功能
6.6 第六步:测试验证
- 测试OSS上传成功场景
- 测试OSS上传失败场景(模拟OSS服务不可用)
- 测试本地文件删除逻辑
- 测试降级方案(返回本地URL)
6.7 第七步:清理机制(可选)
- 实现定期清理OSS上传失败后保留的本地文件
- 添加监控和告警
七、代码结构建议
7.1 方法组织
// 备份方法(保持不变)
Uploader_bak
UploadBase64Image_bak
UploadFileByType_bak
// 改造后的方法
Uploader // 使用新的上传逻辑
UploadBase64Image // 使用新的上传逻辑
UploadFileToLocalThenOSS // 新的核心上传方法(可选)
7.2 辅助方法
可能需要添加的辅助方法:
SaveFileToLocal:保存文件到本地UploadLocalFileToOSS:从本地上传到OSSDeleteLocalFile:删除本地文件(带异常处理)GetFileUrl:根据OSS上传结果返回URL
八、注意事项
8.1 文件权限
- 确保应用有本地文件写入权限
- 确保应用有本地文件删除权限
8.2 路径安全
- 验证文件路径,防止路径遍历攻击
- 使用
Path.GetFullPath规范化路径
8.3 资源释放
- 确保文件流正确释放(使用
using语句) - 避免文件句柄泄漏
8.4 日志记录
- 记录OSS上传成功/失败日志
- 记录本地文件删除成功/失败日志
- 便于问题排查和监控
九、测试用例
9.1 正常流程测试
OSS上传成功:
- 文件保存到本地 ✅
- 文件上传到OSS ✅
- 本地文件被删除 ✅
- 返回OSS URL ✅
非OSS类型:
- 文件保存到本地 ✅
- 不进行OSS上传 ✅
- 返回本地URL ✅
9.2 异常流程测试
OSS上传失败:
- 文件保存到本地 ✅
- OSS上传失败(模拟) ✅
- 本地文件保留 ✅
- 返回本地URL ✅
本地保存失败:
- 本地保存失败(模拟磁盘满) ✅
- 抛出异常,不进行OSS上传 ✅
OSS上传成功但删除本地文件失败:
- 文件上传到OSS ✅
- 删除本地文件失败(模拟) ✅
- 记录警告日志 ✅
- 返回OSS URL ✅
十、总结
10.1 改造核心
- 先本地后OSS:确保数据安全
- 成功删除本地:节省存储空间
- 失败保留本地:提供降级方案
- 异常处理完善:保证系统稳定
10.2 改造收益
- ✅ 提高数据安全性
- ✅ 提供降级方案
- ✅ 便于问题排查
- ✅ 不影响现有功能(有备份)
10.3 后续优化
- 定期清理机制
- 监控和告警
- 性能优化(如需要)
文档完成时间:2025年1月
文档状态:✅ 逻辑梳理完成,等待实施