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) => { export const ClubSummary = (props: Props) => {
const [isArticleOpen, setIsArticleOpen] = useState(false); const [isArticleOpen, setIsArticleOpen] = useState(false);
const requestClubSummary = useRequest<ClubDetail, []>(async () => { 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 }) }, { manual: false, refreshDeps: [props.clubId], debounceWait: 300 })
const info = useMemo(() => requestClubSummary.data, [requestClubSummary]); const info = useMemo(() => requestClubSummary.data, [requestClubSummary]);
const noArticle = !info?.article || info.article === '还没有公告'; const noArticle = !info?.article || info.article === '还没有公告';
@ -43,6 +43,7 @@ export const ClubSummary = (props: Props) => {
}, },
] ]
}, [info]); }, [info]);
if (requestClubSummary.data === null) return null;
return ( return (
<div style={{ width: '100%' }}> <div style={{ width: '100%' }}>
{requestClubSummary.loading ? ( {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 ics from 'ics';
import index from "./index.html"; import index from "./index.html";
import { getUidScore } from "./services/uidScoreStore"; import { getUidScore } from "./services/uidScoreStore";
@ -159,7 +159,7 @@ const server = Bun.serve({
}, },
"/api/match/:matchId": { "/api/match/:matchId": {
async GET(req) { async GET(req) {
const data = await getMatchInfo(req.params.matchId); const data = await KaiqiuService.getMatchDetail(req.params.matchId);
return Response.json(data); return Response.json(data);
} }
}, },

View File

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