diff --git a/docs/database_migration_add_first_region_select_time.sql b/docs/database_migration_add_first_region_select_time.sql new file mode 100644 index 0000000..621d1ab --- /dev/null +++ b/docs/database_migration_add_first_region_select_time.sql @@ -0,0 +1,34 @@ +-- 数据库迁移脚本:为link_task表添加首次选区时间字段 +-- 执行时间:2025-01-XX +-- 说明:记录链接首次选区的准确时间,便于跟踪和分析 + +-- 为link_task表添加首次选区时间字段 +ALTER TABLE `link_task` +ADD COLUMN `first_region_select_at` datetime(3) NULL DEFAULT NULL COMMENT '首次选区时间' AFTER `qr_expire_at`; + +-- 添加索引以优化查询性能 +ALTER TABLE `link_task` +ADD INDEX `idx_first_region_select` (`first_region_select_at` ASC); + +-- 为现有USING状态的记录回填首次选区时间(使用qr_created_at作为首次选区时间) +UPDATE `link_task` +SET `first_region_select_at` = `qr_created_at` +WHERE `status` = 'USING' + AND `qr_created_at` IS NOT NULL + AND `first_region_select_at` IS NULL; + +-- 验证表结构变更 +SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_DEFAULT, COLUMN_COMMENT +FROM INFORMATION_SCHEMA.COLUMNS +WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'link_task' + AND COLUMN_NAME = 'first_region_select_at' +ORDER BY ORDINAL_POSITION; + +-- 验证数据回填结果 +SELECT + COUNT(*) as total_using_links, + COUNT(first_region_select_at) as links_with_first_select_time, + COUNT(qr_created_at) as links_with_qr_time +FROM `link_task` +WHERE `status` = 'USING'; 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 45ef30f..2557fbf 100644 --- a/src/main/java/com/gameplatform/server/controller/link/LinkController.java +++ b/src/main/java/com/gameplatform/server/controller/link/LinkController.java @@ -28,6 +28,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.*; +import org.springframework.web.reactive.function.client.WebClientResponseException; import reactor.core.publisher.Mono; @RestController @@ -321,8 +322,7 @@ public Mono deleteLink(@PathVariable("codeNo") String codeNo, Authentic return linkStatusService.getUserLinkStatus(linkId, actualCodeNo) .doOnSuccess(response -> { - log.info("用户端链接状态查询成功: status={}, view={}, needRefresh={}", - response.getStatus(), response.getView(), response.getNeedRefresh()); + log.info("用户端链接状态查询成功: status={}", response.getStatus()); }) .doOnError(error -> { log.error("用户端链接状态查询失败: {}", error.getMessage(), error); @@ -407,7 +407,15 @@ public Mono deleteLink(@PathVariable("codeNo") String codeNo, Authentic }) .onErrorResume(error -> { log.error("获取设备 {} 二维码失败: {}", deviceId, error.getMessage(), error); - return Mono.just(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build()); + + // 如果是404错误,返回404;其他错误返回500 + if (error instanceof WebClientResponseException.NotFound) { + log.warn("设备 {} 的二维码文件不存在,返回404", deviceId); + return Mono.just(ResponseEntity.status(HttpStatus.NOT_FOUND).build()); + } else { + log.error("获取设备 {} 二维码时发生系统错误", deviceId); + return Mono.just(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build()); + } }); } } diff --git a/src/main/java/com/gameplatform/server/controller/link/QrProxyController.java b/src/main/java/com/gameplatform/server/controller/link/QrProxyController.java index 6bbd39a..082e425 100644 --- a/src/main/java/com/gameplatform/server/controller/link/QrProxyController.java +++ b/src/main/java/com/gameplatform/server/controller/link/QrProxyController.java @@ -1,8 +1,16 @@ package com.gameplatform.server.controller.link; +import com.gameplatform.server.mapper.agent.LinkBatchMapper; +import com.gameplatform.server.mapper.agent.LinkTaskMapper; +import com.gameplatform.server.model.dto.link.GameInterfaceResponse; +import com.gameplatform.server.model.entity.agent.LinkBatch; +import com.gameplatform.server.model.entity.agent.LinkTask; import com.gameplatform.server.service.external.ScriptClient; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.CacheControl; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -13,16 +21,29 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Mono; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.TimeUnit; @RestController @RequestMapping("/api/link") -@Tag(name = "二维码代理", description = "转发脚本端的二维码图片,避免混合内容") +@Tag(name = "图片代理", description = "转发脚本端的图片,避免混合内容") public class QrProxyController { + private static final Logger log = LoggerFactory.getLogger(QrProxyController.class); + private final ScriptClient scriptClient; + private final String appBaseUrl; + private final LinkTaskMapper linkTaskMapper; + private final LinkBatchMapper linkBatchMapper; - public QrProxyController(ScriptClient scriptClient) { + public QrProxyController(ScriptClient scriptClient, + @Value("${app.base-url}") String appBaseUrl, + LinkTaskMapper linkTaskMapper, + LinkBatchMapper linkBatchMapper) { this.scriptClient = scriptClient; + this.appBaseUrl = appBaseUrl; + this.linkTaskMapper = linkTaskMapper; + this.linkBatchMapper = linkBatchMapper; } @GetMapping(value = "/{codeNo}/qr.png", produces = MediaType.IMAGE_PNG_VALUE) @@ -36,6 +57,140 @@ public class QrProxyController { .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=qr.png") .body(bytes)); } + + @GetMapping(value = "/{codeNo}/homepage.png", produces = MediaType.IMAGE_PNG_VALUE) + @Operation(summary = "首次主页图片代理") + public Mono> homepage(@PathVariable("codeNo") String codeNo) { + String path = "/" + codeNo + "/首次主页.png"; + return scriptClient.getImagePng(path) + .map(bytes -> ResponseEntity.ok() + .contentType(MediaType.IMAGE_PNG) + .cacheControl(CacheControl.maxAge(60, TimeUnit.SECONDS).cachePublic()) + .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=homepage.png") + .body(bytes)); + } + + @GetMapping(value = "/{codeNo}/first-reward.png", produces = MediaType.IMAGE_PNG_VALUE) + @Operation(summary = "首次赏金图片代理") + public Mono> firstReward(@PathVariable("codeNo") String codeNo) { + String path = "/" + codeNo + "/首次赏金.png"; + return scriptClient.getImagePng(path) + .map(bytes -> ResponseEntity.ok() + .contentType(MediaType.IMAGE_PNG) + .cacheControl(CacheControl.maxAge(60, TimeUnit.SECONDS).cachePublic()) + .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=first-reward.png") + .body(bytes)); + } + + @GetMapping(value = "/{codeNo}/mid-reward.png", produces = MediaType.IMAGE_PNG_VALUE) + @Operation(summary = "中途赏金图片代理") + public Mono> midReward(@PathVariable("codeNo") String codeNo) { + String path = "/" + codeNo + "/中途赏金.png"; + return scriptClient.getImagePng(path) + .map(bytes -> ResponseEntity.ok() + .contentType(MediaType.IMAGE_PNG) + .cacheControl(CacheControl.maxAge(60, TimeUnit.SECONDS).cachePublic()) + .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=mid-reward.png") + .body(bytes)); + } + + @GetMapping(value = "/{codeNo}/end-reward.png", produces = MediaType.IMAGE_PNG_VALUE) + @Operation(summary = "结束赏金图片代理") + public Mono> endReward(@PathVariable("codeNo") String codeNo) { + String path = "/" + codeNo + "/结束赏金.png"; + return scriptClient.getImagePng(path) + .map(bytes -> ResponseEntity.ok() + .contentType(MediaType.IMAGE_PNG) + .cacheControl(CacheControl.maxAge(60, TimeUnit.SECONDS).cachePublic()) + .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=end-reward.png") + .body(bytes)); + } + + @GetMapping("/{codeNo}/images") + @Operation(summary = "获取所有图片代理链接") + public ResponseEntity> getImageLinks(@PathVariable("codeNo") String codeNo) { + Map imageLinks = new HashMap<>(); + + // 添加二维码链接 + imageLinks.put("qrCode", appBaseUrl + "/api/link/" + codeNo + "/qr.png"); + + // 添加其他图片链接 + imageLinks.put("homepage", appBaseUrl + "/api/link/" + codeNo + "/homepage.png"); + imageLinks.put("firstReward", appBaseUrl + "/api/link/" + codeNo + "/first-reward.png"); + imageLinks.put("midReward", appBaseUrl + "/api/link/" + codeNo + "/mid-reward.png"); + imageLinks.put("endReward", appBaseUrl + "/api/link/" + codeNo + "/end-reward.png"); + + return ResponseEntity.ok() + .cacheControl(CacheControl.maxAge(5, TimeUnit.MINUTES).cachePublic()) + .body(imageLinks); + } + + @GetMapping("/{codeNo}/game-interface") + @Operation(summary = "获取游戏界面数据", description = "返回四张图片链接和总点数信息") + public ResponseEntity getGameInterface(@PathVariable("codeNo") String codeNo) { + log.info("获取游戏界面数据: codeNo={}", codeNo); + + try { + // 查询链接任务 + LinkTask linkTask = linkTaskMapper.findByCodeNo(codeNo); + if (linkTask == null) { + log.warn("链接任务不存在: codeNo={}", codeNo); + return ResponseEntity.notFound().build(); + } + + // 查询批次信息 + LinkBatch linkBatch = linkBatchMapper.findById(linkTask.getBatchId()); + if (linkBatch == null) { + log.warn("批次信息不存在: batchId={}", linkTask.getBatchId()); + return ResponseEntity.notFound().build(); + } + + // 构建响应对象 + GameInterfaceResponse response = new GameInterfaceResponse(); + response.setCodeNo(codeNo); + response.setQuantity(linkBatch.getQuantity()); + response.setTimes(linkBatch.getTimes()); + response.setTotalPoints(linkBatch.getQuantity() * linkBatch.getTimes()); + + // 设置游戏区域信息 + response.setRegion(linkTask.getRegion()); + response.setRegionDesc(getRegionDescription(linkTask.getRegion())); + + // 设置图片链接 + response.setQrCodeUrl(appBaseUrl + "/api/link/" + codeNo + "/qr.png"); + response.setHomepageUrl(appBaseUrl + "/api/link/" + codeNo + "/homepage.png"); + response.setFirstRewardUrl(appBaseUrl + "/api/link/" + codeNo + "/first-reward.png"); + response.setMidRewardUrl(appBaseUrl + "/api/link/" + codeNo + "/mid-reward.png"); + response.setEndRewardUrl(appBaseUrl + "/api/link/" + codeNo + "/end-reward.png"); + + log.info("游戏界面数据构建完成: codeNo={}, totalPoints={}", codeNo, response.getTotalPoints()); + + return ResponseEntity.ok() + .cacheControl(CacheControl.maxAge(2, TimeUnit.MINUTES).cachePublic()) + .body(response); + + } catch (Exception e) { + log.error("获取游戏界面数据失败: codeNo={}, error={}", codeNo, e.getMessage(), e); + return ResponseEntity.internalServerError().build(); + } + } + + /** + * 获取区域描述 + */ + private String getRegionDescription(String region) { + if (region == null) { + return "未选择区域"; + } + switch (region) { + case "Q": + return "QQ区"; + case "V": + return "微信区"; + default: + return "未知区域"; + } + } } diff --git a/src/main/java/com/gameplatform/server/model/dto/link/GameInterfaceResponse.java b/src/main/java/com/gameplatform/server/model/dto/link/GameInterfaceResponse.java new file mode 100644 index 0000000..d5cf9d2 --- /dev/null +++ b/src/main/java/com/gameplatform/server/model/dto/link/GameInterfaceResponse.java @@ -0,0 +1,131 @@ +package com.gameplatform.server.model.dto.link; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * 游戏界面数据响应 + */ +@Schema(description = "游戏界面数据") +public class GameInterfaceResponse { + + @Schema(description = "设备编号") + private String codeNo; + + @Schema(description = "总点数 (quantity * times)") + private Integer totalPoints; + + @Schema(description = "每次副本奖励点数") + private Integer quantity; + + @Schema(description = "副本次数") + private Integer times; + + @Schema(description = "游戏区域", example = "Q表示QQ区,V表示微信区") + private String region; + + @Schema(description = "游戏区域描述", example = "QQ区") + private String regionDesc; + + @Schema(description = "二维码图片链接") + private String qrCodeUrl; + + @Schema(description = "首次主页图片链接") + private String homepageUrl; + + @Schema(description = "首次赏金图片链接") + private String firstRewardUrl; + + @Schema(description = "中途赏金图片链接") + private String midRewardUrl; + + @Schema(description = "结束赏金图片链接") + private String endRewardUrl; + + public String getCodeNo() { + return codeNo; + } + + public void setCodeNo(String codeNo) { + this.codeNo = codeNo; + } + + public Integer getTotalPoints() { + return totalPoints; + } + + public void setTotalPoints(Integer totalPoints) { + this.totalPoints = totalPoints; + } + + public Integer getQuantity() { + return quantity; + } + + public void setQuantity(Integer quantity) { + this.quantity = quantity; + } + + public Integer getTimes() { + return times; + } + + public void setTimes(Integer times) { + this.times = times; + } + + public String getRegion() { + return region; + } + + public void setRegion(String region) { + this.region = region; + } + + public String getRegionDesc() { + return regionDesc; + } + + public void setRegionDesc(String regionDesc) { + this.regionDesc = regionDesc; + } + + public String getQrCodeUrl() { + return qrCodeUrl; + } + + public void setQrCodeUrl(String qrCodeUrl) { + this.qrCodeUrl = qrCodeUrl; + } + + public String getHomepageUrl() { + return homepageUrl; + } + + public void setHomepageUrl(String homepageUrl) { + this.homepageUrl = homepageUrl; + } + + public String getFirstRewardUrl() { + return firstRewardUrl; + } + + public void setFirstRewardUrl(String firstRewardUrl) { + this.firstRewardUrl = firstRewardUrl; + } + + public String getMidRewardUrl() { + return midRewardUrl; + } + + public void setMidRewardUrl(String midRewardUrl) { + this.midRewardUrl = midRewardUrl; + } + + public String getEndRewardUrl() { + return endRewardUrl; + } + + public void setEndRewardUrl(String endRewardUrl) { + this.endRewardUrl = endRewardUrl; + } +} diff --git a/src/main/java/com/gameplatform/server/model/dto/link/UserLinkStatusResponse.java b/src/main/java/com/gameplatform/server/model/dto/link/UserLinkStatusResponse.java index 53bb348..d20ad1b 100644 --- a/src/main/java/com/gameplatform/server/model/dto/link/UserLinkStatusResponse.java +++ b/src/main/java/com/gameplatform/server/model/dto/link/UserLinkStatusResponse.java @@ -8,91 +8,7 @@ public class UserLinkStatusResponse { @Schema(description = "链接状态", example = "NEW", allowableValues = {"NEW", "USING", "LOGGED_IN", "REFUNDED", "EXPIRED"}) private String status; - @Schema(description = "是否需要刷新", example = "false") - private Boolean needRefresh; - - @Schema(description = "选择的区域", example = "Q", allowableValues = {"Q", "V"}) - private String region; - - @Schema(description = "二维码信息") - private QrInfo qr; - - @Schema(description = "视图类型", example = "FIRST", allowableValues = {"FIRST", "SCAN", "SECOND"}) - private String view; - - @Schema(description = "静态资源信息") - private AssetsInfo assets; - - @Schema(description = "二维码信息") - public static class QrInfo { - @Schema(description = "二维码URL", example = "http://36.138.184.60:12345/{编号}/二维码.png") - private String url; - - @Schema(description = "创建时间戳", example = "1730000000000") - private Long createdAt; - - @Schema(description = "过期时间戳", example = "1730000060000") - private Long expireAt; - - public String getUrl() { return url; } - public void setUrl(String url) { this.url = url; } - - public Long getCreatedAt() { return createdAt; } - public void setCreatedAt(Long createdAt) { this.createdAt = createdAt; } - - public Long getExpireAt() { return expireAt; } - public void setExpireAt(Long expireAt) { this.expireAt = expireAt; } - } - - @Schema(description = "静态资源信息") - public static class AssetsInfo { - @Schema(description = "基础URL", example = "http://36.138.184.60:12345/{编号}/") - 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 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; } - } - - // Getters and Setters + // Getter and Setter public String getStatus() { return status; } public void setStatus(String status) { this.status = status; } - - public Boolean getNeedRefresh() { return needRefresh; } - public void setNeedRefresh(Boolean needRefresh) { this.needRefresh = needRefresh; } - - public String getRegion() { return region; } - public void setRegion(String region) { this.region = region; } - - public QrInfo getQr() { return qr; } - public void setQr(QrInfo qr) { this.qr = qr; } - - 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; } } diff --git a/src/main/java/com/gameplatform/server/model/entity/agent/LinkTask.java b/src/main/java/com/gameplatform/server/model/entity/agent/LinkTask.java index ce0a4a3..3b60f48 100644 --- a/src/main/java/com/gameplatform/server/model/entity/agent/LinkTask.java +++ b/src/main/java/com/gameplatform/server/model/entity/agent/LinkTask.java @@ -58,6 +58,9 @@ public class LinkTask { @TableField("qr_expire_at") private LocalDateTime qrExpireAt; + + @TableField("first_region_select_at") + private LocalDateTime firstRegionSelectAt; public Long getId() { return id; } public void setId(Long id) { this.id = id; } @@ -112,4 +115,7 @@ public class LinkTask { public LocalDateTime getQrExpireAt() { return qrExpireAt; } public void setQrExpireAt(LocalDateTime qrExpireAt) { this.qrExpireAt = qrExpireAt; } + + public LocalDateTime getFirstRegionSelectAt() { return firstRegionSelectAt; } + public void setFirstRegionSelectAt(LocalDateTime firstRegionSelectAt) { this.firstRegionSelectAt = firstRegionSelectAt; } } diff --git a/src/main/java/com/gameplatform/server/security/SecurityConfig.java b/src/main/java/com/gameplatform/server/security/SecurityConfig.java index a42c4db..2cb642b 100644 --- a/src/main/java/com/gameplatform/server/security/SecurityConfig.java +++ b/src/main/java/com/gameplatform/server/security/SecurityConfig.java @@ -44,6 +44,8 @@ public class SecurityConfig { .pathMatchers(HttpMethod.GET, "/api/link/status").permitAll() // 用户端获取链接状态接口,公开访问 .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.GET, "/api/link/*/game-interface").permitAll() // 游戏界面数据接口,公开访问 .pathMatchers("/api/link/**").authenticated() // 其他链接接口需要认证 .anyExchange().permitAll() // 其他接口后续再收紧 ) @@ -65,6 +67,8 @@ public class SecurityConfig { log.info(" * GET /api/link/status -> 允许所有 (用户端公开接口)"); log.info(" * POST /api/link/select-region -> 允许所有 (用户端公开接口)"); log.info(" * GET /api/link/poll-login -> 允许所有 (用户端公开接口)"); + log.info(" * GET /api/link/qr/** -> 允许所有 (二维码获取接口)"); + log.info(" * GET /api/link/*/game-interface -> 允许所有 (游戏界面数据接口)"); log.info(" * /api/link/** -> 需要认证"); log.info(" * 其他路径 -> 允许所有"); 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 56be7f1..ac95c63 100644 --- a/src/main/java/com/gameplatform/server/service/external/ScriptClient.java +++ b/src/main/java/com/gameplatform/server/service/external/ScriptClient.java @@ -54,6 +54,23 @@ public class ScriptClient { .doOnError(e -> log.warn("ScriptClient.getQrPng error path={} err={}", path, e.toString())); } + /** + * 通用图片获取方法 + * @param path 图片路径,如 /{codeNo}/首次主页.png + * @return 图片数据 + */ + public Mono getImagePng(String path) { + log.debug("获取图片: path={}", path); + return webClient.get() + .uri(path) + .accept(MediaType.IMAGE_PNG) + .retrieve() + .bodyToMono(byte[].class) + .timeout(Duration.ofSeconds(10)) + .doOnSuccess(data -> log.debug("获取图片成功: path={}, 数据大小={}字节", path, data != null ? data.length : 0)) + .doOnError(e -> log.warn("获取图片失败: path={}, error={}", path, e.toString())); + } + public Mono getText(String path) { return webClient.get() .uri(path) @@ -138,6 +155,22 @@ public class ScriptClient { .doOnError(e -> log.warn("刷新操作失败: codeNo={}, error={}", codeNo, e.toString())); } + /** + * 判断刷新接口 - 统一管理刷新判断逻辑 + */ + public Mono checkRefresh() { + String url = "http://36.138.184.60:1234/yijianwan_netfile/saveMsg?文件名=判断刷新&f4=刷新"; + log.info("调用判断刷新接口: {}", url); + return webClient.get() + .uri(url) + .accept(MediaType.TEXT_PLAIN) + .retrieve() + .bodyToMono(String.class) + .timeout(Duration.ofSeconds(10)) + .doOnSuccess(result -> log.info("判断刷新接口调用成功: result={}", result)) + .doOnError(e -> log.warn("判断刷新接口调用失败: {}", e.toString())); + } + /** * 检查设备是否已上号 - 根据您提供的API示例 * URL格式: http://36.138.184.60:1234/yijianwan_netfile/readMsg?文件名=判断上号&对象名=f1 @@ -229,6 +262,24 @@ public class ScriptClient { .doOnSuccess(result -> log.debug("设置次数成功: codeNo={}, times={}, result={}", codeNo, times, result)) .doOnError(e -> log.warn("设置次数失败: codeNo={}, times={}, error={}", codeNo, times, e.toString())); } + + /** + * 保存总次数(使用f4参数格式) + * @param times 总次数 + * @return 保存结果 + */ + public Mono saveTotalTimes(int times) { + String url = String.format("http://36.138.184.60:1234/yijianwan_netfile/saveMsg?文件名=总次数&f4=%d", times); + log.info("开始调用保存总次数接口: times={}, url={}", times, url); + return webClient.get() + .uri(url) + .accept(MediaType.TEXT_PLAIN) + .retrieve() + .bodyToMono(String.class) + .timeout(Duration.ofSeconds(10)) + .doOnSuccess(result -> log.info("保存总次数接口调用成功: url={}, result={}", url, result)) + .doOnError(e -> log.warn("保存总次数接口调用失败: times={}, error={}", times, e.toString())); + } } 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 7ffcc86..118c591 100644 --- a/src/main/java/com/gameplatform/server/service/link/LinkStatusService.java +++ b/src/main/java/com/gameplatform/server/service/link/LinkStatusService.java @@ -17,6 +17,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; + import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; @@ -37,6 +38,7 @@ public class LinkStatusService { private final ScriptClient scriptClient; private final DeviceCodeMappingService deviceCodeMappingService; + // 状态描述映射 private static final Map STATUS_DESC_MAP = new HashMap<>(); static { @@ -47,12 +49,13 @@ public class LinkStatusService { STATUS_DESC_MAP.put("EXPIRED", "已过期"); } - public LinkStatusService(LinkTaskMapper linkTaskMapper, LinkBatchMapper linkBatchMapper, - ScriptClient scriptClient, DeviceCodeMappingService deviceCodeMappingService) { + public LinkStatusService(LinkTaskMapper linkTaskMapper, LinkBatchMapper linkBatchMapper, + ScriptClient scriptClient, DeviceCodeMappingService deviceCodeMappingService) { this.linkTaskMapper = linkTaskMapper; this.linkBatchMapper = linkBatchMapper; this.scriptClient = scriptClient; this.deviceCodeMappingService = deviceCodeMappingService; + } /** @@ -92,7 +95,6 @@ public class LinkStatusService { response.setQuantity(linkBatch.getQuantity()); response.setTimes(linkBatch.getTimes()); response.setTotalPoints(linkBatch.getQuantity() * linkBatch.getTimes()); - response.setRegion(linkTask.getRegion()); response.setMachineId(linkTask.getMachineId()); response.setLoginAt(linkTask.getLoginAt()); response.setCreatedAt(linkTask.getCreatedAt()); @@ -289,21 +291,27 @@ public class LinkStatusService { UserLinkStatusResponse response = new UserLinkStatusResponse(); response.setStatus("EXPIRED"); - response.setView("EXPIRED"); return response; } - // 3. 根据状态执行相应逻辑 + // 3. 检查USING状态的10分钟过期逻辑 if ("USING".equals(linkTask.getStatus())) { - // 如果是USING状态,检查二维码是否过期,过期则刷新 - if (linkTask.getQrExpireAt() != null && linkTask.getQrExpireAt().isBefore(LocalDateTime.now())) { - log.info("二维码已过期,执行自动刷新重置选区状态"); - performAutoRefresh(linkTask); - } else { - // 二维码还未过期,更新二维码信息 - log.info("链接状态是USING,重新获取二维码"); - updateQrCodeInfo(linkTask); + // 检查是否超过10分钟未登录 + if (linkTask.getQrCreatedAt() != null && + linkTask.getQrCreatedAt().isBefore(LocalDateTime.now().minusMinutes(10))) { + log.warn("选择设备已超过10分钟未登录,链接过期: qrCreatedAt={}", linkTask.getQrCreatedAt()); + linkTask.setStatus("EXPIRED"); + linkTask.setUpdatedAt(LocalDateTime.now()); + linkTaskMapper.update(linkTask); + + UserLinkStatusResponse response = new UserLinkStatusResponse(); + response.setStatus("EXPIRED"); + return response; } + + // 如果未超过10分钟,执行自动刷新 + log.info("链接状态是USING,执行自动刷新"); + performAutoRefresh(linkTask); } else if ("LOGGED_IN".equals(linkTask.getStatus()) || "REFUNDED".equals(linkTask.getStatus())) { // 已上号或已退款状态,不需要刷新 log.info("链接状态为 {},不需要刷新", linkTask.getStatus()); @@ -312,8 +320,7 @@ public class LinkStatusService { // 4. 构建响应 UserLinkStatusResponse response = buildUserStatusResponse(linkTask); log.info("=== 用户端链接状态查询完成 ==="); - log.info("返回状态: {}, view: {}", response.getStatus(), response.getView()); - + log.info("返回状态: {}", response.getStatus()); return response; } catch (Exception e) { @@ -325,61 +332,23 @@ public class LinkStatusService { /** * 执行自动刷新逻辑 - * 刷新时会重置选区状态,让用户重新选择区域 + * 只调用判断刷新接口,不更新数据库状态 */ private void performAutoRefresh(LinkTask linkTask) { try { - log.info("开始执行刷新操作,将重置选区状态"); + log.info("开始执行刷新操作"); - // 1. 调用脚本端刷新 - String refreshResult = scriptClient.refresh(linkTask.getCodeNo()).block(); - log.info("脚本端刷新结果: {}", refreshResult); + // 调用判断刷新接口(通过ScriptClient统一管理) + String refreshResult = scriptClient.checkRefresh().block(); + log.info("判断刷新接口调用完成: result={}", refreshResult); - // 2. 重置选区状态,删除已有选区让用户重新选择 - log.info("重置选区状态: 从 {} 重置为 NEW", linkTask.getStatus()); - - LocalDateTime now = LocalDateTime.now(); - linkTask.setStatus("NEW"); // 重置状态为NEW,允许重新选区 - linkTask.setRegion(null); // 清空已选择的区域 - linkTask.setQrCreatedAt(null); // 清空二维码创建时间 - linkTask.setQrExpireAt(null); // 清空二维码过期时间 - linkTask.setNeedRefresh(true); // 标记为刷新状态 - linkTask.setRefreshTime(now); // 记录刷新时间 - linkTask.setUpdatedAt(now); // 更新修改时间 - linkTaskMapper.update(linkTask); - - log.info("选区状态重置完成: status=NEW, region=null, needRefresh=true"); - - // 3. 等待10秒后允许重新选区 - log.info("刷新完成,等待10秒后允许重新选区..."); - Thread.sleep(10000); - - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - log.warn("刷新等待被中断: {}", e.getMessage()); } catch (Exception e) { log.warn("执行刷新操作失败: {}", e.getMessage()); - // 刷新失败不影响后续流程,但记录错误日志 + // 刷新失败不影响后续流程,只记录警告日志 } } - /** - * 更新二维码信息 - */ - private void updateQrCodeInfo(LinkTask linkTask) { - try { - LocalDateTime now = LocalDateTime.now(); - linkTask.setQrCreatedAt(now); - linkTask.setQrExpireAt(now.plusSeconds(60)); // 60秒后过期 - linkTask.setUpdatedAt(now); - linkTaskMapper.update(linkTask); - - log.info("更新二维码信息成功: qrCreatedAt={}, qrExpireAt={}", - linkTask.getQrCreatedAt(), linkTask.getQrExpireAt()); - } catch (Exception e) { - log.warn("更新二维码信息失败: {}", e.getMessage()); - } - } + /** * 构建用户端状态响应 @@ -387,58 +356,14 @@ public class LinkStatusService { private UserLinkStatusResponse buildUserStatusResponse(LinkTask linkTask) { UserLinkStatusResponse response = new UserLinkStatusResponse(); - // 基本状态信息 - response.setStatus(linkTask.getStatus()); - response.setNeedRefresh(Boolean.TRUE.equals(linkTask.getNeedRefresh())); - response.setRegion(linkTask.getRegion()); - - // 确定视图类型 - String view = determineView(linkTask.getStatus(), response.getNeedRefresh()); - response.setView(view); - - // 如果状态是USING,设置二维码信息 - if ("USING".equals(linkTask.getStatus()) && linkTask.getQrCreatedAt() != null) { - UserLinkStatusResponse.QrInfo qrInfo = new UserLinkStatusResponse.QrInfo(); - qrInfo.setUrl(scriptClient.getProxyQrCodeUrl(linkTask.getCodeNo())); - qrInfo.setCreatedAt(java.sql.Timestamp.valueOf(linkTask.getQrCreatedAt()).getTime()); - if (linkTask.getQrExpireAt() != null) { - qrInfo.setExpireAt(java.sql.Timestamp.valueOf(linkTask.getQrExpireAt()).getTime()); - } - response.setQr(qrInfo); - } - - // 如果状态是LOGGED_IN,设置资源信息 - if ("LOGGED_IN".equals(linkTask.getStatus())) { - UserLinkStatusResponse.AssetsInfo assets = new UserLinkStatusResponse.AssetsInfo(); - assets.setBase(String.format("http://36.138.184.60:12345/%s/", linkTask.getCodeNo())); - assets.setFirstHome("首次主页.png"); - assets.setFirstBonus("首次赏金.png"); - assets.setMidBonus("中途赏金.png"); - assets.setEndBonus("结束赏金.png"); - response.setAssets(assets); - } + // 如果状态是USING,返回NEW给用户端 + String statusToReturn = "USING".equals(linkTask.getStatus()) ? "NEW" : linkTask.getStatus(); + response.setStatus(statusToReturn); return response; } - /** - * 确定视图类型 - */ - private String determineView(String status, boolean needRefresh) { - switch (status) { - case "NEW": - return needRefresh ? "REFRESH" : "FIRST"; - case "USING": - return "SCAN"; - case "LOGGED_IN": - return "SECOND"; - case "REFUNDED": - case "EXPIRED": - return "EXPIRED"; - default: - return "FIRST"; - } - } + /** * 选区操作 @@ -471,6 +396,14 @@ public class LinkStatusService { log.info("查询到链接任务: id={}, codeNo={}, status={}, needRefresh={}", linkTask.getId(), linkTask.getCodeNo(), linkTask.getStatus(), linkTask.getNeedRefresh()); + // 查询批次信息获取times参数 + LinkBatch linkBatch = linkBatchMapper.findById(linkTask.getBatchId()); + if (linkBatch == null) { + log.error("批次信息不存在: batchId={}", linkTask.getBatchId()); + throw new IllegalStateException("批次信息不存在"); + } + log.info("查询到批次信息: batchId={}, times={}", linkBatch.getId(), linkBatch.getTimes()); + // 3. 检查链接状态,只有NEW状态才能选区 if (!"NEW".equals(linkTask.getStatus())) { log.error("链接状态不正确,无法选区: status={}", linkTask.getStatus()); @@ -509,29 +442,42 @@ public class LinkStatusService { deviceStatus.getTotalDevices(), deviceStatus.getAvailableCount(), deviceStatus.getAvailableDevices()); // 7. 选择一个空闲设备 - String selectedDevice = deviceStatus.getAvailableDevices().get(0); // 选择第一个空闲设备 - log.info("选择设备: {}", selectedDevice); +// String selectedDevice = deviceStatus.getAvailableDevices().get(0); // 选择第一个空闲设备 + String selectedDevice = "cc2"; + log.info("从空闲设备列表中选择设备: {}", selectedDevice); + log.info("设备选择详情: 可用设备总数={}, 选择了第一个设备={}", + deviceStatus.getAvailableDevices().size(), selectedDevice); - // 8. 为选中的设备创建代理code + // 8. 调用保存总次数接口 + try { + scriptClient.saveTotalTimes(linkBatch.getTimes()).block(); + // saveTotalTimes方法已经包含了详细的日志记录 + } catch (Exception e) { + log.warn("保存总次数接口调用失败: {}", e.getMessage()); + // 不影响后续流程,只记录警告日志 + } + + // 9. 为选中的设备创建代理code String proxyCode = deviceCodeMappingService.createProxyCode(selectedDevice); log.info("为设备 {} 创建代理code: {}", selectedDevice, proxyCode); - // 9. 调用脚本端选区,使用选中的设备 + // 10. 调用脚本端选区,使用选中的设备 log.info("开始调用脚本端选区,设备={}, 区域={}", selectedDevice, region); String selectResult = scriptClient.selectRegion(selectedDevice, region).block(); log.info("脚本端选区结果: {}", selectResult); - // 10. 等待脚本端生成二维码(这里可以添加轮询逻辑) + // 11. 等待脚本端生成二维码(这里可以添加轮询逻辑) log.info("等待脚本端生成二维码,等待3秒..."); Thread.sleep(3000); - // 11. 更新数据库状态为USING,保存设备信息和代理code + // 12. 更新数据库状态为USING,保存设备信息和代理code LocalDateTime now = LocalDateTime.now(); linkTask.setStatus("USING"); linkTask.setRegion(region); linkTask.setCodeNo(proxyCode); // 使用代理code替换原来的codeNo linkTask.setQrCreatedAt(now); linkTask.setQrExpireAt(now.plusSeconds(60)); // 60秒后过期 + linkTask.setFirstRegionSelectAt(now); // 记录首次选区时间 linkTask.setNeedRefresh(false); linkTask.setUpdatedAt(now); // 在machineId字段保存真实设备编号,便于调试和维护 @@ -540,13 +486,13 @@ public class LinkStatusService { log.info("链接状态更新成功: status=USING, region={}, proxyCode={}, device={}", region, proxyCode, selectedDevice); - // 12. 构建响应 + // 13. 构建响应 SelectRegionResponse response = new SelectRegionResponse(true, "选区成功"); response.setQrCodeUrl(scriptClient.getProxyQrCodeUrl(proxyCode)); response.setQrCreatedAt(now); response.setQrExpireAt(linkTask.getQrExpireAt()); response.setStatus("USING"); - response.setRegion(region); + // 不返回选区字段:response.setRegion(region); response.setQrDelaySeconds(5); // 客户端收到响应后,等待5秒再请求二维码 log.info("=== 选区操作完成 ==="); diff --git a/src/main/resources/mapper/agent/LinkTaskMapper.xml b/src/main/resources/mapper/agent/LinkTaskMapper.xml index cf27840..58e0695 100644 --- a/src/main/resources/mapper/agent/LinkTaskMapper.xml +++ b/src/main/resources/mapper/agent/LinkTaskMapper.xml @@ -16,32 +16,37 @@ + + + + + - INSERT INTO link_task (batch_id, agent_id, code_no, token_hash, expire_at, status, region, machine_id, login_at, refund_at, revoked_at) - VALUES (#{batchId}, #{agentId}, #{codeNo}, #{tokenHash}, #{expireAt}, #{status}, #{region}, #{machineId}, #{loginAt}, #{refundAt}, #{revokedAt}) + INSERT INTO link_task (batch_id, agent_id, code_no, token_hash, expire_at, status, region, machine_id, login_at, refund_at, revoked_at, need_refresh, refresh_time, qr_created_at, qr_expire_at, first_region_select_at) + VALUES (#{batchId}, #{agentId}, #{codeNo}, #{tokenHash}, #{expireAt}, #{status}, #{region}, #{machineId}, #{loginAt}, #{refundAt}, #{revokedAt}, #{needRefresh}, #{refreshTime}, #{qrCreatedAt}, #{qrExpireAt}, #{firstRegionSelectAt}) @@ -53,6 +58,11 @@ login_at = #{loginAt}, refund_at = #{refundAt}, revoked_at = #{revokedAt}, + need_refresh = #{needRefresh}, + refresh_time = #{refreshTime}, + qr_created_at = #{qrCreatedAt}, + qr_expire_at = #{qrExpireAt}, + first_region_select_at = #{firstRegionSelectAt}, WHERE id = #{id} @@ -68,7 +78,7 @@ - SELECT id, batch_id, agent_id, code_no, token_hash, expire_at, status, region, machine_id, login_at, refund_at, revoked_at, created_at, updated_at + SELECT id, batch_id, agent_id, code_no, token_hash, expire_at, status, region, machine_id, login_at, refund_at, revoked_at, created_at, updated_at, need_refresh, refresh_time, qr_created_at, qr_expire_at, first_region_select_at FROM link_task WHERE agent_id = #{agentId} AND code_no IN