From 1eb986177d1b56ee8baf5daf03a70e4229c9bb51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=B7=E6=AD=A6=20=E9=BB=84?= <865114447@qq.com> Date: Thu, 25 Jun 2026 14:53:45 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=96=B9=E6=B3=95=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E3=80=81=E4=BB=A5=E5=8F=8A=E4=B8=9A=E5=8A=A1=E8=B0=83?= =?UTF-8?q?=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../IUserAnswerTaskService.cs | 8 +- .../BatchClaimPointsOutput.cs | 45 +- .../ClaimTaskPointsOutput.cs | 5 + .../CrossPageTaskOutput.cs | 7 +- .../UserAnswerTaskListOutput.cs | 7 +- .../Entity/JournalPageTaskUserAnswer.cs | 7 + .../UserAnswerTaskService.cs | 761 ++++++++++++++---- .../WeChat/UserAnswerTaskController.cs | 18 +- 8 files changed, 688 insertions(+), 170 deletions(-) diff --git a/QYZH.InteractiveMagazine.IService/IUserAnswerTaskService.cs b/QYZH.InteractiveMagazine.IService/IUserAnswerTaskService.cs index 75ec763..6b21c58 100644 --- a/QYZH.InteractiveMagazine.IService/IUserAnswerTaskService.cs +++ b/QYZH.InteractiveMagazine.IService/IUserAnswerTaskService.cs @@ -50,10 +50,10 @@ public interface IUserAnswerTaskService : IBaseService /// 领取任务积分 /// - /// 任务Id + /// 任务分组Id(JournalPageTask.GroupId) /// 用户Id /// 领取结果 - Task ClaimTaskPointsAsync(long journalPageTaskId, long userId); + Task ClaimTaskPointsAsync(long journalPageTaskGroupId, long userId); /// /// 批量领取任务积分(一键领取) @@ -75,7 +75,7 @@ public interface IUserAnswerTaskService : IBaseService /// 新增学生参数 - /// 当前登录用户Id(WxUserId) + /// 当前登录用户微信UserId(WxUserId) /// 新增学生结果 - Task AddStudentAsync(AddStudentInput input, long userId); + Task AddStudentAsync(AddStudentInput input, long wxUserId); } diff --git a/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/BatchClaimPointsOutput.cs b/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/BatchClaimPointsOutput.cs index 651bd3f..4054b17 100644 --- a/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/BatchClaimPointsOutput.cs +++ b/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/BatchClaimPointsOutput.cs @@ -8,7 +8,7 @@ public class BatchClaimPointsInput /// /// 任务分组Id列表(JournalPageTaskGroupId) /// - public List GroupIds { get; set; } = new(); + public List GroupIds { get; set; } = []; } /// @@ -31,15 +31,56 @@ public class BatchClaimPointsOutput /// public int TotalPoints { get; set; } + /// + /// 累计获得成长值 + /// + public int TotalGrowthPoints { get; set; } + /// /// 领取后积分余额 /// public int BalanceAfter { get; set; } + /// + /// 成功领取详情列表 + /// + public List SuccessItems { get; set; } = []; + /// /// 失败详情 /// - public List FailItems { get; set; } = new(); + public List FailItems { get; set; } = []; +} + +/// +/// 批量领取成功项 +/// +public class BatchClaimSuccessItem +{ + /// + /// 任务分组Id + /// + public long GroupId { get; set; } + + /// + /// 任务名称 + /// + public string TaskName { get; set; } + + /// + /// 积分值 + /// + public int Points { get; set; } + + /// + /// 成长值 + /// + public int GrowthPoints { get; set; } + + /// + /// 题目得分(从 JournalPageTaskUserAnswer 表获取,该 GroupId 下所有相关任务得分之和) + /// + public float Score { get; set; } } /// diff --git a/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/ClaimTaskPointsOutput.cs b/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/ClaimTaskPointsOutput.cs index e402652..4a6f831 100644 --- a/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/ClaimTaskPointsOutput.cs +++ b/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/ClaimTaskPointsOutput.cs @@ -29,4 +29,9 @@ public class ClaimTaskPointsOutput /// 任务名称 /// public string TaskName { get; set; } + + /// + /// 题目得分(从 JournalPageTaskUserAnswer 表获取,跨页题目为所有相关任务得分之和) + /// + public float Score { get; set; } } diff --git a/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/CrossPageTaskOutput.cs b/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/CrossPageTaskOutput.cs index 845a7f5..c9c9200 100644 --- a/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/CrossPageTaskOutput.cs +++ b/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/CrossPageTaskOutput.cs @@ -6,7 +6,12 @@ namespace QYZH.InteractiveMagazine.Models.Dto.UserAnswerTaskService; public class CrossPageTaskOutput { /// - /// 栏目名称 + /// 一级栏目名称 + /// + public string FirstLevelCatalogName { get; set; } + + /// + /// 栏目名称(二级栏目/文章名称) /// public string CatalogName { get; set; } diff --git a/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/UserAnswerTaskListOutput.cs b/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/UserAnswerTaskListOutput.cs index 59eafd4..b9d3951 100644 --- a/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/UserAnswerTaskListOutput.cs +++ b/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/UserAnswerTaskListOutput.cs @@ -6,7 +6,12 @@ namespace QYZH.InteractiveMagazine.Models.Dto.UserAnswerTaskService; public class UserAnswerTaskListOutput { /// - /// 目录名称 + /// 一级栏目名称 + /// + public string FirstLevelCatalogName { get; set; } + + /// + /// 目录名称(二级栏目) /// public string CatalogName { get; set; } diff --git a/QYZH.InteractiveMagazine.Models/Entity/JournalPageTaskUserAnswer.cs b/QYZH.InteractiveMagazine.Models/Entity/JournalPageTaskUserAnswer.cs index 2dda2bc..dfb1dfd 100644 --- a/QYZH.InteractiveMagazine.Models/Entity/JournalPageTaskUserAnswer.cs +++ b/QYZH.InteractiveMagazine.Models/Entity/JournalPageTaskUserAnswer.cs @@ -64,6 +64,13 @@ namespace QYZH.InteractiveMagazine.Models.Entity /// public float GrowthPoints { get; set; } + /// + /// Desc:题目得分 + /// Default:0 + /// Nullable:False + /// + public float Score { get; set; } + /// /// Desc:题目作答图片地址 /// Default: diff --git a/QYZH.InteractiveMagazine.Service/UserAnswerTaskService.cs b/QYZH.InteractiveMagazine.Service/UserAnswerTaskService.cs index dd8a94d..c5a832f 100644 --- a/QYZH.InteractiveMagazine.Service/UserAnswerTaskService.cs +++ b/QYZH.InteractiveMagazine.Service/UserAnswerTaskService.cs @@ -28,10 +28,37 @@ public class UserAnswerTaskService( : BaseRepository, IUserAnswerTaskService { /// - /// 获取用户期刊学习进度列表(按期刊分组,批量预加载避免N+1) + /// ============================================ + /// 方法思路: + /// 1. 批量预加载策略:为避免N+1查询问题,先按期刊维度批量加载所有关联数据 + /// 2. 跨页题目处理核心: + /// - 跨页题目定义:JournalPageTask.Id != GroupId(同一个GroupId下有多个任务) + /// - 进度计算:按GroupId去重,已完成条件是该GroupId下所有任务都完成(Status=1) + /// - 积分计算:按GroupId去重,每个GroupId只计算一次积分(取第一个任务的积分) + /// 3. 状态判定逻辑: + /// - 未开始:无任何作答记录 + /// - 进行中:有作答记录但未全部完成 + /// - 已完成:所有GroupId都完成 + /// ============================================ /// + /// 用户ID + /// 用户所有期刊的学习进度列表 public async Task> GetJournalProgressAsync(long userId) { + // ============================================ + // 跨页题目处理核心逻辑说明: + // 1. 跨页题目定义:JournalPageTask.Id != GroupId + // - Id 是任务的唯一标识 + // - GroupId 是跨页题目的分组标识,同一个跨页题目的所有任务共享同一个GroupId + // 2. 进度计算:按 GroupId 去重 + // - 已完成条件:该 GroupId 下所有任务都完成(Status=1) + // 3. 积分计算:按 GroupId 去重,每个 GroupId 只计算一次积分 + // 4. 状态判定: + // - 未开始:无任何 GroupId 被作答 + // - 进行中:有 GroupId 被作答但未全部完成 + // - 已完成:所有 GroupId 都完成 + // ============================================ + // 1. 查询用户的所有作答记录 var userAnswers = await answerRepository.Context.Queryable() .Where(a => a.UserId == userId && !a.IsDeleted) @@ -45,17 +72,17 @@ public class UserAnswerTaskService( // 2. 提取关联的期刊ID(去重) var journalIds = userAnswers.Select(a => a.JournalId).Distinct().ToList(); - // 3. 批量预加载关联实体(按期刊维度全量加载) + // 3. 批量预加载关联实体(按期刊维度全量加载,避免N+1查询) var journals = await journalRepository.Context.Queryable() .Where(j => journalIds.Contains(j.Id) && !j.IsDeleted) .ToListAsync(); - // 加载这些期刊的一级目录(ParentId == 0 表示一级) + // 加载这些期刊的一级目录(ParentId == 0 表示一级目录) var allCatalogs = await catalogRepository.Context.Queryable() .Where(c => journalIds.Contains(c.JournalId) && c.ParentId == 0 && !c.IsDeleted) .ToListAsync(); - // 加载这些期刊的所有任务(直接按 JournalId) + // 加载这些期刊的所有任务 var allTasks = await taskRepository.Context.Queryable() .Where(t => journalIds.Contains(t.JournalId) && !t.IsDeleted) .ToListAsync(); @@ -68,7 +95,7 @@ public class UserAnswerTaskService( // 4. 查询用户连续打卡天数 var consecutiveDays = await CalculateConsecutiveDaysAsync(userId); - // 5. 提取用户已作答的 ID 集合(包含进行中和已完成) + // 5. 提取用户已作答的任务ID集合(包含进行中和已完成) var userAnsweredTaskIds = userAnswers.Select(a => a.JournalPageTaskId).Distinct().ToHashSet(); var userAnsweredPageIds = userAnswers.Select(a => a.JournalPageId).Distinct().ToHashSet(); @@ -80,29 +107,50 @@ public class UserAnswerTaskService( foreach (var journal in journals) { - // 一级栏目数 - var catalogCount = allCatalogs.Count(c => c.JournalId == journal.Id); - // 该期刊下所有任务 var journalTasks = allTasks.Where(t => t.JournalId == journal.Id).ToList(); - var totalTaskCount = journalTasks.Count; - // 已完成任务数(用户作答中属于本期刊的 Distinct JournalPageTaskGroupId) - var completedTaskCount = userAnswers - .Where(a => a.JournalId == journal.Id) - .Select(a => a.JournalPageTaskGroupId) - .Distinct() + // 该期刊的一级栏目数 + var catalogCount = allCatalogs.Count(c => c.JournalId == journal.Id); + + // 【跨页题目-进度计算】总任务数:按 GroupId 去重,跨页题目只算一个 + var totalTaskCount = journalTasks.Select(t => t.GroupId).Distinct().Count(); + + // 【跨页题目-已完成判断】已完成任务数:按 GroupId 去重 + // 关键:必须该 GroupId 下所有任务都完成,才算该 GroupId 完成 + var completedTaskCount = journalTasks + .GroupBy(t => t.GroupId) + .Where(g => g.All(t => userCompletedTaskIds.Contains(t.Id))) .Count(); - // 成长值 & 积分值(用户已完成任务的 JournalPageTask 聚合,Status=1) - var completedTasks = journalTasks.Where(t => userCompletedTaskIds.Contains(t.Id)).ToList(); - var totalGrowthPoints = completedTasks.Sum(t => t.GrowthPoint ?? 0); - var totalPoints = completedTasks.Sum(t => t.Points); + // 【跨页题目-积分计算】获取已完成的 GroupId 列表 + var completedGroupIds = journalTasks + .GroupBy(t => t.GroupId) + .Where(g => g.All(t => userCompletedTaskIds.Contains(t.Id))) + .Select(g => g.Key) + .ToList(); - // 进度:已完成任务ID数 / 总任务数(Status=1才算完成) - var completedTaskIdCount = journalTasks.Count(t => userCompletedTaskIds.Contains(t.Id)); - var progress = totalTaskCount > 0 - ? Math.Round((double)completedTaskIdCount / totalTaskCount * 100, 2) + // 【跨页题目-积分去重】每个已完成的 GroupId 只取第一个任务的积分 + // 原因:同一个跨页题目的多个任务只应该计算一次积分 + var distinctCompletedTasks = journalTasks + .Where(t => completedGroupIds.Contains(t.GroupId)) + .GroupBy(t => t.GroupId) + .Select(g => g.First()) + .ToList(); + + // 累计已完成的跨页题目的总成长值和总积分 + var totalGrowthPoints = distinctCompletedTasks.Sum(t => t.GrowthPoint ?? 0); + var totalPoints = distinctCompletedTasks.Sum(t => t.Points); + + // 【跨页题目-进度百分比计算】 + var totalGroupIds = journalTasks.Select(t => t.GroupId).Distinct().Count(); + var completedGroupIdCount = journalTasks + .GroupBy(t => t.GroupId) + .Where(g => g.All(t => userCompletedTaskIds.Contains(t.Id))) + .Count(); + + var progress = totalGroupIds > 0 + ? Math.Round((double)completedGroupIdCount / totalGroupIds * 100, 2) : 0; // 剩余篇数:该期刊总页数 - 用户已作答页数 @@ -129,8 +177,15 @@ public class UserAnswerTaskService( } /// - /// 计算用户连续打卡天数 + /// ============================================ + /// 方法思路:计算用户连续打卡天数 + /// 1. 查询用户最近一次签到记录 + /// 2. 判断是否连续:如果最后一次签到是今天或昨天,连续天数延续 + /// 3. 否则返回0(中断了) + /// ============================================ /// + /// 用户ID + /// 连续打卡天数 private async Task CalculateConsecutiveDaysAsync(long userId) { var today = DateTime.Now.Date; @@ -158,8 +213,26 @@ public class UserAnswerTaskService( } /// - /// 获取目录列表(按一级目录分组,二级目录含完成状态,批量预加载避免N+1) + /// ============================================ + /// 方法思路:获取目录列表(按一级目录分组展示) + /// 1. 批量预加载:该期刊下所有目录、页面、任务、用户作答记录 + /// 2. 目录结构处理: + /// - 一级目录:ParentId=0 且 Type=Unit + /// - 二级目录:ParentId=一级目录Id 且 Type=Unit(可选) + /// - 文章目录:Type=Page + /// 3. 跨页题目处理: + /// - 状态判断:按GroupId去重,已完成条件是该GroupId下所有任务都完成(Status=1) + /// - 积分计算:按GroupId去重,每个GroupId只计算一次积分 + /// - 已作答判断:只要有一个任务作答就算该GroupId已作答(包含进行中和已完成) + /// 4. 状态判定: + /// - 未开始:无任何GroupId被作答 + /// - 进行中:有GroupId被作答但未全部完成 + /// - 已完成:所有GroupId都完成 + /// ============================================ /// + /// 期刊ID + /// 用户ID + /// 目录列表(按一级目录分组) public async Task> GetCatalogListAsync(long journalId, long userId) { // 1. 查询期刊 @@ -247,7 +320,8 @@ public class UserAnswerTaskService( // 二级单元下的所有任务 var secondLevelTasks = allTasks.Where(t => secondLevelPageIds.Contains(t.JournalPageId)).ToList(); - // 判定状态(Status=0进行中,Status=1已完成) + // 判定状态(Status=0进行中,Status=1已完成,跨页题目按GroupId去重) + // 跨页题目必须所有任务都完成,才算该 GroupId 已完成 string status; if (secondLevelTasks.Count == 0) { @@ -255,19 +329,40 @@ public class UserAnswerTaskService( } else { - var completedCount = secondLevelTasks.Count(t => userCompletedTaskIds.Contains(t.Id)); - var answeredCount = secondLevelTasks.Count(t => userAnsweredTaskIds.Contains(t.Id)); - if (answeredCount == 0) - status = "未开始"; - else if (completedCount == secondLevelTasks.Count) - status = "已完成"; - else - status = "进行中"; - } + // 总 GroupId 数(跨页题目只算一个) + var totalGroupCount = secondLevelTasks.Select(t => t.GroupId).Distinct().Count(); + // 已完成 GroupId 数(该 GroupId 下所有任务都完成才算完成) + var completedGroupCount = secondLevelTasks + .GroupBy(t => t.GroupId) + .Where(g => g.All(t => userCompletedTaskIds.Contains(t.Id))) + .Count(); + // 【关键】已作答 GroupId 数:只要有一个任务作答就算该 GroupId 已作答(包含进行中和已完成) + // 这个字段用于判断状态:未开始 / 进行中 / 已完成 + var answeredGroupCount = secondLevelTasks + .Where(t => userAnsweredTaskIds.Contains(t.Id)) + .Select(t => t.GroupId) + .Distinct() + .Count(); - // 计算该二级目录的总成长值和总积分 - var totalGrowthPoints = secondLevelTasks.Sum(t => t.GrowthPoint) ?? 0; - var totalPoints = secondLevelTasks.Sum(t => t.Points); + // 状态判定逻辑: + // - answeredGroupCount == 0:没有任何 GroupId 被作答 → 未开始 + // - completedGroupCount == totalGroupCount:所有 GroupId 都完成 → 已完成 + // - 其他:有 GroupId 被作答但未全部完成 → 进行中 + if (answeredGroupCount == 0) + status = "未开始"; + else if (completedGroupCount == totalGroupCount) + status = "已完成"; + else + status = "进行中"; + } + + // 计算该二级目录的总成长值和总积分(跨页题目按GroupId去重) + var distinctSecondLevelTasks = secondLevelTasks + .GroupBy(t => t.GroupId) + .Select(g => g.First()) + .ToList(); + var totalGrowthPoints = distinctSecondLevelTasks.Sum(t => t.GrowthPoint) ?? 0; + var totalPoints = distinctSecondLevelTasks.Sum(t => t.Points); children.Add(new SecondCatalogOutput { @@ -293,7 +388,8 @@ public class UserAnswerTaskService( // 该文章下的所有任务 var articleTasks = allTasks.Where(t => articlePageIds.Contains(t.JournalPageId)).ToList(); - // 判定状态(Status=0进行中,Status=1已完成) + // 判定状态(Status=0进行中,Status=1已完成,跨页题目按GroupId去重) + // 跨页题目必须所有任务都完成,才算该 GroupId 已完成 string status; if (articleTasks.Count == 0) { @@ -301,19 +397,35 @@ public class UserAnswerTaskService( } else { - var completedCount = articleTasks.Count(t => userCompletedTaskIds.Contains(t.Id)); - var answeredCount = articleTasks.Count(t => userAnsweredTaskIds.Contains(t.Id)); - if (answeredCount == 0) + // 总 GroupId 数(跨页题目只算一个) + var totalGroupCount = articleTasks.Select(t => t.GroupId).Distinct().Count(); + // 已完成 GroupId 数(该 GroupId 下所有任务都完成才算完成) + var completedGroupCount = articleTasks + .GroupBy(t => t.GroupId) + .Where(g => g.All(t => userCompletedTaskIds.Contains(t.Id))) + .Count(); + // 已作答 GroupId 数(只要有一个任务作答就算已作答,包含进行中和已完成) + var answeredGroupCount = articleTasks + .Where(t => userAnsweredTaskIds.Contains(t.Id)) + .Select(t => t.GroupId) + .Distinct() + .Count(); + + if (answeredGroupCount == 0) status = "未开始"; - else if (completedCount == articleTasks.Count) + else if (completedGroupCount == totalGroupCount) status = "已完成"; else status = "进行中"; } - // 计算该文章的总成长值和总积分 - var totalGrowthPoints = articleTasks.Sum(t => t.GrowthPoint) ?? 0; - var totalPoints = articleTasks.Sum(t => t.Points); + // 计算该文章的总成长值和总积分(跨页题目按GroupId去重) + var distinctArticleTasks = articleTasks + .GroupBy(t => t.GroupId) + .Select(g => g.First()) + .ToList(); + var totalGrowthPoints = distinctArticleTasks.Sum(t => t.GrowthPoint) ?? 0; + var totalPoints = distinctArticleTasks.Sum(t => t.Points); children.Add(new SecondCatalogOutput { @@ -343,11 +455,21 @@ public class UserAnswerTaskService( } /// - /// 获取一级目录概览(含二级目录详情、页码范围、任务完成状态,批量预加载避免N+1) + /// ============================================ + /// 方法思路:获取一级目录概览(含二级目录详情和任务完成状态) + /// 1. 批量预加载:该期刊下所有目录、页面、任务、用户作答记录 + /// 2. 获取该一级目录下的所有文章目录(Type=Page) + /// 3. 跨页题目处理: + /// - 总任务数:按GroupId去重(跨页题目只算一个) + /// - 状态判断:按GroupId去重,已完成条件是该GroupId下所有任务都完成(Status=1) + /// - 已作答判断:只要有一个任务作答就算该GroupId已作答 + /// 4. 状态格式:"已完成数/总数 已完成/进行中/未开始" + /// 例如:"2/3 进行中" 表示3个GroupId中已完成2个 + /// ============================================ /// - /// 一级烂ID + /// 一级目录ID /// 用户ID - /// + /// 一级目录概览信息 public async Task GetCatalogOverviewAsync(long catalogId, long userId) { // 1. 查询一级目录 @@ -397,8 +519,12 @@ public class UserAnswerTaskService( var allLevelPages = allPages.Where(p => pageCatalogIds.Contains(p.JournalCatalogId)).ToList(); var allLevelPageIds = allLevelPages.Select(p => p.Id).ToHashSet(); - // 7. 总任务数 - var totalTaskCount = allTasks.Count(t => allLevelPageIds.Contains(t.JournalPageId)); + // 7. 总任务数(按 GroupId 去重,跨页题目只算一个) + var totalTaskCount = allTasks + .Where(t => allLevelPageIds.Contains(t.JournalPageId)) + .Select(t => t.GroupId) + .Distinct() + .Count(); // 8. 构建子目录详情列表 var subCatalogs = new List(); @@ -415,27 +541,41 @@ public class UserAnswerTaskService( // 该文章下的所有任务 var articleTasks = allTasks.Where(t => articlePageIds.Contains(t.JournalPageId)).ToList(); - var totalTasks = articleTasks.Count; - - // 已完成任务数(Status=1) - var completedTasks = articleTasks.Count(t => userCompletedTaskIds.Contains(t.Id)); - // 已作答任务数(包含进行中和已完成) - var answeredTasks = articleTasks.Count(t => userAnsweredTaskIds.Contains(t.Id)); + // 总 GroupId 数(按 GroupId 去重,跨页题目只算一个) + var totalGroupCount = articleTasks.Select(t => t.GroupId).Distinct().Count(); // 页码范围 var startPage = articlePages.Count > 0 ? articlePages.Min(p => p.PageNum) : 0; var endPage = articlePages.Count > 0 ? articlePages.Max(p => p.PageNum) : 0; - // 状态格式:1/2 进行中、2/2 已完成、0/2 未开始(Status=0进行中,Status=1已完成) + // 状态格式:1/2 进行中、2/2 已完成、0/2 未开始(跨页题目按GroupId去重) + // 跨页题目必须所有任务都完成,才算该 GroupId 已完成 string status; - if (totalTasks == 0) + if (totalGroupCount == 0) + { status = $"0/0 未开始"; - else if (answeredTasks == 0) - status = $"0/{totalTasks} 未开始"; - else if (completedTasks == totalTasks) - status = $"{completedTasks}/{totalTasks} 已完成"; + } else - status = $"{completedTasks}/{totalTasks} 进行中"; + { + // 已完成 GroupId 数(该 GroupId 下所有任务都完成才算完成) + var completedGroupCount = articleTasks + .GroupBy(t => t.GroupId) + .Where(g => g.All(t => userCompletedTaskIds.Contains(t.Id))) + .Count(); + // 已作答 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 = $"{completedGroupCount}/{totalGroupCount} 已完成"; + else + status = $"{completedGroupCount}/{totalGroupCount} 进行中"; + } subCatalogs.Add(new SubCatalogDetailOutput { @@ -443,7 +583,7 @@ public class UserAnswerTaskService( SecondLevelCatalogName = pageCatalog.Name ?? string.Empty, StartPage = startPage, EndPage = endPage, - TaskCount = totalTasks, + TaskCount = totalGroupCount, Status = status }); } @@ -459,11 +599,24 @@ public class UserAnswerTaskService( } /// - /// 获取用户答题任务列表 + /// ============================================ + /// 方法思路:获取用户答题任务列表(某个二级栏目/文章下的所有任务) + /// 1. 查询目录信息:通过catalogId查询二级栏目 + /// 2. 查询一级栏目名称:通过ParentId关联查询一级栏目名称 + /// 3. 批量预加载:该栏目下的所有页面、该期刊下所有任务、用户作答记录 + /// 4. 跨页题目处理: + /// - 状态判断:按GroupId去重,已完成条件是该GroupId下所有任务都完成(Status=1) + /// - 积分计算:按GroupId去重,每个GroupId只计算一次积分和成长值 + /// - 已作答判断:只要有一个任务作答就算该GroupId已作答 + /// 5. 徽章统计:统计该期刊下已获得的徽章数量 + /// ============================================ /// + /// 二级栏目/文章目录ID + /// 用户ID + /// 用户答题任务列表信息 public async Task GetUserAnswerTaskListAsync(long catalogId, long userId) { - // 1. 查询目录 + // 1. 查询目录(二级栏目) var catalog = await catalogRepository.Context.Queryable() .Where(c => c.Id == catalogId && !c.IsDeleted) .FirstAsync(); @@ -473,15 +626,35 @@ public class UserAnswerTaskService( return null; } + // ============================================ + // 【获取一级栏目名称】 + // 通过 ParentId 关联查询一级栏目名称 + // 一级栏目:ParentId = 0 + // 二级栏目/文章:ParentId = 一级栏目Id,Type = Page + // ============================================ + string firstLevelCatalogName = string.Empty; + if (catalog.ParentId > 0) + { + var firstLevelCatalog = await catalogRepository.Context.Queryable() + .Where(c => c.Id == catalog.ParentId && !c.IsDeleted) + .FirstAsync(); + firstLevelCatalogName = firstLevelCatalog?.Name ?? string.Empty; + } + + // ============================================ // 2. 批量加载关联数据 + // ============================================ + // 该栏目下的所有页面 var allPages = await pageRepository.Context.Queryable() .Where(p => p.JournalCatalogId == catalogId && !p.IsDeleted) .ToListAsync(); + // 该期刊下所有任务(用于跨页题目关联查询) var allTasks = await taskRepository.Context.Queryable() .Where(t => t.JournalId == catalog.JournalId && !t.IsDeleted) .ToListAsync(); + // 该用户在该期刊下所有作答记录 var userAnswers = await answerRepository.Context.Queryable() .Where(a => a.JournalId == catalog.JournalId && a.UserId == userId && !a.IsDeleted) .ToListAsync(); @@ -492,7 +665,7 @@ public class UserAnswerTaskService( // 4. 该目录下的所有任务 var catalogTasks = allTasks.Where(t => pageIds.Contains(t.JournalPageId)).ToList(); - // 5. 任务名称列表 + // 5. 任务名称列表(去重) var taskNames = catalogTasks .Where(t => !string.IsNullOrEmpty(t.Task)) .Select(t => t.Task) @@ -511,7 +684,14 @@ public class UserAnswerTaskService( .Sum(a => a.AnswerSeconds); var answerMinutes = Math.Round(totalAnswerSeconds / 60.0, 2); - // 8. 判定状态(Status=0进行中,Status=1已完成) + // ============================================ + // 【跨页题目-状态判断】 + // 状态判定: + // - 未开始:无任何 GroupId 被作答 + // - 进行中:有 GroupId 被作答但未全部完成 + // - 已完成:所有 GroupId 都完成 + // 关键:必须该 GroupId 下所有任务都完成,才算该 GroupId 完成 + // ============================================ string status; if (catalogTasks.Count == 0) { @@ -519,11 +699,23 @@ public class UserAnswerTaskService( } else { - var completedCount = catalogTasks.Count(t => userCompletedTaskIds.Contains(t.Id)); - var answeredCount = catalogTasks.Count(t => userAnsweredTaskIds.Contains(t.Id)); - if (answeredCount == 0) + // 总 GroupId 数(跨页题目只算一个) + var totalGroupCount = catalogTasks.Select(t => t.GroupId).Distinct().Count(); + // 已完成 GroupId 数(该 GroupId 下所有任务都完成才算完成) + var completedGroupCount = catalogTasks + .GroupBy(t => t.GroupId) + .Where(g => g.All(t => userCompletedTaskIds.Contains(t.Id))) + .Count(); + // 已作答 GroupId 数(只要有一个任务作答就算已作答,包含进行中和已完成) + var answeredGroupCount = catalogTasks + .Where(t => userAnsweredTaskIds.Contains(t.Id)) + .Select(t => t.GroupId) + .Distinct() + .Count(); + + if (answeredGroupCount == 0) status = "未开始"; - else if (completedCount == catalogTasks.Count) + else if (completedGroupCount == totalGroupCount) status = "已完成"; else status = "进行中"; @@ -547,12 +739,17 @@ public class UserAnswerTaskService( var startPage = allPages.Count > 0 ? allPages.Min(p => p.PageNum) : 0; var endPage = allPages.Count > 0 ? allPages.Max(p => p.PageNum) : 0; - // 11. 计算总成长值和总积分 - var totalGrowthPoints = catalogTasks.Sum(t => t.GrowthPoint) ?? 0; - var totalPoints = catalogTasks.Sum(t => t.Points); + // 11. 计算总成长值和总积分(跨页题目按GroupId去重,只算一次) + var distinctCatalogTasks = catalogTasks + .GroupBy(t => t.GroupId) + .Select(g => g.First()) + .ToList(); + var totalGrowthPoints = distinctCatalogTasks.Sum(t => t.GrowthPoint) ?? 0; + var totalPoints = distinctCatalogTasks.Sum(t => t.Points); return new UserAnswerTaskListOutput { + FirstLevelCatalogName = firstLevelCatalogName, CatalogName = catalog.Name ?? string.Empty, StartPage = startPage, EndPage = endPage, @@ -566,8 +763,21 @@ public class UserAnswerTaskService( } /// - /// 获取跨页题目详情 + /// ============================================ + /// 方法思路:获取跨页题目详情 + /// 1. 判断是否跨页题目:journalPageTaskId != GroupId 则为跨页题目 + /// 2. 跨页题目处理:根据GroupId查询所有相关任务 + /// 3. 图片数组获取逻辑: + /// - 已答题:使用用户上传的PageAnswerUrl数组 + /// - 未答题:使用任务的TaskUrl数组 + /// 4. 栏目名称获取: + /// - 二级栏目名称:通过页面关联的JournalCatalogId查询 + /// - 一级栏目名称:通过二级栏目的ParentId查询 + /// ============================================ /// + /// 任务ID + /// 用户ID + /// 跨页题目详情 public async Task GetCrossPageTaskAsync(long journalPageTaskId, long userId) { // 1. 查询任务信息 @@ -580,14 +790,22 @@ public class UserAnswerTaskService( return null; } - // 2. 判断是否跨页题目 + // ============================================ + // 【跨页题目判定】 + // 判断标准:journalPageTaskId != task.GroupId 则为跨页题目 + // 跨页题目有多个任务,但共享同一个 GroupId + // ============================================ var isCrossPage = journalPageTaskId != task.GroupId; - // 3. 获取所有相关的任务(根据GroupId查询) + // ============================================ + // 【获取相关任务】 + // 跨页题目:根据 GroupId 查询所有相关任务 + // 非跨页题目:只返回当前任务 + // ============================================ List relatedTasks; if (isCrossPage) { - // 跨页题目:根据GroupId查询所有数据 + // 跨页题目:根据 GroupId 查询所有数据 relatedTasks = await taskRepository.Context.Queryable() .Where(t => t.GroupId == task.GroupId && t.JournalId == task.JournalId && !t.IsDeleted) .OrderBy(t => t.Id) @@ -595,28 +813,48 @@ public class UserAnswerTaskService( } else { + // 非跨页题目:只返回当前任务 relatedTasks = new List { task }; } - // 4. 获取栏目名称 + // ============================================ + // 【获取栏目名称】 + // 二级栏目名称:通过页面关联的 JournalCatalogId 查询 + // 一级栏目名称:通过二级栏目的 ParentId 查询 + // ============================================ var page = await pageRepository.Context.Queryable() .Where(p => p.Id == task.JournalPageId && !p.IsDeleted) .FirstAsync(); string catalogName = string.Empty; + string firstLevelCatalogName = string.Empty; if (page != null) { + // 获取二级栏目/文章名称 var catalog = await catalogRepository.Context.Queryable() .Where(c => c.Id == page.JournalCatalogId && !c.IsDeleted) .FirstAsync(); catalogName = catalog?.Name ?? string.Empty; + + // 获取一级栏目名称 + if (catalog != null && catalog.ParentId > 0) + { + var firstLevelCatalog = await catalogRepository.Context.Queryable() + .Where(c => c.Id == catalog.ParentId && !c.IsDeleted) + .FirstAsync(); + firstLevelCatalogName = firstLevelCatalog?.Name ?? string.Empty; + } } - // 5. 获取图片数组 + // ============================================ + // 【获取图片数组】 + // 已答题:使用用户上传的 PageAnswerUrl 数组 + // 未答题:使用任务的 TaskUrl 数组 + // ============================================ var images = new List(); if (isCrossPage) { - // 跨页题目:查询该GroupId下所有用户的答题记录 + // 跨页题目:查询该 GroupId 下所有用户的答题记录 var userAnswers = await answerRepository.Context.Queryable() .Where(a => a.JournalPageTaskGroupId == task.GroupId && a.UserId == userId && !a.IsDeleted) .OrderBy(a => a.JournalPageTaskId) @@ -624,7 +862,7 @@ public class UserAnswerTaskService( if (userAnswers.Any(a => !string.IsNullOrEmpty(a.PageAnswerUrl))) { - // 已答题,使用PageAnswerUrl数组 + // 已答题:使用 PageAnswerUrl 数组 images.AddRange(userAnswers.Where(a => !string.IsNullOrEmpty(a.PageAnswerUrl)).Select(a => a.PageAnswerUrl)); } else @@ -663,6 +901,7 @@ public class UserAnswerTaskService( return new CrossPageTaskOutput { + FirstLevelCatalogName = firstLevelCatalogName, CatalogName = catalogName, TaskName = task.Task ?? string.Empty, Images = images, @@ -671,55 +910,112 @@ public class UserAnswerTaskService( } /// - /// 领取任务积分 + /// ============================================ + /// 方法思路:领取任务积分 + /// 1. 根据 GroupId 查询任务信息,判断是否跨页题目 + /// - 普通题目:该 GroupId 下只有1个任务,且 Id == GroupId + /// - 跨页题目:该 GroupId 下有多个任务,或 Id != GroupId + /// 2. 通过 JournalPageTaskGroupId 查询用户答题记录 + /// 3. 跨页题目完成校验:必须该 GroupId 下所有任务都完成(Status=1) + /// 普通题目校验:只需要该任务完成(Status=1) + /// 4. 重复领取校验:通过 PointsRecord 表的 RelatedId + ChangeType 判断 + /// 5. 积分计算: + /// - 跨页题目:累计该 GroupId 下所有任务的积分 + /// - 普通题目:取当前任务的积分 + /// 6. 得分计算:从 JournalPageTaskUserAnswer 表获取 Score + /// ============================================ /// - public async Task ClaimTaskPointsAsync(long journalPageTaskId, long userId) + /// 任务分组Id(JournalPageTask.GroupId) + /// 用户ID + /// 领取积分结果 + public async Task ClaimTaskPointsAsync(long journalPageTaskGroupId, long userId) { - // 1. 查询任务信息 - var task = await taskRepository.Context.Queryable() - .Where(t => t.Id == journalPageTaskId && !t.IsDeleted) - .FirstAsync(); + // ============================================ + // 1. 根据 GroupId 查询所有相关任务 + // ============================================ + var relatedTasks = await taskRepository.Context.Queryable() + .Where(t => t.GroupId == journalPageTaskGroupId && !t.IsDeleted) + .ToListAsync(); - if (task == null) + if (relatedTasks == null || relatedTasks.Count == 0) { throw new BusinessException("任务不存在", 404); } + // ============================================ // 2. 判断是否跨页题目 - var isCrossPage = journalPageTaskId != task.GroupId; - - // 3. 查询用户答题记录(跨页题目按GroupId查询,普通题目按任务Id查询) - List userAnswers; - if (isCrossPage) + // 普通题目:该 GroupId 下只有1个任务,且该任务的 Id == GroupId + // 跨页题目:该 GroupId 下有多个任务,或任务的 Id != GroupId + // ============================================ + bool isCrossPage; + JournalPageTask mainTask; + if (relatedTasks.Count == 1 && relatedTasks[0].Id == journalPageTaskGroupId) { - userAnswers = await answerRepository.Context.Queryable() - .Where(a => a.JournalPageTaskGroupId == task.GroupId && a.UserId == userId && !a.IsDeleted) - .ToListAsync(); + // 普通题目:只有1个任务,且 Id == GroupId + isCrossPage = false; + mainTask = relatedTasks[0]; } else { - userAnswers = await answerRepository.Context.Queryable() - .Where(a => a.JournalPageTaskId == journalPageTaskId && a.UserId == userId && !a.IsDeleted) - .ToListAsync(); + // 跨页题目:有多个任务,或者 Id != GroupId + isCrossPage = true; + // 取 Id == GroupId 的任务作为主任务(跨页题目的第一条记录) + mainTask = relatedTasks.FirstOrDefault(t => t.Id == journalPageTaskGroupId) ?? relatedTasks[0]; } + // ============================================ + // 3. 查询用户答题记录(统一通过 JournalPageTaskGroupId 查询) + // ============================================ + var userAnswers = await answerRepository.Context.Queryable() + .Where(a => a.JournalPageTaskGroupId == journalPageTaskGroupId && a.UserId == userId && !a.IsDeleted) + .ToListAsync(); + if (userAnswers == null || userAnswers.Count == 0) { throw new BusinessException("请先完成答题再领取积分", 400); } - // 4. 计算积分(跨页题目需要累计所有相关任务的积分) + // ============================================ + // 4. 跨页题目完成校验 + // 跨页题目必须该 GroupId 下所有任务都完成(Status=1)才能领取积分 + // 普通题目只需要该任务完成(Status=1)即可领取 + // ============================================ + if (isCrossPage) + { + // 跨页题目:检查该 GroupId 下每个任务是否都有 Status=1 的答题记录 + foreach (var task in relatedTasks) + { + var completedAnswer = userAnswers.FirstOrDefault(a => a.JournalPageTaskId == task.Id && a.Status == (int)UserAnswerStatusEnum.Complete); + if (completedAnswer == null) + { + throw new BusinessException("跨页题目需要完成所有任务才能领取积分", 400); + } + } + } + else + { + // 普通题目:只需要 Status=1 即可 + if (userAnswers.FirstOrDefault()?.Status != (int)UserAnswerStatusEnum.Complete) + { + throw new BusinessException("请先完成答题再领取积分", 400); + } + } + + // ============================================ + // 5. 积分计算 + // 跨页题目:累计该 GroupId 下所有任务的积分 + // 普通题目:取当前任务的积分 + // ============================================ int totalPoints = 0; if (isCrossPage) { - var relatedTasks = await taskRepository.Context.Queryable() - .Where(t => t.GroupId == task.GroupId && t.JournalId == task.JournalId && !t.IsDeleted) - .ToListAsync(); + // 跨页题目:累计所有任务的积分 totalPoints = relatedTasks.Sum(t => t.Points); } else { - totalPoints = task.Points; + // 普通题目:取主任务的积分 + totalPoints = mainTask.Points; } if (totalPoints <= 0) @@ -727,11 +1023,33 @@ public class UserAnswerTaskService( throw new BusinessException("该任务无积分可领取", 400); } - // 5. 检查是否已领取过积分(跨页题目按GroupId作为RelatedId) - var relatedId = isCrossPage ? task.GroupId : journalPageTaskId; + // ============================================ + // 6. 得分计算 + // 从 JournalPageTaskUserAnswer 表获取 Score + // 【关键】Score 取 JournalPageTaskId == JournalPageTaskGroupId 的分数 + // 跨页题目:取主任务(Id == GroupId)的分数,不是累加所有任务的分数 + // 普通题目:取当前任务对应答题记录的分数 + // ============================================ + float totalScore = 0; + if (isCrossPage) + { + // 跨页题目:取 JournalPageTaskId == JournalPageTaskGroupId 的分数 + totalScore = userAnswers.FirstOrDefault(a => a.JournalPageTaskId == journalPageTaskGroupId)?.Score ?? 0; + } + else + { + // 普通题目:取当前任务对应答题记录的分数 + totalScore = userAnswers.FirstOrDefault()?.Score ?? 0; + } + + // ============================================ + // 7. 重复领取校验 + // 通过 PointsRecord 表的 RelatedId + ChangeType 判断是否已领取 + // 使用 GroupId 作为 RelatedId + // ============================================ var existingRecord = await answerRepository.Context.Queryable() .Where(r => r.UserId == userId && !r.IsDeleted) - .Where(r => r.ChangeType == PointsChangeTypeEnum.TaskReward.ToString() && r.RelatedId == relatedId) + .Where(r => r.ChangeType == PointsChangeTypeEnum.TaskReward.ToString() && r.RelatedId == journalPageTaskGroupId) .FirstAsync(); if (existingRecord != null) @@ -739,14 +1057,16 @@ public class UserAnswerTaskService( throw new BusinessException("积分已领取,请勿重复领取", 400); } - // 6. 调用积分服务增加积分 + // ============================================ + // 8. 调用积分服务添加积分记录 + // ============================================ var addPointsResult = await pointsService.AddPointsAsync(new AddPointsInput { UserId = userId, Amount = totalPoints, ChangeType = PointsChangeTypeEnum.TaskReward, - RelatedId = relatedId, - Description = $"完成任务奖励:{task.Task}", + RelatedId = journalPageTaskGroupId, + Description = $"完成任务奖励:{mainTask.Task}", OperatorName = "系统" }); @@ -756,13 +1076,34 @@ public class UserAnswerTaskService( PreviousBalance = addPointsResult.PreviousBalance, NewBalance = addPointsResult.NewBalance, AddedAmount = addPointsResult.AddedAmount, - TaskName = task.Task ?? string.Empty + TaskName = mainTask.Task ?? string.Empty, + Score = totalScore }; } /// - /// 批量领取任务积分(一键领取) + /// ============================================ + /// 方法思路:批量领取任务积分(一键领取) + /// 1. 遍历每个传入的 GroupId: + /// a. 根据 GroupId 查询所有相关任务,判断是否跨页题目 + /// - 普通题目:该 GroupId 下只有1个任务,且 Id == GroupId + /// - 跨页题目:该 GroupId 下有多个任务,或 Id != GroupId + /// b. 通过 JournalPageTaskGroupId 查询用户答题记录 + /// c. 跨页题目完成校验:必须该 GroupId 下所有任务都完成(Status=1) + /// d. 重复领取校验 + /// e. 计算积分、成长值、得分: + /// - 跨页题目:所有计算都只算一次(第一个任务的积分/成长值/得分) + /// - 普通题目:正常计算 + /// 2. 返回结果包含: + /// - 成功数量和失败数量 + /// - 成功详情列表(每个GroupId的名称、积分、成长值、得分) + /// - 失败详情列表(每个GroupId的错误原因) + /// - 总积分、总成长值、领取后余额 + /// ============================================ /// + /// 批量领取输入(GroupId列表) + /// 用户ID + /// 批量领取结果 public async Task BatchClaimPointsAsync(BatchClaimPointsInput input, long userId) { var result = new BatchClaimPointsOutput(); @@ -774,41 +1115,68 @@ public class UserAnswerTaskService( var groupIds = input.GroupIds.Distinct().ToList(); - // 1. 批量查询用户答题记录(按GroupId) - var userAnswers = await answerRepository.Context.Queryable() - .Where(a => groupIds.Contains(a.JournalPageTaskGroupId) && a.UserId == userId && !a.IsDeleted) - .ToListAsync(); - - // 已答题的GroupId集合 - var answeredGroupIds = userAnswers.Select(a => a.JournalPageTaskGroupId).Distinct().ToHashSet(); - - // 2. 批量查询已领取的积分记录 + // 1. 批量查询已领取的积分记录(用于重复领取校验) var claimedRecords = await answerRepository.Context.Queryable() .Where(r => groupIds.Contains((long)r.RelatedId!) && r.UserId == userId && !r.IsDeleted) .Where(r => r.ChangeType == PointsChangeTypeEnum.TaskReward.ToString()) .ToListAsync(); - var claimedGroupIds = claimedRecords.Select(r => r.RelatedId!.Value).Distinct().ToHashSet(); - // 3. 批量查询任务信息 - var allTasks = await taskRepository.Context.Queryable() - .Where(t => groupIds.Contains(t.GroupId) && !t.IsDeleted) - .ToListAsync(); - - // 按GroupId分组任务 - var tasksByGroup = allTasks.GroupBy(t => t.GroupId) - .ToDictionary(g => g.Key, g => g.ToList()); - - // 4. 逐个处理领取 + // 2. 逐个处理每个 GroupId int totalPoints = 0; + int totalGrowthPoints = 0; int currentBalance = 0; foreach (var groupId in groupIds) { try { - // 检查是否已答题 - if (!answeredGroupIds.Contains(groupId)) + // ============================================ + // 2.1 根据 GroupId 查询所有相关任务 + // ============================================ + var relatedTasks = await taskRepository.Context.Queryable() + .Where(t => t.GroupId == groupId && !t.IsDeleted) + .ToListAsync(); + + if (relatedTasks == null || relatedTasks.Count == 0) + { + result.FailItems.Add(new BatchClaimFailItem + { + GroupId = groupId, + Message = "任务不存在" + }); + continue; + } + + // ============================================ + // 2.2 判断是否跨页题目 + // 普通题目:该 GroupId 下只有1个任务,且 Id == GroupId + // 跨页题目:有多个任务,或 Id != GroupId + // ============================================ + bool isCrossPage; + JournalPageTask mainTask; + if (relatedTasks.Count == 1 && relatedTasks[0].Id == groupId) + { + // 普通题目 + isCrossPage = false; + mainTask = relatedTasks[0]; + } + else + { + // 跨页题目 + isCrossPage = true; + // 取 Id == GroupId 的任务作为主任务 + mainTask = relatedTasks.FirstOrDefault(t => t.Id == groupId) ?? relatedTasks[0]; + } + + // ============================================ + // 2.3 查询用户答题记录(统一通过 JournalPageTaskGroupId 查询) + // ============================================ + var userAnswers = await answerRepository.Context.Queryable() + .Where(a => a.JournalPageTaskGroupId == groupId && a.UserId == userId && !a.IsDeleted) + .ToListAsync(); + + if (userAnswers == null || userAnswers.Count == 0) { result.FailItems.Add(new BatchClaimFailItem { @@ -818,7 +1186,45 @@ public class UserAnswerTaskService( continue; } - // 检查是否已领取 + // ============================================ + // 2.4 跨页题目完成校验 + // 跨页题目必须该 GroupId 下所有任务都完成(Status=1) + // 普通题目只需要该任务完成(Status=1) + // ============================================ + if (isCrossPage) + { + // 跨页题目:检查该 GroupId 下每个任务是否都有 Status=1 的答题记录 + foreach (var task in relatedTasks) + { + var completedAnswer = userAnswers.FirstOrDefault(a => a.JournalPageTaskId == task.Id && a.Status == (int)UserAnswerStatusEnum.Complete); + if (completedAnswer == null) + { + result.FailItems.Add(new BatchClaimFailItem + { + GroupId = groupId, + Message = "跨页题目需要完成所有任务才能领取积分" + }); + continue; + } + } + } + else + { + // 普通题目:只需要 Status=1 即可 + if (userAnswers.FirstOrDefault()?.Status != (int)UserAnswerStatusEnum.Complete) + { + result.FailItems.Add(new BatchClaimFailItem + { + GroupId = groupId, + Message = "请先完成答题再领取积分" + }); + continue; + } + } + + // ============================================ + // 2.5 重复领取校验 + // ============================================ if (claimedGroupIds.Contains(groupId)) { result.FailItems.Add(new BatchClaimFailItem @@ -829,19 +1235,32 @@ public class UserAnswerTaskService( continue; } - // 获取该分组的任务 - if (!tasksByGroup.TryGetValue(groupId, out var groupTasks) || groupTasks.Count == 0) + // ============================================ + // 2.6 计算积分、成长值、得分 + // 【关键】跨页题目所有计算都只算一次(取 JournalPageTaskId == JournalPageTaskGroupId 的值) + // Score 取 JournalPageTaskId == JournalPageTaskGroupId 的分数,不是累加所有任务的分数 + // ============================================ + int points; + int growthPoints; + float score; + + if (isCrossPage) { - result.FailItems.Add(new BatchClaimFailItem - { - GroupId = groupId, - Message = "任务不存在" - }); - continue; + // 跨页题目:取 JournalPageTaskId == JournalPageTaskGroupId 的积分、成长值、得分 + // mainTask 已经是 Id == GroupId 的任务,所以 mainTask.Id == groupId + points = mainTask.Points; + growthPoints = mainTask.GrowthPoint ?? 0; + // 跨页题目:取 JournalPageTaskId == JournalPageTaskGroupId 的分数 + score = userAnswers.FirstOrDefault(a => a.JournalPageTaskId == groupId)?.Score ?? 0; + } + else + { + // 普通题目:正常计算 + points = mainTask.Points; + growthPoints = mainTask.GrowthPoint ?? 0; + score = userAnswers.FirstOrDefault()?.Score ?? 0; } - // 计算积分 - int points = groupTasks.Sum(t => t.Points); if (points <= 0) { result.FailItems.Add(new BatchClaimFailItem @@ -852,8 +1271,8 @@ public class UserAnswerTaskService( continue; } - // 获取任务名称(取第一个任务的名称) - var taskName = groupTasks.First().Task ?? string.Empty; + // 获取任务名称 + var taskName = mainTask.Task ?? string.Empty; // 调用积分服务增加积分 var addResult = await pointsService.AddPointsAsync(new AddPointsInput @@ -866,12 +1285,25 @@ public class UserAnswerTaskService( OperatorName = "系统" }); + // 累计总积分和总成长值 totalPoints += points; + totalGrowthPoints += growthPoints; currentBalance = addResult.NewBalance; result.SuccessCount++; + + // 添加成功详情到成功列表 + result.SuccessItems.Add(new BatchClaimSuccessItem + { + GroupId = groupId, + TaskName = taskName, + Points = points, + GrowthPoints = growthPoints, + Score = score + }); } catch (Exception ex) { + // 捕获异常并添加到失败列表,不中断其他任务的处理 result.FailItems.Add(new BatchClaimFailItem { GroupId = groupId, @@ -882,14 +1314,27 @@ public class UserAnswerTaskService( result.FailCount = result.FailItems.Count; result.TotalPoints = totalPoints; + result.TotalGrowthPoints = totalGrowthPoints; result.BalanceAfter = currentBalance; return result; } /// - /// 获取杂志列表 + /// ============================================ + /// 方法思路:获取杂志列表 + /// 1. 查询所有已归档的杂志(Status=Archive) + /// 2. 查询用户已绑定的杂志ID列表 + /// 3. 根据queryType过滤: + /// - "Bound":只返回已绑定的杂志 + /// - "Unbound":只返回未绑定的杂志 + /// - 其他:返回所有杂志 + /// 4. 返回每个杂志的ID、名称、标题、封面、是否已绑定 + /// ============================================ /// + /// 用户ID + /// 查询类型:Bound/Unbound/空(全部) + /// 杂志列表 public async Task> GetJournalListAsync(long userId, string? queryType) { var allJournals = await journalRepository.Queryable() @@ -924,14 +1369,24 @@ public class UserAnswerTaskService( } /// - /// 新增学生 + /// ============================================ + /// 方法思路:新增学生 + /// 1. 创建新的Users记录 + /// 2. WxUserId设置为当前登录人ID(wxUserId参数传入) + /// 3. 初始化积分和成长值为0 + /// 4. 用户类型设置为普通用户(Normal) + /// 5. 插入数据库并返回结果 + /// ============================================ /// - public async Task AddStudentAsync(AddStudentInput input, long userId) + /// 新增学生输入(姓名、头像URL) + /// 当前登录人ID(微信用户ID) + /// 是否插入成功 + public async Task AddStudentAsync(AddStudentInput input, long wxUserId) { // 创建新的学生记录 var student = new Users { - WxUserId = userId, + WxUserId = wxUserId, Name = input.Name, AvatarUrl = input.AvatarUrl, GrowthPoints = 0, diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/UserAnswerTaskController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/UserAnswerTaskController.cs index 6619c61..c6f5e96 100644 --- a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/UserAnswerTaskController.cs +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/UserAnswerTaskController.cs @@ -193,10 +193,10 @@ public class UserAnswerTaskController : WeChatBaseController /// /// 领取任务积分 /// - /// 任务Id + /// 任务分组Id(JournalPageTask.GroupId) /// 领取结果 - [HttpPost("claim-points/{journalPageTaskId:long}")] - public async Task> ClaimTaskPointsAsync([Required] long journalPageTaskId) + [HttpPost("claim-points/{journalPageTaskGroupId:long}")] + public async Task> ClaimTaskPointsAsync([Required] long journalPageTaskGroupId) { try { @@ -206,22 +206,22 @@ public class UserAnswerTaskController : WeChatBaseController return BaseResponse.Fail(ResultCode.DENY, "未获取到用户信息"); } - if (journalPageTaskId <= 0) + if (journalPageTaskGroupId <= 0) { - return BaseResponse.Fail(ResultCode.PARAM_ERROR, "任务Id不能为空"); + return BaseResponse.Fail(ResultCode.PARAM_ERROR, "任务分组Id不能为空"); } - var result = await _userAnswerTaskService.ClaimTaskPointsAsync(journalPageTaskId, userId); + var result = await _userAnswerTaskService.ClaimTaskPointsAsync(journalPageTaskGroupId, userId); return Success(result); } catch (BusinessException ex) { - _logger.LogWarning(ex, "领取任务积分业务异常,JournalPageTaskId: {JournalPageTaskId}, UserId: {UserId}", journalPageTaskId, GetCurrentUserId()); + _logger.LogWarning(ex, "领取任务积分业务异常,JournalPageTaskGroupId: {JournalPageTaskGroupId}, UserId: {UserId}", journalPageTaskGroupId, GetCurrentUserId()); return BaseResponse.Fail(ex.Message); } catch (Exception ex) { - _logger.LogError(ex, "领取任务积分失败,JournalPageTaskId: {JournalPageTaskId}, UserId: {UserId}", journalPageTaskId, GetCurrentUserId()); + _logger.LogError(ex, "领取任务积分失败,JournalPageTaskGroupId: {JournalPageTaskGroupId}, UserId: {UserId}", journalPageTaskGroupId, GetCurrentUserId()); return BaseResponse.Fail("领取任务积分失败,请稍后重试"); } } @@ -309,7 +309,7 @@ public class UserAnswerTaskController : WeChatBaseController return BaseResponse.Fail(ResultCode.DENY, "未获取到用户信息"); } - var result = await _userAnswerTaskService.AddStudentAsync(input, userId); + var result = await _userAnswerTaskService.AddStudentAsync(input, GetCurrentWxUserId().Value); return Success(result); } catch (BusinessException ex)