student 3 недель назад
Родитель
Сommit
993fa7a981

+ 2 - 0
Kitty_Fall/Kitty_Fall/Database/Database/ModelContext.cs

@@ -878,6 +878,8 @@ public partial class ModelContext : DbContext
 
             entity.HasIndex(e => new { e.DrawId, e.PrizeCode, e.Msisdn }, "UK_GMW_DRAW_PRIZE_MSISDN").IsUnique();
 
+            entity.HasIndex(e => new { e.DrawId, e.Msisdn }, "UK_GMW_DRAW_MSISDN").IsUnique();
+
             entity.Property(e => e.WinnerId)
                 .HasPrecision(18)
                 .ValueGeneratedNever()

+ 49 - 29
Kitty_Fall/Kitty_Fall/Kitty_Fall.Apis/Business/Game/GameBusinessImpl.cs

@@ -121,12 +121,18 @@ namespace Kitty_Fall.Apis.Business.Game
                 var items = attempts.Select(attempt =>
                 {
                     var eventTime = attempt.SubmitTime ?? attempt.StartTime;
-                    var rewardLog = attempt.GameRewardLog != null && attempt.GameRewardLog.Status == 2
-                        ? attempt.GameRewardLog
-                        : null;
-                    var rewardType = rewardLog?.RewardType ?? attempt.RewardType ?? "";
-                    var rewardValue = rewardLog?.RewardValue ?? attempt.RewardValue ?? 0;
-                    var rewardUnit = rewardLog?.RewardUnit ?? attempt.RewardUnit ?? "";
+                    // Ket qua random tren ATTEMPT la nguon su that. Reward log chi la
+                    // trang thai payout, worker khong duoc lam thay doi loai da trung.
+                    var hasAttemptReward = !string.IsNullOrWhiteSpace(attempt.RewardType);
+                    var rewardType = hasAttemptReward
+                        ? attempt.RewardType!
+                        : attempt.GameRewardLog?.RewardType ?? "";
+                    var rewardValue = hasAttemptReward
+                        ? attempt.RewardValue ?? 0
+                        : attempt.GameRewardLog?.RewardValue ?? 0;
+                    var rewardUnit = hasAttemptReward
+                        ? attempt.RewardUnit ?? ""
+                        : attempt.GameRewardLog?.RewardUnit ?? "";
                     if (string.IsNullOrWhiteSpace(rewardType) && attempt.AwardedScore > 0)
                     {
                         // Level thuong khong random reward option van phai hien thi
@@ -388,41 +394,47 @@ namespace Kitty_Fall.Apis.Business.Game
                         && x.StartTime < startOfMonth.AddMonths(1))
                     .SumAsync(x => (long?)x.TotalScore) ?? 0;
 
-                // Lay thue bao trung DATA/LOYALTY trong ngay da chon.
-                // SCORE la diem noi bo nen khong hien thi o man danh sach trung ngay.
-                var winnerQuery = dbContext.GameRewardLogs.AsNoTracking()
+                // Daily Winners la ket qua choi trung thuong cua tat ca thue bao.
+                // Lay tu ATTEMPT (ket qua random goc), khong cho worker payout thanh cong
+                // moi hien thi. SCORE la diem noi bo nen khong nam trong danh sach nay.
+                var winnerQuery = dbContext.GameLevelAttempts.AsNoTracking()
                     .Where(x => x.Status == 2
                         && (x.RewardType == "DATA" || x.RewardType == "LOYALTY")
-                        && ((x.ResponseTime ?? x.CreatedTime) >= selectedDate)
-                        && ((x.ResponseTime ?? x.CreatedTime) < toExclusive))
-                    .OrderByDescending(x => x.ResponseTime ?? x.CreatedTime)
-                    .ThenByDescending(x => x.RewardLogId);
+                        && x.RewardValue != null
+                        && x.RewardValue > 0
+                        && ((x.SubmitTime ?? x.CreatedTime) >= selectedDate)
+                        && ((x.SubmitTime ?? x.CreatedTime) < toExclusive))
+                    .OrderByDescending(x => x.SubmitTime ?? x.CreatedTime)
+                    .ThenByDescending(x => x.AttemptId);
 
                 var totalRow = await winnerQuery.CountAsync();
 
                 // Lay so dong can hien thi tren bang thiet ke.
-                var rewardLogs = await winnerQuery
+                var winnerAttempts = await winnerQuery
                     .Take(pageSize)
                     .ToListAsync();
 
-                // Chuyen log thuong sang dinh dang web dang dung.
-                var items = rewardLogs.Select((rewardLog, index) =>
+                // Chuyen ket qua random trung thuong sang dinh dang web dang dung.
+                var items = winnerAttempts.Select((attempt, index) =>
                 {
-                    var rewardTime = rewardLog.ResponseTime ?? rewardLog.CreatedTime;
+                    var rewardTime = attempt.SubmitTime ?? attempt.CreatedTime;
+                    var rewardType = attempt.RewardType ?? "";
+                    var rewardValue = attempt.RewardValue ?? 0;
+                    var rewardUnit = attempt.RewardUnit ?? "";
 
                     return new UserDailyWinnerItem
                     {
                         no = index + 1,
-                        msisdn = rewardLog.Msisdn,
-                        phoneText = MaskMiddleFiveMsisdn(rewardLog.Msisdn),
+                        msisdn = attempt.Msisdn,
+                        phoneText = MaskMiddleFiveMsisdn(attempt.Msisdn),
                         timeText = rewardTime.ToString("HH:mm:ss", CultureInfo.InvariantCulture),
-                        rewardType = rewardLog.RewardType,
-                        rewardValue = rewardLog.RewardValue,
-                        rewardValueText = FormatRewardValue(rewardLog.RewardValue),
-                        rewardUnit = rewardLog.RewardUnit,
-                        rewardUnitText = ToRewardUnitText(rewardLog.RewardType, rewardLog.RewardUnit),
-                        rewardCssClass = ToRewardCssClass(rewardLog.RewardType),
-                        rewardIcon = ToRewardIcon(rewardLog.RewardType)
+                        rewardType = rewardType,
+                        rewardValue = rewardValue,
+                        rewardValueText = FormatRewardValue(rewardValue),
+                        rewardUnit = rewardUnit,
+                        rewardUnitText = ToRewardUnitText(rewardType, rewardUnit),
+                        rewardCssClass = ToRewardCssClass(rewardType),
+                        rewardIcon = ToRewardIcon(rewardType)
                     };
                 }).ToList();
 
@@ -767,12 +779,19 @@ namespace Kitty_Fall.Apis.Business.Game
                     // DATA/LOYALTY/SCORE. Khong cong them score client tai level moc.
                     awardedScore = 0;
                     var rewardOptions = await dbContext.GameLevelRewardOptions.AsNoTracking()
-                        .Where(x => x.ModeId == mode.ModeId && x.LevelNo == levelNo.Value && x.Status != false)
+                        .Where(x => x.ModeId == mode.ModeId
+                            && x.LevelNo == levelNo.Value
+                            && x.Status != false
+                            && (x.RewardType == "DATA" || x.RewardType == "LOYALTY" || x.RewardType == "SCORE"))
                         .OrderByDescending(x => x.IsDefault == true)
                         .ThenByDescending(x => x.Weight)
                         .ThenBy(x => x.RewardOptionId)
                         .ToListAsync();
                     rewardOption = SelectRewardOption(rewardOptions);
+                    if (rewardOption != null)
+                    {
+                        log.Info($"Milestone reward selected. msisdn={msisdn}, transId={request.transId}, modeId={mode.ModeId}, level={levelNo.Value}, optionId={rewardOption.RewardOptionId}, rewardType={rewardOption.RewardType}, rewardValue={rewardOption.RewardValue}, rewardUnit={rewardOption.RewardUnit}");
+                    }
                 }
 
                 var attemptNo = ((await dbContext.GameLevelAttempts.AsNoTracking()
@@ -832,8 +851,8 @@ namespace Kitty_Fall.Apis.Business.Game
                 });
 
                 var responseData = "";
-                // Basic score cua level chi duoc phan anh trong score. Point va thong tin
-                // reward chi co gia tri khi level milestone thuc su random ra phan thuong.
+                // score la tong SCORE game. point chi la gia tri LOYALTY de popup
+                // hien thi, khong duoc cong vao score/ranking.
                 var responsePoint = "0";
                 var responseRewardType = "";
                 decimal responseRewardValue = 0;
@@ -878,6 +897,7 @@ namespace Kitty_Fall.Apis.Business.Game
                     {
                         // LOYALTY la point cua he thong ngoai game. Chi tao reward log
                         // de worker cong ben ngoai va luu lich su, khong cong vao score/ranking.
+                        responsePoint = FormatPrizeValue(rewardOption.RewardValue);
                     }
 
                     var waitWorkerPayout = string.Equals(rewardOption.RewardType, "DATA", StringComparison.OrdinalIgnoreCase)

+ 13 - 4
Kitty_Fall/Kitty_Fall/Kitty_Fall.Cms/Controllers/MonthlyWinnerController.cs

@@ -132,15 +132,24 @@ public class MonthlyWinnerController : CmsAuthorizedController
                 return Json(new { errorCode = 400, message = "MSISDN or ticket is required." });
             }
 
