something

This commit is contained in:
2025-10-01 01:56:05 +03:00
parent ece9cedb37
commit 40342a0e14
112 changed files with 5468 additions and 5468 deletions

View File

@@ -1,41 +1,41 @@
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Services.Models;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Services;
/// <summary>
/// Provides aggregated analytics metrics for dashboards.
/// </summary>
public class AnalyticsService : IAnalyticsService
{
private readonly IUnitOfWork _uow;
public AnalyticsService(IUnitOfWork uow) => _uow = uow;
public async Task<AnalyticsSummary> GetSummaryAsync(CancellationToken ct = default)
{
try
{
var totalUsers = await _uow.Users.Query().CountAsync(ct);
var totalMissions = await _uow.Missions.Query().CountAsync(ct);
var totalStoreItems = await _uow.StoreItems.Query().CountAsync(ct);
var totalExperience = await _uow.Players.Query().SumAsync(p => (long)p.Experience, ct);
var completedMissions = await _uow.PlayerMissions.Query(pm => pm.Completed != null).CountAsync(ct);
return new AnalyticsSummary
{
TotalUsers = totalUsers,
TotalMissions = totalMissions,
TotalStoreItems = totalStoreItems,
TotalExperience = totalExperience,
CompletedMissions = completedMissions
};
}
catch (Exception ex)
{
Log.Error(ex, "Failed to build analytics summary");
throw;
}
}
}
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Services.Models;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Services;
/// <summary>
/// Provides aggregated analytics metrics for dashboards.
/// </summary>
public class AnalyticsService : IAnalyticsService
{
private readonly IUnitOfWork _uow;
public AnalyticsService(IUnitOfWork uow) => _uow = uow;
public async Task<AnalyticsSummary> GetSummaryAsync(CancellationToken ct = default)
{
try
{
var totalUsers = await _uow.Users.Query().CountAsync(ct);
var totalMissions = await _uow.Missions.Query().CountAsync(ct);
var totalStoreItems = await _uow.StoreItems.Query().CountAsync(ct);
var totalExperience = await _uow.Players.Query().SumAsync(p => (long)p.Experience, ct);
var completedMissions = await _uow.PlayerMissions.Query(pm => pm.Completed != null).CountAsync(ct);
return new AnalyticsSummary
{
TotalUsers = totalUsers,
TotalMissions = totalMissions,
TotalStoreItems = totalStoreItems,
TotalExperience = totalExperience,
CompletedMissions = completedMissions
};
}
catch (Exception ex)
{
Log.Error(ex, "Failed to build analytics summary");
throw;
}
}
}

View File

@@ -1,8 +1,8 @@
using LctMonolith.Services.Models;
namespace LctMonolith.Services;
public interface IAnalyticsService
{
Task<AnalyticsSummary> GetSummaryAsync(CancellationToken ct = default);
}
using LctMonolith.Services.Models;
namespace LctMonolith.Services;
public interface IAnalyticsService
{
Task<AnalyticsSummary> GetSummaryAsync(CancellationToken ct = default);
}

View File

@@ -1,12 +1,12 @@
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Interfaces;
public interface IDialogueService
{
Task<Dialogue?> GetDialogueByMissionIdAsync(Guid missionId);
Task<Dialogue> CreateDialogueAsync(Dialogue dialogue);
Task<DialogueMessage?> GetDialogueMessageByIdAsync(Guid messageId);
Task<IEnumerable<DialogueMessageResponseOption>> GetResponseOptionsAsync(Guid messageId);
Task<DialogueMessage?> ProcessDialogueResponseAsync(Guid messageId, Guid responseOptionId, Guid playerId);
}
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Interfaces;
public interface IDialogueService
{
Task<Dialogue?> GetDialogueByMissionIdAsync(Guid missionId);
Task<Dialogue> CreateDialogueAsync(Dialogue dialogue);
Task<DialogueMessage?> GetDialogueMessageByIdAsync(Guid messageId);
Task<IEnumerable<DialogueMessageResponseOption>> GetResponseOptionsAsync(Guid messageId);
Task<DialogueMessage?> ProcessDialogueResponseAsync(Guid messageId, Guid responseOptionId, Guid playerId);
}

View File

@@ -1,8 +1,8 @@
namespace LctMonolith.Services.Interfaces;
public interface IFileStorageService
{
Task<string> UploadAsync(Stream content, string contentType, string keyPrefix, CancellationToken ct = default);
Task DeleteAsync(string key, CancellationToken ct = default);
Task<string> GetPresignedUrlAsync(string key, TimeSpan? expires = null, CancellationToken ct = default);
}
namespace LctMonolith.Services.Interfaces;
public interface IFileStorageService
{
Task<string> UploadAsync(Stream content, string contentType, string keyPrefix, CancellationToken ct = default);
Task DeleteAsync(string key, CancellationToken ct = default);
Task<string> GetPresignedUrlAsync(string key, TimeSpan? expires = null, CancellationToken ct = default);
}

View File

@@ -1,8 +1,8 @@
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Contracts;
public interface IInventoryService
{
Task<IEnumerable<UserInventoryItem>> GetStoreInventoryAsync(Guid userId, CancellationToken ct = default);
}
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Contracts;
public interface IInventoryService
{
Task<IEnumerable<UserInventoryItem>> GetStoreInventoryAsync(Guid userId, CancellationToken ct = default);
}

View File

@@ -1,14 +1,14 @@
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Interfaces;
public interface IMissionCategoryService
{
// CRUD should be enough
Task<MissionCategory?> GetCategoryByIdAsync(Guid categoryId);
Task<MissionCategory?> GetCategoryByTitleAsync(string title);
Task<IEnumerable<MissionCategory>> GetAllCategoriesAsync();
Task<MissionCategory> CreateCategoryAsync(MissionCategory category);
Task<MissionCategory> UpdateCategoryAsync(MissionCategory category);
Task<bool> DeleteCategoryAsync(Guid categoryId);
}
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Interfaces;
public interface IMissionCategoryService
{
// CRUD should be enough
Task<MissionCategory?> GetCategoryByIdAsync(Guid categoryId);
Task<MissionCategory?> GetCategoryByTitleAsync(string title);
Task<IEnumerable<MissionCategory>> GetAllCategoriesAsync();
Task<MissionCategory> CreateCategoryAsync(MissionCategory category);
Task<MissionCategory> UpdateCategoryAsync(MissionCategory category);
Task<bool> DeleteCategoryAsync(Guid categoryId);
}

View File

@@ -1,17 +1,17 @@
using LctMonolith.Models.Database;
using LctMonolith.Models.DTO;
namespace LctMonolith.Services.Interfaces;
public interface IMissionService
{
Task<Mission?> GetMissionByIdAsync(Guid missionId);
Task<IEnumerable<Mission>> GetMissionsByCategoryAsync(Guid categoryId);
Task<IEnumerable<Mission>> GetAvailableMissionsForPlayerAsync(Guid playerId);
Task<IEnumerable<Mission>> GetChildMissionsAsync(Guid parentMissionId);
Task<Mission> CreateMissionAsync(Mission mission);
Task<Mission> UpdateMissionAsync(Mission mission);
Task<bool> DeleteMissionAsync(Guid missionId);
Task<bool> IsMissionAvailableForPlayerAsync(Guid missionId, Guid playerId);
Task<MissionCompletionResult> CompleteMissionAsync(Guid missionId, Guid playerId, object? missionProof = null);
}
using LctMonolith.Models.Database;
using LctMonolith.Models.DTO;
namespace LctMonolith.Services.Interfaces;
public interface IMissionService
{
Task<Mission?> GetMissionByIdAsync(Guid missionId);
Task<IEnumerable<Mission>> GetMissionsByCategoryAsync(Guid categoryId);
Task<IEnumerable<Mission>> GetAvailableMissionsForPlayerAsync(Guid playerId);
Task<IEnumerable<Mission>> GetChildMissionsAsync(Guid parentMissionId);
Task<Mission> CreateMissionAsync(Mission mission);
Task<Mission> UpdateMissionAsync(Mission mission);
Task<bool> DeleteMissionAsync(Guid missionId);
Task<bool> IsMissionAvailableForPlayerAsync(Guid missionId, Guid playerId);
Task<MissionCompletionResult> CompleteMissionAsync(Guid missionId, Guid playerId, object? missionProof = null);
}

View File

@@ -1,12 +1,12 @@
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Contracts;
public interface INotificationService
{
Task<Notification> CreateAsync(Guid userId, string type, string title, string message, CancellationToken ct = default);
Task<IEnumerable<Notification>> GetUnreadAsync(Guid userId, CancellationToken ct = default);
Task<IEnumerable<Notification>> GetAllAsync(Guid userId, int take = 100, CancellationToken ct = default);
Task MarkReadAsync(Guid userId, Guid notificationId, CancellationToken ct = default);
Task<int> MarkAllReadAsync(Guid userId, CancellationToken ct = default);
}
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Contracts;
public interface INotificationService
{
Task<Notification> CreateAsync(Guid userId, string type, string title, string message, CancellationToken ct = default);
Task<IEnumerable<Notification>> GetUnreadAsync(Guid userId, CancellationToken ct = default);
Task<IEnumerable<Notification>> GetAllAsync(Guid userId, int take = 100, CancellationToken ct = default);
Task MarkReadAsync(Guid userId, Guid notificationId, CancellationToken ct = default);
Task<int> MarkAllReadAsync(Guid userId, CancellationToken ct = default);
}

View File

@@ -1,14 +1,14 @@
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Interfaces;
public interface IPlayerService
{
Task<Player?> GetPlayerByUserIdAsync(string userId);
Task<Player> CreatePlayerAsync(string userId, string username);
Task<Player> UpdatePlayerRankAsync(Guid playerId, Guid newRankId);
Task<Player> AddPlayerExperienceAsync(Guid playerId, int experience);
Task<Player> AddPlayerManaAsync(Guid playerId, int mana);
Task<IEnumerable<Player>> GetTopPlayersAsync(int topCount, TimeSpan timeFrame);
Task<Player> GetPlayerWithProgressAsync(Guid playerId);
}
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Interfaces;
public interface IPlayerService
{
Task<Player?> GetPlayerByUserIdAsync(string userId);
Task<Player> CreatePlayerAsync(string userId, string username);
Task<Player> UpdatePlayerRankAsync(Guid playerId, Guid newRankId);
Task<Player> AddPlayerExperienceAsync(Guid playerId, int experience);
Task<Player> AddPlayerManaAsync(Guid playerId, int mana);
Task<IEnumerable<Player>> GetTopPlayersAsync(int topCount, TimeSpan timeFrame);
Task<Player> GetPlayerWithProgressAsync(Guid playerId);
}

View File

@@ -1,11 +1,11 @@
namespace LctMonolith.Services.Interfaces;
using LctMonolith.Models.Database;
public interface IProfileService
{
Task<Profile?> GetByUserIdAsync(Guid userId, CancellationToken ct = default);
Task<Profile> UpsertAsync(Guid userId, string? firstName, string? lastName, DateOnly? birthDate, string? about, string? location, CancellationToken ct = default);
Task<Profile> UpdateAvatarAsync(Guid userId, Stream fileStream, string contentType, string? fileName, CancellationToken ct = default);
Task<bool> DeleteAvatarAsync(Guid userId, CancellationToken ct = default);
}
namespace LctMonolith.Services.Interfaces;
using LctMonolith.Models.Database;
public interface IProfileService
{
Task<Profile?> GetByUserIdAsync(Guid userId, CancellationToken ct = default);
Task<Profile> UpsertAsync(Guid userId, string? firstName, string? lastName, DateOnly? birthDate, string? about, string? location, CancellationToken ct = default);
Task<Profile> UpdateAvatarAsync(Guid userId, Stream fileStream, string contentType, string? fileName, CancellationToken ct = default);
Task<bool> DeleteAvatarAsync(Guid userId, CancellationToken ct = default);
}

View File

@@ -1,14 +1,14 @@
using LctMonolith.Models.Database;
using LctMonolith.Models.DTO;
namespace LctMonolith.Services.Interfaces;
public interface IProgressTrackingService
{
Task<PlayerMission> StartMissionAsync(Guid missionId, Guid playerId);
Task<PlayerMission> UpdateMissionProgressAsync(Guid playerMissionId, int progressPercentage, object? proof = null);
Task<PlayerMission> CompleteMissionAsync(Guid playerMissionId, object? proof = null);
Task<IEnumerable<PlayerMission>> GetPlayerMissionsAsync(Guid playerId);
Task<PlayerMission?> GetPlayerMissionAsync(Guid playerId, Guid missionId);
Task<PlayerProgress> GetPlayerOverallProgressAsync(Guid playerId);
}
using LctMonolith.Models.Database;
using LctMonolith.Models.DTO;
namespace LctMonolith.Services.Interfaces;
public interface IProgressTrackingService
{
Task<PlayerMission> StartMissionAsync(Guid missionId, Guid playerId);
Task<PlayerMission> UpdateMissionProgressAsync(Guid playerMissionId, int progressPercentage, object? proof = null);
Task<PlayerMission> CompleteMissionAsync(Guid playerMissionId, object? proof = null);
Task<IEnumerable<PlayerMission>> GetPlayerMissionsAsync(Guid playerId);
Task<PlayerMission?> GetPlayerMissionAsync(Guid playerId, Guid missionId);
Task<PlayerProgress> GetPlayerOverallProgressAsync(Guid playerId);
}

View File

@@ -1,15 +1,15 @@
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Interfaces;
public interface IRankService
{
Task<Rank?> GetRankByIdAsync(Guid rankId);
Task<Rank?> GetRankByTitleAsync(string title);
Task<IEnumerable<Rank>> GetAllRanksAsync();
Task<Rank> CreateRankAsync(Rank rank);
Task<Rank> UpdateRankAsync(Rank rank);
Task<bool> DeleteRankAsync(Guid rankId);
Task<bool> CanPlayerAdvanceToRankAsync(Guid playerId, Guid rankId);
Task<Rank?> GetNextRankAsync(Guid currentRankId);
}
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Interfaces;
public interface IRankService
{
Task<Rank?> GetRankByIdAsync(Guid rankId);
Task<Rank?> GetRankByTitleAsync(string title);
Task<IEnumerable<Rank>> GetAllRanksAsync();
Task<Rank> CreateRankAsync(Rank rank);
Task<Rank> UpdateRankAsync(Rank rank);
Task<bool> DeleteRankAsync(Guid rankId);
Task<bool> CanPlayerAdvanceToRankAsync(Guid playerId, Guid rankId);
Task<Rank?> GetNextRankAsync(Guid currentRankId);
}

View File

@@ -1,11 +1,11 @@
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Interfaces;
public interface IRewardService
{
Task<IEnumerable<MissionSkillReward>> GetMissionSkillRewardsAsync(Guid missionId);
Task<IEnumerable<MissionItemReward>> GetMissionItemRewardsAsync(Guid missionId);
Task DistributeMissionRewardsAsync(Guid missionId, Guid playerId);
Task<bool> CanClaimRewardAsync(Guid rewardId, Guid playerId);
}
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Interfaces;
public interface IRewardService
{
Task<IEnumerable<MissionSkillReward>> GetMissionSkillRewardsAsync(Guid missionId);
Task<IEnumerable<MissionItemReward>> GetMissionItemRewardsAsync(Guid missionId);
Task DistributeMissionRewardsAsync(Guid missionId, Guid playerId);
Task<bool> CanClaimRewardAsync(Guid rewardId, Guid playerId);
}

