diff --git a/CIRCULAR_DEPENDENCY_SOLUTION.md b/CIRCULAR_DEPENDENCY_SOLUTION.md new file mode 100644 index 0000000..db28ffb --- /dev/null +++ b/CIRCULAR_DEPENDENCY_SOLUTION.md @@ -0,0 +1,58 @@ +# 循环依赖解决方案 + +## 当前问题 + +在应用程序上下文中存在循环依赖: + +``` +┌─────┐ +| printController (field private com.goeing.printserver.main.service.PrintQueueService com.goeing.printserver.main.PrintController.printQueueService) +↑ ↓ +| printQueueService (field private com.goeing.printserver.main.PrintController com.goeing.printserver.main.service.PrintQueueService.printController) +└─────┘ +``` + +## 临时解决方案 + +已在 `application.properties` 中添加以下配置,临时允许循环依赖: + +```properties +spring.main.allow-circular-references=true +``` + +## 已实施的重构步骤 + +1. 创建了 `PrintService` 接口,定义打印相关操作 +2. 修改 `PrintController` 实现 `PrintService` 接口 +3. 修改 `PrintQueueService` 依赖 `PrintService` 而不是直接依赖 `PrintController` + +## 后续重构建议 + +为彻底解决循环依赖问题,建议进一步重构: + +### 方案一:使用事件驱动架构 + +1. 使用 Spring 的事件机制(ApplicationEventPublisher)替代直接方法调用 +2. `PrintQueueService` 发布打印事件 +3. `PrintController` 订阅并处理这些事件 + +### 方案二:引入服务层抽象 + +1. 创建更完整的服务层抽象,明确职责分离 +2. 将 `PrintController` 中的业务逻辑移至专门的服务类 +3. 让 `PrintController` 和 `PrintQueueService` 都依赖这个新服务类 + +### 方案三:重新设计组件职责 + +1. 重新评估 `PrintController` 和 `PrintQueueService` 的职责 +2. 可能的职责划分: + - `PrintController`:仅处理 HTTP 请求和响应 + - `PrintQueueService`:管理打印队列和执行打印操作 + - 新增 `PrintExecutionService`:实际执行打印操作的逻辑 + +## 最佳实践 + +- 遵循单一职责原则,每个类只负责一个功能领域 +- 使用依赖注入,但避免双向依赖 +- 考虑使用事件驱动架构处理组件间通信 +- 使用接口进行解耦,降低组件间直接依赖 \ No newline at end of file diff --git a/docs/MACOS_HEADLESS_SOLUTION.md b/docs/MACOS_HEADLESS_SOLUTION.md new file mode 100644 index 0000000..c455f69 --- /dev/null +++ b/docs/MACOS_HEADLESS_SOLUTION.md @@ -0,0 +1,76 @@ +# macOS 系统上的 HeadlessException 解决方案 + +## 问题描述 + +在 macOS 系统上运行打印服务器时,可能会遇到 `java.awt.HeadlessException` 错误,错误信息类似: + +``` +Exception in thread "AWT-EventQueue-0" java.awt.HeadlessException + at java.desktop/java.awt.GraphicsEnvironment.checkHeadless(GraphicsEnvironment.java:166) + at java.desktop/java.awt.Window.(Window.java:553) + at java.desktop/java.awt.Frame.(Frame.java:428) + at java.desktop/javax.swing.JFrame.(JFrame.java:224) + at com.goeing.printserver.main.gui.PrintQueueGUI.initializeGUI(PrintQueueGUI.java:58) +``` + +这个错误表明应用程序在无头模式(Headless Mode)下运行,但尝试创建图形界面组件。在 macOS 系统上,特别是在某些环境下(如远程会话、无显示器连接或特定的系统配置),Java 应用程序可能会自动进入无头模式。 + +## 解决方案 + +我们已经对应用程序进行了优化,以更好地处理无头模式。现在有以下几种方式可以解决这个问题: + +### 1. 使用命令行参数启用无头模式 + +如果您知道系统不支持图形界面,可以在启动应用程序时明确指定无头模式: + +```bash +java -Djava.awt.headless=true -jar goeingPrintServer.jar +``` + +### 2. 通过配置文件设置 + +在 `application.properties` 文件中,我们添加了一个配置项: + +```properties +# 在macOS系统上,如果遇到HeadlessException,可以设置为true强制使用无头模式 +app.force.headless=false +``` + +将此值设置为 `true` 可以强制应用程序以无头模式运行。 + +### 3. 自动检测和适应 + +应用程序现在会自动检测系统是否支持图形界面,并在不支持时自动切换到无头模式。在无头模式下: + +- 图形界面组件不会被初始化 +- 系统托盘图标不会显示 +- 通知功能将被禁用 +- 打印功能仍然正常工作 + +## 无头模式下的功能 + +在无头模式下,应用程序仍然可以通过以下方式使用: + +1. **REST API**:所有打印功能都可以通过 REST API 访问 +2. **WebSocket**:打印请求可以通过 WebSocket 连接发送 +3. **命令行**:可以通过命令行工具与应用程序交互 + +## 日志输出 + +当应用程序检测到无头模式时,会在日志中输出相关信息: + +``` +当前环境不支持图形界面,将以无头模式运行 +系统运行在无头模式下,通知功能将被禁用 +``` + +## 技术说明 + +我们通过以下方式改进了应用程序对无头模式的处理: + +1. 在应用启动时检测系统环境 +2. 在检测到无头模式时设置系统属性 `app.headless.mode=true` +3. 所有图形界面组件在初始化前检查此属性 +4. 添加了异常处理,防止图形界面初始化失败导致整个应用崩溃 + +这些改进确保了应用程序在各种环境下都能稳定运行,无论是否支持图形界面。 \ No newline at end of file diff --git a/src/main/java/com/goeing/printserver/GoeingPrintServerApplication.java b/src/main/java/com/goeing/printserver/GoeingPrintServerApplication.java index ef734eb..4e6dad1 100644 --- a/src/main/java/com/goeing/printserver/GoeingPrintServerApplication.java +++ b/src/main/java/com/goeing/printserver/GoeingPrintServerApplication.java @@ -2,12 +2,47 @@ package com.goeing.printserver; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.core.env.Environment; +import org.springframework.context.ConfigurableApplicationContext; +import java.awt.GraphicsEnvironment; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; @SpringBootApplication public class GoeingPrintServerApplication { + + private static final Logger log = LoggerFactory.getLogger(GoeingPrintServerApplication.class); public static void main(String[] args) { - SpringApplication.run(GoeingPrintServerApplication.class, args); + // 检查是否在macOS系统上运行 + String osName = System.getProperty("os.name").toLowerCase(); + boolean isMacOS = osName.contains("mac"); + + // 检查是否已经设置了java.awt.headless系统属性 + String headlessProperty = System.getProperty("java.awt.headless"); + + // 如果是macOS并且没有明确设置headless属性,可能需要特殊处理 + if (isMacOS && headlessProperty == null) { + log.info("在macOS系统上运行,检查是否需要启用无头模式"); + + // 检查是否支持图形界面 + if (GraphicsEnvironment.isHeadless()) { + log.warn("检测到系统不支持图形界面,自动启用无头模式"); + System.setProperty("java.awt.headless", "true"); + System.setProperty("app.headless.mode", "true"); + } + } + + ConfigurableApplicationContext context = SpringApplication.run(GoeingPrintServerApplication.class, args); + + // 从配置中读取是否强制使用无头模式 + Environment env = context.getEnvironment(); + boolean forceHeadless = Boolean.parseBoolean(env.getProperty("app.force.headless", "false")); + + if (forceHeadless) { + log.info("根据配置强制启用无头模式"); + System.setProperty("java.awt.headless", "true"); + System.setProperty("app.headless.mode", "true"); + } } - } diff --git a/src/main/java/com/goeing/printserver/main/PrintController.java b/src/main/java/com/goeing/printserver/main/PrintController.java index cce3ce0..221a33a 100644 --- a/src/main/java/com/goeing/printserver/main/PrintController.java +++ b/src/main/java/com/goeing/printserver/main/PrintController.java @@ -3,11 +3,15 @@ package com.goeing.printserver.main; import cn.hutool.core.util.IdUtil; import cn.hutool.http.HttpUtil; import cn.hutool.json.JSONUtil; +import com.goeing.printserver.main.config.PrintServerConfig; import com.goeing.printserver.main.domain.request.PrintRequest; +import com.goeing.printserver.main.service.PrintQueueService; +import com.goeing.printserver.main.service.PrintService; import com.goeing.printserver.main.utils.PdfPrinter; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; -import javax.print.PrintService; +// 使用完全限定名称避免与自定义PrintService接口冲突 import java.awt.print.PrinterJob; import java.io.File; import java.util.Arrays; @@ -19,7 +23,14 @@ import java.util.stream.Collectors; @RestController @RequestMapping("/api") -public class PrintController { +public class PrintController implements PrintService { + + @Autowired + private PrintQueueService printQueueService; + + @Autowired + private PrintServerConfig config; + private final String rootPath = "/Users/fl0919/work_space/goeingPrintServer/pdf"; /** @@ -29,8 +40,8 @@ public class PrintController { */ @GetMapping("printerList") public List printerList() { - PrintService[] printServices = PrinterJob.lookupPrintServices(); - Set collect = Arrays.stream(printServices).map(PrintService::getName).collect(Collectors.toSet()); + javax.print.PrintService[] printServices = PrinterJob.lookupPrintServices(); + Set collect = Arrays.stream(printServices).map(service -> service.getName()).collect(Collectors.toSet()); return collect.stream().sorted().collect(Collectors.toList()); } @@ -51,6 +62,34 @@ public class PrintController { } + /** + * 获取当前打印队列状态 + * + * @return 包含队列信息的Map + */ + @GetMapping("queue/status") + public Map getQueueStatus() { + Map status = new HashMap<>(); + status.put("queueSize", printQueueService.getQueueSize()); + status.put("timestamp", System.currentTimeMillis()); + status.put("currentTask", printQueueService.getCurrentTaskInfo()); + return status; + } + + /** + * 获取打印队列中的所有任务 + * + * @return 任务列表 + */ + @GetMapping("queue/tasks") + public Map getQueueTasks() { + Map result = new HashMap<>(); + result.put("currentTask", printQueueService.getCurrentTaskInfo()); + result.put("queuedTasks", printQueueService.getQueuedTasksInfo()); + result.put("timestamp", System.currentTimeMillis()); + return result; + } + @PostMapping("print") public String print(@RequestBody PrintRequest request) { // 记录请求信息 @@ -65,8 +104,14 @@ public class PrintController { throw new IllegalArgumentException("File URL cannot be null or empty"); } + // 如果打印机名称为空,使用默认打印机 if (request.getPrinterName() == null || request.getPrinterName().trim().isEmpty()) { - throw new IllegalArgumentException("Printer name cannot be null or empty"); + String defaultPrinter = config.getDefaultPrinter(); + if (defaultPrinter == null || defaultPrinter.trim().isEmpty()) { + throw new IllegalArgumentException("Printer name cannot be null or empty, and no default printer is configured"); + } + request.setPrinterName(defaultPrinter); + System.out.println("Using default printer: " + defaultPrinter); } // 验证打印机是否存在 diff --git a/src/main/java/com/goeing/printserver/main/config/PrintServerConfig.java b/src/main/java/com/goeing/printserver/main/config/PrintServerConfig.java new file mode 100644 index 0000000..75da53d --- /dev/null +++ b/src/main/java/com/goeing/printserver/main/config/PrintServerConfig.java @@ -0,0 +1,122 @@ +package com.goeing.printserver.main.config; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import jakarta.annotation.PostConstruct; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Properties; + +/** + * 打印服务器配置类 + * 负责管理打印服务器的所有配置信息,包括默认打印机、最大队列大小、通知设置等 + */ +@Component +@Slf4j +@Data +public class PrintServerConfig { + + private static final String CONFIG_FILE = "printserver.properties"; + + // 默认配置值 + private static final String DEFAULT_PRINTER = "默认打印机"; + private static final int DEFAULT_MAX_QUEUE_SIZE = 10; + private static final boolean DEFAULT_ENABLE_NOTIFICATIONS = true; + private static final boolean DEFAULT_START_MINIMIZED = false; + private static final boolean DEFAULT_AUTO_START = false; + + // 配置属性 + private String defaultPrinter = DEFAULT_PRINTER; + private int maxQueueSize = DEFAULT_MAX_QUEUE_SIZE; + private boolean enableNotifications = DEFAULT_ENABLE_NOTIFICATIONS; + private boolean startMinimized = DEFAULT_START_MINIMIZED; + private boolean autoStart = DEFAULT_AUTO_START; + + private Properties properties = new Properties(); + private File configFile; + + @PostConstruct + public void init() { + // 确定配置文件路径 + String userHome = System.getProperty("user.home"); + configFile = new File(userHome + File.separator + ".goeing" + File.separator + CONFIG_FILE); + + // 确保目录存在 + if (!configFile.getParentFile().exists()) { + configFile.getParentFile().mkdirs(); + } + + // 加载配置 + loadConfig(); + } + + /** + * 加载配置 + */ + public void loadConfig() { + if (configFile.exists()) { + try (FileInputStream fis = new FileInputStream(configFile)) { + properties.load(fis); + + // 读取配置值 + defaultPrinter = properties.getProperty("defaultPrinter", DEFAULT_PRINTER); + maxQueueSize = Integer.parseInt(properties.getProperty("maxQueueSize", String.valueOf(DEFAULT_MAX_QUEUE_SIZE))); + enableNotifications = Boolean.parseBoolean(properties.getProperty("enableNotifications", String.valueOf(DEFAULT_ENABLE_NOTIFICATIONS))); + startMinimized = Boolean.parseBoolean(properties.getProperty("startMinimized", String.valueOf(DEFAULT_START_MINIMIZED))); + autoStart = Boolean.parseBoolean(properties.getProperty("autoStart", String.valueOf(DEFAULT_AUTO_START))); + + log.info("配置已加载: {}", configFile.getAbsolutePath()); + } catch (IOException e) { + log.error("加载配置文件失败", e); + // 使用默认值 + resetToDefaults(); + } catch (NumberFormatException e) { + log.error("解析配置值失败", e); + // 使用默认值 + resetToDefaults(); + } + } else { + log.info("配置文件不存在,使用默认配置"); + // 使用默认值并保存 + resetToDefaults(); + saveConfig(); + } + } + + /** + * 保存配置 + */ + public void saveConfig() { + try { + // 更新属性 + properties.setProperty("defaultPrinter", defaultPrinter); + properties.setProperty("maxQueueSize", String.valueOf(maxQueueSize)); + properties.setProperty("enableNotifications", String.valueOf(enableNotifications)); + properties.setProperty("startMinimized", String.valueOf(startMinimized)); + properties.setProperty("autoStart", String.valueOf(autoStart)); + + // 保存到文件 + try (FileOutputStream fos = new FileOutputStream(configFile)) { + properties.store(fos, "Goeing Print Server Configuration"); + log.info("配置已保存: {}", configFile.getAbsolutePath()); + } + } catch (IOException e) { + log.error("保存配置文件失败", e); + } + } + + /** + * 重置为默认配置 + */ + public void resetToDefaults() { + defaultPrinter = DEFAULT_PRINTER; + maxQueueSize = DEFAULT_MAX_QUEUE_SIZE; + enableNotifications = DEFAULT_ENABLE_NOTIFICATIONS; + startMinimized = DEFAULT_START_MINIMIZED; + autoStart = DEFAULT_AUTO_START; + } +} \ No newline at end of file diff --git a/src/main/java/com/goeing/printserver/main/domain/PrintTask.java b/src/main/java/com/goeing/printserver/main/domain/PrintTask.java new file mode 100644 index 0000000..0da0783 --- /dev/null +++ b/src/main/java/com/goeing/printserver/main/domain/PrintTask.java @@ -0,0 +1,55 @@ +package com.goeing.printserver.main.domain; + +import com.goeing.printserver.main.domain.bo.PrintOption; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; + +/** + * 打印任务实体类,用于表示一个打印任务 + */ +@Data +public class PrintTask { + // 文件URL + private String fileUrl; + + // 打印机名称 + private String printer; + + // 任务状态:queued, processing, completed, failed + private String status; + + // 排队时间 + private LocalDateTime queuedTime; + + // 开始处理时间 + private LocalDateTime startTime; + + // 结束时间 + private LocalDateTime endTime; + + // 打印选项 + private PrintOption printOptions; + + /** + * 将任务转换为Map + * + * @return 包含任务信息的Map + */ + public Map toMap() { + Map map = new HashMap<>(); + map.put("fileUrl", fileUrl); + map.put("printerName", printer); + map.put("status", status); + map.put("queuedTime", queuedTime); + if (startTime != null) { + map.put("startTime", startTime); + } + if (endTime != null) { + map.put("endTime", endTime); + } + return map; + } +} \ No newline at end of file diff --git a/src/main/java/com/goeing/printserver/main/gui/AboutDialog.java b/src/main/java/com/goeing/printserver/main/gui/AboutDialog.java new file mode 100644 index 0000000..db00526 --- /dev/null +++ b/src/main/java/com/goeing/printserver/main/gui/AboutDialog.java @@ -0,0 +1,137 @@ +package com.goeing.printserver.main.gui; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.image.BufferedImage; + +/** + * 关于对话框,显示应用程序信息 + */ +public class AboutDialog extends JDialog { + + /** + * 创建关于对话框 + * + * @param parent 父窗口 + */ + public AboutDialog(Window parent) { + super(parent, "关于打印服务器", ModalityType.APPLICATION_MODAL); + initializeUI(); + } + + /** + * 初始化用户界面 + */ + private void initializeUI() { + setSize(400, 300); + setLocationRelativeTo(getOwner()); + setResizable(false); + setLayout(new BorderLayout()); + + // 创建图标面板 + JPanel iconPanel = new JPanel(new FlowLayout(FlowLayout.CENTER)); + iconPanel.setBorder(BorderFactory.createEmptyBorder(20, 0, 10, 0)); + + // 创建应用图标 + ImageIcon appIcon = createAppIcon(); + JLabel iconLabel = new JLabel(appIcon); + iconPanel.add(iconLabel); + + add(iconPanel, BorderLayout.NORTH); + + // 创建信息面板 + JPanel infoPanel = new JPanel(); + infoPanel.setLayout(new BoxLayout(infoPanel, BoxLayout.Y_AXIS)); + infoPanel.setBorder(BorderFactory.createEmptyBorder(10, 20, 10, 20)); + + // 添加应用信息 + addInfoLabel(infoPanel, "打印服务器", Font.BOLD, 16); + addInfoLabel(infoPanel, "版本: 1.0.0", Font.PLAIN, 12); + addInfoLabel(infoPanel, "构建日期: " + java.time.LocalDate.now().toString(), Font.PLAIN, 12); + addInfoLabel(infoPanel, "JDK版本: " + System.getProperty("java.version"), Font.PLAIN, 12); + addInfoLabel(infoPanel, "操作系统: " + System.getProperty("os.name") + " " + System.getProperty("os.version"), Font.PLAIN, 12); + + // 添加空白间隔 + infoPanel.add(Box.createVerticalStrut(10)); + + // 添加版权信息 + addInfoLabel(infoPanel, "© " + java.time.Year.now().getValue() + " Goeing. 保留所有权利。", Font.ITALIC, 12); + + add(infoPanel, BorderLayout.CENTER); + + // 创建按钮面板 + JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER)); + JButton closeButton = new JButton("关闭"); + closeButton.addActionListener(e -> dispose()); + buttonPanel.add(closeButton); + + add(buttonPanel, BorderLayout.SOUTH); + } + + /** + * 添加信息标签 + * + * @param panel 面板 + * @param text 文本 + * @param fontStyle 字体样式 + * @param fontSize 字体大小 + */ + private void addInfoLabel(JPanel panel, String text, int fontStyle, int fontSize) { + JLabel label = new JLabel(text); + label.setFont(new Font(label.getFont().getName(), fontStyle, fontSize)); + label.setAlignmentX(0.5f); // CENTER_ALIGNMENT = 0.5f + panel.add(label); + panel.add(Box.createVerticalStrut(5)); + } + + /** + * 创建应用图标 + * + * @return 图标 + */ + private ImageIcon createAppIcon() { + // 创建一个简单的打印机图标 + int iconSize = 64; + BufferedImage image = new BufferedImage(iconSize, iconSize, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2d = image.createGraphics(); + + // 启用抗锯齿 + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + // 绘制打印机主体 + g2d.setColor(new Color(50, 50, 50)); + g2d.fillRect(10, 25, 44, 25); + + // 绘制打印机纸盒 + g2d.setColor(new Color(200, 200, 200)); + g2d.fillRect(15, 50, 34, 5); + + // 绘制打印机出纸口 + g2d.setColor(new Color(240, 240, 240)); + g2d.fillRect(15, 15, 34, 10); + + // 绘制打印纸 + g2d.setColor(Color.WHITE); + g2d.fillRect(20, 5, 24, 20); + + // 绘制打印机按钮 + g2d.setColor(new Color(0, 150, 200)); + g2d.fillOval(45, 30, 5, 5); + + g2d.dispose(); + + return new ImageIcon(image); + } + + /** + * 显示关于对话框 + * + * @param parent 父窗口 + */ + public static void showDialog(Window parent) { + AboutDialog dialog = new AboutDialog(parent); + dialog.setVisible(true); + } +} \ No newline at end of file diff --git a/src/main/java/com/goeing/printserver/main/gui/GUILauncher.java b/src/main/java/com/goeing/printserver/main/gui/GUILauncher.java new file mode 100644 index 0000000..4d4f6b9 --- /dev/null +++ b/src/main/java/com/goeing/printserver/main/gui/GUILauncher.java @@ -0,0 +1,85 @@ +package com.goeing.printserver.main.gui; + +import jakarta.annotation.PreDestroy; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +import java.awt.*; + +/** + * GUI启动器,负责在Spring Boot应用程序启动后初始化图形界面 + */ +@Component +@Slf4j +public class GUILauncher { + + private final PrintQueueGUI printQueueGUI; + private final PrintServerTray printServerTray; + private final PrintSettingsPanel settingsPanel; + + @Autowired + public GUILauncher(PrintQueueGUI printQueueGUI, PrintServerTray printServerTray, PrintSettingsPanel settingsPanel) { + this.printQueueGUI = printQueueGUI; + this.printServerTray = printServerTray; + this.settingsPanel = settingsPanel; + } + + /** + * 在应用程序准备就绪后启动GUI + */ + @EventListener(ApplicationReadyEvent.class) + public void launchGUI() { + log.info("应用程序已准备就绪,正在启动图形界面..."); + + // 检查是否支持图形界面 + boolean isHeadless = GraphicsEnvironment.isHeadless(); + if (isHeadless) { + log.warn("当前环境不支持图形界面,将以无头模式运行"); + // 在无头模式下,设置一个系统属性,其他组件可以检查这个属性 + System.setProperty("app.headless.mode", "true"); + return; + } + + try { + // 初始化GUI和系统托盘 + printServerTray.initialize(); + + // 根据设置决定是否显示主窗口 + if (!settingsPanel.isStartMinimized()) { + printQueueGUI.show(); + } else { + log.info("根据用户设置,应用程序启动时最小化到系统托盘"); + } + + // 显示欢迎通知 + printServerTray.displayMessage( + "打印服务器已启动", + "打印服务器正在后台运行,点击托盘图标可打开主窗口", + TrayIcon.MessageType.INFO + ); + + // 更新托盘提示 + printServerTray.updateTooltip("打印服务器 - 运行中"); + + log.info("图形界面已启动"); + } catch (Exception e) { + log.error("初始化图形界面时发生错误: {}", e.getMessage()); + log.warn("将以无头模式运行"); + System.setProperty("app.headless.mode", "true"); + } + } + + /** + * 在应用程序关闭时释放资源 + */ + @PreDestroy + public void shutdown() { + log.info("正在关闭图形界面..."); + printQueueGUI.shutdown(); + printServerTray.shutdown(); + log.info("图形界面已关闭"); + } +} \ No newline at end of file diff --git a/src/main/java/com/goeing/printserver/main/gui/PrintNotificationService.java b/src/main/java/com/goeing/printserver/main/gui/PrintNotificationService.java new file mode 100644 index 0000000..178d339 --- /dev/null +++ b/src/main/java/com/goeing/printserver/main/gui/PrintNotificationService.java @@ -0,0 +1,163 @@ +package com.goeing.printserver.main.gui; + +import com.goeing.printserver.main.domain.request.PrintRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.awt.*; + +/** + * 打印通知服务,用于在打印任务状态变化时发送系统通知 + */ +@Service +@Slf4j +public class PrintNotificationService { + + private final PrintServerTray printServerTray; + private final PrintSettingsPanel settingsPanel; + private boolean headless = false; + + @Autowired + public PrintNotificationService(PrintServerTray printServerTray, PrintSettingsPanel settingsPanel) { + this.printServerTray = printServerTray; + this.settingsPanel = settingsPanel; + // 检查是否在无头模式下运行 + this.headless = GraphicsEnvironment.isHeadless() || Boolean.getBoolean("app.headless.mode"); + if (headless) { + log.info("系统运行在无头模式下,通知功能将被禁用"); + } + } + + /** + * 通知打印任务已添加到队列 + * + * @param printRequest 打印请求 + * @param queueSize 当前队列大小 + */ + public void notifyTaskQueued(PrintRequest printRequest, int queueSize) { + if (headless || !settingsPanel.isEnableNotifications()) { + return; // 无头模式或通知被禁用时不发送通知 + } + + String fileName = extractFileName(printRequest.getFileUrl()); + String message = String.format( + "文件 '%s' 已添加到打印队列\n打印机: %s\n队列位置: %d", + fileName, + printRequest.getPrinterName(), + queueSize + ); + + printServerTray.displayMessage("新打印任务", message, TrayIcon.MessageType.INFO); + updateTrayTooltip(queueSize); + } + + /** + * 通知打印任务开始处理 + * + * @param printRequest 打印请求 + * @param queueSize 当前队列大小 + */ + public void notifyTaskStarted(PrintRequest printRequest, int queueSize) { + if (headless || !settingsPanel.isEnableNotifications()) { + return; // 无头模式或通知被禁用时不发送通知 + } + + String fileName = extractFileName(printRequest.getFileUrl()); + String message = String.format( + "开始打印文件 '%s'\n打印机: %s", + fileName, + printRequest.getPrinterName() + ); + + printServerTray.displayMessage("打印开始", message, TrayIcon.MessageType.INFO); + updateTrayTooltip(queueSize); + } + + /** + * 通知打印任务完成 + * + * @param printRequest 打印请求 + * @param queueSize 当前队列大小 + */ + public void notifyTaskCompleted(PrintRequest printRequest, int queueSize) { + if (headless || !settingsPanel.isEnableNotifications()) { + return; // 无头模式或通知被禁用时不发送通知 + } + + String fileName = extractFileName(printRequest.getFileUrl()); + String message = String.format( + "文件 '%s' 打印完成\n打印机: %s", + fileName, + printRequest.getPrinterName() + ); + + printServerTray.displayMessage("打印完成", message, TrayIcon.MessageType.INFO); + updateTrayTooltip(queueSize); + } + + /** + * 通知打印任务失败 + * + * @param printRequest 打印请求 + * @param errorMessage 错误消息 + * @param queueSize 当前队列大小 + */ + public void notifyTaskFailed(PrintRequest printRequest, String errorMessage, int queueSize) { + if (headless || !settingsPanel.isEnableNotifications()) { + return; // 无头模式或通知被禁用时不发送通知 + } + + String fileName = extractFileName(printRequest.getFileUrl()); + String message = String.format( + "文件 '%s' 打印失败\n打印机: %s\n错误: %s", + fileName, + printRequest.getPrinterName(), + errorMessage + ); + + printServerTray.displayMessage("打印失败", message, TrayIcon.MessageType.ERROR); + updateTrayTooltip(queueSize); + } + + /** + * 从URL中提取文件名 + * + * @param fileUrl 文件URL + * @return 文件名 + */ + private String extractFileName(String fileUrl) { + if (fileUrl == null || fileUrl.isEmpty()) { + return "未知文件"; + } + + // 尝试从URL中提取文件名 + int lastSlashIndex = fileUrl.lastIndexOf('/'); + if (lastSlashIndex >= 0 && lastSlashIndex < fileUrl.length() - 1) { + String fileName = fileUrl.substring(lastSlashIndex + 1); + // 移除查询参数 + int queryIndex = fileName.indexOf('?'); + if (queryIndex > 0) { + fileName = fileName.substring(0, queryIndex); + } + return fileName; + } + + return fileUrl; // 如果无法提取,则返回完整URL + } + + /** + * 更新托盘提示 + * + * @param queueSize 当前队列大小 + */ + private void updateTrayTooltip(int queueSize) { + String tooltip; + if (queueSize > 0) { + tooltip = String.format("打印服务器 - 运行中 (队列中有 %d 个任务)", queueSize); + } else { + tooltip = "打印服务器 - 运行中 (队列为空)"; + } + printServerTray.updateTooltip(tooltip); + } +} \ No newline at end of file diff --git a/src/main/java/com/goeing/printserver/main/gui/PrintQueueGUI.java b/src/main/java/com/goeing/printserver/main/gui/PrintQueueGUI.java new file mode 100644 index 0000000..b76a28b --- /dev/null +++ b/src/main/java/com/goeing/printserver/main/gui/PrintQueueGUI.java @@ -0,0 +1,388 @@ +package com.goeing.printserver.main.gui; + +import com.goeing.printserver.main.service.PrintQueueService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import jakarta.annotation.PreDestroy; + +import javax.swing.*; +import javax.swing.table.DefaultTableModel; +import java.awt.*; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * 打印队列图形界面 + */ +@Component +public class PrintQueueGUI { + + private final PrintQueueService printQueueService; + private final PrinterStatusPanel printerStatusPanel; + private final PrintStatisticsPanel statisticsPanel; + private final PrintTaskSearchPanel searchPanel; + private final PrintSettingsPanel settingsPanel; + private JFrame frame; + private JTable currentTaskTable; + private JTable queuedTasksTable; + private DefaultTableModel currentTaskModel; + private DefaultTableModel queuedTasksModel; + private JLabel statusLabel; + private final ScheduledExecutorService refreshExecutor = Executors.newSingleThreadScheduledExecutor(); + private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + @Autowired + public PrintQueueGUI(PrintQueueService printQueueService, PrinterStatusPanel printerStatusPanel, + PrintStatisticsPanel statisticsPanel, PrintTaskSearchPanel searchPanel, + PrintSettingsPanel settingsPanel) { + this.printQueueService = printQueueService; + this.printerStatusPanel = printerStatusPanel; + this.statisticsPanel = statisticsPanel; + this.searchPanel = searchPanel; + this.settingsPanel = settingsPanel; + + // 只有在非无头模式下才初始化GUI + if (!GraphicsEnvironment.isHeadless() && !Boolean.getBoolean("app.headless.mode")) { + SwingUtilities.invokeLater(this::initializeGUI); + } + } + + /** + * 初始化图形界面 + */ + private void initializeGUI() { + // 创建主窗口 + frame = new JFrame("打印队列监控"); + frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); + frame.setSize(800, 600); + frame.setLayout(new BorderLayout()); + + // 添加窗口关闭监听器 + frame.addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + // 只隐藏窗口,不关闭应用程序 + frame.setVisible(false); + } + }); + + // 创建菜单栏 + JMenuBar menuBar = createMenuBar(); + frame.setJMenuBar(menuBar); + + // 创建顶部面板 + JPanel topPanel = new JPanel(new BorderLayout()); + statusLabel = new JLabel("队列状态: 空闲"); + statusLabel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + topPanel.add(statusLabel, BorderLayout.WEST); + + // 创建刷新按钮 + JButton refreshButton = new JButton("刷新"); + refreshButton.addActionListener(e -> refreshData()); + topPanel.add(refreshButton, BorderLayout.EAST); + + frame.add(topPanel, BorderLayout.NORTH); + + // 创建选项卡面板 + JTabbedPane tabbedPane = new JTabbedPane(); + + // 创建当前任务面板 + JPanel currentTaskPanel = createCurrentTaskPanel(); + tabbedPane.addTab("当前任务", currentTaskPanel); + + // 创建队列任务面板 + JPanel queuedTasksPanel = createQueuedTasksPanel(); + tabbedPane.addTab("队列任务", queuedTasksPanel); + + // 添加打印机状态面板 + tabbedPane.addTab("打印机状态", printerStatusPanel); + + // 添加统计面板 + tabbedPane.addTab("统计信息", statisticsPanel); + + // 添加任务搜索面板 + tabbedPane.addTab("任务搜索", searchPanel); + + // 添加设置面板 + tabbedPane.addTab("设置", settingsPanel); + + frame.add(tabbedPane, BorderLayout.CENTER); + + // 启动定时刷新 + startRefreshTimer(); + + // 显示窗口 + frame.setLocationRelativeTo(null); + frame.setVisible(true); + } + + /** + * 创建当前任务面板 + */ + private JPanel createCurrentTaskPanel() { + JPanel panel = new JPanel(new BorderLayout()); + panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + + // 创建表格模型 + String[] columnNames = {"文件URL", "打印机", "状态", "队列时间", "开始时间", "结束时间"}; + currentTaskModel = new DefaultTableModel(columnNames, 0) { + @Override + public boolean isCellEditable(int row, int column) { + return false; // 禁止编辑单元格 + } + }; + + // 创建表格 + currentTaskTable = new JTable(currentTaskModel); + currentTaskTable.getTableHeader().setReorderingAllowed(false); + currentTaskTable.setFillsViewportHeight(true); + + // 添加双击事件监听器 + currentTaskTable.addMouseListener(new java.awt.event.MouseAdapter() { + @Override + public void mouseClicked(java.awt.event.MouseEvent e) { + if (e.getClickCount() == 2) { + showTaskDetails(currentTaskTable); + } + } + }); + + // 添加滚动面板 + JScrollPane scrollPane = new JScrollPane(currentTaskTable); + panel.add(scrollPane, BorderLayout.CENTER); + + return panel; + } + + /** + * 创建队列任务面板 + */ + private JPanel createQueuedTasksPanel() { + JPanel panel = new JPanel(new BorderLayout()); + panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + + // 创建表格模型 + String[] columnNames = {"文件URL", "打印机", "状态", "队列时间"}; + queuedTasksModel = new DefaultTableModel(columnNames, 0) { + @Override + public boolean isCellEditable(int row, int column) { + return false; // 禁止编辑单元格 + } + }; + + // 创建表格 + queuedTasksTable = new JTable(queuedTasksModel); + queuedTasksTable.getTableHeader().setReorderingAllowed(false); + queuedTasksTable.setFillsViewportHeight(true); + + // 添加双击事件监听器 + queuedTasksTable.addMouseListener(new java.awt.event.MouseAdapter() { + @Override + public void mouseClicked(java.awt.event.MouseEvent e) { + if (e.getClickCount() == 2) { + showTaskDetails(queuedTasksTable); + } + } + }); + + // 添加滚动面板 + JScrollPane scrollPane = new JScrollPane(queuedTasksTable); + panel.add(scrollPane, BorderLayout.CENTER); + + return panel; + } + + /** + * 启动定时刷新 + */ + private void startRefreshTimer() { + refreshExecutor.scheduleAtFixedRate(this::refreshData, 0, 2, TimeUnit.SECONDS); + } + + /** + * 刷新数据 + */ + private void refreshData() { + SwingUtilities.invokeLater(() -> { + try { + // 更新队列状态 + int queueSize = printQueueService.getQueueSize(); + Map currentTask = printQueueService.getCurrentTaskInfo(); + + if (currentTask != null) { + statusLabel.setText("队列状态: 正在处理任务 (队列中还有 " + queueSize + " 个任务)"); + } else if (queueSize > 0) { + statusLabel.setText("队列状态: 等待处理 (队列中有 " + queueSize + " 个任务)"); + } else { + statusLabel.setText("队列状态: 空闲"); + } + + // 更新当前任务表格 + updateCurrentTaskTable(currentTask); + + // 更新队列任务表格 + List> queuedTasks = printQueueService.getQueuedTasksInfo(); + updateQueuedTasksTable(queuedTasks); + } catch (Exception e) { + e.printStackTrace(); + statusLabel.setText("刷新数据时发生错误: " + e.getMessage()); + } + }); + } + + /** + * 更新当前任务表格 + */ + private void updateCurrentTaskTable(Map currentTask) { + // 清空表格 + currentTaskModel.setRowCount(0); + + if (currentTask != null) { + Object[] rowData = new Object[6]; + rowData[0] = currentTask.get("fileUrl"); + rowData[1] = currentTask.get("printerName"); + rowData[2] = currentTask.get("status"); + rowData[3] = formatDateTime(currentTask.get("queuedTime")); + rowData[4] = formatDateTime(currentTask.get("startTime")); + rowData[5] = formatDateTime(currentTask.get("endTime")); + currentTaskModel.addRow(rowData); + } + } + + /** + * 更新队列任务表格 + */ + private void updateQueuedTasksTable(List> queuedTasks) { + // 清空表格 + queuedTasksModel.setRowCount(0); + + if (queuedTasks != null && !queuedTasks.isEmpty()) { + for (Map task : queuedTasks) { + Object[] rowData = new Object[4]; + rowData[0] = task.get("fileUrl"); + rowData[1] = task.get("printerName"); + rowData[2] = task.get("status"); + rowData[3] = formatDateTime(task.get("queuedTime")); + queuedTasksModel.addRow(rowData); + } + } + } + + /** + * 格式化日期时间 + */ + private String formatDateTime(Object dateTimeObj) { + if (dateTimeObj == null) { + return ""; + } + + if (dateTimeObj instanceof LocalDateTime) { + return ((LocalDateTime) dateTimeObj).format(dateFormatter); + } + + return dateTimeObj.toString(); + } + + /** + * 显示窗口 + */ + public void show() { + if (frame != null) { + frame.setVisible(true); + frame.toFront(); + } + } + + /** + * 创建菜单栏 + */ + private JMenuBar createMenuBar() { + JMenuBar menuBar = new JMenuBar(); + + // 文件菜单 + JMenu fileMenu = new JMenu("文件"); + JMenuItem refreshItem = new JMenuItem("刷新"); + JMenuItem exitItem = new JMenuItem("退出"); + + refreshItem.addActionListener(e -> refreshData()); + exitItem.addActionListener(e -> System.exit(0)); + + fileMenu.add(refreshItem); + fileMenu.addSeparator(); + fileMenu.add(exitItem); + + // 视图菜单 + JMenu viewMenu = new JMenu("视图"); + JMenuItem alwaysOnTopItem = new JCheckBoxMenuItem("窗口置顶"); + + alwaysOnTopItem.addActionListener(e -> { + boolean selected = ((JCheckBoxMenuItem) e.getSource()).isSelected(); + frame.setAlwaysOnTop(selected); + }); + + viewMenu.add(alwaysOnTopItem); + + // 帮助菜单 + JMenu helpMenu = new JMenu("帮助"); + JMenuItem aboutItem = new JMenuItem("关于"); + + aboutItem.addActionListener(e -> AboutDialog.showDialog(frame)); + + helpMenu.add(aboutItem); + + // 添加菜单到菜单栏 + menuBar.add(fileMenu); + menuBar.add(viewMenu); + menuBar.add(helpMenu); + + return menuBar; + } + + /** + * 显示任务详情 + * + * @param table 表格 + */ + private void showTaskDetails(JTable table) { + int selectedRow = table.getSelectedRow(); + if (selectedRow >= 0) { + Map taskInfo = null; + + if (table == currentTaskTable) { + // 当前任务表格 + taskInfo = printQueueService.getCurrentTaskInfo(); + } else if (table == queuedTasksTable) { + // 队列任务表格 + List> queuedTasks = printQueueService.getQueuedTasksInfo(); + if (selectedRow < queuedTasks.size()) { + taskInfo = queuedTasks.get(selectedRow); + } + } + + if (taskInfo != null) { + PrintTaskDetailDialog dialog = new PrintTaskDetailDialog(frame, taskInfo); + dialog.setVisible(true); + } + } + } + + /** + * 关闭窗口和资源 + */ + @PreDestroy + public void shutdown() { + refreshExecutor.shutdownNow(); + printerStatusPanel.shutdown(); + statisticsPanel.shutdown(); + if (frame != null) { + frame.dispose(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/goeing/printserver/main/gui/PrintServerTray.java b/src/main/java/com/goeing/printserver/main/gui/PrintServerTray.java new file mode 100644 index 0000000..654acc9 --- /dev/null +++ b/src/main/java/com/goeing/printserver/main/gui/PrintServerTray.java @@ -0,0 +1,162 @@ +package com.goeing.printserver.main.gui; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.image.BufferedImage; + +/** + * 打印服务器系统托盘 + */ +@Component +@Slf4j +public class PrintServerTray { + + private final PrintQueueGUI printQueueGUI; + private TrayIcon trayIcon; + private SystemTray systemTray; + private boolean traySupported; + + @Autowired + public PrintServerTray(PrintQueueGUI printQueueGUI) { + this.printQueueGUI = printQueueGUI; + // 初始化逻辑移至initialize()方法 + } + + /** + * 初始化系统托盘 + */ + public void initialize() { + // 在无头模式下不初始化系统托盘 + if (GraphicsEnvironment.isHeadless() || Boolean.getBoolean("app.headless.mode")) { + log.info("在无头模式下运行,跳过系统托盘初始化"); + traySupported = false; + return; + } + initializeTray(); + } + + /** + * 初始化系统托盘 + */ + private void initializeTray() { + // 检查系统是否支持系统托盘 + if (!SystemTray.isSupported()) { + log.warn("系统不支持系统托盘功能"); + traySupported = false; + return; + } + + traySupported = true; + systemTray = SystemTray.getSystemTray(); + + // 创建托盘图标 + Image trayImage = createTrayImage(); + trayIcon = new TrayIcon(trayImage, "打印服务器"); + trayIcon.setImageAutoSize(true); + + // 创建弹出菜单 + PopupMenu popupMenu = createPopupMenu(); + trayIcon.setPopupMenu(popupMenu); + + // 添加鼠标点击事件 + trayIcon.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (e.getButton() == MouseEvent.BUTTON1) { + // 左键点击显示主窗口 + printQueueGUI.show(); + } + } + }); + + // 添加托盘图标 + try { + systemTray.add(trayIcon); + log.info("系统托盘图标已添加"); + } catch (AWTException e) { + log.error("添加系统托盘图标失败", e); + traySupported = false; + } + } + + /** + * 创建托盘图标图像 + */ + private Image createTrayImage() { + // 创建一个简单的打印机图标 + BufferedImage image = new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2d = image.createGraphics(); + + // 设置抗锯齿 + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + // 绘制打印机图标 + g2d.setColor(Color.BLACK); + g2d.fillRect(2, 10, 12, 5); // 打印机底座 + g2d.fillRect(3, 3, 10, 7); // 打印机主体 + g2d.setColor(Color.WHITE); + g2d.fillRect(4, 4, 8, 5); // 打印机内部 + g2d.setColor(Color.BLACK); + g2d.fillRect(5, 6, 6, 1); // 打印纸 + + g2d.dispose(); + return image; + } + + /** + * 创建弹出菜单 + */ + private PopupMenu createPopupMenu() { + PopupMenu popupMenu = new PopupMenu(); + + // 添加菜单项 + MenuItem openItem = new MenuItem("打开主窗口"); + MenuItem exitItem = new MenuItem("退出"); + + // 设置事件监听器 + openItem.addActionListener(e -> printQueueGUI.show()); + exitItem.addActionListener(e -> System.exit(0)); + + // 添加菜单项到弹出菜单 + popupMenu.add(openItem); + popupMenu.addSeparator(); + popupMenu.add(exitItem); + + return popupMenu; + } + + /** + * 显示通知消息 + */ + public void displayMessage(String caption, String text, TrayIcon.MessageType messageType) { + if (traySupported && trayIcon != null) { + trayIcon.displayMessage(caption, text, messageType); + } + } + + /** + * 更新托盘图标提示文本 + */ + public void updateTooltip(String tooltip) { + if (traySupported && trayIcon != null) { + trayIcon.setToolTip(tooltip); + } + } + + /** + * 关闭系统托盘 + */ + public void shutdown() { + if (traySupported && systemTray != null && trayIcon != null) { + systemTray.remove(trayIcon); + log.info("系统托盘图标已移除"); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/goeing/printserver/main/gui/PrintSettingsPanel.java b/src/main/java/com/goeing/printserver/main/gui/PrintSettingsPanel.java new file mode 100644 index 0000000..f1ef629 --- /dev/null +++ b/src/main/java/com/goeing/printserver/main/gui/PrintSettingsPanel.java @@ -0,0 +1,322 @@ +package com.goeing.printserver.main.gui; + +import com.goeing.printserver.main.PrintController; +import com.goeing.printserver.main.config.PrintServerConfig; +import com.goeing.printserver.main.service.PrintQueueService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +import java.util.List; +import com.goeing.printserver.main.gui.PrinterStatusPanel.PrinterListUpdatedEvent; + +import javax.swing.*; +import java.awt.*; + +/** + * 打印设置面板 + */ +@Component +@Slf4j +public class PrintSettingsPanel extends JPanel { + + private final PrintQueueService printQueueService; + private final PrintServerConfig config; + private final PrintController printController; + + // UI组件 + private JComboBox defaultPrinterComboBox; + private JSpinner maxQueueSizeSpinner; + private JCheckBox enableNotificationsCheckBox; + private JCheckBox startMinimizedCheckBox; + private JCheckBox autoStartCheckBox; + + @Autowired + public PrintSettingsPanel(PrintQueueService printQueueService, PrintServerConfig config, PrintController printController) { + this.printQueueService = printQueueService; + this.config = config; + this.printController = printController; + initializeUI(); + loadSettings(); + } + + /** + * 初始化用户界面 + */ + private void initializeUI() { + setLayout(new BorderLayout()); + setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + + // 创建标题 + JLabel titleLabel = new JLabel("打印服务器设置"); + titleLabel.setFont(new Font(titleLabel.getFont().getName(), Font.BOLD, 16)); + titleLabel.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0)); + add(titleLabel, BorderLayout.NORTH); + + // 创建设置面板 + JPanel settingsPanel = new JPanel(new GridBagLayout()); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.insets = new Insets(5, 5, 5, 5); + gbc.anchor = GridBagConstraints.WEST; + gbc.fill = GridBagConstraints.HORIZONTAL; + + // 默认打印机设置 + gbc.gridx = 0; + gbc.gridy = 0; + gbc.gridwidth = 1; + settingsPanel.add(new JLabel("默认打印机:"), gbc); + + gbc.gridx = 1; + gbc.weightx = 1.0; + defaultPrinterComboBox = new JComboBox<>(); + updatePrinterList(); // 初始化打印机列表 + settingsPanel.add(defaultPrinterComboBox, gbc); + + // 最大队列大小设置 + gbc.gridx = 0; + gbc.gridy = 1; + gbc.weightx = 0.0; + settingsPanel.add(new JLabel("最大队列大小:"), gbc); + + gbc.gridx = 1; + gbc.weightx = 1.0; + SpinnerNumberModel spinnerModel = new SpinnerNumberModel(10, 1, 100, 1); + maxQueueSizeSpinner = new JSpinner(spinnerModel); + settingsPanel.add(maxQueueSizeSpinner, gbc); + + // 启用通知设置 + gbc.gridx = 0; + gbc.gridy = 2; + gbc.gridwidth = 2; + enableNotificationsCheckBox = new JCheckBox("启用系统通知"); + settingsPanel.add(enableNotificationsCheckBox, gbc); + + // 启动时最小化设置 + gbc.gridx = 0; + gbc.gridy = 3; + startMinimizedCheckBox = new JCheckBox("启动时最小化到系统托盘"); + settingsPanel.add(startMinimizedCheckBox, gbc); + + // 开机自启动设置 + gbc.gridx = 0; + gbc.gridy = 4; + autoStartCheckBox = new JCheckBox("开机自动启动"); + settingsPanel.add(autoStartCheckBox, gbc); + + // 添加一个弹性空间 + gbc.gridx = 0; + gbc.gridy = 5; + gbc.weighty = 1.0; + settingsPanel.add(Box.createVerticalGlue(), gbc); + + // 添加设置面板到滚动面板 + JScrollPane scrollPane = new JScrollPane(settingsPanel); + scrollPane.setBorder(null); + add(scrollPane, BorderLayout.CENTER); + + // 创建按钮面板 + JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); + JButton saveButton = new JButton("保存设置"); + saveButton.addActionListener(e -> saveSettings()); + buttonPanel.add(saveButton); + + JButton resetButton = new JButton("重置默认"); + resetButton.addActionListener(e -> resetSettings()); + buttonPanel.add(resetButton); + + add(buttonPanel, BorderLayout.SOUTH); + } + + /** + * 加载设置 + */ + private void loadSettings() { + // 从配置对象加载设置 + config.loadConfig(); + + // 更新UI组件 + defaultPrinterComboBox.setSelectedItem(config.getDefaultPrinter()); + maxQueueSizeSpinner.setValue(config.getMaxQueueSize()); + enableNotificationsCheckBox.setSelected(config.isEnableNotifications()); + startMinimizedCheckBox.setSelected(config.isStartMinimized()); + autoStartCheckBox.setSelected(config.isAutoStart()); + } + + /** + * 保存设置 + */ + private void saveSettings() { + try { + // 从UI组件获取设置值 + String defaultPrinter = defaultPrinterComboBox.getSelectedItem().toString(); + int maxQueueSize = (Integer) maxQueueSizeSpinner.getValue(); + boolean enableNotifications = enableNotificationsCheckBox.isSelected(); + boolean startMinimized = startMinimizedCheckBox.isSelected(); + boolean autoStart = autoStartCheckBox.isSelected(); + + // 更新配置对象 + config.setDefaultPrinter(defaultPrinter); + config.setMaxQueueSize(maxQueueSize); + config.setEnableNotifications(enableNotifications); + config.setStartMinimized(startMinimized); + config.setAutoStart(autoStart); + + // 保存配置 + config.saveConfig(); + + // 应用设置到服务 + applySettings(); + + JOptionPane.showMessageDialog(this, "设置已保存", "成功", JOptionPane.INFORMATION_MESSAGE); + } catch (Exception e) { + log.error("保存设置时发生错误", e); + JOptionPane.showMessageDialog(this, "保存设置失败: " + e.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); + } + } + + /** + * 重置设置为默认值 + */ + private void resetSettings() { + int option = JOptionPane.showConfirmDialog(this, "确定要重置所有设置为默认值吗?", "确认", JOptionPane.YES_NO_OPTION); + if (option == JOptionPane.YES_OPTION) { + try { + // 重置配置对象到默认值 + config.resetToDefaults(); + // 保存默认配置 + config.saveConfig(); + // 更新UI + loadSettings(); + // 应用默认设置 + applySettings(); + + JOptionPane.showMessageDialog(this, "设置已重置为默认值", "成功", JOptionPane.INFORMATION_MESSAGE); + } catch (Exception e) { + log.error("重置设置时发生错误", e); + JOptionPane.showMessageDialog(this, "重置设置失败: " + e.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); + } + } + } + + /** + * 应用设置到服务 + */ + private void applySettings() { + // 在这里应用设置到相关服务 + // 例如,可以设置打印队列服务的最大队列大小等 + printQueueService.setMaxQueueSize(config.getMaxQueueSize()); + + // 通知设置变更 + log.info("应用设置: 默认打印机={}, 最大队列大小={}, 启用通知={}, 启动时最小化={}, 开机自启动={}", + config.getDefaultPrinter(), + config.getMaxQueueSize(), + config.isEnableNotifications(), + config.isStartMinimized(), + config.isAutoStart()); + } + + /** + * 获取是否启动时最小化设置 + * + * @return 是否启动时最小化 + */ + public boolean isStartMinimized() { + return config.isStartMinimized(); + } + + /** + * 获取是否启用通知设置 + * + * @return 是否启用通知 + */ + public boolean isEnableNotifications() { + return config.isEnableNotifications(); + } + + /** + * 获取默认打印机设置 + * + * @return 默认打印机名称 + */ + public String getDefaultPrinter() { + return config.getDefaultPrinter(); + } + + /** + * 获取最大队列大小设置 + * + * @return 最大队列大小 + */ + public int getMaxQueueSize() { + return config.getMaxQueueSize(); + } + + /** + * 更新打印机列表 + */ + public void updatePrinterList() { + try { + // 保存当前选中的打印机 + String selectedPrinter = defaultPrinterComboBox.getSelectedItem() != null ? + defaultPrinterComboBox.getSelectedItem().toString() : null; + + // 清空打印机列表 + defaultPrinterComboBox.removeAllItems(); + + List printers = printController.printerList(); + updatePrinterComboBox(printers, selectedPrinter); + } catch (Exception e) { + log.error("更新打印机列表时发生错误", e); + } + } + + /** + * 使用提供的打印机列表更新下拉框 + * + * @param printers 打印机列表 + * @param selectedPrinter 当前选中的打印机 + */ + private void updatePrinterComboBox(List printers, String selectedPrinter) { + if (printers != null && !printers.isEmpty()) { + for (String printer : printers) { + defaultPrinterComboBox.addItem(printer); + } + + // 尝试恢复之前选中的打印机 + if (selectedPrinter != null && printers.contains(selectedPrinter)) { + defaultPrinterComboBox.setSelectedItem(selectedPrinter); + } else { + // 如果之前选中的打印机不存在,则使用配置中的默认打印机 + String defaultPrinter = config.getDefaultPrinter(); + if (defaultPrinter != null && printers.contains(defaultPrinter)) { + defaultPrinterComboBox.setSelectedItem(defaultPrinter); + } + } + + // 使用debug级别记录日志,减少info日志输出 + log.debug("已更新打印机列表,共{}个打印机", printers.size()); + } + } + + /** + * 监听打印机列表更新事件 + * + * @param event 打印机列表更新事件 + */ + @EventListener + public void onPrinterListUpdated(PrinterListUpdatedEvent event) { + SwingUtilities.invokeLater(() -> { + // 保存当前选中的打印机 + String selectedPrinter = defaultPrinterComboBox.getSelectedItem() != null ? + defaultPrinterComboBox.getSelectedItem().toString() : null; + + // 清空并重新填充打印机列表 + defaultPrinterComboBox.removeAllItems(); + + updatePrinterComboBox(event.getPrinters(), selectedPrinter); + // 使用debug级别记录日志,减少info日志输出 + log.debug("收到打印机列表更新事件,已更新打印机下拉列表"); + }); + } +} \ No newline at end of file diff --git a/src/main/java/com/goeing/printserver/main/gui/PrintStatisticsPanel.java b/src/main/java/com/goeing/printserver/main/gui/PrintStatisticsPanel.java new file mode 100644 index 0000000..90f3103 --- /dev/null +++ b/src/main/java/com/goeing/printserver/main/gui/PrintStatisticsPanel.java @@ -0,0 +1,200 @@ +package com.goeing.printserver.main.gui; + +import com.goeing.printserver.main.service.PrintQueueService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.swing.*; +import java.awt.*; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * 打印统计面板 + */ +@Component +@Slf4j +public class PrintStatisticsPanel extends JPanel { + + private final PrintQueueService printQueueService; + private final ScheduledExecutorService refreshExecutor = Executors.newSingleThreadScheduledExecutor(); + private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + // 统计数据 + private final AtomicInteger totalTasksCount = new AtomicInteger(0); + private final AtomicInteger completedTasksCount = new AtomicInteger(0); + private final AtomicInteger failedTasksCount = new AtomicInteger(0); + private LocalDateTime startTime = LocalDateTime.now(); + + // UI组件 + private JLabel totalTasksLabel; + private JLabel completedTasksLabel; + private JLabel failedTasksLabel; + private JLabel queueSizeLabel; + private JLabel upTimeLabel; + private JLabel currentTimeLabel; + + @Autowired + public PrintStatisticsPanel(PrintQueueService printQueueService) { + this.printQueueService = printQueueService; + initializeUI(); + startRefreshTimer(); + } + + /** + * 初始化用户界面 + */ + private void initializeUI() { + setLayout(new BorderLayout()); + setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + + // 创建顶部面板 + JPanel topPanel = new JPanel(new BorderLayout()); + JLabel titleLabel = new JLabel("打印统计"); + titleLabel.setFont(new Font(titleLabel.getFont().getName(), Font.BOLD, 14)); + topPanel.add(titleLabel, BorderLayout.WEST); + + // 创建刷新按钮 + JButton refreshButton = new JButton("刷新"); + refreshButton.addActionListener(e -> refreshStatistics()); + topPanel.add(refreshButton, BorderLayout.EAST); + + add(topPanel, BorderLayout.NORTH); + + // 创建统计信息面板 + JPanel statsPanel = new JPanel(); + statsPanel.setLayout(new BoxLayout(statsPanel, BoxLayout.Y_AXIS)); + statsPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + + // 添加统计信息 + totalTasksLabel = createStatLabel("总任务数: 0"); + completedTasksLabel = createStatLabel("已完成任务: 0"); + failedTasksLabel = createStatLabel("失败任务: 0"); + queueSizeLabel = createStatLabel("当前队列长度: 0"); + upTimeLabel = createStatLabel("运行时间: 0小时0分钟"); + currentTimeLabel = createStatLabel("当前时间: " + LocalDateTime.now().format(dateFormatter)); + + statsPanel.add(totalTasksLabel); + statsPanel.add(Box.createVerticalStrut(10)); + statsPanel.add(completedTasksLabel); + statsPanel.add(Box.createVerticalStrut(10)); + statsPanel.add(failedTasksLabel); + statsPanel.add(Box.createVerticalStrut(10)); + statsPanel.add(queueSizeLabel); + statsPanel.add(Box.createVerticalStrut(10)); + statsPanel.add(upTimeLabel); + statsPanel.add(Box.createVerticalStrut(10)); + statsPanel.add(currentTimeLabel); + + // 添加一个弹性空间,使内容居上 + statsPanel.add(Box.createVerticalGlue()); + + // 添加重置按钮 + JButton resetButton = new JButton("重置统计"); + resetButton.addActionListener(e -> resetStatistics()); + resetButton.setAlignmentX(0.0f); // LEFT_ALIGNMENT = 0.0f + + JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + buttonPanel.add(resetButton); + buttonPanel.setAlignmentX(0.0f); // LEFT_ALIGNMENT = 0.0f + + statsPanel.add(buttonPanel); + + add(new JScrollPane(statsPanel), BorderLayout.CENTER); + } + + /** + * 创建统计标签 + * + * @param text 标签文本 + * @return 标签组件 + */ + private JLabel createStatLabel(String text) { + JLabel label = new JLabel(text); + label.setFont(new Font(label.getFont().getName(), Font.PLAIN, 14)); + label.setAlignmentX(0.0f); // LEFT_ALIGNMENT = 0.0f + return label; + } + + /** + * 启动定时刷新 + */ + private void startRefreshTimer() { + refreshExecutor.scheduleAtFixedRate(this::refreshStatistics, 0, 1, TimeUnit.SECONDS); + } + + /** + * 刷新统计信息 + */ + private void refreshStatistics() { + SwingUtilities.invokeLater(() -> { + try { + // 更新队列大小 + int queueSize = printQueueService.getQueueSize(); + queueSizeLabel.setText("当前队列长度: " + queueSize); + + // 更新运行时间 + LocalDateTime now = LocalDateTime.now(); + long hours = java.time.Duration.between(startTime, now).toHours(); + long minutes = java.time.Duration.between(startTime, now).toMinutes() % 60; + upTimeLabel.setText(String.format("运行时间: %d小时%d分钟", hours, minutes)); + + // 更新当前时间 + currentTimeLabel.setText("当前时间: " + now.format(dateFormatter)); + } catch (Exception e) { + log.error("刷新统计信息时发生错误", e); + } + }); + } + + /** + * 重置统计信息 + */ + private void resetStatistics() { + totalTasksCount.set(0); + completedTasksCount.set(0); + failedTasksCount.set(0); + startTime = LocalDateTime.now(); + + totalTasksLabel.setText("总任务数: 0"); + completedTasksLabel.setText("已完成任务: 0"); + failedTasksLabel.setText("失败任务: 0"); + upTimeLabel.setText("运行时间: 0小时0分钟"); + } + + /** + * 增加总任务数 + */ + public void incrementTotalTasks() { + int total = totalTasksCount.incrementAndGet(); + SwingUtilities.invokeLater(() -> totalTasksLabel.setText("总任务数: " + total)); + } + + /** + * 增加已完成任务数 + */ + public void incrementCompletedTasks() { + int completed = completedTasksCount.incrementAndGet(); + SwingUtilities.invokeLater(() -> completedTasksLabel.setText("已完成任务: " + completed)); + } + + /** + * 增加失败任务数 + */ + public void incrementFailedTasks() { + int failed = failedTasksCount.incrementAndGet(); + SwingUtilities.invokeLater(() -> failedTasksLabel.setText("失败任务: " + failed)); + } + + /** + * 关闭资源 + */ + public void shutdown() { + refreshExecutor.shutdownNow(); + } +} \ No newline at end of file diff --git a/src/main/java/com/goeing/printserver/main/gui/PrintTaskDetailDialog.java b/src/main/java/com/goeing/printserver/main/gui/PrintTaskDetailDialog.java new file mode 100644 index 0000000..29cbcf5 --- /dev/null +++ b/src/main/java/com/goeing/printserver/main/gui/PrintTaskDetailDialog.java @@ -0,0 +1,193 @@ +package com.goeing.printserver.main.gui; + +import com.goeing.printserver.main.domain.PrintTask; +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import java.awt.*; +import java.awt.Dialog.ModalityType; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Map; + +/** + * 打印任务详情对话框 + */ +public class PrintTaskDetailDialog extends JDialog { + + private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + /** + * 创建打印任务详情对话框 + * + * @param parent 父窗口 + * @param task 任务信息 + */ + public PrintTaskDetailDialog(Frame parent, Map task) { + super(parent, "打印任务详情", true); + initializeUI(task); + } + + /** + * 创建打印任务详情对话框 + * + * @param parent 父窗口 + * @param task 打印任务对象 + */ + public PrintTaskDetailDialog(Frame parent, PrintTask task) { + super(parent, "打印任务详情", true); + initializeUI(task.toMap()); + } + + /** + * 创建打印任务详情对话框(接受Window类型参数) + * + * @param parent 父窗口 + * @param task 打印任务对象 + */ + public PrintTaskDetailDialog(Window parent, PrintTask task) { + super(parent, "打印任务详情", ModalityType.APPLICATION_MODAL); + initializeUI(task.toMap()); + } + + /** + * 创建打印任务详情对话框(接受Window类型参数和Map类型任务信息) + * + * @param parent 父窗口 + * @param task 任务信息 + */ + public PrintTaskDetailDialog(Window parent, Map task) { + super(parent, "打印任务详情", ModalityType.APPLICATION_MODAL); + initializeUI(task); + } + + /** + * 初始化用户界面 + * + * @param task 任务信息 + */ + private void initializeUI(Map task) { + setSize(500, 400); + setLocationRelativeTo(getParent()); + setLayout(new BorderLayout()); + + // 创建内容面板 + JPanel contentPanel = new JPanel(); + contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS)); + contentPanel.setBorder(new EmptyBorder(10, 10, 10, 10)); + + // 添加任务信息 + addTaskInfo(contentPanel, task); + + // 添加滚动面板 + JScrollPane scrollPane = new JScrollPane(contentPanel); + scrollPane.setBorder(null); + add(scrollPane, BorderLayout.CENTER); + + // 添加关闭按钮 + JButton closeButton = new JButton("关闭"); + closeButton.addActionListener(e -> dispose()); + + JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); + buttonPanel.add(closeButton); + add(buttonPanel, BorderLayout.SOUTH); + } + + /** + * 添加任务信息 + * + * @param panel 面板 + * @param task 任务信息 + */ + private void addTaskInfo(JPanel panel, Map task) { + // 添加标题 + JLabel titleLabel = new JLabel("打印任务详情"); + titleLabel.setFont(new Font(titleLabel.getFont().getName(), Font.BOLD, 16)); + titleLabel.setAlignmentX(0.0f); // LEFT_ALIGNMENT = 0.0f + panel.add(titleLabel); + panel.add(Box.createVerticalStrut(10)); + + // 添加基本信息 + addInfoField(panel, "文件URL:", getStringValue(task.get("fileUrl"))); + addInfoField(panel, "打印机:", getStringValue(task.get("printerName"))); + addInfoField(panel, "状态:", getStringValue(task.get("status"))); + addInfoField(panel, "队列时间:", formatDateTime(task.get("queuedTime"))); + + // 添加开始和结束时间(如果有) + if (task.containsKey("startTime") && task.get("startTime") != null) { + addInfoField(panel, "开始时间:", formatDateTime(task.get("startTime"))); + } + + if (task.containsKey("endTime") && task.get("endTime") != null) { + addInfoField(panel, "结束时间:", formatDateTime(task.get("endTime"))); + } + + // 添加打印选项(如果有) + if (task.containsKey("printOption") && task.get("printOption") != null) { + panel.add(Box.createVerticalStrut(10)); + JLabel optionsLabel = new JLabel("打印选项"); + optionsLabel.setFont(new Font(optionsLabel.getFont().getName(), Font.BOLD, 14)); + optionsLabel.setAlignmentX(0.0f); // LEFT_ALIGNMENT = 0.0f + panel.add(optionsLabel); + panel.add(Box.createVerticalStrut(5)); + + Map printOption = (Map) task.get("printOption"); + for (Map.Entry entry : printOption.entrySet()) { + addInfoField(panel, entry.getKey() + ":", getStringValue(entry.getValue())); + } + } + } + + /** + * 添加信息字段 + * + * @param panel 面板 + * @param label 标签 + * @param value 值 + */ + private void addInfoField(JPanel panel, String label, String value) { + JPanel fieldPanel = new JPanel(new BorderLayout()); + fieldPanel.setAlignmentX(0.0f); // LEFT_ALIGNMENT = 0.0f + fieldPanel.setMaximumSize(new Dimension(Integer.MAX_VALUE, 25)); + + JLabel labelComponent = new JLabel(label); + labelComponent.setPreferredSize(new Dimension(100, 20)); + fieldPanel.add(labelComponent, BorderLayout.WEST); + + JTextField valueField = new JTextField(value); + valueField.setEditable(false); + valueField.setBorder(null); + valueField.setBackground(null); + fieldPanel.add(valueField, BorderLayout.CENTER); + + panel.add(fieldPanel); + panel.add(Box.createVerticalStrut(5)); + } + + /** + * 获取字符串值 + * + * @param value 值 + * @return 字符串值 + */ + private String getStringValue(Object value) { + return value != null ? value.toString() : ""; + } + + /** + * 格式化日期时间 + * + * @param dateTimeObj 日期时间对象 + * @return 格式化后的日期时间字符串 + */ + private String formatDateTime(Object dateTimeObj) { + if (dateTimeObj == null) { + return ""; + } + + if (dateTimeObj instanceof LocalDateTime) { + return ((LocalDateTime) dateTimeObj).format(dateFormatter); + } + + return dateTimeObj.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/com/goeing/printserver/main/gui/PrintTaskSearchPanel.java b/src/main/java/com/goeing/printserver/main/gui/PrintTaskSearchPanel.java new file mode 100644 index 0000000..e258a11 --- /dev/null +++ b/src/main/java/com/goeing/printserver/main/gui/PrintTaskSearchPanel.java @@ -0,0 +1,305 @@ +package com.goeing.printserver.main.gui; + +import com.goeing.printserver.main.domain.PrintTask; +import com.goeing.printserver.main.service.PrintQueueService; +import com.goeing.printserver.main.service.PrintHistoryService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.swing.*; +import javax.swing.table.DefaultTableModel; +import java.awt.*; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; + +/** + * 打印任务搜索面板 + */ +@Component +@Slf4j +public class PrintTaskSearchPanel extends JPanel { + + private final PrintQueueService printQueueService; + private final PrintHistoryService historyService; + private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + // UI组件 + private JComboBox printerComboBox; + private JComboBox statusComboBox; + private JTextField fileUrlField; + private JCheckBox includeHistoryCheckBox; + private JTable resultsTable; + private DefaultTableModel tableModel; + + @Autowired + public PrintTaskSearchPanel(PrintQueueService printQueueService, PrintHistoryService historyService) { + this.printQueueService = printQueueService; + this.historyService = historyService; + initializeUI(); + } + + /** + * 初始化用户界面 + */ + private void initializeUI() { + setLayout(new BorderLayout()); + setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + + // 创建搜索条件面板 + JPanel searchPanel = new JPanel(new GridBagLayout()); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.insets = new Insets(5, 5, 5, 5); + gbc.fill = GridBagConstraints.HORIZONTAL; + + // 打印机选择 + gbc.gridx = 0; + gbc.gridy = 0; + searchPanel.add(new JLabel("打印机:"), gbc); + + gbc.gridx = 1; + gbc.weightx = 1.0; + printerComboBox = new JComboBox<>(new String[]{"全部", "打印机1", "打印机2", "打印机3"}); + searchPanel.add(printerComboBox, gbc); + + // 状态选择 + gbc.gridx = 0; + gbc.gridy = 1; + gbc.weightx = 0.0; + searchPanel.add(new JLabel("状态:"), gbc); + + gbc.gridx = 1; + gbc.weightx = 1.0; + statusComboBox = new JComboBox<>(new String[]{"全部", "queued", "processing", "completed", "failed"}); + searchPanel.add(statusComboBox, gbc); + + // 文件URL + gbc.gridx = 0; + gbc.gridy = 2; + gbc.weightx = 0.0; + searchPanel.add(new JLabel("文件URL:"), gbc); + + gbc.gridx = 1; + gbc.weightx = 1.0; + fileUrlField = new JTextField(); + searchPanel.add(fileUrlField, gbc); + + // 包含历史记录选项 + gbc.gridx = 0; + gbc.gridy = 3; + gbc.gridwidth = 2; + includeHistoryCheckBox = new JCheckBox("包含历史记录"); + includeHistoryCheckBox.setSelected(true); + searchPanel.add(includeHistoryCheckBox, gbc); + + // 搜索按钮 + gbc.gridx = 0; + gbc.gridy = 4; + gbc.gridwidth = 2; + gbc.weightx = 1.0; + gbc.anchor = GridBagConstraints.CENTER; + JButton searchButton = new JButton("搜索"); + searchButton.addActionListener(e -> performSearch()); + searchPanel.add(searchButton, gbc); + + add(searchPanel, BorderLayout.NORTH); + + // 创建结果表格 + String[] columnNames = {"文件URL", "打印机", "状态", "队列时间", "开始时间", "结束时间"}; + tableModel = new DefaultTableModel(columnNames, 0) { + @Override + public boolean isCellEditable(int row, int column) { + return false; + } + }; + resultsTable = new JTable(tableModel); + resultsTable.getTableHeader().setReorderingAllowed(false); + resultsTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + + // 添加双击事件 + resultsTable.addMouseListener(new java.awt.event.MouseAdapter() { + @Override + public void mouseClicked(java.awt.event.MouseEvent evt) { + if (evt.getClickCount() == 2) { + showTaskDetails(); + } + } + }); + + JScrollPane scrollPane = new JScrollPane(resultsTable); + add(scrollPane, BorderLayout.CENTER); + + // 创建按钮面板 + JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); + JButton detailsButton = new JButton("查看详情"); + detailsButton.addActionListener(e -> showTaskDetails()); + buttonPanel.add(detailsButton); + + JButton clearButton = new JButton("清空结果"); + clearButton.addActionListener(e -> clearResults()); + buttonPanel.add(clearButton); + + add(buttonPanel, BorderLayout.SOUTH); + } + + /** + * 执行搜索 + */ + private void performSearch() { + // 清空当前结果 + clearResults(); + + // 获取搜索条件 + String printer = printerComboBox.getSelectedItem().toString(); + String status = statusComboBox.getSelectedItem().toString(); + String fileUrl = fileUrlField.getText().trim(); + + // 获取当前任务和队列任务 + List currentTasks = printQueueService.getCurrentTask() != null ? + List.of(printQueueService.getCurrentTask()) : List.of(); + List queuedTasks = printQueueService.getQueuedTasks(); + + // 添加当前任务和队列任务到结果中 + addTasksToResults(currentTasks, printer, status, fileUrl); + addTasksToResults(queuedTasks, printer, status, fileUrl); + + // 如果选择包含历史记录,则添加历史任务到结果中 + if (includeHistoryCheckBox.isSelected()) { + List historyTasks = historyService.getAllHistory(); + addTasksToResults(historyTasks, printer, status, fileUrl); + } + + // 更新状态 + updateStatus(); + } + + /** + * 将符合条件的任务添加到结果表格中 + * + * @param tasks 任务列表 + * @param printer 打印机筛选条件 + * @param status 状态筛选条件 + * @param fileUrl 文件URL筛选条件 + */ + private void addTasksToResults(List tasks, String printer, String status, String fileUrl) { + for (PrintTask task : tasks) { + // 应用筛选条件 + if (!"全部".equals(printer) && !printer.equals(task.getPrinter())) { + continue; + } + if (!"全部".equals(status) && !status.equals(task.getStatus())) { + continue; + } + if (!fileUrl.isEmpty() && !task.getFileUrl().contains(fileUrl)) { + continue; + } + + // 添加到表格 + Object[] row = { + task.getFileUrl(), + task.getPrinter(), + task.getStatus(), + formatDateTime(task.getQueuedTime()), + formatDateTime(task.getStartTime()), + formatDateTime(task.getEndTime()) + }; + tableModel.addRow(row); + } + } + + /** + * 格式化日期时间 + * + * @param dateTime 日期时间 + * @return 格式化后的字符串 + */ + private String formatDateTime(LocalDateTime dateTime) { + return dateTime != null ? dateTime.format(dateFormatter) : "--"; + } + + /** + * 更新状态信息 + */ + private void updateStatus() { + int resultCount = tableModel.getRowCount(); + if (resultCount == 0) { + JOptionPane.showMessageDialog(this, "没有找到符合条件的任务", "搜索结果", JOptionPane.INFORMATION_MESSAGE); + } + } + + /** + * 清空搜索结果 + */ + private void clearResults() { + while (tableModel.getRowCount() > 0) { + tableModel.removeRow(0); + } + } + + /** + * 显示任务详情 + */ + private void showTaskDetails() { + int selectedRow = resultsTable.getSelectedRow(); + if (selectedRow >= 0) { + String fileUrl = (String) tableModel.getValueAt(selectedRow, 0); + String printer = (String) tableModel.getValueAt(selectedRow, 1); + String status = (String) tableModel.getValueAt(selectedRow, 2); + String queuedTime = (String) tableModel.getValueAt(selectedRow, 3); + String startTime = (String) tableModel.getValueAt(selectedRow, 4); + String endTime = (String) tableModel.getValueAt(selectedRow, 5); + + // 查找对应的任务对象 + PrintTask task = findTaskByFileUrl(fileUrl); + if (task != null) { + // 显示详情对话框 + PrintTaskDetailDialog dialog = new PrintTaskDetailDialog(SwingUtilities.getWindowAncestor(this), task); + dialog.setVisible(true); + } else { + // 使用表格中的数据创建一个简化的任务对象 + PrintTask simpleTask = new PrintTask(); + simpleTask.setFileUrl(fileUrl); + simpleTask.setPrinter(printer); + simpleTask.setStatus(status); + + // 显示详情对话框 + PrintTaskDetailDialog dialog = new PrintTaskDetailDialog(SwingUtilities.getWindowAncestor(this), simpleTask); + dialog.setVisible(true); + } + } else { + JOptionPane.showMessageDialog(this, "请先选择一个任务", "提示", JOptionPane.INFORMATION_MESSAGE); + } + } + + /** + * 根据文件URL查找任务 + * + * @param fileUrl 文件URL + * @return 任务对象,如果未找到则返回null + */ + private PrintTask findTaskByFileUrl(String fileUrl) { + // 检查当前任务 + PrintTask currentTask = printQueueService.getCurrentTask(); + if (currentTask != null && currentTask.getFileUrl().equals(fileUrl)) { + return currentTask; + } + + // 检查队列中的任务 + for (PrintTask task : printQueueService.getQueuedTasks()) { + if (task.getFileUrl().equals(fileUrl)) { + return task; + } + } + + // 检查历史记录中的任务 + if (includeHistoryCheckBox.isSelected()) { + List historyTasks = historyService.getHistoryByFileUrl(fileUrl); + if (!historyTasks.isEmpty()) { + return historyTasks.get(0); + } + } + + return null; + } +} \ No newline at end of file diff --git a/src/main/java/com/goeing/printserver/main/gui/PrinterStatusPanel.java b/src/main/java/com/goeing/printserver/main/gui/PrinterStatusPanel.java new file mode 100644 index 0000000..43bc206 --- /dev/null +++ b/src/main/java/com/goeing/printserver/main/gui/PrinterStatusPanel.java @@ -0,0 +1,166 @@ +package com.goeing.printserver.main.gui; + +import com.goeing.printserver.main.PrintController; +import com.goeing.printserver.main.config.PrintServerConfig; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.swing.*; +import javax.swing.table.DefaultTableModel; +import java.awt.*; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * 打印机状态面板 + */ +@Component +@Slf4j +public class PrinterStatusPanel extends JPanel { + + private final PrintController printController; + private final PrintServerConfig config; + private JTable printerTable; + private DefaultTableModel printerModel; + private final ScheduledExecutorService refreshExecutor = Executors.newSingleThreadScheduledExecutor(); + + // 使用ApplicationEventPublisher来发布事件,避免循环依赖 + @Autowired + private org.springframework.context.ApplicationEventPublisher eventPublisher; + + @Autowired + public PrinterStatusPanel(PrintController printController, PrintServerConfig config) { + this.printController = printController; + this.config = config; + initializeUI(); + startRefreshTimer(); + } + + /** + * 初始化用户界面 + */ + private void initializeUI() { + setLayout(new BorderLayout()); + setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + + // 创建顶部面板 + JPanel topPanel = new JPanel(new BorderLayout()); + JLabel titleLabel = new JLabel("打印机状态"); + titleLabel.setFont(new Font(titleLabel.getFont().getName(), Font.BOLD, 14)); + topPanel.add(titleLabel, BorderLayout.WEST); + + // 创建刷新按钮 + JButton refreshButton = new JButton("刷新"); + refreshButton.addActionListener(e -> refreshPrinterList()); + topPanel.add(refreshButton, BorderLayout.EAST); + + add(topPanel, BorderLayout.NORTH); + + // 创建表格模型 + String[] columnNames = {"打印机名称", "状态", "默认"}; + printerModel = new DefaultTableModel(columnNames, 0) { + @Override + public boolean isCellEditable(int row, int column) { + return false; // 禁止编辑单元格 + } + + @Override + public Class getColumnClass(int columnIndex) { + return columnIndex == 2 ? Boolean.class : String.class; + } + }; + + // 创建表格 + printerTable = new JTable(printerModel); + printerTable.getTableHeader().setReorderingAllowed(false); + printerTable.setFillsViewportHeight(true); + + // 添加滚动面板 + JScrollPane scrollPane = new JScrollPane(printerTable); + add(scrollPane, BorderLayout.CENTER); + } + + /** + * 启动定时刷新 + */ + private void startRefreshTimer() { + // 增加刷新间隔到60秒,减少刷新频率 + refreshExecutor.scheduleAtFixedRate(this::refreshPrinterList, 0, 60, TimeUnit.SECONDS); + } + + /** + * 刷新打印机列表 + */ + private void refreshPrinterList() { + SwingUtilities.invokeLater(() -> { + try { + List printers = printController.printerList(); + updatePrinterTable(printers); + } catch (Exception e) { + log.error("刷新打印机列表时发生错误", e); + } + }); + } + + /** + * 更新打印机表格 + * + * @param printers 打印机列表 + */ + private void updatePrinterTable(List printers) { + // 清空表格 + printerModel.setRowCount(0); + + if (printers != null && !printers.isEmpty()) { + String defaultPrinter = config.getDefaultPrinter(); + + for (String printer : printers) { + Object[] rowData = new Object[3]; + rowData[0] = printer; + rowData[1] = "可用"; // 默认状态为可用 + rowData[2] = printer.equals(defaultPrinter); // 是否为默认打印机 + printerModel.addRow(rowData); + } + + // 更新PrintSettingsPanel中的打印机下拉列表 + updatePrinterComboBoxes(printers); + } + } + + /** + * 更新所有打印机下拉列表 + * + * @param printers 打印机列表 + */ + private void updatePrinterComboBoxes(List printers) { + // 发布打印机列表更新事件 + eventPublisher.publishEvent(new PrinterListUpdatedEvent(printers)); + // 使用debug级别记录日志,减少info日志输出 + log.debug("已发布打印机列表更新事件,共{}个打印机", printers.size()); + } + + /** + * 打印机列表更新事件 + */ + public static class PrinterListUpdatedEvent { + private final List printers; + + public PrinterListUpdatedEvent(List printers) { + this.printers = printers; + } + + public List getPrinters() { + return printers; + } + } + + /** + * 关闭资源 + */ + public void shutdown() { + refreshExecutor.shutdownNow(); + } +} \ No newline at end of file diff --git a/src/main/java/com/goeing/printserver/main/service/PrintHistoryService.java b/src/main/java/com/goeing/printserver/main/service/PrintHistoryService.java new file mode 100644 index 0000000..432c506 --- /dev/null +++ b/src/main/java/com/goeing/printserver/main/service/PrintHistoryService.java @@ -0,0 +1,168 @@ +package com.goeing.printserver.main.service; + +import com.goeing.printserver.main.domain.PrintTask; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import jakarta.annotation.PreDestroy; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Collectors; + +/** + * 打印历史记录服务,用于保存和查询打印任务的历史记录 + */ +@Service +@Slf4j +public class PrintHistoryService { + + // 使用线程安全的列表存储历史记录 + private final CopyOnWriteArrayList historyTasks = new CopyOnWriteArrayList<>(); + + // 最大历史记录数量 + private static final int MAX_HISTORY_SIZE = 1000; + + /** + * 添加任务到历史记录 + * + * @param task 打印任务 + */ + public void addTaskToHistory(PrintTask task) { + // 创建任务的副本,避免引用原始对象 + PrintTask historyCopy = createTaskCopy(task); + + // 添加到历史记录 + historyTasks.add(historyCopy); + log.debug("添加任务到历史记录: {}", historyCopy.getFileUrl()); + + // 如果历史记录超过最大数量,移除最旧的记录 + if (historyTasks.size() > MAX_HISTORY_SIZE) { + historyTasks.remove(0); + log.debug("历史记录超过最大数量,移除最旧的记录"); + } + } + + /** + * 创建任务的副本 + * + * @param original 原始任务 + * @return 任务副本 + */ + private PrintTask createTaskCopy(PrintTask original) { + PrintTask copy = new PrintTask(); + copy.setFileUrl(original.getFileUrl()); + copy.setPrinter(original.getPrinter()); + copy.setStatus(original.getStatus()); + copy.setQueuedTime(original.getQueuedTime()); + copy.setStartTime(original.getStartTime()); + copy.setEndTime(original.getEndTime()); + copy.setPrintOptions(original.getPrintOptions()); + return copy; + } + + /** + * 获取所有历史记录 + * + * @return 历史记录列表 + */ + public List getAllHistory() { + return Collections.unmodifiableList(new ArrayList<>(historyTasks)); + } + + /** + * 根据打印机名称查询历史记录 + * + * @param printer 打印机名称 + * @return 历史记录列表 + */ + public List getHistoryByPrinter(String printer) { + return historyTasks.stream() + .filter(task -> task.getPrinter().equals(printer)) + .collect(Collectors.toList()); + } + + /** + * 根据状态查询历史记录 + * + * @param status 状态 + * @return 历史记录列表 + */ + public List getHistoryByStatus(String status) { + return historyTasks.stream() + .filter(task -> task.getStatus().equals(status)) + .collect(Collectors.toList()); + } + + /** + * 根据时间范围查询历史记录 + * + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return 历史记录列表 + */ + public List getHistoryByTimeRange(LocalDateTime startTime, LocalDateTime endTime) { + return historyTasks.stream() + .filter(task -> { + LocalDateTime taskTime = task.getQueuedTime(); + return taskTime != null && !taskTime.isBefore(startTime) && !taskTime.isAfter(endTime); + }) + .collect(Collectors.toList()); + } + + /** + * 根据文件URL查询历史记录 + * + * @param fileUrl 文件URL + * @return 历史记录列表 + */ + public List getHistoryByFileUrl(String fileUrl) { + return historyTasks.stream() + .filter(task -> task.getFileUrl().contains(fileUrl)) + .collect(Collectors.toList()); + } + + /** + * 清空历史记录 + */ + public void clearHistory() { + historyTasks.clear(); + log.info("历史记录已清空"); + } + + /** + * 获取历史记录数量 + * + * @return 历史记录数量 + */ + public int getHistoryCount() { + return historyTasks.size(); + } + + /** + * 获取最近的历史记录 + * + * @param count 数量 + * @return 历史记录列表 + */ + public List getRecentHistory(int count) { + int size = historyTasks.size(); + if (size <= count) { + return Collections.unmodifiableList(new ArrayList<>(historyTasks)); + } else { + return Collections.unmodifiableList(new ArrayList<>(historyTasks.subList(size - count, size))); + } + } + + /** + * 在应用程序关闭时执行清理操作 + */ + @PreDestroy + public void shutdown() { + log.info("正在关闭打印历史记录服务..."); + // 可以在这里添加持久化历史记录的逻辑 + log.info("打印历史记录服务已关闭"); + } +} \ No newline at end of file diff --git a/src/main/java/com/goeing/printserver/main/service/PrintQueueService.java b/src/main/java/com/goeing/printserver/main/service/PrintQueueService.java new file mode 100644 index 0000000..da7c09e --- /dev/null +++ b/src/main/java/com/goeing/printserver/main/service/PrintQueueService.java @@ -0,0 +1,394 @@ +package com.goeing.printserver.main.service; + +import com.goeing.printserver.main.domain.dto.WebSocketMessageDTO; +import com.goeing.printserver.main.domain.request.PrintRequest; +import com.goeing.printserver.main.gui.PrintNotificationService; +import com.goeing.printserver.main.gui.PrintStatisticsPanel; +import cn.hutool.json.JSONUtil; +import jakarta.websocket.Session; +import lombok.extern.slf4j.Slf4j; +import jakarta.annotation.PreDestroy; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.time.LocalDateTime; + +/** + * 打印队列服务,用于管理打印任务的队列和执行 + */ +@Service +@Slf4j +public class PrintQueueService { + + @Autowired + private PrintService printService; + + @Autowired + private PrintNotificationService notificationService; + + @Autowired + private PrintStatisticsPanel statisticsPanel; + + @Autowired + private PrintHistoryService historyService; + + // 默认最大队列大小 + private static final int DEFAULT_MAX_QUEUE_SIZE = 10; + + // 当前最大队列大小 + private int maxQueueSize = DEFAULT_MAX_QUEUE_SIZE; + + // 打印任务队列 + private final BlockingQueue printQueue = new LinkedBlockingQueue<>(); + + // 记录当前正在处理的任务 + private PrintTask currentTask; + + // 打印任务线程池,使用单线程确保任务按顺序执行 + private final ThreadPoolExecutor printExecutor = new ThreadPoolExecutor( + 1, // 核心线程数 + 1, // 最大线程数 + 0L, // 空闲线程存活时间 + TimeUnit.MILLISECONDS, + new LinkedBlockingQueue<>() // 工作队列 + ); + + /** + * 打印任务内部类,封装打印请求和WebSocket会话 + */ + private static class PrintTask { + private final PrintRequest printRequest; + private final WebSocketMessageDTO messageDTO; + private final Session session; + private final LocalDateTime queuedTime; + private LocalDateTime startTime; + private LocalDateTime endTime; + private String status; // queued, processing, completed, failed + + public PrintTask(PrintRequest printRequest, WebSocketMessageDTO messageDTO, Session session) { + this.printRequest = printRequest; + this.messageDTO = messageDTO; + this.session = session; + this.queuedTime = LocalDateTime.now(); + this.status = "queued"; + } + + public PrintRequest getPrintRequest() { + return printRequest; + } + + public WebSocketMessageDTO getMessageDTO() { + return messageDTO; + } + + public Session getSession() { + return session; + } + + public LocalDateTime getQueuedTime() { + return queuedTime; + } + + public LocalDateTime getStartTime() { + return startTime; + } + + public void setStartTime(LocalDateTime startTime) { + this.startTime = startTime; + } + + public LocalDateTime getEndTime() { + return endTime; + } + + public void setEndTime(LocalDateTime endTime) { + this.endTime = endTime; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public Map toMap() { + Map map = new HashMap<>(); + map.put("fileUrl", printRequest.getFileUrl()); + map.put("printerName", printRequest.getPrinterName()); + map.put("status", status); + map.put("queuedTime", queuedTime); + if (startTime != null) { + map.put("startTime", startTime); + } + if (endTime != null) { + map.put("endTime", endTime); + } + return map; + } + } + + /** + * 构造函数,启动打印任务处理线程 + */ + public PrintQueueService() { + // 启动打印任务处理线程 + startPrintTaskProcessor(); + } + + /** + * 添加打印任务到队列 + * + * @param printRequest 打印请求 + * @param messageDTO WebSocket消息DTO + * @param session WebSocket会话 + * @return 是否成功添加到队列,如果队列已满则返回false + */ + public boolean addPrintTask(PrintRequest printRequest, WebSocketMessageDTO messageDTO, Session session) { + // 检查队列是否已满 + if (printQueue.size() >= maxQueueSize) { + log.warn("打印队列已满,无法添加新任务: {}, 当前队列长度: {}, 最大队列大小: {}", + printRequest.getFileUrl(), printQueue.size(), maxQueueSize); + return false; + } + + PrintTask task = new PrintTask(printRequest, messageDTO, session); + printQueue.offer(task); + int queueSize = printQueue.size(); + log.info("打印任务已添加到队列: {}, 当前队列长度: {}, 最大队列大小: {}", + printRequest.getFileUrl(), queueSize, maxQueueSize); + + // 发送任务已加入队列的通知 + notificationService.notifyTaskQueued(printRequest, queueSize); + statisticsPanel.incrementTotalTasks(); + return true; + } + + /** + * 启动打印任务处理线程 + */ + private void startPrintTaskProcessor() { + printExecutor.execute(() -> { + while (!Thread.currentThread().isInterrupted()) { + try { + // 从队列中获取打印任务,如果队列为空则阻塞等待 + PrintTask task = printQueue.take(); + processPrintTask(task); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("打印任务处理线程被中断", e); + } catch (Exception e) { + log.error("处理打印任务时发生错误", e); + } + } + }); + log.info("打印任务处理线程已启动"); + } + + /** + * 处理打印任务 + * + * @param task 打印任务 + */ + private void processPrintTask(PrintTask task) { + PrintRequest printRequest = task.getPrintRequest(); + WebSocketMessageDTO messageDTO = task.getMessageDTO(); + Session session = task.getSession(); + + // 更新任务状态 + currentTask = task; + task.setStartTime(LocalDateTime.now()); + task.setStatus("processing"); + log.info("开始处理打印任务: {}", printRequest.getFileUrl()); + + // 发送任务开始处理的通知 + notificationService.notifyTaskStarted(printRequest, printQueue.size()); + + try { + // 执行打印 + printService.print(printRequest); + log.info("打印任务完成: {}", printRequest.getFileUrl()); + + // 更新任务状态 + task.setEndTime(LocalDateTime.now()); + task.setStatus("completed"); + + // 发送任务完成的通知 + notificationService.notifyTaskCompleted(printRequest, printQueue.size()); + statisticsPanel.incrementCompletedTasks(); + historyService.addTaskToHistory(convertToHistoryTask(task)); + + // 发送成功响应 + Map map = new HashMap<>(); + map.put("status", "success"); + map.put("msg", ""); + + messageDTO.setType("RESPONSE"); + messageDTO.setPayload(JSONUtil.toJsonStr(map)); + session.getBasicRemote().sendText(JSONUtil.toJsonStr(messageDTO)); + } catch (Exception e) { + log.error("打印失败: {}", printRequest.getFileUrl(), e); + + // 更新任务状态 + task.setEndTime(LocalDateTime.now()); + task.setStatus("failed"); + + // 获取错误消息 + String errorMsg = e.getMessage(); + if (errorMsg == null) { + errorMsg = "未知错误"; + } else { + // 转义JSON特殊字符 + errorMsg = errorMsg.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n"); + } + + // 发送任务失败的通知 + notificationService.notifyTaskFailed(printRequest, errorMsg, printQueue.size()); + statisticsPanel.incrementFailedTasks(); + historyService.addTaskToHistory(convertToHistoryTask(task)); + + Map map = new HashMap<>(); + map.put("status", "fail"); + map.put("msg", errorMsg); + messageDTO.setType("RESPONSE"); + messageDTO.setPayload(JSONUtil.toJsonStr(map)); + + try { + session.getBasicRemote().sendText(JSONUtil.toJsonStr(messageDTO)); + } catch (IOException ex) { + log.error("发送打印失败消息时发生错误", ex); + } + } finally { + currentTask = null; + } + } + + /** + * 获取当前队列长度 + * + * @return 队列长度 + */ + public int getQueueSize() { + return printQueue.size(); + } + + /** + * 获取当前正在处理的任务信息 + * + * @return 当前任务信息,如果没有则返回null + */ + public Map getCurrentTaskInfo() { + if (currentTask == null) { + return null; + } + return currentTask.toMap(); + } + + /** + * 获取当前正在处理的任务 + * + * @return 当前任务,如果没有则返回null + */ + public com.goeing.printserver.main.domain.PrintTask getCurrentTask() { + if (currentTask == null) { + return null; + } + return convertToHistoryTask(currentTask); + } + + /** + * 获取队列中所有任务的信息 + * + * @return 任务信息列表 + */ + public List> getQueuedTasksInfo() { + List> tasksInfo = new ArrayList<>(); + for (PrintTask task : printQueue) { + tasksInfo.add(task.toMap()); + } + return tasksInfo; + } + + /** + * 获取队列中所有任务 + * + * @return 任务列表 + */ + public List getQueuedTasks() { + List tasks = new ArrayList<>(); + for (PrintTask task : printQueue) { + tasks.add(convertToHistoryTask(task)); + } + return tasks; + } + + /** + * 设置最大队列大小 + * + * @param maxQueueSize 最大队列大小 + */ + public void setMaxQueueSize(int maxQueueSize) { + if (maxQueueSize < 1) { + log.warn("最大队列大小不能小于1,设置为默认值: {}", DEFAULT_MAX_QUEUE_SIZE); + this.maxQueueSize = DEFAULT_MAX_QUEUE_SIZE; + } else { + this.maxQueueSize = maxQueueSize; + log.info("最大队列大小已设置为: {}", maxQueueSize); + } + } + + /** + * 获取最大队列大小 + * + * @return 最大队列大小 + */ + public int getMaxQueueSize() { + return maxQueueSize; + } + + /** + * 关闭打印任务处理线程池 + */ + @PreDestroy + public void shutdown() { + log.info("正在关闭打印队列服务..."); + printExecutor.shutdown(); + try { + if (!printExecutor.awaitTermination(5, TimeUnit.SECONDS)) { + printExecutor.shutdownNow(); + } + } catch (InterruptedException e) { + printExecutor.shutdownNow(); + Thread.currentThread().interrupt(); + } + log.info("打印任务处理线程池已关闭"); + } + + /** + * 将内部PrintTask转换为域对象PrintTask + * + * @param task 内部PrintTask对象 + * @return 域对象PrintTask + */ + private com.goeing.printserver.main.domain.PrintTask convertToHistoryTask(PrintTask task) { + com.goeing.printserver.main.domain.PrintTask historyTask = new com.goeing.printserver.main.domain.PrintTask(); + historyTask.setFileUrl(task.getPrintRequest().getFileUrl()); + historyTask.setPrinter(task.getPrintRequest().getPrinterName()); + historyTask.setStatus(task.getStatus()); + historyTask.setQueuedTime(task.getQueuedTime()); + historyTask.setStartTime(task.getStartTime()); + historyTask.setEndTime(task.getEndTime()); + historyTask.setPrintOptions(task.getPrintRequest().getPrintOption()); + return historyTask; + } +} \ No newline at end of file diff --git a/src/main/java/com/goeing/printserver/main/service/PrintService.java b/src/main/java/com/goeing/printserver/main/service/PrintService.java new file mode 100644 index 0000000..fede80e --- /dev/null +++ b/src/main/java/com/goeing/printserver/main/service/PrintService.java @@ -0,0 +1,18 @@ +package com.goeing.printserver.main.service; + +import com.goeing.printserver.main.domain.request.PrintRequest; + +/** + * 打印服务接口,定义打印相关操作 + * 用于解决PrintController和PrintQueueService之间的循环依赖 + */ +public interface PrintService { + + /** + * 执行打印操作 + * + * @param request 打印请求 + * @return 打印结果 + */ + String print(PrintRequest request); +} \ No newline at end of file diff --git a/src/main/java/com/goeing/printserver/main/sse/PrinterClient.java b/src/main/java/com/goeing/printserver/main/sse/PrinterClient.java index f2af61d..f51a665 100644 --- a/src/main/java/com/goeing/printserver/main/sse/PrinterClient.java +++ b/src/main/java/com/goeing/printserver/main/sse/PrinterClient.java @@ -6,6 +6,7 @@ import com.alibaba.fastjson.JSON; import com.goeing.printserver.main.PrintController; import com.goeing.printserver.main.domain.dto.WebSocketMessageDTO; import com.goeing.printserver.main.domain.request.PrintRequest; +import com.goeing.printserver.main.service.PrintQueueService; import jakarta.websocket.*; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -27,7 +28,8 @@ import java.util.stream.Collectors; @ClientEndpoint @Component @Slf4j -public class PrinterClient implements ApplicationRunner { +public class PrinterClient implements ApplicationRunner { + private final PrintQueueService printQueueService; private Session session; @Value("${print.printer.id}") private String printerId; @@ -40,8 +42,9 @@ public class PrinterClient implements ApplicationRunner { private final ScheduledExecutorService heartbeatExecutor = Executors.newSingleThreadScheduledExecutor(); private boolean isConnecting = false; - // 无参构造函数,由Spring管理 - public PrinterClient() { + // 构造函数,注入PrintQueueService + public PrinterClient(PrintQueueService printQueueService) { + this.printQueueService = printQueueService; // 构造函数不做连接操作,在run方法中进行连接 } @@ -63,47 +66,31 @@ public class PrinterClient implements ApplicationRunner { if ("print".equals(type)) { String payload = webSocketMessageDTO.getPayload(); PrintRequest printRequest = JSONUtil.toBean(payload, PrintRequest.class); - PrintController bean = SpringUtil.getBean(PrintController.class); - - - // 处理打印任务 - log.info("收到打印任务: {}, ", printRequest); + + // 将打印任务添加到队列 + log.info("收到打印任务: {}, 添加到打印队列", printRequest); + printQueueService.addPrintTask(printRequest, webSocketMessageDTO, session); + + // 发送任务已接收的确认消息 + Map map = new HashMap<>(); + map.put("status", "queued"); + map.put("msg", "打印任务已加入队列,等待处理"); + map.put("queueSize", String.valueOf(printQueueService.getQueueSize())); + + WebSocketMessageDTO queuedResponse = new WebSocketMessageDTO(); + queuedResponse.setType("QUEUED"); + queuedResponse.setRequestId(webSocketMessageDTO.getRequestId()); + queuedResponse.setPayload(JSONUtil.toJsonStr(map)); try { - bean.print(printRequest); - log.info("打印任务完成: {}", printRequest.getFileUrl()); - - Map map = new HashMap<>(); - map.put("status", "success"); - map.put("msg", ""); - - webSocketMessageDTO.setType("RESPONSE"); - webSocketMessageDTO.setPayload(JSONUtil.toJsonStr(map)); - session.getBasicRemote().sendText(JSONUtil.toJsonStr(webSocketMessageDTO)); - } catch (Exception e) { - log.error("打印失败: {}", printRequest.getFileUrl(), e); - // 发送打印失败消息 - String errorMsg = e.getMessage(); - if (errorMsg == null) { - errorMsg = "未知错误"; - } else { - // 转义JSON特殊字符 - errorMsg = errorMsg.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n"); - } - - Map map = new HashMap<>(); - map.put("status", "fail"); - map.put("msg", errorMsg); - webSocketMessageDTO.setType("RESPONSE"); - webSocketMessageDTO.setPayload(JSONUtil.toJsonStr(map)); - - session.getBasicRemote().sendText(JSONUtil.toJsonStr(webSocketMessageDTO)); + session.getBasicRemote().sendText(JSONUtil.toJsonStr(queuedResponse)); + } catch (IOException e) { + log.error("发送队列确认消息失败", e); } } else if ("heartbeat_ack".equals(type)) { // 心跳响应,可以记录最后一次心跳时间 log.debug("收到心跳响应"); - }else if ("printerList".equals(type)) { - + } else if ("printerList".equals(type)) { PrintService[] printServices = PrinterJob.lookupPrintServices(); Set collect = Arrays.stream(printServices).map(PrintService::getName).collect(Collectors.toSet()); @@ -112,6 +99,26 @@ public class PrinterClient implements ApplicationRunner { webSocketMessageDTO.setType("RESPONSE"); webSocketMessageDTO.setPayload(JSONUtil.toJsonStr(collect1)); session.getBasicRemote().sendText(JSONUtil.toJsonStr(webSocketMessageDTO)); + } else if ("queueStatus".equals(type)) { + // 返回当前打印队列状态 + Map status = new HashMap<>(); + status.put("queueSize", printQueueService.getQueueSize()); + status.put("timestamp", System.currentTimeMillis()); + status.put("currentTask", printQueueService.getCurrentTaskInfo()); + + webSocketMessageDTO.setType("RESPONSE"); + webSocketMessageDTO.setPayload(JSONUtil.toJsonStr(status)); + session.getBasicRemote().sendText(JSONUtil.toJsonStr(webSocketMessageDTO)); + } else if ("queueTasks".equals(type)) { + // 返回打印队列中的所有任务 + Map result = new HashMap<>(); + result.put("currentTask", printQueueService.getCurrentTaskInfo()); + result.put("queuedTasks", printQueueService.getQueuedTasksInfo()); + result.put("timestamp", System.currentTimeMillis()); + + webSocketMessageDTO.setType("RESPONSE"); + webSocketMessageDTO.setPayload(JSONUtil.toJsonStr(result)); + session.getBasicRemote().sendText(JSONUtil.toJsonStr(webSocketMessageDTO)); } } catch (Exception e) { diff --git a/src/main/java/com/goeing/printserver/main/utils/PdfPrinter.java b/src/main/java/com/goeing/printserver/main/utils/PdfPrinter.java index 57d3cb9..015aeda 100644 --- a/src/main/java/com/goeing/printserver/main/utils/PdfPrinter.java +++ b/src/main/java/com/goeing/printserver/main/utils/PdfPrinter.java @@ -8,7 +8,7 @@ import org.apache.pdfbox.printing.Orientation; import org.apache.pdfbox.printing.PDFPrintable; import org.apache.pdfbox.printing.Scaling; -import javax.print.PrintService; +// 使用完全限定名称避免与自定义PrintService接口冲突 import javax.print.PrintServiceLookup; import javax.print.attribute.HashPrintRequestAttributeSet; import javax.print.attribute.PrintRequestAttributeSet; @@ -125,12 +125,12 @@ public class PdfPrinter { return false; } - PrintService[] services = PrintServiceLookup.lookupPrintServices(null, null); + javax.print.PrintService[] services = PrintServiceLookup.lookupPrintServices(null, null); if (services == null || services.length == 0) { return false; } - for (PrintService service : services) { + for (javax.print.PrintService service : services) { if (service.getName().equalsIgnoreCase(printerName)) { return true; } @@ -151,13 +151,13 @@ public class PdfPrinter { throw new IllegalArgumentException("Printer name cannot be null or empty"); } - PrintService[] services = PrintServiceLookup.lookupPrintServices(null, null); + javax.print.PrintService[] services = PrintServiceLookup.lookupPrintServices(null, null); if (services == null || services.length == 0) { throw new RuntimeException("No working printers found"); } - for (PrintService service : services) { + for (javax.print.PrintService service : services) { if (service.getName().equalsIgnoreCase(printerName)) { PrinterJob job = PrinterJob.getPrinterJob(); job.setPrintService(service); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 3a26a2c..4062958 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -5,3 +5,15 @@ print.websocket.url=ws://127.0.0.1:8080/print-websocket print.printer.id=123456 print.websocket.apiKey=519883ab-3677-ce4b-59ba-7263870d0a26 +# 临时允许循环依赖,后续应该通过重构完全消除 +spring.main.allow-circular-references=true + +# 在macOS系统上,如果遇到HeadlessException,可以设置为true强制使用无头模式 +# 或者在启动时添加JVM参数:-Djava.awt.headless=true +app.force.headless=false + +# 日志配置 +# 设置打印机状态面板和设置面板的日志级别为WARN,减少日志输出 +logging.level.com.goeing.printserver.main.gui.PrinterStatusPanel=WARN +logging.level.com.goeing.printserver.main.gui.PrintSettingsPanel=WARN + diff --git a/start-printserver.sh b/start-printserver.sh new file mode 100755 index 0000000..591cc5b --- /dev/null +++ b/start-printserver.sh @@ -0,0 +1,89 @@ +#!/bin/bash + +# 打印服务器启动脚本 +# 此脚本提供了多种启动模式,适应不同的环境需求 + +# 默认配置 +JAR_FILE="target/goeingPrintServer.jar" +JAVA_OPTS="" +HEADLESS_MODE=false +DEBUG_MODE=false +MEMORY="512m" + +# 显示帮助信息 +show_help() { + echo "打印服务器启动脚本" + echo "用法: $0 [选项]" + echo "" + echo "选项:" + echo " -h, --help 显示此帮助信息" + echo " -j, --jar FILE 指定JAR文件路径 (默认: $JAR_FILE)" + echo " --headless 以无头模式运行 (无图形界面)" + echo " --debug 启用远程调试 (端口: 5005)" + echo " -m, --memory SIZE 设置最大内存 (默认: $MEMORY)" + echo "" + echo "示例:" + echo " $0 --headless 以无头模式启动服务器" + echo " $0 --memory 1g 设置最大内存为1GB" + echo " $0 --jar custom.jar 使用自定义JAR文件" + exit 0 +} + +# 解析命令行参数 +while [[ $# -gt 0 ]]; do + case $1 in + -h|--help) + show_help + ;; + -j|--jar) + JAR_FILE="$2" + shift + ;; + --headless) + HEADLESS_MODE=true + ;; + --debug) + DEBUG_MODE=true + ;; + -m|--memory) + MEMORY="$2" + shift + ;; + *) + echo "未知选项: $1" + echo "使用 --help 查看帮助信息" + exit 1 + ;; + esac + shift +done + +# 检查JAR文件是否存在 +if [ ! -f "$JAR_FILE" ]; then + echo "错误: JAR文件 '$JAR_FILE' 不存在" + echo "请先构建项目或使用 --jar 选项指定正确的JAR文件路径" + exit 1 +fi + +# 构建Java选项 +JAVA_OPTS="$JAVA_OPTS -Xmx$MEMORY" + +# 添加无头模式选项 +if [ "$HEADLESS_MODE" = true ]; then + JAVA_OPTS="$JAVA_OPTS -Djava.awt.headless=true" + echo "启用无头模式 (无图形界面)" +fi + +# 添加调试选项 +if [ "$DEBUG_MODE" = true ]; then + JAVA_OPTS="$JAVA_OPTS -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005" + echo "启用远程调试模式 (端口: 5005)" +fi + +# 启动应用程序 +echo "正在启动打印服务器..." +echo "使用JAR文件: $JAR_FILE" +echo "Java选项: $JAVA_OPTS" +echo "" + +java $JAVA_OPTS -jar "$JAR_FILE" \ No newline at end of file