using Admin.NET.Plugin.AiDOP.MaterialWarehouse.Dto; namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse; /// /// S5 IQC 项目实例 + 逐样本 的只读读取器(Batch 3)。 /// /// 单独成类的原因:Detail(GET)与 save-detail(POST 回执)需要**完全相同**的行形状与汇总口径, /// 放一处可避免两边 SQL 漂移。本类只读,不含任何写入。 /// /// 【租户边界】所有查询都显式带 tenant_id 谓词,且首列即 tenant_id(命中 uk/idx 最左前缀)。 /// 绝不依赖 billId / itemId 的全局唯一性作为隔离手段。 /// public class IqcInspBillItemReader : ITransient { private readonly ISqlSugarClient _db; public IqcInspBillItemReader(ISqlSugarClient db) => _db = db; /// /// 该检验单的项目实例条数。0 ⇒ LEGACY MODE(历史单 / NO_SPEC 新单 / MATCHED+0 明细新单,同一语义)。 /// 判据只看数据,不看日期 / 单号 / 版本。 /// public async Task CountItemsAsync(long billId, long tenantId) => await _db.Ado.GetIntAsync( "SELECT COUNT(1) FROM ado_s5_iqc_insp_bill_item WHERE tenant_id=@TenantId AND inspection_bill_id=@BillId", new SugarParameter("@TenantId", tenantId), new SugarParameter("@BillId", billId)); /// /// 载入该检验单的全部项目实例(含其样本,按 seq/id 与 sample_index 升序)。 /// itemCount=0 时返回空列表(不是 null)。 /// public async Task> LoadLinesAsync(long billId, long tenantId) { var pars = new List { new("@TenantId", tenantId), new("@BillId", billId), }; var lines = await _db.Ado.SqlQueryAsync( """ SELECT id AS Id, seq AS Seq, source_spec_id AS SourceSpecId, source_spec_item_id AS SourceSpecItemId, inspection_item AS InspectionItem, inspection_standard AS InspectionStandard, method_tool AS MethodTool, image_category AS ImageCategory, sampling_scheme AS SamplingScheme, spec_remark AS SpecRemark, upper_limit AS UpperLimit, lower_limit AS LowerLimit, result_type AS ResultType, item_judgement AS ItemJudgement, judgement_source AS JudgementSource, qualified_qty AS QualifiedQty, unqualified_qty AS UnqualifiedQty, result_remark AS ResultRemark, last_result_by AS LastResultBy, last_result_at AS LastResultAt FROM ado_s5_iqc_insp_bill_item WHERE tenant_id = @TenantId AND inspection_bill_id = @BillId ORDER BY seq, id """, pars); if (lines.Count == 0) return lines; // 样本按单一次取(反规范化的 inspection_bill_id 使其免 JOIN),再按 item 分组 var samples = await _db.Ado.SqlQueryAsync( """ SELECT id AS Id, item_id AS ItemId, sample_index AS SampleIndex, sample_value_raw AS SampleValueRaw, sample_numeric AS SampleNumeric, judgement AS Judgement, remark AS Remark, inspector_id AS InspectorId, inspection_time AS InspectionTime FROM ado_s5_iqc_insp_bill_item_sample WHERE tenant_id = @TenantId AND inspection_bill_id = @BillId ORDER BY item_id, sample_index """, pars); var byItem = samples.GroupBy(s => s.ItemId).ToDictionary(g => g.Key, g => g.ToList()); foreach (var line in lines) { if (!byItem.TryGetValue(line.Id, out var list)) continue; line.Samples = list.Select(s => new IqcInspBillItemSampleRow { Id = s.Id, SampleIndex = s.SampleIndex, SampleValueRaw = s.SampleValueRaw, SampleNumeric = s.SampleNumeric, Judgement = s.Judgement, Remark = s.Remark, InspectorId = s.InspectorId, InspectionTime = s.InspectionTime, }).ToList(); } return lines; } /// 实时聚合汇总(不落表,避免与 item 行形成第二个判定真源)。 public static IqcInspBillLineSummary Summarize(IReadOnlyList lines) { var s = new IqcInspBillLineSummary { ItemCount = lines.Count }; foreach (var l in lines) { switch (l.ItemJudgement) { case IqcJudgement.Pass: s.PassCount++; s.JudgedCount++; break; case IqcJudgement.Fail: s.FailCount++; s.JudgedCount++; break; case IqcJudgement.Invalid: s.InvalidCount++; break; default: s.UnjudgedCount++; break; } } return s; } /// 样本原始行(含 ItemId 用于分组,不外泄到 API 契约)。 private sealed class SampleRow { public long Id { get; set; } public long ItemId { get; set; } public int SampleIndex { get; set; } public string? SampleValueRaw { get; set; } public decimal? SampleNumeric { get; set; } public string? Judgement { get; set; } public string? Remark { get; set; } public long? InspectorId { get; set; } public DateTime? InspectionTime { get; set; } } }