View File

@@ -1,10 +1,10 @@
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Interfaces;
public interface IRuleValidationService
{
Task<bool> ValidateMissionRankRulesAsync(Guid missionId, Guid playerId);
Task<bool> ValidateRankAdvancementRulesAsync(Guid playerId, Guid targetRankId);
Task<IEnumerable<MissionRankRule>> GetApplicableRankRulesAsync(Guid missionId);
}
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Interfaces;
public interface IRuleValidationService
{
Task<bool> ValidateMissionRankRulesAsync(Guid missionId, Guid playerId);
Task<bool> ValidateRankAdvancementRulesAsync(Guid playerId, Guid targetRankId);
Task<IEnumerable<MissionRankRule>> GetApplicableRankRulesAsync(Guid missionId);
}

View File

@@ -1,16 +1,16 @@
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Interfaces;
public interface ISkillService
{
Task<Skill?> GetSkillByIdAsync(Guid skillId);
Task<Skill?> GetSkillByTitleAsync(string title);
Task<IEnumerable<Skill>> GetAllSkillsAsync();
Task<Skill> CreateSkillAsync(Skill skill);
Task<Skill> UpdateSkillAsync(Skill skill);
Task<bool> DeleteSkillAsync(Guid skillId);
Task<PlayerSkill> UpdatePlayerSkillAsync(Guid playerId, Guid skillId, int level);
Task<IEnumerable<PlayerSkill>> GetPlayerSkillsAsync(Guid playerId);
Task<int> GetPlayerSkillLevelAsync(Guid playerId, Guid skillId);
}
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Interfaces;
public interface ISkillService
{
Task<Skill?> GetSkillByIdAsync(Guid skillId);
Task<Skill?> GetSkillByTitleAsync(string title);
Task<IEnumerable<Skill>> GetAllSkillsAsync();
Task<Skill> CreateSkillAsync(Skill skill);
Task<Skill> UpdateSkillAsync(Skill skill);
Task<bool> DeleteSkillAsync(Guid skillId);
Task<PlayerSkill> UpdatePlayerSkillAsync(Guid playerId, Guid skillId, int level);
Task<IEnumerable<PlayerSkill>> GetPlayerSkillsAsync(Guid playerId);
Task<int> GetPlayerSkillLevelAsync(Guid playerId, Guid skillId);
}

View File

@@ -1,9 +1,9 @@
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Contracts;
public interface IStoreService
{
Task<IEnumerable<StoreItem>> GetActiveItemsAsync(CancellationToken ct = default);
Task<UserInventoryItem> PurchaseAsync(Guid userId, Guid itemId, int quantity, CancellationToken ct = default);
}
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Contracts;
public interface IStoreService
{
Task<IEnumerable<StoreItem>> GetActiveItemsAsync(CancellationToken ct = default);
Task<UserInventoryItem> PurchaseAsync(Guid userId, Guid itemId, int quantity, CancellationToken ct = default);
}

View File

@@ -1,11 +1,11 @@
using LctMonolith.Models.Database;
using LctMonolith.Services.Models;
namespace LctMonolith.Services.Contracts;
public interface ITokenService
{
Task<TokenPair> IssueAsync(AppUser user, CancellationToken ct = default);
Task<TokenPair> RefreshAsync(string refreshToken, CancellationToken ct = default);
Task RevokeAsync(string refreshToken, CancellationToken ct = default);
}
using LctMonolith.Models.Database;
using LctMonolith.Services.Models;
namespace LctMonolith.Services.Contracts;
public interface ITokenService
{
Task<TokenPair> IssueAsync(AppUser user, CancellationToken ct = default);
Task<TokenPair> RefreshAsync(string refreshToken, CancellationToken ct = default);
Task RevokeAsync(string refreshToken, CancellationToken ct = default);
}

View File

@@ -1,55 +1,55 @@
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Services.Interfaces;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Services;
public class DialogueService : IDialogueService
{
private readonly IUnitOfWork _uow;
public DialogueService(IUnitOfWork uow) => _uow = uow;
public async Task<Dialogue?> GetDialogueByMissionIdAsync(Guid missionId)
{
try { return await _uow.Dialogues.Query(d => d.MissionId == missionId).FirstOrDefaultAsync(); }
catch (Exception ex) { Log.Error(ex, "GetDialogueByMissionIdAsync failed {MissionId}", missionId); throw; }
}
public async Task<Dialogue> CreateDialogueAsync(Dialogue dialogue)
{
try
{
dialogue.Id = Guid.NewGuid();
await _uow.Dialogues.AddAsync(dialogue);
await _uow.SaveChangesAsync();
return dialogue;
}
catch (Exception ex) { Log.Error(ex, "CreateDialogueAsync failed {MissionId}", dialogue.MissionId); throw; }
}
public async Task<DialogueMessage?> GetDialogueMessageByIdAsync(Guid messageId)
{
try { return await _uow.DialogueMessages.GetByIdAsync(messageId); }
catch (Exception ex) { Log.Error(ex, "GetDialogueMessageByIdAsync failed {MessageId}", messageId); throw; }
}
public async Task<IEnumerable<DialogueMessageResponseOption>> GetResponseOptionsAsync(Guid messageId)
{
try { return await _uow.DialogueMessageResponseOptions.Query(o => o.ParentDialogueMessageId == messageId).ToListAsync(); }
catch (Exception ex) { Log.Error(ex, "GetResponseOptionsAsync failed {MessageId}", messageId); throw; }
}
public async Task<DialogueMessage?> ProcessDialogueResponseAsync(Guid messageId, Guid responseOptionId, Guid playerId)
{
try
{
var option = await _uow.DialogueMessageResponseOptions.Query(o => o.Id == responseOptionId && o.ParentDialogueMessageId == messageId).FirstOrDefaultAsync();
if (option == null) return null;
if (option.DestinationDialogueMessageId == null) return null; // end branch
return await _uow.DialogueMessages.GetByIdAsync(option.DestinationDialogueMessageId);
}
catch (Exception ex) { Log.Error(ex, "ProcessDialogueResponseAsync failed {MessageId} {ResponseId}", messageId, responseOptionId); throw; }
}
}
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Services.Interfaces;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Services;
public class DialogueService : IDialogueService
{
private readonly IUnitOfWork _uow;
public DialogueService(IUnitOfWork uow) => _uow = uow;
public async Task<Dialogue?> GetDialogueByMissionIdAsync(Guid missionId)
{
try { return await _uow.Dialogues.Query(d => d.MissionId == missionId).FirstOrDefaultAsync(); }
catch (Exception ex) { Log.Error(ex, "GetDialogueByMissionIdAsync failed {MissionId}", missionId); throw; }
}
public async Task<Dialogue> CreateDialogueAsync(Dialogue dialogue)
{
try
{
dialogue.Id = Guid.NewGuid();
await _uow.Dialogues.AddAsync(dialogue);
await _uow.SaveChangesAsync();
return dialogue;
}
catch (Exception ex) { Log.Error(ex, "CreateDialogueAsync failed {MissionId}", dialogue.MissionId); throw; }
}
public async Task<DialogueMessage?> GetDialogueMessageByIdAsync(Guid messageId)
{
try { return await _uow.DialogueMessages.GetByIdAsync(messageId); }
catch (Exception ex) { Log.Error(ex, "GetDialogueMessageByIdAsync failed {MessageId}", messageId); throw; }
}
public async Task<IEnumerable<DialogueMessageResponseOption>> GetResponseOptionsAsync(Guid messageId)
{
try { return await _uow.DialogueMessageResponseOptions.Query(o => o.ParentDialogueMessageId == messageId).ToListAsync(); }
catch (Exception ex) { Log.Error(ex, "GetResponseOptionsAsync failed {MessageId}", messageId); throw; }
}
public async Task<DialogueMessage?> ProcessDialogueResponseAsync(Guid messageId, Guid responseOptionId, Guid playerId)
{
try
{
var option = await _uow.DialogueMessageResponseOptions.Query(o => o.Id == responseOptionId && o.ParentDialogueMessageId == messageId).FirstOrDefaultAsync();
if (option == null) return null;
if (option.DestinationDialogueMessageId == null) return null; // end branch
return await _uow.DialogueMessages.GetByIdAsync(option.DestinationDialogueMessageId);
}
catch (Exception ex) { Log.Error(ex, "ProcessDialogueResponseAsync failed {MessageId} {ResponseId}", messageId, responseOptionId); throw; }
}
}

View File

@@ -1,26 +1,26 @@
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Services.Contracts;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Services;
public class InventoryService : IInventoryService
{
private readonly IUnitOfWork _uow;
public InventoryService(IUnitOfWork uow) => _uow = uow;
public async Task<IEnumerable<UserInventoryItem>> GetStoreInventoryAsync(Guid userId, CancellationToken ct = default)
{
try
{
return await _uow.UserInventoryItems.Query(i => i.UserId == userId, null, i => i.StoreItem).ToListAsync(ct);
}
catch (Exception ex)
{
Log.Error(ex, "GetStoreInventoryAsync failed {UserId}", userId);
throw;
}
}
}
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Services.Contracts;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Services;
public class InventoryService : IInventoryService
{
private readonly IUnitOfWork _uow;
public InventoryService(IUnitOfWork uow) => _uow = uow;
public async Task<IEnumerable<UserInventoryItem>> GetStoreInventoryAsync(Guid userId, CancellationToken ct = default)
{
try
{
return await _uow.UserInventoryItems.Query(i => i.UserId == userId, null, i => i.StoreItem).ToListAsync(ct);
}
catch (Exception ex)
{
Log.Error(ex, "GetStoreInventoryAsync failed {UserId}", userId);
throw;
}
}
}

View File

@@ -1,49 +1,49 @@
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Services.Interfaces;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Services;
public class MissionCategoryService : IMissionCategoryService
{
private readonly IUnitOfWork _uow;
public MissionCategoryService(IUnitOfWork uow) => _uow = uow;
public async Task<MissionCategory?> GetCategoryByIdAsync(Guid categoryId)
{
try { return await _uow.MissionCategories.GetByIdAsync(categoryId); }
catch (Exception ex) { Log.Error(ex, "GetCategoryByIdAsync failed {CategoryId}", categoryId); throw; }
}
public async Task<MissionCategory?> GetCategoryByTitleAsync(string title)
{
try { return await _uow.MissionCategories.Query(c => c.Title == title).FirstOrDefaultAsync(); }
catch (Exception ex) { Log.Error(ex, "GetCategoryByTitleAsync failed {Title}", title); throw; }
}
public async Task<IEnumerable<MissionCategory>> GetAllCategoriesAsync()
{
try { return await _uow.MissionCategories.Query().OrderBy(c => c.Title).ToListAsync(); }
catch (Exception ex) { Log.Error(ex, "GetAllCategoriesAsync failed"); throw; }
}
public async Task<MissionCategory> CreateCategoryAsync(MissionCategory category)
{
try { category.Id = Guid.NewGuid(); await _uow.MissionCategories.AddAsync(category); await _uow.SaveChangesAsync(); return category; }
catch (Exception ex) { Log.Error(ex, "CreateCategoryAsync failed {Title}", category.Title); throw; }
}
public async Task<MissionCategory> UpdateCategoryAsync(MissionCategory category)
{
try { _uow.MissionCategories.Update(category); await _uow.SaveChangesAsync(); return category; }
catch (Exception ex) { Log.Error(ex, "UpdateCategoryAsync failed {Id}", category.Id); throw; }
}
public async Task<bool> DeleteCategoryAsync(Guid categoryId)
{
try { var c = await _uow.MissionCategories.GetByIdAsync(categoryId); if (c == null) return false; _uow.MissionCategories.Remove(c); await _uow.SaveChangesAsync(); return true; }
catch (Exception ex) { Log.Error(ex, "DeleteCategoryAsync failed {CategoryId}", categoryId); throw; }
}
}
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Services.Interfaces;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Services;
public class MissionCategoryService : IMissionCategoryService
{
private readonly IUnitOfWork _uow;
public MissionCategoryService(IUnitOfWork uow) => _uow = uow;
public async Task<MissionCategory?> GetCategoryByIdAsync(Guid categoryId)
{
try { return await _uow.MissionCategories.GetByIdAsync(categoryId); }
catch (Exception ex) { Log.Error(ex, "GetCategoryByIdAsync failed {CategoryId}", categoryId); throw; }
}
public async Task<MissionCategory?> GetCategoryByTitleAsync(string title)
{
try { return await _uow.MissionCategories.Query(c => c.Title == title).FirstOrDefaultAsync(); }
catch (Exception ex) { Log.Error(ex, "GetCategoryByTitleAsync failed {Title}", title); throw; }
}
public async Task<IEnumerable<MissionCategory>> GetAllCategoriesAsync()
{
try { return await _uow.MissionCategories.Query().OrderBy(c => c.Title).ToListAsync(); }
catch (Exception ex) { Log.Error(ex, "GetAllCategoriesAsync failed"); throw; }
}
public async Task<MissionCategory> CreateCategoryAsync(MissionCategory category)
{
try { category.Id = Guid.NewGuid(); await _uow.MissionCategories.AddAsync(category); await _uow.SaveChangesAsync(); return category; }
catch (Exception ex) { Log.Error(ex, "CreateCategoryAsync failed {Title}", category.Title); throw; }
}
public async Task<MissionCategory> UpdateCategoryAsync(MissionCategory category)
{
try { _uow.MissionCategories.Update(category); await _uow.SaveChangesAsync(); return category; }
catch (Exception ex) { Log.Error(ex, "UpdateCategoryAsync failed {Id}", category.Id); throw; }
}
public async Task<bool> DeleteCategoryAsync(Guid categoryId)
{
try { var c = await _uow.MissionCategories.GetByIdAsync(categoryId); if (c == null) return false; _uow.MissionCategories.Remove(c); await _uow.SaveChangesAsync(); return true; }
catch (Exception ex) { Log.Error(ex, "DeleteCategoryAsync failed {CategoryId}", categoryId); throw; }
}
}

View File

