Merge pull request 'feature-nxdev' (#58) from feature-nxdev into main

Reviewed-on: #58
This commit is contained in:
西街长安 2026-02-07 22:39:43 +08:00
commit 77c71f4017
118 changed files with 10691 additions and 452 deletions

View File

@ -0,0 +1,89 @@
using AutoMapper;
using IM_API.Dtos.Group;
using IM_API.Models;
using IM_API.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using MassTransit;
using IM_API.Configs;
namespace IM_API.Tests.Services
{
public class GroupServiceTests
{
private readonly ImContext _context;
private readonly IMapper _mapper;
private readonly Mock<ILogger<GroupService>> _loggerMock = new();
private readonly Mock<IPublishEndpoint> _publishMock = new();
public GroupServiceTests()
{
// 1. 配置内存数据库
var options = new DbContextOptionsBuilder<ImContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
_context = new ImContext(options);
// 2. 配置真实的 AutoMapper (ProjectTo 必须使用真实配置,不能用 Mock)
var config = new MapperConfiguration(cfg =>
{
// 替换为你项目中真实的 Profile 类名
cfg.AddProfile<MapperConfig>();
// 如果有多个 Profile可以继续添加或者加载整个程序集
// cfg.AddMaps(typeof(GroupProfile).Assembly);
});
_mapper = config.CreateMapper();
}
[Fact]
public async Task GetGroupList_ShouldReturnPagedAndOrderedData()
{
// Arrange (准备数据)
var userId = 1;
var groups = new List<Group>
{
new Group { Id = 101, Name = "Group A", Avatar = "1111" },
new Group { Id = 102, Name = "Group B" , Avatar = "1111"},
new Group { Id = 103, Name = "Group C", Avatar = "1111" }
};
_context.Groups.AddRange(groups);
_context.GroupMembers.AddRange(new List<GroupMember>
{
new GroupMember { UserId = userId, GroupId = 101 },
new GroupMember { UserId = userId, GroupId = 102 },
new GroupMember { UserId = userId, GroupId = 103 }
});
await _context.SaveChangesAsync();
var service = new GroupService(_context, _mapper, _loggerMock.Object, _publishMock.Object);
// Act (执行测试: 第1页每页2条倒序)
var result = await service.GetGroupListAsync(userId, page: 1, limit: 2, desc: true);
// Assert (断言)
Assert.NotNull(result);
Assert.Equal(2, result.Count);
Assert.Equal(103, result[0].Id); // 倒序最大的是 103
Assert.Equal(102, result[1].Id);
}
[Fact]
public async Task GetGroupList_ShouldReturnEmpty_WhenUserHasNoGroups()
{
// Arrange
var service = new GroupService(_context, _mapper, _loggerMock.Object, _publishMock.Object);
// Act
var result = await service.GetGroupListAsync(userId: 999, page: 1, limit: 10, desc: false);
// Assert
Assert.Empty(result);
}
}
}

View File

@ -250,6 +250,14 @@
"System.Text.Encodings.Web": "8.0.0"
}
},
"Microsoft.Bcl.AsyncInterfaces/1.1.0": {
"runtime": {
"lib/netstandard2.1/Microsoft.Bcl.AsyncInterfaces.dll": {
"assemblyVersion": "1.0.0.0",
"fileVersion": "4.700.19.56404"
}
}
},
"Microsoft.CodeCoverage/17.8.0": {
"runtime": {
"lib/netcoreapp3.1/Microsoft.VisualStudio.CodeCoverage.Shim.dll": {
@ -900,6 +908,24 @@
}
}
},
"RedLock.net/2.3.2": {
"dependencies": {
"Microsoft.Bcl.AsyncInterfaces": "1.1.0",
"Microsoft.Extensions.Logging": "8.0.1",
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
"StackExchange.Redis": "2.9.32"
},
"runtime": {
"lib/netstandard2.0/RedLockNet.Abstractions.dll": {
"assemblyVersion": "2.3.2.0",
"fileVersion": "2.3.2.0"
},
"lib/netstandard2.0/RedLockNet.SERedis.dll": {
"assemblyVersion": "2.3.2.0",
"fileVersion": "2.3.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": {},
@ -1637,6 +1663,7 @@
"Microsoft.VisualStudio.Azure.Containers.Tools.Targets": "1.22.1",
"Newtonsoft.Json": "13.0.4",
"Pomelo.EntityFrameworkCore.MySql": "8.0.3",
"RedLock.net": "2.3.2",
"StackExchange.Redis": "2.9.32",
"Swashbuckle.AspNetCore": "6.6.2",
"System.IdentityModel.Tokens.Jwt": "8.14.0"
@ -1852,6 +1879,13 @@
"path": "microsoft.aspnetcore.webutilities/2.3.0",
"hashPath": "microsoft.aspnetcore.webutilities.2.3.0.nupkg.sha512"
},
"Microsoft.Bcl.AsyncInterfaces/1.1.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-1Am6l4Vpn3/K32daEqZI+FFr96OlZkgwK2LcT3pZ2zWubR5zTPW3/FkO1Rat9kb7oQOa4rxgl9LJHc5tspCWfg==",
"path": "microsoft.bcl.asyncinterfaces/1.1.0",
"hashPath": "microsoft.bcl.asyncinterfaces.1.1.0.nupkg.sha512"
},
"Microsoft.CodeCoverage/17.8.0": {
"type": "package",
"serviceable": true,
@ -2174,6 +2208,13 @@
"path": "rabbitmq.client/7.1.2",
"hashPath": "rabbitmq.client.7.1.2.nupkg.sha512"
},
"RedLock.net/2.3.2": {
"type": "package",
"serviceable": true,
"sha512": "sha512-jlrALAArm4dCE292U3EtRoMnVKJ9M6sunbZn/oG5OuzlGtTpusXBfvDrnGWbgGDlWV027f5E9H5CiVnPxiq8+g==",
"path": "redlock.net/2.3.2",
"hashPath": "redlock.net.2.3.2.nupkg.sha512"
},
"runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.0": {
"type": "package",
"serviceable": true,

View File

@ -18,6 +18,7 @@
"Microsoft.VisualStudio.Azure.Containers.Tools.Targets": "1.22.1",
"Newtonsoft.Json": "13.0.4",
"Pomelo.EntityFrameworkCore.MySql": "8.0.3",
"RedLock.net": "2.3.2",
"StackExchange.Redis": "2.9.32",
"Swashbuckle.AspNetCore": "6.6.2",
"System.IdentityModel.Tokens.Jwt": "8.14.0"
@ -822,6 +823,24 @@
}
}
},
"RedLock.net/2.3.2": {
"dependencies": {
"Microsoft.Bcl.AsyncInterfaces": "6.0.0",
"Microsoft.Extensions.Logging": "8.0.1",
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
"StackExchange.Redis": "2.9.32"
},
"runtime": {
"lib/netstandard2.0/RedLockNet.Abstractions.dll": {
"assemblyVersion": "2.3.2.0",
"fileVersion": "2.3.2.0"
},
"lib/netstandard2.0/RedLockNet.SERedis.dll": {
"assemblyVersion": "2.3.2.0",
"fileVersion": "2.3.2.0"
}
}
},
"StackExchange.Redis/2.9.32": {
"dependencies": {
"Microsoft.Extensions.Logging.Abstractions": "8.0.2",
@ -1494,6 +1513,13 @@
"path": "rabbitmq.client/7.1.2",
"hashPath": "rabbitmq.client.7.1.2.nupkg.sha512"
},
"RedLock.net/2.3.2": {
"type": "package",
"serviceable": true,
"sha512": "sha512-jlrALAArm4dCE292U3EtRoMnVKJ9M6sunbZn/oG5OuzlGtTpusXBfvDrnGWbgGDlWV027f5E9H5CiVnPxiq8+g==",
"path": "redlock.net/2.3.2",
"hashPath": "redlock.net.2.3.2.nupkg.sha512"
},
"StackExchange.Redis/2.9.32": {
"type": "package",
"serviceable": true,

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+22038345c32db146ffb78173c5410f954daa64e0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+07bb01808f00b598a75ab9490d12e3514580fcab")]
[assembly: System.Reflection.AssemblyProductAttribute("IMTest")]
[assembly: System.Reflection.AssemblyTitleAttribute("IMTest")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]

View File

@ -1 +1 @@
ee1fc45f192938903a153f1c2e3b53f60a2184cb806b87d9b57b487095b98264
ed76e92b5e8795c53e98fa0f534a65801467f19a9f8a7b0c127cdc4e442cbb2a

View File

@ -1 +1 @@
97ad978fabe5fb8f7f258c9878659ee9a5f2492332ad5efd70b1d456ebc42a59
f5bc8c7a1703bdc5e206f40b9687382b19faace8ff21b9d0a842dec47bff95a8

View File

@ -144,3 +144,6 @@ 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
C:\Users\nanxun\Documents\IM\backend\IMTest\bin\Debug\net8.0\Microsoft.Bcl.AsyncInterfaces.dll
C:\Users\nanxun\Documents\IM\backend\IMTest\bin\Debug\net8.0\RedLockNet.Abstractions.dll
C:\Users\nanxun\Documents\IM\backend\IMTest\bin\Debug\net8.0\RedLockNet.SERedis.dll

View File

@ -179,6 +179,10 @@
"target": "Package",
"version": "[8.0.21, )"
},
"Microsoft.Extensions.Caching.StackExchangeRedis": {
"target": "Package",
"version": "[10.0.2, )"
},
"Microsoft.VisualStudio.Azure.Containers.Tools.Targets": {
"target": "Package",
"version": "[1.22.1, )"
@ -191,6 +195,10 @@
"target": "Package",
"version": "[8.0.3, )"
},
"RedLock.net": {
"target": "Package",
"version": "[2.3.2, )"
},
"StackExchange.Redis": {
"target": "Package",
"version": "[2.9.32, )"

View File

@ -2,8 +2,8 @@
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ImportGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<Import Project="$(NuGetPackageRoot)xunit.core\2.5.3\build\xunit.core.targets" Condition="Exists('$(NuGetPackageRoot)xunit.core\2.5.3\build\xunit.core.targets')" />
<Import Project="$(NuGetPackageRoot)microsoft.extensions.logging.abstractions\8.0.2\buildTransitive\net6.0\Microsoft.Extensions.Logging.Abstractions.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.extensions.logging.abstractions\8.0.2\buildTransitive\net6.0\Microsoft.Extensions.Logging.Abstractions.targets')" />
<Import Project="$(NuGetPackageRoot)microsoft.extensions.options\8.0.2\buildTransitive\net6.0\Microsoft.Extensions.Options.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.extensions.options\8.0.2\buildTransitive\net6.0\Microsoft.Extensions.Options.targets')" />
<Import Project="$(NuGetPackageRoot)microsoft.extensions.logging.abstractions\10.0.2\buildTransitive\net8.0\Microsoft.Extensions.Logging.Abstractions.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.extensions.logging.abstractions\10.0.2\buildTransitive\net8.0\Microsoft.Extensions.Logging.Abstractions.targets')" />
<Import Project="$(NuGetPackageRoot)microsoft.extensions.options\10.0.2\buildTransitive\net8.0\Microsoft.Extensions.Options.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.extensions.options\10.0.2\buildTransitive\net8.0\Microsoft.Extensions.Options.targets')" />
<Import Project="$(NuGetPackageRoot)microsoft.codecoverage\17.8.0\build\netstandard2.0\Microsoft.CodeCoverage.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.codecoverage\17.8.0\build\netstandard2.0\Microsoft.CodeCoverage.targets')" />
<Import Project="$(NuGetPackageRoot)microsoft.net.test.sdk\17.8.0\build\netcoreapp3.1\Microsoft.NET.Test.Sdk.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.net.test.sdk\17.8.0\build\netcoreapp3.1\Microsoft.NET.Test.Sdk.targets')" />
<Import Project="$(NuGetPackageRoot)coverlet.collector\6.0.0\build\netstandard1.0\coverlet.collector.targets" Condition="Exists('$(NuGetPackageRoot)coverlet.collector\6.0.0\build\netstandard1.0\coverlet.collector.targets')" />

View File

@ -491,6 +491,17 @@
}
}
},
"Microsoft.Bcl.AsyncInterfaces/1.1.0": {
"type": "package",
"compile": {
"ref/netstandard2.1/Microsoft.Bcl.AsyncInterfaces.dll": {}
},
"runtime": {
"lib/netstandard2.1/Microsoft.Bcl.AsyncInterfaces.dll": {
"related": ".xml"
}
}
},
"Microsoft.CodeCoverage/17.8.0": {
"type": "package",
"compile": {
@ -599,10 +610,10 @@
"buildMultiTargeting/_._": {}
}
},
"Microsoft.Extensions.Caching.Abstractions/8.0.0": {
"Microsoft.Extensions.Caching.Abstractions/10.0.2": {
"type": "package",
"dependencies": {
"Microsoft.Extensions.Primitives": "8.0.0"
"Microsoft.Extensions.Primitives": "10.0.2"
},
"compile": {
"lib/net8.0/Microsoft.Extensions.Caching.Abstractions.dll": {
@ -615,7 +626,7 @@
}
},
"build": {
"buildTransitive/net6.0/_._": {}
"buildTransitive/net8.0/_._": {}
}
},
"Microsoft.Extensions.Caching.Memory/8.0.1": {
@ -641,6 +652,25 @@
"buildTransitive/net6.0/_._": {}
}
},
"Microsoft.Extensions.Caching.StackExchangeRedis/10.0.2": {
"type": "package",
"dependencies": {
"Microsoft.Extensions.Caching.Abstractions": "10.0.2",
"Microsoft.Extensions.Logging.Abstractions": "10.0.2",
"Microsoft.Extensions.Options": "10.0.2",
"StackExchange.Redis": "2.7.27"
},
"compile": {
"lib/netstandard2.0/Microsoft.Extensions.Caching.StackExchangeRedis.dll": {
"related": ".xml"
}
},
"runtime": {
"lib/netstandard2.0/Microsoft.Extensions.Caching.StackExchangeRedis.dll": {
"related": ".xml"
}
}
},
"Microsoft.Extensions.Configuration.Abstractions/8.0.0": {
"type": "package",
"dependencies": {
@ -679,7 +709,7 @@
"buildTransitive/net6.0/_._": {}
}
},
"Microsoft.Extensions.DependencyInjection.Abstractions/8.0.2": {
"Microsoft.Extensions.DependencyInjection.Abstractions/10.0.2": {
"type": "package",
"compile": {
"lib/net8.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll": {
@ -692,7 +722,7 @@
}
},
"build": {
"buildTransitive/net6.0/_._": {}
"buildTransitive/net8.0/_._": {}
}
},
"Microsoft.Extensions.Diagnostics.Abstractions/8.0.1": {
@ -810,10 +840,11 @@
"buildTransitive/net6.0/_._": {}
}
},
"Microsoft.Extensions.Logging.Abstractions/8.0.2": {
"Microsoft.Extensions.Logging.Abstractions/10.0.2": {
"type": "package",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2"
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.2",
"System.Diagnostics.DiagnosticSource": "10.0.2"
},
"compile": {
"lib/net8.0/Microsoft.Extensions.Logging.Abstractions.dll": {
@ -826,7 +857,7 @@
}
},
"build": {
"buildTransitive/net6.0/Microsoft.Extensions.Logging.Abstractions.targets": {}
"buildTransitive/net8.0/Microsoft.Extensions.Logging.Abstractions.targets": {}
}
},
"Microsoft.Extensions.ObjectPool/8.0.11": {
@ -842,11 +873,11 @@
}
}
},
"Microsoft.Extensions.Options/8.0.2": {
"Microsoft.Extensions.Options/10.0.2": {
"type": "package",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
"Microsoft.Extensions.Primitives": "8.0.0"
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.2",
"Microsoft.Extensions.Primitives": "10.0.2"
},
"compile": {
"lib/net8.0/Microsoft.Extensions.Options.dll": {
@ -859,10 +890,10 @@
}
},
"build": {
"buildTransitive/net6.0/Microsoft.Extensions.Options.targets": {}
"buildTransitive/net8.0/Microsoft.Extensions.Options.targets": {}
}
},
"Microsoft.Extensions.Primitives/8.0.0": {
"Microsoft.Extensions.Primitives/10.0.2": {
"type": "package",
"compile": {
"lib/net8.0/Microsoft.Extensions.Primitives.dll": {
@ -875,7 +906,7 @@
}
},
"build": {
"buildTransitive/net6.0/_._": {}
"buildTransitive/net8.0/_._": {}
}
},
"Microsoft.IdentityModel.Abstractions/8.14.0": {
@ -1460,6 +1491,23 @@
}
}
},
"RedLock.net/2.3.2": {
"type": "package",
"dependencies": {
"Microsoft.Bcl.AsyncInterfaces": "1.1.0",
"Microsoft.Extensions.Logging": "2.0.0",
"Microsoft.Extensions.Logging.Abstractions": "2.0.0",
"StackExchange.Redis": "2.0.513"
},
"compile": {
"lib/netstandard2.0/RedLockNet.Abstractions.dll": {},
"lib/netstandard2.0/RedLockNet.SERedis.dll": {}
},
"runtime": {
"lib/netstandard2.0/RedLockNet.Abstractions.dll": {},
"lib/netstandard2.0/RedLockNet.SERedis.dll": {}
}
},
"runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.0": {
"type": "package",
"runtimeTargets": {
@ -1798,24 +1846,20 @@
}
}
},
"System.Diagnostics.DiagnosticSource/4.3.0": {
"System.Diagnostics.DiagnosticSource/10.0.2": {
"type": "package",
"dependencies": {
"System.Collections": "4.3.0",
"System.Diagnostics.Tracing": "4.3.0",
"System.Reflection": "4.3.0",
"System.Runtime": "4.3.0",
"System.Threading": "4.3.0"
},
"compile": {
"lib/netstandard1.3/_._": {
"lib/net8.0/System.Diagnostics.DiagnosticSource.dll": {
"related": ".xml"
}
},
"runtime": {
"lib/netstandard1.3/System.Diagnostics.DiagnosticSource.dll": {
"lib/net8.0/System.Diagnostics.DiagnosticSource.dll": {
"related": ".xml"
}
},
"build": {
"buildTransitive/net8.0/_._": {}
}
},
"System.Diagnostics.EventLog/6.0.0": {
@ -2978,9 +3022,11 @@
"MassTransit.RabbitMQ": "8.5.5",
"Microsoft.AspNetCore.Authentication.JwtBearer": "8.0.21",
"Microsoft.AspNetCore.SignalR": "1.2.0",
"Microsoft.Extensions.Caching.StackExchangeRedis": "10.0.2",
"Microsoft.VisualStudio.Azure.Containers.Tools.Targets": "1.22.1",
"Newtonsoft.Json": "13.0.4",
"Pomelo.EntityFrameworkCore.MySql": "8.0.3",
"RedLock.net": "2.3.2",
"StackExchange.Redis": "2.9.32",
"Swashbuckle.AspNetCore": "6.6.2",
"System.IdentityModel.Tokens.Jwt": "8.14.0"
@ -3442,6 +3488,30 @@
"microsoft.aspnetcore.webutilities.nuspec"
]
},
"Microsoft.Bcl.AsyncInterfaces/1.1.0": {
"sha512": "1Am6l4Vpn3/K32daEqZI+FFr96OlZkgwK2LcT3pZ2zWubR5zTPW3/FkO1Rat9kb7oQOa4rxgl9LJHc5tspCWfg==",
"type": "package",
"path": "microsoft.bcl.asyncinterfaces/1.1.0",
"files": [
".nupkg.metadata",
".signature.p7s",
"LICENSE.TXT",
"THIRD-PARTY-NOTICES.TXT",
"lib/net461/Microsoft.Bcl.AsyncInterfaces.dll",
"lib/net461/Microsoft.Bcl.AsyncInterfaces.xml",
"lib/netstandard2.0/Microsoft.Bcl.AsyncInterfaces.dll",
"lib/netstandard2.0/Microsoft.Bcl.AsyncInterfaces.xml",
"lib/netstandard2.1/Microsoft.Bcl.AsyncInterfaces.dll",
"lib/netstandard2.1/Microsoft.Bcl.AsyncInterfaces.xml",
"microsoft.bcl.asyncinterfaces.1.1.0.nupkg.sha512",
"microsoft.bcl.asyncinterfaces.nuspec",
"ref/net461/Microsoft.Bcl.AsyncInterfaces.dll",
"ref/netstandard2.0/Microsoft.Bcl.AsyncInterfaces.dll",
"ref/netstandard2.1/Microsoft.Bcl.AsyncInterfaces.dll",
"useSharedDesignerContext.txt",
"version.txt"
]
},
"Microsoft.CodeCoverage/17.8.0": {
"sha512": "KC8SXWbGIdoFVdlxKk9WHccm0llm9HypcHMLUUFabRiTS3SO2fQXNZfdiF3qkEdTJhbRrxhdRxjL4jbtwPq4Ew==",
"type": "package",
@ -3884,32 +3954,31 @@
"tools/netcoreapp2.1/System.Diagnostics.DiagnosticSource.dll"
]
},
"Microsoft.Extensions.Caching.Abstractions/8.0.0": {
"sha512": "3KuSxeHoNYdxVYfg2IRZCThcrlJ1XJqIXkAWikCsbm5C/bCjv7G0WoKDyuR98Q+T607QT2Zl5GsbGRkENcV2yQ==",
"Microsoft.Extensions.Caching.Abstractions/10.0.2": {
"sha512": "WIRPDa/qoKHmJhTAPCO/zLu9kRLQ2Fd6HD5tzgdXJ3xGEVXDHP6FvakKJjynwKrVDld8H4G4tcbW53wuC/wxMQ==",
"type": "package",
"path": "microsoft.extensions.caching.abstractions/8.0.0",
"path": "microsoft.extensions.caching.abstractions/10.0.2",
"files": [
".nupkg.metadata",
".signature.p7s",
"Icon.png",
"LICENSE.TXT",
"PACKAGE.md",
"THIRD-PARTY-NOTICES.TXT",
"buildTransitive/net461/Microsoft.Extensions.Caching.Abstractions.targets",
"buildTransitive/net462/_._",
"buildTransitive/net6.0/_._",
"buildTransitive/net8.0/_._",
"buildTransitive/netcoreapp2.0/Microsoft.Extensions.Caching.Abstractions.targets",
"lib/net10.0/Microsoft.Extensions.Caching.Abstractions.dll",
"lib/net10.0/Microsoft.Extensions.Caching.Abstractions.xml",
"lib/net462/Microsoft.Extensions.Caching.Abstractions.dll",
"lib/net462/Microsoft.Extensions.Caching.Abstractions.xml",
"lib/net6.0/Microsoft.Extensions.Caching.Abstractions.dll",
"lib/net6.0/Microsoft.Extensions.Caching.Abstractions.xml",
"lib/net7.0/Microsoft.Extensions.Caching.Abstractions.dll",
"lib/net7.0/Microsoft.Extensions.Caching.Abstractions.xml",
"lib/net8.0/Microsoft.Extensions.Caching.Abstractions.dll",
"lib/net8.0/Microsoft.Extensions.Caching.Abstractions.xml",
"lib/net9.0/Microsoft.Extensions.Caching.Abstractions.dll",
"lib/net9.0/Microsoft.Extensions.Caching.Abstractions.xml",
"lib/netstandard2.0/Microsoft.Extensions.Caching.Abstractions.dll",
"lib/netstandard2.0/Microsoft.Extensions.Caching.Abstractions.xml",
"microsoft.extensions.caching.abstractions.8.0.0.nupkg.sha512",
"microsoft.extensions.caching.abstractions.10.0.2.nupkg.sha512",
"microsoft.extensions.caching.abstractions.nuspec",
"useSharedDesignerContext.txt"
]
@ -3944,6 +4013,26 @@
"useSharedDesignerContext.txt"
]
},
"Microsoft.Extensions.Caching.StackExchangeRedis/10.0.2": {
"sha512": "WEx0VM6KVv4Bf6lwe4WQTd4EixIfw38ZU3u/7zMe+uC5fOyiANu8Os/qyiqv2iEsIJb296tbd2E2BTaWIha3Vg==",
"type": "package",
"path": "microsoft.extensions.caching.stackexchangeredis/10.0.2",
"files": [
".nupkg.metadata",
".signature.p7s",
"Icon.png",
"PACKAGE.md",
"THIRD-PARTY-NOTICES.TXT",
"lib/net10.0/Microsoft.Extensions.Caching.StackExchangeRedis.dll",
"lib/net10.0/Microsoft.Extensions.Caching.StackExchangeRedis.xml",
"lib/net462/Microsoft.Extensions.Caching.StackExchangeRedis.dll",
"lib/net462/Microsoft.Extensions.Caching.StackExchangeRedis.xml",
"lib/netstandard2.0/Microsoft.Extensions.Caching.StackExchangeRedis.dll",
"lib/netstandard2.0/Microsoft.Extensions.Caching.StackExchangeRedis.xml",
"microsoft.extensions.caching.stackexchangeredis.10.0.2.nupkg.sha512",
"microsoft.extensions.caching.stackexchangeredis.nuspec"
]
},
"Microsoft.Extensions.Configuration.Abstractions/8.0.0": {
"sha512": "3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==",
"type": "package",
@ -4006,34 +4095,33 @@
"useSharedDesignerContext.txt"
]
},
"Microsoft.Extensions.DependencyInjection.Abstractions/8.0.2": {
"sha512": "3iE7UF7MQkCv1cxzCahz+Y/guQbTqieyxyaWKhrRO91itI9cOKO76OHeQDahqG4MmW5umr3CcCvGmK92lWNlbg==",
"Microsoft.Extensions.DependencyInjection.Abstractions/10.0.2": {
"sha512": "zOIurr59+kUf9vNcsUkCvKWZv+fPosUZXURZesYkJCvl0EzTc9F7maAO4Cd2WEV7ZJJ0AZrFQvuH6Npph9wdBw==",
"type": "package",
"path": "microsoft.extensions.dependencyinjection.abstractions/8.0.2",
"path": "microsoft.extensions.dependencyinjection.abstractions/10.0.2",
"files": [
".nupkg.metadata",
".signature.p7s",
"Icon.png",
"LICENSE.TXT",
"PACKAGE.md",
"THIRD-PARTY-NOTICES.TXT",
"buildTransitive/net461/Microsoft.Extensions.DependencyInjection.Abstractions.targets",
"buildTransitive/net462/_._",
"buildTransitive/net6.0/_._",
"buildTransitive/net8.0/_._",
"buildTransitive/netcoreapp2.0/Microsoft.Extensions.DependencyInjection.Abstractions.targets",
"lib/net10.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll",
"lib/net10.0/Microsoft.Extensions.DependencyInjection.Abstractions.xml",
"lib/net462/Microsoft.Extensions.DependencyInjection.Abstractions.dll",
"lib/net462/Microsoft.Extensions.DependencyInjection.Abstractions.xml",
"lib/net6.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll",
"lib/net6.0/Microsoft.Extensions.DependencyInjection.Abstractions.xml",
"lib/net7.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll",
"lib/net7.0/Microsoft.Extensions.DependencyInjection.Abstractions.xml",
"lib/net8.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll",
"lib/net8.0/Microsoft.Extensions.DependencyInjection.Abstractions.xml",
"lib/net9.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll",
"lib/net9.0/Microsoft.Extensions.DependencyInjection.Abstractions.xml",
"lib/netstandard2.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll",
"lib/netstandard2.0/Microsoft.Extensions.DependencyInjection.Abstractions.xml",
"lib/netstandard2.1/Microsoft.Extensions.DependencyInjection.Abstractions.dll",
"lib/netstandard2.1/Microsoft.Extensions.DependencyInjection.Abstractions.xml",
"microsoft.extensions.dependencyinjection.abstractions.8.0.2.nupkg.sha512",
"microsoft.extensions.dependencyinjection.abstractions.10.0.2.nupkg.sha512",
"microsoft.extensions.dependencyinjection.abstractions.nuspec",
"useSharedDesignerContext.txt"
]
@ -4197,15 +4285,14 @@
"useSharedDesignerContext.txt"
]
},
"Microsoft.Extensions.Logging.Abstractions/8.0.2": {
"sha512": "nroMDjS7hNBPtkZqVBbSiQaQjWRDxITI8Y7XnDs97rqG3EbzVTNLZQf7bIeUJcaHOV8bca47s1Uxq94+2oGdxA==",
"Microsoft.Extensions.Logging.Abstractions/10.0.2": {
"sha512": "RZkez/JjpnO+MZ6efKkSynN6ZztLpw3WbxNzjLCPBd97wWj1S9ZYPWi0nmT4kWBRa6atHsdM1ydGkUr8GudyDQ==",
"type": "package",
"path": "microsoft.extensions.logging.abstractions/8.0.2",
"path": "microsoft.extensions.logging.abstractions/10.0.2",
"files": [
".nupkg.metadata",
".signature.p7s",
"Icon.png",
"LICENSE.TXT",
"PACKAGE.md",
"THIRD-PARTY-NOTICES.TXT",
"analyzers/dotnet/roslyn3.11/cs/Microsoft.Extensions.Logging.Generators.dll",
@ -4252,20 +4339,20 @@
"analyzers/dotnet/roslyn4.4/cs/zh-Hant/Microsoft.Extensions.Logging.Generators.resources.dll",
"buildTransitive/net461/Microsoft.Extensions.Logging.Abstractions.targets",
"buildTransitive/net462/Microsoft.Extensions.Logging.Abstractions.targets",
"buildTransitive/net6.0/Microsoft.Extensions.Logging.Abstractions.targets",
"buildTransitive/net8.0/Microsoft.Extensions.Logging.Abstractions.targets",
"buildTransitive/netcoreapp2.0/Microsoft.Extensions.Logging.Abstractions.targets",
"buildTransitive/netstandard2.0/Microsoft.Extensions.Logging.Abstractions.targets",
"lib/net10.0/Microsoft.Extensions.Logging.Abstractions.dll",
"lib/net10.0/Microsoft.Extensions.Logging.Abstractions.xml",
"lib/net462/Microsoft.Extensions.Logging.Abstractions.dll",
"lib/net462/Microsoft.Extensions.Logging.Abstractions.xml",
"lib/net6.0/Microsoft.Extensions.Logging.Abstractions.dll",
"lib/net6.0/Microsoft.Extensions.Logging.Abstractions.xml",
"lib/net7.0/Microsoft.Extensions.Logging.Abstractions.dll",
"lib/net7.0/Microsoft.Extensions.Logging.Abstractions.xml",
"lib/net8.0/Microsoft.Extensions.Logging.Abstractions.dll",
"lib/net8.0/Microsoft.Extensions.Logging.Abstractions.xml",
"lib/net9.0/Microsoft.Extensions.Logging.Abstractions.dll",
"lib/net9.0/Microsoft.Extensions.Logging.Abstractions.xml",
"lib/netstandard2.0/Microsoft.Extensions.Logging.Abstractions.dll",
"lib/netstandard2.0/Microsoft.Extensions.Logging.Abstractions.xml",
"microsoft.extensions.logging.abstractions.8.0.2.nupkg.sha512",
"microsoft.extensions.logging.abstractions.10.0.2.nupkg.sha512",
"microsoft.extensions.logging.abstractions.nuspec",
"useSharedDesignerContext.txt"
]
@ -4289,15 +4376,14 @@
"microsoft.extensions.objectpool.nuspec"
]
},
"Microsoft.Extensions.Options/8.0.2": {
"sha512": "dWGKvhFybsaZpGmzkGCbNNwBD1rVlWzrZKANLW/CcbFJpCEceMCGzT7zZwHOGBCbwM0SzBuceMj5HN1LKV1QqA==",
"Microsoft.Extensions.Options/10.0.2": {
"sha512": "1De2LJjmxdqopI5AYC5dIhoZQ79AR5ayywxNF1rXrXFtKQfbQOV9+n/IsZBa7qWlr0MqoGpW8+OY2v/57udZOA==",
"type": "package",
"path": "microsoft.extensions.options/8.0.2",
"path": "microsoft.extensions.options/10.0.2",
"files": [
".nupkg.metadata",
".signature.p7s",
"Icon.png",
"LICENSE.TXT",
"PACKAGE.md",
"THIRD-PARTY-NOTICES.TXT",
"analyzers/dotnet/roslyn4.4/cs/Microsoft.Extensions.Options.SourceGeneration.dll",
@ -4316,52 +4402,51 @@
"analyzers/dotnet/roslyn4.4/cs/zh-Hant/Microsoft.Extensions.Options.SourceGeneration.resources.dll",
"buildTransitive/net461/Microsoft.Extensions.Options.targets",
"buildTransitive/net462/Microsoft.Extensions.Options.targets",
"buildTransitive/net6.0/Microsoft.Extensions.Options.targets",
"buildTransitive/net8.0/Microsoft.Extensions.Options.targets",
"buildTransitive/netcoreapp2.0/Microsoft.Extensions.Options.targets",
"buildTransitive/netstandard2.0/Microsoft.Extensions.Options.targets",
"lib/net10.0/Microsoft.Extensions.Options.dll",
"lib/net10.0/Microsoft.Extensions.Options.xml",
"lib/net462/Microsoft.Extensions.Options.dll",
"lib/net462/Microsoft.Extensions.Options.xml",
"lib/net6.0/Microsoft.Extensions.Options.dll",
"lib/net6.0/Microsoft.Extensions.Options.xml",
"lib/net7.0/Microsoft.Extensions.Options.dll",
"lib/net7.0/Microsoft.Extensions.Options.xml",
"lib/net8.0/Microsoft.Extensions.Options.dll",
"lib/net8.0/Microsoft.Extensions.Options.xml",
"lib/net9.0/Microsoft.Extensions.Options.dll",
"lib/net9.0/Microsoft.Extensions.Options.xml",
"lib/netstandard2.0/Microsoft.Extensions.Options.dll",
"lib/netstandard2.0/Microsoft.Extensions.Options.xml",
"lib/netstandard2.1/Microsoft.Extensions.Options.dll",
"lib/netstandard2.1/Microsoft.Extensions.Options.xml",
"microsoft.extensions.options.8.0.2.nupkg.sha512",
"microsoft.extensions.options.10.0.2.nupkg.sha512",
"microsoft.extensions.options.nuspec",
"useSharedDesignerContext.txt"
]
},
"Microsoft.Extensions.Primitives/8.0.0": {
"sha512": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==",
"Microsoft.Extensions.Primitives/10.0.2": {
"sha512": "QmSiO+oLBEooGgB3i0GRXyeYRDHjllqt3k365jwfZlYWhvSHA3UL2NEVV5m8aZa041eIlblo6KMI5txvTMpTwA==",
"type": "package",
"path": "microsoft.extensions.primitives/8.0.0",
"path": "microsoft.extensions.primitives/10.0.2",
"files": [
".nupkg.metadata",
".signature.p7s",
"Icon.png",
"LICENSE.TXT",
"PACKAGE.md",
"THIRD-PARTY-NOTICES.TXT",
"buildTransitive/net461/Microsoft.Extensions.Primitives.targets",
"buildTransitive/net462/_._",
"buildTransitive/net6.0/_._",
"buildTransitive/net8.0/_._",
"buildTransitive/netcoreapp2.0/Microsoft.Extensions.Primitives.targets",
"lib/net10.0/Microsoft.Extensions.Primitives.dll",
"lib/net10.0/Microsoft.Extensions.Primitives.xml",
"lib/net462/Microsoft.Extensions.Primitives.dll",
"lib/net462/Microsoft.Extensions.Primitives.xml",
"lib/net6.0/Microsoft.Extensions.Primitives.dll",
"lib/net6.0/Microsoft.Extensions.Primitives.xml",
"lib/net7.0/Microsoft.Extensions.Primitives.dll",
"lib/net7.0/Microsoft.Extensions.Primitives.xml",
"lib/net8.0/Microsoft.Extensions.Primitives.dll",
"lib/net8.0/Microsoft.Extensions.Primitives.xml",
"lib/net9.0/Microsoft.Extensions.Primitives.dll",
"lib/net9.0/Microsoft.Extensions.Primitives.xml",
"lib/netstandard2.0/Microsoft.Extensions.Primitives.dll",
"lib/netstandard2.0/Microsoft.Extensions.Primitives.xml",
"microsoft.extensions.primitives.8.0.0.nupkg.sha512",
"microsoft.extensions.primitives.10.0.2.nupkg.sha512",
"microsoft.extensions.primitives.nuspec",
"useSharedDesignerContext.txt"
]
@ -5036,6 +5121,24 @@
"rabbitmq.client.nuspec"
]
},
"RedLock.net/2.3.2": {
"sha512": "jlrALAArm4dCE292U3EtRoMnVKJ9M6sunbZn/oG5OuzlGtTpusXBfvDrnGWbgGDlWV027f5E9H5CiVnPxiq8+g==",
"type": "package",
"path": "redlock.net/2.3.2",
"files": [
".nupkg.metadata",
".signature.p7s",
"lib/net461/RedLockNet.Abstractions.dll",
"lib/net461/RedLockNet.SERedis.dll",
"lib/net472/RedLockNet.Abstractions.dll",
"lib/net472/RedLockNet.SERedis.dll",
"lib/netstandard2.0/RedLockNet.Abstractions.dll",
"lib/netstandard2.0/RedLockNet.SERedis.dll",
"redlock-icon.png",
"redlock.net.2.3.2.nupkg.sha512",
"redlock.net.nuspec"
]
},
"runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.0": {
"sha512": "HdSSp5MnJSsg08KMfZThpuLPJpPwE5hBXvHwoKWosyHHfe8Mh5WKT0ylEOf6yNzX6Ngjxe4Whkafh5q7Ymac4Q==",
"type": "package",
@ -5703,25 +5806,32 @@
"system.diagnostics.debug.nuspec"
]
},
"System.Diagnostics.DiagnosticSource/4.3.0": {
"sha512": "tD6kosZnTAGdrEa0tZSuFyunMbt/5KYDnHdndJYGqZoNy00XVXyACd5d6KnE1YgYv3ne2CjtAfNXo/fwEhnKUA==",
"System.Diagnostics.DiagnosticSource/10.0.2": {
"sha512": "lYWBy8fKkJHaRcOuw30d67PrtVjR5754sz5Wl76s8P+vJ9FSThh9b7LIcTSODx1LY7NB3Srvg+JMnzd67qNZOw==",
"type": "package",
"path": "system.diagnostics.diagnosticsource/4.3.0",
"path": "system.diagnostics.diagnosticsource/10.0.2",
"files": [
".nupkg.metadata",
".signature.p7s",
"ThirdPartyNotices.txt",
"dotnet_library_license.txt",
"lib/net46/System.Diagnostics.DiagnosticSource.dll",
"lib/net46/System.Diagnostics.DiagnosticSource.xml",
"lib/netstandard1.1/System.Diagnostics.DiagnosticSource.dll",
"lib/netstandard1.1/System.Diagnostics.DiagnosticSource.xml",
"lib/netstandard1.3/System.Diagnostics.DiagnosticSource.dll",
"lib/netstandard1.3/System.Diagnostics.DiagnosticSource.xml",
"lib/portable-net45+win8+wpa81/System.Diagnostics.DiagnosticSource.dll",
"lib/portable-net45+win8+wpa81/System.Diagnostics.DiagnosticSource.xml",
"system.diagnostics.diagnosticsource.4.3.0.nupkg.sha512",
"system.diagnostics.diagnosticsource.nuspec"
"Icon.png",
"THIRD-PARTY-NOTICES.TXT",
"buildTransitive/net461/System.Diagnostics.DiagnosticSource.targets",
"buildTransitive/net462/_._",
"buildTransitive/net8.0/_._",
"buildTransitive/netcoreapp2.0/System.Diagnostics.DiagnosticSource.targets",
"lib/net10.0/System.Diagnostics.DiagnosticSource.dll",
"lib/net10.0/System.Diagnostics.DiagnosticSource.xml",
"lib/net462/System.Diagnostics.DiagnosticSource.dll",
"lib/net462/System.Diagnostics.DiagnosticSource.xml",
"lib/net8.0/System.Diagnostics.DiagnosticSource.dll",
"lib/net8.0/System.Diagnostics.DiagnosticSource.xml",
"lib/net9.0/System.Diagnostics.DiagnosticSource.dll",
"lib/net9.0/System.Diagnostics.DiagnosticSource.xml",
"lib/netstandard2.0/System.Diagnostics.DiagnosticSource.dll",
"lib/netstandard2.0/System.Diagnostics.DiagnosticSource.xml",
"system.diagnostics.diagnosticsource.10.0.2.nupkg.sha512",
"system.diagnostics.diagnosticsource.nuspec",
"useSharedDesignerContext.txt"
]
},
"System.Diagnostics.EventLog/6.0.0": {

View File

@ -1,6 +1,6 @@
{
"version": 2,
"dgSpecHash": "tVGTA3KwBHQ=",
"dgSpecHash": "ueA0djhC8vQ=",
"success": true,
"projectFilePath": "C:\\Users\\nanxun\\Documents\\IM\\backend\\IMTest\\IMTest.csproj",
"expectedPackageFiles": [
@ -32,6 +32,7 @@
"C:\\Users\\nanxun\\.nuget\\packages\\microsoft.aspnetcore.signalr.protocols.json\\1.2.0\\microsoft.aspnetcore.signalr.protocols.json.1.2.0.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\microsoft.aspnetcore.websockets\\2.3.0\\microsoft.aspnetcore.websockets.2.3.0.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\microsoft.aspnetcore.webutilities\\2.3.0\\microsoft.aspnetcore.webutilities.2.3.0.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\microsoft.bcl.asyncinterfaces\\1.1.0\\microsoft.bcl.asyncinterfaces.1.1.0.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\microsoft.codecoverage\\17.8.0\\microsoft.codecoverage.17.8.0.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\microsoft.csharp\\4.7.0\\microsoft.csharp.4.7.0.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\microsoft.entityframeworkcore\\8.0.22\\microsoft.entityframeworkcore.8.0.22.nupkg.sha512",
@ -40,21 +41,22 @@
"C:\\Users\\nanxun\\.nuget\\packages\\microsoft.entityframeworkcore.inmemory\\8.0.22\\microsoft.entityframeworkcore.inmemory.8.0.22.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\microsoft.entityframeworkcore.relational\\8.0.13\\microsoft.entityframeworkcore.relational.8.0.13.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\microsoft.extensions.apidescription.server\\6.0.5\\microsoft.extensions.apidescription.server.6.0.5.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\microsoft.extensions.caching.abstractions\\8.0.0\\microsoft.extensions.caching.abstractions.8.0.0.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\microsoft.extensions.caching.abstractions\\10.0.2\\microsoft.extensions.caching.abstractions.10.0.2.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\microsoft.extensions.caching.memory\\8.0.1\\microsoft.extensions.caching.memory.8.0.1.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\microsoft.extensions.caching.stackexchangeredis\\10.0.2\\microsoft.extensions.caching.stackexchangeredis.10.0.2.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\microsoft.extensions.configuration.abstractions\\8.0.0\\microsoft.extensions.configuration.abstractions.8.0.0.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\microsoft.extensions.dependencyinjection\\8.0.1\\microsoft.extensions.dependencyinjection.8.0.1.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\microsoft.extensions.dependencyinjection.abstractions\\8.0.2\\microsoft.extensions.dependencyinjection.abstractions.8.0.2.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\microsoft.extensions.dependencyinjection.abstractions\\10.0.2\\microsoft.extensions.dependencyinjection.abstractions.10.0.2.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\microsoft.extensions.diagnostics.abstractions\\8.0.1\\microsoft.extensions.diagnostics.abstractions.8.0.1.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\microsoft.extensions.diagnostics.healthchecks\\8.0.0\\microsoft.extensions.diagnostics.healthchecks.8.0.0.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\microsoft.extensions.diagnostics.healthchecks.abstractions\\8.0.0\\microsoft.extensions.diagnostics.healthchecks.abstractions.8.0.0.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\microsoft.extensions.fileproviders.abstractions\\8.0.0\\microsoft.extensions.fileproviders.abstractions.8.0.0.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\microsoft.extensions.hosting.abstractions\\8.0.1\\microsoft.extensions.hosting.abstractions.8.0.1.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\microsoft.extensions.logging\\8.0.1\\microsoft.extensions.logging.8.0.1.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\microsoft.extensions.logging.abstractions\\8.0.2\\microsoft.extensions.logging.abstractions.8.0.2.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\microsoft.extensions.logging.abstractions\\10.0.2\\microsoft.extensions.logging.abstractions.10.0.2.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\microsoft.extensions.objectpool\\8.0.11\\microsoft.extensions.objectpool.8.0.11.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\microsoft.extensions.options\\8.0.2\\microsoft.extensions.options.8.0.2.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\microsoft.extensions.primitives\\8.0.0\\microsoft.extensions.primitives.8.0.0.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\microsoft.extensions.options\\10.0.2\\microsoft.extensions.options.10.0.2.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\microsoft.extensions.primitives\\10.0.2\\microsoft.extensions.primitives.10.0.2.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\microsoft.identitymodel.abstractions\\8.14.0\\microsoft.identitymodel.abstractions.8.14.0.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\microsoft.identitymodel.jsonwebtokens\\8.14.0\\microsoft.identitymodel.jsonwebtokens.8.14.0.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\microsoft.identitymodel.logging\\8.14.0\\microsoft.identitymodel.logging.8.14.0.nupkg.sha512",
@ -78,6 +80,7 @@
"C:\\Users\\nanxun\\.nuget\\packages\\pipelines.sockets.unofficial\\2.2.8\\pipelines.sockets.unofficial.2.2.8.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\pomelo.entityframeworkcore.mysql\\8.0.3\\pomelo.entityframeworkcore.mysql.8.0.3.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\rabbitmq.client\\7.1.2\\rabbitmq.client.7.1.2.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\redlock.net\\2.3.2\\redlock.net.2.3.2.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\runtime.debian.8-x64.runtime.native.system.security.cryptography.openssl\\4.3.0\\runtime.debian.8-x64.runtime.native.system.security.cryptography.openssl.4.3.0.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\runtime.fedora.23-x64.runtime.native.system.security.cryptography.openssl\\4.3.0\\runtime.fedora.23-x64.runtime.native.system.security.cryptography.openssl.4.3.0.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\runtime.fedora.24-x64.runtime.native.system.security.cryptography.openssl\\4.3.0\\runtime.fedora.24-x64.runtime.native.system.security.cryptography.openssl.4.3.0.nupkg.sha512",
@ -105,7 +108,7 @@
"C:\\Users\\nanxun\\.nuget\\packages\\system.collections.concurrent\\4.3.0\\system.collections.concurrent.4.3.0.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\system.console\\4.3.0\\system.console.4.3.0.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\system.diagnostics.debug\\4.3.0\\system.diagnostics.debug.4.3.0.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\system.diagnostics.diagnosticsource\\4.3.0\\system.diagnostics.diagnosticsource.4.3.0.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\system.diagnostics.diagnosticsource\\10.0.2\\system.diagnostics.diagnosticsource.10.0.2.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\system.diagnostics.eventlog\\6.0.0\\system.diagnostics.eventlog.6.0.0.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\system.diagnostics.tools\\4.3.0\\system.diagnostics.tools.4.3.0.nupkg.sha512",
"C:\\Users\\nanxun\\.nuget\\packages\\system.diagnostics.tracing\\4.3.0\\system.diagnostics.tracing.4.3.0.nupkg.sha512",

View File

@ -26,10 +26,10 @@ namespace IM_API.Application.EventHandlers.FriendAddHandler
{
ChatType = ChatType.PRIVATE,
Content = "您有新的好友关系已添加",
MsgId = @event.EventId.ToString(),
//MsgId = @event.EventId.ToString(),
ReceiverId = @event.ResponseUserId,
SenderId = @event.RequestUserId,
TimeStamp = DateTime.UtcNow
TimeStamp = DateTime.Now
});
await _chathub.Clients.Users(usersList).SendAsync("ReceiveMessage", res);
}

View File

@ -35,28 +35,15 @@ namespace IM_API.Application.EventHandlers.MessageCreatedHandler
public async Task Consume(ConsumeContext<MessageCreatedEvent> context)
{
var @event = context.Message;
if (@event.ChatType == ChatType.PRIVATE)
await _conversationService.UpdateConversationAfterSentAsync(new Dtos.Conversation.UpdateConversationDto
{
Conversation? userAConversation = await _context.Conversations.FirstOrDefaultAsync(
x => x.UserId == @event.MsgSenderId && x.TargetId == @event.MsgRecipientId
);
Conversation? userBConversation = await _context.Conversations.FirstOrDefaultAsync(
x => x.UserId == @event.MsgRecipientId && x.TargetId == @event.MsgSenderId
);
if (userAConversation is null || userBConversation is null)
{
_logger.LogError("消息事件更新会话信息失败:{@event}", @event);
}
userAConversation.LastMessage = @event.MessageContent;
userAConversation.LastReadMessageId = @event.MessageId;
userAConversation.LastMessageTime = @event.MessageCreated;
userBConversation.LastMessage = @event.MessageContent;
userBConversation.UnreadCount += 1;
userBConversation.LastMessageTime = @event.MessageCreated;
_context.UpdateRange(userAConversation, userBConversation);
await _context.SaveChangesAsync();
}
LastMessage = @event.MessageContent,
LastSequenceId = @event.SequenceId,
ReceiptId = @event.MsgRecipientId,
SenderId = @event.MsgSenderId,
StreamKey = @event.StreamKey,
DateTime = @event.MessageCreated
});
}
}

