|
|
@@ -2,6 +2,7 @@ using Admin.NET.Core;
|
|
|
using Admin.NET.Plugin.AiDOP.Entity;
|
|
|
using Admin.NET.Plugin.AiDOP.Infrastructure;
|
|
|
using Admin.NET.Plugin.AiDOP.Production;
|
|
|
+using Admin.NET.Plugin.AiDOP.Supply;
|
|
|
using SqlSugar;
|
|
|
|
|
|
namespace Admin.NET.Plugin.AiDOP.Controllers;
|
|
|
@@ -14,11 +15,13 @@ public partial class AidopKanbanController : ControllerBase
|
|
|
{
|
|
|
private readonly ISqlSugarClient _db;
|
|
|
private readonly S2MdpSyncTransformService _s2MdpSyncTransformService;
|
|
|
+ private readonly S3MdpSyncTransformService _s3MdpSyncTransformService;
|
|
|
|
|
|
- public AidopKanbanController(ISqlSugarClient db, S2MdpSyncTransformService s2MdpSyncTransformService)
|
|
|
+ public AidopKanbanController(ISqlSugarClient db, S2MdpSyncTransformService s2MdpSyncTransformService, S3MdpSyncTransformService s3MdpSyncTransformService)
|
|
|
{
|
|
|
_db = db;
|
|
|
_s2MdpSyncTransformService = s2MdpSyncTransformService;
|
|
|
+ _s3MdpSyncTransformService = s3MdpSyncTransformService;
|
|
|
}
|
|
|
|
|
|
[HttpGet("home-l1")]
|
|
|
@@ -146,6 +149,7 @@ LIMIT 60
|
|
|
var decomposition = new List<S2DecompositionDto>();
|
|
|
var trend = new List<S2TrendDto>();
|
|
|
var distribution = new List<S2DistributionDto>();
|
|
|
+ var branchKpis = new List<S3BranchKpiGroupDto>();
|
|
|
S2SyncStatusDto? syncStatus = null;
|
|
|
if (moduleCode == "S2")
|
|
|
{
|
|
|
@@ -189,6 +193,17 @@ LIMIT 60
|
|
|
schedules = new List<S2ScheduleDto>();
|
|
|
}
|
|
|
}
|
|
|
+ if (moduleCode == "S3")
|
|
|
+ {
|
|
|
+ syncStatus = await GetMdpSyncStatusAsync("S3_MDP_SYNC_TRANSFORM");
|
|
|
+ var s3Alerts = await GetS3DerivedAlertsAsync(tenantId, factoryId);
|
|
|
+ if (s3Alerts.Count > 0)
|
|
|
+ alerts = s3Alerts;
|
|
|
+ decomposition = await GetS3DecompositionAsync(tenantId, factoryId, l2);
|
|
|
+ branchKpis = await GetS3BranchKpisAsync(tenantId, factoryId, l2);
|
|
|
+ trend = await GetS3TrendAsync(tenantId, factoryId);
|
|
|
+ distribution = await GetS3DistributionAsync(tenantId, factoryId);
|
|
|
+ }
|
|
|
return Ok(new
|
|
|
{
|
|
|
moduleCode,
|
|
|
@@ -196,6 +211,7 @@ LIMIT 60
|
|
|
l3,
|
|
|
syncStatus,
|
|
|
decomposition,
|
|
|
+ branchKpis,
|
|
|
trend,
|
|
|
distribution,
|
|
|
schedules,
|
|
|
@@ -223,7 +239,27 @@ LIMIT 60
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+ [HttpPost("s3-mdp/refresh")]
|
|
|
+ public async Task<IActionResult> RefreshS3Mdp(CancellationToken cancellationToken)
|
|
|
+ {
|
|
|
+ var result = await _s3MdpSyncTransformService.RunFullAsync(cancellationToken, "MANUAL");
|
|
|
+ return Ok(new
|
|
|
+ {
|
|
|
+ ok = true,
|
|
|
+ result.BatchId,
|
|
|
+ result.StageRows,
|
|
|
+ result.StandardRows,
|
|
|
+ result.DwdRows,
|
|
|
+ result.KpiRows
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
private async Task<S2SyncStatusDto?> GetS2SyncStatusAsync()
|
|
|
+ {
|
|
|
+ return await GetMdpSyncStatusAsync("S2_MDP_SYNC_TRANSFORM");
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task<S2SyncStatusDto?> GetMdpSyncStatusAsync(string jobCode)
|
|
|
{
|
|
|
try
|
|
|
{
|
|
|
@@ -233,10 +269,11 @@ LIMIT 60
|
|
|
standard_rows AS StandardRows, dwd_rows AS DwdRows,
|
|
|
start_time AS StartTime, end_time AS EndTime, error_message AS ErrorMessage
|
|
|
FROM mdp_transform_run_log
|
|
|
- WHERE job_code='S2_MDP_SYNC_TRANSFORM'
|
|
|
+ WHERE job_code=@JobCode
|
|
|
ORDER BY start_time DESC, id DESC
|
|
|
LIMIT 1
|
|
|
- """);
|
|
|
+ """,
|
|
|
+ new SugarParameter("@JobCode", jobCode));
|
|
|
}
|
|
|
catch
|
|
|
{
|
|
|
@@ -457,6 +494,230 @@ LIMIT 60
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ private async Task<List<S2DecompositionDto>> GetS3DecompositionAsync(long tenantId, long factoryId, List<KpiDetailDto> l2)
|
|
|
+ {
|
|
|
+ var l1 = await GetLatestModuleL1Async("S3", tenantId, factoryId);
|
|
|
+ var latestL2 = l2.Where(u => !string.IsNullOrWhiteSpace(u.MetricCode))
|
|
|
+ .GroupBy(u => u.MetricCode!, StringComparer.OrdinalIgnoreCase)
|
|
|
+ .ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
|
|
|
+ var readiness = await GetS3ReadinessSummaryAsync(tenantId);
|
|
|
+ var delivery = await GetS3DeliverySummaryAsync(tenantId);
|
|
|
+
|
|
|
+ return new List<S2DecompositionDto>
|
|
|
+ {
|
|
|
+ new()
|
|
|
+ {
|
|
|
+ Title = "物料需求计划",
|
|
|
+ Active = true,
|
|
|
+ Metrics = new List<string>
|
|
|
+ {
|
|
|
+ $"1. 周期:{FormatMetric(l1, "S3_L1_001", "天")}",
|
|
|
+ $"2. 满足率:{Math.Round(readiness.ReadyRatePct ?? GetMetricValue(l1, "S3_L1_002") ?? 0, 2)}%",
|
|
|
+ $"3. 覆盖SKU:{readiness.ComponentCount}",
|
|
|
+ $"4. 缺口数量:{Math.Round(readiness.ShortageQty ?? 0, 2)}"
|
|
|
+ }
|
|
|
+ },
|
|
|
+ new()
|
|
|
+ {
|
|
|
+ Title = "物料交货计划",
|
|
|
+ Metrics = new List<string>
|
|
|
+ {
|
|
|
+ $"1. 周期:{FormatMetric(latestL2, "S3_L2_004", "天")}",
|
|
|
+ $"2. 满足率:{FormatMetric(latestL2, "S3_L2_005", "%")}",
|
|
|
+ $"3. 风险供应商:{delivery.RiskSupplierCount}",
|
|
|
+ $"4. 待交数量:{Math.Round(delivery.RemainingQty ?? 0, 2)}"
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task<List<S3BranchKpiGroupDto>> GetS3BranchKpisAsync(long tenantId, long factoryId, List<KpiDetailDto> l2)
|
|
|
+ {
|
|
|
+ var l1 = await GetLatestModuleL1Async("S3", tenantId, factoryId);
|
|
|
+ var latestL2 = l2.Where(u => !string.IsNullOrWhiteSpace(u.MetricCode))
|
|
|
+ .GroupBy(u => u.MetricCode!, StringComparer.OrdinalIgnoreCase)
|
|
|
+ .ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
|
|
|
+ var readiness = await GetS3ReadinessSummaryAsync(tenantId);
|
|
|
+ var delivery = await GetS3DeliverySummaryAsync(tenantId);
|
|
|
+
|
|
|
+ var demandCycle = GetMetricValue(l1, "S3_L1_001");
|
|
|
+ var demandReadyRate = readiness.ReadyRatePct ?? GetMetricValue(l1, "S3_L1_002");
|
|
|
+ var deliveryCycle = GetMetricValue(latestL2, "S3_L2_004");
|
|
|
+ var deliveryRate = GetMetricValue(latestL2, "S3_L2_005");
|
|
|
+
|
|
|
+ return new List<S3BranchKpiGroupDto>
|
|
|
+ {
|
|
|
+ new()
|
|
|
+ {
|
|
|
+ Branch = "MRP",
|
|
|
+ Title = "物料需求计划",
|
|
|
+ Items = new List<S3BranchKpiDto>
|
|
|
+ {
|
|
|
+ BuildBranchKpi("需求计划周期", demandCycle, "d", InverseBar(demandCycle, 15m)),
|
|
|
+ BuildBranchKpi("需求计划满足率", demandReadyRate, "%", PercentBar(demandReadyRate)),
|
|
|
+ BuildBranchKpi("计划覆盖SKU", readiness.ComponentCount, "SKU", PercentBar(readiness.ComponentCount, Math.Max(readiness.ComponentCount, 1)))
|
|
|
+ }
|
|
|
+ },
|
|
|
+ new()
|
|
|
+ {
|
|
|
+ Branch = "MDP",
|
|
|
+ Title = "物料交货计划",
|
|
|
+ Items = new List<S3BranchKpiDto>
|
|
|
+ {
|
|
|
+ BuildBranchKpi("交货计划周期", deliveryCycle, "d", InverseBar(deliveryCycle, 10.5m)),
|
|
|
+ BuildBranchKpi("交货计划满足率", deliveryRate, "%", PercentBar(deliveryRate)),
|
|
|
+ BuildBranchKpi("风险供应商", delivery.RiskSupplierCount, "家", InverseBar(delivery.RiskSupplierCount, Math.Max(delivery.SupplierCount, 1)))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task<List<S2TrendDto>> GetS3TrendAsync(long tenantId, long factoryId)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ return await _db.Ado.SqlQueryAsync<S2TrendDto>(
|
|
|
+ """
|
|
|
+ SELECT DATE_FORMAT(d.biz_date, '%m-%d') AS DateLabel,
|
|
|
+ MAX(CASE WHEN d.metric_code='S3_L1_001' THEN d.metric_value END) AS CycleDays,
|
|
|
+ MAX(CASE WHEN d.metric_code='S3_L1_002' THEN d.metric_value END) AS SatisfactionPct
|
|
|
+ FROM ado_s9_kpi_value_l1_day d
|
|
|
+ WHERE d.tenant_id=@tenantId AND d.factory_id=@factoryId AND d.module_code='S3' AND d.is_deleted=0
|
|
|
+ AND d.metric_code IN ('S3_L1_001','S3_L1_002')
|
|
|
+ GROUP BY d.biz_date
|
|
|
+ ORDER BY d.biz_date DESC
|
|
|
+ LIMIT 7
|
|
|
+ """,
|
|
|
+ new { tenantId, factoryId });
|
|
|
+ }
|
|
|
+ catch
|
|
|
+ {
|
|
|
+ return new List<S2TrendDto>();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task<List<S2DistributionDto>> GetS3DistributionAsync(long tenantId, long factoryId)
|
|
|
+ {
|
|
|
+ _ = factoryId;
|
|
|
+ try
|
|
|
+ {
|
|
|
+ return await _db.Ado.SqlQueryAsync<S2DistributionDto>(
|
|
|
+ """
|
|
|
+ SELECT COALESCE(NULLIF(supplier_name,''), NULLIF(supplier_code,''), '未分配') AS Name,
|
|
|
+ ROUND(100 * SUM(CASE WHEN IFNULL(schedule_qty,0) >= IFNULL(order_qty,0) AND IFNULL(order_qty,0) > 0 THEN 1 ELSE 0 END) / NULLIF(COUNT(1), 0), 2) AS SatisfactionPct,
|
|
|
+ COUNT(1) AS TotalCount,
|
|
|
+ SUM(CASE WHEN risk_level IN ('HIGH','MEDIUM') THEN 1 ELSE 0 END) AS RiskCount
|
|
|
+ FROM dwd_supplier_delivery
|
|
|
+ WHERE tenant_id=@tenantId
|
|
|
+ AND stat_date=(SELECT MAX(stat_date) FROM dwd_supplier_delivery WHERE tenant_id=@tenantId)
|
|
|
+ GROUP BY COALESCE(NULLIF(supplier_name,''), NULLIF(supplier_code,''), '未分配')
|
|
|
+ ORDER BY RiskCount DESC, SatisfactionPct, TotalCount DESC
|
|
|
+ LIMIT 8
|
|
|
+ """,
|
|
|
+ new { tenantId });
|
|
|
+ }
|
|
|
+ catch
|
|
|
+ {
|
|
|
+ return new List<S2DistributionDto>();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task<List<S8AlertDto>> GetS3DerivedAlertsAsync(long tenantId, long factoryId)
|
|
|
+ {
|
|
|
+ _ = factoryId;
|
|
|
+ try
|
|
|
+ {
|
|
|
+ return await _db.Ado.SqlQueryAsync<S8AlertDto>(
|
|
|
+ """
|
|
|
+ SELECT DATE_FORMAT(calc_time, '%H:%i:%s') AS Time,
|
|
|
+ CASE WHEN risk_level='HIGH' THEN 'critical'
|
|
|
+ WHEN risk_level='MEDIUM' THEN 'warning'
|
|
|
+ ELSE 'info' END AS LevelCode,
|
|
|
+ CONCAT('供应商 ', COALESCE(NULLIF(supplier_name,''), supplier_code, '未分配'),
|
|
|
+ ' 物料 ', COALESCE(NULLIF(item_code,''), '未指定'),
|
|
|
+ ':', COALESCE(NULLIF(risk_reason,''), risk_type, '供应风险')) AS Message
|
|
|
+ FROM dwd_supplier_risk
|
|
|
+ WHERE tenant_id=@tenantId
|
|
|
+ AND stat_date=(SELECT MAX(stat_date) FROM dwd_supplier_risk WHERE tenant_id=@tenantId)
|
|
|
+ ORDER BY FIELD(risk_level, 'HIGH', 'MEDIUM', 'LOW'), risk_count DESC, calc_time DESC
|
|
|
+ LIMIT 20
|
|
|
+ """,
|
|
|
+ new { tenantId });
|
|
|
+ }
|
|
|
+ catch
|
|
|
+ {
|
|
|
+ return new List<S8AlertDto>();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task<Dictionary<string, KpiDetailDto>> GetLatestModuleL1Async(string moduleCode, long tenantId, long factoryId)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ var rows = await _db.Ado.SqlQueryAsync<KpiDetailDto>(
|
|
|
+ """
|
|
|
+ SELECT v.module_code AS ModuleCode, v.metric_code AS MetricCode, k.MetricName AS MetricName,
|
|
|
+ v.metric_value AS MetricValue, v.target_value AS TargetValue,
|
|
|
+ v.status_color AS StatusColor, v.trend_flag AS TrendFlag, v.biz_date AS StatDate
|
|
|
+ FROM ado_s9_kpi_value_l1_day v
|
|
|
+ LEFT JOIN ado_smart_ops_kpi_master k ON k.TenantId=v.tenant_id AND k.MetricCode=v.metric_code
|
|
|
+ WHERE v.tenant_id=@tenantId AND v.factory_id=@factoryId AND v.module_code=@moduleCode AND v.is_deleted=0
|
|
|
+ AND v.biz_date=(SELECT MAX(biz_date) FROM ado_s9_kpi_value_l1_day
|
|
|
+ WHERE tenant_id=@tenantId AND factory_id=@factoryId AND module_code=@moduleCode AND is_deleted=0)
|
|
|
+ """,
|
|
|
+ new { moduleCode, tenantId, factoryId });
|
|
|
+ return rows.Where(u => !string.IsNullOrWhiteSpace(u.MetricCode))
|
|
|
+ .GroupBy(u => u.MetricCode!, StringComparer.OrdinalIgnoreCase)
|
|
|
+ .ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
|
|
|
+ }
|
|
|
+ catch
|
|
|
+ {
|
|
|
+ return new Dictionary<string, KpiDetailDto>(StringComparer.OrdinalIgnoreCase);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task<S3ReadinessSummaryDto> GetS3ReadinessSummaryAsync(long tenantId)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ return await _db.Ado.SqlQuerySingleAsync<S3ReadinessSummaryDto>(
|
|
|
+ """
|
|
|
+ SELECT COUNT(DISTINCT component_item_code) AS ComponentCount,
|
|
|
+ ROUND(100 * SUM(CASE WHEN IFNULL(shortage_qty,0) <= 0 OR UPPER(IFNULL(ready_status,'')) IN ('READY','SUFFICIENT','OK') THEN 1 ELSE 0 END) / NULLIF(COUNT(1), 0), 2) AS ReadyRatePct,
|
|
|
+ SUM(IFNULL(shortage_qty, 0)) AS ShortageQty
|
|
|
+ FROM dwd_material_readiness
|
|
|
+ WHERE tenant_id=@tenantId
|
|
|
+ AND stat_date=(SELECT MAX(stat_date) FROM dwd_material_readiness WHERE tenant_id=@tenantId)
|
|
|
+ """,
|
|
|
+ new { tenantId }) ?? new S3ReadinessSummaryDto();
|
|
|
+ }
|
|
|
+ catch
|
|
|
+ {
|
|
|
+ return new S3ReadinessSummaryDto();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task<S3DeliverySummaryDto> GetS3DeliverySummaryAsync(long tenantId)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ return await _db.Ado.SqlQuerySingleAsync<S3DeliverySummaryDto>(
|
|
|
+ """
|
|
|
+ SELECT COUNT(DISTINCT supplier_code) AS SupplierCount,
|
|
|
+ COUNT(DISTINCT CASE WHEN risk_level IN ('HIGH','MEDIUM') THEN supplier_code END) AS RiskSupplierCount,
|
|
|
+ SUM(IFNULL(remaining_qty, 0)) AS RemainingQty
|
|
|
+ FROM dwd_supplier_delivery
|
|
|
+ WHERE tenant_id=@tenantId
|
|
|
+ AND stat_date=(SELECT MAX(stat_date) FROM dwd_supplier_delivery WHERE tenant_id=@tenantId)
|
|
|
+ """,
|
|
|
+ new { tenantId }) ?? new S3DeliverySummaryDto();
|
|
|
+ }
|
|
|
+ catch
|
|
|
+ {
|
|
|
+ return new S3DeliverySummaryDto();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
private static string FormatMetric(Dictionary<string, KpiDetailDto> rows, string metricCode, string unit)
|
|
|
{
|
|
|
if (!rows.TryGetValue(metricCode, out var row) || row.MetricValue == null)
|
|
|
@@ -464,6 +725,37 @@ LIMIT 60
|
|
|
return $"{Math.Round(row.MetricValue.Value, 2)}{unit}";
|
|
|
}
|
|
|
|
|
|
+ private static decimal? GetMetricValue(Dictionary<string, KpiDetailDto> rows, string metricCode)
|
|
|
+ {
|
|
|
+ return rows.TryGetValue(metricCode, out var row) ? row.MetricValue : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static S3BranchKpiDto BuildBranchKpi(string label, decimal? value, string unit, decimal barPct)
|
|
|
+ {
|
|
|
+ return new S3BranchKpiDto
|
|
|
+ {
|
|
|
+ Label = label,
|
|
|
+ Value = value,
|
|
|
+ Unit = unit,
|
|
|
+ BarPct = barPct,
|
|
|
+ Status = barPct >= 90 ? "success" : barPct >= 70 ? "warning" : "danger"
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ private static decimal PercentBar(decimal? value, decimal max = 100m)
|
|
|
+ {
|
|
|
+ if (value == null || max <= 0)
|
|
|
+ return 0;
|
|
|
+ return Math.Round(Math.Max(0, Math.Min(100, value.Value / max * 100)), 2);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static decimal InverseBar(decimal? value, decimal target)
|
|
|
+ {
|
|
|
+ if (value == null || value <= 0 || target <= 0)
|
|
|
+ return 0;
|
|
|
+ return Math.Round(Math.Max(0, Math.Min(100, target / value.Value * 100)), 2);
|
|
|
+ }
|
|
|
+
|
|
|
/// <summary>
|
|
|
/// 智慧运营看板基础查询下拉:产品、订单号、产线(来自 Demo 业务表;无租户列时忽略 tenant/factory)。
|
|
|
/// </summary>
|
|
|
@@ -656,5 +948,35 @@ LIMIT 60
|
|
|
public int RiskCount { get; set; }
|
|
|
public int LineCount { get; set; }
|
|
|
}
|
|
|
+
|
|
|
+ private sealed class S3BranchKpiGroupDto
|
|
|
+ {
|
|
|
+ public string Branch { get; set; } = string.Empty;
|
|
|
+ public string Title { get; set; } = string.Empty;
|
|
|
+ public List<S3BranchKpiDto> Items { get; set; } = new();
|
|
|
+ }
|
|
|
+
|
|
|
+ private sealed class S3BranchKpiDto
|
|
|
+ {
|
|
|
+ public string Label { get; set; } = string.Empty;
|
|
|
+ public decimal? Value { get; set; }
|
|
|
+ public string Unit { get; set; } = string.Empty;
|
|
|
+ public decimal BarPct { get; set; }
|
|
|
+ public string Status { get; set; } = "info";
|
|
|
+ }
|
|
|
+
|
|
|
+ private sealed class S3ReadinessSummaryDto
|
|
|
+ {
|
|
|
+ public int ComponentCount { get; set; }
|
|
|
+ public decimal? ReadyRatePct { get; set; }
|
|
|
+ public decimal? ShortageQty { get; set; }
|
|
|
+ }
|
|
|
+
|
|
|
+ private sealed class S3DeliverySummaryDto
|
|
|
+ {
|
|
|
+ public int SupplierCount { get; set; }
|
|
|
+ public int RiskSupplierCount { get; set; }
|
|
|
+ public decimal? RemainingQty { get; set; }
|
|
|
+ }
|
|
|
}
|
|
|
|