refactor: 移除图片保存相关逻辑,直接更新任务状态为完成

This commit is contained in:
zyh
2025-08-30 15:43:58 +08:00
parent 63e42368cb
commit abe1447e0c
9 changed files with 51 additions and 834 deletions

View File

@@ -1,200 +0,0 @@
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.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);
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 codeNo 任务编号
* @return 保存后的本地图片URL映射
*/
public Mono<Map<String, String>> downloadAndSaveCompletionImages(String deviceId, String codeNo) {
log.info("开始为任务 {} 设备 {} 下载完成图片", codeNo, deviceId);
Map<String, String> imageTypes = Map.of(
"homepage", "首次主页.png",
"firstReward", "首次赏金.png",
"midReward", "中途赏金.png",
"endReward", "结束赏金.png"
);
return downloadAndSaveImages(deviceId, codeNo, imageTypes)
.doOnSuccess(result -> log.info("任务 {} 设备 {} 完成图片下载保存完成,共保存{}张图片",
codeNo, deviceId, result.size()))
.doOnError(error -> log.error("任务 {} 设备 {} 完成图片下载保存失败", codeNo, deviceId, error));
}
/**
* 下载并保存进行中的任务快照图片
* @param deviceId 设备ID
* @param codeNo 任务编号
* @return 保存后的本地图片URL映射
*/
public Mono<Map<String, String>> downloadAndSaveProgressImages(String deviceId, String codeNo) {
log.debug("开始为任务 {} 设备 {} 下载进度图片", codeNo, deviceId);
// 进行中只保存可能有的图片
Map<String, String> imageTypes = Map.of(
"homepage", "首次主页.png",
"firstReward", "首次赏金.png",
"midReward", "中途赏金.png"
);
return downloadAndSaveImages(deviceId, codeNo, imageTypes)
.doOnSuccess(result -> log.debug("任务 {} 设备 {} 进度图片下载保存完成,共保存{}张图片",
codeNo, deviceId, result.size()))
.doOnError(error -> log.warn("任务 {} 设备 {} 进度图片下载保存失败", codeNo, deviceId, error));
}
/**
* 通用的图片下载保存方法
*/
private Mono<Map<String, String>> downloadAndSaveImages(String deviceId, String codeNo, Map<String, String> imageTypes) {
Map<String, String> savedImageUrls = new HashMap<>();
return Mono.fromCallable(() -> {
// 使用codeNo作为目录名
Path taskDirPath = Paths.get(imageSavePath, codeNo);
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 + 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

@@ -1,12 +1,10 @@
package com.gameplatform.server.service.link;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
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;
@@ -16,7 +14,6 @@ import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
/**
* 设备任务更新服务
@@ -28,15 +25,11 @@ public class DeviceTaskUpdateService {
private final LinkTaskMapper linkTaskMapper;
private final ObjectMapper objectMapper;
private final ImageSaveService imageSaveService;
public DeviceTaskUpdateService(LinkTaskMapper linkTaskMapper,
ObjectMapper objectMapper,
ImageSaveService imageSaveService) {
ObjectMapper objectMapper) {
this.linkTaskMapper = linkTaskMapper;
this.objectMapper = objectMapper;
this.imageSaveService = imageSaveService;
}
/**
@@ -84,59 +77,19 @@ public class DeviceTaskUpdateService {
for (LinkTask task : tasks) {
try {
// 异步下载并保存完成图片
imageSaveService.downloadAndSaveCompletionImages(deviceId, task.getCodeNo())
.subscribe(
savedImages -> {
// 保存成功后更新任务
updateTaskWithCompletionImages(task, savedImages);
},
error -> {
// 即使图片保存失败,也要标记任务为完成,但不保存图片信息
log.error("保存完成图片失败,任务 {} 仍将标记为完成", task.getId(), error);
updateTaskWithoutImages(task);
}
);
// 直接更新任务状态为完成
updateTaskAsCompleted(task);
} catch (Exception 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) {
private void updateTaskAsCompleted(LinkTask task) {
try {
task.setStatus("COMPLETED");
task.setUpdatedAt(LocalDateTime.now());
@@ -148,7 +101,7 @@ public class DeviceTaskUpdateService {
int updated = linkTaskMapper.update(task);
if (updated > 0) {
log.info("链接任务 {} (代码: {}) 已标记为完成,完成点数: {} (图片保存失败)",
log.info("链接任务 {} (代码: {}) 已标记为完成,完成点数: {}",
task.getId(), task.getCodeNo(), task.getCompletedPoints());
} else {
log.warn("更新链接任务 {} 失败", task.getId());
@@ -169,24 +122,11 @@ public class DeviceTaskUpdateService {
for (LinkTask task : tasks) {
try {
// 异步下载并保存完成图片
imageSaveService.downloadAndSaveCompletionImages(deviceId, task.getCodeNo())
.subscribe(
savedImages -> {
// 保存成功后更新任务
updateTaskWithCompletionImages(task, savedImages);
},
error -> {
// 即使图片保存失败,也要标记任务为完成
log.error("保存完成图片失败,任务 {} 仍将标记为完成", task.getId(), error);
updateTaskWithoutImages(task);
}
);
// 直接更新任务状态为完成
updateTaskAsCompleted(task);
} catch (Exception e) {
log.error("处理空闲状态任务 {} 时发生异常", task.getId(), e);
// 出现异常也要尝试标记为完成
updateTaskWithoutImages(task);
}
}
}
@@ -215,54 +155,6 @@ public class DeviceTaskUpdateService {
}
}
/**
* 定期保存进行中任务的图片快照
* @param deviceId 设备ID
* @param codeNo 任务编号
*/
public void saveProgressImagesForTask(String deviceId, String codeNo) {
log.debug("开始为任务 {} 设备 {} 保存进度图片", codeNo, deviceId);
imageSaveService.downloadAndSaveProgressImages(deviceId, codeNo)
.subscribe(
savedImages -> {
if (!savedImages.isEmpty()) {
log.info("任务 {} 设备 {} 进度图片保存成功,共保存{}张图片",
codeNo, deviceId, savedImages.size());
// 可选:更新任务记录中的进度图片信息
updateTaskProgressImagesByCodeNo(codeNo, savedImages);
}
},
error -> log.warn("任务 {} 设备 {} 进度图片保存失败: {}",
codeNo, deviceId, error.getMessage())
);
}
/**
* 更新任务的进度图片信息(可选)
*/
private void updateTaskProgressImagesByCodeNo(String codeNo, Map<String, String> savedImages) {
try {
// 这里可以选择是否要将进度图片也保存到数据库中
// 目前只记录日志,不修改数据库记录
log.debug("任务 {} 进度图片已保存到本地: {}", codeNo, savedImages.keySet());
} catch (Exception e) {
log.warn("更新任务 {} 进度图片记录失败", codeNo, e);
}
}
/**
* 将图片URL映射转换为JSON字符串
*/
private String convertToJson(Map<String, String> images) {
try {
return objectMapper.writeValueAsString(images);
} catch (JsonProcessingException e) {
log.error("转换完成图片URL为JSON失败", e);
return "{}";
}
}
/**
* 批量处理设备状态更新
* @param deviceStatus 设备状态响应

View File

@@ -669,15 +669,15 @@ public class LinkStatusService {
}
// 7. 调用保存总次数接口
log.info("步骤7: 开始保存总次数到脚本端");
try {
log.info("保存总次数: 设备={}, 次数={}", selectedDeviceId, linkBatch.getTimes());
scriptClient.saveTotalTimes(selectedDeviceId, linkBatch.getTimes()).block();
log.info("总次数保存成功: 设备={}, 次数={}", selectedDeviceId, linkBatch.getTimes());
} catch (Exception e) {
log.warn("总次数保存失败: 设备={}, 次数={}, 错误={}, 继续后续流程", selectedDeviceId, linkBatch.getTimes(), e.getMessage());
// 不影响后续流程,只记录警告日志
}
// log.info("步骤7: 开始保存总次数到脚本端");
// try {
// log.info("保存总次数: 设备={}, 次数={}", selectedDeviceId, linkBatch.getTimes());
// scriptClient.saveTotalTimes(selectedDeviceId, linkBatch.getTimes()).block();
// log.info("总次数保存成功: 设备={}, 次数={}", selectedDeviceId, linkBatch.getTimes());
// } catch (Exception e) {
// log.warn("总次数保存失败: 设备={}, 次数={}, 错误={}, 继续后续流程", selectedDeviceId, linkBatch.getTimes(), e.getMessage());
// // 不影响后续流程,只记录警告日志
// }
// 8. 调用脚本端选区,使用选中的设备
log.info("步骤8: 开始调用脚本端选区");