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.

392 lines
15 KiB

1 month ago
1 month ago
11 months ago
10 months ago
1 month ago
10 months 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.Map;
  18. import java.util.concurrent.ConcurrentHashMap;
  19. import java.util.concurrent.ExecutorService;
  20. import java.util.concurrent.Executors;
  21. import java.util.concurrent.ThreadFactory;
  22. import java.util.concurrent.atomic.AtomicBoolean;
  23. import java.util.concurrent.atomic.AtomicInteger;
  24. import java.util.concurrent.atomic.AtomicLong;
  25. import java.nio.charset.StandardCharsets;
  26. /**
  27. * 打印任务定时调度器
  28. *
  29. * <p><b>功能说明</b></p>
  30. * <ul>
  31. * <li>每2秒扫描一次打印任务队列</li>
  32. * <li>按站点site拆分任务不同站点并发处理</li>
  33. * <li>同一站点内按FIFO顺序created_date取出PENDING状态的任务</li>
  34. * <li>执行打印任务并更新状态</li>
  35. * <li>失败任务记录错误信息</li>
  36. * </ul>
  37. *
  38. * <p><b>并发控制</b></p>
  39. * <ul>
  40. * <li>使用数据库乐观锁markAsProcessing时检查状态</li>
  41. * <li>确保同一任务不会被重复执行</li>
  42. * </ul>
  43. */
  44. @Slf4j
  45. @Component
  46. public class PrintTaskScheduler {
  47. @Autowired
  48. private PrintTaskService printTaskService;
  49. private static final Gson gson = new Gson();
  50. /**
  51. * 打印调度总开关未配置时默认开启
  52. */
  53. @Value("${dashboard.print.enabled:false}")
  54. private boolean dashboardPushEnabled;
  55. /**
  56. * 每轮调度最多扫描的站点数量未配置时默认20
  57. */
  58. @Value("${print.scheduler.site.max-sites-per-round:20}")
  59. private int maxSitesPerRound;
  60. /**
  61. * 每个站点每轮最多处理的任务数量未配置时默认10
  62. */
  63. @Value("${print.scheduler.site.batch-size:10}")
  64. private int siteBatchSize;
  65. /**
  66. * 站点并发线程池大小未配置时默认8
  67. */
  68. @Value("${print.scheduler.site.thread-pool-size:8}")
  69. private int siteThreadPoolSize;
  70. /**
  71. * 打印机连接超时毫秒未配置时默认3000
  72. */
  73. @Value("${print.scheduler.printer-connect-timeout-ms:3000}")
  74. private int printerConnectTimeoutMs;
  75. /**
  76. * 打印机读写超时毫秒未配置时默认10000
  77. */
  78. @Value("${print.scheduler.printer-read-timeout-ms:10000}")
  79. private int printerReadTimeoutMs;
  80. /** Maximum age of a PROCESSING task before it is considered abandoned. */
  81. @Value("${print.scheduler.stale-processing-timeout-minutes:30}")
  82. private int staleProcessingTimeoutMinutes;
  83. /** How often abandoned PROCESSING tasks are recovered. */
  84. @Value("${print.scheduler.recovery-interval-ms:60000}")
  85. private long recoveryIntervalMs;
  86. private static final AtomicInteger THREAD_INDEX = new AtomicInteger(1);
  87. private static final AtomicLong LAST_IDLE_LOG_AT = new AtomicLong(0);
  88. private final Map<String, AtomicBoolean> siteProcessingFlags = new ConcurrentHashMap<>();
  89. private ExecutorService siteTaskExecutor;
  90. @PostConstruct
  91. public void initSiteExecutor() {
  92. if (maxSitesPerRound <= 0) {
  93. log.warn("Invalid print.scheduler.site.max-sites-per-round={}, using 20", maxSitesPerRound);
  94. maxSitesPerRound = 20;
  95. }
  96. if (siteBatchSize <= 0) {
  97. log.warn("Invalid print.scheduler.site.batch-size={}, using 10", siteBatchSize);
  98. siteBatchSize = 10;
  99. }
  100. if (staleProcessingTimeoutMinutes <= 0) {
  101. log.warn("Invalid print.scheduler.stale-processing-timeout-minutes={}, using 30",
  102. staleProcessingTimeoutMinutes);
  103. staleProcessingTimeoutMinutes = 30;
  104. }
  105. if (recoveryIntervalMs < 1000) {
  106. log.warn("Invalid print.scheduler.recovery-interval-ms={}, using 60000", recoveryIntervalMs);
  107. recoveryIntervalMs = 60000;
  108. }
  109. int actualThreadPoolSize = Math.max(1, siteThreadPoolSize);
  110. if (actualThreadPoolSize != siteThreadPoolSize) {
  111. log.warn("打印站点线程池配置无效,使用默认最小值1,原配置={}", siteThreadPoolSize);
  112. }
  113. ThreadFactory threadFactory = runnable -> {
  114. Thread thread = new Thread(runnable);
  115. thread.setName("print-site-worker-" + THREAD_INDEX.getAndIncrement());
  116. return thread;
  117. };
  118. siteTaskExecutor = Executors.newFixedThreadPool(actualThreadPoolSize, threadFactory);
  119. if (dashboardPushEnabled) {
  120. log.info("Print task scheduler config: enabled={}, maxSites={}, batchSize={}, siteWorkers={}, " +
  121. "staleTimeoutMinutes={}, recoveryIntervalMs={}",
  122. dashboardPushEnabled, maxSitesPerRound, siteBatchSize, actualThreadPoolSize,
  123. staleProcessingTimeoutMinutes, recoveryIntervalMs);
  124. }
  125. log.info("打印任务站点线程池初始化完成,线程数={}", actualThreadPoolSize);
  126. }
  127. @PreDestroy
  128. public void shutdownSiteExecutor() {
  129. if (siteTaskExecutor != null) {
  130. siteTaskExecutor.shutdown();
  131. log.info("打印任务站点线程池已关闭");
  132. }
  133. }
  134. // 每2秒执行一次
  135. @Scheduled(fixedDelayString = "${print.scheduler.poll-interval-ms:2000}",
  136. scheduler = "printQueueTaskScheduler")
  137. public void processPrintTasks() {
  138. // 检查定时任务开关
  139. if (!dashboardPushEnabled) {
  140. return;
  141. } else {
  142. log.debug("Print task scheduler tick: enabled={}, thread={}",
  143. dashboardPushEnabled, Thread.currentThread().getName());
  144. }
  145. if (siteTaskExecutor == null || siteTaskExecutor.isShutdown()) {
  146. log.warn("打印任务站点线程池不可用,跳过本轮调度");
  147. return;
  148. }
  149. try {
  150. // 1. 获取有待执行任务的站点列表
  151. List<String> pendingSites = printTaskService.getPendingSites(maxSitesPerRound);
  152. if (pendingSites.isEmpty()) {
  153. long now = System.currentTimeMillis();
  154. long lastLog = LAST_IDLE_LOG_AT.get();
  155. if (now - lastLog >= 30000 && LAST_IDLE_LOG_AT.compareAndSet(lastLog, now)) {
  156. log.info("Print task scheduler is running; no PENDING sites found");
  157. }
  158. return; // 没有待处理任务
  159. }
  160. log.info("【打印队列】发现 {} 个待执行站点", pendingSites.size());
  161. // 2. 按站点并发处理,不同站点互不影响
  162. for (String site : pendingSites) {
  163. if (site == null || site.trim().isEmpty()) {
  164. continue;
  165. }
  166. AtomicBoolean siteProcessingFlag = siteProcessingFlags
  167. .computeIfAbsent(site, key -> new AtomicBoolean(false));
  168. // 同一个site只允许一个worker在执行,避免站内并发
  169. if (!siteProcessingFlag.compareAndSet(false, true)) {
  170. log.debug("【打印队列】site={} 正在执行中,跳过本轮", site);
  171. continue;
  172. }
  173. try {
  174. siteTaskExecutor.submit(() -> processSiteTasks(site, siteProcessingFlag));
  175. } catch (Exception submitEx) {
  176. siteProcessingFlag.set(false);
  177. log.error("【打印队列】提交site任务失败 site={}, error={}",
  178. site, submitEx.getMessage(), submitEx);
  179. }
  180. }
  181. } catch (Exception e) {
  182. log.error("打印任务调度器执行异常: {}", e.getMessage(), e);
  183. }
  184. }
  185. // Recover tasks left in PROCESSING after a server crash or forced restart.
  186. @Scheduled(fixedDelayString = "${print.scheduler.recovery-interval-ms:60000}",
  187. initialDelayString = "${print.scheduler.recovery-initial-delay-ms:5000}",
  188. scheduler = "printQueueTaskScheduler")
  189. public void recoverStaleProcessingTasks() {
  190. if (!dashboardPushEnabled) {
  191. return;
  192. }
  193. try {
  194. long timeoutMillis = staleProcessingTimeoutMinutes * 60_000L;
  195. Date before = new Date(System.currentTimeMillis() - timeoutMillis);
  196. int recovered = printTaskService.resetStaleProcessingTasks(before);
  197. if (recovered > 0) {
  198. log.warn("Recovered {} stale PROCESSING print tasks older than {} minutes",
  199. recovered, staleProcessingTimeoutMinutes);
  200. }
  201. } catch (Exception e) {
  202. log.error("Failed to recover stale PROCESSING print tasks", e);
  203. }
  204. }
  205. /**
  206. * 处理指定站点的打印任务站内串行
  207. */
  208. private void processSiteTasks(String site, AtomicBoolean siteProcessingFlag) {
  209. try {
  210. List<PrintTask> pendingTasks = printTaskService.getPendingTasksBySite(site, siteBatchSize);
  211. if (pendingTasks.isEmpty()) {
  212. return;
  213. }
  214. log.info("【打印队列】site={} 发现 {} 个待执行任务", site, pendingTasks.size());
  215. for (PrintTask task : pendingTasks) {
  216. processTask(task);
  217. }
  218. } catch (Exception e) {
  219. log.error("【打印队列】site任务执行异常 site={}, error={}", site, e.getMessage(), e);
  220. } finally {
  221. siteProcessingFlag.set(false);
  222. }
  223. }
  224. /**
  225. * 处理单个打印任务
  226. */
  227. private void processTask(PrintTask task) {
  228. Long taskId = task.getId();
  229. String site = task.getSite();
  230. String printerIp = task.getPrinterIp();
  231. try {
  232. // 1. 标记为执行中(乐观锁,防止并发重复执行)
  233. boolean marked = printTaskService.markAsProcessing(taskId);
  234. if (!marked) {
  235. log.debug("任务 {} 已被其他线程处理,跳过", taskId);
  236. return;
  237. }
  238. log.info("【打印队列】开始执行任务 taskId={}, site={}, 打印机={}, 份数={}",
  239. taskId, site, printerIp, task.getCopies());
  240. // 2. 解析标签数据
  241. Map<String, Object> labelData = null;
  242. if (task.getLabelData() != null && !task.getLabelData().trim().isEmpty()) {
  243. labelData = gson.fromJson(task.getLabelData(), Map.class);
  244. }
  245. // 3. 执行打印
  246. boolean success = executePrint(
  247. printerIp,
  248. task.getZplCode(),
  249. task.getCopies(),
  250. task.getRfidFlag(),
  251. labelData
  252. );
  253. // 4. 更新任务状态
  254. if (success) {
  255. printTaskService.markAsSuccess(taskId);
  256. log.info("【打印队列】✓ 任务完成 taskId={}, site={}, 打印机={}", taskId, site, printerIp);
  257. } else {
  258. printTaskService.markAsFailed(taskId, "打印失败,未返回成功标识");
  259. log.error("【打印队列】✗ 任务失败 taskId={}, site={}, 打印机={}", taskId, site, printerIp);
  260. }
  261. } catch (Exception e) {
  262. log.error("【打印队列】✗ 任务执行异常 taskId={}, site={}, 错误: {}", taskId, site, e.getMessage(), e);
  263. printTaskService.markAsFailed(taskId, "执行异常: " + e.getMessage());
  264. }
  265. }
  266. /**
  267. * 执行打印发送ZPL到打印机
  268. * 注意ZPL代码已在入队时组装好包括RFID指令这里只需要直接发送
  269. *
  270. * @param printerIp 打印机IP
  271. * @param zplCode ZPL代码已包含RFID指令
  272. * @param copies 打印份数
  273. * @param rfidFlag RFID标识用于日志
  274. * @param labelData 标签数据未使用保留以备后续扩展
  275. * @return 是否成功
  276. */
  277. private boolean executePrint(String printerIp, String zplCode, Integer copies,
  278. String rfidFlag, Map<String, Object> labelData) {
  279. Socket socket = null;
  280. try {
  281. // 1. 连接打印机
  282. socket = new Socket();
  283. socket.connect(new InetSocketAddress(printerIp, 9100), printerConnectTimeoutMs);
  284. socket.setSoTimeout(printerReadTimeoutMs);
  285. OutputStream os = socket.getOutputStream();
  286. int actualCopies = copies != null && copies > 0 ? copies : 1;
  287. // 2. 直接发送ZPL代码(已在入队时组装好,包括RFID指令)
  288. log.debug("开始发送ZPL到打印机 {}, 份数: {}, RFID: {}", printerIp, actualCopies, rfidFlag);
  289. for (int i = 0; i < actualCopies; i++) {
  290. os.write(zplCode.getBytes(StandardCharsets.UTF_8));
  291. os.flush();
  292. // RFID标签需要等待处理时间
  293. if ("Y".equals(rfidFlag)) {
  294. Thread.sleep(1000); // RFID标签等待1秒
  295. log.debug("RFID标签打印完成 {} / {}", i + 1, actualCopies);
  296. }
  297. }
  298. log.debug("✓ ZPL已发送到打印机 {}, 份数: {}", printerIp, actualCopies);
  299. return true;
  300. } catch (Exception e) {
  301. log.error("发送ZPL到打印机失败: printerIp={}, error={}", printerIp, e.getMessage(), e);
  302. return false;
  303. } finally {
  304. if (socket != null) {
  305. try {
  306. socket.close();
  307. } catch (Exception e) {
  308. log.warn("关闭socket失败: {}", e.getMessage());
  309. }
  310. }
  311. }
  312. }
  313. /**
  314. * 归档打印任务历史数据
  315. *
  316. * <p><b>功能说明</b></p>
  317. * <ul>
  318. * <li>每天凌晨5点自动执行</li>
  319. * <li>将已完成的打印任务备份到 print_task_history </li>
  320. * <li>清理10天之前的历史数据</li>
  321. * </ul>
  322. *
  323. * <p><b>执行时间</b>每天凌晨5点固定</p>
  324. * <p><b>归档规则</b>归档10天前的已完成任务固定</p>
  325. */
  326. @Scheduled(cron = "0 0 5 * * ?", scheduler = "printQueueTaskScheduler")
  327. public void archivePrintTaskHistory() {
  328. // 检查定时任务开关
  329. if (!dashboardPushEnabled) {
  330. return;
  331. }
  332. log.info("=== 开始归档打印任务历史数据 ===");
  333. try {
  334. // 调用Service层执行归档操作
  335. int archivedCount = printTaskService.archivePrintTaskHistory();
  336. log.info("=== 打印任务历史数据归档完成:归档任务数量={} ===", archivedCount);
  337. } catch (Exception e) {
  338. log.error("=== 打印任务历史数据归档失败 ===", e);
  339. }
  340. }
  341. }