From 0bc50f39f13ccece8098eafe55d5603f7ef356f7 Mon Sep 17 00:00:00 2001 From: "yingjie.liu" Date: Fri, 11 Jul 2025 16:12:14 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E4=BB=A3=E7=A0=81=E7=94=9F=E6=88=90)?= =?UTF-8?q?=EF=BC=9A=E5=BF=AB=E9=80=9F=E7=94=9F=E6=88=90=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E5=88=B0=E6=8C=87=E5=AE=9A=E9=A1=B9=E7=9B=AE=E7=9B=AE=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/codegen/CodegenController.java | 17 +- .../vo/CodegenGenerateToPathReqVO.java | 34 +++ .../vo/CodegenGenerateToPathRespVO.java | 79 +++++ .../infra/service/codegen/CodegenService.java | 12 + .../service/codegen/CodegenServiceImpl.java | 65 +++++ .../codegen/inner/CodegenFileUtils.java | 272 ++++++++++++++++++ 6 files changed, 475 insertions(+), 4 deletions(-) create mode 100644 yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/CodegenGenerateToPathReqVO.java create mode 100644 yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/CodegenGenerateToPathRespVO.java create mode 100644 yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenFileUtils.java diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/CodegenController.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/CodegenController.java index 9df1344edd..b39dfb1939 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/CodegenController.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/CodegenController.java @@ -5,10 +5,7 @@ import cn.hutool.core.util.ZipUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.CodegenCreateListReqVO; -import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.CodegenDetailRespVO; -import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.CodegenPreviewRespVO; -import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.CodegenUpdateReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.*; import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table.CodegenTablePageReqVO; import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table.CodegenTableRespVO; import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table.DatabaseTableRespVO; @@ -148,4 +145,16 @@ public class CodegenController { writeAttachment(response, "codegen.zip", outputStream.toByteArray()); } + @Operation(summary = "生成代码到指定路径") + @PostMapping("/generate-to-path") + @PreAuthorize("@ss.hasPermission('infra:codegen:download')") + public CommonResult generateCodegenToPath(@Valid @RequestBody CodegenGenerateToPathReqVO reqVO) { + CodegenGenerateToPathRespVO result = codegenService.generateCodesToPath( + reqVO.getTableId(), + reqVO.getBackendPath(), + reqVO.getFrontendPath(), + reqVO.getOverwrite() + ); + return success(result); + } } diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/CodegenGenerateToPathReqVO.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/CodegenGenerateToPathReqVO.java new file mode 100644 index 0000000000..2160b1fcfb --- /dev/null +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/CodegenGenerateToPathReqVO.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.infra.controller.admin.codegen.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * 代码生成器 - 生成到指定路径 Request VO + * + * @author 芋道源码 + */ +@Schema(description = "管理后台 - 代码生成器生成到指定路径 Request VO") +@Data +public class CodegenGenerateToPathReqVO { + + @Schema(description = "表编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "表编号不能为空") + private Long tableId; + + @Schema(description = "后端代码目标路径", requiredMode = Schema.RequiredMode.REQUIRED, + example = "D:/projects/yudao-boot-mini") + @NotBlank(message = "后端代码目标路径不能为空") + private String backendPath; + + @Schema(description = "前端代码目标路径", + example = "D:/projects/yudao-ui-admin-vue3") + private String frontendPath; + + @Schema(description = "是否覆盖已存在的文件", example = "false") + private Boolean overwrite = false; + +} diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/CodegenGenerateToPathRespVO.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/CodegenGenerateToPathRespVO.java new file mode 100644 index 0000000000..2ca3038af9 --- /dev/null +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/CodegenGenerateToPathRespVO.java @@ -0,0 +1,79 @@ +package cn.iocoder.yudao.module.infra.controller.admin.codegen.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +/** + * 代码生成器 - 生成到指定路径 Response VO + * + * @author 芋道源码 + */ +@Schema(description = "管理后台 - 代码生成器生成到指定路径 Response VO") +@Data +public class CodegenGenerateToPathRespVO { + + @Schema(description = "是否成功", requiredMode = Schema.RequiredMode.REQUIRED) + private Boolean success; + + @Schema(description = "总文件数量", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer totalCount; + + @Schema(description = "成功生成的文件数量", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer successCount; + + @Schema(description = "失败的文件数量", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer failCount; + + @Schema(description = "后端文件数量") + private Integer backendFileCount; + + @Schema(description = "前端文件数量") + private Integer frontendFileCount; + + @Schema(description = "SQL 文件数量") + private Integer sqlFileCount; + + @Schema(description = "成功生成的文件列表") + private List successFiles; + + @Schema(description = "失败的文件列表") + private List failFiles; + + @Schema(description = "错误信息") + private String errorMessage; + + /** + * 创建成功响应 + */ + public static CodegenGenerateToPathRespVO success(Integer totalCount, Integer successCount, + List successFiles, List failFiles, + Integer backendFileCount, Integer frontendFileCount, Integer sqlFileCount) { + CodegenGenerateToPathRespVO respVO = new CodegenGenerateToPathRespVO(); + respVO.setSuccess(true); + respVO.setTotalCount(totalCount); + respVO.setSuccessCount(successCount); + respVO.setFailCount(totalCount - successCount); + respVO.setBackendFileCount(backendFileCount); + respVO.setFrontendFileCount(frontendFileCount); + respVO.setSqlFileCount(sqlFileCount); + respVO.setSuccessFiles(successFiles); + respVO.setFailFiles(failFiles); + return respVO; + } + + /** + * 创建失败响应 + */ + public static CodegenGenerateToPathRespVO fail(String errorMessage) { + CodegenGenerateToPathRespVO respVO = new CodegenGenerateToPathRespVO(); + respVO.setSuccess(false); + respVO.setErrorMessage(errorMessage); + respVO.setTotalCount(0); + respVO.setSuccessCount(0); + respVO.setFailCount(0); + return respVO; + } + +} diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenService.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenService.java index 7adc9f7f1c..42f0358d32 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenService.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenService.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.infra.service.codegen; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.CodegenCreateListReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.CodegenGenerateToPathRespVO; import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.CodegenUpdateReqVO; import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table.CodegenTablePageReqVO; import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table.DatabaseTableRespVO; @@ -88,6 +89,17 @@ public interface CodegenService { */ Map generationCodes(Long tableId); + /** + * 生成代码到指定路径 + * + * @param tableId 表编号 + * @param backendPath 后端代码目标路径 + * @param frontendPath 前端代码目标路径 + * @param overwrite 是否覆盖已存在的文件 + * @return 生成结果 + */ + CodegenGenerateToPathRespVO generateCodesToPath(Long tableId, String backendPath, String frontendPath, Boolean overwrite); + /** * 获得数据库自带的表定义列表 * diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenServiceImpl.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenServiceImpl.java index 439473b5e0..9603b1b77a 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenServiceImpl.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenServiceImpl.java @@ -5,6 +5,7 @@ import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.CodegenCreateListReqVO; +import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.CodegenGenerateToPathRespVO; import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.CodegenUpdateReqVO; import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table.CodegenTablePageReqVO; import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table.DatabaseTableRespVO; @@ -17,6 +18,7 @@ import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum; import cn.iocoder.yudao.module.infra.framework.codegen.config.CodegenProperties; import cn.iocoder.yudao.module.infra.service.codegen.inner.CodegenBuilder; import cn.iocoder.yudao.module.infra.service.codegen.inner.CodegenEngine; +import cn.iocoder.yudao.module.infra.service.codegen.inner.CodegenFileUtils; import cn.iocoder.yudao.module.infra.service.db.DatabaseTableService; import com.baomidou.mybatisplus.generator.config.po.TableField; import com.baomidou.mybatisplus.generator.config.po.TableInfo; @@ -25,6 +27,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; +import java.nio.file.Paths; import java.util.*; import java.util.function.BiPredicate; import java.util.stream.Collectors; @@ -279,6 +282,68 @@ public class CodegenServiceImpl implements CodegenService { return codegenEngine.execute(table, columns, subTables, subColumnsList); } + @Override + public CodegenGenerateToPathRespVO generateCodesToPath(Long tableId, String backendPath, String frontendPath, Boolean overwrite) { + try { + // 1. 参数校验 + if (StrUtil.isNotBlank(backendPath) && !CodegenFileUtils.isValidPath(backendPath)) { + return CodegenGenerateToPathRespVO.fail("后端路径无效或无权限访问:" + backendPath); + } + if (StrUtil.isNotBlank(frontendPath) && !CodegenFileUtils.isValidPath(frontendPath)) { + return CodegenGenerateToPathRespVO.fail("前端路径无效或无权限访问:" + frontendPath); + } + + // 2. 生成代码 + Map codes = generationCodes(tableId); + + // 3. 分类文件 + CodegenFileUtils.FileCategoryResult categoryResult = CodegenFileUtils.categorizeFiles(codes); + + // 4. 写入文件 + List allSuccessFiles = new ArrayList<>(); + List allFailFiles = new ArrayList<>(); + + // 写入后端文件 + if (StrUtil.isNotBlank(backendPath) && !categoryResult.getBackendFiles().isEmpty()) { + CodegenFileUtils.FileWriteResult backendResult = + CodegenFileUtils.writeFilesToPath(categoryResult.getBackendFiles(), backendPath, overwrite); + allSuccessFiles.addAll(backendResult.getSuccessFiles()); + allFailFiles.addAll(backendResult.getFailFiles()); + } + + // 写入前端文件 + if (StrUtil.isNotBlank(frontendPath) && !categoryResult.getFrontendFiles().isEmpty()) { + CodegenFileUtils.FileWriteResult frontendResult = + CodegenFileUtils.writeFilesToPath(categoryResult.getFrontendFiles(), frontendPath, overwrite); + allSuccessFiles.addAll(frontendResult.getSuccessFiles()); + allFailFiles.addAll(frontendResult.getFailFiles()); + } + + // 写入 SQL 文件到后端路径的 sql 目录 + if (StrUtil.isNotBlank(backendPath) && !categoryResult.getSqlFiles().isEmpty()) { + String sqlPath = Paths.get(backendPath, "sql").toString(); + CodegenFileUtils.FileWriteResult sqlResult = + CodegenFileUtils.writeFilesToPath(categoryResult.getSqlFiles(), sqlPath, overwrite); + allSuccessFiles.addAll(sqlResult.getSuccessFiles()); + allFailFiles.addAll(sqlResult.getFailFiles()); + } + + // 5. 返回结果 + return CodegenGenerateToPathRespVO.success( + codes.size(), + allSuccessFiles.size(), + allSuccessFiles, + allFailFiles, + categoryResult.getBackendFiles().size(), + categoryResult.getFrontendFiles().size(), + categoryResult.getSqlFiles().size() + ); + + } catch (Exception e) { + return CodegenGenerateToPathRespVO.fail("生成代码失败:" + e.getMessage()); + } + } + @Override public List getDatabaseTableList(Long dataSourceConfigId, String name, String comment) { List tables = databaseTableService.getTableList(dataSourceConfigId, name, comment); diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenFileUtils.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenFileUtils.java new file mode 100644 index 0000000000..75b59f16d7 --- /dev/null +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenFileUtils.java @@ -0,0 +1,272 @@ +package cn.iocoder.yudao.module.infra.service.codegen.inner; + +import cn.hutool.core.util.StrUtil; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 代码生成器文件处理工具类 + * + * @author 芋道源码 + */ +public class CodegenFileUtils { + + /** + * 文件类型枚举 + */ + public enum FileType { + BACKEND, FRONTEND, SQL + } + + /** + * 文件分类结果 + */ + public static class FileCategoryResult { + private final Map backendFiles = new HashMap<>(); + private final Map frontendFiles = new HashMap<>(); + private final Map sqlFiles = new HashMap<>(); + + public Map getBackendFiles() { + return backendFiles; + } + + public Map getFrontendFiles() { + return frontendFiles; + } + + public Map getSqlFiles() { + return sqlFiles; + } + + public void addFile(FileType type, String path, String content) { + switch (type) { + case BACKEND: + backendFiles.put(path, content); + break; + case FRONTEND: + frontendFiles.put(path, content); + break; + case SQL: + sqlFiles.put(path, content); + break; + } + } + } + + /** + * 文件写入结果 + */ + public static class FileWriteResult { + private final List successFiles = new ArrayList<>(); + private final List failFiles = new ArrayList<>(); + + public List getSuccessFiles() { + return successFiles; + } + + public List getFailFiles() { + return failFiles; + } + + public void addSuccess(String filePath) { + successFiles.add(filePath); + } + + public void addFail(String filePath, String reason) { + failFiles.add(filePath + " (" + reason + ")"); + } + } + + /** + * 分类生成的文件 + * + * @param codes 生成的代码文件 Map + * @return 分类结果 + */ + public static FileCategoryResult categorizeFiles(Map codes) { + FileCategoryResult result = new FileCategoryResult(); + + for (Map.Entry entry : codes.entrySet()) { + String filePath = entry.getKey(); + String content = entry.getValue(); + FileType type = determineFileType(filePath); + result.addFile(type, filePath, content); + } + + return result; + } + + /** + * 判断文件类型 + * + * @param filePath 文件路径 + * @return 文件类型 + */ + public static FileType determineFileType(String filePath) { + // SQL 文件 + if (filePath.endsWith(".sql")) { + return FileType.SQL; + } + + // 前端文件判断 + if (isFrontendFile(filePath)) { + return FileType.FRONTEND; + } + + // 默认为后端文件 + return FileType.BACKEND; + } + + /** + * 判断是否为前端文件 + */ + private static boolean isFrontendFile(String filePath) { + // 包含前端项目标识 + if (filePath.contains("yudao-ui-") || filePath.contains("vue3") || filePath.contains("vue2")) { + return true; + } + + // 前端文件扩展名 + if (filePath.endsWith(".vue") || filePath.endsWith(".ts") || filePath.endsWith(".js")) { + return true; + } + + // 前端目录结构 + if (filePath.contains("/views/") || filePath.contains("/api/") || + filePath.contains("/components/") || filePath.contains("/assets/")) { + return true; + } + + return false; + } + + /** + * 写入文件到指定路径 + * + * @param files 文件映射 + * @param basePath 基础路径 + * @param overwrite 是否覆盖 + * @return 写入结果 + */ + public static FileWriteResult writeFilesToPath(Map files, String basePath, Boolean overwrite) { + FileWriteResult result = new FileWriteResult(); + + for (Map.Entry entry : files.entrySet()) { + String relativePath = entry.getKey(); + String content = entry.getValue(); + + try { + // 清理文件路径 + String cleanPath = cleanFilePath(relativePath); + Path targetPath = Paths.get(basePath, cleanPath); + + // 检查文件是否存在 + if (Files.exists(targetPath) && !Boolean.TRUE.equals(overwrite)) { + result.addFail(targetPath.toString(), "文件已存在,未覆盖"); + continue; + } + + // 创建父目录 + Files.createDirectories(targetPath.getParent()); + + // 写入文件 + Files.write(targetPath, content.getBytes("UTF-8"), + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + + result.addSuccess(targetPath.toString()); + + } catch (IOException e) { + result.addFail(relativePath, "写入失败: " + e.getMessage()); + } + } + + return result; + } + + /** + * 清理文件路径,移除项目根目录前缀 + * + * @param filePath 原始文件路径 + * @return 清理后的路径 + */ + public static String cleanFilePath(String filePath) { + // 移除常见的项目根目录前缀 + String[] prefixesToRemove = { + "yudao-module-system/", + "yudao-module-infra/", + "yudao-module-member/", + "yudao-module-pay/", + "yudao-module-mall/", + "yudao-module-bpm/", + "yudao-module-mp/", + "yudao-module-report/", + "yudao-ui-admin-vue3/", + "yudao-ui-admin-vue2/", + "yudao-ui-admin-vben/", + "sql/" + }; + + for (String prefix : prefixesToRemove) { + if (filePath.startsWith(prefix)) { + return filePath.substring(prefix.length()); + } + } + + return filePath; + } + + /** + * 验证路径是否有效 + * + * @param path 路径 + * @return 是否有效 + */ + public static boolean isValidPath(String path) { + if (StrUtil.isBlank(path)) { + return false; + } + + try { + Path targetPath = Paths.get(path); + // 检查路径是否为绝对路径 + if (!targetPath.isAbsolute()) { + return false; + } + // 检查父目录是否存在或可创建 + Path parentPath = targetPath.getParent(); + if (parentPath != null && !Files.exists(parentPath)) { + // 尝试创建目录来验证权限 + Files.createDirectories(parentPath); + } + return true; + } catch (Exception e) { + return false; + } + } + + /** + * 获取文件扩展名 + * + * @param filePath 文件路径 + * @return 扩展名 + */ + public static String getFileExtension(String filePath) { + if (StrUtil.isBlank(filePath)) { + return ""; + } + int lastDotIndex = filePath.lastIndexOf('.'); + if (lastDotIndex > 0 && lastDotIndex < filePath.length() - 1) { + return filePath.substring(lastDotIndex + 1); + } + return ""; + } + +} -- Gitee