@@ -1,165 +1,165 @@
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Models.DTO;
using LctMonolith.Services.Interfaces;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Services;
public class MissionService : IMissionService
{
private readonly IUnitOfWork _uow;
private readonly IRewardService _rewardService;
private readonly IRuleValidationService _ruleValidationService;
public MissionService(IUnitOfWork uow, IRewardService rewardService, IRuleValidationService ruleValidationService)
{
_uow = uow;
_rewardService = rewardService;
_ruleValidationService = ruleValidationService;
}
public async Task<Mission?> GetMissionByIdAsync(Guid missionId)
{
try { return await _uow.Missions.GetByIdAsync(missionId); }
catch (Exception ex) { Log.Error(ex, "GetMissionByIdAsync failed {MissionId}", missionId); throw; }
}
public async Task<IEnumerable<Mission>> GetMissionsByCategoryAsync(Guid categoryId)
{
try { return await _uow.Missions.Query(m => m.MissionCategoryId == categoryId).ToListAsync(); }
catch (Exception ex) { Log.Error(ex, "GetMissionsByCategoryAsync failed {Category}", categoryId); throw; }
}
public async Task<IEnumerable<Mission>> GetAvailableMissionsForPlayerAsync(Guid playerId)
{
try
{
var missions = await _uow.Missions.Query().ToListAsync();
var result = new List<Mission>();
foreach (var m in missions)
{
if (await IsMissionAvailableForPlayerAsync(m.Id, playerId)) result.Add(m);
}
return result;
}
catch (Exception ex) { Log.Error(ex, "GetAvailableMissionsForPlayerAsync failed {Player}", playerId); throw; }
}
public async Task<IEnumerable<Mission>> GetChildMissionsAsync(Guid parentMissionId)
{
try { return await _uow.Missions.Query(m => m.ParentMissionId == parentMissionId).ToListAsync(); }
catch (Exception ex) { Log.Error(ex, "GetChildMissionsAsync failed {ParentMission}", parentMissionId); throw; }
}
public async Task<Mission> CreateMissionAsync(Mission mission)
{
try
{
mission.Id = Guid.NewGuid();
await _uow.Missions.AddAsync(mission);
await _uow.SaveChangesAsync();
return mission;
}
catch (Exception ex) { Log.Error(ex, "CreateMissionAsync failed {Title}", mission.Title); throw; }
}
public async Task<Mission> UpdateMissionAsync(Mission mission)
{
try { _uow.Missions.Update(mission); await _uow.SaveChangesAsync(); return mission; }
catch (Exception ex) { Log.Error(ex, "UpdateMissionAsync failed {MissionId}", mission.Id); throw; }
}
public async Task<bool> DeleteMissionAsync(Guid missionId)
{
try { var m = await _uow.Missions.GetByIdAsync(missionId); if (m == null) return false; _uow.Missions.Remove(m); await _uow.SaveChangesAsync(); return true; }
catch (Exception ex) { Log.Error(ex, "DeleteMissionAsync failed {MissionId}", missionId); throw; }
}
public async Task<bool> IsMissionAvailableForPlayerAsync(Guid missionId, Guid playerId)
{
try
{
var mission = await _uow.Missions.GetByIdAsync(missionId);
if (mission == null) return false;
// rule validation
if (!await _ruleValidationService.ValidateMissionRankRulesAsync(missionId, playerId)) return false;
// already completed? then not available
var completed = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.MissionId == missionId && pm.Completed != null).AnyAsync();
if (completed) return false;
// if parent mission required ensure parent completed
if (mission.ParentMissionId.HasValue)
{
var parentDone = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.MissionId == mission.ParentMissionId && pm.Completed != null).AnyAsync();
if (!parentDone) return false;
}
return true;
}
catch (Exception ex) { Log.Error(ex, "IsMissionAvailableForPlayerAsync failed {MissionId} {PlayerId}", missionId, playerId); throw; }
}
public async Task<MissionCompletionResult> CompleteMissionAsync(Guid missionId, Guid playerId, object? missionProof = null)
{
try
{
if (!await IsMissionAvailableForPlayerAsync(missionId, playerId))
{
return new MissionCompletionResult { Success = false, Message = "Mission not available" };
}
var mission = await _uow.Missions.GetByIdAsync(missionId) ?? throw new KeyNotFoundException("Mission not found");
var player = await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException("Player not found");
// snapshot skill levels before
var skillRewards = await _uow.MissionSkillRewards.Query(r => r.MissionId == missionId).ToListAsync();
var beforeSkills = await _uow.PlayerSkills.Query(ps => ps.PlayerId == playerId).ToListAsync();
// mark PlayerMission
var pm = await _uow.PlayerMissions.Query(x => x.PlayerId == playerId && x.MissionId == missionId).FirstOrDefaultAsync();
if (pm == null)
{
pm = new PlayerMission { Id = Guid.NewGuid(), PlayerId = playerId, MissionId = missionId, Started = DateTime.UtcNow };
await _uow.PlayerMissions.AddAsync(pm);
}
pm.Completed = DateTime.UtcNow;
pm.ProgressPercent = 100;
await _uow.SaveChangesAsync();
var prevExp = player.Experience;
var prevMana = player.Mana;
// distribute rewards (XP/Mana/Skills/Items)
await _rewardService.DistributeMissionRewardsAsync(missionId, playerId);
await _uow.SaveChangesAsync();
// build skill progress
var afterSkills = await _uow.PlayerSkills.Query(ps => ps.PlayerId == playerId).ToListAsync();
var skillProgress = new List<SkillProgress>();
foreach (var r in skillRewards)
{
var before = beforeSkills.FirstOrDefault(s => s.SkillId == r.SkillId)?.Score ?? 0;
var after = afterSkills.FirstOrDefault(s => s.SkillId == r.SkillId)?.Score ?? before;
if (after != before)
{
var skill = await _uow.Skills.GetByIdAsync(r.SkillId);
skillProgress.Add(new SkillProgress { SkillId = r.SkillId, SkillTitle = skill?.Title ?? string.Empty, PreviousLevel = before, NewLevel = after });
}
}
return new MissionCompletionResult
{
Success = true,
Message = "Mission completed",
ExperienceGained = player.Experience - prevExp,
ManaGained = player.Mana - prevMana,
SkillsProgress = skillProgress,
UnlockedMissions = (await _uow.Missions.Query(m => m.ParentMissionId == missionId).Select(m => m.Id).ToListAsync())
};
}
catch (Exception ex)
{
Log.Error(ex, "CompleteMissionAsync failed {MissionId} {PlayerId}", missionId, playerId);
throw;
}
}
}
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Models.DTO;
using LctMonolith.Services.Interfaces;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Services;
public class MissionService : IMissionService
{
private readonly IUnitOfWork _uow;
private readonly IRewardService _rewardService;
private readonly IRuleValidationService _ruleValidationService;
public MissionService(IUnitOfWork uow, IRewardService rewardService, IRuleValidationService ruleValidationService)
{
_uow = uow;
_rewardService = rewardService;
_ruleValidationService = ruleValidationService;
}
public async Task<Mission?> GetMissionByIdAsync(Guid missionId)
{
try { return await _uow.Missions.GetByIdAsync(missionId); }
catch (Exception ex) { Log.Error(ex, "GetMissionByIdAsync failed {MissionId}", missionId); throw; }
}
public async Task<IEnumerable<Mission>> GetMissionsByCategoryAsync(Guid categoryId)
{
try { return await _uow.Missions.Query(m => m.MissionCategoryId == categoryId).ToListAsync(); }
catch (Exception ex) { Log.Error(ex, "GetMissionsByCategoryAsync failed {Category}", categoryId); throw; }
}
public async Task<IEnumerable<Mission>> GetAvailableMissionsForPlayerAsync(Guid playerId)
{
try
{
var missions = await _uow.Missions.Query().ToListAsync();
var result = new List<Mission>();
foreach (var m in missions)
{
if (await IsMissionAvailableForPlayerAsync(m.Id, playerId)) result.Add(m);
}
return result;
}
catch (Exception ex) { Log.Error(ex, "GetAvailableMissionsForPlayerAsync failed {Player}", playerId); throw; }
}
public async Task<IEnumerable<Mission>> GetChildMissionsAsync(Guid parentMissionId)
{
try { return await _uow.Missions.Query(m => m.ParentMissionId == parentMissionId).ToListAsync(); }
catch (Exception ex) { Log.Error(ex, "GetChildMissionsAsync failed {ParentMission}", parentMissionId); throw; }
}
public async Task<Mission> CreateMissionAsync(Mission mission)
{
try
{
mission.Id = Guid.NewGuid();
await _uow.Missions.AddAsync(mission);
await _uow.SaveChangesAsync();
return mission;
}
catch (Exception ex) { Log.Error(ex, "CreateMissionAsync failed {Title}", mission.Title); throw; }
}
public async Task<Mission> UpdateMissionAsync(Mission mission)
{
try { _uow.Missions.Update(mission); await _uow.SaveChangesAsync(); return mission; }
catch (Exception ex) { Log.Error(ex, "UpdateMissionAsync failed {MissionId}", mission.Id); throw; }
}
public async Task<bool> DeleteMissionAsync(Guid missionId)
{
try { var m = await _uow.Missions.GetByIdAsync(missionId); if (m == null) return false; _uow.Missions.Remove(m); await _uow.SaveChangesAsync(); return true; }
catch (Exception ex) { Log.Error(ex, "DeleteMissionAsync failed {MissionId}", missionId); throw; }
}
public async Task<bool> IsMissionAvailableForPlayerAsync(Guid missionId, Guid playerId)
{
try
{
var mission = await _uow.Missions.GetByIdAsync(missionId);
if (mission == null) return false;
// rule validation
if (!await _ruleValidationService.ValidateMissionRankRulesAsync(missionId, playerId)) return false;
// already completed? then not available
var completed = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.MissionId == missionId && pm.Completed != null).AnyAsync();
if (completed) return false;
// if parent mission required ensure parent completed
if (mission.ParentMissionId.HasValue)
{
var parentDone = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.MissionId == mission.ParentMissionId && pm.Completed != null).AnyAsync();
if (!parentDone) return false;
}
return true;
}
catch (Exception ex) { Log.Error(ex, "IsMissionAvailableForPlayerAsync failed {MissionId} {PlayerId}", missionId, playerId); throw; }
}
public async Task<MissionCompletionResult> CompleteMissionAsync(Guid missionId, Guid playerId, object? missionProof = null)
{
try
{
if (!await IsMissionAvailableForPlayerAsync(missionId, playerId))
{
return new MissionCompletionResult { Success = false, Message = "Mission not available" };
}
var mission = await _uow.Missions.GetByIdAsync(missionId) ?? throw new KeyNotFoundException("Mission not found");
var player = await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException("Player not found");
// snapshot skill levels before
var skillRewards = await _uow.MissionSkillRewards.Query(r => r.MissionId == missionId).ToListAsync();
var beforeSkills = await _uow.PlayerSkills.Query(ps => ps.PlayerId == playerId).ToListAsync();
// mark PlayerMission
var pm = await _uow.PlayerMissions.Query(x => x.PlayerId == playerId && x.MissionId == missionId).FirstOrDefaultAsync();
if (pm == null)
{
pm = new PlayerMission { Id = Guid.NewGuid(), PlayerId = playerId, MissionId = missionId, Started = DateTime.UtcNow };
await _uow.PlayerMissions.AddAsync(pm);
}
pm.Completed = DateTime.UtcNow;
pm.ProgressPercent = 100;
await _uow.SaveChangesAsync();
var prevExp = player.Experience;
var prevMana = player.Mana;
// distribute rewards (XP/Mana/Skills/Items)
await _rewardService.DistributeMissionRewardsAsync(missionId, playerId);
await _uow.SaveChangesAsync();
// build skill progress
var afterSkills = await _uow.PlayerSkills.Query(ps => ps.PlayerId == playerId).ToListAsync();
var skillProgress = new List<SkillProgress>();
foreach (var r in skillRewards)
{
var before = beforeSkills.FirstOrDefault(s => s.SkillId == r.SkillId)?.Score ?? 0;
var after = afterSkills.FirstOrDefault(s => s.SkillId == r.SkillId)?.Score ?? before;
if (after != before)
{
var skill = await _uow.Skills.GetByIdAsync(r.SkillId);
skillProgress.Add(new SkillProgress { SkillId = r.SkillId, SkillTitle = skill?.Title ?? string.Empty, PreviousLevel = before, NewLevel = after });
}
}
return new MissionCompletionResult
{
Success = true,
Message = "Mission completed",
ExperienceGained = player.Experience - prevExp,
ManaGained = player.Mana - prevMana,
SkillsProgress = skillProgress,
UnlockedMissions = (await _uow.Missions.Query(m => m.ParentMissionId == missionId).Select(m => m.Id).ToListAsync())
};
}
catch (Exception ex)
{
Log.Error(ex, "CompleteMissionAsync failed {MissionId} {PlayerId}", missionId, playerId);
throw;
}
}
}

View File

@@ -1,11 +1,11 @@
namespace LctMonolith.Services.Models;
public class AnalyticsSummary
{
public int TotalUsers { get; set; }
public int TotalMissions { get; set; }
public int CompletedMissions { get; set; }
public int TotalStoreItems { get; set; }
public long TotalExperience { get; set; }
public DateTime GeneratedAtUtc { get; set; } = DateTime.UtcNow;
}
namespace LctMonolith.Services.Models;
public class AnalyticsSummary
{
public int TotalUsers { get; set; }
public int TotalMissions { get; set; }
public int CompletedMissions { get; set; }
public int TotalStoreItems { get; set; }
public long TotalExperience { get; set; }
public DateTime GeneratedAtUtc { get; set; } = DateTime.UtcNow;
}

View File

@@ -1,9 +1,9 @@
namespace LctMonolith.Services.Models;
public class AuthRequest
{
public string Email { get; set; } = null!;
public string Password { get; set; } = null!;
public string? FirstName { get; set; }
public string? LastName { get; set; }
}
namespace LctMonolith.Services.Models;
public class AuthRequest
{
public string Email { get; set; } = null!;
public string Password { get; set; } = null!;
public string? FirstName { get; set; }
public string? LastName { get; set; }
}

View File

@@ -1,8 +1,8 @@
namespace LctMonolith.Services.Models;
public class CompetencyRewardModel
{
public Guid CompetencyId { get; set; }
public int LevelDelta { get; set; }
public int ProgressPointsDelta { get; set; }
}
namespace LctMonolith.Services.Models;
public class CompetencyRewardModel
{
public Guid CompetencyId { get; set; }
public int LevelDelta { get; set; }
public int ProgressPointsDelta { get; set; }
}

View File

@@ -1,15 +1,15 @@
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Models;
public class CreateMissionModel
{
public string Title { get; set; } = null!;
public string? Description { get; set; }
public string? Branch { get; set; }
public MissionCategory Category { get; set; }
public Guid? MinRankId { get; set; }
public int ExperienceReward { get; set; }
public int ManaReward { get; set; }
public List<CompetencyRewardModel> CompetencyRewards { get; set; } = new();
}
using LctMonolith.Models.Database;
namespace LctMonolith.Services.Models;
public class CreateMissionModel
{
public string Title { get; set; } = null!;
public string? Description { get; set; }
public string? Branch { get; set; }
public MissionCategory Category { get; set; }
public Guid? MinRankId { get; set; }
public int ExperienceReward { get; set; }
public int ManaReward { get; set; }
public List<CompetencyRewardModel> CompetencyRewards { get; set; } = new();
}

View File

@@ -1,23 +1,23 @@
namespace LctMonolith.Services.Models;
public class ProgressSnapshot
{
public int Experience { get; set; }
public int Mana { get; set; }
public Guid? CurrentRankId { get; set; }
public string? CurrentRankName { get; set; }
public Guid? NextRankId { get; set; }
public string? NextRankName { get; set; }
public int? RequiredExperienceForNextRank { get; set; }
public int? ExperienceRemaining => RequiredExperienceForNextRank.HasValue ? Math.Max(0, RequiredExperienceForNextRank.Value - Experience) : null;
public List<Guid> OutstandingMissionIds { get; set; } = new();
public List<OutstandingCompetency> OutstandingCompetencies { get; set; } = new();
}
public class OutstandingCompetency
{
public Guid CompetencyId { get; set; }
public string? CompetencyName { get; set; }
public int RequiredLevel { get; set; }
public int CurrentLevel { get; set; }
}
namespace LctMonolith.Services.Models;
public class ProgressSnapshot
{
public int Experience { get; set; }
public int Mana { get; set; }
public Guid? CurrentRankId { get; set; }
public string? CurrentRankName { get; set; }
public Guid? NextRankId { get; set; }
public string? NextRankName { get; set; }
public int? RequiredExperienceForNextRank { get; set; }
public int? ExperienceRemaining => RequiredExperienceForNextRank.HasValue ? Math.Max(0, RequiredExperienceForNextRank.Value - Experience) : null;
public List<Guid> OutstandingMissionIds { get; set; } = new();
public List<OutstandingCompetency> OutstandingCompetencies { get; set; } = new();
}
public class OutstandingCompetency
{
public Guid CompetencyId { get; set; }
public string? CompetencyName { get; set; }
public int RequiredLevel { get; set; }
public int CurrentLevel { get; set; }
}

