Browse Source

Add 添加生成新项目功能

YueYunyun 1 year ago
parent
commit
96338e08f4

+ 1 - 1
SERVER/VberAdminPlusV3/pom.xml

@@ -9,7 +9,7 @@
     <version>${revision}</version>
     <version>${revision}</version>
     <packaging>pom</packaging>
     <packaging>pom</packaging>
     <name>${project.artifactId}</name>
     <name>${project.artifactId}</name>
-    <description>VberAdminPlus后台管理系统</description>
+    <description>VAP后台管理系统</description>
     <modules>
     <modules>
         <module>vber-common</module>
         <module>vber-common</module>
         <module>vber-modules</module>
         <module>vber-modules</module>

+ 0 - 1
SERVER/VberAdminPlusV3/vber-admin/Dockerfile

@@ -1,4 +1,3 @@
-#FROM findepi/graalvm:java17-native
 FROM openjdk:17.0.2-oraclelinux8
 FROM openjdk:17.0.2-oraclelinux8
 
 
 MAINTAINER IWB
 MAINTAINER IWB

+ 1 - 1
SERVER/VberAdminPlusV3/vber-admin/src/main/java/com/vber/web/controller/AuthController.java

@@ -100,7 +100,7 @@ public class AuthController {
 
 
         Long userId = LoginHelper.getUserId();
         Long userId = LoginHelper.getUserId();
         scheduledExecutorService.schedule(() -> {
         scheduledExecutorService.schedule(() -> {
-            WebSocketUtils.sendMessage(userId, "欢迎登录VberAdminPlus后台管理系统");
+            WebSocketUtils.sendMessage(userId, "欢迎登录VAP后台管理系统");
         }, 3, TimeUnit.SECONDS);
         }, 3, TimeUnit.SECONDS);
         return R.ok(loginVo);
         return R.ok(loginVo);
     }
     }

+ 15 - 0
SERVER/VberAdminPlusV3/vber-common/vber-common-core/src/main/java/com/vber/common/core/utils/SetUtils.java

@@ -0,0 +1,15 @@
+package com.vber.common.core.utils;
+
+import cn.hutool.core.collection.CollUtil;
+
+import java.util.Set;
+
+/**
+ * @author Yue
+ */
+public class SetUtils {
+    @SafeVarargs
+    public static <T> Set<T> asSet(T... objs) {
+        return CollUtil.newHashSet(objs);
+    }
+}

+ 10 - 0
SERVER/VberAdminPlusV3/vber-modules/vber-generator/pom.xml

@@ -45,6 +45,16 @@
             <groupId>org.apache.velocity</groupId>
             <groupId>org.apache.velocity</groupId>
             <artifactId>velocity-engine-core</artifactId>
             <artifactId>velocity-engine-core</artifactId>
         </dependency>
         </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
     </dependencies>
 
 
 
 

+ 35 - 0
SERVER/VberAdminPlusV3/vber-modules/vber-generator/src/main/java/com/vber/reactor/controller/ProjectReactorController.java

@@ -0,0 +1,35 @@
+package com.vber.reactor.controller;
+
+import com.vber.common.core.domain.R;
+import com.vber.common.web.core.BaseController;
+import com.vber.reactor.core.ProjectReactor;
+import com.vber.reactor.domain.ProjectInfo;
+import lombok.RequiredArgsConstructor;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * @author Yue
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/tool/project")
+public class ProjectReactorController extends BaseController {
+
+    /**
+     * 生成新项目
+     */
+    @PostMapping("/reactor")
+    public R<Void> projectReactor(@Validated @RequestBody ProjectInfo projectInfo) {
+        try {
+            ProjectReactor.build(projectInfo);
+        } catch (Exception e) {
+            return R.fail(e.getMessage());
+        }
+        return R.ok();
+    }
+}

+ 217 - 0
SERVER/VberAdminPlusV3/vber-modules/vber-generator/src/main/java/com/vber/reactor/core/ProjectReactor.java

