Răsfoiți Sursa

giải tháng

student 3 săptămâni în urmă
părinte
comite
cfbfd22ddd

+ 2 - 2
Kitty_Fall/Kitty_Fall/Kitty_Fall.Apis/Business/Game/GameBusinessImpl.cs

@@ -504,9 +504,9 @@ namespace Kitty_Fall.Apis.Business.Game
                         && x.StartTime < startOfMonth.AddMonths(1))
                     .SumAsync(x => (long?)x.TotalScore) ?? 0;
 
-                // Lay danh sach trung giai thang da chon theo thu tu giai.
+                // Chi hien thi giai thang da duoc CMS cong bo hoac da tra tien mat.
                 var winners = await dbContext.GameMonthlyWinners.AsNoTracking()
-                    .Where(x => x.MonthKey == monthKey && x.Status > 0)
+                    .Where(x => x.MonthKey == monthKey && (x.Status == 1 || x.Status == 2))
                     .OrderByDescending(x => x.PaidTime ?? x.AnnounceTime ?? x.CreatedTime)
                     .ToListAsync();
 

+ 1 - 0
Kitty_Fall/Kitty_Fall/Kitty_Fall.Woker/Program.cs

@@ -31,6 +31,7 @@ builder.Services.AddDbContext<ModelContext>(options =>
     options.UseOracle(builder.Configuration["Connection"]));
 
 builder.Services.AddHostedService<GameRewardPayoutWorker>();
+builder.Services.AddHostedService<GameMonthlyWinnerWorker>();
 
 
 builder.Services.AddEndpointsApiExplorer();

+ 319 - 0
Kitty_Fall/Kitty_Fall/Kitty_Fall.Woker/Services/GameMonthlyWinnerWorker.cs

@@ -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;
+        }
+    }
+}

+ 24 - 0
Kitty_Fall/Kitty_Fall/Kitty_Fall.Woker/appsettings.Development.json

@@ -16,6 +16,30 @@
     "LoyaltyPackageName": "GAME_LOYALTY",
     "DefaultPackageName": "GAME_REWARD"
   },
+  "MonthlyWinner": {
+    // Bat/tat worker tu sinh ky quay, ticket va winner draft cho giai thang.
+    "AutoGenerate": true,
+    // Neu true, worker tu chon winner nhap tu danh sach ticket de CMS duyet.
+    "AutoSelectDraftWinners": true,
+    // Neu true, winner duoc cong bo luon; neu false CMS doi status 0 -> 1.
+    "AutoAnnounceWinners": false,
+    // So giay giua moi lan worker quet xu ly giai thang.
+    "IntervalSeconds": 3600,
+    // Thang can xu ly so voi thang hien tai, -1 la thang truoc.
+    "TargetMonthOffset": -1,
+    // Diem toi thieu de user duoc sinh ticket tham gia giai thang.
+    "MinEligibleScore": 5000,
+    // So luot choi toi thieu trong thang de duoc tham gia.
+    "MinEligiblePlay": 1,
+    // So ticket mac dinh neu DB chua co rule theo khoang diem.
+    "DefaultTicketCount": 1,
+    // Gioi han so ticket toi da cho moi user.
+    "MaxTicketsPerUser": 100,
+    // Gioi han tong ticket worker sinh cho mot ky thang.
+    "MaxTotalTicket": 100000,
+    // Khong auto chon lai user da trung trong so thang gan nhat.
+    "ExcludeRecentWinnerMonths": 3
+  },
   "Logging": {
     "LogLevel": {
       "Default": "Information",

+ 24 - 0
Kitty_Fall/Kitty_Fall/Kitty_Fall.Woker/appsettings.json

@@ -16,6 +16,30 @@
     "LoyaltyPackageName": "GAME_LOYALTY",
     "DefaultPackageName": "GAME_REWARD"
   },
+  "MonthlyWinner": {
+    // Bat/tat worker tu sinh ky quay, ticket va winner draft cho giai thang.
+    "AutoGenerate": true,
+    // Neu true, worker tu chon winner nhap tu danh sach ticket de CMS duyet.
+    "AutoSelectDraftWinners": true,
+    // Neu true, winner duoc cong bo luon; neu false CMS doi status 0 -> 1.
+    "AutoAnnounceWinners": false,
+    // So giay giua moi lan worker quet xu ly giai thang.
+    "IntervalSeconds": 3600,
+    // Thang can xu ly so voi thang hien tai, -1 la thang truoc.
+    "TargetMonthOffset": -1,
+    // Diem toi thieu de user duoc sinh ticket tham gia giai thang.
+    "MinEligibleScore": 5000,
+    // So luot choi toi thieu trong thang de duoc tham gia.
+    "MinEligiblePlay": 1,
+    // So ticket mac dinh neu DB chua co rule theo khoang diem.
+    "DefaultTicketCount": 1,
+    // Gioi han so ticket toi da cho moi user.
+    "MaxTicketsPerUser": 100,
+    // Gioi han tong ticket worker sinh cho mot ky thang.
+    "MaxTotalTicket": 100000,
+    // Khong auto chon lai user da trung trong so thang gan nhat.
+    "ExcludeRecentWinnerMonths": 3
+  },
   "Logging": {
     "LogLevel": {
       "Default": "Information",