using Admin.NET.Plugin.AiDOP.Dto.S8;
using Admin.NET.Plugin.AiDOP.Entity.S8;
namespace Admin.NET.Plugin.AiDOP.Service.S8;
///
/// 大屏卡片数据查询服务:按 (pageCode, cellCode) 读取配置,按 binding_type 分发组装结果。
/// 配置解析:工厂覆盖优先(tenant+factory 精确匹配)→ 全局基线(tenant=0/factory=0)兜底。
///
public class S8DashboardCellDataService : ITransient
{
private readonly SqlSugarRepository _cfgRep;
private readonly SqlSugarRepository _exRep;
private readonly SqlSugarRepository _orgRep;
public S8DashboardCellDataService(
SqlSugarRepository cfgRep,
SqlSugarRepository exRep,
SqlSugarRepository orgRep)
{
_cfgRep = cfgRep;
_exRep = exRep;
_orgRep = orgRep;
}
public async Task GetAsync(AdoS8CellDataQueryDto q)
{
if (string.IsNullOrWhiteSpace(q.PageCode) || string.IsNullOrWhiteSpace(q.CellCode))
throw new S8BizException("pageCode 和 cellCode 必填");
var cfg = await ResolveConfigAsync(q.TenantId, q.FactoryId, q.PageCode, q.CellCode);
if (cfg is null)
return new AdoS8CellDataDto
{
PageCode = q.PageCode,
CellCode = q.CellCode,
Message = "未配置",
};
if (!cfg.Enabled)
return new AdoS8CellDataDto
{
PageCode = q.PageCode,
CellCode = q.CellCode,
BindingType = cfg.BindingType,
Title = cfg.CellTitle,
StatMetric = cfg.StatMetric,
TimeWindow = cfg.TimeWindow,
Message = "已禁用",
};
var (from, to) = TimeRange(cfg.TimeWindow);
var effectiveDeptGroupBy = !string.IsNullOrWhiteSpace(q.DeptGroupBy) ? q.DeptGroupBy! : cfg.DeptGroupBy;
// 取事实数据
var events = await _exRep.AsQueryable()
.Where(e => e.TenantId == q.TenantId && e.FactoryId == q.FactoryId && !e.IsDeleted)
.Where(e => e.CreatedAt >= from && e.CreatedAt < to)
.Select(e => new EvtRow
{
Status = e.Status,
ExceptionTypeCode = e.ExceptionTypeCode,
ModuleCode = e.ModuleCode,
SceneCode = e.SceneCode,
ResponsibleDeptId = e.ResponsibleDeptId,
OccurrenceDeptId = e.OccurrenceDeptId,
CreatedAt = e.CreatedAt,
ClosedAt = e.ClosedAt,
})
.ToListAsync();
// 按 binding_type 过滤
IEnumerable scoped = cfg.BindingType switch
{
"EXCEPTION_TYPE" => events.Where(e => !string.IsNullOrEmpty(cfg.ExceptionTypeCode) && e.ExceptionTypeCode == cfg.ExceptionTypeCode),
"AGGREGATE" => ApplyAggregateScope(events, cfg.AggregateScope),
_ => events, // CUSTOM:不按类型过滤,返回同页范围聚合值供前端参考
};
var list = scoped.ToList();
var dto = new AdoS8CellDataDto
{
CellCode = cfg.CellCode,
PageCode = cfg.PageCode,
Title = cfg.CellTitle,
BindingType = cfg.BindingType,
StatMetric = cfg.StatMetric,
TimeWindow = cfg.TimeWindow,
Value = ComputeValue(list, cfg.StatMetric),
};
// 明细分组:AGGREGATE 按部门;EXCEPTION_TYPE 可选按部门;CUSTOM 跳过
if (cfg.BindingType == "AGGREGATE" || cfg.BindingType == "EXCEPTION_TYPE")
{
dto.Breakdown = await BuildDeptBreakdownAsync(list, effectiveDeptGroupBy, cfg.StatMetric);
}
return dto;
}
private async Task ResolveConfigAsync(long tenantId, long factoryId, string pageCode, string cellCode)
{
var rows = await _cfgRep.AsQueryable()
.Where(x => x.PageCode == pageCode && x.CellCode == cellCode
&& ((x.TenantId == 0 && x.FactoryId == 0)
|| (x.TenantId == tenantId && x.FactoryId == factoryId)))
.ToListAsync();
return rows.OrderByDescending(x => x.FactoryId).FirstOrDefault();
}
private static (DateTime from, DateTime to) TimeRange(string window)
{
var now = DateTime.Now;
return window switch
{
"TODAY" => (now.Date, now.Date.AddDays(1)),
"LAST_24H" => (now.AddHours(-24), now.AddMinutes(1)),
"LAST_7D" => (now.AddDays(-7), now.AddMinutes(1)),
"LAST_30D" => (now.AddDays(-30), now.AddMinutes(1)),
_ => (now.AddHours(-24), now.AddMinutes(1)),
};
}
private static IEnumerable ApplyAggregateScope(List events, string? scope) => scope switch
{
"DOMAIN_DELIVERY" => events.Where(e => e.SceneCode == "S1S7_DELIVERY"),
"DOMAIN_PRODUCTION" => events.Where(e => e.SceneCode == "S2S6_PRODUCTION"),
"DOMAIN_SUPPLY" => events.Where(e => e.SceneCode == "S3S5_SUPPLY"),
"ALL" => events,
_ => events,
};
private static double ComputeValue(List rows, string metric)
{
if (rows.Count == 0) return 0;
return metric switch
{
"OPEN_COUNT" => rows.Count(e => e.Status != "CLOSED"),
"FREQUENCY" => rows.Count,
"AVG_DURATION" => AvgHours(rows),
"CLOSE_RATE" => Math.Round(rows.Count(e => e.Status == "CLOSED") * 100.0 / rows.Count, 1),
_ => rows.Count,
};
}
private static double AvgHours(List rows)
{
var closed = rows.Where(r => r.ClosedAt.HasValue).ToList();
if (closed.Count == 0) return 0;
return Math.Round(closed.Average(r => (r.ClosedAt!.Value - r.CreatedAt).TotalHours), 1);
}
private async Task> BuildDeptBreakdownAsync(List rows, string groupBy, string metric)
{
Func keySel = groupBy == "OCCUR"
? (EvtRow e) => e.OccurrenceDeptId
: (EvtRow e) => e.ResponsibleDeptId;
var groups = rows.Where(e => keySel(e) > 0).GroupBy(keySel).ToList();
if (groups.Count == 0) return new();
var deptIds = groups.Select(g => g.Key).Distinct().ToList();
var names = (await _orgRep.AsQueryable().Where(o => deptIds.Contains(o.Id)).Select(o => new { o.Id, o.Name }).ToListAsync())
.ToDictionary(o => o.Id, o => o.Name);
return groups.Select(g =>
{
var list = g.ToList();
return new AdoS8CellBreakdownItem
{
Code = g.Key.ToString(),
Label = names.TryGetValue(g.Key, out var n) ? n : $"部门{g.Key}",
Value = ComputeValue(list, metric),
};
})
.OrderByDescending(i => i.Value)
.ToList();
}
private class EvtRow
{
public string Status { get; set; } = string.Empty;
public string? ExceptionTypeCode { get; set; }
public string? ModuleCode { get; set; }
public string? SceneCode { get; set; }
public long ResponsibleDeptId { get; set; }
public long OccurrenceDeptId { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? ClosedAt { get; set; }
}
}