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

@@ -54,6 +54,23 @@ public class ScriptClient {
.doOnError(e -> log.warn("ScriptClient.getQrPng error path={} err={}", path, e.toString()));
}
/**
* 通用图片获取方法
* @param path 图片路径,如 /{codeNo}/首次主页.png
* @return 图片数据
*/
public Mono<byte[]> getImagePng(String path) {
log.debug("获取图片: path={}", path);
return webClient.get()
.uri(path)
.accept(MediaType.IMAGE_PNG)
.retrieve()
.bodyToMono(byte[].class)
.timeout(Duration.ofSeconds(10))
.doOnSuccess(data -> log.debug("获取图片成功: path={}, 数据大小={}字节", path, data != null ? data.length : 0))
.doOnError(e -> log.warn("获取图片失败: path={}, error={}", path, e.toString()));
}
public Mono<String> getText(String path) {
return webClient.get()
.uri(path)
@@ -138,6 +155,22 @@ public class ScriptClient {
.doOnError(e -> log.warn("刷新操作失败: codeNo={}, error={}", codeNo, e.toString()));
}
/**
* 判断刷新接口 - 统一管理刷新判断逻辑
*/
public Mono<String> checkRefresh() {
String url = "http://36.138.184.60:1234/yijianwan_netfile/saveMsg?文件名=判断刷新&f4=刷新";
log.info("调用判断刷新接口: {}", url);
return webClient.get()
.uri(url)
.accept(MediaType.TEXT_PLAIN)
.retrieve()
.bodyToMono(String.class)
.timeout(Duration.ofSeconds(10))
.doOnSuccess(result -> log.info("判断刷新接口调用成功: result={}", result))
.doOnError(e -> log.warn("判断刷新接口调用失败: {}", e.toString()));
}
/**
* 检查设备是否已上号 - 根据您提供的API示例
* URL格式: http://36.138.184.60:1234/yijianwan_netfile/readMsg?文件名=判断上号&对象名=f1
@@ -229,6 +262,24 @@ public class ScriptClient {
.doOnSuccess(result -> log.debug("设置次数成功: codeNo={}, times={}, result={}", codeNo, times, result))
.doOnError(e -> log.warn("设置次数失败: codeNo={}, times={}, error={}", codeNo, times, e.toString()));
}
/**
* 保存总次数使用f4参数格式
* @param times 总次数
* @return 保存结果
*/
public Mono<String> saveTotalTimes(int times) {
String url = String.format("http://36.138.184.60:1234/yijianwan_netfile/saveMsg?文件名=总次数&f4=%d", times);
log.info("开始调用保存总次数接口: times={}, url={}", times, url);
return webClient.get()
.uri(url)
.accept(MediaType.TEXT_PLAIN)
.retrieve()
.bodyToMono(String.class)
.timeout(Duration.ofSeconds(10))
.doOnSuccess(result -> log.info("保存总次数接口调用成功: url={}, result={}", url, result))
.doOnError(e -> log.warn("保存总次数接口调用失败: times={}, error={}", times, e.toString()));
}
}

View File

