refactor: 收口 BangDream 试炼数据仓储

This commit is contained in:
sunlei 2026-06-07 01:53:45 +08:00
parent ca3b2a0cb1
commit 00b6df9471
4 changed files with 109 additions and 9 deletions

View File

@ -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/<file>.spec.ts ...`,路径用正斜杠。
### Phase 4搜索 specification 和 matcher

View File

@ -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 EventStageDataType> =
T extends 'stages' ? EventStageTypeRow[] : EventStageRotationMusicRow[];
export class EventStageDataRepository {
constructor(
private readonly provider: BangDreamDataProvider = bangDreamBestdoriProvider,
) {}
/**
*
*
* @param eventId - ID
* @param type -
* @param update -
*/
async getFestivalData<T extends EventStageDataType>(
eventId: number,
type: T,
update: boolean = true,
): Promise<EventStageDataRows<T>> {
return await this.provider.getJson<EventStageDataRows<T>>(
`/api/festival/${type}/${eventId}.json`,
{ cacheTime: update ? 0 : 1 / 0 },
);
}
}
export const eventStageDataRepository = new EventStageDataRepository();

View File

@ -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;
}
/**

View File

@ -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<BangDreamDataProvider> {
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 },
);
});
});