Files
flatrender/services/identity/FlatRender.IdentitySvc/Application/Services/GamificationService.cs
T
soroush.asadi 90ac0b81d1 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>
2026-05-29 23:29:31 +03:30

152 lines
5.8 KiB
C#

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