From 7895b5692e3fb27e866ed7beecdc4691086aca1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E9=B9=8F?= Date: Sat, 20 Jun 2026 00:08:32 +0800 Subject: [PATCH] Generate admin chart aggregation endpoints --- .../front/dto/block/ChartQueryParameter.java | 39 +++++++ .../BusinessBlockGenerationService.java | 23 ++++ .../backend/AdminChartController.java.vm | 23 ++++ .../backend/AdminChartMapper.java.vm | 9 ++ .../templates/backend/AdminChartMapper.xml.vm | 25 +++++ .../backend/AdminChartService.java.vm | 8 ++ .../backend/AdminChartServiceImpl.java.vm | 101 ++++++++++++++++++ .../BusinessBlockGenerationServiceTest.java | 37 +++++++ .../util/ChartBlockRenderSupportTest.java | 11 ++ 9 files changed, 276 insertions(+) create mode 100644 RuoYi-Vue/ruoyi-generator/src/main/resources/business-blocks/chart/templates/backend/AdminChartController.java.vm create mode 100644 RuoYi-Vue/ruoyi-generator/src/main/resources/business-blocks/chart/templates/backend/AdminChartMapper.java.vm create mode 100644 RuoYi-Vue/ruoyi-generator/src/main/resources/business-blocks/chart/templates/backend/AdminChartMapper.xml.vm create mode 100644 RuoYi-Vue/ruoyi-generator/src/main/resources/business-blocks/chart/templates/backend/AdminChartService.java.vm create mode 100644 RuoYi-Vue/ruoyi-generator/src/main/resources/business-blocks/chart/templates/backend/AdminChartServiceImpl.java.vm diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/block/ChartQueryParameter.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/block/ChartQueryParameter.java index 01c1892..4fe19c9 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/block/ChartQueryParameter.java +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/block/ChartQueryParameter.java @@ -34,4 +34,43 @@ public class ChartQueryParameter { 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(); + } } diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/BusinessBlockGenerationService.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/BusinessBlockGenerationService.java index 28475ad..1c59e97 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/BusinessBlockGenerationService.java +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/BusinessBlockGenerationService.java @@ -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.BusinessBlockInstance; 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.PageBusinessBlockLayout; import com.ruoyi.generator.service.front.BusinessBlockRegistryService; @@ -473,6 +475,11 @@ public class BusinessBlockGenerationService context.put("chartType", file.getDefinition().getChartType()); context.put("chartDataset", instance.getDataset()); 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("cartPkColumn", pkColumnName(project, renderConfig.get("cartTable"))); @@ -530,6 +537,22 @@ public class BusinessBlockGenerationService return renderConfig; } + private List> chartMetricRenderModels(List metrics) + { + List> result = new ArrayList>(); + for (ChartMetricConfig metric : metrics == null + ? Collections.emptyList() : metrics) + { + Map item = new LinkedHashMap(); + 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, FrontProjectPageDesign pageDesign) { diff --git a/RuoYi-Vue/ruoyi-generator/src/main/resources/business-blocks/chart/templates/backend/AdminChartController.java.vm b/RuoYi-Vue/ruoyi-generator/src/main/resources/business-blocks/chart/templates/backend/AdminChartController.java.vm new file mode 100644 index 0000000..85f542a --- /dev/null +++ b/RuoYi-Vue/ruoyi-generator/src/main/resources/business-blocks/chart/templates/backend/AdminChartController.java.vm @@ -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> data() + { + return Result.success(${instanceClassName}Service.loadData()); + } +} diff --git a/RuoYi-Vue/ruoyi-generator/src/main/resources/business-blocks/chart/templates/backend/AdminChartMapper.java.vm b/RuoYi-Vue/ruoyi-generator/src/main/resources/business-blocks/chart/templates/backend/AdminChartMapper.java.vm new file mode 100644 index 0000000..2eb78cd --- /dev/null +++ b/RuoYi-Vue/ruoyi-generator/src/main/resources/business-blocks/chart/templates/backend/AdminChartMapper.java.vm @@ -0,0 +1,9 @@ +package ${packageName}.mapper.block; + +import java.util.List; +import java.util.Map; + +public interface ${InstanceClassName}Mapper +{ + List> selectData(Map params); +} diff --git a/RuoYi-Vue/ruoyi-generator/src/main/resources/business-blocks/chart/templates/backend/AdminChartMapper.xml.vm b/RuoYi-Vue/ruoyi-generator/src/main/resources/business-blocks/chart/templates/backend/AdminChartMapper.xml.vm new file mode 100644 index 0000000..d91fa0d --- /dev/null +++ b/RuoYi-Vue/ruoyi-generator/src/main/resources/business-blocks/chart/templates/backend/AdminChartMapper.xml.vm @@ -0,0 +1,25 @@ + + + + + + diff --git a/RuoYi-Vue/ruoyi-generator/src/main/resources/business-blocks/chart/templates/backend/AdminChartService.java.vm b/RuoYi-Vue/ruoyi-generator/src/main/resources/business-blocks/chart/templates/backend/AdminChartService.java.vm new file mode 100644 index 0000000..4e8ddf3 --- /dev/null +++ b/RuoYi-Vue/ruoyi-generator/src/main/resources/business-blocks/chart/templates/backend/AdminChartService.java.vm @@ -0,0 +1,8 @@ +package ${packageName}.service.block; + +import java.util.Map; + +public interface I${InstanceClassName}Service +{ + Map loadData(); +} diff --git a/RuoYi-Vue/ruoyi-generator/src/main/resources/business-blocks/chart/templates/backend/AdminChartServiceImpl.java.vm b/RuoYi-Vue/ruoyi-generator/src/main/resources/business-blocks/chart/templates/backend/AdminChartServiceImpl.java.vm new file mode 100644 index 0000000..91f833b --- /dev/null +++ b/RuoYi-Vue/ruoyi-generator/src/main/resources/business-blocks/chart/templates/backend/AdminChartServiceImpl.java.vm @@ -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 loadData() + { + Map params = new LinkedHashMap(); +#foreach($parameter in $chartQuery.parameters) + params.put("${parameter.name}", ${parameter.javaLiteral}); +#end + putDateRange(params); + + List> rows = ${instanceClassName}Mapper.selectData(params); + List> dimensions = new ArrayList>(); + List> series = new ArrayList>(); + Map summary = new LinkedHashMap(); + +#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 row : rows) + { + Map dimension = new LinkedHashMap(); + 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 result = new LinkedHashMap(); + result.put("dimensions", dimensions); + result.put("series", series); + result.put("summary", summary); + return result; + } + + private Map seriesItem(String name, String alias) + { + Map item = new LinkedHashMap(); + item.put("name", name); + item.put("alias", alias); + item.put("values", new ArrayList()); + return item; + } + + @SuppressWarnings("unchecked") + private void appendSeriesValue(List> series, int index, Object value) + { + ((List) series.get(index).get("values")).add(value); + } + + private void putDateRange(Map 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 + } +} diff --git a/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/BusinessBlockGenerationServiceTest.java b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/BusinessBlockGenerationServiceTest.java index 233ecee..7376843 100644 --- a/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/BusinessBlockGenerationServiceTest.java +++ b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/BusinessBlockGenerationServiceTest.java @@ -344,6 +344,29 @@ public class BusinessBlockGenerationServiceTest assertEquals(Integer.valueOf(4), blocks.get(1).get("span")); } + @Test + public void generatedChartBackendUsesBoundParametersAndUnifiedResponse() + { + Map rendered = renderChartFiles(chartProject()); + + assertTrue(rendered.get("mapper").contains( + "List> selectData(Map 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 { Field field = BusinessBlockGenerationService.class.getDeclaredField(name); @@ -406,6 +429,19 @@ public class BusinessBlockGenerationServiceTest return count; } + private Map renderChartFiles(GenProject project) + { + Map rendered = new LinkedHashMap(); + 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 files) { for (GeneratedBusinessBlockFile file : files) @@ -550,6 +586,7 @@ public class BusinessBlockGenerationServiceTest + "\"dimension\":{\"field\":\"createTime\",\"timeUnit\":\"day\"}," + "\"metrics\":[{\"field\":\"totalAmount\",\"aggregate\":\"sum\",\"alias\":\"salesAmount\",\"label\":\"Sales\"}]," + "\"filters\":[{\"field\":\"status\",\"operator\":\"eq\",\"value\":\"PAID\"}]," + + "\"dateRange\":{\"field\":\"createTime\",\"preset\":\"last_30_days\"}," + "\"sort\":{\"by\":\"dimension\",\"order\":\"asc\"},\"limit\":100}," + "\"display\":{\"title\":\"Sales Trend\",\"smooth\":true}}," + "{\"id\":\"admin_metric_chart_001\",\"blockCode\":\"admin_metric_chart\",\"name\":\"Order Count\",\"span\":4," diff --git a/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/util/ChartBlockRenderSupportTest.java b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/util/ChartBlockRenderSupportTest.java index 1d1053b..fbc1807 100644 --- a/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/util/ChartBlockRenderSupportTest.java +++ b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/util/ChartBlockRenderSupportTest.java @@ -53,6 +53,17 @@ public class ChartBlockRenderSupportTest 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 public void formatsDayWeekAndMonthDimensionsFromResolvedColumns() {