using Admin.NET.Plugin.AiDOP.DataPlatform.S0Dim;
using Admin.NET.Plugin.AiDOP.Infrastructure;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Admin.NET.Plugin.AiDOP.Controllers.S0;
///
/// S0 主数据维度供给层的运维端点:source → mdp_stg_s0_* → dim_*。
///
/// 选址说明(刻意不挂看板控制器):
///
/// - AidopKanbanController 的 mdp/refresh/{moduleCode} 是 S1–S7 的硬编码 switch,
/// 且该文件是多模块共享文件;
/// - MdpRebuildScope 的模块白名单为 S1–S7/S9,其阶段模型含 DWD/KPI,S0 没有这两级;
/// - 看板控制器用的是 AidopTenantScope,与 S0 的 是两套租户契约。
///
///
/// 租户:一律经 解析(**无默认回落**,
/// 超管未选目标租户直接拒绝)。本控制器**不读也不写任何 S0 业务主表**,只读源、写 stg/dim。
///
/// 授权:。**不可删** ——
/// 平台 JwtHandler 对未登记到 SysMenu 的路由默认放行,
/// 去掉这道门即等于「登录即可触发本租户 4 张 dim 表的 DELETE + 全量重建」。
///
[ApiController]
[Route("api/s0/dim")]
[Authorize]
[AdoS0DimOpsAuthorize]
[NonUnify]
public class AdoS0DimRefreshController : ControllerBase
{
private readonly S0DimRefreshService _service;
/// 构造。
public AdoS0DimRefreshController(S0DimRefreshService service) => _service = service;
/// 刷新当前租户的全部 S0 维度(FULL replace)。
[HttpPost("refresh")]
public Task RefreshAllAsync(CancellationToken cancellationToken)
=> RefreshCoreAsync(null, cancellationToken);
/// 刷新当前租户的单个维度。key ∈ WORK_CENTER | DEPARTMENT | LOCATION | LOCATION_SHELF。
[HttpPost("refresh/{key}")]
public Task RefreshOneAsync(string key, CancellationToken cancellationToken)
=> RefreshCoreAsync(key, cancellationToken);
/// 只读对账(不改数据)。key 省略表示全部。
[HttpGet("reconcile")]
public async Task ReconcileAsync([FromQuery] string? key, CancellationToken cancellationToken)
{
if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
try
{
var list = await _service.ReconcileAsync(tenantId, key, cancellationToken);
return Ok(new { tenantId = tenantId.ToString(), items = list });
}
catch (InvalidOperationException ex)
{
return AdoS0ApiErrors.InvalidRequest(ex.Message);
}
}
/// 当前租户最近一次刷新的运行记录。
[HttpGet("last-run")]
public async Task LastRunAsync(CancellationToken cancellationToken)
{
if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
var row = await _service.LastRunAsync(tenantId, cancellationToken);
return Ok(new { tenantId = tenantId.ToString(), lastRun = row });
}
///
/// 维度清单(只读元数据,便于运维核对映射口径)。
///
/// 本 action 不访问任何数据,但仍解析租户:一是它暴露的是映射口径(源表名 / JSON 路径 /
/// 业务键),属运维视角的结构信息;二是 S0ControllerTenantClosureTests 要求 S0 控制器
/// 每个 action 都显式解析租户,豁免需登记进共享白名单并给出评审理由 —— 与其扩白名单,
/// 不如让这个运维接口也要求一个已解析的运维上下文(代价为零,口径更一致)。
///
[HttpGet("catalog")]
public IActionResult Catalog()
{
if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
S0DimCatalog.ValidateAll();
_ = tenantId; // 仅用于确保调用方具备已解析的租户上下文,清单内容本身与租户无关
return Ok(S0DimCatalog.All.Select(d => new
{
d.Key,
d.EntityCode,
d.SourceTable,
d.SourceSystem,
d.StagingTable,
d.DimTable,
BusinessKey = d.BusinessKeyColumns,
SourceBizKey = d.SourceBizKeyColumns,
Columns = d.Columns.Select(c => new { c.TargetColumn, Kind = c.Kind.ToString(), c.JsonPath, c.Required })
}));
}
private async Task RefreshCoreAsync(string? key, CancellationToken cancellationToken)
{
if (!AdoS0TenantScope.TryResolveRequired(out var tenantId, out var tenantError)) return tenantError!;
try
{
var result = await _service.RefreshAsync(tenantId, key, "MANUAL", cancellationToken);
// 刷新失败不抛 5xx:结果体里已带逐维度 status / error / 对账明细,便于运维直接读
return Ok(result);
}
catch (InvalidOperationException ex)
{
return AdoS0ApiErrors.InvalidRequest(ex.Message);
}
}
}