1. 新增代码生成器支持生成时预设基础封装组件(查询、表格、列表、编辑弹窗)

2. 新增模块与 Server 新增脚本方法
This commit is contained in:
chenbowen
2025-09-02 09:46:13 +08:00
parent 1ed31a4f49
commit 287f4bbd7e
6 changed files with 624 additions and 230 deletions

View File

@@ -0,0 +1,241 @@
package cn.iocoder.yudao.framework.test.core.ut;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Scanner;
import java.util.stream.Stream;
/**
* Yudao 模块代码生成器
*
* 使用方法:
* 1. 在 IDE 中,右键运行 {@link #main(String[])} 方法
* 2. 根据提示,输入模块名,例如 "order"
* 3. 根据提示,输入作者名,例如 "yudao"
*
* @author 芋道源码
*/
public class ModuleGenerator {
private static final String TEMPLATE_PATH = "yudao-module-template";
private static final String BASE_PACKAGE = "cn.iocoder.yudao.module.";
public static void main(String[] args) throws IOException {
// 1. 获取用户输入
Scanner scanner = new Scanner(System.in);
System.out.print("请输入新模块的名称支持多个模块用逗号分割例如order,userManagement,payment: ");
String moduleNames = scanner.nextLine();
System.out.print("请输入作者名称例如yudao: ");
String author = scanner.nextLine();
scanner.close();
// 分割模块名
String[] modules = moduleNames.split(",");
// 2. 定义项目根路径
Path projectRoot = Paths.get("").toAbsolutePath();
Path templateDir = projectRoot.resolve(TEMPLATE_PATH);
if (!Files.exists(templateDir)) {
System.err.println("错误:模板目录不存在: " + templateDir);
return;
}
// 3. 批量创建模块
for (String moduleName : modules) {
moduleName = moduleName.trim(); // 去除空格
if (moduleName.isEmpty()) {
continue;
}
System.out.println("\n=== 开始创建模块: " + moduleName + " ===");
createSingleModule(templateDir, projectRoot, moduleName, author);
}
System.out.println("\n所有模块创建完成!");
System.out.println("请手动将以下模块添加到根 pom.xml 的 <modules> 标签中:");
for (String moduleName : modules) {
moduleName = moduleName.trim();
if (!moduleName.isEmpty()) {
String dashModuleName = camelToKebabCase(moduleName);
System.out.println("<module>yudao-module-" + dashModuleName + "</module>");
}
}
}
private static void createSingleModule(Path templateDir, Path projectRoot, String moduleName, String author) throws IOException {
String packageName = moduleName.replace("-", "");
String capitalizedModuleName = capitalize(packageName);
// 将小驼峰转换为短横线分割的模块名
String dashModuleName = camelToKebabCase(moduleName);
// 定义新模块路径
Path newModuleDir = projectRoot.resolve("yudao-module-" + dashModuleName);
if (Files.exists(newModuleDir)) {
System.err.println("警告:模块 '" + dashModuleName + "' 已存在于 " + newModuleDir + ",跳过创建");
return;
}
System.out.println("将在以下位置创建新模块: " + newModuleDir);
// 复制并处理文件
copyAndProcessDirectory(templateDir, newModuleDir, dashModuleName, packageName, capitalizedModuleName, author);
// 创建启动类
createApplicationClass(newModuleDir, dashModuleName, packageName, capitalizedModuleName, author);
System.out.println("模块 '" + dashModuleName + "' 创建成功!");
}
private static void copyAndProcessDirectory(Path sourceDir, Path targetDir, String moduleName, String packageName, String capitalizedModuleName, String author) throws IOException {
try (Stream<Path> stream = Files.walk(sourceDir)) {
stream.filter(path -> shouldIncludePath(sourceDir.relativize(path)))
.forEach(sourcePath -> {
try {
Path relativePath = sourceDir.relativize(sourcePath);
Path targetPath = targetDir.resolve(relativePath);
String targetPathStr = targetPath.toString();
// 替换路径中的 'template'
targetPathStr = targetPathStr.replace("template", moduleName);
// 替换包名路径
targetPathStr = targetPathStr.replace(java.io.File.separator + "template" + java.io.File.separator, java.io.File.separator + packageName + java.io.File.separator);
targetPath = Paths.get(targetPathStr);
if (Files.isDirectory(sourcePath)) {
Files.createDirectories(targetPath);
} else {
// 确保父目录存在
Files.createDirectories(targetPath.getParent());
String content = new String(Files.readAllBytes(sourcePath));
String newContent = processFileContent(content, sourcePath.getFileName().toString(), moduleName, packageName, capitalizedModuleName, author);
Files.write(targetPath, newContent.getBytes());
}
} catch (IOException e) {
throw new RuntimeException("处理文件失败: " + sourcePath, e);
}
});
}
}
private static boolean shouldIncludePath(Path relativePath) {
String pathStr = relativePath.toString();
// 排除 target 目录
if (pathStr.contains("target")) {
return false;
}
// 排除 .flattened-pom.xml 文件
if (pathStr.contains(".flattened-pom.xml")) {
return false;
}
// 如果是根目录或者包含 example 的路径,则包含
if (relativePath.getNameCount() <= 3) { // 根目录和基础包结构
return true;
}
// 只包含 example 相关的文件和目录
return pathStr.contains("example") || pathStr.contains("ErrorCodeConstants");
}
private static String processFileContent(String content, String fileName, String moduleName, String packageName, String capitalizedModuleName, String author) {
// 如果是 ErrorCodeConstants 文件,特殊处理
if (fileName.equals("ErrorCodeConstants.java")) {
return processErrorCodeConstants(content, moduleName, packageName, author);
}
return content.replace("yudao-module-template", "yudao-module-" + moduleName)
.replace("cn.iocoder.yudao.module.template", BASE_PACKAGE + packageName)
.replace("TemplateServerApplication", capitalizedModuleName + "ServerApplication")
.replace("样例模块", moduleName + "模块")
.replace("周迪", author) // 替换作者
.replace("template", moduleName) // 最后替换纯 "template"
.replace("Example", capitalize(moduleName) + "Example");
}
private static String processErrorCodeConstants(String content, String moduleName, String packageName, String author) {
// 构建新的 ErrorCodeConstants 内容,只保留基本结构,移除具体的错误码常量
StringBuilder sb = new StringBuilder();
sb.append("package ").append(BASE_PACKAGE).append(packageName).append(".enums;\n\n");
sb.append("import cn.iocoder.yudao.framework.common.exception.ErrorCode;\n\n");
sb.append("/**\n");
sb.append(" * ").append(moduleName).append(" 错误码枚举类\n");
sb.append(" *\n");
sb.append(" * ").append(moduleName).append(" 系统,使用 1-xxx-xxx-xxx 段\n");
sb.append(" *\n");
sb.append(" * @author ").append(author).append("\n");
sb.append(" */\n");
sb.append("public interface ErrorCodeConstants {\n\n");
sb.append(" // ========== 示例模块 1-001-000-000 ==========\n");
sb.append(" // ErrorCode EXAMPLE_NOT_EXISTS = new ErrorCode(1_001_000_001, \"示例不存在\");\n\n");
sb.append("}\n");
return sb.toString();
}
/**
* 创建启动类
*/
private static void createApplicationClass(Path moduleDir, String dashModuleName, String packageName, String capitalizedModuleName, String author) throws IOException {
// 创建启动类目录
Path applicationDir = moduleDir.resolve("yudao-module-" + dashModuleName + "-server")
.resolve("src/main/java/cn/iocoder/yudao/module/" + packageName);
Files.createDirectories(applicationDir);
// 创建启动类文件
Path applicationFile = applicationDir.resolve(capitalizedModuleName + "ServerApplication.java");
StringBuilder sb = new StringBuilder();
sb.append("package ").append(BASE_PACKAGE).append(packageName).append(";\n\n");
sb.append("import org.springframework.boot.SpringApplication;\n");
sb.append("import org.springframework.boot.autoconfigure.SpringBootApplication;\n\n");
sb.append("/**\n");
sb.append(" * ").append(dashModuleName).append(" 模块的启动类\n");
sb.append(" *\n");
sb.append(" * @author ").append(author).append("\n");
sb.append(" */\n");
sb.append("@SpringBootApplication\n");
sb.append("public class ").append(capitalizedModuleName).append("ServerApplication {\n\n");
sb.append(" public static void main(String[] args) {\n");
sb.append(" SpringApplication.run(").append(capitalizedModuleName).append("ServerApplication.class, args);\n");
sb.append(" }\n\n");
sb.append("}\n");
Files.write(applicationFile, sb.toString().getBytes());
System.out.println("创建启动类: " + applicationFile);
}
private static String capitalize(String str) {
if (str == null || str.isEmpty()) {
return str;
}
return str.substring(0, 1).toUpperCase() + str.substring(1);
}
/**
* 将小驼峰命名转换为短横线分割的命名
* 例如orderManagement -> order-management
*/
private static String camelToKebabCase(String camelCase) {
if (camelCase == null || camelCase.isEmpty()) {
return camelCase;
}
StringBuilder result = new StringBuilder();
for (int i = 0; i < camelCase.length(); i++) {
char c = camelCase.charAt(i);
if (Character.isUpperCase(c) && i > 0) {
result.append('-');
}
result.append(Character.toLowerCase(c));
}
return result.toString();
}
}

