feat: Implement JWT authentication, SignalR, external service clients in MiningService, and update documentation across services.

This commit is contained in:
Ho Ngoc Hai
2026-01-17 18:08:46 +07:00
parent e285f3d3c6
commit c6bcc8d0df
18 changed files with 1477 additions and 16 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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" />

View File

@@ -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 { }

View File

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

View File

@@ -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.

View File

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

View File

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

View File

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