-            var duplicate = await _db.GameMonthlyWinners
+            var prizeWinnerCount = await _db.GameMonthlyWinners
                 .AsNoTracking()
-                .AnyAsync(x => x.DrawId == draw.DrawId
+                .CountAsync(x => x.DrawId == draw.DrawId
                     && x.PrizeCode == prize.PrizeCode
+                    && x.WinnerId != request.WinnerId);
+            if (prizeWinnerCount >= prize.Quantity)
+            {
+                return Json(new { errorCode = 409, message = $"This monthly prize already has {prize.Quantity} winner(s)." });
+            }
+
+            var userAlreadyHasPrize = await _db.GameMonthlyWinners
+                .AsNoTracking()
+                .AnyAsync(x => x.DrawId == draw.DrawId
                     && x.Msisdn == msisdn
                     && x.WinnerId != request.WinnerId);
-            if (duplicate)
+            if (userAlreadyHasPrize)
             {
-                return Json(new { errorCode = 409, message = "This winner already exists for the selected prize." });
+                return Json(new { errorCode = 409, message = "This user already has a prize in the selected month." });
             }
 
             var now = DateTime.Now;

+ 17 - 7
Kitty_Fall/Kitty_Fall/Kitty_Fall.Woker/Services/GameMonthlyWinnerWorker.cs

@@ -167,11 +167,6 @@ namespace Kitty_Fall.Woker.Services
 
         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)
