| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115 |
- 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;
- /// <summary>
- /// S0 主数据维度供给层的运维端点:source → <c>mdp_stg_s0_*</c> → <c>dim_*</c>。
- ///
- /// 选址说明(刻意不挂看板控制器):
- /// <list type="bullet">
- /// <item><c>AidopKanbanController</c> 的 <c>mdp/refresh/{moduleCode}</c> 是 S1–S7 的硬编码 switch,
- /// 且该文件是多模块共享文件;</item>
- /// <item><c>MdpRebuildScope</c> 的模块白名单为 S1–S7/S9,其阶段模型含 DWD/KPI,S0 没有这两级;</item>
- /// <item>看板控制器用的是 <c>AidopTenantScope</c>,与 S0 的 <see cref="AdoS0TenantScope"/> 是两套租户契约。</item>
- /// </list>
- ///
- /// 租户:一律经 <see cref="AdoS0TenantScope.TryResolveRequired"/> 解析(**无默认回落**,
- /// 超管未选目标租户直接拒绝)。本控制器**不读也不写任何 S0 业务主表**,只读源、写 stg/dim。
- ///
- /// 授权:<see cref="AdoS0DimOpsAuthorizeAttribute"/>。**不可删** ——
- /// 平台 <c>JwtHandler</c> 对未登记到 <c>SysMenu</c> 的路由默认放行,
- /// 去掉这道门即等于「登录即可触发本租户 4 张 dim 表的 DELETE + 全量重建」。
- /// </summary>
- [ApiController]
- [Route("api/s0/dim")]
- [Authorize]
- [AdoS0DimOpsAuthorize]
- [NonUnify]
- public class AdoS0DimRefreshController : ControllerBase
- {
- private readonly S0DimRefreshService _service;
- /// <summary>构造。</summary>
- public AdoS0DimRefreshController(S0DimRefreshService service) => _service = service;
- /// <summary>刷新当前租户的全部 S0 维度(FULL replace)。</summary>
- [HttpPost("refresh")]
- public Task<IActionResult> RefreshAllAsync(CancellationToken cancellationToken)
- => RefreshCoreAsync(null, cancellationToken);
- /// <summary>刷新当前租户的单个维度。key ∈ WORK_CENTER | DEPARTMENT | LOCATION | LOCATION_SHELF。</summary>
- [HttpPost("refresh/{key}")]
- public Task<IActionResult> RefreshOneAsync(string key, CancellationToken cancellationToken)
- => RefreshCoreAsync(key, cancellationToken);
- /// <summary>只读对账(不改数据)。key 省略表示全部。</summary>
- [HttpGet("reconcile")]
- public async Task<IActionResult> 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);
- }
- }
- /// <summary>当前租户最近一次刷新的运行记录。</summary>
- [HttpGet("last-run")]
- public async Task<IActionResult> 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 });
- }
- /// <summary>
- /// 维度清单(只读元数据,便于运维核对映射口径)。
- ///
- /// 本 action 不访问任何数据,但仍解析租户:一是它暴露的是映射口径(源表名 / JSON 路径 /
- /// 业务键),属运维视角的结构信息;二是 <c>S0ControllerTenantClosureTests</c> 要求 S0 控制器
- /// 每个 action 都显式解析租户,豁免需登记进共享白名单并给出评审理由 —— 与其扩白名单,
- /// 不如让这个运维接口也要求一个已解析的运维上下文(代价为零,口径更一致)。
- /// </summary>
- [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<IActionResult> 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);
- }
- }
- }
|