|
|
@@ -14,8 +14,13 @@ namespace Kitty_Fall.Woker.Services
|
|
|
private const byte StatusProcessing = 1;
|
|
|
private const byte StatusSuccess = 2;
|
|
|
private const byte StatusFailed = 3;
|
|
|
+ private const decimal DataToMmkRate = 0.665m;
|
|
|
+ private const decimal LoyaltyToMmkRate = 1.33m;
|
|
|
|
|
|
private static readonly log4net.ILog log = log4net.LogManager.GetLogger(typeof(GameRewardPayoutWorker));
|
|
|
+ private static readonly SemaphoreSlim RewardBudgetCacheLock = new(1, 1);
|
|
|
+ private static int cachedRewardBudgetDateKey;
|
|
|
+ private static decimal cachedRewardBudget;
|
|
|
|
|
|
private readonly IServiceScopeFactory scopeFactory;
|
|
|
private readonly IConfiguration configuration;
|
|
|
@@ -95,13 +100,6 @@ namespace Kitty_Fall.Woker.Services
|
|
|
|
|
|
try
|
|
|
{
|
|
|
- var limitResult = await ApplyLimitBeforePayoutAsync(dbContext, rewardLog, now, cancellationToken);
|
|
|
- if (!limitResult.CanPayout)
|
|
|
- {
|
|
|
- await dbContext.SaveChangesAsync(cancellationToken);
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
if (!IsExternalReward(rewardLog.RewardType))
|
|
|
{
|
|
|
// Fallback khong can goi WS thi danh dau xong de khong retry vo nghia.
|
|
|
@@ -114,6 +112,25 @@ 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);
|
|
|
+ var currentRewardCost = ConvertRewardToMmk(rewardLog.RewardType, rewardLog.RewardValue);
|
|
|
+ if (reservedRewardCost > rewardBudget)
|
|
|
+ {
|
|
|
+ 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}",
|
|
|
+ 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}");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
var payoutPackageName = GetPackageName(rewardLog);
|
|
|
var addPointRes = DotnetLib.Mytel.AddingPointLogic.AddingPointHandler(
|
|
|
log,
|
|
|
@@ -157,7 +174,6 @@ namespace Kitty_Fall.Woker.Services
|
|
|
AccountId = null,
|
|
|
Status = CommonConstants.UNCHARGE_STATUS
|
|
|
});
|
|
|
- await UpdateLimitUsageAsync(dbContext, rewardLog, now, cancellationToken);
|
|
|
await dbContext.SaveChangesAsync(cancellationToken);
|
|
|
await SendSuccessMtAsync(dbContext, rewardLog, cancellationToken);
|
|
|
return;
|
|
|
@@ -388,6 +404,103 @@ namespace Kitty_Fall.Woker.Services
|
|
|
|| string.Equals(rewardType, "LOYALTY", StringComparison.OrdinalIgnoreCase);
|
|
|
}
|
|
|
|
|
|
+ private async Task<decimal> GetDailyExternalRewardBudgetAsync(
|
|
|
+ ModelContext dbContext,
|
|
|
+ DateTime now,
|
|
|
+ CancellationToken cancellationToken)
|
|
|
+ {
|
|
|
+ var budgetDate = now.Date;
|
|
|
+ var budgetDateKey = budgetDate.Year * 10000 + budgetDate.Month * 100 + budgetDate.Day;
|
|
|
+ if (Volatile.Read(ref cachedRewardBudgetDateKey) == budgetDateKey)
|
|
|
+ {
|
|
|
+ return cachedRewardBudget;
|
|
|
+ }
|
|
|
+
|
|
|
+ await RewardBudgetCacheLock.WaitAsync(cancellationToken);
|
|
|
+ try
|
|
|
+ {
|
|
|
+ if (cachedRewardBudgetDateKey == budgetDateKey)
|
|
|
+ {
|
|
|
+ return cachedRewardBudget;
|
|
|
+ }
|
|
|
+
|
|
|
+ var configuredRevenueText = configuration["REVENUE"];
|
|
|
+ if (string.IsNullOrWhiteSpace(configuredRevenueText))
|
|
|
+ {
|
|
|
+ configuredRevenueText = configuration["REVENUE_MIN"];
|
|
|
+ }
|
|
|
+
|
|
|
+ var configuredRevenue = decimal.TryParse(
|
|
|
+ configuredRevenueText,
|
|
|
+ NumberStyles.Number,
|
|
|
+ CultureInfo.InvariantCulture,
|
|
|
+ out var parsedRevenue)
|
|
|
+ ? Math.Max(0, parsedRevenue)
|
|
|
+ : 0;
|
|
|
+
|
|
|
+ var previousDay = budgetDate.AddDays(-1);
|
|
|
+ var previousDayRevenue = await dbContext.ReportByTimes.AsNoTracking()
|
|
|
+ .Where(x => x.Type == CommonConstants.DAY_REPORT
|
|
|
+ && x.TimeReport >= previousDay
|
|
|
+ && x.TimeReport < budgetDate)
|
|
|
+ .SumAsync(x => x.Revenue ?? 0, cancellationToken);
|
|
|
+
|
|
|
+ var previousDayRevenueLimit = Math.Max(0, previousDayRevenue) * 0.10m;
|
|
|
+ cachedRewardBudget = Math.Max(previousDayRevenueLimit, configuredRevenue);
|
|
|
+ Volatile.Write(ref cachedRewardBudgetDateKey, budgetDateKey);
|
|
|
+
|
|
|
+ log.Info($"Worker daily reward budget cache loaded. date={budgetDate:yyyy-MM-dd}, previousDayRevenue={previousDayRevenue}, configuredRevenue={configuredRevenue}, budgetMmk={cachedRewardBudget}");
|
|
|
+ return cachedRewardBudget;
|
|
|
+ }
|
|
|
+ finally
|
|
|
+ {
|
|
|
+ RewardBudgetCacheLock.Release();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static async Task<decimal> GetDailyReservedExternalRewardCostAsync(
|
|
|
+ ModelContext dbContext,
|
|
|
+ DateTime now,
|
|
|
+ CancellationToken cancellationToken)
|
|
|
+ {
|
|
|
+ var start = now.Date;
|
|
|
+ var end = start.AddDays(1);
|
|
|
+ var totals = await dbContext.GameRewardLogs.AsNoTracking()
|
|
|
+ .Where(x => (x.Status == StatusSuccess || x.Status == StatusProcessing)
|
|
|
+ && 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)))
|
|
|
+ .GroupBy(x => x.RewardType)
|
|
|
+ .Select(x => new
|
|
|
+ {
|
|
|
+ RewardType = x.Key,
|
|
|
+ TotalValue = x.Sum(y => y.RewardValue)
|
|
|
+ })
|
|
|
+ .ToListAsync(cancellationToken);
|
|
|
+
|
|
|
+ return totals.Sum(x => ConvertRewardToMmk(x.RewardType, x.TotalValue));
|
|
|
+ }
|
|
|
+
|
|
|
+ private static decimal ConvertRewardToMmk(string? rewardType, decimal rewardValue)
|
|
|
+ {
|
|
|
+ if (string.Equals(rewardType, "DATA", StringComparison.OrdinalIgnoreCase))
|
|
|
+ {
|
|
|
+ return rewardValue * DataToMmkRate;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (string.Equals(rewardType, "LOYALTY", StringComparison.OrdinalIgnoreCase))
|
|
|
+ {
|
|
|
+ return rewardValue * LoyaltyToMmkRate;
|
|
|
+ }
|
|
|
+
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
private string GetPackageName(GameRewardLog rewardLog)
|
|
|
{
|
|
|
if (string.Equals(rewardLog.RewardType, "DATA", StringComparison.OrdinalIgnoreCase))
|