Bladeren bron

chặn trần chặt chẽ hơn

student 3 weken geleden
bovenliggende
commit
04acf2c606

+ 16 - 3
Kitty_Fall/Kitty_Fall/Kitty_Fall.Apis/Business/Game/GameBusinessImpl.cs

@@ -25,6 +25,10 @@ namespace Kitty_Fall.Apis.Business.Game
         private static readonly SemaphoreSlim RewardBudgetCacheLock = new(1, 1);
         private static int cachedRewardBudgetDateKey;
         private static decimal cachedRewardBudget;
+        // Han cua cache ngan sach (UTC ticks). Bao cao doanh thu hom truoc chua co luc dau ngay
+        // thi chi giu ngan sach tam 10 phut roi tinh lai, khong giu muc toi thieu ca ngay.
+        private static long cachedRewardBudgetExpiresTicks;
+        private static readonly TimeSpan RewardBudgetRetryInterval = TimeSpan.FromMinutes(10);
 
         private readonly ModelContext dbContext;
         private readonly IConfiguration configuration;
@@ -1193,7 +1197,7 @@ namespace Kitty_Fall.Apis.Business.Game
         {
             var budgetDate = now.Date;
             var budgetDateKey = budgetDate.Year * 10000 + budgetDate.Month * 100 + budgetDate.Day;
-            if (Volatile.Read(ref cachedRewardBudgetDateKey) == budgetDateKey)
+            if (IsRewardBudgetCacheValid(budgetDateKey))
             {
                 return cachedRewardBudget;
             }
@@ -1202,7 +1206,7 @@ namespace Kitty_Fall.Apis.Business.Game
             try
             {
                 // Double-check sau khi lay lock: request khac co the da nap cache trong luc cho.
-                if (cachedRewardBudgetDateKey == budgetDateKey)
+                if (IsRewardBudgetCacheValid(budgetDateKey))
                 {
                     return cachedRewardBudget;
                 }
@@ -1230,9 +1234,12 @@ namespace Kitty_Fall.Apis.Business.Game
 
                 var previousDayRevenueLimit = Math.Max(0, previousDayRevenue) * 0.10m;
                 cachedRewardBudget = Math.Max(previousDayRevenueLimit, configuredRevenue);
+                Volatile.Write(ref cachedRewardBudgetExpiresTicks, previousDayRevenue > 0
+                    ? long.MaxValue
+                    : DateTime.UtcNow.Add(RewardBudgetRetryInterval).Ticks);
                 Volatile.Write(ref cachedRewardBudgetDateKey, budgetDateKey);
 
-                log.Info($"Daily reward budget cache loaded. date={budgetDate:yyyy-MM-dd}, previousDayRevenue={previousDayRevenue}, configuredRevenue={configuredRevenue}, budgetMmk={cachedRewardBudget}");
+                log.Info($"Daily reward budget cache loaded. date={budgetDate:yyyy-MM-dd}, previousDayRevenue={previousDayRevenue}, configuredRevenue={configuredRevenue}, budgetMmk={cachedRewardBudget}, reloadInMinutes={(previousDayRevenue > 0 ? "none" : RewardBudgetRetryInterval.TotalMinutes.ToString(CultureInfo.InvariantCulture))}");
                 return cachedRewardBudget;
             }
             finally
@@ -1241,6 +1248,12 @@ namespace Kitty_Fall.Apis.Business.Game
             }
         }
 
+        private static bool IsRewardBudgetCacheValid(int budgetDateKey)
+        {
+            return Volatile.Read(ref cachedRewardBudgetDateKey) == budgetDateKey
+                && DateTime.UtcNow.Ticks < Volatile.Read(ref cachedRewardBudgetExpiresTicks);
+        }
+
         private async Task<decimal> GetDailyExternalRewardCostAsync(DateTime now)
         {
             var start = now.Date;

+ 41 - 24
Kitty_Fall/Kitty_Fall/Kitty_Fall.Woker/Services/GameRewardPayoutWorker.cs

@@ -21,6 +21,9 @@ namespace Kitty_Fall.Woker.Services
         private static readonly SemaphoreSlim RewardBudgetCacheLock = new(1, 1);
         private static int cachedRewardBudgetDateKey;
         private static decimal cachedRewardBudget;
+        // Giong API: bao cao doanh thu hom truoc chua co thi chi giu ngan sach tam 10 phut.
+        private static long cachedRewardBudgetExpiresTicks;
+        private static readonly TimeSpan RewardBudgetRetryInterval = TimeSpan.FromMinutes(10);
 
         private readonly IServiceScopeFactory scopeFactory;
         private readonly IConfiguration configuration;
@@ -112,22 +115,27 @@ namespace Kitty_Fall.Woker.Services
                     return;
                 }
 
