using System.Diagnostics;
using Admin.NET.Core;
using SqlSugar;
namespace Admin.NET.Plugin.AiDOP.Infrastructure;
///
/// 一次性迁移:将历史 TenantId=1 的 AiDOP 业务数据更新为框架默认租户。
/// 幂等——若已无 TenantId=1 的行则不做任何事。
///
public static class AidopTenantMigration
{
private static readonly (string Table, string TenantColumn)[] Tables =
{
("ado_smart_ops_kpi_master", "TenantId"),
("ado_smart_ops_layout_item", "TenantId"),
("ado_smart_ops_home_module", "TenantId"),
("ado_s9_kpi_value_l1_day", "tenant_id"),
("ado_s9_kpi_value_l2_day", "tenant_id"),
("ado_s9_kpi_value_l3_day", "tenant_id"),
("ado_s9_kpi_value_l4_day", "tenant_id"),
("ado_smart_ops_kpi_atomic_day", "tenant_id")
};
public static void MigrateOldTenantId(ISqlSugarClient db)
{
EnsureKpiValueL4Table(db);
EnsureKpiAtomicTable(db);
var target = SqlSugarConst.DefaultTenantId;
if (target == 1) return;
foreach (var (table, tenantColumn) in Tables)
{
try
{
db.Ado.ExecuteCommand(
$"UPDATE `{table}` SET `{tenantColumn}` = @target WHERE `{tenantColumn}` = 1",
new { target });
}
catch (Exception ex)
{
Trace.TraceWarning($"AidopTenantMigration [{table}]: {ex.Message}");
}
}
DropOldUniqueIndex(db);
BackfillThresholds(db);
}
public static void EnsureKpiValueL4Table(ISqlSugarClient db)
{
try
{
db.Ado.ExecuteCommand(
"CREATE TABLE IF NOT EXISTS `ado_s9_kpi_value_l4_day` LIKE `ado_s9_kpi_value_l3_day`");
}
catch (Exception ex)
{
Trace.TraceWarning($"AidopTenantMigration ensure L4 value table: {ex.Message}");
}
}
public static void EnsureKpiAtomicTable(ISqlSugarClient db)
{
RepairKpiAtomicTableSchemaIfNeeded(db);
try
{
db.Ado.ExecuteCommand(
"""
CREATE TABLE IF NOT EXISTS ado_smart_ops_kpi_atomic_day (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
tenant_id BIGINT NOT NULL COMMENT '租户ID',
factory_id BIGINT NOT NULL DEFAULT 1 COMMENT '工厂ID',
domain_code VARCHAR(50) NOT NULL COMMENT '业务对象域',
metric_code VARCHAR(50) NOT NULL COMMENT '指标编码',
metric_level TINYINT NOT NULL DEFAULT 1 COMMENT '指标层级',
stat_date DATE NOT NULL COMMENT '统计日期',
customer_code VARCHAR(100) NOT NULL DEFAULT '' COMMENT '客户编码',
product_code VARCHAR(100) NOT NULL DEFAULT '' COMMENT '产品编码',
order_no VARCHAR(100) NOT NULL DEFAULT '' COMMENT '订单号',
production_line VARCHAR(100) NOT NULL DEFAULT '' COMMENT '产线',
supplier_code VARCHAR(100) NOT NULL DEFAULT '' COMMENT '供应商编码',
material_code VARCHAR(100) NOT NULL DEFAULT '' COMMENT '物料编码',
po_no VARCHAR(100) NOT NULL DEFAULT '' COMMENT '采购单号',
warehouse_code VARCHAR(100) NOT NULL DEFAULT '' COMMENT '仓库编码',
work_order_no VARCHAR(100) NOT NULL DEFAULT '' COMMENT '工单号',
equipment_code VARCHAR(100) NOT NULL DEFAULT '' COMMENT '设备编码',
outbound_no VARCHAR(100) NOT NULL DEFAULT '' COMMENT '出库单号',
sum_value DECIMAL(18, 6) NULL COMMENT '可累加值',
numerator_value DECIMAL(18, 6) NULL COMMENT '比率分子',
denominator_value DECIMAL(18, 6) NULL COMMENT '比率分母',
sample_count INT NULL COMMENT '样本数',
min_value DECIMAL(18, 6) NULL COMMENT '最小值',
max_value DECIMAL(18, 6) NULL COMMENT '最大值',
batch_id VARCHAR(100) NULL COMMENT 'MDP批次',
calc_time DATETIME(3) NULL COMMENT '计算时间',
grain VARCHAR(20) NOT NULL DEFAULT 'day' COMMENT '统计粒度',
is_deleted TINYINT NOT NULL DEFAULT 0 COMMENT '删除标识',
PRIMARY KEY (id),
UNIQUE KEY uk_atomic_grain (
tenant_id, factory_id, domain_code, metric_code, stat_date,
customer_code, product_code, order_no, production_line,
supplier_code, material_code, po_no, warehouse_code,
work_order_no, equipment_code, outbound_no
),
KEY idx_tenant_domain_metric_date (tenant_id, domain_code, metric_code, stat_date),
KEY idx_tenant_module_lookup (tenant_id, factory_id, metric_code, stat_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='KPI原子聚合层(日)'
""");
EnsureKpiAtomicColumnDefaults(db);
}
catch (Exception ex)
{
Trace.TraceWarning($"AidopTenantMigration ensure KPI atomic table: {ex.Message}");
}
}
///
/// 补齐维度列 DEFAULT '',避免 CodeFirst 误建表后 INSERT 缺列报错。
///
private static void EnsureKpiAtomicColumnDefaults(ISqlSugarClient db)
{
try
{
var columns = db.Ado.SqlQuery(
"""
SELECT COLUMN_NAME AS Name, COLUMN_DEFAULT AS DefaultValue, IS_NULLABLE AS IsNullable
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='ado_smart_ops_kpi_atomic_day'
""");
var clauses = BuildKpiAtomicDefaultAlterClauses(columns);
if (clauses.Count == 0) return;
db.Ado.ExecuteCommand(
$"ALTER TABLE ado_smart_ops_kpi_atomic_day {string.Join(", ", clauses)}");
}
catch (Exception ex)
{
Trace.TraceWarning($"AidopTenantMigration ensure KPI atomic column defaults: {ex.Message}");
}
}
internal static IReadOnlyList BuildKpiAtomicDefaultAlterClauses(
IEnumerable columns)
{
var actual = columns.ToDictionary(x => x.Name, StringComparer.OrdinalIgnoreCase);
var expected = new (string Name, string DefaultValue, string Clause)[]
{
("customer_code", "", "MODIFY customer_code VARCHAR(100) NOT NULL DEFAULT '' COMMENT '客户编码'"),
("product_code", "", "MODIFY product_code VARCHAR(100) NOT NULL DEFAULT '' COMMENT '产品编码'"),
("order_no", "", "MODIFY order_no VARCHAR(100) NOT NULL DEFAULT '' COMMENT '订单号'"),
("production_line", "", "MODIFY production_line VARCHAR(100) NOT NULL DEFAULT '' COMMENT '产线'"),
("supplier_code", "", "MODIFY supplier_code VARCHAR(100) NOT NULL DEFAULT '' COMMENT '供应商编码'"),
("material_code", "", "MODIFY material_code VARCHAR(100) NOT NULL DEFAULT '' COMMENT '物料编码'"),
("po_no", "", "MODIFY po_no VARCHAR(100) NOT NULL DEFAULT '' COMMENT '采购单号'"),
("warehouse_code", "", "MODIFY warehouse_code VARCHAR(100) NOT NULL DEFAULT '' COMMENT '仓库编码'"),
("work_order_no", "", "MODIFY work_order_no VARCHAR(100) NOT NULL DEFAULT '' COMMENT '工单号'"),
("equipment_code", "", "MODIFY equipment_code VARCHAR(100) NOT NULL DEFAULT '' COMMENT '设备编码'"),
("outbound_no", "", "MODIFY outbound_no VARCHAR(100) NOT NULL DEFAULT '' COMMENT '出库单号'"),
("grain", "day", "MODIFY grain VARCHAR(20) NOT NULL DEFAULT 'day' COMMENT '统计粒度'"),
("is_deleted", "0", "MODIFY is_deleted TINYINT NOT NULL DEFAULT 0 COMMENT '删除标识'")
};
return expected
.Where(item =>
!actual.TryGetValue(item.Name, out var column) ||
!string.Equals(column.IsNullable, "NO", StringComparison.OrdinalIgnoreCase) ||
!string.Equals(column.DefaultValue ?? "", item.DefaultValue, StringComparison.OrdinalIgnoreCase))
.Select(item => item.Clause)
.ToList();
}
///
/// 早期 SqlSugar 误建 PascalCase 列名时,DROP 后由 EnsureKpiAtomicTable 按 snake_case 重建。
///
private static void RepairKpiAtomicTableSchemaIfNeeded(ISqlSugarClient db)
{
try
{
var columns = db.Ado.SqlQuery(
"""
SELECT COLUMN_NAME
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'ado_smart_ops_kpi_atomic_day'
""");
if (columns == null || columns.Count == 0)
return;
var hasSnakeDomain = columns.Any(c => string.Equals(c, "domain_code", StringComparison.OrdinalIgnoreCase));
var hasPascalDomain = columns.Any(c => string.Equals(c, "DomainCode", StringComparison.OrdinalIgnoreCase));
if (hasSnakeDomain || !hasPascalDomain)
return;
Trace.TraceWarning("AidopTenantMigration: ado_smart_ops_kpi_atomic_day 检测到 PascalCase 列名,将 DROP 后重建为 snake_case。");
db.Ado.ExecuteCommand("DROP TABLE ado_smart_ops_kpi_atomic_day");
}
catch (Exception ex)
{
Trace.TraceWarning($"AidopTenantMigration repair KPI atomic schema: {ex.Message}");
}
}
private static void BackfillThresholds(ISqlSugarClient db)
{
try
{
db.Ado.ExecuteCommand(
"UPDATE ado_smart_ops_kpi_master SET YellowThreshold=95, RedThreshold=80 " +
"WHERE YellowThreshold IS NULL AND Direction='higher_is_better'");
db.Ado.ExecuteCommand(
"UPDATE ado_smart_ops_kpi_master SET YellowThreshold=110, RedThreshold=120 " +
"WHERE YellowThreshold IS NULL AND Direction='lower_is_better'");
}
catch (Exception ex)
{
Trace.TraceWarning($"AidopTenantMigration backfill thresholds: {ex.Message}");
}
}
private static void DropOldUniqueIndex(ISqlSugarClient db)
{
try
{
var rows = db.Ado.SqlQuery(
"SHOW INDEX FROM ado_smart_ops_kpi_master WHERE Key_name = 'uk_kpi_master_code'");
if (rows != null && rows.Count > 0)
db.Ado.ExecuteCommand("ALTER TABLE ado_smart_ops_kpi_master DROP INDEX uk_kpi_master_code");
}
catch (Exception ex)
{
Trace.TraceWarning($"AidopTenantMigration drop old index: {ex.Message}");
}
}
}
internal sealed class KpiAtomicColumnState
{
public string Name { get; set; } = string.Empty;
public string? DefaultValue { get; set; }
public string IsNullable { get; set; } = string.Empty;
}