diff --git a/docs/qqbot-bangdream-tsugu-global-refactor-plan.md b/docs/qqbot-bangdream-tsugu-global-refactor-plan.md index b8d7bda..4d63ec5 100644 --- a/docs/qqbot-bangdream-tsugu-global-refactor-plan.md +++ b/docs/qqbot-bangdream-tsugu-global-refactor-plan.md @@ -312,6 +312,7 @@ export interface TsuguHook { - 已新增 `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 输出非空。 - 已新增 `models/degree-resource-repository.ts`,把 `Degree` 的称号缩略图、称号框、称号图标、动态称号脚本和纹理素材路径从 `bestdoriUrl + downloadFile/downloadFileCache` 收口到 provider-backed repository;`degree-resource-repository.spec.ts` 覆盖旧/新缩略图 fallback、动态称号旧纹理白名单和 provider 调用,本地 `/ycx 10 100 cn` 图片 smoke 输出非空。 - 已新增 `render-blocks/deck-rank-resource-repository.ts`,把玩家详情“乐队编成等级”Rank 图片从 `list-band-detail.ts` 的本地路径和 Bestdori URL 直拼收口到 repository,渲染层只负责 `Buffer -> Image`;`deck-rank-resource-repository.spec.ts` 覆盖本地素材优先、远端 `/res/icon/*.png` 兜底和 provider 调用,本地 `/查玩家 26591455 jp` 图片 smoke 输出非空。 +- `models/event-data-repository.ts` 已改为 provider-injected repository,并为活动背景和 topscreen trim 增加路径工厂;活动详情背景现在跟随 `displayedServerList` 选服,不再无条件按全局默认服取底图,避免 `/查活动 ... jp` 这类指定服务器查询出现横幅和背景不一致。 - 固化: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/command-renderers/event-detail.ts b/src/qqbot/plugins/bangDream/tsugu/command-renderers/event-detail.ts index fec497b..a2c72f4 100644 --- a/src/qqbot/plugins/bangDream/tsugu/command-renderers/event-detail.ts +++ b/src/qqbot/plugins/bangDream/tsugu/command-renderers/event-detail.ts @@ -459,7 +459,9 @@ export async function drawEventDetail( all.push(gachaImageList[i]); } - const BGimage = useEasyBG ? undefined : await event.getEventBGImage(); + const BGimage = useEasyBG + ? undefined + : await event.getEventBGImage(displayedServerList); return await createOutputFinalImages({ useEasyBG, diff --git a/src/qqbot/plugins/bangDream/tsugu/models/event-data-repository.ts b/src/qqbot/plugins/bangDream/tsugu/models/event-data-repository.ts index 2d1bf32..1255c61 100644 --- a/src/qqbot/plugins/bangDream/tsugu/models/event-data-repository.ts +++ b/src/qqbot/plugins/bangDream/tsugu/models/event-data-repository.ts @@ -1,6 +1,5 @@ import { Canvas, Image, loadImage } from 'skia-canvas'; import { bangDreamBestdoriProvider } from '@/qqbot/plugins/bangDream/tsugu/data-clients/bestdori-provider'; -import { bangDreamMainDataRepository } from '@/qqbot/plugins/bangDream/tsugu/models/main-data-repository'; import { getServerByPriority, Server, @@ -35,7 +34,11 @@ type RewardWithId = { rewardType: string; }; -class EventDataRepository { +export class EventDataRepository { + constructor( + private readonly provider = bangDreamBestdoriProvider, + ) {} + /** * 获取活动远端详情数据。 * @@ -44,12 +47,40 @@ class EventDataRepository { */ async getDetail(eventId: number, update = true): Promise> { const cacheTime = update ? 0 : 1 / 0; - return await bangDreamBestdoriProvider.getJson>( + return await this.provider.getJson>( `/api/events/${eventId}.json`, { cacheTime }, ); } + /** + * 获取活动背景资源路径。 + * + * @param event - 活动资源上下文。 + * @param displayedServerList - 展示服务器优先级。 + */ + getBackgroundImagePath( + event: EventAssetContext, + displayedServerList: Server[] = globalDefaultServer, + ): string { + const server = getServerByPriority(event.startAt, displayedServerList); + return `/assets/${Server[server]}/event/${event.assetBundleName}/topscreen_rip/bg_eventtop.png`; + } + + /** + * 获取活动主界面裁切图资源路径。 + * + * @param event - 活动资源上下文。 + * @param displayedServerList - 展示服务器优先级。 + */ + getTopscreenTrimImagePath( + event: EventAssetContext, + displayedServerList: Server[] = globalDefaultServer, + ): string { + const server = getServerByPriority(event.startAt, displayedServerList); + return `/assets/${Server[server]}/event/${event.assetBundleName}/topscreen_rip/trim_eventtop.png`; + } + /** * 获取活动横幅图,优先活动资源,失败时回退 homebanner。 * @@ -62,13 +93,13 @@ class EventDataRepository { ): Promise { const server = getServerByPriority(event.startAt, displayedServerList); try { - const bannerImageBuffer = await bangDreamBestdoriProvider.getAsset( + const bannerImageBuffer = await this.provider.getAsset( `/assets/${Server[server]}/event/${event.assetBundleName}/images_rip/banner.png`, { ignoreError: false }, ); return await loadImage(bannerImageBuffer); } catch { - const bannerImageBuffer = await bangDreamBestdoriProvider.getAsset( + const bannerImageBuffer = await this.provider.getAsset( `/assets/jp/homebanner_rip/${event.bannerAssetBundleName}.png`, ); return await loadImage(bannerImageBuffer); @@ -79,15 +110,21 @@ class EventDataRepository { * 获取活动背景图。 * * @param event - 活动资源上下文。 + * @param displayedServerList - 展示服务器优先级。 */ - async getBackgroundImage(event: EventAssetContext): Promise { - const server = getServerByPriority(event.startAt); - const bgImageBuffer = await bangDreamBestdoriProvider.getAsset( - `/assets/${Server[server]}/event/${event.assetBundleName}/topscreen_rip/bg_eventtop.png`, + async getBackgroundImage( + event: EventAssetContext, + displayedServerList: Server[] = globalDefaultServer, + ): Promise { + const bgImageBuffer = await this.provider.getAsset( + this.getBackgroundImagePath(event, displayedServerList), ); const backgroundImage = await loadImage(bgImageBuffer); try { - const trimImage = await this.getTopscreenTrimImage(event); + const trimImage = await this.getTopscreenTrimImage( + event, + displayedServerList, + ); return this.mergeTopscreenImages(backgroundImage, trimImage); } catch { return backgroundImage; @@ -110,7 +147,7 @@ class EventDataRepository { let ruleNumber = 1; while (true) { try { - const slideImageBuffer = await bangDreamBestdoriProvider.getAsset( + const slideImageBuffer = await this.provider.getAsset( `${basePath}rule${ruleNumber}.png`, { ignoreError: false }, ); @@ -127,11 +164,14 @@ class EventDataRepository { * 获取活动主界面裁切图。 * * @param event - 活动资源上下文。 + * @param displayedServerList - 展示服务器优先级。 */ - async getTopscreenTrimImage(event: EventAssetContext): Promise { - const server = getServerByPriority(event.startAt); - const topscreenTrimImageBuffer = await bangDreamBestdoriProvider.getAsset( - `/assets/${Server[server]}/event/${event.assetBundleName}/topscreen_rip/trim_eventtop.png`, + async getTopscreenTrimImage( + event: EventAssetContext, + displayedServerList: Server[] = globalDefaultServer, + ): Promise { + const topscreenTrimImageBuffer = await this.provider.getAsset( + this.getTopscreenTrimImagePath(event, displayedServerList), ); return await loadImage(topscreenTrimImageBuffer); } @@ -172,7 +212,7 @@ class EventDataRepository { tempServer: Server, ): Promise { const server = getServerByPriority(event.startAt, [tempServer]); - const logoImageBuffer = await bangDreamBestdoriProvider.getAsset( + const logoImageBuffer = await this.provider.getAsset( `/assets/${Server[server]}/event/${event.assetBundleName}/images_rip/logo.png`, ); return await loadImage(logoImageBuffer); @@ -188,7 +228,7 @@ class EventDataRepository { event: EventRewardContext, server: Server, ): Promise { - const allStamps = await bangDreamBestdoriProvider.getJson< + const allStamps = await this.provider.getJson< Record >('/api/stamps/all.2.json'); const rewardId = this.pickRewardId(event.pointRewards, 'stamp'); @@ -202,7 +242,7 @@ class EventDataRepository { const serverName = this.pickReleasedServerName(event.startAt, server, 'jp'); try { - const stampBuffer = await bangDreamBestdoriProvider.getAsset( + const stampBuffer = await this.provider.getAsset( `/assets/${serverName}/stamp/01_rip/${stampAssetName}.png`, { ignoreError: false }, ); @@ -227,6 +267,9 @@ class EventDataRepository { const rewardId = this.pickRewardId(event.rankingRewards, 'deco_pins'); if (rewardId === undefined) return undefined; + const { bangDreamMainDataRepository } = await import( + '@/qqbot/plugins/bangDream/tsugu/models/main-data-repository' + ); const allDeco = bangDreamMainDataRepository.getCollection< Record >('deco'); @@ -235,7 +278,7 @@ class EventDataRepository { const serverName = this.pickReleasedServerName(event.startAt, server, 'cn'); try { - const decoBuffer = await bangDreamBestdoriProvider.getAsset( + const decoBuffer = await this.provider.getAsset( `/assets/${serverName}/deco/pins_rip/${decoAssetName}.png`, { ignoreError: false }, ); diff --git a/src/qqbot/plugins/bangDream/tsugu/models/event.ts b/src/qqbot/plugins/bangDream/tsugu/models/event.ts index 3defd1b..7474723 100644 --- a/src/qqbot/plugins/bangDream/tsugu/models/event.ts +++ b/src/qqbot/plugins/bangDream/tsugu/models/event.ts @@ -214,8 +214,13 @@ export class Event { * * @returns 异步处理结果。 */ - async getEventBGImage(): Promise { - return await eventDataRepository.getBackgroundImage(this); + async getEventBGImage( + displayedServerList: Server[] = globalDefaultServer, + ): Promise { + return await eventDataRepository.getBackgroundImage( + this, + displayedServerList, + ); } //活动规则轮播图 /** @@ -233,8 +238,13 @@ export class Event { * * @returns 异步处理结果。 */ - async getEventTopscreenTrimImage(): Promise { - return await eventDataRepository.getTopscreenTrimImage(this); + async getEventTopscreenTrimImage( + displayedServerList: Server[] = globalDefaultServer, + ): Promise { + return await eventDataRepository.getTopscreenTrimImage( + this, + displayedServerList, + ); } /** * 在 Event 模型中获取活动Logo图片。 diff --git a/test/qqbot/plugins/bangDream/tsugu/event-data-repository.spec.ts b/test/qqbot/plugins/bangDream/tsugu/event-data-repository.spec.ts new file mode 100644 index 0000000..27f7e18 --- /dev/null +++ b/test/qqbot/plugins/bangDream/tsugu/event-data-repository.spec.ts @@ -0,0 +1,61 @@ +import type { BangDreamDataProvider } from '@/qqbot/plugins/bangDream/tsugu/data-clients/data-provider'; +import { + EventDataRepository, + type EventAssetContext, +} from '@/qqbot/plugins/bangDream/tsugu/models/event-data-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}`), + }; +} + +function createEventAssetContext(): EventAssetContext { + return { + assetBundleName: 'event_test', + bannerAssetBundleName: 'homebanner_test', + startAt: [1, null, null, 2, null], + }; +} + +describe('BangDream event data repository', () => { + it('builds event background paths from the requested server priority', () => { + const repository = new EventDataRepository(createProviderMock()); + const event = createEventAssetContext(); + + expect(repository.getBackgroundImagePath(event, [Server.jp])).toBe( + '/assets/jp/event/event_test/topscreen_rip/bg_eventtop.png', + ); + expect(repository.getTopscreenTrimImagePath(event, [Server.jp])).toBe( + '/assets/jp/event/event_test/topscreen_rip/trim_eventtop.png', + ); + }); + + it('falls back to released servers when requested server has no event assets', () => { + const repository = new EventDataRepository(createProviderMock()); + const event = createEventAssetContext(); + + expect(repository.getBackgroundImagePath(event, [Server.en])).toBe( + '/assets/cn/event/event_test/topscreen_rip/bg_eventtop.png', + ); + }); + + it('routes event detail requests through the provider cache policy', async () => { + const provider = createProviderMock(); + provider.getJson.mockResolvedValue({ eventName: ['test'] }); + const repository = new EventDataRepository(provider); + + await expect(repository.getDetail(50, false)).resolves.toEqual({ + eventName: ['test'], + }); + + expect(provider.getJson).toHaveBeenCalledWith('/api/events/50.json', { + cacheTime: Infinity, + }); + }); +});