feat: 增加选区和轮询上号功能

主要修改:
1. 在LinkController中新增选区和轮询上号接口,支持用户选择游戏区域和检查上号状态。
2. 在LinkStatusService中实现选区操作逻辑,包含空闲设备检查和状态更新。
3. 更新ScriptClient,增加获取设备二维码和检查设备状态的功能。
4. 修改SecurityConfig,允许选区和轮询上号接口公开访问。
5. 更新application.yml,添加应用基础URL配置。

技术细节:
- 新增SelectRegionResponse和PollLoginResponse DTO以支持新功能的返回格式。
- 通过脚本端接口实现选区和上号状态的检查与更新。
This commit is contained in:
zyh
2025-08-26 20:29:27 +08:00
parent 3847250c2b
commit 400d6757c8
12 changed files with 1288 additions and 143 deletions

View File

@@ -0,0 +1,139 @@
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

@@ -0,0 +1,162 @@
package com.gameplatform.server.service.device;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.gameplatform.server.model.dto.device.DeviceStatusResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 设备状态服务
*/
@Service
public class DeviceStatusService {
private static final Logger log = LoggerFactory.getLogger(DeviceStatusService.class);
private final ObjectMapper objectMapper;
// 设备编号解析正则
private static final Pattern DEVICE_PATTERN = Pattern.compile("^(f|s|g|d|ss|gg)(\\d+)$");
public DeviceStatusService(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
/**
* 解析设备状态JSON响应
*/
public DeviceStatusResponse parseDeviceStatus(String jsonResponse) {
try {
log.debug("开始解析设备状态响应");
JsonNode rootNode = objectMapper.readTree(jsonResponse);
Map<String, DeviceStatusResponse.DeviceInfo> devices = new HashMap<>();
List<String> availableDevices = new ArrayList<>();
// 遍历所有设备
Iterator<Map.Entry<String, JsonNode>> fields = rootNode.fields();
while (fields.hasNext()) {
Map.Entry<String, JsonNode> field = fields.next();
String deviceId = field.getKey();
JsonNode deviceNode = field.getValue();
// 解析设备信息
DeviceStatusResponse.DeviceInfo deviceInfo = parseDeviceInfo(deviceId, deviceNode);
devices.put(deviceId, deviceInfo);
// 检查是否空闲
if (deviceInfo.isAvailable()) {
availableDevices.add(deviceId);
}
}
// 构建响应
DeviceStatusResponse response = new DeviceStatusResponse();
response.setDevices(devices);
response.setAvailableDevices(availableDevices);
response.setTotalDevices(devices.size());
response.setAvailableCount(availableDevices.size());
log.info("设备状态解析完成: 总设备数={}, 空闲设备数={}, 空闲设备={}",
response.getTotalDevices(), response.getAvailableCount(), availableDevices);
return response;
} catch (JsonProcessingException e) {
log.error("解析设备状态JSON失败: {}", e.getMessage(), e);
throw new RuntimeException("解析设备状态失败", e);
}
}
/**
* 解析单个设备信息
*/
private DeviceStatusResponse.DeviceInfo parseDeviceInfo(String deviceId, JsonNode deviceNode) {
DeviceStatusResponse.DeviceInfo deviceInfo = new DeviceStatusResponse.DeviceInfo();
deviceInfo.setDeviceId(deviceId);
// 解析val和time
String val = deviceNode.get("val").asText();
String time = deviceNode.get("time").asText();
deviceInfo.setVal(val);
deviceInfo.setTime(time);
// 解析设备系列和序号
parseDeviceIdComponents(deviceId, deviceInfo);
// 判断是否空闲
boolean available = isDeviceAvailable(val);
deviceInfo.setAvailable(available);
log.debug("解析设备信息: deviceId={}, val={}, time={}, available={}, series={}, index={}",
deviceId, val, time, available, deviceInfo.getSeries(), deviceInfo.getIndex());
return deviceInfo;
}
/**
* 解析设备编号的组成部分
*/
private void parseDeviceIdComponents(String deviceId, DeviceStatusResponse.DeviceInfo deviceInfo) {
Matcher matcher = DEVICE_PATTERN.matcher(deviceId);
if (matcher.matches()) {
String series = matcher.group(1);
String indexStr = matcher.group(2);
deviceInfo.setSeries(series);
try {
deviceInfo.setIndex(Integer.parseInt(indexStr));
} catch (NumberFormatException e) {
log.warn("解析设备序号失败: deviceId={}, indexStr={}", deviceId, indexStr);
deviceInfo.setIndex(null);
}
} else {
log.warn("设备编号格式不匹配: deviceId={}", deviceId);
deviceInfo.setSeries(null);
deviceInfo.setIndex(null);
}
}
/**
* 判断设备是否空闲
*/
private boolean isDeviceAvailable(String val) {
return "空闲".equals(val);
}
/**
* 按系列分组空闲设备
*/
public Map<String, List<String>> groupAvailableDevicesBySeries(DeviceStatusResponse deviceStatus) {
Map<String, List<String>> groupedDevices = new HashMap<>();
for (String deviceId : deviceStatus.getAvailableDevices()) {
DeviceStatusResponse.DeviceInfo deviceInfo = deviceStatus.getDevices().get(deviceId);
if (deviceInfo != null && deviceInfo.getSeries() != null) {
String series = deviceInfo.getSeries();
groupedDevices.computeIfAbsent(series, k -> new ArrayList<>()).add(deviceId);
}
}
return groupedDevices;
}
/**
* 获取指定系列的空闲设备
*/
public List<String> getAvailableDevicesBySeries(DeviceStatusResponse deviceStatus, String series) {
return deviceStatus.getAvailableDevices().stream()
.filter(deviceId -> {
DeviceStatusResponse.DeviceInfo deviceInfo = deviceStatus.getDevices().get(deviceId);
return deviceInfo != null && series.equals(deviceInfo.getSeries());
})
.sorted() // 按设备编号排序
.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
}
}

View File

@@ -1,5 +1,7 @@
package com.gameplatform.server.service.external;
import com.gameplatform.server.model.dto.device.DeviceStatusResponse;
import com.gameplatform.server.service.device.DeviceStatusService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
@@ -17,13 +19,19 @@ public class ScriptClient {
private final WebClient webClient;
private final String baseUrl;
private final String appBaseUrl;
private final DeviceStatusService deviceStatusService;
public ScriptClient(
@Value("${script.base-url}") String baseUrl,
@Value("${script.connect-timeout-ms:3000}") int connectTimeoutMs,
@Value("${script.read-timeout-ms:5000}") int readTimeoutMs
@Value("${script.read-timeout-ms:5000}") int readTimeoutMs,
@Value("${app.base-url}") String appBaseUrl,
DeviceStatusService deviceStatusService
) {
this.baseUrl = baseUrl;
this.appBaseUrl = appBaseUrl;
this.deviceStatusService = deviceStatusService;
this.webClient = WebClient.builder()
.baseUrl(baseUrl)
.exchangeStrategies(ExchangeStrategies.builder()
@@ -57,35 +65,61 @@ public class ScriptClient {
}
/**
* 检查空闲设备
* 检查空闲设备(返回原始字符串)
*/
public Mono<String> checkAvailableDevice() {
String url = "http://36.138.184.60:1234/yijianwan_netfile/readAllMsg?文件名=判断分数";
log.debug("检查空闲设备: {}", url);
return webClient.get()
.uri(url)
.accept(MediaType.TEXT_PLAIN)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(String.class)
.timeout(Duration.ofSeconds(10))
.doOnSuccess(result -> log.debug("检查空闲设备成功: {}", result))
.doOnError(e -> log.warn("检查空闲设备失败: {}", e.toString()));
}
/**
* 检查空闲设备(解析后的结构化数据)
*/
public Mono<DeviceStatusResponse> checkAvailableDeviceStatus() {
return checkAvailableDevice()
.map(jsonResponse -> deviceStatusService.parseDeviceStatus(jsonResponse))
.doOnSuccess(deviceStatus -> {
log.info("设备状态检查完成: 总设备数={}, 空闲设备数={}",
deviceStatus.getTotalDevices(), deviceStatus.getAvailableCount());
if (deviceStatus.getAvailableCount() > 0) {
log.info("空闲设备列表: {}", deviceStatus.getAvailableDevices());
}
})
.doOnError(e -> log.error("设备状态解析失败: {}", e.getMessage(), e));
}
/**
* 选区操作
* 选区操作 - 使用空闲设备编号
* @param deviceId 空闲设备编号 (如 f1, ss9)
* @param region 区域参数 (Q或V)
* @return 选区结果
*/
public Mono<String> selectRegion(String codeNo, String region) {
String url = String.format("http://36.138.184.60:1234/yijianwan_netfile/saveMsg?文件名=判断系统&编号=%s", region);
log.debug("选区操作: codeNo={}, region={}, url={}", codeNo, region, url);
return webClient.post()
public Mono<String> selectRegion(String deviceId, String region) {
// 构建选区URL使用设备编号作为参数名区域作为参数值
// 示例: http://36.138.184.60:1234/yijianwan_netfile/saveMsg?文件名=判断系统&f1=Q
String url = String.format("http://36.138.184.60:1234/yijianwan_netfile/saveMsg?文件名=判断系统&%s=%s", deviceId, region);
log.info("选区操作: 设备={}, 区域={}, url={}", deviceId, region, url);
return webClient.get() // 根据您的curl示例这应该是GET请求
.uri(url)
.accept(MediaType.TEXT_PLAIN)
.retrieve()
.bodyToMono(String.class)
.timeout(Duration.ofSeconds(10))
.doOnSuccess(result -> log.debug("选区操作成功: codeNo={}, region={}, result={}", codeNo, region, result))
.doOnError(e -> log.warn("选区操作失败: codeNo={}, region={}, error={}", codeNo, region, e.toString()));
.doOnSuccess(result -> {
log.info("选区操作成功: 设备={}, 区域={}, 结果={}", deviceId, region, result);
})
.doOnError(e -> {
log.error("选区操作失败: 设备={}, 区域={}, 错误={}", deviceId, region, e.toString());
});
}
/**
@@ -105,28 +139,64 @@ public class ScriptClient {
}
/**
* 检查上号状态
* 检查设备是否已上号 - 根据您提供的API示例
* URL格式: http://36.138.184.60:1234/yijianwan_netfile/readMsg?文件名=判断上号&对象名=f1
* 返回: "未上号" 或 其他状态
*
* @param deviceId 设备编号 (真实设备编号,如 f1, ss9)
* @return 上号状态文本
*/
public Mono<String> checkLoginStatus(String codeNo) {
String url = String.format("http://36.138.184.60:1234/yijianwan_netfile/readMsg?文件名=判断上号&对象名=%s", codeNo);
log.debug("检查上号状态: codeNo={}, url={}", codeNo, url);
public Mono<String> checkLoginStatus(String deviceId) {
String url = String.format("http://36.138.184.60:1234/yijianwan_netfile/readMsg?文件名=判断上号&对象名=%s", deviceId);
log.debug("检查设备上号状态: 设备={}, url={}", deviceId, 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()));
.timeout(Duration.ofSeconds(10))
.doOnSuccess(result -> log.debug("检查设备上号状态成功: 设备={}, 状态={}", deviceId, result))
.doOnError(e -> log.warn("检查设备上号状态失败: 设备={}, 错误={}", deviceId, e.toString()));
}
/**
* 获取二维码URL带时间戳防缓存
* 获取二维码URL带时间戳防缓存- 直接链接到脚本服务器
*/
public String getQrCodeUrl(String codeNo) {
long timestamp = System.currentTimeMillis();
return String.format("http://36.138.184.60:12345/%s/二维码.png?t=%d", codeNo, timestamp);
}
/**
* 获取代理二维码URL通过本服务器代理使用代理code隐藏真实设备编号
*/
public String getProxyQrCodeUrl(String proxyCode) {
long timestamp = System.currentTimeMillis();
return String.format("%s/api/link/qr/%s?t=%d", appBaseUrl, proxyCode, timestamp);
}
/**
* 获取真实设备的二维码数据
* @param deviceId 真实设备编号 (如 f1, ss9)
* @return 二维码图片数据
*/
public Mono<byte[]> getDeviceQrCode(String deviceId) {
String path = String.format("/%s/二维码.png", deviceId);
log.debug("获取设备二维码: 设备={}, path={}", deviceId, path);
return webClient.get()
.uri(path)
.accept(MediaType.IMAGE_PNG)
.retrieve()
.bodyToMono(byte[].class)
.timeout(Duration.ofSeconds(10))
.doOnSuccess(data -> {
log.debug("获取设备二维码成功: 设备={}, 数据大小={}字节", deviceId, data.length);
})
.doOnError(e -> {
log.error("获取设备二维码失败: 设备={}, 错误={}", deviceId, e.toString());
});
}
/**
* 获取目标分数

View File

@@ -2,13 +2,17 @@ package com.gameplatform.server.service.link;
import com.gameplatform.server.mapper.agent.LinkBatchMapper;
import com.gameplatform.server.mapper.agent.LinkTaskMapper;
import com.gameplatform.server.model.dto.device.DeviceStatusResponse;
import com.gameplatform.server.model.dto.link.BatchDeleteResponse;
import com.gameplatform.server.model.dto.link.LinkStatusResponse;
import com.gameplatform.server.model.dto.link.PollLoginResponse;
import com.gameplatform.server.model.dto.link.SelectRegionResponse;
import com.gameplatform.server.model.dto.link.UserLinkStatusResponse;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
@@ -30,8 +34,8 @@ public class LinkStatusService {
private final LinkTaskMapper linkTaskMapper;
private final LinkBatchMapper linkBatchMapper;
private final ScriptClient scriptClient;
private final DeviceCodeMappingService deviceCodeMappingService;
// 状态描述映射
private static final Map<String, String> STATUS_DESC_MAP = new HashMap<>();
@@ -44,10 +48,11 @@ public class LinkStatusService {
}
public LinkStatusService(LinkTaskMapper linkTaskMapper, LinkBatchMapper linkBatchMapper,
ScriptClient scriptClient) {
ScriptClient scriptClient, DeviceCodeMappingService deviceCodeMappingService) {
this.linkTaskMapper = linkTaskMapper;
this.linkBatchMapper = linkBatchMapper;
this.scriptClient = scriptClient;
this.deviceCodeMappingService = deviceCodeMappingService;
}
/**
@@ -288,19 +293,23 @@ public class LinkStatusService {
return response;
}
// 3. 如果状态不是NEW执行自动刷新逻辑
if (!"NEW".equals(linkTask.getStatus())) {
log.info("链接状态不是NEW执行自动刷新逻辑");
performAutoRefresh(linkTask);
}
// 4. 如果状态是USING重新获取二维码
// 3. 根据状态执行相应逻辑
if ("USING".equals(linkTask.getStatus())) {
log.info("链接状态是USING重新获取二维码");
updateQrCodeInfo(linkTask);
// 如果是USING状态检查二维码是否过期过期则刷新
if (linkTask.getQrExpireAt() != null && linkTask.getQrExpireAt().isBefore(LocalDateTime.now())) {
log.info("二维码已过期,执行自动刷新重置选区状态");
performAutoRefresh(linkTask);
} else {
// 二维码还未过期,更新二维码信息
log.info("链接状态是USING重新获取二维码");
updateQrCodeInfo(linkTask);
}
} else if ("LOGGED_IN".equals(linkTask.getStatus()) || "REFUNDED".equals(linkTask.getStatus())) {
// 已上号或已退款状态,不需要刷新
log.info("链接状态为 {},不需要刷新", linkTask.getStatus());
}
// 5. 构建响应
// 4. 构建响应
UserLinkStatusResponse response = buildUserStatusResponse(linkTask);
log.info("=== 用户端链接状态查询完成 ===");
log.info("返回状态: {}, view: {}", response.getStatus(), response.getView());
@@ -316,31 +325,41 @@ public class LinkStatusService {
/**
* 执行自动刷新逻辑
* 刷新时会重置选区状态,让用户重新选择区域
*/
private void performAutoRefresh(LinkTask linkTask) {
try {
log.info("开始执行刷新操作");
log.info("开始执行刷新操作,将重置选区状态");
// 调用脚本端刷新
// 1. 调用脚本端刷新
String refreshResult = scriptClient.refresh(linkTask.getCodeNo()).block();
log.info("脚本端刷新结果: {}", refreshResult);
// 更新刷新状态
linkTask.setNeedRefresh(true);
linkTask.setRefreshTime(LocalDateTime.now());
linkTask.setUpdatedAt(LocalDateTime.now());
// 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);
// 等待10秒
log.info("刷新完成等待10秒...");
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());
log.warn("刷新等待被中断: {}", e.getMessage());
} catch (Exception e) {
log.warn("执行刷新操作失败: {}", e.getMessage());
// 刷新失败不影响后续流程
// 刷新失败不影响后续流程,但记录错误日志
}
}
@@ -380,7 +399,7 @@ public class LinkStatusService {
// 如果状态是USING设置二维码信息
if ("USING".equals(linkTask.getStatus()) && linkTask.getQrCreatedAt() != null) {
UserLinkStatusResponse.QrInfo qrInfo = new UserLinkStatusResponse.QrInfo();
qrInfo.setUrl(scriptClient.getQrCodeUrl(linkTask.getCodeNo()));
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());
@@ -420,4 +439,231 @@ public class LinkStatusService {
return "FIRST";
}
}
/**
* 选区操作
*/
public Mono<SelectRegionResponse> selectRegion(String code, String region) {
return Mono.fromCallable(() -> doSelectRegion(code, region))
.subscribeOn(Schedulers.boundedElastic());
}
private SelectRegionResponse doSelectRegion(String code, String region) {
log.info("=== 开始选区操作 ===");
log.info("code: {}, region: {}", code, region);
try {
// 1. 验证参数
if (code == null || code.trim().isEmpty()) {
throw new IllegalArgumentException("code不能为空");
}
if (region == null || (!region.equals("Q") && !region.equals("V"))) {
throw new IllegalArgumentException("region只能是Q或V");
}
// 2. 查询链接任务
LinkTask linkTask = linkTaskMapper.findByCodeNo(code.trim());
if (linkTask == null) {
log.error("链接任务不存在: code={}", code);
throw new IllegalArgumentException("链接不存在");
}
log.info("查询到链接任务: id={}, codeNo={}, status={}, needRefresh={}",
linkTask.getId(), linkTask.getCodeNo(), linkTask.getStatus(), linkTask.getNeedRefresh());
// 3. 检查链接状态只有NEW状态才能选区
if (!"NEW".equals(linkTask.getStatus())) {
log.error("链接状态不正确,无法选区: status={}", linkTask.getStatus());
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;
}
}
// 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. 选择一个空闲设备
String selectedDevice = deviceStatus.getAvailableDevices().get(0); // 选择第一个空闲设备
log.info("选择设备: {}", selectedDevice);
// 8. 为选中的设备创建代理code
String proxyCode = deviceCodeMappingService.createProxyCode(selectedDevice);
log.info("为设备 {} 创建代理code: {}", selectedDevice, proxyCode);
// 9. 调用脚本端选区,使用选中的设备
log.info("开始调用脚本端选区,设备={}, 区域={}", selectedDevice, region);
String selectResult = scriptClient.selectRegion(selectedDevice, region).block();
log.info("脚本端选区结果: {}", selectResult);
// 10. 等待脚本端生成二维码(这里可以添加轮询逻辑)
log.info("等待脚本端生成二维码等待3秒...");
Thread.sleep(3000);
// 11. 更新数据库状态为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.setNeedRefresh(false);
linkTask.setUpdatedAt(now);
// 在machineId字段保存真实设备编号便于调试和维护
linkTask.setMachineId(selectedDevice);
linkTaskMapper.update(linkTask);
log.info("链接状态更新成功: status=USING, region={}, proxyCode={}, device={}", region, proxyCode, selectedDevice);
// 12. 构建响应
SelectRegionResponse response = new SelectRegionResponse(true, "选区成功");
response.setQrCodeUrl(scriptClient.getProxyQrCodeUrl(proxyCode));
response.setQrCreatedAt(now);
response.setQrExpireAt(linkTask.getQrExpireAt());
response.setStatus("USING");
response.setRegion(region);
response.setQrDelaySeconds(5); // 客户端收到响应后等待5秒再请求二维码
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);
throw e;
}
}
/**
* 轮询上号接口
*/
public Mono<PollLoginResponse> pollLogin(String code) {
log.info("=== 开始轮询上号 ===");
log.info("code: {}", code);
return Mono.fromCallable(() -> {
// 1. 验证code参数
if (code == null || code.trim().isEmpty()) {
throw new IllegalArgumentException("参数错误code不能为空");
}
// 2. 获取链接任务
LinkTask linkTask = linkTaskMapper.findByCodeNo(code);
if (linkTask == null) {
throw new IllegalArgumentException("链接不存在");
}
log.info("找到链接任务: id={}, status={}, codeNo={}",
linkTask.getId(), linkTask.getStatus(), linkTask.getCodeNo());
// 3. 检查链接状态只有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);
}
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;
}
if (realDeviceId == null) {
log.error("无法获取设备编号: codeNo={}, machineId={}", linkTask.getCodeNo(), linkTask.getMachineId());
throw new RuntimeException("设备信息异常,无法检查上号状态");
}
// 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. 返回成功响应和资源信息,使用真实设备编号构建资源链接
PollLoginResponse response = new PollLoginResponse(true, "LOGGED_IN", "SECOND",
new PollLoginResponse.AssetsInfo(
String.format("http://36.138.184.60:12345/%s/", 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);
});
}
}