HaniBlindBox/server/HoneyBox/tests/HoneyBox.Tests/Integration/DesignatedPrizeLotteryIntegrationTests.cs
2026-02-01 19:30:51 +08:00

581 lines
22 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using HoneyBox.Core.Interfaces;
using HoneyBox.Core.Services;
using HoneyBox.Model.Data;
using HoneyBox.Model.Entities;
using HoneyBox.Model.Models.Lottery;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;
namespace HoneyBox.Tests.Integration;
/// <summary>
/// 指定用户中奖功能端到端集成测试
/// 测试完整抽奖流程:配置指定中奖 → 用户抽奖 → 验证结果
/// Requirements: 2.3, 2.4, 2.5, 3.3, 3.4, 3.5
/// </summary>
public class DesignatedPrizeLotteryIntegrationTests : IDisposable
{
private readonly HoneyBoxDbContext _dbContext;
private readonly Mock<IInventoryManager> _mockInventoryManager;
private readonly Mock<IRewardService> _mockRewardService;
private readonly Mock<ILogger<LotteryEngine>> _mockLogger;
private readonly LotteryEngine _lotteryEngine;
public DesignatedPrizeLotteryIntegrationTests()
{
var options = new DbContextOptionsBuilder<HoneyBoxDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.ConfigureWarnings(w => w.Ignore(InMemoryEventId.TransactionIgnoredWarning))
.Options;
_dbContext = new HoneyBoxDbContext(options);
_mockInventoryManager = new Mock<IInventoryManager>();
_mockRewardService = new Mock<IRewardService>();
_mockLogger = new Mock<ILogger<LotteryEngine>>();
_lotteryEngine = new LotteryEngine(_dbContext, _mockInventoryManager.Object, _mockRewardService.Object, _mockLogger.Object);
}
public void Dispose()
{
_dbContext.Dispose();
}
#region Test Data Setup
private async Task<User> CreateTestUserAsync(int id, string nickname = "测试用户")
{
var user = new User
{
Id = id,
OpenId = $"test_openid_{id}",
Uid = $"test_uid_{id}",
Nickname = nickname,
HeadImg = "avatar.jpg",
Mobile = $"1380013800{id}",
Money = 1000,
Integral = 10000,
Money2 = 5000,
IsTest = 0,
Status = 1,
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
};
_dbContext.Users.Add(user);
await _dbContext.SaveChangesAsync();
return user;
}
private async Task<Good> CreateTestGoodsAsync(int id, byte type = 1, int status = 1)
{
var goods = new Good
{
Id = id,
Title = $"测试商品{id}",
Type = type,
Status = (byte)status,
ShowIs = 0,
Price = 10,
Stock = 100,
SaleStock = 0,
LockIs = 0,
IsShouZhe = 0,
QuanjuXiangou = 0,
DailyXiangou = 0,
ChoujiangXianzhi = 0,
ImgUrl = "img.jpg",
ImgUrlDetail = "detail.jpg",
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
};
_dbContext.Goods.Add(goods);
await _dbContext.SaveChangesAsync();
return goods;
}
private async Task<List<GoodsItem>> CreateTestGoodsItemsAsync(int goodsId, int count = 5)
{
var items = new List<GoodsItem>();
for (int i = 1; i <= count; i++)
{
var item = new GoodsItem
{
Id = goodsId * 100 + i,
GoodsId = goodsId,
Num = 1,
Title = $"奖品{i}",
Stock = 10,
SurplusStock = 10,
Price = 100 * i,
Money = 50 * i,
ScMoney = 40 * i,
ShangId = i,
GoodsListId = 0,
ImgUrl = $"prize{i}.jpg",
Sort = i,
RealPro = 20, // 每个奖品20%概率
GoodsType = 1,
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
};
items.Add(item);
}
_dbContext.GoodsItems.AddRange(items);
await _dbContext.SaveChangesAsync();
return items;
}
private async Task<GoodsDesignatedPrize> CreateDesignatedPrizeConfigAsync(int goodsId, int goodsItemId, int userId, bool isActive = true)
{
var config = new GoodsDesignatedPrize
{
GoodsId = goodsId,
GoodsItemId = goodsItemId,
UserId = userId,
IsActive = isActive,
Remark = "测试指定中奖配置",
CreatedAt = DateTime.Now
};
_dbContext.GoodsDesignatedPrizes.Add(config);
await _dbContext.SaveChangesAsync();
return config;
}
private List<PrizeProbability> CreatePrizeProbabilities(List<GoodsItem> items)
{
var totalStock = items.Sum(i => i.SurplusStock);
return items.Select(i => new PrizeProbability
{
GoodsItemId = i.Id,
Title = i.Title,
ImgUrl = i.ImgUrl,
ShangId = i.ShangId ?? 0,
SurplusStock = i.SurplusStock,
Probability = (decimal)i.SurplusStock / totalStock * 100,
Weight = i.SurplusStock,
Price = i.Price,
ScMoney = i.ScMoney,
GoodsType = i.GoodsType,
Doubling = i.Doubling,
IsLingzhu = i.IsLingzhu,
ParentGoodsListId = i.GoodsListId
}).ToList();
}
private List<PrizeProbability> CreatePrizeProbabilitiesByRealPro(List<GoodsItem> items)
{
var totalPro = items.Sum(i => i.RealPro);
return items.Select(i => new PrizeProbability
{
GoodsItemId = i.Id,
Title = i.Title,
ImgUrl = i.ImgUrl,
ShangId = i.ShangId ?? 0,
SurplusStock = i.SurplusStock,
Probability = i.RealPro,
Weight = (double)i.RealPro,
Price = i.Price,
ScMoney = i.ScMoney,
GoodsType = i.GoodsType,
Doubling = i.Doubling,
IsLingzhu = i.IsLingzhu,
ParentGoodsListId = i.GoodsListId
}).ToList();
}
#endregion
#region (Requirements 2.3, 2.4, 2.5)
/// <summary>
/// 有限赏完整流程:配置指定中奖 → 指定用户抽奖 → 验证指定用户可以抽到指定奖品
/// Requirements: 2.3
/// </summary>
[Fact]
public async Task FiniteLottery_DesignatedUser_CanDrawDesignatedPrize()
{
// Arrange
var designatedUser = await CreateTestUserAsync(1, "指定用户");
var goods = await CreateTestGoodsAsync(1, type: 1); // 有限赏
var items = await CreateTestGoodsItemsAsync(goods.Id, 5);
var designatedItem = items[0]; // 第一个奖品指定给用户1
await CreateDesignatedPrizeConfigAsync(goods.Id, designatedItem.Id, designatedUser.Id);
var prizePool = CreatePrizeProbabilities(items);
var designatedPrizes = await _dbContext.GoodsDesignatedPrizes
.Where(dp => dp.GoodsId == goods.Id && dp.IsActive)
.ToDictionaryAsync(dp => dp.GoodsItemId, dp => dp.UserId);
// Act
var filteredPool = _lotteryEngine.FilterPrizePoolWithFallback(prizePool, designatedUser.Id, designatedPrizes);
// Assert
Assert.Contains(filteredPool, p => p.GoodsItemId == designatedItem.Id);
Assert.Equal(5, filteredPool.Count); // 指定用户可以看到所有奖品(普通+自己的指定)
}
/// <summary>
/// 有限赏完整流程:配置指定中奖 → 普通用户抽奖 → 验证普通用户无法抽到指定奖品
/// Requirements: 2.4
/// </summary>
[Fact]
public async Task FiniteLottery_NonDesignatedUser_CannotDrawDesignatedPrize()
{
// Arrange
var designatedUser = await CreateTestUserAsync(1, "指定用户");
var normalUser = await CreateTestUserAsync(2, "普通用户");
var goods = await CreateTestGoodsAsync(1, type: 1);
var items = await CreateTestGoodsItemsAsync(goods.Id, 5);
var designatedItem = items[0];
await CreateDesignatedPrizeConfigAsync(goods.Id, designatedItem.Id, designatedUser.Id);
var prizePool = CreatePrizeProbabilities(items);
var designatedPrizes = await _dbContext.GoodsDesignatedPrizes
.Where(dp => dp.GoodsId == goods.Id && dp.IsActive)
.ToDictionaryAsync(dp => dp.GoodsItemId, dp => dp.UserId);
// Act
var filteredPool = _lotteryEngine.FilterPrizePoolWithFallback(prizePool, normalUser.Id, designatedPrizes);
// Assert
Assert.DoesNotContain(filteredPool, p => p.GoodsItemId == designatedItem.Id);
Assert.Equal(4, filteredPool.Count); // 普通用户只能看到4个普通奖品
}
/// <summary>
/// 有限赏兜底机制测试:只剩指定奖品时普通用户可以抽奖
/// Requirements: 2.5
/// </summary>
[Fact]
public async Task FiniteLottery_FallbackMechanism_WhenOnlyDesignatedPrizesRemain()
{
// Arrange
var designatedUser = await CreateTestUserAsync(1, "指定用户");
var normalUser = await CreateTestUserAsync(2, "普通用户");
var goods = await CreateTestGoodsAsync(1, type: 1);
var items = await CreateTestGoodsItemsAsync(goods.Id, 3);
// 所有奖品都指定给用户1
foreach (var item in items)
{
await CreateDesignatedPrizeConfigAsync(goods.Id, item.Id, designatedUser.Id);
}
var prizePool = CreatePrizeProbabilities(items);
var designatedPrizes = await _dbContext.GoodsDesignatedPrizes
.Where(dp => dp.GoodsId == goods.Id && dp.IsActive)
.ToDictionaryAsync(dp => dp.GoodsItemId, dp => dp.UserId);
// Act
var filteredPool = _lotteryEngine.FilterPrizePoolWithFallback(prizePool, normalUser.Id, designatedPrizes);
// Assert - 兜底机制触发,普通用户可以抽到所有奖品
Assert.Equal(3, filteredPool.Count);
Assert.Equal(prizePool.Count, filteredPool.Count);
}
/// <summary>
/// 有限赏:多个指定奖品配置测试
/// Requirements: 2.3, 2.4
/// </summary>
[Fact]
public async Task FiniteLottery_MultipleDesignatedPrizes_FilteredCorrectly()
{
// Arrange
var user1 = await CreateTestUserAsync(1, "用户1");
var user2 = await CreateTestUserAsync(2, "用户2");
var user3 = await CreateTestUserAsync(3, "用户3");
var goods = await CreateTestGoodsAsync(1, type: 1);
var items = await CreateTestGoodsItemsAsync(goods.Id, 5);
// 奖品1指定给用户1奖品2指定给用户2
await CreateDesignatedPrizeConfigAsync(goods.Id, items[0].Id, user1.Id);
await CreateDesignatedPrizeConfigAsync(goods.Id, items[1].Id, user2.Id);
var prizePool = CreatePrizeProbabilities(items);
var designatedPrizes = await _dbContext.GoodsDesignatedPrizes
.Where(dp => dp.GoodsId == goods.Id && dp.IsActive)
.ToDictionaryAsync(dp => dp.GoodsItemId, dp => dp.UserId);
// Act - 用户1的奖品池
var user1Pool = _lotteryEngine.FilterPrizePoolWithFallback(prizePool, user1.Id, designatedPrizes);
// Act - 用户2的奖品池
var user2Pool = _lotteryEngine.FilterPrizePoolWithFallback(prizePool, user2.Id, designatedPrizes);
// Act - 用户3的奖品池普通用户
var user3Pool = _lotteryEngine.FilterPrizePoolWithFallback(prizePool, user3.Id, designatedPrizes);
// Assert
Assert.Equal(4, user1Pool.Count); // 3个普通 + 1个自己的指定
Assert.Contains(user1Pool, p => p.GoodsItemId == items[0].Id);
Assert.DoesNotContain(user1Pool, p => p.GoodsItemId == items[1].Id);
Assert.Equal(4, user2Pool.Count); // 3个普通 + 1个自己的指定
Assert.Contains(user2Pool, p => p.GoodsItemId == items[1].Id);
Assert.DoesNotContain(user2Pool, p => p.GoodsItemId == items[0].Id);
Assert.Equal(3, user3Pool.Count); // 只有3个普通奖品
Assert.DoesNotContain(user3Pool, p => p.GoodsItemId == items[0].Id);
Assert.DoesNotContain(user3Pool, p => p.GoodsItemId == items[1].Id);
}
#endregion
#region (Requirements 3.3, 3.4, 3.5)
/// <summary>
/// 无限赏完整流程:配置指定中奖 → 指定用户抽奖 → 验证指定用户可以抽到指定奖品
/// Requirements: 3.4
/// </summary>
[Fact]
public async Task InfiniteLottery_DesignatedUser_CanDrawDesignatedPrize()
{
// Arrange
var designatedUser = await CreateTestUserAsync(1, "指定用户");
var goods = await CreateTestGoodsAsync(1, type: 2); // 无限赏
var items = await CreateTestGoodsItemsAsync(goods.Id, 5);
var designatedItem = items[0];
await CreateDesignatedPrizeConfigAsync(goods.Id, designatedItem.Id, designatedUser.Id);
var prizePool = CreatePrizeProbabilitiesByRealPro(items);
var designatedPrizes = await _dbContext.GoodsDesignatedPrizes
.Where(dp => dp.GoodsId == goods.Id && dp.IsActive)
.ToDictionaryAsync(dp => dp.GoodsItemId, dp => dp.UserId);
// Act
var filteredPool = _lotteryEngine.FilterPrizePoolStrict(prizePool, designatedUser.Id, designatedPrizes);
// Assert
Assert.Contains(filteredPool, p => p.GoodsItemId == designatedItem.Id);
Assert.Equal(5, filteredPool.Count);
}
/// <summary>
/// 无限赏完整流程:配置指定中奖 → 普通用户抽奖 → 验证普通用户永远无法抽到指定奖品
/// Requirements: 3.3, 3.5
/// </summary>
[Fact]
public async Task InfiniteLottery_NonDesignatedUser_CanNeverDrawDesignatedPrize()
{
// Arrange
var designatedUser = await CreateTestUserAsync(1, "指定用户");
var normalUser = await CreateTestUserAsync(2, "普通用户");
var goods = await CreateTestGoodsAsync(1, type: 2);
var items = await CreateTestGoodsItemsAsync(goods.Id, 5);
var designatedItem = items[0];
await CreateDesignatedPrizeConfigAsync(goods.Id, designatedItem.Id, designatedUser.Id);
var prizePool = CreatePrizeProbabilitiesByRealPro(items);
var designatedPrizes = await _dbContext.GoodsDesignatedPrizes
.Where(dp => dp.GoodsId == goods.Id && dp.IsActive)
.ToDictionaryAsync(dp => dp.GoodsItemId, dp => dp.UserId);
// Act
var filteredPool = _lotteryEngine.FilterPrizePoolStrict(prizePool, normalUser.Id, designatedPrizes);
// Assert - 严格模式,普通用户永远看不到指定奖品
Assert.DoesNotContain(filteredPool, p => p.GoodsItemId == designatedItem.Id);
Assert.Equal(4, filteredPool.Count);
}
/// <summary>
/// 无限赏严格模式:所有奖品都指定给其他用户时,普通用户奖品池为空(无兜底)
/// Requirements: 3.5
/// </summary>
[Fact]
public async Task InfiniteLottery_StrictMode_NoFallback_WhenAllPrizesDesignated()
{
// Arrange
var designatedUser = await CreateTestUserAsync(1, "指定用户");
var normalUser = await CreateTestUserAsync(2, "普通用户");
var goods = await CreateTestGoodsAsync(1, type: 2);
var items = await CreateTestGoodsItemsAsync(goods.Id, 3);
// 所有奖品都指定给用户1
foreach (var item in items)
{
await CreateDesignatedPrizeConfigAsync(goods.Id, item.Id, designatedUser.Id);
}
var prizePool = CreatePrizeProbabilitiesByRealPro(items);
var designatedPrizes = await _dbContext.GoodsDesignatedPrizes
.Where(dp => dp.GoodsId == goods.Id && dp.IsActive)
.ToDictionaryAsync(dp => dp.GoodsItemId, dp => dp.UserId);
// Act
var filteredPool = _lotteryEngine.FilterPrizePoolStrict(prizePool, normalUser.Id, designatedPrizes);
// Assert - 严格模式无兜底,奖品池为空
Assert.Empty(filteredPool);
}
/// <summary>
/// 无限赏:多个指定奖品配置测试
/// Requirements: 3.3, 3.4
/// </summary>
[Fact]
public async Task InfiniteLottery_MultipleDesignatedPrizes_StrictFiltering()
{
// Arrange
var user1 = await CreateTestUserAsync(1, "用户1");
var user2 = await CreateTestUserAsync(2, "用户2");
var user3 = await CreateTestUserAsync(3, "用户3");
var goods = await CreateTestGoodsAsync(1, type: 2);
var items = await CreateTestGoodsItemsAsync(goods.Id, 5);
await CreateDesignatedPrizeConfigAsync(goods.Id, items[0].Id, user1.Id);
await CreateDesignatedPrizeConfigAsync(goods.Id, items[1].Id, user2.Id);
var prizePool = CreatePrizeProbabilitiesByRealPro(items);
var designatedPrizes = await _dbContext.GoodsDesignatedPrizes
.Where(dp => dp.GoodsId == goods.Id && dp.IsActive)
.ToDictionaryAsync(dp => dp.GoodsItemId, dp => dp.UserId);
// Act
var user1Pool = _lotteryEngine.FilterPrizePoolStrict(prizePool, user1.Id, designatedPrizes);
var user2Pool = _lotteryEngine.FilterPrizePoolStrict(prizePool, user2.Id, designatedPrizes);
var user3Pool = _lotteryEngine.FilterPrizePoolStrict(prizePool, user3.Id, designatedPrizes);
// Assert
Assert.Equal(4, user1Pool.Count);
Assert.Contains(user1Pool, p => p.GoodsItemId == items[0].Id);
Assert.DoesNotContain(user1Pool, p => p.GoodsItemId == items[1].Id);
Assert.Equal(4, user2Pool.Count);
Assert.Contains(user2Pool, p => p.GoodsItemId == items[1].Id);
Assert.DoesNotContain(user2Pool, p => p.GoodsItemId == items[0].Id);
Assert.Equal(3, user3Pool.Count);
Assert.DoesNotContain(user3Pool, p => p.GoodsItemId == items[0].Id);
Assert.DoesNotContain(user3Pool, p => p.GoodsItemId == items[1].Id);
}
#endregion
#region
/// <summary>
/// 测试禁用的指定中奖配置不生效
/// Requirements: 1.3
/// </summary>
[Fact]
public async Task DisabledDesignatedPrizeConfig_ShouldNotAffectFiltering()
{
// Arrange
var designatedUser = await CreateTestUserAsync(1, "指定用户");
var normalUser = await CreateTestUserAsync(2, "普通用户");
var goods = await CreateTestGoodsAsync(1, type: 1);
var items = await CreateTestGoodsItemsAsync(goods.Id, 5);
// 创建禁用的配置
await CreateDesignatedPrizeConfigAsync(goods.Id, items[0].Id, designatedUser.Id, isActive: false);
var prizePool = CreatePrizeProbabilities(items);
var designatedPrizes = await _dbContext.GoodsDesignatedPrizes
.Where(dp => dp.GoodsId == goods.Id && dp.IsActive)
.ToDictionaryAsync(dp => dp.GoodsItemId, dp => dp.UserId);
// Act
var normalUserPool = _lotteryEngine.FilterPrizePoolWithFallback(prizePool, normalUser.Id, designatedPrizes);
// Assert - 禁用的配置不生效,普通用户可以看到所有奖品
Assert.Equal(5, normalUserPool.Count);
Assert.Contains(normalUserPool, p => p.GoodsItemId == items[0].Id);
}
/// <summary>
/// 测试无指定中奖配置时返回原始奖品池
/// </summary>
[Fact]
public async Task NoDesignatedPrizeConfig_ReturnsOriginalPool()
{
// Arrange
var user = await CreateTestUserAsync(1, "用户");
var goods = await CreateTestGoodsAsync(1, type: 1);
var items = await CreateTestGoodsItemsAsync(goods.Id, 5);
var prizePool = CreatePrizeProbabilities(items);
var designatedPrizes = new Dictionary<int, int>(); // 空配置
// Act
var filteredPool = _lotteryEngine.FilterPrizePoolWithFallback(prizePool, user.Id, designatedPrizes);
// Assert
Assert.Equal(prizePool.Count, filteredPool.Count);
}
#endregion
#region
/// <summary>
/// 测试空奖品池
/// </summary>
[Fact]
public void EmptyPrizePool_ReturnsEmptyPool()
{
// Arrange
var prizePool = new List<PrizeProbability>();
var designatedPrizes = new Dictionary<int, int> { { 1, 1 } };
// Act
var filteredPool = _lotteryEngine.FilterPrizePoolWithFallback(prizePool, 1, designatedPrizes);
// Assert
Assert.Empty(filteredPool);
}
/// <summary>
/// 测试单个奖品且为指定奖品的情况(有限赏兜底)
/// </summary>
[Fact]
public async Task SingleDesignatedPrize_FiniteLottery_FallbackTriggered()
{
// Arrange
var designatedUser = await CreateTestUserAsync(1, "指定用户");
var normalUser = await CreateTestUserAsync(2, "普通用户");
var goods = await CreateTestGoodsAsync(1, type: 1);
var items = await CreateTestGoodsItemsAsync(goods.Id, 1);
await CreateDesignatedPrizeConfigAsync(goods.Id, items[0].Id, designatedUser.Id);
var prizePool = CreatePrizeProbabilities(items);
var designatedPrizes = await _dbContext.GoodsDesignatedPrizes
.Where(dp => dp.GoodsId == goods.Id && dp.IsActive)
.ToDictionaryAsync(dp => dp.GoodsItemId, dp => dp.UserId);
// Act
var normalUserPool = _lotteryEngine.FilterPrizePoolWithFallback(prizePool, normalUser.Id, designatedPrizes);
// Assert - 兜底机制触发
Assert.Single(normalUserPool);
}
/// <summary>
/// 测试单个奖品且为指定奖品的情况(无限赏严格模式)
/// </summary>
[Fact]
public async Task SingleDesignatedPrize_InfiniteLottery_EmptyPool()
{
// Arrange
var designatedUser = await CreateTestUserAsync(1, "指定用户");
var normalUser = await CreateTestUserAsync(2, "普通用户");
var goods = await CreateTestGoodsAsync(1, type: 2);
var items = await CreateTestGoodsItemsAsync(goods.Id, 1);
await CreateDesignatedPrizeConfigAsync(goods.Id, items[0].Id, designatedUser.Id);
var prizePool = CreatePrizeProbabilitiesByRealPro(items);
var designatedPrizes = await _dbContext.GoodsDesignatedPrizes
.Where(dp => dp.GoodsId == goods.Id && dp.IsActive)
.ToDictionaryAsync(dp => dp.GoodsItemId, dp => dp.UserId);
// Act
var normalUserPool = _lotteryEngine.FilterPrizePoolStrict(prizePool, normalUser.Id, designatedPrizes);
// Assert - 严格模式无兜底
Assert.Empty(normalUserPool);
}
#endregion
}