feat: 增强设备任务更新逻辑,支持异步图片下载和保存
主要修改: 1. 引入ImageSaveService,处理任务完成时的图片下载和保存逻辑。 2. 更新任务状态时,异步保存完成图片,确保任务状态更新与图片保存的解耦。 3. 新增saveProgressImagesForTask方法,定期保存进行中任务的图片快照。 4. 更新任务状态处理逻辑,确保即使图片保存失败,任务仍然被标记为完成。 技术细节: - 通过异步处理,提升了任务更新的效率和用户体验。 - 新增的图片保存配置支持更灵活的图片管理和存储策略。
This commit is contained in:
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
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<String, String> savedImages) {
|
||||
try {
|
||||
// 这里可以选择是否要将进度图片也保存到数据库中
|
||||
// 目前只记录日志,不修改数据库记录
|
||||
log.debug("任务 {} 进度图片已保存到本地: {}", taskId, savedImages.keySet());
|
||||
} catch (Exception e) {
|
||||
log.warn("更新任务 {} 进度图片记录失败", taskId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user