Add DeepSeek database generation service

This commit is contained in:
王鹏
2026-05-22 12:01:39 +08:00
parent b8573a5425
commit a3608381ed
12 changed files with 842 additions and 0 deletions

View File

@@ -15,6 +15,8 @@ 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.domain.front.dto.GenerateDatabaseRequest;
import com.ruoyi.generator.service.front.IAiGenerateService;
import com.ruoyi.generator.service.front.IFrontProjectService;
@RestController
@@ -23,6 +25,8 @@ public class FrontProjectController extends BaseController
{
@Autowired
private IFrontProjectService frontProjectService;
@Autowired
private IAiGenerateService aiGenerateService;
@PostMapping("/create")
public AjaxResult create(@RequestBody FrontProjectCreateRequest request)
@@ -72,4 +76,11 @@ public class FrontProjectController extends BaseController
Long userId = SecurityUtils.getUserId();
return AjaxResult.success(frontProjectService.saveDatabase(userId, projectId, request));
}
@PostMapping("/{projectId}/generate-database")
public AjaxResult generateDatabase(@PathVariable Long projectId, @RequestBody GenerateDatabaseRequest request)
{
Long userId = SecurityUtils.getUserId();
return success(aiGenerateService.generateDatabase(userId, projectId, request));
}
}

View File

@@ -14,6 +14,14 @@ ruoyi:
# 验证码类型 math 数字计算 char 字符验证
captchaType: math
deepseek:
apiKey: ${DEEPSEEK_API_KEY:}
baseUrl: https://api.deepseek.com
model: deepseek-chat
timeout: 60000
maxTables: 12
maxColumnsPerTable: 30
# 开发环境配置
server:
# 服务器的HTTP端口默认为8080

View File

@@ -0,0 +1,29 @@
package com.ruoyi.generator.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "deepseek")
public class DeepSeekProperties
{
private String apiKey;
private String baseUrl = "https://api.deepseek.com";
private String model = "deepseek-chat";
private Integer timeout = 60000;
private Integer maxTables = 12;
private Integer maxColumnsPerTable = 30;
public String getApiKey() { return apiKey; }
public void setApiKey(String apiKey) { this.apiKey = apiKey; }
public String getBaseUrl() { return baseUrl; }
public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; }
public String getModel() { return model; }
public void setModel(String model) { this.model = model; }
public Integer getTimeout() { return timeout; }
public void setTimeout(Integer timeout) { this.timeout = timeout; }
public Integer getMaxTables() { return maxTables; }
public void setMaxTables(Integer maxTables) { this.maxTables = maxTables; }
public Integer getMaxColumnsPerTable() { return maxColumnsPerTable; }
public void setMaxColumnsPerTable(Integer maxColumnsPerTable) { this.maxColumnsPerTable = maxColumnsPerTable; }
}

View File

