AdoS8WatchDebugController.cs 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. using Admin.NET.Plugin.AiDOP.Infrastructure.S8;
  2. using Admin.NET.Plugin.AiDOP.Const.S8;
  3. using Admin.NET.Plugin.AiDOP.Infrastructure;
  4. using Admin.NET.Plugin.AiDOP.Service.S8;
  5. using Microsoft.Extensions.Configuration;
  6. namespace Admin.NET.Plugin.AiDOP.Controllers.S8;
  7. /// <summary>
  8. /// G-01 最小补验内部入口(非正式调度接入点)。
  9. /// 仅用于触发 S8 自动监控 MVP 主链一次,供端到端联调与验收取证;
  10. /// 不对外暴露业务语义,不作为长期 API。
  11. /// G08-06:移除匿名访问;由 appsettings 的 `WatchScheduler:DebugEndpointEnabled` 控制是否启用,
  12. /// 未启用时所有 Action 返回 404(语义等价于"未注册")。
  13. ///
  14. /// <para><b>S8-DEBUG-TENANT-GUARD-1(2026-09-20):租户边界收口。</b>
  15. /// 修复前本 Controller 是 S8 里<b>唯一</b>把 <c>tenantId</c> 当作执行上下文的入口:
  16. /// <c>run-once</c> 直接把 query 里的 <c>tenantId</c>(默认值 1)传给
  17. /// <see cref="S8WatchSchedulerService.CreateExceptionsAsync"/>,于是调用方可以
  18. /// <b>在任意租户下建单</b>,并从出参 <c>createdExceptionId</c> / <c>relatedObjectCode</c> /
  19. /// <c>matchedExceptionId</c> <b>读回该租户的业务事实</b>;<c>test-cleanup</c> 同理可软删任意租户的
  20. /// <c>TEST_G09_</c> 数据。两者都只被 <c>DebugEndpointEnabled</c> 这一个<b>功能开关</b>挡着 ——
  21. /// 而功能开关不是信任边界。</para>
  22. ///
  23. /// <para>现在租户一律由 <see cref="S8TrustedScopeResolver"/> 从认证身份解析
  24. /// (最终落到 <see cref="AidopTenantScope.ResolveOrThrow"/>,只信 JWT),
  25. /// 与 S8 其余 Controller <b>同一个</b> authority,不新增第二套租户解析。
  26. /// query 上的 <c>tenantId</c> / <c>factoryId</c> 仅保留形参以兼容既有调用方,
  27. /// <b>不承担任何安全边界作用</b>,显式丢弃。</para>
  28. ///
  29. /// <para><b>设计目标</b>:即使 <c>DebugEndpointEnabled</c> 被误开到生产,
  30. /// 本入口也<b>不可能</b>跨租户 —— 开关只决定「能不能用」,不再决定「能动谁的数据」。</para>
  31. ///
  32. /// <para><b>超管口径</b>:沿用平台既有策略,不在此另开全租户调试权。
  33. /// <see cref="AidopTenantScope.ResolveOrThrow"/> 对「超管 + 未选目标租户」直接抛异常;
  34. /// 超管须先切到目标租户(平台既有机制),切换后即以该租户身份执行。</para>
  35. /// </summary>
  36. [ApiController]
  37. [Route("api/aidop/s8/watch-debug")]
  38. [NonUnify]
  39. public class AdoS8WatchDebugController : ControllerBase
  40. {
  41. private readonly S8WatchSchedulerService _svc;
  42. private readonly S8TrustedScopeResolver _scope;
  43. private readonly bool _debugEndpointEnabled;
  44. public AdoS8WatchDebugController(
  45. S8WatchSchedulerService svc,
  46. S8TrustedScopeResolver scope,
  47. IConfiguration configuration)
  48. {
  49. _svc = svc;
  50. _scope = scope;
  51. _debugEndpointEnabled = configuration.GetValue("WatchScheduler:DebugEndpointEnabled", false);
  52. }
  53. [HttpPost("run-once")]
  54. [S8Permission(S8PermissionCatalog.DebugRun)]
  55. public async Task<IActionResult> RunOnceAsync(
  56. [FromQuery] long tenantId = 1,
  57. [FromQuery] long factoryId = 1)
  58. {
  59. if (!_debugEndpointEnabled) return NotFound();
  60. // S8-DEBUG-TENANT-GUARD-1:兼容形参,安全作用域一律服务端解析。
  61. _ = tenantId; _ = factoryId;
  62. var scope = await _scope.ResolveAsync();
  63. var results = await _svc.CreateExceptionsAsync(scope.TenantId);
  64. return Ok(new
  65. {
  66. count = results.Count,
  67. results = results.Select(r => new
  68. {
  69. created = r.Created,
  70. skipped = r.Skipped,
  71. createdExceptionId = r.CreatedExceptionId,
  72. reason = r.Reason,
  73. errorMessage = r.ErrorMessage,
  74. sourceRuleId = r.DedupResult.Hit.SourceRuleId,
  75. relatedObjectCode = r.DedupResult.Hit.RelatedObjectCode,
  76. matchedExceptionId = r.DedupResult.MatchedExceptionId
  77. })
  78. });
  79. }
  80. /// <summary>
  81. /// S2-EW:仅供 e2e fixture 使用的 TEST_G09_ 数据软删入口。
  82. /// 安全契约:
  83. /// - 与 run-once 共用 _debugEndpointEnabled;未启用时 404。
  84. /// - S8-DEBUG-TENANT-GUARD-1:租户由认证身份解析,<b>不再</b>取 query 的 tenantId。
  85. /// 原先「tenantId 必须显式传入、缺参 400」是当时唯一的防护手段(逼调用方自己写对租户);
  86. /// 现在边界由服务端盖章,该 400 既不再提供安全价值,又会要求调用方传一个已被忽略的参数,
  87. /// 故一并移除;形参保留仅为兼容既有调用方。
  88. /// - 前缀 TEST_G09_ 在 service 端硬编码,本接口不接受 prefix 入参。
  89. /// - 仅 soft delete(SET IsDeleted=true),不触发物理 DELETE。
  90. /// </summary>
  91. [HttpDelete("test-cleanup")]
  92. [S8Permission(S8PermissionCatalog.DebugRun)]
  93. public async Task<IActionResult> TestCleanupAsync(
  94. [FromQuery] long? tenantId,
  95. [FromServices] S8ExceptionService exceptionSvc)
  96. {
  97. if (!_debugEndpointEnabled) return NotFound();
  98. // S8-DEBUG-TENANT-GUARD-1:兼容形参,安全作用域一律服务端解析。
  99. _ = tenantId;
  100. var scope = await _scope.ResolveAsync();
  101. var deletedCount = await exceptionSvc.SoftDeleteTestPrefixAsync(scope.TenantId);
  102. return Ok(new { deletedCount });
  103. }
  104. }