-                var rewardBudget = await GetDailyExternalRewardBudgetAsync(dbContext, now, cancellationToken);
-                // PROCESSING da duoc tinh la giu cho ngan sach. Claim la UPDATE co dieu kien
-                // va commit truoc khi vao day, nen cac worker/instance sau se thay khoan nay.
-                var reservedRewardCost = await GetDailyReservedExternalRewardCostAsync(dbContext, now, cancellationToken);
+                // API la noi quyet dinh ngan sach: chi hua thuong DATA/LOYALTY khi con ngan sach.
+                // Worker tinh lai dung cong thuc cua API, theo NGAY HUA THUONG (CreatedTime), va
+                // chi chan khi vuot ngan sach + bien an toan (vd 2 request dong thoi cung qua kiem tra).
+                // Bi chan o day nghia la cau hinh/logic API lech, can kiem tra.
+                var rewardDate = rewardLog.CreatedTime.Date;
+                var rewardBudget = await GetDailyExternalRewardBudgetAsync(dbContext, rewardDate, cancellationToken);
+                var promisedRewardCost = await GetDailyPromisedExternalRewardCostAsync(dbContext, rewardDate, cancellationToken);
                 var currentRewardCost = ConvertRewardToMmk(rewardLog.RewardType, rewardLog.RewardValue);
-                if (reservedRewardCost > rewardBudget)
+                var overrunPercent = Math.Max(0, GetInt("RewardWorker:BudgetOverrunPercent", 10));
+                var hardLimit = rewardBudget * (100 + overrunPercent) / 100m;
+                if (promisedRewardCost > hardLimit)
                 {
                     rewardLog.Status = StatusFailed;
                     rewardLog.ResponseTime = now;
                     rewardLog.ResponseCode = "DAILY_REWARD_BUDGET_EXCEEDED";
                     rewardLog.ResponseMessage = Truncate(
-                        $"Daily reward budget exceeded. budgetMmk={rewardBudget}, reservedMmk={reservedRewardCost}, rewardMmk={currentRewardCost}",
+                        $"Daily reward budget exceeded. date={rewardDate:yyyy-MM-dd}, budgetMmk={rewardBudget}, hardLimitMmk={hardLimit}, promisedMmk={promisedRewardCost}, rewardMmk={currentRewardCost}",
                         500);
                     rewardLog.UpdatedTime = now;
                     await dbContext.SaveChangesAsync(cancellationToken);
-                    log.Warn($"Reward payout blocked by daily budget. rewardLogId={rewardLog.RewardLogId}, budgetMmk={rewardBudget}, reservedMmk={reservedRewardCost}, rewardMmk={currentRewardCost}");
+                    log.Error($"Reward payout blocked by daily budget although API promised it. rewardLogId={rewardLog.RewardLogId}, date={rewardDate:yyyy-MM-dd}, budgetMmk={rewardBudget}, hardLimitMmk={hardLimit}, promisedMmk={promisedRewardCost}, rewardMmk={currentRewardCost}");
                     return;
                 }
 
