feat(api & services): add error handling for club summary and refresh match details

- Add `.catch(() => null)` in `ClubSummary.tsx` to gracefully handle failed API requests and prevent UI crashes.
- Return `null` from `ClubSummary` when the club data is `null` to skip rendering during errors.
- Extract `getMatchInfo` logic into a new static method `getMatchDetail` within `KaiqiuService` for better separation of concerns.
- Update `getClubLocation` and `getEventInfo` to accept an optional `force` parameter, allowing cache bypass when network errors (like '连接超时') occur.
- Remove the old `getMatchInfo` utility function from `server.ts` and update the `/api/match` route to use `KaiqiuService.getMatchDetail`.
This commit is contained in:
kyuuseiryuu 2026-03-23 14:33:12 +09:00
parent 76b68c0ea6
commit 0c82384fd5
4 changed files with 20 additions and 18 deletions

View File

@ -14,7 +14,7 @@ interface Props {
export const ClubSummary = (props: Props) => {
const [isArticleOpen, setIsArticleOpen] = useState(false);
const requestClubSummary = useRequest<ClubDetail, []>(async () => {
return fetch(`/api/club/${props.clubId}`).then(r => r.json());
return fetch(`/api/club/${props.clubId}`).then(r => r.json()).catch(() => null);
}, { manual: false, refreshDeps: [props.clubId], debounceWait: 300 })
const info = useMemo(() => requestClubSummary.data, [requestClubSummary]);
const noArticle = !info?.article || info.article === '还没有公告';
@ -43,6 +43,7 @@ export const ClubSummary = (props: Props) => {
},
]
}, [info]);
if (requestClubSummary.data === null) return null;
return (
<div style={{ width: '100%' }}>
{requestClubSummary.loading ? (

View File

@ -1,4 +1,4 @@
import { getMatchInfo, verifyLogtoToken, xcxApi } from "./utils/server";
import { verifyLogtoToken, xcxApi } from "./utils/server";
import ics from 'ics';
import index from "./index.html";
import { getUidScore } from "./services/uidScoreStore";
@ -159,7 +159,7 @@ const server = Bun.serve({
},
"/api/match/:matchId": {
async GET(req) {
const data = await getMatchInfo(req.params.matchId);
const data = await KaiqiuService.getMatchDetail(req.params.matchId);
return Response.json(data);
}
},

View File

@ -1,5 +1,5 @@
import * as cheerio from "cheerio";
import { htmlRequestHeaders, redis, REDIS_CACHE_HOUR } from "../utils/server";
import { htmlRequestHeaders, parseEventInfo, redis, REDIS_CACHE_HOUR } from "../utils/server";
import type { ClubInfo, IEventInfo } from "../types";
import dayjs from "dayjs";
import utc from 'dayjs/plugin/utc';
@ -112,11 +112,11 @@ export class KaiqiuService {
};
}
static async #getClubLocation(clubId: string) {
static async #getClubLocation(clubId: string, force?: boolean) {
const url = `${this.#baseURL}/home/space-mtag-tagid-${clubId}-view-map.html`;
const key = `my-kaiqiuwang:location:${clubId}`;
let html: string = await redis.get(key) || '';
if (!html) {
if (!html || html.includes('连接超时') || force) {
html = await fetch(url, { headers: htmlRequestHeaders }).then(res => res.text());
redis.setex(key, 60 * 60 * 24, html);
}
@ -127,12 +127,12 @@ export class KaiqiuService {
return { lng, lat };
}
public static async getEventInfo(eventId: string) {
public static async getEventInfo(eventId: string, force?: boolean) {
const key = `my-kaiqiuwang:match:${eventId}`;
let eventPage = await redis.get(key) ?? '';
// https://kaiqiuwang.cc/home/space-event-id-175775.html
const eventURL = `${this.#baseURL}/home/space-event-id-${eventId}.html`;
if (!eventPage) {
if (!eventPage || eventPage.includes('连接超时') || force) {
eventPage = await fetch(eventURL, { headers: htmlRequestHeaders }).then(res => res.text() ?? '');
await redis.setex(key, 60 * 60 * 10, eventPage)
}
@ -159,6 +159,17 @@ export class KaiqiuService {
}
}
public static async getMatchDetail(eventId: string, force?: boolean) {
const key = `my-kaiqiuwang:match-member:${eventId}`;
let html = await redis.get(key).catch(() => '') || '';
if (!html || force) {
const url = `${this.#baseURL}/home/space.php?do=event&id=${eventId}&view=member&status=2`;
const html = await fetch(url, { headers: htmlRequestHeaders }).then(res => res.text() || '');
redis.setex(key, 60 * 60 * REDIS_CACHE_HOUR, html);
}
return parseEventInfo(html);
}
public static async login(username: string, password: string) {
const loginPageRes = await fetch('https://kaiqiuwang.cc/home/do.php?ac=668g&&ref');
const cookies = loginPageRes.headers.getSetCookie().map(c => c.split(';')[0]).join('; ');

View File

@ -75,16 +75,6 @@ export function parseEventInfo(html: string) {
}
}
export async function getMatchInfo(matchId: string) {
const key = `my-kaiqiuwang:match-member:${matchId}`;
let html = await redis.get(key).catch(() => '');
if (!html) {
html = await fetchEventContentHTML(matchId);
redis.setex(key, 60 * 60 * REDIS_CACHE_HOUR, html);
}
return parseEventInfo(html);
}
export const extractBearerTokenFromHeaders = (authorization: string | null) => {
if (!authorization) {
return null;