fix: align backend APIs and upload flow
This commit is contained in:
@@ -16,22 +16,33 @@ namespace FileService.Application.EventHandler
|
|||||||
private readonly IObjectStorageRouter router;
|
private readonly IObjectStorageRouter router;
|
||||||
private readonly IUploadFileReposity uploadFile;
|
private readonly IUploadFileReposity uploadFile;
|
||||||
private readonly IUnitOfWork uwork;
|
private readonly IUnitOfWork uwork;
|
||||||
private readonly IStorageRedisCache storageCache;
|
private readonly IUploadTaskReposity uploadTask;
|
||||||
|
|
||||||
public UploadTaskCompleteEventHandler(IObjectStorageRouter router, IUploadFileReposity uploadFile, IUnitOfWork uwork, IStorageRedisCache storageCache)
|
public UploadTaskCompleteEventHandler(IObjectStorageRouter router, IUploadFileReposity uploadFile, IUploadTaskReposity uploadTask, IUnitOfWork uwork)
|
||||||
{
|
{
|
||||||
this.router = router;
|
this.router = router;
|
||||||
this.uploadFile = uploadFile;
|
this.uploadFile = uploadFile;
|
||||||
this.uwork = uwork;
|
this.uwork = uwork;
|
||||||
this.storageCache = storageCache;
|
this.uploadTask = uploadTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Consume(ConsumeContext<UploadTaskCompleteEvent> context)
|
public async Task Consume(ConsumeContext<UploadTaskCompleteEvent> context)
|
||||||
{
|
{
|
||||||
var @event = context.Message;
|
var @event = context.Message;
|
||||||
|
var existingFile = await uploadFile.FindBySourceTaskIdAsync(@event.TaskId);
|
||||||
|
if (existingFile != null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var task = await uploadTask.FindByIdAsync(@event.TaskId);
|
||||||
|
if (task is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Upload task {@event.TaskId} has not been committed yet.");
|
||||||
|
}
|
||||||
|
|
||||||
var storage = router.Route(@event.ProviderCode);
|
var storage = router.Route(@event.ProviderCode);
|
||||||
var taskCache = await storageCache.GetAsync(@event.SessionId);
|
try
|
||||||
if(@event.ProviderCode == "Local")
|
|
||||||
{
|
{
|
||||||
await storage.CompleteUploadAsync(new StorageContracts.CompleteUploadCommand(
|
await storage.CompleteUploadAsync(new StorageContracts.CompleteUploadCommand(
|
||||||
ProviderCode: @event.ProviderCode,
|
ProviderCode: @event.ProviderCode,
|
||||||
@@ -46,14 +57,27 @@ namespace FileService.Application.EventHandler
|
|||||||
Checksum: s.Checksum
|
Checksum: s.Checksum
|
||||||
)).ToList()
|
)).ToList()
|
||||||
), context.CancellationToken);
|
), context.CancellationToken);
|
||||||
uploadFile.Create(new Domain.Entities.UploadFile(
|
var file = new Domain.Entities.UploadFile(
|
||||||
ownerId: @event.OperatorId,
|
ownerId: @event.OperatorId,
|
||||||
fileName: @event.FileName,
|
fileName: @event.FileName,
|
||||||
fileSize: taskCache.FileSize,
|
fileSize: @event.FileSize,
|
||||||
contentType: @event.ContentType,
|
contentType: @event.ContentType,
|
||||||
new Domain.ValueObjects.StorageLocation(taskCache.ProviderCode, taskCache.Bucket, taskCache.ObjectKey, taskCache.Region),
|
new Domain.ValueObjects.StorageLocation(@event.ProviderCode, @event.Bucket, @event.ObjectKey, @event.Region),
|
||||||
checkSum: new Domain.ValueObjects.CheckSum("md5", @event.CheckSun)
|
checkSum: new Domain.ValueObjects.CheckSum("md5", @event.CheckSun),
|
||||||
));
|
sourceTaskId: @event.TaskId,
|
||||||
|
chatType: @event.ChatType,
|
||||||
|
targetId: @event.TargetId,
|
||||||
|
isPublic: false
|
||||||
|
);
|
||||||
|
uploadFile.Create(file);
|
||||||
|
task.CompleteUpload(file.Id);
|
||||||
|
await uwork.SaveChangesAsync(context.CancellationToken);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
task.Fail(ex.Message);
|
||||||
|
await uwork.SaveChangesAsync(context.CancellationToken);
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ namespace FileService.Application.StorageContracts
|
|||||||
|
|
||||||
public sealed record PresignedUrl(
|
public sealed record PresignedUrl(
|
||||||
string Url,
|
string Url,
|
||||||
|
string Method,
|
||||||
IReadOnlyDictionary<string, string> Headers,
|
IReadOnlyDictionary<string, string> Headers,
|
||||||
DateTimeOffset ExpiresAt);
|
DateTimeOffset ExpiresAt);
|
||||||
|
|
||||||
|
|||||||
@@ -12,12 +12,14 @@ namespace FileService.Application.UploadFile
|
|||||||
{
|
{
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
public Guid OwnerId { get; set; }
|
public Guid OwnerId { get; set; }
|
||||||
public FileName FileName { get; set; }
|
public string FileName { get; set; }
|
||||||
public long FileSize { get; set; }
|
public long FileSize { get; set; }
|
||||||
public ContentType ContentType { get; set; }
|
public string ContentType { get; set; }
|
||||||
public FileState State { get; set; }
|
public FileState State { get; set; }
|
||||||
public StorageLocation StorageLocation { get; set; }
|
public string CheckSum { get; set; }
|
||||||
public CheckSum CheckSum { get; set; }
|
public string? ChatType { get; set; }
|
||||||
|
public Guid? TargetId { get; set; }
|
||||||
|
public bool IsPublic { get; set; }
|
||||||
public DateTimeOffset Created { get; set; }
|
public DateTimeOffset Created { get; set; }
|
||||||
public DateTimeOffset Updated { get; set; }
|
public DateTimeOffset Updated { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using System.Net.Http.Json;
|
||||||
|
|
||||||
|
namespace FileService.Application.UploadFile
|
||||||
|
{
|
||||||
|
public interface IGroupAccessService
|
||||||
|
{
|
||||||
|
Task<bool> CheckMemberAsync(Guid userId, Guid groupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GroupAccessService(HttpClient httpClient) : IGroupAccessService
|
||||||
|
{
|
||||||
|
public async Task<bool> CheckMemberAsync(Guid userId, Guid groupId)
|
||||||
|
{
|
||||||
|
var result = await httpClient.GetFromJsonAsync<IM.Commons.Result<bool>>(
|
||||||
|
$"api/groupmember/checkmember?userId={userId}&groupId={groupId}");
|
||||||
|
return result?.Succeeded == true && result.Data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,8 +12,11 @@ namespace FileService.Application.UploadFile
|
|||||||
public UploadFileMapperConfig()
|
public UploadFileMapperConfig()
|
||||||
{
|
{
|
||||||
CreateMap<Domain.Entities.UploadFile, FileResponse>()
|
CreateMap<Domain.Entities.UploadFile, FileResponse>()
|
||||||
|
.ForMember(dest => dest.FileName, opt => opt.MapFrom(src => src.FileName.Value))
|
||||||
|
.ForMember(dest => dest.ContentType, opt => opt.MapFrom(src => src.ContentType.Value))
|
||||||
|
.ForMember(dest => dest.CheckSum, opt => opt.MapFrom(src => src.CheckSum.Value))
|
||||||
.ForMember(dest => dest.Created, opt => opt.MapFrom(src => src.CreationTime))
|
.ForMember(dest => dest.Created, opt => opt.MapFrom(src => src.CreationTime))
|
||||||
.ForMember(dest => dest.Updated, opt => opt.MapFrom(src => src.ModificationTime))
|
.ForMember(dest => dest.Updated, opt => opt.MapFrom(src => src.ModificationTime ?? src.CreationTime))
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using FileService.Domain.ValueObjects;
|
|||||||
using IM.Commons;
|
using IM.Commons;
|
||||||
using IM.InitCommon;
|
using IM.InitCommon;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
namespace FileService.Application.UploadFile
|
namespace FileService.Application.UploadFile
|
||||||
{
|
{
|
||||||
@@ -15,27 +16,35 @@ namespace FileService.Application.UploadFile
|
|||||||
private readonly IMapper mapper;
|
private readonly IMapper mapper;
|
||||||
private readonly IObjectStorageRouter router;
|
private readonly IObjectStorageRouter router;
|
||||||
private readonly IOptions<StorageOptions> options;
|
private readonly IOptions<StorageOptions> options;
|
||||||
|
private readonly IGroupAccessService groupAccessService;
|
||||||
|
|
||||||
public UploadFileService(IUploadFileReposity reposity, IMapper mapper,
|
public UploadFileService(IUploadFileReposity reposity, IMapper mapper,
|
||||||
IObjectStorageRouter router, IOptions<StorageOptions> options)
|
IObjectStorageRouter router, IOptions<StorageOptions> options,
|
||||||
|
IGroupAccessService groupAccessService)
|
||||||
{
|
{
|
||||||
this.reposity = reposity;
|
this.reposity = reposity;
|
||||||
this.mapper = mapper;
|
this.mapper = mapper;
|
||||||
this.router = router;
|
this.router = router;
|
||||||
this.options = options;
|
this.options = options;
|
||||||
|
this.groupAccessService = groupAccessService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Result<FileResponse>> GetFileInfoAsync(Guid id)
|
public async Task<Result<FileResponse>> GetFileInfoAsync(Guid id, Guid requesterId)
|
||||||
{
|
{
|
||||||
var file = await reposity.FindByIdAsync(id);
|
var file = await reposity.FindByIdAsync(id);
|
||||||
if (file == null)
|
if (file == null)
|
||||||
{
|
{
|
||||||
return Result.Fail<FileResponse>(ResultCode.FILE_NOT_FOUND);
|
return Result.Fail<FileResponse>(ResultCode.FILE_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
if (!await CanAccessAsync(file, requesterId))
|
||||||
|
{
|
||||||
|
return Result.Fail<FileResponse>(ResultCode.PERMISSION_DENIED);
|
||||||
|
}
|
||||||
|
|
||||||
var response = mapper.Map<FileResponse>(file);
|
var response = mapper.Map<FileResponse>(file);
|
||||||
response.Url = router.Route(file.StorageLocation.StorageProvider)
|
response.Url = router.Route(file.StorageLocation.StorageProvider)
|
||||||
.GetPublicUrl(file.StorageLocation);
|
.GetPublicUrl(file.StorageLocation);
|
||||||
|
response.IsPublic = response.IsPublic || response.Url != null;
|
||||||
return Result.Success(response);
|
return Result.Success(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,15 +54,29 @@ namespace FileService.Application.UploadFile
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<Result<FileResponse>> SimpleUploadAsync(SimpleUploadCommand command, CancellationToken token = default)
|
public async Task<Result<FileResponse>> SimpleUploadAsync(SimpleUploadCommand command, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
// 秒传:相同 checksum 的文件已存在则直接返回已有记录
|
var checksum = command.CheckSum;
|
||||||
if (!string.IsNullOrEmpty(command.CheckSum))
|
if (string.IsNullOrWhiteSpace(checksum))
|
||||||
{
|
{
|
||||||
var existing = await reposity.FindByCheckSumGlobalAsync("md5", command.CheckSum);
|
var hash = await MD5.HashDataAsync(command.Content, token);
|
||||||
if (existing != null)
|
checksum = Convert.ToHexString(hash).ToLowerInvariant();
|
||||||
|
if (command.Content.CanSeek)
|
||||||
|
{
|
||||||
|
command.Content.Position = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 秒传:相同 checksum 的文件已存在则直接返回已有记录
|
||||||
|
if (!string.IsNullOrEmpty(checksum))
|
||||||
|
{
|
||||||
|
var existing = await reposity.FindByCheckSumGlobalAsync("md5", checksum);
|
||||||
|
var existingPublicUrl = existing == null
|
||||||
|
? null
|
||||||
|
: router.Route(existing.StorageLocation.StorageProvider).GetPublicUrl(existing.StorageLocation);
|
||||||
|
if (existing != null && (existingPublicUrl != null ||
|
||||||
|
(!command.IsPublic && existing.OwnerId == command.OwnerId)))
|
||||||
{
|
{
|
||||||
var hit = mapper.Map<FileResponse>(existing);
|
var hit = mapper.Map<FileResponse>(existing);
|
||||||
var hitStorage = router.Route(existing.StorageLocation.StorageProvider);
|
hit.Url = existingPublicUrl;
|
||||||
hit.Url = hitStorage.GetPublicUrl(existing.StorageLocation);
|
hit.IsPublic = hit.IsPublic || hit.Url != null;
|
||||||
return Result.Success(hit);
|
return Result.Success(hit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,7 +107,8 @@ namespace FileService.Application.UploadFile
|
|||||||
fileSize: command.FileSize,
|
fileSize: command.FileSize,
|
||||||
contentType: command.ContentType,
|
contentType: command.ContentType,
|
||||||
storageLocation: location,
|
storageLocation: location,
|
||||||
checkSum: new CheckSum("md5", command.CheckSum ?? string.Empty));
|
checkSum: new CheckSum("md5", checksum),
|
||||||
|
isPublic: command.IsPublic);
|
||||||
|
|
||||||
reposity.Create(file);
|
reposity.Create(file);
|
||||||
|
|
||||||
@@ -103,6 +127,10 @@ namespace FileService.Application.UploadFile
|
|||||||
{
|
{
|
||||||
return Result.Fail<FileDownload>(ResultCode.FILE_NOT_FOUND);
|
return Result.Fail<FileDownload>(ResultCode.FILE_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
if (!await CanAccessAsync(file, requesterId))
|
||||||
|
{
|
||||||
|
return Result.Fail<FileDownload>(ResultCode.PERMISSION_DENIED);
|
||||||
|
}
|
||||||
|
|
||||||
var stream = await router.Route(file.StorageLocation.StorageProvider)
|
var stream = await router.Route(file.StorageLocation.StorageProvider)
|
||||||
.OpenReadAsync(file.StorageLocation, token);
|
.OpenReadAsync(file.StorageLocation, token);
|
||||||
@@ -113,17 +141,33 @@ namespace FileService.Application.UploadFile
|
|||||||
file.FileName.Value));
|
file.FileName.Value));
|
||||||
}
|
}
|
||||||
|
|
||||||
// FileName 值对象限制 20 字符;原始名超长时安全截断(保留扩展名),真实文件名由 objectKey 保证唯一。
|
private async Task<bool> CanAccessAsync(Domain.Entities.UploadFile file, Guid requesterId)
|
||||||
|
{
|
||||||
|
var publicUrl = router.Route(file.StorageLocation.StorageProvider).GetPublicUrl(file.StorageLocation);
|
||||||
|
if (file.IsPublic || publicUrl != null || file.OwnerId == requesterId) return true;
|
||||||
|
if (string.Equals(file.ChatType, "PRIVATE", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return file.TargetId == requesterId;
|
||||||
|
}
|
||||||
|
if (string.Equals(file.ChatType, "GROUP", StringComparison.OrdinalIgnoreCase) && file.TargetId.HasValue)
|
||||||
|
{
|
||||||
|
return await groupAccessService.CheckMemberAsync(requesterId, file.TargetId.Value);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件名超长时安全截断(保留扩展名),真实存储键由 objectKey 保证唯一。
|
||||||
private static FileName SafeFileName(string fileName)
|
private static FileName SafeFileName(string fileName)
|
||||||
{
|
{
|
||||||
if (fileName.Length <= 20)
|
const int maxFileNameLength = 255;
|
||||||
|
if (fileName.Length <= maxFileNameLength)
|
||||||
{
|
{
|
||||||
return new FileName(fileName);
|
return new FileName(fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
var ext = Path.GetExtension(fileName);
|
var ext = Path.GetExtension(fileName);
|
||||||
var stem = Path.GetFileNameWithoutExtension(fileName);
|
var stem = Path.GetFileNameWithoutExtension(fileName);
|
||||||
var keep = Math.Max(0, 20 - ext.Length);
|
var keep = Math.Max(0, maxFileNameLength - ext.Length);
|
||||||
return new FileName(stem[..Math.Min(stem.Length, keep)] + ext);
|
return new FileName(stem[..Math.Min(stem.Length, keep)] + ext);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,5 +13,10 @@ namespace FileService.Application.UploadFileTask
|
|||||||
public string UploadSessionId { get; init; }
|
public string UploadSessionId { get; init; }
|
||||||
|
|
||||||
public StorageLocation StorageLocation { get; init; }
|
public StorageLocation StorageLocation { get; init; }
|
||||||
|
public bool Instant { get; init; }
|
||||||
|
public string UploadMode { get; init; } = "LocalMultipart";
|
||||||
|
public int TotalPartCount { get; init; }
|
||||||
|
public long PartSizeBytes { get; init; }
|
||||||
|
public global::FileService.Application.UploadFile.FileResponse? File { get; init; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,14 +33,22 @@ namespace FileService.Application.UploadFileTask
|
|||||||
|
|
||||||
// 秒传:相同 checksum 的文件若已存在于已完成文件表,直接返回已有记录
|
// 秒传:相同 checksum 的文件若已存在于已完成文件表,直接返回已有记录
|
||||||
var existingFile = await uploadFileReposity.FindByCheckSumGlobalAsync("md5", command.checkSum);
|
var existingFile = await uploadFileReposity.FindByCheckSumGlobalAsync("md5", command.checkSum);
|
||||||
if (existingFile != null)
|
if (existingFile != null && CanReuse(existingFile, command))
|
||||||
{
|
{
|
||||||
var storageForResponse = router.Route(existingFile.StorageLocation.StorageProvider);
|
var storageForResponse = router.Route(existingFile.StorageLocation.StorageProvider);
|
||||||
|
var fileResponse = mapper.Map<UploadFile.FileResponse>(existingFile);
|
||||||
|
fileResponse.Url = storageForResponse.GetPublicUrl(existingFile.StorageLocation);
|
||||||
|
fileResponse.IsPublic = fileResponse.IsPublic || fileResponse.Url != null;
|
||||||
return Result.Success(new TaskInitResponse
|
return Result.Success(new TaskInitResponse
|
||||||
{
|
{
|
||||||
TaskId = existingFile.Id,
|
TaskId = existingFile.Id,
|
||||||
UploadSessionId = existingFile.Id.ToString(),
|
UploadSessionId = existingFile.Id.ToString(),
|
||||||
StorageLocation = existingFile.StorageLocation
|
StorageLocation = existingFile.StorageLocation,
|
||||||
|
Instant = true,
|
||||||
|
UploadMode = "Instant",
|
||||||
|
TotalPartCount = 0,
|
||||||
|
PartSizeBytes = 0,
|
||||||
|
File = fileResponse
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,6 +78,18 @@ namespace FileService.Application.UploadFileTask
|
|||||||
|
|
||||||
var res = mapper.Map<TaskInitResponse>(initRes);
|
var res = mapper.Map<TaskInitResponse>(initRes);
|
||||||
res.TaskId = task.Id;
|
res.TaskId = task.Id;
|
||||||
|
res = new TaskInitResponse
|
||||||
|
{
|
||||||
|
TaskId = task.Id,
|
||||||
|
UploadSessionId = initRes.UploadSessionId,
|
||||||
|
StorageLocation = initRes.Location,
|
||||||
|
Instant = false,
|
||||||
|
UploadMode = string.Equals(storage.ProviderCode, "Local", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? "LocalMultipart"
|
||||||
|
: "Presigned",
|
||||||
|
TotalPartCount = totalPartCount,
|
||||||
|
PartSizeBytes = storageOption.DefaultPartSizeBytes
|
||||||
|
};
|
||||||
|
|
||||||
task.StartUpload();
|
task.StartUpload();
|
||||||
reposity.Create(task);
|
reposity.Create(task);
|
||||||
@@ -97,6 +117,12 @@ namespace FileService.Application.UploadFileTask
|
|||||||
return Result.Fail<PresignedUrl>(ResultCode.CHUNK_NOT_FOUND);
|
return Result.Fail<PresignedUrl>(ResultCode.CHUNK_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var task = await reposity.FindByIdAsync(Guid.Parse(taskCache.TaskId));
|
||||||
|
if (task is null || task.UploaderId != userId)
|
||||||
|
{
|
||||||
|
return Result.Fail<PresignedUrl>(ResultCode.PERMISSION_DENIED);
|
||||||
|
}
|
||||||
|
|
||||||
if (taskCache.TotalPartCount < partNum || partNum < 1)
|
if (taskCache.TotalPartCount < partNum || partNum < 1)
|
||||||
{
|
{
|
||||||
return Result.Fail<PresignedUrl>(ResultCode.INVALID_PART_NUMBER);
|
return Result.Fail<PresignedUrl>(ResultCode.INVALID_PART_NUMBER);
|
||||||
@@ -123,24 +149,58 @@ namespace FileService.Application.UploadFileTask
|
|||||||
return Result.Fail<UploadTaskResponse>(ResultCode.CHUNK_NOT_FOUND);
|
return Result.Fail<UploadTaskResponse>(ResultCode.CHUNK_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var task = await reposity.FindByIdAsync(Guid.Parse(taskCache.TaskId));
|
||||||
|
if (task is null)
|
||||||
|
{
|
||||||
|
return Result.Fail<UploadTaskResponse>(ResultCode.CHUNK_NOT_FOUND);
|
||||||
|
}
|
||||||
|
if (task.UploaderId != command.userId)
|
||||||
|
{
|
||||||
|
return Result.Fail<UploadTaskResponse>(ResultCode.PERMISSION_DENIED);
|
||||||
|
}
|
||||||
|
|
||||||
// 校验分片数量必须匹配
|
// 校验分片数量必须匹配
|
||||||
if (command.Parts.Count != taskCache.TotalPartCount)
|
if (command.Parts.Count != taskCache.TotalPartCount)
|
||||||
{
|
{
|
||||||
return Result.Fail<UploadTaskResponse>(ResultCode.PART_COUNT_MISMATCH);
|
return Result.Fail<UploadTaskResponse>(ResultCode.PART_COUNT_MISMATCH);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 校验所有分片都已在上传缓存中注册
|
var expectedPartNumbers = Enumerable.Range(1, taskCache.TotalPartCount).ToHashSet();
|
||||||
foreach (var part in command.Parts)
|
if (!expectedPartNumbers.SetEquals(command.Parts.Select(x => x.PartNumber)))
|
||||||
{
|
{
|
||||||
if (!taskCache.Parts.TryGetValue(part.PartNumber, out _))
|
return Result.Fail<UploadTaskResponse>(ResultCode.INVALID_PART_NUMBER);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 本地分片必须由本服务接收;预签名模式由对象存储在完成合并时校验 ETag。
|
||||||
|
if (string.Equals(taskCache.ProviderCode, "Local", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
foreach (var part in command.Parts)
|
||||||
{
|
{
|
||||||
return Result.Fail<UploadTaskResponse>(ResultCode.CHUNK_NOT_FOUND);
|
if (!taskCache.Parts.TryGetValue(part.PartNumber, out _))
|
||||||
|
{
|
||||||
|
return Result.Fail<UploadTaskResponse>(ResultCode.CHUNK_NOT_FOUND);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var task = await reposity.FindByIdAsync(Guid.Parse(taskCache.TaskId));
|
if (task.State == Domain.UploadTaskState.Completed)
|
||||||
|
{
|
||||||
|
var completedResponse = mapper.Map<UploadTaskResponse>(task);
|
||||||
|
if (task.ResultFileId.HasValue)
|
||||||
|
{
|
||||||
|
var file = await uploadFileReposity.FindByIdAsync(task.ResultFileId.Value);
|
||||||
|
if (file != null)
|
||||||
|
{
|
||||||
|
completedResponse.File = mapper.Map<UploadFile.FileResponse>(file);
|
||||||
|
completedResponse.File.Url = router.Route(file.StorageLocation.StorageProvider)
|
||||||
|
.GetPublicUrl(file.StorageLocation);
|
||||||
|
completedResponse.File.IsPublic = completedResponse.File.IsPublic || completedResponse.File.Url != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Result.Success(completedResponse);
|
||||||
|
}
|
||||||
|
|
||||||
task.CompleteUpload(new Domain.ValueObjects.StorageLocation(
|
task.StartMerging(new Domain.ValueObjects.StorageLocation(
|
||||||
taskCache.ProviderCode, taskCache.Bucket,
|
taskCache.ProviderCode, taskCache.Bucket,
|
||||||
taskCache.ObjectKey, taskCache.Region
|
taskCache.ObjectKey, taskCache.Region
|
||||||
));
|
));
|
||||||
@@ -162,19 +222,26 @@ namespace FileService.Application.UploadFileTask
|
|||||||
FileSize = task.FileSize,
|
FileSize = task.FileSize,
|
||||||
ContentType = task.ContentType.ToString(),
|
ContentType = task.ContentType.ToString(),
|
||||||
CheckSun = task.CheckSum.Value
|
CheckSun = task.CheckSum.Value
|
||||||
|
,ChatType = task.ChatType
|
||||||
|
,TargetId = task.TargetId
|
||||||
|
|
||||||
}, cancellationToken);
|
}, cancellationToken);
|
||||||
|
|
||||||
return Result.Success(mapper.Map<UploadTaskResponse>(task));
|
return Result.Success(mapper.Map<UploadTaskResponse>(task));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Result<CompleteUploadResult>> UploadPartAsync(UploadPartCommand command)
|
public async Task<Result<CompleteUploadResult>> UploadPartAsync(UploadPartCommand command, Guid userId)
|
||||||
{
|
{
|
||||||
var taskCache = await redis.GetAsync(command.SessionId);
|
var taskCache = await redis.GetAsync(command.SessionId);
|
||||||
if (taskCache is null)
|
if (taskCache is null)
|
||||||
{
|
{
|
||||||
return Result.Fail<CompleteUploadResult>(ResultCode.CHUNK_NOT_FOUND);
|
return Result.Fail<CompleteUploadResult>(ResultCode.CHUNK_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
var task = await reposity.FindByIdAsync(Guid.Parse(taskCache.TaskId));
|
||||||
|
if (task is null || task.UploaderId != userId)
|
||||||
|
{
|
||||||
|
return Result.Fail<CompleteUploadResult>(ResultCode.PERMISSION_DENIED);
|
||||||
|
}
|
||||||
|
|
||||||
var minPartSize = options.Value.Providers[options.Value.DefaultProviderCode].MinPartSizeBytes;
|
var minPartSize = options.Value.Providers[options.Value.DefaultProviderCode].MinPartSizeBytes;
|
||||||
|
|
||||||
@@ -213,6 +280,11 @@ namespace FileService.Application.UploadFileTask
|
|||||||
{
|
{
|
||||||
return Result.Fail<UploadProgressResponse>(ResultCode.CHUNK_NOT_FOUND);
|
return Result.Fail<UploadProgressResponse>(ResultCode.CHUNK_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
var task = await reposity.FindByIdAsync(Guid.Parse(taskCache.TaskId));
|
||||||
|
if (task is null || task.UploaderId != userId)
|
||||||
|
{
|
||||||
|
return Result.Fail<UploadProgressResponse>(ResultCode.PERMISSION_DENIED);
|
||||||
|
}
|
||||||
|
|
||||||
var response = new UploadProgressResponse
|
var response = new UploadProgressResponse
|
||||||
{
|
{
|
||||||
@@ -229,5 +301,54 @@ namespace FileService.Application.UploadFileTask
|
|||||||
|
|
||||||
return Result.Success(response);
|
return Result.Success(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool CanReuse(Domain.Entities.UploadFile file, UploadTaskInitCommand command)
|
||||||
|
{
|
||||||
|
var publicUrl = router.Route(file.StorageLocation.StorageProvider).GetPublicUrl(file.StorageLocation);
|
||||||
|
if (file.IsPublic || publicUrl != null) return true;
|
||||||
|
if (file.OwnerId == command.UploaderId)
|
||||||
|
{
|
||||||
|
return string.Equals(file.ChatType, command.ChatType, StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
file.TargetId == command.TargetId;
|
||||||
|
}
|
||||||
|
if (string.Equals(file.ChatType, "GROUP", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return string.Equals(command.ChatType, "GROUP", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
file.TargetId == command.TargetId;
|
||||||
|
}
|
||||||
|
if (string.Equals(file.ChatType, "PRIVATE", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return string.Equals(command.ChatType, "PRIVATE", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
file.TargetId == command.UploaderId && command.TargetId == file.OwnerId;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<UploadTaskResponse>> GetStatusAsync(Guid taskId, Guid userId)
|
||||||
|
{
|
||||||
|
var task = await reposity.FindByIdAsync(taskId);
|
||||||
|
if (task is null)
|
||||||
|
{
|
||||||
|
return Result.Fail<UploadTaskResponse>(ResultCode.CHUNK_NOT_FOUND);
|
||||||
|
}
|
||||||
|
if (task.UploaderId != userId)
|
||||||
|
{
|
||||||
|
return Result.Fail<UploadTaskResponse>(ResultCode.PERMISSION_DENIED);
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = mapper.Map<UploadTaskResponse>(task);
|
||||||
|
if (task.ResultFileId.HasValue)
|
||||||
|
{
|
||||||
|
var file = await uploadFileReposity.FindByIdAsync(task.ResultFileId.Value);
|
||||||
|
if (file != null)
|
||||||
|
{
|
||||||
|
response.File = mapper.Map<UploadFile.FileResponse>(file);
|
||||||
|
response.File.Url = router.Route(file.StorageLocation.StorageProvider)
|
||||||
|
.GetPublicUrl(file.StorageLocation);
|
||||||
|
response.File.IsPublic = response.File.IsPublic || response.File.Url != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Result.Success(response);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace FileService.Application.UploadFileTask
|
|||||||
{
|
{
|
||||||
public record UploadTaskInitCommand(
|
public record UploadTaskInitCommand(
|
||||||
Guid UploaderId,
|
Guid UploaderId,
|
||||||
Guid ConversationId, string FileName,
|
Guid ConversationId, string? ChatType, Guid? TargetId, string FileName,
|
||||||
long FileSize,string contentType,
|
long FileSize,string contentType,
|
||||||
string checkSum
|
string checkSum
|
||||||
)
|
)
|
||||||
@@ -17,7 +17,7 @@ namespace FileService.Application.UploadFileTask
|
|||||||
{
|
{
|
||||||
return new Domain.Entities.UploadTask(
|
return new Domain.Entities.UploadTask(
|
||||||
UploaderId,
|
UploaderId,
|
||||||
ConversationId, FileName, FileSize, contentType,
|
ConversationId, ChatType, TargetId, FileName, FileSize, contentType,
|
||||||
null,new Domain.ValueObjects.CheckSum("md5", checkSum)
|
null,new Domain.ValueObjects.CheckSum("md5", checkSum)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,5 +19,8 @@ namespace FileService.Application.UploadFileTask
|
|||||||
public StorageLocation StorageLocation { get; set; }
|
public StorageLocation StorageLocation { get; set; }
|
||||||
public string State { get; set; }
|
public string State { get; set; }
|
||||||
public string CheckSum { get; set; }
|
public string CheckSum { get; set; }
|
||||||
|
public Guid? ResultFileId { get; set; }
|
||||||
|
public string? FailureReason { get; set; }
|
||||||
|
public global::FileService.Application.UploadFile.FileResponse? File { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,10 +12,14 @@ namespace FileService.Domain.Entities
|
|||||||
public FileState State { get; private set; } = FileState.Uploaded;
|
public FileState State { get; private set; } = FileState.Uploaded;
|
||||||
public StorageLocation StorageLocation { get; private set; } = new StorageLocation();
|
public StorageLocation StorageLocation { get; private set; } = new StorageLocation();
|
||||||
public CheckSum CheckSum { get; private set; }
|
public CheckSum CheckSum { get; private set; }
|
||||||
|
public Guid? SourceTaskId { get; private set; }
|
||||||
|
public string? ChatType { get; private set; }
|
||||||
|
public Guid? TargetId { get; private set; }
|
||||||
|
public bool IsPublic { get; private set; }
|
||||||
|
|
||||||
private UploadFile() { }
|
private UploadFile() { }
|
||||||
|
|
||||||
public UploadFile(Guid ownerId, FileName fileName, long fileSize, ContentType contentType, StorageLocation? storageLocation, CheckSum checkSum)
|
public UploadFile(Guid ownerId, FileName fileName, long fileSize, ContentType contentType, StorageLocation? storageLocation, CheckSum checkSum, Guid? sourceTaskId = null, string? chatType = null, Guid? targetId = null, bool isPublic = false)
|
||||||
{
|
{
|
||||||
OwnerId = ownerId;
|
OwnerId = ownerId;
|
||||||
FileName = fileName;
|
FileName = fileName;
|
||||||
@@ -23,6 +27,10 @@ namespace FileService.Domain.Entities
|
|||||||
ContentType = contentType;
|
ContentType = contentType;
|
||||||
StorageLocation = storageLocation ?? new StorageLocation();
|
StorageLocation = storageLocation ?? new StorageLocation();
|
||||||
CheckSum = checkSum;
|
CheckSum = checkSum;
|
||||||
|
SourceTaskId = sourceTaskId;
|
||||||
|
ChatType = chatType?.ToUpperInvariant();
|
||||||
|
TargetId = targetId;
|
||||||
|
IsPublic = isPublic;
|
||||||
State = FileState.Uploaded;
|
State = FileState.Uploaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,19 +8,25 @@ namespace FileService.Domain.Entities
|
|||||||
{
|
{
|
||||||
public Guid UploaderId { get; private set; }
|
public Guid UploaderId { get; private set; }
|
||||||
public Guid ConversationId { get; private set; }
|
public Guid ConversationId { get; private set; }
|
||||||
|
public string? ChatType { get; private set; }
|
||||||
|
public Guid? TargetId { get; private set; }
|
||||||
public FileName FileName { get; private set; }
|
public FileName FileName { get; private set; }
|
||||||
public long FileSize { get; private set; }
|
public long FileSize { get; private set; }
|
||||||
public ContentType ContentType { get; private set; }
|
public ContentType ContentType { get; private set; }
|
||||||
public StorageLocation StorageLocation { get; private set; }
|
public StorageLocation StorageLocation { get; private set; }
|
||||||
public UploadTaskState State { get; private set; }
|
public UploadTaskState State { get; private set; }
|
||||||
public CheckSum CheckSum { get; private set; }
|
public CheckSum CheckSum { get; private set; }
|
||||||
|
public Guid? ResultFileId { get; private set; }
|
||||||
|
public string? FailureReason { get; private set; }
|
||||||
|
|
||||||
private UploadTask() { }
|
private UploadTask() { }
|
||||||
|
|
||||||
public UploadTask(Guid uploaderId, Guid conversationId, FileName fileName, long fileSize, ContentType contentType, StorageLocation? storageLocation, CheckSum checkSum)
|
public UploadTask(Guid uploaderId, Guid conversationId, string? chatType, Guid? targetId, FileName fileName, long fileSize, ContentType contentType, StorageLocation? storageLocation, CheckSum checkSum)
|
||||||
{
|
{
|
||||||
UploaderId = uploaderId;
|
UploaderId = uploaderId;
|
||||||
ConversationId = conversationId;
|
ConversationId = conversationId;
|
||||||
|
ChatType = chatType?.ToUpperInvariant();
|
||||||
|
TargetId = targetId;
|
||||||
FileName = fileName;
|
FileName = fileName;
|
||||||
FileSize = fileSize;
|
FileSize = fileSize;
|
||||||
ContentType = contentType;
|
ContentType = contentType;
|
||||||
@@ -33,16 +39,28 @@ namespace FileService.Domain.Entities
|
|||||||
State = UploadTaskState.Uploading;
|
State = UploadTaskState.Uploading;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CompleteUpload(StorageLocation location)
|
public void StartMerging(StorageLocation location)
|
||||||
{
|
{
|
||||||
|
StorageLocation = location;
|
||||||
|
State = UploadTaskState.Merging;
|
||||||
|
FailureReason = null;
|
||||||
|
NotifyModified();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CompleteUpload(Guid fileId)
|
||||||
|
{
|
||||||
|
ResultFileId = fileId;
|
||||||
State = UploadTaskState.Completed;
|
State = UploadTaskState.Completed;
|
||||||
|
FailureReason = null;
|
||||||
|
NotifyModified();
|
||||||
AddDomainEvent(new UploadTaskCompletedDomainEvent(this));
|
AddDomainEvent(new UploadTaskCompletedDomainEvent(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Fail()
|
public void Fail(string reason)
|
||||||
{
|
{
|
||||||
State = UploadTaskState.Failed;
|
State = UploadTaskState.Failed;
|
||||||
|
FailureReason = reason.Length > 500 ? reason[..500] : reason;
|
||||||
|
NotifyModified();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,5 +19,6 @@ namespace FileService.Domain.IReposities
|
|||||||
/// 全局按 checksum 查询(用于跨用户秒传去重)
|
/// 全局按 checksum 查询(用于跨用户秒传去重)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<UploadFile?> FindByCheckSumGlobalAsync(string algorithm, string value);
|
Task<UploadFile?> FindByCheckSumGlobalAsync(string algorithm, string value);
|
||||||
|
Task<UploadFile?> FindBySourceTaskIdAsync(Guid taskId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ namespace FileService.Domain.ValueObjects
|
|||||||
|
|
||||||
public FileName(string value)
|
public FileName(string value)
|
||||||
{
|
{
|
||||||
if(value.Length > 20)
|
if (string.IsNullOrWhiteSpace(value) || value.Length > 255)
|
||||||
{
|
{
|
||||||
throw new ArgumentException("文件名超出长度");
|
throw new ArgumentException("文件名不能为空且不能超过 255 个字符");
|
||||||
}
|
}
|
||||||
|
|
||||||
Value = value;
|
Value = value;
|
||||||
|
|||||||
@@ -15,12 +15,14 @@ namespace FileService.Infrastructure.Configs
|
|||||||
{
|
{
|
||||||
builder.ToTable("upload_files");
|
builder.ToTable("upload_files");
|
||||||
builder.Property(x => x.FileName)
|
builder.Property(x => x.FileName)
|
||||||
|
.HasMaxLength(255)
|
||||||
.HasConversion(
|
.HasConversion(
|
||||||
a => a.Value,
|
a => a.Value,
|
||||||
b => new Domain.ValueObjects.FileName(b)
|
b => new Domain.ValueObjects.FileName(b)
|
||||||
);
|
);
|
||||||
|
|
||||||
builder.Property(x => x.ContentType)
|
builder.Property(x => x.ContentType)
|
||||||
|
.HasMaxLength(255)
|
||||||
.HasConversion(
|
.HasConversion(
|
||||||
a => a.Value,
|
a => a.Value,
|
||||||
b => new Domain.ValueObjects.ContentType(b)
|
b => new Domain.ValueObjects.ContentType(b)
|
||||||
@@ -29,24 +31,32 @@ namespace FileService.Infrastructure.Configs
|
|||||||
builder.ComplexProperty(x => x.CheckSum, c =>
|
builder.ComplexProperty(x => x.CheckSum, c =>
|
||||||
{
|
{
|
||||||
c.Property(p => p.Value)
|
c.Property(p => p.Value)
|
||||||
|
.HasMaxLength(128)
|
||||||
.HasColumnName("checksum_value");
|
.HasColumnName("checksum_value");
|
||||||
|
|
||||||
c.Property(p => p.Algorithm)
|
c.Property(p => p.Algorithm)
|
||||||
|
.HasMaxLength(16)
|
||||||
.HasColumnName("checksum_algorithm");
|
.HasColumnName("checksum_algorithm");
|
||||||
});
|
});
|
||||||
|
builder.HasIndex(x => x.SourceTaskId).IsUnique();
|
||||||
|
builder.Property(x => x.ChatType).HasMaxLength(16);
|
||||||
|
|
||||||
builder.ComplexProperty(x => x.StorageLocation, c =>
|
builder.ComplexProperty(x => x.StorageLocation, c =>
|
||||||
{
|
{
|
||||||
c.Property(p => p.StorageProvider)
|
c.Property(p => p.StorageProvider)
|
||||||
|
.HasMaxLength(64)
|
||||||
.HasColumnName("storage_provider");
|
.HasColumnName("storage_provider");
|
||||||
|
|
||||||
c.Property(p => p.ObjectKey)
|
c.Property(p => p.ObjectKey)
|
||||||
|
.HasMaxLength(1024)
|
||||||
.HasColumnName("storage_key");
|
.HasColumnName("storage_key");
|
||||||
|
|
||||||
c.Property(p => p.Region)
|
c.Property(p => p.Region)
|
||||||
|
.HasMaxLength(128)
|
||||||
.HasColumnName("storage_region");
|
.HasColumnName("storage_region");
|
||||||
|
|
||||||
c.Property(p => p.Bucket)
|
c.Property(p => p.Bucket)
|
||||||
|
.HasMaxLength(255)
|
||||||
.HasColumnName("storage_bucket");
|
.HasColumnName("storage_bucket");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,12 +15,14 @@ namespace FileService.Infrastructure.Configs
|
|||||||
{
|
{
|
||||||
builder.ToTable("upload_tasks");
|
builder.ToTable("upload_tasks");
|
||||||
builder.Property(x => x.FileName)
|
builder.Property(x => x.FileName)
|
||||||
|
.HasMaxLength(255)
|
||||||
.HasConversion(
|
.HasConversion(
|
||||||
a => a.Value,
|
a => a.Value,
|
||||||
b => new Domain.ValueObjects.FileName(b)
|
b => new Domain.ValueObjects.FileName(b)
|
||||||
);
|
);
|
||||||
|
|
||||||
builder.Property(x => x.ContentType)
|
builder.Property(x => x.ContentType)
|
||||||
|
.HasMaxLength(255)
|
||||||
.HasConversion(
|
.HasConversion(
|
||||||
a => a.Value,
|
a => a.Value,
|
||||||
b => new Domain.ValueObjects.ContentType(b)
|
b => new Domain.ValueObjects.ContentType(b)
|
||||||
@@ -29,24 +31,32 @@ namespace FileService.Infrastructure.Configs
|
|||||||
builder.ComplexProperty(x => x.CheckSum, c =>
|
builder.ComplexProperty(x => x.CheckSum, c =>
|
||||||
{
|
{
|
||||||
c.Property(p => p.Value)
|
c.Property(p => p.Value)
|
||||||
|
.HasMaxLength(128)
|
||||||
.HasColumnName("checksum_value");
|
.HasColumnName("checksum_value");
|
||||||
|
|
||||||
c.Property(p => p.Algorithm)
|
c.Property(p => p.Algorithm)
|
||||||
|
.HasMaxLength(16)
|
||||||
.HasColumnName("checksum_algorithm");
|
.HasColumnName("checksum_algorithm");
|
||||||
});
|
});
|
||||||
|
builder.Property(x => x.FailureReason).HasMaxLength(500);
|
||||||
|
builder.Property(x => x.ChatType).HasMaxLength(16);
|
||||||
|
|
||||||
builder.ComplexProperty(x => x.StorageLocation, c =>
|
builder.ComplexProperty(x => x.StorageLocation, c =>
|
||||||
{
|
{
|
||||||
c.Property(p => p.StorageProvider)
|
c.Property(p => p.StorageProvider)
|
||||||
|
.HasMaxLength(64)
|
||||||
.HasColumnName("storage_provider");
|
.HasColumnName("storage_provider");
|
||||||
|
|
||||||
c.Property(p => p.ObjectKey)
|
c.Property(p => p.ObjectKey)
|
||||||
|
.HasMaxLength(1024)
|
||||||
.HasColumnName("storage_key");
|
.HasColumnName("storage_key");
|
||||||
|
|
||||||
c.Property(p => p.Region)
|
c.Property(p => p.Region)
|
||||||
|
.HasMaxLength(128)
|
||||||
.HasColumnName("storage_region");
|
.HasColumnName("storage_region");
|
||||||
|
|
||||||
c.Property(p => p.Bucket)
|
c.Property(p => p.Bucket)
|
||||||
|
.HasMaxLength(255)
|
||||||
.HasColumnName("storage_bucket");
|
.HasColumnName("storage_bucket");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
namespace FileService.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(FileDbContext))]
|
||||||
|
[Migration("20260909000300_AsyncUploadResult")]
|
||||||
|
public partial class AsyncUploadResult : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
AlterStringColumns(migrationBuilder, narrowing: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "ChatType",
|
||||||
|
table: "upload_files",
|
||||||
|
type: "varchar(16)",
|
||||||
|
maxLength: 16,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "IsPublic",
|
||||||
|
table: "upload_files",
|
||||||
|
type: "tinyint(1)",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<Guid>(
|
||||||
|
name: "SourceTaskId",
|
||||||
|
table: "upload_files",
|
||||||
|
type: "char(36)",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<Guid>(
|
||||||
|
name: "TargetId",
|
||||||
|
table: "upload_files",
|
||||||
|
type: "char(36)",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "ChatType",
|
||||||
|
table: "upload_tasks",
|
||||||
|
type: "varchar(16)",
|
||||||
|
maxLength: 16,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "FailureReason",
|
||||||
|
table: "upload_tasks",
|
||||||
|
type: "varchar(500)",
|
||||||
|
maxLength: 500,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<Guid>(
|
||||||
|
name: "ResultFileId",
|
||||||
|
table: "upload_tasks",
|
||||||
|
type: "char(36)",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<Guid>(
|
||||||
|
name: "TargetId",
|
||||||
|
table: "upload_tasks",
|
||||||
|
type: "char(36)",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_upload_files_SourceTaskId",
|
||||||
|
table: "upload_files",
|
||||||
|
column: "SourceTaskId",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_upload_files_checksum",
|
||||||
|
table: "upload_files",
|
||||||
|
columns: new[] { "checksum_algorithm", "checksum_value", "IsDeleted" });
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex("IX_upload_files_checksum", "upload_files");
|
||||||
|
migrationBuilder.DropIndex("IX_upload_files_SourceTaskId", "upload_files");
|
||||||
|
migrationBuilder.DropColumn("ChatType", "upload_files");
|
||||||
|
migrationBuilder.DropColumn("IsPublic", "upload_files");
|
||||||
|
migrationBuilder.DropColumn("SourceTaskId", "upload_files");
|
||||||
|
migrationBuilder.DropColumn("TargetId", "upload_files");
|
||||||
|
migrationBuilder.DropColumn("ChatType", "upload_tasks");
|
||||||
|
migrationBuilder.DropColumn("FailureReason", "upload_tasks");
|
||||||
|
migrationBuilder.DropColumn("ResultFileId", "upload_tasks");
|
||||||
|
migrationBuilder.DropColumn("TargetId", "upload_tasks");
|
||||||
|
AlterStringColumns(migrationBuilder, narrowing: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static void AlterStringColumns(MigrationBuilder migrationBuilder, bool narrowing)
|
||||||
|
{
|
||||||
|
var columns = new (string Table, string Column, int Length, bool Nullable)[]
|
||||||
|
{
|
||||||
|
("upload_files", "FileName", 255, false),
|
||||||
|
("upload_files", "ContentType", 255, false),
|
||||||
|
("upload_files", "checksum_algorithm", 16, false),
|
||||||
|
("upload_files", "checksum_value", 128, false),
|
||||||
|
("upload_files", "storage_provider", 64, false),
|
||||||
|
("upload_files", "storage_bucket", 255, false),
|
||||||
|
("upload_files", "storage_key", 1024, false),
|
||||||
|
("upload_files", "storage_region", 128, true),
|
||||||
|
("upload_tasks", "FileName", 255, false),
|
||||||
|
("upload_tasks", "ContentType", 255, false),
|
||||||
|
("upload_tasks", "checksum_algorithm", 16, false),
|
||||||
|
("upload_tasks", "checksum_value", 128, false),
|
||||||
|
("upload_tasks", "storage_provider", 64, false),
|
||||||
|
("upload_tasks", "storage_bucket", 255, false),
|
||||||
|
("upload_tasks", "storage_key", 1024, false),
|
||||||
|
("upload_tasks", "storage_region", 128, true)
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var (table, column, length, nullable) in columns)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: column,
|
||||||
|
table: table,
|
||||||
|
type: narrowing ? $"varchar({length})" : "longtext",
|
||||||
|
maxLength: narrowing ? length : null,
|
||||||
|
nullable: nullable,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: narrowing ? "longtext" : $"varchar({length})",
|
||||||
|
oldMaxLength: narrowing ? null : length,
|
||||||
|
oldNullable: nullable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,13 +25,18 @@ namespace FileService.Infrastructure.Migrations
|
|||||||
|
|
||||||
modelBuilder.Entity("FileService.Domain.Entities.UploadFile", b =>
|
modelBuilder.Entity("FileService.Domain.Entities.UploadFile", b =>
|
||||||
{
|
{
|
||||||
|
b.Property<string>("ChatType")
|
||||||
|
.HasMaxLength(16)
|
||||||
|
.HasColumnType("varchar(16)");
|
||||||
|
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("char(36)");
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
b.Property<string>("ContentType")
|
b.Property<string>("ContentType")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("longtext");
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("varchar(255)");
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreationTime")
|
b.Property<DateTimeOffset>("CreationTime")
|
||||||
.HasColumnType("datetime(6)");
|
.HasColumnType("datetime(6)");
|
||||||
@@ -41,7 +46,8 @@ namespace FileService.Infrastructure.Migrations
|
|||||||
|
|
||||||
b.Property<string>("FileName")
|
b.Property<string>("FileName")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("longtext");
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("varchar(255)");
|
||||||
|
|
||||||
b.Property<long>("FileSize")
|
b.Property<long>("FileSize")
|
||||||
.HasColumnType("bigint");
|
.HasColumnType("bigint");
|
||||||
@@ -49,27 +55,38 @@ namespace FileService.Infrastructure.Migrations
|
|||||||
b.Property<bool>("IsDeleted")
|
b.Property<bool>("IsDeleted")
|
||||||
.HasColumnType("tinyint(1)");
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsPublic")
|
||||||
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("ModificationTime")
|
b.Property<DateTimeOffset?>("ModificationTime")
|
||||||
.HasColumnType("datetime(6)");
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
b.Property<Guid>("OwnerId")
|
b.Property<Guid>("OwnerId")
|
||||||
.HasColumnType("char(36)");
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("SourceTaskId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
b.Property<int>("State")
|
b.Property<int>("State")
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<Guid?>("TargetId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
b.ComplexProperty<Dictionary<string, object>>("CheckSum", "FileService.Domain.Entities.UploadFile.CheckSum#CheckSum", b1 =>
|
b.ComplexProperty<Dictionary<string, object>>("CheckSum", "FileService.Domain.Entities.UploadFile.CheckSum#CheckSum", b1 =>
|
||||||
{
|
{
|
||||||
b1.IsRequired();
|
b1.IsRequired();
|
||||||
|
|
||||||
b1.Property<string>("Algorithm")
|
b1.Property<string>("Algorithm")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("longtext")
|
.HasMaxLength(16)
|
||||||
|
.HasColumnType("varchar(16)")
|
||||||
.HasColumnName("checksum_algorithm");
|
.HasColumnName("checksum_algorithm");
|
||||||
|
|
||||||
b1.Property<string>("Value")
|
b1.Property<string>("Value")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("longtext")
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("varchar(128)")
|
||||||
.HasColumnName("checksum_value");
|
.HasColumnName("checksum_value");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -79,38 +96,50 @@ namespace FileService.Infrastructure.Migrations
|
|||||||
|
|
||||||
b1.Property<string>("Bucket")
|
b1.Property<string>("Bucket")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("longtext")
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("varchar(255)")
|
||||||
.HasColumnName("storage_bucket");
|
.HasColumnName("storage_bucket");
|
||||||
|
|
||||||
b1.Property<string>("ObjectKey")
|
b1.Property<string>("ObjectKey")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("longtext")
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("varchar(1024)")
|
||||||
.HasColumnName("storage_key");
|
.HasColumnName("storage_key");
|
||||||
|
|
||||||
b1.Property<string>("Region")
|
b1.Property<string>("Region")
|
||||||
.HasColumnType("longtext")
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("varchar(128)")
|
||||||
.HasColumnName("storage_region");
|
.HasColumnName("storage_region");
|
||||||
|
|
||||||
b1.Property<string>("StorageProvider")
|
b1.Property<string>("StorageProvider")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("longtext")
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("varchar(64)")
|
||||||
.HasColumnName("storage_provider");
|
.HasColumnName("storage_provider");
|
||||||
});
|
});
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SourceTaskId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
b.ToTable("upload_files", (string)null);
|
b.ToTable("upload_files", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("FileService.Domain.Entities.UploadTask", b =>
|
modelBuilder.Entity("FileService.Domain.Entities.UploadTask", b =>
|
||||||
{
|
{
|
||||||
|
b.Property<string>("ChatType")
|
||||||
|
.HasMaxLength(16)
|
||||||
|
.HasColumnType("varchar(16)");
|
||||||
|
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("char(36)");
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
b.Property<string>("ContentType")
|
b.Property<string>("ContentType")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("longtext");
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("varchar(255)");
|
||||||
|
|
||||||
b.Property<Guid>("ConversationId")
|
b.Property<Guid>("ConversationId")
|
||||||
.HasColumnType("char(36)");
|
.HasColumnType("char(36)");
|
||||||
@@ -123,20 +152,31 @@ namespace FileService.Infrastructure.Migrations
|
|||||||
|
|
||||||
b.Property<string>("FileName")
|
b.Property<string>("FileName")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("longtext");
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("varchar(255)");
|
||||||
|
|
||||||
b.Property<long>("FileSize")
|
b.Property<long>("FileSize")
|
||||||
.HasColumnType("bigint");
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<string>("FailureReason")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("varchar(500)");
|
||||||
|
|
||||||
b.Property<bool>("IsDeleted")
|
b.Property<bool>("IsDeleted")
|
||||||
.HasColumnType("tinyint(1)");
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("ModificationTime")
|
b.Property<DateTimeOffset?>("ModificationTime")
|
||||||
.HasColumnType("datetime(6)");
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("ResultFileId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
b.Property<int>("State")
|
b.Property<int>("State")
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<Guid?>("TargetId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
b.Property<Guid>("UploaderId")
|
b.Property<Guid>("UploaderId")
|
||||||
.HasColumnType("char(36)");
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
@@ -146,12 +186,14 @@ namespace FileService.Infrastructure.Migrations
|
|||||||
|
|
||||||
b1.Property<string>("Algorithm")
|
b1.Property<string>("Algorithm")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("longtext")
|
.HasMaxLength(16)
|
||||||
|
.HasColumnType("varchar(16)")
|
||||||
.HasColumnName("checksum_algorithm");
|
.HasColumnName("checksum_algorithm");
|
||||||
|
|
||||||
b1.Property<string>("Value")
|
b1.Property<string>("Value")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("longtext")
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("varchar(128)")
|
||||||
.HasColumnName("checksum_value");
|
.HasColumnName("checksum_value");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -161,21 +203,25 @@ namespace FileService.Infrastructure.Migrations
|
|||||||
|
|
||||||
b1.Property<string>("Bucket")
|
b1.Property<string>("Bucket")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("longtext")
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("varchar(255)")
|
||||||
.HasColumnName("storage_bucket");
|
.HasColumnName("storage_bucket");
|
||||||
|
|
||||||
b1.Property<string>("ObjectKey")
|
b1.Property<string>("ObjectKey")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("longtext")
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("varchar(1024)")
|
||||||
.HasColumnName("storage_key");
|
.HasColumnName("storage_key");
|
||||||
|
|
||||||
b1.Property<string>("Region")
|
b1.Property<string>("Region")
|
||||||
.HasColumnType("longtext")
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("varchar(128)")
|
||||||
.HasColumnName("storage_region");
|
.HasColumnName("storage_region");
|
||||||
|
|
||||||
b1.Property<string>("StorageProvider")
|
b1.Property<string>("StorageProvider")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("longtext")
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("varchar(64)")
|
||||||
.HasColumnName("storage_provider");
|
.HasColumnName("storage_provider");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -42,5 +42,10 @@ namespace FileService.Infrastructure.Reposites
|
|||||||
x.CheckSum.Value == value
|
x.CheckSum.Value == value
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<UploadFile?> FindBySourceTaskIdAsync(Guid taskId)
|
||||||
|
{
|
||||||
|
return db.Files.FirstOrDefaultAsync(x => x.SourceTaskId == taskId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ namespace FileService.Infrastructure.Storage
|
|||||||
var baseUrl = options.Value.Providers[options.Value.DefaultProviderCode].LocalUploadApiBaseUrl;
|
var baseUrl = options.Value.Providers[options.Value.DefaultProviderCode].LocalUploadApiBaseUrl;
|
||||||
return new PresignedUrl(
|
return new PresignedUrl(
|
||||||
baseUrl + $"local/parts/upload?sessionId={command.UploadSessionId}&partNumber={command.PartNumber}",
|
baseUrl + $"local/parts/upload?sessionId={command.UploadSessionId}&partNumber={command.PartNumber}",
|
||||||
|
"POST",
|
||||||
new Dictionary<string, string>(),
|
new Dictionary<string, string>(),
|
||||||
ExpiresAt: DateTimeOffset.Now.Add(options.Value.Providers[options.Value.DefaultProviderCode].UploadUrlExpiresIn)
|
ExpiresAt: DateTimeOffset.Now.Add(options.Value.Providers[options.Value.DefaultProviderCode].UploadUrlExpiresIn)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using FileService.Application.UploadFile;
|
using FileService.Application.UploadFile;
|
||||||
using FileService.Infrastructure;
|
using FileService.Infrastructure;
|
||||||
using IM.ASPNETCore;
|
using IM.ASPNETCore;
|
||||||
|
using IM.Commons;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@@ -51,7 +52,8 @@ namespace FileService.WebApi.Controllers.File
|
|||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
public async Task<IActionResult> Get(Guid id)
|
public async Task<IActionResult> Get(Guid id)
|
||||||
{
|
{
|
||||||
var res = await service.GetFileInfoAsync(id);
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
|
var res = await service.GetFileInfoAsync(id, Guid.Parse(userId));
|
||||||
return Ok(res);
|
return Ok(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,11 +67,15 @@ namespace FileService.WebApi.Controllers.File
|
|||||||
var res = await service.OpenDownloadAsync(id, Guid.Parse(userId));
|
var res = await service.OpenDownloadAsync(id, Guid.Parse(userId));
|
||||||
if (res.Data == null)
|
if (res.Data == null)
|
||||||
{
|
{
|
||||||
|
if (res.Code == (int)ResultCode.PERMISSION_DENIED)
|
||||||
|
{
|
||||||
|
return StatusCode(StatusCodes.Status403Forbidden, res);
|
||||||
|
}
|
||||||
return NotFound(res);
|
return NotFound(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
Response.Headers["Cache-Control"] = "private,max-age=86400";
|
Response.Headers["Cache-Control"] = "private,max-age=86400";
|
||||||
return File(res.Data.Content, res.Data.ContentType);
|
return File(res.Data.Content, res.Data.ContentType, enableRangeProcessing: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ namespace FileService.WebApi.Controllers.FileTask
|
|||||||
{
|
{
|
||||||
public class CompleteTaskRequest
|
public class CompleteTaskRequest
|
||||||
{
|
{
|
||||||
public string SessionId { get; set; }
|
public string SessionId { get; set; } = string.Empty;
|
||||||
public List<UploadPart> Parts { get; set; }
|
public List<UploadPart> Parts { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CompleteTaskRequestValidator : AbstractValidator<CompleteTaskRequest>
|
public class CompleteTaskRequestValidator : AbstractValidator<CompleteTaskRequest>
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ namespace FileService.WebApi.Controllers.FileTask
|
|||||||
var res = await service.InitTaskAsync(new UploadTaskInitCommand(
|
var res = await service.InitTaskAsync(new UploadTaskInitCommand(
|
||||||
UploaderId: Guid.Parse(userId),
|
UploaderId: Guid.Parse(userId),
|
||||||
ConversationId: request.ConversationId,
|
ConversationId: request.ConversationId,
|
||||||
|
ChatType: request.ChatType,
|
||||||
|
TargetId: request.TargetId,
|
||||||
FileName: request.FileName,
|
FileName: request.FileName,
|
||||||
FileSize: request.FileSize,
|
FileSize: request.FileSize,
|
||||||
contentType: request.ContentType,
|
contentType: request.ContentType,
|
||||||
@@ -44,6 +46,13 @@ namespace FileService.WebApi.Controllers.FileTask
|
|||||||
return Ok(res);
|
return Ok(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("status")]
|
||||||
|
public async Task<IActionResult> Status(Guid taskId)
|
||||||
|
{
|
||||||
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
|
return Ok(await service.GetStatusAsync(taskId, Guid.Parse(userId)));
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("Getuploadurl")]
|
[HttpGet("Getuploadurl")]
|
||||||
public async Task<IActionResult> GetUploadUrl(string sessionId, int partNum)
|
public async Task<IActionResult> GetUploadUrl(string sessionId, int partNum)
|
||||||
{
|
{
|
||||||
@@ -53,19 +62,20 @@ namespace FileService.WebApi.Controllers.FileTask
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("complete")]
|
[HttpPost("complete")]
|
||||||
|
[UnitOfWork(typeof(FileDbContext))]
|
||||||
public async Task<IActionResult> Complete([FromBody] CompleteTaskRequest request)
|
public async Task<IActionResult> Complete([FromBody] CompleteTaskRequest request)
|
||||||
{
|
{
|
||||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
var res = await service.CompleteTaskAsync(new UploadTaskCompleteCommand(request.SessionId, Guid.Parse(userId), request.Parts));
|
var res = await service.CompleteTaskAsync(new UploadTaskCompleteCommand(request.SessionId, Guid.Parse(userId), request.Parts));
|
||||||
return Ok(res);
|
return res.Succeeded ? Accepted(res) : Ok(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("local/parts/upload")]
|
[HttpPost("local/parts/upload")]
|
||||||
public async Task<IActionResult> LocalUpload(string sessionId, int partNumber, IFormFile file)
|
public async Task<IActionResult> LocalUpload(string sessionId, int partNumber, IFormFile file)
|
||||||
{
|
{
|
||||||
//var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
var stream = file.OpenReadStream();
|
var stream = file.OpenReadStream();
|
||||||
var res = await service.UploadPartAsync(new UploadPartCommand(stream, sessionId, partNumber, file.Length));
|
var res = await service.UploadPartAsync(new UploadPartCommand(stream, sessionId, partNumber, file.Length), Guid.Parse(userId));
|
||||||
return Ok(res);
|
return Ok(res);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,30 @@ namespace FileService.WebApi.Controllers.FileTask
|
|||||||
public class FileTaskInitRequest
|
public class FileTaskInitRequest
|
||||||
{
|
{
|
||||||
public Guid ConversationId { get; set; }
|
public Guid ConversationId { get; set; }
|
||||||
public string FileName { get; set; }
|
public string? ChatType { get; set; }
|
||||||
|
public Guid? TargetId { get; set; }
|
||||||
|
public string FileName { get; set; } = string.Empty;
|
||||||
public long FileSize { get; set; }
|
public long FileSize { get; set; }
|
||||||
public string ContentType { get; set; }
|
public string ContentType { get; set; } = string.Empty;
|
||||||
public string CheckSum { get; set; }
|
public string CheckSum { get; set; } = string.Empty;
|
||||||
|
|
||||||
}
|
}
|
||||||
public class FileTaskInitRequestValidator: AbstractValidator<FileTaskInitRequest>
|
public class FileTaskInitRequestValidator: AbstractValidator<FileTaskInitRequest>
|
||||||
{
|
{
|
||||||
|
public FileTaskInitRequestValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.FileName).NotEmpty().MaximumLength(255);
|
||||||
|
RuleFor(x => x.FileSize).GreaterThan(0);
|
||||||
|
RuleFor(x => x.ContentType).NotEmpty().MaximumLength(255);
|
||||||
|
RuleFor(x => x.CheckSum).NotEmpty().MaximumLength(128);
|
||||||
|
RuleFor(x => x.ChatType)
|
||||||
|
.Must(value => string.IsNullOrWhiteSpace(value) ||
|
||||||
|
value.Equals("PRIVATE", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
value.Equals("GROUP", StringComparison.OrdinalIgnoreCase))
|
||||||
|
.WithMessage("chatType 必须为 PRIVATE 或 GROUP");
|
||||||
|
RuleFor(x => x.TargetId)
|
||||||
|
.NotEmpty()
|
||||||
|
.When(x => !string.IsNullOrWhiteSpace(x.ChatType));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,17 @@ namespace FileService.WebApi
|
|||||||
public void Initialize(IServiceCollection services)
|
public void Initialize(IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddScoped<UploadFileService>();
|
services.AddScoped<UploadFileService>();
|
||||||
|
services.AddHttpClient<IGroupAccessService, GroupAccessService>((sp, client) =>
|
||||||
|
{
|
||||||
|
var configuration = sp.GetRequiredService<IConfiguration>();
|
||||||
|
client.BaseAddress = new Uri(configuration["InternalServices:GroupServiceBaseUrl"]
|
||||||
|
?? "http://im-group-service:8080/");
|
||||||
|
var internalApiKey = configuration["InternalApiKey"];
|
||||||
|
if (!string.IsNullOrWhiteSpace(internalApiKey))
|
||||||
|
{
|
||||||
|
client.DefaultRequestHeaders.Add("X-Internal-Api-Key", internalApiKey);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
{
|
{
|
||||||
|
"InternalApiKey": "development-only-change-me",
|
||||||
|
"InternalServices": {
|
||||||
|
"GroupServiceBaseUrl": "http://localhost:5070/"
|
||||||
|
},
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
|
|||||||
@@ -5,5 +5,9 @@
|
|||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*"
|
"AllowedHosts": "*",
|
||||||
|
"InternalApiKey": "",
|
||||||
|
"InternalServices": {
|
||||||
|
"GroupServiceBaseUrl": "http://im-group-service:8080/"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,5 +49,12 @@ namespace GroupService.Domain.Entities
|
|||||||
{
|
{
|
||||||
GroupNickName = nickname;
|
GroupNickName = nickname;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Leave()
|
||||||
|
{
|
||||||
|
if (IsDeleted) return;
|
||||||
|
SoftDelete();
|
||||||
|
AddDomainEvent(new GroupMemberLeftDomainEvent(this));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
using GroupService.Domain.Entities;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace GroupService.Domain.Events
|
||||||
|
{
|
||||||
|
public record GroupMemberLeftDomainEvent(GroupMember Member) : INotification;
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ namespace GroupService.Domain.IReposities
|
|||||||
/// <param name="userId"></param>
|
/// <param name="userId"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task<IEnumerable<Group>> FindByMasterIdAsync(Guid userId);
|
Task<IEnumerable<Group>> FindByMasterIdAsync(Guid userId);
|
||||||
|
Task<IEnumerable<Group>> FindByMemberIdAsync(Guid userId);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 创建群聊
|
/// 创建群聊
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -8,5 +8,6 @@ namespace GroupService.Domain.IReposities
|
|||||||
Task<GroupJoinRequest> FindByIdAsync(Guid id);
|
Task<GroupJoinRequest> FindByIdAsync(Guid id);
|
||||||
Task<IEnumerable<GroupJoinRequest?>> FindByGroupIdAsync(Guid groupId);
|
Task<IEnumerable<GroupJoinRequest?>> FindByGroupIdAsync(Guid groupId);
|
||||||
Task<IEnumerable<GroupJoinRequest>> FindByUserIdAsync(Guid userId);
|
Task<IEnumerable<GroupJoinRequest>> FindByUserIdAsync(Guid userId);
|
||||||
|
Task<IEnumerable<GroupJoinRequest>> FindVisibleToUserAsync(Guid userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ namespace GroupService.Infrastructure.Configs
|
|||||||
builder.ToTable("group_join_requests");
|
builder.ToTable("group_join_requests");
|
||||||
builder.HasKey(x => x.Id);
|
builder.HasKey(x => x.Id);
|
||||||
builder.HasKey(x => new { x.GroupId, x.UserId });
|
builder.HasKey(x => new { x.GroupId, x.UserId });
|
||||||
|
builder.HasIndex(x => x.Id).IsUnique();
|
||||||
|
builder.HasIndex(x => new { x.GroupId, x.State, x.CreationTime });
|
||||||
|
builder.HasIndex(x => new { x.UserId, x.CreationTime });
|
||||||
|
|
||||||
builder.ComplexProperty(x => x.UserProfile, u =>
|
builder.ComplexProperty(x => x.UserProfile, u =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ namespace GroupService.Infrastructure.Configs
|
|||||||
public void Configure(EntityTypeBuilder<GroupMember> builder)
|
public void Configure(EntityTypeBuilder<GroupMember> builder)
|
||||||
{
|
{
|
||||||
builder.ToTable("group_members");
|
builder.ToTable("group_members");
|
||||||
|
builder.HasIndex(x => new { x.UserId, x.IsDeleted, x.GroupId });
|
||||||
|
builder.HasIndex(x => new { x.GroupId, x.IsDeleted, x.Role });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
namespace GroupService.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(GroupDbContext))]
|
||||||
|
[Migration("20260909000200_ApiAlignmentFixes")]
|
||||||
|
public partial class ApiAlignmentFixes : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_group_members_UserId_IsDeleted_GroupId",
|
||||||
|
table: "group_members",
|
||||||
|
columns: new[] { "UserId", "IsDeleted", "GroupId" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_group_members_GroupId_IsDeleted_Role",
|
||||||
|
table: "group_members",
|
||||||
|
columns: new[] { "GroupId", "IsDeleted", "Role" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_group_join_requests_Id",
|
||||||
|
table: "group_join_requests",
|
||||||
|
column: "Id",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_group_join_requests_GroupId_State_CreationTime",
|
||||||
|
table: "group_join_requests",
|
||||||
|
columns: new[] { "GroupId", "State", "CreationTime" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_group_join_requests_UserId_CreationTime",
|
||||||
|
table: "group_join_requests",
|
||||||
|
columns: new[] { "UserId", "CreationTime" });
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex("IX_group_members_UserId_IsDeleted_GroupId", "group_members");
|
||||||
|
migrationBuilder.DropIndex("IX_group_members_GroupId_IsDeleted_Role", "group_members");
|
||||||
|
migrationBuilder.DropIndex("IX_group_join_requests_Id", "group_join_requests");
|
||||||
|
migrationBuilder.DropIndex("IX_group_join_requests_GroupId_State_CreationTime", "group_join_requests");
|
||||||
|
migrationBuilder.DropIndex("IX_group_join_requests_UserId_CreationTime", "group_join_requests");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -227,6 +227,13 @@ namespace GroupService.Infrastructure.Migrations
|
|||||||
|
|
||||||
b.HasKey("GroupId", "UserId");
|
b.HasKey("GroupId", "UserId");
|
||||||
|
|
||||||
|
b.HasIndex("Id")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("GroupId", "State", "CreationTime");
|
||||||
|
|
||||||
|
b.HasIndex("UserId", "CreationTime");
|
||||||
|
|
||||||
b.ToTable("group_join_requests", (string)null);
|
b.ToTable("group_join_requests", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -266,6 +273,10 @@ namespace GroupService.Infrastructure.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("GroupId", "IsDeleted", "Role");
|
||||||
|
|
||||||
|
b.HasIndex("UserId", "IsDeleted", "GroupId");
|
||||||
|
|
||||||
b.ToTable("group_members", (string)null);
|
b.ToTable("group_members", (string)null);
|
||||||
});
|
});
|
||||||
#pragma warning restore 612, 618
|
#pragma warning restore 612, 618
|
||||||
|
|||||||
@@ -36,5 +36,19 @@ namespace GroupService.Infrastructure.Reposities
|
|||||||
x.UserId == userId || x.OperatorId == userId
|
x.UserId == userId || x.OperatorId == userId
|
||||||
).ToListAsync();
|
).ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<GroupJoinRequest>> FindVisibleToUserAsync(Guid userId)
|
||||||
|
{
|
||||||
|
var managedGroupIds = db.GroupMembers
|
||||||
|
.Where(member => member.UserId == userId &&
|
||||||
|
(member.Role == Domain.Enums.GroupMemberRole.Administrator ||
|
||||||
|
member.Role == Domain.Enums.GroupMemberRole.Master))
|
||||||
|
.Select(member => member.GroupId);
|
||||||
|
|
||||||
|
return await db.GroupJoinRequests
|
||||||
|
.Where(request => request.UserId == userId || managedGroupIds.Contains(request.GroupId))
|
||||||
|
.OrderByDescending(request => request.CreationTime)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,18 @@ namespace GroupService.Infrastructure.Reposities
|
|||||||
return await db.Groups.Where(x => x.GroupMaster == userId).ToListAsync();
|
return await db.Groups.Where(x => x.GroupMaster == userId).ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<Group>> FindByMemberIdAsync(Guid userId)
|
||||||
|
{
|
||||||
|
var groupIds = db.GroupMembers
|
||||||
|
.Where(member => member.UserId == userId)
|
||||||
|
.Select(member => member.GroupId);
|
||||||
|
|
||||||
|
return await db.Groups
|
||||||
|
.Where(group => groupIds.Contains(group.Id))
|
||||||
|
.OrderByDescending(group => group.ModificationTime ?? group.CreationTime)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<Group>> FindByNameAsync(string name)
|
public async Task<IEnumerable<Group>> FindByNameAsync(string name)
|
||||||
{
|
{
|
||||||
return await db.Groups.Where(x => x.Name == name).ToListAsync();
|
return await db.Groups.Where(x => x.Name == name).ToListAsync();
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using GroupService.Domain.Events;
|
||||||
|
using IM.Commons.IntegrationEvents;
|
||||||
|
using MassTransit;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace GroupService.WebApi.Application.EventHandler
|
||||||
|
{
|
||||||
|
public class GroupMemberLeftHandler(IPublishEndpoint endpoint)
|
||||||
|
: INotificationHandler<GroupMemberLeftDomainEvent>
|
||||||
|
{
|
||||||
|
public Task Handle(GroupMemberLeftDomainEvent notification, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return endpoint.Publish(
|
||||||
|
new GroupMemberLeftEvent(notification.Member.UserId, notification.Member.GroupId),
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,11 +27,11 @@ namespace GroupService.WebApi.Application.Group
|
|||||||
|
|
||||||
public async Task<Result<List<GroupResponse>>> GetAllAsync(Guid userId)
|
public async Task<Result<List<GroupResponse>>> GetAllAsync(Guid userId)
|
||||||
{
|
{
|
||||||
var groups = await reposity.FindByMasterIdAsync(userId);
|
var groups = await reposity.FindByMemberIdAsync(userId);
|
||||||
return Result<List<GroupResponse>>.Success(mapper.Map<List<GroupResponse>>(groups));
|
return Result<List<GroupResponse>>.Success(mapper.Map<List<GroupResponse>>(groups));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Result<GroupResponse>> GetByIdAsync(Guid groupId)
|
public async Task<Result<GroupResponse>> GetByIdAsync(Guid groupId, Guid userId)
|
||||||
{
|
{
|
||||||
var group = await reposity.FindByIdAsync(groupId);
|
var group = await reposity.FindByIdAsync(groupId);
|
||||||
if (group is null)
|
if (group is null)
|
||||||
@@ -39,9 +39,36 @@ namespace GroupService.WebApi.Application.Group
|
|||||||
return Result<GroupResponse>.Fail(ResultCode.GROUP_NOT_FOUND);
|
return Result<GroupResponse>.Fail(ResultCode.GROUP_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!await memberReposity.CheckMemberExistAsync(groupId, userId))
|
||||||
|
{
|
||||||
|
return Result<GroupResponse>.Fail(ResultCode.PERMISSION_DENIED);
|
||||||
|
}
|
||||||
|
|
||||||
return Result<GroupResponse>.Success(mapper.Map<GroupResponse>(group));
|
return Result<GroupResponse>.Success(mapper.Map<GroupResponse>(group));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<Result<object>> DissolveAsync(Guid groupId, Guid userId)
|
||||||
|
{
|
||||||
|
var group = await reposity.FindByIdAsync(groupId);
|
||||||
|
if (group is null)
|
||||||
|
{
|
||||||
|
return Result.Fail(ResultCode.GROUP_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (group.GroupMaster != userId)
|
||||||
|
{
|
||||||
|
return Result.Fail(ResultCode.PERMISSION_DENIED);
|
||||||
|
}
|
||||||
|
|
||||||
|
var members = await memberReposity.FindByGroupIdAsync(groupId);
|
||||||
|
foreach (var member in members)
|
||||||
|
{
|
||||||
|
member.Leave();
|
||||||
|
}
|
||||||
|
group.SoftDelete();
|
||||||
|
return Result.Success();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<Result<GroupResponse>> UpdateAsync(GroupUpdateCommand command)
|
public async Task<Result<GroupResponse>> UpdateAsync(GroupUpdateCommand command)
|
||||||
{
|
{
|
||||||
var group = await reposity.FindByIdAsync(command.GroupId);
|
var group = await reposity.FindByIdAsync(command.GroupId);
|
||||||
|
|||||||
@@ -24,13 +24,17 @@ namespace GroupService.WebApi.Application.GroupMember
|
|||||||
this.mapper = mapper;
|
this.mapper = mapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Result<List<GroupMemberResponse>>> GetByGroupIdAsync(Guid groupId)
|
public async Task<Result<List<GroupMemberResponse>>> GetByGroupIdAsync(Guid groupId, Guid userId)
|
||||||
{
|
{
|
||||||
var group = await groupReposity.FindByIdAsync(groupId);
|
var group = await groupReposity.FindByIdAsync(groupId);
|
||||||
if (group is null)
|
if (group is null)
|
||||||
{
|
{
|
||||||
return Result<List<GroupMemberResponse>>.Fail(ResultCode.GROUP_NOT_FOUND);
|
return Result<List<GroupMemberResponse>>.Fail(ResultCode.GROUP_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
if (!await reposity.CheckMemberExistAsync(groupId, userId))
|
||||||
|
{
|
||||||
|
return Result<List<GroupMemberResponse>>.Fail(ResultCode.PERMISSION_DENIED);
|
||||||
|
}
|
||||||
var members = await reposity.FindByGroupIdAsync(groupId);
|
var members = await reposity.FindByGroupIdAsync(groupId);
|
||||||
|
|
||||||
return Result<List<GroupMemberResponse>>.Success(mapper.Map<List<GroupMemberResponse>>(members.ToList()));
|
return Result<List<GroupMemberResponse>>.Success(mapper.Map<List<GroupMemberResponse>>(members.ToList()));
|
||||||
@@ -78,14 +82,32 @@ namespace GroupService.WebApi.Application.GroupMember
|
|||||||
}
|
}
|
||||||
|
|
||||||
var operatorMember = await reposity.FindOneByGroupIdAndUserIdAsync(member.GroupId, operatorId);
|
var operatorMember = await reposity.FindOneByGroupIdAndUserIdAsync(member.GroupId, operatorId);
|
||||||
if (operatorMember is null || operatorMember.Role == Domain.Enums.GroupMemberRole.Normal)
|
if (operatorMember is null || operatorMember.Id == member.Id ||
|
||||||
|
member.Role == Domain.Enums.GroupMemberRole.Master ||
|
||||||
|
operatorMember.Role <= member.Role)
|
||||||
{
|
{
|
||||||
return Result.Fail(ResultCode.PERMISSION_DENIED);
|
return Result.Fail(ResultCode.PERMISSION_DENIED);
|
||||||
}
|
}
|
||||||
|
|
||||||
member.SoftDelete();
|
member.Leave();
|
||||||
|
|
||||||
return Result.Success();
|
return Result.Success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<Result<object>> LeaveAsync(Guid groupId, Guid userId)
|
||||||
|
{
|
||||||
|
var member = await reposity.FindOneByGroupIdAndUserIdAsync(groupId, userId);
|
||||||
|
if (member is null)
|
||||||
|
{
|
||||||
|
return Result.Fail(ResultCode.GROUP_MEMBER_NOT_FOUNT);
|
||||||
|
}
|
||||||
|
if (member.Role == Domain.Enums.GroupMemberRole.Master)
|
||||||
|
{
|
||||||
|
return Result.Fail<object>(ResultCode.PERMISSION_DENIED, "群主不能直接退群,请使用解散群接口");
|
||||||
|
}
|
||||||
|
|
||||||
|
member.Leave();
|
||||||
|
return Result.Success();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ namespace GroupService.WebApi.Application.GroupRequest
|
|||||||
}
|
}
|
||||||
public async Task<Result<List<GroupRequestResponse>>> GetListAsync(Guid userId)
|
public async Task<Result<List<GroupRequestResponse>>> GetListAsync(Guid userId)
|
||||||
{
|
{
|
||||||
var list = await reposity.FindByUserIdAsync(userId);
|
var list = await reposity.FindVisibleToUserAsync(userId);
|
||||||
return Result.Success(mapper.Map<List<GroupRequestResponse>>(list.ToList()));
|
return Result.Success(mapper.Map<List<GroupRequestResponse>>(list.ToList()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ namespace GroupService.WebApi.Controllers.Group
|
|||||||
[ProducesDefaultResponseType(typeof(Result<GroupResponse>))]
|
[ProducesDefaultResponseType(typeof(Result<GroupResponse>))]
|
||||||
public async Task<IActionResult> GetOne(Guid groupId)
|
public async Task<IActionResult> GetOne(Guid groupId)
|
||||||
{
|
{
|
||||||
return Ok(await service.GetByIdAsync(groupId));
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
|
return Ok(await service.GetByIdAsync(groupId, Guid.Parse(userId)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
@@ -49,5 +50,13 @@ namespace GroupService.WebApi.Controllers.Group
|
|||||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
return Ok(await service.UpdateAsync(new GroupUpdateCommand(Guid.Parse(userId), request.GroupId, request.Avatar, request.GroupName, request.Description)));
|
return Ok(await service.UpdateAsync(new GroupUpdateCommand(Guid.Parse(userId), request.GroupId, request.Avatar, request.GroupName, request.Description)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[UnitOfWork(typeof(GroupDbContext))]
|
||||||
|
public async Task<IActionResult> Dissolve([FromQuery] Guid groupId)
|
||||||
|
{
|
||||||
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
|
return Ok(await service.DissolveAsync(groupId, Guid.Parse(userId)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
using GroupService.Infrastructure;
|
using GroupService.Infrastructure;
|
||||||
using GroupService.WebApi.Application.GroupMember;
|
using GroupService.WebApi.Application.GroupMember;
|
||||||
using IM.ASPNETCore;
|
using IM.ASPNETCore;
|
||||||
|
using IM.Commons;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
|
|
||||||
@@ -11,25 +13,36 @@ namespace GroupService.WebApi.Controllers.GroupMember
|
|||||||
public class GroupMemberController : ControllerBase
|
public class GroupMemberController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly GroupMemberService service;
|
private readonly GroupMemberService service;
|
||||||
|
private readonly IConfiguration configuration;
|
||||||
|
|
||||||
public GroupMemberController(GroupMemberService service)
|
public GroupMemberController(GroupMemberService service, IConfiguration configuration)
|
||||||
{
|
{
|
||||||
this.service = service;
|
this.service = service;
|
||||||
|
this.configuration = configuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<IActionResult> CheckMember(Guid userId, Guid groupId)
|
public async Task<IActionResult> CheckMember(Guid userId, Guid groupId)
|
||||||
{
|
{
|
||||||
|
var expectedKey = configuration["InternalApiKey"];
|
||||||
|
var suppliedKey = Request.Headers["X-Internal-Api-Key"].ToString();
|
||||||
|
if (string.IsNullOrWhiteSpace(expectedKey) || suppliedKey != expectedKey)
|
||||||
|
{
|
||||||
|
return Unauthorized(Result.Fail(ResultCode.AUTH_FAILED));
|
||||||
|
}
|
||||||
return Ok(await service.CheckMemberAsync(groupId, userId));
|
return Ok(await service.CheckMemberAsync(groupId, userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
|
[Authorize]
|
||||||
public async Task<IActionResult> List(Guid groupId)
|
public async Task<IActionResult> List(Guid groupId)
|
||||||
{
|
{
|
||||||
return Ok(await service.GetByGroupIdAsync(groupId));
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
|
return Ok(await service.GetByGroupIdAsync(groupId, Guid.Parse(userId)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
|
[Authorize]
|
||||||
[UnitOfWork(typeof(GroupDbContext))]
|
[UnitOfWork(typeof(GroupDbContext))]
|
||||||
public async Task<IActionResult> Delete([FromQuery] Guid memberId)
|
public async Task<IActionResult> Delete([FromQuery] Guid memberId)
|
||||||
{
|
{
|
||||||
@@ -37,6 +50,15 @@ namespace GroupService.WebApi.Controllers.GroupMember
|
|||||||
return Ok(await service.DeleteAsync(memberId, Guid.Parse(userId)));
|
return Ok(await service.DeleteAsync(memberId, Guid.Parse(userId)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Authorize]
|
||||||
|
[UnitOfWork(typeof(GroupDbContext))]
|
||||||
|
public async Task<IActionResult> Leave([FromQuery] Guid groupId)
|
||||||
|
{
|
||||||
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
|
return Ok(await service.LeaveAsync(groupId, Guid.Parse(userId)));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"InternalApiKey": "development-only-change-me",
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
|
|||||||
@@ -5,5 +5,6 @@
|
|||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*"
|
"AllowedHosts": "*",
|
||||||
|
"InternalApiKey": ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
using IM.Commons;
|
using IM.Commons;
|
||||||
using IM.DomainCommons;
|
using IM.DomainCommons;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace IM.ASPNETCore
|
namespace IM.ASPNETCore
|
||||||
{
|
{
|
||||||
public class ExceptionMiddleware
|
public class ExceptionMiddleware
|
||||||
{
|
{
|
||||||
private readonly RequestDelegate _next;
|
private readonly RequestDelegate _next;
|
||||||
public ExceptionMiddleware(RequestDelegate next)
|
private readonly ILogger<ExceptionMiddleware> logger;
|
||||||
|
|
||||||
|
public ExceptionMiddleware(RequestDelegate next, ILogger<ExceptionMiddleware> logger)
|
||||||
{
|
{
|
||||||
_next = next;
|
_next = next;
|
||||||
|
this.logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task InvokeAsync(HttpContext context)
|
public async Task InvokeAsync(HttpContext context)
|
||||||
@@ -22,12 +26,35 @@ namespace IM.ASPNETCore
|
|||||||
{
|
{
|
||||||
await DomainExceptionHandlerAsync(context, ex);
|
await DomainExceptionHandlerAsync(context, ex);
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await UnhandledExceptionHandlerAsync(context, ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task DomainExceptionHandlerAsync(HttpContext context, DomainException ex)
|
public Task DomainExceptionHandlerAsync(HttpContext context, DomainException ex)
|
||||||
{
|
{
|
||||||
context.Response.ContentType = "application/json";
|
context.Response.ContentType = "application/json";
|
||||||
var result = Result<object>.Fail(ResultCode.PARAMETER_ERROR, ex.Message); // 包装成你的 Result
|
context.Response.StatusCode = StatusCodes.Status400BadRequest;
|
||||||
|
var result = Result<object>.Fail(ResultCode.PARAMETER_ERROR, ex.Message);
|
||||||
|
return context.Response.WriteAsJsonAsync(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task UnhandledExceptionHandlerAsync(HttpContext context, Exception ex)
|
||||||
|
{
|
||||||
|
var correlationId = context.TraceIdentifier;
|
||||||
|
logger.LogError(ex,
|
||||||
|
"Unhandled exception. CorrelationId: {CorrelationId}, Path: {Path}",
|
||||||
|
correlationId,
|
||||||
|
context.Request.Path);
|
||||||
|
|
||||||
|
context.Response.ContentType = "application/json";
|
||||||
|
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
|
||||||
|
context.Response.Headers["X-Correlation-ID"] = correlationId;
|
||||||
|
|
||||||
|
var result = Result<object>.Fail(
|
||||||
|
ResultCode.SYSTEM_ERROR,
|
||||||
|
$"系统错误,关联编号:{correlationId}");
|
||||||
return context.Response.WriteAsJsonAsync(result);
|
return context.Response.WriteAsJsonAsync(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
namespace IM.Commons.IntegrationEvents
|
||||||
|
{
|
||||||
|
public record GroupMemberLeftEvent(Guid UserId, Guid GroupId);
|
||||||
|
}
|
||||||
@@ -20,6 +20,8 @@ namespace IM.Commons.IntegrationEvents
|
|||||||
public string ContentType { get; set; }
|
public string ContentType { get; set; }
|
||||||
public string CheckSun { get; set; }
|
public string CheckSun { get; set; }
|
||||||
public IReadOnlyList<UploadPart> Parts { get; set; }
|
public IReadOnlyList<UploadPart> Parts { get; set; }
|
||||||
|
public string? ChatType { get; set; }
|
||||||
|
public Guid? TargetId { get; set; }
|
||||||
}
|
}
|
||||||
public sealed record UploadPart(
|
public sealed record UploadPart(
|
||||||
int PartNumber,
|
int PartNumber,
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ namespace IM.InitCommon
|
|||||||
c.Password(options.Password);
|
c.Password(options.Password);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
cfg.UseMessageRetry(retry => retry.Intervals(
|
||||||
|
TimeSpan.FromMilliseconds(200),
|
||||||
|
TimeSpan.FromSeconds(1),
|
||||||
|
TimeSpan.FromSeconds(5)));
|
||||||
cfg.ConfigureEndpoints(context);
|
cfg.ConfigureEndpoints(context);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ namespace IM.Jwt
|
|||||||
context.HandleResponse();
|
context.HandleResponse();
|
||||||
|
|
||||||
context.Response.ContentType = "application/json";
|
context.Response.ContentType = "application/json";
|
||||||
context.Response.StatusCode = StatusCodes.Status200OK;
|
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||||
|
|
||||||
var result = Result<object>.Fail(ResultCode.AUTH_FAILED);
|
var result = Result<object>.Fail(ResultCode.AUTH_FAILED);
|
||||||
await context.Response.WriteAsJsonAsync(result);
|
await context.Response.WriteAsJsonAsync(result);
|
||||||
@@ -71,7 +71,7 @@ namespace IM.Jwt
|
|||||||
{
|
{
|
||||||
|
|
||||||
context.Response.ContentType = "application/json";
|
context.Response.ContentType = "application/json";
|
||||||
context.Response.StatusCode = StatusCodes.Status200OK;
|
context.Response.StatusCode = StatusCodes.Status403Forbidden;
|
||||||
var result = Result<object>.Fail(ResultCode.PERMISSION_DENIED);
|
var result = Result<object>.Fail(ResultCode.PERMISSION_DENIED);
|
||||||
await context.Response.WriteAsJsonAsync(result);
|
await context.Response.WriteAsJsonAsync(result);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# API 对齐数据库迁移运行手册
|
||||||
|
|
||||||
|
## 适用迁移
|
||||||
|
|
||||||
|
- MessageService:`20260909000100_ApiAlignmentFixes`
|
||||||
|
- GroupService:`20260909000200_ApiAlignmentFixes`
|
||||||
|
- FileService:`20260909000300_AsyncUploadResult`
|
||||||
|
|
||||||
|
## 发布前检查
|
||||||
|
|
||||||
|
先完成三个库的可恢复备份,并在对应数据库执行:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- GroupService:新增 Id 唯一索引前必须无重复。
|
||||||
|
SELECT Id, COUNT(*) AS duplicate_count
|
||||||
|
FROM group_join_requests
|
||||||
|
GROUP BY Id
|
||||||
|
HAVING COUNT(*) > 1;
|
||||||
|
|
||||||
|
-- FileService:收紧 longtext 前检查历史最大长度。
|
||||||
|
SELECT
|
||||||
|
MAX(CHAR_LENGTH(FileName)) AS max_file_name,
|
||||||
|
MAX(CHAR_LENGTH(ContentType)) AS max_content_type,
|
||||||
|
MAX(CHAR_LENGTH(checksum_algorithm)) AS max_checksum_algorithm,
|
||||||
|
MAX(CHAR_LENGTH(checksum_value)) AS max_checksum_value,
|
||||||
|
MAX(CHAR_LENGTH(storage_provider)) AS max_storage_provider,
|
||||||
|
MAX(CHAR_LENGTH(storage_bucket)) AS max_storage_bucket,
|
||||||
|
MAX(CHAR_LENGTH(storage_key)) AS max_storage_key,
|
||||||
|
MAX(CHAR_LENGTH(storage_region)) AS max_storage_region
|
||||||
|
FROM upload_files;
|
||||||
|
```
|
||||||
|
|
||||||
|
上面的长度必须分别不超过 `255/255/16/128/64/255/1024/128`。对 `upload_tasks` 执行同样检查。超长值应先人工确认和修正,不要依赖数据库静默截断。
|
||||||
|
|
||||||
|
## 执行迁移
|
||||||
|
|
||||||
|
在仓库根目录设置目标数据库连接字符串后执行。不要把真实密码写入仓库或命令记录。
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:DefaultDB_ConnStr = '<MessageService MySQL connection string>'
|
||||||
|
dotnet ef database update --project MessageService.Infrastructure --startup-project MessageService.WebApi --context MessageDbContext
|
||||||
|
|
||||||
|
$env:DefaultDB_ConnStr = '<GroupService MySQL connection string>'
|
||||||
|
dotnet ef database update --project GroupService.Infrastructure --startup-project GroupService.WebApi --context GroupDbContext
|
||||||
|
|
||||||
|
$env:DefaultDB_ConnStr = '<FileService MySQL connection string>'
|
||||||
|
dotnet ef database update --project FileService.Infrastructure --startup-project FileService.WebApi --context FileDbContext
|
||||||
|
```
|
||||||
|
|
||||||
|
建议顺序为 Message → Group → File,随后发布后端,再发布最终前端包。
|
||||||
|
|
||||||
|
## 数据兼容说明
|
||||||
|
|
||||||
|
- 所有新业务列均可空或有安全默认值,不删除历史记录。
|
||||||
|
- 历史文件的 `IsPublic` 默认 `false`,无法确认作用域的旧文件因此只允许所有者读取。
|
||||||
|
- 新上传文件会写入 `SourceTaskId/ChatType/TargetId/ResultFileId`;不要批量猜测旧文件作用域。
|
||||||
|
- 群退出、群解散和会话隐藏使用软删除。
|
||||||
|
|
||||||
|
## 回滚
|
||||||
|
|
||||||
|
只有在已经回滚依赖新字段/接口的前后端版本后,才允许回滚数据库迁移:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:DefaultDB_ConnStr = '<MessageService MySQL connection string>'
|
||||||
|
dotnet ef database update 20260423115234_InitMessageDb --project MessageService.Infrastructure --startup-project MessageService.WebApi --context MessageDbContext
|
||||||
|
|
||||||
|
$env:DefaultDB_ConnStr = '<GroupService MySQL connection string>'
|
||||||
|
dotnet ef database update 20260429103435_removeGroupRequestOperatorProfile --project GroupService.Infrastructure --startup-project GroupService.WebApi --context GroupDbContext
|
||||||
|
|
||||||
|
$env:DefaultDB_ConnStr = '<FileService MySQL connection string>'
|
||||||
|
dotnet ef database update 20260509073447_InitFileDb --project FileService.Infrastructure --startup-project FileService.WebApi --context FileDbContext
|
||||||
|
```
|
||||||
|
|
||||||
|
FileService 回滚会删除新作用域和任务结果列,并把收紧的字符串列恢复为 `longtext`;回滚前应另行导出这些新列的数据。
|
||||||
@@ -57,24 +57,22 @@ namespace MessageService.Domain.Entities
|
|||||||
AddDomainEvent(new ConversationCreatedDomainEvent(this));
|
AddDomainEvent(new ConversationCreatedDomainEvent(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Update(long? LastReadSequenceId = default, int? unreadCount = default, string? lastMsg = default)
|
public void SetLastReadSequence(long sequenceId)
|
||||||
{
|
{
|
||||||
if (LastReadSequenceId != null)
|
LastReadSequenceId = sequenceId;
|
||||||
{
|
NotifyModified();
|
||||||
LastReadSequenceId = LastReadSequenceId.Value;
|
}
|
||||||
this.NotifyModified();
|
|
||||||
}
|
|
||||||
if (unreadCount != null)
|
|
||||||
{
|
|
||||||
UnreadCount += unreadCount.Value;
|
|
||||||
NotifyModified();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastMsg != null)
|
public void IncrementUnread()
|
||||||
{
|
{
|
||||||
LastMessage = lastMsg;
|
UnreadCount += 1;
|
||||||
NotifyModified();
|
NotifyModified();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void UpdateLastMessage(string lastMessage)
|
||||||
|
{
|
||||||
|
LastMessage = lastMessage;
|
||||||
|
NotifyModified();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -86,6 +84,7 @@ namespace MessageService.Domain.Entities
|
|||||||
UnreadCount = 0;
|
UnreadCount = 0;
|
||||||
if (lastReadSequenceId.HasValue)
|
if (lastReadSequenceId.HasValue)
|
||||||
LastReadSequenceId = lastReadSequenceId.Value;
|
LastReadSequenceId = lastReadSequenceId.Value;
|
||||||
|
NotifyModified();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateProfile(string name, string avatar)
|
public void UpdateProfile(string name, string avatar)
|
||||||
|
|||||||
@@ -91,10 +91,10 @@ namespace MessageService.Domain.Entities
|
|||||||
/// <param name="thumb">预览图</param>
|
/// <param name="thumb">预览图</param>
|
||||||
/// <param name="sequenceId"></param>
|
/// <param name="sequenceId"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static Message BuildImg(MessageCreateContext ctx, string url,
|
public static Message BuildImg(MessageCreateContext ctx, string? url,
|
||||||
int width, int height, string thumb, long sequenceId)
|
int width, int height, string thumb, long sequenceId, Guid? fileId = null)
|
||||||
{
|
{
|
||||||
var content = new MessageContent("[图片]", new ImageBody(url, width, height, thumb));
|
var content = new MessageContent("[图片]", new ImageBody(url, width, height, thumb, fileId));
|
||||||
return new Message(ctx, sequenceId, MessageType.Image, content);
|
return new Message(ctx, sequenceId, MessageType.Image, content);
|
||||||
}
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -107,10 +107,10 @@ namespace MessageService.Domain.Entities
|
|||||||
/// <param name="thumb"></param>
|
/// <param name="thumb"></param>
|
||||||
/// <param name="sequenceId"></param>
|
/// <param name="sequenceId"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static Message BuildVideo(MessageCreateContext ctx, string url,
|
public static Message BuildVideo(MessageCreateContext ctx, string? url,
|
||||||
int width, int height, string thumb, long sequenceId)
|
int width, int height, string thumb, long sequenceId, Guid? fileId = null)
|
||||||
{
|
{
|
||||||
var content = new MessageContent("[视频]", new VideoBody(url, width, height, thumb));
|
var content = new MessageContent("[视频]", new VideoBody(url, width, height, thumb, fileId));
|
||||||
return new Message(ctx, sequenceId, MessageType.Video, content);
|
return new Message(ctx, sequenceId, MessageType.Video, content);
|
||||||
}
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -121,10 +121,10 @@ namespace MessageService.Domain.Entities
|
|||||||
/// <param name="duration">持续时间</param>
|
/// <param name="duration">持续时间</param>
|
||||||
/// <param name="sequenceId"></param>
|
/// <param name="sequenceId"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static Message BuildVoice(MessageCreateContext ctx, string url,
|
public static Message BuildVoice(MessageCreateContext ctx, string? url,
|
||||||
int duration, long sequenceId)
|
int duration, long sequenceId, Guid? fileId = null)
|
||||||
{
|
{
|
||||||
var content = new MessageContent("[音频]", new VoiceBody(url, duration));
|
var content = new MessageContent("[音频]", new VoiceBody(url, duration, fileId));
|
||||||
return new Message(ctx, sequenceId, MessageType.Voice, content);
|
return new Message(ctx, sequenceId, MessageType.Voice, content);
|
||||||
}
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -137,11 +137,11 @@ namespace MessageService.Domain.Entities
|
|||||||
/// <param name="format">文件格式</param>
|
/// <param name="format">文件格式</param>
|
||||||
/// <param name="sequenceId"></param>
|
/// <param name="sequenceId"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static Message BuildFile(MessageCreateContext ctx, string url,
|
public static Message BuildFile(MessageCreateContext ctx, Guid fileId, string? url,
|
||||||
string name, long size,
|
string name, long size,
|
||||||
string format, long sequenceId)
|
string format, long sequenceId)
|
||||||
{
|
{
|
||||||
var content = new MessageContent("[文件]", new FileBody(url, name, size, format));
|
var content = new MessageContent("[文件]", new FileBody(fileId, url, name, size, format));
|
||||||
return new Message(ctx, sequenceId, MessageType.File, content);
|
return new Message(ctx, sequenceId, MessageType.File, content);
|
||||||
}
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
namespace MessageService.Domain.KeyObjects
|
namespace MessageService.Domain.KeyObjects
|
||||||
{
|
{
|
||||||
public record TextBody(string Text);
|
public record TextBody(string Text);
|
||||||
public record ImageBody(string Url, int Width, int Height, string Thumb);
|
public record ImageBody(string? Url, int Width, int Height, string Thumb, Guid? FileId = null);
|
||||||
public record VideoBody(string Url, int Width, int Height, string Thumb);
|
public record VideoBody(string? Url, int Width, int Height, string Thumb, Guid? FileId = null);
|
||||||
public record VoiceBody(string Url, int Duration);
|
public record VoiceBody(string? Url, int Duration, Guid? FileId = null);
|
||||||
public record FileBody(string Url, string FileName, long Size, string Format);
|
public record FileBody(Guid FileId, string? Url, string FileName, long Size, string Format);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ namespace MessageService.Infrastructure.Configs
|
|||||||
builder.ToTable("conversations");
|
builder.ToTable("conversations");
|
||||||
builder.HasKey(x => x.Id);
|
builder.HasKey(x => x.Id);
|
||||||
builder.HasIndex(x => x.UserId);
|
builder.HasIndex(x => x.UserId);
|
||||||
|
builder.HasIndex(x => new { x.UserId, x.ChatType, x.TargetId, x.IsDeleted });
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
namespace MessageService.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(MessageDbContext))]
|
||||||
|
[Migration("20260909000100_ApiAlignmentFixes")]
|
||||||
|
public partial class ApiAlignmentFixes : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_conversations_UserId_ChatType_TargetId_IsDeleted",
|
||||||
|
table: "conversations",
|
||||||
|
columns: new[] { "UserId", "ChatType", "TargetId", "IsDeleted" });
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_conversations_UserId_ChatType_TargetId_IsDeleted",
|
||||||
|
table: "conversations");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -76,6 +76,8 @@ namespace MessageService.Infrastructure.Migrations
|
|||||||
|
|
||||||
b.HasIndex("UserId");
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId", "ChatType", "TargetId", "IsDeleted");
|
||||||
|
|
||||||
b.ToTable("conversations", (string)null);
|
b.ToTable("conversations", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -26,30 +26,32 @@ namespace MessageService.Infrastructure.Reposities
|
|||||||
public async Task<(IEnumerable<Message> messages, bool hasMore)> GetAsync(string streamKey, long? cusor, int direction, int limit)
|
public async Task<(IEnumerable<Message> messages, bool hasMore)> GetAsync(string streamKey, long? cusor, int direction, int limit)
|
||||||
{
|
{
|
||||||
var query = db.Messages.Where(x => x.StreamKey == streamKey);
|
var query = db.Messages.Where(x => x.StreamKey == streamKey);
|
||||||
List<Message> messages = [];
|
List<Message> fetched;
|
||||||
if (direction == 0) // Before: 找比锚点小的,按倒序排
|
if (direction == 0) // Before: 找比锚点小的,按倒序排
|
||||||
{
|
{
|
||||||
if (cusor.HasValue)
|
if (cusor.HasValue)
|
||||||
query = query.Where(m => m.SequenceId < cusor.Value);
|
query = query.Where(m => m.SequenceId < cusor.Value);
|
||||||
|
|
||||||
var list = await query
|
fetched = await query
|
||||||
.OrderByDescending(m => m.SequenceId) // 最新消息在最前
|
.OrderByDescending(m => m.SequenceId) // 最新消息在最前
|
||||||
.Take(limit + 1)
|
.Take(limit + 1)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
messages = [.. list.OrderBy(s => s.SequenceId)];
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (cusor is null)
|
if (cusor is null)
|
||||||
return (messages, false);
|
return (Array.Empty<Message>(), false);
|
||||||
|
|
||||||
messages = await query.OrderBy(o => o.SequenceId)
|
fetched = await query
|
||||||
|
.Where(m => m.SequenceId > cusor.Value)
|
||||||
|
.OrderBy(o => o.SequenceId)
|
||||||
.Take(limit + 1)
|
.Take(limit + 1)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (messages, messages.Count > limit);
|
var hasMore = fetched.Count > limit;
|
||||||
|
var messages = fetched.Take(limit).OrderBy(s => s.SequenceId).ToList();
|
||||||
|
return (messages, hasMore);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace MessageService.WebApi.Application.Conversation
|
|||||||
public ConversationMapperConfig()
|
public ConversationMapperConfig()
|
||||||
{
|
{
|
||||||
CreateMap<Domain.Entities.Conversation, ConversationResponse>()
|
CreateMap<Domain.Entities.Conversation, ConversationResponse>()
|
||||||
.ForMember(dest => dest.DateTime, opt => opt.MapFrom(src => src.ModificationTime))
|
.ForMember(dest => dest.DateTime, opt => opt.MapFrom(src => src.ModificationTime ?? src.CreationTime))
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,6 @@ namespace MessageService.WebApi.Application.Dtos
|
|||||||
/// 最后一条最新消息
|
/// 最后一条最新消息
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string LastMessage { get; set; }
|
public string LastMessage { get; set; }
|
||||||
public DateTime DateTime { get; set; }
|
public DateTimeOffset DateTime { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ using MessageService.Infrastructure;
|
|||||||
namespace MessageService.WebApi.Application.EventHandlers
|
namespace MessageService.WebApi.Application.EventHandlers
|
||||||
{
|
{
|
||||||
public class ConversationAddHandler : IConsumer<GroupMemberJoinedEvent>,
|
public class ConversationAddHandler : IConsumer<GroupMemberJoinedEvent>,
|
||||||
IConsumer<FriendAddedEvent>
|
IConsumer<FriendAddedEvent>, IConsumer<GroupMemberLeftEvent>
|
||||||
{
|
{
|
||||||
|
|
||||||
private readonly IConversationReposity reposity;
|
private readonly IConversationReposity reposity;
|
||||||
@@ -21,6 +21,11 @@ namespace MessageService.WebApi.Application.EventHandlers
|
|||||||
public async Task Consume(ConsumeContext<GroupMemberJoinedEvent> context)
|
public async Task Consume(ConsumeContext<GroupMemberJoinedEvent> context)
|
||||||
{
|
{
|
||||||
var @event = context.Message;
|
var @event = context.Message;
|
||||||
|
var existing = await reposity.FindByTargetIdAsync(@event.GroupId);
|
||||||
|
if (existing.Any(x => x.UserId == @event.UserId && x.ChatType == Domain.Enums.ChatType.GROUP))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
reposity.Create(new Domain.Entities.Conversation(
|
reposity.Create(new Domain.Entities.Conversation(
|
||||||
userId: @event.UserId,
|
userId: @event.UserId,
|
||||||
targetId: @event.GroupId,
|
targetId: @event.GroupId,
|
||||||
@@ -32,12 +37,17 @@ namespace MessageService.WebApi.Application.EventHandlers
|
|||||||
lastMessage: string.Empty
|
lastMessage: string.Empty
|
||||||
));
|
));
|
||||||
|
|
||||||
await messageDb.SaveChangesAsync();
|
await messageDb.SaveChangesAsync(context.CancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Consume(ConsumeContext<FriendAddedEvent> context)
|
public async Task Consume(ConsumeContext<FriendAddedEvent> context)
|
||||||
{
|
{
|
||||||
var @event = context.Message;
|
var @event = context.Message;
|
||||||
|
var existing = await reposity.FindByUserIdAsync(@event.OwnerId);
|
||||||
|
if (existing.Any(x => x.TargetId == @event.TargetId && x.ChatType == Domain.Enums.ChatType.PRIVATE))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
reposity.Create(new Domain.Entities.Conversation(
|
reposity.Create(new Domain.Entities.Conversation(
|
||||||
userId: @event.OwnerId,
|
userId: @event.OwnerId,
|
||||||
targetId: @event.TargetId,
|
targetId: @event.TargetId,
|
||||||
@@ -49,7 +59,18 @@ namespace MessageService.WebApi.Application.EventHandlers
|
|||||||
lastMessage: string.Empty
|
lastMessage: string.Empty
|
||||||
));
|
));
|
||||||
|
|
||||||
await messageDb.SaveChangesAsync();
|
await messageDb.SaveChangesAsync(context.CancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Consume(ConsumeContext<GroupMemberLeftEvent> context)
|
||||||
|
{
|
||||||
|
var conversations = await reposity.FindByTargetIdAsync(context.Message.GroupId);
|
||||||
|
foreach (var conversation in conversations.Where(x =>
|
||||||
|
x.UserId == context.Message.UserId && x.ChatType == Domain.Enums.ChatType.GROUP))
|
||||||
|
{
|
||||||
|
conversation.SoftDelete();
|
||||||
|
}
|
||||||
|
await messageDb.SaveChangesAsync(context.CancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,20 +24,24 @@ namespace MessageService.WebApi.Application.EventHandlers
|
|||||||
{
|
{
|
||||||
var message = notification.Message;
|
var message = notification.Message;
|
||||||
|
|
||||||
if(message.ChatType == Domain.Enums.ChatType.PRIVATE)
|
var conversations = await reposity.FindByStreamKeyAsync(message.StreamKey);
|
||||||
|
foreach (var conversation in conversations)
|
||||||
{
|
{
|
||||||
var list = await reposity.FindByStreamKeyAsync(message.StreamKey);
|
conversation.UpdateLastMessage(message.Content.Fallback);
|
||||||
var owner = list.First(x => x.UserId == message.SenderId);
|
if (conversation.UserId == message.SenderId)
|
||||||
var target = list.First(x => x.UserId == message.TargetId);
|
{
|
||||||
|
conversation.SetLastReadSequence(message.SequenceId);
|
||||||
owner.Update(message.SequenceId, 0, message.Content.Fallback);
|
}
|
||||||
target.Update(target.LastReadSequenceId, target.UnreadCount + 1, message.Content.Fallback);
|
else
|
||||||
|
{
|
||||||
messageDb.Conversations.UpdateRange(owner,target);
|
conversation.IncrementUnread();
|
||||||
await messageDb.SaveChangesAsync(cancellationToken);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await endpoint.Publish(message.ToIntegrationEvent());
|
messageDb.Conversations.UpdateRange(conversations);
|
||||||
|
await messageDb.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
await endpoint.Publish(message.ToIntegrationEvent(), cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Handle(MessageWithdrawDomainEvent notification, CancellationToken cancellationToken)
|
public async Task Handle(MessageWithdrawDomainEvent notification, CancellationToken cancellationToken)
|
||||||
|
|||||||
@@ -52,13 +52,16 @@ namespace MessageService.WebApi.Application.Message
|
|||||||
{
|
{
|
||||||
MessageType.Text => Domain.Entities.Message.BuildTxt(ctx, command.Text!, sequenceId),
|
MessageType.Text => Domain.Entities.Message.BuildTxt(ctx, command.Text!, sequenceId),
|
||||||
|
|
||||||
MessageType.Image => Domain.Entities.Message.BuildImg(ctx, command.Url!,
|
MessageType.Image => Domain.Entities.Message.BuildImg(ctx, command.Url,
|
||||||
command.Width ?? 0, command.Height ?? 0, command.Thumb!, sequenceId),
|
command.Width ?? 0, command.Height ?? 0, command.Thumb!, sequenceId, command.FileId),
|
||||||
|
|
||||||
MessageType.Video => Domain.Entities.Message.BuildVideo(ctx, command.Url!,
|
MessageType.Video => Domain.Entities.Message.BuildVideo(ctx, command.Url,
|
||||||
command.Width ?? 0, command.Height ?? 0, command.Thumb!, sequenceId),
|
command.Width ?? 0, command.Height ?? 0, command.Thumb!, sequenceId, command.FileId),
|
||||||
|
|
||||||
MessageType.Voice => Domain.Entities.Message.BuildVoice(ctx, command.Url!, command.Duration ?? 0, sequenceId),
|
MessageType.Voice => Domain.Entities.Message.BuildVoice(ctx, command.Url, command.Duration ?? 0, sequenceId, command.FileId),
|
||||||
|
|
||||||
|
MessageType.File => Domain.Entities.Message.BuildFile(ctx, command.FileId!.Value, command.Url,
|
||||||
|
command.FileName!, command.FileSize!.Value, command.FileFormat!, sequenceId),
|
||||||
|
|
||||||
_ => null
|
_ => null
|
||||||
};
|
};
|
||||||
@@ -109,6 +112,11 @@ namespace MessageService.WebApi.Application.Message
|
|||||||
|
|
||||||
public async Task<Result<GetMessagesResponse>> GetMessagesAsync(GetMessageCommand command)
|
public async Task<Result<GetMessagesResponse>> GetMessagesAsync(GetMessageCommand command)
|
||||||
{
|
{
|
||||||
|
if (command.direction is not 0 and not 1 || command.limit is < 1 or > 100)
|
||||||
|
{
|
||||||
|
return Result.Fail<GetMessagesResponse>(ResultCode.PARAMETER_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
var conversation = await conversationReposity.FindByIdAsync(command.conversationId);
|
var conversation = await conversationReposity.FindByIdAsync(command.conversationId);
|
||||||
|
|
||||||
if(conversation is null || conversation.UserId != command.userId)
|
if(conversation is null || conversation.UserId != command.userId)
|
||||||
|
|||||||
@@ -22,8 +22,12 @@ namespace MessageService.WebApi.Application.Message
|
|||||||
public int? Height { get; init; }
|
public int? Height { get; init; }
|
||||||
public string? Thumb { get; init; }
|
public string? Thumb { get; init; }
|
||||||
public int? Duration { get; init; }
|
public int? Duration { get; init; }
|
||||||
|
public Guid? FileId { get; init; }
|
||||||
|
public string? FileName { get; init; }
|
||||||
|
public long? FileSize { get; init; }
|
||||||
|
public string? FileFormat { get; init; }
|
||||||
|
|
||||||
public SendMsgCommand(Guid senderId, Guid targetId, ChatType chatType, MessageType msgType, Guid clientMsgId, Guid? quoteMessageId, Dictionary<string, string>? ext, string? text, string? url, int? width, int? height, string? thumb, int? duration)
|
public SendMsgCommand(Guid senderId, Guid targetId, ChatType chatType, MessageType msgType, Guid clientMsgId, Guid? quoteMessageId, Dictionary<string, string>? ext, string? text, string? url, int? width, int? height, string? thumb, int? duration, Guid? fileId, string? fileName, long? fileSize, string? fileFormat)
|
||||||
{
|
{
|
||||||
SenderId = senderId;
|
SenderId = senderId;
|
||||||
TargetId = targetId;
|
TargetId = targetId;
|
||||||
@@ -38,6 +42,10 @@ namespace MessageService.WebApi.Application.Message
|
|||||||
Height = height;
|
Height = height;
|
||||||
Thumb = thumb;
|
Thumb = thumb;
|
||||||
Duration = duration;
|
Duration = duration;
|
||||||
|
FileId = fileId;
|
||||||
|
FileName = fileName;
|
||||||
|
FileSize = fileSize;
|
||||||
|
FileFormat = fileFormat;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ namespace MessageService.WebApi.Controllers.Message
|
|||||||
public int? Height { get; init; }
|
public int? Height { get; init; }
|
||||||
public string? Thumb { get; init; }
|
public string? Thumb { get; init; }
|
||||||
public int? Duration { get; init; }
|
public int? Duration { get; init; }
|
||||||
|
public Guid? FileId { get; init; }
|
||||||
|
public string? FileName { get; init; }
|
||||||
|
public long? FileSize { get; init; }
|
||||||
|
public string? FileFormat { get; init; }
|
||||||
|
|
||||||
public SendMsgCommand ToCommand(Guid senderId)
|
public SendMsgCommand ToCommand(Guid senderId)
|
||||||
{
|
{
|
||||||
@@ -39,7 +43,11 @@ namespace MessageService.WebApi.Controllers.Message
|
|||||||
Width,
|
Width,
|
||||||
Height,
|
Height,
|
||||||
Thumb,
|
Thumb,
|
||||||
Duration
|
Duration,
|
||||||
|
FileId,
|
||||||
|
FileName,
|
||||||
|
FileSize,
|
||||||
|
FileFormat
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,9 +71,19 @@ namespace MessageService.WebApi.Controllers.Message
|
|||||||
|
|
||||||
// 图片/视频/语音消息:url 必填
|
// 图片/视频/语音消息:url 必填
|
||||||
When(r => r.MsgType == MessageType.Image || r.MsgType == MessageType.Video
|
When(r => r.MsgType == MessageType.Image || r.MsgType == MessageType.Video
|
||||||
|| r.MsgType == MessageType.Voice || r.MsgType == MessageType.File, () =>
|
|| r.MsgType == MessageType.Voice, () =>
|
||||||
{
|
{
|
||||||
RuleFor(r => r.Url).NotEmpty().WithMessage("媒体消息的 url 字段不能为空");
|
RuleFor(r => r)
|
||||||
|
.Must(request => !string.IsNullOrWhiteSpace(request.Url) || request.FileId.HasValue)
|
||||||
|
.WithMessage("媒体消息必须提供 url 或 fileId");
|
||||||
|
});
|
||||||
|
|
||||||
|
When(r => r.MsgType == MessageType.File, () =>
|
||||||
|
{
|
||||||
|
RuleFor(r => r.FileId).NotNull().NotEmpty().WithMessage("文件消息的 fileId 字段不能为空");
|
||||||
|
RuleFor(r => r.FileName).NotEmpty().WithMessage("文件消息的 fileName 字段不能为空");
|
||||||
|
RuleFor(r => r.FileSize).NotNull().GreaterThanOrEqualTo(0).WithMessage("文件消息的 fileSize 字段不合法");
|
||||||
|
RuleFor(r => r.FileFormat).NotEmpty().WithMessage("文件消息的 fileFormat 字段不能为空");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,9 +20,16 @@ namespace MessageService.WebApi
|
|||||||
services.AddScoped<ConversationService>();
|
services.AddScoped<ConversationService>();
|
||||||
services.AddScoped<SquenceService>();
|
services.AddScoped<SquenceService>();
|
||||||
services.AddScoped<IContactIntegrationService, ContactIntegrationService>();
|
services.AddScoped<IContactIntegrationService, ContactIntegrationService>();
|
||||||
services.AddHttpClient<IGroupMemberIntegrationService, GroupMemberIntegrationService>(c =>
|
services.AddHttpClient<IGroupMemberIntegrationService, GroupMemberIntegrationService>((sp, c) =>
|
||||||
{
|
{
|
||||||
c.BaseAddress = new Uri("http://im-group-service:8080/");
|
var configuration = sp.GetRequiredService<IConfiguration>();
|
||||||
|
c.BaseAddress = new Uri(configuration["InternalServices:GroupServiceBaseUrl"]
|
||||||
|
?? "http://im-group-service:8080/");
|
||||||
|
var internalApiKey = configuration["InternalApiKey"];
|
||||||
|
if (!string.IsNullOrWhiteSpace(internalApiKey))
|
||||||
|
{
|
||||||
|
c.DefaultRequestHeaders.Add("X-Internal-Api-Key", internalApiKey);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
services.AddGrpcClient<ContactInternal.ContactInternalClient>((sp, o) =>
|
services.AddGrpcClient<ContactInternal.ContactInternalClient>((sp, o) =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
{
|
{
|
||||||
|
"InternalApiKey": "development-only-change-me",
|
||||||
|
"InternalServices": {
|
||||||
|
"GroupServiceBaseUrl": "http://localhost:5070/"
|
||||||
|
},
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
|
|||||||
@@ -5,5 +5,9 @@
|
|||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*"
|
"AllowedHosts": "*",
|
||||||
|
"InternalApiKey": "",
|
||||||
|
"InternalServices": {
|
||||||
|
"GroupServiceBaseUrl": "http://im-group-service:8080/"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ services:
|
|||||||
ASPNETCORE_ENVIRONMENT: Production
|
ASPNETCORE_ENVIRONMENT: Production
|
||||||
ASPNETCORE_URLS: http://+:8080
|
ASPNETCORE_URLS: http://+:8080
|
||||||
CONSUL_URL: http://consul:8500
|
CONSUL_URL: http://consul:8500
|
||||||
|
InternalApiKey: ${IM_INTERNAL_API_KEY:?IM_INTERNAL_API_KEY is required}
|
||||||
networks:
|
networks:
|
||||||
- im-net
|
- im-net
|
||||||
|
|
||||||
@@ -173,6 +174,7 @@ services:
|
|||||||
ASPNETCORE_ENVIRONMENT: Production
|
ASPNETCORE_ENVIRONMENT: Production
|
||||||
ASPNETCORE_URLS: http://+:8080
|
ASPNETCORE_URLS: http://+:8080
|
||||||
CONSUL_URL: http://consul:8500
|
CONSUL_URL: http://consul:8500
|
||||||
|
InternalApiKey: ${IM_INTERNAL_API_KEY:?IM_INTERNAL_API_KEY is required}
|
||||||
networks:
|
networks:
|
||||||
- im-net
|
- im-net
|
||||||
|
|
||||||
@@ -189,12 +191,15 @@ services:
|
|||||||
condition: service_started
|
condition: service_started
|
||||||
rabbitmq:
|
rabbitmq:
|
||||||
condition: service_started
|
condition: service_started
|
||||||
|
group-service:
|
||||||
|
condition: service_started
|
||||||
ports:
|
ports:
|
||||||
- "5005:8080"
|
- "5005:8080"
|
||||||
environment:
|
environment:
|
||||||
ASPNETCORE_ENVIRONMENT: Production
|
ASPNETCORE_ENVIRONMENT: Production
|
||||||
ASPNETCORE_URLS: http://+:8080
|
ASPNETCORE_URLS: http://+:8080
|
||||||
CONSUL_URL: http://consul:8500
|
CONSUL_URL: http://consul:8500
|
||||||
|
InternalApiKey: ${IM_INTERNAL_API_KEY:?IM_INTERNAL_API_KEY is required}
|
||||||
networks:
|
networks:
|
||||||
- im-net
|
- im-net
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user