🐛 Fixes of lotteries and enrich features
This commit is contained in:
@@ -44,6 +44,10 @@ public class LotteryController(AppDatabase db, LotteryService lotteryService) :
|
||||
{
|
||||
return BadRequest(err.Message);
|
||||
}
|
||||
catch (InvalidOperationException err)
|
||||
{
|
||||
return BadRequest(err.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@@ -80,21 +84,20 @@ public class LotteryController(AppDatabase db, LotteryService lotteryService) :
|
||||
[RequiredPermission("maintenance", "lotteries.draw.perform")]
|
||||
public async Task<IActionResult> PerformLotteryDraw()
|
||||
{
|
||||
await lotteryService.PerformDailyDrawAsync();
|
||||
return Ok("Lottery draw performed successfully.");
|
||||
await lotteryService.DrawLotteries();
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpGet("records")]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<List<SnLotteryRecord>>> GetLotteryRecords(
|
||||
[FromQuery] Instant? startDate = null,
|
||||
[FromQuery] Instant? endDate = null,
|
||||
[FromQuery] int offset = 0,
|
||||
[FromQuery] int limit = 20)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not SnAccount currentUser) return Unauthorized();
|
||||
|
||||
var query = db.LotteryRecords.AsQueryable();
|
||||
var query = db.LotteryRecords
|
||||
.OrderByDescending(r => r.CreatedAt)
|
||||
.AsQueryable();
|
||||
|
||||
if (startDate.HasValue)
|
||||
query = query.Where(r => r.DrawDate >= startDate.Value);
|
||||
@@ -105,7 +108,6 @@ public class LotteryController(AppDatabase db, LotteryService lotteryService) :
|
||||
Response.Headers["X-Total"] = total.ToString();
|
||||
|
||||
var records = await query
|
||||
.OrderByDescending(r => r.DrawDate)
|
||||
.Skip(offset)
|
||||
.Take(limit)
|
||||
.ToListAsync();
|
||||
|
||||
@@ -10,7 +10,7 @@ public class LotteryDrawJob(LotteryService lotteryService, ILogger<LotteryDrawJo
|
||||
|
||||
try
|
||||
{
|
||||
await lotteryService.PerformDailyDrawAsync();
|
||||
await lotteryService.DrawLotteries();
|
||||
logger.LogInformation("Daily lottery draw completed successfully.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -1,12 +1,28 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Pass.Wallet;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NodaTime;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace DysonNetwork.Pass.Lotteries;
|
||||
|
||||
public class LotteryService(AppDatabase db, PaymentService paymentService, WalletService walletService)
|
||||
public class LotteryOrderMetaData
|
||||
{
|
||||
public Guid AccountId { get; set; }
|
||||
public List<int> RegionOneNumbers { get; set; } = new();
|
||||
public int RegionTwoNumber { get; set; }
|
||||
public int Multiplier { get; set; } = 1;
|
||||
}
|
||||
|
||||
public class LotteryService(
|
||||
AppDatabase db,
|
||||
PaymentService paymentService,
|
||||
WalletService walletService,
|
||||
ILogger<LotteryService> logger)
|
||||
{
|
||||
private readonly ILogger<LotteryService> _logger = logger;
|
||||
|
||||
private static bool ValidateNumbers(List<int> region1, int region2)
|
||||
{
|
||||
if (region1.Count != 5 || region1.Distinct().Count() != 5)
|
||||
@@ -62,19 +78,33 @@ public class LotteryService(AppDatabase db, PaymentService paymentService, Walle
|
||||
return 10 + (multiplier - 1) * 10;
|
||||
}
|
||||
|
||||
public async Task<SnWalletOrder> CreateLotteryOrderAsync(Guid accountId, List<int> region1, int region2, int multiplier = 1)
|
||||
public async Task<SnWalletOrder> CreateLotteryOrderAsync(Guid accountId, List<int> region1, int region2,
|
||||
int multiplier = 1)
|
||||
{
|
||||
if (!ValidateNumbers(region1, region2))
|
||||
throw new ArgumentException("Invalid lottery numbers");
|
||||
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
var todayStart = new LocalDateTime(now.InUtc().Year, now.InUtc().Month, now.InUtc().Day, 0, 0).InUtc().ToInstant();
|
||||
var hasPurchasedToday = await db.Lotteries.AnyAsync(l => l.AccountId == accountId && l.CreatedAt >= todayStart);
|
||||
var todayStart = new LocalDateTime(now.InUtc().Year, now.InUtc().Month, now.InUtc().Day, 0, 0).InUtc()
|
||||
.ToInstant();
|
||||
var hasPurchasedToday = await db.Lotteries.AnyAsync(l =>
|
||||
l.AccountId == accountId &&
|
||||
l.CreatedAt >= todayStart &&
|
||||
l.DrawStatus == LotteryDrawStatus.Pending
|
||||
);
|
||||
if (hasPurchasedToday)
|
||||
throw new InvalidOperationException("You can only purchase one lottery per day.");
|
||||
|
||||
var price = CalculateLotteryPrice(multiplier);
|
||||
|
||||
var lotteryData = new LotteryOrderMetaData
|
||||
{
|
||||
AccountId = accountId,
|
||||
RegionOneNumbers = region1,
|
||||
RegionTwoNumber = region2,
|
||||
Multiplier = multiplier
|
||||
};
|
||||
|
||||
return await paymentService.CreateOrderAsync(
|
||||
null,
|
||||
WalletCurrency.SourcePoint,
|
||||
@@ -83,29 +113,33 @@ public class LotteryService(AppDatabase db, PaymentService paymentService, Walle
|
||||
productIdentifier: "lottery",
|
||||
meta: new Dictionary<string, object>
|
||||
{
|
||||
["account_id"] = accountId.ToString(),
|
||||
["region_one_numbers"] = region1,
|
||||
["region_two_number"] = region2,
|
||||
["multiplier"] = multiplier
|
||||
["data"] = JsonSerializer.Serialize(lotteryData)
|
||||
});
|
||||
}
|
||||
|
||||
public async Task HandleLotteryOrder(SnWalletOrder order)
|
||||
{
|
||||
if (order.Status == OrderStatus.Finished)
|
||||
return; // Already processed
|
||||
|
||||
if (order.Status != OrderStatus.Paid ||
|
||||
!order.Meta.TryGetValue("account_id", out var accountIdValue) ||
|
||||
!order.Meta.TryGetValue("region_one_numbers", out var region1Value) ||
|
||||
!order.Meta.TryGetValue("region_two_number", out var region2Value) ||
|
||||
!order.Meta.TryGetValue("multiplier", out var multiplierValue))
|
||||
!order.Meta.TryGetValue("data", out var dataValue) ||
|
||||
dataValue is null ||
|
||||
dataValue is not JsonElement { ValueKind: JsonValueKind.String } jsonElem)
|
||||
throw new InvalidOperationException("Invalid order.");
|
||||
|
||||
var accountId = Guid.Parse((string)accountIdValue!);
|
||||
var region1Json = (System.Text.Json.JsonElement)region1Value;
|
||||
var region1 = region1Json.EnumerateArray().Select(e => e.GetInt32()).ToList();
|
||||
var region2 = Convert.ToInt32((string)region2Value!);
|
||||
var multiplier = Convert.ToInt32((string)multiplierValue!);
|
||||
var jsonString = jsonElem.GetString();
|
||||
if (jsonString is null)
|
||||
throw new InvalidOperationException("Invalid order.");
|
||||
|
||||
await CreateTicketAsync(accountId, region1, region2, multiplier);
|
||||
var data = JsonSerializer.Deserialize<LotteryOrderMetaData>(jsonString);
|
||||
if (data is null)
|
||||
throw new InvalidOperationException("Invalid order data.");
|
||||
|
||||
await CreateTicketAsync(data.AccountId, data.RegionOneNumbers, data.RegionTwoNumber, data.Multiplier);
|
||||
|
||||
order.Status = OrderStatus.Finished;
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private static int CalculateReward(int region1Matches, bool region2Match)
|
||||
@@ -133,6 +167,7 @@ public class LotteryService(AppDatabase db, PaymentService paymentService, Walle
|
||||
var num = random.Next(min, max + 1);
|
||||
if (!numbers.Contains(num)) numbers.Add(num);
|
||||
}
|
||||
|
||||
return numbers.OrderBy(n => n).ToList();
|
||||
}
|
||||
|
||||
@@ -141,68 +176,101 @@ public class LotteryService(AppDatabase db, PaymentService paymentService, Walle
|
||||
return playerNumbers.Intersect(winningNumbers).Count();
|
||||
}
|
||||
|
||||
public async Task PerformDailyDrawAsync()
|
||||
public async Task DrawLotteries()
|
||||
{
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
var yesterdayStart = new LocalDateTime(now.InUtc().Year, now.InUtc().Month, now.InUtc().Day, 0, 0).InUtc().ToInstant().Minus(Duration.FromDays(1));
|
||||
var todayStart = new LocalDateTime(now.InUtc().Year, now.InUtc().Month, now.InUtc().Day, 0, 0).InUtc().ToInstant();
|
||||
|
||||
// Tickets purchased yesterday that are still pending draw
|
||||
var tickets = await db.Lotteries
|
||||
.Where(l => l.CreatedAt >= yesterdayStart && l.CreatedAt < todayStart && l.DrawStatus == LotteryDrawStatus.Pending)
|
||||
.ToListAsync();
|
||||
|
||||
if (!tickets.Any()) return;
|
||||
|
||||
// Generate winning numbers
|
||||
var winningRegion1 = GenerateUniqueRandomNumbers(5, 0, 99);
|
||||
var winningRegion2 = GenerateUniqueRandomNumbers(1, 0, 99)[0];
|
||||
|
||||
var drawDate = Instant.FromDateTimeUtc(DateTime.Today.AddDays(-1)); // Yesterday's date
|
||||
|
||||
var totalPrizesAwarded = 0;
|
||||
long totalPrizeAmount = 0;
|
||||
|
||||
// Process each ticket
|
||||
foreach (var ticket in tickets)
|
||||
try
|
||||
{
|
||||
var region1Matches = CountMatches(ticket.RegionOneNumbers, winningRegion1);
|
||||
var region2Match = ticket.RegionTwoNumber == winningRegion2;
|
||||
var reward = CalculateReward(region1Matches, region2Match);
|
||||
_logger.LogInformation("Starting drawing lotteries...");
|
||||
|
||||
if (reward > 0)
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
|
||||
// All pending lottery tickets
|
||||
var tickets = await db.Lotteries
|
||||
.Where(l => l.DrawStatus == LotteryDrawStatus.Pending)
|
||||
.ToListAsync();
|
||||
|
||||
if (tickets.Count == 0)
|
||||
{
|
||||
var wallet = await walletService.GetWalletAsync(ticket.AccountId);
|
||||
if (wallet != null)
|
||||
{
|
||||
await paymentService.CreateTransactionAsync(
|
||||
payerWalletId: null,
|
||||
payeeWalletId: wallet.Id,
|
||||
currency: "isp",
|
||||
amount: reward,
|
||||
remarks: $"Lottery prize: {region1Matches} matches{(region2Match ? " + special" : "")}"
|
||||
);
|
||||
totalPrizesAwarded++;
|
||||
totalPrizeAmount += reward;
|
||||
}
|
||||
_logger.LogInformation("No pending lottery tickets");
|
||||
return;
|
||||
}
|
||||
|
||||
ticket.DrawStatus = LotteryDrawStatus.Drawn;
|
||||
ticket.DrawDate = drawDate;
|
||||
_logger.LogInformation("Found {Count} pending lottery tickets for draw", tickets.Count);
|
||||
|
||||
// Generate winning numbers
|
||||
var winningRegion1 = GenerateUniqueRandomNumbers(5, 0, 99);
|
||||
var winningRegion2 = GenerateUniqueRandomNumbers(1, 0, 99)[0];
|
||||
|
||||
_logger.LogInformation("Winning numbers generated: Region1 [{Region1}], Region2 [{Region2}]",
|
||||
string.Join(",", winningRegion1), winningRegion2);
|
||||
|
||||
var drawDate = Instant.FromDateTimeUtc(new DateTime(DateTime.UtcNow.Year, DateTime.UtcNow.Month,
|
||||
DateTime.UtcNow.Day, 0, 0, 0, DateTimeKind.Utc).AddDays(-1)); // Yesterday's date
|
||||
|
||||
var totalPrizesAwarded = 0;
|
||||
long totalPrizeAmount = 0;
|
||||
|
||||
// Process each ticket
|
||||
foreach (var ticket in tickets)
|
||||
{
|
||||
var region1Matches = CountMatches(ticket.RegionOneNumbers, winningRegion1);
|
||||
var region2Match = ticket.RegionTwoNumber == winningRegion2;
|
||||
var reward = CalculateReward(region1Matches, region2Match);
|
||||
|
||||
// Record match results
|
||||
ticket.MatchedRegionOneNumbers = ticket.RegionOneNumbers.Intersect(winningRegion1).ToList();
|
||||
ticket.MatchedRegionTwoNumber = region2Match ? (int?)winningRegion2 : null;
|
||||
|
||||
if (reward > 0)
|
||||
{
|
||||
var wallet = await walletService.GetWalletAsync(ticket.AccountId);
|
||||
if (wallet != null)
|
||||
{
|
||||
await paymentService.CreateTransactionAsync(
|
||||
payerWalletId: null,
|
||||
payeeWalletId: wallet.Id,
|
||||
currency: WalletCurrency.SourcePoint,
|
||||
amount: reward,
|
||||
remarks: $"Lottery prize: {region1Matches} matches{(region2Match ? " + special" : "")}"
|
||||
);
|
||||
_logger.LogInformation(
|
||||
"Awarded {Amount} to account {AccountId} for {Matches} matches{(Special ? \" + special\" : \"\")}",
|
||||
reward, ticket.AccountId, region1Matches, region2Match ? " + special" : "");
|
||||
totalPrizesAwarded++;
|
||||
totalPrizeAmount += reward;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Wallet not found for account {AccountId}, skipping prize award",
|
||||
ticket.AccountId);
|
||||
}
|
||||
}
|
||||
|
||||
ticket.DrawStatus = LotteryDrawStatus.Drawn;
|
||||
ticket.DrawDate = drawDate;
|
||||
}
|
||||
|
||||
// Save the draw record
|
||||
var lotteryRecord = new SnLotteryRecord
|
||||
{
|
||||
DrawDate = drawDate,
|
||||
WinningRegionOneNumbers = winningRegion1,
|
||||
WinningRegionTwoNumber = winningRegion2,
|
||||
TotalTickets = tickets.Count,
|
||||
TotalPrizesAwarded = totalPrizesAwarded,
|
||||
TotalPrizeAmount = totalPrizeAmount
|
||||
};
|
||||
|
||||
db.LotteryRecords.Add(lotteryRecord);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("Daily lottery draw completed: {Prizes} prizes awarded, total amount {Amount}",
|
||||
totalPrizesAwarded, totalPrizeAmount);
|
||||
}
|
||||
|
||||
// Save the draw record
|
||||
var lotteryRecord = new SnLotteryRecord
|
||||
catch (Exception ex)
|
||||
{
|
||||
DrawDate = drawDate,
|
||||
WinningRegionOneNumbers = winningRegion1,
|
||||
WinningRegionTwoNumber = winningRegion2,
|
||||
TotalTickets = tickets.Count,
|
||||
TotalPrizesAwarded = totalPrizesAwarded,
|
||||
TotalPrizeAmount = totalPrizeAmount
|
||||
};
|
||||
|
||||
db.LotteryRecords.Add(lotteryRecord);
|
||||
await db.SaveChangesAsync();
|
||||
_logger.LogError(ex, "An error occurred during the daily lottery draw");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2620
DysonNetwork.Pass/Migrations/20251024154539_AddDetailLotteriesStatus.Designer.cs
generated
Normal file
2620
DysonNetwork.Pass/Migrations/20251024154539_AddDetailLotteriesStatus.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,39 @@
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Pass.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddDetailLotteriesStatus : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<List<int>>(
|
||||
name: "matched_region_one_numbers",
|
||||
table: "lotteries",
|
||||
type: "jsonb",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "matched_region_two_number",
|
||||
table: "lotteries",
|
||||
type: "integer",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "matched_region_one_numbers",
|
||||
table: "lotteries");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "matched_region_two_number",
|
||||
table: "lotteries");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1086,6 +1086,14 @@ namespace DysonNetwork.Pass.Migrations
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("draw_status");
|
||||
|
||||
b.Property<List<int>>("MatchedRegionOneNumbers")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("matched_region_one_numbers");
|
||||
|
||||
b.Property<int?>("MatchedRegionTwoNumber")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("matched_region_two_number");
|
||||
|
||||
b.Property<int>("Multiplier")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("multiplier");
|
||||
|
||||
@@ -30,20 +30,25 @@ public class SnLotteryRecord : ModelBase
|
||||
|
||||
public class SnLottery : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public Guid Id { get; init; } = Guid.NewGuid();
|
||||
|
||||
public SnAccount Account { get; set; } = null!;
|
||||
public Guid AccountId { get; set; }
|
||||
public SnAccount Account { get; init; } = null!;
|
||||
public Guid AccountId { get; init; }
|
||||
|
||||
[Column(TypeName = "jsonb")]
|
||||
public List<int> RegionOneNumbers { get; set; } = new(); // 5 numbers, 0-99, unique
|
||||
public List<int> RegionOneNumbers { get; set; } = []; // 5 numbers, 0-99, unique
|
||||
|
||||
[Range(0, 99)]
|
||||
public int RegionTwoNumber { get; set; } // 1 number, 0-99, can repeat
|
||||
public int RegionTwoNumber { get; init; } // 1 number, 0-99, can repeat
|
||||
|
||||
public int Multiplier { get; set; } = 1; // Default 1x
|
||||
public int Multiplier { get; init; } = 1; // Default 1x
|
||||
|
||||
public LotteryDrawStatus DrawStatus { get; set; } = LotteryDrawStatus.Pending; // Status to track draw processing
|
||||
|
||||
public Instant? DrawDate { get; set; } // Date when this ticket was drawn
|
||||
|
||||
[Column(TypeName = "jsonb")]
|
||||
public List<int>? MatchedRegionOneNumbers { get; set; } // The actual numbers that matched in region one
|
||||
|
||||
public int? MatchedRegionTwoNumber { get; set; } // The matched number if special number matched (null otherwise)
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ public class BroadcastEventHandler(
|
||||
break;
|
||||
}
|
||||
default:
|
||||
await msg.NakAsync(cancellationToken: stoppingToken);
|
||||
// ignore
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user