Merge branch 'master' of http://8.137.159.94:3000/glz/QYZH.InteractiveMagazine
This commit is contained in:
@ -62,4 +62,12 @@ public interface IUserAnswerTaskService : IBaseService<JournalPageTaskUserAnswer
|
|||||||
/// <param name="userId">用户Id</param>
|
/// <param name="userId">用户Id</param>
|
||||||
/// <returns>批量领取结果</returns>
|
/// <returns>批量领取结果</returns>
|
||||||
Task<BatchClaimPointsOutput> BatchClaimPointsAsync(BatchClaimPointsInput input, long userId);
|
Task<BatchClaimPointsOutput> BatchClaimPointsAsync(BatchClaimPointsInput input, long userId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取目录树形结构(一级目录和二级目录的完整树形结构,包含任务详情)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="journalId">期刊Id</param>
|
||||||
|
/// <param name="userId">用户Id</param>
|
||||||
|
/// <returns>目录树形结构数据</returns>
|
||||||
|
Task<CatalogTreeOutput> GetCatalogTreeAsync(long journalId, long userId);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,130 @@
|
|||||||
|
namespace QYZH.InteractiveMagazine.Models.Dto.UserAnswerTaskService;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 目录树形结构输出(包含一级目录列表)
|
||||||
|
/// </summary>
|
||||||
|
public class CatalogTreeOutput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 一级目录列表
|
||||||
|
/// </summary>
|
||||||
|
public List<FirstLevelCatalogItem> FirstLevelCatalogs { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 一级目录项输出
|
||||||
|
/// </summary>
|
||||||
|
public class FirstLevelCatalogItem
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 一级目录ID
|
||||||
|
/// </summary>
|
||||||
|
public long CatalogId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 一级目录名称
|
||||||
|
/// </summary>
|
||||||
|
public string CatalogName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 文章篇数(来自二级目录对应的所有有题目的页面数总和)
|
||||||
|
/// </summary>
|
||||||
|
public int ArticleCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 任务数量(来自二级目录对应的所有任务按GroupId去重后的总和)
|
||||||
|
/// </summary>
|
||||||
|
public int TaskCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 二级目录列表
|
||||||
|
/// </summary>
|
||||||
|
public List<SecondLevelCatalogItem> Children { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 二级目录项输出(含页码范围、任务数量、状态、总成长值、总积分及任务列表)
|
||||||
|
/// </summary>
|
||||||
|
public class SecondLevelCatalogItem
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 二级目录ID
|
||||||
|
/// </summary>
|
||||||
|
public long CatalogId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 二级目录名称
|
||||||
|
/// </summary>
|
||||||
|
public string CatalogName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 起始页码
|
||||||
|
/// </summary>
|
||||||
|
public int StartPage { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 结束页码
|
||||||
|
/// </summary>
|
||||||
|
public int EndPage { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 任务数量(按GroupId去重后的数量)
|
||||||
|
/// </summary>
|
||||||
|
public int TaskCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态(格式如:1/2 进行中、0/2 未开始、2/2 已完成)
|
||||||
|
/// </summary>
|
||||||
|
public string Status { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 总成长值(该二级目录下所有任务的成长值总和,按GroupId去重)
|
||||||
|
/// </summary>
|
||||||
|
public int TotalGrowthPoints { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 总积分(该二级目录下所有任务的积分总和,按GroupId去重)
|
||||||
|
/// </summary>
|
||||||
|
public int TotalPoints { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 任务列表(每个任务的详细信息)
|
||||||
|
/// </summary>
|
||||||
|
public List<CatalogTaskItem> Tasks { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 目录任务项输出(任务详情)
|
||||||
|
/// </summary>
|
||||||
|
public class CatalogTaskItem
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 任务分组ID(GroupId)
|
||||||
|
/// </summary>
|
||||||
|
public long GroupId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 任务名称(取主任务的Task字段)
|
||||||
|
/// </summary>
|
||||||
|
public string TaskName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 任务状态(未开始、进行中、已完成)
|
||||||
|
/// </summary>
|
||||||
|
public string Status { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 成长值(取主任务的GrowthPoint)
|
||||||
|
/// </summary>
|
||||||
|
public int GrowthPoints { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 积分值(取主任务的Points)
|
||||||
|
/// </summary>
|
||||||
|
public int Points { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 答题时长(分钟,该GroupId下所有任务的AnswerSeconds累加后转换为分钟)
|
||||||
|
/// </summary>
|
||||||
|
public double AnswerMinutes { get; set; }
|
||||||
|
}
|
||||||
@ -1,4 +1,6 @@
|
|||||||
using QYZH.InteractiveMagazine.Models.Entity;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using QYZH.InteractiveMagazine.Infrastructure.Context;
|
||||||
|
using QYZH.InteractiveMagazine.Models.Entity;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using QYZH.InteractiveMagazine.Infrastructure.Context;
|
using QYZH.InteractiveMagazine.Infrastructure.Context;
|
||||||
using SqlSugar;
|
using SqlSugar;
|
||||||
@ -109,7 +111,7 @@ namespace QYZH.InteractiveMagazine.Repository.Core
|
|||||||
|
|
||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 添加创建人 创建时间 更新人 更新时间 默认值
|
/// 添加创建人 创建时间 更新人 更新时间 默认值
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -126,12 +128,8 @@ namespace QYZH.InteractiveMagazine.Repository.Core
|
|||||||
{
|
{
|
||||||
if (entityInfo.PropertyName == "CreatedAt" && (string.IsNullOrWhiteSpace(entityValue) || entityValue == DateTime.MinValue.ToString()))
|
if (entityInfo.PropertyName == "CreatedAt" && (string.IsNullOrWhiteSpace(entityValue) || entityValue == DateTime.MinValue.ToString()))
|
||||||
entityInfo.SetValue(DateTime.Now);//修改CreateTime字段
|
entityInfo.SetValue(DateTime.Now);//修改CreateTime字段
|
||||||
else if (entityInfo.PropertyName == "CreatedBy" && ShouldSetOperatorName(entityValue, currentUserName))
|
else if (entityInfo.PropertyName == "CreatedBy" && (string.IsNullOrWhiteSpace(entityValue)))
|
||||||
entityInfo.SetValue(currentUserName);//修改创建人字段
|
entityInfo.SetValue(currentUserName);//修改创建人字段
|
||||||
else if (entityInfo.PropertyName == "UpdatedAt" && (string.IsNullOrWhiteSpace(entityValue) || entityValue == DateTime.MinValue.ToString()))
|
|
||||||
entityInfo.SetValue(DateTime.Now);//修改UpdatedTime字段
|
|
||||||
else if (entityInfo.PropertyName == "UpdatedBy" && ShouldSetOperatorName(entityValue, currentUserName))
|
|
||||||
entityInfo.SetValue(currentUserName);//修改更新人字段
|
|
||||||
else if (entityInfo.PropertyName == "IsDeleted" && string.IsNullOrWhiteSpace(entityValue))
|
else if (entityInfo.PropertyName == "IsDeleted" && string.IsNullOrWhiteSpace(entityValue))
|
||||||
entityInfo.SetValue("0");//修改CreateTime字段
|
entityInfo.SetValue("0");//修改CreateTime字段
|
||||||
}
|
}
|
||||||
@ -141,7 +139,7 @@ namespace QYZH.InteractiveMagazine.Repository.Core
|
|||||||
{
|
{
|
||||||
if (entityInfo.PropertyName == "UpdatedAt" && (string.IsNullOrWhiteSpace(entityValue) || entityValue == DateTime.MinValue.ToString()))
|
if (entityInfo.PropertyName == "UpdatedAt" && (string.IsNullOrWhiteSpace(entityValue) || entityValue == DateTime.MinValue.ToString()))
|
||||||
entityInfo.SetValue(DateTime.Now);//修改UpdatedTime字段
|
entityInfo.SetValue(DateTime.Now);//修改UpdatedTime字段
|
||||||
else if (entityInfo.PropertyName == "UpdatedBy" && ShouldSetOperatorName(entityValue, currentUserName))
|
else if (entityInfo.PropertyName == "UpdatedBy" && (string.IsNullOrWhiteSpace(entityValue) || entityValue == "0"))
|
||||||
entityInfo.SetValue(currentUserName);//修改更新人字段
|
entityInfo.SetValue(currentUserName);//修改更新人字段
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1314,9 +1314,7 @@ public class UserAnswerTaskService(
|
|||||||
var newGrowthPointsBalance = user.GrowthPoints + totalGrowthPoints;
|
var newGrowthPointsBalance = user.GrowthPoints + totalGrowthPoints;
|
||||||
await answerRepository.Context.Updateable<Users>()
|
await answerRepository.Context.Updateable<Users>()
|
||||||
.SetColumns(u => u.Points == newPointsBalance)
|
.SetColumns(u => u.Points == newPointsBalance)
|
||||||
.SetColumns(u => u.GrowthPoints == newGrowthPointsBalance)
|
.SetColumns(u => u.GrowthPoints == newGrowthPointsBalance)
|
||||||
.SetColumns(u => u.UpdatedBy == userId.ToString())
|
|
||||||
.SetColumns(u => u.UpdatedAt == DateTime.Now)
|
|
||||||
.Where(u => u.Id == userId)
|
.Where(u => u.Id == userId)
|
||||||
.ExecuteCommandAsync();
|
.ExecuteCommandAsync();
|
||||||
|
|
||||||
@ -1326,16 +1324,12 @@ public class UserAnswerTaskService(
|
|||||||
UserId = userId,
|
UserId = userId,
|
||||||
ChangeAmount = totalPoints,
|
ChangeAmount = totalPoints,
|
||||||
BalanceAfter = newPointsBalance,
|
BalanceAfter = newPointsBalance,
|
||||||
ChangeType = PointsChangeTypeEnum.TaskReward.ToString(),
|
ChangeType = nameof(PointsChangeTypeEnum.TaskReward),
|
||||||
RelatedId = groupId,
|
RelatedId = groupId,
|
||||||
Description = $"完成任务奖励,GroupId:{mainTask.GroupId}",
|
Description = $"完成任务奖励,GroupId:{mainTask.GroupId}",
|
||||||
Type = PointsFlowTypeEnum.Income,
|
Type = PointsFlowTypeEnum.Income,
|
||||||
Status = (int)PointsRecordStatusEnum.Success,
|
Status = (int)PointsRecordStatusEnum.Success
|
||||||
IsDeleted = false,
|
|
||||||
CreatedBy = "系统",
|
|
||||||
CreatedAt = DateTime.Now,
|
|
||||||
UpdatedBy = "系统",
|
|
||||||
UpdatedAt = DateTime.Now
|
|
||||||
};
|
};
|
||||||
await answerRepository.Context.Insertable(pointsRecord).ExecuteCommandAsync();
|
await answerRepository.Context.Insertable(pointsRecord).ExecuteCommandAsync();
|
||||||
|
|
||||||
@ -1438,7 +1432,7 @@ public class UserAnswerTaskService(
|
|||||||
// 3. 在内存中逐个校验每个 GroupId,筛选可领取的任务
|
// 3. 在内存中逐个校验每个 GroupId,筛选可领取的任务
|
||||||
// 所有数据已在步骤2中一次性查询完成,这里只做内存筛选
|
// 所有数据已在步骤2中一次性查询完成,这里只做内存筛选
|
||||||
// ============================================
|
// ============================================
|
||||||
var claimableTasks = new List<(long GroupId, int Points, int GrowthPoints, float Score, string TaskNo)>();
|
var claimableTasks = new List<(long GroupId, int Points, int GrowthPoints, float Score)>();
|
||||||
|
|
||||||
foreach (var groupId in input.GroupIds)
|
foreach (var groupId in input.GroupIds)
|
||||||
{
|
{
|
||||||
@ -1449,8 +1443,7 @@ public class UserAnswerTaskService(
|
|||||||
{
|
{
|
||||||
result.FailItems.Add(new BatchClaimFailItem
|
result.FailItems.Add(new BatchClaimFailItem
|
||||||
{
|
{
|
||||||
GroupId = groupId,
|
GroupId = groupId,
|
||||||
TaskNo = string.Empty,
|
|
||||||
Message = "任务不存在"
|
Message = "任务不存在"
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
@ -1458,30 +1451,13 @@ public class UserAnswerTaskService(
|
|||||||
|
|
||||||
// 3.2 判断是否跨页题目并找主任务
|
// 3.2 判断是否跨页题目并找主任务
|
||||||
bool isCrossPage = relatedTasks.Count > 1;
|
bool isCrossPage = relatedTasks.Count > 1;
|
||||||
var mainTask = relatedTasks.FirstOrDefault(t => t.Id == groupId);
|
var mainTask = relatedTasks.FirstOrDefault(t => t.Id == groupId);
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// 【任务编号计算】
|
|
||||||
// 从 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]}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mainTask == null)
|
if (mainTask == null)
|
||||||
{
|
{
|
||||||
result.FailItems.Add(new BatchClaimFailItem
|
result.FailItems.Add(new BatchClaimFailItem
|
||||||
{
|
{
|
||||||
GroupId = groupId,
|
GroupId = groupId,
|
||||||
TaskNo = taskNo,
|
|
||||||
Message = "子题目无法单独领取积分,请从主任务入口领取"
|
Message = "子题目无法单独领取积分,请从主任务入口领取"
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
@ -1492,8 +1468,7 @@ public class UserAnswerTaskService(
|
|||||||
{
|
{
|
||||||
result.FailItems.Add(new BatchClaimFailItem
|
result.FailItems.Add(new BatchClaimFailItem
|
||||||
{
|
{
|
||||||
GroupId = groupId,
|
GroupId = groupId,
|
||||||
TaskNo = taskNo,
|
|
||||||
Message = "请先完成答题再领取积分"
|
Message = "请先完成答题再领取积分"
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
@ -1505,8 +1480,7 @@ public class UserAnswerTaskService(
|
|||||||
{
|
{
|
||||||
result.FailItems.Add(new BatchClaimFailItem
|
result.FailItems.Add(new BatchClaimFailItem
|
||||||
{
|
{
|
||||||
GroupId = groupId,
|
GroupId = groupId,
|
||||||
TaskNo = taskNo,
|
|
||||||
Message = "请先完成答题再领取积分"
|
Message = "请先完成答题再领取积分"
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
@ -1524,8 +1498,7 @@ public class UserAnswerTaskService(
|
|||||||
{
|
{
|
||||||
result.FailItems.Add(new BatchClaimFailItem
|
result.FailItems.Add(new BatchClaimFailItem
|
||||||
{
|
{
|
||||||
GroupId = groupId,
|
GroupId = groupId,
|
||||||
TaskNo = taskNo,
|
|
||||||
Message = "跨页题目需要完成所有任务才能领取积分"
|
Message = "跨页题目需要完成所有任务才能领取积分"
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
@ -1537,8 +1510,7 @@ public class UserAnswerTaskService(
|
|||||||
{
|
{
|
||||||
result.FailItems.Add(new BatchClaimFailItem
|
result.FailItems.Add(new BatchClaimFailItem
|
||||||
{
|
{
|
||||||
GroupId = groupId,
|
GroupId = groupId,
|
||||||
TaskNo = taskNo,
|
|
||||||
Message = "积分已领取,请勿重复领取"
|
Message = "积分已领取,请勿重复领取"
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
@ -1553,36 +1525,20 @@ public class UserAnswerTaskService(
|
|||||||
{
|
{
|
||||||
result.FailItems.Add(new BatchClaimFailItem
|
result.FailItems.Add(new BatchClaimFailItem
|
||||||
{
|
{
|
||||||
GroupId = groupId,
|
GroupId = groupId,
|
||||||
TaskNo = taskNo,
|
|
||||||
Message = "该任务无积分可领取"
|
Message = "该任务无积分可领取"
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加到可领取列表
|
// 添加到可领取列表
|
||||||
claimableTasks.Add((groupId, points, growthPoints, score, taskNo));
|
claimableTasks.Add((groupId, points, growthPoints, score));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// 异常情况下,尝试从已查询的数据中计算 taskNo
|
|
||||||
string taskNo = string.Empty;
|
|
||||||
if (tasksByGroupId.TryGetValue(groupId, out var tasks) && tasks.Count > 0)
|
|
||||||
{
|
|
||||||
var task = tasks.FirstOrDefault(t => t.Id == groupId) ?? tasks.FirstOrDefault();
|
|
||||||
if (task != null && !string.IsNullOrEmpty(task.No))
|
|
||||||
{
|
|
||||||
var parts = task.No.Split('-');
|
|
||||||
if (parts.Length >= 3)
|
|
||||||
{
|
|
||||||
taskNo = $"任务{parts[2]}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result.FailItems.Add(new BatchClaimFailItem
|
result.FailItems.Add(new BatchClaimFailItem
|
||||||
{
|
{
|
||||||
GroupId = groupId,
|
GroupId = groupId,
|
||||||
TaskNo = taskNo,
|
|
||||||
Message = ex.Message
|
Message = ex.Message
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -1593,6 +1549,7 @@ public class UserAnswerTaskService(
|
|||||||
// ============================================
|
// ============================================
|
||||||
// 4. 事务执行所有领取操作
|
// 4. 事务执行所有领取操作
|
||||||
// - 手动更新 Users.Points(积分累加)
|
// - 手动更新 Users.Points(积分累加)
|
||||||
|
// - 手动更新 Users.GrowthPoints(成长值累加)
|
||||||
// - 插入积分流水记录
|
// - 插入积分流水记录
|
||||||
// - 喂养宠物(如有活跃宠物,更新宠物成长值)
|
// - 喂养宠物(如有活跃宠物,更新宠物成长值)
|
||||||
// ============================================
|
// ============================================
|
||||||
@ -1622,8 +1579,7 @@ public class UserAnswerTaskService(
|
|||||||
|
|
||||||
// 添加成功详情
|
// 添加成功详情
|
||||||
result.SuccessItems.Add(new BatchClaimSuccessItem
|
result.SuccessItems.Add(new BatchClaimSuccessItem
|
||||||
{
|
{
|
||||||
TaskNo = task.TaskNo,
|
|
||||||
Points = task.Points,
|
Points = task.Points,
|
||||||
GrowthPoints = task.GrowthPoints,
|
GrowthPoints = task.GrowthPoints,
|
||||||
Score = task.Score
|
Score = task.Score
|
||||||
@ -1637,31 +1593,27 @@ public class UserAnswerTaskService(
|
|||||||
|
|
||||||
await answerRepository.Context.Updateable<Users>()
|
await answerRepository.Context.Updateable<Users>()
|
||||||
.SetColumns(u => u.Points == newPointsBalance)
|
.SetColumns(u => u.Points == newPointsBalance)
|
||||||
.SetColumns(u => u.GrowthPoints == newGrowthPointsBalance)
|
.SetColumns(u => u.GrowthPoints == newGrowthPointsBalance)
|
||||||
.SetColumns(u => u.UpdatedBy == userId.ToString())
|
|
||||||
.SetColumns(u => u.UpdatedAt == DateTime.Now)
|
|
||||||
.Where(u => u.Id == userId)
|
.Where(u => u.Id == userId)
|
||||||
.ExecuteCommandAsync();
|
.ExecuteCommandAsync();
|
||||||
|
|
||||||
// 4b. 批量插入积分流水记录
|
// 4b. 批量插入积分流水记录
|
||||||
var records = new List<PointsRecord>();
|
var records = new List<PointsRecord>();
|
||||||
|
var tempNewPointsBalance = user.Points;
|
||||||
foreach (var task in claimableTasks)
|
foreach (var task in claimableTasks)
|
||||||
{
|
{
|
||||||
|
tempNewPointsBalance += task.Points;
|
||||||
|
// 计算每条记录的领取后余额
|
||||||
records.Add(new PointsRecord
|
records.Add(new PointsRecord
|
||||||
{
|
{
|
||||||
UserId = userId,
|
UserId = userId,
|
||||||
ChangeAmount = task.Points,
|
ChangeAmount = task.Points,
|
||||||
BalanceAfter = newPointsBalance,
|
BalanceAfter = tempNewPointsBalance,
|
||||||
ChangeType = PointsChangeTypeEnum.TaskReward.ToString(),
|
ChangeType = nameof(PointsChangeTypeEnum.TaskReward),
|
||||||
RelatedId = task.GroupId,
|
RelatedId = task.GroupId,
|
||||||
Description = $"完成任务奖励,GroupId:{task.GroupId}",
|
Description = $"完成任务奖励,GroupId:{task.GroupId}",
|
||||||
Type = PointsFlowTypeEnum.Income,
|
Type = PointsFlowTypeEnum.Income,
|
||||||
Status = (int)PointsRecordStatusEnum.Success,
|
Status = (int)PointsRecordStatusEnum.Success,
|
||||||
IsDeleted = false,
|
|
||||||
CreatedBy = "系统",
|
|
||||||
CreatedAt = DateTime.Now,
|
|
||||||
UpdatedBy = "系统",
|
|
||||||
UpdatedAt = DateTime.Now
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await answerRepository.Context.Insertable(records).ExecuteCommandAsync();
|
await answerRepository.Context.Insertable(records).ExecuteCommandAsync();
|
||||||
@ -1683,8 +1635,269 @@ public class UserAnswerTaskService(
|
|||||||
|
|
||||||
#region 5. 组装返回结果
|
#region 5. 组装返回结果
|
||||||
result.FailCount = result.FailItems.Count;
|
result.FailCount = result.FailItems.Count;
|
||||||
|
for (int i = 0; i < result.SuccessItems.Count; i++)
|
||||||
|
{
|
||||||
|
result.SuccessItems[i].TaskNo ="任务" + (i + 1);
|
||||||
|
}
|
||||||
|
for (int i = 0; i < result.FailItems.Count; i++)
|
||||||
|
{
|
||||||
|
result.FailItems[i].TaskNo ="任务" + (i + 1);
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ============================================
|
||||||
|
/// 方法思路:获取目录树形结构(一级目录和二级目录的完整树形结构,包含任务详情)
|
||||||
|
/// 【核心逻辑】
|
||||||
|
/// 1. 用户绑定验证:查询 UserJournal 表验证用户是否绑定该杂志
|
||||||
|
/// 2. 批量预加载:一次性加载目录、页面、任务、用户作答记录,避免N+1查询
|
||||||
|
/// 3. 一级目录统计:文章篇数(二级目录有题目页面数总和)、任务数(GroupId去重总和)
|
||||||
|
/// 4. 二级目录构建:页码范围、任务数、状态、总成长值、总积分、任务列表
|
||||||
|
/// 5. 任务详情:任务名称(取主任务Task字段)、状态、成长值、积分值、答题时长
|
||||||
|
/// 【状态判定规则】
|
||||||
|
/// - 未开始:无任何任务作答记录
|
||||||
|
/// - 进行中:有作答记录但未全部完成
|
||||||
|
/// - 已完成:所有任务都完成(Status=1)
|
||||||
|
/// 【跨页题目处理】
|
||||||
|
/// - 任务名称:取主任务(Id == GroupId)的Task字段
|
||||||
|
/// - 成长值/积分:取主任务数据,按GroupId去重计算
|
||||||
|
/// - 答题时长:该GroupId下所有任务的AnswerSeconds累加后转换为分钟
|
||||||
|
/// ============================================
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="journalId">期刊Id</param>
|
||||||
|
/// <param name="userId">用户Id</param>
|
||||||
|
/// <returns>目录树形结构数据</returns>
|
||||||
|
public async Task<CatalogTreeOutput> GetCatalogTreeAsync(long journalId, long userId)
|
||||||
|
{
|
||||||
|
#region 0. 用户绑定验证
|
||||||
|
|
||||||
|
// 验证用户是否绑定该杂志,未绑定直接返回空结果
|
||||||
|
var isBound = await userJournalRepository.Context.Queryable<UserJournal>()
|
||||||
|
.Where(uj => uj.UserId == userId && uj.JournalId == journalId)
|
||||||
|
.AnyAsync();
|
||||||
|
|
||||||
|
if (!isBound)
|
||||||
|
{
|
||||||
|
return new CatalogTreeOutput();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 1. 批量预加载关联数据
|
||||||
|
|
||||||
|
// 一次性加载所有关联数据,避免N+1查询
|
||||||
|
var allCatalogs = await catalogRepository.Context.Queryable<JournalCatalog>()
|
||||||
|
.Where(c => c.JournalId == journalId)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var allPages = await pageRepository.Context.Queryable<JournalPage>()
|
||||||
|
.Where(p => p.JournalId == journalId)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var allTasks = await taskRepository.Context.Queryable<JournalPageTask>()
|
||||||
|
.Where(t => t.JournalId == journalId)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var userAnswers = await answerRepository.Context.Queryable<JournalPageTaskUserAnswer>()
|
||||||
|
.Where(a => a.JournalId == journalId && a.UserId == userId)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 2. 构建用户作答状态集合
|
||||||
|
|
||||||
|
// 用户已作答的任务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 3. 构建树形结构
|
||||||
|
|
||||||
|
// 获取一级目录(ParentId=0),按排序字段排序
|
||||||
|
var firstLevelCatalogs = allCatalogs.Where(c => c.ParentId == 0).OrderBy(c => c.Sort).ToList();
|
||||||
|
var result = new CatalogTreeOutput();
|
||||||
|
|
||||||
|
foreach (var firstCatalog in firstLevelCatalogs)
|
||||||
|
{
|
||||||
|
int articleCount = 0;
|
||||||
|
int taskCount = 0;
|
||||||
|
var children = new List<SecondLevelCatalogItem>();
|
||||||
|
|
||||||
|
// 获取该一级目录下的二级目录,按排序字段排序
|
||||||
|
var secondLevelCatalogs = allCatalogs
|
||||||
|
.Where(c => c.ParentId == firstCatalog.Id)
|
||||||
|
.OrderBy(c => c.Sort)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var secondCatalog in secondLevelCatalogs)
|
||||||
|
{
|
||||||
|
// 获取二级目录下的所有页面,按页码排序
|
||||||
|
var secondLevelPages = allPages
|
||||||
|
.Where(p => p.JournalCatalogId == secondCatalog.Id)
|
||||||
|
.OrderBy(p => p.PageNum)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var secondLevelPageIds = secondLevelPages.Select(p => p.Id).ToHashSet();
|
||||||
|
|
||||||
|
// 统计有题目的页面数(用于文章篇数计算)
|
||||||
|
var pagesWithTasks = allTasks
|
||||||
|
.Where(t => secondLevelPageIds.Contains(t.JournalPageId))
|
||||||
|
.Select(t => t.JournalPageId)
|
||||||
|
.ToHashSet();
|
||||||
|
|
||||||
|
articleCount += pagesWithTasks.Count;
|
||||||
|
|
||||||
|
// 获取二级目录下的所有任务
|
||||||
|
var secondLevelTasks = allTasks.Where(t => secondLevelPageIds.Contains(t.JournalPageId)).ToList();
|
||||||
|
|
||||||
|
// 按GroupId分组(跨页题目处理)
|
||||||
|
var groupTasks = secondLevelTasks.GroupBy(t => t.GroupId).ToList();
|
||||||
|
var catalogTaskCount = groupTasks.Count;
|
||||||
|
|
||||||
|
// 如果二级目录没有任务,则跳过不显示
|
||||||
|
if (catalogTaskCount == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
taskCount += catalogTaskCount;
|
||||||
|
|
||||||
|
// 计算页码范围
|
||||||
|
var startPage = secondLevelPages.Count > 0 ? secondLevelPages.Min(p => p.PageNum) : 0;
|
||||||
|
var endPage = secondLevelPages.Count > 0 ? secondLevelPages.Max(p => p.PageNum) : 0;
|
||||||
|
|
||||||
|
// 计算二级目录状态
|
||||||
|
var status = CalculateCatalogStatus(groupTasks, userAnsweredTaskIds, userCompletedTaskIds);
|
||||||
|
|
||||||
|
// 计算总成长值和总积分(按GroupId去重,取主任务数据)
|
||||||
|
var (totalGrowthPoints, totalPoints) = CalculateCatalogPoints(groupTasks);
|
||||||
|
|
||||||
|
// 构建任务列表
|
||||||
|
var taskItems = BuildTaskItems(groupTasks, userAnswers, userAnsweredTaskIds, userCompletedTaskIds);
|
||||||
|
|
||||||
|
// 添加二级目录项
|
||||||
|
children.Add(new SecondLevelCatalogItem
|
||||||
|
{
|
||||||
|
CatalogId = secondCatalog.Id,
|
||||||
|
CatalogName = secondCatalog.Name ?? string.Empty,
|
||||||
|
StartPage = startPage,
|
||||||
|
EndPage = endPage,
|
||||||
|
TaskCount = catalogTaskCount,
|
||||||
|
Status = status,
|
||||||
|
TotalGrowthPoints = totalGrowthPoints,
|
||||||
|
TotalPoints = totalPoints,
|
||||||
|
Tasks = taskItems
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加一级目录项
|
||||||
|
result.FirstLevelCatalogs.Add(new FirstLevelCatalogItem
|
||||||
|
{
|
||||||
|
CatalogId = firstCatalog.Id,
|
||||||
|
CatalogName = firstCatalog.Name ?? string.Empty,
|
||||||
|
ArticleCount = articleCount,
|
||||||
|
TaskCount = taskCount,
|
||||||
|
Children = children
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#region 私有方法,逻辑处理
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 计算目录状态(格式:已完成数/总数 状态描述)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="groupTasks">按GroupId分组的任务列表</param>
|
||||||
|
/// <param name="userAnsweredTaskIds">用户已作答任务ID集合</param>
|
||||||
|
/// <param name="userCompletedTaskIds">用户已完成任务ID集合</param>
|
||||||
|
/// <returns>状态描述(格式:1/2 进行中、0/2 未开始、2/2 已完成)</returns>
|
||||||
|
private static string CalculateCatalogStatus(List<IGrouping<long, JournalPageTask>> groupTasks, HashSet<long> userAnsweredTaskIds, HashSet<long> userCompletedTaskIds)
|
||||||
|
{
|
||||||
|
var catalogTaskCount = groupTasks.Count;
|
||||||
|
|
||||||
|
// 已完成的GroupId数量(该GroupId下所有任务都完成)
|
||||||
|
var completedGroupCount = groupTasks.Count(g => g.All(t => userCompletedTaskIds.Contains(t.Id)));
|
||||||
|
|
||||||
|
// 已作答的GroupId数量(该GroupId下至少有一个任务被作答)
|
||||||
|
var answeredGroupCount = groupTasks.Count(g => g.Any(t => userAnsweredTaskIds.Contains(t.Id)));
|
||||||
|
|
||||||
|
if (answeredGroupCount == 0)
|
||||||
|
return $"0/{catalogTaskCount} 未开始";
|
||||||
|
if (completedGroupCount == catalogTaskCount)
|
||||||
|
return $"{completedGroupCount}/{catalogTaskCount} 已完成";
|
||||||
|
return $"{completedGroupCount}/{catalogTaskCount} 进行中";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 计算目录总成长值和总积分(按GroupId去重)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="groupTasks">按GroupId分组的任务列表</param>
|
||||||
|
/// <returns>总成长值和总积分</returns>
|
||||||
|
private static (int TotalGrowthPoints, int TotalPoints) CalculateCatalogPoints(List<IGrouping<long, JournalPageTask>> groupTasks)
|
||||||
|
{
|
||||||
|
// 取每个GroupId的主任务(Id == GroupId),若不存在则取第一个任务
|
||||||
|
var mainTasks = groupTasks.ConvertAll(g => g.FirstOrDefault(t => t.Id == g.Key) ?? g.First());
|
||||||
|
return (mainTasks.Sum(t => t.GrowthPoint), mainTasks.Sum(t => t.Points));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构建任务详情列表
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="groupTasks">按GroupId分组的任务列表</param>
|
||||||
|
/// <param name="userAnswers">用户作答记录</param>
|
||||||
|
/// <param name="userAnsweredTaskIds">用户已作答任务ID集合</param>
|
||||||
|
/// <param name="userCompletedTaskIds">用户已完成任务ID集合</param>
|
||||||
|
/// <returns>任务详情列表</returns>
|
||||||
|
private static List<CatalogTaskItem> BuildTaskItems(List<IGrouping<long, JournalPageTask>> groupTasks, List<JournalPageTaskUserAnswer> userAnswers, HashSet<long> userAnsweredTaskIds, HashSet<long> userCompletedTaskIds)
|
||||||
|
{
|
||||||
|
var taskItems = new List<CatalogTaskItem>();
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 判断任务状态
|
||||||
|
bool hasAnswer = group.Any(t => userAnsweredTaskIds.Contains(t.Id));
|
||||||
|
bool isAllCompleted = group.All(t => userCompletedTaskIds.Contains(t.Id));
|
||||||
|
|
||||||
|
string taskStatus = !hasAnswer ? "未开始" : (isAllCompleted ? "已完成" : "进行中");
|
||||||
|
|
||||||
|
taskItems.Add(new CatalogTaskItem
|
||||||
|
{
|
||||||
|
GroupId = groupId,
|
||||||
|
TaskName = mainTask.Task ?? string.Empty,
|
||||||
|
Status = taskStatus,
|
||||||
|
GrowthPoints = mainTask.GrowthPoint,
|
||||||
|
Points = mainTask.Points,
|
||||||
|
AnswerMinutes = groupAnswerMinutes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return taskItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -238,8 +238,7 @@ public class UserAnswerTaskController : WeChatBaseController
|
|||||||
public async Task<BaseResponse<BatchClaimPointsOutput>> BatchClaimPointsAsync([FromBody] BatchClaimPointsInput input)
|
public async Task<BaseResponse<BatchClaimPointsOutput>> BatchClaimPointsAsync([FromBody] BatchClaimPointsInput input)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
||||||
var userId = GetCurrentUserId();
|
var userId = GetCurrentUserId();
|
||||||
if (userId == 0)
|
if (userId == 0)
|
||||||
{
|
{
|
||||||
@ -265,4 +264,35 @@ public class UserAnswerTaskController : WeChatBaseController
|
|||||||
return BaseResponse<BatchClaimPointsOutput>.Fail("批量领取积分失败,请稍后重试");
|
return BaseResponse<BatchClaimPointsOutput>.Fail("批量领取积分失败,请稍后重试");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取目录树形结构(一级目录和二级目录的完整树形结构,包含任务详情)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="journalId">期刊Id</param>
|
||||||
|
/// <returns>目录树形结构数据</returns>
|
||||||
|
[HttpGet("catalog-tree/{journalId:long}")]
|
||||||
|
public async Task<BaseResponse<CatalogTreeOutput>> GetCatalogTreeAsync([Required(ErrorMessage = "参数错误")] long journalId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var userId = ConstUserId;//GetCurrentUserId();
|
||||||
|
if (userId == 0)
|
||||||
|
{
|
||||||
|
return BaseResponse<CatalogTreeOutput>.Fail(ResultCode.DENY, "未获取到用户信息");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (journalId <= 0)
|
||||||
|
{
|
||||||
|
return BaseResponse<CatalogTreeOutput>.Fail(ResultCode.PARAM_ERROR, "期刊Id不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await _userAnswerTaskService.GetCatalogTreeAsync(journalId, userId);
|
||||||
|
return Success(result);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "获取目录树形结构失败,JournalId: {JournalId}, UserId: {UserId}", journalId, GetCurrentUserId());
|
||||||
|
return BaseResponse<CatalogTreeOutput>.Fail("获取目录树形结构失败,请稍后重试");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using QYZH.InteractiveMagazine.Infrastructure.Auth;
|
using QYZH.InteractiveMagazine.Infrastructure.Auth;
|
||||||
|
using QYZH.InteractiveMagazine.Infrastructure.Context;
|
||||||
using QYZH.InteractiveMagazine.Models.Dto;
|
using QYZH.InteractiveMagazine.Models.Dto;
|
||||||
using QYZH.InteractiveMagazine.Models.Enum;
|
using QYZH.InteractiveMagazine.Models.Enum;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
|
|||||||
@ -9,6 +9,8 @@
|
|||||||
<Project Path="QYZH.InteractiveMagazine.Repository/QYZH.InteractiveMagazine.Repository.csproj" />
|
<Project Path="QYZH.InteractiveMagazine.Repository/QYZH.InteractiveMagazine.Repository.csproj" />
|
||||||
<Project Path="QYZH.InteractiveMagazine.Service/QYZH.InteractiveMagazine.Service.csproj" />
|
<Project Path="QYZH.InteractiveMagazine.Service/QYZH.InteractiveMagazine.Service.csproj" />
|
||||||
<Project Path="QYZH.InteractiveMagazine.WebApi/QYZH.InteractiveMagazine.WebApi.csproj" />
|
<Project Path="QYZH.InteractiveMagazine.WebApi/QYZH.InteractiveMagazine.WebApi.csproj" />
|
||||||
<Project Path="QYZH.InteractiveMagazine.WeChatApi/QYZH.InteractiveMagazine.WeChatApi.csproj" />
|
<Project Path="QYZH.InteractiveMagazine.WeChatApi/QYZH.InteractiveMagazine.WeChatApi.csproj">
|
||||||
|
<Build Solution="Debug|*" Project="false" />
|
||||||
|
</Project>
|
||||||
<Project Path="QYZH.InteractiveMagazine.WorkService/QYZH.InteractiveMagazine.WorkService.csproj" />
|
<Project Path="QYZH.InteractiveMagazine.WorkService/QYZH.InteractiveMagazine.WorkService.csproj" />
|
||||||
</Solution>
|
</Solution>
|
||||||
|
|||||||
Reference in New Issue
Block a user