View File

@ -0,0 +1,26 @@
using IM_API.Domain.Events;
using MassTransit;
using IM_API.Interface.Services;
using AutoMapper;
using IM_API.Models;
namespace IM_API.Application.EventHandlers.MessageCreatedHandler
{
public class MessageCreatedDbHandler : IConsumer<MessageCreatedEvent>
{
private readonly IMessageSevice _messageService;
public readonly IMapper _mapper;
public MessageCreatedDbHandler(IMessageSevice messageSevice, IMapper mapper)
{
_messageService = messageSevice;
_mapper = mapper;
}
public async Task Consume(ConsumeContext<MessageCreatedEvent> context)
{
var @event = context.Message;
var msg = _mapper.Map<Message>(@event);
await _messageService.MakeMessageAsync(msg);
}
}
}

View File

@ -5,6 +5,7 @@ using IM_API.Dtos;
using IM_API.Hubs;
using IM_API.Models;
using IM_API.Tools;
using IM_API.VOs.Message;
using MassTransit;
using Microsoft.AspNetCore.SignalR;
@ -22,21 +23,31 @@ namespace IM_API.Application.EventHandlers.MessageCreatedHandler
public async Task Consume(ConsumeContext<MessageCreatedEvent> context)
{
Console.ForegroundColor = ConsoleColor.Red;
var @event = context.Message;
if (@event.ChatType == Models.ChatType.PRIVATE)
Console.WriteLine($"[SignalR]handlerCreated!");
try
{
MessageBaseDto messageBaseDto = new MessageBaseDto
{
MsgId = @event.MessageId.ToString(),
ChatType = @event.ChatType,
Content = @event.MessageContent,
GroupMemberId = null,
ReceiverId = @event.MsgRecipientId,
SenderId = @event.MsgSenderId,
TimeStamp = @event.MessageCreated,
Type = @event.MessageMsgType
};
await _hub.Clients.Users(@event.MsgRecipientId.ToString()).SendAsync("ReceiveMessage", messageBaseDto);
// 先转成实体,如果这一步都报错,说明之前的 sbyte -> enum 还没改好
var entity = _mapper.Map<Message>(@event);
// 再从实体转 VO这是最稳妥的路径因为这两者的映射你肯定配过了
var messageBaseVo = _mapper.Map<MessageBaseVo>(entity);
// 2. 打印日志确认逻辑执行到这里了
Console.WriteLine($"[SignalR] 准备向所有人广播消息: {messageBaseVo.Content}");
// 3. 执行广播
await _hub.Clients.User(@event.MsgRecipientId.ToString()).SendAsync("ReceiveMessage", new HubResponse<MessageBaseVo>("Event", messageBaseVo));
Console.WriteLine("[SignalR] 广播指令已发出");
Console.ResetColor();
}
catch (Exception ex)
{
Console.WriteLine($"[SignalR] 发送失败: {ex.Message}");
Console.ResetColor();
throw; // 抛出异常触发 MassTransit 重试
}
}
}

