Sfoglia il codice sorgente

tiền và loya 1 số 1 ngày 1 lần với 1 level

student 1 settimana fa
parent
commit
efc51ac4b7

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

@@ -9,6 +9,7 @@ using System.Globalization;
 using System.Text.RegularExpressions;
 using Database.Database;
 using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Storage;
 
 namespace Database;
 
@@ -29,6 +30,7 @@ public class DbLogic
         }
 
         await using var command = conn.CreateCommand();
+        command.Transaction = dbContext.Database.CurrentTransaction?.GetDbTransaction();
         command.CommandText = $"SELECT {sequence}.NEXTVAL FROM DUAL";
         await using var reader = await command.ExecuteReaderAsync();
         if (await reader.ReadAsync())

+ 35 - 0
Kitty_Fall/Kitty_Fall/Kitty_Fall.Apis/Business/Game/DailyLevelRewardPolicy.cs

@@ -0,0 +1,35 @@
+using Database.Database;
+
+namespace Kitty_Fall.Apis.Business.Game;
+
+public static class DailyLevelRewardPolicy
+{
+    public static IQueryable<GameLevelAttempt> ExternalWinsForDay(
+        IQueryable<GameLevelAttempt> attempts, string msisdn, byte levelNo, DateTime now)
+    {
+        var start = now.Date;
+        var end = start.AddDays(1);
+
+        // One DATA or LOYALTY win per subscriber/level/day, across sessions and modes.
+        // Count the awarded attempt even while its payout is pending or being retried.
+        return attempts.Where(x => x.Msisdn == msisdn
+            && x.LevelNo == levelNo
+            && x.Status == 2
+            && x.IsValid != false
+            && (x.RewardType == "DATA" || x.RewardType == "LOYALTY")
+            && (x.SubmitTime ?? x.CreatedTime) >= start
+            && (x.SubmitTime ?? x.CreatedTime) < end);
+    }
+
+    public static List<GameLevelRewardOption> EligibleOptions(
+        List<GameLevelRewardOption> options, bool alreadyWonExternalReward)
+    {
+        if (!alreadyWonExternalReward) return options;
+
+        var scoreOptions = options.Where(x => x.RewardType == "SCORE").ToList();
+        if (scoreOptions.Count == 0)
+            throw new InvalidOperationException("The level needs an active SCORE reward for repeat wins.");
+
+        return scoreOptions;
+    }
+}

+ 32 - 1
Kitty_Fall/Kitty_Fall/Kitty_Fall.Apis/Business/Game/GameBusinessImpl.cs

@@ -7,6 +7,7 @@ using Kitty_Fall.Apis.Common;
 using Kitty_Fall.Apis.Language;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Storage;
 using Newtonsoft.Json;
 using System.Globalization;
 
@@ -773,8 +774,14 @@ namespace Kitty_Fall.Apis.Business.Game
                     .OrderByDescending(x => x.Id)
                     .FirstOrDefaultAsync();
 
+                // Keep the eligibility check and reward writes atomic across API instances.
+                await using var rewardTransaction = await dbContext.Database.BeginTransactionAsync();
+                if (!await LockRewardSubscriberAsync(msisdn))
+                    return CommonLogic.BuildResponse(url, json, CommonErrorCode.Error, Lang.errorOccurred, new { });
+                now = MyanmarClock.Now;
+
                 var session = await dbContext.GamePlaySessions
-                    .FirstOrDefaultAsync(x => x.RootTransId == request.transId);
+                    .FirstOrDefaultAsync(x => x.RootTransId == request.transId && x.Msisdn == msisdn);
 
                 // Chi cho ghi ket qua khi phien da start va da tru luot.
                 if (session == null || session.TurnDeducted != true)
@@ -790,6 +797,7 @@ namespace Kitty_Fall.Apis.Business.Game
                     session.LastActionTime = now;
                     session.UpdatedTime = now;
                     await dbContext.SaveChangesAsync();
+                    await rewardTransaction.CommitAsync();
                     var totalTurn = await GetAvailableTurnAsync(msisdn, now);
 
                     return CommonLogic.BuildResponse(
@@ -838,6 +846,10 @@ namespace Kitty_Fall.Apis.Business.Game
                         .ThenBy(x => x.RewardOptionId)
                         .ToListAsync();
 
+                    var alreadyWonExternalReward = await DailyLevelRewardPolicy.ExternalWinsForDay(
+                        dbContext.GameLevelAttempts.AsNoTracking(), msisdn, levelNo.Value, now).AnyAsync();
+                    rewardOptions = DailyLevelRewardPolicy.EligibleOptions(rewardOptions, alreadyWonExternalReward);
+
                     var rewardBudget = await GetDailyExternalRewardBudgetAsync(now);
                     var usedRewardBudget = await GetDailyExternalRewardCostAsync(now);
                     rewardOptions = rewardOptions
@@ -991,6 +1003,7 @@ namespace Kitty_Fall.Apis.Business.Game
 
                 await UpsertMonthlyRankingAsync(msisdn, account?.Id, mode.ModeId, awardedScore + rewardScoreDelta, session.MaxLevelReached, now);
                 await dbContext.SaveChangesAsync();
+                await rewardTransaction.CommitAsync();
 
                 // Response cho popup LEVEL COMPLETE chi tra thuong cua level vua
                 // hoan thanh. Tong diem van duoc luu trong session/ranking va chi
@@ -1021,6 +1034,24 @@ namespace Kitty_Fall.Apis.Business.Game
             return CommonLogic.BuildResponse(url, json, CommonErrorCode.Error, Lang.errorOccurred, new { });
         }
 
+        private async Task<bool> LockRewardSubscriberAsync(string msisdn)
+        {
+            // Oracle row locks serialize result submissions for this subscriber, including
+            // submissions from different sessions or different running API instances.
+            await using var command = dbContext.Database.GetDbConnection().CreateCommand();
+            command.Transaction = dbContext.Database.CurrentTransaction!.GetDbTransaction();
+            command.CommandText = "SELECT ID FROM ACCOUNT_USER WHERE MSISDN = :msisdn ORDER BY ID FOR UPDATE";
+            var parameter = command.CreateParameter();
+            parameter.ParameterName = "msisdn";
+            parameter.Value = msisdn;
+            command.Parameters.Add(parameter);
+
+            await using var reader = await command.ExecuteReaderAsync();
+            var found = false;
+            while (await reader.ReadAsync()) found = true;
+            return found;
+        }
+
         private async Task<decimal> NextDecimalIdAsync(string type)
         {
             var sequence = type switch