Add front project draft APIs

This commit is contained in:
王鹏
2026-05-22 11:20:47 +08:00
parent 532f05da99
commit 3f2cdb8444
3 changed files with 374 additions and 18 deletions

View File

@@ -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));
}
}

View File

@@ -1,7 +1,12 @@
package com.ruoyi.generator.service.front; package com.ruoyi.generator.service.front;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List; 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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -24,6 +29,15 @@ import com.ruoyi.generator.mapper.front.FrontProjectTableMapper;
@Service @Service
public class FrontProjectServiceImpl implements IFrontProjectService 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<String> ALLOWED_COLUMN_TYPES = new HashSet<String>(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 @Autowired
private FrontProjectMapper frontProjectMapper; private FrontProjectMapper frontProjectMapper;
@Autowired @Autowired
@@ -76,7 +90,7 @@ public class FrontProjectServiceImpl implements IFrontProjectService
FrontProject project = frontProjectMapper.selectFrontProjectByUserAndId(userId, projectId); FrontProject project = frontProjectMapper.selectFrontProjectByUserAndId(userId, projectId);
if (project == null) if (project == null)
{ {
throw new ServiceException("项目不存在"); throw new ServiceException("项目不存在或无权限");
} }
return project; return project;
} }
@@ -134,6 +148,7 @@ public class FrontProjectServiceImpl implements IFrontProjectService
tables.add(toTableDesign(table)); tables.add(toTableDesign(table));
} }
response.setTables(tables); response.setTables(tables);
response.setSql(joinCreateTableSql(tables));
return response; return response;
} }
@@ -143,26 +158,17 @@ public class FrontProjectServiceImpl implements IFrontProjectService
{ {
assertFrontUserLoggedIn(userId); assertFrontUserLoggedIn(userId);
getProject(userId, projectId); getProject(userId, projectId);
validateDatabaseDesign(request);
frontProjectColumnMapper.deleteColumnsByProjectId(projectId); frontProjectColumnMapper.deleteColumnsByProjectId(projectId);
frontProjectTableMapper.deleteTablesByProjectId(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) FrontProjectColumn column = toFrontProjectColumn(projectId, table.getTableId(), columnDesign);
{ frontProjectColumnMapper.insertFrontProjectColumn(column);
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);
}
}
} }
} }
return getDatabase(userId, projectId); return getDatabase(userId, projectId);
@@ -178,7 +184,87 @@ public class FrontProjectServiceImpl implements IFrontProjectService
private String toProjectFileName(String projectName) 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<DatabaseTableDesign> tables)
{
List<String> sqlList = new ArrayList<String>();
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) private FrontProjectTable toFrontProjectTable(Long projectId, Long userId, DatabaseTableDesign source)

View File

@@ -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<com.ruoyi.generator.domain.front.FrontProjectTable> 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();
}
}