Implement First Currency Game - Stock Market

Added `StockAmount` To `User` Model (Database Update Required)
Reworked `ChatHub` Command Names
Removed Cache On User Info Endpoints
This commit is contained in:
Alan Moon 2025-06-26 16:12:50 -07:00
parent bf1f8d5cdc
commit 41e1ad4bf0
10 changed files with 278 additions and 52 deletions

View File

@ -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<ServiceResponse<int>> GetCurrentPricePerStock()
{
var price = _currencyGamesService.GetCurrentPricePerStock();
if (price != null && price.Success)
{
return Ok(new ServiceResponse<int>
{
Success = true,
Data = price.Data
});
}
else return Ok(new ServiceResponse<int>
{
Success = false,
Message = "Service Indicated Failiure"
});
}
[HttpPost("stock-market/buy-stock")]
[Authorize]
public async Task<ActionResult<ServiceResponse<UserStockActionResultDto>>> BuyStockFromMarket(int amount)
{
var identity = HttpContext.User.Identity as ClaimsIdentity;
if (identity != null)
{
IEnumerable<Claim> 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<UserStockActionResultDto> { Success = true, Data = stocks.Data });
}
else return Ok(new ServiceResponse<UserStockActionResultDto> { Success = false, Message = stocks.Message });
}
else return Ok(new ServiceResponse<UserStockActionResultDto> { Success = false, Message = "Identity User Not Found" });
}
else return Ok(new ServiceResponse<UserStockActionResultDto> { Success = false, Message = "No Identity" });
}
[HttpPost("stock-market/sell-stock")]
[Authorize]
public async Task<ActionResult<ServiceResponse<UserStockActionResultDto>>> SellStockToMarket(int amount)
{
var identity = HttpContext.User.Identity as ClaimsIdentity;
if (identity != null)
{
IEnumerable<Claim> 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<UserStockActionResultDto> { Success = true, Data = stocks.Data });
}
else return Ok(new ServiceResponse<UserStockActionResultDto> { Success = false, Message = stocks.Message });
}
else return Ok(new ServiceResponse<UserStockActionResultDto> { Success = false, Message = "Identity User Not Found" });
}
else return Ok(new ServiceResponse<UserStockActionResultDto> { Success = false, Message = "No Identity" });
}
}
}

View File

@ -20,17 +20,7 @@ namespace qtc_api.Controllers
[Authorize] [Authorize]
public async Task<ActionResult<ServiceResponse<List<UserInformationDto>>>> GetAllUsers() public async Task<ActionResult<ServiceResponse<List<UserInformationDto>>>> GetAllUsers()
{ {
var users = new ServiceResponse<List<UserInformationDto>>(); var users = await _userService.GetAllUsers();
string recordId = $"Users_{DateTime.Now.ToString("yyyyMMdd_hhmm")}";
users = await cache.GetRecordAsync<ServiceResponse<List<UserInformationDto>>>(recordId);
if(users == null)
{
users = await _userService.GetAllUsers();
await cache.SetRecordAsync(recordId, users);
}
return Ok(users); return Ok(users);
} }
@ -38,17 +28,7 @@ namespace qtc_api.Controllers
[Authorize] [Authorize]
public async Task<ActionResult<ServiceResponse<UserInformationDto>>> GetUserInformation(string id) public async Task<ActionResult<ServiceResponse<UserInformationDto>>> GetUserInformation(string id)
{ {
var user = new ServiceResponse<UserInformationDto>(); var user = await _userService.GetUserInformationById(id);
string recordId = $"User_{id}_{DateTime.Now.ToString("yyyyMMdd_hhmm")}";
user = await cache.GetRecordAsync<ServiceResponse<UserInformationDto>>(recordId);
if (user == null)
{
user = await _userService.GetUserInformationById(id);
await cache.SetRecordAsync(recordId, user);
}
return Ok(user); return Ok(user);
} }

View File

@ -0,0 +1,8 @@
namespace qtc_api.Dtos.User
{
public class UserStockActionResultDto
{
public int StockAmount { get; set; }
public int CurrencyAmount { get; set; }
}
}

View File

