AdoS8ExceptionsController.cs 12 KB

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