From fe8542c687d2d4d86a63ad25d0079f7acbf7e0bd Mon Sep 17 00:00:00 2001 From: sunlei Date: Sun, 7 Jun 2026 03:07:59 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=94=B6=E5=8F=A3=20BangDream=20?= =?UTF-8?q?=E6=A1=A3=E7=BA=BF=E6=A6=9C=E8=B5=84=E6=BA=90=E4=BB=93=E5=82=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ot-bangdream-tsugu-global-refactor-plan.md | 1 + .../models/cutoff-event-top-repository.ts | 59 +++++++++++++++++++ .../tsugu/models/cutoff-event-top.ts | 48 ++++----------- .../render-blocks/list-player-ranking.ts | 11 ++-- .../player-ranking-resource-repository.ts | 32 ++++++++++ .../tsugu/cutoff-event-top-repository.spec.ts | 41 +++++++++++++ ...player-ranking-resource-repository.spec.ts | 36 +++++++++++ 7 files changed, 187 insertions(+), 41 deletions(-) create mode 100644 src/qqbot/plugins/bangDream/tsugu/models/cutoff-event-top-repository.ts create mode 100644 src/qqbot/plugins/bangDream/tsugu/render-blocks/player-ranking-resource-repository.ts create mode 100644 test/qqbot/plugins/bangDream/tsugu/cutoff-event-top-repository.spec.ts create mode 100644 test/qqbot/plugins/bangDream/tsugu/player-ranking-resource-repository.spec.ts diff --git a/docs/qqbot-bangdream-tsugu-global-refactor-plan.md b/docs/qqbot-bangdream-tsugu-global-refactor-plan.md index 599323f..4ae205d 100644 --- a/docs/qqbot-bangdream-tsugu-global-refactor-plan.md +++ b/docs/qqbot-bangdream-tsugu-global-refactor-plan.md @@ -309,6 +309,7 @@ export interface TsuguHook { - 已新增 `render-blocks/card-art-resource-repository.ts`,把 `card-art.ts` 的卡牌小图/插画边框资源下载从 `bestdoriUrl + downloadFileCache` 收口到 provider-backed repository,`card-art-spec.ts` 改为只生成 `/res/image/...` 相对资源路径;`card-art-resource-repository.spec.ts` 覆盖边框下载调用,本地 `/查卡 472` 图片 smoke 保持非空输出。 - 已新增 `models/costume-resource-repository.ts`,把 `Costume` 的服装详情请求和 Live2D 缩略图素材路径从 `bestdoriUrl + callAPIAndCacheResponse/downloadFile` 收口到 provider-backed repository;`costume-resource-repository.spec.ts` 覆盖详情路径、`sdchara.png` 路径和非内存缓存下载调用,本地 `/查卡 472` 图片 smoke 保持演出缩略图正常输出。 - 已新增 `models/item-resource-repository.ts`,把 `Item` 的道具缩略图路径从 `bestdoriUrl + downloadFileCache` 收口到 provider-backed repository;`item-resource-repository.spec.ts` 覆盖 material、star 和 common item 三类路径,本地 `/查卡池 259` 图片 smoke 输出非空。 +- 已新增 `models/cutoff-event-top-repository.ts` 和 `render-blocks/player-ranking-resource-repository.ts`,把前十榜 eventtop 数据请求与排名徽章素材路径从 `bestdoriUrl + callAPIAndCacheResponse/downloadFileCache` 收口到 provider-backed repository;相关 spec 覆盖 eventtop query path、排名徽章路径和 provider 调用,本地 `/ycx 10 100 cn` 图片 smoke 输出非空。 - 固化:Windows 下不要用反斜杠测试路径直接调用 Jest pattern,容易出现 `Pattern ... - 0 matches`;指定文件测试统一使用 `pnpm exec jest --runInBand --runTestsByPath test/qqbot/plugins/bangDream/tsugu/.spec.ts ...`,路径用正斜杠。 ### Phase 4:搜索 specification 和 matcher diff --git a/src/qqbot/plugins/bangDream/tsugu/models/cutoff-event-top-repository.ts b/src/qqbot/plugins/bangDream/tsugu/models/cutoff-event-top-repository.ts new file mode 100644 index 0000000..78abcf3 --- /dev/null +++ b/src/qqbot/plugins/bangDream/tsugu/models/cutoff-event-top-repository.ts @@ -0,0 +1,59 @@ +import { bangDreamBestdoriProvider } from '@/qqbot/plugins/bangDream/tsugu/data-clients/bestdori-provider'; +import type { BangDreamDataProvider } from '@/qqbot/plugins/bangDream/tsugu/data-clients/data-provider'; +import { Server } from '@/qqbot/plugins/bangDream/tsugu/models/server'; + +export interface CutoffEventTopPoint { + time: number; + uid: number; + value: number; +} + +export interface CutoffEventTopUser { + currentPt: number; + degrees: number[]; + introduction: string; + name: string; + rank: number; + ranking: number; + sid: number; + strained: number; + uid: number; +} + +export interface CutoffEventTopData { + points: CutoffEventTopPoint[]; + users: CutoffEventTopUser[]; +} + +export class CutoffEventTopRepository { + constructor( + private readonly provider: BangDreamDataProvider = bangDreamBestdoriProvider, + ) {} + + /** + * 获取活动前十榜数据路径。 + * + * @param eventId - 活动 ID。 + * @param server - 目标服务器。 + */ + getTopDataPath(eventId: number, server: Server): string { + return `/api/eventtop/data?server=${server}&event=${eventId}&mid=0&interval=3600000`; + } + + /** + * 获取活动前十榜数据。 + * + * @param eventId - 活动 ID。 + * @param server - 目标服务器。 + */ + async getTopData( + eventId: number, + server: Server, + ): Promise { + return await this.provider.getJson( + this.getTopDataPath(eventId, server), + ); + } +} + +export const cutoffEventTopRepository = new CutoffEventTopRepository(); diff --git a/src/qqbot/plugins/bangDream/tsugu/models/cutoff-event-top.ts b/src/qqbot/plugins/bangDream/tsugu/models/cutoff-event-top.ts index 2971b7a..21f8bad 100644 --- a/src/qqbot/plugins/bangDream/tsugu/models/cutoff-event-top.ts +++ b/src/qqbot/plugins/bangDream/tsugu/models/cutoff-event-top.ts @@ -1,8 +1,11 @@ import { Server } from '@/qqbot/plugins/bangDream/tsugu/models/server'; import { Event } from '@/qqbot/plugins/bangDream/tsugu/models/event'; -import { callAPIAndCacheResponse } from '@/qqbot/plugins/bangDream/tsugu/data-clients/api-cache-client'; -import { bestdoriUrl } from '@/qqbot/plugins/bangDream/tsugu/runtime/config'; import { BangDreamEventStatus } from '@/qqbot/plugins/bangDream/tsugu/models/bangdream-constants'; +import { + cutoffEventTopRepository, + type CutoffEventTopPoint, + type CutoffEventTopUser, +} from '@/qqbot/plugins/bangDream/tsugu/models/cutoff-event-top-repository'; export class CutoffEventTop { eventId: number; @@ -12,22 +15,8 @@ export class CutoffEventTop { status: BangDreamEventStatus; isInitfull: boolean = false; isExist = false; - points: { - time: number; - uid: number; - value: number; - }[]; - users: { - uid: number; - name: string; - introduction: string; - rank: number; - sid: number; - strained: number; - degrees: number[]; - ranking: number; - currentPt: number; - }[]; + points: CutoffEventTopPoint[]; + users: CutoffEventTopUser[]; /** * 构造 CutoffEventTop 实例,并初始化该模型的本地基础字段。 * @@ -64,30 +53,17 @@ export class CutoffEventTop { if (this.isInitfull) { return; } - const topData = await callAPIAndCacheResponse( - `${bestdoriUrl}/api/eventtop/data?server=${this.server}&event=${this.eventId}&mid=0&interval=3600000`, + const topData = await cutoffEventTopRepository.getTopData( + this.eventId, + this.server, ); if (topData == undefined) { this.isExist = false; return; } this.isExist = true; - this.points = topData['points'] as { - time: number; - uid: number; - value: number; - }[]; - this.users = topData['users'] as { - uid: number; - name: string; - introduction: string; - rank: number; - sid: number; - strained: number; - degrees: number[]; - ranking: number; - currentPt: number; - }[]; + this.points = topData.points; + this.users = topData.users; if (this.points.length == 0 || this.users.length == 0) { //如果没有数据,返回不存在 this.isExist = false; diff --git a/src/qqbot/plugins/bangDream/tsugu/render-blocks/list-player-ranking.ts b/src/qqbot/plugins/bangDream/tsugu/render-blocks/list-player-ranking.ts index 893e544..2eb0673 100644 --- a/src/qqbot/plugins/bangDream/tsugu/render-blocks/list-player-ranking.ts +++ b/src/qqbot/plugins/bangDream/tsugu/render-blocks/list-player-ranking.ts @@ -6,8 +6,7 @@ import { drawDegree } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/degree import { Server } from '@/qqbot/plugins/bangDream/tsugu/models/server'; import { Degree } from '@/qqbot/plugins/bangDream/tsugu/models/degree'; import { drawText } from '@/qqbot/plugins/bangDream/tsugu/canvas/text'; -import { downloadFileCache } from '@/qqbot/plugins/bangDream/tsugu/data-clients/asset-cache-client'; -import { bestdoriUrl } from '@/qqbot/plugins/bangDream/tsugu/runtime/config'; +import { playerRankingResourceRepository } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/player-ranking-resource-repository'; interface User { uid: number; @@ -55,9 +54,11 @@ export async function drawPlayerRankingInList( if (user.ranking == undefined) { return; } else if (user.ranking > 0 && user.ranking <= 3) { - const rankIamgeBuffer = await downloadFileCache( - `${bestdoriUrl}/res/image/${Server[server]}_${user.ranking}.png`, - ); + const rankIamgeBuffer = + await playerRankingResourceRepository.getRankImageBuffer( + server, + user.ranking, + ); rankingImage = await loadImage(rankIamgeBuffer); ctx.drawImage(rankingImage, 12, 45, 45, 21); } else { diff --git a/src/qqbot/plugins/bangDream/tsugu/render-blocks/player-ranking-resource-repository.ts b/src/qqbot/plugins/bangDream/tsugu/render-blocks/player-ranking-resource-repository.ts new file mode 100644 index 0000000..d592152 --- /dev/null +++ b/src/qqbot/plugins/bangDream/tsugu/render-blocks/player-ranking-resource-repository.ts @@ -0,0 +1,32 @@ +import { bangDreamBestdoriProvider } from '@/qqbot/plugins/bangDream/tsugu/data-clients/bestdori-provider'; +import type { BangDreamDataProvider } from '@/qqbot/plugins/bangDream/tsugu/data-clients/data-provider'; +import { Server } from '@/qqbot/plugins/bangDream/tsugu/models/server'; + +export class PlayerRankingResourceRepository { + constructor( + private readonly provider: BangDreamDataProvider = bangDreamBestdoriProvider, + ) {} + + /** + * 获取玩家排名徽章资源路径。 + * + * @param server - 目标服务器。 + * @param ranking - 排名。 + */ + getRankImagePath(server: Server, ranking: number): string { + return `/res/image/${Server[server]}_${ranking}.png`; + } + + /** + * 下载玩家排名徽章资源。 + * + * @param server - 目标服务器。 + * @param ranking - 排名。 + */ + async getRankImageBuffer(server: Server, ranking: number): Promise { + return await this.provider.getAsset(this.getRankImagePath(server, ranking)); + } +} + +export const playerRankingResourceRepository = + new PlayerRankingResourceRepository(); diff --git a/test/qqbot/plugins/bangDream/tsugu/cutoff-event-top-repository.spec.ts b/test/qqbot/plugins/bangDream/tsugu/cutoff-event-top-repository.spec.ts new file mode 100644 index 0000000..5d07472 --- /dev/null +++ b/test/qqbot/plugins/bangDream/tsugu/cutoff-event-top-repository.spec.ts @@ -0,0 +1,41 @@ +import type { BangDreamDataProvider } from '@/qqbot/plugins/bangDream/tsugu/data-clients/data-provider'; +import { CutoffEventTopRepository } from '@/qqbot/plugins/bangDream/tsugu/models/cutoff-event-top-repository'; +import { Server } from '@/qqbot/plugins/bangDream/tsugu/models/server'; + +function createProviderMock(): jest.Mocked { + return { + getAsset: jest.fn(), + getJson: jest.fn(), + getTracker: jest.fn(), + name: 'MockBestdori', + resolveUrl: jest.fn((pathOrUrl) => `https://bestdori.example${pathOrUrl}`), + }; +} + +describe('BangDream cutoff event top repository', () => { + it('builds event top data paths', () => { + const repository = new CutoffEventTopRepository(createProviderMock()); + + expect(repository.getTopDataPath(50, Server.cn)).toBe( + '/api/eventtop/data?server=3&event=50&mid=0&interval=3600000', + ); + }); + + it('routes event top data requests through the provider', async () => { + const provider = createProviderMock(); + provider.getJson.mockResolvedValue({ + points: [{ time: 1, uid: 2, value: 3 }], + users: [], + }); + const repository = new CutoffEventTopRepository(provider); + + await expect(repository.getTopData(50, Server.cn)).resolves.toEqual({ + points: [{ time: 1, uid: 2, value: 3 }], + users: [], + }); + + expect(provider.getJson).toHaveBeenCalledWith( + '/api/eventtop/data?server=3&event=50&mid=0&interval=3600000', + ); + }); +}); diff --git a/test/qqbot/plugins/bangDream/tsugu/player-ranking-resource-repository.spec.ts b/test/qqbot/plugins/bangDream/tsugu/player-ranking-resource-repository.spec.ts new file mode 100644 index 0000000..2d4102b --- /dev/null +++ b/test/qqbot/plugins/bangDream/tsugu/player-ranking-resource-repository.spec.ts @@ -0,0 +1,36 @@ +import type { BangDreamDataProvider } from '@/qqbot/plugins/bangDream/tsugu/data-clients/data-provider'; +import { Server } from '@/qqbot/plugins/bangDream/tsugu/models/server'; +import { PlayerRankingResourceRepository } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/player-ranking-resource-repository'; + +function createProviderMock(): jest.Mocked { + return { + getAsset: jest.fn(), + getJson: jest.fn(), + getTracker: jest.fn(), + name: 'MockBestdori', + resolveUrl: jest.fn((pathOrUrl) => `https://bestdori.example${pathOrUrl}`), + }; +} + +describe('BangDream player ranking resource repository', () => { + it('builds player ranking image paths', () => { + const repository = new PlayerRankingResourceRepository(createProviderMock()); + + expect(repository.getRankImagePath(Server.cn, 1)).toBe( + '/res/image/cn_1.png', + ); + }); + + it('downloads player ranking images through the provider', async () => { + const provider = createProviderMock(); + const rankBuffer = Buffer.from('rank'); + provider.getAsset.mockResolvedValue(rankBuffer); + const repository = new PlayerRankingResourceRepository(provider); + + await expect(repository.getRankImageBuffer(Server.cn, 1)).resolves.toBe( + rankBuffer, + ); + + expect(provider.getAsset).toHaveBeenCalledWith('/res/image/cn_1.png'); + }); +});