diff --git a/backend/IMTest/bin/Debug/net8.0/IMTest.dll b/backend/IMTest/bin/Debug/net8.0/IMTest.dll index 044ea8a..7676d44 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 a3d1b90..39fed94 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.dll b/backend/IMTest/bin/Debug/net8.0/IM_API.dll index 317926a..4b6dd57 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 598449d..4d58f5d 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 3ee5a3b..0d24bfb 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/obj/Debug/net8.0/IMTest.AssemblyInfo.cs b/backend/IMTest/obj/Debug/net8.0/IMTest.AssemblyInfo.cs index 29b15fd..ad92b28 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+2ecaa28091b41de707825db3628d380b62fa727f")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+8d952578de8d3725b4fcad34056ce8cc01e73665")] [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 d02a3dc..b1aa42c 100644 --- a/backend/IMTest/obj/Debug/net8.0/IMTest.AssemblyInfoInputs.cache +++ b/backend/IMTest/obj/Debug/net8.0/IMTest.AssemblyInfoInputs.cache @@ -1 +1 @@ -ed4980dfc7aff253176b260ed9015f9a80b52e92cbf3095eff3ed06865ea6e0d +894ce5acc8690cb041ab936657887727ab47ceff5777afb7e84a8a31d3de838c 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 2932c7a..0507af7 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.dll b/backend/IMTest/obj/Debug/net8.0/IMTest.dll index 044ea8a..7676d44 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 a3d1b90..39fed94 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 2793b43..46238b8 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 2793b43..46238b8 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/.idea/.idea.IM_API/.idea/.gitignore b/backend/IM_API/.idea/.idea.IM_API/.idea/.gitignore new file mode 100644 index 0000000..01c334b --- /dev/null +++ b/backend/IM_API/.idea/.idea.IM_API/.idea/.gitignore @@ -0,0 +1,15 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/modules.xml +/projectSettingsUpdater.xml +/.idea.IM_API.iml +/contentModel.xml +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/backend/IM_API/.idea/.idea.IM_API/.idea/encodings.xml b/backend/IM_API/.idea/.idea.IM_API/.idea/encodings.xml new file mode 100644 index 0000000..df87cf9 --- /dev/null +++ b/backend/IM_API/.idea/.idea.IM_API/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/backend/IM_API/.idea/.idea.IM_API/.idea/indexLayout.xml b/backend/IM_API/.idea/.idea.IM_API/.idea/indexLayout.xml new file mode 100644 index 0000000..7b08163 --- /dev/null +++ b/backend/IM_API/.idea/.idea.IM_API/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/backend/IM_API/.idea/.idea.IM_API/.idea/vcs.xml b/backend/IM_API/.idea/.idea.IM_API/.idea/vcs.xml new file mode 100644 index 0000000..b2bdec2 --- /dev/null +++ b/backend/IM_API/.idea/.idea.IM_API/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/backend/IM_API/Application/EventHandlers/FriendAddHandler/FriendAddSignalRHandler.cs b/backend/IM_API/Application/EventHandlers/FriendAddHandler/FriendAddSignalRHandler.cs index e692035..021f3c3 100644 --- a/backend/IM_API/Application/EventHandlers/FriendAddHandler/FriendAddSignalRHandler.cs +++ b/backend/IM_API/Application/EventHandlers/FriendAddHandler/FriendAddSignalRHandler.cs @@ -22,7 +22,7 @@ namespace IM_API.Application.EventHandlers.FriendAddHandler var usersList = new List { @event.RequestUserId.ToString(), @event.ResponseUserId.ToString() }; - var res = new HubResponse("Event", new MessageBaseDto() + var res = new HubResponse(HubResponseType.FriendAccepted, new MessageBaseDto() { ChatType = ChatType.PRIVATE, Content = "您有新的好友关系已添加", diff --git a/backend/IM_API/Application/EventHandlers/GroupInviteActionUpdateHandler/RequestDbHandler.cs b/backend/IM_API/Application/EventHandlers/GroupInviteActionUpdateHandler/RequestDbHandler.cs index c5c7620..eec57dd 100644 --- a/backend/IM_API/Application/EventHandlers/GroupInviteActionUpdateHandler/RequestDbHandler.cs +++ b/backend/IM_API/Application/EventHandlers/GroupInviteActionUpdateHandler/RequestDbHandler.cs @@ -15,7 +15,7 @@ namespace IM_API.Application.EventHandlers.GroupInviteActionUpdateHandler public async Task Consume(ConsumeContext context) { var @event = context.Message; - if(@event.Action == Models.GroupInviteState.Passed) + if(@event.Action == Models.GroupRequestState.TargetPassed) { await _groupService.MakeGroupRequestAsync(@event.UserId, @event.InviteUserId,@event.GroupId); } diff --git a/backend/IM_API/Configs/MapperConfig.cs b/backend/IM_API/Configs/MapperConfig.cs index 431657e..5fb327a 100644 --- a/backend/IM_API/Configs/MapperConfig.cs +++ b/backend/IM_API/Configs/MapperConfig.cs @@ -29,6 +29,7 @@ namespace IM_API.Configs CreateMap() .ForMember(dest => dest.Username,opt => opt.MapFrom(src => src.Username)) .ForMember(dest => dest.Password,opt => opt.MapFrom(src => src.Password)) + .ForMember(dest => dest.Email, opt => opt.MapFrom(src => src.Email)) .ForMember(dest => dest.Avatar,opt => opt.MapFrom(src => "https://ts1.tc.mm.bing.net/th/id/OIP-C.dl0WpkTP6E2J4FnhDC_jHwAAAA?rs=1&pid=ImgDetMain&o=7&rm=3")) .ForMember(dest => dest.StatusEnum,opt => opt.MapFrom(src => UserStatus.Normal)) .ForMember(dest => dest.OnlineStatusEnum,opt => opt.MapFrom(src => UserOnlineStatus.Offline)) @@ -215,6 +216,34 @@ namespace IM_API.Configs CreateMap() .ForMember(dest => dest.Created, opt => opt.MapFrom(src => src.Created)) .ForMember(dest => dest.Role, opt => opt.MapFrom(src => src.RoleEnum)); + + //群更新模型 + + CreateMap() + .ForMember(dest => dest.Avatar, opt => opt.MapFrom(src => src.Avatar)) + .ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.GroupName)) + .ForMember(dest => dest.Announcement, opt => opt.MapFrom(src => src.Description)) + .ForAllMembers(dest => dest.Ignore()); + + CreateMap() + .ForMember(dest => dest.Auhority, opt => opt.MapFrom(src => src.AuhorityEnum)) + .ForMember(dest => dest.AllMembersBanned, opt => opt.MapFrom(src => src.AllMembersBannedEnum)) + .ForMember(dest => dest.Status, opt => opt.MapFrom(src => src.StatusEnum)) + ; + + //群通知模型转换 + CreateMap() + .ForMember(dest => dest.UserId, opt => opt.MapFrom(src => src.UserId)) + .ForMember(dest => dest.GroupId, opt => opt.MapFrom(src => src.GroupId)) + .ForMember(dest => dest.InviteUser, opt => opt.MapFrom(src => src.InviteUserId)) + .ForMember(dest => dest.Status, opt => opt.MapFrom(src => src.StateEnum)) + .ForMember(dest => dest.Description, opt => opt.MapFrom(src => src.Description)) + .ForMember(dest => dest.RequestId, opt => opt.MapFrom(src => src.Id)) + //.ForAllMembers(opt => opt.Ignore()) + ; + + + } } } diff --git a/backend/IM_API/Controllers/GroupController.cs b/backend/IM_API/Controllers/GroupController.cs index 6dfdf8f..12bc59e 100644 --- a/backend/IM_API/Controllers/GroupController.cs +++ b/backend/IM_API/Controllers/GroupController.cs @@ -77,5 +77,33 @@ namespace IM_API.Controllers var members = await _groupService.GetGroupMembers(int.Parse(useridStr), groupId); return Ok(new BaseResponse>(members)); } + + [HttpPost] + [ProducesResponseType(typeof(BaseResponse), StatusCodes.Status200OK)] + + public async Task UpdateGroup([FromQuery]int groupId, [FromBody]GroupUpdateDto dto) + { + var useridStr = User.FindFirstValue(ClaimTypes.NameIdentifier); + + var groupinfo = await _groupService.UpdateGroupInfoAsync(int.Parse(useridStr), groupId, dto); + return Ok(new BaseResponse(groupinfo)); + } + + [HttpGet] + [ProducesResponseType(typeof(BaseResponse), StatusCodes.Status200OK)] + public async Task GetGroupInfo([FromQuery]int groupId) + { + var group = await _groupService.GetGroupInfoAsync(groupId); + return Ok(new BaseResponse(group)); + } + + [HttpGet] + [ProducesResponseType(typeof(BaseResponse>), StatusCodes.Status200OK)] + public async Task GetGroupNotification([FromQuery]int groupId) + { + string userIdStr = User.FindFirstValue(ClaimTypes.NameIdentifier)!; + var data = await _groupService.GetGroupNotificationAsync(int.Parse(userIdStr)); + return Ok(new BaseResponse>(data)); + } } } diff --git a/backend/IM_API/Domain/Events/GroupInviteActionUpdateEvent.cs b/backend/IM_API/Domain/Events/GroupInviteActionUpdateEvent.cs index 3368dfe..2e0de1a 100644 --- a/backend/IM_API/Domain/Events/GroupInviteActionUpdateEvent.cs +++ b/backend/IM_API/Domain/Events/GroupInviteActionUpdateEvent.cs @@ -9,6 +9,6 @@ namespace IM_API.Domain.Events public int InviteUserId { get; set; } public int InviteId { get; set; } public int GroupId { get; set; } - public GroupInviteState Action { get; set; } + public GroupRequestState Action { get; set; } } } diff --git a/backend/IM_API/Dtos/Auth/RegisterRequestDto.cs b/backend/IM_API/Dtos/Auth/RegisterRequestDto.cs index c32dd87..e9d0efa 100644 --- a/backend/IM_API/Dtos/Auth/RegisterRequestDto.cs +++ b/backend/IM_API/Dtos/Auth/RegisterRequestDto.cs @@ -4,6 +4,8 @@ namespace IM_API.Dtos.Auth { public class RegisterRequestDto { + [EmailAddress(ErrorMessage = "邮箱格式错误")] + public string Email { get; set; } [Required(ErrorMessage = "用户名不能为空")] [MaxLength(20, ErrorMessage = "用户名不能超过20字符")] [RegularExpression(@"^[A-Za-z0-9]+$", ErrorMessage = "用户名只能为英文或数字")] diff --git a/backend/IM_API/Dtos/Group/GroupUpdateDto.cs b/backend/IM_API/Dtos/Group/GroupUpdateDto.cs new file mode 100644 index 0000000..27de73f --- /dev/null +++ b/backend/IM_API/Dtos/Group/GroupUpdateDto.cs @@ -0,0 +1,10 @@ +namespace IM_API.Dtos.Group +{ + public class GroupUpdateDto + { + public string? GroupName { get; set; } + public string? Description { get; set; } + public string? Avatar { get; set; } + + } +} diff --git a/backend/IM_API/Dtos/Group/HandleGroupInviteDto.cs b/backend/IM_API/Dtos/Group/HandleGroupInviteDto.cs index 038a4bd..7f26982 100644 --- a/backend/IM_API/Dtos/Group/HandleGroupInviteDto.cs +++ b/backend/IM_API/Dtos/Group/HandleGroupInviteDto.cs @@ -5,6 +5,6 @@ namespace IM_API.Dtos.Group public class HandleGroupInviteDto { public int InviteId { get; set; } - public GroupInviteState Action { get; set; } + public GroupRequestState Action { get; set; } } } diff --git a/backend/IM_API/Dtos/HubResponse.cs b/backend/IM_API/Dtos/HubResponse.cs index 18c2c0e..c0dc9bb 100644 --- a/backend/IM_API/Dtos/HubResponse.cs +++ b/backend/IM_API/Dtos/HubResponse.cs @@ -39,11 +39,44 @@ namespace IM_API.Dtos Message = codeDefine.Message; Data = data; } + public HubResponse(HubResponseType type, T? data) + { + Code = CodeDefine.SUCCESS.Code; + Method = "Event"; + Type = type; + Data = data; + } } public enum HubResponseType { - ChatMsg = 1, // 聊天内容 - SystemNotice = 2, // 系统通知(如:申请好友成功) - ActionStatus = 3 // 状态变更(如:对方正在输入、已读回执) + // --- 基础聊天 (10-19) --- + ChatMsg = 1, // 普通消息 + + // --- 系统与通知 (20-29) --- + SystemNotice = 2, // 通用系统通知 + FriendRequest = 21, // 好友申请消息 + FriendAccepted = 22, // 好友通过通知 + UserInLine = 23, // 好友上线/下线通知 + + // --- 状态变更与交互 (30-39) --- + ActionStatus = 3, // 通用状态(正在输入等) + MsgReadReceipt = 31, // 已读回执 + MsgRevoke = 32, // 消息撤回 + MsgEdit = 33, // 消息二次编辑更新 + + // --- 群组管理 (40-49) --- + GroupInvited = 41, // 被邀请入群 + GroupMemberUpdate = 42, // 群成员变动(进群/退群) + GroupAnnouncement = 43, // 群公告更新 + GroupDismissed = 44, // 群组解散 + + // --- 实时通信控制 (50-59) --- + RTC_CallRequest = 51, // 音视频通话邀请 + RTC_CallHandled = 52, // 音视频接听/挂断/取消状态 + + // --- 异常与安全 (90-99) --- + ErrorInternal = 91, // 服务器内部错误 + TokenExpired = 92, // 登录过期,强制下线 + KickedOut = 93 // 被挤下线(异地登录) } } diff --git a/backend/IM_API/IM_API.sln.DotSettings.user b/backend/IM_API/IM_API.sln.DotSettings.user new file mode 100644 index 0000000..3802d92 --- /dev/null +++ b/backend/IM_API/IM_API.sln.DotSettings.user @@ -0,0 +1,4 @@ + + True + /home/nanxun/dotnet/dotnet + /home/nanxun/dotnet/sdk/10.0.201/MSBuild.dll \ No newline at end of file diff --git a/backend/IM_API/Interface/Services/IGroupService.cs b/backend/IM_API/Interface/Services/IGroupService.cs index fa7e4e8..67bfc8d 100644 --- a/backend/IM_API/Interface/Services/IGroupService.cs +++ b/backend/IM_API/Interface/Services/IGroupService.cs @@ -53,5 +53,15 @@ namespace IM_API.Interface.Services Task MakeGroupRequestAsync(int userId,int? adminUserId,int groupId); Task MakeGroupMemberAsync(int userId, int groupId, GroupMemberRole? role); Task> GetGroupMembers(int userId, int groupId); + Task UpdateGroupInfoAsync(int userId, int groupId, GroupUpdateDto updateDto); + + Task GetGroupInfoAsync(int groupId); + + /// + /// 获取群聊通知 + /// + /// + /// + Task> GetGroupNotificationAsync(int userId); } } diff --git a/backend/IM_API/Migrations/20260309065303_update-group-announcement.Designer.cs b/backend/IM_API/Migrations/20260309065303_update-group-announcement.Designer.cs new file mode 100644 index 0000000..27fcb46 --- /dev/null +++ b/backend/IM_API/Migrations/20260309065303_update-group-announcement.Designer.cs @@ -0,0 +1,1174 @@ +// +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("20260309065303_update-group-announcement")] + partial class updategroupannouncement + { + /// + 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") + .IsRequired() + .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/20260309065303_update-group-announcement.cs b/backend/IM_API/Migrations/20260309065303_update-group-announcement.cs new file mode 100644 index 0000000..44cea10 --- /dev/null +++ b/backend/IM_API/Migrations/20260309065303_update-group-announcement.cs @@ -0,0 +1,54 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace IM_API.Migrations +{ + /// + public partial class updategroupannouncement : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.UpdateData( + table: "groups", + keyColumn: "Announcement", + keyValue: null, + column: "Announcement", + value: ""); + + migrationBuilder.AlterColumn( + name: "Announcement", + table: "groups", + type: "text", + nullable: false, + comment: "群公告", + collation: "utf8mb4_general_ci", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true, + oldComment: "群公告") + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("Relational:Collation", "utf8mb4_general_ci"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Announcement", + table: "groups", + type: "text", + nullable: true, + comment: "群公告", + collation: "utf8mb4_general_ci", + oldClrType: typeof(string), + oldType: "text", + oldComment: "群公告") + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("Relational:Collation", "utf8mb4_general_ci"); + } + } +} diff --git a/backend/IM_API/Migrations/20260312094604_group-invite-request-merge.Designer.cs b/backend/IM_API/Migrations/20260312094604_group-invite-request-merge.Designer.cs new file mode 100644 index 0000000..e2229a8 --- /dev/null +++ b/backend/IM_API/Migrations/20260312094604_group-invite-request-merge.Designer.cs @@ -0,0 +1,1101 @@ +// +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("20260312094604_group-invite-request-merge")] + partial class groupinviterequestmerge + { + /// + 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") + .IsRequired() + .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.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("InviteUserId") + .HasColumnType("int"); + + 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"); + + 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.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("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("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/20260312094604_group-invite-request-merge.cs b/backend/IM_API/Migrations/20260312094604_group-invite-request-merge.cs new file mode 100644 index 0000000..1fe1ed6 --- /dev/null +++ b/backend/IM_API/Migrations/20260312094604_group-invite-request-merge.cs @@ -0,0 +1,87 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace IM_API.Migrations +{ + /// + public partial class groupinviterequestmerge : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "group_invite"); + + migrationBuilder.AddColumn( + name: "InviteUserId", + table: "group_request", + type: "int", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "InviteUserId", + table: "group_request"); + + migrationBuilder.RenameIndex( + name: "GroupId", + table: "group_request", + newName: "GroupId1"); + + migrationBuilder.CreateTable( + name: "group_invite", + columns: table => new + { + ID = table.Column(type: "int(11)", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + GroupId = table.Column(type: "int(11)", nullable: false, comment: "群聊编号"), + InvitedUser = table.Column(type: "int(11)", nullable: true, comment: "被邀请用户"), + InviteUser = table.Column(type: "int(11)", nullable: true, comment: "邀请用户"), + Created = table.Column(type: "datetime", nullable: true, comment: "创建时间"), + State = table.Column(type: "tinyint(4)", nullable: true, comment: "当前状态(0:待被邀请人同意\r\n1:被邀请人已同意)") + }, + constraints: table => + { + table.PrimaryKey("PRIMARY", x => x.ID); + table.ForeignKey( + name: "group_invite_ibfk_1", + column: x => x.InviteUser, + principalTable: "users", + principalColumn: "ID"); + table.ForeignKey( + name: "group_invite_ibfk_2", + column: x => x.GroupId, + principalTable: "groups", + principalColumn: "ID"); + table.ForeignKey( + name: "group_invite_ibfk_3", + column: x => x.InvitedUser, + principalTable: "users", + principalColumn: "ID"); + }) + .Annotation("MySql:CharSet", "utf8mb4") + .Annotation("Relational:Collation", "utf8mb4_general_ci"); + + migrationBuilder.CreateIndex( + name: "GroupId", + table: "group_invite", + column: "GroupId"); + + migrationBuilder.CreateIndex( + name: "InvitedUser", + table: "group_invite", + column: "InvitedUser"); + + migrationBuilder.CreateIndex( + name: "InviteUser", + table: "group_invite", + column: "InviteUser"); + } + } +} diff --git a/backend/IM_API/Migrations/20260315134653_update-user.Designer.cs b/backend/IM_API/Migrations/20260315134653_update-user.Designer.cs new file mode 100644 index 0000000..d45c70f --- /dev/null +++ b/backend/IM_API/Migrations/20260315134653_update-user.Designer.cs @@ -0,0 +1,1110 @@ +// +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("20260315134653_update-user")] + partial class updateuser + { + /// + 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") + .IsRequired() + .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.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("InviteUserId") + .HasColumnType("int"); + + 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"); + + 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("Description") + .HasColumnType("longtext"); + + b.Property("Email") + .HasColumnType("longtext"); + + 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("Region") + .HasColumnType("longtext"); + + 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.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("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("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/20260315134653_update-user.cs b/backend/IM_API/Migrations/20260315134653_update-user.cs new file mode 100644 index 0000000..38752f2 --- /dev/null +++ b/backend/IM_API/Migrations/20260315134653_update-user.cs @@ -0,0 +1,54 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace IM_API.Migrations +{ + /// + public partial class updateuser : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Description", + table: "users", + type: "longtext", + nullable: true, + collation: "utf8mb4_general_ci") + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "Email", + table: "users", + type: "longtext", + nullable: true, + collation: "utf8mb4_general_ci") + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "Region", + table: "users", + type: "longtext", + nullable: true, + collation: "utf8mb4_general_ci") + .Annotation("MySql:CharSet", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Description", + table: "users"); + + migrationBuilder.DropColumn( + name: "Email", + table: "users"); + + migrationBuilder.DropColumn( + name: "Region", + table: "users"); + } + } +} diff --git a/backend/IM_API/Migrations/ImContextModelSnapshot.cs b/backend/IM_API/Migrations/ImContextModelSnapshot.cs index 53e6917..f195355 100644 --- a/backend/IM_API/Migrations/ImContextModelSnapshot.cs +++ b/backend/IM_API/Migrations/ImContextModelSnapshot.cs @@ -333,6 +333,7 @@ namespace IM_API.Migrations .HasComment("全员禁言(0允许发言,2全员禁言)"); b.Property("Announcement") + .IsRequired() .HasColumnType("text") .HasComment("群公告"); @@ -392,50 +393,6 @@ namespace IM_API.Migrations 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") @@ -502,6 +459,9 @@ namespace IM_API.Migrations .HasColumnType("int(11)") .HasComment("群聊编号\r\n"); + b.Property("InviteUserId") + .HasColumnType("int"); + b.Property("State") .HasColumnType("tinyint(4)") .HasComment("申请状态(0:待管理员同意,1:已拒绝,2:已同意)"); @@ -515,8 +475,7 @@ namespace IM_API.Migrations b.HasIndex("UserId"); - b.HasIndex(new[] { "GroupId" }, "GroupId") - .HasDatabaseName("GroupId1"); + b.HasIndex(new[] { "GroupId" }, "GroupId"); b.ToTable("group_request", (string)null); @@ -844,6 +803,12 @@ namespace IM_API.Migrations .HasDefaultValueSql("'1970-01-01 00:00:00'") .HasComment("创建时间"); + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("Email") + .HasColumnType("longtext"); + b.Property("IsDeleted") .HasColumnType("tinyint(4)") .HasComment("软删除标识\r\n0:账号正常\r\n1:账号已删除"); @@ -863,6 +828,9 @@ namespace IM_API.Migrations .HasColumnType("varchar(50)") .HasComment("密码"); + b.Property("Region") + .HasColumnType("longtext"); + b.Property("Status") .ValueGeneratedOnAdd() .HasColumnType("tinyint(4)") @@ -991,31 +959,6 @@ namespace IM_API.Migrations 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") @@ -1108,8 +1051,6 @@ namespace IM_API.Migrations modelBuilder.Entity("IM_API.Models.Group", b => { - b.Navigation("GroupInvites"); - b.Navigation("GroupMembers"); b.Navigation("GroupRequests"); @@ -1148,10 +1089,6 @@ namespace IM_API.Migrations b.Navigation("FriendUsers"); - b.Navigation("GroupInviteInviteUserNavigations"); - - b.Navigation("GroupInviteInvitedUserNavigations"); - b.Navigation("GroupMembers"); b.Navigation("GroupRequests"); diff --git a/backend/IM_API/Models/Group.cs b/backend/IM_API/Models/Group.cs index 8d06fee..5449b21 100644 --- a/backend/IM_API/Models/Group.cs +++ b/backend/IM_API/Models/Group.cs @@ -38,7 +38,7 @@ public partial class Group /// /// 群公告 /// - public string? Announcement { get; set; } + public string Announcement { get; set; } = "暂无群公告,点击编辑添加。"; /// /// 群聊创建时间 @@ -56,7 +56,6 @@ public partial class Group public DateTimeOffset LastUpdateTime { get; set; } = DateTime.UtcNow; - public virtual ICollection GroupInvites { get; set; } = new List(); public virtual User GroupMasterNavigation { get; set; } = null!; diff --git a/backend/IM_API/Models/GroupInviteExt.cs b/backend/IM_API/Models/GroupInviteExt.cs deleted file mode 100644 index 1202368..0000000 --- a/backend/IM_API/Models/GroupInviteExt.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace IM_API.Models -{ - public partial class GroupInvite - { - public GroupInviteState StateEnum - { - get => (GroupInviteState)State; - set => State = (sbyte)value; - } - } -} diff --git a/backend/IM_API/Models/GroupInviteState.cs b/backend/IM_API/Models/GroupInviteState.cs deleted file mode 100644 index 0a69d35..0000000 --- a/backend/IM_API/Models/GroupInviteState.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace IM_API.Models -{ - /// - /// 群邀请状态 - /// - public enum GroupInviteState - { - /// - /// 待处理 - /// - Pending = 0, - /// - /// 已同意 - /// - Passed = 1 - } -} diff --git a/backend/IM_API/Models/GroupRequestState.cs b/backend/IM_API/Models/GroupRequestState.cs index ccc0d94..89cef21 100644 --- a/backend/IM_API/Models/GroupRequestState.cs +++ b/backend/IM_API/Models/GroupRequestState.cs @@ -7,12 +7,24 @@ /// Pending = 0, /// - /// 已拒绝 + /// 管理员已拒绝 /// Declined = 1, /// - /// 已同意 + /// 管理员已同意 /// - Passed = 2 + Passed = 2, + /// + /// 待对方同意 + /// + TargetPending = 3, + /// + /// 对方拒绝 + /// + TargetDeclined = 4, + /// + /// 对方同意 + /// + TargetPassed = 5 } } diff --git a/backend/IM_API/Models/Groupinvite.cs b/backend/IM_API/Models/Groupinvite.cs deleted file mode 100644 index cca0ad9..0000000 --- a/backend/IM_API/Models/Groupinvite.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace IM_API.Models; - -public partial class GroupInvite -{ - public int Id { get; set; } - - /// - /// 群聊编号 - /// - public int GroupId { get; set; } - - /// - /// 被邀请用户 - /// - public int? InvitedUser { get; set; } - - /// - /// 邀请用户 - /// - public int? InviteUser { get; set; } - - /// - /// 当前状态(0:待被邀请人同意 - /// 1:被邀请人已同意) - /// - public sbyte? State { get; set; } - - /// - /// 创建时间 - /// - public DateTimeOffset? Created { get; set; } - - public virtual Group Group { get; set; } = null!; - - public virtual User? InviteUserNavigation { get; set; } - - public virtual User? InvitedUserNavigation { get; set; } -} diff --git a/backend/IM_API/Models/Grouprequest.cs b/backend/IM_API/Models/Grouprequest.cs index a372610..9b20fa5 100644 --- a/backend/IM_API/Models/Grouprequest.cs +++ b/backend/IM_API/Models/Grouprequest.cs @@ -19,8 +19,10 @@ public partial class GroupRequest /// public int UserId { get; set; } + public int? InviteUserId { get; set; } + /// - /// 申请状态(0:待管理员同意,1:已拒绝,2:已同意) + /// 申请状态(0:待管理员同意,1:管理员已拒绝,2:管理员已同意,3:待对方同意,4:对方拒绝) /// public sbyte State { get; set; } diff --git a/backend/IM_API/Models/ImContext.Custom.cs b/backend/IM_API/Models/ImContext.Custom.cs index d02529d..ea2bb95 100644 --- a/backend/IM_API/Models/ImContext.Custom.cs +++ b/backend/IM_API/Models/ImContext.Custom.cs @@ -49,11 +49,6 @@ namespace IM_API.Models entity.Ignore(e => e.AuhorityEnum); }); - modelBuilder.Entity(entity => - { - entity.Ignore(e => e.StateEnum); - }); - modelBuilder.Entity(entity => { entity.Ignore(e => e.RoleEnum); diff --git a/backend/IM_API/Models/ImContext.cs b/backend/IM_API/Models/ImContext.cs index 59599b4..6b263d5 100644 --- a/backend/IM_API/Models/ImContext.cs +++ b/backend/IM_API/Models/ImContext.cs @@ -27,7 +27,6 @@ public partial class ImContext : DbContext public virtual DbSet Groups { get; set; } - public virtual DbSet GroupInvites { get; set; } public virtual DbSet GroupMembers { get; set; } @@ -362,53 +361,6 @@ public partial class ImContext : DbContext .HasConstraintName("groups_ibfk_1"); }); - modelBuilder.Entity(entity => - { - entity.HasKey(e => e.Id).HasName("PRIMARY"); - - entity - .ToTable("group_invite") - .HasCharSet("utf8mb4") - .UseCollation("utf8mb4_general_ci"); - - entity.HasIndex(e => e.GroupId, "GroupId"); - - entity.HasIndex(e => e.InviteUser, "InviteUser"); - - entity.HasIndex(e => e.InvitedUser, "InvitedUser"); - - entity.Property(e => e.Id) - .HasColumnType("int(11)") - .HasColumnName("ID"); - entity.Property(e => e.Created) - .HasComment("创建时间") - .HasColumnType("datetime"); - entity.Property(e => e.GroupId) - .HasComment("群聊编号") - .HasColumnType("int(11)"); - entity.Property(e => e.InviteUser) - .HasComment("邀请用户") - .HasColumnType("int(11)"); - entity.Property(e => e.InvitedUser) - .HasComment("被邀请用户") - .HasColumnType("int(11)"); - entity.Property(e => e.State) - .HasComment("当前状态(0:待被邀请人同意\r\n1:被邀请人已同意)") - .HasColumnType("tinyint(4)"); - - entity.HasOne(d => d.Group).WithMany(p => p.GroupInvites) - .HasForeignKey(d => d.GroupId) - .OnDelete(DeleteBehavior.ClientSetNull) - .HasConstraintName("group_invite_ibfk_2"); - - entity.HasOne(d => d.InviteUserNavigation).WithMany(p => p.GroupInviteInviteUserNavigations) - .HasForeignKey(d => d.InviteUser) - .HasConstraintName("group_invite_ibfk_1"); - - entity.HasOne(d => d.InvitedUserNavigation).WithMany(p => p.GroupInviteInvitedUserNavigations) - .HasForeignKey(d => d.InvitedUser) - .HasConstraintName("group_invite_ibfk_3"); - }); modelBuilder.Entity(entity => { diff --git a/backend/IM_API/Models/User.cs b/backend/IM_API/Models/User.cs index ece733f..2e7ee5d 100644 --- a/backend/IM_API/Models/User.cs +++ b/backend/IM_API/Models/User.cs @@ -23,6 +23,18 @@ public partial class User /// 用户昵称 /// public string? NickName { get; set; } + /// + /// 用户邮箱 + /// + public string? Email { get; set; } + /// + /// 用户签名 + /// + public string? Description { get; set; } = ""; + /// + /// 地区 + /// + public string? Region { get; set; } = "未知地区"; /// /// 用户在线状态 @@ -73,10 +85,6 @@ public partial class User [JsonIgnore] public virtual ICollection FriendUsers { get; set; } = new List(); [JsonIgnore] - public virtual ICollection GroupInviteInviteUserNavigations { get; set; } = new List(); - [JsonIgnore] - public virtual ICollection GroupInviteInvitedUserNavigations { get; set; } = new List(); - [JsonIgnore] public virtual ICollection GroupMembers { get; set; } = new List(); [JsonIgnore] public virtual ICollection GroupRequests { get; set; } = new List(); diff --git a/backend/IM_API/Services/ConversationService.cs b/backend/IM_API/Services/ConversationService.cs index 1cfbb43..36b1746 100644 --- a/backend/IM_API/Services/ConversationService.cs +++ b/backend/IM_API/Services/ConversationService.cs @@ -146,7 +146,8 @@ namespace IM_API.Services public async Task MakeConversationAsync(int userAId, int userBId, ChatType chatType) { - var userAcExist = await _context.Conversations.AnyAsync(x => x.UserId == userAId && x.TargetId == userBId); + var userAcExist = await _context.Conversations.AnyAsync( + x => x.UserId == userAId && x.TargetId == userBId && x.ChatType == chatType); if (userAcExist) return; var streamKey = chatType == ChatType.PRIVATE ? StreamKeyBuilder.Private(userAId, userBId) : StreamKeyBuilder.Group(userBId); diff --git a/backend/IM_API/Services/GroupService.cs b/backend/IM_API/Services/GroupService.cs index 4d09a8a..ff89ac7 100644 --- a/backend/IM_API/Services/GroupService.cs +++ b/backend/IM_API/Services/GroupService.cs @@ -79,6 +79,7 @@ namespace IM_API.Services } } + public Task DeleteGroupAsync(int userId, int groupId) { throw new NotImplementedException(); @@ -90,15 +91,15 @@ namespace IM_API.Services x => x.Id == groupId) ?? throw new BaseException(CodeDefine.GROUP_NOT_FOUND); //过滤非好友 var groupInviteIds = await validFriendshipAsync(userId, userIds); - var inviteList = groupInviteIds.Select(id => new GroupInvite + var inviteList = groupInviteIds.Select(id => new GroupRequest { Created = DateTime.UtcNow, GroupId = group.Id, - InviteUser = userId, - InvitedUser = id, - StateEnum = GroupInviteState.Pending + UserId = id, + InviteUserId = userId, + StateEnum = GroupRequestState.TargetPending }).ToList(); - _context.GroupInvites.AddRange(inviteList); + _context.GroupRequests.AddRange(inviteList); await _context.SaveChangesAsync(); await _endPoint.Publish(new GroupInviteEvent { @@ -132,7 +133,7 @@ namespace IM_API.Services throw new NotImplementedException(); } - public async Task> GetGroupListAsync(int userId, int page, int limit, bool desc) + public async Task> GetGroupListAsync(int userId, int page = 1, int limit = 50, bool desc = false) { var query = _context.GroupMembers .Where(x => x.UserId == userId) @@ -159,14 +160,18 @@ namespace IM_API.Services _context.Groups.Update(group); await _context.SaveChangesAsync(); } + public async Task HandleGroupInviteAsync(int userid, HandleGroupInviteDto dto) { var user = _userService.GetUserInfoAsync(userid); - var inviteInfo = await _context.GroupInvites.FirstOrDefaultAsync(x => x.Id == dto.InviteId) + var inviteInfo = await _context.GroupRequests + .FirstOrDefaultAsync(x => x.UserId == userid && x.StateEnum == GroupRequestState.TargetPending) ?? throw new BaseException(CodeDefine.INVALID_ACTION); - if (inviteInfo.InvitedUser != userid) throw new BaseException(CodeDefine.AUTH_FAILED); + if (!(dto.Action == GroupRequestState.TargetPassed || + dto.Action == GroupRequestState.TargetDeclined)) + return; inviteInfo.StateEnum = dto.Action; - _context.GroupInvites.Update(inviteInfo); + _context.GroupRequests.Update(inviteInfo); await _context.SaveChangesAsync(); await _endPoint.Publish(new GroupInviteActionUpdateEvent { @@ -176,12 +181,13 @@ namespace IM_API.Services EventId = Guid.NewGuid(), GroupId = inviteInfo.GroupId, InviteId = inviteInfo.Id, - InviteUserId = inviteInfo.InviteUser.Value, + InviteUserId = inviteInfo.InviteUserId!.Value, OperatorId = userid, UserId = userid }); } + public async Task HandleGroupRequestAsync(int userid, HandleGroupRequestDto dto) { var user = _userService.GetUserInfoAsync(userid); @@ -266,5 +272,104 @@ namespace IM_API.Services return user; }).ToList(); } + + public async Task UpdateGroupInfoAsync(int userId, int groupId, GroupUpdateDto updateDto) + { + //判断群存在 + var groupInfo = await _context.Groups.FirstOrDefaultAsync(x => x.Id == groupId); + if (groupInfo is null) + throw new BaseException(CodeDefine.GROUP_NOT_FOUND); + + //判断操作者权限 + var memberInfo = await _context.GroupMembers + .FirstOrDefaultAsync(x => x.UserId == userId && x.GroupId == groupId); + + if (memberInfo is null || memberInfo.RoleEnum == GroupMemberRole.Normal) + throw new BaseException(CodeDefine.NO_GROUP_PERMISSION); + + groupInfo.Name = updateDto.GroupName ?? groupInfo.Name; + groupInfo.Avatar = updateDto.Avatar ?? groupInfo.Avatar; + groupInfo.Announcement = updateDto.Description ?? groupInfo.Announcement; + + + _context.Groups.Update(groupInfo); + + await _context.SaveChangesAsync(); + + return _mapper.Map(groupInfo); + } + + public async Task GetGroupInfoAsync(int groupId) + { + var groupInfo = await _context.Groups.FirstOrDefaultAsync(x => x.Id == groupId); + + if (groupInfo is null) + throw new BaseException(CodeDefine.GROUP_NOT_FOUND); + + return _mapper.Map(groupInfo); + } + + public async Task> GetGroupNotificationAsync(int userId) + { + // 1. 查询群请求记录 + var groupList = await _context.GroupMembers + .Where(x => x.UserId == userId && + (x.Role == (sbyte)GroupMemberRole.Master || x.Role == (sbyte)GroupMemberRole.Administrator)) + .Select(s => s.GroupId) + .ToListAsync(); + + var groupRequest = await _context.GroupRequests + .Where(x => groupList.Contains(x.GroupId) || x.UserId == userId || x.InviteUserId == userId) + .OrderByDescending(o => o.Id) + .ToListAsync(); + + if (!groupRequest.Any()) return new List(); + + // 2. 收集所有需要的 ID 并去重 + var userIds = groupRequest.Select(s => s.UserId).Distinct().ToList(); + var inviteUserIds = groupRequest.Where(x => x.InviteUserId != null).Select(s => s.InviteUserId.Value).Distinct().ToList(); + var groupIds = groupRequest.Select(s => s.GroupId).Distinct().ToList(); + + var userList = await _userService.GetUserInfoListAsync(userIds); + var inviteUserList = await _userService.GetUserInfoListAsync(inviteUserIds); + var groupInfoList = await _context.Groups + .Where(x => groupIds.Contains(x.Id)) + .ToListAsync(); + + // 2. 转换为字典 + var userDict = userList.ToDictionary(u => u.Id); + var inviteUserDict = inviteUserList.ToDictionary(u => u.Id); + var groupDict = groupInfoList.ToDictionary(g => g.Id); + + // 3. 组装数据 (Select 逻辑不变) + return groupRequest.Select(g => + { + var gnv = _mapper.Map(g); + + // 匹配用户信息 + if (userDict.TryGetValue(g.UserId, out var u)) + { + gnv.UserAvatar = u.Avatar; + gnv.NickName = u.NickName; + } + + // 匹配邀请人信息 + if (g.InviteUserId.HasValue && inviteUserDict.TryGetValue(g.InviteUserId.Value, out var i)) + { + gnv.InviteUserAvatar = i.Avatar; + gnv.InviteUserNickname = i.NickName; + } + + // 匹配群信息 + if (groupDict.TryGetValue(g.GroupId, out var gi)) + { + gnv.GroupAvatar = gi.Avatar; + gnv.GroupName = gi.Name; + } + + return gnv; + }).ToList(); + + } } } diff --git a/backend/IM_API/VOs/Group/GroupInfoVo.cs b/backend/IM_API/VOs/Group/GroupInfoVo.cs new file mode 100644 index 0000000..50ecbb0 --- /dev/null +++ b/backend/IM_API/VOs/Group/GroupInfoVo.cs @@ -0,0 +1,8 @@ +using IM_API.Dtos.Group; + +namespace IM_API.VOs.Group +{ + public class GroupInfoVo:GroupInfoDto + { + } +} diff --git a/backend/IM_API/VOs/Group/GroupInviteActionUpdateVo.cs b/backend/IM_API/VOs/Group/GroupInviteActionUpdateVo.cs index 23fdd23..65640e2 100644 --- a/backend/IM_API/VOs/Group/GroupInviteActionUpdateVo.cs +++ b/backend/IM_API/VOs/Group/GroupInviteActionUpdateVo.cs @@ -8,6 +8,6 @@ namespace IM_API.VOs.Group public int InviteUserId { get; set; } public int InvitedUserId { get; set; } public int InviteId { get; set; } - public GroupInviteState Action { get; set; } + public GroupRequestState Action { get; set; } } } diff --git a/backend/IM_API/VOs/Group/GroupNotificationVo.cs b/backend/IM_API/VOs/Group/GroupNotificationVo.cs new file mode 100644 index 0000000..4152191 --- /dev/null +++ b/backend/IM_API/VOs/Group/GroupNotificationVo.cs @@ -0,0 +1,21 @@ +using IM_API.Models; + +namespace IM_API.VOs.Group +{ + public class GroupNotificationVo + { + public int RequestId { get; set; } + public int? UserId { get; set; } + public string? NickName { get; set; } + public string? UserAvatar { get; set; } + public int GroupId { get; set; } + public string? GroupAvatar { get; set; } + public string? GroupName { get; set; } + + public GroupRequestState Status { get; set; } + public string Description { get; set; } + public int? InviteUser { get; set; } + public string? InviteUserNickname { get; set; } + public string? InviteUserAvatar { get; set; } + } +} diff --git a/frontend/app/android/settings.gradle.kts b/frontend/app/android/settings.gradle.kts index 2507983..f3cfa80 100644 --- a/frontend/app/android/settings.gradle.kts +++ b/frontend/app/android/settings.gradle.kts @@ -10,6 +10,9 @@ pluginManagement { includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") repositories { + // 👇 添加这一行,用于下载 Flutter 插件相关的依赖 + maven { url = uri("https://storage.flutter-io.cn/download.flutter.io") } + maven { url = uri("https://maven.aliyun.com/repository/google") } maven { url = uri("https://maven.aliyun.com/repository/public") } gradlePluginPortal() @@ -21,6 +24,9 @@ pluginManagement { dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS) repositories { + // 👇 添加这一行,这是解决你报错的核心!强制项目从这里下载 Flutter 引擎 + maven { url = uri("https://storage.flutter-io.cn/download.flutter.io") } + maven { url = uri("https://maven.aliyun.com/repository/google") } maven { url = uri("https://maven.aliyun.com/repository/public") } gradlePluginPortal() @@ -31,8 +37,10 @@ dependencyResolutionManagement { plugins { id("dev.flutter.flutter-plugin-loader") version "1.0.0" - id("com.android.application") version "8.1.1" apply false - id("org.jetbrains.kotlin.android") version "1.8.22" apply false + // 👇 升级到 8.2.1 修复 Java 21 兼容性 bug + id("com.android.application") version "8.2.1" apply false + // 👇 顺便把 Kotlin 版本也稍微升一下,避免后续出现旧版本警告 + id("org.jetbrains.kotlin.android") version "1.9.22" apply false } include(":app") \ No newline at end of file diff --git a/frontend/app/lib/core/network/auth_interceptor.dart b/frontend/app/lib/core/network/auth_interceptor.dart new file mode 100644 index 0000000..5cf8b08 --- /dev/null +++ b/frontend/app/lib/core/network/auth_interceptor.dart @@ -0,0 +1,9 @@ +import 'package:dio/dio.dart'; + +class AuthInterceptor extends Interceptor{ + @override + void onRequest(RequestOptions options, RequestInterceptorHandler handler) { + // TODO: implement onRequest + super.onRequest(options, handler); + } +} \ No newline at end of file diff --git a/frontend/app/lib/core/network/request.dart b/frontend/app/lib/core/network/request.dart new file mode 100644 index 0000000..3c3e5fd --- /dev/null +++ b/frontend/app/lib/core/network/request.dart @@ -0,0 +1,21 @@ +import 'package:app/core/network/auth_interceptor.dart'; +import 'package:dio/dio.dart'; + +class Request { + static final Request _instance = Request._internal(); + factory Request() => _instance; + + late Dio dio; + + Request._internal(){ + BaseOptions options = BaseOptions( + baseUrl: "", + responseType: ResponseType.json, + contentType: 'application/json; charset=utf-8', + ); + dio = Dio(options); + dio.interceptors.addAll([ + AuthInterceptor() + ]); + } +} \ No newline at end of file diff --git a/frontend/app/lib/core/router/app_router.dart b/frontend/app/lib/core/router/app_router.dart index 1e8e01e..538751c 100644 --- a/frontend/app/lib/core/router/app_router.dart +++ b/frontend/app/lib/core/router/app_router.dart @@ -1,6 +1,7 @@ import 'package:app/features/auth/pages/login_page.dart'; +import 'package:app/features/auth/pages/register_page.dart'; import 'package:app/features/home/pages/index_page.dart'; import 'package:go_router/go_router.dart'; @@ -10,6 +11,7 @@ final appRouter = GoRouter( initialLocation: '/auth/login', routes: [ GoRoute(path: '/auth/login', builder: (context, state) => const LoginPage()), + GoRoute(path: '/auth/register', builder: (context, state) => const RegisterPage()), ShellRoute( builder: (context, state, child) { return MainPage(child: child); diff --git a/frontend/app/lib/features/auth/bloc/login_page_state.dart b/frontend/app/lib/features/auth/bloc/login_page_state.dart index 7a410df..1a90032 100644 --- a/frontend/app/lib/features/auth/bloc/login_page_state.dart +++ b/frontend/app/lib/features/auth/bloc/login_page_state.dart @@ -1,6 +1,7 @@ import 'package:app/core/constants/app_colors.dart'; import 'package:app/features/auth/pages/login_page.dart'; import 'package:flutter/material.dart'; +import 'package:app/core/router/app_router.dart'; class LoginPageState extends State { @override @@ -11,10 +12,10 @@ class LoginPageState extends State { backgroundColor: Colors.transparent, elevation: 0, actions: [ - TextButton( - onPressed: () {}, - child: const Text("找回密码", style: TextStyle(color: Colors.grey)), - ), + // TextButton( + // onPressed: () {}, + // child: const Text("找回密码", style: TextStyle(color: Colors.grey)), + // ), ], ), body: SingleChildScrollView( @@ -24,23 +25,9 @@ class LoginPageState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ const SizedBox(height: 20), - // 1. 品牌Logo/头像区域 - Container( - width: 100, - height: 100, - decoration: BoxDecoration( - color: AppColors.primaryColor.withOpacity(0.1), - shape: BoxShape.circle, - ), - child: const Icon( - Icons.person_rounded, - size: 60, - color: AppColors.primaryColor, - ), - ), const SizedBox(height: 24), const Text( - "登录您的聊天账号", + "登陆账号", style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, @@ -109,7 +96,9 @@ class LoginPageState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ TextButton( - onPressed: () {}, + onPressed: () { + appRouter.push("/auth/register"); + }, child: const Text( "注册账号", style: TextStyle(color: Color(0xFF576B95)), @@ -126,6 +115,13 @@ class LoginPageState extends State { style: TextStyle(color: Color(0xFF576B95)), ), ), + TextButton( + onPressed: () {}, + child: const Text( + "找回密码", + style: TextStyle(color: Color(0xFF576B95)), + ), + ), ], ), diff --git a/frontend/app/lib/features/auth/bloc/register_page_state.dart b/frontend/app/lib/features/auth/bloc/register_page_state.dart new file mode 100644 index 0000000..65439cc --- /dev/null +++ b/frontend/app/lib/features/auth/bloc/register_page_state.dart @@ -0,0 +1,156 @@ +import 'package:app/features/auth/pages/register_page.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +import '../../../core/constants/app_colors.dart'; + +class RegisterPageState extends State { + @override + Widget build(BuildContext context) { + // TODO: implement build + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + actions: [ + // TextButton( + // onPressed: () {}, + // child: const Text("找回密码", style: TextStyle(color: Colors.grey)), + // ), + ], + ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 40), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 20), + const SizedBox(height: 24), + const Text( + "注册账号", + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + const SizedBox(height: 60), + + // 2. 账号输入框 (极简下划线风格) + TextField( + decoration: InputDecoration( + labelText: "用户名", + labelStyle: const TextStyle(color: Colors.grey, fontSize: 14), + floatingLabelStyle: const TextStyle(color: AppColors.primaryColor), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: Colors.grey.shade300), + ), + focusedBorder: const UnderlineInputBorder( + borderSide: BorderSide(color: AppColors.primaryColor, width: 2), + ), + ), + ), + const SizedBox(height: 25), + + // 3. 密码输入框 + TextField( + obscureText: true, + decoration: InputDecoration( + labelText: "请输入密码", + labelStyle: const TextStyle(color: Colors.grey, fontSize: 14), + floatingLabelStyle: const TextStyle(color: AppColors.primaryColor), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: Colors.grey.shade300), + ), + focusedBorder: const UnderlineInputBorder( + borderSide: BorderSide(color: AppColors.primaryColor, width: 2), + ), + ), + ), + const SizedBox(height: 60), + + // 4. 登录按钮 (圆润大按钮) + SizedBox( + width: double.infinity, + height: 50, + child: ElevatedButton( + onPressed: () {}, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primaryColor, + foregroundColor: Colors.white, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + child: const Text( + "注 册", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500), + ), + ), + ), + const SizedBox(height: 20), + + // 5. 注册/切换登录方式 + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextButton( + onPressed: () {}, + child: const Text( + "已有账号?点我登录", + style: TextStyle(color: Color(0xFF576B95)), + ), // 经典的链接蓝 + ), + const SizedBox( + height: 20, + child: VerticalDivider(color: Colors.grey), + ), + TextButton( + onPressed: () {}, + child: const Text( + "找回密码", + style: TextStyle(color: Color(0xFF576B95)), + ), + ), + ], + ), + + const SizedBox(height: 80), + // 6. 底部协议 (社交App必有) + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Checkbox( + value: true, + activeColor: AppColors.primaryColor, + onChanged: (v) {}, + ), + const Text( + "我已阅读并同意", + style: TextStyle(color: Colors.grey, fontSize: 12), + ), + const Text( + "《用户协议》", + style: TextStyle(color: Color(0xFF576B95), fontSize: 12), + ), + const Text( + "与", + style: TextStyle(color: Colors.grey, fontSize: 12), + ), + const Text( + "《隐私政策》", + style: TextStyle(color: Color(0xFF576B95), fontSize: 12), + ), + ], + ), + ], + ), + ), + ), + ); + } + +} \ No newline at end of file diff --git a/frontend/app/lib/features/auth/pages/register_page.dart b/frontend/app/lib/features/auth/pages/register_page.dart new file mode 100644 index 0000000..a923907 --- /dev/null +++ b/frontend/app/lib/features/auth/pages/register_page.dart @@ -0,0 +1,12 @@ +import 'package:app/features/auth/bloc/register_page_state.dart'; +import 'package:flutter/cupertino.dart'; + +class RegisterPage extends StatefulWidget { + const RegisterPage({super.key}); + @override + State createState() { + // TODO: implement createState + return RegisterPageState(); + } + +} \ No newline at end of file diff --git a/frontend/app/pubspec.lock b/frontend/app/pubspec.lock index 7d348ac..e0a0e52 100644 --- a/frontend/app/pubspec.lock +++ b/frontend/app/pubspec.lock @@ -5,16 +5,16 @@ packages: dependency: transitive description: name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" - url: "https://pub.dev" + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.flutter-io.cn" source: hosted - version: "2.13.0" + version: "2.13.1" boolean_selector: dependency: transitive description: name: boolean_selector sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" characters: @@ -22,7 +22,7 @@ packages: description: name: characters sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.1" clock: @@ -30,7 +30,7 @@ packages: description: name: clock sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.2" collection: @@ -38,23 +38,39 @@ packages: description: name: collection sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.19.1" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 - url: "https://pub.dev" + sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.0.8" + version: "1.0.9" + dio: + dependency: "direct main" + description: + name: dio + sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.9.2" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" fake_async: dependency: transitive description: name: fake_async sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.3" flutter: @@ -67,7 +83,7 @@ packages: description: name: flutter_lints sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.0.0" flutter_test: @@ -85,15 +101,23 @@ packages: description: name: go_router sha256: "7974313e217a7771557add6ff2238acb63f635317c35fa590d348fb238f00896" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "17.1.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.1.2" leak_tracker: dependency: transitive description: name: leak_tracker sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "11.0.2" leak_tracker_flutter_testing: @@ -101,7 +125,7 @@ packages: description: name: leak_tracker_flutter_testing sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.10" leak_tracker_testing: @@ -109,7 +133,7 @@ packages: description: name: leak_tracker_testing sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.2" lints: @@ -117,7 +141,7 @@ packages: description: name: lints sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.1.0" logging: @@ -125,7 +149,7 @@ packages: description: name: logging sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.0" matcher: @@ -133,7 +157,7 @@ packages: description: name: matcher sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.12.18" material_color_utilities: @@ -141,7 +165,7 @@ packages: description: name: material_color_utilities sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.13.0" meta: @@ -149,15 +173,23 @@ packages: description: name: meta sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.17.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.0" path: dependency: transitive description: name: path sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.9.1" sky_engine: @@ -170,7 +202,7 @@ packages: description: name: source_span sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.10.2" stack_trace: @@ -178,7 +210,7 @@ packages: description: name: stack_trace sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.12.1" stream_channel: @@ -186,7 +218,7 @@ packages: description: name: stream_channel sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.4" string_scanner: @@ -194,7 +226,7 @@ packages: description: name: string_scanner sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.1" term_glyph: @@ -202,7 +234,7 @@ packages: description: name: term_glyph sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.2" test_api: @@ -210,15 +242,23 @@ packages: description: name: test_api sha256: "19a78f63e83d3a61f00826d09bc2f60e191bf3504183c001262be6ac75589fb8" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.7.8" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.0" vector_math: dependency: transitive description: name: vector_math sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.0" vm_service: @@ -226,9 +266,17 @@ packages: description: name: vm_service sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "15.0.2" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" sdks: dart: ">=3.10.4 <4.0.0" flutter: ">=3.35.0" diff --git a/frontend/app/pubspec.yaml b/frontend/app/pubspec.yaml index f953d81..da559f8 100644 --- a/frontend/app/pubspec.yaml +++ b/frontend/app/pubspec.yaml @@ -38,6 +38,7 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 go_router: ^17.0.1 + dio: ^5.9.2 dev_dependencies: flutter_test: diff --git a/frontend/pc/IM/.env b/frontend/pc/IM/.env index ea10082..4994609 100644 --- a/frontend/pc/IM/.env +++ b/frontend/pc/IM/.env @@ -4,5 +4,5 @@ # VITE_SIGNALR_BASE_URL = https://im.test.nxsir.cn/chat/ -VITE_API_BASE_URL = http://192.168.5.116:7070/api -VITE_SIGNALR_BASE_URL = http://192.168.5.116:7070/chat/ +VITE_API_BASE_URL = http://192.168.5.100:8009/api +VITE_SIGNALR_BASE_URL = http://192.168.5.100:8009/chat diff --git a/frontend/pc/IM/.vscode/launch.json b/frontend/pc/IM/.vscode/launch.json index 0b6b9a6..f7a071a 100644 --- a/frontend/pc/IM/.vscode/launch.json +++ b/frontend/pc/IM/.vscode/launch.json @@ -11,6 +11,7 @@ "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd" }, "runtimeArgs": ["--sourcemap"], + "console": "integratedTerminal", "env": { "REMOTE_DEBUGGING_PORT": "9222" } diff --git a/frontend/pc/IM/.vscode/settings.json b/frontend/pc/IM/.vscode/settings.json index 4c05394..e879dfd 100644 --- a/frontend/pc/IM/.vscode/settings.json +++ b/frontend/pc/IM/.vscode/settings.json @@ -3,7 +3,7 @@ "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[javascript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "vscode.typescript-language-features" }, "[json]": { "editor.defaultFormatter": "esbenp.prettier-vscode" diff --git a/frontend/pc/IM/API.md b/frontend/pc/IM/API.md new file mode 100644 index 0000000..733c1dd --- /dev/null +++ b/frontend/pc/IM/API.md @@ -0,0 +1,573 @@ +# IM_NEW 接口文档 + +> 源码基线:`IM_NEW/codex/api-alignment-fixes`(起点 `32177a7293ec9eb7bd731467953da3c51e561beb`) +> 文档以控制器、DTO、枚举、序列化配置和 Nginx 网关配置为准;“当前实现注意事项”用于标明源码中的实际限制,不代表理想设计。 + +## 1. 通用约定 + +### 1.1 网关与路径大小写 + +- 网关默认端口:`8009`。 +- HTTP API 前缀:`/api`。 +- SignalR Hub:`/chat`。 +- Nginx `location` 匹配区分大小写,因此应使用本文给出的全小写控制器前缀,例如 `/api/user/me`。`/api/User/Me` 在当前网关会返回 404。 + +### 1.2 认证 + +除登录、注册以及下文特别标注的接口外,请携带: + +```http +Authorization: Bearer +``` + +SignalR 客户端通过 `/chat?access_token=` 完成握手;使用官方 SignalR 客户端时由 `accessTokenFactory` 自动添加。 + +Token 缺失、无效或过期时返回 **HTTP 401**,业务响应为: + +```json +{ "code": 1006, "message": "认证失败", "data": null } +``` + +已认证但权限不足时返回 **HTTP 403**,业务码为 `1005`。新版前端在滚动发布期间仍兼容旧服务的 HTTP 200 + `code=1006`。 + +### 1.3 统一响应 + +除文件内容下载外,接口通常返回: + +```json +{ + "code": 0, + "message": "成功", + "data": {} +} +``` + +- JSON 属性使用 camelCase。 +- `code === 0` 表示业务成功。 +- 参数校验失败也返回 HTTP 200,业务码为 `1003`。 +- 枚举通过 `JsonStringEnumConverter` 序列化为字符串,并接受字符串枚举请求值。 +- 未处理异常统一返回 HTTP 500 + `code=1000`,响应头 `X-Correlation-ID` 可用于关联服务端日志。 + +## 2. 认证与用户 + +### 2.1 POST `/api/auth/login` + +无需认证。请求: + +```json +{ "userName": "user1", "password": "******" } +``` + +校验:`userName` 5–20 字符;`password` 非空且不超过 50 字符。 + +成功返回 `Result`: + +```json +{ + "code": 0, + "message": "成功", + "data": { + "userId": "guid", + "token": "jwt", + "refreshToken": "string", + "expired": null, + "userName": "string", + "nickName": "string", + "avatar": "string|null", + "creationTime": "datetime-offset" + } +} +``` + +当前实现创建 `LoginResponse` 时将 `expired` 传为 `null`。 + +### 2.2 POST `/api/auth/register` + +无需认证。请求: + +```json +{ "userName": "newuser", "password": "******", "nickName": "新用户" } +``` + +校验:`userName` 5–20 字符;`password` 6–50 字符;`nickName` 非空且不超过 50 字符。返回 `Result`。 + +### 2.3 POST `/api/auth/refresh` + +无需 Access Token。请求: + +```json +{ "refreshToken": "string" } +``` + +返回新的 `Result`。 + +### 2.4 用户接口 + +| 方法 | 路径 | 参数 | 返回 | +|---|---|---|---| +| GET | `/api/user/me` | 无 | `Result` | +| GET | `/api/user/find?userId={guid}` | query: `userId` | `Result` | +| GET | `/api/user/findByUname?username={value}` | query: `username` | `Result` | +| POST | `/api/user/update` | body: `UserUpdateRequest` | `Result` | +| POST | `/api/user/getUsersByIds` | body: GUID 数组 | `Result` | + +`UserUpdateRequest` 的字段均可省略: + +```json +{ + "nickName": "string|null", + "region": "string|null", + "avatar": "string|null", + "description": "string|null" +} +``` + +`UserResponse`: + +```json +{ + "id": "guid", + "userName": "string", + "nickName": "string", + "email": "string|null", + "phone": "string|null", + "region": "string", + "description": "string", + "avatar": "string|null", + "creationTime": "datetime-offset", + "deletion": "datetime-offset|null" +} +``` + +## 3. 好友与好友申请 + +所有接口均要求认证。 + +### 3.1 好友 + +| 方法 | 路径 | 参数 | 返回 | +|---|---|---|---| +| GET | `/api/friend/list` | 无 | `Result` | +| POST | `/api/friend/delete?friendId={guid}` | 好友关系记录 ID,不是对方用户 ID | `Result`,成功时 `data: null` | +| POST | `/api/friend/block?friendId={guid}` | 好友关系记录 ID | `Result` | +| GET | `/api/friend/checkFriend?userId={guid}&targetId={guid}` | 两个用户 ID | `Result` | + +`FriendResponse`: + +```json +{ + "id": "friend-relation-guid", + "targetId": "peer-user-guid", + "avatar": "string|null", + "nickName": "string", + "remarkName": "string|null", + "createTime": "datetime", + "updateTime": "datetime|null", + "status": "Pending|Added|Declined|Blocked" +} +``` + +### 3.2 好友申请 + +#### POST `/api/friendRequest/add` + +```json +{ + "targetId": "guid", + "description": "string|null", + "remarkName": "string|null" +} +``` + +`targetId` 必填。返回 `Result`。 + +#### POST `/api/friendRequest/handle` + +```json +{ + "requestId": "guid", + "action": "Accpet|Reject|Block", + "remarkName": "string|null" +} +``` + +注意:后端枚举当前拼写为 `Accpet`,接受申请时 `remarkName` 必填。 + +#### GET `/api/friendRequest/list` + +返回与当前用户相关的申请:`Result`。 + +`FriendRequestResponse.state`:`Pending`、`Declined`、`Passed`、`Blocked`。 + +## 4. 群组 + +### 4.1 群信息 + +| 方法 | 路径 | 请求 | 返回 | +|---|---|---|---| +| GET | `/api/group/getAll` | 无 | `Result` | +| GET | `/api/group/getOne?groupId={guid}` | query: `groupId` | `Result` | +| POST | `/api/group/create` | `{ "name": "string|null" }` | `Result` | +| POST | `/api/group/update` | `GroupUpdateRequest` | `Result` | +| POST | `/api/group/dissolve?groupId={guid}` | 群 ID;仅群主 | `Result` | + +`name` 最大 20 字符。更新请求: + +```json +{ + "groupId": "guid", + "groupName": "string|null", + "avatar": "string|null", + "description": "string|null" +} +``` + +`description` 实际用于更新群公告 `announcement`。 + +`GroupResponse`: + +```json +{ + "id": "guid", + "name": "string", + "groupMaster": "guid", + "authority": "REQUIRE_CONSENT|ANYONE_CAN_JOIN|NOT_ALLOWED_TO_JOIN", + "allMembersBanned": false, + "status": "Normal|Blocked", + "announcement": "string", + "avatar": "string|null", + "maxSequenceId": 0, + "lastMessage": "string", + "lastSenderName": "string", + "created": "datetime-offset", + "updated": "datetime-offset" +} +``` + +`getAll` 按当前用户的有效群成员关系返回群,普通成员、管理员和群主均可看到已加入群。 + +### 4.2 群成员 + +| 方法 | 路径 | 参数 | 返回 | +|---|---|---|---| +| GET | `/api/groupMember/checkMember?userId={guid}&groupId={guid}` | 用户 ID、群 ID | `Result` | +| GET | `/api/groupMember/list?groupId={guid}` | 群 ID | `Result` | +| POST | `/api/groupMember/delete?memberId={guid}` | 群成员记录 ID | `Result` | +| POST | `/api/groupMember/leave?groupId={guid}` | 群 ID | `Result` | + +`GroupMemberResponse.role`:`Normal`、`Administrator`、`Master`。 + +`list`、`delete`、`leave` 均要求登录。`delete` 是管理操作,只能移除角色低于操作者的其他成员,禁止移除群主或自己;群主必须使用 `dissolve`,不能使用 `leave`。 + +`checkMember` 是服务间内部接口,必须携带 `X-Internal-Api-Key`,不应通过公网网关暴露。 + +### 4.3 群邀请 + +| 方法 | 路径 | 参数 | 返回 | +|---|---|---|---| +| POST | `/api/groupInvitation/send` | body: `{ "groupId": "guid", "userId": "guid" }` | `Result` | +| GET | `/api/groupInvitation/get?invitationId={guid}` | 邀请 ID | `Result` | +| POST | `/api/groupInvitation/handle?invitationId={guid}&action={value}` | `action=Accept|Reject` | `Result` | + +邀请状态:`Pending`、`Passed`、`Reject`。 + +### 4.4 入群申请 + +| 方法 | 路径 | 参数 | 返回 | +|---|---|---|---| +| POST | `/api/groupRequest/send` | body: `{ "groupId": "guid", "desc": "string|null" }` | `Result` | +| POST | `/api/groupRequest/handle?requestId={guid}&action={value}` | `action=Accept|Reject` | `Result` | +| GET | `/api/groupRequest/find?id={guid}` | 申请 ID | `Result` | +| GET | `/api/groupRequest/list` | 无 | `Result` | + +`desc` 最大 20 字符;状态为 `Pending`、`Declined` 或 `Passed`。 + +`list` 返回当前用户提交的申请,以及当前用户作为管理员或群主有权处理的群申请。 + +## 5. 会话与消息 + +所有接口均要求认证。 + +### 5.1 会话 + +| 方法 | 路径 | 参数 | 返回 | +|---|---|---|---| +| GET | `/api/conversation/list` | 无 | `Result` | +| GET | `/api/conversation/get?id={guid}` | 会话 ID | `Result` | +| POST | `/api/conversation/markRead?conversationId={guid}` | 会话 ID | `Result` | + +`ConversationResponse`: + +```json +{ + "id": "guid", + "userId": "guid", + "targetId": "guid", + "targetAvatar": "string", + "targetName": "string", + "lastReadSequenceId": 0, + "unreadCount": 0, + "chatType": "PRIVATE|GROUP", + "lastMessage": "string", + "dateTime": "datetime" +} +``` + +### 5.2 POST `/api/message/send` + +```json +{ + "clientMsgId": "guid", + "targetId": "guid", + "chatType": "PRIVATE|GROUP", + "msgType": "Text|Image|Voice|Video|File|VoiceChat|VideoChat", + "quoteMessageId": "guid|null", + "ext": { "key": "value" }, + "text": "string|null", + "url": "string|null", + "width": 0, + "height": 0, + "thumb": "string|null", + "duration": 0, + "fileId": "guid|null", + "fileName": "string|null", + "fileSize": 0, + "fileFormat": "mime/type|null" +} +``` + +- `clientMsgId`、`targetId` 必填。 +- `Text` 要求 `text`。 +- `Image`、`Video`、`Voice` 至少提供 `url` 或 `fileId`。 +- `File` 要求 `fileId`、`fileName`、`fileSize`、`fileFormat`;服务端已支持构建和保存文件消息。 +- `VoiceChat`、`VideoChat` 仍未实现,发送会返回 `2303`。 + +成功返回 `Result`: + +```json +{ + "id": "guid", + "clientMsgId": "guid", + "chatType": "PRIVATE", + "msgType": "Text", + "senderId": "guid", + "targetId": "guid", + "state": "Sent|Withdrwan", + "streamKey": "string", + "sequenceId": 1, + "creationTime": "datetime-offset", + "content": { + "fallback": "string", + "body": {}, + "ext": {}, + "quote": null + } +} +``` + +### 5.3 其他消息接口 + +| 方法 | 路径 | 参数 | 返回 | +|---|---|---|---| +| POST | `/api/message/withDraw?msgId={guid}` | 消息 ID | `Result` | +| GET | `/api/message/getMessages?conversationId={guid}&cursor={long?}&direction={int}&limit={int}` | 会话、游标、方向、条数 | `Result` | + +`GetMessagesResponse`: + +```json +{ "messages": [], "hasmore": false } +``` + +方向约定:`0` 查询 `SequenceId < cursor` 的历史消息;`1` 查询 `SequenceId > cursor` 的增量消息。两个方向都查询 `limit + 1` 条判断 `hasmore`,但响应最多返回 `limit` 条并按序列号升序;`limit` 范围为 1–100。 + +## 6. 文件服务 + +所有接口均要求认证。 + +### 6.1 文件 + +#### POST `/api/file/simple-upload` + +`multipart/form-data`: + +| 字段 | 类型 | 必填 | +|---|---|---| +| `file` | 文件 | 是 | +| `isPublic` | boolean | 是 | + +服务端计算 MD5 并执行安全秒传:公开文件可复用,私有文件只允许同一所有者复用。返回 `Result`。 + +#### GET `/api/file/{id}` + +返回文件信息 `Result`。 + +#### GET `/api/file/{id}/content` + +返回鉴权后的二进制文件流,支持 HTTP Range,不使用 `Result` 包装。无权限返回 HTTP 403,文件不存在返回 HTTP 404。 + +`FileResponse` 为扁平结构,不暴露存储桶、对象键等内部位置: + +```json +{ + "id": "guid", + "ownerId": "guid", + "fileName": "avatar.png", + "fileSize": 123, + "contentType": "image/png", + "state": "Uploaded", + "checkSum": "md5-hex", + "chatType": "PRIVATE|GROUP|null", + "targetId": "guid|null", + "isPublic": false, + "created": "datetime-offset", + "updated": "datetime-offset", + "url": "string|null" +} +``` + +`url` 仅在存储提供方能够生成公开地址时存在。 + +### 6.2 分片上传 + +#### POST `/api/fileTask/init` + +```json +{ + "conversationId": "guid", + "chatType": "PRIVATE|GROUP", + "targetId": "peer-or-group-guid", + "fileName": "string", + "fileSize": 123, + "contentType": "mime/type", + "checkSum": "md5" +} +``` + +返回: + +```json +{ + "taskId": "guid", + "uploadSessionId": "string", + "instant": false, + "uploadMode": "LocalMultipart|Presigned", + "totalPartCount": 1, + "partSizeBytes": 5242880, + "file": null +} +``` + +秒传命中时返回 `instant: true`、`uploadMode: "Instant"` 和最终 `file`;客户端直接使用该文件,不再调用 `complete`。 + +#### GET `/api/fileTask/getuploadurl?sessionId={value}&partNum={n}` + +返回 `Result`,不是字符串: + +```json +{ + "url": "string", + "method": "PUT|POST", + "headers": {}, + "expiresAt": "datetime-offset" +} +``` + +#### GET `/api/fileTask/progress?sessionId={value}` + +```json +{ + "sessionId": "string", + "taskId": "string", + "fileSize": 123, + "totalPartCount": 1, + "completedPartCount": 0, + "uploadedBytes": 0, + "progressPercent": 0 +} +``` + +#### POST `/api/fileTask/local/parts/upload` + +`multipart/form-data`:`sessionId`、`partNumber`、`file`。返回 `Result`,其中包含 `location`、`eTag`、`size`、`checksum` 和 `versionId`。 + +#### POST `/api/fileTask/complete` + +```json +{ + "sessionId": "string", + "parts": [ + { "partNumber": 1, "eTag": "string", "size": 123, "checksum": null } + ] +} +``` + +成功接受异步合并时返回 HTTP 202 + `Result`,其中 `state` 为 `Merging`。 + +#### GET `/api/fileTask/status?taskId={guid}` + +客户端轮询此接口。处理中返回 `Uploading|Merging`;失败返回 `state: "Failed"` 与 `failureReason`;完成后返回: + +```json +{ + "id": "upload-task-guid", + "uploaderId": "guid", + "conversationId": "guid", + "fileName": "string", + "fileSize": 123, + "contentType": "mime/type", + "storageLocation": {}, + "state": "Completed", + "checkSum": "string", + "resultFileId": "guid", + "failureReason": null, + "file": { "id": "guid", "fileName": "string", "fileSize": 123, "url": null } +} +``` + +任务、分片、进度、完成和状态接口都会校验当前用户是上传者。私有文件的 `url` 为 null,使用 `/api/file/{id}/content` 鉴权读取。 + +## 7. SignalR + +- Hub:`/chat` +- 服务端事件:`ReceiveNewMessage` +- 当前 Hub 没有 `clearUnreadCount` 方法;清零未读应调用 HTTP `/api/conversation/markRead`。 + +推送载荷与 HTTP `MessageResponse` 略有不同: + +```json +{ + "id": "guid", + "clientId": "guid", + "chatType": "PRIVATE|GROUP", + "msgType": "Text|Image|Voice|Video", + "senderId": "guid", + "targetId": "guid", + "state": "Sent|Withdrwan", + "streamKey": "string", + "sequenceId": 1, + "pushTimestamp": 0, + "content": { + "fallback": "string", + "body": {}, + "ext": {}, + "quote": null + } +} +``` + +## 8. 业务状态码 + +| 范围/代码 | 含义 | +|---|---| +| `0` | 成功 | +| `1000`–`1006` | 系统、超时、参数、数据库、权限、认证错误 | +| `2000`–`2004` | 用户不存在、已存在、密码错误、禁用、登录过期 | +| `2100`–`2107` | 好友申请、好友关系和操作错误 | +| `2200`–`2206` | 群不存在、已入群、群满、权限、邀请、申请、成员错误 | +| `2300`–`2303` | 消息发送、消息不存在、撤回、不支持的消息类型 | +| `2400`–`2403` | 文件上传、不存在、过大、类型不支持 | +| `3000`–`3004` | 管理后台错误 | +| `3100` | 会话不存在 | +| `3201`–`3206` | 分片不存在、合并失败、分片过小/数量不符、会话过期、分片号无效 | + +完整名称与中文说明以 `IM.Commons/ResultCode.cs` 为准。 diff --git a/frontend/pc/IM/API_INTEGRATION_AUDIT.md b/frontend/pc/IM/API_INTEGRATION_AUDIT.md new file mode 100644 index 0000000..65f4d52 --- /dev/null +++ b/frontend/pc/IM/API_INTEGRATION_AUDIT.md @@ -0,0 +1,100 @@ +# 前后端 API 修复实施报告 + +## 1. 结论与基线 + +修复计划中的核心阻断项已完成代码落地:认证刷新、会话映射与未读、消息双向分页、群列表/审批/退群/解散、异步分片上传、普通文件消息、私有文件鉴权读取及多环境 CSP 已对齐。 + +| 项目 | 基线/结果 | +|---|---| +| 后端 | `IM_NEW/codex/api-alignment-fixes`,起点 `32177a7293ec9eb7bd731467953da3c51e561beb` | +| 前端 | `feature-nxdev@f1af6e6` 的现有工作区上增量修改,未覆盖用户原有未提交改动 | +| 后端构建 | `dotnet build IM_API_NEW.sln --no-restore`:0 错误 | +| 前端构建 | `npm run build`:通过 | +| 前端测试 | `npm test`:1 个测试文件、3 个上传状态机测试全部通过 | +| 定向 ESLint | 本次涉及的前端文件使用 `npx eslint --quiet`:0 错误 | +| 运行态联调 | 待部署迁移并启动 MySQL、Redis、RabbitMQ、Consul 和网关后执行 | + +## 2. 已完成修复 + +### 认证与公共异常 + +- JWT Challenge/Forbidden 改为标准 HTTP 401/403,保留统一业务响应体。 +- 前端同时兼容新版 HTTP 401 和旧版 HTTP 200 + `code=1006`。 +- Token 刷新采用单飞队列;刷新成功重放请求,失败拒绝全部排队请求并退出登录。 +- 全局异常统一返回 HTTP 500 + `SYSTEM_ERROR`,通过 `X-Correlation-ID` 对应服务端日志。 +- RabbitMQ 消费增加短间隔重试,降低数据库提交与消息消费竞态造成的偶发失败。 + +### 会话与消息 + +- `ConversationResponse.dateTime` 改为 `DateTimeOffset`,映射使用 `ModificationTime ?? CreationTime`。 +- 会话更新拆分为更新摘要、增加未读和推进已读序号,群聊与私聊均更新参与者会话。 +- 会话创建消费者增加幂等判断。 +- `direction=0/1` 分别使用 `< cursor`、`> cursor`,响应裁剪为 `limit` 并保持升序;`limit` 限制为 1–100。 +- 前端断线补消息携带本地最大 `sequenceId`,IndexedDB 保存完整结构化消息。 + +### 群组、审批和权限 + +- 群列表按有效成员关系查询,普通成员也能看到已加入群。 +- 入群申请列表包含申请人自己的记录,以及管理员/群主可处理的目标群申请。 +- 群详情和成员列表验证当前用户是有效成员。 +- 成员移除严格比较角色,禁止移除自己、群主或同级/更高角色。 +- 新增 `/api/groupMember/leave` 和 `/api/group/dissolve`;群主只能解散,其他成员可退出。 +- 退出或解散通过事件软删除对应群会话;前端同步移除群、会话和 IndexedDB 缓存。 +- 内部成员检查要求 `X-Internal-Api-Key`,MessageService/FileService 使用服务端密钥调用。 + +### 文件上传与文件消息 + +- 初始化明确返回 `instant`、`uploadMode`、`totalPartCount`、`partSizeBytes` 和秒传 `file`。 +- 前端遵循服务端分片大小、预签名 method/headers;对象存储请求不携带业务 JWT。 +- 任一分片失败会使整体失败,不再吞错后调用 complete。 +- Vitest 覆盖秒传、正常分片异步完成和分片持续失败不得 complete。 +- complete 返回 HTTP 202,前端轮询 `/api/fileTask/status`,拿到最终 `fileId` 后才发送消息。 +- 消费者以 `SourceTaskId` 幂等创建最终文件,失败写入 `FailureReason`。 +- 上传地址、分片、进度、完成和状态都校验上传者;秒传按公开/私聊/群聊作用域复用。 +- 文件响应扁平化并隐藏内部位置;私有文件通过 `/api/file/{id}/content` 鉴权读取并支持 Range。 +- MessageService 支持 `File`,媒体消息支持稳定 `fileId`;前端可预览和下载普通文件消息。 +- 小文件上传由服务端计算 MD5,文件名支持 255 字符。 + +### 环境与部署 + +- 开发/生产默认网关统一为 `localhost:8009`。 +- Electron CSP 根据 `VITE_API_BASE_URL`、`VITE_SIGNALR_BASE_URL` 在构建时生成,不再固定测试网 IP。 +- Docker Compose 为 Group/Message/File 服务注入必填的 `IM_INTERNAL_API_KEY`。 + +## 3. 数据库变更与影响面 + +| 服务 | 迁移 | 影响 | +|---|---|---| +| MessageService | `20260909000100_ApiAlignmentFixes` | 新增会话复合索引;不改消息数据 | +| GroupService | `20260909000200_ApiAlignmentFixes` | 新增成员、角色、申请查询索引和申请 ID 唯一索引 | +| FileService | `20260909000300_AsyncUploadResult` | 增加上传作用域、结果、失败原因、公开标记和来源任务;收紧字符串列并新增索引 | + +迁移不物理删除业务数据。文件列由 `longtext` 收紧前,应检查历史值长度;`SourceTaskId` 建唯一索引前,应确认没有重复回填值。 + +## 4. 发布顺序 + +1. 备份三个服务数据库,执行历史字段长度与唯一性预检。 +2. 设置同一个非空 `IM_INTERNAL_API_KEY`,配置 FileService 的公开桶/公开地址、存储、Redis 和 RabbitMQ 参数。 +3. 先发布兼容新旧认证协议的前端。 +4. 执行 Message、Group、File 三个迁移并发布后端。 +5. 发布新版前端,确认 CSP 只包含当前环境的 API/SignalR 源。 +6. 用两个隔离账号执行运行态验收。 + +## 5. 部署后验收清单 + +- Token 过期时并发请求只刷新一次;401/403 正确,刷新失败统一退出。 +- 会话列表不再 500;私聊/群聊连续消息未读数正确,markRead 清零。 +- 两个分页方向无重复、无漏页、每页不超过 limit;断线后只补新消息。 +- 普通成员能看到群;群主/管理员能看到待审批申请;退群、解散、越权移人符合规则。 +- 本地分片与预签名上传均完成 `init → parts → complete → status`;秒传直接返回最终文件。 +- 图片、视频、语音和普通文件可发送、SignalR 接收、历史恢复、预览/下载。 +- 私聊第三方不能读取文件;退群成员不能读取群文件;公开头像 URL 非空。 +- 重复投递上传完成事件不会创建重复文件。 + +## 6. 当前保留项 + +- 本轮未启动完整依赖栈执行真实迁移和双账号端到端联调;这属于发布环境验证,不能由编译结果替代。 +- 项目原有全量 ESLint 基线仍有历史问题;本次只保证涉及文件的 `--quiet` 定向检查为 0 错误。 +- 后端仓库原先没有自动化测试项目;本轮完成全解决方案编译,数据库/MQ 行为仍以部署后集成验收为准。 +- `VoiceChat`、`VideoChat` 消息仍不在本次实现范围。 +- 私有文件采用鉴权内容接口而非暴露短时下载 URL,客户端必须携带 JWT 获取 Blob。 diff --git a/frontend/pc/IM/electron.vite.config.mjs b/frontend/pc/IM/electron.vite.config.mjs index 896c153..96843b0 100644 --- a/frontend/pc/IM/electron.vite.config.mjs +++ b/frontend/pc/IM/electron.vite.config.mjs @@ -1,16 +1,43 @@ import { resolve } from 'path' import { defineConfig } from 'electron-vite' +import { loadEnv } from 'vite' import vue from '@vitejs/plugin-vue' +import vueDevTools from 'vite-plugin-vue-devtools' -export default defineConfig({ - main: {}, - preload: {}, - renderer: { - resolve: { - alias: { - '@': resolve('src/renderer/src') - } - }, - plugins: [vue()] +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), '') + // 从 VITE_API_BASE_URL 推导网关源(去掉末尾 /api) + const apiBase = env.VITE_API_BASE_URL || 'http://localhost:8009/api' + const signalRBase = env.VITE_SIGNALR_BASE_URL || 'http://localhost:8009/chat' + const gatewayOrigin = new URL(apiBase).origin + const signalROrigin = new URL(signalRBase).origin + const signalRWebSocketOrigin = signalROrigin.replace(/^http/, 'ws') + const cspPlugin = { + name: 'environment-csp', + transformIndexHtml: (html) => html + .replaceAll('__API_ORIGIN__', gatewayOrigin) + .replaceAll('__SIGNALR_ORIGIN__', signalROrigin) + .replaceAll('__SIGNALR_WS_ORIGIN__', signalRWebSocketOrigin) + } + + return { + main: {}, + preload: {}, + renderer: { + server: { + host: true, + // 开发环境代理,规避浏览器 CORS(Electron 内不受影响) + proxy: { + '/api': { target: gatewayOrigin, changeOrigin: true }, + '/chat': { target: gatewayOrigin, changeOrigin: true, ws: true } + } + }, + resolve: { + alias: { + '@': resolve('src/renderer/src') + } + }, + plugins: [vue(), vueDevTools(), cspPlugin] + } } }) diff --git a/frontend/pc/IM/package-lock.json b/frontend/pc/IM/package-lock.json index 8efbf98..fb54d1c 100644 --- a/frontend/pc/IM/package-lock.json +++ b/frontend/pc/IM/package-lock.json @@ -16,6 +16,7 @@ "@vuelidate/core": "^2.0.3", "@vuelidate/validators": "^2.0.4", "axios": "^1.13.2", + "crypto": "^1.0.1", "electron-updater": "^6.3.9", "feather-icons": "^4.29.2", "hevue-img-preview": "^7.1.3", @@ -38,6 +39,8 @@ "eslint-plugin-vue": "^10.6.2", "prettier": "^3.7.4", "vite": "^7.2.6", + "vite-plugin-vue-devtools": "^8.0.7", + "vitest": "^5.0.0", "vue": "^3.5.25", "vue-eslint-parser": "^10.2.0" } @@ -115,6 +118,19 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmmirror.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-compilation-targets": { "version": "7.28.6", "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", @@ -132,6 +148,28 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", + "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.28.6", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/helper-globals": { "version": "7.28.0", "resolved": "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz", @@ -142,6 +180,20 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-module-imports": { "version": "7.28.6", "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", @@ -174,6 +226,19 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-plugin-utils": { "version": "7.28.6", "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", @@ -184,6 +249,38 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", + "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -241,6 +338,101 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-proposal-decorators": { + "version": "7.29.0", + "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.29.0.tgz", + "integrity": "sha512-CVBVv3VY/XRMxRYq5dwr2DS7/MvqPm23cOCjbwNnVrfOqcWlnefua1uUs0sjdKOGjvPUG633o07uWzJq4oI6dA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-syntax-decorators": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.28.6.tgz", + "integrity": "sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-arrow-functions": { "version": "7.27.1", "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", @@ -257,6 +449,26 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", + "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.28.6.tgz", @@ -1863,6 +2075,13 @@ "url": "https://opencollective.com/pkgr" } }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmmirror.com/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-rc.2", "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz", @@ -2256,6 +2475,17 @@ "@types/responselike": "^1.0.0" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmmirror.com/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmmirror.com/@types/debug/-/debug-4.1.12.tgz", @@ -2266,6 +2496,13 @@ "@types/ms": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", @@ -2377,6 +2614,117 @@ "vue": "^3.2.25" } }, + "node_modules/@vitest/mocker": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/@vitest/mocker/-/mocker-5.0.0.tgz", + "integrity": "sha512-66PGTMIiVJP3t4a5yxU9qPtf7MdTBs8jmToMvy+HVflB3Yy13WJZTtPePdvU+wjRV02SKK5doLbSA6o9pwOmiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.31", + "@vitest/spy": "5.0.0", + "estree-walker": "^3.0.3", + "magic-string": "^1.2.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/mocker/node_modules/magic-string": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-1.2.3.tgz", + "integrity": "sha512-Bpb0W2TbLKOZ7vJnOUnVRGq3WL2p+ISV29M6hYPL1AFCpyKZpdr5ytiXoTSSxRVhg8YW7f65+6gbG8WG6PCa/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/@vitest/spy": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/@vitest/spy/-/spy-5.0.0.tgz", + "integrity": "sha512-uy+luWBAPw9XfthoHi5AkfHUnuPYEESjl0p/r+meoBnU8bxg5GDQ3Ey8MjcJ6sqahkL4PFyrvfMJJBw7LbU06g==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vue/babel-helper-vue-transform-on": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.5.0.tgz", + "integrity": "sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/babel-plugin-jsx": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.5.0.tgz", + "integrity": "sha512-mneBhw1oOqCd2247O0Yw/mRwC9jIGACAJUlawkmMBiNmL4dGA2eMzuNZVNqOUfYTa6vqmND4CtOPzmEEEqLKFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.2", + "@vue/babel-helper-vue-transform-on": "1.5.0", + "@vue/babel-plugin-resolve-type": "1.5.0", + "@vue/shared": "^3.5.18" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + } + } + }, + "node_modules/@vue/babel-plugin-resolve-type": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-1.5.0.tgz", + "integrity": "sha512-Wm/60o+53JwJODm4Knz47dxJnLDJ9FnKnGZJbUUf8nQRAtt6P+undLUAVU3Ha33LxOJe6IPoifRQ6F/0RrU31w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/parser": "^7.28.0", + "@vue/compiler-sfc": "^3.5.18" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@vue/compiler-core": { "version": "3.5.28", "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.28.tgz", @@ -2436,6 +2784,47 @@ "@vue/devtools-kit": "^7.7.9" } }, + "node_modules/@vue/devtools-core": { + "version": "8.0.7", + "resolved": "https://registry.npmmirror.com/@vue/devtools-core/-/devtools-core-8.0.7.tgz", + "integrity": "sha512-PmpiPxvg3Of80ODHVvyckxwEW1Z02VIAvARIZS1xegINn3VuNQLm9iHUmKD+o6cLkMNWV8OG8x7zo0kgydZgdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^8.0.7", + "@vue/devtools-shared": "^8.0.7" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/@vue/devtools-core/node_modules/@vue/devtools-kit": { + "version": "8.0.7", + "resolved": "https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-8.0.7.tgz", + "integrity": "sha512-H6esJGHGl5q0E9iV3m2EoBQHJ+V83WMW83A0/+Fn95eZ2iIvdsq4+UCS6yT/Fdd4cGZSchx/MdWDreM3WqMsDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^8.0.7", + "birpc": "^2.6.1", + "hookable": "^5.5.3", + "perfect-debounce": "^2.0.0" + } + }, + "node_modules/@vue/devtools-core/node_modules/@vue/devtools-shared": { + "version": "8.0.7", + "resolved": "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-8.0.7.tgz", + "integrity": "sha512-CgAb9oJH5NUmbQRdYDj/1zMiaICYSLtm+B1kxcP72LBrifGAjUmt8bx52dDH1gWRPlQgxGPqpAMKavzVirAEhA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/devtools-core/node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "dev": true, + "license": "MIT" + }, "node_modules/@vue/devtools-kit": { "version": "7.7.9", "resolved": "https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz", @@ -2723,6 +3112,16 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/ansis": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/ansis/-/ansis-4.2.0.tgz", + "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, "node_modules/app-builder-bin": { "version": "5.0.0-alpha.12", "resolved": "https://registry.npmmirror.com/app-builder-bin/-/app-builder-bin-5.0.0-alpha.12.tgz", @@ -2913,6 +3312,16 @@ "node": ">=0.8" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/astral-regex/-/astral-regex-2.0.0.tgz", @@ -3212,6 +3621,22 @@ "node": ">= 10.0.0" } }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cac": { "version": "6.7.14", "resolved": "https://registry.npmmirror.com/cac/-/cac-6.7.14.tgz", @@ -3361,6 +3786,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmmirror.com/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", @@ -3656,6 +4091,13 @@ "node": ">= 8" } }, + "node_modules/crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", + "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.", + "license": "ISC" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz", @@ -3739,6 +4181,36 @@ "dev": true, "license": "MIT" }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmmirror.com/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/defaults": { "version": "1.0.4", "resolved": "https://registry.npmmirror.com/defaults/-/defaults-1.0.4.tgz", @@ -3779,6 +4251,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/define-properties": { "version": "1.2.1", "resolved": "https://registry.npmmirror.com/define-properties/-/define-properties-1.2.1.tgz", @@ -4355,6 +4840,16 @@ "dev": true, "license": "MIT" }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", @@ -4373,6 +4868,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -4838,6 +5340,16 @@ "node": ">=12.0.0" } }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/exponential-backoff": { "version": "3.1.3", "resolved": "https://registry.npmmirror.com/exponential-backoff/-/exponential-backoff-3.1.3.tgz", @@ -5671,6 +6183,22 @@ "node": ">= 12" } }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", @@ -5704,6 +6232,25 @@ "node": ">=0.10.0" } }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-interactive": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/is-interactive/-/is-interactive-1.0.0.tgz", @@ -5739,6 +6286,22 @@ "url": "https://github.com/sponsors/mesqueeb" } }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isbinaryfile": { "version": "5.0.7", "resolved": "https://registry.npmmirror.com/isbinaryfile/-/isbinaryfile-5.0.7.tgz", @@ -5902,6 +6465,13 @@ "json-buffer": "3.0.1" } }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lazy-val": { "version": "1.0.5", "resolved": "https://registry.npmmirror.com/lazy-val/-/lazy-val-1.0.5.tgz", @@ -6313,6 +6883,16 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", @@ -6533,6 +7113,27 @@ "node": ">= 0.4" } }, + "node_modules/obug": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmmirror.com/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "dev": true, + "license": "MIT" + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", @@ -6558,6 +7159,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmmirror.com/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz", @@ -6728,6 +7348,13 @@ "dev": true, "license": "ISC" }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/pe-library": { "version": "0.4.1", "resolved": "https://registry.npmmirror.com/pe-library/-/pe-library-0.4.1.tgz", @@ -6762,9 +7389,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.7", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", "engines": { @@ -7225,6 +7852,19 @@ "fsevents": "~2.3.2" } }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmmirror.com/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -7333,6 +7973,13 @@ "node": ">=8" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz", @@ -7366,6 +8013,21 @@ "node": ">=10" } }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/slice-ansi": { "version": "3.0.0", "resolved": "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-3.0.0.tgz", @@ -7488,6 +8150,13 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmmirror.com/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, "node_modules/stat-mode": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/stat-mode/-/stat-mode-1.0.0.tgz", @@ -7498,6 +8167,13 @@ "node": ">= 6" } }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz", @@ -7755,15 +8431,35 @@ "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==", "license": "MIT" }, + "node_modules/tinybench": { + "version": "6.1.4", + "resolved": "https://registry.npmmirror.com/tinybench/-/tinybench-6.1.4.tgz", + "integrity": "sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.17", + "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -7798,6 +8494,16 @@ "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==", "license": "MIT" }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/tough-cookie": { "version": "4.1.4", "resolved": "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-4.1.4.tgz", @@ -7911,6 +8617,23 @@ "node": ">= 4.0.0" } }, + "node_modules/unplugin-utils": { + "version": "0.3.1", + "resolved": "https://registry.npmmirror.com/unplugin-utils/-/unplugin-utils-0.3.1.tgz", + "integrity": "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==", + "dev": true, + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -8092,6 +8815,144 @@ } } }, + "node_modules/vite-dev-rpc": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/vite-dev-rpc/-/vite-dev-rpc-1.1.0.tgz", + "integrity": "sha512-pKXZlgoXGoE8sEKiKJSng4hI1sQ4wi5YT24FCrwrLt6opmkjlqPPVmiPWWJn8M8byMxRGzp1CrFuqQs4M/Z39A==", + "dev": true, + "license": "MIT", + "dependencies": { + "birpc": "^2.4.0", + "vite-hot-client": "^2.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.1 || ^7.0.0-0" + } + }, + "node_modules/vite-hot-client": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/vite-hot-client/-/vite-hot-client-2.1.0.tgz", + "integrity": "sha512-7SpgZmU7R+dDnSmvXE1mfDtnHLHQSisdySVR7lO8ceAXvM0otZeuQQ6C8LrS5d/aYyP/QZ0hI0L+dIPrm4YlFQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0" + } + }, + "node_modules/vite-plugin-inspect": { + "version": "11.3.3", + "resolved": "https://registry.npmmirror.com/vite-plugin-inspect/-/vite-plugin-inspect-11.3.3.tgz", + "integrity": "sha512-u2eV5La99oHoYPHE6UvbwgEqKKOQGz86wMg40CCosP6q8BkB6e5xPneZfYagK4ojPJSj5anHCrnvC20DpwVdRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansis": "^4.1.0", + "debug": "^4.4.1", + "error-stack-parser-es": "^1.0.5", + "ohash": "^2.0.11", + "open": "^10.2.0", + "perfect-debounce": "^2.0.0", + "sirv": "^3.0.1", + "unplugin-utils": "^0.3.0", + "vite-dev-rpc": "^1.1.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + } + } + }, + "node_modules/vite-plugin-inspect/node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite-plugin-vue-devtools": { + "version": "8.0.7", + "resolved": "https://registry.npmmirror.com/vite-plugin-vue-devtools/-/vite-plugin-vue-devtools-8.0.7.tgz", + "integrity": "sha512-BWj/ykGpqVAJVdPyHmSTUm44buz3jPv+6jnvuFdQSRH0kAgP1cEIE4doHiFyqHXOmuB5EQVR/nh2g9YRiRNs9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-core": "^8.0.7", + "@vue/devtools-kit": "^8.0.7", + "@vue/devtools-shared": "^8.0.7", + "sirv": "^3.0.2", + "vite-plugin-inspect": "^11.3.3", + "vite-plugin-vue-inspector": "^5.3.2" + }, + "engines": { + "node": ">=v14.21.3" + }, + "peerDependencies": { + "vite": "^6.0.0 || ^7.0.0-0 || ^8.0.0-0" + } + }, + "node_modules/vite-plugin-vue-devtools/node_modules/@vue/devtools-kit": { + "version": "8.0.7", + "resolved": "https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-8.0.7.tgz", + "integrity": "sha512-H6esJGHGl5q0E9iV3m2EoBQHJ+V83WMW83A0/+Fn95eZ2iIvdsq4+UCS6yT/Fdd4cGZSchx/MdWDreM3WqMsDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^8.0.7", + "birpc": "^2.6.1", + "hookable": "^5.5.3", + "perfect-debounce": "^2.0.0" + } + }, + "node_modules/vite-plugin-vue-devtools/node_modules/@vue/devtools-shared": { + "version": "8.0.7", + "resolved": "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-8.0.7.tgz", + "integrity": "sha512-CgAb9oJH5NUmbQRdYDj/1zMiaICYSLtm+B1kxcP72LBrifGAjUmt8bx52dDH1gWRPlQgxGPqpAMKavzVirAEhA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite-plugin-vue-devtools/node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite-plugin-vue-inspector": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/vite-plugin-vue-inspector/-/vite-plugin-vue-inspector-5.3.2.tgz", + "integrity": "sha512-YvEKooQcSiBTAs0DoYLfefNja9bLgkFM7NI2b07bE2SruuvX0MEa9cMaxjKVMkeCp5Nz9FRIdcN1rOdFVBeL6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.23.0", + "@babel/plugin-proposal-decorators": "^7.23.0", + "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-transform-typescript": "^7.22.15", + "@vue/babel-plugin-jsx": "^1.1.5", + "@vue/compiler-dom": "^3.3.4", + "kolorist": "^1.8.0", + "magic-string": "^0.30.4" + }, + "peerDependencies": { + "vite": "^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0" + } + }, "node_modules/vite/node_modules/@esbuild/aix-ppc64": { "version": "0.27.3", "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", @@ -8576,6 +9437,99 @@ "@esbuild/win32-x64": "0.27.3" } }, + "node_modules/vitest": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/vitest/-/vitest-5.0.0.tgz", + "integrity": "sha512-gpsMNoRhMjMktVxPtstOH4/PJuPyovVaMDr4oDilXaGH1EcqM2OE96SoHT2VIQ6fTGtTjqmHDrEu2X9RQiXf8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/mocker": "5.0.0", + "chai": "^6.2.2", + "es-module-lexer": "^2.3.2", + "expect-type": "^1.4.0", + "magic-string": "^1.2.3", + "obug": "^2.1.4", + "picomatch": "^4.0.7", + "std-env": "^4.2.0", + "tinybench": "6.1.4", + "tinyexec": "1.3.0", + "tinyglobby": "^0.2.17", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^22.12.0 || ^24.0.0 || >=26.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "5.0.0", + "@vitest/browser-preview": "5.0.0", + "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", + "@vitest/coverage-istanbul": "5.0.0", + "@vitest/coverage-v8": "5.0.0", + "@vitest/ui": "5.0.0", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/magic-string": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-1.2.3.tgz", + "integrity": "sha512-Bpb0W2TbLKOZ7vJnOUnVRGq3WL2p+ISV29M6hYPL1AFCpyKZpdr5ytiXoTSSxRVhg8YW7f65+6gbG8WG6PCa/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/vue": { "version": "3.5.28", "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.28.tgz", @@ -8697,6 +9651,23 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz", @@ -8771,6 +9742,22 @@ } } }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/xml-name-validator": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz", diff --git a/frontend/pc/IM/package.json b/frontend/pc/IM/package.json index 6134369..72c30a5 100644 --- a/frontend/pc/IM/package.json +++ b/frontend/pc/IM/package.json @@ -11,6 +11,7 @@ "start": "electron-vite preview", "dev": "electron-vite dev", "build": "electron-vite build", + "test": "vitest run", "postinstall": "electron-builder install-app-deps", "build:unpack": "npm run build && electron-builder --dir", "build:win": "npm run build && electron-builder --win", @@ -25,6 +26,7 @@ "@vuelidate/core": "^2.0.3", "@vuelidate/validators": "^2.0.4", "axios": "^1.13.2", + "crypto": "^1.0.1", "electron-updater": "^6.3.9", "feather-icons": "^4.29.2", "hevue-img-preview": "^7.1.3", @@ -47,6 +49,8 @@ "eslint-plugin-vue": "^10.6.2", "prettier": "^3.7.4", "vite": "^7.2.6", + "vite-plugin-vue-devtools": "^8.0.7", + "vitest": "^5.0.0", "vue": "^3.5.25", "vue-eslint-parser": "^10.2.0" } diff --git a/frontend/pc/IM/src/cache/cacheDir.js b/frontend/pc/IM/src/cache/cacheDir.js new file mode 100644 index 0000000..285532b --- /dev/null +++ b/frontend/pc/IM/src/cache/cacheDir.js @@ -0,0 +1,13 @@ +import { app } from "electron"; +import path from 'path'; + +export const CACHE_ROOT = path.join(app.getPath('userData'), 'resource_cache') + +export const PROTOCOL_HEAD = 'ql-im://' + +export const DIRS = { + Image: 'images', + Video: 'videos', + Voice: 'voices', + File: 'files', +} diff --git a/frontend/pc/IM/src/cache/cacheHandler.js b/frontend/pc/IM/src/cache/cacheHandler.js new file mode 100644 index 0000000..28d71c3 --- /dev/null +++ b/frontend/pc/IM/src/cache/cacheHandler.js @@ -0,0 +1,35 @@ +import path from "path"; +import { FILE_TYPE } from "../renderer/src/constants/fileTypeDefine"; +import { DIRS, CACHE_ROOT, PROTOCOL_HEAD } from "./cacheDir"; +import crypto from 'crypto' +import fs from 'fs-extra' +import axios from "axios"; + +export const getCacheResorce = async (url, type = FILE_TYPE.Image) => { + const hash = crypto.createHash('md5').update(url).digest('hex') + const subDir = hash.substring(0,2); + const targetPath = path.join(DIRS[type], subDir) + + const filePath = path.join(targetPath, hash) + + if(await fs.pathExists(path.join(CACHE_ROOT, filePath))){ + return PROTOCOL_HEAD + filePath.replaceAll('\\', '/') + } + + await fs.ensureDir(path.join(CACHE_ROOT, targetPath)) + + const writer = fs.createWriteStream(path.join(CACHE_ROOT, filePath)) + + const response = await axios({ + url, + method: 'GET', + responseType: 'stream' + }) + + response.data.pipe(writer) + + return new Promise((resolve, reject) => { + writer.on('finish', () => resolve(PROTOCOL_HEAD + filePath.replaceAll('\\', '/'))); + writer.on('error', reject); + }); +} diff --git a/frontend/pc/IM/src/cache/protocolReg.js b/frontend/pc/IM/src/cache/protocolReg.js new file mode 100644 index 0000000..72c8d76 --- /dev/null +++ b/frontend/pc/IM/src/cache/protocolReg.js @@ -0,0 +1,13 @@ +import { net, protocol } from 'electron' +import { CACHE_ROOT } from './cacheDir' +import path from 'path' + +export const addProtocolHandler = () => { + protocol.handle('ql-im', (request) => { + const url = request.url.replace('ql-im://', '') + + const filePath = path.join(CACHE_ROOT, url.replaceAll('/', '\\')) + + return net.fetch(`file://${filePath}`) + }) +} diff --git a/frontend/pc/IM/src/main/index.js b/frontend/pc/IM/src/main/index.js index f0878fd..805cfd1 100644 --- a/frontend/pc/IM/src/main/index.js +++ b/frontend/pc/IM/src/main/index.js @@ -1,22 +1,28 @@ import { app, shell, BrowserWindow, ipcMain } from 'electron' -import { join } from 'path' +import path 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' +import { registerCacheHandler } from './ipcHandlers/cache' +import { addProtocolHandler } from '../cache/protocolReg' +import { CACHE_ROOT } from '../cache/cacheDir' +import fs from 'fs-extra' + +let mainWindow = null function createWindow() { // Create the browser window. - const mainWindow = new BrowserWindow({ + 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 开发环境预览 + icon: path.join(__dirname, '../../resources/icon.png'), // Windows 开发环境预览 webPreferences: { - preload: join(__dirname, '../preload/index.js'), + preload: path.join(__dirname, '../preload/index.js'), sandbox: false } }) @@ -24,7 +30,7 @@ function createWindow() { createTry(mainWindow); mainWindow.on('ready-to-show', () => { - mainWindow.show() + // mainWindow.show() }) @@ -38,7 +44,7 @@ function createWindow() { if (is.dev && process.env['ELECTRON_RENDERER_URL']) { mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']) } else { - mainWindow.loadFile(join(__dirname, '../renderer/index.html')) + mainWindow.loadFile(path.join(__dirname, '../renderer/index.html')) } } @@ -49,6 +55,8 @@ app.whenReady().then(() => { // Set app user model id for windows electronApp.setAppUserModelId('com.electron') + addProtocolHandler() + // 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 @@ -59,7 +67,50 @@ app.whenReady().then(() => { // IPC test ipcMain.on('ping', () => console.log('pong')) - registerWindowHandler() + // 开机自启 + ipcMain.on('setting-autoStart', (_event, enable) => { + app.setLoginItemSettings({ openAtLogin: !!enable }) + }) + + // 清理磁盘文件缓存 + ipcMain.handle('cache-clear-disk', async () => { + try { + await fs.emptyDir(CACHE_ROOT) + return { success: true } + } catch (e) { + return { success: false, error: e.message } + } + }) + + // 获取磁盘缓存大小 + ipcMain.handle('cache-disk-size', async () => { + try { + let total = 0 + const walk = async (dir) => { + const entries = await fs.readdir(dir, { withFileTypes: true }) + for (const e of entries) { + const p = path.join(dir, e.name) + if (e.isDirectory()) { await walk(p) } + else { try { total += (await fs.stat(p)).size } catch { /* Ignore files removed during traversal. */ } } + } + } + if (await fs.pathExists(CACHE_ROOT)) await walk(CACHE_ROOT) + return { success: true, size: total } + } catch { + return { success: false, size: 0 } + } + }) + + // 新消息任务栏/托盘闪动 + ipcMain.on('window-flash', () => { + if (mainWindow && !mainWindow.isFocused()) { + mainWindow.flashFrame(true) + // 也可以设置托盘图标高亮 + } + }) + + registerWindowHandler() + registerCacheHandler() createWindow() diff --git a/frontend/pc/IM/src/main/ipcHandlers/cache.js b/frontend/pc/IM/src/main/ipcHandlers/cache.js new file mode 100644 index 0000000..6955b9c --- /dev/null +++ b/frontend/pc/IM/src/main/ipcHandlers/cache.js @@ -0,0 +1,8 @@ +import { ipcMain } from "electron"; +import { getCacheResorce } from "../../cache/cacheHandler"; + +export function registerCacheHandler(){ + ipcMain.handle('cache-get', (event, url, type) => { + return getCacheResorce(url, type) + }) +} diff --git a/frontend/pc/IM/src/main/ipcHandlers/window.js b/frontend/pc/IM/src/main/ipcHandlers/window.js index 2b71798..6c32000 100644 --- a/frontend/pc/IM/src/main/ipcHandlers/window.js +++ b/frontend/pc/IM/src/main/ipcHandlers/window.js @@ -6,7 +6,8 @@ import { is } from '@electron-toolkit/utils' export function registerWindowHandler() { const windowMapData = new Map() - ipcMain.on('window-action', (event, action) => { + //**窗口控件操作 */ + ipcMain.on('window-action', (event, action, data) => { const win = BrowserWindow.fromWebContents(event.sender) if (!win) return const actions = { @@ -20,14 +21,21 @@ export function registerWindowHandler() { win.destroy() } }, - isMaximized: () => win.isMaximized() + isMaximized: () => win.isMaximized(), + changeSize: () => { + win.setSize(data.width, data.height, true) + win.setResizable(data.resizable) + win.center() + }, + show: () => win.show() } actions[action]?.() }) - ipcMain.on('window-new', (event, { route, data }) => { + /**新开窗口 */ + ipcMain.on('window-new', (event, { route, data, width = 900, height=670 }) => { const win = new BrowserWindow({ - width: 900, - height: 670, + width: width, + height: height, show: true, autoHideMenuBar: true, frame: false, diff --git a/frontend/pc/IM/src/main/trayHandler.js b/frontend/pc/IM/src/main/trayHandler.js index 6fb67f1..932f6e1 100644 --- a/frontend/pc/IM/src/main/trayHandler.js +++ b/frontend/pc/IM/src/main/trayHandler.js @@ -1,6 +1,5 @@ import { app, Tray, Menu, nativeImage } from 'electron' import path from 'path' -import { useRouter } from 'vue-router'; let tray = null; diff --git a/frontend/pc/IM/src/preload/index.js b/frontend/pc/IM/src/preload/index.js index 923143a..a6a56d5 100644 --- a/frontend/pc/IM/src/preload/index.js +++ b/frontend/pc/IM/src/preload/index.js @@ -8,9 +8,18 @@ const api = { maximize: () => ipcRenderer.send('window-action', 'maximize'), close: () => ipcRenderer.send('window-action', 'close'), closeThis: () => ipcRenderer.send('window-action', 'closeThis'), + show: () => ipcRenderer.send('window-action', 'show'), isMaximized: () => ipcRenderer.send('window-action', 'isMaximized'), - newWindow: (route, data) => ipcRenderer.send('window-new', { route, data }), - getWindowData: (winId) => ipcRenderer.invoke('get-window-data', winId) + newWindow: (route, data, width, height) => ipcRenderer.send('window-new', { route, data, width, height }), + getWindowData: (winId) => ipcRenderer.invoke('get-window-data', winId), + setMainSize: (width, height, resizable = true) => + ipcRenderer.send('window-action', 'changeSize', { width, height, resizable }), + flash: () => ipcRenderer.send('window-flash') + }, + cache: { + getCache: (url, type) => ipcRenderer.invoke('cache-get', url, type), + clearDisk: () => ipcRenderer.invoke('cache-clear-disk'), + diskSize: () => ipcRenderer.invoke('cache-disk-size') } } diff --git a/frontend/pc/IM/src/renderer/index.html b/frontend/pc/IM/src/renderer/index.html index 6d8cfaa..f049aae 100644 --- a/frontend/pc/IM/src/renderer/index.html +++ b/frontend/pc/IM/src/renderer/index.html @@ -5,13 +5,13 @@ Electron + media-src 'self' blob: __API_ORIGIN__ ql-im:"> diff --git a/frontend/pc/IM/src/renderer/src/App.vue b/frontend/pc/IM/src/renderer/src/App.vue index 34d8f73..26b97d6 100644 --- a/frontend/pc/IM/src/renderer/src/App.vue +++ b/frontend/pc/IM/src/renderer/src/App.vue @@ -8,7 +8,6 @@ diff --git a/frontend/pc/IM/src/renderer/src/components/Dropdown.vue b/frontend/pc/IM/src/renderer/src/components/Dropdown.vue new file mode 100644 index 0000000..71552d6 --- /dev/null +++ b/frontend/pc/IM/src/renderer/src/components/Dropdown.vue @@ -0,0 +1,252 @@ + + + + + diff --git a/frontend/pc/IM/src/renderer/src/components/MyButton.vue b/frontend/pc/IM/src/renderer/src/components/MyButton.vue index f7ea866..673e4bd 100644 --- a/frontend/pc/IM/src/renderer/src/components/MyButton.vue +++ b/frontend/pc/IM/src/renderer/src/components/MyButton.vue @@ -31,7 +31,7 @@ export default { diff --git a/frontend/pc/IM/src/renderer/src/components/addMenu.vue b/frontend/pc/IM/src/renderer/src/components/addMenu.vue index 66a61b2..143766b 100644 --- a/frontend/pc/IM/src/renderer/src/components/addMenu.vue +++ b/frontend/pc/IM/src/renderer/src/components/addMenu.vue @@ -20,7 +20,7 @@ \ No newline at end of file + +.icon-box.orange { + background: #faad14; +} + +.icon-box.green { + background: #52c41a; +} + +.icon-box.blue { + background: #1890ff; +} + diff --git a/frontend/pc/IM/src/renderer/src/components/electron/ImagePreview.vue b/frontend/pc/IM/src/renderer/src/components/electron/ImagePreview.vue index 50f75eb..4a24b6f 100644 --- a/frontend/pc/IM/src/renderer/src/components/electron/ImagePreview.vue +++ b/frontend/pc/IM/src/renderer/src/components/electron/ImagePreview.vue @@ -1,12 +1,12 @@ + + + diff --git a/frontend/pc/IM/src/renderer/src/components/groups/CreateGroup.vue b/frontend/pc/IM/src/renderer/src/components/groups/CreateGroup.vue index 3e2a68c..2ed22d1 100644 --- a/frontend/pc/IM/src/renderer/src/components/groups/CreateGroup.vue +++ b/frontend/pc/IM/src/renderer/src/components/groups/CreateGroup.vue @@ -1,41 +1,46 @@ \ No newline at end of file + diff --git a/frontend/pc/IM/src/renderer/src/components/groups/groupsShow.vue b/frontend/pc/IM/src/renderer/src/components/groups/groupsShow.vue index 7095cf4..fdb82a7 100644 --- a/frontend/pc/IM/src/renderer/src/components/groups/groupsShow.vue +++ b/frontend/pc/IM/src/renderer/src/components/groups/groupsShow.vue @@ -1,32 +1,47 @@ \ No newline at end of file + diff --git a/frontend/pc/IM/src/renderer/src/components/user/UserHoverCard.vue b/frontend/pc/IM/src/renderer/src/components/user/UserHoverCard.vue index e7ddb9a..b73c3c5 100644 --- a/frontend/pc/IM/src/renderer/src/components/user/UserHoverCard.vue +++ b/frontend/pc/IM/src/renderer/src/components/user/UserHoverCard.vue @@ -1,27 +1,26 @@ - - diff --git a/frontend/pc/IM/src/renderer/src/views/auth/Login.vue b/frontend/pc/IM/src/renderer/src/views/auth/Login.vue index 402c14b..adf0573 100644 --- a/frontend/pc/IM/src/renderer/src/views/auth/Login.vue +++ b/frontend/pc/IM/src/renderer/src/views/auth/Login.vue @@ -1,48 +1,48 @@ @@ -52,12 +52,12 @@ import { useMessage } from '@/components/messages/useAlert' import { authService } from '@/services/auth' import { useRouter } from 'vue-router' import feather from 'feather-icons' -import IconInput from '@/components/IconInput.vue' -import MyButton from '@/components/MyButton.vue' import { required, maxLength, helpers } from '@vuelidate/validators' import useVuelidate from '@vuelidate/core' import { useAuthStore } from '@/stores/auth' import { useSignalRStore } from '@/stores/signalr' +import WindowControls from '../../components/WindowControls.vue' +import { isElectron } from '../../utils/electronHelper' const message = useMessage(); const router = useRouter(); @@ -71,259 +71,244 @@ const form = reactive({ }) const rules = { - username:{ - required:helpers.withMessage('用户名不能为空', required), - maxLength:helpers.withMessage('用户名最大20字符', maxLength(20)) + username: { + required: helpers.withMessage('用户名不能为空', required), + maxLength: helpers.withMessage('用户名最大20字符', maxLength(20)) }, - password:{ - required:helpers.withMessage('密码不能为空', required), - maxLength:helpers.withMessage('密码最大50字符', maxLength(50)) + password: { + required: helpers.withMessage('密码不能为空', required), + maxLength: helpers.withMessage('密码最大50字符', maxLength(50)) } }; -const v$ = useVuelidate(rules,form); +const v$ = useVuelidate(rules, form); const handleLogin = async () => { const isFormCorrect = await v$.value.$validate() if (!isFormCorrect) { - if (v$.value.$errors.length > 0) { - message.error(v$.value.$errors[0].$message) - } - return + if (v$.value.$errors.length > 0) { + message.error(v$.value.$errors[0].$message) + } + return } - try{ + try { loading.value = true; const res = await authService.login(form); - if(res.code === 0){ // Assuming 0 is success + if (res.code === 0) { // Assuming 0 is success message.success('登录成功') - authStore.setLoginInfo(res.data.token, res.data.refreshToken, res.data.userInfo); + authStore.setLoginInfo(res.data); signalRStore.initSignalR(); router.push('/messages') - }else{ + } else { message.error(res.message || '登录失败') } } catch (e) { console.error(e) - } finally{ + } finally { loading.value = false; } } onMounted(() => { + if (isElectron()) { + window.api.window.setMainSize(360, 532, false) + window.api.window.show() + } feather.replace() + }) - - \ No newline at end of file diff --git a/frontend/pc/IM/src/renderer/src/views/auth/Register.vue b/frontend/pc/IM/src/renderer/src/views/auth/Register.vue index 0fd3f73..8c6dec7 100644 --- a/frontend/pc/IM/src/renderer/src/views/auth/Register.vue +++ b/frontend/pc/IM/src/renderer/src/views/auth/Register.vue @@ -1,49 +1,76 @@ diff --git a/frontend/pc/IM/src/renderer/src/views/contact/ContactList.vue b/frontend/pc/IM/src/renderer/src/views/contact/ContactList.vue index 13cb7ad..1dd57d0 100644 --- a/frontend/pc/IM/src/renderer/src/views/contact/ContactList.vue +++ b/frontend/pc/IM/src/renderer/src/views/contact/ContactList.vue @@ -7,17 +7,17 @@ - +
新的朋友
-
+
-
群聊
-
+
群聊通知
+
标签
@@ -27,7 +27,7 @@
- +
@@ -35,8 +35,8 @@
- @@ -48,37 +48,23 @@ import { ref, computed, onMounted } from 'vue' import GroupChatModal from '@/components/groups/GroupChatModal.vue' import feather from 'feather-icons'; import { useContactStore } from '@/stores/contact'; -import { useRouter } from 'vue-router'; +import { useGroupStore } from '@/stores/group'; import contactShow from '@/components/contacts/contactShow.vue'; import groupsShow from '@/components/groups/groupsShow.vue'; const searchQuery = ref('') const contactStore = useContactStore(); +const groupStore = useGroupStore(); const groupModal = ref(false); const contactTab = ref(0); -const myGroups = ref([ - { - id: 1, - name: "产品设计交流群", - avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=1", - lastMessage: "那个UI设计的初稿已经发在群文件了,大家记得看下。", - lastTime: "14:20", - unread: 3, - online: true - }, - { - id: 2, - name: "周五羽毛球小分队", - avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=2", - lastMessage: "这周五晚上 8 点,老地方见!", - lastTime: "昨天", - unread: 0, - online: false - } -]); +const myGroups = computed(() => { + const searchKey = searchQuery.value.toString().trim(); + if (!searchKey) return groupStore.groups; + return groupStore.groups.filter(g => (g.name || '').includes(searchKey)); +}); const filteredContacts = computed(() => { @@ -93,9 +79,10 @@ const filteredContacts = computed(() => { if (!c) return false const remark = c.remarkName || '' - const username = c.userInfo.username || '' + const nickname = c.nickName || '' + const userName = c.targetId || c.userName || '' - return remark.includes(searchKey) || username.includes(searchKey) + return remark.includes(searchKey) || nickname.includes(searchKey) || userName.includes(searchKey) }) }) @@ -104,14 +91,17 @@ const filteredContacts = computed(() => { // 发送事件给父组件(用于切换回聊天Tab并打开会话) const emit = defineEmits(['start-chat']) -const showGroupList = () => { - groupModal.value = true; +// 群聊弹窗选择回调 +const handleChatSelect = (group) => { + groupModal.value = false + emit('start-chat', group) } onMounted(async () => { await contactStore.loadContactList(); + await groupStore.loadGroups(); }) @@ -206,8 +196,8 @@ onMounted(async () => { } /* 去除 hover、active 等状态的效果 */ -a:hover, -a:active, +a:hover, +a:active, a:focus { text-decoration: none; color: inherit; /* 保持颜色不变 */ @@ -237,4 +227,4 @@ a:focus { .icon-box.orange { background: #faad14; } .icon-box.green { background: #52c41a; } .icon-box.blue { background: #1890ff; } - \ No newline at end of file + diff --git a/frontend/pc/IM/src/renderer/src/views/contact/FriendRequestList.vue b/frontend/pc/IM/src/renderer/src/views/contact/FriendRequestList.vue index 560641a..570435a 100644 --- a/frontend/pc/IM/src/renderer/src/views/contact/FriendRequestList.vue +++ b/frontend/pc/IM/src/renderer/src/views/contact/FriendRequestList.vue @@ -1,23 +1,27 @@ diff --git a/frontend/pc/IM/src/renderer/src/views/messages/messageContent/hooks/useRightClickHandler.js b/frontend/pc/IM/src/renderer/src/views/messages/messageContent/hooks/useRightClickHandler.js index 1200802..656476b 100644 --- a/frontend/pc/IM/src/renderer/src/views/messages/messageContent/hooks/useRightClickHandler.js +++ b/frontend/pc/IM/src/renderer/src/views/messages/messageContent/hooks/useRightClickHandler.js @@ -1,21 +1,28 @@ -export function useRightClickHandler() { - const items = [ - { - label: '查看资料', - action: () => console.log('打开之前的悬浮卡片', user) - }, - { - label: '发送消息', - action: () => console.log('进入私聊', user.id) - }, - { - label: '修改备注', - action: () => { } - }, - { - label: '删除好友', - type: 'danger', - action: () => alert('删除成功') - } - ]; -} \ No newline at end of file +import { MSG_TYPE } from '@/constants/MessageType' + +const buildMenu = (message, opts = {}) => { + const { onQuote, onForward, onDelete, onWithdraw, onCopy } = opts + const items = [] + + const isText = message.msgType === MSG_TYPE.Text + const isImage = message.msgType === MSG_TYPE.Image + + if (isText) { + items.push({ label: '复制', action: () => navigator.clipboard.writeText(message.content?.body?.text || message.content?.fallback || message.content || '') }) + } + if (isImage) { + items.push({ label: '复制图片', action: () => onCopy?.(message) }) + } + if (isText) { + items.push({ label: '引用', action: () => onQuote?.(message) }) + } + items.push({ label: '转发', action: () => onForward?.(message) }) + items.push({ label: '撤回', action: () => onWithdraw?.(message) }) + items.push({ label: '删除', type: 'danger', action: () => onDelete?.(message) }) + + return items +} + +export const useRightClickHandler = (message, opts) => { + return buildMenu(message, opts) +} diff --git a/frontend/pc/IM/src/renderer/src/views/messages/messageContent/hooks/useSendMessageHandler.js b/frontend/pc/IM/src/renderer/src/views/messages/messageContent/hooks/useSendMessageHandler.js index b8569bd..a72c3fd 100644 --- a/frontend/pc/IM/src/renderer/src/views/messages/messageContent/hooks/useSendMessageHandler.js +++ b/frontend/pc/IM/src/renderer/src/views/messages/messageContent/hooks/useSendMessageHandler.js @@ -1,109 +1,213 @@ import { useChatStore } from "@/stores/chat"; +import { useAuthStore } from "@/stores/auth"; import { generateSessionId } from "@/utils/sessionIdTools"; -import { MESSAGE_TYPE } from "@/constants/MessageType"; +import { CHAT_TYPE, MSG_TYPE } from "@/constants/MessageType"; import { messageService } from "@/services/message"; import { SYSTEM_BASE_STATUS } from "@/constants/systemBaseStatus"; import { uploadFile } from "@/services/upload/uploader"; import { UPLOAD_STATUS } from "@/constants/uploadStatus"; import { getMessageType } from "@/constants/fileTypeDefine"; import { uploadService } from "@/services/upload/uploadService"; -import { getFileHash } from "@/utils/uploadTools"; export function useSendMessageHandler() { + // 发送者恒等于当前登录用户,避免会话数据缺 userId 导致消息归属错误 + const myId = useAuthStore().userInfo?.id; + const sendMessage = async (msg) => { const chatStore = useChatStore(); - //设置消息为加载状态 - const msgServer = { ...msg } msg.isLoading = true; - //将临时消息推送到消息列表(存库,方便后续重试) - await chatStore.pushAndSortMessagesAsync([msg], generateSessionId(msg.senderId, msg.receiverId, msg.chatType == MESSAGE_TYPE.GROUP), true); - //从列表取出消息 - let updateMsg = msg; + const isGroupChat = msg.chatType == CHAT_TYPE.GROUP; + await chatStore.pushAndSortMessagesAsync( + [msg], + generateSessionId(msg.senderId, msg.targetId, isGroupChat), + true + ); + try { - const res = await messageService.sendMessage(msgServer); + const res = await messageService.sendMessage(msg); if (res.code != SYSTEM_BASE_STATUS.SUCCESS) { - updateMsg.isError = true; + msg.isError = true; } else { - //发送成功将后端生成的sequenceId更新 - updateMsg = res.data; + // 用服务端数据覆盖(保留本地 clientMsgId 等字段用于去重/重发) + const serverData = res.data; + Object.assign(msg, serverData); + msg.isError = false; } } catch { - updateMsg.isError = true; + msg.isError = true; } finally { - updateMsg.isLoading = false; - chatStore.pushAndSortMessagesAsync([updateMsg], generateSessionId(msg.senderId, msg.receiverId, msg.chatType == MESSAGE_TYPE.GROUP), true); - msg.isLoading = false; + await chatStore.pushAndSortMessagesAsync( + [msg], + generateSessionId(msg.senderId, msg.targetId, isGroupChat), + true + ); } } - const sendTextMessage = async (text, conversationInfo) => { + const sendTextMessage = async (text, conversationInfo, quoteMessageId = null) => { const msg = { - type: 'Text', // 消息类型,例如 'Text', 'Image', 'File' - chatType: conversationInfo.value.chatType, // 'PRIVATE' 或 'GROUP' - senderId: conversationInfo.value.userId, // 当前用户ID (对应 int) - receiverId: conversationInfo.value.targetId, // 接收者ID (对应 int) - content: text, - timeStamp: new Date(), // 对应 DateTime - msgId: self.crypto.randomUUID() + clientMsgId: self.crypto.randomUUID(), + chatType: conversationInfo.value.chatType, + targetId: conversationInfo.value.targetId, + msgType: MSG_TYPE.Text, + senderId: myId, + sequenceId: Date.now(), + text, // 顶层 text 供 /message/send(MessageSendRequest 校验 text 非空) + // 统一用 content 结构,本地消息与服务端返回格式一致 + content: { fallback: text, body: { text }, ext: {}, quote: null }, }; - //更新当前会话最新消息 - conversationInfo.value.lastMessage = msg.content; + if (quoteMessageId) msg.quoteMessageId = quoteMessageId; + conversationInfo.value.lastMessage = text; await sendMessage(msg); } - const sendFileMessage = async (file, conversationInfo, info, localUrl) => { - const chatStore = useChatStore(); - const msg = { - type: getMessageType(file.type), // 消息类型,例如 'Text', 'Image', 'File' - chatType: conversationInfo.value.chatType, // 'PRIVATE' 或 'GROUP' - senderId: conversationInfo.value.userId, // 当前用户ID (对应 int) - receiverId: conversationInfo.value.targetId, // 接收者ID (对应 int) - content: '', - timeStamp: new Date(), // 对应 DateTime - msgId: self.crypto.randomUUID(), - localUrl: localUrl, - progress: 0 - }; - //更新当前会话最新消息 - conversationInfo.value.lastMessage = info.text; - msg.isImgLoading = true; - await chatStore.pushAndSortMessagesAsync([msg], generateSessionId(msg.senderId, msg.receiverId, msg.chatType == MESSAGE_TYPE.GROUP), true); - if (info.thumb) { - const hash = await getFileHash(info.thumb); - try { - const { data } = await uploadService.uploadSmallFile(info.thumb, hash); - info.thumb = data.objectName; - } catch (e) { - console.error(e) - msg.isError = true; - msg.isLoading = false; - return; - } + // 重发失败消息 + const retryMessage = async (msg) => { + const chatStore = useChatStore() + msg.isError = false + msg.isLoading = true + const isGroupChat = msg.chatType == CHAT_TYPE.GROUP + // 兼容旧缓存中的纯文本 content,并保留新版结构化消息字段。 + const text = typeof msg.content === 'object' + ? (msg.content?.body?.text || msg.content?.fallback || '') + : (msg.content || '') + const apiMsg = { + clientMsgId: msg.clientMsgId || msg.msgId, + targetId: msg.targetId, + chatType: msg.chatType, + msgType: msg.msgType, + text, + url: msg.url, + thumb: msg.thumb, + width: msg.width, + height: msg.height, + duration: msg.duration, + fileId: msg.fileId, + fileName: msg.fileName, + fileSize: msg.fileSize, + fileFormat: msg.fileFormat, } - await uploadFile(file, { - onProgress: async (e) => { - if (!e.status) return; - switch (e.status) { - case UPLOAD_STATUS.MERGING: - case UPLOAD_STATUS.UPLOADING: - msg.progress = e.progress; - break; - case UPLOAD_STATUS.COMPLETE: - msg.progress = 100; - msg.isImgLoading = false; - info.fileId = e.taskId; - msg.content = JSON.stringify(info); - await sendMessage(msg); - break; - default: - break; - } + await chatStore.pushAndSortMessagesAsync( + [msg], + generateSessionId(msg.senderId, msg.targetId, isGroupChat), + true + ) + try { + const res = await messageService.sendMessage(apiMsg) + if (res.code != SYSTEM_BASE_STATUS.SUCCESS) { + msg.isError = true + } else { + Object.assign(msg, res.data) + msg.isError = false } - }); + } catch { + msg.isError = true + } finally { + msg.isLoading = false + await chatStore.pushAndSortMessagesAsync( + [msg], + generateSessionId(msg.senderId, msg.targetId, isGroupChat), + true + ) + } } - return { sendMessage, sendFileMessage, sendTextMessage }; + const sendFileMessage = async (file, conversationInfo, info, localUrl) => { + const chatStore = useChatStore(); + const isGroupChat = conversationInfo.value.chatType == CHAT_TYPE.GROUP; + const msgType = getMessageType(file.type); + const msgId = self.crypto.randomUUID(); + + const msg = { + clientMsgId: msgId, + chatType: conversationInfo.value.chatType, + targetId: conversationInfo.value.targetId, + msgType: msgType, + senderId: myId, + sequenceId: Date.now(), + localUrl: localUrl, + progress: 0, + isLoading: false, + isImgLoading: true, + }; + + conversationInfo.value.lastMessage = info.text || '[文件]'; + msg.isImgLoading = true; + await chatStore.pushAndSortMessagesAsync( + [msg], + generateSessionId(msg.senderId, msg.targetId, isGroupChat), + true + ); + + // 缩略图直传 + if (info.thumb instanceof Blob) { + try { + const thumbFile = info.thumb instanceof File + ? info.thumb + : new File([info.thumb], `thumb_${msgId}.jpg`, { type: info.thumb.type || 'image/jpeg' }); + const thumbRes = await uploadService.uploadSmallFile(thumbFile, true); + info.thumb = thumbRes.data?.url || thumbRes.data?.id || ''; + } catch (e) { + console.error('缩略图上传失败:', e); + info.thumb = ''; + } + } + + const conversationId = chatStore.activeConversationId; + + try { + const uploadedFile = await uploadFile(file, { + conversationId, + chatType: conversationInfo.value.chatType, + targetId: conversationInfo.value.targetId, + onProgress: async (e) => { + if (!e.status) return; + switch (e.status) { + case UPLOAD_STATUS.MERGING: + case UPLOAD_STATUS.UPLOADING: + msg.progress = e.progress; + break; + case UPLOAD_STATUS.UPLOADED: + msg.isImgLoading = false; + break; + case UPLOAD_STATUS.COMPLETE: + msg.progress = 100; + msg.isImgLoading = false; + break; + default: + break; + } + } + }); + msg.progress = 100; + msg.isImgLoading = false; + msg.fileId = uploadedFile.id; + msg.url = uploadedFile.url || ''; + msg.thumb = info.thumb || ''; + if (info.w) msg.width = info.w; + if (info.h) msg.height = info.h; + if (info.duration) msg.duration = Math.round(info.duration); + if (msgType === MSG_TYPE.File) { + msg.fileName = file.name; + msg.fileSize = file.size; + msg.fileFormat = file.type || file.name.split('.').pop() || 'application/octet-stream'; + } + await sendMessage(msg); + } catch (e) { + console.error('文件上传失败:', e); + msg.isError = true; + msg.isLoading = false; + msg.isImgLoading = false; + await chatStore.pushAndSortMessagesAsync( + [msg], + generateSessionId(msg.senderId, msg.targetId, isGroupChat), + true + ); + } + } + + return { sendMessage, sendFileMessage, sendTextMessage, retryMessage }; } diff --git a/frontend/pc/IM/src/renderer/src/views/settings/AccountSecurity.vue b/frontend/pc/IM/src/renderer/src/views/settings/AccountSecurity.vue index 3b29960..00c3d62 100644 --- a/frontend/pc/IM/src/renderer/src/views/settings/AccountSecurity.vue +++ b/frontend/pc/IM/src/renderer/src/views/settings/AccountSecurity.vue @@ -55,7 +55,7 @@ diff --git a/frontend/pc/IM/src/renderer/src/views/settings/NotificationEdit.vue b/frontend/pc/IM/src/renderer/src/views/settings/NotificationEdit.vue index e2dff8a..6504742 100644 --- a/frontend/pc/IM/src/renderer/src/views/settings/NotificationEdit.vue +++ b/frontend/pc/IM/src/renderer/src/views/settings/NotificationEdit.vue @@ -11,7 +11,7 @@ 当有新版本可用时提醒我 @@ -19,10 +19,10 @@
新消息提醒 - 收到好友或群组新消息时播放提示音 + 收到好友或群组新消息时播放提示音并显示通知
@@ -37,7 +37,7 @@ 在屏幕右下角显示通知气泡 @@ -48,7 +48,7 @@ 每周接收一次系统周报 @@ -63,30 +63,33 @@