feat: 新增宠物封面图支持,优化补偿任务查询与宠物模板创建流程

1.  新增PetSkinImageTypeEnum.Cover枚举,添加封面图类型支持
2.  为PetSkin、PetTemplate实体添加封面图片地址字段
3.  新增补偿任务按用户ID筛选功能,优化UsersService查询逻辑
4.  重构宠物模板创建流程,支持批量创建进化链与图片
5.  移除PetEvolution实体的基础属性字段,简化进化形态结构
6.  新增OSS域名辅助工具与SDK注册扩展类
7.  优化宠物皮肤、进化阶段的DTO与服务层逻辑
This commit is contained in:
glz
2026-06-09 16:15:22 +08:00
parent eb8f40f2ab
commit badf68a8f8
13 changed files with 320 additions and 82 deletions

View File

@ -0,0 +1,26 @@
namespace QYZH.InteractiveMagazine.Common.Helpers
{
public static class DomainHelper
{
public static string OssDomain { get; set; }
public static string VodDomain { get; set; }
public static string OssFullUrl(string url) => GetFullUrl(url, OssDomain);
public static string VodFullUrl(string url) => GetFullUrl(url, VodDomain);
private static string GetFullUrl(string url, string domain)
{
if (string.IsNullOrWhiteSpace(url)) return string.Empty;
if (url.StartsWith("http")) return url;
// 确保域名末尾没有斜杠url开头有斜杠
var cleanDomain = domain.TrimEnd('/');
var cleanUrl = url.StartsWith("/") ? url : "/" + url;
return $"{cleanDomain}{cleanUrl}";
}
}
}

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\QYZH.InteractiveMagazine.Common\QYZH.InteractiveMagazine.Common.csproj" />

View File

@ -0,0 +1,25 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using QYZH.InteractiveMagazine.Common.Helpers;
using QYZH.InteractiveMagazine.Infrastructure.OSS;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace QYZH.InteractiveMagazine.Infrastructure.SDK
{
public static class SDKExtensions
{
public static void AddSDKService(this IServiceCollection services, IConfiguration configuration)
{
services.Configure<OSSOptions>(configuration.GetSection(OSSOptions.Section));
DomainHelper.OssDomain = configuration.GetSection(OSSOptions.Section).Get<OSSOptions>()!.Domain;
services.AddSingleton<OssService>();
services.AddSingleton<OssIMMService>();
}
}
}

View File

@ -75,6 +75,11 @@ public class CompensationTaskOutput
/// </summary>
public class GetCompensationTasksInput
{
/// <summary>
/// 按用户Id筛选
/// </summary>
public long? UserId { get; set; }
/// <summary>
/// 按状态筛选
/// </summary>

View File

@ -36,6 +36,11 @@ public class PetSkinInput
/// 皮肤类型: Normal, Limited, Event
/// </summary>
public string Type { get; set; } = PetSkinTypeEnum.Normal.ToString();
/// <summary>
/// 封面图片地址
/// </summary>
public string? CoverImageUrl { get; set; }
}
/// <summary>
@ -50,6 +55,10 @@ public class PetSkinOutput
public string Rarity { get; set; } = string.Empty;
public int SortOrder { get; set; }
public string Type { get; set; } = string.Empty;
/// <summary>
/// 封面图片地址
/// </summary>
public string? CoverImageUrl { get; set; }
public string? CreatedBy { get; set; }
public DateTime CreatedAt { get; set; }
public string? UpdatedBy { get; set; }

View File

