diff --git a/backend/IMTest/bin/Debug/net8.0/IMTest.deps.json b/backend/IMTest/bin/Debug/net8.0/IMTest.deps.json index 8a8287e..f45fecd 100644 --- a/backend/IMTest/bin/Debug/net8.0/IMTest.deps.json +++ b/backend/IMTest/bin/Debug/net8.0/IMTest.deps.json @@ -1002,6 +1002,14 @@ "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.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": { "dependencies": { "Microsoft.Extensions.Logging.Abstractions": "10.0.2", @@ -1692,6 +1700,7 @@ "Newtonsoft.Json": "13.0.4", "Pomelo.EntityFrameworkCore.MySql": "8.0.3", "RedLock.net": "2.3.2", + "SixLabors.ImageSharp": "3.1.12", "StackExchange.Redis": "2.9.32", "Swashbuckle.AspNetCore": "6.6.2", "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", "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": { "type": "package", "serviceable": true, diff --git a/backend/IMTest/bin/Debug/net8.0/IMTest.dll b/backend/IMTest/bin/Debug/net8.0/IMTest.dll index 20b497b..044ea8a 100644 Binary files a/backend/IMTest/bin/Debug/net8.0/IMTest.dll and b/backend/IMTest/bin/Debug/net8.0/IMTest.dll differ diff --git a/backend/IMTest/bin/Debug/net8.0/IMTest.pdb b/backend/IMTest/bin/Debug/net8.0/IMTest.pdb index 6695021..a3d1b90 100644 Binary files a/backend/IMTest/bin/Debug/net8.0/IMTest.pdb and b/backend/IMTest/bin/Debug/net8.0/IMTest.pdb differ diff --git a/backend/IMTest/bin/Debug/net8.0/IM_API.deps.json b/backend/IMTest/bin/Debug/net8.0/IM_API.deps.json index 5086600..a5796bf 100644 --- a/backend/IMTest/bin/Debug/net8.0/IM_API.deps.json +++ b/backend/IMTest/bin/Debug/net8.0/IM_API.deps.json @@ -20,6 +20,7 @@ "Newtonsoft.Json": "13.0.4", "Pomelo.EntityFrameworkCore.MySql": "8.0.3", "RedLock.net": "2.3.2", + "SixLabors.ImageSharp": "3.1.12", "StackExchange.Redis": "2.9.32", "Swashbuckle.AspNetCore": "6.6.2", "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": { "dependencies": { "Microsoft.Extensions.Logging.Abstractions": "10.0.2", @@ -1564,6 +1573,13 @@ "path": "redlock.net/2.3.2", "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": { "type": "package", "serviceable": true, diff --git a/backend/IMTest/bin/Debug/net8.0/IM_API.dll b/backend/IMTest/bin/Debug/net8.0/IM_API.dll index ad71984..317926a 100644 Binary files a/backend/IMTest/bin/Debug/net8.0/IM_API.dll and b/backend/IMTest/bin/Debug/net8.0/IM_API.dll differ diff --git a/backend/IMTest/bin/Debug/net8.0/IM_API.exe b/backend/IMTest/bin/Debug/net8.0/IM_API.exe index 201e73a..598449d 100644 Binary files a/backend/IMTest/bin/Debug/net8.0/IM_API.exe and b/backend/IMTest/bin/Debug/net8.0/IM_API.exe differ diff --git a/backend/IMTest/bin/Debug/net8.0/IM_API.pdb b/backend/IMTest/bin/Debug/net8.0/IM_API.pdb index 008f17a..3ee5a3b 100644 Binary files a/backend/IMTest/bin/Debug/net8.0/IM_API.pdb and b/backend/IMTest/bin/Debug/net8.0/IM_API.pdb differ diff --git a/backend/IMTest/bin/Debug/net8.0/appsettings.json b/backend/IMTest/bin/Debug/net8.0/appsettings.json index f1d60df..3211965 100644 --- a/backend/IMTest/bin/Debug/net8.0/appsettings.json +++ b/backend/IMTest/bin/Debug/net8.0/appsettings.json @@ -14,7 +14,7 @@ "RefreshTokenDays": 30 }, "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" }, "RabbitMQOptions": { @@ -25,6 +25,6 @@ }, "FileUploadOptions": { "DefaultStorage": "Local", - "ChunkSize": 10, + "ChunkSize": 5000000, } } diff --git a/backend/IMTest/obj/Debug/net8.0/IMTest.AssemblyInfo.cs b/backend/IMTest/obj/Debug/net8.0/IMTest.AssemblyInfo.cs index 758c389..29b15fd 100644 --- a/backend/IMTest/obj/Debug/net8.0/IMTest.AssemblyInfo.cs +++ b/backend/IMTest/obj/Debug/net8.0/IMTest.AssemblyInfo.cs @@ -14,7 +14,7 @@ using System.Reflection; [assembly: System.Reflection.AssemblyCompanyAttribute("IMTest")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] -[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+d42956051108811cdd7be6a07af6b11e1d7f0d15")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+2ecaa28091b41de707825db3628d380b62fa727f")] [assembly: System.Reflection.AssemblyProductAttribute("IMTest")] [assembly: System.Reflection.AssemblyTitleAttribute("IMTest")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] diff --git a/backend/IMTest/obj/Debug/net8.0/IMTest.AssemblyInfoInputs.cache b/backend/IMTest/obj/Debug/net8.0/IMTest.AssemblyInfoInputs.cache index 4919883..d02a3dc 100644 --- a/backend/IMTest/obj/Debug/net8.0/IMTest.AssemblyInfoInputs.cache +++ b/backend/IMTest/obj/Debug/net8.0/IMTest.AssemblyInfoInputs.cache @@ -1 +1 @@ -fa24b386648cc4dba48ae5e3f91e5303b0dfd0971bba62bc27ca1580a5064337 +ed4980dfc7aff253176b260ed9015f9a80b52e92cbf3095eff3ed06865ea6e0d diff --git a/backend/IMTest/obj/Debug/net8.0/IMTest.csproj.AssemblyReference.cache b/backend/IMTest/obj/Debug/net8.0/IMTest.csproj.AssemblyReference.cache index 30142a3..2932c7a 100644 Binary files a/backend/IMTest/obj/Debug/net8.0/IMTest.csproj.AssemblyReference.cache and b/backend/IMTest/obj/Debug/net8.0/IMTest.csproj.AssemblyReference.cache differ diff --git a/backend/IMTest/obj/Debug/net8.0/IMTest.csproj.CoreCompileInputs.cache b/backend/IMTest/obj/Debug/net8.0/IMTest.csproj.CoreCompileInputs.cache index d84992b..6242309 100644 --- a/backend/IMTest/obj/Debug/net8.0/IMTest.csproj.CoreCompileInputs.cache +++ b/backend/IMTest/obj/Debug/net8.0/IMTest.csproj.CoreCompileInputs.cache @@ -1 +1 @@ -b2b545acb4173028f4d41b8d8c0aea03ecbcecb4eeabe997ca466c2e265beff6 +a18d4d5688b125e6729fd465f09e267a2a7532eadaaca930389969ac369409ce diff --git a/backend/IMTest/obj/Debug/net8.0/IMTest.csproj.FileListAbsolute.txt b/backend/IMTest/obj/Debug/net8.0/IMTest.csproj.FileListAbsolute.txt index 6a24730..9b395c6 100644 --- a/backend/IMTest/obj/Debug/net8.0/IMTest.csproj.FileListAbsolute.txt +++ b/backend/IMTest/obj/Debug/net8.0/IMTest.csproj.FileListAbsolute.txt @@ -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.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\SixLabors.ImageSharp.dll diff --git a/backend/IMTest/obj/Debug/net8.0/IMTest.dll b/backend/IMTest/obj/Debug/net8.0/IMTest.dll index 20b497b..044ea8a 100644 Binary files a/backend/IMTest/obj/Debug/net8.0/IMTest.dll and b/backend/IMTest/obj/Debug/net8.0/IMTest.dll differ diff --git a/backend/IMTest/obj/Debug/net8.0/IMTest.pdb b/backend/IMTest/obj/Debug/net8.0/IMTest.pdb index 6695021..a3d1b90 100644 Binary files a/backend/IMTest/obj/Debug/net8.0/IMTest.pdb and b/backend/IMTest/obj/Debug/net8.0/IMTest.pdb differ diff --git a/backend/IMTest/obj/Debug/net8.0/ref/IMTest.dll b/backend/IMTest/obj/Debug/net8.0/ref/IMTest.dll index 5678266..2793b43 100644 Binary files a/backend/IMTest/obj/Debug/net8.0/ref/IMTest.dll and b/backend/IMTest/obj/Debug/net8.0/ref/IMTest.dll differ diff --git a/backend/IMTest/obj/Debug/net8.0/refint/IMTest.dll b/backend/IMTest/obj/Debug/net8.0/refint/IMTest.dll index 5678266..2793b43 100644 Binary files a/backend/IMTest/obj/Debug/net8.0/refint/IMTest.dll and b/backend/IMTest/obj/Debug/net8.0/refint/IMTest.dll differ diff --git a/backend/IM_API/Configs/MapperConfig.cs b/backend/IM_API/Configs/MapperConfig.cs index 4ba8095..431657e 100644 --- a/backend/IM_API/Configs/MapperConfig.cs +++ b/backend/IM_API/Configs/MapperConfig.cs @@ -11,6 +11,7 @@ using IM_API.Models.Upload; using IM_API.Tools; using IM_API.VOs; using IM_API.VOs.Conversation; +using IM_API.VOs.Group; using IM_API.VOs.Message; namespace IM_API.Configs @@ -203,6 +204,17 @@ namespace IM_API.Configs .ForMember(dest => dest.Size, opt => opt.MapFrom(src => src.FileSize)); CreateMap(); + + //群成员模型 + CreateMap() + .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() + .ForMember(dest => dest.Created, opt => opt.MapFrom(src => src.Created)) + .ForMember(dest => dest.Role, opt => opt.MapFrom(src => src.RoleEnum)); } } } diff --git a/backend/IM_API/Controllers/GroupController.cs b/backend/IM_API/Controllers/GroupController.cs index 0f7582f..6dfdf8f 100644 --- a/backend/IM_API/Controllers/GroupController.cs +++ b/backend/IM_API/Controllers/GroupController.cs @@ -1,6 +1,7 @@ using IM_API.Dtos; using IM_API.Dtos.Group; using IM_API.Interface.Services; +using IM_API.VOs.Group; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -67,5 +68,14 @@ namespace IM_API.Controllers var res = new BaseResponse(); return Ok(res); } + + [HttpGet] + [ProducesResponseType(typeof(BaseResponse>), StatusCodes.Status200OK)] + public async Task GetGroupMembers([FromQuery]int groupId) + { + var useridStr = User.FindFirstValue(ClaimTypes.NameIdentifier); + var members = await _groupService.GetGroupMembers(int.Parse(useridStr), groupId); + return Ok(new BaseResponse>(members)); + } } } diff --git a/backend/IM_API/Interface/Services/IGroupService.cs b/backend/IM_API/Interface/Services/IGroupService.cs index 1b80b62..fa7e4e8 100644 --- a/backend/IM_API/Interface/Services/IGroupService.cs +++ b/backend/IM_API/Interface/Services/IGroupService.cs @@ -1,5 +1,6 @@ using IM_API.Dtos.Group; using IM_API.Models; +using IM_API.VOs.Group; namespace IM_API.Interface.Services { @@ -51,5 +52,6 @@ namespace IM_API.Interface.Services Task HandleGroupRequestAsync(int userid, HandleGroupRequestDto dto); Task MakeGroupRequestAsync(int userId,int? adminUserId,int groupId); Task MakeGroupMemberAsync(int userId, int groupId, GroupMemberRole? role); + Task> GetGroupMembers(int userId, int groupId); } } diff --git a/backend/IM_API/Migrations/20260306065353_uploadtask-url.Designer.cs b/backend/IM_API/Migrations/20260306065353_uploadtask-url.Designer.cs new file mode 100644 index 0000000..bf5ce4e --- /dev/null +++ b/backend/IM_API/Migrations/20260306065353_uploadtask-url.Designer.cs @@ -0,0 +1,1173 @@ +// +using System; +using IM_API.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace IM_API.Migrations +{ + [DbContext(typeof(ImContext))] + [Migration("20260306065353_uploadtask-url")] + partial class uploadtaskurl + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .UseCollation("latin1_swedish_ci") + .HasAnnotation("ProductVersion", "8.0.21") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.HasCharSet(modelBuilder, "latin1"); + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("IM_API.Models.Admin", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetime") + .HasComment("创建时间 "); + + b.Property("Password") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .HasComment("密码"); + + b.Property("RoleId") + .HasColumnType("int(11)") + .HasComment("角色"); + + b.Property("State") + .HasColumnType("tinyint(4)") + .HasComment("状态(0:正常,2:封禁) "); + + b.Property("Updated") + .HasColumnType("datetime") + .HasComment("更新时间 "); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .HasComment("用户名"); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.HasIndex(new[] { "RoleId" }, "RoleId"); + + b.ToTable("admins", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.Conversation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ChatType") + .HasColumnType("int(11)"); + + b.Property("LastMessage") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasComment("最后一条最新消息"); + + b.Property("LastMessageTime") + .HasColumnType("datetime") + .HasComment("最后一条消息发送时间"); + + b.Property("LastReadSequenceId") + .HasColumnType("int(11)") + .HasColumnName("lastReadMessageId") + .HasComment("最后一条未读消息ID "); + + b.Property("MessageId") + .HasColumnType("int(11)"); + + b.Property("StreamKey") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasComment("消息推送唯一标识符"); + + b.Property("TargetId") + .HasColumnType("int(11)") + .HasComment("对方ID(群聊为群聊ID,单聊为单聊ID) "); + + b.Property("UnreadCount") + .HasColumnType("int(11)") + .HasComment("未读消息数 "); + + b.Property("UserId") + .HasColumnType("int(11)") + .HasComment("用户"); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.HasIndex("MessageId"); + + b.HasIndex(new[] { "LastReadSequenceId" }, "LastReadSequenceId"); + + b.HasIndex(new[] { "UserId" }, "Userid"); + + b.ToTable("conversations", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Dtype") + .HasColumnType("tinyint(4)") + .HasColumnName("DType") + .HasComment("设备类型(\r\n0:Android,1:Ios,2:PC,3:Pad,4:未知)"); + + b.Property("LastLogin") + .HasColumnType("datetime") + .HasComment("最后一次登录 "); + + b.Property("UserId") + .HasColumnType("int(11)") + .HasComment("设备所属用户 "); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.HasIndex(new[] { "UserId" }, "Userid") + .HasDatabaseName("Userid1"); + + b.ToTable("devices", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.File", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetime") + .HasComment("创建时间 "); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)") + .HasComment("文件类型 "); + + b.Property("MessageId") + .HasColumnType("int(11)") + .HasComment("关联消息ID "); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .HasComment("文件名 "); + + b.Property("Size") + .HasColumnType("int(11)") + .HasComment("文件大小(单位:KB) "); + + b.Property("Url") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("URL") + .HasComment("文件储存URL "); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.HasIndex(new[] { "MessageId" }, "Messageld"); + + b.ToTable("files", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.Friend", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Avatar") + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasComment("好友头像"); + + b.Property("Created") + .HasColumnType("datetime") + .HasComment("好友关系创建时间"); + + b.Property("FriendId") + .HasColumnType("int(11)") + .HasComment("用户2ID"); + + b.Property("RemarkName") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasComment("好友备注名"); + + b.Property("Status") + .HasColumnType("tinyint(4)") + .HasComment("当前好友关系状态\r\n(0:待通过,1:已添加,2:已拒绝,3:已拉黑)"); + + b.Property("UserId") + .HasColumnType("int(11)") + .HasComment("用户ID"); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.HasIndex(new[] { "Id" }, "ID"); + + b.HasIndex(new[] { "UserId", "FriendId" }, "Userld"); + + b.HasIndex(new[] { "FriendId" }, "用户2id"); + + b.ToTable("friends", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.FriendRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetime") + .HasComment("申请时间 "); + + b.Property("Description") + .HasColumnType("text") + .HasComment("申请附言 "); + + b.Property("RemarkName") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasComment("备注"); + + b.Property("RequestUser") + .HasColumnType("int(11)") + .HasComment("申请人 "); + + b.Property("ResponseUser") + .HasColumnType("int(11)") + .HasComment("被申请人 "); + + b.Property("State") + .HasColumnType("tinyint(4)") + .HasComment("申请状态(0:待通过,1:拒绝,2:同意,3:拉黑) "); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.HasIndex(new[] { "RequestUser" }, "RequestUser"); + + b.HasIndex(new[] { "ResponseUser" }, "ResponseUser"); + + b.ToTable("friend_request", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.Group", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AllMembersBanned") + .HasColumnType("tinyint(4)") + .HasComment("全员禁言(0允许发言,2全员禁言)"); + + b.Property("Announcement") + .HasColumnType("text") + .HasComment("群公告"); + + b.Property("Auhority") + .HasColumnType("tinyint(4)") + .HasComment("群权限\r\n(0:需管理员同意,1:任意人可加群,2:不允许任何人加入)"); + + b.Property("Avatar") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasComment("群头像"); + + b.Property("Created") + .HasColumnType("datetime") + .HasComment("群聊创建时间"); + + b.Property("GroupMaster") + .HasColumnType("int(11)") + .HasComment("群主"); + + b.Property("LastMessage") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("LastSenderName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("LastUpdateTime") + .HasColumnType("datetime(6)"); + + b.Property("MaxSequenceId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasComment("群聊名称"); + + b.Property("Status") + .HasColumnType("tinyint(4)") + .HasComment("群聊状态\r\n(1:正常,2:封禁)"); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.HasIndex(new[] { "GroupMaster" }, "GroupMaster"); + + b.HasIndex(new[] { "Id" }, "ID") + .HasDatabaseName("ID1"); + + b.ToTable("groups", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.GroupInvite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetime") + .HasComment("创建时间"); + + b.Property("GroupId") + .HasColumnType("int(11)") + .HasComment("群聊编号"); + + b.Property("InviteUser") + .HasColumnType("int(11)") + .HasComment("邀请用户"); + + b.Property("InvitedUser") + .HasColumnType("int(11)") + .HasComment("被邀请用户"); + + b.Property("State") + .HasColumnType("tinyint(4)") + .HasComment("当前状态(0:待被邀请人同意\r\n1:被邀请人已同意)"); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.HasIndex(new[] { "GroupId" }, "GroupId"); + + b.HasIndex(new[] { "InviteUser" }, "InviteUser"); + + b.HasIndex(new[] { "InvitedUser" }, "InvitedUser"); + + b.ToTable("group_invite", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.GroupMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Created") + .ValueGeneratedOnAdd() + .HasColumnType("datetime") + .HasDefaultValueSql("'1970-01-01 00:00:00'") + .HasComment("加入群聊时间"); + + b.Property("GroupId") + .HasColumnType("int(11)") + .HasComment("群聊编号"); + + b.Property("Role") + .HasColumnType("tinyint(4)") + .HasComment("成员角色(0:普通成员,1:管理员,2:群主)"); + + b.Property("UserId") + .HasColumnType("int(11)") + .HasComment("用户编号"); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.HasIndex(new[] { "GroupId" }, "Groupld"); + + b.HasIndex(new[] { "Id" }, "ID") + .HasDatabaseName("ID2"); + + b.HasIndex(new[] { "UserId" }, "Userld") + .HasDatabaseName("Userld1"); + + b.ToTable("group_member", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.GroupRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetime") + .HasComment("创建时间"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasComment("入群附言"); + + b.Property("GroupId") + .HasColumnType("int(11)") + .HasComment("群聊编号\r\n"); + + b.Property("State") + .HasColumnType("tinyint(4)") + .HasComment("申请状态(0:待管理员同意,1:已拒绝,2:已同意)"); + + b.Property("UserId") + .HasColumnType("int(11)") + .HasComment("申请人 "); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.HasIndex("UserId"); + + b.HasIndex(new[] { "GroupId" }, "GroupId") + .HasDatabaseName("GroupId1"); + + b.ToTable("group_request", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.LoginLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Dtype") + .HasColumnType("tinyint(4)") + .HasColumnName("DType") + .HasComment("设备类型(通Devices/DType) "); + + b.Property("Logined") + .HasColumnType("datetime") + .HasComment("登录时间 "); + + b.Property("State") + .HasColumnType("tinyint(4)") + .HasComment("登录状态(0:登陆成功,1:未验证,2:已被拒绝) "); + + b.Property("UserId") + .HasColumnType("int(11)") + .HasComment("登录用户 "); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.HasIndex(new[] { "UserId" }, "Userld") + .HasDatabaseName("Userld2"); + + b.ToTable("login_log", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ChatType") + .HasColumnType("tinyint(4)") + .HasComment("聊天类型\r\n(0:私聊,1:群聊)"); + + b.Property("ClientMsgId") + .HasColumnType("char(36)"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text") + .HasComment("消息内容 "); + + b.Property("Created") + .HasColumnType("datetime") + .HasComment("发送时间 "); + + b.Property("MsgType") + .HasColumnType("tinyint(4)") + .HasComment("消息类型\r\n(0:文本,1:图片,2:语音,3:视频,4:文件,5:语音聊天,6:视频聊天)"); + + b.Property("Recipient") + .HasColumnType("int(11)") + .HasComment("接收者(私聊为用户ID,群聊为群聊ID) "); + + b.Property("Sender") + .HasColumnType("int(11)") + .HasComment("发送者 "); + + b.Property("SequenceId") + .HasColumnType("bigint"); + + b.Property("State") + .HasColumnType("tinyint(4)") + .HasComment("消息状态(0:已发送,1:已撤回) "); + + b.Property("StreamKey") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasComment("消息推送唯一标识符"); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.HasIndex("SequenceId", "StreamKey") + .IsUnique(); + + b.HasIndex(new[] { "Sender" }, "Sender"); + + b.ToTable("messages", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasColumnType("text") + .HasComment("通知内容"); + + b.Property("Created") + .HasColumnType("datetime") + .HasComment("创建时间"); + + b.Property("Ntype") + .HasColumnType("tinyint(4)") + .HasColumnName("NType") + .HasComment("通知类型(0:文本)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasComment("通知标题"); + + b.Property("UserId") + .HasColumnType("int(11)") + .HasComment("接收人(为空为全体通知)"); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.HasIndex(new[] { "UserId" }, "Userld") + .HasDatabaseName("Userld3"); + + b.ToTable("notifications", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("int(11)") + .HasComment("权限编码 "); + + b.Property("Created") + .HasColumnType("datetime") + .HasComment("创建时间 "); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .HasComment("权限名称 "); + + b.Property("Ptype") + .HasColumnType("int(11)") + .HasColumnName("PType") + .HasComment("权限类型(0:增,1:删,2:改,3:查) "); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.ToTable("permissions", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.Permissionarole", b => + { + b.Property("Id") + .HasColumnType("int(11)") + .HasColumnName("ID"); + + b.Property("PermissionId") + .HasColumnType("int(11)") + .HasComment("权限 "); + + b.Property("RoleId") + .HasColumnType("int(11)") + .HasComment("角色 "); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.HasIndex(new[] { "PermissionId" }, "Permissionld"); + + b.HasIndex(new[] { "RoleId" }, "Roleld"); + + b.ToTable("permissionarole", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetime") + .HasComment("创建时间 "); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasComment("角色描述 "); + + b.Property("Name") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasComment("角色名称 "); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.ToTable("roles", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.Upload.UploadTask", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ChunkSize") + .HasColumnType("int"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FileHash") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("ObjectName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ProviderUploadId") + .HasColumnType("longtext"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("StorageProvider") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TotalChunks") + .HasColumnType("int"); + + b.Property("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 => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Avatar") + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasComment("用户头像链接"); + + b.Property("Created") + .ValueGeneratedOnAdd() + .HasColumnType("datetime") + .HasDefaultValueSql("'1970-01-01 00:00:00'") + .HasComment("创建时间"); + + b.Property("IsDeleted") + .HasColumnType("tinyint(4)") + .HasComment("软删除标识\r\n0:账号正常\r\n1:账号已删除"); + + b.Property("NickName") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .HasComment("用户昵称"); + + b.Property("OnlineStatus") + .HasColumnType("tinyint(4)") + .HasComment("用户在线状态\r\n0(默认):不在线\r\n1:在线"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .HasComment("密码"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(4)") + .HasDefaultValueSql("'1'") + .HasComment("账户状态\r\n(0:未激活,1:正常,2:封禁)"); + + b.Property("Updated") + .HasColumnType("datetime") + .HasComment("修改时间"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .HasComment("唯一用户名"); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.HasIndex(new[] { "Id" }, "ID") + .HasDatabaseName("ID3"); + + b.HasIndex(new[] { "Username" }, "Username") + .IsUnique(); + + b.ToTable("users", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.Admin", b => + { + b.HasOne("IM_API.Models.Role", "Role") + .WithMany("Admins") + .HasForeignKey("RoleId") + .IsRequired() + .HasConstraintName("admins_ibfk_1"); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("IM_API.Models.Conversation", b => + { + b.HasOne("IM_API.Models.Message", null) + .WithMany("Conversations") + .HasForeignKey("MessageId"); + + b.HasOne("IM_API.Models.User", "User") + .WithMany("Conversations") + .HasForeignKey("UserId") + .IsRequired() + .HasConstraintName("conversations_ibfk_1"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IM_API.Models.Device", b => + { + b.HasOne("IM_API.Models.User", "User") + .WithMany("Devices") + .HasForeignKey("UserId") + .IsRequired() + .HasConstraintName("devices_ibfk_1"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IM_API.Models.File", b => + { + b.HasOne("IM_API.Models.Message", "Message") + .WithMany("Files") + .HasForeignKey("MessageId") + .IsRequired() + .HasConstraintName("files_ibfk_1"); + + b.Navigation("Message"); + }); + + modelBuilder.Entity("IM_API.Models.Friend", b => + { + b.HasOne("IM_API.Models.User", "FriendNavigation") + .WithMany("FriendFriendNavigations") + .HasForeignKey("FriendId") + .IsRequired() + .HasConstraintName("用户2id"); + + b.HasOne("IM_API.Models.User", "User") + .WithMany("FriendUsers") + .HasForeignKey("UserId") + .IsRequired() + .HasConstraintName("用户id"); + + b.Navigation("FriendNavigation"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IM_API.Models.FriendRequest", b => + { + b.HasOne("IM_API.Models.User", "RequestUserNavigation") + .WithMany("FriendRequestRequestUserNavigations") + .HasForeignKey("RequestUser") + .IsRequired() + .HasConstraintName("friend_request_ibfk_1"); + + b.HasOne("IM_API.Models.User", "ResponseUserNavigation") + .WithMany("FriendRequestResponseUserNavigations") + .HasForeignKey("ResponseUser") + .IsRequired() + .HasConstraintName("friend_request_ibfk_2"); + + b.Navigation("RequestUserNavigation"); + + b.Navigation("ResponseUserNavigation"); + }); + + modelBuilder.Entity("IM_API.Models.Group", b => + { + b.HasOne("IM_API.Models.User", "GroupMasterNavigation") + .WithMany("Groups") + .HasForeignKey("GroupMaster") + .IsRequired() + .HasConstraintName("groups_ibfk_1"); + + b.Navigation("GroupMasterNavigation"); + }); + + modelBuilder.Entity("IM_API.Models.GroupInvite", b => + { + b.HasOne("IM_API.Models.Group", "Group") + .WithMany("GroupInvites") + .HasForeignKey("GroupId") + .IsRequired() + .HasConstraintName("group_invite_ibfk_2"); + + b.HasOne("IM_API.Models.User", "InviteUserNavigation") + .WithMany("GroupInviteInviteUserNavigations") + .HasForeignKey("InviteUser") + .HasConstraintName("group_invite_ibfk_1"); + + b.HasOne("IM_API.Models.User", "InvitedUserNavigation") + .WithMany("GroupInviteInvitedUserNavigations") + .HasForeignKey("InvitedUser") + .HasConstraintName("group_invite_ibfk_3"); + + b.Navigation("Group"); + + b.Navigation("InviteUserNavigation"); + + b.Navigation("InvitedUserNavigation"); + }); + + modelBuilder.Entity("IM_API.Models.GroupMember", b => + { + b.HasOne("IM_API.Models.Group", "Group") + .WithMany("GroupMembers") + .HasForeignKey("GroupId") + .IsRequired() + .HasConstraintName("group_member_ibfk_2"); + + b.HasOne("IM_API.Models.User", "User") + .WithMany("GroupMembers") + .HasForeignKey("UserId") + .IsRequired() + .HasConstraintName("group_member_ibfk_1"); + + b.Navigation("Group"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IM_API.Models.GroupRequest", b => + { + b.HasOne("IM_API.Models.Group", "Group") + .WithMany("GroupRequests") + .HasForeignKey("GroupId") + .IsRequired() + .HasConstraintName("group_request_ibfk_1"); + + b.HasOne("IM_API.Models.User", "User") + .WithMany("GroupRequests") + .HasForeignKey("UserId") + .IsRequired() + .HasConstraintName("group_request_ibfk_2"); + + b.Navigation("Group"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IM_API.Models.LoginLog", b => + { + b.HasOne("IM_API.Models.User", "User") + .WithMany("LoginLogs") + .HasForeignKey("UserId") + .IsRequired() + .HasConstraintName("login_log_ibfk_1"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IM_API.Models.Message", b => + { + b.HasOne("IM_API.Models.User", "SenderNavigation") + .WithMany("Messages") + .HasForeignKey("Sender") + .IsRequired() + .HasConstraintName("messages_ibfk_1"); + + b.Navigation("SenderNavigation"); + }); + + modelBuilder.Entity("IM_API.Models.Notification", b => + { + b.HasOne("IM_API.Models.User", "User") + .WithMany("Notifications") + .HasForeignKey("UserId") + .IsRequired() + .HasConstraintName("notifications_ibfk_1"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("IM_API.Models.Permissionarole", b => + { + b.HasOne("IM_API.Models.Permission", "Permission") + .WithMany("Permissionaroles") + .HasForeignKey("PermissionId") + .IsRequired() + .HasConstraintName("permissionarole_ibfk_2"); + + b.HasOne("IM_API.Models.Role", "Role") + .WithMany("Permissionaroles") + .HasForeignKey("RoleId") + .IsRequired() + .HasConstraintName("permissionarole_ibfk_1"); + + b.Navigation("Permission"); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("IM_API.Models.Group", b => + { + b.Navigation("GroupInvites"); + + b.Navigation("GroupMembers"); + + b.Navigation("GroupRequests"); + }); + + modelBuilder.Entity("IM_API.Models.Message", b => + { + b.Navigation("Conversations"); + + b.Navigation("Files"); + }); + + modelBuilder.Entity("IM_API.Models.Permission", b => + { + b.Navigation("Permissionaroles"); + }); + + modelBuilder.Entity("IM_API.Models.Role", b => + { + b.Navigation("Admins"); + + b.Navigation("Permissionaroles"); + }); + + modelBuilder.Entity("IM_API.Models.User", b => + { + b.Navigation("Conversations"); + + b.Navigation("Devices"); + + b.Navigation("FriendFriendNavigations"); + + b.Navigation("FriendRequestRequestUserNavigations"); + + b.Navigation("FriendRequestResponseUserNavigations"); + + b.Navigation("FriendUsers"); + + b.Navigation("GroupInviteInviteUserNavigations"); + + b.Navigation("GroupInviteInvitedUserNavigations"); + + b.Navigation("GroupMembers"); + + b.Navigation("GroupRequests"); + + b.Navigation("Groups"); + + b.Navigation("LoginLogs"); + + b.Navigation("Messages"); + + b.Navigation("Notifications"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/IM_API/Migrations/20260306065353_uploadtask-url.cs b/backend/IM_API/Migrations/20260306065353_uploadtask-url.cs new file mode 100644 index 0000000..4f0f57e --- /dev/null +++ b/backend/IM_API/Migrations/20260306065353_uploadtask-url.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace IM_API.Migrations +{ + /// + public partial class uploadtaskurl : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Url", + table: "upload_tasks", + type: "longtext", + nullable: true, + collation: "utf8mb4_general_ci") + .Annotation("MySql:CharSet", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Url", + table: "upload_tasks"); + } + } +} diff --git a/backend/IM_API/Migrations/ImContextModelSnapshot.cs b/backend/IM_API/Migrations/ImContextModelSnapshot.cs index 6b71e71..53e6917 100644 --- a/backend/IM_API/Migrations/ImContextModelSnapshot.cs +++ b/backend/IM_API/Migrations/ImContextModelSnapshot.cs @@ -812,6 +812,9 @@ namespace IM_API.Migrations b.Property("TotalChunks") .HasColumnType("int"); + b.Property("Url") + .HasColumnType("longtext"); + b.HasKey("Id") .HasName("PRIMARY"); diff --git a/backend/IM_API/Models/Upload/UploadTask.cs b/backend/IM_API/Models/Upload/UploadTask.cs index 087b83b..0d44d32 100644 --- a/backend/IM_API/Models/Upload/UploadTask.cs +++ b/backend/IM_API/Models/Upload/UploadTask.cs @@ -20,5 +20,6 @@ public string? ProviderUploadId { get; set; } // OSS/S3 UploadId public DateTimeOffset CreatedAt { get; set; } + public string? Url { get; set; } } } diff --git a/backend/IM_API/Services/ConversationService.cs b/backend/IM_API/Services/ConversationService.cs index 52c6856..1cfbb43 100644 --- a/backend/IM_API/Services/ConversationService.cs +++ b/backend/IM_API/Services/ConversationService.cs @@ -34,8 +34,9 @@ namespace IM_API.Services var privateList = await (from c in _context.Conversations join f in _context.Friends on new { c.UserId, c.TargetId } equals new { UserId = f.UserId, TargetId = f.FriendId } + join u in _context.Users on c.TargetId equals u.Id where c.UserId == userId && c.ChatType == ChatType.PRIVATE - select new { c, f.Avatar, f.RemarkName }) + select new { c, u.Avatar, f.RemarkName }) .ToListAsync(); // 2. 获取群聊会话 diff --git a/backend/IM_API/Services/GroupService.cs b/backend/IM_API/Services/GroupService.cs index c78ebb9..4d09a8a 100644 --- a/backend/IM_API/Services/GroupService.cs +++ b/backend/IM_API/Services/GroupService.cs @@ -2,13 +2,16 @@ using AutoMapper.QueryableExtensions; using IM_API.Domain.Events; using IM_API.Dtos.Group; +using IM_API.Dtos.User; using IM_API.Exceptions; using IM_API.Interface.Services; using IM_API.Models; using IM_API.Tools; +using IM_API.VOs.Group; using MassTransit; using Microsoft.EntityFrameworkCore; using System; +using System.Linq; namespace IM_API.Services { @@ -247,5 +250,21 @@ namespace IM_API.Services }); return; } + + public async Task> 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(u); + _mapper.Map(m, user); + return user; + }).ToList(); + } } } diff --git a/backend/IM_API/Services/LocalStorageService.cs b/backend/IM_API/Services/LocalStorageService.cs index 3947f37..1d877ad 100644 --- a/backend/IM_API/Services/LocalStorageService.cs +++ b/backend/IM_API/Services/LocalStorageService.cs @@ -157,9 +157,14 @@ namespace IM_API.Services public async Task 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) return taskOld; + 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 @@ -184,6 +189,7 @@ namespace IM_API.Services FileSize = size, Id = Guid.NewGuid(), ObjectName = objectname, + Url = UrlTools.GetFullUrl(objectname, ProviderName, baseUrl), ProviderUploadId = Guid.NewGuid().ToString(), Status = UploadStatus.Completed, StorageProvider = ProviderName, diff --git a/backend/IM_API/VOs/Group/GroupMemberVo.cs b/backend/IM_API/VOs/Group/GroupMemberVo.cs new file mode 100644 index 0000000..ab0e7eb --- /dev/null +++ b/backend/IM_API/VOs/Group/GroupMemberVo.cs @@ -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; } + } +} diff --git a/frontend/pc/IM/src/main/index.js b/frontend/pc/IM/src/main/index.js index 00e3911..f0878fd 100644 --- a/frontend/pc/IM/src/main/index.js +++ b/frontend/pc/IM/src/main/index.js @@ -27,6 +27,7 @@ function createWindow() { mainWindow.show() }) + mainWindow.webContents.setWindowOpenHandler((details) => { shell.openExternal(details.url) return { action: 'deny' } @@ -75,9 +76,10 @@ app.whenReady().then(() => { // explicitly with Cmd + Q. app.on('window-all-closed', () => { if (process.platform !== 'darwin') { - app.quit() + //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. diff --git a/frontend/pc/IM/src/main/ipcHandlers/window.js b/frontend/pc/IM/src/main/ipcHandlers/window.js index 828cb62..2b71798 100644 --- a/frontend/pc/IM/src/main/ipcHandlers/window.js +++ b/frontend/pc/IM/src/main/ipcHandlers/window.js @@ -1,15 +1,68 @@ -import { ipcMain, BrowserWindow } from "electron"; +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() -export function registerWindowHandler(){ ipcMain.on('window-action', (event, action) => { - const win = BrowserWindow.fromWebContents(event.sender); - if (!win) return; + 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]?.(); + } + 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)) }) } diff --git a/frontend/pc/IM/src/main/trayHandler.js b/frontend/pc/IM/src/main/trayHandler.js index 5487394..6fb67f1 100644 --- a/frontend/pc/IM/src/main/trayHandler.js +++ b/frontend/pc/IM/src/main/trayHandler.js @@ -1,5 +1,6 @@ import { app, Tray, Menu, nativeImage } from 'electron' import path from 'path' +import { useRouter } from 'vue-router'; let tray = null; @@ -12,7 +13,10 @@ export function createTry(mainWindow){ const menu = Menu.buildFromTemplate([ {label: '退出', click: () => app.quit()}, - {label: '设置', click: () => alert('还没写')} + {label: '设置', click: () => { + mainWindow.webContents.send('router-ctl', '/settings') + mainWindow.show() + }} ]); tray.setToolTip('IM'); tray.setContextMenu(menu); diff --git a/frontend/pc/IM/src/preload/index.js b/frontend/pc/IM/src/preload/index.js index 737bed4..923143a 100644 --- a/frontend/pc/IM/src/preload/index.js +++ b/frontend/pc/IM/src/preload/index.js @@ -7,7 +7,10 @@ const api = { minimize: () => ipcRenderer.send('window-action', 'minimize'), maximize: () => ipcRenderer.send('window-action', 'maximize'), close: () => ipcRenderer.send('window-action', 'close'), - isMaximized: () => ipcRenderer.send('window-action', 'isMaximized') + 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) } } diff --git a/frontend/pc/IM/src/renderer/index.html b/frontend/pc/IM/src/renderer/index.html index 1c95fab..907a690 100644 --- a/frontend/pc/IM/src/renderer/index.html +++ b/frontend/pc/IM/src/renderer/index.html @@ -10,7 +10,8 @@ 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:;"> + font-src 'self' data:; + media-src 'self' blob:;"> diff --git a/frontend/pc/IM/src/renderer/src/App.vue b/frontend/pc/IM/src/renderer/src/App.vue index 103066d..34d8f73 100644 --- a/frontend/pc/IM/src/renderer/src/App.vue +++ b/frontend/pc/IM/src/renderer/src/App.vue @@ -10,14 +10,16 @@ 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(async () => { - const { useSignalRStore } = await import('./stores/signalr'); - const authStore = useAuthStore(); - const signalRStore = useSignalRStore(); - if(authStore.token){ - signalRStore.initSignalR(); +onMounted(() => { + const router = useRouter() + if(window.electron){ + window.electron.ipcRenderer.on('router-ctl', (e, path) => { + router.push(path) + }) } + }) diff --git a/frontend/pc/IM/src/renderer/src/handler/messageHandler.js b/frontend/pc/IM/src/renderer/src/handler/messageHandler.js index 484f53e..aa8fe29 100644 --- a/frontend/pc/IM/src/renderer/src/handler/messageHandler.js +++ b/frontend/pc/IM/src/renderer/src/handler/messageHandler.js @@ -2,7 +2,7 @@ import { useConversationStore } from "@/stores/conversation" export const messageHandler = (msg) => { const conversationStore = useConversationStore(); - const conversation = conversationStore.conversations.find(x => x.targetId == msg.senderId || x.targetId == msg.receiverId); + const conversation = conversationStore.conversations.find(x => (x.targetId == msg.senderId || x.targetId == msg.receiverId) && msg.chatType == x.chatType); conversation.lastMessage = msg.content; if (conversation.targetId == msg.receiverId) { conversation.unreadCount = 0; @@ -11,4 +11,4 @@ export const messageHandler = (msg) => { } conversation.dateTime = new Date().toISOString(); -} \ No newline at end of file +} diff --git a/frontend/pc/IM/src/renderer/src/router/index.js b/frontend/pc/IM/src/renderer/src/router/index.js index 0810e24..7544bdb 100644 --- a/frontend/pc/IM/src/renderer/src/router/index.js +++ b/frontend/pc/IM/src/renderer/src/router/index.js @@ -70,6 +70,9 @@ const routes = [ meta: { requiresAuth: true } }, { path: '/test', component: TestView }, + { + path: '/imgpre', component: () => import('@/components/electron/ImagePreview.vue') + } ] const router = createRouter({ diff --git a/frontend/pc/IM/src/renderer/src/services/userService.js b/frontend/pc/IM/src/renderer/src/services/userService.js new file mode 100644 index 0000000..2109793 --- /dev/null +++ b/frontend/pc/IM/src/renderer/src/services/userService.js @@ -0,0 +1,6 @@ +import { request } from "./api"; + +export const userService = { + updateUserInfo: (params) => request.post('/User/Profile', params), + getInfo: () => request.get('/user/me') +} diff --git a/frontend/pc/IM/src/renderer/src/stores/auth.js b/frontend/pc/IM/src/renderer/src/stores/auth.js index 7b70aca..9d67a4b 100644 --- a/frontend/pc/IM/src/renderer/src/stores/auth.js +++ b/frontend/pc/IM/src/renderer/src/stores/auth.js @@ -36,7 +36,7 @@ export const useAuthStore = defineStore('auth', () => { /** * 登录成功保存状态 - * @param {String} newToken 用户凭证 + * @param {String} newToken 用户凭证 * @param {*} user 用户信息 */ function setLoginInfo(newToken, newRefreshToken, user) { @@ -47,6 +47,11 @@ export const useAuthStore = defineStore('auth', () => { localStorage.setItem('user_token', newToken); localStorage.setItem('refresh_token', newRefreshToken) localStorage.setItem('user_info', JSON.stringify(user)) + }; + + function setUserInfo(user){ + userInfo.value = user; + localStorage.setItem('user_info',JSON.stringify(user)); } /** @@ -61,5 +66,14 @@ export const useAuthStore = defineStore('auth', () => { localStorage.removeItem('user_info') } - return { token, refreshToken, userInfo, isLoggedIn, isTokenExpired, setLoginInfo, logout }; + return { + token, + refreshToken, + userInfo, + isLoggedIn, + isTokenExpired, + setLoginInfo, + setUserInfo, + logout + } }) diff --git a/frontend/pc/IM/src/renderer/src/stores/contact.js b/frontend/pc/IM/src/renderer/src/stores/contact.js index 50c39ba..ca56b02 100644 --- a/frontend/pc/IM/src/renderer/src/stores/contact.js +++ b/frontend/pc/IM/src/renderer/src/stores/contact.js @@ -4,8 +4,8 @@ import { friendService } from "@/services/friend"; import { useMessage } from "@/components/messages/useAlert"; export const useContactStore = defineStore('contact', { - state: () => ({ - contacts: [], + state: () => ({ + contacts: [] }), actions: { @@ -42,4 +42,4 @@ export const useContactStore = defineStore('contact', { } } } -}) \ No newline at end of file +}) diff --git a/frontend/pc/IM/src/renderer/src/stores/settings.js b/frontend/pc/IM/src/renderer/src/stores/settings.js new file mode 100644 index 0000000..4c758f5 --- /dev/null +++ b/frontend/pc/IM/src/renderer/src/stores/settings.js @@ -0,0 +1,20 @@ +import { defineStore } from "pinia"; +import { reactive } from "vue"; + +export const useSettingsStore = defineStore('settings', { + state: () => ({ + notificationOptions: reactive(JSON.parse(localStorage.getItem('notification_options')) || {}), + generalOptions: reactive(JSON.parse(localStorage.getItem('generaN_options')) || {}) + }), + + actions: { + setNotificationOptions(options) { + this.notificationOptions = options; + localStorage.setItem('notification_options', options) + }, + setGeneralOptions(options){ + this.generalOptions = options; + localStorage.setItem('generaN_options', options) + } + } +}) diff --git a/frontend/pc/IM/src/renderer/src/stores/signalr.js b/frontend/pc/IM/src/renderer/src/stores/signalr.js index 3b4f66d..d6badf1 100644 --- a/frontend/pc/IM/src/renderer/src/stores/signalr.js +++ b/frontend/pc/IM/src/renderer/src/stores/signalr.js @@ -6,8 +6,6 @@ import { useChatStore } from "./chat"; import { authService } from "@/services/auth"; import { generateSessionId } from "@/utils/sessionIdTools"; import { messageHandler } from "@/handler/messageHandler"; -import { useBrowserNotification } from "@/services/useBrowserNotification"; -import { useConversationStore } from "./conversation"; import { SignalRMessageHandler } from "@/utils/signalr/SignalMessageHandler"; import { signalRConnectionEventHandler } from "@/utils/signalr/signalRConnectionEventHandler"; diff --git a/frontend/pc/IM/src/renderer/src/views/Main.vue b/frontend/pc/IM/src/renderer/src/views/Main.vue index 8bfe2ea..cfc15a3 100644 --- a/frontend/pc/IM/src/renderer/src/views/Main.vue +++ b/frontend/pc/IM/src/renderer/src/views/Main.vue @@ -20,7 +20,7 @@ diff --git a/frontend/pc/IM/src/renderer/src/views/messages/messageContent/MessageContent.vue b/frontend/pc/IM/src/renderer/src/views/messages/messageContent/MessageContent.vue index 192f833..63d17fc 100644 --- a/frontend/pc/IM/src/renderer/src/views/messages/messageContent/MessageContent.vue +++ b/frontend/pc/IM/src/renderer/src/views/messages/messageContent/MessageContent.vue @@ -4,120 +4,108 @@
{{ conversationInfo?.targetName || '未选择会话' }}
- - - + + +
-
+
- - - - - -
+ + + + + +
-
-
- 正在播放视频 - -
+
+
+ 正在播放视频 + +
-
- -
-
+
+ +
+
-
-
-
- -
- - -
-
{{ m.senderName }}
-
-
{{ m.content }}
-
{{ m.content }}
-
- 图片消息 - -
-
-
- - - - - {{ m.progress || 0 }}% -
-
- - -
-
- - -
- -
+ + + +
+ + +
+
{{ + m.senderName }}
+
+
{{ m.content }}
+
{{ m.content }}
+
+ 图片消息 + +
+
+
+ + + + + {{ m.progress || 0 }}% +
+
+ + +
+
+ + +
+ + +
- {{ formatDate(m.timeStamp) }} + {{ formatDate(m.timeStamp) }} +
-
-
-
- - - -
- -
- -
-
- +
+
+ + + +
+ +
+ +
+
+
@@ -150,10 +138,10 @@ import { isElectron } from '../../../utils/electronHelper'; const props = defineProps({ - id:{ - type: String, - required:true - } + id: { + type: String, + required: true + } }) const infoSideBarShow = ref(false); @@ -162,7 +150,7 @@ const chatStore = useChatStore(); const signalRStore = useSignalRStore(); const conversationStore = useConversationStore(); const message = useMessage(); -const {sendMessage, sendFileMessage, sendTextMessage} = useSendMessageHandler(); +const { sendMessage, sendFileMessage, sendTextMessage } = useSendMessageHandler(); const input = ref(''); // 输入框内容 const historyRef = ref(null); // 绑定 DOM 用于滚动 @@ -186,10 +174,10 @@ const videoUrl = ref(null); const videoOpen = ref(false) const infoShowHandler = () => { - if(infoSideBarShow.value) - infoSideBarShow.value = false; -else -infoSideBarShow.value =true; + if (infoSideBarShow.value) + infoSideBarShow.value = false; + else + infoSideBarShow.value = true; } @@ -221,22 +209,31 @@ const getImageStyle = (content) => { const imagePreview = (m) => { const imageList = chatStore.messages - .filter(x => x.type == 'Image') - ; + .filter(x => x.type == 'Image') + ; const index = imageList.indexOf(m); - previewImages({ - imgList: imageList.map(m => m.content.url), - nowImgIndex: index - }); + if (isElectron()) { + const safeData = JSON.parse(JSON.stringify( { + imageList, + index + })); + window.api.window.newWindow('imgpre',safeData); + } else { + previewImages({ + imgList: imageList.map(m => m.content.url), + nowImgIndex: index + }); + } + } const startRecord = async () => { - try{ - const stream = await navigator.mediaDevices.getUserMedia({audio:true}); + try { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); mediaRecorder = new MediaRecorder(stream); audioChunks = []; mediaRecorder.ondataavailable = e => { - if(e.data.size > 0) audioChunks.push(e.data); + if (e.data.size > 0) audioChunks.push(e.data); } // 录音结束后的处理 mediaRecorder.onstop = () => { @@ -251,13 +248,13 @@ const startRecord = async () => { }); handleFile([audioFile]).finally(() => { // 停止所有轨道以关闭麦克风图标 - stream.getTracks().forEach(track => track.stop()); + stream.getTracks().forEach(track => track.stop()); }); }; mediaRecorder.start(); isRecord.value = true; - }catch(e){ + } catch (e) { console.log(e) message.error('无法获取麦克风权限!'); } @@ -330,15 +327,15 @@ const handleRightClick = (e, m) => { }, { label: '多选', - action: () => {} + action: () => { } }, { label: '翻译', - action: () => {} + action: () => { } }, { label: '引用', - action: () => {} + action: () => { } }, { label: '删除', @@ -356,7 +353,7 @@ watch( conversationStore.conversations.find(x => x.id == conversationInfo.value.id).unreadCount = 0; signalRStore.clearUnreadCount(conversationInfo.value.id); }, - {deep: true} + { deep: true } ); // 自动滚动到底部 @@ -387,30 +384,30 @@ async function handleFile(files) { let info = {}; let localUrl = null; let img = null; - switch(getMessageType(file.type)){ + switch (getMessageType(file.type)) { case FILE_TYPE.Image: localUrl = URL.createObjectURL(file); img = await loadImage(localUrl); info = new ImageInfo(file.type, '[图片]', img.width, img.height, await generateImageThumbnailBlob(await loadImage(localUrl), 200)); break; - case FILE_TYPE.Video: { - const imgBlob = await getVideoThumbnailBlob(file); - localUrl = URL.createObjectURL(imgBlob); - img = await loadImage(localUrl); + case FILE_TYPE.Video: { + const imgBlob = await getVideoThumbnailBlob(file); + localUrl = URL.createObjectURL(imgBlob); + img = await loadImage(localUrl); - info = new VideoInfo(file.type,'[视频]', img.width, img.height, imgBlob, await getVideoDuration(file)); - break; - } - case FILE_TYPE.Voice: { - localUrl = URL.createObjectURL(file); - info = new VoiceInfo(file.type,'[语音消息]', await getVideoDuration(file)); - break; - } + info = new VideoInfo(file.type, '[视频]', img.width, img.height, imgBlob, await getVideoDuration(file)); + break; + } + case FILE_TYPE.Voice: { + localUrl = URL.createObjectURL(file); + info = new VoiceInfo(file.type, '[语音消息]', await getVideoDuration(file)); + break; + } } - await sendFileMessage(file, conversationInfo,info,localUrl); + await sendFileMessage(file, conversationInfo, info, localUrl); } function toggleEmoji() { @@ -422,20 +419,20 @@ async function loadConversation(conversationId) { const res = await messageService.getConversationById(conversationId); conversationInfo.value = res.data; */ - if(conversationStore.conversations.length == 0){ - await conversationStore.loadUserConversations(); - } - conversationInfo.value = conversationStore.conversations.find(x => x.id == Number(conversationId)); + if (conversationStore.conversations.length == 0) { + await conversationStore.loadUserConversations(); + } + conversationInfo.value = conversationStore.conversations.find(x => x.id == Number(conversationId)); } const initChat = async (newId) => { await loadConversation(newId); - if(conversationInfo.value){ + if (conversationInfo.value) { const sessionid = generateSessionId( - conversationInfo.value.userId, conversationInfo.value.targetId, conversationInfo.value.chatType == MESSAGE_TYPE.GROUP) - await chatStore.swtichSession(sessionid,newId); - isFinished.value = false; - scrollToBottom(); + conversationInfo.value.userId, conversationInfo.value.targetId, conversationInfo.value.chatType == MESSAGE_TYPE.GROUP) + await chatStore.swtichSession(sessionid, newId); + isFinished.value = false; + scrollToBottom(); } } @@ -446,7 +443,7 @@ watch( () => { // 如果没有 ID,或者 Store 还没加载,返回一个安全的默认值 if (!conversationStore.conversations.length) { - return [props.id,null]; + return [props.id, null]; } // 尝试找到当前会话 @@ -465,9 +462,9 @@ watch( // 场景 A:路由切换 (ID 变了) // 这里的逻辑是:只要切了 ID,先加载本地的给用户看 //if (newId !== oldId) { - // 注意:这里只是加载本地缓存,不负责去服务器拉新 - // 真正的“拉新”逻辑交给下面的 isInited 判断 - await initChat(newId); + // 注意:这里只是加载本地缓存,不负责去服务器拉新 + // 真正的“拉新”逻辑交给下面的 isInited 判断 + await initChat(newId); //} // 场景 B:检测到需要补洞 (isInited 变为 false) @@ -482,23 +479,23 @@ watch( const msgList = await chatStore.fetchNewMsgFromServier(newId); const session = conversationStore.conversations.find(x => x.id == Number(newId)); - if(msgList && msgList.length > 0){ - const minSequenceId = Math.min(...msgList.map(m => m.sequenceId)); - const locaMaxSequenceId = chatStore.maxSequenceId; - if(locaMaxSequenceId < (minSequenceId - 1)){ - chatStore.messages = [];; - } - await chatStore.pushAndSortMessagesAsync(msgList, generateSessionId(session.userId, session.targetId, session.chatType == MESSAGE_TYPE.GROUP), true); + if (msgList && msgList.length > 0) { + const minSequenceId = Math.min(...msgList.map(m => m.sequenceId)); + const locaMaxSequenceId = chatStore.maxSequenceId; + if (locaMaxSequenceId < (minSequenceId - 1)) { + chatStore.messages = [];; } - // 3. 如果有新消息,存入 Store + await chatStore.pushAndSortMessagesAsync(msgList, generateSessionId(session.userId, session.targetId, session.chatType == MESSAGE_TYPE.GROUP), true); + } + // 3. 如果有新消息,存入 Store // 4. 关键步骤:手动把状态回正! // 必须重新 find 一次对象,确保引用是对的 if (session) { - session.isInitialized = true; - console.log(`[同步完成] 会话 ${newId} 状态已重置为 true`); + session.isInitialized = true; + console.log(`[同步完成] 会话 ${newId} 状态已重置为 true`); } } } catch (err) { @@ -509,14 +506,14 @@ watch( ); const initObs = async () => { -await nextTick(); + await nextTick(); observer = new IntersectionObserver((entries) => { const entry = entries[0]; // 只有当 Loading 组件进入视口,且满足触发条件 if (entry.isIntersecting) { - loadHistoryMsg(); - } + loadHistoryMsg(); + } }, { root: historyRef.value, // 指定监听容器 threshold: 0.1 // 露出 10% 就算触发 @@ -527,7 +524,7 @@ await nextTick(); // 如果 sentinel 是组件,需要拿它底下的 $el observer.observe(loadingRef.value.$el || loadingRef.value); } -// 3. 【主动侦测逻辑】 + // 3. 【主动侦测逻辑】 // 如果当前没有在加载,且还没有结束,且内容还没有撑开容器 const el = historyRef.value; if (el && !isLoading.value && !isFinished.value) { @@ -555,7 +552,8 @@ onUnmounted(() => { .chat-panel { display: flex; flex-direction: column; - height: 100%; /* 确保占满父容器 */ + height: 100%; + /* 确保占满父容器 */ background: #f5f5f5; } @@ -584,6 +582,7 @@ onUnmounted(() => { border-bottom: 1px solid #e0e0e0; -webkit-app-region: drag; } + /* 遮罩层:全屏、黑色半透明、固定定位 */ .video-overlay { position: fixed; @@ -595,7 +594,8 @@ onUnmounted(() => { display: flex; align-items: center; justify-content: center; - z-index: 9999; /* 确保在最顶层 */ + z-index: 9999; + /* 确保在最顶层 */ } /* 播放器弹窗主体 */ @@ -606,7 +606,7 @@ onUnmounted(() => { background: #000; border-radius: 8px; overflow: hidden; - box-shadow: 0 10px 30px rgba(0,0,0,0.5); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); } /* 顶部状态栏(包含关闭按钮) */ @@ -637,15 +637,19 @@ onUnmounted(() => { .player-wrapper { width: 100%; - aspect-ratio: 16 / 9; /* 锁定 16:9 比例 */ + aspect-ratio: 16 / 9; + /* 锁定 16:9 比例 */ background: #000; } /* 进场动画 */ -.fade-enter-active, .fade-leave-active { +.fade-enter-active, +.fade-leave-active { transition: opacity 0.3s ease; } -.fade-enter-from, .fade-leave-to { + +.fade-enter-from, +.fade-leave-to { opacity: 0; } @@ -679,15 +683,23 @@ onUnmounted(() => { } @keyframes pulse { - 0% { transform: scale(1); } - 50% { transform: scale(1.1); } - 100% { transform: scale(1); } + 0% { + transform: scale(1); + } + + 50% { + transform: scale(1.1); + } + + 100% { + transform: scale(1); + } } .image-msg { - max-width: 100px; - max-height: 200px; - object-fit: cover; + max-width: 100px; + max-height: 200px; + object-fit: cover; } /* 容器:由计算属性决定宽高 */ @@ -705,14 +717,16 @@ onUnmounted(() => { .image-msg-content { width: 100%; height: 100%; - object-fit: cover; /* 关键:裁剪而非拉伸 */ + object-fit: cover; + /* 关键:裁剪而非拉伸 */ display: block; } /* 覆盖层 */ .image-overlay { position: absolute; - inset: 0; /* 铺满父容器 */ + inset: 0; + /* 铺满父容器 */ background: rgba(0, 0, 0, 0.3); display: flex; flex-direction: column; @@ -727,21 +741,27 @@ onUnmounted(() => { width: 40px; height: 40px; } + .circular-progress svg { transform: rotate(-90deg); } + .circular-progress circle { fill: none; stroke-width: 3; } + .circular-progress .bg { stroke: rgba(255, 255, 255, 0.3); } + .circular-progress .bar { stroke: #ffffff; - stroke-dasharray: 100; /* 这里的 100 对应周长 */ + stroke-dasharray: 100; + /* 这里的 100 对应周长 */ transition: stroke-dashoffset 0.2s; } + .circular-progress .pct { position: absolute; inset: 0; @@ -750,6 +770,7 @@ onUnmounted(() => { justify-content: center; font-size: 10px; } + .error-icon { color: #ff4d4f; } @@ -789,15 +810,18 @@ onUnmounted(() => { from { transform: rotate(0deg); } + to { transform: rotate(360deg); } } .loaderIcon { - display: inline-block; /* 必须是 block 或 inline-block 才能旋转 */ + display: inline-block; + /* 必须是 block 或 inline-block 才能旋转 */ line-height: 0; - animation: spin 1s linear infinite; /* 1秒转一圈,线性速度,无限循环 */ + animation: spin 1s linear infinite; + /* 1秒转一圈,线性速度,无限循环 */ } .status { @@ -809,6 +833,7 @@ onUnmounted(() => { .msg.mine { flex-direction: row-reverse; } + .msg.mine .msg-content { display: flex; justify-content: flex-end; diff --git a/frontend/pc/IM/src/renderer/src/views/settings/AccountSecurity.vue b/frontend/pc/IM/src/renderer/src/views/settings/AccountSecurity.vue new file mode 100644 index 0000000..3b29960 --- /dev/null +++ b/frontend/pc/IM/src/renderer/src/views/settings/AccountSecurity.vue @@ -0,0 +1,237 @@ + + + + + diff --git a/frontend/pc/IM/src/renderer/src/views/settings/GeneralSettings.vue b/frontend/pc/IM/src/renderer/src/views/settings/GeneralSettings.vue new file mode 100644 index 0000000..b39b86b --- /dev/null +++ b/frontend/pc/IM/src/renderer/src/views/settings/GeneralSettings.vue @@ -0,0 +1,230 @@ + + + + + diff --git a/frontend/pc/IM/src/renderer/src/views/settings/NotificationEdit.vue b/frontend/pc/IM/src/renderer/src/views/settings/NotificationEdit.vue new file mode 100644 index 0000000..e2dff8a --- /dev/null +++ b/frontend/pc/IM/src/renderer/src/views/settings/NotificationEdit.vue @@ -0,0 +1,204 @@ + + + + + diff --git a/frontend/pc/IM/src/renderer/src/views/settings/SettingContent.vue b/frontend/pc/IM/src/renderer/src/views/settings/SettingContent.vue new file mode 100644 index 0000000..cdfaeee --- /dev/null +++ b/frontend/pc/IM/src/renderer/src/views/settings/SettingContent.vue @@ -0,0 +1,151 @@ + + + + + + diff --git a/frontend/pc/IM/src/renderer/src/views/settings/SettingMenu.vue b/frontend/pc/IM/src/renderer/src/views/settings/SettingMenu.vue index 611d33f..1da0780 100644 --- a/frontend/pc/IM/src/renderer/src/views/settings/SettingMenu.vue +++ b/frontend/pc/IM/src/renderer/src/views/settings/SettingMenu.vue @@ -3,8 +3,8 @@ + -
-
-

{{ currentMenuName }}

-

配置您的个性化通讯偏好

-
- -
-
-
-
-
{{ key }}
-
开启后系统将实时同步您的通知偏好
-
- -
-
- -
- ⚙️ -

{{ currentMenuName }} 功能开发中...

-
-
- -
- - -
-
+ +