diff --git a/src/main/java/com/gameplatform/server/controller/image/ImageController.java b/src/main/java/com/gameplatform/server/controller/image/ImageController.java new file mode 100644 index 0000000..f7c895c --- /dev/null +++ b/src/main/java/com/gameplatform/server/controller/image/ImageController.java @@ -0,0 +1,97 @@ +package com.gameplatform.server.controller.image; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * 图片访问控制器 + * 提供本地保存的图片访问接口 + */ +@RestController +@RequestMapping("/api/images") +public class ImageController { + private static final Logger log = LoggerFactory.getLogger(ImageController.class); + + private final String imageSavePath; + + public ImageController(@Value("${app.image-save-path:./images}") String imageSavePath) { + this.imageSavePath = imageSavePath; + } + + /** + * 获取保存的图片 + * @param imagePath 图片相对路径,如 task_123_device_f1_completed_20231201_143000/homepage_1701234567890.png + * @return 图片资源 + */ + @GetMapping("/{imagePath:.+}") + public ResponseEntity getImage(@PathVariable String imagePath) { + try { + // 构建完整的文件路径 + Path fullPath = Paths.get(imageSavePath).resolve(imagePath).normalize(); + + // 安全检查:确保请求的文件在指定目录内 + Path basePath = Paths.get(imageSavePath).normalize(); + if (!fullPath.startsWith(basePath)) { + log.warn("非法的图片路径访问尝试: {}", imagePath); + return ResponseEntity.notFound().build(); + } + + // 检查文件是否存在 + if (!Files.exists(fullPath) || !Files.isRegularFile(fullPath)) { + log.debug("图片文件不存在: {}", fullPath); + return ResponseEntity.notFound().build(); + } + + // 创建资源 + Resource resource = new FileSystemResource(fullPath); + + // 设置响应头 + HttpHeaders headers = new HttpHeaders(); + String fileName = fullPath.getFileName().toString(); + String contentType = getContentType(fileName); + headers.setContentType(MediaType.parseMediaType(contentType)); + headers.setCacheControl("max-age=3600"); // 缓存1小时 + + log.debug("返回图片: {}", fullPath); + return ResponseEntity.ok() + .headers(headers) + .body(resource); + + } catch (Exception e) { + log.error("获取图片失败: {}", imagePath, e); + return ResponseEntity.internalServerError().build(); + } + } + + /** + * 根据文件扩展名获取MIME类型 + */ + private String getContentType(String fileName) { + String extension = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase(); + switch (extension) { + case "png": + return "image/png"; + case "jpg": + case "jpeg": + return "image/jpeg"; + case "gif": + return "image/gif"; + case "bmp": + return "image/bmp"; + default: + return "application/octet-stream"; + } + } +} diff --git a/src/main/java/com/gameplatform/server/service/image/ImageSaveService.java b/src/main/java/com/gameplatform/server/service/image/ImageSaveService.java new file mode 100644 index 0000000..3e836d0 --- /dev/null +++ b/src/main/java/com/gameplatform/server/service/image/ImageSaveService.java @@ -0,0 +1,209 @@ +package com.gameplatform.server.service.image; + +import com.gameplatform.server.service.external.ScriptClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Semaphore; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; + +/** + * 图片下载保存服务 + * 负责从脚本服务器下载图片并保存到本地 + */ +@Service +public class ImageSaveService { + private static final Logger log = LoggerFactory.getLogger(ImageSaveService.class); + + private final ScriptClient scriptClient; + private final String imageSavePath; + private final String serverBaseUrl; + + // 使用信号量限制并发下载数量,防止服务器资源不足 + private final Semaphore downloadSemaphore = new Semaphore(3); + + // 时间格式化器 + private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"); + + public ImageSaveService( + ScriptClient scriptClient, + @Value("${app.image-save-path:./images}") String imageSavePath, + @Value("${app.base-url}") String serverBaseUrl) { + this.scriptClient = scriptClient; + this.imageSavePath = imageSavePath; + this.serverBaseUrl = serverBaseUrl; + + // 确保图片保存目录存在 + try { + Files.createDirectories(Paths.get(imageSavePath)); + log.info("图片保存目录已创建或已存在: {}", imageSavePath); + } catch (IOException e) { + log.error("创建图片保存目录失败: {}", imageSavePath, e); + } + } + + /** + * 下载并保存完成时的所有图片 + * @param deviceId 设备ID + * @param taskId 任务ID + * @return 保存后的本地图片URL映射 + */ + public Mono> downloadAndSaveCompletionImages(String deviceId, Long taskId) { + log.info("开始为任务 {} 设备 {} 下载完成图片", taskId, deviceId); + + String timestamp = LocalDateTime.now().format(TIME_FORMATTER); + String taskDir = String.format("task_%d_device_%s_completed_%s", taskId, deviceId, timestamp); + + Map imageTypes = Map.of( + "homepage", "首次主页.png", + "firstReward", "首次赏金.png", + "midReward", "中途赏金.png", + "endReward", "结束赏金.png" + ); + + return downloadAndSaveImages(deviceId, taskDir, imageTypes) + .doOnSuccess(result -> log.info("任务 {} 设备 {} 完成图片下载保存完成,共保存{}张图片", + taskId, deviceId, result.size())) + .doOnError(error -> log.error("任务 {} 设备 {} 完成图片下载保存失败", taskId, deviceId, error)); + } + + /** + * 下载并保存进行中的任务快照图片 + * @param deviceId 设备ID + * @param taskId 任务ID + * @return 保存后的本地图片URL映射 + */ + public Mono> downloadAndSaveProgressImages(String deviceId, Long taskId) { + log.debug("开始为任务 {} 设备 {} 下载进度图片", taskId, deviceId); + + String timestamp = LocalDateTime.now().format(TIME_FORMATTER); + String taskDir = String.format("task_%d_device_%s_progress_%s", taskId, deviceId, timestamp); + + // 进行中只保存可能有的图片 + Map imageTypes = Map.of( + "homepage", "首次主页.png", + "firstReward", "首次赏金.png", + "midReward", "中途赏金.png" + ); + + return downloadAndSaveImages(deviceId, taskDir, imageTypes) + .doOnSuccess(result -> log.debug("任务 {} 设备 {} 进度图片下载保存完成,共保存{}张图片", + taskId, deviceId, result.size())) + .doOnError(error -> log.warn("任务 {} 设备 {} 进度图片下载保存失败", taskId, deviceId, error)); + } + + /** + * 通用的图片下载保存方法 + */ + private Mono> downloadAndSaveImages(String deviceId, String taskDir, Map imageTypes) { + Map savedImageUrls = new HashMap<>(); + + return Mono.fromCallable(() -> { + // 创建任务专用目录 + Path taskDirPath = Paths.get(imageSavePath, taskDir); + Files.createDirectories(taskDirPath); + return taskDirPath; + }) + .subscribeOn(Schedulers.boundedElastic()) + .flatMap(taskDirPath -> { + // 并行下载所有图片,但使用信号量限制并发数 + return Flux.fromIterable(imageTypes.entrySet()) + .flatMap(entry -> { + String imageType = entry.getKey(); + String imageName = entry.getValue(); + + return downloadSingleImageWithDelay(deviceId, imageName, taskDirPath, imageType) + .doOnSuccess(url -> { + if (url != null) { + savedImageUrls.put(imageType, url); + } + }) + .onErrorResume(error -> { + log.warn("下载图片 {} 失败: {}", imageName, error.getMessage()); + return Mono.empty(); + }); + }, 2) // 最多同时下载2张图片 + .then(Mono.just(savedImageUrls)); + }); + } + + /** + * 下载单张图片(带延迟和重试机制) + */ + private Mono downloadSingleImageWithDelay(String deviceId, String imageName, Path taskDirPath, String imageType) { + // 随机延迟0-2秒,避免同时请求 + int delayMs = ThreadLocalRandom.current().nextInt(0, 2000); + + return Mono.delay(Duration.ofMillis(delayMs)) + .then(Mono.fromCallable(() -> { + // 获取信号量许可 + boolean acquired = downloadSemaphore.tryAcquire(10, TimeUnit.SECONDS); + if (!acquired) { + throw new RuntimeException("获取下载许可超时"); + } + return true; + })) + .subscribeOn(Schedulers.boundedElastic()) + .flatMap(acquired -> { + String imagePath = String.format("/%s/%s", deviceId, imageName); + + return scriptClient.getImagePng(imagePath) + .timeout(Duration.ofSeconds(15)) + .retry(2) // 重试2次 + .flatMap(imageData -> saveImageToLocal(imageData, taskDirPath, imageName, imageType)) + .doFinally(signal -> downloadSemaphore.release()); // 释放信号量 + }); + } + + /** + * 将图片数据保存到本地文件 + */ + private Mono saveImageToLocal(byte[] imageData, Path taskDirPath, String originalName, String imageType) { + return Mono.fromCallable(() -> { + if (imageData == null || imageData.length == 0) { + log.warn("图片数据为空: {}", originalName); + return null; + } + + // 生成本地文件名 + String extension = getFileExtension(originalName); + String localFileName = imageType + "_" + System.currentTimeMillis() + extension; + Path localFilePath = taskDirPath.resolve(localFileName); + + // 保存文件 + Files.write(localFilePath, imageData, StandardOpenOption.CREATE, StandardOpenOption.WRITE); + + // 生成访问URL + String relativePath = taskDirPath.getFileName() + "/" + localFileName; + String imageUrl = serverBaseUrl + "/api/images/" + relativePath; + + log.debug("图片保存成功: {} -> {} ({}字节)", originalName, localFilePath, imageData.length); + return imageUrl; + + }).subscribeOn(Schedulers.boundedElastic()); + } + + /** + * 获取文件扩展名 + */ + private String getFileExtension(String fileName) { + int lastDotIndex = fileName.lastIndexOf('.'); + return lastDotIndex > 0 ? fileName.substring(lastDotIndex) : ".png"; + } +} diff --git a/src/main/java/com/gameplatform/server/service/link/DeviceTaskUpdateService.java b/src/main/java/com/gameplatform/server/service/link/DeviceTaskUpdateService.java index d91bfad..7ec45c4 100644 --- a/src/main/java/com/gameplatform/server/service/link/DeviceTaskUpdateService.java +++ b/src/main/java/com/gameplatform/server/service/link/DeviceTaskUpdateService.java @@ -6,15 +6,15 @@ import com.gameplatform.server.mapper.agent.LinkTaskMapper; import com.gameplatform.server.model.dto.device.DeviceStatusResponse; import com.gameplatform.server.model.entity.agent.LinkTask; import com.gameplatform.server.event.DeviceStatusUpdatedEvent; +import com.gameplatform.server.service.image.ImageSaveService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; + import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; -import java.util.HashMap; import java.util.List; import java.util.Map; @@ -28,14 +28,15 @@ public class DeviceTaskUpdateService { private final LinkTaskMapper linkTaskMapper; private final ObjectMapper objectMapper; - private final String scriptBaseUrl; + + private final ImageSaveService imageSaveService; public DeviceTaskUpdateService(LinkTaskMapper linkTaskMapper, ObjectMapper objectMapper, - @Value("${script.base-url}") String scriptBaseUrl) { + ImageSaveService imageSaveService) { this.linkTaskMapper = linkTaskMapper; this.objectMapper = objectMapper; - this.scriptBaseUrl = scriptBaseUrl; + this.imageSaveService = imageSaveService; } /** @@ -81,35 +82,83 @@ public class DeviceTaskUpdateService { log.info("设备 {} 游戏已完成,发现 {} 个LOGGED_IN状态的链接任务,开始标记为完成状态", deviceId, tasks.size()); - // 生成完成图片URL - Map completionImages = generateCompletionImages(deviceId); - String completionImagesJson = convertToJson(completionImages); - for (LinkTask task : tasks) { try { - task.setStatus("COMPLETED"); - task.setCompletionImages(completionImagesJson); - task.setUpdatedAt(LocalDateTime.now()); - - // 如果之前有点数,保持不变;如果没有,设为0(表示已完成但未获得具体点数) - if (task.getCompletedPoints() == null) { - task.setCompletedPoints(0); - } - - int updated = linkTaskMapper.update(task); - if (updated > 0) { - log.info("链接任务 {} (代码: {}) 已标记为完成,完成点数: {}, 保存了4张完成图片", - task.getId(), task.getCodeNo(), task.getCompletedPoints()); - } else { - log.warn("更新链接任务 {} 失败", task.getId()); - } + // 异步下载并保存完成图片 + imageSaveService.downloadAndSaveCompletionImages(deviceId, task.getId()) + .subscribe( + savedImages -> { + // 保存成功后更新任务 + updateTaskWithCompletionImages(task, savedImages); + }, + error -> { + // 即使图片保存失败,也要标记任务为完成,但不保存图片信息 + log.error("保存完成图片失败,任务 {} 仍将标记为完成", task.getId(), error); + updateTaskWithoutImages(task); + } + ); } catch (Exception e) { - log.error("更新链接任务 {} 时发生异常", task.getId(), e); + log.error("处理已完成任务 {} 时发生异常", task.getId(), e); + // 出现异常也要尝试标记为完成 + updateTaskWithoutImages(task); } } } + /** + * 更新任务状态并保存图片信息 + */ + private void updateTaskWithCompletionImages(LinkTask task, Map savedImages) { + try { + task.setStatus("COMPLETED"); + task.setCompletionImages(convertToJson(savedImages)); + task.setUpdatedAt(LocalDateTime.now()); + + // 如果之前有点数,保持不变;如果没有,设为0 + if (task.getCompletedPoints() == null) { + task.setCompletedPoints(0); + } + + int updated = linkTaskMapper.update(task); + if (updated > 0) { + log.info("链接任务 {} (代码: {}) 已标记为完成,完成点数: {}, 成功保存了{}张完成图片", + task.getId(), task.getCodeNo(), task.getCompletedPoints(), savedImages.size()); + } else { + log.warn("更新链接任务 {} 失败", task.getId()); + } + + } catch (Exception e) { + log.error("更新链接任务 {} 时发生异常", task.getId(), e); + } + } + + /** + * 更新任务状态(无图片信息) + */ + private void updateTaskWithoutImages(LinkTask task) { + try { + task.setStatus("COMPLETED"); + task.setUpdatedAt(LocalDateTime.now()); + + // 如果之前有点数,保持不变;如果没有,设为0 + if (task.getCompletedPoints() == null) { + task.setCompletedPoints(0); + } + + int updated = linkTaskMapper.update(task); + if (updated > 0) { + log.info("链接任务 {} (代码: {}) 已标记为完成,完成点数: {} (图片保存失败)", + task.getId(), task.getCodeNo(), task.getCompletedPoints()); + } else { + log.warn("更新链接任务 {} 失败", task.getId()); + } + + } catch (Exception e) { + log.error("更新链接任务 {} 时发生异常", task.getId(), e); + } + } + /** * 处理空闲状态的任务(可能是完成后变为空闲) */ @@ -118,31 +167,26 @@ public class DeviceTaskUpdateService { log.info("设备 {} 变为空闲状态,发现 {} 个LOGGED_IN状态的链接任务,推测游戏已完成", deviceId, tasks.size()); - // 生成完成图片URL - Map completionImages = generateCompletionImages(deviceId); - String completionImagesJson = convertToJson(completionImages); - for (LinkTask task : tasks) { try { - task.setStatus("COMPLETED"); - task.setCompletionImages(completionImagesJson); - task.setUpdatedAt(LocalDateTime.now()); - - // 如果之前有点数,保持不变;如果没有,设为0 - if (task.getCompletedPoints() == null) { - task.setCompletedPoints(0); - } - - int updated = linkTaskMapper.update(task); - if (updated > 0) { - log.info("链接任务 {} (代码: {}) 因设备空闲推测已完成,完成点数: {}", - task.getId(), task.getCodeNo(), task.getCompletedPoints()); - } else { - log.warn("更新链接任务 {} 失败", task.getId()); - } + // 异步下载并保存完成图片 + imageSaveService.downloadAndSaveCompletionImages(deviceId, task.getId()) + .subscribe( + savedImages -> { + // 保存成功后更新任务 + updateTaskWithCompletionImages(task, savedImages); + }, + error -> { + // 即使图片保存失败,也要标记任务为完成 + log.error("保存完成图片失败,任务 {} 仍将标记为完成", task.getId(), error); + updateTaskWithoutImages(task); + } + ); } catch (Exception e) { - log.error("更新链接任务 {} 时发生异常", task.getId(), e); + log.error("处理空闲状态任务 {} 时发生异常", task.getId(), e); + // 出现异常也要尝试标记为完成 + updateTaskWithoutImages(task); } } } @@ -172,19 +216,39 @@ public class DeviceTaskUpdateService { } /** - * 生成完成任务的4张图片URL + * 定期保存进行中任务的图片快照 + * @param deviceId 设备ID + * @param taskId 任务ID */ - private Map generateCompletionImages(String deviceId) { - Map images = new HashMap<>(); + public void saveProgressImagesForTask(String deviceId, Long taskId) { + log.debug("开始为任务 {} 设备 {} 保存进度图片", taskId, deviceId); - // 直接生成图片URL,不依赖ScriptClient - String baseUrl = String.format("%s/%s/", scriptBaseUrl, deviceId); - images.put("homepage", baseUrl + "首次主页.png"); - images.put("firstReward", baseUrl + "首次赏金.png"); - images.put("midReward", baseUrl + "中途赏金.png"); - images.put("endReward", baseUrl + "结束赏金.png"); - - return images; + imageSaveService.downloadAndSaveProgressImages(deviceId, taskId) + .subscribe( + savedImages -> { + if (!savedImages.isEmpty()) { + log.info("任务 {} 设备 {} 进度图片保存成功,共保存{}张图片", + taskId, deviceId, savedImages.size()); + // 可选:更新任务记录中的进度图片信息 + updateTaskProgressImages(taskId, savedImages); + } + }, + error -> log.warn("任务 {} 设备 {} 进度图片保存失败: {}", + taskId, deviceId, error.getMessage()) + ); + } + + /** + * 更新任务的进度图片信息(可选) + */ + private void updateTaskProgressImages(Long taskId, Map savedImages) { + try { + // 这里可以选择是否要将进度图片也保存到数据库中 + // 目前只记录日志,不修改数据库记录 + log.debug("任务 {} 进度图片已保存到本地: {}", taskId, savedImages.keySet()); + } catch (Exception e) { + log.warn("更新任务 {} 进度图片记录失败", taskId, e); + } } /** diff --git a/src/main/java/com/gameplatform/server/task/ImageSaveScheduleTask.java b/src/main/java/com/gameplatform/server/task/ImageSaveScheduleTask.java new file mode 100644 index 0000000..72b8206 --- /dev/null +++ b/src/main/java/com/gameplatform/server/task/ImageSaveScheduleTask.java @@ -0,0 +1,168 @@ +package com.gameplatform.server.task; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.gameplatform.server.mapper.agent.LinkTaskMapper; +import com.gameplatform.server.model.entity.agent.LinkTask; +import com.gameplatform.server.service.link.DeviceTaskUpdateService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; + +/** + * 图片保存定时任务 + * 每3分钟为进行中的任务保存图片快照 + */ +@Component +public class ImageSaveScheduleTask { + private static final Logger log = LoggerFactory.getLogger(ImageSaveScheduleTask.class); + + private final LinkTaskMapper linkTaskMapper; + private final DeviceTaskUpdateService deviceTaskUpdateService; + + + // 使用单独的线程池执行图片保存任务,避免阻塞主线程 + private final Executor imageTaskExecutor = Executors.newFixedThreadPool(2); + + public ImageSaveScheduleTask( + LinkTaskMapper linkTaskMapper, + DeviceTaskUpdateService deviceTaskUpdateService) { + this.linkTaskMapper = linkTaskMapper; + this.deviceTaskUpdateService = deviceTaskUpdateService; + + log.info("图片保存定时任务已初始化"); + } + + /** + * 每3分钟执行一次的图片保存任务 + * 使用fixedDelay确保上一次执行完成后再开始下一次 + */ + @Scheduled(fixedDelayString = "${image.save-interval-minutes:3}", timeUnit = TimeUnit.MINUTES) + public void saveProgressImages() { + try { + log.debug("开始执行进度图片保存定时任务"); + + // 查找所有LOGGED_IN状态的任务 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("status", "LOGGED_IN"); + List activeTasks = linkTaskMapper.selectList(queryWrapper); + + if (activeTasks.isEmpty()) { + log.debug("当前没有进行中的任务,跳过图片保存"); + return; + } + + log.info("发现 {} 个进行中的任务,开始保存进度图片", activeTasks.size()); + + // 分批处理任务,避免同时处理过多任务 + processBatchTasks(activeTasks); + + } catch (Exception e) { + log.error("执行进度图片保存定时任务时发生异常", e); + } + } + + /** + * 分批处理任务列表 + */ + private void processBatchTasks(List tasks) { + final int batchSize = 5; // 每批处理5个任务 + + for (int i = 0; i < tasks.size(); i += batchSize) { + final int startIndex = i; + final int endIndex = Math.min(i + batchSize, tasks.size()); + final List batch = tasks.subList(startIndex, endIndex); + + // 异步处理每一批任务 + imageTaskExecutor.execute(() -> processSingleBatch(batch, startIndex / batchSize + 1)); + + // 批次之间稍微延迟,避免过度并发 + if (endIndex < tasks.size()) { + try { + Thread.sleep(1000); // 延迟1秒 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.warn("批次间延迟被中断"); + } + } + } + } + + /** + * 处理单个批次的任务 + */ + private void processSingleBatch(List batch, int batchNumber) { + log.debug("开始处理第 {} 批任务,共 {} 个任务", batchNumber, batch.size()); + + for (LinkTask task : batch) { + try { + // 为每个任务随机延迟0-10秒,避免同时请求 + int delaySeconds = ThreadLocalRandom.current().nextInt(0, 10); + Thread.sleep(delaySeconds * 1000L); + + // 保存任务的进度图片 + deviceTaskUpdateService.saveProgressImagesForTask(task.getMachineId(), task.getId()); + + log.debug("任务 {} (设备: {}) 进度图片保存请求已提交", task.getId(), task.getMachineId()); + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.warn("任务 {} 处理被中断", task.getId()); + break; + } catch (Exception e) { + log.error("处理任务 {} 进度图片保存时发生异常", task.getId(), e); + } + } + + log.debug("第 {} 批任务处理完成", batchNumber); + } + + /** + * 手动触发保存所有进行中任务的图片(用于调试或紧急情况) + */ + public void saveAllProgressImages() { + log.info("手动触发进度图片保存"); + saveProgressImages(); + } + + /** + * 为特定设备的任务保存进度图片 + * @param deviceId 设备ID + */ + public void saveProgressImagesForDevice(String deviceId) { + try { + List deviceTasks = linkTaskMapper.findByMachineIdAndStatus(deviceId, "LOGGED_IN"); + + if (deviceTasks.isEmpty()) { + log.debug("设备 {} 没有进行中的任务", deviceId); + return; + } + + log.info("为设备 {} 的 {} 个任务保存进度图片", deviceId, deviceTasks.size()); + + imageTaskExecutor.execute(() -> { + for (LinkTask task : deviceTasks) { + try { + deviceTaskUpdateService.saveProgressImagesForTask(deviceId, task.getId()); + Thread.sleep(2000); // 每个任务间延迟2秒 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } catch (Exception e) { + log.error("为设备 {} 任务 {} 保存进度图片时发生异常", deviceId, task.getId(), e); + } + } + }); + + } catch (Exception e) { + log.error("为设备 {} 保存进度图片时发生异常", deviceId, e); + } + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index fbb3e00..65cc49f 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -71,6 +71,13 @@ script: # 服务器配置 app: base-url: "http://localhost:18080" # 生产环境需要配置为实际域名 + image-save-path: "./images" # 图片保存路径 link: expire-hours: 2 + +# 图片保存配置 +image: + save-interval-minutes: 3 # 进行中任务图片保存间隔(分钟) + max-concurrent-downloads: 3 # 最大并发下载数 + download-timeout-seconds: 15 # 下载超时时间(秒)