From 9d41d068e9689124d42773e69f4ebc58d9c703f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=AE=B6=E8=AE=AD?= Date: Wed, 10 Jun 2026 18:18:39 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=B9=E9=80=86=E5=90=91=E5=B7=A5=E7=A8=8B?= =?UTF-8?q?=E6=8F=92=E4=BB=B6=E4=BC=98=E5=8C=96=E4=BB=A3=E7=A0=81=E6=8F=90?= =?UTF-8?q?=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workutils/biz/diff/DiffPlugin.java | 51 +- .../MyBatisGeneratorPlugin.java | 127 ++-- .../MyBatisGeneratorWindow.java | 547 ++++++++++++++++++ .../generator/MyBatisGeneratorService.java | 120 +++- .../service/generator/SuppressKeyPlugin.java | 55 ++ .../system/support/JfxBootstrap.java | 61 ++ src/main/resources/mybatis-generator.xml | 2 + 7 files changed, 847 insertions(+), 116 deletions(-) create mode 100644 src/main/java/com/example/workutils/biz/mybatisgenerator/MyBatisGeneratorWindow.java create mode 100644 src/main/java/com/example/workutils/biz/mybatisgenerator/service/generator/SuppressKeyPlugin.java create mode 100644 src/main/java/com/example/workutils/system/support/JfxBootstrap.java diff --git a/src/main/java/com/example/workutils/biz/diff/DiffPlugin.java b/src/main/java/com/example/workutils/biz/diff/DiffPlugin.java index 6ad3eb9..f2f9ea4 100644 --- a/src/main/java/com/example/workutils/biz/diff/DiffPlugin.java +++ b/src/main/java/com/example/workutils/biz/diff/DiffPlugin.java @@ -3,6 +3,7 @@ package com.example.workutils.biz.diff; import com.example.workutils.system.config.WorkUtilsProperties; import com.example.workutils.system.plugin.Plugin; import com.example.workutils.system.plugin.PluginDescriptor; +import com.example.workutils.system.support.JfxBootstrap; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -51,60 +52,16 @@ public class DiffPlugin implements Plugin { @Override public String[] getMenuOptions() { return MENU_OPTIONS; } - // ==================== JavaFX 初始化 ==================== + // ==================== JavaFX 初始化(复用共享启动器) ==================== - /** - * Spring Boot 默认开启 headless 模式,Swing 无法初始化。 - * 这里用 JavaFX Application.launch 在后台线程启动 JavaFX 运行时, - * 后续通过 Platform.runLater 在 JavaFX 线程上创建窗口。 - */ - private static final java.util.concurrent.CountDownLatch JFX_READY = - new java.util.concurrent.CountDownLatch(1); - private static volatile boolean jfxFailed; - - static { - Thread jfxLauncher = new Thread(() -> { - try { - javafx.application.Platform.setImplicitExit(false); - javafx.application.Application.launch(JFXBootstrap.class); - } catch (Exception e) { - jfxFailed = true; - JFX_READY.countDown(); - } - }, "JavaFX-Bootstrap"); - jfxLauncher.setDaemon(true); - jfxLauncher.start(); - } - - /** 占位 Application,仅用于触发 JavaFX 工具包初始化 */ - public static class JFXBootstrap extends javafx.application.Application { - @Override - public void start(javafx.stage.Stage stage) { - JFX_READY.countDown(); - } - } - - // ==================== 交互模式 ==================== - - @Override - public void handleChoice(int choice, Scanner scanner) throws Exception { - switch (choice) { - case 1: openVisualDiff(); break; - default: printInvalidOption(); break; - } - } - - /** - * 启动 JavaFX 可视化对比窗口,阻塞直到窗口关闭 - */ private void openVisualDiff() { try { - JFX_READY.await(); + JfxBootstrap.await(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; } - if (jfxFailed) { + if (JfxBootstrap.isFailed()) { System.err.println("JavaFX 初始化失败,请确认 JDK 8 包含 JavaFX 运行时(OracleJDK 内置;OpenJDK 需安装 OpenJFX)"); return; } diff --git a/src/main/java/com/example/workutils/biz/mybatisgenerator/MyBatisGeneratorPlugin.java b/src/main/java/com/example/workutils/biz/mybatisgenerator/MyBatisGeneratorPlugin.java index af12917..a695f8a 100644 --- a/src/main/java/com/example/workutils/biz/mybatisgenerator/MyBatisGeneratorPlugin.java +++ b/src/main/java/com/example/workutils/biz/mybatisgenerator/MyBatisGeneratorPlugin.java @@ -1,18 +1,21 @@ package com.example.workutils.biz.mybatisgenerator; import com.example.workutils.biz.mybatisgenerator.service.generator.MyBatisGeneratorService; +import com.example.workutils.system.config.WorkUtilsProperties; import com.example.workutils.system.plugin.Plugin; import com.example.workutils.system.plugin.PluginDescriptor; +import com.example.workutils.system.support.FileOperationHelper; +import com.example.workutils.system.support.JfxBootstrap; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; -import java.util.Arrays; import java.util.List; import java.util.Scanner; +import java.util.concurrent.CountDownLatch; /** - * MyBatis逆向工程插件(实现 Tool 接口) + * MyBatis逆向工程插件 */ @Slf4j @Component @@ -28,82 +31,84 @@ public class MyBatisGeneratorPlugin implements Plugin { .build(); private static final String[] MENU_OPTIONS = { - "1. 根据表名生成(单表)", - "2. 根据表名生成(多表,逗号分隔)", - "3. 根据表名通配符生成(如 user_%)", - "4. 查看数据库所有表", - "5. 查看当前配置信息", + "1. UI可视化操作(选择表+目录)", + "2. 查看数据库所有表", + "3. 查看当前配置信息", "0. 返回上级菜单" }; private final MyBatisGeneratorService generatorService; + private final FileOperationHelper fileOperationHelper; + private final WorkUtilsProperties properties; - @Override - public PluginDescriptor getDescriptor() { - return DESCRIPTOR; - } - - @Override - public String[] getMenuOptions() { - return MENU_OPTIONS; - } + @Override public PluginDescriptor getDescriptor() { return DESCRIPTOR; } + @Override public String[] getMenuOptions() { return MENU_OPTIONS; } @Override public void handleChoice(int choice, Scanner scanner) throws Exception { switch (choice) { - case 1: generateBySingleTable(scanner); break; - case 2: generateByMultipleTables(scanner); break; - case 3: generateByPattern(scanner); break; - case 4: showAllTables(); break; - case 5: showConfiguration(); break; + case 1: openVisualUI(); break; + case 2: showAllTables(); break; + case 3: showConfiguration(); break; default: printInvalidOption(); break; } } - private void generateBySingleTable(Scanner scanner) throws Exception { - System.out.print("请输入表名: "); - String tableName = scanner.nextLine().trim(); - if (tableName.isEmpty()) { System.out.println("表名不能为空"); return; } - log.info("开始生成表 [{}] 的Mapper文件", tableName); - String result = generatorService.generateByTableNames(Arrays.asList(tableName), true); - System.out.println(result); - printSuccess("生成完成"); - } + // ==================== UI 可视化操作 ==================== - private void generateByMultipleTables(Scanner scanner) throws Exception { - System.out.print("请输入表名(多个用逗号分隔): "); - String input = scanner.nextLine().trim(); - if (input.isEmpty()) { System.out.println("表名不能为空"); return; } - List tableNames = Arrays.asList(input.split(",")); - for (int i = 0; i < tableNames.size(); i++) { - tableNames.set(i, tableNames.get(i).trim()); - } - log.info("开始生成 {} 个表的Mapper文件", tableNames.size()); - String result = generatorService.generateByTableNames(tableNames, true); - System.out.println(result); - printSuccess("生成完成"); - } - - private void generateByPattern(Scanner scanner) throws Exception { - System.out.print("请输入表名通配符(如 user_%): "); - String pattern = scanner.nextLine().trim(); - if (pattern.isEmpty()) { System.out.println("通配符不能为空"); return; } - List matchedTables = generatorService.queryTablesByPattern(pattern); - if (matchedTables.isEmpty()) { - System.out.println("未找到匹配通配符 [" + pattern + "] 的表"); + private void openVisualUI() { + try { + JfxBootstrap.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); return; } - System.out.println("匹配到 " + matchedTables.size() + " 张表:"); - for (int i = 0; i < matchedTables.size(); i++) { - System.out.println(" " + (i + 1) + ". " + matchedTables.get(i)); + if (JfxBootstrap.isFailed()) { + System.err.println("JavaFX 初始化失败,请确认 JDK 8 包含 JavaFX 运行时"); + return; } - System.out.print("是否继续生成?(y/n): "); - String confirm = scanner.nextLine().trim().toLowerCase(); - if (!"y".equals(confirm)) { System.out.println("已取消生成"); return; } - log.info("开始根据通配符 [{}] 生成 {} 张表的Mapper文件", pattern, matchedTables.size()); - String result = generatorService.generateByTableNames(matchedTables, true); - System.out.println(result); - printSuccess("生成完成"); + + System.out.println("正在打开 MyBatis 逆向工程窗口..."); + + CountDownLatch windowClose = new CountDownLatch(1); + + MyBatisGeneratorWindow.GeneratorCallback cb = new MyBatisGeneratorWindow.GeneratorCallback() { + @Override + public List queryTables(String pattern) throws Exception { + return generatorService.queryTablesByPattern(pattern); + } + @Override + public String getDbUrl() { + return generatorService.getDatabaseUrl(); + } + @Override + public String getDbUsername() { + return generatorService.getDatabaseUsername(); + } + @Override + public String generate(List tables, String modelDir, String xmlDir, String mapperDir) throws Exception { + return generatorService.generateByTableNames(tables, true, modelDir, xmlDir, mapperDir); + } + }; + + javafx.application.Platform.runLater(() -> { + try { + MyBatisGeneratorWindow window = new MyBatisGeneratorWindow( + cb, fileOperationHelper, properties, windowClose); + window.show(); + } catch (Exception e) { + System.err.println("窗口创建失败: " + e.getMessage()); + e.printStackTrace(); + windowClose.countDown(); + } + }); + + try { + windowClose.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + System.out.println("MyBatis 逆向工程窗口已关闭"); } private void showAllTables() throws Exception { diff --git a/src/main/java/com/example/workutils/biz/mybatisgenerator/MyBatisGeneratorWindow.java b/src/main/java/com/example/workutils/biz/mybatisgenerator/MyBatisGeneratorWindow.java new file mode 100644 index 0000000..bc4f40d --- /dev/null +++ b/src/main/java/com/example/workutils/biz/mybatisgenerator/MyBatisGeneratorWindow.java @@ -0,0 +1,547 @@ +package com.example.workutils.biz.mybatisgenerator; + +import com.example.workutils.system.config.WorkUtilsProperties; +import com.example.workutils.system.support.FileOperationHelper; +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.value.ObservableValue; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.*; +import javafx.scene.control.cell.CheckBoxListCell; +import javafx.scene.layout.*; +import javafx.scene.text.Font; +import javafx.stage.DirectoryChooser; +import javafx.stage.Modality; +import javafx.stage.Stage; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.concurrent.CountDownLatch; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * MyBatis 逆向工程 JavaFX 可视化窗口 + *

+ * 提供表名通配符查询、输出目录选择(支持从项目路径快速导航)、一键生成功能。 + */ +public class MyBatisGeneratorWindow { + + private final Stage stage; + private final CountDownLatch closeLatch; + private final FileOperationHelper fileOperationHelper; + private final GeneratorCallback callback; + + // ---- UI 组件 ---- + private TextField patternField; + private ListView tableListView; + private final ObservableList tableItems = FXCollections.observableArrayList(); + private TextField modelDirField; + private TextField xmlDirField; + private TextField mapperDirField; + private TreeView projectTree; + private ToggleGroup fillTargetGroup; + + /** 表项(含复选框) */ + public static class TableItem { + private final String tableName; + private final SimpleBooleanProperty selected; + + public TableItem(String tableName, boolean selected) { + this.tableName = tableName; + this.selected = new SimpleBooleanProperty(selected); + } + + public String getTableName() { return tableName; } + public ObservableValue selectedProperty() { return selected; } + public boolean isSelected() { return selected.get(); } + public void setSelected(boolean v) { selected.set(v); } + + @Override + public String toString() { return tableName; } + } + + /** 生成回调接口 */ + public interface GeneratorCallback { + List queryTables(String pattern) throws Exception; + String getDbUrl(); + String getDbUsername(); + String generate(List tables, String modelDir, String xmlDir, String mapperDir) throws Exception; + } + + public MyBatisGeneratorWindow(GeneratorCallback callback, FileOperationHelper fileOperationHelper, + WorkUtilsProperties properties, CountDownLatch closeLatch) { + this.callback = callback; + this.fileOperationHelper = fileOperationHelper; + this.closeLatch = closeLatch; + this.stage = new Stage(); + buildUI(); + } + + public void show() { + stage.show(); + } + + // ==================== UI 构建 ==================== + + private void buildUI() { + stage.setTitle("MyBatis 逆向工程"); + stage.initModality(Modality.NONE); + stage.setOnCloseRequest(e -> close()); + + VBox root = new VBox(10); + root.setPadding(new Insets(15)); + root.setStyle("-fx-background-color: #f5f5f5;"); + + Label titleLabel = new Label("MyBatis 逆向工程"); + titleLabel.setFont(Font.font("Microsoft YaHei", 18)); + + HBox dbBox = createDbInfoSection(); + + SplitPane splitPane = new SplitPane(); + splitPane.setDividerPositions(0.28); + + VBox leftPanel = createProjectTreePanel(); + VBox rightPanel = createFormPanel(); + + splitPane.getItems().addAll(leftPanel, rightPanel); + VBox.setVgrow(splitPane, Priority.ALWAYS); + + HBox btnBox = createButtonBar(); + + root.getChildren().addAll(titleLabel, dbBox, splitPane, btnBox); + + Scene scene = new Scene(root, 960, 660); + stage.setScene(scene); + } + + /** 根据选中的按钮返回对应的输出目录字段 */ + private TextField resolveFillTarget() { + if (fillTargetGroup == null) return null; + Toggle selected = fillTargetGroup.getSelectedToggle(); + if (selected == null) return null; + String type = (String) ((ToggleButton) selected).getUserData(); + if ("model".equals(type)) return modelDirField; + if ("xml".equals(type)) return xmlDirField; + if ("mapper".equals(type)) return mapperDirField; + return null; + } + + private ToggleButton createFillButton(String label, String userData) { + ToggleButton btn = new ToggleButton(label); + btn.setToggleGroup(fillTargetGroup); + btn.setUserData(userData); + btn.setPrefWidth(75); + btn.setStyle("-fx-font-size: 12px; -fx-cursor: hand;"); + // 点击按钮时,将当前树节点路径填充到对应字段 + btn.setOnAction(e -> { + TreeItem selected = projectTree.getSelectionModel().getSelectedItem(); + if (selected != null) { + String path = buildTreePath(selected); + if (path != null && !path.isEmpty()) { + TextField target = resolveFillTarget(); + if (target != null) target.setText(path); + } + } + }); + // 选中/未选中样式切换 + fillTargetGroup.selectedToggleProperty().addListener((obs, old, cur) -> { + boolean active = (btn == cur); + btn.setStyle(active + ? "-fx-background-color: #4a90d9; -fx-text-fill: white; -fx-font-size: 12px; " + + "-fx-background-radius: 4; -fx-padding: 6 16;" + : "-fx-background-color: #e0e0e0; -fx-text-fill: #333; -fx-font-size: 12px; " + + "-fx-background-radius: 4; -fx-padding: 6 16;"); + }); + // 初始化默认选中样式 + btn.fire(); + return btn; + } + + private HBox createDbInfoSection() { + HBox box = new HBox(20); + box.setPadding(new Insets(8, 12, 8, 12)); + box.setStyle("-fx-background-color: #e8f0fe; -fx-background-radius: 4;"); + + Label urlLabel = new Label("数据库: " + callback.getDbUrl()); + urlLabel.setFont(Font.font("Consolas", 11)); + Label userLabel = new Label("用户: " + callback.getDbUsername()); + + box.getChildren().addAll(urlLabel, userLabel); + return box; + } + + // ==================== 左侧:项目路径树 ==================== + + private VBox createProjectTreePanel() { + VBox panel = new VBox(8); + panel.setPadding(new Insets(5)); + + Label treeTitle = new Label("项目路径 (快速定位)"); + treeTitle.setFont(Font.font("Microsoft YaHei", 13)); + + projectTree = new TreeView<>(); + projectTree.setRoot(new TreeItem<>("项目列表")); + projectTree.setShowRoot(false); + projectTree.setCellFactory(tv -> new TreeCell() { + @Override + protected void updateItem(String item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setText(null); + setGraphic(null); + } else { + int lastSep = Math.max(item.lastIndexOf('/'), item.lastIndexOf('\\')); + setText(lastSep >= 0 ? item.substring(lastSep + 1) : item); + } + } + }); + + loadProjectTree(); + + VBox.setVgrow(projectTree, Priority.ALWAYS); + + // 填充目标选择(按钮组) + Label quickLabel = new Label("点击按钮填充到:"); + quickLabel.setFont(Font.font("Microsoft YaHei", 11)); + quickLabel.setPadding(new Insets(10, 0, 2, 0)); + + this.fillTargetGroup = new ToggleGroup(); + + ToggleButton btnModel = createFillButton("Model", "model"); + btnModel.setSelected(true); + ToggleButton btnXml = createFillButton("XML", "xml"); + ToggleButton btnMapper = createFillButton("Mapper", "mapper"); + + HBox rbBox = new HBox(8, btnModel, btnXml, btnMapper); + + panel.getChildren().addAll(treeTitle, projectTree, quickLabel, rbBox); + return panel; + } + + private String buildTreePath(TreeItem item) { + if (item == null || item.getValue() == null) return null; + return item.getValue().replace('\\', '/'); + } + + private void loadProjectTree() { + TreeItem root = projectTree.getRoot(); + List projectPaths; + try { + projectPaths = fileOperationHelper.getProjectPaths(); + } catch (Exception e) { + projectPaths = Collections.emptyList(); + } + + if (projectPaths.isEmpty()) { + TreeItem emptyItem = new TreeItem<>("(未配置项目路径)"); + root.getChildren().add(emptyItem); + return; + } + + for (String projPath : projectPaths) { + TreeItem projNode = new TreeItem<>(projPath.replace('\\', '/')); + root.getChildren().add(projNode); + projNode.expandedProperty().addListener((obs, wasExpanded, isExpanded) -> { + if (isExpanded && !projNode.getChildren().isEmpty() + && "...".equals(projNode.getChildren().get(0).getValue())) { + loadSubDirs(projNode, projPath); + } + }); + projNode.getChildren().add(new TreeItem<>("...")); + } + } + + private void loadSubDirs(TreeItem parentNode, String parentPath) { + parentNode.getChildren().clear(); + Path dir = Paths.get(parentPath); + if (!Files.isDirectory(dir)) return; + + List subDirs = new ArrayList<>(); + try { + Files.list(dir) + .filter(Files::isDirectory) + .filter(p -> !p.getFileName().toString().startsWith(".")) + .sorted() + .forEach(subDirs::add); + } catch (IOException ignored) { return; } + + for (Path sub : subDirs) { + String subPath = sub.toString().replace('\\', '/'); + TreeItem subNode = new TreeItem<>(subPath); + parentNode.getChildren().add(subNode); + + boolean hasSubDirs; + try (Stream stream = Files.list(sub)) { + hasSubDirs = stream.anyMatch(p -> Files.isDirectory(p) + && !p.getFileName().toString().startsWith(".")); + } catch (IOException e) { + hasSubDirs = false; + } + if (hasSubDirs) { + subNode.getChildren().add(new TreeItem<>("...")); + } + subNode.expandedProperty().addListener((obs, wasExpanded, isExpanded) -> { + if (isExpanded && !subNode.getChildren().isEmpty() + && "...".equals(subNode.getChildren().get(0).getValue())) { + loadSubDirs(subNode, subPath); + } + }); + } + } + + // ==================== 右侧:表单 ==================== + + private VBox createFormPanel() { + VBox panel = new VBox(10); + panel.setPadding(new Insets(5)); + + Label patternLabel = new Label("表名通配符:"); + patternLabel.setFont(Font.font("Microsoft YaHei", 12)); + HBox patternBox = new HBox(10); + patternField = new TextField(); + patternField.setPromptText("如 trade%"); + patternField.setPrefWidth(200); + Button queryBtn = new Button("查询表"); + queryBtn.setStyle("-fx-background-color: #4a90d9; -fx-text-fill: white;"); + queryBtn.setOnAction(e -> onQueryTables()); + patternBox.getChildren().addAll(patternField, queryBtn); + + Label tableLabel = new Label("匹配表 (勾选要生成的表):"); + tableLabel.setFont(Font.font("Microsoft YaHei", 12)); + tableListView = new ListView<>(tableItems); + tableListView.setPrefHeight(170); + tableListView.setCellFactory(CheckBoxListCell.forListView(TableItem::selectedProperty)); + + HBox selectBox = new HBox(10); + Button selectAllBtn = new Button("全选"); + selectAllBtn.setOnAction(e -> tableItems.forEach(t -> t.setSelected(true))); + Button deselectAllBtn = new Button("取消全选"); + deselectAllBtn.setOnAction(e -> tableItems.forEach(t -> t.setSelected(false))); + selectBox.getChildren().addAll(selectAllBtn, deselectAllBtn); + + VBox dirBox = createDirectorySection(); + + panel.getChildren().addAll(patternLabel, patternBox, tableLabel, tableListView, selectBox, dirBox); + VBox.setVgrow(tableListView, Priority.ALWAYS); + return panel; + } + + private VBox createDirectorySection() { + VBox box = new VBox(8); + box.setPadding(new Insets(10, 0, 0, 0)); + + Label dirLabel = new Label("输出目录设置:"); + dirLabel.setFont(Font.font("Microsoft YaHei", 13)); + + HBox modelRow = createDirRow("Model (PO)", fm -> modelDirField = fm); + modelDirField = (TextField) modelRow.getChildren().get(1); + HBox xmlRow = createDirRow("XML Mapper", fm -> xmlDirField = fm); + xmlDirField = (TextField) xmlRow.getChildren().get(1); + HBox mapperRow = createDirRow("Mapper 接口", fm -> mapperDirField = fm); + mapperDirField = (TextField) mapperRow.getChildren().get(1); + + loadDefaultDirs(); + + box.getChildren().addAll(dirLabel, modelRow, xmlRow, mapperRow); + return box; + } + + private HBox createDirRow(String label, java.util.function.Consumer fieldRef) { + HBox row = new HBox(8); + row.setAlignment(Pos.CENTER_LEFT); + + Label lbl = new Label(label + ":"); + lbl.setPrefWidth(100); + lbl.setFont(Font.font("Microsoft YaHei", 12)); + + TextField field = new TextField(); + field.setPrefWidth(360); + HBox.setHgrow(field, Priority.ALWAYS); + + Button browseBtn = new Button("浏览..."); + browseBtn.setOnAction(e -> onBrowseDir(field)); + + if (fieldRef != null) fieldRef.accept(field); + row.getChildren().addAll(lbl, field, browseBtn); + return row; + } + + private void loadDefaultDirs() { + Properties p = new Properties(); + try (InputStream is = getClass().getClassLoader().getResourceAsStream("mybatis-generator.properties")) { + if (is != null) p.load(is); + } catch (Exception ignored) {} + + setDirField(modelDirField, p.getProperty("model_target_project", "./generator")); + setDirField(xmlDirField, p.getProperty("sqlmapper_target_project", "./generator")); + setDirField(mapperDirField, p.getProperty("dao_target_project", "./generator")); + } + + private void setDirField(TextField field, String dir) { + if (field == null || dir == null) return; + if (dir.startsWith("./") || dir.startsWith(".\\")) { + field.setText(new File(dir).getAbsolutePath().replace('\\', '/')); + } else { + field.setText(dir.replace('\\', '/')); + } + } + + // ==================== 事件 ==================== + + private void onBrowseDir(TextField targetField) { + DirectoryChooser chooser = new DirectoryChooser(); + chooser.setTitle("选择目录"); + + String current = targetField.getText(); + if (current != null && !current.isEmpty()) { + File initDir = new File(current); + if (initDir.exists() && initDir.isDirectory()) { + chooser.setInitialDirectory(initDir); + } + } + + if (chooser.getInitialDirectory() == null) { + try { + List paths = fileOperationHelper.getProjectPaths(); + if (!paths.isEmpty()) { + File firstProj = new File(paths.get(0)); + if (firstProj.exists() && firstProj.isDirectory()) { + chooser.setInitialDirectory(firstProj); + } + } + } catch (Exception ignored) {} + } + + File selected = chooser.showDialog(stage); + if (selected != null) { + targetField.setText(selected.getAbsolutePath().replace('\\', '/')); + } + } + + private void onQueryTables() { + String pattern = patternField.getText().trim(); + if (pattern.isEmpty()) { + showAlert("请输入表名通配符"); + return; + } + + try { + List tables = callback.queryTables(pattern); + tableItems.clear(); + if (tables.isEmpty()) { + showAlert("未找到匹配通配符 [" + pattern + "] 的表"); + } else { + for (String t : tables) { + tableItems.add(new TableItem(t, true)); + } + } + } catch (Exception e) { + showAlert("查询失败: " + e.getMessage()); + } + } + + private void onGenerate() { + List selectedTables = tableItems.stream() + .filter(TableItem::isSelected) + .map(TableItem::getTableName) + .collect(Collectors.toList()); + + if (selectedTables.isEmpty()) { + showAlert("请至少选择一个表"); + return; + } + + String modelDir = getDirText(modelDirField); + String xmlDir = getDirText(xmlDirField); + String mapperDir = getDirText(mapperDirField); + + try { + String result = callback.generate(selectedTables, modelDir, xmlDir, mapperDir); + showInfo("生成结果", result); + } catch (Exception e) { + showAlert("生成失败: " + e.getMessage()); + e.printStackTrace(); + } + } + + private String getDirText(TextField field) { + String text = field == null ? "" : field.getText().trim(); + return text.isEmpty() ? "./generator" : text; + } + + // ---- 底部按钮栏 ---- + private HBox createButtonBar() { + HBox box = new HBox(15); + box.setAlignment(Pos.CENTER_RIGHT); + box.setPadding(new Insets(10, 0, 0, 0)); + + Button generateBtn = new Button("生成"); + generateBtn.setPrefWidth(100); + generateBtn.setStyle("-fx-background-color: #4caf50; -fx-text-fill: white; -fx-font-size: 14px;"); + generateBtn.setOnAction(e -> onGenerate()); + + Button cancelBtn = new Button("取消"); + cancelBtn.setPrefWidth(80); + cancelBtn.setOnAction(e -> close()); + + box.getChildren().addAll(generateBtn, cancelBtn); + return box; + } + + // ==================== 对话框 ==================== + + private void showAlert(String msg) { + Platform.runLater(() -> { + Alert alert = new Alert(Alert.AlertType.WARNING); + alert.setTitle("提示"); + alert.setHeaderText(null); + alert.setContentText(msg); + alert.initOwner(stage); + alert.showAndWait(); + }); + } + + private void showInfo(String title, String msg) { + Platform.runLater(() -> { + Stage dialog = new Stage(); + dialog.initOwner(stage); + dialog.setTitle(title); + dialog.setResizable(true); + + TextArea textArea = new TextArea(msg); + textArea.setEditable(false); + textArea.setWrapText(true); + textArea.setFont(Font.font("Consolas", 12)); + + Button closeBtn = new Button("关闭"); + closeBtn.setOnAction(e -> dialog.close()); + + HBox btnBar = new HBox(closeBtn); + btnBar.setAlignment(Pos.CENTER_RIGHT); + btnBar.setPadding(new Insets(8)); + btnBar.setStyle("-fx-background-color: #f0f0f0;"); + + VBox root = new VBox(textArea, btnBar); + VBox.setVgrow(textArea, Priority.ALWAYS); + + Scene scene = new Scene(root, 580, 420); + dialog.setScene(scene); + dialog.showAndWait(); + }); + } + + private void close() { + closeLatch.countDown(); + stage.close(); + } +} diff --git a/src/main/java/com/example/workutils/biz/mybatisgenerator/service/generator/MyBatisGeneratorService.java b/src/main/java/com/example/workutils/biz/mybatisgenerator/service/generator/MyBatisGeneratorService.java index 1d01f34..9c08dd7 100644 --- a/src/main/java/com/example/workutils/biz/mybatisgenerator/service/generator/MyBatisGeneratorService.java +++ b/src/main/java/com/example/workutils/biz/mybatisgenerator/service/generator/MyBatisGeneratorService.java @@ -33,7 +33,15 @@ public class MyBatisGeneratorService { * @return 生成结果信息 */ public String generateByTableNames(List tableNames, boolean overwrite) throws Exception { - return generateInternal(tableNames, overwrite); + return generateInternal(tableNames, overwrite, null, null, null); + } + + /** + * 根据表名列表 + 自定义输出目录生成Mapper文件 + */ + public String generateByTableNames(List tableNames, boolean overwrite, + String modelTarget, String xmlTarget, String mapperTarget) throws Exception { + return generateInternal(tableNames, overwrite, modelTarget, xmlTarget, mapperTarget); } /** @@ -52,7 +60,7 @@ public class MyBatisGeneratorService { } log.info("通配符 [{}] 匹配到 {} 张表: {}", pattern, matchedTables.size(), matchedTables); - return generateInternal(matchedTables, overwrite); + return generateInternal(matchedTables, overwrite, null, null, null); } /** @@ -132,11 +140,15 @@ public class MyBatisGeneratorService { /** * 内部生成方法 */ - private String generateInternal(List tableNames, boolean overwrite) throws Exception { + private String generateInternal(List tableNames, boolean overwrite, + String modelTarget, String xmlTarget, String mapperTarget) throws Exception { // 确保 targetProject 目录存在,避免 "does not exist" 警告 - Properties props = getConfigurationProperties(); - for (String key : new String[]{"model_target_project", "sqlmapper_target_project", "dao_target_project"}) { - String dir = props.getProperty(key); + String[] dirs = { + modelTarget != null ? modelTarget : getConfigurationProperties().getProperty("model_target_project", "./generator"), + xmlTarget != null ? xmlTarget : getConfigurationProperties().getProperty("sqlmapper_target_project", "./generator"), + mapperTarget != null ? mapperTarget : getConfigurationProperties().getProperty("dao_target_project", "./generator") + }; + for (String dir : dirs) { if (dir != null && !dir.isEmpty()) { File targetDir = new File(dir); if (!targetDir.exists()) { @@ -146,7 +158,10 @@ public class MyBatisGeneratorService { } } - File tempConfigFile = createTempConfigWithTables(tableNames); + File tempConfigFile = createTempConfigWithTables(tableNames, + modelTarget != null ? dirs[0] : null, + xmlTarget != null ? dirs[1] : null, + mapperTarget != null ? dirs[2] : null); try { List warnings = new ArrayList<>(); @@ -180,7 +195,8 @@ public class MyBatisGeneratorService { /** * 创建临时配置文件 */ - private File createTempConfigWithTables(List tableNames) throws Exception { + private File createTempConfigWithTables(List tableNames, + String modelTarget, String xmlTarget, String mapperTarget) throws Exception { InputStream is = getClass().getClassLoader().getResourceAsStream(CONFIG_FILE); if (is == null) { throw new RuntimeException("无法找到配置文件: " + CONFIG_FILE); @@ -208,6 +224,65 @@ public class MyBatisGeneratorService { ); content = tablePattern.matcher(content).replaceAll(""); + // 如果指定了自定义输出目录,覆盖 XML 中的 targetProject 和 targetPackage + // MBG 会在 targetProject 下按 targetPackage 创建子目录,所以: + // targetProject = src/main/java(或 src/main/resources)根目录 + // targetPackage = 其后的包名路径 + String modelPkg = null; + String mapperPkg = null; + + if (modelTarget != null) { + String[] resolved = resolveTargetAndPackage(modelTarget, "src/main/java/"); + if (resolved != null) { + content = content.replaceFirst( + "(]*targetProject=\")[^\"]*(\")", + "$1" + java.util.regex.Matcher.quoteReplacement(resolved[0]) + "$2"); + modelPkg = resolved[1]; + content = content.replaceFirst( + "(]*targetPackage=\")[^\"]*(\")", + "$1" + java.util.regex.Matcher.quoteReplacement(modelPkg) + "$2"); + } else { + // 路径中不含 src/main/java,回退为仅替换 targetProject + content = content.replaceFirst( + "(]*targetProject=\")[^\"]*(\")", + "$1" + java.util.regex.Matcher.quoteReplacement(modelTarget.replace('\\', '/')) + "$2"); + } + } + if (mapperTarget != null) { + String[] resolved = resolveTargetAndPackage(mapperTarget, "src/main/java/"); + if (resolved != null) { + content = content.replaceFirst( + "(]*targetProject=\")[^\"]*(\")", + "$1" + java.util.regex.Matcher.quoteReplacement(resolved[0]) + "$2"); + mapperPkg = resolved[1]; + content = content.replaceFirst( + "(]*targetPackage=\")[^\"]*(\")", + "$1" + java.util.regex.Matcher.quoteReplacement(mapperPkg) + "$2"); + } else { + content = content.replaceFirst( + "(]*targetProject=\")[^\"]*(\")", + "$1" + java.util.regex.Matcher.quoteReplacement(mapperTarget.replace('\\', '/')) + "$2"); + } + } + if (xmlTarget != null) { + String[] resolved = resolveTargetAndPackage(xmlTarget, "src/main/resources/"); + if (resolved != null) { + content = content.replaceFirst( + "(]*targetProject=\")[^\"]*(\")", + "$1" + java.util.regex.Matcher.quoteReplacement(resolved[0]) + "$2"); + } else { + content = content.replaceFirst( + "(]*targetProject=\")[^\"]*(\")", + "$1" + java.util.regex.Matcher.quoteReplacement(xmlTarget.replace('\\', '/')) + "$2"); + } + } + // sqlmap 的 targetPackage(即 XML namespace)应与 Mapper 接口包名一致 + if (mapperPkg != null) { + content = content.replaceFirst( + "(]*targetPackage=\")[^\"]*(\")", + "$1" + java.util.regex.Matcher.quoteReplacement(mapperPkg) + "$2"); + } + StringBuilder tableElements = new StringBuilder(); for (String tableName : tableNames) { tableElements.append(" + * MBG 默认对含复合主键的表生成两个类: + *
    + *
  • XXXPO(完整 PO,含所有字段)
  • + *
  • XXXPOKey(仅主键字段)
  • + *
+ * 本插件在初始化时将 {@code hasPrimaryKeyColumns} 重置为 false, + * 使 MBG 认为该表没有需要单独提取的主键类, + * 从而只生成一个包含所有字段的 PO 类。 + */ +public class SuppressKeyPlugin extends PluginAdapter { + + private boolean suppressKey; + + @Override + public void setProperties(Properties properties) { + super.setProperties(properties); + suppressKey = !"false".equalsIgnoreCase(properties.getProperty("suppressKey")); + } + + @Override + public boolean validate(List warnings) { + return true; + } + + /** + * 在表内省完成后,清除主键列标记。 + * MBG 仅当表含复合主键且 {@code getHasPrimaryKeyColumns()==true} + * 时才会生成独立的 *Key 类;置为 false 后跳过 Key 类生成。 + */ + @Override + public void initialized(IntrospectedTable introspectedTable) { + if (!suppressKey) return; + + // 通过反射将 hasInternalPrimaryKey 强制设为 false, + // 阻止 MBG 生成单独的 PrimaryKey 类 + try { + java.lang.reflect.Field field = introspectedTable.getClass().getDeclaredField("hasInternalPrimaryKey"); + field.setAccessible(true); + field.set(introspectedTable, false); + } catch (Exception e) { + // 反射失败时静默忽略,回退到默认行为 + } + } +} diff --git a/src/main/java/com/example/workutils/system/support/JfxBootstrap.java b/src/main/java/com/example/workutils/system/support/JfxBootstrap.java new file mode 100644 index 0000000..e460c3a --- /dev/null +++ b/src/main/java/com/example/workutils/system/support/JfxBootstrap.java @@ -0,0 +1,61 @@ +package com.example.workutils.system.support; + +import javafx.application.Application; +import javafx.application.Platform; +import javafx.stage.Stage; + +import java.util.concurrent.CountDownLatch; + +/** + * 共享的 JavaFX 启动器 —— 全局仅初始化一次 JavaFX 运行时 + *

+ * 由于 {@link Application#launch} 每个 JVM 只能调用一次, + * 所有需要 JavaFX 的插件复用此启动器,避免重复启动。 + */ +public final class JfxBootstrap { + + private static final CountDownLatch READY = new CountDownLatch(1); + private static volatile boolean failed; + private static volatile boolean launched; + + private JfxBootstrap() {} + + static { + launchOnce(); + } + + private static synchronized void launchOnce() { + if (launched) return; + launched = true; + Thread t = new Thread(() -> { + try { + Platform.setImplicitExit(false); + Application.launch(DummyApp.class); + } catch (Exception e) { + failed = true; + } finally { + READY.countDown(); + } + }, "JavaFX-Bootstrap"); + t.setDaemon(true); + t.start(); + } + + /** 阻塞等待 JavaFX 运行时就绪 */ + public static void await() throws InterruptedException { + READY.await(); + } + + /** JavaFX 是否初始化失败 */ + public static boolean isFailed() { + return failed; + } + + /** 占位 Application,仅用于触发 JavaFX 工具包初始化 */ + public static class DummyApp extends Application { + @Override + public void start(Stage stage) { + READY.countDown(); + } + } +} diff --git a/src/main/resources/mybatis-generator.xml b/src/main/resources/mybatis-generator.xml index 906e741..681ffb6 100644 --- a/src/main/resources/mybatis-generator.xml +++ b/src/main/resources/mybatis-generator.xml @@ -13,6 +13,8 @@ + +