后端:

新增好友请求事件和好友已添加事件
This commit is contained in:
西街长安 2026-01-31 14:43:20 +08:00 committed by nanxun
commit 136199290b
40 changed files with 550 additions and 194 deletions

View File

@ -1,25 +1,30 @@
using Xunit;
using Microsoft.EntityFrameworkCore;
using Moq;
using AutoMapper;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Threading.Tasks;
using System;
using IM_API.Services;
using IM_API.Models;
using AutoMapper;
using IM_API.Domain.Events;
using IM_API.Dtos;
using IM_API.Exceptions;
using IM_API.Tools;
using IM_API.Models;
using IM_API.Services;
using MassTransit; // 必须引入
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Moq;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
public class FriendServiceTests
{
private readonly Mock<IPublishEndpoint> _mockEndpoint = new();
private readonly Mock<ILogger<FriendService>> _mockLogger = new();
#region
private ImContext CreateDbContext()
{
var options = new DbContextOptionsBuilder<ImContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.UseInMemoryDatabase(Guid.NewGuid().ToString()) // 确保每个测试数据库隔离
.Options;
return new ImContext(options);
}
@ -27,132 +32,115 @@ public class FriendServiceTests
{
var config = new MapperConfiguration(cfg =>
{
// 补充你业务中实际需要的映射规则
cfg.CreateMap<Friend, FriendInfoDto>();
cfg.CreateMap<FriendRequestDto, FriendRequest>();
cfg.CreateMap<HandleFriendRequestDto, FriendRequest>();
cfg.CreateMap<FriendRequest, Friend>()
.ForMember(dest => dest.UserId, opt => opt.MapFrom(src => src.ResponseUser))
.ForMember(dest => dest.FriendId, opt => opt.MapFrom(src => src.RequestUser))
.ForMember(dest => dest.RemarkName, opt => opt.MapFrom(src => "AutoAdded"));
.ForMember(d => d.UserId, o => o.MapFrom(s => s.ResponseUser))
.ForMember(d => d.FriendId, o => o.MapFrom(s => s.RequestUser));
});
return config.CreateMapper();
}
private FriendService CreateService(ImContext context)
{
var logger = new Mock<ILogger<FriendService>>();
return new FriendService(context, logger.Object, CreateMapper());
// 注入 Mock 对象和真实的 Mapper/Context
return new FriendService(context, _mockLogger.Object, CreateMapper(), _mockEndpoint.Object);
}
#endregion
// --------------------------- 测试 BlockFriendAsync ---------------------------
[Fact]
public async Task BlockFriendAsync_Should_Set_Status_To_Blocked()
public async Task SendFriendRequestAsync_Success_ShouldSaveAndPublish()
{
// Arrange
var context = CreateDbContext();
context.Friends.Add(new Friend
{
Id = 1,
UserId = 10,
FriendId = 20,
StatusEnum = FriendStatus.Added,
RemarkName = "test remark"
});
context.Users.AddRange(
new User { Id = 1, Username = "Sender", Password = "..." },
new User { Id = 2, Username = "Receiver", Password = "..." }
);
await context.SaveChangesAsync();
var service = CreateService(context);
var dto = new FriendRequestDto { ToUserId = 2, Description = "Hello" };
var result = await service.BlockeFriendAsync(1);
Assert.True(result);
Assert.Equal(FriendStatus.Blocked, context.Friends.Find(1).StatusEnum);
}
[Fact]
public async Task BlockFriendAsync_Should_Throw_When_NotFound()
{
var service = CreateService(CreateDbContext());
await Assert.ThrowsAsync<BaseException>(() => service.BlockeFriendAsync(99));
}
// --------------------------- 删除好友关系 ---------------------------
[Fact]
public async Task DeleteFriendAsync_Should_Remove_Friend()
{
var context = CreateDbContext();
context.Friends.Add(new Friend
{
Id = 2,
UserId = 1,
FriendId = 3,
RemarkName = "remark",
StatusEnum = FriendStatus.Added
});
await context.SaveChangesAsync();
var service = CreateService(context);
var result = await service.DeleteFriendAsync(2);
Assert.True(result);
Assert.Empty(context.Friends);
}
// --------------------------- 获取好友列表 ---------------------------
[Fact]
public async Task GetFriendListAsync_Should_Return_Only_Added_Friends()
{
var context = CreateDbContext();
context.Friends.AddRange(new List<Friend>
{
new Friend{ UserId = 1, FriendId = 2, RemarkName ="a1", StatusEnum = FriendStatus.Added },
new Friend{ UserId = 1, FriendId = 3, RemarkName ="a2", StatusEnum = FriendStatus.Blocked }
});
await context.SaveChangesAsync();
var service = CreateService(context);
var result = await service.GetFriendListAsync(1, 1, 10, false);
Assert.Single(result);
}
// --------------------------- 发起好友请求 ---------------------------
[Fact]
public async Task SendFriendRequestAsync_Should_Succeed()
{
var context = CreateDbContext();
context.Users.Add(new User { Id = 10, Username = "A", Password = "123" });
context.Users.Add(new User { Id = 20, Username = "B", Password = "123" });
await context.SaveChangesAsync();
var service = CreateService(context);
var result = await service.SendFriendRequestAsync(new FriendRequestDto
{
FromUserId = 10,
ToUserId = 20
});
// Act
var result = await service.SendFriendRequestAsync(dto);
// Assert
Assert.True(result);
Assert.Single(context.FriendRequests);
Assert.Single(context.Friends);
// 验证事件是否发布到了 MQ
_mockEndpoint.Verify(x => x.Publish(
It.Is<RequestFriendEvent>(e => e.FromUserId == 1 && e.ToUserId == 2),
It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task SendFriendRequestAsync_Should_Throw_When_User_NotFound()
public async Task SendFriendRequestAsync_UserNotFound_ShouldThrow()
{
// Arrange
var context = CreateDbContext();
context.Users.Add(new User { Id = 10, Username = "A", Password = "123" });
await context.SaveChangesAsync();
var service = CreateService(context);
var dto = new FriendRequestDto { ToUserId = 99 }; // 不存在的用户
// Act & Assert
await Assert.ThrowsAsync<BaseException>(() => service.SendFriendRequestAsync(dto));
}
[Fact]
public async Task SendFriendRequestAsync_AlreadyExists_ShouldThrow()
{
// Arrange
var context = CreateDbContext();
context.Users.Add(new User { Id = 2 });
context.FriendRequests.Add(new FriendRequest
{
RequestUser = 1,
ResponseUser = 2,
State = (sbyte)FriendRequestState.Pending
});
await context.SaveChangesAsync();
var service = CreateService(context);
await Assert.ThrowsAsync<BaseException>(() => service.SendFriendRequestAsync(new FriendRequestDto
{
FromUserId = 10,
ToUserId = 99
}));
// Act & Assert
await Assert.ThrowsAsync<BaseException>(() => service.SendFriendRequestAsync(new FriendRequestDto { ToUserId = 2 }));
}
}
[Fact]
public async Task BlockFriendAsync_ValidId_ShouldUpdateStatus()
{
// Arrange
var context = CreateDbContext();
var friend = new Friend { Id = 50, UserId = 1, FriendId = 2, StatusEnum = FriendStatus.Added };
context.Friends.Add(friend);
await context.SaveChangesAsync();
var service = CreateService(context);
// Act
await service.BlockeFriendAsync(50);
// Assert
var updated = await context.Friends.FindAsync(50);
Assert.Equal(FriendStatus.Blocked, updated.StatusEnum);
}
[Fact]
public async Task GetFriendListAsync_ShouldFilterByStatus()
{
// Arrange
var context = CreateDbContext();
context.Friends.AddRange(
new Friend { UserId = 1, FriendId = 2, StatusEnum = FriendStatus.Added },
new Friend { UserId = 1, FriendId = 3, StatusEnum = FriendStatus.Blocked }
);
await context.SaveChangesAsync();
var service = CreateService(context);
// Act
var result = await service.GetFriendListAsync(1, 1, 10, false);
// Assert
Assert.Single(result); // 只应该拿到 Added 状态的
}
}

View File

@ -55,6 +55,42 @@
}
},
"coverlet.collector/6.0.0": {},
"MassTransit/8.5.5": {
"dependencies": {
"MassTransit.Abstractions": "8.5.5",
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
"Microsoft.Extensions.Diagnostics.HealthChecks": "8.0.0",
"Microsoft.Extensions.Hosting.Abstractions": "8.0.1",
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
"Microsoft.Extensions.Options": "8.0.2"
},
"runtime": {
"lib/net8.0/MassTransit.dll": {
"assemblyVersion": "8.5.5.0",
"fileVersion": "8.5.5.0"
}
}
},
"MassTransit.Abstractions/8.5.5": {
"runtime": {
"lib/net8.0/MassTransit.Abstractions.dll": {
"assemblyVersion": "8.5.5.0",
"fileVersion": "8.5.5.0"
}
}
},
"MassTransit.RabbitMQ/8.5.5": {
"dependencies": {
"MassTransit": "8.5.5",
"RabbitMQ.Client": "7.1.2"
},
"runtime": {
"lib/net8.0/MassTransit.RabbitMqTransport.dll": {
"assemblyVersion": "8.5.5.0",
"fileVersion": "8.5.5.0"
}
}
},
"Microsoft.AspNetCore.Authentication.Abstractions/2.3.0": {
"dependencies": {
"Microsoft.AspNetCore.Http.Abstractions": "2.3.0",
@ -326,6 +362,15 @@
}
}
},
"Microsoft.Extensions.Diagnostics.HealthChecks/8.0.0": {
"dependencies": {
"Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "8.0.0",
"Microsoft.Extensions.Hosting.Abstractions": "8.0.1",
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
"Microsoft.Extensions.Options": "8.0.2"
}
},
"Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/8.0.0": {},
"Microsoft.Extensions.FileProviders.Abstractions/8.0.0": {
"dependencies": {
"Microsoft.Extensions.Primitives": "8.0.0"
@ -843,6 +888,18 @@
}
}
},
"RabbitMQ.Client/7.1.2": {
"dependencies": {
"System.IO.Pipelines": "8.0.0",
"System.Threading.RateLimiting": "8.0.0"
},
"runtime": {
"lib/net8.0/RabbitMQ.Client.dll": {
"assemblyVersion": "7.0.0.0",
"fileVersion": "7.1.2.0"
}
}
},
"runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.0": {},
"runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.0": {},
"runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.0": {},
@ -1455,6 +1512,7 @@
}
},
"System.Threading.Channels/8.0.0": {},
"System.Threading.RateLimiting/8.0.0": {},
"System.Threading.Tasks/4.3.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0",
@ -1573,6 +1631,7 @@
"dependencies": {
"AutoMapper": "12.0.1",
"AutoMapper.Extensions.Microsoft.DependencyInjection": "12.0.0",
"MassTransit.RabbitMQ": "8.5.5",
"Microsoft.AspNetCore.Authentication.JwtBearer": "8.0.21",
"Microsoft.AspNetCore.SignalR": "1.2.0",
"Microsoft.VisualStudio.Azure.Containers.Tools.Targets": "1.22.1",
@ -1625,6 +1684,27 @@
"path": "coverlet.collector/6.0.0",
"hashPath": "coverlet.collector.6.0.0.nupkg.sha512"
},
"MassTransit/8.5.5": {
"type": "package",
"serviceable": true,
"sha512": "sha512-bSg8k5q+rP1s+dIGXLLbctqDGdIkfDjdxwNWtCUH7xNCN9ZuM7mqSPQPIFgaYIi34e81m4FqAqo4CAHuWPkhRA==",
"path": "masstransit/8.5.5",
"hashPath": "masstransit.8.5.5.nupkg.sha512"
},
"MassTransit.Abstractions/8.5.5": {
"type": "package",
"serviceable": true,
"sha512": "sha512-0mn2Ay17dD6z5tgSLjbVRlldSbL9iowzFEfVgVfBXVG5ttz9dSWeR4TrdD6pqH93GWXp4CvSmF8i1HqxLX7DZw==",
"path": "masstransit.abstractions/8.5.5",
"hashPath": "masstransit.abstractions.8.5.5.nupkg.sha512"
},
"MassTransit.RabbitMQ/8.5.5": {
"type": "package",
"serviceable": true,
"sha512": "sha512-UxWn4o90YVMF9PBkJeoskOFPneh6YtnI1fLJHtvZiSAG0eoiRrWPGa+6FQCvjkQ/ljCKfjzok2eGZc/vmNZ01A==",
"path": "masstransit.rabbitmq/8.5.5",
"hashPath": "masstransit.rabbitmq.8.5.5.nupkg.sha512"
},
"Microsoft.AspNetCore.Authentication.Abstractions/2.3.0": {
"type": "package",
"serviceable": true,
@ -1870,6 +1950,20 @@
"path": "microsoft.extensions.diagnostics.abstractions/8.0.1",
"hashPath": "microsoft.extensions.diagnostics.abstractions.8.0.1.nupkg.sha512"
},
"Microsoft.Extensions.Diagnostics.HealthChecks/8.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-P9SoBuVZhJPpALZmSq72aQEb9ryP67EdquaCZGXGrrcASTNHYdrUhnpgSwIipgM5oVC+dKpRXg5zxobmF9xr5g==",
"path": "microsoft.extensions.diagnostics.healthchecks/8.0.0",
"hashPath": "microsoft.extensions.diagnostics.healthchecks.8.0.0.nupkg.sha512"
},
"Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/8.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-AT2qqos3IgI09ok36Qag9T8bb6kHJ3uT9Q5ki6CySybFsK6/9JbvQAgAHf1pVEjST0/N4JaFaCbm40R5edffwg==",
"path": "microsoft.extensions.diagnostics.healthchecks.abstractions/8.0.0",
"hashPath": "microsoft.extensions.diagnostics.healthchecks.abstractions.8.0.0.nupkg.sha512"
},
"Microsoft.Extensions.FileProviders.Abstractions/8.0.0": {
"type": "package",
"serviceable": true,
@ -2073,6 +2167,13 @@
"path": "pomelo.entityframeworkcore.mysql/8.0.3",
"hashPath": "pomelo.entityframeworkcore.mysql.8.0.3.nupkg.sha512"
},
"RabbitMQ.Client/7.1.2": {
"type": "package",
"serviceable": true,
"sha512": "sha512-y3c6ulgULScWthHw5PLM1ShHRLhxg0vCtzX/hh61gRgNecL3ZC3WoBW2HYHoXOVRqTl99Br9E7CZEytGZEsCyQ==",
"path": "rabbitmq.client/7.1.2",
"hashPath": "rabbitmq.client.7.1.2.nupkg.sha512"
},
"runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.0": {
"type": "package",
"serviceable": true,
@ -2605,6 +2706,13 @@
"path": "system.threading.channels/8.0.0",
"hashPath": "system.threading.channels.8.0.0.nupkg.sha512"
},
"System.Threading.RateLimiting/8.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-7mu9v0QDv66ar3DpGSZHg9NuNcxDaaAcnMULuZlaTpP9+hwXhrxNGsF5GmLkSHxFdb5bBc1TzeujsRgTrPWi+Q==",
"path": "system.threading.ratelimiting/8.0.0",
"hashPath": "system.threading.ratelimiting.8.0.0.nupkg.sha512"
},
"System.Threading.Tasks/4.3.0": {
"type": "package",
"serviceable": true,

View File

@ -10,6 +10,7 @@
"dependencies": {
"AutoMapper": "12.0.1",
"AutoMapper.Extensions.Microsoft.DependencyInjection": "12.0.0",
"MassTransit.RabbitMQ": "8.5.5",
"Microsoft.AspNetCore.Authentication.JwtBearer": "8.0.21",
"Microsoft.AspNetCore.SignalR": "1.2.0",
"Microsoft.EntityFrameworkCore.Design": "8.0.21",
@ -56,6 +57,42 @@
}
}
},
"MassTransit/8.5.5": {
"dependencies": {
"MassTransit.Abstractions": "8.5.5",
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2",
"Microsoft.Extensions.Diagnostics.HealthChecks": "8.0.0",
"Microsoft.Extensions.Hosting.Abstractions": "8.0.1",
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
"Microsoft.Extensions.Options": "8.0.2"
},
"runtime": {
"lib/net8.0/MassTransit.dll": {
"assemblyVersion": "8.5.5.0",
"fileVersion": "8.5.5.0"
}
}
},
"MassTransit.Abstractions/8.5.5": {
"runtime": {
"lib/net8.0/MassTransit.Abstractions.dll": {
"assemblyVersion": "8.5.5.0",
"fileVersion": "8.5.5.0"
}
}
},
"MassTransit.RabbitMQ/8.5.5": {
"dependencies": {
"MassTransit": "8.5.5",
"RabbitMQ.Client": "7.1.2"
},
"runtime": {
"lib/net8.0/MassTransit.RabbitMqTransport.dll": {
"assemblyVersion": "8.5.5.0",
"fileVersion": "8.5.5.0"
}
}
},
"Microsoft.AspNetCore.Authentication.Abstractions/2.3.0": {
"dependencies": {
"Microsoft.AspNetCore.Http.Abstractions": "2.3.0",
@ -565,6 +602,15 @@
}
}
},
"Microsoft.Extensions.Diagnostics.HealthChecks/8.0.0": {
"dependencies": {
"Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "8.0.0",
"Microsoft.Extensions.Hosting.Abstractions": "8.0.1",
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
"Microsoft.Extensions.Options": "8.0.2"
}
},
"Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/8.0.0": {},
"Microsoft.Extensions.FileProviders.Abstractions/8.0.0": {
"dependencies": {
"Microsoft.Extensions.Primitives": "8.0.0"
@ -764,6 +810,18 @@
}
}
},
"RabbitMQ.Client/7.1.2": {
"dependencies": {
"System.IO.Pipelines": "8.0.0",
"System.Threading.RateLimiting": "8.0.0"
},
"runtime": {
"lib/net8.0/RabbitMQ.Client.dll": {
"assemblyVersion": "7.0.0.0",
"fileVersion": "7.1.2.0"
}
}
},
"StackExchange.Redis/2.9.32": {
"dependencies": {
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
@ -922,7 +980,8 @@
}
},
"System.Text.Encodings.Web/8.0.0": {},
"System.Threading.Channels/8.0.0": {}
"System.Threading.Channels/8.0.0": {},
"System.Threading.RateLimiting/8.0.0": {}
}
},
"libraries": {
@ -952,6 +1011,27 @@
"path": "humanizer.core/2.14.1",
"hashPath": "humanizer.core.2.14.1.nupkg.sha512"
},
"MassTransit/8.5.5": {
"type": "package",
"serviceable": true,
"sha512": "sha512-bSg8k5q+rP1s+dIGXLLbctqDGdIkfDjdxwNWtCUH7xNCN9ZuM7mqSPQPIFgaYIi34e81m4FqAqo4CAHuWPkhRA==",
"path": "masstransit/8.5.5",
"hashPath": "masstransit.8.5.5.nupkg.sha512"
},
"MassTransit.Abstractions/8.5.5": {
"type": "package",
"serviceable": true,
"sha512": "sha512-0mn2Ay17dD6z5tgSLjbVRlldSbL9iowzFEfVgVfBXVG5ttz9dSWeR4TrdD6pqH93GWXp4CvSmF8i1HqxLX7DZw==",
"path": "masstransit.abstractions/8.5.5",
"hashPath": "masstransit.abstractions.8.5.5.nupkg.sha512"
},
"MassTransit.RabbitMQ/8.5.5": {
"type": "package",
"serviceable": true,
"sha512": "sha512-UxWn4o90YVMF9PBkJeoskOFPneh6YtnI1fLJHtvZiSAG0eoiRrWPGa+6FQCvjkQ/ljCKfjzok2eGZc/vmNZ01A==",
"path": "masstransit.rabbitmq/8.5.5",
"hashPath": "masstransit.rabbitmq.8.5.5.nupkg.sha512"
},
"Microsoft.AspNetCore.Authentication.Abstractions/2.3.0": {
"type": "package",
"serviceable": true,
@ -1246,6 +1326,20 @@
"path": "microsoft.extensions.diagnostics.abstractions/8.0.1",
"hashPath": "microsoft.extensions.diagnostics.abstractions.8.0.1.nupkg.sha512"
},
"Microsoft.Extensions.Diagnostics.HealthChecks/8.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-P9SoBuVZhJPpALZmSq72aQEb9ryP67EdquaCZGXGrrcASTNHYdrUhnpgSwIipgM5oVC+dKpRXg5zxobmF9xr5g==",
"path": "microsoft.extensions.diagnostics.healthchecks/8.0.0",
"hashPath": "microsoft.extensions.diagnostics.healthchecks.8.0.0.nupkg.sha512"
},
"Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions/8.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-AT2qqos3IgI09ok36Qag9T8bb6kHJ3uT9Q5ki6CySybFsK6/9JbvQAgAHf1pVEjST0/N4JaFaCbm40R5edffwg==",
"path": "microsoft.extensions.diagnostics.healthchecks.abstractions/8.0.0",
"hashPath": "microsoft.extensions.diagnostics.healthchecks.abstractions.8.0.0.nupkg.sha512"
},
"Microsoft.Extensions.FileProviders.Abstractions/8.0.0": {
"type": "package",
"serviceable": true,
@ -1393,6 +1487,13 @@
"path": "pomelo.entityframeworkcore.mysql/8.0.3",
"hashPath": "pomelo.entityframeworkcore.mysql.8.0.3.nupkg.sha512"
},
"RabbitMQ.Client/7.1.2": {
"type": "package",
"serviceable": true,
"sha512": "sha512-y3c6ulgULScWthHw5PLM1ShHRLhxg0vCtzX/hh61gRgNecL3ZC3WoBW2HYHoXOVRqTl99Br9E7CZEytGZEsCyQ==",
"path": "rabbitmq.client/7.1.2",
"hashPath": "rabbitmq.client.7.1.2.nupkg.sha512"
},
"StackExchange.Redis/2.9.32": {
"type": "package",
"serviceable": true,
@ -1553,6 +1654,13 @@
"sha512": "sha512-CMaFr7v+57RW7uZfZkPExsPB6ljwzhjACWW1gfU35Y56rk72B/Wu+sTqxVmGSk4SFUlPc3cjeKND0zktziyjBA==",
"path": "system.threading.channels/8.0.0",
"hashPath": "system.threading.channels.8.0.0.nupkg.sha512"
},
"System.Threading.RateLimiting/8.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-7mu9v0QDv66ar3DpGSZHg9NuNcxDaaAcnMULuZlaTpP9+hwXhrxNGsF5GmLkSHxFdb5bBc1TzeujsRgTrPWi+Q==",
"path": "system.threading.ratelimiting/8.0.0",
"hashPath": "system.threading.ratelimiting.8.0.0.nupkg.sha512"
}
}
}

