using Admin.NET.Plugin.AiDOP.Dto.S8;
using Admin.NET.Plugin.AiDOP.Entity.S8;
namespace Admin.NET.Plugin.AiDOP.Service.S8;
///
/// S8 大屏卡片配置(ado_s8_dashboard_cell_config)CRUD。
/// 列表合并全局基线(0/0)与工厂覆盖,同 (pageCode, cellCode) 以工厂记录优先。
///
public class S8DashboardCellConfigService : ITransient
{
private readonly SqlSugarRepository _rep;
public S8DashboardCellConfigService(SqlSugarRepository rep) => _rep = rep;
public async Task> ListAsync(long tenantId, long factoryId)
{
var all = await _rep.AsQueryable()
.Where(x => (x.TenantId == 0 && x.FactoryId == 0)
|| (x.TenantId == tenantId && x.FactoryId == factoryId))
.ToListAsync();
return all
.GroupBy(x => (x.PageCode, x.CellCode))
.Select(g => g.OrderByDescending(x => x.FactoryId).First())
.OrderBy(x => x.PageCode)
.ThenBy(x => x.SortNo)
.ThenBy(x => x.CellCode)
.ToList();
}
public async Task CreateAsync(AdoS8DashboardCellConfig body)
{
ValidateAndNormalize(body);
var exists = await _rep.AsQueryable()
.AnyAsync(x => x.TenantId == body.TenantId && x.FactoryId == body.FactoryId
&& x.PageCode == body.PageCode && x.CellCode == body.CellCode);
if (exists) throw new S8BizException("同一工厂下页面与卡片编码组合已存在");
body.Id = 0;
body.CreatedAt = DateTime.Now;
body.UpdatedAt = null;
await _rep.InsertAsync(body);
return body;
}
public async Task UpdateAsync(long id, AdoS8DashboardCellConfig body)
{
var e = await _rep.GetByIdAsync(id) ?? throw new S8BizException("记录不存在");
ValidateAndNormalize(body);
var dup = await _rep.AsQueryable()
.AnyAsync(x => x.Id != id && x.TenantId == body.TenantId && x.FactoryId == body.FactoryId
&& x.PageCode == body.PageCode && x.CellCode == body.CellCode);
if (dup) throw new S8BizException("同一工厂下页面与卡片编码组合已存在");
body.Id = id;
body.CreatedAt = e.CreatedAt;
body.UpdatedAt = DateTime.Now;
await _rep.UpdateAsync(body);
return body;
}
public async Task DeleteAsync(long id) => await _rep.DeleteByIdAsync(id);
///
/// 获取指定页面的渲染配置(G-09 一期)。
/// 合并规则:全局基线 (0/0) + 工厂覆盖 (tenantId/factoryId),同 cell_code 工厂优先;
/// 过滤规则:合并后再过滤 enabled=true(禁止读取 override 时提前过滤 enabled);
/// 排序规则:layout_area(MODULES→ANALYSIS→SIDEBAR)→ sort_no → cell_code。
/// 本接口只读 ado_s8_dashboard_cell_config,不触发 ado_s8_exception 查询。
///
public async Task GetPageConfigAsync(AdoS8PageConfigQueryDto q)
{
// 1. pageCode 严格校验(大写精确匹配,非法返回 400)
if (string.IsNullOrWhiteSpace(q.PageCode))
throw new S8BizException("pageCode 不能为空");
var allowed = new[] { "OVERVIEW", "DELIVERY", "PRODUCTION", "SUPPLY" };
if (!allowed.Contains(q.PageCode))
throw new S8BizException($"Invalid pageCode: {q.PageCode}");
// 2. 取全局基线(不过滤 enabled)
var baseline = await _rep.AsQueryable()
.Where(x => x.TenantId == 0 && x.FactoryId == 0 && x.PageCode == q.PageCode)
.ToListAsync();
// 3. 取工厂覆盖(若有;同样不过滤 enabled,否则工厂显式关闭的卡会被基线重新激活)
var overrides = (q.TenantId != 0 || q.FactoryId != 0)
? await _rep.AsQueryable()
.Where(x => x.TenantId == q.TenantId && x.FactoryId == q.FactoryId
&& x.PageCode == q.PageCode)
.ToListAsync()
: new List();
// 4. 按 cell_code 合并,工厂覆盖优先
var merged = new Dictionary();
foreach (var cfg in baseline) merged[cfg.CellCode] = cfg;
foreach (var cfg in overrides) merged[cfg.CellCode] = cfg;
// 5. 合并后再过滤 enabled=true,然后按 layout_area → sort_no → cell_code 排序
var ordered = merged.Values
.Where(c => c.Enabled)
.OrderBy(c => LayoutAreaOrder(c.LayoutArea))
.ThenBy(c => c.SortNo)
.ThenBy(c => c.CellCode)
.ToList();
return new AdoS8PageConfigDto
{
PageCode = q.PageCode,
Cells = ordered.Select(ToCellDto).ToList(),
};
}
private static int LayoutAreaOrder(string? area) => (area ?? string.Empty).ToUpperInvariant() switch
{
"MODULES" => 1,
"ANALYSIS" => 2,
"SIDEBAR" => 3,
_ => 99,
};
private static AdoS8PageConfigCellDto ToCellDto(AdoS8DashboardCellConfig c) => new()
{
CellCode = c.CellCode,
CellTitle = c.CellTitle,
Icon = c.Icon,
LayoutArea = string.IsNullOrWhiteSpace(c.LayoutArea) ? "ANALYSIS" : c.LayoutArea,
DisplayMode = string.IsNullOrWhiteSpace(c.DisplayMode) ? "CATEGORY_CARD" : c.DisplayMode,
SortNo = c.SortNo,
BindingType = c.BindingType,
ExceptionTypeCode = c.ExceptionTypeCode,
AggregateScope = c.AggregateScope,
StatMetric = c.StatMetric,
TimeWindow = c.TimeWindow,
DeptGroupBy = c.DeptGroupBy,
Enabled = c.Enabled,
// 注意:不映射 ShowInSidebar / FilterExpression / TenantId / FactoryId / Id / CreatedAt / UpdatedAt
};
private static void ValidateAndNormalize(AdoS8DashboardCellConfig body)
{
if (string.IsNullOrWhiteSpace(body.PageCode) || string.IsNullOrWhiteSpace(body.CellCode))
throw new S8BizException("页面编码与卡片编码必填");
body.PageCode = body.PageCode.Trim();
body.CellCode = body.CellCode.Trim();
if (body.CellTitle != null) body.CellTitle = body.CellTitle.Trim();
if (string.IsNullOrWhiteSpace(body.BindingType))
body.BindingType = "CUSTOM";
body.BindingType = body.BindingType.Trim().ToUpperInvariant();
if (body.BindingType is not ("EXCEPTION_TYPE" or "AGGREGATE" or "CUSTOM"))
throw new S8BizException("绑定类型须为 EXCEPTION_TYPE / AGGREGATE / CUSTOM");
switch (body.BindingType)
{
case "EXCEPTION_TYPE":
if (string.IsNullOrWhiteSpace(body.ExceptionTypeCode))
throw new S8BizException("绑定类型为异常类型时须填写异常类型编码");
body.ExceptionTypeCode = body.ExceptionTypeCode.Trim();
body.AggregateScope = null;
break;
case "AGGREGATE":
if (string.IsNullOrWhiteSpace(body.AggregateScope))
throw new S8BizException("绑定类型为域聚合时须选择聚合范围");
body.AggregateScope = body.AggregateScope!.Trim();
body.ExceptionTypeCode = null;
break;
default:
body.ExceptionTypeCode = null;
body.AggregateScope = null;
break;
}
if (string.IsNullOrWhiteSpace(body.StatMetric)) body.StatMetric = "OPEN_COUNT";
body.StatMetric = body.StatMetric.Trim().ToUpperInvariant();
if (body.StatMetric is not ("OPEN_COUNT" or "FREQUENCY" or "AVG_DURATION" or "CLOSE_RATE"))
throw new S8BizException("统计指标须为 OPEN_COUNT / FREQUENCY / AVG_DURATION / CLOSE_RATE");
if (string.IsNullOrWhiteSpace(body.TimeWindow)) body.TimeWindow = "LAST_24H";
body.TimeWindow = body.TimeWindow.Trim().ToUpperInvariant();
if (body.TimeWindow is not ("TODAY" or "LAST_24H" or "LAST_7D" or "LAST_30D"))
throw new S8BizException("时间窗须为 TODAY / LAST_24H / LAST_7D / LAST_30D");
if (string.IsNullOrWhiteSpace(body.DeptGroupBy)) body.DeptGroupBy = "OWNER";
body.DeptGroupBy = body.DeptGroupBy.Trim().ToUpperInvariant();
if (body.DeptGroupBy is not ("OWNER" or "OCCUR"))
throw new S8BizException("部门聚合维度须为 OWNER(责任部门)或 OCCUR(发生部门)");
if (body.FilterExpression != null && body.FilterExpression.Length > 1000)
throw new S8BizException("筛选表达式过长");
}
}