2026-06-24 16:32:59 +08:00
|
|
|
|
using Microsoft.Extensions.Logging;
|
2026-07-01 08:18:07 +08:00
|
|
|
|
using QYZH.InteractiveMagazine.Common.Helpers;
|
2026-06-24 16:32:59 +08:00
|
|
|
|
using QYZH.InteractiveMagazine.IService;
|
|
|
|
|
|
using QYZH.InteractiveMagazine.Models.Common;
|
2026-06-29 16:34:26 +08:00
|
|
|
|
using QYZH.InteractiveMagazine.Models.Dto;
|
2026-06-24 16:32:59 +08:00
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 用户作答任务服务实现
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public class UserAnswerTaskService(
|
|
|
|
|
|
BaseRepository<JournalPageTaskUserAnswer> answerRepository,
|
|
|
|
|
|
BaseRepository<Journal> journalRepository,
|
|
|
|
|
|
BaseRepository<JournalCatalog> catalogRepository,
|
|
|
|
|
|
BaseRepository<JournalPage> pageRepository,
|
2026-06-29 15:09:23 +08:00
|
|
|
|
BaseRepository<JournalPageTask> taskRepository,
|
2026-06-24 16:32:59 +08:00
|
|
|
|
BaseRepository<UserJournal> userJournalRepository,
|
|
|
|
|
|
BaseRepository<Users> usersRepository,
|
|
|
|
|
|
BaseRepository<CheckInRecord> checkInRecordRepository,
|
2026-07-01 08:18:07 +08:00
|
|
|
|
BaseRepository<UserPet> userPetRepository,
|
2026-06-24 16:32:59 +08:00
|
|
|
|
IPointsService pointsService,
|
2026-07-01 08:18:07 +08:00
|
|
|
|
IPetService petService,
|
2026-06-24 16:32:59 +08:00
|
|
|
|
ILogger<UserAnswerTaskService> logger)
|
|
|
|
|
|
: BaseRepository<JournalPageTaskUserAnswer>, IUserAnswerTaskService
|
|
|
|
|
|
{
|
|
|
|
|
|
/// <summary>
|
2026-06-25 14:53:45 +08:00
|
|
|
|
/// ============================================
|
|
|
|
|
|
/// 方法思路:
|
2026-07-01 08:18:07 +08:00
|
|
|
|
/// 1. 首先验证 UserJournal 表,获取用户绑定的杂志列表
|
|
|
|
|
|
/// - 如果用户没有绑定任何杂志,直接返回空列表
|
|
|
|
|
|
/// 2. 批量预加载策略:为避免N+1查询问题,先按期刊维度批量加载所有关联数据
|
|
|
|
|
|
/// - 期刊基本信息
|
|
|
|
|
|
/// - 一级栏目(ParentId == 0)
|
|
|
|
|
|
/// - 所有任务(按期刊ID)
|
|
|
|
|
|
/// - 所有页面(按期刊ID)
|
|
|
|
|
|
/// 3. 查询用户在这些期刊下的作答记录
|
|
|
|
|
|
/// 4. 跨页题目处理核心:
|
2026-06-25 14:53:45 +08:00
|
|
|
|
/// - 跨页题目定义:JournalPageTask.Id != GroupId(同一个GroupId下有多个任务)
|
|
|
|
|
|
/// - 进度计算:按GroupId去重,已完成条件是该GroupId下所有任务都完成(Status=1)
|
2026-07-01 08:18:07 +08:00
|
|
|
|
/// - 积分计算:按GroupId去重,每个GroupId只计算一次积分(取主任务的积分)
|
|
|
|
|
|
/// 5. 状态判定逻辑:
|
2026-06-25 14:53:45 +08:00
|
|
|
|
/// - 未开始:无任何作答记录
|
|
|
|
|
|
/// - 进行中:有作答记录但未全部完成
|
|
|
|
|
|
/// - 已完成:所有GroupId都完成
|
2026-07-01 08:18:07 +08:00
|
|
|
|
/// 6. 用户未作答任何任务时:
|
2026-06-29 15:09:23 +08:00
|
|
|
|
/// - 仍返回绑定杂志的进度信息
|
|
|
|
|
|
/// - 进度、成长值、积分为0
|
|
|
|
|
|
/// - 连续打卡、剩余篇数正常计算
|
2026-06-25 14:53:45 +08:00
|
|
|
|
/// ============================================
|
2026-06-24 16:32:59 +08:00
|
|
|
|
/// </summary>
|
2026-06-25 14:53:45 +08:00
|
|
|
|
/// <param name="userId">用户ID</param>
|
|
|
|
|
|
/// <returns>用户所有期刊的学习进度列表</returns>
|
2026-06-24 16:32:59 +08:00
|
|
|
|
public async Task<List<JournalProgressOutput>> GetJournalProgressAsync(long userId)
|
|
|
|
|
|
{
|
2026-07-01 08:18:07 +08:00
|
|
|
|
#region 1. 验证 UserJournal 表,获取用户绑定的杂志
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
2026-06-29 15:09:23 +08:00
|
|
|
|
// ============================================
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 第一步:先验证 UserJournal 表数据
|
|
|
|
|
|
// 获取用户绑定的所有杂志 ID
|
|
|
|
|
|
// 如果没有绑定的杂志,直接返回空列表,不进行后续查询
|
2026-06-29 15:09:23 +08:00
|
|
|
|
// ============================================
|
|
|
|
|
|
var boundJournalIds = await userJournalRepository.Context.Queryable<UserJournal>()
|
2026-07-01 08:18:07 +08:00
|
|
|
|
.Where(uj => uj.UserId == userId)
|
2026-06-29 15:09:23 +08:00
|
|
|
|
.Select(uj => uj.JournalId)
|
|
|
|
|
|
.ToListAsync();
|
|
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
if (boundJournalIds == null || boundJournalIds.Count == 0)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
{
|
|
|
|
|
|
return new List<JournalProgressOutput>();
|
|
|
|
|
|
}
|
2026-07-01 08:18:07 +08:00
|
|
|
|
|
|
|
|
|
|
var journalIds = boundJournalIds.Distinct().ToList();
|
|
|
|
|
|
|
2026-06-29 15:09:23 +08:00
|
|
|
|
#endregion
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
#region 2. 批量预加载关联数据
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
// 第二步:批量预加载关联实体(按期刊维度全量加载,避免N+1查询)
|
|
|
|
|
|
// - 期刊基本信息
|
|
|
|
|
|
// - 一级栏目(ParentId == 0 表示一级目录)
|
|
|
|
|
|
// - 所有任务
|
|
|
|
|
|
// - 所有页面(用于计算剩余篇数)
|
|
|
|
|
|
// ============================================
|
2026-06-24 16:32:59 +08:00
|
|
|
|
var journals = await journalRepository.Context.Queryable<Journal>()
|
2026-07-01 08:18:07 +08:00
|
|
|
|
.Where(j => journalIds.Contains(j.Id))
|
2026-06-24 16:32:59 +08:00
|
|
|
|
.ToListAsync();
|
|
|
|
|
|
|
|
|
|
|
|
var allCatalogs = await catalogRepository.Context.Queryable<JournalCatalog>()
|
2026-07-01 08:18:07 +08:00
|
|
|
|
.Where(c => journalIds.Contains(c.JournalId) && c.ParentId == 0)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
.ToListAsync();
|
|
|
|
|
|
|
|
|
|
|
|
var allTasks = await taskRepository.Context.Queryable<JournalPageTask>()
|
2026-07-01 08:18:07 +08:00
|
|
|
|
.Where(t => journalIds.Contains(t.JournalId))
|
2026-06-24 16:32:59 +08:00
|
|
|
|
.ToListAsync();
|
|
|
|
|
|
|
|
|
|
|
|
var allPages = await pageRepository.Context.Queryable<JournalPage>()
|
2026-07-01 08:18:07 +08:00
|
|
|
|
.Where(p => journalIds.Contains(p.JournalId))
|
2026-06-24 16:32:59 +08:00
|
|
|
|
.ToListAsync();
|
2026-07-01 08:18:07 +08:00
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region 3. 查询用户作答记录(仅绑定的杂志)
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
// 第三步:查询用户在这些绑定杂志下的作答记录
|
|
|
|
|
|
// 只查询绑定杂志的作答记录,减少数据量
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
var userAnswers = await answerRepository.Context.Queryable<JournalPageTaskUserAnswer>()
|
|
|
|
|
|
.Where(a => a.UserId == userId && journalIds.Contains(a.JournalId))
|
|
|
|
|
|
.ToListAsync();
|
|
|
|
|
|
|
2026-06-29 15:09:23 +08:00
|
|
|
|
#endregion
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
2026-06-29 15:09:23 +08:00
|
|
|
|
#region 4. 查询连续打卡天数
|
2026-07-01 08:18:07 +08:00
|
|
|
|
|
2026-06-24 16:32:59 +08:00
|
|
|
|
var consecutiveDays = await CalculateConsecutiveDaysAsync(userId);
|
2026-07-01 08:18:07 +08:00
|
|
|
|
|
2026-06-29 15:09:23 +08:00
|
|
|
|
#endregion
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
2026-06-29 15:09:23 +08:00
|
|
|
|
#region 5. 提取用户已作答/已完成任务ID集合
|
2026-07-01 08:18:07 +08:00
|
|
|
|
|
|
|
|
|
|
// 用户已作答的任务ID集合(包含进行中和已完成)
|
2026-06-29 15:09:23 +08:00
|
|
|
|
var userAnsweredTaskIds = userAnswers.Select(a => a.JournalPageTaskId).ToHashSet();
|
|
|
|
|
|
var userAnsweredPageIds = userAnswers.Select(a => a.JournalPageId).ToHashSet();
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 用户已完成的任务ID集合
|
|
|
|
|
|
// 【已完成判断逻辑】
|
2026-07-01 09:20:07 +08:00
|
|
|
|
// Status: 0=未批阅,1=已批阅
|
2026-07-01 08:18:07 +08:00
|
|
|
|
var userCompletedTaskIds = userAnswers
|
2026-07-01 09:20:07 +08:00
|
|
|
|
.Where(a => a.Status == (int)UserAnswerStatusEnum.Complete)
|
2026-07-01 08:18:07 +08:00
|
|
|
|
.Select(a => a.JournalPageTaskId)
|
|
|
|
|
|
.ToHashSet();
|
|
|
|
|
|
|
2026-06-29 15:09:23 +08:00
|
|
|
|
#endregion
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
2026-06-29 15:09:23 +08:00
|
|
|
|
#region 6. 按期刊分组聚合并返回结果
|
2026-07-01 08:18:07 +08:00
|
|
|
|
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
// 第四步:按期刊分组聚合
|
|
|
|
|
|
// 遍历每个绑定的杂志,计算进度、积分、成长值等
|
|
|
|
|
|
// ============================================
|
2026-06-24 16:32:59 +08:00
|
|
|
|
var result = new List<JournalProgressOutput>();
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var journal in journals)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 该期刊下所有任务
|
|
|
|
|
|
var journalTasks = allTasks.Where(t => t.JournalId == journal.Id).ToList();
|
|
|
|
|
|
|
2026-06-25 14:53:45 +08:00
|
|
|
|
// 该期刊的一级栏目数
|
|
|
|
|
|
var catalogCount = allCatalogs.Count(c => c.JournalId == journal.Id);
|
|
|
|
|
|
|
|
|
|
|
|
// 【跨页题目-进度计算】总任务数:按 GroupId 去重,跨页题目只算一个
|
2026-07-01 08:18:07 +08:00
|
|
|
|
var totalGroupIds = journalTasks.Select(t => t.GroupId).Distinct().ToList();
|
|
|
|
|
|
var totalTaskCount = totalGroupIds.Count;
|
2026-06-25 14:53:45 +08:00
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 【跨页题目-已完成判断】已完成的 GroupId 列表
|
2026-06-25 14:53:45 +08:00
|
|
|
|
// 关键:必须该 GroupId 下所有任务都完成,才算该 GroupId 完成
|
|
|
|
|
|
var completedGroupIds = journalTasks
|
|
|
|
|
|
.GroupBy(t => t.GroupId)
|
|
|
|
|
|
.Where(g => g.All(t => userCompletedTaskIds.Contains(t.Id)))
|
|
|
|
|
|
.Select(g => g.Key)
|
|
|
|
|
|
.ToList();
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
var completedTaskCount = completedGroupIds.Count;
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
// 【积分和成长值计算】
|
|
|
|
|
|
// 1. totalGrowthPoints:从已完成作答记录中,按 JournalPageTaskGroupId 查询主任务(Id=GroupId)的 GrowthPoint
|
|
|
|
|
|
// 2. totalPoints:统计该期刊下已完成任务的 Points 总数
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
// 该期刊下用户已完成的作答记录
|
|
|
|
|
|
// 【已完成判断逻辑】
|
2026-07-01 09:20:07 +08:00
|
|
|
|
// Status: 0=未批阅,1=已批阅
|
2026-07-01 08:18:07 +08:00
|
|
|
|
var completedAnswers = userAnswers
|
2026-07-01 09:20:07 +08:00
|
|
|
|
.Where(a => a.JournalId == journal.Id && a.Status == (int)UserAnswerStatusEnum.Complete)
|
2026-06-25 14:53:45 +08:00
|
|
|
|
.ToList();
|
|
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 按 JournalPageTaskGroupId 去重,获取已完成的主任务 ID 列表
|
|
|
|
|
|
var completedGroupIdsSet = completedAnswers.Select(a => a.JournalPageTaskGroupId).Distinct().ToList();
|
2026-06-25 14:53:45 +08:00
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 从主任务表查询这些 GroupId 对应的 GrowthPoint 和 Points
|
|
|
|
|
|
var mainTasks = allTasks
|
|
|
|
|
|
.Where(t => completedGroupIdsSet.Contains(t.GroupId) && t.Id == t.GroupId)
|
|
|
|
|
|
.ToList();
|
2026-06-25 14:53:45 +08:00
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 累计成长值(从主任务的 GrowthPoint 累加)
|
2026-07-01 10:27:19 +08:00
|
|
|
|
var totalGrowthPoints = mainTasks.Sum(t => t.GrowthPoint);
|
2026-07-01 08:18:07 +08:00
|
|
|
|
|
|
|
|
|
|
// 累计积分(从已完成任务的 Points 累加,每个 GroupId 只计算一次)
|
|
|
|
|
|
var totalPoints = mainTasks.Sum(t => t.Points);
|
|
|
|
|
|
|
|
|
|
|
|
// 【进度百分比计算】按篇数统计
|
|
|
|
|
|
// 只计算有题目的页面,进度 = 已答篇数 / 有题目的总篇数 × 100
|
|
|
|
|
|
var pagesWithTasks = journalTasks.Select(t => t.JournalPageId).Distinct().ToHashSet();
|
|
|
|
|
|
var totalPagesWithTasks = pagesWithTasks.Count;
|
|
|
|
|
|
var answeredPagesWithTasks = pagesWithTasks.Count(userAnsweredPageIds.Contains);
|
|
|
|
|
|
var progress = totalPagesWithTasks > 0
|
|
|
|
|
|
? Math.Round((double)answeredPagesWithTasks / totalPagesWithTasks * 100, 2)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
: 0;
|
|
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 剩余篇数:有题目的页面总数 - 已作答的有题目页面数
|
|
|
|
|
|
var remainingPages = totalPagesWithTasks - answeredPagesWithTasks;
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
|
|
|
|
|
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;
|
2026-06-29 15:09:23 +08:00
|
|
|
|
#endregion
|
2026-06-24 16:32:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-06-25 14:53:45 +08:00
|
|
|
|
/// ============================================
|
|
|
|
|
|
/// 方法思路:计算用户连续打卡天数
|
2026-06-29 15:09:23 +08:00
|
|
|
|
/// 1. 获取今天和昨天的日期
|
|
|
|
|
|
/// 2. 查询昨天是否有打卡记录
|
|
|
|
|
|
/// 3. 如果昨天没有打卡记录,返回0(连续打卡中断)
|
|
|
|
|
|
/// 4. 如果昨天有打卡记录:
|
|
|
|
|
|
/// - 查询今天是否有打卡记录
|
|
|
|
|
|
/// - 今天有记录:返回今天的连续天数
|
|
|
|
|
|
/// - 今天无记录:返回昨天的连续天数
|
2026-06-25 14:53:45 +08:00
|
|
|
|
/// ============================================
|
2026-06-24 16:32:59 +08:00
|
|
|
|
/// </summary>
|
2026-06-25 14:53:45 +08:00
|
|
|
|
/// <param name="userId">用户ID</param>
|
|
|
|
|
|
/// <returns>连续打卡天数</returns>
|
2026-06-24 16:32:59 +08:00
|
|
|
|
private async Task<int> CalculateConsecutiveDaysAsync(long userId)
|
|
|
|
|
|
{
|
|
|
|
|
|
var today = DateTime.Now.Date;
|
2026-06-29 15:09:23 +08:00
|
|
|
|
var yesterday = today.AddDays(-1);
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
2026-06-29 15:09:23 +08:00
|
|
|
|
// 查询昨天是否有打卡记录
|
|
|
|
|
|
var yesterdayRecord = await checkInRecordRepository.Context.Queryable<CheckInRecord>()
|
2026-07-01 08:18:07 +08:00
|
|
|
|
.Where(r => r.UserId == userId && r.CheckInDate.Date == yesterday)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
.FirstAsync();
|
|
|
|
|
|
|
2026-06-29 15:09:23 +08:00
|
|
|
|
// 如果昨天没有打卡记录,连续天数为0(连续打卡中断)
|
|
|
|
|
|
if (yesterdayRecord == null)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
{
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-29 15:09:23 +08:00
|
|
|
|
// 昨天有打卡记录,检查今天是否已打卡
|
|
|
|
|
|
var todayRecord = await checkInRecordRepository.Context.Queryable<CheckInRecord>()
|
2026-07-01 08:18:07 +08:00
|
|
|
|
.Where(r => r.UserId == userId && r.CheckInDate.Date == today)
|
2026-06-29 15:09:23 +08:00
|
|
|
|
.FirstAsync();
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
2026-06-29 15:09:23 +08:00
|
|
|
|
// 今天已打卡:返回今天的连续天数;今天未打卡:返回昨天的连续天数
|
|
|
|
|
|
return todayRecord?.ConsecutiveDays ?? yesterdayRecord.ConsecutiveDays;
|
2026-06-24 16:32:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-06-25 14:53:45 +08:00
|
|
|
|
/// ============================================
|
|
|
|
|
|
/// 方法思路:获取目录列表(按一级目录分组展示)
|
|
|
|
|
|
/// 1. 批量预加载:该期刊下所有目录、页面、任务、用户作答记录
|
|
|
|
|
|
/// 2. 目录结构处理:
|
|
|
|
|
|
/// - 一级目录:ParentId=0 且 Type=Unit
|
2026-06-29 15:09:23 +08:00
|
|
|
|
/// - 二级目录:ParentId=一级目录Id 且 Type=Unit
|
2026-06-25 14:53:45 +08:00
|
|
|
|
/// - 文章目录:Type=Page
|
2026-06-29 15:09:23 +08:00
|
|
|
|
/// 3. 【关键】如果一级目录下没有二级栏目(Type=Unit的子目录),
|
|
|
|
|
|
/// 则该一级目录不参与任何计算,直接跳过
|
|
|
|
|
|
/// 4. 跨页题目处理:
|
2026-06-25 14:53:45 +08:00
|
|
|
|
/// - 状态判断:按GroupId去重,已完成条件是该GroupId下所有任务都完成(Status=1)
|
|
|
|
|
|
/// - 积分计算:按GroupId去重,每个GroupId只计算一次积分
|
|
|
|
|
|
/// - 已作答判断:只要有一个任务作答就算该GroupId已作答(包含进行中和已完成)
|
2026-06-29 15:09:23 +08:00
|
|
|
|
/// 5. 状态判定:
|
2026-06-25 14:53:45 +08:00
|
|
|
|
/// - 未开始:无任何GroupId被作答
|
|
|
|
|
|
/// - 进行中:有GroupId被作答但未全部完成
|
|
|
|
|
|
/// - 已完成:所有GroupId都完成
|
|
|
|
|
|
/// ============================================
|
2026-06-24 16:32:59 +08:00
|
|
|
|
/// </summary>
|
2026-06-25 14:53:45 +08:00
|
|
|
|
/// <param name="journalId">期刊ID</param>
|
|
|
|
|
|
/// <param name="userId">用户ID</param>
|
|
|
|
|
|
/// <returns>目录列表(按一级目录分组)</returns>
|
2026-06-24 16:32:59 +08:00
|
|
|
|
public async Task<List<CatalogListOutput>> GetCatalogListAsync(long journalId, long userId)
|
|
|
|
|
|
{
|
2026-07-01 08:18:07 +08:00
|
|
|
|
#region 0. 验证用户是否绑定该杂志
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
// 第一步:验证 UserJournal 表,确认用户是否绑定了该杂志
|
|
|
|
|
|
// 如果用户没有绑定该杂志,直接返回空列表
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
var isBound = await userJournalRepository.Context.Queryable<UserJournal>()
|
|
|
|
|
|
.Where(uj => uj.UserId == userId && uj.JournalId == journalId)
|
|
|
|
|
|
.AnyAsync();
|
|
|
|
|
|
|
|
|
|
|
|
if (!isBound)
|
|
|
|
|
|
{
|
|
|
|
|
|
return new List<CatalogListOutput>();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
2026-06-29 15:09:23 +08:00
|
|
|
|
#region 1. 查询期刊
|
2026-06-24 16:32:59 +08:00
|
|
|
|
// 1. 查询期刊
|
|
|
|
|
|
var journal = await journalRepository.Context.Queryable<Journal>()
|
2026-07-01 08:18:07 +08:00
|
|
|
|
.Where(j => j.Id == journalId)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
.FirstAsync();
|
|
|
|
|
|
|
|
|
|
|
|
if (journal == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return new List<CatalogListOutput>();
|
|
|
|
|
|
}
|
2026-06-29 15:09:23 +08:00
|
|
|
|
#endregion
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
2026-06-29 15:09:23 +08:00
|
|
|
|
#region 2. 批量加载关联数据
|
2026-06-24 16:32:59 +08:00
|
|
|
|
// 2. 批量加载关联数据
|
|
|
|
|
|
var allCatalogs = await catalogRepository.Context.Queryable<JournalCatalog>()
|
2026-07-01 08:18:07 +08:00
|
|
|
|
.Where(c => c.JournalId == journalId)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
.ToListAsync();
|
|
|
|
|
|
|
|
|
|
|
|
var allPages = await pageRepository.Context.Queryable<JournalPage>()
|
2026-07-01 08:18:07 +08:00
|
|
|
|
.Where(p => p.JournalId == journalId)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
.ToListAsync();
|
|
|
|
|
|
|
|
|
|
|
|
var allTasks = await taskRepository.Context.Queryable<JournalPageTask>()
|
2026-07-01 08:18:07 +08:00
|
|
|
|
.Where(t => t.JournalId == journalId)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
.ToListAsync();
|
|
|
|
|
|
|
|
|
|
|
|
var userAnswers = await answerRepository.Context.Queryable<JournalPageTaskUserAnswer>()
|
2026-07-01 08:18:07 +08:00
|
|
|
|
.Where(a => a.JournalId == journalId && a.UserId == userId)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
.ToListAsync();
|
2026-06-29 15:09:23 +08:00
|
|
|
|
#endregion
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
2026-06-29 15:09:23 +08:00
|
|
|
|
#region 3. 用户已作答/已完成任务ID集合
|
2026-06-24 16:32:59 +08:00
|
|
|
|
// 3. 用户已作答的任务ID集合(包含进行中和已完成)
|
2026-06-29 15:09:23 +08:00
|
|
|
|
var userAnsweredTaskIds = userAnswers.Select(a => a.JournalPageTaskId).ToHashSet();
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 用户已完成的任务ID集合
|
2026-07-01 09:20:07 +08:00
|
|
|
|
// 【已完成判断逻辑】
|
2026-07-01 08:18:07 +08:00
|
|
|
|
var userCompletedTaskIds = userAnswers
|
2026-07-01 09:20:07 +08:00
|
|
|
|
.Where(a => a.Status == (int)UserAnswerStatusEnum.Complete)
|
2026-07-01 08:18:07 +08:00
|
|
|
|
.Select(a => a.JournalPageTaskId)
|
|
|
|
|
|
.ToHashSet();
|
2026-06-29 15:09:23 +08:00
|
|
|
|
#endregion
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
2026-06-29 15:09:23 +08:00
|
|
|
|
#region 4. 按一级目录分组计算
|
2026-06-24 16:32:59 +08:00
|
|
|
|
// 4. 按一级目录分组
|
2026-07-01 08:18:07 +08:00
|
|
|
|
var firstLevelCatalogs = allCatalogs.Where(c => c.ParentId == 0).OrderBy(c => c.Sort).ToList();
|
2026-06-24 16:32:59 +08:00
|
|
|
|
var result = new List<CatalogListOutput>();
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var firstCatalog in firstLevelCatalogs)
|
|
|
|
|
|
{
|
2026-06-29 15:09:23 +08:00
|
|
|
|
// ============================================
|
|
|
|
|
|
// 文章篇数计算逻辑:
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// - 篇数 = 有题目的页面数(有 JournalPageTask 的页面)
|
|
|
|
|
|
// - 有二级栏目:累加所有二级栏目下有题目的页面数
|
|
|
|
|
|
// - 没有二级栏目:统计一级栏目下直接有题目的页面数
|
2026-06-29 15:09:23 +08:00
|
|
|
|
// ============================================
|
|
|
|
|
|
int articleCount = 0;
|
2026-06-24 16:32:59 +08:00
|
|
|
|
var children = new List<SecondCatalogOutput>();
|
|
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 该一级目录下的二级单元目录
|
|
|
|
|
|
var secondLevelCatalogs = allCatalogs
|
|
|
|
|
|
.Where(c => c.ParentId == firstCatalog.Id)
|
|
|
|
|
|
.OrderBy(c => c.Sort)
|
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
2026-06-24 16:32:59 +08:00
|
|
|
|
if (secondLevelCatalogs.Count > 0)
|
|
|
|
|
|
{
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 有二级单元:按二级单元分组统计
|
2026-06-24 16:32:59 +08:00
|
|
|
|
foreach (var secondCatalog in secondLevelCatalogs)
|
|
|
|
|
|
{
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 二级目录下的所有页面(JournalPage.JournalCatalogId = 二级目录ID)
|
|
|
|
|
|
var secondLevelPages = allPages
|
|
|
|
|
|
.Where(p => p.JournalCatalogId == secondCatalog.Id)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
.ToList();
|
2026-06-29 15:09:23 +08:00
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
var secondLevelPageIds = secondLevelPages.Select(p => p.Id).ToHashSet();
|
2026-06-29 15:09:23 +08:00
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 二级目录下有题目的页面(有 JournalPageTask 的页面)
|
|
|
|
|
|
var pagesWithTasks = allTasks
|
|
|
|
|
|
.Where(t => secondLevelPageIds.Contains(t.JournalPageId))
|
|
|
|
|
|
.Select(t => t.JournalPageId)
|
|
|
|
|
|
.Distinct()
|
2026-06-24 16:32:59 +08:00
|
|
|
|
.ToHashSet();
|
|
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 【关键】如果二级目录下没有有题目的页面,则跳过,不统计到 children
|
|
|
|
|
|
if (pagesWithTasks.Count == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 累加该二级栏目下有题目的页面数(文章篇数)
|
|
|
|
|
|
articleCount += pagesWithTasks.Count;
|
|
|
|
|
|
|
2026-06-24 16:32:59 +08:00
|
|
|
|
// 二级单元下的所有任务
|
|
|
|
|
|
var secondLevelTasks = allTasks.Where(t => secondLevelPageIds.Contains(t.JournalPageId)).ToList();
|
|
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 判定状态(基于 AnswerStatus >= 1 判断已完成,按页面维度统计)
|
|
|
|
|
|
// 【已完成判断逻辑】
|
|
|
|
|
|
// AnswerStatus: 0=未答题,1=已答题,2=正确,3=半对,4=错误
|
|
|
|
|
|
// 只要 AnswerStatus >= 1(已答题)说明该页面已完成
|
2026-06-24 16:32:59 +08:00
|
|
|
|
string status;
|
2026-07-01 08:18:07 +08:00
|
|
|
|
|
|
|
|
|
|
// 总页面数(有题目的页面)
|
|
|
|
|
|
var totalPageCount = pagesWithTasks.Count;
|
|
|
|
|
|
// 已完成页面数:该页面有答题记录且 AnswerStatus >= 1
|
|
|
|
|
|
var completedPageCount = pagesWithTasks
|
2026-07-01 09:20:07 +08:00
|
|
|
|
.Count(pageId => userAnswers.Any(a => a.JournalPageId == pageId && a.Status == (int)UserAnswerStatusEnum.Complete));
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 已作答页面数:该页面有答题记录即可
|
|
|
|
|
|
var answeredPageCount = pagesWithTasks
|
|
|
|
|
|
.Count(pageId => userAnswers.Any(a => a.JournalPageId == pageId));
|
|
|
|
|
|
|
|
|
|
|
|
// 状态判定逻辑:
|
|
|
|
|
|
// - answeredPageCount == 0:没有任何页面被作答 → 未开始
|
|
|
|
|
|
// - completedPageCount == totalPageCount:所有页面都完成 → 已完成
|
|
|
|
|
|
// - 其他:有页面被作答但未全部完成 → 进行中
|
|
|
|
|
|
if (answeredPageCount == 0)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
status = "未开始";
|
2026-07-01 08:18:07 +08:00
|
|
|
|
else if (completedPageCount == totalPageCount)
|
|
|
|
|
|
status = "已完成";
|
2026-06-24 16:32:59 +08:00
|
|
|
|
else
|
2026-07-01 08:18:07 +08:00
|
|
|
|
status = "进行中";
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
2026-06-29 15:09:23 +08:00
|
|
|
|
// 计算该二级目录的总成长值和总积分(跨页题目按GroupId去重)
|
|
|
|
|
|
var distinctSecondLevelTasks = secondLevelTasks
|
2026-06-25 14:53:45 +08:00
|
|
|
|
.GroupBy(t => t.GroupId)
|
|
|
|
|
|
.Select(g => g.First())
|
|
|
|
|
|
.ToList();
|
2026-07-01 10:27:19 +08:00
|
|
|
|
var totalGrowthPoints = distinctSecondLevelTasks.Sum(t => t.GrowthPoint);
|
2026-06-29 15:09:23 +08:00
|
|
|
|
var totalPoints = distinctSecondLevelTasks.Sum(t => t.Points);
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
|
|
|
|
|
children.Add(new SecondCatalogOutput
|
|
|
|
|
|
{
|
2026-06-29 15:09:23 +08:00
|
|
|
|
CatalogId = secondCatalog.Id,
|
|
|
|
|
|
CatalogName = secondCatalog.Name ?? string.Empty,
|
2026-06-24 16:32:59 +08:00
|
|
|
|
TotalPoints = totalPoints,
|
|
|
|
|
|
TotalGrowthPoints = totalGrowthPoints,
|
|
|
|
|
|
Status = status
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-06-29 15:09:23 +08:00
|
|
|
|
else
|
|
|
|
|
|
{
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 没有二级单元:统计一级栏目下直接有题目的页面数
|
|
|
|
|
|
var firstLevelPages = allPages
|
|
|
|
|
|
.Where(p => p.JournalCatalogId == firstCatalog.Id)
|
|
|
|
|
|
.ToList();
|
|
|
|
|
|
var firstLevelPageIds = firstLevelPages.Select(p => p.Id).ToHashSet();
|
|
|
|
|
|
var pagesWithTasks = allTasks
|
|
|
|
|
|
.Where(t => firstLevelPageIds.Contains(t.JournalPageId))
|
|
|
|
|
|
.Select(t => t.JournalPageId)
|
|
|
|
|
|
.Distinct()
|
|
|
|
|
|
.ToHashSet();
|
|
|
|
|
|
articleCount = pagesWithTasks.Count;
|
2026-06-29 15:09:23 +08:00
|
|
|
|
// Children保持为空集合,不添加任何子项
|
|
|
|
|
|
}
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
|
|
|
|
|
result.Add(new CatalogListOutput
|
|
|
|
|
|
{
|
|
|
|
|
|
CatalogId = firstCatalog.Id,
|
|
|
|
|
|
CatalogName = firstCatalog.Name ?? string.Empty,
|
|
|
|
|
|
JournalName = journal.Name ?? string.Empty,
|
|
|
|
|
|
ArticleCount = articleCount,
|
|
|
|
|
|
Children = children
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-06-29 15:09:23 +08:00
|
|
|
|
#endregion
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
2026-06-29 15:09:23 +08:00
|
|
|
|
#region 5. 返回结果
|
2026-06-24 16:32:59 +08:00
|
|
|
|
return result;
|
2026-06-29 15:09:23 +08:00
|
|
|
|
#endregion
|
2026-06-24 16:32:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-06-25 14:53:45 +08:00
|
|
|
|
/// ============================================
|
|
|
|
|
|
/// 方法思路:获取一级目录概览(含二级目录详情和任务完成状态)
|
|
|
|
|
|
/// 1. 批量预加载:该期刊下所有目录、页面、任务、用户作答记录
|
|
|
|
|
|
/// 2. 获取该一级目录下的所有文章目录(Type=Page)
|
|
|
|
|
|
/// 3. 跨页题目处理:
|
|
|
|
|
|
/// - 总任务数:按GroupId去重(跨页题目只算一个)
|
|
|
|
|
|
/// - 状态判断:按GroupId去重,已完成条件是该GroupId下所有任务都完成(Status=1)
|
|
|
|
|
|
/// - 已作答判断:只要有一个任务作答就算该GroupId已作答
|
|
|
|
|
|
/// 4. 状态格式:"已完成数/总数 已完成/进行中/未开始"
|
|
|
|
|
|
/// 例如:"2/3 进行中" 表示3个GroupId中已完成2个
|
|
|
|
|
|
/// ============================================
|
2026-06-24 16:32:59 +08:00
|
|
|
|
/// </summary>
|
2026-06-25 14:53:45 +08:00
|
|
|
|
/// <param name="catalogId">一级目录ID</param>
|
2026-06-24 16:32:59 +08:00
|
|
|
|
/// <param name="userId">用户ID</param>
|
2026-06-25 14:53:45 +08:00
|
|
|
|
/// <returns>一级目录概览信息</returns>
|
2026-06-24 16:32:59 +08:00
|
|
|
|
public async Task<CatalogOverviewOutput> GetCatalogOverviewAsync(long catalogId, long userId)
|
|
|
|
|
|
{
|
2026-06-29 15:09:23 +08:00
|
|
|
|
#region 1. 查询一级目录
|
2026-06-24 16:32:59 +08:00
|
|
|
|
// 1. 查询一级目录
|
|
|
|
|
|
var firstCatalog = await catalogRepository.Context.Queryable<JournalCatalog>()
|
2026-07-01 08:18:07 +08:00
|
|
|
|
.Where(c => c.Id == catalogId)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
.FirstAsync();
|
|
|
|
|
|
|
|
|
|
|
|
if (firstCatalog == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
2026-06-29 15:09:23 +08:00
|
|
|
|
#endregion
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
#region 2. 验证用户是否绑定该杂志
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
// 验证 UserJournal 表,确认用户是否绑定了该杂志
|
|
|
|
|
|
// 如果用户没有绑定该杂志,直接返回 null
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
var isBound = await userJournalRepository.Context.Queryable<UserJournal>()
|
|
|
|
|
|
.Where(uj => uj.UserId == userId && uj.JournalId == firstCatalog.JournalId)
|
|
|
|
|
|
.AnyAsync();
|
|
|
|
|
|
|
|
|
|
|
|
if (!isBound)
|
|
|
|
|
|
{
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region 3. 批量加载关联数据
|
2026-06-24 16:32:59 +08:00
|
|
|
|
// 2. 批量加载关联数据
|
|
|
|
|
|
var allCatalogs = await catalogRepository.Context.Queryable<JournalCatalog>()
|
2026-07-01 08:18:07 +08:00
|
|
|
|
.Where(c => c.JournalId == firstCatalog.JournalId)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
.ToListAsync();
|
|
|
|
|
|
|
|
|
|
|
|
var allPages = await pageRepository.Context.Queryable<JournalPage>()
|
2026-07-01 08:18:07 +08:00
|
|
|
|
.Where(p => p.JournalId == firstCatalog.JournalId)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
.ToListAsync();
|
|
|
|
|
|
|
|
|
|
|
|
var allTasks = await taskRepository.Context.Queryable<JournalPageTask>()
|
2026-07-01 08:18:07 +08:00
|
|
|
|
.Where(t => t.JournalId == firstCatalog.JournalId)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
.ToListAsync();
|
|
|
|
|
|
|
|
|
|
|
|
var userAnswers = await answerRepository.Context.Queryable<JournalPageTaskUserAnswer>()
|
2026-07-01 08:18:07 +08:00
|
|
|
|
.Where(a => a.JournalId == firstCatalog.JournalId && a.UserId == userId)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
.ToListAsync();
|
2026-06-29 15:09:23 +08:00
|
|
|
|
#endregion
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
#region 4. 用户已作答/已完成任务ID集合
|
|
|
|
|
|
// 用户已作答的任务ID集合(包含进行中和已完成)
|
2026-06-29 15:09:23 +08:00
|
|
|
|
var userAnsweredTaskIds = userAnswers.Select(a => a.JournalPageTaskId).ToHashSet();
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 用户已完成的任务ID集合
|
|
|
|
|
|
// 【已完成判断逻辑】
|
|
|
|
|
|
// AnswerStatus: 0=未答题,1=已答题,2=正确,3=半对,4=错误
|
|
|
|
|
|
// 只要 AnswerStatus >= 1(已答题)说明作业已完成
|
|
|
|
|
|
var userCompletedTaskIds = userAnswers
|
2026-07-01 09:20:07 +08:00
|
|
|
|
.Where(a => a.Status == (int)UserAnswerStatusEnum.Complete)
|
2026-07-01 08:18:07 +08:00
|
|
|
|
.Select(a => a.JournalPageTaskId)
|
|
|
|
|
|
.ToHashSet();
|
2026-06-29 15:09:23 +08:00
|
|
|
|
#endregion
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
#region 5. 获取一级栏目下所有二级目录
|
|
|
|
|
|
// 获取该一级目录下所有二级目录(不区分Type)
|
|
|
|
|
|
var secondLevelCatalogs = allCatalogs
|
|
|
|
|
|
.Where(c => c.ParentId == firstCatalog.Id)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
.OrderBy(c => c.Sort)
|
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 文章篇数 = 有题目的页面数(有 JournalPageTask 的页面)
|
|
|
|
|
|
int articleCount = 0;
|
|
|
|
|
|
// 总任务数(按 GroupId 去重)
|
|
|
|
|
|
int totalTaskCount = 0;
|
|
|
|
|
|
// 构建子目录详情列表
|
2026-06-24 16:32:59 +08:00
|
|
|
|
var subCatalogs = new List<SubCatalogDetailOutput>();
|
|
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
foreach (var secondCatalog in secondLevelCatalogs)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
{
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 二级目录下的所有页面
|
|
|
|
|
|
var secondLevelPages = allPages
|
|
|
|
|
|
.Where(p => p.JournalCatalogId == secondCatalog.Id)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
.OrderBy(p => p.PageNum)
|
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
var secondLevelPageIds = secondLevelPages.Select(p => p.Id).ToHashSet();
|
|
|
|
|
|
|
|
|
|
|
|
// 二级目录下有题目的页面(有 JournalPageTask 的页面)
|
|
|
|
|
|
var pagesWithTasks = allTasks
|
|
|
|
|
|
.Where(t => secondLevelPageIds.Contains(t.JournalPageId))
|
|
|
|
|
|
.Select(t => t.JournalPageId)
|
|
|
|
|
|
.Distinct()
|
|
|
|
|
|
.ToHashSet();
|
|
|
|
|
|
|
|
|
|
|
|
// 如果二级目录下没有有题目的页面,则跳过
|
|
|
|
|
|
if (pagesWithTasks.Count == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 累加文章篇数
|
|
|
|
|
|
articleCount += pagesWithTasks.Count;
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
|
|
|
|
|
// 该文章下的所有任务
|
2026-07-01 08:18:07 +08:00
|
|
|
|
var articleTasks = allTasks.Where(t => secondLevelPageIds.Contains(t.JournalPageId)).ToList();
|
|
|
|
|
|
// taskCount = GroupId 去重数(跨页题目只算一个)
|
|
|
|
|
|
var taskCount = articleTasks.Select(t => t.GroupId).Distinct().Count();
|
|
|
|
|
|
// GroupId 去重数(用于状态判断)
|
|
|
|
|
|
var totalGroupCount = taskCount;
|
|
|
|
|
|
|
|
|
|
|
|
// 累加总任务数
|
|
|
|
|
|
totalTaskCount += taskCount;
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
|
|
|
|
|
// 页码范围
|
2026-07-01 08:18:07 +08:00
|
|
|
|
var startPage = secondLevelPages.Count > 0 ? secondLevelPages.Min(p => p.PageNum) : 0;
|
|
|
|
|
|
var endPage = secondLevelPages.Count > 0 ? secondLevelPages.Max(p => p.PageNum) : 0;
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 状态格式:1/2 进行中、2/2 已完成、0/2 未开始(按 GroupId 去重统计)
|
|
|
|
|
|
// 只要 AnswerStatus >= 1(已答题)说明该 GroupId 已完成
|
2026-06-24 16:32:59 +08:00
|
|
|
|
string status;
|
2026-06-25 14:53:45 +08:00
|
|
|
|
if (totalGroupCount == 0)
|
|
|
|
|
|
{
|
2026-07-01 08:18:07 +08:00
|
|
|
|
status = "0/0 未开始";
|
2026-06-25 14:53:45 +08:00
|
|
|
|
}
|
2026-06-24 16:32:59 +08:00
|
|
|
|
else
|
2026-06-25 14:53:45 +08:00
|
|
|
|
{
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 已完成 GroupId 数(该 GroupId 下所有任务都 AnswerStatus >= 1 才算完成)
|
2026-06-25 14:53:45 +08:00
|
|
|
|
var completedGroupCount = articleTasks
|
|
|
|
|
|
.GroupBy(t => t.GroupId)
|
|
|
|
|
|
.Where(g => g.All(t => userCompletedTaskIds.Contains(t.Id)))
|
|
|
|
|
|
.Count();
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 已作答 GroupId 数(只要有一个任务作答就算该 GroupId 已作答)
|
2026-06-25 14:53:45 +08:00
|
|
|
|
var answeredGroupCount = articleTasks
|
|
|
|
|
|
.Where(t => userAnsweredTaskIds.Contains(t.Id))
|
|
|
|
|
|
.Select(t => t.GroupId)
|
|
|
|
|
|
.Distinct()
|
|
|
|
|
|
.Count();
|
2026-07-01 08:18:07 +08:00
|
|
|
|
|
2026-06-25 14:53:45 +08:00
|
|
|
|
if (answeredGroupCount == 0)
|
|
|
|
|
|
status = $"0/{totalGroupCount} 未开始";
|
|
|
|
|
|
else if (completedGroupCount == totalGroupCount)
|
2026-07-01 08:18:07 +08:00
|
|
|
|
status = $"{totalGroupCount}/{totalGroupCount} 已完成";
|
2026-06-25 14:53:45 +08:00
|
|
|
|
else
|
|
|
|
|
|
status = $"{completedGroupCount}/{totalGroupCount} 进行中";
|
|
|
|
|
|
}
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
|
|
|
|
|
subCatalogs.Add(new SubCatalogDetailOutput
|
|
|
|
|
|
{
|
2026-07-01 08:18:07 +08:00
|
|
|
|
CatalogId = secondCatalog.Id,
|
|
|
|
|
|
SecondLevelCatalogName = secondCatalog.Name ?? string.Empty,
|
2026-06-24 16:32:59 +08:00
|
|
|
|
StartPage = startPage,
|
|
|
|
|
|
EndPage = endPage,
|
2026-07-01 08:18:07 +08:00
|
|
|
|
TaskCount = taskCount,
|
2026-06-24 16:32:59 +08:00
|
|
|
|
Status = status
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-06-29 15:09:23 +08:00
|
|
|
|
#endregion
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
2026-06-29 15:09:23 +08:00
|
|
|
|
#region 6. 返回结果
|
2026-06-24 16:32:59 +08:00
|
|
|
|
return new CatalogOverviewOutput
|
|
|
|
|
|
{
|
|
|
|
|
|
CatalogId = firstCatalog.Id,
|
|
|
|
|
|
FirstLevelCatalogName = firstCatalog.Name ?? string.Empty,
|
|
|
|
|
|
PageCount = articleCount,
|
|
|
|
|
|
TotalTaskCount = totalTaskCount,
|
|
|
|
|
|
SubCatalogs = subCatalogs
|
|
|
|
|
|
};
|
2026-06-29 15:09:23 +08:00
|
|
|
|
#endregion
|
2026-06-24 16:32:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-06-25 14:53:45 +08:00
|
|
|
|
/// ============================================
|
2026-06-29 15:09:23 +08:00
|
|
|
|
/// 方法思路:获取用户答题任务列表
|
|
|
|
|
|
/// 1. 根据二级栏目ID查询栏目基本信息(一级栏目名称、栏目名称、页码范围
|
|
|
|
|
|
/// 2. 按GroupId分组,每个GroupId对应一个任务项
|
|
|
|
|
|
/// 3. 每个任务项包含:任务名称、总成长值、总积分、状态
|
|
|
|
|
|
/// 4. 【跨页题目处理】
|
|
|
|
|
|
/// - 是否跨页:Id != GroupId
|
|
|
|
|
|
/// - 任务名称:取 Id == GroupId 的主任务的Task字段
|
|
|
|
|
|
/// - 成长值/积分:取主任务的,每个GroupId只算一次
|
|
|
|
|
|
/// - 状态判断:必须该GroupId下所有任务都完成(Status=1)才算该GroupId完成
|
2026-06-25 14:53:45 +08:00
|
|
|
|
/// ============================================
|
2026-06-29 15:09:23 +08:00
|
|
|
|
/// <param name="catalogId">二级栏目ID/param>
|
2026-06-25 14:53:45 +08:00
|
|
|
|
/// <param name="userId">用户ID</param>
|
2026-06-29 15:09:23 +08:00
|
|
|
|
/// </summary>
|
2026-06-25 14:53:45 +08:00
|
|
|
|
/// <returns>用户答题任务列表信息</returns>
|
2026-06-24 16:32:59 +08:00
|
|
|
|
public async Task<UserAnswerTaskListOutput> GetUserAnswerTaskListAsync(long catalogId, long userId)
|
|
|
|
|
|
{
|
2026-06-29 15:09:23 +08:00
|
|
|
|
#region 1. 查询二级栏目目录
|
|
|
|
|
|
|
2026-06-25 14:53:45 +08:00
|
|
|
|
// 1. 查询目录(二级栏目)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
var catalog = await catalogRepository.Context.Queryable<JournalCatalog>()
|
2026-07-01 08:18:07 +08:00
|
|
|
|
.Where(c => c.Id == catalogId)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
.FirstAsync();
|
|
|
|
|
|
|
|
|
|
|
|
if (catalog == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-29 15:09:23 +08:00
|
|
|
|
#endregion
|
|
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
#region 2. 验证用户是否绑定该杂志
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
// 验证 UserJournal 表,确认用户是否绑定了该杂志
|
|
|
|
|
|
// 如果用户没有绑定该杂志,直接返回 null
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
var isBound = await userJournalRepository.Context.Queryable<UserJournal>()
|
|
|
|
|
|
.Where(uj => uj.UserId == userId && uj.JournalId == catalog.JournalId)
|
|
|
|
|
|
.AnyAsync();
|
|
|
|
|
|
|
|
|
|
|
|
if (!isBound)
|
|
|
|
|
|
{
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region 3. 获取一级栏目名称
|
2026-06-29 15:09:23 +08:00
|
|
|
|
|
2026-06-25 14:53:45 +08:00
|
|
|
|
// ============================================
|
|
|
|
|
|
// 【获取一级栏目名称】
|
|
|
|
|
|
// 通过 ParentId 关联查询一级栏目名称
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
string firstLevelCatalogName = string.Empty;
|
|
|
|
|
|
if (catalog.ParentId > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
var firstLevelCatalog = await catalogRepository.Context.Queryable<JournalCatalog>()
|
2026-07-01 08:18:07 +08:00
|
|
|
|
.Where(c => c.Id == catalog.ParentId)
|
2026-06-25 14:53:45 +08:00
|
|
|
|
.FirstAsync();
|
|
|
|
|
|
firstLevelCatalogName = firstLevelCatalog?.Name ?? string.Empty;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-29 15:09:23 +08:00
|
|
|
|
#endregion
|
|
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
#region 4. 批量加载关联数据
|
2026-06-29 15:09:23 +08:00
|
|
|
|
|
2026-06-25 14:53:45 +08:00
|
|
|
|
// ============================================
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 批量加载关联数据
|
2026-06-25 14:53:45 +08:00
|
|
|
|
// ============================================
|
|
|
|
|
|
// 该栏目下的所有页面
|
2026-06-24 16:32:59 +08:00
|
|
|
|
var allPages = await pageRepository.Context.Queryable<JournalPage>()
|
2026-07-01 08:18:07 +08:00
|
|
|
|
.Where(p => p.JournalCatalogId == catalogId)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
.ToListAsync();
|
|
|
|
|
|
|
2026-06-25 14:53:45 +08:00
|
|
|
|
// 该期刊下所有任务(用于跨页题目关联查询)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
var allTasks = await taskRepository.Context.Queryable<JournalPageTask>()
|
2026-07-01 08:18:07 +08:00
|
|
|
|
.Where(t => t.JournalId == catalog.JournalId)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
.ToListAsync();
|
|
|
|
|
|
|
2026-06-25 14:53:45 +08:00
|
|
|
|
// 该用户在该期刊下所有作答记录
|
2026-06-24 16:32:59 +08:00
|
|
|
|
var userAnswers = await answerRepository.Context.Queryable<JournalPageTaskUserAnswer>()
|
2026-07-01 08:18:07 +08:00
|
|
|
|
.Where(a => a.JournalId == catalog.JournalId && a.UserId == userId)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
.ToListAsync();
|
|
|
|
|
|
|
2026-06-29 15:09:23 +08:00
|
|
|
|
#endregion
|
|
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
#region 5. 获取该目录下的页面ID及任务
|
2026-06-29 15:09:23 +08:00
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 获取该目录下的所有页面ID
|
2026-06-24 16:32:59 +08:00
|
|
|
|
var pageIds = allPages.Select(p => p.Id).ToHashSet();
|
|
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 该目录下的所有任务(仅用于筛选哪些GroupId属于当前栏目)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
var catalogTasks = allTasks.Where(t => pageIds.Contains(t.JournalPageId)).ToList();
|
|
|
|
|
|
|
2026-06-29 15:09:23 +08:00
|
|
|
|
// 【跨页题目-关键】
|
|
|
|
|
|
// 跨页题目的任务可能分布在不同页面(甚至不同栏目)下
|
|
|
|
|
|
// 所以需要从整个期刊的所有任务(allTasks)中按GroupId获取完整的任务组
|
|
|
|
|
|
// 否则无法正确判断跨页题目是否全部完成
|
|
|
|
|
|
var catalogGroupIds = catalogTasks.Select(t => t.GroupId).ToHashSet();
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 用户已作答的任务ID集合(包含进行中和已完成)
|
2026-06-29 15:09:23 +08:00
|
|
|
|
var userAnsweredTaskIds = userAnswers.Select(a => a.JournalPageTaskId).ToHashSet();
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
|
|
|
|
|
// 用户已完成的任务ID集合(Status=1表示已完成)
|
2026-06-29 15:09:23 +08:00
|
|
|
|
var userCompletedTaskIds = userAnswers.Where(a => a.Status == (int)UserAnswerStatusEnum.Complete).Select(a => a.JournalPageTaskId).ToHashSet();
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 计算页码范围
|
2026-06-29 15:09:23 +08:00
|
|
|
|
var startPage = allPages.Count > 0 ? allPages.Min(p => p.PageNum) : 0;
|
|
|
|
|
|
var endPage = allPages.Count > 0 ? allPages.Max(p => p.PageNum) : 0;
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
#region 6. 按GroupId分组生成任务列表
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
2026-06-25 14:53:45 +08:00
|
|
|
|
// ============================================
|
2026-06-29 15:09:23 +08:00
|
|
|
|
// 【跨页题目-按GroupId分组生成任务列表】
|
|
|
|
|
|
// 关键:从整个期刊的所有任务中按GroupId取完整分组
|
|
|
|
|
|
// 每个GroupId对应一个任务项
|
|
|
|
|
|
// - 任务名称:取 Id == GroupId 的主任务的Task字段
|
|
|
|
|
|
// - 成长值/积分:取主任务的,每个GroupId只算一次
|
|
|
|
|
|
// - 状态:
|
|
|
|
|
|
// - 未开始:该GroupId下无任何任务被作答
|
|
|
|
|
|
// - 已完成:该GroupId下所有任务都完成(Status=1)
|
|
|
|
|
|
// - 进行中:有作答但未全部完成
|
2026-06-25 14:53:45 +08:00
|
|
|
|
// ============================================
|
2026-06-29 15:09:23 +08:00
|
|
|
|
var taskItems = new List<UserAnswerTaskItemOutput>();
|
|
|
|
|
|
|
|
|
|
|
|
// 从整个期刊的所有任务中,筛选出属于当前栏目的GroupId的完整分组
|
|
|
|
|
|
var groupTasks = allTasks
|
|
|
|
|
|
.Where(t => catalogGroupIds.Contains(t.GroupId))
|
|
|
|
|
|
.GroupBy(t => t.GroupId)
|
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var group in groupTasks)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
{
|
2026-06-29 15:09:23 +08:00
|
|
|
|
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 = "已完成";
|
2026-06-24 16:32:59 +08:00
|
|
|
|
else
|
2026-06-29 15:09:23 +08:00
|
|
|
|
taskStatus = "进行中";
|
|
|
|
|
|
|
|
|
|
|
|
taskItems.Add(new UserAnswerTaskItemOutput
|
|
|
|
|
|
{
|
|
|
|
|
|
GroupId = groupId,
|
|
|
|
|
|
TaskName = mainTask.Task ?? string.Empty,
|
2026-07-01 10:27:19 +08:00
|
|
|
|
TotalGrowthPoints = mainTask.GrowthPoint,
|
2026-06-29 15:09:23 +08:00
|
|
|
|
TotalPoints = mainTask.Points,
|
|
|
|
|
|
AnswerMinutes = groupAnswerMinutes,
|
|
|
|
|
|
Status = taskStatus
|
|
|
|
|
|
});
|
2026-06-24 16:32:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-29 15:09:23 +08:00
|
|
|
|
#endregion
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
#region 7. 返回结果
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
|
|
|
|
|
return new UserAnswerTaskListOutput
|
|
|
|
|
|
{
|
2026-06-25 14:53:45 +08:00
|
|
|
|
FirstLevelCatalogName = firstLevelCatalogName,
|
2026-06-24 16:32:59 +08:00
|
|
|
|
CatalogName = catalog.Name ?? string.Empty,
|
|
|
|
|
|
StartPage = startPage,
|
|
|
|
|
|
EndPage = endPage,
|
2026-06-29 15:09:23 +08:00
|
|
|
|
Tasks = taskItems
|
2026-06-24 16:32:59 +08:00
|
|
|
|
};
|
2026-06-29 15:09:23 +08:00
|
|
|
|
|
|
|
|
|
|
#endregion
|
2026-06-24 16:32:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-06-25 14:53:45 +08:00
|
|
|
|
/// ============================================
|
|
|
|
|
|
/// 方法思路:获取跨页题目详情
|
2026-06-29 15:09:23 +08:00
|
|
|
|
/// 1. 根据 groupId 查询该分组下的所有任务
|
|
|
|
|
|
/// 2. 跨页题目判定:
|
|
|
|
|
|
/// - 任务数 > 1 → 跨页题目
|
|
|
|
|
|
/// - 任务数 == 1 且 Id != groupId → 跨页题目(子任务进入)
|
|
|
|
|
|
/// - 任务数 == 1 且 Id == groupId → 普通题目
|
2026-06-25 14:53:45 +08:00
|
|
|
|
/// 3. 图片数组获取逻辑:
|
2026-06-29 15:09:23 +08:00
|
|
|
|
/// - 已答题:使用用户上传的 PageAnswerUrl 数组
|
|
|
|
|
|
/// - 未答题:使用任务的 TaskUrl 数组补齐
|
2026-06-25 14:53:45 +08:00
|
|
|
|
/// 4. 栏目名称获取:
|
2026-06-29 15:09:23 +08:00
|
|
|
|
/// - 二级栏目名称:通过页面关联的 JournalCatalogId 查询
|
|
|
|
|
|
/// - 一级栏目名称:通过二级栏目的 ParentId 查询
|
|
|
|
|
|
/// 5. 是否可领取积分判断:
|
|
|
|
|
|
/// - 任务必须已完成(Status=1)
|
|
|
|
|
|
/// - 跨页题目:所有任务都完成才能领取
|
|
|
|
|
|
/// - 普通题目:当前任务完成即可领取
|
|
|
|
|
|
/// - 且未领取过积分
|
2026-06-25 14:53:45 +08:00
|
|
|
|
/// ============================================
|
2026-06-24 16:32:59 +08:00
|
|
|
|
/// </summary>
|
2026-07-01 08:18:07 +08:00
|
|
|
|
/// <param name="groupId">任务分组ID(JournalPageTaskGroupId)</param>
|
2026-06-25 14:53:45 +08:00
|
|
|
|
/// <param name="userId">用户ID</param>
|
|
|
|
|
|
/// <returns>跨页题目详情</returns>
|
2026-06-29 15:09:23 +08:00
|
|
|
|
public async Task<CrossPageTaskOutput> GetCrossPageTaskAsync(long groupId, long userId)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
{
|
2026-07-01 08:18:07 +08:00
|
|
|
|
#region 0. 查询任务并验证用户绑定
|
|
|
|
|
|
|
2026-06-29 15:09:23 +08:00
|
|
|
|
// 1. 查询该 GroupId 下的所有任务
|
|
|
|
|
|
var allTasks = await taskRepository.Context.Queryable<JournalPageTask>()
|
2026-07-01 08:18:07 +08:00
|
|
|
|
.Where(t => t.GroupId == groupId)
|
2026-06-29 15:09:23 +08:00
|
|
|
|
.OrderBy(t => t.Id)
|
|
|
|
|
|
.ToListAsync();
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
2026-06-29 15:09:23 +08:00
|
|
|
|
if (allTasks == null || allTasks.Count == 0)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
{
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// ============================================
|
|
|
|
|
|
// 验证 UserJournal 表,确认用户是否绑定了该杂志
|
|
|
|
|
|
// 如果用户没有绑定该杂志,直接返回 null
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
var journalId = allTasks[0].JournalId;
|
|
|
|
|
|
var isBound = await userJournalRepository.Context.Queryable<UserJournal>()
|
|
|
|
|
|
.Where(uj => uj.UserId == userId && uj.JournalId == journalId)
|
|
|
|
|
|
.AnyAsync();
|
|
|
|
|
|
|
|
|
|
|
|
if (!isBound)
|
|
|
|
|
|
{
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
2026-06-29 15:09:23 +08:00
|
|
|
|
#region 1. 跨页题目判定
|
|
|
|
|
|
|
2026-06-25 14:53:45 +08:00
|
|
|
|
// ============================================
|
|
|
|
|
|
// 【跨页题目判定】
|
2026-06-29 15:09:23 +08:00
|
|
|
|
// 判断标准:
|
|
|
|
|
|
// - 任务数 > 1 → 跨页题目
|
|
|
|
|
|
// - 任务数 == 1 且 Id != groupId → 跨页题目(子任务进入)
|
|
|
|
|
|
// - 任务数 == 1 且 Id == groupId → 普通题目
|
2026-06-25 14:53:45 +08:00
|
|
|
|
// ============================================
|
2026-06-29 15:09:23 +08:00
|
|
|
|
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. 获取相关任务列表
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
2026-06-25 14:53:45 +08:00
|
|
|
|
// ============================================
|
2026-06-29 15:09:23 +08:00
|
|
|
|
// 【获取相关任务列表】
|
|
|
|
|
|
// 跨页题目:allTasks 即为相关任务列表
|
|
|
|
|
|
// 非跨页题目:单任务列表
|
2026-06-25 14:53:45 +08:00
|
|
|
|
// ============================================
|
2026-06-29 15:09:23 +08:00
|
|
|
|
List<JournalPageTask> relatedTasks = allTasks;
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region 3. 获取栏目名称(一级/二级)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
2026-06-25 14:53:45 +08:00
|
|
|
|
// ============================================
|
|
|
|
|
|
// 【获取栏目名称】
|
|
|
|
|
|
// 二级栏目名称:通过页面关联的 JournalCatalogId 查询
|
|
|
|
|
|
// 一级栏目名称:通过二级栏目的 ParentId 查询
|
|
|
|
|
|
// ============================================
|
2026-06-24 16:32:59 +08:00
|
|
|
|
var page = await pageRepository.Context.Queryable<JournalPage>()
|
2026-07-01 08:18:07 +08:00
|
|
|
|
.Where(p => p.Id == baseTask.JournalPageId)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
.FirstAsync();
|
|
|
|
|
|
|
|
|
|
|
|
string catalogName = string.Empty;
|
2026-06-25 14:53:45 +08:00
|
|
|
|
string firstLevelCatalogName = string.Empty;
|
2026-06-24 16:32:59 +08:00
|
|
|
|
if (page != null)
|
|
|
|
|
|
{
|
2026-06-29 15:09:23 +08:00
|
|
|
|
// 获取二级栏目
|
2026-06-24 16:32:59 +08:00
|
|
|
|
var catalog = await catalogRepository.Context.Queryable<JournalCatalog>()
|
2026-07-01 08:18:07 +08:00
|
|
|
|
.Where(c => c.Id == page.JournalCatalogId)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
.FirstAsync();
|
|
|
|
|
|
catalogName = catalog?.Name ?? string.Empty;
|
2026-06-25 14:53:45 +08:00
|
|
|
|
|
|
|
|
|
|
// 获取一级栏目名称
|
|
|
|
|
|
if (catalog != null && catalog.ParentId > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
var firstLevelCatalog = await catalogRepository.Context.Queryable<JournalCatalog>()
|
2026-07-01 08:18:07 +08:00
|
|
|
|
.Where(c => c.Id == catalog.ParentId)
|
2026-06-25 14:53:45 +08:00
|
|
|
|
.FirstAsync();
|
|
|
|
|
|
firstLevelCatalogName = firstLevelCatalog?.Name ?? string.Empty;
|
|
|
|
|
|
}
|
2026-06-24 16:32:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-29 15:09:23 +08:00
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region 4. 获取图片数组(Images)
|
|
|
|
|
|
|
2026-06-25 14:53:45 +08:00
|
|
|
|
// ============================================
|
|
|
|
|
|
// 【获取图片数组】
|
2026-06-29 15:09:23 +08:00
|
|
|
|
// 核心逻辑:已答题返回 PageAnswerUrl,未答题返回 TaskUrl 补齐
|
|
|
|
|
|
// 跨页题目:
|
|
|
|
|
|
// - 用户答了部分:已答页用 PageAnswerUrl,未答页用 TaskUrl 补齐
|
|
|
|
|
|
// - 用户全部作答:全部用 PageAnswerUrl
|
|
|
|
|
|
// - 用户未作答:全部用 TaskUrl
|
|
|
|
|
|
// - 按主页面页码(PageNum)升序排列,确保拼图顺序正确
|
|
|
|
|
|
// 非跨页题目:取单条答题记录的 PageAnswerUrl,未答题返回空集合
|
2026-06-25 14:53:45 +08:00
|
|
|
|
// ============================================
|
2026-06-24 16:32:59 +08:00
|
|
|
|
var images = new List<string>();
|
2026-06-29 15:09:23 +08:00
|
|
|
|
|
|
|
|
|
|
// 统一查询用户答题记录(用于图片获取和后续领取积分判断)
|
|
|
|
|
|
List<JournalPageTaskUserAnswer> userAnswers;
|
2026-06-24 16:32:59 +08:00
|
|
|
|
if (isCrossPage)
|
|
|
|
|
|
{
|
2026-06-25 14:53:45 +08:00
|
|
|
|
// 跨页题目:查询该 GroupId 下所有用户的答题记录
|
2026-06-29 15:09:23 +08:00
|
|
|
|
userAnswers = await answerRepository.Context.Queryable<JournalPageTaskUserAnswer>()
|
2026-07-01 08:18:07 +08:00
|
|
|
|
.Where(a => a.JournalPageTaskGroupId == groupId && a.UserId == userId)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
.OrderBy(a => a.JournalPageTaskId)
|
|
|
|
|
|
.ToListAsync();
|
|
|
|
|
|
|
2026-06-29 15:09:23 +08:00
|
|
|
|
// ============================================
|
|
|
|
|
|
// 【跨页题目图片获取逻辑】
|
|
|
|
|
|
// 1. 收集相关任务所在页面的页码信息
|
|
|
|
|
|
// 2. 遍历每个相关任务:已答用 PageAnswerUrl,未答用 TaskUrl 补齐
|
|
|
|
|
|
// 3. 按页码升序排序后输出,确保拼图顺序正确
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
// 查询相关任务对应的页面信息(用于排序)
|
|
|
|
|
|
var pageIds = relatedTasks.Select(t => t.JournalPageId).Distinct().ToList();
|
|
|
|
|
|
var pageInfoList = await pageRepository.Context.Queryable<JournalPage>()
|
2026-07-01 08:18:07 +08:00
|
|
|
|
.Where(p => pageIds.Contains(p.Id))
|
2026-06-29 15:09:23 +08:00
|
|
|
|
.ToListAsync();
|
|
|
|
|
|
var pageInfoDict = pageInfoList.ToDictionary(p => p.Id, p => p.PageNum);
|
|
|
|
|
|
|
|
|
|
|
|
// 组装图片项:(页码, 图片地址)
|
|
|
|
|
|
var imageItems = new List<(int PageNum, string Url)>();
|
|
|
|
|
|
foreach (var relatedTask in relatedTasks)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
{
|
2026-06-29 15:09:23 +08:00
|
|
|
|
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))
|
2026-06-24 16:32:59 +08:00
|
|
|
|
{
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 已答题:使用 PageAnswerUrl(加上域名)
|
|
|
|
|
|
imageUrl = DomainHelper.OssFullUrl(userAnswer.PageAnswerUrl);
|
2026-06-29 15:09:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
else if (!string.IsNullOrEmpty(relatedTask.TaskUrl))
|
|
|
|
|
|
{
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 未答题或无 PageAnswerUrl:使用任务的 TaskUrl 补齐(加上域名)
|
|
|
|
|
|
imageUrl = DomainHelper.OssFullUrl(relatedTask.TaskUrl);
|
2026-06-24 16:32:59 +08:00
|
|
|
|
}
|
2026-06-29 15:09:23 +08:00
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
// 无任何图片可用,跳过
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
imageItems.Add((pageNum, imageUrl));
|
2026-06-24 16:32:59 +08:00
|
|
|
|
}
|
2026-06-29 15:09:23 +08:00
|
|
|
|
|
|
|
|
|
|
// 按页码升序排序后输出图片地址
|
|
|
|
|
|
images.AddRange(imageItems.OrderBy(x => x.PageNum).Select(x => x.Url));
|
2026-06-24 16:32:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
// 非跨页题目:查询单条答题记录
|
|
|
|
|
|
var userAnswer = await answerRepository.Context.Queryable<JournalPageTaskUserAnswer>()
|
2026-07-01 08:18:07 +08:00
|
|
|
|
.Where(a => a.JournalPageTaskId == mainTask.Id && a.UserId == userId)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
.FirstAsync();
|
|
|
|
|
|
|
2026-06-29 15:09:23 +08:00
|
|
|
|
// 赋值给 userAnswers(用于后续领取积分判断)
|
|
|
|
|
|
userAnswers = userAnswer != null ? new List<JournalPageTaskUserAnswer> { userAnswer } : new List<JournalPageTaskUserAnswer>();
|
|
|
|
|
|
|
2026-06-24 16:32:59 +08:00
|
|
|
|
if (userAnswer != null && !string.IsNullOrEmpty(userAnswer.PageAnswerUrl))
|
|
|
|
|
|
{
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 已答题,使用 PageAnswerUrl(加上域名)
|
|
|
|
|
|
images.Add(DomainHelper.OssFullUrl(userAnswer.PageAnswerUrl));
|
2026-06-24 16:32:59 +08:00
|
|
|
|
}
|
2026-06-29 15:09:23 +08:00
|
|
|
|
// 未答题不返回任何图片,images 保持空集合
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region 5. 获取页面作答点读图片集合(PageAnswerDotUrl)
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
// 【获取页面作答点读图片地址集合】
|
|
|
|
|
|
// 规则:已答题且 PageAnswerDotUrl 有值才返回;未答题或字段为空则跳过
|
|
|
|
|
|
// 跨页题目:遍历所有相关任务,收集有答题记录的 PageAnswerDotUrl
|
|
|
|
|
|
// 非跨页题目:取当前任务答题记录的 PageAnswerDotUrl(仅在有值时返回)
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
var pageAnswerDotUrls = new List<string>();
|
|
|
|
|
|
if (isCrossPage)
|
|
|
|
|
|
{
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 跨页题目:遍历相关任务,收集有答题记录的 PageAnswerDotUrl(加上域名)
|
2026-06-29 15:09:23 +08:00
|
|
|
|
foreach (var relatedTask in relatedTasks)
|
|
|
|
|
|
{
|
|
|
|
|
|
var userAnswer = userAnswers.FirstOrDefault(a => a.JournalPageTaskId == relatedTask.Id);
|
|
|
|
|
|
if (userAnswer != null && !string.IsNullOrEmpty(userAnswer.PageAnswerDotUrl))
|
|
|
|
|
|
{
|
2026-07-01 08:18:07 +08:00
|
|
|
|
pageAnswerDotUrls.Add(DomainHelper.OssFullUrl(userAnswer.PageAnswerDotUrl));
|
2026-06-29 15:09:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 非跨页题目:取当前任务答题记录的 PageAnswerDotUrl(加上域名)
|
2026-06-29 15:09:23 +08:00
|
|
|
|
// 仅在已答题且 PageAnswerDotUrl 有值时返回
|
|
|
|
|
|
if (userAnswers != null && userAnswers.Count > 0)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
{
|
2026-06-29 15:09:23 +08:00
|
|
|
|
var userAnswer = userAnswers.FirstOrDefault();
|
|
|
|
|
|
if (userAnswer != null && !string.IsNullOrEmpty(userAnswer.PageAnswerDotUrl))
|
2026-06-24 16:32:59 +08:00
|
|
|
|
{
|
2026-07-01 08:18:07 +08:00
|
|
|
|
pageAnswerDotUrls.Add(DomainHelper.OssFullUrl(userAnswer.PageAnswerDotUrl));
|
2026-06-24 16:32:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-29 15:09:23 +08:00
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region 6. 判断是否可领取积分
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
// 【是否可领取积分判断】
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 条件:任务已完成且未领取过积分
|
|
|
|
|
|
// - Status = 1(已批阅)
|
|
|
|
|
|
// - AnswerStatus >= 1(已答题:正确/半对/错误)
|
|
|
|
|
|
// 跨页题目:所有任务都满足条件才能领取
|
|
|
|
|
|
// 普通题目:当前任务满足条件即可领取
|
|
|
|
|
|
// 【关键】子题目入口(mainTask == null)无法领取积分
|
2026-06-29 15:09:23 +08:00
|
|
|
|
// ============================================
|
|
|
|
|
|
bool canClaimPoints = false;
|
|
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 【关键】如果主任务不存在(子题目入口),无法领取积分
|
|
|
|
|
|
if (mainTask == null)
|
2026-06-29 15:09:23 +08:00
|
|
|
|
{
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 子题目无法单独领取积分,CanClaimPoints 为 false
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
// 查询是否已领取过积分(用主任务的 GroupId 作为 RelatedId)
|
|
|
|
|
|
var existingRecord = await answerRepository.Context.Queryable<PointsRecord>()
|
|
|
|
|
|
.Where(r => r.UserId == userId)
|
|
|
|
|
|
.Where(r => r.ChangeType == PointsChangeTypeEnum.TaskReward.ToString() && r.RelatedId == mainTask.GroupId)
|
|
|
|
|
|
.AnyAsync();
|
|
|
|
|
|
|
|
|
|
|
|
if (!existingRecord)
|
2026-06-29 15:09:23 +08:00
|
|
|
|
{
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 未领取过积分,检查任务是否完成
|
2026-07-01 09:20:07 +08:00
|
|
|
|
// 条件:Status=1
|
|
|
|
|
|
canClaimPoints = relatedTasks.All(t => userAnswers.Any(a => a.JournalPageTaskId == t.Id && a.Status == (int)UserAnswerStatusEnum.Complete));
|
2026-06-29 15:09:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region 7. 获取任务名称
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
// 【获取任务名称】
|
|
|
|
|
|
// 跨页题目:取主任务(Id == GroupId)的 Task 字段
|
|
|
|
|
|
// 普通题目:取当前任务的 Task 字段
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 子题目入口:查询主任务的 Task 字段
|
2026-06-29 15:09:23 +08:00
|
|
|
|
// ============================================
|
|
|
|
|
|
string taskName;
|
2026-07-01 08:18:07 +08:00
|
|
|
|
if (mainTask != null)
|
2026-06-29 15:09:23 +08:00
|
|
|
|
{
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 主任务入口,直接使用 mainTask
|
|
|
|
|
|
taskName = mainTask.Task ?? string.Empty;
|
2026-06-29 15:09:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 子题目入口:查询主任务(Id == GroupId)
|
|
|
|
|
|
var realMainTask = allTasks.FirstOrDefault(t => t.Id == groupId);
|
|
|
|
|
|
if (realMainTask != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
taskName = realMainTask.Task ?? string.Empty;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
// 通过 GroupId 查询主任务(适用于子任务入口的情况)
|
|
|
|
|
|
var mainTaskByGroupId = await taskRepository.Context.Queryable<JournalPageTask>()
|
|
|
|
|
|
.Where(t => t.Id == t.GroupId && t.GroupId == groupId)
|
|
|
|
|
|
.FirstAsync();
|
|
|
|
|
|
taskName = mainTaskByGroupId?.Task ?? string.Empty;
|
|
|
|
|
|
}
|
2026-06-29 15:09:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region 8. 返回结果
|
|
|
|
|
|
|
2026-06-24 16:32:59 +08:00
|
|
|
|
return new CrossPageTaskOutput
|
|
|
|
|
|
{
|
2026-06-25 14:53:45 +08:00
|
|
|
|
FirstLevelCatalogName = firstLevelCatalogName,
|
2026-06-24 16:32:59 +08:00
|
|
|
|
CatalogName = catalogName,
|
2026-06-29 15:09:23 +08:00
|
|
|
|
TaskName = taskName,
|
2026-06-24 16:32:59 +08:00
|
|
|
|
Images = images,
|
2026-06-29 15:09:23 +08:00
|
|
|
|
PageAnswerDotUrl = pageAnswerDotUrls,
|
|
|
|
|
|
IsCrossPage = isCrossPage,
|
|
|
|
|
|
CanClaimPoints = canClaimPoints
|
2026-06-24 16:32:59 +08:00
|
|
|
|
};
|
2026-06-29 15:09:23 +08:00
|
|
|
|
|
|
|
|
|
|
#endregion
|
2026-06-24 16:32:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-06-25 14:53:45 +08:00
|
|
|
|
/// ============================================
|
|
|
|
|
|
/// 方法思路:领取任务积分
|
|
|
|
|
|
/// 1. 根据 GroupId 查询任务信息,判断是否跨页题目
|
2026-07-01 08:18:07 +08:00
|
|
|
|
/// 2. 查询用户答题记录,校验主任务(JournalPageTaskId == GroupId)才能领取
|
|
|
|
|
|
/// 3. 完成校验:Status=1 且 AnswerStatus>=1,跨页题目需所有任务都完成
|
2026-06-25 14:53:45 +08:00
|
|
|
|
/// 4. 重复领取校验:通过 PointsRecord 表的 RelatedId + ChangeType 判断
|
2026-07-01 08:18:07 +08:00
|
|
|
|
/// 5. 计算积分、成长值、得分(均取主任务数据)
|
|
|
|
|
|
/// 6. 调用积分服务增加积分
|
2026-06-25 14:53:45 +08:00
|
|
|
|
/// ============================================
|
2026-06-24 16:32:59 +08:00
|
|
|
|
/// </summary>
|
2026-06-29 15:09:23 +08:00
|
|
|
|
/// <param name="groupId">任务分组Id(JournalPageTask.GroupId)</param>
|
2026-06-25 14:53:45 +08:00
|
|
|
|
/// <param name="userId">用户ID</param>
|
2026-07-01 08:18:07 +08:00
|
|
|
|
/// <returns>领取积分结果(总成长值、总积分、得分、任务编号)</returns>
|
2026-06-29 15:09:23 +08:00
|
|
|
|
public async Task<ClaimTaskPointsOutput> ClaimTaskPointsAsync(long groupId, long userId)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
{
|
2026-07-01 08:18:07 +08:00
|
|
|
|
#region 1. 查询任务与答题记录
|
2026-06-25 14:53:45 +08:00
|
|
|
|
// ============================================
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 1. 查询任务与答题记录
|
2026-06-25 14:53:45 +08:00
|
|
|
|
// ============================================
|
|
|
|
|
|
var relatedTasks = await taskRepository.Context.Queryable<JournalPageTask>()
|
2026-07-01 08:18:07 +08:00
|
|
|
|
.Where(t => t.GroupId == groupId)
|
2026-06-25 14:53:45 +08:00
|
|
|
|
.ToListAsync();
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
2026-06-25 14:53:45 +08:00
|
|
|
|
if (relatedTasks == null || relatedTasks.Count == 0)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
{
|
2026-06-29 16:34:26 +08:00
|
|
|
|
throw new BusinessException("任务不存在", ResultCode.NOT_FOUND);
|
2026-06-24 16:32:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 主任务:Id == GroupId
|
|
|
|
|
|
var mainTask = relatedTasks.FirstOrDefault(t => t.Id == groupId);
|
|
|
|
|
|
if (mainTask == null)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
{
|
2026-07-01 15:04:42 +08:00
|
|
|
|
throw new BusinessException("跨页题目需要完成所有任务才能领取积分", ResultCode.BAD_REQUEST);
|
2026-06-24 16:32:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 查询用户答题记录
|
2026-06-25 14:53:45 +08:00
|
|
|
|
var userAnswers = await answerRepository.Context.Queryable<JournalPageTaskUserAnswer>()
|
2026-07-01 08:18:07 +08:00
|
|
|
|
.Where(a => a.JournalPageTaskGroupId == groupId && a.UserId == userId)
|
2026-06-25 14:53:45 +08:00
|
|
|
|
.ToListAsync();
|
|
|
|
|
|
|
2026-06-24 16:32:59 +08:00
|
|
|
|
if (userAnswers == null || userAnswers.Count == 0)
|
|
|
|
|
|
{
|
2026-06-29 16:34:26 +08:00
|
|
|
|
throw new BusinessException("请先完成答题再领取积分", ResultCode.BAD_REQUEST);
|
2026-06-24 16:32:59 +08:00
|
|
|
|
}
|
2026-06-29 15:09:23 +08:00
|
|
|
|
#endregion
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
#region 2. 完成校验
|
2026-06-25 14:53:45 +08:00
|
|
|
|
// ============================================
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 2. 完成校验
|
|
|
|
|
|
// 主任务校验 + 跨页题目所有任务校验
|
2026-07-01 09:20:07 +08:00
|
|
|
|
// 完成条件:Status=1
|
2026-06-25 14:53:45 +08:00
|
|
|
|
// ============================================
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 校验主任务
|
|
|
|
|
|
var mainTaskAnswer = userAnswers.FirstOrDefault(a => a.JournalPageTaskId == groupId);
|
2026-07-01 09:20:07 +08:00
|
|
|
|
if (mainTaskAnswer == null || mainTaskAnswer.Status != (int)UserAnswerStatusEnum.Complete)
|
2026-07-01 08:18:07 +08:00
|
|
|
|
{
|
|
|
|
|
|
throw new BusinessException("请先完成答题再领取积分", ResultCode.BAD_REQUEST);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 跨页题目:校验所有任务
|
|
|
|
|
|
if (relatedTasks.Count > 1)
|
2026-06-25 14:53:45 +08:00
|
|
|
|
{
|
|
|
|
|
|
foreach (var task in relatedTasks)
|
|
|
|
|
|
{
|
2026-07-01 08:18:07 +08:00
|
|
|
|
var answer = userAnswers.FirstOrDefault(a => a.JournalPageTaskId == task.Id);
|
2026-07-01 09:20:07 +08:00
|
|
|
|
if (answer == null || answer.Status != (int)UserAnswerStatusEnum.Complete)
|
2026-06-25 14:53:45 +08:00
|
|
|
|
{
|
2026-06-29 16:34:26 +08:00
|
|
|
|
throw new BusinessException("跨页题目需要完成所有任务才能领取积分", ResultCode.BAD_REQUEST);
|
2026-06-25 14:53:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-06-29 15:09:23 +08:00
|
|
|
|
#endregion
|
2026-06-25 14:53:45 +08:00
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
#region 3. 重复领取校验与积分计算
|
2026-06-25 14:53:45 +08:00
|
|
|
|
// ============================================
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 3. 重复领取校验与积分计算
|
2026-06-25 14:53:45 +08:00
|
|
|
|
// ============================================
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 重复领取校验
|
|
|
|
|
|
var existingRecord = await answerRepository.Context.Queryable<PointsRecord>()
|
|
|
|
|
|
.Where(r => r.UserId == userId)
|
2026-07-01 10:27:19 +08:00
|
|
|
|
.Where(r => r.ChangeType == nameof(PointsChangeTypeEnum.TaskReward) && r.RelatedId == groupId)
|
2026-07-01 08:18:07 +08:00
|
|
|
|
.FirstAsync();
|
|
|
|
|
|
|
|
|
|
|
|
if (existingRecord != null)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
{
|
2026-07-01 08:18:07 +08:00
|
|
|
|
throw new BusinessException("积分已领取,请勿重复领取", ResultCode.BAD_REQUEST);
|
2026-06-24 16:32:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 积分、成长值、得分(均取主任务数据)
|
2026-07-01 15:04:42 +08:00
|
|
|
|
int totalPoints = mainTaskAnswer.Points;
|
|
|
|
|
|
int totalGrowthPoints = mainTaskAnswer.GrowthPoints;
|
2026-07-01 08:18:07 +08:00
|
|
|
|
float score = mainTaskAnswer.Score;
|
|
|
|
|
|
|
2026-06-24 16:32:59 +08:00
|
|
|
|
if (totalPoints <= 0)
|
|
|
|
|
|
{
|
2026-06-29 16:34:26 +08:00
|
|
|
|
throw new BusinessException("该任务无积分可领取", ResultCode.BAD_REQUEST);
|
2026-06-24 16:32:59 +08:00
|
|
|
|
}
|
2026-06-29 15:09:23 +08:00
|
|
|
|
#endregion
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
#region 4. 事务执行领取操作
|
2026-06-25 14:53:45 +08:00
|
|
|
|
// ============================================
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 4. 事务执行领取操作
|
|
|
|
|
|
// - 手动更新 Users.Points(积分累加)
|
|
|
|
|
|
// - 插入积分流水记录
|
|
|
|
|
|
// - 喂养宠物(如有活跃宠物,更新宠物成长值)
|
2026-06-25 14:53:45 +08:00
|
|
|
|
// ============================================
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 查询用户信息(获取当前积分)
|
|
|
|
|
|
var user = await usersRepository.GetByIdAsync(userId);
|
|
|
|
|
|
if (user == null)
|
2026-06-25 14:53:45 +08:00
|
|
|
|
{
|
2026-07-01 08:18:07 +08:00
|
|
|
|
throw new BusinessException("用户不存在", ResultCode.NOT_FOUND);
|
2026-06-25 14:53:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 查询用户活跃宠物
|
|
|
|
|
|
var pet = await userPetRepository.Context.Queryable<UserPet>()
|
|
|
|
|
|
.Where(p => p.UserId == userId && p.Status == (int)UserPetStatusEnum.Active)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
.FirstAsync();
|
|
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
var result = new ClaimTaskPointsOutput();
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
await answerRepository.UseTranAsync(async () =>
|
2026-06-24 16:32:59 +08:00
|
|
|
|
{
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// 4a. 手动更新 Users.Points(积分累加)
|
|
|
|
|
|
var newPointsBalance = user.Points + totalPoints;
|
2026-07-01 10:27:19 +08:00
|
|
|
|
// 4b. 手动更新 Users.GrowthPoints(成长值累加)
|
|
|
|
|
|
var newGrowthPointsBalance = user.GrowthPoints + totalGrowthPoints;
|
2026-07-01 08:18:07 +08:00
|
|
|
|
await answerRepository.Context.Updateable<Users>()
|
|
|
|
|
|
.SetColumns(u => u.Points == newPointsBalance)
|
2026-07-01 10:27:19 +08:00
|
|
|
|
.SetColumns(u => u.GrowthPoints == newGrowthPointsBalance)
|
2026-07-01 15:04:42 +08:00
|
|
|
|
.SetColumns(u => u.UpdatedBy == userId.ToString())
|
2026-07-01 08:18:07 +08:00
|
|
|
|
.SetColumns(u => u.UpdatedAt == DateTime.Now)
|
|
|
|
|
|
.Where(u => u.Id == userId)
|
|
|
|
|
|
.ExecuteCommandAsync();
|
|
|
|
|
|
|
|
|
|
|
|
// 4b. 插入积分流水记录
|
|
|
|
|
|
var pointsRecord = new PointsRecord
|
|
|
|
|
|
{
|
|
|
|
|
|
UserId = userId,
|
|
|
|
|
|
ChangeAmount = totalPoints,
|
|
|
|
|
|
BalanceAfter = newPointsBalance,
|
|
|
|
|
|
ChangeType = PointsChangeTypeEnum.TaskReward.ToString(),
|
|
|
|
|
|
RelatedId = groupId,
|
2026-07-01 10:27:19 +08:00
|
|
|
|
Description = $"完成任务奖励,GroupId:{mainTask.GroupId}",
|
2026-07-01 08:18:07 +08:00
|
|
|
|
Type = PointsFlowTypeEnum.Income,
|
|
|
|
|
|
Status = (int)PointsRecordStatusEnum.Success,
|
|
|
|
|
|
IsDeleted = false,
|
|
|
|
|
|
CreatedBy = "系统",
|
|
|
|
|
|
CreatedAt = DateTime.Now,
|
|
|
|
|
|
UpdatedBy = "系统",
|
|
|
|
|
|
UpdatedAt = DateTime.Now
|
|
|
|
|
|
};
|
|
|
|
|
|
await answerRepository.Context.Insertable(pointsRecord).ExecuteCommandAsync();
|
|
|
|
|
|
|
|
|
|
|
|
// 4c. 如果有活跃宠物,喂养宠物(同一事务内,更新宠物成长值)
|
|
|
|
|
|
if (pet != null && totalGrowthPoints > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
await petService.FeedPetInTranAsync(userId, new Models.Dto.Pet.FeedPetInput
|
|
|
|
|
|
{
|
|
|
|
|
|
PetId = pet.Id,
|
|
|
|
|
|
GrowthPoints = totalGrowthPoints
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
// ============================================
|
|
|
|
|
|
// 【任务编号计算】
|
|
|
|
|
|
// 从 JournalPageTask.No 字段按 - 分割,取索引为 2 的部分
|
|
|
|
|
|
// 例如:"A-B-C-D" → 分割后 ["A","B","C","D"] → 取索引2 = "C"
|
|
|
|
|
|
// 前面加上"任务"
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
string taskNo = string.Empty;
|
|
|
|
|
|
if (!string.IsNullOrEmpty(mainTask.No))
|
2026-06-29 17:16:38 +08:00
|
|
|
|
{
|
2026-07-01 08:18:07 +08:00
|
|
|
|
var parts = mainTask.No.Split('-');
|
|
|
|
|
|
if (parts.Length >= 3)
|
|
|
|
|
|
{
|
|
|
|
|
|
taskNo = $"任务{parts[2]}";
|
|
|
|
|
|
}
|
2026-06-29 17:16:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
result.TotalGrowthPoints = totalGrowthPoints;
|
|
|
|
|
|
result.TotalPoints = totalPoints;
|
|
|
|
|
|
result.Score = score;
|
|
|
|
|
|
result.TaskNo = taskNo;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return result;
|
2026-06-29 15:09:23 +08:00
|
|
|
|
#endregion
|
2026-06-24 16:32:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2026-06-25 14:53:45 +08:00
|
|
|
|
/// ============================================
|
|
|
|
|
|
/// 方法思路:批量领取任务积分(一键领取)
|
2026-07-01 08:18:07 +08:00
|
|
|
|
/// 1. 遍历每个传入的 GroupId,做校验,筛选出可领取的任务
|
|
|
|
|
|
/// 2. 在事务中执行所有写操作:
|
|
|
|
|
|
/// - 更新 Users 表 Points(积分服务内部处理)
|
|
|
|
|
|
/// - 更新 Users 表 GrowthPoints(累加)
|
|
|
|
|
|
/// - 喂养宠物(如有活跃宠物)
|
|
|
|
|
|
/// 3. 返回结果包含:
|
2026-06-25 14:53:45 +08:00
|
|
|
|
/// - 成功数量和失败数量
|
|
|
|
|
|
/// - 成功详情列表(每个GroupId的名称、积分、成长值、得分)
|
|
|
|
|
|
/// - 失败详情列表(每个GroupId的错误原因)
|
|
|
|
|
|
/// - 总积分、总成长值、领取后余额
|
|
|
|
|
|
/// ============================================
|
2026-06-24 16:32:59 +08:00
|
|
|
|
/// </summary>
|
2026-06-25 14:53:45 +08:00
|
|
|
|
/// <param name="input">批量领取输入(GroupId列表)</param>
|
|
|
|
|
|
/// <param name="userId">用户ID</param>
|
|
|
|
|
|
/// <returns>批量领取结果</returns>
|
2026-06-24 16:32:59 +08:00
|
|
|
|
public async Task<BatchClaimPointsOutput> BatchClaimPointsAsync(BatchClaimPointsInput input, long userId)
|
|
|
|
|
|
{
|
2026-06-29 15:09:23 +08:00
|
|
|
|
#region 1. 初始化与参数校验
|
2026-06-24 16:32:59 +08:00
|
|
|
|
var result = new BatchClaimPointsOutput();
|
|
|
|
|
|
|
|
|
|
|
|
if (input == null || input.GroupIds == null || input.GroupIds.Count == 0)
|
|
|
|
|
|
{
|
2026-06-29 16:34:26 +08:00
|
|
|
|
throw new BusinessException("任务分组Id列表不能为空", ResultCode.BAD_REQUEST);
|
2026-06-24 16:32:59 +08:00
|
|
|
|
}
|
2026-07-01 15:04:42 +08:00
|
|
|
|
|
2026-06-29 15:09:23 +08:00
|
|
|
|
#endregion
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
2026-07-01 15:04:42 +08:00
|
|
|
|
#region 2. 批量查询所有数据(一次性查询,避免N+1)
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
// 2. 批量查询所有数据
|
|
|
|
|
|
// - 所有 GroupId 对应的任务(JournalPageTask)
|
|
|
|
|
|
// - 所有 GroupId 对应的用户答题记录(JournalPageTaskUserAnswer)
|
|
|
|
|
|
// - 已领取的积分记录(用于重复领取校验)
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
|
|
|
|
|
|
// 2.1 批量查询所有相关任务(按 GroupId 分组)
|
|
|
|
|
|
var allTasks = await taskRepository.Context.Queryable<JournalPageTask>()
|
|
|
|
|
|
.Where(t => input.GroupIds.Contains(t.GroupId))
|
|
|
|
|
|
.ToListAsync();
|
|
|
|
|
|
var tasksByGroupId = allTasks.GroupBy(t => t.GroupId).ToDictionary(g => g.Key, g => g.ToList());
|
|
|
|
|
|
|
|
|
|
|
|
// 2.2 批量查询所有用户答题记录(按 JournalPageTaskGroupId 分组)
|
|
|
|
|
|
var allUserAnswers = await answerRepository.Context.Queryable<JournalPageTaskUserAnswer>()
|
|
|
|
|
|
.Where(a => input.GroupIds.Contains(a.JournalPageTaskGroupId) && a.UserId == userId)
|
|
|
|
|
|
.ToListAsync();
|
|
|
|
|
|
var answersByGroupId = allUserAnswers.GroupBy(a => a.JournalPageTaskGroupId).ToDictionary(g => g.Key, g => g.ToList());
|
|
|
|
|
|
|
|
|
|
|
|
// 2.3 批量查询已领取的积分记录(用于重复领取校验)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
var claimedRecords = await answerRepository.Context.Queryable<PointsRecord>()
|
2026-07-01 15:04:42 +08:00
|
|
|
|
.Where(r => input.GroupIds.Contains((long)r.RelatedId!) && r.UserId == userId)
|
|
|
|
|
|
.Where(r => r.ChangeType == nameof(PointsChangeTypeEnum.TaskReward))
|
2026-06-24 16:32:59 +08:00
|
|
|
|
.ToListAsync();
|
2026-06-29 15:09:23 +08:00
|
|
|
|
var claimedGroupIds = claimedRecords.Select(r => r.RelatedId!.Value).ToHashSet();
|
|
|
|
|
|
#endregion
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
2026-07-01 15:04:42 +08:00
|
|
|
|
#region 3. 内存中筛选可领取的任务
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
// 3. 在内存中逐个校验每个 GroupId,筛选可领取的任务
|
|
|
|
|
|
// 所有数据已在步骤2中一次性查询完成,这里只做内存筛选
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
var claimableTasks = new List<(long GroupId, int Points, int GrowthPoints, float Score, string TaskNo)>();
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
2026-07-01 15:04:42 +08:00
|
|
|
|
foreach (var groupId in input.GroupIds)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-07-01 15:04:42 +08:00
|
|
|
|
// 3.1 从内存中获取相关任务
|
|
|
|
|
|
if (!tasksByGroupId.TryGetValue(groupId, out var relatedTasks) || relatedTasks.Count == 0)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
{
|
|
|
|
|
|
result.FailItems.Add(new BatchClaimFailItem
|
|
|
|
|
|
{
|
|
|
|
|
|
GroupId = groupId,
|
2026-07-01 15:04:42 +08:00
|
|
|
|
TaskNo = string.Empty,
|
2026-06-25 14:53:45 +08:00
|
|
|
|
Message = "任务不存在"
|
2026-06-24 16:32:59 +08:00
|
|
|
|
});
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-01 15:04:42 +08:00
|
|
|
|
// 3.2 判断是否跨页题目并找主任务
|
2026-07-01 08:18:07 +08:00
|
|
|
|
bool isCrossPage = relatedTasks.Count > 1;
|
|
|
|
|
|
var mainTask = relatedTasks.FirstOrDefault(t => t.Id == groupId);
|
2026-07-01 15:04:42 +08:00
|
|
|
|
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
// 【任务编号计算】
|
|
|
|
|
|
// 从 JournalPageTask.No 字段按 - 分割,取索引为 2 的部分,前面加"任务"
|
|
|
|
|
|
// 如果找不到主任务,尝试从子任务中计算
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
string taskNo = string.Empty;
|
|
|
|
|
|
var taskForNo = mainTask ?? relatedTasks.FirstOrDefault();
|
|
|
|
|
|
if (taskForNo != null && !string.IsNullOrEmpty(taskForNo.No))
|
|
|
|
|
|
{
|
|
|
|
|
|
var parts = taskForNo.No.Split('-');
|
|
|
|
|
|
if (parts.Length >= 3)
|
|
|
|
|
|
{
|
|
|
|
|
|
taskNo = $"任务{parts[2]}";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
if (mainTask == null)
|
2026-06-25 14:53:45 +08:00
|
|
|
|
{
|
2026-07-01 08:18:07 +08:00
|
|
|
|
result.FailItems.Add(new BatchClaimFailItem
|
|
|
|
|
|
{
|
|
|
|
|
|
GroupId = groupId,
|
2026-07-01 15:04:42 +08:00
|
|
|
|
TaskNo = taskNo,
|
2026-07-01 08:18:07 +08:00
|
|
|
|
Message = "子题目无法单独领取积分,请从主任务入口领取"
|
|
|
|
|
|
});
|
|
|
|
|
|
continue;
|
2026-06-25 14:53:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-01 15:04:42 +08:00
|
|
|
|
// 3.3 从内存中获取用户答题记录
|
|
|
|
|
|
if (!answersByGroupId.TryGetValue(groupId, out var userAnswers) || userAnswers.Count == 0)
|
2026-06-24 16:32:59 +08:00
|
|
|
|
{
|
|
|
|
|
|
result.FailItems.Add(new BatchClaimFailItem
|
|
|
|
|
|
{
|
|
|
|
|
|
GroupId = groupId,
|
2026-07-01 15:04:42 +08:00
|
|
|
|
TaskNo = taskNo,
|
2026-06-25 14:53:45 +08:00
|
|
|
|
Message = "请先完成答题再领取积分"
|
2026-06-24 16:32:59 +08:00
|
|
|
|
});
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-01 15:04:42 +08:00
|
|
|
|
// 3.4 完成校验
|
2026-07-01 08:18:07 +08:00
|
|
|
|
var mainTaskAnswer = userAnswers.FirstOrDefault(a => a.JournalPageTaskId == groupId);
|
2026-07-01 09:20:07 +08:00
|
|
|
|
if (mainTaskAnswer == null || mainTaskAnswer.Status != (int)UserAnswerStatusEnum.Complete)
|
2026-07-01 08:18:07 +08:00
|
|
|
|
{
|
|
|
|
|
|
result.FailItems.Add(new BatchClaimFailItem
|
|
|
|
|
|
{
|
|
|
|
|
|
GroupId = groupId,
|
2026-07-01 15:04:42 +08:00
|
|
|
|
TaskNo = taskNo,
|
2026-07-01 08:18:07 +08:00
|
|
|
|
Message = "请先完成答题再领取积分"
|
|
|
|
|
|
});
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 跨页题目:校验所有任务
|
2026-06-25 14:53:45 +08:00
|
|
|
|
if (isCrossPage)
|
|
|
|
|
|
{
|
2026-07-01 15:04:42 +08:00
|
|
|
|
bool allCompleted = relatedTasks.All(task =>
|
2026-06-25 14:53:45 +08:00
|
|
|
|
{
|
2026-07-01 08:18:07 +08:00
|
|
|
|
var answer = userAnswers.FirstOrDefault(a => a.JournalPageTaskId == task.Id);
|
2026-07-01 15:04:42 +08:00
|
|
|
|
return answer != null && answer.Status == (int)UserAnswerStatusEnum.Complete;
|
|
|
|
|
|
});
|
2026-07-01 08:18:07 +08:00
|
|
|
|
if (!allCompleted)
|
2026-06-29 15:09:23 +08:00
|
|
|
|
{
|
|
|
|
|
|
result.FailItems.Add(new BatchClaimFailItem
|
|
|
|
|
|
{
|
|
|
|
|
|
GroupId = groupId,
|
2026-07-01 15:04:42 +08:00
|
|
|
|
TaskNo = taskNo,
|
2026-06-29 15:09:23 +08:00
|
|
|
|
Message = "跨页题目需要完成所有任务才能领取积分"
|
|
|
|
|
|
});
|
2026-06-25 14:53:45 +08:00
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-01 15:04:42 +08:00
|
|
|
|
// 3.5 重复领取校验
|
2026-06-25 14:53:45 +08:00
|
|
|
|
if (claimedGroupIds.Contains(groupId))
|
2026-06-24 16:32:59 +08:00
|
|
|
|
{
|
|
|
|
|
|
result.FailItems.Add(new BatchClaimFailItem
|
|
|
|
|
|
{
|
|
|
|
|
|
GroupId = groupId,
|
2026-07-01 15:04:42 +08:00
|
|
|
|
TaskNo = taskNo,
|
2026-06-25 14:53:45 +08:00
|
|
|
|
Message = "积分已领取,请勿重复领取"
|
2026-06-24 16:32:59 +08:00
|
|
|
|
});
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-01 15:04:42 +08:00
|
|
|
|
// 3.6 计算积分、成长值、得分
|
|
|
|
|
|
int points = mainTaskAnswer.Points;
|
|
|
|
|
|
int growthPoints = mainTaskAnswer.GrowthPoints;
|
2026-07-01 08:18:07 +08:00
|
|
|
|
float score = mainTaskAnswer.Score;
|
2026-06-25 14:53:45 +08:00
|
|
|
|
|
2026-06-24 16:32:59 +08:00
|
|
|
|
if (points <= 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
result.FailItems.Add(new BatchClaimFailItem
|
|
|
|
|
|
{
|
|
|
|
|
|
GroupId = groupId,
|
2026-07-01 15:04:42 +08:00
|
|
|
|
TaskNo = taskNo,
|
2026-06-24 16:32:59 +08:00
|
|
|
|
Message = "该任务无积分可领取"
|
|
|
|
|
|
});
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-01 15:04:42 +08:00
|
|
|
|
// 添加到可领取列表
|
|
|
|
|
|
claimableTasks.Add((groupId, points, growthPoints, score, taskNo));
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 异常情况下,尝试从已查询的数据中计算 taskNo
|
2026-06-29 17:16:38 +08:00
|
|
|
|
string taskNo = string.Empty;
|
2026-07-01 15:04:42 +08:00
|
|
|
|
if (tasksByGroupId.TryGetValue(groupId, out var tasks) && tasks.Count > 0)
|
2026-06-29 17:16:38 +08:00
|
|
|
|
{
|
2026-07-01 15:04:42 +08:00
|
|
|
|
var task = tasks.FirstOrDefault(t => t.Id == groupId) ?? tasks.FirstOrDefault();
|
|
|
|
|
|
if (task != null && !string.IsNullOrEmpty(task.No))
|
2026-06-29 17:16:38 +08:00
|
|
|
|
{
|
2026-07-01 15:04:42 +08:00
|
|
|
|
var parts = task.No.Split('-');
|
|
|
|
|
|
if (parts.Length >= 3)
|
|
|
|
|
|
{
|
|
|
|
|
|
taskNo = $"任务{parts[2]}";
|
|
|
|
|
|
}
|
2026-06-29 17:16:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-06-24 16:32:59 +08:00
|
|
|
|
result.FailItems.Add(new BatchClaimFailItem
|
|
|
|
|
|
{
|
|
|
|
|
|
GroupId = groupId,
|
2026-07-01 15:04:42 +08:00
|
|
|
|
TaskNo = taskNo,
|
2026-06-24 16:32:59 +08:00
|
|
|
|
Message = ex.Message
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-06-29 15:09:23 +08:00
|
|
|
|
#endregion
|
2026-06-24 16:32:59 +08:00
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
#region 4. 事务执行所有领取操作
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
// 4. 事务执行所有领取操作
|
|
|
|
|
|
// - 手动更新 Users.Points(积分累加)
|
|
|
|
|
|
// - 插入积分流水记录
|
|
|
|
|
|
// - 喂养宠物(如有活跃宠物,更新宠物成长值)
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
if (claimableTasks.Count > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 查询用户信息(获取当前积分)
|
|
|
|
|
|
var user = await usersRepository.GetByIdAsync(userId);
|
|
|
|
|
|
if (user == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
throw new BusinessException("用户不存在", ResultCode.NOT_FOUND);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 查询用户活跃宠物
|
|
|
|
|
|
var pet = await userPetRepository.Context.Queryable<UserPet>()
|
|
|
|
|
|
.Where(p => p.UserId == userId && p.Status == (int)UserPetStatusEnum.Active)
|
|
|
|
|
|
.FirstAsync();
|
|
|
|
|
|
|
|
|
|
|
|
int totalGrowthPoints = 0;
|
|
|
|
|
|
int totalPointsAmount = 0;
|
|
|
|
|
|
|
|
|
|
|
|
await answerRepository.UseTranAsync(async () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var task in claimableTasks)
|
|
|
|
|
|
{
|
|
|
|
|
|
totalPointsAmount += task.Points;
|
|
|
|
|
|
totalGrowthPoints += task.GrowthPoints;
|
|
|
|
|
|
|
|
|
|
|
|
// 添加成功详情
|
|
|
|
|
|
result.SuccessItems.Add(new BatchClaimSuccessItem
|
|
|
|
|
|
{
|
|
|
|
|
|
TaskNo = task.TaskNo,
|
|
|
|
|
|
Points = task.Points,
|
|
|
|
|
|
GrowthPoints = task.GrowthPoints,
|
|
|
|
|
|
Score = task.Score
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 4a. 手动更新 Users.Points(积分累加)
|
|
|
|
|
|
var newPointsBalance = user.Points + totalPointsAmount;
|
2026-07-01 10:27:19 +08:00
|
|
|
|
// 4b. 手动更新 Users.GrowthPoints(成长值累加)
|
|
|
|
|
|
var newGrowthPointsBalance = user.GrowthPoints + totalGrowthPoints;
|
|
|
|
|
|
|
2026-07-01 08:18:07 +08:00
|
|
|
|
await answerRepository.Context.Updateable<Users>()
|
|
|
|
|
|
.SetColumns(u => u.Points == newPointsBalance)
|
2026-07-01 10:27:19 +08:00
|
|
|
|
.SetColumns(u => u.GrowthPoints == newGrowthPointsBalance)
|
2026-07-01 15:04:42 +08:00
|
|
|
|
.SetColumns(u => u.UpdatedBy == userId.ToString())
|
2026-07-01 08:18:07 +08:00
|
|
|
|
.SetColumns(u => u.UpdatedAt == DateTime.Now)
|
|
|
|
|
|
.Where(u => u.Id == userId)
|
|
|
|
|
|
.ExecuteCommandAsync();
|
|
|
|
|
|
|
|
|
|
|
|
// 4b. 批量插入积分流水记录
|
|
|
|
|
|
var records = new List<PointsRecord>();
|
|
|
|
|
|
foreach (var task in claimableTasks)
|
|
|
|
|
|
{
|
|
|
|
|
|
records.Add(new PointsRecord
|
|
|
|
|
|
{
|
|
|
|
|
|
UserId = userId,
|
|
|
|
|
|
ChangeAmount = task.Points,
|
|
|
|
|
|
BalanceAfter = newPointsBalance,
|
|
|
|
|
|
ChangeType = PointsChangeTypeEnum.TaskReward.ToString(),
|
|
|
|
|
|
RelatedId = task.GroupId,
|
2026-07-01 10:27:19 +08:00
|
|
|
|
Description = $"完成任务奖励,GroupId:{task.GroupId}",
|
2026-07-01 08:18:07 +08:00
|
|
|
|
Type = PointsFlowTypeEnum.Income,
|
|
|
|
|
|
Status = (int)PointsRecordStatusEnum.Success,
|
|
|
|
|
|
IsDeleted = false,
|
|
|
|
|
|
CreatedBy = "系统",
|
|
|
|
|
|
CreatedAt = DateTime.Now,
|
|
|
|
|
|
UpdatedBy = "系统",
|
|
|
|
|
|
UpdatedAt = DateTime.Now
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
await answerRepository.Context.Insertable(records).ExecuteCommandAsync();
|
|
|
|
|
|
|
|
|
|
|
|
// 4c. 喂养宠物(同一事务内,更新宠物成长值)
|
|
|
|
|
|
if (pet != null && totalGrowthPoints > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
await petService.FeedPetInTranAsync(userId, new Models.Dto.Pet.FeedPetInput
|
|
|
|
|
|
{
|
|
|
|
|
|
PetId = pet.Id,
|
|
|
|
|
|
GrowthPoints = totalGrowthPoints
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
result.SuccessCount = claimableTasks.Count;
|
|
|
|
|
|
}
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region 5. 组装返回结果
|
2026-06-24 16:32:59 +08:00
|
|
|
|
result.FailCount = result.FailItems.Count;
|
|
|
|
|
|
|
|
|
|
|
|
return result;
|
2026-06-29 15:09:23 +08:00
|
|
|
|
#endregion
|
2026-06-24 16:32:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|