using System.Collections.Concurrent;
using Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Numbering;
using Xunit;
using Xunit.Abstractions;
namespace Admin.NET.Plugin.AiDOP.Tests.S5.MaterialWarehouse;
///
/// Phase 4A 取号并发测试(同进程;InMemory provider)。
/// **可现在证明**:同进程高并发下取号结果不重复、批内连续、重试有界。
/// **不能声称**:多实例 + 真实 MySQL 最终唯一 / DB UNIQUE 实拒(→ Phase 4B)。
///
public class BusinessNumberConcurrencyTests
{
private readonly ITestOutputHelper _out;
public BusinessNumberConcurrencyTests(ITestOutputHelper output) => _out = output;
private static InMemoryBusinessNumberProvider NewProvider()
=> new InMemoryBusinessNumberProvider(
new[]
{
new BusinessNumberRule
{
RuleCode = "IQC_RCT", Prefix1 = "IR", DateType = "YYYYMMDD",
IsDateType = true, MaxValue = 9999999, IniValue = 1,
},
},
clock: () => new DateTime(2026, 8, 8));
[Fact]
public async Task NextBatch_1000Concurrent_AllUnique_NoDuplicate()
{
const int n = 1000;
var provider = NewProvider();
var bag = new ConcurrentBag();
var tasks = Enumerable.Range(0, n).Select(_ => Task.Run(async () =>
{
var b = await provider.NextBatchAsync("IQC_RCT", "8010", 1, "tester");
bag.Add(b[0]);
}));
await Task.WhenAll(tasks);
var all = bag.ToList();
var unique = all.Distinct().Count();
var duplicates = all.Count - unique;
_out.WriteLine($"[取号并发] 总请求={all.Count} 唯一={unique} 重复={duplicates}");
Assert.Equal(n, all.Count);
Assert.Equal(n, unique);
Assert.Equal(0, duplicates);
}
[Fact]
public async Task NextBatch_ReservesContiguousBlock()
{
var provider = NewProvider();
var batch = await provider.NextBatchAsync("IQC_RCT", "8010", 5, "tester");
Assert.Equal(5, batch.Count);
Assert.Equal(5, batch.Distinct().Count());
// 批内连续:末位流水依次 +1
Assert.EndsWith("0000001", batch[0]);
Assert.EndsWith("0000005", batch[4]);
}
[Fact]
public async Task NextBatch_DifferentDomain_IndependentCounters()
{
var provider = NewProvider();
var a = (await provider.NextBatchAsync("IQC_RCT", "8010", 1, "t"))[0];
var b = (await provider.NextBatchAsync("IQC_RCT", "9010", 1, "t"))[0];
// 不同 domain 各自计数,均从 1 开始 → 号相同但作用域不同(说明按 domain 隔离)
Assert.EndsWith("0000001", a);
Assert.EndsWith("0000001", b);
}
[Fact]
public async Task NextWithUniqueRetry_DuplicateThenSuccess_BoundedAttempts()
{
var svc = new BusinessNumberService(NewProvider());
var calls = 0;
// 前 2 次模拟撞 DB UNIQUE(返回 false),第 3 次成功
var r = await svc.NextWithUniqueRetryAsync("IQC_RCT", "8010", "t",
tryConsume: _ => { calls++; return Task.FromResult(calls >= 3); }, maxRetry: 5);
_out.WriteLine($"[取号重试] success={r.Success} attempts={r.Attempts}");
Assert.True(r.Success);
Assert.Equal(3, r.Attempts);
}
[Fact]
public async Task NextWithUniqueRetry_AllDuplicate_FailsBounded_NoInfiniteLoop()
{
var svc = new BusinessNumberService(NewProvider());
var calls = 0;
var r = await svc.NextWithUniqueRetryAsync("IQC_RCT", "8010", "t",
tryConsume: _ => { calls++; return Task.FromResult(false); }, maxRetry: 5);
_out.WriteLine($"[取号重试-全撞] success={r.Success} attempts={r.Attempts} calls={calls}");
Assert.False(r.Success); // 有界失败,不无限循环
Assert.Equal(5, r.Attempts);
Assert.Equal(5, calls);
}
}