Compare commits

...

2 Commits

Author SHA1 Message Date
weike001 6bd0b2f0e0 feat(mybatis): 添加通用查询和分页功能
- 新增 AutoQueryUtil 工具类,实现自动查询功能
- 添加 BaseEntity基类,包含通用的创建和更新字段
- 修改 DefaultDBFieldHandler,支持 BaseEntity 的字段填充
- 新增 PageQuery 类,用于构建分页查询对象
- 添加 TableDataInfo 类,用于封装分页查询结果
- 在 ServletUtils 中增加 getHeader 方法,用于获取请求头信息
2025-02-20 15:01:49 +08:00
weike001 b62b7c75f5 feat: 应用程序前后端代码生成 2024-12-27 17:29:02 +08:00
13 changed files with 904 additions and 7 deletions

View File

@ -4,6 +4,7 @@ import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.servlet.ServletUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.MediaType;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
@ -13,6 +14,8 @@ import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.Map;
@ -59,6 +62,28 @@ public class ServletUtils {
return ua != null ? ua : "";
}
public static String getHeader(HttpServletRequest request, String name){
String value = request.getHeader(name);
if (StringUtils.isEmpty(value)) {
return StringUtils.EMPTY;
}
return urlDecode(value);
}
/**
* 内容解码
*
* @param str 内容
* @return 解码后的内容
*/
public static String urlDecode(String str) {
try {
return URLDecoder.decode(str,"UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
/**
* 获得请求
*

View File

@ -0,0 +1,71 @@
package cn.iocoder.yudao.framework.mybatis.core.dataobject;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* Entity基类
*
* @author Lion Li
*/
@Data
public class BaseEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 搜索值
*/
@JsonIgnore
@TableField(exist = false)
private String searchValue;
// /**
// * 创建部门
// */
// @TableField(fill = FieldFill.INSERT)
// private Long deptId;
/**
* 创建者
*/
@TableField(fill = FieldFill.INSERT)
private Long createBy;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
/**
* 更新者
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateBy;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
/**
* 请求参数
*/
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@TableField(exist = false)
private Map<String, Object> params = new HashMap<>();
}

View File

@ -1,6 +1,7 @@
package cn.iocoder.yudao.framework.mybatis.core.handler;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseEntity;
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
@ -41,6 +42,28 @@ public class DefaultDBFieldHandler implements MetaObjectHandler {
if (Objects.nonNull(userId) && Objects.isNull(baseDO.getUpdater())) {
baseDO.setUpdater(userId.toString());
}
}else if (Objects.nonNull(metaObject) && metaObject.getOriginalObject() instanceof BaseEntity) {
BaseEntity baseEntity = (BaseEntity) metaObject.getOriginalObject();
LocalDateTime current = LocalDateTime.now();
// 创建时间为空则以当前时间为插入时间
if (Objects.isNull(baseEntity.getCreateTime())) {
baseEntity.setCreateTime(current);
}
// 更新时间为空则以当前时间为更新时间
if (Objects.isNull(baseEntity.getUpdateTime())) {
baseEntity.setUpdateTime(current);
}
Long userId = WebFrameworkUtils.getLoginUserId();
// 当前登录用户不为空创建人为空则当前登录用户为创建人
if (Objects.nonNull(userId) && Objects.isNull(baseEntity.getCreateBy())) {
baseEntity.setCreateBy(userId);
}
// 当前登录用户不为空更新人为空则当前登录用户为更新人
if (Objects.nonNull(userId) && Objects.isNull(baseEntity.getUpdateBy())) {
baseEntity.setUpdateBy(userId);
}
}
}
@ -51,12 +74,20 @@ public class DefaultDBFieldHandler implements MetaObjectHandler {
if (Objects.isNull(modifyTime)) {
setFieldValByName("updateTime", LocalDateTime.now(), metaObject);
}
// 当前登录用户不为空更新人为空则当前登录用户为更新人
Object modifier = getFieldValByName("updater", metaObject);
Long userId = WebFrameworkUtils.getLoginUserId();
if (Objects.nonNull(userId) && Objects.isNull(modifier)) {
setFieldValByName("updater", userId.toString(), metaObject);
if (Objects.nonNull(metaObject) && metaObject.getOriginalObject() instanceof BaseDO) {
// 当前登录用户不为空更新人为空则当前登录用户为更新人
Object modifier = getFieldValByName("updater", metaObject);
Long userId = WebFrameworkUtils.getLoginUserId();
if (Objects.nonNull(userId) && Objects.isNull(modifier)) {
setFieldValByName("updater", userId.toString(), metaObject);
}
}else if (Objects.nonNull(metaObject) && metaObject.getOriginalObject() instanceof BaseEntity) {
// 当前登录用户不为空更新人为空则当前登录用户为更新人
Object modifier = getFieldValByName("updateBy", metaObject);
Long userId = WebFrameworkUtils.getLoginUserId();
if (Objects.nonNull(userId) && Objects.isNull(modifier)) {
setFieldValByName("updateBy", userId, metaObject);
}
}
}
}

View File

@ -0,0 +1,141 @@
package cn.iocoder.yudao.framework.mybatis.core.page;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* 分页查询实体类
*
* @author Lion Li
*/
@Data
public class PageQuery implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 分页大小
*/
private Integer pageSize;
/**
* 当前页数
*/
private Integer pageNum;
/**
* 排序列
*/
private String orderByColumn;
/**
* 排序的方向desc或者asc
*/
private String isAsc;
/**
* 当前记录起始索引 默认值
*/
public static final int DEFAULT_PAGE_NUM = 1;
/**
* 每页显示记录数 默认值 默认查全部
*/
public static final int DEFAULT_PAGE_SIZE = Integer.MAX_VALUE;
/**
* 仅支持字母数字下划线空格逗号小数点支持多个字段排序
*/
public static final String SQL_PATTERN = "[a-zA-Z0-9_\\ \\,\\.]+";
public static final String SEPARATOR = ",";
/**
* 构建分页对象
*/
public <T> Page<T> build() {
Integer pageNum = ObjectUtil.defaultIfNull(getPageNum(), DEFAULT_PAGE_NUM);
Integer pageSize = ObjectUtil.defaultIfNull(getPageSize(), DEFAULT_PAGE_SIZE);
if (pageNum <= 0) {
pageNum = DEFAULT_PAGE_NUM;
}
Page<T> page = new Page<>(pageNum, pageSize);
List<OrderItem> orderItems = buildOrderItem();
if (CollUtil.isNotEmpty(orderItems)) {
page.addOrder(orderItems);
}
return page;
}
/**
* 构建排序
*
* 支持的用法如下:
* {isAsc:"asc",orderByColumn:"id"} order by id asc
* {isAsc:"asc",orderByColumn:"id,createTime"} order by id asc,create_time asc
* {isAsc:"desc",orderByColumn:"id,createTime"} order by id desc,create_time desc
* {isAsc:"asc,desc",orderByColumn:"id,createTime"} order by id asc,create_time desc
*/
private List<OrderItem> buildOrderItem() {
if (StringUtils.isBlank(orderByColumn) || StringUtils.isBlank(isAsc)) {
return null;
}
String orderBy = escapeOrderBySql(orderByColumn);
orderBy = StrUtil.toUnderlineCase(orderBy);
// 兼容前端排序类型
isAsc = StringUtils.replaceEach(isAsc, new String[]{"ascending", "descending"}, new String[]{"asc", "desc"});
String[] orderByArr = orderBy.split(SEPARATOR);
String[] isAscArr = isAsc.split(SEPARATOR);
if (isAscArr.length != 1 && isAscArr.length != orderByArr.length) {
throw new ServiceException(500,"排序参数有误");
}
List<OrderItem> list = new ArrayList<>();
// 每个字段各自排序
for (int i = 0; i < orderByArr.length; i++) {
String orderByStr = orderByArr[i];
String isAscStr = isAscArr.length == 1 ? isAscArr[0] : isAscArr[i];
if ("asc".equals(isAscStr)) {
list.add(OrderItem.asc(orderByStr));
} else if ("desc".equals(isAscStr)) {
list.add(OrderItem.desc(orderByStr));
} else {
throw new ServiceException(500,"排序参数有误");
}
}
return list;
}
public Integer getFirstNum() {
return (pageNum - 1) * pageSize;
}
/**
* 检查字符防止注入绕过
*/
public static String escapeOrderBySql(String value) {
if (StringUtils.isNotEmpty(value) && !isValidOrderBySql(value)) {
throw new IllegalArgumentException("参数不符合规范,不能进行查询");
}
return value;
}
/**
* 验证 order by 语法是否符合规范
*/
public static boolean isValidOrderBySql(String value) {
return value.matches(SQL_PATTERN);
}
}

