feat: 应用程序前后端代码生成
This commit is contained in:
parent
7d0c1c3994
commit
b62b7c75f5
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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
|
Loading…
Reference in New Issue