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
}