View File

@@ -1,7 +1,7 @@
namespace LctMonolith.Services.Models;
public class PurchaseRequest
{
public Guid ItemId { get; set; }
public int Quantity { get; set; } = 1;
}
namespace LctMonolith.Services.Models;
public class PurchaseRequest
{
public Guid ItemId { get; set; }
public int Quantity { get; set; } = 1;
}

View File

@@ -1,6 +1,6 @@
namespace LctMonolith.Services.Models;
public class RefreshRequest
{
public string RefreshToken { get; set; } = null!;
}
namespace LctMonolith.Services.Models;
public class RefreshRequest
{
public string RefreshToken { get; set; } = null!;
}

View File

@@ -1,6 +1,6 @@
namespace LctMonolith.Services.Models;
public class RevokeRequest
{
public string RefreshToken { get; set; } = null!;
}
namespace LctMonolith.Services.Models;
public class RevokeRequest
{
public string RefreshToken { get; set; } = null!;
}

View File

@@ -1,3 +1,3 @@
namespace LctMonolith.Services.Models;
public record TokenPair(string AccessToken, DateTime AccessTokenExpiresAt, string RefreshToken, DateTime RefreshTokenExpiresAt);
namespace LctMonolith.Services.Models;
public record TokenPair(string AccessToken, DateTime AccessTokenExpiresAt, string RefreshToken, DateTime RefreshTokenExpiresAt);

View File

@@ -1,68 +1,68 @@
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Services.Contracts;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Services;
/// <summary>
/// In-app user notifications CRUD / read-state operations.
/// </summary>
public class NotificationService : INotificationService
{
private readonly IUnitOfWork _uow;
public NotificationService(IUnitOfWork uow)
{
_uow = uow;
}
public async Task<Notification> CreateAsync(Guid userId, string type, string title, string message, CancellationToken ct = default)
{
try
{
if (string.IsNullOrWhiteSpace(type)) throw new ArgumentException("Required", nameof(type));
if (string.IsNullOrWhiteSpace(title)) throw new ArgumentException("Required", nameof(title));
if (string.IsNullOrWhiteSpace(message)) throw new ArgumentException("Required", nameof(message));
var n = new Notification
{
UserId = userId,
Type = type.Trim(),
Title = title.Trim(),
Message = message.Trim(),
CreatedAt = DateTime.UtcNow,
IsRead = false
};
await _uow.Notifications.AddAsync(n, ct);
await _uow.SaveChangesAsync(ct);
return n;
}
catch (Exception ex) { Log.Error(ex, "Notification CreateAsync failed {UserId}", userId); throw; }
}
public async Task<IEnumerable<Notification>> GetUnreadAsync(Guid userId, CancellationToken ct = default)
{
try { return await _uow.Notifications.Query(n => n.UserId == userId && !n.IsRead, q => q.OrderByDescending(x => x.CreatedAt)).Take(100).ToListAsync(ct); }
catch (Exception ex) { Log.Error(ex, "GetUnreadAsync failed {UserId}", userId); throw; }
}
public async Task<IEnumerable<Notification>> GetAllAsync(Guid userId, int take = 100, CancellationToken ct = default)
{
try { if (take <= 0) take = 1; if (take > 500) take = 500; return await _uow.Notifications.Query(n => n.UserId == userId, q => q.OrderByDescending(x => x.CreatedAt)).Take(take).ToListAsync(ct); }
catch (Exception ex) { Log.Error(ex, "GetAllAsync failed {UserId}", userId); throw; }
}
public async Task MarkReadAsync(Guid userId, Guid notificationId, CancellationToken ct = default)
{
try { var notif = await _uow.Notifications.Query(n => n.Id == notificationId && n.UserId == userId).FirstOrDefaultAsync(ct) ?? throw new KeyNotFoundException("Notification not found"); if (!notif.IsRead) { notif.IsRead = true; notif.ReadAt = DateTime.UtcNow; await _uow.SaveChangesAsync(ct); } }
catch (Exception ex) { Log.Error(ex, "MarkReadAsync failed {NotificationId}", notificationId); throw; }
}
public async Task<int> MarkAllReadAsync(Guid userId, CancellationToken ct = default)
{
try { var unread = await _uow.Notifications.Query(n => n.UserId == userId && !n.IsRead).ToListAsync(ct); if (unread.Count == 0) return 0; var now = DateTime.UtcNow; foreach (var n in unread) { n.IsRead = true; n.ReadAt = now; } await _uow.SaveChangesAsync(ct); return unread.Count; }
catch (Exception ex) { Log.Error(ex, "MarkAllReadAsync failed {UserId}", userId); throw; }
}
}
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Services.Contracts;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Services;
/// <summary>
/// In-app user notifications CRUD / read-state operations.
/// </summary>
public class NotificationService : INotificationService
{
private readonly IUnitOfWork _uow;
public NotificationService(IUnitOfWork uow)
{
_uow = uow;
}
public async Task<Notification> CreateAsync(Guid userId, string type, string title, string message, CancellationToken ct = default)
{
try
{
if (string.IsNullOrWhiteSpace(type)) throw new ArgumentException("Required", nameof(type));
if (string.IsNullOrWhiteSpace(title)) throw new ArgumentException("Required", nameof(title));
if (string.IsNullOrWhiteSpace(message)) throw new ArgumentException("Required", nameof(message));
var n = new Notification
{
UserId = userId,
Type = type.Trim(),
Title = title.Trim(),
Message = message.Trim(),
CreatedAt = DateTime.UtcNow,
IsRead = false
};
await _uow.Notifications.AddAsync(n, ct);
await _uow.SaveChangesAsync(ct);
return n;
}
catch (Exception ex) { Log.Error(ex, "Notification CreateAsync failed {UserId}", userId); throw; }
}
public async Task<IEnumerable<Notification>> GetUnreadAsync(Guid userId, CancellationToken ct = default)
{
try { return await _uow.Notifications.Query(n => n.UserId == userId && !n.IsRead, q => q.OrderByDescending(x => x.CreatedAt)).Take(100).ToListAsync(ct); }
catch (Exception ex) { Log.Error(ex, "GetUnreadAsync failed {UserId}", userId); throw; }
}
public async Task<IEnumerable<Notification>> GetAllAsync(Guid userId, int take = 100, CancellationToken ct = default)
{
try { if (take <= 0) take = 1; if (take > 500) take = 500; return await _uow.Notifications.Query(n => n.UserId == userId, q => q.OrderByDescending(x => x.CreatedAt)).Take(take).ToListAsync(ct); }
catch (Exception ex) { Log.Error(ex, "GetAllAsync failed {UserId}", userId); throw; }
}
public async Task MarkReadAsync(Guid userId, Guid notificationId, CancellationToken ct = default)
{
try { var notif = await _uow.Notifications.Query(n => n.Id == notificationId && n.UserId == userId).FirstOrDefaultAsync(ct) ?? throw new KeyNotFoundException("Notification not found"); if (!notif.IsRead) { notif.IsRead = true; notif.ReadAt = DateTime.UtcNow; await _uow.SaveChangesAsync(ct); } }
catch (Exception ex) { Log.Error(ex, "MarkReadAsync failed {NotificationId}", notificationId); throw; }
}
public async Task<int> MarkAllReadAsync(Guid userId, CancellationToken ct = default)
{
try { var unread = await _uow.Notifications.Query(n => n.UserId == userId && !n.IsRead).ToListAsync(ct); if (unread.Count == 0) return 0; var now = DateTime.UtcNow; foreach (var n in unread) { n.IsRead = true; n.ReadAt = now; } await _uow.SaveChangesAsync(ct); return unread.Count; }
catch (Exception ex) { Log.Error(ex, "MarkAllReadAsync failed {UserId}", userId); throw; }
}
}

View File

@@ -1,153 +1,153 @@
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Services.Interfaces;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Services;
public class PlayerService : IPlayerService
{
private readonly IUnitOfWork _uow;
public PlayerService(IUnitOfWork uow) => _uow = uow;
public async Task<Player?> GetPlayerByUserIdAsync(string userId)
{
try
{
if (!Guid.TryParse(userId, out var uid)) return null;
return await _uow.Players.Query(p => p.UserId == uid, null, p => p.Rank).FirstOrDefaultAsync();
}
catch (Exception ex)
{
Log.Error(ex, "GetPlayerByUserIdAsync failed {UserId}", userId);
throw;
}
}
public async Task<Player> CreatePlayerAsync(string userId, string username)
{
try
{
if (!Guid.TryParse(userId, out var uid)) throw new ArgumentException("Invalid user id", nameof(userId));
var existing = await GetPlayerByUserIdAsync(userId);
if (existing != null) return existing;
// pick lowest exp rank
var baseRank = await _uow.Ranks.Query().OrderBy(r => r.ExpNeeded).FirstAsync();
var player = new Player
{
Id = Guid.NewGuid(),
UserId = uid,
RankId = baseRank.Id,
Experience = 0,
Mana = 0
};
await _uow.Players.AddAsync(player);
await _uow.SaveChangesAsync();
Log.Information("Created player {PlayerId} for user {UserId}", player.Id, userId);
return player;
}
catch (Exception ex)
{
Log.Error(ex, "CreatePlayerAsync failed {UserId}", userId);
throw;
}
}
public async Task<Player> UpdatePlayerRankAsync(Guid playerId, Guid newRankId)
{
try
{
var player = await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException("Player not found");
var rank = await _uow.Ranks.GetByIdAsync(newRankId) ?? throw new KeyNotFoundException("Rank not found");
player.RankId = rank.Id;
await _uow.SaveChangesAsync();
return player;
}
catch (Exception ex)
{
Log.Error(ex, "UpdatePlayerRankAsync failed {PlayerId} -> {RankId}", playerId, newRankId);
throw;
}
}
public async Task<Player> AddPlayerExperienceAsync(Guid playerId, int experience)
{
try
{
if (experience == 0) return await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException();
var player = await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException("Player not found");
player.Experience += experience;
await _uow.SaveChangesAsync();
await AutoRankUpAsync(player);
return player;
}
catch (Exception ex)
{
Log.Error(ex, "AddPlayerExperienceAsync failed {PlayerId}", playerId);
throw;
}
}
private async Task AutoRankUpAsync(Player player)
{
// find highest rank whose ExpNeeded <= player's experience
var target = await _uow.Ranks.Query(r => r.ExpNeeded <= player.Experience)
.OrderByDescending(r => r.ExpNeeded)
.FirstOrDefaultAsync();
if (target != null && target.Id != player.RankId)
{
player.RankId = target.Id;
await _uow.SaveChangesAsync();
Log.Information("Player {Player} advanced to rank {Rank}", player.Id, target.Title);
}
}
public async Task<Player> AddPlayerManaAsync(Guid playerId, int mana)
{
try
{
if (mana == 0) return await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException();
var player = await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException("Player not found");
player.Mana += mana;
await _uow.SaveChangesAsync();
return player;
}
catch (Exception ex)
{
Log.Error(ex, "AddPlayerManaAsync failed {PlayerId}", playerId);
throw;
}
}
public async Task<IEnumerable<Player>> GetTopPlayersAsync(int topCount, TimeSpan timeFrame)
{
try
{
// Simple ordering by experience (timeFrame ignored due to no timestamp on Player)
return await _uow.Players.Query()
.OrderByDescending(p => p.Experience)
.Take(topCount)
.ToListAsync();
}
catch (Exception ex)
{
Log.Error(ex, "GetTopPlayersAsync failed");
throw;
}
}
public async Task<Player> GetPlayerWithProgressAsync(Guid playerId)
{
try
{
return await _uow.Players.Query(p => p.Id == playerId, null, p => p.Rank)
.FirstOrDefaultAsync() ?? throw new KeyNotFoundException("Player not found");
}
catch (Exception ex)
{
Log.Error(ex, "GetPlayerWithProgressAsync failed {PlayerId}", playerId);
throw;
}
}
}
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Services.Interfaces;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Services;
public class PlayerService : IPlayerService
{
private readonly IUnitOfWork _uow;
public PlayerService(IUnitOfWork uow) => _uow = uow;
public async Task<Player?> GetPlayerByUserIdAsync(string userId)
{
try
{
if (!Guid.TryParse(userId, out var uid)) return null;
return await _uow.Players.Query(p => p.UserId == uid, null, p => p.Rank).FirstOrDefaultAsync();
}
catch (Exception ex)
{
Log.Error(ex, "GetPlayerByUserIdAsync failed {UserId}", userId);
throw;
}
}
public async Task<Player> CreatePlayerAsync(string userId, string username)
{
try
{
if (!Guid.TryParse(userId, out var uid)) throw new ArgumentException("Invalid user id", nameof(userId));
var existing = await GetPlayerByUserIdAsync(userId);
if (existing != null) return existing;
// pick lowest exp rank
var baseRank = await _uow.Ranks.Query().OrderBy(r => r.ExpNeeded).FirstAsync();
var player = new Player
{
Id = Guid.NewGuid(),
UserId = uid,
RankId = baseRank.Id,
Experience = 0,
Mana = 0
};
await _uow.Players.AddAsync(player);
await _uow.SaveChangesAsync();
Log.Information("Created player {PlayerId} for user {UserId}", player.Id, userId);
return player;
}
catch (Exception ex)
{
Log.Error(ex, "CreatePlayerAsync failed {UserId}", userId);
throw;
}
}
public async Task<Player> UpdatePlayerRankAsync(Guid playerId, Guid newRankId)
{
try
{
var player = await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException("Player not found");
var rank = await _uow.Ranks.GetByIdAsync(newRankId) ?? throw new KeyNotFoundException("Rank not found");
player.RankId = rank.Id;
await _uow.SaveChangesAsync();
return player;
}
catch (Exception ex)
{
Log.Error(ex, "UpdatePlayerRankAsync failed {PlayerId} -> {RankId}", playerId, newRankId);
throw;
}
}
public async Task<Player> AddPlayerExperienceAsync(Guid playerId, int experience)
{
try
{
if (experience == 0) return await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException();
var player = await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException("Player not found");
player.Experience += experience;
await _uow.SaveChangesAsync();
await AutoRankUpAsync(player);
return player;
}
catch (Exception ex)
{
Log.Error(ex, "AddPlayerExperienceAsync failed {PlayerId}", playerId);
throw;
}
}
private async Task AutoRankUpAsync(Player player)
{
// find highest rank whose ExpNeeded <= player's experience
var target = await _uow.Ranks.Query(r => r.ExpNeeded <= player.Experience)
.OrderByDescending(r => r.ExpNeeded)
.FirstOrDefaultAsync();
if (target != null && target.Id != player.RankId)
{
player.RankId = target.Id;
await _uow.SaveChangesAsync();
Log.Information("Player {Player} advanced to rank {Rank}", player.Id, target.Title);
}
}
public async Task<Player> AddPlayerManaAsync(Guid playerId, int mana)
{
try
{
if (mana == 0) return await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException();
var player = await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException("Player not found");
player.Mana += mana;
await _uow.SaveChangesAsync();
return player;
}
catch (Exception ex)
{
Log.Error(ex, "AddPlayerManaAsync failed {PlayerId}", playerId);
throw;
}
}
public async Task<IEnumerable<Player>> GetTopPlayersAsync(int topCount, TimeSpan timeFrame)
{
try
{
// Simple ordering by experience (timeFrame ignored due to no timestamp on Player)
return await _uow.Players.Query()
.OrderByDescending(p => p.Experience)
.Take(topCount)
.ToListAsync();
}
catch (Exception ex)
{
Log.Error(ex, "GetTopPlayersAsync failed");
throw;
}
}
public async Task<Player> GetPlayerWithProgressAsync(Guid playerId)
{
try
{
return await _uow.Players.Query(p => p.Id == playerId, null, p => p.Rank)
.FirstOrDefaultAsync() ?? throw new KeyNotFoundException("Player not found");
}
catch (Exception ex)
{
Log.Error(ex, "GetPlayerWithProgressAsync failed {PlayerId}", playerId);
throw;
}
}
}

