InMemoryBusinessNumberProvider.cs 2.4 KB

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