S6ProcessInspectionService.cs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. using Admin.NET.Plugin.AiDOP.Manufacturing.Dto;
  2. namespace Admin.NET.Plugin.AiDOP.Manufacturing;
  3. /// <summary>
  4. /// S6 过程检验单 服务(Phase 1B):生产指令 → 自动唯一解析过程检规 → 生成检验单 + 检规快照 → 只读详情。
  5. ///
  6. /// prepare:入参仅 workOrderNo。后端自解析 CurrentTenant(AidopTenantScope)+ 加载生产指令(mdp_std_work_order_schedule)
  7. /// → S6ProcessSpecResolver 按 (tenant, item_code, order_date) 唯一解析。
  8. /// 仅 MATCHED 才幂等生成/复用检验单 + 冻结检规快照(同一事务);NO_MATCH/AMBIGUOUS/INVALID 一律不写库、原样返回状态。
  9. /// 无人工检规选择;全程 tenant 显式过滤;快照生成后详情只读快照,不再回查 S0。
  10. /// FACTORY_SCOPE = PARTIAL(S0 检规无 factory_id,安全边界为 Tenant;factory 仅随单据承载不做过滤)。
  11. /// </summary>
  12. [ApiDescriptionSettings(Order = 304, Description = "过程检验单")]
  13. [Route("api/S6ProcessInspection")]
  14. [NonUnify]
  15. public class S6ProcessInspectionService : IDynamicApiController, ITransient
  16. {
  17. private readonly ISqlSugarClient _db;
  18. private readonly UserManager _userManager;
  19. private readonly S6ProcessSpecResolver _resolver;
  20. public S6ProcessInspectionService(ISqlSugarClient db, UserManager userManager, S6ProcessSpecResolver resolver)
  21. {
  22. _db = db;
  23. _userManager = userManager;
  24. _resolver = resolver;
  25. }
  26. private long ResolveTenantOrThrow() => Infrastructure.AidopTenantScope.ResolveOrThrow(_userManager);
  27. /// <summary>
  28. /// 准备过程检验单:解析检规,MATCHED 则幂等生成/复用检验单 + 快照,返回 billId + status。
  29. /// </summary>
  30. [DisplayName("准备过程检验单")]
  31. [HttpPost("prepare")]
  32. public async Task<S6ProcessInspectionPrepareResult> Prepare([FromBody] S6ProcessInspectionPrepareInput input)
  33. {
  34. var res = new S6ProcessInspectionPrepareResult();
  35. if (input == null || string.IsNullOrWhiteSpace(input.WorkOrderNo))
  36. {
  37. res.Status = S6SpecResolveStatus.NoMatch;
  38. res.Message = "缺少工单编号。";
  39. return res;
  40. }
  41. var tid = ResolveTenantOrThrow();
  42. var wo = input.WorkOrderNo.Trim();
  43. // 加载生产指令(严格租户;跨租户/不存在 → 明确拒绝,不泄漏)
  44. var pi = await _db.Ado.SqlQuerySingleAsync<ProdInstrRow>(
  45. """
  46. SELECT work_order AS WorkOrder, lot_serial AS LotSerial, item_code AS ItemCode, item_name AS ItemName,
  47. specification AS Specification, factory_id AS FactoryId, order_date AS OrderDate
  48. FROM mdp_std_work_order_schedule
  49. WHERE tenant_id=@TenantId AND work_order=@Wo AND doc_type='PROD_TASK' AND IFNULL(source_system,'') <> 'T8'
  50. LIMIT 1
  51. """,
  52. new List<SugarParameter> { new("@TenantId", tid), new("@Wo", wo) });
  53. if (pi == null)
  54. {
  55. res.Status = S6SpecResolveStatus.NoMatch;
  56. res.Message = "未找到该工单的生产指令。";
  57. return res;
  58. }
  59. // ReferenceDate 取检验时点(now):过程检验在当前执行,应采用"检验时点当前有效"的检规;
  60. // 生产指令 OrderDate 是下单时点,可能早于检规生效日(sxrj),用它会漏掉当前有效检规。(§9 明确说明)
  61. var refDate = DateTime.Now;
  62. var resolve = await _resolver.ResolveAsync(tid, pi.ItemCode, refDate);
  63. res.Status = resolve.Status;
  64. res.Message = resolve.Message;
  65. res.SpecCode = resolve.SpecCode;
  66. res.SpecVersion = resolve.SpecVersion;
  67. res.CandidateCount = resolve.CandidateCount;
  68. if (resolve.Status != S6SpecResolveStatus.Matched || resolve.CandidateCount != 1 || resolve.MatchedSpecId is not { } specId)
  69. return res; // 非唯一解析:不写库
  70. // 幂等:已有 active(DRAFT/ACTIVE) 检验单 → 复用
  71. var existingId = await _db.Ado.SqlQuerySingleAsync<long?>(
  72. """
  73. SELECT id FROM ado_s6_process_inspection_bill
  74. WHERE tenant_id=@TenantId AND work_order_no=@Wo AND inspection_status IN ('DRAFT','ACTIVE')
  75. ORDER BY id DESC LIMIT 1
  76. """,
  77. new List<SugarParameter> { new("@TenantId", tid), new("@Wo", wo) });
  78. if (existingId is { } eid && eid > 0)
  79. {
  80. res.BillId = eid;
  81. res.Reused = true;
  82. return res;
  83. }
  84. // 生成:检验单 + 快照头 + 快照明细(同一事务,任一失败全回滚)
  85. var uid = _userManager.UserId;
  86. var uname = _userManager.RealName ?? _userManager.Account;
  87. try
  88. {
  89. _db.Ado.BeginTran();
  90. await _db.Ado.ExecuteCommandAsync(
  91. """
  92. INSERT INTO ado_s6_process_inspection_bill
  93. (tenant_id, factory_id, work_order_no, lot_serial, item_code, item_name, specification,
  94. spec_id, spec_code, spec_version, inspection_status, created_by, created_by_name)
  95. VALUES (@TenantId, @FactoryId, @Wo, @Lot, @ItemCode, @ItemName, @Spec,
  96. @SpecId, @SpecCode, @SpecVersion, 'DRAFT', @Uid, @Uname)
  97. """,
  98. new List<SugarParameter>
  99. {
  100. new("@TenantId", tid),
  101. new("@FactoryId", (object?)pi.FactoryId ?? DBNull.Value),
  102. new("@Wo", wo),
  103. new("@Lot", (object?)pi.LotSerial ?? DBNull.Value),
  104. new("@ItemCode", (object?)pi.ItemCode ?? DBNull.Value),
  105. new("@ItemName", (object?)pi.ItemName ?? DBNull.Value),
  106. new("@Spec", (object?)pi.Specification ?? DBNull.Value),
  107. new("@SpecId", specId),
  108. new("@SpecCode", (object?)resolve.SpecCode ?? DBNull.Value),
  109. new("@SpecVersion", (object?)resolve.SpecVersion ?? DBNull.Value),
  110. new("@Uid", (object?)uid ?? DBNull.Value),
  111. new("@Uname", (object?)uname ?? DBNull.Value),
  112. });
  113. var billId = await _db.Ado.GetLongAsync("SELECT LAST_INSERT_ID()");
  114. await _db.Ado.ExecuteCommandAsync(
  115. """
  116. INSERT INTO ado_s6_process_inspection_snapshot_head
  117. (tenant_id, bill_id, spec_id, spec_code, spec_version, effective_date, item_code, specification, source_tenant_id)
  118. VALUES (@TenantId, @BillId, @SpecId, @SpecCode, @SpecVersion, @Eff, @ItemCode, @Spec, @TenantId)
  119. """,
  120. new List<SugarParameter>
  121. {
  122. new("@TenantId", tid),
  123. new("@BillId", billId),
  124. new("@SpecId", specId),
  125. new("@SpecCode", (object?)resolve.SpecCode ?? DBNull.Value),
  126. new("@SpecVersion", (object?)resolve.SpecVersion ?? DBNull.Value),
  127. new("@Eff", (object?)resolve.EffectiveDate ?? DBNull.Value),
  128. new("@ItemCode", (object?)pi.ItemCode ?? DBNull.Value),
  129. new("@Spec", (object?)pi.Specification ?? DBNull.Value),
  130. });
  131. var lineCount = await _db.Ado.ExecuteCommandAsync(
  132. """
  133. INSERT INTO ado_s6_process_inspection_snapshot_line
  134. (tenant_id, bill_id, source_line_id, inspection_item, process_code, process_name, method,
  135. spec_value, tech_standard, lower_limit, upper_limit, inspection_frequency, result_type, sort_no, sample_count)
  136. SELECT @TenantId, @BillId, z.id, z.jyxm, z.gxdh, z.gxmc, z.jyff,
  137. z.jygg, z.jsbz, z.xx, z.sx, z.jypc, z.lrlx, NULL, z.sample_count
  138. FROM qms_gcjygfzb z
  139. WHERE z.tenant_id=@TenantId AND z.glid=@SpecId
  140. """,
  141. new List<SugarParameter> { new("@TenantId", tid), new("@BillId", billId), new("@SpecId", specId) });
  142. if (lineCount <= 0)
  143. throw Oops.Oh("过程检验规范无有效明细,无法生成检验单。");
  144. _db.Ado.CommitTran();
  145. res.BillId = billId;
  146. res.Reused = false;
  147. return res;
  148. }
  149. catch
  150. {
  151. _db.Ado.RollbackTran();
  152. throw;
  153. }
  154. }
  155. /// <summary>过程检验单只读详情(单据 + 快照头 + 快照明细)。跨租户/不存在返回 null。</summary>
  156. [DisplayName("过程检验单详情")]
  157. [HttpGet("detail")]
  158. public async Task<S6ProcessInspectionDetailDto?> GetDetail([FromQuery] long billId)
  159. {
  160. if (billId <= 0) return null;
  161. var tid = ResolveTenantOrThrow();
  162. var pars = new List<SugarParameter> { new("@TenantId", tid), new("@BillId", billId) };
  163. var dto = await _db.Ado.SqlQuerySingleAsync<S6ProcessInspectionDetailDto>(
  164. """
  165. SELECT b.id AS BillId, b.work_order_no AS WorkOrderNo, b.lot_serial AS LotSerial, b.item_code AS ItemCode,
  166. b.item_name AS ItemName, b.specification AS Specification, b.spec_id AS SpecId, b.spec_code AS SpecCode,
  167. b.spec_version AS SpecVersion, h.effective_date AS EffectiveDate, b.inspection_status AS InspectionStatus,
  168. b.created_by_name AS CreatedByName, b.create_time AS CreateTime
  169. FROM ado_s6_process_inspection_bill b
  170. LEFT JOIN ado_s6_process_inspection_snapshot_head h ON h.bill_id=b.id AND h.tenant_id=@TenantId
  171. WHERE b.tenant_id=@TenantId AND b.id=@BillId LIMIT 1
  172. """, pars);
  173. if (dto == null) return null;
  174. dto.Lines = await _db.Ado.SqlQueryAsync<S6ProcessInspectionSnapshotLineDto>(
  175. """
  176. SELECT id AS Id, inspection_item AS InspectionItem, process_code AS ProcessCode, process_name AS ProcessName,
  177. method AS Method, spec_value AS SpecValue, tech_standard AS TechStandard, lower_limit AS LowerLimit,
  178. upper_limit AS UpperLimit, inspection_frequency AS InspectionFrequency, result_type AS ResultType,
  179. sort_no AS SortNo, sample_count AS SampleCount
  180. FROM ado_s6_process_inspection_snapshot_line
  181. WHERE tenant_id=@TenantId AND bill_id=@BillId ORDER BY id
  182. """, pars);
  183. // 已录样本 + 后端计算的单项/整单判定(刷新可恢复)
  184. var samples = await LoadResultItemsAsync(tid, billId);
  185. var byLine = samples.GroupBy(s => s.SnapshotLineId).ToDictionary(g => g.Key, g => g.OrderBy(x => x.SampleIndex).ToList());
  186. var itemAgg = new List<(string, string)>();
  187. foreach (var line in dto.Lines)
  188. {
  189. var recorded = byLine.TryGetValue(line.Id, out var list) ? list : new List<ResultItemRow>();
  190. line.Samples = recorded.Select(r => new S6ProcessInspectionResultSampleDto
  191. {
  192. SampleIndex = r.SampleIndex,
  193. ActualNumeric = r.ActualNumeric,
  194. ActualNonNumeric = r.ActualNonNumeric,
  195. Judgement = r.Judgement,
  196. Remark = r.Remark
  197. }).ToList();
  198. var (status, result) = S6ProcessInspectionJudge.AggregateItem(line.SampleCount ?? 1, recorded.Select(r => r.Judgement ?? "").ToList());
  199. line.ItemStatus = status;
  200. line.ItemResult = result;
  201. itemAgg.Add((status, result));
  202. }
  203. var head = await _db.Ado.SqlQuerySingleAsync<ResultHeadRow>(
  204. "SELECT status AS Status, overall_result AS OverallResult FROM ado_s6_process_inspection_result WHERE tenant_id=@TenantId AND bill_id=@BillId LIMIT 1", pars);
  205. dto.OverallStatus = head?.Status ?? S6ItemStatus.NotStarted;
  206. dto.OverallResult = head?.OverallResult ?? S6ProcessInspectionJudge.AggregateOverall(itemAgg);
  207. return dto;
  208. }
  209. /// <summary>
  210. /// 保存检验结果(草稿,可部分)。逐样本 upsert(幂等 UK bill+line+sample),后端按快照自动判定,
  211. /// 重算单项/整单结果,结果头置 IN_PROGRESS;同一事务。前端传的任何判定/租户/限值一律忽略。
  212. /// </summary>
  213. [DisplayName("保存过程检验结果")]
  214. [HttpPost("saveResult")]
  215. public async Task<S6SaveResultOutput> SaveResult([FromBody] S6SaveResultInput input)
  216. {
  217. if (input == null || input.BillId <= 0)
  218. return new S6SaveResultOutput { Ok = false, Message = "缺少检验单。" };
  219. var tid = ResolveTenantOrThrow();
  220. var cfg = await LoadSnapshotConfigAsync(tid, input.BillId);
  221. if (cfg.Count == 0)
  222. return new S6SaveResultOutput { Ok = false, Message = "检验单不存在或无检规快照。" };
  223. var cfgById = cfg.ToDictionary(c => c.Id);
  224. var uid = _userManager.UserId;
  225. var uname = _userManager.RealName ?? _userManager.Account;
  226. var now = DateTime.Now;
  227. try
  228. {
  229. _db.Ado.BeginTran();
  230. var resultId = await EnsureResultHeaderAsync(tid, input.BillId, uid, uname, now);
  231. foreach (var s in input.Samples ?? new List<S6SaveResultSample>())
  232. {
  233. if (!cfgById.TryGetValue(s.SnapshotLineId, out var line)) continue; // 只认本单快照行
  234. var required = line.SampleCount is > 0 ? line.SampleCount!.Value : 1;
  235. if (s.SampleIndex < 1 || s.SampleIndex > required) continue; // 样本序号越界忽略
  236. var clear = string.IsNullOrWhiteSpace(s.ActualValue);
  237. var delPars = new List<SugarParameter> { new("@t", tid), new("@bill", input.BillId), new("@line", s.SnapshotLineId), new("@idx", s.SampleIndex) };
  238. if (clear)
  239. {
  240. await _db.Ado.ExecuteCommandAsync(
  241. "DELETE FROM ado_s6_process_inspection_result_item WHERE tenant_id=@t AND bill_id=@bill AND snapshot_line_id=@line AND sample_index=@idx", delPars);
  242. continue;
  243. }
  244. var judge = S6ProcessInspectionJudge.JudgeSample(line.ResultType, s.ActualValue, line.LowerLimit, line.UpperLimit);
  245. var isNumeric = string.Equals((line.ResultType ?? "").Trim(), S6ResultType.Numeric, StringComparison.OrdinalIgnoreCase);
  246. object numVal = isNumeric && S6ProcessInspectionJudge.ParseNumeric(s.ActualValue) is { } n ? n : DBNull.Value;
  247. object nonNumVal = !isNumeric ? (object)(s.ActualValue?.Trim().ToUpperInvariant() ?? string.Empty) : DBNull.Value;
  248. await _db.Ado.ExecuteCommandAsync(
  249. """
  250. INSERT INTO ado_s6_process_inspection_result_item
  251. (tenant_id, bill_id, result_id, snapshot_line_id, result_type, sample_index, actual_numeric, actual_non_numeric, judgement, remark, inspector_id, inspection_time)
  252. VALUES (@t,@bill,@rid,@line,@rt,@idx,@num,@nonnum,@judge,@remark,@uid,@now)
  253. ON DUPLICATE KEY UPDATE actual_numeric=VALUES(actual_numeric), actual_non_numeric=VALUES(actual_non_numeric),
  254. judgement=VALUES(judgement), remark=VALUES(remark), inspector_id=VALUES(inspector_id), inspection_time=VALUES(inspection_time)
  255. """,
  256. new List<SugarParameter>
  257. {
  258. new("@t", tid), new("@bill", input.BillId), new("@rid", resultId), new("@line", s.SnapshotLineId),
  259. new("@rt", (object?)line.ResultType ?? DBNull.Value), new("@idx", s.SampleIndex),
  260. new("@num", numVal), new("@nonnum", nonNumVal), new("@judge", judge),
  261. new("@remark", (object?)s.Remark ?? DBNull.Value), new("@uid", (object?)uid ?? DBNull.Value), new("@now", now)
  262. });
  263. }
  264. var (overallStatus, overallResult) = await RecomputeAndPersistHeaderAsync(tid, input.BillId, complete: false, now);
  265. _db.Ado.CommitTran();
  266. return new S6SaveResultOutput { Ok = true, OverallStatus = overallStatus, OverallResult = overallResult };
  267. }
  268. catch
  269. {
  270. _db.Ado.RollbackTran();
  271. throw;
  272. }
  273. }
  274. /// <summary>
  275. /// 完成检验。校验所有项目均已录满(COMPLETED),否则 INSPECTION_INCOMPLETE;通过则置 COMPLETED + 整单判定 + completed_at。同一事务。
  276. /// </summary>
  277. [DisplayName("完成过程检验")]
  278. [HttpPost("complete")]
  279. public async Task<S6SaveResultOutput> Complete([FromBody] S6CompleteInput input)
  280. {
  281. if (input == null || input.BillId <= 0)
  282. return new S6SaveResultOutput { Ok = false, Message = "缺少检验单。" };
  283. var tid = ResolveTenantOrThrow();
  284. var cfg = await LoadSnapshotConfigAsync(tid, input.BillId);
  285. if (cfg.Count == 0)
  286. return new S6SaveResultOutput { Ok = false, Message = "检验单不存在或无检规快照。" };
  287. var itemAgg = await AggregateItemsAsync(tid, input.BillId, cfg);
  288. if (!S6ProcessInspectionJudge.AllItemsCompleted(itemAgg))
  289. throw Oops.Oh("INSPECTION_INCOMPLETE:仍有检验项目未录满样本,无法完成检验。");
  290. var now = DateTime.Now;
  291. try
  292. {
  293. _db.Ado.BeginTran();
  294. var (overallStatus, overallResult) = await RecomputeAndPersistHeaderAsync(tid, input.BillId, complete: true, now);
  295. _db.Ado.CommitTran();
  296. return new S6SaveResultOutput { Ok = true, OverallStatus = overallStatus, OverallResult = overallResult };
  297. }
  298. catch
  299. {
  300. _db.Ado.RollbackTran();
  301. throw;
  302. }
  303. }
  304. // ── 内部:快照配置 / 结果读取 / 聚合 ──
  305. private async Task<List<SnapCfgRow>> LoadSnapshotConfigAsync(long tid, long billId) =>
  306. await _db.Ado.SqlQueryAsync<SnapCfgRow>(
  307. """
  308. SELECT id AS Id, result_type AS ResultType, lower_limit AS LowerLimit, upper_limit AS UpperLimit, sample_count AS SampleCount
  309. FROM ado_s6_process_inspection_snapshot_line WHERE tenant_id=@t AND bill_id=@bill ORDER BY id
  310. """, new List<SugarParameter> { new("@t", tid), new("@bill", billId) });
  311. private async Task<List<ResultItemRow>> LoadResultItemsAsync(long tid, long billId) =>
  312. await _db.Ado.SqlQueryAsync<ResultItemRow>(
  313. """
  314. SELECT snapshot_line_id AS SnapshotLineId, sample_index AS SampleIndex, actual_numeric AS ActualNumeric,
  315. actual_non_numeric AS ActualNonNumeric, judgement AS Judgement, remark AS Remark
  316. FROM ado_s6_process_inspection_result_item WHERE tenant_id=@t AND bill_id=@bill ORDER BY snapshot_line_id, sample_index
  317. """, new List<SugarParameter> { new("@t", tid), new("@bill", billId) });
  318. private async Task<List<(string Status, string Result)>> AggregateItemsAsync(long tid, long billId, List<SnapCfgRow> cfg)
  319. {
  320. var items = await LoadResultItemsAsync(tid, billId);
  321. var byLine = items.GroupBy(i => i.SnapshotLineId).ToDictionary(g => g.Key, g => g.Select(x => x.Judgement ?? "").ToList());
  322. return cfg.Select(c => S6ProcessInspectionJudge.AggregateItem(
  323. c.SampleCount is > 0 ? c.SampleCount!.Value : 1,
  324. byLine.TryGetValue(c.Id, out var js) ? js : new List<string>())).ToList();
  325. }
  326. private async Task<long> EnsureResultHeaderAsync(long tid, long billId, long? uid, string? uname, DateTime now)
  327. {
  328. var pars = new List<SugarParameter> { new("@t", tid), new("@bill", billId), new("@uid", (object?)uid ?? DBNull.Value), new("@uname", (object?)uname ?? DBNull.Value), new("@now", now) };
  329. await _db.Ado.ExecuteCommandAsync(
  330. """
  331. INSERT INTO ado_s6_process_inspection_result (tenant_id, bill_id, work_order_no, item_code, status, inspector_id, inspector_name, started_at)
  332. SELECT @t, @bill, b.work_order_no, b.item_code, 'IN_PROGRESS', @uid, @uname, @now
  333. FROM ado_s6_process_inspection_bill b WHERE b.tenant_id=@t AND b.id=@bill
  334. AND NOT EXISTS (SELECT 1 FROM ado_s6_process_inspection_result r WHERE r.tenant_id=@t AND r.bill_id=@bill)
  335. """, pars);
  336. return await _db.Ado.SqlQuerySingleAsync<long>(
  337. "SELECT id FROM ado_s6_process_inspection_result WHERE tenant_id=@t AND bill_id=@bill LIMIT 1",
  338. new List<SugarParameter> { new("@t", tid), new("@bill", billId) });
  339. }
  340. private async Task<(string Status, string Result)> RecomputeAndPersistHeaderAsync(long tid, long billId, bool complete, DateTime now)
  341. {
  342. var cfg = await LoadSnapshotConfigAsync(tid, billId);
  343. var itemAgg = await AggregateItemsAsync(tid, billId, cfg);
  344. var overallResult = S6ProcessInspectionJudge.AggregateOverall(itemAgg);
  345. var anyRecorded = itemAgg.Any(i => i.Status != S6ItemStatus.NotStarted);
  346. var status = complete ? S6ItemStatus.Completed : (anyRecorded ? S6ItemStatus.InProgress : S6ItemStatus.NotStarted);
  347. var pars = new List<SugarParameter>
  348. {
  349. new("@t", tid), new("@bill", billId), new("@status", status), new("@overall", overallResult),
  350. new("@completedAt", complete ? now : (object)DBNull.Value)
  351. };
  352. // completed_at 与 status 严格一致:仅 COMPLETED 有值;一旦回退(再次保存改动)即清空,避免 IN_PROGRESS 却残留完成时间。
  353. await _db.Ado.ExecuteCommandAsync(
  354. """
  355. UPDATE ado_s6_process_inspection_result
  356. SET status=@status, overall_result=@overall, completed_at=CASE WHEN @status='COMPLETED' THEN @completedAt ELSE NULL END, update_time=NOW()
  357. WHERE tenant_id=@t AND bill_id=@bill
  358. """, pars);
  359. return (status, overallResult);
  360. }
  361. private sealed class ProdInstrRow
  362. {
  363. public string? WorkOrder { get; set; }
  364. public string? LotSerial { get; set; }
  365. public string? ItemCode { get; set; }
  366. public string? ItemName { get; set; }
  367. public string? Specification { get; set; }
  368. public long? FactoryId { get; set; }
  369. public DateTime? OrderDate { get; set; }
  370. }
  371. private sealed class SnapCfgRow
  372. {
  373. public long Id { get; set; }
  374. public string? ResultType { get; set; }
  375. public string? LowerLimit { get; set; }
  376. public string? UpperLimit { get; set; }
  377. public int? SampleCount { get; set; }
  378. }
  379. private sealed class ResultItemRow
  380. {
  381. public long SnapshotLineId { get; set; }
  382. public int SampleIndex { get; set; }
  383. public decimal? ActualNumeric { get; set; }
  384. public string? ActualNonNumeric { get; set; }
  385. public string? Judgement { get; set; }
  386. public string? Remark { get; set; }
  387. }
  388. private sealed class ResultHeadRow
  389. {
  390. public string? Status { get; set; }
  391. public string? OverallResult { get; set; }
  392. }
  393. }