FqcInspectionService.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  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. // 头:检验单本体(i) + LEFT JOIN 报检 qms_fqcbj(q, by lydjbh=FBILLNO AND id=hid) 补齐报检申请信息
  37. var head = (await _db.Ado.SqlQueryAsync<FqcInspHeadRow>(
  38. @"SELECT i.id AS Id, i.FBILLNO AS BillNo, i.lydjbh AS SourceBillNo, i.wlmc AS MaterialName, i.ggxh AS Spec,
  39. i.scph AS ProductionBatchNo, i.sczld AS ProductionOrderNo, i.jgbh AS SpecNo, i.jgbb AS SpecVersion,
  40. IFNULL(i.pd,'') AS Pd, i.hgsl AS QualifiedQty, i.bhgsl AS UnqualifiedQty, i.FBILLSTATUS AS BillStatus,
  41. i.sczldsl AS OrderQty,
  42. q.wlbm AS MaterialCode, q.sl AS ApplyQty, q.yxj AS Priority, q.jfbh AS FireNo,
  43. q.FAPPLYUSER AS Applicant, q.FAPPLYTIME AS ApplyTime, q.jyfzr AS OwnerId, q.FCOMMENT AS Remark
  44. FROM qms_qcpp_inspbill i
  45. LEFT JOIN qms_fqcbj q ON q.FBILLNO = i.lydjbh AND q.id = i.hid AND q.tenant_id = i.tenant_id
  46. WHERE i.id=@id AND i.tenant_id=@t LIMIT 1",
  47. new SugarParameter("@id", inspBillId), new SugarParameter("@t", tenantId))).FirstOrDefault();
  48. if (head == null) return null;
  49. // 检验负责人门:仅本人(jyfzr==当前账号)可进入录入检验(查看走 FQC检验结果列表的只读详情,不受此限)
  50. if (!string.Equals((head.OwnerId ?? "").Trim(), _userManager.UserId.ToString(), StringComparison.Ordinal))
  51. throw Oops.Oh("检验负责人与当前账号不一致,无法进入录入检验");
  52. // 检验负责人 jyfzr 双语义:认领/调配写 SysUser id(数字,可能跨租户),检验流程提交写姓名字符串。
  53. // 数字 → ClearFilter 反解姓名(解不到回落原值);非数字 → 本就是姓名,直接展示。
  54. if (!string.IsNullOrWhiteSpace(head.OwnerId))
  55. {
  56. if (long.TryParse(head.OwnerId, out var ownerUid))
  57. {
  58. var owner = await _db.Queryable<SysUser>().ClearFilter()
  59. .Where(u => u.Id == ownerUid)
  60. .Select(u => new { u.RealName, u.Account })
  61. .FirstAsync();
  62. head.OwnerName = owner != null ? (owner.RealName ?? owner.Account) : head.OwnerId;
  63. }
  64. else
  65. {
  66. head.OwnerName = head.OwnerId;
  67. }
  68. }
  69. var jSel = string.Join(",", Enumerable.Range(1, SampleCols).Select(i => $"j{i}"));
  70. var raw = await _db.Ado.SqlQueryAsync<dynamic>(
  71. $@"SELECT id, xh, jyxm, IFNULL(lrlx,'') lrlx, IFNULL(sx,'') sx, IFNULL(xx,'') xx, jsyq, jsbz, cyfa, ybl,
  72. IFNULL(pd,'') pd, NonNumericalTypeOK, NonNumericalTypeNG, {jSel}
  73. FROM qms_qcpp_inspbillst WHERE glid=@g AND tenant_id=@t ORDER BY xh, id",
  74. new SugarParameter("@g", inspBillId), new SugarParameter("@t", tenantId));
  75. var lines = new List<Dictionary<string, object?>>();
  76. foreach (var r in raw)
  77. {
  78. var d = (IDictionary<string, object?>)r;
  79. var samples = new List<string?>();
  80. for (var i = 1; i <= SampleCols; i++) samples.Add(d.TryGetValue($"j{i}", out var v) ? v?.ToString() : null);
  81. lines.Add(new Dictionary<string, object?>
  82. {
  83. ["id"] = d["id"], ["xh"] = d["xh"], ["jyxm"] = d["jyxm"], ["lrlx"] = d["lrlx"],
  84. ["sx"] = d["sx"], ["xx"] = d["xx"], ["jsyq"] = d["jsyq"], ["jsbz"] = d["jsbz"], ["cyfa"] = d["cyfa"], ["ybl"] = d["ybl"],
  85. ["pd"] = d["pd"], ["nonNumericalTypeOK"] = d["NonNumericalTypeOK"], ["nonNumericalTypeNG"] = d["NonNumericalTypeNG"],
  86. ["samples"] = samples,
  87. });
  88. }
  89. return new { head, lines };
  90. }
  91. /// <summary>保存录入(草稿,不起流):逐项写 j1..j50 / 非数值合格·不合格数 / 行 pd。</summary>
  92. [DisplayName("保存检验录入")]
  93. [HttpPost("save-detail")]
  94. public async Task<object> SaveDetail([FromBody] FqcSaveDetailInput input)
  95. {
  96. var tenantId = ResolveTenantOrThrow();
  97. if (input == null || input.InspBillId <= 0 || input.Lines == null || input.Lines.Count == 0)
  98. throw Oops.Oh("入参非法");
  99. // 归属校验
  100. var owns = await _db.Ado.GetIntAsync("SELECT COUNT(1) FROM qms_qcpp_inspbill WHERE id=@id AND tenant_id=@t",
  101. new SugarParameter("@id", input.InspBillId), new SugarParameter("@t", tenantId));
  102. if (owns == 0) throw Oops.Oh("检验单不存在或不属于当前租户");
  103. // 检验负责人门:仅本人(jyfzr==当前账号)可保存录入(防绕过 UI 直接调接口)
  104. var saveOwner = await _db.Ado.SqlQuerySingleAsync<string>(
  105. @"SELECT q.jyfzr FROM qms_qcpp_inspbill i
  106. JOIN qms_fqcbj q ON q.FBILLNO=i.lydjbh AND q.id=i.hid AND q.tenant_id=i.tenant_id
  107. WHERE i.id=@id AND i.tenant_id=@t LIMIT 1",
  108. new SugarParameter("@id", input.InspBillId), new SugarParameter("@t", tenantId));
  109. if (!string.Equals((saveOwner ?? "").Trim(), _userManager.UserId.ToString(), StringComparison.Ordinal))
  110. throw Oops.Oh("检验负责人与当前账号不一致,无法录入");
  111. var tran = await _db.Ado.UseTranAsync(async () =>
  112. {
  113. foreach (var ln in input.Lines)
  114. {
  115. var sets = new StringBuilder();
  116. var pars = new List<SugarParameter>();
  117. var samples = ln.Samples ?? new List<string?>();
  118. for (var i = 1; i <= SampleCols; i++)
  119. {
  120. var v = (i <= samples.Count) ? samples[i - 1] : null;
  121. sets.Append($"j{i}=@j{i},");
  122. pars.Add(new SugarParameter($"@j{i}", string.IsNullOrWhiteSpace(v) ? null : v.Trim()));
  123. }
  124. // 录入类型由检验员在录入页当场选择(不依赖检规预配置):落检验单明细快照 lrlx,覆盖检规空值
  125. var lrlx = ln.Lrlx?.Trim();
  126. if (!string.IsNullOrEmpty(lrlx) && lrlx != "NUMERIC" && lrlx != "NON_NUMERIC")
  127. throw Oops.Oh("录入类型仅支持 数值型(NUMERIC)/非数值型(NON_NUMERIC)");
  128. sets.Append("NonNumericalTypeOK=@ok, NonNumericalTypeNG=@ng, ybl=@ybl, pd=@pd, lrlx=@lrlx");
  129. pars.Add(new SugarParameter("@ok", (object?)ln.NonNumericalTypeOK ?? DBNull.Value));
  130. pars.Add(new SugarParameter("@ng", (object?)ln.NonNumericalTypeNG ?? DBNull.Value));
  131. pars.Add(new SugarParameter("@ybl", (object?)Nz(ln.Ybl) ?? DBNull.Value));
  132. pars.Add(new SugarParameter("@pd", (object?)Nz(ln.Pd) ?? DBNull.Value));
  133. pars.Add(new SugarParameter("@lrlx", string.IsNullOrEmpty(lrlx) ? (object)DBNull.Value : lrlx));
  134. pars.Add(new SugarParameter("@id", ln.Id));
  135. pars.Add(new SugarParameter("@g", input.InspBillId));
  136. pars.Add(new SugarParameter("@t", tenantId));
  137. await _db.Ado.ExecuteCommandAsync(
  138. $"UPDATE qms_qcpp_inspbillst SET {sets} WHERE id=@id AND glid=@g AND tenant_id=@t", pars);
  139. }
  140. });
  141. if (!tran.IsSuccess) throw tran.ErrorException ?? Oops.Oh("保存失败");
  142. return new { ok = true, saved = input.Lines.Count };
  143. }
  144. /// <summary>
  145. /// 自动判定:只读 inspbillst 快照(sx/xx/lrlx)。NUMERIC 且有上/下限 → 逐样本 xx≤v≤sx 判行 pd(合格/不合格);
  146. /// NON_NUMERIC 或无上下限规则 → 跳过(pending),pd 留人工。局部失败不整单失败。
  147. /// </summary>
  148. [DisplayName("自动判定")]
  149. [HttpPost("auto-judge")]
  150. public async Task<object> AutoJudge([FromBody] FqcAutoJudgeInput input)
  151. {
  152. var tenantId = ResolveTenantOrThrow();
  153. if (input == null || input.InspBillId <= 0) throw Oops.Oh("检验单 id 非法");
  154. // 负责人门:仅本人(jyfzr==当前账号)可自动判定(独立公开写口,与 SaveDetail 一致,防越权改他人检验结果)
  155. var owner = await _db.Ado.SqlQuerySingleAsync<string>(
  156. @"SELECT q.jyfzr FROM qms_qcpp_inspbill i
  157. JOIN qms_fqcbj q ON q.FBILLNO=i.lydjbh AND q.id=i.hid AND q.tenant_id=i.tenant_id
  158. WHERE i.id=@id AND i.tenant_id=@t LIMIT 1",
  159. new SugarParameter("@id", input.InspBillId), new SugarParameter("@t", tenantId));
  160. if (!string.Equals((owner ?? "").Trim(), _userManager.UserId.ToString(), StringComparison.Ordinal))
  161. throw Oops.Oh("检验负责人与当前账号不一致,无法自动判定");
  162. var jSel = string.Join(",", Enumerable.Range(1, SampleCols).Select(i => $"j{i}"));
  163. var raw = await _db.Ado.SqlQueryAsync<dynamic>(
  164. $"SELECT id, jyxm, IFNULL(lrlx,'') lrlx, IFNULL(sx,'') sx, IFNULL(xx,'') xx, {jSel} FROM qms_qcpp_inspbillst WHERE glid=@g AND tenant_id=@t",
  165. new SugarParameter("@g", input.InspBillId), new SugarParameter("@t", tenantId));
  166. if (!raw.Any()) throw Oops.Oh("检验单无明细");
  167. int judged = 0, pending = 0;
  168. var items = new List<object>();
  169. var tran = await _db.Ado.UseTranAsync(async () =>
  170. {
  171. foreach (var r in raw)
  172. {
  173. var d = (IDictionary<string, object?>)r;
  174. var lrlx = d["lrlx"]?.ToString();
  175. var sxStr = d["sx"]?.ToString(); var xxStr = d["xx"]?.ToString();
  176. var hasUp = decimal.TryParse(sxStr, out var up);
  177. var hasLo = decimal.TryParse(xxStr, out var lo);
  178. // 硬边界②③:非数值 / 无数值规则 → 不自动判,pd 留人工
  179. if (lrlx != "NUMERIC" || (!hasUp && !hasLo))
  180. {
  181. pending++;
  182. items.Add(new { id = d["id"], jyxm = d["jyxm"]?.ToString(), result = "PENDING", reason = "无结构化数值判定条件,请人工确认" });
  183. continue;
  184. }
  185. var vals = new List<decimal>();
  186. var anyEntered = false;
  187. for (var i = 1; i <= SampleCols; i++)
  188. {
  189. var s = d.TryGetValue($"j{i}", out var v) ? v?.ToString() : null;
  190. if (string.IsNullOrWhiteSpace(s)) continue;
  191. anyEntered = true;
  192. if (decimal.TryParse(s.Trim(), out var num)) vals.Add(num);
  193. }
  194. if (!anyEntered) { pending++; items.Add(new { id = d["id"], jyxm = d["jyxm"]?.ToString(), result = "PENDING", reason = "尚未录入样本值" }); continue; }
  195. var pass = vals.All(v => (!hasLo || v >= lo) && (!hasUp || v <= up));
  196. var pd = pass ? "合格" : "不合格";
  197. await _db.Ado.ExecuteCommandAsync("UPDATE qms_qcpp_inspbillst SET pd=@pd WHERE id=@id AND glid=@g AND tenant_id=@t",
  198. new SugarParameter("@pd", pd), new SugarParameter("@id", d["id"]), new SugarParameter("@g", input.InspBillId), new SugarParameter("@t", tenantId));
  199. judged++;
  200. items.Add(new { id = d["id"], jyxm = d["jyxm"]?.ToString(), result = pd });
  201. }
  202. });
  203. if (!tran.IsSuccess) throw tran.ErrorException ?? Oops.Oh("自动判定失败");
  204. return new { judged, pending, items, message = pending > 0 ? $"已自动判定 {judged} 项,{pending} 项缺结构化判定条件请人工确认" : $"已自动判定 {judged} 项" };
  205. }
  206. private static string? Nz(string? s) => string.IsNullOrWhiteSpace(s) ? null : s.Trim();
  207. }
  208. public class FqcInspHeadRow
  209. {
  210. public long Id { get; set; }
  211. public string? BillNo { get; set; }
  212. public string? SourceBillNo { get; set; }
  213. public string? MaterialName { get; set; }
  214. public string? Spec { get; set; }
  215. public string? ProductionBatchNo { get; set; }
  216. public string? ProductionOrderNo { get; set; }
  217. public string? SpecNo { get; set; }
  218. public string? SpecVersion { get; set; }
  219. public string? Pd { get; set; }
  220. public decimal? QualifiedQty { get; set; }
  221. public decimal? UnqualifiedQty { get; set; }
  222. public string? BillStatus { get; set; }
  223. // ── 报检申请信息(LEFT JOIN qms_fqcbj)──
  224. public decimal? OrderQty { get; set; }
  225. public string? MaterialCode { get; set; }
  226. public decimal? ApplyQty { get; set; }
  227. public string? Priority { get; set; }
  228. public string? FireNo { get; set; }
  229. public string? Applicant { get; set; }
  230. public string? ApplyTime { get; set; }
  231. public string? OwnerId { get; set; }
  232. public string? OwnerName { get; set; }
  233. public string? Remark { get; set; }
  234. }
  235. public class FqcSaveDetailInput
  236. {
  237. public long InspBillId { get; set; }
  238. public List<FqcSaveDetailLine> Lines { get; set; } = new();
  239. }
  240. public class FqcSaveDetailLine
  241. {
  242. public long Id { get; set; }
  243. /// <summary>录入类型(检验员在录入页当场选择):NUMERIC 数值型 / NON_NUMERIC 非数值型 / 空=未定</summary>
  244. public string? Lrlx { get; set; }
  245. public List<string?>? Samples { get; set; }
  246. public long? NonNumericalTypeOK { get; set; }
  247. public long? NonNumericalTypeNG { get; set; }
  248. public string? Ybl { get; set; }
  249. public string? Pd { get; set; }
  250. }
  251. public class FqcAutoJudgeInput
  252. {
  253. public long InspBillId { get; set; }
  254. }