S6ProcessSpecResolver.cs 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. using Admin.NET.Plugin.AiDOP.Manufacturing.Dto;
  2. using System.Globalization;
  3. namespace Admin.NET.Plugin.AiDOP.Manufacturing;
  4. /// <summary>
  5. /// S6 过程检规 自动唯一解析器(Phase 1B)。
  6. ///
  7. /// 输入 (CurrentTenant, ItemCode, ReferenceDate) → 唯一有效过程检规 或 明确的不可解析原因。
  8. /// 运行期只查规范映射表 ado_s6_ipqc_spec_material_map(不再 split wlbm blob)+ S0 检规头/明细。
  9. /// 裁决规则(wjbh 分组,见 <see cref="Decide"/>):
  10. /// 候选 = map(tenant,item,EXACT) ⋈ qms_gcjygf(同租户),取有效者(sxrj 可解析、sxrj &lt;= ReferenceDate、明细行数 &gt; 0);
  11. /// 按 wjbh(文件编号)分组,组内取 sxrj 最新(并列取 version 最大)为有效版本;
  12. /// 恰好 1 个 wjbh 组有效 → MATCHED;≥2 个 → AMBIGUOUS;
  13. /// 有候选但均无明细/日期非法 → INVALID_SPEC;无候选/仅未来生效 → NO_MATCH。
  14. /// 全程 tenant 显式过滤,绝不跨租户。禁止人工选择、禁止 id desc 兜底。
  15. /// </summary>
  16. public class S6ProcessSpecResolver : ITransient
  17. {
  18. private readonly ISqlSugarClient _db;
  19. public S6ProcessSpecResolver(ISqlSugarClient db) => _db = db;
  20. public async Task<S6ProcessSpecResolveResult> ResolveAsync(long tenantId, string? itemCode, DateTime referenceDate)
  21. {
  22. if (tenantId <= 0 || string.IsNullOrWhiteSpace(itemCode))
  23. return new S6ProcessSpecResolveResult { Status = S6SpecResolveStatus.NoMatch, Message = "缺少物料编码,无法解析过程检验规范。" };
  24. var candidates = await _db.Ado.SqlQueryAsync<S6SpecCandidate>(
  25. """
  26. SELECT g.id AS Id, g.wjbh AS Wjbh, g.bb AS Bb, g.version AS Version, g.sxrj AS Sxrj,
  27. (SELECT COUNT(1) FROM qms_gcjygfzb z WHERE z.tenant_id=@TenantId AND z.glid=g.id) AS DetailCount
  28. FROM ado_s6_ipqc_spec_material_map m
  29. JOIN qms_gcjygf g ON g.id=m.spec_id AND g.tenant_id=@TenantId
  30. WHERE m.tenant_id=@TenantId AND m.item_code=@ItemCode AND m.match_status='EXACT'
  31. """,
  32. new List<SugarParameter> { new("@TenantId", tenantId), new("@ItemCode", itemCode.Trim()) });
  33. return Decide(candidates, referenceDate);
  34. }
  35. /// <summary>
  36. /// 纯裁决逻辑(无 DB,便于单测)。candidates 为已按 (tenant,item,EXACT) 命中的检规候选。
  37. /// </summary>
  38. public static S6ProcessSpecResolveResult Decide(IReadOnlyList<S6SpecCandidate>? candidates, DateTime referenceDate)
  39. {
  40. var result = new S6ProcessSpecResolveResult();
  41. if (candidates == null || candidates.Count == 0)
  42. {
  43. result.Status = S6SpecResolveStatus.NoMatch;
  44. result.Message = "当前物料未配置有效过程检验规范,请维护 S0 过程检验规范。";
  45. return result;
  46. }
  47. foreach (var c in candidates) c.NormalizedEffective = NormalizeDate(c.Sxrj);
  48. var effective = candidates
  49. .Where(c => c.NormalizedEffective != null && c.NormalizedEffective.Value.Date <= referenceDate.Date && c.DetailCount > 0)
  50. .ToList();
  51. var structurallyInvalid = candidates.Any(c => c.DetailCount <= 0 || c.NormalizedEffective == null);
  52. if (effective.Count > 0)
  53. {
  54. var groups = effective.GroupBy(c => (c.Wjbh ?? string.Empty).Trim()).ToList();
  55. if (groups.Count == 1)
  56. {
  57. var winner = groups[0]
  58. .OrderByDescending(c => c.NormalizedEffective)
  59. .ThenByDescending(c => c.Version ?? 0)
  60. .ThenByDescending(c => c.Id)
  61. .First();
  62. result.Status = S6SpecResolveStatus.Matched;
  63. result.MatchedSpecId = winner.Id;
  64. result.SpecCode = winner.Wjbh;
  65. result.SpecVersion = winner.Bb;
  66. result.EffectiveDate = winner.Sxrj;
  67. result.CandidateCount = 1;
  68. return result;
  69. }
  70. result.Status = S6SpecResolveStatus.Ambiguous;
  71. result.CandidateCount = groups.Count;
  72. result.Message = "当前物料存在多个同时有效过程检验规范,请修正 S0 规范配置。";
  73. return result;
  74. }
  75. if (structurallyInvalid)
  76. {
  77. result.Status = S6SpecResolveStatus.InvalidSpec;
  78. result.Message = "过程检验规范配置无效,请检查版本/生效日期/明细。";
  79. return result;
  80. }
  81. result.Status = S6SpecResolveStatus.NoMatch;
  82. result.Message = "当前物料未配置有效过程检验规范,请维护 S0 过程检验规范。";
  83. return result;
  84. }
  85. /// <summary>
  86. /// 归一化 S0 sxrj(生效日期)自由文本为日期。支持 2026-08-28 / 2025.12.12 / 2025-05-28 00:00:00 等;不可解析返回 null。
  87. /// </summary>
  88. public static DateTime? NormalizeDate(string? raw)
  89. {
  90. if (string.IsNullOrWhiteSpace(raw)) return null;
  91. var s = raw.Trim().Replace('.', '-').Replace('/', '-');
  92. var sp = s.IndexOf(' ');
  93. if (sp > 0) s = s[..sp];
  94. if (DateTime.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.None, out var d)) return d;
  95. if (DateTime.TryParseExact(s, new[] { "yyyy-M-d", "yyyy-MM-dd" }, CultureInfo.InvariantCulture, DateTimeStyles.None, out d)) return d;
  96. return null;
  97. }
  98. /// <summary>tokenize 多值 wlbm(英/中分号、逗号、空白、换行)→ 规范化片段(建立映射用,非运行期)。</summary>
  99. public static IReadOnlyList<string> Tokenize(string? wlbm)
  100. {
  101. if (string.IsNullOrWhiteSpace(wlbm)) return Array.Empty<string>();
  102. var parts = wlbm.Split(new[] { ';', ';', ',', ',', '、', ' ', '\t', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
  103. return parts.Select(p => p.Trim().ToUpperInvariant()).Where(p => p.Length > 0).Distinct().ToList();
  104. }
  105. }