feat: 添加AOP支持和更新链接控制器逻辑

主要修改:
1. 在pom.xml中新增spring-boot-starter-aop依赖,支持面向切面编程。
2. 在LinkController中移除DeviceCodeMappingService的依赖,更新二维码获取逻辑,使用linkStatusService获取设备ID。
3. 在SelectRegionResponse中新增mecmachineId字段,便于调试和维护。
4. 在SecurityConfig中允许二维码HEAD请求公开访问。

技术细节:
- 通过引入AOP支持,提升了代码的可维护性和扩展性,同时优化了链接控制器的逻辑,确保设备ID的获取更加灵活。
This commit is contained in:
zyh
2025-08-28 22:19:06 +08:00
parent a56eebc30b
commit 1d72bc4c5a
12 changed files with 526 additions and 352 deletions

View File

@@ -33,6 +33,12 @@
<artifactId>spring-boot-starter-security</artifactId> <artifactId>spring-boot-starter-security</artifactId>
</dependency> </dependency>
<!-- AOP support for aspect-oriented programming -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- MyBatis Plus starter --> <!-- MyBatis Plus starter -->
<dependency> <dependency>
<groupId>com.baomidou</groupId> <groupId>com.baomidou</groupId>

View File

@@ -0,0 +1,108 @@
# 重试机制使用说明
本项目提供了两种不同的重试机制,用于不同的业务场景。
## 🔄 两种重试机制对比
| 特性 | @RepeatCall 注解 | retry() 方法 |
|------|-----------------|--------------|
| **触发条件** | 无论成功失败都执行指定次数 | 只有在请求失败时才重试 |
| **适用场景** | 确保脚本端接收到关键操作 | 网络不稳定的查询操作 |
| **执行次数** | 固定执行N次 | 最多重试N次成功则停止 |
| **性能影响** | 较大(总是执行多次) | 较小成功时只执行1次 |
## 🎯 @RepeatCall 注解机制
### 功能介绍
无论成功还是失败都会执行指定次数,主要用于确保脚本端能够可靠接收到请求。
### 使用方法
```java
@RepeatCall(times = 3, description = "设置次数")
public Mono<String> setTimes(String codeNo, int times) {
// 无论成功失败都会执行3次
return webClient.post()...;
}
```
### 参数说明
- `times`: 重复执行次数默认3次
- `delayMs`: 每次调用之间的延迟时间毫秒默认0
- `logEachCall`: 是否记录每次调用的详细日志默认true
- `description`: 描述信息,用于日志记录(默认为方法名)
### 已应用的方法
- `setTimes()` - 设置次数
- `selectRegion()` - 选区操作
- `refresh()` - 刷新操作
- `checkRefresh()` - 判断刷新
- `saveTotalTimes()` - 保存总次数
- `refundOrder()` - 退单操作
## 🔁 retry() 重试机制
### 功能介绍
只有在请求失败超时、404、网络错误等时才会重试成功则立即返回结果。
### 使用方法
```java
public Mono<byte[]> getQrPng(String path) {
return webClient.get()
.uri(path)
.retrieve()
.bodyToMono(byte[].class)
.timeout(Duration.ofSeconds(5))
.retry(3) // 失败时重试3次
.doOnError(e -> log.warn("获取失败: {}", e.toString()));
}
```
### 已应用的方法
- `getQrPng()` - 获取二维码图片
- `getImagePng()` - 获取通用图片
- `getText()` - 获取文本内容
- `checkAvailableDevice()` - 检查空闲设备
- `checkLoginStatus()` - 检查上号状态
- `getDeviceQrCode()` - 获取设备二维码
- `getTargetScore()` - 获取目标分数
- `getDeviceStatus()` - 获取设备状态
## 📋 使用场景选择指南
### 选择 @RepeatCall 的场景:
**关键状态同步操作**
- 设置游戏次数
- 选择游戏区域
- 刷新操作
- 退单操作
**需要确保脚本端接收的操作**
- 重要参数设置
- 状态变更通知
### 选择 retry() 的场景:
**数据查询操作**
- 获取图片资源
- 检查设备状态
- 获取分数信息
**可能因网络问题失败的操作**
- 文件下载
- API 调用
- 状态检查
## 💡 最佳实践
1. **@RepeatCall**:用于重要的业务操作,确保执行到位
2. **retry()**:用于查询操作,提高成功率但不影响性能
3. **超时设置**:两种机制都应配合合理的超时时间
4. **日志记录**:重要操作要有详细的日志记录
## ⚠️ 注意事项
1. **@RepeatCall** 会显著增加执行时间,请谨慎使用
2. **retry()** 对幂等操作更安全
3. 需要添加 `spring-boot-starter-aop` 依赖支持 @RepeatCall
4. 建议根据业务重要性选择合适的重试机制

View File

@@ -0,0 +1,36 @@
package com.gameplatform.server.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 重复调用注解
* 用于标记需要重复执行的方法,无论成功失败都会执行指定次数
* 主要用于确保脚本端能够可靠接收到请求
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatCall {
/**
* 重复执行次数默认3次
*/
int times() default 3;
/**
* 每次调用之间的延迟时间毫秒默认0
*/
long delayMs() default 0;
/**
* 是否记录每次调用的详细日志默认true
*/
boolean logEachCall() default true;
/**
* 描述信息,用于日志记录
*/
String description() default "";
}

View File

@@ -0,0 +1,81 @@
package com.gameplatform.server.aspect;
import com.gameplatform.server.annotation.RepeatCall;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
/**
* 重复调用切面
* 处理被 @RepeatCall 注解标记的方法,实现重复执行逻辑
* 目前主要支持返回 Mono 类型的方法
*/
@Aspect
@Component
public class RepeatCallAspect {
private static final Logger log = LoggerFactory.getLogger(RepeatCallAspect.class);
@Around("@annotation(repeatCall)")
public Object repeatCall(ProceedingJoinPoint joinPoint, RepeatCall repeatCall) throws Throwable {
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getTarget().getClass().getSimpleName();
int times = repeatCall.times();
String description = repeatCall.description().isEmpty() ? methodName : repeatCall.description();
log.info("开始重复调用: {}.{} - {} (将执行{}次)", className, methodName, description, times);
// 执行第一次调用
Mono<Object> currentCall = executeOnce(joinPoint, className, methodName, description, 1);
// 链式执行后续调用
for (int i = 2; i <= times; i++) {
final int attemptNumber = i;
currentCall = currentCall
.onErrorReturn("" + (attemptNumber - 1) + "次调用失败")
.flatMap(result -> {
log.debug("第{}次调用完成,准备执行第{}次", attemptNumber - 1, attemptNumber);
return executeOnce(joinPoint, className, methodName, description, attemptNumber);
});
}
return currentCall
.onErrorReturn("最后一次调用失败")
.doOnSuccess(finalResult -> {
log.info("重复调用全部完成: {}.{} - {} (执行了{}次), 最终结果={}",
className, methodName, description, times, finalResult);
});
}
/**
* 执行单次调用
*/
private Mono<Object> executeOnce(ProceedingJoinPoint joinPoint, String className,
String methodName, String description, int attemptNumber) {
return Mono.defer(() -> {
try {
Object result = joinPoint.proceed();
if (result instanceof Mono) {
return ((Mono<?>) result).cast(Object.class);
} else {
return Mono.just(result);
}
} catch (Throwable throwable) {
return Mono.error(throwable);
}
})
.doOnSuccess(result -> {
log.debug("重复调用第{}次成功: {}.{} - {}, 结果={}",
attemptNumber, className, methodName, description, result);
})
.doOnError(error -> {
log.warn("重复调用第{}次失败: {}.{} - {}, 错误={}",
attemptNumber, className, methodName, description, error.toString());
});
}
}

