From a3608381edc0170436744273d6e122bbac7c7541 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E9=B9=8F?= Date: Fri, 22 May 2026 12:01:39 +0800 Subject: [PATCH] Add DeepSeek database generation service --- .../front/FrontProjectController.java | 11 + .../src/main/resources/application.yml | 8 + .../generator/config/DeepSeekProperties.java | 29 ++ .../front/AiDatabaseSchemaValidator.java | 113 +++++++ .../service/front/AiGenerateServiceImpl.java | 311 ++++++++++++++++++ .../service/front/HttpDeepSeekClient.java | 87 +++++ .../service/front/IAiGenerateService.java | 9 + .../service/front/IDeepSeekClient.java | 6 + .../service/front/MysqlDdlBuilder.java | 58 ++++ .../front/AiDatabaseSchemaValidatorTest.java | 23 ++ .../front/AiGenerateServiceImplTest.java | 140 ++++++++ .../service/front/MysqlDdlBuilderTest.java | 47 +++ 12 files changed, 842 insertions(+) create mode 100644 RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/config/DeepSeekProperties.java create mode 100644 RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiDatabaseSchemaValidator.java create mode 100644 RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiGenerateServiceImpl.java create mode 100644 RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/HttpDeepSeekClient.java create mode 100644 RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/IAiGenerateService.java create mode 100644 RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/IDeepSeekClient.java create mode 100644 RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/MysqlDdlBuilder.java create mode 100644 RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/AiDatabaseSchemaValidatorTest.java create mode 100644 RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/AiGenerateServiceImplTest.java create mode 100644 RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/MysqlDdlBuilderTest.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 index fcf6c3c..04b50af 100644 --- 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 @@ -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)); + } } diff --git a/RuoYi-Vue/ruoyi-admin/src/main/resources/application.yml b/RuoYi-Vue/ruoyi-admin/src/main/resources/application.yml index c874e05..d914e21 100644 --- a/RuoYi-Vue/ruoyi-admin/src/main/resources/application.yml +++ b/RuoYi-Vue/ruoyi-admin/src/main/resources/application.yml @@ -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 diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/config/DeepSeekProperties.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/config/DeepSeekProperties.java new file mode 100644 index 0000000..ae0850d --- /dev/null +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/config/DeepSeekProperties.java @@ -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; } +} diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiDatabaseSchemaValidator.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiDatabaseSchemaValidator.java new file mode 100644 index 0000000..eb69201 --- /dev/null +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiDatabaseSchemaValidator.java @@ -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 EXACT_TYPES = new HashSet(Arrays.asList("datetime", "date", "text", "longtext")); + private static final List 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("每张表必须且只能有一个主键"); + } + } +} diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiGenerateServiceImpl.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiGenerateServiceImpl.java new file mode 100644 index 0000000..6bbeda1 --- /dev/null +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiGenerateServiceImpl.java @@ -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 ddlList = new ArrayList(); + 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(); + } +} diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/HttpDeepSeekClient.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/HttpDeepSeekClient.java new file mode 100644 index 0000000..b16c609 --- /dev/null +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/HttpDeepSeekClient.java @@ -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 entity = new HttpEntity(request.toJSONString(), headers); + ResponseEntity 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"); + } +} diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/IAiGenerateService.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/IAiGenerateService.java new file mode 100644 index 0000000..e2ff156 --- /dev/null +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/IAiGenerateService.java @@ -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); +} diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/IDeepSeekClient.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/IDeepSeekClient.java new file mode 100644 index 0000000..0b34138 --- /dev/null +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/IDeepSeekClient.java @@ -0,0 +1,6 @@ +package com.ruoyi.generator.service.front; + +public interface IDeepSeekClient +{ + String chat(String prompt); +} diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/MysqlDdlBuilder.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/MysqlDdlBuilder.java new file mode 100644 index 0000000..72374d3 --- /dev/null +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/MysqlDdlBuilder.java @@ -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 lines = new ArrayList(); + 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("'", "''"); + } +} diff --git a/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/AiDatabaseSchemaValidatorTest.java b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/AiDatabaseSchemaValidatorTest.java new file mode 100644 index 0000000..2c4860a --- /dev/null +++ b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/AiDatabaseSchemaValidatorTest.java @@ -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")); + } +} diff --git a/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/AiGenerateServiceImplTest.java b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/AiGenerateServiceImplTest.java new file mode 100644 index 0000000..b19f998 --- /dev/null +++ b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/AiGenerateServiceImplTest.java @@ -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.emptyList()); + when(deepSeekClient.chat(org.mockito.ArgumentMatchers.anyString())).thenReturn(mockResponse()); + + DatabaseDesignResponse response = service.generateDatabase(7L, 10L, request()); + + ArgumentCaptor tableCaptor = ArgumentCaptor.forClass(FrontProjectTable.class); + ArgumentCaptor columnCaptor = ArgumentCaptor.forClass(FrontProjectColumn.class); + ArgumentCaptor 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" + + "}"; + } +} diff --git a/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/MysqlDdlBuilderTest.java b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/MysqlDdlBuilderTest.java new file mode 100644 index 0000000..3c2d2a4 --- /dev/null +++ b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/MysqlDdlBuilderTest.java @@ -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; + } +}