@@ -0,0 +1,113 @@
package com.ruoyi.generator.service.front;
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.stereotype.Component;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.generator.domain.front.dto.DatabaseColumnDesign;
import com.ruoyi.generator.domain.front.dto.DatabaseDesignResponse;
import com.ruoyi.generator.domain.front.dto.DatabaseTableDesign;
@Component
public class AiDatabaseSchemaValidator
{
private static final Pattern NAME = Pattern.compile("^[a-z][a-z0-9_]{1,63}$");
private static final Pattern BIGINT = Pattern.compile("^bigint(\\(20\\))?$");
private static final Pattern INT = Pattern.compile("^int(\\(11\\))?$");
private static final Pattern VARCHAR = Pattern.compile("^varchar\\(([1-9][0-9]{0,2}|1000)\\)$");
private static final Pattern CHAR = Pattern.compile("^char\\(([1-9][0-9]?)\\)$");
private static final Pattern DECIMAL = Pattern.compile("^decimal\\(([1-9][0-9]?),([1-9][0-9]?)\\)$");
private static final Set<String> EXACT_TYPES = new HashSet<String>(Arrays.asList("datetime", "date", "text", "longtext"));
private static final List<String> DANGEROUS_SQL = Arrays.asList(
"drop", "truncate", "alter user", "grant", "revoke", "load_file", "into outfile");
public boolean isValidName(String name)
{
return StringUtils.isNotBlank(name) && NAME.matcher(name).matches();
}
public boolean isAllowedColumnType(String columnType)
{
if (StringUtils.isBlank(columnType))
{
return false;
}
String type = columnType.trim().toLowerCase(Locale.ENGLISH);
return EXACT_TYPES.contains(type) || BIGINT.matcher(type).matches() || INT.matcher(type).matches()
|| VARCHAR.matcher(type).matches() || CHAR.matcher(type).matches() || DECIMAL.matcher(type).matches();
}
public boolean isSafeSql(String sql)
{
if (sql == null)
{
return true;
}
String lowerSql = sql.toLowerCase(Locale.ENGLISH);
for (String dangerous : DANGEROUS_SQL)
{
if (lowerSql.contains(dangerous))
{
return false;
}
}
return true;
}
public void validate(DatabaseDesignResponse response, int maxTables, int maxColumnsPerTable)
{
if (response == null || response.getTables() == null || response.getTables().isEmpty())
{
throw new ServiceException("至少需要一张数据表");
}
if (response.getTables().size() > maxTables)
{
throw new ServiceException("数据表数量超过限制");
}
for (DatabaseTableDesign table : response.getTables())
{
validateTable(table, maxColumnsPerTable);
}
}
private void validateTable(DatabaseTableDesign table, int maxColumnsPerTable)
{
if (table == null || !isValidName(table.getTableName()))
{
throw new ServiceException("表名只能包含小写字母、数字和下划线");
}
if (table.getColumns() == null || table.getColumns().isEmpty())
{
throw new ServiceException("每张表至少需要一个字段");
}
if (table.getColumns().size() > maxColumnsPerTable)
{
throw new ServiceException("字段数量超过限制");
}
int primaryKeyCount = 0;
for (DatabaseColumnDesign column : table.getColumns())
{
if (column == null || !isValidName(column.getColumnName()))
{
throw new ServiceException("字段名只能包含小写字母、数字和下划线");
}
if (!isAllowedColumnType(column.getColumnType()))
{
throw new ServiceException("字段类型不在允许范围内");
}
if ("1".equals(column.getIsPk()))
{
primaryKeyCount++;
}
}
if (primaryKeyCount != 1)
{
throw new ServiceException("每张表必须且只能有一个主键");
}
}
}

View File

