Skip to content

Commit 28b74d1

Browse files
committed
add FF14CrystalNews func
1 parent 9c9fd21 commit 28b74d1

3 files changed

Lines changed: 333 additions & 0 deletions

File tree

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
package com.phantoms.phantomsbackend.common.utils;
2+
3+
import com.alibaba.fastjson.JSONArray;
4+
import com.alibaba.fastjson.JSONObject;
5+
import okhttp3.*;
6+
import org.slf4j.Logger;
7+
import org.slf4j.LoggerFactory;
8+
import org.springframework.stereotype.Component;
9+
10+
import java.io.IOException;
11+
import java.text.SimpleDateFormat;
12+
import java.util.ArrayList;
13+
import java.util.Date;
14+
import java.util.List;
15+
import java.util.concurrent.TimeUnit;
16+
17+
@Component
18+
public class FF14CrystalNewsUtils {
19+
20+
private static final Logger logger = LoggerFactory.getLogger(FF14CrystalNewsUtils.class);
21+
22+
private static final String NEWS_API_URL = "https://apps.game.qq.com/cmc/cross";
23+
private static final String USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36";
24+
25+
private static final OkHttpClient client = new OkHttpClient.Builder()
26+
.connectTimeout(10, TimeUnit.SECONDS)
27+
.writeTimeout(10, TimeUnit.SECONDS)
28+
.readTimeout(30, TimeUnit.SECONDS)
29+
.build();
30+
31+
public static class NewsItem {
32+
private String title;
33+
private String author;
34+
private String date;
35+
private String imageUrl;
36+
private String linkUrl;
37+
private String id;
38+
39+
public NewsItem(String title, String author, String date, String imageUrl, String linkUrl, String id) {
40+
this.title = title;
41+
this.author = author;
42+
this.date = date;
43+
this.imageUrl = imageUrl;
44+
this.linkUrl = linkUrl;
45+
this.id = id;
46+
}
47+
48+
public String getTitle() { return title; }
49+
public String getAuthor() { return author; }
50+
public String getDate() { return date; }
51+
public String getImageUrl() { return imageUrl; }
52+
public String getLinkUrl() { return linkUrl; }
53+
public String getId() { return id; }
54+
}
55+
56+
public List<NewsItem> fetchCrystalNews() {
57+
List<NewsItem> newsList = new ArrayList<>();
58+
59+
try {
60+
HttpUrl url = HttpUrl.parse(NEWS_API_URL).newBuilder()
61+
.addQueryParameter("serviceId", "473")
62+
.addQueryParameter("source", "ff14")
63+
.addQueryParameter("tagids", "135991")
64+
.addQueryParameter("start", "0")
65+
.addQueryParameter("limit", "100")
66+
.build();
67+
68+
Request request = new Request.Builder()
69+
.url(url)
70+
.header("User-Agent", USER_AGENT)
71+
.get()
72+
.build();
73+
74+
try (Response response = client.newCall(request).execute()) {
75+
if (!response.isSuccessful()) {
76+
logger.error("Failed to fetch FF14水晶世界新闻: code={}, url={}", response.code(), url);
77+
return newsList;
78+
}
79+
80+
String responseBody = response.body().string();
81+
logger.debug("FF14水晶世界新闻响应: {}", responseBody);
82+
83+
JSONObject jsonResponse = JSONObject.parseObject(responseBody);
84+
85+
if (jsonResponse == null || jsonResponse.getIntValue("status") != 0) {
86+
logger.error("FF14水晶世界新闻API返回错误: {}", jsonResponse);
87+
return newsList;
88+
}
89+
90+
JSONObject data = jsonResponse.getJSONObject("data");
91+
if (data == null) {
92+
logger.error("FF14水晶世界新闻响应中缺少data字段");
93+
return newsList;
94+
}
95+
96+
JSONArray items = data.getJSONArray("items");
97+
if (items == null || items.isEmpty()) {
98+
logger.warn("FF14水晶世界新闻列表为空");
99+
return newsList;
100+
}
101+
102+
for (int i = 0; i < items.size(); i++) {
103+
JSONObject item = items.getJSONObject(i);
104+
105+
String id = item.getString("iId") != null ? item.getString("iId") : item.getString("iNewsId");
106+
String title = item.getString("sTitle") != null ? item.getString("sTitle") : "";
107+
String author = item.getString("sAuthor") != null ? item.getString("sAuthor") : "";
108+
String created = item.getString("sCreated") != null ? item.getString("sCreated") : "";
109+
110+
String date = created;
111+
112+
String imageUrl = "";
113+
JSONArray coverList = item.getJSONArray("sCoverList");
114+
if (coverList != null && !coverList.isEmpty()) {
115+
for (int j = 0; j < coverList.size(); j++) {
116+
JSONObject cover = coverList.getJSONObject(j);
117+
String size = cover.getString("size");
118+
String coverUrl = cover.getString("url");
119+
120+
if (coverUrl != null && !coverUrl.isEmpty()) {
121+
if ("502*282".equals(size)) {
122+
imageUrl = coverUrl;
123+
break;
124+
} else if ("987*444".equals(size) && imageUrl.isEmpty()) {
125+
imageUrl = coverUrl;
126+
} else if ("1036*608".equals(size) && imageUrl.isEmpty()) {
127+
imageUrl = coverUrl;
128+
} else if (imageUrl.isEmpty()) {
129+
imageUrl = coverUrl;
130+
}
131+
}
132+
}
133+
}
134+
135+
String linkUrl = "";
136+
String docId = item.getString("iDocID");
137+
String redirectURL = item.getString("sRedirectURL");
138+
if (docId != null && !docId.isEmpty()) {
139+
linkUrl = "https://ff14m.qq.com/web202409/#/news/detail?id=" + docId;
140+
} else if (redirectURL != null && !redirectURL.isEmpty()) {
141+
linkUrl = redirectURL;
142+
} else {
143+
linkUrl = "https://ff14m.qq.com/web202409/";
144+
}
145+
146+
if (id != null && !id.isEmpty() && !title.isEmpty()) {
147+
newsList.add(new NewsItem(title, author, date, imageUrl, linkUrl, id));
148+
}
149+
}
150+
151+
logger.info("成功获取 {} 条FF14水晶世界新闻", newsList.size());
152+
153+
}
154+
} catch (Exception e) {
155+
logger.error("获取FF14水晶世界新闻失败", e);
156+
}
157+
158+
return newsList;
159+
}
160+
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package com.phantoms.phantomsbackend.service.scheduler;
2+
3+
import com.phantoms.phantomsbackend.common.utils.FF14CrystalNewsUtils;
4+
import com.phantoms.phantomsbackend.common.utils.NapCatQQUtil;
5+
import com.phantoms.phantomsbackend.common.utils.RedisUtil;
6+
import org.slf4j.Logger;
7+
import org.slf4j.LoggerFactory;
8+
import org.springframework.beans.factory.annotation.Autowired;
9+
import org.springframework.beans.factory.annotation.Value;
10+
import org.springframework.scheduling.annotation.Scheduled;
11+
import org.springframework.stereotype.Component;
12+
13+
import javax.annotation.PostConstruct;
14+
import java.util.ArrayList;
15+
import java.util.List;
16+
import java.util.stream.Collectors;
17+
18+
@Component
19+
public class FF14CrystalNewsScheduler {
20+
21+
private static final Logger logger = LoggerFactory.getLogger(FF14CrystalNewsScheduler.class);
22+
23+
private static final String FF14_CRYSTAL_NEWS_CACHE_KEY = "news:crystal:last_ids";
24+
25+
private List<String> inMemoryCache = new ArrayList<>();
26+
27+
@Autowired
28+
private FF14CrystalNewsUtils ff14CrystalNewsUtils;
29+
30+
@Autowired
31+
private NapCatQQUtil napCatQQUtil;
32+
33+
@Autowired
34+
private RedisUtil redisUtil;
35+
36+
@Value("${napcat.phantom-group-id}")
37+
private String phantomGroupId;
38+
39+
@Value("${napcat.default-group-id}")
40+
private String defaultGroupId;
41+
42+
@Value("${napcat.crystal-group-id}")
43+
private String crystalGroupId;
44+
45+
@PostConstruct
46+
public void initCache() {
47+
logger.info("初始化FF14水晶世界新闻缓存");
48+
try {
49+
try {
50+
Object cachedIdsObj = redisUtil.get(FF14_CRYSTAL_NEWS_CACHE_KEY);
51+
if (cachedIdsObj instanceof List) {
52+
inMemoryCache = (List<String>) cachedIdsObj;
53+
logger.info("从Redis加载缓存成功,共 {} 条水晶世界新闻ID", inMemoryCache.size());
54+
return;
55+
}
56+
} catch (Exception e) {
57+
logger.warn("从Redis读取缓存失败: {}", e.getMessage());
58+
}
59+
60+
logger.info("Redis不可用,从新闻源获取初始缓存");
61+
List<FF14CrystalNewsUtils.NewsItem> newsList = ff14CrystalNewsUtils.fetchCrystalNews();
62+
if (!newsList.isEmpty()) {
63+
inMemoryCache = newsList.stream()
64+
.map(FF14CrystalNewsUtils.NewsItem::getId)
65+
.collect(Collectors.toList());
66+
logger.info("从新闻源加载初始缓存成功,共 {} 条水晶世界新闻ID", inMemoryCache.size());
67+
} else {
68+
logger.warn("从新闻源获取初始缓存失败,使用空缓存");
69+
}
70+
} catch (Exception e) {
71+
logger.error("初始化缓存失败", e);
72+
}
73+
}
74+
75+
@Scheduled(fixedRate = 5 * 60 * 1000)
76+
public void fetchAndSendFF14CrystalNews() {
77+
logger.info("开始获取FF14水晶世界新闻列表");
78+
79+
try {
80+
List<FF14CrystalNewsUtils.NewsItem> newsList = ff14CrystalNewsUtils.fetchCrystalNews();
81+
82+
if (newsList.isEmpty()) {
83+
logger.warn("未获取到FF14水晶世界新闻");
84+
return;
85+
}
86+
87+
List<String> currentIds = newsList.stream()
88+
.map(FF14CrystalNewsUtils.NewsItem::getId)
89+
.collect(Collectors.toList());
90+
91+
List<String> cachedIds = new ArrayList<>();
92+
try {
93+
Object cachedIdsObj = redisUtil.get(FF14_CRYSTAL_NEWS_CACHE_KEY);
94+
if (cachedIdsObj instanceof List) {
95+
cachedIds = (List<String>) cachedIdsObj;
96+
}
97+
} catch (Exception e) {
98+
logger.warn("Redis读取缓存失败,使用内存缓存: {}", e.getMessage());
99+
cachedIds = new ArrayList<>(inMemoryCache);
100+
}
101+
102+
List<String> finalCachedIds = cachedIds;
103+
List<FF14CrystalNewsUtils.NewsItem> newNewsList = newsList.stream()
104+
.filter(news -> !finalCachedIds.contains(news.getId()))
105+
.collect(Collectors.toList());
106+
107+
if (newNewsList.size() > 5) {
108+
logger.warn("检测到缓存丢失,新新闻数量 {} 条超过阈值,使用最新新闻更新缓存", newNewsList.size());
109+
try {
110+
redisUtil.set(FF14_CRYSTAL_NEWS_CACHE_KEY, currentIds);
111+
inMemoryCache = new ArrayList<>(currentIds);
112+
logger.info("缓存已更新,共 {} 条水晶世界新闻ID", currentIds.size());
113+
} catch (Exception e) {
114+
logger.warn("Redis更新缓存失败,仅更新内存缓存: {}", e.getMessage());
115+
inMemoryCache = new ArrayList<>(currentIds);
116+
}
117+
return;
118+
}
119+
120+
if (!newNewsList.isEmpty()) {
121+
logger.info("发现 {} 条FF14水晶世界新新闻", newNewsList.size());
122+
sendNewsToGroup(newNewsList);
123+
} else {
124+
logger.debug("没有FF14水晶世界新新闻");
125+
}
126+
127+
try {
128+
redisUtil.set(FF14_CRYSTAL_NEWS_CACHE_KEY, currentIds);
129+
inMemoryCache = new ArrayList<>(currentIds);
130+
} catch (Exception e) {
131+
logger.warn("Redis更新缓存失败,仅更新内存缓存: {}", e.getMessage());
132+
inMemoryCache = new ArrayList<>(currentIds);
133+
}
134+
135+
} catch (Exception e) {
136+
logger.error("获取FF14水晶世界新闻失败", e);
137+
}
138+
}
139+
140+
private void sendNewsToGroup(List<FF14CrystalNewsUtils.NewsItem> newsList) {
141+
try {
142+
for (FF14CrystalNewsUtils.NewsItem news : newsList) {
143+
StringBuilder message = new StringBuilder();
144+
// message.append("【FF14水晶世界新闻】\n");
145+
146+
if (news.getImageUrl() != null && !news.getImageUrl().isEmpty()) {
147+
message.append("[CQ:image,file=").append(news.getImageUrl()).append("]");
148+
}
149+
message.append(news.getTitle()).append("\n");
150+
if (news.getAuthor() != null && !news.getAuthor().isEmpty()) {
151+
message.append("创建者: ").append(news.getAuthor()).append("\n");
152+
}
153+
if (news.getDate() != null && !news.getDate().isEmpty()) {
154+
message.append("创建时间: ").append(news.getDate()).append("\n");
155+
}
156+
if (news.getLinkUrl() != null && !news.getLinkUrl().isEmpty()) {
157+
message.append("详情链接: ").append(news.getLinkUrl()).append("\n");
158+
}
159+
160+
napCatQQUtil.sendGroupMessage(defaultGroupId, message.toString());
161+
logger.info("已发送FF14水晶世界新闻: {}", news.getTitle());
162+
163+
Thread.sleep(1000);
164+
}
165+
166+
logger.info("成功发送 {} 条FF14水晶世界新闻到QQ群", newsList.size());
167+
168+
} catch (Exception e) {
169+
logger.error("发送FF14水晶世界新闻到QQ群失败", e);
170+
}
171+
}
172+
}

src/main/resources/application.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ napcat:
177177
access-token: ${NAPCAT_ACCESS_TOKEN:7saligia}
178178
default-group-id: ${NAPCAT_DEFAULT_GROUP_ID:595883141}
179179
phantom-group-id: ${NAPCAT_PHANTOM_GROUP_ID:787909466}
180+
crystal-group-id: ${NAPCAT_CRYSTAL_GROUP_ID:936746977}
180181
admin-qq: "944989026"
181182

182183
# DaoYu Key监控配置

0 commit comments

Comments
 (0)