View File

@ -1,13 +1,16 @@
using IM_API.Application.EventHandlers.FriendAddHandler;
using AutoMapper;
using IM_API.Application.EventHandlers.FriendAddHandler;
using IM_API.Application.EventHandlers.MessageCreatedHandler;
using IM_API.Application.EventHandlers.RequestFriendHandler;
using IM_API.Configs.Options;
using MassTransit;
using MySqlConnector;
namespace IM_API.Configs
{
public static class MQConfig
{
public static IServiceCollection AddRabbitMQ(this IServiceCollection services, RabbitMqOptions options)
public static IServiceCollection AddRabbitMQ(this IServiceCollection services, RabbitMQOptions options)
{
services.AddMassTransit(x =>
{
@ -17,7 +20,7 @@ namespace IM_API.Configs
x.AddConsumer<FriendAddSignalRHandler>();
x.AddConsumer<RequestFriendSignalRHandler>();
x.AddConsumer<FriendAddConversationHandler>();
x.AddConsumer<MessageCreatedDbHandler>();
x.UsingRabbitMq((ctx,cfg) =>
{
cfg.Host(options.Host, "/", h =>
@ -26,7 +29,15 @@ namespace IM_API.Configs
h.Password(options.Password);
});
cfg.ConfigureEndpoints(ctx);
});
cfg.UseMessageRetry(r =>
{
r.Handle<IOException>();
r.Handle<MySqlException>();
r.Ignore<AutoMapperMappingException>();
r.Exponential(5, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(2));
});
cfg.ConfigureEndpoints(ctx);
});
});
@ -34,11 +45,5 @@ namespace IM_API.Configs
}
}
public class RabbitMqOptions
{
public string Host { get; set; }
public int Port { get; set; }
public string Username { get; set; }
public string Password { get;set; }
}
}

