diff --git a/ConnectorService/ConnectorService.csproj b/ConnectorService/ConnectorService.csproj
new file mode 100644
index 0000000..7ae4266
--- /dev/null
+++ b/ConnectorService/ConnectorService.csproj
@@ -0,0 +1,20 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ConnectorService/ConnectorService.http b/ConnectorService/ConnectorService.http
new file mode 100644
index 0000000..cff0eb9
--- /dev/null
+++ b/ConnectorService/ConnectorService.http
@@ -0,0 +1,6 @@
+@ConnectorService_HostAddress = http://localhost:5100
+
+GET {{ConnectorService_HostAddress}}/weatherforecast/
+Accept: application/json
+
+###
diff --git a/ConnectorService/Consumers/MessageConsumer.cs b/ConnectorService/Consumers/MessageConsumer.cs
new file mode 100644
index 0000000..b5cb79b
--- /dev/null
+++ b/ConnectorService/Consumers/MessageConsumer.cs
@@ -0,0 +1,24 @@
+using ConnectorService.Dtos;
+using ConnectorService.Hubs;
+using IM.Commons.IntegrationEvents;
+using MassTransit;
+using Microsoft.AspNetCore.SignalR;
+
+namespace ConnectorService.Consumers
+{
+ public class MessageConsumer : IConsumer
+ {
+ private readonly IHubContext hub;
+
+ public MessageConsumer(IHubContext hub)
+ {
+ this.hub = hub;
+ }
+
+ public async Task Consume(ConsumeContext context)
+ {
+ var @event = context.Message;
+ await hub.Clients.Group(@event.StreamKey).SendAsync("ReceiveNewMessage", @event.ToHubResponse());
+ }
+ }
+}
diff --git a/ConnectorService/Dtos/MessageHubResponse.cs b/ConnectorService/Dtos/MessageHubResponse.cs
new file mode 100644
index 0000000..92c1e61
--- /dev/null
+++ b/ConnectorService/Dtos/MessageHubResponse.cs
@@ -0,0 +1,81 @@
+using IM.Commons.IntegrationEvents;
+
+namespace ConnectorService.Dtos
+{
+ public record MessageHubResponse
+ {
+ public Guid Id { get; init; }
+
+ ///
+ /// 客户端去重/回执使用的本地 ID
+ ///
+ public Guid ClientId { get; init; }
+
+ public string ChatType { get; init; } = string.Empty;
+ public string MsgType { get; init; } = string.Empty;
+ public Guid SenderId { get; init; }
+ public Guid TargetId { get; init; }
+ public string State { get; init; } = string.Empty;
+ public string StreamKey { get; init; } = string.Empty;
+ public long SequenceId { get; init; }
+
+ ///
+ /// 服务器推送到达时间 (毫秒级时间戳,强烈建议加上)
+ ///
+ public long PushTimestamp { get; init; } = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+
+ public HubMsgContentDto Content { get; init; } = null!;
+ }
+
+ // 嵌套的内容对象,允许 Ext 和 Quote 为 null 以缩减 JSON 体积
+ public record HubMsgContentDto(
+ string Fallback,
+ object Body,
+ Dictionary? Ext,
+ HubQuoteInfoDto? Quote
+ );
+
+ public record HubQuoteInfoDto(
+ Guid MessageId,
+ Guid SenderId,
+ string SenderName,
+ string MessageType,
+ string Preview
+ );
+ public static class MessageEventMapper
+ {
+ ///
+ /// 将内部集成事件转换为对外推送的 DTO
+ ///
+ public static MessageHubResponse ToHubResponse(this MsgCreatedEvent @event)
+ {
+ if (@event == null) throw new ArgumentNullException(nameof(@event));
+
+ return new MessageHubResponse
+ {
+ Id = @event.Id,
+ ClientId = @event.ClientId,
+ ChatType = @event.ChatType,
+ MsgType = @event.MsgType,
+ SenderId = @event.SenderId,
+ TargetId = @event.TargetId,
+ State = @event.State,
+ StreamKey = @event.StreamKey,
+ SequenceId = @event.SequenceId,
+ // 嵌套映射
+ Content = @event.Content != null ? new HubMsgContentDto(
+ @event.Content.Fallback,
+ @event.Content.Body,
+ @event.Content.Ext,
+ @event.Content.Quote != null ? new HubQuoteInfoDto(
+ @event.Content.Quote.MessageId,
+ @event.Content.Quote.SenderId,
+ @event.Content.Quote.SenderName,
+ @event.Content.Quote.MessageType,
+ @event.Content.Quote.Preview
+ ) : null
+ ) : null!
+ };
+ }
+ }
+}
diff --git a/ConnectorService/Hubs/ChatHub.cs b/ConnectorService/Hubs/ChatHub.cs
new file mode 100644
index 0000000..0d9d83e
--- /dev/null
+++ b/ConnectorService/Hubs/ChatHub.cs
@@ -0,0 +1,53 @@
+using ConnectorService.Services;
+using IM.Commons;
+using Microsoft.AspNetCore.SignalR;
+using StackExchange.Redis;
+using System.Security.Claims;
+
+namespace ConnectorService.Hubs
+{
+ public class ChatHub : Hub
+ {
+ private readonly IConversationIntergrationService conService;
+ private readonly StackExchange.Redis.IDatabase redis;
+
+ public ChatHub(IConversationIntergrationService conService, IConnectionMultiplexer multiplexer)
+ {
+ this.conService = conService;
+ this.redis = multiplexer.GetDatabase();
+ }
+
+ public async override Task OnConnectedAsync()
+ {
+ if (!Context.User.Identity.IsAuthenticated)
+ {
+ Context.Abort();
+ return;
+ }
+
+ var userId = Context.User.FindFirstValue(ClaimTypes.NameIdentifier);
+
+ var res = await conService.GetUserStreamKeysAsync(Guid.Parse(userId));
+ foreach (var streamkey in res)
+ {
+ await Groups.AddToGroupAsync(Context.ConnectionId, streamkey);
+ }
+
+ await redis.SetAddAsync(RedisHelper.GetConnectionIdKey(userId), Context.ConnectionId);
+
+
+ await base.OnConnectedAsync();
+ }
+
+ public async override Task OnDisconnectedAsync(Exception? exception)
+ {
+ if (Context.User.Identity.IsAuthenticated)
+ {
+ var userId = Context.User.FindFirstValue(ClaimTypes.NameIdentifier);
+
+ await redis.SetRemoveAsync(RedisHelper.GetConnectionIdKey(userId), Context.ConnectionId);
+ }
+ await base.OnDisconnectedAsync(exception);
+ }
+ }
+}
diff --git a/ConnectorService/ModuleInit.cs b/ConnectorService/ModuleInit.cs
new file mode 100644
index 0000000..11b68e2
--- /dev/null
+++ b/ConnectorService/ModuleInit.cs
@@ -0,0 +1,19 @@
+using IM.Commons;
+using IM.Protocols.Grpc.Conversation;
+using Microsoft.Extensions.Options;
+
+namespace ConnectorService
+{
+ public class ModuleInit : IModuleInitializer
+ {
+ public void Initialize(IServiceCollection services)
+ {
+ services.AddRedisCache();
+ services.AddGrpcClient((sp ,o) =>
+ {
+ var options = sp.GetRequiredService>();
+ o.Address = new Uri(options.CurrentValue.MessageServiceUrl);
+ });
+ }
+ }
+}
diff --git a/ConnectorService/Program.cs b/ConnectorService/Program.cs
new file mode 100644
index 0000000..e2d1b92
--- /dev/null
+++ b/ConnectorService/Program.cs
@@ -0,0 +1,42 @@
+
+using ConnectorService.Hubs;
+using IM.InitCommon;
+
+namespace ConnectorService
+{
+ public class Program
+ {
+ public static void Main(string[] args)
+ {
+ var builder = WebApplication.CreateBuilder(args);
+
+ // Add services to the container.
+
+ builder.ConfigureDbConfiguration();
+
+ builder.Services.AddSignalR();
+
+ // 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.MapHub("/chat");
+
+ app.Run();
+ }
+ }
+}
diff --git a/ConnectorService/Properties/launchSettings.json b/ConnectorService/Properties/launchSettings.json
new file mode 100644
index 0000000..3afd3a3
--- /dev/null
+++ b/ConnectorService/Properties/launchSettings.json
@@ -0,0 +1,41 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:46392",
+ "sslPort": 44313
+ }
+ },
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "http://localhost:5100",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "https://localhost:7115;http://localhost:5100",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/ConnectorService/Services/ConversationIntegrationService.cs b/ConnectorService/Services/ConversationIntegrationService.cs
new file mode 100644
index 0000000..0cd472b
--- /dev/null
+++ b/ConnectorService/Services/ConversationIntegrationService.cs
@@ -0,0 +1,30 @@
+using IM.Commons;
+using IM.Protocols.Grpc.Conversation;
+
+namespace ConnectorService.Services
+{
+ public class ConversationIntegrationService : IConversationIntergrationService
+ {
+ private readonly ConversationInternal.ConversationInternalClient client;
+ public async Task> GetUserStreamKeysAsync(Guid userId)
+ {
+ var req = new GetUserStreamKeysRequest()
+ {
+ UserId = userId.ToString()
+ };
+ var res = await client.GetUserStreamKeysAsync(req);
+ if(res == null)
+ {
+ return [];
+ }
+
+ var list = new List();
+ foreach(var item in res.StreamKeys)
+ {
+ list.Add(item);
+ }
+
+ return list;
+ }
+ }
+}
diff --git a/ConnectorService/Services/IConversationIntergrationService.cs b/ConnectorService/Services/IConversationIntergrationService.cs
new file mode 100644
index 0000000..fb9a0c3
--- /dev/null
+++ b/ConnectorService/Services/IConversationIntergrationService.cs
@@ -0,0 +1,9 @@
+using IM.Commons;
+
+namespace ConnectorService.Services
+{
+ public interface IConversationIntergrationService
+ {
+ Task> GetUserStreamKeysAsync(Guid userId);
+ }
+}
diff --git a/ConnectorService/appsettings.Development.json b/ConnectorService/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/ConnectorService/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/ConnectorService/appsettings.json b/ConnectorService/appsettings.json
new file mode 100644
index 0000000..10f68b8
--- /dev/null
+++ b/ConnectorService/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/ContactService.Domain/ContactService.Domain.csproj b/ContactService.Domain/ContactService.Domain.csproj
new file mode 100644
index 0000000..2c335d3
--- /dev/null
+++ b/ContactService.Domain/ContactService.Domain.csproj
@@ -0,0 +1,14 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
diff --git a/ContactService.Domain/Entities/Friend.cs b/ContactService.Domain/Entities/Friend.cs
new file mode 100644
index 0000000..f8b58b8
--- /dev/null
+++ b/ContactService.Domain/Entities/Friend.cs
@@ -0,0 +1,54 @@
+using ContactService.Domain.Events;
+using ContactService.Domain.ValueObjects;
+using IM.DomainCommons;
+
+namespace ContactService.Domain.Entities
+{
+ public class Friend : AggregateRootEntity
+ {
+ public UserProfile Owner { get; private set; }
+ public UserProfile Target { get; private set; }
+ ///
+ /// 好友备注名
+ ///
+ public string? RemarkName { get; private set; }
+ public FriendStatus Status { get; private set; }
+
+ private Friend() { }
+
+ public Friend(UserProfile owner, UserProfile target, string? remarkName)
+ {
+ Owner = owner;
+ Target = target;
+ RemarkName = remarkName;
+ Status = FriendStatus.Added;
+
+ AddDomainEvent(new FriendAddedDomainEvent(this));
+ }
+
+ public void setRemarkName(string? remarkName)
+ {
+ if (remarkName.Length > 20)
+ {
+ throw new DomainException("备注名过长");
+ }
+ RemarkName = remarkName ?? RemarkName;
+ }
+
+ public void Block()
+ {
+ Status = FriendStatus.Blocked;
+ AddDomainEvent(new FriendBlockDomainEvent(this));
+ }
+
+ public void UpdateUserInfo(UserProfile profile)
+ {
+ if (profile.Id != Target.Id)
+ {
+ return;
+ }
+
+ Target = profile;
+ }
+ }
+}
diff --git a/ContactService.Domain/Entities/FriendRequest.cs b/ContactService.Domain/Entities/FriendRequest.cs
new file mode 100644
index 0000000..7506e48
--- /dev/null
+++ b/ContactService.Domain/Entities/FriendRequest.cs
@@ -0,0 +1,73 @@
+using ContactService.Domain.Events;
+using IM.DomainCommons;
+
+namespace ContactService.Domain.Entities
+{
+ public class FriendRequest : AggregateRootEntity
+ {
+ ///
+ /// 申请人
+ ///
+ public Guid OwnerId { get; private set; }
+
+ ///
+ /// 被申请人
+ ///
+ public Guid TargetId { get; private set; }
+
+
+ ///
+ /// 申请附言
+ ///
+ public string Description { get; private set; } = "申请添加好友";
+
+ ///
+ /// 申请状态(0:待通过,1:拒绝,2:同意,3:拉黑)
+ ///
+ public FriendRequestStatus State { get; private set; } = FriendRequestStatus.Pending;
+
+ ///
+ /// 备注
+ ///
+ public string? RemarkName { get; private set; }
+
+ private FriendRequest() { }
+
+ public FriendRequest(Guid ownerId, Guid targetId, string? description, string? remarkName)
+ {
+ OwnerId = ownerId;
+ TargetId = targetId;
+ Description = description ?? Description;
+ RemarkName = remarkName;
+ AddDomainEvent(new FriendRequestCreatedDomainEvent(this));
+ }
+ public void Accept(string remarkName)
+ {
+ if (State != FriendRequestStatus.Pending)
+ {
+ throw new DomainException("只能处理待处理的好友请求");
+ }
+ State = FriendRequestStatus.Passed;
+ AddDomainEvent(new FriendRequestStateUpdateDomainEvent(this,remarkName));
+ }
+
+ public void Reject()
+ {
+ if (State != FriendRequestStatus.Pending)
+ {
+ throw new DomainException("只能处理待处理的好友请求");
+ }
+ State = FriendRequestStatus.Declined;
+ AddDomainEvent(new FriendRequestStateUpdateDomainEvent(this));
+ }
+ public void Block()
+ {
+ if (State != FriendRequestStatus.Pending)
+ {
+ throw new DomainException("只能处理待处理的好友请求");
+ }
+ State = FriendRequestStatus.Blocked;
+ AddDomainEvent(new FriendRequestStateUpdateDomainEvent(this));
+ }
+ }
+}
diff --git a/ContactService.Domain/Events/FriendAddedDomainEvent.cs b/ContactService.Domain/Events/FriendAddedDomainEvent.cs
new file mode 100644
index 0000000..e9334fd
--- /dev/null
+++ b/ContactService.Domain/Events/FriendAddedDomainEvent.cs
@@ -0,0 +1,7 @@
+using ContactService.Domain.Entities;
+using MediatR;
+
+namespace ContactService.Domain.Events
+{
+ public record FriendAddedDomainEvent(Friend Friend) : INotification;
+}
diff --git a/ContactService.Domain/Events/FriendBlockDomainEvent.cs b/ContactService.Domain/Events/FriendBlockDomainEvent.cs
new file mode 100644
index 0000000..9f8a06e
--- /dev/null
+++ b/ContactService.Domain/Events/FriendBlockDomainEvent.cs
@@ -0,0 +1,7 @@
+using ContactService.Domain.Entities;
+using MediatR;
+
+namespace ContactService.Domain.Events
+{
+ public record FriendBlockDomainEvent(Friend Friend) : INotification;
+}
diff --git a/ContactService.Domain/Events/FriendRequestCreatedDomainEvent.cs b/ContactService.Domain/Events/FriendRequestCreatedDomainEvent.cs
new file mode 100644
index 0000000..4772fca
--- /dev/null
+++ b/ContactService.Domain/Events/FriendRequestCreatedDomainEvent.cs
@@ -0,0 +1,7 @@
+using ContactService.Domain.Entities;
+using MediatR;
+
+namespace ContactService.Domain.Events
+{
+ public record FriendRequestCreatedDomainEvent(FriendRequest Request) : INotification;
+}
diff --git a/ContactService.Domain/Events/FriendRequestStateUpdateDomainEvent.cs b/ContactService.Domain/Events/FriendRequestStateUpdateDomainEvent.cs
new file mode 100644
index 0000000..71610a7
--- /dev/null
+++ b/ContactService.Domain/Events/FriendRequestStateUpdateDomainEvent.cs
@@ -0,0 +1,7 @@
+using ContactService.Domain.Entities;
+using MediatR;
+
+namespace ContactService.Domain.Events
+{
+ public record FriendRequestStateUpdateDomainEvent(FriendRequest Request,string? AcceptRemarkName = default) : INotification;
+}
diff --git a/ContactService.Domain/FriendDomainService.cs b/ContactService.Domain/FriendDomainService.cs
new file mode 100644
index 0000000..9adbcc9
--- /dev/null
+++ b/ContactService.Domain/FriendDomainService.cs
@@ -0,0 +1,28 @@
+using ContactService.Domain.Entities;
+using ContactService.Domain.ValueObjects;
+using IM.Commons;
+
+namespace ContactService.Domain
+{
+ public class FriendDomainService
+ {
+ private readonly IFriendReposity reposity;
+
+ public FriendDomainService(IFriendReposity reposity)
+ {
+ this.reposity = reposity;
+ }
+
+ public async Task> CreateAsync(UserProfile owner, UserProfile target, string? remarkName)
+ {
+ var isExist = await reposity.CheckOwnerIdAndTargetIdAsync(owner.Id, target.Id);
+ if (isExist)
+ {
+ return Result.Fail(ResultCode.ALREADY_FRIENDS);
+ }
+ var friend = new Friend(owner, target, remarkName);
+ var res = await reposity.CreateAsync(friend);
+ return Result.Success(res);
+ }
+ }
+}
diff --git a/ContactService.Domain/FriendRequestDomainService.cs b/ContactService.Domain/FriendRequestDomainService.cs
new file mode 100644
index 0000000..c8184c1
--- /dev/null
+++ b/ContactService.Domain/FriendRequestDomainService.cs
@@ -0,0 +1,22 @@
+using ContactService.Domain.Entities;
+using IM.Commons;
+
+namespace ContactService.Domain
+{
+ public class FriendRequestDomainService
+ {
+ private readonly IFriendRequestReposity reposity;
+
+ public FriendRequestDomainService(IFriendRequestReposity reposity)
+ {
+ this.reposity = reposity;
+ }
+
+ public async Task> CreateAsync(Guid ownerId, Guid targetId, string? description, string? remarkName)
+ {
+ var friendRequest = new FriendRequest(ownerId, targetId, description, remarkName);
+ await reposity.CreateAsync(friendRequest);
+ return Result.Success(friendRequest);
+ }
+ }
+}
diff --git a/ContactService.Domain/FriendRequestStatus.cs b/ContactService.Domain/FriendRequestStatus.cs
new file mode 100644
index 0000000..1f7eba7
--- /dev/null
+++ b/ContactService.Domain/FriendRequestStatus.cs
@@ -0,0 +1,22 @@
+namespace ContactService.Domain
+{
+ public enum FriendRequestStatus
+ {
+ ///
+ /// 待处理
+ ///
+ Pending = 0,
+ ///
+ /// 已通过
+ ///
+ Passed = 2,
+ ///
+ /// 已拒绝
+ ///
+ Declined = 1,
+ ///
+ /// 拉黑
+ ///
+ Blocked = 3
+ }
+}
diff --git a/ContactService.Domain/FriendStatus.cs b/ContactService.Domain/FriendStatus.cs
new file mode 100644
index 0000000..be19b10
--- /dev/null
+++ b/ContactService.Domain/FriendStatus.cs
@@ -0,0 +1,22 @@
+namespace ContactService.Domain
+{
+ public enum FriendStatus
+ {
+ ///
+ /// 待处理
+ ///
+ Pending = 0,
+ ///
+ /// 已添加
+ ///
+ Added = 1,
+ ///
+ /// 已拒绝
+ ///
+ Declined = 2,
+ ///
+ /// 已拉黑
+ ///
+ Blocked = 3
+ }
+}
diff --git a/ContactService.Domain/IFriendReposity.cs b/ContactService.Domain/IFriendReposity.cs
new file mode 100644
index 0000000..61e9595
--- /dev/null
+++ b/ContactService.Domain/IFriendReposity.cs
@@ -0,0 +1,16 @@
+using ContactService.Domain.Entities;
+
+namespace ContactService.Domain
+{
+ public interface IFriendReposity
+ {
+ Task FindByIdAsync(Guid id);
+ Task FindByOwnerAndTargetAsync(Guid ownerId, Guid targetId);
+ Task> FindByTargetAsync(Guid targetId);
+ Task> FindByOwnerAsync(Guid ownerId);
+
+ Task CreateAsync(Friend friend);
+
+ Task CheckOwnerIdAndTargetIdAsync(Guid ownerId, Guid targetId);
+ }
+}
diff --git a/ContactService.Domain/IFriendRequestReposity.cs b/ContactService.Domain/IFriendRequestReposity.cs
new file mode 100644
index 0000000..8845d4a
--- /dev/null
+++ b/ContactService.Domain/IFriendRequestReposity.cs
@@ -0,0 +1,13 @@
+using ContactService.Domain.Entities;
+
+namespace ContactService.Domain
+{
+ public interface IFriendRequestReposity
+ {
+ Task CreateAsync(FriendRequest friendRequest);
+ Task FindByIdAsync(Guid id);
+ Task> FindByOwnerIdAsync(Guid ownerId);
+ Task> FindByTargetIdAsync(Guid targetId);
+
+ }
+}
diff --git a/ContactService.Domain/ValueObjects/UserProfile.cs b/ContactService.Domain/ValueObjects/UserProfile.cs
new file mode 100644
index 0000000..680b53d
--- /dev/null
+++ b/ContactService.Domain/ValueObjects/UserProfile.cs
@@ -0,0 +1,17 @@
+namespace ContactService.Domain.ValueObjects
+{
+ public class UserProfile
+ {
+ public Guid Id { get; private set; }
+ public string NickName { get; private set; }
+ public string? Avatar { get; private set; }
+ public UserProfile() { }
+
+ public UserProfile(Guid id, string nickName, string? avatar)
+ {
+ Id = id;
+ NickName = nickName;
+ Avatar = avatar;
+ }
+ };
+}
diff --git a/ContactService.Infrastructure/Configs/FriendConfig.cs b/ContactService.Infrastructure/Configs/FriendConfig.cs
new file mode 100644
index 0000000..bb4972e
--- /dev/null
+++ b/ContactService.Infrastructure/Configs/FriendConfig.cs
@@ -0,0 +1,35 @@
+using ContactService.Domain.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace ContactService.Infrastructure.Configs
+{
+ public class FriendConfig : IEntityTypeConfiguration
+ {
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.ToTable("friends");
+ builder.HasKey(x => x.Id);
+ builder.OwnsOne(x => x.Owner, owner =>
+ {
+ owner.WithOwner(); // 🔥 关键:明确归属
+
+ owner.Property(p => p.Id).HasColumnName("OwnerId").IsRequired();
+ owner.Property(p => p.NickName).HasColumnName("OwnerNickName");
+ owner.Property(p => p.Avatar).HasColumnName("OwnerAvatarUrl");
+ });
+
+ builder.OwnsOne(x => x.Target, target =>
+ {
+ target.WithOwner(); // 🔥 关键
+
+ target.Property(p => p.Id).HasColumnName("TargetId").IsRequired();
+ target.Property(p => p.NickName).HasColumnName("TargetNickName");
+ target.Property(p => p.Avatar).HasColumnName("TargetAvatarUrl");
+ });
+
+
+
+ }
+ }
+}
diff --git a/ContactService.Infrastructure/Configs/FriendRequestConfig.cs b/ContactService.Infrastructure/Configs/FriendRequestConfig.cs
new file mode 100644
index 0000000..d4e052b
--- /dev/null
+++ b/ContactService.Infrastructure/Configs/FriendRequestConfig.cs
@@ -0,0 +1,20 @@
+using ContactService.Domain.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace ContactService.Infrastructure.Configs
+{
+ public class FriendRequestConfig : IEntityTypeConfiguration
+ {
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.ToTable("friend_requests");
+
+ builder.HasKey(x => x.Id);
+
+ builder.HasIndex(x => new { x.OwnerId, x.TargetId });
+
+
+ }
+ }
+}
diff --git a/ContactService.Infrastructure/ContactDbContext.cs b/ContactService.Infrastructure/ContactDbContext.cs
new file mode 100644
index 0000000..efb2479
--- /dev/null
+++ b/ContactService.Infrastructure/ContactDbContext.cs
@@ -0,0 +1,24 @@
+using ContactService.Domain.Entities;
+using IM.Infrastructure.Efcore;
+using MediatR;
+using Microsoft.EntityFrameworkCore;
+
+namespace ContactService.Infrastructure
+{
+ public class ContactDbContext : BaseDbContext
+ {
+ public DbSet Friends { get; private set; }
+ public DbSet FriendRequests { get; private set; }
+ public ContactDbContext(DbContextOptions options, IMediator mediator) : base(options, mediator)
+ {
+ }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ base.OnModelCreating(modelBuilder);
+ modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
+
+ modelBuilder.EnableSoftDeletionGlobalFilter();
+ }
+ }
+}
diff --git a/ContactService.Infrastructure/ContactService.Infrastructure.csproj b/ContactService.Infrastructure/ContactService.Infrastructure.csproj
new file mode 100644
index 0000000..e443aa7
--- /dev/null
+++ b/ContactService.Infrastructure/ContactService.Infrastructure.csproj
@@ -0,0 +1,25 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ContactService.Infrastructure/FriendReposity.cs b/ContactService.Infrastructure/FriendReposity.cs
new file mode 100644
index 0000000..8605747
--- /dev/null
+++ b/ContactService.Infrastructure/FriendReposity.cs
@@ -0,0 +1,46 @@
+using ContactService.Domain;
+using ContactService.Domain.Entities;
+using Microsoft.EntityFrameworkCore;
+
+namespace ContactService.Infrastructure
+{
+ public class FriendReposity : IFriendReposity
+ {
+ private readonly ContactDbContext db;
+
+ public FriendReposity(ContactDbContext db)
+ {
+ this.db = db;
+ }
+
+ public async Task CheckOwnerIdAndTargetIdAsync(Guid ownerId, Guid targetId)
+ {
+ var exist = await db.Friends.AnyAsync(x => x.Owner.Id == ownerId && x.Target.Id == targetId);
+ return exist;
+ }
+
+ public async Task CreateAsync(Friend friend)
+ {
+ db.Add(friend);
+ return friend;
+ }
+
+ public Task FindByIdAsync(Guid id)
+ {
+ return db.Friends.FirstOrDefaultAsync(x => x.Id == id);
+ }
+
+ public Task FindByOwnerAndTargetAsync(Guid ownerId, Guid targetId)
+ {
+ return db.Friends.FirstOrDefaultAsync(x => x.Owner.Id == ownerId && x.Target.Id == targetId);
+ }
+ public async Task> FindByTargetAsync(Guid targetId)
+ {
+ return await db.Friends.Where(x => x.Target.Id == targetId).ToListAsync();
+ }
+ public async Task> FindByOwnerAsync(Guid ownerId)
+ {
+ return await db.Friends.Where(x => x.Owner.Id == ownerId).ToListAsync();
+ }
+ }
+}
diff --git a/ContactService.Infrastructure/FriendRequestReposity.cs b/ContactService.Infrastructure/FriendRequestReposity.cs
new file mode 100644
index 0000000..989670d
--- /dev/null
+++ b/ContactService.Infrastructure/FriendRequestReposity.cs
@@ -0,0 +1,37 @@
+using ContactService.Domain;
+using ContactService.Domain.Entities;
+using Microsoft.EntityFrameworkCore;
+
+namespace ContactService.Infrastructure
+{
+ public class FriendRequestReposity : IFriendRequestReposity
+ {
+ private readonly ContactDbContext db;
+
+ public FriendRequestReposity(ContactDbContext db)
+ {
+ this.db = db;
+ }
+
+ public async Task CreateAsync(FriendRequest friendRequest)
+ {
+ db.Add(friendRequest);
+ return true;
+ }
+
+ public Task FindByIdAsync(Guid id)
+ {
+ return db.FriendRequests.FirstOrDefaultAsync(x => x.Id == id);
+ }
+
+ public async Task> FindByOwnerIdAsync(Guid ownerId)
+ {
+ return await db.FriendRequests.Where(x => x.OwnerId == ownerId).ToListAsync();
+ }
+
+ public async Task> FindByTargetIdAsync(Guid targetId)
+ {
+ return await db.FriendRequests.Where(x => x.TargetId == targetId).ToListAsync();
+ }
+ }
+}
diff --git a/ContactService.Infrastructure/Migrations/20260413114340_InitDb.Designer.cs b/ContactService.Infrastructure/Migrations/20260413114340_InitDb.Designer.cs
new file mode 100644
index 0000000..25d48dc
--- /dev/null
+++ b/ContactService.Infrastructure/Migrations/20260413114340_InitDb.Designer.cs
@@ -0,0 +1,161 @@
+//
+using System;
+using ContactService.Infrastructure;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace ContactService.Infrastructure.Migrations
+{
+ [DbContext(typeof(ContactDbContext))]
+ [Migration("20260413114340_InitDb")]
+ partial class InitDb
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.0")
+ .HasAnnotation("Relational:MaxIdentifierLength", 64);
+
+ MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
+
+ modelBuilder.Entity("ContactService.Domain.Entities.Friend", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("char(36)");
+
+ b.Property("CreationTime")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Deletion")
+ .HasColumnType("datetime(6)");
+
+ b.Property("IsDeleted")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("ModificationTime")
+ .HasColumnType("datetime(6)");
+
+ b.Property("RemarkName")
+ .HasColumnType("longtext");
+
+ b.Property("Status")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.ToTable("friends", (string)null);
+ });
+
+ modelBuilder.Entity("ContactService.Domain.Entities.FriendRequest", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("char(36)");
+
+ b.Property("CreationTime")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Deletion")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("IsDeleted")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("ModificationTime")
+ .HasColumnType("datetime(6)");
+
+ b.Property("OwnerId")
+ .HasColumnType("char(36)");
+
+ b.Property("RemarkName")
+ .HasColumnType("longtext");
+
+ b.Property("State")
+ .HasColumnType("int");
+
+ b.Property("TargetId")
+ .HasColumnType("char(36)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OwnerId", "TargetId");
+
+ b.ToTable("friend_requests", (string)null);
+ });
+
+ modelBuilder.Entity("ContactService.Domain.Entities.Friend", b =>
+ {
+ b.OwnsOne("ContactService.Domain.ValueObjects.UserProfile", "Owner", b1 =>
+ {
+ b1.Property("FriendId")
+ .HasColumnType("char(36)");
+
+ b1.Property("Avatar")
+ .HasColumnType("longtext")
+ .HasColumnName("OwnerAvatarUrl");
+
+ b1.Property("Id")
+ .HasColumnType("char(36)")
+ .HasColumnName("OwnerId");
+
+ b1.Property("NickName")
+ .IsRequired()
+ .HasColumnType("longtext")
+ .HasColumnName("OwnerNickName");
+
+ b1.HasKey("FriendId");
+
+ b1.ToTable("friends");
+
+ b1.WithOwner()
+ .HasForeignKey("FriendId");
+ });
+
+ b.OwnsOne("ContactService.Domain.ValueObjects.UserProfile", "Target", b1 =>
+ {
+ b1.Property("FriendId")
+ .HasColumnType("char(36)");
+
+ b1.Property("Avatar")
+ .HasColumnType("longtext")
+ .HasColumnName("TargetAvatarUrl");
+
+ b1.Property("Id")
+ .HasColumnType("char(36)")
+ .HasColumnName("TargetId");
+
+ b1.Property("NickName")
+ .IsRequired()
+ .HasColumnType("longtext")
+ .HasColumnName("TargetNickName");
+
+ b1.HasKey("FriendId");
+
+ b1.ToTable("friends");
+
+ b1.WithOwner()
+ .HasForeignKey("FriendId");
+ });
+
+ b.Navigation("Owner")
+ .IsRequired();
+
+ b.Navigation("Target")
+ .IsRequired();
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/ContactService.Infrastructure/Migrations/20260413114340_InitDb.cs b/ContactService.Infrastructure/Migrations/20260413114340_InitDb.cs
new file mode 100644
index 0000000..2ed78db
--- /dev/null
+++ b/ContactService.Infrastructure/Migrations/20260413114340_InitDb.cs
@@ -0,0 +1,84 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace ContactService.Infrastructure.Migrations
+{
+ ///
+ public partial class InitDb : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AlterDatabase()
+ .Annotation("MySql:Charset", "utf8mb4");
+
+ migrationBuilder.CreateTable(
+ name: "friend_requests",
+ columns: table => new
+ {
+ Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
+ OwnerId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
+ TargetId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
+ Description = table.Column(type: "longtext", nullable: false)
+ .Annotation("MySql:Charset", "utf8mb4"),
+ State = table.Column(type: "int", nullable: false),
+ RemarkName = table.Column(type: "longtext", nullable: true)
+ .Annotation("MySql:Charset", "utf8mb4"),
+ IsDeleted = table.Column(type: "tinyint(1)", nullable: false),
+ CreationTime = table.Column(type: "datetime(6)", nullable: false),
+ Deletion = table.Column(type: "datetime(6)", nullable: true),
+ ModificationTime = table.Column(type: "datetime(6)", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_friend_requests", x => x.Id);
+ })
+ .Annotation("MySql:Charset", "utf8mb4");
+
+ migrationBuilder.CreateTable(
+ name: "friends",
+ columns: table => new
+ {
+ Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
+ OwnerId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
+ OwnerNickName = table.Column(type: "longtext", nullable: false)
+ .Annotation("MySql:Charset", "utf8mb4"),
+ OwnerAvatarUrl = table.Column(type: "longtext", nullable: true)
+ .Annotation("MySql:Charset", "utf8mb4"),
+ TargetId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
+ TargetNickName = table.Column(type: "longtext", nullable: false)
+ .Annotation("MySql:Charset", "utf8mb4"),
+ TargetAvatarUrl = table.Column(type: "longtext", nullable: true)
+ .Annotation("MySql:Charset", "utf8mb4"),
+ RemarkName = table.Column(type: "longtext", nullable: true)
+ .Annotation("MySql:Charset", "utf8mb4"),
+ Status = table.Column(type: "int", nullable: false),
+ IsDeleted = table.Column(type: "tinyint(1)", nullable: false),
+ CreationTime = table.Column(type: "datetime(6)", nullable: false),
+ Deletion = table.Column(type: "datetime(6)", nullable: true),
+ ModificationTime = table.Column(type: "datetime(6)", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_friends", x => x.Id);
+ })
+ .Annotation("MySql:Charset", "utf8mb4");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_friend_requests_OwnerId_TargetId",
+ table: "friend_requests",
+ columns: new[] { "OwnerId", "TargetId" });
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "friend_requests");
+
+ migrationBuilder.DropTable(
+ name: "friends");
+ }
+ }
+}
diff --git a/ContactService.Infrastructure/Migrations/ContactDbContextModelSnapshot.cs b/ContactService.Infrastructure/Migrations/ContactDbContextModelSnapshot.cs
new file mode 100644
index 0000000..ce604d5
--- /dev/null
+++ b/ContactService.Infrastructure/Migrations/ContactDbContextModelSnapshot.cs
@@ -0,0 +1,158 @@
+//
+using System;
+using ContactService.Infrastructure;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace ContactService.Infrastructure.Migrations
+{
+ [DbContext(typeof(ContactDbContext))]
+ partial class ContactDbContextModelSnapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.0")
+ .HasAnnotation("Relational:MaxIdentifierLength", 64);
+
+ MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
+
+ modelBuilder.Entity("ContactService.Domain.Entities.Friend", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("char(36)");
+
+ b.Property("CreationTime")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Deletion")
+ .HasColumnType("datetime(6)");
+
+ b.Property("IsDeleted")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("ModificationTime")
+ .HasColumnType("datetime(6)");
+
+ b.Property("RemarkName")
+ .HasColumnType("longtext");
+
+ b.Property("Status")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.ToTable("friends", (string)null);
+ });
+
+ modelBuilder.Entity("ContactService.Domain.Entities.FriendRequest", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("char(36)");
+
+ b.Property("CreationTime")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Deletion")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("IsDeleted")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("ModificationTime")
+ .HasColumnType("datetime(6)");
+
+ b.Property("OwnerId")
+ .HasColumnType("char(36)");
+
+ b.Property("RemarkName")
+ .HasColumnType("longtext");
+
+ b.Property("State")
+ .HasColumnType("int");
+
+ b.Property("TargetId")
+ .HasColumnType("char(36)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OwnerId", "TargetId");
+
+ b.ToTable("friend_requests", (string)null);
+ });
+
+ modelBuilder.Entity("ContactService.Domain.Entities.Friend", b =>
+ {
+ b.OwnsOne("ContactService.Domain.ValueObjects.UserProfile", "Owner", b1 =>
+ {
+ b1.Property("FriendId")
+ .HasColumnType("char(36)");
+
+ b1.Property("Avatar")
+ .HasColumnType("longtext")
+ .HasColumnName("OwnerAvatarUrl");
+
+ b1.Property("Id")
+ .HasColumnType("char(36)")
+ .HasColumnName("OwnerId");
+
+ b1.Property("NickName")
+ .IsRequired()
+ .HasColumnType("longtext")
+ .HasColumnName("OwnerNickName");
+
+ b1.HasKey("FriendId");
+
+ b1.ToTable("friends");
+
+ b1.WithOwner()
+ .HasForeignKey("FriendId");
+ });
+
+ b.OwnsOne("ContactService.Domain.ValueObjects.UserProfile", "Target", b1 =>
+ {
+ b1.Property("FriendId")
+ .HasColumnType("char(36)");
+
+ b1.Property("Avatar")
+ .HasColumnType("longtext")
+ .HasColumnName("TargetAvatarUrl");
+
+ b1.Property("Id")
+ .HasColumnType("char(36)")
+ .HasColumnName("TargetId");
+
+ b1.Property("NickName")
+ .IsRequired()
+ .HasColumnType("longtext")
+ .HasColumnName("TargetNickName");
+
+ b1.HasKey("FriendId");
+
+ b1.ToTable("friends");
+
+ b1.WithOwner()
+ .HasForeignKey("FriendId");
+ });
+
+ b.Navigation("Owner")
+ .IsRequired();
+
+ b.Navigation("Target")
+ .IsRequired();
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/ContactService.Infrastructure/ModuleInit.cs b/ContactService.Infrastructure/ModuleInit.cs
new file mode 100644
index 0000000..e38598e
--- /dev/null
+++ b/ContactService.Infrastructure/ModuleInit.cs
@@ -0,0 +1,17 @@
+using ContactService.Domain;
+using IM.Commons;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace ContactService.Infrastructure
+{
+ public class ModuleInit : IModuleInitializer
+ {
+ public void Initialize(IServiceCollection services)
+ {
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ }
+ }
+}
diff --git a/ContactService.WebApi/Application/Dtos/FriendRequestResponse.cs b/ContactService.WebApi/Application/Dtos/FriendRequestResponse.cs
new file mode 100644
index 0000000..196ff67
--- /dev/null
+++ b/ContactService.WebApi/Application/Dtos/FriendRequestResponse.cs
@@ -0,0 +1,38 @@
+using ContactService.Domain;
+
+namespace ContactService.WebApi.Application.Dtos
+{
+ public class FriendRequestResponse
+ {
+ public Guid Id { get; private set; }
+ ///
+ /// 申请人
+ ///
+ public Guid OwnerId { get; private set; }
+
+ ///
+ /// 被申请人
+ ///
+ public Guid TargetId { get; private set; }
+
+
+ ///
+ /// 申请附言
+ ///
+ public string Description { get; private set; }
+
+ ///
+ /// 申请状态(0:待通过,1:拒绝,2:同意,3:拉黑)
+ ///
+ public FriendRequestStatus State { get; private set; }
+
+ ///
+ /// 备注
+ ///
+ public string? RemarkName { get; private set; }
+ public DateTimeOffset CreationTime { get; private set; }
+ public DateTimeOffset? Deletion { get; private set; }
+
+ public DateTimeOffset? ModificationTime { get; private set; }
+ }
+}
diff --git a/ContactService.WebApi/Application/Dtos/FriendResonse.cs b/ContactService.WebApi/Application/Dtos/FriendResonse.cs
new file mode 100644
index 0000000..6c2a62f
--- /dev/null
+++ b/ContactService.WebApi/Application/Dtos/FriendResonse.cs
@@ -0,0 +1,20 @@
+using ContactService.Domain;
+
+namespace ContactService.WebApi.Application.Dtos
+{
+ public class FriendResonse
+ {
+ public Guid Id { get; private set; }
+ public Guid TargetId { get; private set; }
+ public string? Avatar { get; private set; }
+ public string NickName { get; private set; }
+ ///
+ /// 好友备注名
+ ///
+ public string? RemarkName { get; private set; }
+
+ public DateTime CreateTime { get; private set; }
+ public DateTime? UpdateTime { get; private set; }
+ public FriendStatus Status { get; private set; }
+ }
+}
diff --git a/ContactService.WebApi/Application/Dtos/UserInfoDto.cs b/ContactService.WebApi/Application/Dtos/UserInfoDto.cs
new file mode 100644
index 0000000..30e7972
--- /dev/null
+++ b/ContactService.WebApi/Application/Dtos/UserInfoDto.cs
@@ -0,0 +1,16 @@
+namespace ContactService.WebApi.Application.Dtos
+{
+ public class UserInfoDto
+ {
+ public Guid Id { get; set; }
+ public string UserName { get; set; }
+ public string NickName { get; set; }
+ public string? Email { get; set; }
+ public string? Phone { get; set; }
+ public string Region { get; set; }
+ public string Description { get; set; }
+ public string? Avatar { get; set; }
+ public DateTimeOffset CreationTime { get; set; }
+ public DateTimeOffset? Deletion { get; set; }
+ }
+}
diff --git a/ContactService.WebApi/Application/EventHandler/FriendAddedHandler.cs b/ContactService.WebApi/Application/EventHandler/FriendAddedHandler.cs
new file mode 100644
index 0000000..699f841
--- /dev/null
+++ b/ContactService.WebApi/Application/EventHandler/FriendAddedHandler.cs
@@ -0,0 +1,32 @@
+using ContactService.Domain.Events;
+using IM.Commons.IntegrationEvents;
+using MassTransit;
+using MediatR;
+
+namespace ContactService.WebApi.Application.EventHandler
+{
+ public class FriendAddedHandler : INotificationHandler
+ {
+ private readonly IPublishEndpoint endpoint;
+
+ public FriendAddedHandler(IPublishEndpoint endpoint)
+ {
+ this.endpoint = endpoint;
+ }
+
+ public async Task Handle(FriendAddedDomainEvent notification, CancellationToken cancellationToken)
+ {
+ await endpoint.Publish(new FriendAddedEvent
+ {
+ OwnerAvatar = notification.Friend.Owner.Avatar,
+ OwnerId = notification.Friend.Owner.Id,
+ OwnerNickName = notification.Friend.Owner.NickName,
+ TargetAvatar = notification.Friend.Target.Avatar,
+ TargetId = notification.Friend.Target.Id,
+ TargetNickName = notification.Friend.Target.NickName,
+ RemarkName = notification.Friend.RemarkName,
+ Status = notification.Friend.Status.ToString(),
+ }, cancellationToken);
+ }
+ }
+}
diff --git a/ContactService.WebApi/Application/EventHandler/FriendRequestStatusUpdateHandler.cs b/ContactService.WebApi/Application/EventHandler/FriendRequestStatusUpdateHandler.cs
new file mode 100644
index 0000000..12a5cd8
--- /dev/null
+++ b/ContactService.WebApi/Application/EventHandler/FriendRequestStatusUpdateHandler.cs
@@ -0,0 +1,58 @@
+using ContactService.Domain;
+using ContactService.Domain.Events;
+using ContactService.Domain.ValueObjects;
+using ContactService.Infrastructure;
+using ContactService.WebApi.Application.IntegrationServices;
+using IM.Commons.IntegrationEvents;
+using MassTransit;
+using MediatR;
+
+namespace ContactService.WebApi.Application.EventHandler
+{
+ public class FriendRequestStatusUpdateHandler : INotificationHandler
+ {
+ private readonly FriendDomainService friendService;
+ private readonly ContactDbContext contactDb;
+ private readonly IPublishEndpoint endpoint;
+ private readonly IIdentityIntegrationService idService;
+
+ public FriendRequestStatusUpdateHandler(FriendDomainService friendService, ContactDbContext contactDb, IPublishEndpoint endpoint, IIdentityIntegrationService idService)
+ {
+ this.friendService = friendService;
+ this.contactDb = contactDb;
+ this.endpoint = endpoint;
+ this.idService = idService;
+ }
+
+ public async Task Handle(FriendRequestStateUpdateDomainEvent notification, CancellationToken cancellationToken)
+ {
+ var @event = notification.Request;
+ if (@event.State == FriendRequestStatus.Passed)
+ {
+ var ownerInfo = await idService.FindUserByIdAsync(@event.OwnerId);
+ var targetInfo = await idService.FindUserByIdAsync(@event.TargetId);
+
+ if (!ownerInfo.Succeeded || !targetInfo.Succeeded)
+ {
+ return;
+ }
+
+ var ownerProfile = new UserProfile(ownerInfo.Data.Id, ownerInfo.Data.NickName, ownerInfo.Data.Avatar);
+ var targetProfile = new UserProfile(targetInfo.Data.Id, targetInfo.Data.NickName, targetInfo.Data.Avatar);
+
+ await friendService.CreateAsync(ownerProfile, targetProfile, @event.RemarkName);
+ await friendService.CreateAsync(targetProfile, ownerProfile, notification.AcceptRemarkName);
+ await contactDb.SaveChangesAsync(cancellationToken);
+ }
+
+ await endpoint.Publish(new FriendRequestStateUpdateEvent
+ {
+ CorrelationId = @event.TargetId,
+ Description = @event.Description,
+ OwnerId = @event.OwnerId,
+ RemarkName = @event.RemarkName,
+ State = @event.State.ToString()
+ });
+ }
+ }
+}
diff --git a/ContactService.WebApi/Application/EventHandler/UserProfileUpdateHandler.cs b/ContactService.WebApi/Application/EventHandler/UserProfileUpdateHandler.cs
new file mode 100644
index 0000000..2da73a4
--- /dev/null
+++ b/ContactService.WebApi/Application/EventHandler/UserProfileUpdateHandler.cs
@@ -0,0 +1,35 @@
+using ContactService.Domain;
+using ContactService.Infrastructure;
+using IM.Commons.IntegrationEvents;
+using MassTransit;
+
+namespace ContactService.WebApi.Application.EventHandler
+{
+ public class UserProfileUpdateHandler : IConsumer
+ {
+ private readonly ContactDbContext contactDb;
+ private readonly IFriendReposity reposity;
+
+ public UserProfileUpdateHandler(ContactDbContext contactDb, IFriendReposity reposity)
+ {
+ this.contactDb = contactDb;
+ this.reposity = reposity;
+ }
+
+ public async Task Consume(ConsumeContext context)
+ {
+ var @event = context.Message;
+ var friends = await reposity.FindByTargetAsync(@event.UserId);
+
+ foreach (var friend in friends)
+ {
+ friend.UpdateUserInfo(
+ new Domain.ValueObjects.UserProfile(
+ @event.UserId, @event.Avatar, @event.NickName));
+ }
+
+ await contactDb.SaveChangesAsync();
+
+ }
+ }
+}
diff --git a/ContactService.WebApi/Application/Friend/FriendMapperConfig.cs b/ContactService.WebApi/Application/Friend/FriendMapperConfig.cs
new file mode 100644
index 0000000..ec1105c
--- /dev/null
+++ b/ContactService.WebApi/Application/Friend/FriendMapperConfig.cs
@@ -0,0 +1,23 @@
+using AutoMapper;
+using ContactService.WebApi.Application.Dtos;
+using Google.Protobuf.WellKnownTypes;
+
+namespace ContactService.WebApi.Application.Friend
+{
+ public class FriendMapperConfig : Profile
+ {
+ public FriendMapperConfig()
+ {
+ CreateMap()
+ .ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id))
+ .ForMember(dest => dest.Avatar, opt => opt.MapFrom(src => src.Target.Avatar))
+ .ForMember(dest => dest.Status, opt => opt.MapFrom(src => src.Status))
+ .ForMember(dest => dest.UpdateTime, opt => opt.MapFrom(src => src.ModificationTime.Value.DateTime))
+ .ForMember(dest => dest.CreateTime, opt => opt.MapFrom(src => src.CreationTime.DateTime))
+ .ForMember(dest => dest.NickName, opt => opt.MapFrom(src => src.Target.NickName))
+ .ForMember(dest => dest.RemarkName, opt => opt.MapFrom(src => src.RemarkName))
+ .ForMember(dest => dest.TargetId, opt => opt.MapFrom(src => src.Target.Id))
+ ;
+ }
+ }
+}
diff --git a/ContactService.WebApi/Application/Friend/FriendService.cs b/ContactService.WebApi/Application/Friend/FriendService.cs
new file mode 100644
index 0000000..7647e10
--- /dev/null
+++ b/ContactService.WebApi/Application/Friend/FriendService.cs
@@ -0,0 +1,73 @@
+using AutoMapper;
+using ContactService.Domain;
+using ContactService.WebApi.Application.Dtos;
+using ContactService.WebApi.Application.IntegrationServices;
+using IM.Commons;
+
+namespace ContactService.WebApi.Application.Friend
+{
+ public class FriendService
+ {
+ private readonly IFriendReposity reposity;
+ private readonly FriendDomainService service;
+ private readonly IIdentityIntegrationService idService;
+ private readonly IMapper mapper;
+
+ public FriendService(IFriendReposity reposity, FriendDomainService service
+ , IIdentityIntegrationService idService, IMapper mapper
+ )
+ {
+ this.reposity = reposity;
+ this.service = service;
+ this.idService = idService;
+ this.mapper = mapper;
+ }
+
+ public async Task>> GetFriendsByOwnerIdAsync(Guid ownerId)
+ {
+ IEnumerable friend = await reposity.FindByOwnerAsync(ownerId);
+ return Result>.Success(mapper.Map>(friend.ToList()));
+ }
+
+ public async Task> DeleteFriendAsync(Guid userId, Guid friendId)
+ {
+ var friend = await reposity.FindByIdAsync(friendId);
+ if (friend is null)
+ {
+ return Result