feat: 增强二维码和图片代理功能

主要修改:
1. 在QrProxyController中新增多个图片代理接口,包括首页、首次赏金、中途赏金和结束赏金图片的获取。
2. 更新LinkController中的链接状态查询逻辑,简化日志输出。
3. 在LinkStatusService中优化链接状态处理逻辑,增加对USING状态的过期检查。
4. 在ScriptClient中新增通用图片获取方法,支持从脚本端获取图片数据。
5. 更新SecurityConfig,允许公开访问二维码和游戏界面数据接口。

技术细节:
- 新增GameInterfaceResponse DTO以支持游戏界面数据的返回格式。
- 通过脚本端接口实现图片的动态获取和链接状态的自动刷新。
This commit is contained in:
zyh
2025-08-26 23:11:01 +08:00
parent 400d6757c8
commit bb4136b4ab
10 changed files with 478 additions and 217 deletions

View File

@@ -28,6 +28,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import reactor.core.publisher.Mono;
@RestController
@@ -321,8 +322,7 @@ public Mono<Boolean> deleteLink(@PathVariable("codeNo") String codeNo, Authentic
return linkStatusService.getUserLinkStatus(linkId, actualCodeNo)
.doOnSuccess(response -> {
log.info("用户端链接状态查询成功: status={}, view={}, needRefresh={}",
response.getStatus(), response.getView(), response.getNeedRefresh());
log.info("用户端链接状态查询成功: status={}", response.getStatus());
})
.doOnError(error -> {
log.error("用户端链接状态查询失败: {}", error.getMessage(), error);
@@ -407,7 +407,15 @@ public Mono<Boolean> deleteLink(@PathVariable("codeNo") String codeNo, Authentic
})
.onErrorResume(error -> {
log.error("获取设备 {} 二维码失败: {}", deviceId, error.getMessage(), error);
return Mono.just(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build());
// 如果是404错误返回404其他错误返回500
if (error instanceof WebClientResponseException.NotFound) {
log.warn("设备 {} 的二维码文件不存在返回404", deviceId);
return Mono.just(ResponseEntity.status(HttpStatus.NOT_FOUND).build());
} else {
log.error("获取设备 {} 二维码时发生系统错误", deviceId);
return Mono.just(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build());
}
});
}
}

View File

