|
|
@@ -0,0 +1,319 @@
|
|
|
+using Database.Database;
|
|
|
+using Microsoft.EntityFrameworkCore;
|
|
|
+using Microsoft.Extensions.Configuration;
|
|
|
+using Microsoft.Extensions.DependencyInjection;
|
|
|
+using Microsoft.Extensions.Hosting;
|
|
|
+
|
|
|
+namespace Kitty_Fall.Woker.Services
|
|
|
+{
|
|
|
+ public class GameMonthlyWinnerWorker : BackgroundService
|
|
|
+ {
|
|
|
+ private const byte DrawStatusNew = 0;
|
|
|
+ private const byte DrawStatusTicketReady = 1;
|
|
|
+ private const byte DrawStatusWinnerDraft = 2;
|
|
|
+
|
|
|
+ private const byte WinnerStatusDraft = 0;
|
|
|
+ private const byte WinnerStatusAnnounced = 1;
|
|
|
+
|
|
|
+ private static readonly log4net.ILog log = log4net.LogManager.GetLogger(typeof(GameMonthlyWinnerWorker));
|
|
|
+
|
|
|
+ private readonly IServiceScopeFactory scopeFactory;
|
|
|
+ private readonly IConfiguration configuration;
|
|
|
+
|
|
|
+ public GameMonthlyWinnerWorker(IServiceScopeFactory scopeFactory, IConfiguration configuration)
|
|
|
+ {
|
|
|
+ this.scopeFactory = scopeFactory;
|
|
|
+ this.configuration = configuration;
|
|
|
+ }
|
|
|
+
|
|
|
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
|
+ {
|
|
|
+ var intervalSeconds = GetInt("MonthlyWinner:IntervalSeconds", 3600);
|
|
|
+ log.Info($"GameMonthlyWinnerWorker started. intervalSeconds={intervalSeconds}");
|
|
|
+
|
|
|
+ while (!stoppingToken.IsCancellationRequested)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ if (GetBool("MonthlyWinner:AutoGenerate", true))
|
|
|
+ {
|
|
|
+ await ProcessMonthlyWinnerAsync(stoppingToken);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ log.Error("GameMonthlyWinnerWorker loop error", ex);
|
|
|
+ }
|
|
|
+
|
|
|
+ await Task.Delay(TimeSpan.FromSeconds(intervalSeconds), stoppingToken);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task ProcessMonthlyWinnerAsync(CancellationToken cancellationToken)
|
|
|
+ {
|
|
|
+ using var scope = scopeFactory.CreateScope();
|
|
|
+ var dbContext = scope.ServiceProvider.GetRequiredService<ModelContext>();
|
|
|
+
|
|
|
+ var targetMonth = DateTime.Now.Date.AddMonths(GetInt("MonthlyWinner:TargetMonthOffset", -1));
|
|
|
+ var monthKey = targetMonth.Year * 100 + targetMonth.Month;
|
|
|
+
|
|
|
+ var draw = await EnsureDrawAsync(dbContext, monthKey, cancellationToken);
|
|
|
+ await EnsureTicketsAsync(dbContext, draw, cancellationToken);
|
|
|
+
|
|
|
+ if (GetBool("MonthlyWinner:AutoSelectDraftWinners", true))
|
|
|
+ {
|
|
|
+ await EnsureDraftWinnersAsync(dbContext, draw, cancellationToken);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task<GameMonthlyDraw> EnsureDrawAsync(ModelContext dbContext, int monthKey, CancellationToken cancellationToken)
|
|
|
+ {
|
|
|
+ var now = DateTime.Now;
|
|
|
+ var draw = await dbContext.GameMonthlyDraws
|
|
|
+ .FirstOrDefaultAsync(x => x.MonthKey == monthKey, cancellationToken);
|
|
|
+
|
|
|
+ if (draw != null) return draw;
|
|
|
+
|
|
|
+ // Tao ky quay theo thang de CMS va worker dung chung mot draw id.
|
|
|
+ draw = new GameMonthlyDraw
|
|
|
+ {
|
|
|
+ DrawId = await NextDecimalIdAsync(dbContext, "SEQ_GAME_MONTHLY_DRAW"),
|
|
|
+ MonthKey = monthKey,
|
|
|
+ Status = DrawStatusNew,
|
|
|
+ TotalEligible = 0,
|
|
|
+ TotalTicket = 0,
|
|
|
+ CreatedTime = now
|
|
|
+ };
|
|
|
+
|
|
|
+ dbContext.GameMonthlyDraws.Add(draw);
|
|
|
+ await dbContext.SaveChangesAsync(cancellationToken);
|
|
|
+ return draw;
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task EnsureTicketsAsync(ModelContext dbContext, GameMonthlyDraw draw, CancellationToken cancellationToken)
|
|
|
+ {
|
|
|
+ var hasTicket = await dbContext.GameMonthlyDrawTickets
|
|
|
+ .AsNoTracking()
|
|
|
+ .AnyAsync(x => x.DrawId == draw.DrawId, cancellationToken);
|
|
|
+ if (hasTicket) return;
|
|
|
+
|
|
|
+ var now = DateTime.Now;
|
|
|
+ // Chi user dat diem toi thieu moi duoc sinh ticket tham gia giai thang.
|
|
|
+ var minScore = GetLong("MonthlyWinner:MinEligibleScore", 5000);
|
|
|
+ var minPlay = GetLong("MonthlyWinner:MinEligiblePlay", 1);
|
|
|
+ var defaultTicketCount = GetInt("MonthlyWinner:DefaultTicketCount", 1);
|
|
|
+ var maxTicketsPerUser = GetInt("MonthlyWinner:MaxTicketsPerUser", 100);
|
|
|
+ var maxTotalTicket = GetLong("MonthlyWinner:MaxTotalTicket", 100000);
|
|
|
+
|
|
|
+ var ticketRules = await dbContext.GameMonthlyTicketRules
|
|
|
+ .AsNoTracking()
|
|
|
+ .Where(x => x.Status != false)
|
|
|
+ .OrderBy(x => x.MinScore)
|
|
|
+ .ToListAsync(cancellationToken);
|
|
|
+
|
|
|
+ var rankings = await dbContext.GameMonthlyRankings
|
|
|
+ .Where(x => x.MonthKey == draw.MonthKey
|
|
|
+ && x.TotalScore >= minScore
|
|
|
+ && x.TotalPlay >= minPlay)
|
|
|
+ .OrderByDescending(x => x.TotalScore)
|
|
|
+ .ThenByDescending(x => x.TotalPlay)
|
|
|
+ .ThenBy(x => x.Msisdn)
|
|
|
+ .ToListAsync(cancellationToken);
|
|
|
+
|
|
|
+ long ticketNo = 1;
|
|
|
+ long totalTicket = 0;
|
|
|
+ long rankNo = 1;
|
|
|
+ long totalEligible = 0;
|
|
|
+
|
|
|
+ foreach (var ranking in rankings)
|
|
|
+ {
|
|
|
+ if (totalTicket >= maxTotalTicket) break;
|
|
|
+
|
|
|
+ var ticketCount = CalculateTicketCount(ranking.TotalScore, ticketRules, defaultTicketCount, maxTicketsPerUser);
|
|
|
+ ranking.RankNo = rankNo++;
|
|
|
+ ranking.TicketCount = ticketCount;
|
|
|
+ ranking.IsEligible = ticketCount > 0;
|
|
|
+ ranking.LastCalcTime = now;
|
|
|
+ ranking.UpdatedTime = now;
|
|
|
+
|
|
|
+ if (ticketCount <= 0) continue;
|
|
|
+ totalEligible++;
|
|
|
+
|
|
|
+ // Sinh ticket theo diem thang, CMS co the chon nguoi thang tu danh sach nay.
|
|
|
+ for (var i = 0; i < ticketCount && totalTicket < maxTotalTicket; i++)
|
|
|
+ {
|
|
|
+ dbContext.GameMonthlyDrawTickets.Add(new GameMonthlyDrawTicket
|
|
|
+ {
|
|
|
+ TicketId = await NextDecimalIdAsync(dbContext, "SEQ_GAME_MONTHLY_DRAW_TICKET"),
|
|
|
+ DrawId = draw.DrawId,
|
|
|
+ MonthKey = draw.MonthKey,
|
|
|
+ Msisdn = ranking.Msisdn,
|
|
|
+ TicketNo = ticketNo++,
|
|
|
+ TotalScore = ranking.TotalScore,
|
|
|
+ CreatedTime = now
|
|
|
+ });
|
|
|
+
|
|
|
+ totalTicket++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ draw.TotalEligible = totalEligible;
|
|
|
+ draw.TotalTicket = totalTicket;
|
|
|
+ draw.Status = DrawStatusTicketReady;
|
|
|
+ draw.UpdatedTime = now;
|
|
|
+
|
|
|
+ await dbContext.SaveChangesAsync(cancellationToken);
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task EnsureDraftWinnersAsync(ModelContext dbContext, GameMonthlyDraw draw, CancellationToken cancellationToken)
|
|
|
+ {
|
|
|
+ var hasWinner = await dbContext.GameMonthlyWinners
|
|
|
+ .AsNoTracking()
|
|
|
+ .AnyAsync(x => x.DrawId == draw.DrawId, cancellationToken);
|
|
|
+ if (hasWinner) return;
|
|
|
+
|
|
|
+ var prizeConfigs = await dbContext.GameMonthlyPrizeConfigs
|
|
|
+ .AsNoTracking()
|
|
|
+ .Where(x => x.Status != false)
|
|
|
+ .OrderBy(x => x.DisplayOrder)
|
|
|
+ .ThenBy(x => x.PrizeConfigId)
|
|
|
+ .ToListAsync(cancellationToken);
|
|
|
+ if (prizeConfigs.Count == 0) return;
|
|
|
+
|
|
|
+ var tickets = await dbContext.GameMonthlyDrawTickets
|
|
|
+ .AsNoTracking()
|
|
|
+ .Where(x => x.DrawId == draw.DrawId)
|
|
|
+ .OrderBy(x => x.TicketNo)
|
|
|
+ .ToListAsync(cancellationToken);
|
|
|
+ if (tickets.Count == 0) return;
|
|
|
+
|
|
|
+ var excludedMsisdns = await GetRecentWinnerMsisdnsAsync(dbContext, draw.MonthKey, cancellationToken);
|
|
|
+ var selectedMsisdns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
|
+ var random = new Random(BuildSeed(draw.MonthKey));
|
|
|
+ var now = DateTime.Now;
|
|
|
+ var autoAnnounce = GetBool("MonthlyWinner:AutoAnnounceWinners", false);
|
|
|
+ var winnerStatus = autoAnnounce ? WinnerStatusAnnounced : WinnerStatusDraft;
|
|
|
+
|
|
|
+ foreach (var prizeConfig in prizeConfigs)
|
|
|
+ {
|
|
|
+ for (var index = 0; index < prizeConfig.Quantity; index++)
|
|
|
+ {
|
|
|
+ var ticket = PickTicket(tickets, excludedMsisdns, selectedMsisdns, random);
|
|
|
+ if (ticket == null) break;
|
|
|
+
|
|
|
+ selectedMsisdns.Add(ticket.Msisdn);
|
|
|
+
|
|
|
+ // Sinh winner nhap o trang thai draft, CMS doi status de cong bo/tra tien mat.
|
|
|
+ dbContext.GameMonthlyWinners.Add(new GameMonthlyWinner
|
|
|
+ {
|
|
|
+ WinnerId = await NextDecimalIdAsync(dbContext, "SEQ_GAME_MONTHLY_WINNER"),
|
|
|
+ DrawId = draw.DrawId,
|
|
|
+ MonthKey = draw.MonthKey,
|
|
|
+ Msisdn = ticket.Msisdn,
|
|
|
+ TicketId = ticket.TicketId,
|
|
|
+ PrizeCode = prizeConfig.PrizeCode,
|
|
|
+ PrizeName = prizeConfig.PrizeName,
|
|
|
+ PrizeValue = prizeConfig.PrizeValue,
|
|
|
+ PrizeUnit = prizeConfig.PrizeUnit,
|
|
|
+ Status = winnerStatus,
|
|
|
+ AnnounceTime = autoAnnounce ? now : null,
|
|
|
+ ResponseCode = autoAnnounce ? "AUTO_ANNOUNCED" : "DRAFT",
|
|
|
+ ResponseMessage = autoAnnounce ? "auto announced" : "waiting cms confirm",
|
|
|
+ CreatedTime = now
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ draw.Status = DrawStatusWinnerDraft;
|
|
|
+ draw.DrawTime = now;
|
|
|
+ draw.UpdatedTime = now;
|
|
|
+
|
|
|
+ await dbContext.SaveChangesAsync(cancellationToken);
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task<HashSet<string>> GetRecentWinnerMsisdnsAsync(
|
|
|
+ ModelContext dbContext,
|
|
|
+ int monthKey,
|
|
|
+ CancellationToken cancellationToken)
|
|
|
+ {
|
|
|
+ var recentMonthKeys = new List<int>();
|
|
|
+ var year = monthKey / 100;
|
|
|
+ var month = monthKey % 100;
|
|
|
+ var monthDate = new DateTime(year, month, 1);
|
|
|
+ var lookbackMonths = GetInt("MonthlyWinner:ExcludeRecentWinnerMonths", 3);
|
|
|
+
|
|
|
+ for (var i = 0; i < lookbackMonths; i++)
|
|
|
+ {
|
|
|
+ var date = monthDate.AddMonths(-i);
|
|
|
+ recentMonthKeys.Add(date.Year * 100 + date.Month);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Khong auto chon lai user da co giai thang gan day.
|
|
|
+ var msisdns = await dbContext.GameMonthlyWinners
|
|
|
+ .AsNoTracking()
|
|
|
+ .Where(x => recentMonthKeys.Contains(x.MonthKey)
|
|
|
+ && (x.Status == WinnerStatusAnnounced || x.Status == 2))
|
|
|
+ .Select(x => x.Msisdn)
|
|
|
+ .Distinct()
|
|
|
+ .ToListAsync(cancellationToken);
|
|
|
+
|
|
|
+ return new HashSet<string>(msisdns, StringComparer.OrdinalIgnoreCase);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static GameMonthlyDrawTicket? PickTicket(
|
|
|
+ List<GameMonthlyDrawTicket> tickets,
|
|
|
+ HashSet<string> excludedMsisdns,
|
|
|
+ HashSet<string> selectedMsisdns,
|
|
|
+ Random random)
|
|
|
+ {
|
|
|
+ var availableTickets = tickets
|
|
|
+ .Where(x => !excludedMsisdns.Contains(x.Msisdn) && !selectedMsisdns.Contains(x.Msisdn))
|
|
|
+ .ToList();
|
|
|
+
|
|
|
+ if (availableTickets.Count == 0) return null;
|
|
|
+ return availableTickets[random.Next(availableTickets.Count)];
|
|
|
+ }
|
|
|
+
|
|
|
+ private static int CalculateTicketCount(
|
|
|
+ long totalScore,
|
|
|
+ List<GameMonthlyTicketRule> ticketRules,
|
|
|
+ int defaultTicketCount,
|
|
|
+ int maxTicketsPerUser)
|
|
|
+ {
|
|
|
+ var rule = ticketRules.FirstOrDefault(x => totalScore >= x.MinScore
|
|
|
+ && (x.MaxScore == null || totalScore <= x.MaxScore.Value));
|
|
|
+ var ticketCount = rule?.TicketCount ?? defaultTicketCount;
|
|
|
+ if (ticketCount < 0) ticketCount = 0;
|
|
|
+ return Math.Min(ticketCount, maxTicketsPerUser);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static int BuildSeed(int monthKey)
|
|
|
+ {
|
|
|
+ return HashCode.Combine(monthKey, "KITTY_MONTHLY_WINNER");
|
|
|
+ }
|
|
|
+
|
|
|
+ private static async Task<decimal> NextDecimalIdAsync(ModelContext dbContext, string sequence)
|
|
|
+ {
|
|
|
+ return await Database.DbLogic.GenIdAsync(dbContext, sequence)
|
|
|
+ ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
|
|
+ }
|
|
|
+
|
|
|
+ private int GetInt(string key, int defaultValue)
|
|
|
+ {
|
|
|
+ var value = configuration[key];
|
|
|
+ return int.TryParse(value, out var result) ? result : defaultValue;
|
|
|
+ }
|
|
|
+
|
|
|
+ private long GetLong(string key, long defaultValue)
|
|
|
+ {
|
|
|
+ var value = configuration[key];
|
|
|
+ return long.TryParse(value, out var result) ? result : defaultValue;
|
|
|
+ }
|
|
|
+
|
|
|
+ private bool GetBool(string key, bool defaultValue)
|
|
|
+ {
|
|
|
+ var value = configuration[key];
|
|
|
+ return bool.TryParse(value, out var result) ? result : defaultValue;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|