Merge pull request 'feature-nxdev' (#67) from feature-nxdev into main
Reviewed-on: #67
This commit is contained in:
commit
443cd4618d
@ -1002,6 +1002,14 @@
|
|||||||
"runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.0": {},
|
"runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.0": {},
|
||||||
"runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.0": {},
|
"runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.0": {},
|
||||||
"runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.0": {},
|
"runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.0": {},
|
||||||
|
"SixLabors.ImageSharp/3.1.12": {
|
||||||
|
"runtime": {
|
||||||
|
"lib/net6.0/SixLabors.ImageSharp.dll": {
|
||||||
|
"assemblyVersion": "3.0.0.0",
|
||||||
|
"fileVersion": "3.1.12.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"StackExchange.Redis/2.9.32": {
|
"StackExchange.Redis/2.9.32": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "10.0.2",
|
"Microsoft.Extensions.Logging.Abstractions": "10.0.2",
|
||||||
@ -1692,6 +1700,7 @@
|
|||||||
"Newtonsoft.Json": "13.0.4",
|
"Newtonsoft.Json": "13.0.4",
|
||||||
"Pomelo.EntityFrameworkCore.MySql": "8.0.3",
|
"Pomelo.EntityFrameworkCore.MySql": "8.0.3",
|
||||||
"RedLock.net": "2.3.2",
|
"RedLock.net": "2.3.2",
|
||||||
|
"SixLabors.ImageSharp": "3.1.12",
|
||||||
"StackExchange.Redis": "2.9.32",
|
"StackExchange.Redis": "2.9.32",
|
||||||
"Swashbuckle.AspNetCore": "6.6.2",
|
"Swashbuckle.AspNetCore": "6.6.2",
|
||||||
"System.IdentityModel.Tokens.Jwt": "8.14.0"
|
"System.IdentityModel.Tokens.Jwt": "8.14.0"
|
||||||
@ -2362,6 +2371,13 @@
|
|||||||
"path": "runtime.ubuntu.16.10-x64.runtime.native.system.security.cryptography.openssl/4.3.0",
|
"path": "runtime.ubuntu.16.10-x64.runtime.native.system.security.cryptography.openssl/4.3.0",
|
||||||
"hashPath": "runtime.ubuntu.16.10-x64.runtime.native.system.security.cryptography.openssl.4.3.0.nupkg.sha512"
|
"hashPath": "runtime.ubuntu.16.10-x64.runtime.native.system.security.cryptography.openssl.4.3.0.nupkg.sha512"
|
||||||
},
|
},
|
||||||
|
"SixLabors.ImageSharp/3.1.12": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-iAg6zifihXEFS/t7fiHhZBGAdCp3FavsF4i2ZIDp0JfeYeDVzvmlbY1CNhhIKimaIzrzSi5M/NBFcWvZT2rB/A==",
|
||||||
|
"path": "sixlabors.imagesharp/3.1.12",
|
||||||
|
"hashPath": "sixlabors.imagesharp.3.1.12.nupkg.sha512"
|
||||||
|
},
|
||||||
"StackExchange.Redis/2.9.32": {
|
"StackExchange.Redis/2.9.32": {
|
||||||
"type": "package",
|
"type": "package",
|
||||||
"serviceable": true,
|
"serviceable": true,
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@ -20,6 +20,7 @@
|
|||||||
"Newtonsoft.Json": "13.0.4",
|
"Newtonsoft.Json": "13.0.4",
|
||||||
"Pomelo.EntityFrameworkCore.MySql": "8.0.3",
|
"Pomelo.EntityFrameworkCore.MySql": "8.0.3",
|
||||||
"RedLock.net": "2.3.2",
|
"RedLock.net": "2.3.2",
|
||||||
|
"SixLabors.ImageSharp": "3.1.12",
|
||||||
"StackExchange.Redis": "2.9.32",
|
"StackExchange.Redis": "2.9.32",
|
||||||
"Swashbuckle.AspNetCore": "6.6.2",
|
"Swashbuckle.AspNetCore": "6.6.2",
|
||||||
"System.IdentityModel.Tokens.Jwt": "8.14.0"
|
"System.IdentityModel.Tokens.Jwt": "8.14.0"
|
||||||
@ -870,6 +871,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"SixLabors.ImageSharp/3.1.12": {
|
||||||
|
"runtime": {
|
||||||
|
"lib/net6.0/SixLabors.ImageSharp.dll": {
|
||||||
|
"assemblyVersion": "3.0.0.0",
|
||||||
|
"fileVersion": "3.1.12.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"StackExchange.Redis/2.9.32": {
|
"StackExchange.Redis/2.9.32": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "10.0.2",
|
"Microsoft.Extensions.Logging.Abstractions": "10.0.2",
|
||||||
@ -1564,6 +1573,13 @@
|
|||||||
"path": "redlock.net/2.3.2",
|
"path": "redlock.net/2.3.2",
|
||||||
"hashPath": "redlock.net.2.3.2.nupkg.sha512"
|
"hashPath": "redlock.net.2.3.2.nupkg.sha512"
|
||||||
},
|
},
|
||||||
|
"SixLabors.ImageSharp/3.1.12": {
|
||||||
|
"type": "package",
|
||||||
|
"serviceable": true,
|
||||||
|
"sha512": "sha512-iAg6zifihXEFS/t7fiHhZBGAdCp3FavsF4i2ZIDp0JfeYeDVzvmlbY1CNhhIKimaIzrzSi5M/NBFcWvZT2rB/A==",
|
||||||
|
"path": "sixlabors.imagesharp/3.1.12",
|
||||||
|
"hashPath": "sixlabors.imagesharp.3.1.12.nupkg.sha512"
|
||||||
|
},
|
||||||
"StackExchange.Redis/2.9.32": {
|
"StackExchange.Redis/2.9.32": {
|
||||||
"type": "package",
|
"type": "package",
|
||||||
"serviceable": true,
|
"serviceable": true,
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -14,7 +14,7 @@
|
|||||||
"RefreshTokenDays": 30
|
"RefreshTokenDays": 30
|
||||||
},
|
},
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"DefaultConnection": "Server=frp-era.com;Port=26582;Database=IM;User=product;Password=12345678;",
|
"DefaultConnection": "Server=192.168.5.100;Port=3306;Database=IM;User=product;Password=12345678;",
|
||||||
"Redis": "192.168.5.100:6379"
|
"Redis": "192.168.5.100:6379"
|
||||||
},
|
},
|
||||||
"RabbitMQOptions": {
|
"RabbitMQOptions": {
|
||||||
@ -22,5 +22,9 @@
|
|||||||
"Port": 5672,
|
"Port": 5672,
|
||||||
"Username": "test",
|
"Username": "test",
|
||||||
"Password": "123456"
|
"Password": "123456"
|
||||||
|
},
|
||||||
|
"FileUploadOptions": {
|
||||||
|
"DefaultStorage": "Local",
|
||||||
|
"ChunkSize": 5000000,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,7 @@ using System.Reflection;
|
|||||||
[assembly: System.Reflection.AssemblyCompanyAttribute("IMTest")]
|
[assembly: System.Reflection.AssemblyCompanyAttribute("IMTest")]
|
||||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+dc6ecf224df4e8714171e8b5d23afaa90b3a1f81")]
|
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+2ecaa28091b41de707825db3628d380b62fa727f")]
|
||||||
[assembly: System.Reflection.AssemblyProductAttribute("IMTest")]
|
[assembly: System.Reflection.AssemblyProductAttribute("IMTest")]
|
||||||
[assembly: System.Reflection.AssemblyTitleAttribute("IMTest")]
|
[assembly: System.Reflection.AssemblyTitleAttribute("IMTest")]
|
||||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
1b9e709aa84e0b4f6260cd10cf25bfc3a30c60e75a3966fc7d4cdf489eae898b
|
ed4980dfc7aff253176b260ed9015f9a80b52e92cbf3095eff3ed06865ea6e0d
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@ -1 +1 @@
|
|||||||
6e6df2b3d9fe8d3830882bef146134864f65ca58bc5ea4bac684eaec55cfd628
|
a18d4d5688b125e6729fd465f09e267a2a7532eadaaca930389969ac369409ce
|
||||||
|
|||||||
@ -151,3 +151,4 @@ C:\Users\nanxun\Documents\IM\backend\IMTest\bin\Debug\net8.0\Microsoft.Extension
|
|||||||
C:\Users\nanxun\Documents\IM\backend\IMTest\bin\Debug\net8.0\Microsoft.Extensions.Caching.StackExchangeRedis.dll
|
C:\Users\nanxun\Documents\IM\backend\IMTest\bin\Debug\net8.0\Microsoft.Extensions.Caching.StackExchangeRedis.dll
|
||||||
C:\Users\nanxun\Documents\IM\backend\IMTest\bin\Debug\net8.0\Microsoft.Extensions.Primitives.dll
|
C:\Users\nanxun\Documents\IM\backend\IMTest\bin\Debug\net8.0\Microsoft.Extensions.Primitives.dll
|
||||||
C:\Users\nanxun\Documents\IM\backend\IMTest\bin\Debug\net8.0\System.Diagnostics.DiagnosticSource.dll
|
C:\Users\nanxun\Documents\IM\backend\IMTest\bin\Debug\net8.0\System.Diagnostics.DiagnosticSource.dll
|
||||||
|
C:\Users\nanxun\Documents\IM\backend\IMTest\bin\Debug\net8.0\SixLabors.ImageSharp.dll
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -96,7 +96,7 @@
|
|||||||
"privateAssets": "all"
|
"privateAssets": "all"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json"
|
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.311/PortableRuntimeIdentifierGraph.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -199,6 +199,10 @@
|
|||||||
"target": "Package",
|
"target": "Package",
|
||||||
"version": "[2.3.2, )"
|
"version": "[2.3.2, )"
|
||||||
},
|
},
|
||||||
|
"SixLabors.ImageSharp": {
|
||||||
|
"target": "Package",
|
||||||
|
"version": "[3.1.12, )"
|
||||||
|
},
|
||||||
"StackExchange.Redis": {
|
"StackExchange.Redis": {
|
||||||
"target": "Package",
|
"target": "Package",
|
||||||
"version": "[2.9.32, )"
|
"version": "[2.9.32, )"
|
||||||
@ -231,7 +235,7 @@
|
|||||||
"privateAssets": "all"
|
"privateAssets": "all"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json"
|
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.311/PortableRuntimeIdentifierGraph.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\.nuget\packages\</NuGetPackageRoot>
|
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\.nuget\packages\</NuGetPackageRoot>
|
||||||
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\nanxun\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages</NuGetPackageFolders>
|
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\nanxun\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages</NuGetPackageFolders>
|
||||||
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
|
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
|
||||||
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.14.1</NuGetToolVersion>
|
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.14.2</NuGetToolVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||||
<SourceRoot Include="C:\Users\nanxun\.nuget\packages\" />
|
<SourceRoot Include="C:\Users\nanxun\.nuget\packages\" />
|
||||||
|
|||||||
@ -1679,6 +1679,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"SixLabors.ImageSharp/3.1.12": {
|
||||||
|
"type": "package",
|
||||||
|
"compile": {
|
||||||
|
"lib/net6.0/SixLabors.ImageSharp.dll": {
|
||||||
|
"related": ".xml"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"lib/net6.0/SixLabors.ImageSharp.dll": {
|
||||||
|
"related": ".xml"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"build/_._": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
"StackExchange.Redis/2.9.32": {
|
"StackExchange.Redis/2.9.32": {
|
||||||
"type": "package",
|
"type": "package",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -3027,6 +3043,7 @@
|
|||||||
"Newtonsoft.Json": "13.0.4",
|
"Newtonsoft.Json": "13.0.4",
|
||||||
"Pomelo.EntityFrameworkCore.MySql": "8.0.3",
|
"Pomelo.EntityFrameworkCore.MySql": "8.0.3",
|
||||||
"RedLock.net": "2.3.2",
|
"RedLock.net": "2.3.2",
|
||||||
|
"SixLabors.ImageSharp": "3.1.12",
|
||||||
"StackExchange.Redis": "2.9.32",
|
"StackExchange.Redis": "2.9.32",
|
||||||
"Swashbuckle.AspNetCore": "6.6.2",
|
"Swashbuckle.AspNetCore": "6.6.2",
|
||||||
"System.IdentityModel.Tokens.Jwt": "8.14.0"
|
"System.IdentityModel.Tokens.Jwt": "8.14.0"
|
||||||
@ -5363,6 +5380,22 @@
|
|||||||
"runtimes/ubuntu.16.10-x64/native/System.Security.Cryptography.Native.OpenSsl.so"
|
"runtimes/ubuntu.16.10-x64/native/System.Security.Cryptography.Native.OpenSsl.so"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"SixLabors.ImageSharp/3.1.12": {
|
||||||
|
"sha512": "iAg6zifihXEFS/t7fiHhZBGAdCp3FavsF4i2ZIDp0JfeYeDVzvmlbY1CNhhIKimaIzrzSi5M/NBFcWvZT2rB/A==",
|
||||||
|
"type": "package",
|
||||||
|
"path": "sixlabors.imagesharp/3.1.12",
|
||||||
|
"files": [
|
||||||
|
".nupkg.metadata",
|
||||||
|
".signature.p7s",
|
||||||
|
"LICENSE",
|
||||||
|
"build/SixLabors.ImageSharp.props",
|
||||||
|
"lib/net6.0/SixLabors.ImageSharp.dll",
|
||||||
|
"lib/net6.0/SixLabors.ImageSharp.xml",
|
||||||
|
"sixlabors.imagesharp.128.png",
|
||||||
|
"sixlabors.imagesharp.3.1.12.nupkg.sha512",
|
||||||
|
"sixlabors.imagesharp.nuspec"
|
||||||
|
]
|
||||||
|
},
|
||||||
"StackExchange.Redis/2.9.32": {
|
"StackExchange.Redis/2.9.32": {
|
||||||
"sha512": "j5Rjbf7gWz5izrn0UWQy9RlQY4cQDPkwJfVqATnVsOa/+zzJrps12LOgacMsDl/Vit2f01cDiDkG/Rst8v2iGw==",
|
"sha512": "j5Rjbf7gWz5izrn0UWQy9RlQY4cQDPkwJfVqATnVsOa/+zzJrps12LOgacMsDl/Vit2f01cDiDkG/Rst8v2iGw==",
|
||||||
"type": "package",
|
"type": "package",
|
||||||
@ -8962,7 +8995,7 @@
|
|||||||
"privateAssets": "all"
|
"privateAssets": "all"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json"
|
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.311/PortableRuntimeIdentifierGraph.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"dgSpecHash": "ueA0djhC8vQ=",
|
"dgSpecHash": "E2DnflEnEuk=",
|
||||||
"success": true,
|
"success": true,
|
||||||
"projectFilePath": "C:\\Users\\nanxun\\Documents\\IM\\backend\\IMTest\\IMTest.csproj",
|
"projectFilePath": "C:\\Users\\nanxun\\Documents\\IM\\backend\\IMTest\\IMTest.csproj",
|
||||||
"expectedPackageFiles": [
|
"expectedPackageFiles": [
|
||||||
@ -97,6 +97,7 @@
|
|||||||
"C:\\Users\\nanxun\\.nuget\\packages\\runtime.ubuntu.14.04-x64.runtime.native.system.security.cryptography.openssl\\4.3.0\\runtime.ubuntu.14.04-x64.runtime.native.system.security.cryptography.openssl.4.3.0.nupkg.sha512",
|
"C:\\Users\\nanxun\\.nuget\\packages\\runtime.ubuntu.14.04-x64.runtime.native.system.security.cryptography.openssl\\4.3.0\\runtime.ubuntu.14.04-x64.runtime.native.system.security.cryptography.openssl.4.3.0.nupkg.sha512",
|
||||||
"C:\\Users\\nanxun\\.nuget\\packages\\runtime.ubuntu.16.04-x64.runtime.native.system.security.cryptography.openssl\\4.3.0\\runtime.ubuntu.16.04-x64.runtime.native.system.security.cryptography.openssl.4.3.0.nupkg.sha512",
|
"C:\\Users\\nanxun\\.nuget\\packages\\runtime.ubuntu.16.04-x64.runtime.native.system.security.cryptography.openssl\\4.3.0\\runtime.ubuntu.16.04-x64.runtime.native.system.security.cryptography.openssl.4.3.0.nupkg.sha512",
|
||||||
"C:\\Users\\nanxun\\.nuget\\packages\\runtime.ubuntu.16.10-x64.runtime.native.system.security.cryptography.openssl\\4.3.0\\runtime.ubuntu.16.10-x64.runtime.native.system.security.cryptography.openssl.4.3.0.nupkg.sha512",
|
"C:\\Users\\nanxun\\.nuget\\packages\\runtime.ubuntu.16.10-x64.runtime.native.system.security.cryptography.openssl\\4.3.0\\runtime.ubuntu.16.10-x64.runtime.native.system.security.cryptography.openssl.4.3.0.nupkg.sha512",
|
||||||
|
"C:\\Users\\nanxun\\.nuget\\packages\\sixlabors.imagesharp\\3.1.12\\sixlabors.imagesharp.3.1.12.nupkg.sha512",
|
||||||
"C:\\Users\\nanxun\\.nuget\\packages\\stackexchange.redis\\2.9.32\\stackexchange.redis.2.9.32.nupkg.sha512",
|
"C:\\Users\\nanxun\\.nuget\\packages\\stackexchange.redis\\2.9.32\\stackexchange.redis.2.9.32.nupkg.sha512",
|
||||||
"C:\\Users\\nanxun\\.nuget\\packages\\swashbuckle.aspnetcore\\6.6.2\\swashbuckle.aspnetcore.6.6.2.nupkg.sha512",
|
"C:\\Users\\nanxun\\.nuget\\packages\\swashbuckle.aspnetcore\\6.6.2\\swashbuckle.aspnetcore.6.6.2.nupkg.sha512",
|
||||||
"C:\\Users\\nanxun\\.nuget\\packages\\swashbuckle.aspnetcore.swagger\\6.6.2\\swashbuckle.aspnetcore.swagger.6.6.2.nupkg.sha512",
|
"C:\\Users\\nanxun\\.nuget\\packages\\swashbuckle.aspnetcore.swagger\\6.6.2\\swashbuckle.aspnetcore.swagger.6.6.2.nupkg.sha512",
|
||||||
|
|||||||
3
backend/IM_API/.gitignore
vendored
3
backend/IM_API/.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
bin/
|
bin/
|
||||||
obj/
|
obj/
|
||||||
.vs/
|
.vs/
|
||||||
|
uploads/
|
||||||
@ -17,11 +17,13 @@ namespace IM_API.Application.EventHandlers.MessageCreatedHandler
|
|||||||
private readonly IHubContext<ChatHub> _hub;
|
private readonly IHubContext<ChatHub> _hub;
|
||||||
private readonly IMapper _mapper;
|
private readonly IMapper _mapper;
|
||||||
private readonly IUserService _userService;
|
private readonly IUserService _userService;
|
||||||
public SignalREventHandler(IHubContext<ChatHub> hub, IMapper mapper,IUserService userService)
|
public SignalREventHandler(IHubContext<ChatHub> hub, IMapper mapper,
|
||||||
|
IUserService userService)
|
||||||
{
|
{
|
||||||
_hub = hub;
|
_hub = hub;
|
||||||
_mapper = mapper;
|
_mapper = mapper;
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Consume(ConsumeContext<MessageCreatedEvent> context)
|
public async Task Consume(ConsumeContext<MessageCreatedEvent> context)
|
||||||
@ -35,6 +37,10 @@ namespace IM_API.Application.EventHandlers.MessageCreatedHandler
|
|||||||
var senderinfo = await _userService.GetUserInfoAsync(@event.MsgSenderId);
|
var senderinfo = await _userService.GetUserInfoAsync(@event.MsgSenderId);
|
||||||
messageBaseVo.SenderName = senderinfo.NickName;
|
messageBaseVo.SenderName = senderinfo.NickName;
|
||||||
messageBaseVo.SenderAvatar = senderinfo.Avatar ?? "";
|
messageBaseVo.SenderAvatar = senderinfo.Avatar ?? "";
|
||||||
|
if (messageBaseVo.Type != MessageMsgType.Text)
|
||||||
|
{
|
||||||
|
messageBaseVo.Content = UrlTools.ProcessMessageUrl(messageBaseVo.Content, @event.BaseUrl);
|
||||||
|
}
|
||||||
await _hub.Clients.Group(@event.StreamKey).SendAsync("ReceiveMessage", new HubResponse<MessageBaseVo>("Event", messageBaseVo));
|
await _hub.Clients.Group(@event.StreamKey).SendAsync("ReceiveMessage", new HubResponse<MessageBaseVo>("Event", messageBaseVo));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
@ -0,0 +1,21 @@
|
|||||||
|
using IM_API.Domain.Events;
|
||||||
|
using IM_API.Interface.Services;
|
||||||
|
using MassTransit;
|
||||||
|
|
||||||
|
namespace IM_API.Application.EventHandlers.UploadEventHandler
|
||||||
|
{
|
||||||
|
public class MergeEventHandler : IConsumer<UploadMergeEvent>
|
||||||
|
{
|
||||||
|
private readonly IStorageService _storage;
|
||||||
|
public MergeEventHandler(IStorageService storage)
|
||||||
|
{
|
||||||
|
_storage = storage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Consume(ConsumeContext<UploadMergeEvent> context)
|
||||||
|
{
|
||||||
|
var @event = context.Message;
|
||||||
|
await _storage.MergeAsync(@event.TaskId, @event.ObjectName, @event.ChunckCount, @event.Parts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,6 +7,7 @@ using IM_API.Application.EventHandlers.GroupRequestHandler;
|
|||||||
using IM_API.Application.EventHandlers.GroupRequestUpdateHandler;
|
using IM_API.Application.EventHandlers.GroupRequestUpdateHandler;
|
||||||
using IM_API.Application.EventHandlers.MessageCreatedHandler;
|
using IM_API.Application.EventHandlers.MessageCreatedHandler;
|
||||||
using IM_API.Application.EventHandlers.RequestFriendHandler;
|
using IM_API.Application.EventHandlers.RequestFriendHandler;
|
||||||
|
using IM_API.Application.EventHandlers.UploadEventHandler;
|
||||||
using IM_API.Configs.Options;
|
using IM_API.Configs.Options;
|
||||||
using IM_API.Domain.Events;
|
using IM_API.Domain.Events;
|
||||||
using MassTransit;
|
using MassTransit;
|
||||||
@ -37,6 +38,7 @@ namespace IM_API.Configs
|
|||||||
x.AddConsumer<RequestDbHandler>();
|
x.AddConsumer<RequestDbHandler>();
|
||||||
x.AddConsumer<SignalRHandler>();
|
x.AddConsumer<SignalRHandler>();
|
||||||
x.AddConsumer<RequestUpdateSignalrHandler>();
|
x.AddConsumer<RequestUpdateSignalrHandler>();
|
||||||
|
x.AddConsumer<MergeEventHandler>();
|
||||||
x.UsingRabbitMq((ctx,cfg) =>
|
x.UsingRabbitMq((ctx,cfg) =>
|
||||||
{
|
{
|
||||||
cfg.Host(options.Host, "/", h =>
|
cfg.Host(options.Host, "/", h =>
|
||||||
|
|||||||
@ -4,10 +4,14 @@ using IM_API.Dtos;
|
|||||||
using IM_API.Dtos.Auth;
|
using IM_API.Dtos.Auth;
|
||||||
using IM_API.Dtos.Friend;
|
using IM_API.Dtos.Friend;
|
||||||
using IM_API.Dtos.Group;
|
using IM_API.Dtos.Group;
|
||||||
|
using IM_API.Dtos.Message;
|
||||||
using IM_API.Dtos.User;
|
using IM_API.Dtos.User;
|
||||||
using IM_API.Models;
|
using IM_API.Models;
|
||||||
|
using IM_API.Models.Upload;
|
||||||
using IM_API.Tools;
|
using IM_API.Tools;
|
||||||
|
using IM_API.VOs;
|
||||||
using IM_API.VOs.Conversation;
|
using IM_API.VOs.Conversation;
|
||||||
|
using IM_API.VOs.Group;
|
||||||
using IM_API.VOs.Message;
|
using IM_API.VOs.Message;
|
||||||
|
|
||||||
namespace IM_API.Configs
|
namespace IM_API.Configs
|
||||||
@ -171,6 +175,46 @@ namespace IM_API.Configs
|
|||||||
.ForMember(dest => dest.AuhorityEnum, opt => opt.MapFrom(src => GroupAuhority.REQUIRE_CONSENT))
|
.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))
|
||||||
;
|
;
|
||||||
|
|
||||||
|
//上传任务模型转换
|
||||||
|
CreateMap<CreateUploadTaskDto, UploadTask>()
|
||||||
|
.ForMember(dest => dest.FileName, opt => opt.MapFrom(src => src.FileName))
|
||||||
|
.ForMember(dest => dest.Status, opt => opt.MapFrom(src => UploadStatus.Created))
|
||||||
|
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => Guid.NewGuid()))
|
||||||
|
.ForMember(dest => dest.FileSize, opt => opt.MapFrom(src => src.FileSize))
|
||||||
|
.ForMember(dest => dest.FileHash, opt => opt.MapFrom(src => src.FileHash))
|
||||||
|
.ForMember(dest => dest.ContentType, opt => opt.MapFrom(src => src.ContentType))
|
||||||
|
.ForMember(dest => dest.CreatedAt, opt => opt.MapFrom(src => DateTime.UtcNow))
|
||||||
|
;
|
||||||
|
|
||||||
|
CreateMap<UploadTask, CreateUploadTaskVo>()
|
||||||
|
.ForMember(dest => dest.TaskId, opt => opt.MapFrom(src => src.Id))
|
||||||
|
.ForMember(dest => dest.ChunkSize, opt => opt.MapFrom(src => src.ChunkSize))
|
||||||
|
.ForMember(dest => dest.TotalChunks, opt => opt.MapFrom(src => src.TotalChunks))
|
||||||
|
.ForMember(dest => dest.Concurrency, opt => opt.MapFrom(src => 5))
|
||||||
|
.ForMember(dest => dest.Skip, opt => opt.MapFrom(src => false))
|
||||||
|
.ForMember(dest => dest.Url, opt => opt.MapFrom(src => src.ObjectName))
|
||||||
|
;
|
||||||
|
|
||||||
|
CreateMap<UploadTask, ImageDto>()
|
||||||
|
.ForMember(dest => dest.Url, opt => opt.MapFrom(src => src.ObjectName))
|
||||||
|
.ForMember(dest => dest.FileId, opt => opt.MapFrom(src => src.Id))
|
||||||
|
.ForMember(dest => dest.Provider, opt => opt.MapFrom(src => src.StorageProvider))
|
||||||
|
.ForMember(dest => dest.Format, opt => opt.MapFrom(src => src.ContentType))
|
||||||
|
.ForMember(dest => dest.Size, opt => opt.MapFrom(src => src.FileSize));
|
||||||
|
|
||||||
|
CreateMap<ImageDto, VideoDto>();
|
||||||
|
|
||||||
|
//群成员模型
|
||||||
|
CreateMap<UserInfoDto, GroupMemberVo>()
|
||||||
|
.ForMember(dest => dest.Nickname, opt => opt.MapFrom(src => src.NickName))
|
||||||
|
.ForMember(dest => dest.Username, opt => opt.MapFrom(src => src.Username))
|
||||||
|
.ForMember(dest => dest.UserId, opt => opt.MapFrom(src => src.Id))
|
||||||
|
.ForMember(dest => dest.Avatar, opt => opt.MapFrom(src => src.Avatar));
|
||||||
|
|
||||||
|
CreateMap<GroupMember, GroupMemberVo>()
|
||||||
|
.ForMember(dest => dest.Created, opt => opt.MapFrom(src => src.Created))
|
||||||
|
.ForMember(dest => dest.Role, opt => opt.MapFrom(src => src.RoleEnum));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
backend/IM_API/Configs/Options/FileUploadOptions.cs
Normal file
8
backend/IM_API/Configs/Options/FileUploadOptions.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace IM_API.Configs.Options
|
||||||
|
{
|
||||||
|
public class FileUploadOptions
|
||||||
|
{
|
||||||
|
public string DefaultStorage { get; set; }
|
||||||
|
public int ChunkSize { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -29,7 +29,8 @@ namespace IM_API.Configs
|
|||||||
services.AddScoped<IGroupService, GroupService>();
|
services.AddScoped<IGroupService, GroupService>();
|
||||||
services.AddScoped<ISequenceIdService, SequenceIdService>();
|
services.AddScoped<ISequenceIdService, SequenceIdService>();
|
||||||
services.AddScoped<ICacheService, RedisCacheService>();
|
services.AddScoped<ICacheService, RedisCacheService>();
|
||||||
services.AddScoped<IEventBus, InMemoryEventBus>();
|
services.AddScoped<IStorageService, LocalStorageService>();
|
||||||
|
services.AddScoped<IUploadTaskService, UploadTaskService>();
|
||||||
services.AddSingleton<IJWTService, JWTService>();
|
services.AddSingleton<IJWTService, JWTService>();
|
||||||
services.AddSingleton<IRefreshTokenService, RedisRefreshTokenService>();
|
services.AddSingleton<IRefreshTokenService, RedisRefreshTokenService>();
|
||||||
services.AddSingleton<IDistributedLockFactory>(sp =>
|
services.AddSingleton<IDistributedLockFactory>(sp =>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
using IM_API.Dtos;
|
using IM_API.Dtos;
|
||||||
using IM_API.Dtos.Group;
|
using IM_API.Dtos.Group;
|
||||||
using IM_API.Interface.Services;
|
using IM_API.Interface.Services;
|
||||||
|
using IM_API.VOs.Group;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@ -67,5 +68,14 @@ namespace IM_API.Controllers
|
|||||||
var res = new BaseResponse<object?>();
|
var res = new BaseResponse<object?>();
|
||||||
return Ok(res);
|
return Ok(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[ProducesResponseType(typeof(BaseResponse<List<GroupMemberVo>>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<IActionResult> GetGroupMembers([FromQuery]int groupId)
|
||||||
|
{
|
||||||
|
var useridStr = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
|
var members = await _groupService.GetGroupMembers(int.Parse(useridStr), groupId);
|
||||||
|
return Ok(new BaseResponse<List<GroupMemberVo>>(members));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,8 @@ using IM_API.Domain.Events;
|
|||||||
using IM_API.Dtos;
|
using IM_API.Dtos;
|
||||||
using IM_API.Dtos.Message;
|
using IM_API.Dtos.Message;
|
||||||
using IM_API.Interface.Services;
|
using IM_API.Interface.Services;
|
||||||
|
using IM_API.Models;
|
||||||
|
using IM_API.Tools;
|
||||||
using IM_API.VOs.Message;
|
using IM_API.VOs.Message;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
@ -19,12 +21,12 @@ namespace IM_API.Controllers
|
|||||||
{
|
{
|
||||||
private readonly IMessageSevice _messageService;
|
private readonly IMessageSevice _messageService;
|
||||||
private readonly ILogger<MessageController> _logger;
|
private readonly ILogger<MessageController> _logger;
|
||||||
private readonly IEventBus _eventBus;
|
public MessageController(IMessageSevice messageService,
|
||||||
public MessageController(IMessageSevice messageService, ILogger<MessageController> logger, IEventBus eventBus)
|
ILogger<MessageController> logger)
|
||||||
{
|
{
|
||||||
_messageService = messageService;
|
_messageService = messageService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_eventBus = eventBus;
|
|
||||||
}
|
}
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[ProducesResponseType(typeof(BaseResponse<MessageBaseVo>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(BaseResponse<MessageBaseVo>), StatusCodes.Status200OK)]
|
||||||
@ -32,15 +34,23 @@ namespace IM_API.Controllers
|
|||||||
{
|
{
|
||||||
var userIdstr = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
var userIdstr = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
MessageBaseVo messageBaseVo = new MessageBaseVo();
|
MessageBaseVo messageBaseVo = new MessageBaseVo();
|
||||||
|
var handledMessage = await _messageService.HandleFileMessageContentAsync(dto);
|
||||||
if(dto.ChatType == Models.ChatType.PRIVATE)
|
if(dto.ChatType == Models.ChatType.PRIVATE)
|
||||||
{
|
{
|
||||||
messageBaseVo = await _messageService.SendPrivateMessageAsync(int.Parse(userIdstr), dto.ReceiverId, dto);
|
messageBaseVo = await _messageService.SendPrivateMessageAsync(int.Parse(userIdstr), dto.ReceiverId, handledMessage);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
messageBaseVo = await _messageService.SendGroupMessageAsync(int.Parse(userIdstr), dto.ReceiverId, dto);
|
messageBaseVo = await _messageService.SendGroupMessageAsync(int.Parse(userIdstr), dto.ReceiverId, handledMessage);
|
||||||
}
|
}
|
||||||
return Ok(new BaseResponse<MessageBaseVo>(messageBaseVo));
|
|
||||||
|
if (messageBaseVo.Type != MessageMsgType.Text)
|
||||||
|
{
|
||||||
|
var request = HttpContext?.Request;
|
||||||
|
var baseUrl = $"{request.Scheme}://{request.Host}";
|
||||||
|
messageBaseVo.Content = UrlTools.ProcessMessageUrl(messageBaseVo.Content, baseUrl);
|
||||||
|
}
|
||||||
|
return Ok(new BaseResponse<MessageBaseVo>(messageBaseVo));
|
||||||
}
|
}
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[ProducesResponseType(typeof(BaseResponse<List<MessageBaseVo>>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(BaseResponse<List<MessageBaseVo>>), StatusCodes.Status200OK)]
|
||||||
|
|||||||
124
backend/IM_API/Controllers/UploadController.cs
Normal file
124
backend/IM_API/Controllers/UploadController.cs
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
using IM_API.Dtos;
|
||||||
|
using IM_API.Interface.Services;
|
||||||
|
using IM_API.Models.Upload;
|
||||||
|
using IM_API.Tools;
|
||||||
|
using IM_API.VOs;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using IDatabase = StackExchange.Redis.IDatabase;
|
||||||
|
|
||||||
|
namespace IM_API.Controllers
|
||||||
|
{
|
||||||
|
[Authorize]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[ApiController]
|
||||||
|
public class UploadController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IWebHostEnvironment _env;
|
||||||
|
private readonly IStorageService _storage;
|
||||||
|
private readonly IDatabase _redis;
|
||||||
|
public UploadController(IWebHostEnvironment env, IStorageService storage, IConnectionMultiplexer connectionMultiplexer)
|
||||||
|
{
|
||||||
|
_env = env;
|
||||||
|
_storage = storage;
|
||||||
|
_redis = connectionMultiplexer.GetDatabase();
|
||||||
|
}
|
||||||
|
[HttpPost("local/{taskId}/parts/{partNumber}")]
|
||||||
|
[ProducesResponseType(typeof(BaseResponse<object?>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<IActionResult> LocalUpload(Guid taskId, int partNumber, IFormFile file)
|
||||||
|
{
|
||||||
|
var baseDir = Path.Combine(_env.ContentRootPath, "uploads"); // 项目根目录下 uploads
|
||||||
|
Directory.CreateDirectory(baseDir);
|
||||||
|
|
||||||
|
var path = Path.Combine(baseDir, "temp", taskId.ToString(), $"{partNumber}.part.tmp");
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||||
|
|
||||||
|
using var stream = System.IO.File.Create(path);
|
||||||
|
await file.CopyToAsync(stream);
|
||||||
|
|
||||||
|
await _redis.SetAddAsync(RedisKeys.GetUploadPartKey(taskId), partNumber);
|
||||||
|
|
||||||
|
return Ok(new BaseResponse<object?>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("CreateTask")]
|
||||||
|
[ProducesResponseType(typeof(BaseResponse<CreateUploadTaskVo>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<IActionResult> CreateUpload(CreateUploadTaskDto dto)
|
||||||
|
{
|
||||||
|
var vo = await _storage.InitTaskAsync(dto);
|
||||||
|
return Ok(new BaseResponse<CreateUploadTaskVo>(vo));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("CreatePart")]
|
||||||
|
public async Task<IActionResult> CreatePart(Guid taskId, int partNum)
|
||||||
|
{
|
||||||
|
var vo = await _storage.CreatePartInstructionAsync(taskId, partNum);
|
||||||
|
return Ok(new BaseResponse<UploadPartInstructionVo>(vo));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("CompleteTask")]
|
||||||
|
public async Task<IActionResult> CompleteTask([FromQuery]Guid taskId, [FromBody]List<UploadPartDto> dtos)
|
||||||
|
{
|
||||||
|
var taskIdRes = await _storage.CompleteAsync(taskId, dtos);
|
||||||
|
return Ok(new BaseResponse<string>(data: taskIdRes.ToString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("events/{taskId}")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task Events(Guid taskId)
|
||||||
|
{
|
||||||
|
Response.Headers.Add("Content-Type", "text/event-stream");
|
||||||
|
Response.Headers.Add("Cache-Control", "no-cache");
|
||||||
|
Response.Headers.Add("Connection", "keep-alive");
|
||||||
|
var lastProgress = -1;
|
||||||
|
|
||||||
|
while (!HttpContext.RequestAborted.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
var hash = await _redis.HashGetAllAsync(RedisKeys.MergeStatus(taskId));
|
||||||
|
if (hash.Length == 0)
|
||||||
|
{
|
||||||
|
await Task.Delay(1000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var status = hash.FirstOrDefault(x => x.Name == "status").Value;
|
||||||
|
var progress = hash.FirstOrDefault(x => x.Name == "progress").Value;
|
||||||
|
var url = hash.FirstOrDefault(x => x.Name == "url").Value;
|
||||||
|
|
||||||
|
// 避免重复发送
|
||||||
|
if (progress != lastProgress)
|
||||||
|
{
|
||||||
|
var data = new
|
||||||
|
{
|
||||||
|
status = status.ToString(),
|
||||||
|
progress = progress.ToString(),
|
||||||
|
url = (string)url
|
||||||
|
};
|
||||||
|
|
||||||
|
await Response.WriteAsync($"data: {JsonSerializer.Serialize(data)}\n\n");
|
||||||
|
await Response.Body.FlushAsync();
|
||||||
|
|
||||||
|
// 完成后关闭 SSE
|
||||||
|
if (status == "Completed")
|
||||||
|
break;
|
||||||
|
|
||||||
|
await Task.Delay(1000); // 每秒检查一次
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("upload/{hash}")]
|
||||||
|
public async Task<IActionResult> UploadSmallFile(IFormFile file,string hash)
|
||||||
|
{
|
||||||
|
using var stream = file.OpenReadStream();
|
||||||
|
var res = await _storage.UploadSmallFileAsync(stream, file.FileName, file.ContentType, file.Length, hash);
|
||||||
|
return Ok(new BaseResponse<UploadTask>(res));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -16,6 +16,7 @@ namespace IM_API.Domain.Events
|
|||||||
public DateTimeOffset MessageCreated { get; set; }
|
public DateTimeOffset MessageCreated { get; set; }
|
||||||
public string StreamKey { get; set; }
|
public string StreamKey { get; set; }
|
||||||
public Guid ClientMsgId { get; set; }
|
public Guid ClientMsgId { get; set; }
|
||||||
|
public string BaseUrl { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
13
backend/IM_API/Domain/Events/UploadMergeEvent.cs
Normal file
13
backend/IM_API/Domain/Events/UploadMergeEvent.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using IM_API.Dtos;
|
||||||
|
|
||||||
|
namespace IM_API.Domain.Events
|
||||||
|
{
|
||||||
|
public record UploadMergeEvent : DomainEvent
|
||||||
|
{
|
||||||
|
public override string EventType => "IM.FILES_UPLOAD_MERGE";
|
||||||
|
public Guid TaskId { get; init; }
|
||||||
|
public List<UploadPartDto> Parts { get; init; }
|
||||||
|
public int ChunckCount { get; set; }
|
||||||
|
public string ObjectName { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
10
backend/IM_API/Dtos/CreateUploadTaskDto.cs
Normal file
10
backend/IM_API/Dtos/CreateUploadTaskDto.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
namespace IM_API.Dtos
|
||||||
|
{
|
||||||
|
public class CreateUploadTaskDto
|
||||||
|
{
|
||||||
|
public string FileName { get; set; } = default!;
|
||||||
|
public long FileSize { get; set; }
|
||||||
|
public string ContentType { get; set; } = default!;
|
||||||
|
public string FileHash { get; set; } = default!;
|
||||||
|
}
|
||||||
|
}
|
||||||
26
backend/IM_API/Dtos/Message/MessagTypeDto.cs
Normal file
26
backend/IM_API/Dtos/Message/MessagTypeDto.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
namespace IM_API.Dtos.Message
|
||||||
|
{
|
||||||
|
public class RequestMessageType
|
||||||
|
{
|
||||||
|
public Guid FileId { get; set; }
|
||||||
|
public long Size { get; set; }
|
||||||
|
}
|
||||||
|
public class BaseMessageType: RequestMessageType
|
||||||
|
{
|
||||||
|
public string Url { get; set; }
|
||||||
|
public string Provider { get; set; }
|
||||||
|
public string Format { get; set; }
|
||||||
|
public string Text { get; set; }
|
||||||
|
}
|
||||||
|
public class ImageDto() : BaseMessageType
|
||||||
|
{
|
||||||
|
public string Thumb { get; set; }
|
||||||
|
public int W { get; set; }
|
||||||
|
public int H { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class VideoDto() : ImageDto
|
||||||
|
{
|
||||||
|
public int Duration { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,7 +10,7 @@ namespace IM_API.Dtos
|
|||||||
public Guid MsgId { get; init; }
|
public Guid MsgId { get; init; }
|
||||||
public int SenderId { get; init; }
|
public int SenderId { get; init; }
|
||||||
public int ReceiverId { get; init; }
|
public int ReceiverId { get; init; }
|
||||||
public string Content { get; init; } = default!;
|
public string Content { get; set; } = default!;
|
||||||
public DateTimeOffset TimeStamp { get; init; }
|
public DateTimeOffset TimeStamp { get; init; }
|
||||||
public MessageBaseDto() { }
|
public MessageBaseDto() { }
|
||||||
}
|
}
|
||||||
|
|||||||
8
backend/IM_API/Dtos/UploadPartDto.cs
Normal file
8
backend/IM_API/Dtos/UploadPartDto.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace IM_API.Dtos
|
||||||
|
{
|
||||||
|
public class UploadPartDto
|
||||||
|
{
|
||||||
|
public int PartNumber { get; set; }
|
||||||
|
public string? ETag { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -28,6 +28,7 @@
|
|||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
||||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.3" />
|
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.3" />
|
||||||
<PackageReference Include="RedLock.net" Version="2.3.2" />
|
<PackageReference Include="RedLock.net" Version="2.3.2" />
|
||||||
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||||
<PackageReference Include="StackExchange.Redis" Version="2.9.32" />
|
<PackageReference Include="StackExchange.Redis" Version="2.9.32" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.14.0" />
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.14.0" />
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
using IM_API.Dtos.Group;
|
using IM_API.Dtos.Group;
|
||||||
using IM_API.Models;
|
using IM_API.Models;
|
||||||
|
using IM_API.VOs.Group;
|
||||||
|
|
||||||
namespace IM_API.Interface.Services
|
namespace IM_API.Interface.Services
|
||||||
{
|
{
|
||||||
@ -51,5 +52,6 @@ namespace IM_API.Interface.Services
|
|||||||
Task HandleGroupRequestAsync(int userid, HandleGroupRequestDto dto);
|
Task HandleGroupRequestAsync(int userid, HandleGroupRequestDto dto);
|
||||||
Task MakeGroupRequestAsync(int userId,int? adminUserId,int groupId);
|
Task MakeGroupRequestAsync(int userId,int? adminUserId,int groupId);
|
||||||
Task MakeGroupMemberAsync(int userId, int groupId, GroupMemberRole? role);
|
Task MakeGroupMemberAsync(int userId, int groupId, GroupMemberRole? role);
|
||||||
|
Task<List<GroupMemberVo>> GetGroupMembers(int userId, int groupId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -48,6 +48,6 @@ namespace IM_API.Interface.Services
|
|||||||
Task<bool> MarkConversationAsReadAsync(int userId,int? userBId,int? groupId);
|
Task<bool> MarkConversationAsReadAsync(int userId,int? userBId,int? groupId);
|
||||||
Task<bool> RecallMessageAsync(int userId,int messageId);
|
Task<bool> RecallMessageAsync(int userId,int messageId);
|
||||||
|
|
||||||
|
Task<MessageBaseDto> HandleFileMessageContentAsync(MessageBaseDto dto);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
40
backend/IM_API/Interface/Services/IStorageService.cs
Normal file
40
backend/IM_API/Interface/Services/IStorageService.cs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
using IM_API.Dtos;
|
||||||
|
using IM_API.Models.Upload;
|
||||||
|
using IM_API.VOs;
|
||||||
|
|
||||||
|
namespace IM_API.Interface.Services
|
||||||
|
{
|
||||||
|
public interface IStorageService
|
||||||
|
{
|
||||||
|
string ProviderName { get; }
|
||||||
|
UploadMode Mode { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化上传任务
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<CreateUploadTaskVo> InitTaskAsync(CreateUploadTaskDto dto);
|
||||||
|
/// <summary>
|
||||||
|
/// 创建分片任务
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="taskId">文件上传任务ID</param>
|
||||||
|
/// <param name="partNumer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<UploadPartInstructionVo> CreatePartInstructionAsync(Guid taskId, int partNumer);
|
||||||
|
Task<Guid> CompleteAsync(
|
||||||
|
Guid taskId,
|
||||||
|
List<UploadPartDto> parts
|
||||||
|
);
|
||||||
|
|
||||||
|
Task MergeAsync(Guid taskId, string objectName, int totalChunks, List<UploadPartDto> parts);
|
||||||
|
|
||||||
|
Task<UploadTask> UploadSmallFileAsync(Stream stream, string fileName, string fileType, long size, string hash);
|
||||||
|
string GetDownloadUrl(string objectname);
|
||||||
|
}
|
||||||
|
public enum UploadMode
|
||||||
|
{
|
||||||
|
Proxy, // 本地 / 后端中转
|
||||||
|
Direct // 云直传
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
12
backend/IM_API/Interface/Services/IUploadTaskService.cs
Normal file
12
backend/IM_API/Interface/Services/IUploadTaskService.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using IM_API.Models.Upload;
|
||||||
|
|
||||||
|
namespace IM_API.Interface.Services
|
||||||
|
{
|
||||||
|
public interface IUploadTaskService
|
||||||
|
{
|
||||||
|
Task AddAsync(UploadTask task);
|
||||||
|
Task<UploadTask?> GetTaskAsync(Guid taskId);
|
||||||
|
Task<UploadTask?> GetTaskAsync(string hash);
|
||||||
|
Task UpdateStatusAsync(Guid taskId, UploadStatus status);
|
||||||
|
}
|
||||||
|
}
|
||||||
1167
backend/IM_API/Migrations/20260214101014_add-uploadtask.Designer.cs
generated
Normal file
1167
backend/IM_API/Migrations/20260214101014_add-uploadtask.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
52
backend/IM_API/Migrations/20260214101014_add-uploadtask.cs
Normal file
52
backend/IM_API/Migrations/20260214101014_add-uploadtask.cs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace IM_API.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class adduploadtask : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "UploadTasks",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
FileName = table.Column<string>(type: "longtext", nullable: false, collation: "latin1_swedish_ci")
|
||||||
|
.Annotation("MySql:CharSet", "latin1"),
|
||||||
|
FileSize = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
FileHash = table.Column<string>(type: "longtext", nullable: false, collation: "latin1_swedish_ci")
|
||||||
|
.Annotation("MySql:CharSet", "latin1"),
|
||||||
|
ContentType = table.Column<string>(type: "longtext", nullable: false, collation: "latin1_swedish_ci")
|
||||||
|
.Annotation("MySql:CharSet", "latin1"),
|
||||||
|
ChunkSize = table.Column<int>(type: "int", nullable: false),
|
||||||
|
TotalChunks = table.Column<int>(type: "int", nullable: false),
|
||||||
|
Status = table.Column<int>(type: "int", nullable: false),
|
||||||
|
StorageProvider = table.Column<string>(type: "longtext", nullable: false, collation: "latin1_swedish_ci")
|
||||||
|
.Annotation("MySql:CharSet", "latin1"),
|
||||||
|
ObjectName = table.Column<string>(type: "longtext", nullable: false, collation: "latin1_swedish_ci")
|
||||||
|
.Annotation("MySql:CharSet", "latin1"),
|
||||||
|
ProviderUploadId = table.Column<string>(type: "longtext", nullable: true, collation: "latin1_swedish_ci")
|
||||||
|
.Annotation("MySql:CharSet", "latin1"),
|
||||||
|
CreatedAt = table.Column<DateTimeOffset>(type: "datetime(6)", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PRIMARY", x => x.Id);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "latin1")
|
||||||
|
.Annotation("Relational:Collation", "latin1_swedish_ci");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "UploadTasks");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1170
backend/IM_API/Migrations/20260214131542_update-uploadtask.Designer.cs
generated
Normal file
1170
backend/IM_API/Migrations/20260214131542_update-uploadtask.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
186
backend/IM_API/Migrations/20260214131542_update-uploadtask.cs
Normal file
186
backend/IM_API/Migrations/20260214131542_update-uploadtask.cs
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace IM_API.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class updateuploadtask : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.RenameTable(
|
||||||
|
name: "UploadTasks",
|
||||||
|
newName: "upload_tasks");
|
||||||
|
|
||||||
|
migrationBuilder.AlterTable(
|
||||||
|
name: "upload_tasks")
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.Annotation("Relational:Collation", "utf8mb4_general_ci")
|
||||||
|
.OldAnnotation("MySql:CharSet", "latin1")
|
||||||
|
.OldAnnotation("Relational:Collation", "latin1_swedish_ci");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "StorageProvider",
|
||||||
|
table: "upload_tasks",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: false,
|
||||||
|
collation: "utf8mb4_general_ci",
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "longtext")
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.OldAnnotation("MySql:CharSet", "latin1")
|
||||||
|
.OldAnnotation("Relational:Collation", "latin1_swedish_ci");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "ProviderUploadId",
|
||||||
|
table: "upload_tasks",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: true,
|
||||||
|
collation: "utf8mb4_general_ci",
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "longtext",
|
||||||
|
oldNullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.OldAnnotation("MySql:CharSet", "latin1")
|
||||||
|
.OldAnnotation("Relational:Collation", "latin1_swedish_ci");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "ObjectName",
|
||||||
|
table: "upload_tasks",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: false,
|
||||||
|
collation: "utf8mb4_general_ci",
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "longtext")
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.OldAnnotation("MySql:CharSet", "latin1")
|
||||||
|
.OldAnnotation("Relational:Collation", "latin1_swedish_ci");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "FileName",
|
||||||
|
table: "upload_tasks",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: false,
|
||||||
|
collation: "utf8mb4_general_ci",
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "longtext")
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.OldAnnotation("MySql:CharSet", "latin1")
|
||||||
|
.OldAnnotation("Relational:Collation", "latin1_swedish_ci");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "FileHash",
|
||||||
|
table: "upload_tasks",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: false,
|
||||||
|
collation: "utf8mb4_general_ci",
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "longtext")
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.OldAnnotation("MySql:CharSet", "latin1")
|
||||||
|
.OldAnnotation("Relational:Collation", "latin1_swedish_ci");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "ContentType",
|
||||||
|
table: "upload_tasks",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: false,
|
||||||
|
collation: "utf8mb4_general_ci",
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "longtext")
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.OldAnnotation("MySql:CharSet", "latin1")
|
||||||
|
.OldAnnotation("Relational:Collation", "latin1_swedish_ci");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.RenameTable(
|
||||||
|
name: "upload_tasks",
|
||||||
|
newName: "UploadTasks");
|
||||||
|
|
||||||
|
migrationBuilder.AlterTable(
|
||||||
|
name: "UploadTasks")
|
||||||
|
.Annotation("MySql:CharSet", "latin1")
|
||||||
|
.Annotation("Relational:Collation", "latin1_swedish_ci")
|
||||||
|
.OldAnnotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.OldAnnotation("Relational:Collation", "utf8mb4_general_ci");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "StorageProvider",
|
||||||
|
table: "UploadTasks",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: false,
|
||||||
|
collation: "latin1_swedish_ci",
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "longtext")
|
||||||
|
.Annotation("MySql:CharSet", "latin1")
|
||||||
|
.OldAnnotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.OldAnnotation("Relational:Collation", "utf8mb4_general_ci");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "ProviderUploadId",
|
||||||
|
table: "UploadTasks",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: true,
|
||||||
|
collation: "latin1_swedish_ci",
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "longtext",
|
||||||
|
oldNullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "latin1")
|
||||||
|
.OldAnnotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.OldAnnotation("Relational:Collation", "utf8mb4_general_ci");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "ObjectName",
|
||||||
|
table: "UploadTasks",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: false,
|
||||||
|
collation: "latin1_swedish_ci",
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "longtext")
|
||||||
|
.Annotation("MySql:CharSet", "latin1")
|
||||||
|
.OldAnnotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.OldAnnotation("Relational:Collation", "utf8mb4_general_ci");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "FileName",
|
||||||
|
table: "UploadTasks",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: false,
|
||||||
|
collation: "latin1_swedish_ci",
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "longtext")
|
||||||
|
.Annotation("MySql:CharSet", "latin1")
|
||||||
|
.OldAnnotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.OldAnnotation("Relational:Collation", "utf8mb4_general_ci");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "FileHash",
|
||||||
|
table: "UploadTasks",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: false,
|
||||||
|
collation: "latin1_swedish_ci",
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "longtext")
|
||||||
|
.Annotation("MySql:CharSet", "latin1")
|
||||||
|
.OldAnnotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.OldAnnotation("Relational:Collation", "utf8mb4_general_ci");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "ContentType",
|
||||||
|
table: "UploadTasks",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: false,
|
||||||
|
collation: "latin1_swedish_ci",
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "longtext")
|
||||||
|
.Annotation("MySql:CharSet", "latin1")
|
||||||
|
.OldAnnotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.OldAnnotation("Relational:Collation", "utf8mb4_general_ci");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1173
backend/IM_API/Migrations/20260306065353_uploadtask-url.Designer.cs
generated
Normal file
1173
backend/IM_API/Migrations/20260306065353_uploadtask-url.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
backend/IM_API/Migrations/20260306065353_uploadtask-url.cs
Normal file
30
backend/IM_API/Migrations/20260306065353_uploadtask-url.cs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace IM_API.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class uploadtaskurl : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Url",
|
||||||
|
table: "upload_tasks",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: true,
|
||||||
|
collation: "utf8mb4_general_ci")
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Url",
|
||||||
|
table: "upload_tasks");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -768,6 +768,62 @@ namespace IM_API.Migrations
|
|||||||
MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci");
|
MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("IM_API.Models.Upload.UploadTask", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<int>("ChunkSize")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("ContentType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("FileHash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("FileName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<long>("FileSize")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<string>("ObjectName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderUploadId")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("StorageProvider")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<int>("TotalChunks")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Url")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("PRIMARY");
|
||||||
|
|
||||||
|
b.ToTable("upload_tasks", (string)null);
|
||||||
|
|
||||||
|
MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4");
|
||||||
|
MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("IM_API.Models.User", b =>
|
modelBuilder.Entity("IM_API.Models.User", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using IM_API.Models.Upload;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace IM_API.Models;
|
namespace IM_API.Models;
|
||||||
@ -18,6 +19,7 @@ public partial class ImContext : DbContext
|
|||||||
public virtual DbSet<Device> Devices { get; set; }
|
public virtual DbSet<Device> Devices { get; set; }
|
||||||
|
|
||||||
public virtual DbSet<File> Files { get; set; }
|
public virtual DbSet<File> Files { get; set; }
|
||||||
|
public virtual DbSet<UploadTask> UploadTasks { get; set; }
|
||||||
|
|
||||||
public virtual DbSet<Friend> Friends { get; set; }
|
public virtual DbSet<Friend> Friends { get; set; }
|
||||||
|
|
||||||
@ -208,6 +210,17 @@ public partial class ImContext : DbContext
|
|||||||
.HasConstraintName("files_ibfk_1");
|
.HasConstraintName("files_ibfk_1");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<UploadTask>(entity =>
|
||||||
|
{
|
||||||
|
entity.HasKey(e => e.Id).HasName("PRIMARY");
|
||||||
|
|
||||||
|
entity
|
||||||
|
.ToTable("upload_tasks")
|
||||||
|
.HasCharSet("utf8mb4")
|
||||||
|
.UseCollation("utf8mb4_general_ci");
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<Friend>(entity =>
|
modelBuilder.Entity<Friend>(entity =>
|
||||||
{
|
{
|
||||||
entity.HasKey(e => e.Id).HasName("PRIMARY");
|
entity.HasKey(e => e.Id).HasName("PRIMARY");
|
||||||
|
|||||||
10
backend/IM_API/Models/Upload/UploadStatus.cs
Normal file
10
backend/IM_API/Models/Upload/UploadStatus.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
namespace IM_API.Models.Upload
|
||||||
|
{
|
||||||
|
public enum UploadStatus
|
||||||
|
{
|
||||||
|
Created,
|
||||||
|
Uploading,
|
||||||
|
Completed,
|
||||||
|
Aborted
|
||||||
|
}
|
||||||
|
}
|
||||||
25
backend/IM_API/Models/Upload/UploadTask.cs
Normal file
25
backend/IM_API/Models/Upload/UploadTask.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
namespace IM_API.Models.Upload
|
||||||
|
{
|
||||||
|
public class UploadTask
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
public string FileName { get; set; } = default!;
|
||||||
|
public long FileSize { get; set; }
|
||||||
|
public string FileHash { get; set; }
|
||||||
|
public string ContentType { get; set; } = default!;
|
||||||
|
|
||||||
|
public int ChunkSize { get; set; }
|
||||||
|
public int TotalChunks { get; set; }
|
||||||
|
|
||||||
|
public UploadStatus Status { get; set; }
|
||||||
|
|
||||||
|
public string StorageProvider { get; set; } = default!;
|
||||||
|
public string ObjectName { get; set; } = default!;
|
||||||
|
|
||||||
|
public string? ProviderUploadId { get; set; } // OSS/S3 UploadId
|
||||||
|
|
||||||
|
public DateTimeOffset CreatedAt { get; set; }
|
||||||
|
public string? Url { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,6 +7,7 @@ using IM_API.Models;
|
|||||||
using IM_API.Tools;
|
using IM_API.Tools;
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.FileProviders;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using StackExchange.Redis;
|
using StackExchange.Redis;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@ -42,7 +43,9 @@ namespace IM_API
|
|||||||
});
|
});
|
||||||
|
|
||||||
builder.Services.AddRabbitMQ(configuration.GetSection("RabbitMqOptions").Get<RabbitMQOptions>());
|
builder.Services.AddRabbitMQ(configuration.GetSection("RabbitMqOptions").Get<RabbitMQOptions>());
|
||||||
|
|
||||||
|
builder.Services.AddHttpContextAccessor();
|
||||||
|
|
||||||
builder.Services.AddAllService(configuration);
|
builder.Services.AddAllService(configuration);
|
||||||
|
|
||||||
builder.Services.AddSignalR().AddJsonProtocol(options =>
|
builder.Services.AddSignalR().AddJsonProtocol(options =>
|
||||||
@ -134,6 +137,23 @@ namespace IM_API
|
|||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
string uploadPath = Path.Combine(Directory.GetCurrentDirectory(), "Uploads","files");
|
||||||
|
|
||||||
|
// 2. 如果文件夹不存在则创建,防止程序启动报错
|
||||||
|
if (!Directory.Exists(uploadPath))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(uploadPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 配置静态文件映射
|
||||||
|
app.UseStaticFiles(new StaticFileOptions
|
||||||
|
{
|
||||||
|
// 指定物理磁盘路径
|
||||||
|
FileProvider = new PhysicalFileProvider(uploadPath),
|
||||||
|
// 指定浏览器访问的虚拟前缀(例如:http://localhost:5000/files/1.jpg)
|
||||||
|
RequestPath = "/uploads/files"
|
||||||
|
});
|
||||||
|
|
||||||
app.UseCors();
|
app.UseCors();
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
|
|||||||
@ -34,8 +34,9 @@ namespace IM_API.Services
|
|||||||
var privateList = await (from c in _context.Conversations
|
var privateList = await (from c in _context.Conversations
|
||||||
join f in _context.Friends on new { c.UserId, c.TargetId }
|
join f in _context.Friends on new { c.UserId, c.TargetId }
|
||||||
equals new { UserId = f.UserId, TargetId = f.FriendId }
|
equals new { UserId = f.UserId, TargetId = f.FriendId }
|
||||||
|
join u in _context.Users on c.TargetId equals u.Id
|
||||||
where c.UserId == userId && c.ChatType == ChatType.PRIVATE
|
where c.UserId == userId && c.ChatType == ChatType.PRIVATE
|
||||||
select new { c, f.Avatar, f.RemarkName })
|
select new { c, u.Avatar, f.RemarkName })
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
// 2. 获取群聊会话
|
// 2. 获取群聊会话
|
||||||
|
|||||||
@ -2,13 +2,16 @@
|
|||||||
using AutoMapper.QueryableExtensions;
|
using AutoMapper.QueryableExtensions;
|
||||||
using IM_API.Domain.Events;
|
using IM_API.Domain.Events;
|
||||||
using IM_API.Dtos.Group;
|
using IM_API.Dtos.Group;
|
||||||
|
using IM_API.Dtos.User;
|
||||||
using IM_API.Exceptions;
|
using IM_API.Exceptions;
|
||||||
using IM_API.Interface.Services;
|
using IM_API.Interface.Services;
|
||||||
using IM_API.Models;
|
using IM_API.Models;
|
||||||
using IM_API.Tools;
|
using IM_API.Tools;
|
||||||
|
using IM_API.VOs.Group;
|
||||||
using MassTransit;
|
using MassTransit;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace IM_API.Services
|
namespace IM_API.Services
|
||||||
{
|
{
|
||||||
@ -247,5 +250,21 @@ namespace IM_API.Services
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<List<GroupMemberVo>> GetGroupMembers(int userId, int groupId)
|
||||||
|
{
|
||||||
|
var members = await _context.GroupMembers
|
||||||
|
.Where(x => x.GroupId == groupId).ToListAsync();
|
||||||
|
if (members is null || members.Count() == 0)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
var users = await _userService.GetUserInfoListAsync(members.Select(x => x.UserId).ToList());
|
||||||
|
return users.Zip(members, (u, m) =>
|
||||||
|
{
|
||||||
|
var user = _mapper.Map<GroupMemberVo>(u);
|
||||||
|
_mapper.Map(m, user);
|
||||||
|
return user;
|
||||||
|
}).ToList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
242
backend/IM_API/Services/LocalStorageService.cs
Normal file
242
backend/IM_API/Services/LocalStorageService.cs
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using IM_API.Configs.Options;
|
||||||
|
using IM_API.Domain.Events;
|
||||||
|
using IM_API.Dtos;
|
||||||
|
using IM_API.Exceptions;
|
||||||
|
using IM_API.Interface.Services;
|
||||||
|
using IM_API.Models.Upload;
|
||||||
|
using IM_API.Tools;
|
||||||
|
using IM_API.VOs;
|
||||||
|
using MassTransit;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage;
|
||||||
|
using StackExchange.Redis;
|
||||||
|
using System.Security.AccessControl;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using IDatabase = StackExchange.Redis.IDatabase;
|
||||||
|
|
||||||
|
namespace IM_API.Services
|
||||||
|
{
|
||||||
|
public class LocalStorageService : IStorageService
|
||||||
|
{
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
private readonly IHttpContextAccessor _httpContext;
|
||||||
|
private FileUploadOptions _options;
|
||||||
|
private readonly IUploadTaskService _uploadTaskService;
|
||||||
|
private readonly IDatabase _redis;
|
||||||
|
private readonly IHostEnvironment _env;
|
||||||
|
private readonly ILogger<LocalStorageService> _logger;
|
||||||
|
private readonly IPublishEndpoint _endpoint;
|
||||||
|
public LocalStorageService(IMapper mapper, IHttpContextAccessor httpContextAccessor,
|
||||||
|
IConfiguration configuration, IUploadTaskService uploadTaskService,
|
||||||
|
IConnectionMultiplexer connectionMultiplexer, IHostEnvironment hostEnvironment
|
||||||
|
, ILogger<LocalStorageService> logger, IPublishEndpoint publishEndpoint)
|
||||||
|
{
|
||||||
|
_mapper = mapper;
|
||||||
|
_httpContext = httpContextAccessor;
|
||||||
|
_options = configuration.GetSection("FileUploadOptions").Get<FileUploadOptions>()!;
|
||||||
|
_uploadTaskService = uploadTaskService;
|
||||||
|
_redis = connectionMultiplexer.GetDatabase();
|
||||||
|
_env = hostEnvironment;
|
||||||
|
_logger = logger;
|
||||||
|
_endpoint = publishEndpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UploadMode Mode => UploadMode.Proxy;
|
||||||
|
public string ProviderName => "Local";
|
||||||
|
|
||||||
|
public async Task<Guid> CompleteAsync(Guid taskId, List<UploadPartDto> parts)
|
||||||
|
{
|
||||||
|
var task = await _uploadTaskService.GetTaskAsync(taskId);
|
||||||
|
|
||||||
|
if(task is null)
|
||||||
|
throw new BaseException(CodeDefine.CHUNKE_NOT_FOUND);
|
||||||
|
|
||||||
|
var partsToCheck = Enumerable.Range(1, task.TotalChunks)
|
||||||
|
.Select(i => (RedisValue)i).ToArray();
|
||||||
|
|
||||||
|
var results = await _redis.SetContainsAsync(RedisKeys.GetUploadPartKey(taskId), partsToCheck);
|
||||||
|
// 3. 快速判断是否全部存在
|
||||||
|
bool isAllUploaded = results.All(exists => exists);
|
||||||
|
if (!isAllUploaded) throw new BaseException(CodeDefine.CHUNKE_NOT_FOUND);
|
||||||
|
|
||||||
|
await _endpoint.Publish(new UploadMergeEvent
|
||||||
|
{
|
||||||
|
AggregateId = taskId.ToString(),
|
||||||
|
OccurredAt = DateTime.UtcNow,
|
||||||
|
EventId = Guid.NewGuid(),
|
||||||
|
OperatorId = 0,
|
||||||
|
Parts = parts,
|
||||||
|
TaskId = taskId,
|
||||||
|
ChunckCount = task.TotalChunks,
|
||||||
|
ObjectName = task.ObjectName
|
||||||
|
|
||||||
|
});
|
||||||
|
return taskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task MergeAsync(Guid taskId, string objectName, int totalChunks, List<UploadPartDto> parts)
|
||||||
|
{
|
||||||
|
var baseDir = Path.Combine(_env.ContentRootPath, "uploads");
|
||||||
|
var tempPath = Path.Combine(baseDir, "temp", taskId.ToString()); // 项目根目录下 uploads // 最终文件存储路径(这里可以用你之前 ObjectNameGenerator 生成的名字)
|
||||||
|
var finalPath = Path.Combine(baseDir, "files", objectName);
|
||||||
|
var finalDir = Path.GetDirectoryName(finalPath);
|
||||||
|
Directory.CreateDirectory(finalDir);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var finalStream = new FileStream(finalPath, FileMode.Create))
|
||||||
|
{
|
||||||
|
for (var i = 1; i <= totalChunks; i++)
|
||||||
|
{
|
||||||
|
var progress = (i * 100.0 / totalChunks);
|
||||||
|
if (i % 5 == 0 || i == totalChunks)
|
||||||
|
{
|
||||||
|
await _redis.HashSetAsync(RedisKeys.MergeStatus(taskId), new HashEntry[]
|
||||||
|
{
|
||||||
|
new("status", "processing"),
|
||||||
|
new("progress", progress.ToString("F2"))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var chunkPath = Path.Combine(tempPath, $"{i}.part.tmp");
|
||||||
|
if (!File.Exists(chunkPath))
|
||||||
|
throw new BaseException(CodeDefine.CHUNKE_NOT_FOUND);
|
||||||
|
using (var chunkStream = new FileStream(chunkPath, FileMode.Open))
|
||||||
|
{
|
||||||
|
await chunkStream.CopyToAsync(finalStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Directory.Delete(tempPath, true);
|
||||||
|
await _redis.KeyDeleteAsync(RedisKeys.GetUploadPartKey(taskId));
|
||||||
|
await _uploadTaskService.UpdateStatusAsync(taskId, UploadStatus.Completed);
|
||||||
|
await _redis.HashSetAsync(RedisKeys.MergeStatus(taskId), new HashEntry[]
|
||||||
|
{
|
||||||
|
new("status", "Completed"),
|
||||||
|
new("progress", "100"),
|
||||||
|
new("url", objectName)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (Exception e) when (e is not BaseException)
|
||||||
|
{
|
||||||
|
_logger.LogError(e, e.Message);
|
||||||
|
throw new BaseException(CodeDefine.CHUNKE_COMBINE_FAIL);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UploadPartInstructionVo> CreatePartInstructionAsync(Guid taskId, int partNumer)
|
||||||
|
{
|
||||||
|
if (await _redis.SetContainsAsync(RedisKeys.GetUploadPartKey(taskId), partNumer)){
|
||||||
|
return new UploadPartInstructionVo
|
||||||
|
{
|
||||||
|
PartNumber = partNumer,
|
||||||
|
Skip = true,
|
||||||
|
Headers = new Dictionary<string, string>()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
var request = _httpContext.HttpContext!.Request;
|
||||||
|
|
||||||
|
var scheme = request.Scheme; // http 或 https
|
||||||
|
var host = request.Host.Value; // localhost:5000 或域名
|
||||||
|
|
||||||
|
var baseUrl = $"{scheme}://{host}/api/upload/local/{taskId}/parts/{partNumer}";
|
||||||
|
var headers = new Dictionary<string, string>();
|
||||||
|
headers.Add("Content-Type", "multipart/form-data");
|
||||||
|
return new UploadPartInstructionVo
|
||||||
|
{
|
||||||
|
Method = "POST",
|
||||||
|
PartNumber = partNumer,
|
||||||
|
Skip = false,
|
||||||
|
Url = baseUrl,
|
||||||
|
Headers = headers
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UploadTask> UploadSmallFileAsync(Stream stream, string fileName, string fileType, long size, string hash)
|
||||||
|
{
|
||||||
|
var request = _httpContext.HttpContext?.Request;
|
||||||
|
var baseUrl = $"{request.Scheme}://{request.Host}";
|
||||||
|
var taskOld = await _uploadTaskService.GetTaskAsync(hash);
|
||||||
|
if (taskOld is not null) {
|
||||||
|
taskOld.Url = UrlTools.GetFullUrl(taskOld.ObjectName, ProviderName, baseUrl);
|
||||||
|
return taskOld;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var userId = _httpContext.HttpContext?.User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
|
var objectname = ObjectNameGenerator.Generate(new ObjectNameContext
|
||||||
|
{
|
||||||
|
ContentType = fileType,
|
||||||
|
FileName = fileName,
|
||||||
|
UserId = int.Parse(userId)
|
||||||
|
});
|
||||||
|
var path = GetDownloadUrl(objectname);
|
||||||
|
// 4. 将 Stream 写入本地文件
|
||||||
|
using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||||
|
{
|
||||||
|
await stream.CopyToAsync(fileStream);
|
||||||
|
}
|
||||||
|
var task = new UploadTask
|
||||||
|
{
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
ChunkSize = (int)size,
|
||||||
|
ContentType = fileType,
|
||||||
|
FileHash = hash,
|
||||||
|
FileName = fileName,
|
||||||
|
FileSize = size,
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
ObjectName = objectname,
|
||||||
|
Url = UrlTools.GetFullUrl(objectname, ProviderName, baseUrl),
|
||||||
|
ProviderUploadId = Guid.NewGuid().ToString(),
|
||||||
|
Status = UploadStatus.Completed,
|
||||||
|
StorageProvider = ProviderName,
|
||||||
|
TotalChunks = 1
|
||||||
|
};
|
||||||
|
await _uploadTaskService.AddAsync(task);
|
||||||
|
return task;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetDownloadUrl(string objectname)
|
||||||
|
{
|
||||||
|
var baseDir = Path.Combine(_env.ContentRootPath, "uploads"); // 最终文件存储路径(这里可以用你之前 ObjectNameGenerator 生成的名字)
|
||||||
|
var finalPath = Path.Combine(baseDir, "files", objectname);
|
||||||
|
var finalDir = Path.GetDirectoryName(finalPath);
|
||||||
|
Directory.CreateDirectory(finalDir);
|
||||||
|
return finalPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<CreateUploadTaskVo> InitTaskAsync(CreateUploadTaskDto dto)
|
||||||
|
{
|
||||||
|
var userId = _httpContext.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
|
UploadTask task = _mapper.Map<UploadTask>(dto);
|
||||||
|
var taskOld = await _uploadTaskService.GetTaskAsync(dto.FileHash);
|
||||||
|
if(taskOld != null)
|
||||||
|
{
|
||||||
|
var t = _mapper.Map<CreateUploadTaskVo>(taskOld);
|
||||||
|
t.Skip = false;
|
||||||
|
if (taskOld.Status == UploadStatus.Completed)
|
||||||
|
{
|
||||||
|
t.Skip = true;
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
task = taskOld;
|
||||||
|
}
|
||||||
|
task.ObjectName = ObjectNameGenerator.Generate(new ObjectNameContext
|
||||||
|
{
|
||||||
|
ContentType = task.ContentType,
|
||||||
|
FileName = task.FileName,
|
||||||
|
UserId = int.Parse(userId)
|
||||||
|
});
|
||||||
|
task.StorageProvider = ProviderName;
|
||||||
|
task.ProviderUploadId = Guid.NewGuid().ToString();
|
||||||
|
task.ChunkSize = _options.ChunkSize;
|
||||||
|
task.TotalChunks = (int)Math.Ceiling((double)task.FileSize / _options.ChunkSize);
|
||||||
|
await _uploadTaskService.AddAsync(task);
|
||||||
|
return _mapper.Map<CreateUploadTaskVo>(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,9 +10,11 @@ using IM_API.Tools;
|
|||||||
using IM_API.VOs.Message;
|
using IM_API.VOs.Message;
|
||||||
using MassTransit;
|
using MassTransit;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using StackExchange.Redis;
|
using StackExchange.Redis;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using static MassTransit.Monitoring.Performance.BuiltInCounters;
|
using static MassTransit.Monitoring.Performance.BuiltInCounters;
|
||||||
using static Microsoft.EntityFrameworkCore.DbLoggerCategory;
|
using static Microsoft.EntityFrameworkCore.DbLoggerCategory;
|
||||||
|
|
||||||
@ -28,10 +30,13 @@ namespace IM_API.Services
|
|||||||
private readonly IPublishEndpoint _endpoint;
|
private readonly IPublishEndpoint _endpoint;
|
||||||
private readonly ISequenceIdService _sequenceIdService;
|
private readonly ISequenceIdService _sequenceIdService;
|
||||||
private readonly IUserService _userService;
|
private readonly IUserService _userService;
|
||||||
|
private readonly IUploadTaskService _uploadService;
|
||||||
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||||
public MessageService(
|
public MessageService(
|
||||||
ImContext context, ILogger<MessageService> logger, IMapper mapper,
|
ImContext context, ILogger<MessageService> logger, IMapper mapper,
|
||||||
IPublishEndpoint publishEndpoint, ISequenceIdService sequenceIdService,
|
IPublishEndpoint publishEndpoint, ISequenceIdService sequenceIdService,
|
||||||
IUserService userService
|
IUserService userService, IUploadTaskService uploadTaskService,
|
||||||
|
IHttpContextAccessor httpContextAccessor
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
@ -41,6 +46,8 @@ namespace IM_API.Services
|
|||||||
_endpoint = publishEndpoint;
|
_endpoint = publishEndpoint;
|
||||||
_sequenceIdService = sequenceIdService;
|
_sequenceIdService = sequenceIdService;
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
|
_uploadService = uploadTaskService;
|
||||||
|
_httpContextAccessor = httpContextAccessor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<MessageBaseVo>> GetMessagesAsync(int userId,MessageQueryDto dto)
|
public async Task<List<MessageBaseVo>> GetMessagesAsync(int userId,MessageQueryDto dto)
|
||||||
@ -91,6 +98,12 @@ namespace IM_API.Services
|
|||||||
|
|
||||||
foreach (var item in messages)
|
foreach (var item in messages)
|
||||||
{
|
{
|
||||||
|
if(item.Type != MessageMsgType.Text)
|
||||||
|
{
|
||||||
|
var request = _httpContextAccessor.HttpContext?.Request;
|
||||||
|
var baseUrl = $"{request.Scheme}://{request.Host}";
|
||||||
|
item.Content = UrlTools.ProcessMessageUrl(item.Content, baseUrl);
|
||||||
|
}
|
||||||
if(userDict.TryGetValue(item.SenderId, out var user))
|
if(userDict.TryGetValue(item.SenderId, out var user))
|
||||||
{
|
{
|
||||||
item.SenderName = user.NickName;
|
item.SenderName = user.NickName;
|
||||||
@ -143,7 +156,10 @@ namespace IM_API.Services
|
|||||||
var message = _mapper.Map<Message>(dto);
|
var message = _mapper.Map<Message>(dto);
|
||||||
message.StreamKey = StreamKeyBuilder.Group(groupId);
|
message.StreamKey = StreamKeyBuilder.Group(groupId);
|
||||||
message.SequenceId = await _sequenceIdService.GetNextSquenceIdAsync(message.StreamKey);
|
message.SequenceId = await _sequenceIdService.GetNextSquenceIdAsync(message.StreamKey);
|
||||||
await _endpoint.Publish(_mapper.Map<MessageCreatedEvent>(message));
|
var publishData = _mapper.Map<MessageCreatedEvent>(message);
|
||||||
|
var request = _httpContextAccessor.HttpContext?.Request;
|
||||||
|
publishData.BaseUrl = $"{request.Scheme}://{request.Host}";
|
||||||
|
await _endpoint.Publish(publishData);
|
||||||
return _mapper.Map<MessageBaseVo>(message);
|
return _mapper.Map<MessageBaseVo>(message);
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -156,9 +172,40 @@ namespace IM_API.Services
|
|||||||
var message = _mapper.Map<Message>(dto);
|
var message = _mapper.Map<Message>(dto);
|
||||||
message.StreamKey = StreamKeyBuilder.Private(senderId, receiverId);
|
message.StreamKey = StreamKeyBuilder.Private(senderId, receiverId);
|
||||||
message.SequenceId = await _sequenceIdService.GetNextSquenceIdAsync(message.StreamKey);
|
message.SequenceId = await _sequenceIdService.GetNextSquenceIdAsync(message.StreamKey);
|
||||||
await _endpoint.Publish(_mapper.Map<MessageCreatedEvent>(message));
|
var publishData = _mapper.Map<MessageCreatedEvent>(message);
|
||||||
|
var request = _httpContextAccessor.HttpContext?.Request;
|
||||||
|
publishData.BaseUrl = $"{request.Scheme}://{request.Host}";
|
||||||
|
await _endpoint.Publish(publishData);
|
||||||
return _mapper.Map<MessageBaseVo>(message);
|
return _mapper.Map<MessageBaseVo>(message);
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
public async Task<MessageBaseDto> HandleFileMessageContentAsync(MessageBaseDto dto)
|
||||||
|
{
|
||||||
|
if(dto.Type == MessageMsgType.Text)
|
||||||
|
{
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dic = JsonConvert.DeserializeObject<Dictionary<string, object>>(dto.Content);
|
||||||
|
|
||||||
|
if (dic == null || !dic.TryGetValue("fileId", out var fileIdObj))
|
||||||
|
throw new BaseException(CodeDefine.PARAMETER_ERROR);
|
||||||
|
|
||||||
|
var fileInfo = await _uploadService.GetTaskAsync(new Guid(fileIdObj.ToString()));
|
||||||
|
|
||||||
|
if (fileInfo is null)
|
||||||
|
throw new BaseException(CodeDefine.FILE_NOT_FOUND);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
dic["url"] = fileInfo.ObjectName;
|
||||||
|
dic["provider"] = fileInfo.StorageProvider;
|
||||||
|
dic["size"] = fileInfo.FileSize;
|
||||||
|
|
||||||
|
dto.Content = JsonConvert.SerializeObject(dic);
|
||||||
|
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
43
backend/IM_API/Services/UploadTaskService.cs
Normal file
43
backend/IM_API/Services/UploadTaskService.cs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
using IM_API.Interface.Services;
|
||||||
|
using IM_API.Models;
|
||||||
|
using IM_API.Models.Upload;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace IM_API.Services
|
||||||
|
{
|
||||||
|
public class UploadTaskService : IUploadTaskService
|
||||||
|
{
|
||||||
|
private readonly ImContext _context;
|
||||||
|
public UploadTaskService(ImContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AddAsync(UploadTask task)
|
||||||
|
{
|
||||||
|
_context.UploadTasks.Add(task);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UploadTask?> GetTaskAsync(Guid taskId)
|
||||||
|
{
|
||||||
|
return await _context.UploadTasks.FirstOrDefaultAsync(x => x.Id == taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UploadTask?> GetTaskAsync(string hash)
|
||||||
|
{
|
||||||
|
return await _context.UploadTasks.FirstOrDefaultAsync(x => x.FileHash == hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateStatusAsync(Guid taskId, UploadStatus status)
|
||||||
|
{
|
||||||
|
var task = await _context.UploadTasks.FirstOrDefaultAsync(x => x.Id == taskId);
|
||||||
|
if (task != null)
|
||||||
|
{
|
||||||
|
task.Status = status;
|
||||||
|
_context.UploadTasks.Update(task);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -105,5 +105,11 @@
|
|||||||
// 3.9 会话相关错误(3100 ~ 3199)
|
// 3.9 会话相关错误(3100 ~ 3199)
|
||||||
/// <summary>发送时异常</summary>
|
/// <summary>发送时异常</summary>
|
||||||
public static CodeDefine CONVERSATION_NOT_FOUND = new CodeDefine(3100, "会话不存在");
|
public static CodeDefine CONVERSATION_NOT_FOUND = new CodeDefine(3100, "会话不存在");
|
||||||
|
|
||||||
|
// 3.9 文件相关错误(3200 ~ 3299)
|
||||||
|
/// <summary>分片不存在异常</summary>
|
||||||
|
public static CodeDefine CHUNKE_NOT_FOUND = new CodeDefine(3201, "分片不存在");
|
||||||
|
/// <summary>分片合并异常</summary>
|
||||||
|
public static CodeDefine CHUNKE_COMBINE_FAIL = new CodeDefine(3202, "分片合并失败");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
51
backend/IM_API/Tools/ObjectNameGenerator.cs
Normal file
51
backend/IM_API/Tools/ObjectNameGenerator.cs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
namespace IM_API.Tools
|
||||||
|
{
|
||||||
|
public static class ObjectNameGenerator
|
||||||
|
{
|
||||||
|
public static string Generate(ObjectNameContext ctx)
|
||||||
|
{
|
||||||
|
var ext = GetExtension(ctx.FileName, ctx.ContentType);
|
||||||
|
var shortId = Guid.NewGuid().ToString("N")[..12];
|
||||||
|
|
||||||
|
var parts = new List<string>
|
||||||
|
{
|
||||||
|
ctx.Biz,
|
||||||
|
ctx.Now.Year.ToString(),
|
||||||
|
ctx.Now.Month.ToString("D2")
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ctx.UserId.HasValue)
|
||||||
|
{
|
||||||
|
parts.Add(ctx.UserId.Value.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
parts.Add($"{shortId}{ext}");
|
||||||
|
|
||||||
|
return string.Join("/", parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetExtension(string fileName, string contentType)
|
||||||
|
{
|
||||||
|
var ext = Path.GetExtension(fileName);
|
||||||
|
if (!string.IsNullOrWhiteSpace(ext))
|
||||||
|
return ext.ToLowerInvariant();
|
||||||
|
|
||||||
|
return contentType switch
|
||||||
|
{
|
||||||
|
"image/jpeg" => ".jpg",
|
||||||
|
"image/png" => ".png",
|
||||||
|
"video/mp4" => ".mp4",
|
||||||
|
_ => ".bin"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public class ObjectNameContext
|
||||||
|
{
|
||||||
|
public string Biz { get; init; } = "IM";
|
||||||
|
public long? UserId { get; init; }
|
||||||
|
public string FileName { get; init; } = default!;
|
||||||
|
public string ContentType { get; init; } = default!;
|
||||||
|
public DateTimeOffset Now { get; init; } = DateTimeOffset.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -2,10 +2,13 @@
|
|||||||
{
|
{
|
||||||
public static class RedisKeys
|
public static class RedisKeys
|
||||||
{
|
{
|
||||||
public static string GetUserinfoKey(string userId) => $"user::uinfo::{userId}";
|
public static string GetUserinfoKey(string userId) => $"user:uinfo:{userId}";
|
||||||
public static string GetUserinfoKeyByUsername(string username) => $"user::uinfobyid::{username}";
|
public static string GetUserinfoKeyByUsername(string username) => $"user:uinfobyid:{username}";
|
||||||
public static string GetSequenceIdKey(string streamKey) => $"chat::seq::{streamKey}";
|
public static string GetSequenceIdKey(string streamKey) => $"chat:seq:{streamKey}";
|
||||||
public static string GetSequenceIdLockKey(string streamKey) => $"lock::seq::{streamKey}";
|
public static string GetSequenceIdLockKey(string streamKey) => $"lock:seq:{streamKey}";
|
||||||
public static string GetConnectionIdKey(string userId) => $"signalr::user::con::{userId}";
|
public static string GetConnectionIdKey(string userId) => $"signalr:user:con:{userId}";
|
||||||
|
|
||||||
|
public static string GetUploadPartKey(Guid taskId) => $"upload:task:{taskId}:parts";
|
||||||
|
public static string MergeStatus(Guid taskId) => $"upload:task:{taskId}:merge";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
64
backend/IM_API/Tools/UrlTools.cs
Normal file
64
backend/IM_API/Tools/UrlTools.cs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
using SixLabors.ImageSharp;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
|
||||||
|
namespace IM_API.Tools
|
||||||
|
{
|
||||||
|
public static class UrlTools
|
||||||
|
{
|
||||||
|
public static string GetFullUrl(string objectName, string provider, string? baseUrl)
|
||||||
|
{
|
||||||
|
return provider switch
|
||||||
|
{
|
||||||
|
"Local" => $"{baseUrl}/uploads/files/{objectName}",
|
||||||
|
_ => "http://baidu.com",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<(int width, int height)> GetImageWH(string url)
|
||||||
|
{
|
||||||
|
using var httpClient = new HttpClient();
|
||||||
|
var stream = await httpClient.GetStreamAsync(url);
|
||||||
|
var info = await Image.IdentifyAsync(stream);
|
||||||
|
return (info.Width, info.Height);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ProcessMessageUrl(string contentJson, string? localBaseUrl)
|
||||||
|
{
|
||||||
|
// 1. 解析 JSON 文档(比反序列化快得多)
|
||||||
|
using var doc = JsonDocument.Parse(contentJson);
|
||||||
|
var root = doc.RootElement;
|
||||||
|
|
||||||
|
// 2. 获取 Provider 字段
|
||||||
|
string provider = root.GetProperty("provider").GetString();
|
||||||
|
|
||||||
|
// 3. 根据 Provider 决定前缀
|
||||||
|
string prefix = GetFullUrl("", provider, localBaseUrl);
|
||||||
|
|
||||||
|
// 4. 重新组装(如果只是为了给前端看,建议直接返回带前缀的对象或字符串)
|
||||||
|
// 这里推荐用 JsonNode 方便修改并返回字符串
|
||||||
|
var node = JsonNode.Parse(contentJson);
|
||||||
|
node["url"] = $"{prefix}{node["url"]}";
|
||||||
|
node["thumb"] = $"{prefix}{node["thumb"]}";
|
||||||
|
|
||||||
|
return node.ToJsonString();
|
||||||
|
}
|
||||||
|
public static Stream Base64ToStream(string base64String)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(base64String))
|
||||||
|
throw new ArgumentNullException(nameof(base64String));
|
||||||
|
|
||||||
|
// 1. 自动处理可能存在的 Base64 Data URL 前缀
|
||||||
|
string base64Data = base64String.Contains(",")
|
||||||
|
? base64String.Split(',')[1]
|
||||||
|
: base64String;
|
||||||
|
|
||||||
|
// 2. 解码为字节数组
|
||||||
|
byte[] bytes = Convert.FromBase64String(base64Data);
|
||||||
|
|
||||||
|
// 3. 包装进 MemoryStream
|
||||||
|
// 注意:这里直接把 Position 设为 0,符合“方法a”产生即用的原则
|
||||||
|
return new MemoryStream(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
backend/IM_API/VOs/CreateUploadTaskVo.cs
Normal file
16
backend/IM_API/VOs/CreateUploadTaskVo.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
using IM_API.Interface.Services;
|
||||||
|
|
||||||
|
namespace IM_API.VOs
|
||||||
|
{
|
||||||
|
public class CreateUploadTaskVo
|
||||||
|
{
|
||||||
|
public Guid TaskId { get; set; }
|
||||||
|
|
||||||
|
public int ChunkSize { get; set; }
|
||||||
|
public int TotalChunks { get; set; }
|
||||||
|
|
||||||
|
public int Concurrency { get; set; } = 4;
|
||||||
|
public string? Url { get; set; }
|
||||||
|
public bool Skip { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
14
backend/IM_API/VOs/Group/GroupMemberVo.cs
Normal file
14
backend/IM_API/VOs/Group/GroupMemberVo.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
using IM_API.Models;
|
||||||
|
|
||||||
|
namespace IM_API.VOs.Group
|
||||||
|
{
|
||||||
|
public class GroupMemberVo
|
||||||
|
{
|
||||||
|
public int UserId { get; set; }
|
||||||
|
public string Username { get; set; }
|
||||||
|
public string Nickname { get; set; }
|
||||||
|
public string Avatar { get; set; }
|
||||||
|
public GroupMemberRole Role { get; set; }
|
||||||
|
public DateTimeOffset Created { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
14
backend/IM_API/VOs/UploadPartInstuctionVo.cs
Normal file
14
backend/IM_API/VOs/UploadPartInstuctionVo.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
namespace IM_API.VOs
|
||||||
|
{
|
||||||
|
public class UploadPartInstructionVo
|
||||||
|
{
|
||||||
|
public bool Skip { get; set; }
|
||||||
|
public int PartNumber { get; set; }
|
||||||
|
|
||||||
|
public string Method { get; set; } = "PUT";
|
||||||
|
public string Url { get; set; } = default!;
|
||||||
|
|
||||||
|
public Dictionary<string, string> Headers { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -14,7 +14,7 @@
|
|||||||
"RefreshTokenDays": 30
|
"RefreshTokenDays": 30
|
||||||
},
|
},
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"DefaultConnection": "Server=frp-era.com;Port=26582;Database=IM;User=product;Password=12345678;",
|
"DefaultConnection": "Server=192.168.5.100;Port=3306;Database=IM;User=product;Password=12345678;",
|
||||||
"Redis": "192.168.5.100:6379"
|
"Redis": "192.168.5.100:6379"
|
||||||
},
|
},
|
||||||
"RabbitMQOptions": {
|
"RabbitMQOptions": {
|
||||||
@ -22,5 +22,9 @@
|
|||||||
"Port": 5672,
|
"Port": 5672,
|
||||||
"Username": "test",
|
"Username": "test",
|
||||||
"Password": "123456"
|
"Password": "123456"
|
||||||
|
},
|
||||||
|
"FileUploadOptions": {
|
||||||
|
"DefaultStorage": "Local",
|
||||||
|
"ChunkSize": 5000000,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,21 @@
|
|||||||
allprojects {
|
//buildscript {
|
||||||
repositories {
|
// repositories {
|
||||||
maven { url = uri("https://maven.aliyun.com/repository/google") }
|
// // 将 google() 和 mavenCentral() 替换或放在后面
|
||||||
maven { url = uri("https://maven.aliyun.com/repository/public") }
|
// maven { url = uri("https://maven.aliyun.com/repository/google") }
|
||||||
google()
|
// maven { url = uri("https://maven.aliyun.com/repository/public") }
|
||||||
mavenCentral()
|
// google()
|
||||||
|
// mavenCentral()
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
|
//allprojects {
|
||||||
|
// repositories {
|
||||||
|
// maven { url = uri("https://maven.aliyun.com/repository/google") }
|
||||||
|
// maven { url = uri("https://maven.aliyun.com/repository/public") }
|
||||||
|
// google()
|
||||||
|
// mavenCentral()
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
val newBuildDir: Directory =
|
val newBuildDir: Directory =
|
||||||
rootProject.layout.buildDirectory
|
rootProject.layout.buildDirectory
|
||||||
|
|||||||
663
frontend/app/android/build/reports/problems/problems-report.html
Normal file
663
frontend/app/android/build/reports/problems/problems-report.html
Normal file
File diff suppressed because one or more lines are too long
@ -1,2 +1,12 @@
|
|||||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
|
# ?? HTTP ??
|
||||||
|
systemProp.http.proxyHost=127.0.0.1
|
||||||
|
systemProp.http.proxyPort=10808
|
||||||
|
|
||||||
|
# ?? HTTPS ???????????????
|
||||||
|
systemProp.https.proxyHost=127.0.0.1
|
||||||
|
systemProp.https.proxyPort=10808
|
||||||
|
|
||||||
|
# ????????????????????????
|
||||||
|
systemProp.http.nonProxyHosts=localhost|127.0.0.1|*.aliyun.com|mirrors.*
|
||||||
@ -1,28 +1,38 @@
|
|||||||
pluginManagement {
|
pluginManagement {
|
||||||
val flutterSdkPath =
|
val flutterSdkPath = run {
|
||||||
run {
|
val properties = java.util.Properties()
|
||||||
val properties = java.util.Properties()
|
file("local.properties").inputStream().use { properties.load(it) }
|
||||||
file("local.properties").inputStream().use { properties.load(it) }
|
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||||
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||||
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
flutterSdkPath
|
||||||
flutterSdkPath
|
}
|
||||||
}
|
|
||||||
|
|
||||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
maven { url = uri("https://maven.aliyun.com/repository/google") }
|
maven { url = uri("https://maven.aliyun.com/repository/google") }
|
||||||
maven { url = uri("https://maven.aliyun.com/repository/public") }
|
maven { url = uri("https://maven.aliyun.com/repository/public") }
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
gradlePluginPortal()
|
gradlePluginPortal()
|
||||||
|
//google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencyResolutionManagement {
|
||||||
|
repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
|
||||||
|
repositories {
|
||||||
|
maven { url = uri("https://maven.aliyun.com/repository/google") }
|
||||||
|
maven { url = uri("https://maven.aliyun.com/repository/public") }
|
||||||
|
gradlePluginPortal()
|
||||||
|
//google()
|
||||||
|
mavenCentral()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||||
id("com.android.application") version "8.11.1" apply false
|
id("com.android.application") version "8.1.1" apply false
|
||||||
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
|
id("org.jetbrains.kotlin.android") version "1.8.22" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
include(":app")
|
include(":app")
|
||||||
@ -6,7 +6,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: async
|
name: async
|
||||||
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.13.0"
|
version: "2.13.0"
|
||||||
boolean_selector:
|
boolean_selector:
|
||||||
@ -14,23 +14,23 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: boolean_selector
|
name: boolean_selector
|
||||||
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
version: "2.1.2"
|
||||||
characters:
|
characters:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: characters
|
name: characters
|
||||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.1"
|
||||||
clock:
|
clock:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: clock
|
name: clock
|
||||||
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.2"
|
version: "1.1.2"
|
||||||
collection:
|
collection:
|
||||||
@ -38,7 +38,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: collection
|
name: collection
|
||||||
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.19.1"
|
version: "1.19.1"
|
||||||
cupertino_icons:
|
cupertino_icons:
|
||||||
@ -46,7 +46,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: cupertino_icons
|
name: cupertino_icons
|
||||||
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
|
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.8"
|
version: "1.0.8"
|
||||||
fake_async:
|
fake_async:
|
||||||
@ -54,7 +54,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: fake_async
|
name: fake_async
|
||||||
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.3"
|
version: "1.3.3"
|
||||||
flutter:
|
flutter:
|
||||||
@ -67,7 +67,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: flutter_lints
|
name: flutter_lints
|
||||||
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
|
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.0"
|
version: "6.0.0"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@ -84,16 +84,16 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: go_router
|
name: go_router
|
||||||
sha256: eff94d2a6fc79fa8b811dde79c7549808c2346037ee107a1121b4a644c745f2a
|
sha256: "7974313e217a7771557add6ff2238acb63f635317c35fa590d348fb238f00896"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "17.0.1"
|
version: "17.1.0"
|
||||||
leak_tracker:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker
|
name: leak_tracker
|
||||||
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
|
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "11.0.2"
|
version: "11.0.2"
|
||||||
leak_tracker_flutter_testing:
|
leak_tracker_flutter_testing:
|
||||||
@ -101,7 +101,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: leak_tracker_flutter_testing
|
name: leak_tracker_flutter_testing
|
||||||
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.10"
|
version: "3.0.10"
|
||||||
leak_tracker_testing:
|
leak_tracker_testing:
|
||||||
@ -109,47 +109,47 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: leak_tracker_testing
|
name: leak_tracker_testing
|
||||||
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.2"
|
version: "3.0.2"
|
||||||
lints:
|
lints:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: lints
|
name: lints
|
||||||
sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0
|
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.0"
|
version: "6.1.0"
|
||||||
logging:
|
logging:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: logging
|
name: logging
|
||||||
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
version: "1.3.0"
|
||||||
matcher:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.17"
|
version: "0.12.18"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.11.1"
|
version: "0.13.0"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.17.0"
|
version: "1.17.0"
|
||||||
path:
|
path:
|
||||||
@ -157,7 +157,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: path
|
name: path
|
||||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.1"
|
version: "1.9.1"
|
||||||
sky_engine:
|
sky_engine:
|
||||||
@ -169,16 +169,16 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: source_span
|
name: source_span
|
||||||
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
|
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.10.1"
|
version: "1.10.2"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: stack_trace
|
name: stack_trace
|
||||||
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.12.1"
|
version: "1.12.1"
|
||||||
stream_channel:
|
stream_channel:
|
||||||
@ -186,7 +186,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: stream_channel
|
name: stream_channel
|
||||||
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
version: "2.1.4"
|
||||||
string_scanner:
|
string_scanner:
|
||||||
@ -194,7 +194,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: string_scanner
|
name: string_scanner
|
||||||
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.1"
|
version: "1.4.1"
|
||||||
term_glyph:
|
term_glyph:
|
||||||
@ -202,23 +202,23 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: term_glyph
|
name: term_glyph
|
||||||
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.2"
|
version: "1.2.2"
|
||||||
test_api:
|
test_api:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
sha256: "19a78f63e83d3a61f00826d09bc2f60e191bf3504183c001262be6ac75589fb8"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.7"
|
version: "0.7.8"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vector_math
|
name: vector_math
|
||||||
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.2.0"
|
||||||
vm_service:
|
vm_service:
|
||||||
@ -226,7 +226,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
name: vm_service
|
name: vm_service
|
||||||
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
|
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "15.0.2"
|
version: "15.0.2"
|
||||||
sdks:
|
sdks:
|
||||||
|
|||||||
9
frontend/pc/IM/.editorconfig
Normal file
9
frontend/pc/IM/.editorconfig
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
4
frontend/pc/IM/.env
Normal file
4
frontend/pc/IM/.env
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
VITE_API_BASE_URL = http://localhost:5202/api
|
||||||
|
VITE_SIGNALR_BASE_URL = http://localhost:5202/chat/
|
||||||
|
#VITE_API_BASE_URL = https://im.test.nxsir.cn/api
|
||||||
|
#VITE_SIGNALR_BASE_URL = https://im.test.nxsir.cn/chat/
|
||||||
6
frontend/pc/IM/.gitignore
vendored
Normal file
6
frontend/pc/IM/.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
out
|
||||||
|
.DS_Store
|
||||||
|
.eslintcache
|
||||||
|
*.log*
|
||||||
6
frontend/pc/IM/.prettierignore
Normal file
6
frontend/pc/IM/.prettierignore
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
out
|
||||||
|
dist
|
||||||
|
pnpm-lock.yaml
|
||||||
|
LICENSE.md
|
||||||
|
tsconfig.json
|
||||||
|
tsconfig.*.json
|
||||||
4
frontend/pc/IM/.prettierrc.yaml
Normal file
4
frontend/pc/IM/.prettierrc.yaml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
singleQuote: true
|
||||||
|
semi: false
|
||||||
|
printWidth: 100
|
||||||
|
trailingComma: none
|
||||||
3
frontend/pc/IM/.vscode/extensions.json
vendored
Normal file
3
frontend/pc/IM/.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["dbaeumer.vscode-eslint"]
|
||||||
|
}
|
||||||
39
frontend/pc/IM/.vscode/launch.json
vendored
Normal file
39
frontend/pc/IM/.vscode/launch.json
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Debug Main Process",
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"cwd": "${workspaceRoot}",
|
||||||
|
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite",
|
||||||
|
"windows": {
|
||||||
|
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd"
|
||||||
|
},
|
||||||
|
"runtimeArgs": ["--sourcemap"],
|
||||||
|
"env": {
|
||||||
|
"REMOTE_DEBUGGING_PORT": "9222"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Debug Renderer Process",
|
||||||
|
"port": 9222,
|
||||||
|
"request": "attach",
|
||||||
|
"type": "chrome",
|
||||||
|
"webRoot": "${workspaceFolder}/src/renderer",
|
||||||
|
"timeout": 60000,
|
||||||
|
"presentation": {
|
||||||
|
"hidden": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"compounds": [
|
||||||
|
{
|
||||||
|
"name": "Debug All",
|
||||||
|
"configurations": ["Debug Main Process", "Debug Renderer Process"],
|
||||||
|
"presentation": {
|
||||||
|
"order": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
11
frontend/pc/IM/.vscode/settings.json
vendored
Normal file
11
frontend/pc/IM/.vscode/settings.json
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[json]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
}
|
||||||
|
}
|
||||||
34
frontend/pc/IM/README.md
Normal file
34
frontend/pc/IM/README.md
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# my-electron-app
|
||||||
|
|
||||||
|
An Electron application with Vue
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
- [VSCode](https://code.visualstudio.com/) + [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) + [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar)
|
||||||
|
|
||||||
|
## Project Setup
|
||||||
|
|
||||||
|
### Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# For windows
|
||||||
|
$ npm run build:win
|
||||||
|
|
||||||
|
# For macOS
|
||||||
|
$ npm run build:mac
|
||||||
|
|
||||||
|
# For Linux
|
||||||
|
$ npm run build:linux
|
||||||
|
```
|
||||||
12
frontend/pc/IM/build/entitlements.mac.plist
Normal file
12
frontend/pc/IM/build/entitlements.mac.plist
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.cs.allow-jit</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
BIN
frontend/pc/IM/build/icon.icns
Normal file
BIN
frontend/pc/IM/build/icon.icns
Normal file
Binary file not shown.
BIN
frontend/pc/IM/build/icon.ico
Normal file
BIN
frontend/pc/IM/build/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 121 KiB |
BIN
frontend/pc/IM/build/icon.png
Normal file
BIN
frontend/pc/IM/build/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
3
frontend/pc/IM/dev-app-update.yml
Normal file
3
frontend/pc/IM/dev-app-update.yml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
provider: generic
|
||||||
|
url: https://example.com/auto-updates
|
||||||
|
updaterCacheDirName: my-electron-app-updater
|
||||||
42
frontend/pc/IM/electron-builder.yml
Normal file
42
frontend/pc/IM/electron-builder.yml
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
appId: com.electron.app
|
||||||
|
productName: my-electron-app
|
||||||
|
directories:
|
||||||
|
buildResources: build
|
||||||
|
files:
|
||||||
|
- '!**/.vscode/*'
|
||||||
|
- '!src/*'
|
||||||
|
- '!electron.vite.config.{js,ts,mjs,cjs}'
|
||||||
|
- '!{.eslintcache,eslint.config.mjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
|
||||||
|
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
|
||||||
|
asarUnpack:
|
||||||
|
- resources/**
|
||||||
|
win:
|
||||||
|
executableName: my-electron-app
|
||||||
|
nsis:
|
||||||
|
artifactName: ${name}-${version}-setup.${ext}
|
||||||
|
shortcutName: ${productName}
|
||||||
|
uninstallDisplayName: ${productName}
|
||||||
|
createDesktopShortcut: always
|
||||||
|
mac:
|
||||||
|
entitlementsInherit: build/entitlements.mac.plist
|
||||||
|
extendInfo:
|
||||||
|
- NSCameraUsageDescription: Application requests access to the device's camera.
|
||||||
|
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
||||||
|
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
|
||||||
|
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
||||||
|
notarize: false
|
||||||
|
dmg:
|
||||||
|
artifactName: ${name}-${version}.${ext}
|
||||||
|
linux:
|
||||||
|
target:
|
||||||
|
- AppImage
|
||||||
|
- snap
|
||||||
|
- deb
|
||||||
|
maintainer: electronjs.org
|
||||||
|
category: Utility
|
||||||
|
appImage:
|
||||||
|
artifactName: ${name}-${version}.${ext}
|
||||||
|
npmRebuild: false
|
||||||
|
publish:
|
||||||
|
provider: generic
|
||||||
|
url: https://example.com/auto-updates
|
||||||
16
frontend/pc/IM/electron.vite.config.mjs
Normal file
16
frontend/pc/IM/electron.vite.config.mjs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { resolve } from 'path'
|
||||||
|
import { defineConfig } from 'electron-vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
main: {},
|
||||||
|
preload: {},
|
||||||
|
renderer: {
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': resolve('src/renderer/src')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: [vue()]
|
||||||
|
}
|
||||||
|
})
|
||||||
30
frontend/pc/IM/eslint.config.mjs
Normal file
30
frontend/pc/IM/eslint.config.mjs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import eslintConfig from '@electron-toolkit/eslint-config'
|
||||||
|
import eslintConfigPrettier from '@electron-toolkit/eslint-config-prettier'
|
||||||
|
import eslintPluginVue from 'eslint-plugin-vue'
|
||||||
|
import vueParser from 'vue-eslint-parser'
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{ ignores: ['**/node_modules', '**/dist', '**/out'] },
|
||||||
|
eslintConfig,
|
||||||
|
...eslintPluginVue.configs['flat/recommended'],
|
||||||
|
{
|
||||||
|
files: ['**/*.vue'],
|
||||||
|
languageOptions: {
|
||||||
|
parser: vueParser,
|
||||||
|
parserOptions: {
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true
|
||||||
|
},
|
||||||
|
extraFileExtensions: ['.vue']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.{js,jsx,vue}'],
|
||||||
|
rules: {
|
||||||
|
'vue/require-default-prop': 'off',
|
||||||
|
'vue/multi-word-component-names': 'off'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
eslintConfigPrettier
|
||||||
|
]
|
||||||
8888
frontend/pc/IM/package-lock.json
generated
Normal file
8888
frontend/pc/IM/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
53
frontend/pc/IM/package.json
Normal file
53
frontend/pc/IM/package.json
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"name": "my-electron-app",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "An Electron application with Vue",
|
||||||
|
"main": "./out/main/index.js",
|
||||||
|
"author": "example.com",
|
||||||
|
"homepage": "https://electron-vite.org",
|
||||||
|
"scripts": {
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"lint": "eslint --cache .",
|
||||||
|
"start": "electron-vite preview",
|
||||||
|
"dev": "electron-vite dev",
|
||||||
|
"build": "electron-vite build",
|
||||||
|
"postinstall": "electron-builder install-app-deps",
|
||||||
|
"build:unpack": "npm run build && electron-builder --dir",
|
||||||
|
"build:win": "npm run build && electron-builder --win",
|
||||||
|
"build:mac": "npm run build && electron-builder --mac",
|
||||||
|
"build:linux": "npm run build && electron-builder --linux"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@cloudgeek/vue3-video-player": "^0.3.10",
|
||||||
|
"@electron-toolkit/preload": "^3.0.2",
|
||||||
|
"@electron-toolkit/utils": "^4.0.0",
|
||||||
|
"@microsoft/signalr": "^10.0.0",
|
||||||
|
"@vuelidate/core": "^2.0.3",
|
||||||
|
"@vuelidate/validators": "^2.0.4",
|
||||||
|
"axios": "^1.13.2",
|
||||||
|
"electron-updater": "^6.3.9",
|
||||||
|
"feather-icons": "^4.29.2",
|
||||||
|
"hevue-img-preview": "^7.1.3",
|
||||||
|
"idb": "^8.0.3",
|
||||||
|
"pinia": "^3.0.3",
|
||||||
|
"spark-md5": "^3.0.2",
|
||||||
|
"vee-validate": "^4.15.1",
|
||||||
|
"vue": "^3.5.22",
|
||||||
|
"vue-router": "^4.5.1",
|
||||||
|
"yup": "^1.7.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@electron-toolkit/eslint-config": "^2.1.0",
|
||||||
|
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
|
||||||
|
"@vitejs/plugin-vue": "^6.0.2",
|
||||||
|
"electron": "^39.2.6",
|
||||||
|
"electron-builder": "^26.0.12",
|
||||||
|
"electron-vite": "^5.0.0",
|
||||||
|
"eslint": "^9.39.1",
|
||||||
|
"eslint-plugin-vue": "^10.6.2",
|
||||||
|
"prettier": "^3.7.4",
|
||||||
|
"vite": "^7.2.6",
|
||||||
|
"vue": "^3.5.25",
|
||||||
|
"vue-eslint-parser": "^10.2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
frontend/pc/IM/resources/icon-back.png
Normal file
BIN
frontend/pc/IM/resources/icon-back.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
BIN
frontend/pc/IM/resources/icon.png
Normal file
BIN
frontend/pc/IM/resources/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 523 KiB |
BIN
frontend/pc/IM/resources/icon1.png
Normal file
BIN
frontend/pc/IM/resources/icon1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
85
frontend/pc/IM/src/main/index.js
Normal file
85
frontend/pc/IM/src/main/index.js
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { app, shell, BrowserWindow, ipcMain } from 'electron'
|
||||||
|
import { join } from 'path'
|
||||||
|
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
|
||||||
|
import icon from '../../resources/icon.png?asset'
|
||||||
|
import { registerWindowHandler } from './ipcHandlers/window'
|
||||||
|
import { createTry } from './trayHandler'
|
||||||
|
|
||||||
|
function createWindow() {
|
||||||
|
// Create the browser window.
|
||||||
|
const mainWindow = new BrowserWindow({
|
||||||
|
width: 900,
|
||||||
|
height: 670,
|
||||||
|
show: false,
|
||||||
|
autoHideMenuBar: true,
|
||||||
|
frame:false,
|
||||||
|
...(process.platform === 'linux' ? { icon } : {}), // Linux 必须在这里设
|
||||||
|
icon: join(__dirname, '../../resources/icon.png'), // Windows 开发环境预览
|
||||||
|
webPreferences: {
|
||||||
|
preload: join(__dirname, '../preload/index.js'),
|
||||||
|
sandbox: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
createTry(mainWindow);
|
||||||
|
|
||||||
|
mainWindow.on('ready-to-show', () => {
|
||||||
|
mainWindow.show()
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||||
|
shell.openExternal(details.url)
|
||||||
|
return { action: 'deny' }
|
||||||
|
})
|
||||||
|
|
||||||
|
// HMR for renderer base on electron-vite cli.
|
||||||
|
// Load the remote URL for development or the local html file for production.
|
||||||
|
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
||||||
|
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
|
||||||
|
} else {
|
||||||
|
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This method will be called when Electron has finished
|
||||||
|
// initialization and is ready to create browser windows.
|
||||||
|
// Some APIs can only be used after this event occurs.
|
||||||
|
app.whenReady().then(() => {
|
||||||
|
// Set app user model id for windows
|
||||||
|
electronApp.setAppUserModelId('com.electron')
|
||||||
|
|
||||||
|
// Default open or close DevTools by F12 in development
|
||||||
|
// and ignore CommandOrControl + R in production.
|
||||||
|
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
|
||||||
|
app.on('browser-window-created', (_, window) => {
|
||||||
|
optimizer.watchWindowShortcuts(window)
|
||||||
|
})
|
||||||
|
|
||||||
|
// IPC test
|
||||||
|
ipcMain.on('ping', () => console.log('pong'))
|
||||||
|
|
||||||
|
registerWindowHandler()
|
||||||
|
|
||||||
|
createWindow()
|
||||||
|
|
||||||
|
|
||||||
|
app.on('activate', function () {
|
||||||
|
// On macOS it's common to re-create a window in the app when the
|
||||||
|
// dock icon is clicked and there are no other windows open.
|
||||||
|
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Quit when all windows are closed, except on macOS. There, it's common
|
||||||
|
// for applications and their menu bar to stay active until the user quits
|
||||||
|
// explicitly with Cmd + Q.
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
//app.quit()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// In this file you can include the rest of your app's specific main process
|
||||||
|
// code. You can also put them in separate files and require them here.
|
||||||
68
frontend/pc/IM/src/main/ipcHandlers/window.js
Normal file
68
frontend/pc/IM/src/main/ipcHandlers/window.js
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { ipcMain, BrowserWindow } from 'electron'
|
||||||
|
import icon from '../../../resources/icon.png?asset'
|
||||||
|
import { join } from 'path'
|
||||||
|
import { is } from '@electron-toolkit/utils'
|
||||||
|
|
||||||
|
export function registerWindowHandler() {
|
||||||
|
const windowMapData = new Map()
|
||||||
|
|
||||||
|
ipcMain.on('window-action', (event, action) => {
|
||||||
|
const win = BrowserWindow.fromWebContents(event.sender)
|
||||||
|
if (!win) return
|
||||||
|
const actions = {
|
||||||
|
minimize: () => win.minimize(),
|
||||||
|
maximize: () => (win.isMaximized() ? win.unmaximize() : win.maximize()),
|
||||||
|
close: () => win.hide(),
|
||||||
|
closeThis: () => {
|
||||||
|
const mainWin = BrowserWindow.fromId(1); // 假设 ID 1 是主窗口
|
||||||
|
const win = BrowserWindow.fromWebContents(event.sender)
|
||||||
|
if(win.id != mainWin?.id){
|
||||||
|
win.destroy()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isMaximized: () => win.isMaximized()
|
||||||
|
}
|
||||||
|
actions[action]?.()
|
||||||
|
})
|
||||||
|
ipcMain.on('window-new', (event, { route, data }) => {
|
||||||
|
const win = new BrowserWindow({
|
||||||
|
width: 900,
|
||||||
|
height: 670,
|
||||||
|
show: true,
|
||||||
|
autoHideMenuBar: true,
|
||||||
|
frame: false,
|
||||||
|
...(process.platform === 'linux' ? { icon } : {}), // Linux 必须在这里设
|
||||||
|
icon: join(__dirname, '../../../resources/icon.png'), // Windows 开发环境预览
|
||||||
|
webPreferences: {
|
||||||
|
preload: join(__dirname, '../preload/index.js'),
|
||||||
|
sandbox: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const winId = win.id
|
||||||
|
windowMapData.set(winId, data)
|
||||||
|
|
||||||
|
// 窗口关闭时,记得清理内存,防止内存泄漏
|
||||||
|
win.on('closed', () => {
|
||||||
|
windowMapData.delete(winId)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 构建 Query 参数
|
||||||
|
const queryStr = `?winId=${winId}`
|
||||||
|
|
||||||
|
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
||||||
|
// 开发环境:通常使用 Hash 路由 (如 http://localhost:5173/#/your-route?winId=1)
|
||||||
|
win.loadURL(`${process.env['ELECTRON_RENDERER_URL']}#/${route}${queryStr}`)
|
||||||
|
} else {
|
||||||
|
// 生产环境:loadFile 必须通过 hash 参数来传递路由和参数
|
||||||
|
// 注意:join 会合并路径,hash 部分需要单独传给 options
|
||||||
|
win.loadFile(join(__dirname, '../../renderer/index.html'), {
|
||||||
|
hash: `${route}${queryStr}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// 3. 增加数据索要接口
|
||||||
|
ipcMain.handle('get-window-data', (event, winId) => {
|
||||||
|
return windowMapData.get(Number(winId))
|
||||||
|
})
|
||||||
|
}
|
||||||
28
frontend/pc/IM/src/main/trayHandler.js
Normal file
28
frontend/pc/IM/src/main/trayHandler.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { app, Tray, Menu, nativeImage } from 'electron'
|
||||||
|
import path from 'path'
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
let tray = null;
|
||||||
|
|
||||||
|
export function createTry(mainWindow){
|
||||||
|
const iconPath = path.join(__dirname, '../../resources/icon.png')
|
||||||
|
const icon = nativeImage.createFromPath(iconPath)
|
||||||
|
|
||||||
|
// 2. 创建托盘实例
|
||||||
|
tray = new Tray(icon)
|
||||||
|
|
||||||
|
const menu = Menu.buildFromTemplate([
|
||||||
|
{label: '退出', click: () => app.quit()},
|
||||||
|
{label: '设置', click: () => {
|
||||||
|
mainWindow.webContents.send('router-ctl', '/settings')
|
||||||
|
mainWindow.show()
|
||||||
|
}}
|
||||||
|
]);
|
||||||
|
tray.setToolTip('IM');
|
||||||
|
tray.setContextMenu(menu);
|
||||||
|
|
||||||
|
// 5. 点击托盘图标显示窗口 (可选)
|
||||||
|
tray.on('click', () => {
|
||||||
|
mainWindow.isVisible() ? mainWindow.focus() : mainWindow.show()
|
||||||
|
})
|
||||||
|
}
|
||||||
30
frontend/pc/IM/src/preload/index.js
Normal file
30
frontend/pc/IM/src/preload/index.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { contextBridge, ipcRenderer } from 'electron'
|
||||||
|
import { electronAPI } from '@electron-toolkit/preload'
|
||||||
|
|
||||||
|
// Custom APIs for renderer
|
||||||
|
const api = {
|
||||||
|
window: {
|
||||||
|
minimize: () => ipcRenderer.send('window-action', 'minimize'),
|
||||||
|
maximize: () => ipcRenderer.send('window-action', 'maximize'),
|
||||||
|
close: () => ipcRenderer.send('window-action', 'close'),
|
||||||
|
closeThis: () => ipcRenderer.send('window-action', 'closeThis'),
|
||||||
|
isMaximized: () => ipcRenderer.send('window-action', 'isMaximized'),
|
||||||
|
newWindow: (route, data) => ipcRenderer.send('window-new', { route, data }),
|
||||||
|
getWindowData: (winId) => ipcRenderer.invoke('get-window-data', winId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use `contextBridge` APIs to expose Electron APIs to
|
||||||
|
// renderer only if context isolation is enabled, otherwise
|
||||||
|
// just add to the DOM global.
|
||||||
|
if (process.contextIsolated) {
|
||||||
|
try {
|
||||||
|
contextBridge.exposeInMainWorld('electron', electronAPI)
|
||||||
|
contextBridge.exposeInMainWorld('api', api)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
window.electron = electronAPI
|
||||||
|
window.api = api
|
||||||
|
}
|
||||||
21
frontend/pc/IM/src/renderer/index.html
Normal file
21
frontend/pc/IM/src/renderer/index.html
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Electron</title>
|
||||||
|
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
||||||
|
<meta http-equiv="Content-Security-Policy"
|
||||||
|
content="default-src 'self';
|
||||||
|
script-src 'self' 'unsafe-inline';
|
||||||
|
style-src 'self' 'unsafe-inline';
|
||||||
|
connect-src 'self' http://localhost:5202 ws://localhost:5202;
|
||||||
|
img-src 'self' data: blob: https: http:;
|
||||||
|
font-src 'self' data:;
|
||||||
|
media-src 'self' blob:;">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
52
frontend/pc/IM/src/renderer/src/App.vue
Normal file
52
frontend/pc/IM/src/renderer/src/App.vue
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<template>
|
||||||
|
<div id="app">
|
||||||
|
<RouterView></RouterView>
|
||||||
|
<Alert></Alert>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import Alert from '@/components/messages/Alert.vue';
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
import { useAuthStore } from './stores/auth';
|
||||||
|
//import { useSignalRStore } from './stores/signalr';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const router = useRouter()
|
||||||
|
if(window.electron){
|
||||||
|
window.electron.ipcRenderer.on('router-ctl', (e, path) => {
|
||||||
|
router.push(path)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
#app {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addMenu {
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
}
|
||||||
|
.search-box {
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
}
|
||||||
|
.search-section {
|
||||||
|
-webkit-app-region: drag;
|
||||||
|
}
|
||||||
|
#ContactContainer {
|
||||||
|
flex: 1;
|
||||||
|
background-color: #f5f5f5; /* 与聊天列表右侧背景保持一致 */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user