diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 5557eae..262e986 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,7 +1,9 @@ { "permissions": { "allow": [ - "Bash(git add:*)" + "Bash(git add:*)", + "Bash(mvn clean:*)", + "Bash(mvn test:*)" ], "deny": [], "ask": [] diff --git a/src/main/java/com/gameplatform/server/controller/image/ImageController.java b/src/main/java/com/gameplatform/server/controller/image/ImageController.java deleted file mode 100644 index f7c895c..0000000 --- a/src/main/java/com/gameplatform/server/controller/image/ImageController.java +++ /dev/null @@ -1,97 +0,0 @@ -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/controller/link/QrProxyController.java b/src/main/java/com/gameplatform/server/controller/link/QrProxyController.java index 31d77f4..d770f4f 100644 --- a/src/main/java/com/gameplatform/server/controller/link/QrProxyController.java +++ b/src/main/java/com/gameplatform/server/controller/link/QrProxyController.java @@ -24,13 +24,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.reactive.function.client.WebClientResponseException; 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.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -46,28 +39,17 @@ public class QrProxyController { private final String appBaseUrl; private final LinkTaskMapper linkTaskMapper; private final LinkBatchMapper linkBatchMapper; - private final String imageSavePath; public QrProxyController(ScriptClient scriptClient, LinkStatusService linkStatusService, @Value("${app.base-url}") String appBaseUrl, - @Value("${app.image-save-path:./images}") String imageSavePath, LinkTaskMapper linkTaskMapper, LinkBatchMapper linkBatchMapper) { this.scriptClient = scriptClient; this.linkStatusService = linkStatusService; this.appBaseUrl = appBaseUrl; - this.imageSavePath = imageSavePath; this.linkTaskMapper = linkTaskMapper; this.linkBatchMapper = linkBatchMapper; - - // 确保图片保存目录存在 - try { - Files.createDirectories(Paths.get(imageSavePath)); - log.info("图片保存目录已创建或已存在: {}", imageSavePath); - } catch (IOException e) { - log.error("创建图片保存目录失败: {}", imageSavePath, e); - } } @GetMapping(value = "/image/{codeNo}/qr.png", produces = MediaType.IMAGE_PNG_VALUE) @@ -102,18 +84,6 @@ public class QrProxyController { public Mono> homepage(@PathVariable("codeNo") String codeNo) { return linkStatusService.getLinkStatus(codeNo) .flatMap(linkStatus -> { - // 如果状态是COMPLETED,直接读取本地文件 - if ("COMPLETED".equals(linkStatus.getStatus())) { - log.info("链接已完成,尝试读取本地图片: codeNo={}", codeNo); - return readLocalImageFile(codeNo, "homepage.png") - .flatMap(this::createImageResponseMono) - .switchIfEmpty(Mono.defer(() -> { - log.warn("本地图片文件不存在,返回404: codeNo={}", codeNo); - return createNotFoundResponseMono(); - })); - } - - // 状态不是COMPLETED,使用scriptClient获取图片 String machineId = linkStatus.getMachineId(); if (machineId == null) { log.warn("无法找到codeNo对应的machineId: {}", codeNo); @@ -122,21 +92,14 @@ public class QrProxyController { String path = "/" + machineId + "/首次主页.png"; return scriptClient.getImagePng(path) - .flatMap(bytes -> { - // 异步保存图片到本地 - return saveImageToLocalAsync(codeNo, bytes, "homepage.png") - .then(Mono.just(bytes)); - }) - .flatMap(this::createImageResponseMono) + .map(bytes -> ResponseEntity.ok() + .contentType(MediaType.IMAGE_PNG) + .cacheControl(CacheControl.maxAge(30, TimeUnit.SECONDS).cachePublic()) + .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=homepage.png") + .body(bytes)) .onErrorResume(WebClientResponseException.NotFound.class, ex -> { - log.warn("scriptClient图片不存在,尝试读取本地文件: path={}", path); - // 如果scriptClient返回404,尝试读取本地文件 - return readLocalImageFile(codeNo, "homepage.png") - .flatMap(this::createImageResponseMono) - .switchIfEmpty(Mono.defer(() -> { - log.warn("本地图片文件也不存在: codeNo={}", codeNo); - return createNotFoundResponseMono(); - })); + log.warn("图片不存在: path={}", path); + return createNotFoundResponseMono(); }) .onErrorResume(WebClientResponseException.class, ex -> { log.warn("获取图片失败: path={}, status={}, error={}", path, ex.getStatusCode(), ex.getMessage()); @@ -154,18 +117,6 @@ public class QrProxyController { public Mono> firstReward(@PathVariable("codeNo") String codeNo) { return linkStatusService.getLinkStatus(codeNo) .flatMap(linkStatus -> { - // 如果状态是COMPLETED,直接读取本地文件 - if ("COMPLETED".equals(linkStatus.getStatus())) { - log.info("链接已完成,尝试读取本地图片: codeNo={}", codeNo); - return readLocalImageFile(codeNo, "firstReward.png") - .flatMap(this::createImageResponseMono) - .switchIfEmpty(Mono.defer(() -> { - log.warn("本地图片文件不存在,返回404: codeNo={}", codeNo); - return createNotFoundResponseMono(); - })); - } - - // 状态不是COMPLETED,使用scriptClient获取图片 String machineId = linkStatus.getMachineId(); if (machineId == null) { log.warn("无法找到codeNo对应的machineId: {}", codeNo); @@ -174,21 +125,14 @@ public class QrProxyController { String path = "/" + machineId + "/首次赏金.png"; return scriptClient.getImagePng(path) - .flatMap(bytes -> { - // 异步保存图片到本地 - return saveImageToLocalAsync(codeNo, bytes, "firstReward.png") - .then(Mono.just(bytes)); - }) - .flatMap(this::createImageResponseMono) + .map(bytes -> ResponseEntity.ok() + .contentType(MediaType.IMAGE_PNG) + .cacheControl(CacheControl.maxAge(30, TimeUnit.SECONDS).cachePublic()) + .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=first-reward.png") + .body(bytes)) .onErrorResume(WebClientResponseException.NotFound.class, ex -> { - log.warn("scriptClient图片不存在,尝试读取本地文件: path={}", path); - // 如果scriptClient返回404,尝试读取本地文件 - return readLocalImageFile(codeNo, "firstReward.png") - .flatMap(this::createImageResponseMono) - .switchIfEmpty(Mono.defer(() -> { - log.warn("本地图片文件也不存在: codeNo={}", codeNo); - return createNotFoundResponseMono(); - })); + log.warn("图片不存在: path={}", path); + return createNotFoundResponseMono(); }) .onErrorResume(WebClientResponseException.class, ex -> { log.warn("获取图片失败: path={}, status={}, error={}", path, ex.getStatusCode(), ex.getMessage()); @@ -206,18 +150,6 @@ public class QrProxyController { public Mono> midReward(@PathVariable("codeNo") String codeNo) { return linkStatusService.getLinkStatus(codeNo) .flatMap(linkStatus -> { - // 如果状态是COMPLETED,直接读取本地文件 - if ("COMPLETED".equals(linkStatus.getStatus())) { - log.info("链接已完成,尝试读取本地图片: codeNo={}", codeNo); - return readLocalImageFile(codeNo, "midReward.png") - .flatMap(this::createImageResponseMono) - .switchIfEmpty(Mono.defer(() -> { - log.warn("本地图片文件不存在,返回404: codeNo={}", codeNo); - return createNotFoundResponseMono(); - })); - } - - // 状态不是COMPLETED,使用scriptClient获取图片 String machineId = linkStatus.getMachineId(); if (machineId == null) { log.warn("无法找到codeNo对应的machineId: {}", codeNo); @@ -226,21 +158,14 @@ public class QrProxyController { String path = "/" + machineId + "/中途赏金.png"; return scriptClient.getImagePng(path) - .flatMap(bytes -> { - // 异步保存图片到本地 - return saveImageToLocalAsync(codeNo, bytes, "midReward.png") - .then(Mono.just(bytes)); - }) - .flatMap(this::createImageResponseMono) + .map(bytes -> ResponseEntity.ok() + .contentType(MediaType.IMAGE_PNG) + .cacheControl(CacheControl.maxAge(30, TimeUnit.SECONDS).cachePublic()) + .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=mid-reward.png") + .body(bytes)) .onErrorResume(WebClientResponseException.NotFound.class, ex -> { - log.warn("scriptClient图片不存在,尝试读取本地文件: path={}", path); - // 如果scriptClient返回404,尝试读取本地文件 - return readLocalImageFile(codeNo, "midReward.png") - .flatMap(this::createImageResponseMono) - .switchIfEmpty(Mono.defer(() -> { - log.warn("本地图片文件也不存在: codeNo={}", codeNo); - return createNotFoundResponseMono(); - })); + log.warn("图片不存在: path={}", path); + return createNotFoundResponseMono(); }) .onErrorResume(WebClientResponseException.class, ex -> { log.warn("获取图片失败: path={}, status={}, error={}", path, ex.getStatusCode(), ex.getMessage()); @@ -258,18 +183,6 @@ public class QrProxyController { public Mono> endReward(@PathVariable("codeNo") String codeNo) { return linkStatusService.getLinkStatus(codeNo) .flatMap(linkStatus -> { - // 如果状态是COMPLETED,直接读取本地文件 - if ("COMPLETED".equals(linkStatus.getStatus())) { - log.info("链接已完成,尝试读取本地图片: codeNo={}", codeNo); - return readLocalImageFile(codeNo, "endReward.png") - .flatMap(this::createImageResponseMono) - .switchIfEmpty(Mono.defer(() -> { - log.warn("本地图片文件不存在,返回404: codeNo={}", codeNo); - return createNotFoundResponseMono(); - })); - } - - // 状态不是COMPLETED,使用scriptClient获取图片 String machineId = linkStatus.getMachineId(); if (machineId == null) { log.warn("无法找到codeNo对应的machineId: {}", codeNo); @@ -278,21 +191,14 @@ public class QrProxyController { String path = "/" + machineId + "/结束赏金.png"; return scriptClient.getImagePng(path) - .flatMap(bytes -> { - // 异步保存图片到本地 - return saveImageToLocalAsync(codeNo, bytes, "endReward.png") - .then(Mono.just(bytes)); - }) - .flatMap(this::createImageResponseMono) + .map(bytes -> ResponseEntity.ok() + .contentType(MediaType.IMAGE_PNG) + .cacheControl(CacheControl.maxAge(30, TimeUnit.SECONDS).cachePublic()) + .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=end-reward.png") + .body(bytes)) .onErrorResume(WebClientResponseException.NotFound.class, ex -> { - log.warn("scriptClient图片不存在,尝试读取本地文件: path={}", path); - // 如果scriptClient返回404,尝试读取本地文件 - return readLocalImageFile(codeNo, "endReward.png") - .flatMap(this::createImageResponseMono) - .switchIfEmpty(Mono.defer(() -> { - log.warn("本地图片文件也不存在: codeNo={}", codeNo); - return createNotFoundResponseMono(); - })); + log.warn("图片不存在: path={}", path); + return createNotFoundResponseMono(); }) .onErrorResume(WebClientResponseException.class, ex -> { log.warn("获取图片失败: path={}, status={}, error={}", path, ex.getStatusCode(), ex.getMessage()); @@ -392,96 +298,12 @@ public class QrProxyController { } } - /** - * 保存图片到本地文件系统 - * @param codeNo 任务编号 - * @param imageData 图片数据 - * @param fileName 文件名 - */ - private void saveImageToLocal(String codeNo, byte[] imageData, String fileName) { - if (imageData == null || imageData.length == 0) { - log.debug("图片数据为空,跳过保存: codeNo={}, fileName={}", codeNo, fileName); - return; - } - - try { - // 创建任务目录 - Path taskDirPath = Paths.get(imageSavePath, codeNo); - Files.createDirectories(taskDirPath); - - // 保存文件 - Path filePath = taskDirPath.resolve(fileName); - Files.write(filePath, imageData, StandardOpenOption.CREATE, StandardOpenOption.WRITE); - - log.debug("图片保存成功: {} ({}字节)", filePath, imageData.length); - - } catch (IOException e) { - log.warn("保存图片失败: codeNo={}, fileName={}, error={}", codeNo, fileName, e.getMessage()); - } - } - - /** - * 异步保存图片到本地 - * @param codeNo 任务编号 - * @param imageData 图片数据 - * @param fileName 文件名 - * @return Mono - */ - private Mono saveImageToLocalAsync(String codeNo, byte[] imageData, String fileName) { - return Mono.fromRunnable(() -> saveImageToLocal(codeNo, imageData, fileName)) - .subscribeOn(Schedulers.boundedElastic()) - .then(); - } - - /** - * 读取本地保存的图片文件 - * @param codeNo 任务编号 - * @param fileName 文件名 - * @return Mono 图片数据,如果文件不存在则返回empty - */ - private Mono readLocalImageFile(String codeNo, String fileName) { - return Mono.fromCallable(() -> { - Path filePath = Paths.get(imageSavePath, codeNo, fileName); - if (Files.exists(filePath)) { - byte[] imageBytes = Files.readAllBytes(filePath); - log.info("读取本地图片成功: {} ({}字节)", filePath, imageBytes.length); - return imageBytes; - } else { - log.debug("本地图片文件不存在: {}", filePath); - return null; - } - }) - .subscribeOn(Schedulers.boundedElastic()) - .filter(bytes -> bytes != null); - } - - /** - * 创建图片响应 - * @param bytes 图片字节数据 - * @return ResponseEntity - */ - private ResponseEntity createImageResponse(byte[] bytes) { - return ResponseEntity.ok() - .contentType(MediaType.IMAGE_PNG) - .cacheControl(CacheControl.maxAge(60, TimeUnit.SECONDS).cachePublic()) - .body(bytes); - } - - /** - * 创建图片响应的Mono包装 - * @param bytes 图片字节数据 - * @return Mono> - */ - private Mono> createImageResponseMono(byte[] bytes) { - return Mono.just(createImageResponse(bytes)); - } - /** * 创建404响应的Mono包装 * @return Mono> */ private Mono> createNotFoundResponseMono() { - return createNotFoundResponseMono(); + return Mono.just(ResponseEntity.notFound().build()); } /** @@ -489,7 +311,7 @@ public class QrProxyController { * @return Mono> */ private Mono> createInternalServerErrorResponseMono() { - return createInternalServerErrorResponseMono(); + return Mono.just(ResponseEntity.internalServerError().build()); } } diff --git a/src/main/java/com/gameplatform/server/service/image/ImageSaveService.java b/src/main/java/com/gameplatform/server/service/image/ImageSaveService.java deleted file mode 100644 index 5e8d818..0000000 --- a/src/main/java/com/gameplatform/server/service/image/ImageSaveService.java +++ /dev/null @@ -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> downloadAndSaveCompletionImages(String deviceId, String codeNo) { - log.info("开始为任务 {} 设备 {} 下载完成图片", codeNo, deviceId); - - Map 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> downloadAndSaveProgressImages(String deviceId, String codeNo) { - log.debug("开始为任务 {} 设备 {} 下载进度图片", codeNo, deviceId); - - // 进行中只保存可能有的图片 - Map 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> downloadAndSaveImages(String deviceId, String codeNo, Map imageTypes) { - Map 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 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 + 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 52fdc58..f435100 100644 --- a/src/main/java/com/gameplatform/server/service/link/DeviceTaskUpdateService.java +++ b/src/main/java/com/gameplatform/server/service/link/DeviceTaskUpdateService.java @@ -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 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 savedImages) { - try { - // 这里可以选择是否要将进度图片也保存到数据库中 - // 目前只记录日志,不修改数据库记录 - log.debug("任务 {} 进度图片已保存到本地: {}", codeNo, savedImages.keySet()); - } catch (Exception e) { - log.warn("更新任务 {} 进度图片记录失败", codeNo, e); - } - } - - /** - * 将图片URL映射转换为JSON字符串 - */ - private String convertToJson(Map images) { - try { - return objectMapper.writeValueAsString(images); - } catch (JsonProcessingException e) { - log.error("转换完成图片URL为JSON失败", e); - return "{}"; - } - } - /** * 批量处理设备状态更新 * @param deviceStatus 设备状态响应 diff --git a/src/main/java/com/gameplatform/server/service/link/LinkStatusService.java b/src/main/java/com/gameplatform/server/service/link/LinkStatusService.java index a873322..1e2b364 100644 --- a/src/main/java/com/gameplatform/server/service/link/LinkStatusService.java +++ b/src/main/java/com/gameplatform/server/service/link/LinkStatusService.java @@ -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: 开始调用脚本端选区"); diff --git a/src/main/java/com/gameplatform/server/task/ImageSaveScheduleTask.java b/src/main/java/com/gameplatform/server/task/ImageSaveScheduleTask.java deleted file mode 100644 index 8a4d3de..0000000 --- a/src/main/java/com/gameplatform/server/task/ImageSaveScheduleTask.java +++ /dev/null @@ -1,168 +0,0 @@ -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.getCodeNo()); - - 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.getCodeNo()); - 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 93ef4ef..0cd7028 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -72,13 +72,6 @@ script: app: base-url: "https://2.uzi0.cc" # 生产环境需要配置为实际域名 # 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 # 下载超时时间(秒) diff --git a/src/test/java/com/gameplatform/server/service/link/DeviceTaskUpdateServiceTest.java b/src/test/java/com/gameplatform/server/service/link/DeviceTaskUpdateServiceTest.java index aac6a7d..82fe997 100644 --- a/src/test/java/com/gameplatform/server/service/link/DeviceTaskUpdateServiceTest.java +++ b/src/test/java/com/gameplatform/server/service/link/DeviceTaskUpdateServiceTest.java @@ -4,18 +4,14 @@ 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.service.image.ImageSaveService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import reactor.core.publisher.Mono; import java.time.LocalDateTime; import java.util.Arrays; -import java.util.HashMap; import java.util.List; -import java.util.Map; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; @@ -31,16 +27,13 @@ public class DeviceTaskUpdateServiceTest { @Mock private LinkTaskMapper linkTaskMapper; - @Mock - private ImageSaveService imageSaveService; - private ObjectMapper objectMapper; @BeforeEach void setUp() { MockitoAnnotations.openMocks(this); objectMapper = new ObjectMapper(); - deviceTaskUpdateService = new DeviceTaskUpdateService(linkTaskMapper, objectMapper, imageSaveService); + deviceTaskUpdateService = new DeviceTaskUpdateService(linkTaskMapper, objectMapper); } @Test @@ -85,13 +78,6 @@ public class DeviceTaskUpdateServiceTest { when(linkTaskMapper.findByMachineIdAndStatus("f1", "LOGGED_IN")).thenReturn(tasks); when(linkTaskMapper.update(any(LinkTask.class))).thenReturn(1); - Map mockImages = new HashMap<>(); - mockImages.put("homepage", "首次主页.png"); - mockImages.put("firstReward", "首次赏金.png"); - mockImages.put("midReward", "中途赏金.png"); - mockImages.put("endReward", "结束赏金.png"); - when(imageSaveService.downloadAndSaveCompletionImages(anyString(), anyString())) - .thenReturn(Mono.just(mockImages)); // 执行测试 deviceTaskUpdateService.updateTaskByDeviceStatus(deviceInfo); @@ -103,11 +89,6 @@ public class DeviceTaskUpdateServiceTest { // 验证任务状态已更新为COMPLETED assertEquals("COMPLETED", task.getStatus()); assertEquals(Integer.valueOf(0), task.getCompletedPoints()); - assertNotNull(task.getCompletionImages()); - assertTrue(task.getCompletionImages().contains("首次主页.png")); - assertTrue(task.getCompletionImages().contains("首次赏金.png")); - assertTrue(task.getCompletionImages().contains("中途赏金.png")); - assertTrue(task.getCompletionImages().contains("结束赏金.png")); } @Test @@ -124,13 +105,6 @@ public class DeviceTaskUpdateServiceTest { when(linkTaskMapper.findByMachineIdAndStatus("f1", "LOGGED_IN")).thenReturn(tasks); when(linkTaskMapper.update(any(LinkTask.class))).thenReturn(1); - Map mockImages = new HashMap<>(); - mockImages.put("homepage", "首次主页.png"); - mockImages.put("firstReward", "首次赏金.png"); - mockImages.put("midReward", "中途赏金.png"); - mockImages.put("endReward", "结束赏金.png"); - when(imageSaveService.downloadAndSaveCompletionImages(anyString(), anyString())) - .thenReturn(Mono.just(mockImages)); // 执行测试 deviceTaskUpdateService.updateTaskByDeviceStatus(deviceInfo); @@ -142,7 +116,6 @@ public class DeviceTaskUpdateServiceTest { // 验证任务状态已更新为COMPLETED,点数保持不变 assertEquals("COMPLETED", task.getStatus()); assertEquals(Integer.valueOf(2350), task.getCompletedPoints()); - assertNotNull(task.getCompletionImages()); } @Test