AdoS0DimRefreshController.cs 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. using Admin.NET.Plugin.AiDOP.DataPlatform.S0Dim;
  2. using Admin.NET.Plugin.AiDOP.Infrastructure;
  3. using Microsoft.AspNetCore.Authorization;
  4. using Microsoft.AspNetCore.Mvc;
  5. namespace Admin.NET.Plugin.AiDOP.Controllers.S0;
  6. /// <summary>
  7. /// S0 主数据维度供给层的运维端点:source → <c>mdp_stg_s0_*</c> → <c>dim_*</c>。
  8. ///
  9. /// 选址说明(刻意不挂看板控制器):
  10. /// <list type="bullet">
  11. /// <item><c>AidopKanbanController</c> 的 <c>mdp/refresh/{moduleCode}</c> 是 S1–S7 的硬编码 switch,
  12. /// 且该文件是多模块共享文件;</item>
  13. /// <item><c>MdpRebuildScope</c> 的模块白名单为 S1–S7/S9,其阶段模型含 DWD/KPI,S0 没有这两级;</item>
  14. /// <item>看板控制器用的是 <c>AidopTenantScope</c>,与 S0 的 <see cref="AdoS0TenantScope"/> 是两套租户契约。</item>
  15. /// </list>
  16. ///
  17. /// 租户:一律经 <see cref="AdoS0TenantScope.TryResolveRequired"/> 解析(**无默认回落**,
  18. /// 超管未选目标租户直接拒绝)。本控制器**不读也不写任何 S0 业务主表**,只读源、写 stg/dim。
  19. ///
  20. /// 授权:<see cref="AdoS0DimOpsAuthorizeAttribute"/>。**不可删** ——
  21. /// 平台 <c>JwtHandler</c> 对未登记到 <c>SysMenu</c> 的路由默认放行,
  22. /// 去掉这道门即等于「登录即可触发本租户 4 张 dim 表的 DELETE + 全量重建」。
  23. /// </summary>
  24. [ApiController]
  25. [Route("api/s0/dim")]
  26. [Authorize]
  27. [AdoS0DimOpsAuthorize]
  28. [NonUnify]
  29. public class AdoS0DimRefreshController : ControllerBase
  30. {
  31. private readonly S0DimRefreshService _service;
  32. /// <summary>构造。</summary>
  33. public AdoS0DimRefreshController(S0DimRefreshService service) => _service = service;
  34. /// <summary>刷新当前租户的全部 S0 维度(FULL replace)。</summary>
  35. [HttpPost("refresh")]
  36. public Task<IActionResult> RefreshAllAsync(CancellationToken cancellationToken)
  37. => RefreshCoreAsync(null, cancellationToken);
  38. /// <summary>刷新当前租户的单个维度。key ∈ WORK_CENTER | DEPARTMENT | LOCATION | LOCATION_SHELF。</summary>
  39. [HttpPost("refresh/{key}")]
  40. public Task<IActionResult> RefreshOneAsync(string key, CancellationToken cancellationToken)
  41. => RefreshCoreAsync(key, cancellationToken);
  42. /// <summary>只读对账(不改数据)。key 省略表示全部。</summary>
  43. [HttpGet("reconcile")]
  44. public async Task<IActionResult> ReconcileAsync([FromQuery] string? key, CancellationToken cancellationToken)
  45. {
  46. if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
  47. try
  48. {
  49. var list = await _service.ReconcileAsync(tenantId, key, cancellationToken);
  50. return Ok(new { tenantId = tenantId.ToString(), items = list });
  51. }
  52. catch (InvalidOperationException ex)
  53. {
  54. return AdoS0ApiErrors.InvalidRequest(ex.Message);
  55. }
  56. }
  57. /// <summary>当前租户最近一次刷新的运行记录。</summary>
  58. [HttpGet("last-run")]
  59. public async Task<IActionResult> LastRunAsync(CancellationToken cancellationToken)
  60. {
  61. if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
  62. var row = await _service.LastRunAsync(tenantId, cancellationToken);
  63. return Ok(new { tenantId = tenantId.ToString(), lastRun = row });
  64. }
  65. /// <summary>
  66. /// 维度清单(只读元数据,便于运维核对映射口径)。
  67. ///
  68. /// 本 action 不访问任何数据,但仍解析租户:一是它暴露的是映射口径(源表名 / JSON 路径 /
  69. /// 业务键),属运维视角的结构信息;二是 <c>S0ControllerTenantClosureTests</c> 要求 S0 控制器
  70. /// 每个 action 都显式解析租户,豁免需登记进共享白名单并给出评审理由 —— 与其扩白名单,
  71. /// 不如让这个运维接口也要求一个已解析的运维上下文(代价为零,口径更一致)。
  72. /// </summary>
  73. [HttpGet("catalog")]
  74. public IActionResult Catalog()
  75. {
  76. if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
  77. S0DimCatalog.ValidateAll();
  78. _ = tenantId; // 仅用于确保调用方具备已解析的租户上下文,清单内容本身与租户无关
  79. return Ok(S0DimCatalog.All.Select(d => new
  80. {
  81. d.Key,
  82. d.EntityCode,
  83. d.SourceTable,
  84. d.SourceSystem,
  85. d.StagingTable,
  86. d.DimTable,
  87. BusinessKey = d.BusinessKeyColumns,
  88. SourceBizKey = d.SourceBizKeyColumns,
  89. Columns = d.Columns.Select(c => new { c.TargetColumn, Kind = c.Kind.ToString(), c.JsonPath, c.Required })
  90. }));
  91. }
  92. private async Task<IActionResult> RefreshCoreAsync(string? key, CancellationToken cancellationToken)
  93. {
  94. if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
  95. try
  96. {
  97. var result = await _service.RefreshAsync(tenantId, key, "MANUAL", cancellationToken);
  98. // 刷新失败不抛 5xx:结果体里已带逐维度 status / error / 对账明细,便于运维直接读
  99. return Ok(result);
  100. }
  101. catch (InvalidOperationException ex)
  102. {
  103. return AdoS0ApiErrors.InvalidRequest(ex.Message);
  104. }
  105. }
  106. }