This commit is contained in:
2026-05-09 21:17:56 +08:00
6 changed files with 160 additions and 28 deletions
@@ -0,0 +1,45 @@
using FileService.Application.Ports;
using IM.Commons.IntegrationEvents;
using MassTransit;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace FileService.Application.EventHandler
{
public class UploadTaskCompleteEventHandler : IConsumer<UploadTaskCompleteEvent>
{
private readonly IObjectStorageRouter router;
private readonly IStorageRedisCache cache;
public UploadTaskCompleteEventHandler(IObjectStorageRouter router, IStorageRedisCache cache)
{
this.router = router;
this.cache = cache;
}
public async Task Consume(ConsumeContext<UploadTaskCompleteEvent> context)
{
var @event = context.Message;
var storage = router.Route(@event.ProviderCode);
if(@event.ProviderCode == "Local")
{
await storage.CompleteUploadAsync(new StorageContracts.CompleteUploadCommand(
ProviderCode: @event.ProviderCode,
Bucket: @event.Bucket,
Region: @event.Region,
ObjectKey: @event.ObjectKey,
UploadSessionId: @event.SessionId,
Parts: @event.Parts.Select(s => new StorageContracts.UploadPart(
PartNumber: s.PartNumber,
ETag: s.ETag,
Size: s.Size,
Checksum: s.Checksum
)).ToList()
), context.CancellationToken);
}
}
}
}
@@ -4,7 +4,9 @@ using FileService.Application.StorageContracts;
using FileService.Domain.Entities; using FileService.Domain.Entities;
using FileService.Domain.IReposities; using FileService.Domain.IReposities;
using IM.Commons; using IM.Commons;
using IM.Commons.IntegrationEvents;
using IM.InitCommon; using IM.InitCommon;
using MassTransit;
using MassTransit.Internals; using MassTransit.Internals;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using System; using System;
@@ -17,13 +19,16 @@ namespace FileService.Application.UploadFileTask
{ {
public class UploadFileTaskService(IUploadTaskReposity reposity, public class UploadFileTaskService(IUploadTaskReposity reposity,
IMapper mapper, IObjectStorageRouter router, IMapper mapper, IObjectStorageRouter router,
IOptions<StorageOptions> options, IStorageRedisCache redis) IOptions<StorageOptions> options, IStorageRedisCache redis,
IPublishEndpoint endpoint
)
{ {
private readonly IUploadTaskReposity reposity = reposity; private readonly IUploadTaskReposity reposity = reposity;
private readonly IMapper mapper = mapper; private readonly IMapper mapper = mapper;
private readonly IObjectStorageRouter router = router; private readonly IObjectStorageRouter router = router;
private readonly IOptions<StorageOptions> options = options; private readonly IOptions<StorageOptions> options = options;
private readonly IStorageRedisCache redis = redis; private readonly IStorageRedisCache redis = redis;
private readonly IPublishEndpoint endpoint = endpoint;
private readonly IObjectStoragePort storage = router.Route(options.Value.DefaultProviderCode); private readonly IObjectStoragePort storage = router.Route(options.Value.DefaultProviderCode);
public async Task<Result<TaskInitResponse>> InitTaskAsync(UploadTaskInitCommand command) public async Task<Result<TaskInitResponse>> InitTaskAsync(UploadTaskInitCommand command)
@@ -34,9 +39,9 @@ namespace FileService.Application.UploadFileTask
var storageOption = options.Value.Providers[options.Value.DefaultProviderCode]; var storageOption = options.Value.Providers[options.Value.DefaultProviderCode];
var storage = router.Route(storageOption.ProviderCode); var storage = router.Route(storageOption.ProviderCode);
var initRes = await storage.InitUploadAsync(new StorageContracts.InitiateUploadCommand( var initRes = await storage.InitUploadAsync(new StorageContracts.InitiateUploadCommand(
ProviderCode: options.Value.ProviderCode, ProviderCode: storageOption.ProviderCode,
Bucket: options.Value.Bucket, Bucket: storageOption.Bucket,
ObjectKey: options.Value.Endpoint, ObjectKey: storageOption.Endpoint,
ContentType:task.ContentType.Value, ContentType:task.ContentType.Value,
ContentLength: command.FileSize, ContentLength: command.FileSize,
null null
@@ -95,20 +100,35 @@ namespace FileService.Application.UploadFileTask
} }
var task = await reposity.FindByIdAsync(Guid.Parse(taskCache.TaskId)); var task = await reposity.FindByIdAsync(Guid.Parse(taskCache.TaskId));
var res = await storage.CompleteUploadAsync(new CompleteUploadCommand( //var res = await storage.CompleteUploadAsync(new CompleteUploadCommand(
ProviderCode: taskCache.ProviderCode, // ProviderCode: taskCache.ProviderCode,
Bucket: taskCache.Bucket, // Bucket: taskCache.Bucket,
Region: taskCache.Region, // Region: taskCache.Region,
ObjectKey: taskCache.ObjectKey, // ObjectKey: taskCache.ObjectKey,
UploadSessionId: taskCache.UploadSessionId, // UploadSessionId: taskCache.UploadSessionId,
Parts: command.Parts // Parts: command.Parts
), cancellationToken); // ), cancellationToken);
task.CompleteUpload(new Domain.ValueObjects.StorageLocation( task.CompleteUpload(new Domain.ValueObjects.StorageLocation(
taskCache.ProviderCode, taskCache.Bucket, taskCache.ProviderCode, taskCache.Bucket,
taskCache.ObjectKey, taskCache.Region taskCache.ObjectKey, taskCache.Region
)); ));
await endpoint.Publish(new UploadTaskCompleteEvent()
{
Bucket = taskCache.Bucket,
FileName = task.FileName.ToString(),
ObjectKey = taskCache.ObjectKey,
Parts = command.Parts.Select(s =>
new IM.Commons.IntegrationEvents.UploadPart(
s.PartNumber, s.ETag, s.Size, s.Checksum)
).ToList(),
ProviderCode = taskCache.ProviderCode,
Region = taskCache.Region,
SessionId = command.UploadSessionId,
TaskId = task.Id
});
return Result.Success(mapper.Map<UploadTaskResponse>(task)); return Result.Success(mapper.Map<UploadTaskResponse>(task));
} }
} }
+1
View File
@@ -14,6 +14,7 @@ namespace FileService.Infrastructure
services.AddScoped<IUploadFileReposity, UploadFileReposity>(); services.AddScoped<IUploadFileReposity, UploadFileReposity>();
services.AddScoped<IUploadTaskReposity, UploadTaskReposity>(); services.AddScoped<IUploadTaskReposity, UploadTaskReposity>();
services.AddScoped<IObjectStoragePort, LocalStorageAdapter>(); services.AddScoped<IObjectStoragePort, LocalStorageAdapter>();
services.AddScoped<IRedisService, RedisCacheService>();
services.AddScoped<IObjectStorageRouter, ObjectStorageRouter>(); services.AddScoped<IObjectStorageRouter, ObjectStorageRouter>();
services.AddScoped<IStorageRedisCache,StorageCacheService>(); services.AddScoped<IStorageRedisCache,StorageCacheService>();
} }
@@ -3,27 +3,73 @@ using FileService.Application.StorageContracts;
using FileService.Domain.ValueObjects; using FileService.Domain.ValueObjects;
using IM.Commons; using IM.Commons;
using IM.InitCommon; using IM.InitCommon;
using MassTransit;
using MassTransit.Configuration;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using System; using StackExchange.Redis;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace FileService.Infrastructure.Storage namespace FileService.Infrastructure.Storage
{ {
public class LocalStorageAdapter(IRedisService redis, IOptions<StorageOptions> options) : IObjectStoragePort public class LocalStorageAdapter(IStorageRedisCache redis, IOptions<StorageOptions> options) : IObjectStoragePort
{ {
private readonly IRedisService redis = redis; private readonly IStorageRedisCache redis = redis;
private readonly IOptions<StorageOptions> options = options; private readonly IOptions<StorageOptions> options = options;
public string ProviderCode => "Local"; public string ProviderCode => "Local";
public Task<CompleteUploadResult> CompleteUploadAsync(CompleteUploadCommand command, CancellationToken token) public async Task<CompleteUploadResult> CompleteUploadAsync(CompleteUploadCommand command, CancellationToken token)
{ {
throw new NotImplementedException(); var res = await MergeAsync(command.UploadSessionId, command.ObjectKey, command.Parts);
return new CompleteUploadResult(new StorageLocation(
storageProvider: command.ProviderCode,
bucket: command.Bucket,
objectKey: command.ObjectKey,
region: command.Region
), null, command.Parts.Sum(x => x.Size).Value);
}
public async Task<Result<object>> MergeAsync(string sessionId, string objectKey, IReadOnlyList<UploadPart> parts)
{
var rootPath = options.Value.Providers[options.Value.DefaultProviderCode].LocalRootPath;
var tempPath = Path.Combine(rootPath, "temp"); // 项目根目录下 uploads // 最终文件存储路径(这里可以用你之前 ObjectNameGenerator 生成的名字)
var finalPath = Path.Combine(rootPath, objectKey);
var finalDir = Path.GetDirectoryName(finalPath);
Directory.CreateDirectory(finalDir);
var storageCache = await redis.GetAsync(sessionId);
var totalChunks = storageCache.TotalPartCount;
try
{
using (var finalStream = new FileStream(finalPath, FileMode.Create))
{
for (var i = 1; i <= totalChunks; i++)
{
var progress = (i * 100.0 / totalChunks);
if (i % 5 == 0 || i == totalChunks)
{
//await _redis.HashSetAsync(RedisKeys.MergeStatus(taskId), new HashEntry[]
//{
// new("status", "processing"),
// new("progress", progress.ToString("F2"))
//});
}
var chunkPath = Path.Combine(tempPath, $"{i}.part.tmp");
if (!File.Exists(chunkPath))
return Result.Fail(ResultCode.CHUNK_NOT_FOUND);
using (var chunkStream = new FileStream(chunkPath, FileMode.Open))
{
await chunkStream.CopyToAsync(finalStream);
}
}
Directory.Delete(tempPath, true);
await redis.DeleteAsync(sessionId);
}
return Result.Success();
}
catch (Exception e)
{
//_logger.LogError(e, e.Message);
throw;
}
} }
public async Task<PresignedUrl> GenerateUploadUrlAsync(GenerateUploadUrlCommand command, CancellationToken token) public async Task<PresignedUrl> GenerateUploadUrlAsync(GenerateUploadUrlCommand command, CancellationToken token)
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IM.Commons.IntegrationEvents
{
public class UploadTaskCompleteEvent
{
public Guid TaskId { get; set; }
public string SessionId { get; set; }
public string FileName { get; set; }
public string ProviderCode { get; set; }
public string Bucket { get; set; }
public string Region { get; set; }
public string ObjectKey { get; set; }
public IReadOnlyList<UploadPart> Parts { get; set; }
}
public sealed record UploadPart(
int PartNumber,
string ETag,
long? Size = null,
string? Checksum = null);
}
@@ -73,8 +73,6 @@ namespace MessageService.WebApi.Application.Message
{ {
// 2. 处理引用逻辑(如果传了 QuoteMessageId // 2. 处理引用逻辑(如果传了 QuoteMessageId
QuoteInfo? quote = null; QuoteInfo? quote = null;
if (command.QuoteMessageId.HasValue)
{
var originMsg = await reposity.FindByIdAsync(command.QuoteMessageId.Value); var originMsg = await reposity.FindByIdAsync(command.QuoteMessageId.Value);
if (originMsg != null) if (originMsg != null)
{ {
@@ -89,9 +87,6 @@ namespace MessageService.WebApi.Application.Message
} }
message.WithQuote(quote); message.WithQuote(quote);
} }
reposity.Create(message); reposity.Create(message);
return Result.Success(mapper.Map<MessageResponse>(message)); return Result.Success(mapper.Map<MessageResponse>(message));