using System.Text.RegularExpressions; using SqlSugar; namespace Admin.NET.Plugin.AiDOP.DataPlatform.Wms; /// /// 领料保管员解析。移植基准:旧 DOP ResourceExamineAppService.GenerateNbr /// (按 EmpWorkDutyMaster.ItemNum1..ItemNum2 字符串区间匹配 Employee)。 /// 比较口径见 WP7 §3.6(B5):Trim + ToUpperInvariant 后 Ordinal;最窄区间优先。 /// public sealed class KeeperResolveService : ITransient { private static readonly Regex SafeItemNum = new(@"^[0-9A-Z]+$", RegexOptions.Compiled); private readonly MdpSourceScopeFactory _scopeFactory; public KeeperResolveService(MdpSourceScopeFactory scopeFactory) { _scopeFactory = scopeFactory; } public sealed class KeeperHit { public string ItemNum { get; set; } = ""; public int DutyRecID { get; set; } public string Employee { get; set; } = ""; public string? Location { get; set; } } public sealed class KeeperResolveResult { public IReadOnlyList Hits { get; init; } = Array.Empty(); /// 未匹配到职责的物料号 public IReadOnlyList MissingItems { get; init; } = Array.Empty(); /// 命中但 Location 为空的物料号 public IReadOnlyList EmptyLocationItems { get; init; } = Array.Empty(); /// 字符集越界(物料号或职责区间) public IReadOnlyList UnsafeItems { get; init; } = Array.Empty(); public bool Ok => MissingItems.Count == 0 && EmptyLocationItems.Count == 0 && UnsafeItems.Count == 0; } /// /// 按物料号匹配职责;返回命中与 WP7 §3.2 硬校验 / §3.6 字符集校验结果。 /// public async Task ResolveByItemsAsync( string domain, IEnumerable itemNums, CancellationToken ct = default) { var items = itemNums.Where(x => !string.IsNullOrWhiteSpace(x)).Distinct(StringComparer.OrdinalIgnoreCase).ToList(); if (items.Count == 0) return new KeeperResolveResult(); var db = await _scopeFactory.GetScopeAsync("DOPDEMORQ_SQLSERVER", ct); var duties = await db.Ado.SqlQueryAsync( @"SELECT RecID, Employee, ItemNum1, ItemNum2, Location FROM EmpWorkDutyMaster WHERE Domain=@d ORDER BY ItemNum1, ItemNum2, RecID", new SugarParameter("@d", domain)); var hits = new List(); var missing = new List(); var emptyLoc = new List(); var unsafeItems = new List(); var hasUnsafeDuty = duties.Any(s => !IsSafe(Norm(s.ItemNum1)) || !IsSafe(Norm(s.ItemNum2))); foreach (var raw in items) { var key = Norm(raw); if (!IsSafe(key)) { unsafeItems.Add(raw); continue; } // 最窄区间优先:下界最大、上界最小、同宽 RecID 小者 var match = duties .Select(d => (Row: d, A: Norm(d.ItemNum1), B: Norm(d.ItemNum2))) .Where(d => IsSafe(d.A) && IsSafe(d.B) && string.Compare(d.A, key, StringComparison.Ordinal) <= 0 && string.Compare(key, d.B, StringComparison.Ordinal) <= 0) .OrderByDescending(d => d.A, StringComparer.Ordinal) .ThenBy(d => d.B, StringComparer.Ordinal) .ThenBy(d => d.Row.RecID) .Select(d => d.Row) .FirstOrDefault(); if (match == null) { if (hasUnsafeDuty) unsafeItems.Add(raw); else missing.Add(raw); continue; } if (string.IsNullOrWhiteSpace(match.Employee)) { missing.Add(raw); continue; } if (string.IsNullOrWhiteSpace(match.Location)) { emptyLoc.Add(raw); continue; } hits.Add(new KeeperHit { ItemNum = raw, DutyRecID = match.RecID, Employee = match.Employee!, Location = match.Location }); } return new KeeperResolveResult { Hits = hits, MissingItems = missing, EmptyLocationItems = emptyLoc, UnsafeItems = unsafeItems }; } /// 拼 NbrMaster.User1(逗号)与 User2(JSON 数组字符串),与旧 DOP 一致。 public static (string user1, string user2) FormatKeeperUsers(IEnumerable hits) { var emps = hits.Select(x => x.Employee).Where(x => !string.IsNullOrWhiteSpace(x)).Distinct().ToList(); if (emps.Count == 0) return ("", "[]"); var user1 = string.Join(",", emps); var user2 = System.Text.Json.JsonSerializer.Serialize(emps); return (user1, user2); } internal static string Norm(string? s) => (s ?? string.Empty).Trim().ToUpperInvariant(); private static bool IsSafe(string s) => SafeItemNum.IsMatch(s); private sealed class DutyRow { public int RecID { get; set; } public string? Employee { get; set; } public string? ItemNum1 { get; set; } public string? ItemNum2 { get; set; } public string? Location { get; set; } } }