View File

@ -16,5 +16,11 @@
"ConnectionStrings": {
"DefaultConnection": "Server=frp-era.com;Port=26582;Database=IM;User=product;Password=12345678;",
"Redis": "192.168.5.100:6379"
},
"RabbitMQOptions": {
"Host": "192.168.5.100",
"Port": 5672,
"Username": "test",
"Password": "123456"
}
}

View File

@ -14,7 +14,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("IMTest")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+7ebf23ddd8a0d5536167313c4cf37af1552a5057")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+83be063e7fbdba82984ef3beb231bd8a0a983c2f")]
[assembly: System.Reflection.AssemblyProductAttribute("IMTest")]
[assembly: System.Reflection.AssemblyTitleAttribute("IMTest")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]

View File

@ -1 +1 @@
ca0390a4d5773daae2e747d7512c190701a7a942186c769e42327a4864733f0b
495895405696fa7fe9836aaaa2da1791d39c26be9cfe301758e0fe7d7c9164b6

View File

@ -1,7 +1,5 @@
is_global = true
build_property.TargetFramework = net8.0
build_property.TargetFrameworkIdentifier = .NETCoreApp
build_property.TargetFrameworkVersion = v8.0
build_property.TargetPlatformMinVersion =
build_property.UsingMicrosoftNETSdkWeb =
build_property.ProjectTypeGuids =

