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