View File

@@ -0,0 +1,156 @@
package cn.iocoder.yudao.framework.test.core.ut;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Scanner;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Yudao Server 代码生成器
*
* 使用方法:
* 1. 在 IDE 中,右键运行 {@link #main(String[])} 方法
* 2. 根据提示,输入新 Server 的名称,例如 "demo-server"
* 3. 根据提示,输入作者名,例如 "yudao"
*
* @author 芋道源码
*/
public class ServerGenerator {
private static final String TEMPLATE_PATH = "yudao-server";
private static final String BASE_PACKAGE_PREFIX = "cn.iocoder.yudao.";
public static void main(String[] args) throws IOException {
// 1. 获取用户输入
Scanner scanner = new Scanner(System.in);
System.out.print("请输入新 Server 的基础名称小写短横线例如demo: ");
String baseName = scanner.nextLine();
String serverName = baseName + "-server";
System.out.print("请输入作者名称例如yudao: ");
String author = scanner.nextLine();
scanner.close();
// 2. 定义项目根路径
// 注意:请在项目的根目录(例如 ztcloud下运行该生成器
Path projectRoot = Paths.get("").toAbsolutePath();
Path templateDir = projectRoot.resolve(TEMPLATE_PATH);
if (!Files.exists(templateDir)) {
System.err.println("错误:模板目录 '" + templateDir + "' 不存在。");
System.err.println("请确保在项目根目录下运行此生成器。");
return;
}
System.out.println("\n=== 开始创建 Server: " + serverName + " ===");
createSingleServer(templateDir, projectRoot, serverName, author);
System.out.println("\nServer '" + serverName + "' 创建成功!");
System.out.println("请手动将以下模块添加到根 pom.xml 的 <modules> 标签中:");
System.out.println("<module>" + serverName + "</module>");
}
private static void createSingleServer(Path templateDir, Path projectRoot, String serverName, String author) throws IOException {
String packageName = serverName.replace("-", "");
String capitalizedName = toPascalCase(serverName);
// 定义新 Server 路径
Path newServerDir = projectRoot.resolve(serverName);
if (Files.exists(newServerDir)) {
System.err.println("警告Server '" + serverName + "' 已存在于 '" + newServerDir + "',跳过创建。");
return;
}
System.out.println("将在以下位置创建新 Server: " + newServerDir);
// 复制并处理文件
copyAndProcessDirectory(templateDir, newServerDir, serverName, packageName, capitalizedName, author);
}
private static void copyAndProcessDirectory(Path sourceDir, Path targetDir, String serverName, String packageName, String capitalizedName, String author) throws IOException {
try (Stream<Path> stream = Files.walk(sourceDir)) {
stream.forEach(sourcePath -> {
try {
Path relativePath = sourceDir.relativize(sourcePath);
if (!shouldIncludePath(relativePath)) {
return;
}
// 处理路径和文件名
String javaPackagePath = "src" + java.io.File.separator + "main" + java.io.File.separator + "java" + java.io.File.separator
+ "cn" + java.io.File.separator + "iocoder" + java.io.File.separator + "yudao" + java.io.File.separator;
String targetPathStr = relativePath.toString()
.replace(javaPackagePath + "server", javaPackagePath + packageName)
.replace("YudaoServerApplication.java", capitalizedName + "Application.java");
Path targetPath = targetDir.resolve(targetPathStr);
if (Files.isDirectory(sourcePath)) {
Files.createDirectories(targetPath);
} else {
Files.createDirectories(targetPath.getParent());
String content = new String(Files.readAllBytes(sourcePath));
String newContent = processFileContent(content, sourcePath.getFileName().toString(), serverName, packageName, capitalizedName, author);
Files.write(targetPath, newContent.getBytes());
}
} catch (IOException e) {
throw new RuntimeException("处理文件失败: " + sourcePath, e);
}
});
}
}
private static boolean shouldIncludePath(Path relativePath) {
String pathStr = relativePath.toString();
// 排除 target, .idea, .git 等目录和 .iml, .flattened-pom.xml 等文件
return !pathStr.startsWith("target")
&& !pathStr.startsWith(".idea")
&& !pathStr.startsWith(".git")
&& !pathStr.endsWith(".iml")
&& !pathStr.contains(".flattened-pom.xml");
}
private static String processFileContent(String content, String fileName, String serverName, String packageName, String capitalizedName, String author) {
String newContent = content.replace("芋道源码", author);
switch (fileName) {
case "pom.xml":
// 替换 artifactId
newContent = newContent.replace("<artifactId>yudao-server</artifactId>", "<artifactId>" + serverName + "</artifactId>");
// 移除对 yudao-module-xxx-server 的依赖,但保留 system-server 和 infra-server
return newContent.replaceAll("(?m)^\\s*<dependency>\\s*<groupId>cn\\.iocoder\\.cloud</groupId>\\s*<artifactId>yudao-module-(?!system-server|infra-server).*-server</artifactId>[\\s\\S]*?</dependency>\\s*", "");
case "application.yaml":
return newContent.replace("name: yudao-server", "name: " + serverName);
case "YudaoServerApplication.java":
return newContent.replace("package cn.iocoder.yudao.server;", "package " + BASE_PACKAGE_PREFIX + packageName + ";")
.replace("YudaoServerApplication", capitalizedName + "Application");
case "Dockerfile":
return newContent.replace("yudao-server.jar", serverName + ".jar")
.replace("/yudao-server", "/" + serverName);
default:
return newContent;
}
}
/**
* 将 kebab-case 转换为 PascalCase
* 例如demo-server -> DemoServer
*/
private static String toPascalCase(String kebabCase) {
if (kebabCase == null || kebabCase.isEmpty()) {
return "";
}
return Arrays.stream(kebabCase.split("-"))
.map(s -> {
if (s.isEmpty()) {
return "";
}
return s.substring(0, 1).toUpperCase() + s.substring(1);
})
.collect(Collectors.joining());
}
}