319 lines
12 KiB
C#
319 lines
12 KiB
C#
using FreeRedis;
|
||
using Microsoft.Extensions.Caching.Distributed;
|
||
using Volo.Abp.Caching;
|
||
using Volo.Abp.Domain.Services;
|
||
using Volo.Abp.Settings;
|
||
using Volo.Abp.Threading;
|
||
using Yi.Framework.DigitalCollectibles.Domain.Dtos;
|
||
using Yi.Framework.DigitalCollectibles.Domain.Entities;
|
||
using Yi.Framework.DigitalCollectibles.Domain.Shared.Consts;
|
||
using Yi.Framework.DigitalCollectibles.Domain.Shared.Enums;
|
||
using Yi.Framework.SettingManagement.Domain;
|
||
using Yi.Framework.SqlSugarCore.Abstractions;
|
||
|
||
namespace Yi.Framework.DigitalCollectibles.Domain.Managers;
|
||
|
||
/// <summary>
|
||
/// 矿池领域服务
|
||
/// 处理矿池相关业务,例如挖矿等
|
||
/// </summary>
|
||
public class MiningPoolManager : DomainService
|
||
{
|
||
private readonly ISqlSugarRepository<CollectiblesAggregateRoot> _collectiblesRepository;
|
||
private readonly ISqlSugarRepository<OnHookAggregateRoot> _onHookRepository;
|
||
private readonly ISettingProvider _settingProvider;
|
||
private readonly IDistributedCache<MiningPoolContent?> _miningPoolCache;
|
||
private readonly IDistributedCache<UserMiningLimitCacheDto?> _userMiningLimitCache;
|
||
private readonly ISqlSugarRepository<CollectiblesUserStoreAggregateRoot> _userStoreRepository;
|
||
private IRedisClient RedisClient => LazyServiceProvider.LazyGetRequiredService<IRedisClient>();
|
||
|
||
public MiningPoolManager(ISettingProvider settingProvider, IDistributedCache<MiningPoolContent> miningPoolCache,
|
||
ISqlSugarRepository<CollectiblesAggregateRoot> collectiblesRepository,
|
||
ISqlSugarRepository<OnHookAggregateRoot> onHookRepository,
|
||
ISqlSugarRepository<CollectiblesUserStoreAggregateRoot> userStoreRepository,
|
||
IDistributedCache<UserMiningLimitCacheDto> userMiningLimitCache)
|
||
{
|
||
_settingProvider = settingProvider;
|
||
this._miningPoolCache = miningPoolCache;
|
||
_collectiblesRepository = collectiblesRepository;
|
||
_onHookRepository = onHookRepository;
|
||
_userStoreRepository = userStoreRepository;
|
||
_userMiningLimitCache = userMiningLimitCache;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 每次挖矿概率,每天根据特定算法计算
|
||
/// </summary>
|
||
private decimal CurrentMiningProbability => AsyncHelper.RunSync(async () =>
|
||
{
|
||
return await ComputeMiningProbabilityAsync();
|
||
});
|
||
|
||
public async Task<MiningPoolContent> GetMiningPoolContentAsync()
|
||
{
|
||
var pool = await _miningPoolCache.GetAsync(MiningCacheConst.MiningPoolContent);
|
||
return pool;
|
||
}
|
||
|
||
public async Task GetOnHookAsync(Guid userId)
|
||
{
|
||
var onHook = await _onHookRepository._DbQueryable.Where(x => x.UserId == userId)
|
||
.Where(x => x.IsActive == true)
|
||
.Where(x => x.EndTime <= DateTime.Now)
|
||
.FirstAsync();
|
||
|
||
if (onHook is not null)
|
||
{
|
||
throw new UserFriendlyException($"当前你正在进行自动挂机,结束时间:{onHook.EndTime.Value.ToString("MM月dd日HH分mm秒")})");
|
||
}
|
||
|
||
await _onHookRepository.InsertAsync(new OnHookAggregateRoot(userId, 24));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 校验挖矿限制
|
||
/// </summary>
|
||
/// <param name="userId"></param>
|
||
private async Task VerifyMiningLimitAsync(Guid userId)
|
||
{
|
||
//每天最大次数限制
|
||
var miningMaxLimit = int.Parse(await _settingProvider.GetOrNullAsync("MiningMaxLimit"));
|
||
//每次间隔
|
||
var miningMinIntervalSeconds = int.Parse(await _settingProvider.GetOrNullAsync("MiningMinIntervalSeconds"));
|
||
|
||
var currentNumber = 1;
|
||
|
||
//根据用户进行上锁
|
||
if (await RedisClient.SetNxAsync($"UserMiningLimitLock:{userId}", true,
|
||
TimeSpan.FromSeconds(miningMinIntervalSeconds)))
|
||
{
|
||
var userLimit = await _userMiningLimitCache.GetAsync($"{MiningCacheConst.UserMiningLimit}:{userId}");
|
||
|
||
if (userLimit is not null)
|
||
{
|
||
//符合条件,成功挖矿
|
||
if (userLimit.Number < miningMaxLimit)
|
||
{
|
||
currentNumber = userLimit.Number + 1;
|
||
}
|
||
else
|
||
{
|
||
throw new UserFriendlyException($"失败,你已达当轮矿池最大限制,给其他人留点吧");
|
||
}
|
||
}
|
||
|
||
//没有缓存过,必定成功,直接走
|
||
await _userMiningLimitCache.SetAsync($"{MiningCacheConst.UserMiningLimit}:{userId}",
|
||
new UserMiningLimitCacheDto
|
||
{
|
||
Number = currentNumber,
|
||
LastMiningTime = DateTime.Now,
|
||
},
|
||
new DistributedCacheEntryOptions()
|
||
{
|
||
//虽然新增的是一天,但是每次刷新是早上10点,矿池刷新时,还需要清除限制
|
||
AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(1)
|
||
});
|
||
}
|
||
|
||
//已上过锁,并且没有到限制时间,必定失败
|
||
//不符合条件,直接走
|
||
throw new UserFriendlyException($"失败,你都挖的冒烟了,请稍后再挖");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 用户进行一次挖矿
|
||
/// </summary>
|
||
/// <returns></returns>
|
||
public async Task<MiningPoolResult> MiningAsync(Guid userId)
|
||
{
|
||
//检验限制
|
||
await VerifyMiningLimitAsync(userId);
|
||
|
||
var result = new MiningPoolResult();
|
||
//从矿池中开挖,判断矿池是否还有矿
|
||
//根据挖矿概率,进行挖掘
|
||
//挖到了放到用户仓库即可
|
||
|
||
//如果概率是挖到了矿,再从矿物中随机选择一个稀有度,再在当前稀有度中的矿物列表,随机选择一个具体的矿物
|
||
var pool = await GetMiningPoolContentAsync();
|
||
if (pool.TotalNumber == 0)
|
||
{
|
||
throw new UserFriendlyException($"失败,矿池已经被掏空了,请等矿池刷新后再来");
|
||
}
|
||
|
||
// 生成一个 0 到 1 之间的随机数
|
||
double randomValue = new Random().NextDouble();
|
||
//如果随机的概率在当前概率内,成功
|
||
if (randomValue.To<decimal>() < CurrentMiningProbability)
|
||
{
|
||
//成功后在藏品中根据稀有度概率必定获取一个
|
||
var probabilityArray = RarityEnumExtensions.GetProbabilityArray();
|
||
var index = GetRandomIndex(probabilityArray);
|
||
var rarityType = (RarityEnum)Enum.GetValues(typeof(RarityEnum)).GetValue(index)!;
|
||
var collectiblesList =
|
||
await _collectiblesRepository._DbQueryable.Where(x => x.Rarity == rarityType).ToListAsync();
|
||
int randomIndex = new Random().Next(collectiblesList.Count);
|
||
var currentCollectibles = collectiblesList[randomIndex];
|
||
|
||
result.Collectibles = currentCollectibles;
|
||
|
||
//使用结果新增给对应的用户
|
||
await _userStoreRepository.InsertAsync(new CollectiblesUserStoreAggregateRoot
|
||
{
|
||
UserId = userId,
|
||
CollectiblesId = result.Collectibles.Id,
|
||
IsRead = false
|
||
});
|
||
|
||
|
||
return result;
|
||
}
|
||
|
||
throw new UserFriendlyException($"恭喜!空空如也,挖了个寂寞");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 挂机挖矿
|
||
/// </summary>
|
||
public async Task OnHookMiningAsync()
|
||
{
|
||
//获取当前激活的挂机挖矿
|
||
var currentOnHook = await _onHookRepository._DbQueryable.Where(x => x.IsActive == true)
|
||
.Where(x => x.EndTime <= DateTime.Now).ToListAsync();
|
||
|
||
//根据用户对挂机卡hash关系
|
||
var userOnHookDic = currentOnHook.GroupBy(x => x.UserId).ToDictionary(x => x.Key, y => y.First());
|
||
foreach (var onHookItem in userOnHookDic)
|
||
{
|
||
await MiningAsync(onHookItem.Value.UserId);
|
||
}
|
||
}
|
||
|
||
private int GetRandomIndex(decimal[] probabilities)
|
||
{
|
||
// 生成0到1之间的随机数
|
||
Random random = new Random();
|
||
decimal randomValue = random.NextDouble().To<decimal>();
|
||
|
||
decimal cumulativeProbability = 0.0m;
|
||
|
||
for (int i = 0; i < probabilities.Length; i++)
|
||
{
|
||
cumulativeProbability += probabilities[i];
|
||
|
||
// 判断随机数是否小于当前的累积概率
|
||
if (randomValue < cumulativeProbability)
|
||
{
|
||
return i; // 返回中标的索引
|
||
}
|
||
}
|
||
|
||
//剩余情况都是普通
|
||
return 0;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算当前挖矿概率
|
||
/// </summary>
|
||
/// <returns></returns>
|
||
public async Task<decimal> ComputeMiningProbabilityAsync()
|
||
{
|
||
//当前的挖矿期望:当天的所有藏品能被刚好挖完
|
||
|
||
//保底概率:最大不能高过一个值,百分之10
|
||
//手动挖矿:1天可挖10次,每次至少间隔6秒
|
||
//自动挖矿:1天可以挖24次 每次间隔60分钟(需要使用自动挖矿卡)
|
||
//可影响因素:剩余手动挖矿次数+剩余自动挖矿次数
|
||
|
||
//简单模式,1/15
|
||
var miningMaxLimit = decimal.Parse(await _settingProvider.GetOrNullAsync("MiningMinProbability"));
|
||
return miningMaxLimit;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 刷新矿池
|
||
/// </summary>
|
||
/// <returns></returns>
|
||
public async Task RefreshMiningPoolAsync()
|
||
{
|
||
//获取当前最大的限制
|
||
var maximumPoolLimit = int.Parse(await _settingProvider.GetOrNullAsync("MaxPoolLimit"));
|
||
|
||
DateTime startTime = DateTime.Today.AddHours(10);
|
||
DateTime endTime = startTime.AddDays(1);
|
||
var probabilityValues = RarityEnumExtensions.GetProbabilityArray();
|
||
var result = GenerateDistribution(maximumPoolLimit, probabilityValues);
|
||
|
||
//根据配置,将不同比例的矿,塞入矿池,
|
||
//矿池,交给redis
|
||
await _miningPoolCache.SetAsync(MiningCacheConst.MiningPoolContent, new MiningPoolContent(startTime, endTime)
|
||
{
|
||
I0_OrdinaryNumber = result[0],
|
||
I1_SeniorNumber = result[1],
|
||
I2_RareNumber = result[2],
|
||
I3_GemNumber = result[3],
|
||
I4_LegendNumber = result[4]
|
||
}, new DistributedCacheEntryOptions
|
||
{
|
||
AbsoluteExpiration = endTime
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 刷新用户挖矿限制
|
||
/// </summary>
|
||
public async Task RefreshMiningUserLimitAsync()
|
||
{
|
||
await RedisClient.DelAsync($"{MiningCacheConst.UserMiningLimit}*");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 根据概率生成给对应稀有度藏品
|
||
/// </summary>
|
||
/// <param name="totalCount"></param>
|
||
/// <param name="probabilities"></param>
|
||
/// <returns></returns>
|
||
/// <exception cref="ArgumentException"></exception>
|
||
private int[] GenerateDistribution(int totalCount, decimal[] probabilities)
|
||
{
|
||
int[] counts = new int[probabilities.Length];
|
||
decimal totalProbability = 0;
|
||
|
||
// 计算概率总和,确保为 1
|
||
foreach (var prob in probabilities)
|
||
{
|
||
totalProbability += prob;
|
||
}
|
||
|
||
// 检查概率之和是否为 1
|
||
if (totalProbability < 0.99m || totalProbability > 1.01m)
|
||
{
|
||
throw new ArgumentException("概率总和必须接近1");
|
||
}
|
||
|
||
// 生成分布
|
||
for (int i = 0; i < probabilities.Length; i++)
|
||
{
|
||
counts[i] = (int)(totalCount * probabilities[i]);
|
||
}
|
||
|
||
// 处理可能因精度问题导致的总数不足
|
||
int sum = 0;
|
||
foreach (var count in counts)
|
||
{
|
||
sum += count;
|
||
}
|
||
|
||
int difference = totalCount - sum;
|
||
|
||
// 将差值分配给概率最大的一项
|
||
if (difference > 0)
|
||
{
|
||
int maxIndex = Array.IndexOf(counts, Math.Max(counts[0], counts[1]));
|
||
counts[maxIndex] += difference;
|
||
}
|
||
|
||
return counts;
|
||
}
|
||
} |