feat: Implement JWT authentication, SignalR, external service clients in MiningService, and update documentation across services.
This commit is contained in:
@@ -109,6 +109,33 @@ try
|
||||
Version = "v1",
|
||||
Description = "Real-time Chat Service with SignalR, AI Integration / Chat Service thời gian thực với SignalR, tích hợp AI"
|
||||
});
|
||||
|
||||
// EN: Add JWT Bearer security definition / VI: Thêm định nghĩa bảo mật JWT Bearer
|
||||
options.AddSecurityDefinition("Bearer", new Microsoft.OpenApi.Models.OpenApiSecurityScheme
|
||||
{
|
||||
Description = "JWT Authorization header using Bearer scheme. Example: \"Bearer {token}\" / Header Authorization JWT sử dụng scheme Bearer. Ví dụ: \"Bearer {token}\"",
|
||||
Name = "Authorization",
|
||||
In = Microsoft.OpenApi.Models.ParameterLocation.Header,
|
||||
Type = Microsoft.OpenApi.Models.SecuritySchemeType.Http,
|
||||
Scheme = "bearer",
|
||||
BearerFormat = "JWT"
|
||||
});
|
||||
|
||||
// EN: Add security requirement / VI: Thêm yêu cầu bảo mật
|
||||
options.AddSecurityRequirement(new Microsoft.OpenApi.Models.OpenApiSecurityRequirement
|
||||
{
|
||||
{
|
||||
new Microsoft.OpenApi.Models.OpenApiSecurityScheme
|
||||
{
|
||||
Reference = new Microsoft.OpenApi.Models.OpenApiReference
|
||||
{
|
||||
Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme,
|
||||
Id = "Bearer"
|
||||
}
|
||||
},
|
||||
Array.Empty<string>()
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// EN: Add health checks / VI: Thêm health checks
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MiningService.API.Application.Commands;
|
||||
using MiningService.API.Application.Queries;
|
||||
@@ -11,6 +12,7 @@ namespace MiningService.API.Controllers;
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/v1/admin")]
|
||||
[Authorize(Roles = "Admin")]
|
||||
public class AdminController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MiningService.API.Application.Commands;
|
||||
using MiningService.API.Application.Queries;
|
||||
@@ -11,6 +12,7 @@ namespace MiningService.API.Controllers;
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/v1/[controller]")]
|
||||
[Authorize]
|
||||
public class CirclesController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MiningService.API.Application.Commands;
|
||||
using MiningService.API.Application.Queries;
|
||||
@@ -11,6 +12,7 @@ namespace MiningService.API.Controllers;
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/v1/[controller]")]
|
||||
[Authorize]
|
||||
public class MiningController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MiningService.API.Application.Commands;
|
||||
using MiningService.API.Application.Queries;
|
||||
@@ -11,6 +12,7 @@ namespace MiningService.API.Controllers;
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/v1/[controller]")]
|
||||
[Authorize]
|
||||
public class ReferralsController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
using MiningService.API.ExternalServices;
|
||||
using Polly;
|
||||
using Polly.Extensions.Http;
|
||||
|
||||
namespace MiningService.API.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Extension methods for registering external service clients.
|
||||
/// VI: Extension methods để đăng ký các external service clients.
|
||||
/// </summary>
|
||||
public static class ExternalServicesExtensions
|
||||
{
|
||||
public static IServiceCollection AddExternalServices(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
// IAM Service Client
|
||||
services.AddHttpClient<IIamServiceClient, IamServiceClient>(client =>
|
||||
{
|
||||
client.BaseAddress = new Uri(configuration["ExternalServices:IamService:BaseUrl"] ?? "http://iam-service-net:8080");
|
||||
client.DefaultRequestHeaders.Add("Accept", "application/json");
|
||||
client.Timeout = TimeSpan.FromSeconds(30);
|
||||
})
|
||||
.AddPolicyHandler(GetRetryPolicy())
|
||||
.AddPolicyHandler(GetCircuitBreakerPolicy());
|
||||
|
||||
// Wallet Service Client
|
||||
services.AddHttpClient<IWalletServiceClient, WalletServiceClient>(client =>
|
||||
{
|
||||
client.BaseAddress = new Uri(configuration["ExternalServices:WalletService:BaseUrl"] ?? "http://wallet-service-net:8080");
|
||||
client.DefaultRequestHeaders.Add("Accept", "application/json");
|
||||
client.Timeout = TimeSpan.FromSeconds(30);
|
||||
})
|
||||
.AddPolicyHandler(GetRetryPolicy())
|
||||
.AddPolicyHandler(GetCircuitBreakerPolicy());
|
||||
|
||||
// Social Service Client
|
||||
services.AddHttpClient<ISocialServiceClient, SocialServiceClient>(client =>
|
||||
{
|
||||
client.BaseAddress = new Uri(configuration["ExternalServices:SocialService:BaseUrl"] ?? "http://social-service-net:8080");
|
||||
client.DefaultRequestHeaders.Add("Accept", "application/json");
|
||||
client.Timeout = TimeSpan.FromSeconds(30);
|
||||
})
|
||||
.AddPolicyHandler(GetRetryPolicy())
|
||||
.AddPolicyHandler(GetCircuitBreakerPolicy());
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
|
||||
{
|
||||
return HttpPolicyExtensions
|
||||
.HandleTransientHttpError()
|
||||
.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
|
||||
}
|
||||
|
||||
private static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
|
||||
{
|
||||
return HttpPolicyExtensions
|
||||
.HandleTransientHttpError()
|
||||
.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
namespace MiningService.API.ExternalServices;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Client interface for IAM Service communication.
|
||||
/// VI: Interface client giao tiếp với IAM Service.
|
||||
/// </summary>
|
||||
public interface IIamServiceClient
|
||||
{
|
||||
Task<UserInfo?> GetUserInfoAsync(Guid userId, CancellationToken cancellationToken = default);
|
||||
Task<bool> ValidateUserAsync(Guid userId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public record UserInfo(
|
||||
Guid UserId,
|
||||
string Email,
|
||||
string DisplayName,
|
||||
bool IsEmailVerified,
|
||||
bool IsKycVerified,
|
||||
DateTime CreatedAt);
|
||||
|
||||
/// <summary>
|
||||
/// EN: HTTP client implementation for IAM Service.
|
||||
/// VI: Triển khai HTTP client cho IAM Service.
|
||||
/// </summary>
|
||||
public class IamServiceClient : IIamServiceClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger<IamServiceClient> _logger;
|
||||
|
||||
public IamServiceClient(HttpClient httpClient, ILogger<IamServiceClient> logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<UserInfo?> GetUserInfoAsync(Guid userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetAsync($"/api/v1/users/{userId}", cancellationToken);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogWarning("Failed to get user info for {UserId}: {StatusCode}", userId, response.StatusCode);
|
||||
return null;
|
||||
}
|
||||
return await response.Content.ReadFromJsonAsync<UserInfo>(cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error calling IAM Service for user {UserId}", userId);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> ValidateUserAsync(Guid userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var userInfo = await GetUserInfoAsync(userId, cancellationToken);
|
||||
return userInfo != null && userInfo.IsEmailVerified;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
namespace MiningService.API.ExternalServices;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Client interface for Social Service communication.
|
||||
/// VI: Interface client giao tiếp với Social Service.
|
||||
/// </summary>
|
||||
public interface ISocialServiceClient
|
||||
{
|
||||
Task<List<FriendSuggestion>> GetFriendSuggestionsAsync(Guid userId, int limit = 10, CancellationToken cancellationToken = default);
|
||||
Task<bool> AreFriendsAsync(Guid userId1, Guid userId2, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public record FriendSuggestion(Guid UserId, string DisplayName, int MutualFriends, decimal TrustScore);
|
||||
|
||||
/// <summary>
|
||||
/// EN: HTTP client implementation for Social Service.
|
||||
/// VI: Triển khai HTTP client cho Social Service.
|
||||
/// </summary>
|
||||
public class SocialServiceClient : ISocialServiceClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger<SocialServiceClient> _logger;
|
||||
|
||||
public SocialServiceClient(HttpClient httpClient, ILogger<SocialServiceClient> logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<List<FriendSuggestion>> GetFriendSuggestionsAsync(Guid userId, int limit = 10, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetAsync($"/api/v1/social/friends/{userId}/suggestions?limit={limit}", cancellationToken);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogWarning("Failed to get friend suggestions for {UserId}: {StatusCode}", userId, response.StatusCode);
|
||||
return new List<FriendSuggestion>();
|
||||
}
|
||||
return await response.Content.ReadFromJsonAsync<List<FriendSuggestion>>(cancellationToken) ?? new();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error calling Social Service for user {UserId}", userId);
|
||||
return new List<FriendSuggestion>();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> AreFriendsAsync(Guid userId1, Guid userId2, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetAsync($"/api/v1/social/friends/{userId1}/check/{userId2}", cancellationToken);
|
||||
if (!response.IsSuccessStatusCode) return false;
|
||||
var result = await response.Content.ReadFromJsonAsync<FriendCheckResult>(cancellationToken);
|
||||
return result?.AreFriends ?? false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error checking friendship between {UserId1} and {UserId2}", userId1, userId2);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private record FriendCheckResult(bool AreFriends);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
namespace MiningService.API.ExternalServices;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Client interface for Wallet Service communication.
|
||||
/// VI: Interface client giao tiếp với Wallet Service.
|
||||
/// </summary>
|
||||
public interface IWalletServiceClient
|
||||
{
|
||||
Task<bool> TransferPointsAsync(Guid userId, decimal amount, string description, CancellationToken cancellationToken = default);
|
||||
Task<WalletBalance?> GetBalanceAsync(Guid userId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public record WalletBalance(Guid WalletId, Guid UserId, decimal AvailableBalance, decimal PendingBalance);
|
||||
|
||||
/// <summary>
|
||||
/// EN: HTTP client implementation for Wallet Service.
|
||||
/// VI: Triển khai HTTP client cho Wallet Service.
|
||||
/// </summary>
|
||||
public class WalletServiceClient : IWalletServiceClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger<WalletServiceClient> _logger;
|
||||
|
||||
public WalletServiceClient(HttpClient httpClient, ILogger<WalletServiceClient> logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<bool> TransferPointsAsync(Guid userId, decimal amount, string description, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new { UserId = userId, Amount = amount, Description = description, Source = "Mining" };
|
||||
var response = await _httpClient.PostAsJsonAsync("/api/v1/points/add", request, cancellationToken);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogWarning("Failed to transfer points for {UserId}: {StatusCode}", userId, response.StatusCode);
|
||||
return false;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Transferred {Amount} points to user {UserId}", amount, userId);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error calling Wallet Service for user {UserId}", userId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<WalletBalance?> GetBalanceAsync(Guid userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetAsync($"/api/v1/wallets/user/{userId}/balance", cancellationToken);
|
||||
if (!response.IsSuccessStatusCode) return null;
|
||||
return await response.Content.ReadFromJsonAsync<WalletBalance>(cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting wallet balance for user {UserId}", userId);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
using MediatR;
|
||||
using MiningService.Domain.AggregatesModel.MinerAggregate;
|
||||
using MiningService.Domain.AggregatesModel.ReferralAggregate;
|
||||
|
||||
namespace MiningService.API.IntegrationEvents.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for UserRegisteredIntegrationEvent - creates a new Miner when user registers.
|
||||
/// VI: Handler cho UserRegisteredIntegrationEvent - tạo Miner mới khi user đăng ký.
|
||||
/// </summary>
|
||||
public class UserRegisteredIntegrationEventHandler : INotificationHandler<UserRegisteredNotification>
|
||||
{
|
||||
private readonly IMinerRepository _minerRepository;
|
||||
private readonly IReferralRepository _referralRepository;
|
||||
private readonly ILogger<UserRegisteredIntegrationEventHandler> _logger;
|
||||
|
||||
public UserRegisteredIntegrationEventHandler(
|
||||
IMinerRepository minerRepository,
|
||||
IReferralRepository referralRepository,
|
||||
ILogger<UserRegisteredIntegrationEventHandler> logger)
|
||||
{
|
||||
_minerRepository = minerRepository;
|
||||
_referralRepository = referralRepository;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task Handle(UserRegisteredNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
var @event = notification.Event;
|
||||
|
||||
// Check if miner already exists
|
||||
var existingMiner = await _minerRepository.GetByUserIdAsync(@event.UserId, cancellationToken);
|
||||
if (existingMiner != null)
|
||||
{
|
||||
_logger.LogWarning("Miner already exists for user {UserId}", @event.UserId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new miner
|
||||
var miner = Miner.Create(@event.UserId);
|
||||
_minerRepository.Add(miner);
|
||||
|
||||
// If referral code provided, create referral relationship
|
||||
if (!string.IsNullOrEmpty(@event.ReferralCode))
|
||||
{
|
||||
var referrer = await _minerRepository.GetByReferralCodeAsync(@event.ReferralCode, cancellationToken);
|
||||
if (referrer != null)
|
||||
{
|
||||
var referral = Referral.Create(referrer.Id, miner.Id, @event.ReferralCode);
|
||||
_referralRepository.Add(referral);
|
||||
_logger.LogInformation("Created referral from {ReferrerId} to {ReferredId}", referrer.Id, miner.Id);
|
||||
}
|
||||
}
|
||||
|
||||
await _minerRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
|
||||
_logger.LogInformation("Created miner for user {UserId} with referral code {ReferralCode}",
|
||||
@event.UserId, miner.ReferralCode);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for UserKycCompletedIntegrationEvent - activates referral when user completes KYC.
|
||||
/// VI: Handler cho UserKycCompletedIntegrationEvent - kích hoạt referral khi user hoàn thành KYC.
|
||||
/// </summary>
|
||||
public class UserKycCompletedIntegrationEventHandler : INotificationHandler<UserKycCompletedNotification>
|
||||
{
|
||||
private readonly IMinerRepository _minerRepository;
|
||||
private readonly IReferralRepository _referralRepository;
|
||||
private readonly ILogger<UserKycCompletedIntegrationEventHandler> _logger;
|
||||
|
||||
public UserKycCompletedIntegrationEventHandler(
|
||||
IMinerRepository minerRepository,
|
||||
IReferralRepository referralRepository,
|
||||
ILogger<UserKycCompletedIntegrationEventHandler> logger)
|
||||
{
|
||||
_minerRepository = minerRepository;
|
||||
_referralRepository = referralRepository;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task Handle(UserKycCompletedNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
var @event = notification.Event;
|
||||
|
||||
var miner = await _minerRepository.GetByUserIdAsync(@event.UserId, cancellationToken);
|
||||
if (miner == null)
|
||||
{
|
||||
_logger.LogWarning("Miner not found for user {UserId} during KYC completion", @event.UserId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find inactive referral for this user
|
||||
var referral = await _referralRepository.GetByReferredIdAsync(miner.Id, cancellationToken);
|
||||
if (referral != null && !referral.IsActive)
|
||||
{
|
||||
referral.Activate();
|
||||
_logger.LogInformation("Activated referral {ReferralId} for user {UserId}", referral.Id, @event.UserId);
|
||||
await _referralRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MediatR notification wrappers
|
||||
public record UserRegisteredNotification(UserRegisteredIntegrationEvent Event) : INotification;
|
||||
public record UserKycCompletedNotification(UserKycCompletedIntegrationEvent Event) : INotification;
|
||||
@@ -0,0 +1,67 @@
|
||||
namespace MiningService.API.IntegrationEvents;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Base interface for integration events.
|
||||
/// VI: Interface cơ sở cho integration events.
|
||||
/// </summary>
|
||||
public interface IIntegrationEvent
|
||||
{
|
||||
Guid EventId { get; }
|
||||
DateTime OccurredOn { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Event published when points are mined and should be credited to wallet.
|
||||
/// VI: Event được publish khi điểm được đào và cần credit vào ví.
|
||||
/// </summary>
|
||||
public record PointsMinedIntegrationEvent(
|
||||
Guid EventId,
|
||||
DateTime OccurredOn,
|
||||
Guid UserId,
|
||||
Guid MinerId,
|
||||
decimal Points,
|
||||
string Source,
|
||||
int StreakDays) : IIntegrationEvent;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Event published when a referral is activated (user completed KYC).
|
||||
/// VI: Event được publish khi referral được kích hoạt (user đã KYC).
|
||||
/// </summary>
|
||||
public record ReferralActivatedIntegrationEvent(
|
||||
Guid EventId,
|
||||
DateTime OccurredOn,
|
||||
Guid ReferrerId,
|
||||
Guid ReferredUserId,
|
||||
decimal BonusRate) : IIntegrationEvent;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Event published when a security circle is completed (3+ members).
|
||||
/// VI: Event được publish khi vòng tròn an toàn hoàn thành (3+ thành viên).
|
||||
/// </summary>
|
||||
public record CircleCompletedIntegrationEvent(
|
||||
Guid EventId,
|
||||
DateTime OccurredOn,
|
||||
Guid CircleId,
|
||||
Guid OwnerId,
|
||||
int MemberCount,
|
||||
decimal BonusMultiplier) : IIntegrationEvent;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Event consumed when user registered in IAM Service.
|
||||
/// VI: Event được consume khi user đăng ký trong IAM Service.
|
||||
/// </summary>
|
||||
public record UserRegisteredIntegrationEvent(
|
||||
Guid EventId,
|
||||
DateTime OccurredOn,
|
||||
Guid UserId,
|
||||
string Email,
|
||||
string? ReferralCode) : IIntegrationEvent;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Event consumed when user completed KYC verification.
|
||||
/// VI: Event được consume khi user hoàn thành xác thực KYC.
|
||||
/// </summary>
|
||||
public record UserKycCompletedIntegrationEvent(
|
||||
Guid EventId,
|
||||
DateTime OccurredOn,
|
||||
Guid UserId) : IIntegrationEvent;
|
||||
@@ -14,10 +14,12 @@
|
||||
<!-- EN: FluentValidation for request validation / VI: FluentValidation cho validation request -->
|
||||
<PackageReference Include="FluentValidation" Version="11.11.0" />
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.11.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="10.0.2" />
|
||||
|
||||
<!-- EN: Swagger/OpenAPI / VI: Swagger/OpenAPI -->
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
using Asp.Versioning;
|
||||
using FluentValidation;
|
||||
using Hellang.Middleware.ProblemDetails;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using MiningService.API.Application.Behaviors;
|
||||
using MiningService.API.Extensions;
|
||||
using MiningService.API.Hubs;
|
||||
using MiningService.Infrastructure;
|
||||
using Serilog;
|
||||
using System.Text;
|
||||
|
||||
// EN: Configure Serilog early / VI: Cấu hình Serilog sớm
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
@@ -26,6 +32,9 @@ try
|
||||
// EN: Add Infrastructure services / VI: Thêm Infrastructure services
|
||||
builder.Services.AddInfrastructure(builder.Configuration);
|
||||
|
||||
// EN: Add External Service clients (IAM, Wallet, Social) / VI: Thêm External Service clients
|
||||
builder.Services.AddExternalServices(builder.Configuration);
|
||||
|
||||
// EN: Add MediatR with behaviors / VI: Thêm MediatR với behaviors
|
||||
builder.Services.AddMediatR(cfg =>
|
||||
{
|
||||
@@ -38,6 +47,51 @@ try
|
||||
// EN: Add FluentValidation / VI: Thêm FluentValidation
|
||||
builder.Services.AddValidatorsFromAssemblyContaining<Program>();
|
||||
|
||||
// EN: Add JWT Authentication / VI: Thêm JWT Authentication
|
||||
builder.Services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
})
|
||||
.AddJwtBearer(options =>
|
||||
{
|
||||
var jwtSecret = builder.Configuration["Jwt:Secret"] ?? "your-super-secret-key-min-32-characters-long";
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidateAudience = true,
|
||||
ValidateLifetime = true,
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidIssuer = builder.Configuration["Jwt:Issuer"] ?? "goodgo-platform",
|
||||
ValidAudience = builder.Configuration["Jwt:Audience"] ?? "goodgo-services",
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSecret))
|
||||
};
|
||||
|
||||
// EN: Support JWT in SignalR query string / VI: Hỗ trợ JWT trong query string SignalR
|
||||
options.Events = new JwtBearerEvents
|
||||
{
|
||||
OnMessageReceived = context =>
|
||||
{
|
||||
var accessToken = context.Request.Query["access_token"];
|
||||
var path = context.HttpContext.Request.Path;
|
||||
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs"))
|
||||
{
|
||||
context.Token = accessToken;
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
};
|
||||
});
|
||||
builder.Services.AddAuthorization();
|
||||
|
||||
// EN: Add SignalR for real-time updates / VI: Thêm SignalR cho cập nhật thời gian thực
|
||||
builder.Services.AddSignalR(options =>
|
||||
{
|
||||
options.EnableDetailedErrors = builder.Environment.IsDevelopment();
|
||||
options.KeepAliveInterval = TimeSpan.FromSeconds(15);
|
||||
});
|
||||
builder.Services.AddSingleton<IMiningHubService, MiningHubService>();
|
||||
|
||||
// EN: Add API versioning / VI: Thêm API versioning
|
||||
builder.Services.AddApiVersioning(options =>
|
||||
{
|
||||
@@ -64,7 +118,7 @@ try
|
||||
builder.Environment.IsDevelopment();
|
||||
});
|
||||
|
||||
// EN: Add Swagger / VI: Thêm Swagger
|
||||
// EN: Add Swagger with JWT Bearer / VI: Thêm Swagger với JWT Bearer
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen(options =>
|
||||
{
|
||||
@@ -72,7 +126,33 @@ try
|
||||
{
|
||||
Title = "MiningService API",
|
||||
Version = "v1",
|
||||
Description = "MiningService microservice API / API microservice MiningService"
|
||||
Description = "Pi Network-style Point Mining Service / Dịch vụ đào điểm kiểu Pi Network"
|
||||
});
|
||||
|
||||
// EN: Add JWT Bearer security definition / VI: Thêm định nghĩa bảo mật JWT Bearer
|
||||
options.AddSecurityDefinition("Bearer", new Microsoft.OpenApi.Models.OpenApiSecurityScheme
|
||||
{
|
||||
Description = "JWT Authorization header. Example: \"Bearer {token}\"",
|
||||
Name = "Authorization",
|
||||
In = Microsoft.OpenApi.Models.ParameterLocation.Header,
|
||||
Type = Microsoft.OpenApi.Models.SecuritySchemeType.Http,
|
||||
Scheme = "bearer",
|
||||
BearerFormat = "JWT"
|
||||
});
|
||||
|
||||
options.AddSecurityRequirement(new Microsoft.OpenApi.Models.OpenApiSecurityRequirement
|
||||
{
|
||||
{
|
||||
new Microsoft.OpenApi.Models.OpenApiSecurityScheme
|
||||
{
|
||||
Reference = new Microsoft.OpenApi.Models.OpenApiReference
|
||||
{
|
||||
Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme,
|
||||
Id = "Bearer"
|
||||
}
|
||||
},
|
||||
Array.Empty<string>()
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -90,9 +170,12 @@ try
|
||||
{
|
||||
options.AddDefaultPolicy(policy =>
|
||||
{
|
||||
policy.AllowAnyOrigin()
|
||||
policy.WithOrigins(
|
||||
builder.Configuration.GetSection("AllowedOrigins").Get<string[]>()
|
||||
?? ["http://localhost:3000", "http://localhost:5173"])
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader();
|
||||
.AllowAnyHeader()
|
||||
.AllowCredentials(); // EN: Required for SignalR / VI: Bắt buộc cho SignalR
|
||||
});
|
||||
});
|
||||
|
||||
@@ -114,6 +197,10 @@ try
|
||||
|
||||
app.UseCors();
|
||||
app.UseRouting();
|
||||
|
||||
// EN: Add Authentication & Authorization middleware / VI: Thêm middleware xác thực & phân quyền
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
// EN: Map health check endpoints / VI: Map health check endpoints
|
||||
app.MapHealthChecks("/health");
|
||||
@@ -125,6 +212,9 @@ try
|
||||
|
||||
// EN: Map controllers / VI: Map controllers
|
||||
app.MapControllers();
|
||||
|
||||
// EN: Map SignalR Hub / VI: Map SignalR Hub
|
||||
app.MapHub<MiningHub>("/hubs/mining");
|
||||
|
||||
// EN: Run the application / VI: Chạy ứng dụng
|
||||
app.Run();
|
||||
@@ -142,3 +232,4 @@ finally
|
||||
// EN: Make Program class accessible for integration tests
|
||||
// VI: Làm cho class Program có thể truy cập cho integration tests
|
||||
public partial class Program { }
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
Technical architecture documentation for the Mission Service.
|
||||
|
||||
📖 **See also:** [README - Service Overview](./README.md)
|
||||
|
||||
## High-Level Architecture
|
||||
|
||||
```mermaid
|
||||
@@ -196,6 +198,129 @@ classDiagram
|
||||
UserCheckIn --> StreakBonus : applies
|
||||
```
|
||||
|
||||
### Reward Aggregate
|
||||
|
||||
```mermaid
|
||||
%%{init: {'theme':'dark'}}%%
|
||||
classDiagram
|
||||
class UserReward {
|
||||
+Guid Id
|
||||
+Guid UserId
|
||||
+Guid SourceId
|
||||
+RewardType Type
|
||||
+RewardStatus Status
|
||||
+RewardAmount Amount
|
||||
+DateTime EarnedAt
|
||||
+DateTime ClaimedAt
|
||||
+Claim()
|
||||
+Expire()
|
||||
}
|
||||
|
||||
class RewardAmount {
|
||||
+decimal Points
|
||||
+decimal BonusPoints
|
||||
+string Currency
|
||||
+TotalPoints()
|
||||
}
|
||||
|
||||
class RewardType {
|
||||
<<enumeration>>
|
||||
MISSION_COMPLETE
|
||||
CHECKIN_DAILY
|
||||
CHECKIN_MILESTONE
|
||||
REFERRAL_BONUS
|
||||
SOCIAL_ACTION
|
||||
}
|
||||
|
||||
UserReward --> RewardAmount : has
|
||||
UserReward --> RewardType : uses
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Value Objects
|
||||
|
||||
### MissionReward
|
||||
|
||||
```csharp
|
||||
public record MissionReward : ValueObject
|
||||
{
|
||||
public decimal Points { get; init; }
|
||||
public decimal MiningBoostPercent { get; init; }
|
||||
public int ExperiencePoints { get; init; }
|
||||
public string? BadgeId { get; init; }
|
||||
|
||||
public static MissionReward Create(
|
||||
decimal points,
|
||||
decimal miningBoost = 0,
|
||||
int xp = 0,
|
||||
string? badge = null)
|
||||
{
|
||||
Guard.Against.Negative(points, nameof(points));
|
||||
Guard.Against.Negative(miningBoost, nameof(miningBoost));
|
||||
|
||||
return new MissionReward
|
||||
{
|
||||
Points = points,
|
||||
MiningBoostPercent = miningBoost,
|
||||
ExperiencePoints = xp,
|
||||
BadgeId = badge
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### TaskProgress
|
||||
|
||||
```csharp
|
||||
public record TaskProgress : ValueObject
|
||||
{
|
||||
public int CurrentValue { get; init; }
|
||||
public int TargetValue { get; init; }
|
||||
public DateTime LastUpdated { get; init; }
|
||||
|
||||
public decimal PercentComplete =>
|
||||
TargetValue > 0
|
||||
? Math.Min(100, (CurrentValue * 100m) / TargetValue)
|
||||
: 0;
|
||||
|
||||
public bool IsComplete => CurrentValue >= TargetValue;
|
||||
|
||||
public TaskProgress UpdateProgress(int newValue)
|
||||
{
|
||||
return this with
|
||||
{
|
||||
CurrentValue = Math.Min(newValue, TargetValue),
|
||||
LastUpdated = DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### TaskEvidence
|
||||
|
||||
```csharp
|
||||
public record TaskEvidence : ValueObject
|
||||
{
|
||||
public EvidenceType Type { get; init; }
|
||||
public string Data { get; init; } = string.Empty;
|
||||
public string? ScreenshotUrl { get; init; }
|
||||
public string? VideoUrl { get; init; }
|
||||
public DateTime CapturedAt { get; init; }
|
||||
|
||||
public enum EvidenceType
|
||||
{
|
||||
WatchDuration, // Video watch duration
|
||||
ClickData, // Click information
|
||||
UploadedContent, // Uploaded content URL
|
||||
SocialProof, // Screenshot of social action
|
||||
InviteCode // Used invite code
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Schema
|
||||
|
||||
### ER Diagram
|
||||
@@ -360,9 +485,38 @@ flowchart LR
|
||||
style Wallet fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px
|
||||
```
|
||||
|
||||
### Events Consumed
|
||||
|
||||
```mermaid
|
||||
%%{init: {'theme':'dark'}}%%
|
||||
flowchart RL
|
||||
subgraph Publishers["📤 Publishers"]
|
||||
IAM[IAM Service]
|
||||
Mining[Mining Service]
|
||||
end
|
||||
|
||||
subgraph Events["📨 Integration Events"]
|
||||
E1[UserRegisteredEvent]
|
||||
E2[UserDeletedEvent]
|
||||
E3[ReferralActivatedEvent]
|
||||
end
|
||||
|
||||
subgraph Mission["📋 Mission Service"]
|
||||
M[Events Consumer]
|
||||
end
|
||||
|
||||
IAM --> E1 --> M
|
||||
IAM --> E2 --> M
|
||||
Mining --> E3 --> M
|
||||
|
||||
style Events fill:#3498DB,color:#ECF0F1,stroke:#2980B9,stroke-width:2px
|
||||
style Mission fill:#8E44AD,color:#ECF0F1,stroke:#7D3C98,stroke-width:3px
|
||||
```
|
||||
|
||||
### Event Payloads
|
||||
|
||||
```csharp
|
||||
// Published Events
|
||||
public record MissionCompletedEvent(
|
||||
Guid TaskId,
|
||||
Guid UserId,
|
||||
@@ -382,15 +536,116 @@ public record CheckInCompletedEvent(
|
||||
DateTime CheckedInAt
|
||||
);
|
||||
|
||||
public record RewardClaimedEvent(
|
||||
Guid TaskId,
|
||||
// Consumed Events from IAM
|
||||
public record UserRegisteredEvent(
|
||||
Guid UserId,
|
||||
decimal PointsAmount,
|
||||
string RewardType,
|
||||
DateTime ClaimedAt
|
||||
string Email,
|
||||
string DisplayName,
|
||||
DateTime RegisteredAt
|
||||
);
|
||||
|
||||
// Consumed Events from Mining
|
||||
public record ReferralActivatedEvent(
|
||||
Guid InviterId,
|
||||
Guid InviteeId,
|
||||
string InviteCode,
|
||||
DateTime ActivatedAt
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## IAM Service Integration
|
||||
|
||||
### JWT Authentication Flow
|
||||
|
||||
```mermaid
|
||||
%%{init: {'theme':'dark'}}%%
|
||||
sequenceDiagram
|
||||
participant Client as 📱 Client
|
||||
participant API as 🌐 Mission API
|
||||
participant Auth as 🔐 JWT Middleware
|
||||
|
||||
Client->>API: Request + Bearer Token
|
||||
API->>Auth: Validate Token
|
||||
Auth->>Auth: Verify Signature
|
||||
Auth->>Auth: Check Expiration
|
||||
Auth->>Auth: Extract Claims
|
||||
|
||||
alt Token Valid
|
||||
Auth->>API: UserId, Roles, Permissions
|
||||
API->>API: Process Request
|
||||
API-->>Client: 200 OK Response
|
||||
else Token Invalid
|
||||
Auth-->>Client: 401 Unauthorized
|
||||
end
|
||||
```
|
||||
|
||||
### IAM Service Client
|
||||
|
||||
```csharp
|
||||
public interface IIamServiceClient
|
||||
{
|
||||
Task<UserInfo?> GetUserInfoAsync(Guid userId, CancellationToken ct);
|
||||
Task<List<UserInfo>> GetUsersByIdsAsync(List<Guid> userIds, CancellationToken ct);
|
||||
}
|
||||
|
||||
public record UserInfo(
|
||||
Guid Id,
|
||||
string Email,
|
||||
string DisplayName,
|
||||
string? AvatarUrl,
|
||||
DateTime CreatedAt
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Storage Service Integration
|
||||
|
||||
### Upload Flow (Pay Per Upload)
|
||||
|
||||
```mermaid
|
||||
%%{init: {'theme':'dark'}}%%
|
||||
sequenceDiagram
|
||||
participant Client as 📱 Client
|
||||
participant API as 🌐 Mission API
|
||||
participant Storage as 📦 Storage Service
|
||||
participant AI as 🤖 Content Moderation
|
||||
|
||||
Client->>API: POST /tasks/{id}/upload
|
||||
API->>Storage: POST /api/v1/upload
|
||||
Storage-->>API: { fileId, url }
|
||||
API->>AI: Moderate Content (async)
|
||||
API-->>Client: 202 Accepted
|
||||
|
||||
Note over AI: Background Processing
|
||||
AI-->>API: Moderation Result
|
||||
```
|
||||
|
||||
### Storage Service Client
|
||||
|
||||
```csharp
|
||||
public interface IStorageServiceClient
|
||||
{
|
||||
Task<UploadResult> UploadFileAsync(
|
||||
Stream fileStream,
|
||||
string fileName,
|
||||
string contentType,
|
||||
CancellationToken ct);
|
||||
|
||||
Task<SignedUrlResult> GetSignedUrlAsync(
|
||||
Guid fileId,
|
||||
TimeSpan ttl,
|
||||
CancellationToken ct);
|
||||
}
|
||||
|
||||
public record UploadResult(Guid FileId, string Url, long SizeBytes);
|
||||
public record SignedUrlResult(string SignedUrl, DateTime ExpiresAt);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Service Dependencies
|
||||
|
||||
```mermaid
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
> **EN**: Mission & Task gamification service for GoodGo Platform.
|
||||
> **VI**: Dịch vụ gamification Mission & Task cho GoodGo Platform.
|
||||
|
||||
📖 **See also:** [Detailed Architecture Documentation](./ARCHITECTURE.md)
|
||||
|
||||
## Overview
|
||||
|
||||
The **Mission Service** manages gamification missions and tasks, enabling users to earn rewards through various activities on the GoodGo platform.
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
Tài liệu kiến trúc kỹ thuật cho Mission Service.
|
||||
|
||||
📖 **Xem thêm:** [README - Tổng Quan Dịch Vụ](./README.md)
|
||||
|
||||
## Kiến Trúc Tổng Quan
|
||||
|
||||
```mermaid
|
||||
@@ -196,6 +198,128 @@ classDiagram
|
||||
UserCheckIn --> StreakBonus : áp dụng
|
||||
```
|
||||
|
||||
### Reward Aggregate
|
||||
|
||||
```mermaid
|
||||
%%{init: {'theme':'dark'}}%%
|
||||
classDiagram
|
||||
class UserReward {
|
||||
+Guid Id
|
||||
+Guid UserId
|
||||
+Guid SourceId
|
||||
+RewardType Type
|
||||
+RewardStatus Status
|
||||
+RewardAmount Amount
|
||||
+DateTime EarnedAt
|
||||
+DateTime ClaimedAt
|
||||
+Claim()
|
||||
+Expire()
|
||||
}
|
||||
|
||||
class RewardAmount {
|
||||
+decimal Points
|
||||
+decimal BonusPoints
|
||||
+string Currency
|
||||
+TotalPoints()
|
||||
}
|
||||
|
||||
class RewardType {
|
||||
<<enumeration>>
|
||||
MISSION_COMPLETE
|
||||
CHECKIN_DAILY
|
||||
CHECKIN_MILESTONE
|
||||
REFERRAL_BONUS
|
||||
SOCIAL_ACTION
|
||||
}
|
||||
|
||||
UserReward --> RewardAmount : has
|
||||
UserReward --> RewardType : uses
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Value Objects
|
||||
|
||||
### MissionReward
|
||||
|
||||
```csharp
|
||||
public record MissionReward : ValueObject
|
||||
{
|
||||
public decimal Points { get; init; }
|
||||
public decimal MiningBoostPercent { get; init; }
|
||||
public int ExperiencePoints { get; init; }
|
||||
public string? BadgeId { get; init; }
|
||||
|
||||
public static MissionReward Create(
|
||||
decimal points,
|
||||
decimal miningBoost = 0,
|
||||
int xp = 0,
|
||||
string? badge = null)
|
||||
{
|
||||
Guard.Against.Negative(points, nameof(points));
|
||||
Guard.Against.Negative(miningBoost, nameof(miningBoost));
|
||||
|
||||
return new MissionReward
|
||||
{
|
||||
Points = points,
|
||||
MiningBoostPercent = miningBoost,
|
||||
ExperiencePoints = xp,
|
||||
BadgeId = badge
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### TaskProgress
|
||||
|
||||
```csharp
|
||||
public record TaskProgress : ValueObject
|
||||
{
|
||||
public int CurrentValue { get; init; }
|
||||
public int TargetValue { get; init; }
|
||||
public DateTime LastUpdated { get; init; }
|
||||
|
||||
public decimal PercentComplete =>
|
||||
TargetValue > 0
|
||||
? Math.Min(100, (CurrentValue * 100m) / TargetValue)
|
||||
: 0;
|
||||
|
||||
public bool IsComplete => CurrentValue >= TargetValue;
|
||||
|
||||
public TaskProgress UpdateProgress(int newValue)
|
||||
{
|
||||
return this with
|
||||
{
|
||||
CurrentValue = Math.Min(newValue, TargetValue),
|
||||
LastUpdated = DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### TaskEvidence
|
||||
|
||||
```csharp
|
||||
public record TaskEvidence : ValueObject
|
||||
{
|
||||
public EvidenceType Type { get; init; }
|
||||
public string Data { get; init; } = string.Empty;
|
||||
public string? ScreenshotUrl { get; init; }
|
||||
public string? VideoUrl { get; init; }
|
||||
public DateTime CapturedAt { get; init; }
|
||||
|
||||
public enum EvidenceType
|
||||
{
|
||||
WatchDuration, // Video xem được bao lâu
|
||||
ClickData, // Thông tin click
|
||||
UploadedContent, // URL nội dung tải lên
|
||||
SocialProof, // Screenshot hành động xã hội
|
||||
InviteCode // Mã mời đã sử dụng
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
## Database Schema
|
||||
|
||||
### ER Diagram
|
||||
@@ -360,9 +484,38 @@ flowchart LR
|
||||
style Wallet fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px
|
||||
```
|
||||
|
||||
### Các Event Được Tiêu Thụ
|
||||
|
||||
```mermaid
|
||||
%%{init: {'theme':'dark'}}%%
|
||||
flowchart RL
|
||||
subgraph Publishers["📤 Nhà Xuất Bản"]
|
||||
IAM[IAM Service]
|
||||
Mining[Mining Service]
|
||||
end
|
||||
|
||||
subgraph Events["📨 Integration Events"]
|
||||
E1[UserRegisteredEvent]
|
||||
E2[UserDeletedEvent]
|
||||
E3[ReferralActivatedEvent]
|
||||
end
|
||||
|
||||
subgraph Mission["📋 Mission Service"]
|
||||
M[Events Consumer]
|
||||
end
|
||||
|
||||
IAM --> E1 --> M
|
||||
IAM --> E2 --> M
|
||||
Mining --> E3 --> M
|
||||
|
||||
style Events fill:#3498DB,color:#ECF0F1,stroke:#2980B9,stroke-width:2px
|
||||
style Mission fill:#8E44AD,color:#ECF0F1,stroke:#7D3C98,stroke-width:3px
|
||||
```
|
||||
|
||||
### Event Payloads
|
||||
|
||||
```csharp
|
||||
// Events Xuất Bản
|
||||
public record MissionCompletedEvent(
|
||||
Guid TaskId,
|
||||
Guid UserId,
|
||||
@@ -389,8 +542,224 @@ public record RewardClaimedEvent(
|
||||
string RewardType,
|
||||
DateTime ClaimedAt
|
||||
);
|
||||
|
||||
// Events Tiêu Thụ từ IAM
|
||||
public record UserRegisteredEvent(
|
||||
Guid UserId,
|
||||
string Email,
|
||||
string DisplayName,
|
||||
DateTime RegisteredAt
|
||||
);
|
||||
|
||||
public record UserDeletedEvent(
|
||||
Guid UserId,
|
||||
DateTime DeletedAt
|
||||
);
|
||||
|
||||
// Events Tiêu Thụ từ Mining
|
||||
public record ReferralActivatedEvent(
|
||||
Guid InviterId,
|
||||
Guid InviteeId,
|
||||
string InviteCode,
|
||||
DateTime ActivatedAt
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tích Hợp IAM Service
|
||||
|
||||
### Luồng Xác Thực JWT
|
||||
|
||||
```mermaid
|
||||
%%{init: {'theme':'dark'}}%%
|
||||
sequenceDiagram
|
||||
participant Client as 📱 Client
|
||||
participant API as 🌐 Mission API
|
||||
participant Auth as 🔐 JWT Middleware
|
||||
participant IAM as 👤 IAM Service
|
||||
|
||||
Client->>API: Request + Bearer Token
|
||||
API->>Auth: Validate Token
|
||||
Auth->>Auth: Verify Signature (public key)
|
||||
Auth->>Auth: Check Expiration
|
||||
Auth->>Auth: Extract Claims
|
||||
|
||||
alt Token Hợp Lệ
|
||||
Auth->>API: UserId, Roles, Permissions
|
||||
API->>API: Xử lý Request
|
||||
API-->>Client: 200 OK Response
|
||||
else Token Không Hợp Lệ
|
||||
Auth-->>Client: 401 Unauthorized
|
||||
else Token Hết Hạn
|
||||
Auth-->>Client: 401 Token Expired
|
||||
end
|
||||
```
|
||||
|
||||
### Handler UserRegisteredEvent
|
||||
|
||||
```csharp
|
||||
public class UserRegisteredEventConsumer : IConsumer<UserRegisteredEvent>
|
||||
{
|
||||
private readonly IMissionDbContext _dbContext;
|
||||
private readonly ILogger<UserRegisteredEventConsumer> _logger;
|
||||
|
||||
public async Task Consume(ConsumeContext<UserRegisteredEvent> context)
|
||||
{
|
||||
var @event = context.Message;
|
||||
|
||||
// Tạo hồ sơ check-in cho user mới
|
||||
var userCheckIn = new UserCheckIn(
|
||||
userId: @event.UserId,
|
||||
currentStreak: 0,
|
||||
longestStreak: 0,
|
||||
totalCheckIns: 0
|
||||
);
|
||||
|
||||
_dbContext.UserCheckIns.Add(userCheckIn);
|
||||
await _dbContext.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation(
|
||||
"Created check-in profile for new user {UserId}",
|
||||
@event.UserId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### IAM Service Client
|
||||
|
||||
```csharp
|
||||
public interface IIamServiceClient
|
||||
{
|
||||
Task<UserInfo?> GetUserInfoAsync(Guid userId, CancellationToken ct);
|
||||
Task<List<UserInfo>> GetUsersByIdsAsync(List<Guid> userIds, CancellationToken ct);
|
||||
}
|
||||
|
||||
public record UserInfo(
|
||||
Guid Id,
|
||||
string Email,
|
||||
string DisplayName,
|
||||
string? AvatarUrl,
|
||||
DateTime CreatedAt
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tích Hợp Storage Service
|
||||
|
||||
### Luồng Upload Nội Dung (Pay Per Upload)
|
||||
|
||||
```mermaid
|
||||
%%{init: {'theme':'dark'}}%%
|
||||
sequenceDiagram
|
||||
participant Client as 📱 Client
|
||||
participant API as 🌐 Mission API
|
||||
participant Storage as 📦 Storage Service
|
||||
participant AI as 🤖 Content Moderation
|
||||
participant DB as 💾 PostgreSQL
|
||||
|
||||
Client->>API: POST /tasks/{id}/upload
|
||||
API->>API: Validate Task Status
|
||||
|
||||
API->>Storage: POST /api/v1/upload (multipart)
|
||||
Storage->>Storage: Validate File (type, size)
|
||||
Storage->>Storage: Store File
|
||||
Storage-->>API: { fileId, url, metadata }
|
||||
|
||||
API->>AI: Moderate Content (async)
|
||||
|
||||
API->>DB: Save TaskEvidence { fileUrl }
|
||||
API-->>Client: 202 Accepted { evidenceId }
|
||||
|
||||
Note over AI,DB: Background Processing
|
||||
AI-->>API: Moderation Result
|
||||
API->>DB: Update Verification Status
|
||||
```
|
||||
|
||||
### Luồng Lấy Video cho Mission
|
||||
|
||||
```mermaid
|
||||
%%{init: {'theme':'dark'}}%%
|
||||
sequenceDiagram
|
||||
participant Client as 📱 Client
|
||||
participant API as 🌐 Mission API
|
||||
participant Storage as 📦 Storage Service
|
||||
|
||||
Client->>API: GET /missions/{id}
|
||||
API->>API: Get Mission Details
|
||||
|
||||
alt Mission có Video
|
||||
API->>Storage: GET /api/v1/files/{fileId}/signed-url
|
||||
Storage->>Storage: Generate Signed URL (TTL: 1h)
|
||||
Storage-->>API: { signedUrl, expiresAt }
|
||||
API-->>Client: Mission { ..., videoUrl }
|
||||
else Mission không có Video
|
||||
API-->>Client: Mission { ... }
|
||||
end
|
||||
```
|
||||
|
||||
### Storage Service Client
|
||||
|
||||
```csharp
|
||||
public interface IStorageServiceClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Upload file lên Storage Service
|
||||
/// </summary>
|
||||
Task<UploadResult> UploadFileAsync(
|
||||
Stream fileStream,
|
||||
string fileName,
|
||||
string contentType,
|
||||
string folder,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Lấy signed URL để truy cập file
|
||||
/// </summary>
|
||||
Task<SignedUrlResult> GetSignedUrlAsync(
|
||||
Guid fileId,
|
||||
TimeSpan ttl,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Xóa file
|
||||
/// </summary>
|
||||
Task DeleteFileAsync(Guid fileId, CancellationToken ct);
|
||||
}
|
||||
|
||||
public record UploadResult(
|
||||
Guid FileId,
|
||||
string Url,
|
||||
string ContentType,
|
||||
long SizeBytes,
|
||||
DateTime UploadedAt
|
||||
);
|
||||
|
||||
public record SignedUrlResult(
|
||||
string SignedUrl,
|
||||
DateTime ExpiresAt
|
||||
);
|
||||
```
|
||||
|
||||
### Cấu Hình Storage
|
||||
|
||||
```csharp
|
||||
public class StorageServiceOptions
|
||||
{
|
||||
public string BaseUrl { get; set; } = "http://storage-service:8080";
|
||||
public int TimeoutSeconds { get; set; } = 30;
|
||||
public long MaxFileSizeBytes { get; set; } = 50 * 1024 * 1024; // 50MB
|
||||
public string[] AllowedContentTypes { get; set; } =
|
||||
{
|
||||
"image/jpeg", "image/png", "image/webp",
|
||||
"video/mp4", "video/webm"
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phụ Thuộc Dịch Vụ
|
||||
|
||||
```mermaid
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
> **EN**: Mission & Task gamification service for GoodGo Platform.
|
||||
> **VI**: Dịch vụ gamification Mission & Task cho GoodGo Platform.
|
||||
|
||||
📖 **Xem thêm:** [Tài liệu Kiến Trúc Chi Tiết](./ARCHITECTURE.md)
|
||||
|
||||
## Tổng Quan
|
||||
|
||||
**Mission Service** quản lý các nhiệm vụ (missions) và tác vụ (tasks) gamification, cho phép người dùng kiếm phần thưởng thông qua các hoạt động trên nền tảng GoodGo.
|
||||
@@ -178,6 +180,173 @@ flowchart LR
|
||||
|
||||
---
|
||||
|
||||
## Chi Tiết Từng Loại Mission
|
||||
|
||||
### 🎬 Xem Video
|
||||
|
||||
```mermaid
|
||||
%%{init: {'theme':'dark'}}%%
|
||||
sequenceDiagram
|
||||
participant U as 📱 User
|
||||
participant A as 🌐 Mission API
|
||||
participant V as 🎬 Video Player
|
||||
participant W as 💰 Wallet
|
||||
|
||||
U->>A: GET /missions?type=VIDEO
|
||||
A-->>U: Danh sách video missions
|
||||
U->>A: POST /tasks/start {missionId}
|
||||
A-->>U: {taskId, videoUrl, requiredDuration}
|
||||
|
||||
U->>V: Bắt đầu xem video
|
||||
V->>A: POST /tasks/{id}/progress {watchedSeconds}
|
||||
Note over V,A: Cập nhật định kỳ (mỗi 5s)
|
||||
|
||||
V->>A: POST /tasks/{id}/complete {totalWatched}
|
||||
A->>A: Xác thực thời lượng xem ≥ yêu cầu
|
||||
A-->>U: Task hoàn thành!
|
||||
|
||||
U->>A: POST /tasks/{id}/claim
|
||||
A->>W: GrantPoints(userId, amount)
|
||||
A-->>U: Đã nhận thưởng!
|
||||
```
|
||||
|
||||
| Tham Số | Giá Trị | Mô Tả |
|
||||
|---------|---------|-------|
|
||||
| `minWatchPercent` | 80% | Tỷ lệ xem tối thiểu |
|
||||
| `skipDetection` | true | Phát hiện tua nhanh |
|
||||
| `antiBot` | true | CAPTCHA cho hoạt động đáng ngờ |
|
||||
| `reward` | 5-20 MP | Tùy độ dài video |
|
||||
|
||||
---
|
||||
|
||||
### 💰 Pay Per Click
|
||||
|
||||
```mermaid
|
||||
%%{init: {'theme':'dark'}}%%
|
||||
flowchart LR
|
||||
U[📱 User] --> M[📋 Xem Mission]
|
||||
M --> C{🖱️ Nhấn Link/Ads}
|
||||
C --> V[✅ Xác Thực Click]
|
||||
V --> R[🎁 Nhận Thưởng]
|
||||
|
||||
style R fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px
|
||||
```
|
||||
|
||||
| Hành Động | Điểm | Giới Hạn |
|
||||
|-----------|------|----------|
|
||||
| Xem quảng cáo | 1 MP | 20/ngày |
|
||||
| Nhấn link ngoài | 2 MP | 10/ngày |
|
||||
| Cài đặt app (CPI) | 50 MP | 2/ngày |
|
||||
|
||||
**Quy Tắc Chống Gian Lận:**
|
||||
- Rate Limit: 10 clicks/ngày
|
||||
- Cooldown: 30 giây giữa các lần
|
||||
- Chỉ URL unique
|
||||
|
||||
---
|
||||
|
||||
### 📤 Pay Per Upload (UGC)
|
||||
|
||||
```mermaid
|
||||
%%{init: {'theme':'dark'}}%%
|
||||
flowchart TD
|
||||
U[📱 Upload Nội Dung] --> T{Loại Nội Dung}
|
||||
T -->|Ảnh| P[Xem Xét Ảnh]
|
||||
T -->|Video| V[Xem Xét Video]
|
||||
T -->|Review| R[Xem Xét Text]
|
||||
|
||||
P --> AI[🤖 AI Moderation]
|
||||
V --> AI
|
||||
R --> AI
|
||||
|
||||
AI -->|Pass| A[✅ Duyệt]
|
||||
AI -->|Flag| M[👨💻 Xem Xét Thủ Công]
|
||||
M --> A
|
||||
M --> X[❌ Từ Chối]
|
||||
|
||||
A --> RW[🎁 Cấp Thưởng]
|
||||
|
||||
style A fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px
|
||||
style X fill:#C0392B,color:#ECF0F1,stroke:#A93226,stroke-width:2px
|
||||
```
|
||||
|
||||
| Loại Nội Dung | Yêu Cầu | Review | Thưởng |
|
||||
|---------------|---------|--------|--------|
|
||||
| Ảnh | 720p, <5MB | AI + Manual | 5-20 MP |
|
||||
| Video | 480p, 15-60s | AI + Manual | 20-100 MP |
|
||||
| Text Review | 50-500 ký tự | AI | 3-10 MP |
|
||||
|
||||
---
|
||||
|
||||
### 👥 Mời Bạn Bè
|
||||
|
||||
```mermaid
|
||||
%%{init: {'theme':'dark'}}%%
|
||||
flowchart LR
|
||||
subgraph Invite["👥 Mời Bạn Bè"]
|
||||
A[Chia Sẻ Code] --> B[Bạn Đăng Ký]
|
||||
B --> C[Bạn Hoàn Thành Mission]
|
||||
C --> D[✅ Cả Hai Nhận Thưởng]
|
||||
end
|
||||
|
||||
style D fill:#27AE60,color:#ECF0F1,stroke:#229954,stroke-width:2px
|
||||
```
|
||||
|
||||
| Số Bạn Mời | Thưởng/Bạn | Bonus |
|
||||
|------------|------------|-------|
|
||||
| 1-5 người | 10 MP | - |
|
||||
| 6-20 người | 15 MP | +50% |
|
||||
| 21+ người | 20 MP | +100% |
|
||||
|
||||
**Tích hợp Mining Service:**
|
||||
- Đồng bộ với `ReferralAggregate`
|
||||
- Bonus stacking: Mission reward + Mining rate boost
|
||||
|
||||
---
|
||||
|
||||
### ❤️ Tương Tác Xã Hội
|
||||
|
||||
```mermaid
|
||||
%%{init: {'theme':'dark'}}%%
|
||||
flowchart TD
|
||||
subgraph Actions["Hành Động"]
|
||||
L[❤️ Like Post]
|
||||
S[🔄 Share Content]
|
||||
F[➕ Follow/Subscribe]
|
||||
C[💬 Comment]
|
||||
end
|
||||
|
||||
subgraph Platforms["Nền Tảng Hỗ Trợ"]
|
||||
FB[Facebook]
|
||||
TW[Twitter/X]
|
||||
IG[Instagram]
|
||||
YT[YouTube]
|
||||
TK[TikTok]
|
||||
end
|
||||
|
||||
Actions --> V{Xác Thực}
|
||||
V -->|OAuth| API[Platform API]
|
||||
V -->|Screenshot| AI[AI Verify]
|
||||
V -->|URL Check| SC[Scraper]
|
||||
|
||||
style V fill:#E67E22,color:#ECF0F1,stroke:#D35400,stroke-width:2px
|
||||
```
|
||||
|
||||
| Phương Thức Xác Thực | Độ Chính Xác | Tốc Độ | Chi Phí |
|
||||
|---------------------|--------------|--------|---------|
|
||||
| OAuth API | Cao | Nhanh | Trung bình |
|
||||
| Screenshot AI | Trung bình | TB | Thấp |
|
||||
| URL Scraping | Thấp | Nhanh | Miễn phí |
|
||||
|
||||
| Hành Động | Điểm | Giới Hạn |
|
||||
|-----------|------|----------|
|
||||
| Like | 1 MP | 10/ngày |
|
||||
| Share | 3 MP | 5/ngày |
|
||||
| Follow/Subscribe | 5 MP | 3/ngày |
|
||||
| Comment | 2 MP | 10/ngày |
|
||||
|
||||
---
|
||||
|
||||
## Hệ Thống Điểm Danh Hàng Ngày
|
||||
|
||||
### 🔥 Thưởng Streak
|
||||
@@ -365,13 +534,99 @@ flowchart LR
|
||||
|
||||
### Biến Môi Trường
|
||||
|
||||
| Biến | Mô Tả | Bắt Buộc |
|
||||
|------|-------|----------|
|
||||
| `DATABASE_URL` | Kết nối PostgreSQL | Có |
|
||||
| `REDIS_URL` | Kết nối Redis | Có |
|
||||
| `RABBITMQ_URL` | Kết nối RabbitMQ | Có |
|
||||
| `JWT_AUTHORITY` | URL phát hành JWT | Có |
|
||||
| `MAX_DAILY_TASKS` | Số task tối đa/ngày | Không |
|
||||
| Biến | Mô Tả | Bắt Buộc | Mặc Định |
|
||||
|------|-------|----------|----------|
|
||||
| `DATABASE_URL` | Kết nối PostgreSQL | Có | - |
|
||||
| `REDIS_URL` | Kết nối Redis | Có | - |
|
||||
| `RABBITMQ_URL` | Kết nối RabbitMQ | Có | - |
|
||||
| `JWT_AUTHORITY` | URL phát hành JWT | Có | - |
|
||||
| `MAX_DAILY_TASKS` | Số task tối đa/ngày | Không | 50 |
|
||||
| `CHECKIN_STREAK_ENABLED` | Bật thưởng streak | Không | true |
|
||||
| `CONTENT_MODERATION_API` | API AI moderation | Không | - |
|
||||
|
||||
---
|
||||
|
||||
## Admin Configuration Entity
|
||||
|
||||
### MissionConfiguration Aggregate
|
||||
|
||||
```mermaid
|
||||
%%{init: {'theme':'dark'}}%%
|
||||
classDiagram
|
||||
class MissionConfiguration {
|
||||
+Guid Id
|
||||
+bool IsGloballyEnabled
|
||||
+int MaxDailyTasks
|
||||
+int TaskCooldownSeconds
|
||||
+DateTime UpdatedAt
|
||||
+Guid UpdatedBy
|
||||
}
|
||||
|
||||
class CheckInConfiguration {
|
||||
+Guid Id
|
||||
+List~StreakTier~ Tiers
|
||||
+int BasePoints
|
||||
+bool StreakEnabled
|
||||
}
|
||||
|
||||
class StreakTier {
|
||||
+int MinDays
|
||||
+int MaxDays
|
||||
+int DailyPoints
|
||||
+int MilestoneBonus
|
||||
+string BadgeName
|
||||
}
|
||||
|
||||
class RewardConfiguration {
|
||||
+Guid Id
|
||||
+decimal VideoRewardMin
|
||||
+decimal VideoRewardMax
|
||||
+decimal ClickReward
|
||||
+decimal UploadRewardMin
|
||||
+decimal UploadRewardMax
|
||||
+decimal InviteReward
|
||||
+decimal SocialReward
|
||||
}
|
||||
|
||||
MissionConfiguration --> CheckInConfiguration
|
||||
MissionConfiguration --> RewardConfiguration
|
||||
CheckInConfiguration --> "*" StreakTier
|
||||
```
|
||||
|
||||
### Giá Trị Cấu Hình Mặc Định
|
||||
|
||||
| Danh Mục | Tham Số | Mặc Định | Mô Tả |
|
||||
|----------|---------|----------|-------|
|
||||
| **Hệ Thống** | Bật Toàn Cục | true | Công tắc khẩn cấp |
|
||||
| **Hệ Thống** | Task Tối Đa/Ngày | 50 | Giới hạn tasks |
|
||||
| **Hệ Thống** | Cooldown Tasks | 60 giây | Thời gian chờ |
|
||||
| **Video** | Thưởng Min | 5 MP | Video ngắn |
|
||||
| **Video** | Thưởng Max | 20 MP | Video dài |
|
||||
| **Video** | Tỷ Lệ Xem Min | 80% | Phần trăm xem tối thiểu |
|
||||
| **Click** | Thưởng/Click | 1-2 MP | Tùy loại link |
|
||||
| **Click** | Giới Hạn/Ngày | 20 | Số clicks tối đa |
|
||||
| **Upload** | Thưởng Ảnh | 5-20 MP | Tùy chất lượng |
|
||||
| **Upload** | Thưởng Video | 20-100 MP | Tùy độ dài |
|
||||
| **Invite** | Thưởng/Bạn | 10-20 MP | Tùy số lượng |
|
||||
| **Social** | Like/Share | 1-3 MP | Tùy hành động |
|
||||
| **Check-in** | Điểm Ngày 1-6 | 2 MP | Điểm cơ bản |
|
||||
| **Check-in** | Bonus Ngày 7 | 20 MP | Mốc tuần đầu |
|
||||
| **Check-in** | Bonus Ngày 30 | 100 MP | Mốc tháng |
|
||||
|
||||
### Nhật Ký Cấu Hình (Audit Log)
|
||||
|
||||
```csharp
|
||||
public record ConfigAuditLog(
|
||||
Guid Id,
|
||||
Guid AdminUserId,
|
||||
string ConfigType, // "Mission" | "CheckIn" | "Reward"
|
||||
string PreviousValue, // JSON cấu hình cũ
|
||||
string NewValue, // JSON cấu hình mới
|
||||
string Reason, // Lý do admin thay đổi
|
||||
DateTime CreatedAt,
|
||||
string IpAddress
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -74,6 +74,32 @@ try
|
||||
Version = "v1",
|
||||
Description = "SocialService microservice API / API microservice SocialService"
|
||||
});
|
||||
|
||||
// EN: Add JWT Bearer authentication / VI: Thêm xác thực JWT Bearer
|
||||
options.AddSecurityDefinition("Bearer", new Microsoft.OpenApi.Models.OpenApiSecurityScheme
|
||||
{
|
||||
Description = "JWT Authorization header using the Bearer scheme. Example: 'Bearer {token}'",
|
||||
Name = "Authorization",
|
||||
In = Microsoft.OpenApi.Models.ParameterLocation.Header,
|
||||
Type = Microsoft.OpenApi.Models.SecuritySchemeType.Http,
|
||||
Scheme = "bearer",
|
||||
BearerFormat = "JWT"
|
||||
});
|
||||
|
||||
options.AddSecurityRequirement(new Microsoft.OpenApi.Models.OpenApiSecurityRequirement
|
||||
{
|
||||
{
|
||||
new Microsoft.OpenApi.Models.OpenApiSecurityScheme
|
||||
{
|
||||
Reference = new Microsoft.OpenApi.Models.OpenApiReference
|
||||
{
|
||||
Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme,
|
||||
Id = "Bearer"
|
||||
}
|
||||
},
|
||||
Array.Empty<string>()
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// EN: Add health checks / VI: Thêm health checks
|
||||
|
||||
Reference in New Issue
Block a user