View File

@ -2,11 +2,13 @@
using IM_API.Domain.Events;
using IM_API.Dtos;
using IM_API.Dtos.Auth;
using IM_API.Dtos.Conversation;
using IM_API.Dtos.Friend;
using IM_API.Dtos.Group;
using IM_API.Dtos.User;
using IM_API.Models;
using IM_API.Tools;
using IM_API.VOs.Conversation;
using IM_API.VOs.Message;
namespace IM_API.Configs
{
@ -41,13 +43,13 @@ namespace IM_API.Configs
.ForMember(dest => dest.FriendId , opt => opt.MapFrom(src => src.ToUserId))
.ForMember(dest => dest.StatusEnum , opt =>opt.MapFrom(src => FriendStatus.Pending))
.ForMember(dest => dest.RemarkName , opt => opt.MapFrom(src => src.RemarkName))
.ForMember(dest => dest.Created , opt => opt.MapFrom(src => DateTime.UtcNow))
.ForMember(dest => dest.Created , opt => opt.MapFrom(src => DateTime.Now))
;
//发起好友请求转换请求对象
CreateMap<FriendRequestDto, FriendRequest>()
.ForMember(dest => dest.RequestUser , opt => opt.MapFrom(src => src.FromUserId))
.ForMember(dest => dest.ResponseUser , opt => opt.MapFrom(src => src.ToUserId))
.ForMember(dest => dest.Created , opt => opt.MapFrom(src => DateTime.UtcNow))
.ForMember(dest => dest.Created , opt => opt.MapFrom(src => DateTime.Now))
.ForMember(dest => dest.StateEnum , opt => opt.MapFrom(src => FriendRequestState.Pending))
.ForMember(dest => dest.Description , opt => opt.MapFrom(src => src.Description))
;
@ -59,15 +61,15 @@ namespace IM_API.Configs
.ForMember(dest => dest.Description, opt => opt.MapFrom(src => src.Description))
;
//消息模型转换
CreateMap<Message, MessageBaseDto>()
.ForMember(dest => dest.Type , opt => opt.MapFrom(src => src.MsgTypeEnum.ToString()))
.ForMember(dest => dest.MsgId , opt => opt.MapFrom(src => src.Id))
CreateMap<Message, MessageBaseVo>()
.ForMember(dest => dest.Type , opt => opt.MapFrom(src => src.MsgTypeEnum))
.ForMember(dest => dest.MsgId , opt => opt.MapFrom(src => src.ClientMsgId))
.ForMember(dest => dest.SenderId , opt => opt.MapFrom(src => src.Sender))
.ForMember(dest => dest.ChatType , opt => opt.MapFrom(src => src.ChatTypeEnum.ToString()))
.ForMember(dest => dest.ChatType , opt => opt.MapFrom(src => src.ChatTypeEnum))
.ForMember(dest => dest.ReceiverId, opt => opt.MapFrom(src => src.Recipient))
.ForMember(dest => dest.Content, opt => opt.MapFrom(src => src.Content))
.ForMember(dest => dest.TimeStamp, opt => opt.MapFrom(src => src.Created))
.ForMember(dest => dest.GroupMemberId , opt => opt.MapFrom(src => src.GroupMemberId))
.ForMember(dest => dest.SequenceId, opt => opt.MapFrom(src => src.SequenceId))
;
CreateMap<MessageBaseDto, Message>()
.ForMember(dest => dest.Sender, opt => opt.MapFrom(src => src.SenderId))
@ -78,9 +80,9 @@ namespace IM_API.Configs
.ForMember(dest => dest.Recipient, opt => opt.MapFrom(src => src.ReceiverId))
.ForMember(dest => dest.StreamKey, opt => opt.Ignore() )
.ForMember(dest => dest.StateEnum, opt => opt.MapFrom(src => MessageState.Sent))
.ForMember(dest => dest.GroupMemberId, opt => opt.MapFrom(src => src.GroupMemberId))
.ForMember(dest => dest.ChatType, opt => opt.Ignore())
.ForMember(dest => dest.MsgType, opt => opt.Ignore())
.ForMember(dest => dest.ClientMsgId, opt => opt.MapFrom(src => src.MsgId))
;
//会话对象深拷贝
@ -99,41 +101,58 @@ namespace IM_API.Configs
.ForMember(dest => dest.MessageContent, opt => opt.MapFrom(src => src.Content))
.ForMember(dest => dest.State, opt => opt.MapFrom(src => src.StateEnum))
.ForMember(dest => dest.MessageCreated, opt => opt.MapFrom(src => src.Created))
.ForMember(dest => dest.MessageId, opt => opt.MapFrom(src => src.Id))
.ForMember(dest => dest.MsgRecipientId, opt => opt.MapFrom(src => src.Recipient))
.ForMember(dest => dest.MsgSenderId, opt => opt.MapFrom(src => src.Sender))
.ForMember(dest => dest.EventId, opt => opt.MapFrom(src => Guid.NewGuid()))
.ForMember(dest => dest.AggregateId, opt => opt.MapFrom(src => src.Sender.ToString()))
.ForMember(dest => dest.OccurredAt , opt => opt.MapFrom(src => DateTime.Now))
.ForMember(dest => dest.OperatorId, opt => opt.MapFrom(src => src.Sender))
.ForMember(dest => dest.StreamKey, opt => opt.MapFrom(src => src.StreamKey))
;
CreateMap<MessageCreatedEvent, Message>()
.ForMember(dest => dest.SequenceId, opt => opt.MapFrom(src => src.SequenceId))
.ForMember(dest => dest.ClientMsgId, opt => opt.MapFrom(src => src.ClientMsgId))
.ForMember(dest => dest.StateEnum, opt => opt.MapFrom(src => src.State))
.ForMember(dest => dest.ChatTypeEnum, opt => opt.MapFrom(src => src.ChatType))
.ForMember(dest => dest.Content, opt => opt.MapFrom(src => src.MessageContent))
.ForMember(dest => dest.Created, opt => opt.MapFrom(src => src.MessageCreated))
.ForMember(dest => dest.MsgTypeEnum, opt => opt.MapFrom(src => src.MessageMsgType))
.ForMember(dest => dest.Recipient, opt => opt.MapFrom(src => src.MsgRecipientId))
.ForMember(dest => dest.Sender, opt => opt.MapFrom(src => src.MsgSenderId))
.ForMember(dest => dest.StreamKey, opt => opt.MapFrom(src => src.StreamKey))
.ForMember(dest => dest.ChatType, opt => opt.Ignore())
.ForMember(dest => dest.State, opt => opt.Ignore())
.ForMember(dest => dest.MsgType, opt => opt.Ignore());
//消息发送事件转换会话对象
CreateMap<MessageCreatedEvent, Conversation>()
.ForMember(dest => dest.LastReadMessageId, opt => opt.MapFrom(src => src.MessageId))
//.ForMember(dest => dest.LastReadMessageId, opt => opt.MapFrom(src => src.MessageId))
.ForMember(dest => dest.LastMessage, opt => opt.MapFrom(src => src.MessageContent))
.ForMember(dest => dest.ChatType, opt => opt.MapFrom(src => (int)src.ChatType))
.ForMember(dest => dest.ChatType, opt => opt.MapFrom(src => src.ChatType))
.ForMember(dest => dest.UserId, opt => opt.MapFrom(src => src.MsgSenderId))
.ForMember(dest => dest.TargetId, opt => opt.MapFrom(src => src.MsgRecipientId))
.ForMember(dest => dest.UnreadCount, opt => opt.MapFrom(src => 0))
.ForMember(dest => dest.StreamKey, opt => opt.MapFrom(src => src.StreamKey))
.ForMember(dest => dest.LastMessageTime, opt => opt.MapFrom(src => DateTime.UtcNow))
.ForMember(dest => dest.LastMessageTime, opt => opt.MapFrom(src => DateTime.Now))
;
//创建会话对象
CreateMap<Conversation, ConversationDto>()
CreateMap<Conversation, ConversationVo>()
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id))
.ForMember(dest => dest.LastMessage, opt => opt.MapFrom(src => src.LastMessage))
.ForMember(dest => dest.LastReadMessage, opt => opt.MapFrom(src => src.LastReadMessage))
.ForMember(dest => dest.LastReadMessageId, opt => opt.MapFrom(src => src.LastReadMessageId))
.ForMember(dest => dest.ChatType, opt => opt.MapFrom(src => src.ChatType))
.ForMember(dest => dest.LastSequenceId, opt => opt.MapFrom(src => src.LastReadSequenceId))
.ForMember(dest => dest.ChatType, opt => opt.MapFrom(src => src.ChatTypeEnum))
.ForMember(dest => dest.DateTime, opt => opt.MapFrom(src => src.LastMessageTime))
.ForMember(dest => dest.TargetId, opt => opt.MapFrom(src => src.TargetId))
.ForMember(dest => dest.UnreadCount, opt => opt.MapFrom(src => src.UnreadCount))
.ForMember(dest => dest.UserId, opt => opt.MapFrom(src => src.UserId));
CreateMap<Friend, ConversationDto>()
.ForMember(dest => dest.TargetAvatar, opt => opt.MapFrom(src => src.Avatar))
CreateMap<Friend, ConversationVo>()
.ForMember(dest => dest.TargetAvatar, opt => opt.MapFrom(src => src.FriendNavigation.Avatar))
.ForMember(dest => dest.TargetName, opt => opt.MapFrom(src => src.RemarkName));
CreateMap<Group, ConversationDto>()
CreateMap<Group, ConversationVo>()
.ForMember(dest => dest.TargetAvatar, opt => opt.MapFrom(src => src.Avatar))
.ForMember(dest => dest.TargetName, opt => opt.MapFrom(src => src.Name));
@ -147,10 +166,11 @@ namespace IM_API.Configs
CreateMap<GroupCreateDto, Group>()
.ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Name))
.ForMember(dest => dest.Avatar, opt => opt.MapFrom(src => src.Avatar))
.ForMember(dest => dest.Created, opt => opt.MapFrom(src => DateTime.UtcNow))
.ForMember(dest => dest.Created, opt => opt.MapFrom(src => DateTime.Now))
.ForMember(dest => dest.AllMembersBannedEnum, opt => opt.MapFrom(src => GroupAllMembersBanned.ALLOWED))
.ForMember(dest => dest.AuhorityEnum, opt => opt.MapFrom(src => GroupAuhority.REQUIRE_CONSENT))
.ForMember(dest => dest.StatusEnum, opt => opt.MapFrom(src => GroupStatus.Normal));
.ForMember(dest => dest.StatusEnum, opt => opt.MapFrom(src => GroupStatus.Normal))
;
}
}
}

