一、功能概述
场景:后台系统需要一个点击生成中奖名单的功能,点击即可随机抽取符合中奖条件的用户,并随机发放奖品表中的奖品。中奖概率与用户参与游戏次数权重正相关。
本文记录了一个中奖名单随机生成系统的实现,该系统能够根据用户答题情况,按照答题次数作为权重进行随机抽奖,并确保历史中奖用户不会重复中奖。系统主要包含候选用户筛选、权重随机算法实现、中奖记录生成与存储等核心功能。
二、核心功能模块
2.1 数据查询与筛选模块
历史批次中奖的用户不可重复中奖,也就是每人只能中一次。
手机号+身份证号后四位相同被认为同一个自然人。
该模块负责从数据库中查询历史中奖用户和本次参与抽奖的候选用户,并进行数据筛选处理:
// 查询历史中奖用户信息
var historyWinners = dataService.Query<WinningRecordEntity>(p => p.IsActive)
.Join<UserInfoEntity>((w, u) => w.UserId == u.UserId)
.Select((w, u) => new UserInfoOutput
{
UserId = w.UserId,
IdNumber = u.IdNumber.Trim(),
PhoneNumber = u.PhoneNumber.Trim(),
}).Distinct().ToList();
// 查询本次参与抽奖的候选用户(按答题次数排序)
var candidateUsers = dataService.Query<AnswerRecordEntity>(p => p.IsActive)
.Join<GameRecordEntity>((a, g) => a.UserId == g.UserId && g.IsAllCorrect)
.Join<UserInfoEntity>((a, g, u) => a.UserId == u.UserId)
.Select((a, g, u) => new { a.UserId, u.PhoneNumber, u.IdNumber })
.MergeTable()
.GroupBy(p => new { p.UserId, p.PhoneNumber, p.IdNumber })
.Select(p => new UserAnswerCountOutput {
UserId = p.UserId,
PhoneNumber = p.PhoneNumber,
IdNumber = p.IdNumber,
AnswerCount = SqlFunc.Count(p.UserId)
})
.OrderByDescending(p => p.AnswerCount)
.ToList();
// 排除历史中奖用户(通过身份证和手机号匹配)
var filteredCandidates = candidateUsers
.Where(u =>
!historyWinners.Any(h =>
h.IdNumber.Equals(u.IdNumber.Trim(), StringComparison.OrdinalIgnoreCase) &&
h.PhoneNumber.Equals(u.PhoneNumber.Trim(), StringComparison.OrdinalIgnoreCase)
)
).ToList();
// 对本次候选用户按身份信息去重,保留答题次数最多的记录
var uniqueCandidates = filteredCandidates
.GroupBy(u => new { IdNumber = u.IdNumber.Trim(), PhoneNumber = u.PhoneNumber.Trim() })
.Select(g => g.OrderByDescending(u => u.AnswerCount).First())
.ToList();
这部分代码主要完成了:
- 从数据库中查询历史中奖用户信息
- 查询符合抽奖条件的候选用户(已完成答题且全部正确)
- 对候选用户进行筛选,排除已中奖用户
- 对候选用户按身份信息去重,确保一个用户只保留一条最高答题次数的记录
2.2 奖品库存计算模块
该模块负责计算各奖品的剩余库存数量:
// 计算各奖品剩余可发放数量
List<PrizeEntity> availablePrizes = new List<PrizeEntity>();
var allPrizes = dataService.Query<PrizeEntity>(p => p.IsActive && p.IsDisplay).ToList();
int totalPrizeCount = 0;
// 查询已发放的各奖品数量
var distributedPrizes = dataService.Query<WinningRecordEntity>(p => p.IsActive)
.GroupBy(p => p.PrizeId)
.Select(p => new { p.PrizeId, DistributedCount = SqlFunc.Count(p.Id) });
foreach (var prize in allPrizes)
{
// 查找该奖品已发放数量
var distributed = distributedPrizes.FirstOrDefault(p => p.PrizeId == prize.Id);
if (distributed != null)
{
// 计算剩余可发放数量
int remaining = prize.TotalCount - distributed.DistributedCount;
for (int i = 0; i < remaining; i++)
{
availablePrizes.Add(prize);
}
totalPrizeCount += remaining;
}
else
{
// 未发放过的奖品,按总数量添加
for (int i = 0; i < prize.TotalCount; i++)
{
availablePrizes.Add(prize);
}
totalPrizeCount += prize.TotalCount;
}
}
// 随机打乱奖品顺序
availablePrizes = RandomHelper.Shuffle(availablePrizes);
此模块的核心逻辑是:
- 查询所有可发放的奖品信息
- 统计每个奖品已发放的数量
- 计算每个奖品剩余可发放数量
- 生成可发放奖品列表,并随机打乱顺序
2.3 中奖名单生成与记录模块
该模块是整个系统的核心,负责根据权重随机算法生成中奖名单,并将中奖记录写入数据库:
// 获取上一批次编号
int lastBatchNumber = 0;
var lastBatchRecord = dataService.Query<WinningRecordEntity>(p => p.IsActive)
.OrderByDescending(p => p.Id).FirstOrDefault();
if (lastBatchRecord != null)
{
lastBatchNumber = lastBatchRecord.BatchNumber;
}
// 生成中奖用户列表
var winningUsers = GenerateLotteryUsers(uniqueCandidates, totalPrizeCount);
// 记录中奖信息到数据库
for (int i = 0; i < winningUsers.Count; i++)
{
var user = winningUsers[i];
var prize = availablePrizes[i];
var winRecord = new WinningRecordEntity
{
UserId = user.UserId,
PrizeId = prize.Id,
IsRedeemed = false,
BatchNumber = lastBatchNumber + 1,
IsPublished = false,
};
dataService.Add(winRecord);
}
return result;
}
三、权重随机算法实现
3.1 算法核心逻辑
权重随机算法是整个系统的核心,它根据用户答题次数作为权重,实现概率性的随机选择:
/// <summary>
/// 根据答题次数作为权重,随机选择中奖用户
/// </summary>
/// <param name="userAnswerCounts">用户答题次数统计(含权重)</param>
/// <param name="prizeCount">需要抽取的中奖人数</param>
/// <returns>中奖用户列表</returns>
private List<UserAnswerCountOutput> GenerateLotteryUsers(List<UserAnswerCountOutput> userAnswerCounts, int prizeCount)
{
// 用于记录已中奖的用户身份标识,确保一个用户只能中奖一次
var uniqueIdentifiers = new HashSet<string>();
var winningUsers = new List<UserAnswerCountOutput>();
var random = new Random();
// 循环抽奖,直到抽满奖品数量或没有候选用户
while (winningUsers.Count < prizeCount && userAnswerCounts.Count > 0)
{
// 计算总权重(所有用户答题次数之和)
int totalWeight = userAnswerCounts.Sum(u => u.AnswerCount);
if (totalWeight == 0) break; // 没有权重,无法抽奖
// 生成随机权重值
int randomWeight = random.Next(1, totalWeight + 1);
int currentWeight = 0;
UserAnswerCountOutput selectedUser = null;
// 根据权重选择用户
foreach (var user in userAnswerCounts)
{
currentWeight += user.AnswerCount;
if (currentWeight >= randomWeight)
{
selectedUser = user;
break;
}
}
if (selectedUser != null)
{
// 生成用户唯一标识(身份证+手机号)
string idNumber = selectedUser.IdNumber.Trim();
string phoneNumber = selectedUser.PhoneNumber.Trim();
string uniqueKey = $"{idNumber}_{phoneNumber}";
// 检查是否已中奖
if (!uniqueIdentifiers.Contains(uniqueKey))
{
// 未中奖,添加到中奖列表
winningUsers.Add(selectedUser);
uniqueIdentifiers.Add(uniqueKey);
// 从候选列表中移除该用户(按身份信息)
userAnswerCounts.RemoveAll(u => u.IdNumber.Trim() == idNumber && u.PhoneNumber.Trim() == phoneNumber);
}
else
{
// 已中奖,从候选列表中移除该用户
userAnswerCounts.Remove(selectedUser);
}
}
}
return winningUsers;
}
3.2 算法关键点解析
- 权重计算:以用户答题次数作为抽奖权重,答题次数越多,中奖概率越大
- 唯一性保证:通过身份证和手机号组合作为唯一标识,确保同一用户只能中奖一次
- 去重处理:在候选用户中,对同一用户的多条记录,保留答题次数最多的一条
- 随机打乱:对奖品列表进行随机排序,避免奖品发放顺序固定
注意:
需要重写Equals方法:
public class UserInfoOutput: IEquatable<UserInfoOutput>
{
public string UserId { get; set; }
public int Count { get; set; }
public string IDCard { get; set; }
public string Mobile { get; set; }
public bool Equals(UserInfoOutput other)
{
if (other is null)
return false;
return this.IDCard == other.IDCard && this.Mobile == other.Mobile;
//return UserId == other.UserId;
}
public override int GetHashCode()
{
//return UserId.GetHashCode();
return HashCode.Combine(IDCard, Mobile);
}
public override bool Equals(object obj)
{
return Equals(obj as UserInfoOutput);
}
}
920

被折叠的 条评论
为什么被折叠?



