diff --git a/QYZH.InteractiveMagazine.IService/IPetService.cs b/QYZH.InteractiveMagazine.IService/IPetService.cs index c982f4c..a0b9f48 100644 --- a/QYZH.InteractiveMagazine.IService/IPetService.cs +++ b/QYZH.InteractiveMagazine.IService/IPetService.cs @@ -152,4 +152,21 @@ public interface IPetService : IBaseService /// 获取指定皮肤的所有图片 /// Task> GetSkinImagesBySkinIdAsync(long skinId); + + // ==================== 便捷查询接口 ==================== + + /// + /// 根据模板Id获取进化链列表(非分页,按StageLevel排序) + /// + Task> GetEvolutionsByTemplateIdAsync(long templateId); + + /// + /// 根据模板Id获取皮肤列表(非分页,按SortOrder排序) + /// + Task> GetSkinsByTemplateIdAsync(long templateId); + + /// + /// 根据皮肤Id获取各阶段图片列表(按进化阶段分组,含阶段基本信息) + /// + Task> GetSkinImagesGroupedBySkinIdAsync(long skinId); } diff --git a/QYZH.InteractiveMagazine.Infrastructure/OSS/OssImageHelper.cs b/QYZH.InteractiveMagazine.Infrastructure/OSS/OssImageHelper.cs new file mode 100644 index 0000000..aed0d25 --- /dev/null +++ b/QYZH.InteractiveMagazine.Infrastructure/OSS/OssImageHelper.cs @@ -0,0 +1,110 @@ +using Microsoft.Extensions.Logging; +using QYZH.InteractiveMagazine.Common.Extensions; +using QYZH.InteractiveMagazine.Models.Common; + +namespace QYZH.InteractiveMagazine.Infrastructure.OSS; + +/// +/// OSS 图片搬运辅助类 +/// 负责将 temp 临时目录下的文件搬运到正式目录 +/// +public class OssImageHelper +{ + private readonly OssService _ossService; + private readonly ILogger _logger; + + public OssImageHelper(OssService ossService, ILogger logger) + { + _ossService = ossService; + _logger = logger; + } + + /// + /// 判断图片是否在 temp 临时目录 + /// + public static bool IsTempImage(string? imageUrl) + { + if (string.IsNullOrWhiteSpace(imageUrl)) return false; + return imageUrl.Contains("/temp/", StringComparison.OrdinalIgnoreCase); + } + + /// + /// 将 temp 目录下的图片搬运到正式目录,返回正式路径 + /// 如果图片不在 temp 目录,原样返回 + /// + /// 原始图片 URL(可能是完整 URL 或相对路径) + /// 正式目录前缀,如 "product/makeupcard/123" 或 "pet/skin/456" + /// 正式路径(相对路径),搬运失败时抛出异常 + public async Task MoveToFormalAsync(string imageUrl, string formalFolder) + { + if (!IsTempImage(imageUrl)) + return imageUrl; + + // 提取 OSS 相对路径(去掉域名前缀) + var sourcePath = imageUrl.RemoveDomain(); + if (string.IsNullOrWhiteSpace(sourcePath)) + { + _logger.LogWarning("图片路径解析失败,ImageUrl: {ImageUrl}", imageUrl); + return imageUrl; + } + + // URL 解码(处理中文文件名) + sourcePath = Uri.UnescapeDataString(sourcePath); + + // 提取文件名 + var fileName = Path.GetFileName(sourcePath); + if (string.IsNullOrWhiteSpace(fileName)) + { + _logger.LogWarning("无法从路径中提取文件名,Path: {Path}", sourcePath); + return imageUrl; + } + + // 生成正式路径 + var targetPath = $"{formalFolder}/{fileName}"; + + _logger.LogInformation("开始搬运图片,Source: {Source} -> Target: {Target}", sourcePath, targetPath); + + try + { + // 拷贝文件到正式目录 + var copyResult = _ossService.CopyObject(sourcePath, targetPath); + if (!copyResult) + { + _logger.LogError("OSS 图片拷贝失败,Source: {Source}, Target: {Target}", sourcePath, targetPath); + throw new Exception("OSS 图片拷贝失败"); + } + + // 删除临时文件(失败不影响主流程) + var deleteResult = _ossService.DeleteObject(sourcePath); + if (!deleteResult) + { + _logger.LogWarning("OSS 临时文件删除失败(不影响主流程),Source: {Source}", sourcePath); + } + + _logger.LogInformation("图片搬运成功,Target: {Target}", targetPath); + return targetPath; + } + catch (Exception ex) when (ex is not BusinessException) + { + _logger.LogError(ex, "OSS 图片搬运异常,Source: {Source}, Target: {Target}", sourcePath, targetPath); + throw new BusinessException("图片搬运失败,请重试", 500); + } + } + + /// + /// 批量搬运 temp 图片到正式目录 + /// + /// 原始图片 URL 列表 + /// 正式目录前缀 + /// 正式路径列表(顺序与输入一致) + public async Task> MoveBatchToFormalAsync(IEnumerable imageUrls, string formalFolder) + { + var results = new List(); + foreach (var url in imageUrls) + { + var formal = await MoveToFormalAsync(url, formalFolder); + results.Add(formal); + } + return results; + } +} diff --git a/QYZH.InteractiveMagazine.Models/Dto/Pet/PetSkinDto.cs b/QYZH.InteractiveMagazine.Models/Dto/Pet/PetSkinDto.cs index c1fac68..1edf671 100644 --- a/QYZH.InteractiveMagazine.Models/Dto/Pet/PetSkinDto.cs +++ b/QYZH.InteractiveMagazine.Models/Dto/Pet/PetSkinDto.cs @@ -138,3 +138,16 @@ public class PetSkinImageOutput public string? UpdatedBy { get; set; } public DateTime? UpdatedAt { get; set; } } + +/// +/// 皮肤按进化阶段分组的图片输出(每阶段包含阶段信息和图片列表) +/// +public class SkinEvolutionStageOutput +{ + public long EvolutionId { get; set; } + public string StageName { get; set; } = string.Empty; + public int StageLevel { get; set; } + public int RequiredGrowth { get; set; } + public long? PreviousEvolutionId { get; set; } + public List Images { get; set; } = new(); +} diff --git a/QYZH.InteractiveMagazine.Service/PetService.cs b/QYZH.InteractiveMagazine.Service/PetService.cs index caf6ccb..61a533f 100644 --- a/QYZH.InteractiveMagazine.Service/PetService.cs +++ b/QYZH.InteractiveMagazine.Service/PetService.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Logging; +using QYZH.InteractiveMagazine.Infrastructure.OSS; using QYZH.InteractiveMagazine.IService; using QYZH.InteractiveMagazine.Models.Common; using QYZH.InteractiveMagazine.Models.Dto; @@ -20,9 +21,11 @@ public class PetService( BaseRepository petEvolutionRepository, BaseRepository petSkinImageRepository, BaseRepository petSkinRepository, + OssService ossService, ILogger logger) : BaseRepository, IPetService { + private OssImageHelper CreateImageHelper() => new(ossService, logger); /// /// 获取用户宠物信息(含当前形态名称、皮肤图片序列) /// @@ -442,6 +445,8 @@ public class PetService( // 3. 为该阶段创建默认皮肤图片 if (evoInput.Images != null && evoInput.Images.Count > 0) { + var helper = CreateImageHelper(); + foreach (var imgInput in evoInput.Images.OrderBy(i => i.SortOrder)) { var skinImage = new PetSkinImage @@ -460,6 +465,14 @@ public class PetService( await petSkinImageRepository.InsertAsync(skinImage); + // 搬运图片到正式目录 + if (OssImageHelper.IsTempImage(skinImage.ImageUrl)) + { + skinImage.ImageUrl = await helper.MoveToFormalAsync( + skinImage.ImageUrl, $"pet/template/{template.Id}/stage/{evolution.Id}"); + await petSkinImageRepository.UpdateAsync(skinImage); + } + // 记录初始阶段的封面图 if (previousEvolution == null && imgInput.Type == PetSkinImageTypeEnum.Cover.ToString() @@ -850,6 +863,14 @@ public class PetService( if (!result) throw new BusinessException("创建皮肤失败", 500); + // 搬运封面图到正式目录 + if (OssImageHelper.IsTempImage(skin.CoverImageUrl)) + { + var helper = CreateImageHelper(); + skin.CoverImageUrl = await helper.MoveToFormalAsync(skin.CoverImageUrl, $"pet/skin/{skin.Id}"); + await petSkinRepository.UpdateAsync(skin); + } + logger.LogInformation("创建皮肤成功,Id: {Id}, Name: {Name}", skin.Id, skin.Name); return BuildSkinOutput(skin, null); } @@ -872,7 +893,16 @@ public class PetService( skin.Rarity = input.Rarity; skin.SortOrder = input.SortOrder; skin.Type = Enum.Parse(input.Type, true); - skin.CoverImageUrl = input.CoverImageUrl; + + // 搬运封面图到正式目录 + var newCoverUrl = input.CoverImageUrl; + if (OssImageHelper.IsTempImage(newCoverUrl)) + { + var helper = CreateImageHelper(); + newCoverUrl = await helper.MoveToFormalAsync(newCoverUrl, $"pet/skin/{skin.Id}"); + } + skin.CoverImageUrl = newCoverUrl; + skin.UpdatedBy = "System"; skin.UpdatedAt = DateTime.Now; @@ -1045,6 +1075,14 @@ public class PetService( if (!result) throw new BusinessException("创建皮肤图片失败", 500); + // 搬运图片到正式目录 + if (OssImageHelper.IsTempImage(image.ImageUrl)) + { + var helper = CreateImageHelper(); + image.ImageUrl = await helper.MoveToFormalAsync(image.ImageUrl, $"pet/skinimage/{image.Id}"); + await petSkinImageRepository.UpdateAsync(image); + } + logger.LogInformation("创建皮肤图片成功,Id: {Id}, SkinId: {SkinId}", image.Id, image.SkinId); return BuildSkinImageOutput(image); } @@ -1063,7 +1101,16 @@ public class PetService( image.SkinId = input.SkinId; image.EvolutionStageId = input.EvolutionStageId; - image.ImageUrl = input.ImageUrl.Trim(); + + // 搬运图片到正式目录 + var newImageUrl = input.ImageUrl.Trim(); + if (OssImageHelper.IsTempImage(newImageUrl)) + { + var helper = CreateImageHelper(); + newImageUrl = await helper.MoveToFormalAsync(newImageUrl, $"pet/skinimage/{image.Id}"); + } + image.ImageUrl = newImageUrl; + image.SortOrder = input.SortOrder; image.Type = Enum.Parse(input.Type, true); image.UpdatedBy = "System"; @@ -1123,4 +1170,115 @@ public class PetService( UpdatedAt = i.UpdatedAt }; } + + // ==================== 便捷查询接口 ==================== + + /// + /// 根据模板Id获取进化链列表(非分页,按StageLevel排序) + /// + public async Task> GetEvolutionsByTemplateIdAsync(long templateId) + { + if (templateId <= 0) + throw new BusinessException("模板Id不能为空", 400); + + var list = await petEvolutionRepository.Queryable() + .Where(e => e.TemplateId == templateId && !e.IsDeleted) + .OrderBy(e => e.StageLevel) + .ToListAsync(); + + return list.Select(BuildEvolutionOutput).ToList(); + } + + /// + /// 根据模板Id获取皮肤列表(非分页,按SortOrder排序) + /// + public async Task> GetSkinsByTemplateIdAsync(long templateId) + { + if (templateId <= 0) + throw new BusinessException("模板Id不能为空", 400); + + var list = await petSkinRepository.Queryable() + .Where(s => s.TemplateId == templateId && !s.IsDeleted) + .OrderBy(s => s.SortOrder) + .ToListAsync(); + + var result = list.Select(s => new PetSkinOutput + { + Id = s.Id, + TemplateId = s.TemplateId, + Name = s.Name, + Description = s.Description, + Rarity = s.Rarity, + SortOrder = s.SortOrder, + Type = s.Type.ToString(), + CoverImageUrl = s.CoverImageUrl, + CreatedBy = s.CreatedBy, + CreatedAt = s.CreatedAt, + UpdatedBy = s.UpdatedBy, + UpdatedAt = s.UpdatedAt + }).ToList(); + + // 批量查询关联图片 + var skinIds = result.Select(s => s.Id).ToList(); + if (skinIds.Count > 0) + { + var allImages = await petSkinImageRepository.Queryable() + .Where(i => skinIds.Contains(i.SkinId) && !i.IsDeleted) + .OrderBy(i => i.SortOrder) + .ToListAsync(); + + var imageMap = allImages + .GroupBy(i => i.SkinId) + .ToDictionary(g => g.Key, g => g.Select(BuildSkinImageOutput).ToList()); + + foreach (var skin in result) + { + if (imageMap.ContainsKey(skin.Id)) + skin.Images = imageMap[skin.Id]; + } + } + + return result; + } + + /// + /// 根据皮肤Id获取各阶段图片列表(按进化阶段分组,含阶段基本信息) + /// + public async Task> GetSkinImagesGroupedBySkinIdAsync(long skinId) + { + if (skinId <= 0) + throw new BusinessException("皮肤Id不能为空", 400); + + // 1. 获取皮肤信息 + var skin = await petSkinRepository.GetByIdAsync(skinId); + if (skin == null || skin.IsDeleted) + throw new BusinessException("皮肤不存在", 404); + + // 2. 获取该模板的所有进化阶段(按阶段等级排序) + var evolutions = await petEvolutionRepository.Queryable() + .Where(e => e.TemplateId == skin.TemplateId && !e.IsDeleted) + .OrderBy(e => e.StageLevel) + .ToListAsync(); + + // 3. 获取该皮肤的所有图片 + var images = await petSkinImageRepository.Queryable() + .Where(i => i.SkinId == skinId && !i.IsDeleted) + .OrderBy(i => i.SortOrder) + .ToListAsync(); + + var imagesByStage = images + .GroupBy(i => i.EvolutionStageId) + .ToDictionary(g => g.Key, g => g.Select(BuildSkinImageOutput).ToList()); + + // 4. 组装结果:每个进化阶段 + 对应的图片列表 + return evolutions.Select(e => new SkinEvolutionStageOutput + { + EvolutionId = e.Id, + StageName = e.StageName, + StageLevel = e.StageLevel, + RequiredGrowth = e.RequiredGrowth, + PreviousEvolutionId = e.PreviousEvolutionId, + Images = imagesByStage.GetValueOrDefault(e.Id, new List()) + }).ToList(); + } } diff --git a/QYZH.InteractiveMagazine.Service/ProductService.cs b/QYZH.InteractiveMagazine.Service/ProductService.cs index b006abe..db11059 100644 --- a/QYZH.InteractiveMagazine.Service/ProductService.cs +++ b/QYZH.InteractiveMagazine.Service/ProductService.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Logging; +using QYZH.InteractiveMagazine.Infrastructure.OSS; using QYZH.InteractiveMagazine.IService; using QYZH.InteractiveMagazine.Models.Common; using QYZH.InteractiveMagazine.Models.Dto; @@ -12,7 +13,10 @@ namespace QYZH.InteractiveMagazine.Service; /// /// 商品服务实现 /// -public class ProductService(BaseRepository productRepository, ILogger logger) : BaseRepository, IProductService +public class ProductService( + BaseRepository productRepository, + OssService ossService, + ILogger logger) : BaseRepository, IProductService { /// @@ -62,6 +66,16 @@ public class ProductService(BaseRepository productRepository, ILogger

