feat: Implement mission and task management APIs with dedicated controllers and query/command handlers, including admin features for check-ins.
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
using MediatR;
|
||||
using MissionService.Domain.AggregatesModel.MissionAggregate;
|
||||
using MissionService.Domain.AggregatesModel.TaskAggregate;
|
||||
using TaskStatus = MissionService.Domain.AggregatesModel.TaskAggregate.TaskStatus;
|
||||
|
||||
namespace MissionService.API.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for StartMissionTaskCommand
|
||||
/// VI: Handler cho StartMissionTaskCommand
|
||||
/// </summary>
|
||||
public class StartMissionTaskCommandHandler : IRequestHandler<StartMissionTaskCommand, StartTaskResult>
|
||||
{
|
||||
private readonly IMissionRepository _missionRepository;
|
||||
private readonly IUserTaskRepository _taskRepository;
|
||||
private readonly ILogger<StartMissionTaskCommandHandler> _logger;
|
||||
|
||||
public StartMissionTaskCommandHandler(
|
||||
IMissionRepository missionRepository,
|
||||
IUserTaskRepository taskRepository,
|
||||
ILogger<StartMissionTaskCommandHandler> logger)
|
||||
{
|
||||
_missionRepository = missionRepository;
|
||||
_taskRepository = taskRepository;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<StartTaskResult> Handle(StartMissionTaskCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// EN: Check if mission exists and is available / VI: Kiểm tra mission tồn tại và khả dụng
|
||||
var mission = await _missionRepository.GetByIdAsync(request.MissionId, cancellationToken);
|
||||
if (mission == null)
|
||||
{
|
||||
return new StartTaskResult(false, null, "Mission not found / Không tìm thấy mission");
|
||||
}
|
||||
|
||||
if (!mission.IsAvailable())
|
||||
{
|
||||
return new StartTaskResult(false, null, "Mission not available / Mission không khả dụng");
|
||||
}
|
||||
|
||||
// EN: Check if user already has active task for this mission
|
||||
// VI: Kiểm tra user đã có task đang hoạt động cho mission này chưa
|
||||
var existingTask = await _taskRepository.GetByUserAndMissionAsync(
|
||||
request.UserId, request.MissionId, cancellationToken);
|
||||
|
||||
if (existingTask != null)
|
||||
{
|
||||
return new StartTaskResult(false, existingTask.Id,
|
||||
"Task already exists / Task đã tồn tại");
|
||||
}
|
||||
|
||||
// EN: Check max completions / VI: Kiểm tra số lần hoàn thành tối đa
|
||||
var completions = await _taskRepository.CountCompletionsByUserAndMissionAsync(
|
||||
request.UserId, request.MissionId, cancellationToken);
|
||||
|
||||
if (completions >= mission.MaxCompletions)
|
||||
{
|
||||
return new StartTaskResult(false, null,
|
||||
"Max completions reached / Đã đạt số lần hoàn thành tối đa");
|
||||
}
|
||||
|
||||
// EN: Create new task / VI: Tạo task mới
|
||||
var task = new UserTask(request.UserId, request.MissionId);
|
||||
_taskRepository.Add(task);
|
||||
await _taskRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Task {TaskId} started for user {UserId} on mission {MissionId}",
|
||||
task.Id, request.UserId, request.MissionId);
|
||||
|
||||
return new StartTaskResult(true, task.Id, "Task started / Đã bắt đầu task");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for UpdateTaskProgressCommand
|
||||
/// VI: Handler cho UpdateTaskProgressCommand
|
||||
/// </summary>
|
||||
public class UpdateTaskProgressCommandHandler : IRequestHandler<UpdateTaskProgressCommand, UpdateProgressResult>
|
||||
{
|
||||
private readonly IUserTaskRepository _taskRepository;
|
||||
private readonly ILogger<UpdateTaskProgressCommandHandler> _logger;
|
||||
|
||||
public UpdateTaskProgressCommandHandler(
|
||||
IUserTaskRepository taskRepository,
|
||||
ILogger<UpdateTaskProgressCommandHandler> logger)
|
||||
{
|
||||
_taskRepository = taskRepository;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<UpdateProgressResult> Handle(UpdateTaskProgressCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var task = await _taskRepository.GetByIdAsync(request.TaskId, cancellationToken);
|
||||
|
||||
if (task == null)
|
||||
{
|
||||
return new UpdateProgressResult(false, 0, 0, 0, false, "Task not found / Không tìm thấy task");
|
||||
}
|
||||
|
||||
if (task.UserId != request.UserId)
|
||||
{
|
||||
return new UpdateProgressResult(false, 0, 0, 0, false, "Unauthorized / Không có quyền");
|
||||
}
|
||||
|
||||
if (task.Status != TaskStatus.InProgress)
|
||||
{
|
||||
return new UpdateProgressResult(false,
|
||||
task.Progress.CurrentValue,
|
||||
task.Progress.TargetValue,
|
||||
task.Progress.PercentComplete,
|
||||
task.Progress.IsComplete,
|
||||
"Task not in progress / Task không đang thực hiện");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
task.UpdateProgress(request.CurrentValue);
|
||||
|
||||
// EN: Submit evidence if provided / VI: Nộp bằng chứng nếu có
|
||||
if (request.Evidence != null && task.Progress.IsComplete)
|
||||
{
|
||||
var evidence = MapEvidence(request.Evidence);
|
||||
task.SubmitEvidence(evidence);
|
||||
}
|
||||
|
||||
// EN: Auto-complete if no verification needed / VI: Tự động hoàn thành nếu không cần xác thực
|
||||
if (task.Progress.IsComplete && request.Evidence == null)
|
||||
{
|
||||
task.AutoComplete();
|
||||
}
|
||||
|
||||
_taskRepository.Update(task);
|
||||
await _taskRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
|
||||
|
||||
return new UpdateProgressResult(
|
||||
true,
|
||||
task.Progress.CurrentValue,
|
||||
task.Progress.TargetValue,
|
||||
task.Progress.PercentComplete,
|
||||
task.Progress.IsComplete,
|
||||
task.Progress.IsComplete ? "Task completed / Hoàn thành task" : "Progress updated / Đã cập nhật tiến độ");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to update task {TaskId}", request.TaskId);
|
||||
return new UpdateProgressResult(false, 0, 0, 0, false, "Update failed / Cập nhật thất bại");
|
||||
}
|
||||
}
|
||||
|
||||
private static TaskEvidence MapEvidence(TaskEvidenceDto dto)
|
||||
{
|
||||
return dto.Type.ToLowerInvariant() switch
|
||||
{
|
||||
"watchduration" => TaskEvidence.WatchDuration(int.Parse(dto.Data)),
|
||||
"click" => TaskEvidence.Click(dto.Data),
|
||||
"socialproof" => TaskEvidence.SocialProof(dto.ScreenshotUrl ?? "", dto.Data),
|
||||
"invitecode" => TaskEvidence.InviteCode(dto.Data),
|
||||
_ => TaskEvidence.Upload(dto.Data, dto.ScreenshotUrl != null)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for ClaimTaskRewardCommand
|
||||
/// VI: Handler cho ClaimTaskRewardCommand
|
||||
/// </summary>
|
||||
public class ClaimTaskRewardCommandHandler : IRequestHandler<ClaimTaskRewardCommand, ClaimRewardResult>
|
||||
{
|
||||
private readonly IUserTaskRepository _taskRepository;
|
||||
private readonly IMissionRepository _missionRepository;
|
||||
private readonly ILogger<ClaimTaskRewardCommandHandler> _logger;
|
||||
|
||||
public ClaimTaskRewardCommandHandler(
|
||||
IUserTaskRepository taskRepository,
|
||||
IMissionRepository missionRepository,
|
||||
ILogger<ClaimTaskRewardCommandHandler> logger)
|
||||
{
|
||||
_taskRepository = taskRepository;
|
||||
_missionRepository = missionRepository;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<ClaimRewardResult> Handle(ClaimTaskRewardCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var task = await _taskRepository.GetByIdAsync(request.TaskId, cancellationToken);
|
||||
|
||||
if (task == null)
|
||||
{
|
||||
return new ClaimRewardResult(false, null, "Task not found / Không tìm thấy task");
|
||||
}
|
||||
|
||||
if (task.UserId != request.UserId)
|
||||
{
|
||||
return new ClaimRewardResult(false, null, "Unauthorized / Không có quyền");
|
||||
}
|
||||
|
||||
if (task.Status != TaskStatus.Completed)
|
||||
{
|
||||
return new ClaimRewardResult(false, null, "Task not completed / Task chưa hoàn thành");
|
||||
}
|
||||
|
||||
if (task.RewardClaimed)
|
||||
{
|
||||
return new ClaimRewardResult(false, null, "Reward already claimed / Đã nhận thưởng rồi");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var mission = await _missionRepository.GetByIdAsync(task.MissionId, cancellationToken);
|
||||
var points = mission?.Reward.Points ?? 0;
|
||||
|
||||
task.ClaimReward();
|
||||
_taskRepository.Update(task);
|
||||
await _taskRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Reward claimed for task {TaskId} by user {UserId}, points: {Points}",
|
||||
request.TaskId, request.UserId, points);
|
||||
|
||||
return new ClaimRewardResult(true, points, "Reward claimed / Đã nhận thưởng");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to claim reward for task {TaskId}", request.TaskId);
|
||||
return new ClaimRewardResult(false, null, "Claim failed / Nhận thưởng thất bại");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using MediatR;
|
||||
|
||||
namespace MissionService.API.Application.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Command to start a mission task
|
||||
/// VI: Command bắt đầu một task mission
|
||||
/// </summary>
|
||||
public record StartMissionTaskCommand(Guid UserId, Guid MissionId) : IRequest<StartTaskResult>;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Result of starting a task
|
||||
/// VI: Kết quả bắt đầu task
|
||||
/// </summary>
|
||||
public record StartTaskResult(
|
||||
bool Success,
|
||||
Guid? TaskId,
|
||||
string? Message);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Command to update task progress
|
||||
/// VI: Command cập nhật tiến độ task
|
||||
/// </summary>
|
||||
public record UpdateTaskProgressCommand(
|
||||
Guid UserId,
|
||||
Guid TaskId,
|
||||
int CurrentValue,
|
||||
TaskEvidenceDto? Evidence = null) : IRequest<UpdateProgressResult>;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Task evidence DTO
|
||||
/// VI: DTO bằng chứng task
|
||||
/// </summary>
|
||||
public record TaskEvidenceDto(
|
||||
string Type,
|
||||
string Data,
|
||||
string? ScreenshotUrl = null,
|
||||
string? VideoUrl = null);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Result of updating progress
|
||||
/// VI: Kết quả cập nhật tiến độ
|
||||
/// </summary>
|
||||
public record UpdateProgressResult(
|
||||
bool Success,
|
||||
int CurrentValue,
|
||||
int TargetValue,
|
||||
decimal PercentComplete,
|
||||
bool IsComplete,
|
||||
string? Message);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Command to claim task reward
|
||||
/// VI: Command nhận thưởng task
|
||||
/// </summary>
|
||||
public record ClaimTaskRewardCommand(Guid UserId, Guid TaskId) : IRequest<ClaimRewardResult>;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Result of claiming reward
|
||||
/// VI: Kết quả nhận thưởng
|
||||
/// </summary>
|
||||
public record ClaimRewardResult(
|
||||
bool Success,
|
||||
decimal? PointsEarned,
|
||||
string? Message);
|
||||
@@ -110,7 +110,7 @@ public record UserTaskProgressDto(
|
||||
/// </summary>
|
||||
public record UserMissionProgressResult(
|
||||
int TotalMissions,
|
||||
int CompletedMissions,
|
||||
int CompletedMissionsCount,
|
||||
int InProgressMissions,
|
||||
decimal TotalPointsEarned,
|
||||
IReadOnlyList<MissionSummaryDto> ActiveMissions,
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
using MediatR;
|
||||
using MissionService.Domain.AggregatesModel.MissionAggregate;
|
||||
using MissionService.Domain.AggregatesModel.TaskAggregate;
|
||||
using TaskStatus = MissionService.Domain.AggregatesModel.TaskAggregate.TaskStatus;
|
||||
|
||||
namespace MissionService.API.Application.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for GetAvailableMissionsQuery
|
||||
/// VI: Handler cho GetAvailableMissionsQuery
|
||||
/// </summary>
|
||||
public class GetAvailableMissionsQueryHandler : IRequestHandler<GetAvailableMissionsQuery, MissionsListResult>
|
||||
{
|
||||
private readonly IMissionRepository _missionRepository;
|
||||
private readonly IUserTaskRepository _taskRepository;
|
||||
|
||||
public GetAvailableMissionsQueryHandler(
|
||||
IMissionRepository missionRepository,
|
||||
IUserTaskRepository taskRepository)
|
||||
{
|
||||
_missionRepository = missionRepository;
|
||||
_taskRepository = taskRepository;
|
||||
}
|
||||
|
||||
public async Task<MissionsListResult> Handle(GetAvailableMissionsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var missions = await _missionRepository.GetActiveMissionsAsync(cancellationToken);
|
||||
var userTasks = await _taskRepository.GetByUserIdAsync(request.UserId, cancellationToken);
|
||||
|
||||
var summaries = missions.Select(m => MapToSummary(m, userTasks)).ToList();
|
||||
return new MissionsListResult(summaries);
|
||||
}
|
||||
|
||||
private static MissionSummaryDto MapToSummary(Mission mission, IReadOnlyList<UserTask> userTasks)
|
||||
{
|
||||
var userTask = userTasks.FirstOrDefault(t => t.MissionId == mission.Id &&
|
||||
(t.Status == TaskStatus.InProgress || t.Status == TaskStatus.PendingVerification));
|
||||
|
||||
return new MissionSummaryDto(
|
||||
Id: mission.Id,
|
||||
Code: mission.Code,
|
||||
Title: mission.TitleEn,
|
||||
Description: mission.DescriptionEn,
|
||||
Type: mission.Type.Name,
|
||||
Category: mission.Category.Name,
|
||||
RewardPoints: mission.Reward.Points,
|
||||
Frequency: mission.Frequency.Name,
|
||||
MaxCompletions: mission.MaxCompletions,
|
||||
IsAvailable: mission.IsAvailable(),
|
||||
UserProgress: userTask != null ? MapToProgress(userTask) : null);
|
||||
}
|
||||
|
||||
private static UserTaskProgressDto MapToProgress(UserTask task)
|
||||
{
|
||||
return new UserTaskProgressDto(
|
||||
TaskId: task.Id,
|
||||
CurrentValue: task.Progress.CurrentValue,
|
||||
TargetValue: task.Progress.TargetValue,
|
||||
PercentComplete: task.Progress.PercentComplete,
|
||||
Status: task.Status.Name,
|
||||
StartedAt: task.StartedAt,
|
||||
CompletedAt: task.CompletedAt,
|
||||
RewardClaimed: task.RewardClaimed);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for GetMissionDetailsQuery
|
||||
/// VI: Handler cho GetMissionDetailsQuery
|
||||
/// </summary>
|
||||
public class GetMissionDetailsQueryHandler : IRequestHandler<GetMissionDetailsQuery, MissionDetailsResult?>
|
||||
{
|
||||
private readonly IMissionRepository _missionRepository;
|
||||
private readonly IUserTaskRepository _taskRepository;
|
||||
|
||||
public GetMissionDetailsQueryHandler(
|
||||
IMissionRepository missionRepository,
|
||||
IUserTaskRepository taskRepository)
|
||||
{
|
||||
_missionRepository = missionRepository;
|
||||
_taskRepository = taskRepository;
|
||||
}
|
||||
|
||||
public async Task<MissionDetailsResult?> Handle(GetMissionDetailsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var mission = await _missionRepository.GetByIdAsync(request.MissionId, cancellationToken);
|
||||
if (mission == null) return null;
|
||||
|
||||
UserTaskProgressDto? userProgress = null;
|
||||
if (request.UserId.HasValue)
|
||||
{
|
||||
var userTask = await _taskRepository.GetByUserAndMissionAsync(
|
||||
request.UserId.Value, request.MissionId, cancellationToken);
|
||||
if (userTask != null)
|
||||
{
|
||||
userProgress = new UserTaskProgressDto(
|
||||
TaskId: userTask.Id,
|
||||
CurrentValue: userTask.Progress.CurrentValue,
|
||||
TargetValue: userTask.Progress.TargetValue,
|
||||
PercentComplete: userTask.Progress.PercentComplete,
|
||||
Status: userTask.Status.Name,
|
||||
StartedAt: userTask.StartedAt,
|
||||
CompletedAt: userTask.CompletedAt,
|
||||
RewardClaimed: userTask.RewardClaimed);
|
||||
}
|
||||
}
|
||||
|
||||
return new MissionDetailsResult(
|
||||
Id: mission.Id,
|
||||
Code: mission.Code,
|
||||
TitleEn: mission.TitleEn,
|
||||
TitleVi: mission.TitleVi,
|
||||
DescriptionEn: mission.DescriptionEn,
|
||||
DescriptionVi: mission.DescriptionVi,
|
||||
Type: mission.Type.Name,
|
||||
Category: mission.Category.Name,
|
||||
Reward: new MissionRewardDto(
|
||||
mission.Reward.Points,
|
||||
mission.Reward.MiningBoostPercent,
|
||||
mission.Reward.ExperiencePoints,
|
||||
mission.Reward.BadgeId),
|
||||
Frequency: mission.Frequency.Name,
|
||||
MaxCompletions: mission.MaxCompletions,
|
||||
StartDate: mission.StartDate,
|
||||
EndDate: mission.EndDate,
|
||||
Status: mission.Status.Name,
|
||||
Rules: mission.Rules.Select(r => new MissionRuleDto(r.RuleType, r.Operator, r.Value)).ToList(),
|
||||
UserProgress: userProgress);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for GetMissionsByCategoryQuery
|
||||
/// VI: Handler cho GetMissionsByCategoryQuery
|
||||
/// </summary>
|
||||
public class GetMissionsByCategoryQueryHandler : IRequestHandler<GetMissionsByCategoryQuery, MissionsListResult>
|
||||
{
|
||||
private readonly IMissionRepository _missionRepository;
|
||||
|
||||
public GetMissionsByCategoryQueryHandler(IMissionRepository missionRepository)
|
||||
{
|
||||
_missionRepository = missionRepository;
|
||||
}
|
||||
|
||||
public async Task<MissionsListResult> Handle(GetMissionsByCategoryQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var category = MissionCategory.FromDisplayName<MissionCategory>(request.Category);
|
||||
var missions = await _missionRepository.GetByCategoryAsync(category, cancellationToken);
|
||||
|
||||
var summaries = missions.Select(m => new MissionSummaryDto(
|
||||
Id: m.Id,
|
||||
Code: m.Code,
|
||||
Title: m.TitleEn,
|
||||
Description: m.DescriptionEn,
|
||||
Type: m.Type.Name,
|
||||
Category: m.Category.Name,
|
||||
RewardPoints: m.Reward.Points,
|
||||
Frequency: m.Frequency.Name,
|
||||
MaxCompletions: m.MaxCompletions,
|
||||
IsAvailable: m.IsAvailable(),
|
||||
UserProgress: null)).ToList();
|
||||
|
||||
return new MissionsListResult(summaries);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MissionService.Domain.AggregatesModel.CheckInAggregate;
|
||||
using MissionService.Domain.AggregatesModel.TaskAggregate;
|
||||
using TaskStatus = MissionService.Domain.AggregatesModel.TaskAggregate.TaskStatus;
|
||||
|
||||
namespace MissionService.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Admin controller for check-in and task management
|
||||
/// VI: Controller admin để quản lý check-in và task
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/v1/admin")]
|
||||
[Authorize(Roles = "Admin")]
|
||||
public class AdminController : ControllerBase
|
||||
{
|
||||
private readonly IUserCheckInRepository _checkInRepository;
|
||||
private readonly IUserTaskRepository _taskRepository;
|
||||
private readonly ILogger<AdminController> _logger;
|
||||
|
||||
public AdminController(
|
||||
IUserCheckInRepository checkInRepository,
|
||||
IUserTaskRepository taskRepository,
|
||||
ILogger<AdminController> logger)
|
||||
{
|
||||
_checkInRepository = checkInRepository;
|
||||
_taskRepository = taskRepository;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
#region Check-In Admin
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get user's check-in details / VI: Lấy chi tiết check-in của user
|
||||
/// </summary>
|
||||
[HttpGet("checkins/users/{userId:guid}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetUserCheckIn(Guid userId)
|
||||
{
|
||||
var checkIn = await _checkInRepository.GetByUserIdAsync(userId);
|
||||
if (checkIn == null)
|
||||
{
|
||||
return NotFound(new { Message = "Check-in profile not found" });
|
||||
}
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
checkIn.Id,
|
||||
checkIn.UserId,
|
||||
checkIn.CurrentStreak,
|
||||
checkIn.LongestStreak,
|
||||
checkIn.TotalCheckIns,
|
||||
checkIn.LastCheckInDate,
|
||||
CanCheckInToday = checkIn.CanCheckInToday()
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Reset user's streak (admin action) / VI: Reset streak của user (hành động admin)
|
||||
/// </summary>
|
||||
[HttpPost("checkins/users/{userId:guid}/reset-streak")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ResetUserStreak(Guid userId)
|
||||
{
|
||||
var checkIn = await _checkInRepository.GetByUserIdAsync(userId);
|
||||
if (checkIn == null)
|
||||
{
|
||||
return NotFound(new { Message = "Check-in profile not found" });
|
||||
}
|
||||
|
||||
checkIn.ResetStreak();
|
||||
_checkInRepository.Update(checkIn);
|
||||
await _checkInRepository.UnitOfWork.SaveEntitiesAsync();
|
||||
|
||||
_logger.LogWarning("Admin reset streak for user {UserId}", userId);
|
||||
|
||||
return Ok(new { Message = "Streak reset successfully", checkIn.CurrentStreak });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get top streaks / VI: Lấy streaks cao nhất
|
||||
/// </summary>
|
||||
[HttpGet("checkins/top-streaks")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> GetTopStreaks([FromQuery] int count = 20)
|
||||
{
|
||||
var topStreaks = await _checkInRepository.GetTopStreaksAsync(Math.Min(count, 100));
|
||||
|
||||
return Ok(topStreaks.Select((c, i) => new
|
||||
{
|
||||
Rank = i + 1,
|
||||
c.UserId,
|
||||
c.CurrentStreak,
|
||||
c.LongestStreak,
|
||||
c.TotalCheckIns,
|
||||
c.LastCheckInDate
|
||||
}));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Task Admin
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get pending verification tasks / VI: Lấy tasks đang chờ xác thực
|
||||
/// </summary>
|
||||
[HttpGet("tasks/pending-verification")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> GetPendingVerificationTasks()
|
||||
{
|
||||
var tasks = await _taskRepository.GetPendingVerificationAsync();
|
||||
|
||||
return Ok(tasks.Select(t => new
|
||||
{
|
||||
t.Id,
|
||||
t.UserId,
|
||||
t.MissionId,
|
||||
Status = t.Status.Name,
|
||||
Progress = new
|
||||
{
|
||||
t.Progress.CurrentValue,
|
||||
t.Progress.TargetValue,
|
||||
t.Progress.PercentComplete
|
||||
},
|
||||
Evidence = t.Evidence != null ? new
|
||||
{
|
||||
Type = t.Evidence.Type.ToString(),
|
||||
t.Evidence.Data,
|
||||
t.Evidence.ScreenshotUrl,
|
||||
t.Evidence.VideoUrl,
|
||||
t.Evidence.CapturedAt
|
||||
} : null,
|
||||
t.StartedAt
|
||||
}));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Approve a task / VI: Duyệt task
|
||||
/// </summary>
|
||||
[HttpPost("tasks/{taskId:guid}/approve")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> ApproveTask(Guid taskId)
|
||||
{
|
||||
var task = await _taskRepository.GetByIdAsync(taskId);
|
||||
if (task == null)
|
||||
{
|
||||
return NotFound(new { Message = "Task not found" });
|
||||
}
|
||||
|
||||
if (task.Status != TaskStatus.PendingVerification)
|
||||
{
|
||||
return BadRequest(new { Message = "Task is not pending verification" });
|
||||
}
|
||||
|
||||
var adminId = GetCurrentUserId();
|
||||
task.Complete(VerificationResult.ManualApproved(adminId));
|
||||
_taskRepository.Update(task);
|
||||
await _taskRepository.UnitOfWork.SaveEntitiesAsync();
|
||||
|
||||
_logger.LogInformation("Task {TaskId} approved by admin {AdminId}", taskId, adminId);
|
||||
|
||||
return Ok(new { Message = "Task approved", Status = task.Status.Name });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Reject a task / VI: Từ chối task
|
||||
/// </summary>
|
||||
[HttpPost("tasks/{taskId:guid}/reject")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> RejectTask(Guid taskId, [FromBody] RejectTaskRequest request)
|
||||
{
|
||||
var task = await _taskRepository.GetByIdAsync(taskId);
|
||||
if (task == null)
|
||||
{
|
||||
return NotFound(new { Message = "Task not found" });
|
||||
}
|
||||
|
||||
if (task.Status != TaskStatus.PendingVerification)
|
||||
{
|
||||
return BadRequest(new { Message = "Task is not pending verification" });
|
||||
}
|
||||
|
||||
var adminId = GetCurrentUserId();
|
||||
task.Complete(VerificationResult.Rejected(request.Reason, VerificationMethod.Manual, adminId));
|
||||
_taskRepository.Update(task);
|
||||
await _taskRepository.UnitOfWork.SaveEntitiesAsync();
|
||||
|
||||
_logger.LogInformation("Task {TaskId} rejected by admin {AdminId}: {Reason}", taskId, adminId, request.Reason);
|
||||
|
||||
return Ok(new { Message = "Task rejected", Status = task.Status.Name, request.Reason });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get user's tasks / VI: Lấy tasks của user
|
||||
/// </summary>
|
||||
[HttpGet("tasks/users/{userId:guid}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> GetUserTasks(Guid userId)
|
||||
{
|
||||
var tasks = await _taskRepository.GetByUserIdAsync(userId);
|
||||
|
||||
return Ok(tasks.Select(t => new
|
||||
{
|
||||
t.Id,
|
||||
t.MissionId,
|
||||
Status = t.Status.Name,
|
||||
Progress = new
|
||||
{
|
||||
t.Progress.CurrentValue,
|
||||
t.Progress.TargetValue,
|
||||
t.Progress.PercentComplete
|
||||
},
|
||||
t.RewardClaimed,
|
||||
t.StartedAt,
|
||||
t.CompletedAt,
|
||||
t.ClaimedAt
|
||||
}));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private Guid GetCurrentUserId()
|
||||
{
|
||||
var userIdClaim = User.FindFirst("sub") ?? User.FindFirst("user_id");
|
||||
if (userIdClaim != null && Guid.TryParse(userIdClaim.Value, out var userId))
|
||||
{
|
||||
return userId;
|
||||
}
|
||||
throw new UnauthorizedAccessException("User ID not found in token");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Request to reject a task / VI: Request từ chối task
|
||||
/// </summary>
|
||||
public record RejectTaskRequest(string Reason);
|
||||
@@ -0,0 +1,223 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MissionService.Domain.AggregatesModel.MissionAggregate;
|
||||
|
||||
namespace MissionService.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Admin controller for mission management
|
||||
/// VI: Controller admin để quản lý mission
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/v1/admin/missions")]
|
||||
[Authorize(Roles = "Admin")]
|
||||
public class AdminMissionsController : ControllerBase
|
||||
{
|
||||
private readonly IMissionRepository _missionRepository;
|
||||
private readonly ILogger<AdminMissionsController> _logger;
|
||||
|
||||
public AdminMissionsController(
|
||||
IMissionRepository missionRepository,
|
||||
ILogger<AdminMissionsController> logger)
|
||||
{
|
||||
_missionRepository = missionRepository;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get all missions (admin) / VI: Lấy tất cả missions (admin)
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> GetAllMissions()
|
||||
{
|
||||
var missions = await _missionRepository.GetActiveMissionsAsync();
|
||||
return Ok(missions.Select(m => MapToAdminDto(m)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Create new mission / VI: Tạo mission mới
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[ProducesResponseType(StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> CreateMission([FromBody] CreateMissionRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var reward = MissionReward.Create(
|
||||
request.RewardPoints,
|
||||
request.RewardMiningBoost,
|
||||
request.RewardXp,
|
||||
request.RewardBadgeId);
|
||||
|
||||
var mission = new Mission(
|
||||
code: request.Code,
|
||||
titleEn: request.TitleEn,
|
||||
titleVi: request.TitleVi,
|
||||
type: MissionType.FromValue<MissionType>(request.TypeId),
|
||||
category: MissionCategory.FromValue<MissionCategory>(request.CategoryId),
|
||||
reward: reward,
|
||||
frequency: FrequencyType.FromValue<FrequencyType>(request.FrequencyId),
|
||||
maxCompletions: request.MaxCompletions,
|
||||
startDate: request.StartDate,
|
||||
endDate: request.EndDate,
|
||||
priority: request.Priority);
|
||||
|
||||
_missionRepository.Add(mission);
|
||||
await _missionRepository.UnitOfWork.SaveEntitiesAsync();
|
||||
|
||||
_logger.LogInformation("Mission {Code} created by admin", request.Code);
|
||||
|
||||
return CreatedAtAction(nameof(GetMissionById), new { id = mission.Id }, MapToAdminDto(mission));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to create mission {Code}", request.Code);
|
||||
return BadRequest(new { Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get mission by ID / VI: Lấy mission theo ID
|
||||
/// </summary>
|
||||
[HttpGet("{id:guid}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetMissionById(Guid id)
|
||||
{
|
||||
var mission = await _missionRepository.GetByIdAsync(id);
|
||||
if (mission == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
return Ok(MapToAdminDto(mission));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Activate a mission / VI: Kích hoạt mission
|
||||
/// </summary>
|
||||
[HttpPost("{id:guid}/activate")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> ActivateMission(Guid id)
|
||||
{
|
||||
var mission = await _missionRepository.GetByIdAsync(id);
|
||||
if (mission == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
mission.Activate();
|
||||
_missionRepository.Update(mission);
|
||||
await _missionRepository.UnitOfWork.SaveEntitiesAsync();
|
||||
|
||||
_logger.LogInformation("Mission {Id} activated", id);
|
||||
return Ok(MapToAdminDto(mission));
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return BadRequest(new { Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Pause a mission / VI: Tạm dừng mission
|
||||
/// </summary>
|
||||
[HttpPost("{id:guid}/pause")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> PauseMission(Guid id)
|
||||
{
|
||||
var mission = await _missionRepository.GetByIdAsync(id);
|
||||
if (mission == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
mission.Pause();
|
||||
_missionRepository.Update(mission);
|
||||
await _missionRepository.UnitOfWork.SaveEntitiesAsync();
|
||||
|
||||
_logger.LogInformation("Mission {Id} paused", id);
|
||||
return Ok(MapToAdminDto(mission));
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return BadRequest(new { Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Archive a mission / VI: Lưu trữ mission
|
||||
/// </summary>
|
||||
[HttpPost("{id:guid}/archive")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> ArchiveMission(Guid id)
|
||||
{
|
||||
var mission = await _missionRepository.GetByIdAsync(id);
|
||||
if (mission == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
mission.Archive();
|
||||
_missionRepository.Update(mission);
|
||||
await _missionRepository.UnitOfWork.SaveEntitiesAsync();
|
||||
|
||||
_logger.LogInformation("Mission {Id} archived", id);
|
||||
return Ok(MapToAdminDto(mission));
|
||||
}
|
||||
|
||||
private static object MapToAdminDto(Mission m) => new
|
||||
{
|
||||
m.Id,
|
||||
m.Code,
|
||||
m.TitleEn,
|
||||
m.TitleVi,
|
||||
m.DescriptionEn,
|
||||
m.DescriptionVi,
|
||||
Type = m.Type.Name,
|
||||
Category = m.Category.Name,
|
||||
Status = m.Status.Name,
|
||||
Frequency = m.Frequency.Name,
|
||||
Reward = new
|
||||
{
|
||||
m.Reward.Points,
|
||||
m.Reward.MiningBoostPercent,
|
||||
m.Reward.ExperiencePoints,
|
||||
m.Reward.BadgeId
|
||||
},
|
||||
m.MaxCompletions,
|
||||
m.StartDate,
|
||||
m.EndDate,
|
||||
m.Priority,
|
||||
Rules = m.Rules.Select(r => new { r.RuleType, r.Operator, r.Value })
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Request to create a new mission / VI: Request tạo mission mới
|
||||
/// </summary>
|
||||
public record CreateMissionRequest(
|
||||
string Code,
|
||||
string TitleEn,
|
||||
string TitleVi,
|
||||
string? DescriptionEn,
|
||||
string? DescriptionVi,
|
||||
int TypeId,
|
||||
int CategoryId,
|
||||
int FrequencyId,
|
||||
decimal RewardPoints,
|
||||
decimal RewardMiningBoost,
|
||||
int RewardXp,
|
||||
string? RewardBadgeId,
|
||||
int MaxCompletions,
|
||||
DateTime StartDate,
|
||||
DateTime? EndDate,
|
||||
int Priority = 0);
|
||||
@@ -0,0 +1,156 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MissionService.API.Application.Commands;
|
||||
using MissionService.API.Application.Queries;
|
||||
|
||||
namespace MissionService.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Controller for mission operations
|
||||
/// VI: Controller cho các thao tác mission
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/v1/[controller]")]
|
||||
[Authorize]
|
||||
public class MissionsController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
private readonly ILogger<MissionsController> _logger;
|
||||
|
||||
public MissionsController(IMediator mediator, ILogger<MissionsController> logger)
|
||||
{
|
||||
_mediator = mediator;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get all available missions / VI: Lấy tất cả missions khả dụng
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(MissionsListResult), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> GetAvailableMissions()
|
||||
{
|
||||
var userId = GetCurrentUserId();
|
||||
var result = await _mediator.Send(new GetAvailableMissionsQuery(userId));
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get missions by category / VI: Lấy missions theo danh mục
|
||||
/// </summary>
|
||||
[HttpGet("category/{category}")]
|
||||
[AllowAnonymous]
|
||||
[ProducesResponseType(typeof(MissionsListResult), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> GetByCategory(string category)
|
||||
{
|
||||
var result = await _mediator.Send(new GetMissionsByCategoryQuery(category));
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get mission details / VI: Lấy chi tiết mission
|
||||
/// </summary>
|
||||
[HttpGet("{id:guid}")]
|
||||
[AllowAnonymous]
|
||||
[ProducesResponseType(typeof(MissionDetailsResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetDetails(Guid id)
|
||||
{
|
||||
var userId = TryGetCurrentUserId();
|
||||
var result = await _mediator.Send(new GetMissionDetailsQuery(id, userId));
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
return NotFound(new { Message = "Mission not found / Không tìm thấy mission" });
|
||||
}
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Start a mission task / VI: Bắt đầu task mission
|
||||
/// </summary>
|
||||
[HttpPost("{id:guid}/start")]
|
||||
[ProducesResponseType(typeof(StartTaskResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> StartMission(Guid id)
|
||||
{
|
||||
var userId = GetCurrentUserId();
|
||||
var result = await _mediator.Send(new StartMissionTaskCommand(userId, id));
|
||||
|
||||
if (!result.Success)
|
||||
{
|
||||
return BadRequest(result);
|
||||
}
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Update task progress / VI: Cập nhật tiến độ task
|
||||
/// </summary>
|
||||
[HttpPut("tasks/{taskId:guid}/progress")]
|
||||
[ProducesResponseType(typeof(UpdateProgressResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> UpdateProgress(Guid taskId, [FromBody] UpdateProgressRequest request)
|
||||
{
|
||||
var userId = GetCurrentUserId();
|
||||
var result = await _mediator.Send(new UpdateTaskProgressCommand(
|
||||
userId,
|
||||
taskId,
|
||||
request.CurrentValue,
|
||||
request.Evidence));
|
||||
|
||||
if (!result.Success)
|
||||
{
|
||||
return BadRequest(result);
|
||||
}
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Claim task reward / VI: Nhận thưởng task
|
||||
/// </summary>
|
||||
[HttpPost("tasks/{taskId:guid}/claim")]
|
||||
[ProducesResponseType(typeof(ClaimRewardResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> ClaimReward(Guid taskId)
|
||||
{
|
||||
var userId = GetCurrentUserId();
|
||||
var result = await _mediator.Send(new ClaimTaskRewardCommand(userId, taskId));
|
||||
|
||||
if (!result.Success)
|
||||
{
|
||||
return BadRequest(result);
|
||||
}
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
private Guid GetCurrentUserId()
|
||||
{
|
||||
var userIdClaim = User.FindFirst("sub") ?? User.FindFirst("user_id");
|
||||
if (userIdClaim != null && Guid.TryParse(userIdClaim.Value, out var userId))
|
||||
{
|
||||
return userId;
|
||||
}
|
||||
throw new UnauthorizedAccessException("User ID not found in token");
|
||||
}
|
||||
|
||||
private Guid? TryGetCurrentUserId()
|
||||
{
|
||||
var userIdClaim = User.FindFirst("sub") ?? User.FindFirst("user_id");
|
||||
if (userIdClaim != null && Guid.TryParse(userIdClaim.Value, out var userId))
|
||||
{
|
||||
return userId;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Request to update progress / VI: Request cập nhật tiến độ
|
||||
/// </summary>
|
||||
public record UpdateProgressRequest(int CurrentValue, TaskEvidenceDto? Evidence = null);
|
||||
Reference in New Issue
Block a user