From 3f2cdb8444e00021788d3e60111c4ebf3759691f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E9=B9=8F?= Date: Fri, 22 May 2026 11:20:47 +0800 Subject: [PATCH] Add front project draft APIs --- .../front/FrontProjectController.java | 75 +++++++ .../front/FrontProjectServiceImpl.java | 122 +++++++++-- .../front/FrontProjectServiceImplTest.java | 195 ++++++++++++++++++ 3 files changed, 374 insertions(+), 18 deletions(-) create mode 100644 RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/web/controller/front/FrontProjectController.java create mode 100644 RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontProjectServiceImplTest.java diff --git a/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/web/controller/front/FrontProjectController.java b/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/web/controller/front/FrontProjectController.java new file mode 100644 index 0000000..fcf6c3c --- /dev/null +++ b/RuoYi-Vue/ruoyi-admin/src/main/java/com/ruoyi/web/controller/front/FrontProjectController.java @@ -0,0 +1,75 @@ +package com.ruoyi.web.controller.front; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.utils.SecurityUtils; +import com.ruoyi.generator.domain.front.dto.DatabaseDesignResponse; +import com.ruoyi.generator.domain.front.dto.FrontProjectCreateRequest; +import com.ruoyi.generator.domain.front.dto.FrontProjectUpdateRequest; +import com.ruoyi.generator.service.front.IFrontProjectService; + +@RestController +@RequestMapping("/front/project") +public class FrontProjectController extends BaseController +{ + @Autowired + private IFrontProjectService frontProjectService; + + @PostMapping("/create") + public AjaxResult create(@RequestBody FrontProjectCreateRequest request) + { + Long userId = SecurityUtils.getUserId(); + return AjaxResult.success(frontProjectService.createProject(userId, request)); + } + + @GetMapping("/list") + public AjaxResult list() + { + Long userId = SecurityUtils.getUserId(); + return AjaxResult.success(frontProjectService.listProjects(userId)); + } + + @GetMapping("/{projectId}") + public AjaxResult get(@PathVariable Long projectId) + { + Long userId = SecurityUtils.getUserId(); + return AjaxResult.success(frontProjectService.getProject(userId, projectId)); + } + + @PutMapping("/{projectId}") + public AjaxResult update(@PathVariable Long projectId, @RequestBody FrontProjectUpdateRequest request) + { + Long userId = SecurityUtils.getUserId(); + return toAjax(frontProjectService.updateProject(userId, projectId, request)); + } + + @DeleteMapping("/{projectId}") + public AjaxResult delete(@PathVariable Long projectId) + { + Long userId = SecurityUtils.getUserId(); + return toAjax(frontProjectService.deleteProject(userId, projectId)); + } + + @GetMapping("/{projectId}/database") + public AjaxResult database(@PathVariable Long projectId) + { + Long userId = SecurityUtils.getUserId(); + return AjaxResult.success(frontProjectService.getDatabase(userId, projectId)); + } + + @PutMapping("/{projectId}/database") + public AjaxResult saveDatabase(@PathVariable Long projectId, @RequestBody DatabaseDesignResponse request) + { + Long userId = SecurityUtils.getUserId(); + return AjaxResult.success(frontProjectService.saveDatabase(userId, projectId, request)); + } +} diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectServiceImpl.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectServiceImpl.java index 3636752..4c27f24 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectServiceImpl.java +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectServiceImpl.java @@ -1,7 +1,12 @@ package com.ruoyi.generator.service.front; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.regex.Pattern; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -24,6 +29,15 @@ import com.ruoyi.generator.mapper.front.FrontProjectTableMapper; @Service public class FrontProjectServiceImpl implements IFrontProjectService { + private static final Pattern SAFE_PROJECT_FILE_NAME = Pattern.compile("^[a-z][a-z0-9-]*[a-z0-9]$|^[a-z]$"); + private static final Pattern DB_NAME_PATTERN = Pattern.compile("^[a-z][a-z0-9_]{1,63}$"); + private static final Set ALLOWED_COLUMN_TYPES = new HashSet(Arrays.asList( + "tinyint", "smallint", "mediumint", "int", "integer", "bigint", + "float", "double", "decimal", "numeric", + "bit", "bool", "boolean", + "char", "varchar", "text", "tinytext", "mediumtext", "longtext", + "date", "time", "datetime", "timestamp", "json")); + @Autowired private FrontProjectMapper frontProjectMapper; @Autowired @@ -76,7 +90,7 @@ public class FrontProjectServiceImpl implements IFrontProjectService FrontProject project = frontProjectMapper.selectFrontProjectByUserAndId(userId, projectId); if (project == null) { - throw new ServiceException("项目不存在"); + throw new ServiceException("项目不存在或无权限"); } return project; } @@ -134,6 +148,7 @@ public class FrontProjectServiceImpl implements IFrontProjectService tables.add(toTableDesign(table)); } response.setTables(tables); + response.setSql(joinCreateTableSql(tables)); return response; } @@ -143,26 +158,17 @@ public class FrontProjectServiceImpl implements IFrontProjectService { assertFrontUserLoggedIn(userId); getProject(userId, projectId); + validateDatabaseDesign(request); frontProjectColumnMapper.deleteColumnsByProjectId(projectId); frontProjectTableMapper.deleteTablesByProjectId(projectId); - if (request != null && request.getTables() != null) + for (DatabaseTableDesign tableDesign : request.getTables()) { - for (DatabaseTableDesign tableDesign : request.getTables()) + FrontProjectTable table = toFrontProjectTable(projectId, userId, tableDesign); + frontProjectTableMapper.insertFrontProjectTable(table); + for (DatabaseColumnDesign columnDesign : tableDesign.getColumns()) { - if (tableDesign == null) - { - continue; - } - FrontProjectTable table = toFrontProjectTable(projectId, userId, tableDesign); - frontProjectTableMapper.insertFrontProjectTable(table); - if (tableDesign.getColumns() != null) - { - for (DatabaseColumnDesign columnDesign : tableDesign.getColumns()) - { - FrontProjectColumn column = toFrontProjectColumn(projectId, table.getTableId(), columnDesign); - frontProjectColumnMapper.insertFrontProjectColumn(column); - } - } + FrontProjectColumn column = toFrontProjectColumn(projectId, table.getTableId(), columnDesign); + frontProjectColumnMapper.insertFrontProjectColumn(column); } } return getDatabase(userId, projectId); @@ -178,7 +184,87 @@ public class FrontProjectServiceImpl implements IFrontProjectService private String toProjectFileName(String projectName) { - return StringUtils.defaultString(projectName).trim().replaceAll("\\s+", "-").toLowerCase(); + String fileName = StringUtils.defaultString(projectName).trim() + .replaceAll("[^A-Za-z0-9]+", "-") + .replaceAll("^-+|-+$", "") + .replaceAll("-{2,}", "-") + .toLowerCase(Locale.ENGLISH); + if (!SAFE_PROJECT_FILE_NAME.matcher(fileName).matches()) + { + return "easycode-project-" + System.currentTimeMillis(); + } + return fileName; + } + + private void validateDatabaseDesign(DatabaseDesignResponse request) + { + if (request == null || request.getTables() == null || request.getTables().isEmpty()) + { + throw new ServiceException("至少需要一张数据表"); + } + for (DatabaseTableDesign table : request.getTables()) + { + if (table == null || !isValidDatabaseName(table.getTableName())) + { + throw new ServiceException("表名只能包含小写字母、数字和下划线"); + } + if (table.getColumns() == null || table.getColumns().isEmpty()) + { + throw new ServiceException("每张表至少需要一个字段"); + } + int primaryKeyCount = 0; + for (DatabaseColumnDesign column : table.getColumns()) + { + if (column == null || !isValidDatabaseName(column.getColumnName())) + { + throw new ServiceException("字段名只能包含小写字母、数字和下划线"); + } + if (!isAllowedColumnType(column.getColumnType())) + { + throw new ServiceException("字段类型不在允许范围内"); + } + if ("1".equals(column.getIsPk())) + { + primaryKeyCount++; + } + } + if (primaryKeyCount != 1) + { + throw new ServiceException("每张表必须且只能有一个主键"); + } + } + } + + private boolean isValidDatabaseName(String name) + { + return StringUtils.isNotEmpty(name) && DB_NAME_PATTERN.matcher(name).matches(); + } + + private boolean isAllowedColumnType(String columnType) + { + if (StringUtils.isEmpty(columnType)) + { + return false; + } + String normalizedType = columnType.trim().toLowerCase(Locale.ENGLISH) + .replaceAll("\\s+unsigned$", "") + .replaceAll("\\s+zerofill$", ""); + int bracketIndex = normalizedType.indexOf('('); + String baseType = bracketIndex >= 0 ? normalizedType.substring(0, bracketIndex) : normalizedType; + return ALLOWED_COLUMN_TYPES.contains(baseType); + } + + private String joinCreateTableSql(List tables) + { + List sqlList = new ArrayList(); + for (DatabaseTableDesign table : tables) + { + if (StringUtils.isNotEmpty(table.getCreateTableSql())) + { + sqlList.add(table.getCreateTableSql()); + } + } + return StringUtils.join(sqlList, "\n\n"); } private FrontProjectTable toFrontProjectTable(Long projectId, Long userId, DatabaseTableDesign source) diff --git a/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontProjectServiceImplTest.java b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontProjectServiceImplTest.java new file mode 100644 index 0000000..0d38e0f --- /dev/null +++ b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontProjectServiceImplTest.java @@ -0,0 +1,195 @@ +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.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Collections; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.generator.domain.front.FrontProject; +import com.ruoyi.generator.domain.front.dto.DatabaseColumnDesign; +import com.ruoyi.generator.domain.front.dto.DatabaseDesignResponse; +import com.ruoyi.generator.domain.front.dto.DatabaseTableDesign; +import com.ruoyi.generator.domain.front.dto.FrontProjectCreateRequest; +import com.ruoyi.generator.mapper.front.FrontProjectColumnMapper; +import com.ruoyi.generator.mapper.front.FrontProjectFileMapper; +import com.ruoyi.generator.mapper.front.FrontProjectGenerationMapper; +import com.ruoyi.generator.mapper.front.FrontProjectMapper; +import com.ruoyi.generator.mapper.front.FrontProjectTableMapper; + +public class FrontProjectServiceImplTest +{ + private FrontProjectServiceImpl service; + + @Mock + private FrontProjectMapper frontProjectMapper; + @Mock + private FrontProjectTableMapper frontProjectTableMapper; + @Mock + private FrontProjectColumnMapper frontProjectColumnMapper; + @Mock + private FrontProjectFileMapper frontProjectFileMapper; + @Mock + private FrontProjectGenerationMapper frontProjectGenerationMapper; + + @Before + public void setUp() throws Exception + { + MockitoAnnotations.initMocks(this); + service = new FrontProjectServiceImpl(); + setField("frontProjectMapper", frontProjectMapper); + setField("frontProjectTableMapper", frontProjectTableMapper); + setField("frontProjectColumnMapper", frontProjectColumnMapper); + setField("frontProjectFileMapper", frontProjectFileMapper); + setField("frontProjectGenerationMapper", frontProjectGenerationMapper); + } + + @Test + public void createProjectUsesSafeDefaults() + { + FrontProjectCreateRequest request = new FrontProjectCreateRequest(); + request.setProjectName("车辆管理系统"); + + FrontProject project = service.createProject(7L, request); + + assertEquals(Long.valueOf(7L), project.getUserId()); + assertEquals("com.easycode.generated", project.getPackageName()); + assertEquals("1.0.0", project.getVersion()); + assertEquals("Vue3 + Element Plus", project.getFrontFramework()); + assertEquals("Spring Boot + MyBatis", project.getBackFramework()); + assertEquals("0", project.getGenerateStatus()); + assertEquals("0", project.getPreviewStatus()); + assertEquals("0", project.getStatus()); + assertTrue(project.getProjectFileName().startsWith("easycode-project-")); + verify(frontProjectMapper).insertFrontProject(project); + } + + @Test + public void saveDatabaseRejectsInvalidTableName() + { + when(frontProjectMapper.selectFrontProjectByUserAndId(7L, 10L)).thenReturn(project()); + + ServiceException exception = expectServiceException(new ThrowingRunnable() + { + @Override + public void run() + { + service.saveDatabase(7L, 10L, database(table("Sys_User", pkColumn()))); + } + }); + + assertEquals("表名只能包含小写字母、数字和下划线", exception.getMessage()); + verify(frontProjectColumnMapper, never()).deleteColumnsByProjectId(any(Long.class)); + verify(frontProjectTableMapper, never()).deleteTablesByProjectId(any(Long.class)); + } + + @Test + public void saveDatabaseRejectsMissingPrimaryKey() + { + when(frontProjectMapper.selectFrontProjectByUserAndId(7L, 10L)).thenReturn(project()); + + ServiceException exception = expectServiceException(new ThrowingRunnable() + { + @Override + public void run() + { + service.saveDatabase(7L, 10L, database(table("sys_user", column("user_name", "varchar(64)", "0")))); + } + }); + + assertEquals("每张表必须且只能有一个主键", exception.getMessage()); + verify(frontProjectColumnMapper, never()).deleteColumnsByProjectId(any(Long.class)); + verify(frontProjectTableMapper, never()).deleteTablesByProjectId(any(Long.class)); + } + + @Test + public void saveDatabasePersistsValidDraft() + { + when(frontProjectMapper.selectFrontProjectByUserAndId(7L, 10L)).thenReturn(project()); + when(frontProjectTableMapper.selectTablesByProjectId(10L)).thenReturn(Collections.emptyList()); + DatabaseDesignResponse request = database(table("sys_user", pkColumn(), column("user_name", "varchar(64)", "0"))); + + service.saveDatabase(7L, 10L, request); + + ArgumentCaptor tableCaptor = + ArgumentCaptor.forClass(com.ruoyi.generator.domain.front.FrontProjectTable.class); + verify(frontProjectTableMapper).insertFrontProjectTable(tableCaptor.capture()); + assertEquals(Long.valueOf(7L), tableCaptor.getValue().getUserId()); + assertEquals(Long.valueOf(10L), tableCaptor.getValue().getProjectId()); + verify(frontProjectColumnMapper).deleteColumnsByProjectId(10L); + verify(frontProjectTableMapper).deleteTablesByProjectId(10L); + } + + private void setField(String name, Object value) throws Exception + { + Field field = FrontProjectServiceImpl.class.getDeclaredField(name); + field.setAccessible(true); + field.set(service, value); + } + + private FrontProject project() + { + FrontProject project = new FrontProject(); + project.setProjectId(10L); + project.setUserId(7L); + project.setProjectName("Demo"); + return project; + } + + private DatabaseDesignResponse database(DatabaseTableDesign table) + { + DatabaseDesignResponse response = new DatabaseDesignResponse(); + response.setTables(Collections.singletonList(table)); + return response; + } + + private DatabaseTableDesign table(String tableName, DatabaseColumnDesign... columns) + { + DatabaseTableDesign table = new DatabaseTableDesign(); + table.setTableName(tableName); + table.setColumns(Arrays.asList(columns)); + return table; + } + + private DatabaseColumnDesign pkColumn() + { + return column("id", "bigint", "1"); + } + + private DatabaseColumnDesign column(String columnName, String columnType, String isPk) + { + DatabaseColumnDesign column = new DatabaseColumnDesign(); + column.setColumnName(columnName); + column.setColumnType(columnType); + column.setIsPk(isPk); + return column; + } + + private ServiceException expectServiceException(ThrowingRunnable runnable) + { + try + { + runnable.run(); + } + catch (ServiceException e) + { + return e; + } + throw new AssertionError("Expected ServiceException"); + } + + private interface ThrowingRunnable + { + void run(); + } +}