View File

@ -0,0 +1,8 @@
namespace IM_API.Configs.Options
{
public class ConnectionOptions
{
public string DefaultConnection { get; set; }
public string Redis { get; set; }
}
}

View File

@ -0,0 +1,10 @@
namespace IM_API.Configs.Options
{
public class RabbitMQOptions
{
public string Host { get; set; }
public int Port { get; set; }
public string Username { get; set; }
public string Password { get; set; }
}
}

View File

@ -8,6 +8,10 @@ using IM_API.Services;
using IM_API.Tools;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using RedLockNet;
using RedLockNet.SERedis;
using RedLockNet.SERedis.Configuration;
using StackExchange.Redis;
namespace IM_API.Configs
{
@ -17,14 +21,23 @@ namespace IM_API.Configs
{
services.AddAutoMapper(typeof(MapperConfig));
services.AddTransient<IAuthService, AuthService>();
services.AddTransient<IUserService, UserService>();
services.AddTransient<IFriendSerivce, FriendService>();
services.AddTransient<IMessageSevice, MessageService>();
services.AddTransient<IConversationService, ConversationService>();
services.AddScoped<IAuthService, AuthService>();
services.AddScoped<IUserService, UserService>();
services.AddScoped<IFriendSerivce, FriendService>();
services.AddScoped<IMessageSevice, MessageService>();
services.AddScoped<IConversationService, ConversationService>();
services.AddScoped<IGroupService, GroupService>();
services.AddScoped<ISequenceIdService, SequenceIdService>();
services.AddScoped<ICacheService, RedisCacheService>();
services.AddScoped<IEventBus, InMemoryEventBus>();
services.AddSingleton<IJWTService, JWTService>();
services.AddSingleton<IRefreshTokenService, RedisRefreshTokenService>();
services.AddSingleton<IDistributedLockFactory>(sp =>
{
var connection = sp.GetRequiredService<IConnectionMultiplexer>();
// 这里可以配置多个 Redis 节点提高安全性,单机运行传一个即可
return RedLockFactory.Create(new List<RedLockMultiplexer> { new RedLockMultiplexer(connection) });
});
return services;
}
public static IServiceCollection AddModelValidation(this IServiceCollection services, IConfiguration configuration)
@ -40,7 +53,7 @@ namespace IM_API.Configs
Field = e.Key,
Message = e.Value.Errors.First().ErrorMessage
});
Console.WriteLine(errors);
return new BadRequestObjectResult(new BaseResponse<object?>(CodeDefine.PARAMETER_ERROR.Code, errors.First().Message));
};
});

View File

@ -4,6 +4,7 @@ using IM_API.Dtos.Auth;
using IM_API.Dtos.User;
using IM_API.Interface.Services;
using IM_API.Tools;
using IM_API.VOs.Auth;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
@ -44,7 +45,7 @@ namespace IM_API.Controllers
(string token,DateTime expiresAt) = _jwtService.CreateAccessTokenForUser(user.Id,user.Username,"user");
//生成刷新凭证
string refreshToken = await _refreshTokenService.CreateRefreshTokenAsync(user.Id);
var res = new BaseResponse<LoginDto>(new LoginDto(userInfo,token,refreshToken, expiresAt));
var res = new BaseResponse<LoginVo>(new LoginVo(userInfo,token,refreshToken, expiresAt));
return Ok(res);
}
[HttpPost]
@ -55,18 +56,18 @@ namespace IM_API.Controllers
return Ok(res);
}
[HttpPost]
[ProducesResponseType(typeof(BaseResponse<LoginDto>),StatusCodes.Status200OK)]
[ProducesResponseType(typeof(BaseResponse<LoginVo>),StatusCodes.Status200OK)]
public async Task<IActionResult> Refresh(RefreshDto dto)
{
(bool ok,int userId) = await _refreshTokenService.ValidateRefreshTokenAsync(dto.refreshToken);
if (!ok)
{
var err = new BaseResponse<LoginDto>(CodeDefine.AUTH_FAILED);
var err = new BaseResponse<LoginVo>(CodeDefine.AUTH_FAILED);
return Unauthorized(err);
}
var userinfo = await _userService.GetUserInfoAsync(userId);
(string token,DateTime expiresAt) = _jwtService.CreateAccessTokenForUser(userinfo.Id,userinfo.Username,"user");
var res = new BaseResponse<LoginDto>(new LoginDto(userinfo,token, dto.refreshToken, expiresAt));
var res = new BaseResponse<LoginVo>(new LoginVo(userinfo,token, dto.refreshToken, expiresAt));
return Ok(res);
}
}

View File

@ -1,7 +1,7 @@
using IM_API.Dtos;
using IM_API.Dtos.Conversation;
using IM_API.Interface.Services;
using IM_API.Models;
using IM_API.VOs.Conversation;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@ -26,7 +26,7 @@ namespace IM_API.Controllers
{
var userIdStr = User.FindFirstValue(ClaimTypes.NameIdentifier);
var list = await _conversationSerivice.GetConversationsAsync(int.Parse(userIdStr));
var res = new BaseResponse<List<ConversationDto>>(list);
var res = new BaseResponse<List<ConversationVo>>(list);
return Ok(res);
}
[HttpGet]
@ -34,7 +34,7 @@ namespace IM_API.Controllers
{
var userIdStr = User.FindFirstValue(ClaimTypes.NameIdentifier);
var conversation = await _conversationSerivice.GetConversationByIdAsync(int.Parse(userIdStr), conversationId);
var res = new BaseResponse<ConversationDto>(conversation);
var res = new BaseResponse<ConversationVo>(conversation);
return Ok(res);
}
[HttpPost]

View File

@ -0,0 +1,43 @@
using IM_API.Dtos;
using IM_API.Dtos.Group;
using IM_API.Interface.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
namespace IM_API.Controllers
{
[Authorize]
[Route("api/[controller]/[action]")]
[ApiController]
public class GroupController : ControllerBase
{
private readonly IGroupService _groupService;
private readonly ILogger<GroupController> _logger;
public GroupController(IGroupService groupService, ILogger<GroupController> logger)
{
_groupService = groupService;
_logger = logger;
}
[HttpGet]
[ProducesResponseType(typeof(BaseResponse<List<GroupInfoDto>>) ,StatusCodes.Status200OK)]
public async Task<IActionResult> GetGroups(int page = 1, int limit = 100, bool desc = false)
{
var userIdStr = User.FindFirstValue(ClaimTypes.NameIdentifier);
var list = await _groupService.GetGroupListAsync(int.Parse(userIdStr), page, limit, desc);
var res = new BaseResponse<List<GroupInfoDto>>(list);
return Ok(res);
}
[HttpPost]
[ProducesResponseType(typeof(BaseResponse<GroupInfoDto>), StatusCodes.Status200OK)]
public async Task<ActionResult> CreateGroup([FromBody]GroupCreateDto groupCreateDto)
{
var userIdStr = User.FindFirstValue(ClaimTypes.NameIdentifier);
var groupInfo = await _groupService.CreateGroupAsync(int.Parse(userIdStr), groupCreateDto);
var res = new BaseResponse<GroupInfoDto>(groupInfo);
return Ok(res);
}
}
}

View File

@ -1,5 +1,9 @@
using IM_API.Dtos;
using IM_API.Application.Interfaces;
using IM_API.Domain.Events;
using IM_API.Dtos;
using IM_API.Dtos.Message;
using IM_API.Interface.Services;
using IM_API.VOs.Message;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@ -15,31 +19,36 @@ namespace IM_API.Controllers
{
private readonly IMessageSevice _messageService;
private readonly ILogger<MessageController> _logger;
public MessageController(IMessageSevice messageService, ILogger<MessageController> logger)
private readonly IEventBus _eventBus;
public MessageController(IMessageSevice messageService, ILogger<MessageController> logger, IEventBus eventBus)
{
_messageService = messageService;
_logger = logger;
_eventBus = eventBus;
}
[HttpPost]
public async Task<IActionResult> SendPrivateMessage(MessageBaseDto dto)
[ProducesResponseType(typeof(BaseResponse<MessageBaseVo>), StatusCodes.Status200OK)]
public async Task<IActionResult> SendMessage(MessageBaseDto dto)
{
var userIdstr = User.FindFirstValue(ClaimTypes.NameIdentifier);
await _messageService.SendPrivateMessageAsync(int.Parse(userIdstr), dto.ReceiverId, dto);
return Ok(new BaseResponse<object?>());
}
[HttpPost]
public async Task<IActionResult> SendGroupMessage(MessageBaseDto dto)
{
var userIdstr = User.FindFirstValue(ClaimTypes.NameIdentifier);
await _messageService.SendGroupMessageAsync(int.Parse(userIdstr), dto.ReceiverId, dto);
return Ok(new BaseResponse<object?>());
MessageBaseVo messageBaseVo = new MessageBaseVo();
if(dto.ChatType == Models.ChatType.PRIVATE)
{
messageBaseVo = await _messageService.SendPrivateMessageAsync(int.Parse(userIdstr), dto.ReceiverId, dto);
}
else
{
messageBaseVo = await _messageService.SendGroupMessageAsync(int.Parse(userIdstr), dto.ReceiverId, dto);
}
return Ok(new BaseResponse<MessageBaseVo>(messageBaseVo));
}
[HttpGet]
public async Task<IActionResult> GetMessageList([Required]int conversationId, int? msgId, int? pageSize)
[ProducesResponseType(typeof(BaseResponse<List<MessageBaseVo>>), StatusCodes.Status200OK)]
public async Task<IActionResult> GetMessageList([FromQuery]MessageQueryDto dto)
{
var userIdStr = User.FindFirstValue(ClaimTypes.NameIdentifier);
var msgList = await _messageService.GetMessagesAsync(int.Parse(userIdStr), conversationId, msgId, pageSize, false);
var res = new BaseResponse<List<MessageBaseDto>>(msgList);
var msgList = await _messageService.GetMessagesAsync(int.Parse(userIdStr),dto);
var res = new BaseResponse<List<MessageBaseVo>>(msgList);
return Ok(res);
}
}

View File

@ -5,7 +5,7 @@ namespace IM_API.Domain.Events
public abstract record DomainEvent: IEvent
{
public Guid EventId { get; init; } = Guid.NewGuid();
public DateTime OccurredAt { get; init; } = DateTime.UtcNow;
public DateTimeOffset OccurredAt { get; init; } = DateTime.Now;
public long OperatorId { get; init; }
public string AggregateId { get; init; } = "";
public abstract string EventType { get; }

View File

@ -19,7 +19,7 @@ namespace IM_API.Domain.Events
/// <summary>
/// 好友关系创建时间
/// </summary>
public DateTime Created { get; init; }
public DateTimeOffset Created { get; init; }
}
}

View File

@ -8,13 +8,14 @@ namespace IM_API.Domain.Events
public override string EventType => "IM.MESSAGE.MESSAGE_CREATED";
public ChatType ChatType { get; set; }
public MessageMsgType MessageMsgType { get; set; }
public int MessageId { get; set; }
public long SequenceId { get; set; }
public string MessageContent { get; set; }
public int MsgSenderId { get; set; }
public int MsgRecipientId { get; set; }
public MessageState State { get; set; }
public DateTime MessageCreated { get; set; }
public DateTimeOffset MessageCreated { get; set; }
public string StreamKey { get; set; }
public Guid ClientMsgId { get; set; }

View File

@ -0,0 +1,12 @@
namespace IM_API.Dtos.Conversation
{
public class UpdateConversationDto
{
public string StreamKey { get; set; }
public int SenderId { get; set; }
public int ReceiptId { get; set; }
public string LastMessage { get; set; }
public long LastSequenceId { get; set; }
public DateTimeOffset DateTime { get; set; }
}
}

View File

@ -14,7 +14,7 @@ namespace IM_API.Dtos.Friend
public FriendStatus StatusEnum { get; init; }
public DateTime Created { get; init; }
public DateTimeOffset Created { get; init; }
public string RemarkName { get; init; } = string.Empty;

View File

@ -21,7 +21,7 @@ namespace IM_API.Dtos.Friend
/// <summary>
/// 申请时间
/// </summary>
public DateTime Created { get; set; }
public DateTimeOffset Created { get; set; }
/// <summary>
/// 申请附言

View File

@ -1,4 +1,5 @@
using IM_API.Models;
using System.ComponentModel.DataAnnotations;
namespace IM_API.Dtos.Group
{
@ -7,11 +8,15 @@ namespace IM_API.Dtos.Group
/// <summary>
/// 群聊名称
/// </summary>
[Required(ErrorMessage = "群名称必填")]
[MaxLength(20, ErrorMessage = "群名称不能大于20字符")]
public string Name { get; set; } = null!;
/// <summary>
/// 群头像
/// </summary>
[Required(ErrorMessage = "群头像必填")]
public string Avatar { get; set; } = null!;
public List<int>? UserIDs { get; set; }
}
}

View File

@ -41,7 +41,7 @@ namespace IM_API.Dtos.Group
/// <summary>
/// 群聊创建时间
/// </summary>
public DateTime Created { get; set; }
public DateTimeOffset Created { get; set; }
/// <summary>
/// 群头像

View File

@ -14,6 +14,7 @@ namespace IM_API.Dtos
Code = CodeDefine.SUCCESS.Code;
Message = CodeDefine.SUCCESS.Message;
Type = HubResponseType.ActionStatus;
Method = method;
}
public HubResponse(string method,T data)
{
@ -21,6 +22,7 @@ namespace IM_API.Dtos
Message = CodeDefine.SUCCESS.Message;
Type = HubResponseType.ActionStatus;
Data = data;
Method = method;
}
public HubResponse(CodeDefine codedefine,string method)
{

View File

@ -0,0 +1,18 @@
using System.ComponentModel.DataAnnotations;
namespace IM_API.Dtos.Message
{
public class MessageQueryDto
{
[Required(ErrorMessage = "会话ID必填")]
public int ConversationId { get; set; }
// 锚点序号(如果为空,说明是第一次进聊天框,拉最新的)
public long? Cursor { get; set; }
// 查询方向0 - 查旧(Before), 1 - 查新(After)
public int Direction { get; set; } = 0;
public int Limit { get; set; } = 20;
}
}

View File

@ -7,12 +7,11 @@ namespace IM_API.Dtos
// 使用 { get; init; } 确保对象创建后不可修改,且支持无参构造
public MessageMsgType Type { get; init; } = default!;
public ChatType ChatType { get; init; } = default!;
public string? MsgId { get; init; }
public Guid MsgId { get; init; }
public int SenderId { get; init; }
public int ReceiverId { get; init; }
public int? GroupMemberId { get; init; }
public string Content { get; init; } = default!;
public DateTime TimeStamp { get; init; }
public DateTimeOffset TimeStamp { get; init; }
public MessageBaseDto() { }
}
}

View File