View File

@@ -1,121 +1,121 @@
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Services.Interfaces;
using Serilog;
using Microsoft.EntityFrameworkCore;
using System.Linq;
namespace LctMonolith.Services;
public class ProfileService : IProfileService
{
private readonly IUnitOfWork _uow;
private readonly IFileStorageService _storage;
public ProfileService(IUnitOfWork uow, IFileStorageService storage)
{
_uow = uow;
_storage = storage;
}
public async Task<Profile?> GetByUserIdAsync(Guid userId, CancellationToken ct = default)
{
try
{
return await _uow.Profiles.Query(p => p.UserId == userId).FirstOrDefaultAsync(ct);
}
catch (Exception ex)
{
Log.Error(ex, "Profile get failed {UserId}", userId);
throw;
}
}
public async Task<Profile> UpsertAsync(Guid userId, string? firstName, string? lastName, DateOnly? birthDate, string? about, string? location, CancellationToken ct = default)
{
try
{
var profile = await _uow.Profiles.Query(p => p.UserId == userId).FirstOrDefaultAsync(ct);
if (profile == null)
{
profile = new Profile
{
Id = Guid.NewGuid(),
UserId = userId,
FirstName = firstName,
LastName = lastName,
BirthDate = birthDate,
About = about,
Location = location
};
await _uow.Profiles.AddAsync(profile, ct);
}
else
{
profile.FirstName = firstName;
profile.LastName = lastName;
profile.BirthDate = birthDate;
profile.About = about;
profile.Location = location;
profile.UpdatedAt = DateTime.UtcNow;
_uow.Profiles.Update(profile);
}
await _uow.SaveChangesAsync(ct);
return profile;
}
catch (Exception ex)
{
Log.Error(ex, "Profile upsert failed {UserId}", userId);
throw;
}
}
public async Task<Profile> UpdateAvatarAsync(Guid userId, Stream fileStream, string contentType, string? fileName, CancellationToken ct = default)
{
try
{
var profile = await _uow.Profiles.Query(p => p.UserId == userId).FirstOrDefaultAsync(ct) ??
await UpsertAsync(userId, null, null, null, null, null, ct);
// Delete old if exists
if (!string.IsNullOrWhiteSpace(profile.AvatarS3Key))
{
await _storage.DeleteAsync(profile.AvatarS3Key!, ct);
}
var key = await _storage.UploadAsync(fileStream, contentType, $"avatars/{userId}", ct);
var url = await _storage.GetPresignedUrlAsync(key, TimeSpan.FromHours(6), ct);
profile.AvatarS3Key = key;
profile.AvatarUrl = url;
profile.UpdatedAt = DateTime.UtcNow;
_uow.Profiles.Update(profile);
await _uow.SaveChangesAsync(ct);
return profile;
}
catch (Exception ex)
{
Log.Error(ex, "Avatar update failed {UserId}", userId);
throw;
}
}
public async Task<bool> DeleteAvatarAsync(Guid userId, CancellationToken ct = default)
{
try
{
var profile = await _uow.Profiles.Query(p => p.UserId == userId).FirstOrDefaultAsync(ct);
if (profile == null || string.IsNullOrWhiteSpace(profile.AvatarS3Key)) return false;
await _storage.DeleteAsync(profile.AvatarS3Key!, ct);
profile.AvatarS3Key = null;
profile.AvatarUrl = null;
profile.UpdatedAt = DateTime.UtcNow;
_uow.Profiles.Update(profile);
await _uow.SaveChangesAsync(ct);
return true;
}
catch (Exception ex)
{
Log.Error(ex, "Delete avatar failed {UserId}", userId);
throw;
}
}
}
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Services.Interfaces;
using Serilog;
using Microsoft.EntityFrameworkCore;
using System.Linq;
namespace LctMonolith.Services;
public class ProfileService : IProfileService
{
private readonly IUnitOfWork _uow;
private readonly IFileStorageService _storage;
public ProfileService(IUnitOfWork uow, IFileStorageService storage)
{
_uow = uow;
_storage = storage;
}
public async Task<Profile?> GetByUserIdAsync(Guid userId, CancellationToken ct = default)
{
try
{
return await _uow.Profiles.Query(p => p.UserId == userId).FirstOrDefaultAsync(ct);
}
catch (Exception ex)
{
Log.Error(ex, "Profile get failed {UserId}", userId);
throw;
}
}
public async Task<Profile> UpsertAsync(Guid userId, string? firstName, string? lastName, DateOnly? birthDate, string? about, string? location, CancellationToken ct = default)
{
try
{
var profile = await _uow.Profiles.Query(p => p.UserId == userId).FirstOrDefaultAsync(ct);
if (profile == null)
{
profile = new Profile
{
Id = Guid.NewGuid(),
UserId = userId,
FirstName = firstName,
LastName = lastName,
BirthDate = birthDate,
About = about,
Location = location
};
await _uow.Profiles.AddAsync(profile, ct);
}
else
{
profile.FirstName = firstName;
profile.LastName = lastName;
profile.BirthDate = birthDate;
profile.About = about;
profile.Location = location;
profile.UpdatedAt = DateTime.UtcNow;
_uow.Profiles.Update(profile);
}
await _uow.SaveChangesAsync(ct);
return profile;
}
catch (Exception ex)
{
Log.Error(ex, "Profile upsert failed {UserId}", userId);
throw;
}
}
public async Task<Profile> UpdateAvatarAsync(Guid userId, Stream fileStream, string contentType, string? fileName, CancellationToken ct = default)
{
try
{
var profile = await _uow.Profiles.Query(p => p.UserId == userId).FirstOrDefaultAsync(ct) ??
await UpsertAsync(userId, null, null, null, null, null, ct);
// Delete old if exists
if (!string.IsNullOrWhiteSpace(profile.AvatarS3Key))
{
await _storage.DeleteAsync(profile.AvatarS3Key!, ct);
}
var key = await _storage.UploadAsync(fileStream, contentType, $"avatars/{userId}", ct);
var url = await _storage.GetPresignedUrlAsync(key, TimeSpan.FromHours(6), ct);
profile.AvatarS3Key = key;
profile.AvatarUrl = url;
profile.UpdatedAt = DateTime.UtcNow;
_uow.Profiles.Update(profile);
await _uow.SaveChangesAsync(ct);
return profile;
}
catch (Exception ex)
{
Log.Error(ex, "Avatar update failed {UserId}", userId);
throw;
}
}
public async Task<bool> DeleteAvatarAsync(Guid userId, CancellationToken ct = default)
{
try
{
var profile = await _uow.Profiles.Query(p => p.UserId == userId).FirstOrDefaultAsync(ct);
if (profile == null || string.IsNullOrWhiteSpace(profile.AvatarS3Key)) return false;
await _storage.DeleteAsync(profile.AvatarS3Key!, ct);
profile.AvatarS3Key = null;
profile.AvatarUrl = null;
profile.UpdatedAt = DateTime.UtcNow;
_uow.Profiles.Update(profile);
await _uow.SaveChangesAsync(ct);
return true;
}
catch (Exception ex)
{
Log.Error(ex, "Delete avatar failed {UserId}", userId);
throw;
}
}
}

View File

@@ -1,103 +1,103 @@
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Models.DTO;
using LctMonolith.Services.Interfaces;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Services;
public class ProgressTrackingService : IProgressTrackingService
{
private readonly IUnitOfWork _uow;
private readonly IMissionService _missionService;
public ProgressTrackingService(IUnitOfWork uow, IMissionService missionService)
{
_uow = uow;
_missionService = missionService;
}
public async Task<PlayerMission> StartMissionAsync(Guid missionId, Guid playerId)
{
try
{
var existing = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.MissionId == missionId).FirstOrDefaultAsync();
if (existing != null) return existing;
var pm = new PlayerMission { Id = Guid.NewGuid(), PlayerId = playerId, MissionId = missionId, Started = DateTime.UtcNow, ProgressPercent = 0 };
await _uow.PlayerMissions.AddAsync(pm);
await _uow.SaveChangesAsync();
return pm;
}
catch (Exception ex) { Log.Error(ex, "StartMissionAsync failed {MissionId} {PlayerId}", missionId, playerId); throw; }
}
public async Task<PlayerMission> UpdateMissionProgressAsync(Guid playerMissionId, int progressPercentage, object? proof = null)
{
try
{
if (progressPercentage is < 0 or > 100) throw new ArgumentOutOfRangeException(nameof(progressPercentage));
var pm = await _uow.PlayerMissions.GetByIdAsync(playerMissionId) ?? throw new KeyNotFoundException("PlayerMission not found");
if (pm.Completed != null) return pm;
pm.ProgressPercent = progressPercentage;
if (progressPercentage == 100 && pm.Completed == null)
{
// Complete mission through mission service to allocate rewards, etc.
await _missionService.CompleteMissionAsync(pm.MissionId, pm.PlayerId);
}
await _uow.SaveChangesAsync();
return pm;
}
catch (Exception ex) { Log.Error(ex, "UpdateMissionProgressAsync failed {PlayerMissionId}", playerMissionId); throw; }
}
public async Task<PlayerMission> CompleteMissionAsync(Guid playerMissionId, object? proof = null)
{
try
{
var pm = await _uow.PlayerMissions.GetByIdAsync(playerMissionId) ?? throw new KeyNotFoundException("PlayerMission not found");
if (pm.Completed != null) return pm;
await _missionService.CompleteMissionAsync(pm.MissionId, pm.PlayerId, proof);
await _uow.SaveChangesAsync();
return pm;
}
catch (Exception ex) { Log.Error(ex, "CompleteMissionAsync (progress) failed {PlayerMissionId}", playerMissionId); throw; }
}
public async Task<IEnumerable<PlayerMission>> GetPlayerMissionsAsync(Guid playerId)
{
try { return await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId).ToListAsync(); }
catch (Exception ex) { Log.Error(ex, "GetPlayerMissionsAsync failed {PlayerId}", playerId); throw; }
}
public async Task<PlayerMission?> GetPlayerMissionAsync(Guid playerId, Guid missionId)
{
try { return await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.MissionId == missionId).FirstOrDefaultAsync(); }
catch (Exception ex) { Log.Error(ex, "GetPlayerMissionAsync failed {MissionId} {PlayerId}", missionId, playerId); throw; }
}
public async Task<PlayerProgress> GetPlayerOverallProgressAsync(Guid playerId)
{
try
{
var player = await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException("Player not found");
var missions = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId).ToListAsync();
var completed = missions.Count(m => m.Completed != null);
var totalMissions = await _uow.Missions.Query().CountAsync();
var skillLevels = await _uow.PlayerSkills.Query(ps => ps.PlayerId == playerId, null, ps => ps.Skill)
.ToDictionaryAsync(ps => ps.Skill.Title, ps => ps.Score);
return new PlayerProgress
{
PlayerId = playerId,
PlayerName = playerId.ToString(),
CurrentRank = await _uow.Ranks.GetByIdAsync(player.RankId),
TotalExperience = player.Experience,
TotalMana = player.Mana,
CompletedMissions = completed,
TotalAvailableMissions = totalMissions,
SkillLevels = skillLevels
};
}
catch (Exception ex) { Log.Error(ex, "GetPlayerOverallProgressAsync failed {PlayerId}", playerId); throw; }
}
}
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Models.DTO;
using LctMonolith.Services.Interfaces;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Services;
public class ProgressTrackingService : IProgressTrackingService
{
private readonly IUnitOfWork _uow;
private readonly IMissionService _missionService;
public ProgressTrackingService(IUnitOfWork uow, IMissionService missionService)
{
_uow = uow;
_missionService = missionService;
}
public async Task<PlayerMission> StartMissionAsync(Guid missionId, Guid playerId)
{
try
{
var existing = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.MissionId == missionId).FirstOrDefaultAsync();
if (existing != null) return existing;
var pm = new PlayerMission { Id = Guid.NewGuid(), PlayerId = playerId, MissionId = missionId, Started = DateTime.UtcNow, ProgressPercent = 0 };
await _uow.PlayerMissions.AddAsync(pm);
await _uow.SaveChangesAsync();
return pm;
}
catch (Exception ex) { Log.Error(ex, "StartMissionAsync failed {MissionId} {PlayerId}", missionId, playerId); throw; }
}
public async Task<PlayerMission> UpdateMissionProgressAsync(Guid playerMissionId, int progressPercentage, object? proof = null)
{
try
{
if (progressPercentage is < 0 or > 100) throw new ArgumentOutOfRangeException(nameof(progressPercentage));
var pm = await _uow.PlayerMissions.GetByIdAsync(playerMissionId) ?? throw new KeyNotFoundException("PlayerMission not found");
if (pm.Completed != null) return pm;
pm.ProgressPercent = progressPercentage;
if (progressPercentage == 100 && pm.Completed == null)
{
// Complete mission through mission service to allocate rewards, etc.
await _missionService.CompleteMissionAsync(pm.MissionId, pm.PlayerId);
}
await _uow.SaveChangesAsync();
return pm;
}
catch (Exception ex) { Log.Error(ex, "UpdateMissionProgressAsync failed {PlayerMissionId}", playerMissionId); throw; }
}
public async Task<PlayerMission> CompleteMissionAsync(Guid playerMissionId, object? proof = null)
{
try
{
var pm = await _uow.PlayerMissions.GetByIdAsync(playerMissionId) ?? throw new KeyNotFoundException("PlayerMission not found");
if (pm.Completed != null) return pm;
await _missionService.CompleteMissionAsync(pm.MissionId, pm.PlayerId, proof);
await _uow.SaveChangesAsync();
return pm;
}
catch (Exception ex) { Log.Error(ex, "CompleteMissionAsync (progress) failed {PlayerMissionId}", playerMissionId); throw; }
}
public async Task<IEnumerable<PlayerMission>> GetPlayerMissionsAsync(Guid playerId)
{
try { return await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId).ToListAsync(); }
catch (Exception ex) { Log.Error(ex, "GetPlayerMissionsAsync failed {PlayerId}", playerId); throw; }
}
public async Task<PlayerMission?> GetPlayerMissionAsync(Guid playerId, Guid missionId)
{
try { return await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.MissionId == missionId).FirstOrDefaultAsync(); }
catch (Exception ex) { Log.Error(ex, "GetPlayerMissionAsync failed {MissionId} {PlayerId}", missionId, playerId); throw; }
}
public async Task<PlayerProgress> GetPlayerOverallProgressAsync(Guid playerId)
{
try
{
var player = await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException("Player not found");
var missions = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId).ToListAsync();
var completed = missions.Count(m => m.Completed != null);
var totalMissions = await _uow.Missions.Query().CountAsync();
var skillLevels = await _uow.PlayerSkills.Query(ps => ps.PlayerId == playerId, null, ps => ps.Skill)
.ToDictionaryAsync(ps => ps.Skill.Title, ps => ps.Score);
return new PlayerProgress
{
PlayerId = playerId,
PlayerName = playerId.ToString(),
CurrentRank = await _uow.Ranks.GetByIdAsync(player.RankId),
TotalExperience = player.Experience,
TotalMana = player.Mana,
CompletedMissions = completed,
TotalAvailableMissions = totalMissions,
SkillLevels = skillLevels
};
}
catch (Exception ex) { Log.Error(ex, "GetPlayerOverallProgressAsync failed {PlayerId}", playerId); throw; }
}
}

