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