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";
}