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:
Ho Ngoc Hai
2026-01-17 18:32:02 +07:00
parent 7dd4f14f1b
commit 1dfd72a10a
7 changed files with 1081 additions and 1 deletions

View File

@@ -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");
}
}
}

View File

@@ -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);

View File

@@ -110,7 +110,7 @@ public record UserTaskProgressDto(
/// </summary>
public record UserMissionProgressResult(
int TotalMissions,
int CompletedMissions,
int CompletedMissionsCount,
int InProgressMissions,
decimal TotalPointsEarned,
IReadOnlyList<MissionSummaryDto> ActiveMissions,

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);