@@ -188,7 +183,17 @@ namespace Kitty_Fall.Woker.Services
             if (tickets.Count == 0) return;
 
             var excludedMsisdns = await GetRecentWinnerMsisdnsAsync(dbContext, draw.MonthKey, cancellationToken);
-            var selectedMsisdns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+            var existingWinners = await dbContext.GameMonthlyWinners
+                .AsNoTracking()
+                .Where(x => x.DrawId == draw.DrawId)
+                .Select(x => new { x.PrizeCode, x.Msisdn })
+                .ToListAsync(cancellationToken);
+            var existingWinnerCountByPrize = existingWinners
+                .GroupBy(x => x.PrizeCode, StringComparer.OrdinalIgnoreCase)
+                .ToDictionary(x => x.Key, x => x.Count(), StringComparer.OrdinalIgnoreCase);
+            var selectedMsisdns = new HashSet<string>(
+                existingWinners.Select(x => x.Msisdn),
+                StringComparer.OrdinalIgnoreCase);
             var random = new Random(BuildSeed(draw.MonthKey));
             var now = DateTime.Now;
             var autoAnnounce = GetBool("MonthlyWinner:AutoAnnounceWinners", false);
@@ -196,7 +201,12 @@ namespace Kitty_Fall.Woker.Services
 
             foreach (var prizeConfig in prizeConfigs)
             {
-                for (var index = 0; index < prizeConfig.Quantity; index++)
+                // Interval chi de kiem tra va bu so winner con thieu den Quantity.
+                // Mot user chi duoc trung mot giai trong cung ky quay.
+                existingWinnerCountByPrize.TryGetValue(prizeConfig.PrizeCode, out var existingCount);
+                var missingCount = Math.Max(0, prizeConfig.Quantity - existingCount);
+
+                for (var index = 0; index < missingCount; index++)
                 {
                     var ticket = PickTicket(tickets, excludedMsisdns, selectedMsisdns, random);
                     if (ticket == null) break;

+ 18 - 2
Kitty_Fall/Kitty_Fall/Kitty_Fall.Woker/Services/GameRewardPayoutWorker.cs

@@ -196,9 +196,25 @@ namespace Kitty_Fall.Woker.Services
                     && limitConfig.FallbackRewardValue.GetValueOrDefault() > 0
                     && !string.IsNullOrWhiteSpace(limitConfig.FallbackRewardUnit))
                 {
-                    // Doi sang reward fallback theo cau hinh han muc.
                     rewardLog.LimitConfigId = limitConfig.LimitConfigId;
-                    rewardLog.RewardType = limitConfig.FallbackRewardType!;
+
+                    // Khong cho phep worker doi cheo loai reward da random. Neu random
+                    // LOYALTY thi log va payout bat buoc van la LOYALTY; SCORE chi do API game cong.
+                    if (!string.Equals(
+                        rewardLog.RewardType,
+                        limitConfig.FallbackRewardType,
+                        StringComparison.OrdinalIgnoreCase))
+                    {
+                        rewardLog.Status = StatusFailed;
+                        rewardLog.RetryCount = (short)maxRetry;
+                        rewardLog.ResponseTime = now;
+                        rewardLog.ResponseCode = "INVALID_CROSS_TYPE_FALLBACK";
+                        rewardLog.ResponseMessage = $"Cross-type fallback is not allowed: {rewardLog.RewardType} -> {limitConfig.FallbackRewardType}";
+                        rewardLog.UpdatedTime = now;
+                        return new LimitCheckResult(false);
+                    }
+
+                    // Fallback cung loai chi duoc thay doi gia tri payout theo cau hinh.
                     rewardLog.RewardValue = limitConfig.FallbackRewardValue!.Value;
                     rewardLog.RewardUnit = limitConfig.FallbackRewardUnit!;
                     return new LimitCheckResult(true);

+ 1 - 0
ddl_game_kitty_fall.sql

@@ -808,6 +808,7 @@ CREATE TABLE GAME_MONTHLY_WINNER
     UPDATED_TIME    DATE,
     CONSTRAINT PK_GAME_MONTHLY_WINNER PRIMARY KEY (WINNER_ID),
     CONSTRAINT UK_GMW_DRAW_PRIZE_MSISDN UNIQUE (DRAW_ID, PRIZE_CODE, MSISDN),
+    CONSTRAINT UK_GMW_DRAW_MSISDN UNIQUE (DRAW_ID, MSISDN),
     CONSTRAINT FK_GMW_DRAW FOREIGN KEY (DRAW_ID) REFERENCES GAME_MONTHLY_DRAW (DRAW_ID),
     CONSTRAINT FK_GMW_TICKET FOREIGN KEY (TICKET_ID) REFERENCES GAME_MONTHLY_DRAW_TICKET (TICKET_ID),
     CONSTRAINT CK_GMW_MONTH_KEY CHECK (MONTH_KEY BETWEEN 200001 AND 299912),