@@ -0,0 +1,311 @@
package com.ruoyi.generator.service.front;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.alibaba.fastjson2.JSON;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.generator.config.DeepSeekProperties;
import com.ruoyi.generator.domain.front.FrontProject;
import com.ruoyi.generator.domain.front.FrontProjectColumn;
import com.ruoyi.generator.domain.front.FrontProjectGeneration;
import com.ruoyi.generator.domain.front.FrontProjectTable;
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.GenerateDatabaseRequest;
import com.ruoyi.generator.mapper.front.FrontProjectColumnMapper;
import com.ruoyi.generator.mapper.front.FrontProjectGenerationMapper;
import com.ruoyi.generator.mapper.front.FrontProjectMapper;
import com.ruoyi.generator.mapper.front.FrontProjectTableMapper;
@Service
public class AiGenerateServiceImpl implements IAiGenerateService
{
@Autowired
private IDeepSeekClient deepSeekClient;
@Autowired
private DeepSeekProperties deepSeekProperties;
@Autowired
private AiDatabaseSchemaValidator validator;
@Autowired
private MysqlDdlBuilder ddlBuilder;
@Autowired
private FrontProjectMapper frontProjectMapper;
@Autowired
private FrontProjectTableMapper frontProjectTableMapper;
@Autowired
private FrontProjectColumnMapper frontProjectColumnMapper;
@Autowired
private FrontProjectGenerationMapper frontProjectGenerationMapper;
@Override
@Transactional
public DatabaseDesignResponse generateDatabase(Long userId, Long projectId, GenerateDatabaseRequest request)
{
assertFrontUserLoggedIn(userId);
FrontProject project = getProject(userId, projectId);
String prompt = buildPrompt(project, request);
long start = System.currentTimeMillis();
FrontProjectGeneration generation = newGeneration(projectId, userId, prompt);
try
{
String aiContent = deepSeekClient.chat(prompt);
DatabaseDesignResponse response = parseResponse(aiContent);
response.setProjectId(projectId);
normalizeAndBuildSql(response);
validator.validate(response, maxTables(), maxColumnsPerTable());
persistDatabase(userId, projectId, response);
generation.setResponsePayload(aiContent);
generation.setSuccess("1");
generation.setElapsedMs(System.currentTimeMillis() - start);
frontProjectGenerationMapper.insertFrontProjectGeneration(generation);
return response;
}
catch (RuntimeException e)
{
generation.setSuccess("0");
generation.setErrorMessage(e.getMessage());
generation.setElapsedMs(System.currentTimeMillis() - start);
frontProjectGenerationMapper.insertFrontProjectGeneration(generation);
throw e;
}
}
private void assertFrontUserLoggedIn(Long userId)
{
if (userId == null)
{
throw new ServiceException("前台用户未登录");
}
}
private FrontProject getProject(Long userId, Long projectId)
{
FrontProject project = frontProjectMapper.selectFrontProjectByUserAndId(userId, projectId);
if (project == null)
{
throw new ServiceException("项目不存在或无权限");
}
return project;
}
private String buildPrompt(FrontProject project, GenerateDatabaseRequest request)
{
StringBuilder prompt = new StringBuilder();
prompt.append("请为企业管理系统生成MySQL数据库设计只返回JSON格式为{\"tables\":[{\"tableName\":\"car_info\",\"tableComment\":\"车辆信息表\",\"columns\":[]}]}。\n");
prompt.append("限制表名和字段名使用小写字母、数字、下划线必须且只能有一个主键字段类型只能使用bigint(20)、int(11)、varchar(n)、char(n)、decimal(p,s)、datetime、date、text、longtext。\n");
prompt.append("项目名称:").append(firstNonBlank(request == null ? null : request.getProjectName(), project.getProjectName())).append("\n");
prompt.append("项目描述:").append(firstNonBlank(request == null ? null : request.getProjectDesc(), project.getProjectDesc())).append("\n");
prompt.append("行业模板:").append(firstNonBlank(request == null ? null : request.getIndustryTemplate(), project.getIndustryTemplate())).append("\n");
prompt.append("补充要求:").append(request == null ? "" : StringUtils.defaultString(request.getExtraRequirements())).append("\n");
prompt.append("最多生成").append(maxTables()).append("张表,每张表最多").append(maxColumnsPerTable()).append("个字段。");
return prompt.toString();
}
private String firstNonBlank(String first, String second)
{
return StringUtils.isNotBlank(first) ? first : StringUtils.defaultString(second);
}
private DatabaseDesignResponse parseResponse(String aiContent)
{
String content = stripMarkdownFence(aiContent);
DatabaseDesignResponse response = JSON.parseObject(content, DatabaseDesignResponse.class);
if (response == null)
{
throw new ServiceException("AI 数据库设计为空");
}
return response;
}
private String stripMarkdownFence(String content)
{
String text = StringUtils.defaultString(content).trim();
if (text.startsWith("```"))
{
text = text.replaceFirst("^```[a-zA-Z]*\\s*", "");
text = text.replaceFirst("\\s*```$", "");
}
return text;
}
private void normalizeAndBuildSql(DatabaseDesignResponse response)
{
List<String> ddlList = new ArrayList<String>();
if (response.getTables() == null)
{
return;
}
int tableSort = 1;
for (DatabaseTableDesign table : response.getTables())
{
table.setClassName(StringUtils.defaultIfEmpty(table.getClassName(), toClassName(table.getTableName())));
table.setBusinessName(StringUtils.defaultIfEmpty(table.getBusinessName(), table.getTableName()));
table.setFunctionName(StringUtils.defaultIfEmpty(table.getFunctionName(), table.getTableComment()));
table.setTplCategory(StringUtils.defaultIfEmpty(table.getTplCategory(), "crud"));
table.setTplWebType(StringUtils.defaultIfEmpty(table.getTplWebType(), "element-plus"));
table.setSort(table.getSort() == null ? tableSort : table.getSort());
int columnSort = 1;
if (table.getColumns() != null)
{
for (DatabaseColumnDesign column : table.getColumns())
{
column.setJavaType(StringUtils.defaultIfEmpty(column.getJavaType(), guessJavaType(column.getColumnType())));
column.setJavaField(StringUtils.defaultIfEmpty(column.getJavaField(), toJavaField(column.getColumnName())));
column.setIsPk(StringUtils.defaultIfEmpty(column.getIsPk(), "0"));
column.setIsIncrement(StringUtils.defaultIfEmpty(column.getIsIncrement(), "0"));
column.setIsRequired(StringUtils.defaultIfEmpty(column.getIsRequired(), "0"));
column.setIsInsert(StringUtils.defaultIfEmpty(column.getIsInsert(), "1"));
column.setIsEdit(StringUtils.defaultIfEmpty(column.getIsEdit(), "1"));
column.setIsList(StringUtils.defaultIfEmpty(column.getIsList(), "1"));
column.setIsQuery(StringUtils.defaultIfEmpty(column.getIsQuery(), "0"));
column.setQueryType(StringUtils.defaultIfEmpty(column.getQueryType(), "EQ"));
column.setHtmlType(StringUtils.defaultIfEmpty(column.getHtmlType(), "input"));
column.setSort(column.getSort() == null ? columnSort : column.getSort());
columnSort++;
}
}
table.setCreateTableSql(ddlBuilder.build(table));
if (!validator.isSafeSql(table.getCreateTableSql()))
{
throw new ServiceException("生成SQL包含危险语句");
}
ddlList.add(table.getCreateTableSql());
tableSort++;
}
response.setSql(StringUtils.join(ddlList, "\n\n"));
}
private void persistDatabase(Long userId, Long projectId, DatabaseDesignResponse response)
{
frontProjectColumnMapper.deleteColumnsByProjectId(projectId);
frontProjectTableMapper.deleteTablesByProjectId(projectId);
for (DatabaseTableDesign tableDesign : response.getTables())
{
FrontProjectTable table = toFrontProjectTable(projectId, userId, tableDesign);
frontProjectTableMapper.insertFrontProjectTable(table);
for (DatabaseColumnDesign columnDesign : tableDesign.getColumns())
{
FrontProjectColumn column = toFrontProjectColumn(projectId, table.getTableId(), columnDesign);
frontProjectColumnMapper.insertFrontProjectColumn(column);
}
}
}
private FrontProjectGeneration newGeneration(Long projectId, Long userId, String prompt)
{
FrontProjectGeneration generation = new FrontProjectGeneration();
generation.setProjectId(projectId);
generation.setUserId(userId);
generation.setGenerateType("database");
generation.setProvider("deepseek");
generation.setPromptSummary(StringUtils.abbreviate(prompt, 255));
generation.setRequestPayload(prompt);
return generation;
}
private FrontProjectTable toFrontProjectTable(Long projectId, Long userId, DatabaseTableDesign source)
{
FrontProjectTable table = new FrontProjectTable();
table.setProjectId(projectId);
table.setUserId(userId);
table.setTableName(source.getTableName());
table.setTableComment(source.getTableComment());
table.setClassName(source.getClassName());
table.setModuleName(source.getModuleName());
table.setBusinessName(source.getBusinessName());
table.setFunctionName(source.getFunctionName());
table.setTplCategory(source.getTplCategory());
table.setTplWebType(source.getTplWebType());
table.setOptions(source.getOptions());
table.setCreateTableSql(source.getCreateTableSql());
table.setSort(source.getSort());
table.setStatus("0");
return table;
}
private FrontProjectColumn toFrontProjectColumn(Long projectId, Long tableId, DatabaseColumnDesign source)
{
FrontProjectColumn column = new FrontProjectColumn();
column.setProjectId(projectId);
column.setTableId(tableId);
column.setColumnName(source.getColumnName());
column.setColumnComment(source.getColumnComment());
column.setColumnType(source.getColumnType());
column.setJavaType(source.getJavaType());
column.setJavaField(source.getJavaField());
column.setIsPk(source.getIsPk());
column.setIsIncrement(source.getIsIncrement());
column.setIsRequired(source.getIsRequired());
column.setIsInsert(source.getIsInsert());
column.setIsEdit(source.getIsEdit());
column.setIsList(source.getIsList());
column.setIsQuery(source.getIsQuery());
column.setQueryType(source.getQueryType());
column.setHtmlType(source.getHtmlType());
column.setDictType(source.getDictType());
column.setDefaultValue(source.getDefaultValue());
column.setSort(source.getSort());
return column;
}
private String toClassName(String tableName)
{
StringBuilder builder = new StringBuilder();
for (String part : StringUtils.defaultString(tableName).split("_"))
{
if (part.length() > 0)
{
builder.append(part.substring(0, 1).toUpperCase(Locale.ENGLISH)).append(part.substring(1));
}
}
return builder.toString();
}
private String toJavaField(String columnName)
{
String className = toClassName(columnName);
if (className.length() == 0)
{
return className;
}
return className.substring(0, 1).toLowerCase(Locale.ENGLISH) + className.substring(1);
}
private String guessJavaType(String columnType)
{
String type = StringUtils.defaultString(columnType).toLowerCase(Locale.ENGLISH);
if (type.startsWith("bigint"))
{
return "Long";
}
if (type.startsWith("int"))
{
return "Integer";
}
if (type.startsWith("decimal"))
{
return "BigDecimal";
}
if ("datetime".equals(type))
{
return "Date";
}
return "String";
}
private int maxTables()
{
return deepSeekProperties.getMaxTables() == null ? 12 : deepSeekProperties.getMaxTables();
}
private int maxColumnsPerTable()
{
return deepSeekProperties.getMaxColumnsPerTable() == null ? 30 : deepSeekProperties.getMaxColumnsPerTable();
}
}