View File

@ -1,9 +1,9 @@
// <auto-generated/>
global using System;
global using System.Collections.Generic;
global using System.IO;
global using System.Linq;
global using System.Net.Http;
global using System.Threading;
global using System.Threading.Tasks;
global using Xunit;
global using global::System;
global using global::System.Collections.Generic;
global using global::System.IO;
global using global::System.Linq;
global using global::System.Net.Http;
global using global::System.Threading;
global using global::System.Threading.Tasks;
global using global::Xunit;

View File

@ -1 +1 @@
b97252705aa63c43e1310042fe4fd7115206f72ef0f55d39109c2b849fe2a37e
97ad978fabe5fb8f7f258c9878659ee9a5f2492332ad5efd70b1d456ebc42a59

View File

@ -140,3 +140,7 @@ C:\Users\nanxun\Documents\IM\backend\IMTest\obj\Debug\net8.0\refint\IMTest.dll
C:\Users\nanxun\Documents\IM\backend\IMTest\obj\Debug\net8.0\IMTest.pdb
C:\Users\nanxun\Documents\IM\backend\IMTest\obj\Debug\net8.0\IMTest.genruntimeconfig.cache
C:\Users\nanxun\Documents\IM\backend\IMTest\obj\Debug\net8.0\ref\IMTest.dll
C:\Users\nanxun\Documents\IM\backend\IMTest\bin\Debug\net8.0\MassTransit.dll
C:\Users\nanxun\Documents\IM\backend\IMTest\bin\Debug\net8.0\MassTransit.Abstractions.dll
C:\Users\nanxun\Documents\IM\backend\IMTest\bin\Debug\net8.0\MassTransit.RabbitMqTransport.dll
C:\Users\nanxun\Documents\IM\backend\IMTest\bin\Debug\net8.0\RabbitMQ.Client.dll

