添加项目文件。
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
using AutoMapper;
|
||||
using MessageService.WebApi.Application.Dtos;
|
||||
|
||||
namespace MessageService.WebApi.Application.Conversation
|
||||
{
|
||||
public class ConversationMapperConfig : Profile
|
||||
{
|
||||
public ConversationMapperConfig()
|
||||
{
|
||||
CreateMap<Domain.Entities.Conversation, ConversationResponse>()
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using AutoMapper;
|
||||
using IM.Commons;
|
||||
using MessageService.Domain.IReposities;
|
||||
using MessageService.WebApi.Application.Dtos;
|
||||
|
||||
namespace MessageService.WebApi.Application.Conversation
|
||||
{
|
||||
public class ConversationService
|
||||
{
|
||||
private readonly IConversationReposity reposity;
|
||||
private readonly IMapper mapper;
|
||||
|
||||
public ConversationService(IConversationReposity reposity, IMapper mapper)
|
||||
{
|
||||
this.reposity = reposity;
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
public async Task<Result<List<ConversationResponse>>> GetByOwnerIdAsync(Guid userId)
|
||||
{
|
||||
var list = await reposity.FindByUserIdAsync(userId);
|
||||
return Result.Success(mapper.Map<List<ConversationResponse>>(list.ToList()));
|
||||
}
|
||||
|
||||
public async Task<Result<ConversationResponse>> GetByIdAsync(Guid id, Guid userId)
|
||||
{
|
||||
var conversation = await reposity.FindByIdAsync(id);
|
||||
|
||||
if (conversation is null || conversation.UserId != userId)
|
||||
{
|
||||
return Result.Fail<ConversationResponse>(ResultCode.CONVERSATION_NOT_FOUND);
|
||||
}
|
||||
|
||||
return Result.Success(mapper.Map<ConversationResponse>(conversation));
|
||||
}
|
||||
|
||||
public async Task<Result<List<string>>> GetStreamkeysAsync(Guid userId)
|
||||
{
|
||||
var list = await reposity.FindAllStreamKeyAsync(userId);
|
||||
return Result.Success(list.ToList());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using MessageService.Domain.Enums;
|
||||
|
||||
namespace MessageService.WebApi.Application.Dtos
|
||||
{
|
||||
public class ConversationResponse
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 对方ID(群聊为群聊ID,单聊为单聊ID)
|
||||
/// </summary>
|
||||
public Guid TargetId { get; set; }
|
||||
public string TargetAvatar { get; set; }
|
||||
public string TargetName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最后一条未读消息ID
|
||||
/// </summary>
|
||||
public long? LastReadSequenceId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 未读消息数
|
||||
/// </summary>
|
||||
public int UnreadCount { get; set; }
|
||||
|
||||
public ChatType ChatType { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 最后一条最新消息
|
||||
/// </summary>
|
||||
public string LastMessage { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace MessageService.WebApi.Application.Dtos
|
||||
{
|
||||
public class GetMessagesResponse
|
||||
{
|
||||
public List<MessageResponse> Messages { get; init; }
|
||||
public bool Hasmore { get; init; }
|
||||
|
||||
public GetMessagesResponse(List<MessageResponse> messages, bool hasmore)
|
||||
{
|
||||
Messages = messages;
|
||||
Hasmore = hasmore;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using MessageService.Domain.Enums;
|
||||
|
||||
namespace MessageService.WebApi.Application.Dtos
|
||||
{
|
||||
public record MessageResponse
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public Guid ClientMsgId { get; init; }
|
||||
public ChatType ChatType { get; init; }
|
||||
public MessageType MsgType { get; init; }
|
||||
public Guid SenderId { get; init; }
|
||||
public Guid TargetId { get; init; }
|
||||
public MessageState State { get; init; }
|
||||
public string StreamKey { get; init; }
|
||||
public long SequenceId { get; init; }
|
||||
public DateTimeOffset CreationTime { get; init; }
|
||||
|
||||
// 关键:展开 Content
|
||||
public MessageContentResponse Content { get; init; }
|
||||
}
|
||||
|
||||
public record MessageContentResponse(
|
||||
string Fallback,
|
||||
object Body, // 已经是反序列化后的具体对象
|
||||
Dictionary<string, string> Ext,
|
||||
QuoteInfoResponse? Quote
|
||||
);
|
||||
|
||||
public record QuoteInfoResponse(
|
||||
Guid MessageId,
|
||||
Guid SenderId,
|
||||
string SenderName,
|
||||
MessageType MessageType,
|
||||
string Preview
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using IM.Commons.IntegrationEvents;
|
||||
using MassTransit;
|
||||
using MessageService.Domain.IReposities;
|
||||
using MessageService.Infrastructure;
|
||||
|
||||
namespace MessageService.WebApi.Application.EventHandlers
|
||||
{
|
||||
public class ConversationAddHandler : IConsumer<GroupMemberJoinedEvent>,
|
||||
IConsumer<FriendAddedEvent>
|
||||
{
|
||||
|
||||
private readonly IConversationReposity reposity;
|
||||
private readonly MessageDbContext messageDb;
|
||||
|
||||
public ConversationAddHandler(IConversationReposity reposity, MessageDbContext messageDb)
|
||||
{
|
||||
this.reposity = reposity;
|
||||
this.messageDb = messageDb;
|
||||
}
|
||||
|
||||
public async Task Consume(ConsumeContext<GroupMemberJoinedEvent> context)
|
||||
{
|
||||
var @event = context.Message;
|
||||
reposity.Create(new Domain.Entities.Conversation(
|
||||
userId: @event.UserId,
|
||||
targetId: @event.GroupId,
|
||||
targetAvatar: @event.Avatar,
|
||||
targetName: @event.GroupNickName,
|
||||
lastReadSequenceId: null,
|
||||
unreadCount:0,
|
||||
chatType: Domain.Enums.ChatType.GROUP,
|
||||
lastMessage: string.Empty
|
||||
));
|
||||
|
||||
await messageDb.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task Consume(ConsumeContext<FriendAddedEvent> context)
|
||||
{
|
||||
var @event = context.Message;
|
||||
reposity.Create(new Domain.Entities.Conversation(
|
||||
userId: @event.OwnerId,
|
||||
targetId: @event.TargetId,
|
||||
targetAvatar: @event.TargetAvatar,
|
||||
targetName: @event.TargetNickName,
|
||||
lastReadSequenceId: null,
|
||||
unreadCount: 0,
|
||||
chatType: Domain.Enums.ChatType.PRIVATE,
|
||||
lastMessage: string.Empty
|
||||
));
|
||||
|
||||
await messageDb.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using IM.Commons.IntegrationEvents;
|
||||
using MassTransit;
|
||||
using MediatR;
|
||||
using MessageService.Domain.Events;
|
||||
using MessageService.Domain.IReposities;
|
||||
using MessageService.Infrastructure;
|
||||
|
||||
namespace MessageService.WebApi.Application.EventHandlers
|
||||
{
|
||||
public class MessageHandler : INotificationHandler<MessageCreatedDomainEvent>, INotificationHandler<MessageWithdrawDomainEvent>
|
||||
{
|
||||
private readonly IPublishEndpoint endpoint;
|
||||
private readonly IConversationReposity reposity;
|
||||
private readonly MessageDbContext messageDb;
|
||||
|
||||
public MessageHandler(IPublishEndpoint endpoint, IConversationReposity reposity, MessageDbContext messageDb)
|
||||
{
|
||||
this.endpoint = endpoint;
|
||||
this.reposity = reposity;
|
||||
this.messageDb = messageDb;
|
||||
}
|
||||
|
||||
public async Task Handle(MessageCreatedDomainEvent notification, CancellationToken cancellationToken)
|
||||
{
|
||||
var message = notification.Message;
|
||||
|
||||
if(message.ChatType == Domain.Enums.ChatType.PRIVATE)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
await endpoint.Publish(message.ToIntegrationEvent());
|
||||
}
|
||||
|
||||
public async Task Handle(MessageWithdrawDomainEvent notification, CancellationToken cancellationToken)
|
||||
{
|
||||
var message = notification.Message;
|
||||
await endpoint.Publish(new MsgWithdrawEvent(message.Id, message.State.ToString(), message.StreamKey), cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using IM.Commons.IntegrationEvents;
|
||||
using MassTransit;
|
||||
using MessageService.Domain.IReposities;
|
||||
using MessageService.Infrastructure;
|
||||
|
||||
namespace MessageService.WebApi.Application.EventHandlers
|
||||
{
|
||||
public class UserProfileUpdateHandler : IConsumer<UserProfileUpdateEvent>
|
||||
{
|
||||
private readonly MessageDbContext db;
|
||||
private readonly IConversationReposity reposity;
|
||||
|
||||
public UserProfileUpdateHandler(MessageDbContext db, IConversationReposity reposity)
|
||||
{
|
||||
this.db = db;
|
||||
this.reposity = reposity;
|
||||
}
|
||||
|
||||
public async Task Consume(ConsumeContext<UserProfileUpdateEvent> context)
|
||||
{
|
||||
var @event = context.Message;
|
||||
var conversations = await reposity.FindByTargetIdAsync(@event.UserId);
|
||||
foreach (var conversation in conversations)
|
||||
{
|
||||
conversation.UpdateProfile(@event.NickName, @event.Avatar);
|
||||
}
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
|
||||
using IM.Commons;
|
||||
using IM.Protocols.Grpc.Contact;
|
||||
|
||||
namespace MessageService.WebApi.Application.IntegrationServices
|
||||
{
|
||||
public class ContactIntegrationService : IContactIntegrationService
|
||||
{
|
||||
private readonly ContactInternal.ContactInternalClient client;
|
||||
|
||||
public ContactIntegrationService(ContactInternal.ContactInternalClient client)
|
||||
{
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
public async Task<bool> CheckContactAsync(Guid ownerId, Guid targetId)
|
||||
{
|
||||
var req = new CheckFriendshipRequest()
|
||||
{
|
||||
OwnerId = ownerId.ToString(),
|
||||
TargetId = targetId.ToString(),
|
||||
};
|
||||
var res = await client.CheckFriendshipAsync(req);
|
||||
return res.Checked;
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
|
||||
using IM.Commons;
|
||||
|
||||
namespace MessageService.WebApi.Application.IntegrationServices
|
||||
{
|
||||
public class GroupMemberIntegrationService : IGroupMemberIntegrationService
|
||||
{
|
||||
private readonly HttpClient http;
|
||||
public async Task<bool> CheckGroupMemberAsync(Guid userId, Guid groupId)
|
||||
{
|
||||
var result = await http.GetFromJsonAsync<Result<bool>>(
|
||||
$"api/groupmember/checkmember?userId={userId}&groupId={groupId}");
|
||||
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return result.Data;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace MessageService.WebApi.Application.IntegrationServices
|
||||
{
|
||||
public interface IContactIntegrationService
|
||||
{
|
||||
Task<bool> CheckContactAsync(Guid ownerId, Guid targetId);
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
namespace MessageService.WebApi.Application.IntegrationServices
|
||||
{
|
||||
public interface IGroupMemberIntegrationService
|
||||
{
|
||||
Task<bool> CheckGroupMemberAsync(Guid userId, Guid groupId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace MessageService.WebApi.Application.Message
|
||||
{
|
||||
public record GetMessageCommand(Guid conversationId, Guid userId, long? cusor, int direction, int limit);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using AutoMapper;
|
||||
using MessageService.Domain.KeyObjects;
|
||||
using MessageService.WebApi.Application.Dtos;
|
||||
|
||||
namespace MessageService.WebApi.Application.Message
|
||||
{
|
||||
public class MessageMapperConfig : Profile
|
||||
{
|
||||
public MessageMapperConfig()
|
||||
{
|
||||
// 1. 配置 QuoteInfo -> QuoteInfoResponse
|
||||
CreateMap<QuoteInfo, QuoteInfoResponse>();
|
||||
|
||||
// 2. 配置 MessageContent -> MessageContentResponse
|
||||
CreateMap<MessageContent, MessageContentResponse>()
|
||||
// 关键点:Body 是 object,由于我们在实体里写了 Body 计算属性
|
||||
// AutoMapper 默认会识别到同名的 Body 属性并进行映射
|
||||
// 如果你想显式指定逻辑,可以取消下面这行的注释:
|
||||
// .ForMember(dest => dest.Body, opt => opt.MapFrom(src => src.Body))
|
||||
.ForMember(dest => dest.Ext, opt => opt.MapFrom(src => src.Ext))
|
||||
.ForMember(dest => dest.Quote, opt => opt.MapFrom(src =>
|
||||
src.Quote.MessageId == Guid.Empty ? null : src.Quote));
|
||||
|
||||
// 3. 配置 Message -> MessageResponse
|
||||
CreateMap<Domain.Entities.Message, MessageResponse>()
|
||||
// 映射审计字段中的创建时间
|
||||
.ForMember(dest => dest.CreationTime, opt => opt.MapFrom(src => src.CreationTime))
|
||||
// 嵌套映射 Content
|
||||
.ForMember(dest => dest.Content, opt => opt.MapFrom(src => src.Content));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
using AutoMapper;
|
||||
using IM.Commons;
|
||||
using MessageService.Domain.Enums;
|
||||
using MessageService.Domain.IReposities;
|
||||
using MessageService.Domain.KeyObjects;
|
||||
using MessageService.Domain.Tools;
|
||||
using MessageService.WebApi.Application.Dtos;
|
||||
using MessageService.WebApi.Application.IntegrationServices;
|
||||
|
||||
namespace MessageService.WebApi.Application.Message
|
||||
{
|
||||
public class MessageService
|
||||
{
|
||||
private readonly IMessageReposity reposity;
|
||||
private readonly IConversationReposity conversationReposity;
|
||||
private readonly IMapper mapper;
|
||||
private readonly IGroupMemberIntegrationService memberService;
|
||||
private readonly IContactIntegrationService contactService;
|
||||
private readonly SquenceService squenceService;
|
||||
|
||||
public MessageService(IMessageReposity reposity, IConversationReposity conversationReposity, IMapper mapper, IGroupMemberIntegrationService memberService, IContactIntegrationService contactService, SquenceService squenceService)
|
||||
{
|
||||
this.reposity = reposity;
|
||||
this.conversationReposity = conversationReposity;
|
||||
this.mapper = mapper;
|
||||
this.memberService = memberService;
|
||||
this.contactService = contactService;
|
||||
this.squenceService = squenceService;
|
||||
}
|
||||
|
||||
public async Task<Result<MessageResponse>> SendMsgAsync(SendMsgCommand command)
|
||||
{
|
||||
if (command.ChatType == Domain.Enums.ChatType.PRIVATE)
|
||||
{
|
||||
bool passed = await contactService.CheckContactAsync(command.SenderId, command.TargetId);
|
||||
if (!passed)
|
||||
return Result.Fail<MessageResponse>(ResultCode.FRIEND_RELATION_NOT_FOUND);
|
||||
}
|
||||
else
|
||||
{
|
||||
bool passed = await memberService.CheckGroupMemberAsync(command.SenderId, command.TargetId);
|
||||
if (!passed)
|
||||
return Result.Fail<MessageResponse>(ResultCode.NO_GROUP_PERMISSION);
|
||||
}
|
||||
|
||||
var ctx = new MessageCreateContext(command.ChatType, command.ClientMsgId, command.SenderId, command.TargetId);
|
||||
|
||||
var streamKey = command.ChatType == ChatType.GROUP ? StreamKeyBuilder.Group(ctx.TargetId) : StreamKeyBuilder.Private(ctx.SenderId, ctx.TargetId);
|
||||
long sequenceId = await squenceService.GetNextSquenceIdAsync(streamKey);
|
||||
|
||||
Domain.Entities.Message message = command.MsgType switch
|
||||
{
|
||||
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.Video => Domain.Entities.Message.BuildVideo(ctx, command.Url!,
|
||||
command.Width ?? 0, command.Height ?? 0, command.Thumb!, sequenceId),
|
||||
|
||||
MessageType.Voice => Domain.Entities.Message.BuildVoice(ctx, command.Url!, command.Duration ?? 0, sequenceId),
|
||||
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (message == null)
|
||||
{
|
||||
return Result.Fail<MessageResponse>(ResultCode.UNSUPPORTED_MESSAGE_TYPE);
|
||||
}
|
||||
|
||||
|
||||
if (command.QuoteMessageId.HasValue)
|
||||
{
|
||||
// 2. 处理引用逻辑(如果传了 QuoteMessageId)
|
||||
QuoteInfo? quote = null;
|
||||
var originMsg = await reposity.FindByIdAsync(command.QuoteMessageId.Value);
|
||||
if (originMsg != null)
|
||||
{
|
||||
quote = new QuoteInfo
|
||||
{
|
||||
MessageId = originMsg.Id,
|
||||
SenderId = originMsg.SenderId,
|
||||
SenderName = "未知昵称", // 这里建议从缓存或DB获取发送者昵称
|
||||
MessageType = originMsg.MsgType,
|
||||
Preview = originMsg.Content.Fallback
|
||||
};
|
||||
}
|
||||
message.WithQuote(quote);
|
||||
}
|
||||
|
||||
|
||||
|
||||
reposity.Create(message);
|
||||
|
||||
return Result.Success(mapper.Map<MessageResponse>(message));
|
||||
|
||||
}
|
||||
|
||||
public async Task<Result<object>> WithDrawMsgAsync(Guid msgId, Guid senderId)
|
||||
{
|
||||
var msg = await reposity.FindByIdAsync(msgId);
|
||||
if (msg == null || msg.SenderId != senderId)
|
||||
{
|
||||
return Result.Fail<object>(ResultCode.MESSAGE_NOT_FOUND);
|
||||
}
|
||||
|
||||
msg.Withdraw();
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
|
||||
public async Task<Result<GetMessagesResponse>> GetMessagesAsync(GetMessageCommand command)
|
||||
{
|
||||
var conversation = await conversationReposity.FindByIdAsync(command.conversationId);
|
||||
|
||||
if(conversation is null || conversation.UserId != command.userId)
|
||||
{
|
||||
return Result.Fail<GetMessagesResponse>(ResultCode.PERMISSION_DENIED);
|
||||
}
|
||||
|
||||
|
||||
var messages = await reposity.GetAsync(conversation.StreamKey, command.cusor, command.direction, command.limit);
|
||||
|
||||
return Result.Success(new GetMessagesResponse(mapper.Map<List<MessageResponse>>(messages.messages.ToList()),messages.hasMore));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using MessageService.Domain.Enums;
|
||||
|
||||
namespace MessageService.WebApi.Application.Message
|
||||
{
|
||||
public record SendMsgCommand
|
||||
{
|
||||
// 核心区别:Command 必须包含 SenderId,这是从后端 Token 解析出来的
|
||||
public Guid SenderId { get; init; }
|
||||
|
||||
public Guid TargetId { get; init; }
|
||||
public ChatType ChatType { get; init; }
|
||||
public MessageType MsgType { get; init; }
|
||||
public Guid ClientMsgId { get; init; }
|
||||
|
||||
public Guid? QuoteMessageId { get; init; }
|
||||
public Dictionary<string, string>? Ext { get; init; }
|
||||
|
||||
// 拍扁后的参数,方便 Service 直接调用工厂方法
|
||||
public string? Text { get; init; }
|
||||
public string? Url { get; init; }
|
||||
public int? Width { get; init; }
|
||||
public int? Height { get; init; }
|
||||
public string? Thumb { get; init; }
|
||||
public int? Duration { 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)
|
||||
{
|
||||
SenderId = senderId;
|
||||
TargetId = targetId;
|
||||
ChatType = chatType;
|
||||
MsgType = msgType;
|
||||
ClientMsgId = clientMsgId;
|
||||
QuoteMessageId = quoteMessageId;
|
||||
Ext = ext;
|
||||
Text = text;
|
||||
Url = url;
|
||||
Width = width;
|
||||
Height = height;
|
||||
Thumb = thumb;
|
||||
Duration = duration;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using IM.Commons;
|
||||
using MessageService.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RedLockNet;
|
||||
using StackExchange.Redis;
|
||||
|
||||
namespace MessageService.WebApi.Application
|
||||
{
|
||||
public class SquenceService
|
||||
{
|
||||
private readonly IDatabase _database;
|
||||
private readonly IDistributedLockFactory _lockFactory;
|
||||
private readonly MessageDbContext messageDb;
|
||||
public SquenceService(IConnectionMultiplexer multiplexer,
|
||||
IDistributedLockFactory distributedLockFactory,
|
||||
MessageDbContext messageDb
|
||||
)
|
||||
{
|
||||
_database = multiplexer.GetDatabase();
|
||||
_lockFactory = distributedLockFactory;
|
||||
this.messageDb = messageDb;
|
||||
}
|
||||
public async Task<long> GetNextSquenceIdAsync(string streamKey)
|
||||
{
|
||||
string key = RedisHelper.GetSequenceIdKey(streamKey);
|
||||
string lockKey = RedisHelper.GetSequenceIdLockKey(streamKey);
|
||||
var exists = await _database.KeyExistsAsync(key);
|
||||
if (!exists)
|
||||
{
|
||||
using (var _lock = await _lockFactory.CreateLockAsync(lockKey, TimeSpan.FromSeconds(5)))
|
||||
{
|
||||
if (_lock.IsAcquired)
|
||||
{
|
||||
if (!await _database.KeyExistsAsync(key))
|
||||
{
|
||||
var max = await messageDb.Messages
|
||||
.Where(x => x.StreamKey == streamKey)
|
||||
.MaxAsync(m => (long?)m.SequenceId) ?? 0;
|
||||
await _database.StringSetAsync(key, max, TimeSpan.FromDays(7));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return await _database.StringIncrementAsync(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using MessageService.WebApi.Application.Conversation;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace MessageService.WebApi.Controllers.Conversation
|
||||
{
|
||||
[Route("api/[controller]/[action]")]
|
||||
[Authorize]
|
||||
[ApiController]
|
||||
public class ConversationController : ControllerBase
|
||||
{
|
||||
private readonly ConversationService service;
|
||||
|
||||
public ConversationController(ConversationService service)
|
||||
{
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> List()
|
||||
{
|
||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
return Ok(await service.GetByOwnerIdAsync(Guid.Parse(userId)));
|
||||
}
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Get([FromQuery]Guid id)
|
||||
{
|
||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
return Ok(await service.GetByIdAsync(id, Guid.Parse(userId)));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using IM.ASPNETCore;
|
||||
using MessageService.Infrastructure;
|
||||
using MessageService.WebApi.Application.Message;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace MessageService.WebApi.Controllers.Message
|
||||
{
|
||||
[Route("api/[controller]/[action]")]
|
||||
[Authorize]
|
||||
[ApiController]
|
||||
public class MessageController : ControllerBase
|
||||
{
|
||||
private readonly Application.Message.MessageService service;
|
||||
|
||||
public MessageController(Application.Message.MessageService service)
|
||||
{
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[UnitOfWork(typeof(MessageDbContext))]
|
||||
public async Task<IActionResult> Send(MessageSendRequest request)
|
||||
{
|
||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
var command = request.ToCommand(Guid.Parse(userId));
|
||||
return Ok(await service.SendMsgAsync(command));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[UnitOfWork(typeof(MessageDbContext))]
|
||||
public async Task<IActionResult> WithDraw([FromQuery] Guid msgId)
|
||||
{
|
||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
return Ok(await service.WithDrawMsgAsync(msgId, Guid.Parse(userId)));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetMessages([FromQuery]Guid conversationId, long? cursor, int direction, int limit)
|
||||
{
|
||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
var command = new GetMessageCommand(conversationId, Guid.Parse(userId), cursor, direction, limit);
|
||||
return Ok(await service.GetMessagesAsync(command));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using FluentValidation;
|
||||
using MessageService.Domain.Enums;
|
||||
using MessageService.WebApi.Application.Message;
|
||||
|
||||
namespace MessageService.WebApi.Controllers.Message
|
||||
{
|
||||
public class MessageSendRequest
|
||||
{
|
||||
// 基础元数据
|
||||
public Guid ClientMsgId { get; init; }
|
||||
public Guid TargetId { get; init; }
|
||||
public ChatType ChatType { get; init; }
|
||||
public MessageType MsgType { get; init; }
|
||||
|
||||
// 业务属性
|
||||
public Guid? QuoteMessageId { get; init; }
|
||||
public Dictionary<string, string>? Ext { get; init; }
|
||||
|
||||
// 载荷数据(根据 MsgType 选择性填充)
|
||||
public string? Text { get; init; }
|
||||
public string? Url { get; init; }
|
||||
public int? Width { get; init; }
|
||||
public int? Height { get; init; }
|
||||
public string? Thumb { get; init; }
|
||||
public int? Duration { get; init; }
|
||||
|
||||
public SendMsgCommand ToCommand(Guid senderId)
|
||||
{
|
||||
return new SendMsgCommand(
|
||||
senderId,
|
||||
TargetId,
|
||||
ChatType,
|
||||
MsgType,
|
||||
ClientMsgId,
|
||||
QuoteMessageId,
|
||||
Ext,
|
||||
Text,
|
||||
Url,
|
||||
Width,
|
||||
Height,
|
||||
Thumb,
|
||||
Duration
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public class MessageSendRequestValidator : AbstractValidator<MessageSendRequest>
|
||||
{
|
||||
public MessageSendRequestValidator()
|
||||
{
|
||||
RuleFor(r => r.ClientMsgId)
|
||||
.NotNull()
|
||||
.NotEmpty();
|
||||
RuleFor(r => r.TargetId)
|
||||
.NotEmpty()
|
||||
.NotNull();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using IM.InitCommon;
|
||||
using MessageService.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
|
||||
namespace MessageService.WebApi
|
||||
{
|
||||
public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<MessageDbContext>
|
||||
{
|
||||
public MessageDbContext CreateDbContext(string[] args)
|
||||
{
|
||||
// 1. 复用你写好的配置工厂,提取连接字符串
|
||||
var optionsBuilder = DbContextOptionsBuilderFactory.Create<MessageDbContext>();
|
||||
|
||||
// 2. 🌟 关键补刀:把假的 Mediator 传进去,满足构造函数的要求!
|
||||
return new MessageDbContext(optionsBuilder.Options, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
WORKDIR /src
|
||||
|
||||
COPY IM_API_NEW.sln ./
|
||||
|
||||
COPY MessageService.WebApi/MessageService.WebApi.csproj MessageService.WebApi/
|
||||
COPY MessageService.Domain/MessageService.Domain.csproj MessageService.Domain/
|
||||
COPY MessageService.Infrastructure/MessageService.Infrastructure.csproj MessageService.Infrastructure/
|
||||
COPY DomainCommons/IM.DomainCommons.csproj DomainCommons/
|
||||
COPY Infrastructure/IM.Infrastructure.csproj Infrastructure/
|
||||
COPY IM.ASPNETCore/IM.ASPNETCore.csproj IM.ASPNETCore/
|
||||
COPY IM.Commons/IM.Commons.csproj IM.Commons/
|
||||
COPY IM.InitCommon/IM.InitCommon.csproj IM.InitCommon/
|
||||
COPY IM.Jwt/IM.Jwt.csproj IM.Jwt/
|
||||
COPY IM.Protocols/IM.Protocols.csproj IM.Protocols/
|
||||
|
||||
RUN dotnet restore MessageService.WebApi/MessageService.WebApi.csproj
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN dotnet publish MessageService.WebApi/MessageService.WebApi.csproj \
|
||||
-c Release \
|
||||
-o /app/publish \
|
||||
--no-restore
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
|
||||
WORKDIR /app
|
||||
|
||||
ENV ASPNETCORE_ENVIRONMENT=Production
|
||||
ENV ASPNETCORE_URLS=http://+:8080
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
COPY --from=build /app/publish .
|
||||
|
||||
ENTRYPOINT ["dotnet", "MessageService.WebApi.dll"]
|
||||
@@ -0,0 +1,29 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper" Version="16.1.1" />
|
||||
<PackageReference Include="MassTransit" Version="9.1.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="RedLock.net" Version="2.3.2" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\IM.ASPNETCore\IM.ASPNETCore.csproj" />
|
||||
<ProjectReference Include="..\IM.Commons\IM.Commons.csproj" />
|
||||
<ProjectReference Include="..\IM.InitCommon\IM.InitCommon.csproj" />
|
||||
<ProjectReference Include="..\IM.Protocols\IM.Protocols.csproj" />
|
||||
<ProjectReference Include="..\MessageService.Domain\MessageService.Domain.csproj" />
|
||||
<ProjectReference Include="..\MessageService.Infrastructure\MessageService.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,6 @@
|
||||
@MessageService.WebApi_HostAddress = http://localhost:5067
|
||||
|
||||
GET {{MessageService.WebApi_HostAddress}}/weatherforecast/
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
@@ -0,0 +1,27 @@
|
||||
using IM.Commons;
|
||||
using IM.Protocols.Grpc.Contact;
|
||||
using IM.Protocols.Grpc.User;
|
||||
using MessageService.WebApi.Application;
|
||||
using MessageService.WebApi.Application.Conversation;
|
||||
using MessageService.WebApi.Application.IntegrationServices;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace MessageService.WebApi
|
||||
{
|
||||
public class ModuleInit : IModuleInitializer
|
||||
{
|
||||
public void Initialize(IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<Application.Message.MessageService>();
|
||||
services.AddScoped<ConversationService>();
|
||||
services.AddScoped<SquenceService>();
|
||||
services.AddScoped<IContactIntegrationService, ContactIntegrationService>();
|
||||
services.AddScoped<IGroupMemberIntegrationService, GroupMemberIntegrationService>();
|
||||
services.AddGrpcClient<ContactInternal.ContactInternalClient>((sp, o) =>
|
||||
{
|
||||
var options = sp.GetRequiredService<IOptionsMonitor<GrpcOptions>>();
|
||||
o.Address = new Uri(options.CurrentValue.ContactServiceUrl);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
|
||||
using IM.InitCommon;
|
||||
|
||||
namespace MessageService.WebApi
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add services to the container.
|
||||
builder.ConfigureDbConfiguration();
|
||||
builder.Services.AddControllers();
|
||||
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
builder.ConfigExtraServices();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
app.UseAppDefault();
|
||||
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
app.Run();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"dotnetRunMessages": true,
|
||||
"applicationUrl": "http://localhost:5067"
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"dotnetRunMessages": true,
|
||||
"applicationUrl": "https://localhost:7143;http://localhost:5067"
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
},
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:25172",
|
||||
"sslPort": 44390
|
||||
}
|
||||
},
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json"
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Grpc.Core;
|
||||
using IM.Protocols.Grpc.Conversation;
|
||||
using MessageService.WebApi.Application.Conversation;
|
||||
|
||||
namespace MessageService.WebApi.Services
|
||||
{
|
||||
public class ConversationIntegrationService:ConversationInternal.ConversationInternalBase
|
||||
{
|
||||
private readonly ConversationService service;
|
||||
|
||||
public ConversationIntegrationService(ConversationService service)
|
||||
{
|
||||
this.service = service;
|
||||
}
|
||||
public override async Task<UserStreamKeysResponse> GetUserStreamKeys(GetUserStreamKeysRequest request, ServerCallContext context)
|
||||
{
|
||||
var res = await service.GetStreamkeysAsync(Guid.Parse(request.UserId));
|
||||
if (!res.Succeeded)
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.InvalidArgument, res.Message));
|
||||
}
|
||||
|
||||
var response = new UserStreamKeysResponse();
|
||||
response.StreamKeys.AddRange(res.Data);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
Reference in New Issue
Block a user