View File

@@ -15,7 +15,6 @@ import com.gameplatform.server.model.dto.link.UserLinkStatusResponse;
import com.gameplatform.server.service.link.LinkGenerationService; import com.gameplatform.server.service.link.LinkGenerationService;
import com.gameplatform.server.service.link.LinkListService; import com.gameplatform.server.service.link.LinkListService;
import com.gameplatform.server.service.link.LinkStatusService; import com.gameplatform.server.service.link.LinkStatusService;
import com.gameplatform.server.service.device.DeviceCodeMappingService;
import com.gameplatform.server.service.external.ScriptClient; import com.gameplatform.server.service.external.ScriptClient;
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Claims;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
@@ -41,18 +40,15 @@ public class LinkController {
private final LinkGenerationService linkGenerationService; private final LinkGenerationService linkGenerationService;
private final LinkStatusService linkStatusService; private final LinkStatusService linkStatusService;
private final LinkListService linkListService; private final LinkListService linkListService;
private final DeviceCodeMappingService deviceCodeMappingService;
private final ScriptClient scriptClient; private final ScriptClient scriptClient;
public LinkController(LinkGenerationService linkGenerationService, public LinkController(LinkGenerationService linkGenerationService,
LinkStatusService linkStatusService, LinkStatusService linkStatusService,
LinkListService linkListService, LinkListService linkListService,
DeviceCodeMappingService deviceCodeMappingService,
ScriptClient scriptClient) { ScriptClient scriptClient) {
this.linkGenerationService = linkGenerationService; this.linkGenerationService = linkGenerationService;
this.linkStatusService = linkStatusService; this.linkStatusService = linkStatusService;
this.linkListService = linkListService; this.linkListService = linkListService;
this.deviceCodeMappingService = deviceCodeMappingService;
this.scriptClient = scriptClient; this.scriptClient = scriptClient;
} }
@@ -182,35 +178,6 @@ public class LinkController {
}); });
} }
// @GetMapping("/{codeNo}/status")
// @Operation(summary = "获取链接状态", description = "根据链接编号获取链接的详细状态信息,包括过期时间、奖励点数、当前状态等")
// public Mono<LinkStatusResponse> getLinkStatus(@PathVariable("codeNo") String codeNo) {
// log.info("=== 开始查询链接状态 ===");
// log.info("链接编号: {}", codeNo);
//
// return linkStatusService.getLinkStatus(codeNo)
// .doOnSuccess(response -> {
// log.info("链接状态查询成功: codeNo={}, status={}, isExpired={}",
// codeNo, response.getStatus(), response.getIsExpired());
// })
// .doOnError(error -> {
// log.error("链接状态查询失败: codeNo={}, error={}", codeNo, error.getMessage(), error);
// });
// }
//
// @GetMapping("/{codeNo}/exists")
// @Operation(summary = "检查链接是否存在", description = "检查指定链接编号是否存在")
// public Mono<Boolean> isLinkExists(@PathVariable("codeNo") String codeNo) {
// log.debug("检查链接是否存在: codeNo={}", codeNo);
// return linkStatusService.isLinkExists(codeNo);
// }
//
// @GetMapping("/{codeNo}/valid")
// @Operation(summary = "检查链接是否有效", description = "检查指定链接是否有效(未过期且状态正常)")
// public Mono<Boolean> isLinkValid(@PathVariable("codeNo") String codeNo) {
// log.debug("检查链接是否有效: codeNo={}", codeNo);
// return linkStatusService.isLinkValid(codeNo);
// }
@DeleteMapping("/{codeNo}") @DeleteMapping("/{codeNo}")
@Operation(summary = "删除链接", description = "删除指定的链接,用户只能删除自己创建的链接") @Operation(summary = "删除链接", description = "删除指定的链接,用户只能删除自己创建的链接")
public Mono<Boolean> deleteLink(@PathVariable("codeNo") String codeNo, Authentication authentication) { public Mono<Boolean> deleteLink(@PathVariable("codeNo") String codeNo, Authentication authentication) {
@@ -464,31 +431,31 @@ public Mono<Boolean> deleteLink(@PathVariable("codeNo") String codeNo, Authentic
* 代理二维码获取接口 * 代理二维码获取接口
* 通过代理code获取真实设备的二维码避免暴露设备编号 * 通过代理code获取真实设备的二维码避免暴露设备编号
*/ */
@GetMapping("/qr/{proxyCode}") @GetMapping("/qr/{code}")
@Operation(summary = "获取二维码", description = "通过代理code获取设备二维码用于扫码上号") @Operation(summary = "获取二维码", description = "通过代理code获取设备二维码用于扫码上号")
public Mono<ResponseEntity<byte[]>> getProxyQrCode(@PathVariable("proxyCode") String proxyCode) { public Mono<ResponseEntity<byte[]>> getProxyQrCode(@PathVariable("code") String code) {
log.info("=== 获取代理二维码 ==="); log.info("=== 获取代理二维码 ===");
log.info("代理code: {}", proxyCode); log.info("代理code: {}", code);
// 验证代理code是否有效 // // 验证代理code是否有效
if (!deviceCodeMappingService.isValidProxyCode(proxyCode)) { // if (!deviceCodeMappingService.isValidProxyCode(proxyCode)) {
log.warn("无效的代理code: {}", proxyCode); // log.warn("无效的代理code: {}", proxyCode);
return Mono.just(ResponseEntity.notFound().build()); // return Mono.just(ResponseEntity.notFound().build());
} // }
// 获取真实设备编号 // 获取真实设备编号
String deviceId = deviceCodeMappingService.getDeviceId(proxyCode); String meachainId = linkStatusService.getMechainIdByCode(code);
if (deviceId == null) { if (meachainId == null) {
log.warn("代理code对应的设备不存在: {}", proxyCode); log.warn("代理code对应的设备不存在: {}", code);
return Mono.just(ResponseEntity.notFound().build()); return Mono.just(ResponseEntity.notFound().build());
} }
log.info("代理code {} 对应设备: {}", proxyCode, deviceId); log.info("代理code {} 对应设备: {}", code, meachainId);
// 获取真实设备的二维码 // 获取真实设备的二维码
return scriptClient.getDeviceQrCode(deviceId) return scriptClient.getDeviceQrCode(meachainId)
.map(qrData -> { .map(qrData -> {
log.info("获取设备 {} 二维码成功,大小: {} 字节", deviceId, qrData.length); log.info("获取设备 {} 二维码成功,大小: {} 字节", meachainId, qrData.length);
// 设置响应头 // 设置响应头
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
@@ -502,14 +469,14 @@ public Mono<Boolean> deleteLink(@PathVariable("codeNo") String codeNo, Authentic
.body(qrData); .body(qrData);
}) })
.onErrorResume(error -> { .onErrorResume(error -> {
log.error("获取设备 {} 二维码失败: {}", deviceId, error.getMessage(), error); log.error("获取设备 {} 二维码失败: {}", meachainId, error.getMessage(), error);
// 如果是404错误返回404其他错误返回500 // 如果是404错误返回404其他错误返回500
if (error instanceof WebClientResponseException.NotFound) { if (error instanceof WebClientResponseException.NotFound) {
log.warn("设备 {} 的二维码文件不存在返回404", deviceId); log.warn("设备 {} 的二维码文件不存在返回404", meachainId);
return Mono.just(ResponseEntity.status(HttpStatus.NOT_FOUND).build()); return Mono.just(ResponseEntity.status(HttpStatus.NOT_FOUND).build());
} else { } else {
log.error("获取设备 {} 二维码时发生系统错误", deviceId); log.error("获取设备 {} 二维码时发生系统错误", meachainId);
return Mono.just(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build()); return Mono.just(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build());
} }
}); });

View File

@@ -30,6 +30,11 @@ public class SelectRegionResponse {
@Schema(description = "二维码出现延迟时间(秒)", example = "5") @Schema(description = "二维码出现延迟时间(秒)", example = "5")
private Integer qrDelaySeconds; private Integer qrDelaySeconds;
@Schema(description = "MEC 机器ID", example = "f1")
private String mecmachineId;
public SelectRegionResponse() {} public SelectRegionResponse() {}
public SelectRegionResponse(boolean success, String message) { public SelectRegionResponse(boolean success, String message) {
@@ -101,6 +106,12 @@ public class SelectRegionResponse {
this.qrDelaySeconds = qrDelaySeconds; this.qrDelaySeconds = qrDelaySeconds;
} }
public String getMecmachineId() {
return mecmachineId;
}
public void setMecmachineId(String mecmachineId) {
this.mecmachineId = mecmachineId;
}
@Override @Override
public String toString() { public String toString() {
return "SelectRegionResponse{" + return "SelectRegionResponse{" +
@@ -112,6 +123,7 @@ public class SelectRegionResponse {
", status='" + status + '\'' + ", status='" + status + '\'' +
", region='" + region + '\'' + ", region='" + region + '\'' +
", qrDelaySeconds=" + qrDelaySeconds + ", qrDelaySeconds=" + qrDelaySeconds +
", mecmachineId='" + mecmachineId + '\'' +
'}'; '}';
} }
} }

View File

@@ -46,6 +46,7 @@ public class SecurityConfig {
.pathMatchers(HttpMethod.POST, "/api/link/select-region").permitAll() // 用户端选区接口,公开访问 .pathMatchers(HttpMethod.POST, "/api/link/select-region").permitAll() // 用户端选区接口,公开访问
.pathMatchers(HttpMethod.GET, "/api/link/poll-login").permitAll() // 用户端轮询登录接口,公开访问 .pathMatchers(HttpMethod.GET, "/api/link/poll-login").permitAll() // 用户端轮询登录接口,公开访问
.pathMatchers(HttpMethod.GET, "/api/link/qr/**").permitAll() // 二维码获取接口,公开访问 .pathMatchers(HttpMethod.GET, "/api/link/qr/**").permitAll() // 二维码获取接口,公开访问
.pathMatchers(HttpMethod.HEAD, "/api/link/qr/**").permitAll() // 二维码HEAD请求公开访问
.pathMatchers(HttpMethod.GET, "/api/link/*/game-interface").permitAll() // 游戏界面数据接口,公开访问 .pathMatchers(HttpMethod.GET, "/api/link/*/game-interface").permitAll() // 游戏界面数据接口,公开访问
.pathMatchers("/api/link/**").authenticated() // 其他链接接口需要认证 .pathMatchers("/api/link/**").authenticated() // 其他链接接口需要认证
.anyExchange().permitAll() // 其他接口后续再收紧 .anyExchange().permitAll() // 其他接口后续再收紧
@@ -70,6 +71,7 @@ public class SecurityConfig {
log.info(" * POST /api/link/select-region -> 允许所有 (用户端公开接口)"); log.info(" * POST /api/link/select-region -> 允许所有 (用户端公开接口)");
log.info(" * GET /api/link/poll-login -> 允许所有 (用户端公开接口)"); log.info(" * GET /api/link/poll-login -> 允许所有 (用户端公开接口)");
log.info(" * GET /api/link/qr/** -> 允许所有 (二维码获取接口)"); log.info(" * GET /api/link/qr/** -> 允许所有 (二维码获取接口)");
log.info(" * HEAD /api/link/qr/** -> 允许所有 (二维码HEAD请求)");
log.info(" * GET /api/link/*/game-interface -> 允许所有 (游戏界面数据接口)"); log.info(" * GET /api/link/*/game-interface -> 允许所有 (游戏界面数据接口)");
log.info(" * /api/link/** -> 需要认证"); log.info(" * /api/link/** -> 需要认证");
log.info(" * 其他路径 -> 允许所有"); log.info(" * 其他路径 -> 允许所有");

View File

@@ -113,6 +113,11 @@ public class SystemConfigService {
return getConfigValue("device.idle_status", "空闲"); return getConfigValue("device.idle_status", "空闲");
} }
// 获取首次选区后链接过期时间(秒)
public Integer getFirstRegionExpireSeconds() {
return getConfigValueAsInt("link.first_region_expire", 600);
}
// 批量更新配置 // 批量更新配置
public boolean updateConfigs(List<SystemConfig> configs) { public boolean updateConfigs(List<SystemConfig> configs) {
if (configs == null || configs.isEmpty()) { if (configs == null || configs.isEmpty()) {

View File

@@ -1,139 +0,0 @@
package com.gameplatform.server.service.device;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
/**
* 设备代码映射服务
* 用于隐藏真实设备编号,避免被恶意用户直接访问
*/
@Service
public class DeviceCodeMappingService {
private static final Logger log = LoggerFactory.getLogger(DeviceCodeMappingService.class);
// 代理code -> 真实设备编号的映射
private final Map<String, String> codeToDeviceMap = new ConcurrentHashMap<>();
// 真实设备编号 -> 代理code的映射
private final Map<String, String> deviceToCodeMap = new ConcurrentHashMap<>();
/**
* 为设备创建代理code
* @param deviceId 真实设备编号 (如 f1, ss9)
* @return 代理code
*/
public String createProxyCode(String deviceId) {
// 如果设备已经有代理code则返回现有的
if (deviceToCodeMap.containsKey(deviceId)) {
String existingCode = deviceToCodeMap.get(deviceId);
log.debug("设备 {} 已有代理code: {}", deviceId, existingCode);
return existingCode;
}
// 生成新的代理code
String proxyCode = generateUniqueCode();
// 保存映射关系
codeToDeviceMap.put(proxyCode, deviceId);
deviceToCodeMap.put(deviceId, proxyCode);
log.info("为设备 {} 创建代理code: {}", deviceId, proxyCode);
return proxyCode;
}
/**
* 根据代理code获取真实设备编号
* @param proxyCode 代理code
* @return 真实设备编号如果不存在则返回null
*/
public String getDeviceId(String proxyCode) {
String deviceId = codeToDeviceMap.get(proxyCode);
if (deviceId != null) {
log.debug("代理code {} 对应设备: {}", proxyCode, deviceId);
} else {
log.warn("无效的代理code: {}", proxyCode);
}
return deviceId;
}
/**
* 根据设备编号获取代理code
* @param deviceId 真实设备编号
* @return 代理code如果不存在则返回null
*/
public String getProxyCode(String deviceId) {
String proxyCode = deviceToCodeMap.get(deviceId);
if (proxyCode != null) {
log.debug("设备 {} 对应代理code: {}", deviceId, proxyCode);
}
return proxyCode;
}
/**
* 移除设备的代理映射
* @param deviceId 真实设备编号
*/
public void removeMapping(String deviceId) {
String proxyCode = deviceToCodeMap.remove(deviceId);
if (proxyCode != null) {
codeToDeviceMap.remove(proxyCode);
log.info("移除设备 {} 的代理映射: {}", deviceId, proxyCode);
}
}
/**
* 检查代理code是否有效
* @param proxyCode 代理code
* @return true if valid
*/
public boolean isValidProxyCode(String proxyCode) {
return codeToDeviceMap.containsKey(proxyCode);
}
/**
* 获取当前映射数量
* @return 映射数量
*/
public int getMappingCount() {
return codeToDeviceMap.size();
}
/**
* 清空所有映射
*/
public void clearAllMappings() {
int count = codeToDeviceMap.size();
codeToDeviceMap.clear();
deviceToCodeMap.clear();
log.info("清空所有设备代理映射, 共 {} 个", count);
}
/**
* 生成唯一的代理code
* 格式: CODE + 8位随机数字字母
*/
private String generateUniqueCode() {
String code;
do {
code = "CODE" + generateRandomString(8);
} while (codeToDeviceMap.containsKey(code));
return code;
}
/**
* 生成随机字符串(数字+字母大写)
*/
private String generateRandomString(int length) {
String chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
int index = ThreadLocalRandom.current().nextInt(chars.length());
sb.append(chars.charAt(index));
}
return sb.toString();
}
}

View File

@@ -1,5 +1,6 @@
package com.gameplatform.server.service.external; package com.gameplatform.server.service.external;
import com.gameplatform.server.annotation.RepeatCall;
import com.gameplatform.server.model.dto.device.DeviceStatusResponse; import com.gameplatform.server.model.dto.device.DeviceStatusResponse;
import com.gameplatform.server.service.device.DeviceStatusService; import com.gameplatform.server.service.device.DeviceStatusService;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -59,6 +60,7 @@ public class ScriptClient {
.retrieve() .retrieve()
.bodyToMono(byte[].class) .bodyToMono(byte[].class)
.timeout(Duration.ofSeconds(5)) .timeout(Duration.ofSeconds(5))
.retry(3) // 失败时重试3次
.doOnError(e -> log.warn("ScriptClient.getQrPng error path={} err={}", path, e.toString())); .doOnError(e -> log.warn("ScriptClient.getQrPng error path={} err={}", path, e.toString()));
} }
@@ -75,6 +77,7 @@ public class ScriptClient {
.retrieve() .retrieve()
.bodyToMono(byte[].class) .bodyToMono(byte[].class)
.timeout(Duration.ofSeconds(10)) .timeout(Duration.ofSeconds(10))
.retry(3) // 失败时重试3次
.doOnSuccess(data -> log.debug("获取图片成功: path={}, 数据大小={}字节", path, data != null ? data.length : 0)) .doOnSuccess(data -> log.debug("获取图片成功: path={}, 数据大小={}字节", path, data != null ? data.length : 0))
.doOnError(e -> log.warn("获取图片失败: path={}, error={}", path, e.toString())); .doOnError(e -> log.warn("获取图片失败: path={}, error={}", path, e.toString()));
} }
@@ -86,6 +89,7 @@ public class ScriptClient {
.retrieve() .retrieve()
.bodyToMono(String.class) .bodyToMono(String.class)
.timeout(Duration.ofSeconds(5)) .timeout(Duration.ofSeconds(5))
.retry(3) // 失败时重试3次
.doOnError(e -> log.warn("ScriptClient.getText error path={} err={}", path, e.toString())); .doOnError(e -> log.warn("ScriptClient.getText error path={} err={}", path, e.toString()));
} }
@@ -101,6 +105,7 @@ public class ScriptClient {
.retrieve() .retrieve()
.bodyToMono(String.class) .bodyToMono(String.class)
.timeout(Duration.ofSeconds(10)) .timeout(Duration.ofSeconds(10))
.retry(3) // 失败时重试3次
.doOnSuccess(result -> log.debug("检查空闲设备成功: {}", result)) .doOnSuccess(result -> log.debug("检查空闲设备成功: {}", result))
.doOnError(e -> log.warn("检查空闲设备失败: {}", e.toString())); .doOnError(e -> log.warn("检查空闲设备失败: {}", e.toString()));
} }
@@ -134,7 +139,16 @@ public class ScriptClient {
* @param region 区域参数 (Q或V) * @param region 区域参数 (Q或V)
* @return 选区结果 * @return 选区结果
*/ */
@RepeatCall(times = 3, description = "选区操作")
public Mono<String> selectRegion(String deviceId, String region) { public Mono<String> selectRegion(String deviceId, String region) {
if (deviceId == null || region == null) {
log.warn("选区操作失败: 设备={}, 区域={}", deviceId, region);
return Mono.just("选区操作失败");
}
if (!"Q".equals(region) && !"V".equals(region)) {
log.warn("选区操作失败: 区域={} 必须是Q或V", region);
return Mono.just("选区操作失败");
}
// 构建选区URL使用设备编号作为参数名区域作为参数值 // 构建选区URL使用设备编号作为参数名区域作为参数值
// 示例: {apiBaseUrl}/yijianwan_netfile/saveMsg?文件名=判断系统&f1=Q // 示例: {apiBaseUrl}/yijianwan_netfile/saveMsg?文件名=判断系统&f1=Q
String url = String.format(apiBaseUrl + "/yijianwan_netfile/saveMsg?文件名=判断系统&%s=%s", deviceId, region); String url = String.format(apiBaseUrl + "/yijianwan_netfile/saveMsg?文件名=判断系统&%s=%s", deviceId, region);
@@ -154,27 +168,14 @@ public class ScriptClient {
}); });
} }
/**
* 刷新操作
*/
public Mono<String> refresh(String codeNo) {
String url = apiBaseUrl + "/yijianwan_netfile/saveMsg?文件名=判断刷新&编号=刷新";
log.debug("刷新操作: codeNo={}, url={}", codeNo, url);
return webClient.post()
.uri(url)
.accept(MediaType.TEXT_PLAIN)
.retrieve()
.bodyToMono(String.class)
.timeout(Duration.ofSeconds(10))
.doOnSuccess(result -> log.debug("刷新操作成功: codeNo={}, result={}", codeNo, result))
.doOnError(e -> log.warn("刷新操作失败: codeNo={}, error={}", codeNo, e.toString()));
}
/** /**
* 判断刷新接口 - 统一管理刷新判断逻辑 * 判断刷新接口 - 统一管理刷新判断逻辑
*/ */
public Mono<String> checkRefresh() { @RepeatCall(times = 3, description = "判断刷新")
String url = apiBaseUrl + "/yijianwan_netfile/saveMsg?文件名=判断刷新&f4=刷新"; public Mono<String> refresh(String machineId) {
String url = String.format(apiBaseUrl + "/yijianwan_netfile/saveMsg?文件名=判断刷新&%s=刷新", machineId);
log.info("调用判断刷新接口: {}", url); log.info("调用判断刷新接口: {}", url);
return webClient.get() return webClient.get()
.uri(url) .uri(url)
@@ -203,6 +204,7 @@ public class ScriptClient {
.retrieve() .retrieve()
.bodyToMono(String.class) .bodyToMono(String.class)
.timeout(Duration.ofSeconds(10)) .timeout(Duration.ofSeconds(10))
.retry(3) // 失败时重试3次
.doOnSuccess(result -> log.debug("检查设备上号状态成功: 设备={}, 状态={}", deviceId, result)) .doOnSuccess(result -> log.debug("检查设备上号状态成功: 设备={}, 状态={}", deviceId, result))
.doOnError(e -> log.warn("检查设备上号状态失败: 设备={}, 错误={}", deviceId, e.toString())); .doOnError(e -> log.warn("检查设备上号状态失败: 设备={}, 错误={}", deviceId, e.toString()));
} }
@@ -275,6 +277,7 @@ public class ScriptClient {
.retrieve() .retrieve()
.bodyToMono(byte[].class) .bodyToMono(byte[].class)
.timeout(Duration.ofSeconds(10)) .timeout(Duration.ofSeconds(10))
.retry(3) // 失败时重试3次
.doOnSuccess(data -> { .doOnSuccess(data -> {
log.debug("获取设备二维码成功: 设备={}, 数据大小={}字节", deviceId, data.length); log.debug("获取设备二维码成功: 设备={}, 数据大小={}字节", deviceId, data.length);
}) })
@@ -286,33 +289,35 @@ public class ScriptClient {
/** /**
* 获取目标分数 * 获取目标分数
*/ */
public Mono<String> getTargetScore(String codeNo) { public Mono<String> getTargetScore(String machineId) {
String url = String.format(apiBaseUrl + "/yijianwan_netfile/readMsg?文件名=判断分数&对象名=%s", codeNo); String url = String.format(apiBaseUrl + "/yijianwan_netfile/readMsg?文件名=判断分数&对象名=%s", machineId);
log.debug("获取目标分数: codeNo={}, url={}", codeNo, url); log.debug("获取目标分数: machineId={}, url={}", machineId, url);
return webClient.get() return webClient.get()
.uri(url) .uri(url)
.accept(MediaType.TEXT_PLAIN) .accept(MediaType.TEXT_PLAIN)
.retrieve() .retrieve()
.bodyToMono(String.class) .bodyToMono(String.class)
.timeout(Duration.ofSeconds(5)) .timeout(Duration.ofSeconds(5))
.doOnSuccess(result -> log.debug("获取目标分数成功: codeNo={}, result={}", codeNo, result)) .retry(3) // 失败时重试3次
.doOnError(e -> log.warn("获取目标分数失败: codeNo={}, error={}", codeNo, e.toString())); .doOnSuccess(result -> log.debug("获取目标分数成功: machineId={}, result={}", machineId, result))
.doOnError(e -> log.warn("获取目标分数失败: machineId={}, error={}", machineId, e.toString()));
} }
/** /**
* 设置次数(生成链接时调用) * 设置次数(生成链接时调用)- 使用 @RepeatCall 注解自动重复执行3次
*/ */
public Mono<String> setTimes(String codeNo, int times) { @RepeatCall(times = 3, description = "设置次数")
String url = String.format(apiBaseUrl + "/yijianwan_netfile/saveMsg?文件名=总次数&编号=%d", times); public Mono<String> setTimes(String machineId, int times) {
log.debug("设置次数: codeNo={}, times={}, url={}", codeNo, times, url); String url = String.format(apiBaseUrl + "/yijianwan_netfile/saveMsg?文件名=总次数&%s=%d", machineId, times);
log.debug("设置次数: machineId={}, times={}, url={}", machineId, times, url);
return webClient.post() return webClient.post()
.uri(url) .uri(url)
.accept(MediaType.TEXT_PLAIN) .accept(MediaType.TEXT_PLAIN)
.retrieve() .retrieve()
.bodyToMono(String.class) .bodyToMono(String.class)
.timeout(Duration.ofSeconds(10)) .timeout(Duration.ofSeconds(10))
.doOnSuccess(result -> log.debug("设置次数成功: codeNo={}, times={}, result={}", codeNo, times, result)) .doOnSuccess(result -> log.debug("设置次数成功: machineId={}, times={}, result={}", machineId, times, result))
.doOnError(e -> log.warn("设置次数失败: codeNo={}, times={}, error={}", codeNo, times, e.toString())); .doOnError(e -> log.warn("设置次数失败: machineId={}, times={}, error={}", machineId, times, e.toString()));
} }
/** /**
@@ -320,8 +325,10 @@ public class ScriptClient {
* @param times 总次数 * @param times 总次数
* @return 保存结果 * @return 保存结果
*/ */
public Mono<String> saveTotalTimes(int times) { @RepeatCall(times = 3, description = "保存总次数")
String url = String.format(apiBaseUrl + "/yijianwan_netfile/saveMsg?文件名=总次数&f4=%d", times); public Mono<String> saveTotalTimes(String meachainId,int times) {
String url = String.format(apiBaseUrl + "/yijianwan_netfile/saveMsg?文件名=总次数&%s=%d", meachainId,times);
log.info("保存刷副本的次数的url: {}", url);
log.info("开始调用保存总次数接口: times={}, url={}", times, url); log.info("开始调用保存总次数接口: times={}, url={}", times, url);
return webClient.get() return webClient.get()
.uri(url) .uri(url)
@@ -334,6 +341,7 @@ public class ScriptClient {
} }
/** /**
* TODO
* 获取特定设备的状态信息(用于检查是否完成游戏) * 获取特定设备的状态信息(用于检查是否完成游戏)
* @param machineId 设备ID * @param machineId 设备ID
* @return 设备状态信息的Map包含f0(点数)和f1(状态)等信息 * @return 设备状态信息的Map包含f0(点数)和f1(状态)等信息
@@ -348,6 +356,7 @@ public class ScriptClient {
.retrieve() .retrieve()
.bodyToMono(String.class) .bodyToMono(String.class)
.timeout(Duration.ofSeconds(10)) .timeout(Duration.ofSeconds(10))
.retry(3) // 失败时重试3次
.map(jsonResponse -> { .map(jsonResponse -> {
// 解析JSON响应提取指定设备的状态信息 // 解析JSON响应提取指定设备的状态信息
return deviceStatusService.parseDeviceStatusForMachine(jsonResponse, machineId); return deviceStatusService.parseDeviceStatusForMachine(jsonResponse, machineId);
@@ -365,8 +374,9 @@ public class ScriptClient {
* @param machineId 真实设备编号 (如 f1, ss9) * @param machineId 真实设备编号 (如 f1, ss9)
* @return 退单操作结果 * @return 退单操作结果
*/ */
@RepeatCall(times = 3, description = "退单操作")
public Mono<String> refundOrder(String machineId) { public Mono<String> refundOrder(String machineId) {
String url = String.format(apiBaseUrl + "/yijianwan_netfile/saveMsg?文件名=判断退单&cc2=%s", machineId); String url = String.format(apiBaseUrl + "/yijianwan_netfile/saveMsg?文件名=判断退单&%s=T", machineId);
log.info("调用退单接口: 设备={}, url={}", machineId, url); log.info("调用退单接口: 设备={}, url={}", machineId, url);
return webClient.get() return webClient.get()

View File

@@ -12,8 +12,8 @@ import com.gameplatform.server.model.entity.agent.LinkBatch;
import com.gameplatform.server.model.entity.agent.LinkTask; import com.gameplatform.server.model.entity.agent.LinkTask;
import com.gameplatform.server.service.external.ScriptClient; import com.gameplatform.server.service.external.ScriptClient;
import com.gameplatform.server.service.device.DeviceCodeMappingService;
import com.gameplatform.server.service.device.DeviceStatusCheckService; import com.gameplatform.server.service.device.DeviceStatusCheckService;
import com.gameplatform.server.service.admin.SystemConfigService;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -37,8 +37,8 @@ public class LinkStatusService {
private final LinkTaskMapper linkTaskMapper; private final LinkTaskMapper linkTaskMapper;
private final LinkBatchMapper linkBatchMapper; private final LinkBatchMapper linkBatchMapper;
private final ScriptClient scriptClient; private final ScriptClient scriptClient;
private final DeviceCodeMappingService deviceCodeMappingService;
private final DeviceStatusCheckService deviceStatusCheckService; private final DeviceStatusCheckService deviceStatusCheckService;
private final SystemConfigService systemConfigService;
// 状态描述映射 // 状态描述映射
@@ -53,13 +53,13 @@ public class LinkStatusService {
} }
public LinkStatusService(LinkTaskMapper linkTaskMapper, LinkBatchMapper linkBatchMapper, public LinkStatusService(LinkTaskMapper linkTaskMapper, LinkBatchMapper linkBatchMapper,
ScriptClient scriptClient, DeviceCodeMappingService deviceCodeMappingService, ScriptClient scriptClient,
DeviceStatusCheckService deviceStatusCheckService) { DeviceStatusCheckService deviceStatusCheckService, SystemConfigService systemConfigService) {
this.linkTaskMapper = linkTaskMapper; this.linkTaskMapper = linkTaskMapper;
this.linkBatchMapper = linkBatchMapper; this.linkBatchMapper = linkBatchMapper;
this.scriptClient = scriptClient; this.scriptClient = scriptClient;
this.deviceCodeMappingService = deviceCodeMappingService;
this.deviceStatusCheckService = deviceStatusCheckService; this.deviceStatusCheckService = deviceStatusCheckService;
this.systemConfigService = systemConfigService;
} }
/** /**
@@ -515,9 +515,12 @@ public class LinkStatusService {
private void performAutoRefresh(LinkTask linkTask) { private void performAutoRefresh(LinkTask linkTask) {
try { try {
log.info("开始执行刷新操作"); log.info("开始执行刷新操作");
if (linkTask.getMachineId() == null) {
log.warn("机器ID为空无法执行刷新操作");
return;
}
// 调用判断刷新接口通过ScriptClient统一管理 // 调用判断刷新接口通过ScriptClient统一管理
String refreshResult = scriptClient.checkRefresh().block(); String refreshResult = scriptClient.refresh(linkTask.getMachineId()).block();
log.info("判断刷新接口调用完成: result={}", refreshResult); log.info("判断刷新接口调用完成: result={}", refreshResult);
} catch (Exception e) { } catch (Exception e) {
@@ -588,25 +591,20 @@ public class LinkStatusService {
throw new IllegalArgumentException("链接状态不正确,只有新建或使用中状态的链接才能选区"); throw new IllegalArgumentException("链接状态不正确,只有新建或使用中状态的链接才能选区");
} }
// 4. 检查链接是否过期
if (linkTask.getExpireAt() != null && linkTask.getExpireAt().isBefore(LocalDateTime.now())) {
log.error("链接已过期: expireAt={}", linkTask.getExpireAt());
throw new IllegalArgumentException("链接已过期");
}
// 5. 如果need_refresh=true检查是否已等待10秒 // 5. 如果need_refresh=true检查是否已等待10秒
if (Boolean.TRUE.equals(linkTask.getNeedRefresh()) && linkTask.getRefreshTime() != null) { // if (Boolean.TRUE.equals(linkTask.getNeedRefresh()) && linkTask.getRefreshTime() != null) {
LocalDateTime now = LocalDateTime.now(); // LocalDateTime now = LocalDateTime.now();
long secondsSinceRefresh = ChronoUnit.SECONDS.between(linkTask.getRefreshTime(), now); // long secondsSinceRefresh = ChronoUnit.SECONDS.between(linkTask.getRefreshTime(), now);
if (secondsSinceRefresh < 10) { // if (secondsSinceRefresh < 10) {
long waitTime = 10 - secondsSinceRefresh; // long waitTime = 10 - secondsSinceRefresh;
log.error("刷新后需要等待,剩余等待时间: {}秒", waitTime); // log.error("刷新后需要等待,剩余等待时间: {}秒", waitTime);
SelectRegionResponse response = new SelectRegionResponse(false, "刷新后需要等待" + waitTime + "秒才能选区"); // SelectRegionResponse response = new SelectRegionResponse(false, "刷新后需要等待" + waitTime + "秒才能选区");
return response; // return response;
} // }
} // }
String selectedDeviceId;
// 6. 检查空闲设备 if(linkTask.getFirstRegionSelectAt() == null){
log.info("开始检查空闲设备"); log.info("开始检查空闲设备");
DeviceStatusResponse deviceStatus = scriptClient.checkAvailableDeviceStatus().block(); DeviceStatusResponse deviceStatus = scriptClient.checkAvailableDeviceStatus().block();
@@ -620,24 +618,49 @@ public class LinkStatusService {
deviceStatus.getTotalDevices(), deviceStatus.getAvailableCount(), deviceStatus.getAvailableDevices()); deviceStatus.getTotalDevices(), deviceStatus.getAvailableCount(), deviceStatus.getAvailableDevices());
// 7. 选择一个空闲设备 TODO // 7. 选择一个空闲设备 TODO
String selectedDevice = deviceStatus.getAvailableDevices().get(0); // 选择第一个空闲设备 selectedDeviceId = deviceStatus.getAvailableDevices().get(0); // 选择第一个空闲设备
// String selectedDevice = "cc2"; // String selectedDevice = "cc2";
log.info("从空闲设备列表中选择设备: {}", selectedDevice); log.info("从空闲设备列表中选择设备: {}", selectedDeviceId);
log.info("设备选择详情: 可用设备总数={}, 选择了第一个设备={}", log.info("设备选择详情: 可用设备总数={}, 选择了第一个设备={}",
deviceStatus.getAvailableDevices().size(), selectedDevice); deviceStatus.getAvailableDevices().size(), selectedDeviceId);
}else{
// 检查首次选区是否已过期
LocalDateTime firstSelectTime = linkTask.getFirstRegionSelectAt();
long expireSeconds = systemConfigService.getFirstRegionExpireSeconds();
LocalDateTime expireTime = firstSelectTime.plusSeconds(expireSeconds);
if (LocalDateTime.now().isAfter(expireTime)) {
log.warn("链接首次选区已过期: firstSelectTime={}, expireTime={}, now={}",
firstSelectTime, expireTime, LocalDateTime.now());
// 将链接状态设置为过期
linkTask.setStatus("EXPIRED");
linkTask.setUpdatedAt(LocalDateTime.now());
linkTaskMapper.updateById(linkTask);
log.info("链接状态已更新为EXPIRED: linkTaskId={}", linkTask.getId());
throw new RuntimeException("链接已过期,请重新获取");
}
selectedDeviceId = linkTask.getMachineId();
scriptClient.refresh(selectedDeviceId).block();
log.info("链接已选过区且未过期,继续使用之前的设备: {}", selectedDeviceId);
}
// 6. 检查空闲设备
// 7.5. 检查该设备是否有之前的LOGGED_IN状态链接任务需要完成 // 7.5. 检查该设备是否有之前的LOGGED_IN状态链接任务需要完成
try { try {
log.info("检查设备 {} 是否有需要完成的链接任务", selectedDevice); log.info("检查设备 {} 是否有需要完成的链接任务", selectedDeviceId);
deviceStatusCheckService.checkDeviceStatusAndUpdateTasks(selectedDevice, "选区请求"); deviceStatusCheckService.checkDeviceStatusAndUpdateTasks(selectedDeviceId, "选区请求");
} catch (Exception e) { } catch (Exception e) {
log.warn("检查设备状态时发生异常,继续选区流程: {}", e.getMessage()); log.warn("检查设备状态时发生异常,继续选区流程: {}", e.getMessage());
// 不影响选区流程,只记录警告日志 // 不影响选区流程,只记录警告日志
} }
// 8. 调用保存总次数接口 // 8. 调用保存总次数接口 TODO 应该放在登录成功之后
try { try {
scriptClient.saveTotalTimes(linkBatch.getTimes()).block(); scriptClient.saveTotalTimes(selectedDeviceId,linkBatch.getTimes()).block();
// saveTotalTimes方法已经包含了详细的日志记录 // saveTotalTimes方法已经包含了详细的日志记录
} catch (Exception e) { } catch (Exception e) {
log.warn("保存总次数接口调用失败: {}", e.getMessage()); log.warn("保存总次数接口调用失败: {}", e.getMessage());
@@ -645,52 +668,48 @@ public class LinkStatusService {
} }
// 9. 为选中的设备创建代理code // 9. 为选中的设备创建代理code
String proxyCode = deviceCodeMappingService.createProxyCode(selectedDevice); // String proxyCode = deviceCodeMappingService.createProxyCode(selectedDeviceId);
log.info("为设备 {} 创建代理code: {}", selectedDevice, proxyCode); // log.info("为设备 {} 创建代理code: {}", selectedDeviceId, proxyCode);
// 10. 调用脚本端选区,使用选中的设备 // 10. 调用脚本端选区,使用选中的设备
log.info("开始调用脚本端选区,设备={}, 区域={}", selectedDevice, region); log.info("开始调用脚本端选区,设备={}, 区域={}", selectedDeviceId, region);
String selectResult = scriptClient.selectRegion(selectedDevice, region).block(); String selectResult = scriptClient.selectRegion(selectedDeviceId, region).block();
log.info("脚本端选区结果: {}", selectResult); log.info("脚本端选区结果: {}", selectResult);
// 11. 等待脚本端生成二维码(这里可以添加轮询逻辑) // 11. 等待脚本端生成二维码(这里可以添加轮询逻辑)
log.info("等待脚本端生成二维码等待3秒..."); // log.info("等待脚本端生成二维码等待3秒...");
Thread.sleep(3000); // Thread.sleep(3000);
// 12. 更新数据库状态为USING保存设备信息和代理code // 12. 更新数据库状态为USING保存设备信息和代理code
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
linkTask.setStatus("USING"); linkTask.setStatus("USING");
linkTask.setRegion(region); linkTask.setRegion(region);
linkTask.setCodeNo(proxyCode); // 使用代理code替换原来的codeNo linkTask.setCodeNo(code); // 使用代理code替换原来的codeNo
linkTask.setQrCreatedAt(now); linkTask.setQrCreatedAt(now);
linkTask.setQrExpireAt(now.plusSeconds(60)); // 60秒后过期 linkTask.setQrExpireAt(now.plusSeconds(60)); // 60秒后过期 ToDO
linkTask.setFirstRegionSelectAt(now); // 记录首次选区时间 linkTask.setFirstRegionSelectAt(now); // 记录首次选区时间
linkTask.setNeedRefresh(false); linkTask.setNeedRefresh(false);
linkTask.setUpdatedAt(now); linkTask.setUpdatedAt(now);
// 在machineId字段保存真实设备编号便于调试和维护 // 在machineId字段保存真实设备编号便于调试和维护
linkTask.setMachineId(selectedDevice); linkTask.setMachineId(selectedDeviceId);
linkTaskMapper.update(linkTask); linkTaskMapper.update(linkTask);
log.info("链接状态更新成功: status=USING, region={}, proxyCode={}, device={}", region, proxyCode, selectedDevice); log.info("链接状态更新成功: status=USING, region={}, proxyCode={}, device={}", region, code, selectedDeviceId);
// 13. 构建响应 // 13. 构建响应
SelectRegionResponse response = new SelectRegionResponse(true, "选区成功"); SelectRegionResponse response = new SelectRegionResponse(true, "选区成功");
response.setQrCodeUrl(scriptClient.getProxyQrCodeUrl(proxyCode)); response.setQrCodeUrl(scriptClient.getProxyQrCodeUrl(code));
response.setQrCreatedAt(now); response.setQrCreatedAt(now);
response.setQrExpireAt(linkTask.getQrExpireAt()); response.setQrExpireAt(linkTask.getQrExpireAt());
response.setStatus("USING"); response.setStatus("USING");
// 不返回选区字段response.setRegion(region); // 不返回选区字段response.setRegion(region);
response.setQrDelaySeconds(5); // 客户端收到响应后等待5秒再请求二维码 response.setQrDelaySeconds(5); // 客户端收到响应后等待5秒再请求二维码
response.setMecmachineId(selectedDeviceId); // 便于调试和维护
log.info("=== 选区操作完成 ==="); log.info("=== 选区操作完成 ===");
log.info("二维码URL: {}", response.getQrCodeUrl()); log.info("二维码URL: {}", response.getQrCodeUrl());
return response; return response;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("选区操作被中断: {}", e.getMessage());
throw new RuntimeException("选区操作被中断", e);
} catch (Exception e) { } catch (Exception e) {
log.error("=== 选区操作失败 ==="); log.error("=== 选区操作失败 ===");
log.error("错误详情: {}", e.getMessage(), e); log.error("错误详情: {}", e.getMessage(), e);
@@ -705,13 +724,34 @@ public class LinkStatusService {
log.info("=== 开始轮询上号 ==="); log.info("=== 开始轮询上号 ===");
log.info("code: {}", code); log.info("code: {}", code);
return validatePollLoginRequest(code)
.flatMap(result -> {
if (result instanceof PollLoginResponse) {
// 如果验证返回响应对象,直接返回
return Mono.just((PollLoginResponse) result);
}
// 否则继续处理登录状态检查
return checkAndHandleLoginStatus((LinkTask) result);
})
.doOnSuccess(response -> {
log.info("轮询上号完成: success={}, status={}", response.isSuccess(), response.getStatus());
})
.doOnError(error -> {
log.error("轮询上号失败: code={}, error={}", code, error.getMessage(), error);
});
}
/**
* 验证轮询上号请求参数和链接状态
*/
private Mono<Object> validatePollLoginRequest(String code) {
return Mono.fromCallable(() -> { return Mono.fromCallable(() -> {
// 1. 验证code参数 // 验证code参数
if (code == null || code.trim().isEmpty()) { if (code == null || code.trim().isEmpty()) {
throw new IllegalArgumentException("参数错误code不能为空"); throw new IllegalArgumentException("参数错误code不能为空");
} }
// 2. 获取链接任务 // 获取链接任务
LinkTask linkTask = linkTaskMapper.findByCodeNo(code); LinkTask linkTask = linkTaskMapper.findByCodeNo(code);
if (linkTask == null) { if (linkTask == null) {
throw new IllegalArgumentException("链接不存在"); throw new IllegalArgumentException("链接不存在");
@@ -720,48 +760,80 @@ public class LinkStatusService {
log.info("找到链接任务: id={}, status={}, codeNo={}", log.info("找到链接任务: id={}, status={}, codeNo={}",
linkTask.getId(), linkTask.getStatus(), linkTask.getCodeNo()); linkTask.getId(), linkTask.getStatus(), linkTask.getCodeNo());
// 3. 检查链接状态只有USING状态才能轮询 // 检查链接状态只有USING状态才能轮询
if (!"USING".equals(linkTask.getStatus())) { if (!"USING".equals(linkTask.getStatus())) {
log.warn("链接状态不是USING当前状态: {}", linkTask.getStatus()); log.warn("链接状态不是USING当前状态: {}", linkTask.getStatus());
return new PollLoginResponse(false, linkTask.getStatus()); return new PollLoginResponse(false, linkTask.getStatus());
} }
return linkTask; return linkTask;
}) }).subscribeOn(Schedulers.boundedElastic());
.subscribeOn(Schedulers.boundedElastic())
.flatMap(result -> {
if (result instanceof PollLoginResponse) {
// 如果已经是响应对象,直接返回
return Mono.just((PollLoginResponse) result);
} }
LinkTask linkTask = (LinkTask) result; /**
* 获取任务对应的设备ID
// 4. 获取真实设备编号 - 从代理code映射中获取 */
final String realDeviceId; private String getDeviceIdForTask(LinkTask linkTask) {
if (linkTask.getMachineId() != null) { if (linkTask.getMachineId() != null) {
// 如果已保存了设备编号,直接使用 // 如果已保存了设备编号,直接使用
realDeviceId = linkTask.getMachineId(); return linkTask.getMachineId();
} else if (linkTask.getCodeNo() != null) {
// 如果没有设备编号尝试从代理code获取
realDeviceId = deviceCodeMappingService.getDeviceId(linkTask.getCodeNo());
} else {
realDeviceId = null;
} }
return null;
}
/**
* 检查登录状态并处理结果
*/
private Mono<PollLoginResponse> checkAndHandleLoginStatus(LinkTask linkTask) {
// 获取真实设备编号
String realDeviceId = getDeviceIdForTask(linkTask);
if (realDeviceId == null) { if (realDeviceId == null) {
log.error("无法获取设备编号: codeNo={}, machineId={}", linkTask.getCodeNo(), linkTask.getMachineId()); log.error("无法获取设备编号: codeNo={}, machineId={}", linkTask.getCodeNo(), linkTask.getMachineId());
throw new RuntimeException("设备信息异常,无法检查上号状态"); return Mono.error(new RuntimeException("设备信息异常,无法检查上号状态"));
} }
// 5. 调用脚本端检查上号状态,使用真实设备编号 // 调用脚本端检查上号状态
log.info("调用脚本端检查上号状态: 代理code={}, 真实设备={}", linkTask.getCodeNo(), realDeviceId); log.info("调用脚本端检查上号状态: 代理code={}, 真实设备={}", linkTask.getCodeNo(), realDeviceId);
return scriptClient.checkLoginStatus(realDeviceId) return scriptClient.checkLoginStatus(realDeviceId)
.map(loginResult -> { .map(loginResult -> processLoginResult(linkTask, realDeviceId, loginResult))
.onErrorResume(error -> {
log.warn("调用脚本端检查上号状态失败: codeNo={}, error={}",
linkTask.getCodeNo(), error.getMessage());
// 脚本端出错时,返回当前状态,不影响轮询
return Mono.just(new PollLoginResponse(false, "USING"));
});
}
/**
* 处理登录检查结果
*/
private PollLoginResponse processLoginResult(LinkTask linkTask, String deviceId, String loginResult) {
log.info("脚本端返回结果: {}", loginResult); log.info("脚本端返回结果: {}", loginResult);
// 6. 如果返回"已上号"更新状态为LOGGED_IN // 检查是否已上号
if ("已上号".equals(loginResult) || "已登录".equals(loginResult)) { if ("已上号".equals(loginResult) || "已登录".equals(loginResult)) {
try {
LinkBatch linkBatch = linkBatchMapper.findById(linkTask.getBatchId());
scriptClient.saveTotalTimes(deviceId,linkBatch.getTimes()).block();
// saveTotalTimes方法已经包含了详细的日志记录
} catch (Exception e) {
log.warn("保存总次数接口调用失败: {}", e.getMessage());
// 不影响后续流程,只记录警告日志
}
return handleSuccessfulLogin(linkTask, deviceId);
} else {
// 未上号,返回当前状态
log.debug("尚未上号,返回当前状态");
return new PollLoginResponse(false, "USING");
}
}
/**
* 处理成功登录的情况
*/
private PollLoginResponse handleSuccessfulLogin(LinkTask linkTask, String deviceId) {
log.info("检测到已上号更新状态为LOGGED_IN"); log.info("检测到已上号更新状态为LOGGED_IN");
// 更新数据库状态 // 更新数据库状态
@@ -771,32 +843,45 @@ public class LinkStatusService {
log.info("状态更新完成: codeNo={}, status=LOGGED_IN", linkTask.getCodeNo()); log.info("状态更新完成: codeNo={}, status=LOGGED_IN", linkTask.getCodeNo());
// 7. 返回成功响应和资源信息使用ScriptClient统一管理资源链接 // 构建成功响应
PollLoginResponse response = new PollLoginResponse(true, "LOGGED_IN", "SECOND", PollLoginResponse response = new PollLoginResponse(true, "LOGGED_IN", "SECOND",
new PollLoginResponse.AssetsInfo( new PollLoginResponse.AssetsInfo(
scriptClient.getAssetsBaseUrl(realDeviceId) scriptClient.getAssetsBaseUrl(deviceId)
)); ));
log.info("=== 轮询上号成功 ==="); log.info("=== 轮询上号成功 ===");
return response; return response;
} else {
// 未上号,返回当前状态
log.debug("尚未上号,返回当前状态");
return new PollLoginResponse(false, "USING");
} }
})
.onErrorResume(error -> { /**
log.warn("调用脚本端检查上号状态失败: codeNo={}, error={}", * 根据链接编号获取设备ID
linkTask.getCodeNo(), error.getMessage()); * @param code 链接编号
// 脚本端出错时,返回当前状态,不影响轮询 * @return 设备ID如果链接不存在或未关联设备则返回null
return Mono.just(new PollLoginResponse(false, "USING")); */
}); public String getMechainIdByCode(String code) {
}) if (code == null || code.trim().isEmpty()) {
.doOnSuccess(response -> { log.warn("获取设备ID失败: code参数为空");
log.info("轮询上号完成: success={}, status={}", response.isSuccess(), response.getStatus()); return null;
}) }
.doOnError(error -> {
log.error("轮询上号失败: code={}, error={}", code, error.getMessage(), error); log.debug("根据code获取设备ID: code={}", code);
});
try {
// 查询链接任务
LinkTask linkTask = linkTaskMapper.findByCodeNo(code.trim());
if (linkTask == null) {
log.warn("链接任务不存在: code={}", code);
return null;
}
String machineId = linkTask.getMachineId();
log.debug("查询到设备ID: code={}, machineId={}", code, machineId);
return machineId;
} catch (Exception e) {
log.error("根据code获取设备ID时发生异常: code={}, error={}", code, e.getMessage(), e);
return null;
}
} }
} }

View File

@@ -70,7 +70,8 @@ script:
# 服务器配置 # 服务器配置
app: app:
base-url: "https://2.uzi0.cc" # 生产环境需要配置为实际域名 # base-url: "https://2.uzi0.cc" # 生产环境需要配置为实际域名
base-url: "http://localhost:18080" # 本地测试环境
image-save-path: "./images" # 图片保存路径 image-save-path: "./images" # 图片保存路径
link: link: