S6ProcessInspectionReviewService.cs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  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>
  47. /// 提交审核(仅 COMPLETED 可提交),同时承载**首次提交**与**主管退回后的重新提交**。
  48. ///
  49. /// 两者是同一条业务命令:校验相同(本租户单据 + 结果 COMPLETED)、都不写业务行,
  50. /// 唯一差异是要不要先起流程。故复用同一入口,由后端按真实 Flow 状态分流:
  51. /// 无实例 / 上一轮已 Rejected|Cancelled → 首次提交:StartFlow + 通过 N1 → N2
  52. /// Running 且已退回 N1 且本人持有该 N1 待办 → 重新提交:沿用同一实例,只通过既有 N1 任务 → N2
  53. ///
  54. /// S6-PI-RESUBMIT-1:原实现用「实例 Running」当作「已提交」的代理判据,对 N2/N3 正确,
  55. /// 但 ReturnToPrev 只回退 CurrentNodeId、**实例仍是 Running**,于是把合法重提一并挡死,
  56. /// 单据卡在 N1 无任何专用出口。此处改为按「节点 + 本人待办」精确判定 ——
  57. /// **不是放宽 Running**:N2/N3 仍拒绝,非本人待办仍拒绝,终态仍拒绝。
  58. /// </summary>
  59. [DisplayName("提交审核")]
  60. [HttpPost("submit-review")]
  61. public async Task<S6ReviewStateOutput> SubmitReview([FromBody] S6SubmitReviewInput input)
  62. {
  63. var bill = await LoadBillAsync(input.BillId);
  64. if (!string.Equals(bill.ResultStatus, S6ProcessInspectionFlowConst.StatusCompleted, StringComparison.OrdinalIgnoreCase))
  65. throw Oops.Oh("INSPECTION_NOT_COMPLETED:检验未完成,无法提交审核");
  66. var inst = await GetLatestInstanceAsync(input.BillId);
  67. var resubmitTask = inst != null && inst.Status == FlowInstanceStatusEnum.Running
  68. ? await RequireResubmittableTaskAsync(inst)
  69. : null;
  70. if (resubmitTask == null && inst != null
  71. && inst.Status != FlowInstanceStatusEnum.Rejected && inst.Status != FlowInstanceStatusEnum.Cancelled)
  72. throw Oops.Oh("该检验单审核流程已结束");
  73. var tran = await _db.AsTenant().UseTranAsync(async () =>
  74. {
  75. var taskId = resubmitTask?.Id;
  76. if (taskId == null)
  77. {
  78. var instanceId = await _flowEngine.StartFlow(new StartFlowInput
  79. {
  80. BizType = S6ProcessInspectionFlowConst.BizType,
  81. BizId = input.BillId,
  82. BizNo = bill.WorkOrderNo,
  83. Title = $"过程检验单 {bill.WorkOrderNo}",
  84. Comment = input.Comment,
  85. });
  86. // N1_SUBMIT=发起人本人,取自己的 N1 待办并自动通过 → N2
  87. var myTask = await GetMyPendingTaskAsync(instanceId);
  88. if (myTask == null || myTask.NodeId != S6ProcessInspectionFlowConst.NodeSubmit)
  89. throw Oops.Oh("提交审核节点异常,未取到发起人任务");
  90. taskId = myTask.Id;
  91. }
  92. // 重提路径复用同一实例的既有 N1 待办,不再 StartFlow —— StartFlow 每次都会新建实例,
  93. // 重跑首次路径会造成同一单据出现多个并存实例。
  94. await _flowEngine.Approve(taskId.Value, input.Comment);
  95. });
  96. if (!tran.IsSuccess) throw tran.ErrorException;
  97. return await BuildStateAsync(input.BillId);
  98. }
  99. /// <summary>主管审核通过(N2):Approve→GW_RESULT,按 overall_result 自动分流(FAIL→N3 / PASS→end)。不改写质量结果。</summary>
  100. [DisplayName("主管审核通过")]
  101. [HttpPost("supervisor-approve")]
  102. public async Task<S6ReviewStateOutput> SupervisorApprove([FromBody] S6ReviewApproveInput input)
  103. {
  104. await LoadBillAsync(input.BillId);
  105. var (_, myTask) = await RequireSupervisorTaskAsync(input.BillId);
  106. var tran = await _db.AsTenant().UseTranAsync(async () =>
  107. {
  108. await _flowEngine.Approve(myTask.Id, input.Comment);
  109. });
  110. if (!tran.IsSuccess) throw tran.ErrorException;
  111. return await BuildStateAsync(input.BillId);
  112. }
  113. /// <summary>主管退回(N2→N1,意见必填)。质量结果不改写。</summary>
  114. [DisplayName("主管退回")]
  115. [HttpPost("supervisor-return")]
  116. public async Task<S6ReviewStateOutput> SupervisorReturn([FromBody] S6ReviewReturnInput input)
  117. {
  118. if (string.IsNullOrWhiteSpace(input.Comment)) throw Oops.Oh("退回必须填写意见");
  119. await LoadBillAsync(input.BillId);
  120. var (_, myTask) = await RequireSupervisorTaskAsync(input.BillId);
  121. var tran = await _db.AsTenant().UseTranAsync(async () =>
  122. {
  123. await _flowEngine.ReturnToPrev(myTask.Id, input.Comment);
  124. });
  125. if (!tran.IsSuccess) throw tran.ErrorException;
  126. return await BuildStateAsync(input.BillId);
  127. }
  128. /// <summary>QE 质量处置(N3):落处置记录(不改写 overall_result),Approve→end。一单一有效处置。</summary>
  129. [DisplayName("质量处置")]
  130. [HttpPost("qe-disposition")]
  131. public async Task<S6ReviewStateOutput> QeDisposition([FromBody] S6QeDispositionInput input)
  132. {
  133. var bill = await LoadBillAsync(input.BillId);
  134. var type = (input.DispositionType ?? "").Trim().ToUpperInvariant();
  135. if (type != S6ProcessInspectionFlowConst.DispTypeRework && type != S6ProcessInspectionFlowConst.DispTypeConcession
  136. && type != S6ProcessInspectionFlowConst.DispTypeScrap && type != S6ProcessInspectionFlowConst.DispTypeOther)
  137. throw Oops.Oh("处置类型非法(REWORK/CONCESSION/SCRAP/OTHER)");
  138. if (string.IsNullOrWhiteSpace(input.DispositionOpinion)) throw Oops.Oh("处置意见必填");
  139. var inst = await GetLatestInstanceAsync(input.BillId);
  140. if (inst == null || inst.Status != FlowInstanceStatusEnum.Running) throw Oops.Oh("该检验单审核流程已结束");
  141. var myTask = await GetMyPendingTaskAsync(inst.Id);
  142. if (myTask == null) throw Oops.Oh("当前用户没有待处理的处置任务(需质量工程师角色成员)");
  143. if (myTask.NodeId != S6ProcessInspectionFlowConst.NodeDisposition) throw Oops.Oh("当前不在质量处置节点");
  144. var tid = ResolveTenantOrThrow();
  145. // 一单一有效处置(status=1 且未软删)+ InspectionId 唯一约束
  146. var existed = await _db.Queryable<AdoS6IpqcQualityDisposition>().ClearFilter()
  147. .AnyAsync(x => x.InspectionId == input.BillId && x.TenantId == tid && x.Status == 1);
  148. if (existed) throw Oops.Oh("该检验单已存在有效质量处置记录");
  149. var now = DateTime.Now;
  150. var opName = _userManager.RealName ?? _userManager.Account;
  151. var tran = await _db.AsTenant().UseTranAsync(async () =>
  152. {
  153. await _db.Insertable(new AdoS6IpqcQualityDisposition
  154. {
  155. Id = YitIdHelper.NextId(),
  156. TenantId = tid,
  157. InspectionId = input.BillId,
  158. InspectionNo = bill.WorkOrderNo,
  159. BizType = S6ProcessInspectionFlowConst.BizType,
  160. FlowInstanceId = inst.Id,
  161. DispositionNo = $"S6PIDISP-{bill.WorkOrderNo}-{now:yyyyMMddHHmmss}",
  162. DispositionType = type,
  163. DispositionQty = input.DispositionQty,
  164. ReworkQty = input.ReworkQty,
  165. ConcessionQty = input.ConcessionQty,
  166. ScrapQty = input.ScrapQty,
  167. DispositionOpinion = input.DispositionOpinion,
  168. ResponsibilityDept = input.ResponsibilityDept,
  169. ResponsibleUser = input.ResponsibleUser,
  170. Status = 1,
  171. SubmittedBy = _userManager.UserId,
  172. SubmittedByName = opName,
  173. SubmittedAt = now,
  174. RowVersion = 1,
  175. CreateTime = now,
  176. }).ExecuteCommandAsync();
  177. await _flowEngine.Approve(myTask.Id, input.Comment);
  178. });
  179. if (!tran.IsSuccess) throw tran.ErrorException;
  180. return await BuildStateAsync(input.BillId);
  181. }
  182. // ───────────────────────── helpers ─────────────────────────
  183. private sealed class BillRow
  184. {
  185. public long Id { get; set; }
  186. public string? WorkOrderNo { get; set; }
  187. public string? OverallResult { get; set; }
  188. public string? ResultStatus { get; set; }
  189. }
  190. /// <summary>加载 Phase 1C 单据 + 结果(严格租户隔离)。</summary>
  191. private async Task<BillRow> LoadBillAsync(long billId)
  192. {
  193. if (billId <= 0) throw Oops.Oh("检验单 id 非法");
  194. var tid = ResolveTenantOrThrow();
  195. var row = await _db.Ado.SqlQuerySingleAsync<BillRow>(
  196. """
  197. SELECT b.id AS Id, b.work_order_no AS WorkOrderNo, r.overall_result AS OverallResult, r.status AS ResultStatus
  198. FROM ado_s6_process_inspection_bill b
  199. LEFT JOIN ado_s6_process_inspection_result r ON r.bill_id=b.id AND r.tenant_id=b.tenant_id
  200. WHERE b.id=@id AND b.tenant_id=@tid LIMIT 1
  201. """,
  202. new List<SugarParameter> { new("@id", billId), new("@tid", tid) });
  203. if (row == null) throw Oops.Oh("检验单不存在");
  204. return row;
  205. }
  206. private async Task<ApprovalFlowInstance?> GetLatestInstanceAsync(long bizId)
  207. => await _db.Queryable<ApprovalFlowInstance>().ClearFilter()
  208. .Where(x => x.BizType == S6ProcessInspectionFlowConst.BizType && x.BizId == bizId)
  209. .OrderByDescending(x => x.Id).FirstAsync();
  210. private async Task<ApprovalFlowTask?> GetMyPendingTaskAsync(long instanceId)
  211. {
  212. var userId = _userManager.UserId;
  213. return await _db.Queryable<ApprovalFlowTask>().ClearFilter()
  214. .Where(x => x.InstanceId == instanceId && x.AssigneeId == userId && x.Status == FlowTaskStatusEnum.Pending)
  215. .FirstAsync();
  216. }
  217. private async Task<(ApprovalFlowInstance inst, ApprovalFlowTask task)> RequireSupervisorTaskAsync(long bizId)
  218. {
  219. var inst = await GetLatestInstanceAsync(bizId);
  220. if (inst == null) throw Oops.Oh("该检验单尚未发起审核");
  221. if (inst.Status != FlowInstanceStatusEnum.Running) throw Oops.Oh("该检验单审核流程已结束");
  222. var myTask = await GetMyPendingTaskAsync(inst.Id);
  223. if (myTask == null) throw Oops.Oh("当前用户没有待处理的审核任务(需检验主管角色成员)");
  224. if (myTask.NodeId != S6ProcessInspectionFlowConst.NodeSupervisor) throw Oops.Oh("当前不在主管审核节点");
  225. return (inst, myTask);
  226. }
  227. /// <summary>
  228. /// S6-PI-RESUBMIT-1:判定一个 Running 实例是否处于「合法可重新提交」状态,是则返回本人那条 N1 待办。
  229. ///
  230. /// 仅凭 <c>CurrentNodeId == N1_SUBMIT</c> 不够——实例可能异常残留该字段却无待办,
  231. /// 或待办属于别人。故三条同时成立才放行:节点已回到 N1、当前用户持有 Pending 任务、该任务就在 N1。
  232. /// 租户隔离由调用前的 <see cref="LoadBillAsync"/> 承担(Flow 两表无 TenantId 列,
  233. /// 与 <see cref="RequireSupervisorTaskAsync"/> 同一既有模式)。
  234. /// </summary>
  235. private async Task<ApprovalFlowTask> RequireResubmittableTaskAsync(ApprovalFlowInstance inst)
  236. {
  237. if (inst.CurrentNodeId != S6ProcessInspectionFlowConst.NodeSubmit)
  238. throw Oops.Oh($"当前流程节点不允许提交审核({NodeNameOf(inst.CurrentNodeId) ?? inst.CurrentNodeId})");
  239. var myTask = await GetMyPendingTaskAsync(inst.Id);
  240. if (myTask == null || myTask.NodeId != S6ProcessInspectionFlowConst.NodeSubmit)
  241. throw Oops.Oh("当前用户不是该检验单当前检验任务处理人");
  242. return myTask;
  243. }
  244. private async Task<bool> HasRoleAsync(long roleId)
  245. {
  246. var userId = _userManager.UserId;
  247. return await _db.Queryable<SysUserRole>().ClearFilter().AnyAsync(x => x.UserId == userId && x.RoleId == roleId);
  248. }
  249. private async Task<S6ReviewStateOutput> BuildStateAsync(long billId)
  250. {
  251. var tid = ResolveTenantOrThrow();
  252. var output = new S6ReviewStateOutput { BillId = billId };
  253. var bill = await _db.Ado.SqlQuerySingleAsync<BillRow>(
  254. """
  255. SELECT b.id AS Id, b.work_order_no AS WorkOrderNo, r.overall_result AS OverallResult, r.status AS ResultStatus
  256. FROM ado_s6_process_inspection_bill b
  257. LEFT JOIN ado_s6_process_inspection_result r ON r.bill_id=b.id AND r.tenant_id=b.tenant_id
  258. WHERE b.id=@id AND b.tenant_id=@tid LIMIT 1
  259. """,
  260. new List<SugarParameter> { new("@id", billId), new("@tid", tid) });
  261. var completed = string.Equals(bill?.ResultStatus, S6ProcessInspectionFlowConst.StatusCompleted, StringComparison.OrdinalIgnoreCase);
  262. output.OverallResult = bill?.OverallResult;
  263. output.ResultStatus = bill?.ResultStatus;
  264. output.QualityText = QualityTextOf(bill?.OverallResult);
  265. output.DispositionRequired = string.Equals(bill?.OverallResult, S6ProcessInspectionFlowConst.ResultFail, StringComparison.OrdinalIgnoreCase) ? 1 : 0;
  266. output.Disposition = await LoadDispositionAsync(billId, tid);
  267. output.HasDisposition = output.Disposition != null;
  268. var inst = await GetLatestInstanceAsync(billId);
  269. if (inst == null)
  270. {
  271. output.FlowStatus = "NotStarted";
  272. // S6-IPQC-EDIT-1:流程未起 → 首次录入阶段,可编辑(不受 completed 约束,否则录完就锁死)。
  273. output.CanEdit = true;
  274. output.CanSubmitReview = completed;
  275. output.DisplayStatus = completed ? "待提交审核" : "检验中";
  276. output.Message = completed ? "检验完成,待提交审核" : "检验未完成,暂不可提交审核";
  277. return output;
  278. }
  279. output.InstanceId = inst.Id;
  280. output.FlowStatus = inst.Status.ToString();
  281. output.History = await LoadHistoryAsync(inst.Id, tid);
  282. if (inst.Status != FlowInstanceStatusEnum.Running)
  283. {
  284. output.IsCompleted = true;
  285. output.DisplayStatus = inst.Status == FlowInstanceStatusEnum.Approved ? "已关闭"
  286. : inst.Status == FlowInstanceStatusEnum.Rejected ? "已驳回"
  287. : inst.Status == FlowInstanceStatusEnum.Cancelled ? "已撤回" : "已结束";
  288. output.Message = inst.Status == FlowInstanceStatusEnum.Approved ? "审核已闭环(CLOSED)" : $"流程已结束({inst.Status})";
  289. output.CanSubmitReview = inst.Status is FlowInstanceStatusEnum.Rejected or FlowInstanceStatusEnum.Cancelled && completed;
  290. // S6-IPQC-EDIT-1:流程已结束 → 一律不可编辑。
  291. // 注:Rejected / Cancelled 下 CanSubmitReview 为 true(会新起一条流程),但此处仍保持
  292. // CanEdit=false —— 与本次修复前的前端表达式对该状态的取值一致,不在本轮放宽未经运行时
  293. // 验证的路径(专用链的主管退回走 ReturnToPrev 保持 Running@N1,不进入本分支)。
  294. output.CanEdit = false;
  295. return output;
  296. }
  297. output.CurrentNodeCode = inst.CurrentNodeId;
  298. output.DisplayStatus = inst.CurrentNodeId switch
  299. {
  300. S6ProcessInspectionFlowConst.NodeSupervisor => "主管审核中",
  301. S6ProcessInspectionFlowConst.NodeDisposition => "QE质量处置中",
  302. _ => "审核中",
  303. };
  304. var myTask = await GetMyPendingTaskAsync(inst.Id);
  305. if (myTask != null)
  306. {
  307. output.CurrentNodeName = NodeNameOf(myTask.NodeId) ?? myTask.NodeName;
  308. if (myTask.NodeId == S6ProcessInspectionFlowConst.NodeSupervisor)
  309. {
  310. output.CurrentAssigneeRole = S6ProcessInspectionFlowConst.RoleSupervisorCode;
  311. output.CanSupervisorApprove = true;
  312. output.CanSupervisorReturn = true;
  313. output.Message = "待当前用户主管审核";
  314. }
  315. else if (myTask.NodeId == S6ProcessInspectionFlowConst.NodeDisposition)
  316. {
  317. output.CurrentAssigneeRole = S6ProcessInspectionFlowConst.RoleQualityEngineerCode;
  318. output.CanSubmitDisposition = true;
  319. output.Message = "待当前用户提交质量处置";
  320. }
  321. else if (myTask.NodeId == S6ProcessInspectionFlowConst.NodeSubmit)
  322. {
  323. // S6-PI-RESUBMIT-1:主管退回后流程回到 N1,本人持有该待办 → 可修正结果后重新提交。
  324. // 复用既有 canSubmitReview(前端已按它显示同一个「提交审核」按钮),
  325. // 不新增 isRejected / isResubmit 之类让前端自己推断业务状态的字段。
  326. // S6-IPQC-EDIT-1:同一份待办归属即编辑权限来源(node==N1 且 AssigneeId==当前用户 且 Pending)。
  327. // 这里**不叠加 completed** —— 退回后通常要把某项结果改掉,改的过程中 ResultStatus 会回到
  328. // IN_PROGRESS,若叠加 completed 会在"刚开始改"的一瞬间把编辑权关掉。
  329. output.CanEdit = true;
  330. output.CanSubmitReview = completed;
  331. output.DisplayStatus = "已退回待重新提交";
  332. output.Message = completed ? "已退回,修正检验结果后可重新提交审核" : "已退回,检验结果未完成,暂不可提交审核";
  333. }
  334. else
  335. {
  336. output.Message = "流程进行中";
  337. }
  338. }
  339. else
  340. {
  341. output.CurrentNodeName = NodeNameOf(inst.CurrentNodeId);
  342. output.CurrentAssigneeRole = inst.CurrentNodeId switch
  343. {
  344. S6ProcessInspectionFlowConst.NodeDisposition => S6ProcessInspectionFlowConst.RoleQualityEngineerCode,
  345. S6ProcessInspectionFlowConst.NodeSupervisor => S6ProcessInspectionFlowConst.RoleSupervisorCode,
  346. _ => null,
  347. };
  348. output.Message = "流程进行中(当前用户无待办)";
  349. }
  350. return output;
  351. }
  352. // ── Phase 1E 展示辅助(业务中文映射;后端拥有语义)──
  353. private static string QualityTextOf(string? overallResult) => (overallResult ?? "").ToUpperInvariant() switch
  354. {
  355. "PASS" => "合格",
  356. "FAIL" => "不合格",
  357. "PENDING" or "IN_PROGRESS" => "检验中",
  358. "INVALID" => "无效",
  359. _ => "—",
  360. };
  361. private static string? NodeNameOf(string? nodeId) => nodeId switch
  362. {
  363. S6ProcessInspectionFlowConst.NodeSubmit => "提交审核",
  364. S6ProcessInspectionFlowConst.NodeSupervisor => "检验主管审核",
  365. S6ProcessInspectionFlowConst.NodeDisposition => "质量处置",
  366. _ => null,
  367. };
  368. private static string DispositionTypeTextOf(string? t) => (t ?? "").ToUpperInvariant() switch
  369. {
  370. "REWORK" => "返工",
  371. "CONCESSION" => "让步接收",
  372. "SCRAP" => "报废",
  373. "OTHER" => "其它",
  374. _ => t ?? "",
  375. };
  376. private static string ActionTextOf(int action) => action switch
  377. {
  378. 1 => "提交",
  379. 2 => "通过",
  380. 3 => "驳回",
  381. 5 => "撤回",
  382. 6 => "退回",
  383. 9 => "取消",
  384. _ => "操作",
  385. };
  386. private async Task<S6DispositionView?> LoadDispositionAsync(long billId, long tid)
  387. {
  388. var d = await _db.Queryable<AdoS6IpqcQualityDisposition>().ClearFilter()
  389. .Where(x => x.InspectionId == billId && x.TenantId == tid && x.Status == 1)
  390. .OrderByDescending(x => x.Id).FirstAsync();
  391. if (d == null) return null;
  392. return new S6DispositionView
  393. {
  394. DispositionType = d.DispositionType,
  395. DispositionTypeText = DispositionTypeTextOf(d.DispositionType),
  396. DispositionQty = d.DispositionQty,
  397. DispositionOpinion = d.DispositionOpinion,
  398. ResponsibilityDept = d.ResponsibilityDept,
  399. ResponsibleUser = d.ResponsibleUser,
  400. OperatorName = d.SubmittedByName,
  401. SubmittedAt = d.SubmittedAt,
  402. };
  403. }
  404. private sealed class FlowLogRow
  405. {
  406. public int Action { get; set; }
  407. public string? NodeId { get; set; }
  408. public string? OperatorName { get; set; }
  409. public string? Comment { get; set; }
  410. public DateTime? CreateTime { get; set; }
  411. }
  412. private async Task<List<S6ReviewHistoryItem>> LoadHistoryAsync(long instanceId, long tid)
  413. {
  414. // 复用正式 ApprovalFlowLog(不造第二套 history)。租户隔离由实例归属保证(instanceId 已按 BizType+BizId 定位)。
  415. var rows = await _db.Ado.SqlQueryAsync<FlowLogRow>(
  416. "SELECT Action AS Action, NodeId AS NodeId, OperatorName AS OperatorName, Comment AS Comment, CreateTime AS CreateTime FROM ApprovalFlowLog WHERE InstanceId=@iid ORDER BY Id",
  417. new List<SugarParameter> { new("@iid", instanceId) });
  418. return rows.Select(r => new S6ReviewHistoryItem
  419. {
  420. Time = r.CreateTime,
  421. OperatorName = r.OperatorName,
  422. NodeName = NodeNameOf(r.NodeId) ?? r.NodeId,
  423. ActionText = ActionTextOf(r.Action),
  424. Comment = r.Comment,
  425. }).ToList();
  426. }
  427. }