@ -9,7 +9,7 @@ namespace qtc_api.Extensions
{ {
var options = new DistributedCacheEntryOptions(); var options = new DistributedCacheEntryOptions();
options.AbsoluteExpirationRelativeToNow = absoluteExpireTime ?? TimeSpan.FromMinutes(5); options.AbsoluteExpirationRelativeToNow = absoluteExpireTime ?? TimeSpan.FromSeconds(15);
options.SlidingExpiration = unusuedExpireTime; options.SlidingExpiration = unusuedExpireTime;
var jsonData = JsonSerializer.Serialize(data); var jsonData = JsonSerializer.Serialize(data);

View File

@ -33,7 +33,7 @@ namespace qtc_api.Hubs
} }
} }
[HubMethodName("l")] [HubMethodName("LoginHub")]
public async Task LoginAsync(User user) public async Task LoginAsync(User user)
{ {
await Clients.All.SendAsync("rm", $"[SERVER] User {user.Username} Is Now Online"); 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<ServerConfig>(); ServerConfig serverConfig = JsonDocument.Parse(File.ReadAllText("./ServerConfig.json")).Deserialize<ServerConfig>();
await Clients.Client(ConnectedUsers.FirstOrDefault(e => e.ConnectionId == Context.ConnectionId)!.ConnectionId!).SendAsync("rc", serverConfig); await Clients.Client(ConnectedUsers.FirstOrDefault(e => e.ConnectionId == Context.ConnectionId)!.ConnectionId!).SendAsync("ReceiveServerConfig", serverConfig);
await Clients.All.SendAsync("cf", "rul"); await Clients.All.SendAsync("RefreshUserList");
} }
[HubMethodName("us")] [HubMethodName("UpdateStatus")]
public async Task UpdateStatusAsync(User user, int status) public async Task UpdateStatusAsync(User user, int status)
{ {
var statusDto = new UserStatusDto { Id = user.Id, Status = status }; var statusDto = new UserStatusDto { Id = user.Id, Status = status };
await _userService.UpdateStatus(statusDto); await _userService.UpdateStatus(statusDto);
await Clients.All.SendAsync("cf", "rul"); await Clients.All.SendAsync("RefreshUserList");
} }
[HubMethodName("jl")] [HubMethodName("JoinLobby")]
public async Task JoinHubAsync(User user) public async Task JoinLobbyAsync(User user)
{ {
await Groups.AddToGroupAsync(Context.ConnectionId, "LOBBY"); 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"); Log($"User {user.Username} Has Joined The Lobby");
} }
[HubMethodName("ll")] [HubMethodName("LeaveLobby")]
public async Task LeaveLobbyAsync(User user) public async Task LeaveLobbyAsync(User user)
{ {
await Groups.RemoveFromGroupAsync(Context.ConnectionId, "LOBBY"); 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"); Log($"User {user.Username} Has Left The Lobby");
} }
[HubMethodName("jr")] [HubMethodName("JoinRoom")]
public async Task JoinRoomAsync(User user, Room room) public async Task JoinRoomAsync(User user, Room room)
{ {
await Groups.AddToGroupAsync(Context.ConnectionId, room.Id); 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}"); Log($"User {user.Username} Has Joined {room.Name}");
} }
[HubMethodName("lr")] [HubMethodName("LeaveRoom")]
public async Task LeaveRoomAsync(User user, Room room) public async Task LeaveRoomAsync(User user, Room room)
{ {
await Groups.RemoveFromGroupAsync(Context.ConnectionId, room.Id); 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}"); Log($"User {user.Username} Has Left {room.Name}");
} }
[HubMethodName("hdr")] [HubMethodName("HandleDeletedRoom")]
public async Task HandleDeletedRoomAsync(Room room) 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.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) public async Task RefreshContactsListForUser(UserInformationDto user, User execUser)
{ {
var connection = ConnectedUsers.FirstOrDefault(e => e.ConnectedUser.Id == user.Id); var connection = ConnectedUsers.FirstOrDefault(e => e.ConnectedUser.Id == user.Id);
var connection2 = ConnectedUsers.FirstOrDefault(e => e.ConnectedUser.Id == execUser.Id); var connection2 = ConnectedUsers.FirstOrDefault(e => e.ConnectedUser.Id == execUser.Id);
if (connection != null && connection2 != null) if (connection != null && connection2 != null)
{ {
await Clients.Client(connection.ConnectionId).SendAsync("cf", "rcl"); await Clients.Client(connection.ConnectionId).SendAsync("RefreshContactsList");
await Clients.Client(connection2.ConnectionId).SendAsync("cf", "rcl"); await Clients.Client(connection2.ConnectionId).SendAsync("RefreshContactsList");
return; return;
} }
} }
[HubMethodName("s")] [HubMethodName("SendMessage")]
public async Task SendMessageAsync(User user, Message message, bool IsLobbyMsg, Room room = null!) 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; } if(IsLobbyMsg == true) { await Clients.Group("LOBBY").SendAsync("RoomMessage", $"[{user.Username}] {message.Content}"); return; }
await Clients.Group(room.Id).SendAsync("rm", $"[{user.Username}] {message.Content}"); 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) public async Task SendDirectMessageAsync(User user, UserInformationDto userToMsg, Message message)
{ {
// send direct message directly to connected user // send direct message directly to connected user
@ -135,7 +135,7 @@ namespace qtc_api.Hubs
if (connection != null) 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 }; 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; return;
} }
} }
@ -159,9 +159,6 @@ namespace qtc_api.Hubs
ConnectedUsers.Remove(connection!); ConnectedUsers.Remove(connection!);
} }
private void Log(string message) private void Log(string message) => _logger.LogInformation(message);
{
_logger.LogInformation(message);
}
} }
} }