@ -32,7 +32,7 @@ namespace IM_API.Dtos.User
/// <summary>
/// 创建时间
/// </summary>
public DateTime Created { get; set; }
public DateTimeOffset Created { get; set; }
/// <summary>
/// 账户状态

View File

@ -18,6 +18,7 @@ namespace IM_API.Filters
{
if(context.Exception is BaseException)
{
_logger.LogWarning(context.Exception, context.Exception.Message);
var exception = (BaseException)context.Exception;
BaseResponse<object?> res = new BaseResponse<object?>(
code:exception.Code,

View File

@ -5,6 +5,7 @@ using IM_API.Dtos;
using IM_API.Interface.Services;
using IM_API.Models;
using IM_API.Tools;
using IM_API.VOs.Message;
using Microsoft.AspNetCore.SignalR;
using System.Security.Claims;
@ -40,16 +41,16 @@ namespace IM_API.Hubs
}
await base.OnConnectedAsync();
}
public async Task<HubResponse<MessageBaseDto?>> SendMessage(MessageBaseDto dto)
public async Task<HubResponse<MessageBaseVo?>> SendMessage(MessageBaseDto dto)
{
if (!Context.User.Identity.IsAuthenticated)
{
await Clients.Caller.SendAsync("ReceiveMessage", new BaseResponse<object?>(CodeDefine.AUTH_FAILED));
Context.Abort();
return new HubResponse<MessageBaseDto?>(CodeDefine.AUTH_FAILED, "SendMessage");
return new HubResponse<MessageBaseVo?>(CodeDefine.AUTH_FAILED, "SendMessage");
}
var userIdStr = Context.User.FindFirstValue(ClaimTypes.NameIdentifier);
MessageBaseDto msgInfo = null;
MessageBaseVo? msgInfo = null;
if(dto.ChatType == ChatType.PRIVATE)
{
msgInfo = await _messageService.SendPrivateMessageAsync(int.Parse(userIdStr), dto.ReceiverId, dto);
@ -58,7 +59,7 @@ namespace IM_API.Hubs
{
msgInfo = await _messageService.SendGroupMessageAsync(int.Parse(userIdStr), dto.ReceiverId, dto);
}
return new HubResponse<MessageBaseDto?>("SendMessage", msgInfo);
return new HubResponse<MessageBaseVo?>("SendMessage", msgInfo);
}
public async Task<HubResponse<object?>> ClearUnreadCount(int conversationId)
{

View File

@ -23,9 +23,11 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.2" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.3" />
<PackageReference Include="RedLock.net" Version="2.3.2" />
<PackageReference Include="StackExchange.Redis" Version="2.9.32" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.14.0" />

View File

@ -0,0 +1,49 @@
using IM_API.Models;
namespace IM_API.Interface.Services
{
public interface ICacheService
{
/// <summary>
/// 设置缓存
/// </summary>
/// <typeparam name="T">缓存对象类型</typeparam>
/// <param name="key">缓存索引值</param>
/// <param name="value">要缓存的对象</param>
/// <param name="expiration">过期时间</param>
/// <returns></returns>
Task SetAsync<T>(string key, T value, TimeSpan? expiration = null);
/// <summary>
/// 获取缓存
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key">缓存索引</param>
/// <returns></returns>
Task<T?> GetAsync<T>(string key);
/// <summary>
/// 删除缓存
/// </summary>
/// <param name="key">缓存索引</param>
/// <returns></returns>
Task RemoveAsync(string key);
/// <summary>
/// 设置用户缓存
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
Task SetUserCacheAsync(User user);
/// <summary>
/// 通过用户名获取用户缓存
/// </summary>
/// <param name="username"></param>
/// <returns></returns>
Task<User?> GetUserCacheAsync(string username);
/// <summary>
/// 删除用户缓存
/// </summary>
/// <param name="id"></param>
/// <param name="username"></param>
/// <returns></returns>
Task RemoveUserCacheAsync(string username);
}
}

View File

@ -1,5 +1,6 @@
using IM_API.Dtos.Conversation;
using IM_API.Models;
using IM_API.VOs.Conversation;
namespace IM_API.Interface.Services
{
@ -22,7 +23,7 @@ namespace IM_API.Interface.Services
/// </summary>
/// <param name="userId">用户id</param>
/// <returns></returns>
Task<List<ConversationDto>> GetConversationsAsync(int userId);
Task<List<ConversationVo>> GetConversationsAsync(int userId);
/// <summary>
/// 获取指定用户的所有推送标识符
/// </summary>
@ -34,7 +35,7 @@ namespace IM_API.Interface.Services
/// </summary>
/// <param name="conversationId"></param>
/// <returns></returns>
Task<ConversationDto> GetConversationByIdAsync(int userId, int conversationId);
Task<ConversationVo> GetConversationByIdAsync(int userId, int conversationId);
/// <summary>
/// 清空未读消息
/// </summary>
@ -50,5 +51,6 @@ namespace IM_API.Interface.Services
/// <param name="chatType"></param>
/// <returns></returns>
Task MakeConversationAsync(int userAId, int userBId, ChatType chatType);
Task UpdateConversationAfterSentAsync(UpdateConversationDto dto);
}
}

View File

@ -11,14 +11,14 @@ namespace IM_API.Interface.Services
/// <param name="groupId">群ID</param>
/// <param name="userIds">邀请的用户列表</param>
/// <returns></returns>
Task InviteUsers(int userId,int groupId, List<int> userIds);
Task InviteUsersAsync(int userId,int groupId, List<int> userIds);
/// <summary>
/// 加入群聊
/// </summary>
/// <param name="userId">操作者ID</param>
/// <param name="groupId">群ID</param>
/// <returns></returns>
Task JoinGroup(int userId,int groupId);
Task JoinGroupAsync(int userId,int groupId);
/// <summary>
/// 创建群聊
/// </summary>
@ -26,14 +26,22 @@ namespace IM_API.Interface.Services
/// <param name="groupCreateDto">群信息</param>
/// <param name="userIds">邀请用户列表</param>
/// <returns></returns>
Task<GroupInfoDto> CreateGroup(int userId, GroupCreateDto groupCreateDto, List<int> userIds);
Task<GroupInfoDto> CreateGroupAsync(int userId, GroupCreateDto groupCreateDto);
/// <summary>
/// 删除群
/// </summary>
/// <param name="userId">操作者ID</param>
/// <param name="groupId">群ID</param>
/// <returns></returns>
Task DeleteGroup(int userId, int groupId);
//Task UpdateGroupAuthori
Task DeleteGroupAsync(int userId, int groupId);
/// <summary>
/// 获取当前用户群列表
/// </summary>
/// <param name="userId">操作者ID</param>
/// <param name="page"></param>
/// <param name="limit"></param>
/// <param name="desc"></param>
/// <returns></returns>
Task<List<GroupInfoDto>> GetGroupListAsync(int userId, int page, int limit, bool desc);
}
}

View File

@ -1,4 +1,7 @@
using IM_API.Dtos;
using IM_API.Dtos.Message;
using IM_API.Models;
using IM_API.VOs.Message;
namespace IM_API.Interface.Services
{
@ -11,7 +14,7 @@ namespace IM_API.Interface.Services
/// <param name="receiverId">接收人</param>
/// <param name="dto"></param>
/// <returns></returns>
Task<MessageBaseDto> SendPrivateMessageAsync(int senderId,int receiverId,MessageBaseDto dto);
Task<MessageBaseVo> SendPrivateMessageAsync(int senderId,int receiverId,MessageBaseDto dto);
/// <summary>
/// 发送群聊消息
/// </summary>
@ -19,16 +22,21 @@ namespace IM_API.Interface.Services
/// <param name="groupId">接收群id</param>
/// <param name="dto"></param>
/// <returns></returns>
Task<MessageBaseDto> SendGroupMessageAsync(int senderId,int groupId,MessageBaseDto dto);
Task<MessageBaseVo> SendGroupMessageAsync(int senderId,int groupId,MessageBaseDto dto);
/// <summary>
/// 获取消息列表
/// 消息入库
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
Task MakeMessageAsync(Message message);
/// <summary>
/// 获取历史消息列表
/// </summary>
/// <param name="conversationId">会话id用于获取指定用户间聊天消息</param>
/// <param name="msgId">消息id</param>
/// <param name="sequenceId">消息id</param>
/// <param name="pageSize">获取消息数量</param>
/// <param name="desc"></param>
/// <returns></returns>
Task<List<MessageBaseDto>> GetMessagesAsync(int userId, int conversationId,int? msgId,int? pageSize,bool desc);
Task<List<MessageBaseVo>> GetMessagesAsync(int userId,MessageQueryDto dto);
/// <summary>
/// 获取未读消息数
/// </summary>

View File

@ -0,0 +1,12 @@
namespace IM_API.Interface.Services
{
public interface ISequenceIdService
{
/// <summary>
/// 创建消息序号
/// </summary>
/// <param name="streamKey">聊天唯一标识/param>
/// <returns></returns>
Task<long> GetNextSquenceIdAsync(string streamKey);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,73 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace IM_API.Migrations
{
/// <inheritdoc />
public partial class InitialCreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "admins");
migrationBuilder.DropTable(
name: "conversations");
migrationBuilder.DropTable(
name: "devices");
migrationBuilder.DropTable(
name: "files");
migrationBuilder.DropTable(
name: "friend_request");
migrationBuilder.DropTable(
name: "friends");
migrationBuilder.DropTable(
name: "group_invite");
migrationBuilder.DropTable(
name: "group_member");
migrationBuilder.DropTable(
name: "group_request");
migrationBuilder.DropTable(
name: "login_log");
migrationBuilder.DropTable(
name: "notifications");
migrationBuilder.DropTable(
name: "permissionarole");
migrationBuilder.DropTable(
name: "messages");
migrationBuilder.DropTable(
name: "groups");
migrationBuilder.DropTable(
name: "roles");
migrationBuilder.DropTable(
name: "permissions");
migrationBuilder.DropTable(
name: "users");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace IM_API.Migrations
{
/// <inheritdoc />
public partial class change_file : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "Type",
table: "files",
newName: "FileType");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "FileType",
table: "files",
newName: "Type");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace IM_API.Migrations
{
/// <inheritdoc />
public partial class addSequenceId : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "GroupMemberId",
table: "messages");
migrationBuilder.AddColumn<long>(
name: "SequenceId",
table: "messages",
type: "bigint",
nullable: false,
defaultValue: 0L);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "SequenceId",
table: "messages");
migrationBuilder.AddColumn<int>(
name: "GroupMemberId",
table: "messages",
type: "int(11)",
nullable: true,
comment: "若为群消息则表示具体的成员id");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,41 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace IM_API.Migrations
{
/// <inheritdoc />
public partial class updateconversationlastreadmessageid : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "conversations_ibfk_2",
table: "conversations");
migrationBuilder.AddForeignKey(
name: "conversations_ibfk_2",
table: "conversations",
column: "lastReadMessageId",
principalTable: "messages",
principalColumn: "ID",
onDelete: ReferentialAction.SetNull);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "conversations_ibfk_2",
table: "conversations");
migrationBuilder.AddForeignKey(
name: "conversations_ibfk_2",
table: "conversations",
column: "lastReadMessageId",
principalTable: "messages",
principalColumn: "ID");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,93 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace IM_API.Migrations
{
/// <inheritdoc />
public partial class updatemessage : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "conversations_ibfk_2",
table: "conversations");
migrationBuilder.RenameIndex(
name: "lastMessageId",
table: "conversations",
newName: "LastReadSequenceId");
migrationBuilder.AddColumn<Guid>(
name: "ClientMsgId",
table: "messages",
type: "char(36)",
nullable: false,
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"),
collation: "ascii_general_ci");
migrationBuilder.AddColumn<int>(
name: "MessageId",
table: "conversations",
type: "int(11)",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_messages_SequenceId_StreamKey",
table: "messages",
columns: new[] { "SequenceId", "StreamKey" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_conversations_MessageId",
table: "conversations",
column: "MessageId");
migrationBuilder.AddForeignKey(
name: "FK_conversations_messages_MessageId",
table: "conversations",
column: "MessageId",
principalTable: "messages",
principalColumn: "ID");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_conversations_messages_MessageId",
table: "conversations");
migrationBuilder.DropIndex(
name: "IX_messages_SequenceId_StreamKey",
table: "messages");
migrationBuilder.DropIndex(
name: "IX_conversations_MessageId",
table: "conversations");
migrationBuilder.DropColumn(
name: "ClientMsgId",
table: "messages");
migrationBuilder.DropColumn(
name: "MessageId",
table: "conversations");
migrationBuilder.RenameIndex(
name: "LastReadSequenceId",
table: "conversations",
newName: "lastMessageId");
migrationBuilder.AddForeignKey(
name: "conversations_ibfk_2",
table: "conversations",
column: "lastReadMessageId",
principalTable: "messages",
principalColumn: "ID",
onDelete: ReferentialAction.SetNull);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace IM_API.Migrations
{
/// <inheritdoc />
public partial class updatedatetimeToDateTimeOffset : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace IM_API.Migrations
{
/// <inheritdoc />
public partial class updateDateTimeOffsettype : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -30,12 +30,12 @@ public partial class Admin
/// <summary>
/// 创建时间
/// </summary>
public DateTime Created { get; set; }
public DateTimeOffset Created { get; set; }
/// <summary>
/// 更新时间
/// </summary>
public DateTime Updated { get; set; }
public DateTimeOffset Updated { get; set; }
public virtual Role Role { get; set; } = null!;
}

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
namespace IM_API.Models;
@ -20,14 +21,14 @@ public partial class Conversation
/// <summary>
/// 最后一条未读消息ID
/// </summary>
public int? LastReadMessageId { get; set; }
public long? LastReadSequenceId { get; set; }
/// <summary>
/// 未读消息数
/// </summary>
public int UnreadCount { get; set; }
public int ChatType { get; set; }
public ChatType ChatType { get; set; }
/// <summary>
/// 消息推送唯一标识符
@ -42,9 +43,9 @@ public partial class Conversation
/// <summary>
/// 最后一条消息发送时间
/// </summary>
public DateTime LastMessageTime { get; set; }
[Column(TypeName = "datetimeoffset")]
public DateTimeOffset LastMessageTime { get; set; }
public virtual Message? LastReadMessage { get; set; }
public virtual User User { get; set; } = null!;
}

View File

@ -0,0 +1,18 @@
using IM_API.Dtos.Conversation;
namespace IM_API.Models
{
public partial class Conversation
{
public ChatType ChatTypeEnum {
get
{
return ChatType;
}
set
{
ChatType = value;
}
}
}
}

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
namespace IM_API.Models;
@ -21,7 +22,8 @@ public partial class Device
/// <summary>
/// 最后一次登录
/// </summary>
public DateTime LastLogin { get; set; }
[Column(TypeName = "datetimeoffset")]
public DateTimeOffset LastLogin { get; set; }
public virtual User User { get; set; } = null!;
}

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
namespace IM_API.Models;
@ -25,7 +26,7 @@ public partial class File
/// <summary>
/// 文件类型
/// </summary>
public string Type { get; set; } = null!;
public string FileType { get; set; } = null!;
/// <summary>
/// 关联消息ID
@ -35,7 +36,8 @@ public partial class File
/// <summary>
/// 创建时间
/// </summary>
public DateTime Created { get; set; }
[Column(TypeName = "datetimeoffset")]
public DateTimeOffset Created { get; set; }
public virtual Message Message { get; set; } = null!;
}

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
namespace IM_API.Models;
@ -26,7 +27,8 @@ public partial class Friend
/// <summary>
/// 好友关系创建时间
/// </summary>
public DateTime Created { get; set; }
[Column(TypeName = "datetimeoffset")]
public DateTimeOffset Created { get; set; }
/// <summary>
/// 好友备注名

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
namespace IM_API.Models;
@ -20,7 +21,8 @@ public partial class FriendRequest
/// <summary>
/// 申请时间
/// </summary>
public DateTime Created { get; set; }
[Column(TypeName = "datetimeoffset")]
public DateTimeOffset Created { get; set; }
/// <summary>
/// 申请附言

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
namespace IM_API.Models;
@ -42,7 +43,8 @@ public partial class Group
/// <summary>
/// 群聊创建时间
/// </summary>
public DateTime Created { get; set; }
[Column(TypeName = "datetimeoffset")]
public DateTimeOffset Created { get; set; }
/// <summary>
/// 群头像