View File

@ -0,0 +1,70 @@
package cn.iocoder.yudao.framework.mybatis.core.page;
import com.baomidou.mybatisplus.core.metadata.IPage;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.List;
/**
* 表格分页数据对象
*
* @author Lion Li
*/
@Data
@NoArgsConstructor
public class TableDataInfo<T> implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 总记录数
*/
private long total;
/**
* 列表数据
*/
private List<T> list;
/**
* 分页
*
* @param list 列表数据
* @param total 总记录数
*/
public TableDataInfo(List<T> list, long total) {
this.list = list;
this.total = total;
}
/**
* 根据分页对象构建表格分页数据对象
*/
public static <T> TableDataInfo<T> build(IPage<T> page) {
TableDataInfo<T> rspData = new TableDataInfo<>();
rspData.setList(page.getRecords());
rspData.setTotal(page.getTotal());
return rspData;
}
/**
* 根据数据列表构建表格分页数据对象
*/
public static <T> TableDataInfo<T> build(List<T> list) {
TableDataInfo<T> rspData = new TableDataInfo<>();
rspData.setList(list);
rspData.setTotal(list.size());
return rspData;
}
/**
* 构建表格分页数据对象
*/
public static <T> TableDataInfo<T> build() {
TableDataInfo<T> rspData = new TableDataInfo<>();
return rspData;
}
}

