添加项目文件。
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
using IM.DomainCommons;
|
||||
using MessageService.Domain.Enums;
|
||||
using MessageService.Domain.Events;
|
||||
using MessageService.Domain.Tools;
|
||||
|
||||
namespace MessageService.Domain.Entities
|
||||
{
|
||||
public class Conversation : AggregateRootEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// 用户
|
||||
/// </summary>
|
||||
public Guid UserId { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 对方ID(群聊为群聊ID,单聊为单聊ID)
|
||||
/// </summary>
|
||||
public Guid TargetId { get; private set; }
|
||||
public string TargetAvatar { get; private set; }
|
||||
public string TargetName { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最后一条未读消息ID
|
||||
/// </summary>
|
||||
public long? LastReadSequenceId { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 未读消息数
|
||||
/// </summary>
|
||||
public int UnreadCount { get; private set; }
|
||||
|
||||
public ChatType ChatType { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 消息推送唯一标识符
|
||||
/// </summary>
|
||||
public string StreamKey { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最后一条最新消息
|
||||
/// </summary>
|
||||
public string LastMessage { get; private set; }
|
||||
private Conversation() { }
|
||||
|
||||
public Conversation(Guid userId, Guid targetId, string targetAvatar, string targetName, long? lastReadSequenceId, int unreadCount, ChatType chatType, string lastMessage)
|
||||
{
|
||||
UserId = userId;
|
||||
TargetId = targetId;
|
||||
TargetAvatar = targetAvatar;
|
||||
TargetName = targetName;
|
||||
LastReadSequenceId = lastReadSequenceId;
|
||||
UnreadCount = unreadCount;
|
||||
ChatType = chatType;
|
||||
LastMessage = lastMessage;
|
||||
StreamKey = ChatType == ChatType.GROUP ? StreamKeyBuilder.Group(targetId) : StreamKeyBuilder.Private(userId, targetId);
|
||||
AddDomainEvent(new ConversationCreatedDomainEvent(this));
|
||||
}
|
||||
|
||||
public void Update(long? LastReadSequenceId = default, int? unreadCount = default, string? lastMsg = default)
|
||||
{
|
||||
if (LastReadSequenceId != null)
|
||||
{
|
||||
LastReadSequenceId = LastReadSequenceId.Value;
|
||||
}
|
||||
if (unreadCount != null)
|
||||
{
|
||||
UnreadCount += unreadCount.Value;
|
||||
}
|
||||
|
||||
if (lastMsg != null)
|
||||
{
|
||||
LastMessage = lastMsg;
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateProfile(string name, string avatar)
|
||||
{
|
||||
TargetAvatar = avatar;
|
||||
TargetName = name;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
using IM.Commons.IntegrationEvents;
|
||||
using IM.DomainCommons;
|
||||
using MessageService.Domain.Enums;
|
||||
using MessageService.Domain.Events;
|
||||
using MessageService.Domain.KeyObjects;
|
||||
using MessageService.Domain.Tools;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace MessageService.Domain.Entities
|
||||
{
|
||||
public class Message : AggregateRootEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// 聊天类型
|
||||
/// (0:私聊,1:群聊)
|
||||
/// </summary>
|
||||
public ChatType ChatType { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 消息类型
|
||||
/// (0:文本,1:图片,2:语音,3:视频,4:文件,5:语音聊天,6:视频聊天)
|
||||
/// </summary>
|
||||
public MessageType MsgType { get; private set; }
|
||||
public Guid ClientMsgId { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 消息内容
|
||||
/// </summary>
|
||||
public MessageContent Content { get; private set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// 发送者
|
||||
/// </summary>
|
||||
public Guid SenderId { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 接收者(私聊为用户ID,群聊为群聊ID)
|
||||
/// </summary>
|
||||
public Guid TargetId { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 消息状态(0:已发送,1:已撤回)
|
||||
/// </summary>
|
||||
public MessageState State { get; private set; } = MessageState.Sent;
|
||||
|
||||
/// <summary>
|
||||
/// 消息推送唯一标识符
|
||||
/// </summary>
|
||||
public string StreamKey { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 消息排序标识
|
||||
/// </summary>
|
||||
|
||||
public long SequenceId { get; private set; }
|
||||
|
||||
private Message() { }
|
||||
|
||||
private Message(MessageCreateContext ctx, long sequenceId, MessageType messageType
|
||||
, MessageContent content
|
||||
)
|
||||
{
|
||||
ChatType = ctx.ChatType;
|
||||
SenderId = ctx.SenderId;
|
||||
ClientMsgId = ctx.ClientMsgId;
|
||||
TargetId = ctx.TargetId;
|
||||
SequenceId = sequenceId;
|
||||
MsgType = messageType;
|
||||
StreamKey = ChatType == ChatType.GROUP ? StreamKeyBuilder.Group(ctx.TargetId) : StreamKeyBuilder.Private(ctx.SenderId, ctx.TargetId);
|
||||
Content = content;
|
||||
}
|
||||
/// <summary>
|
||||
/// 文本消息
|
||||
/// </summary>
|
||||
/// <param name="ctx"></param>
|
||||
/// <param name="text">文本内容</param>
|
||||
/// <param name="sequenceId"></param>
|
||||
/// <returns></returns>
|
||||
public static Message BuildTxt(MessageCreateContext ctx, string text, long sequenceId)
|
||||
{
|
||||
var content = new MessageContent(text, new TextBody(text));
|
||||
return new Message(ctx, sequenceId, MessageType.Text, content);
|
||||
}
|
||||
/// <summary>
|
||||
/// 图片消息
|
||||
/// </summary>
|
||||
/// <param name="ctx"></param>
|
||||
/// <param name="url">相对路径</param>
|
||||
/// <param name="width">宽度</param>
|
||||
/// <param name="height">高度</param>
|
||||
/// <param name="thumb">预览图</param>
|
||||
/// <param name="sequenceId"></param>
|
||||
/// <returns></returns>
|
||||
public static Message BuildImg(MessageCreateContext ctx, string url,
|
||||
int width, int height, string thumb, long sequenceId)
|
||||
{
|
||||
var content = new MessageContent("[图片]", new ImageBody(url, width, height, thumb));
|
||||
return new Message(ctx, sequenceId, MessageType.Image, content);
|
||||
}
|
||||
/// <summary>
|
||||
/// 视频消息
|
||||
/// </summary>
|
||||
/// <param name="ctx"></param>
|
||||
/// <param name="url"></param>
|
||||
/// <param name="width"></param>
|
||||
/// <param name="height"></param>
|
||||
/// <param name="thumb"></param>
|
||||
/// <param name="sequenceId"></param>
|
||||
/// <returns></returns>
|
||||
public static Message BuildVideo(MessageCreateContext ctx, string url,
|
||||
int width, int height, string thumb, long sequenceId)
|
||||
{
|
||||
var content = new MessageContent("[视频]", new VideoBody(url, width, height, thumb));
|
||||
return new Message(ctx, sequenceId, MessageType.Video, content);
|
||||
}
|
||||
/// <summary>
|
||||
/// 语音消息
|
||||
/// </summary>
|
||||
/// <param name="ctx"></param>
|
||||
/// <param name="url"></param>
|
||||
/// <param name="duration">持续时间</param>
|
||||
/// <param name="sequenceId"></param>
|
||||
/// <returns></returns>
|
||||
public static Message BuildVoice(MessageCreateContext ctx, string url,
|
||||
int duration, long sequenceId)
|
||||
{
|
||||
var content = new MessageContent("[音频]", new VoiceBody(url, duration));
|
||||
return new Message(ctx, sequenceId, MessageType.Voice, content);
|
||||
}
|
||||
/// <summary>
|
||||
/// 文件
|
||||
/// </summary>
|
||||
/// <param name="ctx"></param>
|
||||
/// <param name="url">相对路径</param>
|
||||
/// <param name="name">文件名</param>
|
||||
/// <param name="size">文件大小(KB)</param>
|
||||
/// <param name="format">文件格式</param>
|
||||
/// <param name="sequenceId"></param>
|
||||
/// <returns></returns>
|
||||
public static Message BuildFile(MessageCreateContext ctx, string url,
|
||||
string name, long size,
|
||||
string format, long sequenceId)
|
||||
{
|
||||
var content = new MessageContent("[文件]", new FileBody(url, name, size, format));
|
||||
return new Message(ctx, sequenceId, MessageType.File, content);
|
||||
}
|
||||
/// <summary>
|
||||
/// 添加扩展字段
|
||||
/// </summary>
|
||||
/// <param name="key">字段名</param>
|
||||
/// <param name="value">值</param>
|
||||
/// <returns></returns>
|
||||
public Message WithExt(string key, string value)
|
||||
{
|
||||
var dic = new Dictionary<string, string>();
|
||||
dic.Add(key, value);
|
||||
Content.RawExt = JsonSerializer.Serialize(dic);
|
||||
return this;
|
||||
}
|
||||
/// <summary>
|
||||
/// 添加消息引用
|
||||
/// </summary>
|
||||
/// <param name="quote"></param>
|
||||
/// <returns></returns>
|
||||
public Message WithQuote(QuoteInfo quote)
|
||||
{
|
||||
this.Content.Quote = quote;
|
||||
return this;
|
||||
}
|
||||
/// <summary>
|
||||
/// 完成消息
|
||||
/// </summary>
|
||||
public void Send()
|
||||
{
|
||||
AddDomainEvent(new MessageCreatedDomainEvent(this));
|
||||
}
|
||||
|
||||
public void Withdraw()
|
||||
{
|
||||
if (State != MessageState.Sent)
|
||||
{
|
||||
throw new DomainException("消息不可撤回");
|
||||
}
|
||||
State = MessageState.Withdrwan;
|
||||
AddDomainEvent(new MessageWithdrawDomainEvent(this));
|
||||
}
|
||||
public MsgCreatedEvent ToIntegrationEvent()
|
||||
{
|
||||
return new MsgCreatedEvent
|
||||
{
|
||||
Id = this.Id,
|
||||
ClientId = this.ClientMsgId, // 假设你有存发送端的设备ID
|
||||
ChatType = this.ChatType.ToString(), // 比如 "private" 或 "group"
|
||||
MsgType = this.MsgType.ToString() ?? MessageType.Text.ToString(), // 动态获取类型名,或从 Content 里的定义拿
|
||||
SenderId = this.SenderId,
|
||||
TargetId = this.TargetId,
|
||||
State = State.ToString(),
|
||||
StreamKey = this.StreamKey,
|
||||
SequenceId = this.SequenceId,
|
||||
// 核心载荷:利用计算属性触发反序列化,打包成 DTO
|
||||
Content = new MsgContent(
|
||||
this.Content.Fallback,
|
||||
this.Content.Body, // 触发实体内部的反序列化逻辑
|
||||
this.Content.Ext,
|
||||
this.Content.Quote != null ? new QuoteInfoDto(
|
||||
this.Content.Quote.MessageId,
|
||||
this.Content.Quote.SenderId,
|
||||
this.Content.Quote.SenderName,
|
||||
this.Content.Quote.MessageType.ToString(),
|
||||
this.Content.Quote.Preview
|
||||
) : null
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace MessageService.Domain.Enums
|
||||
{
|
||||
public enum ChatType
|
||||
{
|
||||
PRIVATE = 0,
|
||||
GROUP = 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace MessageService.Domain.Enums
|
||||
{
|
||||
public enum MessageState
|
||||
{
|
||||
/// <summary>
|
||||
/// 已发送
|
||||
/// </summary>
|
||||
Sent = 0,
|
||||
/// <summary>
|
||||
/// 已撤回
|
||||
/// </summary>
|
||||
Withdrwan = 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace MessageService.Domain.Enums
|
||||
{
|
||||
public enum MessageType
|
||||
{
|
||||
Text = 0,
|
||||
Image = 1,
|
||||
Voice = 2,
|
||||
Video = 3,
|
||||
File = 4,
|
||||
VoiceChat = 5,
|
||||
VideoChat = 6
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using MediatR;
|
||||
using MessageService.Domain.Entities;
|
||||
|
||||
namespace MessageService.Domain.Events
|
||||
{
|
||||
public record ConversationCreatedDomainEvent(Conversation Conversation) : INotification;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using MediatR;
|
||||
using MessageService.Domain.Entities;
|
||||
|
||||
namespace MessageService.Domain.Events
|
||||
{
|
||||
public record MessageCreatedDomainEvent(Message Message) : INotification;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using MediatR;
|
||||
using MessageService.Domain.Entities;
|
||||
|
||||
namespace MessageService.Domain.Events
|
||||
{
|
||||
public record MessageWithdrawDomainEvent(Message Message) : INotification;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using MessageService.Domain.Entities;
|
||||
|
||||
namespace MessageService.Domain.IReposities
|
||||
{
|
||||
public interface IConversationReposity
|
||||
{
|
||||
Task<Conversation?> FindByIdAsync(Guid id);
|
||||
Task<IEnumerable<Conversation>> FindByUserIdAsync(Guid userId);
|
||||
Task<IEnumerable<Conversation>> FindByTargetIdAsync(Guid targetId);
|
||||
Task<IEnumerable<Conversation>> FindByStreamKeyAsync(string streamKey);
|
||||
void Create(Conversation conversation);
|
||||
Task<IEnumerable<string>> FindAllStreamKeyAsync(Guid userId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using MessageService.Domain.Entities;
|
||||
|
||||
namespace MessageService.Domain.IReposities
|
||||
{
|
||||
public interface IMessageReposity
|
||||
{
|
||||
Task<Message?> FindByIdAsync(Guid id);
|
||||
Task<(IEnumerable<Message> messages, bool hasMore)> GetAsync(string streamKey, long? cusor, int direction, int limit);
|
||||
void Create(Message message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MessageService.Domain.KeyObjects
|
||||
{
|
||||
public class MessageContent
|
||||
{
|
||||
// 兜底文本
|
||||
[JsonPropertyName("fallback")]
|
||||
public string Fallback { get; init; }
|
||||
|
||||
// 核心 Payload,反序列化时根据外层 MessageType 转换为具体子类
|
||||
public string RawBody { get; init; }
|
||||
public string RawExt { get; set; } = string.Empty;
|
||||
[JsonPropertyName("body")]
|
||||
// 业务代码使用的对象
|
||||
[NotMapped]
|
||||
public object Body => string.IsNullOrEmpty(RawBody) ? null : JsonSerializer.Deserialize<object>(RawBody);
|
||||
|
||||
[NotMapped]
|
||||
public Dictionary<string, string> Ext => string.IsNullOrEmpty(RawExt)
|
||||
? new()
|
||||
: JsonSerializer.Deserialize<Dictionary<string, string>>(RawExt);
|
||||
|
||||
// 引用信息
|
||||
[JsonPropertyName("quote")]
|
||||
public QuoteInfo Quote { get; set; } = new();
|
||||
|
||||
private MessageContent() { }
|
||||
public MessageContent(string fallback, object body)
|
||||
{
|
||||
Fallback = fallback;
|
||||
RawBody = body == null ? null : JsonSerializer.Serialize(body);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using MessageService.Domain.Enums;
|
||||
|
||||
namespace MessageService.Domain.KeyObjects
|
||||
{
|
||||
public record MessageCreateContext(
|
||||
ChatType ChatType,
|
||||
Guid ClientMsgId,
|
||||
Guid SenderId,
|
||||
Guid TargetId
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace MessageService.Domain.KeyObjects
|
||||
{
|
||||
public record TextBody(string Text);
|
||||
public record ImageBody(string Url, int Width, int Height, string Thumb);
|
||||
public record VideoBody(string Url, int Width, int Height, string Thumb);
|
||||
public record VoiceBody(string Url, int Duration);
|
||||
public record FileBody(string Url, string FileName, long Size, string Format);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using IM.DomainCommons;
|
||||
using MessageService.Domain.Enums;
|
||||
|
||||
namespace MessageService.Domain.KeyObjects
|
||||
{
|
||||
public class QuoteInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// 被引用消息的唯一标识
|
||||
/// </summary>
|
||||
public Guid MessageId { get; init; } = Guid.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 被引用消息的发送者 ID
|
||||
/// </summary>
|
||||
public Guid SenderId { get; init; } = Guid.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 快照:发送者当时的昵称 (非常关键)
|
||||
/// 避免客户端为了显示 "回复 @张三" 而去额外查询一次用户信息
|
||||
/// </summary>
|
||||
public string SenderName { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 被引用消息的类型 (例如:1=文本, 2=图片, 3=文件)
|
||||
/// 帮助客户端决定如何渲染左侧的 Icon (比如是一段文字,还是一个小图片占位符)
|
||||
/// </summary>
|
||||
public MessageType MessageType { get; init; } = MessageType.Text;
|
||||
|
||||
/// <summary>
|
||||
/// 被引用消息的内容预览
|
||||
/// 如果原消息是文本,则截取前50个字符;如果是图片,可以是 "[图片]"
|
||||
/// </summary>
|
||||
public string Preview { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数与自校验
|
||||
/// </summary>
|
||||
public QuoteInfo() { }
|
||||
public QuoteInfo(Guid messageId, Guid senderId, string senderName, MessageType messageType, string preview)
|
||||
{
|
||||
if (messageId == Guid.Empty)
|
||||
throw new DomainException("回复消息ID不可为空");
|
||||
|
||||
if (senderId == Guid.Empty)
|
||||
throw new DomainException("回复发送者ID不可为空");
|
||||
|
||||
MessageId = messageId;
|
||||
SenderId = senderId;
|
||||
SenderName = string.IsNullOrWhiteSpace(senderName) ? "Unknown" : senderName;
|
||||
MessageType = messageType;
|
||||
|
||||
// 限制预览文本的长度,防止 Payload 过大(截断处理)
|
||||
Preview = string.IsNullOrWhiteSpace(preview)
|
||||
? string.Empty
|
||||
: preview.Length > 50 ? preview[..47] + "..." : preview;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\IM.Commons\IM.Commons.csproj" />
|
||||
<ProjectReference Include="..\DomainCommons\IM.DomainCommons.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace MessageService.Domain.Tools
|
||||
{
|
||||
public static class StreamKeyBuilder
|
||||
{
|
||||
public static string Private(Guid a, Guid b)
|
||||
{
|
||||
// Guid 类型使用 Guid.Empty 来判断是否为无效/初始化的 ID
|
||||
if (a == Guid.Empty || b == Guid.Empty)
|
||||
throw new ArgumentException("IDs cannot be empty.");
|
||||
|
||||
// Guid 实现了 IComparable,使用 CompareTo 来保持生成 Key 的顺序一致性
|
||||
return $"p:{(a.CompareTo(b) < 0 ? $"{a}_{b}" : $"{b}_{a}")}";
|
||||
}
|
||||
|
||||
public static string Group(Guid groupId)
|
||||
{
|
||||
if (groupId == Guid.Empty)
|
||||
throw new ArgumentException("Group ID cannot be empty.");
|
||||
|
||||
return $"g:{groupId}";
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user