fix: align backend APIs and upload flow

This commit is contained in:
2026-09-15 14:10:55 +08:00
parent 32177a7293
commit 53e6195938
149 changed files with 4791 additions and 435 deletions
@@ -8,7 +8,7 @@ namespace MessageService.WebApi.Application.Conversation
public ConversationMapperConfig()
{
CreateMap<Domain.Entities.Conversation, ConversationResponse>()
.ForMember(dest => dest.DateTime, opt => opt.MapFrom(src => src.ModificationTime))
.ForMember(dest => dest.DateTime, opt => opt.MapFrom(src => src.LastMessageTime ?? src.CreationTime))
;
}
}
@@ -1,5 +1,6 @@
using AutoMapper;
using IM.Commons;
using System.Diagnostics;
using MessageService.Domain.IReposities;
using MessageService.WebApi.Application.Dtos;
@@ -9,22 +10,53 @@ namespace MessageService.WebApi.Application.Conversation
{
private readonly IConversationReposity reposity;
private readonly IMapper mapper;
private readonly ILogger<ConversationService> logger;
public ConversationService(IConversationReposity reposity, IMapper mapper)
public ConversationService(IConversationReposity reposity, IMapper mapper, ILogger<ConversationService> logger)
{
this.reposity = reposity;
this.mapper = mapper;
this.logger = logger;
}
public async Task<Result<List<ConversationResponse>>> GetByOwnerIdAsync(Guid userId)
public async Task<Result<List<ConversationResponse>>> GetByOwnerIdAsync(Guid userId, CancellationToken cancellationToken = default)
{
var list = await reposity.FindByUserIdAsync(userId);
return Result.Success(mapper.Map<List<ConversationResponse>>(list.ToList()));
var stopwatch = Stopwatch.StartNew();
try
{
var list = await reposity.ListByUserIdAsync(userId, cancellationToken);
return Result.Success(list.Select(item => new ConversationResponse
{
Id = item.Id,
UserId = item.UserId,
TargetId = item.TargetId,
TargetAvatar = item.TargetAvatar,
TargetName = item.TargetName,
LastReadSequenceId = item.LastReadSequenceId,
UnreadCount = item.UnreadCount,
ChatType = item.ChatType,
LastMessage = item.LastMessage,
DateTime = item.DateTime
}).ToList());
}
finally
{
stopwatch.Stop();
var traceId = Activity.Current?.TraceId.ToString() ?? string.Empty;
if (stopwatch.ElapsedMilliseconds > 1000)
{
logger.LogWarning("Conversation list query was slow. TraceId={TraceId} UserId={UserId} ElapsedMs={ElapsedMs}", traceId, userId, stopwatch.ElapsedMilliseconds);
}
else
{
logger.LogInformation("Conversation list query completed. TraceId={TraceId} UserId={UserId} ElapsedMs={ElapsedMs}", traceId, userId, stopwatch.ElapsedMilliseconds);
}
}
}
public async Task<Result<ConversationResponse>> GetByIdAsync(Guid id, Guid userId)
public async Task<Result<ConversationResponse>> GetByIdAsync(Guid id, Guid userId, CancellationToken cancellationToken = default)
{
var conversation = await reposity.FindByIdAsync(id);
var conversation = await reposity.FindByIdAsync(id, cancellationToken);
if (conversation is null || conversation.UserId != userId)
{
@@ -34,21 +66,21 @@ namespace MessageService.WebApi.Application.Conversation
return Result.Success(mapper.Map<ConversationResponse>(conversation));
}
public async Task<Result<List<string>>> GetStreamkeysAsync(Guid userId)
public async Task<Result<List<string>>> GetStreamkeysAsync(Guid userId, CancellationToken cancellationToken = default)
{
var list = await reposity.FindAllStreamKeyAsync(userId);
var list = await reposity.FindAllStreamKeyAsync(userId, cancellationToken);
return Result.Success(list.ToList());
}
public async Task<Result<object>> MarkAsReadAsync(Guid conversationId, Guid userId)
public async Task<Result<object>> MarkAsReadAsync(Guid conversationId, Guid userId, long? lastReadSequenceId = null, CancellationToken cancellationToken = default)
{
var conversation = await reposity.FindByIdAsync(conversationId);
var conversation = await reposity.FindByIdAsync(conversationId, cancellationToken);
if (conversation is null || conversation.UserId != userId)
{
return Result.Fail<object>(ResultCode.CONVERSATION_NOT_FOUND);
}
conversation.MarkAsRead(lastReadSequenceId: null);
conversation.MarkAsRead(lastReadSequenceId);
return Result.Success();
}
}
@@ -31,6 +31,6 @@ namespace MessageService.WebApi.Application.Dtos
/// 最后一条最新消息
/// </summary>
public string LastMessage { get; set; }
public DateTime DateTime { get; set; }
public DateTimeOffset DateTime { get; set; }
}
}
@@ -2,11 +2,13 @@
using MassTransit;
using MessageService.Domain.IReposities;
using MessageService.Infrastructure;
using Microsoft.EntityFrameworkCore;
using MySqlConnector;
namespace MessageService.WebApi.Application.EventHandlers
{
public class ConversationAddHandler : IConsumer<GroupMemberJoinedEvent>,
IConsumer<FriendAddedEvent>
IConsumer<FriendAddedEvent>, IConsumer<GroupMemberLeftEvent>
{
private readonly IConversationReposity reposity;
@@ -21,6 +23,11 @@ namespace MessageService.WebApi.Application.EventHandlers
public async Task Consume(ConsumeContext<GroupMemberJoinedEvent> context)
{
var @event = context.Message;
var existing = await reposity.FindActiveAsync(@event.UserId, @event.GroupId, Domain.Enums.ChatType.GROUP, context.CancellationToken);
if (existing is not null)
{
return;
}
reposity.Create(new Domain.Entities.Conversation(
userId: @event.UserId,
targetId: @event.GroupId,
@@ -32,12 +39,17 @@ namespace MessageService.WebApi.Application.EventHandlers
lastMessage: string.Empty
));
await messageDb.SaveChangesAsync();
await SaveIdempotentlyAsync(context.CancellationToken);
}
public async Task Consume(ConsumeContext<FriendAddedEvent> context)
{
var @event = context.Message;
var existing = await reposity.FindActiveAsync(@event.OwnerId, @event.TargetId, Domain.Enums.ChatType.PRIVATE, context.CancellationToken);
if (existing is not null)
{
return;
}
reposity.Create(new Domain.Entities.Conversation(
userId: @event.OwnerId,
targetId: @event.TargetId,
@@ -49,7 +61,33 @@ namespace MessageService.WebApi.Application.EventHandlers
lastMessage: string.Empty
));
await messageDb.SaveChangesAsync();
await SaveIdempotentlyAsync(context.CancellationToken);
}
public async Task Consume(ConsumeContext<GroupMemberLeftEvent> context)
{
var conversation = await reposity.FindActiveAsync(
context.Message.UserId,
context.Message.GroupId,
Domain.Enums.ChatType.GROUP,
context.CancellationToken);
if (conversation is not null)
{
conversation.SoftDelete();
}
await messageDb.SaveChangesAsync(context.CancellationToken);
}
private async Task SaveIdempotentlyAsync(CancellationToken cancellationToken)
{
try
{
await messageDb.SaveChangesAsync(cancellationToken);
}
catch (DbUpdateException exception) when (exception.InnerException is MySqlException { Number: 1062 })
{
messageDb.ChangeTracker.Clear();
}
}
}
}
@@ -24,20 +24,24 @@ namespace MessageService.WebApi.Application.EventHandlers
{
var message = notification.Message;
if(message.ChatType == Domain.Enums.ChatType.PRIVATE)
var conversations = await reposity.FindByStreamKeyAsync(message.StreamKey, cancellationToken);
foreach (var conversation in conversations)
{
var list = await reposity.FindByStreamKeyAsync(message.StreamKey);
var owner = list.First(x => x.UserId == message.SenderId);
var target = list.First(x => x.UserId == message.TargetId);
owner.Update(message.SequenceId, 0, message.Content.Fallback);
target.Update(target.LastReadSequenceId, target.UnreadCount + 1, message.Content.Fallback);
messageDb.Conversations.UpdateRange(owner,target);
await messageDb.SaveChangesAsync(cancellationToken);
conversation.UpdateLastMessage(message.Content.Fallback, message.CreationTime);
if (conversation.UserId == message.SenderId)
{
conversation.SetLastReadSequence(message.SequenceId);
}
else
{
conversation.IncrementUnread();
}
}
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)
@@ -19,12 +19,12 @@ namespace MessageService.WebApi.Application.EventHandlers
public async Task Consume(ConsumeContext<UserProfileUpdateEvent> context)
{
var @event = context.Message;
var conversations = await reposity.FindByTargetIdAsync(@event.UserId);
var conversations = await reposity.FindByTargetIdAsync(@event.UserId, context.CancellationToken);
foreach (var conversation in conversations)
{
conversation.UpdateProfile(@event.NickName, @event.Avatar);
}
await db.SaveChangesAsync();
await db.SaveChangesAsync(context.CancellationToken);
}
}
}
@@ -1,4 +1,4 @@
using AutoMapper;
using AutoMapper;
using IM.Commons;
using MessageService.Domain.Enums;
using MessageService.Domain.IReposities;
@@ -16,20 +16,22 @@ namespace MessageService.WebApi.Application.Message
private readonly IMapper mapper;
private readonly IGroupMemberIntegrationService memberService;
private readonly IContactIntegrationService contactService;
private readonly SquenceService squenceService;
private readonly SquenceService squenceService; private readonly IM.InitCommon.Management.RuntimePolicy runtime;
public MessageService(IMessageReposity reposity, IConversationReposity conversationReposity, IMapper mapper, IGroupMemberIntegrationService memberService, IContactIntegrationService contactService, SquenceService squenceService)
public MessageService(IMessageReposity reposity, IConversationReposity conversationReposity, IMapper mapper, IGroupMemberIntegrationService memberService, IContactIntegrationService contactService, SquenceService squenceService, IM.InitCommon.Management.RuntimePolicy runtime)
{
this.reposity = reposity;
this.conversationReposity = conversationReposity;
this.mapper = mapper;
this.memberService = memberService;
this.contactService = contactService;
this.squenceService = squenceService;
this.squenceService = squenceService; this.runtime = runtime;
}
public async Task<Result<MessageResponse>> SendMsgAsync(SendMsgCommand command)
{
if (runtime.Current.TextLimit > 0 && command.MsgType == MessageType.Text && command.Text?.Length > runtime.Current.TextLimit)
return Result.Fail<MessageResponse>(ResultCode.PARAMETER_ERROR, "文本超过平台长度限制");
if (command.ChatType == Domain.Enums.ChatType.PRIVATE)
{
bool passed = await contactService.CheckContactAsync(command.SenderId, command.TargetId);
@@ -52,13 +54,16 @@ namespace MessageService.WebApi.Application.Message
{
MessageType.Text => Domain.Entities.Message.BuildTxt(ctx, command.Text!, sequenceId),
MessageType.Image => Domain.Entities.Message.BuildImg(ctx, command.Url!,
command.Width ?? 0, command.Height ?? 0, command.Thumb!, sequenceId),
MessageType.Image => Domain.Entities.Message.BuildImg(ctx, command.Url,
command.Width ?? 0, command.Height ?? 0, command.Thumb!, sequenceId, command.FileId),
MessageType.Video => Domain.Entities.Message.BuildVideo(ctx, command.Url!,
command.Width ?? 0, command.Height ?? 0, command.Thumb!, sequenceId),
MessageType.Video => Domain.Entities.Message.BuildVideo(ctx, command.Url,
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
};
@@ -102,14 +107,21 @@ namespace MessageService.WebApi.Application.Message
return Result.Fail<object>(ResultCode.MESSAGE_NOT_FOUND);
}
if (runtime.Current.RecallMinutes > 0 && DateTimeOffset.UtcNow - msg.CreationTime > TimeSpan.FromMinutes(runtime.Current.RecallMinutes))
return Result.Fail<object>(ResultCode.PERMISSION_DENIED, "已超过平台撤回时限");
msg.Withdraw();
return Result.Success();
}
public async Task<Result<GetMessagesResponse>> GetMessagesAsync(GetMessageCommand command)
public async Task<Result<GetMessagesResponse>> GetMessagesAsync(GetMessageCommand command, CancellationToken cancellationToken = default)
{
var conversation = await conversationReposity.FindByIdAsync(command.conversationId);
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, cancellationToken);
if(conversation is null || conversation.UserId != command.userId)
{
@@ -117,9 +129,37 @@ namespace MessageService.WebApi.Application.Message
}
var messages = await reposity.GetAsync(conversation.StreamKey, command.cusor, command.direction, command.limit);
var messages = await reposity.GetAsync(conversation.StreamKey, command.cusor, command.direction, command.limit, cancellationToken);
return Result.Success(new GetMessagesResponse(mapper.Map<List<MessageResponse>>(messages.messages.ToList()),messages.hasMore));
}
public async Task<Result<GetMessagesResponse>> SearchMessagesAsync(
SearchMessageCommand command,
CancellationToken cancellationToken = default)
{
var keyword = command.Keyword?.Trim() ?? string.Empty;
if (keyword.Length is < 1 or > 50 || command.Limit is < 1 or > 50)
{
return Result.Fail<GetMessagesResponse>(ResultCode.PARAMETER_ERROR);
}
var conversation = await conversationReposity.FindByIdAsync(command.ConversationId, cancellationToken);
if (conversation is null || conversation.UserId != command.UserId)
{
return Result.Fail<GetMessagesResponse>(ResultCode.PERMISSION_DENIED);
}
var messages = await reposity.SearchAsync(
conversation.StreamKey,
keyword,
command.Cursor,
command.Limit,
cancellationToken);
return Result.Success(new GetMessagesResponse(
mapper.Map<List<MessageResponse>>(messages.messages.ToList()),
messages.hasMore));
}
}
}
@@ -0,0 +1,9 @@
namespace MessageService.WebApi.Application.Message
{
public record SearchMessageCommand(
Guid ConversationId,
Guid UserId,
string Keyword,
long? Cursor,
int Limit);
}
@@ -22,8 +22,12 @@ namespace MessageService.WebApi.Application.Message
public int? Height { get; init; }
public string? Thumb { 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;
TargetId = targetId;
@@ -38,6 +42,10 @@ namespace MessageService.WebApi.Application.Message
Height = height;
Thumb = thumb;
Duration = duration;
FileId = fileId;
FileName = fileName;
FileSize = fileSize;
FileFormat = fileFormat;
}
}
}