feat: V2 microservices stack — backend services, gateway, JWT auth
Add full V2 architecture: identity, content, studio (.NET 10) and file, render, notification, gateway (Go) services with vendored deps, plus DB migrations, event/API contracts, and an init-db script. Wire the Next.js frontend to the gateway: server-side JWT auth routes (login/register/refresh/logout/me), gateway fetch helper, and session/ cookie/jwt helpers under src/lib. Containerize the stack via docker-compose.v2.yml and per-service Dockerfiles. Base images resolve through a Nexus mirror (Docker Hub) and MCR directly; npm/NuGet pull from Nexus groups. Self-host fonts via next/font/local to avoid Google Fonts (geo-blocked). Add CI workflow and ignore .env.v2, *.stackdump, and .NET bin/obj. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
using FlatRender.IdentitySvc.Application.Services.Interfaces;
|
||||
using FlatRender.IdentitySvc.Domain.Entities;
|
||||
using FlatRender.IdentitySvc.Domain.Enums;
|
||||
using FlatRender.IdentitySvc.Infrastructure.Data;
|
||||
using FlatRender.IdentitySvc.Models.Responses;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace FlatRender.IdentitySvc.Application.Services;
|
||||
|
||||
public class GamificationService(IdentityDbContext db) : IGamificationService
|
||||
{
|
||||
public async Task<List<QuestResponse>> GetActiveQuestsAsync(Guid userId, Guid tenantId)
|
||||
{
|
||||
var quests = await db.Quests
|
||||
.Where(q => q.IsActive &&
|
||||
(q.TenantId == null || q.TenantId == tenantId) &&
|
||||
(q.StartsAt == null || q.StartsAt <= DateTime.UtcNow) &&
|
||||
(q.ExpiresAt == null || q.ExpiresAt > DateTime.UtcNow))
|
||||
.OrderBy(q => q.OrderValue)
|
||||
.ToListAsync();
|
||||
|
||||
var progressMap = await db.UserQuestProgresses
|
||||
.Where(p => p.UserId == userId && quests.Select(q => q.Id).Contains(p.QuestId))
|
||||
.ToDictionaryAsync(p => p.QuestId, p => p);
|
||||
|
||||
return quests.Select(q =>
|
||||
{
|
||||
progressMap.TryGetValue(q.Id, out var progress);
|
||||
return new QuestResponse(
|
||||
q.Id, q.Title, q.Challenge, q.Why, q.Hint, q.Icon,
|
||||
q.QuestType.ToString(), q.TargetCount,
|
||||
progress?.CurrentCount ?? 0,
|
||||
progress?.IsCompleted ?? false,
|
||||
progress?.PrizeClaimed ?? false,
|
||||
q.PrizeType.ToString(), q.PrizeAmount, q.ExpiresAt
|
||||
);
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public async Task ClaimQuestPrizeAsync(Guid userId, Guid questId)
|
||||
{
|
||||
var progress = await db.UserQuestProgresses
|
||||
.FirstOrDefaultAsync(p => p.UserId == userId && p.QuestId == questId && p.IsCompleted && !p.PrizeClaimed)
|
||||
?? throw new InvalidOperationException("Quest not completed or prize already claimed");
|
||||
|
||||
var quest = await db.Quests.FindAsync(questId)
|
||||
?? throw new KeyNotFoundException("Quest not found");
|
||||
|
||||
await ApplyPrizeAsync(userId, quest.PrizeType, quest.PrizeAmount);
|
||||
|
||||
progress.PrizeClaimed = true;
|
||||
progress.PrizeClaimedAt = DateTime.UtcNow;
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<List<EarnedGiftResponse>> GetEarnedGiftsAsync(Guid userId)
|
||||
{
|
||||
var gifts = await db.EarnedGifts
|
||||
.Include(eg => eg.Gift)
|
||||
.Where(eg => eg.UserId == userId && !eg.IsUsed && (eg.ExpiresAt == null || eg.ExpiresAt > DateTime.UtcNow))
|
||||
.OrderByDescending(eg => eg.EarnedAt)
|
||||
.ToListAsync();
|
||||
|
||||
return gifts.Select(eg => new EarnedGiftResponse(
|
||||
eg.Id, eg.GiftId, eg.Gift.Name, eg.Gift.Description,
|
||||
eg.Gift.PrizeType.ToString(), eg.Gift.Value, eg.Gift.Unit,
|
||||
eg.EarnedAt, eg.ExpiresAt, eg.IsUsed
|
||||
)).ToList();
|
||||
}
|
||||
|
||||
public async Task UseEarnedGiftAsync(Guid userId, Guid earnedGiftId)
|
||||
{
|
||||
var earned = await db.EarnedGifts
|
||||
.Include(eg => eg.Gift)
|
||||
.FirstOrDefaultAsync(eg => eg.Id == earnedGiftId && eg.UserId == userId && !eg.IsUsed &&
|
||||
(eg.ExpiresAt == null || eg.ExpiresAt > DateTime.UtcNow))
|
||||
?? throw new InvalidOperationException("Earned gift not found or already used");
|
||||
|
||||
await ApplyPrizeAsync(userId, earned.Gift.PrizeType, earned.Gift.Value);
|
||||
|
||||
earned.IsUsed = true;
|
||||
earned.UsedAt = DateTime.UtcNow;
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task IncrementQuestProgressAsync(Guid userId, Guid tenantId, string targetEvent)
|
||||
{
|
||||
var matchingQuests = await db.Quests
|
||||
.Where(q => q.IsActive && q.TargetEvent == targetEvent &&
|
||||
(q.TenantId == null || q.TenantId == tenantId) &&
|
||||
(q.ExpiresAt == null || q.ExpiresAt > DateTime.UtcNow))
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var quest in matchingQuests)
|
||||
{
|
||||
var today = DateOnly.FromDateTime(DateTime.UtcNow);
|
||||
DateOnly? periodStart = quest.QuestType switch
|
||||
{
|
||||
QuestType.Daily => today,
|
||||
QuestType.Weekly => today.AddDays(-(int)DateTime.UtcNow.DayOfWeek),
|
||||
_ => null
|
||||
};
|
||||
|
||||
var progress = await db.UserQuestProgresses
|
||||
.FirstOrDefaultAsync(p => p.UserId == userId && p.QuestId == quest.Id && p.PeriodStart == periodStart);
|
||||
|
||||
if (progress == null)
|
||||
{
|
||||
progress = new UserQuestProgress
|
||||
{
|
||||
UserId = userId,
|
||||
QuestId = quest.Id,
|
||||
PeriodStart = periodStart,
|
||||
};
|
||||
db.UserQuestProgresses.Add(progress);
|
||||
}
|
||||
|
||||
if (progress.IsCompleted) continue;
|
||||
|
||||
progress.CurrentCount++;
|
||||
if (progress.CurrentCount >= quest.TargetCount)
|
||||
{
|
||||
progress.IsCompleted = true;
|
||||
progress.CompletedAt = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private async Task ApplyPrizeAsync(Guid userId, PrizeType prizeType, long amount)
|
||||
{
|
||||
var user = await db.Users.FindAsync(userId) ?? throw new KeyNotFoundException("User not found");
|
||||
|
||||
switch (prizeType)
|
||||
{
|
||||
case PrizeType.Balance:
|
||||
user.BalanceMinor += amount;
|
||||
break;
|
||||
case PrizeType.RenderSeconds:
|
||||
user.UserDailyFreeChargeSec += (int)amount;
|
||||
break;
|
||||
case PrizeType.LoyaltyPoints:
|
||||
user.LoyaltyScore += (int)amount;
|
||||
break;
|
||||
// StorageGB, Plan, Discount require more complex handling (not inline)
|
||||
}
|
||||
|
||||
user.UpdatedAt = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user