IpqcInspectionDispositionService.cs 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. using Admin.NET.Plugin.AiDOP.Entity;
  2. using Admin.NET.Plugin.AiDOP.Manufacturing.Dto;
  3. using Admin.NET.Plugin.ApprovalFlow;
  4. using Admin.NET.Plugin.ApprovalFlow.Service;
  5. using Yitter.IdGenerator;
  6. namespace Admin.NET.Plugin.AiDOP.Manufacturing;
  7. /// <summary>
  8. /// S6 过程检验单(IPQC)不合格质量处置服务(N3_QUALITY_DISPOSITION)。
  9. ///
  10. /// 整批不合格检验单经主管 N2 判定后进入 N3;质量工程师提交处置方案 → 写 ado_s6_ipqc_quality_disposition + 推进 N3→end。
  11. /// 仅"记录处置结论"(disposition_type=REWORK/CONCESSION/SCRAP/OTHER);
  12. /// 不写 qms_gcjyd/qms_gcjydzb、不写库存/生产/仓储、不产生返工/报废/入出库实际动作、不做附件上传。
  13. /// 一单一有效处置(inspection_id 唯一 + status=1 校验);正常鉴权,权限以当前用户 N3 pending task 归属为准。
  14. /// 职责分离:本服务管处置数据;N1/N2 流程见 IpqcInspectionFlowService。
  15. /// </summary>
  16. [ApiDescriptionSettings(Order = 313, Description = "过程检验单质量处置")]
  17. [Route("api/IpqcInspectionDisposition")]
  18. [NonUnify]
  19. public class IpqcInspectionDispositionService : IDynamicApiController, ITransient
  20. {
  21. private static readonly string[] DispositionTypes =
  22. {
  23. IpqcInspectionFlowConst.DispTypeRework,
  24. IpqcInspectionFlowConst.DispTypeConcession,
  25. IpqcInspectionFlowConst.DispTypeScrap,
  26. IpqcInspectionFlowConst.DispTypeOther,
  27. };
  28. /// <summary>主/系统租户哨兵:超管未选择目标租户时其 JWT TenantId 即此值,拒绝作为业务租户。</summary>
  29. private const long MainTenantId = 1300000000001L;
  30. private readonly ISqlSugarClient _db;
  31. private readonly FlowEngineService _flowEngine;
  32. private readonly UserManager _userManager;
  33. public IpqcInspectionDispositionService(ISqlSugarClient db, FlowEngineService flowEngine, UserManager userManager)
  34. {
  35. _db = db;
  36. _flowEngine = flowEngine;
  37. _userManager = userManager;
  38. }
  39. /// <summary>严格可信租户解析:只来自认证后 JWT;无 Token(&lt;=0) 或超管未选主租户 → 拒绝;不读前端 tenantId、无默认回退。</summary>
  40. private long ResolveTenantOrThrow()
  41. {
  42. var tid = _userManager.TenantId;
  43. if (tid <= 0)
  44. throw Oops.Oh("无法确定当前租户,请重新登录或选择目标租户");
  45. if (_userManager.SuperAdmin && tid == MainTenantId)
  46. throw Oops.Oh("超级管理员操作前必须选择目标租户");
  47. return tid;
  48. }
  49. /// <summary>
  50. /// 质量处置详情(只读,供前端展示已提交处置)。查不到返回 null。
  51. /// </summary>
  52. [DisplayName("过程检验单质量处置详情")]
  53. [HttpGet("state")]
  54. public async Task<IpqcDispositionOutput?> GetState([FromQuery] long id)
  55. {
  56. if (id <= 0) return null;
  57. // 严格租户隔离:ClearFilter 移除框架租户过滤器后,必须补 TenantId 谓词,防止跨租户读他租户处置。
  58. var tid = ResolveTenantOrThrow();
  59. return await _db.Queryable<AdoS6IpqcQualityDisposition>()
  60. .ClearFilter()
  61. .Where(x => x.InspectionId == id && x.TenantId == tid && x.Status == 1)
  62. .OrderByDescending(x => x.Id)
  63. .Select(x => new IpqcDispositionOutput
  64. {
  65. Id = x.Id,
  66. InspectionId = x.InspectionId,
  67. InspectionNo = x.InspectionNo,
  68. DispositionNo = x.DispositionNo,
  69. DispositionType = x.DispositionType,
  70. DispositionQty = x.DispositionQty,
  71. ReworkQty = x.ReworkQty,
  72. ConcessionQty = x.ConcessionQty,
  73. ScrapQty = x.ScrapQty,
  74. DispositionOpinion = x.DispositionOpinion,
  75. ResponsibilityDept = x.ResponsibilityDept,
  76. ResponsibleUser = x.ResponsibleUser,
  77. Status = x.Status,
  78. SubmittedByName = x.SubmittedByName,
  79. SubmittedAt = x.SubmittedAt,
  80. })
  81. .FirstAsync();
  82. }
  83. /// <summary>
  84. /// 质量工程师提交处置方案(写处置表 + 推进 N3→end)。仅记录方案,不触发库存/生产/仓储动作。
  85. /// </summary>
  86. [DisplayName("质量工程师提交处置")]
  87. [HttpPost("submit")]
  88. public async Task<IpqcDispositionOutput?> Submit([FromBody] IpqcSubmitDispositionInput input)
  89. {
  90. var billNo = await EnsureBillExistsAsync(input.Id);
  91. // 写库前严格解析可信租户(无 Token / 超管未选主租户 → 抛错,不落库、不查他租户)。
  92. var tid = ResolveTenantOrThrow();
  93. // 参数校验
  94. var type = (input.DispositionType ?? string.Empty).Trim().ToUpperInvariant();
  95. if (!DispositionTypes.Contains(type))
  96. throw Oops.Oh("处置类型只能为 REWORK(返工)/CONCESSION(让步)/SCRAP(报废)/OTHER(其它)");
  97. if (string.IsNullOrWhiteSpace(input.DispositionOpinion))
  98. throw Oops.Oh("处置意见必填");
  99. foreach (var q in new[] { input.DispositionQty, input.ReworkQty, input.ConcessionQty, input.ScrapQty })
  100. if (q is < 0) throw Oops.Oh("处置数量不能为负");
  101. var sub = (input.ReworkQty ?? 0) + (input.ConcessionQty ?? 0) + (input.ScrapQty ?? 0);
  102. if (input.DispositionQty.HasValue && sub > input.DispositionQty.Value)
  103. throw Oops.Oh("返工+让步+报废数量之和不能大于处置数量");
  104. var inst = await GetLatestInstanceAsync(input.Id);
  105. if (inst == null) throw Oops.Oh("该检验单尚未发起流程");
  106. if (inst.Status != FlowInstanceStatusEnum.Running) throw Oops.Oh("该检验单流程已结束");
  107. var myTask = await GetMyPendingTaskAsync(inst.Id);
  108. if (myTask == null) throw Oops.Oh("当前用户没有待处理的质量处置任务(需质量工程师角色成员)");
  109. if (myTask.NodeId != IpqcInspectionFlowConst.NodeDisposition) throw Oops.Oh("当前不在质量处置节点");
  110. // 一单一有效处置(ClearFilter 后显式补 TenantId 谓词,防止跨租户误判/命中)
  111. var hasEffective = await _db.Queryable<AdoS6IpqcQualityDisposition>()
  112. .ClearFilter()
  113. .AnyAsync(x => x.InspectionId == input.Id && x.TenantId == tid && x.Status == 1);
  114. if (hasEffective) throw Oops.Oh("该检验单已有有效质量处置,不可重复提交");
  115. var now = DateTime.Now;
  116. var userName = _userManager.RealName ?? _userManager.Account;
  117. var disposition = new AdoS6IpqcQualityDisposition
  118. {
  119. Id = YitIdHelper.NextId(),
  120. TenantId = tid,
  121. OrgId = inst.OrgId,
  122. InspectionId = input.Id,
  123. InspectionNo = billNo,
  124. BizType = IpqcInspectionFlowConst.BizType,
  125. FlowInstanceId = inst.Id,
  126. DispositionNo = $"IPQCDISP-{billNo}-{now:yyyyMMddHHmmss}",
  127. DispositionType = type,
  128. DispositionQty = input.DispositionQty,
  129. ReworkQty = input.ReworkQty,
  130. ConcessionQty = input.ConcessionQty,
  131. ScrapQty = input.ScrapQty,
  132. DispositionOpinion = input.DispositionOpinion,
  133. ResponsibilityDept = input.ResponsibilityDept,
  134. ResponsibleUser = input.ResponsibleUser,
  135. Status = 1,
  136. SubmittedBy = _userManager.UserId,
  137. SubmittedByName = userName,
  138. SubmittedAt = now,
  139. RowVersion = 1,
  140. CreateTime = now,
  141. };
  142. var tran = await _db.AsTenant().UseTranAsync(async () =>
  143. {
  144. await _db.Insertable(disposition).ExecuteCommandAsync();
  145. // 更新扩展表业务状态 DISPOSED(不回写 qms_gcjyd.status)
  146. await _db.Ado.ExecuteCommandAsync(
  147. "UPDATE ado_s6_ipqc_inspection_flow_state SET business_status=@st, current_node=NULL, UpdateTime=@now WHERE inspection_id=@id AND tenant_id=@tid AND IsDelete=0",
  148. new List<SugarParameter>
  149. {
  150. new("@st", IpqcInspectionFlowConst.StatusDisposed),
  151. new("@now", now),
  152. new("@id", input.Id),
  153. new("@tid", tid),
  154. });
  155. // 推进 N3 → end
  156. await _flowEngine.Approve(myTask.Id, input.Comment);
  157. });
  158. if (!tran.IsSuccess) throw tran.ErrorException;
  159. return await GetState(input.Id);
  160. }
  161. // ───────────────────────── helpers ─────────────────────────
  162. private async Task<string> EnsureBillExistsAsync(long id)
  163. {
  164. if (id <= 0) throw Oops.Oh("检验单 id 非法");
  165. // 强制租户隔离:qms_gcjyd 全库共享无框架租户过滤器,裸 SQL 显式按当前用户租户校验。
  166. // 严格解析租户(无 Token / 超管未选主租户 → 拒绝,不回退默认)。
  167. var tid = ResolveTenantOrThrow();
  168. var rows = await _db.Ado.SqlQueryAsync<string>(
  169. "SELECT djbh FROM qms_gcjyd WHERE id=@id AND tenant_id=@tid LIMIT 1",
  170. new List<SugarParameter> { new("@id", id), new("@tid", tid) });
  171. if (rows.Count == 0) throw Oops.Oh("检验单不存在");
  172. return rows[0] ?? id.ToString();
  173. }
  174. private async Task<ApprovalFlowInstance?> GetLatestInstanceAsync(long bizId)
  175. {
  176. return await _db.Queryable<ApprovalFlowInstance>()
  177. .ClearFilter()
  178. .Where(x => x.BizType == IpqcInspectionFlowConst.BizType && x.BizId == bizId)
  179. .OrderByDescending(x => x.Id)
  180. .FirstAsync();
  181. }
  182. private async Task<ApprovalFlowTask?> GetMyPendingTaskAsync(long instanceId)
  183. {
  184. var userId = _userManager.UserId;
  185. return await _db.Queryable<ApprovalFlowTask>()
  186. .ClearFilter()
  187. .Where(x => x.InstanceId == instanceId && x.AssigneeId == userId && x.Status == FlowTaskStatusEnum.Pending)
  188. .FirstAsync();
  189. }
  190. }