@ -3,7 +3,7 @@ using QYZH.InteractiveMagazine.Models.Enum;
namespace QYZH.InteractiveMagazine.Models.Dto.Pet;
/// <summary>
/// 宠物模板创建/更新输入
/// 宠物模板创建/更新输入(包含进化链及图片)
/// </summary>
public class PetTemplateInput
{
@ -17,11 +17,6 @@ public class PetTemplateInput
/// </summary>
public string? Description { get; set; }
/// <summary>
/// 默认初始进化形态Id
/// </summary>
public long DefaultEvolutionId { get; set; }
/// <summary>
/// 模板图标地址
/// </summary>
@ -36,6 +31,63 @@ public class PetTemplateInput
/// 模板类型: Normal, Special, Limited
/// </summary>
public string Type { get; set; } = PetTemplateTypeEnum.Normal.ToString();
/// <summary>
/// 进化链列表(创建时一并传入,每个阶段含一组图片)
/// </summary>
public List<PetEvolutionStageInput>? Evolutions { get; set; }
}
/// <summary>
/// 进化阶段输入(含该阶段的图片列表,用于模板创建时一并提交)
/// </summary>
public class PetEvolutionStageInput
{
/// <summary>
/// 阶段名称
/// </summary>
public string StageName { get; set; } = string.Empty;
/// <summary>
/// 阶段等级
/// </summary>
public int StageLevel { get; set; }
/// <summary>
/// 进化所需成长值
/// </summary>
public int RequiredGrowth { get; set; }
/// <summary>
/// 进化类型: Normal, Special
/// </summary>
public string Type { get; set; } = PetEvolutionTypeEnum.Normal.ToString();
/// <summary>
/// 该阶段对应的默认皮肤图片列表(其中 Type="Cover" 的为封面图)
/// </summary>
public List<PetStageImageInput>? Images { get; set; }
}
/// <summary>
/// 进化阶段图片输入(创建模板时使用)
/// </summary>
public class PetStageImageInput
{
/// <summary>
/// 图片地址
/// </summary>
public string ImageUrl { get; set; } = string.Empty;
/// <summary>
/// 图片顺序(动画帧序号)
/// </summary>
public int SortOrder { get; set; }
/// <summary>
/// 图片类型: Normal=普通帧, Cover=封面图
/// </summary>
public string Type { get; set; } = PetSkinImageTypeEnum.Normal.ToString();
}
/// <summary>
@ -48,6 +100,10 @@ public class PetTemplateOutput
public string? Description { get; set; }
public long DefaultEvolutionId { get; set; }
public string? IconUrl { get; set; }
/// <summary>
/// 封面图片地址(默认取初始阶段封面图)
/// </summary>
public string? CoverImageUrl { get; set; }
public int SortOrder { get; set; }
public string Type { get; set; } = string.Empty;
public string Status { get; set; } = string.Empty;

View File

@ -46,34 +46,6 @@ namespace QYZH.InteractiveMagazine.Models.Entity
/// </summary>
public long? PreviousEvolutionId { get; set; }
/// <summary>
/// Desc:基础力量
/// Default:0
/// Nullable:False
/// </summary>
public int BaseStrength { get; set; }
/// <summary>
/// Desc:基础敏捷
/// Default:0
/// Nullable:False
/// </summary>
public int BaseAgility { get; set; }
/// <summary>
/// Desc:基础智力
/// Default:0
/// Nullable:False
/// </summary>
public int BaseIntelligence { get; set; }
/// <summary>
/// Desc:基础魅力
/// Default:0
/// Nullable:False
/// </summary>
public int BaseCharm { get; set; }
/// <summary>
/// Desc:进化类型: Normal, Special
/// Default:Normal

View File

@ -52,5 +52,12 @@ namespace QYZH.InteractiveMagazine.Models.Entity
/// Nullable:False
/// </summary>
public PetSkinTypeEnum Type { get; set; }
/// <summary>
/// Desc:封面图片地址
/// Default:
/// Nullable:True
/// </summary>
public string? CoverImageUrl { get; set; }
}
}

View File

@ -53,5 +53,12 @@ namespace QYZH.InteractiveMagazine.Models.Entity
/// </summary>
public PetTemplateTypeEnum Type { get; set; }
/// <summary>
/// Desc:封面图片地址(默认取初始阶段封面图)
/// Default:
/// Nullable:True
/// </summary>
public string? CoverImageUrl { get; set; }
}
}

View File

@ -16,5 +16,10 @@ public enum PetSkinImageTypeEnum
/// 特殊图片
/// </summary>
[Description("特殊图片")]
Special = 2
Special = 2,
/// <summary>
/// 封面图
/// </summary>
[Description("封面图")]
Cover = 3
}

View File

