From 7499bce07bd297dde3b62a3b1bbc43b9cce094b6 Mon Sep 17 00:00:00 2001 From: zyh Date: Fri, 29 Aug 2025 00:04:08 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E4=BA=8C=E7=BB=B4?= =?UTF-8?q?=E7=A0=81=E5=92=8C=E5=9B=BE=E7=89=87=E4=BB=A3=E7=90=86=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 主要修改: 1. 在QrProxyController中引入图片保存路径配置,确保图片保存目录存在。 2. 更新图片获取逻辑,支持从本地读取和异步保存图片,提升了系统的灵活性和性能。 3. 增加了处理404和500错误的逻辑,增强了接口的健壮性。 技术细节: - 通过优化图片处理流程,提升了用户体验,同时确保了图片的有效管理和存储。 --- .../controller/link/QrProxyController.java | 368 +++++++++++++++--- 1 file changed, 314 insertions(+), 54 deletions(-) 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 a72a8f4..ccd1ee6 100644 --- a/src/main/java/com/gameplatform/server/controller/link/QrProxyController.java +++ b/src/main/java/com/gameplatform/server/controller/link/QrProxyController.java @@ -3,12 +3,14 @@ package com.gameplatform.server.controller.link; import com.gameplatform.server.mapper.agent.LinkBatchMapper; import com.gameplatform.server.mapper.agent.LinkTaskMapper; import com.gameplatform.server.model.dto.link.GameInterfaceResponse; + import com.gameplatform.server.model.entity.agent.LinkBatch; import com.gameplatform.server.model.entity.agent.LinkTask; import com.gameplatform.server.service.external.ScriptClient; import com.gameplatform.server.service.link.LinkStatusService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; @@ -20,8 +22,15 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; 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; @@ -37,17 +46,28 @@ 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) @@ -57,7 +77,7 @@ public class QrProxyController { String machineId = linkStatusService.getMechainIdByCode(codeNo); if (machineId == null) { log.warn("无法找到codeNo对应的machineId: {}", codeNo); - return Mono.just(ResponseEntity.notFound().build()); + return createNotFoundResponseMono(); } String path = "/" + machineId + "/二维码.png"; @@ -66,83 +86,223 @@ public class QrProxyController { .contentType(MediaType.IMAGE_PNG) .cacheControl(CacheControl.maxAge(30, TimeUnit.SECONDS).cachePublic()) .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=qr.png") - .body(bytes)); + .body(bytes)) + .onErrorResume(WebClientResponseException.NotFound.class, ex -> { + log.warn("图片不存在: path={}", path); + return createNotFoundResponseMono(); + }) + .onErrorResume(WebClientResponseException.class, ex -> { + log.warn("获取图片失败: path={}, status={}, error={}", path, ex.getStatusCode(), ex.getMessage()); + return Mono.just(ResponseEntity.status(ex.getStatusCode()).build()); + }); } @GetMapping(value = "/image/{codeNo}/homepage.png", produces = MediaType.IMAGE_PNG_VALUE) @Operation(summary = "首次主页图片代理") public Mono> homepage(@PathVariable("codeNo") String codeNo) { - // 通过codeNo查询machineId - String machineId = linkStatusService.getMechainIdByCode(codeNo); - if (machineId == null) { - log.warn("无法找到codeNo对应的machineId: {}", codeNo); - return Mono.just(ResponseEntity.notFound().build()); - } + 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); + return createNotFoundResponseMono(); + } - String path = "/" + machineId + "/首次主页.png"; - return scriptClient.getImagePng(path) - .map(bytes -> ResponseEntity.ok() - .contentType(MediaType.IMAGE_PNG) - .cacheControl(CacheControl.maxAge(60, TimeUnit.SECONDS).cachePublic()) - .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=homepage.png") - .body(bytes)); + String path = "/" + machineId + "/首次主页.png"; + return scriptClient.getImagePng(path) + .flatMap(bytes -> { + // 异步保存图片到本地 + return saveImageToLocalAsync(codeNo, bytes, "homepage.png") + .then(Mono.just(bytes)); + }) + .flatMap(this::createImageResponseMono) + .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(); + })); + }) + .onErrorResume(WebClientResponseException.class, ex -> { + log.warn("获取图片失败: path={}, status={}, error={}", path, ex.getStatusCode(), ex.getMessage()); + return Mono.just(ResponseEntity.status(ex.getStatusCode()).build()); + }); + }) + .onErrorResume(Exception.class, ex -> { + log.error("获取链接状态失败: codeNo={}, error={}", codeNo, ex.getMessage()); + return createInternalServerErrorResponseMono(); + }); } @GetMapping(value = "/image/{codeNo}/first-reward.png", produces = MediaType.IMAGE_PNG_VALUE) @Operation(summary = "首次赏金图片代理") public Mono> firstReward(@PathVariable("codeNo") String codeNo) { - // 通过codeNo查询machineId - String machineId = linkStatusService.getMechainIdByCode(codeNo); - if (machineId == null) { - log.warn("无法找到codeNo对应的machineId: {}", codeNo); - return Mono.just(ResponseEntity.notFound().build()); - } + 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); + return createNotFoundResponseMono(); + } - String path = "/" + machineId + "/首次赏金.png"; - return scriptClient.getImagePng(path) - .map(bytes -> ResponseEntity.ok() - .contentType(MediaType.IMAGE_PNG) - .cacheControl(CacheControl.maxAge(60, TimeUnit.SECONDS).cachePublic()) - .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=first-reward.png") - .body(bytes)); + String path = "/" + machineId + "/首次赏金.png"; + return scriptClient.getImagePng(path) + .flatMap(bytes -> { + // 异步保存图片到本地 + return saveImageToLocalAsync(codeNo, bytes, "firstReward.png") + .then(Mono.just(bytes)); + }) + .flatMap(this::createImageResponseMono) + .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(); + })); + }) + .onErrorResume(WebClientResponseException.class, ex -> { + log.warn("获取图片失败: path={}, status={}, error={}", path, ex.getStatusCode(), ex.getMessage()); + return Mono.just(ResponseEntity.status(ex.getStatusCode()).build()); + }); + }) + .onErrorResume(Exception.class, ex -> { + log.error("获取链接状态失败: codeNo={}, error={}", codeNo, ex.getMessage()); + return createInternalServerErrorResponseMono(); + }); } @GetMapping(value = "/image/{codeNo}/mid-reward.png", produces = MediaType.IMAGE_PNG_VALUE) @Operation(summary = "中途赏金图片代理") public Mono> midReward(@PathVariable("codeNo") String codeNo) { - // 通过codeNo查询machineId - String machineId = linkStatusService.getMechainIdByCode(codeNo); - if (machineId == null) { - log.warn("无法找到codeNo对应的machineId: {}", codeNo); - return Mono.just(ResponseEntity.notFound().build()); - } + 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); + return createNotFoundResponseMono(); + } - String path = "/" + machineId + "/中途赏金.png"; - return scriptClient.getImagePng(path) - .map(bytes -> ResponseEntity.ok() - .contentType(MediaType.IMAGE_PNG) - .cacheControl(CacheControl.maxAge(60, TimeUnit.SECONDS).cachePublic()) - .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=mid-reward.png") - .body(bytes)); + String path = "/" + machineId + "/中途赏金.png"; + return scriptClient.getImagePng(path) + .flatMap(bytes -> { + // 异步保存图片到本地 + return saveImageToLocalAsync(codeNo, bytes, "midReward.png") + .then(Mono.just(bytes)); + }) + .flatMap(this::createImageResponseMono) + .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(); + })); + }) + .onErrorResume(WebClientResponseException.class, ex -> { + log.warn("获取图片失败: path={}, status={}, error={}", path, ex.getStatusCode(), ex.getMessage()); + return Mono.just(ResponseEntity.status(ex.getStatusCode()).build()); + }); + }) + .onErrorResume(Exception.class, ex -> { + log.error("获取链接状态失败: codeNo={}, error={}", codeNo, ex.getMessage()); + return createInternalServerErrorResponseMono(); + }); } @GetMapping(value = "/image/{codeNo}/end-reward.png", produces = MediaType.IMAGE_PNG_VALUE) @Operation(summary = "结束赏金图片代理") public Mono> endReward(@PathVariable("codeNo") String codeNo) { - // 通过codeNo查询machineId - String machineId = linkStatusService.getMechainIdByCode(codeNo); - if (machineId == null) { - log.warn("无法找到codeNo对应的machineId: {}", codeNo); - return Mono.just(ResponseEntity.notFound().build()); - } + 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); + return createNotFoundResponseMono(); + } - String path = "/" + machineId + "/结束赏金.png"; - return scriptClient.getImagePng(path) - .map(bytes -> ResponseEntity.ok() - .contentType(MediaType.IMAGE_PNG) - .cacheControl(CacheControl.maxAge(60, TimeUnit.SECONDS).cachePublic()) - .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=end-reward.png") - .body(bytes)); + String path = "/" + machineId + "/结束赏金.png"; + return scriptClient.getImagePng(path) + .flatMap(bytes -> { + // 异步保存图片到本地 + return saveImageToLocalAsync(codeNo, bytes, "endReward.png") + .then(Mono.just(bytes)); + }) + .flatMap(this::createImageResponseMono) + .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(); + })); + }) + .onErrorResume(WebClientResponseException.class, ex -> { + log.warn("获取图片失败: path={}, status={}, error={}", path, ex.getStatusCode(), ex.getMessage()); + return Mono.just(ResponseEntity.status(ex.getStatusCode()).build()); + }); + }) + .onErrorResume(Exception.class, ex -> { + log.error("获取链接状态失败: codeNo={}, error={}", codeNo, ex.getMessage()); + return createInternalServerErrorResponseMono(); + }); } @GetMapping("/{codeNo}/images") @@ -232,6 +392,106 @@ public class QrProxyController { return "未知区域"; } } + + /** + * 保存图片到本地文件系统 + * @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(); + } + + /** + * 创建500响应的Mono包装 + * @return Mono> + */ + private Mono> createInternalServerErrorResponseMono() { + return createInternalServerErrorResponseMono(); + } }