using System.Collections.Concurrent; using Admin.NET.Plugin.AiDOP.Supply; namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse.InventoryPosting.Numbering; /// /// 进程内原子取号 provider(线程安全)——用于单元/并发测试与「同进程强唯一」证明。 /// 用 ConcurrentDictionary 计数器按 scope(ruleCode|domain|dateBucket) 原子预留连续区段, /// 复用 NumberRuleFormatter 做格式化。**不代表跨实例/真实 MySQL 唯一性**(那属 Phase 4B)。 /// public sealed class InMemoryBusinessNumberProvider : IBusinessNumberProvider { private readonly Dictionary _rules; private readonly Func _clock; private readonly ConcurrentDictionary _counters = new(); public InMemoryBusinessNumberProvider(IEnumerable rules, Func clock = null) { _rules = (rules ?? throw new ArgumentNullException(nameof(rules))) .ToDictionary(r => r.RuleCode, StringComparer.OrdinalIgnoreCase); _clock = clock ?? (() => DateTime.Now); } public Task> NextBatchAsync(string ruleCode, string domain, int count, string operatorNo) { if (string.IsNullOrWhiteSpace(ruleCode)) throw new ArgumentException("ruleCode 不能为空", nameof(ruleCode)); if (count <= 0) throw new ArgumentException("count 必须大于 0", nameof(count)); if (!_rules.TryGetValue(ruleCode.Trim(), out var rule)) throw new InvalidOperationException($"未找到编号规则:{ruleCode}"); var now = _clock(); var sequenceDate = NumberRuleFormatter.ResolveSequenceDate(rule.DateType, rule.IsDateType, now); var bucket = sequenceDate?.ToString("yyyyMMdd") ?? "-"; var key = $"{rule.RuleCode}|{domain ?? string.Empty}|{bucket}"; // 原子预留连续 count 个流水:AddOrUpdate 经 CAS 串行化,返回值即本次区段末值,无重叠。 var last = _counters.AddOrUpdate(key, rule.IniValue + count - 1, (_, v) => v + count); var first = last - count + 1; var numbers = Enumerable.Range(first, count) .Select(serial => NumberRuleFormatter.BuildNumber( rule.Prefix1, rule.Prefix2, rule.Prefix3, rule.DateType, rule.IsDateType, serial, rule.MaxValue, now)) .ToList(); return Task.FromResult>(numbers); } }