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