From 41e1ad4bf0bec926d7b0ffba2aa6fda4f5474f06 Mon Sep 17 00:00:00 2001 From: AlanMoonbase Date: Thu, 26 Jun 2025 16:12:50 -0700 Subject: [PATCH] Implement First Currency Game - Stock Market Added `StockAmount` To `User` Model (Database Update Required) Reworked `ChatHub` Command Names Removed Cache On User Info Endpoints --- .../Controllers/CurrencyGamesController.cs | 95 ++++++++++++++++++ qtc-net-server/Controllers/UsersController.cs | 24 +---- .../Dtos/User/UserStockActionResultDto.cs | 8 ++ .../Extensions/DistributedCacheExtensions.cs | 2 +- qtc-net-server/Hubs/ChatHub.cs | 55 +++++------ qtc-net-server/Models/User.cs | 1 + qtc-net-server/Program.cs | 5 + .../CurrencyGamesService.cs | 96 +++++++++++++++++++ .../Services/UserService/IUserService.cs | 2 + .../Services/UserService/UserService.cs | 42 ++++++++ 10 files changed, 278 insertions(+), 52 deletions(-) create mode 100644 qtc-net-server/Controllers/CurrencyGamesController.cs create mode 100644 qtc-net-server/Dtos/User/UserStockActionResultDto.cs create mode 100644 qtc-net-server/Services/CurrencyGamesService/CurrencyGamesService.cs diff --git a/qtc-net-server/Controllers/CurrencyGamesController.cs b/qtc-net-server/Controllers/CurrencyGamesController.cs new file mode 100644 index 0000000..b61fffc --- /dev/null +++ b/qtc-net-server/Controllers/CurrencyGamesController.cs @@ -0,0 +1,95 @@ +using Microsoft.AspNetCore.Mvc; +using qtc_api.Services.CurrencyGamesService; +using System.Security.Claims; + +namespace qtc_api.Controllers +{ + [Route("api/games")] + [ApiController] + public class CurrencyGamesController : ControllerBase + { + private IUserService _userService; + private CurrencyGamesService _currencyGamesService; + public CurrencyGamesController(IUserService userService, CurrencyGamesService currencyGamesService) + { + _userService = userService; + _currencyGamesService = currencyGamesService; + } + + [HttpGet("stock-market/current-price")] + public ActionResult> GetCurrentPricePerStock() + { + var price = _currencyGamesService.GetCurrentPricePerStock(); + + if (price != null && price.Success) + { + return Ok(new ServiceResponse + { + Success = true, + Data = price.Data + }); + } + else return Ok(new ServiceResponse + { + Success = false, + Message = "Service Indicated Failiure" + }); + } + + [HttpPost("stock-market/buy-stock")] + [Authorize] + public async Task>> BuyStockFromMarket(int amount) + { + var identity = HttpContext.User.Identity as ClaimsIdentity; + + if (identity != null) + { + IEnumerable claims = identity.Claims; + var id = claims.First().Value; + + var user = await _userService.GetUserById(id); + + if (user != null && user.Success && user.Data != null) + { + var stocks = await _currencyGamesService.BuyStock(user.Data, amount); + + if (stocks.Success) + { + return Ok(new ServiceResponse { Success = true, Data = stocks.Data }); + } + else return Ok(new ServiceResponse { Success = false, Message = stocks.Message }); + } + else return Ok(new ServiceResponse { Success = false, Message = "Identity User Not Found" }); + } + else return Ok(new ServiceResponse { Success = false, Message = "No Identity" }); + } + + [HttpPost("stock-market/sell-stock")] + [Authorize] + public async Task>> SellStockToMarket(int amount) + { + var identity = HttpContext.User.Identity as ClaimsIdentity; + + if (identity != null) + { + IEnumerable claims = identity.Claims; + var id = claims.First().Value; + + var user = await _userService.GetUserById(id); + + if (user != null && user.Success && user.Data != null) + { + var stocks = await _currencyGamesService.SellStock(user.Data, amount); + + if (stocks.Success) + { + return Ok(new ServiceResponse { Success = true, Data = stocks.Data }); + } + else return Ok(new ServiceResponse { Success = false, Message = stocks.Message }); + } + else return Ok(new ServiceResponse { Success = false, Message = "Identity User Not Found" }); + } + else return Ok(new ServiceResponse { Success = false, Message = "No Identity" }); + } + } +} diff --git a/qtc-net-server/Controllers/UsersController.cs b/qtc-net-server/Controllers/UsersController.cs index 6ed4edd..4e69275 100644 --- a/qtc-net-server/Controllers/UsersController.cs +++ b/qtc-net-server/Controllers/UsersController.cs @@ -20,17 +20,7 @@ namespace qtc_api.Controllers [Authorize] public async Task>>> GetAllUsers() { - var users = new ServiceResponse>(); - - string recordId = $"Users_{DateTime.Now.ToString("yyyyMMdd_hhmm")}"; - users = await cache.GetRecordAsync>>(recordId); - - if(users == null) - { - users = await _userService.GetAllUsers(); - await cache.SetRecordAsync(recordId, users); - } - + var users = await _userService.GetAllUsers(); return Ok(users); } @@ -38,17 +28,7 @@ namespace qtc_api.Controllers [Authorize] public async Task>> GetUserInformation(string id) { - var user = new ServiceResponse(); - - string recordId = $"User_{id}_{DateTime.Now.ToString("yyyyMMdd_hhmm")}"; - user = await cache.GetRecordAsync>(recordId); - - if (user == null) - { - user = await _userService.GetUserInformationById(id); - await cache.SetRecordAsync(recordId, user); - } - + var user = await _userService.GetUserInformationById(id); return Ok(user); } diff --git a/qtc-net-server/Dtos/User/UserStockActionResultDto.cs b/qtc-net-server/Dtos/User/UserStockActionResultDto.cs new file mode 100644 index 0000000..943c3d6 --- /dev/null +++ b/qtc-net-server/Dtos/User/UserStockActionResultDto.cs @@ -0,0 +1,8 @@ +namespace qtc_api.Dtos.User +{ + public class UserStockActionResultDto + { + public int StockAmount { get; set; } + public int CurrencyAmount { get; set; } + } +} diff --git a/qtc-net-server/Extensions/DistributedCacheExtensions.cs b/qtc-net-server/Extensions/DistributedCacheExtensions.cs index e01eac4..a928441 100644 --- a/qtc-net-server/Extensions/DistributedCacheExtensions.cs +++ b/qtc-net-server/Extensions/DistributedCacheExtensions.cs @@ -9,7 +9,7 @@ namespace qtc_api.Extensions { var options = new DistributedCacheEntryOptions(); - options.AbsoluteExpirationRelativeToNow = absoluteExpireTime ?? TimeSpan.FromMinutes(5); + options.AbsoluteExpirationRelativeToNow = absoluteExpireTime ?? TimeSpan.FromSeconds(15); options.SlidingExpiration = unusuedExpireTime; var jsonData = JsonSerializer.Serialize(data); diff --git a/qtc-net-server/Hubs/ChatHub.cs b/qtc-net-server/Hubs/ChatHub.cs index 7151b37..ae836a7 100644 --- a/qtc-net-server/Hubs/ChatHub.cs +++ b/qtc-net-server/Hubs/ChatHub.cs @@ -33,7 +33,7 @@ namespace qtc_api.Hubs } } - [HubMethodName("l")] + [HubMethodName("LoginHub")] public async Task LoginAsync(User user) { await Clients.All.SendAsync("rm", $"[SERVER] User {user.Username} Is Now Online"); @@ -48,86 +48,86 @@ namespace qtc_api.Hubs ServerConfig serverConfig = JsonDocument.Parse(File.ReadAllText("./ServerConfig.json")).Deserialize(); - await Clients.Client(ConnectedUsers.FirstOrDefault(e => e.ConnectionId == Context.ConnectionId)!.ConnectionId!).SendAsync("rc", serverConfig); - await Clients.All.SendAsync("cf", "rul"); + await Clients.Client(ConnectedUsers.FirstOrDefault(e => e.ConnectionId == Context.ConnectionId)!.ConnectionId!).SendAsync("ReceiveServerConfig", serverConfig); + await Clients.All.SendAsync("RefreshUserList"); } - [HubMethodName("us")] + [HubMethodName("UpdateStatus")] public async Task UpdateStatusAsync(User user, int status) { var statusDto = new UserStatusDto { Id = user.Id, Status = status }; await _userService.UpdateStatus(statusDto); - await Clients.All.SendAsync("cf", "rul"); + await Clients.All.SendAsync("RefreshUserList"); } - [HubMethodName("jl")] - public async Task JoinHubAsync(User user) + [HubMethodName("JoinLobby")] + public async Task JoinLobbyAsync(User user) { await Groups.AddToGroupAsync(Context.ConnectionId, "LOBBY"); - await Clients.Group("LOBBY").SendAsync("rm", $"[SERVER] User {user.Username} Has Joined The Lobby"); + await Clients.Group("LOBBY").SendAsync("RoomMessage", $"[SERVER] User {user.Username} Has Joined The Lobby"); Log($"User {user.Username} Has Joined The Lobby"); } - [HubMethodName("ll")] + [HubMethodName("LeaveLobby")] public async Task LeaveLobbyAsync(User user) { await Groups.RemoveFromGroupAsync(Context.ConnectionId, "LOBBY"); - await Clients.Group("LOBBY").SendAsync("rm", $"[SERVER] User {user.Username} Has Left The Lobby"); + await Clients.Group("LOBBY").SendAsync("RoomMessage", $"[SERVER] User {user.Username} Has Left The Lobby"); Log($"User {user.Username} Has Left The Lobby"); } - [HubMethodName("jr")] + [HubMethodName("JoinRoom")] public async Task JoinRoomAsync(User user, Room room) { await Groups.AddToGroupAsync(Context.ConnectionId, room.Id); - await Clients.Group(room.Id).SendAsync("rm", $"[SERVER] User {user.Username} Has Joined The Room"); + await Clients.Group(room.Id).SendAsync("RoomMessage", $"[SERVER] User {user.Username} Has Joined {room.Name}"); Log($"User {user.Username} Has Joined {room.Name}"); } - [HubMethodName("lr")] + [HubMethodName("LeaveRoom")] public async Task LeaveRoomAsync(User user, Room room) { await Groups.RemoveFromGroupAsync(Context.ConnectionId, room.Id); - await Clients.Group(room.Id).SendAsync("rm", $"[SERVER] User {user.Username} Has Left The Room"); + await Clients.Group(room.Id).SendAsync("RoomMessage", $"[SERVER] User {user.Username} Has Left {room.Name}"); Log($"User {user.Username} Has Left {room.Name}"); } - [HubMethodName("hdr")] + [HubMethodName("HandleDeletedRoom")] public async Task HandleDeletedRoomAsync(Room room) { - await Clients.Group(room.Id).SendAsync("rm", $"[SERVER] This Room Has Been Deleted By An Administrator."); + await Clients.Group(room.Id).SendAsync("RoomMessage", $"[SERVER] This Room Has Been Deleted By An Administrator."); await Clients.Group(room.Id).SendAsync("cf", "rtl"); - await Clients.All.SendAsync("cf", "rr"); + await Clients.All.SendAsync("RefreshRoomList"); } - [HubMethodName("rcl")] + [HubMethodName("RefreshContactsListOnUser")] public async Task RefreshContactsListForUser(UserInformationDto user, User execUser) { var connection = ConnectedUsers.FirstOrDefault(e => e.ConnectedUser.Id == user.Id); var connection2 = ConnectedUsers.FirstOrDefault(e => e.ConnectedUser.Id == execUser.Id); if (connection != null && connection2 != null) { - await Clients.Client(connection.ConnectionId).SendAsync("cf", "rcl"); - await Clients.Client(connection2.ConnectionId).SendAsync("cf", "rcl"); + await Clients.Client(connection.ConnectionId).SendAsync("RefreshContactsList"); + await Clients.Client(connection2.ConnectionId).SendAsync("RefreshContactsList"); return; } } - [HubMethodName("s")] + [HubMethodName("SendMessage")] public async Task SendMessageAsync(User user, Message message, bool IsLobbyMsg, Room room = null!) { - if(IsLobbyMsg == true) { await Clients.Group("LOBBY").SendAsync("rm", $"[{user.Username}] {message.Content}"); return; } - await Clients.Group(room.Id).SendAsync("rm", $"[{user.Username}] {message.Content}"); + if(IsLobbyMsg == true) { await Clients.Group("LOBBY").SendAsync("RoomMessage", $"[{user.Username}] {message.Content}"); return; } + await Clients.Group(room.Id).SendAsync("RoomMessage", $"[{user.Username}] {message.Content}"); } - [HubMethodName("sdm")] + [HubMethodName("SendDirectMessage")] public async Task SendDirectMessageAsync(User user, UserInformationDto userToMsg, Message message) { // send direct message directly to connected user @@ -135,7 +135,7 @@ namespace qtc_api.Hubs if (connection != null) { UserInformationDto userInformationDto = new UserInformationDto { Id = user.Id, Username = user.Username, Bio = user.Bio, Role = user.Role, Status = user.Status, CreatedAt = user.CreatedAt, DateOfBirth = user.DateOfBirth, ProfilePicture = user.ProfilePicture }; - await Clients.Client(connection.ConnectionId).SendAsync("rdm", message, userInformationDto); + await Clients.Client(connection.ConnectionId).SendAsync("ReceiveDirectMessage", message, userInformationDto); return; } } @@ -159,9 +159,6 @@ namespace qtc_api.Hubs ConnectedUsers.Remove(connection!); } - private void Log(string message) - { - _logger.LogInformation(message); - } + private void Log(string message) => _logger.LogInformation(message); } } diff --git a/qtc-net-server/Models/User.cs b/qtc-net-server/Models/User.cs index c2342dd..594c039 100644 --- a/qtc-net-server/Models/User.cs +++ b/qtc-net-server/Models/User.cs @@ -13,6 +13,7 @@ public DateTime CreatedAt { get; set; } public int Status { get; set; } = 0; public int CurrencyAmount { get; set; } = 0; + public int StockAmount { get; set; } = 0; public DateTime LastCurrencySpin { get; set; } public virtual IEnumerable? RefreshTokens { get; } diff --git a/qtc-net-server/Program.cs b/qtc-net-server/Program.cs index f2610a2..a256efb 100644 --- a/qtc-net-server/Program.cs +++ b/qtc-net-server/Program.cs @@ -13,6 +13,8 @@ global using qtc_api.Services.TokenService; global using qtc_api.Hubs; using qtc_api.Services.RoomService; using qtc_api.Services.ContactService; +using qtc_api.Services.CurrencyGamesService; +using Microsoft.Extensions.DependencyInjection; var builder = WebApplication.CreateBuilder(args); @@ -56,6 +58,9 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddSingleton(); +builder.Services.AddHostedService(provider => provider.GetService()!); + var app = builder.Build(); using var scope = app.Services.CreateScope(); diff --git a/qtc-net-server/Services/CurrencyGamesService/CurrencyGamesService.cs b/qtc-net-server/Services/CurrencyGamesService/CurrencyGamesService.cs new file mode 100644 index 0000000..e7b07ea --- /dev/null +++ b/qtc-net-server/Services/CurrencyGamesService/CurrencyGamesService.cs @@ -0,0 +1,96 @@ +using qtc_api.Data; +using qtc_api.Models; +using System.Runtime.CompilerServices; + +namespace qtc_api.Services.CurrencyGamesService +{ + public class CurrencyGamesService : IHostedService, IDisposable + { + private Timer? Timer { get; set; } + private int CurrentPricePerStock { get; set; } + + private IServiceScopeFactory _scopeFactory; + private ILogger logger; + public CurrencyGamesService(IServiceScopeFactory scopeFactory, ILogger logger) + { + _scopeFactory = scopeFactory; + this.logger = logger; + } + + // Stock Market + + public Task StartAsync(CancellationToken cancellationToken) + { + Timer = new Timer(Timer_Elapsed, null, TimeSpan.Zero, TimeSpan.FromMinutes(1)); + + logger.LogInformation("Stock Market Loop Started"); + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + Timer?.Change(Timeout.Infinite, 0); + + logger.LogInformation("Stock Market Loop Stopped"); + + return Task.CompletedTask; + } + + public void Dispose() + { + Timer?.Dispose(); + } + + public ServiceResponse GetCurrentPricePerStock() { return new ServiceResponse { Success = true, Data = CurrentPricePerStock }; } + + public async Task> BuyStock(User user, int amount) + { + using(var scope = _scopeFactory.CreateScope()) + { + var userServiceScoped = scope.ServiceProvider.GetRequiredService(); + + int total = 0; + for (int i = 0; i < amount; i++) + { + total += CurrentPricePerStock; + } + + if (total > user.CurrencyAmount) return new ServiceResponse { Success = false, Message = "Cannot Afford Stock(s)" }; + + var currencyResult = await userServiceScoped.RemoveCurrencyFromUser(user.Id, total); + var stockResult = await userServiceScoped.AddStockToUser(user.Id, amount); + + return new ServiceResponse { Success = true, Data = new UserStockActionResultDto { StockAmount = stockResult.Data, CurrencyAmount = currencyResult.Data } }; + } + } + + public async Task> SellStock(User user, int amount) + { + using (var scope = _scopeFactory.CreateScope()) + { + var userServiceScoped = scope.ServiceProvider.GetRequiredService(); + + if (amount > user.StockAmount) return new ServiceResponse { Success = false, Message = "Not Enough Stocks" }; + + int total = 0; + for (int i = 0; i < amount; i++) + { + total += CurrentPricePerStock; + } + + var currencyResult = await userServiceScoped.AddCurrencyToUser(user.Id, total, false); + var stockResult = await userServiceScoped.RemoveStockFromUser(user.Id, amount); + + return new ServiceResponse { Success = true, Data = new UserStockActionResultDto { StockAmount = stockResult.Data, CurrencyAmount = currencyResult.Data } }; + } + } + + private void Timer_Elapsed(object? state) + { + Random rnd = new Random(); + CurrentPricePerStock = rnd.Next(5, 200); + logger.LogInformation($"Current Price Per Stock - {CurrentPricePerStock}"); + } + } +} diff --git a/qtc-net-server/Services/UserService/IUserService.cs b/qtc-net-server/Services/UserService/IUserService.cs index bff847e..592a557 100644 --- a/qtc-net-server/Services/UserService/IUserService.cs +++ b/qtc-net-server/Services/UserService/IUserService.cs @@ -17,5 +17,7 @@ namespace qtc_api.Services.UserService public Task> DeleteUser(string id); public Task> AddCurrencyToUser(string id, int amount, bool isSpinClaim); public Task> RemoveCurrencyFromUser(string id, int amount); + public Task> AddStockToUser(string id, int amount); + public Task> RemoveStockFromUser(string id, int amount); } } diff --git a/qtc-net-server/Services/UserService/UserService.cs b/qtc-net-server/Services/UserService/UserService.cs index dd38f98..51b23ef 100644 --- a/qtc-net-server/Services/UserService/UserService.cs +++ b/qtc-net-server/Services/UserService/UserService.cs @@ -377,6 +377,48 @@ return response; } } + public async Task> AddStockToUser(string id, int amount) + { + var response = new ServiceResponse(); + var user = _dataContext.Users.FirstOrDefault(e => e.Id == id); + + if (user != null) + { + user.StockAmount += amount; + await _dataContext.SaveChangesAsync(); + + response.Success = true; + response.Data = user.StockAmount; + return response; + } else + { + response.Success = false; + response.Message = "User Not Found"; + return response; + } + } + + public async Task> RemoveStockFromUser(string id, int amount) + { + var response = new ServiceResponse(); + var user = _dataContext.Users.FirstOrDefault(e => e.Id == id); + + if (user != null) + { + user.StockAmount -= amount; + await _dataContext.SaveChangesAsync(); + + response.Success = true; + response.Data = user.StockAmount; + return response; + } + else + { + response.Success = false; + response.Message = "User Not Found"; + return response; + } + } private long LongRandom(long min, long max, Random rnd) {