|
|
@@ -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);
|
|
|
|