my-kaiqiuwang/src/utils/server.ts
kyuuseiryuu 1fae8206f2 refactor(redis): centralize Redis client and add HTML cache for event endpoints
- export shared redis client from utils/server
- reuse shared redis in uidScoreStore
- cache listEvent HTML for 30 minutes
- cache getMatchInfo HTML for 3 minutes
2026-02-26 20:42:18 +09:00

114 lines
3.7 KiB
TypeScript

import type { IEventInfo, Player } from "../types";
import * as cheerio from "cheerio";
import { XCXAPI } from "../services/xcxApi";
import { BASE_URL } from "./common";
import { RedisClient } from "bun";
export const xcxApi = new XCXAPI(process.env.KAIQIUCC_TOKEN ?? '');
export const redis = new RedisClient('redis://default:redis_8YnmBw@192.168.50.126:6379');
const htmlRequestHeaders = {
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
"accept-language": "zh-CN,zh;q=0.8",
"cache-control": "max-age=0",
"priority": "u=0, i",
"sec-ch-ua": "\"Not(A:Brand\";v=\"8\", \"Chromium\";v=\"144\", \"Brave\";v=\"144\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"Windows\"",
"sec-fetch-dest": "document",
"sec-fetch-mode": "navigate",
"sec-fetch-site": "none",
"sec-fetch-user": "?1",
"sec-gpc": "1",
"upgrade-insecure-requests": "1",
"cookie": "SECKEY_ABVK=oTGgqH4ypGPFVdQ3J9K7PoAOPdZ+8R7CsUzi75gelcg%3D; uchome_sendmail=1"
}
/**
* @param tagid 俱乐部 ID
*/
export async function listEvent(tagid: string): Promise<IEventInfo[]> {
const key = `my-kaiqiuwang:evnet:${tagid}`;
let html = await redis.get(key);
if (!html) {
html = await fetchEventListHTML(tagid);
await redis.setex(key, 60 * 30, html);
}
return parseEventList(html);
}
/**
* @param tagid 俱乐部 ID
* @return HTML
*/
export async function fetchEventListHTML(tagid: string) {
const url = `${BASE_URL}/home/space.php?do=mtag&tagid=${tagid}&view=event`;
const resp = await fetch(url, { headers: htmlRequestHeaders });
return resp.text() ?? '';
}
export function parseEventList(html: string) {
const $ = cheerio.load(html);
const blockList = $('div.event_list > ol > li');
const list: IEventInfo[] = [];
for (const block of blockList) {
const titleEl = $(block).find('.event_title');
const title = titleEl.text();
const url = $(titleEl).find('a').attr('href') ?? '';
const startDate = $(block).find('ul li:nth-of-type(1)').text().replace('比赛开始: \\t', '').trim();
const place = $(block).find('ul li:nth-of-type(2)').text().replace('比赛地点: \\t', '').trim();
const event: IEventInfo = {
title,
info: [startDate, place],
url: `${BASE_URL}/home/${url}`,
matchId: /\S+-(\d+).html$/.exec(url)?.[1] ?? '',
}
list.push(event);
}
return list;
}
/**
*
* @param matchId 比赛 ID
* @returns HTML
*/
export async function fetchEventContentHTML(matchId: string) {
const url = `${BASE_URL}/home/space.php?do=event&id=${matchId}&view=member&status=2`;
const resp = await fetch(url, { headers: htmlRequestHeaders });
return resp.text() ?? ''
}
export function parseEventInfo(html: string) {
const $ = cheerio.load(html);
const title = $('h2.title a').text();
const itemHref = $('.sub_menu a.active').attr('href') ?? '';
const itemId = /\S+item_id=(\d+)$/.exec(itemHref)?.[1] ?? '';
const players: Player[] = [];
const playersEl = $('.thumb');
for (const player of playersEl) {
const img = $(player).find('.image img').attr('src') ?? '';
const name = $(player).find('h6').text().trim();
const uid = /space-(?<uid>\d+).html/.exec($(player).find('h6 a').attr('href') ?? '')?.groups?.uid ?? '';
const info = $(player).find('p:nth-of-type(2)').text().replace(/\s/g, '');
const score = /^.*?\b(\d+)\b/.exec(info)?.[1] ?? '';
players.push({ name, avatar: img, score, info, uid });
}
return {
itemId,
title,
players,
}
}
export async function getMatchInfo(matchId: string) {
const key = `my-kaiqiuwang:matchinfo:${matchId}`;
let html = await redis.get(key);
if (!html) {
html = await fetchEventContentHTML(matchId);
await redis.setex(key, 60 * 3, html);
}
return parseEventInfo(html);
}