using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging.Abstractions; using VendingMachine.Application.DTOs.Vending; using VendingMachine.Domain.Entities; using VendingMachine.Infrastructure.Data; using VendingMachine.Infrastructure.Services; using Xunit; namespace VendingMachine.Tests; /// /// 集成测试:贩卖机扫码-锁定-支付回调-解锁 完整流程 /// public class VendingFlowIntegrationTests : IDisposable { private readonly AppDbContext _db; private readonly PointsService _pointsService; private readonly VendingMachineService _vendingService; private readonly InMemoryRedisStore _redis; public VendingFlowIntegrationTests() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .ConfigureWarnings(w => w.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.InMemoryEventId.TransactionIgnoredWarning)) .Options; _db = new AppDbContext(options); _redis = new InMemoryRedisStore(); _pointsService = new PointsService(_db, NullLogger.Instance); _vendingService = new VendingMachineService( _db, _redis, _pointsService, NullLogger.Instance); } /// /// 完整贩卖机流程:生成二维码 → 扫码获取用户信息(锁定)→ 支付回调(积分发放+优惠券核销+解锁) /// [Fact] public async Task FullVendingFlow_Qrcode_Lock_PaymentCallback_Unlock() { // === 种子数据:会员用户 + 积分配置 + 可用优惠券 === var user = new User { Phone = "1234567890", AreaCode = "+86", Nickname = "用户888888", IsMember = true, MembershipType = MembershipType.Monthly, MembershipExpireAt = DateTime.UtcNow.AddDays(20), PointsBalance = 500 }; _db.Users.Add(user); var pointsConfig = new PointsConfig { Id = 1, ConversionRate = 1m }; _db.PointsConfigs.Add(pointsConfig); var couponTemplate = new CouponTemplate { Id = "tpl-10off", NameZhCn = "满100减10", Type = CouponType.ThresholdDiscount, ThresholdAmount = 100m, DiscountAmount = 10m, PointsCost = 0, ExpireAt = DateTime.UtcNow.AddDays(30), IsActive = true }; _db.CouponTemplates.Add(couponTemplate); var userCoupon = new UserCoupon { CouponTemplateId = "tpl-10off", UserId = user.Uid, Status = CouponStatus.Available, ExpireAt = DateTime.UtcNow.AddDays(30) }; _db.UserCoupons.Add(userCoupon); await _db.SaveChangesAsync(); var couponId = userCoupon.Id; // === 第一步:生成会员二维码 === var qrcodeResult = await _vendingService.GenerateQrcodeAsync(user.Uid); Assert.True(qrcodeResult.Success); Assert.False(string.IsNullOrEmpty(qrcodeResult.Data!.Token)); Assert.True(qrcodeResult.Data.ExpiresAt > 0); var qrcodeToken = qrcodeResult.Data.Token; // === 第二步:贩卖机扫码获取用户信息(同时锁定用户) === var userInfoResult = await _vendingService.GetUserByQrcodeAsync( new QrcodeRequest { QrcodeToken = qrcodeToken }, "machine-001"); Assert.True(userInfoResult.Success); Assert.Equal(user.Uid, userInfoResult.Data!.UserId); Assert.True(userInfoResult.Data.IsMember); Assert.False(userInfoResult.Data.IsLocked); Assert.NotEmpty(userInfoResult.Data.Coupons); // === 第三步:验证用户已被锁定(其他贩卖机请求应返回锁定状态) === // 需要重新生成二维码(上一个已被消费) var qrcode2 = await _vendingService.GenerateQrcodeAsync(user.Uid); var lockedResult = await _vendingService.GetUserByQrcodeAsync( new QrcodeRequest { QrcodeToken = qrcode2.Data!.Token }, "machine-002"); Assert.True(lockedResult.Success); Assert.True(lockedResult.Data!.IsLocked); // 被 machine-001 锁定 // === 第四步:支付回调(成功支付,使用优惠券) === var initialBalance = (await _db.Users.FirstAsync(u => u.Uid == user.Uid)).PointsBalance; var paymentResult = await _vendingService.ReportPaymentAsync(new VendingPaymentPayload { UserId = user.Uid, MachineId = "machine-001", PaymentAmount = 150m, UsedCouponId = couponId, PaymentStatus = "success", TransactionId = "txn-001" }); Assert.True(paymentResult.Success); // === 第五步:验证积分已发放 === var updatedUser = await _db.Users.FirstAsync(u => u.Uid == user.Uid); // 150 * 1 = 150 积分 Assert.Equal(initialBalance + 150, updatedUser.PointsBalance); // === 第六步:验证优惠券已核销 === var usedCoupon = await _db.UserCoupons.FirstAsync(uc => uc.Id == couponId); Assert.Equal(CouponStatus.Used, usedCoupon.Status); Assert.NotNull(usedCoupon.UsedAt); // === 第七步:验证用户锁定已解除 === var lockValue = await _redis.GetAsync($"vending:lock:{user.Uid}"); Assert.Null(lockValue); } /// /// 非会员用户无法生成二维码 /// [Fact] public async Task GenerateQrcode_NonMember_ShouldFail() { var user = new User { Phone = "9999999999", AreaCode = "+86", Nickname = "用户000000", IsMember = false }; _db.Users.Add(user); await _db.SaveChangesAsync(); var result = await _vendingService.GenerateQrcodeAsync(user.Uid); Assert.False(result.Success); Assert.Contains("非会员", result.Message); } /// /// 过期二维码应被拒绝 /// [Fact] public async Task GetUserByQrcode_ExpiredToken_ShouldFail() { var result = await _vendingService.GetUserByQrcodeAsync( new QrcodeRequest { QrcodeToken = "invalid-or-expired-token" }, "machine-001"); Assert.False(result.Success); Assert.Contains("无效", result.Message); } /// /// 支付失败时也应解除用户锁定 /// [Fact] public async Task PaymentCallback_Failed_ShouldStillUnlockUser() { var user = new User { Phone = "5555555555", AreaCode = "+86", Nickname = "用户555555", IsMember = true, MembershipType = MembershipType.Monthly, MembershipExpireAt = DateTime.UtcNow.AddDays(10), PointsBalance = 100 }; _db.Users.Add(user); _db.PointsConfigs.Add(new PointsConfig { Id = 1, ConversionRate = 1m }); await _db.SaveChangesAsync(); // 模拟用户被锁定 await _redis.SetAsync($"vending:lock:{user.Uid}", "machine-003", TimeSpan.FromMinutes(10)); // 支付失败回调 var result = await _vendingService.ReportPaymentAsync(new VendingPaymentPayload { UserId = user.Uid, MachineId = "machine-003", PaymentAmount = 50m, PaymentStatus = "failed", TransactionId = "txn-fail-001" }); Assert.True(result.Success); // 验证锁定已解除 var lockValue = await _redis.GetAsync($"vending:lock:{user.Uid}"); Assert.Null(lockValue); // 验证积分未变化(支付失败不发放积分) var updatedUser = await _db.Users.FirstAsync(u => u.Uid == user.Uid); Assert.Equal(100, updatedUser.PointsBalance); } public void Dispose() { _db.Dispose(); } }