using Microsoft.Extensions.Logging; using QYZH.InteractiveMagazine.Common.Helpers; using QYZH.InteractiveMagazine.IService; using QYZH.InteractiveMagazine.Models.Common; using QYZH.InteractiveMagazine.Models.Dto; using QYZH.InteractiveMagazine.Models.Dto.Points; using QYZH.InteractiveMagazine.Models.Dto.UserAnswerTaskService; using QYZH.InteractiveMagazine.Models.Entity; using QYZH.InteractiveMagazine.Models.Enum; using QYZH.InteractiveMagazine.Repository; namespace QYZH.InteractiveMagazine.Service; /// /// 用户作答任务服务实现 /// public class UserAnswerTaskService( BaseRepository answerRepository, BaseRepository journalRepository, BaseRepository catalogRepository, BaseRepository pageRepository, BaseRepository taskRepository, BaseRepository userJournalRepository, BaseRepository usersRepository, BaseRepository checkInRecordRepository, BaseRepository userPetRepository, IPointsService pointsService, IPetService petService, ILogger logger) : BaseRepository, IUserAnswerTaskService { /// /// ============================================ /// 方法思路: /// 1. 首先验证 UserJournal 表,获取用户绑定的杂志列表 /// - 如果用户没有绑定任何杂志,直接返回空列表 /// 2. 批量预加载策略:为避免N+1查询问题,先按期刊维度批量加载所有关联数据 /// - 期刊基本信息 /// - 一级栏目(ParentId == 0) /// - 所有任务(按期刊ID) /// - 所有页面(按期刊ID) /// 3. 查询用户在这些期刊下的作答记录 /// 4. 跨页题目处理核心: /// - 跨页题目定义:JournalPageTask.Id != GroupId(同一个GroupId下有多个任务) /// - 进度计算:按GroupId去重,已完成条件是该GroupId下所有任务都完成(Status=1) /// - 积分计算:按GroupId去重,每个GroupId只计算一次积分(取主任务的积分) /// 5. 状态判定逻辑: /// - 未开始:无任何作答记录 /// - 进行中:有作答记录但未全部完成 /// - 已完成:所有GroupId都完成 /// 6. 用户未作答任何任务时: /// - 仍返回绑定杂志的进度信息 /// - 进度、成长值、积分为0 /// - 连续打卡、剩余篇数正常计算 /// ============================================ /// /// 用户ID /// 用户所有期刊的学习进度列表 public async Task> GetJournalProgressAsync(long userId) { #region 1. 验证 UserJournal 表,获取用户绑定的杂志 // ============================================ // 第一步:先验证 UserJournal 表数据 // 获取用户绑定的所有杂志 ID // 如果没有绑定的杂志,直接返回空列表,不进行后续查询 // ============================================ var boundJournalIds = await userJournalRepository.Context.Queryable() .Where(uj => uj.UserId == userId) .Select(uj => uj.JournalId) .ToListAsync(); if (boundJournalIds == null || boundJournalIds.Count == 0) { return new List(); } var journalIds = boundJournalIds.Distinct().ToList(); #endregion #region 2. 批量预加载关联数据 // ============================================ // 第二步:批量预加载关联实体(按期刊维度全量加载,避免N+1查询) // - 期刊基本信息 // - 一级栏目(ParentId == 0 表示一级目录) // - 所有任务 // - 所有页面(用于计算剩余篇数) // ============================================ var journals = await journalRepository.Context.Queryable() .Where(j => journalIds.Contains(j.Id)) .ToListAsync(); var allCatalogs = await catalogRepository.Context.Queryable() .Where(c => journalIds.Contains(c.JournalId) && c.ParentId == 0) .ToListAsync(); var allTasks = await taskRepository.Context.Queryable() .Where(t => journalIds.Contains(t.JournalId)) .ToListAsync(); var allPages = await pageRepository.Context.Queryable() .Where(p => journalIds.Contains(p.JournalId)) .ToListAsync(); #endregion #region 3. 查询用户作答记录(仅绑定的杂志) // ============================================ // 第三步:查询用户在这些绑定杂志下的作答记录 // 只查询绑定杂志的作答记录,减少数据量 // ============================================ var userAnswers = await answerRepository.Context.Queryable() .Where(a => a.UserId == userId && journalIds.Contains(a.JournalId)) .ToListAsync(); #endregion #region 4. 查询连续打卡天数 var consecutiveDays = await CalculateConsecutiveDaysAsync(userId); #endregion #region 5. 提取用户已作答/已完成任务ID集合 // 用户已作答的任务ID集合(包含进行中和已完成) var userAnsweredTaskIds = userAnswers.Select(a => a.JournalPageTaskId).ToHashSet(); var userAnsweredPageIds = userAnswers.Select(a => a.JournalPageId).ToHashSet(); // 用户已完成的任务ID集合 // 【已完成判断逻辑】 // Status: 0=未批阅,1=已批阅 var userCompletedTaskIds = userAnswers .Where(a => a.Status == (int)UserAnswerStatusEnum.Complete) .Select(a => a.JournalPageTaskId) .ToHashSet(); #endregion #region 6. 按期刊分组聚合并返回结果 // ============================================ // 第四步:按期刊分组聚合 // 遍历每个绑定的杂志,计算进度、积分、成长值等 // ============================================ var result = new List(); foreach (var journal in journals) { // 该期刊下所有任务 var journalTasks = allTasks.Where(t => t.JournalId == journal.Id).ToList(); // 该期刊的一级栏目数 var catalogCount = allCatalogs.Count(c => c.JournalId == journal.Id); // 【跨页题目-进度计算】总任务数:按 GroupId 去重,跨页题目只算一个 var totalGroupIds = journalTasks.Select(t => t.GroupId).Distinct().ToList(); var totalTaskCount = totalGroupIds.Count; // 【跨页题目-已完成判断】已完成的 GroupId 列表 // 关键:必须该 GroupId 下所有任务都完成,才算该 GroupId 完成 var completedGroupIds = journalTasks .GroupBy(t => t.GroupId) .Where(g => g.All(t => userCompletedTaskIds.Contains(t.Id))) .Select(g => g.Key) .ToList(); var completedTaskCount = completedGroupIds.Count; // ============================================ // 【积分和成长值计算】 // 1. totalGrowthPoints:从已完成作答记录中,按 JournalPageTaskGroupId 查询主任务(Id=GroupId)的 GrowthPoint // 2. totalPoints:统计该期刊下已完成任务的 Points 总数 // ============================================ // 该期刊下用户已完成的作答记录 // 【已完成判断逻辑】 // Status: 0=未批阅,1=已批阅 var completedAnswers = userAnswers .Where(a => a.JournalId == journal.Id && a.Status == (int)UserAnswerStatusEnum.Complete) .ToList(); // 按 JournalPageTaskGroupId 去重,获取已完成的主任务 ID 列表 var completedGroupIdsSet = completedAnswers.Select(a => a.JournalPageTaskGroupId).Distinct().ToList(); // 从主任务表查询这些 GroupId 对应的 GrowthPoint 和 Points var mainTasks = allTasks .Where(t => completedGroupIdsSet.Contains(t.GroupId) && t.Id == t.GroupId) .ToList(); // 累计成长值(从主任务的 GrowthPoint 累加) var totalGrowthPoints = mainTasks.Sum(t => t.GrowthPoint); // 累计积分(从已完成任务的 Points 累加,每个 GroupId 只计算一次) var totalPoints = mainTasks.Sum(t => t.Points); // 【进度百分比计算】按篇数统计 // 只计算有题目的页面,进度 = 已答篇数 / 有题目的总篇数 × 100 var pagesWithTasks = journalTasks.Select(t => t.JournalPageId).Distinct().ToHashSet(); var totalPagesWithTasks = pagesWithTasks.Count; var answeredPagesWithTasks = pagesWithTasks.Count(userAnsweredPageIds.Contains); var progress = totalPagesWithTasks > 0 ? Math.Round((double)answeredPagesWithTasks / totalPagesWithTasks * 100, 2) : 0; // 剩余篇数:有题目的页面总数 - 已作答的有题目页面数 var remainingPages = totalPagesWithTasks - answeredPagesWithTasks; result.Add(new JournalProgressOutput { JournalId = journal.Id, JournalName = journal.Name ?? string.Empty, CatalogCount = catalogCount, TotalTaskCount = totalTaskCount, CompletedTaskCount = completedTaskCount, TotalGrowthPoints = totalGrowthPoints, TotalPoints = totalPoints, Progress = progress, RemainingPages = remainingPages, ConsecutiveDays = consecutiveDays }); } return result; #endregion } /// /// ============================================ /// 方法思路:计算用户连续打卡天数 /// 1. 获取今天和昨天的日期 /// 2. 查询昨天是否有打卡记录 /// 3. 如果昨天没有打卡记录,返回0(连续打卡中断) /// 4. 如果昨天有打卡记录: /// - 查询今天是否有打卡记录 /// - 今天有记录:返回今天的连续天数 /// - 今天无记录:返回昨天的连续天数 /// ============================================ /// /// 用户ID /// 连续打卡天数 private async Task CalculateConsecutiveDaysAsync(long userId) { var today = DateTime.Now.Date; var yesterday = today.AddDays(-1); // 查询昨天是否有打卡记录 var yesterdayRecord = await checkInRecordRepository.Context.Queryable() .Where(r => r.UserId == userId && r.CheckInDate.Date == yesterday) .FirstAsync(); // 如果昨天没有打卡记录,连续天数为0(连续打卡中断) if (yesterdayRecord == null) { return 0; } // 昨天有打卡记录,检查今天是否已打卡 var todayRecord = await checkInRecordRepository.Context.Queryable() .Where(r => r.UserId == userId && r.CheckInDate.Date == today) .FirstAsync(); // 今天已打卡:返回今天的连续天数;今天未打卡:返回昨天的连续天数 return todayRecord?.ConsecutiveDays ?? yesterdayRecord.ConsecutiveDays; } /// /// ============================================ /// 方法思路:获取目录列表(按一级目录分组展示) /// 1. 批量预加载:该期刊下所有目录、页面、任务、用户作答记录 /// 2. 目录结构处理: /// - 一级目录:ParentId=0 且 Type=Unit /// - 二级目录:ParentId=一级目录Id 且 Type=Unit /// - 文章目录:Type=Page /// 3. 【关键】如果一级目录下没有二级栏目(Type=Unit的子目录), /// 则该一级目录不参与任何计算,直接跳过 /// 4. 跨页题目处理: /// - 状态判断:按GroupId去重,已完成条件是该GroupId下所有任务都完成(Status=1) /// - 积分计算:按GroupId去重,每个GroupId只计算一次积分 /// - 已作答判断:只要有一个任务作答就算该GroupId已作答(包含进行中和已完成) /// 5. 状态判定: /// - 未开始:无任何GroupId被作答 /// - 进行中:有GroupId被作答但未全部完成 /// - 已完成:所有GroupId都完成 /// ============================================ /// /// 期刊ID /// 用户ID /// 目录列表(按一级目录分组) public async Task> GetCatalogListAsync(long journalId, long userId) { #region 0. 验证用户是否绑定该杂志 // ============================================ // 第一步:验证 UserJournal 表,确认用户是否绑定了该杂志 // 如果用户没有绑定该杂志,直接返回空列表 // ============================================ var isBound = await userJournalRepository.Context.Queryable() .Where(uj => uj.UserId == userId && uj.JournalId == journalId) .AnyAsync(); if (!isBound) { return new List(); } #endregion #region 1. 查询期刊 // 1. 查询期刊 var journal = await journalRepository.Context.Queryable() .Where(j => j.Id == journalId) .FirstAsync(); if (journal == null) { return new List(); } #endregion #region 2. 批量加载关联数据 // 2. 批量加载关联数据 var allCatalogs = await catalogRepository.Context.Queryable() .Where(c => c.JournalId == journalId) .ToListAsync(); var allPages = await pageRepository.Context.Queryable() .Where(p => p.JournalId == journalId) .ToListAsync(); var allTasks = await taskRepository.Context.Queryable() .Where(t => t.JournalId == journalId) .ToListAsync(); var userAnswers = await answerRepository.Context.Queryable() .Where(a => a.JournalId == journalId && a.UserId == userId) .ToListAsync(); #endregion #region 3. 用户已作答/已完成任务ID集合 // 3. 用户已作答的任务ID集合(包含进行中和已完成) var userAnsweredTaskIds = userAnswers.Select(a => a.JournalPageTaskId).ToHashSet(); // 用户已完成的任务ID集合 // 【已完成判断逻辑】 var userCompletedTaskIds = userAnswers .Where(a => a.Status == (int)UserAnswerStatusEnum.Complete) .Select(a => a.JournalPageTaskId) .ToHashSet(); #endregion #region 4. 按一级目录分组计算 // 4. 按一级目录分组 var firstLevelCatalogs = allCatalogs.Where(c => c.ParentId == 0).OrderBy(c => c.Sort).ToList(); var result = new List(); foreach (var firstCatalog in firstLevelCatalogs) { // ============================================ // 文章篇数计算逻辑: // - 篇数 = 有题目的页面数(有 JournalPageTask 的页面) // - 有二级栏目:累加所有二级栏目下有题目的页面数 // - 没有二级栏目:统计一级栏目下直接有题目的页面数 // ============================================ int articleCount = 0; var children = new List(); // 该一级目录下的二级单元目录 var secondLevelCatalogs = allCatalogs .Where(c => c.ParentId == firstCatalog.Id) .OrderBy(c => c.Sort) .ToList(); if (secondLevelCatalogs.Count > 0) { // 有二级单元:按二级单元分组统计 foreach (var secondCatalog in secondLevelCatalogs) { // 二级目录下的所有页面(JournalPage.JournalCatalogId = 二级目录ID) var secondLevelPages = allPages .Where(p => p.JournalCatalogId == secondCatalog.Id) .ToList(); var secondLevelPageIds = secondLevelPages.Select(p => p.Id).ToHashSet(); // 二级目录下有题目的页面(有 JournalPageTask 的页面) var pagesWithTasks = allTasks .Where(t => secondLevelPageIds.Contains(t.JournalPageId)) .Select(t => t.JournalPageId) .Distinct() .ToHashSet(); // 【关键】如果二级目录下没有有题目的页面,则跳过,不统计到 children if (pagesWithTasks.Count == 0) { continue; } // 累加该二级栏目下有题目的页面数(文章篇数) articleCount += pagesWithTasks.Count; // 二级单元下的所有任务 var secondLevelTasks = allTasks.Where(t => secondLevelPageIds.Contains(t.JournalPageId)).ToList(); // 判定状态(基于 AnswerStatus >= 1 判断已完成,按页面维度统计) // 【已完成判断逻辑】 // AnswerStatus: 0=未答题,1=已答题,2=正确,3=半对,4=错误 // 只要 AnswerStatus >= 1(已答题)说明该页面已完成 string status; // 总页面数(有题目的页面) var totalPageCount = pagesWithTasks.Count; // 已完成页面数:该页面有答题记录且 AnswerStatus >= 1 var completedPageCount = pagesWithTasks .Count(pageId => userAnswers.Any(a => a.JournalPageId == pageId && a.Status == (int)UserAnswerStatusEnum.Complete)); // 已作答页面数:该页面有答题记录即可 var answeredPageCount = pagesWithTasks .Count(pageId => userAnswers.Any(a => a.JournalPageId == pageId)); // 状态判定逻辑: // - answeredPageCount == 0:没有任何页面被作答 → 未开始 // - completedPageCount == totalPageCount:所有页面都完成 → 已完成 // - 其他:有页面被作答但未全部完成 → 进行中 if (answeredPageCount == 0) status = "未开始"; else if (completedPageCount == totalPageCount) status = "已完成"; else status = "进行中"; // 计算该二级目录的总成长值和总积分(跨页题目按GroupId去重) var distinctSecondLevelTasks = secondLevelTasks .GroupBy(t => t.GroupId) .Select(g => g.First()) .ToList(); var totalGrowthPoints = distinctSecondLevelTasks.Sum(t => t.GrowthPoint); var totalPoints = distinctSecondLevelTasks.Sum(t => t.Points); children.Add(new SecondCatalogOutput { CatalogId = secondCatalog.Id, CatalogName = secondCatalog.Name ?? string.Empty, TotalPoints = totalPoints, TotalGrowthPoints = totalGrowthPoints, Status = status }); } } else { // 没有二级单元:统计一级栏目下直接有题目的页面数 var firstLevelPages = allPages .Where(p => p.JournalCatalogId == firstCatalog.Id) .ToList(); var firstLevelPageIds = firstLevelPages.Select(p => p.Id).ToHashSet(); var pagesWithTasks = allTasks .Where(t => firstLevelPageIds.Contains(t.JournalPageId)) .Select(t => t.JournalPageId) .Distinct() .ToHashSet(); articleCount = pagesWithTasks.Count; // Children保持为空集合,不添加任何子项 } result.Add(new CatalogListOutput { CatalogId = firstCatalog.Id, CatalogName = firstCatalog.Name ?? string.Empty, JournalName = journal.Name ?? string.Empty, ArticleCount = articleCount, Children = children }); } #endregion #region 5. 返回结果 return result; #endregion } /// /// ============================================ /// 方法思路:获取一级目录概览(含二级目录详情和任务完成状态) /// 1. 批量预加载:该期刊下所有目录、页面、任务、用户作答记录 /// 2. 获取该一级目录下的所有文章目录(Type=Page) /// 3. 跨页题目处理: /// - 总任务数:按GroupId去重(跨页题目只算一个) /// - 状态判断:按GroupId去重,已完成条件是该GroupId下所有任务都完成(Status=1) /// - 已作答判断:只要有一个任务作答就算该GroupId已作答 /// 4. 状态格式:"已完成数/总数 已完成/进行中/未开始" /// 例如:"2/3 进行中" 表示3个GroupId中已完成2个 /// ============================================ /// /// 一级目录ID /// 用户ID /// 一级目录概览信息 public async Task GetCatalogOverviewAsync(long catalogId, long userId) { #region 1. 查询一级目录 // 1. 查询一级目录 var firstCatalog = await catalogRepository.Context.Queryable() .Where(c => c.Id == catalogId) .FirstAsync(); if (firstCatalog == null) { return null; } #endregion #region 2. 验证用户是否绑定该杂志 // ============================================ // 验证 UserJournal 表,确认用户是否绑定了该杂志 // 如果用户没有绑定该杂志,直接返回 null // ============================================ var isBound = await userJournalRepository.Context.Queryable() .Where(uj => uj.UserId == userId && uj.JournalId == firstCatalog.JournalId) .AnyAsync(); if (!isBound) { return null; } #endregion #region 3. 批量加载关联数据 // 2. 批量加载关联数据 var allCatalogs = await catalogRepository.Context.Queryable() .Where(c => c.JournalId == firstCatalog.JournalId) .ToListAsync(); var allPages = await pageRepository.Context.Queryable() .Where(p => p.JournalId == firstCatalog.JournalId) .ToListAsync(); var allTasks = await taskRepository.Context.Queryable() .Where(t => t.JournalId == firstCatalog.JournalId) .ToListAsync(); var userAnswers = await answerRepository.Context.Queryable() .Where(a => a.JournalId == firstCatalog.JournalId && a.UserId == userId) .ToListAsync(); #endregion #region 4. 用户已作答/已完成任务ID集合 // 用户已作答的任务ID集合(包含进行中和已完成) var userAnsweredTaskIds = userAnswers.Select(a => a.JournalPageTaskId).ToHashSet(); // 用户已完成的任务ID集合 // 【已完成判断逻辑】 // AnswerStatus: 0=未答题,1=已答题,2=正确,3=半对,4=错误 // 只要 AnswerStatus >= 1(已答题)说明作业已完成 var userCompletedTaskIds = userAnswers .Where(a => a.Status == (int)UserAnswerStatusEnum.Complete) .Select(a => a.JournalPageTaskId) .ToHashSet(); #endregion #region 5. 获取一级栏目下所有二级目录 // 获取该一级目录下所有二级目录(不区分Type) var secondLevelCatalogs = allCatalogs .Where(c => c.ParentId == firstCatalog.Id) .OrderBy(c => c.Sort) .ToList(); // 文章篇数 = 有题目的页面数(有 JournalPageTask 的页面) int articleCount = 0; // 总任务数(按 GroupId 去重) int totalTaskCount = 0; // 构建子目录详情列表 var subCatalogs = new List(); foreach (var secondCatalog in secondLevelCatalogs) { // 二级目录下的所有页面 var secondLevelPages = allPages .Where(p => p.JournalCatalogId == secondCatalog.Id) .OrderBy(p => p.PageNum) .ToList(); var secondLevelPageIds = secondLevelPages.Select(p => p.Id).ToHashSet(); // 二级目录下有题目的页面(有 JournalPageTask 的页面) var pagesWithTasks = allTasks .Where(t => secondLevelPageIds.Contains(t.JournalPageId)) .Select(t => t.JournalPageId) .Distinct() .ToHashSet(); // 如果二级目录下没有有题目的页面,则跳过 if (pagesWithTasks.Count == 0) { continue; } // 累加文章篇数 articleCount += pagesWithTasks.Count; // 该文章下的所有任务 var articleTasks = allTasks.Where(t => secondLevelPageIds.Contains(t.JournalPageId)).ToList(); // taskCount = GroupId 去重数(跨页题目只算一个) var taskCount = articleTasks.Select(t => t.GroupId).Distinct().Count(); // GroupId 去重数(用于状态判断) var totalGroupCount = taskCount; // 累加总任务数 totalTaskCount += taskCount; // 页码范围 var startPage = secondLevelPages.Count > 0 ? secondLevelPages.Min(p => p.PageNum) : 0; var endPage = secondLevelPages.Count > 0 ? secondLevelPages.Max(p => p.PageNum) : 0; // 状态格式:1/2 进行中、2/2 已完成、0/2 未开始(按 GroupId 去重统计) // 只要 AnswerStatus >= 1(已答题)说明该 GroupId 已完成 string status; if (totalGroupCount == 0) { status = "0/0 未开始"; } else { // 已完成 GroupId 数(该 GroupId 下所有任务都 AnswerStatus >= 1 才算完成) var completedGroupCount = articleTasks .GroupBy(t => t.GroupId) .Where(g => g.All(t => userCompletedTaskIds.Contains(t.Id))) .Count(); // 已作答 GroupId 数(只要有一个任务作答就算该 GroupId 已作答) var answeredGroupCount = articleTasks .Where(t => userAnsweredTaskIds.Contains(t.Id)) .Select(t => t.GroupId) .Distinct() .Count(); if (answeredGroupCount == 0) status = $"0/{totalGroupCount} 未开始"; else if (completedGroupCount == totalGroupCount) status = $"{totalGroupCount}/{totalGroupCount} 已完成"; else status = $"{completedGroupCount}/{totalGroupCount} 进行中"; } subCatalogs.Add(new SubCatalogDetailOutput { CatalogId = secondCatalog.Id, SecondLevelCatalogName = secondCatalog.Name ?? string.Empty, StartPage = startPage, EndPage = endPage, TaskCount = taskCount, Status = status }); } #endregion #region 6. 返回结果 return new CatalogOverviewOutput { CatalogId = firstCatalog.Id, FirstLevelCatalogName = firstCatalog.Name ?? string.Empty, PageCount = articleCount, TotalTaskCount = totalTaskCount, SubCatalogs = subCatalogs }; #endregion } /// /// ============================================ /// 方法思路:获取用户答题任务列表 /// 1. 根据二级栏目ID查询栏目基本信息(一级栏目名称、栏目名称、页码范围 /// 2. 按GroupId分组,每个GroupId对应一个任务项 /// 3. 每个任务项包含:任务名称、总成长值、总积分、状态 /// 4. 【跨页题目处理】 /// - 是否跨页:Id != GroupId /// - 任务名称:取 Id == GroupId 的主任务的Task字段 /// - 成长值/积分:取主任务的,每个GroupId只算一次 /// - 状态判断:必须该GroupId下所有任务都完成(Status=1)才算该GroupId完成 /// ============================================ /// 二级栏目ID/param> /// 用户ID /// /// 用户答题任务列表信息 public async Task GetUserAnswerTaskListAsync(long catalogId, long userId) { #region 1. 查询二级栏目目录 // 1. 查询目录(二级栏目) var catalog = await catalogRepository.Context.Queryable() .Where(c => c.Id == catalogId) .FirstAsync(); if (catalog == null) { return null; } #endregion #region 2. 验证用户是否绑定该杂志 // ============================================ // 验证 UserJournal 表,确认用户是否绑定了该杂志 // 如果用户没有绑定该杂志,直接返回 null // ============================================ var isBound = await userJournalRepository.Context.Queryable() .Where(uj => uj.UserId == userId && uj.JournalId == catalog.JournalId) .AnyAsync(); if (!isBound) { return null; } #endregion #region 3. 获取一级栏目名称 // ============================================ // 【获取一级栏目名称】 // 通过 ParentId 关联查询一级栏目名称 // ============================================ string firstLevelCatalogName = string.Empty; if (catalog.ParentId > 0) { var firstLevelCatalog = await catalogRepository.Context.Queryable() .Where(c => c.Id == catalog.ParentId) .FirstAsync(); firstLevelCatalogName = firstLevelCatalog?.Name ?? string.Empty; } #endregion #region 4. 批量加载关联数据 // ============================================ // 批量加载关联数据 // ============================================ // 该栏目下的所有页面 var allPages = await pageRepository.Context.Queryable() .Where(p => p.JournalCatalogId == catalogId) .ToListAsync(); // 该期刊下所有任务(用于跨页题目关联查询) var allTasks = await taskRepository.Context.Queryable() .Where(t => t.JournalId == catalog.JournalId) .ToListAsync(); // 该用户在该期刊下所有作答记录 var userAnswers = await answerRepository.Context.Queryable() .Where(a => a.JournalId == catalog.JournalId && a.UserId == userId) .ToListAsync(); #endregion #region 5. 获取该目录下的页面ID及任务 // 获取该目录下的所有页面ID var pageIds = allPages.Select(p => p.Id).ToHashSet(); // 该目录下的所有任务(仅用于筛选哪些GroupId属于当前栏目) var catalogTasks = allTasks.Where(t => pageIds.Contains(t.JournalPageId)).ToList(); // 【跨页题目-关键】 // 跨页题目的任务可能分布在不同页面(甚至不同栏目)下 // 所以需要从整个期刊的所有任务(allTasks)中按GroupId获取完整的任务组 // 否则无法正确判断跨页题目是否全部完成 var catalogGroupIds = catalogTasks.Select(t => t.GroupId).ToHashSet(); // 用户已作答的任务ID集合(包含进行中和已完成) var userAnsweredTaskIds = userAnswers.Select(a => a.JournalPageTaskId).ToHashSet(); // 用户已完成的任务ID集合(Status=1表示已完成) var userCompletedTaskIds = userAnswers.Where(a => a.Status == (int)UserAnswerStatusEnum.Complete).Select(a => a.JournalPageTaskId).ToHashSet(); // 计算页码范围 var startPage = allPages.Count > 0 ? allPages.Min(p => p.PageNum) : 0; var endPage = allPages.Count > 0 ? allPages.Max(p => p.PageNum) : 0; #endregion #region 6. 按GroupId分组生成任务列表 // ============================================ // 【跨页题目-按GroupId分组生成任务列表】 // 关键:从整个期刊的所有任务中按GroupId取完整分组 // 每个GroupId对应一个任务项 // - 任务名称:取 Id == GroupId 的主任务的Task字段 // - 成长值/积分:取主任务的,每个GroupId只算一次 // - 状态: // - 未开始:该GroupId下无任何任务被作答 // - 已完成:该GroupId下所有任务都完成(Status=1) // - 进行中:有作答但未全部完成 // ============================================ var taskItems = new List(); // 从整个期刊的所有任务中,筛选出属于当前栏目的GroupId的完整分组 var groupTasks = allTasks .Where(t => catalogGroupIds.Contains(t.GroupId)) .GroupBy(t => t.GroupId) .ToList(); foreach (var group in groupTasks) { var groupId = group.Key; // 取主任务(Id == GroupId) var mainTask = group.FirstOrDefault(t => t.Id == groupId) ?? group.First(); // 计算该GroupId下所有任务的答题时间(秒) var taskIds = group.Select(t => t.Id).ToHashSet(); var groupAnswerSeconds = userAnswers .Where(a => taskIds.Contains(a.JournalPageTaskId)) .Sum(a => a.AnswerSeconds); var groupAnswerMinutes = Math.Round(groupAnswerSeconds / 60.0, 2); // 判断该GroupId是否有作答记录 bool hasAnswer = group.Any(t => userAnsweredTaskIds.Contains(t.Id)); // 判断该GroupId是否全部完成 bool isAllCompleted = group.All(t => userCompletedTaskIds.Contains(t.Id)); // 计算状态 string taskStatus; if (!hasAnswer) taskStatus = "未开始"; else if (isAllCompleted) taskStatus = "已完成"; else taskStatus = "进行中"; taskItems.Add(new UserAnswerTaskItemOutput { GroupId = groupId, TaskName = mainTask.Task ?? string.Empty, TotalGrowthPoints = mainTask.GrowthPoint, TotalPoints = mainTask.Points, AnswerMinutes = groupAnswerMinutes, Status = taskStatus }); } #endregion #region 7. 返回结果 return new UserAnswerTaskListOutput { FirstLevelCatalogName = firstLevelCatalogName, CatalogName = catalog.Name ?? string.Empty, StartPage = startPage, EndPage = endPage, Tasks = taskItems }; #endregion } /// /// ============================================ /// 方法思路:获取跨页题目详情 /// 1. 根据 groupId 查询该分组下的所有任务 /// 2. 跨页题目判定: /// - 任务数 > 1 → 跨页题目 /// - 任务数 == 1 且 Id != groupId → 跨页题目(子任务进入) /// - 任务数 == 1 且 Id == groupId → 普通题目 /// 3. 图片数组获取逻辑: /// - 已答题:使用用户上传的 PageAnswerUrl 数组 /// - 未答题:使用任务的 TaskUrl 数组补齐 /// 4. 栏目名称获取: /// - 二级栏目名称:通过页面关联的 JournalCatalogId 查询 /// - 一级栏目名称:通过二级栏目的 ParentId 查询 /// 5. 是否可领取积分判断: /// - 任务必须已完成(Status=1) /// - 跨页题目:所有任务都完成才能领取 /// - 普通题目:当前任务完成即可领取 /// - 且未领取过积分 /// ============================================ /// /// 任务分组ID(JournalPageTaskGroupId) /// 用户ID /// 跨页题目详情 public async Task GetCrossPageTaskAsync(long groupId, long userId) { #region 0. 查询任务并验证用户绑定 // 1. 查询该 GroupId 下的所有任务 var allTasks = await taskRepository.Context.Queryable() .Where(t => t.GroupId == groupId) .OrderBy(t => t.Id) .ToListAsync(); if (allTasks == null || allTasks.Count == 0) { return null; } // ============================================ // 验证 UserJournal 表,确认用户是否绑定了该杂志 // 如果用户没有绑定该杂志,直接返回 null // ============================================ var journalId = allTasks[0].JournalId; var isBound = await userJournalRepository.Context.Queryable() .Where(uj => uj.UserId == userId && uj.JournalId == journalId) .AnyAsync(); if (!isBound) { return null; } #endregion #region 1. 跨页题目判定 // ============================================ // 【跨页题目判定】 // 判断标准: // - 任务数 > 1 → 跨页题目 // - 任务数 == 1 且 Id != groupId → 跨页题目(子任务进入) // - 任务数 == 1 且 Id == groupId → 普通题目 // ============================================ bool isCrossPage = allTasks.Count > 1 || (allTasks.Count == 1 && allTasks[0].Id != groupId); // 获取主任务(Id == GroupId),用于跨页题目获取任务名称、积分等 var mainTask = allTasks.FirstOrDefault(t => t.Id == groupId); // 获取任一任务作为基础信息(用于栏目查询等) var baseTask = allTasks[0]; #endregion #region 2. 获取相关任务列表 // ============================================ // 【获取相关任务列表】 // 跨页题目:allTasks 即为相关任务列表 // 非跨页题目:单任务列表 // ============================================ List relatedTasks = allTasks; #endregion #region 3. 获取栏目名称(一级/二级) // ============================================ // 【获取栏目名称】 // 二级栏目名称:通过页面关联的 JournalCatalogId 查询 // 一级栏目名称:通过二级栏目的 ParentId 查询 // ============================================ var page = await pageRepository.Context.Queryable() .Where(p => p.Id == baseTask.JournalPageId) .FirstAsync(); string catalogName = string.Empty; string firstLevelCatalogName = string.Empty; if (page != null) { // 获取二级栏目 var catalog = await catalogRepository.Context.Queryable() .Where(c => c.Id == page.JournalCatalogId) .FirstAsync(); catalogName = catalog?.Name ?? string.Empty; // 获取一级栏目名称 if (catalog != null && catalog.ParentId > 0) { var firstLevelCatalog = await catalogRepository.Context.Queryable() .Where(c => c.Id == catalog.ParentId) .FirstAsync(); firstLevelCatalogName = firstLevelCatalog?.Name ?? string.Empty; } } #endregion #region 4. 获取图片数组(Images) // ============================================ // 【获取图片数组】 // 核心逻辑:已答题返回 PageAnswerUrl,未答题返回 TaskUrl 补齐 // 跨页题目: // - 用户答了部分:已答页用 PageAnswerUrl,未答页用 TaskUrl 补齐 // - 用户全部作答:全部用 PageAnswerUrl // - 用户未作答:全部用 TaskUrl // - 按主页面页码(PageNum)升序排列,确保拼图顺序正确 // 非跨页题目:取单条答题记录的 PageAnswerUrl,未答题返回空集合 // ============================================ var images = new List(); // 统一查询用户答题记录(用于图片获取和后续领取积分判断) List userAnswers; if (isCrossPage) { // 跨页题目:查询该 GroupId 下所有用户的答题记录 userAnswers = await answerRepository.Context.Queryable() .Where(a => a.JournalPageTaskGroupId == groupId && a.UserId == userId) .OrderBy(a => a.JournalPageTaskId) .ToListAsync(); // ============================================ // 【跨页题目图片获取逻辑】 // 1. 收集相关任务所在页面的页码信息 // 2. 遍历每个相关任务:已答用 PageAnswerUrl,未答用 TaskUrl 补齐 // 3. 按页码升序排序后输出,确保拼图顺序正确 // ============================================ // 查询相关任务对应的页面信息(用于排序) var pageIds = relatedTasks.Select(t => t.JournalPageId).Distinct().ToList(); var pageInfoList = await pageRepository.Context.Queryable() .Where(p => pageIds.Contains(p.Id)) .ToListAsync(); var pageInfoDict = pageInfoList.ToDictionary(p => p.Id, p => p.PageNum); // 组装图片项:(页码, 图片地址) var imageItems = new List<(int PageNum, string Url)>(); foreach (var relatedTask in relatedTasks) { var pageNum = pageInfoDict.TryGetValue(relatedTask.JournalPageId, out var num) ? num : 0; var userAnswer = userAnswers.FirstOrDefault(a => a.JournalPageTaskId == relatedTask.Id); string imageUrl; if (userAnswer != null && !string.IsNullOrEmpty(userAnswer.PageAnswerUrl)) { // 已答题:使用 PageAnswerUrl(加上域名) imageUrl = DomainHelper.OssFullUrl(userAnswer.PageAnswerUrl); } else if (!string.IsNullOrEmpty(relatedTask.TaskUrl)) { // 未答题或无 PageAnswerUrl:使用任务的 TaskUrl 补齐(加上域名) imageUrl = DomainHelper.OssFullUrl(relatedTask.TaskUrl); } else { // 无任何图片可用,跳过 continue; } imageItems.Add((pageNum, imageUrl)); } // 按页码升序排序后输出图片地址 images.AddRange(imageItems.OrderBy(x => x.PageNum).Select(x => x.Url)); } else { // 非跨页题目:查询单条答题记录 var userAnswer = await answerRepository.Context.Queryable() .Where(a => a.JournalPageTaskId == mainTask.Id && a.UserId == userId) .FirstAsync(); // 赋值给 userAnswers(用于后续领取积分判断) userAnswers = userAnswer != null ? new List { userAnswer } : new List(); if (userAnswer != null && !string.IsNullOrEmpty(userAnswer.PageAnswerUrl)) { // 已答题,使用 PageAnswerUrl(加上域名) images.Add(DomainHelper.OssFullUrl(userAnswer.PageAnswerUrl)); } // 未答题不返回任何图片,images 保持空集合 } #endregion #region 5. 获取页面作答点读图片集合(PageAnswerDotUrl) // ============================================ // 【获取页面作答点读图片地址集合】 // 规则:已答题且 PageAnswerDotUrl 有值才返回;未答题或字段为空则跳过 // 跨页题目:遍历所有相关任务,收集有答题记录的 PageAnswerDotUrl // 非跨页题目:取当前任务答题记录的 PageAnswerDotUrl(仅在有值时返回) // ============================================ var pageAnswerDotUrls = new List(); if (isCrossPage) { // 跨页题目:遍历相关任务,收集有答题记录的 PageAnswerDotUrl(加上域名) foreach (var relatedTask in relatedTasks) { var userAnswer = userAnswers.FirstOrDefault(a => a.JournalPageTaskId == relatedTask.Id); if (userAnswer != null && !string.IsNullOrEmpty(userAnswer.PageAnswerDotUrl)) { pageAnswerDotUrls.Add(DomainHelper.OssFullUrl(userAnswer.PageAnswerDotUrl)); } } } else { // 非跨页题目:取当前任务答题记录的 PageAnswerDotUrl(加上域名) // 仅在已答题且 PageAnswerDotUrl 有值时返回 if (userAnswers != null && userAnswers.Count > 0) { var userAnswer = userAnswers.FirstOrDefault(); if (userAnswer != null && !string.IsNullOrEmpty(userAnswer.PageAnswerDotUrl)) { pageAnswerDotUrls.Add(DomainHelper.OssFullUrl(userAnswer.PageAnswerDotUrl)); } } } #endregion #region 6. 判断是否可领取积分 // ============================================ // 【是否可领取积分判断】 // 条件:任务已完成且未领取过积分 // - Status = 1(已批阅) // - AnswerStatus >= 1(已答题:正确/半对/错误) // 跨页题目:所有任务都满足条件才能领取 // 普通题目:当前任务满足条件即可领取 // 【关键】子题目入口(mainTask == null)无法领取积分 // ============================================ bool canClaimPoints = false; // 【关键】如果主任务不存在(子题目入口),无法领取积分 if (mainTask == null) { // 子题目无法单独领取积分,CanClaimPoints 为 false } else { // 查询是否已领取过积分(用主任务的 GroupId 作为 RelatedId) var existingRecord = await answerRepository.Context.Queryable() .Where(r => r.UserId == userId) .Where(r => r.ChangeType == PointsChangeTypeEnum.TaskReward.ToString() && r.RelatedId == mainTask.GroupId) .AnyAsync(); if (!existingRecord) { // 未领取过积分,检查任务是否完成 // 条件:Status=1 canClaimPoints = relatedTasks.All(t => userAnswers.Any(a => a.JournalPageTaskId == t.Id && a.Status == (int)UserAnswerStatusEnum.Complete)); } } #endregion #region 7. 获取任务名称 // ============================================ // 【获取任务名称】 // 跨页题目:取主任务(Id == GroupId)的 Task 字段 // 普通题目:取当前任务的 Task 字段 // 子题目入口:查询主任务的 Task 字段 // ============================================ string taskName; if (mainTask != null) { // 主任务入口,直接使用 mainTask taskName = mainTask.Task ?? string.Empty; } else { // 子题目入口:查询主任务(Id == GroupId) var realMainTask = allTasks.FirstOrDefault(t => t.Id == groupId); if (realMainTask != null) { taskName = realMainTask.Task ?? string.Empty; } else { // 通过 GroupId 查询主任务(适用于子任务入口的情况) var mainTaskByGroupId = await taskRepository.Context.Queryable() .Where(t => t.Id == t.GroupId && t.GroupId == groupId) .FirstAsync(); taskName = mainTaskByGroupId?.Task ?? string.Empty; } } #endregion #region 8. 返回结果 return new CrossPageTaskOutput { FirstLevelCatalogName = firstLevelCatalogName, CatalogName = catalogName, TaskName = taskName, Images = images, PageAnswerDotUrl = pageAnswerDotUrls, IsCrossPage = isCrossPage, CanClaimPoints = canClaimPoints }; #endregion } /// /// ============================================ /// 方法思路:领取任务积分 /// 1. 根据 GroupId 查询任务信息,判断是否跨页题目 /// 2. 查询用户答题记录,校验主任务(JournalPageTaskId == GroupId)才能领取 /// 3. 完成校验:Status=1 且 AnswerStatus>=1,跨页题目需所有任务都完成 /// 4. 重复领取校验:通过 PointsRecord 表的 RelatedId + ChangeType 判断 /// 5. 计算积分、成长值、得分(均取主任务数据) /// 6. 调用积分服务增加积分 /// ============================================ /// /// 任务分组Id(JournalPageTask.GroupId) /// 用户ID /// 领取积分结果(总成长值、总积分、得分、任务编号) public async Task ClaimTaskPointsAsync(long groupId, long userId) { #region 1. 查询任务与答题记录 // ============================================ // 1. 查询任务与答题记录 // ============================================ var relatedTasks = await taskRepository.Context.Queryable() .Where(t => t.GroupId == groupId) .ToListAsync(); if (relatedTasks == null || relatedTasks.Count == 0) { throw new BusinessException("任务不存在", ResultCode.NOT_FOUND); } // 主任务:Id == GroupId var mainTask = relatedTasks.FirstOrDefault(t => t.Id == groupId); if (mainTask == null) { throw new BusinessException("子题目无法单独领取积分,请从主任务入口领取", ResultCode.BAD_REQUEST); } // 查询用户答题记录 var userAnswers = await answerRepository.Context.Queryable() .Where(a => a.JournalPageTaskGroupId == groupId && a.UserId == userId) .ToListAsync(); if (userAnswers == null || userAnswers.Count == 0) { throw new BusinessException("请先完成答题再领取积分", ResultCode.BAD_REQUEST); } #endregion #region 2. 完成校验 // ============================================ // 2. 完成校验 // 主任务校验 + 跨页题目所有任务校验 // 完成条件:Status=1 // ============================================ // 校验主任务 var mainTaskAnswer = userAnswers.FirstOrDefault(a => a.JournalPageTaskId == groupId); if (mainTaskAnswer == null || mainTaskAnswer.Status != (int)UserAnswerStatusEnum.Complete) { throw new BusinessException("请先完成答题再领取积分", ResultCode.BAD_REQUEST); } // 跨页题目:校验所有任务 if (relatedTasks.Count > 1) { foreach (var task in relatedTasks) { var answer = userAnswers.FirstOrDefault(a => a.JournalPageTaskId == task.Id); if (answer == null || answer.Status != (int)UserAnswerStatusEnum.Complete) { throw new BusinessException("跨页题目需要完成所有任务才能领取积分", ResultCode.BAD_REQUEST); } } } #endregion #region 3. 重复领取校验与积分计算 // ============================================ // 3. 重复领取校验与积分计算 // ============================================ // 重复领取校验 var existingRecord = await answerRepository.Context.Queryable() .Where(r => r.UserId == userId) .Where(r => r.ChangeType == nameof(PointsChangeTypeEnum.TaskReward) && r.RelatedId == groupId) .FirstAsync(); if (existingRecord != null) { throw new BusinessException("积分已领取,请勿重复领取", ResultCode.BAD_REQUEST); } // 积分、成长值、得分(均取主任务数据) int totalPoints = mainTask.Points; int totalGrowthPoints = mainTask.GrowthPoint; float score = mainTaskAnswer.Score; if (totalPoints <= 0) { throw new BusinessException("该任务无积分可领取", ResultCode.BAD_REQUEST); } #endregion #region 4. 事务执行领取操作 // ============================================ // 4. 事务执行领取操作 // - 手动更新 Users.Points(积分累加) // - 插入积分流水记录 // - 喂养宠物(如有活跃宠物,更新宠物成长值) // ============================================ // 查询用户信息(获取当前积分) var user = await usersRepository.GetByIdAsync(userId); if (user == null) { throw new BusinessException("用户不存在", ResultCode.NOT_FOUND); } // 查询用户活跃宠物 var pet = await userPetRepository.Context.Queryable() .Where(p => p.UserId == userId && p.Status == (int)UserPetStatusEnum.Active) .FirstAsync(); var result = new ClaimTaskPointsOutput(); await answerRepository.UseTranAsync(async () => { // 4a. 手动更新 Users.Points(积分累加) var newPointsBalance = user.Points + totalPoints; // 4b. 手动更新 Users.GrowthPoints(成长值累加) var newGrowthPointsBalance = user.GrowthPoints + totalGrowthPoints; await answerRepository.Context.Updateable() .SetColumns(u => u.Points == newPointsBalance) .SetColumns(u => u.GrowthPoints == newGrowthPointsBalance) .SetColumns(u => u.UpdatedAt == DateTime.Now) .Where(u => u.Id == userId) .ExecuteCommandAsync(); // 4b. 插入积分流水记录 var pointsRecord = new PointsRecord { UserId = userId, ChangeAmount = totalPoints, BalanceAfter = newPointsBalance, ChangeType = PointsChangeTypeEnum.TaskReward.ToString(), RelatedId = groupId, Description = $"完成任务奖励,GroupId:{mainTask.GroupId}", Type = PointsFlowTypeEnum.Income, Status = (int)PointsRecordStatusEnum.Success, IsDeleted = false, CreatedBy = "系统", CreatedAt = DateTime.Now, UpdatedBy = "系统", UpdatedAt = DateTime.Now }; await answerRepository.Context.Insertable(pointsRecord).ExecuteCommandAsync(); // 4c. 如果有活跃宠物,喂养宠物(同一事务内,更新宠物成长值) if (pet != null && totalGrowthPoints > 0) { await petService.FeedPetInTranAsync(userId, new Models.Dto.Pet.FeedPetInput { PetId = pet.Id, GrowthPoints = totalGrowthPoints }); } // ============================================ // 【任务编号计算】 // 从 JournalPageTask.No 字段按 - 分割,取索引为 2 的部分 // 例如:"A-B-C-D" → 分割后 ["A","B","C","D"] → 取索引2 = "C" // 前面加上"任务" // ============================================ string taskNo = string.Empty; if (!string.IsNullOrEmpty(mainTask.No)) { var parts = mainTask.No.Split('-'); if (parts.Length >= 3) { taskNo = $"任务{parts[2]}"; } } result.TotalGrowthPoints = totalGrowthPoints; result.TotalPoints = totalPoints; result.Score = score; result.TaskNo = taskNo; }); return result; #endregion } /// /// ============================================ /// 方法思路:批量领取任务积分(一键领取) /// 1. 遍历每个传入的 GroupId,做校验,筛选出可领取的任务 /// 2. 在事务中执行所有写操作: /// - 更新 Users 表 Points(积分服务内部处理) /// - 更新 Users 表 GrowthPoints(累加) /// - 喂养宠物(如有活跃宠物) /// 3. 返回结果包含: /// - 成功数量和失败数量 /// - 成功详情列表(每个GroupId的名称、积分、成长值、得分) /// - 失败详情列表(每个GroupId的错误原因) /// - 总积分、总成长值、领取后余额 /// ============================================ /// /// 批量领取输入(GroupId列表) /// 用户ID /// 批量领取结果 public async Task BatchClaimPointsAsync(BatchClaimPointsInput input, long userId) { #region 1. 初始化与参数校验 var result = new BatchClaimPointsOutput(); if (input == null || input.GroupIds == null || input.GroupIds.Count == 0) { throw new BusinessException("任务分组Id列表不能为空", ResultCode.BAD_REQUEST); } var groupIds = input.GroupIds.Distinct().ToList(); #endregion #region 2. 批量查询已领取的积分记录 // 批量查询已领取的积分记录(用于重复领取校验) var claimedRecords = await answerRepository.Context.Queryable() .Where(r => groupIds.Contains((long)r.RelatedId!) && r.UserId == userId) .Where(r => r.ChangeType == PointsChangeTypeEnum.TaskReward.ToString()) .ToListAsync(); var claimedGroupIds = claimedRecords.Select(r => r.RelatedId!.Value).ToHashSet(); #endregion #region 3. 逐个校验每个 GroupId,筛选可领取的任务 // 存储可领取的任务信息 var claimableTasks = new List<(long GroupId, int Points, int GrowthPoints, float Score, string TaskName, string TaskNo)>(); foreach (var groupId in groupIds) { try { #region 3.1 根据 GroupId 查询所有相关任务 var relatedTasks = await taskRepository.Context.Queryable() .Where(t => t.GroupId == groupId) .ToListAsync(); if (relatedTasks == null || relatedTasks.Count == 0) { result.FailItems.Add(new BatchClaimFailItem { GroupId = groupId, Message = "任务不存在" }); continue; } #endregion #region 3.2 判断是否跨页题目并找主任务 bool isCrossPage = relatedTasks.Count > 1; // 主任务:Id == GroupId var mainTask = relatedTasks.FirstOrDefault(t => t.Id == groupId); if (mainTask == null) { result.FailItems.Add(new BatchClaimFailItem { GroupId = groupId, Message = "子题目无法单独领取积分,请从主任务入口领取" }); continue; } #endregion #region 3.3 查询用户答题记录 var userAnswers = await answerRepository.Context.Queryable() .Where(a => a.JournalPageTaskGroupId == groupId && a.UserId == userId) .ToListAsync(); if (userAnswers == null || userAnswers.Count == 0) { result.FailItems.Add(new BatchClaimFailItem { GroupId = groupId, Message = "请先完成答题再领取积分" }); continue; } #endregion #region 3.4 完成校验 // 校验主任务完成状态:Status=1 var mainTaskAnswer = userAnswers.FirstOrDefault(a => a.JournalPageTaskId == groupId); if (mainTaskAnswer == null || mainTaskAnswer.Status != (int)UserAnswerStatusEnum.Complete) { result.FailItems.Add(new BatchClaimFailItem { GroupId = groupId, Message = "请先完成答题再领取积分" }); continue; } // 跨页题目:校验所有任务 if (isCrossPage) { bool allCompleted = true; foreach (var task in relatedTasks) { var answer = userAnswers.FirstOrDefault(a => a.JournalPageTaskId == task.Id); if (answer == null || answer.Status != (int)UserAnswerStatusEnum.Complete) { allCompleted = false; break; } } if (!allCompleted) { result.FailItems.Add(new BatchClaimFailItem { GroupId = groupId, Message = "跨页题目需要完成所有任务才能领取积分" }); continue; } } #endregion #region 3.5 重复领取校验 if (claimedGroupIds.Contains(groupId)) { result.FailItems.Add(new BatchClaimFailItem { GroupId = groupId, Message = "积分已领取,请勿重复领取" }); continue; } #endregion #region 3.6 计算积分、成长值、得分 int points = mainTask.Points; int growthPoints = mainTask.GrowthPoint; float score = mainTaskAnswer.Score; if (points <= 0) { result.FailItems.Add(new BatchClaimFailItem { GroupId = groupId, Message = "该任务无积分可领取" }); continue; } var taskName = mainTask.Task ?? string.Empty; // 任务编号计算 string taskNo = string.Empty; if (!string.IsNullOrEmpty(mainTask.No)) { var parts = mainTask.No.Split('-'); if (parts.Length >= 3) { taskNo = $"任务{parts[2]}"; } } #endregion // 添加到可领取列表 claimableTasks.Add((groupId, points, growthPoints, score, taskName, taskNo)); } catch (Exception ex) { // 捕获异常并添加到失败列表,不中断其他任务的处理 result.FailItems.Add(new BatchClaimFailItem { GroupId = groupId, Message = ex.Message }); } } #endregion #region 4. 事务执行所有领取操作 // ============================================ // 4. 事务执行所有领取操作 // - 手动更新 Users.Points(积分累加) // - 插入积分流水记录 // - 喂养宠物(如有活跃宠物,更新宠物成长值) // ============================================ if (claimableTasks.Count > 0) { // 查询用户信息(获取当前积分) var user = await usersRepository.GetByIdAsync(userId); if (user == null) { throw new BusinessException("用户不存在", ResultCode.NOT_FOUND); } // 查询用户活跃宠物 var pet = await userPetRepository.Context.Queryable() .Where(p => p.UserId == userId && p.Status == (int)UserPetStatusEnum.Active) .FirstAsync(); int totalGrowthPoints = 0; int totalPointsAmount = 0; await answerRepository.UseTranAsync(async () => { foreach (var task in claimableTasks) { totalPointsAmount += task.Points; totalGrowthPoints += task.GrowthPoints; // 添加成功详情 result.SuccessItems.Add(new BatchClaimSuccessItem { TaskNo = task.TaskNo, Points = task.Points, GrowthPoints = task.GrowthPoints, Score = task.Score }); } // 4a. 手动更新 Users.Points(积分累加) var newPointsBalance = user.Points + totalPointsAmount; // 4b. 手动更新 Users.GrowthPoints(成长值累加) var newGrowthPointsBalance = user.GrowthPoints + totalGrowthPoints; await answerRepository.Context.Updateable() .SetColumns(u => u.Points == newPointsBalance) .SetColumns(u => u.GrowthPoints == newGrowthPointsBalance) .SetColumns(u => u.UpdatedAt == DateTime.Now) .Where(u => u.Id == userId) .ExecuteCommandAsync(); // 4b. 批量插入积分流水记录 var records = new List(); foreach (var task in claimableTasks) { records.Add(new PointsRecord { UserId = userId, ChangeAmount = task.Points, BalanceAfter = newPointsBalance, ChangeType = PointsChangeTypeEnum.TaskReward.ToString(), RelatedId = task.GroupId, Description = $"完成任务奖励,GroupId:{task.GroupId}", Type = PointsFlowTypeEnum.Income, Status = (int)PointsRecordStatusEnum.Success, IsDeleted = false, CreatedBy = "系统", CreatedAt = DateTime.Now, UpdatedBy = "系统", UpdatedAt = DateTime.Now }); } await answerRepository.Context.Insertable(records).ExecuteCommandAsync(); // 4c. 喂养宠物(同一事务内,更新宠物成长值) if (pet != null && totalGrowthPoints > 0) { await petService.FeedPetInTranAsync(userId, new Models.Dto.Pet.FeedPetInput { PetId = pet.Id, GrowthPoints = totalGrowthPoints }); } }); result.SuccessCount = claimableTasks.Count; } #endregion #region 5. 组装返回结果 result.FailCount = result.FailItems.Count; return result; #endregion } }