View File

@ -31,7 +31,7 @@ public partial class GroupInvite
/// <summary>
/// 创建时间
/// </summary>
public DateTime? Created { get; set; }
public DateTimeOffset? Created { get; set; }
public virtual Group Group { get; set; } = null!;

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
namespace IM_API.Models;
@ -25,7 +26,8 @@ public partial class GroupMember
/// <summary>
/// 加入群聊时间
/// </summary>
public DateTime Created { get; set; }
[Column(TypeName = "datetimeoffset")]
public DateTimeOffset Created { get; set; }
public virtual Group Group { get; set; } = null!;

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
namespace IM_API.Models;
@ -31,7 +32,8 @@ public partial class GroupRequest
/// <summary>
/// 创建时间
/// </summary>
public DateTime Created { get; set; }
[Column(TypeName = "datetimeoffset")]
public DateTimeOffset Created { get; set; }
public virtual Group Group { get; set; } = null!;

View File

@ -19,7 +19,7 @@ namespace IM_API.Models
});
modelBuilder.Entity<Conversation>(entity =>
{
entity.Ignore(e => e.ChatTypeEnum);
});
modelBuilder.Entity<Device>(entity =>

View File

@ -101,7 +101,8 @@ public partial class ImContext : DbContext
entity.HasIndex(e => e.UserId, "Userid");
entity.HasIndex(e => e.LastReadMessageId, "lastMessageId");
entity.HasIndex(e => e.LastReadSequenceId, "LastReadSequenceId");
entity.Property(e => e.Id)
.HasColumnType("int(11)")
@ -113,7 +114,7 @@ public partial class ImContext : DbContext
entity.Property(e => e.LastMessageTime)
.HasComment("最后一条消息发送时间")
.HasColumnType("datetime");
entity.Property(e => e.LastReadMessageId)
entity.Property(e => e.LastReadSequenceId)
.HasComment("最后一条未读消息ID ")
.HasColumnType("int(11)")
.HasColumnName("lastReadMessageId");
@ -130,10 +131,6 @@ public partial class ImContext : DbContext
.HasComment("用户")
.HasColumnType("int(11)");
entity.HasOne(d => d.LastReadMessage).WithMany(p => p.Conversations)
.HasForeignKey(d => d.LastReadMessageId)
.HasConstraintName("conversations_ibfk_2");
entity.HasOne(d => d.User).WithMany(p => p.Conversations)
.HasForeignKey(d => d.UserId)
.OnDelete(DeleteBehavior.ClientSetNull)
@ -197,7 +194,7 @@ public partial class ImContext : DbContext
entity.Property(e => e.Size)
.HasComment("文件大小单位KB ")
.HasColumnType("int(11)");
entity.Property(e => e.Type)
entity.Property(e => e.FileType)
.HasMaxLength(10)
.HasComment("文件类型 ");
entity.Property(e => e.Url)
@ -529,6 +526,11 @@ public partial class ImContext : DbContext
entity.HasIndex(e => e.Sender, "Sender");
//设置联合唯一索引
entity.HasIndex(e => new { e.SequenceId, e.StreamKey })
.IsUnique();
entity.Property(e => e.Id)
.HasColumnType("int(11)")
.HasColumnName("ID");
@ -541,9 +543,6 @@ public partial class ImContext : DbContext
entity.Property(e => e.Created)
.HasComment("发送时间 ")
.HasColumnType("datetime");
entity.Property(e => e.GroupMemberId)
.HasComment("若为群消息则表示具体的成员id")
.HasColumnType("int(11)");
entity.Property(e => e.MsgType)
.HasComment("消息类型\r\n(0:文本,1图片,2语音,3视频,4文件5语音聊天,6视频聊天)")
.HasColumnType("tinyint(4)");

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
namespace IM_API.Models;
@ -15,7 +16,8 @@ public partial class LoginLog
/// <summary>
/// 登录时间
/// </summary>
public DateTime Logined { get; set; }
[Column(TypeName = "datetimeoffset")]
public DateTimeOffset Logined { get; set; }
/// <summary>
/// 登录用户

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
namespace IM_API.Models;
@ -18,6 +19,7 @@ public partial class Message
/// (0:文本,1图片,2语音,3视频,4文件5语音聊天,6视频聊天)
/// </summary>
public sbyte MsgType { get; set; }
public Guid ClientMsgId { get; set; }
/// <summary>
/// 消息内容
@ -42,7 +44,8 @@ public partial class Message
/// <summary>
/// 发送时间
/// </summary>
public DateTime Created { get; set; }
[Column(TypeName = "datetimeoffset")]
public DateTimeOffset Created { get; set; }
/// <summary>
/// 消息推送唯一标识符
@ -50,9 +53,10 @@ public partial class Message
public string StreamKey { get; set; } = null!;
/// <summary>
/// 若为群消息则表示具体的成员id
/// 消息排序标识
/// </summary>
public int? GroupMemberId { get; set; }
public long SequenceId { get; set; }
public virtual ICollection<Conversation> Conversations { get; set; } = new List<Conversation>();

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
namespace IM_API.Models;
@ -30,7 +31,8 @@ public partial class Notification
/// <summary>
/// 创建时间
/// </summary>
public DateTime Created { get; set; }
[Column(TypeName = "datetimeoffset")]
public DateTimeOffset Created { get; set; }
public virtual User User { get; set; } = null!;
}

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
namespace IM_API.Models;
@ -25,7 +26,8 @@ public partial class Permission
/// <summary>
/// 创建时间
/// </summary>
public DateTime Created { get; set; }
[Column(TypeName = "datetimeoffset")]
public DateTimeOffset Created { get; set; }
public virtual ICollection<Permissionarole> Permissionaroles { get; set; } = new List<Permissionarole>();
}

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
namespace IM_API.Models;
@ -20,7 +21,8 @@ public partial class Role
/// <summary>
/// 创建时间
/// </summary>
public DateTime Created { get; set; }
[Column(TypeName = "datetimeoffset")]
public DateTimeOffset Created { get; set; }
public virtual ICollection<Admin> Admins { get; set; } = new List<Admin>();

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
namespace IM_API.Models;
@ -32,12 +33,13 @@ public partial class User
/// <summary>
/// 创建时间
/// </summary>
public DateTime Created { get; set; }
public DateTimeOffset Created { get; set; }
/// <summary>
/// 修改时间
/// </summary>
public DateTime? Updated { get; set; }
[Column(TypeName = "datetimeoffset")]
public DateTimeOffset? Updated { get; set; }
/// <summary>
/// 账户状态

View File

