refactor: 收口 BangDream 卡池资源仓储

This commit is contained in:
sunlei 2026-06-07 01:44:07 +08:00
parent 742dae2349
commit ca3b2a0cb1
4 changed files with 301 additions and 43 deletions

View File

@ -14,7 +14,7 @@
## 当前事实
- Tsugu 源码目录:`src/qqbot/plugins/bangDream/tsugu`
- TS 文件:初始基线 92当前 `tsugu` 源码 133
- TS 文件:初始基线 92当前 `tsugu` 源码 134
- 函数节点481其中稳定函数 410匿名/内联回调 71
- 源码 JSDoc稳定函数 410/410 已覆盖
- 变量声明1896
@ -300,6 +300,7 @@ export interface TsuguHook {
- 本地图片烟测已生成查歌 `136`、查活动 `50` 简易背景和真实活动背景图片,证明 provider/repository 第一段迁移后仍能输出非空图片。
- 已新增 `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 输出非空。
- 固化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,141 @@
import { bangDreamBestdoriProvider } from '@/qqbot/plugins/bangDream/tsugu/data-clients/bestdori-provider';
import type { BangDreamDataProvider } from '@/qqbot/plugins/bangDream/tsugu/data-clients/data-provider';
import {
getServerByPriority,
Server,
} from '@/qqbot/plugins/bangDream/tsugu/models/server';
import { globalDefaultServer } from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
export interface GachaResourceSource {
bannerAssetBundleName?: string;
gachaId: number;
publishedAt: Array<number | null>;
resourceName: string;
}
export type GachaScreenImageType = 'background' | 'backgroundFallback' | 'logo';
/** 将服务器枚举值转换为 Bestdori 资源路径中的服务器编码。 */
function toServerCode(server: Server | undefined): string {
return server == null ? 'undefined' : Server[server];
}
export class GachaResourceRepository {
constructor(
private readonly provider: BangDreamDataProvider = bangDreamBestdoriProvider,
) {}
/**
*
*
* @param gachaId - ID
* @param update -
*/
async getDetail(
gachaId: number,
update: boolean = true,
): Promise<Record<string, any>> {
return await this.provider.getJson<Record<string, any>>(
`/api/gacha/${gachaId}.json`,
{ cacheTime: update ? 0 : 1 / 0 },
);
}
/**
*
*
* @param source -
*/
getBannerImagePath(source: GachaResourceSource): string | null {
if (!source.bannerAssetBundleName) return null;
return `/assets/jp/homebanner_rip/${source.bannerAssetBundleName}.png`;
}
/**
* screen
*
* @param source -
* @param imageType - screen
* @param displayedServerList -
*/
getScreenImagePath(
source: GachaResourceSource,
imageType: GachaScreenImageType,
displayedServerList: Server[] = globalDefaultServer,
): string {
const serverCode = toServerCode(
getServerByPriority(source.publishedAt, displayedServerList),
);
const fileName =
imageType === 'background'
? 'bg'
: imageType === 'backgroundFallback'
? 'bg1'
: 'logo';
return `/assets/${serverCode}/gacha/screen/${source.resourceName}_rip/${fileName}.png`;
}
/**
* 退 Logo
*
* @param source -
* @param displayedServerList -
*/
async getBannerImageBuffer(
source: GachaResourceSource,
displayedServerList: Server[] = globalDefaultServer,
): Promise<Buffer> {
const bannerPath = this.getBannerImagePath(source);
if (!bannerPath) {
return await this.getLogoImageBuffer(source, displayedServerList);
}
try {
return await this.provider.getAsset(bannerPath, { ignoreError: false });
} catch {
return await this.getLogoImageBuffer(source, displayedServerList);
}
}
/**
* `bg.png` 退 `bg1.png`
*
* @param source -
* @param displayedServerList -
*/
async getBackgroundImageBuffer(
source: GachaResourceSource,
displayedServerList: Server[] = globalDefaultServer,
): Promise<Buffer> {
try {
return await this.provider.getAsset(
this.getScreenImagePath(source, 'background', displayedServerList),
{ ignoreError: false },
);
} catch {
return await this.provider.getAsset(
this.getScreenImagePath(
source,
'backgroundFallback',
displayedServerList,
),
);
}
}
/**
* Logo
*
* @param source -
* @param displayedServerList -
*/
async getLogoImageBuffer(
source: GachaResourceSource,
displayedServerList: Server[] = globalDefaultServer,
): Promise<Buffer> {
return await this.provider.getAsset(
this.getScreenImagePath(source, 'logo', displayedServerList),
);
}
}
export const gachaResourceRepository = new GachaResourceRepository();

View File

@ -1,4 +1,3 @@
import { bangDreamBestdoriProvider } from '@/qqbot/plugins/bangDream/tsugu/data-clients/bestdori-provider';
import { bangDreamMainDataRepository } from '@/qqbot/plugins/bangDream/tsugu/models/main-data-repository';
import { Image, loadImage } from 'skia-canvas';
import {
@ -7,14 +6,13 @@ import {
serverList,
} from '@/qqbot/plugins/bangDream/tsugu/models/server';
import { getPresentEvent } from '@/qqbot/plugins/bangDream/tsugu/models/event';
import {
globalDefaultServer,
} from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
import { globalDefaultServer } from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
import { BANGDREAM_GACHA_TYPE_NAME } from '@/qqbot/plugins/bangDream/tsugu/models/bangdream-constants';
import {
isFreeGachaType,
isPermanentJapaneseGachaPeriod,
} from '@/qqbot/plugins/bangDream/tsugu/models/gacha-policy';
import { gachaResourceRepository } from '@/qqbot/plugins/bangDream/tsugu/models/gacha-resource-repository';
const gachaDataCache = {};
@ -81,10 +79,9 @@ export class Gacha {
*/
constructor(gachaId: number) {
this.gachaId = gachaId;
const gachaData = bangDreamMainDataRepository.getEntity<Record<string, any>>(
'gacha',
gachaId,
);
const gachaData = bangDreamMainDataRepository.getEntity<
Record<string, any>
>('gacha', gachaId);
if (gachaData == undefined) {
this.isExist = false;
return;
@ -147,14 +144,7 @@ export class Gacha {
* @param update - update参数使
*/
async getData(update: boolean = true) {
const time = update ? 0 : 1 / 0;
const gachaData = await bangDreamBestdoriProvider.getJson<
Record<string, any>
>(
`/api/gacha/${this.gachaId}.json`,
{ cacheTime: time },
);
return gachaData;
return await gachaResourceRepository.getDetail(this.gachaId, update);
}
/**
* Gacha
@ -162,16 +152,9 @@ export class Gacha {
* @returns
*/
async getBannerImage(): Promise<Image> {
try {
if (this.bannerAssetBundleName == undefined) return this.getGachaLogo();
const BannerImageBuffer = await bangDreamBestdoriProvider.getAsset(
`/assets/jp/homebanner_rip/${this.bannerAssetBundleName}.png`,
{ ignoreError: false },
);
return await loadImage(BannerImageBuffer);
} catch {
return this.getGachaLogo();
}
const bannerImageBuffer =
await gachaResourceRepository.getBannerImageBuffer(this);
return await loadImage(bannerImageBuffer);
}
/**
* Gacha
@ -183,19 +166,12 @@ export class Gacha {
displayedServerList: Server[] = globalDefaultServer,
): Promise<Image> {
if (!displayedServerList) displayedServerList = globalDefaultServer;
const server = getServerByPriority(this.publishedAt);
let BGImageBuffer: Buffer;
try {
BGImageBuffer = await bangDreamBestdoriProvider.getAsset(
`/assets/${Server[server]}/gacha/screen/${this.resourceName}_rip/bg.png`,
{ ignoreError: false },
const backgroundImageBuffer =
await gachaResourceRepository.getBackgroundImageBuffer(
this,
displayedServerList,
);
} catch {
BGImageBuffer = await bangDreamBestdoriProvider.getAsset(
`/assets/${Server[server]}/gacha/screen/${this.resourceName}_rip/bg1.png`,
);
}
return await loadImage(BGImageBuffer);
return await loadImage(backgroundImageBuffer);
}
/**
* Gacha Logo
@ -207,11 +183,11 @@ export class Gacha {
displayedServerList: Server[] = globalDefaultServer,
): Promise<Image> {
if (!displayedServerList) displayedServerList = globalDefaultServer;
const server = getServerByPriority(this.publishedAt);
const LogoImageBuffer = await bangDreamBestdoriProvider.getAsset(
`/assets/${Server[server]}/gacha/screen/${this.resourceName}_rip/logo.png`,
const logoImageBuffer = await gachaResourceRepository.getLogoImageBuffer(
this,
displayedServerList,
);
return await loadImage(LogoImageBuffer);
return await loadImage(logoImageBuffer);
}
/**
* Gacha ID

View File

@ -0,0 +1,140 @@
import type { BangDreamDataProvider } from '@/qqbot/plugins/bangDream/tsugu/data-clients/data-provider';
import {
GachaResourceRepository,
type GachaResourceSource,
} from '@/qqbot/plugins/bangDream/tsugu/models/gacha-resource-repository';
import { Server } from '@/qqbot/plugins/bangDream/tsugu/models/server';
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}`),
};
}
function createGachaSource(
overrides: Partial<GachaResourceSource> = {},
): GachaResourceSource {
return {
bannerAssetBundleName: 'banner_gacha0259',
gachaId: 259,
publishedAt: [100, null, null, 200, null],
resourceName: 'gacha0259',
...overrides,
};
}
describe('BangDream gacha resource repository', () => {
it('routes gacha detail requests through the provider with explicit cache policy', async () => {
const provider = createProviderMock();
provider.getJson.mockResolvedValue({ ok: true });
const repository = new GachaResourceRepository(provider);
await expect(repository.getDetail(259, true)).resolves.toEqual({
ok: true,
});
await expect(repository.getDetail(259, false)).resolves.toEqual({
ok: true,
});
expect(provider.getJson).toHaveBeenNthCalledWith(1, '/api/gacha/259.json', {
cacheTime: 0,
});
expect(provider.getJson).toHaveBeenNthCalledWith(2, '/api/gacha/259.json', {
cacheTime: Infinity,
});
});
it('builds banner, background, fallback background, and logo paths', () => {
const repository = new GachaResourceRepository(createProviderMock());
const source = createGachaSource();
const serverPriority = [Server.cn, Server.jp];
expect(repository.getBannerImagePath(source)).toBe(
'/assets/jp/homebanner_rip/banner_gacha0259.png',
);
expect(
repository.getScreenImagePath(source, 'background', serverPriority),
).toBe('/assets/cn/gacha/screen/gacha0259_rip/bg.png');
expect(
repository.getScreenImagePath(
source,
'backgroundFallback',
serverPriority,
),
).toBe('/assets/cn/gacha/screen/gacha0259_rip/bg1.png');
expect(repository.getScreenImagePath(source, 'logo', serverPriority)).toBe(
'/assets/cn/gacha/screen/gacha0259_rip/logo.png',
);
});
it('falls back to logo when the banner is missing or unavailable', async () => {
const provider = createProviderMock();
const logoBuffer = Buffer.from('logo');
provider.getAsset.mockResolvedValue(logoBuffer);
const repository = new GachaResourceRepository(provider);
await expect(
repository.getBannerImageBuffer(
createGachaSource({ bannerAssetBundleName: undefined }),
[Server.cn, Server.jp],
),
).resolves.toBe(logoBuffer);
expect(provider.getAsset).toHaveBeenNthCalledWith(
1,
'/assets/cn/gacha/screen/gacha0259_rip/logo.png',
);
provider.getAsset.mockReset();
provider.getAsset
.mockRejectedValueOnce(new Error('banner missing'))
.mockResolvedValueOnce(logoBuffer);
await expect(
repository.getBannerImageBuffer(createGachaSource(), [
Server.cn,
Server.jp,
]),
).resolves.toBe(logoBuffer);
expect(provider.getAsset).toHaveBeenNthCalledWith(
1,
'/assets/jp/homebanner_rip/banner_gacha0259.png',
{ ignoreError: false },
);
expect(provider.getAsset).toHaveBeenNthCalledWith(
2,
'/assets/cn/gacha/screen/gacha0259_rip/logo.png',
);
});
it('falls back from bg.png to bg1.png for gacha backgrounds', async () => {
const provider = createProviderMock();
const fallbackBuffer = Buffer.from('bg1');
provider.getAsset
.mockRejectedValueOnce(new Error('background missing'))
.mockResolvedValueOnce(fallbackBuffer);
const repository = new GachaResourceRepository(provider);
await expect(
repository.getBackgroundImageBuffer(createGachaSource(), [
Server.cn,
Server.jp,
]),
).resolves.toBe(fallbackBuffer);
expect(provider.getAsset).toHaveBeenNthCalledWith(
1,
'/assets/cn/gacha/screen/gacha0259_rip/bg.png',
{ ignoreError: false },
);
expect(provider.getAsset).toHaveBeenNthCalledWith(
2,
'/assets/cn/gacha/screen/gacha0259_rip/bg1.png',
);
});
});