@@ -0,0 +1,217 @@
+package com.vber.reactor.core;
+
+import cn.hutool.core.io.FileTypeUtil;
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.util.StrUtil;
+import com.vber.common.core.utils.SetUtils;
+import com.vber.reactor.domain.ProjectInfo;
+import jakarta.validation.constraints.NotNull;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.util.Collection;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.stream.Collectors;
+
+import static java.io.File.separator;
+
+/**
+ * @author Yue
+ */
+@Slf4j
+public class ProjectReactor {
+    private static final String GROUP_ID = "com.vap";
+    private static final String ARTIFACT_ID = "VberAdminPlus";
+    private static final String VERSION = "V3";
+    private static final String ARTIFACT_ID_VERSION = ARTIFACT_ID + VERSION;
+    private static final String PACKAGE_NAME = "com.vber";
+    private static final String MODULE_PREFIX = "vber-";
+    private static final String MODULE_ARTIFACT_ID_PREFIX = "<artifactId>" + MODULE_PREFIX;
+    private static final String MODULE_ID_PREFIX = "<module>" + MODULE_PREFIX;
+    private static final String TITLE = "VAP后台管理系统";
+    private static final String UI_VUE_FILE = "VAP_V3.VUE";
+    private static final String UI_APP_FILE = "VAP_V3.APP";
+    /**
+     * 白名单文件,不进行重写,避免出问题
+     */
+    private static final Set<String> WHITE_FILE_TYPES = SetUtils.asSet("gif", "jpg", "svg", "png", // 图片
+            "eot", "woff2", "ttf", "woff"); // 字体
+
+    /**
+     * 构建项目。
+     * 该方法使用提供的参数配置项目信息,并调用另一个方法进行实际的项目构建。
+     *
+     * @param groupId      项目的组ID,标识项目的所属组织或组。
+     * @param artifactId   项目的artifact ID,是项目在组内的唯一标识。
+     * @param packageName  项目的包名,用于Java项目的包结构定义。
+     * @param title        项目的标题,可用于描述项目的名称或目的。
+     * @param projectDir   项目目录的路径,指定项目文件和资源所在的物理位置。
+     * @param modulePrefix 项目模块前缀,如果为空则使用默认值"vber-"。用于构建模块名称。
+     */
+    public static void build(@NotNull String groupId, @NotNull String artifactId, @NotNull String packageName, @NotNull String title, @NotNull String projectDir, String modulePrefix) {
+        // 如果modulePrefix为空,则使用默认前缀
+        modulePrefix = StrUtil.isEmpty(modulePrefix) ? "vber-" : modulePrefix;
+        modulePrefix = modulePrefix.endsWith("-") ? modulePrefix : modulePrefix + "-";
+        // 确保projectDir以反斜杠结尾。
+        projectDir = projectDir.endsWith("\\") ? projectDir : projectDir + "\\";
+        build(new ProjectInfo(groupId, artifactId, packageName, title, projectDir, modulePrefix));
+    }
+
+    /**
+     * 构建项目的方法。
+     * 根据提供的项目信息,该方法会创建一个新的项目目录,复制旧项目中的文件到新目录,并根据提供的项目信息修改文件内容。
+     *
+     * @param projectInfo 包含项目的基本信息,如组ID、artifactID、包名等。
+     */
+    public static void build(ProjectInfo projectInfo) {
+        // 记录开始时间
+        long start = System.currentTimeMillis();
+
+        // 分割获取项目的基本目录路径
+        String[] arr = getProjectBaseDir().split("SERVER");
+        String oldProjectDir = arr[0];
+
+        // 使用项目信息中的值,如果为空则使用默认值
+        String groupId = StrUtil.isEmpty(projectInfo.getGroupId()) ? GROUP_ID : projectInfo.getGroupId();
+        String artifactId = StrUtil.isEmpty(projectInfo.getArtifactId()) ? ARTIFACT_ID : projectInfo.getArtifactId();
+        String packageName = StrUtil.isEmpty(projectInfo.getPackageName()) ? PACKAGE_NAME : projectInfo.getPackageName();
+        String modulePrefix = StrUtil.isEmpty(projectInfo.getModulePrefix()) ? MODULE_PREFIX : projectInfo.getModulePrefix();
+        String moduleArtifactIdPrefix = "<artifactId>" + modulePrefix;
+        String moduleIdPrefix = "<module>" + modulePrefix;
+        String uiFile = modulePrefix.replace("-", "").toUpperCase();
+        String title = StrUtil.isEmpty(projectInfo.getTitle()) ? TITLE : projectInfo.getTitle();
+
+        if (StrUtil.isEmpty(projectInfo.getGroupId())) {
+            log.warn("[新项目 GroupId 为空!]");
+        }
+        if (StrUtil.isEmpty(projectInfo.getArtifactId())) {
+            log.warn("[新项目 ArtifactId 为空!]");
+        }
+        if (StrUtil.isEmpty(projectInfo.getPackageName())) {
+            log.warn("[新项目 PackageName 为空!]");
+        }
+        if (StrUtil.isEmpty(projectInfo.getTitle())) {
+            log.warn("[新项目 Title 为空!]");
+        }
+        log.info("[原项目路劲改地址 ({})]", oldProjectDir);
+        String projectDir = StrUtil.isEmpty(projectInfo.getProjectDir()) ? oldProjectDir + "_New\\" :
+                projectInfo.getProjectDir().endsWith("\\") ? projectInfo.getProjectDir() : projectInfo.getProjectDir() + "\\";
+
+        log.info("[检测新项目目录 ({})是否存在]", projectDir);
+        if (FileUtil.exist(projectDir)) {
+            log.error("[新项目目录检测 ({})已存在,请更改新的目录!程序退出]", projectDir);
+            throw new RuntimeException(StrUtil.format("新项目目录检测 ({})已存在,请更改新的目录!", projectDir));
+        }
+        // 如果新目录中存在 PACKAGE_NAME,ARTIFACT_ID 等关键字,路径会被替换,导致生成的文件不在预期目录
+        if (StrUtil.containsAny(projectDir, PACKAGE_NAME, ARTIFACT_ID, StrUtil.upperFirst(ARTIFACT_ID))) {
+            log.error("[新项目目录 `projectBaseDirNew` 检测 ({}) 存在冲突名称「{}」或者「{}」,请更改新的目录!程序退出]",
+                    projectDir, PACKAGE_NAME, ARTIFACT_ID);
+            throw new RuntimeException(StrUtil.format("新项目目录检测 ({})存在冲突名称「{}」或者「{}」,请更改新的目录!", projectDir, PACKAGE_NAME, ARTIFACT_ID));
+        }
+        log.info("[完成新项目目录检测,新项目路径地址 ({})]", projectDir);
+        // 获得需要复制的文件
+        log.info("[开始获得需要重写的文件,预计需要 5-15 秒]");
+        Collection<File> files = listFiles(oldProjectDir);
+        log.info("[需要重写的文件数量:{},预计需要 {}-{} 秒]", files.size(), files.size() / 200, files.size() / 100);
+        // 写入文件
+        files.forEach(file -> {
+            // 如果是白名单的文件类型,不进行重写,直接拷贝
+            String fileType = getFileType(file);
+            if (WHITE_FILE_TYPES.contains(fileType)) {
+                copyFile(file, oldProjectDir, projectDir, packageName, artifactId, modulePrefix, uiFile);
+                return;
+            }
+            // 如果非白名单的文件类型,重写内容,在生成文件
+            String content = replaceFileContent(file, groupId, artifactId, packageName, title, moduleArtifactIdPrefix, moduleIdPrefix);
+            writeFile(file, content, oldProjectDir, projectDir, packageName, artifactId, modulePrefix, uiFile);
+        });
+        log.info("[重写完成]共耗时:{} 秒", (System.currentTimeMillis() - start) / 1000);
+    }
+
+    private static Collection<File> listFiles(String projectBaseDir) {
+        Collection<File> files = FileUtil.loopFiles(projectBaseDir);
+        // 移除 IDEA、Git 自身的文件、Node 编译出来的文件
+        files = files.stream()
+                .filter(file -> !file.getPath().contains(separator + "target" + separator)
+                        && !file.getPath().contains(separator + "node_modules" + separator)
+                        && !file.getPath().contains(separator + ".idea" + separator)
+                        && !file.getPath().contains(separator + ".git" + separator)
+                        && !file.getPath().contains(separator + "dist" + separator)
+                        && !file.getPath().contains(separator + ".vscode" + separator)
+                        && !file.getPath().contains(separator + ".vs" + separator)
+                        && !file.getPath().contains(separator + "vber" + separator + "logs" + separator)
+                        && !file.getPath().contains(separator + "vber" + separator + "profile" + separator)
+                        && !file.getPath().contains(separator + "com" + separator + "vber" + separator + "reactor" + separator)
+                        && !file.getPath().contains(".flattened-pom.xml")
+                        && !file.getPath().contains("dist.zip")
+                        && !file.getPath().contains("dist.rar")
+                        && !file.getPath().contains(".iml")
+                        && !file.getPath().contains(".lock")
+                        && !file.getPath().contains(".html.gz"))
+                .collect(Collectors.toList());
+        return files;
+    }
+
+
+    private static void copyFile(File file, String projectBaseDir, String projectBaseDirNew, String packageNameNew, String artifactIdNew, String modulePrefix, String uiFile) {
+        String newPath = buildNewFilePath(file, projectBaseDir, projectBaseDirNew, packageNameNew, artifactIdNew, modulePrefix, uiFile);
+        FileUtil.copyFile(file, new File(newPath));
+    }
+
+    private static String replaceFileContent(File file, String groupIdNew,
+                                             String artifactIdNew, String packageNameNew,
+                                             String titleNew, String moduleArtifactIdPrefix, String moduleIdSuffix) {
+        String content = FileUtil.readString(file, StandardCharsets.UTF_8);
+        // 如果是白名单的文件类型,不进行重写
+        String fileType = getFileType(file);
+        if (WHITE_FILE_TYPES.contains(fileType)) {
+            return content;
+        }
+        // 执行文件内容都重写
+        return content.replaceAll(GROUP_ID, groupIdNew)
+                .replaceAll(PACKAGE_NAME, packageNameNew)
+                .replaceAll(MODULE_ARTIFACT_ID_PREFIX, moduleArtifactIdPrefix)
+                .replaceAll(ARTIFACT_ID_VERSION, artifactIdNew) // 带版本的ARTIFACT_ID也替换成无版本的!
+                .replaceAll(ARTIFACT_ID, artifactIdNew) // 必须放在最后替换,因为 ARTIFACT_ID 太短!
+                .replaceAll(StrUtil.upperFirst(ARTIFACT_ID), StrUtil.upperFirst(artifactIdNew))
+                .replaceAll(StrUtil.upperFirst(MODULE_ID_PREFIX), StrUtil.upperFirst(moduleIdSuffix))
+                .replaceAll("permission: \"no_auth\" //NEW PROJECT", "permission: \"\"") //隐藏生成新项目按钮
+                .replaceAll(TITLE, titleNew);
+    }
+
+
+    private static void writeFile(File file, String fileContent, String projectBaseDir, String projectBaseDirNew, String packageNameNew, String artifactIdNew, String modulePrefix, String uiFile) {
+        String newPath = buildNewFilePath(file, projectBaseDir, projectBaseDirNew, packageNameNew, artifactIdNew, modulePrefix, uiFile);
+        FileUtil.writeUtf8String(fileContent, newPath);
+    }
+
+
+    private static String buildNewFilePath(File file, String projectBaseDir, String projectBaseDirNew, String packageNameNew, String artifactIdNew, String modulePrefix, String uiFile) {
+        String uiVueFile = uiFile + ".VUE";
+        String uiAppFile = uiFile + ".APP";
+        return file.getPath().replace(projectBaseDir, projectBaseDirNew) // 新目录
+                .replace(PACKAGE_NAME.replaceAll("\\.", Matcher.quoteReplacement(separator)),
+                        packageNameNew.replaceAll("\\.", Matcher.quoteReplacement(separator)))
+                .replace(ARTIFACT_ID, artifactIdNew) //
+                .replace(MODULE_PREFIX, modulePrefix) //
+                .replace(UI_VUE_FILE, uiVueFile) //
+                .replace(UI_APP_FILE, uiAppFile) //
+                .replaceAll(StrUtil.upperFirst(ARTIFACT_ID), StrUtil.upperFirst(artifactIdNew));
+    }
+
+    private static String getFileType(File file) {
+        return file.length() > 0 ? FileTypeUtil.getType(file) : "";
+    }
+
+    private static String getProjectBaseDir() {
+        String baseDir = System.getProperty("user.dir");
+        if (StrUtil.isEmpty(baseDir)) {
+            throw new NullPointerException("项目基础路径不存在");
+        }
+        return baseDir;
+    }
+
+
+}

