Compare commits

...

7 Commits

Author SHA1 Message Date
lifangliang
191c6dfce7 feat: 新增默认打印机设置 2025-08-29 17:48:27 +08:00
lifangliang
478f8c4549 feat: 新增取消任务 2025-08-29 17:48:19 +08:00
lifangliang
41ad507f68 fix: 打印任务不显示的问题 2025-08-29 17:48:19 +08:00
lifangliang
93bef5c0ea fix: 格式化时间 2025-08-29 17:48:19 +08:00
lifangliang
d00cf7a5c2 fix: 修复ws连接心跳的问题 2025-08-29 17:48:19 +08:00
lifangliang
ae2f6366c4 fix: 修复日志输出bug 2025-08-20 10:22:02 +08:00
lifangliang
cf8e27d18f feat: 计算页边距 2025-08-20 09:19:24 +08:00
8 changed files with 288 additions and 61 deletions

View File

@ -8,6 +8,7 @@ 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 com.goeing.printserver.main.ws.PrinterClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@ -15,13 +16,12 @@ import org.springframework.web.bind.annotation.*;
// 使用完全限定名称避免与自定义PrintService接口冲突
import java.awt.print.PrinterJob;
import java.io.File;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@RestController
@ -36,7 +36,7 @@ public class PrintController implements PrintService {
private PrintServerConfig config;
@Autowired
private com.goeing.printserver.main.sse.PrinterClient printerClient;
private PrinterClient printerClient;
private final String rootPath = System.getProperty("java.io.tmpdir") + File.separator + "goeingprint" + File.separator + "pdfTemp";
@ -107,6 +107,51 @@ public class PrintController implements PrintService {
return result;
}
/**
* 清空打印队列
*
* @return 清空结果
*/
@DeleteMapping("queue/clear")
public Map<String, Object> clearQueue() {
int clearedCount = printQueueService.clearQueue();
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("clearedCount", clearedCount);
result.put("message", "队列已清空,共清空 " + clearedCount + " 个任务");
result.put("timestamp", System.currentTimeMillis());
return result;
}
/**
* 取消单个任务
*
* @param taskId 任务ID
* @return 取消结果
*/
@DeleteMapping("queue/task/{taskId}")
public Map<String, Object> cancelTask(@PathVariable String taskId) {
Map<String, Object> result = new HashMap<>();
try {
boolean cancelled = printQueueService.cancelTask(taskId);
if (cancelled) {
result.put("success", true);
result.put("message", "任务已取消");
result.put("taskId", taskId);
} else {
result.put("success", false);
result.put("message", "未找到指定任务");
result.put("taskId", taskId);
}
} catch (Exception e) {
result.put("success", false);
result.put("message", "取消任务失败: " + e.getMessage());
result.put("taskId", taskId);
}
result.put("timestamp", System.currentTimeMillis());
return result;
}
/**
* 搜索打印任务
*
@ -145,7 +190,13 @@ public class PrintController implements PrintService {
taskMap.put("fileName", extractFileName(task.getFileUrl()));
taskMap.put("printer", task.getPrinter());
taskMap.put("status", task.getStatus());
taskMap.put("createTime", task.getQueuedTime());
// 格式化创建时间为 yyyy-MM-dd HH:mm:ss
if (task.getQueuedTime() != null) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
taskMap.put("createTime", task.getQueuedTime().format(formatter));
} else {
taskMap.put("createTime", "N/A");
}
taskMap.put("fileUrl", task.getFileUrl());
return taskMap;
})

View File

@ -20,13 +20,19 @@ public class LogbackConfig {
public void registerAppender() {
LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
// // 添加 appender root logger
// Spring 管理的 appender 关联到 Logback 的上下文
memoryLogAppender.setContext(context);
// / 添加 appender root logger供其它包使用
Logger rootLogger = context.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
rootLogger.addAppender(memoryLogAppender);
// 同时添加到 com.goeing.printserver 包级 logger logger 配置了 additivity=false
Logger appLogger = context.getLogger("com.goeing.printserver");
appLogger.addAppender(memoryLogAppender);
// 启动 appender
memoryLogAppender.start();
// 可选也加到你的包 logger
// Logger appLogger = context.getLogger("com.goeing.printserver");
// appLogger.addAppender(memoryLogAppender);
}
}

View File

@ -4,6 +4,7 @@ import com.goeing.printserver.main.domain.bo.PrintOption;
import lombok.Data;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
@ -40,16 +41,27 @@ public class PrintTask {
*/
public Map<String, Object> toMap() {
Map<String, Object> map = new HashMap<>();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
map.put("fileUrl", fileUrl);
map.put("printerName", printer);
map.put("status", status);
map.put("queuedTime", queuedTime);
// 格式化时间字段
if (queuedTime != null) {
map.put("queuedTime", queuedTime.format(formatter));
} else {
map.put("queuedTime", "N/A");
}
if (startTime != null) {
map.put("startTime", startTime);
map.put("startTime", startTime.format(formatter));
}
if (endTime != null) {
map.put("endTime", endTime);
map.put("endTime", endTime.format(formatter));
}
return map;
}
}

