feat: 应用程序前后端代码生成

This commit is contained in:
weike001 2024-12-27 17:29:02 +08:00
parent 7d0c1c3994
commit b62b7c75f5
6 changed files with 393 additions and 0 deletions

View File

@ -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<String> genAppCode( String tableIdStr, String appName){
return success(codegenService.genAppCode(tableIdStr, appName));
}
}

View File

@ -98,4 +98,12 @@ public interface CodegenService {
*/
List<DatabaseTableRespVO> getDatabaseTableList(Long dataSourceConfigId, String name, String comment);
/**
* 应用程序前后端代码生成并解压到配置的路径
* @param tableIdStr
* @param appName
* @return
*/
String genAppCode(String tableIdStr, String appName);
}

View File

@ -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<Long> 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<String,String> codeResult=new HashMap<String, String>();
// 生成代码
for (Long tableId : tableIds) {
Map<String, String> 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;
}
}

View File

@ -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);
}
}

View File

@ -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<InputStream> inputStreams = new Vector<>();
for (ByteArrayInputStream byteArrayInputStream : ins) {
inputStreams.add(byteArrayInputStream);
}
return new SequenceInputStream(new Enumeration<InputStream>() {
private final Enumeration<InputStream> enumeration = inputStreams.elements();
@Override
public boolean hasMoreElements() {
return enumeration.hasMoreElements();
}
@Override
public InputStream nextElement() {
return enumeration.nextElement();
}
});
}
}

View File

@ -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