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>() {}); + for (Map.Entry e : temp.entrySet()) { + map.put(e.getKey(), e.getValue() == null ? "" : String.valueOf(e.getValue())); + } + return map; + } catch (Exception ignored) { + // 解析失败 —— 退回到 cookie 规则解析 + } + } + + // 2) cookie 格式解析: "a=1; b=2; c=3" + String[] parts = redisValue.split(";"); + for (String p : parts) { + if (p == null) continue; + String kv = p.trim(); + if (kv.isEmpty()) continue; + int idx = kv.indexOf('='); + if (idx <= 0) continue; + String k = kv.substring(0, idx).trim(); + String v = kv.substring(idx + 1).trim(); + if (v.startsWith("\"") && v.endsWith("\"") && v.length() >= 2) { + v = v.substring(1, v.length() - 1); + } + map.put(k, v); + } + return map; + } + + /** + * 生成 auth_ext raw JSON 字符串(未 encode) + * 形如: {"main_login":"qq","vqq_openid":"...","vqq_appid":"...","vqq_access_token":"..."} + * 若 cookie 缺字段则使用空串 + */ + private String genAuthExt(Map cookie) { + Map auth = new LinkedHashMap<>(); + auth.put("main_login", cookie.getOrDefault("main_login", "qq")); + auth.put("vqq_openid", cookie.getOrDefault("vqq_openid", cookie.getOrDefault("openid", ""))); + auth.put("vqq_appid", cookie.getOrDefault("vqq_appid", cookie.getOrDefault("appid", ""))); + auth.put("vqq_access_token", cookie.getOrDefault("vqq_access_token", cookie.getOrDefault("access_token", ""))); + + try { + return objectMapper.writeValueAsString(auth); + } catch (Exception e) { + // 手工构造(极小概率) + StringBuilder sb = new StringBuilder("{"); + boolean first = true; + for (Map.Entry en : auth.entrySet()) { + if (!first) sb.append(","); + sb.append("\"").append(escapeJson(en.getKey())).append("\":"); + sb.append("\"").append(escapeJson(en.getValue())).append("\""); + first = false; + } + sb.append("}"); + return sb.toString(); + } + } + + /** + * 生成 logintoken raw JSON 字符串(未 encode) + * 常见字段来源于 cookie:vqq_access_token, vqq_appid, vqq_vusession/vqq_vsession, vqq_openid, vqq_vuserid, video_guid, main_login + */ + private String genLoginToken(Map cookie) { + Map t = new LinkedHashMap<>(); + t.put("access_token", cookie.getOrDefault("vqq_access_token", cookie.getOrDefault("access_token", ""))); + t.put("appid", cookie.getOrDefault("vqq_appid", cookie.getOrDefault("appid", ""))); + // 多种可能命名,优先常见 vqq_vusession -> vqq_vusession -> vusession -> vusess + String vusession = cookie.getOrDefault("vqq_vusession", + cookie.getOrDefault("vqq_vsession", + cookie.getOrDefault("vusession", ""))); + t.put("vusession", vusession); + t.put("openid", cookie.getOrDefault("vqq_openid", cookie.getOrDefault("openid", ""))); + t.put("vuserid", cookie.getOrDefault("vqq_vuserid", cookie.getOrDefault("vuserid", ""))); + t.put("video_guid", cookie.getOrDefault("video_guid", cookie.getOrDefault("qq_domain_video_guid_verify", ""))); + t.put("main_login", cookie.getOrDefault("main_login", "qq")); + + try { + return objectMapper.writeValueAsString(t); + } catch (Exception e) { + StringBuilder sb = new StringBuilder("{"); + boolean first = true; + for (Map.Entry en : t.entrySet()) { + if (!first) sb.append(","); + sb.append("\"").append(escapeJson(en.getKey())).append("\":"); + sb.append("\"").append(escapeJson(en.getValue())).append("\""); + first = false; + } + sb.append("}"); + return sb.toString(); + } + } + + private String urlEncode(String s) { + if (s == null) return ""; + return URLEncoder.encode(s, StandardCharsets.UTF_8); + } + + // 简单 JSON 字符串转义(用于 fallback) + private String escapeJson(String s) { + if (s == null) return ""; + StringBuilder sb = new StringBuilder(); + for (char ch : s.toCharArray()) { + switch (ch) { + case '"': sb.append("\\\""); break; + case '\\': sb.append("\\\\"); break; + case '\b': sb.append("\\b"); break; + case '\f': sb.append("\\f"); break; + case '\n': sb.append("\\n"); break; + case '\r': sb.append("\\r"); break; + case '\t': sb.append("\\t"); break; + default: + if (ch < 0x20) sb.append(String.format("\\u%04x", (int) ch)); + else sb.append(ch); + } + } + return sb.toString(); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 2a584ee..0fe6000 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,17 +1,63 @@ server: + servlet: + session: + cookie: + same-site: None + secure: true # 生产是 https 必须 true port: 9005 +#spring: +# data: +# redis: +# host: 167.253.156.235 +# port: 6739 +# password: redis_a7Gw6f +#spring: +# data: +# redis: +# host: 103.244.88.91 +# port: 6379 +# password: inspur123 +# timeout: 5000ms spring: data: redis: - host: locahost - port: 6739 + host: 116.62.173.2 + port: 6379 + password: redis_cfF356 + timeout: 5000ms application: name: NBA datasource: - driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://locahost:3306/NBA?useUnicode=true&characterEncoding=utf-8&useSSL=false + #mysql5.7.4配置 + driver-class-name: com.mysql.jdbc.Driver + url: jdbc:mysql://localhost:3306/nba?useUnicode=true&characterEncoding=utf-8&useSSL=false +# url: jdbc:mysql://116.62.173.2:3306/nba?useUnicode=true&characterEncoding=utf-8&useSSL=false +# url: jdbc:mysql://154.36.154.211:9002/nba?useUnicode=true&characterEncoding=utf-8&useSSL=false username: root - password: password + password: 123123321 #本地 +# password: mysql_k2KzJj #阿里云杭州 + # driver-class-name: com.mysql.cj.jdbc.Driver +# url: jdbc:mysql://103.244.88.91:3306/nba?useUnicode=true&characterEncoding=utf-8&useSSL=false +# username: nba +# password: d2mzG6w8277NpxFW + mail: + host: smtp.qq.com + port: 465 + username: xdd9@vip.qq.com + password: qhcladjicfydbejj + default-encoding: UTF-8 + properties: + mail.smtp.connectiontimeout: 5000 # 连接超时 5 秒 + mail.smtp.timeout: 5000 # 读超时 5 秒 + mail.smtp.writetimeout: 5000 # 写超时 5 秒 + mail: + smtp: + auth: true + ssl: + enable: true + starttls: + enable: true + required: true mybatis: configuration: map-underscore-to-camel-case: true diff --git a/src/main/resources/com/ping/study/mapper/LoginLogMapper.xml b/src/main/resources/com/ping/study/mapper/LoginLogMapper.xml new file mode 100644 index 0000000..56c6834 --- /dev/null +++ b/src/main/resources/com/ping/study/mapper/LoginLogMapper.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + id,user_id,login_time,login_ip + + + + + + delete from login_log + where id = #{id} + + + insert into login_log + ( id,user_id,login_time,login_ip) + values (#{id},#{userId},#{loginTime},#{loginIp}) + + + insert into login_log + + id, + user_id, + login_time, + login_ip, + + + #{id}, + #{userId}, + #{loginTime}, + #{loginIp}, + + + + update login_log + + + user_id = #{userId}, + + + login_time = #{loginTime}, + + + login_ip = #{loginIp}, + + + where id = #{id} + + + update login_log + set + user_id = #{userId}, + login_time = #{loginTime}, + login_ip = #{loginIp} + where id = #{id} + + diff --git a/src/main/resources/com/ping/study/mapper/NbaUserMapper.xml b/src/main/resources/com/ping/study/mapper/NbaUserMapper.xml new file mode 100644 index 0000000..0e3b7b1 --- /dev/null +++ b/src/main/resources/com/ping/study/mapper/NbaUserMapper.xml @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + id,username,password,email,create_time,update_time, + last_login_time,last_login_ip,register_ip + + + + + + delete from nba_user + where id = #{id} + + + insert into nba_user + ( id,username,password,email,create_time,update_time, + last_login_time,last_login_ip,register_ip) + values (#{id},#{username},#{password},#{email},#{createTime},#{updateTime}, + #{lastLoginTime},#{lastLoginIp},#{registerIp}) + + + insert into nba_user + + id, + username, + password, + email, + create_time, + update_time, + last_login_time, + last_login_ip, + register_ip, + + + #{id}, + #{username}, + #{password}, + #{email}, + #{createTime}, + #{updateTime}, + #{lastLoginTime}, + #{lastLoginIp}, + #{registerIp}, + + + + update nba_user + + + username = #{username}, + + + password = #{password}, + + + email = #{email}, + + + create_time = #{createTime}, + + + update_time = #{updateTime}, + + + last_login_time = #{lastLoginTime}, + + + last_login_ip = #{lastLoginIp}, + + + register_ip = #{registerIp}, + + + where id = #{id} + + + update nba_user + set + username = #{username}, + password = #{password}, + email = #{email}, + create_time = #{createTime}, + update_time = #{updateTime}, + last_login_time = #{lastLoginTime}, + last_login_ip = #{lastLoginIp}, + register_ip = #{registerIp} + where id = #{id} + + + + + + update nba_user set remember_token = #{token} where id = #{userId} + + + + + + update nba_user set remember_token = null where remember_token = #{token} + + + + + + + + update nba_user set password = #{password} where email = #{email} + + + diff --git a/src/main/resources/mapper/UrlsMapper.xml b/src/main/resources/mapper/UrlsMapper.xml index 611c37c..a8cac0c 100644 --- a/src/main/resources/mapper/UrlsMapper.xml +++ b/src/main/resources/mapper/UrlsMapper.xml @@ -77,12 +77,16 @@ + + insert into urls - (game_id, url, type) + (game_id, m3u8_url, type) values (#{gameId}, #{item.url}, #{item.type}) @@ -98,4 +102,20 @@ update urls set url = #{s} where game_id = #{gameId} and type = #{type} + + + + + + + + + update urls set status = #{status} where game_id = #{gameId} + + + + update urls set m3u8_url = #{url} where id = #{id} +