|
|
@@ -0,0 +1,434 @@
|
|
|
+using Admin.NET.Plugin.AiDOP.MaterialWarehouse.Dto;
|
|
|
+
|
|
|
+namespace Admin.NET.Plugin.AiDOP.MaterialWarehouse;
|
|
|
+
|
|
|
+/// <summary>
|
|
|
+/// S5 IQC 项目级检验结果录入(Batch 3)。只保存结果,**不推进流程**。
|
|
|
+///
|
|
|
+/// 【职责边界】
|
|
|
+/// save-detail = 保存 item-level 测量事实与判定(草稿,可反复保存)
|
|
|
+/// submit-result = 校验 + 写整单结论 + 推进 N1→N2(仍在 IqcInspBillFlowService,本类不碰)
|
|
|
+/// ⇒ 保存 ≠ 审批,两者不可混合(原则:save-detail 绝不 Approve)
|
|
|
+///
|
|
|
+/// 【单一真源】
|
|
|
+/// NUMERIC → 测量真源 = ado_s5_iqc_insp_bill_item_sample;item 两个 qty 必须 NULL
|
|
|
+/// NON_NUMERIC → 测量真源 = item.qualified_qty / unqualified_qty;该 item 不得有样本行
|
|
|
+/// 违反者一律 400,不静默存双真源(DB 侧另有 ck_s5_iqc_item_truth 兜底)
|
|
|
+///
|
|
|
+/// 【编辑授权】唯一判据是 IqcInspBillEditGate.CanEditAsync(N1 可编辑 / N2·N3·Done 只读)。
|
|
|
+/// 刻意不叠加 jyfzr==当前用户(与 S7 FqcInspectionService 的做法分歧,理由见 Gate 注释)。
|
|
|
+/// 注意:S7 的 save-detail 完全不查流程态,只要是 jyfzr 本人在 N2/N3/终态仍可改明细
|
|
|
+/// (FqcInspectionService.cs:104-155)—— 这是已上线的口子,本类刻意不复制。
|
|
|
+///
|
|
|
+/// 【前端不得 author 的字段】sampleNumeric · sample.judgement · itemJudgement · judgementSource
|
|
|
+/// · inspectorId · inspectionTime · lastResultBy · lastResultAt · tenantId
|
|
|
+/// —— 即使 DTO 里出现也一律忽略/拒绝,全部由后端派生。
|
|
|
+/// </summary>
|
|
|
+[ApiDescriptionSettings(Order = 312, Description = "来料检验项目录入")]
|
|
|
+[Route("api/S5IqcInspection")]
|
|
|
+[NonUnify]
|
|
|
+public class IqcInspectionService : IDynamicApiController, ITransient
|
|
|
+{
|
|
|
+ /// <summary>主/系统租户哨兵:超管未选择目标租户时其 JWT TenantId 即此值,拒绝作为业务租户。</summary>
|
|
|
+ private const long MainTenantId = 1300000000001L;
|
|
|
+
|
|
|
+ private readonly ISqlSugarClient _db;
|
|
|
+ private readonly UserManager _userManager;
|
|
|
+ private readonly IqcInspBillEditGate _editGate;
|
|
|
+ private readonly IqcInspBillItemReader _reader;
|
|
|
+
|
|
|
+ public IqcInspectionService(
|
|
|
+ ISqlSugarClient db,
|
|
|
+ UserManager userManager,
|
|
|
+ IqcInspBillEditGate editGate,
|
|
|
+ IqcInspBillItemReader reader)
|
|
|
+ {
|
|
|
+ _db = db;
|
|
|
+ _userManager = userManager;
|
|
|
+ _editGate = editGate;
|
|
|
+ _reader = reader;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>严格可信租户解析:只来自认证后 JWT;无 Token 或超管未选租户 → 拒绝;不读前端 tenantId。</summary>
|
|
|
+ private long ResolveTenantOrThrow()
|
|
|
+ {
|
|
|
+ var tid = _userManager.TenantId;
|
|
|
+ if (tid <= 0) throw Oops.Oh("无法确定当前租户,请重新登录或选择目标租户");
|
|
|
+ if (_userManager.SuperAdmin && tid == MainTenantId)
|
|
|
+ throw Oops.Oh("超级管理员操作前必须选择目标租户");
|
|
|
+ return tid;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 保存项目级检验结果。item 级增量 + item 内样本全量替换;全事务,任一项失败整体回滚。
|
|
|
+ /// </summary>
|
|
|
+ [DisplayName("保存来料检验项目结果")]
|
|
|
+ [HttpPost("save-detail")]
|
|
|
+ public async Task<IqcSaveDetailOutput> SaveDetail([FromBody] IqcSaveDetailInput input)
|
|
|
+ {
|
|
|
+ var tenantId = ResolveTenantOrThrow();
|
|
|
+ if (input == null || input.BillId <= 0) throw Oops.Oh("检验单 id 非法");
|
|
|
+ if (input.Items == null || input.Items.Count == 0) throw Oops.Oh("请至少提交一个检验项目");
|
|
|
+
|
|
|
+ // ── ① 单据归属(租户谓词) ──
|
|
|
+ var owns = await _db.Ado.GetIntAsync(
|
|
|
+ "SELECT COUNT(1) FROM qms_qcp_inspbill WHERE id=@id AND tenant_id=@TenantId",
|
|
|
+ new SugarParameter("@id", input.BillId),
|
|
|
+ new SugarParameter("@TenantId", tenantId));
|
|
|
+ if (owns == 0) throw Oops.Oh("检验单不存在或不属于当前租户");
|
|
|
+
|
|
|
+ // ── ② 编辑授权(唯一判据) ──
|
|
|
+ if (!await _editGate.CanEditAsync(input.BillId, tenantId))
|
|
|
+ throw Oops.Oh("当前节点不允许修改检验明细");
|
|
|
+
|
|
|
+ // ── ③ itemId 重复检测 ──
|
|
|
+ var dup = input.Items.GroupBy(x => x.ItemId).FirstOrDefault(g => g.Count() > 1);
|
|
|
+ if (dup != null) throw Oops.Oh($"检验项目 {dup.Key} 在本次请求中重复提交");
|
|
|
+
|
|
|
+ // ── ④ 只认本单本租户的项目(跨 bill / 跨租户注入在进入事务前即被挡下) ──
|
|
|
+ var cfgList = await _db.Ado.SqlQueryAsync<ItemCfgRow>(
|
|
|
+ """
|
|
|
+ SELECT id AS Id, result_type AS ResultType, upper_limit AS UpperLimit, lower_limit AS LowerLimit,
|
|
|
+ qualified_qty AS QualifiedQty, unqualified_qty AS UnqualifiedQty,
|
|
|
+ item_judgement AS ItemJudgement, judgement_source AS JudgementSource
|
|
|
+ FROM ado_s5_iqc_insp_bill_item
|
|
|
+ WHERE tenant_id = @TenantId AND inspection_bill_id = @BillId
|
|
|
+ """,
|
|
|
+ new List<SugarParameter>
|
|
|
+ {
|
|
|
+ new("@TenantId", tenantId),
|
|
|
+ new("@BillId", input.BillId),
|
|
|
+ });
|
|
|
+ var cfg = cfgList.ToDictionary(x => x.Id);
|
|
|
+
|
|
|
+ foreach (var it in input.Items)
|
|
|
+ {
|
|
|
+ if (it.ItemId <= 0) throw Oops.Oh("检验项目 id 非法");
|
|
|
+ if (!cfg.ContainsKey(it.ItemId))
|
|
|
+ throw Oops.Oh($"检验明细 {it.ItemId} 不属于该检验单");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ── ⑤ 逐项语义校验(全部在事务外完成 ⇒ 失败时 0 partial writes) ──
|
|
|
+ var plans = new List<ItemPlan>(input.Items.Count);
|
|
|
+ foreach (var it in input.Items)
|
|
|
+ {
|
|
|
+ var db = cfg[it.ItemId];
|
|
|
+ var plan = BuildPlan(it, db);
|
|
|
+ plans.Add(plan);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ── ⑥ 单事务写入 ──
|
|
|
+ var savedItems = 0;
|
|
|
+ var savedSamples = 0;
|
|
|
+ var deletedSamples = 0;
|
|
|
+ var clearedByTypeChange = new List<long>();
|
|
|
+ var manualInvalidated = new List<long>();
|
|
|
+ var now = DateTime.Now;
|
|
|
+ var uid = _userManager.UserId;
|
|
|
+
|
|
|
+ var tran = await _db.Ado.UseTranAsync(async () =>
|
|
|
+ {
|
|
|
+ foreach (var p in plans)
|
|
|
+ {
|
|
|
+ var itemPars = new List<SugarParameter>
|
|
|
+ {
|
|
|
+ new("@TenantId", tenantId),
|
|
|
+ new("@BillId", input.BillId),
|
|
|
+ new("@ItemId", p.ItemId),
|
|
|
+ };
|
|
|
+
|
|
|
+ // 类型切换:清空该项目的**全部**旧测量事实(两类真源都清),避免残留第二真源
|
|
|
+ if (p.TypeChanged)
|
|
|
+ {
|
|
|
+ deletedSamples += await _db.Ado.ExecuteCommandAsync(
|
|
|
+ "DELETE FROM ado_s5_iqc_insp_bill_item_sample WHERE tenant_id=@TenantId AND inspection_bill_id=@BillId AND item_id=@ItemId",
|
|
|
+ itemPars);
|
|
|
+ clearedByTypeChange.Add(p.ItemId);
|
|
|
+ }
|
|
|
+
|
|
|
+ // NUMERIC:样本全量替换(携带 samples 时先删后插;未携带则不动样本集)
|
|
|
+ if (p.EffectiveResultType == IqcResultType.Numeric && p.Samples != null)
|
|
|
+ {
|
|
|
+ if (!p.TypeChanged)
|
|
|
+ {
|
|
|
+ deletedSamples += await _db.Ado.ExecuteCommandAsync(
|
|
|
+ "DELETE FROM ado_s5_iqc_insp_bill_item_sample WHERE tenant_id=@TenantId AND inspection_bill_id=@BillId AND item_id=@ItemId",
|
|
|
+ itemPars);
|
|
|
+ }
|
|
|
+
|
|
|
+ foreach (var s in p.Samples)
|
|
|
+ {
|
|
|
+ savedSamples += await _db.Ado.ExecuteCommandAsync(
|
|
|
+ """
|
|
|
+ INSERT INTO ado_s5_iqc_insp_bill_item_sample (
|
|
|
+ tenant_id, inspection_bill_id, item_id, sample_index,
|
|
|
+ sample_value_raw, sample_numeric, judgement, remark,
|
|
|
+ inspector_id, inspection_time
|
|
|
+ ) VALUES (
|
|
|
+ @TenantId, @BillId, @ItemId, @Idx,
|
|
|
+ @Raw, @Num, @Judge, @Remark,
|
|
|
+ @Uid, @Now
|
|
|
+ )
|
|
|
+ """,
|
|
|
+ new List<SugarParameter>
|
|
|
+ {
|
|
|
+ new("@TenantId", tenantId),
|
|
|
+ new("@BillId", input.BillId),
|
|
|
+ new("@ItemId", p.ItemId),
|
|
|
+ new("@Idx", s.SampleIndex),
|
|
|
+ new("@Raw", s.Raw),
|
|
|
+ new("@Num", (object?)s.Numeric ?? DBNull.Value),
|
|
|
+ new("@Judge", s.Judgement),
|
|
|
+ new("@Remark", (object?)s.Remark ?? DBNull.Value),
|
|
|
+ new("@Uid", (object?)uid ?? DBNull.Value),
|
|
|
+ new("@Now", now),
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // NON_NUMERIC:确保无样本残留(类型切换已删;此处兜住历史脏数据)
|
|
|
+ if (p.EffectiveResultType == IqcResultType.NonNumeric && !p.TypeChanged)
|
|
|
+ {
|
|
|
+ deletedSamples += await _db.Ado.ExecuteCommandAsync(
|
|
|
+ "DELETE FROM ado_s5_iqc_insp_bill_item_sample WHERE tenant_id=@TenantId AND inspection_bill_id=@BillId AND item_id=@ItemId",
|
|
|
+ itemPars);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 判定:AUTO 由真源实时重算;MANUAL 只覆盖 verdict、不改 measurement
|
|
|
+ string? finalJudgement;
|
|
|
+ string? finalSource;
|
|
|
+ if (p.ManualJudgement != null)
|
|
|
+ {
|
|
|
+ finalJudgement = p.ManualJudgement;
|
|
|
+ finalSource = IqcJudgementSource.Manual;
|
|
|
+ }
|
|
|
+ else if (p.KeepExistingVerdict)
|
|
|
+ {
|
|
|
+ // 未改测量事实且原判定为 MANUAL ⇒ 原样保留(只更新备注/审计)
|
|
|
+ finalJudgement = p.DbItemJudgement;
|
|
|
+ finalSource = p.DbJudgementSource;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ finalSource = IqcJudgementSource.Auto;
|
|
|
+ if (p.EffectiveResultType == IqcResultType.Numeric)
|
|
|
+ {
|
|
|
+ // 样本判定清单:本次替换过则用内存值,否则回读该项目现存样本
|
|
|
+ List<string?> judgements;
|
|
|
+ if (p.Samples != null)
|
|
|
+ {
|
|
|
+ judgements = p.Samples.Select(x => (string?)x.Judgement).ToList();
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ var existing = await _db.Ado.SqlQueryAsync<string>(
|
|
|
+ "SELECT judgement FROM ado_s5_iqc_insp_bill_item_sample WHERE tenant_id=@TenantId AND inspection_bill_id=@BillId AND item_id=@ItemId ORDER BY sample_index",
|
|
|
+ itemPars);
|
|
|
+ judgements = existing.Select(x => (string?)x).ToList();
|
|
|
+ }
|
|
|
+ finalJudgement = IqcInspectionJudge.AggregateNumericItem(judgements);
|
|
|
+ }
|
|
|
+ else if (p.EffectiveResultType == IqcResultType.NonNumeric)
|
|
|
+ {
|
|
|
+ finalJudgement = IqcInspectionJudge.JudgeNonNumericItem(p.FinalQualifiedQty, p.FinalUnqualifiedQty);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ // 类型未定 ⇒ 不允许有任何测量事实,判定必为空
|
|
|
+ finalJudgement = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (p.DbJudgementSource == IqcJudgementSource.Manual)
|
|
|
+ manualInvalidated.Add(p.ItemId);
|
|
|
+ }
|
|
|
+
|
|
|
+ savedItems += await _db.Ado.ExecuteCommandAsync(
|
|
|
+ """
|
|
|
+ UPDATE ado_s5_iqc_insp_bill_item
|
|
|
+ SET result_type = @ResultType,
|
|
|
+ qualified_qty = @QualifiedQty,
|
|
|
+ unqualified_qty = @UnqualifiedQty,
|
|
|
+ item_judgement = @ItemJudgement,
|
|
|
+ judgement_source = @JudgementSource,
|
|
|
+ result_remark = @ResultRemark,
|
|
|
+ last_result_by = @Uid,
|
|
|
+ last_result_at = @Now
|
|
|
+ WHERE id = @ItemId AND inspection_bill_id = @BillId AND tenant_id = @TenantId
|
|
|
+ """,
|
|
|
+ new List<SugarParameter>
|
|
|
+ {
|
|
|
+ new("@ResultType", (object?)p.EffectiveResultType ?? DBNull.Value),
|
|
|
+ new("@QualifiedQty", (object?)p.FinalQualifiedQty ?? DBNull.Value),
|
|
|
+ new("@UnqualifiedQty", (object?)p.FinalUnqualifiedQty ?? DBNull.Value),
|
|
|
+ new("@ItemJudgement", (object?)finalJudgement ?? DBNull.Value),
|
|
|
+ new("@JudgementSource", (object?)finalSource ?? DBNull.Value),
|
|
|
+ new("@ResultRemark", (object?)p.ResultRemark ?? DBNull.Value),
|
|
|
+ new("@Uid", (object?)uid ?? DBNull.Value),
|
|
|
+ new("@Now", now),
|
|
|
+ new("@ItemId", p.ItemId),
|
|
|
+ new("@BillId", input.BillId),
|
|
|
+ new("@TenantId", tenantId),
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+ if (!tran.IsSuccess) throw tran.ErrorException ?? Oops.Oh("保存检验项目结果失败");
|
|
|
+
|
|
|
+ // ── ⑦ 回执:重读全量行 + 实时汇总 ──
|
|
|
+ var lines = await _reader.LoadLinesAsync(input.BillId, tenantId);
|
|
|
+ var touched = plans.Select(p => p.ItemId).ToHashSet();
|
|
|
+ return new IqcSaveDetailOutput
|
|
|
+ {
|
|
|
+ Ok = true,
|
|
|
+ SavedItems = savedItems,
|
|
|
+ SavedSamples = savedSamples,
|
|
|
+ DeletedSamples = deletedSamples,
|
|
|
+ ClearedByTypeChange = clearedByTypeChange,
|
|
|
+ ManualInvalidated = manualInvalidated,
|
|
|
+ LineSummary = IqcInspBillItemReader.Summarize(lines),
|
|
|
+ Items = lines.Where(l => touched.Contains(l.Id)).ToList(),
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ // ───────────────────────── 计划构建(纯校验,无 DB) ─────────────────────────
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 把入参归一成写入计划,并在此完成全部语义校验。任何违规直接抛,保证事务前失败。
|
|
|
+ /// </summary>
|
|
|
+ private static ItemPlan BuildPlan(IqcSaveDetailItemInput it, ItemCfgRow db)
|
|
|
+ {
|
|
|
+ // 录入类型
|
|
|
+ if (it.ResultType != null && !IqcResultType.IsValid(it.ResultType))
|
|
|
+ throw Oops.Oh($"检验明细 {it.ItemId} 的录入类型仅支持 NUMERIC / NON_NUMERIC");
|
|
|
+
|
|
|
+ var effective = it.ResultType ?? db.ResultType;
|
|
|
+ var typeChanged = it.ResultType != null && it.ResultType != db.ResultType;
|
|
|
+
|
|
|
+ var hasQty = it.QualifiedQty.HasValue || it.UnqualifiedQty.HasValue;
|
|
|
+ var hasSamples = it.Samples != null;
|
|
|
+
|
|
|
+ // 单一真源:类型与测量形态必须匹配
|
|
|
+ if (effective == IqcResultType.Numeric && hasQty)
|
|
|
+ throw Oops.Oh($"检验明细 {it.ItemId} 为数值型,测量真源是样本值,不接受合格/不合格数");
|
|
|
+ if (effective == IqcResultType.NonNumeric && hasSamples)
|
|
|
+ throw Oops.Oh($"检验明细 {it.ItemId} 为非数值型,测量真源是合格/不合格数,不接受样本值");
|
|
|
+ if (effective == null && (hasQty || hasSamples))
|
|
|
+ throw Oops.Oh($"检验明细 {it.ItemId} 尚未选择录入类型,不能录入测量结果");
|
|
|
+
|
|
|
+ // 数量合法性
|
|
|
+ if (it.QualifiedQty is < 0m) throw Oops.Oh($"检验明细 {it.ItemId} 的合格数不能为负");
|
|
|
+ if (it.UnqualifiedQty is < 0m) throw Oops.Oh($"检验明细 {it.ItemId} 的不合格数不能为负");
|
|
|
+
|
|
|
+ // 人工判定
|
|
|
+ if (it.ManualJudgement != null && !IqcInspectionJudge.IsValidManualJudgement(it.ManualJudgement))
|
|
|
+ throw Oops.Oh($"检验明细 {it.ItemId} 的人工判定仅允许 PASS / FAIL");
|
|
|
+
|
|
|
+ // 样本:序号 >0、项目内不重复、原文非空
|
|
|
+ List<SamplePlan>? samples = null;
|
|
|
+ if (hasSamples)
|
|
|
+ {
|
|
|
+ samples = new List<SamplePlan>(it.Samples!.Count);
|
|
|
+ var seen = new HashSet<int>();
|
|
|
+ foreach (var s in it.Samples!)
|
|
|
+ {
|
|
|
+ if (s.SampleIndex < 1)
|
|
|
+ throw Oops.Oh($"检验明细 {it.ItemId} 的样本序号必须大于 0");
|
|
|
+ if (!seen.Add(s.SampleIndex))
|
|
|
+ throw Oops.Oh($"检验明细 {it.ItemId} 的样本序号 {s.SampleIndex} 重复");
|
|
|
+
|
|
|
+ var raw = s.SampleValueRaw?.Trim();
|
|
|
+ if (string.IsNullOrEmpty(raw))
|
|
|
+ throw Oops.Oh($"检验明细 {it.ItemId} 的样本 {s.SampleIndex} 未填录入值(如需删除请从数组中移除该样本)");
|
|
|
+
|
|
|
+ samples.Add(new SamplePlan
|
|
|
+ {
|
|
|
+ SampleIndex = s.SampleIndex,
|
|
|
+ Raw = raw,
|
|
|
+ Numeric = IqcInspectionJudge.ParseNumeric(raw),
|
|
|
+ // 判定只用快照冻结的上下限,绝不解析 inspection_standard 自由文本
|
|
|
+ Judgement = IqcInspectionJudge.JudgeSampleNumeric(raw, db.LowerLimit, db.UpperLimit),
|
|
|
+ Remark = s.Remark,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ samples = samples.OrderBy(x => x.SampleIndex).ToList();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 计数真源的最终值:切到非数值型、或本次显式给了数量 → 取本次值;否则沿用库中值
|
|
|
+ decimal? finalQualified;
|
|
|
+ decimal? finalUnqualified;
|
|
|
+ if (effective == IqcResultType.Numeric)
|
|
|
+ {
|
|
|
+ finalQualified = null; // 强制:数值型不得携带计数
|
|
|
+ finalUnqualified = null;
|
|
|
+ }
|
|
|
+ else if (effective == IqcResultType.NonNumeric && (typeChanged || hasQty))
|
|
|
+ {
|
|
|
+ finalQualified = it.QualifiedQty;
|
|
|
+ finalUnqualified = it.UnqualifiedQty;
|
|
|
+ }
|
|
|
+ else if (effective == IqcResultType.NonNumeric)
|
|
|
+ {
|
|
|
+ finalQualified = db.QualifiedQty;
|
|
|
+ finalUnqualified = db.UnqualifiedQty;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ finalQualified = null;
|
|
|
+ finalUnqualified = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 测量事实是否变化 —— 决定旧 MANUAL 是否失效(§禁止 stale verdict)
|
|
|
+ var measurementChanged = typeChanged || hasSamples || hasQty;
|
|
|
+
|
|
|
+ return new ItemPlan
|
|
|
+ {
|
|
|
+ ItemId = it.ItemId,
|
|
|
+ EffectiveResultType = effective,
|
|
|
+ TypeChanged = typeChanged,
|
|
|
+ Samples = samples,
|
|
|
+ FinalQualifiedQty = finalQualified,
|
|
|
+ FinalUnqualifiedQty = finalUnqualified,
|
|
|
+ ResultRemark = it.ResultRemark,
|
|
|
+ ManualJudgement = it.ManualJudgement,
|
|
|
+ DbItemJudgement = db.ItemJudgement,
|
|
|
+ DbJudgementSource = db.JudgementSource,
|
|
|
+ // 未改测量事实且原本是人工结论 ⇒ 保留原判定(只更备注/审计),不被 AUTO 覆盖
|
|
|
+ KeepExistingVerdict = !measurementChanged && db.JudgementSource == IqcJudgementSource.Manual,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ private sealed class ItemCfgRow
|
|
|
+ {
|
|
|
+ public long Id { get; set; }
|
|
|
+ public string? ResultType { get; set; }
|
|
|
+ public string? UpperLimit { get; set; }
|
|
|
+ public string? LowerLimit { get; set; }
|
|
|
+ public decimal? QualifiedQty { get; set; }
|
|
|
+ public decimal? UnqualifiedQty { get; set; }
|
|
|
+ public string? ItemJudgement { get; set; }
|
|
|
+ public string? JudgementSource { get; set; }
|
|
|
+ }
|
|
|
+
|
|
|
+ private sealed class ItemPlan
|
|
|
+ {
|
|
|
+ public long ItemId { get; set; }
|
|
|
+ public string? EffectiveResultType { get; set; }
|
|
|
+ public bool TypeChanged { get; set; }
|
|
|
+ public List<SamplePlan>? Samples { get; set; }
|
|
|
+ public decimal? FinalQualifiedQty { get; set; }
|
|
|
+ public decimal? FinalUnqualifiedQty { get; set; }
|
|
|
+ public string? ResultRemark { get; set; }
|
|
|
+ public string? ManualJudgement { get; set; }
|
|
|
+ public string? DbItemJudgement { get; set; }
|
|
|
+ public string? DbJudgementSource { get; set; }
|
|
|
+ public bool KeepExistingVerdict { get; set; }
|
|
|
+ }
|
|
|
+
|
|
|
+ private sealed class SamplePlan
|
|
|
+ {
|
|
|
+ public int SampleIndex { get; set; }
|
|
|
+ public string Raw { get; set; } = "";
|
|
|
+ public decimal? Numeric { get; set; }
|
|
|
+ public string Judgement { get; set; } = "";
|
|
|
+ public string? Remark { get; set; }
|
|
|
+ }
|
|
|
+}
|