From 4453dd64302adbd8f195c5baec8e9dee6538edd7 Mon Sep 17 00:00:00 2001 From: kyuuseiryuu Date: Wed, 25 Mar 2026 23:20:33 +0900 Subject: [PATCH] refactor(service, routes, types): extract match summary and link to club - Extract club information (clubId, clubName) from event page in `getEventInfo`. - Update `getMatchDetail` to return both `detail` and `summary`. - Introduce `MatchSummary` interface in `src/types/index.ts`. - Update `EventPage` to display the club name and link to the club page. - Adjust `EventSubscribeService` and loaders to handle the new return structure. - Clean up test files and mock data loading logic. --- __test__/kaiqiu-login.test.ts | 2 +- __test__/kaiqiu.test.ts | 2 +- __test__/utils.load-html.test.ts | 18 ------------------ __test__/utils.test.ts | 25 +------------------------ src/page/EventPage.tsx | 10 +++++++--- src/routes.tsx | 11 ++++++++--- src/services/EventSubscribeService.ts | 2 +- src/services/KaiqiuService.ts | 22 ++++++++++++++++------ src/types/index.ts | 14 ++++++++++++++ 9 files changed, 49 insertions(+), 57 deletions(-) delete mode 100644 __test__/utils.load-html.test.ts diff --git a/__test__/kaiqiu-login.test.ts b/__test__/kaiqiu-login.test.ts index 5fc3d70..a9f181b 100644 --- a/__test__/kaiqiu-login.test.ts +++ b/__test__/kaiqiu-login.test.ts @@ -2,7 +2,7 @@ import { test, expect } from 'bun:test'; import { KaiqiuService } from '../src/services/KaiqiuService'; test('login success', async () => { - const { html, ...result } = await KaiqiuService.login('', ''); + const { ...result } = await KaiqiuService.login('', ''); console.debug(result); expect(result.cookies).toBeDefined(); }); \ No newline at end of file diff --git a/__test__/kaiqiu.test.ts b/__test__/kaiqiu.test.ts index 4664d69..1d5b0b2 100644 --- a/__test__/kaiqiu.test.ts +++ b/__test__/kaiqiu.test.ts @@ -6,7 +6,7 @@ test("Test find 东华 club", async () => { console.debug(result); expect(result.clubs.length).toBeGreaterThan(0); expect(result.total).toBeGreaterThan(result.clubs.length - 1); - const clubId = result.clubs[0]?.clubId ?? ''; + const clubId = result.clubs[0]?.id ?? ''; const club = await KaiqiuService.getClubInfo(clubId); console.debug(club); expect(club).toBeDefined(); diff --git a/__test__/utils.load-html.test.ts b/__test__/utils.load-html.test.ts deleted file mode 100644 index 3f0309f..0000000 --- a/__test__/utils.load-html.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { expect, test } from 'bun:test'; -import path from 'path'; -import fs from 'fs'; -import { fetchEventContentHTML, fetchEventListHTML } from '../src/utils/'; - -test('load html', async () => { - const saveTo = path.resolve(__dirname, 'data', 'view-event.html'); - const html = await fetchEventListHTML(`47`); - fs.writeFileSync(saveTo, html?? ''); - expect(html?.length).not.toBe(0); -}); - -test('load html', async () => { - const saveTo = path.resolve(__dirname, 'data', 'view-event-content.html'); - const html = await fetchEventContentHTML('167684'); - fs.writeFileSync(saveTo, html?? ''); - expect(html?.length).not.toBe(0); -}); \ No newline at end of file diff --git a/__test__/utils.test.ts b/__test__/utils.test.ts index c9a40d0..2915851 100644 --- a/__test__/utils.test.ts +++ b/__test__/utils.test.ts @@ -1,28 +1,5 @@ -import fs from 'fs'; import { expect, test } from 'bun:test'; -import path from 'path'; -import { parseEventInfo, parseEventList, sneckGroup } from '../src/utils/'; - - -const matchId = '167684'; -const item_id = '7098063'; -test('event list not empty', () => { - const html = fs.readFileSync(path.resolve(__dirname, 'data', 'view-event.html')).toString(); - const list = parseEventList(html); - expect(list.length).toBe(10); - console.log(list[0]?.matchId); -}); - -test('event content not empty', () => { - const html = fs.readFileSync( - path.resolve(__dirname, 'data', 'view-event-content.html') - ).toString(); - const { itemId, players } = parseEventInfo(html); - expect(itemId).toBe(item_id); - expect(players.length).toBeGreaterThan(0); - console.log(players); - expect(players[0]?.uid).not.toBe(''); -}); +import { sneckGroup } from '../src/utils/common'; test("group", () => { const group = sneckGroup(48, 6); diff --git a/src/page/EventPage.tsx b/src/page/EventPage.tsx index 8d64da6..91a4b6f 100644 --- a/src/page/EventPage.tsx +++ b/src/page/EventPage.tsx @@ -1,19 +1,22 @@ import { useLoaderData } from "react-router"; import { GamePanel } from "../components/GamePanel"; -import type { BasePlayer, MatchInfo, XCXMember } from "../types"; -import { Typography } from "antd"; +import type { BasePlayer, MatchInfo, MatchSummary, XCXMember } from "../types"; +import { Button, Typography } from "antd"; import { useMemo } from "react"; import { useTitle } from "ahooks"; +import { TeamOutlined } from "@ant-design/icons"; export default function EventPage() { const { info: game, members, uidScore, + summary, } = useLoaderData<{ uidScore: Map, info: MatchInfo, members: XCXMember[], + summary: MatchSummary, }>(); const map = useMemo(() => Object.fromEntries(members.map(e => [e.uid, e])), [members]); const players = useMemo(() => { @@ -35,7 +38,8 @@ export default function EventPage() { useTitle(game.title, { restoreOnUnmount: true }); return (
- {game.title} + {game.title} +
); diff --git a/src/routes.tsx b/src/routes.tsx index afefee2..4409279 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -1,7 +1,7 @@ import { createBrowserRouter } from "react-router"; import ProfilePage from "./page/ProfilePage"; import EventPage from "./page/EventPage"; -import type { MatchInfo, XCXMember } from "./types"; +import type { MatchInfo, MatchSummary, XCXMember } from "./types"; import { FindUserPage } from "./page/FindUserPage"; import { FavePlayersPage } from "./page/FavPlayersPage"; import { UserCenter } from "./page/UserCenter"; @@ -41,7 +41,12 @@ export const route = createBrowserRouter([ { path: '/event/:matchId', loader: async ({ params }) => { - const info: MatchInfo = await (await fetch(`/api/match/${params.matchId}`)).json(); + const { detail: info, summary } = await fetch(`/api/match/${params.matchId}`) + .then(res => res.json()) + .then(data => data as { + detail: MatchInfo, + summary: MatchSummary, + }); const members = info.itemId ? await (await fetch(`/api/match/${params.matchId}/${info.itemId}`)).json() : info.players.map((e, i) => ({ @@ -56,7 +61,7 @@ export const route = createBrowserRouter([ method: "POST", body: JSON.stringify({ uids }), }).then(res => res.json()).catch(() => ({})); - return { info, members, uidScore: new Map(Object.entries(uidScore)) }; + return { info, members, uidScore: new Map(Object.entries(uidScore)), summary }; }, Component: EventPage, HydrateFallback: () => diff --git a/src/services/EventSubscribeService.ts b/src/services/EventSubscribeService.ts index 23bfb5a..664c130 100644 --- a/src/services/EventSubscribeService.ts +++ b/src/services/EventSubscribeService.ts @@ -66,7 +66,7 @@ export class EventSubscribeService { }); const details: EventDetail[] = []; for (const e of beforeEvents) { - const result = await KaiqiuService.getMatchDetail(e.eventId, true) + const { detail: result } = await KaiqiuService.getMatchDetail(e.eventId, true) console.debug('Get match detail: %s - %s, url: %s', e.title, e.eventId, `https://tt.ksr.la/event/${e.eventId}`) details.push(result); } diff --git a/src/services/KaiqiuService.ts b/src/services/KaiqiuService.ts index 786e7d2..25f669e 100644 --- a/src/services/KaiqiuService.ts +++ b/src/services/KaiqiuService.ts @@ -1,6 +1,6 @@ import * as cheerio from "cheerio"; import { htmlRequestHeaders, redis } from "../utils/server"; -import type { ClubInfo, EventDetail, IEventInfo, Player } from "../types"; +import type { ClubInfo, EventDetail, IEventInfo, MatchSummary, Player } from "../types"; import dayjs from "dayjs"; import utc from 'dayjs/plugin/utc'; import timezone from 'dayjs/plugin/timezone'; @@ -60,7 +60,7 @@ export class KaiqiuService { return { id: clubId, name, img, article, geo }; } - static async #fetchEventListHTML(clubId: string, page: number) { + static async fetchEventListHTML(clubId: string, page: number) { const url = `https://kaiqiuwang.cc/home/space-0-do-mtag-tagid-${clubId}-view-event-page-${page}.html`; const resp = await fetch(url, { headers: htmlRequestHeaders }); return resp.text(); @@ -73,7 +73,7 @@ export class KaiqiuService { }; let html = await redis.get(`my-kaiqiuwang:events:${clubId}:${page}`); if (!html || html.includes('连接超时') || force) { - html = await this.#fetchEventListHTML(clubId, page); + html = await this.fetchEventListHTML(clubId, page); await redis.setex(`my-kaiqiuwang:events:${clubId}:${page}`, 60 * 3, html); } const data = await this.#parseEventList(html); @@ -133,7 +133,7 @@ export class KaiqiuService { return { lng, lat }; } - public static async getEventInfo(eventId: string, force?: boolean) { + public static async getEventInfo(eventId: string, force?: boolean): Promise { // https://kaiqiuwang.cc/home/space-event-id-175775.html const key = `my-kaiqiuwang:event-info:${eventId}`; const eventURL = `${this.#baseURL}/home/space-event-id-${eventId}.html`; @@ -150,6 +150,10 @@ export class KaiqiuService { const title = $('#mainarea > h2 > a:nth-child(3)').text().trim(); const nums = $('#content > div:nth-child(1) > div > div.event_content > ul > li:nth-child(2)').text(); const see = /(?\d+)\b/.exec($('#content > div:nth-child(1) > div > div.event_content > ul > li:nth-child(1)').text())?.groups?.see ?? ''; + const club = $('#sidebar .sidebox > p > a'); + const clubLink = club.attr('href') ?? ''; + const clubName = club.text() ?? ''; + const clubId = /-(?\d+).html$/.exec(clubLink)?.groups?.clubId ?? ''; const location = $('#content > div:nth-child(1) > div > div.event_content > dl > dd:nth-child(6) > a') .text().split(' ')?.[1]?.trim() ?? ''; const startTime = dayjs.tz(startDate || new Date(), 'Asia/Tokyo'); @@ -160,9 +164,12 @@ export class KaiqiuService { if (isFinished) { await redis.set(key, eventPage); } + console.debug('clubId', clubId); return { title, eventId, + clubId, + clubName, startDate, url: eventURL, isProcessing, @@ -177,16 +184,19 @@ export class KaiqiuService { const url = `${this.#baseURL}/home/space.php?do=event&id=${eventId}&view=member&status=2`; const key = `my-kaiqiuwang:match-detail:${eventId}`; let html = await redis.get(key) ?? ''; + const info = await this.getEventInfo(eventId); if (!html || html.includes('连接超时') || force) { html = await fetch(url, { headers: htmlRequestHeaders }).then(res => res.text() || ''); - const info = await this.getEventInfo(eventId) if (info.isFinished) { await redis.set(key, html); } else { await redis.setex(key, 60 * 5, html); } } - return this.parseEventInfo(html, eventId); + return { + detail: this.parseEventInfo(html, eventId), + summary: info, + }; } private static parseEventInfo(html: string, eventId: string): EventDetail { diff --git a/src/types/index.ts b/src/types/index.ts index 53f6d31..4564eaa 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -22,6 +22,20 @@ export interface MatchInfo { players: Player[]; } +export interface MatchSummary { + title: string; + eventId: string; + clubName: string; + clubId: string; + startDate: string; + url: string; + isProcessing: boolean; + isFinished: boolean; + location: string; + nums: string; + see: string; +} + export interface BasePlayer { uid: string; name: string;