S6ProcessInspectionService.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  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 LIMIT 1
  50. """,
  51. new List<SugarParameter> { new("@TenantId", tid), new("@Wo", wo) });
  52. if (pi == null)
  53. {
  54. res.Status = S6SpecResolveStatus.NoMatch;
  55. res.Message = "未找到该工单的生产指令。";
  56. return res;
  57. }
  58. // ReferenceDate 取检验时点(now):过程检验在当前执行,应采用"检验时点当前有效"的检规;
  59. // 生产指令 OrderDate 是下单时点,可能早于检规生效日(sxrj),用它会漏掉当前有效检规。(§9 明确说明)
  60. var refDate = DateTime.Now;
  61. var resolve = await _resolver.ResolveAsync(tid, pi.ItemCode, refDate);
  62. res.Status = resolve.Status;
  63. res.Message = resolve.Message;
  64. res.SpecCode = resolve.SpecCode;
  65. res.SpecVersion = resolve.SpecVersion;
  66. res.CandidateCount = resolve.CandidateCount;
  67. if (resolve.Status != S6SpecResolveStatus.Matched || resolve.CandidateCount != 1 || resolve.MatchedSpecId is not { } specId)
  68. return res; // 非唯一解析:不写库
  69. // 幂等:已有 active(DRAFT/ACTIVE) 检验单 → 复用
  70. var existingId = await _db.Ado.SqlQuerySingleAsync<long?>(
  71. """
  72. SELECT id FROM ado_s6_process_inspection_bill
  73. WHERE tenant_id=@TenantId AND work_order_no=@Wo AND inspection_status IN ('DRAFT','ACTIVE')
  74. ORDER BY id DESC LIMIT 1
  75. """,
  76. new List<SugarParameter> { new("@TenantId", tid), new("@Wo", wo) });
  77. if (existingId is { } eid && eid > 0)
  78. {
  79. res.BillId = eid;
  80. res.Reused = true;
  81. return res;
  82. }
  83. // 生成:检验单 + 快照头 + 快照明细(同一事务,任一失败全回滚)
  84. var uid = _userManager.UserId;
  85. var uname = _userManager.RealName ?? _userManager.Account;
  86. try
  87. {
  88. _db.Ado.BeginTran();
  89. await _db.Ado.ExecuteCommandAsync(
  90. """
  91. INSERT INTO ado_s6_process_inspection_bill
  92. (tenant_id, factory_id, work_order_no, lot_serial, item_code, item_name, specification,
  93. spec_id, spec_code, spec_version, inspection_status, created_by, created_by_name)
  94. VALUES (@TenantId, @FactoryId, @Wo, @Lot, @ItemCode, @ItemName, @Spec,
  95. @SpecId, @SpecCode, @SpecVersion, 'DRAFT', @Uid, @Uname)
  96. """,
  97. new List<SugarParameter>
  98. {
  99. new("@TenantId", tid),
  100. new("@FactoryId", (object?)pi.FactoryId ?? DBNull.Value),
  101. new("@Wo", wo),
  102. new("@Lot", (object?)pi.LotSerial ?? DBNull.Value),
  103. new("@ItemCode", (object?)pi.ItemCode ?? DBNull.Value),
  104. new("@ItemName", (object?)pi.ItemName ?? DBNull.Value),
  105. new("@Spec", (object?)pi.Specification ?? DBNull.Value),
  106. new("@SpecId", specId),
  107. new("@SpecCode", (object?)resolve.SpecCode ?? DBNull.Value),
  108. new("@SpecVersion", (object?)resolve.SpecVersion ?? DBNull.Value),
  109. new("@Uid", (object?)uid ?? DBNull.Value),
  110. new("@Uname", (object?)uname ?? DBNull.Value),
  111. });
  112. var billId = await _db.Ado.GetLongAsync("SELECT LAST_INSERT_ID()");
  113. await _db.Ado.ExecuteCommandAsync(
  114. """
  115. INSERT INTO ado_s6_process_inspection_snapshot_head
  116. (tenant_id, bill_id, spec_id, spec_code, spec_version, effective_date, item_code, specification, source_tenant_id)
  117. VALUES (@TenantId, @BillId, @SpecId, @SpecCode, @SpecVersion, @Eff, @ItemCode, @Spec, @TenantId)
  118. """,
  119. new List<SugarParameter>
  120. {
  121. new("@TenantId", tid),
  122. new("@BillId", billId),
  123. new("@SpecId", specId),
  124. new("@SpecCode", (object?)resolve.SpecCode ?? DBNull.Value),
  125. new("@SpecVersion", (object?)resolve.SpecVersion ?? DBNull.Value),
  126. new("@Eff", (object?)resolve.EffectiveDate ?? DBNull.Value),
  127. new("@ItemCode", (object?)pi.ItemCode ?? DBNull.Value),
  128. new("@Spec", (object?)pi.Specification ?? DBNull.Value),
  129. });
  130. var lineCount = await _db.Ado.ExecuteCommandAsync(
  131. """
  132. INSERT INTO ado_s6_process_inspection_snapshot_line
  133. (tenant_id, bill_id, source_line_id, inspection_item, process_code, process_name, method,
  134. spec_value, tech_standard, lower_limit, upper_limit, inspection_frequency, result_type, sort_no)
  135. SELECT @TenantId, @BillId, z.id, z.jyxm, z.gxdh, z.gxmc, z.jyff,
  136. z.jygg, z.jsbz, z.xx, z.sx, z.jypc, z.lrlx, NULL
  137. FROM qms_gcjygfzb z
  138. WHERE z.tenant_id=@TenantId AND z.glid=@SpecId
  139. """,
  140. new List<SugarParameter> { new("@TenantId", tid), new("@BillId", billId), new("@SpecId", specId) });
  141. if (lineCount <= 0)
  142. throw Oops.Oh("过程检验规范无有效明细,无法生成检验单。");
  143. _db.Ado.CommitTran();
  144. res.BillId = billId;
  145. res.Reused = false;
  146. return res;
  147. }
  148. catch
  149. {
  150. _db.Ado.RollbackTran();
  151. throw;
  152. }
  153. }
  154. /// <summary>过程检验单只读详情(单据 + 快照头 + 快照明细)。跨租户/不存在返回 null。</summary>
  155. [DisplayName("过程检验单详情")]
  156. [HttpGet("detail")]
  157. public async Task<S6ProcessInspectionDetailDto?> GetDetail([FromQuery] long billId)
  158. {
  159. if (billId <= 0) return null;
  160. var tid = ResolveTenantOrThrow();
  161. var pars = new List<SugarParameter> { new("@TenantId", tid), new("@BillId", billId) };
  162. var dto = await _db.Ado.SqlQuerySingleAsync<S6ProcessInspectionDetailDto>(
  163. """
  164. SELECT b.id AS BillId, b.work_order_no AS WorkOrderNo, b.lot_serial AS LotSerial, b.item_code AS ItemCode,
  165. b.item_name AS ItemName, b.specification AS Specification, b.spec_id AS SpecId, b.spec_code AS SpecCode,
  166. b.spec_version AS SpecVersion, h.effective_date AS EffectiveDate, b.inspection_status AS InspectionStatus,
  167. b.created_by_name AS CreatedByName, b.create_time AS CreateTime
  168. FROM ado_s6_process_inspection_bill b
  169. LEFT JOIN ado_s6_process_inspection_snapshot_head h ON h.bill_id=b.id AND h.tenant_id=@TenantId
  170. WHERE b.tenant_id=@TenantId AND b.id=@BillId LIMIT 1
  171. """, pars);
  172. if (dto == null) return null;
  173. dto.Lines = await _db.Ado.SqlQueryAsync<S6ProcessInspectionSnapshotLineDto>(
  174. """
  175. SELECT id AS Id, inspection_item AS InspectionItem, process_code AS ProcessCode, process_name AS ProcessName,
  176. method AS Method, spec_value AS SpecValue, tech_standard AS TechStandard, lower_limit AS LowerLimit,
  177. upper_limit AS UpperLimit, inspection_frequency AS InspectionFrequency, result_type AS ResultType, sort_no AS SortNo
  178. FROM ado_s6_process_inspection_snapshot_line
  179. WHERE tenant_id=@TenantId AND bill_id=@BillId ORDER BY id
  180. """, pars);
  181. return dto;
  182. }
  183. private sealed class ProdInstrRow
  184. {
  185. public string? WorkOrder { get; set; }
  186. public string? LotSerial { get; set; }
  187. public string? ItemCode { get; set; }
  188. public string? ItemName { get; set; }
  189. public string? Specification { get; set; }
  190. public long? FactoryId { get; set; }
  191. public DateTime? OrderDate { get; set; }
  192. }
  193. }