View File

@ -13,6 +13,7 @@
public DateTime CreatedAt { get; set; } public DateTime CreatedAt { get; set; }
public int Status { get; set; } = 0; public int Status { get; set; } = 0;
public int CurrencyAmount { get; set; } = 0; public int CurrencyAmount { get; set; } = 0;
public int StockAmount { get; set; } = 0;
public DateTime LastCurrencySpin { get; set; } public DateTime LastCurrencySpin { get; set; }
public virtual IEnumerable<RefreshToken>? RefreshTokens { get; } public virtual IEnumerable<RefreshToken>? RefreshTokens { get; }

View File

@ -13,6 +13,8 @@ global using qtc_api.Services.TokenService;
global using qtc_api.Hubs; global using qtc_api.Hubs;
using qtc_api.Services.RoomService; using qtc_api.Services.RoomService;
using qtc_api.Services.ContactService; using qtc_api.Services.ContactService;
using qtc_api.Services.CurrencyGamesService;
using Microsoft.Extensions.DependencyInjection;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@ -56,6 +58,9 @@ builder.Services.AddScoped<ITokenService, TokenService>();
builder.Services.AddScoped<IRoomService, RoomService>(); builder.Services.AddScoped<IRoomService, RoomService>();
builder.Services.AddScoped<IContactService, ContactService>(); builder.Services.AddScoped<IContactService, ContactService>();
builder.Services.AddSingleton<CurrencyGamesService>();
builder.Services.AddHostedService<CurrencyGamesService>(provider => provider.GetService<CurrencyGamesService>()!);
var app = builder.Build(); var app = builder.Build();
using var scope = app.Services.CreateScope(); using var scope = app.Services.CreateScope();

View File

@ -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<CurrencyGamesService> logger;
public CurrencyGamesService(IServiceScopeFactory scopeFactory, ILogger<CurrencyGamesService> 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<int> GetCurrentPricePerStock() { return new ServiceResponse<int> { Success = true, Data = CurrentPricePerStock }; }
public async Task<ServiceResponse<UserStockActionResultDto>> BuyStock(User user, int amount)
{
using(var scope = _scopeFactory.CreateScope())
{
var userServiceScoped = scope.ServiceProvider.GetRequiredService<IUserService>();
int total = 0;
for (int i = 0; i < amount; i++)
{
total += CurrentPricePerStock;
}
if (total > user.CurrencyAmount) return new ServiceResponse<UserStockActionResultDto> { 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<UserStockActionResultDto> { Success = true, Data = new UserStockActionResultDto { StockAmount = stockResult.Data, CurrencyAmount = currencyResult.Data } };
}
}
public async Task<ServiceResponse<UserStockActionResultDto>> SellStock(User user, int amount)
{
using (var scope = _scopeFactory.CreateScope())
{
var userServiceScoped = scope.ServiceProvider.GetRequiredService<IUserService>();
if (amount > user.StockAmount) return new ServiceResponse<UserStockActionResultDto> { 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<UserStockActionResultDto> { 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}");
}
}
}

View File

@ -17,5 +17,7 @@ namespace qtc_api.Services.UserService
public Task<ServiceResponse<User>> DeleteUser(string id); public Task<ServiceResponse<User>> DeleteUser(string id);
public Task<ServiceResponse<int>> AddCurrencyToUser(string id, int amount, bool isSpinClaim); public Task<ServiceResponse<int>> AddCurrencyToUser(string id, int amount, bool isSpinClaim);
public Task<ServiceResponse<int>> RemoveCurrencyFromUser(string id, int amount); public Task<ServiceResponse<int>> RemoveCurrencyFromUser(string id, int amount);
public Task<ServiceResponse<int>> AddStockToUser(string id, int amount);
public Task<ServiceResponse<int>> RemoveStockFromUser(string id, int amount);
} }
} }

View File

@ -377,6 +377,48 @@
return response; return response;
} }
} }
public async Task<ServiceResponse<int>> AddStockToUser(string id, int amount)
{
var response = new ServiceResponse<int>();
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<ServiceResponse<int>> RemoveStockFromUser(string id, int amount)
{
var response = new ServiceResponse<int>();
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) private long LongRandom(long min, long max, Random rnd)
{ {