From 00fa7a21f679c0aa209d2f8e96a8983157ba467e 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, 24 Jun 2026 16:32:59 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=BE=AE=E4=BF=A1=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E4=BB=A5=E5=8F=8A=E4=B8=80=E4=BA=9B=E9=97=AE=E5=A4=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../IJournalService.cs | 2 +- .../IUserAnswerTaskService.cs | 81 ++ .../UserAnswerTaskService/AddStudentInput.cs | 18 + .../UserAnswerTaskService/AddStudentOutput.cs | 43 + .../BatchClaimPointsOutput.cs | 59 ++ .../CatalogListOutput.cs | 32 + .../CatalogOverviewOutput.cs | 32 + .../CatalogTaskDetailOutput.cs | 37 + .../ClaimTaskPointsOutput.cs | 32 + .../CrossPageTaskOutput.cs | 27 + .../JournalListOutput.cs | 32 + .../JournalProgressOutput.cs | 57 ++ .../SecondCatalogOutput.cs | 32 + .../SubCatalogDetailOutput.cs | 37 + .../UserAnswerTaskService/TaskStatusOutput.cs | 27 + .../UserAnswerTaskListOutput.cs | 52 + .../Entity/JournalPageTaskUserAnswer.cs | 346 ++++--- .../Entity/UserMedal.cs | 2 +- .../Enum/UserAnswerStatusEnum.cs | 21 + .../JournalService.cs | 2 +- .../UserAnswerTaskService.cs | 946 ++++++++++++++++++ .../WeChat/UserAnswerTaskController.cs | 326 ++++++ ...y-22eabfbc-9d6d-4d02-a009-12a7eb9886c2.xml | 16 + 23 files changed, 2079 insertions(+), 180 deletions(-) create mode 100644 QYZH.InteractiveMagazine.IService/IUserAnswerTaskService.cs create mode 100644 QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/AddStudentInput.cs create mode 100644 QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/AddStudentOutput.cs create mode 100644 QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/BatchClaimPointsOutput.cs create mode 100644 QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/CatalogListOutput.cs create mode 100644 QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/CatalogOverviewOutput.cs create mode 100644 QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/CatalogTaskDetailOutput.cs create mode 100644 QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/ClaimTaskPointsOutput.cs create mode 100644 QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/CrossPageTaskOutput.cs create mode 100644 QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/JournalListOutput.cs create mode 100644 QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/JournalProgressOutput.cs create mode 100644 QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/SecondCatalogOutput.cs create mode 100644 QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/SubCatalogDetailOutput.cs create mode 100644 QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/TaskStatusOutput.cs create mode 100644 QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/UserAnswerTaskListOutput.cs create mode 100644 QYZH.InteractiveMagazine.Models/Enum/UserAnswerStatusEnum.cs create mode 100644 QYZH.InteractiveMagazine.Service/UserAnswerTaskService.cs create mode 100644 QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/UserAnswerTaskController.cs create mode 100644 QYZH.InteractiveMagazine.WebApi/DataProtection/key-22eabfbc-9d6d-4d02-a009-12a7eb9886c2.xml diff --git a/QYZH.InteractiveMagazine.IService/IJournalService.cs b/QYZH.InteractiveMagazine.IService/IJournalService.cs index 2f4cd00..1f16412 100644 --- a/QYZH.InteractiveMagazine.IService/IJournalService.cs +++ b/QYZH.InteractiveMagazine.IService/IJournalService.cs @@ -1,4 +1,4 @@ - + using QYZH.InteractiveMagazine.Models.Dto.Journal; using QYZH.InteractiveMagazine.Models.Dto.DotMatrix; using QYZH.InteractiveMagazine.Models.Enum; diff --git a/QYZH.InteractiveMagazine.IService/IUserAnswerTaskService.cs b/QYZH.InteractiveMagazine.IService/IUserAnswerTaskService.cs new file mode 100644 index 0000000..75ec763 --- /dev/null +++ b/QYZH.InteractiveMagazine.IService/IUserAnswerTaskService.cs @@ -0,0 +1,81 @@ +using QYZH.InteractiveMagazine.Models.Dto.UserAnswerTaskService; +using QYZH.InteractiveMagazine.Models.Entity; + +namespace QYZH.InteractiveMagazine.IService; + +/// +/// 用户作答任务服务接口 +/// +public interface IUserAnswerTaskService : IBaseService +{ + /// + /// 获取用户期刊学习进度列表(按期刊分组) + /// + /// 用户Id + /// 按期刊分组的学习进度数据 + Task> GetJournalProgressAsync(long userId); + + /// + /// 获取目录列表(按一级目录分组,二级目录含完成状态) + /// + /// 期刊Id + /// 用户Id + /// 按一级目录分组的目录列表 + Task> GetCatalogListAsync(long journalId, long userId); + + /// + /// 获取一级目录概览(含二级目录详情、页码范围、任务完成状态) + /// + /// 一级目录Id(JournalCatalogId) + /// 用户Id + /// 一级目录概览数据 + Task GetCatalogOverviewAsync(long catalogId, long userId); + + /// + /// 获取用户答题任务列表 + /// + /// 二级目录Id(JournalCatalogId) + /// 用户Id + /// 答题任务列表 + Task GetUserAnswerTaskListAsync(long catalogId, long userId); + + /// + /// 获取跨页题目详情 + /// + /// 任务Id + /// 用户Id + /// 跨页题目详情 + Task GetCrossPageTaskAsync(long journalPageTaskId, long userId); + + /// + /// 领取任务积分 + /// + /// 任务Id + /// 用户Id + /// 领取结果 + Task ClaimTaskPointsAsync(long journalPageTaskId, long userId); + + /// + /// 批量领取任务积分(一键领取) + /// + /// 批量领取参数 + /// 用户Id + /// 批量领取结果 + Task BatchClaimPointsAsync(BatchClaimPointsInput input, long userId); + + /// + /// 获取杂志列表 + /// + /// 用户Id + /// 查询类型:Bound=已绑定,Unbound=未绑定(添加书籍用),默认是:Bound + /// 杂志列表 + Task> GetJournalListAsync(long userId, string? queryType); + + /// + /// 新增学生 + /// + /// 新增学生参数 + /// 当前登录用户Id(WxUserId) + /// 新增学生结果 + Task AddStudentAsync(AddStudentInput input, long userId); +} diff --git a/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/AddStudentInput.cs b/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/AddStudentInput.cs new file mode 100644 index 0000000..32acfac --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/AddStudentInput.cs @@ -0,0 +1,18 @@ +namespace QYZH.InteractiveMagazine.Models.Dto.UserAnswerTaskService +{ + /// + /// 新增学生输入 + /// + public class AddStudentInput + { + /// + /// 昵称 + /// + public string? Name { get; set; } + + /// + /// 头像地址 + /// + public string? AvatarUrl { get; set; } + } +} \ No newline at end of file diff --git a/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/AddStudentOutput.cs b/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/AddStudentOutput.cs new file mode 100644 index 0000000..8a073bf --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/AddStudentOutput.cs @@ -0,0 +1,43 @@ +namespace QYZH.InteractiveMagazine.Models.Dto.UserAnswerTaskService +{ + /// + /// 新增学生输出 + /// + public class AddStudentOutput + { + /// + /// 学生Id + /// + public long Id { get; set; } + + /// + /// 关联微信用户Id + /// + public long WxUserId { get; set; } + + /// + /// 昵称 + /// + public string? Name { get; set; } + + /// + /// 头像地址 + /// + public string? AvatarUrl { get; set; } + + /// + /// 用户类型 + /// + public string Type { get; set; } = "Normal"; + + /// + /// 当前成长值 + /// + public int GrowthPoints { get; set; } + + /// + /// 积分余额 + /// + public int Points { get; set; } + } +} \ No newline at end of file diff --git a/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/BatchClaimPointsOutput.cs b/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/BatchClaimPointsOutput.cs new file mode 100644 index 0000000..651bd3f --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/BatchClaimPointsOutput.cs @@ -0,0 +1,59 @@ +namespace QYZH.InteractiveMagazine.Models.Dto.UserAnswerTaskService; + +/// +/// 批量领取任务积分输入 +/// +public class BatchClaimPointsInput +{ + /// + /// 任务分组Id列表(JournalPageTaskGroupId) + /// + public List GroupIds { get; set; } = new(); +} + +/// +/// 批量领取任务积分输出 +/// +public class BatchClaimPointsOutput +{ + /// + /// 成功领取数量 + /// + public int SuccessCount { get; set; } + + /// + /// 失败数量 + /// + public int FailCount { get; set; } + + /// + /// 累计获得积分 + /// + public int TotalPoints { get; set; } + + /// + /// 领取后积分余额 + /// + public int BalanceAfter { get; set; } + + /// + /// 失败详情 + /// + public List FailItems { get; set; } = new(); +} + +/// +/// 批量领取失败项 +/// +public class BatchClaimFailItem +{ + /// + /// 任务分组Id + /// + public long GroupId { get; set; } + + /// + /// 失败原因 + /// + public string Message { get; set; } +} diff --git a/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/CatalogListOutput.cs b/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/CatalogListOutput.cs new file mode 100644 index 0000000..8d0b001 --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/CatalogListOutput.cs @@ -0,0 +1,32 @@ +namespace QYZH.InteractiveMagazine.Models.Dto.UserAnswerTaskService; + +/// +/// 目录列表输出(按一级目录分组) +/// +public class CatalogListOutput +{ + /// + /// 一级目录ID + /// + public long CatalogId { get; set; } + + /// + /// 一级目录名称 + /// + public string CatalogName { get; set; } + + /// + /// 杂志名称 + /// + public string JournalName { get; set; } + + /// + /// 文章篇数 + /// + public int ArticleCount { get; set; } + + /// + /// 二级栏目列表 + /// + public List Children { get; set; } = new(); +} \ No newline at end of file diff --git a/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/CatalogOverviewOutput.cs b/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/CatalogOverviewOutput.cs new file mode 100644 index 0000000..934742f --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/CatalogOverviewOutput.cs @@ -0,0 +1,32 @@ +namespace QYZH.InteractiveMagazine.Models.Dto.UserAnswerTaskService; + +/// +/// 一级目录概览输出(含二级目录详情列表) +/// +public class CatalogOverviewOutput +{ + /// + /// 一级目录ID + /// + public long CatalogId { get; set; } + + /// + /// 一级目录名称 + /// + public string FirstLevelCatalogName { get; set; } + + /// + /// 文章篇数(该一级目录下 JournalPage 的个数) + /// + public int PageCount { get; set; } + + /// + /// 总任务数(该一级目录下所有 JournalPageTask 的条数) + /// + public int TotalTaskCount { get; set; } + + /// + /// 二级目录详情列表 + /// + public List SubCatalogs { get; set; } = new(); +} diff --git a/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/CatalogTaskDetailOutput.cs b/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/CatalogTaskDetailOutput.cs new file mode 100644 index 0000000..1377610 --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/CatalogTaskDetailOutput.cs @@ -0,0 +1,37 @@ +namespace QYZH.InteractiveMagazine.Models.Dto.UserAnswerTaskService; + +/// +/// 目录任务详情输出(含任务列表和徽章数) +/// +public class CatalogTaskDetailOutput +{ + /// + /// 目录ID + /// + public long CatalogId { get; set; } + + /// + /// 目录名称 + /// + public string CatalogName { get; set; } + + /// + /// 杂志起始页码 + /// + public int StartPage { get; set; } + + /// + /// 杂志结束页码 + /// + public int EndPage { get; set; } + + /// + /// 徽章个数 + /// + public int MedalCount { get; set; } + + /// + /// 任务列表 + /// + public List Tasks { get; set; } = new(); +} diff --git a/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/ClaimTaskPointsOutput.cs b/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/ClaimTaskPointsOutput.cs new file mode 100644 index 0000000..e402652 --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/ClaimTaskPointsOutput.cs @@ -0,0 +1,32 @@ +namespace QYZH.InteractiveMagazine.Models.Dto.UserAnswerTaskService; + +/// +/// 领取任务积分输出 +/// +public class ClaimTaskPointsOutput +{ + /// + /// 积分流水记录Id + /// + public long RecordId { get; set; } + + /// + /// 操作前积分余额 + /// + public int PreviousBalance { get; set; } + + /// + /// 操作后积分余额 + /// + public int NewBalance { get; set; } + + /// + /// 实际增加数量 + /// + public int AddedAmount { get; set; } + + /// + /// 任务名称 + /// + public string TaskName { get; set; } +} diff --git a/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/CrossPageTaskOutput.cs b/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/CrossPageTaskOutput.cs new file mode 100644 index 0000000..845a7f5 --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/CrossPageTaskOutput.cs @@ -0,0 +1,27 @@ +namespace QYZH.InteractiveMagazine.Models.Dto.UserAnswerTaskService; + +/// +/// 跨页题目详情输出 +/// +public class CrossPageTaskOutput +{ + /// + /// 栏目名称 + /// + public string CatalogName { get; set; } + + /// + /// 任务名称 + /// + public string TaskName { get; set; } + + /// + /// 图片数组(已答题用PageAnswerUrl,未答题用TaskUrl) + /// + public List Images { get; set; } = new(); + + /// + /// 是否跨页题目 + /// + public bool IsCrossPage { get; set; } +} diff --git a/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/JournalListOutput.cs b/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/JournalListOutput.cs new file mode 100644 index 0000000..ece7bd6 --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/JournalListOutput.cs @@ -0,0 +1,32 @@ +namespace QYZH.InteractiveMagazine.Models.Dto.UserAnswerTaskService; + +/// +/// 杂志列表输出 +/// +public class JournalListOutput +{ + /// + /// 杂志Id + /// + public long Id { get; set; } + + /// + /// 杂志名称 + /// + public string Name { get; set; } + + /// + /// 杂志标题 + /// + public string Title { get; set; } + + /// + /// 封面 + /// + public string Cover { get; set; } + + /// + /// 是否已绑定 + /// + public bool IsBound { get; set; } +} diff --git a/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/JournalProgressOutput.cs b/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/JournalProgressOutput.cs new file mode 100644 index 0000000..cf09387 --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/JournalProgressOutput.cs @@ -0,0 +1,57 @@ +namespace QYZH.InteractiveMagazine.Models.Dto.UserAnswerTaskService; + +/// +/// 用户期刊学习进度输出(按期刊分组) +/// +public class JournalProgressOutput +{ + /// + /// 期刊ID + /// + public long JournalId { get; set; } + + /// + /// 期刊名称 + /// + public string JournalName { get; set; } + + /// + /// 栏目数(一级目录个数) + /// + public int CatalogCount { get; set; } + + /// + /// 任务总数(该期刊下 JournalPageTask 的条数) + /// + public int TotalTaskCount { get; set; } + + /// + /// 已完成任务数(按 JournalPageTaskGroupId 去重,同一题跨页算1个) + /// + public int CompletedTaskCount { get; set; } + + /// + /// 成长值(已完成任务的 GrowthPoint 累加) + /// + public int TotalGrowthPoints { get; set; } + + /// + /// 积分值(已完成任务的 Points 累加) + /// + public int TotalPoints { get; set; } + + /// + /// 进度百分比(已完成任务ID数 / 总任务数 × 100) + /// + public double Progress { get; set; } + + /// + /// 剩余篇数(该期刊总页数 - 用户已作答页数) + /// + public int RemainingPages { get; set; } + + /// + /// 连续打卡天数 + /// + public int ConsecutiveDays { get; set; } +} diff --git a/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/SecondCatalogOutput.cs b/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/SecondCatalogOutput.cs new file mode 100644 index 0000000..359c788 --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/SecondCatalogOutput.cs @@ -0,0 +1,32 @@ +namespace QYZH.InteractiveMagazine.Models.Dto.UserAnswerTaskService; + +/// +/// 二级栏目输出 +/// +public class SecondCatalogOutput +{ + /// + /// 二级栏目ID + /// + public long CatalogId { get; set; } + + /// + /// 二级栏目名称 + /// + public string CatalogName { get; set; } + + /// + /// 总积分值 + /// + public int TotalPoints { get; set; } + + /// + /// 总成长值 + /// + public int TotalGrowthPoints { get; set; } + + /// + /// 状态:未开始 / 进行中 / 已完成 + /// + public string Status { get; set; } +} \ No newline at end of file diff --git a/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/SubCatalogDetailOutput.cs b/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/SubCatalogDetailOutput.cs new file mode 100644 index 0000000..de3988c --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/SubCatalogDetailOutput.cs @@ -0,0 +1,37 @@ +namespace QYZH.InteractiveMagazine.Models.Dto.UserAnswerTaskService; + +/// +/// 二级目录详情输出(含页码范围、任务数、完成状态) +/// +public class SubCatalogDetailOutput +{ + /// + /// 二级目录ID + /// + public long CatalogId { get; set; } + + /// + /// 二级目录名字 + /// + public string SecondLevelCatalogName { get; set; } + + /// + /// 起始页码(杂志第几页开始) + /// + public int StartPage { get; set; } + + /// + /// 结束页码(杂志第几页结束) + /// + public int EndPage { get; set; } + + /// + /// 任务数量 + /// + public int TaskCount { get; set; } + + /// + /// 状态描述(格式如:1/2 进行中、2/2 已完成、0/2 未开始) + /// + public string Status { get; set; } +} diff --git a/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/TaskStatusOutput.cs b/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/TaskStatusOutput.cs new file mode 100644 index 0000000..a4da908 --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/TaskStatusOutput.cs @@ -0,0 +1,27 @@ +namespace QYZH.InteractiveMagazine.Models.Dto.UserAnswerTaskService; + +/// +/// 任务状态输出 +/// +public class TaskStatusOutput +{ + /// + /// 任务ID + /// + public long TaskId { get; set; } + + /// + /// 任务名字 + /// + public string TaskName { get; set; } + + /// + /// 任务状态:未开始 / 进行中 / 已完成 + /// + public string Status { get; set; } + + /// + /// 答题时间(分钟,由 AnswerSeconds 累加后转换) + /// + public double AnswerTime { get; set; } +} diff --git a/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/UserAnswerTaskListOutput.cs b/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/UserAnswerTaskListOutput.cs new file mode 100644 index 0000000..59eafd4 --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Dto/UserAnswerTaskService/UserAnswerTaskListOutput.cs @@ -0,0 +1,52 @@ +namespace QYZH.InteractiveMagazine.Models.Dto.UserAnswerTaskService; + +/// +/// 用户答题任务列表输出 +/// +public class UserAnswerTaskListOutput +{ + /// + /// 目录名称 + /// + public string CatalogName { get; set; } + + /// + /// 开始页码 + /// + public int StartPage { get; set; } + + /// + /// 结束页码 + /// + public int EndPage { get; set; } + + /// + /// 任务名称列表 + /// + public List TaskNames { get; set; } = new(); + + /// + /// 任务状态(进行中、已完成、未开始) + /// + public string Status { get; set; } + + /// + /// 答题时间(分钟) + /// + public double AnswerMinutes { get; set; } + + /// + /// 徽章个数 + /// + public int MedalCount { get; set; } + + /// + /// 总成长值 + /// + public int TotalGrowthPoints { get; set; } + + /// + /// 总积分 + /// + public int TotalPoints { get; set; } +} diff --git a/QYZH.InteractiveMagazine.Models/Entity/JournalPageTaskUserAnswer.cs b/QYZH.InteractiveMagazine.Models/Entity/JournalPageTaskUserAnswer.cs index 21849fd..2dda2bc 100644 --- a/QYZH.InteractiveMagazine.Models/Entity/JournalPageTaskUserAnswer.cs +++ b/QYZH.InteractiveMagazine.Models/Entity/JournalPageTaskUserAnswer.cs @@ -1,7 +1,4 @@ using SqlSugar; -using System; -using System.Linq; -using System.Text; namespace QYZH.InteractiveMagazine.Models.Entity { @@ -9,208 +6,203 @@ namespace QYZH.InteractiveMagazine.Models.Entity ///杂志任务用户作答记录; /// [SugarTable("JournalPageTaskUserAnswer")] - public partial class JournalPageTaskUserAnswer : SqlSugarBaseEntity + public class JournalPageTaskUserAnswer : SqlSugarBaseEntity { - public JournalPageTaskUserAnswer(){ + /// + /// Desc:书id + /// Default: + /// Nullable:False + /// + public long JournalId { get; set; } + /// + /// Desc:书页id + /// Default: + /// Nullable:False + /// + public long JournalPageId { get; set; } - } + /// + /// Desc:任务id + /// Default: + /// Nullable:False + /// + public long JournalPageTaskId { get; set; } - /// - /// Desc:书id - /// Default: - /// Nullable:False - /// - public long JournalId {get;set;} + /// + /// Desc:任务分组id + /// Default: + /// Nullable:False + /// + public long JournalPageTaskGroupId { get; set; } - /// - /// Desc:书页id - /// Default: - /// Nullable:False - /// - public long JournalPageId {get;set;} + /// + /// Desc:用户id + /// Default: + /// Nullable:False + /// + public long UserId { get; set; } - /// - /// Desc:任务id - /// Default: - /// Nullable:False - /// - public long JournalPageTaskId {get;set;} + /// + /// Desc:作答结果 + /// Default: + /// Nullable:True + /// + public string Result { get; set; } - /// - /// Desc:任务分组id - /// Default: - /// Nullable:False - /// - public long JournalPageTaskGroupId {get;set;} + /// + /// Desc:积分 + /// Default:0 + /// Nullable:False + /// + public float Points { get; set; } - /// - /// Desc:用户id - /// Default: - /// Nullable:False - /// - public long UserId {get;set;} + /// + /// Desc:成长值 + /// Default:0 + /// Nullable:False + /// + public float GrowthPoints { get; set; } - /// - /// Desc:作答结果 - /// Default: - /// Nullable:True - /// - public string Result {get;set;} + /// + /// Desc:题目作答图片地址 + /// Default: + /// Nullable:True + /// + public string QuestionAnswerUrl { get; set; } - /// - /// Desc:积分 - /// Default:0 - /// Nullable:False - /// - public float Points {get;set;} + /// + /// Desc:答案图片地址 + /// Default: + /// Nullable:True + /// + public string AnswerUrl { get; set; } - /// - /// Desc:成长值 - /// Default:0 - /// Nullable:False - /// - public float GrowthPoints {get;set;} + /// + /// Desc:页面作答图片地址 + /// Default: + /// Nullable:True + /// + public string PageAnswerUrl { get; set; } - /// - /// Desc:题目作答图片地址 - /// Default: - /// Nullable:True - /// - public string QuestionAnswerUrl {get;set;} + /// + /// Desc:乐观锁 + /// Default: + /// Nullable:False + /// + public int Revision { get; set; } - /// - /// Desc:答案图片地址 - /// Default: - /// Nullable:True - /// - public string AnswerUrl {get;set;} + /// + /// Desc:作答状态 + /// Default: + /// Nullable:False + /// + public int AnswerStatus { get; set; } - /// - /// Desc:页面作答图片地址 - /// Default: - /// Nullable:True - /// - public string PageAnswerUrl {get;set;} + /// + /// Desc:作答开始时间 + /// Default: + /// Nullable:True + /// + public DateTime AnswerStartTime { get; set; } - /// - /// Desc:乐观锁 - /// Default: - /// Nullable:False - /// - public int Revision {get;set;} + /// + /// Desc:作答结束时间 + /// Default: + /// Nullable:True + /// + public DateTime AnswerEndTime { get; set; } - /// - /// Desc:作答状态 - /// Default: - /// Nullable:False - /// - public int AnswerStatus {get;set;} + /// + /// Desc:作答耗时(秒) + /// Default:0 + /// Nullable:False + /// + public int AnswerSeconds { get; set; } - /// - /// Desc:作答开始时间 - /// Default: - /// Nullable:True - /// - public DateTime AnswerStartTime {get;set;} + /// + /// Desc:图像识别结果 + /// Default: + /// Nullable:False + /// + public int ImageRecognition { get; set; } - /// - /// Desc:作答结束时间 - /// Default: - /// Nullable:True - /// - public DateTime AnswerEndTime {get;set;} + /// + /// Desc:书页序号 + /// Default: + /// Nullable:False + /// + public int JournalPageNum { get; set; } - /// - /// Desc:作答耗时(秒) - /// Default:0 - /// Nullable:False - /// - public int AnswerSeconds {get;set;} + /// + /// Desc:修改次数 + /// Default: + /// Nullable:False + /// + public int Modify { get; set; } - /// - /// Desc:图像识别结果 - /// Default: - /// Nullable:False - /// - public int ImageRecognition {get;set;} + /// + /// Desc:最后标签id + /// Default: + /// Nullable:False + /// + public long LastTag { get; set; } - /// - /// Desc:书页序号 - /// Default: - /// Nullable:False - /// - public int JournalPageNum {get;set;} + /// + /// Desc:点读书页序号 + /// Default: + /// Nullable:False + /// + public int DotPageNum { get; set; } - /// - /// Desc:修改次数 - /// Default: - /// Nullable:False - /// - public int Modify {get;set;} + /// + /// Desc:页面结果图片地址 + /// Default: + /// Nullable:True + /// + public string PageResultUrl { get; set; } - /// - /// Desc:最后标签id - /// Default: - /// Nullable:False - /// - public long LastTag {get;set;} + /// + /// Desc:题型 + /// Default: + /// Nullable:True + /// + public string Type { get; set; } - /// - /// Desc:点读书页序号 - /// Default: - /// Nullable:False - /// - public int DotPageNum {get;set;} + /// + /// Desc:点读书页编号 + /// Default: + /// Nullable:True + /// + public string DotPageNo { get; set; } - /// - /// Desc:页面结果图片地址 - /// Default: - /// Nullable:True - /// - public string PageResultUrl {get;set;} + /// + /// Desc:页面作答点读图片地址 + /// Default: + /// Nullable:True + /// + public string PageAnswerDotUrl { get; set; } - /// - /// Desc:题型 - /// Default: - /// Nullable:True - /// - public string Type {get;set;} + /// + /// Desc:中断次数 + /// Default:0 + /// Nullable:False + /// + public int BreakCount { get; set; } - /// - /// Desc:点读书页编号 - /// Default: - /// Nullable:True - /// - public string DotPageNo {get;set;} + /// + /// Desc:中断时间记录 + /// Default: + /// Nullable:True + /// + public string BreakTimes { get; set; } - /// - /// Desc:页面作答点读图片地址 - /// Default: - /// Nullable:True - /// - public string PageAnswerDotUrl {get;set;} - - /// - /// Desc:中断次数 - /// Default:0 - /// Nullable:False - /// - public int BreakCount {get;set;} - - /// - /// Desc:中断时间记录 - /// Default: - /// Nullable:True - /// - public string BreakTimes {get;set;} - - /// - /// Desc:作业状态 - /// Default: - /// Nullable:True - /// - public string AssignmentStatus {get;set;} + /// + /// Desc:作业状态 + /// Default: + /// Nullable:True + /// + public string AssignmentStatus { get; set; } } } diff --git a/QYZH.InteractiveMagazine.Models/Entity/UserMedal.cs b/QYZH.InteractiveMagazine.Models/Entity/UserMedal.cs index 9af5a40..e6d1f44 100644 --- a/QYZH.InteractiveMagazine.Models/Entity/UserMedal.cs +++ b/QYZH.InteractiveMagazine.Models/Entity/UserMedal.cs @@ -25,7 +25,7 @@ namespace QYZH.InteractiveMagazine.Models.Entity /// Default: /// Nullable:False /// - public int MedalId {get;set;} + public long MedalId {get;set;} /// /// Desc:获得时间 diff --git a/QYZH.InteractiveMagazine.Models/Enum/UserAnswerStatusEnum.cs b/QYZH.InteractiveMagazine.Models/Enum/UserAnswerStatusEnum.cs new file mode 100644 index 0000000..99427b5 --- /dev/null +++ b/QYZH.InteractiveMagazine.Models/Enum/UserAnswerStatusEnum.cs @@ -0,0 +1,21 @@ +using System.ComponentModel; + +namespace QYZH.InteractiveMagazine.Models.Enum; + +/// +/// 学生答题的完成状态:0-进行中,1-已完成,未开始(没有数据) +/// +public enum UserAnswerStatusEnum +{ + /// + /// 进行中 + /// + [Description("进行中")] + Processing = 0, + + /// + /// 已完成 + /// + [Description("已完成")] + Complete = 1, +} diff --git a/QYZH.InteractiveMagazine.Service/JournalService.cs b/QYZH.InteractiveMagazine.Service/JournalService.cs index 25dedce..64d0c47 100644 --- a/QYZH.InteractiveMagazine.Service/JournalService.cs +++ b/QYZH.InteractiveMagazine.Service/JournalService.cs @@ -1,4 +1,4 @@ -using Mapster; +using Mapster; using Microsoft.Extensions.Logging; using QYZH.InteractiveMagazine.Common.Extensions; using QYZH.InteractiveMagazine.Infrastructure.OSS; diff --git a/QYZH.InteractiveMagazine.Service/UserAnswerTaskService.cs b/QYZH.InteractiveMagazine.Service/UserAnswerTaskService.cs new file mode 100644 index 0000000..dd8a94d --- /dev/null +++ b/QYZH.InteractiveMagazine.Service/UserAnswerTaskService.cs @@ -0,0 +1,946 @@ +using Microsoft.Extensions.Logging; +using QYZH.InteractiveMagazine.IService; +using QYZH.InteractiveMagazine.Models.Common; +using QYZH.InteractiveMagazine.Models.Dto.Points; +using QYZH.InteractiveMagazine.Models.Dto.UserAnswerTaskService; +using QYZH.InteractiveMagazine.Models.Entity; +using QYZH.InteractiveMagazine.Models.Enum; +using QYZH.InteractiveMagazine.Repository; + +namespace QYZH.InteractiveMagazine.Service; + +/// +/// 用户作答任务服务实现 +/// +public class UserAnswerTaskService( + BaseRepository answerRepository, + BaseRepository journalRepository, + BaseRepository catalogRepository, + BaseRepository pageRepository, + BaseRepository taskRepository, + BaseRepository userMedalRepository, + BaseRepository medalRepository, + BaseRepository userJournalRepository, + BaseRepository usersRepository, + BaseRepository checkInRecordRepository, + IPointsService pointsService, + ILogger logger) + : BaseRepository, IUserAnswerTaskService +{ + /// + /// 获取用户期刊学习进度列表(按期刊分组,批量预加载避免N+1) + /// + public async Task> GetJournalProgressAsync(long userId) + { + // 1. 查询用户的所有作答记录 + var userAnswers = await answerRepository.Context.Queryable() + .Where(a => a.UserId == userId && !a.IsDeleted) + .ToListAsync(); + + if (userAnswers == null || userAnswers.Count == 0) + { + return new List(); + } + + // 2. 提取关联的期刊ID(去重) + var journalIds = userAnswers.Select(a => a.JournalId).Distinct().ToList(); + + // 3. 批量预加载关联实体(按期刊维度全量加载) + var journals = await journalRepository.Context.Queryable() + .Where(j => journalIds.Contains(j.Id) && !j.IsDeleted) + .ToListAsync(); + + // 加载这些期刊的一级目录(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(); + + // 加载这些期刊的所有页面(用于计算剩余篇数) + var allPages = await pageRepository.Context.Queryable() + .Where(p => journalIds.Contains(p.JournalId) && !p.IsDeleted) + .ToListAsync(); + + // 4. 查询用户连续打卡天数 + var consecutiveDays = await CalculateConsecutiveDaysAsync(userId); + + // 5. 提取用户已作答的 ID 集合(包含进行中和已完成) + var userAnsweredTaskIds = userAnswers.Select(a => a.JournalPageTaskId).Distinct().ToHashSet(); + var userAnsweredPageIds = userAnswers.Select(a => a.JournalPageId).Distinct().ToHashSet(); + + // 用户已完成的任务ID集合(Status=1表示已完成) + var userCompletedTaskIds = userAnswers.Where(a => a.Status == (int)UserAnswerStatusEnum.Complete).Select(a => a.JournalPageTaskId).Distinct().ToHashSet(); + + // 6. 按期刊分组聚合 + var result = new List(); + + 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() + .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); + + // 进度:已完成任务ID数 / 总任务数(Status=1才算完成) + var completedTaskIdCount = journalTasks.Count(t => userCompletedTaskIds.Contains(t.Id)); + var progress = totalTaskCount > 0 + ? Math.Round((double)completedTaskIdCount / totalTaskCount * 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; + + result.Add(new JournalProgressOutput + { + JournalId = journal.Id, + JournalName = journal.Name ?? string.Empty, + CatalogCount = catalogCount, + TotalTaskCount = totalTaskCount, + CompletedTaskCount = completedTaskCount, + TotalGrowthPoints = totalGrowthPoints, + TotalPoints = totalPoints, + Progress = progress, + RemainingPages = remainingPages, + ConsecutiveDays = consecutiveDays + }); + } + + return result; + } + + /// + /// 计算用户连续打卡天数 + /// + private async Task CalculateConsecutiveDaysAsync(long userId) + { + var today = DateTime.Now.Date; + + // 查询最近一次签到记录 + var lastRecord = await checkInRecordRepository.Context.Queryable() + .Where(r => r.UserId == userId && !r.IsDeleted) + .OrderByDescending(r => r.CheckInDate) + .FirstAsync(); + + if (lastRecord == null) + { + return 0; + } + + var lastDate = lastRecord.CheckInDate.Date; + + // 如果最后一次签到是今天或昨天,则连续天数延续 + if (lastDate == today || lastDate == today.AddDays(-1)) + { + return lastRecord.ConsecutiveDays; + } + + return 0; + } + + /// + /// 获取目录列表(按一级目录分组,二级目录含完成状态,批量预加载避免N+1) + /// + public async Task> GetCatalogListAsync(long journalId, long userId) + { + // 1. 查询期刊 + var journal = await journalRepository.Context.Queryable() + .Where(j => j.Id == journalId && !j.IsDeleted) + .FirstAsync(); + + if (journal == null) + { + return new List(); + } + + // 2. 批量加载关联数据 + var allCatalogs = await catalogRepository.Context.Queryable() + .Where(c => c.JournalId == journalId && !c.IsDeleted) + .ToListAsync(); + + var allPages = await pageRepository.Context.Queryable() + .Where(p => p.JournalId == journalId && !p.IsDeleted) + .ToListAsync(); + + var allTasks = await taskRepository.Context.Queryable() + .Where(t => t.JournalId == journalId && !t.IsDeleted) + .ToListAsync(); + + var userAnswers = await answerRepository.Context.Queryable() + .Where(a => a.JournalId == journalId && a.UserId == userId && !a.IsDeleted) + .ToListAsync(); + + // 3. 用户已作答的任务ID集合(包含进行中和已完成) + var userAnsweredTaskIds = userAnswers.Select(a => a.JournalPageTaskId).Distinct().ToHashSet(); + + // 用户已完成的任务ID集合(Status=1表示已完成) + var userCompletedTaskIds = userAnswers.Where(a => a.Status == (int)UserAnswerStatusEnum.Complete).Select(a => a.JournalPageTaskId).Distinct().ToHashSet(); + + // 4. 按一级目录分组 + var firstLevelCatalogs = allCatalogs.Where(c => c.ParentId == 0 && c.Type == JournalCatalogTypeEnum.Unit) + .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(); + + // 文章篇数 = Type=Page 的子目录数量 + var articleCount = pageCatalogs.Count; + 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 的文章作为二级列表展示 + var children = new List(); + + if (secondLevelCatalogs.Count > 0) + { + // 有二级单元:按二级单元分组 + foreach (var secondCatalog in secondLevelCatalogs) + { + // 二级单元下的 Type=Page 子目录 + var subPageCatalogs = allCatalogs + .Where(c => c.ParentId == secondCatalog.Id && c.Type == JournalCatalogTypeEnum.Page) + .ToList(); + var subPageCatalogIds = subPageCatalogs.Select(c => c.Id).ToHashSet(); + + // 二级单元下的所有页面 + var secondLevelPageIds = allPages + .Where(p => subPageCatalogIds.Contains(p.JournalCatalogId)) + .Select(p => p.Id) + .ToHashSet(); + + // 二级单元下的所有任务 + var secondLevelTasks = allTasks.Where(t => secondLevelPageIds.Contains(t.JournalPageId)).ToList(); + + // 判定状态(Status=0进行中,Status=1已完成) + string status; + if (secondLevelTasks.Count == 0) + { + status = "未开始"; + } + 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 = "进行中"; + } + + // 计算该二级目录的总成长值和总积分 + var totalGrowthPoints = secondLevelTasks.Sum(t => t.GrowthPoint) ?? 0; + var totalPoints = secondLevelTasks.Sum(t => t.Points); + + children.Add(new SecondCatalogOutput + { + CatalogId = secondCatalog.Id, + CatalogName = secondCatalog.Name ?? string.Empty, + TotalPoints = totalPoints, + TotalGrowthPoints = totalGrowthPoints, + Status = status + }); + } + } + else + { + // 没有二级单元:直接将文章作为二级列表 + foreach (var pageCatalog in pageCatalogs) + { + // 该文章下的所有页面 + var articlePageIds = allPages + .Where(p => p.JournalCatalogId == pageCatalog.Id) + .Select(p => p.Id) + .ToHashSet(); + + // 该文章下的所有任务 + var articleTasks = allTasks.Where(t => articlePageIds.Contains(t.JournalPageId)).ToList(); + + // 判定状态(Status=0进行中,Status=1已完成) + string status; + if (articleTasks.Count == 0) + { + status = "未开始"; + } + else + { + var completedCount = articleTasks.Count(t => userCompletedTaskIds.Contains(t.Id)); + var answeredCount = articleTasks.Count(t => userAnsweredTaskIds.Contains(t.Id)); + if (answeredCount == 0) + status = "未开始"; + else if (completedCount == articleTasks.Count) + status = "已完成"; + else + status = "进行中"; + } + + // 计算该文章的总成长值和总积分 + var totalGrowthPoints = articleTasks.Sum(t => t.GrowthPoint) ?? 0; + var totalPoints = articleTasks.Sum(t => t.Points); + + children.Add(new SecondCatalogOutput + { + CatalogId = pageCatalog.Id, + CatalogName = pageCatalog.Name ?? string.Empty, + TotalPoints = totalPoints, + TotalGrowthPoints = totalGrowthPoints, + Status = status + }); + } + } + + // 计算该一级目录的总成长值和总积分(基于所有文章页面的任务) + var firstLevelTasks = allTasks.Where(t => allArticlePageIds.Contains(t.JournalPageId)).ToList(); + + result.Add(new CatalogListOutput + { + CatalogId = firstCatalog.Id, + CatalogName = firstCatalog.Name ?? string.Empty, + JournalName = journal.Name ?? string.Empty, + ArticleCount = articleCount, + Children = children + }); + } + + return result; + } + + /// + /// 获取一级目录概览(含二级目录详情、页码范围、任务完成状态,批量预加载避免N+1) + /// + /// 一级烂ID + /// 用户ID + /// + public async Task GetCatalogOverviewAsync(long catalogId, long userId) + { + // 1. 查询一级目录 + var firstCatalog = await catalogRepository.Context.Queryable() + .Where(c => c.Id == catalogId && !c.IsDeleted) + .FirstAsync(); + + if (firstCatalog == null) + { + return null; + } + + // 2. 批量加载关联数据 + var allCatalogs = await catalogRepository.Context.Queryable() + .Where(c => c.JournalId == firstCatalog.JournalId && !c.IsDeleted) + .ToListAsync(); + + var allPages = await pageRepository.Context.Queryable() + .Where(p => p.JournalId == firstCatalog.JournalId && !p.IsDeleted) + .ToListAsync(); + + var allTasks = await taskRepository.Context.Queryable() + .Where(t => t.JournalId == firstCatalog.JournalId && !t.IsDeleted) + .ToListAsync(); + + var userAnswers = await answerRepository.Context.Queryable() + .Where(a => a.JournalId == firstCatalog.JournalId && a.UserId == userId && !a.IsDeleted) + .ToListAsync(); + + // 3. 用户已作答的任务ID集合(包含进行中和已完成) + var userAnsweredTaskIds = userAnswers.Select(a => a.JournalPageTaskId).Distinct().ToHashSet(); + + // 用户已完成的任务ID集合(Status=1表示已完成) + var userCompletedTaskIds = userAnswers.Where(a => a.Status == (int)UserAnswerStatusEnum.Complete).Select(a => a.JournalPageTaskId).Distinct().ToHashSet(); + + // 4. 获取该一级目录下所有 Type=Page 的子目录(即文章/页面目录) + var pageCatalogs = allCatalogs + .Where(c => c.ParentId == firstCatalog.Id && c.Type == JournalCatalogTypeEnum.Page) + .OrderBy(c => c.Sort) + .ToList(); + + // 5. 文章篇数 = Type=Page 的子目录数量 + var articleCount = pageCatalogs.Count; + + // 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. 总任务数 + var totalTaskCount = allTasks.Count(t => allLevelPageIds.Contains(t.JournalPageId)); + + // 8. 构建子目录详情列表 + var subCatalogs = new List(); + + foreach (var pageCatalog in pageCatalogs) + { + // 该文章下的所有页面 + var articlePages = allPages + .Where(p => p.JournalCatalogId == pageCatalog.Id) + .OrderBy(p => p.PageNum) + .ToList(); + + var articlePageIds = articlePages.Select(p => p.Id).ToHashSet(); + + // 该文章下的所有任务 + 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)); + + // 页码范围 + 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已完成) + string status; + if (totalTasks == 0) + status = $"0/0 未开始"; + else if (answeredTasks == 0) + status = $"0/{totalTasks} 未开始"; + else if (completedTasks == totalTasks) + status = $"{completedTasks}/{totalTasks} 已完成"; + else + status = $"{completedTasks}/{totalTasks} 进行中"; + + subCatalogs.Add(new SubCatalogDetailOutput + { + CatalogId = pageCatalog.Id, + SecondLevelCatalogName = pageCatalog.Name ?? string.Empty, + StartPage = startPage, + EndPage = endPage, + TaskCount = totalTasks, + Status = status + }); + } + + return new CatalogOverviewOutput + { + CatalogId = firstCatalog.Id, + FirstLevelCatalogName = firstCatalog.Name ?? string.Empty, + PageCount = articleCount, + TotalTaskCount = totalTaskCount, + SubCatalogs = subCatalogs + }; + } + + /// + /// 获取用户答题任务列表 + /// + public async Task GetUserAnswerTaskListAsync(long catalogId, long userId) + { + // 1. 查询目录 + var catalog = await catalogRepository.Context.Queryable() + .Where(c => c.Id == catalogId && !c.IsDeleted) + .FirstAsync(); + + if (catalog == null) + { + return null; + } + + // 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(); + + // 3. 获取该目录下的所有页面ID + var pageIds = allPages.Select(p => p.Id).ToHashSet(); + + // 4. 该目录下的所有任务 + var catalogTasks = allTasks.Where(t => pageIds.Contains(t.JournalPageId)).ToList(); + + // 5. 任务名称列表 + var taskNames = catalogTasks + .Where(t => !string.IsNullOrEmpty(t.Task)) + .Select(t => t.Task) + .Distinct() + .ToList(); + + // 6. 用户已作答的任务ID集合(包含进行中和已完成) + var userAnsweredTaskIds = userAnswers.Select(a => a.JournalPageTaskId).Distinct().ToHashSet(); + + // 用户已完成的任务ID集合(Status=1表示已完成) + var userCompletedTaskIds = userAnswers.Where(a => a.Status == (int)UserAnswerStatusEnum.Complete).Select(a => a.JournalPageTaskId).Distinct().ToHashSet(); + + // 7. 计算答题时间(分钟) + var totalAnswerSeconds = userAnswers + .Where(a => pageIds.Contains(a.JournalPageId)) + .Sum(a => a.AnswerSeconds); + var answerMinutes = Math.Round(totalAnswerSeconds / 60.0, 2); + + // 8. 判定状态(Status=0进行中,Status=1已完成) + string status; + if (catalogTasks.Count == 0) + { + status = "未开始"; + } + else + { + var completedCount = catalogTasks.Count(t => userCompletedTaskIds.Contains(t.Id)); + var answeredCount = catalogTasks.Count(t => userAnsweredTaskIds.Contains(t.Id)); + if (answeredCount == 0) + status = "未开始"; + else if (completedCount == catalogTasks.Count) + status = "已完成"; + else + status = "进行中"; + } + + // 9. 查询徽章个数(通过Medal表关联JournalId) + var medalCount = await medalRepository.Context.Queryable() + .Where(m => m.JournalId == catalog.JournalId && !m.IsDeleted) + .Select(m => m.Id) + .CountAsync(); + + var medalIds = await medalRepository.Context.Queryable() + .Where(m => m.JournalId == catalog.JournalId && !m.IsDeleted) + .Select(m => m.Id).ToListAsync(); + var userMedalCount = await userMedalRepository.Context.Queryable() + .Where(um => um.UserId == userId && !um.IsDeleted) + .Where(um => medalIds.Contains(um.MedalId)) + .CountAsync(); + + // 10. 计算页码范围 + 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); + + return new UserAnswerTaskListOutput + { + CatalogName = catalog.Name ?? string.Empty, + StartPage = startPage, + EndPage = endPage, + TaskNames = taskNames, + Status = status, + AnswerMinutes = answerMinutes, + MedalCount = userMedalCount, + TotalGrowthPoints = totalGrowthPoints, + TotalPoints = totalPoints + }; + } + + /// + /// 获取跨页题目详情 + /// + public async Task GetCrossPageTaskAsync(long journalPageTaskId, long userId) + { + // 1. 查询任务信息 + var task = await taskRepository.Context.Queryable() + .Where(t => t.Id == journalPageTaskId && !t.IsDeleted) + .FirstAsync(); + + if (task == null) + { + return null; + } + + // 2. 判断是否跨页题目 + var isCrossPage = journalPageTaskId != task.GroupId; + + // 3. 获取所有相关的任务(根据GroupId查询) + List relatedTasks; + if (isCrossPage) + { + // 跨页题目:根据GroupId查询所有数据 + relatedTasks = await taskRepository.Context.Queryable() + .Where(t => t.GroupId == task.GroupId && t.JournalId == task.JournalId && !t.IsDeleted) + .OrderBy(t => t.Id) + .ToListAsync(); + } + else + { + relatedTasks = new List { task }; + } + + // 4. 获取栏目名称 + var page = await pageRepository.Context.Queryable() + .Where(p => p.Id == task.JournalPageId && !p.IsDeleted) + .FirstAsync(); + + string catalogName = 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; + } + + // 5. 获取图片数组 + var images = new List(); + if (isCrossPage) + { + // 跨页题目:查询该GroupId下所有用户的答题记录 + var userAnswers = await answerRepository.Context.Queryable() + .Where(a => a.JournalPageTaskGroupId == task.GroupId && a.UserId == userId && !a.IsDeleted) + .OrderBy(a => a.JournalPageTaskId) + .ToListAsync(); + + if (userAnswers.Any(a => !string.IsNullOrEmpty(a.PageAnswerUrl))) + { + // 已答题,使用PageAnswerUrl数组 + images.AddRange(userAnswers.Where(a => !string.IsNullOrEmpty(a.PageAnswerUrl)).Select(a => a.PageAnswerUrl)); + } + else + { + // 未答题,使用TaskUrl数组 + foreach (var relatedTask in relatedTasks) + { + if (!string.IsNullOrEmpty(relatedTask.TaskUrl)) + { + images.Add(relatedTask.TaskUrl); + } + } + } + } + else + { + // 非跨页题目:查询单条答题记录 + var userAnswer = await answerRepository.Context.Queryable() + .Where(a => a.JournalPageTaskId == journalPageTaskId && a.UserId == userId && !a.IsDeleted) + .FirstAsync(); + + if (userAnswer != null && !string.IsNullOrEmpty(userAnswer.PageAnswerUrl)) + { + // 已答题,使用PageAnswerUrl + images.Add(userAnswer.PageAnswerUrl); + } + else + { + // 未答题,使用TaskUrl + if (!string.IsNullOrEmpty(task.TaskUrl)) + { + images.Add(task.TaskUrl); + } + } + } + + return new CrossPageTaskOutput + { + CatalogName = catalogName, + TaskName = task.Task ?? string.Empty, + Images = images, + IsCrossPage = isCrossPage + }; + } + + /// + /// 领取任务积分 + /// + public async Task ClaimTaskPointsAsync(long journalPageTaskId, long userId) + { + // 1. 查询任务信息 + var task = await taskRepository.Context.Queryable() + .Where(t => t.Id == journalPageTaskId && !t.IsDeleted) + .FirstAsync(); + + if (task == null) + { + throw new BusinessException("任务不存在", 404); + } + + // 2. 判断是否跨页题目 + var isCrossPage = journalPageTaskId != task.GroupId; + + // 3. 查询用户答题记录(跨页题目按GroupId查询,普通题目按任务Id查询) + List userAnswers; + if (isCrossPage) + { + userAnswers = await answerRepository.Context.Queryable() + .Where(a => a.JournalPageTaskGroupId == task.GroupId && a.UserId == userId && !a.IsDeleted) + .ToListAsync(); + } + else + { + userAnswers = await answerRepository.Context.Queryable() + .Where(a => a.JournalPageTaskId == journalPageTaskId && a.UserId == userId && !a.IsDeleted) + .ToListAsync(); + } + + if (userAnswers == null || userAnswers.Count == 0) + { + throw new BusinessException("请先完成答题再领取积分", 400); + } + + // 4. 计算积分(跨页题目需要累计所有相关任务的积分) + 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; + } + + if (totalPoints <= 0) + { + throw new BusinessException("该任务无积分可领取", 400); + } + + // 5. 检查是否已领取过积分(跨页题目按GroupId作为RelatedId) + var relatedId = isCrossPage ? task.GroupId : journalPageTaskId; + var existingRecord = await answerRepository.Context.Queryable() + .Where(r => r.UserId == userId && !r.IsDeleted) + .Where(r => r.ChangeType == PointsChangeTypeEnum.TaskReward.ToString() && r.RelatedId == relatedId) + .FirstAsync(); + + if (existingRecord != null) + { + throw new BusinessException("积分已领取,请勿重复领取", 400); + } + + // 6. 调用积分服务增加积分 + var addPointsResult = await pointsService.AddPointsAsync(new AddPointsInput + { + UserId = userId, + Amount = totalPoints, + ChangeType = PointsChangeTypeEnum.TaskReward, + RelatedId = relatedId, + Description = $"完成任务奖励:{task.Task}", + OperatorName = "系统" + }); + + return new ClaimTaskPointsOutput + { + RecordId = addPointsResult.RecordId, + PreviousBalance = addPointsResult.PreviousBalance, + NewBalance = addPointsResult.NewBalance, + AddedAmount = addPointsResult.AddedAmount, + TaskName = task.Task ?? string.Empty + }; + } + + /// + /// 批量领取任务积分(一键领取) + /// + public async Task BatchClaimPointsAsync(BatchClaimPointsInput input, long userId) + { + var result = new BatchClaimPointsOutput(); + + if (input == null || input.GroupIds == null || input.GroupIds.Count == 0) + { + throw new BusinessException("任务分组Id列表不能为空", 400); + } + + 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. 批量查询已领取的积分记录 + 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. 逐个处理领取 + int totalPoints = 0; + int currentBalance = 0; + + foreach (var groupId in groupIds) + { + try + { + // 检查是否已答题 + if (!answeredGroupIds.Contains(groupId)) + { + result.FailItems.Add(new BatchClaimFailItem + { + GroupId = groupId, + Message = "请先完成答题再领取积分" + }); + continue; + } + + // 检查是否已领取 + if (claimedGroupIds.Contains(groupId)) + { + result.FailItems.Add(new BatchClaimFailItem + { + GroupId = groupId, + Message = "积分已领取,请勿重复领取" + }); + continue; + } + + // 获取该分组的任务 + if (!tasksByGroup.TryGetValue(groupId, out var groupTasks) || groupTasks.Count == 0) + { + result.FailItems.Add(new BatchClaimFailItem + { + GroupId = groupId, + Message = "任务不存在" + }); + continue; + } + + // 计算积分 + int points = groupTasks.Sum(t => t.Points); + if (points <= 0) + { + result.FailItems.Add(new BatchClaimFailItem + { + GroupId = groupId, + Message = "该任务无积分可领取" + }); + continue; + } + + // 获取任务名称(取第一个任务的名称) + var taskName = groupTasks.First().Task ?? string.Empty; + + // 调用积分服务增加积分 + var addResult = await pointsService.AddPointsAsync(new AddPointsInput + { + UserId = userId, + Amount = points, + ChangeType = PointsChangeTypeEnum.TaskReward, + RelatedId = groupId, + Description = $"完成任务奖励:{taskName}", + OperatorName = "系统" + }); + + totalPoints += points; + currentBalance = addResult.NewBalance; + result.SuccessCount++; + } + catch (Exception ex) + { + result.FailItems.Add(new BatchClaimFailItem + { + GroupId = groupId, + Message = ex.Message + }); + } + } + + result.FailCount = result.FailItems.Count; + result.TotalPoints = totalPoints; + result.BalanceAfter = currentBalance; + + return result; + } + + /// + /// 获取杂志列表 + /// + public async Task> GetJournalListAsync(long userId, string? queryType) + { + var allJournals = await journalRepository.Queryable() + .Where(j => !j.IsDeleted && j.Status == (int)JournalStatusEnum.Archive) + .OrderByDescending(j => j.CreatedAt) + .ToListAsync(); + + var userJournals = await userJournalRepository.Context.Queryable() + .Where(uj => uj.UserId == userId && !uj.IsDeleted) + .Select(uj => uj.JournalId) + .ToListAsync(); + + var boundJournalIds = userJournals.ToHashSet(); + + IEnumerable filteredJournals = queryType switch + { + "Bound" => allJournals.Where(j => boundJournalIds.Contains(j.Id)), + "Unbound" => allJournals.Where(j => !boundJournalIds.Contains(j.Id)), + _ => allJournals + }; + + var result = filteredJournals.Select(j => new JournalListOutput + { + Id = j.Id, + Name = j.Name ?? string.Empty, + Title = j.Title ?? string.Empty, + Cover = j.Cover ?? string.Empty, + IsBound = boundJournalIds.Contains(j.Id) + }).ToList(); + + return result; + } + + /// + /// 新增学生 + /// + public async Task AddStudentAsync(AddStudentInput input, long userId) + { + // 创建新的学生记录 + var student = new Users + { + WxUserId = userId, + Name = input.Name, + AvatarUrl = input.AvatarUrl, + GrowthPoints = 0, + Points = 0, + Type = UsersTypeEnum.Normal, + IsLastOnline = false + }; + + // 插入数据库 + return await usersRepository.InsertAsync(student); + } +} diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/UserAnswerTaskController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/UserAnswerTaskController.cs new file mode 100644 index 0000000..6619c61 --- /dev/null +++ b/QYZH.InteractiveMagazine.WebApi/Controllers/WeChat/UserAnswerTaskController.cs @@ -0,0 +1,326 @@ +using Microsoft.AspNetCore.Mvc; +using QYZH.InteractiveMagazine.IService; +using QYZH.InteractiveMagazine.Models.Common; +using QYZH.InteractiveMagazine.Models.Dto; +using QYZH.InteractiveMagazine.Models.Dto.UserAnswerTaskService; +using System.ComponentModel.DataAnnotations; + +namespace QYZH.InteractiveMagazine.WebApi.Controllers.WeChat; + +/// +/// 小程序首页控制器 +/// +public class UserAnswerTaskController : WeChatBaseController +{ + private readonly IUserAnswerTaskService _userAnswerTaskService; + private readonly ILogger _logger; + + /// + /// 构造函数 + /// + public UserAnswerTaskController( + IUserAnswerTaskService userAnswerTaskService, + ILogger logger) + { + _userAnswerTaskService = userAnswerTaskService; + _logger = logger; + } + + /// + /// 获取当前用户的期刊学习进度列表 + /// + /// 按期刊分组的学习进度数据 + [HttpGet("progress")] + public async Task>> GetProgressAsync() + { + try + { + var userId = GetCurrentUserId(); + if (userId == 0) + { + return BaseResponse>.Fail(ResultCode.DENY, "未获取到用户信息"); + } + + var result = await _userAnswerTaskService.GetJournalProgressAsync(userId); + return Success(result); + } + catch (Exception ex) + { + _logger.LogError(ex, "获取学习进度失败,UserId: {UserId}", GetCurrentUserId()); + return BaseResponse>.Fail("获取学习进度失败,请稍后重试"); + } + } + + /// + /// 获取目录列表(按一级目录分组,二级目录含完成状态) + /// + /// 期刊Id + /// 按一级目录分组的目录列表 + [HttpGet("catalog-list/{journalId:long}")] + public async Task>> GetCatalogListAsync([Required] long journalId) + { + try + { + var userId = GetCurrentUserId(); + if (userId == 0) + { + return BaseResponse>.Fail(ResultCode.DENY, "未获取到用户信息"); + } + + if (journalId <= 0) + { + return BaseResponse>.Fail(ResultCode.PARAM_ERROR, "期刊Id不能为空"); + } + + var result = await _userAnswerTaskService.GetCatalogListAsync(journalId, userId); + return Success(result); + } + catch (Exception ex) + { + _logger.LogError(ex, "获取目录列表失败,JournalId: {JournalId}, UserId: {UserId}", journalId, GetCurrentUserId()); + return BaseResponse>.Fail("获取目录列表失败,请稍后重试"); + } + } + + /// + /// 获取一级目录概览(含二级目录详情、页码范围、任务完成状态) + /// + /// 一级目录Id + /// 一级目录概览数据 + [HttpGet("catalog-overview/{catalogId:long}")] + public async Task> GetCatalogOverviewAsync([Required] long catalogId) + { + try + { + var userId = GetCurrentUserId(); + if (userId == 0) + { + return BaseResponse.Fail(ResultCode.DENY, "未获取到用户信息"); + } + + if (catalogId <= 0) + { + return BaseResponse.Fail(ResultCode.PARAM_ERROR, "目录Id不能为空"); + } + + var result = await _userAnswerTaskService.GetCatalogOverviewAsync(catalogId, userId); + if (result == null) + { + return BaseResponse.Fail("未找到该目录"); + } + + return Success(result); + } + catch (Exception ex) + { + _logger.LogError(ex, "获取目录概览失败,CatalogId: {CatalogId}, UserId: {UserId}", catalogId, GetCurrentUserId()); + return BaseResponse.Fail("获取目录概览失败,请稍后重试"); + } + } + + /// + /// 获取用户答题任务列表 + /// + /// 二级目录Id + /// 答题任务列表 + [HttpGet("task-list/{catalogId:long}")] + public async Task> GetTaskListAsync([Required] long catalogId) + { + try + { + var userId = GetCurrentUserId(); + if (userId == 0) + { + return BaseResponse.Fail(ResultCode.DENY, "未获取到用户信息"); + } + + if (catalogId <= 0) + { + return BaseResponse.Fail(ResultCode.PARAM_ERROR, "目录Id不能为空"); + } + + var result = await _userAnswerTaskService.GetUserAnswerTaskListAsync(catalogId, userId); + if (result == null) + { + return BaseResponse.Fail("未找到该目录"); + } + + return Success(result); + } + catch (Exception ex) + { + _logger.LogError(ex, "获取任务列表失败,CatalogId: {CatalogId}, UserId: {UserId}", catalogId, GetCurrentUserId()); + return BaseResponse.Fail("获取任务列表失败,请稍后重试"); + } + } + + /// + /// 获取跨页题目详情 + /// + /// 任务Id + /// 跨页题目详情 + [HttpGet("cross-page-task/{journalPageTaskId:long}")] + public async Task> GetCrossPageTaskAsync([Required] long journalPageTaskId) + { + try + { + var userId = GetCurrentUserId(); + if (userId == 0) + { + return BaseResponse.Fail(ResultCode.DENY, "未获取到用户信息"); + } + + if (journalPageTaskId <= 0) + { + return BaseResponse.Fail(ResultCode.PARAM_ERROR, "任务Id不能为空"); + } + + var result = await _userAnswerTaskService.GetCrossPageTaskAsync(journalPageTaskId, userId); + if (result == null) + { + return BaseResponse.Fail("未找到该任务"); + } + + return Success(result); + } + catch (Exception ex) + { + _logger.LogError(ex, "获取跨页题目失败,JournalPageTaskId: {JournalPageTaskId}, UserId: {UserId}", journalPageTaskId, GetCurrentUserId()); + return BaseResponse.Fail("获取跨页题目失败,请稍后重试"); + } + } + + /// + /// 领取任务积分 + /// + /// 任务Id + /// 领取结果 + [HttpPost("claim-points/{journalPageTaskId:long}")] + public async Task> ClaimTaskPointsAsync([Required] long journalPageTaskId) + { + try + { + var userId = GetCurrentUserId(); + if (userId == 0) + { + return BaseResponse.Fail(ResultCode.DENY, "未获取到用户信息"); + } + + if (journalPageTaskId <= 0) + { + return BaseResponse.Fail(ResultCode.PARAM_ERROR, "任务Id不能为空"); + } + + var result = await _userAnswerTaskService.ClaimTaskPointsAsync(journalPageTaskId, userId); + return Success(result); + } + catch (BusinessException ex) + { + _logger.LogWarning(ex, "领取任务积分业务异常,JournalPageTaskId: {JournalPageTaskId}, UserId: {UserId}", journalPageTaskId, GetCurrentUserId()); + return BaseResponse.Fail(ex.Message); + } + catch (Exception ex) + { + _logger.LogError(ex, "领取任务积分失败,JournalPageTaskId: {JournalPageTaskId}, UserId: {UserId}", journalPageTaskId, GetCurrentUserId()); + return BaseResponse.Fail("领取任务积分失败,请稍后重试"); + } + } + + /// + /// 批量领取任务积分(一键领取) + /// + /// 批量领取参数 + /// 批量领取结果 + [HttpPost("batch-claim-points")] + public async Task> BatchClaimPointsAsync([FromBody] BatchClaimPointsInput input) + { + try + { + var userId = GetCurrentUserId(); + if (userId == 0) + { + return BaseResponse.Fail(ResultCode.DENY, "未获取到用户信息"); + } + + if (input == null || input.GroupIds == null || input.GroupIds.Count == 0) + { + return BaseResponse.Fail(ResultCode.PARAM_ERROR, "任务分组Id列表不能为空"); + } + + var result = await _userAnswerTaskService.BatchClaimPointsAsync(input, userId); + return Success(result); + } + catch (BusinessException ex) + { + _logger.LogWarning(ex, "批量领取积分业务异常,UserId: {UserId}", GetCurrentUserId()); + return BaseResponse.Fail(ex.Message); + } + catch (Exception ex) + { + _logger.LogError(ex, "批量领取积分失败,UserId: {UserId}", GetCurrentUserId()); + return BaseResponse.Fail("批量领取积分失败,请稍后重试"); + } + } + + /// + /// 获取杂志列表 + /// + /// 查询类型:Bound=已绑定,Unbound=未绑定(添加书籍用),默认是:Bound + /// 杂志列表 + [HttpGet("journal-list")] + public async Task>> GetJournalListAsync([FromQuery] string? queryType = "Bound") + { + try + { + var userId = GetCurrentUserId(); + if (userId == 0) + { + return BaseResponse>.Fail(ResultCode.DENY, "未获取到用户信息"); + } + + var result = await _userAnswerTaskService.GetJournalListAsync(userId, queryType); + return Success(result); + } + catch (BusinessException ex) + { + _logger.LogWarning(ex, "查询杂志列表业务异常,UserId: {UserId}", GetCurrentUserId()); + return BaseResponse>.Fail(ex.Message); + } + catch (Exception ex) + { + _logger.LogError(ex, "查询杂志列表失败,UserId: {UserId}", GetCurrentUserId()); + return BaseResponse>.Fail("查询杂志列表失败,请稍后重试"); + } + } + + /// + /// 新增学生 + /// + /// 新增学生参数 + /// 新增学生结果 + [HttpPost("add-student")] + public async Task> AddStudentAsync([FromBody] AddStudentInput input) + { + try + { + var userId = GetCurrentUserId(); + if (userId == 0) + { + return BaseResponse.Fail(ResultCode.DENY, "未获取到用户信息"); + } + + var result = await _userAnswerTaskService.AddStudentAsync(input, userId); + return Success(result); + } + catch (BusinessException ex) + { + _logger.LogWarning(ex, "新增学生业务异常,UserId: {UserId}", GetCurrentUserId()); + return BaseResponse.Fail(ex.Message); + } + catch (Exception ex) + { + _logger.LogError(ex, "新增学生失败,UserId: {UserId}", GetCurrentUserId()); + return BaseResponse.Fail("新增学生失败,请稍后重试"); + } + } +} diff --git a/QYZH.InteractiveMagazine.WebApi/DataProtection/key-22eabfbc-9d6d-4d02-a009-12a7eb9886c2.xml b/QYZH.InteractiveMagazine.WebApi/DataProtection/key-22eabfbc-9d6d-4d02-a009-12a7eb9886c2.xml new file mode 100644 index 0000000..8ec78c5 --- /dev/null +++ b/QYZH.InteractiveMagazine.WebApi/DataProtection/key-22eabfbc-9d6d-4d02-a009-12a7eb9886c2.xml @@ -0,0 +1,16 @@ + + + 2026-06-24T08:30:52.1384894Z + 2026-06-24T08:30:52.1128731Z + 2026-09-22T08:30:52.1128731Z + + + + + + + pejyMxgU0u9FWIYfhFHKOIK2tiEFlkbgD4WVeVom3GNwAP1/JRCwVGkaqaceJzwpzVBcNpwM2JrunPK/KQeAfQ== + + + + \ No newline at end of file