diff --git a/backend/IMTest/bin/Debug/net8.0/IMTest.dll b/backend/IMTest/bin/Debug/net8.0/IMTest.dll index a0cda45..20b497b 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 f9575cb..6695021 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 a3b4ee9..ad71984 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 d0ffae4..201e73a 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 4d12f0b..008f17a 100644 Binary files a/backend/IMTest/bin/Debug/net8.0/IM_API.pdb and b/backend/IMTest/bin/Debug/net8.0/IM_API.pdb differ diff --git a/backend/IMTest/bin/Debug/net8.0/appsettings.json b/backend/IMTest/bin/Debug/net8.0/appsettings.json index fc895fb..f1d60df 100644 --- a/backend/IMTest/bin/Debug/net8.0/appsettings.json +++ b/backend/IMTest/bin/Debug/net8.0/appsettings.json @@ -22,5 +22,9 @@ "Port": 5672, "Username": "test", "Password": "123456" + }, + "FileUploadOptions": { + "DefaultStorage": "Local", + "ChunkSize": 10, } } diff --git a/backend/IMTest/obj/Debug/net8.0/IMTest.AssemblyInfo.cs b/backend/IMTest/obj/Debug/net8.0/IMTest.AssemblyInfo.cs index 2a2b83e..758c389 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+dc6ecf224df4e8714171e8b5d23afaa90b3a1f81")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+d42956051108811cdd7be6a07af6b11e1d7f0d15")] [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 3735720..4919883 100644 --- a/backend/IMTest/obj/Debug/net8.0/IMTest.AssemblyInfoInputs.cache +++ b/backend/IMTest/obj/Debug/net8.0/IMTest.AssemblyInfoInputs.cache @@ -1 +1 @@ -1b9e709aa84e0b4f6260cd10cf25bfc3a30c60e75a3966fc7d4cdf489eae898b +fa24b386648cc4dba48ae5e3f91e5303b0dfd0971bba62bc27ca1580a5064337 diff --git a/backend/IMTest/obj/Debug/net8.0/IMTest.assets.cache b/backend/IMTest/obj/Debug/net8.0/IMTest.assets.cache index b61f83e..f6caa4d 100644 Binary files a/backend/IMTest/obj/Debug/net8.0/IMTest.assets.cache and b/backend/IMTest/obj/Debug/net8.0/IMTest.assets.cache differ 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 278b1a7..30142a3 100644 Binary files a/backend/IMTest/obj/Debug/net8.0/IMTest.csproj.AssemblyReference.cache and b/backend/IMTest/obj/Debug/net8.0/IMTest.csproj.AssemblyReference.cache differ diff --git a/backend/IMTest/obj/Debug/net8.0/IMTest.csproj.CoreCompileInputs.cache b/backend/IMTest/obj/Debug/net8.0/IMTest.csproj.CoreCompileInputs.cache index c05b415..d84992b 100644 --- a/backend/IMTest/obj/Debug/net8.0/IMTest.csproj.CoreCompileInputs.cache +++ b/backend/IMTest/obj/Debug/net8.0/IMTest.csproj.CoreCompileInputs.cache @@ -1 +1 @@ -6e6df2b3d9fe8d3830882bef146134864f65ca58bc5ea4bac684eaec55cfd628 +b2b545acb4173028f4d41b8d8c0aea03ecbcecb4eeabe997ca466c2e265beff6 diff --git a/backend/IMTest/obj/Debug/net8.0/IMTest.dll b/backend/IMTest/obj/Debug/net8.0/IMTest.dll index a0cda45..20b497b 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 f9575cb..6695021 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 9f9bfd6..5678266 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 9f9bfd6..5678266 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/IMTest/obj/IMTest.csproj.nuget.dgspec.json b/backend/IMTest/obj/IMTest.csproj.nuget.dgspec.json index 7422270..038561f 100644 --- a/backend/IMTest/obj/IMTest.csproj.nuget.dgspec.json +++ b/backend/IMTest/obj/IMTest.csproj.nuget.dgspec.json @@ -96,7 +96,7 @@ "privateAssets": "all" } }, - "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json" + "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.311/PortableRuntimeIdentifierGraph.json" } } }, @@ -199,6 +199,10 @@ "target": "Package", "version": "[2.3.2, )" }, + "SixLabors.ImageSharp": { + "target": "Package", + "version": "[3.1.12, )" + }, "StackExchange.Redis": { "target": "Package", "version": "[2.9.32, )" @@ -231,7 +235,7 @@ "privateAssets": "all" } }, - "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json" + "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.311/PortableRuntimeIdentifierGraph.json" } } } diff --git a/backend/IMTest/obj/IMTest.csproj.nuget.g.props b/backend/IMTest/obj/IMTest.csproj.nuget.g.props index a72789c..831aa67 100644 --- a/backend/IMTest/obj/IMTest.csproj.nuget.g.props +++ b/backend/IMTest/obj/IMTest.csproj.nuget.g.props @@ -7,7 +7,7 @@ $(UserProfile)\.nuget\packages\ C:\Users\nanxun\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages PackageReference - 6.14.1 + 6.14.2 diff --git a/backend/IMTest/obj/project.assets.json b/backend/IMTest/obj/project.assets.json index 4e1d539..de30454 100644 --- a/backend/IMTest/obj/project.assets.json +++ b/backend/IMTest/obj/project.assets.json @@ -1679,6 +1679,22 @@ } } }, + "SixLabors.ImageSharp/3.1.12": { + "type": "package", + "compile": { + "lib/net6.0/SixLabors.ImageSharp.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net6.0/SixLabors.ImageSharp.dll": { + "related": ".xml" + } + }, + "build": { + "build/_._": {} + } + }, "StackExchange.Redis/2.9.32": { "type": "package", "dependencies": { @@ -3027,6 +3043,7 @@ "Newtonsoft.Json": "13.0.4", "Pomelo.EntityFrameworkCore.MySql": "8.0.3", "RedLock.net": "2.3.2", + "SixLabors.ImageSharp": "3.1.12", "StackExchange.Redis": "2.9.32", "Swashbuckle.AspNetCore": "6.6.2", "System.IdentityModel.Tokens.Jwt": "8.14.0" @@ -5363,6 +5380,22 @@ "runtimes/ubuntu.16.10-x64/native/System.Security.Cryptography.Native.OpenSsl.so" ] }, + "SixLabors.ImageSharp/3.1.12": { + "sha512": "iAg6zifihXEFS/t7fiHhZBGAdCp3FavsF4i2ZIDp0JfeYeDVzvmlbY1CNhhIKimaIzrzSi5M/NBFcWvZT2rB/A==", + "type": "package", + "path": "sixlabors.imagesharp/3.1.12", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "LICENSE", + "build/SixLabors.ImageSharp.props", + "lib/net6.0/SixLabors.ImageSharp.dll", + "lib/net6.0/SixLabors.ImageSharp.xml", + "sixlabors.imagesharp.128.png", + "sixlabors.imagesharp.3.1.12.nupkg.sha512", + "sixlabors.imagesharp.nuspec" + ] + }, "StackExchange.Redis/2.9.32": { "sha512": "j5Rjbf7gWz5izrn0UWQy9RlQY4cQDPkwJfVqATnVsOa/+zzJrps12LOgacMsDl/Vit2f01cDiDkG/Rst8v2iGw==", "type": "package", @@ -8962,7 +8995,7 @@ "privateAssets": "all" } }, - "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.304/PortableRuntimeIdentifierGraph.json" + "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\9.0.311/PortableRuntimeIdentifierGraph.json" } } } diff --git a/backend/IMTest/obj/project.nuget.cache b/backend/IMTest/obj/project.nuget.cache index e5a6826..0841100 100644 --- a/backend/IMTest/obj/project.nuget.cache +++ b/backend/IMTest/obj/project.nuget.cache @@ -1,6 +1,6 @@ { "version": 2, - "dgSpecHash": "ueA0djhC8vQ=", + "dgSpecHash": "E2DnflEnEuk=", "success": true, "projectFilePath": "C:\\Users\\nanxun\\Documents\\IM\\backend\\IMTest\\IMTest.csproj", "expectedPackageFiles": [ @@ -97,6 +97,7 @@ "C:\\Users\\nanxun\\.nuget\\packages\\runtime.ubuntu.14.04-x64.runtime.native.system.security.cryptography.openssl\\4.3.0\\runtime.ubuntu.14.04-x64.runtime.native.system.security.cryptography.openssl.4.3.0.nupkg.sha512", "C:\\Users\\nanxun\\.nuget\\packages\\runtime.ubuntu.16.04-x64.runtime.native.system.security.cryptography.openssl\\4.3.0\\runtime.ubuntu.16.04-x64.runtime.native.system.security.cryptography.openssl.4.3.0.nupkg.sha512", "C:\\Users\\nanxun\\.nuget\\packages\\runtime.ubuntu.16.10-x64.runtime.native.system.security.cryptography.openssl\\4.3.0\\runtime.ubuntu.16.10-x64.runtime.native.system.security.cryptography.openssl.4.3.0.nupkg.sha512", + "C:\\Users\\nanxun\\.nuget\\packages\\sixlabors.imagesharp\\3.1.12\\sixlabors.imagesharp.3.1.12.nupkg.sha512", "C:\\Users\\nanxun\\.nuget\\packages\\stackexchange.redis\\2.9.32\\stackexchange.redis.2.9.32.nupkg.sha512", "C:\\Users\\nanxun\\.nuget\\packages\\swashbuckle.aspnetcore\\6.6.2\\swashbuckle.aspnetcore.6.6.2.nupkg.sha512", "C:\\Users\\nanxun\\.nuget\\packages\\swashbuckle.aspnetcore.swagger\\6.6.2\\swashbuckle.aspnetcore.swagger.6.6.2.nupkg.sha512", diff --git a/backend/IM_API/.gitignore b/backend/IM_API/.gitignore index 3e16852..8914895 100644 --- a/backend/IM_API/.gitignore +++ b/backend/IM_API/.gitignore @@ -1,3 +1,4 @@ bin/ obj/ -.vs/ \ No newline at end of file +.vs/ +uploads/ \ No newline at end of file diff --git a/backend/IM_API/Application/EventHandlers/MessageCreatedHandler/SignalREventHandler.cs b/backend/IM_API/Application/EventHandlers/MessageCreatedHandler/SignalREventHandler.cs index 8166f2f..cfbb2af 100644 --- a/backend/IM_API/Application/EventHandlers/MessageCreatedHandler/SignalREventHandler.cs +++ b/backend/IM_API/Application/EventHandlers/MessageCreatedHandler/SignalREventHandler.cs @@ -17,11 +17,13 @@ namespace IM_API.Application.EventHandlers.MessageCreatedHandler private readonly IHubContext _hub; private readonly IMapper _mapper; private readonly IUserService _userService; - public SignalREventHandler(IHubContext hub, IMapper mapper,IUserService userService) + public SignalREventHandler(IHubContext hub, IMapper mapper, + IUserService userService) { _hub = hub; _mapper = mapper; _userService = userService; + } public async Task Consume(ConsumeContext context) @@ -35,6 +37,10 @@ namespace IM_API.Application.EventHandlers.MessageCreatedHandler var senderinfo = await _userService.GetUserInfoAsync(@event.MsgSenderId); messageBaseVo.SenderName = senderinfo.NickName; messageBaseVo.SenderAvatar = senderinfo.Avatar ?? ""; + if (messageBaseVo.Type != MessageMsgType.Text) + { + messageBaseVo.Content = UrlTools.ProcessMessageUrl(messageBaseVo.Content, @event.BaseUrl); + } await _hub.Clients.Group(@event.StreamKey).SendAsync("ReceiveMessage", new HubResponse("Event", messageBaseVo)); } catch (Exception ex) diff --git a/backend/IM_API/Application/EventHandlers/UploadEventHandler/MergeEventHandler.cs b/backend/IM_API/Application/EventHandlers/UploadEventHandler/MergeEventHandler.cs new file mode 100644 index 0000000..72539bd --- /dev/null +++ b/backend/IM_API/Application/EventHandlers/UploadEventHandler/MergeEventHandler.cs @@ -0,0 +1,21 @@ +using IM_API.Domain.Events; +using IM_API.Interface.Services; +using MassTransit; + +namespace IM_API.Application.EventHandlers.UploadEventHandler +{ + public class MergeEventHandler : IConsumer + { + private readonly IStorageService _storage; + public MergeEventHandler(IStorageService storage) + { + _storage = storage; + } + + public async Task Consume(ConsumeContext context) + { + var @event = context.Message; + await _storage.MergeAsync(@event.TaskId, @event.ObjectName, @event.ChunckCount, @event.Parts); + } + } +} diff --git a/backend/IM_API/Configs/MQConfig.cs b/backend/IM_API/Configs/MQConfig.cs index bcac5d6..745e2b0 100644 --- a/backend/IM_API/Configs/MQConfig.cs +++ b/backend/IM_API/Configs/MQConfig.cs @@ -7,6 +7,7 @@ using IM_API.Application.EventHandlers.GroupRequestHandler; using IM_API.Application.EventHandlers.GroupRequestUpdateHandler; using IM_API.Application.EventHandlers.MessageCreatedHandler; using IM_API.Application.EventHandlers.RequestFriendHandler; +using IM_API.Application.EventHandlers.UploadEventHandler; using IM_API.Configs.Options; using IM_API.Domain.Events; using MassTransit; @@ -37,6 +38,7 @@ namespace IM_API.Configs x.AddConsumer(); x.AddConsumer(); x.AddConsumer(); + x.AddConsumer(); x.UsingRabbitMq((ctx,cfg) => { cfg.Host(options.Host, "/", h => diff --git a/backend/IM_API/Configs/MapperConfig.cs b/backend/IM_API/Configs/MapperConfig.cs index 9ba80cc..4ba8095 100644 --- a/backend/IM_API/Configs/MapperConfig.cs +++ b/backend/IM_API/Configs/MapperConfig.cs @@ -4,9 +4,12 @@ using IM_API.Dtos; using IM_API.Dtos.Auth; using IM_API.Dtos.Friend; using IM_API.Dtos.Group; +using IM_API.Dtos.Message; using IM_API.Dtos.User; using IM_API.Models; +using IM_API.Models.Upload; using IM_API.Tools; +using IM_API.VOs; using IM_API.VOs.Conversation; using IM_API.VOs.Message; @@ -171,6 +174,35 @@ namespace IM_API.Configs .ForMember(dest => dest.AuhorityEnum, opt => opt.MapFrom(src => GroupAuhority.REQUIRE_CONSENT)) .ForMember(dest => dest.StatusEnum, opt => opt.MapFrom(src => GroupStatus.Normal)) ; + + //上传任务模型转换 + CreateMap() + .ForMember(dest => dest.FileName, opt => opt.MapFrom(src => src.FileName)) + .ForMember(dest => dest.Status, opt => opt.MapFrom(src => UploadStatus.Created)) + .ForMember(dest => dest.Id, opt => opt.MapFrom(src => Guid.NewGuid())) + .ForMember(dest => dest.FileSize, opt => opt.MapFrom(src => src.FileSize)) + .ForMember(dest => dest.FileHash, opt => opt.MapFrom(src => src.FileHash)) + .ForMember(dest => dest.ContentType, opt => opt.MapFrom(src => src.ContentType)) + .ForMember(dest => dest.CreatedAt, opt => opt.MapFrom(src => DateTime.UtcNow)) + ; + + CreateMap() + .ForMember(dest => dest.TaskId, opt => opt.MapFrom(src => src.Id)) + .ForMember(dest => dest.ChunkSize, opt => opt.MapFrom(src => src.ChunkSize)) + .ForMember(dest => dest.TotalChunks, opt => opt.MapFrom(src => src.TotalChunks)) + .ForMember(dest => dest.Concurrency, opt => opt.MapFrom(src => 5)) + .ForMember(dest => dest.Skip, opt => opt.MapFrom(src => false)) + .ForMember(dest => dest.Url, opt => opt.MapFrom(src => src.ObjectName)) + ; + + CreateMap() + .ForMember(dest => dest.Url, opt => opt.MapFrom(src => src.ObjectName)) + .ForMember(dest => dest.FileId, opt => opt.MapFrom(src => src.Id)) + .ForMember(dest => dest.Provider, opt => opt.MapFrom(src => src.StorageProvider)) + .ForMember(dest => dest.Format, opt => opt.MapFrom(src => src.ContentType)) + .ForMember(dest => dest.Size, opt => opt.MapFrom(src => src.FileSize)); + + CreateMap(); } } } diff --git a/backend/IM_API/Configs/Options/FileUploadOptions.cs b/backend/IM_API/Configs/Options/FileUploadOptions.cs new file mode 100644 index 0000000..65554b1 --- /dev/null +++ b/backend/IM_API/Configs/Options/FileUploadOptions.cs @@ -0,0 +1,8 @@ +namespace IM_API.Configs.Options +{ + public class FileUploadOptions + { + public string DefaultStorage { get; set; } + public int ChunkSize { get; set; } + } +} diff --git a/backend/IM_API/Configs/ServiceCollectionExtensions.cs b/backend/IM_API/Configs/ServiceCollectionExtensions.cs index 4823103..57c6753 100644 --- a/backend/IM_API/Configs/ServiceCollectionExtensions.cs +++ b/backend/IM_API/Configs/ServiceCollectionExtensions.cs @@ -29,7 +29,8 @@ namespace IM_API.Configs services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(sp => diff --git a/backend/IM_API/Controllers/MessageController.cs b/backend/IM_API/Controllers/MessageController.cs index be09207..05501a8 100644 --- a/backend/IM_API/Controllers/MessageController.cs +++ b/backend/IM_API/Controllers/MessageController.cs @@ -3,6 +3,8 @@ using IM_API.Domain.Events; using IM_API.Dtos; using IM_API.Dtos.Message; using IM_API.Interface.Services; +using IM_API.Models; +using IM_API.Tools; using IM_API.VOs.Message; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; @@ -19,12 +21,12 @@ namespace IM_API.Controllers { private readonly IMessageSevice _messageService; private readonly ILogger _logger; - private readonly IEventBus _eventBus; - public MessageController(IMessageSevice messageService, ILogger logger, IEventBus eventBus) + public MessageController(IMessageSevice messageService, + ILogger logger) { _messageService = messageService; _logger = logger; - _eventBus = eventBus; + } [HttpPost] [ProducesResponseType(typeof(BaseResponse), StatusCodes.Status200OK)] @@ -32,15 +34,23 @@ namespace IM_API.Controllers { var userIdstr = User.FindFirstValue(ClaimTypes.NameIdentifier); MessageBaseVo messageBaseVo = new MessageBaseVo(); + var handledMessage = await _messageService.HandleFileMessageContentAsync(dto); if(dto.ChatType == Models.ChatType.PRIVATE) { - messageBaseVo = await _messageService.SendPrivateMessageAsync(int.Parse(userIdstr), dto.ReceiverId, dto); + messageBaseVo = await _messageService.SendPrivateMessageAsync(int.Parse(userIdstr), dto.ReceiverId, handledMessage); } else { - messageBaseVo = await _messageService.SendGroupMessageAsync(int.Parse(userIdstr), dto.ReceiverId, dto); + messageBaseVo = await _messageService.SendGroupMessageAsync(int.Parse(userIdstr), dto.ReceiverId, handledMessage); } - return Ok(new BaseResponse(messageBaseVo)); + + if (messageBaseVo.Type != MessageMsgType.Text) + { + var request = HttpContext?.Request; + var baseUrl = $"{request.Scheme}://{request.Host}"; + messageBaseVo.Content = UrlTools.ProcessMessageUrl(messageBaseVo.Content, baseUrl); + } + return Ok(new BaseResponse(messageBaseVo)); } [HttpGet] [ProducesResponseType(typeof(BaseResponse>), StatusCodes.Status200OK)] diff --git a/backend/IM_API/Controllers/UploadController.cs b/backend/IM_API/Controllers/UploadController.cs new file mode 100644 index 0000000..887866c --- /dev/null +++ b/backend/IM_API/Controllers/UploadController.cs @@ -0,0 +1,124 @@ +using IM_API.Dtos; +using IM_API.Interface.Services; +using IM_API.Models.Upload; +using IM_API.Tools; +using IM_API.VOs; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore.Storage; +using StackExchange.Redis; +using System.Text; +using System.Text.Json; +using IDatabase = StackExchange.Redis.IDatabase; + +namespace IM_API.Controllers +{ + [Authorize] + [Route("api/[controller]")] + [ApiController] + public class UploadController : ControllerBase + { + private readonly IWebHostEnvironment _env; + private readonly IStorageService _storage; + private readonly IDatabase _redis; + public UploadController(IWebHostEnvironment env, IStorageService storage, IConnectionMultiplexer connectionMultiplexer) + { + _env = env; + _storage = storage; + _redis = connectionMultiplexer.GetDatabase(); + } + [HttpPost("local/{taskId}/parts/{partNumber}")] + [ProducesResponseType(typeof(BaseResponse), StatusCodes.Status200OK)] + public async Task LocalUpload(Guid taskId, int partNumber, IFormFile file) + { + var baseDir = Path.Combine(_env.ContentRootPath, "uploads"); // 项目根目录下 uploads + Directory.CreateDirectory(baseDir); + + var path = Path.Combine(baseDir, "temp", taskId.ToString(), $"{partNumber}.part.tmp"); + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + + using var stream = System.IO.File.Create(path); + await file.CopyToAsync(stream); + + await _redis.SetAddAsync(RedisKeys.GetUploadPartKey(taskId), partNumber); + + return Ok(new BaseResponse()); + } + + [HttpPost("CreateTask")] + [ProducesResponseType(typeof(BaseResponse), StatusCodes.Status200OK)] + public async Task CreateUpload(CreateUploadTaskDto dto) + { + var vo = await _storage.InitTaskAsync(dto); + return Ok(new BaseResponse(vo)); + } + + [HttpPost("CreatePart")] + public async Task CreatePart(Guid taskId, int partNum) + { + var vo = await _storage.CreatePartInstructionAsync(taskId, partNum); + return Ok(new BaseResponse(vo)); + } + + [HttpPost("CompleteTask")] + public async Task CompleteTask([FromQuery]Guid taskId, [FromBody]List dtos) + { + var taskIdRes = await _storage.CompleteAsync(taskId, dtos); + return Ok(new BaseResponse(data: taskIdRes.ToString())); + } + + [HttpGet("events/{taskId}")] + [AllowAnonymous] + public async Task Events(Guid taskId) + { + Response.Headers.Add("Content-Type", "text/event-stream"); + Response.Headers.Add("Cache-Control", "no-cache"); + Response.Headers.Add("Connection", "keep-alive"); + var lastProgress = -1; + + while (!HttpContext.RequestAborted.IsCancellationRequested) + { + var hash = await _redis.HashGetAllAsync(RedisKeys.MergeStatus(taskId)); + if (hash.Length == 0) + { + await Task.Delay(1000); + continue; + } + var status = hash.FirstOrDefault(x => x.Name == "status").Value; + var progress = hash.FirstOrDefault(x => x.Name == "progress").Value; + var url = hash.FirstOrDefault(x => x.Name == "url").Value; + + // 避免重复发送 + if (progress != lastProgress) + { + var data = new + { + status = status.ToString(), + progress = progress.ToString(), + url = (string)url + }; + + await Response.WriteAsync($"data: {JsonSerializer.Serialize(data)}\n\n"); + await Response.Body.FlushAsync(); + + // 完成后关闭 SSE + if (status == "Completed") + break; + + await Task.Delay(1000); // 每秒检查一次 + } + } + } + + [HttpPost("upload/{hash}")] + public async Task UploadSmallFile(IFormFile file,string hash) + { + using var stream = file.OpenReadStream(); + var res = await _storage.UploadSmallFileAsync(stream, file.FileName, file.ContentType, file.Length, hash); + return Ok(new BaseResponse(res)); + } + + + } +} diff --git a/backend/IM_API/Domain/Events/MessageCreatedEvent.cs b/backend/IM_API/Domain/Events/MessageCreatedEvent.cs index b35e1c0..4484a89 100644 --- a/backend/IM_API/Domain/Events/MessageCreatedEvent.cs +++ b/backend/IM_API/Domain/Events/MessageCreatedEvent.cs @@ -16,6 +16,7 @@ namespace IM_API.Domain.Events public DateTimeOffset MessageCreated { get; set; } public string StreamKey { get; set; } public Guid ClientMsgId { get; set; } + public string BaseUrl { get; set; } diff --git a/backend/IM_API/Domain/Events/UploadMergeEvent.cs b/backend/IM_API/Domain/Events/UploadMergeEvent.cs new file mode 100644 index 0000000..109ecd5 --- /dev/null +++ b/backend/IM_API/Domain/Events/UploadMergeEvent.cs @@ -0,0 +1,13 @@ +using IM_API.Dtos; + +namespace IM_API.Domain.Events +{ + public record UploadMergeEvent : DomainEvent + { + public override string EventType => "IM.FILES_UPLOAD_MERGE"; + public Guid TaskId { get; init; } + public List Parts { get; init; } + public int ChunckCount { get; set; } + public string ObjectName { get; set; } + } +} diff --git a/backend/IM_API/Dtos/CreateUploadTaskDto.cs b/backend/IM_API/Dtos/CreateUploadTaskDto.cs new file mode 100644 index 0000000..ba9790b --- /dev/null +++ b/backend/IM_API/Dtos/CreateUploadTaskDto.cs @@ -0,0 +1,10 @@ +namespace IM_API.Dtos +{ + public class CreateUploadTaskDto + { + public string FileName { get; set; } = default!; + public long FileSize { get; set; } + public string ContentType { get; set; } = default!; + public string FileHash { get; set; } = default!; + } +} diff --git a/backend/IM_API/Dtos/Message/MessagTypeDto.cs b/backend/IM_API/Dtos/Message/MessagTypeDto.cs new file mode 100644 index 0000000..f3c6632 --- /dev/null +++ b/backend/IM_API/Dtos/Message/MessagTypeDto.cs @@ -0,0 +1,26 @@ +namespace IM_API.Dtos.Message +{ + public class RequestMessageType + { + public Guid FileId { get; set; } + public long Size { get; set; } + } + public class BaseMessageType: RequestMessageType + { + public string Url { get; set; } + public string Provider { get; set; } + public string Format { get; set; } + public string Text { get; set; } + } + public class ImageDto() : BaseMessageType + { + public string Thumb { get; set; } + public int W { get; set; } + public int H { get; set; } + } + + public class VideoDto() : ImageDto + { + public int Duration { get; set; } + } +} diff --git a/backend/IM_API/Dtos/MessageDto.cs b/backend/IM_API/Dtos/MessageDto.cs index 3606c48..163a464 100644 --- a/backend/IM_API/Dtos/MessageDto.cs +++ b/backend/IM_API/Dtos/MessageDto.cs @@ -10,7 +10,7 @@ namespace IM_API.Dtos public Guid MsgId { get; init; } public int SenderId { get; init; } public int ReceiverId { get; init; } - public string Content { get; init; } = default!; + public string Content { get; set; } = default!; public DateTimeOffset TimeStamp { get; init; } public MessageBaseDto() { } } diff --git a/backend/IM_API/Dtos/UploadPartDto.cs b/backend/IM_API/Dtos/UploadPartDto.cs new file mode 100644 index 0000000..be4b5c7 --- /dev/null +++ b/backend/IM_API/Dtos/UploadPartDto.cs @@ -0,0 +1,8 @@ +namespace IM_API.Dtos +{ + public class UploadPartDto + { + public int PartNumber { get; set; } + public string? ETag { get; set; } + } +} diff --git a/backend/IM_API/IM_API.csproj b/backend/IM_API/IM_API.csproj index 6361cdc..d8c87cc 100644 --- a/backend/IM_API/IM_API.csproj +++ b/backend/IM_API/IM_API.csproj @@ -28,6 +28,7 @@ + diff --git a/backend/IM_API/Interface/Services/IMessageSevice.cs b/backend/IM_API/Interface/Services/IMessageSevice.cs index 3f82049..2ce3cf7 100644 --- a/backend/IM_API/Interface/Services/IMessageSevice.cs +++ b/backend/IM_API/Interface/Services/IMessageSevice.cs @@ -48,6 +48,6 @@ namespace IM_API.Interface.Services Task MarkConversationAsReadAsync(int userId,int? userBId,int? groupId); Task RecallMessageAsync(int userId,int messageId); - + Task HandleFileMessageContentAsync(MessageBaseDto dto); } } diff --git a/backend/IM_API/Interface/Services/IStorageService.cs b/backend/IM_API/Interface/Services/IStorageService.cs new file mode 100644 index 0000000..c31f143 --- /dev/null +++ b/backend/IM_API/Interface/Services/IStorageService.cs @@ -0,0 +1,40 @@ +using IM_API.Dtos; +using IM_API.Models.Upload; +using IM_API.VOs; + +namespace IM_API.Interface.Services +{ + public interface IStorageService + { + string ProviderName { get; } + UploadMode Mode { get; } + /// + /// 初始化上传任务 + /// + /// + /// + Task InitTaskAsync(CreateUploadTaskDto dto); + /// + /// 创建分片任务 + /// + /// 文件上传任务ID + /// + /// + Task CreatePartInstructionAsync(Guid taskId, int partNumer); + Task CompleteAsync( + Guid taskId, + List parts + ); + + Task MergeAsync(Guid taskId, string objectName, int totalChunks, List parts); + + Task UploadSmallFileAsync(Stream stream, string fileName, string fileType, long size, string hash); + string GetDownloadUrl(string objectname); + } + public enum UploadMode + { + Proxy, // 本地 / 后端中转 + Direct // 云直传 + } + +} diff --git a/backend/IM_API/Interface/Services/IUploadTaskService.cs b/backend/IM_API/Interface/Services/IUploadTaskService.cs new file mode 100644 index 0000000..9f8c91d --- /dev/null +++ b/backend/IM_API/Interface/Services/IUploadTaskService.cs @@ -0,0 +1,12 @@ +using IM_API.Models.Upload; + +namespace IM_API.Interface.Services +{ + public interface IUploadTaskService + { + Task AddAsync(UploadTask task); + Task GetTaskAsync(Guid taskId); + Task GetTaskAsync(string hash); + Task UpdateStatusAsync(Guid taskId, UploadStatus status); + } +} diff --git a/backend/IM_API/Migrations/20260214101014_add-uploadtask.Designer.cs b/backend/IM_API/Migrations/20260214101014_add-uploadtask.Designer.cs new file mode 100644 index 0000000..cc4f2c0 --- /dev/null +++ b/backend/IM_API/Migrations/20260214101014_add-uploadtask.Designer.cs @@ -0,0 +1,1167 @@ +// +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("20260214101014_add-uploadtask")] + partial class adduploadtask + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .UseCollation("latin1_swedish_ci") + .HasAnnotation("ProductVersion", "8.0.21") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.HasCharSet(modelBuilder, "latin1"); + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("IM_API.Models.Admin", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetime") + .HasComment("创建时间 "); + + b.Property("Password") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .HasComment("密码"); + + b.Property("RoleId") + .HasColumnType("int(11)") + .HasComment("角色"); + + b.Property("State") + .HasColumnType("tinyint(4)") + .HasComment("状态(0:正常,2:封禁) "); + + b.Property("Updated") + .HasColumnType("datetime") + .HasComment("更新时间 "); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .HasComment("用户名"); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.HasIndex(new[] { "RoleId" }, "RoleId"); + + b.ToTable("admins", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.Conversation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ChatType") + .HasColumnType("int(11)"); + + b.Property("LastMessage") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasComment("最后一条最新消息"); + + b.Property("LastMessageTime") + .HasColumnType("datetime") + .HasComment("最后一条消息发送时间"); + + b.Property("LastReadSequenceId") + .HasColumnType("int(11)") + .HasColumnName("lastReadMessageId") + .HasComment("最后一条未读消息ID "); + + b.Property("MessageId") + .HasColumnType("int(11)"); + + b.Property("StreamKey") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasComment("消息推送唯一标识符"); + + b.Property("TargetId") + .HasColumnType("int(11)") + .HasComment("对方ID(群聊为群聊ID,单聊为单聊ID) "); + + b.Property("UnreadCount") + .HasColumnType("int(11)") + .HasComment("未读消息数 "); + + b.Property("UserId") + .HasColumnType("int(11)") + .HasComment("用户"); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.HasIndex("MessageId"); + + b.HasIndex(new[] { "LastReadSequenceId" }, "LastReadSequenceId"); + + b.HasIndex(new[] { "UserId" }, "Userid"); + + b.ToTable("conversations", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Dtype") + .HasColumnType("tinyint(4)") + .HasColumnName("DType") + .HasComment("设备类型(\r\n0:Android,1:Ios,2:PC,3:Pad,4:未知)"); + + b.Property("LastLogin") + .HasColumnType("datetime") + .HasComment("最后一次登录 "); + + b.Property("UserId") + .HasColumnType("int(11)") + .HasComment("设备所属用户 "); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.HasIndex(new[] { "UserId" }, "Userid") + .HasDatabaseName("Userid1"); + + b.ToTable("devices", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.File", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetime") + .HasComment("创建时间 "); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)") + .HasComment("文件类型 "); + + b.Property("MessageId") + .HasColumnType("int(11)") + .HasComment("关联消息ID "); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .HasComment("文件名 "); + + b.Property("Size") + .HasColumnType("int(11)") + .HasComment("文件大小(单位:KB) "); + + b.Property("Url") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("URL") + .HasComment("文件储存URL "); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.HasIndex(new[] { "MessageId" }, "Messageld"); + + b.ToTable("files", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.Friend", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Avatar") + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasComment("好友头像"); + + b.Property("Created") + .HasColumnType("datetime") + .HasComment("好友关系创建时间"); + + b.Property("FriendId") + .HasColumnType("int(11)") + .HasComment("用户2ID"); + + b.Property("RemarkName") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasComment("好友备注名"); + + b.Property("Status") + .HasColumnType("tinyint(4)") + .HasComment("当前好友关系状态\r\n(0:待通过,1:已添加,2:已拒绝,3:已拉黑)"); + + b.Property("UserId") + .HasColumnType("int(11)") + .HasComment("用户ID"); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.HasIndex(new[] { "Id" }, "ID"); + + b.HasIndex(new[] { "UserId", "FriendId" }, "Userld"); + + b.HasIndex(new[] { "FriendId" }, "用户2id"); + + b.ToTable("friends", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.FriendRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetime") + .HasComment("申请时间 "); + + b.Property("Description") + .HasColumnType("text") + .HasComment("申请附言 "); + + b.Property("RemarkName") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasComment("备注"); + + b.Property("RequestUser") + .HasColumnType("int(11)") + .HasComment("申请人 "); + + b.Property("ResponseUser") + .HasColumnType("int(11)") + .HasComment("被申请人 "); + + b.Property("State") + .HasColumnType("tinyint(4)") + .HasComment("申请状态(0:待通过,1:拒绝,2:同意,3:拉黑) "); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.HasIndex(new[] { "RequestUser" }, "RequestUser"); + + b.HasIndex(new[] { "ResponseUser" }, "ResponseUser"); + + b.ToTable("friend_request", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.Group", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AllMembersBanned") + .HasColumnType("tinyint(4)") + .HasComment("全员禁言(0允许发言,2全员禁言)"); + + b.Property("Announcement") + .HasColumnType("text") + .HasComment("群公告"); + + b.Property("Auhority") + .HasColumnType("tinyint(4)") + .HasComment("群权限\r\n(0:需管理员同意,1:任意人可加群,2:不允许任何人加入)"); + + b.Property("Avatar") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasComment("群头像"); + + b.Property("Created") + .HasColumnType("datetime") + .HasComment("群聊创建时间"); + + b.Property("GroupMaster") + .HasColumnType("int(11)") + .HasComment("群主"); + + b.Property("LastMessage") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("LastSenderName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("LastUpdateTime") + .HasColumnType("datetime(6)"); + + b.Property("MaxSequenceId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasComment("群聊名称"); + + b.Property("Status") + .HasColumnType("tinyint(4)") + .HasComment("群聊状态\r\n(1:正常,2:封禁)"); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.HasIndex(new[] { "GroupMaster" }, "GroupMaster"); + + b.HasIndex(new[] { "Id" }, "ID") + .HasDatabaseName("ID1"); + + b.ToTable("groups", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.GroupInvite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetime") + .HasComment("创建时间"); + + b.Property("GroupId") + .HasColumnType("int(11)") + .HasComment("群聊编号"); + + b.Property("InviteUser") + .HasColumnType("int(11)") + .HasComment("邀请用户"); + + b.Property("InvitedUser") + .HasColumnType("int(11)") + .HasComment("被邀请用户"); + + b.Property("State") + .HasColumnType("tinyint(4)") + .HasComment("当前状态(0:待被邀请人同意\r\n1:被邀请人已同意)"); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.HasIndex(new[] { "GroupId" }, "GroupId"); + + b.HasIndex(new[] { "InviteUser" }, "InviteUser"); + + b.HasIndex(new[] { "InvitedUser" }, "InvitedUser"); + + b.ToTable("group_invite", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.GroupMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Created") + .ValueGeneratedOnAdd() + .HasColumnType("datetime") + .HasDefaultValueSql("'1970-01-01 00:00:00'") + .HasComment("加入群聊时间"); + + b.Property("GroupId") + .HasColumnType("int(11)") + .HasComment("群聊编号"); + + b.Property("Role") + .HasColumnType("tinyint(4)") + .HasComment("成员角色(0:普通成员,1:管理员,2:群主)"); + + b.Property("UserId") + .HasColumnType("int(11)") + .HasComment("用户编号"); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.HasIndex(new[] { "GroupId" }, "Groupld"); + + b.HasIndex(new[] { "Id" }, "ID") + .HasDatabaseName("ID2"); + + b.HasIndex(new[] { "UserId" }, "Userld") + .HasDatabaseName("Userld1"); + + b.ToTable("group_member", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.GroupRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetime") + .HasComment("创建时间"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasComment("入群附言"); + + b.Property("GroupId") + .HasColumnType("int(11)") + .HasComment("群聊编号\r\n"); + + b.Property("State") + .HasColumnType("tinyint(4)") + .HasComment("申请状态(0:待管理员同意,1:已拒绝,2:已同意)"); + + b.Property("UserId") + .HasColumnType("int(11)") + .HasComment("申请人 "); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.HasIndex("UserId"); + + b.HasIndex(new[] { "GroupId" }, "GroupId") + .HasDatabaseName("GroupId1"); + + b.ToTable("group_request", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.LoginLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Dtype") + .HasColumnType("tinyint(4)") + .HasColumnName("DType") + .HasComment("设备类型(通Devices/DType) "); + + b.Property("Logined") + .HasColumnType("datetime") + .HasComment("登录时间 "); + + b.Property("State") + .HasColumnType("tinyint(4)") + .HasComment("登录状态(0:登陆成功,1:未验证,2:已被拒绝) "); + + b.Property("UserId") + .HasColumnType("int(11)") + .HasComment("登录用户 "); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.HasIndex(new[] { "UserId" }, "Userld") + .HasDatabaseName("Userld2"); + + b.ToTable("login_log", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ChatType") + .HasColumnType("tinyint(4)") + .HasComment("聊天类型\r\n(0:私聊,1:群聊)"); + + b.Property("ClientMsgId") + .HasColumnType("char(36)"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text") + .HasComment("消息内容 "); + + b.Property("Created") + .HasColumnType("datetime") + .HasComment("发送时间 "); + + b.Property("MsgType") + .HasColumnType("tinyint(4)") + .HasComment("消息类型\r\n(0:文本,1:图片,2:语音,3:视频,4:文件,5:语音聊天,6:视频聊天)"); + + b.Property("Recipient") + .HasColumnType("int(11)") + .HasComment("接收者(私聊为用户ID,群聊为群聊ID) "); + + b.Property("Sender") + .HasColumnType("int(11)") + .HasComment("发送者 "); + + b.Property("SequenceId") + .HasColumnType("bigint"); + + b.Property("State") + .HasColumnType("tinyint(4)") + .HasComment("消息状态(0:已发送,1:已撤回) "); + + b.Property("StreamKey") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasComment("消息推送唯一标识符"); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.HasIndex("SequenceId", "StreamKey") + .IsUnique(); + + b.HasIndex(new[] { "Sender" }, "Sender"); + + b.ToTable("messages", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasColumnType("text") + .HasComment("通知内容"); + + b.Property("Created") + .HasColumnType("datetime") + .HasComment("创建时间"); + + b.Property("Ntype") + .HasColumnType("tinyint(4)") + .HasColumnName("NType") + .HasComment("通知类型(0:文本)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasComment("通知标题"); + + b.Property("UserId") + .HasColumnType("int(11)") + .HasComment("接收人(为空为全体通知)"); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.HasIndex(new[] { "UserId" }, "Userld") + .HasDatabaseName("Userld3"); + + b.ToTable("notifications", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("int(11)") + .HasComment("权限编码 "); + + b.Property("Created") + .HasColumnType("datetime") + .HasComment("创建时间 "); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .HasComment("权限名称 "); + + b.Property("Ptype") + .HasColumnType("int(11)") + .HasColumnName("PType") + .HasComment("权限类型(0:增,1:删,2:改,3:查) "); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.ToTable("permissions", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.Permissionarole", b => + { + b.Property("Id") + .HasColumnType("int(11)") + .HasColumnName("ID"); + + b.Property("PermissionId") + .HasColumnType("int(11)") + .HasComment("权限 "); + + b.Property("RoleId") + .HasColumnType("int(11)") + .HasComment("角色 "); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.HasIndex(new[] { "PermissionId" }, "Permissionld"); + + b.HasIndex(new[] { "RoleId" }, "Roleld"); + + b.ToTable("permissionarole", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetime") + .HasComment("创建时间 "); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasComment("角色描述 "); + + b.Property("Name") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasComment("角色名称 "); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.ToTable("roles", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.Upload.UploadTask", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ChunkSize") + .HasColumnType("int"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FileHash") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("ObjectName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ProviderUploadId") + .HasColumnType("longtext"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("StorageProvider") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TotalChunks") + .HasColumnType("int"); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.ToTable("UploadTasks"); + }); + + 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/20260214101014_add-uploadtask.cs b/backend/IM_API/Migrations/20260214101014_add-uploadtask.cs new file mode 100644 index 0000000..8529d88 --- /dev/null +++ b/backend/IM_API/Migrations/20260214101014_add-uploadtask.cs @@ -0,0 +1,52 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace IM_API.Migrations +{ + /// + public partial class adduploadtask : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "UploadTasks", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + FileName = table.Column(type: "longtext", nullable: false, collation: "latin1_swedish_ci") + .Annotation("MySql:CharSet", "latin1"), + FileSize = table.Column(type: "bigint", nullable: false), + FileHash = table.Column(type: "longtext", nullable: false, collation: "latin1_swedish_ci") + .Annotation("MySql:CharSet", "latin1"), + ContentType = table.Column(type: "longtext", nullable: false, collation: "latin1_swedish_ci") + .Annotation("MySql:CharSet", "latin1"), + ChunkSize = table.Column(type: "int", nullable: false), + TotalChunks = table.Column(type: "int", nullable: false), + Status = table.Column(type: "int", nullable: false), + StorageProvider = table.Column(type: "longtext", nullable: false, collation: "latin1_swedish_ci") + .Annotation("MySql:CharSet", "latin1"), + ObjectName = table.Column(type: "longtext", nullable: false, collation: "latin1_swedish_ci") + .Annotation("MySql:CharSet", "latin1"), + ProviderUploadId = table.Column(type: "longtext", nullable: true, collation: "latin1_swedish_ci") + .Annotation("MySql:CharSet", "latin1"), + CreatedAt = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PRIMARY", x => x.Id); + }) + .Annotation("MySql:CharSet", "latin1") + .Annotation("Relational:Collation", "latin1_swedish_ci"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "UploadTasks"); + } + } +} diff --git a/backend/IM_API/Migrations/20260214131542_update-uploadtask.Designer.cs b/backend/IM_API/Migrations/20260214131542_update-uploadtask.Designer.cs new file mode 100644 index 0000000..91a065d --- /dev/null +++ b/backend/IM_API/Migrations/20260214131542_update-uploadtask.Designer.cs @@ -0,0 +1,1170 @@ +// +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("20260214131542_update-uploadtask")] + partial class updateuploadtask + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .UseCollation("latin1_swedish_ci") + .HasAnnotation("ProductVersion", "8.0.21") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.HasCharSet(modelBuilder, "latin1"); + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("IM_API.Models.Admin", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetime") + .HasComment("创建时间 "); + + b.Property("Password") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .HasComment("密码"); + + b.Property("RoleId") + .HasColumnType("int(11)") + .HasComment("角色"); + + b.Property("State") + .HasColumnType("tinyint(4)") + .HasComment("状态(0:正常,2:封禁) "); + + b.Property("Updated") + .HasColumnType("datetime") + .HasComment("更新时间 "); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .HasComment("用户名"); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.HasIndex(new[] { "RoleId" }, "RoleId"); + + b.ToTable("admins", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.Conversation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ChatType") + .HasColumnType("int(11)"); + + b.Property("LastMessage") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasComment("最后一条最新消息"); + + b.Property("LastMessageTime") + .HasColumnType("datetime") + .HasComment("最后一条消息发送时间"); + + b.Property("LastReadSequenceId") + .HasColumnType("int(11)") + .HasColumnName("lastReadMessageId") + .HasComment("最后一条未读消息ID "); + + b.Property("MessageId") + .HasColumnType("int(11)"); + + b.Property("StreamKey") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasComment("消息推送唯一标识符"); + + b.Property("TargetId") + .HasColumnType("int(11)") + .HasComment("对方ID(群聊为群聊ID,单聊为单聊ID) "); + + b.Property("UnreadCount") + .HasColumnType("int(11)") + .HasComment("未读消息数 "); + + b.Property("UserId") + .HasColumnType("int(11)") + .HasComment("用户"); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.HasIndex("MessageId"); + + b.HasIndex(new[] { "LastReadSequenceId" }, "LastReadSequenceId"); + + b.HasIndex(new[] { "UserId" }, "Userid"); + + b.ToTable("conversations", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Dtype") + .HasColumnType("tinyint(4)") + .HasColumnName("DType") + .HasComment("设备类型(\r\n0:Android,1:Ios,2:PC,3:Pad,4:未知)"); + + b.Property("LastLogin") + .HasColumnType("datetime") + .HasComment("最后一次登录 "); + + b.Property("UserId") + .HasColumnType("int(11)") + .HasComment("设备所属用户 "); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.HasIndex(new[] { "UserId" }, "Userid") + .HasDatabaseName("Userid1"); + + b.ToTable("devices", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.File", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetime") + .HasComment("创建时间 "); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)") + .HasComment("文件类型 "); + + b.Property("MessageId") + .HasColumnType("int(11)") + .HasComment("关联消息ID "); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .HasComment("文件名 "); + + b.Property("Size") + .HasColumnType("int(11)") + .HasComment("文件大小(单位:KB) "); + + b.Property("Url") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("URL") + .HasComment("文件储存URL "); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.HasIndex(new[] { "MessageId" }, "Messageld"); + + b.ToTable("files", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.Friend", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Avatar") + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasComment("好友头像"); + + b.Property("Created") + .HasColumnType("datetime") + .HasComment("好友关系创建时间"); + + b.Property("FriendId") + .HasColumnType("int(11)") + .HasComment("用户2ID"); + + b.Property("RemarkName") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasComment("好友备注名"); + + b.Property("Status") + .HasColumnType("tinyint(4)") + .HasComment("当前好友关系状态\r\n(0:待通过,1:已添加,2:已拒绝,3:已拉黑)"); + + b.Property("UserId") + .HasColumnType("int(11)") + .HasComment("用户ID"); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.HasIndex(new[] { "Id" }, "ID"); + + b.HasIndex(new[] { "UserId", "FriendId" }, "Userld"); + + b.HasIndex(new[] { "FriendId" }, "用户2id"); + + b.ToTable("friends", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.FriendRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetime") + .HasComment("申请时间 "); + + b.Property("Description") + .HasColumnType("text") + .HasComment("申请附言 "); + + b.Property("RemarkName") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasComment("备注"); + + b.Property("RequestUser") + .HasColumnType("int(11)") + .HasComment("申请人 "); + + b.Property("ResponseUser") + .HasColumnType("int(11)") + .HasComment("被申请人 "); + + b.Property("State") + .HasColumnType("tinyint(4)") + .HasComment("申请状态(0:待通过,1:拒绝,2:同意,3:拉黑) "); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.HasIndex(new[] { "RequestUser" }, "RequestUser"); + + b.HasIndex(new[] { "ResponseUser" }, "ResponseUser"); + + b.ToTable("friend_request", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.Group", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AllMembersBanned") + .HasColumnType("tinyint(4)") + .HasComment("全员禁言(0允许发言,2全员禁言)"); + + b.Property("Announcement") + .HasColumnType("text") + .HasComment("群公告"); + + b.Property("Auhority") + .HasColumnType("tinyint(4)") + .HasComment("群权限\r\n(0:需管理员同意,1:任意人可加群,2:不允许任何人加入)"); + + b.Property("Avatar") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasComment("群头像"); + + b.Property("Created") + .HasColumnType("datetime") + .HasComment("群聊创建时间"); + + b.Property("GroupMaster") + .HasColumnType("int(11)") + .HasComment("群主"); + + b.Property("LastMessage") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("LastSenderName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("LastUpdateTime") + .HasColumnType("datetime(6)"); + + b.Property("MaxSequenceId") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasComment("群聊名称"); + + b.Property("Status") + .HasColumnType("tinyint(4)") + .HasComment("群聊状态\r\n(1:正常,2:封禁)"); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.HasIndex(new[] { "GroupMaster" }, "GroupMaster"); + + b.HasIndex(new[] { "Id" }, "ID") + .HasDatabaseName("ID1"); + + b.ToTable("groups", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.GroupInvite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetime") + .HasComment("创建时间"); + + b.Property("GroupId") + .HasColumnType("int(11)") + .HasComment("群聊编号"); + + b.Property("InviteUser") + .HasColumnType("int(11)") + .HasComment("邀请用户"); + + b.Property("InvitedUser") + .HasColumnType("int(11)") + .HasComment("被邀请用户"); + + b.Property("State") + .HasColumnType("tinyint(4)") + .HasComment("当前状态(0:待被邀请人同意\r\n1:被邀请人已同意)"); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.HasIndex(new[] { "GroupId" }, "GroupId"); + + b.HasIndex(new[] { "InviteUser" }, "InviteUser"); + + b.HasIndex(new[] { "InvitedUser" }, "InvitedUser"); + + b.ToTable("group_invite", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.GroupMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Created") + .ValueGeneratedOnAdd() + .HasColumnType("datetime") + .HasDefaultValueSql("'1970-01-01 00:00:00'") + .HasComment("加入群聊时间"); + + b.Property("GroupId") + .HasColumnType("int(11)") + .HasComment("群聊编号"); + + b.Property("Role") + .HasColumnType("tinyint(4)") + .HasComment("成员角色(0:普通成员,1:管理员,2:群主)"); + + b.Property("UserId") + .HasColumnType("int(11)") + .HasComment("用户编号"); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.HasIndex(new[] { "GroupId" }, "Groupld"); + + b.HasIndex(new[] { "Id" }, "ID") + .HasDatabaseName("ID2"); + + b.HasIndex(new[] { "UserId" }, "Userld") + .HasDatabaseName("Userld1"); + + b.ToTable("group_member", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.GroupRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetime") + .HasComment("创建时间"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasComment("入群附言"); + + b.Property("GroupId") + .HasColumnType("int(11)") + .HasComment("群聊编号\r\n"); + + b.Property("State") + .HasColumnType("tinyint(4)") + .HasComment("申请状态(0:待管理员同意,1:已拒绝,2:已同意)"); + + b.Property("UserId") + .HasColumnType("int(11)") + .HasComment("申请人 "); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.HasIndex("UserId"); + + b.HasIndex(new[] { "GroupId" }, "GroupId") + .HasDatabaseName("GroupId1"); + + b.ToTable("group_request", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.LoginLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Dtype") + .HasColumnType("tinyint(4)") + .HasColumnName("DType") + .HasComment("设备类型(通Devices/DType) "); + + b.Property("Logined") + .HasColumnType("datetime") + .HasComment("登录时间 "); + + b.Property("State") + .HasColumnType("tinyint(4)") + .HasComment("登录状态(0:登陆成功,1:未验证,2:已被拒绝) "); + + b.Property("UserId") + .HasColumnType("int(11)") + .HasComment("登录用户 "); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.HasIndex(new[] { "UserId" }, "Userld") + .HasDatabaseName("Userld2"); + + b.ToTable("login_log", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ChatType") + .HasColumnType("tinyint(4)") + .HasComment("聊天类型\r\n(0:私聊,1:群聊)"); + + b.Property("ClientMsgId") + .HasColumnType("char(36)"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text") + .HasComment("消息内容 "); + + b.Property("Created") + .HasColumnType("datetime") + .HasComment("发送时间 "); + + b.Property("MsgType") + .HasColumnType("tinyint(4)") + .HasComment("消息类型\r\n(0:文本,1:图片,2:语音,3:视频,4:文件,5:语音聊天,6:视频聊天)"); + + b.Property("Recipient") + .HasColumnType("int(11)") + .HasComment("接收者(私聊为用户ID,群聊为群聊ID) "); + + b.Property("Sender") + .HasColumnType("int(11)") + .HasComment("发送者 "); + + b.Property("SequenceId") + .HasColumnType("bigint"); + + b.Property("State") + .HasColumnType("tinyint(4)") + .HasComment("消息状态(0:已发送,1:已撤回) "); + + b.Property("StreamKey") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)") + .HasComment("消息推送唯一标识符"); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.HasIndex("SequenceId", "StreamKey") + .IsUnique(); + + b.HasIndex(new[] { "Sender" }, "Sender"); + + b.ToTable("messages", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Content") + .IsRequired() + .HasColumnType("text") + .HasComment("通知内容"); + + b.Property("Created") + .HasColumnType("datetime") + .HasComment("创建时间"); + + b.Property("Ntype") + .HasColumnType("tinyint(4)") + .HasColumnName("NType") + .HasComment("通知类型(0:文本)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)") + .HasComment("通知标题"); + + b.Property("UserId") + .HasColumnType("int(11)") + .HasComment("接收人(为空为全体通知)"); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.HasIndex(new[] { "UserId" }, "Userld") + .HasDatabaseName("Userld3"); + + b.ToTable("notifications", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Code") + .HasColumnType("int(11)") + .HasComment("权限编码 "); + + b.Property("Created") + .HasColumnType("datetime") + .HasComment("创建时间 "); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .HasComment("权限名称 "); + + b.Property("Ptype") + .HasColumnType("int(11)") + .HasColumnName("PType") + .HasComment("权限类型(0:增,1:删,2:改,3:查) "); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.ToTable("permissions", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.Permissionarole", b => + { + b.Property("Id") + .HasColumnType("int(11)") + .HasColumnName("ID"); + + b.Property("PermissionId") + .HasColumnType("int(11)") + .HasComment("权限 "); + + b.Property("RoleId") + .HasColumnType("int(11)") + .HasComment("角色 "); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.HasIndex(new[] { "PermissionId" }, "Permissionld"); + + b.HasIndex(new[] { "RoleId" }, "Roleld"); + + b.ToTable("permissionarole", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int(11)") + .HasColumnName("ID"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetime") + .HasComment("创建时间 "); + + b.Property("Description") + .IsRequired() + .HasColumnType("text") + .HasComment("角色描述 "); + + b.Property("Name") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasComment("角色名称 "); + + b.HasKey("Id") + .HasName("PRIMARY"); + + b.ToTable("roles", (string)null); + + MySqlEntityTypeBuilderExtensions.HasCharSet(b, "utf8mb4"); + MySqlEntityTypeBuilderExtensions.UseCollation(b, "utf8mb4_general_ci"); + }); + + modelBuilder.Entity("IM_API.Models.Upload.UploadTask", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("ChunkSize") + .HasColumnType("int"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FileHash") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("ObjectName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ProviderUploadId") + .HasColumnType("longtext"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("StorageProvider") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TotalChunks") + .HasColumnType("int"); + + b.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/20260214131542_update-uploadtask.cs b/backend/IM_API/Migrations/20260214131542_update-uploadtask.cs new file mode 100644 index 0000000..034f306 --- /dev/null +++ b/backend/IM_API/Migrations/20260214131542_update-uploadtask.cs @@ -0,0 +1,186 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace IM_API.Migrations +{ + /// + public partial class updateuploadtask : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameTable( + name: "UploadTasks", + newName: "upload_tasks"); + + migrationBuilder.AlterTable( + name: "upload_tasks") + .Annotation("MySql:CharSet", "utf8mb4") + .Annotation("Relational:Collation", "utf8mb4_general_ci") + .OldAnnotation("MySql:CharSet", "latin1") + .OldAnnotation("Relational:Collation", "latin1_swedish_ci"); + + migrationBuilder.AlterColumn( + name: "StorageProvider", + table: "upload_tasks", + type: "longtext", + nullable: false, + collation: "utf8mb4_general_ci", + oldClrType: typeof(string), + oldType: "longtext") + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "latin1") + .OldAnnotation("Relational:Collation", "latin1_swedish_ci"); + + migrationBuilder.AlterColumn( + name: "ProviderUploadId", + table: "upload_tasks", + type: "longtext", + nullable: true, + collation: "utf8mb4_general_ci", + oldClrType: typeof(string), + oldType: "longtext", + oldNullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "latin1") + .OldAnnotation("Relational:Collation", "latin1_swedish_ci"); + + migrationBuilder.AlterColumn( + name: "ObjectName", + table: "upload_tasks", + type: "longtext", + nullable: false, + collation: "utf8mb4_general_ci", + oldClrType: typeof(string), + oldType: "longtext") + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "latin1") + .OldAnnotation("Relational:Collation", "latin1_swedish_ci"); + + migrationBuilder.AlterColumn( + name: "FileName", + table: "upload_tasks", + type: "longtext", + nullable: false, + collation: "utf8mb4_general_ci", + oldClrType: typeof(string), + oldType: "longtext") + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "latin1") + .OldAnnotation("Relational:Collation", "latin1_swedish_ci"); + + migrationBuilder.AlterColumn( + name: "FileHash", + table: "upload_tasks", + type: "longtext", + nullable: false, + collation: "utf8mb4_general_ci", + oldClrType: typeof(string), + oldType: "longtext") + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "latin1") + .OldAnnotation("Relational:Collation", "latin1_swedish_ci"); + + migrationBuilder.AlterColumn( + name: "ContentType", + table: "upload_tasks", + type: "longtext", + nullable: false, + collation: "utf8mb4_general_ci", + oldClrType: typeof(string), + oldType: "longtext") + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "latin1") + .OldAnnotation("Relational:Collation", "latin1_swedish_ci"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameTable( + name: "upload_tasks", + newName: "UploadTasks"); + + migrationBuilder.AlterTable( + name: "UploadTasks") + .Annotation("MySql:CharSet", "latin1") + .Annotation("Relational:Collation", "latin1_swedish_ci") + .OldAnnotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("Relational:Collation", "utf8mb4_general_ci"); + + migrationBuilder.AlterColumn( + name: "StorageProvider", + table: "UploadTasks", + type: "longtext", + nullable: false, + collation: "latin1_swedish_ci", + oldClrType: typeof(string), + oldType: "longtext") + .Annotation("MySql:CharSet", "latin1") + .OldAnnotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("Relational:Collation", "utf8mb4_general_ci"); + + migrationBuilder.AlterColumn( + name: "ProviderUploadId", + table: "UploadTasks", + type: "longtext", + nullable: true, + collation: "latin1_swedish_ci", + oldClrType: typeof(string), + oldType: "longtext", + oldNullable: true) + .Annotation("MySql:CharSet", "latin1") + .OldAnnotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("Relational:Collation", "utf8mb4_general_ci"); + + migrationBuilder.AlterColumn( + name: "ObjectName", + table: "UploadTasks", + type: "longtext", + nullable: false, + collation: "latin1_swedish_ci", + oldClrType: typeof(string), + oldType: "longtext") + .Annotation("MySql:CharSet", "latin1") + .OldAnnotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("Relational:Collation", "utf8mb4_general_ci"); + + migrationBuilder.AlterColumn( + name: "FileName", + table: "UploadTasks", + type: "longtext", + nullable: false, + collation: "latin1_swedish_ci", + oldClrType: typeof(string), + oldType: "longtext") + .Annotation("MySql:CharSet", "latin1") + .OldAnnotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("Relational:Collation", "utf8mb4_general_ci"); + + migrationBuilder.AlterColumn( + name: "FileHash", + table: "UploadTasks", + type: "longtext", + nullable: false, + collation: "latin1_swedish_ci", + oldClrType: typeof(string), + oldType: "longtext") + .Annotation("MySql:CharSet", "latin1") + .OldAnnotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("Relational:Collation", "utf8mb4_general_ci"); + + migrationBuilder.AlterColumn( + name: "ContentType", + table: "UploadTasks", + type: "longtext", + nullable: false, + collation: "latin1_swedish_ci", + oldClrType: typeof(string), + oldType: "longtext") + .Annotation("MySql:CharSet", "latin1") + .OldAnnotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("Relational:Collation", "utf8mb4_general_ci"); + } + } +} diff --git a/backend/IM_API/Migrations/ImContextModelSnapshot.cs b/backend/IM_API/Migrations/ImContextModelSnapshot.cs index 8159df8..6b71e71 100644 --- a/backend/IM_API/Migrations/ImContextModelSnapshot.cs +++ b/backend/IM_API/Migrations/ImContextModelSnapshot.cs @@ -768,6 +768,59 @@ namespace IM_API.Migrations 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.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") diff --git a/backend/IM_API/Models/ImContext.cs b/backend/IM_API/Models/ImContext.cs index e09154c..59599b4 100644 --- a/backend/IM_API/Models/ImContext.cs +++ b/backend/IM_API/Models/ImContext.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using IM_API.Models.Upload; using Microsoft.EntityFrameworkCore; namespace IM_API.Models; @@ -18,6 +19,7 @@ public partial class ImContext : DbContext public virtual DbSet Devices { get; set; } public virtual DbSet Files { get; set; } + public virtual DbSet UploadTasks { get; set; } public virtual DbSet Friends { get; set; } @@ -208,6 +210,17 @@ public partial class ImContext : DbContext .HasConstraintName("files_ibfk_1"); }); + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("PRIMARY"); + + entity + .ToTable("upload_tasks") + .HasCharSet("utf8mb4") + .UseCollation("utf8mb4_general_ci"); + + }); + modelBuilder.Entity(entity => { entity.HasKey(e => e.Id).HasName("PRIMARY"); diff --git a/backend/IM_API/Models/Upload/UploadStatus.cs b/backend/IM_API/Models/Upload/UploadStatus.cs new file mode 100644 index 0000000..ddca6c4 --- /dev/null +++ b/backend/IM_API/Models/Upload/UploadStatus.cs @@ -0,0 +1,10 @@ +namespace IM_API.Models.Upload +{ + public enum UploadStatus + { + Created, + Uploading, + Completed, + Aborted + } +} diff --git a/backend/IM_API/Models/Upload/UploadTask.cs b/backend/IM_API/Models/Upload/UploadTask.cs new file mode 100644 index 0000000..087b83b --- /dev/null +++ b/backend/IM_API/Models/Upload/UploadTask.cs @@ -0,0 +1,24 @@ +namespace IM_API.Models.Upload +{ + public class UploadTask + { + public Guid Id { get; set; } + + public string FileName { get; set; } = default!; + public long FileSize { get; set; } + public string FileHash { get; set; } + public string ContentType { get; set; } = default!; + + public int ChunkSize { get; set; } + public int TotalChunks { get; set; } + + public UploadStatus Status { get; set; } + + public string StorageProvider { get; set; } = default!; + public string ObjectName { get; set; } = default!; + + public string? ProviderUploadId { get; set; } // OSS/S3 UploadId + + public DateTimeOffset CreatedAt { get; set; } + } +} diff --git a/backend/IM_API/Program.cs b/backend/IM_API/Program.cs index c006058..a595b8a 100644 --- a/backend/IM_API/Program.cs +++ b/backend/IM_API/Program.cs @@ -7,6 +7,7 @@ using IM_API.Models; using IM_API.Tools; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.FileProviders; using Microsoft.IdentityModel.Tokens; using StackExchange.Redis; using System.Text; @@ -42,7 +43,9 @@ namespace IM_API }); builder.Services.AddRabbitMQ(configuration.GetSection("RabbitMqOptions").Get()); - + + builder.Services.AddHttpContextAccessor(); + builder.Services.AddAllService(configuration); builder.Services.AddSignalR().AddJsonProtocol(options => @@ -134,6 +137,23 @@ namespace IM_API var app = builder.Build(); + string uploadPath = Path.Combine(Directory.GetCurrentDirectory(), "Uploads","files"); + + // 2. ļв򴴽ֹ + if (!Directory.Exists(uploadPath)) + { + Directory.CreateDirectory(uploadPath); + } + + // 3. þ̬ļӳ + app.UseStaticFiles(new StaticFileOptions + { + // ָ· + FileProvider = new PhysicalFileProvider(uploadPath), + // ָʵǰ׺磺http://localhost:5000/files/1.jpg + RequestPath = "/uploads/files" + }); + app.UseCors(); // Configure the HTTP request pipeline. diff --git a/backend/IM_API/Services/LocalStorageService.cs b/backend/IM_API/Services/LocalStorageService.cs new file mode 100644 index 0000000..3947f37 --- /dev/null +++ b/backend/IM_API/Services/LocalStorageService.cs @@ -0,0 +1,236 @@ +using AutoMapper; +using IM_API.Configs.Options; +using IM_API.Domain.Events; +using IM_API.Dtos; +using IM_API.Exceptions; +using IM_API.Interface.Services; +using IM_API.Models.Upload; +using IM_API.Tools; +using IM_API.VOs; +using MassTransit; +using Microsoft.EntityFrameworkCore.Storage; +using StackExchange.Redis; +using System.Security.AccessControl; +using System.Security.Claims; +using System.Threading.Tasks; +using IDatabase = StackExchange.Redis.IDatabase; + +namespace IM_API.Services +{ + public class LocalStorageService : IStorageService + { + private readonly IMapper _mapper; + private readonly IHttpContextAccessor _httpContext; + private FileUploadOptions _options; + private readonly IUploadTaskService _uploadTaskService; + private readonly IDatabase _redis; + private readonly IHostEnvironment _env; + private readonly ILogger _logger; + private readonly IPublishEndpoint _endpoint; + public LocalStorageService(IMapper mapper, IHttpContextAccessor httpContextAccessor, + IConfiguration configuration, IUploadTaskService uploadTaskService, + IConnectionMultiplexer connectionMultiplexer, IHostEnvironment hostEnvironment + , ILogger logger, IPublishEndpoint publishEndpoint) + { + _mapper = mapper; + _httpContext = httpContextAccessor; + _options = configuration.GetSection("FileUploadOptions").Get()!; + _uploadTaskService = uploadTaskService; + _redis = connectionMultiplexer.GetDatabase(); + _env = hostEnvironment; + _logger = logger; + _endpoint = publishEndpoint; + } + + public UploadMode Mode => UploadMode.Proxy; + public string ProviderName => "Local"; + + public async Task CompleteAsync(Guid taskId, List parts) + { + var task = await _uploadTaskService.GetTaskAsync(taskId); + + if(task is null) + throw new BaseException(CodeDefine.CHUNKE_NOT_FOUND); + + var partsToCheck = Enumerable.Range(1, task.TotalChunks) + .Select(i => (RedisValue)i).ToArray(); + + var results = await _redis.SetContainsAsync(RedisKeys.GetUploadPartKey(taskId), partsToCheck); + // 3. 快速判断是否全部存在 + bool isAllUploaded = results.All(exists => exists); + if (!isAllUploaded) throw new BaseException(CodeDefine.CHUNKE_NOT_FOUND); + + await _endpoint.Publish(new UploadMergeEvent + { + AggregateId = taskId.ToString(), + OccurredAt = DateTime.UtcNow, + EventId = Guid.NewGuid(), + OperatorId = 0, + Parts = parts, + TaskId = taskId, + ChunckCount = task.TotalChunks, + ObjectName = task.ObjectName + + }); + return taskId; + } + + public async Task MergeAsync(Guid taskId, string objectName, int totalChunks, List parts) + { + var baseDir = Path.Combine(_env.ContentRootPath, "uploads"); + var tempPath = Path.Combine(baseDir, "temp", taskId.ToString()); // 项目根目录下 uploads // 最终文件存储路径(这里可以用你之前 ObjectNameGenerator 生成的名字) + var finalPath = Path.Combine(baseDir, "files", objectName); + var finalDir = Path.GetDirectoryName(finalPath); + Directory.CreateDirectory(finalDir); + try + { + using (var finalStream = new FileStream(finalPath, FileMode.Create)) + { + for (var i = 1; i <= totalChunks; i++) + { + var progress = (i * 100.0 / totalChunks); + if (i % 5 == 0 || i == totalChunks) + { + await _redis.HashSetAsync(RedisKeys.MergeStatus(taskId), new HashEntry[] + { + new("status", "processing"), + new("progress", progress.ToString("F2")) + }); + } + var chunkPath = Path.Combine(tempPath, $"{i}.part.tmp"); + if (!File.Exists(chunkPath)) + throw new BaseException(CodeDefine.CHUNKE_NOT_FOUND); + using (var chunkStream = new FileStream(chunkPath, FileMode.Open)) + { + await chunkStream.CopyToAsync(finalStream); + } + } + Directory.Delete(tempPath, true); + await _redis.KeyDeleteAsync(RedisKeys.GetUploadPartKey(taskId)); + await _uploadTaskService.UpdateStatusAsync(taskId, UploadStatus.Completed); + await _redis.HashSetAsync(RedisKeys.MergeStatus(taskId), new HashEntry[] + { + new("status", "Completed"), + new("progress", "100"), + new("url", objectName) + }); + } + + } + catch (Exception e) when (e is not BaseException) + { + _logger.LogError(e, e.Message); + throw new BaseException(CodeDefine.CHUNKE_COMBINE_FAIL); + + } + } + + public async Task CreatePartInstructionAsync(Guid taskId, int partNumer) + { + if (await _redis.SetContainsAsync(RedisKeys.GetUploadPartKey(taskId), partNumer)){ + return new UploadPartInstructionVo + { + PartNumber = partNumer, + Skip = true, + Headers = new Dictionary() + }; + } + var request = _httpContext.HttpContext!.Request; + + var scheme = request.Scheme; // http 或 https + var host = request.Host.Value; // localhost:5000 或域名 + + var baseUrl = $"{scheme}://{host}/api/upload/local/{taskId}/parts/{partNumer}"; + var headers = new Dictionary(); + headers.Add("Content-Type", "multipart/form-data"); + return new UploadPartInstructionVo + { + Method = "POST", + PartNumber = partNumer, + Skip = false, + Url = baseUrl, + Headers = headers + }; + + + } + + public async Task UploadSmallFileAsync(Stream stream, string fileName, string fileType, long size, string hash) + { + var taskOld = await _uploadTaskService.GetTaskAsync(hash); + + if (taskOld is not null) return taskOld; + + var userId = _httpContext.HttpContext?.User.FindFirstValue(ClaimTypes.NameIdentifier); + var objectname = ObjectNameGenerator.Generate(new ObjectNameContext + { + ContentType = fileType, + FileName = fileName, + UserId = int.Parse(userId) + }); + var path = GetDownloadUrl(objectname); + // 4. 将 Stream 写入本地文件 + using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None)) + { + await stream.CopyToAsync(fileStream); + } + var task = new UploadTask + { + CreatedAt = DateTime.UtcNow, + ChunkSize = (int)size, + ContentType = fileType, + FileHash = hash, + FileName = fileName, + FileSize = size, + Id = Guid.NewGuid(), + ObjectName = objectname, + ProviderUploadId = Guid.NewGuid().ToString(), + Status = UploadStatus.Completed, + StorageProvider = ProviderName, + TotalChunks = 1 + }; + await _uploadTaskService.AddAsync(task); + return task; + + } + + public string GetDownloadUrl(string objectname) + { + var baseDir = Path.Combine(_env.ContentRootPath, "uploads"); // 最终文件存储路径(这里可以用你之前 ObjectNameGenerator 生成的名字) + var finalPath = Path.Combine(baseDir, "files", objectname); + var finalDir = Path.GetDirectoryName(finalPath); + Directory.CreateDirectory(finalDir); + return finalPath; + } + + public async Task InitTaskAsync(CreateUploadTaskDto dto) + { + var userId = _httpContext.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier); + UploadTask task = _mapper.Map(dto); + var taskOld = await _uploadTaskService.GetTaskAsync(dto.FileHash); + if(taskOld != null) + { + var t = _mapper.Map(taskOld); + t.Skip = false; + if (taskOld.Status == UploadStatus.Completed) + { + t.Skip = true; + } + return t; + task = taskOld; + } + task.ObjectName = ObjectNameGenerator.Generate(new ObjectNameContext + { + ContentType = task.ContentType, + FileName = task.FileName, + UserId = int.Parse(userId) + }); + task.StorageProvider = ProviderName; + task.ProviderUploadId = Guid.NewGuid().ToString(); + task.ChunkSize = _options.ChunkSize; + task.TotalChunks = (int)Math.Ceiling((double)task.FileSize / _options.ChunkSize); + await _uploadTaskService.AddAsync(task); + return _mapper.Map(task); + } + } +} diff --git a/backend/IM_API/Services/MessageService.cs b/backend/IM_API/Services/MessageService.cs index 07b0528..24b2e53 100644 --- a/backend/IM_API/Services/MessageService.cs +++ b/backend/IM_API/Services/MessageService.cs @@ -10,9 +10,11 @@ using IM_API.Tools; using IM_API.VOs.Message; using MassTransit; using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json; using StackExchange.Redis; using System.Text.Json; using System.Text.RegularExpressions; +using System.Threading.Tasks; using static MassTransit.Monitoring.Performance.BuiltInCounters; using static Microsoft.EntityFrameworkCore.DbLoggerCategory; @@ -28,10 +30,13 @@ namespace IM_API.Services private readonly IPublishEndpoint _endpoint; private readonly ISequenceIdService _sequenceIdService; private readonly IUserService _userService; + private readonly IUploadTaskService _uploadService; + private readonly IHttpContextAccessor _httpContextAccessor; public MessageService( ImContext context, ILogger logger, IMapper mapper, IPublishEndpoint publishEndpoint, ISequenceIdService sequenceIdService, - IUserService userService + IUserService userService, IUploadTaskService uploadTaskService, + IHttpContextAccessor httpContextAccessor ) { _context = context; @@ -41,6 +46,8 @@ namespace IM_API.Services _endpoint = publishEndpoint; _sequenceIdService = sequenceIdService; _userService = userService; + _uploadService = uploadTaskService; + _httpContextAccessor = httpContextAccessor; } public async Task> GetMessagesAsync(int userId,MessageQueryDto dto) @@ -91,6 +98,12 @@ namespace IM_API.Services foreach (var item in messages) { + if(item.Type != MessageMsgType.Text) + { + var request = _httpContextAccessor.HttpContext?.Request; + var baseUrl = $"{request.Scheme}://{request.Host}"; + item.Content = UrlTools.ProcessMessageUrl(item.Content, baseUrl); + } if(userDict.TryGetValue(item.SenderId, out var user)) { item.SenderName = user.NickName; @@ -143,7 +156,10 @@ namespace IM_API.Services var message = _mapper.Map(dto); message.StreamKey = StreamKeyBuilder.Group(groupId); message.SequenceId = await _sequenceIdService.GetNextSquenceIdAsync(message.StreamKey); - await _endpoint.Publish(_mapper.Map(message)); + var publishData = _mapper.Map(message); + var request = _httpContextAccessor.HttpContext?.Request; + publishData.BaseUrl = $"{request.Scheme}://{request.Host}"; + await _endpoint.Publish(publishData); return _mapper.Map(message); } @@ -156,9 +172,40 @@ namespace IM_API.Services var message = _mapper.Map(dto); message.StreamKey = StreamKeyBuilder.Private(senderId, receiverId); message.SequenceId = await _sequenceIdService.GetNextSquenceIdAsync(message.StreamKey); - await _endpoint.Publish(_mapper.Map(message)); + var publishData = _mapper.Map(message); + var request = _httpContextAccessor.HttpContext?.Request; + publishData.BaseUrl = $"{request.Scheme}://{request.Host}"; + await _endpoint.Publish(publishData); return _mapper.Map(message); } #endregion + + public async Task HandleFileMessageContentAsync(MessageBaseDto dto) + { + if(dto.Type == MessageMsgType.Text) + { + return dto; + } + + var dic = JsonConvert.DeserializeObject>(dto.Content); + + if (dic == null || !dic.TryGetValue("fileId", out var fileIdObj)) + throw new BaseException(CodeDefine.PARAMETER_ERROR); + + var fileInfo = await _uploadService.GetTaskAsync(new Guid(fileIdObj.ToString())); + + if (fileInfo is null) + throw new BaseException(CodeDefine.FILE_NOT_FOUND); + + + + dic["url"] = fileInfo.ObjectName; + dic["provider"] = fileInfo.StorageProvider; + dic["size"] = fileInfo.FileSize; + + dto.Content = JsonConvert.SerializeObject(dic); + + return dto; + } } } diff --git a/backend/IM_API/Services/UploadTaskService.cs b/backend/IM_API/Services/UploadTaskService.cs new file mode 100644 index 0000000..5007e6e --- /dev/null +++ b/backend/IM_API/Services/UploadTaskService.cs @@ -0,0 +1,43 @@ +using IM_API.Interface.Services; +using IM_API.Models; +using IM_API.Models.Upload; +using Microsoft.EntityFrameworkCore; + +namespace IM_API.Services +{ + public class UploadTaskService : IUploadTaskService + { + private readonly ImContext _context; + public UploadTaskService(ImContext context) + { + _context = context; + } + + public async Task AddAsync(UploadTask task) + { + _context.UploadTasks.Add(task); + await _context.SaveChangesAsync(); + } + + public async Task GetTaskAsync(Guid taskId) + { + return await _context.UploadTasks.FirstOrDefaultAsync(x => x.Id == taskId); + } + + public async Task GetTaskAsync(string hash) + { + return await _context.UploadTasks.FirstOrDefaultAsync(x => x.FileHash == hash); + } + + public async Task UpdateStatusAsync(Guid taskId, UploadStatus status) + { + var task = await _context.UploadTasks.FirstOrDefaultAsync(x => x.Id == taskId); + if (task != null) + { + task.Status = status; + _context.UploadTasks.Update(task); + await _context.SaveChangesAsync(); + } + } + } +} diff --git a/backend/IM_API/Tools/CodeDefine.cs b/backend/IM_API/Tools/CodeDefine.cs index e62989e..eec8d8b 100644 --- a/backend/IM_API/Tools/CodeDefine.cs +++ b/backend/IM_API/Tools/CodeDefine.cs @@ -105,5 +105,11 @@ // 3.9 会话相关错误(3100 ~ 3199) /// 发送时异常 public static CodeDefine CONVERSATION_NOT_FOUND = new CodeDefine(3100, "会话不存在"); + + // 3.9 文件相关错误(3200 ~ 3299) + /// 分片不存在异常 + public static CodeDefine CHUNKE_NOT_FOUND = new CodeDefine(3201, "分片不存在"); + /// 分片合并异常 + public static CodeDefine CHUNKE_COMBINE_FAIL = new CodeDefine(3202, "分片合并失败"); } } diff --git a/backend/IM_API/Tools/ObjectNameGenerator.cs b/backend/IM_API/Tools/ObjectNameGenerator.cs new file mode 100644 index 0000000..606e68b --- /dev/null +++ b/backend/IM_API/Tools/ObjectNameGenerator.cs @@ -0,0 +1,51 @@ +namespace IM_API.Tools +{ + public static class ObjectNameGenerator + { + public static string Generate(ObjectNameContext ctx) + { + var ext = GetExtension(ctx.FileName, ctx.ContentType); + var shortId = Guid.NewGuid().ToString("N")[..12]; + + var parts = new List + { + ctx.Biz, + ctx.Now.Year.ToString(), + ctx.Now.Month.ToString("D2") + }; + + if (ctx.UserId.HasValue) + { + parts.Add(ctx.UserId.Value.ToString()); + } + + parts.Add($"{shortId}{ext}"); + + return string.Join("/", parts); + } + + private static string GetExtension(string fileName, string contentType) + { + var ext = Path.GetExtension(fileName); + if (!string.IsNullOrWhiteSpace(ext)) + return ext.ToLowerInvariant(); + + return contentType switch + { + "image/jpeg" => ".jpg", + "image/png" => ".png", + "video/mp4" => ".mp4", + _ => ".bin" + }; + } + } + public class ObjectNameContext + { + public string Biz { get; init; } = "IM"; + public long? UserId { get; init; } + public string FileName { get; init; } = default!; + public string ContentType { get; init; } = default!; + public DateTimeOffset Now { get; init; } = DateTimeOffset.UtcNow; + } + +} diff --git a/backend/IM_API/Tools/RedisKeys.cs b/backend/IM_API/Tools/RedisKeys.cs index d7949be..0a808df 100644 --- a/backend/IM_API/Tools/RedisKeys.cs +++ b/backend/IM_API/Tools/RedisKeys.cs @@ -2,10 +2,13 @@ { public static class RedisKeys { - public static string GetUserinfoKey(string userId) => $"user::uinfo::{userId}"; - public static string GetUserinfoKeyByUsername(string username) => $"user::uinfobyid::{username}"; - public static string GetSequenceIdKey(string streamKey) => $"chat::seq::{streamKey}"; - public static string GetSequenceIdLockKey(string streamKey) => $"lock::seq::{streamKey}"; - public static string GetConnectionIdKey(string userId) => $"signalr::user::con::{userId}"; + public static string GetUserinfoKey(string userId) => $"user:uinfo:{userId}"; + public static string GetUserinfoKeyByUsername(string username) => $"user:uinfobyid:{username}"; + public static string GetSequenceIdKey(string streamKey) => $"chat:seq:{streamKey}"; + public static string GetSequenceIdLockKey(string streamKey) => $"lock:seq:{streamKey}"; + public static string GetConnectionIdKey(string userId) => $"signalr:user:con:{userId}"; + + public static string GetUploadPartKey(Guid taskId) => $"upload:task:{taskId}:parts"; + public static string MergeStatus(Guid taskId) => $"upload:task:{taskId}:merge"; } } diff --git a/backend/IM_API/Tools/UrlTools.cs b/backend/IM_API/Tools/UrlTools.cs new file mode 100644 index 0000000..58b8057 --- /dev/null +++ b/backend/IM_API/Tools/UrlTools.cs @@ -0,0 +1,64 @@ +using SixLabors.ImageSharp; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace IM_API.Tools +{ + public static class UrlTools + { + public static string GetFullUrl(string objectName, string provider, string? baseUrl) + { + return provider switch + { + "Local" => $"{baseUrl}/uploads/files/{objectName}", + _ => "http://baidu.com", + }; + } + + public static async Task<(int width, int height)> GetImageWH(string url) + { + using var httpClient = new HttpClient(); + var stream = await httpClient.GetStreamAsync(url); + var info = await Image.IdentifyAsync(stream); + return (info.Width, info.Height); + } + + public static string ProcessMessageUrl(string contentJson, string? localBaseUrl) + { + // 1. 解析 JSON 文档(比反序列化快得多) + using var doc = JsonDocument.Parse(contentJson); + var root = doc.RootElement; + + // 2. 获取 Provider 字段 + string provider = root.GetProperty("provider").GetString(); + + // 3. 根据 Provider 决定前缀 + string prefix = GetFullUrl("", provider, localBaseUrl); + + // 4. 重新组装(如果只是为了给前端看,建议直接返回带前缀的对象或字符串) + // 这里推荐用 JsonNode 方便修改并返回字符串 + var node = JsonNode.Parse(contentJson); + node["url"] = $"{prefix}{node["url"]}"; + node["thumb"] = $"{prefix}{node["thumb"]}"; + + return node.ToJsonString(); + } + public static Stream Base64ToStream(string base64String) + { + if (string.IsNullOrEmpty(base64String)) + throw new ArgumentNullException(nameof(base64String)); + + // 1. 自动处理可能存在的 Base64 Data URL 前缀 + string base64Data = base64String.Contains(",") + ? base64String.Split(',')[1] + : base64String; + + // 2. 解码为字节数组 + byte[] bytes = Convert.FromBase64String(base64Data); + + // 3. 包装进 MemoryStream + // 注意:这里直接把 Position 设为 0,符合“方法a”产生即用的原则 + return new MemoryStream(bytes); + } + } +} diff --git a/backend/IM_API/VOs/CreateUploadTaskVo.cs b/backend/IM_API/VOs/CreateUploadTaskVo.cs new file mode 100644 index 0000000..4288a06 --- /dev/null +++ b/backend/IM_API/VOs/CreateUploadTaskVo.cs @@ -0,0 +1,16 @@ +using IM_API.Interface.Services; + +namespace IM_API.VOs +{ + public class CreateUploadTaskVo + { + public Guid TaskId { get; set; } + + public int ChunkSize { get; set; } + public int TotalChunks { get; set; } + + public int Concurrency { get; set; } = 4; + public string? Url { get; set; } + public bool Skip { get; set; } + } +} diff --git a/backend/IM_API/VOs/UploadPartInstuctionVo.cs b/backend/IM_API/VOs/UploadPartInstuctionVo.cs new file mode 100644 index 0000000..3f19505 --- /dev/null +++ b/backend/IM_API/VOs/UploadPartInstuctionVo.cs @@ -0,0 +1,14 @@ +namespace IM_API.VOs +{ + public class UploadPartInstructionVo + { + public bool Skip { get; set; } + public int PartNumber { get; set; } + + public string Method { get; set; } = "PUT"; + public string Url { get; set; } = default!; + + public Dictionary Headers { get; set; } = new(); + } + +} diff --git a/backend/IM_API/appsettings.json b/backend/IM_API/appsettings.json index fc895fb..3211965 100644 --- a/backend/IM_API/appsettings.json +++ b/backend/IM_API/appsettings.json @@ -14,7 +14,7 @@ "RefreshTokenDays": 30 }, "ConnectionStrings": { - "DefaultConnection": "Server=frp-era.com;Port=26582;Database=IM;User=product;Password=12345678;", + "DefaultConnection": "Server=192.168.5.100;Port=3306;Database=IM;User=product;Password=12345678;", "Redis": "192.168.5.100:6379" }, "RabbitMQOptions": { @@ -22,5 +22,9 @@ "Port": 5672, "Username": "test", "Password": "123456" + }, + "FileUploadOptions": { + "DefaultStorage": "Local", + "ChunkSize": 5000000, } } diff --git a/frontend/app/android/build.gradle.kts b/frontend/app/android/build.gradle.kts index 224f8a9..febd977 100644 --- a/frontend/app/android/build.gradle.kts +++ b/frontend/app/android/build.gradle.kts @@ -1,12 +1,21 @@ -allprojects { - repositories { - maven { url = uri("https://maven.aliyun.com/repository/google") } - maven { url = uri("https://maven.aliyun.com/repository/public") } - google() - mavenCentral() - - } -} +//buildscript { +// repositories { +// // 将 google() 和 mavenCentral() 替换或放在后面 +// maven { url = uri("https://maven.aliyun.com/repository/google") } +// maven { url = uri("https://maven.aliyun.com/repository/public") } +// google() +// mavenCentral() +// } +//} +// +//allprojects { +// repositories { +// maven { url = uri("https://maven.aliyun.com/repository/google") } +// maven { url = uri("https://maven.aliyun.com/repository/public") } +// google() +// mavenCentral() +// } +//} val newBuildDir: Directory = rootProject.layout.buildDirectory diff --git a/frontend/app/android/build/reports/problems/problems-report.html b/frontend/app/android/build/reports/problems/problems-report.html new file mode 100644 index 0000000..39e604e --- /dev/null +++ b/frontend/app/android/build/reports/problems/problems-report.html @@ -0,0 +1,663 @@ + + + + + + + + + + + + + Gradle Configuration Cache + + + +
+ +
+ Loading... +
+ + + + + + diff --git a/frontend/app/android/gradle.properties b/frontend/app/android/gradle.properties index fbee1d8..13e0135 100644 --- a/frontend/app/android/gradle.properties +++ b/frontend/app/android/gradle.properties @@ -1,2 +1,12 @@ org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true +# ?? HTTP ?? +systemProp.http.proxyHost=127.0.0.1 +systemProp.http.proxyPort=10808 + +# ?? HTTPS ??????????????? +systemProp.https.proxyHost=127.0.0.1 +systemProp.https.proxyPort=10808 + +# ???????????????????????? +systemProp.http.nonProxyHosts=localhost|127.0.0.1|*.aliyun.com|mirrors.* \ No newline at end of file diff --git a/frontend/app/android/settings.gradle.kts b/frontend/app/android/settings.gradle.kts index d36178c..2507983 100644 --- a/frontend/app/android/settings.gradle.kts +++ b/frontend/app/android/settings.gradle.kts @@ -1,28 +1,38 @@ pluginManagement { - val flutterSdkPath = - run { - val properties = java.util.Properties() - file("local.properties").inputStream().use { properties.load(it) } - val flutterSdkPath = properties.getProperty("flutter.sdk") - require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } - flutterSdkPath - } + val flutterSdkPath = run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") repositories { maven { url = uri("https://maven.aliyun.com/repository/google") } maven { url = uri("https://maven.aliyun.com/repository/public") } - google() - mavenCentral() gradlePluginPortal() + //google() + mavenCentral() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS) + repositories { + maven { url = uri("https://maven.aliyun.com/repository/google") } + maven { url = uri("https://maven.aliyun.com/repository/public") } + gradlePluginPortal() + //google() + mavenCentral() } } plugins { id("dev.flutter.flutter-plugin-loader") version "1.0.0" - id("com.android.application") version "8.11.1" apply false - id("org.jetbrains.kotlin.android") version "2.2.20" apply false + id("com.android.application") version "8.1.1" apply false + id("org.jetbrains.kotlin.android") version "1.8.22" apply false } -include(":app") +include(":app") \ No newline at end of file diff --git a/frontend/app/pubspec.lock b/frontend/app/pubspec.lock index 8b91c6e..7d348ac 100644 --- a/frontend/app/pubspec.lock +++ b/frontend/app/pubspec.lock @@ -6,7 +6,7 @@ packages: description: name: async sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.13.0" boolean_selector: @@ -14,23 +14,23 @@ packages: description: name: boolean_selector sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.2" characters: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 - url: "https://pub.flutter-io.cn" + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" clock: dependency: transitive description: name: clock sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.2" collection: @@ -38,7 +38,7 @@ packages: description: name: collection sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.19.1" cupertino_icons: @@ -46,7 +46,7 @@ packages: description: name: cupertino_icons sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.8" fake_async: @@ -54,7 +54,7 @@ packages: description: name: fake_async sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.3.3" flutter: @@ -67,7 +67,7 @@ packages: description: name: flutter_lints sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.0.0" flutter_test: @@ -84,16 +84,16 @@ packages: dependency: "direct main" description: name: go_router - sha256: eff94d2a6fc79fa8b811dde79c7549808c2346037ee107a1121b4a644c745f2a - url: "https://pub.flutter-io.cn" + sha256: "7974313e217a7771557add6ff2238acb63f635317c35fa590d348fb238f00896" + url: "https://pub.dev" source: hosted - version: "17.0.1" + version: "17.1.0" leak_tracker: dependency: transitive description: name: leak_tracker sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "11.0.2" leak_tracker_flutter_testing: @@ -101,7 +101,7 @@ packages: description: name: leak_tracker_flutter_testing sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.10" leak_tracker_testing: @@ -109,47 +109,47 @@ packages: description: name: leak_tracker_testing sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.2" lints: dependency: transitive description: name: lints - sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 - url: "https://pub.flutter-io.cn" + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "6.1.0" logging: dependency: transitive description: name: logging sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.3.0" matcher: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 - url: "https://pub.flutter-io.cn" + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.18" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec - url: "https://pub.flutter-io.cn" + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: name: meta sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.17.0" path: @@ -157,7 +157,7 @@ packages: description: name: path sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.9.1" sky_engine: @@ -169,16 +169,16 @@ packages: dependency: transitive description: name: source_span - sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" - url: "https://pub.flutter-io.cn" + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" source: hosted - version: "1.10.1" + version: "1.10.2" stack_trace: dependency: transitive description: name: stack_trace sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.12.1" stream_channel: @@ -186,7 +186,7 @@ packages: description: name: stream_channel sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.4" string_scanner: @@ -194,7 +194,7 @@ packages: description: name: string_scanner sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.4.1" term_glyph: @@ -202,23 +202,23 @@ packages: description: name: term_glyph sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 - url: "https://pub.flutter-io.cn" + sha256: "19a78f63e83d3a61f00826d09bc2f60e191bf3504183c001262be6ac75589fb8" + url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.8" vector_math: dependency: transitive description: name: vector_math sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.2.0" vm_service: @@ -226,7 +226,7 @@ packages: description: name: vm_service sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "15.0.2" sdks: diff --git a/frontend/web/package-lock.json b/frontend/web/package-lock.json index 4499a81..d561854 100644 --- a/frontend/web/package-lock.json +++ b/frontend/web/package-lock.json @@ -8,6 +8,7 @@ "name": "web", "version": "0.0.0", "dependencies": { + "@cloudgeek/vue3-video-player": "^0.3.10", "@microsoft/signalr": "^10.0.0", "@vuelidate/core": "^2.0.3", "@vuelidate/validators": "^2.0.4", @@ -15,6 +16,7 @@ "feather-icons": "^4.29.2", "idb": "^8.0.3", "pinia": "^3.0.3", + "spark-md5": "^3.0.2", "vee-validate": "^4.15.1", "vue": "^3.5.22", "vue-router": "^4.5.1", @@ -29,6 +31,7 @@ "eslint": "^9.33.0", "eslint-plugin-vue": "~10.4.0", "globals": "^16.3.0", + "hevue-img-preview": "^7.1.3", "jsdom": "^27.0.0", "prettier": "3.6.2", "vite": "^7.1.7", @@ -105,7 +108,6 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -570,6 +572,20 @@ "node": ">=6.9.0" } }, + "node_modules/@cloudgeek/vue3-video-player": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/@cloudgeek/vue3-video-player/-/vue3-video-player-0.3.10.tgz", + "integrity": "sha512-QMpQK20fpUp1WcyIf+PxxW+0OwAx8TR/cGJg/axcVhMtcZvLmP+BPxdMQd5NKZwJ+DqY7ZHEgkfc+9Ng7Vjxuw==", + "license": "MIT", + "dependencies": { + "@multiavatar/multiavatar": "^1.0.5", + "core-js": "^3.6.5", + "event-emitter": "^0.3.5", + "ismobilejs": "^1.1.1", + "load-script": "^1.0.0", + "vue": "^3.0.0" + } + }, "node_modules/@csstools/color-helpers": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", @@ -658,7 +674,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -705,7 +720,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -1497,6 +1511,12 @@ } } }, + "node_modules/@multiavatar/multiavatar": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@multiavatar/multiavatar/-/multiavatar-1.0.7.tgz", + "integrity": "sha512-Yg9Uw57bmlErsWL0CSv4d6D4ZqVBE00OZmYr9MRgygoXZdboNtsEI6FbBRw1AY8l88Sm1ARcyojtlm2uwUn0Zg==", + "license": "SEE LICENSE IN LICENSE" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2670,7 +2690,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2874,7 +2893,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -3165,6 +3183,19 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, + "node_modules/d": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/data-urls": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz", @@ -3418,6 +3449,46 @@ "node": ">= 0.4" } }, + "node_modules/es5-ext": { + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/esbuild": { "version": "0.25.10", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", @@ -3489,7 +3560,6 @@ "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3551,7 +3621,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -3688,6 +3757,21 @@ "node": "*" } }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/espree": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", @@ -3771,6 +3855,16 @@ "node": ">=0.10.0" } }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -3826,6 +3920,15 @@ "node": ">=12.0.0" } }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "license": "ISC", + "dependencies": { + "type": "^2.7.2" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4258,6 +4361,16 @@ "node": ">= 0.4" } }, + "node_modules/hevue-img-preview": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/hevue-img-preview/-/hevue-img-preview-7.1.3.tgz", + "integrity": "sha512-nLB1eyPUqP2UysJpEJaboHMGKPY8zgsyqaPSUHjxr04jDc2PIg61KFFS/UHc0aa2xpbf5UaeCNoVOHFYdWVOTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "vue": "^3.5.17" + } + }, "node_modules/hookable": { "version": "5.5.3", "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", @@ -4537,6 +4650,12 @@ "dev": true, "license": "ISC" }, + "node_modules/ismobilejs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ismobilejs/-/ismobilejs-1.1.1.tgz", + "integrity": "sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw==", + "license": "MIT" + }, "node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", @@ -4733,6 +4852,12 @@ "node": ">= 0.8.0" } }, + "node_modules/load-script": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz", + "integrity": "sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==", + "license": "MIT" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -4917,6 +5042,12 @@ "dev": true, "license": "MIT" }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "license": "ISC" + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -5273,7 +5404,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -5313,7 +5443,6 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -5655,6 +5784,12 @@ "node": ">=0.10.0" } }, + "node_modules/spark-md5": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/spark-md5/-/spark-md5-3.0.2.tgz", + "integrity": "sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==", + "license": "(WTFPL OR MIT)" + }, "node_modules/speakingurl": { "version": "14.0.1", "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", @@ -5937,7 +6072,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -6063,6 +6197,12 @@ "typescript": ">=4.8.4" } }, + "node_modules/type": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "license": "ISC" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6232,7 +6372,6 @@ "integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -6494,7 +6633,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -6508,7 +6646,6 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -6594,7 +6731,6 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.22.tgz", "integrity": "sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==", "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.22", "@vue/compiler-sfc": "3.5.22", @@ -6624,6 +6760,7 @@ "integrity": "sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "debug": "^4.4.0", "eslint-scope": "^8.2.0", @@ -6648,6 +6785,7 @@ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, diff --git a/frontend/web/package.json b/frontend/web/package.json index 4e58dd6..aac94a6 100644 --- a/frontend/web/package.json +++ b/frontend/web/package.json @@ -15,6 +15,7 @@ "format": "prettier --write src/" }, "dependencies": { + "@cloudgeek/vue3-video-player": "^0.3.10", "@microsoft/signalr": "^10.0.0", "@vuelidate/core": "^2.0.3", "@vuelidate/validators": "^2.0.4", @@ -22,6 +23,7 @@ "feather-icons": "^4.29.2", "idb": "^8.0.3", "pinia": "^3.0.3", + "spark-md5": "^3.0.2", "vee-validate": "^4.15.1", "vue": "^3.5.22", "vue-router": "^4.5.1", @@ -36,6 +38,7 @@ "eslint": "^9.33.0", "eslint-plugin-vue": "~10.4.0", "globals": "^16.3.0", + "hevue-img-preview": "^7.1.3", "jsdom": "^27.0.0", "prettier": "3.6.2", "vite": "^7.1.7", diff --git a/frontend/web/src/components/messages/VideoMsg.vue b/frontend/web/src/components/messages/VideoMsg.vue new file mode 100644 index 0000000..b8223f5 --- /dev/null +++ b/frontend/web/src/components/messages/VideoMsg.vue @@ -0,0 +1,124 @@ + + + + + diff --git a/frontend/web/src/components/messages/VideoPreView.vue b/frontend/web/src/components/messages/VideoPreView.vue new file mode 100644 index 0000000..f58a0e4 --- /dev/null +++ b/frontend/web/src/components/messages/VideoPreView.vue @@ -0,0 +1,30 @@ + + + diff --git a/frontend/web/src/components/messages/VoiceMsg.vue b/frontend/web/src/components/messages/VoiceMsg.vue new file mode 100644 index 0000000..5398558 --- /dev/null +++ b/frontend/web/src/components/messages/VoiceMsg.vue @@ -0,0 +1,158 @@ + + + + + diff --git a/frontend/web/src/constants/fileTypeDefine.js b/frontend/web/src/constants/fileTypeDefine.js new file mode 100644 index 0000000..3eed4ab --- /dev/null +++ b/frontend/web/src/constants/fileTypeDefine.js @@ -0,0 +1,40 @@ +export const getMessageType = (fileType) => { + if (!fileType) return FILE_TYPE.File; // 兜底处理 + + // 处理图片 + if (fileType.startsWith('image/')) { + return FILE_TYPE.Image; + } + + // 处理音频(录音消息) + if (fileType.startsWith('audio/')) { + return FILE_TYPE.Voice; + } + + // 处理视频 + if (fileType.startsWith('video/')) { + return FILE_TYPE.Video; + } + + // 常见文档类型的特殊处理(可选) + const documentTypes = [ + 'application/pdf', + 'application/msword', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'text/plain' + ]; + + if (documentTypes.includes(fileType)) { + return FILE_TYPE.File; + } + + // 其他所有情况统一归类为文件 + return FILE_TYPE.File; +}; + +export const FILE_TYPE = Object.freeze({ + Image: 'Image', + Video: 'Video', + Voice: 'Voice', + File: 'File' +}); diff --git a/frontend/web/src/constants/fileTypeInfo.js b/frontend/web/src/constants/fileTypeInfo.js new file mode 100644 index 0000000..989213e --- /dev/null +++ b/frontend/web/src/constants/fileTypeInfo.js @@ -0,0 +1,29 @@ +export class MessageBaseInfo { + constructor(format, text) { + this.format = format; + this.text = text; + } +} + +export class ImageInfo extends MessageBaseInfo { + constructor(format, text, w, h, Thumb) { + super(format, text); + this.w = w; + this.h = h; + this.thumb = Thumb; + } +} + +export class VideoInfo extends ImageInfo { + constructor(format, text, w, h, Thumb, duration) { + super(format, text, w, h, Thumb); + this.duration = duration; + } +} + +export class VoiceInfo extends MessageBaseInfo { + constructor(format, text, duration) { + super(format, text); + this.duration = duration; + } +} diff --git a/frontend/web/src/constants/uploadStatus.js b/frontend/web/src/constants/uploadStatus.js new file mode 100644 index 0000000..5600fef --- /dev/null +++ b/frontend/web/src/constants/uploadStatus.js @@ -0,0 +1,7 @@ + +export const UPLOAD_STATUS = Object.freeze({ + UPLOADING: 'uploading', + UPLOADED: 'uploaded', + MERGING: 'merging', + COMPLETE: 'complete' +}) diff --git a/frontend/web/src/main.js b/frontend/web/src/main.js index bd4463c..1481415 100644 --- a/frontend/web/src/main.js +++ b/frontend/web/src/main.js @@ -7,10 +7,16 @@ import router from './router' import MyButton from './components/MyButton.vue' import IconInput from './components/IconInput.vue' +import Vue3VideoPlayer from '@cloudgeek/vue3-video-player' +import '@cloudgeek/vue3-video-player/dist/vue3-video-player.css' + const app = createApp(App) app.use(createPinia()) app.use(router) +app.use(Vue3VideoPlayer, { + lang: 'zh-CN' // 可选,语言包 +}) app.component('MyButton', MyButton) app.component('IconInput', IconInput) diff --git a/frontend/web/src/services/api.js b/frontend/web/src/services/api.js index ea2eb94..fd327b4 100644 --- a/frontend/web/src/services/api.js +++ b/frontend/web/src/services/api.js @@ -5,99 +5,100 @@ import { useAuthStore } from '@/stores/auth'; import { authService } from './auth'; const message = useMessage(); -const authStore = useAuthStore(); let waitqueue = []; let isRefreshing = false; const authURL = ['/auth/login', '/auth/register', '/auth/refresh']; const api = axios.create({ - baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api', // 从环境变量中读取基础 URL - timeout: 10000, - headers: { - 'Content-Type': 'application/json', - } + baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api', // 从环境变量中读取基础 URL + timeout: 10000, + headers: { + + } }) api.interceptors.request.use( - config => { - const token = authStore.token; - if (token) { - config.headers.Authorization = `Bearer ${token}`; - } - return config; - }, - err => { - return Promise.reject(err); + config => { + const authStore = useAuthStore(); + const token = authStore.token; + if (token) { + config.headers.Authorization = `Bearer ${token}`; } + return config; + }, + err => { + return Promise.reject(err); + } ) api.interceptors.response.use( - response => { - return response.data; - }, - async err => { - const { config, response } = err; - if (response) { - switch (response.status) { - case 401: - if (authURL.some(x => config.url.includes(x))) { - authStore.logout(); - message.error('未登录,请登录后操作。'); - router.push('/auth/login') - break; - } - if (config._retry) { - break; - } + response => { + return response.data; + }, + async err => { + const authStore = useAuthStore(); + const { config, response } = err; + if (response) { + switch (response.status) { + case 401: + if (authURL.some(x => config.url.includes(x))) { + authStore.logout(); + message.error('未登录,请登录后操作。'); + router.push('/auth/login') + break; + } + if (config._retry) { + break; + } - config._retry = true; - // 已经在刷新 → 排队 - if (isRefreshing) { - return new Promise(resolve => { - waitqueue.push(token => { - config.headers.Authorization = `Bearer ${token}` - resolve(api(config)) - }) - }) - } - - isRefreshing = true; - const refreshToken = authStore.refreshToken; - if (refreshToken != null && refreshToken != '') { - const res = await authService.refresh(refreshToken) - authStore.setLoginInfo(res.data.token, res.data.refreshToken, res.data.userInfo) - waitqueue.forEach(cb => cb(authStore.token)); - waitqueue = []; - config.headers.Authorization = `Bearer ${authStore.token}` - return api(config) - } - authStore.logout(); - message.error('未登录,请登录后操作。'); - router.push('/auth/login') - break; - case 400: - if (response.data && response.data.code == 1003) { - message.error(response.data.message); - break; - } - default: - message.error('请求错误,请检查网络。'); - break; - } - return Promise.reject(err); - } else { - message.error('请求错误,请检查网络。'); - return Promise.reject(err); - } + config._retry = true; + // 已经在刷新 → 排队 + if (isRefreshing) { + return new Promise(resolve => { + waitqueue.push(token => { + config.headers.Authorization = `Bearer ${token}` + resolve(api(config)) + }) + }) + } + isRefreshing = true; + const refreshToken = authStore.refreshToken; + if (refreshToken != null && refreshToken != '') { + const res = await authService.refresh(refreshToken) + authStore.setLoginInfo(res.data.token, res.data.refreshToken, res.data.userInfo) + waitqueue.forEach(cb => cb(authStore.token)); + waitqueue = []; + config.headers.Authorization = `Bearer ${authStore.token}` + return api(config) + } + authStore.logout(); + message.error('未登录,请登录后操作。'); + router.push('/auth/login') + break; + case 400: + if (response.data && response.data.code == 1003) { + message.error(response.data.message); + break; + } + default: + message.error('请求错误,请检查网络。'); + break; + } + return Promise.reject(err); + } else { + message.error('请求错误,请检查网络。'); + return Promise.reject(err); } + + } ) export const request = { - get: (url, config) => api.get(url, config), - post: (url, data, config) => api.post(url, data, config), - put: (url, data, config) => api.put(url, data, config), - delete: (url, config) => api.delete(url, config), - instance: api, -}; \ No newline at end of file + get: (url, config) => api.get(url, config), + post: (url, data, config) => api.post(url, data, config), + put: (url, data, config) => api.put(url, data, config), + delete: (url, config) => api.delete(url, config), + instance: api, +}; diff --git a/frontend/web/src/services/upload/uploadService.js b/frontend/web/src/services/upload/uploadService.js new file mode 100644 index 0000000..d1fcc15 --- /dev/null +++ b/frontend/web/src/services/upload/uploadService.js @@ -0,0 +1,52 @@ +import { request } from "../api"; + +export const uploadService = { + /** + * 创建文件上传任务 + * @param {*} fileName 文件名 + * @param {*} fileSize 文件大小 + * @param {*} contentType 文件类型 + * @param {*} fileHash 文件哈希 + * @returns + */ + createUploadTask: (fileName, fileSize, contentType, fileHash) => request.post('/Upload/CreateTask', { + fileName: fileName, + fileSize: fileSize, + contentType: contentType, + fileHash: fileHash + }), + /** + * 创建分段任务 + * @param {*} taskId 任务ID + * @param {*} partNum 分段序号 + * @returns + */ + createPartTask: (taskId, partNum) => request.post(`/Upload/CreatePart?taskId=${taskId}&partNum=${partNum}`), + + completeTask: (taskId, data) => request.post(`/Upload/CompleteTask?taskId=${taskId}`, data), + + uploadPart: (uploadUrl, headers, file, onProgress) => { + const formData = new FormData() + formData.append('file', file) + + return request.post( + uploadUrl, + formData, + { + baseURL: '', + headers, // 不要包含 Content-Type + onUploadProgress: e => { + if (onProgress && e.total) { + onProgress(e.loaded / e.total) + } + } + } + ) + }, + + uploadSmallFile: (file, hash) => { + const formData = new FormData() + formData.append('file', file) + return request.post(`/Upload/upload/${hash}`, formData); + } +} diff --git a/frontend/web/src/services/upload/uploader.js b/frontend/web/src/services/upload/uploader.js new file mode 100644 index 0000000..33f2406 --- /dev/null +++ b/frontend/web/src/services/upload/uploader.js @@ -0,0 +1,122 @@ +import { reactive } from "vue"; +import { uploadService } from "./uploadService"; +import { getFileHash, sliceFile } from "@/utils/uploadTools"; +import { request } from "../api"; +import { UPLOAD_STATUS } from "@/constants/uploadStatus"; + + +export const uploadFile = async (file, { + onProgress, + onPartComplete +} = {}) => { + + const fileHash = await getFileHash(file); + const { taskId, chunkSize, concurrency, skip, url } = (await uploadService.createUploadTask(file.name, file.size, file.type, fileHash)).data; + + if (skip) { + const uploadStatus = { + status: UPLOAD_STATUS.COMPLETE, + progress: 100, + taskId: taskId, + url: url + } + onProgress?.(uploadStatus) + return; + } + + const chunks = sliceFile(file, chunkSize); + + const comleteData = []; + + let chunkProgress = reactive(new Array(chunks.length).fill(0)) + + const tasks = chunks.map((chunk, index) => { + return async () => { + const partNum = index + 1; + const { skip, method, url, headers, partNumber } = (await uploadService.createPartTask(taskId, partNum)).data; + if (!skip) { + const { data } = await uploadService.uploadPart(url, headers, chunks[index], p => { + chunkProgress[index] = p; + // 第三步:计算总进度 + // 把账本上所有的百分比加起来 + const sum = chunkProgress.reduce((acc, cur) => acc + cur, 0); + const total = (sum / chunks.length) * 100; + const displayTotal = total.toFixed(2); + const uploadStatus = { + status: displayTotal == 100 ? UPLOAD_STATUS.UPLOADED : UPLOAD_STATUS.UPLOADING, + progress: displayTotal, + taskId: taskId, + url: null + } + onProgress?.(uploadStatus) + }); + onPartComplete?.(partNum) + return data; + } else { + return { skip, partNumber }; + } + } + }); + const results = await concurrentUpload(tasks, concurrency); + + const errors = results.filter(r => r.status === 'rejected'); + + if (errors.length > 0) return; + + await uploadService.completeTask(taskId, comleteData); + + const evtSource = new EventSource(`${request.instance.defaults.baseURL}/upload/events/${taskId}`); + + evtSource.onmessage = (event) => { + const data = JSON.parse(event.data); + const uploadStatus = { + status: data.progress == 100 ? UPLOAD_STATUS.COMPLETE : UPLOAD_STATUS.MERGING, + taskId: taskId, + progress: data.progress, + url: data.url + } + onProgress?.(uploadStatus) + + if (data.status === "Completed") { + evtSource.close(); + } + }; + + evtSource.onerror = (err) => { + console.error("SSE 连接异常", err); + }; +} + + +const concurrentUpload = async (tasks, limit = 3, maxRetries = 3) => { + const results = []; + const executing = []; + for (const task of tasks) { + const retryTask = async (task) => { + let attempt = 0; + while (attempt <= maxRetries) { + try { + return await task(); + } catch (e) { + attempt++; + if (attempt > maxRetries) { + throw e; + } + } + } + } + const p = Promise.resolve().then(() => retryTask(task)); + results.push(p); + + if (limit <= tasks.length) { + const e = p.finally(() => executing.splice(executing.indexOf(e), 1)); + executing.push(e); + + if (executing.length >= limit) { + await Promise.race(executing); + } + } + } + + return Promise.allSettled(results); +} diff --git a/frontend/web/src/stores/chat.js b/frontend/web/src/stores/chat.js index 8cd9039..49f9414 100644 --- a/frontend/web/src/stores/chat.js +++ b/frontend/web/src/stores/chat.js @@ -4,115 +4,118 @@ import { messageService } from "@/services/message"; import { useConversationStore } from "./conversation"; export const useChatStore = defineStore('chat', { - state: () => ({ - activeConversationId: null, - activeSessionId: null, - maxSequenceId: null, - isEnded: false, - messages: [], - pageSize: 20 - }), - actions: { - // 抽取统一的排序去重方法 - async pushAndSortMessagesAsync(newMsgs, sessionId, shouldSaveToDb = true) { - if (shouldSaveToDb) { - for (const m of newMsgs) { - await messagesDb.save({ ...m, sessionId }); - } - } - - if (sessionId == this.activeSessionId) { - const combined = [...this.messages, ...newMsgs]; - // 1. 根据 msgId 或唯一 key 去重 - const uniqueMap = new Map(); - combined.forEach(m => uniqueMap.set(m.msgId || m.sequenceId, m)); - - // 2. 转换为数组并按sequenceId升序排序 (旧的在前,新的在后) - this.messages = Array.from(uniqueMap.values()).sort((a, b) => { - return a.sequenceId - b.sequenceId; - }); - this.maxSequenceId = this.messages.reduce((max, m) => - m.sequenceId > max ? m.sequenceId : max, - null // 初始值 - ); - } - }, - /** - * 切换会话加载当前会话消息列表 - * @param {*} sessionId - */ - async swtichSession(sessionId, conversationId) { - this.activeSessionId = sessionId; - this.activeConversationId = conversationId; - this.messages = []; - this.isEnded = false; - //先从浏览器缓存加载一部分消息列表 - const localHistory = await messagesDb.getLatestMessages(sessionId, this.pageSize); - console.log(localHistory) - if (localHistory.length > 0) { - this.messages = localHistory; - this.maxSequenceId = this.messages.reduce((max, m) => - m.sequenceId > max ? m.sequenceId : max, - null // 初始值 - ); - } - }, - /** - * 从服务器加载新消息 - * @param {*} sessionId - * @returns - */ - async fetchNewMsgFromServier(conversationId, sequenceId) { - const newMsg = (await messageService.getMessages(conversationId, sequenceId, sequenceId ? 1 : 0, this.pageSize)).data; - if (newMsg.length > 0) { - return newMsg; - } else { - return []; - } - }, - /** - * 从服务器加载历史消息 - * @param {*} sessionId - * @param {*} msgId - * @returns - */ - async fetchHistoryFromServer(conversationId, sequenceId) { - const res = (await messageService.getMessages(conversationId, sequenceId, 0, this.pageSize)).data; - - if (res.length > 0) { - const sessionId = this.activeSessionId; - return res; - } else { - return []; - } - }, - /** - * 加载更多历史消息 - */ - async loadMoreMessages() { - let minSequenceId = 0; - if (!this.messages || this.messages.length === 0) return; - minSequenceId = this.messages.reduce((min, m) => - (m.sequenceId < min ? m.sequenceId : min), - this.messages[0].sequenceId // 使用第一项作为初始参考值 - ); - const dbCacheList = await messagesDb.getPageMessages(this.activeSessionId, minSequenceId, this.pageSize) - const dbMaxSequenceId = dbCacheList.reduce((max, m) => - m.sequenceId > max ? m.sequenceId : max, - null // 初始值 - ); - if (dbCacheList.length < this.pageSize) { - const newList = await this.fetchHistoryFromServer(this.activeConversationId, minSequenceId) - if (newList.length === 0) this.isEnded = true; - await this.pushAndSortMessagesAsync(newList, this.activeSessionId, true); - } else if (dbMaxSequenceId < minSequenceId - 1) { - const newList = await this.fetchHistoryFromServer(this.activeConversationId, minSequenceId) - if (newList.length === 0) this.isEnded = true; - await this.pushAndSortMessagesAsync(newList, this.activeSessionId, true); - } - else { - await this.pushAndSortMessagesAsync(dbCacheList, this.activeSessionId, false); - } + state: () => ({ + activeConversationId: null, + activeSessionId: null, + maxSequenceId: null, + isEnded: false, + messages: [], + pageSize: 20 + }), + actions: { + // 抽取统一的排序去重方法 + async pushAndSortMessagesAsync(newMsgs, sessionId, shouldSaveToDb = true) { + if (shouldSaveToDb) { + for (const m of newMsgs) { + if (m.type != 'Text' && !m.isLoading && !m.isError && !m.isImgLoading) { + m.content = JSON.parse(m.content); + } + await messagesDb.save({ ...m, sessionId }); } + } + + if (sessionId == this.activeSessionId) { + const combined = [...this.messages, ...newMsgs]; + // 1. 根据 msgId 或唯一 key 去重 + const uniqueMap = new Map(); + combined.forEach(m => uniqueMap.set(m.msgId || m.sequenceId, m)); + + // 2. 转换为数组并按sequenceId升序排序 (旧的在前,新的在后) + this.messages = Array.from(uniqueMap.values()).sort((a, b) => { + return a.sequenceId - b.sequenceId; + }); + this.maxSequenceId = this.messages.reduce((max, m) => + m.sequenceId > max ? m.sequenceId : max, + null // 初始值 + ); + } + }, + /** + * 切换会话加载当前会话消息列表 + * @param {*} sessionId + */ + async swtichSession(sessionId, conversationId) { + this.activeSessionId = sessionId; + this.activeConversationId = conversationId; + this.messages = []; + this.isEnded = false; + //先从浏览器缓存加载一部分消息列表 + const localHistory = await messagesDb.getLatestMessages(sessionId, this.pageSize); + console.log(localHistory) + if (localHistory.length > 0) { + this.messages = localHistory; + this.maxSequenceId = this.messages.reduce((max, m) => + m.sequenceId > max ? m.sequenceId : max, + null // 初始值 + ); + } + }, + /** + * 从服务器加载新消息 + * @param {*} sessionId + * @returns + */ + async fetchNewMsgFromServier(conversationId, sequenceId) { + const newMsg = (await messageService.getMessages(conversationId, sequenceId, sequenceId ? 1 : 0, this.pageSize)).data; + if (newMsg.length > 0) { + return newMsg; + } else { + return []; + } + }, + /** + * 从服务器加载历史消息 + * @param {*} sessionId + * @param {*} msgId + * @returns + */ + async fetchHistoryFromServer(conversationId, sequenceId) { + const res = (await messageService.getMessages(conversationId, sequenceId, 0, this.pageSize)).data; + + if (res.length > 0) { + const sessionId = this.activeSessionId; + return res; + } else { + return []; + } + }, + /** + * 加载更多历史消息 + */ + async loadMoreMessages() { + let minSequenceId = 0; + if (!this.messages || this.messages.length === 0) return; + minSequenceId = this.messages.reduce((min, m) => + (m.sequenceId < min ? m.sequenceId : min), + this.messages[0].sequenceId // 使用第一项作为初始参考值 + ); + const dbCacheList = await messagesDb.getPageMessages(this.activeSessionId, minSequenceId, this.pageSize) + const dbMaxSequenceId = dbCacheList.reduce((max, m) => + m.sequenceId > max ? m.sequenceId : max, + null // 初始值 + ); + if (dbCacheList.length < this.pageSize) { + const newList = await this.fetchHistoryFromServer(this.activeConversationId, minSequenceId) + if (newList.length === 0) this.isEnded = true; + await this.pushAndSortMessagesAsync(newList, this.activeSessionId, true); + } else if (dbMaxSequenceId < minSequenceId - 1) { + const newList = await this.fetchHistoryFromServer(this.activeConversationId, minSequenceId) + if (newList.length === 0) this.isEnded = true; + await this.pushAndSortMessagesAsync(newList, this.activeSessionId, true); + } + else { + await this.pushAndSortMessagesAsync(dbCacheList, this.activeSessionId, false); + } } -}) \ No newline at end of file + } +}) diff --git a/frontend/web/src/utils/imageTools.js b/frontend/web/src/utils/imageTools.js new file mode 100644 index 0000000..eaad104 --- /dev/null +++ b/frontend/web/src/utils/imageTools.js @@ -0,0 +1,119 @@ +export const loadImage = (url) => { + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => resolve(img); // 成功时返回 img 对象 + img.onerror = (err) => reject(err); // 失败时报错 + img.src = url; + }); +}; +/** +* 生成图片缩略图 (返回 Blob 文件) +*/ +export function generateImageThumbnailBlob(img, maxWidth = 200) { + return new Promise((resolve) => { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + const scale = maxWidth / img.width; + canvas.width = maxWidth; + canvas.height = img.height * scale; + + ctx.drawImage(img, 0, 0, canvas.width, canvas.height); + + // 关键:导出为 Blob + canvas.toBlob((blob) => { + resolve(blob); + }, 'image/jpeg', 0.8); + }); +} + + +/** + * 获取视频缩略图 (返回 Blob 文件) + * 报错或无法渲染画面时自动生成全黑占位图 + */ +export function getVideoThumbnailBlob(file, seekTime = 1) { + return new Promise((resolve) => { + const video = document.createElement('video'); + const url = URL.createObjectURL(file); + video.muted = true; + video.src = url; + + // 统一定义一个生成黑色背景的方法 + const resolveBlackThumbnail = () => { + const canvas = document.createElement('canvas'); + // 如果视频元数据拿不到宽高,给一个默认的 16:9 尺寸 + canvas.width = video.videoWidth || 320; + canvas.height = video.videoHeight || 180; + const ctx = canvas.getContext('2d'); + ctx.fillStyle = '#000000'; // 全黑 + ctx.fillRect(0, 0, canvas.width, canvas.height); + + canvas.toBlob((blob) => { + URL.revokeObjectURL(url); + resolve(blob); + }, 'image/jpeg', 0.8); + }; + + video.onloadedmetadata = () => { + // 检查:如果元数据加载了但没有画面尺寸(只有音频的视频常现) + if (video.videoWidth === 0) { + resolveBlackThumbnail(); + } else { + video.currentTime = seekTime; + } + }; + + video.onseeked = () => { + try { + const canvas = document.createElement('canvas'); + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + const ctx = canvas.getContext('2d'); + + // 尝试绘制,如果解码失败这里可能抛出异常 + ctx.drawImage(video, 0, 0, canvas.width, canvas.height); + + canvas.toBlob((blob) => { + URL.revokeObjectURL(url); + // 如果导出的 blob 大小极小(可能是空的),可以在这里进一步检查 + resolve(blob); + }, 'image/jpeg', 0.8); + } catch (e) { + resolveBlackThumbnail(); + } + }; + + // 报错处理:不再 reject,而是给一张黑图 + video.onerror = () => { + console.warn("视频封面截取失败,已生成全黑占位图"); + resolveBlackThumbnail(); + }; + }); +} + +/** + * 获取视频时长 + * @param {File} file - 视频文件对象 + * @returns {Promise} - 返回秒数 (float) + */ +export function getVideoDuration(file) { + return new Promise((resolve, reject) => { + const video = document.createElement('video'); + const url = URL.createObjectURL(file); + + video.preload = 'metadata'; // 关键:只加载元数据 + video.src = url; + + // 当元数据加载完成后触发 + video.onloadedmetadata = () => { + const duration = video.duration; + URL.revokeObjectURL(url); // 释放内存 + resolve(duration); + }; + + video.onerror = () => { + URL.revokeObjectURL(url); + reject("无法解析视频元数据"); + }; + }); +} diff --git a/frontend/web/src/utils/uploadTools.js b/frontend/web/src/utils/uploadTools.js new file mode 100644 index 0000000..aaa88d9 --- /dev/null +++ b/frontend/web/src/utils/uploadTools.js @@ -0,0 +1,55 @@ +import SparkMD5 from "spark-md5"; + +export const getFileHash = (file) => { + return new Promise((resolve, reject) => { + const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice; + const chunkSize = 2 * 1024 * 1024; // 每次读取 2MB + const chunks = Math.ceil(file.size / chunkSize); + let currentChunk = 0; + const spark = new SparkMD5.ArrayBuffer(); + const fileReader = new FileReader(); + + fileReader.onload = (e) => { + spark.append(e.target.result); // 将二进制数据添加到计算器 + currentChunk++; + + if (currentChunk < chunks) { + loadNext(); + } else { + resolve(spark.end()); // 完成计算,返回最终结果 + } + }; + + fileReader.onerror = () => { + reject('文件读取出错'); + }; + + function loadNext() { + const start = currentChunk * chunkSize; + const end = start + chunkSize >= file.size ? file.size : start + chunkSize; + fileReader.readAsArrayBuffer(blobSlice.call(file, start, end)); + } + + loadNext(); + }); +} + +/** + * 文件分片 + * @param {*} file 文件 + * @param {*} chunkSize 分片大小 + * @returns + */ +export const sliceFile = (file, chunkSize) => { + const chuncks = []; + let index = 0; + + while (index < file.size) { + chuncks.push(file.slice(index, index + chunkSize)); + index += chunkSize; + } + + return chuncks; +} + + diff --git a/frontend/web/src/views/Test.vue b/frontend/web/src/views/Test.vue index f5eb261..f60e362 100644 --- a/frontend/web/src/views/Test.vue +++ b/frontend/web/src/views/Test.vue @@ -7,38 +7,18 @@ - - -
- - -
-
- 未找到该用户,请检查输入是否正确 -
-