S6ProcessInspectionReviewService.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  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 Phase 1D 过程检验单(Phase 1C bill)审核流程服务(桥接:Phase 1C Complete → ApprovalFlow)。
  9. ///
  10. /// business_id = ado_s6_process_inspection_bill.id(BizType=S6_PROCESS_INSPECTION)。复用 ApprovalFlow 引擎,不改核心。
  11. /// 提交门槛:结果 status=COMPLETED 才能提交审核;幂等:一单最多一个 active flow。
  12. /// N1_SUBMIT=发起人本人(提交即自动通过)→ N2_SUP_REVIEW 主管审核 → GW_RESULT(overall_result=FAIL→N3 / PASS→end)
  13. /// → N3_QE_DISPOSITION 质量工程师处置 → end。质量结果 overall_result 全程只读、审批不改写(§20 硬门)。
  14. /// 处置落 ado_s6_ipqc_quality_disposition(biz_type 区分,复用不建重复表)。严格租户隔离(ResolveTenantOrThrow)。
  15. /// </summary>
  16. [ApiDescriptionSettings(Order = 305, Description = "过程检验单审核")]
  17. [Route("api/S6ProcessInspectionReview")]
  18. [NonUnify]
  19. public class S6ProcessInspectionReviewService : IDynamicApiController, ITransient
  20. {
  21. private const long MainTenantId = 1300000000001L;
  22. private readonly ISqlSugarClient _db;
  23. private readonly FlowEngineService _flowEngine;
  24. private readonly UserManager _userManager;
  25. public S6ProcessInspectionReviewService(ISqlSugarClient db, FlowEngineService flowEngine, UserManager userManager)
  26. {
  27. _db = db;
  28. _flowEngine = flowEngine;
  29. _userManager = userManager;
  30. }
  31. private long ResolveTenantOrThrow()
  32. {
  33. var tid = _userManager.TenantId;
  34. if (tid <= 0) throw Oops.Oh("无法确定当前租户,请重新登录或选择目标租户");
  35. if (_userManager.SuperAdmin && tid == MainTenantId) throw Oops.Oh("超级管理员操作前必须选择目标租户");
  36. return tid;
  37. }
  38. /// <summary>审核流状态(只读,供前端按钮控制)。</summary>
  39. [DisplayName("过程检验单审核状态")]
  40. [HttpGet("state")]
  41. public async Task<S6ReviewStateOutput> GetState([FromQuery] long billId)
  42. {
  43. await LoadBillAsync(billId);
  44. return await BuildStateAsync(billId);
  45. }
  46. /// <summary>提交审核(仅 COMPLETED 可提交;幂等;发起流程并自动通过 N1→N2)。</summary>
  47. [DisplayName("提交审核")]
  48. [HttpPost("submit-review")]
  49. public async Task<S6ReviewStateOutput> SubmitReview([FromBody] S6SubmitReviewInput input)
  50. {
  51. var bill = await LoadBillAsync(input.BillId);
  52. if (!string.Equals(bill.ResultStatus, S6ProcessInspectionFlowConst.StatusCompleted, StringComparison.OrdinalIgnoreCase))
  53. throw Oops.Oh("INSPECTION_NOT_COMPLETED:检验未完成,无法提交审核");
  54. var inst = await GetLatestInstanceAsync(input.BillId);
  55. if (inst != null && inst.Status == FlowInstanceStatusEnum.Running)
  56. throw Oops.Oh("该检验单已在审核中,请勿重复提交");
  57. if (inst != null && inst.Status != FlowInstanceStatusEnum.Running && inst.Status != FlowInstanceStatusEnum.Rejected && inst.Status != FlowInstanceStatusEnum.Cancelled)
  58. throw Oops.Oh("该检验单审核流程已结束");
  59. var tran = await _db.AsTenant().UseTranAsync(async () =>
  60. {
  61. var instanceId = await _flowEngine.StartFlow(new StartFlowInput
  62. {
  63. BizType = S6ProcessInspectionFlowConst.BizType,
  64. BizId = input.BillId,
  65. BizNo = bill.WorkOrderNo,
  66. Title = $"过程检验单 {bill.WorkOrderNo}",
  67. Comment = input.Comment,
  68. });
  69. // N1_SUBMIT=发起人本人,取自己的 N1 待办并自动通过 → N2
  70. var myTask = await GetMyPendingTaskAsync(instanceId);
  71. if (myTask == null || myTask.NodeId != S6ProcessInspectionFlowConst.NodeSubmit)
  72. throw Oops.Oh("提交审核节点异常,未取到发起人任务");
  73. await _flowEngine.Approve(myTask.Id, input.Comment);
  74. });
  75. if (!tran.IsSuccess) throw tran.ErrorException;
  76. return await BuildStateAsync(input.BillId);
  77. }
  78. /// <summary>主管审核通过(N2):Approve→GW_RESULT,按 overall_result 自动分流(FAIL→N3 / PASS→end)。不改写质量结果。</summary>
  79. [DisplayName("主管审核通过")]
  80. [HttpPost("supervisor-approve")]
  81. public async Task<S6ReviewStateOutput> SupervisorApprove([FromBody] S6ReviewApproveInput input)
  82. {
  83. await LoadBillAsync(input.BillId);
  84. var (_, myTask) = await RequireSupervisorTaskAsync(input.BillId);
  85. var tran = await _db.AsTenant().UseTranAsync(async () =>
  86. {
  87. await _flowEngine.Approve(myTask.Id, input.Comment);
  88. });
  89. if (!tran.IsSuccess) throw tran.ErrorException;
  90. return await BuildStateAsync(input.BillId);
  91. }
  92. /// <summary>主管退回(N2→N1,意见必填)。质量结果不改写。</summary>
  93. [DisplayName("主管退回")]
  94. [HttpPost("supervisor-return")]
  95. public async Task<S6ReviewStateOutput> SupervisorReturn([FromBody] S6ReviewReturnInput input)
  96. {
  97. if (string.IsNullOrWhiteSpace(input.Comment)) throw Oops.Oh("退回必须填写意见");
  98. await LoadBillAsync(input.BillId);
  99. var (_, myTask) = await RequireSupervisorTaskAsync(input.BillId);
  100. var tran = await _db.AsTenant().UseTranAsync(async () =>
  101. {
  102. await _flowEngine.ReturnToPrev(myTask.Id, input.Comment);
  103. });
  104. if (!tran.IsSuccess) throw tran.ErrorException;
  105. return await BuildStateAsync(input.BillId);
  106. }
  107. /// <summary>QE 质量处置(N3):落处置记录(不改写 overall_result),Approve→end。一单一有效处置。</summary>
  108. [DisplayName("质量处置")]
  109. [HttpPost("qe-disposition")]
  110. public async Task<S6ReviewStateOutput> QeDisposition([FromBody] S6QeDispositionInput input)
  111. {
  112. var bill = await LoadBillAsync(input.BillId);
  113. var type = (input.DispositionType ?? "").Trim().ToUpperInvariant();
  114. if (type != S6ProcessInspectionFlowConst.DispTypeRework && type != S6ProcessInspectionFlowConst.DispTypeConcession
  115. && type != S6ProcessInspectionFlowConst.DispTypeScrap && type != S6ProcessInspectionFlowConst.DispTypeOther)
  116. throw Oops.Oh("处置类型非法(REWORK/CONCESSION/SCRAP/OTHER)");
  117. if (string.IsNullOrWhiteSpace(input.DispositionOpinion)) throw Oops.Oh("处置意见必填");
  118. var inst = await GetLatestInstanceAsync(input.BillId);
  119. if (inst == null || inst.Status != FlowInstanceStatusEnum.Running) throw Oops.Oh("该检验单审核流程已结束");
  120. var myTask = await GetMyPendingTaskAsync(inst.Id);
  121. if (myTask == null) throw Oops.Oh("当前用户没有待处理的处置任务(需质量工程师角色成员)");
  122. if (myTask.NodeId != S6ProcessInspectionFlowConst.NodeDisposition) throw Oops.Oh("当前不在质量处置节点");
  123. var tid = ResolveTenantOrThrow();
  124. // 一单一有效处置(status=1 且未软删)+ InspectionId 唯一约束
  125. var existed = await _db.Queryable<AdoS6IpqcQualityDisposition>().ClearFilter()
  126. .AnyAsync(x => x.InspectionId == input.BillId && x.TenantId == tid && x.Status == 1);
  127. if (existed) throw Oops.Oh("该检验单已存在有效质量处置记录");
  128. var now = DateTime.Now;
  129. var opName = _userManager.RealName ?? _userManager.Account;
  130. var tran = await _db.AsTenant().UseTranAsync(async () =>
  131. {
  132. await _db.Insertable(new AdoS6IpqcQualityDisposition
  133. {
  134. Id = YitIdHelper.NextId(),
  135. TenantId = tid,
  136. InspectionId = input.BillId,
  137. InspectionNo = bill.WorkOrderNo,
  138. BizType = S6ProcessInspectionFlowConst.BizType,
  139. FlowInstanceId = inst.Id,
  140. DispositionNo = $"S6PIDISP-{bill.WorkOrderNo}-{now:yyyyMMddHHmmss}",
  141. DispositionType = type,
  142. DispositionQty = input.DispositionQty,
  143. ReworkQty = input.ReworkQty,
  144. ConcessionQty = input.ConcessionQty,
  145. ScrapQty = input.ScrapQty,
  146. DispositionOpinion = input.DispositionOpinion,
  147. ResponsibilityDept = input.ResponsibilityDept,
  148. ResponsibleUser = input.ResponsibleUser,
  149. Status = 1,
  150. SubmittedBy = _userManager.UserId,
  151. SubmittedByName = opName,
  152. SubmittedAt = now,
  153. RowVersion = 1,
  154. CreateTime = now,
  155. }).ExecuteCommandAsync();
  156. await _flowEngine.Approve(myTask.Id, input.Comment);
  157. });
  158. if (!tran.IsSuccess) throw tran.ErrorException;
  159. return await BuildStateAsync(input.BillId);
  160. }
  161. // ───────────────────────── helpers ─────────────────────────
  162. private sealed class BillRow
  163. {
  164. public long Id { get; set; }
  165. public string? WorkOrderNo { get; set; }
  166. public string? OverallResult { get; set; }
  167. public string? ResultStatus { get; set; }
  168. }
  169. /// <summary>加载 Phase 1C 单据 + 结果(严格租户隔离)。</summary>
  170. private async Task<BillRow> LoadBillAsync(long billId)
  171. {
  172. if (billId <= 0) throw Oops.Oh("检验单 id 非法");
  173. var tid = ResolveTenantOrThrow();
  174. var row = await _db.Ado.SqlQuerySingleAsync<BillRow>(
  175. """
  176. SELECT b.id AS Id, b.work_order_no AS WorkOrderNo, r.overall_result AS OverallResult, r.status AS ResultStatus
  177. FROM ado_s6_process_inspection_bill b
  178. LEFT JOIN ado_s6_process_inspection_result r ON r.bill_id=b.id AND r.tenant_id=b.tenant_id
  179. WHERE b.id=@id AND b.tenant_id=@tid LIMIT 1
  180. """,
  181. new List<SugarParameter> { new("@id", billId), new("@tid", tid) });
  182. if (row == null) throw Oops.Oh("检验单不存在");
  183. return row;
  184. }
  185. private async Task<ApprovalFlowInstance?> GetLatestInstanceAsync(long bizId)
  186. => await _db.Queryable<ApprovalFlowInstance>().ClearFilter()
  187. .Where(x => x.BizType == S6ProcessInspectionFlowConst.BizType && x.BizId == bizId)
  188. .OrderByDescending(x => x.Id).FirstAsync();
  189. private async Task<ApprovalFlowTask?> GetMyPendingTaskAsync(long instanceId)
  190. {
  191. var userId = _userManager.UserId;
  192. return await _db.Queryable<ApprovalFlowTask>().ClearFilter()
  193. .Where(x => x.InstanceId == instanceId && x.AssigneeId == userId && x.Status == FlowTaskStatusEnum.Pending)
  194. .FirstAsync();
  195. }
  196. private async Task<(ApprovalFlowInstance inst, ApprovalFlowTask task)> RequireSupervisorTaskAsync(long bizId)
  197. {
  198. var inst = await GetLatestInstanceAsync(bizId);
  199. if (inst == null) throw Oops.Oh("该检验单尚未发起审核");
  200. if (inst.Status != FlowInstanceStatusEnum.Running) throw Oops.Oh("该检验单审核流程已结束");
  201. var myTask = await GetMyPendingTaskAsync(inst.Id);
  202. if (myTask == null) throw Oops.Oh("当前用户没有待处理的审核任务(需检验主管角色成员)");
  203. if (myTask.NodeId != S6ProcessInspectionFlowConst.NodeSupervisor) throw Oops.Oh("当前不在主管审核节点");
  204. return (inst, myTask);
  205. }
  206. private async Task<bool> HasRoleAsync(long roleId)
  207. {
  208. var userId = _userManager.UserId;
  209. return await _db.Queryable<SysUserRole>().ClearFilter().AnyAsync(x => x.UserId == userId && x.RoleId == roleId);
  210. }
  211. private async Task<S6ReviewStateOutput> BuildStateAsync(long billId)
  212. {
  213. var tid = ResolveTenantOrThrow();
  214. var output = new S6ReviewStateOutput { BillId = billId };
  215. var bill = await _db.Ado.SqlQuerySingleAsync<BillRow>(
  216. """
  217. SELECT b.id AS Id, b.work_order_no AS WorkOrderNo, r.overall_result AS OverallResult, r.status AS ResultStatus
  218. FROM ado_s6_process_inspection_bill b
  219. LEFT JOIN ado_s6_process_inspection_result r ON r.bill_id=b.id AND r.tenant_id=b.tenant_id
  220. WHERE b.id=@id AND b.tenant_id=@tid LIMIT 1
  221. """,
  222. new List<SugarParameter> { new("@id", billId), new("@tid", tid) });
  223. output.OverallResult = bill?.OverallResult;
  224. output.ResultStatus = bill?.ResultStatus;
  225. output.DispositionRequired = string.Equals(bill?.OverallResult, S6ProcessInspectionFlowConst.ResultFail, StringComparison.OrdinalIgnoreCase) ? 1 : 0;
  226. output.HasDisposition = await _db.Queryable<AdoS6IpqcQualityDisposition>().ClearFilter()
  227. .AnyAsync(x => x.InspectionId == billId && x.TenantId == tid && x.Status == 1);
  228. var inst = await GetLatestInstanceAsync(billId);
  229. if (inst == null)
  230. {
  231. output.FlowStatus = "NotStarted";
  232. output.CanSubmitReview = string.Equals(bill?.ResultStatus, S6ProcessInspectionFlowConst.StatusCompleted, StringComparison.OrdinalIgnoreCase);
  233. output.Message = output.CanSubmitReview ? "检验完成,待提交审核" : "检验未完成,暂不可提交审核";
  234. return output;
  235. }
  236. output.InstanceId = inst.Id;
  237. output.FlowStatus = inst.Status.ToString();
  238. if (inst.Status != FlowInstanceStatusEnum.Running)
  239. {
  240. output.IsCompleted = true;
  241. output.Message = inst.Status == FlowInstanceStatusEnum.Approved ? "审核已闭环(CLOSED)" : $"流程已结束({inst.Status})";
  242. // 已结束且非通过(驳回/撤回)→ 允许重新提交
  243. output.CanSubmitReview = inst.Status is FlowInstanceStatusEnum.Rejected or FlowInstanceStatusEnum.Cancelled
  244. && string.Equals(bill?.ResultStatus, S6ProcessInspectionFlowConst.StatusCompleted, StringComparison.OrdinalIgnoreCase);
  245. return output;
  246. }
  247. output.CurrentNodeCode = inst.CurrentNodeId;
  248. var myTask = await GetMyPendingTaskAsync(inst.Id);
  249. if (myTask != null)
  250. {
  251. output.CurrentNodeName = myTask.NodeName;
  252. if (myTask.NodeId == S6ProcessInspectionFlowConst.NodeSupervisor)
  253. {
  254. output.CurrentAssigneeRole = S6ProcessInspectionFlowConst.RoleSupervisorCode;
  255. output.CanSupervisorApprove = true;
  256. output.CanSupervisorReturn = true;
  257. output.Message = "待当前用户主管审核";
  258. }
  259. else if (myTask.NodeId == S6ProcessInspectionFlowConst.NodeDisposition)
  260. {
  261. output.CurrentAssigneeRole = S6ProcessInspectionFlowConst.RoleQualityEngineerCode;
  262. output.CanSubmitDisposition = true;
  263. output.Message = "待当前用户提交质量处置";
  264. }
  265. else
  266. {
  267. output.Message = "流程进行中";
  268. }
  269. }
  270. else
  271. {
  272. output.CurrentAssigneeRole = inst.CurrentNodeId switch
  273. {
  274. S6ProcessInspectionFlowConst.NodeDisposition => S6ProcessInspectionFlowConst.RoleQualityEngineerCode,
  275. S6ProcessInspectionFlowConst.NodeSupervisor => S6ProcessInspectionFlowConst.RoleSupervisorCode,
  276. _ => null,
  277. };
  278. output.Message = "流程进行中(当前用户无待办)";
  279. }
  280. return output;
  281. }
  282. }