Validate chart business block datasets
This commit is contained in:
@@ -0,0 +1,357 @@
|
|||||||
|
package com.ruoyi.generator.service.front;
|
||||||
|
|
||||||
|
import java.lang.reflect.Array;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import com.ruoyi.common.exception.ServiceException;
|
||||||
|
import com.ruoyi.common.utils.StringUtils;
|
||||||
|
import com.ruoyi.generator.domain.GenTable;
|
||||||
|
import com.ruoyi.generator.domain.GenTableColumn;
|
||||||
|
import com.ruoyi.generator.domain.front.dto.block.BusinessBlockDefinition;
|
||||||
|
import com.ruoyi.generator.domain.front.dto.block.BusinessBlockInstance;
|
||||||
|
import com.ruoyi.generator.domain.front.dto.block.ChartDatasetConfig;
|
||||||
|
import com.ruoyi.generator.domain.front.dto.block.ChartDateRangeConfig;
|
||||||
|
import com.ruoyi.generator.domain.front.dto.block.ChartDimensionConfig;
|
||||||
|
import com.ruoyi.generator.domain.front.dto.block.ChartFilterConfig;
|
||||||
|
import com.ruoyi.generator.domain.front.dto.block.ChartMetricConfig;
|
||||||
|
import com.ruoyi.generator.domain.front.dto.block.ChartSortConfig;
|
||||||
|
|
||||||
|
public class ChartDatasetValidator
|
||||||
|
{
|
||||||
|
public static final String DATASET_SCHEMA = "single-table-aggregate-v1";
|
||||||
|
|
||||||
|
private static final Set<String> CHART_TYPES =
|
||||||
|
new HashSet<String>(Arrays.asList("metric", "line", "bar", "pie"));
|
||||||
|
private static final Set<String> AGGREGATES =
|
||||||
|
new HashSet<String>(Arrays.asList("count", "sum", "avg"));
|
||||||
|
private static final Set<String> TIME_UNITS =
|
||||||
|
new HashSet<String>(Arrays.asList("day", "week", "month"));
|
||||||
|
private static final Set<String> OPERATORS =
|
||||||
|
new HashSet<String>(Arrays.asList(
|
||||||
|
"eq", "ne", "gt", "gte", "lt", "lte", "between", "in", "is_null", "is_not_null"));
|
||||||
|
private static final Set<String> DATE_PRESETS =
|
||||||
|
new HashSet<String>(Arrays.asList(
|
||||||
|
"today", "last_7_days", "last_30_days", "this_month", "this_year", "fixed"));
|
||||||
|
private static final Set<Integer> SPANS =
|
||||||
|
new HashSet<Integer>(Arrays.asList(Integer.valueOf(12), Integer.valueOf(6), Integer.valueOf(4)));
|
||||||
|
private static final Pattern ALIAS_PATTERN = Pattern.compile("[A-Za-z][A-Za-z0-9_]{0,39}");
|
||||||
|
|
||||||
|
public void validate(BusinessBlockDefinition definition, BusinessBlockInstance instance, List<GenTable> tables)
|
||||||
|
{
|
||||||
|
if (definition == null || instance == null)
|
||||||
|
{
|
||||||
|
throw new ServiceException("图表业务块配置不存在");
|
||||||
|
}
|
||||||
|
String blockName = StringUtils.defaultIfEmpty(instance.getName(), definition.getName());
|
||||||
|
ChartDatasetConfig dataset = instance.getDataset();
|
||||||
|
if (dataset == null)
|
||||||
|
{
|
||||||
|
throw error(blockName, "数据集配置缺失");
|
||||||
|
}
|
||||||
|
|
||||||
|
String expectedSchema = StringUtils.defaultIfEmpty(definition.getDatasetSchema(), DATASET_SCHEMA);
|
||||||
|
if (!expectedSchema.equals(dataset.getSchema()))
|
||||||
|
{
|
||||||
|
throw error(blockName, "数据集协议必须为 " + expectedSchema);
|
||||||
|
}
|
||||||
|
|
||||||
|
GenTable table = requireTable(blockName, dataset.getTable(), tables);
|
||||||
|
validateSpan(definition, instance, blockName);
|
||||||
|
|
||||||
|
String chartType = StringUtils.defaultString(definition.getChartType()).toLowerCase();
|
||||||
|
if (!CHART_TYPES.contains(chartType))
|
||||||
|
{
|
||||||
|
throw error(blockName, "未知图表类型:" + chartType);
|
||||||
|
}
|
||||||
|
|
||||||
|
validateDimension(blockName, chartType, dataset.getDimension(), table);
|
||||||
|
Set<String> aliases = validateMetrics(blockName, chartType, dataset.getMetrics(), table);
|
||||||
|
validateFilters(blockName, dataset.getFilters(), table);
|
||||||
|
validateDateRange(blockName, dataset.getDateRange(), table);
|
||||||
|
validateSort(blockName, dataset.getSort(), aliases);
|
||||||
|
validateLimit(blockName, chartType, dataset);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateSpan(BusinessBlockDefinition definition, BusinessBlockInstance instance, String blockName)
|
||||||
|
{
|
||||||
|
if (instance.getSpan() == null)
|
||||||
|
{
|
||||||
|
Integer defaultSpan = definition.getDefaultSpan();
|
||||||
|
instance.setSpan(SPANS.contains(defaultSpan) ? defaultSpan : Integer.valueOf(12));
|
||||||
|
}
|
||||||
|
if (!SPANS.contains(instance.getSpan()))
|
||||||
|
{
|
||||||
|
throw error(blockName, "宽度只能为 12、6 或 4");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateDimension(String blockName, String chartType, ChartDimensionConfig dimension, GenTable table)
|
||||||
|
{
|
||||||
|
if ("metric".equals(chartType))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (dimension == null || StringUtils.isEmpty(dimension.getField()))
|
||||||
|
{
|
||||||
|
throw error(blockName, "请选择维度字段");
|
||||||
|
}
|
||||||
|
GenTableColumn column = requireColumn(blockName, table, dimension.getField());
|
||||||
|
String timeUnit = StringUtils.defaultString(dimension.getTimeUnit()).toLowerCase();
|
||||||
|
if (StringUtils.isEmpty(timeUnit))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!TIME_UNITS.contains(timeUnit))
|
||||||
|
{
|
||||||
|
throw error(blockName, "时间粒度只能为 day、week 或 month");
|
||||||
|
}
|
||||||
|
if (!isTemporal(column))
|
||||||
|
{
|
||||||
|
throw error(blockName, "字段 " + dimension.getField() + " 不是时间类型,不能设置时间粒度");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<String> validateMetrics(String blockName, String chartType, List<ChartMetricConfig> metrics,
|
||||||
|
GenTable table)
|
||||||
|
{
|
||||||
|
List<ChartMetricConfig> safeMetrics = metrics == null
|
||||||
|
? Collections.<ChartMetricConfig>emptyList() : metrics;
|
||||||
|
int size = safeMetrics.size();
|
||||||
|
if (("metric".equals(chartType) || "pie".equals(chartType)) && size != 1)
|
||||||
|
{
|
||||||
|
throw error(blockName, "指标卡和饼图必须配置 1 个指标");
|
||||||
|
}
|
||||||
|
if (("line".equals(chartType) || "bar".equals(chartType)) && (size < 1 || size > 3))
|
||||||
|
{
|
||||||
|
throw error(blockName, "折线图和柱状图必须配置 1 到 3 个指标");
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<String> aliases = new HashSet<String>();
|
||||||
|
for (ChartMetricConfig metric : safeMetrics)
|
||||||
|
{
|
||||||
|
if (metric == null)
|
||||||
|
{
|
||||||
|
throw error(blockName, "指标配置不能为空");
|
||||||
|
}
|
||||||
|
String aggregate = StringUtils.defaultString(metric.getAggregate()).toLowerCase();
|
||||||
|
if (!AGGREGATES.contains(aggregate))
|
||||||
|
{
|
||||||
|
throw error(blockName, "未知聚合方式:" + aggregate);
|
||||||
|
}
|
||||||
|
GenTableColumn column = null;
|
||||||
|
if (StringUtils.isNotEmpty(metric.getField()))
|
||||||
|
{
|
||||||
|
column = requireColumn(blockName, table, metric.getField());
|
||||||
|
}
|
||||||
|
else if (!"count".equals(aggregate))
|
||||||
|
{
|
||||||
|
throw error(blockName, aggregate.toUpperCase() + " 必须选择指标字段");
|
||||||
|
}
|
||||||
|
if (("sum".equals(aggregate) || "avg".equals(aggregate)) && !isNumeric(column))
|
||||||
|
{
|
||||||
|
throw error(blockName, "指标字段 " + metric.getField() + " 不是数值类型,不能使用 "
|
||||||
|
+ aggregate.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
String alias = StringUtils.defaultString(metric.getAlias());
|
||||||
|
if (!ALIAS_PATTERN.matcher(alias).matches())
|
||||||
|
{
|
||||||
|
throw error(blockName, "指标别名必须以字母开头,且只能包含字母、数字和下划线");
|
||||||
|
}
|
||||||
|
if (!aliases.add(alias))
|
||||||
|
{
|
||||||
|
throw error(blockName, "指标别名不能重复:" + alias);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return aliases;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateFilters(String blockName, List<ChartFilterConfig> filters, GenTable table)
|
||||||
|
{
|
||||||
|
List<ChartFilterConfig> safeFilters = filters == null
|
||||||
|
? Collections.<ChartFilterConfig>emptyList() : filters;
|
||||||
|
if (safeFilters.size() > 10)
|
||||||
|
{
|
||||||
|
throw error(blockName, "筛选条件不能超过 10 个");
|
||||||
|
}
|
||||||
|
for (ChartFilterConfig filter : safeFilters)
|
||||||
|
{
|
||||||
|
if (filter == null)
|
||||||
|
{
|
||||||
|
throw error(blockName, "筛选条件不能为空");
|
||||||
|
}
|
||||||
|
requireColumn(blockName, table, filter.getField());
|
||||||
|
String operator = StringUtils.defaultString(filter.getOperator()).toLowerCase();
|
||||||
|
if (!OPERATORS.contains(operator))
|
||||||
|
{
|
||||||
|
throw error(blockName, "未知筛选运算符:" + operator);
|
||||||
|
}
|
||||||
|
validateFilterValue(blockName, operator, filter.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateFilterValue(String blockName, String operator, Object value)
|
||||||
|
{
|
||||||
|
if ("is_null".equals(operator) || "is_not_null".equals(operator))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ("between".equals(operator))
|
||||||
|
{
|
||||||
|
if (valueCount(value) != 2)
|
||||||
|
{
|
||||||
|
throw error(blockName, "between 筛选必须提供两个值");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ("in".equals(operator))
|
||||||
|
{
|
||||||
|
if (valueCount(value) < 1)
|
||||||
|
{
|
||||||
|
throw error(blockName, "in 筛选至少提供一个值");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value == null || (value instanceof String && StringUtils.isEmpty((String) value)))
|
||||||
|
{
|
||||||
|
throw error(blockName, operator + " 筛选必须提供值");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int valueCount(Object value)
|
||||||
|
{
|
||||||
|
if (value instanceof Collection)
|
||||||
|
{
|
||||||
|
return ((Collection<?>) value).size();
|
||||||
|
}
|
||||||
|
if (value != null && value.getClass().isArray())
|
||||||
|
{
|
||||||
|
return Array.getLength(value);
|
||||||
|
}
|
||||||
|
return value == null ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateDateRange(String blockName, ChartDateRangeConfig dateRange, GenTable table)
|
||||||
|
{
|
||||||
|
if (dateRange == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
GenTableColumn column = requireColumn(blockName, table, dateRange.getField());
|
||||||
|
if (!isTemporal(column))
|
||||||
|
{
|
||||||
|
throw error(blockName, "日期范围字段 " + dateRange.getField() + " 不是时间类型");
|
||||||
|
}
|
||||||
|
String preset = StringUtils.defaultString(dateRange.getPreset()).toLowerCase();
|
||||||
|
if (!DATE_PRESETS.contains(preset))
|
||||||
|
{
|
||||||
|
throw error(blockName, "未知日期范围:" + preset);
|
||||||
|
}
|
||||||
|
if ("fixed".equals(preset)
|
||||||
|
&& (StringUtils.isEmpty(dateRange.getStart()) || StringUtils.isEmpty(dateRange.getEnd())))
|
||||||
|
{
|
||||||
|
throw error(blockName, "固定日期范围必须提供开始和结束日期");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateSort(String blockName, ChartSortConfig sort, Set<String> aliases)
|
||||||
|
{
|
||||||
|
if (sort == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String by = StringUtils.defaultString(sort.getBy());
|
||||||
|
if (!"dimension".equals(by) && !aliases.contains(by))
|
||||||
|
{
|
||||||
|
throw error(blockName, "排序字段必须为 dimension 或已配置的指标别名");
|
||||||
|
}
|
||||||
|
String order = StringUtils.defaultString(sort.getOrder()).toLowerCase();
|
||||||
|
if (!"asc".equals(order) && !"desc".equals(order))
|
||||||
|
{
|
||||||
|
throw error(blockName, "排序方向只能为 asc 或 desc");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateLimit(String blockName, String chartType, ChartDatasetConfig dataset)
|
||||||
|
{
|
||||||
|
if (dataset.getLimit() == null)
|
||||||
|
{
|
||||||
|
dataset.setLimit(Integer.valueOf(defaultLimit(chartType)));
|
||||||
|
}
|
||||||
|
if (dataset.getLimit().intValue() < 1 || dataset.getLimit().intValue() > 500)
|
||||||
|
{
|
||||||
|
throw error(blockName, "分组上限必须在 1 到 500 之间");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int defaultLimit(String chartType)
|
||||||
|
{
|
||||||
|
if ("metric".equals(chartType)) return 1;
|
||||||
|
if ("pie".equals(chartType)) return 10;
|
||||||
|
if ("bar".equals(chartType)) return 20;
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
private GenTable requireTable(String blockName, String tableName, List<GenTable> tables)
|
||||||
|
{
|
||||||
|
Map<String, GenTable> tableMap = new LinkedHashMap<String, GenTable>();
|
||||||
|
for (GenTable table : tables == null ? Collections.<GenTable>emptyList() : tables)
|
||||||
|
{
|
||||||
|
if (table != null && StringUtils.isNotEmpty(table.getTableName()))
|
||||||
|
{
|
||||||
|
tableMap.put(table.getTableName(), table);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (StringUtils.isEmpty(tableName) || !tableMap.containsKey(tableName))
|
||||||
|
{
|
||||||
|
throw error(blockName, "数据表不存在:" + StringUtils.defaultString(tableName));
|
||||||
|
}
|
||||||
|
return tableMap.get(tableName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private GenTableColumn requireColumn(String blockName, GenTable table, String fieldName)
|
||||||
|
{
|
||||||
|
for (GenTableColumn column : table.getColumns() == null
|
||||||
|
? Collections.<GenTableColumn>emptyList() : table.getColumns())
|
||||||
|
{
|
||||||
|
if (column != null && (StringUtils.equals(fieldName, column.getColumnName())
|
||||||
|
|| StringUtils.equals(fieldName, column.getJavaField())))
|
||||||
|
{
|
||||||
|
return column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw error(blockName, "字段不存在:" + table.getTableName() + "." + StringUtils.defaultString(fieldName));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isNumeric(GenTableColumn column)
|
||||||
|
{
|
||||||
|
return column != null && StringUtils.equalsAnyIgnoreCase(column.getJavaType(),
|
||||||
|
"Integer", "Long", "Double", "Float", "Short", "BigDecimal");
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isTemporal(GenTableColumn column)
|
||||||
|
{
|
||||||
|
if (column == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String columnType = StringUtils.defaultString(column.getColumnType()).toLowerCase();
|
||||||
|
return StringUtils.equalsAnyIgnoreCase(column.getJavaType(), "Date", "LocalDate", "LocalDateTime")
|
||||||
|
|| columnType.contains("date")
|
||||||
|
|| columnType.contains("time")
|
||||||
|
|| columnType.contains("timestamp")
|
||||||
|
|| columnType.contains("year");
|
||||||
|
}
|
||||||
|
|
||||||
|
private ServiceException error(String blockName, String message)
|
||||||
|
{
|
||||||
|
return new ServiceException("图表“" + StringUtils.defaultString(blockName) + "”" + message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
package com.ruoyi.generator.service.front;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.Test;
|
||||||
|
import com.ruoyi.common.exception.ServiceException;
|
||||||
|
import com.ruoyi.generator.domain.GenTable;
|
||||||
|
import com.ruoyi.generator.domain.GenTableColumn;
|
||||||
|
import com.ruoyi.generator.domain.front.dto.block.BusinessBlockDefinition;
|
||||||
|
import com.ruoyi.generator.domain.front.dto.block.BusinessBlockInstance;
|
||||||
|
import com.ruoyi.generator.domain.front.dto.block.ChartDatasetConfig;
|
||||||
|
import com.ruoyi.generator.domain.front.dto.block.ChartDimensionConfig;
|
||||||
|
import com.ruoyi.generator.domain.front.dto.block.ChartFilterConfig;
|
||||||
|
import com.ruoyi.generator.domain.front.dto.block.ChartMetricConfig;
|
||||||
|
import com.ruoyi.generator.domain.front.dto.block.ChartSortConfig;
|
||||||
|
|
||||||
|
public class ChartDatasetValidatorTest
|
||||||
|
{
|
||||||
|
private final ChartDatasetValidator validator = new ChartDatasetValidator();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void chartValidationAcceptsSingleTableTimeSeries()
|
||||||
|
{
|
||||||
|
validator.validate(lineChartDefinition(), validLineChartInstance(), chartTables());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void chartValidationRejectsSumOnTextField()
|
||||||
|
{
|
||||||
|
BusinessBlockInstance instance = validLineChartInstance();
|
||||||
|
instance.getDataset().getMetrics().get(0).setField("status");
|
||||||
|
|
||||||
|
ServiceException exception = expectServiceException(() ->
|
||||||
|
validator.validate(lineChartDefinition(), instance, chartTables()));
|
||||||
|
|
||||||
|
assertTrue(exception.getMessage().contains("SUM"));
|
||||||
|
assertTrue(exception.getMessage().contains("status"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void chartValidationRejectsInvalidSpan()
|
||||||
|
{
|
||||||
|
BusinessBlockInstance instance = validLineChartInstance();
|
||||||
|
instance.setSpan(Integer.valueOf(5));
|
||||||
|
|
||||||
|
ServiceException exception = expectServiceException(() ->
|
||||||
|
validator.validate(lineChartDefinition(), instance, chartTables()));
|
||||||
|
|
||||||
|
assertTrue(exception.getMessage().contains("宽度"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void chartValidationRejectsMoreThanThreeMetrics()
|
||||||
|
{
|
||||||
|
BusinessBlockInstance instance = validLineChartInstance();
|
||||||
|
instance.getDataset().setMetrics(Arrays.asList(
|
||||||
|
metric("amount", "sum", "metric1"),
|
||||||
|
metric("amount", "avg", "metric2"),
|
||||||
|
metric("id", "count", "metric3"),
|
||||||
|
metric("amount", "sum", "metric4")));
|
||||||
|
|
||||||
|
ServiceException exception = expectServiceException(() ->
|
||||||
|
validator.validate(lineChartDefinition(), instance, chartTables()));
|
||||||
|
|
||||||
|
assertTrue(exception.getMessage().contains("1 到 3"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void chartValidationRejectsMoreThanTenFilters()
|
||||||
|
{
|
||||||
|
BusinessBlockInstance instance = validLineChartInstance();
|
||||||
|
List<ChartFilterConfig> filters = new ArrayList<ChartFilterConfig>();
|
||||||
|
for (int i = 0; i < 11; i++)
|
||||||
|
{
|
||||||
|
filters.add(filter("status", "eq", "PAID"));
|
||||||
|
}
|
||||||
|
instance.getDataset().setFilters(filters);
|
||||||
|
|
||||||
|
ServiceException exception = expectServiceException(() ->
|
||||||
|
validator.validate(lineChartDefinition(), instance, chartTables()));
|
||||||
|
|
||||||
|
assertTrue(exception.getMessage().contains("10"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void chartValidationRejectsInvalidTimeUnit()
|
||||||
|
{
|
||||||
|
BusinessBlockInstance instance = validLineChartInstance();
|
||||||
|
instance.getDataset().getDimension().setTimeUnit("quarter");
|
||||||
|
|
||||||
|
ServiceException exception = expectServiceException(() ->
|
||||||
|
validator.validate(lineChartDefinition(), instance, chartTables()));
|
||||||
|
|
||||||
|
assertTrue(exception.getMessage().contains("时间粒度"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void chartValidationRejectsUnknownFilterOperator()
|
||||||
|
{
|
||||||
|
BusinessBlockInstance instance = validLineChartInstance();
|
||||||
|
instance.getDataset().setFilters(Collections.singletonList(filter("status", "contains_sql", "PAID")));
|
||||||
|
|
||||||
|
ServiceException exception = expectServiceException(() ->
|
||||||
|
validator.validate(lineChartDefinition(), instance, chartTables()));
|
||||||
|
|
||||||
|
assertTrue(exception.getMessage().contains("运算符"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void chartValidationRejectsDuplicateMetricAliases()
|
||||||
|
{
|
||||||
|
BusinessBlockInstance instance = validLineChartInstance();
|
||||||
|
instance.getDataset().setMetrics(Arrays.asList(
|
||||||
|
metric("amount", "sum", "metric1"),
|
||||||
|
metric("amount", "avg", "metric1")));
|
||||||
|
|
||||||
|
ServiceException exception = expectServiceException(() ->
|
||||||
|
validator.validate(lineChartDefinition(), instance, chartTables()));
|
||||||
|
|
||||||
|
assertTrue(exception.getMessage().contains("别名"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void chartValidationRejectsLimitAboveFiveHundred()
|
||||||
|
{
|
||||||
|
BusinessBlockInstance instance = validLineChartInstance();
|
||||||
|
instance.getDataset().setLimit(Integer.valueOf(501));
|
||||||
|
|
||||||
|
ServiceException exception = expectServiceException(() ->
|
||||||
|
validator.validate(lineChartDefinition(), instance, chartTables()));
|
||||||
|
|
||||||
|
assertTrue(exception.getMessage().contains("500"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private BusinessBlockDefinition lineChartDefinition()
|
||||||
|
{
|
||||||
|
BusinessBlockDefinition definition = new BusinessBlockDefinition();
|
||||||
|
definition.setCode("admin_line_chart");
|
||||||
|
definition.setName("折线图");
|
||||||
|
definition.setKind("chart");
|
||||||
|
definition.setChartType("line");
|
||||||
|
definition.setDatasetSchema("single-table-aggregate-v1");
|
||||||
|
definition.setSurfaces(Collections.singletonList("admin"));
|
||||||
|
definition.setPlacements(Arrays.asList("embedded", "page"));
|
||||||
|
definition.setDefaultSpan(Integer.valueOf(12));
|
||||||
|
return definition;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BusinessBlockInstance validLineChartInstance()
|
||||||
|
{
|
||||||
|
BusinessBlockInstance instance = new BusinessBlockInstance();
|
||||||
|
instance.setId("admin_line_chart_001");
|
||||||
|
instance.setBlockCode("admin_line_chart");
|
||||||
|
instance.setName("近 30 天销售趋势");
|
||||||
|
instance.setSpan(Integer.valueOf(6));
|
||||||
|
|
||||||
|
ChartDimensionConfig dimension = new ChartDimensionConfig();
|
||||||
|
dimension.setField("create_time");
|
||||||
|
dimension.setTimeUnit("day");
|
||||||
|
|
||||||
|
ChartDatasetConfig dataset = new ChartDatasetConfig();
|
||||||
|
dataset.setSchema("single-table-aggregate-v1");
|
||||||
|
dataset.setTable("shop_order");
|
||||||
|
dataset.setDimension(dimension);
|
||||||
|
dataset.setMetrics(Collections.singletonList(metric("amount", "sum", "salesAmount")));
|
||||||
|
dataset.setFilters(Collections.singletonList(filter("status", "eq", "PAID")));
|
||||||
|
ChartSortConfig sort = new ChartSortConfig();
|
||||||
|
sort.setBy("dimension");
|
||||||
|
sort.setOrder("asc");
|
||||||
|
dataset.setSort(sort);
|
||||||
|
dataset.setLimit(Integer.valueOf(100));
|
||||||
|
instance.setDataset(dataset);
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChartMetricConfig metric(String field, String aggregate, String alias)
|
||||||
|
{
|
||||||
|
ChartMetricConfig metric = new ChartMetricConfig();
|
||||||
|
metric.setField(field);
|
||||||
|
metric.setAggregate(aggregate);
|
||||||
|
metric.setAlias(alias);
|
||||||
|
metric.setLabel(alias);
|
||||||
|
return metric;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChartFilterConfig filter(String field, String operator, Object value)
|
||||||
|
{
|
||||||
|
ChartFilterConfig filter = new ChartFilterConfig();
|
||||||
|
filter.setField(field);
|
||||||
|
filter.setOperator(operator);
|
||||||
|
filter.setValue(value);
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<GenTable> chartTables()
|
||||||
|
{
|
||||||
|
GenTable table = new GenTable();
|
||||||
|
table.setTableName("shop_order");
|
||||||
|
table.setColumns(Arrays.asList(
|
||||||
|
typedColumn("id", "id", "Long", "bigint"),
|
||||||
|
typedColumn("amount", "amount", "BigDecimal", "decimal(10,2)"),
|
||||||
|
typedColumn("status", "status", "String", "varchar(32)"),
|
||||||
|
typedColumn("create_time", "createTime", "Date", "datetime")));
|
||||||
|
return Collections.singletonList(table);
|
||||||
|
}
|
||||||
|
|
||||||
|
private GenTableColumn typedColumn(String columnName, String javaField, String javaType, String columnType)
|
||||||
|
{
|
||||||
|
GenTableColumn column = new GenTableColumn();
|
||||||
|
column.setColumnName(columnName);
|
||||||
|
column.setJavaField(javaField);
|
||||||
|
column.setJavaType(javaType);
|
||||||
|
column.setColumnType(columnType);
|
||||||
|
return column;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ServiceException expectServiceException(ThrowingRunnable runnable)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
runnable.run();
|
||||||
|
}
|
||||||
|
catch (ServiceException e)
|
||||||
|
{
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
throw new AssertionError("Expected ServiceException");
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface ThrowingRunnable
|
||||||
|
{
|
||||||
|
void run();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user