feat:增加查询消息接口模块

This commit is contained in:
2026-06-21 21:59:05 +08:00
parent f9084cfb23
commit c66bd02084
22 changed files with 398 additions and 38 deletions

View File

@@ -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<SnowflakeAttribute>(false) != null;
if (e.Column.CsType == typeof(long) && hasSnowflake)
{
if (e.Value == null || e.Value.ToString() == "0")
{
e.Value = YitIdHelper.NextId();
}
}
};
return fsql;
}
}
}

View File

@@ -0,0 +1,4 @@
namespace SweetChatService.Config
{
public class SnowflakeAttribute : Attribute { }
}

View File

@@ -11,11 +11,7 @@
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
app.UseEndpoints(endpoints => endpoints.MapControllers());
}
}
}

View File

@@ -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
});
}
}

View File

@@ -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));
}
}
}

View File

@@ -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)

View File

@@ -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"]

View File

@@ -0,0 +1,14 @@
namespace SweetChatService.Dto
{
/// <summary>
/// 会话消息
/// </summary>
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; }
}
}

View File

@@ -0,0 +1,48 @@
namespace SweetChatService.Dto
{
/// <summary>
/// 会话列表项
/// </summary>
public class ChatSessionDto
{
/// <summary>
/// 会话ID
/// </summary>
public long ConversationId { get; set; }
/// <summary>
/// 对方用户ID
/// </summary>
public long TargetUserId { get; set; }
/// <summary>
/// 对方昵称(优先备注名)
/// </summary>
public string Title { get; set; } = string.Empty;
/// <summary>
/// 对方头像
/// </summary>
public string AvatarUrl { get; set; } = string.Empty;
/// <summary>
/// 最后一条消息类型
/// </summary>
public int MsgType { get; set; }
/// <summary>
/// 最后一条消息内容
/// </summary>
public string LastMessageContent { get; set; } = string.Empty;
/// <summary>
/// 最后一条消息时间
/// </summary>
public DateTime LastMessageTime { get; set; }
/// <summary>
/// 未读消息数
/// </summary>
public int UnreadCount { get; set; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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
{

View File

@@ -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

View File

@@ -3,7 +3,7 @@
namespace SweetChatService.Models
{
[JsonConverter(typeof(JsonStringEnumConverter))]
internal enum ImMessageType
public enum ImMessageType
{
CHAT_PRIVATE,
CHAT_GROUP,

View File

@@ -10,9 +10,9 @@
<targets async="true">
<target name="fluentbit" xsi:type="Network"
newLine="true" lineEnding="LF"
address="tcp://60.247.145.200:5170">
address="tcp://host.docker.internal:5170">
<layout xsi:type="JsonLayout">
<attribute name="topic" layout="message-hub"/>
<attribute name="topic" layout="sweet-chat"/>
<attribute name="timestamp" layout="${longdate:universalTime=true}"/>
<attribute name="level" layout="${level:uppercase=true}"/>
<attribute name="logger" layout="${logger}"/>
@@ -27,6 +27,6 @@
</targets>
<rules>
<logger name="*" minlevel="Info" writeTo="stdout"/>
<logger name="*" minlevel="Info" writeTo="stdout, fluentbit"/>
</rules>
</nlog>

View File

@@ -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<WsMsgHandler>();
services.AddSingleton<WsSessionManager>();
@@ -19,6 +23,7 @@ var host = Host.CreateDefaultBuilder(args)
services.AddSingleton(FreeSqlSetup.CreateFreeSql(context.Configuration["ConnectionStrings:Default"]));
services.AddSingleton<UserService>();
services.AddSingleton<MessageService>();
})
.AsMultipleServerHostBuilder()
.AddWebSocketServer(builder =>

View File

@@ -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;
/// <summary>
/// 获取或创建单聊会话
/// </summary>
public async Task<bool> 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<ConversationMember>()
.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<Conversation>()
.Set(c => c.LastMsgId, message.MsgId)
.Set(c => c.UpdateTime, DateTime.Now)
.Where(c => c.Id == convId)
.ExecuteAffrowsAsync();
// 接收方未读+1
await _fsql.Update<ConversationMember>()
.Set(a => a.UnreadCount + 1)
.Where(w => w.ConversationId == convId && w.UserId == userId2)
.ExecuteAffrowsAsync();
// 发送方更新已读位置
await _fsql.Update<ConversationMember>()
.Set(a => a.LastReadMsgId, message.MsgId)
.Where(w => w.ConversationId == convId && w.UserId == userId1)
.ExecuteAffrowsAsync();
return true;
}
public List<ChatSessionDto> QueryChatSession(long userId)
{
return _fsql.Select<ConversationMember, Conversation, ConversationMember, User, Message>()
.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<ChatMessageDto> QueryChatMessage(long conversationId)
{
return _fsql.Select<Message>()
.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
});
}
}
}

View File

@@ -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))
{

View File

@@ -11,9 +11,17 @@
<PackageReference Include="FreeSql.Provider.MySqlConnector" Version="3.5.310" />
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="8.0.0" />
<PackageReference Include="NLog.Extensions.Hosting" Version="6.1.3" />
<PackageReference Include="NLog.Targets.Network" Version="6.0.4" />
<PackageReference Include="SuperSocket" Version="2.0.2" />
<PackageReference Include="SuperSocket.WebSocket" Version="2.1.0" />
<PackageReference Include="SuperSocket.WebSocket.Server" Version="2.1.0" />
<PackageReference Include="Yitter.IdGenerator" Version="1.0.14" />
</ItemGroup>
<ItemGroup>
<Content Update="NLog.config">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
@@ -23,6 +31,9 @@
<None Update="NLog.config">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Properties\launchSettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,6 @@
namespace Yitter
{
internal class YitIdHelper
{
}
}

View File

@@ -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": {