|
|
|
@ -16,11 +16,15 @@ import java.net.InetSocketAddress; |
|
|
|
import java.net.Socket; |
|
|
|
import java.util.Date; |
|
|
|
import java.util.List; |
|
|
|
import java.util.Set; |
|
|
|
import java.util.Map; |
|
|
|
import java.util.concurrent.ConcurrentHashMap; |
|
|
|
import java.util.concurrent.ExecutorService; |
|
|
|
import java.util.concurrent.Executors; |
|
|
|
import java.util.concurrent.ThreadFactory; |
|
|
|
import java.util.concurrent.ScheduledThreadPoolExecutor; |
|
|
|
import java.util.concurrent.ScheduledFuture; |
|
|
|
import java.util.concurrent.TimeUnit; |
|
|
|
import java.util.concurrent.atomic.AtomicBoolean; |
|
|
|
import java.util.concurrent.atomic.AtomicInteger; |
|
|
|
import java.util.concurrent.atomic.AtomicLong; |
|
|
|
@ -54,7 +58,7 @@ public class PrintTaskScheduler { |
|
|
|
private static final Gson gson = new Gson(); |
|
|
|
|
|
|
|
/** |
|
|
|
* 打印调度总开关(未配置时默认开启) |
|
|
|
* 打印调度总开关(未配置时默认关闭) |
|
|
|
*/ |
|
|
|
@Value("${dashboard.print.enabled:false}") |
|
|
|
private boolean dashboardPushEnabled; |
|
|
|
@ -84,16 +88,20 @@ public class PrintTaskScheduler { |
|
|
|
private int printerConnectTimeoutMs; |
|
|
|
|
|
|
|
/** |
|
|
|
* 打印机读写超时(毫秒,未配置时默认10000) |
|
|
|
* 打印机读取超时(不限制写入,毫秒,未配置时默认10000) |
|
|
|
*/ |
|
|
|
@Value("${print.scheduler.printer-read-timeout-ms:10000}") |
|
|
|
private int printerReadTimeoutMs; |
|
|
|
|
|
|
|
/** Maximum age of a PROCESSING task before it is considered abandoned. */ |
|
|
|
/** 单次打印发送的总时限,包含全部份数及RFID等待时间。 */ |
|
|
|
@Value("${print.scheduler.printer-task-timeout-ms:60000}") |
|
|
|
private int printerTaskTimeoutMs = 60000; |
|
|
|
|
|
|
|
/** 执行中任务超过该时长后视为异常遗留任务。 */ |
|
|
|
@Value("${print.scheduler.stale-processing-timeout-minutes:30}") |
|
|
|
private int staleProcessingTimeoutMinutes; |
|
|
|
|
|
|
|
/** How often abandoned PROCESSING tasks are recovered. */ |
|
|
|
/** 异常遗留任务的恢复检查间隔。 */ |
|
|
|
@Value("${print.scheduler.recovery-interval-ms:60000}") |
|
|
|
private long recoveryIntervalMs; |
|
|
|
|
|
|
|
@ -101,6 +109,8 @@ public class PrintTaskScheduler { |
|
|
|
private static final AtomicLong LAST_IDLE_LOG_AT = new AtomicLong(0); |
|
|
|
private final Map<String, AtomicBoolean> siteProcessingFlags = new ConcurrentHashMap<>(); |
|
|
|
private ExecutorService siteTaskExecutor; |
|
|
|
private ScheduledThreadPoolExecutor printerTimeoutExecutor; |
|
|
|
private final Set<Socket> activePrinterSockets = ConcurrentHashMap.newKeySet(); |
|
|
|
|
|
|
|
@PostConstruct |
|
|
|
public void initSiteExecutor() { |
|
|
|
@ -122,6 +132,17 @@ public class PrintTaskScheduler { |
|
|
|
recoveryIntervalMs = 60000; |
|
|
|
} |
|
|
|
|
|
|
|
if (printerTaskTimeoutMs <= 0) { |
|
|
|
log.warn("打印任务超时配置无效,使用60000毫秒,原配置={}", printerTaskTimeoutMs); |
|
|
|
printerTaskTimeoutMs = 60000; |
|
|
|
} |
|
|
|
printerTimeoutExecutor = new ScheduledThreadPoolExecutor(1, runnable -> { |
|
|
|
Thread thread = new Thread(runnable, "print-socket-timeout"); |
|
|
|
thread.setDaemon(true); |
|
|
|
return thread; |
|
|
|
}); |
|
|
|
printerTimeoutExecutor.setRemoveOnCancelPolicy(true); |
|
|
|
|
|
|
|
int actualThreadPoolSize = Math.max(1, siteThreadPoolSize); |
|
|
|
if (actualThreadPoolSize != siteThreadPoolSize) { |
|
|
|
log.warn("打印站点线程池配置无效,使用默认最小值1,原配置={}", siteThreadPoolSize); |
|
|
|
@ -144,6 +165,10 @@ public class PrintTaskScheduler { |
|
|
|
|
|
|
|
@PreDestroy |
|
|
|
public void shutdownSiteExecutor() { |
|
|
|
if (printerTimeoutExecutor != null) { |
|
|
|
printerTimeoutExecutor.shutdownNow(); |
|
|
|
} |
|
|
|
activePrinterSockets.forEach(this::closePrinterSocket); |
|
|
|
if (siteTaskExecutor != null) { |
|
|
|
siteTaskExecutor.shutdown(); |
|
|
|
log.info("打印任务站点线程池已关闭"); |
|
|
|
@ -204,11 +229,11 @@ public class PrintTaskScheduler { |
|
|
|
} |
|
|
|
|
|
|
|
} catch (Exception e) { |
|
|
|
log.error("打印任务调度器执行异常: {}", e.getMessage(), e); |
|
|
|
log.error("打印任务调度器执行异常,阶段=查询待打印站点: {}", e.getMessage(), e); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// Recover tasks left in PROCESSING after a server crash or forced restart. |
|
|
|
// 遗留任务可能已经出纸,只转为失败待核对,不自动重打。 |
|
|
|
@Scheduled(fixedDelayString = "${print.scheduler.recovery-interval-ms:60000}", |
|
|
|
initialDelayString = "${print.scheduler.recovery-initial-delay-ms:5000}", |
|
|
|
scheduler = "printQueueTaskScheduler") |
|
|
|
@ -222,11 +247,11 @@ public class PrintTaskScheduler { |
|
|
|
Date before = new Date(System.currentTimeMillis() - timeoutMillis); |
|
|
|
int recovered = printTaskService.resetStaleProcessingTasks(before); |
|
|
|
if (recovered > 0) { |
|
|
|
log.warn("Recovered {} stale PROCESSING print tasks older than {} minutes", |
|
|
|
log.warn("已将 {} 个超过 {} 分钟的执行中打印任务转为失败,可能已出纸,重试前必须人工核对", |
|
|
|
recovered, staleProcessingTimeoutMinutes); |
|
|
|
} |
|
|
|
} catch (Exception e) { |
|
|
|
log.error("Failed to recover stale PROCESSING print tasks", e); |
|
|
|
log.error("打印队列遗留任务处理失败,阶段=超时任务转失败", e); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -234,6 +259,7 @@ public class PrintTaskScheduler { |
|
|
|
* 处理指定站点的打印任务(站内串行) |
|
|
|
*/ |
|
|
|
private void processSiteTasks(String site, AtomicBoolean siteProcessingFlag) { |
|
|
|
String stage = "查询站点待打印任务"; |
|
|
|
try { |
|
|
|
List<PrintTask> pendingTasks = printTaskService.getPendingTasksBySite(site, siteBatchSize); |
|
|
|
if (pendingTasks.isEmpty()) { |
|
|
|
@ -242,11 +268,12 @@ public class PrintTaskScheduler { |
|
|
|
|
|
|
|
log.info("【打印队列】site={} 发现 {} 个待执行任务", site, pendingTasks.size()); |
|
|
|
|
|
|
|
stage = "处理站点任务"; |
|
|
|
for (PrintTask task : pendingTasks) { |
|
|
|
processTask(task); |
|
|
|
} |
|
|
|
} catch (Exception e) { |
|
|
|
log.error("【打印队列】site任务执行异常 site={}, error={}", site, e.getMessage(), e); |
|
|
|
log.error("【打印队列】退出本轮站点任务 site={}, 阶段={}, error={}", site, stage, e.getMessage(), e); |
|
|
|
} finally { |
|
|
|
siteProcessingFlag.set(false); |
|
|
|
} |
|
|
|
@ -259,6 +286,9 @@ public class PrintTaskScheduler { |
|
|
|
Long taskId = task.getId(); |
|
|
|
String site = task.getSite(); |
|
|
|
String printerIp = task.getPrinterIp(); |
|
|
|
String stage = "领取任务"; |
|
|
|
boolean claimed = false; |
|
|
|
boolean updatingStatus = false; |
|
|
|
|
|
|
|
try { |
|
|
|
// 1. 标记为执行中(乐观锁,防止并发重复执行) |
|
|
|
@ -268,6 +298,8 @@ public class PrintTaskScheduler { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
claimed = true; |
|
|
|
stage = "解析标签数据"; |
|
|
|
log.info("【打印队列】开始执行任务 taskId={}, site={}, 打印机={}, 份数={}", |
|
|
|
taskId, site, printerIp, task.getCopies()); |
|
|
|
|
|
|
|
@ -278,6 +310,7 @@ public class PrintTaskScheduler { |
|
|
|
} |
|
|
|
|
|
|
|
// 3. 执行打印 |
|
|
|
stage = "发送打印数据"; |
|
|
|
boolean success = executePrint( |
|
|
|
printerIp, |
|
|
|
task.getZplCode(), |
|
|
|
@ -286,18 +319,36 @@ public class PrintTaskScheduler { |
|
|
|
labelData |
|
|
|
); |
|
|
|
|
|
|
|
// 4. 更新任务状态 |
|
|
|
// 状态回写异常时结果可能未知,不能再写失败覆盖已提交的成功结果。 |
|
|
|
updatingStatus = true; |
|
|
|
stage = success ? "发送完成后回写成功" : "发送失败后回写失败"; |
|
|
|
if (success) { |
|
|
|
printTaskService.markAsSuccess(taskId); |
|
|
|
if (!printTaskService.markAsSuccess(taskId)) { |
|
|
|
throw new IllegalStateException("成功状态未更新,可能已出纸,需人工核对"); |
|
|
|
} |
|
|
|
log.info("【打印队列】✓ 任务完成 taskId={}, site={}, 打印机={}", taskId, site, printerIp); |
|
|
|
} else { |
|
|
|
printTaskService.markAsFailed(taskId, "打印失败,未返回成功标识"); |
|
|
|
if (!printTaskService.markAsFailed(taskId, "打印发送失败或超时,可能已部分出纸,重试前请核对标签及服务日志")) { |
|
|
|
throw new IllegalStateException("失败状态未更新,需人工核对"); |
|
|
|
} |
|
|
|
log.error("【打印队列】✗ 任务失败 taskId={}, site={}, 打印机={}", taskId, site, printerIp); |
|
|
|
} |
|
|
|
|
|
|
|
} catch (Exception e) { |
|
|
|
log.error("【打印队列】✗ 任务执行异常 taskId={}, site={}, 错误: {}", taskId, site, e.getMessage(), e); |
|
|
|
printTaskService.markAsFailed(taskId, "执行异常: " + e.getMessage()); |
|
|
|
log.error("【打印队列】任务异常 taskId={}, site={}, 阶段={}, 错误={}", |
|
|
|
taskId, site, stage, e.getMessage(), e); |
|
|
|
// 领取结果不确定时不打印、不改失败;回写结果不确定时停止本批次,由遗留任务检查兜底。 |
|
|
|
if (!claimed || updatingStatus) { |
|
|
|
throw new IllegalStateException("数据库操作未确认,停止当前站点批次,taskId=" + taskId, e); |
|
|
|
} |
|
|
|
try { |
|
|
|
if (!printTaskService.markAsFailed(taskId, "执行异常,重试前请核对: " + e.getMessage())) { |
|
|
|
throw new IllegalStateException("异常任务的失败状态未更新"); |
|
|
|
} |
|
|
|
} catch (Exception updateEx) { |
|
|
|
log.error("【打印队列】异常后回写失败 taskId={}, site={}, 阶段=异常回写失败", taskId, site, updateEx); |
|
|
|
throw new IllegalStateException("失败状态回写未确认,停止当前站点批次,taskId=" + taskId, updateEx); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -314,45 +365,60 @@ public class PrintTaskScheduler { |
|
|
|
*/ |
|
|
|
private boolean executePrint(String printerIp, String zplCode, Integer copies, |
|
|
|
String rfidFlag, Map<String, Object> labelData) { |
|
|
|
Socket socket = null; |
|
|
|
Socket socket = new Socket(); |
|
|
|
ScheduledFuture<?> timeoutFuture = null; |
|
|
|
AtomicBoolean timedOut = new AtomicBoolean(false); |
|
|
|
try { |
|
|
|
// 1. 连接打印机 |
|
|
|
socket = new Socket(); |
|
|
|
activePrinterSockets.add(socket); |
|
|
|
// 保持原站点线程串行发送,只用独立计时器关连接,不另开线程接管任务。 |
|
|
|
// 先注册超时再连接,避免写入阻塞;取消成功任务的计时器,防止队列累积。 |
|
|
|
timeoutFuture = printerTimeoutExecutor.schedule(() -> { |
|
|
|
timedOut.set(true); |
|
|
|
closePrinterSocket(socket); |
|
|
|
log.error("打印发送超过{}毫秒,已关闭连接,可能部分出纸,重试前请核对: printerIp={}", |
|
|
|
printerTaskTimeoutMs, printerIp); |
|
|
|
}, printerTaskTimeoutMs, TimeUnit.MILLISECONDS); |
|
|
|
if (zplCode == null || zplCode.isBlank()) { |
|
|
|
throw new IllegalArgumentException("打印内容为空"); |
|
|
|
} |
|
|
|
socket.connect(new InetSocketAddress(printerIp, 9100), printerConnectTimeoutMs); |
|
|
|
socket.setSoTimeout(printerReadTimeoutMs); |
|
|
|
|
|
|
|
OutputStream os = socket.getOutputStream(); |
|
|
|
|
|
|
|
int actualCopies = copies != null && copies > 0 ? copies : 1; |
|
|
|
|
|
|
|
// 2. 直接发送ZPL代码(已在入队时组装好,包括RFID指令) |
|
|
|
log.debug("开始发送ZPL到打印机 {}, 份数: {}, RFID: {}", printerIp, actualCopies, rfidFlag); |
|
|
|
|
|
|
|
byte[] payload = zplCode.getBytes(StandardCharsets.UTF_8); |
|
|
|
for (int i = 0; i < actualCopies; i++) { |
|
|
|
os.write(zplCode.getBytes(StandardCharsets.UTF_8)); |
|
|
|
if (timedOut.get() || socket.isClosed() || Thread.currentThread().isInterrupted()) { |
|
|
|
return false; |
|
|
|
} |
|
|
|
os.write(payload); |
|
|
|
os.flush(); |
|
|
|
|
|
|
|
// RFID标签需要等待处理时间 |
|
|
|
if ("Y".equals(rfidFlag)) { |
|
|
|
Thread.sleep(1000); // RFID标签等待1秒 |
|
|
|
log.debug("RFID标签打印完成 {} / {}", i + 1, actualCopies); |
|
|
|
Thread.sleep(1000); // 保留原有RFID间隔,超时后最多等待本次间隔结束。 |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
log.debug("✓ ZPL已发送到打印机 {}, 份数: {}", printerIp, actualCopies); |
|
|
|
if (timedOut.get() || socket.isClosed()) return false; |
|
|
|
log.debug("ZPL已发送到打印机 {}, 份数: {}(不代表实际出纸确认)", printerIp, actualCopies); |
|
|
|
return true; |
|
|
|
|
|
|
|
} catch (InterruptedException e) { |
|
|
|
Thread.currentThread().interrupt(); |
|
|
|
log.warn("打印发送被中断: printerIp={}", printerIp); |
|
|
|
return false; |
|
|
|
} catch (Exception e) { |
|
|
|
log.error("发送ZPL到打印机失败: printerIp={}, error={}", printerIp, e.getMessage(), e); |
|
|
|
log.error("发送ZPL失败: printerIp={}, timedOut={}, error={}", printerIp, timedOut.get(), e.getMessage(), e); |
|
|
|
return false; |
|
|
|
} finally { |
|
|
|
if (socket != null) { |
|
|
|
try { |
|
|
|
socket.close(); |
|
|
|
} catch (Exception e) { |
|
|
|
log.warn("关闭socket失败: {}", e.getMessage()); |
|
|
|
} |
|
|
|
} |
|
|
|
if (timeoutFuture != null) timeoutFuture.cancel(false); |
|
|
|
closePrinterSocket(socket); |
|
|
|
activePrinterSockets.remove(socket); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** 关闭连接会解除阻塞的网络写入,让原工作线程正常退出并释放站点占用。 */ |
|
|
|
private void closePrinterSocket(Socket socket) { |
|
|
|
try { |
|
|
|
socket.close(); |
|
|
|
} catch (Exception e) { |
|
|
|
log.warn("关闭打印机连接失败: {}", e.getMessage()); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|