using Admin.NET.Plugin.AiDOP.Infrastructure.S8;
using Admin.NET.Plugin.AiDOP.Const.S8;
using Admin.NET.Plugin.AiDOP.Infrastructure;
using Admin.NET.Plugin.AiDOP.Service.S8;
using Microsoft.Extensions.Configuration;
namespace Admin.NET.Plugin.AiDOP.Controllers.S8;
///
/// G-01 最小补验内部入口(非正式调度接入点)。
/// 仅用于触发 S8 自动监控 MVP 主链一次,供端到端联调与验收取证;
/// 不对外暴露业务语义,不作为长期 API。
/// G08-06:移除匿名访问;由 appsettings 的 `WatchScheduler:DebugEndpointEnabled` 控制是否启用,
/// 未启用时所有 Action 返回 404(语义等价于"未注册")。
///
/// S8-DEBUG-TENANT-GUARD-1(2026-09-20):租户边界收口。
/// 修复前本 Controller 是 S8 里唯一把 tenantId 当作执行上下文的入口:
/// run-once 直接把 query 里的 tenantId(默认值 1)传给
/// ,于是调用方可以
/// 在任意租户下建单,并从出参 createdExceptionId / relatedObjectCode /
/// matchedExceptionId 读回该租户的业务事实;test-cleanup 同理可软删任意租户的
/// TEST_G09_ 数据。两者都只被 DebugEndpointEnabled 这一个功能开关挡着 ——
/// 而功能开关不是信任边界。
///
/// 现在租户一律由 从认证身份解析
/// (最终落到 ,只信 JWT),
/// 与 S8 其余 Controller 同一个 authority,不新增第二套租户解析。
/// query 上的 tenantId / factoryId 仅保留形参以兼容既有调用方,
/// 不承担任何安全边界作用,显式丢弃。
///
/// 设计目标:即使 DebugEndpointEnabled 被误开到生产,
/// 本入口也不可能跨租户 —— 开关只决定「能不能用」,不再决定「能动谁的数据」。
///
/// 超管口径:沿用平台既有策略,不在此另开全租户调试权。
/// 对「超管 + 未选目标租户」直接抛异常;
/// 超管须先切到目标租户(平台既有机制),切换后即以该租户身份执行。
///
[ApiController]
[Route("api/aidop/s8/watch-debug")]
[NonUnify]
public class AdoS8WatchDebugController : ControllerBase
{
private readonly S8WatchSchedulerService _svc;
private readonly S8TrustedScopeResolver _scope;
private readonly bool _debugEndpointEnabled;
public AdoS8WatchDebugController(
S8WatchSchedulerService svc,
S8TrustedScopeResolver scope,
IConfiguration configuration)
{
_svc = svc;
_scope = scope;
_debugEndpointEnabled = configuration.GetValue("WatchScheduler:DebugEndpointEnabled", false);
}
[HttpPost("run-once")]
[S8Permission(S8PermissionCatalog.DebugRun)]
public async Task RunOnceAsync(
[FromQuery] long tenantId = 1,
[FromQuery] long factoryId = 1)
{
if (!_debugEndpointEnabled) return NotFound();
// S8-DEBUG-TENANT-GUARD-1:兼容形参,安全作用域一律服务端解析。
_ = tenantId; _ = factoryId;
var scope = await _scope.ResolveAsync();
var results = await _svc.CreateExceptionsAsync(scope.TenantId);
return Ok(new
{
count = results.Count,
results = results.Select(r => new
{
created = r.Created,
skipped = r.Skipped,
createdExceptionId = r.CreatedExceptionId,
reason = r.Reason,
errorMessage = r.ErrorMessage,
sourceRuleId = r.DedupResult.Hit.SourceRuleId,
relatedObjectCode = r.DedupResult.Hit.RelatedObjectCode,
matchedExceptionId = r.DedupResult.MatchedExceptionId
})
});
}
///
/// S2-EW:仅供 e2e fixture 使用的 TEST_G09_ 数据软删入口。
/// 安全契约:
/// - 与 run-once 共用 _debugEndpointEnabled;未启用时 404。
/// - S8-DEBUG-TENANT-GUARD-1:租户由认证身份解析,不再取 query 的 tenantId。
/// 原先「tenantId 必须显式传入、缺参 400」是当时唯一的防护手段(逼调用方自己写对租户);
/// 现在边界由服务端盖章,该 400 既不再提供安全价值,又会要求调用方传一个已被忽略的参数,
/// 故一并移除;形参保留仅为兼容既有调用方。
/// - 前缀 TEST_G09_ 在 service 端硬编码,本接口不接受 prefix 入参。
/// - 仅 soft delete(SET IsDeleted=true),不触发物理 DELETE。
///
[HttpDelete("test-cleanup")]
[S8Permission(S8PermissionCatalog.DebugRun)]
public async Task TestCleanupAsync(
[FromQuery] long? tenantId,
[FromServices] S8ExceptionService exceptionSvc)
{
if (!_debugEndpointEnabled) return NotFound();
// S8-DEBUG-TENANT-GUARD-1:兼容形参,安全作用域一律服务端解析。
_ = tenantId;
var scope = await _scope.ResolveAsync();
var deletedCount = await exceptionSvc.SoftDeleteTestPrefixAsync(scope.TenantId);
return Ok(new { deletedCount });
}
}