diff --git a/YD_Event.Application/Client/Dtos/MyDtos.cs b/YD_Event.Application/Client/Dtos/MyDtos.cs index 5d4c480..2c4d568 100644 --- a/YD_Event.Application/Client/Dtos/MyDtos.cs +++ b/YD_Event.Application/Client/Dtos/MyDtos.cs @@ -600,3 +600,14 @@ public class MyRegistrationDto /// public string ProjectNames { get; set; } } + +/// +/// 更新头像输出 +/// +public class UpdateAvatarOutput +{ + /// + /// 头像URL + /// + public string AvatarUrl { get; set; } = string.Empty; +} diff --git a/YD_Event.Application/Client/MyController.cs b/YD_Event.Application/Client/MyController.cs index 3034738..1245397 100644 --- a/YD_Event.Application/Client/MyController.cs +++ b/YD_Event.Application/Client/MyController.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Mvc; using YD_Event.Application.Client.Dtos; +using YD_Event.Application.Common; using YD_Event.Core.Entities; using YD_Event.Core.Enum; using SqlSugar; @@ -8,6 +9,7 @@ using Microsoft.AspNetCore.Authorization; using System.Security.Claims; using Furion.FriendlyException; using EasyCaching.Core; +using SkiaSharp; using ProjectEntity = YD_Event.Core.Entities.Project; namespace YD_Event.Application.Client; @@ -31,6 +33,7 @@ public class MyController : IDynamicApiController private readonly ISqlSugarRepository _merchantCertificationRepository; private readonly IHttpContextAccessor _httpContextAccessor; private readonly IEasyCachingProvider _cachingProvider; + private readonly FileService _fileService; public MyController( ISqlSugarRepository appUserRepository, @@ -44,7 +47,8 @@ public class MyController : IDynamicApiController ISqlSugarRepository eventRepository, ISqlSugarRepository merchantCertificationRepository, IHttpContextAccessor httpContextAccessor, - IEasyCachingProvider cachingProvider) + IEasyCachingProvider cachingProvider, + FileService fileService) { _appUserRepository = appUserRepository; _teamRepository = teamRepository; @@ -58,6 +62,7 @@ public class MyController : IDynamicApiController _merchantCertificationRepository = merchantCertificationRepository; _httpContextAccessor = httpContextAccessor; _cachingProvider = cachingProvider; + _fileService = fileService; } private long GetCurrentUserId() @@ -658,4 +663,157 @@ public class MyController : IDynamicApiController if (string.IsNullOrEmpty(mobile) || mobile.Length < 7) return mobile; return mobile.Substring(0, 3) + "****" + mobile.Substring(7); } + + /// + /// 更新头像 - 上传图片文件 + /// + /// 头像图片文件 + /// 更新后的头像信息 + [HttpPost("avatar/upload")] + [DisplayName("上传头像")] + public async Task UpdateAvatarByFile(IFormFile file) + { + if (file == null || file.Length == 0) + { + throw Oops.Bah("请上传头像图片"); + } + + // 验证文件类型 + var allowedExtensions = new[] { ".jpg", ".jpeg", ".png", ".gif", ".bmp" }; + var extension = Path.GetExtension(file.FileName).ToLower(); + if (!allowedExtensions.Contains(extension)) + { + throw Oops.Bah("仅支持 JPG、JPEG、PNG、GIF、BMP 格式的图片"); + } + + // 验证文件大小 (最大 5MB) + if (file.Length > 5 * 1024 * 1024) + { + throw Oops.Bah("图片大小不能超过 5MB"); + } + + // 处理图片 + using var processedStream = await ProcessAvatarImage(file.OpenReadStream()); + + // 上传处理后的图片 + var now = DateTime.Now; + var fileName = $"avatar/{now.Year}/{now.Month:D2}/{now.Day:D2}/{Yitter.IdGenerator.YitIdHelper.NextId()}.jpg"; + var avatarUrl = await _fileService.UploadFileStreamAsync(fileName, processedStream); + + // 更新用户头像 + var userId = GetCurrentUserId(); + var user = await _appUserRepository.GetFirstAsync(u => u.Id == userId); + if (user == null) + { + throw Oops.Bah("用户信息不存在"); + } + + user.Avatar = avatarUrl; + user.UpdatedTime = DateTime.Now; + await _appUserRepository.UpdateAsync(user); + + return new UpdateAvatarOutput + { + AvatarUrl = avatarUrl + }; + } + + /// + /// 更新头像 - 使用图片URL + /// + /// 头像图片URL + /// 更新后的头像信息 + [HttpPost("avatar/url")] + [DisplayName("更新头像URL")] + public async Task UpdateAvatarByUrl([FromBody] string avatarUrl) + { + if (string.IsNullOrWhiteSpace(avatarUrl)) + { + throw Oops.Bah("头像URL不能为空"); + } + + // 验证URL格式 + if (!Uri.TryCreate(avatarUrl, UriKind.Absolute, out _)) + { + throw Oops.Bah("头像URL格式不正确"); + } + + // 更新用户头像 + var userId = GetCurrentUserId(); + var user = await _appUserRepository.GetFirstAsync(u => u.Id == userId); + if (user == null) + { + throw Oops.Bah("用户信息不存在"); + } + + user.Avatar = avatarUrl; + user.UpdatedTime = DateTime.Now; + await _appUserRepository.UpdateAsync(user); + + return new UpdateAvatarOutput + { + AvatarUrl = avatarUrl + }; + } + + /// + /// 处理头像图片 - 压缩和格式转换 + /// + /// 原始图片流 + /// 处理后的图片流 + private async Task ProcessAvatarImage(Stream inputStream) + { + // 将流复制到内存流,以便可以多次读取 + var memoryStream = new MemoryStream(); + await inputStream.CopyToAsync(memoryStream); + memoryStream.Position = 0; + + // 使用 SkiaSharp 处理图片 + using var originalBitmap = SKBitmap.Decode(memoryStream); + if (originalBitmap == null) + { + throw Oops.Bah("图片解码失败,请检查图片格式"); + } + + // 计算缩放后的尺寸(最大 200x200) + const int maxSize = 200; + int width = originalBitmap.Width; + int height = originalBitmap.Height; + + if (width > maxSize || height > maxSize) + { + if (width > height) + { + height = (int)(height * (double)maxSize / width); + width = maxSize; + } + else + { + width = (int)(width * (double)maxSize / height); + height = maxSize; + } + } + + // 创建缩放后的图片 + using var scaledBitmap = originalBitmap.Resize(new SKImageInfo(width, height), SKFilterQuality.High); + + // 裁剪为正方形(从中心裁剪) + int cropSize = Math.Min(width, height); + int cropX = (width - cropSize) / 2; + int cropY = (height - cropSize) / 2; + + using var croppedBitmap = new SKBitmap(cropSize, cropSize); + using var canvas = new SKCanvas(croppedBitmap); + canvas.Clear(SKColors.Transparent); + canvas.DrawBitmap(scaledBitmap, new SKRect(cropX, cropY, cropX + cropSize, cropY + cropSize), new SKRect(0, 0, cropSize, cropSize)); + + // 转换为 JPG 格式并压缩 + var outputStream = new MemoryStream(); + using var image = SKImage.FromBitmap(croppedBitmap); + using var data = image.Encode(SKEncodedImageFormat.Jpeg, 80); // 80% 质量 + data.SaveTo(outputStream); + outputStream.Position = 0; + + return outputStream; + } }