S5IqcMaterialTokenizer.cs 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. using System.Globalization;
  2. using System.Text.RegularExpressions;
  3. namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse;
  4. /// <summary>
  5. /// S5 IQC 原材料检规 <c>qms_jygf.wlbm</c> 分词 / 标准化 / 分级 —— 全链路唯一口径来源。
  6. ///
  7. /// 【为什么不复用 S7 的 tokenizer】
  8. /// S7 <c>FqcSpecMaterialMapService.TokenSplitter</c> = <c>[;;,,\r\n\t/::\s]+</c>,**不含顿号 `、`**;
  9. /// 而实测 <c>qms_jygf</c> 4712 行中含 `、` 者 273 行、含换行 186 行、含 `;`/`,` 者 **0 行** ——
  10. /// 顿号才是 IQC 侧的主力分隔符,原样复用会把 273 行多物料串整体当成一个 token。
  11. /// 同理 S7 的 <c>PlainCodeShape = ^[A-Za-z0-9]{4,12}$</c> 也不能复用:实测 ItemMaster 存在
  12. /// <c>9.0314.01.001</c>(13 位含点) 与 <c>钉11匣转向杆-加长0.3</c>(13 位含中文与横杠) 两个合法物料码。
  13. ///
  14. /// 【分隔符安全性证据(实测 ItemMaster 22684 行)】
  15. /// <c>; ; , , 、 空格 : : /</c> 在 ItemNum 中出现次数**全为 0** → 可安全作为分隔符;
  16. /// <c>.</c> 出现 2 次、<c>-</c> 出现 1 次、<c>_</c> 出现 0 次 → **绝不可作为分隔符**(会切断合法编码)。
  17. ///
  18. /// 【标准化】实测 ItemMaster 全部已是大写(<c>ItemNum &lt;&gt; UPPER(ItemNum)</c> 计数 = 0),
  19. /// 故 Trim + ToUpperInvariant 是无损的,且标准化结果即 ItemMaster 的规范写法。
  20. ///
  21. /// 本类为纯函数、无 DB、无租户概念,便于单测;运行期 SyncSpecAsync 与 UpdateScripts 迁移脚本
  22. /// 必须共用本类定义的同一套规则(迁移脚本的等价性保证见 <see cref="IsSingleTokenSafeForSqlSeed"/>)。
  23. /// </summary>
  24. public static class S5IqcMaterialTokenizer
  25. {
  26. /// <summary>
  27. /// wlbm 分隔符集合:半/全角分号、半/全角逗号、顿号、半/全角冒号、斜杠、空白(空格/Tab/CR/LF)。
  28. /// **不含 `.` `-` `_`** —— 它们出现在合法物料编码内部。
  29. /// </summary>
  30. public const string DelimiterChars = ";;,,、::/ \t\r\n";
  31. private static readonly Regex Splitter = new(@"[;;,,、::/\s]+", RegexOptions.Compiled);
  32. /// <summary>物料码长度合理区间。实测 ItemMaster 实际区间为 [5,13],此处放宽为 [4,16] 留容差。</summary>
  33. private const int MinCodeLength = 4;
  34. private const int MaxCodeLength = 16;
  35. /// <summary>
  36. /// 切分 wlbm 为原始片段(保序、去空白片段)。**不做去重、不做大小写变换** —— 保留 raw_token 原貌用于审计。
  37. /// </summary>
  38. public static IReadOnlyList<string> Split(string? wlbm)
  39. {
  40. if (string.IsNullOrWhiteSpace(wlbm)) return Array.Empty<string>();
  41. return Splitter.Split(wlbm)
  42. .Select(x => x.Trim())
  43. .Where(x => x.Length > 0)
  44. .ToList();
  45. }
  46. /// <summary>标准化:Trim + 转大写不变文化。实测 ItemMaster 全大写,故该结果即规范物料码写法。</summary>
  47. public static string Normalize(string rawToken)
  48. => (rawToken ?? string.Empty).Trim().ToUpperInvariant();
  49. /// <summary>
  50. /// 分级:EXACT / NOT_FOUND / GLUE_UNRESOLVED。
  51. /// 判定顺序固定:先精确命中,再形状判粘连,最后未找到。
  52. /// GLUE_UNRESOLVED 表示"该片段根本不像一个物料码"(长度越界或含 CJK 文本,如 `00参考明细列表`);
  53. /// 注意含 CJK 的**合法**物料码会在第一步 EXACT 命中,不会走到形状判断,故此规则安全。
  54. /// </summary>
  55. public static string Classify(string normalizedToken, ISet<string> itemCodeSet)
  56. {
  57. ArgumentNullException.ThrowIfNull(itemCodeSet);
  58. if (string.IsNullOrEmpty(normalizedToken)) return S5IqcMapStatus.GlueUnresolved;
  59. if (itemCodeSet.Contains(normalizedToken)) return S5IqcMapStatus.Exact;
  60. if (normalizedToken.Length < MinCodeLength || normalizedToken.Length > MaxCodeLength)
  61. return S5IqcMapStatus.GlueUnresolved;
  62. if (ContainsCjk(normalizedToken)) return S5IqcMapStatus.GlueUnresolved;
  63. return S5IqcMapStatus.NotFound;
  64. }
  65. private static bool ContainsCjk(string s)
  66. {
  67. foreach (var ch in s)
  68. {
  69. // CJK 统一表意文字主区 + 扩展A + 兼容表意 + 中文标点
  70. if ((ch >= '一' && ch <= '鿿')
  71. || (ch >= '㐀' && ch <= '䶿')
  72. || (ch >= '豈' && ch <= '﫿')
  73. || (ch >= ' ' && ch <= '〿'))
  74. return true;
  75. }
  76. return false;
  77. }
  78. /// <summary>
  79. /// SQL 迁移等价性守卫:判断某条 wlbm 是否为"无分隔符单 token"。
  80. ///
  81. /// UpdateScripts 迁移脚本**只允许**为该函数返回 true 的行生成映射 —— 此时分词退化为恒等变换
  82. /// (<c>TRIM(UPPER(wlbm))</c>),SQL 与本 tokenizer 的结果**可证明相同**,杜绝"迁移一套规则、
  83. /// 运行期另一套规则"。含分隔符的行一律不由 SQL 落地,改由 <c>SyncSpecAsync</c> / <c>Rebuild</c> 生成。
  84. /// </summary>
  85. public static bool IsSingleTokenSafeForSqlSeed(string? wlbm)
  86. {
  87. if (string.IsNullOrWhiteSpace(wlbm)) return false;
  88. return wlbm.Trim().IndexOfAny(DelimiterChars.ToCharArray()) < 0;
  89. }
  90. }
  91. /// <summary>桥表 match_status 取值(与 S7 <c>ado_s7_fqc_spec_material_map</c> 同名同义)。</summary>
  92. public static class S5IqcMapStatus
  93. {
  94. /// <summary>token 与 ItemMaster.ItemNum 精确命中(自动派生)。</summary>
  95. public const string Exact = "EXACT";
  96. /// <summary>人工绑定(脏 wlbm 的订正结果),优先级高于自动派生。</summary>
  97. public const string Manual = "MANUAL";
  98. /// <summary>形状像物料码但 ItemMaster 无此码。</summary>
  99. public const string NotFound = "NOT_FOUND";
  100. /// <summary>不像单个物料码(长度越界 / 含中文描述),如 `00参考明细列表`。</summary>
  101. public const string GlueUnresolved = "GLUE_UNRESOLVED";
  102. }