# 文件存储 (16_OSS)

最后更新: 2024-09-20


# 📚 概述

DarkM 框架的文件存储模块提供了统一的对象存储服务接口。支持多种存储提供器(本地存储、七牛云、阿里云 OSS),实现一次开发,自由切换存储方案。

源代码位置: DarkM/src/Framework/OSS


# 🏗️ 模块架构

# 项目结构

OSS/
├── OSS.Abstractions/              # 抽象层(接口、配置)
│   ├── IFileStorageProvider.cs    # 文件存储接口
│   ├── OSSProvider.cs             # OSS 提供器枚举
│   ├── OSSConfig.cs               # OSS 配置类
│   ├── QiniuConfig.cs             # 七牛配置
│   └── AliyunConfig.cs            # 阿里云配置
│
├── OSS.Integration/               # 集成层(DI 注册)
│   └── ServiceCollectionExtensions.cs  # DI 扩展
│
├── OSS.Local/                     # 本地存储实现
│   └── LocalFileStorageProvider.cs     # 本地文件存储
│
├── OSS.Qiniu/                     # 七牛云实现
│   └── QiniuFileStorageProvider.cs     # 七牛文件存储
│
├── OSS.Aliyun/                    # 阿里云实现
│   └── AliyunFileStorageProvider.cs    # 阿里云 OSS 存储
│
└── OSS.Qiniu.SDK/                 # 七牛 SDK(封装)
    ├── Util/                      # 工具类
    │   ├── CRC32.cs
    │   ├── ETag.cs
    │   ├── Auth.cs
    │   └── Signature.cs
    └── Storage/                   # 存储类
        ├── UploadManager.cs       # 上传管理器
        ├── BucketManager.cs       # 空间管理
        └── ResumableUploader.cs   # 断点续传
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# 🔧 核心接口

# IFileStorageProvider(文件存储提供器)

/// <summary>
/// 文件存储提供器
/// </summary>
public interface IFileStorageProvider
{
    /// <summary>
    /// 上传文件到 OSS
    /// </summary>
    /// <param name="fileName">实际文件名</param>
    /// <param name="path">保存到 OSS 的路径</param>
    /// <param name="saveName">保存到 OSS 的文件名</param>
    /// <param name="moduleCode">数据所属模块编码</param>
    /// <param name="group">数据所属分组</param>
    /// <param name="contentOrUrl">内容或者内容对应的路径</param>
    /// <param name="isStringData">是否字符串数据(true 表示 contentOrUrl 为字符串数据,false 表示为文件路径)</param>
    /// <param name="accessMode">访问模式</param>
    /// <returns>上传结果</returns>
    ValueTask<bool> Upload(
        string fileName, 
        string path, 
        string saveName, 
        string moduleCode, 
        string group, 
        string contentOrUrl, 
        bool isStringData = true, 
        FileAccessMode accessMode = FileAccessMode.Open);

    /// <summary>
    /// 上传文件对象
    /// </summary>
    /// <param name="fileObject">文件对象</param>
    ValueTask<bool> Upload(FileObject fileObject);

    /// <summary>
    /// 删除文件
    /// </summary>
    /// <param name="fileObject">文件对象</param>
    ValueTask<bool> Delete(FileObject fileObject);

    /// <summary>
    /// 获取文件的物理绝对路径(主要用于本地存储)
    /// </summary>
    /// <param name="fullPath">完整路径</param>
    /// <param name="accessMode">访问模式</param>
    string GetPhysicalPath(string fullPath, FileAccessMode accessMode = FileAccessMode.Open);

    /// <summary>
    /// 获取完整的访问 URL
    /// </summary>
    /// <param name="fullPath">文件完整路径</param>
    /// <param name="accessMode">访问模式</param>
    string GetUrl(string fullPath, FileAccessMode accessMode = FileAccessMode.Open);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

# OSSProvider(OSS 提供器)

[Description("OSS 提供器")]
public enum OSSProvider
{
    /// <summary>
    /// 本地存储
    /// </summary>
    [Description("本地存储")]
    Local,
    