View File

@@ -0,0 +1,87 @@
package com.ruoyi.generator.service.front;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.generator.config.DeepSeekProperties;
@Component
public class HttpDeepSeekClient implements IDeepSeekClient
{
private static final String SYSTEM_PROMPT = "你是企业管理系统数据库设计助手只返回JSON。";
@Autowired
private DeepSeekProperties deepSeekProperties;
@Override
public String chat(String prompt)
{
if (StringUtils.isBlank(deepSeekProperties.getApiKey()))
{
throw new ServiceException("DeepSeek API Key 未配置");
}
JSONObject request = new JSONObject();
request.put("model", deepSeekProperties.getModel());
JSONArray messages = new JSONArray();
messages.add(message("system", SYSTEM_PROMPT));
messages.add(message("user", prompt));
request.put("messages", messages);
request.put("temperature", 0.2);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(deepSeekProperties.getApiKey());
HttpEntity<String> entity = new HttpEntity<String>(request.toJSONString(), headers);
ResponseEntity<String> response = restTemplate().postForEntity(chatUrl(), entity, String.class);
return parseContent(response.getBody());
}
private JSONObject message(String role, String content)
{
JSONObject message = new JSONObject();
message.put("role", role);
message.put("content", content);
return message;
}
private RestTemplate restTemplate()
{
int timeout = deepSeekProperties.getTimeout() == null ? 60000 : deepSeekProperties.getTimeout();
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout((int) TimeUnit.MILLISECONDS.toMillis(timeout));
factory.setReadTimeout((int) TimeUnit.MILLISECONDS.toMillis(timeout));
return new RestTemplate(factory);
}
private String chatUrl()
{
String baseUrl = StringUtils.defaultIfEmpty(deepSeekProperties.getBaseUrl(), "https://api.deepseek.com");
return baseUrl.replaceAll("/+$", "") + "/chat/completions";
}
private String parseContent(String body)
{
JSONObject json = JSONObject.parseObject(body);
JSONArray choices = json.getJSONArray("choices");
if (choices == null || choices.isEmpty())
{
throw new ServiceException("DeepSeek 返回结果为空");
}
JSONObject message = choices.getJSONObject(0).getJSONObject("message");
if (message == null || StringUtils.isBlank(message.getString("content")))
{
throw new ServiceException("DeepSeek 返回内容为空");
}
return message.getString("content");
}
}