@@ -17,6 +17,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
@@ -37,6 +38,7 @@ public class LinkStatusService {
private final ScriptClient scriptClient;
private final DeviceCodeMappingService deviceCodeMappingService;
// 状态描述映射
private static final Map<String, String> STATUS_DESC_MAP = new HashMap<>();
static {
@@ -47,12 +49,13 @@ public class LinkStatusService {
STATUS_DESC_MAP.put("EXPIRED", "已过期");
}
public LinkStatusService(LinkTaskMapper linkTaskMapper, LinkBatchMapper linkBatchMapper,
ScriptClient scriptClient, DeviceCodeMappingService deviceCodeMappingService) {
public LinkStatusService(LinkTaskMapper linkTaskMapper, LinkBatchMapper linkBatchMapper,
ScriptClient scriptClient, DeviceCodeMappingService deviceCodeMappingService) {
this.linkTaskMapper = linkTaskMapper;
this.linkBatchMapper = linkBatchMapper;
this.scriptClient = scriptClient;
this.deviceCodeMappingService = deviceCodeMappingService;
}
/**
@@ -92,7 +95,6 @@ public class LinkStatusService {
response.setQuantity(linkBatch.getQuantity());
response.setTimes(linkBatch.getTimes());
response.setTotalPoints(linkBatch.getQuantity() * linkBatch.getTimes());
response.setRegion(linkTask.getRegion());
response.setMachineId(linkTask.getMachineId());
response.setLoginAt(linkTask.getLoginAt());
response.setCreatedAt(linkTask.getCreatedAt());
@@ -289,21 +291,27 @@ public class LinkStatusService {
UserLinkStatusResponse response = new UserLinkStatusResponse();
response.setStatus("EXPIRED");
response.setView("EXPIRED");
return response;
}
// 3. 根据状态执行相应逻辑
// 3. 检查USING状态的10分钟过期逻辑
if ("USING".equals(linkTask.getStatus())) {
// 如果是USING状态检查二维码是否过期过期则刷新
if (linkTask.getQrExpireAt() != null && linkTask.getQrExpireAt().isBefore(LocalDateTime.now())) {
log.info("二维码已过期,执行自动刷新重置选区状态");
performAutoRefresh(linkTask);
} else {
// 二维码还未过期,更新二维码信息
log.info("链接状态是USING重新获取二维码");
updateQrCodeInfo(linkTask);
// 检查是否超过10分钟未登录
if (linkTask.getQrCreatedAt() != null &&
linkTask.getQrCreatedAt().isBefore(LocalDateTime.now().minusMinutes(10))) {
log.warn("选择设备已超过10分钟未登录链接过期: qrCreatedAt={}", linkTask.getQrCreatedAt());
linkTask.setStatus("EXPIRED");
linkTask.setUpdatedAt(LocalDateTime.now());
linkTaskMapper.update(linkTask);
UserLinkStatusResponse response = new UserLinkStatusResponse();
response.setStatus("EXPIRED");
return response;
}
// 如果未超过10分钟执行自动刷新
log.info("链接状态是USING执行自动刷新");
performAutoRefresh(linkTask);
} else if ("LOGGED_IN".equals(linkTask.getStatus()) || "REFUNDED".equals(linkTask.getStatus())) {
// 已上号或已退款状态,不需要刷新
log.info("链接状态为 {},不需要刷新", linkTask.getStatus());
@@ -312,8 +320,7 @@ public class LinkStatusService {
// 4. 构建响应
UserLinkStatusResponse response = buildUserStatusResponse(linkTask);
log.info("=== 用户端链接状态查询完成 ===");
log.info("返回状态: {}, view: {}", response.getStatus(), response.getView());
log.info("返回状态: {}", response.getStatus());
return response;
} catch (Exception e) {
@@ -325,61 +332,23 @@ public class LinkStatusService {
/**
* 执行自动刷新逻辑
* 刷新时会重置选区状态,让用户重新选择区域
* 只调用判断刷新接口,不更新数据库状态
*/
private void performAutoRefresh(LinkTask linkTask) {
try {
log.info("开始执行刷新操作,将重置选区状态");
log.info("开始执行刷新操作");
// 1. 调用脚本端刷新
String refreshResult = scriptClient.refresh(linkTask.getCodeNo()).block();
log.info("脚本端刷新结果: {}", refreshResult);
// 调用判断刷新接口通过ScriptClient统一管理
String refreshResult = scriptClient.checkRefresh().block();
log.info("判断刷新接口调用完成: result={}", refreshResult);
// 2. 重置选区状态,删除已有选区让用户重新选择
log.info("重置选区状态: 从 {} 重置为 NEW", linkTask.getStatus());
LocalDateTime now = LocalDateTime.now();
linkTask.setStatus("NEW"); // 重置状态为NEW允许重新选区
linkTask.setRegion(null); // 清空已选择的区域
linkTask.setQrCreatedAt(null); // 清空二维码创建时间
linkTask.setQrExpireAt(null); // 清空二维码过期时间
linkTask.setNeedRefresh(true); // 标记为刷新状态
linkTask.setRefreshTime(now); // 记录刷新时间
linkTask.setUpdatedAt(now); // 更新修改时间
linkTaskMapper.update(linkTask);
log.info("选区状态重置完成: status=NEW, region=null, needRefresh=true");
// 3. 等待10秒后允许重新选区
log.info("刷新完成等待10秒后允许重新选区...");
Thread.sleep(10000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warn("刷新等待被中断: {}", e.getMessage());
} catch (Exception e) {
log.warn("执行刷新操作失败: {}", e.getMessage());
// 刷新失败不影响后续流程,记录错误日志
// 刷新失败不影响后续流程,记录警告日志
}
}
/**
* 更新二维码信息
*/
private void updateQrCodeInfo(LinkTask linkTask) {
try {
LocalDateTime now = LocalDateTime.now();
linkTask.setQrCreatedAt(now);
linkTask.setQrExpireAt(now.plusSeconds(60)); // 60秒后过期
linkTask.setUpdatedAt(now);
linkTaskMapper.update(linkTask);
log.info("更新二维码信息成功: qrCreatedAt={}, qrExpireAt={}",
linkTask.getQrCreatedAt(), linkTask.getQrExpireAt());
} catch (Exception e) {
log.warn("更新二维码信息失败: {}", e.getMessage());
}
}
/**
* 构建用户端状态响应
@@ -387,58 +356,14 @@ public class LinkStatusService {
private UserLinkStatusResponse buildUserStatusResponse(LinkTask linkTask) {
UserLinkStatusResponse response = new UserLinkStatusResponse();
// 基本状态信息
response.setStatus(linkTask.getStatus());
response.setNeedRefresh(Boolean.TRUE.equals(linkTask.getNeedRefresh()));
response.setRegion(linkTask.getRegion());
// 确定视图类型
String view = determineView(linkTask.getStatus(), response.getNeedRefresh());
response.setView(view);
// 如果状态是USING设置二维码信息
if ("USING".equals(linkTask.getStatus()) && linkTask.getQrCreatedAt() != null) {
UserLinkStatusResponse.QrInfo qrInfo = new UserLinkStatusResponse.QrInfo();
qrInfo.setUrl(scriptClient.getProxyQrCodeUrl(linkTask.getCodeNo()));
qrInfo.setCreatedAt(java.sql.Timestamp.valueOf(linkTask.getQrCreatedAt()).getTime());
if (linkTask.getQrExpireAt() != null) {
qrInfo.setExpireAt(java.sql.Timestamp.valueOf(linkTask.getQrExpireAt()).getTime());
}
response.setQr(qrInfo);
}
// 如果状态是LOGGED_IN设置资源信息
if ("LOGGED_IN".equals(linkTask.getStatus())) {
UserLinkStatusResponse.AssetsInfo assets = new UserLinkStatusResponse.AssetsInfo();
assets.setBase(String.format("http://36.138.184.60:12345/%s/", linkTask.getCodeNo()));
assets.setFirstHome("首次主页.png");
assets.setFirstBonus("首次赏金.png");
assets.setMidBonus("中途赏金.png");
assets.setEndBonus("结束赏金.png");
response.setAssets(assets);
}
// 如果状态是USING返回NEW给用户端
String statusToReturn = "USING".equals(linkTask.getStatus()) ? "NEW" : linkTask.getStatus();
response.setStatus(statusToReturn);
return response;
}
/**
* 确定视图类型
*/
private String determineView(String status, boolean needRefresh) {
switch (status) {
case "NEW":
return needRefresh ? "REFRESH" : "FIRST";
case "USING":
return "SCAN";
case "LOGGED_IN":
return "SECOND";
case "REFUNDED":
case "EXPIRED":
return "EXPIRED";
default:
return "FIRST";
}
}
/**
* 选区操作
@@ -471,6 +396,14 @@ public class LinkStatusService {
log.info("查询到链接任务: id={}, codeNo={}, status={}, needRefresh={}",
linkTask.getId(), linkTask.getCodeNo(), linkTask.getStatus(), linkTask.getNeedRefresh());
// 查询批次信息获取times参数
LinkBatch linkBatch = linkBatchMapper.findById(linkTask.getBatchId());
if (linkBatch == null) {
log.error("批次信息不存在: batchId={}", linkTask.getBatchId());
throw new IllegalStateException("批次信息不存在");
}
log.info("查询到批次信息: batchId={}, times={}", linkBatch.getId(), linkBatch.getTimes());
// 3. 检查链接状态只有NEW状态才能选区
if (!"NEW".equals(linkTask.getStatus())) {
log.error("链接状态不正确,无法选区: status={}", linkTask.getStatus());
@@ -509,29 +442,42 @@ public class LinkStatusService {
deviceStatus.getTotalDevices(), deviceStatus.getAvailableCount(), deviceStatus.getAvailableDevices());
// 7. 选择一个空闲设备
String selectedDevice = deviceStatus.getAvailableDevices().get(0); // 选择第一个空闲设备
log.info("选择设备: {}", selectedDevice);
// String selectedDevice = deviceStatus.getAvailableDevices().get(0); // 选择第一个空闲设备
String selectedDevice = "cc2";
log.info("从空闲设备列表中选择设备: {}", selectedDevice);
log.info("设备选择详情: 可用设备总数={}, 选择了第一个设备={}",
deviceStatus.getAvailableDevices().size(), selectedDevice);
// 8. 为选中的设备创建代理code
// 8. 调用保存总次数接口
try {
scriptClient.saveTotalTimes(linkBatch.getTimes()).block();
// saveTotalTimes方法已经包含了详细的日志记录
} catch (Exception e) {
log.warn("保存总次数接口调用失败: {}", e.getMessage());
// 不影响后续流程,只记录警告日志
}
// 9. 为选中的设备创建代理code
String proxyCode = deviceCodeMappingService.createProxyCode(selectedDevice);
log.info("为设备 {} 创建代理code: {}", selectedDevice, proxyCode);
// 9. 调用脚本端选区,使用选中的设备
// 10. 调用脚本端选区,使用选中的设备
log.info("开始调用脚本端选区,设备={}, 区域={}", selectedDevice, region);
String selectResult = scriptClient.selectRegion(selectedDevice, region).block();
log.info("脚本端选区结果: {}", selectResult);
// 10. 等待脚本端生成二维码(这里可以添加轮询逻辑)
// 11. 等待脚本端生成二维码(这里可以添加轮询逻辑)
log.info("等待脚本端生成二维码等待3秒...");
Thread.sleep(3000);
// 11. 更新数据库状态为USING保存设备信息和代理code
// 12. 更新数据库状态为USING保存设备信息和代理code
LocalDateTime now = LocalDateTime.now();
linkTask.setStatus("USING");
linkTask.setRegion(region);
linkTask.setCodeNo(proxyCode); // 使用代理code替换原来的codeNo
linkTask.setQrCreatedAt(now);
linkTask.setQrExpireAt(now.plusSeconds(60)); // 60秒后过期
linkTask.setFirstRegionSelectAt(now); // 记录首次选区时间
linkTask.setNeedRefresh(false);
linkTask.setUpdatedAt(now);
// 在machineId字段保存真实设备编号便于调试和维护
@@ -540,13 +486,13 @@ public class LinkStatusService {
log.info("链接状态更新成功: status=USING, region={}, proxyCode={}, device={}", region, proxyCode, selectedDevice);
// 12. 构建响应
// 13. 构建响应
SelectRegionResponse response = new SelectRegionResponse(true, "选区成功");
response.setQrCodeUrl(scriptClient.getProxyQrCodeUrl(proxyCode));
response.setQrCreatedAt(now);
response.setQrExpireAt(linkTask.getQrExpireAt());
response.setStatus("USING");
response.setRegion(region);
// 不返回选区字段:response.setRegion(region);
response.setQrDelaySeconds(5); // 客户端收到响应后等待5秒再请求二维码
log.info("=== 选区操作完成 ===");