Register admin chart business blocks

This commit is contained in:
王鹏
2026-06-19 23:36:36 +08:00
parent 29354508f9
commit 93007343a6
9 changed files with 834 additions and 24 deletions

View File

@@ -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<BusinessBlockDefinition> cachedDefinitions;
@Autowired
private ChartDatasetValidator chartDatasetValidator;
public List<BusinessBlockDefinition> listDefinitions()
{
if (cachedDefinitions == null)
@@ -92,6 +96,11 @@ public class BusinessBlockRegistryService
}
public void validateLayout(String layoutJson, List<GenTable> tables)
{
validateLayout(layoutJson, tables, "frontend");
}
public void validateLayout(String layoutJson, List<GenTable> 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<GenTable> tables)
{
validateEmbeddedBlocks(layoutJson, tables, "frontend");
}
public void validateEmbeddedBlocks(String layoutJson, List<GenTable> tables, String surface)
{
Map<String, List<BusinessBlockInstance>> zones = parseEmbeddedBlockZones(layoutJson);
for (List<BusinessBlockInstance> 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<GenTable> 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<BusinessBlockDefinition> loadDefinitions()
{
try

View File

@@ -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";

View File

@@ -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<String> PAGE_TYPES = new HashSet<String>(
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<String> REGION_TYPES = new HashSet<String>(
Arrays.asList("query", "toolbar", "table", "dialogForm", "detail", "form", "cardList", "actions"));
private static final Set<String> BUILTIN_ACTIONS = new HashSet<String>(
@@ -65,7 +71,8 @@ public class FrontendPageDesignService
public List<FrontendPageDesignResponse> 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<String, FrontProjectPageDesign> existingByPageCode = new LinkedHashMap<String, FrontProjectPageDesign>();
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<String, TableBundle> tablesByName = loadTables(projectId);
List<FrontProjectPageDesign> merged = new ArrayList<FrontProjectPageDesign>();
Set<String> blueprintPageCodes = new LinkedHashSet<String>();
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<FrontendPageDesignResponse> detached = new ArrayList<FrontendPageDesignResponse>();
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<String, TableBundle> unmatchedTables = new LinkedHashMap<String, TableBundle>(tablesByName);
List<FrontProjectPageDesign> merged = new ArrayList<FrontProjectPageDesign>();
Set<String> expectedPageCodes = new LinkedHashSet<String>();
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<String, FrontProjectPageDesign> 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<String, Object> layout = new LinkedHashMap<String, Object>();
layout.put("canvas", BusinessBlockRegistryService.BUSINESS_BLOCK_CANVAS);
layout.put("blocks", new ArrayList<Object>());
layout.put("navigation", defaultHomeNavigation(pageCode));
return writeJson(layout);
}
private Map<String, Object> defaultHomeNavigation(String pageCode)
{
Map<String, Object> navigation = new LinkedHashMap<String, Object>();
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<String, Object> action = new LinkedHashMap<String, Object>();
action.put("toolbarActions", new ArrayList<Object>());
action.put("rowActions", new ArrayList<Object>());
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<String, TableBundle> loadTables(Long projectId)
{
Map<String, TableBundle> result = new LinkedHashMap<String, TableBundle>();
@@ -507,6 +612,13 @@ public class FrontendPageDesignService
private String defaultLayoutJson(String pageType, TableBundle table, String scope)
{
if (isMenuDirectory(pageType))
{
Map<String, Object> layout = new LinkedHashMap<String, Object>();
layout.put("canvas", "menu-directory-v1");
layout.put("navigation", defaultMenuDirectoryNavigation(null, null));
return writeJson(layout);
}
if ("business_block".equals(pageType))
{
Map<String, Object> businessLayout = new LinkedHashMap<String, Object>();
@@ -525,6 +637,39 @@ public class FrontendPageDesignService
return writeJson(layout);
}
private String defaultLayoutJson(String pageType, TableBundle table, String scope, AppPageDesign page)
{
Map<String, Object> layout = readMap(defaultLayoutJson(pageType, table, scope), "椤甸潰甯冨眬JSON鏍煎紡閿欒");
applyAssociatedPageNavigationDefaults(layout, pageType, page == null ? null : page.getParentPageCode());
return writeJson(layout);
}
@SuppressWarnings("unchecked")
private void applyAssociatedPageNavigationDefaults(Map<String, Object> layout, String pageType, String parentPageCode)
{
if (layout == null || isMenuDirectory(pageType))
{
return;
}
if (!isAssociatedPageType(pageType) && StringUtils.isEmpty(parentPageCode))
{
return;
}
Object navigationObject = layout.get("navigation");
Map<String, Object> navigation = navigationObject instanceof Map
? new LinkedHashMap<String, Object>((Map<String, Object>) navigationObject)
: new LinkedHashMap<String, Object>();
if (isAssociatedPageType(pageType))
{
navigation.put("showInMenu", false);
}
if (StringUtils.isNotEmpty(parentPageCode))
{
navigation.put("parentPageCode", parentPageCode);
}
layout.put("navigation", navigation);
}
private Map<String, Object> region(String id, String type, List<String> fields,
List<String> actions, List<String> rowActions)
{
@@ -662,6 +807,10 @@ public class FrontendPageDesignService
private String normalizeLayout(String layoutJson, TableBundle table, String pageType, String scope,
Map<String, TableBundle> 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<String, Object> layout = StringUtils.isEmpty(layoutJson)
? new LinkedHashMap<String, Object>()
: readMap(layoutJson, "页面布局JSON格式错误");
Object navigationObject = layout.get("navigation");
Map<String, Object> navigation = navigationObject instanceof Map
? new LinkedHashMap<String, Object>((Map<String, Object>) navigationObject)
: new LinkedHashMap<String, Object>();
Map<String, Object> 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<String, Object> defaultMenuDirectoryNavigation(String menuCode, String menuName)
{
Map<String, Object> navigation = new LinkedHashMap<String, Object>();
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<String, Object> region, String key, Map<String, String> aliases)
{
Object value = region.get(key);
@@ -752,11 +943,16 @@ public class FrontendPageDesignService
"admin-form-group-v1")));
result.put("business_block", new LinkedHashSet<String>(Arrays.asList(
BusinessBlockRegistryService.BUSINESS_BLOCK_CANVAS)));
result.put(MENU_DIRECTORY_PAGE_TYPE, new LinkedHashSet<String>(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<FrontendPageDesignResponse> toScopedResponses(List<FrontProjectPageDesign> designs, String scope)
{
List<FrontendPageDesignResponse> responses = new ArrayList<FrontendPageDesignResponse>();
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<String, Object> layout = OBJECT_MAPPER.readValue(layoutJson, new TypeReference<Map<String, Object>>() { });
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;

View File

@@ -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" }
]
}

View File

@@ -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" }
]
}

View File

@@ -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" }
]
}

View File

@@ -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" }
]
}

View File

@@ -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<BusinessBlockDefinition> 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<GenTable> tables()
{
return Arrays.asList(
@@ -228,6 +287,15 @@ public class BusinessBlockRegistryServiceTest
column("price", "price")));
}
private java.util.List<GenTable> 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<BusinessBlockDefinition> definitions, String code)
{
for (BusinessBlockDefinition definition : definitions)
{
if (code.equals(definition.getCode()))
{
return true;
}
}
return false;
}
private int countKind(java.util.List<BusinessBlockDefinition> definitions, String kind)
{
int count = 0;
for (BusinessBlockDefinition definition : definitions)
{
if (kind.equals(definition.getKind()))
{
count++;
}
}
return count;
}
private ServiceException expectServiceException(ThrowingRunnable runnable)
{
try

View File

@@ -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.<FrontProjectPageDesign>emptyList());
when(pageDesignMapper.selectDesignsByProjectIdAndScope(10L, "frontend"))
.thenReturn(Collections.<FrontProjectPageDesign>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<FrontendPageDesignResponse> 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.<FrontProjectPageDesign>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<FrontendPageDesignResponse> 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.<FrontProjectPageDesign>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<FrontProjectPageDesign> 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.<FrontProjectPageDesign>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<FrontendPageDesignResponse> 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<FrontProjectPageDesign> 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<FrontendPageDesignResponse> 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<FrontendPageDesignResponse> 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.<FrontProjectPageDesign>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<FrontProjectPageDesign> 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.<FrontProjectTable>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<FrontProjectPageDesign> 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<FrontProjectPageDesign> 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();