View File

@@ -0,0 +1,9 @@
package com.ruoyi.generator.service.front;
import com.ruoyi.generator.domain.front.dto.DatabaseDesignResponse;
import com.ruoyi.generator.domain.front.dto.GenerateDatabaseRequest;
public interface IAiGenerateService
{
DatabaseDesignResponse generateDatabase(Long userId, Long projectId, GenerateDatabaseRequest request);
}

View File

@@ -0,0 +1,6 @@
package com.ruoyi.generator.service.front;
public interface IDeepSeekClient
{
String chat(String prompt);
}

View File

@@ -0,0 +1,58 @@
package com.ruoyi.generator.service.front;
import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Component;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.generator.domain.front.dto.DatabaseColumnDesign;
import com.ruoyi.generator.domain.front.dto.DatabaseTableDesign;
@Component
public class MysqlDdlBuilder
{
public String build(DatabaseTableDesign table)
{
List<String> lines = new ArrayList<String>();
String primaryKey = null;
for (DatabaseColumnDesign column : table.getColumns())
{
lines.add(" " + buildColumn(column));
if ("1".equals(column.getIsPk()))
{
primaryKey = column.getColumnName();
}
}
if (primaryKey != null)
{
lines.add(" PRIMARY KEY (`" + primaryKey + "`)");
}
return "CREATE TABLE `" + table.getTableName() + "` (\n"
+ StringUtils.join(lines, ",\n")
+ "\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='" + escapeComment(table.getTableComment()) + "';";
}
private String buildColumn(DatabaseColumnDesign column)
{
StringBuilder sql = new StringBuilder();
sql.append("`").append(column.getColumnName()).append("` ").append(column.getColumnType());
if ("1".equals(column.getIsRequired()) || "1".equals(column.getIsPk()))
{
sql.append(" NOT NULL");
}
else
{
sql.append(" DEFAULT NULL");
}
if ("1".equals(column.getIsIncrement()))
{
sql.append(" AUTO_INCREMENT");
}
sql.append(" COMMENT '").append(escapeComment(column.getColumnComment())).append("'");
return sql.toString();
}
private String escapeComment(String comment)
{
return StringUtils.defaultString(comment).replace("'", "''");
}
}

