diff --git a/.idea/IntelliLang.xml b/.idea/IntelliLang.xml
new file mode 100644
index 0000000..49f06f1
--- /dev/null
+++ b/.idea/IntelliLang.xml
@@ -0,0 +1,278 @@
+
+
+
+
+ Apache HttpClient 4 HTTP Header (org.apache.http)
+
+
+
+
+
+ Apache HttpClient 5 HTTP Header (org.apache.hc.core5)
+
+
+
+
+
+
+
+
+ AsyncQueryRunner (org.apache.commons.dbutils)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Jodd (jodd.db)
+
+
+
+
+
+
+
+ MockServer Header (org.mockserver)
+
+
+
+
+
+
+ QueryRunner (org.apache.commons.dbutils)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ R2DBC (io.r2dbc)
+
+
+
+
+
+ Reactiverse Postgres Client (io.reactiverse)
+
+
+
+
+
+
+
+
+
+
+
+
+ RestAssured HTTP Header (io.restassured)
+
+
+
+
+
+
+
+ SmallRye Axle SqlClient (io.vertx.axle.sqlclient)
+
+
+
+
+
+ SmallRye Mutiny SqlClient (io.vertx.mutiny.sqlclient)
+
+
+
+
+
+ SmallRye Mutiny SqlConnection (io.vertx.mutiny.sqlclient)
+
+
+
+
+
+
+
+ Spring @Cacheable and @CacheEvict
+
+
+
+
+
+
+
+
+
+
+
+ Spring HttpHeaders (org.springframework.http)
+
+
+
+
+
+
+ Spring Integration/Messaging
+
+
+
+
+
+
+ Spring JDBC (org.springframework.jdbc.core.JdbcOperations)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Spring JDBC (org.springframework.jdbc.core.PreparedStatementCreatorFactory)
+
+
+
+
+
+
+ Spring JDBC (org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator)
+
+
+
+
+
+
+
+ Spring Security @PostAuthorize/@PostFilter/@PreAuthorize/@PreFilter/@AuthenticationPrincipal
+
+
+
+
+
+
+
+
+
+ Spring State Machine
+
+
+
+
+
+
+
+ Vert.x SQL Extensions (io.vertx.ext.sql)
+
+
+
+
+
+
+ Vert.x SQL Reactive Extensions (io.vertx.reactivex.ext.sql)
+
+
+
+
+
+
+
+
+
+ Vert.x SqlClient (io.vertx.sqlclient)
+
+
+
+
+
+
+
+
+
+
+ Vert.x SqlClient RxJava2 (io.vertx.reactivex.sqlclient)
+
+
+
+
+
+
+
+
+
+
+
+ WireMock (com.github.tomakehurst.wiremock.client)
+
+
+
+
+
+
+
+ WireMock (com.github.tomakehurst.wiremock.client)
+
+
+
+
+
+
+ WireMock (com.github.tomakehurst.wiremock.client)
+
+
+
+
+
+
+
+ jOOQ (org.jooq.DSLContext)
+
+
+
+
+
+
+
+ rxjava2-jdbc (org.davidmoten.rx.jdbc)
+
+
+
+
+
+
+ SpEL for Spring Cache
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml
index 72a378b..793b668 100644
--- a/.idea/dataSources.xml
+++ b/.idea/dataSources.xml
@@ -1,11 +1,35 @@
-
+
mysql.8
true
com.mysql.cj.jdbc.Driver
- jdbc:mysql://110.42.255.182:3306/NBA
+ jdbc:mysql://154.36.154.211:9002/nba
+
+
+
+
+
+ $ProjectFileDir$
+
+
+ redis
+ true
+ jdbc.RedisDriver
+ jdbc:redis://103.244.88.91:6739/
+
+
+
+
+
+ $ProjectFileDir$
+
+
+ redis
+ true
+ jdbc.RedisDriver
+ jdbc:redis://154.36.154.211:6379/0
diff --git a/.idea/db-forest-config.xml b/.idea/db-forest-config.xml
new file mode 100644
index 0000000..95e943d
--- /dev/null
+++ b/.idea/db-forest-config.xml
@@ -0,0 +1,11 @@
+
+
+
+ .
+ ----------------------------------------
+ 1:0:989a1610-33a4-4371-ba47-77639ee274e4
+ 2:0:198b1b07-b5bc-4475-ad72-25ab848d510c
+ 3:0:310cedf0-fad7-43c7-84b6-88e011089af6
+ .
+
+
\ No newline at end of file
diff --git a/.idea/dbnavigator.xml b/.idea/dbnavigator.xml
new file mode 100644
index 0000000..400481d
--- /dev/null
+++ b/.idea/dbnavigator.xml
@@ -0,0 +1,563 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 82dbec8..abc4263 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -8,7 +8,7 @@
-
+
\ No newline at end of file
diff --git a/.idea/mybatisx/templates.xml b/.idea/mybatisx/templates.xml
index 58e393c..b64be32 100644
--- a/.idea/mybatisx/templates.xml
+++ b/.idea/mybatisx/templates.xml
@@ -37,19 +37,18 @@
-
+
-
diff --git a/pom.xml b/pom.xml
index 3be4919..6f15bdc 100644
--- a/pom.xml
+++ b/pom.xml
@@ -68,6 +68,23 @@
3.8.1
test
+
+
+ com.sun.mail
+ jakarta.mail
+ 2.0.1
+
+
+
+ org.springframework.boot
+ spring-boot-starter-mail
+
+
+ javax.activation
+ activation
+
+
+
diff --git a/src/main/java/com/ping/study/config/GlobalException.java b/src/main/java/com/ping/study/config/GlobalException.java
index fdbac2b..1788047 100644
--- a/src/main/java/com/ping/study/config/GlobalException.java
+++ b/src/main/java/com/ping/study/config/GlobalException.java
@@ -7,6 +7,7 @@ import org.springframework.web.bind.annotation.RestControllerAdvice;
public class GlobalException {
@ExceptionHandler(Exception.class)
public Object handleException(Exception e) {
+ //e.printStackTrace();
e.printStackTrace();
return e.getMessage();
}
diff --git a/src/main/java/com/ping/study/config/RedisConf.java b/src/main/java/com/ping/study/config/RedisConf.java
new file mode 100644
index 0000000..7342d0a
--- /dev/null
+++ b/src/main/java/com/ping/study/config/RedisConf.java
@@ -0,0 +1,33 @@
+package com.ping.study.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+@Configuration
+public class RedisConf {
+
+
+ @Bean
+ public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
+
+ RedisTemplate template = new RedisTemplate<>();
+ template.setConnectionFactory(factory);
+
+ StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
+ GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer();
+
+ // key
+ template.setKeySerializer(stringRedisSerializer);
+ template.setHashKeySerializer(stringRedisSerializer);
+
+ // value
+ template.setValueSerializer(jsonSerializer);
+ template.setHashValueSerializer(jsonSerializer);
+
+ return template;
+ }
+}
diff --git a/src/main/java/com/ping/study/config/WebMvc.java b/src/main/java/com/ping/study/config/WebMvc.java
index a07de3f..1f9d1b9 100644
--- a/src/main/java/com/ping/study/config/WebMvc.java
+++ b/src/main/java/com/ping/study/config/WebMvc.java
@@ -2,18 +2,37 @@ package com.ping.study.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.reactive.function.client.WebClient;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvc implements WebMvcConfigurer {
+ @Override
+ public void addCorsMappings(CorsRegistry registry) {
+ registry.addMapping("/**")
+ .allowedOrigins("http://jrs77.xyz","https://jrs77.xyz",
+ "http://localhost:5173", "https://nba.1024x.icu",
+ "http://nba.1024x.icu","http://167.253.156.235:9006",
+ "http://116.62.173.2:9001")
+ .allowedMethods("GET","POST","PUT","DELETE","OPTIONS")
+ .allowedHeaders("X-Timestamp","X-Sign","*")
+ .allowCredentials(true) // 若不用 Cookie 也可为 false
+ .maxAge(3600);
+ }
// 原有的 NBA WebClient
@Bean("nbaWebClient")
public WebClient nbaWebClient() {
return WebClient.create("https://api.nba.cn/sib/v2");
}
+ @Bean("matchWebClient")
+ public WebClient txWebClient() {
+ return WebClient.create("https://matchweb.sports.qq.com");
+ }
+
// 新增的腾讯视频 WebClient
@Bean("tencentWebClient")
diff --git a/src/main/java/com/ping/study/controller/NbaController.java b/src/main/java/com/ping/study/controller/NbaController.java
index be7941f..34083aa 100644
--- a/src/main/java/com/ping/study/controller/NbaController.java
+++ b/src/main/java/com/ping/study/controller/NbaController.java
@@ -5,26 +5,37 @@ package com.ping.study.controller;
import com.ping.study.model.dto.addUrls;
import com.ping.study.model.vo.live.LiveUrl;
import com.ping.study.pojo.Games;
+import com.ping.study.pojo.NbaUser;
import com.ping.study.service.GamesService;
import com.ping.study.service.UrlsService;
import com.ping.study.utils.NbaApi;
+import com.ping.study.utils.UserUtil;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpSession;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.util.DigestUtils;
import org.springframework.web.bind.annotation.*;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
+import java.util.Objects;
@RequestMapping("/api")
@RestController
@Data
@Slf4j
-//@CrossOrigin(origins = "http://nba.new9.me")
-@CrossOrigin
+//@CrossOrigin(origins = {"http://nba.1024x.icu/","https://nba.1024x.icu/","http://localhost:5173/"})
public class NbaController {
@Autowired
@@ -33,20 +44,59 @@ public class NbaController {
private GamesService gamesService;
@Autowired
private UrlsService urlsService;
+ @Autowired
+ private UserUtil userUtil;
+ @Autowired
+ private RedisTemplate stringRedisTemplate;
//添加定时任务,每日凌晨0点执行一次
// 每天 00:00:00 执行
@Scheduled(cron = "0 0 0 * * ?")
@RequestMapping("/add")
- public List addGames() {
+ public List addGames() throws InterruptedException {
//先删除数据库中所有赛程
log.info("执行定时方法删除数据库中所有赛程");
gamesService.deleteAllGames();
+ Thread.sleep(100);
//再删除数据库中所有直播链接
log.info("执行定时方法删除数据库中所有直播链接");
urlsService.deleteAllUrls();
+ Thread.sleep(100);
log.info("执行定时方法添加当天赛程");
- return nbaApi.addGames();
+ List gamesList = new ArrayList<>();
+ try {
+ gamesList = nbaApi.addGames();
+ if (gamesList.isEmpty()){
+ stringRedisTemplate.opsForValue().set("games","0");
+ }
+ stringRedisTemplate.delete("live:urls:all");
+ log.info("添加赛程成功:{}",gamesList);
+ }catch (Exception e){
+ stringRedisTemplate.opsForValue().set("games","0");
+ log.info("添加赛程失败:{}",e.getMessage());
+ }
+ return gamesList;
+ }
+
+ @Scheduled(cron = "0 0/10 0-1 * * ?")
+ public void updateGames(){
+ String games_status = stringRedisTemplate.opsForValue().get("games");
+ List gamesList = new ArrayList<>();
+ if (Objects.equals(games_status, "0")){
+ try {
+ gamesList = nbaApi.addGames();
+ if (gamesList.isEmpty()){
+ stringRedisTemplate.opsForValue().set("games","0");
+ return;
+ }
+ stringRedisTemplate.opsForValue().set("games","1");
+ stringRedisTemplate.delete("live:urls:all");
+ }catch (Exception e){
+ stringRedisTemplate.opsForValue().set("games","0");
+ log.info("添加赛程失败:{}",e.getMessage());
+ }
+ }
+ log.info("=======跳过更新数据库赛程========");
}
@RequestMapping("/games")
public List getGames() {
@@ -54,12 +104,85 @@ public class NbaController {
return gamesService.getGames();
}
- @RequestMapping("/urls")
- public List>> getUrls() {
- log.info("获取所有赛程直播链接");
- return urlsService.getUrls();
+// @RequestMapping("/urls")
+// public List>> getUrls() {
+// log.info("获取所有赛程直播链接");
+// return urlsService.getUrls();
+// }
+
+ //根据比赛id和直播流类型获取流
+ @GetMapping("/live/url")
+ public ResponseEntity> getPlayUrl(
+ @RequestParam("gameId") String gameId,
+ @RequestParam("type") String playType,
+ @RequestHeader(value = "X-Timestamp", required = false) String timestamp,
+ @RequestHeader(value = "X-Sign", required = false) String sign,
+ HttpSession session,
+ HttpServletRequest request
+ ) throws Exception {
+
+ String secretKey = "20251125";
+ if (timestamp == null || sign == null) {
+ return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Missing headers");
+ }
+ // 超时校验(5分钟内有效)
+ long ts = Long.parseLong(timestamp);
+ if (Math.abs(System.currentTimeMillis() - ts) > 5 * 60 * 1000) {
+// log.info("Request expired: " + timestamp);
+ return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Request expired");
+ }
+// log.info("Request timestamp: "+timestamp +", sign: " + sign);
+
+ String expected = DigestUtils.md5DigestAsHex((timestamp + secretKey).getBytes(StandardCharsets.UTF_8));
+
+ if (!expected.equalsIgnoreCase(sign)) {
+// log.info("Invalid sign: " + sign + ", expected: " + expected);
+ return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Invalid sign");
+ }
+ NbaUser loginUser = (NbaUser) session.getAttribute("loginUser");
+ if (loginUser == null){
+ return ResponseEntity.status(HttpStatus.FORBIDDEN).body("请先登录");
+ }
+ log.info("{}:获取直播链接开始",loginUser.getEmail());
+ String ip = userUtil.getClientIp(request);
+ String url = urlsService.findUrlByGameIdAndType(gameId, playType);
+// log.info("来自ip:{} 用户:{}:获取直播链接成功:{}",ip,loginUser.getEmail(),url);
+ return ResponseEntity.ok(url);
}
+@GetMapping(value = "/urls")
+public ResponseEntity> getUrls(
+ @RequestHeader(value = "X-Timestamp", required = false) String timestamp,
+ @RequestHeader(value = "X-Sign", required = false) String sign,
+ @RequestParam(value = "includeM3u8", required = false, defaultValue = "0") String includeM3u8
+) {
+ String secretKey = "20251125";
+ if (timestamp == null || sign == null) {
+ return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Missing headers");
+ }
+
+ // 超时校验(5分钟内有效)
+ long ts = Long.parseLong(timestamp);
+ if (Math.abs(System.currentTimeMillis() - ts) > 5 * 60 * 1000) {
+// log.info("Request expired: " + timestamp);
+ return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Request expired");
+ }
+ //log.info("Request timestamp: "+timestamp +", sign: " + sign);
+
+ String expected = DigestUtils.md5DigestAsHex((timestamp + secretKey).getBytes(StandardCharsets.UTF_8));
+
+ if (!expected.equalsIgnoreCase(sign)) {
+// log.info("Invalid sign: " + sign + ", expected: " + expected);
+ return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Invalid sign");
+ }
+
+ log.info("================"+String.valueOf(includeM3u8));
+ List>> data = urlsService.getUrls(includeM3u8);
+
+ return ResponseEntity.ok(data);
+}
+
+
@RequestMapping("/go")
public Boolean go(@RequestParam("pwd") String pwd) {
return pwd.equals("inspur123");
@@ -75,4 +198,13 @@ public class NbaController {
public void deleteUrlById(@PathVariable("id") Integer id){
urlsService.deleteUrlById(id);
}
+ //修改直播链接根据id
+ @RequestMapping("/update")
+ //使用hashmap接收参数
+ public void updateUrlById(@RequestBody HashMap liveUrl){
+ urlsService.updateUrlById(liveUrl);
+ }
+// public void updateUrlById(@RequestBody LiveUrl liveUrl){
+// urlsService.updateUrlById(liveUrl);
+// }
}
diff --git a/src/main/java/com/ping/study/controller/tx/LiveInfoController.java b/src/main/java/com/ping/study/controller/tx/LiveInfoController.java
index bcd2108..d6a664e 100644
--- a/src/main/java/com/ping/study/controller/tx/LiveInfoController.java
+++ b/src/main/java/com/ping/study/controller/tx/LiveInfoController.java
@@ -1,12 +1,11 @@
package com.ping.study.controller.tx;
+import com.ping.study.service.UrlsService;
import com.ping.study.service.tx.LiveInfoService;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
-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 org.springframework.web.bind.annotation.*;
@RestController
@@ -22,13 +21,13 @@ public class LiveInfoController {
@GetMapping("/live/{cnlid}")
public String getLiveInfo(@PathVariable String cnlid) throws Exception {
- log.info("执行查询直播cnlid: {}", cnlid);
+ log.info("执行查询直播id: {}", cnlid);
return liveInfoService.getLiveInfo(cnlid);
}
//定时执行更新直播链接
- //定时任务 从北京时间凌晨到12点,每过半个小时执行一次
- @Scheduled(cron = "0 0/15 0-12 * * ?")
+ //定时任务 从北京时间凌晨到12:00点,每过5分钟执行一次
+ @Scheduled(cron = "0 0/10 0-12 * * ?")
@RequestMapping("/live/refresh")
public String refreshLiveInfo() throws Exception {
log.info("=========开始执行更新直播链接=========");
diff --git a/src/main/java/com/ping/study/controller/tx/MatchController.java b/src/main/java/com/ping/study/controller/tx/MatchController.java
index a94d062..af043a5 100644
--- a/src/main/java/com/ping/study/controller/tx/MatchController.java
+++ b/src/main/java/com/ping/study/controller/tx/MatchController.java
@@ -1,8 +1,10 @@
package com.ping.study.controller.tx;
import com.ping.study.model.dto.tx.MatchListRequest;
+import com.ping.study.model.vo.live.LiveIds;
import com.ping.study.service.tx.MatchService;
import com.ping.study.service.tx.SportsQqService;
+import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@@ -23,6 +25,7 @@ public class MatchController {
this.matchService = matchService;
}
+
@GetMapping("/matches")
public Mono getMatches(
@RequestParam(required = false, defaultValue = "100000") Integer columnId,
@@ -40,7 +43,7 @@ public class MatchController {
}
@GetMapping("/lives")
- public List getPlayoffLiveIds(
+ public List getPlayoffLiveIds(
@RequestParam String startTime,
@RequestParam String endTime) {
diff --git a/src/main/java/com/ping/study/controller/tx/TokenRefreshController.java b/src/main/java/com/ping/study/controller/tx/TokenRefreshController.java
index 6c2494d..1a737f8 100644
--- a/src/main/java/com/ping/study/controller/tx/TokenRefreshController.java
+++ b/src/main/java/com/ping/study/controller/tx/TokenRefreshController.java
@@ -14,8 +14,6 @@ public class TokenRefreshController {
@Autowired
private TxSportTokenRefreshService txSportTokenRefreshService;
-
-
@RequestMapping("/cookie/refresh")
public Boolean refreshToken()
{
diff --git a/src/main/java/com/ping/study/controller/user/UserController.java b/src/main/java/com/ping/study/controller/user/UserController.java
new file mode 100644
index 0000000..6c3b940
--- /dev/null
+++ b/src/main/java/com/ping/study/controller/user/UserController.java
@@ -0,0 +1,168 @@
+package com.ping.study.controller.user;
+
+
+import com.ping.study.model.dto.DtoUpdateUser;
+import com.ping.study.model.dto.registerUserDto;
+
+import com.ping.study.pojo.NbaUser;
+import com.ping.study.service.NbaUserService;
+import com.ping.study.utils.MailUtil;
+import com.ping.study.utils.UserUtil;
+import jakarta.servlet.http.Cookie;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.servlet.http.HttpSession;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.ResponseCookie;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.UUID;
+
+@RestController
+@CrossOrigin(
+ origins = {"http://nba.1024x.icu", "https://nba.1024x.icu","http://jrs77.xyz","https://jrs77.xyz"},
+ allowCredentials = "true"
+)
+@RequestMapping("/user")
+@Slf4j
+public class UserController {
+
+ @Autowired
+ private MailUtil mailUtil;
+
+ @Autowired
+ private NbaUserService nbaUserService;
+
+ @Autowired
+ private UserUtil userUtil;
+
+ //发送验证码
+ @RequestMapping("/send")
+ public String send(String email,Integer type) {
+ switch (type){
+ case 1://注册用户
+ if (nbaUserService.findByEmail(email) != null){
+ return "邮箱已存在";
+ }
+ break;
+ case 2://修改密码
+ if (nbaUserService.findByEmail(email) == null){
+ return "邮箱不存在";
+ }
+ break;
+ }
+
+ return mailUtil.sendVerificationCode(email);
+// return "发送成功";
+ }
+
+ //注册
+ @PostMapping("/register")
+ public String register(@RequestBody registerUserDto registerUserDto, HttpServletRequest request){
+ String ip = userUtil.getClientIp(request);
+ registerUserDto.setRegisterIp(ip);
+ return nbaUserService.register(registerUserDto);
+ }
+
+ // 登录 + rememberToken 功能
+ @PostMapping("/login")
+ public String login(
+ @RequestParam String email,
+ @RequestParam String password,
+ HttpSession session,
+ HttpServletRequest request,
+ HttpServletResponse response
+ ) {
+ String ip = userUtil.getClientIp(request);
+ NbaUser user = nbaUserService.login(email, password,ip);
+ if (user != null){
+ // session 存 7 天
+ session.setMaxInactiveInterval(1 * 24 * 60 * 60);
+ session.setAttribute("loginUser", user);
+
+ // 生成 rememberToken
+ String token = UUID.randomUUID().toString();
+ nbaUserService.saveRememberToken(Long.valueOf(user.getId()), token);
+
+
+ ResponseCookie rememberCookie = ResponseCookie
+ .from("rememberToken", token)
+ .httpOnly(true)
+ .secure(true) // 生产 https 环境一定要 true
+ .path("/")
+ .sameSite("None")
+ .maxAge(1 * 24 * 60 * 60)
+ .build();
+ response.addHeader(HttpHeaders.SET_COOKIE, rememberCookie.toString());
+// Cookie cookie = new Cookie("rememberToken", token);
+// cookie.setPath("/");
+// cookie.setMaxAge(7 * 24 * 60 * 60); // 7 天
+// cookie.setHttpOnly(true); // JS 不能读
+// // 如果你用 https,请启用:cookie.setSecure(true);
+// response.addCookie(cookie);
+
+ log.info("用户 {} 登录成功(记住我 token={})", user.getEmail(), token);
+
+ return "登录成功";
+ }
+ return "登录失败";
+ }
+
+ @PostMapping("/update")
+ public String updateUser(@RequestBody DtoUpdateUser dtoUpdateUser){
+ return nbaUserService.updateUser(dtoUpdateUser);
+ }
+
+ // 返回当前登录用户(支持自动登录)
+ @GetMapping("/me")
+ public Object me(
+ HttpSession session,
+ @CookieValue(value = "rememberToken", required = false) String rememberToken
+ ) {
+ // 1. session 已登录
+ NbaUser loginUser = (NbaUser) session.getAttribute("loginUser");
+ if (loginUser != null) {
+ return loginUser.getEmail();
+ }
+
+ // 2. 尝试自动登录(记住我)
+ if (rememberToken != null && !rememberToken.isEmpty()) {
+ NbaUser user = nbaUserService.findByRememberToken(rememberToken);
+ if (user != null) {
+ // 自动恢复 session
+ session.setMaxInactiveInterval(1 * 24 * 60 * 60);
+ session.setAttribute("loginUser", user);
+
+ log.info("自动登录成功 -> {}", user.getEmail());
+ return user.getEmail();
+ }
+ }
+ return "未登录";
+ }
+
+ // 退出登录(清 session + 清 token + 清 cookie)
+ @PostMapping("/logout")
+ public String logout(
+ HttpSession session,
+ HttpServletResponse response,
+ @CookieValue(value = "rememberToken", required = false) String rememberToken
+ ) {
+ session.invalidate();
+
+ // 清数据库 token
+ if (rememberToken != null) {
+ nbaUserService.deleteRememberToken(rememberToken);
+ }
+
+ // 清 cookie
+ Cookie cookie = new Cookie("rememberToken", "");
+ cookie.setPath("/");
+ cookie.setMaxAge(0);
+ response.addCookie(cookie);
+
+ return "退出成功";
+ }
+}
+
diff --git a/src/main/java/com/ping/study/mapper/LoginLogMapper.java b/src/main/java/com/ping/study/mapper/LoginLogMapper.java
new file mode 100644
index 0000000..6604bff
--- /dev/null
+++ b/src/main/java/com/ping/study/mapper/LoginLogMapper.java
@@ -0,0 +1,25 @@
+package com.ping.study.mapper;
+
+import com.ping.study.pojo.LoginLog;
+
+/**
+* @author Administrator
+* @description 针对表【login_log】的数据库操作Mapper
+* @createDate 2025-12-22 13:10:39
+* @Entity com.ping.study.pojo.LoginLog
+*/
+public interface LoginLogMapper {
+
+ int deleteByPrimaryKey(Long id);
+
+ int insert(LoginLog record);
+
+ int insertSelective(LoginLog record);
+
+ LoginLog selectByPrimaryKey(Long id);
+
+ int updateByPrimaryKeySelective(LoginLog record);
+
+ int updateByPrimaryKey(LoginLog record);
+
+}
diff --git a/src/main/java/com/ping/study/mapper/NbaUserMapper.java b/src/main/java/com/ping/study/mapper/NbaUserMapper.java
new file mode 100644
index 0000000..953a8ae
--- /dev/null
+++ b/src/main/java/com/ping/study/mapper/NbaUserMapper.java
@@ -0,0 +1,38 @@
+package com.ping.study.mapper;
+
+import com.ping.study.pojo.NbaUser;
+
+/**
+* @author Administrator
+* @description 针对表【nba_user】的数据库操作Mapper
+* @createDate 2025-11-19 22:54:55
+* @Entity com.ping.study.pojo.NbaUser
+*/
+public interface NbaUserMapper {
+
+ int deleteByPrimaryKey(Long id);
+
+ int insert(NbaUser record);
+
+ int insertSelective(NbaUser record);
+
+ NbaUser selectByPrimaryKey(Long id);
+
+ int updateByPrimaryKeySelective(NbaUser record);
+
+ int updateByPrimaryKey(NbaUser record);
+
+ NbaUser login(String email, String password);
+
+ void updateRememberToken(Long userId, String token);
+
+ NbaUser selectByRememberToken(String token);
+
+ void clearRememberToken(String token);
+
+ NbaUser findByEmail(String email);
+
+ NbaUser selectByEmail(String email);
+
+ void updatePasswordByEmail(String email, String password);
+}
diff --git a/src/main/java/com/ping/study/mapper/UrlsMapper.java b/src/main/java/com/ping/study/mapper/UrlsMapper.java
index 268a869..0f9770b 100644
--- a/src/main/java/com/ping/study/mapper/UrlsMapper.java
+++ b/src/main/java/com/ping/study/mapper/UrlsMapper.java
@@ -29,7 +29,13 @@ public interface UrlsMapper {
List selectGameIds();
- List selectUrlsListByGameId(String gameId);
+ List selectUrlsListByGameId(
+ @Param("gameId") String gameId
+ );
+
+ List selectUrlsListByAdmin(
+ @Param("gameId") String gameId
+ );
void insertUrlsWithGameId(@Param("gameId") String gameId, @Param("list") List urls);
@@ -40,4 +46,11 @@ public interface UrlsMapper {
//更新比赛直播链接
void updateUrlsWithGameId(String gameId, String s,String type);
+
+ String selectUrlsListByGameIdAndType(String gameId, String type);
+
+ //更新比赛状态
+ void updateStatusWithGameId(String gameId, String status);
+
+ void updateUrlsById(String id, String url);
}
diff --git a/src/main/java/com/ping/study/model/dto/DtoUpdateUser.java b/src/main/java/com/ping/study/model/dto/DtoUpdateUser.java
new file mode 100644
index 0000000..1ac936e
--- /dev/null
+++ b/src/main/java/com/ping/study/model/dto/DtoUpdateUser.java
@@ -0,0 +1,11 @@
+package com.ping.study.model.dto;
+
+import lombok.Data;
+
+@Data
+public class DtoUpdateUser {
+
+ private String email;
+ private String password;
+ private String code;
+}
diff --git a/src/main/java/com/ping/study/model/dto/registerUserDto.java b/src/main/java/com/ping/study/model/dto/registerUserDto.java
new file mode 100644
index 0000000..a3b7e67
--- /dev/null
+++ b/src/main/java/com/ping/study/model/dto/registerUserDto.java
@@ -0,0 +1,20 @@
+package com.ping.study.model.dto;
+
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+public class registerUserDto {
+// private Integer id;
+
+ private String username;
+
+ private String password;
+
+ private String email;
+
+ private String registerIp;
+
+ private String code;
+}
diff --git a/src/main/java/com/ping/study/model/dto/tx/TxMatchRequest.java b/src/main/java/com/ping/study/model/dto/tx/TxMatchRequest.java
new file mode 100644
index 0000000..6f09487
--- /dev/null
+++ b/src/main/java/com/ping/study/model/dto/tx/TxMatchRequest.java
@@ -0,0 +1,59 @@
+package com.ping.study.model.dto.tx;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class TxMatchRequest {
+
+ /**
+ * 当前日期 yyyy-MM-dd(默认:当前日期)
+ */
+ @Builder.Default
+ private String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
+
+ /**
+ * 查询开始日期 yyyy-MM-dd(默认:当前日期)
+ */
+ @Builder.Default
+ private String startTime = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
+
+ /**
+ * 查询结束日期 yyyy-MM-dd(默认:当前日期)
+ */
+ @Builder.Default
+ private String endTime = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
+
+ /**
+ * 栏目 ID
+ */
+ private Integer columnId=100000;
+
+ /**
+ * 页码或索引
+ */
+ private Integer index =1;
+
+ /**
+ * 是否初始化
+ */
+ private Boolean isInit= true;
+
+ /**
+ * 时间戳(默认:当前系统时间毫秒)
+ */
+ @Builder.Default
+ private Long timestamp = System.currentTimeMillis();
+
+ /**
+ * 回调函数名
+ */
+ private String callback = "fetchScheduleListCallback100000";
+}
diff --git a/src/main/java/com/ping/study/model/vo/Game.java b/src/main/java/com/ping/study/model/vo/Game.java
index 40efd4b..54acfbe 100644
--- a/src/main/java/com/ping/study/model/vo/Game.java
+++ b/src/main/java/com/ping/study/model/vo/Game.java
@@ -11,4 +11,5 @@ public class Game {
private String awayTeamLogoDark;
private String startDate; // 格式:"2025-04-17"
private String startTime; // 格式:"07:30:00"
+ private String status;//比赛状态
}
\ No newline at end of file
diff --git a/src/main/java/com/ping/study/model/vo/live/LiveIds.java b/src/main/java/com/ping/study/model/vo/live/LiveIds.java
new file mode 100644
index 0000000..127feb1
--- /dev/null
+++ b/src/main/java/com/ping/study/model/vo/live/LiveIds.java
@@ -0,0 +1,11 @@
+package com.ping.study.model.vo.live;
+
+
+import lombok.Data;
+
+@Data
+public class LiveIds {
+
+ private String mId;
+ private String playId;
+}
diff --git a/src/main/java/com/ping/study/model/vo/live/LiveUrl.java b/src/main/java/com/ping/study/model/vo/live/LiveUrl.java
index 5f45ce9..8ab1210 100644
--- a/src/main/java/com/ping/study/model/vo/live/LiveUrl.java
+++ b/src/main/java/com/ping/study/model/vo/live/LiveUrl.java
@@ -12,10 +12,9 @@ import lombok.NoArgsConstructor;
@NoArgsConstructor
public class LiveUrl {
-// private Integer gameId;
+ // private Integer gameId;
private Integer id;
private String type;
private String url;
-
}
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
index 1f516fa..403128c 100644
--- a/src/main/java/com/ping/study/model/vo/tx/MatchInfo.java
+++ b/src/main/java/com/ping/study/model/vo/tx/MatchInfo.java
@@ -5,7 +5,9 @@ import lombok.Data;
@Data
public class MatchInfo {
+ private String mId;
private String liveId;
private String matchType;
+ private String competitionId;
// 其他需要的字段...
}
diff --git a/src/main/java/com/ping/study/pojo/Games.java b/src/main/java/com/ping/study/pojo/Games.java
index b865b9d..5120bde 100644
--- a/src/main/java/com/ping/study/pojo/Games.java
+++ b/src/main/java/com/ping/study/pojo/Games.java
@@ -56,4 +56,7 @@ public class Games implements Serializable {
private String awayTeamLogoDark;
private static final long serialVersionUID = 1L;
+
+ //比赛状态
+ private String status;
}
\ No newline at end of file
diff --git a/src/main/java/com/ping/study/pojo/LoginLog.java b/src/main/java/com/ping/study/pojo/LoginLog.java
new file mode 100644
index 0000000..4e34214
--- /dev/null
+++ b/src/main/java/com/ping/study/pojo/LoginLog.java
@@ -0,0 +1,18 @@
+package com.ping.study.pojo;
+
+import java.util.Date;
+import lombok.Data;
+
+/**
+ * @TableName login_log
+ */
+@Data
+public class LoginLog {
+ private Integer id;
+
+ private Integer userId;
+
+ private Date loginTime;
+
+ private String loginIp;
+}
\ No newline at end of file
diff --git a/src/main/java/com/ping/study/pojo/NbaUser.java b/src/main/java/com/ping/study/pojo/NbaUser.java
new file mode 100644
index 0000000..9085f8f
--- /dev/null
+++ b/src/main/java/com/ping/study/pojo/NbaUser.java
@@ -0,0 +1,28 @@
+package com.ping.study.pojo;
+
+import java.util.Date;
+import lombok.Data;
+
+/**
+ * @TableName nba_user
+ */
+@Data
+public class NbaUser {
+ private Integer id;
+
+ private String username;
+
+ private String password;
+
+ private String email;
+
+ private Date createTime;
+
+ private Date updateTime;
+
+ private Date lastLoginTime;
+
+ private String lastLoginIp;
+
+ private String registerIp;
+}
\ No newline at end of file
diff --git a/src/main/java/com/ping/study/pojo/Urls.java b/src/main/java/com/ping/study/pojo/Urls.java
index 39acbd5..beeb114 100644
--- a/src/main/java/com/ping/study/pojo/Urls.java
+++ b/src/main/java/com/ping/study/pojo/Urls.java
@@ -27,4 +27,6 @@ public class Urls {
*
*/
private String type;
+
+ private Integer status;
}
\ No newline at end of file
diff --git a/src/main/java/com/ping/study/service/NbaUserService.java b/src/main/java/com/ping/study/service/NbaUserService.java
new file mode 100644
index 0000000..412cabf
--- /dev/null
+++ b/src/main/java/com/ping/study/service/NbaUserService.java
@@ -0,0 +1,27 @@
+package com.ping.study.service;
+
+import com.ping.study.model.dto.DtoUpdateUser;
+import com.ping.study.model.dto.registerUserDto;
+import com.ping.study.pojo.NbaUser;
+
+/**
+* @author Administrator
+* @description 针对表【nba_user】的数据库操作Service
+* @createDate 2025-11-19 22:52:24
+*/
+public interface NbaUserService{
+
+ String register(registerUserDto registerUserDto);
+
+ NbaUser login(String email, String password,String ip);
+
+ void saveRememberToken(Long userId, String token);
+
+ NbaUser findByRememberToken(String token);
+
+ void deleteRememberToken(String token);
+
+ String updateUser(DtoUpdateUser dtoUpdateUser);
+
+ NbaUser findByEmail(String email);
+}
diff --git a/src/main/java/com/ping/study/service/UrlsService.java b/src/main/java/com/ping/study/service/UrlsService.java
index 41b947e..3d0cb7d 100644
--- a/src/main/java/com/ping/study/service/UrlsService.java
+++ b/src/main/java/com/ping/study/service/UrlsService.java
@@ -11,7 +11,7 @@ public interface UrlsService {
//查询当天的直播地址
- public List>> getUrls();
+ public List>> getUrls(String includeM3u8);
//添加直播地址到对应赛事
public void addUrls(addUrls addUrls);
@@ -19,4 +19,8 @@ public interface UrlsService {
public void deleteUrlById(Integer id);
void deleteAllUrls();
+
+ String findUrlByGameIdAndType(String gameId, String type);
+
+ void updateUrlById(HashMap liveUrl);
}
diff --git a/src/main/java/com/ping/study/service/impl/NbaUserServiceImpl.java b/src/main/java/com/ping/study/service/impl/NbaUserServiceImpl.java
new file mode 100644
index 0000000..0ccaf10
--- /dev/null
+++ b/src/main/java/com/ping/study/service/impl/NbaUserServiceImpl.java
@@ -0,0 +1,112 @@
+package com.ping.study.service.impl;
+
+import com.ping.study.mapper.LoginLogMapper;
+import com.ping.study.mapper.NbaUserMapper;
+import com.ping.study.model.dto.DtoUpdateUser;
+import com.ping.study.model.dto.registerUserDto;
+import com.ping.study.pojo.LoginLog;
+import com.ping.study.pojo.NbaUser;
+import com.ping.study.service.NbaUserService;
+
+import com.ping.study.utils.UserUtil;
+import lombok.extern.slf4j.Slf4j;
+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 org.springframework.transaction.annotation.Transactional;
+
+import java.util.Date;
+
+@Service
+@Slf4j
+public class NbaUserServiceImpl implements NbaUserService {
+
+ @Autowired
+ private RedisTemplate stringRedisTemplate;
+ @Autowired
+ private NbaUserMapper nbaUserMapper;
+ @Autowired
+ private LoginLogMapper loginLogMapper;
+
+ @Override
+ public String register(registerUserDto registerUserDto) {
+ String s = stringRedisTemplate.opsForValue().get(registerUserDto.getEmail());
+ if (s != null && s.equals(registerUserDto.getCode())) {
+ if (nbaUserMapper.findByEmail(registerUserDto.getEmail()) != null){
+ return "邮箱已存在";
+ }
+ NbaUser nbaUser = new NbaUser();
+ nbaUser.setUsername(registerUserDto.getUsername());
+ nbaUser.setPassword(registerUserDto.getPassword());
+ nbaUser.setEmail(registerUserDto.getEmail());
+ nbaUser.setCreateTime(new Date());
+ nbaUser.setUpdateTime(new Date());
+ nbaUser.setLastLoginTime(new Date());
+ nbaUser.setLastLoginIp("");
+ nbaUser.setRegisterIp(registerUserDto.getRegisterIp());
+ stringRedisTemplate.delete(registerUserDto.getEmail());
+ nbaUserMapper.insert(nbaUser);
+ return "注册成功";
+ }
+ else {
+ //throw new RuntimeException("验证码错误");
+ return "验证码错误";
+ }
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public NbaUser login(String email, String password, String ip) {
+ NbaUser loginUser = nbaUserMapper.login(email, password);
+ if (loginUser!=null){
+ LoginLog loginLog = new LoginLog();
+ loginLog.setUserId(loginUser.getId());
+ loginLog.setLoginTime(new Date());
+ loginLog.setLoginIp(ip);
+ loginLogMapper.insert(loginLog);
+ loginUser.setLastLoginTime(new Date());
+ nbaUserMapper.updateByPrimaryKeySelective(loginUser);
+ return loginUser;
+ }
+ else return null;
+ //return loginUser;
+ }
+
+ @Override
+ public void saveRememberToken(Long userId, String token) {
+ // update nba_user set remember_token = ? where id = ?
+ nbaUserMapper.updateRememberToken(userId, token);
+ }
+
+ @Override
+ public NbaUser findByRememberToken(String token) {
+ return nbaUserMapper.selectByRememberToken(token);
+ }
+
+ @Override
+ public void deleteRememberToken(String token) {
+ nbaUserMapper.clearRememberToken(token);
+ }
+
+ @Override
+ public String updateUser(DtoUpdateUser dtoUpdateUser) {
+ NbaUser nbaUser = nbaUserMapper.selectByEmail(dtoUpdateUser.getEmail());
+ if (nbaUser != null){
+ String s = stringRedisTemplate.opsForValue().get(dtoUpdateUser.getEmail());
+ if (s != null && s.equals(dtoUpdateUser.getCode())){
+ nbaUserMapper.updatePasswordByEmail(dtoUpdateUser.getEmail(), dtoUpdateUser.getPassword());
+ stringRedisTemplate.delete(dtoUpdateUser.getEmail());
+ return "更新成功";
+ }
+ return "验证码错误";
+ }
+ return "当前邮箱无账号";
+ }
+
+ @Override
+ public NbaUser findByEmail(String email) {
+ return nbaUserMapper.findByEmail(email);
+ }
+
+}
diff --git a/src/main/java/com/ping/study/service/impl/UrlsServiceImpl.java b/src/main/java/com/ping/study/service/impl/UrlsServiceImpl.java
index 9aaef8b..d92647f 100644
--- a/src/main/java/com/ping/study/service/impl/UrlsServiceImpl.java
+++ b/src/main/java/com/ping/study/service/impl/UrlsServiceImpl.java
@@ -5,39 +5,85 @@ import com.ping.study.model.dto.addUrls;
import com.ping.study.model.vo.live.LiveUrl;
import com.ping.study.service.UrlsService;
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.ArrayList;
import java.util.HashMap;
import java.util.List;
+import java.util.concurrent.TimeUnit;
@Service
public class UrlsServiceImpl implements UrlsService {
@Autowired
private UrlsMapper urlsMapper;
+
+ @Autowired
+ private RedisTemplate redisTemplate;
@Override
- public List>> getUrls() {
+ public List>> getUrls(String includeM3u8) {
+
+ String redisKey = "live:urls:all";
+
+ // ② 查数据库兜底
List>> urlsList = new ArrayList<>();
List gameIds = urlsMapper.selectGameIds();
+
+ if (includeM3u8.equals("1")){
+ gameIds.forEach(gameId -> {
+ List maps = urlsMapper.selectUrlsListByAdmin(gameId);
+ HashMap> map = new HashMap<>();
+ map.put(gameId,maps);
+ urlsList.add(map);
+ });
+ return urlsList;
+ }
+ // ① 先读缓存,Redis 挂了会抛异常,进入 catch → 去 DB
+ try {
+ List>> cache =
+ (List>>) redisTemplate.opsForValue().get(redisKey);
+
+ if(cache != null){
+ return cache;
+ }
+ } catch (Exception ignored) {
+ // Redis 错了不要打印到控制台,生产只记录日志
+ }
+
gameIds.forEach(gameId -> {
List maps = urlsMapper.selectUrlsListByGameId(gameId);
HashMap> map = new HashMap<>();
map.put(gameId,maps);
urlsList.add(map);
- }); // 添加右括号和分号
+ });
+
+ // ③ DB 查询结果写回 Redis(可选)
+ try {
+// redisTemplate.opsForValue().set(redisKey, urlsList, 1, TimeUnit.HOURS);
+ redisTemplate.opsForValue().set(redisKey, urlsList);
+ } catch (Exception ignored) {
+ // redis 宕机不影响业务
+ }
+
return urlsList;
}
+
@Override
public void addUrls(addUrls addUrls) {
urlsMapper.insertUrlsWithGameId(addUrls.getGameId(), addUrls.getUrls());
+ //删除redis缓存
+ redisTemplate.delete("live:urls:all");
}
@Override
public void deleteUrlById(Integer id) {
try {
urlsMapper.deleteUrlById(id);
+ //删除redis缓存
+ redisTemplate.delete("live:urls:all");
}
catch (Exception e) {
e.printStackTrace();
@@ -48,6 +94,40 @@ public class UrlsServiceImpl implements UrlsService {
@Override
public void deleteAllUrls() {
urlsMapper.deleteAllUrls();
+ //删除redis缓存
+ redisTemplate.delete("live:urls:all");
}
+ @Override
+ public String findUrlByGameIdAndType(String gameId, String type) {
+ String redis_url = "url:" + gameId+":"+type;
+ String url;
+ try {
+ url = (String) redisTemplate.opsForValue().get(redis_url);
+ if (url != null){
+ return url;
+ }
+ }catch (Exception ignored){
+
+ }
+ url = urlsMapper.selectUrlsListByGameIdAndType(gameId, type);
+ try {
+ if (!url.equals("null")){
+ redisTemplate.opsForValue().set(redis_url, url,1,TimeUnit.HOURS);
+ }
+ }catch (Exception ignored){
+ }
+ return url;
+ }
+
+ //修改直播链接
+ @Override
+ public void updateUrlById(HashMap liveUrl) {
+ //修改当前id直播链接
+ urlsMapper.updateUrlsById(liveUrl.get("id"),liveUrl.get("url"));
+ //删除redis缓存 url:0042500224:tx
+ redisTemplate.delete("url:"+liveUrl.get("gameId")+":"+liveUrl.get("type"));
+ }
+
+
}
diff --git a/src/main/java/com/ping/study/service/tx/LiveInfoService.java b/src/main/java/com/ping/study/service/tx/LiveInfoService.java
index f1bf7e3..e218289 100644
--- a/src/main/java/com/ping/study/service/tx/LiveInfoService.java
+++ b/src/main/java/com/ping/study/service/tx/LiveInfoService.java
@@ -10,10 +10,12 @@ import com.ping.study.model.vo.live.LiveInfoResponse;
import com.ping.study.pojo.Games;
import com.ping.study.utils.tx.CKeyGenerator;
+import com.ping.study.utils.tx.TxVideoAuthRedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
@@ -40,12 +42,15 @@ public class LiveInfoService {
@Autowired
private TxSportTokenRefreshService txSportTokenRefreshService;;
- private StringRedisTemplate stringRedisTemplate;
+ @Autowired
+ private RedisTemplate stringRedisTemplate;
- public LiveInfoService(@Qualifier("tencentWebClient")WebClient webClient, CKeyGenerator cKeyGenerator, StringRedisTemplate stringRedisTemplate) {
+ @Autowired
+ private TxVideoAuthRedisUtil txVideoAuthRedisUtil;
+
+ public LiveInfoService(@Qualifier("tencentWebClient")WebClient webClient, CKeyGenerator cKeyGenerator) {
this.webClient = webClient;
this.cKeyGenerator = cKeyGenerator;
- this.stringRedisTemplate = stringRedisTemplate;
}
// 2. 远程调用时拼接 Cookie
@@ -60,45 +65,82 @@ public class LiveInfoService {
public String getLiveInfo(String cnlid) throws Exception {
//先调用刷新cookie
txSportTokenRefreshService.refreshCookies();
- Thread.sleep(100);
+ Thread.sleep(3000);
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);
+// 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);
-
+// String s = txVideoAuthRedisUtil.buildAuthAndLoginToken("tx_sports_cookie_map").get("auth_ext_raw");
+// String s1 = txVideoAuthRedisUtil.buildAuthAndLoginToken("tx_sports_cookie_map").get("logintoken_raw");
+// log.info("请求头 auth_ext_encoded: {}", s);
+// log.info("请求头 logintoken_encoded: {}", s1);
// 3. 发起请求
+ //defnswitch、auth_ext、logintoken为高帧率fhd_hfps请求参数
String res = webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/cgi-bin/getliveinfo")
.queryParam("ckey", ckey)
- .queryParam("encrypt_ver", LiveInfoRequest.generateEncryptVer(
- DayOfWeek.from(LocalDate.now())))
+ .queryParam("auth_ext", txVideoAuthRedisUtil.buildAuthAndLoginToken("tx_sports_cookie_map").get("auth_ext_encoded"))
+ .queryParam("logintoken", txVideoAuthRedisUtil.buildAuthAndLoginToken("tx_sports_cookie_map").get("logintoken_encoded"))
+ .queryParam("encrypt_ver", LiveInfoRequest.generateEncryptVer(DayOfWeek.from(LocalDate.now())))
.queryParam("platform", platform)
+ .queryParam("auth_from", 4001)
+ .queryParam("sphttps", 1)
.queryParam("tm", Instant.now().getEpochSecond())
.queryParam("cnlid", cnlid)
- .queryParam("defn","fhd")
- .queryParam("ufps","auto")
+ .queryParam("defn", "fhd")
+ .queryParam("ufps", "auto")
.build())
+ // --- 在这里添加伪造 IP 的 Header ---
+ .header("X-Forwarded-For", "59.54.11.234")
+ .header("X-Real-IP", "59.54.11.234")
+ .header("Client-IP", "59.54.11.234")
+ // -------------------------------------
.header("Cookie", cookie)
+ .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36") // 确保 UA 也像浏览器
+ .header("Referer", "https://sports.qq.com/") // 强烈建议加上 Referer,通常防盗链会查这个
.retrieve()
.bodyToMono(String.class)
.block();
+// String res = webClient.get()
+// .uri(uriBuilder -> uriBuilder
+// .path("/cgi-bin/getliveinfo")
+// .queryParam("ckey", ckey)
+//// .queryParam("defnswitch",1)
+// .queryParam("auth_ext", txVideoAuthRedisUtil.buildAuthAndLoginToken("tx_sports_cookie_map").get("auth_ext_encoded"))
+// .queryParam("logintoken",txVideoAuthRedisUtil.buildAuthAndLoginToken("tx_sports_cookie_map").get("logintoken_encoded"))
+// .queryParam("encrypt_ver", LiveInfoRequest.generateEncryptVer(
+// DayOfWeek.from(LocalDate.now())))
+// .queryParam("platform", platform)
+//// .queryParam("hevclv",26)
+//// .queryParam("host","sports.qq.com")
+// .queryParam("auth_from",4001)
+// .queryParam("sphttps",1)
+// .queryParam("tm", Instant.now().getEpochSecond())
+// .queryParam("cnlid", cnlid)
+// .queryParam("defn","fhd")
+// .queryParam("ufps","auto")
+// .build())
+// .header("Cookie", cookie)
+// .retrieve()
+// .bodyToMono(String.class)
+// .block();
List list = extractPlayUrls(res);
- log.info("提取到的播放URL: {}", list);
+// log.info("提取到的播放URL: {}", list);
return res;
}
@@ -128,7 +170,7 @@ public class LiveInfoService {
}
}
}
- log.info("提取到的播放URL数量: {}", urls.size());
+// log.info("提取到的播放URL数量: {}", urls.size());
return urls;
}
@@ -137,12 +179,26 @@ public class LiveInfoService {
for (Games games : gamesList) {
String playId = games.getPlayId();
String liveInfo = getLiveInfo(playId);
- log.info("获取到的直播信息: {}", liveInfo);
+// log.info("当前直播id: {}", playId);
+// log.info("获取到的直播信息: {}", liveInfo);
try {
+// log.info("进入refreshLiveInfo解析liveInfo");
List urls = extractPlayUrls(liveInfo);
- log.info("提取到的播放URL: {}", urls);
- urlsMapper.updateUrlsWithGameId(games.getGameId(), urls.get(urls.size()-1),"tx");
- urlsMapper.updateUrlsWithGameId(games.getGameId(), urls.get(0),"zb");
+// log.info("提取到的播放URL: {}", urls);
+ if (urls.size()<2){
+ log.info("当前直播id: {}, 获取直播源失败", playId);
+ log.info("跳过当前直播,等待下一次获取");
+ continue;
+// Thread.sleep(500);
+// liveInfo = getLiveInfo(playId);
+// urls = extractPlayUrls(liveInfo);
+ }
+ urlsMapper.updateUrlsWithGameId(games.getGameId(), urls.get(0),"tx");
+ log.info("当前直播id: {}, 获取直播源成功", playId);
+ Thread.sleep(100);
+// log.info("提取的直播链接:{}",urls.get(urls.size()-1));
+// urlsMapper.updateUrlsWithGameId(games.getGameId(), urls.get(1),"tx2");
+// urlsMapper.updateUrlsWithGameId(games.getGameId(), urls.get(0),"zb");
// 这里可以保存urls到数据库或其他处理
} catch (Exception e) {
log.error("解析直播信息失败");
diff --git a/src/main/java/com/ping/study/service/tx/MatchService.java b/src/main/java/com/ping/study/service/tx/MatchService.java
index 22fbdda..4ebd9b8 100644
--- a/src/main/java/com/ping/study/service/tx/MatchService.java
+++ b/src/main/java/com/ping/study/service/tx/MatchService.java
@@ -1,5 +1,6 @@
package com.ping.study.service.tx;
+import com.ping.study.model.vo.live.LiveIds;
import com.ping.study.model.vo.tx.MatchInfo;
import com.ping.study.model.vo.tx.MatchResponse;
import org.springframework.stereotype.Service;
@@ -28,7 +29,7 @@ public class MatchService {
/**
* 获取指定日期范围内所有NBA季后赛(matchType=2)的直播ID
*/
- public List getPlayoffLiveIdsBlocking(LocalDate startDate, LocalDate endDate) {
+ public List getPlayoffLiveIdsBlocking(LocalDate startDate, LocalDate endDate) {
return webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/matchUnion/list")
@@ -42,18 +43,53 @@ public class MatchService {
if (response.getCode() != 0 || response.getData() == null) {
return Flux.error(new RuntimeException("API返回异常: " + response.getMsg()));
}
- // 提取所有matchType=2的比赛
- List liveIds = response.getData().values().stream()
+
+ // 提取所有 matchType=2 & competitionId=100000 的比赛
+ List liveIds = response.getData().values().stream()
.flatMap(List::stream)
.filter(match -> "2".equals(match.getMatchType()))
- .map(MatchInfo::getLiveId)
+ .filter(match -> "100000".equals(match.getCompetitionId()))
+ .map(match -> {
+ LiveIds ids = new LiveIds();
+ ids.setMId(match.getMId());
+ ids.setPlayId(match.getLiveId());
+ return ids;
+ })
.collect(Collectors.toList());
+
return Flux.fromIterable(liveIds);
})
.collectList()
.block(); // 阻塞等待结果
}
+// 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()))
+// .filter(match -> "100000".equals(match.getCompetitionId()))
+// .map(MatchInfo::getLiveId)
+// .collect(Collectors.toList());
+// return Flux.fromIterable(liveIds);
+// })
+// .collectList()
+// .block(); // 阻塞等待结果
+// }
+
/**
* 获取完整比赛信息(过滤matchType=2)
*/
diff --git a/src/main/java/com/ping/study/service/tx/SportsQqService.java b/src/main/java/com/ping/study/service/tx/SportsQqService.java
index bd08dc8..357b12e 100644
--- a/src/main/java/com/ping/study/service/tx/SportsQqService.java
+++ b/src/main/java/com/ping/study/service/tx/SportsQqService.java
@@ -1,18 +1,35 @@
package com.ping.study.service.tx;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.ping.study.mapper.UrlsMapper;
import com.ping.study.model.dto.tx.MatchListRequest;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
+
+import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
+@Slf4j
@Service
public class SportsQqService {
+
private final WebClient webClient;
private static final DateTimeFormatter DATE_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd");
+ @Autowired
+ private UrlsMapper urlsMapper;
+
+ @Autowired
+ private ObjectMapper objectMapper; // Spring Boot 自动注入 Jackson
+
public SportsQqService(WebClient.Builder webClientBuilder) {
this.webClient = webClientBuilder
.baseUrl("https://matchweb.sports.qq.com")
@@ -20,8 +37,34 @@ public class SportsQqService {
.build();
}
+ // --- 修改点 1: 新增一个无参方法专门用于定时任务 ---
+ // 定时任务入口:不需要参数,也不需要返回值(或者返回Mono)
+ @Scheduled(cron = "0 0/5 3-16 * * ?")
+ public void scheduledSyncTask() {
+ log.info("开始执行定时同步直播源任务...");
+
+ // 1. 在这里手动构造请求参数
+ // 假设你需要查询当天的比赛
+ MatchListRequest request = new MatchListRequest();
+ //request.setColumnId("100000"); // 示例值,根据你的实际情况设置
+ request.setStartTime(LocalDate.now()); // 设置开始时间
+ request.setEndTime(LocalDate.now()); // 设置结束时间
+
+ // 2. 调用业务逻辑,并且必须订阅 (.subscribe) 才能触发执行
+ getMatchList(request)
+ .subscribe(
+ result -> log.info("定时任务执行完成"),
+ error -> log.error("定时任务执行出错", error)
+ );
+ }
+
+ // --- 修改点 2: 原来的方法去掉 @Scheduled,作为一个普通的服务方法 ---
public Mono getMatchList(MatchListRequest request) {
- request.validate(); // 参数校验
+ // 参数校验 (建议保留)
+ if (request == null) {
+ return Mono.error(new IllegalArgumentException("Request cannot be null"));
+ }
+ // request.validate(); // 如果你有 validate 方法
return webClient.get()
.uri(uriBuilder -> uriBuilder
@@ -31,6 +74,128 @@ public class SportsQqService {
.queryParam("endTime", request.getEndTime().format(DATE_FORMATTER))
.build())
.retrieve()
- .bodyToMono(String.class);
+ .bodyToMono(String.class)
+ .doOnNext(jsonString -> {
+ try {
+ JsonNode rootNode = objectMapper.readTree(jsonString);
+ JsonNode dataNode = rootNode.path("data");
+
+ dataNode.fields().forEachRemaining(entry -> {
+ JsonNode matchesArray = entry.getValue();
+ if (matchesArray.isArray()) {
+ for (JsonNode match : matchesArray) {
+ String mid = match.path("mid").asText("");
+ String matchPeriod = match.path("matchPeriod").asText("");
+
+ // 3. 处理 mid 获取后 10 位
+ String targetId = "";
+ if (mid.contains(":")) {
+ String[] parts = mid.split(":");
+ String suffix = parts[1]; // 这里拿到的是冒号后的完整部分,如 10022500079 (11位) 或 20257089 (8位)
+
+ // 如果长度大于10,截取最后10位;否则保持原样
+ if (suffix.length() > 10) {
+ targetId = suffix.substring(suffix.length() - 10);
+ } else {
+ targetId = suffix;
+ }
+ } else {
+ // 防止没有冒号的情况,同样做截取保护
+ if (mid.length() > 10) {
+ targetId = mid.substring(mid.length() - 10);
+ } else {
+ targetId = mid;
+ }
+ }
+
+ if (!targetId.isEmpty()) {
+ urlsMapper.updateStatusWithGameId(targetId, matchPeriod);
+ log.info("更新比赛:{} 直播状态成功,阶段: {}", targetId, matchPeriod);
+ }
+ }
+ }
+ });
+ } catch (Exception e) {
+ log.error("解析或更新数据库失败", e);
+ }
+ });
}
+
+ //更新直播源状态
+ //定时任务 每隔10分钟执行一次,从每天早上3点到下午5点之间执行
+// @Scheduled(cron = "0 0/5 3-16 * * ?")
+// public Mono updateGameStatus() {
+//// request.validate();
+// MatchListRequest request = new MatchListRequest();
+// 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)
+// .doOnNext(jsonString -> {
+// // --- 在这里进行解析和数据库保存操作 ---
+// try {
+// JsonNode rootNode = objectMapper.readTree(jsonString);
+// JsonNode dataNode = rootNode.path("data");
+//
+// // 1. data 下面的 key 是动态日期 (例如 "2025-11-29"),所以我们要遍历字段
+// dataNode.fields().forEachRemaining(entry -> {
+// // String dateKey = entry.getKey(); // 如果需要日期,可以拿这个
+// JsonNode matchesArray = entry.getValue();
+//
+// // 2. 遍历当天的比赛列表
+// if (matchesArray.isArray()) {
+// for (JsonNode match : matchesArray) {
+// // 提取 mid
+// String mid = match.path("mid").asText("");
+// // 提取 matchPeriod
+// String matchPeriod = match.path("matchPeriod").asText("");
+//
+// // 3. 处理 mid 获取后 10 位 (或冒号后的部分)
+// String targetId = "";
+// if (mid.contains(":")) {
+// // 逻辑A:获取冒号后面的所有数字 (推荐,通常这是唯一ID)
+// String[] parts = mid.split(":");
+// targetId = parts[1];
+//
+// // 逻辑B:如果你严格需要“最后10位字符” (如果ID长度超过10,会截取)
+// if (targetId.length() > 10) {
+// targetId = targetId.substring(targetId.length() - 10);
+// }
+// } else {
+// targetId = mid; // 防止没有冒号的情况
+// }
+//
+// // 4. 保存到数据库
+// if (!targetId.isEmpty()) {
+// //saveMatchInfoToDb(mid, targetId, matchPeriod);
+// urlsMapper.updateStatusWithGameId(targetId,matchPeriod);
+// log.info("更新比赛:{}直播状态成功:",targetId);
+// }
+// }
+// }
+// });
+// } catch (Exception e) {
+// e.printStackTrace();
+// // 记录日志:解析失败
+// }
+// });
+// }
+// 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
index e2e917e..db0a5ad 100644
--- a/src/main/java/com/ping/study/service/tx/TxSportTokenRefreshService.java
+++ b/src/main/java/com/ping/study/service/tx/TxSportTokenRefreshService.java
@@ -3,7 +3,10 @@ package com.ping.study.service.tx;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.ping.study.utils.MailUtil;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
@@ -18,12 +21,14 @@ import java.util.stream.Collectors;
public class TxSportTokenRefreshService {
private final WebClient webClient;
- private final StringRedisTemplate redisTemplate;
+ private final RedisTemplate redisTemplate;
private final ObjectMapper objectMapper = new ObjectMapper();
private static final String REDIS_COOKIE_MAP_KEY = "tx_sports_cookie_map";
+ @Autowired
+ private MailUtil mailUtil;
public TxSportTokenRefreshService(WebClient.Builder builder,
- StringRedisTemplate redisTemplate) {
+ RedisTemplate redisTemplate) {
this.webClient = builder.baseUrl("https://app.sports.qq.com").build();
this.redisTemplate = redisTemplate;
}
@@ -35,11 +40,12 @@ public class TxSportTokenRefreshService {
log.error("Redis 中未找到 Cookie,无法刷新");
return false;
}
- log.info("旧的cookie:{}",cookieJson);
+// log.info("旧的cookie:{}",cookieJson);
Map baseCookies = objectMapper.readValue(cookieJson, new TypeReference<>() {});
String cookieHeader = buildCookieHeader(baseCookies);
+ log.info("新的cookie:{}",cookieHeader);
String response = webClient.get()
.uri("/init/refresh?os=web")
.header("Cookie", cookieHeader)
@@ -60,7 +66,7 @@ public class TxSportTokenRefreshService {
);
return true;
} catch (Exception e) {
- log.error("刷新 Cookie 失败", e);
+ log.error("刷新 Cookie 失败,原因:{}", e.getMessage());
return false;
}
}
@@ -82,11 +88,30 @@ public class TxSportTokenRefreshService {
}
// 可选:定时自动刷新(你可以打开)
- @Scheduled(fixedRate = 5400000) // 每90分钟执行一次
+ @Scheduled(fixedRate = 1800000)
public void scheduledRefresh() {
log.info("定时刷新Cookie");
- if (!refreshCookies()) {
- log.warn("Cookie 刷新失败,Redis 中可能缺失数据");
+
+ boolean success = refreshCookies();
+
+ if (success) {
+ redisTemplate.delete("errors");
+ log.info("Cookie 刷新成功");
+ return;
+ }
+
+ Long errorCount = redisTemplate.opsForValue().increment("errors");
+
+ log.warn("Cookie 刷新失败,当前连续失败次数:{}", errorCount);
+
+ if (errorCount != null && errorCount > 2) {
+ log.warn("Cookie 刷新失败超过 2 次,Redis 中可能缺失数据");
+
+ mailUtil.sendEmail2(
+ "1131302745@qq.com",
+ "NBA服务异常通知",
+ "腾讯体育超级会员 Cookie 刷新失败,请排查。当前连续失败次数:" + errorCount
+ );
}
}
}
diff --git a/src/main/java/com/ping/study/test/TxMatch.java b/src/main/java/com/ping/study/test/TxMatch.java
new file mode 100644
index 0000000..f423a32
--- /dev/null
+++ b/src/main/java/com/ping/study/test/TxMatch.java
@@ -0,0 +1,47 @@
+package com.ping.study.test;
+
+import com.ping.study.model.dto.tx.TxMatchRequest;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import org.springframework.web.reactive.function.client.WebClient;
+
+
+@RestController
+@RequestMapping("/test")
+public class TxMatch {
+
+ private final WebClient webClient;
+
+ public TxMatch(@Qualifier("matchWebClient") WebClient webClient) {
+ this.webClient = webClient;
+ }
+
+ @RequestMapping("/match")
+ public String getMatch() {
+ TxMatchRequest request = new TxMatchRequest();
+ System.out.println("TxMatchRequest = " + request);
+
+ // 正式发起请求
+ return webClient.get()
+ .uri(uriBuilder -> {
+ uriBuilder.path("/matchUnion/list")
+ .queryParam("today", request.getToday())
+ .queryParam("startTime", request.getStartTime())
+ .queryParam("endTime", request.getEndTime())
+ .queryParam("columnId", request.getColumnId())
+ .queryParam("index", request.getIndex())
+ .queryParam("isInit", request.getIsInit())
+ .queryParam("timestamp", request.getTimestamp())
+ .queryParam("callback", request.getCallback());
+ return uriBuilder.build();
+ })
+ .retrieve()
+ .bodyToMono(String.class)
+ .block();
+ }
+
+
+
+}
diff --git a/src/main/java/com/ping/study/utils/MailUtil.java b/src/main/java/com/ping/study/utils/MailUtil.java
new file mode 100644
index 0000000..62bbcb3
--- /dev/null
+++ b/src/main/java/com/ping/study/utils/MailUtil.java
@@ -0,0 +1,201 @@
+package com.ping.study.utils;
+
+import jakarta.mail.MessagingException;
+import jakarta.mail.internet.MimeMessage;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.mail.SimpleMailMessage;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.mail.javamail.MimeMessageHelper;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+import org.springframework.util.Assert;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Random;
+
+/**
+ * 邮件发送工具类
+ */
+@Component
+@Slf4j
+public class MailUtil {
+
+ @Autowired
+ private RedisUtil redisUtil;
+
+ private final JavaMailSender javaMailSender;
+
+ @Autowired
+ public MailUtil(JavaMailSender javaMailSender) {
+ this.javaMailSender = javaMailSender;
+ }
+
+ /**
+ * 生成 6 位数字验证码
+ *
+ * @return 6 位验证码
+ */
+ private String generateVerificationCode() {
+ Random random = new Random();
+ int code = 100000 + random.nextInt(900000); // 生成 100000 - 999999 的随机数
+ return String.valueOf(code);
+ }
+
+ /**
+ * 发送验证码邮件
+ *
+ * @param to 收件人邮箱
+ */
+ public String sendVerificationCode(String to) {
+ Assert.hasText(to, "邮箱不能为空");
+
+ // 生成验证码
+ String verificationCode = generateVerificationCode();
+ // 构造邮件
+ SimpleMailMessage message = new SimpleMailMessage();
+ message.setFrom("xdd9@vip.qq.com");
+ message.setTo(to);
+ message.setSubject("【NBA轻松看验证码】");
+ message.setText("您的验证码是:" + verificationCode + ",有效期 5 分钟"+"。请勿回复此邮件。"+"\n反馈邮箱:super@2026123.xyz"+"\n发布页:2026123.xyz");
+// message.setText("发布页:2026123.xyz");
+ // 发送邮件
+ try {
+ // 返回验证码(存入 Redis 用于验证)
+ if (redisUtil.redisTemplateT(to,verificationCode)){
+ javaMailSender.send(message);
+ log.info("收件邮箱 -> {}", to);
+ log.info("验证码存入redis -> {}", verificationCode);
+ return "验证码发送成功,因邮件发送有延迟请不要重复发送";
+ }
+// redisUtil.redisTemplateT(to,verificationCode);
+ return "验证码已发送到邮箱,请不要重复点击,如未收到邮件检查垃圾箱或邮箱是否正确";
+// return verificationCode;
+ }catch (Exception e){
+ //System.out.println("发送邮件失败:" + e.getMessage());
+ return "发送邮件失败,请检查邮箱号是否正确";
+ }
+ }
+
+ /**
+ * 发送邮件
+ *
+ * @param to 收件人邮箱
+ * @param subject 邮件主题
+ * @param text 邮件内容
+ */
+ public String sendEmail2(String to,String subject,String text) {
+ Assert.hasText(to, "邮箱不能为空");
+
+ // 构造邮件
+ SimpleMailMessage message = new SimpleMailMessage();
+ message.setFrom("xdd9@vip.qq.com");
+ message.setTo(to);
+ message.setSubject(subject);
+ message.setText(text);
+// message.setText("发布页:2026123.xyz");
+ // 发送邮件
+ try {
+ javaMailSender.send(message);
+ return "发送成功";
+// return verificationCode;
+ }catch (Exception e){
+ log.info("发送邮件失败:{}", e.getMessage());
+ return "发送邮件失败";
+ }
+ }
+ /**
+ *
+ * @param map
+ */
+ public void sendEmail(HashMap map) {
+ Assert.hasText(map.get("mail"), "收件人不能为空");
+
+ // 构造邮件
+ SimpleMailMessage message = new SimpleMailMessage();
+ message.setFrom("xdd9@vip.qq.com");
+ message.setTo(map.get("mail"));
+ message.setSubject(map.get("subject"));
+ message.setText(map.get("text"));
+ // 发送邮件
+ javaMailSender.send(message);
+ }
+
+ /**
+ * 发送 HTML 邮件
+ *
+ * @param to 收件人邮箱
+ * @param subject 邮件主题
+ * @param html HTML 内容
+ */
+ public void sendHtmlEmail(String to, String subject, String html) throws MessagingException, jakarta.mail.MessagingException {
+ Assert.hasText(to, "邮箱不能为空");
+ Assert.hasText(subject, "邮件主题不能为空");
+ Assert.hasText(html, "HTML 内容不能为空");
+
+ jakarta.mail.internet.MimeMessage message = javaMailSender.createMimeMessage();
+ MimeMessageHelper helper = new MimeMessageHelper(message, true);
+ helper.setTo(to);
+ helper.setSubject(subject);
+ helper.setText(html, true); // true 表示支持 HTML
+ javaMailSender.send(message);
+ }
+
+ /**
+ * 发送带附件的邮件
+ *
+ * @param to 收件人邮箱
+ * @param subject 邮件主题
+ * @param text 邮件内容
+ * @param attachments 附件文件列表
+ */
+ public void sendEmailWithAttachments(String to, String subject, String text, List attachments) throws MessagingException, jakarta.mail.MessagingException {
+ Assert.hasText(to, "收件人邮箱不能为空");
+ Assert.hasText(subject, "邮件主题不能为空");
+ Assert.hasText(text, "邮件内容不能为空");
+ Assert.notEmpty(attachments, "附件列表不能为空");
+
+ jakarta.mail.internet.MimeMessage message = javaMailSender.createMimeMessage();
+ MimeMessageHelper helper = new MimeMessageHelper(message, true);
+ helper.setTo(to);
+ helper.setSubject(subject);
+ helper.setText(text);
+
+ // 添加附件
+ for (File file : attachments) {
+ helper.addAttachment(file.getName(), file);
+ }
+
+ javaMailSender.send(message);
+ }
+
+ /**
+ * 发送带内联资源的 HTML 邮件
+ *
+ * @param to 收件人邮箱
+ * @param subject 邮件主题
+ * @param html HTML 内容
+ * @param inlineFiles 内联资源文件列表
+ */
+ public void sendHtmlEmailWithInline(String to, String subject, String html, List inlineFiles) throws MessagingException, jakarta.mail.MessagingException {
+ Assert.hasText(to, "收件人邮箱不能为空");
+ Assert.hasText(subject, "邮件主题不能为空");
+ Assert.hasText(html, "HTML 内容不能为空");
+ Assert.notEmpty(inlineFiles, "内联资源文件列表不能为空");
+
+ MimeMessage message = javaMailSender.createMimeMessage();
+ MimeMessageHelper helper = new MimeMessageHelper(message, true);
+ helper.setTo(to);
+ helper.setSubject(subject);
+ helper.setText(html, true);
+
+ // 添加内联资源
+ for (File file : inlineFiles) {
+ helper.addInline(file.getName(), file);
+ }
+
+ javaMailSender.send(message);
+ }
+}
diff --git a/src/main/java/com/ping/study/utils/NbaApi.java b/src/main/java/com/ping/study/utils/NbaApi.java
index 8208506..291efe8 100644
--- a/src/main/java/com/ping/study/utils/NbaApi.java
+++ b/src/main/java/com/ping/study/utils/NbaApi.java
@@ -5,6 +5,7 @@ import com.ping.study.model.dto.NbaStatsRequestDto;
import com.ping.study.model.vo.ApiResponse;
import com.ping.study.model.vo.Game;
import com.ping.study.model.vo.Group;
+import com.ping.study.model.vo.live.LiveIds;
import com.ping.study.pojo.Games;
import com.ping.study.pojo.Urls;
import com.ping.study.service.GamesService;
@@ -43,13 +44,15 @@ public class NbaApi {
this.webClient = webClient;
}
- public List addGames() {
+ public List addGames() throws InterruptedException {
// 创建请求DTO(会自动设置当前时间戳和日期)
NbaStatsRequestDto requestParams = new NbaStatsRequestDto();
- log.info("{}", requestParams);
- log.info("进入{},执行{}", this.getClass().getName(), "getGames");
- //获取所有当天直播id
- List lives = matchService.getPlayoffLiveIdsBlocking(LocalDate.now(), LocalDate.now());
+// log.info("{}", requestParams);
+// log.info("进入{},执行{}", this.getClass().getName(), "getGames");
+ //获取所有当天直播id集合
+ List lives = matchService.getPlayoffLiveIdsBlocking(LocalDate.now(), LocalDate.now());
+
+ Thread.sleep(1000);
List gameEntities = webClient.get()
.uri("/game/schedule", uriBuilder -> uriBuilder
.queryParam("app_key", requestParams.getAppKey())
@@ -69,7 +72,7 @@ public class NbaApi {
.retrieve()
.bodyToMono(ApiResponse.class)
.map(response -> {
- log.info("{}", response);
+
List entities = new ArrayList<>();
// 遍历groups中的games
for (Group group : response.getData().getGroups()) {
@@ -81,7 +84,14 @@ public class NbaApi {
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));
+ entity.setStatus(group.getGames().get(i).getStatus());
+ for (int j = 0; j < lives.size(); j++){
+ if (lives.get(j).getMId().contains(group.getGames().get(i).getGameId())){
+ entity.setPlayId(lives.get(j).getPlayId());
+ break;
+ }
+ }
+// entity.setPlayId(lives.get(i));
// 合并日期和时间
String dateTimeStr = group.getGames().get(i).getStartDate() + " " + group.getGames().get(i).getStartTime();
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@@ -103,33 +113,16 @@ public class NbaApi {
urls.setType("zb");
urls.setUrl("");
break;
+// case 2:
+// urls.setType("zb");
+// urls.setUrl("");
+// break;
}
urlsMapper.insertSelective(urls);
}
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/RedisUtil.java b/src/main/java/com/ping/study/utils/RedisUtil.java
new file mode 100644
index 0000000..d63c9eb
--- /dev/null
+++ b/src/main/java/com/ping/study/utils/RedisUtil.java
@@ -0,0 +1,24 @@
+package com.ping.study.utils;
+
+import lombok.extern.slf4j.Slf4j;
+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.Component;
+
+import java.time.Duration;
+
+@Component
+@Slf4j
+public class RedisUtil {
+
+ @Autowired
+ private RedisTemplate stringRedisTemplate;
+
+ public boolean redisTemplateT(String email,String code) {
+ //log.info("验证码:{}",code);
+ return stringRedisTemplate.opsForValue().setIfAbsent(email, code, Duration.ofSeconds(60*5));
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/ping/study/utils/UserUtil.java b/src/main/java/com/ping/study/utils/UserUtil.java
new file mode 100644
index 0000000..766cef7
--- /dev/null
+++ b/src/main/java/com/ping/study/utils/UserUtil.java
@@ -0,0 +1,24 @@
+package com.ping.study.utils;
+
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.stereotype.Component;
+
+@Component
+public class UserUtil {
+
+ public String getClientIp(HttpServletRequest request) {
+ String ip = request.getHeader("X-Forwarded-For");
+ if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
+ // 取第一段 IP
+ return ip.split(",")[0];
+ }
+
+ ip = request.getHeader("X-Real-IP");
+ if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
+ return ip;
+ }
+
+ return request.getRemoteAddr();
+ }
+
+}
diff --git a/src/main/java/com/ping/study/utils/tx/TxVideoAuthRedisUtil.java b/src/main/java/com/ping/study/utils/tx/TxVideoAuthRedisUtil.java
new file mode 100644
index 0000000..c1d37da
--- /dev/null
+++ b/src/main/java/com/ping/study/utils/tx/TxVideoAuthRedisUtil.java
@@ -0,0 +1,194 @@
+package com.ping.study.utils.tx;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Component;
+
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+
+/**
+ * 从 Redis 获取 cookie 并生成 auth_ext / logintoken(raw + urlencoded)
+ *
+ * 使用:
+ * @Autowired TxVideoAuthRedisUtil txUtil;
+ * Map result = txUtil.buildAuthAndLoginToken("tx_sports_cookie_map");
+ * String authExtEncoded = result.get("auth_ext_encoded");
+ * String loginTokenEncoded = result.get("logintoken_encoded");
+ */
+@Component
+public class TxVideoAuthRedisUtil {
+
+ private final RedisTemplate stringRedisTemplate;
+ private final ObjectMapper objectMapper;
+
+ public TxVideoAuthRedisUtil(RedisTemplate stringRedisTemplate) {
+ this.stringRedisTemplate = stringRedisTemplate;
+ this.objectMapper = new ObjectMapper();
+ }
+
+ /**
+ * 主方法:给定 redis key(存 cookie 的值),返回 map 包含:
+ * - auth_ext_raw
+ * - auth_ext_encoded
+ * - logintoken_raw
+ * - logintoken_encoded
+ * - cookie_map_json (string representation of parsed cookie map)
+ */
+ public Map buildAuthAndLoginToken(String redisKey) {
+ String redisValue = stringRedisTemplate.opsForValue().get(redisKey);
+ Map cookieMap = parseRedisCookieValue(redisValue);
+
+ String authExtRaw = genAuthExt(cookieMap);
+ String logintokenRaw = genLoginToken(cookieMap);
+
+ String authExtEncoded = urlEncode(authExtRaw);
+ String logintokenEncoded = urlEncode(logintokenRaw);
+
+ Map out = new HashMap<>();
+ out.put("auth_ext_raw", authExtRaw);
+ out.put("auth_ext_encoded", authExtEncoded);
+ out.put("logintoken_raw", logintokenRaw);
+ out.put("logintoken_encoded", logintokenEncoded);
+ try {
+ out.put("cookie_map_json", objectMapper.writeValueAsString(cookieMap));
+ } catch (Exception e) {
+ out.put("cookie_map_json", cookieMap.toString());
+ }
+ return out;
+ }
+
+ /**
+ * 解析 Redis 中的值 —— 支持两种常见情况:
+ * 1) JSON 字符串(例如你直接把 Map->JSON 存进去);
+ * 2) Cookie 的原始字符串 "k=v; k2=v2; ..."
+ */
+ private Map parseRedisCookieValue(String redisValue) {
+ Map map = new HashMap<>();
+ if (redisValue == null) return map;
+
+ String trimmed = redisValue.trim();
+ // 1) 如果像 JSON(以 { 开头 和 } 结尾),尝试解析为 Map
+ if ((trimmed.startsWith("{") && trimmed.endsWith("}")) ||
+ (trimmed.startsWith("[") && trimmed.endsWith("]"))) {
+ try {
+ Map temp = objectMapper.readValue(trimmed, new TypeReference