diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/BusinessBlockRegistryService.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/BusinessBlockRegistryService.java index 350d848..db2e2a4 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/BusinessBlockRegistryService.java +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/BusinessBlockRegistryService.java @@ -19,6 +19,7 @@ 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.PageBusinessBlockLayout; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.Resource; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.stereotype.Service; @@ -32,6 +33,9 @@ public class BusinessBlockRegistryService private volatile List cachedDefinitions; + @Autowired + private ChartDatasetValidator chartDatasetValidator; + public List listDefinitions() { if (cachedDefinitions == null) @@ -92,6 +96,11 @@ public class BusinessBlockRegistryService } public void validateLayout(String layoutJson, List tables) + { + validateLayout(layoutJson, tables, "frontend"); + } + + public void validateLayout(String layoutJson, List tables, String surface) { PageBusinessBlockLayout layout = parseLayout(layoutJson); if (!BUSINESS_BLOCK_CANVAS.equals(layout.getCanvas())) @@ -105,7 +114,7 @@ public class BusinessBlockRegistryService continue; } BusinessBlockDefinition definition = findDefinition(instance.getBlockCode()); - validateInstanceConfig(definition, instance, tables); + validateInstanceConfig(definition, instance, tables, surface); } } @@ -147,6 +156,11 @@ public class BusinessBlockRegistryService } public void validateEmbeddedBlocks(String layoutJson, List tables) + { + validateEmbeddedBlocks(layoutJson, tables, "frontend"); + } + + public void validateEmbeddedBlocks(String layoutJson, List tables, String surface) { Map> zones = parseEmbeddedBlockZones(layoutJson); for (List blocks : zones.values()) @@ -166,7 +180,7 @@ public class BusinessBlockRegistryService { throw new ServiceException("业务块只能放在业务块页面:" + instance.getBlockCode()); } - validateInstanceConfig(definition, instance, tables); + validateInstanceConfig(definition, instance, tables, surface); } } } @@ -218,6 +232,37 @@ public class BusinessBlockRegistryService } } + public void validateInstanceConfig(BusinessBlockDefinition definition, BusinessBlockInstance instance, + List tables, String surface) + { + String normalizedSurface = "admin".equals(surface) ? "admin" : "frontend"; + if (definition == null || definition.getSurfaces() == null + || !definition.getSurfaces().contains(normalizedSurface)) + { + String name = definition == null + ? StringUtils.defaultString(instance == null ? null : instance.getBlockCode()) + : StringUtils.defaultIfEmpty(definition.getName(), definition.getCode()); + String allowedSurface = definition != null && definition.getSurfaces() != null + && definition.getSurfaces().contains("admin") ? "后台" : "前台"; + throw new ServiceException("业务块“" + name + "”仅支持" + allowedSurface + "页面"); + } + if ("chart".equals(definition.getKind())) + { + chartValidator().validate(definition, instance, tables); + return; + } + validateInstanceConfig(definition, instance, tables); + } + + private ChartDatasetValidator chartValidator() + { + if (chartDatasetValidator == null) + { + chartDatasetValidator = new ChartDatasetValidator(); + } + return chartDatasetValidator; + } + private List loadDefinitions() { try diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/ChartDatasetValidator.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/ChartDatasetValidator.java index 24010a3..0f0a43f 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/ChartDatasetValidator.java +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/ChartDatasetValidator.java @@ -22,7 +22,9 @@ 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; +import org.springframework.stereotype.Service; +@Service public class ChartDatasetValidator { public static final String DATASET_SCHEMA = "single-table-aggregate-v1"; diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontendPageDesignService.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontendPageDesignService.java index 3855483..8c921b2 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontendPageDesignService.java +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontendPageDesignService.java @@ -7,6 +7,7 @@ import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; import com.fasterxml.jackson.core.type.TypeReference; @@ -36,8 +37,13 @@ import com.ruoyi.generator.mapper.front.FrontProjectTableMapper; public class FrontendPageDesignService { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final String MENU_DIRECTORY_PAGE_TYPE = "menu_directory"; + private static final String HOME_PAGE_TYPE = "business_block"; + private static final String HOME_PAGE_NAME = "\u4e3b\u9875"; + private static final String FRONTEND_HOME_PAGE_CODE = "home"; + private static final String ADMIN_HOME_PAGE_CODE = "admin_home"; private static final Set PAGE_TYPES = new HashSet( - Arrays.asList("list", "detail", "form", "current_user_list", "business_block")); + Arrays.asList("list", "detail", "form", "current_user_list", "business_block", MENU_DIRECTORY_PAGE_TYPE)); private static final Set REGION_TYPES = new HashSet( Arrays.asList("query", "toolbar", "table", "dialogForm", "detail", "form", "cardList", "actions")); private static final Set BUILTIN_ACTIONS = new HashSet( @@ -65,7 +71,8 @@ public class FrontendPageDesignService public List listPageDesigns(Long userId, Long projectId, String scope) { frontProjectService.getProject(userId, projectId); - return toResponses(pageDesignMapper.selectDesignsByProjectIdAndScope(projectId, normalizeScope(scope))); + String normalizedScope = normalizeScope(scope); + return toScopedResponses(pageDesignMapper.selectDesignsByProjectIdAndScope(projectId, normalizedScope), normalizedScope); } public FrontendPageDesignResponse getPageDesign(Long userId, Long projectId, Long designId) @@ -109,7 +116,7 @@ public class FrontendPageDesignService Map existingByPageCode = new LinkedHashMap(); for (FrontProjectPageDesign design : existing) { - if (StringUtils.isNotEmpty(design.getPageCode())) + if (shouldExposeDesign(design, FRONTEND_SCOPE) && StringUtils.isNotEmpty(design.getPageCode())) { existingByPageCode.put(design.getPageCode(), design); } @@ -118,13 +125,20 @@ public class FrontendPageDesignService Map tablesByName = loadTables(projectId); List merged = new ArrayList(); Set blueprintPageCodes = new LinkedHashSet(); + FrontProjectPageDesign homeDesign = getOrCreateHomeDesign(projectId, userId, FRONTEND_SCOPE, existingByPageCode); + merged.add(homeDesign); + blueprintPageCodes.add(homeDesign.getPageCode()); for (AppPageDesign page : blueprint.getFrontendPages()) { - if (page == null || StringUtils.isEmpty(page.getCode())) + if (page == null || StringUtils.isEmpty(page.getCode()) || isAdminLikePage(page)) { continue; } blueprintPageCodes.add(page.getCode()); + if (isHomePageCode(page.getCode(), FRONTEND_SCOPE)) + { + continue; + } FrontProjectPageDesign design = existingByPageCode.get(page.getCode()); if (design == null) { @@ -137,7 +151,7 @@ public class FrontendPageDesignService List detached = new ArrayList(); for (FrontProjectPageDesign design : existing) { - if (!blueprintPageCodes.contains(design.getPageCode())) + if (shouldExposeDesign(design, FRONTEND_SCOPE) && !blueprintPageCodes.contains(design.getPageCode())) { detached.add(FrontendPageDesignResponse.from(design)); } @@ -172,6 +186,9 @@ public class FrontendPageDesignService Map unmatchedTables = new LinkedHashMap(tablesByName); List merged = new ArrayList(); Set expectedPageCodes = new LinkedHashSet(); + FrontProjectPageDesign homeDesign = getOrCreateHomeDesign(projectId, userId, ADMIN_SCOPE, existingByPageCode); + merged.add(homeDesign); + expectedPageCodes.add(homeDesign.getPageCode()); if (blueprint.getAdminMenus() != null) { for (AppMenuDesign menu : blueprint.getAdminMenus()) @@ -180,6 +197,11 @@ public class FrontendPageDesignService { continue; } + if (isHomePageCode(menu.getCode(), ADMIN_SCOPE)) + { + expectedPageCodes.add(menu.getCode()); + continue; + } TableBundle table = findAdminMenuTable(menu, unmatchedTables); if (table == null) { @@ -253,6 +275,7 @@ public class FrontendPageDesignService create.setRoutePath(request.getRoutePath()); create.setPageType(defaultText(request.getPageType(), "list")); create.setTableName(request.getTableName()); + normalizeMenuDirectoryMeta(create); create.setStatus(defaultText(request.getStatus(), "0")); create.setRemark(request.getRemark()); create.setVersion(1); @@ -272,7 +295,8 @@ public class FrontendPageDesignService } String layoutJson = defaultText(request.getLayoutJson(), defaultLayoutJson(create.getPageType(), table, normalizedScope)); - String actionJson = defaultText(request.getActionJson(), defaultActionJson(table, normalizedScope)); + String actionJson = defaultText(request.getActionJson(), + isMenuDirectory(create.getPageType()) ? "{}" : defaultActionJson(table, normalizedScope)); create.setLayoutJson(normalizeLayout(layoutJson, table, create.getPageType(), create.getPageScope(), tablesByName)); create.setActionJson(normalizeAction(actionJson, table, create.getPageScope())); pageDesignMapper.insertFrontProjectPageDesign(create); @@ -315,6 +339,7 @@ public class FrontendPageDesignService update.setRoutePath(defaultText(request.getRoutePath(), current.getRoutePath())); update.setPageType(defaultText(request.getPageType(), current.getPageType())); update.setTableName(defaultText(request.getTableName(), current.getTableName())); + normalizeMenuDirectoryMeta(update); update.setStatus(defaultText(request.getStatus(), current.getStatus())); update.setRemark(defaultText(request.getRemark(), current.getRemark())); update.setVersion(currentVersion + 1); @@ -385,15 +410,83 @@ public class FrontendPageDesignService design.setMenuCode(page.getMenuCode()); design.setPageName(defaultText(page.getName(), page.getCode())); design.setRoutePath(defaultText(page.getPath(), "/" + page.getCode())); - design.setPageType(defaultText(page.getPageType(), "list")); + String pageType = defaultText(page.getPageType(), "list"); + design.setPageType(pageType); design.setTableName(page.getTableName()); - design.setLayoutJson(defaultLayoutJson(defaultText(page.getPageType(), "list"), table, scope)); + design.setLayoutJson(defaultLayoutJson(pageType, table, scope, page)); design.setActionJson(defaultActionJson(table, scope)); design.setStatus("0"); design.setVersion(1); return design; } + private FrontProjectPageDesign getOrCreateHomeDesign(Long projectId, Long userId, String scope, + Map existingByPageCode) + { + String pageCode = homePageCode(scope); + FrontProjectPageDesign design = existingByPageCode.get(pageCode); + if (design != null) + { + return design; + } + design = newDefaultHomeDesign(projectId, userId, scope); + pageDesignMapper.insertFrontProjectPageDesign(design); + existingByPageCode.put(pageCode, design); + return design; + } + + private FrontProjectPageDesign newDefaultHomeDesign(Long projectId, Long userId, String scope) + { + String normalizedScope = normalizeScope(scope); + String pageCode = homePageCode(normalizedScope); + FrontProjectPageDesign design = new FrontProjectPageDesign(); + design.setProjectId(projectId); + design.setUserId(userId); + design.setPageScope(normalizedScope); + design.setPageCode(pageCode); + design.setMenuCode(pageCode); + design.setPageName(HOME_PAGE_NAME); + design.setRoutePath(homeRoutePath(normalizedScope)); + design.setPageType(HOME_PAGE_TYPE); + design.setTableName(""); + design.setLayoutJson(defaultHomeLayoutJson(normalizedScope, pageCode)); + design.setActionJson(emptyActionJson()); + design.setStatus("0"); + design.setVersion(1); + return design; + } + + private String defaultHomeLayoutJson(String scope, String pageCode) + { + Map layout = new LinkedHashMap(); + layout.put("canvas", BusinessBlockRegistryService.BUSINESS_BLOCK_CANVAS); + layout.put("blocks", new ArrayList()); + layout.put("navigation", defaultHomeNavigation(pageCode)); + return writeJson(layout); + } + + private Map defaultHomeNavigation(String pageCode) + { + Map navigation = new LinkedHashMap(); + navigation.put("source", "pageDesigns"); + navigation.put("position", "top"); + navigation.put("display", "dropdown"); + navigation.put("showInMenu", true); + navigation.put("menuLevel", "primary"); + navigation.put("menuCode", pageCode); + navigation.put("menuName", HOME_PAGE_NAME); + navigation.put("menuOrder", Integer.valueOf(1)); + return navigation; + } + + private String emptyActionJson() + { + Map action = new LinkedHashMap(); + action.put("toolbarActions", new ArrayList()); + action.put("rowActions", new ArrayList()); + return writeJson(action); + } + private AppPageDesign adminPageFromMenu(AppMenuDesign menu, FrontProjectTable table) { AppPageDesign page = new AppPageDesign(); @@ -466,7 +559,8 @@ public class FrontendPageDesignService { throw new ServiceException("页面名称不能为空"); } - if (StringUtils.isEmpty(design.getRoutePath())) + boolean menuDirectory = isMenuDirectory(design.getPageType()); + if (StringUtils.isEmpty(design.getRoutePath()) && !menuDirectory) { throw new ServiceException("页面路由不能为空"); } @@ -474,12 +568,23 @@ public class FrontendPageDesignService { throw new ServiceException("页面类型不支持: " + design.getPageType()); } - if (StringUtils.isEmpty(design.getTableName()) && !"business_block".equals(design.getPageType())) + if (StringUtils.isEmpty(design.getTableName()) && !"business_block".equals(design.getPageType()) && !menuDirectory) { throw new ServiceException("页面必须绑定数据表"); } } + private void normalizeMenuDirectoryMeta(FrontProjectPageDesign design) + { + if (!isMenuDirectory(design.getPageType())) + { + return; + } + design.setRoutePath(""); + design.setTableName(""); + design.setMenuCode(defaultText(design.getMenuCode(), design.getPageCode())); + } + private Map loadTables(Long projectId) { Map result = new LinkedHashMap(); @@ -507,6 +612,13 @@ public class FrontendPageDesignService private String defaultLayoutJson(String pageType, TableBundle table, String scope) { + if (isMenuDirectory(pageType)) + { + Map layout = new LinkedHashMap(); + layout.put("canvas", "menu-directory-v1"); + layout.put("navigation", defaultMenuDirectoryNavigation(null, null)); + return writeJson(layout); + } if ("business_block".equals(pageType)) { Map businessLayout = new LinkedHashMap(); @@ -525,6 +637,39 @@ public class FrontendPageDesignService return writeJson(layout); } + private String defaultLayoutJson(String pageType, TableBundle table, String scope, AppPageDesign page) + { + Map layout = readMap(defaultLayoutJson(pageType, table, scope), "椤甸潰甯冨眬JSON鏍煎紡閿欒"); + applyAssociatedPageNavigationDefaults(layout, pageType, page == null ? null : page.getParentPageCode()); + return writeJson(layout); + } + + @SuppressWarnings("unchecked") + private void applyAssociatedPageNavigationDefaults(Map layout, String pageType, String parentPageCode) + { + if (layout == null || isMenuDirectory(pageType)) + { + return; + } + if (!isAssociatedPageType(pageType) && StringUtils.isEmpty(parentPageCode)) + { + return; + } + Object navigationObject = layout.get("navigation"); + Map navigation = navigationObject instanceof Map + ? new LinkedHashMap((Map) navigationObject) + : new LinkedHashMap(); + if (isAssociatedPageType(pageType)) + { + navigation.put("showInMenu", false); + } + if (StringUtils.isNotEmpty(parentPageCode)) + { + navigation.put("parentPageCode", parentPageCode); + } + layout.put("navigation", navigation); + } + private Map region(String id, String type, List fields, List actions, List rowActions) { @@ -662,6 +807,10 @@ public class FrontendPageDesignService private String normalizeLayout(String layoutJson, TableBundle table, String pageType, String scope, Map tablesByName) { + if (isMenuDirectory(pageType)) + { + return normalizeMenuDirectoryLayout(layoutJson); + } if (StringUtils.isEmpty(layoutJson)) { return defaultLayoutJson(pageType, table, scope); @@ -676,7 +825,7 @@ public class FrontendPageDesignService if (BusinessBlockRegistryService.BUSINESS_BLOCK_CANVAS.equals(canvas)) { String normalizedLayout = writeJson(layout); - businessBlockRegistryService.validateLayout(normalizedLayout, toGenTables(tablesByName)); + businessBlockRegistryService.validateLayout(normalizedLayout, toGenTables(tablesByName), scope); return normalizedLayout; } Object regionsObject = layout.get("regions"); @@ -709,10 +858,52 @@ public class FrontendPageDesignService normalizeActionCodeList(region, "actions", table, scope); normalizeActionCodeList(region, "rowActions", table, scope); } - businessBlockRegistryService.validateEmbeddedBlocks(writeJson(layout), toGenTables(tablesByName)); + businessBlockRegistryService.validateEmbeddedBlocks(writeJson(layout), toGenTables(tablesByName), scope); return writeJson(layout); } + @SuppressWarnings("unchecked") + private String normalizeMenuDirectoryLayout(String layoutJson) + { + Map layout = StringUtils.isEmpty(layoutJson) + ? new LinkedHashMap() + : readMap(layoutJson, "页面布局JSON格式错误"); + Object navigationObject = layout.get("navigation"); + Map navigation = navigationObject instanceof Map + ? new LinkedHashMap((Map) navigationObject) + : new LinkedHashMap(); + Map defaults = defaultMenuDirectoryNavigation( + stringValue(navigation.get("menuCode")), + stringValue(navigation.get("menuName"))); + defaults.putAll(navigation); + defaults.put("showInMenu", true); + defaults.put("menuLevel", "primary"); + defaults.put("nodeType", "directory"); + layout.put("canvas", "menu-directory-v1"); + layout.put("navigation", defaults); + return writeJson(layout); + } + + private Map defaultMenuDirectoryNavigation(String menuCode, String menuName) + { + Map navigation = new LinkedHashMap(); + navigation.put("source", "pageDesigns"); + navigation.put("position", "top"); + navigation.put("display", "dropdown"); + navigation.put("showInMenu", true); + navigation.put("menuLevel", "primary"); + navigation.put("nodeType", "directory"); + if (StringUtils.isNotEmpty(menuCode)) + { + navigation.put("menuCode", menuCode); + } + if (StringUtils.isNotEmpty(menuName)) + { + navigation.put("menuName", menuName); + } + return navigation; + } + private void normalizeSingleField(Map region, String key, Map aliases) { Object value = region.get(key); @@ -752,11 +943,16 @@ public class FrontendPageDesignService "admin-form-group-v1"))); result.put("business_block", new LinkedHashSet(Arrays.asList( BusinessBlockRegistryService.BUSINESS_BLOCK_CANVAS))); + result.put(MENU_DIRECTORY_PAGE_TYPE, new LinkedHashSet(Arrays.asList("menu-directory-v1"))); return result; } private String defaultLayoutCanvas(String pageType, String scope) { + if (isMenuDirectory(pageType)) + { + return "menu-directory-v1"; + } if (ADMIN_SCOPE.equals(scope)) { if ("detail".equals(pageType)) @@ -779,6 +975,14 @@ public class FrontendPageDesignService private boolean layoutCanvasAllowed(String pageType, String canvas, String scope) { + if (isMenuDirectory(pageType)) + { + return "menu-directory-v1".equals(canvas); + } + if ("business_block".equals(pageType)) + { + return BusinessBlockRegistryService.BUSINESS_BLOCK_CANVAS.equals(canvas); + } if (ADMIN_SCOPE.equals(scope) && canvas != null && !canvas.startsWith("admin-")) { return false; @@ -966,6 +1170,8 @@ public class FrontendPageDesignService column.setColumnName(source.getColumnName()); column.setJavaField(source.getJavaField()); column.setColumnComment(source.getColumnComment()); + column.setJavaType(source.getJavaType()); + column.setColumnType(source.getColumnType()); columns.add(column); } table.setColumns(columns); @@ -1065,6 +1271,67 @@ public class FrontendPageDesignService return responses; } + private List toScopedResponses(List designs, String scope) + { + List responses = new ArrayList(); + for (FrontProjectPageDesign design : safeDesigns(designs)) + { + if (shouldExposeDesign(design, scope)) + { + responses.add(FrontendPageDesignResponse.from(design)); + } + } + return responses; + } + + private boolean shouldExposeDesign(FrontProjectPageDesign design, String scope) + { + if (design == null || !scopeMatches(design.getPageScope(), scope)) + { + return false; + } + return !FRONTEND_SCOPE.equals(normalizeScope(scope)) || !isAdminLikeDesign(design); + } + + private boolean isAdminLikeDesign(FrontProjectPageDesign design) + { + return isAdminRoutePath(design.getRoutePath()) || layoutCanvasStartsWith(design.getLayoutJson(), "admin-"); + } + + private boolean isAdminLikePage(AppPageDesign page) + { + return page != null && isAdminRoutePath(page.getPath()); + } + + private boolean layoutCanvasStartsWith(String layoutJson, String prefix) + { + if (StringUtils.isEmpty(layoutJson)) + { + return false; + } + try + { + Map layout = OBJECT_MAPPER.readValue(layoutJson, new TypeReference>() { }); + String canvas = stringValue(layout.get("canvas")); + return canvas != null && canvas.startsWith(prefix); + } + catch (Exception e) + { + return false; + } + } + + private boolean isAdminRoutePath(String routePath) + { + if (StringUtils.isEmpty(routePath)) + { + return false; + } + String normalized = routePath.trim().replace('\\', '/').toLowerCase(Locale.ROOT); + return "admin".equals(normalized) || normalized.startsWith("admin/") + || "/admin".equals(normalized) || normalized.startsWith("/admin/"); + } + private String stringValue(Object value) { return value == null ? null : String.valueOf(value); @@ -1075,11 +1342,36 @@ public class FrontendPageDesignService return StringUtils.isEmpty(value) ? fallback : value; } + private boolean isMenuDirectory(String pageType) + { + return MENU_DIRECTORY_PAGE_TYPE.equals(pageType); + } + + private boolean isAssociatedPageType(String pageType) + { + return "detail".equals(pageType); + } + private String normalizeScope(String scope) { return ADMIN_SCOPE.equals(scope) ? ADMIN_SCOPE : FRONTEND_SCOPE; } + private String homePageCode(String scope) + { + return ADMIN_SCOPE.equals(normalizeScope(scope)) ? ADMIN_HOME_PAGE_CODE : FRONTEND_HOME_PAGE_CODE; + } + + private String homeRoutePath(String scope) + { + return ADMIN_SCOPE.equals(normalizeScope(scope)) ? "/admin/home" : "/home"; + } + + private boolean isHomePageCode(String pageCode, String scope) + { + return StringUtils.isNotEmpty(pageCode) && pageCode.equals(homePageCode(scope)); + } + private boolean scopeMatches(String designScope, String expectedScope) { String normalizedDesignScope = StringUtils.isEmpty(designScope) ? FRONTEND_SCOPE : designScope; diff --git a/RuoYi-Vue/ruoyi-generator/src/main/resources/business-blocks/admin_bar_chart/block.json b/RuoYi-Vue/ruoyi-generator/src/main/resources/business-blocks/admin_bar_chart/block.json new file mode 100644 index 0000000..71c89a4 --- /dev/null +++ b/RuoYi-Vue/ruoyi-generator/src/main/resources/business-blocks/admin_bar_chart/block.json @@ -0,0 +1,23 @@ +{ + "code": "admin_bar_chart", + "name": "柱状图", + "category": "数据图表", + "description": "按分类维度对比一个到三个聚合指标,支持横向和堆叠展示。", + "kind": "chart", + "surfaces": ["admin"], + "placements": ["embedded", "page"], + "chartType": "bar", + "datasetSchema": "single-table-aggregate-v1", + "version": 2, + "defaultSpan": 6, + "configs": [], + "templates": [ + { "key": "frontendComponent", "template": "/business-blocks/chart/templates/frontend/AdminChartBlock.vue.vm", "outputPath": "src/views/pages/${pageCode}/blocks/${instanceCode}/${InstanceClassName}.vue" }, + { "key": "frontendApi", "template": "/business-blocks/chart/templates/frontend/chartApi.js.vm", "outputPath": "src/api/business-blocks/${instanceCode}.js" }, + { "key": "controller", "template": "/business-blocks/chart/templates/backend/AdminChartController.java.vm", "outputPath": "src/main/java/${packagePath}/controller/block/${InstanceClassName}Controller.java" }, + { "key": "service", "template": "/business-blocks/chart/templates/backend/AdminChartService.java.vm", "outputPath": "src/main/java/${packagePath}/service/block/I${InstanceClassName}Service.java" }, + { "key": "serviceImpl", "template": "/business-blocks/chart/templates/backend/AdminChartServiceImpl.java.vm", "outputPath": "src/main/java/${packagePath}/service/block/impl/${InstanceClassName}ServiceImpl.java" }, + { "key": "mapper", "template": "/business-blocks/chart/templates/backend/AdminChartMapper.java.vm", "outputPath": "src/main/java/${packagePath}/mapper/block/${InstanceClassName}Mapper.java" }, + { "key": "mapperXml", "template": "/business-blocks/chart/templates/backend/AdminChartMapper.xml.vm", "outputPath": "src/main/resources/mapper/block/${InstanceClassName}Mapper.xml" } + ] +} diff --git a/RuoYi-Vue/ruoyi-generator/src/main/resources/business-blocks/admin_line_chart/block.json b/RuoYi-Vue/ruoyi-generator/src/main/resources/business-blocks/admin_line_chart/block.json new file mode 100644 index 0000000..285d85b --- /dev/null +++ b/RuoYi-Vue/ruoyi-generator/src/main/resources/business-blocks/admin_line_chart/block.json @@ -0,0 +1,23 @@ +{ + "code": "admin_line_chart", + "name": "折线图", + "category": "数据图表", + "description": "按时间或分类维度展示一个到三个指标的变化趋势。", + "kind": "chart", + "surfaces": ["admin"], + "placements": ["embedded", "page"], + "chartType": "line", + "datasetSchema": "single-table-aggregate-v1", + "version": 2, + "defaultSpan": 12, + "configs": [], + "templates": [ + { "key": "frontendComponent", "template": "/business-blocks/chart/templates/frontend/AdminChartBlock.vue.vm", "outputPath": "src/views/pages/${pageCode}/blocks/${instanceCode}/${InstanceClassName}.vue" }, + { "key": "frontendApi", "template": "/business-blocks/chart/templates/frontend/chartApi.js.vm", "outputPath": "src/api/business-blocks/${instanceCode}.js" }, + { "key": "controller", "template": "/business-blocks/chart/templates/backend/AdminChartController.java.vm", "outputPath": "src/main/java/${packagePath}/controller/block/${InstanceClassName}Controller.java" }, + { "key": "service", "template": "/business-blocks/chart/templates/backend/AdminChartService.java.vm", "outputPath": "src/main/java/${packagePath}/service/block/I${InstanceClassName}Service.java" }, + { "key": "serviceImpl", "template": "/business-blocks/chart/templates/backend/AdminChartServiceImpl.java.vm", "outputPath": "src/main/java/${packagePath}/service/block/impl/${InstanceClassName}ServiceImpl.java" }, + { "key": "mapper", "template": "/business-blocks/chart/templates/backend/AdminChartMapper.java.vm", "outputPath": "src/main/java/${packagePath}/mapper/block/${InstanceClassName}Mapper.java" }, + { "key": "mapperXml", "template": "/business-blocks/chart/templates/backend/AdminChartMapper.xml.vm", "outputPath": "src/main/resources/mapper/block/${InstanceClassName}Mapper.xml" } + ] +} diff --git a/RuoYi-Vue/ruoyi-generator/src/main/resources/business-blocks/admin_metric_chart/block.json b/RuoYi-Vue/ruoyi-generator/src/main/resources/business-blocks/admin_metric_chart/block.json new file mode 100644 index 0000000..f16a2e9 --- /dev/null +++ b/RuoYi-Vue/ruoyi-generator/src/main/resources/business-blocks/admin_metric_chart/block.json @@ -0,0 +1,23 @@ +{ + "code": "admin_metric_chart", + "name": "指标卡", + "category": "数据图表", + "description": "展示一个核心聚合数值,适合订单数、销售额、待办数等后台指标。", + "kind": "chart", + "surfaces": ["admin"], + "placements": ["embedded", "page"], + "chartType": "metric", + "datasetSchema": "single-table-aggregate-v1", + "version": 2, + "defaultSpan": 4, + "configs": [], + "templates": [ + { "key": "frontendComponent", "template": "/business-blocks/chart/templates/frontend/AdminChartBlock.vue.vm", "outputPath": "src/views/pages/${pageCode}/blocks/${instanceCode}/${InstanceClassName}.vue" }, + { "key": "frontendApi", "template": "/business-blocks/chart/templates/frontend/chartApi.js.vm", "outputPath": "src/api/business-blocks/${instanceCode}.js" }, + { "key": "controller", "template": "/business-blocks/chart/templates/backend/AdminChartController.java.vm", "outputPath": "src/main/java/${packagePath}/controller/block/${InstanceClassName}Controller.java" }, + { "key": "service", "template": "/business-blocks/chart/templates/backend/AdminChartService.java.vm", "outputPath": "src/main/java/${packagePath}/service/block/I${InstanceClassName}Service.java" }, + { "key": "serviceImpl", "template": "/business-blocks/chart/templates/backend/AdminChartServiceImpl.java.vm", "outputPath": "src/main/java/${packagePath}/service/block/impl/${InstanceClassName}ServiceImpl.java" }, + { "key": "mapper", "template": "/business-blocks/chart/templates/backend/AdminChartMapper.java.vm", "outputPath": "src/main/java/${packagePath}/mapper/block/${InstanceClassName}Mapper.java" }, + { "key": "mapperXml", "template": "/business-blocks/chart/templates/backend/AdminChartMapper.xml.vm", "outputPath": "src/main/resources/mapper/block/${InstanceClassName}Mapper.xml" } + ] +} diff --git a/RuoYi-Vue/ruoyi-generator/src/main/resources/business-blocks/admin_pie_chart/block.json b/RuoYi-Vue/ruoyi-generator/src/main/resources/business-blocks/admin_pie_chart/block.json new file mode 100644 index 0000000..45499df --- /dev/null +++ b/RuoYi-Vue/ruoyi-generator/src/main/resources/business-blocks/admin_pie_chart/block.json @@ -0,0 +1,23 @@ +{ + "code": "admin_pie_chart", + "name": "饼图", + "category": "数据图表", + "description": "展示分类维度下一个聚合指标的占比,可切换实心或环形。", + "kind": "chart", + "surfaces": ["admin"], + "placements": ["embedded", "page"], + "chartType": "pie", + "datasetSchema": "single-table-aggregate-v1", + "version": 2, + "defaultSpan": 6, + "configs": [], + "templates": [ + { "key": "frontendComponent", "template": "/business-blocks/chart/templates/frontend/AdminChartBlock.vue.vm", "outputPath": "src/views/pages/${pageCode}/blocks/${instanceCode}/${InstanceClassName}.vue" }, + { "key": "frontendApi", "template": "/business-blocks/chart/templates/frontend/chartApi.js.vm", "outputPath": "src/api/business-blocks/${instanceCode}.js" }, + { "key": "controller", "template": "/business-blocks/chart/templates/backend/AdminChartController.java.vm", "outputPath": "src/main/java/${packagePath}/controller/block/${InstanceClassName}Controller.java" }, + { "key": "service", "template": "/business-blocks/chart/templates/backend/AdminChartService.java.vm", "outputPath": "src/main/java/${packagePath}/service/block/I${InstanceClassName}Service.java" }, + { "key": "serviceImpl", "template": "/business-blocks/chart/templates/backend/AdminChartServiceImpl.java.vm", "outputPath": "src/main/java/${packagePath}/service/block/impl/${InstanceClassName}ServiceImpl.java" }, + { "key": "mapper", "template": "/business-blocks/chart/templates/backend/AdminChartMapper.java.vm", "outputPath": "src/main/java/${packagePath}/mapper/block/${InstanceClassName}Mapper.java" }, + { "key": "mapperXml", "template": "/business-blocks/chart/templates/backend/AdminChartMapper.xml.vm", "outputPath": "src/main/resources/mapper/block/${InstanceClassName}Mapper.xml" } + ] +} diff --git a/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/BusinessBlockRegistryServiceTest.java b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/BusinessBlockRegistryServiceTest.java index c9514d1..25ac2d0 100644 --- a/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/BusinessBlockRegistryServiceTest.java +++ b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/BusinessBlockRegistryServiceTest.java @@ -12,6 +12,9 @@ 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.ChartMetricConfig; import com.ruoyi.generator.domain.front.dto.block.PageBusinessBlockLayout; public class BusinessBlockRegistryServiceTest @@ -70,6 +73,34 @@ public class BusinessBlockRegistryServiceTest } } + @Test + public void adminSurfaceIncludesFourChartBlocks() + { + java.util.List definitions = service.listDefinitions("admin"); + + assertTrue(hasCode(definitions, "admin_metric_chart")); + assertTrue(hasCode(definitions, "admin_line_chart")); + assertTrue(hasCode(definitions, "admin_bar_chart")); + assertTrue(hasCode(definitions, "admin_pie_chart")); + assertEquals(4, countKind(definitions, "chart")); + } + + @Test + public void frontendSurfaceExcludesAdminCharts() + { + assertEquals(0, countKind(service.listDefinitions("frontend"), "chart")); + } + + @Test + public void chartValidationRejectsAdminChartOnFrontendPage() + { + ServiceException exception = expectServiceException(() -> + service.validateInstanceConfig(service.findDefinition("admin_line_chart"), + validLineChartInstance(), chartTables(), "frontend")); + + assertTrue(exception.getMessage().contains("后台")); + } + @Test public void parseLayoutReadsBusinessBlockInstances() { @@ -210,6 +241,34 @@ public class BusinessBlockRegistryServiceTest return instance; } + 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(12)); + + ChartDimensionConfig dimension = new ChartDimensionConfig(); + dimension.setField("create_time"); + dimension.setTimeUnit("day"); + + ChartMetricConfig metric = new ChartMetricConfig(); + metric.setField("amount"); + metric.setAggregate("sum"); + metric.setAlias("salesAmount"); + metric.setLabel("销售额"); + + ChartDatasetConfig dataset = new ChartDatasetConfig(); + dataset.setSchema("single-table-aggregate-v1"); + dataset.setTable("shop_order"); + dataset.setDimension(dimension); + dataset.setMetrics(Collections.singletonList(metric)); + dataset.setLimit(Integer.valueOf(100)); + instance.setDataset(dataset); + return instance; + } + private java.util.List tables() { return Arrays.asList( @@ -228,6 +287,15 @@ public class BusinessBlockRegistryServiceTest column("price", "price"))); } + private java.util.List chartTables() + { + GenTable order = table("shop_order", + typedColumn("id", "id", "Long", "bigint"), + typedColumn("amount", "amount", "BigDecimal", "decimal(10,2)"), + typedColumn("create_time", "createTime", "Date", "datetime")); + return Collections.singletonList(order); + } + private GenTable table(String tableName, GenTableColumn... columns) { GenTable table = new GenTable(); @@ -244,6 +312,39 @@ public class BusinessBlockRegistryServiceTest return column; } + private GenTableColumn typedColumn(String columnName, String javaField, String javaType, String columnType) + { + GenTableColumn column = column(columnName, javaField); + column.setJavaType(javaType); + column.setColumnType(columnType); + return column; + } + + private boolean hasCode(java.util.List definitions, String code) + { + for (BusinessBlockDefinition definition : definitions) + { + if (code.equals(definition.getCode())) + { + return true; + } + } + return false; + } + + private int countKind(java.util.List definitions, String kind) + { + int count = 0; + for (BusinessBlockDefinition definition : definitions) + { + if (kind.equals(definition.getKind())) + { + count++; + } + } + return count; + } + private ServiceException expectServiceException(ThrowingRunnable runnable) { try diff --git a/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontendPageDesignServiceTest.java b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontendPageDesignServiceTest.java index a25e6e1..37d9691 100644 --- a/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontendPageDesignServiceTest.java +++ b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontendPageDesignServiceTest.java @@ -3,6 +3,7 @@ package com.ruoyi.generator.service.front; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -56,7 +57,8 @@ public class FrontendPageDesignServiceTest public void initializeCreatesMissingDesignFromFrontendPages() { when(frontProjectService.getProject(7L, 10L)).thenReturn(project()); - when(pageDesignMapper.selectDesignsByProjectId(10L)).thenReturn(Collections.emptyList()); + when(pageDesignMapper.selectDesignsByProjectIdAndScope(10L, "frontend")) + .thenReturn(Collections.emptyList()); when(frontProjectTableMapper.selectTablesByProjectId(10L)).thenReturn(Collections.singletonList(bookTable())); when(frontProjectColumnMapper.selectColumnsByTableId(99L)).thenReturn(Arrays.asList(column("title", "title", "1", "1", "1"), column("status", "status", "1", "1", "1"))); @@ -64,11 +66,101 @@ public class FrontendPageDesignServiceTest FrontendPageDesignInitResponse response = service.initializePageDesigns(7L, 10L); List designs = response.getDesigns(); + assertEquals(2, designs.size()); + assertEquals("home", designs.get(0).getPageCode()); + assertEquals("book_catalog_page", designs.get(1).getPageCode()); + assertTrue(designs.get(1).getLayoutJson().contains("\"type\":\"query\"")); + assertTrue(designs.get(1).getLayoutJson().contains("\"title\"")); + verify(pageDesignMapper, times(2)).insertFrontProjectPageDesign(any(FrontProjectPageDesign.class)); + } + + @Test + public void initializeCreatesFrontendHomePageBeforeBlueprintPages() + { + when(frontProjectService.getProject(7L, 10L)).thenReturn(project()); + when(pageDesignMapper.selectDesignsByProjectIdAndScope(10L, "frontend")) + .thenReturn(Collections.emptyList()); + when(frontProjectTableMapper.selectTablesByProjectId(10L)).thenReturn(Collections.singletonList(bookTable())); + when(frontProjectColumnMapper.selectColumnsByTableId(99L)).thenReturn(Arrays.asList( + column("title", "title", "1", "1", "1"), + column("status", "status", "1", "1", "1"))); + + FrontendPageDesignInitResponse response = service.initializePageDesigns(7L, 10L, "frontend"); + + List designs = response.getDesigns(); + assertEquals(2, designs.size()); + assertEquals("home", designs.get(0).getPageCode()); + assertEquals("\u4e3b\u9875", designs.get(0).getPageName()); + assertEquals("/home", designs.get(0).getRoutePath()); + assertEquals("business_block", designs.get(0).getPageType()); + assertEquals("frontend", designs.get(0).getPageScope()); + assertTrue(designs.get(0).getLayoutJson().contains("\"menuOrder\":1")); + assertEquals("book_catalog_page", designs.get(1).getPageCode()); + } + + @Test + public void initializeCopiesParentPageCodeToHiddenDetailNavigation() + { + when(frontProjectService.getProject(7L, 10L)).thenReturn(projectWithAssociatedDetailPage()); + when(pageDesignMapper.selectDesignsByProjectIdAndScope(10L, "frontend")) + .thenReturn(Collections.emptyList()); + when(frontProjectTableMapper.selectTablesByProjectId(10L)).thenReturn(Collections.singletonList(bookTable())); + when(frontProjectColumnMapper.selectColumnsByTableId(99L)).thenReturn(Arrays.asList( + column("id", "id", "1", "1", "1"), + column("book_title", "bookTitle", "1", "1", "1"))); + + service.initializePageDesigns(7L, 10L, "frontend"); + + ArgumentCaptor captor = ArgumentCaptor.forClass(FrontProjectPageDesign.class); + verify(pageDesignMapper, times(3)).insertFrontProjectPageDesign(captor.capture()); + FrontProjectPageDesign detail = captor.getAllValues().get(2); + assertEquals("book_detail_page", detail.getPageCode()); + assertTrue(detail.getLayoutJson().contains("\"parentPageCode\":\"book_catalog_page\"")); + assertTrue(detail.getLayoutJson().contains("\"showInMenu\":false")); + } + + @Test + public void initializeFrontendDesignsSkipsAdminBlueprintPages() + { + when(frontProjectService.getProject(7L, 10L)).thenReturn(projectWithMixedFrontendPages()); + when(pageDesignMapper.selectDesignsByProjectIdAndScope(10L, "frontend")) + .thenReturn(Collections.emptyList()); + when(frontProjectTableMapper.selectTablesByProjectId(10L)).thenReturn(Collections.singletonList(bookTable())); + when(frontProjectColumnMapper.selectColumnsByTableId(99L)).thenReturn(Arrays.asList( + column("title", "title", "1", "1", "1"), + column("status", "status", "1", "1", "1"))); + + FrontendPageDesignInitResponse response = service.initializePageDesigns(7L, 10L, "frontend"); + + List designs = response.getDesigns(); + assertEquals(2, designs.size()); + assertEquals("home", designs.get(0).getPageCode()); + assertEquals("book_catalog_page", designs.get(1).getPageCode()); + assertEquals("/books", designs.get(1).getRoutePath()); + ArgumentCaptor captor = ArgumentCaptor.forClass(FrontProjectPageDesign.class); + verify(pageDesignMapper, times(2)).insertFrontProjectPageDesign(captor.capture()); + assertEquals("home", captor.getAllValues().get(0).getPageCode()); + assertEquals("book_catalog_page", captor.getAllValues().get(1).getPageCode()); + } + + @Test + public void listFrontendDesignsExcludesAdminRouteLeaks() + { + when(frontProjectService.getProject(7L, 10L)).thenReturn(project()); + FrontProjectPageDesign catalog = currentDesign(); + FrontProjectPageDesign leakedAdmin = currentDesign(); + leakedAdmin.setDesignId(89L); + leakedAdmin.setPageCode("book_admin"); + leakedAdmin.setPageName("Book Admin"); + leakedAdmin.setRoutePath("/admin/books"); + leakedAdmin.setLayoutJson("{\"canvas\":\"admin-list-v1\",\"navigation\":{\"showInMenu\":true}}"); + when(pageDesignMapper.selectDesignsByProjectIdAndScope(10L, "frontend")) + .thenReturn(Arrays.asList(catalog, leakedAdmin)); + + List designs = service.listPageDesigns(7L, 10L, "frontend"); + assertEquals(1, designs.size()); assertEquals("book_catalog_page", designs.get(0).getPageCode()); - assertTrue(designs.get(0).getLayoutJson().contains("\"type\":\"query\"")); - assertTrue(designs.get(0).getLayoutJson().contains("\"title\"")); - verify(pageDesignMapper).insertFrontProjectPageDesign(any(FrontProjectPageDesign.class)); } @Test @@ -86,13 +178,18 @@ public class FrontendPageDesignServiceTest FrontendPageDesignInitResponse response = service.initializePageDesigns(7L, 10L, "admin"); List designs = response.getDesigns(); - assertEquals(2, designs.size()); + assertEquals(3, designs.size()); assertEquals("admin", designs.get(0).getPageScope()); - assertEquals("book_admin", designs.get(0).getPageCode()); - assertEquals("book_info", designs.get(0).getTableName()); - assertEquals("admin_customer_info", designs.get(1).getPageCode()); - assertEquals("customer_info", designs.get(1).getTableName()); - assertTrue(designs.get(1).getLayoutJson().contains("\"canvas\":\"admin-list-v1\"")); + assertEquals("admin_home", designs.get(0).getPageCode()); + assertEquals("\u4e3b\u9875", designs.get(0).getPageName()); + assertEquals("/admin/home", designs.get(0).getRoutePath()); + assertEquals("business_block", designs.get(0).getPageType()); + assertTrue(designs.get(0).getLayoutJson().contains("\"menuOrder\":1")); + assertEquals("book_admin", designs.get(1).getPageCode()); + assertEquals("book_info", designs.get(1).getTableName()); + assertEquals("admin_customer_info", designs.get(2).getPageCode()); + assertEquals("customer_info", designs.get(2).getTableName()); + assertTrue(designs.get(2).getLayoutJson().contains("\"canvas\":\"admin-list-v1\"")); } @Test @@ -124,6 +221,30 @@ public class FrontendPageDesignServiceTest assertTrue(captor.getValue().getLayoutJson().contains("bookTitle")); } + @Test + public void createPageDesignPersistsMenuDirectoryWithoutRouteOrTable() + { + when(frontProjectService.getProject(7L, 10L)).thenReturn(project()); + when(pageDesignMapper.selectDesignsByProjectIdAndScope(10L, "frontend")) + .thenReturn(Collections.emptyList()); + FrontendPageDesignRequest request = new FrontendPageDesignRequest(); + request.setPageCode("library_directory"); + request.setMenuCode("library_directory"); + request.setPageName("Library"); + request.setPageType("menu_directory"); + request.setLayoutJson("{\"navigation\":{\"showInMenu\":true,\"menuLevel\":\"primary\",\"nodeType\":\"directory\",\"menuCode\":\"library_directory\",\"menuName\":\"Library\"}}"); + + service.createPageDesign(7L, 10L, request, "frontend"); + + ArgumentCaptor captor = ArgumentCaptor.forClass(FrontProjectPageDesign.class); + verify(pageDesignMapper).insertFrontProjectPageDesign(captor.capture()); + assertEquals("menu_directory", captor.getValue().getPageType()); + assertEquals("", captor.getValue().getRoutePath()); + assertEquals("", captor.getValue().getTableName()); + assertTrue(captor.getValue().getLayoutJson().contains("\"nodeType\":\"directory\"")); + assertTrue(captor.getValue().getLayoutJson().contains("\"menuLevel\":\"primary\"")); + } + @Test public void deletePageDesignRemovesOwnedDesign() { @@ -349,6 +470,73 @@ public class FrontendPageDesignServiceTest assertTrue(captor.getValue().getLayoutJson().contains("\"blockCode\":\"cart\"")); } + @Test + public void updatePersistsAdminHomeBusinessBlockWithoutTable() + { + FrontProjectPageDesign current = currentAdminHomeDesign(); + when(frontProjectService.getProject(7L, 10L)).thenReturn(project()); + when(pageDesignMapper.selectFrontProjectPageDesignById(88L)).thenReturn(current); + when(frontProjectTableMapper.selectTablesByProjectId(10L)).thenReturn(Collections.emptyList()); + FrontendPageDesignRequest request = new FrontendPageDesignRequest(); + request.setVersion(1); + request.setPageName("\u4e3b\u9875"); + request.setRoutePath("/admin/home"); + request.setPageType("business_block"); + request.setLayoutJson("{\"canvas\":\"business-blocks-v1\",\"blocks\":[],\"navigation\":{\"showInMenu\":true,\"menuOrder\":1}}"); + request.setActionJson("{\"toolbarActions\":[],\"rowActions\":[]}"); + + service.updatePageDesign(7L, 10L, 88L, request, "admin"); + + ArgumentCaptor captor = ArgumentCaptor.forClass(FrontProjectPageDesign.class); + verify(pageDesignMapper).updateFrontProjectPageDesign(captor.capture()); + assertEquals("admin", captor.getValue().getPageScope()); + assertEquals("", captor.getValue().getTableName()); + assertTrue(captor.getValue().getLayoutJson().contains("\"canvas\":\"business-blocks-v1\"")); + } + + @Test + public void updatePersistsValidAdminChartLayout() + { + FrontProjectPageDesign current = currentAdminHomeDesign(); + when(frontProjectService.getProject(7L, 10L)).thenReturn(project()); + when(pageDesignMapper.selectFrontProjectPageDesignById(88L)).thenReturn(current); + when(frontProjectTableMapper.selectTablesByProjectId(10L)).thenReturn(Collections.singletonList(orderTable())); + when(frontProjectColumnMapper.selectColumnsByTableId(203L)).thenReturn(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"))); + FrontendPageDesignRequest request = adminChartRequest(validAdminChartLayout()); + + service.updatePageDesign(7L, 10L, 88L, request, "admin"); + + ArgumentCaptor captor = ArgumentCaptor.forClass(FrontProjectPageDesign.class); + verify(pageDesignMapper).updateFrontProjectPageDesign(captor.capture()); + assertTrue(captor.getValue().getLayoutJson().contains("\"blockCode\":\"admin_line_chart\"")); + assertTrue(captor.getValue().getLayoutJson().contains("\"table\":\"shop_order\"")); + } + + @Test + public void updateRejectsAdminChartOnFrontendPage() + { + FrontProjectPageDesign current = currentBusinessBlockDesign(); + current.setTableName("shop_order"); + when(frontProjectService.getProject(7L, 10L)).thenReturn(project()); + when(pageDesignMapper.selectFrontProjectPageDesignById(88L)).thenReturn(current); + when(frontProjectTableMapper.selectTablesByProjectId(10L)).thenReturn(Collections.singletonList(orderTable())); + when(frontProjectColumnMapper.selectColumnsByTableId(203L)).thenReturn(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"))); + FrontendPageDesignRequest request = adminChartRequest(validAdminChartLayout()); + + ServiceException exception = expectServiceException(() -> + service.updatePageDesign(7L, 10L, 88L, request, "frontend")); + + assertTrue(exception.getMessage().contains("后台")); + } + @Test public void updateRejectsBusinessBlockMissingRequiredConfig() { @@ -402,6 +590,30 @@ public class FrontendPageDesignServiceTest return project; } + private FrontProject projectWithMixedFrontendPages() + { + FrontProject project = new FrontProject(); + project.setProjectId(10L); + project.setUserId(7L); + project.setAppBlueprint("{\"frontendPages\":[" + + "{\"code\":\"book_catalog_page\",\"name\":\"Book Catalog\",\"menuCode\":\"book_catalog\",\"path\":\"/books\",\"pageType\":\"list\",\"tableName\":\"book_info\"}," + + "{\"code\":\"book_admin\",\"name\":\"Book Admin\",\"menuCode\":\"book_admin\",\"path\":\"/admin/books\",\"pageType\":\"list\",\"tableName\":\"book_info\"}" + + "]}"); + return project; + } + + private FrontProject projectWithAssociatedDetailPage() + { + FrontProject project = new FrontProject(); + project.setProjectId(10L); + project.setUserId(7L); + project.setAppBlueprint("{\"frontendPages\":[" + + "{\"code\":\"book_catalog_page\",\"name\":\"Book Catalog\",\"menuCode\":\"book_catalog\",\"path\":\"/books\",\"pageType\":\"list\",\"tableName\":\"book_info\"}," + + "{\"code\":\"book_detail_page\",\"name\":\"Book Detail\",\"parentPageCode\":\"book_catalog_page\",\"path\":\"/books/:id\",\"pageType\":\"detail\",\"tableName\":\"book_info\"}" + + "]}"); + return project; + } + private FrontProjectTable bookTable() { FrontProjectTable table = new FrontProjectTable(); @@ -452,6 +664,18 @@ public class FrontendPageDesignServiceTest return table; } + private FrontProjectTable orderTable() + { + FrontProjectTable table = new FrontProjectTable(); + table.setTableId(203L); + table.setProjectId(10L); + table.setTableName("shop_order"); + table.setAllowAdd("1"); + table.setAllowEdit("1"); + table.setAllowRemove("1"); + return table; + } + private FrontProjectColumn column(String columnName, String javaField, String isQuery, String isList, String isEdit) { FrontProjectColumn column = new FrontProjectColumn(); @@ -464,6 +688,14 @@ public class FrontendPageDesignServiceTest return column; } + private FrontProjectColumn typedColumn(String columnName, String javaField, String javaType, String columnType) + { + FrontProjectColumn column = column(columnName, javaField, "0", "1", "0"); + column.setJavaType(javaType); + column.setColumnType(columnType); + return column; + } + private FrontProjectPageDesign currentDesign() { FrontProjectPageDesign design = new FrontProjectPageDesign(); @@ -489,6 +721,21 @@ public class FrontendPageDesignServiceTest return design; } + private FrontProjectPageDesign currentAdminHomeDesign() + { + FrontProjectPageDesign design = currentDesign(); + design.setPageScope("admin"); + design.setPageCode("admin_home"); + design.setMenuCode("admin_home"); + design.setPageName("\u4e3b\u9875"); + design.setRoutePath("/admin/home"); + design.setPageType("business_block"); + design.setTableName(""); + design.setLayoutJson("{\"canvas\":\"business-blocks-v1\",\"blocks\":[],\"navigation\":{\"showInMenu\":true,\"menuOrder\":1}}"); + design.setActionJson("{\"toolbarActions\":[],\"rowActions\":[]}"); + return design; + } + private FrontendPageDesignRequest businessBlockRequest(String layoutJson) { FrontendPageDesignRequest request = new FrontendPageDesignRequest(); @@ -502,6 +749,19 @@ public class FrontendPageDesignServiceTest return request; } + private FrontendPageDesignRequest adminChartRequest(String layoutJson) + { + FrontendPageDesignRequest request = new FrontendPageDesignRequest(); + request.setVersion(1); + request.setPageName("\u4e3b\u9875"); + request.setRoutePath("/admin/home"); + request.setPageType("business_block"); + request.setTableName(""); + request.setLayoutJson(layoutJson); + request.setActionJson("{\"toolbarActions\":[],\"rowActions\":[]}"); + return request; + } + private String validBusinessBlockLayout() { return "{\"canvas\":\"business-blocks-v1\",\"blocks\":[{\"id\":\"cart_001\",\"blockCode\":\"cart\",\"config\":{" @@ -517,6 +777,24 @@ public class FrontendPageDesignServiceTest + "}}]}"; } + private String validAdminChartLayout() + { + return "{\"canvas\":\"business-blocks-v1\",\"blocks\":[{" + + "\"id\":\"admin_line_chart_001\"," + + "\"blockCode\":\"admin_line_chart\"," + + "\"name\":\"近 30 天销售趋势\"," + + "\"span\":12," + + "\"dataset\":{" + + "\"schema\":\"single-table-aggregate-v1\"," + + "\"table\":\"shop_order\"," + + "\"dimension\":{\"field\":\"create_time\",\"timeUnit\":\"day\"}," + + "\"metrics\":[{\"field\":\"amount\",\"aggregate\":\"sum\",\"alias\":\"salesAmount\",\"label\":\"销售额\"}]," + + "\"filters\":[{\"field\":\"status\",\"operator\":\"eq\",\"value\":\"PAID\"}]," + + "\"sort\":{\"by\":\"dimension\",\"order\":\"asc\"}," + + "\"limit\":100}," + + "\"display\":{\"showLegend\":true}}]}"; + } + private ServiceException expectBusinessBlockUpdateFailure(String layoutJson) { FrontProjectPageDesign current = currentBusinessBlockDesign();