| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120 |
- using Admin.NET.Plugin.AiDOP.Manufacturing.Dto;
- using System.Globalization;
- namespace Admin.NET.Plugin.AiDOP.Manufacturing;
- /// <summary>
- /// S6 过程检规 自动唯一解析器(Phase 1B)。
- ///
- /// 输入 (CurrentTenant, ItemCode, ReferenceDate) → 唯一有效过程检规 或 明确的不可解析原因。
- /// 运行期只查规范映射表 ado_s6_ipqc_spec_material_map(不再 split wlbm blob)+ S0 检规头/明细。
- /// 裁决规则(wjbh 分组,见 <see cref="Decide"/>):
- /// 候选 = map(tenant,item,EXACT) ⋈ qms_gcjygf(同租户),取有效者(sxrj 可解析、sxrj <= ReferenceDate、明细行数 > 0);
- /// 按 wjbh(文件编号)分组,组内取 sxrj 最新(并列取 version 最大)为有效版本;
- /// 恰好 1 个 wjbh 组有效 → MATCHED;≥2 个 → AMBIGUOUS;
- /// 有候选但均无明细/日期非法 → INVALID_SPEC;无候选/仅未来生效 → NO_MATCH。
- /// 全程 tenant 显式过滤,绝不跨租户。禁止人工选择、禁止 id desc 兜底。
- /// </summary>
- public class S6ProcessSpecResolver : ITransient
- {
- private readonly ISqlSugarClient _db;
- public S6ProcessSpecResolver(ISqlSugarClient db) => _db = db;
- public async Task<S6ProcessSpecResolveResult> ResolveAsync(long tenantId, string? itemCode, DateTime referenceDate)
- {
- if (tenantId <= 0 || string.IsNullOrWhiteSpace(itemCode))
- return new S6ProcessSpecResolveResult { Status = S6SpecResolveStatus.NoMatch, Message = "缺少物料编码,无法解析过程检验规范。" };
- var candidates = await _db.Ado.SqlQueryAsync<S6SpecCandidate>(
- """
- SELECT g.id AS Id, g.wjbh AS Wjbh, g.bb AS Bb, g.version AS Version, g.sxrj AS Sxrj,
- (SELECT COUNT(1) FROM qms_gcjygfzb z WHERE z.tenant_id=@TenantId AND z.glid=g.id) AS DetailCount
- FROM ado_s6_ipqc_spec_material_map m
- JOIN qms_gcjygf g ON g.id=m.spec_id AND g.tenant_id=@TenantId
- WHERE m.tenant_id=@TenantId AND m.item_code=@ItemCode AND m.match_status='EXACT'
- """,
- new List<SugarParameter> { new("@TenantId", tenantId), new("@ItemCode", itemCode.Trim()) });
- return Decide(candidates, referenceDate);
- }
- /// <summary>
- /// 纯裁决逻辑(无 DB,便于单测)。candidates 为已按 (tenant,item,EXACT) 命中的检规候选。
- /// </summary>
- public static S6ProcessSpecResolveResult Decide(IReadOnlyList<S6SpecCandidate>? candidates, DateTime referenceDate)
- {
- var result = new S6ProcessSpecResolveResult();
- if (candidates == null || candidates.Count == 0)
- {
- result.Status = S6SpecResolveStatus.NoMatch;
- result.Message = "当前物料未配置有效过程检验规范,请维护 S0 过程检验规范。";
- return result;
- }
- foreach (var c in candidates) c.NormalizedEffective = NormalizeDate(c.Sxrj);
- var effective = candidates
- .Where(c => c.NormalizedEffective != null && c.NormalizedEffective.Value.Date <= referenceDate.Date && c.DetailCount > 0)
- .ToList();
- var structurallyInvalid = candidates.Any(c => c.DetailCount <= 0 || c.NormalizedEffective == null);
- if (effective.Count > 0)
- {
- var groups = effective.GroupBy(c => (c.Wjbh ?? string.Empty).Trim()).ToList();
- if (groups.Count == 1)
- {
- var winner = groups[0]
- .OrderByDescending(c => c.NormalizedEffective)
- .ThenByDescending(c => c.Version ?? 0)
- .ThenByDescending(c => c.Id)
- .First();
- result.Status = S6SpecResolveStatus.Matched;
- result.MatchedSpecId = winner.Id;
- result.SpecCode = winner.Wjbh;
- result.SpecVersion = winner.Bb;
- result.EffectiveDate = winner.Sxrj;
- result.CandidateCount = 1;
- return result;
- }
- result.Status = S6SpecResolveStatus.Ambiguous;
- result.CandidateCount = groups.Count;
- result.Message = "当前物料存在多个同时有效过程检验规范,请修正 S0 规范配置。";
- return result;
- }
- if (structurallyInvalid)
- {
- result.Status = S6SpecResolveStatus.InvalidSpec;
- result.Message = "过程检验规范配置无效,请检查版本/生效日期/明细。";
- return result;
- }
- result.Status = S6SpecResolveStatus.NoMatch;
- result.Message = "当前物料未配置有效过程检验规范,请维护 S0 过程检验规范。";
- return result;
- }
- /// <summary>
- /// 归一化 S0 sxrj(生效日期)自由文本为日期。支持 2026-08-28 / 2025.12.12 / 2025-05-28 00:00:00 等;不可解析返回 null。
- /// </summary>
- public static DateTime? NormalizeDate(string? raw)
- {
- if (string.IsNullOrWhiteSpace(raw)) return null;
- var s = raw.Trim().Replace('.', '-').Replace('/', '-');
- var sp = s.IndexOf(' ');
- if (sp > 0) s = s[..sp];
- if (DateTime.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.None, out var d)) return d;
- if (DateTime.TryParseExact(s, new[] { "yyyy-M-d", "yyyy-MM-dd" }, CultureInfo.InvariantCulture, DateTimeStyles.None, out d)) return d;
- return null;
- }
- /// <summary>tokenize 多值 wlbm(英/中分号、逗号、空白、换行)→ 规范化片段(建立映射用,非运行期)。</summary>
- public static IReadOnlyList<string> Tokenize(string? wlbm)
- {
- if (string.IsNullOrWhiteSpace(wlbm)) return Array.Empty<string>();
- var parts = wlbm.Split(new[] { ';', ';', ',', ',', '、', ' ', '\t', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
- return parts.Select(p => p.Trim().ToUpperInvariant()).Where(p => p.Length > 0).Distinct().ToList();
- }
- }
|