something
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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!;
|
||||
}
|
||||
|
||||
@@ -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!;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user