From 00b6df9471ab303caaee6c590a34191800d5881c Mon Sep 17 00:00:00 2001 From: sunlei Date: Sun, 7 Jun 2026 01:53:45 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=94=B6=E5=8F=A3=20BangDream=20?= =?UTF-8?q?=E8=AF=95=E7=82=BC=E6=95=B0=E6=8D=AE=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 | 3 +- .../models/event-stage-data-repository.ts | 45 ++++++++++++++++ .../bangDream/tsugu/models/event-stage.ts | 17 +++--- .../tsugu/event-stage-data-repository.spec.ts | 53 +++++++++++++++++++ 4 files changed, 109 insertions(+), 9 deletions(-) create mode 100644 src/qqbot/plugins/bangDream/tsugu/models/event-stage-data-repository.ts create mode 100644 test/qqbot/plugins/bangDream/tsugu/event-stage-data-repository.spec.ts diff --git a/docs/qqbot-bangdream-tsugu-global-refactor-plan.md b/docs/qqbot-bangdream-tsugu-global-refactor-plan.md index d4f4408..52a541c 100644 --- a/docs/qqbot-bangdream-tsugu-global-refactor-plan.md +++ b/docs/qqbot-bangdream-tsugu-global-refactor-plan.md @@ -14,7 +14,7 @@ ## 当前事实 - Tsugu 源码目录:`src/qqbot/plugins/bangDream/tsugu` -- TS 文件:初始基线 92;当前 `tsugu` 源码 134 +- TS 文件:初始基线 92;当前 `tsugu` 源码 135 - 函数节点:481,其中稳定函数 410,匿名/内联回调 71 - 源码 JSDoc:稳定函数 410/410 已覆盖 - 变量声明:1896 @@ -301,6 +301,7 @@ export interface TsuguHook { - 已新增 `models/song-resource-repository.ts`,把 `Song` 的歌曲详情、谱面、封面路径、封面完整 URL 和封面缺失时服务器回退下载收口到不依赖 `Song` 类的资源 repository,避免 `song.ts` 继续直接拼 Bestdori API/asset 路径;`song-resource-repository.spec.ts` 覆盖 detail/chart provider 调用、13/40 旧封面批次、273 强制 CN 和封面 fallback 行为,本地 `/查曲 136`、`/查谱面 136 expert` 图片 smoke 输出非空。 - 已新增 `models/card-resource-repository.ts`,把 `Card` 的详情请求、资源批次目录、图标/插画/trim 图片路径和重图缓存策略收口到 provider-backed repository;`card-resource-repository.spec.ts` 覆盖 `/api/cards` 缓存参数、`9999` 后资源批次、CN 优先资源路径和非 icon 图片 `memoryCache=false`,本地 `/查卡 472` 图片 smoke 输出非空。 - 已新增 `models/gacha-resource-repository.ts`,把 `Gacha` 的详情请求、首页横幅、screen 背景、`bg1` fallback 和 Logo 资源路径收口到 provider-backed repository;`gacha-resource-repository.spec.ts` 覆盖 `/api/gacha` 缓存参数、CN 优先 screen 路径、横幅缺失回退 Logo 和背景 fallback,本地 `/查卡池 259`、`/抽卡模拟 10 259` 图片 smoke 输出非空。 +- 已新增 `models/event-stage-data-repository.ts`,把 `EventStage` 的 festival stages/rotationMusics 数据请求从 `bestdoriUrl + callAPIAndCacheResponse` 收口到 provider-backed repository;`event-stage-data-repository.spec.ts` 覆盖两个 festival API 路径和缓存参数,本地 `/查试炼 310` 保持拆成 5 张图片输出。 - 固化: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/event-stage-data-repository.ts b/src/qqbot/plugins/bangDream/tsugu/models/event-stage-data-repository.ts new file mode 100644 index 0000000..7eb1aeb --- /dev/null +++ b/src/qqbot/plugins/bangDream/tsugu/models/event-stage-data-repository.ts @@ -0,0 +1,45 @@ +import { bangDreamBestdoriProvider } from '@/qqbot/plugins/bangDream/tsugu/data-clients/bestdori-provider'; +import type { BangDreamDataProvider } from '@/qqbot/plugins/bangDream/tsugu/data-clients/data-provider'; + +export type EventStageDataType = 'stages' | 'rotationMusics'; + +export interface EventStageTypeRow { + endAt: string; + startAt: string; + type: string; +} + +export interface EventStageRotationMusicRow { + endAt: string; + musicId: string; + startAt: string; +} + +export type EventStageDataRows = + T extends 'stages' ? EventStageTypeRow[] : EventStageRotationMusicRow[]; + +export class EventStageDataRepository { + constructor( + private readonly provider: BangDreamDataProvider = bangDreamBestdoriProvider, + ) {} + + /** + * 获取试炼活动阶段或轮换歌曲数据。 + * + * @param eventId - 活动 ID。 + * @param type - 试炼数据类型。 + * @param update - 是否绕过缓存。 + */ + async getFestivalData( + eventId: number, + type: T, + update: boolean = true, + ): Promise> { + return await this.provider.getJson>( + `/api/festival/${type}/${eventId}.json`, + { cacheTime: update ? 0 : 1 / 0 }, + ); + } +} + +export const eventStageDataRepository = new EventStageDataRepository(); diff --git a/src/qqbot/plugins/bangDream/tsugu/models/event-stage.ts b/src/qqbot/plugins/bangDream/tsugu/models/event-stage.ts index 829a4e2..647ad4d 100644 --- a/src/qqbot/plugins/bangDream/tsugu/models/event-stage.ts +++ b/src/qqbot/plugins/bangDream/tsugu/models/event-stage.ts @@ -1,12 +1,14 @@ import { Event } from '@/qqbot/plugins/bangDream/tsugu/models/event'; -import { bestdoriUrl } from '@/qqbot/plugins/bangDream/tsugu/runtime/config'; -import { callAPIAndCacheResponse } from '@/qqbot/plugins/bangDream/tsugu/data-clients/api-cache-client'; import { BANGDREAM_EVENT_STAGE_NAME, BANGDREAM_EVENT_STAGE_STROKE_COLOR, BANGDREAM_EVENT_STAGE_TYPES, BangDreamEventStageType, } from '@/qqbot/plugins/bangDream/tsugu/models/bangdream-constants'; +import { + eventStageDataRepository, + type EventStageDataType, +} from '@/qqbot/plugins/bangDream/tsugu/models/event-stage-data-repository'; export interface Stage { type: string; @@ -72,13 +74,12 @@ export class EventStage { * @param update - update参数,未传入时使用默认值。 * @param type - 数据类型或匹配类型。 */ - async getData(update: boolean = true, type: 'stages' | 'rotationMusics') { - const time = update ? 0 : 1 / 0; - const eventData = await callAPIAndCacheResponse( - `${bestdoriUrl}/api/festival/${type}/${this.eventId}.json`, - time, + async getData(update: boolean = true, type: EventStageDataType) { + return await eventStageDataRepository.getFestivalData( + this.eventId, + type, + update, ); - return eventData; } /** diff --git a/test/qqbot/plugins/bangDream/tsugu/event-stage-data-repository.spec.ts b/test/qqbot/plugins/bangDream/tsugu/event-stage-data-repository.spec.ts new file mode 100644 index 0000000..9382a5b --- /dev/null +++ b/test/qqbot/plugins/bangDream/tsugu/event-stage-data-repository.spec.ts @@ -0,0 +1,53 @@ +import type { BangDreamDataProvider } from '@/qqbot/plugins/bangDream/tsugu/data-clients/data-provider'; +import { EventStageDataRepository } from '@/qqbot/plugins/bangDream/tsugu/models/event-stage-data-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 event stage data repository', () => { + it('routes stage and rotation music requests through the provider', async () => { + const provider = createProviderMock(); + provider.getJson + .mockResolvedValueOnce([{ type: 'combo' }]) + .mockResolvedValueOnce([{ musicId: '136' }]); + const repository = new EventStageDataRepository(provider); + + await expect(repository.getFestivalData(310, 'stages')).resolves.toEqual([ + { type: 'combo' }, + ]); + await expect( + repository.getFestivalData(310, 'rotationMusics'), + ).resolves.toEqual([{ musicId: '136' }]); + + expect(provider.getJson).toHaveBeenNthCalledWith( + 1, + '/api/festival/stages/310.json', + { cacheTime: 0 }, + ); + expect(provider.getJson).toHaveBeenNthCalledWith( + 2, + '/api/festival/rotationMusics/310.json', + { cacheTime: 0 }, + ); + }); + + it('uses an infinite cache when update is disabled', async () => { + const provider = createProviderMock(); + provider.getJson.mockResolvedValue([]); + const repository = new EventStageDataRepository(provider); + + await repository.getFestivalData(310, 'stages', false); + + expect(provider.getJson).toHaveBeenCalledWith( + '/api/festival/stages/310.json', + { cacheTime: Infinity }, + ); + }); +});