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; /// /// 指定用户中奖功能端到端集成测试 /// 测试完整抽奖流程:配置指定中奖 → 用户抽奖 → 验证结果 /// Requirements: 2.3, 2.4, 2.5, 3.3, 3.4, 3.5 /// public class DesignatedPrizeLotteryIntegrationTests : IDisposable { private readonly HoneyBoxDbContext _dbContext; private readonly Mock _mockInventoryManager; private readonly Mock _mockRewardService; private readonly Mock> _mockLogger; private readonly LotteryEngine _lotteryEngine; public DesignatedPrizeLotteryIntegrationTests() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .ConfigureWarnings(w => w.Ignore(InMemoryEventId.TransactionIgnoredWarning)) .Options; _dbContext = new HoneyBoxDbContext(options); _mockInventoryManager = new Mock(); _mockRewardService = new Mock(); _mockLogger = new Mock>(); _lotteryEngine = new LotteryEngine(_dbContext, _mockInventoryManager.Object, _mockRewardService.Object, _mockLogger.Object); } public void Dispose() { _dbContext.Dispose(); } #region Test Data Setup private async Task 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 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> CreateTestGoodsItemsAsync(int goodsId, int count = 5) { var items = new List(); 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 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 CreatePrizeProbabilities(List 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 CreatePrizeProbabilitiesByRealPro(List 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) /// /// 有限赏完整流程:配置指定中奖 → 指定用户抽奖 → 验证指定用户可以抽到指定奖品 /// Requirements: 2.3 /// [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); // 指定用户可以看到所有奖品(普通+自己的指定) } /// /// 有限赏完整流程:配置指定中奖 → 普通用户抽奖 → 验证普通用户无法抽到指定奖品 /// Requirements: 2.4 /// [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个普通奖品 } /// /// 有限赏兜底机制测试:只剩指定奖品时普通用户可以抽奖 /// Requirements: 2.5 /// [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); } /// /// 有限赏:多个指定奖品配置测试 /// Requirements: 2.3, 2.4 /// [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) /// /// 无限赏完整流程:配置指定中奖 → 指定用户抽奖 → 验证指定用户可以抽到指定奖品 /// Requirements: 3.4 /// [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); } /// /// 无限赏完整流程:配置指定中奖 → 普通用户抽奖 → 验证普通用户永远无法抽到指定奖品 /// Requirements: 3.3, 3.5 /// [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); } /// /// 无限赏严格模式:所有奖品都指定给其他用户时,普通用户奖品池为空(无兜底) /// Requirements: 3.5 /// [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); } /// /// 无限赏:多个指定奖品配置测试 /// Requirements: 3.3, 3.4 /// [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 配置状态测试 /// /// 测试禁用的指定中奖配置不生效 /// Requirements: 1.3 /// [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); } /// /// 测试无指定中奖配置时返回原始奖品池 /// [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(); // 空配置 // Act var filteredPool = _lotteryEngine.FilterPrizePoolWithFallback(prizePool, user.Id, designatedPrizes); // Assert Assert.Equal(prizePool.Count, filteredPool.Count); } #endregion #region 边界情况测试 /// /// 测试空奖品池 /// [Fact] public void EmptyPrizePool_ReturnsEmptyPool() { // Arrange var prizePool = new List(); var designatedPrizes = new Dictionary { { 1, 1 } }; // Act var filteredPool = _lotteryEngine.FilterPrizePoolWithFallback(prizePool, 1, designatedPrizes); // Assert Assert.Empty(filteredPool); } /// /// 测试单个奖品且为指定奖品的情况(有限赏兜底) /// [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); } /// /// 测试单个奖品且为指定奖品的情况(无限赏严格模式) /// [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 }