View File

@ -49,7 +49,7 @@
"auditLevel": "low",
"auditMode": "direct"
},
"SdkAnalysisLevel": "10.0.100"
"SdkAnalysisLevel": "9.0.300"
},
"frameworks": {
"net8.0": {
@ -96,7 +96,7 @@
"privateAssets": "all"
}
},
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.102/PortableRuntimeIdentifierGraph.json"
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json"
}
}
},
@ -141,7 +141,7 @@
"auditLevel": "low",
"auditMode": "direct"
},
"SdkAnalysisLevel": "10.0.100"
"SdkAnalysisLevel": "9.0.300"
},
"frameworks": {
"net8.0": {
@ -223,7 +223,7 @@
"privateAssets": "all"
}
},
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.102/PortableRuntimeIdentifierGraph.json"
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json"
}
}
}

View File

@ -7,7 +7,7 @@
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\.nuget\packages\</NuGetPackageRoot>
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\nanxun\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages</NuGetPackageFolders>
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">7.0.0</NuGetToolVersion>
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.14.1</NuGetToolVersion>
</PropertyGroup>
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<SourceRoot Include="C:\Users\nanxun\.nuget\packages\" />

View File

@ -8805,7 +8805,7 @@
"auditLevel": "low",
"auditMode": "direct"
},
"SdkAnalysisLevel": "10.0.100"
"SdkAnalysisLevel": "9.0.300"
},
"frameworks": {
"net8.0": {
@ -8852,16 +8852,8 @@
"privateAssets": "all"
}
},
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.102/PortableRuntimeIdentifierGraph.json"
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json"
}
}
},
"logs": [
{
"code": "Undefined",
"level": "Warning",
"warningLevel": 1,
"message": "读取缓存文件 C:\\Users\\nanxun\\Documents\\IM\\backend\\IMTest\\obj\\project.nuget.cache 时遇到问题: '<' is an invalid start of a property name. Expected a '\"'. Path: $ | LineNumber: 2 | BytePositionInLine: 0."
}
]
}
}

