diff --git a/pom.xml b/pom.xml index c8021eb..72dc917 100644 --- a/pom.xml +++ b/pom.xml @@ -33,6 +33,12 @@ spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-aop + + com.baomidou diff --git a/src/main/java/com/gameplatform/server/annotation/README_RepeatCall.md b/src/main/java/com/gameplatform/server/annotation/README_RepeatCall.md new file mode 100644 index 0000000..b6fd886 --- /dev/null +++ b/src/main/java/com/gameplatform/server/annotation/README_RepeatCall.md @@ -0,0 +1,108 @@ +# 重试机制使用说明 + +本项目提供了两种不同的重试机制,用于不同的业务场景。 + +## 🔄 两种重试机制对比 + +| 特性 | @RepeatCall 注解 | retry() 方法 | +|------|-----------------|--------------| +| **触发条件** | 无论成功失败都执行指定次数 | 只有在请求失败时才重试 | +| **适用场景** | 确保脚本端接收到关键操作 | 网络不稳定的查询操作 | +| **执行次数** | 固定执行N次 | 最多重试N次(成功则停止) | +| **性能影响** | 较大(总是执行多次) | 较小(成功时只执行1次) | + +## 🎯 @RepeatCall 注解机制 + +### 功能介绍 +无论成功还是失败都会执行指定次数,主要用于确保脚本端能够可靠接收到请求。 + +### 使用方法 + +```java +@RepeatCall(times = 3, description = "设置次数") +public Mono 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 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. 建议根据业务重要性选择合适的重试机制 diff --git a/src/main/java/com/gameplatform/server/annotation/RepeatCall.java b/src/main/java/com/gameplatform/server/annotation/RepeatCall.java new file mode 100644 index 0000000..4ce3dfd --- /dev/null +++ b/src/main/java/com/gameplatform/server/annotation/RepeatCall.java @@ -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 ""; +} diff --git a/src/main/java/com/gameplatform/server/aspect/RepeatCallAspect.java b/src/main/java/com/gameplatform/server/aspect/RepeatCallAspect.java new file mode 100644 index 0000000..bf8a2a3 --- /dev/null +++ b/src/main/java/com/gameplatform/server/aspect/RepeatCallAspect.java @@ -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 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 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()); + }); + } +} \ No newline at end of file diff --git a/src/main/java/com/gameplatform/server/controller/link/LinkController.java b/src/main/java/com/gameplatform/server/controller/link/LinkController.java index 6a62dd3..bcf4b9c 100644 --- a/src/main/java/com/gameplatform/server/controller/link/LinkController.java +++ b/src/main/java/com/gameplatform/server/controller/link/LinkController.java @@ -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.LinkListService; import com.gameplatform.server.service.link.LinkStatusService; -import com.gameplatform.server.service.device.DeviceCodeMappingService; import com.gameplatform.server.service.external.ScriptClient; import io.jsonwebtoken.Claims; import io.swagger.v3.oas.annotations.Operation; @@ -41,18 +40,15 @@ public class LinkController { private final LinkGenerationService linkGenerationService; private final LinkStatusService linkStatusService; private final LinkListService linkListService; - private final DeviceCodeMappingService deviceCodeMappingService; private final ScriptClient scriptClient; public LinkController(LinkGenerationService linkGenerationService, LinkStatusService linkStatusService, LinkListService linkListService, - DeviceCodeMappingService deviceCodeMappingService, ScriptClient scriptClient) { this.linkGenerationService = linkGenerationService; this.linkStatusService = linkStatusService; this.linkListService = linkListService; - this.deviceCodeMappingService = deviceCodeMappingService; this.scriptClient = scriptClient; } @@ -182,35 +178,6 @@ public class LinkController { }); } -// @GetMapping("/{codeNo}/status") -// @Operation(summary = "获取链接状态", description = "根据链接编号获取链接的详细状态信息,包括过期时间、奖励点数、当前状态等") -// public Mono 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 isLinkExists(@PathVariable("codeNo") String codeNo) { -// log.debug("检查链接是否存在: codeNo={}", codeNo); -// return linkStatusService.isLinkExists(codeNo); -// } -// -// @GetMapping("/{codeNo}/valid") -// @Operation(summary = "检查链接是否有效", description = "检查指定链接是否有效(未过期且状态正常)") -// public Mono isLinkValid(@PathVariable("codeNo") String codeNo) { -// log.debug("检查链接是否有效: codeNo={}", codeNo); -// return linkStatusService.isLinkValid(codeNo); -// } @DeleteMapping("/{codeNo}") @Operation(summary = "删除链接", description = "删除指定的链接,用户只能删除自己创建的链接") public Mono deleteLink(@PathVariable("codeNo") String codeNo, Authentication authentication) { @@ -464,31 +431,31 @@ public Mono deleteLink(@PathVariable("codeNo") String codeNo, Authentic * 代理二维码获取接口 * 通过代理code获取真实设备的二维码,避免暴露设备编号 */ - @GetMapping("/qr/{proxyCode}") + @GetMapping("/qr/{code}") @Operation(summary = "获取二维码", description = "通过代理code获取设备二维码,用于扫码上号") - public Mono> getProxyQrCode(@PathVariable("proxyCode") String proxyCode) { + public Mono> getProxyQrCode(@PathVariable("code") String code) { log.info("=== 获取代理二维码 ==="); - log.info("代理code: {}", proxyCode); + log.info("代理code: {}", code); - // 验证代理code是否有效 - if (!deviceCodeMappingService.isValidProxyCode(proxyCode)) { - log.warn("无效的代理code: {}", proxyCode); - return Mono.just(ResponseEntity.notFound().build()); - } +// // 验证代理code是否有效 +// if (!deviceCodeMappingService.isValidProxyCode(proxyCode)) { +// log.warn("无效的代理code: {}", proxyCode); +// return Mono.just(ResponseEntity.notFound().build()); +// } // 获取真实设备编号 - String deviceId = deviceCodeMappingService.getDeviceId(proxyCode); - if (deviceId == null) { - log.warn("代理code对应的设备不存在: {}", proxyCode); + String meachainId = linkStatusService.getMechainIdByCode(code); + if (meachainId == null) { + log.warn("代理code对应的设备不存在: {}", code); 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 -> { - log.info("获取设备 {} 二维码成功,大小: {} 字节", deviceId, qrData.length); + log.info("获取设备 {} 二维码成功,大小: {} 字节", meachainId, qrData.length); // 设置响应头 HttpHeaders headers = new HttpHeaders(); @@ -502,14 +469,14 @@ public Mono deleteLink(@PathVariable("codeNo") String codeNo, Authentic .body(qrData); }) .onErrorResume(error -> { - log.error("获取设备 {} 二维码失败: {}", deviceId, error.getMessage(), error); + log.error("获取设备 {} 二维码失败: {}", meachainId, error.getMessage(), error); // 如果是404错误,返回404;其他错误返回500 if (error instanceof WebClientResponseException.NotFound) { - log.warn("设备 {} 的二维码文件不存在,返回404", deviceId); + log.warn("设备 {} 的二维码文件不存在,返回404", meachainId); return Mono.just(ResponseEntity.status(HttpStatus.NOT_FOUND).build()); } else { - log.error("获取设备 {} 二维码时发生系统错误", deviceId); + log.error("获取设备 {} 二维码时发生系统错误", meachainId); return Mono.just(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build()); } }); diff --git a/src/main/java/com/gameplatform/server/model/dto/link/SelectRegionResponse.java b/src/main/java/com/gameplatform/server/model/dto/link/SelectRegionResponse.java index e26abe8..5c07795 100644 --- a/src/main/java/com/gameplatform/server/model/dto/link/SelectRegionResponse.java +++ b/src/main/java/com/gameplatform/server/model/dto/link/SelectRegionResponse.java @@ -29,7 +29,12 @@ public class SelectRegionResponse { @Schema(description = "二维码出现延迟时间(秒)", example = "5") private Integer qrDelaySeconds; - + + @Schema(description = "MEC 机器ID", example = "f1") + private String mecmachineId; + + + public SelectRegionResponse() {} public SelectRegionResponse(boolean success, String message) { @@ -100,7 +105,13 @@ public class SelectRegionResponse { public void setQrDelaySeconds(Integer qrDelaySeconds) { this.qrDelaySeconds = qrDelaySeconds; } - + + public String getMecmachineId() { + return mecmachineId; + } + public void setMecmachineId(String mecmachineId) { + this.mecmachineId = mecmachineId; + } @Override public String toString() { return "SelectRegionResponse{" + @@ -112,6 +123,7 @@ public class SelectRegionResponse { ", status='" + status + '\'' + ", region='" + region + '\'' + ", qrDelaySeconds=" + qrDelaySeconds + + ", mecmachineId='" + mecmachineId + '\'' + '}'; } } diff --git a/src/main/java/com/gameplatform/server/security/SecurityConfig.java b/src/main/java/com/gameplatform/server/security/SecurityConfig.java index e956f88..e7da1de 100644 --- a/src/main/java/com/gameplatform/server/security/SecurityConfig.java +++ b/src/main/java/com/gameplatform/server/security/SecurityConfig.java @@ -46,6 +46,7 @@ public class SecurityConfig { .pathMatchers(HttpMethod.POST, "/api/link/select-region").permitAll() // 用户端选区接口,公开访问 .pathMatchers(HttpMethod.GET, "/api/link/poll-login").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("/api/link/**").authenticated() // 其他链接接口需要认证 .anyExchange().permitAll() // 其他接口后续再收紧 @@ -70,6 +71,7 @@ public class SecurityConfig { log.info(" * POST /api/link/select-region -> 允许所有 (用户端公开接口)"); log.info(" * GET /api/link/poll-login -> 允许所有 (用户端公开接口)"); log.info(" * GET /api/link/qr/** -> 允许所有 (二维码获取接口)"); + log.info(" * HEAD /api/link/qr/** -> 允许所有 (二维码HEAD请求)"); log.info(" * GET /api/link/*/game-interface -> 允许所有 (游戏界面数据接口)"); log.info(" * /api/link/** -> 需要认证"); log.info(" * 其他路径 -> 允许所有"); diff --git a/src/main/java/com/gameplatform/server/service/admin/SystemConfigService.java b/src/main/java/com/gameplatform/server/service/admin/SystemConfigService.java index d1caabe..e0c86e5 100644 --- a/src/main/java/com/gameplatform/server/service/admin/SystemConfigService.java +++ b/src/main/java/com/gameplatform/server/service/admin/SystemConfigService.java @@ -113,6 +113,11 @@ public class SystemConfigService { return getConfigValue("device.idle_status", "空闲"); } + // 获取首次选区后链接过期时间(秒) + public Integer getFirstRegionExpireSeconds() { + return getConfigValueAsInt("link.first_region_expire", 600); + } + // 批量更新配置 public boolean updateConfigs(List configs) { if (configs == null || configs.isEmpty()) { diff --git a/src/main/java/com/gameplatform/server/service/device/DeviceCodeMappingService.java b/src/main/java/com/gameplatform/server/service/device/DeviceCodeMappingService.java deleted file mode 100644 index b9c37ac..0000000 --- a/src/main/java/com/gameplatform/server/service/device/DeviceCodeMappingService.java +++ /dev/null @@ -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 codeToDeviceMap = new ConcurrentHashMap<>(); - // 真实设备编号 -> 代理code的映射 - private final Map 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(); - } -} diff --git a/src/main/java/com/gameplatform/server/service/external/ScriptClient.java b/src/main/java/com/gameplatform/server/service/external/ScriptClient.java index 4e1a6e4..8d0143e 100644 --- a/src/main/java/com/gameplatform/server/service/external/ScriptClient.java +++ b/src/main/java/com/gameplatform/server/service/external/ScriptClient.java @@ -1,5 +1,6 @@ package com.gameplatform.server.service.external; +import com.gameplatform.server.annotation.RepeatCall; import com.gameplatform.server.model.dto.device.DeviceStatusResponse; import com.gameplatform.server.service.device.DeviceStatusService; import org.slf4j.Logger; @@ -59,6 +60,7 @@ public class ScriptClient { .retrieve() .bodyToMono(byte[].class) .timeout(Duration.ofSeconds(5)) + .retry(3) // 失败时重试3次 .doOnError(e -> log.warn("ScriptClient.getQrPng error path={} err={}", path, e.toString())); } @@ -75,6 +77,7 @@ public class ScriptClient { .retrieve() .bodyToMono(byte[].class) .timeout(Duration.ofSeconds(10)) + .retry(3) // 失败时重试3次 .doOnSuccess(data -> log.debug("获取图片成功: path={}, 数据大小={}字节", path, data != null ? data.length : 0)) .doOnError(e -> log.warn("获取图片失败: path={}, error={}", path, e.toString())); } @@ -86,6 +89,7 @@ public class ScriptClient { .retrieve() .bodyToMono(String.class) .timeout(Duration.ofSeconds(5)) + .retry(3) // 失败时重试3次 .doOnError(e -> log.warn("ScriptClient.getText error path={} err={}", path, e.toString())); } @@ -101,6 +105,7 @@ public class ScriptClient { .retrieve() .bodyToMono(String.class) .timeout(Duration.ofSeconds(10)) + .retry(3) // 失败时重试3次 .doOnSuccess(result -> log.debug("检查空闲设备成功: {}", result)) .doOnError(e -> log.warn("检查空闲设备失败: {}", e.toString())); } @@ -134,7 +139,16 @@ public class ScriptClient { * @param region 区域参数 (Q或V) * @return 选区结果 */ + @RepeatCall(times = 3, description = "选区操作") public Mono 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,使用设备编号作为参数名,区域作为参数值 // 示例: {apiBaseUrl}/yijianwan_netfile/saveMsg?文件名=判断系统&f1=Q String url = String.format(apiBaseUrl + "/yijianwan_netfile/saveMsg?文件名=判断系统&%s=%s", deviceId, region); @@ -154,27 +168,14 @@ public class ScriptClient { }); } - /** - * 刷新操作 - */ - public Mono 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 checkRefresh() { - String url = apiBaseUrl + "/yijianwan_netfile/saveMsg?文件名=判断刷新&f4=刷新"; + @RepeatCall(times = 3, description = "判断刷新") + public Mono refresh(String machineId) { + String url = String.format(apiBaseUrl + "/yijianwan_netfile/saveMsg?文件名=判断刷新&%s=刷新", machineId); log.info("调用判断刷新接口: {}", url); return webClient.get() .uri(url) @@ -203,6 +204,7 @@ public class ScriptClient { .retrieve() .bodyToMono(String.class) .timeout(Duration.ofSeconds(10)) + .retry(3) // 失败时重试3次 .doOnSuccess(result -> log.debug("检查设备上号状态成功: 设备={}, 状态={}", deviceId, result)) .doOnError(e -> log.warn("检查设备上号状态失败: 设备={}, 错误={}", deviceId, e.toString())); } @@ -275,6 +277,7 @@ public class ScriptClient { .retrieve() .bodyToMono(byte[].class) .timeout(Duration.ofSeconds(10)) + .retry(3) // 失败时重试3次 .doOnSuccess(data -> { log.debug("获取设备二维码成功: 设备={}, 数据大小={}字节", deviceId, data.length); }) @@ -286,33 +289,35 @@ public class ScriptClient { /** * 获取目标分数 */ - public Mono getTargetScore(String codeNo) { - String url = String.format(apiBaseUrl + "/yijianwan_netfile/readMsg?文件名=判断分数&对象名=%s", codeNo); - log.debug("获取目标分数: codeNo={}, url={}", codeNo, url); + public Mono getTargetScore(String machineId) { + String url = String.format(apiBaseUrl + "/yijianwan_netfile/readMsg?文件名=判断分数&对象名=%s", machineId); + log.debug("获取目标分数: machineId={}, url={}", machineId, url); return webClient.get() .uri(url) .accept(MediaType.TEXT_PLAIN) .retrieve() .bodyToMono(String.class) .timeout(Duration.ofSeconds(5)) - .doOnSuccess(result -> log.debug("获取目标分数成功: codeNo={}, result={}", codeNo, result)) - .doOnError(e -> log.warn("获取目标分数失败: codeNo={}, error={}", codeNo, e.toString())); + .retry(3) // 失败时重试3次 + .doOnSuccess(result -> log.debug("获取目标分数成功: machineId={}, result={}", machineId, result)) + .doOnError(e -> log.warn("获取目标分数失败: machineId={}, error={}", machineId, e.toString())); } /** - * 设置次数(生成链接时调用) + * 设置次数(生成链接时调用)- 使用 @RepeatCall 注解自动重复执行3次 */ - public Mono setTimes(String codeNo, int times) { - String url = String.format(apiBaseUrl + "/yijianwan_netfile/saveMsg?文件名=总次数&编号=%d", times); - log.debug("设置次数: codeNo={}, times={}, url={}", codeNo, times, url); + @RepeatCall(times = 3, description = "设置次数") + public Mono setTimes(String machineId, int times) { + String url = String.format(apiBaseUrl + "/yijianwan_netfile/saveMsg?文件名=总次数&%s=%d", machineId, times); + log.debug("设置次数: machineId={}, times={}, url={}", machineId, times, url); return webClient.post() .uri(url) .accept(MediaType.TEXT_PLAIN) .retrieve() .bodyToMono(String.class) .timeout(Duration.ofSeconds(10)) - .doOnSuccess(result -> log.debug("设置次数成功: codeNo={}, times={}, result={}", codeNo, times, result)) - .doOnError(e -> log.warn("设置次数失败: codeNo={}, times={}, error={}", codeNo, times, e.toString())); + .doOnSuccess(result -> log.debug("设置次数成功: machineId={}, times={}, result={}", machineId, times, result)) + .doOnError(e -> log.warn("设置次数失败: machineId={}, times={}, error={}", machineId, times, e.toString())); } /** @@ -320,8 +325,10 @@ public class ScriptClient { * @param times 总次数 * @return 保存结果 */ - public Mono saveTotalTimes(int times) { - String url = String.format(apiBaseUrl + "/yijianwan_netfile/saveMsg?文件名=总次数&f4=%d", times); + @RepeatCall(times = 3, description = "保存总次数") + public Mono 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); return webClient.get() .uri(url) @@ -334,6 +341,7 @@ public class ScriptClient { } /** + * TODO * 获取特定设备的状态信息(用于检查是否完成游戏) * @param machineId 设备ID * @return 设备状态信息的Map,包含f0(点数)和f1(状态)等信息 @@ -348,6 +356,7 @@ public class ScriptClient { .retrieve() .bodyToMono(String.class) .timeout(Duration.ofSeconds(10)) + .retry(3) // 失败时重试3次 .map(jsonResponse -> { // 解析JSON响应,提取指定设备的状态信息 return deviceStatusService.parseDeviceStatusForMachine(jsonResponse, machineId); @@ -365,8 +374,9 @@ public class ScriptClient { * @param machineId 真实设备编号 (如 f1, ss9) * @return 退单操作结果 */ + @RepeatCall(times = 3, description = "退单操作") public Mono 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); return webClient.get() diff --git a/src/main/java/com/gameplatform/server/service/link/LinkStatusService.java b/src/main/java/com/gameplatform/server/service/link/LinkStatusService.java index ed5ef74..e2bd6a4 100644 --- a/src/main/java/com/gameplatform/server/service/link/LinkStatusService.java +++ b/src/main/java/com/gameplatform/server/service/link/LinkStatusService.java @@ -12,8 +12,8 @@ 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.device.DeviceCodeMappingService; import com.gameplatform.server.service.device.DeviceStatusCheckService; +import com.gameplatform.server.service.admin.SystemConfigService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; @@ -37,8 +37,8 @@ public class LinkStatusService { private final LinkTaskMapper linkTaskMapper; private final LinkBatchMapper linkBatchMapper; private final ScriptClient scriptClient; - private final DeviceCodeMappingService deviceCodeMappingService; private final DeviceStatusCheckService deviceStatusCheckService; + private final SystemConfigService systemConfigService; // 状态描述映射 @@ -52,14 +52,14 @@ public class LinkStatusService { STATUS_DESC_MAP.put("EXPIRED", "已过期"); } - public LinkStatusService(LinkTaskMapper linkTaskMapper, LinkBatchMapper linkBatchMapper, - ScriptClient scriptClient, DeviceCodeMappingService deviceCodeMappingService, - DeviceStatusCheckService deviceStatusCheckService) { + public LinkStatusService(LinkTaskMapper linkTaskMapper, LinkBatchMapper linkBatchMapper, + ScriptClient scriptClient, + DeviceStatusCheckService deviceStatusCheckService, SystemConfigService systemConfigService) { this.linkTaskMapper = linkTaskMapper; this.linkBatchMapper = linkBatchMapper; this.scriptClient = scriptClient; - this.deviceCodeMappingService = deviceCodeMappingService; this.deviceStatusCheckService = deviceStatusCheckService; + this.systemConfigService = systemConfigService; } /** @@ -515,9 +515,12 @@ public class LinkStatusService { private void performAutoRefresh(LinkTask linkTask) { try { log.info("开始执行刷新操作"); - + if (linkTask.getMachineId() == null) { + log.warn("机器ID为空,无法执行刷新操作"); + return; + } // 调用判断刷新接口(通过ScriptClient统一管理) - String refreshResult = scriptClient.checkRefresh().block(); + String refreshResult = scriptClient.refresh(linkTask.getMachineId()).block(); log.info("判断刷新接口调用完成: result={}", refreshResult); } catch (Exception e) { @@ -588,56 +591,76 @@ public class LinkStatusService { 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秒 - if (Boolean.TRUE.equals(linkTask.getNeedRefresh()) && linkTask.getRefreshTime() != null) { - LocalDateTime now = LocalDateTime.now(); - long secondsSinceRefresh = ChronoUnit.SECONDS.between(linkTask.getRefreshTime(), now); - if (secondsSinceRefresh < 10) { - long waitTime = 10 - secondsSinceRefresh; - log.error("刷新后需要等待,剩余等待时间: {}秒", waitTime); - SelectRegionResponse response = new SelectRegionResponse(false, "刷新后需要等待" + waitTime + "秒才能选区"); - return response; +// if (Boolean.TRUE.equals(linkTask.getNeedRefresh()) && linkTask.getRefreshTime() != null) { +// LocalDateTime now = LocalDateTime.now(); +// long secondsSinceRefresh = ChronoUnit.SECONDS.between(linkTask.getRefreshTime(), now); +// if (secondsSinceRefresh < 10) { +// long waitTime = 10 - secondsSinceRefresh; +// log.error("刷新后需要等待,剩余等待时间: {}秒", waitTime); +// SelectRegionResponse response = new SelectRegionResponse(false, "刷新后需要等待" + waitTime + "秒才能选区"); +// return response; +// } +// } + String selectedDeviceId; + if(linkTask.getFirstRegionSelectAt() == null){ + log.info("开始检查空闲设备"); + DeviceStatusResponse deviceStatus = scriptClient.checkAvailableDeviceStatus().block(); + + // 检查是否有空闲设备 + if (deviceStatus.getAvailableCount() == 0) { + log.warn("当前没有空闲设备,无法选择区域"); + throw new RuntimeException("当前没有空闲设备,请稍后再试"); } - } + log.info("空闲设备检查完成: 总设备数={}, 空闲设备数={}, 空闲设备列表={}", + deviceStatus.getTotalDevices(), deviceStatus.getAvailableCount(), deviceStatus.getAvailableDevices()); + + // 7. 选择一个空闲设备 TODO + selectedDeviceId = deviceStatus.getAvailableDevices().get(0); // 选择第一个空闲设备 + // String selectedDevice = "cc2"; + log.info("从空闲设备列表中选择设备: {}", selectedDeviceId); + log.info("设备选择详情: 可用设备总数={}, 选择了第一个设备={}", + 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. 检查空闲设备 - log.info("开始检查空闲设备"); - DeviceStatusResponse deviceStatus = scriptClient.checkAvailableDeviceStatus().block(); - - // 检查是否有空闲设备 - if (deviceStatus.getAvailableCount() == 0) { - log.warn("当前没有空闲设备,无法选择区域"); - throw new RuntimeException("当前没有空闲设备,请稍后再试"); - } - - log.info("空闲设备检查完成: 总设备数={}, 空闲设备数={}, 空闲设备列表={}", - deviceStatus.getTotalDevices(), deviceStatus.getAvailableCount(), deviceStatus.getAvailableDevices()); - // 7. 选择一个空闲设备 TODO - String selectedDevice = deviceStatus.getAvailableDevices().get(0); // 选择第一个空闲设备 - // String selectedDevice = "cc2"; - log.info("从空闲设备列表中选择设备: {}", selectedDevice); - log.info("设备选择详情: 可用设备总数={}, 选择了第一个设备={}", - deviceStatus.getAvailableDevices().size(), selectedDevice); // 7.5. 检查该设备是否有之前的LOGGED_IN状态链接任务需要完成 try { - log.info("检查设备 {} 是否有需要完成的链接任务", selectedDevice); - deviceStatusCheckService.checkDeviceStatusAndUpdateTasks(selectedDevice, "选区请求"); + log.info("检查设备 {} 是否有需要完成的链接任务", selectedDeviceId); + deviceStatusCheckService.checkDeviceStatusAndUpdateTasks(selectedDeviceId, "选区请求"); } catch (Exception e) { log.warn("检查设备状态时发生异常,继续选区流程: {}", e.getMessage()); // 不影响选区流程,只记录警告日志 } - // 8. 调用保存总次数接口 + // 8. 调用保存总次数接口 TODO 应该放在登录成功之后 try { - scriptClient.saveTotalTimes(linkBatch.getTimes()).block(); + scriptClient.saveTotalTimes(selectedDeviceId,linkBatch.getTimes()).block(); // saveTotalTimes方法已经包含了详细的日志记录 } catch (Exception e) { log.warn("保存总次数接口调用失败: {}", e.getMessage()); @@ -645,52 +668,48 @@ public class LinkStatusService { } // 9. 为选中的设备创建代理code - String proxyCode = deviceCodeMappingService.createProxyCode(selectedDevice); - log.info("为设备 {} 创建代理code: {}", selectedDevice, proxyCode); +// String proxyCode = deviceCodeMappingService.createProxyCode(selectedDeviceId); +// log.info("为设备 {} 创建代理code: {}", selectedDeviceId, proxyCode); // 10. 调用脚本端选区,使用选中的设备 - log.info("开始调用脚本端选区,设备={}, 区域={}", selectedDevice, region); - String selectResult = scriptClient.selectRegion(selectedDevice, region).block(); + log.info("开始调用脚本端选区,设备={}, 区域={}", selectedDeviceId, region); + String selectResult = scriptClient.selectRegion(selectedDeviceId, region).block(); log.info("脚本端选区结果: {}", selectResult); // 11. 等待脚本端生成二维码(这里可以添加轮询逻辑) - log.info("等待脚本端生成二维码,等待3秒..."); - Thread.sleep(3000); +// log.info("等待脚本端生成二维码,等待3秒..."); +// Thread.sleep(3000); // 12. 更新数据库状态为USING,保存设备信息和代理code LocalDateTime now = LocalDateTime.now(); linkTask.setStatus("USING"); linkTask.setRegion(region); - linkTask.setCodeNo(proxyCode); // 使用代理code替换原来的codeNo + linkTask.setCodeNo(code); // 使用代理code替换原来的codeNo linkTask.setQrCreatedAt(now); - linkTask.setQrExpireAt(now.plusSeconds(60)); // 60秒后过期 + linkTask.setQrExpireAt(now.plusSeconds(60)); // 60秒后过期 ToDO linkTask.setFirstRegionSelectAt(now); // 记录首次选区时间 linkTask.setNeedRefresh(false); linkTask.setUpdatedAt(now); // 在machineId字段保存真实设备编号,便于调试和维护 - linkTask.setMachineId(selectedDevice); + linkTask.setMachineId(selectedDeviceId); linkTaskMapper.update(linkTask); - log.info("链接状态更新成功: status=USING, region={}, proxyCode={}, device={}", region, proxyCode, selectedDevice); + log.info("链接状态更新成功: status=USING, region={}, proxyCode={}, device={}", region, code, selectedDeviceId); // 13. 构建响应 SelectRegionResponse response = new SelectRegionResponse(true, "选区成功"); - response.setQrCodeUrl(scriptClient.getProxyQrCodeUrl(proxyCode)); + response.setQrCodeUrl(scriptClient.getProxyQrCodeUrl(code)); response.setQrCreatedAt(now); response.setQrExpireAt(linkTask.getQrExpireAt()); response.setStatus("USING"); // 不返回选区字段:response.setRegion(region); response.setQrDelaySeconds(5); // 客户端收到响应后,等待5秒再请求二维码 - + response.setMecmachineId(selectedDeviceId); // 便于调试和维护 log.info("=== 选区操作完成 ==="); log.info("二维码URL: {}", response.getQrCodeUrl()); return response; - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - log.error("选区操作被中断: {}", e.getMessage()); - throw new RuntimeException("选区操作被中断", e); } catch (Exception e) { log.error("=== 选区操作失败 ==="); log.error("错误详情: {}", e.getMessage(), e); @@ -705,13 +724,34 @@ public class LinkStatusService { log.info("=== 开始轮询上号 ==="); 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 validatePollLoginRequest(String code) { return Mono.fromCallable(() -> { - // 1. 验证code参数 + // 验证code参数 if (code == null || code.trim().isEmpty()) { throw new IllegalArgumentException("参数错误:code不能为空"); } - // 2. 获取链接任务 + // 获取链接任务 LinkTask linkTask = linkTaskMapper.findByCodeNo(code); if (linkTask == null) { throw new IllegalArgumentException("链接不存在"); @@ -720,83 +760,128 @@ public class LinkStatusService { log.info("找到链接任务: id={}, status={}, codeNo={}", linkTask.getId(), linkTask.getStatus(), linkTask.getCodeNo()); - // 3. 检查链接状态,只有USING状态才能轮询 + // 检查链接状态,只有USING状态才能轮询 if (!"USING".equals(linkTask.getStatus())) { log.warn("链接状态不是USING,当前状态: {}", linkTask.getStatus()); return new PollLoginResponse(false, linkTask.getStatus()); } return linkTask; - }) - .subscribeOn(Schedulers.boundedElastic()) - .flatMap(result -> { - if (result instanceof PollLoginResponse) { - // 如果已经是响应对象,直接返回 - return Mono.just((PollLoginResponse) result); + }).subscribeOn(Schedulers.boundedElastic()); + } + + /** + * 获取任务对应的设备ID + */ + private String getDeviceIdForTask(LinkTask linkTask) { + if (linkTask.getMachineId() != null) { + // 如果已保存了设备编号,直接使用 + return linkTask.getMachineId(); + } + return null; + } + + /** + * 检查登录状态并处理结果 + */ + private Mono checkAndHandleLoginStatus(LinkTask linkTask) { + // 获取真实设备编号 + String realDeviceId = getDeviceIdForTask(linkTask); + + if (realDeviceId == null) { + log.error("无法获取设备编号: codeNo={}, machineId={}", linkTask.getCodeNo(), linkTask.getMachineId()); + return Mono.error(new RuntimeException("设备信息异常,无法检查上号状态")); + } + + // 调用脚本端检查上号状态 + log.info("调用脚本端检查上号状态: 代理code={}, 真实设备={}", linkTask.getCodeNo(), realDeviceId); + + return scriptClient.checkLoginStatus(realDeviceId) + .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); + + // 检查是否已上号 + 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()); + // 不影响后续流程,只记录警告日志 } - - LinkTask linkTask = (LinkTask) result; - - // 4. 获取真实设备编号 - 从代理code映射中获取 - final String realDeviceId; - if (linkTask.getMachineId() != null) { - // 如果已保存了设备编号,直接使用 - realDeviceId = linkTask.getMachineId(); - } else if (linkTask.getCodeNo() != null) { - // 如果没有设备编号,尝试从代理code获取 - realDeviceId = deviceCodeMappingService.getDeviceId(linkTask.getCodeNo()); - } else { - realDeviceId = null; + return handleSuccessfulLogin(linkTask, deviceId); + } else { + // 未上号,返回当前状态 + log.debug("尚未上号,返回当前状态"); + return new PollLoginResponse(false, "USING"); + } + } + + /** + * 处理成功登录的情况 + */ + private PollLoginResponse handleSuccessfulLogin(LinkTask linkTask, String deviceId) { + log.info("检测到已上号,更新状态为LOGGED_IN"); + + // 更新数据库状态 + linkTask.setStatus("LOGGED_IN"); + linkTask.setUpdatedAt(LocalDateTime.now()); + linkTaskMapper.updateById(linkTask); + + log.info("状态更新完成: codeNo={}, status=LOGGED_IN", linkTask.getCodeNo()); + + // 构建成功响应 + PollLoginResponse response = new PollLoginResponse(true, "LOGGED_IN", "SECOND", + new PollLoginResponse.AssetsInfo( + scriptClient.getAssetsBaseUrl(deviceId) + )); + + log.info("=== 轮询上号成功 ==="); + return response; + } + + /** + * 根据链接编号获取设备ID + * @param code 链接编号 + * @return 设备ID,如果链接不存在或未关联设备则返回null + */ + public String getMechainIdByCode(String code) { + if (code == null || code.trim().isEmpty()) { + log.warn("获取设备ID失败: code参数为空"); + return null; + } + + 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); - if (realDeviceId == null) { - log.error("无法获取设备编号: codeNo={}, machineId={}", linkTask.getCodeNo(), linkTask.getMachineId()); - throw new RuntimeException("设备信息异常,无法检查上号状态"); - } + return machineId; - // 5. 调用脚本端检查上号状态,使用真实设备编号 - log.info("调用脚本端检查上号状态: 代理code={}, 真实设备={}", linkTask.getCodeNo(), realDeviceId); - return scriptClient.checkLoginStatus(realDeviceId) - .map(loginResult -> { - log.info("脚本端返回结果: {}", loginResult); - - // 6. 如果返回"已上号",更新状态为LOGGED_IN - if ("已上号".equals(loginResult) || "已登录".equals(loginResult)) { - log.info("检测到已上号,更新状态为LOGGED_IN"); - - // 更新数据库状态 - linkTask.setStatus("LOGGED_IN"); - linkTask.setUpdatedAt(LocalDateTime.now()); - linkTaskMapper.updateById(linkTask); - - log.info("状态更新完成: codeNo={}, status=LOGGED_IN", linkTask.getCodeNo()); - - // 7. 返回成功响应和资源信息,使用ScriptClient统一管理资源链接 - PollLoginResponse response = new PollLoginResponse(true, "LOGGED_IN", "SECOND", - new PollLoginResponse.AssetsInfo( - scriptClient.getAssetsBaseUrl(realDeviceId) - )); - - log.info("=== 轮询上号成功 ==="); - return response; - } else { - // 未上号,返回当前状态 - log.debug("尚未上号,返回当前状态"); - return new PollLoginResponse(false, "USING"); - } - }) - .onErrorResume(error -> { - log.warn("调用脚本端检查上号状态失败: codeNo={}, error={}", - linkTask.getCodeNo(), error.getMessage()); - // 脚本端出错时,返回当前状态,不影响轮询 - return Mono.just(new PollLoginResponse(false, "USING")); - }); - }) - .doOnSuccess(response -> { - log.info("轮询上号完成: success={}, status={}", response.isSuccess(), response.getStatus()); - }) - .doOnError(error -> { - log.error("轮询上号失败: code={}, error={}", code, error.getMessage(), error); - }); + } catch (Exception e) { + log.error("根据code获取设备ID时发生异常: code={}, error={}", code, e.getMessage(), e); + return null; + } } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 211fb25..1d7732b 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -70,7 +70,8 @@ script: # 服务器配置 app: - base-url: "https://2.uzi0.cc" # 生产环境需要配置为实际域名 +# base-url: "https://2.uzi0.cc" # 生产环境需要配置为实际域名 + base-url: "http://localhost:18080" # 本地测试环境 image-save-path: "./images" # 图片保存路径 link: