feat: 增强设备任务更新逻辑,支持异步图片下载和保存

主要修改:
1. 引入ImageSaveService,处理任务完成时的图片下载和保存逻辑。
2. 更新任务状态时,异步保存完成图片,确保任务状态更新与图片保存的解耦。
3. 新增saveProgressImagesForTask方法,定期保存进行中任务的图片快照。
4. 更新任务状态处理逻辑,确保即使图片保存失败,任务仍然被标记为完成。

技术细节:
- 通过异步处理,提升了任务更新的效率和用户体验。
- 新增的图片保存配置支持更灵活的图片管理和存储策略。
This commit is contained in:
zyh
2025-08-27 17:01:05 +08:00
parent 90c47df7a3
commit 01bc703ea2
5 changed files with 603 additions and 58 deletions

View File

@@ -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<Resource> 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";
}
}
}

View File

@@ -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<Map<String, String>> 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<String, String> 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<Map<String, String>> 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<String, String> 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<Map<String, String>> downloadAndSaveImages(String deviceId, String taskDir, Map<String, String> imageTypes) {
Map<String, String> 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<String> 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<String> 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";
}
}

View File

@@ -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<String, String> 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<String, String> 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<String, String> 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<String, String> generateCompletionImages(String deviceId) {
Map<String, String> 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");
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())
);
}
return images;
/**
* 更新任务的进度图片信息(可选)
*/
private void updateTaskProgressImages(Long taskId, Map<String, String> savedImages) {
try {
// 这里可以选择是否要将进度图片也保存到数据库中
// 目前只记录日志,不修改数据库记录
log.debug("任务 {} 进度图片已保存到本地: {}", taskId, savedImages.keySet());
} catch (Exception e) {
log.warn("更新任务 {} 进度图片记录失败", taskId, e);
}
}
/**

View File

@@ -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<LinkTask> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("status", "LOGGED_IN");
List<LinkTask> 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<LinkTask> 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<LinkTask> 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<LinkTask> 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<LinkTask> 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);
}
}
}

View File

@@ -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 # 下载超时时间(秒)