View File

@ -1,6 +1,6 @@
{
"version": 2,
"dgSpecHash": "j7OjEXb1ZGE=",
"dgSpecHash": "tVGTA3KwBHQ=",
"success": true,
"projectFilePath": "C:\\Users\\nanxun\\Documents\\IM\\backend\\IMTest\\IMTest.csproj",
"expectedPackageFiles": [
@ -169,15 +169,5 @@
"C:\\Users\\nanxun\\.nuget\\packages\\xunit.extensibility.execution\\2.5.3\\xunit.extensibility.execution.2.5.3.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\xunit.runner.visualstudio\\2.5.3\\xunit.runner.visualstudio.2.5.3.nupkg.sha512"
],
"logs": [
{
"code": "Undefined",
"level": "Warning",
"message": "读取缓存文件 C:\\Users\\nanxun\\Documents\\IM\\backend\\IMTest\\obj\\project.nuget.cache 时遇到问题: '<' is an invalid start of a property name. Expected a '\"'. Path: $ | LineNumber: 2 | BytePositionInLine: 0.",
"projectPath": "C:\\Users\\nanxun\\Documents\\IM\\backend\\IMTest\\IMTest.csproj",
"warningLevel": 1,
"filePath": "C:\\Users\\nanxun\\Documents\\IM\\backend\\IMTest\\IMTest.csproj",
"targetGraphs": []
}
]
"logs": []
}