    /// <summary>
    /// 七牛云
    /// </summary>
    [Description("七牛")]
    Qiniu,
    
    /// <summary>
    /// 阿里云 OSS
    /// </summary>
    [Description("阿里云")]
    Aliyun
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# OSSConfig(OSS 配置)

public class OSSConfig
{
    /// <summary>
    /// OSS 提供器
    /// </summary>
    public OSSProvider Provider { get; set; } = OSSProvider.Local;

    /// <summary>
    /// 七牛配置
    /// </summary>
    public QiniuConfig Qiniu { get; set; } = new QiniuConfig();

    /// <summary>
    /// 阿里云配置
    /// </summary>
    public AliyunConfig Aliyun { get; set; } = new AliyunConfig();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# QiniuConfig(七牛配置)

public class QiniuConfig
{
    /// <summary>
    /// 访问 Key
    /// </summary>
    public string AccessKey { get; set; }

    /// <summary>
    /// 密钥
    /// </summary>
    public string SecretKey { get; set; }

    /// <summary>
    /// 域名
    /// </summary>
    public string Domain { get; set; }

    /// <summary>
    /// 空间名称
    /// </summary>
    public string Bucket { get; set; }

    /// <summary>
    /// 存储区域
    /// </summary>
    public QiniuZone Zone { get; set; }

    /// <summary>
    /// 令牌有效期(秒)
    /// </summary>
    public int TokenExpires { get; set; } = 7200;

    /// <summary>
    /// 配置检查
    /// </summary>
    public bool Check()
    {
        return AccessKey.NotNull() && SecretKey.NotNull() && Domain.NotNull();
    }
}

/// <summary>
/// 七牛存储区域
/// </summary>
public enum QiniuZone
{
    /// <summary>
    /// 华东(浙江)
    /// </summary>
    [Description("华东")]
    ZONE_CN_East,
    
    /// <summary>
    /// 华北(北京)
    /// </summary>
    [Description("华北")]
    ZONE_CN_North,
    
    /// <summary>
    /// 华南(广东)
    /// </summary>
    [Description("华南")]
    ZONE_CN_South,
    
    /// <summary>
    /// 北美(洛杉矶)
    /// </summary>
    [Description("北美")]
    ZONE_US_North
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

# AliyunConfig(阿里云配置)

public class AliyunConfig
{
    /// <summary>
    /// 域名(Endpoint)
    /// </summary>
    public string Endpoint { get; set; }

    /// <summary>
    /// 访问令牌 ID
    /// </summary>
    public string AccessKeyId { get; set; }

    /// <summary>
    /// 访问令牌密钥
    /// </summary>
    public string AccessKeySecret { get; set; }

    /// <summary>
    /// 存储空间名称(Bucket)
    /// </summary>
    public string BucketName { get; set; }