+ 34 - 0
SERVER/VberAdminPlusV3/vber-modules/vber-generator/src/main/java/com/vber/reactor/domain/ProjectInfo.java

@@ -0,0 +1,34 @@
+package com.vber.reactor.domain;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+
+/**
+ * @author Yue
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class ProjectInfo implements Serializable {
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @NotBlank(message = "新项目GroupId不能为空")
+    private String groupId;
+    @NotBlank(message = "新项目ArtifactId不能为空")
+    private String artifactId;
+    @NotBlank(message = "新项目包名称不能为空")
+    private String packageName;
+    @NotBlank(message = "新项目系统标题不能为空")
+    private String title;
+    @NotBlank(message = "新项目存放路径不能为空")
+    private String projectDir;
+
+    private String modulePrefix;
+}

+ 21 - 0
SERVER/VberAdminPlusV3/vber-modules/vber-generator/src/test/java/com/vber/reactor/ProjectReactorTest.java

@@ -0,0 +1,21 @@
+package com.vber.reactor;
+
+
+import com.vber.reactor.core.ProjectReactor;
+import org.junit.Test;
+
+public class ProjectReactorTest {
+    @Test
+    public void reactor() {
+        // ========== 手动修改配置后运行 ==========
+        String groupId = "com.va";
+        String artifactId = "VberAdmin";
+        String packageName = "cn.vber";
+        String title = "VberAdmin管理平台";
+        String modulePrefix = "vb-";
+        String projectDir = "D:\\Project\\VberPlus\\";
+        // ========== 手动修改配置后运行 ==========
+        ProjectReactor.build(groupId, artifactId, packageName, title, projectDir, modulePrefix);
+
+    }
+}

+ 8 - 0
UI/VAP_V3.VUE/src/api/tool/_gen.ts

@@ -86,6 +86,14 @@ class genApi {
 			url: "/tool/gen/getDataNames"
 			url: "/tool/gen/getDataNames"
 		})
 		})
 	}
 	}
+
+	// 获取数据源名称
+	projectReactor = (data: any) => {
+		return Rs.post({
+			url: "/tool/project/reactor",
+			data
+		})
+	}
 }
 }
 
 
 export default genApi
 export default genApi

+ 78 - 1
UI/VAP_V3.VUE/src/views/tool/gen/index.vue

@@ -10,6 +10,7 @@ const importTableRef = ref()
 const modalRef = ref()
 const modalRef = ref()
 const importModalRef = ref()
 const importModalRef = ref()
 const previewModalRef = ref()
 const previewModalRef = ref()
+
 const opts = reactive<any>({
 const opts = reactive<any>({
 	columns: [
 	columns: [
 		{ field: "tableId", name: `表ID`, width: "auto", visible: false },
 		{ field: "tableId", name: `表ID`, width: "auto", visible: false },
@@ -58,7 +59,18 @@ const opts = reactive<any>({
 	handleFuns: {
 	handleFuns: {
 		handleImport
 		handleImport
 	},
 	},
-
+	customBtns: [
+		{
+			show: true,
+			key: "handleProject",
+			name: "生成新项目",
+			btnClass: "btn btn-light-info",
+			clickFun: handleProject,
+			iconType: "class",
+			icon: "bi bi-bookmark-plus",
+			permission: "no_auth" //NEW PROJECT
+		}
+	],
 	tableListFun: apis.tool.genApi.listTable,
 	tableListFun: apis.tool.genApi.listTable,
 	getEntityFun: apis.tool.genApi.getGenTable,
 	getEntityFun: apis.tool.genApi.getGenTable,
 	deleteEntityFun: apis.tool.genApi.delTable,
 	deleteEntityFun: apis.tool.genApi.delTable,
@@ -205,6 +217,63 @@ function getTemplateName(templateName: string) {
 	}
 	}
 	return templateName
 	return templateName
 }
 }
+
+const newProjectModalRef = ref()
+const newProjectForm = ref<any>({
+	groupId: "com.vap",
+	artifactId: "VberAdminPlus",
+	packageName: "com.vber",
+	modulePrefix: "vber-",
+	title: "VAP后台管理系统",
+	projectDir: "D:\\Project\\"
+})
+const newProjectFormItem: any = [
+	{
+		field: "groupId",
+		label: "GROUP ID",
+		placeholder: "请输入GROUP ID",
+		required: true
+	},
+	{
+		field: "artifactId",
+		label: "ARTIFACT ID",
+		placeholder: "请输入ARTIFACT ID",
+		required: true
+	},
+	{
+		field: "packageName",
+		label: "PACKAGE NAME",
+		placeholder: "请输入PACKAGE NAME",
+		required: true
+	},
+	{
+		field: "modulePrefix",
+		label: "MODULE PREFIX",
+		placeholder: "请输入MODULE PREFIX",
+		required: true
+	},
+	{
+		field: "title",
+		label: "TITLE",
+		placeholder: "请输入TITLE",
+		required: true
+	},
+	{
+		field: "projectDir",
+		label: "PROJECT DIR",
+		placeholder: "请输入PROJECT DIR",
+		required: true
+	}
+]
+function handleProject() {
+	newProjectModalRef.value?.show()
+}
+
+function submitProjectForm() {
+	apis.tool.genApi.projectReactor(newProjectForm.value).then(() => {
+		message.msgSuccess("新项目生成成功:", newProjectForm.value.projectDir)
+	})
+}
 </script>
 </script>
 
 
 <template>
 <template>
@@ -332,5 +401,13 @@ function getTemplateName(templateName: string) {
 				</div>
 				</div>
 			</template>
 			</template>
 		</VbModal>
 		</VbModal>
+		<VbModal
+			v-model:modal="newProjectModalRef"
+			title="生成新项目"
+			:form-items="newProjectFormItem"
+			:form-data="newProjectForm"
+			@confirm="submitProjectForm"
+			:form-label-width="140"
+			append-to-body></VbModal>
 	</div>
 	</div>
 </template>
 </template>