From b62b7c75f582ecdfe5062ccd4faa88c990b051e6 Mon Sep 17 00:00:00 2001 From: weike001 Date: Fri, 27 Dec 2024 17:29:02 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=BA=94=E7=94=A8=E7=A8=8B=E5=BA=8F?= =?UTF-8?q?=E5=89=8D=E5=90=8E=E7=AB=AF=E4=BB=A3=E7=A0=81=E7=94=9F=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/codegen/CodegenController.java | 19 ++ .../infra/service/codegen/CodegenService.java | 8 + .../service/codegen/CodegenServiceImpl.java | 291 ++++++++++++++++++ .../module/infra/util/DistributedLock.java | 22 ++ .../infra/util/InputStreamConverter.java | 29 ++ .../src/main/resources/application.yaml | 24 ++ 6 files changed, 393 insertions(+) create mode 100644 yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/util/DistributedLock.java create mode 100644 yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/util/InputStreamConverter.java diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/CodegenController.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/CodegenController.java index 6889d03..033ce85 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/CodegenController.java +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/CodegenController.java @@ -25,11 +25,13 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; +import javax.annotation.security.PermitAll; import javax.servlet.http.HttpServletResponse; import javax.validation.Valid; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.sql.SQLException; import java.util.List; import java.util.Map; @@ -148,4 +150,21 @@ public class CodegenController { writeAttachment(response, "codegen.zip", outputStream.toByteArray()); } + /** + * 应用程序前后端代码生成,并解压到配置的路径 + * + * @param tableIdStr 表ID串 + */ + @Operation(summary = "应用程序前后端代码生成") + @Parameters({ + @Parameter(name = "tableIdStr", description = "表编号", required = true, example = "1024,1025"), + @Parameter(name = "appName", description = "应用名称", required = true, example = "testApp") + }) + @GetMapping("/genAppCode") + @PermitAll +// @PreAuthorize("@ss.hasPermission('infra:codegen:genappcode')") + public CommonResult genAppCode( String tableIdStr, String appName){ + return success(codegenService.genAppCode(tableIdStr, appName)); + } + } diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenService.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenService.java index 00e36af..f7f47eb 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenService.java +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenService.java @@ -98,4 +98,12 @@ public interface CodegenService { */ List getDatabaseTableList(Long dataSourceConfigId, String name, String comment); + /** + * 应用程序前后端代码生成,并解压到配置的路径 + * @param tableIdStr + * @param appName + * @return + */ + String genAppCode(String tableIdStr, String appName); + } diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenServiceImpl.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenServiceImpl.java index b086c35..74a0d52 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenServiceImpl.java +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenServiceImpl.java @@ -1,7 +1,12 @@ package cn.iocoder.yudao.module.infra.service.codegen; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.ZipUtil; +import cn.iocoder.yudao.framework.common.exception.ServiceException; 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; @@ -18,29 +23,51 @@ 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.db.DatabaseTableService; +import cn.iocoder.yudao.module.infra.util.DistributedLock; +import cn.iocoder.yudao.module.infra.util.InputStreamConverter; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import com.alibaba.excel.util.DateUtils; import com.baomidou.mybatisplus.generator.config.po.TableField; import com.baomidou.mybatisplus.generator.config.po.TableInfo; import com.google.common.annotations.VisibleForTesting; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; +import javax.sql.DataSource; +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; import java.util.*; +import java.util.concurrent.TimeUnit; import java.util.function.BiPredicate; import java.util.stream.Collectors; import java.util.stream.IntStream; +import java.util.zip.ZipInputStream; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.module.infra.framework.file.core.utils.FileTypeUtils.writeAttachment; /** * 代码生成 Service 实现类 * * @author 芋道源码 */ +@Slf4j @Service public class CodegenServiceImpl implements CodegenService { @@ -63,6 +90,31 @@ public class CodegenServiceImpl implements CodegenService { @Resource private CodegenProperties codegenProperties; + @Autowired + private DataSource dataSource; + + @Value("${runsystem.project.basedir.unzip}") + private String unzipPath; + + + @Value("${runsystem.project.basedir.menusql}") + private String menuSqlPath; + + @Value("${runsystem.project.basedir.shell}") + private String shellPath; + + @Value("${runsystem.project.basedir.config}") + private String configPath; + + @Value("${runsystem.project.basedir.nginx}") + private String nginxPath; + + @Autowired + private DistributedLock distributedLock; + + @Value("${runsystem.project.host}") + private String host; + @Override @Transactional(rollbackFor = Exception.class) public List createCodegenList(Long userId, CodegenCreateListReqVO reqVO) { @@ -293,4 +345,243 @@ public class CodegenServiceImpl implements CodegenService { return BeanUtils.toBean(tables, DatabaseTableRespVO.class); } + /** + * 应用程序前后端代码生成,并解压到配置的路径 + */ + @Override + public String genAppCode(String tableIdStr, String appName) { + appName = appName.toLowerCase().replaceAll("[^a-zA-Z0-9]", ""); + log.info("应用程序前后端代码生成开始,tableIdStr:{},appName:{}", tableIdStr, appName); + + String lockKey = "genAppCode:" + ":" + appName; + if (!distributedLock.lock(lockKey, 10, TimeUnit.MINUTES)) { + log.error("Failed to acquire lock for appName: {}", appName); + throw new ServiceException(500,appName + " Failed to acquire lock"); + } + Long[] tableIds = Convert.toLongArray(tableIdStr); + try { + + String appDir = "/" + appName + "-" + DateUtils.DATE_FORMAT_10; + String savePath = unzipPath + appDir; + String url = ""; + + Map codeResult=new HashMap(); + // 生成代码 + for (Long tableId : tableIds) { + Map codes = this.generationCodes(tableId); + codeResult.putAll(codes); + } + + // 构建 zip 包 + String[] paths = codeResult.keySet().toArray(new String[0]); + ByteArrayInputStream[] ins = codeResult.values().stream().map(IoUtil::toUtf8Stream).toArray(ByteArrayInputStream[]::new); + + // 解压ZIP文件内容到目标文件夹 + File targetFolder = new File(savePath); + if (!targetFolder.exists()) { + targetFolder.mkdirs(); + } else { + FileUtils.deleteDirectory(targetFolder); + } + + InputStream bais = InputStreamConverter.convertByteArrayInputStreamArrayToInputStream(ins); + ZipInputStream zis = new ZipInputStream(bais); + ZipUtil.unzip(zis, targetFolder); + + //运行系统-执行菜单文件夹中的SQL脚本 + Connection connection = dataSource.getConnection(); + String menuScriptsDir = savePath + menuSqlPath; + if (null != connection) { + executeMenuScripts(menuScriptsDir, savePath); + } + + //运行系统 + executeRunSystemShell(appName, savePath); + + //构建nginx配置文件 + url = buildNginxConf(appName, savePath); + + // 返回成功信息 + log.info("代码生成成功,已保存到: " + savePath, "运行系统url地址:" + url); + return StringUtils.isNotBlank(url) ? url : "应用容器启动失败"; + } catch (Exception e) { + log.error("Failed to generate app code: {}", e.getMessage()); + return "应用容器启动失败:" + e.getMessage(); + } finally { + distributedLock.unlock(lockKey); + log.info("Lock released for tableIdStr: {}, appName: {}", tableIdStr, appName); + //删除gen_table和gen_table_column中table_id相关的数据 + codegenTableMapper.deleteByIds(Arrays.asList(tableIds)); + } + } + + /** + * 执行菜单脚本并合并为一个文件 + */ + private static void executeMenuScripts(String menuScriptsDir, String targetDir) throws IOException { + File dir = new File(menuScriptsDir); + if (!dir.exists() || !dir.isDirectory()) { + throw new IllegalArgumentException("指定的目录不存在或不是一个文件夹: " + menuScriptsDir); + } + + File[] sqlFiles = dir.listFiles((dir1, name) -> name.toLowerCase().endsWith(".sql")); + if (sqlFiles == null || sqlFiles.length == 0) { + System.out.println("没有找到 SQL 脚本文件"); + return; + } + + Arrays.sort(sqlFiles); // 按文件名排序 + + StringBuilder combinedSql = new StringBuilder(); + + for (File sqlFile : sqlFiles) { + String sqlContent = readFile(sqlFile); + combinedSql.append(sqlContent).append(";\n"); + } + + // 将合并后的 SQL 内容写入目标文件 + String targetFilePath = targetDir + File.separator + "menu.sql"; + Path targetPath = Paths.get(targetFilePath); + Files.createDirectories(targetPath.getParent()); + Files.write(targetPath, combinedSql.toString().getBytes()); + + System.out.println("合并后的menu SQL脚本已保存到: " + targetFilePath); + } + + private static String readFile(File file) throws IOException { + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + return reader.lines().collect(Collectors.joining("\n")); + } + } + + /** + * 执行运行系统shell脚本 + * + * @param appName + * @return + */ + private String executeRunSystemShell(String appName, String savePath) { + String result = ""; + try { + // 组合命令和参数 +// String[] command = {"/bin/sh", "bash "+savePath + shellPath + "/runSystem.sh", appName}; + + + // 执行Shell脚本 + Process process = Runtime.getRuntime().exec("sh " + savePath + shellPath + "/runSystem.sh " + appName); + + // 读取脚本的输出 + + BufferedReader reader = new BufferedReader(new BufferedReader(new InputStreamReader(Runtime.getRuntime().exec("ls").getInputStream()))); + String line; + while ((line = reader.readLine()) != null) { + result += line; + } + + // 等待脚本执行完成 +// process.waitFor(); + int exitCode = process.waitFor(); + log.info("Script runSystem executed with exit code: " + exitCode); + +// Thread.sleep(15000); + + } catch (IOException | InterruptedException e) { + e.printStackTrace(); + } + return result; + } + + @SneakyThrows + private String buildNginxConf(String appName, String savePath) { + String url = ""; + // 读取nginx.conf文件内容并替换{port}和{host_ip} + // 读取文件内容 + StringBuilder contentBuilder = new StringBuilder(); + try (BufferedReader reader = new BufferedReader(new FileReader(savePath + configPath + "/nginx.conf"))) { + String line; + while ((line = reader.readLine()) != null) { + contentBuilder.append(line).append("\n"); + } + } + + + String hostIp = executeSelectIpShell(appName, savePath); + String port = getPortSequence().toString(); + + if (ObjectUtil.isAllNotEmpty(hostIp, port)) { + String content = contentBuilder.toString(); + content = content.replace("{port}", port); + content = content.replace("{host_ip}", hostIp); + + // 构建输出文件路径 + File outputDir = new File(nginxPath); + if (!outputDir.exists()) { + outputDir.mkdirs(); // 如果目录不存在,则创建目录 + } + File outputFile = new File(outputDir, appName + ".conf"); + + // 写入替换后的内容到新文件 + try (BufferedWriter writer = new BufferedWriter(new FileWriter(outputFile))) { + writer.write(content); + } + log.info("文件内容已成功替换并保存到: " + outputFile.getAbsolutePath()); + url = host + ":" + port; + } + // 替换 {port} 和 {host_ip} + + + Runtime.getRuntime().exec("nginx -s reload"); + + return url; + } + + /** + * 执行查找ip shell脚本 + * + * @param appName + * @return + */ + private String executeSelectIpShell(String appName, String savePath) { + String result = ""; + try { + // 执行Shell脚本 + Process process = Runtime.getRuntime().exec("sh " + savePath + shellPath + "/selectIp.sh " + appName); + + // 读取脚本的输出 + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); + String line; + while ((line = reader.readLine()) != null) { + result += line; + } + + // 等待脚本执行完成 + int exitCode = process.waitFor(); + log.info("Script selectIp executed with exit code: " + exitCode); + } catch (IOException | InterruptedException e) { + e.printStackTrace(); + } + return result; + } + + /** + * 获取端口号序列值 + * + * @return + * @throws SQLException + */ + private Long getPortSequence() throws SQLException { + Long port = 10000L; + Connection connection = dataSource.getConnection(); + CallableStatement callableStatement = connection.prepareCall("{call get_next_sequence()}"); + boolean hasResult = callableStatement.execute(); + if (hasResult) { + ResultSet resultSet = callableStatement.getResultSet(); + if (resultSet.next()) { + port = resultSet.getLong(1); + log.info("Next sequence value: " + port); + } + } + return port; + } + } diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/util/DistributedLock.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/util/DistributedLock.java new file mode 100644 index 0000000..8ac8037 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/util/DistributedLock.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.infra.util; + +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; +import javax.annotation.Resource; +import java.util.concurrent.TimeUnit; + +@Component +public class DistributedLock { + + @Resource + private StringRedisTemplate stringRedisTemplate; + + public boolean lock(String key, long timeout, TimeUnit unit) { + Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(key, "locked", timeout, unit); + return result != null && result; + } + + public void unlock(String key) { + stringRedisTemplate.delete(key); + } +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/util/InputStreamConverter.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/util/InputStreamConverter.java new file mode 100644 index 0000000..890c788 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/util/InputStreamConverter.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.infra.util; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.SequenceInputStream; +import java.util.Enumeration; +import java.util.Vector; + +public class InputStreamConverter { + public static InputStream convertByteArrayInputStreamArrayToInputStream(ByteArrayInputStream[] ins) { + Vector inputStreams = new Vector<>(); + for (ByteArrayInputStream byteArrayInputStream : ins) { + inputStreams.add(byteArrayInputStream); + } + return new SequenceInputStream(new Enumeration() { + private final Enumeration enumeration = inputStreams.elements(); + + @Override + public boolean hasMoreElements() { + return enumeration.hasMoreElements(); + } + + @Override + public InputStream nextElement() { + return enumeration.nextElement(); + } + }); + } +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/application.yaml b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/application.yaml index 51e84e4..54ed2d7 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/application.yaml +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/application.yaml @@ -179,3 +179,27 @@ yudao: - infra_data_source_config debug: false +#应用运行系统基线项目文件地址 +runsystem: + project: + host: 10.31.0.172 + basedir: + # 基线项目根目录 服务器路径:/root/RunSystemBaseLineProject + # source: D:/RunSystemBaseLineProject + source: /root/RunSystemBaseLineProject + # java源文件路径 + java: /app/icss-xm-app/ruoyi-modules/ruoyi-system/src/ + # web源文件路径 + web: /web/icss-xm-app-web/src/ + # 运行系统解压路径 服务器路径:/root/dockerTest + # unzip: D:/RunSystemSavePath + unzip: /root/dockerTest + # 菜单sql文件路径 + menusql: /app/icss-xm-app/ruoyi-modules/ruoyi-system/src/main/resources/menusql + # shell脚本路径 + shell: + # config文件路径 + config: /app/icss-xm-app/script/config + # nginx配置文件路径 服务器路径:/etc/nginx/conf.d + # nginx: D:/code/icss-xm-app/script/nginx + nginx: /etc/nginx/conf.d \ No newline at end of file