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 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 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 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 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 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 GetEvidencesAsync(long id) { if (!await IsInScopeAsync(id)) return NotFound(); return Ok(await _decisionSvc.GetEvidencesAsync(id)); } /// 子资源归属由父异常派生;不在可信作用域内一律按「不存在」处理,不泄露他租户资源是否存在。 private async Task IsInScopeAsync(long exceptionId) { var scope = await _scope.ResolveAsync(); return await _decisionSvc.IsExceptionInScopeAsync(exceptionId, scope.TenantId); } [HttpPost("{id:long}/claim")] [S8ExceptionAction(S8ExceptionActionCode.Claim)] /// /// 认领。处理人恒为当前登录账号,请求体只带备注 —— /// 「替别人指派」是 POST {id}/transfer(权限位 ExceptionAssign)。 /// 已被他人抢先认领时返回 400「该异常已被其他人员认领」。 /// public async Task 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 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 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 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 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 }); } } [HttpPost("{id:long}/submit-verification")] [S8ExceptionAction(S8ExceptionActionCode.SubmitVerify)] public async Task 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 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 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 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 }); } } } /// /// 认领请求体。刻意不含处理人字段 —— 处理人由服务端从登录上下文取, /// 留一个"服务端会忽略"的 assignee 字段只会让调用方以为自己能替别人认领。 /// public class AdoS8ClaimBody { public string? Remark { get; set; } }