FqcInspectionService.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. using System.Text;
  2. using Admin.NET.Plugin.AiDOP.Infrastructure;
  3. namespace Admin.NET.Plugin.AiDOP.FinishedWarehouse;
  4. /// <summary>
  5. /// S7 FQC 检验录入/自动判定(Phase 1E)。读写"活"检验单 qms_qcpp_inspbill / qms_qcpp_inspbillst。
  6. ///
  7. /// NUMERIC 项目:逐样本录 j1..j50,自动判定按快照 sx/xx 判每个样本(xx≤v≤sx);
  8. /// NON_NUMERIC 项目:录本次实际合格数(NonNumericalTypeOK)/不合格数(NonNumericalTypeNG),无 Ac/Re 规则不自动判 → pd 人工确认。
  9. ///
  10. /// 硬边界:① 自动判定只读 inspbillst 快照(sx/xx/lrlx),绝不回查实时检规 qms_ccjygfzb;
  11. /// ② NON_NUMERIC 无接收规则时不默认 NG>0=FAIL,pd 留人工;③ 保存不起流(仅本 service),
  12. /// 首次正式提交走现有 FqcInspBillFlow.submit-result 惰性 StartFlow。tenant 全程 ResolveTenantOrThrow。
  13. /// 本 service 不做提交/审核/处置(提交复用现有流程端点)。
  14. /// </summary>
  15. [ApiDescriptionSettings(Order = 343, Description = "FQC检验录入")]
  16. [Route("api/S7FqcInspection")]
  17. [NonUnify]
  18. public class FqcInspectionService : IDynamicApiController, ITransient
  19. {
  20. private readonly ISqlSugarClient _db;
  21. private readonly UserManager _userManager;
  22. public FqcInspectionService(ISqlSugarClient db, UserManager userManager)
  23. {
  24. _db = db;
  25. _userManager = userManager;
  26. }
  27. private long ResolveTenantOrThrow() => AidopTenantScope.ResolveOrThrow(_userManager);
  28. private const int SampleCols = 50; // j1..j50
  29. /// <summary>检验单录入详情(活单 qms_qcpp_inspbill 头 + qms_qcpp_inspbillst 明细,含快照与已录入值)。</summary>
  30. [DisplayName("检验单录入详情")]
  31. [HttpGet("detail")]
  32. public async Task<object?> Detail([FromQuery] long inspBillId)
  33. {
  34. if (inspBillId <= 0) return null;
  35. var tenantId = ResolveTenantOrThrow();
  36. var head = (await _db.Ado.SqlQueryAsync<FqcInspHeadRow>(
  37. @"SELECT id AS Id, FBILLNO AS BillNo, lydjbh AS SourceBillNo, wlmc AS MaterialName, ggxh AS Spec,
  38. scph AS ProductionBatchNo, sczld AS ProductionOrderNo, jgbh AS SpecNo, jgbb AS SpecVersion,
  39. IFNULL(pd,'') AS Pd, hgsl AS QualifiedQty, bhgsl AS UnqualifiedQty, FBILLSTATUS AS BillStatus
  40. FROM qms_qcpp_inspbill WHERE id=@id AND tenant_id=@t LIMIT 1",
  41. new SugarParameter("@id", inspBillId), new SugarParameter("@t", tenantId))).FirstOrDefault();
  42. if (head == null) return null;
  43. var jSel = string.Join(",", Enumerable.Range(1, SampleCols).Select(i => $"j{i}"));
  44. var raw = await _db.Ado.SqlQueryAsync<dynamic>(
  45. $@"SELECT id, xh, jyxm, IFNULL(lrlx,'') lrlx, IFNULL(sx,'') sx, IFNULL(xx,'') xx, jsyq, jsbz, cyfa, ybl,
  46. IFNULL(pd,'') pd, NonNumericalTypeOK, NonNumericalTypeNG, {jSel}
  47. FROM qms_qcpp_inspbillst WHERE glid=@g AND tenant_id=@t ORDER BY xh, id",
  48. new SugarParameter("@g", inspBillId), new SugarParameter("@t", tenantId));
  49. var lines = new List<Dictionary<string, object?>>();
  50. foreach (var r in raw)
  51. {
  52. var d = (IDictionary<string, object?>)r;
  53. var samples = new List<string?>();
  54. for (var i = 1; i <= SampleCols; i++) samples.Add(d.TryGetValue($"j{i}", out var v) ? v?.ToString() : null);
  55. lines.Add(new Dictionary<string, object?>
  56. {
  57. ["id"] = d["id"], ["xh"] = d["xh"], ["jyxm"] = d["jyxm"], ["lrlx"] = d["lrlx"],
  58. ["sx"] = d["sx"], ["xx"] = d["xx"], ["jsyq"] = d["jsyq"], ["jsbz"] = d["jsbz"], ["cyfa"] = d["cyfa"], ["ybl"] = d["ybl"],
  59. ["pd"] = d["pd"], ["nonNumericalTypeOK"] = d["NonNumericalTypeOK"], ["nonNumericalTypeNG"] = d["NonNumericalTypeNG"],
  60. ["samples"] = samples,
  61. });
  62. }
  63. return new { head, lines };
  64. }
  65. /// <summary>保存录入(草稿,不起流):逐项写 j1..j50 / 非数值合格·不合格数 / 行 pd。</summary>
  66. [DisplayName("保存检验录入")]
  67. [HttpPost("save-detail")]
  68. public async Task<object> SaveDetail([FromBody] FqcSaveDetailInput input)
  69. {
  70. var tenantId = ResolveTenantOrThrow();
  71. if (input == null || input.InspBillId <= 0 || input.Lines == null || input.Lines.Count == 0)
  72. throw Oops.Oh("入参非法");
  73. // 归属校验
  74. var owns = await _db.Ado.GetIntAsync("SELECT COUNT(1) FROM qms_qcpp_inspbill WHERE id=@id AND tenant_id=@t",
  75. new SugarParameter("@id", input.InspBillId), new SugarParameter("@t", tenantId));
  76. if (owns == 0) throw Oops.Oh("检验单不存在或不属于当前租户");
  77. var tran = await _db.Ado.UseTranAsync(async () =>
  78. {
  79. foreach (var ln in input.Lines)
  80. {
  81. var sets = new StringBuilder();
  82. var pars = new List<SugarParameter>();
  83. var samples = ln.Samples ?? new List<string?>();
  84. for (var i = 1; i <= SampleCols; i++)
  85. {
  86. var v = (i <= samples.Count) ? samples[i - 1] : null;
  87. sets.Append($"j{i}=@j{i},");
  88. pars.Add(new SugarParameter($"@j{i}", string.IsNullOrWhiteSpace(v) ? null : v.Trim()));
  89. }
  90. // 录入类型由检验员在录入页当场选择(不依赖检规预配置):落检验单明细快照 lrlx,覆盖检规空值
  91. var lrlx = ln.Lrlx?.Trim();
  92. if (!string.IsNullOrEmpty(lrlx) && lrlx != "NUMERIC" && lrlx != "NON_NUMERIC")
  93. throw Oops.Oh("录入类型仅支持 数值型(NUMERIC)/非数值型(NON_NUMERIC)");
  94. sets.Append("NonNumericalTypeOK=@ok, NonNumericalTypeNG=@ng, ybl=@ybl, pd=@pd, lrlx=@lrlx");
  95. pars.Add(new SugarParameter("@ok", (object?)ln.NonNumericalTypeOK ?? DBNull.Value));
  96. pars.Add(new SugarParameter("@ng", (object?)ln.NonNumericalTypeNG ?? DBNull.Value));
  97. pars.Add(new SugarParameter("@ybl", (object?)Nz(ln.Ybl) ?? DBNull.Value));
  98. pars.Add(new SugarParameter("@pd", (object?)Nz(ln.Pd) ?? DBNull.Value));
  99. pars.Add(new SugarParameter("@lrlx", string.IsNullOrEmpty(lrlx) ? (object)DBNull.Value : lrlx));
  100. pars.Add(new SugarParameter("@id", ln.Id));
  101. pars.Add(new SugarParameter("@g", input.InspBillId));
  102. pars.Add(new SugarParameter("@t", tenantId));
  103. await _db.Ado.ExecuteCommandAsync(
  104. $"UPDATE qms_qcpp_inspbillst SET {sets} WHERE id=@id AND glid=@g AND tenant_id=@t", pars);
  105. }
  106. });
  107. if (!tran.IsSuccess) throw tran.ErrorException ?? Oops.Oh("保存失败");
  108. return new { ok = true, saved = input.Lines.Count };
  109. }
  110. /// <summary>
  111. /// 自动判定:只读 inspbillst 快照(sx/xx/lrlx)。NUMERIC 且有上/下限 → 逐样本 xx≤v≤sx 判行 pd(合格/不合格);
  112. /// NON_NUMERIC 或无上下限规则 → 跳过(pending),pd 留人工。局部失败不整单失败。
  113. /// </summary>
  114. [DisplayName("自动判定")]
  115. [HttpPost("auto-judge")]
  116. public async Task<object> AutoJudge([FromBody] FqcAutoJudgeInput input)
  117. {
  118. var tenantId = ResolveTenantOrThrow();
  119. if (input == null || input.InspBillId <= 0) throw Oops.Oh("检验单 id 非法");
  120. var jSel = string.Join(",", Enumerable.Range(1, SampleCols).Select(i => $"j{i}"));
  121. var raw = await _db.Ado.SqlQueryAsync<dynamic>(
  122. $"SELECT id, jyxm, IFNULL(lrlx,'') lrlx, IFNULL(sx,'') sx, IFNULL(xx,'') xx, {jSel} FROM qms_qcpp_inspbillst WHERE glid=@g AND tenant_id=@t",
  123. new SugarParameter("@g", input.InspBillId), new SugarParameter("@t", tenantId));
  124. if (!raw.Any()) throw Oops.Oh("检验单无明细");
  125. int judged = 0, pending = 0;
  126. var items = new List<object>();
  127. var tran = await _db.Ado.UseTranAsync(async () =>
  128. {
  129. foreach (var r in raw)
  130. {
  131. var d = (IDictionary<string, object?>)r;
  132. var lrlx = d["lrlx"]?.ToString();
  133. var sxStr = d["sx"]?.ToString(); var xxStr = d["xx"]?.ToString();
  134. var hasUp = decimal.TryParse(sxStr, out var up);
  135. var hasLo = decimal.TryParse(xxStr, out var lo);
  136. // 硬边界②③:非数值 / 无数值规则 → 不自动判,pd 留人工
  137. if (lrlx != "NUMERIC" || (!hasUp && !hasLo))
  138. {
  139. pending++;
  140. items.Add(new { id = d["id"], jyxm = d["jyxm"]?.ToString(), result = "PENDING", reason = "无结构化数值判定条件,请人工确认" });
  141. continue;
  142. }
  143. var vals = new List<decimal>();
  144. var anyEntered = false;
  145. for (var i = 1; i <= SampleCols; i++)
  146. {
  147. var s = d.TryGetValue($"j{i}", out var v) ? v?.ToString() : null;
  148. if (string.IsNullOrWhiteSpace(s)) continue;
  149. anyEntered = true;
  150. if (decimal.TryParse(s.Trim(), out var num)) vals.Add(num);
  151. }
  152. if (!anyEntered) { pending++; items.Add(new { id = d["id"], jyxm = d["jyxm"]?.ToString(), result = "PENDING", reason = "尚未录入样本值" }); continue; }
  153. var pass = vals.All(v => (!hasLo || v >= lo) && (!hasUp || v <= up));
  154. var pd = pass ? "合格" : "不合格";
  155. await _db.Ado.ExecuteCommandAsync("UPDATE qms_qcpp_inspbillst SET pd=@pd WHERE id=@id AND glid=@g AND tenant_id=@t",
  156. new SugarParameter("@pd", pd), new SugarParameter("@id", d["id"]), new SugarParameter("@g", input.InspBillId), new SugarParameter("@t", tenantId));
  157. judged++;
  158. items.Add(new { id = d["id"], jyxm = d["jyxm"]?.ToString(), result = pd });
  159. }
  160. });
  161. if (!tran.IsSuccess) throw tran.ErrorException ?? Oops.Oh("自动判定失败");
  162. return new { judged, pending, items, message = pending > 0 ? $"已自动判定 {judged} 项,{pending} 项缺结构化判定条件请人工确认" : $"已自动判定 {judged} 项" };
  163. }
  164. private static string? Nz(string? s) => string.IsNullOrWhiteSpace(s) ? null : s.Trim();
  165. }
  166. public class FqcInspHeadRow
  167. {
  168. public long Id { get; set; }
  169. public string? BillNo { get; set; }
  170. public string? SourceBillNo { get; set; }
  171. public string? MaterialName { get; set; }
  172. public string? Spec { get; set; }
  173. public string? ProductionBatchNo { get; set; }
  174. public string? ProductionOrderNo { get; set; }
  175. public string? SpecNo { get; set; }
  176. public string? SpecVersion { get; set; }
  177. public string? Pd { get; set; }
  178. public decimal? QualifiedQty { get; set; }
  179. public decimal? UnqualifiedQty { get; set; }
  180. public string? BillStatus { get; set; }
  181. }
  182. public class FqcSaveDetailInput
  183. {
  184. public long InspBillId { get; set; }
  185. public List<FqcSaveDetailLine> Lines { get; set; } = new();
  186. }
  187. public class FqcSaveDetailLine
  188. {
  189. public long Id { get; set; }
  190. /// <summary>录入类型(检验员在录入页当场选择):NUMERIC 数值型 / NON_NUMERIC 非数值型 / 空=未定</summary>
  191. public string? Lrlx { get; set; }
  192. public List<string?>? Samples { get; set; }
  193. public long? NonNumericalTypeOK { get; set; }
  194. public long? NonNumericalTypeNG { get; set; }
  195. public string? Ybl { get; set; }
  196. public string? Pd { get; set; }
  197. }
  198. public class FqcAutoJudgeInput
  199. {
  200. public long InspBillId { get; set; }
  201. }