| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281 |
- using Admin.NET.Plugin.AiDOP.Infrastructure.S8;
- using Admin.NET.Plugin.AiDOP.Const.S8;
- using Admin.NET.Plugin.AiDOP.Dto.S8;
- using Admin.NET.Plugin.AiDOP.Infrastructure;
- using Admin.NET.Plugin.AiDOP.Service.S8;
- namespace Admin.NET.Plugin.AiDOP.Controllers.S8;
- [ApiController]
- [Route("api/aidop/s8/exceptions")]
- [NonUnify]
- public class AdoS8ExceptionsController : ControllerBase
- {
- private readonly S8ExceptionService _exceptionSvc;
- private readonly S8TaskFlowService _taskFlowSvc;
- private readonly S8DecisionService _decisionSvc;
- private readonly S8TrustedScopeResolver _scope;
- public AdoS8ExceptionsController(
- S8ExceptionService exceptionSvc,
- S8TaskFlowService taskFlowSvc,
- S8DecisionService decisionSvc,
- S8TrustedScopeResolver scope)
- {
- _exceptionSvc = exceptionSvc;
- _taskFlowSvc = taskFlowSvc;
- _decisionSvc = decisionSvc;
- _scope = scope;
- }
- [HttpGet]
- [S8ExceptionAction(S8ExceptionActionCode.View)]
- public async Task<IActionResult> GetPagedAsync([FromQuery] AdoS8ExceptionQueryDto q)
- {
- // S8-TENANT-FACTORY-P0-CLOSURE-1:作用域一律服务端解析,覆盖客户端传入值。
- var scope = await _scope.ResolveAsync();
- q.TenantId = scope.TenantId;
- var (total, list) = await _exceptionSvc.GetPagedAsync(q);
- return Ok(new { total, page = q.Page, pageSize = q.PageSize, list });
- }
- [HttpGet("filter-options")]
- [S8ExceptionAction(S8ExceptionActionCode.View)]
- public async Task<IActionResult> GetFilterOptionsAsync([FromQuery] long tenantId = 1, [FromQuery] long factoryId = 1)
- {
- // Compatibility-only. Security scope is resolved server-side.
- _ = tenantId; _ = factoryId;
- var scope = await _scope.ResolveAsync();
- return Ok(await _exceptionSvc.GetFilterOptionsAsync(scope.TenantId));
- }
- [HttpGet("{id:long}")]
- [S8ExceptionAction(S8ExceptionActionCode.View)]
- public async Task<IActionResult> GetDetailAsync(long id, [FromQuery] long tenantId = 1, [FromQuery] long factoryId = 1)
- {
- // Compatibility-only. Security scope is resolved server-side.
- _ = tenantId; _ = factoryId;
- var scope = await _scope.ResolveAsync();
- var detail = await _exceptionSvc.GetDetailAsync(id, scope.TenantId);
- return detail == null ? NotFound() : Ok(detail);
- }
- [HttpGet("{id:long}/timeline")]
- [S8ExceptionAction(S8ExceptionActionCode.View)]
- public async Task<IActionResult> GetTimelineAsync(long id)
- {
- if (!await IsInScopeAsync(id)) return NotFound();
- return Ok(await _decisionSvc.GetTimelineAsync(id));
- }
- [HttpGet("{id:long}/decisions")]
- [S8ExceptionAction(S8ExceptionActionCode.View)]
- public async Task<IActionResult> GetDecisionsAsync(long id)
- {
- if (!await IsInScopeAsync(id)) return NotFound();
- return Ok(await _decisionSvc.GetDecisionsAsync(id));
- }
- [HttpGet("{id:long}/evidences")]
- [S8ExceptionAction(S8ExceptionActionCode.View)]
- public async Task<IActionResult> GetEvidencesAsync(long id)
- {
- if (!await IsInScopeAsync(id)) return NotFound();
- return Ok(await _decisionSvc.GetEvidencesAsync(id));
- }
- /// <summary>子资源归属由父异常派生;不在可信作用域内一律按「不存在」处理,不泄露他租户资源是否存在。</summary>
- private async Task<bool> IsInScopeAsync(long exceptionId)
- {
- var scope = await _scope.ResolveAsync();
- return await _decisionSvc.IsExceptionInScopeAsync(exceptionId, scope.TenantId);
- }
- [HttpPost("{id:long}/claim")]
- [S8ExceptionAction(S8ExceptionActionCode.Claim)]
- /// <summary>
- /// 认领。<b>处理人恒为当前登录账号</b>,请求体只带备注 ——
- /// 「替别人指派」是 <c>POST {id}/transfer</c>(权限位 <c>ExceptionAssign</c>)。
- /// 已被他人抢先认领时返回 400「该异常已被其他人员认领」。
- /// </summary>
- public async Task<IActionResult> ClaimAsync(long id, [FromQuery] long tenantId = 1, [FromQuery] long factoryId = 1,
- [FromBody] AdoS8ClaimBody? body = null)
- {
- _ = tenantId; _ = factoryId;
- try
- {
- var scope = await _scope.ResolveAsync();
- var e = await _taskFlowSvc.ClaimAsync(id, scope.TenantId, body?.Remark);
- return Ok(new { id = e.Id, status = e.Status, assigneeUserId = e.AssigneeUserId });
- }
- catch (S8BizException ex) { return BadRequest(new { message = ex.Message }); }
- }
- [HttpPost("{id:long}/transfer")]
- [S8ExceptionAction(S8ExceptionActionCode.Transfer)]
- public async Task<IActionResult> TransferAsync(long id, [FromQuery] long tenantId = 1, [FromQuery] long factoryId = 1,
- [FromBody] AdoS8TransferDto? body = null)
- {
- _ = tenantId; _ = factoryId;
- try
- {
- var scope = await _scope.ResolveAsync();
- var e = await _taskFlowSvc.TransferAsync(id, scope.TenantId, body?.AssigneeUserId ?? 0, body?.Remark);
- return Ok(new { id = e.Id, assigneeUserId = e.AssigneeUserId });
- }
- catch (S8BizException ex) { return BadRequest(new { message = ex.Message }); }
- }
- [HttpPost("{id:long}/start-progress")]
- [S8ExceptionAction(S8ExceptionActionCode.Start)]
- public async Task<IActionResult> StartProgressAsync(long id, [FromQuery] long tenantId = 1, [FromQuery] long factoryId = 1,
- [FromBody] AdoS8CommentDto? body = null)
- {
- _ = tenantId; _ = factoryId;
- try
- {
- var scope = await _scope.ResolveAsync();
- var e = await _taskFlowSvc.StartProgressAsync(id, scope.TenantId, body?.Remark);
- return Ok(new { id = e.Id, status = e.Status });
- }
- catch (S8BizException ex) { return BadRequest(new { message = ex.Message }); }
- }
- [HttpPost("{id:long}/upgrade")]
- [S8ExceptionAction(S8ExceptionActionCode.Upgrade)]
- public async Task<IActionResult> UpgradeAsync(long id, [FromQuery] long tenantId = 1, [FromQuery] long factoryId = 1,
- [FromBody] AdoS8CommentDto? body = null)
- {
- _ = tenantId; _ = factoryId;
- try
- {
- var scope = await _scope.ResolveAsync();
- var e = await _taskFlowSvc.UpgradeAsync(id, scope.TenantId, body?.Remark);
- return Ok(new { id = e.Id, status = e.Status });
- }
- catch (S8BizException ex) { return BadRequest(new { message = ex.Message }); }
- }
- [HttpPost("{id:long}/reject")]
- [S8ExceptionAction(S8ExceptionActionCode.Reject)]
- public async Task<IActionResult> RejectAsync(long id, [FromQuery] long tenantId = 1, [FromQuery] long factoryId = 1,
- [FromBody] AdoS8CommentDto? body = null)
- {
- _ = tenantId; _ = factoryId;
- try
- {
- var scope = await _scope.ResolveAsync();
- var e = await _taskFlowSvc.RejectAsync(id, scope.TenantId, body?.Remark);
- return Ok(new { id = e.Id, status = e.Status });
- }
- catch (S8BizException ex) { return BadRequest(new { message = ex.Message }); }
- }
- /// <summary>
- /// S8-RESPONSIBILITY-POOL-1:该异常当前可选的复核人。
- ///
- /// <para>候选 = 该规则的<b>复核账号池</b> ∩ 拥有「审核」动作权限 ∩ 同租户启用账号。
- /// 与 <c>submit-verification</c> 的服务端校验<b>同一份实现</b>
- /// (<see cref="IS8VerifierEligibility"/>)—— 候选列表只是"好用",
- /// 真正的门在服务端,绕过 UI 直接调 API 注入池外账号同样会被拒。</para>
- ///
- /// <para>人工提报的异常没有来源规则,候选回落为本租户内有审核权限的账号
- /// (与认领 / 转派对人工提报的豁免同一口径)。</para>
- /// </summary>
- [HttpGet("{id:long}/verifier-candidates")]
- [S8ExceptionAction(S8ExceptionActionCode.SubmitVerify)]
- public async Task<IActionResult> VerifierCandidatesAsync(long id,
- [FromServices] IS8VerifierEligibility eligibility,
- [FromServices] SqlSugarRepository<Admin.NET.Plugin.AiDOP.Entity.S8.AdoS8Exception> rep)
- {
- try
- {
- var scope = await _scope.ResolveAsync();
- var e = await rep.AsQueryable().ClearFilter()
- .Where(x => x.Id == id && x.TenantId == scope.TenantId && !x.IsDeleted)
- .FirstAsync()
- ?? throw new S8BizException("异常不存在");
- var candidates = await eligibility.ListCandidatesAsync(scope.TenantId, e);
- return Ok(new
- {
- ruleCode = e.SourceRuleCode,
- // 无来源规则 = 人工提报,前端据此说明"候选来自全租户"而不是"复核池"。
- fromReviewerPool = !string.IsNullOrWhiteSpace(e.SourceRuleCode),
- candidates,
- });
- }
- catch (S8BizException ex) { return BadRequest(new { message = ex.Message }); }
- }
- [HttpPost("{id:long}/submit-verification")]
- [S8ExceptionAction(S8ExceptionActionCode.SubmitVerify)]
- public async Task<IActionResult> SubmitVerificationAsync(long id,
- [FromQuery] long tenantId = 1, [FromQuery] long factoryId = 1,
- [FromBody] AdoS8SubmitVerificationDto? body = null)
- {
- _ = tenantId; _ = factoryId;
- try
- {
- var scope = await _scope.ResolveAsync();
- var e = await _taskFlowSvc.SubmitVerificationAsync(
- id, scope.TenantId, body?.VerifierUserId ?? 0, body?.Remark);
- return Ok(new { id = e.Id, status = e.Status, verifierUserId = e.VerifierUserId });
- }
- catch (S8BizException ex) { return BadRequest(new { message = ex.Message }); }
- }
- [HttpPost("{id:long}/approve-verification")]
- [S8ExceptionAction(S8ExceptionActionCode.Verify)]
- public async Task<IActionResult> ApproveVerificationAsync(long id,
- [FromQuery] long tenantId = 1, [FromQuery] long factoryId = 1,
- [FromBody] AdoS8ApproveVerificationDto? body = null)
- {
- _ = tenantId; _ = factoryId;
- try
- {
- var scope = await _scope.ResolveAsync();
- var e = await _taskFlowSvc.ApproveVerificationAsync(id, scope.TenantId, body?.Remark);
- return Ok(new { id = e.Id, status = e.Status });
- }
- catch (S8BizException ex) { return BadRequest(new { message = ex.Message }); }
- }
- [HttpPost("{id:long}/reject-verification")]
- [S8ExceptionAction(S8ExceptionActionCode.Verify)]
- public async Task<IActionResult> RejectVerificationAsync(long id,
- [FromQuery] long tenantId = 1, [FromQuery] long factoryId = 1,
- [FromBody] AdoS8RejectVerificationDto? body = null)
- {
- _ = tenantId; _ = factoryId;
- try
- {
- var scope = await _scope.ResolveAsync();
- var e = await _taskFlowSvc.RejectVerificationAsync(id, scope.TenantId, body?.Remark ?? string.Empty);
- return Ok(new { id = e.Id, status = e.Status });
- }
- catch (S8BizException ex) { return BadRequest(new { message = ex.Message }); }
- }
- [HttpPost("{id:long}/comment")]
- [S8ExceptionAction(S8ExceptionActionCode.Comment)]
- public async Task<IActionResult> CommentAsync(long id, [FromBody] AdoS8CommentDto? body = null)
- {
- try
- {
- var scope = await _scope.ResolveAsync();
- await _taskFlowSvc.CommentAsync(id, scope.TenantId, body?.Remark);
- return Ok(new { id });
- }
- catch (S8BizException ex) { return BadRequest(new { message = ex.Message }); }
- }
- }
- /// <summary>
- /// 认领请求体。<b>刻意不含处理人字段</b> —— 处理人由服务端从登录上下文取,
- /// 留一个"服务端会忽略"的 assignee 字段只会让调用方以为自己能替别人认领。
- /// </summary>
- public class AdoS8ClaimBody
- {
- public string? Remark { get; set; }
- }
|