using Microsoft.Extensions.Logging;
namespace Admin.NET.Plugin.AiDOP.DataPlatform;
///
/// 运行期核对中立层必填列是否整列为空。
///
/// 是「投影 SQL 有没有写这一列」的静态守卫,守不住
/// 「写了但取到的全是 NULL」和「这批存量是加列之前物化的」两种情况。而整列为空不会报错,
/// 只会让依赖它的 KPI 一起算出 NO_DATA —— 2026-09 S5/S7 全租户掉数就是这么发生的。
/// 故物化之后必须实测一次,把整列为空变成一条可查询的告警。
///
///
public sealed class NeutralRequiredColumnsMonitor : ITransient
{
private readonly ISqlSugarClient _db;
private readonly ILogger _logger;
public NeutralRequiredColumnsMonitor(ISqlSugarClient db, ILoggerFactory loggerFactory)
{
_db = db;
_logger = loggerFactory.CreateLogger(nameof(NeutralRequiredColumnsMonitor));
}
/// 返回该租户该表里「有行但整列为空」的必填列名。顺带落 mdp_source_gate_log 与错误日志。
public async Task> AssertAsync(
long tenantId, string stdObject, string table, string? batchId, CancellationToken cancellationToken = default)
{
if (tenantId <= 0) return [];
var required = NeutralRequiredColumns.For(stdObject).Select(c => c.Name).ToList();
if (required.Count == 0) return [];
cancellationToken.ThrowIfCancellationRequested();
var present = await _db.Ado.SqlQueryAsync(
"""
SELECT COLUMN_NAME FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=@table
""",
new { table });
var columns = required
.Where(c => present.Contains(c, StringComparer.OrdinalIgnoreCase))
.ToList();
if (columns.Count == 0) return [];
// 列名取自 NeutralRequiredColumns 常量,不来自外部输入,故可直接拼进 SQL。
var counters = string.Join(", ", columns.Select(c => $"SUM(`{c}` IS NOT NULL) AS `{c}`"));
var stats = await _db.Ado.GetDataTableAsync(
$"SELECT COUNT(*) AS `__total`, {counters} FROM `{table}` WHERE tenant_id=@tenant",
new { tenant = tenantId });
if (stats.Rows.Count == 0) return [];
var row = stats.Rows[0];
if (Convert.ToInt64(row["__total"]) <= 0) return [];
var empty = columns
.Where(c => row[c] == DBNull.Value || Convert.ToInt64(row[c]) == 0)
.ToList();
if (empty.Count == 0) return empty;
_logger.LogError(
"中立层必填列整列为空 tenant={Tenant} object={Object} table={Table} columns={Columns}",
tenantId, stdObject, table, string.Join(",", empty));
await _db.Ado.ExecuteCommandAsync(
"""
INSERT INTO mdp_source_gate_log
(tenant_id, std_object, source_system, gate_reason, row_count, sample_keys, sync_batch_id)
VALUES (@tenant, @obj, '', 'REQUIRED_COLUMN_EMPTY', @rows, @sample, @batch)
""",
new
{
tenant = tenantId,
obj = stdObject,
rows = empty.Count,
sample = string.Join(",", empty),
batch = batchId ?? ""
});
return empty;
}
}