View File

@@ -1,75 +1,75 @@
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Services.Interfaces;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Services;
public class RankService : IRankService
{
private readonly IUnitOfWork _uow;
public RankService(IUnitOfWork uow) => _uow = uow;
public async Task<Rank?> GetRankByIdAsync(Guid rankId)
{
try { return await _uow.Ranks.GetByIdAsync(rankId); }
catch (Exception ex) { Log.Error(ex, "GetRankByIdAsync failed {RankId}", rankId); throw; }
}
public async Task<Rank?> GetRankByTitleAsync(string title)
{
try { return await _uow.Ranks.Query(r => r.Title == title).FirstOrDefaultAsync(); }
catch (Exception ex) { Log.Error(ex, "GetRankByTitleAsync failed {Title}", title); throw; }
}
public async Task<IEnumerable<Rank>> GetAllRanksAsync()
{
try { return await _uow.Ranks.Query().OrderBy(r => r.ExpNeeded).ToListAsync(); }
catch (Exception ex) { Log.Error(ex, "GetAllRanksAsync failed"); throw; }
}
public async Task<Rank> CreateRankAsync(Rank rank)
{
try { rank.Id = Guid.NewGuid(); await _uow.Ranks.AddAsync(rank); await _uow.SaveChangesAsync(); return rank; }
catch (Exception ex) { Log.Error(ex, "CreateRankAsync failed {Title}", rank.Title); throw; }
}
public async Task<Rank> UpdateRankAsync(Rank rank)
{
try { _uow.Ranks.Update(rank); await _uow.SaveChangesAsync(); return rank; }
catch (Exception ex) { Log.Error(ex, "UpdateRankAsync failed {RankId}", rank.Id); throw; }
}
public async Task<bool> DeleteRankAsync(Guid rankId)
{
try { var r = await _uow.Ranks.GetByIdAsync(rankId); if (r == null) return false; _uow.Ranks.Remove(r); await _uow.SaveChangesAsync(); return true; }
catch (Exception ex) { Log.Error(ex, "DeleteRankAsync failed {RankId}", rankId); throw; }
}
public async Task<bool> CanPlayerAdvanceToRankAsync(Guid playerId, Guid rankId)
{
try {
var player = await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException("Player not found");
var rank = await _uow.Ranks.GetByIdAsync(rankId) ?? throw new KeyNotFoundException("Rank not found");
if (player.Experience < rank.ExpNeeded) return false;
var missionReqs = await _uow.RankMissionRules.Query(rmr => rmr.RankId == rankId).Select(r => r.MissionId).ToListAsync();
if (missionReqs.Count > 0)
{
var completed = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.Completed != null).Select(pm => pm.MissionId).ToListAsync();
if (missionReqs.Except(completed).Any()) return false;
}
var skillReqs = await _uow.RankSkillRules.Query(rsr => rsr.RankId == rankId).ToListAsync();
if (skillReqs.Count > 0)
{
var playerSkills = await _uow.PlayerSkills.Query(ps => ps.PlayerId == playerId).ToListAsync();
foreach (var req in skillReqs)
{
var ps = playerSkills.FirstOrDefault(s => s.SkillId == req.SkillId);
if (ps == null || ps.Score < req.Min) return false;
}
}
return true; }
catch (Exception ex) { Log.Error(ex, "CanPlayerAdvanceToRankAsync failed {PlayerId}->{RankId}", playerId, rankId); throw; }
}
public async Task<Rank?> GetNextRankAsync(Guid currentRankId)
{
try {
var current = await _uow.Ranks.GetByIdAsync(currentRankId); if (current == null) return null; return await _uow.Ranks.Query(r => r.ExpNeeded > current.ExpNeeded).OrderBy(r => r.ExpNeeded).FirstOrDefaultAsync(); }
catch (Exception ex) { Log.Error(ex, "GetNextRankAsync failed {RankId}", currentRankId); throw; }
}
}
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Services.Interfaces;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Services;
public class RankService : IRankService
{
private readonly IUnitOfWork _uow;
public RankService(IUnitOfWork uow) => _uow = uow;
public async Task<Rank?> GetRankByIdAsync(Guid rankId)
{
try { return await _uow.Ranks.GetByIdAsync(rankId); }
catch (Exception ex) { Log.Error(ex, "GetRankByIdAsync failed {RankId}", rankId); throw; }
}
public async Task<Rank?> GetRankByTitleAsync(string title)
{
try { return await _uow.Ranks.Query(r => r.Title == title).FirstOrDefaultAsync(); }
catch (Exception ex) { Log.Error(ex, "GetRankByTitleAsync failed {Title}", title); throw; }
}
public async Task<IEnumerable<Rank>> GetAllRanksAsync()
{
try { return await _uow.Ranks.Query().OrderBy(r => r.ExpNeeded).ToListAsync(); }
catch (Exception ex) { Log.Error(ex, "GetAllRanksAsync failed"); throw; }
}
public async Task<Rank> CreateRankAsync(Rank rank)
{
try { rank.Id = Guid.NewGuid(); await _uow.Ranks.AddAsync(rank); await _uow.SaveChangesAsync(); return rank; }
catch (Exception ex) { Log.Error(ex, "CreateRankAsync failed {Title}", rank.Title); throw; }
}
public async Task<Rank> UpdateRankAsync(Rank rank)
{
try { _uow.Ranks.Update(rank); await _uow.SaveChangesAsync(); return rank; }
catch (Exception ex) { Log.Error(ex, "UpdateRankAsync failed {RankId}", rank.Id); throw; }
}
public async Task<bool> DeleteRankAsync(Guid rankId)
{
try { var r = await _uow.Ranks.GetByIdAsync(rankId); if (r == null) return false; _uow.Ranks.Remove(r); await _uow.SaveChangesAsync(); return true; }
catch (Exception ex) { Log.Error(ex, "DeleteRankAsync failed {RankId}", rankId); throw; }
}
public async Task<bool> CanPlayerAdvanceToRankAsync(Guid playerId, Guid rankId)
{
try {
var player = await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException("Player not found");
var rank = await _uow.Ranks.GetByIdAsync(rankId) ?? throw new KeyNotFoundException("Rank not found");
if (player.Experience < rank.ExpNeeded) return false;
var missionReqs = await _uow.RankMissionRules.Query(rmr => rmr.RankId == rankId).Select(r => r.MissionId).ToListAsync();
if (missionReqs.Count > 0)
{
var completed = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.Completed != null).Select(pm => pm.MissionId).ToListAsync();
if (missionReqs.Except(completed).Any()) return false;
}
var skillReqs = await _uow.RankSkillRules.Query(rsr => rsr.RankId == rankId).ToListAsync();
if (skillReqs.Count > 0)
{
var playerSkills = await _uow.PlayerSkills.Query(ps => ps.PlayerId == playerId).ToListAsync();
foreach (var req in skillReqs)
{
var ps = playerSkills.FirstOrDefault(s => s.SkillId == req.SkillId);
if (ps == null || ps.Score < req.Min) return false;
}
}
return true; }
catch (Exception ex) { Log.Error(ex, "CanPlayerAdvanceToRankAsync failed {PlayerId}->{RankId}", playerId, rankId); throw; }
}
public async Task<Rank?> GetNextRankAsync(Guid currentRankId)
{
try {
var current = await _uow.Ranks.GetByIdAsync(currentRankId); if (current == null) return null; return await _uow.Ranks.Query(r => r.ExpNeeded > current.ExpNeeded).OrderBy(r => r.ExpNeeded).FirstOrDefaultAsync(); }
catch (Exception ex) { Log.Error(ex, "GetNextRankAsync failed {RankId}", currentRankId); throw; }
}
}

View File

@@ -1,92 +1,92 @@
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Services.Interfaces;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Services;
public class RewardService : IRewardService
{
private readonly IUnitOfWork _uow;
public RewardService(IUnitOfWork uow) => _uow = uow;
public async Task<IEnumerable<MissionSkillReward>> GetMissionSkillRewardsAsync(Guid missionId)
{
try { return await _uow.MissionSkillRewards.Query(r => r.MissionId == missionId, null, r => r.Skill).ToListAsync(); }
catch (Exception ex) { Log.Error(ex, "GetMissionSkillRewardsAsync failed {MissionId}", missionId); throw; }
}
public async Task<IEnumerable<MissionItemReward>> GetMissionItemRewardsAsync(Guid missionId)
{
try { return await _uow.MissionItemRewards.Query(r => r.MissionId == missionId).ToListAsync(); }
catch (Exception ex) { Log.Error(ex, "GetMissionItemRewardsAsync failed {MissionId}", missionId); throw; }
}
public async Task DistributeMissionRewardsAsync(Guid missionId, Guid playerId)
{
try
{
var mission = await _uow.Missions.GetByIdAsync(missionId) ?? throw new KeyNotFoundException("Mission not found");
var player = await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException("Player not found");
player.Experience += mission.ExpReward;
player.Mana += mission.ManaReward;
// Skill rewards
var skillRewards = await _uow.MissionSkillRewards.Query(r => r.MissionId == missionId).ToListAsync();
foreach (var sr in skillRewards)
{
var ps = await _uow.PlayerSkills.Query(x => x.PlayerId == playerId && x.SkillId == sr.SkillId).FirstOrDefaultAsync();
if (ps == null)
{
ps = new PlayerSkill { Id = Guid.NewGuid(), PlayerId = playerId, SkillId = sr.SkillId, Score = sr.Value };
await _uow.PlayerSkills.AddAsync(ps);
}
else
{
ps.Score += sr.Value;
_uow.PlayerSkills.Update(ps);
}
}
// Item rewards (store items) one each
var itemRewards = await _uow.MissionItemRewards.Query(r => r.MissionId == missionId).ToListAsync();
foreach (var ir in itemRewards)
{
var inv = await _uow.UserInventoryItems.FindAsync(player.UserId, ir.ItemId);
if (inv == null)
{
inv = new UserInventoryItem { UserId = player.UserId, StoreItemId = ir.ItemId, Quantity = 1, AcquiredAt = DateTime.UtcNow };
await _uow.UserInventoryItems.AddAsync(inv);
}
else inv.Quantity += 1;
}
// Mark redeemed
var pm = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.MissionId == missionId).FirstOrDefaultAsync();
if (pm != null && pm.RewardsRedeemed == null)
{
pm.RewardsRedeemed = DateTime.UtcNow;
_uow.PlayerMissions.Update(pm);
}
}
catch (Exception ex)
{
Log.Error(ex, "DistributeMissionRewardsAsync failed {MissionId} {PlayerId}", missionId, playerId);
throw;
}
}
public async Task<bool> CanClaimRewardAsync(Guid rewardId, Guid playerId)
{
try
{
// Interpret rewardId as missionId; claim if mission completed and rewards not yet redeemed
var pm = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.MissionId == rewardId).FirstOrDefaultAsync();
if (pm == null || pm.Completed == null) return false;
return pm.RewardsRedeemed == null;
}
catch (Exception ex) { Log.Error(ex, "CanClaimRewardAsync failed {RewardId} {PlayerId}", rewardId, playerId); throw; }
}
}
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Services.Interfaces;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Services;
public class RewardService : IRewardService
{
private readonly IUnitOfWork _uow;
public RewardService(IUnitOfWork uow) => _uow = uow;
public async Task<IEnumerable<MissionSkillReward>> GetMissionSkillRewardsAsync(Guid missionId)
{
try { return await _uow.MissionSkillRewards.Query(r => r.MissionId == missionId, null, r => r.Skill).ToListAsync(); }
catch (Exception ex) { Log.Error(ex, "GetMissionSkillRewardsAsync failed {MissionId}", missionId); throw; }
}
public async Task<IEnumerable<MissionItemReward>> GetMissionItemRewardsAsync(Guid missionId)
{
try { return await _uow.MissionItemRewards.Query(r => r.MissionId == missionId).ToListAsync(); }
catch (Exception ex) { Log.Error(ex, "GetMissionItemRewardsAsync failed {MissionId}", missionId); throw; }
}
public async Task DistributeMissionRewardsAsync(Guid missionId, Guid playerId)
{
try
{
var mission = await _uow.Missions.GetByIdAsync(missionId) ?? throw new KeyNotFoundException("Mission not found");
var player = await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException("Player not found");
player.Experience += mission.ExpReward;
player.Mana += mission.ManaReward;
// Skill rewards
var skillRewards = await _uow.MissionSkillRewards.Query(r => r.MissionId == missionId).ToListAsync();
foreach (var sr in skillRewards)
{
var ps = await _uow.PlayerSkills.Query(x => x.PlayerId == playerId && x.SkillId == sr.SkillId).FirstOrDefaultAsync();
if (ps == null)
{
ps = new PlayerSkill { Id = Guid.NewGuid(), PlayerId = playerId, SkillId = sr.SkillId, Score = sr.Value };
await _uow.PlayerSkills.AddAsync(ps);
}
else
{
ps.Score += sr.Value;
_uow.PlayerSkills.Update(ps);
}
}
// Item rewards (store items) one each
var itemRewards = await _uow.MissionItemRewards.Query(r => r.MissionId == missionId).ToListAsync();
foreach (var ir in itemRewards)
{
var inv = await _uow.UserInventoryItems.FindAsync(player.UserId, ir.ItemId);
if (inv == null)
{
inv = new UserInventoryItem { UserId = player.UserId, StoreItemId = ir.ItemId, Quantity = 1, AcquiredAt = DateTime.UtcNow };
await _uow.UserInventoryItems.AddAsync(inv);
}
else inv.Quantity += 1;
}
// Mark redeemed
var pm = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.MissionId == missionId).FirstOrDefaultAsync();
if (pm != null && pm.RewardsRedeemed == null)
{
pm.RewardsRedeemed = DateTime.UtcNow;
_uow.PlayerMissions.Update(pm);
}
}
catch (Exception ex)
{
Log.Error(ex, "DistributeMissionRewardsAsync failed {MissionId} {PlayerId}", missionId, playerId);
throw;
}
}
public async Task<bool> CanClaimRewardAsync(Guid rewardId, Guid playerId)
{
try
{
// Interpret rewardId as missionId; claim if mission completed and rewards not yet redeemed
var pm = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.MissionId == rewardId).FirstOrDefaultAsync();
if (pm == null || pm.Completed == null) return false;
return pm.RewardsRedeemed == null;
}
catch (Exception ex) { Log.Error(ex, "CanClaimRewardAsync failed {RewardId} {PlayerId}", rewardId, playerId); throw; }
}
}

