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.
This commit is contained in:
parent
3cd47a1b4d
commit
4453dd6430
@ -2,7 +2,7 @@ import { test, expect } from 'bun:test';
|
|||||||
import { KaiqiuService } from '../src/services/KaiqiuService';
|
import { KaiqiuService } from '../src/services/KaiqiuService';
|
||||||
|
|
||||||
test('login success', async () => {
|
test('login success', async () => {
|
||||||
const { html, ...result } = await KaiqiuService.login('', '');
|
const { ...result } = await KaiqiuService.login('', '');
|
||||||
console.debug(result);
|
console.debug(result);
|
||||||
expect(result.cookies).toBeDefined();
|
expect(result.cookies).toBeDefined();
|
||||||
});
|
});
|
||||||
@ -6,7 +6,7 @@ test("Test find 东华 club", async () => {
|
|||||||
console.debug(result);
|
console.debug(result);
|
||||||
expect(result.clubs.length).toBeGreaterThan(0);
|
expect(result.clubs.length).toBeGreaterThan(0);
|
||||||
expect(result.total).toBeGreaterThan(result.clubs.length - 1);
|
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);
|
const club = await KaiqiuService.getClubInfo(clubId);
|
||||||
console.debug(club);
|
console.debug(club);
|
||||||
expect(club).toBeDefined();
|
expect(club).toBeDefined();
|
||||||
|
|||||||
@ -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);
|
|
||||||
});
|
|
||||||
@ -1,28 +1,5 @@
|
|||||||
import fs from 'fs';
|
|
||||||
import { expect, test } from 'bun:test';
|
import { expect, test } from 'bun:test';
|
||||||
import path from 'path';
|
import { sneckGroup } from '../src/utils/common';
|
||||||
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('');
|
|
||||||
});
|
|
||||||
|
|
||||||
test("group", () => {
|
test("group", () => {
|
||||||
const group = sneckGroup(48, 6);
|
const group = sneckGroup(48, 6);
|
||||||
|
|||||||
@ -1,19 +1,22 @@
|
|||||||
import { useLoaderData } from "react-router";
|
import { useLoaderData } from "react-router";
|
||||||
import { GamePanel } from "../components/GamePanel";
|
import { GamePanel } from "../components/GamePanel";
|
||||||
import type { BasePlayer, MatchInfo, XCXMember } from "../types";
|
import type { BasePlayer, MatchInfo, MatchSummary, XCXMember } from "../types";
|
||||||
import { Typography } from "antd";
|
import { Button, Typography } from "antd";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useTitle } from "ahooks";
|
import { useTitle } from "ahooks";
|
||||||
|
import { TeamOutlined } from "@ant-design/icons";
|
||||||
|
|
||||||
export default function EventPage() {
|
export default function EventPage() {
|
||||||
const {
|
const {
|
||||||
info: game,
|
info: game,
|
||||||
members,
|
members,
|
||||||
uidScore,
|
uidScore,
|
||||||
|
summary,
|
||||||
} = useLoaderData<{
|
} = useLoaderData<{
|
||||||
uidScore: Map<string, string>,
|
uidScore: Map<string, string>,
|
||||||
info: MatchInfo,
|
info: MatchInfo,
|
||||||
members: XCXMember[],
|
members: XCXMember[],
|
||||||
|
summary: MatchSummary,
|
||||||
}>();
|
}>();
|
||||||
const map = useMemo(() => Object.fromEntries(members.map(e => [e.uid, e])), [members]);
|
const map = useMemo(() => Object.fromEntries(members.map(e => [e.uid, e])), [members]);
|
||||||
const players = useMemo(() => {
|
const players = useMemo(() => {
|
||||||
@ -35,7 +38,8 @@ export default function EventPage() {
|
|||||||
useTitle(game.title, { restoreOnUnmount: true });
|
useTitle(game.title, { restoreOnUnmount: true });
|
||||||
return (
|
return (
|
||||||
<div className="app">
|
<div className="app">
|
||||||
<Typography.Title>{game.title}</Typography.Title>
|
<Typography.Title level={3}>{game.title}</Typography.Title>
|
||||||
|
<Button type="link" onClick={() => open(`/club/${summary.clubId}`)} icon={<TeamOutlined />}>{summary.clubName}</Button>
|
||||||
<GamePanel members={basePlayers} title={game.title} players={players} />
|
<GamePanel members={basePlayers} title={game.title} players={players} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { createBrowserRouter } from "react-router";
|
import { createBrowserRouter } from "react-router";
|
||||||
import ProfilePage from "./page/ProfilePage";
|
import ProfilePage from "./page/ProfilePage";
|
||||||
import EventPage from "./page/EventPage";
|
import EventPage from "./page/EventPage";
|
||||||
import type { MatchInfo, XCXMember } from "./types";
|
import type { MatchInfo, MatchSummary, XCXMember } from "./types";
|
||||||
import { FindUserPage } from "./page/FindUserPage";
|
import { FindUserPage } from "./page/FindUserPage";
|
||||||
import { FavePlayersPage } from "./page/FavPlayersPage";
|
import { FavePlayersPage } from "./page/FavPlayersPage";
|
||||||
import { UserCenter } from "./page/UserCenter";
|
import { UserCenter } from "./page/UserCenter";
|
||||||
@ -41,7 +41,12 @@ export const route = createBrowserRouter([
|
|||||||
{
|
{
|
||||||
path: '/event/:matchId',
|
path: '/event/:matchId',
|
||||||
loader: async ({ params }) => {
|
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
|
const members = info.itemId
|
||||||
? await (await fetch(`/api/match/${params.matchId}/${info.itemId}`)).json()
|
? await (await fetch(`/api/match/${params.matchId}/${info.itemId}`)).json()
|
||||||
: info.players.map((e, i) => ({
|
: info.players.map((e, i) => ({
|
||||||
@ -56,7 +61,7 @@ export const route = createBrowserRouter([
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ uids }),
|
body: JSON.stringify({ uids }),
|
||||||
}).then(res => res.json()).catch(() => ({}));
|
}).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,
|
Component: EventPage,
|
||||||
HydrateFallback: () => <HydrateFallback />
|
HydrateFallback: () => <HydrateFallback />
|
||||||
|
|||||||
@ -66,7 +66,7 @@ export class EventSubscribeService {
|
|||||||
});
|
});
|
||||||
const details: EventDetail[] = [];
|
const details: EventDetail[] = [];
|
||||||
for (const e of beforeEvents) {
|
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}`)
|
console.debug('Get match detail: %s - %s, url: %s', e.title, e.eventId, `https://tt.ksr.la/event/${e.eventId}`)
|
||||||
details.push(result);
|
details.push(result);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import * as cheerio from "cheerio";
|
import * as cheerio from "cheerio";
|
||||||
import { htmlRequestHeaders, redis } from "../utils/server";
|
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 dayjs from "dayjs";
|
||||||
import utc from 'dayjs/plugin/utc';
|
import utc from 'dayjs/plugin/utc';
|
||||||
import timezone from 'dayjs/plugin/timezone';
|
import timezone from 'dayjs/plugin/timezone';
|
||||||
@ -60,7 +60,7 @@ export class KaiqiuService {
|
|||||||
return { id: clubId, name, img, article, geo };
|
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 url = `https://kaiqiuwang.cc/home/space-0-do-mtag-tagid-${clubId}-view-event-page-${page}.html`;
|
||||||
const resp = await fetch(url, { headers: htmlRequestHeaders });
|
const resp = await fetch(url, { headers: htmlRequestHeaders });
|
||||||
return resp.text();
|
return resp.text();
|
||||||
@ -73,7 +73,7 @@ export class KaiqiuService {
|
|||||||
};
|
};
|
||||||
let html = await redis.get(`my-kaiqiuwang:events:${clubId}:${page}`);
|
let html = await redis.get(`my-kaiqiuwang:events:${clubId}:${page}`);
|
||||||
if (!html || html.includes('连接超时') || force) {
|
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);
|
await redis.setex(`my-kaiqiuwang:events:${clubId}:${page}`, 60 * 3, html);
|
||||||
}
|
}
|
||||||
const data = await this.#parseEventList(html);
|
const data = await this.#parseEventList(html);
|
||||||
@ -133,7 +133,7 @@ export class KaiqiuService {
|
|||||||
return { lng, lat };
|
return { lng, lat };
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async getEventInfo(eventId: string, force?: boolean) {
|
public static async getEventInfo(eventId: string, force?: boolean): Promise<MatchSummary> {
|
||||||
// https://kaiqiuwang.cc/home/space-event-id-175775.html
|
// https://kaiqiuwang.cc/home/space-event-id-175775.html
|
||||||
const key = `my-kaiqiuwang:event-info:${eventId}`;
|
const key = `my-kaiqiuwang:event-info:${eventId}`;
|
||||||
const eventURL = `${this.#baseURL}/home/space-event-id-${eventId}.html`;
|
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 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 nums = $('#content > div:nth-child(1) > div > div.event_content > ul > li:nth-child(2)').text();
|
||||||
const see = /(?<see>\d+)\b/.exec($('#content > div:nth-child(1) > div > div.event_content > ul > li:nth-child(1)').text())?.groups?.see ?? '';
|
const see = /(?<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 = /-(?<clubId>\d+).html$/.exec(clubLink)?.groups?.clubId ?? '';
|
||||||
const location = $('#content > div:nth-child(1) > div > div.event_content > dl > dd:nth-child(6) > a')
|
const location = $('#content > div:nth-child(1) > div > div.event_content > dl > dd:nth-child(6) > a')
|
||||||
.text().split(' ')?.[1]?.trim() ?? '';
|
.text().split(' ')?.[1]?.trim() ?? '';
|
||||||
const startTime = dayjs.tz(startDate || new Date(), 'Asia/Tokyo');
|
const startTime = dayjs.tz(startDate || new Date(), 'Asia/Tokyo');
|
||||||
@ -160,9 +164,12 @@ export class KaiqiuService {
|
|||||||
if (isFinished) {
|
if (isFinished) {
|
||||||
await redis.set(key, eventPage);
|
await redis.set(key, eventPage);
|
||||||
}
|
}
|
||||||
|
console.debug('clubId', clubId);
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
eventId,
|
eventId,
|
||||||
|
clubId,
|
||||||
|
clubName,
|
||||||
startDate,
|
startDate,
|
||||||
url: eventURL,
|
url: eventURL,
|
||||||
isProcessing,
|
isProcessing,
|
||||||
@ -177,16 +184,19 @@ export class KaiqiuService {
|
|||||||
const url = `${this.#baseURL}/home/space.php?do=event&id=${eventId}&view=member&status=2`;
|
const url = `${this.#baseURL}/home/space.php?do=event&id=${eventId}&view=member&status=2`;
|
||||||
const key = `my-kaiqiuwang:match-detail:${eventId}`;
|
const key = `my-kaiqiuwang:match-detail:${eventId}`;
|
||||||
let html = await redis.get(key) ?? '';
|
let html = await redis.get(key) ?? '';
|
||||||
|
const info = await this.getEventInfo(eventId);
|
||||||
if (!html || html.includes('连接超时') || force) {
|
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() || '');
|
||||||
const info = await this.getEventInfo(eventId)
|
|
||||||
if (info.isFinished) {
|
if (info.isFinished) {
|
||||||
await redis.set(key, html);
|
await redis.set(key, html);
|
||||||
} else {
|
} else {
|
||||||
await redis.setex(key, 60 * 5, html);
|
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 {
|
private static parseEventInfo(html: string, eventId: string): EventDetail {
|
||||||
|
|||||||
@ -22,6 +22,20 @@ export interface MatchInfo {
|
|||||||
players: Player[];
|
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 {
|
export interface BasePlayer {
|
||||||
uid: string;
|
uid: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user