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 userJournalRepository,
BaseRepository usersRepository,
BaseRepository checkInRecordRepository,
IPointsService pointsService,
ILogger logger)
: BaseRepository, IUserAnswerTaskService
{
///
/// ============================================
/// 方法思路:
/// 1. 批量预加载策略:为避免N+1查询问题,先按期刊维度批量加载所有关联数据
/// 2. 期刊来源:
/// - 用户绑定的杂志(UserJournal表)
/// - 用户有作答记录的杂志
/// 合并去重后返回
/// 3. 跨页题目处理核心:
/// - 跨页题目定义:JournalPageTask.Id != GroupId(同一个GroupId下有多个任务)
/// - 进度计算:按GroupId去重,已完成条件是该GroupId下所有任务都完成(Status=1)
/// - 积分计算:按GroupId去重,每个GroupId只计算一次积分(取第一个任务的积分)
/// 4. 状态判定逻辑:
/// - 未开始:无任何作答记录
/// - 进行中:有作答记录但未全部完成
/// - 已完成:所有GroupId都完成
/// 5. 用户未作答任何任务时:
/// - 仍返回绑定杂志的进度信息
/// - 进度、成长值、积分为0
/// - 连续打卡、剩余篇数正常计算
/// ============================================
///
/// 用户ID
/// 用户所有期刊的学习进度列表
public async Task> GetJournalProgressAsync(long userId)
{
// ============================================
// 跨页题目处理核心逻辑说明:
// 1. 跨页题目定义:JournalPageTask.Id != GroupId
// - Id 是任务的唯一标识
// - GroupId 是跨页题目的分组标识,同一个跨页题目的所有任务共享同一个GroupId
// 2. 进度计算:按 GroupId 去重
// - 已完成条件:该 GroupId 下所有任务都完成(Status=1)
// 3. 积分计算:按 GroupId 去重,每个 GroupId 只计算一次积分
// 4. 状态判定:
// - 未开始:无任何 GroupId 被作答
// - 进行中:有 GroupId 被作答但未全部完成
// - 已完成:所有 GroupId 都完成
// ============================================
#region 1. 查询用户作答记录
// 1. 查询用户的所有作答记录
var userAnswers = await answerRepository.Context.Queryable()
.Where(a => a.UserId == userId && !a.IsDeleted)
.ToListAsync();
#endregion
#region 2. 收集期刊ID(绑定+作答去重)
// ============================================
// 2. 收集所有需要展示的期刊ID
// - 用户绑定的杂志(UserJournal表)
// - 用户有作答记录的杂志
// 合并去重
// ============================================
var boundJournalIds = await userJournalRepository.Context.Queryable()
.Where(uj => uj.UserId == userId && !uj.IsDeleted)
.Select(uj => uj.JournalId)
.ToListAsync();
var answeredJournalIds = userAnswers.Select(a => a.JournalId).Distinct().ToList();
var journalIds = boundJournalIds
.Concat(answeredJournalIds)
.Distinct()
.ToList();
if (journalIds.Count == 0)
{
return new List();
}
#endregion
#region 3. 批量预加载关联数据
// 3. 批量预加载关联实体(按期刊维度全量加载,避免N+1查询)
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();
// 加载这些期刊的所有任务
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();
#endregion
#region 4. 查询连续打卡天数
// 4. 查询用户连续打卡天数
var consecutiveDays = await CalculateConsecutiveDaysAsync(userId);
#endregion
#region 5. 提取用户已作答/已完成任务ID集合
// 5. 提取用户已作答的任务ID集合(包含进行中和已完成)
var userAnsweredTaskIds = userAnswers.Select(a => a.JournalPageTaskId).ToHashSet();
var userAnsweredPageIds = userAnswers.Select(a => a.JournalPageId).ToHashSet();
// 用户已完成的任务ID集合(Status=1表示已完成)
var userCompletedTaskIds = userAnswers.Where(a => a.Status == (int)UserAnswerStatusEnum.Complete).Select(a => a.JournalPageTaskId).ToHashSet();
#endregion
#region 6. 按期刊分组聚合并返回结果
// 6. 按期刊分组聚合
var result = new List();
foreach (var journal in journals)
{
// 该期刊下所有任务
var journalTasks = allTasks.Where(t => t.JournalId == journal.Id).ToList();
// 该期刊的一级栏目数
var catalogCount = allCatalogs.Count(c => c.JournalId == journal.Id);
// 【跨页题目-进度计算】总任务数:按 GroupId 去重,跨页题目只算一个
var totalTaskCount = journalTasks.Select(t => t.GroupId).Distinct().Count();
// 【跨页题目-已完成判断】已完成任务数:按 GroupId 去重
// 关键:必须该 GroupId 下所有任务都完成,才算该 GroupId 完成
var completedTaskCount = journalTasks
.GroupBy(t => t.GroupId)
.Where(g => g.All(t => userCompletedTaskIds.Contains(t.Id)))
.Count();
// 【跨页题目-积分计算】获取已完成的 GroupId 列表
var completedGroupIds = journalTasks
.GroupBy(t => t.GroupId)
.Where(g => g.All(t => userCompletedTaskIds.Contains(t.Id)))
.Select(g => g.Key)
.ToList();
// 【跨页题目-积分去重】每个已完成的 GroupId 只取第一个任务的积分
// 原因:同一个跨页题目的多个任务只应该计算一次积分
var distinctCompletedTasks = journalTasks
.Where(t => completedGroupIds.Contains(t.GroupId))
.GroupBy(t => t.GroupId)
.Select(g => g.First())
.ToList();
// 累计已完成的跨页题目的总成长值和总积分
var totalGrowthPoints = distinctCompletedTasks.Sum(t => t.GrowthPoint ?? 0);
var totalPoints = distinctCompletedTasks.Sum(t => t.Points);
// 【跨页题目-进度百分比计算】
var totalGroupIds = journalTasks.Select(t => t.GroupId).Distinct().Count();
var completedGroupIdCount = journalTasks
.GroupBy(t => t.GroupId)
.Where(g => g.All(t => userCompletedTaskIds.Contains(t.Id)))
.Count();
var progress = totalGroupIds > 0
? Math.Round((double)completedGroupIdCount / totalGroupIds * 100, 2)
: 0;
// 剩余篇数:该期刊总页数 - 用户已作答页数
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;
#endregion
}
///
/// ============================================
/// 方法思路:计算用户连续打卡天数
/// 1. 获取今天和昨天的日期
/// 2. 查询昨天是否有打卡记录
/// 3. 如果昨天没有打卡记录,返回0(连续打卡中断)
/// 4. 如果昨天有打卡记录:
/// - 查询今天是否有打卡记录
/// - 今天有记录:返回今天的连续天数
/// - 今天无记录:返回昨天的连续天数
/// ============================================
///
/// 用户ID
/// 连续打卡天数
private async Task CalculateConsecutiveDaysAsync(long userId)
{
var today = DateTime.Now.Date;
var yesterday = today.AddDays(-1);
// 查询昨天是否有打卡记录
var yesterdayRecord = await checkInRecordRepository.Context.Queryable()
.Where(r => r.UserId == userId && !r.IsDeleted && r.CheckInDate.Date == yesterday)
.FirstAsync();
// 如果昨天没有打卡记录,连续天数为0(连续打卡中断)
if (yesterdayRecord == null)
{
return 0;
}
// 昨天有打卡记录,检查今天是否已打卡
var todayRecord = await checkInRecordRepository.Context.Queryable()
.Where(r => r.UserId == userId && !r.IsDeleted && r.CheckInDate.Date == today)
.FirstAsync();
// 今天已打卡:返回今天的连续天数;今天未打卡:返回昨天的连续天数
return todayRecord?.ConsecutiveDays ?? yesterdayRecord.ConsecutiveDays;
}
///
/// ============================================
/// 方法思路:获取目录列表(按一级目录分组展示)
/// 1. 批量预加载:该期刊下所有目录、页面、任务、用户作答记录
/// 2. 目录结构处理:
/// - 一级目录:ParentId=0 且 Type=Unit
/// - 二级目录:ParentId=一级目录Id 且 Type=Unit
/// - 文章目录:Type=Page
/// 3. 【关键】如果一级目录下没有二级栏目(Type=Unit的子目录),
/// 则该一级目录不参与任何计算,直接跳过
/// 4. 跨页题目处理:
/// - 状态判断:按GroupId去重,已完成条件是该GroupId下所有任务都完成(Status=1)
/// - 积分计算:按GroupId去重,每个GroupId只计算一次积分
/// - 已作答判断:只要有一个任务作答就算该GroupId已作答(包含进行中和已完成)
/// 5. 状态判定:
/// - 未开始:无任何GroupId被作答
/// - 进行中:有GroupId被作答但未全部完成
/// - 已完成:所有GroupId都完成
/// ============================================
///
/// 期刊ID
/// 用户ID
/// 目录列表(按一级目录分组)
public async Task> GetCatalogListAsync(long journalId, long userId)
{
#region 1. 查询期刊
// 1. 查询期刊
var journal = await journalRepository.Context.Queryable()
.Where(j => j.Id == journalId && !j.IsDeleted)
.FirstAsync();
if (journal == null)
{
return new List();
}
#endregion
#region 2. 批量加载关联数据
// 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();
#endregion
#region 3. 用户已作答/已完成任务ID集合
// 3. 用户已作答的任务ID集合(包含进行中和已完成)
var userAnsweredTaskIds = userAnswers.Select(a => a.JournalPageTaskId).ToHashSet();
// 用户已完成的任务ID集合(Status=1表示已完成)
var userCompletedTaskIds = userAnswers.Where(a => a.Status == (int)UserAnswerStatusEnum.Complete).Select(a => a.JournalPageTaskId).ToHashSet();
#endregion
#region 4. 按一级目录分组计算
// 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();
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)
// ============================================
int articleCount = 0;
var children = new List();
if (secondLevelCatalogs.Count > 0)
{
// 有二级单元:按二级单元分组
// 先统计一级栏目下直接的文章数(Type=Page,ParentId=一级栏目Id)
articleCount = pageCatalogs.Count;
foreach (var secondCatalog in secondLevelCatalogs)
{
// 二级单元下的 Type=Page 子目录
var subPageCatalogs = allCatalogs
.Where(c => c.ParentId == secondCatalog.Id && c.Type == JournalCatalogTypeEnum.Page)
.ToList();
// 累加该二级栏目下的文章数
articleCount += subPageCatalogs.Count;
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已完成,跨页题目按GroupId去重)
// 跨页题目必须所有任务都完成,才算该 GroupId 已完成
string status;
if (secondLevelTasks.Count == 0)
{
status = "未开始";
}
else
{
// 总 GroupId 数(跨页题目只算一个)
var totalGroupCount = secondLevelTasks.Select(t => t.GroupId).Distinct().Count();
// 已完成 GroupId 数(该 GroupId 下所有任务都完成才算完成)
var completedGroupCount = secondLevelTasks
.GroupBy(t => t.GroupId)
.Where(g => g.All(t => userCompletedTaskIds.Contains(t.Id)))
.Count();
// 【关键】已作答 GroupId 数:只要有一个任务作答就算该 GroupId 已作答(包含进行中和已完成)
// 这个字段用于判断状态:未开始 / 进行中 / 已完成
var answeredGroupCount = secondLevelTasks
.Where(t => userAnsweredTaskIds.Contains(t.Id))
.Select(t => t.GroupId)
.Distinct()
.Count();
// 状态判定逻辑:
// - answeredGroupCount == 0:没有任何 GroupId 被作答 → 未开始
// - completedGroupCount == totalGroupCount:所有 GroupId 都完成 → 已完成
// - 其他:有 GroupId 被作答但未全部完成 → 进行中
if (answeredGroupCount == 0)
status = "未开始";
else if (completedGroupCount == totalGroupCount)
status = "已完成";
else
status = "进行中";
}
// 计算该二级目录的总成长值和总积分(跨页题目按GroupId去重)
var distinctSecondLevelTasks = secondLevelTasks
.GroupBy(t => t.GroupId)
.Select(g => g.First())
.ToList();
var totalGrowthPoints = distinctSecondLevelTasks.Sum(t => t.GrowthPoint) ?? 0;
var totalPoints = distinctSecondLevelTasks.Sum(t => t.Points);
children.Add(new SecondCatalogOutput
{
CatalogId = secondCatalog.Id,
CatalogName = secondCatalog.Name ?? string.Empty,
TotalPoints = totalPoints,
TotalGrowthPoints = totalGrowthPoints,
Status = status
});
}
}
else
{
// 没有二级单元:只统计一级栏目下直接的文章数,Children为空集合
articleCount = pageCatalogs.Count;
// Children保持为空集合,不添加任何子项
}
// 计算该一级目录的总成长值和总积分(基于所有文章页面的任务)
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
});
}
#endregion
#region 5. 返回结果
return result;
#endregion
}
///
/// ============================================
/// 方法思路:获取一级目录概览(含二级目录详情和任务完成状态)
/// 1. 批量预加载:该期刊下所有目录、页面、任务、用户作答记录
/// 2. 获取该一级目录下的所有文章目录(Type=Page)
/// 3. 跨页题目处理:
/// - 总任务数:按GroupId去重(跨页题目只算一个)
/// - 状态判断:按GroupId去重,已完成条件是该GroupId下所有任务都完成(Status=1)
/// - 已作答判断:只要有一个任务作答就算该GroupId已作答
/// 4. 状态格式:"已完成数/总数 已完成/进行中/未开始"
/// 例如:"2/3 进行中" 表示3个GroupId中已完成2个
/// ============================================
///
/// 一级目录ID
/// 用户ID
/// 一级目录概览信息
public async Task GetCatalogOverviewAsync(long catalogId, long userId)
{
#region 1. 查询一级目录
// 1. 查询一级目录
var firstCatalog = await catalogRepository.Context.Queryable()
.Where(c => c.Id == catalogId && !c.IsDeleted)
.FirstAsync();
if (firstCatalog == null)
{
return null;
}
#endregion
#region 2. 批量加载关联数据
// 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();
#endregion
#region 3. 用户已作答/已完成任务ID集合
// 3. 用户已作答的任务ID集合(包含进行中和已完成)
var userAnsweredTaskIds = userAnswers.Select(a => a.JournalPageTaskId).ToHashSet();
// 用户已完成的任务ID集合(Status=1表示已完成)
var userCompletedTaskIds = userAnswers.Where(a => a.Status == (int)UserAnswerStatusEnum.Complete).Select(a => a.JournalPageTaskId).ToHashSet();
#endregion
#region 4. 获取一级栏目下所有Type=Page子目录
// 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;
#endregion
#region 5. 总任务数与子目录详情
// 6. 获取所有文章目录下的页面
var pageCatalogIds = pageCatalogs.Select(c => c.Id).ToHashSet();
var allLevelPages = allPages.Where(p => pageCatalogIds.Contains(p.JournalCatalogId)).ToList();
var allLevelPageIds = allLevelPages.Select(p => p.Id).ToHashSet();
// 7. 总任务数(按 GroupId 去重,跨页题目只算一个)
var totalTaskCount = allTasks
.Where(t => allLevelPageIds.Contains(t.JournalPageId))
.Select(t => t.GroupId)
.Distinct()
.Count();
// 8. 构建子目录详情列表
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();
// 总 GroupId 数(按 GroupId 去重,跨页题目只算一个)
var totalGroupCount = articleTasks.Select(t => t.GroupId).Distinct().Count();
// 页码范围
var startPage = articlePages.Count > 0 ? articlePages.Min(p => p.PageNum) : 0;
var endPage = articlePages.Count > 0 ? articlePages.Max(p => p.PageNum) : 0;
// 状态格式:1/2 进行中、2/2 已完成、0/2 未开始(跨页题目按GroupId去重)
// 跨页题目必须所有任务都完成,才算该 GroupId 已完成
string status;
if (totalGroupCount == 0)
{
status = $"0/0 未开始";
}
else
{
// 已完成 GroupId 数(该 GroupId 下所有任务都完成才算完成)
var completedGroupCount = articleTasks
.GroupBy(t => t.GroupId)
.Where(g => g.All(t => userCompletedTaskIds.Contains(t.Id)))
.Count();
// 已作答 GroupId 数(只要有一个任务作答就算已作答,包含进行中和已完成)
var answeredGroupCount = articleTasks
.Where(t => userAnsweredTaskIds.Contains(t.Id))
.Select(t => t.GroupId)
.Distinct()
.Count();
if (answeredGroupCount == 0)
status = $"0/{totalGroupCount} 未开始";
else if (completedGroupCount == totalGroupCount)
status = $"{completedGroupCount}/{totalGroupCount} 已完成";
else
status = $"{completedGroupCount}/{totalGroupCount} 进行中";
}
subCatalogs.Add(new SubCatalogDetailOutput
{
CatalogId = pageCatalog.Id,
SecondLevelCatalogName = pageCatalog.Name ?? string.Empty,
StartPage = startPage,
EndPage = endPage,
TaskCount = totalGroupCount,
Status = status
});
}
#endregion
#region 6. 返回结果
return new CatalogOverviewOutput
{
CatalogId = firstCatalog.Id,
FirstLevelCatalogName = firstCatalog.Name ?? string.Empty,
PageCount = articleCount,
TotalTaskCount = totalTaskCount,
SubCatalogs = subCatalogs
};
#endregion
}
///
/// ============================================
/// 方法思路:获取用户答题任务列表
/// 1. 根据二级栏目ID查询栏目基本信息(一级栏目名称、栏目名称、页码范围
/// 2. 按GroupId分组,每个GroupId对应一个任务项
/// 3. 每个任务项包含:任务名称、总成长值、总积分、状态
/// 4. 【跨页题目处理】
/// - 是否跨页:Id != GroupId
/// - 任务名称:取 Id == GroupId 的主任务的Task字段
/// - 成长值/积分:取主任务的,每个GroupId只算一次
/// - 状态判断:必须该GroupId下所有任务都完成(Status=1)才算该GroupId完成
/// ============================================
/// 二级栏目ID/param>
/// 用户ID
///
/// 用户答题任务列表信息
public async Task GetUserAnswerTaskListAsync(long catalogId, long userId)
{
#region 1. 查询二级栏目目录
// 1. 查询目录(二级栏目)
var catalog = await catalogRepository.Context.Queryable()
.Where(c => c.Id == catalogId && !c.IsDeleted)
.FirstAsync();
if (catalog == null)
{
return null;
}
#endregion
#region 2. 获取一级栏目名称
// ============================================
// 【获取一级栏目名称】
// 通过 ParentId 关联查询一级栏目名称
// ============================================
string firstLevelCatalogName = string.Empty;
if (catalog.ParentId > 0)
{
var firstLevelCatalog = await catalogRepository.Context.Queryable()
.Where(c => c.Id == catalog.ParentId && !c.IsDeleted)
.FirstAsync();
firstLevelCatalogName = firstLevelCatalog?.Name ?? string.Empty;
}
#endregion
#region 3. 批量加载关联数据
// ============================================
// 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();
#endregion
#region 4. 获取该目录下的页面ID及任务
// 3. 获取该目录下的所有页面ID
var pageIds = allPages.Select(p => p.Id).ToHashSet();
// 4. 该目录下的所有任务(仅用于筛选哪些GroupId属于当前栏目)
var catalogTasks = allTasks.Where(t => pageIds.Contains(t.JournalPageId)).ToList();
// 【跨页题目-关键】
// 跨页题目的任务可能分布在不同页面(甚至不同栏目)下
// 所以需要从整个期刊的所有任务(allTasks)中按GroupId获取完整的任务组
// 否则无法正确判断跨页题目是否全部完成
var catalogGroupIds = catalogTasks.Select(t => t.GroupId).ToHashSet();
// 5. 用户已作答的任务ID集合(包含进行中和已完成)
var userAnsweredTaskIds = userAnswers.Select(a => a.JournalPageTaskId).ToHashSet();
// 用户已完成的任务ID集合(Status=1表示已完成)
var userCompletedTaskIds = userAnswers.Where(a => a.Status == (int)UserAnswerStatusEnum.Complete).Select(a => a.JournalPageTaskId).ToHashSet();
// 6. 计算页码范围
var startPage = allPages.Count > 0 ? allPages.Min(p => p.PageNum) : 0;
var endPage = allPages.Count > 0 ? allPages.Max(p => p.PageNum) : 0;
#endregion
#region 5. 按GroupId分组生成任务列表
// ============================================
// 【跨页题目-按GroupId分组生成任务列表】
// 关键:从整个期刊的所有任务中按GroupId取完整分组
// 每个GroupId对应一个任务项
// - 任务名称:取 Id == GroupId 的主任务的Task字段
// - 成长值/积分:取主任务的,每个GroupId只算一次
// - 状态:
// - 未开始:该GroupId下无任何任务被作答
// - 已完成:该GroupId下所有任务都完成(Status=1)
// - 进行中:有作答但未全部完成
// ============================================
var taskItems = new List();
// 从整个期刊的所有任务中,筛选出属于当前栏目的GroupId的完整分组
var groupTasks = allTasks
.Where(t => catalogGroupIds.Contains(t.GroupId))
.GroupBy(t => t.GroupId)
.ToList();
foreach (var group in groupTasks)
{
var groupId = group.Key;
// 取主任务(Id == GroupId)
var mainTask = group.FirstOrDefault(t => t.Id == groupId) ?? group.First();
// 计算该GroupId下所有任务的答题时间(秒)
var taskIds = group.Select(t => t.Id).ToHashSet();
var groupAnswerSeconds = userAnswers
.Where(a => taskIds.Contains(a.JournalPageTaskId))
.Sum(a => a.AnswerSeconds);
var groupAnswerMinutes = Math.Round(groupAnswerSeconds / 60.0, 2);
// 判断该GroupId是否有作答记录
bool hasAnswer = group.Any(t => userAnsweredTaskIds.Contains(t.Id));
// 判断该GroupId是否全部完成
bool isAllCompleted = group.All(t => userCompletedTaskIds.Contains(t.Id));
// 计算状态
string taskStatus;
if (!hasAnswer)
taskStatus = "未开始";
else if (isAllCompleted)
taskStatus = "已完成";
else
taskStatus = "进行中";
taskItems.Add(new UserAnswerTaskItemOutput
{
GroupId = groupId,
TaskName = mainTask.Task ?? string.Empty,
TotalGrowthPoints = mainTask.GrowthPoint ?? 0,
TotalPoints = mainTask.Points,
AnswerMinutes = groupAnswerMinutes,
Status = taskStatus
});
}
#endregion
#region 6. 返回结果
return new UserAnswerTaskListOutput
{
FirstLevelCatalogName = firstLevelCatalogName,
CatalogName = catalog.Name ?? string.Empty,
StartPage = startPage,
EndPage = endPage,
Tasks = taskItems
};
#endregion
}
///
/// ============================================
/// 方法思路:获取跨页题目详情
/// 1. 根据 groupId 查询该分组下的所有任务
/// 2. 跨页题目判定:
/// - 任务数 > 1 → 跨页题目
/// - 任务数 == 1 且 Id != groupId → 跨页题目(子任务进入)
/// - 任务数 == 1 且 Id == groupId → 普通题目
/// 3. 图片数组获取逻辑:
/// - 已答题:使用用户上传的 PageAnswerUrl 数组
/// - 未答题:使用任务的 TaskUrl 数组补齐
/// 4. 栏目名称获取:
/// - 二级栏目名称:通过页面关联的 JournalCatalogId 查询
/// - 一级栏目名称:通过二级栏目的 ParentId 查询
/// 5. 是否可领取积分判断:
/// - 任务必须已完成(Status=1)
/// - 跨页题目:所有任务都完成才能领取
/// - 普通题目:当前任务完成即可领取
/// - 且未领取过积分
/// ============================================
///
/// 任务分组ID(GroupId)
/// 用户ID
/// 跨页题目详情
public async Task GetCrossPageTaskAsync(long groupId, long userId)
{
// 1. 查询该 GroupId 下的所有任务
var allTasks = await taskRepository.Context.Queryable()
.Where(t => t.GroupId == groupId && !t.IsDeleted)
.OrderBy(t => t.Id)
.ToListAsync();
if (allTasks == null || allTasks.Count == 0)
{
return null;
}
#region 1. 跨页题目判定
// ============================================
// 【跨页题目判定】
// 判断标准:
// - 任务数 > 1 → 跨页题目
// - 任务数 == 1 且 Id != groupId → 跨页题目(子任务进入)
// - 任务数 == 1 且 Id == groupId → 普通题目
// ============================================
bool isCrossPage = allTasks.Count > 1 || (allTasks.Count == 1 && allTasks[0].Id != groupId);
// 获取主任务(Id == GroupId),用于跨页题目获取任务名称、积分等
var mainTask = allTasks.FirstOrDefault(t => t.Id == groupId);
// 获取任一任务作为基础信息(用于栏目查询等)
var baseTask = allTasks[0];
#endregion
#region 2. 获取相关任务列表
// ============================================
// 【获取相关任务列表】
// 跨页题目:allTasks 即为相关任务列表
// 非跨页题目:单任务列表
// ============================================
List relatedTasks = allTasks;
#endregion
#region 3. 获取栏目名称(一级/二级)
// ============================================
// 【获取栏目名称】
// 二级栏目名称:通过页面关联的 JournalCatalogId 查询
// 一级栏目名称:通过二级栏目的 ParentId 查询
// ============================================
var page = await pageRepository.Context.Queryable()
.Where(p => p.Id == baseTask.JournalPageId && !p.IsDeleted)
.FirstAsync();
string catalogName = string.Empty;
string firstLevelCatalogName = string.Empty;
if (page != null)
{
// 获取二级栏目
var catalog = await catalogRepository.Context.Queryable()
.Where(c => c.Id == page.JournalCatalogId && !c.IsDeleted)
.FirstAsync();
catalogName = catalog?.Name ?? string.Empty;
// 获取一级栏目名称
if (catalog != null && catalog.ParentId > 0)
{
var firstLevelCatalog = await catalogRepository.Context.Queryable()
.Where(c => c.Id == catalog.ParentId && !c.IsDeleted)
.FirstAsync();
firstLevelCatalogName = firstLevelCatalog?.Name ?? string.Empty;
}
}
#endregion
#region 4. 获取图片数组(Images)
// ============================================
// 【获取图片数组】
// 核心逻辑:已答题返回 PageAnswerUrl,未答题返回 TaskUrl 补齐
// 跨页题目:
// - 用户答了部分:已答页用 PageAnswerUrl,未答页用 TaskUrl 补齐
// - 用户全部作答:全部用 PageAnswerUrl
// - 用户未作答:全部用 TaskUrl
// - 按主页面页码(PageNum)升序排列,确保拼图顺序正确
// 非跨页题目:取单条答题记录的 PageAnswerUrl,未答题返回空集合
// ============================================
var images = new List();
// 统一查询用户答题记录(用于图片获取和后续领取积分判断)
List userAnswers;
if (isCrossPage)
{
// 跨页题目:查询该 GroupId 下所有用户的答题记录
userAnswers = await answerRepository.Context.Queryable()
.Where(a => a.JournalPageTaskGroupId == groupId && a.UserId == userId && !a.IsDeleted)
.OrderBy(a => a.JournalPageTaskId)
.ToListAsync();
// ============================================
// 【跨页题目图片获取逻辑】
// 1. 收集相关任务所在页面的页码信息
// 2. 遍历每个相关任务:已答用 PageAnswerUrl,未答用 TaskUrl 补齐
// 3. 按页码升序排序后输出,确保拼图顺序正确
// ============================================
// 查询相关任务对应的页面信息(用于排序)
var pageIds = relatedTasks.Select(t => t.JournalPageId).Distinct().ToList();
var pageInfoList = await pageRepository.Context.Queryable()
.Where(p => pageIds.Contains(p.Id) && !p.IsDeleted)
.ToListAsync();
var pageInfoDict = pageInfoList.ToDictionary(p => p.Id, p => p.PageNum);
// 组装图片项:(页码, 图片地址)
var imageItems = new List<(int PageNum, string Url)>();
foreach (var relatedTask in relatedTasks)
{
var pageNum = pageInfoDict.TryGetValue(relatedTask.JournalPageId, out var num) ? num : 0;
var userAnswer = userAnswers.FirstOrDefault(a => a.JournalPageTaskId == relatedTask.Id);
string imageUrl;
if (userAnswer != null && !string.IsNullOrEmpty(userAnswer.PageAnswerUrl))
{
// 已答题:使用 PageAnswerUrl
imageUrl = userAnswer.PageAnswerUrl;
}
else if (!string.IsNullOrEmpty(relatedTask.TaskUrl))
{
// 未答题或无 PageAnswerUrl:使用任务的 TaskUrl 补齐
imageUrl = relatedTask.TaskUrl;
}
else
{
// 无任何图片可用,跳过
continue;
}
imageItems.Add((pageNum, imageUrl));
}
// 按页码升序排序后输出图片地址
images.AddRange(imageItems.OrderBy(x => x.PageNum).Select(x => x.Url));
}
else
{
// 非跨页题目:查询单条答题记录
var userAnswer = await answerRepository.Context.Queryable()
.Where(a => a.JournalPageTaskId == mainTask.Id && a.UserId == userId && !a.IsDeleted)
.FirstAsync();
// 赋值给 userAnswers(用于后续领取积分判断)
userAnswers = userAnswer != null ? new List { userAnswer } : new List();
if (userAnswer != null && !string.IsNullOrEmpty(userAnswer.PageAnswerUrl))
{
// 已答题,使用 PageAnswerUrl
images.Add(userAnswer.PageAnswerUrl);
}
// 未答题不返回任何图片,images 保持空集合
}
#endregion
#region 5. 获取页面作答点读图片集合(PageAnswerDotUrl)
// ============================================
// 【获取页面作答点读图片地址集合】
// 规则:已答题且 PageAnswerDotUrl 有值才返回;未答题或字段为空则跳过
// 跨页题目:遍历所有相关任务,收集有答题记录的 PageAnswerDotUrl
// 非跨页题目:取当前任务答题记录的 PageAnswerDotUrl(仅在有值时返回)
// ============================================
var pageAnswerDotUrls = new List();
if (isCrossPage)
{
// 跨页题目:遍历相关任务,收集有答题记录的 PageAnswerDotUrl
foreach (var relatedTask in relatedTasks)
{
var userAnswer = userAnswers.FirstOrDefault(a => a.JournalPageTaskId == relatedTask.Id);
if (userAnswer != null && !string.IsNullOrEmpty(userAnswer.PageAnswerDotUrl))
{
pageAnswerDotUrls.Add(userAnswer.PageAnswerDotUrl);
}
}
}
else
{
// 非跨页题目:取当前任务答题记录的 PageAnswerDotUrl
// 仅在已答题且 PageAnswerDotUrl 有值时返回
if (userAnswers != null && userAnswers.Count > 0)
{
var userAnswer = userAnswers.FirstOrDefault();
if (userAnswer != null && !string.IsNullOrEmpty(userAnswer.PageAnswerDotUrl))
{
pageAnswerDotUrls.Add(userAnswer.PageAnswerDotUrl);
}
}
}
#endregion
#region 6. 判断是否可领取积分
// ============================================
// 【是否可领取积分判断】
// 条件:任务已完成(Status=1)且未领取过积分
// 跨页题目:所有任务都完成才能领取
// 普通题目:当前任务完成即可领取
// ============================================
bool canClaimPoints = false;
// 查询是否已领取过积分
var existingRecord = await answerRepository.Context.Queryable()
.Where(r => r.UserId == userId && !r.IsDeleted)
.Where(r => r.ChangeType == PointsChangeTypeEnum.TaskReward.ToString() && r.RelatedId == groupId)
.AnyAsync();
if (!existingRecord)
{
// 未领取过积分,检查任务是否完成
if (isCrossPage)
{
// 跨页题目:检查该 GroupId 下所有任务是否都完成(Status=1)
var allCompleted = relatedTasks.All(t =>
userAnswers.Any(a => a.JournalPageTaskId == t.Id && a.Status == (int)UserAnswerStatusEnum.Complete));
canClaimPoints = allCompleted;
}
else
{
// 普通题目:检查当前任务是否完成(Status=1)
var userAnswer = await answerRepository.Context.Queryable()
.Where(a => a.JournalPageTaskId == mainTask.Id && a.UserId == userId && !a.IsDeleted)
.FirstAsync();
canClaimPoints = userAnswer?.Status == (int)UserAnswerStatusEnum.Complete;
}
}
#endregion
#region 7. 获取任务名称
// ============================================
// 【获取任务名称】
// 跨页题目:取主任务(Id == GroupId)的 Task 字段
// 普通题目:取当前任务的 Task 字段
// ============================================
string taskName;
if (isCrossPage)
{
taskName = mainTask?.Task ?? string.Empty;
}
else
{
taskName = mainTask?.Task ?? string.Empty;
}
#endregion
#region 8. 返回结果
return new CrossPageTaskOutput
{
FirstLevelCatalogName = firstLevelCatalogName,
CatalogName = catalogName,
TaskName = taskName,
Images = images,
PageAnswerDotUrl = pageAnswerDotUrls,
IsCrossPage = isCrossPage,
CanClaimPoints = canClaimPoints
};
#endregion
}
///
/// ============================================
/// 方法思路:领取任务积分
/// 1. 根据 GroupId 查询任务信息,判断是否跨页题目
/// - 普通题目:该 GroupId 下只有1个任务,且 Id == GroupId
/// - 跨页题目:该 GroupId 下有多个任务,或 Id != GroupId
/// 2. 通过 groupId 查询用户答题记录
/// 3. 跨页题目完成校验:必须该 GroupId 下所有任务都完成(Status=1)
/// 普通题目校验:只需要该任务完成(Status=1)
/// 4. 重复领取校验:通过 PointsRecord 表的 RelatedId + ChangeType 判断
/// 5. 积分计算:
/// - 跨页题目:累计该 GroupId 下所有任务的积分
/// - 普通题目:取当前任务的积分
/// 6. 得分计算:从 JournalPageTaskUserAnswer 表获取 Score
/// ============================================
///
/// 任务分组Id(JournalPageTask.GroupId)
/// 用户ID
/// 领取积分结果
public async Task ClaimTaskPointsAsync(long groupId, long userId)
{
#region 1. 根据GroupId查询所有相关任务
// ============================================
// 1. 根据 GroupId 查询所有相关任务
// ============================================
var relatedTasks = await taskRepository.Context.Queryable()
.Where(t => t.GroupId == groupId && !t.IsDeleted)
.ToListAsync();
if (relatedTasks == null || relatedTasks.Count == 0)
{
throw new BusinessException("任务不存在", 404);
}
#endregion
#region 2. 判断是否跨页题目
// ============================================
// 2. 判断是否跨页题目
// 普通题目:该 GroupId 下只有1个任务,且该任务的 Id == GroupId
// 跨页题目:该 GroupId 下有多个任务,或任务的 Id != GroupId
// ============================================
bool isCrossPage;
JournalPageTask mainTask;
if (relatedTasks.Count == 1 && relatedTasks[0].Id == groupId)
{
// 普通题目:只有1个任务,且 Id == GroupId
isCrossPage = false;
mainTask = relatedTasks[0];
}
else
{
// 跨页题目:有多个任务,或者 Id != GroupId
isCrossPage = true;
// 取 Id == GroupId 的任务作为主任务(跨页题目的第一条记录)
mainTask = relatedTasks.FirstOrDefault(t => t.Id == groupId) ?? relatedTasks[0];
}
#endregion
#region 3. 查询用户答题记录
// ============================================
// 3. 查询用户答题记录(统一通过 JournalPageTaskGroupId 查询)
// ============================================
var userAnswers = await answerRepository.Context.Queryable()
.Where(a => a.JournalPageTaskGroupId == groupId && a.UserId == userId && !a.IsDeleted)
.ToListAsync();
if (userAnswers == null || userAnswers.Count == 0)
{
throw new BusinessException("请先完成答题再领取积分", 400);
}
#endregion
#region 4. 跨页题目完成校验
// ============================================
// 4. 跨页题目完成校验
// 跨页题目必须该 GroupId 下所有任务都完成(Status=1)才能领取积分
// 普通题目只需要该任务完成(Status=1)即可领取
// ============================================
if (isCrossPage)
{
// 跨页题目:检查该 GroupId 下每个任务是否都有 Status=1 的答题记录
foreach (var task in relatedTasks)
{
var completedAnswer = userAnswers.FirstOrDefault(a => a.JournalPageTaskId == task.Id && a.Status == (int)UserAnswerStatusEnum.Complete);
if (completedAnswer == null)
{
throw new BusinessException("跨页题目需要完成所有任务才能领取积分", 400);
}
}
}
else
{
// 普通题目:只需要 Status=1 即可
if (userAnswers.FirstOrDefault()?.Status != (int)UserAnswerStatusEnum.Complete)
{
throw new BusinessException("请先完成答题再领取积分", 400);
}
}
#endregion
#region 5. 积分计算
// ============================================
// 5. 积分计算
// 【关键】跨页题目所有计算都只算一次(取 JournalPageTaskId == JournalPageTaskGroupId 的值)
// 跨页题目:只取主任务(Id == GroupId)的积分,不是累加所有任务的积分
// 普通题目:取主任务的积分
// ============================================
int totalPoints = 0;
if (isCrossPage)
{
// 跨页题目:只取主任务(Id == GroupId)的积分
totalPoints = mainTask.Points;
}
else
{
// 普通题目:取主任务的积分
totalPoints = mainTask.Points;
}
if (totalPoints <= 0)
{
throw new BusinessException("该任务无积分可领取", 400);
}
#endregion
#region 6. 得分计算
// ============================================
// 6. 得分计算
// 从 JournalPageTaskUserAnswer 表获取 Score
// 【关键】Score 取 JournalPageTaskId == JournalPageTaskGroupId 的分数
// 跨页题目:取主任务(Id == GroupId)的分数,不是累加所有任务的分数
// 普通题目:取当前任务对应答题记录的分数
// ============================================
float totalScore = 0;
if (isCrossPage)
{
// 跨页题目:取 JournalPageTaskId == JournalPageTaskGroupId 的分数
totalScore = userAnswers.FirstOrDefault(a => a.JournalPageTaskId == groupId)?.Score ?? 0;
}
else
{
// 普通题目:取当前任务对应答题记录的分数
totalScore = userAnswers.FirstOrDefault()?.Score ?? 0;
}
#endregion
#region 7. 重复领取校验
// ============================================
// 7. 重复领取校验
// 通过 PointsRecord 表的 RelatedId + ChangeType 判断是否已领取
// 使用 GroupId 作为 RelatedId
// ============================================
var existingRecord = await answerRepository.Context.Queryable()
.Where(r => r.UserId == userId && !r.IsDeleted)
.Where(r => r.ChangeType == PointsChangeTypeEnum.TaskReward.ToString() && r.RelatedId == groupId)
.FirstAsync();
if (existingRecord != null)
{
throw new BusinessException("积分已领取,请勿重复领取", 400);
}
#endregion
#region 8. 添加积分记录并返回结果
// ============================================
// 8. 调用积分服务添加积分记录
// ============================================
var addPointsResult = await pointsService.AddPointsAsync(new AddPointsInput
{
UserId = userId,
Amount = totalPoints,
ChangeType = PointsChangeTypeEnum.TaskReward,
RelatedId = groupId,
Description = $"完成任务奖励:{mainTask.Task}",
OperatorName = "系统"
});
// ============================================
// 【任务编号计算】
// 从 JournalPageTask.No 字段按 - 分割,取索引为 2 的部分
// No 最多出现索引为 3(即最多4段,0-3)
// 例如:"A-B-C-D" → 分割后 ["A","B","C","D"] → 取索引2 = "C"
// ============================================
string taskNo = string.Empty;
if (!string.IsNullOrEmpty(mainTask.No))
{
var parts = mainTask.No.Split('-');
if (parts.Length >= 3)
{
taskNo = parts[2];
}
}
return new ClaimTaskPointsOutput
{
RecordId = addPointsResult.RecordId,
PreviousBalance = addPointsResult.PreviousBalance,
NewBalance = addPointsResult.NewBalance,
AddedAmount = addPointsResult.AddedAmount,
TaskName = mainTask.Task ?? string.Empty,
TaskNo = !string.IsNullOrWhiteSpace(taskNo) ? "任务:"+ taskNo : string.Empty,
Score = totalScore
};
#endregion
}
///
/// ============================================
/// 方法思路:批量领取任务积分(一键领取)
/// 1. 遍历每个传入的 GroupId:
/// a. 根据 GroupId 查询所有相关任务,判断是否跨页题目
/// - 普通题目:该 GroupId 下只有1个任务,且 Id == GroupId
/// - 跨页题目:该 GroupId 下有多个任务,或 Id != GroupId
/// b. 通过 JournalPageTaskGroupId 查询用户答题记录
/// c. 跨页题目完成校验:必须该 GroupId 下所有任务都完成(Status=1)
/// d. 重复领取校验
/// e. 计算积分、成长值、得分:
/// - 跨页题目:所有计算都只算一次(第一个任务的积分/成长值/得分)
/// - 普通题目:正常计算
/// 2. 返回结果包含:
/// - 成功数量和失败数量
/// - 成功详情列表(每个GroupId的名称、积分、成长值、得分)
/// - 失败详情列表(每个GroupId的错误原因)
/// - 总积分、总成长值、领取后余额
/// ============================================
///
/// 批量领取输入(GroupId列表)
/// 用户ID
/// 批量领取结果
public async Task BatchClaimPointsAsync(BatchClaimPointsInput input, long userId)
{
#region 1. 初始化与参数校验
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();
#endregion
#region 2. 批量查询已领取的积分记录
// 1. 批量查询已领取的积分记录(用于重复领取校验)
var claimedRecords = await answerRepository.Context.Queryable()
.Where(r => groupIds.Contains((long)r.RelatedId!) && r.UserId == userId && !r.IsDeleted)
.Where(r => r.ChangeType == PointsChangeTypeEnum.TaskReward.ToString())
.ToListAsync();
var claimedGroupIds = claimedRecords.Select(r => r.RelatedId!.Value).ToHashSet();
#endregion
#region 3. 逐个处理每个 GroupId
// 2. 逐个处理每个 GroupId
int totalPoints = 0;
int totalGrowthPoints = 0;
int currentBalance = 0;
foreach (var groupId in groupIds)
{
try
{
#region 3.1 根据 GroupId 查询所有相关任务
// ============================================
// 2.1 根据 GroupId 查询所有相关任务
// ============================================
var relatedTasks = await taskRepository.Context.Queryable()
.Where(t => t.GroupId == groupId && !t.IsDeleted)
.ToListAsync();
if (relatedTasks == null || relatedTasks.Count == 0)
{
result.FailItems.Add(new BatchClaimFailItem
{
GroupId = groupId,
Message = "任务不存在"
});
continue;
}
#endregion
#region 3.2 判断是否跨页题目
// ============================================
// 2.2 判断是否跨页题目
// 普通题目:该 GroupId 下只有1个任务,且 Id == GroupId
// 跨页题目:有多个任务,或 Id != GroupId
// ============================================
bool isCrossPage;
JournalPageTask mainTask;
if (relatedTasks.Count == 1 && relatedTasks[0].Id == groupId)
{
// 普通题目
isCrossPage = false;
mainTask = relatedTasks[0];
}
else
{
// 跨页题目
isCrossPage = true;
// 取 Id == GroupId 的任务作为主任务
mainTask = relatedTasks.FirstOrDefault(t => t.Id == groupId) ?? relatedTasks[0];
}
#endregion
#region 3.3 查询用户答题记录
// ============================================
// 2.3 查询用户答题记录(统一通过 JournalPageTaskGroupId 查询)
// ============================================
var userAnswers = await answerRepository.Context.Queryable()
.Where(a => a.JournalPageTaskGroupId == groupId && a.UserId == userId && !a.IsDeleted)
.ToListAsync();
if (userAnswers == null || userAnswers.Count == 0)
{
result.FailItems.Add(new BatchClaimFailItem
{
GroupId = groupId,
Message = "请先完成答题再领取积分"
});
continue;
}
#endregion
#region 3.4 跨页题目完成校验
// ============================================
// 2.4 跨页题目完成校验
// 跨页题目必须该 GroupId 下所有任务都完成(Status=1)
// 普通题目只需要该任务完成(Status=1)
// ============================================
bool isCompleted = true; // 完成状态标志
if (isCrossPage)
{
// 跨页题目:检查该 GroupId 下每个任务是否都有 Status=1 的答题记录
foreach (var task in relatedTasks)
{
var completedAnswer = userAnswers.FirstOrDefault(a => a.JournalPageTaskId == task.Id && a.Status == (int)UserAnswerStatusEnum.Complete);
if (completedAnswer == null)
{
isCompleted = false;
break; // 发现未完成的任务,立即退出循环
}
}
if (!isCompleted)
{
result.FailItems.Add(new BatchClaimFailItem
{
GroupId = groupId,
Message = "跨页题目需要完成所有任务才能领取积分"
});
continue; // 跳出外层循环,处理下一个 groupId
}
}
else
{
// 普通题目:只需要 Status=1 即可
if (userAnswers.FirstOrDefault()?.Status != (int)UserAnswerStatusEnum.Complete)
{
result.FailItems.Add(new BatchClaimFailItem
{
GroupId = groupId,
Message = "请先完成答题再领取积分"
});
continue;
}
}
#endregion
#region 3.5 重复领取校验
// ============================================
// 2.5 重复领取校验
// ============================================
if (claimedGroupIds.Contains(groupId))
{
result.FailItems.Add(new BatchClaimFailItem
{
GroupId = groupId,
Message = "积分已领取,请勿重复领取"
});
continue;
}
#endregion
#region 3.6 计算积分、成长值、得分
// ============================================
// 2.6 计算积分、成长值、得分
// 【关键】跨页题目所有计算都只算一次(取 JournalPageTaskId == JournalPageTaskGroupId 的值)
// Score 取 JournalPageTaskId == JournalPageTaskGroupId 的分数,不是累加所有任务的分数
// ============================================
int points;
int growthPoints;
float score;
if (isCrossPage)
{
// 跨页题目:取 JournalPageTaskId == JournalPageTaskGroupId 的积分、成长值、得分
// mainTask 已经是 Id == GroupId 的任务,所以 mainTask.Id == groupId
points = mainTask.Points;
growthPoints = mainTask.GrowthPoint ?? 0;
// 跨页题目:取 JournalPageTaskId == JournalPageTaskGroupId 的分数
score = userAnswers.FirstOrDefault(a => a.JournalPageTaskId == groupId)?.Score ?? 0;
}
else
{
// 普通题目:正常计算
points = mainTask.Points;
growthPoints = mainTask.GrowthPoint ?? 0;
score = userAnswers.FirstOrDefault()?.Score ?? 0;
}
if (points <= 0)
{
result.FailItems.Add(new BatchClaimFailItem
{
GroupId = groupId,
Message = "该任务无积分可领取"
});
continue;
}
// 获取任务名称
var taskName = mainTask.Task ?? string.Empty;
// ============================================
// 【任务编号计算】
// 从 JournalPageTask.No 字段按 - 分割,取索引为 2 的部分
// No 最多出现索引为 3(即最多4段,0-3)
// 例如:"A-B-C-D" → 分割后 ["A","B","C","D"] → 取索引2 = "C"
// ============================================
string taskNo = string.Empty;
if (!string.IsNullOrEmpty(mainTask.No))
{
var parts = mainTask.No.Split('-');
if (parts.Length >= 3)
{
taskNo = parts[2];
}
}
#endregion
#region 3.7 调用积分服务并记录结果
// 调用积分服务增加积分
var addResult = await pointsService.AddPointsAsync(new AddPointsInput
{
UserId = userId,
Amount = points,
ChangeType = PointsChangeTypeEnum.TaskReward,
RelatedId = groupId,
Description = $"完成任务奖励:{taskName}",
OperatorName = "系统"
});
// 累计总积分和总成长值
totalPoints += points;
totalGrowthPoints += growthPoints;
currentBalance = addResult.NewBalance;
result.SuccessCount++;
// 添加成功详情到成功列表
result.SuccessItems.Add(new BatchClaimSuccessItem
{
GroupId = groupId,
TaskName = taskName,
TaskNo = !string.IsNullOrWhiteSpace(taskNo) ? "任务:" + taskNo : string.Empty,
Points = points,
GrowthPoints = growthPoints,
Score = score
});
#endregion
}
catch (Exception ex)
{
// 捕获异常并添加到失败列表,不中断其他任务的处理
result.FailItems.Add(new BatchClaimFailItem
{
GroupId = groupId,
Message = ex.Message
});
}
}
#endregion
#region 4. 组装返回结果
result.FailCount = result.FailItems.Count;
result.TotalPoints = totalPoints;
result.TotalGrowthPoints = totalGrowthPoints;
result.BalanceAfter = currentBalance;
return result;
#endregion
}
///
/// ============================================
/// 方法思路:获取杂志列表
/// 1. 查询所有已归档的杂志(Status=Archive)
/// 2. 查询用户已绑定的杂志ID列表
/// 3. 根据queryType过滤(大小写不敏感):
/// - "Bound":只返回已绑定的杂志
/// - "Unbound":只返回未绑定的杂志
/// - 其他:返回空集合
/// 4. 返回每个杂志的ID、名称、标题、封面、是否已绑定
/// ============================================
///
/// 用户ID
/// 查询类型:bound/unbound(大小写不敏感),其他返回空
/// 杂志列表
public async Task> GetJournalListAsync(long userId, string? queryType = "Bound")
{
#region 1. 查询所有已归档杂志
// 查询所有已归档且未删除的杂志
var allJournals = await journalRepository.Queryable()
.Where(j => !j.IsDeleted && j.Status == (int)JournalStatusEnum.Archive)
.OrderByDescending(j => j.CreatedAt)
.ToListAsync();
#endregion
#region 2. 查询用户已绑定杂志
// 查询用户已绑定的杂志ID列表
var userJournals = await userJournalRepository.Context.Queryable()
.Where(uj => uj.UserId == userId && !uj.IsDeleted)
.Select(uj => uj.JournalId)
.ToListAsync();
var boundJournalIds = userJournals.ToHashSet();
#endregion
#region 3. 根据queryType过滤
// ============================================
// 【大小写兼容处理 + 合法值校验】
// queryType 只接受两种值(大小写不敏感):
// - "Bound" / "bound" / "BOUND" → 返回已绑定杂志
// - "Unbound" / "unbound" / "UNBOUND" → 返回未绑定杂志
// - 空 / 其他值 → 返回空集合
// ============================================
var normalizedQueryType = queryType?.ToLowerInvariant() ?? string.Empty;
IEnumerable filteredJournals;
if (normalizedQueryType == "bound")
{
filteredJournals = allJournals.Where(j => boundJournalIds.Contains(j.Id));
}
else if (normalizedQueryType == "unbound")
{
filteredJournals = allJournals.Where(j => !boundJournalIds.Contains(j.Id));
}
else
{
// 非 Bound/Unbound 类型,返回空集合
filteredJournals = Enumerable.Empty();
}
#endregion
#region 4. 返回结果映射
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;
#endregion
}
///
/// ============================================
/// 方法思路:添加用户与杂志关联(绑定杂志)
/// 1. 验证杂志是否存在
/// 2. 检查用户是否已绑定过该杂志
/// - 查询 UserJournal 表,条件:UserId + JournalId
/// 3. 已绑定过:返回 false
/// 4. 未绑定:插入 UserJournal 记录,返回 true
/// ============================================
///
/// 杂志ID
/// 用户ID
/// 是否绑定成功(已绑定过返回false)
public async Task AddUserJournalAsync(long journalId, long userId)
{
#region 1. 验证杂志是否存在
var journal = await journalRepository.Queryable()
.Where(j => j.Id == journalId && !j.IsDeleted)
.FirstAsync();
if (journal == null)
{
throw new BusinessException("杂志不存在", 404);
}
#endregion
#region 2. 检查用户是否已绑定过该杂志
var exist = await userJournalRepository.Context.Queryable()
.Where(uj => uj.UserId == userId && uj.JournalId == journalId && !uj.IsDeleted)
.AnyAsync();
if (exist)
{
// 已绑定过,返回 false
return false;
}
#endregion
#region 3. 插入 UserJournal 记录
var userJournal = new UserJournal
{
UserId = userId,
JournalId = journalId,
Type = UserJournalTypeEnum.Subscribe
};
var result = await userJournalRepository.InsertAsync(userJournal);
return result;
#endregion
}
///
/// ============================================
/// 方法思路:新增学生
/// 1. 创建新的Users记录
/// 2. WxUserId设置为当前登录人ID(wxUserId参数传入)
/// 3. 初始化积分和成长值为0
/// 4. 用户类型设置为普通用户(Normal)
/// 5. 插入数据库并返回结果
/// ============================================
///
/// 新增学生输入(姓名、头像URL)
/// 当前登录人ID(微信用户ID)
/// 是否插入成功
public async Task AddStudentAsync(AddStudentInput input, long wxUserId)
{
#region 1. 创建学生记录
var student = new Users
{
WxUserId = wxUserId,
Name = input.Name,
AvatarUrl = input.AvatarUrl,
GrowthPoints = 0,
Points = 0,
Type = UsersTypeEnum.Normal,
IsLastOnline = false
};
#endregion
#region 2. 插入数据库
return await usersRepository.InsertAsync(student);
#endregion
}
}