View File

@ -1,13 +1,26 @@
using IM_API.Domain.Events;
using IM_API.Interface.Services;
using MassTransit;
namespace IM_API.Application.EventHandlers.FriendAddHandler
{
public class FriendAddConversationHandler : IConsumer<FriendAddEvent>
{
public Task Consume(ConsumeContext<FriendAddEvent> context)
private readonly IFriendSerivce _friendService;
public FriendAddConversationHandler(IFriendSerivce friendService)
{
throw new NotImplementedException();
_friendService = friendService;
}
public async Task Consume(ConsumeContext<FriendAddEvent> context)
{
var @event = context.Message;
//为请求发起人添加好友记录
await _friendService.MakeFriendshipAsync(
@event.RequestUserId, @event.ResponseUserId, @event.RequestInfo.RemarkName);
//为接收人添加好友记录
await _friendService.MakeFriendshipAsync(
@event.ResponseUserId, @event.RequestUserId, @event.requestUserRemarkname);
}
}
}

View File

@ -1,28 +1,37 @@
using IM_API.Domain.Events;
using IM_API.Dtos;
using IM_API.Hubs;
using IM_API.Interface.Services;
using IM_API.Models;
using MassTransit;
using Microsoft.AspNetCore.SignalR;
namespace IM_API.Application.EventHandlers.FriendAddHandler
{
public class FriendAddSignalRHandler : IConsumer<FriendAddEvent>
{
private readonly IFriendSerivce _friendService;
public FriendAddSignalRHandler(IFriendSerivce friendSerivce)
private readonly IHubContext<ChatHub> _chathub;
public FriendAddSignalRHandler(IHubContext<ChatHub> chathub)
{
_friendService = friendSerivce;
_chathub = chathub;
}
public async Task Consume(ConsumeContext<FriendAddEvent> context)
{
var @event = context.Message;
//为请求发起人添加好友记录
await _friendService.MakeFriendshipAsync(
@event.RequestUser.Id, @event.ResponseUser.Id, @event.RequestInfo.RemarkName);
//为接收人添加好友记录
await _friendService.MakeFriendshipAsync(
@event.ResponseUser.Id, @event.RequestUser.Id, @event.requestUserRemarkname);
var usersList = new List<string> {
@event.RequestUserId.ToString(), @event.ResponseUserId.ToString()
};
var res = new HubResponse<MessageBaseDto>("Event", new MessageBaseDto()
{
ChatType = ChatType.PRIVATE,
Content = "您有新的好友关系已添加",
MsgId = @event.EventId.ToString(),
ReceiverId = @event.ResponseUserId,
SenderId = @event.RequestUserId,
TimeStamp = DateTime.UtcNow
});
await _chathub.Clients.Users(usersList).SendAsync("ReceiveMessage", res);
}
}
}

View File

@ -28,13 +28,13 @@ namespace IM_API.Application.EventHandlers.MessageCreatedHandler
MessageBaseDto messageBaseDto = new MessageBaseDto
{
MsgId = @event.MessageId.ToString(),
ChatType = @event.ChatType.ToString(),
ChatType = @event.ChatType,
Content = @event.MessageContent,
GroupMemberId = null,
ReceiverId = @event.MsgRecipientId,
SenderId = @event.MsgSenderId,
TimeStamp = @event.MessageCreated,
Type = @event.MessageMsgType.ToString()
Type = @event.MessageMsgType
};
await _hub.Clients.Users(@event.MsgRecipientId.ToString()).SendAsync("ReceiveMessage", messageBaseDto);
}

View File

@ -0,0 +1,36 @@
using IM_API.Domain.Events;
using IM_API.Dtos;
using IM_API.Hubs;
using IM_API.Interface.Services;
using MassTransit;
using Microsoft.AspNetCore.SignalR;
namespace IM_API.Application.EventHandlers.RequestFriendHandler
{
public class RequestFriendSignalRHandler:IConsumer<RequestFriendEvent>
{
private readonly IHubContext<ChatHub> _hub;
private readonly IUserService _userService;
public RequestFriendSignalRHandler(IHubContext<ChatHub> hubContext, IUserService userService)
{
_hub = hubContext;
_userService = userService;
}
public async Task Consume(ConsumeContext<RequestFriendEvent> context)
{
var @event = context.Message;
var userInfo = await _userService.GetUserInfoAsync(@event.FromUserId);
var res = new HubResponse<FriendRequestResDto>("Event", new FriendRequestResDto()
{
RequestUser = @event.FromUserId,
ResponseUser = @event.ToUserId,
Created = DateTime.UtcNow,
Description = @event.Description,
Avatar = userInfo.Avatar,
NickName = userInfo.NickName
});
await _hub.Clients.User(@event.ToUserId.ToString()).SendAsync("ReceiveMessage", res);
}
}
}

