diff --git a/SweetChatService/Config/FreeSqlSetup.cs b/SweetChatService/Config/FreeSqlSetup.cs index c8399b6..f5761cb 100644 --- a/SweetChatService/Config/FreeSqlSetup.cs +++ b/SweetChatService/Config/FreeSqlSetup.cs @@ -1,4 +1,6 @@ using FreeSql; +using System.Reflection; +using Yitter.IdGenerator; namespace SweetChatService.Config { @@ -6,14 +8,29 @@ namespace SweetChatService.Config { public static IFreeSql CreateFreeSql(string connectionString) { - return new FreeSqlBuilder() + var fsql = new FreeSqlBuilder() .UseConnectionString(DataType.MySql, connectionString) .UseAutoSyncStructure(false) - .UseMonitorCommand(cmd => - { - Console.WriteLine(cmd.CommandText); - }) + //.UseMonitorCommand(cmd => + //{ + // Console.WriteLine(cmd.CommandText); + //}) .Build(); + + fsql.Aop.AuditValue += (_, e) => + { + var hasSnowflake = e.Property.GetCustomAttribute(false) != null; + + if (e.Column.CsType == typeof(long) && hasSnowflake) + { + if (e.Value == null || e.Value.ToString() == "0") + { + e.Value = YitIdHelper.NextId(); + } + } + }; + + return fsql; } } } diff --git a/SweetChatService/Config/SnowflakeAttribute.cs b/SweetChatService/Config/SnowflakeAttribute.cs new file mode 100644 index 0000000..3ba397b --- /dev/null +++ b/SweetChatService/Config/SnowflakeAttribute.cs @@ -0,0 +1,4 @@ +namespace SweetChatService.Config +{ + public class SnowflakeAttribute : Attribute { } +} diff --git a/SweetChatService/Config/Startup.cs b/SweetChatService/Config/Startup.cs index d6c92ec..5d1f181 100644 --- a/SweetChatService/Config/Startup.cs +++ b/SweetChatService/Config/Startup.cs @@ -11,11 +11,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseRouting(); - - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); + app.UseEndpoints(endpoints => endpoints.MapControllers()); } } } diff --git a/SweetChatService/Controllers/AuthController.cs b/SweetChatService/Controllers/AuthController.cs index baa24bf..8cdfab0 100644 --- a/SweetChatService/Controllers/AuthController.cs +++ b/SweetChatService/Controllers/AuthController.cs @@ -6,14 +6,9 @@ namespace SweetChatService.Controllers { [ApiController] [Route("api/[controller]")] - public class AuthController : ControllerBase + public class AuthController(UserService userService) : ControllerBase { - private readonly UserService _userService; - - public AuthController(UserService userService) - { - _userService = userService; - } + private readonly UserService _userService = userService; [HttpPost("login")] public IActionResult Login([FromBody] LoginDto dto) @@ -31,7 +26,7 @@ namespace SweetChatService.Controllers { return Ok(new { - userId = user.Id + user }); } } diff --git a/SweetChatService/Controllers/MessageController.cs b/SweetChatService/Controllers/MessageController.cs new file mode 100644 index 0000000..a08fabe --- /dev/null +++ b/SweetChatService/Controllers/MessageController.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Mvc; +using SweetChatService.Services; + +namespace SweetChatService.Controllers +{ + [ApiController] + [Route("api/[controller]")] + public class MessageController(MessageService messageService) : ControllerBase + { + private readonly MessageService _messageService = messageService; + + [HttpGet("session/{userId}")] + public IActionResult QueryChatSession(long userId) + { + return Ok(_messageService.QueryChatSession(userId)); + } + + [HttpGet("session/{conversationId}/detail")] + public IActionResult QueryChatMessage(long conversationId) + { + return Ok(_messageService.QueryChatMessage(conversationId)); + } + } +} diff --git a/SweetChatService/Controllers/UserController.cs b/SweetChatService/Controllers/UserController.cs index aa98ad9..b828857 100644 --- a/SweetChatService/Controllers/UserController.cs +++ b/SweetChatService/Controllers/UserController.cs @@ -7,14 +7,9 @@ namespace SweetChatService.Controllers { [ApiController] [Route("api/[controller]")] - public class UserController : ControllerBase + public class UserController(UserService userService) : ControllerBase { - private readonly UserService _userService; - - public UserController(UserService userService) - { - _userService = userService; - } + private readonly UserService _userService = userService; [HttpGet("friend/{userId}")] public IActionResult QueryFriendByUserId(long userId) diff --git a/SweetChatService/Dockerfile b/SweetChatService/Dockerfile new file mode 100644 index 0000000..75b8a6c --- /dev/null +++ b/SweetChatService/Dockerfile @@ -0,0 +1,8 @@ +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime +WORKDIR /app +COPY ./publish . +EXPOSE 5050 +EXPOSE 5051 +RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime +RUN echo 'Asia/Shanghai' > /etc/timezone +ENTRYPOINT ["dotnet", "SweetChatService.dll"] \ No newline at end of file diff --git a/SweetChatService/Dto/ChatMessageDto.cs b/SweetChatService/Dto/ChatMessageDto.cs new file mode 100644 index 0000000..23078df --- /dev/null +++ b/SweetChatService/Dto/ChatMessageDto.cs @@ -0,0 +1,14 @@ +namespace SweetChatService.Dto +{ + /// + /// 会话消息 + /// + public class ChatMessageDto + { + public long MsgId { get; set; } + public long SenderId { get; set; } + public string Content { get; set; } = string.Empty; + public byte MsgType { get; set; } + public DateTime CreateTime { get; set; } + } +} diff --git a/SweetChatService/Dto/ChatSessionDto.cs b/SweetChatService/Dto/ChatSessionDto.cs new file mode 100644 index 0000000..4db1059 --- /dev/null +++ b/SweetChatService/Dto/ChatSessionDto.cs @@ -0,0 +1,48 @@ +namespace SweetChatService.Dto +{ + /// + /// 会话列表项 + /// + public class ChatSessionDto + { + /// + /// 会话ID + /// + public long ConversationId { get; set; } + + /// + /// 对方用户ID + /// + public long TargetUserId { get; set; } + + /// + /// 对方昵称(优先备注名) + /// + public string Title { get; set; } = string.Empty; + + /// + /// 对方头像 + /// + public string AvatarUrl { get; set; } = string.Empty; + + /// + /// 最后一条消息类型 + /// + public int MsgType { get; set; } + + /// + /// 最后一条消息内容 + /// + public string LastMessageContent { get; set; } = string.Empty; + + /// + /// 最后一条消息时间 + /// + public DateTime LastMessageTime { get; set; } + + /// + /// 未读消息数 + /// + public int UnreadCount { get; set; } + } +} diff --git a/SweetChatService/Entity/Conversation.cs b/SweetChatService/Entity/Conversation.cs new file mode 100644 index 0000000..de761a9 --- /dev/null +++ b/SweetChatService/Entity/Conversation.cs @@ -0,0 +1,26 @@ +using FreeSql.DataAnnotations; +using System.ComponentModel; + +namespace SweetChatService.Entity +{ + [Table(Name = "conversation")] + [Description("会话表")] + public class Conversation + { + [Column(Name = "id", IsPrimary = true, IsIdentity = true)] + public long Id { get; set; } + + [Column(Name = "type")] + [Description("1单聊 2群聊")] + public byte Type { get; set; } + + [Column(Name = "last_msg_id")] + public long LastMsgId { get; set; } + + [Column(Name = "create_time", ServerTime = DateTimeKind.Local, CanInsert = true, CanUpdate = false)] + public DateTime CreateTime { get; set; } + + [Column(Name = "update_time", ServerTime = DateTimeKind.Local)] + public DateTime UpdateTime { get; set; } + } +} diff --git a/SweetChatService/Entity/ConversationMember.cs b/SweetChatService/Entity/ConversationMember.cs new file mode 100644 index 0000000..4f6cdc3 --- /dev/null +++ b/SweetChatService/Entity/ConversationMember.cs @@ -0,0 +1,28 @@ +using FreeSql.DataAnnotations; +using System.ComponentModel; + +namespace SweetChatService.Entity +{ + [Table(Name = "conversation_member")] + [Description("会话成员表")] + public class ConversationMember + { + [Column(Name = "conversation_id", IsPrimary = true)] + public long ConversationId { get; set; } + + [Column(Name = "user_id", IsPrimary = true)] + public long UserId { get; set; } + + [Column(Name = "unread_count")] + public int UnreadCount { get; set; } + + [Column(Name = "last_read_msg_id")] + public long LastReadMsgId { get; set; } + + [Column(Name = "create_time", ServerTime = DateTimeKind.Local, CanInsert = true, CanUpdate = false)] + public DateTime CreateTime { get; set; } + + [Column(Name = "update_time", ServerTime = DateTimeKind.Local)] + public DateTime UpdateTime { get; set; } + } +} diff --git a/SweetChatService/Entity/Message.cs b/SweetChatService/Entity/Message.cs new file mode 100644 index 0000000..367bc2a --- /dev/null +++ b/SweetChatService/Entity/Message.cs @@ -0,0 +1,34 @@ +using FreeSql.DataAnnotations; +using SweetChatService.Config; +using System.ComponentModel; + +namespace SweetChatService.Entity +{ + [Table(Name = "message")] + [Description("消息表")] + public class Message + { + [Column(Name = "msg_id", IsPrimary = true, IsIdentity = false)] + [Snowflake] + public long MsgId { get; set; } + + [Column(Name = "conversation_id")] + public long ConversationId { get; set; } + + [Column(Name = "sender_id")] + public long SenderId { get; set; } + + [Column(Name = "content")] + public string Content { get; set; } = ""; + + [Column(Name = "msg_type")] + [Description("1文本 2图片 3语音")] + public byte MsgType { get; set; } + + [Column(Name = "create_time", ServerTime = DateTimeKind.Local, CanInsert = true, CanUpdate = false)] + public DateTime CreateTime { get; set; } + + [Column(Name = "update_time", ServerTime = DateTimeKind.Local)] + public DateTime UpdateTime { get; set; } + } +} diff --git a/SweetChatService/Handlers/WsMsgHandler.cs b/SweetChatService/Handlers/WsMsgHandler.cs index de95cad..2b56e42 100644 --- a/SweetChatService/Handlers/WsMsgHandler.cs +++ b/SweetChatService/Handlers/WsMsgHandler.cs @@ -1,15 +1,9 @@ using Microsoft.AspNetCore.WebUtilities; -using Microsoft.Extensions.Logging; using SuperSocket.WebSocket; using SuperSocket.WebSocket.Server; using SweetChatService.Models; using SweetChatService.Services; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Text.Json; -using System.Threading.Tasks; namespace SweetChatService.Handlers { diff --git a/SweetChatService/Models/ImMessage.cs b/SweetChatService/Models/ImMessage.cs index 4c3814c..1ce560e 100644 --- a/SweetChatService/Models/ImMessage.cs +++ b/SweetChatService/Models/ImMessage.cs @@ -3,11 +3,14 @@ using System.Text.Json.Serialization; namespace SweetChatService.Models { - internal class ImMessage + public class ImMessage { [JsonPropertyName("type")] public ImMessageType Type { get; set; } + [JsonPropertyName("title")] + public string Title { get; set; } = default!; + [JsonPropertyName("from")] public string From { get; set; } = default!; @@ -22,6 +25,7 @@ namespace SweetChatService.Models return new ImMessage { Type = ImMessageType.SYSTEM, + Title = "系统通知", From = "SYSTEM", To = to, Content = content diff --git a/SweetChatService/Models/ImMessageType.cs b/SweetChatService/Models/ImMessageType.cs index c4691de..d06b6d9 100644 --- a/SweetChatService/Models/ImMessageType.cs +++ b/SweetChatService/Models/ImMessageType.cs @@ -3,7 +3,7 @@ namespace SweetChatService.Models { [JsonConverter(typeof(JsonStringEnumConverter))] - internal enum ImMessageType + public enum ImMessageType { CHAT_PRIVATE, CHAT_GROUP, diff --git a/SweetChatService/NLog.config b/SweetChatService/NLog.config index 2483d17..99050fb 100644 --- a/SweetChatService/NLog.config +++ b/SweetChatService/NLog.config @@ -10,9 +10,9 @@ + address="tcp://host.docker.internal:5170"> - + @@ -27,6 +27,6 @@ - + \ No newline at end of file diff --git a/SweetChatService/Program.cs b/SweetChatService/Program.cs index 2460cad..b3906bb 100644 --- a/SweetChatService/Program.cs +++ b/SweetChatService/Program.cs @@ -4,6 +4,7 @@ using SuperSocket.WebSocket.Server; using SweetChatService.Config; using SweetChatService.Handlers; using SweetChatService.Services; +using Yitter.IdGenerator; var host = Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => @@ -12,6 +13,9 @@ var host = Host.CreateDefaultBuilder(args) }) .ConfigureServices((context, services) => { + var options = new IdGeneratorOptions(6); + YitIdHelper.SetIdGenerator(options); + services.AddSingleton(); services.AddSingleton(); @@ -19,6 +23,7 @@ var host = Host.CreateDefaultBuilder(args) services.AddSingleton(FreeSqlSetup.CreateFreeSql(context.Configuration["ConnectionStrings:Default"])); services.AddSingleton(); + services.AddSingleton(); }) .AsMultipleServerHostBuilder() .AddWebSocketServer(builder => diff --git a/SweetChatService/Services/MessageService.cs b/SweetChatService/Services/MessageService.cs new file mode 100644 index 0000000..398e721 --- /dev/null +++ b/SweetChatService/Services/MessageService.cs @@ -0,0 +1,141 @@ +using SweetChatService.Dto; +using SweetChatService.Entity; +using SweetChatService.Models; + +namespace SweetChatService.Services +{ + public class MessageService + { + private readonly IFreeSql _fsql; + + public MessageService(IFreeSql fsql) => _fsql = fsql; + + /// + /// 获取或创建单聊会话 + /// + public async Task SavePrivateMessageAsync(ImMessage msg) + { + long userId1 = long.Parse(msg.From); + long userId2 = long.Parse(msg.To); + + var ids = new[] { userId1, userId2 }; + Array.Sort(ids); + + // 1️ 查询是否已存在会话 + + // 先从ConversationMember把包含用户1和用户2的找到 + // 然后按照ConversationId分组 + // 如果正好这个组里面的个数为2,则说明只有1和2,否则是其他的,比如群 + var convId = await _fsql + .Select() + .Where(m => new[] { userId1, userId2 }.Contains(m.UserId)) + .GroupBy(m => m.ConversationId) + .Having(g => g.Count() == 2) + .FirstAsync(g => g.Key); + + // 2. 如果没查到 + if (convId <= 0) + { + // 则先新增1条会话记录 + var conversation = new Conversation + { + Type = 1 + }; + conversation.Id = await _fsql.Insert(conversation).ExecuteIdentityAsync(); + + // 然后新增2条会话成员记录 + var conversation_member = new[] + { + // 一条是user1的 + new ConversationMember + { + ConversationId = conversation.Id, + UserId = ids[0], + UnreadCount = 0 + }, + // 一条是user2的 + new ConversationMember + { + ConversationId = conversation.Id, + UserId = ids[1], + UnreadCount = 0 + } + }; + await _fsql.Insert(conversation_member).ExecuteAffrowsAsync(); + + // 重新赋值 + convId = conversation.Id; + } + + // 3. 插入消息表 + var message = new Message + { + ConversationId = convId, + SenderId = userId1, + Content = msg.Content, + MsgType = 1, + CreateTime = DateTime.Now + }; + + message.MsgId = await _fsql.Insert(message).ExecuteIdentityAsync(); + + // 更新会话表 + await _fsql.Update() + .Set(c => c.LastMsgId, message.MsgId) + .Set(c => c.UpdateTime, DateTime.Now) + .Where(c => c.Id == convId) + .ExecuteAffrowsAsync(); + + // 接收方未读+1 + await _fsql.Update() + .Set(a => a.UnreadCount + 1) + .Where(w => w.ConversationId == convId && w.UserId == userId2) + .ExecuteAffrowsAsync(); + + // 发送方更新已读位置 + await _fsql.Update() + .Set(a => a.LastReadMsgId, message.MsgId) + .Where(w => w.ConversationId == convId && w.UserId == userId1) + .ExecuteAffrowsAsync(); + + return true; + } + + public List QueryChatSession(long userId) + { + return _fsql.Select() + .LeftJoin((cm, c, cmo, u, m) => cm.ConversationId == c.Id && c.Type == 1) + .LeftJoin((cm, c, cmo, u, m) => cmo.ConversationId == c.Id && cmo.UserId != cm.UserId) + .LeftJoin((cm, c, cmo, u, m) => u.Id == cmo.UserId) + .LeftJoin((cm, c, cmo, u, m) => c.LastMsgId == m.MsgId) + .Where((cm, _, _, _, _) => cm.UserId == userId) + .OrderByDescending((_, _, _, _, m) => m.CreateTime) + .ToList((cm, c, cmo, u, m) => new ChatSessionDto + { + ConversationId = c.Id, + TargetUserId = u.Id, + Title = u.Nickname, + AvatarUrl = u.AvatarUrl, + MsgType = m.MsgType, + LastMessageContent = m.Content ?? "", + LastMessageTime = m.CreateTime, + UnreadCount = cm.UnreadCount + }); + } + + public List QueryChatMessage(long conversationId) + { + return _fsql.Select() + .Where(m => m.ConversationId == conversationId) + .OrderBy(m => m.MsgId) + .ToList((m) => new ChatMessageDto + { + MsgId = m.MsgId, + SenderId = m.SenderId, + Content = m.Content, + MsgType = m.MsgType, + CreateTime = m.CreateTime + }); + } + } +} diff --git a/SweetChatService/Services/PrivateChatService.cs b/SweetChatService/Services/PrivateChatService.cs index a78ee10..56bf44d 100644 --- a/SweetChatService/Services/PrivateChatService.cs +++ b/SweetChatService/Services/PrivateChatService.cs @@ -3,12 +3,15 @@ using SweetChatService.Models; namespace SweetChatService.Services { - internal class PrivateChatService(WsSessionManager wsSessionManager) + internal class PrivateChatService(WsSessionManager wsSessionManager, MessageService messageService) { private readonly WsSessionManager _wsSessionManager = wsSessionManager; + private readonly MessageService _messageService = messageService; public async Task HandleAsync(ImMessage msg) { + await _messageService.SavePrivateMessageAsync(msg); + // 推送给对方 if (_wsSessionManager.TryGet(msg.To, out var toSession)) { diff --git a/SweetChatService/SweetChatService.csproj b/SweetChatService/SweetChatService.csproj index 396e04c..cc12ca5 100644 --- a/SweetChatService/SweetChatService.csproj +++ b/SweetChatService/SweetChatService.csproj @@ -11,9 +11,17 @@ + + + + + + + Always + @@ -23,6 +31,9 @@ Always + + Always + diff --git a/SweetChatService/Yitter/YitIdHelper.cs b/SweetChatService/Yitter/YitIdHelper.cs new file mode 100644 index 0000000..f98597a --- /dev/null +++ b/SweetChatService/Yitter/YitIdHelper.cs @@ -0,0 +1,6 @@ +namespace Yitter +{ + internal class YitIdHelper + { + } +} \ No newline at end of file diff --git a/SweetChatService/appsettings.json b/SweetChatService/appsettings.json index 38e2d10..ebed9b6 100644 --- a/SweetChatService/appsettings.json +++ b/SweetChatService/appsettings.json @@ -1,6 +1,13 @@ { "ConnectionStrings": { - "Default": "Server=localhost;Port=3306;Database=sweet_chat;Uid=root;Pwd=estun@medical;Charset=utf8mb4;" + "Default": "Server=host.docker.internal;Port=3306;Database=sweet_chat;Uid=root;Pwd=19940822Cxx@1213;Charset=utf8mb4;" + }, + "Kestrel": { + "Endpoints": { + "Http": { + "Url": "http://+:5051" + } + } }, "serverOptions": { "WebSocketServer": {