diff --git a/pom.xml b/pom.xml
index 341677e..18c88f0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -82,7 +82,10 @@
redisson-spring-boot-starter
3.13.3
-->
-
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+
com.github.pagehelper
pagehelper
diff --git a/src/main/java/cn/stock/market/domain/basic/service/SiteNewsService.java b/src/main/java/cn/stock/market/domain/basic/service/SiteNewsService.java
index 08d39db..dcb72bc 100644
--- a/src/main/java/cn/stock/market/domain/basic/service/SiteNewsService.java
+++ b/src/main/java/cn/stock/market/domain/basic/service/SiteNewsService.java
@@ -1,11 +1,30 @@
package cn.stock.market.domain.basic.service;
-import java.util.List;
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
import javax.servlet.http.HttpServletRequest;
+import cn.qutaojing.common.jpa.ConditionBuilder;
+import cn.stock.market.infrastructure.db.po.QSiteNewsPO;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
+
import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import com.github.pagehelper.PageHelper;
@@ -25,7 +44,8 @@ import cn.stock.market.utils.StringUtils;
import cn.stock.market.utils.Utils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
-import net.sf.json.JSONObject;
+import org.springframework.web.client.RestTemplate;
+
/**
* SiteNewsService
@@ -40,6 +60,8 @@ import net.sf.json.JSONObject;
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class SiteNewsService {
final SiteNewsRepository repository;
+ private final RedisTemplate redisTemplate;
+ private final RestTemplate restTemplate;
final SiteNewsFactory factory;
/*新闻资讯-查询列表*/
@@ -54,6 +76,92 @@ public class SiteNewsService {
return ServerResponse.createBySuccess(Utils.toPageHelperInfo(page));
}
+ public ServerResponse getLatestStockNews(int pageNum) {
+ int pageSize = 20;
+ String redisKey = "fmp:stock:latest:news:page:" + pageNum;
+
+ try {
+ // Step 1: get from cache
+ String cached = redisTemplate.opsForValue().get(redisKey);
+ List list;
+ if (cached!= null && !cached.isEmpty()) {
+ list = JSON.parseArray(cached, SiteNews.class);
+ } else {
+ // Step 2: call FMP API
+ String url = String.format("https://financialmodelingprep.com/stable/news/stock-latest?page=%d&limit=%d&apikey=57ZI1xeAsqHY7ag0FBuMkwQzt6TQ60dG", pageNum, pageSize);
+ HttpHeaders headers = new HttpHeaders();
+ headers.add("accept", "application/json");
+ HttpEntity entity = new HttpEntity<>(headers);
+ ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
+
+ if (response.getStatusCode().value() != 200 || response.getBody() == null) {
+ return ServerResponse.createByErrorMsg("Failed to fetch news");
+ }
+
+ JSONArray newsArray = JSON.parseArray(response.getBody());
+ list = new ArrayList<>();
+ for (int i = 0; i < newsArray.size(); i++) {
+ JSONObject obj = newsArray.getJSONObject(i);
+ String sourceId = obj.getString("url");
+
+ SiteNews news = new SiteNews();
+ news.setSourceId(sourceId);
+ news.setTitle(obj.getString("title"));
+ news.setSourceName(obj.getString("publisher"));
+ news.setDescription(obj.getString("text"));
+ news.setContent(obj.getString("text"));
+ news.setImgurl(obj.getString("image"));
+ news.setStatus(1);
+ news.setType(1);
+ news.setViews(0);
+ news.setAddTime(new Date());
+
+ try {
+ ZonedDateTime ny = ZonedDateTime.of(
+ LocalDateTime.parse(obj.getString("publishedDate"), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")),
+ ZoneId.of("America/New_York")
+ );
+ news.setShowTime(Date.from(ny.withZoneSameInstant(ZoneId.systemDefault()).toInstant()));
+ } catch (Exception e) {
+ news.setShowTime(new Date());
+ }
+
+ list.add(news);
+ }
+
+ redisTemplate.opsForValue().set(redisKey, JSON.toJSONString(list), Duration.ofMinutes(10));
+ }
+ int simulatedTotal = 100;
+
+ Page page = buildPage(list, pageNum, pageSize, simulatedTotal);
+ return ServerResponse.createBySuccess(page);
+
+ } catch (Exception e) {
+ log.error("Error getting latest stock news page {}", pageNum, e);
+ try{
+ Page page = repository.findAll(ConditionBuilder.builder().build(), PageParam.of(pageNum, pageSize), QSiteNewsPO.siteNewsPO.showTime.desc());
+
+ return ServerResponse.createBySuccess(page);
+ }catch (Exception e1) {
+ return ServerResponse.createByErrorMsg("Internal error");
+ }
+ }
+ }
+
+ private Page buildPage(List allNews, int pageNum, int pageSize, int totalCount) {
+ int offset = pageNum * pageSize;
+
+ // Defensive bounds check
+ int toIndex = Math.min(offset + pageSize, allNews.size());
+
+ List pagedList = offset >= allNews.size() ? allNews : allNews.subList(offset, toIndex);
+
+ Pageable pageable = PageRequest.of(pageNum, pageSize);
+
+ // You can pass total = 100 to simulate full dataset
+ return new PageImpl<>(pagedList, pageable, totalCount);
+ }
+
/*新闻资讯-查询详情*/
public ServerResponse getDetail(int id) {
return ServerResponse.createBySuccess(repository.find(id));
@@ -76,89 +184,8 @@ public class SiteNewsService {
return ServerResponse.createBySuccess(pageInfo);
}
- /*新闻资讯-抓取*/
- public int grabNews() {
- int ret = 0;
- //新闻类型:1、财经要闻,2、经济数据,3、全球股市,4、7*24全球,5、商品资讯,6、上市公司,7、全球央行
- ret = addNews(1, PropertiesUtil.getProperty("news.main.url") + "/pc_news/FastNews/GetImportantNewsList");
- log.info("财经要闻-抓取条数:" + ret);
- ret = addNews(2, PropertiesUtil.getProperty("news.main.url") + "/pc_news/FastNews/GetInfoList?code=125&pageNumber=1&pagesize=20&condition=&r=");
- log.info("经济数据-抓取条数:" + ret);
- ret = addNews(3, PropertiesUtil.getProperty("news.main.url") + "/pc_news/FastNews/GetInfoList?code=105&pageNumber=1&pagesize=20&condition=&r=");
- log.info("全球股市-抓取条数:" + ret);
-
- ret = addNews(4, PropertiesUtil.getProperty("news.main.url") + "/pc_news/FastNews/GetInfoList?code=100&pageNumber=1&pagesize=20&condition=&r=");
- log.info("7*24全球-抓取条数:" + ret);
-
- ret = addNews(5, PropertiesUtil.getProperty("news.main.url") + "/pc_news/FastNews/GetInfoList?code=106&pageNumber=1&pagesize=20&condition=&r=");
- log.info("商品资讯-抓取条数:" + ret);
-
- ret = addNews(6, PropertiesUtil.getProperty("news.main.url") + "/pc_news/FastNews/GetInfoList?code=103&pageNumber=1&pagesize=20&condition=&r=");
- log.info("上市公司-抓取条数:" + ret);
-
- ret = addNews(7, PropertiesUtil.getProperty("news.main.url") + "/pc_news/FastNews/GetInfoList?code=118&pageNumber=1&pagesize=20&condition=&r=");
- log.info("全球央行-抓取条数:" + ret);
-
- return ret;
- }
-
- /*
- *抓取新闻专用
- * type:新闻类型:1、财经要闻,2、经济数据,3、全球股市,4、7*24全球,5、商品资讯,6、上市公司,7、全球央行
- * */
- private int addNews(Integer type, String url){
- int k = 0;
- try {
- String newlist = HttpRequest.doGrabGet(url);
- JSONObject json = JSONObject.fromObject(newlist);
- if(json != null && json.getJSONArray("items") != null && json.getJSONArray("items").size() > 0){
- for (int i = 0; i < json.getJSONArray("items").size(); i++){
- JSONObject model = JSONObject.fromObject(json.getJSONArray("items").getString(i));
- String newsId = model.getString("code");
- String imgUrl = null;
- if(model.has("imgUrl")){
- imgUrl = model.getString("imgUrl");
- }
- //新闻不存在则添加
- if(repository.getNewsBySourceIdCount(newsId) == 0){
- //获取新闻详情
- String newdata = HttpRequest.doGrabGet(PropertiesUtil.getProperty("news.main.url") + "/PC_News/Detail/GetDetailContent?id="+ newsId +"&type=1");
- newdata = newdata.substring(1,newdata.length()-1).replace("\\\\\\\"","\"");
- newdata = newdata.replace("\\\"","\"");
- newdata = StringUtils.UnicodeToCN(newdata);
- newdata = StringUtils.delHTMLTag(newdata);
-
- JSONObject jsonnew = JSONObject.fromObject(newdata);
- if(jsonnew != null && jsonnew.get("data") != null){
- JSONObject news = JSONObject.fromObject(jsonnew.get("data"));
- SiteNews siteNews = new SiteNews();
- siteNews.setSourceId(newsId);
- siteNews.setSourceName(news.getString("source"));
- siteNews.setTitle(news.getString("title"));
- String showTime = news.getString("showTime");
- siteNews.setShowTime(DateTimeUtil.strToDate(showTime));
- siteNews.setImgurl(imgUrl);
- siteNews.setDescription(news.getString("description"));
- siteNews.setContent(news.getString("content"));
- siteNews.setStatus(1);
- siteNews.setType(type);
- try {
- repository.saveAndFlush(siteNews);
- } catch(Exception e) {
- log.warn("siteNewsMapper insert error: {}", e.getLocalizedMessage());
- }
- k++;
- }
- }
- }
- }
- } catch (Exception e) {
- e.printStackTrace();
- }
- return k;
- }
public SiteNewsRepository repository() {
return repository;
diff --git a/src/main/java/cn/stock/market/infrastructure/job/InvestingTask.java b/src/main/java/cn/stock/market/infrastructure/job/InvestingTask.java
index d807405..42a21f2 100644
--- a/src/main/java/cn/stock/market/infrastructure/job/InvestingTask.java
+++ b/src/main/java/cn/stock/market/infrastructure/job/InvestingTask.java
@@ -16,6 +16,7 @@ import com.alibaba.fastjson.JSONObject;
import com.google.common.base.Stopwatch;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.codec.digest.DigestUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
@@ -36,6 +37,7 @@ import javax.annotation.PostConstruct;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.ZoneId;
+import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.TimeUnit;
@@ -188,128 +190,93 @@ public class InvestingTask {
}
/*德国新闻接口*/
-// @Scheduled(cron = "0 0 0/3 * * ?")
+ @Scheduled(cron = "0 0 0/3 * * ?")
+// @PostConstruct
public void saveGerNews() {
- log.info("德国股票新闻数据同步开始");
+ log.info("FMP 股票新闻数据同步开始");
int savedCount = 0;
int totalCount = 0;
+
try {
- // API URL for getting news list
- String newsListUrl = "https://api.boerse-frankfurt.de/v1/data/category_news?newsType=ALL&lang=de&offset=0&limit=50";
-
- // Headers for the API request
+ String newsListUrl = "https://financialmodelingprep.com/stable/news/stock-latest?page=0&limit=30&apikey=57ZI1xeAsqHY7ag0FBuMkwQzt6TQ60dG";
+
HttpHeaders headers = new HttpHeaders();
- headers.add("accept", "application/json, text/plain, */*");
- headers.add("accept-language", "en-US,en;q=0.9,vi;q=0.8,ug;q=0.7,fr;q=0.6");
- headers.add("origin", "https://www.boerse-frankfurt.de");
- headers.add("priority", "u=1, i");
- headers.add("referer", "https://www.boerse-frankfurt.de/");
- headers.add("user-agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36");
+ headers.add("accept", "application/json");
HttpEntity entity = new HttpEntity<>(headers);
-
- // Get news list
ResponseEntity response = restTemplate.exchange(
- newsListUrl,
- HttpMethod.GET,
- entity,
- String.class
+ newsListUrl,
+ HttpMethod.GET,
+ entity,
+ String.class
);
if (response.getStatusCode().value() == 200 && response.getBody() != null) {
- JSONObject newsListResponse = JSON.parseObject(response.getBody());
- JSONArray newsData = newsListResponse.getJSONArray("data");
-
- if (newsData != null && newsData.size() > 0) {
- totalCount = newsData.size();
- log.info("Found {} German news items to process", totalCount);
-
- for (int i = 0; i < newsData.size(); i++) {
- try {
- JSONObject newsItem = newsData.getJSONObject(i);
- String newsId = newsItem.getString("id");
- String headline = newsItem.getString("headline");
- String time = newsItem.getString("time");
- String source = newsItem.getString("source");
- String teaserText = newsItem.getString("teaserText");
- String teaserImageUrl = newsItem.getString("teaserImageUrl");
-
- // Check if news already exists
- List existingNews = newsRepository.findAll(QSiteNewsPO.siteNewsPO.sourceId.eq(newsId));
- if (existingNews.size() == 0) {
- // Get news detail
- String newsDetailUrl = "https://api.boerse-frankfurt.de/v1/data/news?id=" + newsId + "&lang=de";
- HttpEntity detailEntity = new HttpEntity<>(headers);
-
- ResponseEntity detailResponse = restTemplate.exchange(
- newsDetailUrl,
- HttpMethod.GET,
- detailEntity,
- String.class
- );
-
- if (detailResponse.getStatusCode().value() == 200 && detailResponse.getBody() != null) {
- JSONObject newsDetail = JSON.parseObject(detailResponse.getBody());
- String body = newsDetail.getString("body");
-
- // Create SiteNews entity
- SiteNews siteNews = new SiteNews();
- siteNews.setAddTime(new Date());
- siteNews.setSourceId(newsId);
- siteNews.setTitle(headline);
- siteNews.setSourceName(source);
- siteNews.setDescription(teaserText != null ? teaserText : "");
- siteNews.setImgurl(teaserImageUrl);
- siteNews.setContent(body != null ? body : "");
- siteNews.setStatus(1);
- siteNews.setType(1); // Set as financial news type
- siteNews.setViews(0);
-
- // Parse and set show time
- if (time != null && !time.isEmpty()) {
- try {
- // Parse ISO 8601 format: "2025-06-19T08:37:58+02:00"
- // Remove timezone offset and convert to standard format
- String timeStr = time.replace("+02:00", "").replace("T", " ");
- siteNews.setShowTime(DateTimeUtil.strToDate(timeStr, "yyyy-MM-dd HH:mm:ss"));
- } catch (Exception e) {
- log.warn("Failed to parse time for news {}: {}", newsId, time);
- siteNews.setShowTime(new Date());
- }
- } else {
- siteNews.setShowTime(new Date());
- }
-
- try {
- newsRepository.save(siteNews);
- savedCount++;
- log.info("Saved German news [{}/{}]: {}", savedCount, totalCount, headline);
- } catch (Exception e) {
- log.warn("Failed to save German news {}: {}", newsId, e.getMessage());
- }
- } else {
- log.warn("Failed to get news detail for {}: HTTP {}", newsId, detailResponse.getStatusCode());
- }
- } else {
- log.debug("News {} already exists, skipping", newsId);
- }
- } catch (Exception e) {
- log.warn("Error processing news item {}: {}", i, e.getMessage());
+ JSONArray newsArray = JSON.parseArray(response.getBody());
+ totalCount = newsArray.size();
+ log.info("Found {} news items to process", totalCount);
+
+ for (int i = 0; i < newsArray.size(); i++) {
+ try {
+ JSONObject newsItem = newsArray.getJSONObject(i);
+ String sourceId = newsItem.getString("url");
+
+ // Check existence
+ List existingNews = newsRepository.findAll(QSiteNewsPO.siteNewsPO.sourceId.eq(sourceId));
+ if (!existingNews.isEmpty()) {
+ log.debug("News {} already exists, skipping", sourceId);
+ continue;
}
+
+ // Create and populate SiteNews entity
+ SiteNews siteNews = new SiteNews();
+ siteNews.setAddTime(new Date());
+ siteNews.setSourceId(sourceId);
+ siteNews.setTitle(newsItem.getString("title"));
+ siteNews.setSourceName(newsItem.getString("publisher"));
+ siteNews.setDescription(newsItem.getString("symbol"));
+ siteNews.setImgurl(newsItem.getString("image"));
+ siteNews.setContent(newsItem.getString("text"));
+ siteNews.setStatus(1);
+ siteNews.setType(1);
+ siteNews.setViews(0);
+
+ // Parse publishedDate
+ String publishedDate = newsItem.getString("publishedDate");
+ try {
+ ZonedDateTime nyTime = ZonedDateTime.of(
+ LocalDateTime.parse(publishedDate, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")),
+ ZoneId.of("America/New_York")
+ );
+
+ ZonedDateTime localTime = nyTime.withZoneSameInstant(ZoneId.systemDefault());
+ siteNews.setShowTime(Date.from(localTime.toInstant()));
+ } catch (Exception e) {
+ log.warn("Failed to parse publishedDate with timezone for {}: {}", sourceId, publishedDate);
+ siteNews.setShowTime(new Date());
+ }
+
+ // Save news
+ newsRepository.save(siteNews);
+ savedCount++;
+ log.info("Saved news [{}/{}]: {}", savedCount, totalCount, siteNews.getTitle());
+
+ } catch (Exception e) {
+ log.warn("Error processing news item {}: {}", i, e.getMessage());
}
- } else {
- log.warn("No news data found in API response");
}
+
} else {
- log.error("Failed to get news list: HTTP {}", response.getStatusCode());
+ log.error("Failed to fetch news: HTTP {}", response.getStatusCode());
}
- log.info("德国股票新闻数据同步完成,处理了 {} 条新闻,保存了 {} 条新闻", totalCount, savedCount);
+
+ log.info("FMP 股票新闻数据同步完成,总数 {},已保存 {}", totalCount, savedCount);
} catch (Exception e) {
- log.error("德国新闻数据同步异常,异常信息: {}", e.getMessage(), e);
+ log.error("FMP 新闻同步异常: {}", e.getMessage(), e);
}
}
- @Scheduled(cron = "0 0 0/3 * * ?")
+// @Scheduled(cron = "0 0 0/3 * * ?")
// @PostConstruct
public void getBoerseNews(){
String url_request = "https://www.boerse-online.de";
diff --git a/src/main/java/cn/stock/market/infrastructure/job/JobBoot.java b/src/main/java/cn/stock/market/infrastructure/job/JobBoot.java
index a3aa98f..d3ca295 100644
--- a/src/main/java/cn/stock/market/infrastructure/job/JobBoot.java
+++ b/src/main/java/cn/stock/market/infrastructure/job/JobBoot.java
@@ -57,11 +57,11 @@ public class JobBoot {
/*
* 新闻资讯抓取
* */
- @Scheduled(cron = "0 0/30 9-20 * * ?")
- public void newsInfoTask() {
- MdcUtil.setTraceIdIfAbsent();
- Stopwatch stopwatch = Stopwatch.createStarted();
- int count = SiteNewsService.of().grabNews();
- log.info("newsInfoTask执行, 受影响数{}, 耗时:{}毫秒", count, stopwatch.elapsed(TimeUnit.MILLISECONDS));
- }
+// @Scheduled(cron = "0 0/30 9-20 * * ?")
+// public void newsInfoTask() {
+// MdcUtil.setTraceIdIfAbsent();
+// Stopwatch stopwatch = Stopwatch.createStarted();
+// int count = SiteNewsService.of().grabNews();
+// log.info("newsInfoTask执行, 受影响数{}, 耗时:{}毫秒", count, stopwatch.elapsed(TimeUnit.MILLISECONDS));
+// }
}
diff --git a/src/main/java/cn/stock/market/infrastructure/redis/config/RedisConfig.java b/src/main/java/cn/stock/market/infrastructure/redis/config/RedisConfig.java
index 2bab268..9f5c238 100644
--- a/src/main/java/cn/stock/market/infrastructure/redis/config/RedisConfig.java
+++ b/src/main/java/cn/stock/market/infrastructure/redis/config/RedisConfig.java
@@ -1,70 +1,53 @@
-//package cn.stock.market.infrastructure.redis.config;
-//
-//import java.time.Duration;
-//
-//import org.redisson.api.RedissonClient;
-//import org.springframework.cache.CacheManager;
-//import org.springframework.cache.annotation.EnableCaching;
-//import org.springframework.context.annotation.Bean;
-//import org.springframework.context.annotation.Configuration;
-//import org.springframework.data.redis.cache.RedisCacheConfiguration;
-//import org.springframework.data.redis.cache.RedisCacheManager;
-//import org.springframework.data.redis.connection.RedisConnectionFactory;
-//import org.springframework.data.redis.core.RedisTemplate;
-//import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
-//import org.springframework.data.redis.serializer.RedisSerializationContext;
-//import org.springframework.data.redis.serializer.StringRedisSerializer;
-//
-//import cn.qutaojing.common.aop.distributedlock.DistributedLockTemplate;
-//import cn.stock.market.infrastructure.redis.SingleDistributedLockTemplate;
-//
-///**
-// *
-// * @author xlfd
-// * @email xlfd@gmail.com
-// * @version 1.0
-// * @created Jun 3, 2021 4:56:28 PM
-// */
-//@Configuration
-//@EnableCaching
-//public class RedisConfig {
-//
-// @Bean
-// public RedisTemplate