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

@ -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)
{