feat: 增加选区和轮询上号功能
主要修改: 1. 在LinkController中新增选区和轮询上号接口,支持用户选择游戏区域和检查上号状态。 2. 在LinkStatusService中实现选区操作逻辑,包含空闲设备检查和状态更新。 3. 更新ScriptClient,增加获取设备二维码和检查设备状态的功能。 4. 修改SecurityConfig,允许选区和轮询上号接口公开访问。 5. 更新application.yml,添加应用基础URL配置。 技术细节: - 新增SelectRegionResponse和PollLoginResponse DTO以支持新功能的返回格式。 - 通过脚本端接口实现选区和上号状态的检查与更新。
This commit is contained in:
@@ -6,18 +6,26 @@ import com.gameplatform.server.model.dto.link.LinkGenerateRequest;
|
|||||||
import com.gameplatform.server.model.dto.link.LinkGenerateResponse;
|
import com.gameplatform.server.model.dto.link.LinkGenerateResponse;
|
||||||
import com.gameplatform.server.model.dto.link.LinkListRequest;
|
import com.gameplatform.server.model.dto.link.LinkListRequest;
|
||||||
import com.gameplatform.server.model.dto.link.LinkListResponse;
|
import com.gameplatform.server.model.dto.link.LinkListResponse;
|
||||||
import com.gameplatform.server.model.dto.link.LinkStatusResponse;
|
|
||||||
|
import com.gameplatform.server.model.dto.link.PollLoginResponse;
|
||||||
|
import com.gameplatform.server.model.dto.link.SelectRegionRequest;
|
||||||
|
import com.gameplatform.server.model.dto.link.SelectRegionResponse;
|
||||||
import com.gameplatform.server.model.dto.link.UserLinkStatusResponse;
|
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 io.jsonwebtoken.Claims;
|
import io.jsonwebtoken.Claims;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import javax.validation.Valid;
|
import javax.validation.Valid;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
@@ -31,13 +39,19 @@ 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;
|
||||||
|
|
||||||
public LinkController(LinkGenerationService linkGenerationService,
|
public LinkController(LinkGenerationService linkGenerationService,
|
||||||
LinkStatusService linkStatusService,
|
LinkStatusService linkStatusService,
|
||||||
LinkListService linkListService) {
|
LinkListService linkListService,
|
||||||
|
DeviceCodeMappingService deviceCodeMappingService,
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/list")
|
@GetMapping("/list")
|
||||||
@@ -166,70 +180,38 @@ public class LinkController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{codeNo}/status")
|
// @GetMapping("/{codeNo}/status")
|
||||||
@Operation(summary = "获取链接状态", description = "根据链接编号获取链接的详细状态信息,包括过期时间、奖励点数、当前状态等")
|
// @Operation(summary = "获取链接状态", description = "根据链接编号获取链接的详细状态信息,包括过期时间、奖励点数、当前状态等")
|
||||||
public Mono<LinkStatusResponse> getLinkStatus(@PathVariable("codeNo") String codeNo) {
|
// public Mono<LinkStatusResponse> getLinkStatus(@PathVariable("codeNo") String codeNo) {
|
||||||
log.info("=== 开始查询链接状态 ===");
|
// log.info("=== 开始查询链接状态 ===");
|
||||||
log.info("链接编号: {}", codeNo);
|
// log.info("链接编号: {}", codeNo);
|
||||||
|
//
|
||||||
return linkStatusService.getLinkStatus(codeNo)
|
// return linkStatusService.getLinkStatus(codeNo)
|
||||||
.doOnSuccess(response -> {
|
// .doOnSuccess(response -> {
|
||||||
log.info("链接状态查询成功: codeNo={}, status={}, isExpired={}",
|
// log.info("链接状态查询成功: codeNo={}, status={}, isExpired={}",
|
||||||
codeNo, response.getStatus(), response.getIsExpired());
|
// codeNo, response.getStatus(), response.getIsExpired());
|
||||||
})
|
// })
|
||||||
.doOnError(error -> {
|
// .doOnError(error -> {
|
||||||
log.error("链接状态查询失败: codeNo={}, error={}", codeNo, error.getMessage(), error);
|
// log.error("链接状态查询失败: codeNo={}, error={}", codeNo, error.getMessage(), error);
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
@GetMapping("/{codeNo}/exists")
|
// @GetMapping("/{codeNo}/exists")
|
||||||
@Operation(summary = "检查链接是否存在", description = "检查指定链接编号是否存在")
|
// @Operation(summary = "检查链接是否存在", description = "检查指定链接编号是否存在")
|
||||||
public Mono<Boolean> isLinkExists(@PathVariable("codeNo") String codeNo) {
|
// public Mono<Boolean> isLinkExists(@PathVariable("codeNo") String codeNo) {
|
||||||
log.debug("检查链接是否存在: codeNo={}", codeNo);
|
// log.debug("检查链接是否存在: codeNo={}", codeNo);
|
||||||
return linkStatusService.isLinkExists(codeNo);
|
// return linkStatusService.isLinkExists(codeNo);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
@GetMapping("/{codeNo}/valid")
|
// @GetMapping("/{codeNo}/valid")
|
||||||
@Operation(summary = "检查链接是否有效", description = "检查指定链接是否有效(未过期且状态正常)")
|
// @Operation(summary = "检查链接是否有效", description = "检查指定链接是否有效(未过期且状态正常)")
|
||||||
public Mono<Boolean> isLinkValid(@PathVariable("codeNo") String codeNo) {
|
// public Mono<Boolean> isLinkValid(@PathVariable("codeNo") String codeNo) {
|
||||||
log.debug("检查链接是否有效: codeNo={}", codeNo);
|
// log.debug("检查链接是否有效: codeNo={}", codeNo);
|
||||||
return linkStatusService.isLinkValid(codeNo);
|
// return linkStatusService.isLinkValid(codeNo);
|
||||||
}
|
// }
|
||||||
|
@DeleteMapping("/{codeNo}")
|
||||||
@GetMapping("/status")
|
@Operation(summary = "删除链接", description = "删除指定的链接,用户只能删除自己创建的链接")
|
||||||
@Operation(summary = "用户端获取链接状态", description = "根据链接ID或链接编号获取链接状态,包含自动刷新逻辑,用于用户端页面")
|
public Mono<Boolean> deleteLink(@PathVariable("codeNo") String codeNo, Authentication authentication) {
|
||||||
public Mono<UserLinkStatusResponse> getUserLinkStatus(
|
|
||||||
@RequestParam(value = "linkId", required = false) Long linkId,
|
|
||||||
@RequestParam(value = "codeNo", required = false) String codeNo,
|
|
||||||
@RequestParam(value = "code", required = false) String code) {
|
|
||||||
log.info("=== 用户端获取链接状态 ===");
|
|
||||||
log.info("linkId: {}, codeNo: {}, code: {}", linkId, codeNo, code);
|
|
||||||
|
|
||||||
// 如果提供了code参数,则将其作为codeNo使用
|
|
||||||
String actualCodeNo = codeNo;
|
|
||||||
if (actualCodeNo == null || actualCodeNo.trim().isEmpty()) {
|
|
||||||
actualCodeNo = code;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证参数:linkId和实际的codeNo至少提供一个
|
|
||||||
if (linkId == null && (actualCodeNo == null || actualCodeNo.trim().isEmpty())) {
|
|
||||||
log.error("参数错误:linkId、codeNo或code至少提供一个");
|
|
||||||
return Mono.error(new IllegalArgumentException("参数错误:linkId、codeNo或code至少提供一个"));
|
|
||||||
}
|
|
||||||
|
|
||||||
return linkStatusService.getUserLinkStatus(linkId, actualCodeNo)
|
|
||||||
.doOnSuccess(response -> {
|
|
||||||
log.info("用户端链接状态查询成功: status={}, view={}, needRefresh={}",
|
|
||||||
response.getStatus(), response.getView(), response.getNeedRefresh());
|
|
||||||
})
|
|
||||||
.doOnError(error -> {
|
|
||||||
log.error("用户端链接状态查询失败: {}", error.getMessage(), error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@DeleteMapping("/{codeNo}")
|
|
||||||
@Operation(summary = "删除链接", description = "删除指定的链接,用户只能删除自己创建的链接")
|
|
||||||
public Mono<Boolean> deleteLink(@PathVariable("codeNo") String codeNo, Authentication authentication) {
|
|
||||||
log.info("=== 开始删除链接 ===");
|
log.info("=== 开始删除链接 ===");
|
||||||
log.info("链接编号: {}", codeNo);
|
log.info("链接编号: {}", codeNo);
|
||||||
|
|
||||||
@@ -268,7 +250,7 @@ public class LinkController {
|
|||||||
log.error("删除链接时发生错误: codeNo={}, agentId={}, error={}",
|
log.error("删除链接时发生错误: codeNo={}, agentId={}, error={}",
|
||||||
codeNo, agentId, error.getMessage(), error);
|
codeNo, agentId, error.getMessage(), error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/batch-delete")
|
@PostMapping("/batch-delete")
|
||||||
@Operation(summary = "批量删除链接", description = "批量删除指定的链接,用户只能删除自己创建的链接,最多一次删除100个")
|
@Operation(summary = "批量删除链接", description = "批量删除指定的链接,用户只能删除自己创建的链接,最多一次删除100个")
|
||||||
@@ -315,6 +297,119 @@ public class LinkController {
|
|||||||
agentId, request.getCodeNos(), error.getMessage(), error);
|
agentId, request.getCodeNos(), error.getMessage(), error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/status")
|
||||||
|
@Operation(summary = "用户端获取链接状态", description = "根据链接ID或链接编号获取链接状态,包含自动刷新逻辑,用于用户端页面")
|
||||||
|
public Mono<UserLinkStatusResponse> getUserLinkStatus(
|
||||||
|
@RequestParam(value = "linkId", required = false) Long linkId,
|
||||||
|
@RequestParam(value = "codeNo", required = false) String codeNo,
|
||||||
|
@RequestParam(value = "code", required = false) String code) {
|
||||||
|
log.info("=== 用户端获取链接状态 ===");
|
||||||
|
log.info("linkId: {}, codeNo: {}, code: {}", linkId, codeNo, code);
|
||||||
|
|
||||||
|
// 如果提供了code参数,则将其作为codeNo使用
|
||||||
|
String actualCodeNo = codeNo;
|
||||||
|
if (actualCodeNo == null || actualCodeNo.trim().isEmpty()) {
|
||||||
|
actualCodeNo = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证参数:linkId和实际的codeNo至少提供一个
|
||||||
|
if (linkId == null && (actualCodeNo == null || actualCodeNo.trim().isEmpty())) {
|
||||||
|
log.error("参数错误:linkId、codeNo或code至少提供一个");
|
||||||
|
return Mono.error(new IllegalArgumentException("参数错误:linkId、codeNo或code至少提供一个"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return linkStatusService.getUserLinkStatus(linkId, actualCodeNo)
|
||||||
|
.doOnSuccess(response -> {
|
||||||
|
log.info("用户端链接状态查询成功: status={}, view={}, needRefresh={}",
|
||||||
|
response.getStatus(), response.getView(), response.getNeedRefresh());
|
||||||
|
})
|
||||||
|
.doOnError(error -> {
|
||||||
|
log.error("用户端链接状态查询失败: {}", error.getMessage(), error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@PostMapping("/select-region")
|
||||||
|
@Operation(summary = "选择区域", description = "用户选择游戏区域(Q或V),选区成功后生成二维码")
|
||||||
|
public Mono<SelectRegionResponse> selectRegion(@Valid @RequestBody SelectRegionRequest request) {
|
||||||
|
log.info("=== 开始处理选区请求 ===");
|
||||||
|
log.info("请求参数: code={}, region={}", request.getCode(), request.getRegion());
|
||||||
|
|
||||||
|
return linkStatusService.selectRegion(request.getCode(), request.getRegion())
|
||||||
|
.doOnSuccess(response -> {
|
||||||
|
log.info("选区请求处理成功: success={}, status={}, region={}",
|
||||||
|
response.isSuccess(), response.getStatus(), response.getRegion());
|
||||||
|
})
|
||||||
|
.doOnError(error -> {
|
||||||
|
log.error("选区请求处理失败: code={}, region={}, error={}",
|
||||||
|
request.getCode(), request.getRegion(), error.getMessage(), error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/poll-login")
|
||||||
|
@Operation(summary = "轮询上号", description = "轮询检查用户是否已上号,仅在USING状态下有效")
|
||||||
|
public Mono<PollLoginResponse> pollLogin(@RequestParam("code") String code) {
|
||||||
|
log.info("=== 开始轮询上号 ===");
|
||||||
|
log.info("请求参数: code={}", code);
|
||||||
|
|
||||||
|
return linkStatusService.pollLogin(code)
|
||||||
|
.doOnSuccess(response -> {
|
||||||
|
log.info("轮询上号完成: success={}, status={}, view={}",
|
||||||
|
response.isSuccess(), response.getStatus(), response.getView());
|
||||||
|
})
|
||||||
|
.doOnError(error -> {
|
||||||
|
log.error("轮询上号失败: code={}, error={}",
|
||||||
|
code, error.getMessage(), error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 代理二维码获取接口
|
||||||
|
* 通过代理code获取真实设备的二维码,避免暴露设备编号
|
||||||
|
*/
|
||||||
|
@GetMapping("/qr/{proxyCode}")
|
||||||
|
@Operation(summary = "获取二维码", description = "通过代理code获取设备二维码,用于扫码上号")
|
||||||
|
public Mono<ResponseEntity<byte[]>> getProxyQrCode(@PathVariable("proxyCode") String proxyCode) {
|
||||||
|
log.info("=== 获取代理二维码 ===");
|
||||||
|
log.info("代理code: {}", proxyCode);
|
||||||
|
|
||||||
|
// 验证代理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);
|
||||||
|
return Mono.just(ResponseEntity.notFound().build());
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("代理code {} 对应设备: {}", proxyCode, deviceId);
|
||||||
|
|
||||||
|
// 获取真实设备的二维码
|
||||||
|
return scriptClient.getDeviceQrCode(deviceId)
|
||||||
|
.map(qrData -> {
|
||||||
|
log.info("获取设备 {} 二维码成功,大小: {} 字节", deviceId, qrData.length);
|
||||||
|
|
||||||
|
// 设置响应头
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.IMAGE_PNG);
|
||||||
|
headers.setCacheControl("no-cache, no-store, must-revalidate");
|
||||||
|
headers.setPragma("no-cache");
|
||||||
|
headers.setExpires(0);
|
||||||
|
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.headers(headers)
|
||||||
|
.body(qrData);
|
||||||
|
})
|
||||||
|
.onErrorResume(error -> {
|
||||||
|
log.error("获取设备 {} 二维码失败: {}", deviceId, error.getMessage(), error);
|
||||||
|
return Mono.just(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build());
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package com.gameplatform.server.model.dto.device;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备状态响应
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class DeviceStatusResponse {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备状态映射 (设备编号 -> 设备信息)
|
||||||
|
*/
|
||||||
|
private Map<String, DeviceInfo> devices;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 空闲设备列表
|
||||||
|
*/
|
||||||
|
private List<String> availableDevices;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 总设备数
|
||||||
|
*/
|
||||||
|
private int totalDevices;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 空闲设备数
|
||||||
|
*/
|
||||||
|
private int availableCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单个设备信息
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public static class DeviceInfo {
|
||||||
|
/**
|
||||||
|
* 设备编号
|
||||||
|
*/
|
||||||
|
private String deviceId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备状态值
|
||||||
|
*/
|
||||||
|
private String val;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态更新时间
|
||||||
|
*/
|
||||||
|
private String time;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否空闲
|
||||||
|
*/
|
||||||
|
private boolean available;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备系列 (f, s, g, d, ss, gg)
|
||||||
|
*/
|
||||||
|
private String series;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备序号 (0-9)
|
||||||
|
*/
|
||||||
|
private Integer index;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
package com.gameplatform.server.model.dto.link;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 轮询上号接口响应
|
||||||
|
*/
|
||||||
|
@Schema(description = "轮询上号接口响应")
|
||||||
|
public class PollLoginResponse {
|
||||||
|
|
||||||
|
@Schema(description = "是否上号成功", example = "false")
|
||||||
|
private boolean success;
|
||||||
|
|
||||||
|
@Schema(description = "当前状态", example = "USING")
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
@Schema(description = "前端渲染建议", example = "SECOND")
|
||||||
|
private String view;
|
||||||
|
|
||||||
|
@Schema(description = "静态资源信息")
|
||||||
|
private AssetsInfo assets;
|
||||||
|
|
||||||
|
public PollLoginResponse() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public PollLoginResponse(boolean success, String status) {
|
||||||
|
this.success = success;
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PollLoginResponse(boolean success, String status, String view, AssetsInfo assets) {
|
||||||
|
this.success = success;
|
||||||
|
this.status = status;
|
||||||
|
this.view = view;
|
||||||
|
this.assets = assets;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSuccess() {
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSuccess(boolean success) {
|
||||||
|
this.success = success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(String status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getView() {
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setView(String view) {
|
||||||
|
this.view = view;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AssetsInfo getAssets() {
|
||||||
|
return assets;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAssets(AssetsInfo assets) {
|
||||||
|
this.assets = assets;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 静态资源信息
|
||||||
|
*/
|
||||||
|
@Schema(description = "静态资源信息")
|
||||||
|
public static class AssetsInfo {
|
||||||
|
|
||||||
|
@Schema(description = "资源基础URL", example = "http://36.138.184.60:12345/ABC123/")
|
||||||
|
private String base;
|
||||||
|
|
||||||
|
@Schema(description = "首次主页图片", example = "首次主页.png")
|
||||||
|
private String firstHome;
|
||||||
|
|
||||||
|
@Schema(description = "首次赏金图片", example = "首次赏金.png")
|
||||||
|
private String firstBonus;
|
||||||
|
|
||||||
|
@Schema(description = "中途赏金图片", example = "中途赏金.png")
|
||||||
|
private String midBonus;
|
||||||
|
|
||||||
|
@Schema(description = "结束赏金图片", example = "结束赏金.png")
|
||||||
|
private String endBonus;
|
||||||
|
|
||||||
|
public AssetsInfo() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public AssetsInfo(String base) {
|
||||||
|
this.base = base;
|
||||||
|
this.firstHome = "首次主页.png";
|
||||||
|
this.firstBonus = "首次赏金.png";
|
||||||
|
this.midBonus = "中途赏金.png";
|
||||||
|
this.endBonus = "结束赏金.png";
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBase() {
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBase(String base) {
|
||||||
|
this.base = base;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFirstHome() {
|
||||||
|
return firstHome;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFirstHome(String firstHome) {
|
||||||
|
this.firstHome = firstHome;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFirstBonus() {
|
||||||
|
return firstBonus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFirstBonus(String firstBonus) {
|
||||||
|
this.firstBonus = firstBonus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMidBonus() {
|
||||||
|
return midBonus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMidBonus(String midBonus) {
|
||||||
|
this.midBonus = midBonus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEndBonus() {
|
||||||
|
return endBonus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEndBonus(String endBonus) {
|
||||||
|
this.endBonus = endBonus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.gameplatform.server.model.dto.link;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import javax.validation.constraints.NotBlank;
|
||||||
|
import javax.validation.constraints.Pattern;
|
||||||
|
|
||||||
|
@Schema(description = "选区请求")
|
||||||
|
public class SelectRegionRequest {
|
||||||
|
|
||||||
|
@NotBlank(message = "code不能为空")
|
||||||
|
@Schema(description = "链接编码", example = "66L8NM3L")
|
||||||
|
private String code;
|
||||||
|
|
||||||
|
@NotBlank(message = "region不能为空")
|
||||||
|
@Pattern(regexp = "^[QV]$", message = "region只能是Q或V")
|
||||||
|
@Schema(description = "选择的区域", example = "Q", allowableValues = {"Q", "V"})
|
||||||
|
private String region;
|
||||||
|
|
||||||
|
public SelectRegionRequest() {}
|
||||||
|
|
||||||
|
public SelectRegionRequest(String code, String region) {
|
||||||
|
this.code = code;
|
||||||
|
this.region = region;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCode(String code) {
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRegion() {
|
||||||
|
return region;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRegion(String region) {
|
||||||
|
this.region = region;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "SelectRegionRequest{" +
|
||||||
|
"code='" + code + '\'' +
|
||||||
|
", region='" + region + '\'' +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
package com.gameplatform.server.model.dto.link;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Schema(description = "选区响应")
|
||||||
|
public class SelectRegionResponse {
|
||||||
|
|
||||||
|
@Schema(description = "操作是否成功", example = "true")
|
||||||
|
private boolean success;
|
||||||
|
|
||||||
|
@Schema(description = "消息", example = "选区成功")
|
||||||
|
private String message;
|
||||||
|
|
||||||
|
@Schema(description = "二维码URL", example = "http://36.138.184.60:12345/66L8NM3L/二维码.png")
|
||||||
|
private String qrCodeUrl;
|
||||||
|
|
||||||
|
@Schema(description = "二维码创建时间")
|
||||||
|
private LocalDateTime qrCreatedAt;
|
||||||
|
|
||||||
|
@Schema(description = "二维码过期时间")
|
||||||
|
private LocalDateTime qrExpireAt;
|
||||||
|
|
||||||
|
@Schema(description = "链接状态", example = "USING")
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
@Schema(description = "选择的区域", example = "Q")
|
||||||
|
private String region;
|
||||||
|
|
||||||
|
@Schema(description = "二维码出现延迟时间(秒)", example = "5")
|
||||||
|
private Integer qrDelaySeconds;
|
||||||
|
|
||||||
|
public SelectRegionResponse() {}
|
||||||
|
|
||||||
|
public SelectRegionResponse(boolean success, String message) {
|
||||||
|
this.success = success;
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSuccess() {
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSuccess(boolean success) {
|
||||||
|
this.success = success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMessage() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMessage(String message) {
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getQrCodeUrl() {
|
||||||
|
return qrCodeUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setQrCodeUrl(String qrCodeUrl) {
|
||||||
|
this.qrCodeUrl = qrCodeUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getQrCreatedAt() {
|
||||||
|
return qrCreatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setQrCreatedAt(LocalDateTime qrCreatedAt) {
|
||||||
|
this.qrCreatedAt = qrCreatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getQrExpireAt() {
|
||||||
|
return qrExpireAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setQrExpireAt(LocalDateTime qrExpireAt) {
|
||||||
|
this.qrExpireAt = qrExpireAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(String status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRegion() {
|
||||||
|
return region;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRegion(String region) {
|
||||||
|
this.region = region;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getQrDelaySeconds() {
|
||||||
|
return qrDelaySeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setQrDelaySeconds(Integer qrDelaySeconds) {
|
||||||
|
this.qrDelaySeconds = qrDelaySeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "SelectRegionResponse{" +
|
||||||
|
"success=" + success +
|
||||||
|
", message='" + message + '\'' +
|
||||||
|
", qrCodeUrl='" + qrCodeUrl + '\'' +
|
||||||
|
", qrCreatedAt=" + qrCreatedAt +
|
||||||
|
", qrExpireAt=" + qrExpireAt +
|
||||||
|
", status='" + status + '\'' +
|
||||||
|
", region='" + region + '\'' +
|
||||||
|
", qrDelaySeconds=" + qrDelaySeconds +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,6 +42,8 @@ public class SecurityConfig {
|
|||||||
.pathMatchers(HttpMethod.POST, "/api/auth/login").permitAll()
|
.pathMatchers(HttpMethod.POST, "/api/auth/login").permitAll()
|
||||||
.pathMatchers(HttpMethod.GET, "/api/auth/me").permitAll()
|
.pathMatchers(HttpMethod.GET, "/api/auth/me").permitAll()
|
||||||
.pathMatchers(HttpMethod.GET, "/api/link/status").permitAll() // 用户端获取链接状态接口,公开访问
|
.pathMatchers(HttpMethod.GET, "/api/link/status").permitAll() // 用户端获取链接状态接口,公开访问
|
||||||
|
.pathMatchers(HttpMethod.POST, "/api/link/select-region").permitAll() // 用户端选区接口,公开访问
|
||||||
|
.pathMatchers(HttpMethod.GET, "/api/link/poll-login").permitAll() // 用户端轮询登录接口,公开访问
|
||||||
.pathMatchers("/api/link/**").authenticated() // 其他链接接口需要认证
|
.pathMatchers("/api/link/**").authenticated() // 其他链接接口需要认证
|
||||||
.anyExchange().permitAll() // 其他接口后续再收紧
|
.anyExchange().permitAll() // 其他接口后续再收紧
|
||||||
)
|
)
|
||||||
@@ -61,6 +63,8 @@ public class SecurityConfig {
|
|||||||
log.info(" * POST /api/auth/login -> 允许所有");
|
log.info(" * POST /api/auth/login -> 允许所有");
|
||||||
log.info(" * GET /api/auth/me -> 允许所有");
|
log.info(" * GET /api/auth/me -> 允许所有");
|
||||||
log.info(" * GET /api/link/status -> 允许所有 (用户端公开接口)");
|
log.info(" * GET /api/link/status -> 允许所有 (用户端公开接口)");
|
||||||
|
log.info(" * POST /api/link/select-region -> 允许所有 (用户端公开接口)");
|
||||||
|
log.info(" * GET /api/link/poll-login -> 允许所有 (用户端公开接口)");
|
||||||
log.info(" * /api/link/** -> 需要认证");
|
log.info(" * /api/link/** -> 需要认证");
|
||||||
log.info(" * 其他路径 -> 允许所有");
|
log.info(" * 其他路径 -> 允许所有");
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.gameplatform.server.service.external;
|
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.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
@@ -17,13 +19,19 @@ public class ScriptClient {
|
|||||||
|
|
||||||
private final WebClient webClient;
|
private final WebClient webClient;
|
||||||
private final String baseUrl;
|
private final String baseUrl;
|
||||||
|
private final String appBaseUrl;
|
||||||
|
private final DeviceStatusService deviceStatusService;
|
||||||
|
|
||||||
public ScriptClient(
|
public ScriptClient(
|
||||||
@Value("${script.base-url}") String baseUrl,
|
@Value("${script.base-url}") String baseUrl,
|
||||||
@Value("${script.connect-timeout-ms:3000}") int connectTimeoutMs,
|
@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.baseUrl = baseUrl;
|
||||||
|
this.appBaseUrl = appBaseUrl;
|
||||||
|
this.deviceStatusService = deviceStatusService;
|
||||||
this.webClient = WebClient.builder()
|
this.webClient = WebClient.builder()
|
||||||
.baseUrl(baseUrl)
|
.baseUrl(baseUrl)
|
||||||
.exchangeStrategies(ExchangeStrategies.builder()
|
.exchangeStrategies(ExchangeStrategies.builder()
|
||||||
@@ -57,14 +65,14 @@ public class ScriptClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查空闲设备
|
* 检查空闲设备(返回原始字符串)
|
||||||
*/
|
*/
|
||||||
public Mono<String> checkAvailableDevice() {
|
public Mono<String> checkAvailableDevice() {
|
||||||
String url = "http://36.138.184.60:1234/yijianwan_netfile/readAllMsg?文件名=判断分数";
|
String url = "http://36.138.184.60:1234/yijianwan_netfile/readAllMsg?文件名=判断分数";
|
||||||
log.debug("检查空闲设备: {}", url);
|
log.debug("检查空闲设备: {}", url);
|
||||||
return webClient.get()
|
return webClient.get()
|
||||||
.uri(url)
|
.uri(url)
|
||||||
.accept(MediaType.TEXT_PLAIN)
|
.accept(MediaType.APPLICATION_JSON)
|
||||||
.retrieve()
|
.retrieve()
|
||||||
.bodyToMono(String.class)
|
.bodyToMono(String.class)
|
||||||
.timeout(Duration.ofSeconds(10))
|
.timeout(Duration.ofSeconds(10))
|
||||||
@@ -73,19 +81,45 @@ public class ScriptClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 选区操作
|
* 检查空闲设备(解析后的结构化数据)
|
||||||
*/
|
*/
|
||||||
public Mono<String> selectRegion(String codeNo, String region) {
|
public Mono<DeviceStatusResponse> checkAvailableDeviceStatus() {
|
||||||
String url = String.format("http://36.138.184.60:1234/yijianwan_netfile/saveMsg?文件名=判断系统&编号=%s", region);
|
return checkAvailableDevice()
|
||||||
log.debug("选区操作: codeNo={}, region={}, url={}", codeNo, region, url);
|
.map(jsonResponse -> deviceStatusService.parseDeviceStatus(jsonResponse))
|
||||||
return webClient.post()
|
.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 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)
|
.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={}, region={}, result={}", codeNo, region, result))
|
.doOnSuccess(result -> {
|
||||||
.doOnError(e -> log.warn("选区操作失败: codeNo={}, region={}, error={}", codeNo, region, e.toString()));
|
log.info("选区操作成功: 设备={}, 区域={}, 结果={}", deviceId, region, result);
|
||||||
|
})
|
||||||
|
.doOnError(e -> {
|
||||||
|
log.error("选区操作失败: 设备={}, 区域={}, 错误={}", deviceId, region, e.toString());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -105,29 +139,65 @@ 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) {
|
public Mono<String> checkLoginStatus(String deviceId) {
|
||||||
String url = String.format("http://36.138.184.60:1234/yijianwan_netfile/readMsg?文件名=判断上号&对象名=%s", codeNo);
|
String url = String.format("http://36.138.184.60:1234/yijianwan_netfile/readMsg?文件名=判断上号&对象名=%s", deviceId);
|
||||||
log.debug("检查上号状态: codeNo={}, url={}", codeNo, url);
|
log.debug("检查设备上号状态: 设备={}, url={}", deviceId, 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(10))
|
||||||
.doOnSuccess(result -> log.debug("检查上号状态成功: codeNo={}, result={}", codeNo, result))
|
.doOnSuccess(result -> log.debug("检查设备上号状态成功: 设备={}, 状态={}", deviceId, result))
|
||||||
.doOnError(e -> log.warn("检查上号状态失败: codeNo={}, error={}", codeNo, e.toString()));
|
.doOnError(e -> log.warn("检查设备上号状态失败: 设备={}, 错误={}", deviceId, e.toString()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取二维码URL(带时间戳防缓存)
|
* 获取二维码URL(带时间戳防缓存)- 直接链接到脚本服务器
|
||||||
*/
|
*/
|
||||||
public String getQrCodeUrl(String codeNo) {
|
public String getQrCodeUrl(String codeNo) {
|
||||||
long timestamp = System.currentTimeMillis();
|
long timestamp = System.currentTimeMillis();
|
||||||
return String.format("http://36.138.184.60:12345/%s/二维码.png?t=%d", codeNo, timestamp);
|
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());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取目标分数
|
* 获取目标分数
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2,13 +2,17 @@ package com.gameplatform.server.service.link;
|
|||||||
|
|
||||||
import com.gameplatform.server.mapper.agent.LinkBatchMapper;
|
import com.gameplatform.server.mapper.agent.LinkBatchMapper;
|
||||||
import com.gameplatform.server.mapper.agent.LinkTaskMapper;
|
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.BatchDeleteResponse;
|
||||||
import com.gameplatform.server.model.dto.link.LinkStatusResponse;
|
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.dto.link.UserLinkStatusResponse;
|
||||||
import com.gameplatform.server.model.entity.agent.LinkBatch;
|
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 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;
|
||||||
@@ -30,8 +34,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 static final Map<String, String> STATUS_DESC_MAP = new HashMap<>();
|
private static final Map<String, String> STATUS_DESC_MAP = new HashMap<>();
|
||||||
@@ -44,10 +48,11 @@ public class LinkStatusService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public LinkStatusService(LinkTaskMapper linkTaskMapper, LinkBatchMapper linkBatchMapper,
|
public LinkStatusService(LinkTaskMapper linkTaskMapper, LinkBatchMapper linkBatchMapper,
|
||||||
ScriptClient scriptClient) {
|
ScriptClient scriptClient, DeviceCodeMappingService deviceCodeMappingService) {
|
||||||
this.linkTaskMapper = linkTaskMapper;
|
this.linkTaskMapper = linkTaskMapper;
|
||||||
this.linkBatchMapper = linkBatchMapper;
|
this.linkBatchMapper = linkBatchMapper;
|
||||||
this.scriptClient = scriptClient;
|
this.scriptClient = scriptClient;
|
||||||
|
this.deviceCodeMappingService = deviceCodeMappingService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -288,19 +293,23 @@ public class LinkStatusService {
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 如果状态不是NEW,执行自动刷新逻辑
|
// 3. 根据状态执行相应逻辑
|
||||||
if (!"NEW".equals(linkTask.getStatus())) {
|
|
||||||
log.info("链接状态不是NEW,执行自动刷新逻辑");
|
|
||||||
performAutoRefresh(linkTask);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 如果状态是USING,重新获取二维码
|
|
||||||
if ("USING".equals(linkTask.getStatus())) {
|
if ("USING".equals(linkTask.getStatus())) {
|
||||||
|
// 如果是USING状态,检查二维码是否过期,过期则刷新
|
||||||
|
if (linkTask.getQrExpireAt() != null && linkTask.getQrExpireAt().isBefore(LocalDateTime.now())) {
|
||||||
|
log.info("二维码已过期,执行自动刷新重置选区状态");
|
||||||
|
performAutoRefresh(linkTask);
|
||||||
|
} else {
|
||||||
|
// 二维码还未过期,更新二维码信息
|
||||||
log.info("链接状态是USING,重新获取二维码");
|
log.info("链接状态是USING,重新获取二维码");
|
||||||
updateQrCodeInfo(linkTask);
|
updateQrCodeInfo(linkTask);
|
||||||
}
|
}
|
||||||
|
} else if ("LOGGED_IN".equals(linkTask.getStatus()) || "REFUNDED".equals(linkTask.getStatus())) {
|
||||||
|
// 已上号或已退款状态,不需要刷新
|
||||||
|
log.info("链接状态为 {},不需要刷新", linkTask.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
// 5. 构建响应
|
// 4. 构建响应
|
||||||
UserLinkStatusResponse response = buildUserStatusResponse(linkTask);
|
UserLinkStatusResponse response = buildUserStatusResponse(linkTask);
|
||||||
log.info("=== 用户端链接状态查询完成 ===");
|
log.info("=== 用户端链接状态查询完成 ===");
|
||||||
log.info("返回状态: {}, view: {}", response.getStatus(), response.getView());
|
log.info("返回状态: {}, view: {}", response.getStatus(), response.getView());
|
||||||
@@ -316,31 +325,41 @@ public class LinkStatusService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行自动刷新逻辑
|
* 执行自动刷新逻辑
|
||||||
|
* 刷新时会重置选区状态,让用户重新选择区域
|
||||||
*/
|
*/
|
||||||
private void performAutoRefresh(LinkTask linkTask) {
|
private void performAutoRefresh(LinkTask linkTask) {
|
||||||
try {
|
try {
|
||||||
log.info("开始执行刷新操作");
|
log.info("开始执行刷新操作,将重置选区状态");
|
||||||
|
|
||||||
// 调用脚本端刷新
|
// 1. 调用脚本端刷新
|
||||||
String refreshResult = scriptClient.refresh(linkTask.getCodeNo()).block();
|
String refreshResult = scriptClient.refresh(linkTask.getCodeNo()).block();
|
||||||
log.info("脚本端刷新结果: {}", refreshResult);
|
log.info("脚本端刷新结果: {}", refreshResult);
|
||||||
|
|
||||||
// 更新刷新状态
|
// 2. 重置选区状态,删除已有选区让用户重新选择
|
||||||
linkTask.setNeedRefresh(true);
|
log.info("重置选区状态: 从 {} 重置为 NEW", linkTask.getStatus());
|
||||||
linkTask.setRefreshTime(LocalDateTime.now());
|
|
||||||
linkTask.setUpdatedAt(LocalDateTime.now());
|
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);
|
linkTaskMapper.update(linkTask);
|
||||||
|
|
||||||
// 等待10秒
|
log.info("选区状态重置完成: status=NEW, region=null, needRefresh=true");
|
||||||
log.info("刷新完成,等待10秒...");
|
|
||||||
|
// 3. 等待10秒后允许重新选区
|
||||||
|
log.info("刷新完成,等待10秒后允许重新选区...");
|
||||||
Thread.sleep(10000);
|
Thread.sleep(10000);
|
||||||
|
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
log.warn("等待被中断: {}", e.getMessage());
|
log.warn("刷新等待被中断: {}", e.getMessage());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("执行刷新操作失败: {}", e.getMessage());
|
log.warn("执行刷新操作失败: {}", e.getMessage());
|
||||||
// 刷新失败不影响后续流程
|
// 刷新失败不影响后续流程,但记录错误日志
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -380,7 +399,7 @@ public class LinkStatusService {
|
|||||||
// 如果状态是USING,设置二维码信息
|
// 如果状态是USING,设置二维码信息
|
||||||
if ("USING".equals(linkTask.getStatus()) && linkTask.getQrCreatedAt() != null) {
|
if ("USING".equals(linkTask.getStatus()) && linkTask.getQrCreatedAt() != null) {
|
||||||
UserLinkStatusResponse.QrInfo qrInfo = new UserLinkStatusResponse.QrInfo();
|
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());
|
qrInfo.setCreatedAt(java.sql.Timestamp.valueOf(linkTask.getQrCreatedAt()).getTime());
|
||||||
if (linkTask.getQrExpireAt() != null) {
|
if (linkTask.getQrExpireAt() != null) {
|
||||||
qrInfo.setExpireAt(java.sql.Timestamp.valueOf(linkTask.getQrExpireAt()).getTime());
|
qrInfo.setExpireAt(java.sql.Timestamp.valueOf(linkTask.getQrExpireAt()).getTime());
|
||||||
@@ -420,4 +439,231 @@ public class LinkStatusService {
|
|||||||
return "FIRST";
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,5 +67,9 @@ script:
|
|||||||
connect-timeout-ms: 3000
|
connect-timeout-ms: 3000
|
||||||
read-timeout-ms: 5000
|
read-timeout-ms: 5000
|
||||||
|
|
||||||
|
# 服务器配置
|
||||||
|
app:
|
||||||
|
base-url: "http://localhost:18080" # 生产环境需要配置为实际域名
|
||||||
|
|
||||||
link:
|
link:
|
||||||
expire-hours: 2
|
expire-hours: 2
|
||||||
|
|||||||
48
test_select_region.http
Normal file
48
test_select_region.http
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
### 测试选区接口
|
||||||
|
|
||||||
|
# 测试选区API - 选择Q区
|
||||||
|
POST http://localhost:8080/api/link/select-region
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"code": "66L8NM3L",
|
||||||
|
"region": "Q"
|
||||||
|
}
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
# 测试选区API - 选择V区
|
||||||
|
POST http://localhost:8080/api/link/select-region
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"code": "66L8NM3L",
|
||||||
|
"region": "V"
|
||||||
|
}
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
# 测试链接状态API
|
||||||
|
GET http://localhost:8080/api/link/status?code=66L8NM3L
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
# 测试参数验证 - 无效region
|
||||||
|
POST http://localhost:8080/api/link/select-region
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"code": "66L8NM3L",
|
||||||
|
"region": "X"
|
||||||
|
}
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
# 测试参数验证 - 空code
|
||||||
|
POST http://localhost:8080/api/link/select-region
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"code": "",
|
||||||
|
"region": "Q"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user