diff --git a/backend/pom.xml b/backend/pom.xml
index 16c963ab..e68b5775 100644
--- a/backend/pom.xml
+++ b/backend/pom.xml
@@ -235,6 +235,12 @@
json-schema-validator
1.0.86
+
+
+ org.freemarker
+ freemarker
+ 2.3.32
+
diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/entity/ProjectGroup.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/entity/ProjectGroup.java
index f94e190c..af3e9c36 100644
--- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/entity/ProjectGroup.java
+++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/entity/ProjectGroup.java
@@ -1,20 +1,11 @@
package com.qqchen.deploy.backend.deploy.entity;
-import com.fasterxml.jackson.annotation.JsonManagedReference;
import com.qqchen.deploy.backend.deploy.enums.ProjectGroupTypeEnum;
+import com.qqchen.deploy.backend.framework.annotation.LogicDelete;
import com.qqchen.deploy.backend.framework.domain.Entity;
-import com.qqchen.deploy.backend.system.entity.User;
import jakarta.persistence.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
-import org.hibernate.annotations.FetchMode;
-import org.hibernate.annotations.SQLDelete;
-import org.hibernate.annotations.Where;
-
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
/**
* 项目组实体
@@ -25,6 +16,7 @@ import java.util.Set;
@Table(name = "deploy_project_group")
//@SQLDelete(sql = "UPDATE deploy_project_group SET deleted = TRUE WHERE id = ?; DELETE FROM deploy_project_group_environment WHERE project_group_id = ?")
//@Where(clause = "deleted = false")
+@LogicDelete
public class ProjectGroup extends Entity {
/**
diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/generator/CodeGenerator.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/generator/CodeGenerator.java
new file mode 100644
index 00000000..9fc2430c
--- /dev/null
+++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/generator/CodeGenerator.java
@@ -0,0 +1,106 @@
+package com.qqchen.deploy.backend.framework.generator;
+
+import com.qqchen.deploy.backend.framework.utils.CodeGeneratorUtils;
+import com.qqchen.deploy.backend.framework.utils.CodeGeneratorUtils.GeneratorConfig;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Scanner;
+
+/**
+ * 代码生成器主入口
+ */
+@Slf4j
+public class CodeGenerator {
+
+ public static void main(String[] args) {
+ Scanner scanner = new Scanner(System.in);
+
+ try {
+ // 1. 输入模块信息
+ System.out.println("请输入模块名称(中文,如:用户):");
+ String moduleName = scanner.nextLine();
+
+ System.out.println("请输入类名(英文,如:User):");
+ String className = scanner.nextLine();
+
+ System.out.println("请输入基础包路径(如:com.qqchen.deploy.backend.system):");
+ String basePackage = scanner.nextLine();
+
+ System.out.println("请输入输出路径(如:src/main/java):");
+ String outputPath = scanner.nextLine();
+
+ System.out.println("请输入表名(如:sys_user):");
+ String tableName = scanner.nextLine();
+
+ System.out.println("请输入URL路径(如:user):");
+ String urlPath = scanner.nextLine();
+
+ // 2. 输入建表SQL
+ System.out.println("请输入CREATE TABLE SQL语句(以分号结束):");
+ StringBuilder sqlBuilder = new StringBuilder();
+ String line;
+ while (!(line = scanner.nextLine()).contains(";")) {
+ sqlBuilder.append(line).append("\n");
+ }
+ sqlBuilder.append(line);
+ String createTableSql = sqlBuilder.toString();
+
+ // 3. 验证输入
+ if (StringUtils.isAnyBlank(moduleName, className, basePackage, outputPath, tableName, urlPath, createTableSql)) {
+ throw new IllegalArgumentException("所有输入项都不能为空");
+ }
+
+ // 4. 配置生成器
+ GeneratorConfig config = new GeneratorConfig();
+ config.setModuleName(moduleName);
+ config.setClassName(className);
+ config.setBasePackage(basePackage);
+ config.setOutputPath(outputPath);
+ config.setTableName(tableName);
+ config.setUrlPath(urlPath);
+
+ // 5. 生成代码
+ CodeGeneratorUtils.generateCode(createTableSql, config);
+
+ System.out.println("代码生成成功!");
+ System.out.println("生成的文件路径:" + outputPath);
+
+ } catch (Exception e) {
+ log.error("代码生成失败", e);
+ System.err.println("代码生成失败:" + e.getMessage());
+ } finally {
+ scanner.close();
+ }
+ }
+
+ /**
+ * 通过API方式生成代码
+ */
+ public static void generate(String moduleName, String className, String basePackage,
+ String outputPath, String tableName, String urlPath, String createTableSql) {
+ try {
+ // 1. 验证输入
+ if (StringUtils.isAnyBlank(moduleName, className, basePackage, outputPath, tableName, urlPath, createTableSql)) {
+ throw new IllegalArgumentException("所有输入项都不能为空");
+ }
+
+ // 2. 配置生成器
+ GeneratorConfig config = new GeneratorConfig();
+ config.setModuleName(moduleName);
+ config.setClassName(className);
+ config.setBasePackage(basePackage);
+ config.setOutputPath(outputPath);
+ config.setTableName(tableName);
+ config.setUrlPath(urlPath);
+
+ // 3. 生成代码
+ CodeGeneratorUtils.generateCode(createTableSql, config);
+
+ log.info("代码生成成功!输出路径:{}", outputPath);
+ } catch (Exception e) {
+ log.error("代码生成失败", e);
+ throw new RuntimeException("代码生成失败:" + e.getMessage(), e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/utils/CodeGeneratorUtils.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/utils/CodeGeneratorUtils.java
new file mode 100644
index 00000000..48afe2c1
--- /dev/null
+++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/utils/CodeGeneratorUtils.java
@@ -0,0 +1,257 @@
+package com.qqchen.deploy.backend.framework.utils;
+
+import com.qqchen.deploy.backend.framework.generator.CodeGenerator;
+import freemarker.template.Configuration;
+import freemarker.template.Template;
+import freemarker.template.TemplateException;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * 代码生成器工具类
+ */
+@Slf4j
+public class CodeGeneratorUtils {
+
+ @Data
+ public static class FieldInfo {
+ private String fieldName; // 字段名
+
+ private String fieldType; // 字段类型
+
+ private String columnName; // 数据库列名
+
+ private String comment; // 注释
+
+ private boolean nullable; // 是否可空
+
+ private boolean isPrimaryKey; // 是否主键
+ }
+
+ @Data
+ public static class GeneratorConfig {
+ private String moduleName; // 模块名称(中文)
+
+ private String className; // 类名
+
+ private String basePackage; // 基础包路径
+
+ private String outputPath; // 输出路径
+
+ private String tableName; // 表名
+
+ private String urlPath; // URL路径
+
+ private List fields; // 字段信息
+ }
+
+ private static final Configuration configuration;
+
+ static {
+ configuration = new Configuration(Configuration.VERSION_2_3_32);
+ configuration.setClassLoaderForTemplateLoading(CodeGeneratorUtils.class.getClassLoader(), "templates");
+ configuration.setDefaultEncoding("UTF-8");
+ }
+
+ /**
+ * 生成所有代码文件
+ *
+ * @param createTableSql CREATE TABLE SQL语句
+ * @param config 生成器配置
+ */
+ public static void generateCode(String createTableSql, GeneratorConfig config) {
+ try {
+ // 解析SQL获取字段信息
+ config.setFields(parseSqlFields(createTableSql));
+
+ // 生成各种类型的文件
+ generateFile("entity.ftl", config, "/entity/" + config.getClassName() + ".java");
+ generateFile("dto.ftl", config, "/dto/" + config.getClassName() + "DTO.java");
+ generateFile("query.ftl", config, "/query/" + config.getClassName() + "Query.java");
+ generateFile("repository.ftl", config, "/repository/I" + config.getClassName() + "Repository.java");
+ generateFile("service.ftl", config, "/service/I" + config.getClassName() + "Service.java");
+ generateFile("serviceImpl.ftl", config, "/service/impl/" + config.getClassName() + "ServiceImpl.java");
+ generateFile("controller.ftl", config, "/controller/" + config.getClassName() + "ApiController.java");
+ generateFile("converter.ftl", config, "/converter/" + config.getClassName() + "Converter.java");
+
+ log.info("代码生成完成,输出路径: {}", config.getOutputPath());
+ } catch (Exception e) {
+ log.error("代码生成失败", e);
+ throw new RuntimeException("代码生成失败", e);
+ }
+ }
+
+ /**
+ * 解析CREATE TABLE SQL获取字段信息
+ */
+ private static List parseSqlFields(String createTableSql) {
+ List fields = new ArrayList<>();
+ // 移除多余的空白字符和换行
+ createTableSql = createTableSql.replaceAll("\\s+", " ").trim();
+
+ // 提取表定义部分
+ int startIndex = createTableSql.indexOf("(");
+ int endIndex = createTableSql.lastIndexOf(")");
+ if (startIndex == -1 || endIndex == -1) {
+ throw new IllegalArgumentException("无效的CREATE TABLE语句");
+ }
+
+ // 获取字段定义部分
+ String fieldDefinitions = createTableSql.substring(startIndex + 1, endIndex);
+
+ // 按逗号分割字段定义
+ String[] fieldDefs = fieldDefinitions.split(",");
+
+ for (String fieldDef : fieldDefs) {
+ fieldDef = fieldDef.trim();
+
+ // 跳过主键定义等非字段定义行
+ if (fieldDef.toLowerCase().startsWith("primary key") ||
+ fieldDef.toLowerCase().startsWith("key") ||
+ fieldDef.toLowerCase().startsWith("constraint") ||
+ fieldDef.toLowerCase().startsWith("index") ||
+ fieldDef.isEmpty()) {
+ continue;
+ }
+
+ // 解析字段定义
+ String[] parts = fieldDef.split("\\s+", 3);
+ if (parts.length < 2) {
+ continue;
+ }
+
+ FieldInfo field = new FieldInfo();
+
+ // 设置字段名(移除反引号)
+ field.setColumnName(parts[0].replace("`", ""));
+
+ // 设置字段类型
+ String typeStr = parts[1].toUpperCase();
+ if (typeStr.contains("(")) {
+ typeStr = typeStr.substring(0, typeStr.indexOf("("));
+ }
+ field.setFieldType(sqlTypeToJavaType(typeStr));
+
+ // 设置字段名(驼峰命名)
+ field.setFieldName(columnToField(field.getColumnName()));
+
+ // 设置是否可空
+ field.setNullable(!fieldDef.toLowerCase().contains("not null"));
+
+ // 设置注释
+ int commentIndex = fieldDef.toLowerCase().indexOf("comment");
+ if (commentIndex != -1) {
+ String comment = fieldDef.substring(commentIndex + 7).trim();
+ // 提取引号中的注释内容
+ if (comment.startsWith("'") && comment.endsWith("'")) {
+ field.setComment(comment.substring(1, comment.length() - 1));
+ }
+ }
+
+ // 设置是否为主键
+ field.setPrimaryKey(fieldDef.toLowerCase().contains("primary key"));
+
+ // 基础字段不需要生成
+ if (!isBaseField(field.getColumnName())) {
+ fields.add(field);
+ }
+ }
+
+ return fields;
+ }
+
+ /**
+ * 判断是否为基础字段(这些字段由父类Entity提供)
+ */
+ private static boolean isBaseField(String columnName) {
+ return columnName.equals("id") ||
+ columnName.equals("create_by") ||
+ columnName.equals("create_time") ||
+ columnName.equals("update_by") ||
+ columnName.equals("update_time") ||
+ columnName.equals("version") ||
+ columnName.equals("deleted");
+ }
+
+ /**
+ * 生成单个文件
+ */
+ private static void generateFile(String templateName, GeneratorConfig config, String subPath) throws IOException, TemplateException {
+ Template template = configuration.getTemplate(templateName);
+ String outputPath = config.getOutputPath() + subPath;
+ File outputFile = new File(outputPath);
+
+ // 确保目录存在
+ outputFile.getParentFile().mkdirs();
+
+ // 准备数据模型
+ Map dataModel = new HashMap<>();
+ dataModel.put("basePackage", config.getBasePackage());
+ dataModel.put("moduleName", config.getModuleName());
+ dataModel.put("className", config.getClassName());
+ dataModel.put("tableName", config.getTableName());
+ dataModel.put("fields", config.getFields());
+ dataModel.put("urlPath", config.getUrlPath());
+
+ // 生成文件
+ try (Writer writer = new FileWriter(outputFile)) {
+ template.process(dataModel, writer);
+ }
+
+ log.info("生成文件: {}", outputPath);
+ }
+
+ /**
+ * 数据库列名转Java字段名
+ */
+ private static String columnToField(String columnName) {
+ String[] parts = columnName.toLowerCase().split("_");
+ StringBuilder result = new StringBuilder(parts[0]);
+ for (int i = 1; i < parts.length; i++) {
+ result.append(StringUtils.capitalize(parts[i]));
+ }
+ return result.toString();
+ }
+
+ /**
+ * SQL类型转Java类型
+ */
+ private static String sqlTypeToJavaType(String sqlType) {
+ sqlType = sqlType.toUpperCase();
+ switch (sqlType) {
+ case "VARCHAR":
+ case "CHAR":
+ case "TEXT":
+ return "String";
+ case "INT":
+ case "INTEGER":
+ return "Integer";
+ case "BIGINT":
+ return "Long";
+ case "DECIMAL":
+ return "BigDecimal";
+ case "DATETIME":
+ case "TIMESTAMP":
+ return "LocalDateTime";
+ case "DATE":
+ return "LocalDate";
+ case "BIT":
+ case "TINYINT":
+ return "Boolean";
+ default:
+ return "String";
+ }
+ }
+}
\ No newline at end of file
diff --git a/backend/src/main/resources/db/migration/V1.0.1__init_data.sql b/backend/src/main/resources/db/migration/V1.0.1__init_data.sql
index 943da9d0..b95943ba 100644
--- a/backend/src/main/resources/db/migration/V1.0.1__init_data.sql
+++ b/backend/src/main/resources/db/migration/V1.0.1__init_data.sql
@@ -79,9 +79,11 @@ VALUES
(203, '环境管理', '/deploy/environments', '/src/pages/Deploy/Environment/List/index', 'CloudOutlined', 2, 200, 3, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
-(204, '部署配置管理', '/deploy/deployment', '/src/pages/Deploy/Deployment/List/index', 'CloudOutlined', 2, 200, 3, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
+(204, '部署配置管理', '/deploy/deployment', '/src/pages/Deploy/Deployment/List/index', 'CloudOutlined', 2, 200, 4, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
+
+(205, 'Jenkins管理', '/deploy/jenkins', '/src/pages/Deploy/Jenkins/List/index', 'CloudOutlined', 2, 200, 5, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE),
-- 三方系统
-(205, '三方系统管理', '/deploy/external', '/src/pages/Deploy/external/index', 'ApiOutlined', 2, 200, 70, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE);
+(206, '三方系统管理', '/deploy/external', '/src/pages/Deploy/external/index', 'ApiOutlined', 2, 200, 6, FALSE, TRUE, 'system', '2024-01-01 00:00:00', 0, FALSE);
-- 初始化角色数据
INSERT INTO sys_role (id, create_time, code, name, type, description, sort)
diff --git a/backend/src/main/resources/templates/controller.ftl b/backend/src/main/resources/templates/controller.ftl
new file mode 100644
index 00000000..8145ad37
--- /dev/null
+++ b/backend/src/main/resources/templates/controller.ftl
@@ -0,0 +1,28 @@
+package ${basePackage}.controller;
+
+import com.qqchen.deploy.backend.framework.controller.BaseController;
+import ${basePackage}.entity.${className};
+import ${basePackage}.dto.${className}DTO;
+import ${basePackage}.query.${className}Query;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import jakarta.servlet.http.HttpServletResponse;
+import java.util.List;
+
+/**
+ * ${moduleName} Controller
+ */
+@Slf4j
+@RestController
+@RequestMapping("/api/v1/${urlPath}")
+@Tag(name = "${moduleName}管理", description = "${moduleName}管理相关接口")
+public class ${className}ApiController extends BaseController<${className}, ${className}DTO, Long, ${className}Query> {
+
+ @Override
+ protected void exportData(HttpServletResponse response, List<${className}DTO> data) {
+ // TODO: 实现导出逻辑
+ log.info("导出${moduleName}数据,数据量:{}", data.size());
+ }
+}
\ No newline at end of file
diff --git a/backend/src/main/resources/templates/converter.ftl b/backend/src/main/resources/templates/converter.ftl
new file mode 100644
index 00000000..6714457b
--- /dev/null
+++ b/backend/src/main/resources/templates/converter.ftl
@@ -0,0 +1,13 @@
+package ${basePackage}.converter;
+
+import com.qqchen.deploy.backend.framework.converter.BaseConverter;
+import ${basePackage}.entity.${className};
+import ${basePackage}.dto.${className}DTO;
+import org.mapstruct.Mapper;
+
+/**
+ * ${moduleName} Converter
+ */
+@Mapper(config = BaseConverter.class)
+public interface ${className}Converter extends BaseConverter<${className}, ${className}DTO> {
+}
\ No newline at end of file
diff --git a/backend/src/main/resources/templates/dto.ftl b/backend/src/main/resources/templates/dto.ftl
new file mode 100644
index 00000000..8de6489b
--- /dev/null
+++ b/backend/src/main/resources/templates/dto.ftl
@@ -0,0 +1,24 @@
+package ${basePackage}.dto;
+
+import com.qqchen.deploy.backend.framework.dto.BaseDTO;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import java.time.LocalDateTime;
+
+/**
+ * ${moduleName} DTO
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ${className}DTO extends BaseDTO {
+
+<#list fields as field>
+ <#if field.comment??>
+ /**
+ * ${field.comment}
+ */
+ #if>
+ private ${field.fieldType} ${field.fieldName};
+
+#list>
+}
\ No newline at end of file
diff --git a/backend/src/main/resources/templates/entity.ftl b/backend/src/main/resources/templates/entity.ftl
new file mode 100644
index 00000000..6768128a
--- /dev/null
+++ b/backend/src/main/resources/templates/entity.ftl
@@ -0,0 +1,28 @@
+package ${basePackage}.entity;
+
+import com.qqchen.deploy.backend.framework.domain.Entity;
+import jakarta.persistence.*;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import java.time.LocalDateTime;
+
+/**
+ * ${moduleName}实体
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@jakarta.persistence.Entity
+@Table(name = "${tableName}")
+public class ${className} extends Entity {
+
+<#list fields as field>
+ <#if field.comment??>
+ /**
+ * ${field.comment}
+ */
+ #if>
+ @Column(name = "${field.columnName}"<#if !field.nullable>, nullable = false#if>)
+ private ${field.fieldType} ${field.fieldName};
+
+#list>
+}
\ No newline at end of file
diff --git a/backend/src/main/resources/templates/query.ftl b/backend/src/main/resources/templates/query.ftl
new file mode 100644
index 00000000..81ba09dc
--- /dev/null
+++ b/backend/src/main/resources/templates/query.ftl
@@ -0,0 +1,31 @@
+package ${basePackage}.query;
+
+import com.qqchen.deploy.backend.framework.annotation.QueryField;
+import com.qqchen.deploy.backend.framework.enums.QueryType;
+import com.qqchen.deploy.backend.framework.query.BaseQuery;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import java.time.LocalDateTime;
+
+/**
+ * ${moduleName}查询对象
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ${className}Query extends BaseQuery {
+
+<#list fields as field>
+ <#if field.comment??>
+ /**
+ * ${field.comment}
+ */
+ #if>
+ <#if field.fieldType == "String">
+ @QueryField(field = "${field.columnName}", type = QueryType.LIKE)
+ <#else>
+ @QueryField(field = "${field.columnName}")
+ #if>
+ private ${field.fieldType} ${field.fieldName};
+
+#list>
+}
\ No newline at end of file
diff --git a/backend/src/main/resources/templates/repository.ftl b/backend/src/main/resources/templates/repository.ftl
new file mode 100644
index 00000000..468c5991
--- /dev/null
+++ b/backend/src/main/resources/templates/repository.ftl
@@ -0,0 +1,12 @@
+package ${basePackage}.repository;
+
+import com.qqchen.deploy.backend.framework.repository.IBaseRepository;
+import ${basePackage}.entity.${className};
+import org.springframework.stereotype.Repository;
+
+/**
+ * ${moduleName} Repository
+ */
+@Repository
+public interface I${className}Repository extends IBaseRepository<${className}, Long> {
+}
\ No newline at end of file
diff --git a/backend/src/main/resources/templates/service.ftl b/backend/src/main/resources/templates/service.ftl
new file mode 100644
index 00000000..96a7ae8a
--- /dev/null
+++ b/backend/src/main/resources/templates/service.ftl
@@ -0,0 +1,12 @@
+package ${basePackage}.service;
+
+import com.qqchen.deploy.backend.framework.service.IBaseService;
+import ${basePackage}.entity.${className};
+import ${basePackage}.dto.${className}DTO;
+import ${basePackage}.query.${className}Query;
+
+/**
+ * ${moduleName} Service接口
+ */
+public interface I${className}Service extends IBaseService<${className}, ${className}DTO, Long, ${className}Query> {
+}
\ No newline at end of file
diff --git a/backend/src/main/resources/templates/serviceImpl.ftl b/backend/src/main/resources/templates/serviceImpl.ftl
new file mode 100644
index 00000000..ccdc2ce7
--- /dev/null
+++ b/backend/src/main/resources/templates/serviceImpl.ftl
@@ -0,0 +1,18 @@
+package ${basePackage}.service.impl;
+
+import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl;
+import ${basePackage}.entity.${className};
+import ${basePackage}.dto.${className}DTO;
+import ${basePackage}.query.${className}Query;
+import ${basePackage}.service.I${className}Service;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+/**
+ * ${moduleName} Service实现
+ */
+@Slf4j
+@Service
+public class ${className}ServiceImpl extends BaseServiceImpl<${className}, ${className}DTO, Long, ${className}Query>
+ implements I${className}Service {
+}
\ No newline at end of file