@ -98,6 +98,9 @@ public class CompensationTaskService(
var query = taskRepository.Queryable()
.Where(t => !t.IsDeleted);
if (input.UserId.HasValue)
query = query.Where(t => t.UserId == input.UserId.Value);
if (input.Status.HasValue)
query = query.Where(t => t.Status == (int)input.Status);

View File

@ -119,20 +119,38 @@ public class PetService(
throw new Exception("宠物模板配置缺失");
}
// 查询模板对应的初始进化形态
var initialEvolution = await petEvolutionRepository.Queryable()
.Where(e => e.TemplateId == defaultTemplate.Id
&& e.PreviousEvolutionId == null
&& e.Status == (int)PetEvolutionStatusEnum.Active)
.OrderBy(e => e.StageLevel)
.FirstAsync();
// 确定初始进化形态:优先使用模板的 DefaultEvolutionId为 0 时动态查询兜底
PetEvolution? initialEvolution = null;
if (defaultTemplate.DefaultEvolutionId > 0)
{
initialEvolution = await petEvolutionRepository.Queryable()
.Where(e => e.Id == defaultTemplate.DefaultEvolutionId && !e.IsDeleted)
.FirstAsync();
}
if (initialEvolution == null)
{
// 兜底:查询该模板下 PreviousEvolutionId 为 null 的初始形态
initialEvolution = await petEvolutionRepository.Queryable()
.Where(e => e.TemplateId == defaultTemplate.Id
&& e.PreviousEvolutionId == null
&& e.Status == (int)PetEvolutionStatusEnum.Active)
.OrderBy(e => e.StageLevel)
.FirstAsync();
}
if (initialEvolution == null)
{
logger.LogError("模板 {TemplateId} 未配置初始进化形态", defaultTemplate.Id);
throw new Exception("宠物模板初始进化形态配置缺失");
}
var pet = new UserPet
{
UserId = userId,
TemplateId = defaultTemplate.Id,
Name = defaultTemplate.Name,
CurrentEvolutionId = initialEvolution?.Id ?? 0,
CurrentEvolutionId = initialEvolution.Id,
GrowthPoints = 0,
FeedingCount = 0,
CurrentSkinId = 0,
@ -352,34 +370,149 @@ public class PetService(
// ==================== 后台管理:宠物模板 ====================
/// <summary>
/// 创建宠物模板
/// 创建宠物模板(事务:同时创建进化链、默认皮肤及图片)
/// </summary>
public async Task<PetTemplateOutput> CreateTemplateAsync(PetTemplateInput input)
{
if (string.IsNullOrWhiteSpace(input.Name))
throw new BusinessException("模板名称不能为空", 400);
var template = new PetTemplate
if (input.Evolutions == null || input.Evolutions.Count == 0)
throw new BusinessException("至少需要一个进化阶段", 400);
PetTemplate template = null!;
await UseTranAsync(async () =>
{
Name = input.Name.Trim(),
Description = input.Description,
DefaultEvolutionId = input.DefaultEvolutionId,
IconUrl = input.IconUrl,
SortOrder = input.SortOrder,
Type = Enum.Parse<PetTemplateTypeEnum>(input.Type, true),
Status = (int)PetTemplateStatusEnum.Active,
CreatedBy = "System",
CreatedAt = DateTime.Now,
UpdatedBy = "System",
UpdatedAt = DateTime.Now,
IsDeleted = false
};
// 1. 创建模板DefaultEvolutionId 先置 0稍后回填
template = new PetTemplate
{
Name = input.Name.Trim(),
Description = input.Description,
DefaultEvolutionId = 0,
IconUrl = input.IconUrl,
SortOrder = input.SortOrder,
Type = Enum.Parse<PetTemplateTypeEnum>(input.Type, true),
Status = (int)PetTemplateStatusEnum.Active,
CreatedBy = "System",
CreatedAt = DateTime.Now,
UpdatedBy = "System",
UpdatedAt = DateTime.Now,
IsDeleted = false
};
var result = await petTemplateRepository.InsertAsync(template);
if (!result)
throw new BusinessException("创建宠物模板失败", 500);
var inserted = await petTemplateRepository.InsertAsync(template);
if (!inserted)
throw new BusinessException("创建宠物模板失败", 500);
logger.LogInformation("创建宠物模板成功Id: {Id}, Name: {Name}", template.Id, template.Name);
// 2. 按 StageLevel 排序,依次创建进化阶段
var sortedEvolutions = input.Evolutions.OrderBy(e => e.StageLevel).ToList();
PetEvolution? previousEvolution = null;
long initialEvolutionId = 0;
string? initialCoverUrl = null;
foreach (var evoInput in sortedEvolutions)
{
var evolution = new PetEvolution
{
TemplateId = template.Id,
StageName = evoInput.StageName.Trim(),
StageLevel = evoInput.StageLevel,
RequiredGrowth = evoInput.RequiredGrowth,
PreviousEvolutionId = previousEvolution?.Id, // 第一个为 null
Type = Enum.Parse<PetEvolutionTypeEnum>(evoInput.Type, true),
Status = (int)PetEvolutionStatusEnum.Active,
CreatedBy = "System",
CreatedAt = DateTime.Now,
UpdatedBy = "System",
UpdatedAt = DateTime.Now,
IsDeleted = false
};
var evoInserted = await petEvolutionRepository.InsertAsync(evolution);
if (!evoInserted)
throw new BusinessException($"创建进化阶段 [{evoInput.StageName}] 失败", 500);
// 记录初始阶段(第一个)
if (previousEvolution == null)
{
initialEvolutionId = evolution.Id;
}
// 3. 为该阶段创建默认皮肤图片
if (evoInput.Images != null && evoInput.Images.Count > 0)
{
foreach (var imgInput in evoInput.Images.OrderBy(i => i.SortOrder))
{
var skinImage = new PetSkinImage
{
SkinId = 0, // 先置 0等默认皮肤创建后回填
EvolutionStageId = evolution.Id,
ImageUrl = imgInput.ImageUrl.Trim(),
SortOrder = imgInput.SortOrder,
Type = Enum.Parse<PetSkinImageTypeEnum>(imgInput.Type, true),
CreatedBy = "System",
CreatedAt = DateTime.Now,
UpdatedBy = "System",
UpdatedAt = DateTime.Now,
IsDeleted = false
};
await petSkinImageRepository.InsertAsync(skinImage);
// 记录初始阶段的封面图
if (previousEvolution == null
&& imgInput.Type == PetSkinImageTypeEnum.Cover.ToString()
&& initialCoverUrl == null)
{
initialCoverUrl = skinImage.ImageUrl;
}
}
}
previousEvolution = evolution;
}
// 4. 创建默认皮肤
var defaultSkin = new PetSkin
{
TemplateId = template.Id,
Name = "默认皮肤",
Description = "系统默认皮肤",
Rarity = "Normal",
SortOrder = 0,
Type = Enum.Parse<PetSkinTypeEnum>("Normal", true),
CoverImageUrl = initialCoverUrl,
CreatedBy = "System",
CreatedAt = DateTime.Now,
UpdatedBy = "System",
UpdatedAt = DateTime.Now,
IsDeleted = false
};
await petSkinRepository.InsertAsync(defaultSkin);
// 5. 回填图片的 SkinId
var evolutionIds = await petEvolutionRepository.Context.Queryable<PetEvolution>()
.Where(e => e.TemplateId == template.Id && !e.IsDeleted)
.Select(e => e.Id)
.ToListAsync();
if (evolutionIds.Count > 0)
{
await petSkinImageRepository.Context.Updateable<PetSkinImage>()
.SetColumns(i => i.SkinId == defaultSkin.Id)
.Where(i => i.SkinId == 0 && i.IsDeleted == false
&& evolutionIds.Contains(i.EvolutionStageId))
.ExecuteCommandAsync();
}
// 6. 回填模板的 DefaultEvolutionId 和 CoverImageUrl
template.DefaultEvolutionId = initialEvolutionId;
template.CoverImageUrl = initialCoverUrl;
await petTemplateRepository.UpdateAsync(template);
});
logger.LogInformation("创建宠物模板成功Id: {Id}, 初始进化Id: {EvoId}", template.Id, template.DefaultEvolutionId);
return BuildTemplateOutput(template);
}
@ -397,7 +530,6 @@ public class PetService(
template.Name = input.Name.Trim();
template.Description = input.Description;
template.DefaultEvolutionId = input.DefaultEvolutionId;
template.IconUrl = input.IconUrl;
template.SortOrder = input.SortOrder;
template.Type = Enum.Parse<PetTemplateTypeEnum>(input.Type, true);
@ -468,6 +600,7 @@ public class PetService(
Description = t.Description,
DefaultEvolutionId = t.DefaultEvolutionId,
IconUrl = t.IconUrl,
CoverImageUrl = t.CoverImageUrl,
SortOrder = t.SortOrder,
Type = t.Type.ToString(),
Status = t.Status.ToString(),
@ -509,6 +642,7 @@ public class PetService(
Description = t.Description,
DefaultEvolutionId = t.DefaultEvolutionId,
IconUrl = t.IconUrl,
CoverImageUrl = t.CoverImageUrl,
SortOrder = t.SortOrder,
Type = t.Type.ToString(),
Status = t.Status.ToString(),
@ -545,10 +679,6 @@ public class PetService(
StageLevel = input.StageLevel,
RequiredGrowth = input.RequiredGrowth,
PreviousEvolutionId = input.PreviousEvolutionId,
BaseStrength = input.BaseStrength,
BaseAgility = input.BaseAgility,
BaseIntelligence = input.BaseIntelligence,
BaseCharm = input.BaseCharm,
Type = Enum.Parse<PetEvolutionTypeEnum>(input.Type, true),
Status = (int)PetEvolutionStatusEnum.Active,
CreatedBy = "System",
@ -583,10 +713,6 @@ public class PetService(
evolution.StageLevel = input.StageLevel;
evolution.RequiredGrowth = input.RequiredGrowth;
evolution.PreviousEvolutionId = input.PreviousEvolutionId;
evolution.BaseStrength = input.BaseStrength;
evolution.BaseAgility = input.BaseAgility;
evolution.BaseIntelligence = input.BaseIntelligence;
evolution.BaseCharm = input.BaseCharm;
evolution.Type = Enum.Parse<PetEvolutionTypeEnum>(input.Type, true);
evolution.UpdatedBy = "System";
evolution.UpdatedAt = DateTime.Now;
@ -654,10 +780,6 @@ public class PetService(
StageLevel = e.StageLevel,
RequiredGrowth = e.RequiredGrowth,
PreviousEvolutionId = e.PreviousEvolutionId,
BaseStrength = e.BaseStrength,
BaseAgility = e.BaseAgility,
BaseIntelligence = e.BaseIntelligence,
BaseCharm = e.BaseCharm,
Type = e.Type.ToString(),
Status = e.Status.ToString(),
CreatedBy = e.CreatedBy,
@ -680,10 +802,6 @@ public class PetService(
StageLevel = e.StageLevel,
RequiredGrowth = e.RequiredGrowth,
PreviousEvolutionId = e.PreviousEvolutionId,
BaseStrength = e.BaseStrength,
BaseAgility = e.BaseAgility,
BaseIntelligence = e.BaseIntelligence,
BaseCharm = e.BaseCharm,
Type = e.Type.ToString(),
Status = e.Status.ToString(),
CreatedBy = e.CreatedBy,
@ -720,6 +838,7 @@ public class PetService(
Rarity = input.Rarity,
SortOrder = input.SortOrder,
Type = Enum.Parse<PetSkinTypeEnum>(input.Type, true),
CoverImageUrl = input.CoverImageUrl,
CreatedBy = "System",
CreatedAt = DateTime.Now,
UpdatedBy = "System",
@ -753,6 +872,7 @@ public class PetService(
skin.Rarity = input.Rarity;
skin.SortOrder = input.SortOrder;
skin.Type = Enum.Parse<PetSkinTypeEnum>(input.Type, true);
skin.CoverImageUrl = input.CoverImageUrl;
skin.UpdatedBy = "System";
skin.UpdatedAt = DateTime.Now;
@ -834,6 +954,7 @@ public class PetService(
Rarity = s.Rarity,
SortOrder = s.SortOrder,
Type = s.Type.ToString(),
CoverImageUrl = s.CoverImageUrl,
CreatedBy = s.CreatedBy,
CreatedAt = s.CreatedAt,
UpdatedBy = s.UpdatedBy,
@ -875,6 +996,7 @@ public class PetService(
Rarity = s.Rarity,
SortOrder = s.SortOrder,
Type = s.Type.ToString(),
CoverImageUrl = s.CoverImageUrl,
CreatedBy = s.CreatedBy,
CreatedAt = s.CreatedAt,
UpdatedBy = s.UpdatedBy,

View File

@ -113,10 +113,11 @@ public class UsersService(
{
var tasks = await compensationTaskService.GetTasksAsync(new GetCompensationTasksInput
{
UserId = userId,
Status = CompensationTaskStatusEnum.Failed,
Limit = 50
Limit = 20
});
return tasks.Where(t => t.UserId == userId).ToList();
return tasks;
}
catch (Exception ex)
{