fix: app
This commit is contained in:
@@ -4,13 +4,14 @@ using Serilog;
|
|||||||
|
|
||||||
namespace LctMonolith.Application.Middleware;
|
namespace LctMonolith.Application.Middleware;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Global error handling middleware capturing unhandled exceptions and converting to standardized JSON response.
|
|
||||||
/// </summary>
|
|
||||||
public class ErrorHandlingMiddleware
|
public class ErrorHandlingMiddleware
|
||||||
{
|
{
|
||||||
private readonly RequestDelegate _next;
|
private readonly RequestDelegate _next;
|
||||||
public ErrorHandlingMiddleware(RequestDelegate next) => _next = next;
|
|
||||||
|
public ErrorHandlingMiddleware(RequestDelegate next)
|
||||||
|
{
|
||||||
|
_next = next;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task Invoke(HttpContext ctx)
|
public async Task Invoke(HttpContext ctx)
|
||||||
{
|
{
|
||||||
@@ -20,17 +21,18 @@ public class ErrorHandlingMiddleware
|
|||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
// Client aborted request (non-standard 499 code used by some proxies)
|
|
||||||
if (!ctx.Response.HasStarted)
|
if (!ctx.Response.HasStarted)
|
||||||
{
|
{
|
||||||
ctx.Response.StatusCode = 499; // Client Closed Request (custom)
|
ctx.Response.StatusCode = 499;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Log.Error(ex, "Unhandled exception");
|
Log.Error(ex, "Unhandled exception");
|
||||||
if (ctx.Response.HasStarted) throw;
|
if (ctx.Response.HasStarted)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
ctx.Response.ContentType = "application/json";
|
ctx.Response.ContentType = "application/json";
|
||||||
ctx.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
|
ctx.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
|
||||||
var payload = new { error = new { message = ex.Message, traceId = ctx.TraceIdentifier } };
|
var payload = new { error = new { message = ex.Message, traceId = ctx.TraceIdentifier } };
|
||||||
@@ -41,6 +43,8 @@ public class ErrorHandlingMiddleware
|
|||||||
|
|
||||||
public static class ErrorHandlingMiddlewareExtensions
|
public static class ErrorHandlingMiddlewareExtensions
|
||||||
{
|
{
|
||||||
/// <summary>Adds global error handling middleware.</summary>
|
public static IApplicationBuilder UseErrorHandling(this IApplicationBuilder app)
|
||||||
public static IApplicationBuilder UseErrorHandling(this IApplicationBuilder app) => app.UseMiddleware<ErrorHandlingMiddleware>();
|
{
|
||||||
|
return app.UseMiddleware<ErrorHandlingMiddleware>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
namespace LctMonolith.Application.Options;
|
namespace LctMonolith.Application.Options;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// JWT issuing configuration loaded from appsettings (section Jwt).
|
|
||||||
/// </summary>
|
|
||||||
public class JwtOptions
|
public class JwtOptions
|
||||||
{
|
{
|
||||||
public string Key { get; set; } = string.Empty;
|
public string Key { get; set; } = string.Empty;
|
||||||
@@ -11,4 +8,3 @@ public class JwtOptions
|
|||||||
public int AccessTokenMinutes { get; set; } = 60;
|
public int AccessTokenMinutes { get; set; } = 60;
|
||||||
public int RefreshTokenDays { get; set; } = 7;
|
public int RefreshTokenDays { get; set; } = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,21 +4,18 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
|
|
||||||
namespace LctMonolith.Controllers;
|
namespace LctMonolith.Controllers;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Basic analytics endpoints.
|
|
||||||
/// </summary>
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/analytics")]
|
[Route("api/analytics")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
public class AnalyticsController : ControllerBase
|
public class AnalyticsController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly IAnalyticsService _analytics;
|
private readonly IAnalyticsService _analytics;
|
||||||
|
|
||||||
public AnalyticsController(IAnalyticsService analytics)
|
public AnalyticsController(IAnalyticsService analytics)
|
||||||
{
|
{
|
||||||
_analytics = analytics;
|
_analytics = analytics;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Get aggregate system summary metrics.</summary>
|
|
||||||
[HttpGet("summary")]
|
[HttpGet("summary")]
|
||||||
public async Task<IActionResult> GetSummary(CancellationToken ct)
|
public async Task<IActionResult> GetSummary(CancellationToken ct)
|
||||||
{
|
{
|
||||||
@@ -26,4 +23,3 @@ public class AnalyticsController : ControllerBase
|
|||||||
return Ok(summary);
|
return Ok(summary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,9 +11,6 @@ using RefreshRequest = LctMonolith.Services.Models.RefreshRequest;
|
|||||||
|
|
||||||
namespace LctMonolith.Controllers;
|
namespace LctMonolith.Controllers;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Authentication endpoints (mocked local identity + JWT issuing).
|
|
||||||
/// </summary>
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/auth")]
|
[Route("api/auth")]
|
||||||
public class AuthController : ControllerBase
|
public class AuthController : ControllerBase
|
||||||
@@ -29,34 +26,43 @@ public class AuthController : ControllerBase
|
|||||||
_tokenService = tokenService;
|
_tokenService = tokenService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Registers a new user (simplified).</summary>
|
|
||||||
[HttpPost("register")]
|
[HttpPost("register")]
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
public async Task<ActionResult<TokenPair>> Register(AuthRequest req, CancellationToken ct)
|
public async Task<ActionResult<TokenPair>> Register(AuthRequest req, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var existing = await _userManager.FindByEmailAsync(req.Email);
|
var existing = await _userManager.FindByEmailAsync(req.Email);
|
||||||
if (existing != null) return Conflict("Email already registered");
|
if (existing != null)
|
||||||
|
{
|
||||||
|
return Conflict("Email already registered");
|
||||||
|
}
|
||||||
var user = new AppUser { UserName = req.Email, Email = req.Email, FirstName = req.FirstName, LastName = req.LastName };
|
var user = new AppUser { UserName = req.Email, Email = req.Email, FirstName = req.FirstName, LastName = req.LastName };
|
||||||
var result = await _userManager.CreateAsync(user, req.Password);
|
var result = await _userManager.CreateAsync(user, req.Password);
|
||||||
if (!result.Succeeded) return BadRequest(result.Errors);
|
if (!result.Succeeded)
|
||||||
|
{
|
||||||
|
return BadRequest(result.Errors);
|
||||||
|
}
|
||||||
var tokens = await _tokenService.IssueAsync(user, ct);
|
var tokens = await _tokenService.IssueAsync(user, ct);
|
||||||
return Ok(tokens);
|
return Ok(tokens);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Login with email + password.</summary>
|
|
||||||
[HttpPost("login")]
|
[HttpPost("login")]
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
public async Task<ActionResult<TokenPair>> Login(AuthRequest req, CancellationToken ct)
|
public async Task<ActionResult<TokenPair>> Login(AuthRequest req, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var user = await _userManager.FindByEmailAsync(req.Email);
|
var user = await _userManager.FindByEmailAsync(req.Email);
|
||||||
if (user == null) return Unauthorized();
|
if (user == null)
|
||||||
var passOk = await _signInManager.CheckPasswordSignInAsync(user, req.Password, lockoutOnFailure: false);
|
{
|
||||||
if (!passOk.Succeeded) return Unauthorized();
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
var passOk = await _signInManager.CheckPasswordSignInAsync(user, req.Password, false);
|
||||||
|
if (!passOk.Succeeded)
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
var tokens = await _tokenService.IssueAsync(user, ct);
|
var tokens = await _tokenService.IssueAsync(user, ct);
|
||||||
return Ok(tokens);
|
return Ok(tokens);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Refresh access token by refresh token.</summary>
|
|
||||||
[HttpPost("refresh")]
|
[HttpPost("refresh")]
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
public async Task<ActionResult<TokenPair>> Refresh(RefreshRequest req, CancellationToken ct)
|
public async Task<ActionResult<TokenPair>> Refresh(RefreshRequest req, CancellationToken ct)
|
||||||
@@ -65,7 +71,6 @@ public class AuthController : ControllerBase
|
|||||||
return Ok(pair);
|
return Ok(pair);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Revoke refresh token (logout).</summary>
|
|
||||||
[HttpPost("revoke")]
|
[HttpPost("revoke")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task<IActionResult> Revoke(RevokeRequest req, CancellationToken ct)
|
public async Task<IActionResult> Revoke(RevokeRequest req, CancellationToken ct)
|
||||||
@@ -74,13 +79,11 @@ public class AuthController : ControllerBase
|
|||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Returns current user id (debug).</summary>
|
|
||||||
[HttpGet("me")]
|
[HttpGet("me")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
public ActionResult<object> Me()
|
public ActionResult<object> Me()
|
||||||
{
|
{
|
||||||
var id = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue(ClaimTypes.Name);
|
var id = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? User.FindFirstValue(ClaimTypes.Name);
|
||||||
return Ok(new { userId = id });
|
return Ok(new { userId = id });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,22 +20,30 @@ public class DialogueController : ControllerBase
|
|||||||
[HttpGet("mission/{missionId:guid}")]
|
[HttpGet("mission/{missionId:guid}")]
|
||||||
public async Task<IActionResult> GetByMission(Guid missionId)
|
public async Task<IActionResult> GetByMission(Guid missionId)
|
||||||
{
|
{
|
||||||
var d = await _dialogueService.GetDialogueByMissionIdAsync(missionId);
|
var dialogue = await _dialogueService.GetDialogueByMissionIdAsync(missionId);
|
||||||
return d == null ? NotFound() : Ok(d);
|
if (dialogue == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
return Ok(dialogue);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("message/{messageId:guid}")]
|
[HttpGet("message/{messageId:guid}")]
|
||||||
public async Task<IActionResult> GetMessage(Guid messageId)
|
public async Task<IActionResult> GetMessage(Guid messageId)
|
||||||
{
|
{
|
||||||
var m = await _dialogueService.GetDialogueMessageByIdAsync(messageId);
|
var message = await _dialogueService.GetDialogueMessageByIdAsync(messageId);
|
||||||
return m == null ? NotFound() : Ok(m);
|
if (message == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
return Ok(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("message/{messageId:guid}/options")]
|
[HttpGet("message/{messageId:guid}/options")]
|
||||||
public async Task<IActionResult> GetOptions(Guid messageId)
|
public async Task<IActionResult> GetOptions(Guid messageId)
|
||||||
{
|
{
|
||||||
var opts = await _dialogueService.GetResponseOptionsAsync(messageId);
|
var options = await _dialogueService.GetResponseOptionsAsync(messageId);
|
||||||
return Ok(opts);
|
return Ok(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public record DialogueResponseRequest(Guid ResponseOptionId, Guid PlayerId);
|
public record DialogueResponseRequest(Guid ResponseOptionId, Guid PlayerId);
|
||||||
@@ -44,7 +52,10 @@ public class DialogueController : ControllerBase
|
|||||||
public async Task<IActionResult> Respond(Guid messageId, DialogueResponseRequest req)
|
public async Task<IActionResult> Respond(Guid messageId, DialogueResponseRequest req)
|
||||||
{
|
{
|
||||||
var next = await _dialogueService.ProcessDialogueResponseAsync(messageId, req.ResponseOptionId, req.PlayerId);
|
var next = await _dialogueService.ProcessDialogueResponseAsync(messageId, req.ResponseOptionId, req.PlayerId);
|
||||||
if (next == null) return Ok(new { end = true });
|
if (next == null)
|
||||||
|
{
|
||||||
|
return Ok(new { end = true });
|
||||||
|
}
|
||||||
return Ok(next);
|
return Ok(next);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,16 +71,16 @@ public class DialogueController : ControllerBase
|
|||||||
[Authorize(Roles = "Admin")]
|
[Authorize(Roles = "Admin")]
|
||||||
public async Task<IActionResult> Create(CreateDialogueRequest dto)
|
public async Task<IActionResult> Create(CreateDialogueRequest dto)
|
||||||
{
|
{
|
||||||
var d = new Dialogue
|
var dialogue = new Dialogue
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
MissionId = dto.MissionId,
|
MissionId = dto.MissionId,
|
||||||
InitialDialogueMessageId = dto.InitialDialogueMessageId,
|
InitialDialogueMessageId = dto.InitialDialogueMessageId,
|
||||||
InterimDialogueMessageId = dto.InterimDialogueMessageId,
|
InterimDialogueMessageId = dto.InterimDialogueMessageId,
|
||||||
EndDialogueMessageId = dto.EndDialogueMessageId,
|
EndDialogueMessageId = dto.EndDialogueMessageId,
|
||||||
Mission = null! // EF will populate if included
|
Mission = null!
|
||||||
};
|
};
|
||||||
d = await _dialogueService.CreateDialogueAsync(d);
|
dialogue = await _dialogueService.CreateDialogueAsync(dialogue);
|
||||||
return CreatedAtAction(nameof(GetByMission), new { missionId = d.MissionId }, d);
|
return CreatedAtAction(nameof(GetByMission), new { missionId = dialogue.MissionId }, dialogue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,19 +11,25 @@ namespace LctMonolith.Controllers;
|
|||||||
public class InventoryController : ControllerBase
|
public class InventoryController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly IInventoryService _inventoryService;
|
private readonly IInventoryService _inventoryService;
|
||||||
public InventoryController(IInventoryService inventoryService) => _inventoryService = inventoryService;
|
|
||||||
|
|
||||||
private Guid CurrentUserId() => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
|
public InventoryController(IInventoryService inventoryService)
|
||||||
|
{
|
||||||
|
_inventoryService = inventoryService;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Guid CurrentUserId()
|
||||||
|
{
|
||||||
|
return Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Get inventory for current authenticated user.</summary>
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<IActionResult> GetMine(CancellationToken ct)
|
public async Task<IActionResult> GetMine(CancellationToken ct)
|
||||||
{
|
{
|
||||||
var items = await _inventoryService.GetStoreInventoryAsync(CurrentUserId(), ct);
|
var items = await _inventoryService.GetStoreInventoryAsync(CurrentUserId(), ct);
|
||||||
return Ok(items.Select(i => new { i.StoreItemId, i.Quantity, i.AcquiredAt }));
|
var shaped = items.Select(i => new { i.StoreItemId, i.Quantity, i.AcquiredAt });
|
||||||
|
return Ok(shaped);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Admin: get inventory for specific user.</summary>
|
|
||||||
[HttpGet("user/{userId:guid}")]
|
[HttpGet("user/{userId:guid}")]
|
||||||
[Authorize(Roles = "Admin")]
|
[Authorize(Roles = "Admin")]
|
||||||
public async Task<IActionResult> GetByUser(Guid userId, CancellationToken ct)
|
public async Task<IActionResult> GetByUser(Guid userId, CancellationToken ct)
|
||||||
|
|||||||
@@ -12,7 +12,11 @@ namespace LctMonolith.Controllers;
|
|||||||
public class MissionCategoriesController : ControllerBase
|
public class MissionCategoriesController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly IMissionCategoryService _service;
|
private readonly IMissionCategoryService _service;
|
||||||
public MissionCategoriesController(IMissionCategoryService service) => _service = service;
|
|
||||||
|
public MissionCategoriesController(IMissionCategoryService service)
|
||||||
|
{
|
||||||
|
_service = service;
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<IActionResult> GetAll()
|
public async Task<IActionResult> GetAll()
|
||||||
@@ -24,35 +28,45 @@ public class MissionCategoriesController : ControllerBase
|
|||||||
[HttpGet("{id:guid}")]
|
[HttpGet("{id:guid}")]
|
||||||
public async Task<IActionResult> Get(Guid id)
|
public async Task<IActionResult> Get(Guid id)
|
||||||
{
|
{
|
||||||
var c = await _service.GetCategoryByIdAsync(id);
|
var category = await _service.GetCategoryByIdAsync(id);
|
||||||
return c == null ? NotFound() : Ok(c);
|
if (category == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
return Ok(category);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Authorize(Roles = "Admin")]
|
[Authorize(Roles = "Admin")]
|
||||||
public async Task<IActionResult> Create(CreateMissionCategoryDto dto)
|
public async Task<IActionResult> Create(CreateMissionCategoryDto dto)
|
||||||
{
|
{
|
||||||
var c = await _service.CreateCategoryAsync(new MissionCategory { Title = dto.Title });
|
var created = await _service.CreateCategoryAsync(new MissionCategory { Title = dto.Title });
|
||||||
return CreatedAtAction(nameof(Get), new { id = c.Id }, c);
|
return CreatedAtAction(nameof(Get), new { id = created.Id }, created);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("{id:guid}")]
|
[HttpPut("{id:guid}")]
|
||||||
[Authorize(Roles = "Admin")]
|
[Authorize(Roles = "Admin")]
|
||||||
public async Task<IActionResult> Update(Guid id, CreateMissionCategoryDto dto)
|
public async Task<IActionResult> Update(Guid id, CreateMissionCategoryDto dto)
|
||||||
{
|
{
|
||||||
var c = await _service.GetCategoryByIdAsync(id);
|
var existing = await _service.GetCategoryByIdAsync(id);
|
||||||
if (c == null) return NotFound();
|
if (existing == null)
|
||||||
c.Title = dto.Title;
|
{
|
||||||
await _service.UpdateCategoryAsync(c);
|
return NotFound();
|
||||||
return Ok(c);
|
}
|
||||||
|
existing.Title = dto.Title;
|
||||||
|
await _service.UpdateCategoryAsync(existing);
|
||||||
|
return Ok(existing);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("{id:guid}")]
|
[HttpDelete("{id:guid}")]
|
||||||
[Authorize(Roles = "Admin")]
|
[Authorize(Roles = "Admin")]
|
||||||
public async Task<IActionResult> Delete(Guid id)
|
public async Task<IActionResult> Delete(Guid id)
|
||||||
{
|
{
|
||||||
var ok = await _service.DeleteCategoryAsync(id);
|
var removed = await _service.DeleteCategoryAsync(id);
|
||||||
return ok ? NoContent() : NotFound();
|
if (!removed)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
return NoContent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,8 +23,12 @@ public class MissionsController : ControllerBase
|
|||||||
[HttpGet("{id:guid}")]
|
[HttpGet("{id:guid}")]
|
||||||
public async Task<IActionResult> Get(Guid id)
|
public async Task<IActionResult> Get(Guid id)
|
||||||
{
|
{
|
||||||
var m = await _missions.GetMissionByIdAsync(id);
|
var mission = await _missions.GetMissionByIdAsync(id);
|
||||||
return m == null ? NotFound() : Ok(m);
|
if (mission == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
return Ok(mission);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("category/{categoryId:guid}")]
|
[HttpGet("category/{categoryId:guid}")]
|
||||||
@@ -80,7 +84,10 @@ public class MissionsController : ControllerBase
|
|||||||
public async Task<IActionResult> Update(Guid id, CreateMissionRequest dto)
|
public async Task<IActionResult> Update(Guid id, CreateMissionRequest dto)
|
||||||
{
|
{
|
||||||
var existing = await _missions.GetMissionByIdAsync(id);
|
var existing = await _missions.GetMissionByIdAsync(id);
|
||||||
if (existing == null) return NotFound();
|
if (existing == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
existing.Title = dto.Title;
|
existing.Title = dto.Title;
|
||||||
existing.Description = dto.Description ?? string.Empty;
|
existing.Description = dto.Description ?? string.Empty;
|
||||||
existing.MissionCategoryId = dto.MissionCategoryId;
|
existing.MissionCategoryId = dto.MissionCategoryId;
|
||||||
@@ -95,8 +102,12 @@ public class MissionsController : ControllerBase
|
|||||||
[Authorize(Roles = "Admin")]
|
[Authorize(Roles = "Admin")]
|
||||||
public async Task<IActionResult> Delete(Guid id)
|
public async Task<IActionResult> Delete(Guid id)
|
||||||
{
|
{
|
||||||
var ok = await _missions.DeleteMissionAsync(id);
|
var removed = await _missions.DeleteMissionAsync(id);
|
||||||
return ok ? NoContent() : NotFound();
|
if (!removed)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public record CompleteMissionRequest(Guid PlayerId, object? Proof);
|
public record CompleteMissionRequest(Guid PlayerId, object? Proof);
|
||||||
@@ -105,8 +116,10 @@ public class MissionsController : ControllerBase
|
|||||||
public async Task<IActionResult> Complete(Guid missionId, CompleteMissionRequest r)
|
public async Task<IActionResult> Complete(Guid missionId, CompleteMissionRequest r)
|
||||||
{
|
{
|
||||||
var result = await _missions.CompleteMissionAsync(missionId, r.PlayerId, r.Proof);
|
var result = await _missions.CompleteMissionAsync(missionId, r.PlayerId, r.Proof);
|
||||||
if (!result.Success) return BadRequest(result);
|
if (!result.Success)
|
||||||
|
{
|
||||||
|
return BadRequest(result);
|
||||||
|
}
|
||||||
return Ok(result);
|
return Ok(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,6 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
|
|
||||||
namespace LctMonolith.Controllers;
|
namespace LctMonolith.Controllers;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// In-app user notifications API.
|
|
||||||
/// </summary>
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/notifications")]
|
[Route("api/notifications")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
@@ -21,9 +18,11 @@ public class NotificationsController : ControllerBase
|
|||||||
_notifications = notifications;
|
_notifications = notifications;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Guid GetUserId() => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
|
private Guid GetUserId()
|
||||||
|
{
|
||||||
|
return Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Get up to 100 unread notifications.</summary>
|
|
||||||
[HttpGet("unread")]
|
[HttpGet("unread")]
|
||||||
public async Task<IActionResult> GetUnread(CancellationToken ct)
|
public async Task<IActionResult> GetUnread(CancellationToken ct)
|
||||||
{
|
{
|
||||||
@@ -31,7 +30,6 @@ public class NotificationsController : ControllerBase
|
|||||||
return Ok(list);
|
return Ok(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Get recent notifications (paged by take).</summary>
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<IActionResult> GetAll([FromQuery] int take = 100, CancellationToken ct = default)
|
public async Task<IActionResult> GetAll([FromQuery] int take = 100, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
@@ -39,7 +37,6 @@ public class NotificationsController : ControllerBase
|
|||||||
return Ok(list);
|
return Ok(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Mark a notification as read.</summary>
|
|
||||||
[HttpPost("mark/{id:guid}")]
|
[HttpPost("mark/{id:guid}")]
|
||||||
public async Task<IActionResult> MarkRead(Guid id, CancellationToken ct)
|
public async Task<IActionResult> MarkRead(Guid id, CancellationToken ct)
|
||||||
{
|
{
|
||||||
@@ -47,12 +44,10 @@ public class NotificationsController : ControllerBase
|
|||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Mark all notifications as read.</summary>
|
|
||||||
[HttpPost("mark-all")]
|
[HttpPost("mark-all")]
|
||||||
public async Task<IActionResult> MarkAll(CancellationToken ct)
|
public async Task<IActionResult> MarkAll(CancellationToken ct)
|
||||||
{
|
{
|
||||||
var cnt = await _notifications.MarkAllReadAsync(GetUserId(), ct);
|
var updated = await _notifications.MarkAllReadAsync(GetUserId(), ct);
|
||||||
return Ok(new { updated = cnt });
|
return Ok(new { updated });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,37 +28,44 @@ public class PlayersController : ControllerBase
|
|||||||
[HttpGet("user/{userId:guid}")]
|
[HttpGet("user/{userId:guid}")]
|
||||||
public async Task<IActionResult> GetByUser(Guid userId)
|
public async Task<IActionResult> GetByUser(Guid userId)
|
||||||
{
|
{
|
||||||
var p = await _playerService.GetPlayerByUserIdAsync(userId.ToString());
|
var player = await _playerService.GetPlayerByUserIdAsync(userId.ToString());
|
||||||
if (p == null) return NotFound();
|
if (player == null)
|
||||||
return Ok(p);
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
return Ok(player);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CreatePlayerRequest { public Guid UserId { get; set; } public string Username { get; set; } = string.Empty; }
|
public class CreatePlayerRequest
|
||||||
|
{
|
||||||
|
public Guid UserId { get; set; }
|
||||||
|
public string Username { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Authorize(Roles = "Admin")]
|
[Authorize(Roles = "Admin")]
|
||||||
public async Task<IActionResult> Create(CreatePlayerRequest req)
|
public async Task<IActionResult> Create(CreatePlayerRequest req)
|
||||||
{
|
{
|
||||||
var p = await _playerService.CreatePlayerAsync(req.UserId.ToString(), req.Username);
|
var player = await _playerService.CreatePlayerAsync(req.UserId.ToString(), req.Username);
|
||||||
return CreatedAtAction(nameof(GetPlayer), new { playerId = p.Id }, p);
|
return CreatedAtAction(nameof(GetPlayer), new { playerId = player.Id }, player);
|
||||||
}
|
}
|
||||||
|
|
||||||
public record AdjustValueRequest(int Value);
|
public record AdjustValueRequest(int Value);
|
||||||
|
|
||||||
[HttpPost("{playerId:guid}/experience")]
|
[HttpPost("{playerId:guid}/experience")]
|
||||||
[Authorize(Roles = "Admin")] // manual adjust
|
[Authorize(Roles = "Admin")]
|
||||||
public async Task<IActionResult> AddExperience(Guid playerId, AdjustValueRequest r)
|
public async Task<IActionResult> AddExperience(Guid playerId, AdjustValueRequest r)
|
||||||
{
|
{
|
||||||
var p = await _playerService.AddPlayerExperienceAsync(playerId, r.Value);
|
var player = await _playerService.AddPlayerExperienceAsync(playerId, r.Value);
|
||||||
return Ok(new { p.Id, p.Experience });
|
return Ok(new { player.Id, player.Experience });
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{playerId:guid}/mana")]
|
[HttpPost("{playerId:guid}/mana")]
|
||||||
[Authorize(Roles = "Admin")] // manual adjust
|
[Authorize(Roles = "Admin")]
|
||||||
public async Task<IActionResult> AddMana(Guid playerId, AdjustValueRequest r)
|
public async Task<IActionResult> AddMana(Guid playerId, AdjustValueRequest r)
|
||||||
{
|
{
|
||||||
var p = await _playerService.AddPlayerManaAsync(playerId, r.Value);
|
var player = await _playerService.AddPlayerManaAsync(playerId, r.Value);
|
||||||
return Ok(new { p.Id, p.Mana });
|
return Ok(new { player.Id, player.Mana });
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("top")]
|
[HttpGet("top")]
|
||||||
@@ -71,8 +78,7 @@ public class PlayersController : ControllerBase
|
|||||||
[HttpGet("{playerId:guid}/progress")]
|
[HttpGet("{playerId:guid}/progress")]
|
||||||
public async Task<IActionResult> GetProgress(Guid playerId)
|
public async Task<IActionResult> GetProgress(Guid playerId)
|
||||||
{
|
{
|
||||||
var prog = await _progressService.GetPlayerOverallProgressAsync(playerId);
|
var progress = await _progressService.GetPlayerOverallProgressAsync(playerId);
|
||||||
return Ok(prog);
|
return Ok(progress);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,9 +12,16 @@ namespace LctMonolith.Controllers;
|
|||||||
public class ProfileController : ControllerBase
|
public class ProfileController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly IProfileService _profiles;
|
private readonly IProfileService _profiles;
|
||||||
public ProfileController(IProfileService profiles) => _profiles = profiles;
|
|
||||||
|
|
||||||
private Guid CurrentUserId() => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
|
public ProfileController(IProfileService profiles)
|
||||||
|
{
|
||||||
|
_profiles = profiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Guid CurrentUserId()
|
||||||
|
{
|
||||||
|
return Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
|
||||||
|
}
|
||||||
|
|
||||||
public class UpdateProfileDto
|
public class UpdateProfileDto
|
||||||
{
|
{
|
||||||
@@ -28,40 +35,54 @@ public class ProfileController : ControllerBase
|
|||||||
[HttpGet("me")]
|
[HttpGet("me")]
|
||||||
public async Task<IActionResult> GetMe(CancellationToken ct)
|
public async Task<IActionResult> GetMe(CancellationToken ct)
|
||||||
{
|
{
|
||||||
var p = await _profiles.GetByUserIdAsync(CurrentUserId(), ct);
|
var profile = await _profiles.GetByUserIdAsync(CurrentUserId(), ct);
|
||||||
if (p == null) return NotFound();
|
if (profile == null)
|
||||||
return Ok(p);
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
return Ok(profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{userId:guid}")]
|
[HttpGet("{userId:guid}")]
|
||||||
[Authorize(Roles = "Admin")]
|
[Authorize(Roles = "Admin")]
|
||||||
public async Task<IActionResult> GetByUser(Guid userId, CancellationToken ct)
|
public async Task<IActionResult> GetByUser(Guid userId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var p = await _profiles.GetByUserIdAsync(userId, ct);
|
var profile = await _profiles.GetByUserIdAsync(userId, ct);
|
||||||
return p == null ? NotFound() : Ok(p);
|
if (profile == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
return Ok(profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut]
|
[HttpPut]
|
||||||
public async Task<IActionResult> Upsert(UpdateProfileDto dto, CancellationToken ct)
|
public async Task<IActionResult> Upsert(UpdateProfileDto dto, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var p = await _profiles.UpsertAsync(CurrentUserId(), dto.FirstName, dto.LastName, dto.BirthDate, dto.About, dto.Location, ct);
|
var profile = await _profiles.UpsertAsync(CurrentUserId(), dto.FirstName, dto.LastName, dto.BirthDate, dto.About, dto.Location, ct);
|
||||||
return Ok(p);
|
return Ok(profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("avatar")]
|
[HttpPost("avatar")]
|
||||||
[RequestSizeLimit(7_000_000)] // ~7MB
|
[RequestSizeLimit(7_000_000)]
|
||||||
public async Task<IActionResult> UploadAvatar(IFormFile file, CancellationToken ct)
|
public async Task<IActionResult> UploadAvatar(IFormFile file, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (file == null || file.Length == 0) return BadRequest("File required");
|
if (file == null || file.Length == 0)
|
||||||
|
{
|
||||||
|
return BadRequest("File required");
|
||||||
|
}
|
||||||
await using var stream = file.OpenReadStream();
|
await using var stream = file.OpenReadStream();
|
||||||
var p = await _profiles.UpdateAvatarAsync(CurrentUserId(), stream, file.ContentType, file.FileName, ct);
|
var profile = await _profiles.UpdateAvatarAsync(CurrentUserId(), stream, file.ContentType, file.FileName, ct);
|
||||||
return Ok(new { p.AvatarUrl });
|
return Ok(new { profile.AvatarUrl });
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("avatar")]
|
[HttpDelete("avatar")]
|
||||||
public async Task<IActionResult> DeleteAvatar(CancellationToken ct)
|
public async Task<IActionResult> DeleteAvatar(CancellationToken ct)
|
||||||
{
|
{
|
||||||
var ok = await _profiles.DeleteAvatarAsync(CurrentUserId(), ct);
|
var removed = await _profiles.DeleteAvatarAsync(CurrentUserId(), ct);
|
||||||
return ok ? NoContent() : NotFound();
|
if (!removed)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
return NoContent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,8 +30,12 @@ public class RanksController : ControllerBase
|
|||||||
[HttpGet("{id:guid}")]
|
[HttpGet("{id:guid}")]
|
||||||
public async Task<IActionResult> Get(Guid id)
|
public async Task<IActionResult> Get(Guid id)
|
||||||
{
|
{
|
||||||
var r = await _rankService.GetRankByIdAsync(id);
|
var rank = await _rankService.GetRankByIdAsync(id);
|
||||||
return r == null ? NotFound() : Ok(r);
|
if (rank == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
return Ok(rank);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
@@ -46,27 +50,33 @@ public class RanksController : ControllerBase
|
|||||||
[Authorize(Roles = "Admin")]
|
[Authorize(Roles = "Admin")]
|
||||||
public async Task<IActionResult> Update(Guid id, CreateRankDto dto)
|
public async Task<IActionResult> Update(Guid id, CreateRankDto dto)
|
||||||
{
|
{
|
||||||
var r = await _rankService.GetRankByIdAsync(id);
|
var rank = await _rankService.GetRankByIdAsync(id);
|
||||||
if (r == null) return NotFound();
|
if (rank == null)
|
||||||
r.Title = dto.Title;
|
{
|
||||||
r.ExpNeeded = dto.ExpNeeded;
|
return NotFound();
|
||||||
await _rankService.UpdateRankAsync(r);
|
}
|
||||||
return Ok(r);
|
rank.Title = dto.Title;
|
||||||
|
rank.ExpNeeded = dto.ExpNeeded;
|
||||||
|
await _rankService.UpdateRankAsync(rank);
|
||||||
|
return Ok(rank);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("{id:guid}")]
|
[HttpDelete("{id:guid}")]
|
||||||
[Authorize(Roles = "Admin")]
|
[Authorize(Roles = "Admin")]
|
||||||
public async Task<IActionResult> Delete(Guid id)
|
public async Task<IActionResult> Delete(Guid id)
|
||||||
{
|
{
|
||||||
var ok = await _rankService.DeleteRankAsync(id);
|
var removed = await _rankService.DeleteRankAsync(id);
|
||||||
return ok ? NoContent() : NotFound();
|
if (!removed)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("validate-advance/{playerId:guid}/{targetRankId:guid}")]
|
[HttpGet("validate-advance/{playerId:guid}/{targetRankId:guid}")]
|
||||||
public async Task<IActionResult> CanAdvance(Guid playerId, Guid targetRankId)
|
public async Task<IActionResult> CanAdvance(Guid playerId, Guid targetRankId)
|
||||||
{
|
{
|
||||||
var ok = await _ruleValidation.ValidateRankAdvancementRulesAsync(playerId, targetRankId);
|
var can = await _ruleValidation.ValidateRankAdvancementRulesAsync(playerId, targetRankId);
|
||||||
return Ok(new { playerId, targetRankId, canAdvance = ok });
|
return Ok(new { playerId, targetRankId, canAdvance = can });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,23 +16,22 @@ public class RewardController : ControllerBase
|
|||||||
_rewardService = rewardService;
|
_rewardService = rewardService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>List skill rewards configured for a mission.</summary>
|
|
||||||
[HttpGet("mission/{missionId:guid}/skills")]
|
[HttpGet("mission/{missionId:guid}/skills")]
|
||||||
public async Task<IActionResult> GetMissionSkillRewards(Guid missionId)
|
public async Task<IActionResult> GetMissionSkillRewards(Guid missionId)
|
||||||
{
|
{
|
||||||
var rewards = await _rewardService.GetMissionSkillRewardsAsync(missionId);
|
var rewards = await _rewardService.GetMissionSkillRewardsAsync(missionId);
|
||||||
return Ok(rewards.Select(r => new { r.SkillId, r.Value }));
|
var shaped = rewards.Select(r => new { r.SkillId, r.Value });
|
||||||
|
return Ok(shaped);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>List item rewards configured for a mission.</summary>
|
|
||||||
[HttpGet("mission/{missionId:guid}/items")]
|
[HttpGet("mission/{missionId:guid}/items")]
|
||||||
public async Task<IActionResult> GetMissionItemRewards(Guid missionId)
|
public async Task<IActionResult> GetMissionItemRewards(Guid missionId)
|
||||||
{
|
{
|
||||||
var rewards = await _rewardService.GetMissionItemRewardsAsync(missionId);
|
var rewards = await _rewardService.GetMissionItemRewardsAsync(missionId);
|
||||||
return Ok(rewards.Select(r => new { r.ItemId }));
|
var shaped = rewards.Select(r => new { r.ItemId });
|
||||||
|
return Ok(shaped);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Check if mission rewards can be claimed by player (missionId used as rewardId).</summary>
|
|
||||||
[HttpGet("mission/{missionId:guid}/can-claim/{playerId:guid}")]
|
[HttpGet("mission/{missionId:guid}/can-claim/{playerId:guid}")]
|
||||||
public async Task<IActionResult> CanClaim(Guid missionId, Guid playerId)
|
public async Task<IActionResult> CanClaim(Guid missionId, Guid playerId)
|
||||||
{
|
{
|
||||||
@@ -42,19 +41,20 @@ public class RewardController : ControllerBase
|
|||||||
|
|
||||||
public record ClaimRewardRequest(Guid PlayerId);
|
public record ClaimRewardRequest(Guid PlayerId);
|
||||||
|
|
||||||
/// <summary>Claim mission rewards if available (idempotent on already claimed).</summary>
|
|
||||||
[HttpPost("mission/{missionId:guid}/claim")]
|
[HttpPost("mission/{missionId:guid}/claim")]
|
||||||
public async Task<IActionResult> Claim(Guid missionId, ClaimRewardRequest req)
|
public async Task<IActionResult> Claim(Guid missionId, ClaimRewardRequest req)
|
||||||
{
|
{
|
||||||
var can = await _rewardService.CanClaimRewardAsync(missionId, req.PlayerId);
|
var can = await _rewardService.CanClaimRewardAsync(missionId, req.PlayerId);
|
||||||
if (!can) return Conflict(new { message = "Rewards already claimed or mission not completed" });
|
if (!can)
|
||||||
|
{
|
||||||
|
return Conflict(new { message = "Rewards already claimed or mission not completed" });
|
||||||
|
}
|
||||||
await _rewardService.DistributeMissionRewardsAsync(missionId, req.PlayerId);
|
await _rewardService.DistributeMissionRewardsAsync(missionId, req.PlayerId);
|
||||||
return Ok(new { missionId, req.PlayerId, status = "claimed" });
|
return Ok(new { missionId, req.PlayerId, status = "claimed" });
|
||||||
}
|
}
|
||||||
|
|
||||||
public record ForceDistributeRequest(Guid PlayerId);
|
public record ForceDistributeRequest(Guid PlayerId);
|
||||||
|
|
||||||
/// <summary>Admin: force distribute rewards regardless of previous state (may duplicate). Use cautiously.</summary>
|
|
||||||
[HttpPost("mission/{missionId:guid}/force-distribute")]
|
[HttpPost("mission/{missionId:guid}/force-distribute")]
|
||||||
[Authorize(Roles = "Admin")]
|
[Authorize(Roles = "Admin")]
|
||||||
public async Task<IActionResult> ForceDistribute(Guid missionId, ForceDistributeRequest req)
|
public async Task<IActionResult> ForceDistribute(Guid missionId, ForceDistributeRequest req)
|
||||||
|
|||||||
@@ -28,8 +28,12 @@ public class SkillsController : ControllerBase
|
|||||||
[HttpGet("{id:guid}")]
|
[HttpGet("{id:guid}")]
|
||||||
public async Task<IActionResult> Get(Guid id)
|
public async Task<IActionResult> Get(Guid id)
|
||||||
{
|
{
|
||||||
var s = await _skillService.GetSkillByIdAsync(id);
|
var skill = await _skillService.GetSkillByIdAsync(id);
|
||||||
return s == null ? NotFound() : Ok(s);
|
if (skill == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
return Ok(skill);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
@@ -44,19 +48,26 @@ public class SkillsController : ControllerBase
|
|||||||
[Authorize(Roles = "Admin")]
|
[Authorize(Roles = "Admin")]
|
||||||
public async Task<IActionResult> Update(Guid id, CreateSkillDto dto)
|
public async Task<IActionResult> Update(Guid id, CreateSkillDto dto)
|
||||||
{
|
{
|
||||||
var s = await _skillService.GetSkillByIdAsync(id);
|
var skill = await _skillService.GetSkillByIdAsync(id);
|
||||||
if (s == null) return NotFound();
|
if (skill == null)
|
||||||
s.Title = dto.Title;
|
{
|
||||||
await _skillService.UpdateSkillAsync(s);
|
return NotFound();
|
||||||
return Ok(s);
|
}
|
||||||
|
skill.Title = dto.Title;
|
||||||
|
await _skillService.UpdateSkillAsync(skill);
|
||||||
|
return Ok(skill);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("{id:guid}")]
|
[HttpDelete("{id:guid}")]
|
||||||
[Authorize(Roles = "Admin")]
|
[Authorize(Roles = "Admin")]
|
||||||
public async Task<IActionResult> Delete(Guid id)
|
public async Task<IActionResult> Delete(Guid id)
|
||||||
{
|
{
|
||||||
var ok = await _skillService.DeleteSkillAsync(id);
|
var removed = await _skillService.DeleteSkillAsync(id);
|
||||||
return ok ? NoContent() : NotFound();
|
if (!removed)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("player/{playerId:guid}")]
|
[HttpGet("player/{playerId:guid}")]
|
||||||
@@ -76,4 +87,3 @@ public class SkillsController : ControllerBase
|
|||||||
return Ok(new { ps.PlayerId, ps.SkillId, ps.Score });
|
return Ok(new { ps.PlayerId, ps.SkillId, ps.Score });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,6 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
|
|
||||||
namespace LctMonolith.Controllers;
|
namespace LctMonolith.Controllers;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Store endpoints for listing items and purchasing.
|
|
||||||
/// </summary>
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/store")]
|
[Route("api/store")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
@@ -22,9 +19,11 @@ public class StoreController : ControllerBase
|
|||||||
_storeService = storeService;
|
_storeService = storeService;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Guid GetUserId() => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
|
private Guid GetUserId()
|
||||||
|
{
|
||||||
|
return Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>List active store items.</summary>
|
|
||||||
[HttpGet("items")]
|
[HttpGet("items")]
|
||||||
public async Task<IActionResult> GetItems(CancellationToken ct)
|
public async Task<IActionResult> GetItems(CancellationToken ct)
|
||||||
{
|
{
|
||||||
@@ -32,7 +31,6 @@ public class StoreController : ControllerBase
|
|||||||
return Ok(items);
|
return Ok(items);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Purchase an item for the authenticated user.</summary>
|
|
||||||
[HttpPost("purchase")]
|
[HttpPost("purchase")]
|
||||||
public async Task<IActionResult> Purchase(PurchaseRequest req, CancellationToken ct)
|
public async Task<IActionResult> Purchase(PurchaseRequest req, CancellationToken ct)
|
||||||
{
|
{
|
||||||
@@ -40,4 +38,3 @@ public class StoreController : ControllerBase
|
|||||||
return Ok(new { inv.StoreItemId, inv.Quantity, inv.AcquiredAt });
|
return Ok(new { inv.StoreItemId, inv.Quantity, inv.AcquiredAt });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
using LctMonolith.Models.Database;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Serilog;
|
|
||||||
|
|
||||||
namespace LctMonolith.Database.Data;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Development database seeder for initial ranks, competencies, sample store items.
|
|
||||||
/// Idempotent: checks existence before inserting.
|
|
||||||
/// </summary>
|
|
||||||
public static class DbSeeder
|
|
||||||
{
|
|
||||||
public static async Task SeedAsync(AppDbContext db, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
// await db.Database.EnsureCreatedAsync(ct);
|
|
||||||
//
|
|
||||||
// if (!await db.Ranks.AnyAsync(ct))
|
|
||||||
// {
|
|
||||||
// var ranks = new List<Rank>
|
|
||||||
// {
|
|
||||||
// new() { Title = "Искатель", ExpNeeded = 0 },
|
|
||||||
// new() { Title = "Пилот-кандидат", ExpNeeded = 500 },
|
|
||||||
// new() { Title = "Принятый в экипаж", ExpNeeded = 1500 }
|
|
||||||
// };
|
|
||||||
// db.Ranks.AddRange(ranks);
|
|
||||||
// Log.Information("Seeded {Count} ranks", ranks.Count);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if (!await db.Skills.AnyAsync(ct))
|
|
||||||
// {
|
|
||||||
// var comps = new[]
|
|
||||||
// {
|
|
||||||
// "Вера в дело","Стремление к большему","Общение","Аналитика","Командование","Юриспруденция","Трёхмерное мышление","Базовая экономика","Основы аэронавигации"
|
|
||||||
// }.Select(n => new Skill { Title = n });
|
|
||||||
// db.Skills.AddRange(comps);
|
|
||||||
// Log.Information("Seeded competencies");
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if (!await db.StoreItems.AnyAsync(ct))
|
|
||||||
// {
|
|
||||||
// db.StoreItems.AddRange(new StoreItem { Name = "Футболка Алабуга", Price = 100 }, new StoreItem { Name = "Брелок Буран", Price = 50 });
|
|
||||||
// Log.Information("Seeded store items");
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// await db.SaveChangesAsync(ct);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,9 +4,6 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
|
|
||||||
namespace LctMonolith.Database.Repositories;
|
namespace LctMonolith.Database.Repositories;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Generic repository implementation for common CRUD and query composition.
|
|
||||||
/// </summary>
|
|
||||||
public class GenericRepository<TEntity> : IGenericRepository<TEntity> where TEntity : class
|
public class GenericRepository<TEntity> : IGenericRepository<TEntity> where TEntity : class
|
||||||
{
|
{
|
||||||
protected readonly AppDbContext Context;
|
protected readonly AppDbContext Context;
|
||||||
@@ -24,33 +21,61 @@ public class GenericRepository<TEntity> : IGenericRepository<TEntity> where TEnt
|
|||||||
params Expression<Func<TEntity, object>>[] includes)
|
params Expression<Func<TEntity, object>>[] includes)
|
||||||
{
|
{
|
||||||
IQueryable<TEntity> query = Set;
|
IQueryable<TEntity> query = Set;
|
||||||
if (filter != null) query = query.Where(filter);
|
if (filter != null)
|
||||||
|
{
|
||||||
|
query = query.Where(filter);
|
||||||
|
}
|
||||||
if (includes != null)
|
if (includes != null)
|
||||||
{
|
{
|
||||||
foreach (var include in includes)
|
foreach (var include in includes)
|
||||||
|
{
|
||||||
query = query.Include(include);
|
query = query.Include(include);
|
||||||
}
|
}
|
||||||
if (orderBy != null) query = orderBy(query);
|
}
|
||||||
|
if (orderBy != null)
|
||||||
|
{
|
||||||
|
query = orderBy(query);
|
||||||
|
}
|
||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<TEntity?> GetByIdAsync(object id) => await Set.FindAsync(id) ?? null;
|
public async Task<TEntity?> GetByIdAsync(object id)
|
||||||
|
{
|
||||||
|
return await Set.FindAsync(id);
|
||||||
|
}
|
||||||
|
|
||||||
public ValueTask<TEntity?> FindAsync(params object[] keyValues) => Set.FindAsync(keyValues);
|
public ValueTask<TEntity?> FindAsync(params object[] keyValues)
|
||||||
|
{
|
||||||
|
return Set.FindAsync(keyValues);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task AddAsync(TEntity entity, CancellationToken ct = default) => await Set.AddAsync(entity, ct);
|
public async Task AddAsync(TEntity entity, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await Set.AddAsync(entity, ct);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task AddRangeAsync(IEnumerable<TEntity> entities, CancellationToken ct = default) => await Set.AddRangeAsync(entities, ct);
|
public async Task AddRangeAsync(IEnumerable<TEntity> entities, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await Set.AddRangeAsync(entities, ct);
|
||||||
|
}
|
||||||
|
|
||||||
public void Update(TEntity entity) => Set.Update(entity);
|
public void Update(TEntity entity)
|
||||||
|
{
|
||||||
|
Set.Update(entity);
|
||||||
|
}
|
||||||
|
|
||||||
public void Remove(TEntity entity) => Set.Remove(entity);
|
public void Remove(TEntity entity)
|
||||||
|
{
|
||||||
|
Set.Remove(entity);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task RemoveByIdAsync(object id, CancellationToken ct = default)
|
public async Task RemoveByIdAsync(object id, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var entity = await Set.FindAsync([id], ct);
|
var entity = await Set.FindAsync([id], ct);
|
||||||
if (entity == null) throw new KeyNotFoundException($"Entity {typeof(TEntity).Name} id={id} not found");
|
if (entity == null)
|
||||||
|
{
|
||||||
|
throw new KeyNotFoundException($"Entity {typeof(TEntity).Name} id={id} not found");
|
||||||
|
}
|
||||||
Set.Remove(entity);
|
Set.Remove(entity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,6 @@ using System.Linq.Expressions;
|
|||||||
|
|
||||||
namespace LctMonolith.Database.Repositories;
|
namespace LctMonolith.Database.Repositories;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Generic repository abstraction for aggregate root / entity access. Read operations return IQueryable for composition.
|
|
||||||
/// </summary>
|
|
||||||
public interface IGenericRepository<TEntity> where TEntity : class
|
public interface IGenericRepository<TEntity> where TEntity : class
|
||||||
{
|
{
|
||||||
IQueryable<TEntity> Query(
|
IQueryable<TEntity> Query(
|
||||||
@@ -22,4 +19,3 @@ public interface IGenericRepository<TEntity> where TEntity : class
|
|||||||
void Remove(TEntity entity);
|
void Remove(TEntity entity);
|
||||||
Task RemoveByIdAsync(object id, CancellationToken ct = default);
|
Task RemoveByIdAsync(object id, CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,11 @@ namespace LctMonolith.Models.Database;
|
|||||||
public class Player
|
public class Player
|
||||||
{
|
{
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
public Guid UserId { get; set; } // 1:1 to AppUser (retain linkage)
|
public Guid UserId { get; set; }
|
||||||
public Guid RankId { get; set; }
|
public Guid RankId { get; set; }
|
||||||
public Rank? Rank { get; set; }
|
public Rank? Rank { get; set; }
|
||||||
public int Experience { get; set; }
|
public int Experience { get; set; }
|
||||||
public int Mana { get; set; }
|
public int Mana { get; set; }
|
||||||
|
|
||||||
public ICollection<PlayerMission> PlayerMissions { get; set; } = new List<PlayerMission>();
|
public ICollection<PlayerMission> PlayerMissions { get; set; } = new List<PlayerMission>();
|
||||||
public ICollection<PlayerSkill> PlayerSkills { get; set; } = new List<PlayerSkill>();
|
public ICollection<PlayerSkill> PlayerSkills { get; set; } = new List<PlayerSkill>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,26 +4,24 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using LctMonolith.Models.Database; // replaced Domain.Entities
|
using LctMonolith.Models.Database;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using LctMonolith.Application.Middleware;
|
using LctMonolith.Application.Middleware;
|
||||||
using LctMonolith.Services;
|
using LctMonolith.Services;
|
||||||
using LctMonolith.Application.Options;
|
using LctMonolith.Application.Options;
|
||||||
using LctMonolith.Database.Data;
|
using LctMonolith.Database.Data;
|
||||||
using LctMonolith.Database.UnitOfWork;
|
using LctMonolith.Database.UnitOfWork;
|
||||||
using LctMonolith.Services.Contracts; // Added for JwtOptions
|
using LctMonolith.Services.Contracts;
|
||||||
using LctMonolith.Application.Extensions; // added
|
using LctMonolith.Application.Extensions;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
// Serilog configuration
|
|
||||||
builder.Host.UseSerilog((ctx, services, loggerConfig) =>
|
builder.Host.UseSerilog((ctx, services, loggerConfig) =>
|
||||||
loggerConfig
|
loggerConfig
|
||||||
.ReadFrom.Configuration(ctx.Configuration)
|
.ReadFrom.Configuration(ctx.Configuration)
|
||||||
.Enrich.FromLogContext()
|
.Enrich.FromLogContext()
|
||||||
.Enrich.WithProperty("Application", "LctMonolith"));
|
.Enrich.WithProperty("Application", "LctMonolith"));
|
||||||
|
|
||||||
// Configuration values
|
|
||||||
var connectionString = builder.Configuration.GetConnectionString("Default") ?? "Host=localhost;Port=5432;Database=lct2025;Username=postgres;Password=postgres";
|
var connectionString = builder.Configuration.GetConnectionString("Default") ?? "Host=localhost;Port=5432;Database=lct2025;Username=postgres;Password=postgres";
|
||||||
var jwtSection = builder.Configuration.GetSection("Jwt");
|
var jwtSection = builder.Configuration.GetSection("Jwt");
|
||||||
var jwtKey = jwtSection["Key"] ?? "Dev_Insecure_Key_Change_Me";
|
var jwtKey = jwtSection["Key"] ?? "Dev_Insecure_Key_Change_Me";
|
||||||
@@ -42,11 +40,9 @@ builder.Services.Configure<JwtOptions>(o =>
|
|||||||
o.RefreshTokenDays = refreshDays;
|
o.RefreshTokenDays = refreshDays;
|
||||||
});
|
});
|
||||||
|
|
||||||
// DbContext
|
|
||||||
builder.Services.AddDbContext<AppDbContext>(opt =>
|
builder.Services.AddDbContext<AppDbContext>(opt =>
|
||||||
opt.UseNpgsql(connectionString));
|
opt.UseNpgsql(connectionString));
|
||||||
|
|
||||||
// Identity Core
|
|
||||||
builder.Services.AddIdentityCore<AppUser>(options =>
|
builder.Services.AddIdentityCore<AppUser>(options =>
|
||||||
{
|
{
|
||||||
options.Password.RequireDigit = false;
|
options.Password.RequireDigit = false;
|
||||||
@@ -60,7 +56,6 @@ builder.Services.AddIdentityCore<AppUser>(options =>
|
|||||||
.AddSignInManager<SignInManager<AppUser>>()
|
.AddSignInManager<SignInManager<AppUser>>()
|
||||||
.AddDefaultTokenProviders();
|
.AddDefaultTokenProviders();
|
||||||
|
|
||||||
// Authentication & JWT
|
|
||||||
builder.Services.AddAuthentication(o =>
|
builder.Services.AddAuthentication(o =>
|
||||||
{
|
{
|
||||||
o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||||
@@ -81,38 +76,19 @@ builder.Services.AddAuthentication(o =>
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Controllers + NewtonsoftJson
|
builder.Services.AddControllers().AddNewtonsoftJson();
|
||||||
builder.Services.AddControllers()
|
|
||||||
.AddNewtonsoftJson();
|
|
||||||
|
|
||||||
// OpenAPI
|
|
||||||
builder.Services.AddOpenApi();
|
builder.Services.AddOpenApi();
|
||||||
|
|
||||||
// Health checks
|
|
||||||
builder.Services.AddHealthChecks();
|
builder.Services.AddHealthChecks();
|
||||||
|
|
||||||
// Remove individual service registrations and replace with extension
|
|
||||||
// builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
|
|
||||||
// builder.Services.AddScoped<ITokenService, TokenService>();
|
|
||||||
// builder.Services.AddScoped<IStoreService, StoreService>();
|
|
||||||
// builder.Services.AddScoped<INotificationService, NotificationService>();
|
|
||||||
// builder.Services.AddScoped<IAnalyticsService, AnalyticsService>();
|
|
||||||
|
|
||||||
builder.Services.AddApplicationServices(builder.Configuration);
|
builder.Services.AddApplicationServices(builder.Configuration);
|
||||||
|
|
||||||
// CORS
|
|
||||||
builder.Services.AddCors(p => p.AddDefaultPolicy(policy =>
|
builder.Services.AddCors(p => p.AddDefaultPolicy(policy =>
|
||||||
policy.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin()));
|
policy.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin()));
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
using (var scope = app.Services.CreateScope())
|
|
||||||
{
|
|
||||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
||||||
db.Database.Migrate();
|
|
||||||
await DbSeeder.SeedAsync(db); // seed dev data
|
|
||||||
}
|
|
||||||
|
|
||||||
app.UseSerilogRequestLogging();
|
app.UseSerilogRequestLogging();
|
||||||
|
|
||||||
if (app.Environment.IsDevelopment())
|
if (app.Environment.IsDevelopment())
|
||||||
|
|||||||
@@ -6,13 +6,14 @@ using Serilog;
|
|||||||
|
|
||||||
namespace LctMonolith.Services;
|
namespace LctMonolith.Services;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Provides aggregated analytics metrics for dashboards.
|
|
||||||
/// </summary>
|
|
||||||
public class AnalyticsService : IAnalyticsService
|
public class AnalyticsService : IAnalyticsService
|
||||||
{
|
{
|
||||||
private readonly IUnitOfWork _uow;
|
private readonly IUnitOfWork _uow;
|
||||||
public AnalyticsService(IUnitOfWork uow) => _uow = uow;
|
|
||||||
|
public AnalyticsService(IUnitOfWork uow)
|
||||||
|
{
|
||||||
|
_uow = uow;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<AnalyticsSummary> GetSummaryAsync(CancellationToken ct = default)
|
public async Task<AnalyticsSummary> GetSummaryAsync(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,17 +4,30 @@ using LctMonolith.Services.Interfaces;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
|
||||||
namespace LctMonolith.Services;
|
namespace LctMonolith.Services
|
||||||
|
|
||||||
public class DialogueService : IDialogueService
|
|
||||||
{
|
{
|
||||||
|
public class DialogueService : IDialogueService
|
||||||
|
{
|
||||||
private readonly IUnitOfWork _uow;
|
private readonly IUnitOfWork _uow;
|
||||||
public DialogueService(IUnitOfWork uow) => _uow = uow;
|
|
||||||
|
public DialogueService(IUnitOfWork uow)
|
||||||
|
{
|
||||||
|
_uow = uow;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<Dialogue?> GetDialogueByMissionIdAsync(Guid missionId)
|
public async Task<Dialogue?> GetDialogueByMissionIdAsync(Guid missionId)
|
||||||
{
|
{
|
||||||
try { return await _uow.Dialogues.Query(d => d.MissionId == missionId).FirstOrDefaultAsync(); }
|
try
|
||||||
catch (Exception ex) { Log.Error(ex, "GetDialogueByMissionIdAsync failed {MissionId}", missionId); throw; }
|
{
|
||||||
|
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)
|
public async Task<Dialogue> CreateDialogueAsync(Dialogue dialogue)
|
||||||
@@ -26,30 +39,63 @@ public class DialogueService : IDialogueService
|
|||||||
await _uow.SaveChangesAsync();
|
await _uow.SaveChangesAsync();
|
||||||
return dialogue;
|
return dialogue;
|
||||||
}
|
}
|
||||||
catch (Exception ex) { Log.Error(ex, "CreateDialogueAsync failed {MissionId}", dialogue.MissionId); throw; }
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Error(ex, "CreateDialogueAsync failed {MissionId}", dialogue.MissionId);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<DialogueMessage?> GetDialogueMessageByIdAsync(Guid messageId)
|
public async Task<DialogueMessage?> GetDialogueMessageByIdAsync(Guid messageId)
|
||||||
{
|
{
|
||||||
try { return await _uow.DialogueMessages.GetByIdAsync(messageId); }
|
try
|
||||||
catch (Exception ex) { Log.Error(ex, "GetDialogueMessageByIdAsync failed {MessageId}", messageId); throw; }
|
{
|
||||||
|
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)
|
public async Task<IEnumerable<DialogueMessageResponseOption>> GetResponseOptionsAsync(Guid messageId)
|
||||||
{
|
{
|
||||||
try { return await _uow.DialogueMessageResponseOptions.Query(o => o.ParentDialogueMessageId == messageId).ToListAsync(); }
|
try
|
||||||
catch (Exception ex) { Log.Error(ex, "GetResponseOptionsAsync failed {MessageId}", messageId); throw; }
|
{
|
||||||
|
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)
|
public async Task<DialogueMessage?> ProcessDialogueResponseAsync(Guid messageId, Guid responseOptionId, Guid playerId)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var option = await _uow.DialogueMessageResponseOptions.Query(o => o.Id == responseOptionId && o.ParentDialogueMessageId == messageId).FirstOrDefaultAsync();
|
var option = await _uow.DialogueMessageResponseOptions
|
||||||
if (option == null) return null;
|
.Query(o => o.Id == responseOptionId && o.ParentDialogueMessageId == messageId)
|
||||||
if (option.DestinationDialogueMessageId == null) return null; // end branch
|
.FirstOrDefaultAsync();
|
||||||
|
if (option == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (option.DestinationDialogueMessageId == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return await _uow.DialogueMessages.GetByIdAsync(option.DestinationDialogueMessageId);
|
return await _uow.DialogueMessages.GetByIdAsync(option.DestinationDialogueMessageId);
|
||||||
}
|
}
|
||||||
catch (Exception ex) { Log.Error(ex, "ProcessDialogueResponseAsync failed {MessageId} {ResponseId}", messageId, responseOptionId); throw; }
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Error(ex, "ProcessDialogueResponseAsync failed {MessageId} {ResponseId}", messageId, responseOptionId);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,46 +4,102 @@ using LctMonolith.Services.Interfaces;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
|
||||||
namespace LctMonolith.Services;
|
namespace LctMonolith.Services
|
||||||
|
|
||||||
public class MissionCategoryService : IMissionCategoryService
|
|
||||||
{
|
{
|
||||||
|
public class MissionCategoryService : IMissionCategoryService
|
||||||
|
{
|
||||||
private readonly IUnitOfWork _uow;
|
private readonly IUnitOfWork _uow;
|
||||||
public MissionCategoryService(IUnitOfWork uow) => _uow = uow;
|
|
||||||
|
public MissionCategoryService(IUnitOfWork uow)
|
||||||
|
{
|
||||||
|
_uow = uow;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<MissionCategory?> GetCategoryByIdAsync(Guid categoryId)
|
public async Task<MissionCategory?> GetCategoryByIdAsync(Guid categoryId)
|
||||||
{
|
{
|
||||||
try { return await _uow.MissionCategories.GetByIdAsync(categoryId); }
|
try
|
||||||
catch (Exception ex) { Log.Error(ex, "GetCategoryByIdAsync failed {CategoryId}", categoryId); throw; }
|
{
|
||||||
|
return await _uow.MissionCategories.GetByIdAsync(categoryId);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Error(ex, "GetCategoryByIdAsync failed {CategoryId}", categoryId);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<MissionCategory?> GetCategoryByTitleAsync(string title)
|
public async Task<MissionCategory?> GetCategoryByTitleAsync(string title)
|
||||||
{
|
{
|
||||||
try { return await _uow.MissionCategories.Query(c => c.Title == title).FirstOrDefaultAsync(); }
|
try
|
||||||
catch (Exception ex) { Log.Error(ex, "GetCategoryByTitleAsync failed {Title}", title); throw; }
|
{
|
||||||
|
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()
|
public async Task<IEnumerable<MissionCategory>> GetAllCategoriesAsync()
|
||||||
{
|
{
|
||||||
try { return await _uow.MissionCategories.Query().OrderBy(c => c.Title).ToListAsync(); }
|
try
|
||||||
catch (Exception ex) { Log.Error(ex, "GetAllCategoriesAsync failed"); throw; }
|
{
|
||||||
|
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)
|
public async Task<MissionCategory> CreateCategoryAsync(MissionCategory category)
|
||||||
{
|
{
|
||||||
try { category.Id = Guid.NewGuid(); await _uow.MissionCategories.AddAsync(category); await _uow.SaveChangesAsync(); return category; }
|
try
|
||||||
catch (Exception ex) { Log.Error(ex, "CreateCategoryAsync failed {Title}", category.Title); throw; }
|
{
|
||||||
|
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)
|
public async Task<MissionCategory> UpdateCategoryAsync(MissionCategory category)
|
||||||
{
|
{
|
||||||
try { _uow.MissionCategories.Update(category); await _uow.SaveChangesAsync(); return category; }
|
try
|
||||||
catch (Exception ex) { Log.Error(ex, "UpdateCategoryAsync failed {Id}", category.Id); throw; }
|
{
|
||||||
|
_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)
|
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; }
|
try
|
||||||
catch (Exception ex) { Log.Error(ex, "DeleteCategoryAsync failed {CategoryId}", categoryId); throw; }
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,50 +9,124 @@ namespace LctMonolith.Services;
|
|||||||
public class RankService : IRankService
|
public class RankService : IRankService
|
||||||
{
|
{
|
||||||
private readonly IUnitOfWork _uow;
|
private readonly IUnitOfWork _uow;
|
||||||
public RankService(IUnitOfWork uow) => _uow = uow;
|
|
||||||
|
public RankService(IUnitOfWork uow)
|
||||||
|
{
|
||||||
|
_uow = uow;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<Rank?> GetRankByIdAsync(Guid rankId)
|
public async Task<Rank?> GetRankByIdAsync(Guid rankId)
|
||||||
{
|
{
|
||||||
try { return await _uow.Ranks.GetByIdAsync(rankId); }
|
try
|
||||||
catch (Exception ex) { Log.Error(ex, "GetRankByIdAsync failed {RankId}", rankId); throw; }
|
{
|
||||||
|
return await _uow.Ranks.GetByIdAsync(rankId);
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Error(ex, "GetRankByIdAsync failed {RankId}", rankId);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<Rank?> GetRankByTitleAsync(string title)
|
public async Task<Rank?> GetRankByTitleAsync(string title)
|
||||||
{
|
{
|
||||||
try { return await _uow.Ranks.Query(r => r.Title == title).FirstOrDefaultAsync(); }
|
try
|
||||||
catch (Exception ex) { Log.Error(ex, "GetRankByTitleAsync failed {Title}", title); throw; }
|
{
|
||||||
|
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()
|
public async Task<IEnumerable<Rank>> GetAllRanksAsync()
|
||||||
{
|
{
|
||||||
try { return await _uow.Ranks.Query().OrderBy(r => r.ExpNeeded).ToListAsync(); }
|
try
|
||||||
catch (Exception ex) { Log.Error(ex, "GetAllRanksAsync failed"); throw; }
|
{
|
||||||
|
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)
|
public async Task<Rank> CreateRankAsync(Rank rank)
|
||||||
{
|
{
|
||||||
try { rank.Id = Guid.NewGuid(); await _uow.Ranks.AddAsync(rank); await _uow.SaveChangesAsync(); return rank; }
|
try
|
||||||
catch (Exception ex) { Log.Error(ex, "CreateRankAsync failed {Title}", rank.Title); throw; }
|
{
|
||||||
|
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)
|
public async Task<Rank> UpdateRankAsync(Rank rank)
|
||||||
{
|
{
|
||||||
try { _uow.Ranks.Update(rank); await _uow.SaveChangesAsync(); return rank; }
|
try
|
||||||
catch (Exception ex) { Log.Error(ex, "UpdateRankAsync failed {RankId}", rank.Id); throw; }
|
{
|
||||||
|
_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)
|
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; }
|
try
|
||||||
catch (Exception ex) { Log.Error(ex, "DeleteRankAsync failed {RankId}", rankId); throw; }
|
{
|
||||||
|
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)
|
public async Task<bool> CanPlayerAdvanceToRankAsync(Guid playerId, Guid rankId)
|
||||||
{
|
{
|
||||||
try {
|
try
|
||||||
|
{
|
||||||
var player = await _uow.Players.GetByIdAsync(playerId) ?? throw new KeyNotFoundException("Player not found");
|
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");
|
var rank = await _uow.Ranks.GetByIdAsync(rankId) ?? throw new KeyNotFoundException("Rank not found");
|
||||||
if (player.Experience < rank.ExpNeeded) return false;
|
if (player.Experience < rank.ExpNeeded)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var missionReqs = await _uow.RankMissionRules.Query(rmr => rmr.RankId == rankId).Select(r => r.MissionId).ToListAsync();
|
var missionReqs = await _uow.RankMissionRules.Query(rmr => rmr.RankId == rankId).Select(r => r.MissionId).ToListAsync();
|
||||||
if (missionReqs.Count > 0)
|
if (missionReqs.Count > 0)
|
||||||
{
|
{
|
||||||
var completed = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.Completed != null).Select(pm => pm.MissionId).ToListAsync();
|
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;
|
if (missionReqs.Except(completed).Any())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var skillReqs = await _uow.RankSkillRules.Query(rsr => rsr.RankId == rankId).ToListAsync();
|
var skillReqs = await _uow.RankSkillRules.Query(rsr => rsr.RankId == rankId).ToListAsync();
|
||||||
if (skillReqs.Count > 0)
|
if (skillReqs.Count > 0)
|
||||||
{
|
{
|
||||||
@@ -60,16 +134,38 @@ public class RankService : IRankService
|
|||||||
foreach (var req in skillReqs)
|
foreach (var req in skillReqs)
|
||||||
{
|
{
|
||||||
var ps = playerSkills.FirstOrDefault(s => s.SkillId == req.SkillId);
|
var ps = playerSkills.FirstOrDefault(s => s.SkillId == req.SkillId);
|
||||||
if (ps == null || ps.Score < req.Min) return false;
|
if (ps == null || ps.Score < req.Min)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true; }
|
|
||||||
catch (Exception ex) { Log.Error(ex, "CanPlayerAdvanceToRankAsync failed {PlayerId}->{RankId}", playerId, rankId); throw; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Error(ex, "CanPlayerAdvanceToRankAsync failed {PlayerId}->{RankId}", playerId, rankId);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<Rank?> GetNextRankAsync(Guid currentRankId)
|
public async Task<Rank?> GetNextRankAsync(Guid currentRankId)
|
||||||
{
|
{
|
||||||
try {
|
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; }
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,18 +9,36 @@ namespace LctMonolith.Services;
|
|||||||
public class RewardService : IRewardService
|
public class RewardService : IRewardService
|
||||||
{
|
{
|
||||||
private readonly IUnitOfWork _uow;
|
private readonly IUnitOfWork _uow;
|
||||||
public RewardService(IUnitOfWork uow) => _uow = uow;
|
|
||||||
|
public RewardService(IUnitOfWork uow)
|
||||||
|
{
|
||||||
|
_uow = uow;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<MissionSkillReward>> GetMissionSkillRewardsAsync(Guid missionId)
|
public async Task<IEnumerable<MissionSkillReward>> GetMissionSkillRewardsAsync(Guid missionId)
|
||||||
{
|
{
|
||||||
try { return await _uow.MissionSkillRewards.Query(r => r.MissionId == missionId, null, r => r.Skill).ToListAsync(); }
|
try
|
||||||
catch (Exception ex) { Log.Error(ex, "GetMissionSkillRewardsAsync failed {MissionId}", missionId); throw; }
|
{
|
||||||
|
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)
|
public async Task<IEnumerable<MissionItemReward>> GetMissionItemRewardsAsync(Guid missionId)
|
||||||
{
|
{
|
||||||
try { return await _uow.MissionItemRewards.Query(r => r.MissionId == missionId).ToListAsync(); }
|
try
|
||||||
catch (Exception ex) { Log.Error(ex, "GetMissionItemRewardsAsync failed {MissionId}", missionId); throw; }
|
{
|
||||||
|
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)
|
public async Task DistributeMissionRewardsAsync(Guid missionId, Guid playerId)
|
||||||
@@ -33,7 +51,6 @@ public class RewardService : IRewardService
|
|||||||
player.Experience += mission.ExpReward;
|
player.Experience += mission.ExpReward;
|
||||||
player.Mana += mission.ManaReward;
|
player.Mana += mission.ManaReward;
|
||||||
|
|
||||||
// Skill rewards
|
|
||||||
var skillRewards = await _uow.MissionSkillRewards.Query(r => r.MissionId == missionId).ToListAsync();
|
var skillRewards = await _uow.MissionSkillRewards.Query(r => r.MissionId == missionId).ToListAsync();
|
||||||
foreach (var sr in skillRewards)
|
foreach (var sr in skillRewards)
|
||||||
{
|
{
|
||||||
@@ -50,7 +67,6 @@ public class RewardService : IRewardService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Item rewards (store items) one each
|
|
||||||
var itemRewards = await _uow.MissionItemRewards.Query(r => r.MissionId == missionId).ToListAsync();
|
var itemRewards = await _uow.MissionItemRewards.Query(r => r.MissionId == missionId).ToListAsync();
|
||||||
foreach (var ir in itemRewards)
|
foreach (var ir in itemRewards)
|
||||||
{
|
{
|
||||||
@@ -60,10 +76,12 @@ public class RewardService : IRewardService
|
|||||||
inv = new UserInventoryItem { UserId = player.UserId, StoreItemId = ir.ItemId, Quantity = 1, AcquiredAt = DateTime.UtcNow };
|
inv = new UserInventoryItem { UserId = player.UserId, StoreItemId = ir.ItemId, Quantity = 1, AcquiredAt = DateTime.UtcNow };
|
||||||
await _uow.UserInventoryItems.AddAsync(inv);
|
await _uow.UserInventoryItems.AddAsync(inv);
|
||||||
}
|
}
|
||||||
else inv.Quantity += 1;
|
else
|
||||||
|
{
|
||||||
|
inv.Quantity += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark redeemed
|
|
||||||
var pm = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.MissionId == missionId).FirstOrDefaultAsync();
|
var pm = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.MissionId == missionId).FirstOrDefaultAsync();
|
||||||
if (pm != null && pm.RewardsRedeemed == null)
|
if (pm != null && pm.RewardsRedeemed == null)
|
||||||
{
|
{
|
||||||
@@ -82,11 +100,17 @@ public class RewardService : IRewardService
|
|||||||
{
|
{
|
||||||
try
|
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();
|
var pm = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.MissionId == rewardId).FirstOrDefaultAsync();
|
||||||
if (pm == null || pm.Completed == null) return false;
|
if (pm == null || pm.Completed == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return pm.RewardsRedeemed == null;
|
return pm.RewardsRedeemed == null;
|
||||||
}
|
}
|
||||||
catch (Exception ex) { Log.Error(ex, "CanClaimRewardAsync failed {RewardId} {PlayerId}", rewardId, playerId); throw; }
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Error(ex, "CanClaimRewardAsync failed {RewardId} {PlayerId}", rewardId, playerId);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,19 +9,35 @@ namespace LctMonolith.Services;
|
|||||||
public class RuleValidationService : IRuleValidationService
|
public class RuleValidationService : IRuleValidationService
|
||||||
{
|
{
|
||||||
private readonly IUnitOfWork _uow;
|
private readonly IUnitOfWork _uow;
|
||||||
public RuleValidationService(IUnitOfWork uow) => _uow = uow;
|
|
||||||
|
public RuleValidationService(IUnitOfWork uow)
|
||||||
|
{
|
||||||
|
_uow = uow;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<bool> ValidateMissionRankRulesAsync(Guid missionId, Guid playerId)
|
public async Task<bool> ValidateMissionRankRulesAsync(Guid missionId, Guid playerId)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var player = await _uow.Players.GetByIdAsync(playerId);
|
var player = await _uow.Players.GetByIdAsync(playerId);
|
||||||
if (player == null) return false;
|
if (player == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var rankRules = await _uow.MissionRankRules.Query(r => r.MissionId == missionId).Select(r => r.RankId).ToListAsync();
|
var rankRules = await _uow.MissionRankRules.Query(r => r.MissionId == missionId).Select(r => r.RankId).ToListAsync();
|
||||||
if (rankRules.Count == 0) return true; // no restriction
|
if (rankRules.Count == 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return rankRules.Contains(player.RankId);
|
return rankRules.Contains(player.RankId);
|
||||||
}
|
}
|
||||||
catch (Exception ex) { Log.Error(ex, "ValidateMissionRankRulesAsync failed {MissionId} {PlayerId}", missionId, playerId); throw; }
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Error(ex, "ValidateMissionRankRulesAsync failed {MissionId} {PlayerId}", missionId, playerId);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> ValidateRankAdvancementRulesAsync(Guid playerId, Guid targetRankId)
|
public async Task<bool> ValidateRankAdvancementRulesAsync(Guid playerId, Guid targetRankId)
|
||||||
@@ -29,18 +45,32 @@ public class RuleValidationService : IRuleValidationService
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var player = await _uow.Players.GetByIdAsync(playerId);
|
var player = await _uow.Players.GetByIdAsync(playerId);
|
||||||
if (player == null) return false;
|
if (player == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var rank = await _uow.Ranks.GetByIdAsync(targetRankId);
|
var rank = await _uow.Ranks.GetByIdAsync(targetRankId);
|
||||||
if (rank == null) return false;
|
if (rank == null)
|
||||||
if (player.Experience < rank.ExpNeeded) return false;
|
{
|
||||||
// required missions
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (player.Experience < rank.ExpNeeded)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var missionReqs = await _uow.RankMissionRules.Query(r => r.RankId == targetRankId).Select(r => r.MissionId).ToListAsync();
|
var missionReqs = await _uow.RankMissionRules.Query(r => r.RankId == targetRankId).Select(r => r.MissionId).ToListAsync();
|
||||||
if (missionReqs.Count > 0)
|
if (missionReqs.Count > 0)
|
||||||
{
|
{
|
||||||
var completed = await _uow.PlayerMissions.Query(pm => pm.PlayerId == playerId && pm.Completed != null).Select(pm => pm.MissionId).ToListAsync();
|
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;
|
if (missionReqs.Except(completed).Any())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
// required skills
|
}
|
||||||
|
|
||||||
var skillReqs = await _uow.RankSkillRules.Query(r => r.RankId == targetRankId).ToListAsync();
|
var skillReqs = await _uow.RankSkillRules.Query(r => r.RankId == targetRankId).ToListAsync();
|
||||||
if (skillReqs.Count > 0)
|
if (skillReqs.Count > 0)
|
||||||
{
|
{
|
||||||
@@ -48,17 +78,32 @@ public class RuleValidationService : IRuleValidationService
|
|||||||
foreach (var req in skillReqs)
|
foreach (var req in skillReqs)
|
||||||
{
|
{
|
||||||
var ps = playerSkills.FirstOrDefault(s => s.SkillId == req.SkillId);
|
var ps = playerSkills.FirstOrDefault(s => s.SkillId == req.SkillId);
|
||||||
if (ps == null || ps.Score < req.Min) return false;
|
if (ps == null || ps.Score < req.Min)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (Exception ex) { Log.Error(ex, "ValidateRankAdvancementRulesAsync failed {PlayerId}->{RankId}", playerId, targetRankId); throw; }
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Error(ex, "ValidateRankAdvancementRulesAsync failed {PlayerId}->{RankId}", playerId, targetRankId);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<MissionRankRule>> GetApplicableRankRulesAsync(Guid missionId)
|
public async Task<IEnumerable<MissionRankRule>> GetApplicableRankRulesAsync(Guid missionId)
|
||||||
{
|
{
|
||||||
try { return await _uow.MissionRankRules.Query(r => r.MissionId == missionId, null, r => r.Rank).ToListAsync(); }
|
try
|
||||||
catch (Exception ex) { Log.Error(ex, "GetApplicableRankRulesAsync failed {MissionId}", missionId); throw; }
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ namespace LctMonolith.Services;
|
|||||||
public class SkillService : ISkillService
|
public class SkillService : ISkillService
|
||||||
{
|
{
|
||||||
private readonly IUnitOfWork _uow;
|
private readonly IUnitOfWork _uow;
|
||||||
public SkillService(IUnitOfWork uow) => _uow = uow;
|
public SkillService(IUnitOfWork uow) { _uow = uow; }
|
||||||
|
|
||||||
public async Task<Skill?> GetSkillByIdAsync(Guid skillId)
|
public async Task<Skill?> GetSkillByIdAsync(Guid skillId)
|
||||||
{
|
{
|
||||||
@@ -38,7 +38,7 @@ public class SkillService : ISkillService
|
|||||||
}
|
}
|
||||||
public async Task<bool> DeleteSkillAsync(Guid skillId)
|
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; }
|
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; }
|
catch (Exception ex) { Log.Error(ex, "DeleteSkillAsync failed {SkillId}", skillId); throw; }
|
||||||
}
|
}
|
||||||
public async Task<PlayerSkill> UpdatePlayerSkillAsync(Guid playerId, Guid skillId, int level)
|
public async Task<PlayerSkill> UpdatePlayerSkillAsync(Guid playerId, Guid skillId, int level)
|
||||||
|
|||||||
@@ -7,9 +7,6 @@ using LctMonolith.Services.Contracts;
|
|||||||
|
|
||||||
namespace LctMonolith.Services;
|
namespace LctMonolith.Services;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Store purchase operations and inventory management.
|
|
||||||
/// </summary>
|
|
||||||
public class StoreService : IStoreService
|
public class StoreService : IStoreService
|
||||||
{
|
{
|
||||||
private readonly IUnitOfWork _uow;
|
private readonly IUnitOfWork _uow;
|
||||||
@@ -21,8 +18,15 @@ public class StoreService : IStoreService
|
|||||||
|
|
||||||
public async Task<IEnumerable<StoreItem>> GetActiveItemsAsync(CancellationToken ct = default)
|
public async Task<IEnumerable<StoreItem>> GetActiveItemsAsync(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
try { return await _uow.StoreItems.Query(i => i.IsActive).ToListAsync(ct); }
|
try
|
||||||
catch (Exception ex) { Log.Error(ex, "GetActiveItemsAsync failed"); throw; }
|
{
|
||||||
|
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)
|
public async Task<UserInventoryItem> PurchaseAsync(Guid userId, Guid itemId, int quantity, CancellationToken ct = default)
|
||||||
@@ -45,7 +49,10 @@ public class StoreService : IStoreService
|
|||||||
inv = new UserInventoryItem { UserId = userId, StoreItemId = itemId, Quantity = quantity, AcquiredAt = DateTime.UtcNow };
|
inv = new UserInventoryItem { UserId = userId, StoreItemId = itemId, Quantity = quantity, AcquiredAt = DateTime.UtcNow };
|
||||||
await _uow.UserInventoryItems.AddAsync(inv, ct);
|
await _uow.UserInventoryItems.AddAsync(inv, ct);
|
||||||
}
|
}
|
||||||
else inv.Quantity += quantity;
|
else
|
||||||
|
{
|
||||||
|
inv.Quantity += quantity;
|
||||||
|
}
|
||||||
|
|
||||||
await _uow.Transactions.AddAsync(new Transaction { UserId = userId, StoreItemId = itemId, Type = TransactionType.Purchase, ManaAmount = -totalPrice }, ct);
|
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.EventLogs.AddAsync(new EventLog { Type = EventType.ItemPurchased, UserId = userId, Data = JsonSerializer.Serialize(new { itemId, quantity, totalPrice }) }, ct);
|
||||||
|
|||||||
Reference in New Issue
Block a user