View File

@@ -0,0 +1,23 @@
package com.ruoyi.generator.service.front;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
public class AiDatabaseSchemaValidatorTest
{
private final AiDatabaseSchemaValidator validator = new AiDatabaseSchemaValidator();
@Test
public void validatesNamesColumnTypesAndUnsafeSql()
{
assertTrue(validator.isValidName("car_info"));
assertFalse(validator.isValidName("CarInfo"));
assertFalse(validator.isValidName("sys user"));
assertTrue(validator.isAllowedColumnType("varchar(100)"));
assertTrue(validator.isAllowedColumnType("decimal(10,2)"));
assertFalse(validator.isAllowedColumnType("json"));
assertFalse(validator.isSafeSql("drop table sys_user"));
}
}

View File

@@ -0,0 +1,140 @@
package com.ruoyi.generator.service.front;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.lang.reflect.Field;
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.generator.config.DeepSeekProperties;
import com.ruoyi.generator.domain.front.FrontProject;
import com.ruoyi.generator.domain.front.FrontProjectColumn;
import com.ruoyi.generator.domain.front.FrontProjectGeneration;
import com.ruoyi.generator.domain.front.FrontProjectTable;
import com.ruoyi.generator.domain.front.dto.DatabaseDesignResponse;
import com.ruoyi.generator.domain.front.dto.GenerateDatabaseRequest;
import com.ruoyi.generator.mapper.front.FrontProjectColumnMapper;
import com.ruoyi.generator.mapper.front.FrontProjectGenerationMapper;
import com.ruoyi.generator.mapper.front.FrontProjectMapper;
import com.ruoyi.generator.mapper.front.FrontProjectTableMapper;
public class AiGenerateServiceImplTest
{
private AiGenerateServiceImpl service;
@Mock
private IDeepSeekClient deepSeekClient;
@Mock
private FrontProjectMapper frontProjectMapper;
@Mock
private FrontProjectTableMapper frontProjectTableMapper;
@Mock
private FrontProjectColumnMapper frontProjectColumnMapper;
@Mock
private FrontProjectGenerationMapper frontProjectGenerationMapper;
@Before
public void setUp() throws Exception
{
MockitoAnnotations.initMocks(this);
service = new AiGenerateServiceImpl();
setField("deepSeekClient", deepSeekClient);
setField("frontProjectMapper", frontProjectMapper);
setField("frontProjectTableMapper", frontProjectTableMapper);
setField("frontProjectColumnMapper", frontProjectColumnMapper);
setField("frontProjectGenerationMapper", frontProjectGenerationMapper);
setField("validator", new AiDatabaseSchemaValidator());
setField("ddlBuilder", new MysqlDdlBuilder());
setField("deepSeekProperties", new DeepSeekProperties());
}
@Test
public void generateDatabasePersistsTablesColumnsAndGenerationRecord()
{
when(frontProjectMapper.selectFrontProjectByUserAndId(7L, 10L)).thenReturn(project());
when(frontProjectTableMapper.selectTablesByProjectId(10L)).thenReturn(Collections.<FrontProjectTable>emptyList());
when(deepSeekClient.chat(org.mockito.ArgumentMatchers.anyString())).thenReturn(mockResponse());
DatabaseDesignResponse response = service.generateDatabase(7L, 10L, request());
ArgumentCaptor<FrontProjectTable> tableCaptor = ArgumentCaptor.forClass(FrontProjectTable.class);
ArgumentCaptor<FrontProjectColumn> columnCaptor = ArgumentCaptor.forClass(FrontProjectColumn.class);
ArgumentCaptor<FrontProjectGeneration> generationCaptor = ArgumentCaptor.forClass(FrontProjectGeneration.class);
verify(frontProjectTableMapper).insertFrontProjectTable(tableCaptor.capture());
verify(frontProjectColumnMapper, org.mockito.Mockito.times(2)).insertFrontProjectColumn(columnCaptor.capture());
verify(frontProjectGenerationMapper).insertFrontProjectGeneration(generationCaptor.capture());
assertEquals("car_info", tableCaptor.getValue().getTableName());
assertTrue(tableCaptor.getValue().getCreateTableSql().contains("CREATE TABLE `car_info`"));
assertEquals(2, columnCaptor.getAllValues().size());
assertEquals("1", generationCaptor.getValue().getSuccess());
assertTrue(response.getSql().contains("CREATE TABLE `car_info`"));
}
private void setField(String name, Object value) throws Exception
{
Field field = AiGenerateServiceImpl.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("车辆管理系统");
project.setProjectDesc("管理车辆档案");
project.setIndustryTemplate("fleet");
return project;
}
private GenerateDatabaseRequest request()
{
GenerateDatabaseRequest request = new GenerateDatabaseRequest();
request.setProjectName("车辆管理系统");
request.setProjectDesc("管理车辆档案");
request.setIndustryTemplate("fleet");
request.setExtraRequirements("车辆基础信息");
return request;
}
private String mockResponse()
{
return "{\n"
+ " \"tables\": [\n"
+ " {\n"
+ " \"tableName\": \"car_info\",\n"
+ " \"tableComment\": \"车辆信息表\",\n"
+ " \"columns\": [\n"
+ " {\n"
+ " \"columnName\": \"car_id\",\n"
+ " \"columnType\": \"bigint(20)\",\n"
+ " \"javaType\": \"Long\",\n"
+ " \"javaField\": \"carId\",\n"
+ " \"isPk\": \"1\",\n"
+ " \"isIncrement\": \"1\",\n"
+ " \"isRequired\": \"1\",\n"
+ " \"columnComment\": \"车辆ID\"\n"
+ " },\n"
+ " {\n"
+ " \"columnName\": \"car_no\",\n"
+ " \"columnType\": \"varchar(30)\",\n"
+ " \"javaType\": \"String\",\n"
+ " \"javaField\": \"carNo\",\n"
+ " \"isPk\": \"0\",\n"
+ " \"isIncrement\": \"0\",\n"
+ " \"isRequired\": \"1\",\n"
+ " \"columnComment\": \"车牌号\"\n"
+ " }\n"
+ " ]\n"
+ " }\n"
+ " ]\n"
+ "}";
}
}

