diff --git a/.idea/mybatisx/templates.xml b/.idea/mybatisx/templates.xml index 840a8f8..58e393c 100644 --- a/.idea/mybatisx/templates.xml +++ b/.idea/mybatisx/templates.xml @@ -48,8 +48,8 @@ diff --git a/pom.xml b/pom.xml index a242365..3be4919 100644 --- a/pom.xml +++ b/pom.xml @@ -53,6 +53,15 @@ spring-boot-starter-validation + + org.springframework.boot + spring-boot-starter-data-redis + + + + + + junit junit diff --git a/src/main/java/com/ping/study/config/WebMvc.java b/src/main/java/com/ping/study/config/WebMvc.java index 87445bf..a07de3f 100644 --- a/src/main/java/com/ping/study/config/WebMvc.java +++ b/src/main/java/com/ping/study/config/WebMvc.java @@ -2,14 +2,27 @@ package com.ping.study.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebMvc implements WebMvcConfigurer { - @Bean - public WebClient webClient() { + // 原有的 NBA WebClient + @Bean("nbaWebClient") + public WebClient nbaWebClient() { return WebClient.create("https://api.nba.cn/sib/v2"); } + + // 新增的腾讯视频 WebClient + @Bean("tencentWebClient") + public WebClient tencentWebClient() { + return WebClient.create("https://infozb6.video.qq.com"); + } + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } } diff --git a/src/main/java/com/ping/study/controller/NbaController.java b/src/main/java/com/ping/study/controller/NbaController.java index be7941f..b305f9c 100644 --- a/src/main/java/com/ping/study/controller/NbaController.java +++ b/src/main/java/com/ping/study/controller/NbaController.java @@ -48,6 +48,12 @@ public class NbaController { log.info("执行定时方法添加当天赛程"); return nbaApi.addGames(); } + @Scheduled(cron = "0 0 0 * * ?") + @RequestMapping("/updateLive") + public void updateLive() { + log.info("执行定时方法更新当天赛程直播链接"); + + } @RequestMapping("/games") public List getGames() { log.info("获取所有赛程"); diff --git a/src/main/java/com/ping/study/controller/tx/LiveInfoController.java b/src/main/java/com/ping/study/controller/tx/LiveInfoController.java new file mode 100644 index 0000000..766ad25 --- /dev/null +++ b/src/main/java/com/ping/study/controller/tx/LiveInfoController.java @@ -0,0 +1,24 @@ +package com.ping.study.controller.tx; + +import com.ping.study.service.tx.LiveInfoService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Mono; + +@RestController +@RequestMapping("/tx/nba") +public class LiveInfoController { + + private final LiveInfoService liveInfoService; + + public LiveInfoController(LiveInfoService liveInfoService) { + this.liveInfoService = liveInfoService; + } + + @GetMapping("/live/{cnlid}") + public String getLiveInfo(@PathVariable String cnlid) { + return liveInfoService.getLiveInfo(cnlid); + } +} diff --git a/src/main/java/com/ping/study/controller/tx/MatchController.java b/src/main/java/com/ping/study/controller/tx/MatchController.java new file mode 100644 index 0000000..97cbbec --- /dev/null +++ b/src/main/java/com/ping/study/controller/tx/MatchController.java @@ -0,0 +1,50 @@ +package com.ping.study.controller.tx; + +import com.ping.study.model.dto.tx.MatchListRequest; +import com.ping.study.service.tx.MatchService; +import com.ping.study.service.tx.SportsQqService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Mono; + +import java.time.LocalDate; +import java.util.List; + +@RestController +@RequestMapping("/tx/nba") +public class MatchController { + private final SportsQqService sportsQqService; + private final MatchService matchService; + + public MatchController(MatchService matchService,SportsQqService sportsQqService) { + this.sportsQqService = sportsQqService; + this.matchService = matchService; + } + + @GetMapping("/matches") + public Mono getMatches( + @RequestParam(required = false, defaultValue = "100000") Integer columnId, + @RequestParam String startTime, + @RequestParam String endTime) { + + MatchListRequest request = new MatchListRequest(); + request.setColumnId(columnId); + request.setStartTime(LocalDate.parse(startTime)); + request.setEndTime(LocalDate.parse(endTime)); + + return sportsQqService.getMatchList(request); + } + + @GetMapping("/lives") + public List getPlayoffLiveIds( + @RequestParam String startTime, + @RequestParam String endTime) { + + LocalDate startDate = LocalDate.parse(startTime); + LocalDate endDate = LocalDate.parse(endTime); + + return matchService.getPlayoffLiveIdsBlocking(startDate, endDate); + } +} \ No newline at end of file diff --git a/src/main/java/com/ping/study/controller/tx/TokenRefreshController.java b/src/main/java/com/ping/study/controller/tx/TokenRefreshController.java new file mode 100644 index 0000000..2b40fbc --- /dev/null +++ b/src/main/java/com/ping/study/controller/tx/TokenRefreshController.java @@ -0,0 +1,22 @@ +package com.ping.study.controller.tx; + + +import com.ping.study.service.tx.TxSportTokenRefreshService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/tx") +public class TokenRefreshController { + + @Autowired + private TxSportTokenRefreshService txSportTokenRefreshService; + + + @RequestMapping("/refresh") + public Boolean refreshToken() + { + return txSportTokenRefreshService.refreshCookies(); + } +} diff --git a/src/main/java/com/ping/study/mapper/GamesMapper.java b/src/main/java/com/ping/study/mapper/GamesMapper.java index 684058a..85222ad 100644 --- a/src/main/java/com/ping/study/mapper/GamesMapper.java +++ b/src/main/java/com/ping/study/mapper/GamesMapper.java @@ -5,9 +5,9 @@ import com.ping.study.pojo.Games; import java.util.List; /** -* @author Administrator +* @author Ping01 * @description 针对表【games】的数据库操作Mapper -* @createDate 2025-04-17 21:23:08 +* @createDate 2025-04-20 20:13:51 * @Entity com.ping.study.pojo.Games */ public interface GamesMapper { @@ -30,4 +30,5 @@ public interface GamesMapper { //删除所有赛程 void deleteAllGames(); + } diff --git a/src/main/java/com/ping/study/model/dto/tx/LiveInfoRequest.java b/src/main/java/com/ping/study/model/dto/tx/LiveInfoRequest.java new file mode 100644 index 0000000..dfade6e --- /dev/null +++ b/src/main/java/com/ping/study/model/dto/tx/LiveInfoRequest.java @@ -0,0 +1,42 @@ +package com.ping.study.model.dto.tx; + +import lombok.Data; +import java.time.DayOfWeek; +import java.util.Map; + +@Data +public class LiveInfoRequest { + + private String ckey; + private String encryptVer; + private String platform; + private String tm; + private String cnlid; + //private String cookie = + + public static final String getCookie() { + return "pgv_pvid=67939534; fqm_pvqid=5e0a1a80-2ffb-42c9-8878-99ef08c0bd21; RK=dudkRjOGUW; ptcz=d9e319494b15cb621d9f54a8e03a1b55fb6c6c3564ff6c4985d1ff7b4797ce4e; video_platform=2; qq_domain_video_guid_verify=119f9c981e9ffd34; _qimei_uuid42=184170b170810037e9eacb93c4fe35a57c7df28f38; _qimei_q36=; _qimei_h38=215914e0566ee066f810eb4b0200000e81840b; video_guid=119f9c981e9ffd34; tvfe_boss_uuid=d41b10b066856291; eas_sid=E1i7L1k4j4D7q1D0Q5J894p636; livelink_pvid=7900946432; check_16=584445b5d815fd331bb7206205f8c603; _qimei_fingerprint=3c06d28104a1e802703470a164d81dca; vqq_refresh_token=; vqq_next_refresh_time=6196; vqq_login_time_init=1745153828; v_qq_com_session_lapse_time=1745160024778; vqq_appid=101481799; vqq_openid=2E6A77E3002C87A9CA68908FB058D4E5; vqq_access_token=308C7CAFBC867AF65212034AC90E71D9; vqq_vuserid=3468482246; vqq_vusession=Kcz0B9FBUDZuIvwHKErD3g.N"; + } + + // 密钥映射表 + private static final Map WEEKDAY_KEYS = Map.of( + DayOfWeek.MONDAY, "06fc1464", + DayOfWeek.TUESDAY, "4244ce1b", + DayOfWeek.WEDNESDAY, "77de31c5", + DayOfWeek.THURSDAY, "e0149fa2", + DayOfWeek.FRIDAY, "60394ced", + DayOfWeek.SATURDAY, "2da639f0", + DayOfWeek.SUNDAY, "c2f0cf9f" + ); + + // 生成加密版本号 + public static String generateEncryptVer(DayOfWeek dayOfWeek) { + int dayValue = dayOfWeek == DayOfWeek.SUNDAY ? 7 : dayOfWeek.getValue(); + return "7." + dayValue; + } + + // 获取当天密钥 + public static String getTodaySecretKey() { + return WEEKDAY_KEYS.get(DayOfWeek.from(java.time.LocalDate.now())); + } +} \ No newline at end of file diff --git a/src/main/java/com/ping/study/model/dto/tx/MatchListRequest.java b/src/main/java/com/ping/study/model/dto/tx/MatchListRequest.java new file mode 100644 index 0000000..f1bdb7b --- /dev/null +++ b/src/main/java/com/ping/study/model/dto/tx/MatchListRequest.java @@ -0,0 +1,18 @@ +package com.ping.study.model.dto.tx; + +import lombok.Data; +import java.time.LocalDate; + +@Data +public class MatchListRequest { + private Integer columnId = 100000; // 默认值 + private LocalDate startTime; + private LocalDate endTime; + + // 自定义参数校验逻辑(可选) + public void validate() { + if (startTime.isAfter(endTime)) { + throw new IllegalArgumentException("开始时间不能晚于结束时间"); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/ping/study/model/vo/tx/MatchInfo.java b/src/main/java/com/ping/study/model/vo/tx/MatchInfo.java new file mode 100644 index 0000000..1f516fa --- /dev/null +++ b/src/main/java/com/ping/study/model/vo/tx/MatchInfo.java @@ -0,0 +1,11 @@ +package com.ping.study.model.vo.tx; + +import lombok.Data; + +@Data +public class MatchInfo { + + private String liveId; + private String matchType; + // 其他需要的字段... +} diff --git a/src/main/java/com/ping/study/model/vo/tx/MatchResponse.java b/src/main/java/com/ping/study/model/vo/tx/MatchResponse.java new file mode 100644 index 0000000..89de24b --- /dev/null +++ b/src/main/java/com/ping/study/model/vo/tx/MatchResponse.java @@ -0,0 +1,14 @@ +package com.ping.study.model.vo.tx; + +import lombok.Data; +import java.util.List; +import java.util.Map; + +@Data +public class MatchResponse { + private Integer code; + private String msg; + private Map> data; + + +} diff --git a/src/main/java/com/ping/study/pojo/Games.java b/src/main/java/com/ping/study/pojo/Games.java index 12b1cc1..b865b9d 100644 --- a/src/main/java/com/ping/study/pojo/Games.java +++ b/src/main/java/com/ping/study/pojo/Games.java @@ -1,5 +1,6 @@ package com.ping.study.pojo; +import java.io.Serializable; import java.util.Date; import lombok.Data; @@ -8,7 +9,7 @@ import lombok.Data; * @TableName games */ @Data -public class Games { +public class Games implements Serializable { /** * */ @@ -24,6 +25,11 @@ public class Games { */ private String gameId; + /** + * + */ + private String playId; + /** * */ @@ -48,4 +54,6 @@ public class Games { * */ private String awayTeamLogoDark; + + private static final long serialVersionUID = 1L; } \ No newline at end of file diff --git a/src/main/java/com/ping/study/service/tx/LiveInfoService.java b/src/main/java/com/ping/study/service/tx/LiveInfoService.java new file mode 100644 index 0000000..a827be4 --- /dev/null +++ b/src/main/java/com/ping/study/service/tx/LiveInfoService.java @@ -0,0 +1,77 @@ +package com.ping.study.service.tx; + +import com.ping.study.model.dto.tx.LiveInfoRequest; +import com.ping.study.utils.tx.CKeyGenerator; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; + +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import java.time.DayOfWeek; +import java.time.Instant; +import java.time.LocalDate; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class LiveInfoService { + + private final WebClient webClient; + private final CKeyGenerator cKeyGenerator; + + private StringRedisTemplate stringRedisTemplate; + + public LiveInfoService(@Qualifier("tencentWebClient")WebClient webClient, CKeyGenerator cKeyGenerator, StringRedisTemplate stringRedisTemplate) { + this.webClient = webClient; + this.cKeyGenerator = cKeyGenerator; + this.stringRedisTemplate = stringRedisTemplate; + } + + // 2. 远程调用时拼接 Cookie + public String getCookieForRequest() { +// 读取 + String cookie = stringRedisTemplate.opsForValue().get("tx_sports_cookie"); + return cookie; + } + + public String getLiveInfo(String cnlid) { + String platform = "40201"; + String ckey = cKeyGenerator.generateCKey(cnlid, platform); + + // 1. 打印请求参数 + String queryParams = String.format( + "ckey=%s&encrypt_ver=%s&platform=%s&tm=%d&cnlid=%s", + ckey, + LiveInfoRequest.generateEncryptVer(DayOfWeek.from(LocalDate.now())), + platform, + Instant.now().getEpochSecond(), + cnlid + ); + log.info("请求参数: {}", queryParams); + + // 2. 打印请求头(Cookie) + String cookie = getCookieForRequest(); + log.info("请求头 Cookie: {}", cookie); + + // 3. 发起请求 + return webClient.get() + .uri(uriBuilder -> uriBuilder + .path("/cgi-bin/getliveinfo") + .queryParam("ckey", ckey) + .queryParam("encrypt_ver", LiveInfoRequest.generateEncryptVer( + DayOfWeek.from(LocalDate.now()))) + .queryParam("platform", platform) + .queryParam("tm", Instant.now().getEpochSecond()) + .queryParam("cnlid", cnlid) + .build()) + .header("Cookie", "pgv_pvid=67939534; fqm_pvqid=5e0a1a80-2ffb-42c9-8878-99ef08c0bd21; RK=dudkRjOGUW; ptcz=d9e319494b15cb621d9f54a8e03a1b55fb6c6c3564ff6c4985d1ff7b4797ce4e; video_platform=2; qq_domain_video_guid_verify=119f9c981e9ffd34; _qimei_uuid42=184170b170810037e9eacb93c4fe35a57c7df28f38; _qimei_q36=; _qimei_h38=215914e0566ee066f810eb4b0200000e81840b; video_guid=119f9c981e9ffd34; tvfe_boss_uuid=d41b10b066856291; eas_sid=E1i7L1k4j4D7q1D0Q5J894p636; livelink_pvid=7900946432; check_16=584445b5d815fd331bb7206205f8c603; o2_uin=941039061; uin_cookie=o0941039061; ied_qq=o0941039061; o_cookie=941039061; pac_uid=0_ePEHB57t75p7c; LW_sid=31h7T2M8f3O5O280q0K0L365H2; LW_uid=P1J7P2i8S3R5a2r0j020t3n543; ptui_loginuin=1131302745; qq_nick=A; qq_head=https://tvpic.gtimg.cn/head/dbb128053992612c0c1327e8e32e95f8da39a3ee5e6b4b0d3255bfef95601890afd80709/346; vqq_refresh_token=; vqq_next_refresh_time=6505; vqq_login_time_init=1745154214; _qimei_fingerprint=829539b60aaa7ed28f0e5f604c391a63; main_login=qq; vqq_appid=101481799; vqq_openid=2E6A77E3002C87A9CA68908FB058D4E5; vqq_access_token=308C7CAFBC867AF65212034AC90E71D9; vqq_vuserid=3468482246; vqq_vusession=H1-ZxeRN0vDUVk4_wKLkyA.N") + .retrieve() + .bodyToMono(String.class) + .block(); + } +} \ No newline at end of file diff --git a/src/main/java/com/ping/study/service/tx/MatchService.java b/src/main/java/com/ping/study/service/tx/MatchService.java new file mode 100644 index 0000000..22fbdda --- /dev/null +++ b/src/main/java/com/ping/study/service/tx/MatchService.java @@ -0,0 +1,60 @@ +package com.ping.study.service.tx; + +import com.ping.study.model.vo.tx.MatchInfo; +import com.ping.study.model.vo.tx.MatchResponse; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Flux; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class MatchService { + + private final WebClient webClient; + private static final DateTimeFormatter DATE_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + public MatchService(WebClient.Builder webClientBuilder) { + this.webClient = webClientBuilder + .baseUrl("https://matchweb.sports.qq.com") + .build(); + } + + /** + * 获取指定日期范围内所有NBA季后赛(matchType=2)的直播ID + */ + public List getPlayoffLiveIdsBlocking(LocalDate startDate, LocalDate endDate) { + return webClient.get() + .uri(uriBuilder -> uriBuilder + .path("/matchUnion/list") + .queryParam("columnId", 100000) + .queryParam("startTime", startDate.format(DATE_FORMATTER)) + .queryParam("endTime", endDate.format(DATE_FORMATTER)) + .build()) + .retrieve() + .bodyToMono(MatchResponse.class) + .flatMapMany(response -> { + if (response.getCode() != 0 || response.getData() == null) { + return Flux.error(new RuntimeException("API返回异常: " + response.getMsg())); + } + // 提取所有matchType=2的比赛 + List liveIds = response.getData().values().stream() + .flatMap(List::stream) + .filter(match -> "2".equals(match.getMatchType())) + .map(MatchInfo::getLiveId) + .collect(Collectors.toList()); + return Flux.fromIterable(liveIds); + }) + .collectList() + .block(); // 阻塞等待结果 + } + + /** + * 获取完整比赛信息(过滤matchType=2) + */ +} \ No newline at end of file diff --git a/src/main/java/com/ping/study/service/tx/QQCookieService.java b/src/main/java/com/ping/study/service/tx/QQCookieService.java new file mode 100644 index 0000000..4a3d062 --- /dev/null +++ b/src/main/java/com/ping/study/service/tx/QQCookieService.java @@ -0,0 +1,36 @@ +package com.ping.study.service.tx; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +@Service +public class QQCookieService { + @Autowired + private StringRedisTemplate stringRedisTemplate; + + private static final String REDIS_KEY = "user:nba"; // 固定 Key + + // 存储 Cookie(无需 uin 参数) + public void saveCookie(Map cookies) { + stringRedisTemplate.opsForHash().putAll(REDIS_KEY, cookies); + stringRedisTemplate.expire(REDIS_KEY, 30, TimeUnit.DAYS); + } + + // 获取 Cookie(无需 uin 参数) + public Map getCookie() { + Map entries = stringRedisTemplate.opsForHash().entries(REDIS_KEY); + Map result = new HashMap<>(); + + entries.forEach((key, value) -> + result.put(key.toString(), value.toString()) + ); + + return result; + } +} diff --git a/src/main/java/com/ping/study/service/tx/QQRequestService.java b/src/main/java/com/ping/study/service/tx/QQRequestService.java new file mode 100644 index 0000000..eb04f90 --- /dev/null +++ b/src/main/java/com/ping/study/service/tx/QQRequestService.java @@ -0,0 +1,49 @@ +package com.ping.study.service.tx; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.io.IOException; +import java.util.Map; + +@Service +public class QQRequestService { + @Autowired + private RestTemplate restTemplate; // 现在可以正常注入 + @Autowired + private QQCookieService qqCookieService; + @Autowired + private QQTokenRefreshService tokenRefreshService; + + // 发起请求(自动处理 Cookie 和 Token 刷新) + public String fetchData(String uin, String apiUrl) throws IOException { + Map cookies = qqCookieService.getCookie(); + + // 检查 Token 是否即将过期 + long expiresIn = Long.parseLong(cookies.getOrDefault("vqq_next_refresh_time", "0")); + if (expiresIn < 300) { // 剩余 <5 分钟时刷新 + tokenRefreshService.refreshToken(); + cookies = qqCookieService.getCookie(); // 重新加载 + } + + // 构造请求头(模拟浏览器) + HttpHeaders headers = new HttpHeaders(); + headers.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"); + cookies.forEach((name, value) -> headers.add("Cookie", name + "=" + value)); + + // 发起请求 + ResponseEntity response = restTemplate.exchange( + apiUrl, + HttpMethod.GET, + new HttpEntity<>(headers), + String.class + ); + + return response.getBody(); + } +} \ No newline at end of file diff --git a/src/main/java/com/ping/study/service/tx/QQTokenRefreshService.java b/src/main/java/com/ping/study/service/tx/QQTokenRefreshService.java new file mode 100644 index 0000000..2a57736 --- /dev/null +++ b/src/main/java/com/ping/study/service/tx/QQTokenRefreshService.java @@ -0,0 +1,50 @@ +package com.ping.study.service.tx; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.io.IOException; +import java.util.Map; + +@Service +public class QQTokenRefreshService { + @Autowired + private QQCookieService qqCookieService; + @Autowired + private RestTemplate restTemplate; + + // 刷新 Token + public boolean refreshToken() throws IOException { + Map cookies = qqCookieService.getCookie(); + String refreshToken = cookies.get("vqq_refresh_token"); + if (refreshToken == null || refreshToken.isEmpty()) { + throw new IllegalStateException("No refresh_token available, need re-login"); + } + + // 调用 QQ 的 Token 刷新接口(示例,需替换真实 API) + String url = "https://xui.ptlogin2.qq.com/refresh?refresh_token=" + refreshToken; + ResponseEntity response = restTemplate.getForEntity(url, String.class); + + if (response.getStatusCode().is2xxSuccessful()) { + // 解析新 Token 并更新 Cookie + String newAccessToken = parseNewToken(response.getBody()); + cookies.put("vqq_access_token", newAccessToken); + cookies.put("vqq_next_refresh_time", "7200"); // 假设新有效期 2 小时 + qqCookieService.saveCookie(cookies); + return true; + } else { + return false; // 刷新失败,需重新登录 + } + } + + private String parseNewToken(String responseBody) throws IOException { + ObjectMapper mapper = new ObjectMapper(); + JsonNode root = mapper.readTree(responseBody); + return root.get("access_token").asText(); + } +} \ No newline at end of file diff --git a/src/main/java/com/ping/study/service/tx/SportsQqService.java b/src/main/java/com/ping/study/service/tx/SportsQqService.java new file mode 100644 index 0000000..bd08dc8 --- /dev/null +++ b/src/main/java/com/ping/study/service/tx/SportsQqService.java @@ -0,0 +1,36 @@ +package com.ping.study.service.tx; + +import com.ping.study.model.dto.tx.MatchListRequest; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; +import java.time.format.DateTimeFormatter; + +@Service +public class SportsQqService { + + private final WebClient webClient; + private static final DateTimeFormatter DATE_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + public SportsQqService(WebClient.Builder webClientBuilder) { + this.webClient = webClientBuilder + .baseUrl("https://matchweb.sports.qq.com") + .defaultHeader("User-Agent", "YourApp/1.0") + .build(); + } + + public Mono getMatchList(MatchListRequest request) { + request.validate(); // 参数校验 + + return webClient.get() + .uri(uriBuilder -> uriBuilder + .path("/matchUnion/list") + .queryParam("columnId", request.getColumnId()) + .queryParam("startTime", request.getStartTime().format(DATE_FORMATTER)) + .queryParam("endTime", request.getEndTime().format(DATE_FORMATTER)) + .build()) + .retrieve() + .bodyToMono(String.class); + } +} \ No newline at end of file diff --git a/src/main/java/com/ping/study/service/tx/TxSportTokenRefreshService.java b/src/main/java/com/ping/study/service/tx/TxSportTokenRefreshService.java new file mode 100644 index 0000000..17ad8b7 --- /dev/null +++ b/src/main/java/com/ping/study/service/tx/TxSportTokenRefreshService.java @@ -0,0 +1,108 @@ +package com.ping.study.service.tx; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +@Slf4j +@Service +public class TxSportTokenRefreshService { + + private final WebClient webClient; + private final StringRedisTemplate redisTemplate; + private static final String REDIS_COOKIE_KEY = "tx_sports_cookie"; + + public TxSportTokenRefreshService(WebClient.Builder builder, + StringRedisTemplate redisTemplate) { + this.webClient = builder.baseUrl("https://app.sports.qq.com").build(); + this.redisTemplate = redisTemplate; + } + + /** + * 刷新Cookie并存储到Redis + */ +// 在首次成功时,分离并存储基础登录态Cookie + public boolean refreshCookies() { + try { + // 1. 从Redis获取基础登录态Cookie(首次需手动初始化) + String baseCookie = redisTemplate.opsForValue().get("tx_base_cookies"); + if (baseCookie == null) { + baseCookie = "pgv_pvid=67939534; ptui_loginuin=1131302745; ..."; // 你的浏览器完整Cookie + } + + // 2. 携带基础Cookie发起请求 + String response = webClient.get() + .uri("/init/refresh?os=web") + .header("Cookie", baseCookie) // 关键点:始终携带基础登录态 + .retrieve() + .bodyToMono(String.class) + .block(); + + // 3. 合并新旧Cookie(保留基础登录态) + JsonNode data = new ObjectMapper().readTree(response).path("data"); + String newCookie = extractNewCookies(data); // 提取spt_token等 + String finalCookie = mergeCookies(baseCookie, newCookie); + + // 4. 存储到Redis + redisTemplate.opsForValue().set( + "tx_sports_cookies", + finalCookie, + Long.parseLong(data.path("tokenTTL").asText()), + TimeUnit.SECONDS + ); + return true; + } catch (Exception e) { + log.error("刷新失败", e); + return false; + } + } + + + // 合并Cookie的工具方法 + private String mergeCookies(String base, String news) { + Map map = new LinkedHashMap<>(); + // 先解析基础Cookie + Arrays.stream(base.split("; ")) + .forEach(c -> { + String[] kv = c.split("=", 2); + if (kv.length == 2) map.put(kv[0], kv[1]); + }); + // 用新Cookie覆盖 + Arrays.stream(news.split("; ")) + .forEach(c -> { + String[] kv = c.split("=", 2); + if (kv.length == 2) map.put(kv[0], kv[1]); + }); + // 重新拼接 + return map.entrySet().stream() + .map(e -> e.getKey() + "=" + e.getValue()) + .collect(Collectors.joining("; ")); + } + + /** + * 定时刷新任务(每1小时执行一次) + */ +// @Scheduled(fixedRate = 3600000) // 单位:毫秒 +// public void scheduledRefresh() { +// if (!refreshCookies()) { +// log.warn("定时刷新Cookie失败,将重试..."); +// // 失败后延迟5分钟重试 +// try { +// Thread.sleep(300000); +// refreshCookies(); +// } catch (InterruptedException ignored) {} +// } +// } +} \ No newline at end of file diff --git a/src/main/java/com/ping/study/utils/NbaApi.java b/src/main/java/com/ping/study/utils/NbaApi.java index 0669e3d..2a229d9 100644 --- a/src/main/java/com/ping/study/utils/NbaApi.java +++ b/src/main/java/com/ping/study/utils/NbaApi.java @@ -6,14 +6,17 @@ import com.ping.study.model.vo.Game; import com.ping.study.model.vo.Group; import com.ping.study.pojo.Games; import com.ping.study.service.GamesService; +import com.ping.study.service.tx.MatchService; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.client.WebClient; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.time.LocalDate; import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -28,7 +31,10 @@ public class NbaApi { private WebClient webClient; - public NbaApi(WebClient webClient) { + @Autowired + private MatchService matchService; + + public NbaApi(@Qualifier("nbaWebClient")WebClient webClient) { this.webClient = webClient; } @@ -37,6 +43,8 @@ public class NbaApi { NbaStatsRequestDto requestParams = new NbaStatsRequestDto(); log.info("{}", requestParams); log.info("进入{},执行{}", this.getClass().getName(), "getGames"); + //获取所有当天直播id + List lives = matchService.getPlayoffLiveIdsBlocking(LocalDate.now(), LocalDate.now()); List gameEntities = webClient.get() .uri("/game/schedule", uriBuilder -> uriBuilder .queryParam("app_key", requestParams.getAppKey()) @@ -60,17 +68,17 @@ public class NbaApi { List entities = new ArrayList<>(); // 遍历groups中的games for (Group group : response.getData().getGroups()) { - for (Game game : group.getGames()) { + for (int i = 0; i < group.getGames().size(); i++){ Games entity = new Games(); entity.setDate(new SimpleDateFormat("yyyy-MM-dd").format(new Date())); - entity.setGameId(game.getGameId()); - entity.setHomeTeamName(game.getHomeTeamName()); - entity.setAwayTeamName(game.getAwayTeamName()); - entity.setHomeTeamLogoDark(game.getHomeTeamLogoDark()); - entity.setAwayTeamLogoDark(game.getAwayTeamLogoDark()); - + entity.setGameId(group.getGames().get(i).getGameId()); + entity.setHomeTeamName(group.getGames().get(i).getHomeTeamName()); + entity.setAwayTeamName(group.getGames().get(i).getAwayTeamName()); + entity.setHomeTeamLogoDark(group.getGames().get(i).getHomeTeamLogoDark()); + entity.setAwayTeamLogoDark(group.getGames().get(i).getAwayTeamLogoDark()); + entity.setPlayId(lives.get(i)); // 合并日期和时间 - String dateTimeStr = game.getStartDate() + " " + game.getStartTime(); + String dateTimeStr = group.getGames().get(i).getStartDate() + " " + group.getGames().get(i).getStartTime(); SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); try { entity.setStartTime(format.parse(dateTimeStr)); @@ -78,9 +86,31 @@ public class NbaApi { // 处理异常或设置默认值 entity.setStartTime(null); } + gamesService.insertGames(entity); entities.add(entity); } +// for (Game game : group.getGames()) { +// Games entity = new Games(); +// entity.setDate(new SimpleDateFormat("yyyy-MM-dd").format(new Date())); +// entity.setGameId(game.getGameId()); +// entity.setHomeTeamName(game.getHomeTeamName()); +// entity.setAwayTeamName(game.getAwayTeamName()); +// entity.setHomeTeamLogoDark(game.getHomeTeamLogoDark()); +// entity.setAwayTeamLogoDark(game.getAwayTeamLogoDark()); +// +// // 合并日期和时间 +// String dateTimeStr = game.getStartDate() + " " + game.getStartTime(); +// SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); +// try { +// entity.setStartTime(format.parse(dateTimeStr)); +// } catch (ParseException e) { +// // 处理异常或设置默认值 +// entity.setStartTime(null); +// } +// gamesService.insertGames(entity); +// entities.add(entity); +// } } return entities; }) diff --git a/src/main/java/com/ping/study/utils/tx/CKeyGenerator.java b/src/main/java/com/ping/study/utils/tx/CKeyGenerator.java new file mode 100644 index 0000000..66e86bb --- /dev/null +++ b/src/main/java/com/ping/study/utils/tx/CKeyGenerator.java @@ -0,0 +1,46 @@ +package com.ping.study.utils.tx; + +import com.ping.study.model.dto.tx.LiveInfoRequest; +import org.springframework.stereotype.Component; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.time.DayOfWeek; +import java.time.Instant; + +@Component +public class CKeyGenerator { + + public String generateCKey(String cnlid, String platform) { + // 获取当前时间戳(秒级) + String tm = String.valueOf(Instant.now().getEpochSecond()); + + // 获取当天星期几和密钥 + DayOfWeek today = DayOfWeek.from(java.time.LocalDate.now()); + String secretKey = LiveInfoRequest.getTodaySecretKey(); + String encryptVer = LiveInfoRequest.generateEncryptVer(today); + + // 构建输入字符串 + String input = secretKey + cnlid + tm + "*#06#" + platform; + + try { + // 计算MD5 + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] hash = md.digest(input.getBytes(StandardCharsets.UTF_8)); + + // 转换为16进制字符串 + StringBuilder hexString = new StringBuilder(); + for (byte b : hash) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + + return hexString.toString(); + } catch (Exception e) { + throw new RuntimeException("生成ckey失败", e); + } + } +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index e20553c..a0b2a8f 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,6 +1,10 @@ server: port: 9005 spring: + data: + redis: + host: 110.42.255.182 + port: 6739 application: name: NBA datasource: diff --git a/src/main/resources/mapper/GamesMapper.xml b/src/main/resources/mapper/GamesMapper.xml index 87795ae..5cdee35 100644 --- a/src/main/resources/mapper/GamesMapper.xml +++ b/src/main/resources/mapper/GamesMapper.xml @@ -8,6 +8,7 @@ + @@ -17,8 +18,8 @@ id,date,game_id, - start_time,home_team_name,away_team_name, - home_team_logo_dark,away_team_logo_dark + play_id,start_time,home_team_name, + away_team_name,home_team_logo_dark,away_team_logo_dark select @@ -114,8 +122,8 @@ delete from games