View File

@ -0,0 +1,166 @@
package cn.iocoder.yudao.framework.mybatis.core.util;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.MethodUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.time.*;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class AutoQueryUtil {
private static final Logger log = LoggerFactory.getLogger(AutoQueryUtil.class);
/**
* 根据实体类的属性值是否为空自动创建LambdaQueryWrapper
*
* @param entity 实体对象
* @param <T> 实体类型
* @return LambdaQueryWrapper
*/
public static <T> QueryWrapper<T> autoQuery(T entity) {
QueryWrapper<T> queryWrapper = new QueryWrapper<>();
if (entity == null) {
return queryWrapper;
}
Class<?> clazz = entity.getClass();
// 遍历当前类和所有父类的字段
while (clazz != null) {
Map<String, Method> getterMethods = new HashMap<>();
for (Field field : clazz.getDeclaredFields()) {
field.setAccessible(true);
String fieldName = field.getName();
Class<?> finalClazz = clazz;
Method getterMethod = getterMethods.computeIfAbsent(
"get" + capitalize(fieldName),
k -> {
try {
return MethodUtils.getAccessibleMethod(finalClazz, k, (Class<?>[]) null);
} catch (Exception e) {
log.error("No such getter method: {}", k, e);
return null;
}
}
);
if (getterMethod != null) {
try {
Object value = getterMethod.invoke(entity);
if (value != null && !"".equals(value)) {
handleFieldValue(queryWrapper, field, value);
}
} catch (Exception e) {
log.error("Error invoking getter method: {}", getterMethod.getName(), e);
}
}
}
// 获取父类继续遍历父类的字段
clazz = clazz.getSuperclass();
}
return queryWrapper;
}
public static String getTenantId(){
HttpServletRequest request = ServletUtils.getRequest();
if (null != request) {
// 获取request的header中的x-tenant-id
String tenantId = ServletUtils.getHeader(request, "x-tenant-id");
if (StringUtils.isNotBlank(tenantId)) {
return tenantId;
}
}
return null;
}
private static void handleFieldValue(QueryWrapper<?> queryWrapper, Field field, Object value) {
String fieldName = field.getName();
if (String.class.isAssignableFrom(field.getType())) {
queryWrapper.like(StrUtil.toUnderlineCase(fieldName), value);
} else if (Integer.class.isAssignableFrom(field.getType()) ||
Long.class.isAssignableFrom(field.getType()) ||
Double.class.isAssignableFrom(field.getType()) ||
Float.class.isAssignableFrom(field.getType()) ||
Boolean.class.isAssignableFrom(field.getType()) ||
Short.class.isAssignableFrom(field.getType()) ||
Byte.class.isAssignableFrom(field.getType()) ||
Character.class.isAssignableFrom(field.getType()) ||
Date.class.isAssignableFrom(field.getType()) ||
LocalDateTime.class.isAssignableFrom(field.getType()) ||
LocalDate.class.isAssignableFrom(field.getType()) ||
LocalTime.class.isAssignableFrom(field.getType()) ||
ZonedDateTime.class.isAssignableFrom(field.getType()) ||
Instant.class.isAssignableFrom(field.getType())) {
queryWrapper.eq(StrUtil.toUnderlineCase(fieldName), String.valueOf(value));
} else {
// 如果是其他类型可以添加更多条件或者忽略
log.warn("Unsupported field type: {}", field.getType().getName());
}
}
/**
* 获取 pageReqVO 中所有 LocalDate[]LocalDateTime[] Date[] 类型字段的名称
*/
public static String[] getDateRangeFieldNames(Object pageReqVO) {
return Arrays.stream(pageReqVO.getClass().getDeclaredFields())
.filter(field -> field.getType().equals(LocalDate[].class)
|| field.getType().equals(LocalDateTime[].class)
|| field.getType().equals(Date[].class))
.map(Field::getName)
.toArray(String[]::new);
}
/**
* 动态处理 pageReqVO 中所有 LocalDate[]LocalDateTime[] Date[] 类型字段设置时间段查询条件
*/
public static void handleDateRangeFields(Object pageReqVO, QueryWrapper<?> queryWrapper) {
for (Field field : pageReqVO.getClass().getDeclaredFields()) {
field.setAccessible(true);
try {
Object fieldValue = field.get(pageReqVO);
if (fieldValue == null) {
continue; // 跳过值为 null 的字段
}
if (field.getType().equals(LocalDate[].class)) {
LocalDate[] dateRange = (LocalDate[]) fieldValue;
if (dateRange.length == 2) {
String columnName = StrUtil.toUnderlineCase(field.getName()); // 字段名转换为数据库列名
queryWrapper.between(columnName, dateRange[0], dateRange[1]);
}
} else if (field.getType().equals(LocalDateTime[].class)) {
LocalDateTime[] dateTimeRange = (LocalDateTime[]) fieldValue;
if (dateTimeRange.length == 2) {
String columnName = StrUtil.toUnderlineCase(field.getName()); // 字段名转换为数据库列名
queryWrapper.between(columnName, dateTimeRange[0], dateTimeRange[1]);
}
} else if (field.getType().equals(Date[].class)) {
Date[] range = (Date[]) fieldValue;
if (range.length == 2) {
String columnName = StrUtil.toUnderlineCase(field.getName()); // 字段名转换为数据库列名
queryWrapper.between(columnName, range[0], range[1]);
}
}
} catch (IllegalAccessException e) {
throw new ServiceException(500,"Failed to access field " + field.getName() + ": " + e.getMessage());
}
}
}
private static String capitalize(String str) {
return str.substring(0, 1).toUpperCase() + str.substring(1);
}
}

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

View File

@ -163,7 +163,7 @@ yudao:
description: 提供管理员管理的所有功能
version: ${yudao.info.version}
tenant: # 多租户相关配置项
enable: true
enable: false
ignore-urls:
- /admin-api/system/tenant/get-id-by-name # 基于名字获取租户,不许带租户编号
- /admin-api/system/tenant/get-by-website # 基于域名获取租户,不许带租户编号