View File

@ -12,6 +12,7 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@ -63,6 +64,7 @@ public class PrintQueueService {
* 打印任务内部类封装打印请求和WebSocket会话
*/
private static class PrintTask {
private final String id;
private final PrintRequest printRequest;
private final WebSocketMessageDTO messageDTO;
private final Session session;
@ -72,12 +74,17 @@ public class PrintQueueService {
private String status; // queued, processing, completed, failed
public PrintTask(PrintRequest printRequest, WebSocketMessageDTO messageDTO, Session session) {
this.id = "TASK_" + System.currentTimeMillis() + "_" + (int)(Math.random() * 1000);
this.printRequest = printRequest;
this.messageDTO = messageDTO;
this.session = session;
this.queuedTime = LocalDateTime.now();
this.status = "queued";
}
public String getId() {
return id;
}
public PrintRequest getPrintRequest() {
return printRequest;
@ -121,16 +128,28 @@ public class PrintQueueService {
public Map<String, Object> toMap() {
Map<String, Object> map = new HashMap<>();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
map.put("id", id);
map.put("fileUrl", printRequest.getFileUrl());
map.put("printerName", printRequest.getPrinterName());
map.put("status", status);
map.put("queuedTime", queuedTime);
// 格式化时间字段
if (queuedTime != null) {
map.put("queuedTime", queuedTime.format(formatter));
} else {
map.put("queuedTime", "N/A");
}
if (startTime != null) {
map.put("startTime", startTime);
map.put("startTime", startTime.format(formatter));
}
if (endTime != null) {
map.put("endTime", endTime);
map.put("endTime", endTime.format(formatter));
}
return map;
}
}
@ -212,6 +231,7 @@ public class PrintQueueService {
try {
// 执行打印
// Thread.sleep(20000L);
printService.print(printRequest);
log.info("打印任务完成: {}", printRequest.getFileUrl());
@ -350,6 +370,43 @@ public class PrintQueueService {
return maxQueueSize;
}
/**
* 清空打印队列
* 注意此操作不会影响当前正在处理的任务
*
* @return 清空的任务数量
*/
public int clearQueue() {
int clearedCount = printQueue.size();
printQueue.clear();
log.info("打印队列已清空,共清空 {} 个任务", clearedCount);
return clearedCount;
}
/**
* 取消指定任务
* @param taskId 任务ID
* @return 是否成功取消
*/
public boolean cancelTask(String taskId) {
// 检查当前任务
if (currentTask != null && taskId.equals(currentTask.getId())) {
log.info("取消当前正在执行的任务: {}", taskId);
currentTask.setStatus("cancelled");
currentTask = null;
return true;
}
// 从队列中移除任务
boolean removed = printQueue.removeIf(task -> taskId.equals(task.getId()));
if (removed) {
log.info("成功从队列中取消任务: {}", taskId);
} else {
log.warn("未找到要取消的任务: {}", taskId);
}
return removed;
}
/**
* 获取历史服务实例
*

View File

@ -2,15 +2,12 @@ package com.goeing.printserver.main.utils;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.AppenderBase;
import cn.hutool.extra.spring.SpringUtil;
import lombok.Getter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;
/**
* 内存日志追加器用于缓存日志到内存中

View File

@ -1,10 +1,8 @@
package com.goeing.printserver.main.utils;// src/main/java/com/example/printer/PdfPrinter.java
package com.goeing.printserver.main.utils;
import cn.hutool.core.util.StrUtil;
import com.goeing.printserver.main.domain.bo.PrintOption;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.printing.PDFPageable;
import org.apache.pdfbox.printing.Orientation;
import org.apache.pdfbox.printing.PDFPrintable;
import org.apache.pdfbox.printing.Scaling;
@ -30,6 +28,10 @@ import java.util.Map;
public class PdfPrinter {
private static final Map<String, MediaSizeName> PAPER_SIZES = new HashMap<>();
// 装订边距常量英寸
private static final double BINDING_MARGIN_LEFT = 0.5; // 左装订额外边距
private static final double BINDING_MARGIN_TOP = 0.5; // 顶装订额外边距
static {
PAPER_SIZES.put("Letter", MediaSizeName.NA_LETTER); // 8.5" × 11"
@ -77,9 +79,7 @@ public class PdfPrinter {
String color = option.getColor();
//如果是封面彩打 那么要把封面和 内容分开打印 就做两个打印任务 先打封面 再打内容
if (color.equalsIgnoreCase("Cover Letter Color Only")){
if ("Cover Letter Color Only".equals(color)) {
PDDocument cover = new PDDocument();
cover.addPage(document.getPage(0));
cover.close();
@ -87,7 +87,7 @@ public class PdfPrinter {
PDDocument content = new PDDocument();
for (int i = 0; i < document.getNumberOfPages(); i++) {
//第一页是封面
if (i==0){
if (i == 0) {
continue;
}
content.addPage(document.getPage(i));
@ -99,7 +99,10 @@ public class PdfPrinter {
option.setColor("black & white");
setPageStyle(content, job, option);
}else {
} else {
if (StrUtil.containsIgnoreCase(color,"color")){
option.setColor("color");
}
//全部打印
setPageStyle(document, job, option);
}
@ -265,8 +268,8 @@ public class PdfPrinter {
// 获取边距默认为0.5英寸
double marginInches = option.getMargin();
// 创建并返回Paper对象
return createPaper(dimensions[0], dimensions[1], marginInches);
// 创建并返回Paper对象考虑装订选项
return createPaper(dimensions[0], dimensions[1], marginInches, option.getPosition());
}
/**
@ -382,22 +385,52 @@ public class PdfPrinter {
*
* @param width 纸张宽度
* @param height 纸张高度
* @param marginInches 边距英寸
* @param marginInches 基础边距英寸
* @param bindingOption 装订选项
* @return 配置好的Paper对象
*/
private static Paper createPaper(double width, double height, double marginInches) {
private static Paper createPaper(double width, double height, double marginInches, String bindingOption) {
Paper paper = new Paper();
paper.setSize(width, height);
// 计算各边的边距考虑装订选项
double leftMargin = marginInches;
double topMargin = marginInches;
double rightMargin = marginInches;
double bottomMargin = marginInches;
// 根据装订选项调整边距
if (bindingOption != null) {
switch (bindingOption) {
case "Left":
leftMargin += BINDING_MARGIN_LEFT;
break;
case "Top":
topMargin += BINDING_MARGIN_TOP;
break;
case "Staple":
leftMargin += BINDING_MARGIN_LEFT;
topMargin += BINDING_MARGIN_TOP;
break;
case "None":
default:
// 不调整边距
break;
}
}
// 将边距从英寸转换为点
double marginPoints = marginInches * 72;
double leftMarginPoints = leftMargin * 72;
double topMarginPoints = topMargin * 72;
double rightMarginPoints = rightMargin * 72;
double bottomMarginPoints = bottomMargin * 72;
// 设置可打印区域
paper.setImageableArea(
marginPoints,
marginPoints,
width - 2 * marginPoints,
height - 2 * marginPoints
leftMarginPoints,
topMarginPoints,
width - leftMarginPoints - rightMarginPoints,
height - topMarginPoints - bottomMarginPoints
);
return paper;

View File

@ -1,4 +1,4 @@
package com.goeing.printserver.main.sse;
package com.goeing.printserver.main.ws;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
@ -21,6 +21,7 @@ import java.net.URI;
import java.util.*;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@ -32,9 +33,12 @@ public class PrinterClient implements ApplicationRunner {
private final PrintServerConfig config;
private Session session;
private final ScheduledExecutorService reconnectExecutor = Executors.newSingleThreadScheduledExecutor();
private final ScheduledExecutorService heartbeatExecutor = Executors.newSingleThreadScheduledExecutor();
private boolean isConnecting = false;
private final ScheduledExecutorService connectionMonitor = Executors.newSingleThreadScheduledExecutor();
private volatile ScheduledFuture<?> heartbeatTask;
private volatile boolean isConnecting = false;
private int reconnectAttempts = 0;
private static final int MAX_RECONNECT_ATTEMPTS = 10;
// 构造函数注入PrintQueueService和PrintServerConfig
public PrinterClient(@Lazy PrintQueueService printQueueService, PrintServerConfig config) {
@ -48,6 +52,8 @@ public class PrinterClient implements ApplicationRunner {
this.session = session;
log.info("WebSocket连接已建立");
isConnecting = false;
reconnectAttempts = 0; // 重置重连计数
startHeartbeat();
}
@OnMessage
@ -89,10 +95,18 @@ public class PrinterClient implements ApplicationRunner {
PrintService[] printServices = PrinterJob.lookupPrintServices();
Set<String> collect = Arrays.stream(printServices).map(PrintService::getName).collect(Collectors.toSet());
List<String> collect1 = collect.stream().sorted().collect(Collectors.toList());
List<String> printerList = collect.stream().sorted().collect(Collectors.toList());
// 获取设置中的默认打印机
String defaultPrinter = config.getDefaultPrinter();
// 构建响应对象
Map<String, Object> response = new HashMap<>();
response.put("printerList", printerList);
response.put("defaultPrinter", defaultPrinter);
webSocketMessageDTO.setType("RESPONSE");
webSocketMessageDTO.setPayload(JSONUtil.toJsonStr(collect1));
webSocketMessageDTO.setPayload(JSONUtil.toJsonStr(response));
session.getBasicRemote().sendText(JSONUtil.toJsonStr(webSocketMessageDTO));
} else if ("queueStatus".equals(type)) {
// 返回当前打印队列状态
@ -125,25 +139,22 @@ public class PrinterClient implements ApplicationRunner {
public void onClose(Session session, CloseReason closeReason) {
log.warn("WebSocket连接关闭: {}", closeReason.getReasonPhrase());
this.session = null;
// 安排重连任务
scheduleReconnect();
// 连接监控器会自动处理重连
}
@OnError
public void onError(Session session, Throwable throwable) {
log.error("WebSocket连接发生错误", throwable);
this.session = null;
// 安排重连任务
scheduleReconnect();
// 连接监控器会自动处理重连
}
/**
* 连接到WebSocket服务器
*/
private void connect() {
if (isConnecting || (session != null && session.isOpen())) {
return; // 已经连接或正在连接中
if (isConnecting) {
return; // 正在连接中
}
// 从配置对象中获取最新的连接参数
@ -151,6 +162,11 @@ public class PrinterClient implements ApplicationRunner {
String printerId = config.getPrinterId();
String apiKey = config.getApiKey();
if (serverUri == null || serverUri.trim().isEmpty()) {
log.warn("WebSocket URL未配置跳过连接");
return;
}
// 添加调试日志
log.info("当前配置 - WebSocket URL: {}, PrinterId: {}, ApiKey: {}", serverUri, printerId, apiKey);
@ -165,9 +181,8 @@ public class PrinterClient implements ApplicationRunner {
container.connectToServer(this, new URI(tempUrl));
} catch (Exception e) {
log.error("连接到WebSocket服务器失败", e);
} finally {
isConnecting = false;
// 连接失败安排重连
scheduleReconnect();
}
}
@ -175,27 +190,63 @@ public class PrinterClient implements ApplicationRunner {
* 启动心跳机制
*/
private void startHeartbeat() {
heartbeatExecutor.scheduleAtFixedRate(() -> {
// 取消之前的心跳任务
if (heartbeatTask != null && !heartbeatTask.isCancelled()) {
heartbeatTask.cancel(false);
}
heartbeatTask = heartbeatExecutor.scheduleAtFixedRate(() -> {
if (session != null && session.isOpen()) {
try {
session.getBasicRemote().sendText("{\"type\":\"heartbeat\"}");
log.debug("发送心跳");
} catch (IOException e) {
log.error("发送心跳失败", e);
log.debug("发送心跳失败", e);
}
}
}, 30, 30, TimeUnit.SECONDS);
}, 30, 20, TimeUnit.SECONDS);
log.info("心跳机制已启动");
}
/**
* 安排重连任务
* 启动连接监控
*/
private void scheduleReconnect() {
reconnectExecutor.schedule(() -> {
log.info("尝试重新连接到WebSocket服务器...");
connect();
}, 5, TimeUnit.SECONDS);
private void startConnectionMonitor() {
connectionMonitor.scheduleWithFixedDelay(() -> {
try {
checkAndReconnect();
} catch (Exception e) {
log.error("连接监控任务执行失败", e);
}
}, 10, 20, TimeUnit.SECONDS); // 每10秒检查一次
log.info("连接监控已启动每10秒检查一次连接状态");
}
/**
* 检查连接状态并在需要时重连
*/
private void checkAndReconnect() {
if (isConnected()) {
// 连接正常重置重连计数
reconnectAttempts = 0;
log.debug("WebSocket连接状态正常");
return;
}
if (isConnecting) {
log.debug("正在连接中,跳过此次检查");
return;
}
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
log.error("重连次数已达上限({}次),停止重连", MAX_RECONNECT_ATTEMPTS);
return;
}
log.info("检测到连接断开,开始重连...(第{}次尝试)", reconnectAttempts + 1);
reconnectAttempts++;
connect();
}
/**
@ -204,6 +255,16 @@ public class PrinterClient implements ApplicationRunner {
@PreDestroy
public void shutdown() {
log.info("正在关闭WebSocket客户端...");
// 取消心跳任务
if (heartbeatTask != null && !heartbeatTask.isCancelled()) {
heartbeatTask.cancel(false);
}
// 停止连接监控和心跳
connectionMonitor.shutdownNow();
heartbeatExecutor.shutdownNow();
if (session != null) {
try {
session.close();
@ -211,8 +272,6 @@ public class PrinterClient implements ApplicationRunner {
log.error("关闭WebSocket连接失败", e);
}
}
reconnectExecutor.shutdownNow();
heartbeatExecutor.shutdownNow();
log.info("WebSocket客户端已关闭");
}
@ -221,6 +280,12 @@ public class PrinterClient implements ApplicationRunner {
*/
public void reconnect() {
log.info("配置已更改重新连接WebSocket服务器...");
// 停止旧的心跳任务
if (heartbeatTask != null && !heartbeatTask.isCancelled()) {
heartbeatTask.cancel(false);
}
if (session != null && session.isOpen()) {
try {
session.close();
@ -230,6 +295,10 @@ public class PrinterClient implements ApplicationRunner {
}
session = null;
isConnecting = false;
// 重置重连计数
reconnectAttempts = 0;
connect();
}
@ -270,6 +339,8 @@ public class PrinterClient implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) {
// 启动连接监控
startConnectionMonitor();
// 应用启动后连接到WebSocket服务器
connect();
}

View File

@ -42,8 +42,8 @@
</logger>
<!-- Spring Boot 相关日志 -->
<logger name="org.springframework" level="INFO" />
<logger name="org.springframework.web" level="DEBUG" />
<logger name="org.springframework" level="WARN" />
<logger name="org.springframework.web" level="WARN" />
<!-- Hibernate 相关日志 -->
<logger name="org.hibernate" level="WARN" />