productRepository, ILogger

(input.Type, true), product.Id); + newImageUrl = await helper.MoveToFormalAsync(newImageUrl, folder); + } + product.Name = input.Name.Trim(); product.Description = input.Description; - product.ImageUrl = input.ImageUrl; + product.ImageUrl = newImageUrl; product.Price = input.Price; product.Type = Enum.Parse(input.Type, true); product.Status = (int)input.Status; @@ -284,4 +307,21 @@ public class ProductService(BaseRepository productRepository, ILogger

new(ossService, logger); + + private static string GetProductFormalFolder(ProductTypeEnum type, long productId) + { + var typeFolder = type switch + { + ProductTypeEnum.MakeUpCard => "makeupcard", + ProductTypeEnum.PetSkin => "petskin", + _ => "other" + }; + return $"product/{typeFolder}/{productId}"; + } + + #endregion } diff --git a/QYZH.InteractiveMagazine.Service/WeChatAuthService.cs b/QYZH.InteractiveMagazine.Service/WeChatAuthService.cs index 4513a15..40b9f43 100644 --- a/QYZH.InteractiveMagazine.Service/WeChatAuthService.cs +++ b/QYZH.InteractiveMagazine.Service/WeChatAuthService.cs @@ -77,7 +77,7 @@ public class WeChatAuthService(BaseRepository usersRepository, IConfigura IsLastOnline = true }; - var insertResult = await usersRepository.Insertable(newUser).ExecuteReturnIdentityAsync(); + var insertResult = usersRepository.Insertable(newUser).ExecuteReturnIdentity(); if (insertResult <= 0) { logger.LogError("创建微信用户失败,OpenId: {OpenId}", wxResponse.OpenId); diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/PetManageController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/PetManageController.cs index 830c4dc..7e5c394 100644 --- a/QYZH.InteractiveMagazine.WebApi/Controllers/PetManageController.cs +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/PetManageController.cs @@ -491,4 +491,75 @@ public class PetManageController : BaseController return BaseResponse>.Fail("获取皮肤图片列表失败,请稍后重试"); } } + + // ==================== 便捷查询接口 ==================== + + ///

+ /// 根据模板Id获取进化链列表(非分页) + /// + [HttpGet("evolutions/{templateId}")] + public async Task>> GetEvolutionsByTemplateIdAsync(long templateId) + { + try + { + var result = await _petService.GetEvolutionsByTemplateIdAsync(templateId); + return Success(result); + } + catch (BusinessException ex) + { + _logger.LogWarning(ex, "获取进化链列表业务异常: {Message}", ex.Message); + return BaseResponse>.Fail(ex.Message); + } + catch (Exception ex) + { + _logger.LogError(ex, "获取进化链列表系统异常,TemplateId:{TemplateId}", templateId); + return BaseResponse>.Fail("获取进化链列表失败,请稍后重试"); + } + } + + /// + /// 根据模板Id获取皮肤列表(非分页) + /// + [HttpGet("skins/{templateId}")] + public async Task>> GetSkinsByTemplateIdAsync(long templateId) + { + try + { + var result = await _petService.GetSkinsByTemplateIdAsync(templateId); + return Success(result); + } + catch (BusinessException ex) + { + _logger.LogWarning(ex, "获取皮肤列表业务异常: {Message}", ex.Message); + return BaseResponse>.Fail(ex.Message); + } + catch (Exception ex) + { + _logger.LogError(ex, "获取皮肤列表系统异常,TemplateId:{TemplateId}", templateId); + return BaseResponse>.Fail("获取皮肤列表失败,请稍后重试"); + } + } + + /// + /// 根据皮肤Id获取各阶段图片列表(按进化阶段分组,含阶段基本信息) + /// + [HttpGet("skin-images-grouped/{skinId}")] + public async Task>> GetSkinImagesGroupedBySkinIdAsync(long skinId) + { + try + { + var result = await _petService.GetSkinImagesGroupedBySkinIdAsync(skinId); + return Success(result); + } + catch (BusinessException ex) + { + _logger.LogWarning(ex, "获取分组皮肤图片业务异常: {Message}", ex.Message); + return BaseResponse>.Fail(ex.Message); + } + catch (Exception ex) + { + _logger.LogError(ex, "获取分组皮肤图片系统异常,SkinId:{SkinId}", skinId); + return BaseResponse>.Fail("获取分组皮肤图片失败,请稍后重试"); + } + } } diff --git a/QYZH.InteractiveMagazine.WebApi/Program.cs b/QYZH.InteractiveMagazine.WebApi/Program.cs index a8a7d40..2622f55 100644 --- a/QYZH.InteractiveMagazine.WebApi/Program.cs +++ b/QYZH.InteractiveMagazine.WebApi/Program.cs @@ -13,6 +13,7 @@ using QYZH.InteractiveMagazine.Infrastructure.Context; using QYZH.InteractiveMagazine.Infrastructure.Extensions; using QYZH.InteractiveMagazine.Infrastructure.Middleware; using QYZH.InteractiveMagazine.Infrastructure.Redis; +using QYZH.InteractiveMagazine.Infrastructure.SDK; using QYZH.InteractiveMagazine.Models.Entity; using QYZH.InteractiveMagazine.Models.Enum; using QYZH.InteractiveMagazine.Models.Settings; @@ -162,6 +163,7 @@ builder.Services.AddCors(options => .AllowAnyHeader(); }); }); +builder.Services.AddSDKService(builder.Configuration); #region 配置数据压缩选项 diff --git a/QYZH.InteractiveMagazine.WebApi/appsettings.json b/QYZH.InteractiveMagazine.WebApi/appsettings.json index 4b83df5..8669a56 100644 --- a/QYZH.InteractiveMagazine.WebApi/appsettings.json +++ b/QYZH.InteractiveMagazine.WebApi/appsettings.json @@ -21,8 +21,8 @@ "VirtualHost": "InteractiveMagazine" }, "WeChatSettings": { - "AppId": "your-wechat-appid", - "AppSecret": "your-wechat-appsecret" + "AppId": "wx7922cc9b6023f3ac", + "AppSecret": "95a1d61b7be03d1dab81fe19b7483dcf" }, "Serilog": { "MinimumLevel": { @@ -50,12 +50,12 @@ "AccessKeyID": "LTAI5tEBXGewpHSLiSxyx6Bf", "AccessKeySecret": "w29b8wkw6XQVL8GWXgp3ZesgYeDKvf", "VodBucketName": "outin-5277bbb52bec11f08dbd00163e169e2b.oss-cn-beijing.aliyuncs.com", - "BucketName": "qyzh-qb", + "BucketName": "qyzh2025test", "Region": "beijing", "RoleArn": "acs:ram::1064745380176636:role/aliyunosstokengeneratorrole", "DurationSeconds": 3600, //过期时间(秒) "Endpoint": "oss-cn-beijing.aliyuncs.com", - "ProjectName": "QuestionBank", - "Domain": "https://qyzh-qb.oss-cn-beijing.aliyuncs.com/" + "ProjectName": "InteractiveMagazine", + "Domain": "https://oss.test.qyzhjy.com/" } }