Generate admin chart aggregation endpoints

This commit is contained in:
王鹏
2026-06-20 00:08:32 +08:00
parent a8b1769e32
commit 7895b5692e
9 changed files with 276 additions and 0 deletions

View File

@@ -34,4 +34,43 @@ public class ChartQueryParameter
{ {
this.value = value; this.value = value;
} }
public String getJavaLiteral()
{
return toJavaLiteral(value);
}
public static String toJavaLiteral(Object value)
{
if (value == null)
{
return "null";
}
if (value instanceof Number || value instanceof Boolean)
{
return String.valueOf(value);
}
return "\"" + escapeJava(String.valueOf(value)) + "\"";
}
private static String escapeJava(String value)
{
StringBuilder escaped = new StringBuilder();
for (int index = 0; index < value.length(); index++)
{
char ch = value.charAt(index);
switch (ch)
{
case '\\': escaped.append("\\\\"); break;
case '"': escaped.append("\\\""); break;
case '\n': escaped.append("\\n"); break;
case '\r': escaped.append("\\r"); break;
case '\t': escaped.append("\\t"); break;
case '\b': escaped.append("\\b"); break;
case '\f': escaped.append("\\f"); break;
default: escaped.append(ch);
}
}
return escaped.toString();
}
} }

View File

@@ -22,6 +22,8 @@ import com.ruoyi.generator.domain.front.dto.block.BusinessBlockConfigField;
import com.ruoyi.generator.domain.front.dto.block.BusinessBlockDefinition; 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.BusinessBlockInstance;
import com.ruoyi.generator.domain.front.dto.block.BusinessBlockTemplateFile; import com.ruoyi.generator.domain.front.dto.block.BusinessBlockTemplateFile;
import com.ruoyi.generator.domain.front.dto.block.ChartMetricConfig;
import com.ruoyi.generator.domain.front.dto.block.ChartQueryParameter;
import com.ruoyi.generator.domain.front.dto.block.ChartQueryRenderModel; import com.ruoyi.generator.domain.front.dto.block.ChartQueryRenderModel;
import com.ruoyi.generator.domain.front.dto.block.PageBusinessBlockLayout; import com.ruoyi.generator.domain.front.dto.block.PageBusinessBlockLayout;
import com.ruoyi.generator.service.front.BusinessBlockRegistryService; import com.ruoyi.generator.service.front.BusinessBlockRegistryService;
@@ -473,6 +475,11 @@ public class BusinessBlockGenerationService
context.put("chartType", file.getDefinition().getChartType()); context.put("chartType", file.getDefinition().getChartType());
context.put("chartDataset", instance.getDataset()); context.put("chartDataset", instance.getDataset());
context.put("chartDisplay", instance.getDisplay()); context.put("chartDisplay", instance.getDisplay());
context.put("chartMetrics", chartMetricRenderModels(chartQuery.getMetrics()));
context.put("chartDateStartLiteral", ChartQueryParameter.toJavaLiteral(
chartQuery.getDateRange() == null ? null : chartQuery.getDateRange().getStart()));
context.put("chartDateEndLiteral", ChartQueryParameter.toJavaLiteral(
chartQuery.getDateRange() == null ? null : chartQuery.getDateRange().getEnd()));
context.put("apiBasePath", "/admin/business-blocks/" + file.getInstanceCode()); context.put("apiBasePath", "/admin/business-blocks/" + file.getInstanceCode());
} }
context.put("cartPkColumn", pkColumnName(project, renderConfig.get("cartTable"))); context.put("cartPkColumn", pkColumnName(project, renderConfig.get("cartTable")));
@@ -530,6 +537,22 @@ public class BusinessBlockGenerationService
return renderConfig; return renderConfig;
} }
private List<Map<String, String>> chartMetricRenderModels(List<ChartMetricConfig> metrics)
{
List<Map<String, String>> result = new ArrayList<Map<String, String>>();
for (ChartMetricConfig metric : metrics == null
? Collections.<ChartMetricConfig>emptyList() : metrics)
{
Map<String, String> item = new LinkedHashMap<String, String>();
item.put("alias", metric.getAlias());
item.put("aliasLiteral", ChartQueryParameter.toJavaLiteral(metric.getAlias()));
item.put("labelLiteral", ChartQueryParameter.toJavaLiteral(
StringUtils.defaultIfEmpty(metric.getLabel(), metric.getAlias())));
result.add(item);
}
return result;
}
public void putEmbeddedBlockVelocityContext(VelocityContext context, GenProject project, public void putEmbeddedBlockVelocityContext(VelocityContext context, GenProject project,
FrontProjectPageDesign pageDesign) FrontProjectPageDesign pageDesign)
{ {

View File

@@ -0,0 +1,23 @@
package ${packageName}.controller.block;
import java.util.Map;
import ${packageName}.common.Result;
import ${packageName}.service.block.I${InstanceClassName}Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("${apiBasePath}")
public class ${InstanceClassName}Controller
{
@Autowired
private I${InstanceClassName}Service ${instanceClassName}Service;
@GetMapping("/data")
public Result<Map<String, Object>> data()
{
return Result.success(${instanceClassName}Service.loadData());
}
}

View File

@@ -0,0 +1,9 @@
package ${packageName}.mapper.block;
import java.util.List;
import java.util.Map;
public interface ${InstanceClassName}Mapper
{
List<Map<String, Object>> selectData(Map<String, Object> params);
}

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="${packageName}.mapper.block.${InstanceClassName}Mapper">
<select id="selectData" parameterType="java.util.Map" resultType="java.util.LinkedHashMap">
select
#if(!$chartQuery.metricOnly)
${chartQuery.dimensionSql} as dimensionKey,
#end
${chartQuery.metricSelectSql}
from ${chartQuery.tableName} t
#if($chartQuery.whereSql)
where ${chartQuery.whereSql}
#end
#if($chartQuery.groupBySql)
group by ${chartQuery.groupBySql}
#end
#if($chartQuery.orderBySql)
order by ${chartQuery.orderBySql}
#end
limit ${chartQuery.limit}
</select>
</mapper>

View File

@@ -0,0 +1,8 @@
package ${packageName}.service.block;
import java.util.Map;
public interface I${InstanceClassName}Service
{
Map<String, Object> loadData();
}

View File

@@ -0,0 +1,101 @@
package ${packageName}.service.block.impl;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import ${packageName}.mapper.block.${InstanceClassName}Mapper;
import ${packageName}.service.block.I${InstanceClassName}Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class ${InstanceClassName}ServiceImpl implements I${InstanceClassName}Service
{
@Autowired
private ${InstanceClassName}Mapper ${instanceClassName}Mapper;
@Override
public Map<String, Object> loadData()
{
Map<String, Object> params = new LinkedHashMap<String, Object>();
#foreach($parameter in $chartQuery.parameters)
params.put("${parameter.name}", ${parameter.javaLiteral});
#end
putDateRange(params);
List<Map<String, Object>> rows = ${instanceClassName}Mapper.selectData(params);
List<Map<String, Object>> dimensions = new ArrayList<Map<String, Object>>();
List<Map<String, Object>> series = new ArrayList<Map<String, Object>>();
Map<String, Object> summary = new LinkedHashMap<String, Object>();
#if($chartQuery.metricOnly)
Object value = rows.isEmpty() ? null : rows.get(0).get(${chartMetrics.get(0).aliasLiteral});
summary.put("value", value);
summary.put("count", Integer.valueOf(rows.size()));
#else
#foreach($metric in $chartMetrics)
series.add(seriesItem(${metric.labelLiteral}, ${metric.aliasLiteral}));
#end
for (Map<String, Object> row : rows)
{
Map<String, Object> dimension = new LinkedHashMap<String, Object>();
dimension.put("label", row.get("dimensionKey"));
dimension.put("value", row.get("dimensionKey"));
dimensions.add(dimension);
#foreach($metric in $chartMetrics)
appendSeriesValue(series, ${foreach.index}, row.get(${metric.aliasLiteral}));
#end
}
summary.put("count", Integer.valueOf(rows.size()));
#end
Map<String, Object> result = new LinkedHashMap<String, Object>();
result.put("dimensions", dimensions);
result.put("series", series);
result.put("summary", summary);
return result;
}
private Map<String, Object> seriesItem(String name, String alias)
{
Map<String, Object> item = new LinkedHashMap<String, Object>();
item.put("name", name);
item.put("alias", alias);
item.put("values", new ArrayList<Object>());
return item;
}
@SuppressWarnings("unchecked")
private void appendSeriesValue(List<Map<String, Object>> series, int index, Object value)
{
((List<Object>) series.get(index).get("values")).add(value);
}
private void putDateRange(Map<String, Object> params)
{
#if($chartQuery.dateRange)
LocalDate today = LocalDate.now();
#if($chartQuery.dateRange.preset == "today")
params.put("dateStart", today.atStartOfDay());
params.put("dateEnd", today.plusDays(1).atStartOfDay());
#elseif($chartQuery.dateRange.preset == "last_7_days")
params.put("dateStart", today.minusDays(6).atStartOfDay());
params.put("dateEnd", today.plusDays(1).atStartOfDay());
#elseif($chartQuery.dateRange.preset == "last_30_days")
params.put("dateStart", today.minusDays(29).atStartOfDay());
params.put("dateEnd", today.plusDays(1).atStartOfDay());
#elseif($chartQuery.dateRange.preset == "this_month")
params.put("dateStart", today.withDayOfMonth(1).atStartOfDay());
params.put("dateEnd", today.withDayOfMonth(1).plusMonths(1).atStartOfDay());
#elseif($chartQuery.dateRange.preset == "this_year")
params.put("dateStart", today.withDayOfYear(1).atStartOfDay());
params.put("dateEnd", today.withDayOfYear(1).plusYears(1).atStartOfDay());
#else
params.put("dateStart", LocalDate.parse(${chartDateStartLiteral}).atStartOfDay());
params.put("dateEnd", LocalDate.parse(${chartDateEndLiteral}).plusDays(1).atStartOfDay());
#end
#end
}
}

View File

@@ -344,6 +344,29 @@ public class BusinessBlockGenerationServiceTest
assertEquals(Integer.valueOf(4), blocks.get(1).get("span")); assertEquals(Integer.valueOf(4), blocks.get(1).get("span"));
} }
@Test
public void generatedChartBackendUsesBoundParametersAndUnifiedResponse()
{
Map<String, String> rendered = renderChartFiles(chartProject());
assertTrue(rendered.get("mapper").contains(
"List<Map<String, Object>> selectData(Map<String, Object> params)"));
assertTrue(rendered.get("mapperXml").contains("t.status = #{filter0}"));
assertTrue(rendered.get("mapperXml").contains("t.create_time >= #{dateStart}"));
assertTrue(rendered.get("mapperXml").contains("t.create_time < #{dateEnd}"));
assertFalse(rendered.get("mapperXml").contains("PAID"));
assertTrue(rendered.get("mapperXml").contains("limit 100"));
assertTrue(rendered.get("serviceImpl").contains("params.put(\"filter0\", \"PAID\")"));
assertTrue(rendered.get("serviceImpl").contains("today.minusDays(29).atStartOfDay()"));
assertTrue(rendered.get("serviceImpl").contains("seriesItem(\"Sales\", \"salesAmount\")"));
assertTrue(rendered.get("serviceImpl").contains("\"dimensions\""));
assertTrue(rendered.get("serviceImpl").contains("\"series\""));
assertTrue(rendered.get("serviceImpl").contains("\"summary\""));
assertTrue(rendered.get("controller").contains(
"@RequestMapping(\"/admin/business-blocks/admin_line_chart_001\")"));
assertTrue(rendered.get("controller").contains("@GetMapping(\"/data\")"));
}
private void setField(String name, Object value) throws Exception private void setField(String name, Object value) throws Exception
{ {
Field field = BusinessBlockGenerationService.class.getDeclaredField(name); Field field = BusinessBlockGenerationService.class.getDeclaredField(name);
@@ -406,6 +429,19 @@ public class BusinessBlockGenerationServiceTest
return count; return count;
} }
private Map<String, String> renderChartFiles(GenProject project)
{
Map<String, String> rendered = new LinkedHashMap<String, String>();
for (GeneratedBusinessBlockFile file : service.listFiles(project, "backend"))
{
if ("admin_line_chart_001".equals(file.getInstanceCode()))
{
rendered.put(file.getTemplateKey(), service.renderFile(project, file));
}
}
return rendered;
}
private void assertRenderedFiles(GenProject project, List<GeneratedBusinessBlockFile> files) private void assertRenderedFiles(GenProject project, List<GeneratedBusinessBlockFile> files)
{ {
for (GeneratedBusinessBlockFile file : files) for (GeneratedBusinessBlockFile file : files)
@@ -550,6 +586,7 @@ public class BusinessBlockGenerationServiceTest
+ "\"dimension\":{\"field\":\"createTime\",\"timeUnit\":\"day\"}," + "\"dimension\":{\"field\":\"createTime\",\"timeUnit\":\"day\"},"
+ "\"metrics\":[{\"field\":\"totalAmount\",\"aggregate\":\"sum\",\"alias\":\"salesAmount\",\"label\":\"Sales\"}]," + "\"metrics\":[{\"field\":\"totalAmount\",\"aggregate\":\"sum\",\"alias\":\"salesAmount\",\"label\":\"Sales\"}],"
+ "\"filters\":[{\"field\":\"status\",\"operator\":\"eq\",\"value\":\"PAID\"}]," + "\"filters\":[{\"field\":\"status\",\"operator\":\"eq\",\"value\":\"PAID\"}],"
+ "\"dateRange\":{\"field\":\"createTime\",\"preset\":\"last_30_days\"},"
+ "\"sort\":{\"by\":\"dimension\",\"order\":\"asc\"},\"limit\":100}," + "\"sort\":{\"by\":\"dimension\",\"order\":\"asc\"},\"limit\":100},"
+ "\"display\":{\"title\":\"Sales Trend\",\"smooth\":true}}," + "\"display\":{\"title\":\"Sales Trend\",\"smooth\":true}},"
+ "{\"id\":\"admin_metric_chart_001\",\"blockCode\":\"admin_metric_chart\",\"name\":\"Order Count\",\"span\":4," + "{\"id\":\"admin_metric_chart_001\",\"blockCode\":\"admin_metric_chart\",\"name\":\"Order Count\",\"span\":4,"

View File

@@ -53,6 +53,17 @@ public class ChartBlockRenderSupportTest
assertFalse(model.getWhereSql().contains("OR 1=1")); assertFalse(model.getWhereSql().contains("OR 1=1"));
} }
@Test
public void rendersFilterValuesAsSafeJavaLiteralsForGeneratedServices()
{
BusinessBlockInstance instance = lineInstance();
instance.getDataset().getFilters().get(0).setValue("PAID\" + run();\n//");
ChartQueryRenderModel model = support.build(orderTable(), definition("line"), instance);
assertEquals("\"PAID\\\" + run();\\n//\"", model.getParameters().get(0).getJavaLiteral());
}
@Test @Test
public void formatsDayWeekAndMonthDimensionsFromResolvedColumns() public void formatsDayWeekAndMonthDimensionsFromResolvedColumns()
{ {