View File

@ -46,6 +46,13 @@ namespace IM_API.Configs
.ForMember(dest => dest.StateEnum , opt => opt.MapFrom(src => FriendRequestState.Pending))
.ForMember(dest => dest.Description , opt => opt.MapFrom(src => src.Description))
;
CreateMap<FriendRequest, FriendRequestDto>()
.ForMember(dest => dest.ToUserId, opt => opt.MapFrom(src => src.ResponseUser))
.ForMember(dest => dest.FromUserId, opt => opt.MapFrom(src => src.RequestUser))
.ForMember(dest => dest.RemarkName, opt => opt.MapFrom(src => src.RemarkName))
.ForMember(dest => dest.Description, opt => opt.MapFrom(src => src.Description))
;
//消息模型转换
CreateMap<Message, MessageBaseDto>()
.ForMember(dest => dest.Type , opt => opt.MapFrom(src => src.MsgTypeEnum.ToString()))
@ -59,8 +66,8 @@ namespace IM_API.Configs
;
CreateMap<MessageBaseDto, Message>()
.ForMember(dest => dest.Sender, opt => opt.MapFrom(src => src.SenderId))
.ForMember(dest => dest.ChatTypeEnum,opt => opt.MapFrom(src => Enum.Parse<ChatType>(src.ChatType,true)))
.ForMember(dest => dest.MsgTypeEnum, opt => opt.MapFrom(src => Enum.Parse<MessageMsgType>(src.Type,true)))
.ForMember(dest => dest.ChatTypeEnum,opt => opt.MapFrom(src => src.ChatType))
.ForMember(dest => dest.MsgTypeEnum, opt => opt.MapFrom(src => src.Type))
.ForMember(dest => dest.Created, opt => opt.MapFrom(src => src.TimeStamp))
.ForMember(dest => dest.Content, opt => opt.MapFrom(src => src.Content))
.ForMember(dest => dest.Recipient, opt => opt.MapFrom(src => src.ReceiverId))

View File

@ -8,12 +8,12 @@ namespace IM_API.Domain.Events
/// <summary>
/// 发起请求用户
/// </summary>
public UserInfoDto RequestUser { get; init; }
public int RequestUserId { get; init; }
public string? requestUserRemarkname { get; init; }
/// <summary>
/// 接受请求用户
/// </summary>
public UserInfoDto ResponseUser { get; init; }
public int ResponseUserId { get; init; }
public FriendRequestDto RequestInfo { get; init; }
/// <summary>

View File

@ -0,0 +1,10 @@
namespace IM_API.Domain.Events
{
public record RequestFriendEvent : DomainEvent
{
public override string EventType => "IM.FRIENDS_FRIEND_REQUEST";
public int FromUserId { get; init; }
public int ToUserId { get; init; }
public string Description { get; init; }
}
}

View File

@ -0,0 +1,47 @@
using IM_API.Tools;
namespace IM_API.Dtos
{
public class HubResponse<T>
{
public int Code { get; init; }
public string Method { get; init; }
public HubResponseType Type { get; init; }
public string Message { get; init; }
public T? Data { get; init; }
public HubResponse(string method)
{
Code = CodeDefine.SUCCESS.Code;
Message = CodeDefine.SUCCESS.Message;
Type = HubResponseType.ActionStatus;
}
public HubResponse(string method,T data)
{
Code = CodeDefine.SUCCESS.Code;
Message = CodeDefine.SUCCESS.Message;
Type = HubResponseType.ActionStatus;
Data = data;
}
public HubResponse(CodeDefine codedefine,string method)
{
Code = codedefine.Code;
Method = method;
Message = codedefine.Message;
Type = HubResponseType.ActionStatus;
}
public HubResponse(CodeDefine codeDefine, string method, HubResponseType type, T? data)
{
Code = codeDefine.Code;
Method = method;
Type = type;
Message = codeDefine.Message;
Data = data;
}
}
public enum HubResponseType
{
ChatMsg = 1, // 聊天内容
SystemNotice = 2, // 系统通知(如:申请好友成功)
ActionStatus = 3 // 状态变更(如:对方正在输入、已读回执)
}
}

View File

@ -1,10 +1,12 @@
namespace IM_API.Dtos
using IM_API.Models;
namespace IM_API.Dtos
{
public record MessageBaseDto
{
// 使用 { get; init; } 确保对象创建后不可修改,且支持无参构造
public string Type { get; init; } = default!;
public string ChatType { get; init; } = default!;
public MessageMsgType Type { get; init; } = default!;
public ChatType ChatType { get; init; } = default!;
public string? MsgId { get; init; }
public int SenderId { get; init; }
public int ReceiverId { get; init; }

View File

@ -40,17 +40,17 @@ namespace IM_API.Hubs
}
await base.OnConnectedAsync();
}
public async Task SendMessage(MessageBaseDto dto)
public async Task<HubResponse<MessageBaseDto?>> SendMessage(MessageBaseDto dto)
{
if (!Context.User.Identity.IsAuthenticated)
{
await Clients.Caller.SendAsync("ReceiveMessage", new BaseResponse<object?>(CodeDefine.AUTH_FAILED));
Context.Abort();
return;
return new HubResponse<MessageBaseDto?>(CodeDefine.AUTH_FAILED, "SendMessage");
}
var userIdStr = Context.User.FindFirstValue(ClaimTypes.NameIdentifier);
MessageBaseDto msgInfo = null;
if(dto.ChatType.ToLower() == ChatType.PRIVATE.ToString().ToLower())
if(dto.ChatType == ChatType.PRIVATE)
{
msgInfo = await _messageService.SendPrivateMessageAsync(int.Parse(userIdStr), dto.ReceiverId, dto);
}
@ -58,19 +58,19 @@ namespace IM_API.Hubs
{
msgInfo = await _messageService.SendGroupMessageAsync(int.Parse(userIdStr), dto.ReceiverId, dto);
}
return;
return new HubResponse<MessageBaseDto?>("SendMessage", msgInfo);
}
public async Task ClearUnreadCount(int conversationId)
public async Task<HubResponse<object?>> ClearUnreadCount(int conversationId)
{
if (!Context.User.Identity.IsAuthenticated)
{
await Clients.Caller.SendAsync("ReceiveMessage", new BaseResponse<object?>(CodeDefine.AUTH_FAILED));
Context.Abort();
return;
return new HubResponse<object?>(CodeDefine.AUTH_FAILED, "ClearUnreadCount"); ;
}
var userIdStr = Context.User.FindFirstValue(ClaimTypes.NameIdentifier);
await _conversationService.ClearUnreadCountAsync(int.Parse(userIdStr), conversationId);
return;
return new HubResponse<object?>("ClearUnreadCount");
}
}
}