    /// <summary>
    /// 自定义域名
    /// </summary>
    public string Domain { get; set; }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 🏗️ 依赖注入

# 自动注册机制

// OSS.Integration/ServiceCollectionExtensions.cs
public static class ServiceCollectionExtensions
{
    /// <summary>
    /// 添加 OSS 功能
    /// </summary>
    public static IServiceCollection AddOSS(
        this IServiceCollection services, 
        IConfiguration cfg)
    {
        var config = new OSSConfig();
        var section = cfg.GetSection("OSS");
        if (section != null)
        {
            section.Bind(config);
        }

        // 自动修正域名(添加末尾斜杠)
        if (config.Qiniu != null && config.Qiniu.Domain.NotNull() 
            && !config.Qiniu.Domain.EndsWith("/"))
        {
            config.Qiniu.Domain += "/";
        }
        if (config.Aliyun != null && config.Aliyun.Domain.NotNull() 
            && !config.Aliyun.Domain.EndsWith("/"))
        {
            config.Aliyun.Domain += "/";
        }

        services.AddSingleton(config);

        // 根据配置加载对应的实现程序集
        var assembly = AssemblyHelper.LoadByNameEndString(
            $".Lib.OSS.{config.Provider.ToString()}");
        
        if (assembly == null)
            return services;

        // 注册文件存储提供器
        var providerType = assembly.GetTypes()
            .FirstOrDefault(m => typeof(IFileStorageProvider).IsAssignableFrom(m));
        if (providerType != null)
        {
            services.AddSingleton(typeof(IFileStorageProvider), providerType);
        }

        return services;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

# 💡 使用示例

# 1. 配置 OSS 模块

// appsettings.json
{
  "OSS": {
    "Provider": "Qiniu",
    "Qiniu": {
      "AccessKey": "your-access-key",
      "SecretKey": "your-secret-key",
      "Domain": "https://cdn.example.com",
      "Bucket": "your-bucket-name",
      "Zone": "ZONE_CN_East",
      "TokenExpires": 7200
    },
    "Aliyun": {
      "Endpoint": "oss-cn-hangzhou.aliyuncs.com",
      "AccessKeyId": "your-access-key-id",
      "AccessKeySecret": "your-access-key-secret",
      "BucketName": "your-bucket-name",
      "Domain": "https://cdn.example.com"
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 2. 注册服务

// Program.cs 或 Startup.cs
var builder = WebApplication.CreateBuilder(args);

// 添加 OSS 服务
builder.Services.AddOSS(builder.Configuration);

var app = builder.Build();
1
2
3
4
5
6
7

# 3. 上传文件(字符串内容)

public class FileUploadService
{
    private readonly IFileStorageProvider _storageProvider;
    private readonly ILogger<FileUploadService> _logger;

    public FileUploadService(
        IFileStorageProvider storageProvider,
        ILogger<FileUploadService> logger)
    {
        _storageProvider = storageProvider;
        _logger = logger;
    }

    /// <summary>
    /// 上传 Base64 图片
    /// </summary>
    public async Task<string> UploadBase64Image(string base64Data, string moduleCode, string group)
    {
        // 生成文件名
        var fileName = $"{Guid.NewGuid()}.jpg";
        var path = $"/{moduleCode}/{group}/{DateTime.Now:yyyyMMdd}";
        var saveName = fileName;

        // 上传
        var success = await _storageProvider.Upload(
            fileName: fileName,
            path: path,
            saveName: saveName,
            moduleCode: moduleCode,
            group: group,
            contentOrUrl: base64Data,
            isStringData: true  // 表示 contentOrUrl 是字符串数据
        );

        if (!success)
            throw new Exception("文件上传失败");

        // 返回完整 URL
        var fullPath = $"{path}/{saveName}";
        return _storageProvider.GetUrl(fullPath);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

# 4. 上传文件(文件对象)

public class FileUploadController : ModuleController
{
    private readonly IFileStorageProvider _storageProvider;

    public FileUploadController(IFileStorageProvider storageProvider)
    {
        _storageProvider = storageProvider;
    }

    /// <summary>
    /// 上传单个文件
    /// </summary>
    [HttpPost("upload")]
    public async Task<IResultModel> UploadFile(
        [FromForm] IFormFile file,
        [FromForm] string moduleCode = "common",
        [FromForm] string group = "default")
    {
        if (file == null || file.Length == 0)
            return ResultModel.Error("请选择要上传的文件");

        // 构建文件对象
        var fileObject = new FileObject
        {
            FileName = file.FileName,
            OriginalName = file.FileName,
            ContentType = file.ContentType,
            Size = file.Length,
            ModuleCode = moduleCode,
            Group = group,
            Path = $"/{moduleCode}/{group}/{DateTime.Now:yyyyMMdd}"
        };

        // 读取文件内容
        using var stream = file.OpenReadStream();
        using var reader = new StreamReader(stream);
        fileObject.Content = await reader.ReadToEndAsync();

        // 上传
        var success = await _storageProvider.Upload(fileObject);

        if (!success)
            return ResultModel.Error("文件上传失败");

        // 返回文件信息
        var fullPath = $"{fileObject.Path}/{fileObject.SaveName}";
        return ResultModel.Success(new
        {
            fileName = fileObject.SaveName,
            originalName = fileObject.OriginalName,
            size = fileObject.Size,
            url = _storageProvider.GetUrl(fullPath),
            fullPath = fullPath
        });
    }

    /// <summary>
    /// 批量上传文件
    /// </summary>
    [HttpPost("upload-batch")]
    public async Task<IResultModel> UploadBatch(
        [FromForm] List<IFormFile> files,
        [FromForm] string moduleCode = "common",
        [FromForm] string group = "default")
    {
        var results = new List<object>();

        foreach (var file in files)
        {
            var fileObject = new FileObject
            {
                FileName = file.FileName,
                OriginalName = file.FileName,
                ContentType = file.ContentType,
                Size = file.Length,
                ModuleCode = moduleCode,
                Group = group,
                Path = $"/{moduleCode}/{group}/{DateTime.Now:yyyyMMdd}"
            };

            using var stream = file.OpenReadStream();
            using var reader = new StreamReader(stream);
            fileObject.Content = await reader.ReadToEndAsync();

            var success = await _storageProvider.Upload(fileObject);

            if (success)
            {
                var fullPath = $"{fileObject.Path}/{fileObject.SaveName}";
                results.Add(new
                {
                    fileName = fileObject.SaveName,
                    originalName = fileObject.OriginalName,
                    url = _storageProvider.GetUrl(fullPath)
                });
            }
        }

        return ResultModel.Success(results);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101

# 5. 删除文件

public class FileManagementService
{
    private readonly IFileStorageProvider _storageProvider;

    public FileManagementService(IFileStorageProvider storageProvider)
    {
        _storageProvider = storageProvider;
    }

    /// <summary>
    /// 删除文件
    /// </summary>
    public async Task<bool> DeleteFile(string fullPath)
    {
        var fileObject = new FileObject
        {
            FullPath = fullPath
        };

        return await _storageProvider.Delete(fileObject);
    }

    /// <summary>
    /// 批量删除文件
    /// </summary>
    public async Task<int> DeleteBatch(List<string> fullPaths)
    {
        var deletedCount = 0;

        foreach (var fullPath in fullPaths)
        {
            try
            {
                var success = await DeleteFile(fullPath);
                if (success)
                    deletedCount++;
            }
            catch (Exception ex)
            {
                // 记录日志
                _logger.LogError(ex, "删除文件失败:{FullPath}", fullPath);
            }
        }

        return deletedCount;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

# 6. 获取文件 URL

public class FileUrlService
{
    private readonly IFileStorageProvider _storageProvider;

    public FileUrlService(IFileStorageProvider storageProvider)
    {
        _storageProvider = storageProvider;
    }

    /// <summary>
    /// 获取文件访问 URL
    /// </summary>
    public string GetFileUrl(string fullPath)
    {
        return _storageProvider.GetUrl(fullPath);
    }

    /// <summary>
    /// 获取文件物理路径(仅本地存储有效)
    /// </summary>
    public string GetPhysicalPath(string fullPath)
    {
        return _storageProvider.GetPhysicalPath(fullPath);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 🔧 存储提供器对比

# OSS.Local(本地存储)

特点:

  • ✅ 无需外部服务,部署简单
  • ✅ 零成本
  • ✅ 适合开发测试环境
  • ⚠️ 需要自行处理备份和 CDN
  • ⚠️ 不适合大规模生产环境

适用场景:

  • 开发环境
  • 内部系统
  • 小规模应用

# OSS.Qiniu(七牛云)

特点:

  • ✅ 国内 CDN 覆盖广
  • ✅ 价格亲民
  • ✅ 支持断点续传
  • ✅ 支持图片处理(缩放、水印等)
  • ⚠️ 免费额度有限(10GB 存储 +10GB 流量/月)

适用场景:

  • 中小企业应用
  • 图片/视频存储
  • 需要 CDN 加速

# OSS.Aliyun(阿里云 OSS)

特点:

  • ✅ 阿里云生态集成
  • ✅ 高可靠性(99.9999999999%)
  • ✅ 支持多种存储类型(标准/低频/归档)
  • ✅ 完善的安全控制
  • ⚠️ 价格相对较高

适用场景:

  • 大型企业应用
  • 高可靠性要求
  • 已在阿里云部署

# 💡 最佳实践

# 1. 文件命名规范

public class FileNameGenerator
{
    /// <summary>
    /// 生成唯一文件名
    /// </summary>
    public static string GenerateFileName(string originalFileName)
    {
        var extension = Path.GetExtension(originalFileName);
        var guid = Guid.NewGuid().ToString("N");
        var timestamp = DateTimeOffset.Now.ToUnixTimeSeconds();
        
        return $"{timestamp}_{guid}{extension}";
    }

    /// <summary>
    /// 生成存储路径(按日期分目录)
    /// </summary>
    public static string GeneratePath(string moduleCode, string group)
    {
        var date = DateTime.Now.ToString("yyyyMMdd");
        return $"/{moduleCode}/{group}/{date}";
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 2. 文件类型限制

public class FileUploadValidator
{
    private static readonly Dictionary<string, long> AllowedTypes = new()
    {
        // 图片
        { ".jpg", 10 * 1024 * 1024 },      // 10MB
        { ".jpeg", 10 * 1024 * 1024 },
        { ".png", 10 * 1024 * 1024 },
        { ".gif", 5 * 1024 * 1024 },       // 5MB
        
        // 文档
        { ".pdf", 50 * 1024 * 1024 },      // 50MB
        { ".doc", 50 * 1024 * 1024 },
        { ".docx", 50 * 1024 * 1024 },
        { ".xls", 50 * 1024 * 1024 },
        { ".xlsx", 50 * 1024 * 1024 },
        
        // 视频
        { ".mp4", 500 * 1024 * 1024 },     // 500MB
        { ".avi", 500 * 1024 * 1024 },
        { ".mov", 500 * 1024 * 1024 }
    };

    public static (bool isValid, string message) Validate(IFormFile file)
    {
        var extension = Path.GetExtension(file.FileName).ToLower();
        
        if (!AllowedTypes.ContainsKey(extension))
            return (false, $"不支持的文件类型:{extension}");
        
        if (file.Length > AllowedTypes[extension])
            return (false, $"文件大小超过限制({AllowedTypes[extension] / 1024 / 1024}MB)");
        
        return (true, "验证通过");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

# 3. 上传进度跟踪

public class UploadProgressService
{
    private readonly IMemoryCache _cache;

    public UploadProgressService(IMemoryCache cache)
    {
        _cache = cache;
    }

    public void ReportProgress(string uploadId, int percent)
    {
        _cache.Set(uploadId, percent, TimeSpan.FromMinutes(30));
    }

    public int GetProgress(string uploadId)
    {
        return _cache.TryGetValue(uploadId, out int percent) ? percent : 0;
    }

    public void Complete(string uploadId)
    {
        _cache.Remove(uploadId);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 4. 敏感文件加密存储

public class EncryptedFileStorage
{
    private readonly IFileStorageProvider _storageProvider;
    private readonly IEncryptionService _encryptionService;

    public EncryptedFileStorage(
        IFileStorageProvider storageProvider,
        IEncryptionService encryptionService)
    {
        _storageProvider = storageProvider;
        _encryptionService = encryptionService;
    }

    public async Task<string> UploadEncrypted(
        string content, 
        string moduleCode, 
        string group)
    {
        // 加密内容
        var encryptedContent = _encryptionService.Encrypt(content);
        
        // 上传加密后的内容
        var fileName = FileNameGenerator.GenerateFileName("encrypted.dat");
        var path = FileNameGenerator.GeneratePath(moduleCode, group);
        
        await _storageProvider.Upload(
            fileName: fileName,
            path: path,
            saveName: fileName,
            moduleCode: moduleCode,
            group: group,
            contentOrUrl: encryptedContent,
            isStringData: true
        );
        
        return $"{path}/{fileName}";
    }

    public async Task<string> DownloadDecrypted(string fullPath)
    {
        // 下载加密内容(需要实现下载接口)
        var encryptedContent = await DownloadContent(fullPath);
        
        // 解密
        return _encryptionService.Decrypt(encryptedContent);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

# 🔍 常见问题

# Q1: 如何选择存储提供器?

场景 推荐提供器 理由
开发测试 Local 零成本、部署简单
中小企业 Qiniu 性价比高、CDN 覆盖好
大型企业 Aliyun 高可靠、阿里云生态
已有阿里云 Aliyun 内网传输、生态集成
图片/视频为主 Qiniu 图片处理功能强

# Q2: 七牛云 Token 过期如何处理?

public class QiniuTokenService
{
    private readonly OSSConfig _config;
    private readonly IMemoryCache _cache;

    public QiniuTokenService(
        IOptions<OSSConfig> config,
        IMemoryCache cache)
    {
        _config = config.Value;
        _cache = cache;
    }

    public string GetUploadToken()
    {
        return _cache.GetOrCreate("qiniu_upload_token", entry =>
        {
            entry.AbsoluteExpirationRelativeToNow = 
                TimeSpan.FromSeconds(_config.Qiniu.TokenExpires - 600); // 提前 10 分钟过期
            
            // 生成上传凭证
            var putPolicy = new PutPolicy(_config.Qiniu.Bucket);
            var auth = new Auth(_config.Qiniu.AccessKey, _config.Qiniu.SecretKey);
            return auth.CreateUploadToken(putPolicy.ToJsonString());
        });
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# Q3: 如何实现断点续传?

public class ResumableUploadService
{
    private readonly IFileStorageProvider _storageProvider;
    private readonly string _tempPath;

    public ResumableUploadService(
        IFileStorageProvider storageProvider,
        IOptions<OSSConfig> config)
    {
        _storageProvider = storageProvider;
        _tempPath = config.Value.Qiniu.TempPath ?? "/temp/upload";
    }

    /// <summary>
    /// 分片上传初始化
    /// </summary>
    public string InitMultipartUpload(string fileName, long fileSize)
    {
        var uploadId = Guid.NewGuid().ToString("N");
        var tempFile = Path.Combine(_tempPath, uploadId);
        
        // 记录上传信息
        _cache.Set(uploadId, new UploadInfo
        {
            FileName = fileName,
            FileSize = fileSize,
            TempFile = tempFile,
            UploadedChunks = new List<int>()
        });
        
        return uploadId;
    }

    /// <summary>
    /// 上传分片
    /// </summary>
    public async Task UploadChunk(string uploadId, int chunkIndex, byte[] chunkData)
    {
        var info = _cache.Get<UploadInfo>(uploadId);
        var tempFile = info.TempFile;
        
        // 追加写入临时文件
        await File.AppendAllBytesAsync(tempFile, chunkData);
        info.UploadedChunks.Add(chunkIndex);
    }

    /// <summary>
    /// 完成上传
    /// </summary>
    public async Task CompleteUpload(string uploadId, string moduleCode, string group)
    {
        var info = _cache.Get<UploadInfo>(uploadId);
        
        // 上传完整文件
        var content = await File.ReadAllBytesAsync(info.TempFile);
        var base64 = Convert.ToBase64String(content);
        
        await _storageProvider.Upload(
            fileName: info.FileName,
            path: $"/{moduleCode}/{group}",
            saveName: info.FileName,
            moduleCode: moduleCode,
            group: group,
            contentOrUrl: base64,
            isStringData: true
        );
        
        // 清理临时文件
        File.Delete(info.TempFile);
        _cache.Remove(uploadId);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72

# 📚 相关文档


# 🔗 参考链接


最后更新: 2024-09-20