KeeperResolveService.cs 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. using System.Text.RegularExpressions;
  2. using SqlSugar;
  3. namespace Admin.NET.Plugin.AiDOP.DataPlatform.Wms;
  4. /// <summary>
  5. /// 领料保管员解析。移植基准:旧 DOP ResourceExamineAppService.GenerateNbr
  6. /// (按 EmpWorkDutyMaster.ItemNum1..ItemNum2 字符串区间匹配 Employee)。
  7. /// 比较口径见 WP7 §3.6(B5):Trim + ToUpperInvariant 后 Ordinal;最窄区间优先。
  8. /// </summary>
  9. public sealed class KeeperResolveService : ITransient
  10. {
  11. private static readonly Regex SafeItemNum = new(@"^[0-9A-Z]+$", RegexOptions.Compiled);
  12. private readonly MdpSourceScopeFactory _scopeFactory;
  13. public KeeperResolveService(MdpSourceScopeFactory scopeFactory)
  14. {
  15. _scopeFactory = scopeFactory;
  16. }
  17. public sealed class KeeperHit
  18. {
  19. public string ItemNum { get; set; } = "";
  20. public int DutyRecID { get; set; }
  21. public string Employee { get; set; } = "";
  22. public string? Location { get; set; }
  23. }
  24. public sealed class KeeperResolveResult
  25. {
  26. public IReadOnlyList<KeeperHit> Hits { get; init; } = Array.Empty<KeeperHit>();
  27. /// <summary>未匹配到职责的物料号</summary>
  28. public IReadOnlyList<string> MissingItems { get; init; } = Array.Empty<string>();
  29. /// <summary>命中但 Location 为空的物料号</summary>
  30. public IReadOnlyList<string> EmptyLocationItems { get; init; } = Array.Empty<string>();
  31. /// <summary>字符集越界(物料号或职责区间)</summary>
  32. public IReadOnlyList<string> UnsafeItems { get; init; } = Array.Empty<string>();
  33. public bool Ok => MissingItems.Count == 0 && EmptyLocationItems.Count == 0 && UnsafeItems.Count == 0;
  34. }
  35. /// <summary>
  36. /// 按物料号匹配职责;返回命中与 WP7 §3.2 硬校验 / §3.6 字符集校验结果。
  37. /// </summary>
  38. public async Task<KeeperResolveResult> ResolveByItemsAsync(
  39. string domain, IEnumerable<string> itemNums, CancellationToken ct = default)
  40. {
  41. var items = itemNums.Where(x => !string.IsNullOrWhiteSpace(x)).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
  42. if (items.Count == 0)
  43. return new KeeperResolveResult();
  44. var db = await _scopeFactory.GetScopeAsync("DOPDEMORQ_SQLSERVER", ct);
  45. var duties = await db.Ado.SqlQueryAsync<DutyRow>(
  46. @"SELECT RecID, Employee, ItemNum1, ItemNum2, Location
  47. FROM EmpWorkDutyMaster
  48. WHERE Domain=@d
  49. ORDER BY ItemNum1, ItemNum2, RecID",
  50. new SugarParameter("@d", domain));
  51. var hits = new List<KeeperHit>();
  52. var missing = new List<string>();
  53. var emptyLoc = new List<string>();
  54. var unsafeItems = new List<string>();
  55. var hasUnsafeDuty = duties.Any(s => !IsSafe(Norm(s.ItemNum1)) || !IsSafe(Norm(s.ItemNum2)));
  56. foreach (var raw in items)
  57. {
  58. var key = Norm(raw);
  59. if (!IsSafe(key))
  60. {
  61. unsafeItems.Add(raw);
  62. continue;
  63. }
  64. // 最窄区间优先:下界最大、上界最小、同宽 RecID 小者
  65. var match = duties
  66. .Select(d => (Row: d, A: Norm(d.ItemNum1), B: Norm(d.ItemNum2)))
  67. .Where(d => IsSafe(d.A) && IsSafe(d.B)
  68. && string.Compare(d.A, key, StringComparison.Ordinal) <= 0
  69. && string.Compare(key, d.B, StringComparison.Ordinal) <= 0)
  70. .OrderByDescending(d => d.A, StringComparer.Ordinal)
  71. .ThenBy(d => d.B, StringComparer.Ordinal)
  72. .ThenBy(d => d.Row.RecID)
  73. .Select(d => d.Row)
  74. .FirstOrDefault();
  75. if (match == null)
  76. {
  77. if (hasUnsafeDuty) unsafeItems.Add(raw);
  78. else missing.Add(raw);
  79. continue;
  80. }
  81. if (string.IsNullOrWhiteSpace(match.Employee))
  82. {
  83. missing.Add(raw);
  84. continue;
  85. }
  86. if (string.IsNullOrWhiteSpace(match.Location))
  87. {
  88. emptyLoc.Add(raw);
  89. continue;
  90. }
  91. hits.Add(new KeeperHit
  92. {
  93. ItemNum = raw,
  94. DutyRecID = match.RecID,
  95. Employee = match.Employee!,
  96. Location = match.Location
  97. });
  98. }
  99. return new KeeperResolveResult
  100. {
  101. Hits = hits,
  102. MissingItems = missing,
  103. EmptyLocationItems = emptyLoc,
  104. UnsafeItems = unsafeItems
  105. };
  106. }
  107. /// <summary>拼 NbrMaster.User1(逗号)与 User2(JSON 数组字符串),与旧 DOP 一致。</summary>
  108. public static (string user1, string user2) FormatKeeperUsers(IEnumerable<KeeperHit> hits)
  109. {
  110. var emps = hits.Select(x => x.Employee).Where(x => !string.IsNullOrWhiteSpace(x)).Distinct().ToList();
  111. if (emps.Count == 0) return ("", "[]");
  112. var user1 = string.Join(",", emps);
  113. var user2 = System.Text.Json.JsonSerializer.Serialize(emps);
  114. return (user1, user2);
  115. }
  116. internal static string Norm(string? s) => (s ?? string.Empty).Trim().ToUpperInvariant();
  117. private static bool IsSafe(string s) => SafeItemNum.IsMatch(s);
  118. private sealed class DutyRow
  119. {
  120. public int RecID { get; set; }
  121. public string? Employee { get; set; }
  122. public string? ItemNum1 { get; set; }
  123. public string? ItemNum2 { get; set; }
  124. public string? Location { get; set; }
  125. }
  126. }