AdoS8ExceptionsController.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. using Admin.NET.Plugin.AiDOP.Infrastructure.S8;
  2. using Admin.NET.Plugin.AiDOP.Const.S8;
  3. using Admin.NET.Plugin.AiDOP.Dto.S8;
  4. using Admin.NET.Plugin.AiDOP.Infrastructure;
  5. using Admin.NET.Plugin.AiDOP.Service.S8;
  6. namespace Admin.NET.Plugin.AiDOP.Controllers.S8;
  7. [ApiController]
  8. [Route("api/aidop/s8/exceptions")]
  9. [NonUnify]
  10. [RequirePcMenuPath("/aidop/s8/exceptions")]
  11. public class AdoS8ExceptionsController : ControllerBase
  12. {
  13. private readonly S8ExceptionService _exceptionSvc;
  14. private readonly S8TaskFlowService _taskFlowSvc;
  15. private readonly S8DecisionService _decisionSvc;
  16. private readonly S8TrustedScopeResolver _scope;
  17. public AdoS8ExceptionsController(
  18. S8ExceptionService exceptionSvc,
  19. S8TaskFlowService taskFlowSvc,
  20. S8DecisionService decisionSvc,
  21. S8TrustedScopeResolver scope)
  22. {
  23. _exceptionSvc = exceptionSvc;
  24. _taskFlowSvc = taskFlowSvc;
  25. _decisionSvc = decisionSvc;
  26. _scope = scope;
  27. }
  28. [HttpGet]
  29. [S8ExceptionAction(S8ExceptionActionCode.View)]
  30. public async Task<IActionResult> GetPagedAsync([FromQuery] AdoS8ExceptionQueryDto q)
  31. {
  32. // S8-TENANT-FACTORY-P0-CLOSURE-1:作用域一律服务端解析,覆盖客户端传入值。
  33. var scope = await _scope.ResolveAsync();
  34. q.TenantId = scope.TenantId;
  35. var (total, list) = await _exceptionSvc.GetPagedAsync(q);
  36. return Ok(new { total, page = q.Page, pageSize = q.PageSize, list });
  37. }
  38. [HttpGet("filter-options")]
  39. [S8ExceptionAction(S8ExceptionActionCode.View)]
  40. public async Task<IActionResult> GetFilterOptionsAsync([FromQuery] long tenantId = 1, [FromQuery] long factoryId = 1)
  41. {
  42. // Compatibility-only. Security scope is resolved server-side.
  43. _ = tenantId; _ = factoryId;
  44. var scope = await _scope.ResolveAsync();
  45. return Ok(await _exceptionSvc.GetFilterOptionsAsync(scope.TenantId));
  46. }
  47. [HttpGet("{id:long}")]
  48. [S8ExceptionAction(S8ExceptionActionCode.View)]
  49. public async Task<IActionResult> GetDetailAsync(long id, [FromQuery] long tenantId = 1, [FromQuery] long factoryId = 1)
  50. {
  51. // Compatibility-only. Security scope is resolved server-side.
  52. _ = tenantId; _ = factoryId;
  53. var scope = await _scope.ResolveAsync();
  54. var detail = await _exceptionSvc.GetDetailAsync(id, scope.TenantId);
  55. return detail == null ? NotFound() : Ok(detail);
  56. }
  57. [HttpGet("{id:long}/timeline")]
  58. [S8ExceptionAction(S8ExceptionActionCode.View)]
  59. public async Task<IActionResult> GetTimelineAsync(long id)
  60. {
  61. if (!await IsInScopeAsync(id)) return NotFound();
  62. return Ok(await _decisionSvc.GetTimelineAsync(id));
  63. }
  64. [HttpGet("{id:long}/decisions")]
  65. [S8ExceptionAction(S8ExceptionActionCode.View)]
  66. public async Task<IActionResult> GetDecisionsAsync(long id)
  67. {
  68. if (!await IsInScopeAsync(id)) return NotFound();
  69. return Ok(await _decisionSvc.GetDecisionsAsync(id));
  70. }
  71. [HttpGet("{id:long}/evidences")]
  72. [S8ExceptionAction(S8ExceptionActionCode.View)]
  73. public async Task<IActionResult> GetEvidencesAsync(long id)
  74. {
  75. if (!await IsInScopeAsync(id)) return NotFound();
  76. return Ok(await _decisionSvc.GetEvidencesAsync(id));
  77. }
  78. /// <summary>子资源归属由父异常派生;不在可信作用域内一律按「不存在」处理,不泄露他租户资源是否存在。</summary>
  79. private async Task<bool> IsInScopeAsync(long exceptionId)
  80. {
  81. var scope = await _scope.ResolveAsync();
  82. return await _decisionSvc.IsExceptionInScopeAsync(exceptionId, scope.TenantId);
  83. }
  84. [HttpPost("{id:long}/claim")]
  85. [S8ExceptionAction(S8ExceptionActionCode.Claim)]
  86. /// <summary>
  87. /// 认领。<b>处理人恒为当前登录账号</b>,请求体只带备注 ——
  88. /// 「替别人指派」是 <c>POST {id}/transfer</c>(权限位 <c>ExceptionAssign</c>)。
  89. /// 已被他人抢先认领时返回 400「该异常已被其他人员认领」。
  90. /// </summary>
  91. public async Task<IActionResult> ClaimAsync(long id, [FromQuery] long tenantId = 1, [FromQuery] long factoryId = 1,
  92. [FromBody] AdoS8ClaimBody? body = null)
  93. {
  94. _ = tenantId; _ = factoryId;
  95. try
  96. {
  97. var scope = await _scope.ResolveAsync();
  98. var e = await _taskFlowSvc.ClaimAsync(id, scope.TenantId, body?.Remark);
  99. return Ok(new { id = e.Id, status = e.Status, assigneeUserId = e.AssigneeUserId });
  100. }
  101. catch (S8BizException ex) { return BadRequest(new { message = ex.Message }); }
  102. }
  103. [HttpPost("{id:long}/transfer")]
  104. [S8ExceptionAction(S8ExceptionActionCode.Transfer)]
  105. public async Task<IActionResult> TransferAsync(long id, [FromQuery] long tenantId = 1, [FromQuery] long factoryId = 1,
  106. [FromBody] AdoS8TransferDto? body = null)
  107. {
  108. _ = tenantId; _ = factoryId;
  109. try
  110. {
  111. var scope = await _scope.ResolveAsync();
  112. var e = await _taskFlowSvc.TransferAsync(id, scope.TenantId, body?.AssigneeUserId ?? 0, body?.Remark);
  113. return Ok(new { id = e.Id, assigneeUserId = e.AssigneeUserId });
  114. }
  115. catch (S8BizException ex) { return BadRequest(new { message = ex.Message }); }
  116. }
  117. [HttpPost("{id:long}/start-progress")]
  118. [S8ExceptionAction(S8ExceptionActionCode.Start)]
  119. public async Task<IActionResult> StartProgressAsync(long id, [FromQuery] long tenantId = 1, [FromQuery] long factoryId = 1,
  120. [FromBody] AdoS8CommentDto? body = null)
  121. {
  122. _ = tenantId; _ = factoryId;
  123. try
  124. {
  125. var scope = await _scope.ResolveAsync();
  126. var e = await _taskFlowSvc.StartProgressAsync(id, scope.TenantId, body?.Remark);
  127. return Ok(new { id = e.Id, status = e.Status });
  128. }
  129. catch (S8BizException ex) { return BadRequest(new { message = ex.Message }); }
  130. }
  131. [HttpPost("{id:long}/upgrade")]
  132. [S8ExceptionAction(S8ExceptionActionCode.Upgrade)]
  133. public async Task<IActionResult> UpgradeAsync(long id, [FromQuery] long tenantId = 1, [FromQuery] long factoryId = 1,
  134. [FromBody] AdoS8CommentDto? body = null)
  135. {
  136. _ = tenantId; _ = factoryId;
  137. try
  138. {
  139. var scope = await _scope.ResolveAsync();
  140. var e = await _taskFlowSvc.UpgradeAsync(id, scope.TenantId, body?.Remark);
  141. return Ok(new { id = e.Id, status = e.Status });
  142. }
  143. catch (S8BizException ex) { return BadRequest(new { message = ex.Message }); }
  144. }
  145. [HttpPost("{id:long}/reject")]
  146. [S8ExceptionAction(S8ExceptionActionCode.Reject)]
  147. public async Task<IActionResult> RejectAsync(long id, [FromQuery] long tenantId = 1, [FromQuery] long factoryId = 1,
  148. [FromBody] AdoS8CommentDto? body = null)
  149. {
  150. _ = tenantId; _ = factoryId;
  151. try
  152. {
  153. var scope = await _scope.ResolveAsync();
  154. var e = await _taskFlowSvc.RejectAsync(id, scope.TenantId, body?.Remark);
  155. return Ok(new { id = e.Id, status = e.Status });
  156. }
  157. catch (S8BizException ex) { return BadRequest(new { message = ex.Message }); }
  158. }
  159. /// <summary>
  160. /// S8-RESPONSIBILITY-POOL-1:该异常当前可选的复核人。
  161. ///
  162. /// <para>候选 = 该规则的<b>复核账号池</b> ∩ 拥有「审核」动作权限 ∩ 同租户启用账号。
  163. /// 与 <c>submit-verification</c> 的服务端校验<b>同一份实现</b>
  164. /// (<see cref="IS8VerifierEligibility"/>)—— 候选列表只是"好用",
  165. /// 真正的门在服务端,绕过 UI 直接调 API 注入池外账号同样会被拒。</para>
  166. ///
  167. /// <para>人工提报的异常没有来源规则,候选回落为本租户内有审核权限的账号
  168. /// (与认领 / 转派对人工提报的豁免同一口径)。</para>
  169. /// </summary>
  170. [HttpGet("{id:long}/verifier-candidates")]
  171. [S8ExceptionAction(S8ExceptionActionCode.SubmitVerify)]
  172. public async Task<IActionResult> VerifierCandidatesAsync(long id,
  173. [FromServices] IS8VerifierEligibility eligibility,
  174. [FromServices] SqlSugarRepository<Admin.NET.Plugin.AiDOP.Entity.S8.AdoS8Exception> rep)
  175. {
  176. try
  177. {
  178. var scope = await _scope.ResolveAsync();
  179. var e = await rep.AsQueryable().ClearFilter()
  180. .Where(x => x.Id == id && x.TenantId == scope.TenantId && !x.IsDeleted)
  181. .FirstAsync()
  182. ?? throw new S8BizException("异常不存在");
  183. var candidates = await eligibility.ListCandidatesAsync(scope.TenantId, e);
  184. return Ok(new
  185. {
  186. ruleCode = e.SourceRuleCode,
  187. // 无来源规则 = 人工提报,前端据此说明"候选来自全租户"而不是"复核池"。
  188. fromReviewerPool = !string.IsNullOrWhiteSpace(e.SourceRuleCode),
  189. candidates,
  190. });
  191. }
  192. catch (S8BizException ex) { return BadRequest(new { message = ex.Message }); }
  193. }
  194. [HttpPost("{id:long}/submit-verification")]
  195. [S8ExceptionAction(S8ExceptionActionCode.SubmitVerify)]
  196. public async Task<IActionResult> SubmitVerificationAsync(long id,
  197. [FromQuery] long tenantId = 1, [FromQuery] long factoryId = 1,
  198. [FromBody] AdoS8SubmitVerificationDto? body = null)
  199. {
  200. _ = tenantId; _ = factoryId;
  201. try
  202. {
  203. var scope = await _scope.ResolveAsync();
  204. var e = await _taskFlowSvc.SubmitVerificationAsync(
  205. id, scope.TenantId, body?.VerifierUserId ?? 0, body?.Remark);
  206. return Ok(new { id = e.Id, status = e.Status, verifierUserId = e.VerifierUserId });
  207. }
  208. catch (S8BizException ex) { return BadRequest(new { message = ex.Message }); }
  209. }
  210. [HttpPost("{id:long}/approve-verification")]
  211. [S8ExceptionAction(S8ExceptionActionCode.Verify)]
  212. public async Task<IActionResult> ApproveVerificationAsync(long id,
  213. [FromQuery] long tenantId = 1, [FromQuery] long factoryId = 1,
  214. [FromBody] AdoS8ApproveVerificationDto? body = null)
  215. {
  216. _ = tenantId; _ = factoryId;
  217. try
  218. {
  219. var scope = await _scope.ResolveAsync();
  220. var e = await _taskFlowSvc.ApproveVerificationAsync(id, scope.TenantId, body?.Remark);
  221. return Ok(new { id = e.Id, status = e.Status });
  222. }
  223. catch (S8BizException ex) { return BadRequest(new { message = ex.Message }); }
  224. }
  225. [HttpPost("{id:long}/reject-verification")]
  226. [S8ExceptionAction(S8ExceptionActionCode.Verify)]
  227. public async Task<IActionResult> RejectVerificationAsync(long id,
  228. [FromQuery] long tenantId = 1, [FromQuery] long factoryId = 1,
  229. [FromBody] AdoS8RejectVerificationDto? body = null)
  230. {
  231. _ = tenantId; _ = factoryId;
  232. try
  233. {
  234. var scope = await _scope.ResolveAsync();
  235. var e = await _taskFlowSvc.RejectVerificationAsync(id, scope.TenantId, body?.Remark ?? string.Empty);
  236. return Ok(new { id = e.Id, status = e.Status });
  237. }
  238. catch (S8BizException ex) { return BadRequest(new { message = ex.Message }); }
  239. }
  240. [HttpPost("{id:long}/comment")]
  241. [S8ExceptionAction(S8ExceptionActionCode.Comment)]
  242. public async Task<IActionResult> CommentAsync(long id, [FromBody] AdoS8CommentDto? body = null)
  243. {
  244. try
  245. {
  246. var scope = await _scope.ResolveAsync();
  247. await _taskFlowSvc.CommentAsync(id, scope.TenantId, body?.Remark);
  248. return Ok(new { id });
  249. }
  250. catch (S8BizException ex) { return BadRequest(new { message = ex.Message }); }
  251. }
  252. }
  253. /// <summary>
  254. /// 认领请求体。<b>刻意不含处理人字段</b> —— 处理人由服务端从登录上下文取,
  255. /// 留一个"服务端会忽略"的 assignee 字段只会让调用方以为自己能替别人认领。
  256. /// </summary>
  257. public class AdoS8ClaimBody
  258. {
  259. public string? Remark { get; set; }
  260. }