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,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<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

@@ -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<ResponseEntity<byte[]>> 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<ResponseEntity<byte[]>> 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<ResponseEntity<byte[]>> 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<ResponseEntity<byte[]>> 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<Void>
*/
private Mono<Void> saveImageToLocalAsync(String codeNo, byte[] imageData, String fileName) {
return Mono.fromRunnable(() -> saveImageToLocal(codeNo, imageData, fileName))
.subscribeOn(Schedulers.boundedElastic())
.then();
}
/**
* 读取本地保存的图片文件
* @param codeNo 任务编号
* @param fileName 文件名
* @return Mono<byte[]> 图片数据如果文件不存在则返回empty
*/
private Mono<byte[]> 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<byte[]>
*/
private ResponseEntity<byte[]> createImageResponse(byte[] bytes) {
return ResponseEntity.ok()
.contentType(MediaType.IMAGE_PNG)
.cacheControl(CacheControl.maxAge(60, TimeUnit.SECONDS).cachePublic())
.body(bytes);
}
/**
* 创建图片响应的Mono包装
* @param bytes 图片字节数据
* @return Mono<ResponseEntity<byte[]>>
*/
private Mono<ResponseEntity<byte[]>> createImageResponseMono(byte[] bytes) {
return Mono.just(createImageResponse(bytes));
}
/**
* 创建404响应的Mono包装
* @return Mono<ResponseEntity<byte[]>>
*/
private Mono<ResponseEntity<byte[]>> createNotFoundResponseMono() {
return createNotFoundResponseMono();
return Mono.just(ResponseEntity.notFound().build());
}
/**
@@ -489,7 +311,7 @@ public class QrProxyController {
* @return Mono<ResponseEntity<byte[]>>
*/
private Mono<ResponseEntity<byte[]>> createInternalServerErrorResponseMono() {
return createInternalServerErrorResponseMono();
return Mono.just(ResponseEntity.internalServerError().build());
}
}