@@ -419,12 +427,12 @@ namespace Kitty_Fall.Woker.Services
 
         private async Task<decimal> GetDailyExternalRewardBudgetAsync(
             ModelContext dbContext,
-            DateTime now,
+            DateTime budgetDay,
             CancellationToken cancellationToken)
         {
-            var budgetDate = now.Date;
+            var budgetDate = budgetDay.Date;
             var budgetDateKey = budgetDate.Year * 10000 + budgetDate.Month * 100 + budgetDate.Day;
-            if (Volatile.Read(ref cachedRewardBudgetDateKey) == budgetDateKey)
+            if (IsRewardBudgetCacheValid(budgetDateKey))
             {
                 return cachedRewardBudget;
             }
@@ -432,7 +440,7 @@ namespace Kitty_Fall.Woker.Services
             await RewardBudgetCacheLock.WaitAsync(cancellationToken);
             try
             {
-                if (cachedRewardBudgetDateKey == budgetDateKey)
+                if (IsRewardBudgetCacheValid(budgetDateKey))
                 {
                     return cachedRewardBudget;
                 }
@@ -460,9 +468,12 @@ namespace Kitty_Fall.Woker.Services
 
                 var previousDayRevenueLimit = Math.Max(0, previousDayRevenue) * 0.10m;
                 cachedRewardBudget = Math.Max(previousDayRevenueLimit, configuredRevenue);
+                Volatile.Write(ref cachedRewardBudgetExpiresTicks, previousDayRevenue > 0
+                    ? long.MaxValue
+                    : DateTime.UtcNow.Add(RewardBudgetRetryInterval).Ticks);
                 Volatile.Write(ref cachedRewardBudgetDateKey, budgetDateKey);
 
-                log.Info($"Worker daily reward budget cache loaded. date={budgetDate:yyyy-MM-dd}, previousDayRevenue={previousDayRevenue}, configuredRevenue={configuredRevenue}, budgetMmk={cachedRewardBudget}");
+                log.Info($"Worker daily reward budget cache loaded. date={budgetDate:yyyy-MM-dd}, previousDayRevenue={previousDayRevenue}, configuredRevenue={configuredRevenue}, budgetMmk={cachedRewardBudget}, reloadInMinutes={(previousDayRevenue > 0 ? "none" : RewardBudgetRetryInterval.TotalMinutes.ToString(CultureInfo.InvariantCulture))}");
                 return cachedRewardBudget;
             }
             finally
@@ -471,28 +482,34 @@ namespace Kitty_Fall.Woker.Services
             }
         }
 
-        private static async Task<decimal> GetDailyReservedExternalRewardCostAsync(
+        private static bool IsRewardBudgetCacheValid(int budgetDateKey)
+        {
+            return Volatile.Read(ref cachedRewardBudgetDateKey) == budgetDateKey
+                && DateTime.UtcNow.Ticks < Volatile.Read(ref cachedRewardBudgetExpiresTicks);
+        }
+
+        // Tong thuong DATA/LOYALTY da hua trong ngay, DUNG cong thuc API dung khi chon thuong
+        // (GameBusinessImpl.GetDailyExternalRewardCostAsync) de hai ben khong lech nhau.
+        private static async Task<decimal> GetDailyPromisedExternalRewardCostAsync(
             ModelContext dbContext,
-            DateTime now,
+            DateTime day,
             CancellationToken cancellationToken)
         {
-            var start = now.Date;
+            var start = day.Date;
             var end = start.AddDays(1);
-            var totals = await dbContext.GameRewardLogs.AsNoTracking()
-                .Where(x => (x.Status == StatusSuccess || x.Status == StatusProcessing)
+            var totals = await dbContext.GameLevelAttempts.AsNoTracking()
+                .Where(x => x.Status == 2
+                    && x.IsValid != false
+                    && x.RewardValue != null
                     && x.RewardValue > 0
                     && (x.RewardType == "DATA" || x.RewardType == "LOYALTY")
-                    && ((x.Status == StatusSuccess
-                            && (x.ResponseTime ?? x.CreatedTime) >= start
-                            && (x.ResponseTime ?? x.CreatedTime) < end)
-                        || (x.Status == StatusProcessing
-                            && (x.RequestTime ?? x.CreatedTime) >= start
-                            && (x.RequestTime ?? x.CreatedTime) < end)))
+                    && (x.SubmitTime ?? x.CreatedTime) >= start
+                    && (x.SubmitTime ?? x.CreatedTime) < end)
                 .GroupBy(x => x.RewardType)
                 .Select(x => new
                 {
                     RewardType = x.Key,
-                    TotalValue = x.Sum(y => y.RewardValue)
+                    TotalValue = x.Sum(y => y.RewardValue ?? 0)
                 })
                 .ToListAsync(cancellationToken);
 

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

@@ -18,6 +18,8 @@
     // Worker chi goi moi reward NEW mot lan; loi/timeout/PROCESSING de admin doi soat, khong auto retry.
     "IntervalSeconds": 10,
     "BatchSize": 20,
+    // API quyet dinh ngan sach; worker chi chan khi tong da hua vuot ngan sach qua % nay.
+    "BudgetOverrunPercent": 10,
     "MockPayoutSuccess": false,
     "ServiceId": "Kitty_Fall_DAYLY",
     "DataPackageName": "DATA_",