View File

@@ -0,0 +1,47 @@
package com.ruoyi.generator.service.front;
import static org.junit.Assert.assertTrue;
import java.util.Arrays;
import org.junit.Test;
import com.ruoyi.generator.domain.front.dto.DatabaseColumnDesign;
import com.ruoyi.generator.domain.front.dto.DatabaseTableDesign;
public class MysqlDdlBuilderTest
{
private final MysqlDdlBuilder builder = new MysqlDdlBuilder();
@Test
public void buildsCreateTableSqlWithPrimaryKeyNullabilityAndEscapedComments()
{
DatabaseTableDesign table = new DatabaseTableDesign();
table.setTableName("car_info");
table.setTableComment("车辆'信息表");
table.setColumns(Arrays.asList(
column("car_id", "bigint(20)", "1", "1", "1", "车辆ID"),
column("car_no", "varchar(30)", "0", "0", "1", "车牌号"),
column("remark", "text", "0", "0", "0", "备注'信息")));
String sql = builder.build(table);
assertTrue(sql.contains("CREATE TABLE `car_info`"));
assertTrue(sql.contains("`car_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '车辆ID'"));
assertTrue(sql.contains("`car_no` varchar(30) NOT NULL COMMENT '车牌号'"));
assertTrue(sql.contains("`remark` text DEFAULT NULL COMMENT '备注''信息'"));
assertTrue(sql.contains("PRIMARY KEY (`car_id`)"));
assertTrue(sql.endsWith("COMMENT='车辆''信息表';"));
}
private DatabaseColumnDesign column(String name, String type, String isPk, String isIncrement,
String isRequired, String comment)
{
DatabaseColumnDesign column = new DatabaseColumnDesign();
column.setColumnName(name);
column.setColumnType(type);
column.setIsPk(isPk);
column.setIsIncrement(isIncrement);
column.setIsRequired(isRequired);
column.setColumnComment(comment);
return column;
}
}