View File

@ -32,6 +32,11 @@ public partial class FriendRequest
/// </summary>
public sbyte State { get; set; }
/// <summary>
/// 备注
/// </summary>
public string RemarkName { get; set; } = null!;
public virtual User RequestUserNavigation { get; set; } = null!;
public virtual User ResponseUserNavigation { get; set; } = null!;

View File

@ -281,6 +281,9 @@ public partial class ImContext : DbContext
entity.Property(e => e.Description)
.HasComment("申请附言 ")
.HasColumnType("text");
entity.Property(e => e.RemarkName)
.HasMaxLength(20)
.HasComment("备注");
entity.Property(e => e.RequestUser)
.HasComment("申请人 ")
.HasColumnType("int(11)");

View File

@ -9,6 +9,8 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using StackExchange.Redis;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace IM_API
{
@ -38,7 +40,12 @@ namespace IM_API
builder.Services.AddAllService(configuration);
builder.Services.AddSignalR();
builder.Services.AddSignalR().AddJsonProtocol(options =>
{
// 枚举输出字符串
options.PayloadSerializerOptions.Converters.Add(new JsonStringEnumConverter());
options.PayloadSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
});
//允许所有来源(跨域)
builder.Services.AddCors(options =>
{
@ -110,6 +117,10 @@ namespace IM_API
{
// 保持 ISO 8601 格式
options.JsonSerializerOptions.Converters.Add(new UtcDateTimeConverter());
// 将枚举转换为字符串
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
// 建议:保持驼峰命名
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
});
builder.Services.AddModelValidation(configuration);
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle

View File

@ -1,9 +1,11 @@
using AutoMapper;
using IM_API.Domain.Events;
using IM_API.Dtos;
using IM_API.Exceptions;
using IM_API.Interface.Services;
using IM_API.Models;
using IM_API.Tools;
using MassTransit;
using Microsoft.EntityFrameworkCore;
namespace IM_API.Services
@ -13,11 +15,13 @@ namespace IM_API.Services
private readonly ImContext _context;
private readonly ILogger<FriendService> _logger;
private readonly IMapper _mapper;
public FriendService(ImContext context, ILogger<FriendService> logger, IMapper mapper)
private readonly IPublishEndpoint _endpoint;
public FriendService(ImContext context, ILogger<FriendService> logger, IMapper mapper, IPublishEndpoint endpoint)
{
_context = context;
_logger = logger;
_mapper = mapper;
_endpoint = endpoint;
}
#region
public async Task<bool> BlockeFriendAsync(int friendId)
@ -127,13 +131,18 @@ namespace IM_API.Services
//同意后标记
case HandleFriendRequestAction.Accept:
friend.StatusEnum = FriendStatus.Added;
friendRequest.StateEnum = FriendRequestState.Passed;
await _endpoint.Publish(new FriendAddEvent()
{
AggregateId = friendRequest.Id.ToString(),
OccurredAt = DateTime.UtcNow,
Created = DateTime.UtcNow,
EventId = Guid.NewGuid(),
OperatorId = friendRequest.ResponseUser,
RequestInfo = _mapper.Map<FriendRequestDto>(friendRequest),
requestUserRemarkname = requestDto.RemarkName,
//根据当前好友请求为被申请方添加一条好友记录(注意:好友记录为双向)
var ResponseFriend = _mapper.Map<Friend>(friendRequest);
if (!string.IsNullOrEmpty(requestDto.RemarkName)) ResponseFriend.RemarkName = requestDto.RemarkName;
_context.Friends.Add(ResponseFriend);
});
break;
//无效操作
@ -172,6 +181,16 @@ namespace IM_API.Services
var friendRequst = _mapper.Map<FriendRequest>(dto);
_context.FriendRequests.Add(friendRequst);
await _context.SaveChangesAsync();
await _endpoint.Publish(new RequestFriendEvent()
{
AggregateId = friendRequst.Id.ToString(),
OccurredAt = friendRequst.Created,
Description = friendRequst.Description,
EventId = Guid.NewGuid(),
FromUserId = friendRequst.RequestUser,
ToUserId = friendRequst.ResponseUser,
OperatorId = friendRequst.RequestUser
});
return true;
}
#endregion

View File

@ -1,4 +1,4 @@
#VITE_API_BASE_URL = http://localhost:5202/api
#VITE_SIGNALR_BASE_URL = http://localhost:5202/chat
VITE_API_BASE_URL = https://im.test.nxsir.cn/api
VITE_SIGNALR_BASE_URL = https://im.test.nxsir.cn/chat/
VITE_API_BASE_URL = http://localhost:5202/api
VITE_SIGNALR_BASE_URL = http://localhost:5202/chat/
#VITE_API_BASE_URL = https://im.test.nxsir.cn/api
#VITE_SIGNALR_BASE_URL = https://im.test.nxsir.cn/chat/