View File

@@ -1,64 +1,64 @@
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Services.Interfaces;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Services;
public class RuleValidationService : IRuleValidationService
{
private readonly IUnitOfWork _uow;
public RuleValidationService(IUnitOfWork uow) => _uow = uow;
public async Task<bool> ValidateMissionRankRulesAsync(Guid missionId, Guid playerId)
{
try
{
var player = await _uow.Players.GetByIdAsync(playerId);
if (player == null) return false;
var rankRules = await _uow.MissionRankRules.Query(r => r.MissionId == missionId).Select(r => r.RankId).ToListAsync();
if (rankRules.Count == 0) return true; // no restriction
return rankRules.Contains(player.RankId);
}
catch (Exception ex) { Log.Error(ex, "ValidateMissionRankRulesAsync failed {MissionId} {PlayerId}", missionId, playerId); throw; }
}
public async Task<bool> ValidateRankAdvancementRulesAsync(Guid playerId, Guid targetRankId)
{
try
{
var player = await _uow.Players.GetByIdAsync(playerId);
if (player == null) return false;
var rank = await _uow.Ranks.GetByIdAsync(targetRankId);
if (rank == null) return false;
if (player.Experience < rank.ExpNeeded) return false;
// required missions
var missionReqs = await _uow.RankMissionRules.Query(r => r.RankId == targetRankId).Select(r => r.MissionId).ToListAsync();
if (missionReqs.Count > 0)
{
var completed = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.Completed != null).Select(pm => pm.MissionId).ToListAsync();
if (missionReqs.Except(completed).Any()) return false;
}
// required skills
var skillReqs = await _uow.RankSkillRules.Query(r => r.RankId == targetRankId).ToListAsync();
if (skillReqs.Count > 0)
{
var playerSkills = await _uow.PlayerSkills.Query(ps => ps.PlayerId == playerId).ToListAsync();
foreach (var req in skillReqs)
{
var ps = playerSkills.FirstOrDefault(s => s.SkillId == req.SkillId);
if (ps == null || ps.Score < req.Min) return false;
}
}
return true;
}
catch (Exception ex) { Log.Error(ex, "ValidateRankAdvancementRulesAsync failed {PlayerId}->{RankId}", playerId, targetRankId); throw; }
}
public async Task<IEnumerable<MissionRankRule>> GetApplicableRankRulesAsync(Guid missionId)
{
try { return await _uow.MissionRankRules.Query(r => r.MissionId == missionId, null, r => r.Rank).ToListAsync(); }
catch (Exception ex) { Log.Error(ex, "GetApplicableRankRulesAsync failed {MissionId}", missionId); throw; }
}
}
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Services.Interfaces;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Services;
public class RuleValidationService : IRuleValidationService
{
private readonly IUnitOfWork _uow;
public RuleValidationService(IUnitOfWork uow) => _uow = uow;
public async Task<bool> ValidateMissionRankRulesAsync(Guid missionId, Guid playerId)
{
try
{
var player = await _uow.Players.GetByIdAsync(playerId);
if (player == null) return false;
var rankRules = await _uow.MissionRankRules.Query(r => r.MissionId == missionId).Select(r => r.RankId).ToListAsync();
if (rankRules.Count == 0) return true; // no restriction
return rankRules.Contains(player.RankId);
}
catch (Exception ex) { Log.Error(ex, "ValidateMissionRankRulesAsync failed {MissionId} {PlayerId}", missionId, playerId); throw; }
}
public async Task<bool> ValidateRankAdvancementRulesAsync(Guid playerId, Guid targetRankId)
{
try
{
var player = await _uow.Players.GetByIdAsync(playerId);
if (player == null) return false;
var rank = await _uow.Ranks.GetByIdAsync(targetRankId);
if (rank == null) return false;
if (player.Experience < rank.ExpNeeded) return false;
// required missions
var missionReqs = await _uow.RankMissionRules.Query(r => r.RankId == targetRankId).Select(r => r.MissionId).ToListAsync();
if (missionReqs.Count > 0)
{
var completed = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.Completed != null).Select(pm => pm.MissionId).ToListAsync();
if (missionReqs.Except(completed).Any()) return false;
}
// required skills
var skillReqs = await _uow.RankSkillRules.Query(r => r.RankId == targetRankId).ToListAsync();
if (skillReqs.Count > 0)
{
var playerSkills = await _uow.PlayerSkills.Query(ps => ps.PlayerId == playerId).ToListAsync();
foreach (var req in skillReqs)
{
var ps = playerSkills.FirstOrDefault(s => s.SkillId == req.SkillId);
if (ps == null || ps.Score < req.Min) return false;
}
}
return true;
}
catch (Exception ex) { Log.Error(ex, "ValidateRankAdvancementRulesAsync failed {PlayerId}->{RankId}", playerId, targetRankId); throw; }
}
public async Task<IEnumerable<MissionRankRule>> GetApplicableRankRulesAsync(Guid missionId)
{
try { return await _uow.MissionRankRules.Query(r => r.MissionId == missionId, null, r => r.Rank).ToListAsync(); }
catch (Exception ex) { Log.Error(ex, "GetApplicableRankRulesAsync failed {MissionId}", missionId); throw; }
}
}

View File

@@ -1,102 +1,102 @@
using Amazon.S3;
using Amazon.S3.Model;
using Amazon;
using Microsoft.Extensions.Options;
using LctMonolith.Application.Options;
using LctMonolith.Services.Interfaces;
using Serilog;
namespace LctMonolith.Services;
public class S3FileStorageService : IFileStorageService, IDisposable
{
private readonly S3StorageOptions _opts;
private readonly IAmazonS3 _client;
private bool _bucketChecked;
public S3FileStorageService(IOptions<S3StorageOptions> options)
{
_opts = options.Value;
var cfg = new AmazonS3Config
{
ServiceURL = _opts.Endpoint,
ForcePathStyle = true,
UseHttp = !_opts.UseSsl,
Timeout = TimeSpan.FromSeconds(30),
MaxErrorRetry = 2,
};
_client = new AmazonS3Client(_opts.AccessKey, _opts.SecretKey, cfg);
}
private async Task EnsureBucketAsync(CancellationToken ct)
{
if (_bucketChecked) return;
try
{
var list = await _client.ListBucketsAsync(ct);
if (!list.Buckets.Any(b => string.Equals(b.BucketName, _opts.Bucket, StringComparison.OrdinalIgnoreCase)))
{
await _client.PutBucketAsync(new PutBucketRequest { BucketName = _opts.Bucket }, ct);
}
_bucketChecked = true;
}
catch (Exception ex)
{
Log.Error(ex, "Failed ensuring bucket {Bucket}", _opts.Bucket);
throw;
}
}
public async Task<string> UploadAsync(Stream content, string contentType, string keyPrefix, CancellationToken ct = default)
{
await EnsureBucketAsync(ct);
var key = $"{keyPrefix.Trim('/')}/{DateTime.UtcNow:yyyyMMdd}/{Guid.NewGuid():N}";
var putReq = new PutObjectRequest
{
BucketName = _opts.Bucket,
Key = key,
InputStream = content,
ContentType = contentType
};
await _client.PutObjectAsync(putReq, ct);
Log.Information("Uploaded object {Key} to bucket {Bucket}", key, _opts.Bucket);
return key;
}
public async Task DeleteAsync(string key, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(key)) return;
try
{
await _client.DeleteObjectAsync(_opts.Bucket, key, ct);
Log.Information("Deleted object {Key}", key);
}
catch (AmazonS3Exception ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
// ignore
}
}
public Task<string> GetPresignedUrlAsync(string key, TimeSpan? expires = null, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(key)) throw new ArgumentNullException(nameof(key));
if (!string.IsNullOrWhiteSpace(_opts.PublicBaseUrl))
{
var url = _opts.PublicBaseUrl!.TrimEnd('/') + "/" + key;
return Task.FromResult(url);
}
var req = new GetPreSignedUrlRequest
{
BucketName = _opts.Bucket,
Key = key,
Expires = DateTime.UtcNow.Add(expires ?? TimeSpan.FromMinutes(_opts.PresignExpirationMinutes))
};
var urlSigned = _client.GetPreSignedURL(req);
return Task.FromResult(urlSigned);
}
public void Dispose()
{
_client.Dispose();
}
}
using Amazon.S3;
using Amazon.S3.Model;
using Amazon;
using Microsoft.Extensions.Options;
using LctMonolith.Application.Options;
using LctMonolith.Services.Interfaces;
using Serilog;
namespace LctMonolith.Services;
public class S3FileStorageService : IFileStorageService, IDisposable
{
private readonly S3StorageOptions _opts;
private readonly IAmazonS3 _client;
private bool _bucketChecked;
public S3FileStorageService(IOptions<S3StorageOptions> options)
{
_opts = options.Value;
var cfg = new AmazonS3Config
{
ServiceURL = _opts.Endpoint,
ForcePathStyle = true,
UseHttp = !_opts.UseSsl,
Timeout = TimeSpan.FromSeconds(30),
MaxErrorRetry = 2,
};
_client = new AmazonS3Client(_opts.AccessKey, _opts.SecretKey, cfg);
}
private async Task EnsureBucketAsync(CancellationToken ct)
{
if (_bucketChecked) return;
try
{
var list = await _client.ListBucketsAsync(ct);
if (!list.Buckets.Any(b => string.Equals(b.BucketName, _opts.Bucket, StringComparison.OrdinalIgnoreCase)))
{
await _client.PutBucketAsync(new PutBucketRequest { BucketName = _opts.Bucket }, ct);
}
_bucketChecked = true;
}
catch (Exception ex)
{
Log.Error(ex, "Failed ensuring bucket {Bucket}", _opts.Bucket);
throw;
}
}
public async Task<string> UploadAsync(Stream content, string contentType, string keyPrefix, CancellationToken ct = default)
{
await EnsureBucketAsync(ct);
var key = $"{keyPrefix.Trim('/')}/{DateTime.UtcNow:yyyyMMdd}/{Guid.NewGuid():N}";
var putReq = new PutObjectRequest
{
BucketName = _opts.Bucket,
Key = key,
InputStream = content,
ContentType = contentType
};
await _client.PutObjectAsync(putReq, ct);
Log.Information("Uploaded object {Key} to bucket {Bucket}", key, _opts.Bucket);
return key;
}
public async Task DeleteAsync(string key, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(key)) return;
try
{
await _client.DeleteObjectAsync(_opts.Bucket, key, ct);
Log.Information("Deleted object {Key}", key);
}
catch (AmazonS3Exception ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
// ignore
}
}
public Task<string> GetPresignedUrlAsync(string key, TimeSpan? expires = null, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(key)) throw new ArgumentNullException(nameof(key));
if (!string.IsNullOrWhiteSpace(_opts.PublicBaseUrl))
{
var url = _opts.PublicBaseUrl!.TrimEnd('/') + "/" + key;
return Task.FromResult(url);
}
var req = new GetPreSignedUrlRequest
{
BucketName = _opts.Bucket,
Key = key,
Expires = DateTime.UtcNow.Add(expires ?? TimeSpan.FromMinutes(_opts.PresignExpirationMinutes))
};
var urlSigned = _client.GetPreSignedURL(req);
return Task.FromResult(urlSigned);
}
public void Dispose()
{
_client.Dispose();
}
}

View File

@@ -1,59 +1,59 @@
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Services.Interfaces;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Services;
public class SkillService : ISkillService
{
private readonly IUnitOfWork _uow;
public SkillService(IUnitOfWork uow) => _uow = uow;
public async Task<Skill?> GetSkillByIdAsync(Guid skillId)
{
try { return await _uow.Skills.GetByIdAsync(skillId); }
catch (Exception ex) { Log.Error(ex, "GetSkillByIdAsync failed {SkillId}", skillId); throw; }
}
public async Task<Skill?> GetSkillByTitleAsync(string title)
{
try { return await _uow.Skills.Query(s => s.Title == title).FirstOrDefaultAsync(); }
catch (Exception ex) { Log.Error(ex, "GetSkillByTitleAsync failed {Title}", title); throw; }
}
public async Task<IEnumerable<Skill>> GetAllSkillsAsync()
{
try { return await _uow.Skills.Query().OrderBy(s => s.Title).ToListAsync(); }
catch (Exception ex) { Log.Error(ex, "GetAllSkillsAsync failed"); throw; }
}
public async Task<Skill> CreateSkillAsync(Skill skill)
{
try { skill.Id = Guid.NewGuid(); await _uow.Skills.AddAsync(skill); await _uow.SaveChangesAsync(); return skill; }
catch (Exception ex) { Log.Error(ex, "CreateSkillAsync failed {Title}", skill.Title); throw; }
}
public async Task<Skill> UpdateSkillAsync(Skill skill)
{
try { _uow.Skills.Update(skill); await _uow.SaveChangesAsync(); return skill; }
catch (Exception ex) { Log.Error(ex, "UpdateSkillAsync failed {SkillId}", skill.Id); throw; }
}
public async Task<bool> DeleteSkillAsync(Guid skillId)
{
try { var skill = await _uow.Skills.GetByIdAsync(skillId); if (skill == null) return false; _uow.Skills.Remove(skill); await _uow.SaveChangesAsync(); return true; }
catch (Exception ex) { Log.Error(ex, "DeleteSkillAsync failed {SkillId}", skillId); throw; }
}
public async Task<PlayerSkill> UpdatePlayerSkillAsync(Guid playerId, Guid skillId, int level)
{
try { var ps = await _uow.PlayerSkills.Query(x => x.PlayerId == playerId && x.SkillId == skillId).FirstOrDefaultAsync(); if (ps == null) { ps = new PlayerSkill { Id = Guid.NewGuid(), PlayerId = playerId, SkillId = skillId, Score = level }; await _uow.PlayerSkills.AddAsync(ps); } else { ps.Score = level; _uow.PlayerSkills.Update(ps); } await _uow.SaveChangesAsync(); return ps; }
catch (Exception ex) { Log.Error(ex, "UpdatePlayerSkillAsync failed {PlayerId} {SkillId}", playerId, skillId); throw; }
}
public async Task<IEnumerable<PlayerSkill>> GetPlayerSkillsAsync(Guid playerId)
{
try { return await _uow.PlayerSkills.Query(ps => ps.PlayerId == playerId, null, ps => ps.Skill).ToListAsync(); }
catch (Exception ex) { Log.Error(ex, "GetPlayerSkillsAsync failed {PlayerId}", playerId); throw; }
}
public async Task<int> GetPlayerSkillLevelAsync(Guid playerId, Guid skillId)
{
try { var ps = await _uow.PlayerSkills.Query(x => x.PlayerId == playerId && x.SkillId == skillId).FirstOrDefaultAsync(); return ps?.Score ?? 0; }
catch (Exception ex) { Log.Error(ex, "GetPlayerSkillLevelAsync failed {PlayerId} {SkillId}", playerId, skillId); throw; }
}
}
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Services.Interfaces;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace LctMonolith.Services;
public class SkillService : ISkillService
{
private readonly IUnitOfWork _uow;
public SkillService(IUnitOfWork uow) => _uow = uow;
public async Task<Skill?> GetSkillByIdAsync(Guid skillId)
{
try { return await _uow.Skills.GetByIdAsync(skillId); }
catch (Exception ex) { Log.Error(ex, "GetSkillByIdAsync failed {SkillId}", skillId); throw; }
}
public async Task<Skill?> GetSkillByTitleAsync(string title)
{
try { return await _uow.Skills.Query(s => s.Title == title).FirstOrDefaultAsync(); }
catch (Exception ex) { Log.Error(ex, "GetSkillByTitleAsync failed {Title}", title); throw; }
}
public async Task<IEnumerable<Skill>> GetAllSkillsAsync()
{
try { return await _uow.Skills.Query().OrderBy(s => s.Title).ToListAsync(); }
catch (Exception ex) { Log.Error(ex, "GetAllSkillsAsync failed"); throw; }
}
public async Task<Skill> CreateSkillAsync(Skill skill)
{
try { skill.Id = Guid.NewGuid(); await _uow.Skills.AddAsync(skill); await _uow.SaveChangesAsync(); return skill; }
catch (Exception ex) { Log.Error(ex, "CreateSkillAsync failed {Title}", skill.Title); throw; }
}
public async Task<Skill> UpdateSkillAsync(Skill skill)
{
try { _uow.Skills.Update(skill); await _uow.SaveChangesAsync(); return skill; }
catch (Exception ex) { Log.Error(ex, "UpdateSkillAsync failed {SkillId}", skill.Id); throw; }
}
public async Task<bool> DeleteSkillAsync(Guid skillId)
{
try { var skill = await _uow.Skills.GetByIdAsync(skillId); if (skill == null) return false; _uow.Skills.Remove(skill); await _uow.SaveChangesAsync(); return true; }
catch (Exception ex) { Log.Error(ex, "DeleteSkillAsync failed {SkillId}", skillId); throw; }
}
public async Task<PlayerSkill> UpdatePlayerSkillAsync(Guid playerId, Guid skillId, int level)
{
try { var ps = await _uow.PlayerSkills.Query(x => x.PlayerId == playerId && x.SkillId == skillId).FirstOrDefaultAsync(); if (ps == null) { ps = new PlayerSkill { Id = Guid.NewGuid(), PlayerId = playerId, SkillId = skillId, Score = level }; await _uow.PlayerSkills.AddAsync(ps); } else { ps.Score = level; _uow.PlayerSkills.Update(ps); } await _uow.SaveChangesAsync(); return ps; }
catch (Exception ex) { Log.Error(ex, "UpdatePlayerSkillAsync failed {PlayerId} {SkillId}", playerId, skillId); throw; }
}
public async Task<IEnumerable<PlayerSkill>> GetPlayerSkillsAsync(Guid playerId)
{
try { return await _uow.PlayerSkills.Query(ps => ps.PlayerId == playerId, null, ps => ps.Skill).ToListAsync(); }
catch (Exception ex) { Log.Error(ex, "GetPlayerSkillsAsync failed {PlayerId}", playerId); throw; }
}
public async Task<int> GetPlayerSkillLevelAsync(Guid playerId, Guid skillId)
{
try { var ps = await _uow.PlayerSkills.Query(x => x.PlayerId == playerId && x.SkillId == skillId).FirstOrDefaultAsync(); return ps?.Score ?? 0; }
catch (Exception ex) { Log.Error(ex, "GetPlayerSkillLevelAsync failed {PlayerId} {SkillId}", playerId, skillId); throw; }
}
}

