You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

458 lines
19 KiB

​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
3 months ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
3 months ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
1 year ago
​
1 year ago
​
​
3 months ago
​
​
1 year ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
  1. package com.gaotao.modules.base.task;
  2. import com.gaotao.modules.base.entity.PrintTask;
  3. import com.gaotao.modules.base.service.PrintTaskService;
  4. import com.google.gson.Gson;
  5. import jakarta.annotation.PostConstruct;
  6. import jakarta.annotation.PreDestroy;
  7. import lombok.extern.slf4j.Slf4j;
  8. import org.springframework.beans.factory.annotation.Autowired;
  9. import org.springframework.beans.factory.annotation.Value;
  10. import org.springframework.scheduling.annotation.Scheduled;
  11. import org.springframework.stereotype.Component;
  12. import java.io.OutputStream;
  13. import java.net.InetSocketAddress;
  14. import java.net.Socket;
  15. import java.util.Date;
  16. import java.util.List;
  17. import java.util.Set;
  18. import java.util.Map;
  19. import java.util.concurrent.ConcurrentHashMap;
  20. import java.util.concurrent.ExecutorService;
  21. import java.util.concurrent.Executors;
  22. import java.util.concurrent.ThreadFactory;
  23. import java.util.concurrent.ScheduledThreadPoolExecutor;
  24. import java.util.concurrent.ScheduledFuture;
  25. import java.util.concurrent.TimeUnit;
  26. import java.util.concurrent.atomic.AtomicBoolean;
  27. import java.util.concurrent.atomic.AtomicInteger;
  28. import java.util.concurrent.atomic.AtomicLong;
  29. import java.nio.charset.StandardCharsets;
  30. /**
  31. * 打印任务定时调度器
  32. *
  33. * <p><b>功能说明:</b></p>
  34. * <ul>
  35. * <li>每2秒扫描一次打印任务队列</li>
  36. * <li>按站点(site)拆分任务,不同站点并发处理</li>
  37. * <li>同一站点内按FIFO顺序(created_date)取出PENDING状态的任务</li>
  38. * <li>执行打印任务并更新状态</li>
  39. * <li>失败任务记录错误信息</li>
  40. * </ul>
  41. *
  42. * <p><b>并发控制:</b></p>
  43. * <ul>
  44. * <li>使用数据库乐观锁(markAsProcessing时检查状态)</li>
  45. * <li>确保同一任务不会被重复执行</li>
  46. * </ul>
  47. */
  48. @Slf4j
  49. @Component
  50. public class PrintTaskScheduler {
  51. @Autowired
  52. private PrintTaskService printTaskService;
  53. private static final Gson gson = new Gson();
  54. /**
  55. * 打印调度总开关(未配置时默认关闭)
  56. */
  57. @Value("${dashboard.print.enabled:false}")
  58. private boolean dashboardPushEnabled;
  59. /**
  60. * 每轮调度最多扫描的站点数量(未配置时默认20)
  61. */
  62. @Value("${print.scheduler.site.max-sites-per-round:20}")
  63. private int maxSitesPerRound;
  64. /**
  65. * 每个站点每轮最多处理的任务数量(未配置时默认10)
  66. */
  67. @Value("${print.scheduler.site.batch-size:10}")
  68. private int siteBatchSize;
  69. /**
  70. * 站点并发线程池大小(未配置时默认8)
  71. */
  72. @Value("${print.scheduler.site.thread-pool-size:8}")
  73. private int siteThreadPoolSize;
  74. /**
  75. * 打印机连接超时(毫秒,未配置时默认3000)
  76. */
  77. @Value("${print.scheduler.printer-connect-timeout-ms:3000}")
  78. private int printerConnectTimeoutMs;
  79. /**
  80. * 打印机读取超时(不限制写入,毫秒,未配置时默认10000)
  81. */
  82. @Value("${print.scheduler.printer-read-timeout-ms:10000}")
  83. private int printerReadTimeoutMs;
  84. /** 单次打印发送的总时限,包含全部份数及RFID等待时间。 */
  85. @Value("${print.scheduler.printer-task-timeout-ms:60000}")
  86. private int printerTaskTimeoutMs = 60000;
  87. /** 执行中任务超过该时长后视为异常遗留任务。 */
  88. @Value("${print.scheduler.stale-processing-timeout-minutes:30}")
  89. private int staleProcessingTimeoutMinutes;
  90. /** 异常遗留任务的恢复检查间隔。 */
  91. @Value("${print.scheduler.recovery-interval-ms:60000}")
  92. private long recoveryIntervalMs;
  93. private static final AtomicInteger THREAD_INDEX = new AtomicInteger(1);
  94. private static final AtomicLong LAST_IDLE_LOG_AT = new AtomicLong(0);
  95. private final Map<String, AtomicBoolean> siteProcessingFlags = new ConcurrentHashMap<>();
  96. private ExecutorService siteTaskExecutor;
  97. private ScheduledThreadPoolExecutor printerTimeoutExecutor;
  98. private final Set<Socket> activePrinterSockets = ConcurrentHashMap.newKeySet();
  99. @PostConstruct
  100. public void initSiteExecutor() {
  101. if (maxSitesPerRound <= 0) {
  102. log.warn("Invalid print.scheduler.site.max-sites-per-round={}, using 20", maxSitesPerRound);
  103. maxSitesPerRound = 20;
  104. }
  105. if (siteBatchSize <= 0) {
  106. log.warn("Invalid print.scheduler.site.batch-size={}, using 10", siteBatchSize);
  107. siteBatchSize = 10;
  108. }
  109. if (staleProcessingTimeoutMinutes <= 0) {
  110. log.warn("Invalid print.scheduler.stale-processing-timeout-minutes={}, using 30",
  111. staleProcessingTimeoutMinutes);
  112. staleProcessingTimeoutMinutes = 30;
  113. }
  114. if (recoveryIntervalMs < 1000) {
  115. log.warn("Invalid print.scheduler.recovery-interval-ms={}, using 60000", recoveryIntervalMs);
  116. recoveryIntervalMs = 60000;
  117. }
  118. if (printerTaskTimeoutMs <= 0) {
  119. log.warn("打印任务超时配置无效,使用60000毫秒,原配置={}", printerTaskTimeoutMs);
  120. printerTaskTimeoutMs = 60000;
  121. }
  122. printerTimeoutExecutor = new ScheduledThreadPoolExecutor(1, runnable -> {
  123. Thread thread = new Thread(runnable, "print-socket-timeout");
  124. thread.setDaemon(true);
  125. return thread;
  126. });
  127. printerTimeoutExecutor.setRemoveOnCancelPolicy(true);
  128. int actualThreadPoolSize = Math.max(1, siteThreadPoolSize);
  129. if (actualThreadPoolSize != siteThreadPoolSize) {
  130. log.warn("打印站点线程池配置无效,使用默认最小值1,原配置={}", siteThreadPoolSize);
  131. }
  132. ThreadFactory threadFactory = runnable -> {
  133. Thread thread = new Thread(runnable);
  134. thread.setName("print-site-worker-" + THREAD_INDEX.getAndIncrement());
  135. return thread;
  136. };
  137. siteTaskExecutor = Executors.newFixedThreadPool(actualThreadPoolSize, threadFactory);
  138. if (dashboardPushEnabled) {
  139. log.info("Print task scheduler config: enabled={}, maxSites={}, batchSize={}, siteWorkers={}, " +
  140. "staleTimeoutMinutes={}, recoveryIntervalMs={}",
  141. dashboardPushEnabled, maxSitesPerRound, siteBatchSize, actualThreadPoolSize,
  142. staleProcessingTimeoutMinutes, recoveryIntervalMs);
  143. }
  144. log.info("打印任务站点线程池初始化完成,线程数={}", actualThreadPoolSize);
  145. }
  146. @PreDestroy
  147. public void shutdownSiteExecutor() {
  148. if (printerTimeoutExecutor != null) {
  149. printerTimeoutExecutor.shutdownNow();
  150. }
  151. activePrinterSockets.forEach(this::closePrinterSocket);
  152. if (siteTaskExecutor != null) {
  153. siteTaskExecutor.shutdown();
  154. log.info("打印任务站点线程池已关闭");
  155. }
  156. }
  157. // 每2秒执行一次
  158. @Scheduled(fixedDelayString = "${print.scheduler.poll-interval-ms:2000}",
  159. scheduler = "printQueueTaskScheduler")
  160. public void processPrintTasks() {
  161. // 检查定时任务开关
  162. if (!dashboardPushEnabled) {
  163. return;
  164. } else {
  165. log.debug("Print task scheduler tick: enabled={}, thread={}",
  166. dashboardPushEnabled, Thread.currentThread().getName());
  167. }
  168. if (siteTaskExecutor == null || siteTaskExecutor.isShutdown()) {
  169. log.warn("打印任务站点线程池不可用,跳过本轮调度");
  170. return;
  171. }
  172. try {
  173. // 1. 获取有待执行任务的站点列表
  174. List<String> pendingSites = printTaskService.getPendingSites(maxSitesPerRound);
  175. if (pendingSites.isEmpty()) {
  176. long now = System.currentTimeMillis();
  177. long lastLog = LAST_IDLE_LOG_AT.get();
  178. if (now - lastLog >= 30000 && LAST_IDLE_LOG_AT.compareAndSet(lastLog, now)) {
  179. log.info("Print task scheduler is running; no PENDING sites found");
  180. }
  181. return; // 没有待处理任务
  182. }
  183. log.info("【打印队列】发现 {} 个待执行站点", pendingSites.size());
  184. // 2. 按站点并发处理,不同站点互不影响
  185. for (String site : pendingSites) {
  186. if (site == null || site.trim().isEmpty()) {
  187. continue;
  188. }
  189. AtomicBoolean siteProcessingFlag = siteProcessingFlags
  190. .computeIfAbsent(site, key -> new AtomicBoolean(false));
  191. // 同一个site只允许一个worker在执行,避免站内并发
  192. if (!siteProcessingFlag.compareAndSet(false, true)) {
  193. log.debug("【打印队列】site={} 正在执行中,跳过本轮", site);
  194. continue;
  195. }
  196. try {
  197. siteTaskExecutor.submit(() -> processSiteTasks(site, siteProcessingFlag));
  198. } catch (Exception submitEx) {
  199. siteProcessingFlag.set(false);
  200. log.error("【打印队列】提交site任务失败 site={}, error={}",
  201. site, submitEx.getMessage(), submitEx);
  202. }
  203. }
  204. } catch (Exception e) {
  205. log.error("打印任务调度器执行异常,阶段=查询待打印站点: {}", e.getMessage(), e);
  206. }
  207. }
  208. // 遗留任务可能已经出纸,只转为失败待核对,不自动重打。
  209. @Scheduled(fixedDelayString = "${print.scheduler.recovery-interval-ms:60000}",
  210. initialDelayString = "${print.scheduler.recovery-initial-delay-ms:5000}",
  211. scheduler = "printQueueTaskScheduler")
  212. public void recoverStaleProcessingTasks() {
  213. if (!dashboardPushEnabled) {
  214. return;
  215. }
  216. try {
  217. long timeoutMillis = staleProcessingTimeoutMinutes * 60_000L;
  218. Date before = new Date(System.currentTimeMillis() - timeoutMillis);
  219. int recovered = printTaskService.resetStaleProcessingTasks(before);
  220. if (recovered > 0) {
  221. log.warn("已将 {} 个超过 {} 分钟的执行中打印任务转为失败,可能已出纸,重试前必须人工核对",
  222. recovered, staleProcessingTimeoutMinutes);
  223. }
  224. } catch (Exception e) {
  225. log.error("打印队列遗留任务处理失败,阶段=超时任务转失败", e);
  226. }
  227. }
  228. /**
  229. * 处理指定站点的打印任务(站内串行)
  230. */
  231. private void processSiteTasks(String site, AtomicBoolean siteProcessingFlag) {
  232. String stage = "查询站点待打印任务";
  233. try {
  234. List<PrintTask> pendingTasks = printTaskService.getPendingTasksBySite(site, siteBatchSize);
  235. if (pendingTasks.isEmpty()) {
  236. return;
  237. }
  238. log.info("【打印队列】site={} 发现 {} 个待执行任务", site, pendingTasks.size());
  239. stage = "处理站点任务";
  240. for (PrintTask task : pendingTasks) {
  241. processTask(task);
  242. }
  243. } catch (Exception e) {
  244. log.error("【打印队列】退出本轮站点任务 site={}, 阶段={}, error={}", site, stage, e.getMessage(), e);
  245. } finally {
  246. siteProcessingFlag.set(false);
  247. }
  248. }
  249. /**
  250. * 处理单个打印任务
  251. */
  252. private void processTask(PrintTask task) {
  253. Long taskId = task.getId();
  254. String site = task.getSite();
  255. String printerIp = task.getPrinterIp();
  256. String stage = "领取任务";
  257. boolean claimed = false;
  258. boolean updatingStatus = false;
  259. try {
  260. // 1. 标记为执行中(乐观锁,防止并发重复执行)
  261. boolean marked = printTaskService.markAsProcessing(taskId);
  262. if (!marked) {
  263. log.debug("任务 {} 已被其他线程处理,跳过", taskId);
  264. return;
  265. }
  266. claimed = true;
  267. stage = "解析标签数据";
  268. log.info("【打印队列】开始执行任务 taskId={}, site={}, 打印机={}, 份数={}",
  269. taskId, site, printerIp, task.getCopies());
  270. // 2. 解析标签数据
  271. Map<String, Object> labelData = null;
  272. if (task.getLabelData() != null && !task.getLabelData().trim().isEmpty()) {
  273. labelData = gson.fromJson(task.getLabelData(), Map.class);
  274. }
  275. // 3. 执行打印
  276. stage = "发送打印数据";
  277. boolean success = executePrint(
  278. printerIp,
  279. task.getZplCode(),
  280. task.getCopies(),
  281. task.getRfidFlag(),
  282. labelData
  283. );
  284. // 状态回写异常时结果可能未知,不能再写失败覆盖已提交的成功结果。
  285. updatingStatus = true;
  286. stage = success ? "发送完成后回写成功" : "发送失败后回写失败";
  287. if (success) {
  288. if (!printTaskService.markAsSuccess(taskId)) {
  289. throw new IllegalStateException("成功状态未更新,可能已出纸,需人工核对");
  290. }
  291. log.info("【打印队列】✓ 任务完成 taskId={}, site={}, 打印机={}", taskId, site, printerIp);
  292. } else {
  293. if (!printTaskService.markAsFailed(taskId, "打印发送失败或超时,可能已部分出纸,重试前请核对标签及服务日志")) {
  294. throw new IllegalStateException("失败状态未更新,需人工核对");
  295. }
  296. log.error("【打印队列】✗ 任务失败 taskId={}, site={}, 打印机={}", taskId, site, printerIp);
  297. }
  298. } catch (Exception e) {
  299. log.error("【打印队列】任务异常 taskId={}, site={}, 阶段={}, 错误={}",
  300. taskId, site, stage, e.getMessage(), e);
  301. // 领取结果不确定时不打印、不改失败;回写结果不确定时停止本批次,由遗留任务检查兜底。
  302. if (!claimed || updatingStatus) {
  303. throw new IllegalStateException("数据库操作未确认,停止当前站点批次,taskId=" + taskId, e);
  304. }
  305. try {
  306. if (!printTaskService.markAsFailed(taskId, "执行异常,重试前请核对: " + e.getMessage())) {
  307. throw new IllegalStateException("异常任务的失败状态未更新");
  308. }
  309. } catch (Exception updateEx) {
  310. log.error("【打印队列】异常后回写失败 taskId={}, site={}, 阶段=异常回写失败", taskId, site, updateEx);
  311. throw new IllegalStateException("失败状态回写未确认,停止当前站点批次,taskId=" + taskId, updateEx);
  312. }
  313. }
  314. }
  315. /**
  316. * 执行打印(发送ZPL到打印机)
  317. * 注意:ZPL代码已在入队时组装好(包括RFID指令),这里只需要直接发送
  318. *
  319. * @param printerIp 打印机IP
  320. * @param zplCode ZPL代码(已包含RFID指令)
  321. * @param copies 打印份数
  322. * @param rfidFlag RFID标识(用于日志)
  323. * @param labelData 标签数据(未使用,保留以备后续扩展)
  324. * @return 是否成功
  325. */
  326. private boolean executePrint(String printerIp, String zplCode, Integer copies,
  327. String rfidFlag, Map<String, Object> labelData) {
  328. Socket socket = new Socket();
  329. ScheduledFuture<?> timeoutFuture = null;
  330. AtomicBoolean timedOut = new AtomicBoolean(false);
  331. try {
  332. activePrinterSockets.add(socket);
  333. // 保持原站点线程串行发送,只用独立计时器关连接,不另开线程接管任务。
  334. // 先注册超时再连接,避免写入阻塞;取消成功任务的计时器,防止队列累积。
  335. timeoutFuture = printerTimeoutExecutor.schedule(() -> {
  336. timedOut.set(true);
  337. closePrinterSocket(socket);
  338. log.error("打印发送超过{}毫秒,已关闭连接,可能部分出纸,重试前请核对: printerIp={}",
  339. printerTaskTimeoutMs, printerIp);
  340. }, printerTaskTimeoutMs, TimeUnit.MILLISECONDS);
  341. if (zplCode == null || zplCode.isBlank()) {
  342. throw new IllegalArgumentException("打印内容为空");
  343. }
  344. socket.connect(new InetSocketAddress(printerIp, 9100), printerConnectTimeoutMs);
  345. socket.setSoTimeout(printerReadTimeoutMs);
  346. OutputStream os = socket.getOutputStream();
  347. int actualCopies = copies != null && copies > 0 ? copies : 1;
  348. byte[] payload = zplCode.getBytes(StandardCharsets.UTF_8);
  349. for (int i = 0; i < actualCopies; i++) {
  350. if (timedOut.get() || socket.isClosed() || Thread.currentThread().isInterrupted()) {
  351. return false;
  352. }
  353. os.write(payload);
  354. os.flush();
  355. if ("Y".equals(rfidFlag)) {
  356. Thread.sleep(1000); // 保留原有RFID间隔,超时后最多等待本次间隔结束。
  357. }
  358. }
  359. if (timedOut.get() || socket.isClosed()) return false;
  360. log.debug("ZPL已发送到打印机 {}, 份数: {}(不代表实际出纸确认)", printerIp, actualCopies);
  361. return true;
  362. } catch (InterruptedException e) {
  363. Thread.currentThread().interrupt();
  364. log.warn("打印发送被中断: printerIp={}", printerIp);
  365. return false;
  366. } catch (Exception e) {
  367. log.error("发送ZPL失败: printerIp={}, timedOut={}, error={}", printerIp, timedOut.get(), e.getMessage(), e);
  368. return false;
  369. } finally {
  370. if (timeoutFuture != null) timeoutFuture.cancel(false);
  371. closePrinterSocket(socket);
  372. activePrinterSockets.remove(socket);
  373. }
  374. }
  375. /** 关闭连接会解除阻塞的网络写入,让原工作线程正常退出并释放站点占用。 */
  376. private void closePrinterSocket(Socket socket) {
  377. try {
  378. socket.close();
  379. } catch (Exception e) {
  380. log.warn("关闭打印机连接失败: {}", e.getMessage());
  381. }
  382. }
  383. /**
  384. * 归档打印任务历史数据
  385. *
  386. * <p><b>功能说明:</b></p>
  387. * <ul>
  388. * <li>每天凌晨5点自动执行</li>
  389. * <li>将已完成的打印任务备份到 print_task_history 表</li>
  390. * <li>清理10天之前的历史数据</li>
  391. * </ul>
  392. *
  393. * <p><b>执行时间:</b>每天凌晨5点(固定)</p>
  394. * <p><b>归档规则:</b>归档10天前的已完成任务(固定)</p>
  395. */
  396. @Scheduled(cron = "0 0 5 * * ?", scheduler = "printQueueTaskScheduler")
  397. public void archivePrintTaskHistory() {
  398. // 检查定时任务开关
  399. if (!dashboardPushEnabled) {
  400. return;
  401. }
  402. log.info("=== 开始归档打印任务历史数据 ===");
  403. try {
  404. // 调用Service层执行归档操作
  405. int archivedCount = printTaskService.archivePrintTaskHistory();
  406. log.info("=== 打印任务历史数据归档完成:归档任务数量={} ===", archivedCount);
  407. } catch (Exception e) {
  408. log.error("=== 打印任务历史数据归档失败 ===", e);
  409. }
  410. }
  411. }