From 85a58123be7f67fcfdff148f825a5b148a1dc9e4 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: Wed, 1 Jul 2026 08:18:07 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E9=80=BB=E8=BE=91=E4=BF=AE?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../IUserAnswerTaskService.cs | 4 +- .../BatchClaimPointsOutput.cs | 29 +- .../ClaimTaskPointsOutput.cs | 35 +- .../Entity/JournalPageTaskUserAnswer.cs | 3 +- .../Enum/AnswerStatusEnum.cs | 40 + .../UserAnswerTaskService.cs | 1218 +++++++++-------- .../WeChat/UserAnswerTaskController.cs | 20 +- 7 files changed, 722 insertions(+), 627 deletions(-) create mode 100644 QYZH.InteractiveMagazine.Models/Enum/AnswerStatusEnum.cs diff --git a/QYZH.InteractiveMagazine.IService/IUserAnswerTaskService.cs b/QYZH.InteractiveMagazine.IService/IUserAnswerTaskService.cs index 4621c1a..d5e3222 100644 --- a/QYZH.InteractiveMagazine.IService/IUserAnswerTaskService.cs +++ b/QYZH.InteractiveMagazine.IService/IUserAnswerTaskService.cs @@ -42,10 +42,10 @@ public interface IUserAnswerTaskService : IBaseService /// 获取跨页题目详情 /// - /// 任务Id + /// 任务分组ID(JournalPageTaskGroupId) /// 用户Id /// 跨页题目详情 - Task GetCrossPageTaskAsync(long journalPageTaskId, long userId); + Task GetCrossPageTaskAsync(long groupId, long userId); /// /// 领取任务积分 diff --git a/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/BatchClaimPointsOutput.cs b/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/BatchClaimPointsOutput.cs index f325be9..b9a2f48 100644 --- a/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/BatchClaimPointsOutput.cs +++ b/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/BatchClaimPointsOutput.cs @@ -26,21 +26,6 @@ public class BatchClaimPointsOutput /// public int FailCount { get; set; } - /// - /// 累计获得积分 - /// - public int TotalPoints { get; set; } - - /// - /// 累计获得成长值 - /// - public int TotalGrowthPoints { get; set; } - - /// - /// 领取后积分余额 - /// - public int BalanceAfter { get; set; } - /// /// 成功领取详情列表 /// @@ -58,17 +43,7 @@ public class BatchClaimPointsOutput public class BatchClaimSuccessItem { /// - /// 任务分组Id - /// - public long GroupId { get; set; } - - /// - /// 任务名称 - /// - public string TaskName { get; set; } - - /// - /// 任务编号(从 JournalPageTask.No 字段按 - 分割取索引2) + /// 任务编号(从 JournalPageTask.No 字段按 - 分割取索引2,前面加"任务") /// public string TaskNo { get; set; } @@ -83,7 +58,7 @@ public class BatchClaimSuccessItem 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 1b8c131..86d326f 100644 --- a/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/ClaimTaskPointsOutput.cs +++ b/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/ClaimTaskPointsOutput.cs @@ -6,37 +6,22 @@ namespace QYZH.InteractiveMagazine.Models.Dto.UserAnswerTaskService; public class ClaimTaskPointsOutput { /// - /// 积分流水记录Id + /// 总成长值(主任务的 GrowthPoint) /// - public long RecordId { get; set; } + public int TotalGrowthPoints { get; set; } /// - /// 操作前积分余额 + /// 总积分(主任务的 Points) /// - public int PreviousBalance { get; set; } + public int TotalPoints { get; set; } /// - /// 操作后积分余额 - /// - public int NewBalance { get; set; } - - /// - /// 实际增加数量 - /// - public int AddedAmount { get; set; } - - /// - /// 任务名称 - /// - public string TaskName { get; set; } - - /// - /// 任务编号(从 JournalPageTask.No 字段按 - 分割取索引2) - /// - public string TaskNo { get; set; } - - /// - /// 题目得分(从 JournalPageTaskUserAnswer 表获取,JournalPageTaskId == GroupId 的 Score) + /// 得分(从 JournalPageTaskUserAnswer 表获取,JournalPageTaskId == GroupId 的 Score) /// public float Score { get; set; } + + /// + /// 任务编号(从 JournalPageTask.No 字段按 - 分割取索引2,前面加"任务") + /// + public string TaskNo { get; set; } } diff --git a/QYZH.InteractiveMagazine.Models/Entity/JournalPageTaskUserAnswer.cs b/QYZH.InteractiveMagazine.Models/Entity/JournalPageTaskUserAnswer.cs index dfb1dfd..737e93c 100644 --- a/QYZH.InteractiveMagazine.Models/Entity/JournalPageTaskUserAnswer.cs +++ b/QYZH.InteractiveMagazine.Models/Entity/JournalPageTaskUserAnswer.cs @@ -1,3 +1,4 @@ +using QYZH.InteractiveMagazine.Models.Enum; using SqlSugar; namespace QYZH.InteractiveMagazine.Models.Entity @@ -104,7 +105,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity /// Default: /// Nullable:False /// - public int AnswerStatus { get; set; } + public AnswerStatusEnum AnswerStatus { get; set; } /// /// Desc:作答开始时间 diff --git a/QYZH.InteractiveMagazine.Models/Enum/AnswerStatusEnum.cs b/QYZH.InteractiveMagazine.Models/Enum/AnswerStatusEnum.cs new file mode 100644 index 0000000..9da4f52 --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Enum/AnswerStatusEnum.cs @@ -0,0 +1,40 @@ +using System.ComponentModel; + +namespace QYZH.InteractiveMagazine.Models.Enum +{ + /// + /// 答题状态枚举: 0-未答题,1-已答题,2-正确,3-半对,4-错误 + /// + public enum AnswerStatusEnum + { + /// + /// 未答题 + /// + [Description("未答题")] + None = 0, + + /// + /// 已答题 + /// + [Description("已答题")] + Answer = 1, + + /// + /// 正确 + /// + [Description("正确")] + Correct = 2, + + /// + /// 半对 + /// + [Description("半对")] + HalfRight = 3, + + /// + /// 错误 + /// + [Description("错误")] + Error = 4 + } +} diff --git a/QYZH.InteractiveMagazine.Service/UserAnswerTaskService.cs b/QYZH.InteractiveMagazine.Service/UserAnswerTaskService.cs index 0fdd4c0..094c69a 100644 --- a/QYZH.InteractiveMagazine.Service/UserAnswerTaskService.cs +++ b/QYZH.InteractiveMagazine.Service/UserAnswerTaskService.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Logging; +using QYZH.InteractiveMagazine.Common.Helpers; using QYZH.InteractiveMagazine.IService; using QYZH.InteractiveMagazine.Models.Common; using QYZH.InteractiveMagazine.Models.Dto; @@ -22,27 +23,32 @@ public class UserAnswerTaskService( BaseRepository userJournalRepository, BaseRepository usersRepository, BaseRepository checkInRecordRepository, + BaseRepository userPetRepository, IPointsService pointsService, + IPetService petService, ILogger logger) : BaseRepository, IUserAnswerTaskService { /// /// ============================================ /// 方法思路: - /// 1. 批量预加载策略:为避免N+1查询问题,先按期刊维度批量加载所有关联数据 - /// 2. 期刊来源: - /// - 用户绑定的杂志(UserJournal表) - /// - 用户有作答记录的杂志 - /// 合并去重后返回 - /// 3. 跨页题目处理核心: + /// 1. 首先验证 UserJournal 表,获取用户绑定的杂志列表 + /// - 如果用户没有绑定任何杂志,直接返回空列表 + /// 2. 批量预加载策略:为避免N+1查询问题,先按期刊维度批量加载所有关联数据 + /// - 期刊基本信息 + /// - 一级栏目(ParentId == 0) + /// - 所有任务(按期刊ID) + /// - 所有页面(按期刊ID) + /// 3. 查询用户在这些期刊下的作答记录 + /// 4. 跨页题目处理核心: /// - 跨页题目定义:JournalPageTask.Id != GroupId(同一个GroupId下有多个任务) /// - 进度计算:按GroupId去重,已完成条件是该GroupId下所有任务都完成(Status=1) - /// - 积分计算:按GroupId去重,每个GroupId只计算一次积分(取第一个任务的积分) - /// 4. 状态判定逻辑: + /// - 积分计算:按GroupId去重,每个GroupId只计算一次积分(取主任务的积分) + /// 5. 状态判定逻辑: /// - 未开始:无任何作答记录 /// - 进行中:有作答记录但未全部完成 /// - 已完成:所有GroupId都完成 - /// 5. 用户未作答任何任务时: + /// 6. 用户未作答任何任务时: /// - 仍返回绑定杂志的进度信息 /// - 进度、成长值、积分为0 /// - 连续打卡、剩余篇数正常计算 @@ -52,90 +58,96 @@ public class UserAnswerTaskService( /// 用户所有期刊的学习进度列表 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 都完成 - // ============================================ + #region 1. 验证 UserJournal 表,获取用户绑定的杂志 - #region 1. 查询用户作答记录 - // 1. 查询用户的所有作答记录 - var userAnswers = await answerRepository.Context.Queryable() - .Where(a => a.UserId == userId && !a.IsDeleted) - .ToListAsync(); - #endregion - - #region 2. 收集期刊ID(绑定+作答去重) // ============================================ - // 2. 收集所有需要展示的期刊ID - // - 用户绑定的杂志(UserJournal表) - // - 用户有作答记录的杂志 - // 合并去重 + // 第一步:先验证 UserJournal 表数据 + // 获取用户绑定的所有杂志 ID + // 如果没有绑定的杂志,直接返回空列表,不进行后续查询 // ============================================ var boundJournalIds = await userJournalRepository.Context.Queryable() - .Where(uj => uj.UserId == userId && !uj.IsDeleted) + .Where(uj => uj.UserId == userId) .Select(uj => uj.JournalId) .ToListAsync(); - var answeredJournalIds = userAnswers.Select(a => a.JournalId).Distinct().ToList(); - - var journalIds = boundJournalIds - .Concat(answeredJournalIds) - .Distinct() - .ToList(); - - if (journalIds.Count == 0) + if (boundJournalIds == null || boundJournalIds.Count == 0) { return new List(); } + + var journalIds = boundJournalIds.Distinct().ToList(); + #endregion - #region 3. 批量预加载关联数据 - // 3. 批量预加载关联实体(按期刊维度全量加载,避免N+1查询) + #region 2. 批量预加载关联数据 + + // ============================================ + // 第二步:批量预加载关联实体(按期刊维度全量加载,避免N+1查询) + // - 期刊基本信息 + // - 一级栏目(ParentId == 0 表示一级目录) + // - 所有任务 + // - 所有页面(用于计算剩余篇数) + // ============================================ var journals = await journalRepository.Context.Queryable() - .Where(j => journalIds.Contains(j.Id) && !j.IsDeleted) + .Where(j => journalIds.Contains(j.Id)) .ToListAsync(); - // 加载这些期刊的一级目录(ParentId == 0 表示一级目录) var allCatalogs = await catalogRepository.Context.Queryable() - .Where(c => journalIds.Contains(c.JournalId) && c.ParentId == 0 && !c.IsDeleted) + .Where(c => journalIds.Contains(c.JournalId) && c.ParentId == 0) .ToListAsync(); - // 加载这些期刊的所有任务 var allTasks = await taskRepository.Context.Queryable() - .Where(t => journalIds.Contains(t.JournalId) && !t.IsDeleted) + .Where(t => journalIds.Contains(t.JournalId)) .ToListAsync(); - // 加载这些期刊的所有页面(用于计算剩余篇数) var allPages = await pageRepository.Context.Queryable() - .Where(p => journalIds.Contains(p.JournalId) && !p.IsDeleted) + .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. 查询连续打卡天数 - // 4. 查询用户连续打卡天数 + var consecutiveDays = await CalculateConsecutiveDaysAsync(userId); + #endregion #region 5. 提取用户已作答/已完成任务ID集合 - // 5. 提取用户已作答的任务ID集合(包含进行中和已完成) + + // 用户已作答的任务ID集合(包含进行中和已完成) var userAnsweredTaskIds = userAnswers.Select(a => a.JournalPageTaskId).ToHashSet(); var userAnsweredPageIds = userAnswers.Select(a => a.JournalPageId).ToHashSet(); - // 用户已完成的任务ID集合(Status=1表示已完成) - var userCompletedTaskIds = userAnswers.Where(a => a.Status == (int)UserAnswerStatusEnum.Complete).Select(a => a.JournalPageTaskId).ToHashSet(); + // 用户已完成的任务ID集合 + // 【已完成判断逻辑】 + // Status: 0=未批阅,1=已批阅 + // AnswerStatus: 0=未答题,1=已答题,2=正确,3=半对,4=错误 + // 必须同时满足: AnswerStatus >= 1(Correct/HalfRight/Error),作业才算完成,看情况再加Status == 1(已批阅) + var userCompletedTaskIds = userAnswers + .Where(a => a.AnswerStatus >= AnswerStatusEnum.Answer) + .Select(a => a.JournalPageTaskId) + .ToHashSet(); + #endregion #region 6. 按期刊分组聚合并返回结果 - // 6. 按期刊分组聚合 + + // ============================================ + // 第四步:按期刊分组聚合 + // 遍历每个绑定的杂志,计算进度、积分、成长值等 + // ============================================ var result = new List(); foreach (var journal in journals) @@ -147,49 +159,58 @@ public class UserAnswerTaskService( var catalogCount = allCatalogs.Count(c => c.JournalId == journal.Id); // 【跨页题目-进度计算】总任务数:按 GroupId 去重,跨页题目只算一个 - var totalTaskCount = journalTasks.Select(t => t.GroupId).Distinct().Count(); + var totalGroupIds = journalTasks.Select(t => t.GroupId).Distinct().ToList(); + var totalTaskCount = totalGroupIds.Count; - // 【跨页题目-已完成判断】已完成任务数:按 GroupId 去重 + // 【跨页题目-已完成判断】已完成的 GroupId 列表 // 关键:必须该 GroupId 下所有任务都完成,才算该 GroupId 完成 - var completedTaskCount = journalTasks - .GroupBy(t => t.GroupId) - .Where(g => g.All(t => userCompletedTaskIds.Contains(t.Id))) - .Count(); - - // 【跨页题目-积分计算】获取已完成的 GroupId 列表 var completedGroupIds = journalTasks .GroupBy(t => t.GroupId) .Where(g => g.All(t => userCompletedTaskIds.Contains(t.Id))) .Select(g => g.Key) .ToList(); - // 【跨页题目-积分去重】每个已完成的 GroupId 只取第一个任务的积分 - // 原因:同一个跨页题目的多个任务只应该计算一次积分 - var distinctCompletedTasks = journalTasks - .Where(t => completedGroupIds.Contains(t.GroupId)) - .GroupBy(t => t.GroupId) - .Select(g => g.First()) + var completedTaskCount = completedGroupIds.Count; + + // ============================================ + // 【积分和成长值计算】 + // 1. totalGrowthPoints:从已完成作答记录中,按 JournalPageTaskGroupId 查询主任务(Id=GroupId)的 GrowthPoint + // 2. totalPoints:统计该期刊下已完成任务的 Points 总数 + // ============================================ + // 该期刊下用户已完成的作答记录 + // 【已完成判断逻辑】 + // Status: 0=未批阅,1=已批阅 + // AnswerStatus: 0=未答题,1=已答题,2=正确,3=半对,4=错误 + // 必须同时满足: AnswerStatus >= 1(Correct/HalfRight/Error),作业才算完成,看情况再加:Status == 1(已批阅)且 + var completedAnswers = userAnswers + .Where(a => a.JournalId == journal.Id && a.AnswerStatus >= AnswerStatusEnum.Answer) .ToList(); - // 累计已完成的跨页题目的总成长值和总积分 - var totalGrowthPoints = distinctCompletedTasks.Sum(t => t.GrowthPoint ?? 0); - var totalPoints = distinctCompletedTasks.Sum(t => t.Points); + // 按 JournalPageTaskGroupId 去重,获取已完成的主任务 ID 列表 + var completedGroupIdsSet = completedAnswers.Select(a => a.JournalPageTaskGroupId).Distinct().ToList(); - // 【跨页题目-进度百分比计算】 - 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(); + // 从主任务表查询这些 GroupId 对应的 GrowthPoint 和 Points + var mainTasks = allTasks + .Where(t => completedGroupIdsSet.Contains(t.GroupId) && t.Id == t.GroupId) + .ToList(); - var progress = totalGroupIds > 0 - ? Math.Round((double)completedGroupIdCount / totalGroupIds * 100, 2) + // 累计成长值(从主任务的 GrowthPoint 累加) + var totalGrowthPoints = mainTasks.Sum(t => t.GrowthPoint ?? 0); + + // 累计积分(从已完成任务的 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 journalPages = allPages.Where(p => p.JournalId == journal.Id).ToList(); - var answeredPageCount = journalPages.Count(p => userAnsweredPageIds.Contains(p.Id)); - var remainingPages = journalPages.Count - answeredPageCount; + // 剩余篇数:有题目的页面总数 - 已作答的有题目页面数 + var remainingPages = totalPagesWithTasks - answeredPagesWithTasks; result.Add(new JournalProgressOutput { @@ -231,7 +252,7 @@ public class UserAnswerTaskService( // 查询昨天是否有打卡记录 var yesterdayRecord = await checkInRecordRepository.Context.Queryable() - .Where(r => r.UserId == userId && !r.IsDeleted && r.CheckInDate.Date == yesterday) + .Where(r => r.UserId == userId && r.CheckInDate.Date == yesterday) .FirstAsync(); // 如果昨天没有打卡记录,连续天数为0(连续打卡中断) @@ -242,7 +263,7 @@ public class UserAnswerTaskService( // 昨天有打卡记录,检查今天是否已打卡 var todayRecord = await checkInRecordRepository.Context.Queryable() - .Where(r => r.UserId == userId && !r.IsDeleted && r.CheckInDate.Date == today) + .Where(r => r.UserId == userId && r.CheckInDate.Date == today) .FirstAsync(); // 今天已打卡:返回今天的连续天数;今天未打卡:返回昨天的连续天数 @@ -274,10 +295,27 @@ public class UserAnswerTaskService( /// 目录列表(按一级目录分组) 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 && !j.IsDeleted) + .Where(j => j.Id == journalId) .FirstAsync(); if (journal == null) @@ -289,19 +327,19 @@ public class UserAnswerTaskService( #region 2. 批量加载关联数据 // 2. 批量加载关联数据 var allCatalogs = await catalogRepository.Context.Queryable() - .Where(c => c.JournalId == journalId && !c.IsDeleted) + .Where(c => c.JournalId == journalId) .ToListAsync(); var allPages = await pageRepository.Context.Queryable() - .Where(p => p.JournalId == journalId && !p.IsDeleted) + .Where(p => p.JournalId == journalId) .ToListAsync(); var allTasks = await taskRepository.Context.Queryable() - .Where(t => t.JournalId == journalId && !t.IsDeleted) + .Where(t => t.JournalId == journalId) .ToListAsync(); var userAnswers = await answerRepository.Context.Queryable() - .Where(a => a.JournalId == journalId && a.UserId == userId && !a.IsDeleted) + .Where(a => a.JournalId == journalId && a.UserId == userId) .ToListAsync(); #endregion @@ -309,107 +347,94 @@ public class UserAnswerTaskService( // 3. 用户已作答的任务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(); + // 用户已完成的任务ID集合 + // 【已完成判断逻辑】 + // AnswerStatus: 0=未答题,1=已答题,2=正确,3=半对,4=错误 + // 只要 AnswerStatus >= 1(已答题)说明作业已完成 + var userCompletedTaskIds = userAnswers + .Where(a => a.AnswerStatus >= AnswerStatusEnum.Answer) + .Select(a => a.JournalPageTaskId) + .ToHashSet(); #endregion #region 4. 按一级目录分组计算 // 4. 按一级目录分组 - var firstLevelCatalogs = allCatalogs.Where(c => c.ParentId == 0 && c.Type == JournalCatalogTypeEnum.Unit).OrderBy(c => c.Sort).ToList(); + var firstLevelCatalogs = allCatalogs.Where(c => c.ParentId == 0).OrderBy(c => c.Sort).ToList(); var result = new List(); foreach (var firstCatalog in firstLevelCatalogs) { - // 该一级目录下的 Type=Page 子目录(即文章) - var pageCatalogs = allCatalogs - .Where(c => c.ParentId == firstCatalog.Id && c.Type == JournalCatalogTypeEnum.Page) - .OrderBy(c => c.Sort) - .ToList(); - - var pageCatalogIds = pageCatalogs.Select(c => c.Id).ToHashSet(); - - // 获取所有文章下的页面ID - var allArticlePageIds = allPages - .Where(p => pageCatalogIds.Contains(p.JournalCatalogId)) - .Select(p => p.Id) - .ToHashSet(); - - // 该一级目录下的二级单元目录(Type=Unit 的子目录) - var secondLevelCatalogs = allCatalogs - .Where(c => c.ParentId == firstCatalog.Id && c.Type == JournalCatalogTypeEnum.Unit) - .OrderBy(c => c.Sort) - .ToList(); - // ============================================ // 文章篇数计算逻辑: - // - 有二级栏目:累加所有二级栏目下的文章数 + 一级栏目下直接的文章数 - // - 没有二级栏目:计算一级栏目下直接的文章数(Type=Page) + // - 篇数 = 有题目的页面数(有 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) { - // 有二级单元:按二级单元分组 - // 先统计一级栏目下直接的文章数(Type=Page,ParentId=一级栏目Id) - articleCount = pageCatalogs.Count; - + // 有二级单元:按二级单元分组统计 foreach (var secondCatalog in secondLevelCatalogs) { - // 二级单元下的 Type=Page 子目录 - var subPageCatalogs = allCatalogs - .Where(c => c.ParentId == secondCatalog.Id && c.Type == JournalCatalogTypeEnum.Page) + // 二级目录下的所有页面(JournalPage.JournalCatalogId = 二级目录ID) + var secondLevelPages = allPages + .Where(p => p.JournalCatalogId == secondCatalog.Id) .ToList(); - // 累加该二级栏目下的文章数 - articleCount += subPageCatalogs.Count; + var secondLevelPageIds = secondLevelPages.Select(p => p.Id).ToHashSet(); - var subPageCatalogIds = subPageCatalogs.Select(c => c.Id).ToHashSet(); - - // 二级单元下的所有页面 - var secondLevelPageIds = allPages - .Where(p => subPageCatalogIds.Contains(p.JournalCatalogId)) - .Select(p => p.Id) + // 二级目录下有题目的页面(有 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(); - // 判定状态(Status=0进行中,Status=1已完成,跨页题目按GroupId去重) - // 跨页题目必须所有任务都完成,才算该 GroupId 已完成 + // 判定状态(基于 AnswerStatus >= 1 判断已完成,按页面维度统计) + // 【已完成判断逻辑】 + // AnswerStatus: 0=未答题,1=已答题,2=正确,3=半对,4=错误 + // 只要 AnswerStatus >= 1(已答题)说明该页面已完成 string status; - if (secondLevelTasks.Count == 0) - { - status = "未开始"; - } - else - { - // 总 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(); - // 状态判定逻辑: - // - answeredGroupCount == 0:没有任何 GroupId 被作答 → 未开始 - // - completedGroupCount == totalGroupCount:所有 GroupId 都完成 → 已完成 - // - 其他:有 GroupId 被作答但未全部完成 → 进行中 - if (answeredGroupCount == 0) - status = "未开始"; - else if (completedGroupCount == totalGroupCount) - status = "已完成"; - else - status = "进行中"; - } + // 总页面数(有题目的页面) + var totalPageCount = pagesWithTasks.Count; + // 已完成页面数:该页面有答题记录且 AnswerStatus >= 1 + var completedPageCount = pagesWithTasks + .Count(pageId => userAnswers.Any(a => a.JournalPageId == pageId && a.AnswerStatus >= AnswerStatusEnum.Answer)); + // 已作答页面数:该页面有答题记录即可 + 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 @@ -431,14 +456,20 @@ public class UserAnswerTaskService( } else { - // 没有二级单元:只统计一级栏目下直接的文章数,Children为空集合 - articleCount = pageCatalogs.Count; + // 没有二级单元:统计一级栏目下直接有题目的页面数 + 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保持为空集合,不添加任何子项 } - // 计算该一级目录的总成长值和总积分(基于所有文章页面的任务) - var firstLevelTasks = allTasks.Where(t => allArticlePageIds.Contains(t.JournalPageId)).ToList(); - result.Add(new CatalogListOutput { CatalogId = firstCatalog.Id, @@ -476,7 +507,7 @@ public class UserAnswerTaskService( #region 1. 查询一级目录 // 1. 查询一级目录 var firstCatalog = await catalogRepository.Context.Queryable() - .Where(c => c.Id == catalogId && !c.IsDeleted) + .Where(c => c.Id == catalogId) .FirstAsync(); if (firstCatalog == null) @@ -485,115 +516,146 @@ public class UserAnswerTaskService( } #endregion - #region 2. 批量加载关联数据 + #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 && !c.IsDeleted) + .Where(c => c.JournalId == firstCatalog.JournalId) .ToListAsync(); var allPages = await pageRepository.Context.Queryable() - .Where(p => p.JournalId == firstCatalog.JournalId && !p.IsDeleted) + .Where(p => p.JournalId == firstCatalog.JournalId) .ToListAsync(); var allTasks = await taskRepository.Context.Queryable() - .Where(t => t.JournalId == firstCatalog.JournalId && !t.IsDeleted) + .Where(t => t.JournalId == firstCatalog.JournalId) .ToListAsync(); var userAnswers = await answerRepository.Context.Queryable() - .Where(a => a.JournalId == firstCatalog.JournalId && a.UserId == userId && !a.IsDeleted) + .Where(a => a.JournalId == firstCatalog.JournalId && a.UserId == userId) .ToListAsync(); #endregion - #region 3. 用户已作答/已完成任务ID集合 - // 3. 用户已作答的任务ID集合(包含进行中和已完成) + #region 4. 用户已作答/已完成任务ID集合 + // 用户已作答的任务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(); + // 用户已完成的任务ID集合 + // 【已完成判断逻辑】 + // AnswerStatus: 0=未答题,1=已答题,2=正确,3=半对,4=错误 + // 只要 AnswerStatus >= 1(已答题)说明作业已完成 + var userCompletedTaskIds = userAnswers + .Where(a => a.AnswerStatus >= AnswerStatusEnum.Answer) + .Select(a => a.JournalPageTaskId) + .ToHashSet(); #endregion - #region 4. 获取一级栏目下所有Type=Page子目录 - // 4. 获取该一级目录下所有 Type=Page 的子目录(即文章/页面目录) - var pageCatalogs = allCatalogs - .Where(c => c.ParentId == firstCatalog.Id && c.Type == JournalCatalogTypeEnum.Page) + #region 5. 获取一级栏目下所有二级目录 + // 获取该一级目录下所有二级目录(不区分Type) + var secondLevelCatalogs = allCatalogs + .Where(c => c.ParentId == firstCatalog.Id) .OrderBy(c => c.Sort) .ToList(); - // 5. 文章篇数 = Type=Page 的子目录数量 - var articleCount = pageCatalogs.Count; - #endregion - - #region 5. 总任务数与子目录详情 - // 6. 获取所有文章目录下的页面 - var pageCatalogIds = pageCatalogs.Select(c => c.Id).ToHashSet(); - var allLevelPages = allPages.Where(p => pageCatalogIds.Contains(p.JournalCatalogId)).ToList(); - var allLevelPageIds = allLevelPages.Select(p => p.Id).ToHashSet(); - - // 7. 总任务数(按 GroupId 去重,跨页题目只算一个) - var totalTaskCount = allTasks - .Where(t => allLevelPageIds.Contains(t.JournalPageId)) - .Select(t => t.GroupId) - .Distinct() - .Count(); - - // 8. 构建子目录详情列表 + // 文章篇数 = 有题目的页面数(有 JournalPageTask 的页面) + int articleCount = 0; + // 总任务数(按 GroupId 去重) + int totalTaskCount = 0; + // 构建子目录详情列表 var subCatalogs = new List(); - foreach (var pageCatalog in pageCatalogs) + foreach (var secondCatalog in secondLevelCatalogs) { - // 该文章下的所有页面 - var articlePages = allPages - .Where(p => p.JournalCatalogId == pageCatalog.Id) + // 二级目录下的所有页面 + var secondLevelPages = allPages + .Where(p => p.JournalCatalogId == secondCatalog.Id) .OrderBy(p => p.PageNum) .ToList(); - var articlePageIds = articlePages.Select(p => p.Id).ToHashSet(); + 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 => articlePageIds.Contains(t.JournalPageId)).ToList(); - // 总 GroupId 数(按 GroupId 去重,跨页题目只算一个) - var totalGroupCount = articleTasks.Select(t => t.GroupId).Distinct().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 = articlePages.Count > 0 ? articlePages.Min(p => p.PageNum) : 0; - var endPage = articlePages.Count > 0 ? articlePages.Max(p => p.PageNum) : 0; + 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去重) - // 跨页题目必须所有任务都完成,才算该 GroupId 已完成 + // 状态格式:1/2 进行中、2/2 已完成、0/2 未开始(按 GroupId 去重统计) + // 只要 AnswerStatus >= 1(已答题)说明该 GroupId 已完成 string status; if (totalGroupCount == 0) { - status = $"0/0 未开始"; + status = "0/0 未开始"; } else { - // 已完成 GroupId 数(该 GroupId 下所有任务都完成才算完成) + // 已完成 GroupId 数(该 GroupId 下所有任务都 AnswerStatus >= 1 才算完成) var completedGroupCount = articleTasks .GroupBy(t => t.GroupId) .Where(g => g.All(t => userCompletedTaskIds.Contains(t.Id))) .Count(); - // 已作答 GroupId 数(只要有一个任务作答就算已作答,包含进行中和已完成) + // 已作答 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 = $"{completedGroupCount}/{totalGroupCount} 已完成"; + status = $"{totalGroupCount}/{totalGroupCount} 已完成"; else status = $"{completedGroupCount}/{totalGroupCount} 进行中"; } subCatalogs.Add(new SubCatalogDetailOutput { - CatalogId = pageCatalog.Id, - SecondLevelCatalogName = pageCatalog.Name ?? string.Empty, + CatalogId = secondCatalog.Id, + SecondLevelCatalogName = secondCatalog.Name ?? string.Empty, StartPage = startPage, EndPage = endPage, - TaskCount = totalGroupCount, + TaskCount = taskCount, Status = status }); } @@ -633,7 +695,7 @@ public class UserAnswerTaskService( // 1. 查询目录(二级栏目) var catalog = await catalogRepository.Context.Queryable() - .Where(c => c.Id == catalogId && !c.IsDeleted) + .Where(c => c.Id == catalogId) .FirstAsync(); if (catalog == null) @@ -643,7 +705,24 @@ public class UserAnswerTaskService( #endregion - #region 2. 获取一级栏目名称 + #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. 获取一级栏目名称 // ============================================ // 【获取一级栏目名称】 @@ -653,41 +732,41 @@ public class UserAnswerTaskService( if (catalog.ParentId > 0) { var firstLevelCatalog = await catalogRepository.Context.Queryable() - .Where(c => c.Id == catalog.ParentId && !c.IsDeleted) + .Where(c => c.Id == catalog.ParentId) .FirstAsync(); firstLevelCatalogName = firstLevelCatalog?.Name ?? string.Empty; } #endregion - #region 3. 批量加载关联数据 + #region 4. 批量加载关联数据 // ============================================ - // 2. 批量加载关联数据 + // 批量加载关联数据 // ============================================ // 该栏目下的所有页面 var allPages = await pageRepository.Context.Queryable() - .Where(p => p.JournalCatalogId == catalogId && !p.IsDeleted) + .Where(p => p.JournalCatalogId == catalogId) .ToListAsync(); // 该期刊下所有任务(用于跨页题目关联查询) var allTasks = await taskRepository.Context.Queryable() - .Where(t => t.JournalId == catalog.JournalId && !t.IsDeleted) + .Where(t => t.JournalId == catalog.JournalId) .ToListAsync(); // 该用户在该期刊下所有作答记录 var userAnswers = await answerRepository.Context.Queryable() - .Where(a => a.JournalId == catalog.JournalId && a.UserId == userId && !a.IsDeleted) + .Where(a => a.JournalId == catalog.JournalId && a.UserId == userId) .ToListAsync(); #endregion - #region 4. 获取该目录下的页面ID及任务 + #region 5. 获取该目录下的页面ID及任务 - // 3. 获取该目录下的所有页面ID + // 获取该目录下的所有页面ID var pageIds = allPages.Select(p => p.Id).ToHashSet(); - // 4. 该目录下的所有任务(仅用于筛选哪些GroupId属于当前栏目) + // 该目录下的所有任务(仅用于筛选哪些GroupId属于当前栏目) var catalogTasks = allTasks.Where(t => pageIds.Contains(t.JournalPageId)).ToList(); // 【跨页题目-关键】 @@ -696,19 +775,19 @@ public class UserAnswerTaskService( // 否则无法正确判断跨页题目是否全部完成 var catalogGroupIds = catalogTasks.Select(t => t.GroupId).ToHashSet(); - // 5. 用户已作答的任务ID集合(包含进行中和已完成) + // 用户已作答的任务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(); - // 6. 计算页码范围 + // 计算页码范围 var startPage = allPages.Count > 0 ? allPages.Min(p => p.PageNum) : 0; var endPage = allPages.Count > 0 ? allPages.Max(p => p.PageNum) : 0; #endregion - #region 5. 按GroupId分组生成任务列表 + #region 6. 按GroupId分组生成任务列表 // ============================================ // 【跨页题目-按GroupId分组生成任务列表】 @@ -771,7 +850,7 @@ public class UserAnswerTaskService( #endregion - #region 6. 返回结果 + #region 7. 返回结果 return new UserAnswerTaskListOutput { @@ -806,14 +885,16 @@ public class UserAnswerTaskService( /// - 且未领取过积分 /// ============================================ /// - /// 任务分组ID(GroupId) + /// 任务分组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 && !t.IsDeleted) + .Where(t => t.GroupId == groupId) .OrderBy(t => t.Id) .ToListAsync(); @@ -822,6 +903,22 @@ public class UserAnswerTaskService( 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. 跨页题目判定 // ============================================ @@ -859,7 +956,7 @@ public class UserAnswerTaskService( // 一级栏目名称:通过二级栏目的 ParentId 查询 // ============================================ var page = await pageRepository.Context.Queryable() - .Where(p => p.Id == baseTask.JournalPageId && !p.IsDeleted) + .Where(p => p.Id == baseTask.JournalPageId) .FirstAsync(); string catalogName = string.Empty; @@ -868,7 +965,7 @@ public class UserAnswerTaskService( { // 获取二级栏目 var catalog = await catalogRepository.Context.Queryable() - .Where(c => c.Id == page.JournalCatalogId && !c.IsDeleted) + .Where(c => c.Id == page.JournalCatalogId) .FirstAsync(); catalogName = catalog?.Name ?? string.Empty; @@ -876,7 +973,7 @@ public class UserAnswerTaskService( if (catalog != null && catalog.ParentId > 0) { var firstLevelCatalog = await catalogRepository.Context.Queryable() - .Where(c => c.Id == catalog.ParentId && !c.IsDeleted) + .Where(c => c.Id == catalog.ParentId) .FirstAsync(); firstLevelCatalogName = firstLevelCatalog?.Name ?? string.Empty; } @@ -904,7 +1001,7 @@ public class UserAnswerTaskService( { // 跨页题目:查询该 GroupId 下所有用户的答题记录 userAnswers = await answerRepository.Context.Queryable() - .Where(a => a.JournalPageTaskGroupId == groupId && a.UserId == userId && !a.IsDeleted) + .Where(a => a.JournalPageTaskGroupId == groupId && a.UserId == userId) .OrderBy(a => a.JournalPageTaskId) .ToListAsync(); @@ -917,7 +1014,7 @@ public class UserAnswerTaskService( // 查询相关任务对应的页面信息(用于排序) var pageIds = relatedTasks.Select(t => t.JournalPageId).Distinct().ToList(); var pageInfoList = await pageRepository.Context.Queryable() - .Where(p => pageIds.Contains(p.Id) && !p.IsDeleted) + .Where(p => pageIds.Contains(p.Id)) .ToListAsync(); var pageInfoDict = pageInfoList.ToDictionary(p => p.Id, p => p.PageNum); @@ -931,13 +1028,13 @@ public class UserAnswerTaskService( string imageUrl; if (userAnswer != null && !string.IsNullOrEmpty(userAnswer.PageAnswerUrl)) { - // 已答题:使用 PageAnswerUrl - imageUrl = userAnswer.PageAnswerUrl; + // 已答题:使用 PageAnswerUrl(加上域名) + imageUrl = DomainHelper.OssFullUrl(userAnswer.PageAnswerUrl); } else if (!string.IsNullOrEmpty(relatedTask.TaskUrl)) { - // 未答题或无 PageAnswerUrl:使用任务的 TaskUrl 补齐 - imageUrl = relatedTask.TaskUrl; + // 未答题或无 PageAnswerUrl:使用任务的 TaskUrl 补齐(加上域名) + imageUrl = DomainHelper.OssFullUrl(relatedTask.TaskUrl); } else { @@ -955,7 +1052,7 @@ public class UserAnswerTaskService( { // 非跨页题目:查询单条答题记录 var userAnswer = await answerRepository.Context.Queryable() - .Where(a => a.JournalPageTaskId == mainTask.Id && a.UserId == userId && !a.IsDeleted) + .Where(a => a.JournalPageTaskId == mainTask.Id && a.UserId == userId) .FirstAsync(); // 赋值给 userAnswers(用于后续领取积分判断) @@ -963,8 +1060,8 @@ public class UserAnswerTaskService( if (userAnswer != null && !string.IsNullOrEmpty(userAnswer.PageAnswerUrl)) { - // 已答题,使用 PageAnswerUrl - images.Add(userAnswer.PageAnswerUrl); + // 已答题,使用 PageAnswerUrl(加上域名) + images.Add(DomainHelper.OssFullUrl(userAnswer.PageAnswerUrl)); } // 未答题不返回任何图片,images 保持空集合 } @@ -982,26 +1079,26 @@ public class UserAnswerTaskService( var pageAnswerDotUrls = new List(); if (isCrossPage) { - // 跨页题目:遍历相关任务,收集有答题记录的 PageAnswerDotUrl + // 跨页题目:遍历相关任务,收集有答题记录的 PageAnswerDotUrl(加上域名) foreach (var relatedTask in relatedTasks) { var userAnswer = userAnswers.FirstOrDefault(a => a.JournalPageTaskId == relatedTask.Id); if (userAnswer != null && !string.IsNullOrEmpty(userAnswer.PageAnswerDotUrl)) { - pageAnswerDotUrls.Add(userAnswer.PageAnswerDotUrl); + pageAnswerDotUrls.Add(DomainHelper.OssFullUrl(userAnswer.PageAnswerDotUrl)); } } } else { - // 非跨页题目:取当前任务答题记录的 PageAnswerDotUrl + // 非跨页题目:取当前任务答题记录的 PageAnswerDotUrl(加上域名) // 仅在已答题且 PageAnswerDotUrl 有值时返回 if (userAnswers != null && userAnswers.Count > 0) { var userAnswer = userAnswers.FirstOrDefault(); if (userAnswer != null && !string.IsNullOrEmpty(userAnswer.PageAnswerDotUrl)) { - pageAnswerDotUrls.Add(userAnswer.PageAnswerDotUrl); + pageAnswerDotUrls.Add(DomainHelper.OssFullUrl(userAnswer.PageAnswerDotUrl)); } } } @@ -1012,35 +1109,36 @@ public class UserAnswerTaskService( // ============================================ // 【是否可领取积分判断】 - // 条件:任务已完成(Status=1)且未领取过积分 - // 跨页题目:所有任务都完成才能领取 - // 普通题目:当前任务完成即可领取 + // 条件:任务已完成且未领取过积分 + // - Status = 1(已批阅) + // - AnswerStatus >= 1(已答题:正确/半对/错误) + // 跨页题目:所有任务都满足条件才能领取 + // 普通题目:当前任务满足条件即可领取 + // 【关键】子题目入口(mainTask == null)无法领取积分 // ============================================ bool canClaimPoints = false; - // 查询是否已领取过积分 - var existingRecord = await answerRepository.Context.Queryable() - .Where(r => r.UserId == userId && !r.IsDeleted) - .Where(r => r.ChangeType == PointsChangeTypeEnum.TaskReward.ToString() && r.RelatedId == groupId) - .AnyAsync(); - - if (!existingRecord) + // 【关键】如果主任务不存在(子题目入口),无法领取积分 + if (mainTask == null) { - // 未领取过积分,检查任务是否完成 - if (isCrossPage) + // 子题目无法单独领取积分,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) { - // 跨页题目:检查该 GroupId 下所有任务是否都完成(Status=1) - var allCompleted = relatedTasks.All(t => - userAnswers.Any(a => a.JournalPageTaskId == t.Id && a.Status == (int)UserAnswerStatusEnum.Complete)); - canClaimPoints = allCompleted; - } - else - { - // 普通题目:检查当前任务是否完成(Status=1) - var userAnswer = await answerRepository.Context.Queryable() - .Where(a => a.JournalPageTaskId == mainTask.Id && a.UserId == userId && !a.IsDeleted) - .FirstAsync(); - canClaimPoints = userAnswer?.Status == (int)UserAnswerStatusEnum.Complete; + // 未领取过积分,检查任务是否完成 + // 条件:Status=1 且 AnswerStatus>=1 + canClaimPoints = relatedTasks.All(t => + userAnswers.Any(a => a.JournalPageTaskId == t.Id + && a.Status == (int)UserAnswerStatusEnum.Complete + && a.AnswerStatus >= AnswerStatusEnum.Answer)); } } @@ -1052,15 +1150,30 @@ public class UserAnswerTaskService( // 【获取任务名称】 // 跨页题目:取主任务(Id == GroupId)的 Task 字段 // 普通题目:取当前任务的 Task 字段 + // 子题目入口:查询主任务的 Task 字段 // ============================================ string taskName; - if (isCrossPage) + if (mainTask != null) { - taskName = mainTask?.Task ?? string.Empty; + // 主任务入口,直接使用 mainTask + taskName = mainTask.Task ?? string.Empty; } else { - taskName = mainTask?.Task ?? string.Empty; + // 子题目入口:查询主任务(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 @@ -1085,66 +1198,41 @@ public class UserAnswerTaskService( /// ============================================ /// 方法思路:领取任务积分 /// 1. 根据 GroupId 查询任务信息,判断是否跨页题目 - /// - 普通题目:该 GroupId 下只有1个任务,且 Id == GroupId - /// - 跨页题目:该 GroupId 下有多个任务,或 Id != GroupId - /// 2. 通过 groupId 查询用户答题记录 - /// 3. 跨页题目完成校验:必须该 GroupId 下所有任务都完成(Status=1) - /// 普通题目校验:只需要该任务完成(Status=1) + /// 2. 查询用户答题记录,校验主任务(JournalPageTaskId == GroupId)才能领取 + /// 3. 完成校验:Status=1 且 AnswerStatus>=1,跨页题目需所有任务都完成 /// 4. 重复领取校验:通过 PointsRecord 表的 RelatedId + ChangeType 判断 - /// 5. 积分计算: - /// - 跨页题目:累计该 GroupId 下所有任务的积分 - /// - 普通题目:取当前任务的积分 - /// 6. 得分计算:从 JournalPageTaskUserAnswer 表获取 Score + /// 5. 计算积分、成长值、得分(均取主任务数据) + /// 6. 调用积分服务增加积分 /// ============================================ /// /// 任务分组Id(JournalPageTask.GroupId) /// 用户ID - /// 领取积分结果 + /// 领取积分结果(总成长值、总积分、得分、任务编号) public async Task ClaimTaskPointsAsync(long groupId, long userId) { - #region 1. 根据GroupId查询所有相关任务 + #region 1. 查询任务与答题记录 // ============================================ - // 1. 根据 GroupId 查询所有相关任务 + // 1. 查询任务与答题记录 // ============================================ var relatedTasks = await taskRepository.Context.Queryable() - .Where(t => t.GroupId == groupId && !t.IsDeleted) + .Where(t => t.GroupId == groupId) .ToListAsync(); if (relatedTasks == null || relatedTasks.Count == 0) { throw new BusinessException("任务不存在", ResultCode.NOT_FOUND); } - #endregion - #region 2. 判断是否跨页题目 - // ============================================ - // 2. 判断是否跨页题目 - // 普通题目:该 GroupId 下只有1个任务,且该任务的 Id == GroupId - // 跨页题目:该 GroupId 下有多个任务,或任务的 Id != GroupId - // ============================================ - bool isCrossPage; - JournalPageTask mainTask; - if (relatedTasks.Count == 1 && relatedTasks[0].Id == groupId) + // 主任务:Id == GroupId + var mainTask = relatedTasks.FirstOrDefault(t => t.Id == groupId); + if (mainTask == null) { - // 普通题目:只有1个任务,且 Id == GroupId - isCrossPage = false; - mainTask = relatedTasks[0]; + throw new BusinessException("子题目无法单独领取积分,请从主任务入口领取", ResultCode.BAD_REQUEST); } - else - { - // 跨页题目:有多个任务,或者 Id != GroupId - isCrossPage = true; - // 取 Id == GroupId 的任务作为主任务(跨页题目的第一条记录) - mainTask = relatedTasks.FirstOrDefault(t => t.Id == groupId) ?? relatedTasks[0]; - } - #endregion - #region 3. 查询用户答题记录 - // ============================================ - // 3. 查询用户答题记录(统一通过 JournalPageTaskGroupId 查询) - // ============================================ + // 查询用户答题记录 var userAnswers = await answerRepository.Context.Queryable() - .Where(a => a.JournalPageTaskGroupId == groupId && a.UserId == userId && !a.IsDeleted) + .Where(a => a.JournalPageTaskGroupId == groupId && a.UserId == userId) .ToListAsync(); if (userAnswers == null || userAnswers.Count == 0) @@ -1153,88 +1241,40 @@ public class UserAnswerTaskService( } #endregion - #region 4. 跨页题目完成校验 + #region 2. 完成校验 // ============================================ - // 4. 跨页题目完成校验 - // 跨页题目必须该 GroupId 下所有任务都完成(Status=1)才能领取积分 - // 普通题目只需要该任务完成(Status=1)即可领取 + // 2. 完成校验 + // 主任务校验 + 跨页题目所有任务校验 + // 完成条件:Status=1 且 AnswerStatus>=1 // ============================================ - if (isCrossPage) + // 校验主任务 + var mainTaskAnswer = userAnswers.FirstOrDefault(a => a.JournalPageTaskId == groupId); + if (mainTaskAnswer == null || mainTaskAnswer.Status != (int)UserAnswerStatusEnum.Complete || mainTaskAnswer.AnswerStatus < AnswerStatusEnum.Answer) + { + throw new BusinessException("请先完成答题再领取积分", ResultCode.BAD_REQUEST); + } + + // 跨页题目:校验所有任务 + if (relatedTasks.Count > 1) { - // 跨页题目:检查该 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) + var answer = userAnswers.FirstOrDefault(a => a.JournalPageTaskId == task.Id); + if (answer == null || answer.Status != (int)UserAnswerStatusEnum.Complete || answer.AnswerStatus < AnswerStatusEnum.Answer) { throw new BusinessException("跨页题目需要完成所有任务才能领取积分", ResultCode.BAD_REQUEST); } } } - else - { - // 普通题目:只需要 Status=1 即可 - if (userAnswers.FirstOrDefault()?.Status != (int)UserAnswerStatusEnum.Complete) - { - throw new BusinessException("请先完成答题再领取积分", ResultCode.BAD_REQUEST); - } - } #endregion - #region 5. 积分计算 + #region 3. 重复领取校验与积分计算 // ============================================ - // 5. 积分计算 - // 【关键】跨页题目所有计算都只算一次(取 JournalPageTaskId == JournalPageTaskGroupId 的值) - // 跨页题目:只取主任务(Id == GroupId)的积分,不是累加所有任务的积分 - // 普通题目:取主任务的积分 - // ============================================ - int totalPoints = 0; - if (isCrossPage) - { - // 跨页题目:只取主任务(Id == GroupId)的积分 - totalPoints = mainTask.Points; - } - else - { - // 普通题目:取主任务的积分 - totalPoints = mainTask.Points; - } - - if (totalPoints <= 0) - { - throw new BusinessException("该任务无积分可领取", ResultCode.BAD_REQUEST); - } - #endregion - - #region 6. 得分计算 - // ============================================ - // 6. 得分计算 - // 从 JournalPageTaskUserAnswer 表获取 Score - // 【关键】Score 取 JournalPageTaskId == JournalPageTaskGroupId 的分数 - // 跨页题目:取主任务(Id == GroupId)的分数,不是累加所有任务的分数 - // 普通题目:取当前任务对应答题记录的分数 - // ============================================ - float totalScore = 0; - if (isCrossPage) - { - // 跨页题目:取 JournalPageTaskId == JournalPageTaskGroupId 的分数 - totalScore = userAnswers.FirstOrDefault(a => a.JournalPageTaskId == groupId)?.Score ?? 0; - } - else - { - // 普通题目:取当前任务对应答题记录的分数 - totalScore = userAnswers.FirstOrDefault()?.Score ?? 0; - } - #endregion - - #region 7. 重复领取校验 - // ============================================ - // 7. 重复领取校验 - // 通过 PointsRecord 表的 RelatedId + ChangeType 判断是否已领取 - // 使用 GroupId 作为 RelatedId + // 3. 重复领取校验与积分计算 // ============================================ + // 重复领取校验 var existingRecord = await answerRepository.Context.Queryable() - .Where(r => r.UserId == userId && !r.IsDeleted) + .Where(r => r.UserId == userId) .Where(r => r.ChangeType == PointsChangeTypeEnum.TaskReward.ToString() && r.RelatedId == groupId) .FirstAsync(); @@ -1242,65 +1282,113 @@ public class UserAnswerTaskService( { throw new BusinessException("积分已领取,请勿重复领取", ResultCode.BAD_REQUEST); } + + // 积分、成长值、得分(均取主任务数据) + int totalPoints = mainTask.Points; + int totalGrowthPoints = mainTask.GrowthPoint ?? 0; + float score = mainTaskAnswer.Score; + + if (totalPoints <= 0) + { + throw new BusinessException("该任务无积分可领取", ResultCode.BAD_REQUEST); + } #endregion - #region 8. 添加积分记录并返回结果 + #region 4. 事务执行领取操作 // ============================================ - // 8. 调用积分服务添加积分记录 + // 4. 事务执行领取操作 + // - 手动更新 Users.Points(积分累加) + // - 插入积分流水记录 + // - 喂养宠物(如有活跃宠物,更新宠物成长值) // ============================================ - var addPointsResult = await pointsService.AddPointsAsync(new AddPointsInput + // 查询用户信息(获取当前积分) + var user = await usersRepository.GetByIdAsync(userId); + if (user == null) { - UserId = userId, - Amount = totalPoints, - ChangeType = PointsChangeTypeEnum.TaskReward, - RelatedId = groupId, - Description = $"完成任务奖励:{mainTask.Task}", - OperatorName = "系统" - }); - - // ============================================ - // 【任务编号计算】 - // 从 JournalPageTask.No 字段按 - 分割,取索引为 2 的部分 - // No 最多出现索引为 3(即最多4段,0-3) - // 例如:"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]; - } + throw new BusinessException("用户不存在", ResultCode.NOT_FOUND); } - return new ClaimTaskPointsOutput + // 查询用户活跃宠物 + 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 () => { - RecordId = addPointsResult.RecordId, - PreviousBalance = addPointsResult.PreviousBalance, - NewBalance = addPointsResult.NewBalance, - AddedAmount = addPointsResult.AddedAmount, - TaskName = mainTask.Task ?? string.Empty, - TaskNo = !string.IsNullOrWhiteSpace(taskNo) ? "任务:"+ taskNo : string.Empty, - Score = totalScore - }; + // 4a. 手动更新 Users.Points(积分累加) + var newPointsBalance = user.Points + totalPoints; + await answerRepository.Context.Updateable() + .SetColumns(u => u.Points == newPointsBalance) + .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 = $"完成任务奖励:{mainTask.Task}", + 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: - /// a. 根据 GroupId 查询所有相关任务,判断是否跨页题目 - /// - 普通题目:该 GroupId 下只有1个任务,且 Id == GroupId - /// - 跨页题目:该 GroupId 下有多个任务,或 Id != GroupId - /// b. 通过 JournalPageTaskGroupId 查询用户答题记录 - /// c. 跨页题目完成校验:必须该 GroupId 下所有任务都完成(Status=1) - /// d. 重复领取校验 - /// e. 计算积分、成长值、得分: - /// - 跨页题目:所有计算都只算一次(第一个任务的积分/成长值/得分) - /// - 普通题目:正常计算 - /// 2. 返回结果包含: + /// 1. 遍历每个传入的 GroupId,做校验,筛选出可领取的任务 + /// 2. 在事务中执行所有写操作: + /// - 更新 Users 表 Points(积分服务内部处理) + /// - 更新 Users 表 GrowthPoints(累加) + /// - 喂养宠物(如有活跃宠物) + /// 3. 返回结果包含: /// - 成功数量和失败数量 /// - 成功详情列表(每个GroupId的名称、积分、成长值、得分) /// - 失败详情列表(每个GroupId的错误原因) @@ -1324,30 +1412,25 @@ public class UserAnswerTaskService( #endregion #region 2. 批量查询已领取的积分记录 - // 1. 批量查询已领取的积分记录(用于重复领取校验) + // 批量查询已领取的积分记录(用于重复领取校验) var claimedRecords = await answerRepository.Context.Queryable() - .Where(r => groupIds.Contains((long)r.RelatedId!) && r.UserId == userId && !r.IsDeleted) + .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 - // 2. 逐个处理每个 GroupId - int totalPoints = 0; - int totalGrowthPoints = 0; - int currentBalance = 0; + #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 查询所有相关任务 - // ============================================ - // 2.1 根据 GroupId 查询所有相关任务 - // ============================================ var relatedTasks = await taskRepository.Context.Queryable() - .Where(t => t.GroupId == groupId && !t.IsDeleted) + .Where(t => t.GroupId == groupId) .ToListAsync(); if (relatedTasks == null || relatedTasks.Count == 0) @@ -1361,35 +1444,24 @@ public class UserAnswerTaskService( } #endregion - #region 3.2 判断是否跨页题目 - // ============================================ - // 2.2 判断是否跨页题目 - // 普通题目:该 GroupId 下只有1个任务,且 Id == GroupId - // 跨页题目:有多个任务,或 Id != GroupId - // ============================================ - bool isCrossPage; - JournalPageTask mainTask; - if (relatedTasks.Count == 1 && relatedTasks[0].Id == groupId) + #region 3.2 判断是否跨页题目并找主任务 + bool isCrossPage = relatedTasks.Count > 1; + // 主任务:Id == GroupId + var mainTask = relatedTasks.FirstOrDefault(t => t.Id == groupId); + if (mainTask == null) { - // 普通题目 - isCrossPage = false; - mainTask = relatedTasks[0]; - } - else - { - // 跨页题目 - isCrossPage = true; - // 取 Id == GroupId 的任务作为主任务 - mainTask = relatedTasks.FirstOrDefault(t => t.Id == groupId) ?? relatedTasks[0]; + result.FailItems.Add(new BatchClaimFailItem + { + GroupId = groupId, + Message = "子题目无法单独领取积分,请从主任务入口领取" + }); + continue; } #endregion #region 3.3 查询用户答题记录 - // ============================================ - // 2.3 查询用户答题记录(统一通过 JournalPageTaskGroupId 查询) - // ============================================ var userAnswers = await answerRepository.Context.Queryable() - .Where(a => a.JournalPageTaskGroupId == groupId && a.UserId == userId && !a.IsDeleted) + .Where(a => a.JournalPageTaskGroupId == groupId && a.UserId == userId) .ToListAsync(); if (userAnswers == null || userAnswers.Count == 0) @@ -1403,54 +1475,45 @@ public class UserAnswerTaskService( } #endregion - #region 3.4 跨页题目完成校验 - // ============================================ - // 2.4 跨页题目完成校验 - // 跨页题目必须该 GroupId 下所有任务都完成(Status=1) - // 普通题目只需要该任务完成(Status=1) - // ============================================ - bool isCompleted = true; // 完成状态标志 + #region 3.4 完成校验 + // 校验主任务完成状态:Status=1 且 AnswerStatus>=1 + var mainTaskAnswer = userAnswers.FirstOrDefault(a => a.JournalPageTaskId == groupId); + if (mainTaskAnswer == null || mainTaskAnswer.Status != (int)UserAnswerStatusEnum.Complete || mainTaskAnswer.AnswerStatus < AnswerStatusEnum.Answer) + { + result.FailItems.Add(new BatchClaimFailItem + { + GroupId = groupId, + Message = "请先完成答题再领取积分" + }); + continue; + } + + // 跨页题目:校验所有任务 if (isCrossPage) { - // 跨页题目:检查该 GroupId 下每个任务是否都有 Status=1 的答题记录 + bool allCompleted = true; foreach (var task in relatedTasks) { - var completedAnswer = userAnswers.FirstOrDefault(a => a.JournalPageTaskId == task.Id && a.Status == (int)UserAnswerStatusEnum.Complete); - if (completedAnswer == null) + var answer = userAnswers.FirstOrDefault(a => a.JournalPageTaskId == task.Id); + if (answer == null || answer.Status != (int)UserAnswerStatusEnum.Complete || answer.AnswerStatus < AnswerStatusEnum.Answer) { - isCompleted = false; - break; // 发现未完成的任务,立即退出循环 + allCompleted = false; + break; } } - if (!isCompleted) + if (!allCompleted) { result.FailItems.Add(new BatchClaimFailItem { GroupId = groupId, Message = "跨页题目需要完成所有任务才能领取积分" }); - continue; // 跳出外层循环,处理下一个 groupId - } - } - else - { - // 普通题目:只需要 Status=1 即可 - if (userAnswers.FirstOrDefault()?.Status != (int)UserAnswerStatusEnum.Complete) - { - result.FailItems.Add(new BatchClaimFailItem - { - GroupId = groupId, - Message = "请先完成答题再领取积分" - }); continue; } } #endregion #region 3.5 重复领取校验 - // ============================================ - // 2.5 重复领取校验 - // ============================================ if (claimedGroupIds.Contains(groupId)) { result.FailItems.Add(new BatchClaimFailItem @@ -1463,31 +1526,9 @@ public class UserAnswerTaskService( #endregion #region 3.6 计算积分、成长值、得分 - // ============================================ - // 2.6 计算积分、成长值、得分 - // 【关键】跨页题目所有计算都只算一次(取 JournalPageTaskId == JournalPageTaskGroupId 的值) - // Score 取 JournalPageTaskId == JournalPageTaskGroupId 的分数,不是累加所有任务的分数 - // ============================================ - int points; - int growthPoints; - float score; - - if (isCrossPage) - { - // 跨页题目:取 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 = mainTask.Points; + int growthPoints = mainTask.GrowthPoint ?? 0; + float score = mainTaskAnswer.Score; if (points <= 0) { @@ -1499,55 +1540,22 @@ public class UserAnswerTaskService( continue; } - // 获取任务名称 var taskName = mainTask.Task ?? string.Empty; - // ============================================ - // 【任务编号计算】 - // 从 JournalPageTask.No 字段按 - 分割,取索引为 2 的部分 - // No 最多出现索引为 3(即最多4段,0-3) - // 例如:"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]; + taskNo = $"任务{parts[2]}"; } } #endregion - #region 3.7 调用积分服务并记录结果 - // 调用积分服务增加积分 - var addResult = await pointsService.AddPointsAsync(new AddPointsInput - { - UserId = userId, - Amount = points, - ChangeType = PointsChangeTypeEnum.TaskReward, - RelatedId = groupId, - Description = $"完成任务奖励:{taskName}", - OperatorName = "系统" - }); - - // 累计总积分和总成长值 - totalPoints += points; - totalGrowthPoints += growthPoints; - currentBalance = addResult.NewBalance; - result.SuccessCount++; - - // 添加成功详情到成功列表 - result.SuccessItems.Add(new BatchClaimSuccessItem - { - GroupId = groupId, - TaskName = taskName, - TaskNo = !string.IsNullOrWhiteSpace(taskNo) ? "任务:" + taskNo : string.Empty, - Points = points, - GrowthPoints = growthPoints, - Score = score - }); - #endregion + // 添加到可领取列表 + claimableTasks.Add((groupId, points, growthPoints, score, taskName, taskNo)); } catch (Exception ex) { @@ -1561,11 +1569,95 @@ public class UserAnswerTaskService( } #endregion - #region 4. 组装返回结果 + #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; + await answerRepository.Context.Updateable() + .SetColumns(u => u.Points == newPointsBalance) + .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 = $"完成任务奖励:{task.TaskName}", + 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; - result.TotalPoints = totalPoints; - result.TotalGrowthPoints = totalGrowthPoints; - result.BalanceAfter = currentBalance; return result; #endregion diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/UserAnswerTaskController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/UserAnswerTaskController.cs index a558df6..6290909 100644 --- a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/UserAnswerTaskController.cs +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/UserAnswerTaskController.cs @@ -26,6 +26,8 @@ public class UserAnswerTaskController : WeChatBaseController _logger = logger; } + private const long ConstUserId = 821752253853766; + /// /// 获取当前用户的期刊学习进度列表 /// @@ -34,7 +36,7 @@ public class UserAnswerTaskController : WeChatBaseController public async Task>> GetProgressAsync() { try - { + { var userId = GetCurrentUserId(); if (userId == 0) { @@ -49,7 +51,7 @@ public class UserAnswerTaskController : WeChatBaseController _logger.LogError(ex, "获取学习进度失败,UserId: {UserId}", GetCurrentUserId()); return BaseResponse>.Fail("获取学习进度失败,请稍后重试"); } - } + } /// @@ -158,10 +160,10 @@ public class UserAnswerTaskController : WeChatBaseController /// /// 获取跨页题目详情 /// - /// 任务Id + /// 任务分组ID /// 跨页题目详情 - [HttpGet("cross-page-task/{journalPageTaskId:long}")] - public async Task> GetCrossPageTaskAsync([Required(ErrorMessage ="参数错误")] long journalPageTaskId) + [HttpGet("cross-page-task/{groupId:long}")] + public async Task> GetCrossPageTaskAsync([Required(ErrorMessage ="参数错误")] long groupId) { try { @@ -171,12 +173,12 @@ public class UserAnswerTaskController : WeChatBaseController return BaseResponse.Fail(ResultCode.DENY, "未获取到用户信息"); } - if (journalPageTaskId <= 0) + if (groupId <= 0) { return BaseResponse.Fail(ResultCode.PARAM_ERROR, "任务Id不能为空"); } - var result = await _userAnswerTaskService.GetCrossPageTaskAsync(journalPageTaskId, userId); + var result = await _userAnswerTaskService.GetCrossPageTaskAsync(groupId, userId); if (result == null) { return BaseResponse.Fail("未找到该任务"); @@ -186,7 +188,7 @@ public class UserAnswerTaskController : WeChatBaseController } catch (Exception ex) { - _logger.LogError(ex, "获取跨页题目失败,JournalPageTaskId: {JournalPageTaskId}, UserId: {UserId}", journalPageTaskId, GetCurrentUserId()); + _logger.LogError(ex, "获取跨页题目失败,JournalPageTaskGroupId: {groupId}, UserId: {UserId}", groupId, GetCurrentUserId()); return BaseResponse.Fail("获取跨页题目失败,请稍后重试"); } } @@ -201,7 +203,7 @@ public class UserAnswerTaskController : WeChatBaseController { try { - var userId = GetCurrentUserId(); + var userId = ConstUserId;//GetCurrentUserId(); if (userId == 0) { return BaseResponse.Fail(ResultCode.DENY, "未获取到用户信息");