@ -1,5 +1,6 @@
using IM_API.Configs;
using IM_API.Configs.Options;
using IM_API.Filters;
using IM_API.Hubs;
using IM_API.Models;
@ -25,18 +26,22 @@ namespace IM_API
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.Build();
string conStr = configuration.GetConnectionString("DefaultConnection")!;
string redisConStr = configuration.GetConnectionString("Redis");
var conOptions = configuration.GetSection("ConnectionStrings").Get<ConnectionOptions>();
//注入数据库上下文
builder.Services.AddDbContext<ImContext>(options =>
{
options.UseMySql(conStr,ServerVersion.AutoDetect(conStr));
options.UseMySql(conOptions.DefaultConnection,ServerVersion.AutoDetect(conOptions.DefaultConnection));
});
//注入redis
var redis = ConnectionMultiplexer.Connect(redisConStr);
var redis = ConnectionMultiplexer.Connect(conOptions.Redis);
builder.Services.AddSingleton<IConnectionMultiplexer>(redis);
builder.Services.AddRabbitMQ(configuration.GetSection("RabbitMqOptions").Get<RabbitMqOptions>());
builder.Services.AddStackExchangeRedisCache(options =>
{
options.ConnectionMultiplexerFactory = () => Task.FromResult<IConnectionMultiplexer>(redis);
});
builder.Services.AddRabbitMQ(configuration.GetSection("RabbitMqOptions").Get<RabbitMQOptions>());
builder.Services.AddAllService(configuration);
@ -116,7 +121,7 @@ namespace IM_API
}).AddJsonOptions(options =>
{
// 保持 ISO 8601 格式
options.JsonSerializerOptions.Converters.Add(new UtcDateTimeConverter());
//options.JsonSerializerOptions.Converters.Add(new UtcDateTimeConverter());
// 将枚举转换为字符串
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
// 建议:保持驼峰命名

View File

@ -14,15 +14,19 @@ namespace IM_API.Services
private readonly ImContext _context;
private readonly ILogger<AuthService> _logger;
private readonly IMapper _mapper;
public AuthService(ImContext context, ILogger<AuthService> logger, IMapper mapper)
private readonly ICacheService _cache;
public AuthService(ImContext context, ILogger<AuthService> logger, IMapper mapper, ICacheService cache)
{
_context = context;
_logger = logger;
_mapper = mapper;
_cache = cache;
}
public async Task<User> LoginAsync(LoginRequestDto dto)
{
var userinfo = await _cache.GetUserCacheAsync(dto.Username);
if (userinfo != null && userinfo.Password == dto.Password) return userinfo;
string username = dto.Username;
string password = dto.Password;
var user = await _context.Users.FirstOrDefaultAsync(x => x.Username == username && x.Password == password);
@ -30,6 +34,7 @@ namespace IM_API.Services
{
throw new BaseException(CodeDefine.PASSWORD_ERROR);
}
await _cache.SetUserCacheAsync(user);
return user;
}

View File

@ -4,6 +4,7 @@ using IM_API.Exceptions;
using IM_API.Interface.Services;
using IM_API.Models;
using IM_API.Tools;
using IM_API.VOs.Conversation;
using Microsoft.EntityFrameworkCore;
namespace IM_API.Services
@ -27,26 +28,26 @@ namespace IM_API.Services
#endregion
#region
public async Task<List<ConversationDto>> GetConversationsAsync(int userId)
public async Task<List<ConversationVo>> GetConversationsAsync(int userId)
{
// 1. 获取私聊会话
var privateList = await (from c in _context.Conversations
join f in _context.Friends on new { c.UserId, c.TargetId }
equals new { UserId = f.UserId, TargetId = f.FriendId }
where c.UserId == userId && c.ChatType == (int)ChatType.PRIVATE
where c.UserId == userId && c.ChatType == ChatType.PRIVATE
select new { c, f.Avatar, f.RemarkName })
.ToListAsync();
// 2. 获取群聊会话
var groupList = await (from c in _context.Conversations
join g in _context.Groups on c.TargetId equals g.Id
where c.UserId == userId && c.ChatType == (int)ChatType.GROUP
where c.UserId == userId && c.ChatType == ChatType.GROUP
select new { c, g.Avatar, g.Name })
.ToListAsync();
var privateDtos = privateList.Select(x =>
{
var dto = _mapper.Map<ConversationDto>(x.c);
var dto = _mapper.Map<ConversationVo>(x.c);
dto.TargetAvatar = x.Avatar;
dto.TargetName = x.RemarkName;
return dto;
@ -54,7 +55,7 @@ namespace IM_API.Services
var groupDtos = groupList.Select(x =>
{
var dto = _mapper.Map<ConversationDto>(x.c);
var dto = _mapper.Map<ConversationVo>(x.c);
dto.TargetAvatar = x.Avatar;
dto.TargetName = x.Name;
return dto;
@ -89,25 +90,23 @@ namespace IM_API.Services
#endregion
#region
public async Task<ConversationDto> GetConversationByIdAsync(int userId, int conversationId)
public async Task<ConversationVo> GetConversationByIdAsync(int userId, int conversationId)
{
var conversation = await _context.Conversations
.Include(x => x.LastReadMessage)
.FirstOrDefaultAsync(
x => x.UserId == userId && x.Id == conversationId
);
if (conversation is null) throw new BaseException(CodeDefine.CONVERSATION_NOT_FOUND);
var dto = _mapper.Map<ConversationDto>(conversation);
//dto.LastReadMessage = _mapper.Map<MessageBaseDto>(conversation);
if(conversation.ChatType == (int)ChatType.PRIVATE)
var dto = _mapper.Map<ConversationVo>(conversation);
if(conversation.ChatType == ChatType.PRIVATE)
{
var friendInfo = await _context.Friends.FirstOrDefaultAsync(
x => x.UserId == userId && x.FriendId == conversation.TargetId
var friendInfo = await _context.Friends.Include(n => n.FriendNavigation).FirstOrDefaultAsync(
x => x.UserId == conversation.UserId && x.FriendId == conversation.TargetId
);
if (friendInfo is null) throw new BaseException(CodeDefine.FRIEND_RELATION_NOT_FOUND);
_mapper.Map(friendInfo,dto);
}
if(conversation.ChatType == (int)ChatType.GROUP)
if(conversation.ChatType == ChatType.GROUP)
{
var groupInfo = await _context.Groups.FirstOrDefaultAsync(
x => x.Id == conversation.TargetId
@ -125,12 +124,18 @@ namespace IM_API.Services
if (conversation is null) throw new BaseException(CodeDefine.CONVERSATION_NOT_FOUND);
var message = await _context.Messages
.Where(x => x.StreamKey == conversation.StreamKey)
.OrderByDescending(x => x.Id)
.OrderByDescending(x => x.SequenceId)
.FirstOrDefaultAsync();
conversation.LastReadMessage = message;
conversation.UnreadCount = 0;
_context.Conversations.Update(conversation);
await _context.SaveChangesAsync();
if(message != null)
{
conversation.UnreadCount = 0;
conversation.LastMessage = message.Content;
conversation.LastReadSequenceId = message.SequenceId;
conversation.LastMessageTime = message.Created;
_context.Conversations.Update(conversation);
await _context.SaveChangesAsync();
}
return true;
}
@ -143,10 +148,10 @@ namespace IM_API.Services
StreamKeyBuilder.Private(userAId, userBId) : StreamKeyBuilder.Group(userBId);
var conversation = new Conversation()
{
ChatType = (int)chatType,
ChatType = chatType,
LastMessage = "",
LastMessageTime = DateTime.UtcNow,
LastReadMessageId = -1,
LastMessageTime = DateTime.Now,
LastReadSequenceId = null,
StreamKey = streamKey,
TargetId = userBId,
UnreadCount = 0,
@ -156,5 +161,19 @@ namespace IM_API.Services
_context.Conversations.Add(conversation);
await _context.SaveChangesAsync();
}
public async Task UpdateConversationAfterSentAsync(UpdateConversationDto dto)
{
var cList = await _context.Conversations.Where(x => x.StreamKey == dto.StreamKey).ToListAsync();
foreach(var c in cList)
{
bool isSender = dto.SenderId == c.UserId;
c.LastMessage = dto.LastMessage;
c.LastMessageTime = dto.DateTime;
c.LastReadSequenceId = isSender ? dto.LastSequenceId : c.LastReadSequenceId;
c.UnreadCount = isSender ? 0 : c.UnreadCount + 1;
}
_context.Conversations.UpdateRange(cList);
await _context.SaveChangesAsync();
}
}
}

View File

@ -132,8 +132,8 @@ namespace IM_API.Services
await _endpoint.Publish(new FriendAddEvent()
{
AggregateId = friendRequest.Id.ToString(),
OccurredAt = DateTime.UtcNow,
Created = DateTime.UtcNow,
OccurredAt = DateTime.Now,
Created = DateTime.Now,
EventId = Guid.NewGuid(),
OperatorId = friendRequest.ResponseUser,
RequestInfo = _mapper.Map<FriendRequestDto>(friendRequest),
@ -183,7 +183,7 @@ namespace IM_API.Services
await _endpoint.Publish(new RequestFriendEvent()
{
AggregateId = friendRequst.Id.ToString(),
OccurredAt = friendRequst.Created,
OccurredAt = friendRequst.Created.UtcDateTime,
Description = friendRequst.Description,
EventId = Guid.NewGuid(),
FromUserId = friendRequst.RequestUser,
@ -205,7 +205,7 @@ namespace IM_API.Services
Friend friendA = new Friend()
{
Avatar = userbInfo.Avatar,
Created = DateTime.UtcNow,
Created = DateTime.Now,
FriendId = userbInfo.Id,
RemarkName = remarkName ?? userbInfo.NickName,
StatusEnum = FriendStatus.Added,

View File

@ -1,4 +1,5 @@
using AutoMapper;
using AutoMapper.QueryableExtensions;
using IM_API.Domain.Events;
using IM_API.Dtos.Group;
using IM_API.Exceptions;
@ -27,7 +28,7 @@ namespace IM_API.Services
private async Task<List<GroupInvite>> GetGroupInvites(int userId, int groupId, List<int> ids)
{
DateTime dateTime = DateTime.UtcNow;
DateTime dateTime = DateTime.Now;
//验证被邀请用户是否为好友
var validFriendIds = await _context.Friends
.Where(f => f.UserId == userId && ids.Contains(f.FriendId))
@ -44,13 +45,14 @@ namespace IM_API.Services
}).ToList();
}
public async Task<GroupInfoDto> CreateGroup(int userId, GroupCreateDto groupCreateDto, List<int> userIds)
public async Task<GroupInfoDto> CreateGroupAsync(int userId, GroupCreateDto groupCreateDto)
{
List<int> userIds = groupCreateDto.UserIDs ?? [];
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
//先创建群
DateTime dateTime = DateTime.UtcNow;
DateTime dateTime = DateTime.Now;
Group group = _mapper.Map<Group>(groupCreateDto);
group.GroupMaster = userId;
_context.Groups.Add(group);
@ -90,21 +92,38 @@ namespace IM_API.Services
}
}
public Task DeleteGroup(int userId, int groupId)
public Task DeleteGroupAsync(int userId, int groupId)
{
throw new NotImplementedException();
}
public async Task InviteUsers(int userId, int groupId, List<int> userIds)
public async Task InviteUsersAsync(int userId, int groupId, List<int> userIds)
{
var group = await _context.Groups.FirstOrDefaultAsync(
x => x.Id == groupId) ?? throw new BaseException(CodeDefine.GROUP_NOT_FOUND);
}
public Task JoinGroup(int userId, int groupId)
public Task JoinGroupAsync(int userId, int groupId)
{
throw new NotImplementedException();
}
public async Task<List<GroupInfoDto>> GetGroupListAsync(int userId, int page, int limit, bool desc)
{
var query = _context.GroupMembers
.Where(x => x.UserId == userId)
.Select(s => s.Group);
if (desc)
{
query = query.OrderByDescending(x => x.Id);
}
var list = await query
.Skip((page - 1) * limit)
.Take(limit)
.ProjectTo<GroupInfoDto>(_mapper.ConfigurationProvider)
.ToListAsync();
return list;
}
}
}

View File

@ -41,7 +41,7 @@ namespace IM_API.Services
public (string token, DateTime expiresAt) CreateAccessTokenForUser(int userId, string username, string role)
{
var expiresAt = DateTime.UtcNow.AddMinutes(_accessMinutes);
var expiresAt = DateTime.Now.AddMinutes(_accessMinutes);
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, userId.ToString()),

View File

@ -2,12 +2,18 @@
using IM_API.Application.Interfaces;
using IM_API.Domain.Events;
using IM_API.Dtos;
using IM_API.Dtos.Message;
using IM_API.Exceptions;
using IM_API.Interface.Services;
using IM_API.Models;
using IM_API.Tools;
using IM_API.VOs.Message;
using MassTransit;
using Microsoft.EntityFrameworkCore;
using StackExchange.Redis;
using System.Text.RegularExpressions;
using static MassTransit.Monitoring.Performance.BuiltInCounters;
using static Microsoft.EntityFrameworkCore.DbLoggerCategory;
namespace IM_API.Services
{
@ -19,9 +25,10 @@ namespace IM_API.Services
//废弃此处已使用rabbitMQ替代
//private readonly IEventBus _eventBus;
private readonly IPublishEndpoint _endpoint;
private readonly ISequenceIdService _sequenceIdService;
public MessageService(
ImContext context, ILogger<MessageService> logger, IMapper mapper, IEventBus eventBus,
IPublishEndpoint publishEndpoint
ImContext context, ILogger<MessageService> logger, IMapper mapper,
IPublishEndpoint publishEndpoint, ISequenceIdService sequenceIdService
)
{
_context = context;
@ -29,40 +36,43 @@ namespace IM_API.Services
_mapper = mapper;
//_eventBus = eventBus;
_endpoint = publishEndpoint;
_sequenceIdService = sequenceIdService;
}
public async Task<List<MessageBaseDto>> GetMessagesAsync(int userId, int conversationId, int? msgId, int? pageSize, bool desc)
public async Task<List<MessageBaseVo>> GetMessagesAsync(int userId,MessageQueryDto dto)
{
//获取会话信息用于获取双方聊天的唯一标识streamkey
Conversation? conversation = await _context.Conversations.FirstOrDefaultAsync(
x => x.Id == conversationId && x.UserId == userId
x => x.Id == dto.ConversationId && x.UserId == userId
);
if (conversation is null) throw new BaseException(CodeDefine.CONVERSATION_NOT_FOUND);
var query = _context.Messages.AsQueryable();
if(msgId != null)
var baseQuery = _context.Messages.Where(x => x.StreamKey == conversation.StreamKey);
if (dto.Direction == 0) // Before: 找比锚点小的,按倒序排
{
query = query.Where(
x => x.StreamKey == conversation.StreamKey && x.Id < msgId.Value
)
.OrderByDescending(x => x.Id);
if (dto.Cursor.HasValue)
baseQuery = baseQuery.Where(m => m.SequenceId < dto.Cursor.Value);
var list = await baseQuery
.OrderByDescending(m => m.SequenceId) // 最新消息在最前
.Take(dto.Limit)
.Select(m => _mapper.Map<MessageBaseVo>(m))
.ToListAsync();
return list.OrderBy(s => s.SequenceId).ToList();
}
else
else // After: 找比锚点大的,按正序排(用于补洞或刷新)
{
query = query.Where(
x => x.StreamKey == conversation.StreamKey && x.Id > conversation.LastReadMessageId
);
// 如果 Cursor 为空且是 After逻辑上说不通通常直接返回空或报错
if (!dto.Cursor.HasValue) return new List<MessageBaseVo>();
return await baseQuery
.Where(m => m.SequenceId > dto.Cursor.Value)
.OrderBy(m => m.SequenceId) // 按时间线正序
.Take(dto.Limit)
.Select(m => _mapper.Map<MessageBaseVo>(m))
.ToListAsync();
}
if(pageSize != null)
{
query = query.Take(pageSize.Value);
}
var msgList = await query
.ToListAsync();
msgList = msgList
.OrderBy(x => x.Created)
.ThenBy(t => t.Id)
.ToList();
return _mapper.Map<List<MessageBaseDto>>(msgList);
}
public Task<int> GetUnreadCountAsync(int userId)
@ -89,8 +99,14 @@ namespace IM_API.Services
{
throw new NotImplementedException();
}
public async Task MakeMessageAsync(Message message)
{
_context.Messages.Add(message);
await _context.SaveChangesAsync();
}
#region
public async Task<MessageBaseDto> SendGroupMessageAsync(int senderId, int groupId, MessageBaseDto dto)
public async Task<MessageBaseVo> SendGroupMessageAsync(int senderId, int groupId, MessageBaseDto dto)
{
//判断群存在
var isExist = await _context.Groups.AnyAsync(x => x.Id == groupId);
@ -99,30 +115,23 @@ namespace IM_API.Services
var isMember = await _context.GroupMembers.AnyAsync(x => x.GroupId == groupId && x.UserId == senderId);
if (!isMember) throw new BaseException(CodeDefine.NO_GROUP_PERMISSION);
var message = _mapper.Map<Message>(dto);
message.Sender = senderId;
message.StreamKey = StreamKeyBuilder.Group(
groupId);
_context.Messages.Add(message);
await _context.SaveChangesAsync();
message.StreamKey = StreamKeyBuilder.Group(groupId);
message.SequenceId = await _sequenceIdService.GetNextSquenceIdAsync(message.StreamKey);
await _endpoint.Publish(_mapper.Map<MessageCreatedEvent>(message));
return _mapper.Map<MessageBaseDto>(message);
return _mapper.Map<MessageBaseVo>(message);
}
#endregion
#region
public async Task<MessageBaseDto> SendPrivateMessageAsync(int senderId, int receiverId, MessageBaseDto dto)
public async Task<MessageBaseVo> SendPrivateMessageAsync(int senderId, int receiverId, MessageBaseDto dto)
{
bool isExist = await _context.Friends.AnyAsync(x => x.FriendId == receiverId);
if (!isExist) throw new BaseException(CodeDefine.FRIEND_RELATION_NOT_FOUND);
var message = _mapper.Map<Message>(dto);
message.Sender = senderId;
//生成消息流唯一标识符
message.StreamKey = StreamKeyBuilder.Private(dto.SenderId, dto.ReceiverId);
_context.Messages.Add(message);
await _context.SaveChangesAsync();
message.StreamKey = StreamKeyBuilder.Private(senderId, receiverId);
message.SequenceId = await _sequenceIdService.GetNextSquenceIdAsync(message.StreamKey);
await _endpoint.Publish(_mapper.Map<MessageCreatedEvent>(message));
return _mapper.Map<MessageBaseDto>(message);
return _mapper.Map<MessageBaseVo>(message);
}
#endregion
}

View File

@ -0,0 +1,62 @@
using IM_API.Interface.Services;
using IM_API.Models;
using IM_API.Tools;
using Microsoft.Extensions.Caching.Distributed;
using System.Text.Json;
namespace IM_API.Services
{
public class RedisCacheService:ICacheService
{
private readonly IDistributedCache _cache;
public RedisCacheService(IDistributedCache cache)
{
_cache = cache;
}
public async Task<T?> GetAsync<T>(string key)
{
var valueBytes= await _cache.GetAsync(key);
if (valueBytes is null || valueBytes.Length == 0) return default;
return JsonSerializer.Deserialize<T>(valueBytes);
}
public async Task<User?> GetUserCacheAsync(string username)
{
var usernameKey = RedisKeys.GetUserinfoKeyByUsername(username);
var userid = await GetAsync<string>(usernameKey);
if (userid is null) return default;
var key = RedisKeys.GetUserinfoKey(userid);
return await GetAsync<User>(key);
}
public async Task RemoveAsync(string key) => await _cache.RemoveAsync(key);
public async Task RemoveUserCacheAsync(string username)
{
var usernameKey = RedisKeys.GetUserinfoKeyByUsername(username);
var userid = await GetAsync<string>(usernameKey);
if (userid is null) return;
var key = RedisKeys.GetUserinfoKey(userid);
await RemoveAsync(key);
}
public async Task SetAsync<T>(string key, T value, TimeSpan? expiration = null)
{
var options = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = expiration ?? TimeSpan.FromHours(1)
};
var valueBytes = JsonSerializer.SerializeToUtf8Bytes(value);
await _cache.SetAsync(key, valueBytes, options);
}
public async Task SetUserCacheAsync(User user)
{
var idKey = RedisKeys.GetUserinfoKey(user.Id.ToString());
await SetAsync(idKey, user);
var usernameKey = RedisKeys.GetUserinfoKeyByUsername(user.Username);
await SetAsync(usernameKey, user.Id.ToString());
}
}
}

View File

@ -0,0 +1,46 @@
using IM_API.Interface.Services;
using IM_API.Models;
using IM_API.Tools;
using Microsoft.EntityFrameworkCore;
using RedLockNet;
using StackExchange.Redis;
namespace IM_API.Services
{
public class SequenceIdService : ISequenceIdService
{
private IDatabase _database;
private IDistributedLockFactory _lockFactory;
private ImContext _context;
public SequenceIdService(IConnectionMultiplexer connectionMultiplexer,
IDistributedLockFactory distributedLockFactory, ImContext imContext)
{
_database = connectionMultiplexer.GetDatabase();
_lockFactory = distributedLockFactory;
_context = imContext;
}
public async Task<long> GetNextSquenceIdAsync(string streamKey)
{
string key = RedisKeys.GetSequenceIdKey(streamKey);
string lockKey = RedisKeys.GetSequenceIdLockKey(streamKey);
var exists = await _database.KeyExistsAsync(key);
if (!exists)
{
using (var _lock = await _lockFactory.CreateLockAsync(lockKey, TimeSpan.FromSeconds(5)))
{
if (_lock.IsAcquired)
{
if(!await _database.KeyExistsAsync(key))
{
var max = await _context.Messages
.Where(x => x.StreamKey == streamKey)
.MaxAsync(m => (long?)m.SequenceId) ?? 0;
await _database.StringSetAsync(key, max, TimeSpan.FromDays(7));
}
}
}
}
return await _database.StringIncrementAsync(key);
}
}
}

View File

@ -5,6 +5,7 @@ using IM_API.Interface.Services;
using IM_API.Models;
using IM_API.Tools;
using Microsoft.EntityFrameworkCore;
using StackExchange.Redis;
namespace IM_API.Services
{
@ -13,31 +14,43 @@ namespace IM_API.Services
private readonly ImContext _context;
private readonly ILogger<UserService> _logger;
private readonly IMapper _mapper;
public UserService(ImContext imContext,ILogger<UserService> logger,IMapper mapper)
private readonly ICacheService _cacheService;
public UserService(ImContext imContext,ILogger<UserService> logger,IMapper mapper, ICacheService cacheService)
{
this._context = imContext;
this._logger = logger;
this._mapper = mapper;
_cacheService = cacheService;
}
#region
public async Task<UserInfoDto> GetUserInfoAsync(int userId)
{
var user = await _context.Users.FirstOrDefaultAsync(x => x.Id == userId);
//查询redis缓存如果存在直接返回不走查库逻辑
var key = RedisKeys.GetUserinfoKey(userId.ToString());
var userinfoCache = await _cacheService.GetAsync<User>(key);
if (userinfoCache != null) return _mapper.Map<UserInfoDto>(userinfoCache);
//无缓存查库
var user = await _context.Users
.FirstOrDefaultAsync(x => x.Id == userId);
if (user == null)
{
throw new BaseException(CodeDefine.USER_NOT_FOUND);
}
await _cacheService.SetUserCacheAsync(user);
return _mapper.Map<UserInfoDto>(user);
}
#endregion
#region
public async Task<UserInfoDto> GetUserInfoByUsernameAsync(string username)
{
var userinfo = await _cacheService.GetUserCacheAsync(username);
if (userinfo != null) return _mapper.Map<UserInfoDto>(userinfo);
var user = await _context.Users.FirstOrDefaultAsync(x => x.Username == username);
if (user == null)
{
throw new BaseException(CodeDefine.USER_NOT_FOUND);
}
await _cacheService.SetUserCacheAsync(user);
return _mapper.Map<UserInfoDto>(user);
}
#endregion
@ -70,6 +83,7 @@ namespace IM_API.Services
if (user is null) throw new BaseException(CodeDefine.USER_NOT_FOUND);
_mapper.Map(dto,user);
await _context.SaveChangesAsync();
await _cacheService.SetUserCacheAsync(user);
return _mapper.Map<UserInfoDto>(user);
}
#endregion

View File

@ -0,0 +1,22 @@
namespace IM_API.Tools
{
public static class RedisKeys
{
public static string GetUserinfoKey(string userId)
{
return $"user::uinfo::{userId}";
}
public static string GetUserinfoKeyByUsername(string username)
{
return $"user::uinfobyid::{username}";
}
public static string GetSequenceIdKey(string streamKey)
{
return $"chat::seq::{streamKey}";
}
public static string GetSequenceIdLockKey(string streamKey)
{
return $"lock::seq::{streamKey}";
}
}
}

Some files were not shown because too many files have changed in this diff Show More