Add DeepSeek database generation service
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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("每张表必须且只能有一个主键");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.ruoyi.generator.service.front;
|
||||
|
||||
public interface IDeepSeekClient
|
||||
{
|
||||
String chat(String prompt);
|
||||
}
|
||||
@@ -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("'", "''");
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
+ "}";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user