@@ -1,8 +1,16 @@
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 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;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
@@ -13,16 +21,29 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@RestController
@RequestMapping("/api/link")
@Tag(name = "二维码代理", description = "转发脚本端的二维码图片,避免混合内容")
@Tag(name = "图片代理", description = "转发脚本端的图片,避免混合内容")
public class QrProxyController {
private static final Logger log = LoggerFactory.getLogger(QrProxyController.class);
private final ScriptClient scriptClient;
private final String appBaseUrl;
private final LinkTaskMapper linkTaskMapper;
private final LinkBatchMapper linkBatchMapper;
public QrProxyController(ScriptClient scriptClient) {
public QrProxyController(ScriptClient scriptClient,
@Value("${app.base-url}") String appBaseUrl,
LinkTaskMapper linkTaskMapper,
LinkBatchMapper linkBatchMapper) {
this.scriptClient = scriptClient;
this.appBaseUrl = appBaseUrl;
this.linkTaskMapper = linkTaskMapper;
this.linkBatchMapper = linkBatchMapper;
}
@GetMapping(value = "/{codeNo}/qr.png", produces = MediaType.IMAGE_PNG_VALUE)
@@ -36,6 +57,140 @@ public class QrProxyController {
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=qr.png")
.body(bytes));
}
@GetMapping(value = "/{codeNo}/homepage.png", produces = MediaType.IMAGE_PNG_VALUE)
@Operation(summary = "首次主页图片代理")
public Mono<ResponseEntity<byte[]>> homepage(@PathVariable("codeNo") String codeNo) {
String path = "/" + codeNo + "/首次主页.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));
}
@GetMapping(value = "/{codeNo}/first-reward.png", produces = MediaType.IMAGE_PNG_VALUE)
@Operation(summary = "首次赏金图片代理")
public Mono<ResponseEntity<byte[]>> firstReward(@PathVariable("codeNo") String codeNo) {
String path = "/" + codeNo + "/首次赏金.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));
}
@GetMapping(value = "/{codeNo}/mid-reward.png", produces = MediaType.IMAGE_PNG_VALUE)
@Operation(summary = "中途赏金图片代理")
public Mono<ResponseEntity<byte[]>> midReward(@PathVariable("codeNo") String codeNo) {
String path = "/" + codeNo + "/中途赏金.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));
}
@GetMapping(value = "/{codeNo}/end-reward.png", produces = MediaType.IMAGE_PNG_VALUE)
@Operation(summary = "结束赏金图片代理")
public Mono<ResponseEntity<byte[]>> endReward(@PathVariable("codeNo") String codeNo) {
String path = "/" + codeNo + "/结束赏金.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));
}
@GetMapping("/{codeNo}/images")
@Operation(summary = "获取所有图片代理链接")
public ResponseEntity<Map<String, String>> getImageLinks(@PathVariable("codeNo") String codeNo) {
Map<String, String> imageLinks = new HashMap<>();
// 添加二维码链接
imageLinks.put("qrCode", appBaseUrl + "/api/link/" + codeNo + "/qr.png");
// 添加其他图片链接
imageLinks.put("homepage", appBaseUrl + "/api/link/" + codeNo + "/homepage.png");
imageLinks.put("firstReward", appBaseUrl + "/api/link/" + codeNo + "/first-reward.png");
imageLinks.put("midReward", appBaseUrl + "/api/link/" + codeNo + "/mid-reward.png");
imageLinks.put("endReward", appBaseUrl + "/api/link/" + codeNo + "/end-reward.png");
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(5, TimeUnit.MINUTES).cachePublic())
.body(imageLinks);
}
@GetMapping("/{codeNo}/game-interface")
@Operation(summary = "获取游戏界面数据", description = "返回四张图片链接和总点数信息")
public ResponseEntity<GameInterfaceResponse> getGameInterface(@PathVariable("codeNo") String codeNo) {
log.info("获取游戏界面数据: codeNo={}", codeNo);
try {
// 查询链接任务
LinkTask linkTask = linkTaskMapper.findByCodeNo(codeNo);
if (linkTask == null) {
log.warn("链接任务不存在: codeNo={}", codeNo);
return ResponseEntity.notFound().build();
}
// 查询批次信息
LinkBatch linkBatch = linkBatchMapper.findById(linkTask.getBatchId());
if (linkBatch == null) {
log.warn("批次信息不存在: batchId={}", linkTask.getBatchId());
return ResponseEntity.notFound().build();
}
// 构建响应对象
GameInterfaceResponse response = new GameInterfaceResponse();
response.setCodeNo(codeNo);
response.setQuantity(linkBatch.getQuantity());
response.setTimes(linkBatch.getTimes());
response.setTotalPoints(linkBatch.getQuantity() * linkBatch.getTimes());
// 设置游戏区域信息
response.setRegion(linkTask.getRegion());
response.setRegionDesc(getRegionDescription(linkTask.getRegion()));
// 设置图片链接
response.setQrCodeUrl(appBaseUrl + "/api/link/" + codeNo + "/qr.png");
response.setHomepageUrl(appBaseUrl + "/api/link/" + codeNo + "/homepage.png");
response.setFirstRewardUrl(appBaseUrl + "/api/link/" + codeNo + "/first-reward.png");
response.setMidRewardUrl(appBaseUrl + "/api/link/" + codeNo + "/mid-reward.png");
response.setEndRewardUrl(appBaseUrl + "/api/link/" + codeNo + "/end-reward.png");
log.info("游戏界面数据构建完成: codeNo={}, totalPoints={}", codeNo, response.getTotalPoints());
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(2, TimeUnit.MINUTES).cachePublic())
.body(response);
} catch (Exception e) {
log.error("获取游戏界面数据失败: codeNo={}, error={}", codeNo, e.getMessage(), e);
return ResponseEntity.internalServerError().build();
}
}
/**
* 获取区域描述
*/
private String getRegionDescription(String region) {
if (region == null) {
return "未选择区域";
}
switch (region) {
case "Q":
return "QQ区";
case "V":
return "微信区";
default:
return "未知区域";
}
}
}