View File

@@ -1,62 +1,62 @@
using LctMonolith.Models.Database;
using Microsoft.EntityFrameworkCore;
using Serilog;
using System.Text.Json;
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Services.Contracts;
namespace LctMonolith.Services;
/// <summary>
/// Store purchase operations and inventory management.
/// </summary>
public class StoreService : IStoreService
{
private readonly IUnitOfWork _uow;
public StoreService(IUnitOfWork uow)
{
_uow = uow;
}
public async Task<IEnumerable<StoreItem>> GetActiveItemsAsync(CancellationToken ct = default)
{
try { return await _uow.StoreItems.Query(i => i.IsActive).ToListAsync(ct); }
catch (Exception ex) { Log.Error(ex, "GetActiveItemsAsync failed"); throw; }
}
public async Task<UserInventoryItem> PurchaseAsync(Guid userId, Guid itemId, int quantity, CancellationToken ct = default)
{
try
{
if (quantity <= 0) throw new ArgumentOutOfRangeException(nameof(quantity));
var player = await _uow.Players.Query(p => p.UserId == userId).FirstOrDefaultAsync(ct) ?? throw new KeyNotFoundException("Player not found for user");
var item = await _uow.StoreItems.Query(i => i.Id == itemId && i.IsActive).FirstOrDefaultAsync(ct) ?? throw new KeyNotFoundException("Item not found or inactive");
var totalPrice = item.Price * quantity;
if (player.Mana < totalPrice) throw new InvalidOperationException("Insufficient mana");
if (item.Stock.HasValue && item.Stock.Value < quantity) throw new InvalidOperationException("Insufficient stock");
player.Mana -= totalPrice;
if (item.Stock.HasValue) item.Stock -= quantity;
var inv = await _uow.UserInventoryItems.FindAsync(userId, itemId);
if (inv == null)
{
inv = new UserInventoryItem { UserId = userId, StoreItemId = itemId, Quantity = quantity, AcquiredAt = DateTime.UtcNow };
await _uow.UserInventoryItems.AddAsync(inv, ct);
}
else inv.Quantity += quantity;
await _uow.Transactions.AddAsync(new Transaction { UserId = userId, StoreItemId = itemId, Type = TransactionType.Purchase, ManaAmount = -totalPrice }, ct);
await _uow.EventLogs.AddAsync(new EventLog { Type = EventType.ItemPurchased, UserId = userId, Data = JsonSerializer.Serialize(new { itemId, quantity, totalPrice }) }, ct);
await _uow.SaveChangesAsync(ct);
Log.Information("User {User} purchased {Qty} of {Item}", userId, quantity, itemId);
return inv;
}
catch (Exception ex)
{
Log.Error(ex, "PurchaseAsync failed {UserId} {ItemId}", userId, itemId);
throw;
}
}
}
using LctMonolith.Models.Database;
using Microsoft.EntityFrameworkCore;
using Serilog;
using System.Text.Json;
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Services.Contracts;
namespace LctMonolith.Services;
/// <summary>
/// Store purchase operations and inventory management.
/// </summary>
public class StoreService : IStoreService
{
private readonly IUnitOfWork _uow;
public StoreService(IUnitOfWork uow)
{
_uow = uow;
}
public async Task<IEnumerable<StoreItem>> GetActiveItemsAsync(CancellationToken ct = default)
{
try { return await _uow.StoreItems.Query(i => i.IsActive).ToListAsync(ct); }
catch (Exception ex) { Log.Error(ex, "GetActiveItemsAsync failed"); throw; }
}
public async Task<UserInventoryItem> PurchaseAsync(Guid userId, Guid itemId, int quantity, CancellationToken ct = default)
{
try
{
if (quantity <= 0) throw new ArgumentOutOfRangeException(nameof(quantity));
var player = await _uow.Players.Query(p => p.UserId == userId).FirstOrDefaultAsync(ct) ?? throw new KeyNotFoundException("Player not found for user");
var item = await _uow.StoreItems.Query(i => i.Id == itemId && i.IsActive).FirstOrDefaultAsync(ct) ?? throw new KeyNotFoundException("Item not found or inactive");
var totalPrice = item.Price * quantity;
if (player.Mana < totalPrice) throw new InvalidOperationException("Insufficient mana");
if (item.Stock.HasValue && item.Stock.Value < quantity) throw new InvalidOperationException("Insufficient stock");
player.Mana -= totalPrice;
if (item.Stock.HasValue) item.Stock -= quantity;
var inv = await _uow.UserInventoryItems.FindAsync(userId, itemId);
if (inv == null)
{
inv = new UserInventoryItem { UserId = userId, StoreItemId = itemId, Quantity = quantity, AcquiredAt = DateTime.UtcNow };
await _uow.UserInventoryItems.AddAsync(inv, ct);
}
else inv.Quantity += quantity;
await _uow.Transactions.AddAsync(new Transaction { UserId = userId, StoreItemId = itemId, Type = TransactionType.Purchase, ManaAmount = -totalPrice }, ct);
await _uow.EventLogs.AddAsync(new EventLog { Type = EventType.ItemPurchased, UserId = userId, Data = JsonSerializer.Serialize(new { itemId, quantity, totalPrice }) }, ct);
await _uow.SaveChangesAsync(ct);
Log.Information("User {User} purchased {Qty} of {Item}", userId, quantity, itemId);
return inv;
}
catch (Exception ex)
{
Log.Error(ex, "PurchaseAsync failed {UserId} {ItemId}", userId, itemId);
throw;
}
}
}

View File

@@ -1,123 +1,123 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using LctMonolith.Application.Options;
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Services.Contracts;
using LctMonolith.Services.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Serilog;
namespace LctMonolith.Services;
/// <summary>
/// Issues and refreshes JWT + refresh tokens.
/// </summary>
public class TokenService : ITokenService
{
private readonly IUnitOfWork _uow;
private readonly UserManager<AppUser> _userManager;
private readonly JwtOptions _options;
private readonly SigningCredentials _creds;
public TokenService(IUnitOfWork uow, UserManager<AppUser> userManager, IOptions<JwtOptions> options)
{
_uow = uow;
_userManager = userManager;
_options = options.Value;
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.Key));
_creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
}
public async Task<TokenPair> IssueAsync(AppUser user, CancellationToken ct = default)
{
try
{
var now = DateTime.UtcNow;
var accessExp = now.AddMinutes(_options.AccessTokenMinutes);
var refreshExp = now.AddDays(_options.RefreshTokenDays);
var claims = new List<Claim>
{
new(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
new(ClaimTypes.NameIdentifier, user.Id.ToString()),
new(JwtRegisteredClaimNames.Email, user.Email ?? string.Empty),
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
};
var jwt = new JwtSecurityToken(
issuer: _options.Issuer,
audience: _options.Audience,
claims: claims,
notBefore: now,
expires: accessExp,
signingCredentials: _creds);
var accessToken = new JwtSecurityTokenHandler().WriteToken(jwt);
var refreshToken = GenerateSecureToken();
var rt = new RefreshToken
{
Token = refreshToken,
UserId = user.Id,
ExpiresAt = refreshExp
};
await _uow.RefreshTokens.AddAsync(rt, ct);
await _uow.SaveChangesAsync(ct);
return new TokenPair(accessToken, accessExp, refreshToken, refreshExp);
}
catch (Exception ex)
{
Log.Error(ex, "IssueAsync failed for user {UserId}", user.Id);
throw;
}
}
public async Task<TokenPair> RefreshAsync(string refreshToken, CancellationToken ct = default)
{
try
{
var token = await _uow.RefreshTokens.Query(r => r.Token == refreshToken).FirstOrDefaultAsync(ct);
if (token == null || token.IsRevoked || token.ExpiresAt < DateTime.UtcNow)
throw new SecurityTokenException("Invalid refresh token");
var user = await _userManager.FindByIdAsync(token.UserId.ToString()) ?? throw new SecurityTokenException("User not found");
token.IsRevoked = true; // rotate
await _uow.SaveChangesAsync(ct);
return await IssueAsync(user, ct);
}
catch (Exception ex)
{
Log.Error(ex, "RefreshAsync failed");
throw;
}
}
public async Task RevokeAsync(string refreshToken, CancellationToken ct = default)
{
try
{
var token = await _uow.RefreshTokens.Query(r => r.Token == refreshToken).FirstOrDefaultAsync(ct);
if (token == null) return; // idempotent
token.IsRevoked = true;
await _uow.SaveChangesAsync(ct);
}
catch (Exception ex)
{
Log.Error(ex, "RevokeAsync failed");
throw;
}
}
private static string GenerateSecureToken()
{
Span<byte> bytes = stackalloc byte[64];
RandomNumberGenerator.Fill(bytes);
return Convert.ToBase64String(bytes);
}
}
internal static class EfAsyncExtensions
{
public static Task<T?> FirstOrDefaultAsync<T>(this IQueryable<T> query, CancellationToken ct = default) => Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.FirstOrDefaultAsync(query, ct);
}
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using LctMonolith.Application.Options;
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Services.Contracts;
using LctMonolith.Services.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Serilog;
namespace LctMonolith.Services;
/// <summary>
/// Issues and refreshes JWT + refresh tokens.
/// </summary>
public class TokenService : ITokenService
{
private readonly IUnitOfWork _uow;
private readonly UserManager<AppUser> _userManager;
private readonly JwtOptions _options;
private readonly SigningCredentials _creds;
public TokenService(IUnitOfWork uow, UserManager<AppUser> userManager, IOptions<JwtOptions> options)
{
_uow = uow;
_userManager = userManager;
_options = options.Value;
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.Key));
_creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
}
public async Task<TokenPair> IssueAsync(AppUser user, CancellationToken ct = default)
{
try
{
var now = DateTime.UtcNow;
var accessExp = now.AddMinutes(_options.AccessTokenMinutes);
var refreshExp = now.AddDays(_options.RefreshTokenDays);
var claims = new List<Claim>
{
new(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
new(ClaimTypes.NameIdentifier, user.Id.ToString()),
new(JwtRegisteredClaimNames.Email, user.Email ?? string.Empty),
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
};
var jwt = new JwtSecurityToken(
issuer: _options.Issuer,
audience: _options.Audience,
claims: claims,
notBefore: now,
expires: accessExp,
signingCredentials: _creds);
var accessToken = new JwtSecurityTokenHandler().WriteToken(jwt);
var refreshToken = GenerateSecureToken();
var rt = new RefreshToken
{
Token = refreshToken,
UserId = user.Id,
ExpiresAt = refreshExp
};
await _uow.RefreshTokens.AddAsync(rt, ct);
await _uow.SaveChangesAsync(ct);
return new TokenPair(accessToken, accessExp, refreshToken, refreshExp);
}
catch (Exception ex)
{
Log.Error(ex, "IssueAsync failed for user {UserId}", user.Id);
throw;
}
}
public async Task<TokenPair> RefreshAsync(string refreshToken, CancellationToken ct = default)
{
try
{
var token = await _uow.RefreshTokens.Query(r => r.Token == refreshToken).FirstOrDefaultAsync(ct);
if (token == null || token.IsRevoked || token.ExpiresAt < DateTime.UtcNow)
throw new SecurityTokenException("Invalid refresh token");
var user = await _userManager.FindByIdAsync(token.UserId.ToString()) ?? throw new SecurityTokenException("User not found");
token.IsRevoked = true; // rotate
await _uow.SaveChangesAsync(ct);
return await IssueAsync(user, ct);
}
catch (Exception ex)
{
Log.Error(ex, "RefreshAsync failed");
throw;
}
}
public async Task RevokeAsync(string refreshToken, CancellationToken ct = default)
{
try
{
var token = await _uow.RefreshTokens.Query(r => r.Token == refreshToken).FirstOrDefaultAsync(ct);
if (token == null) return; // idempotent
token.IsRevoked = true;
await _uow.SaveChangesAsync(ct);
}
catch (Exception ex)
{
Log.Error(ex, "RevokeAsync failed");
throw;
}
}
private static string GenerateSecureToken()
{
Span<byte> bytes = stackalloc byte[64];
RandomNumberGenerator.Fill(bytes);
return Convert.ToBase64String(bytes);
}
}
internal static class EfAsyncExtensions
{
public static Task<T?> FirstOrDefaultAsync<T>(this IQueryable<T> query, CancellationToken ct = default) => Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.FirstOrDefaultAsync(query, ct);
}