refactor: 收口 BangDream 角色资源仓储
This commit is contained in:
parent
00b6df9471
commit
00ff99d2b6
@ -302,6 +302,7 @@ export interface TsuguHook {
|
|||||||
- 已新增 `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/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/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 张图片输出。
|
- 已新增 `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 张图片输出。
|
||||||
|
- 已新增 `models/character-resource-repository.ts`,把 `Character` 的详情请求、角色图标、KV 立绘和名称横幅资源路径从 `bestdoriUrl + callAPIAndCacheResponse/downloadFileCache` 收口到 provider-backed repository;`character-resource-repository.spec.ts` 覆盖详情缓存策略和三类资源路径,本地 `/查角色 1` 图片 smoke 输出非空。
|
||||||
- 固化:Windows 下不要用反斜杠测试路径直接调用 Jest pattern,容易出现 `Pattern ... - 0 matches`;指定文件测试统一使用 `pnpm exec jest --runInBand --runTestsByPath test/qqbot/plugins/bangDream/tsugu/<file>.spec.ts ...`,路径用正斜杠。
|
- 固化:Windows 下不要用反斜杠测试路径直接调用 Jest pattern,容易出现 `Pattern ... - 0 matches`;指定文件测试统一使用 `pnpm exec jest --runInBand --runTestsByPath test/qqbot/plugins/bangDream/tsugu/<file>.spec.ts ...`,路径用正斜杠。
|
||||||
|
|
||||||
### Phase 4:搜索 specification 和 matcher
|
### Phase 4:搜索 specification 和 matcher
|
||||||
|
|||||||
@ -0,0 +1,84 @@
|
|||||||
|
import { bangDreamBestdoriProvider } from '@/qqbot/plugins/bangDream/tsugu/data-clients/bestdori-provider';
|
||||||
|
import type { BangDreamDataProvider } from '@/qqbot/plugins/bangDream/tsugu/data-clients/data-provider';
|
||||||
|
import { formatNumber } from '@/qqbot/plugins/bangDream/tsugu/models/model-utils';
|
||||||
|
|
||||||
|
export class CharacterResourceRepository {
|
||||||
|
constructor(
|
||||||
|
private readonly provider: BangDreamDataProvider = bangDreamBestdoriProvider,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取角色远端详情。
|
||||||
|
*
|
||||||
|
* @param characterId - 角色 ID。
|
||||||
|
* @param update - 是否绕过缓存。
|
||||||
|
*/
|
||||||
|
async getDetail(
|
||||||
|
characterId: number,
|
||||||
|
update: boolean = true,
|
||||||
|
): Promise<Record<string, any>> {
|
||||||
|
return await this.provider.getJson<Record<string, any>>(
|
||||||
|
`/api/characters/${characterId}.json`,
|
||||||
|
{ cacheTime: update ? 0 : 1 / 0 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取角色图标资源路径。
|
||||||
|
*
|
||||||
|
* @param characterId - 角色 ID。
|
||||||
|
*/
|
||||||
|
getIconPath(characterId: number): string {
|
||||||
|
return `/res/icon/chara_icon_${characterId}.png`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取角色 KV 立绘资源路径。
|
||||||
|
*
|
||||||
|
* @param characterId - 角色 ID。
|
||||||
|
*/
|
||||||
|
getIllustrationPath(characterId: number): string {
|
||||||
|
return `/assets/jp/ui/character_kv_image/${formatNumber(characterId, 3)}_rip/image.png`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取角色名称横幅资源路径。
|
||||||
|
*
|
||||||
|
* @param characterId - 角色 ID。
|
||||||
|
*/
|
||||||
|
getNameBannerPath(characterId: number): string {
|
||||||
|
return `/assets/jp/character_name_rip/name_top_chr${formatNumber(
|
||||||
|
characterId,
|
||||||
|
2,
|
||||||
|
)}.png`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载角色图标资源。
|
||||||
|
*
|
||||||
|
* @param characterId - 角色 ID。
|
||||||
|
*/
|
||||||
|
async getIconBuffer(characterId: number): Promise<Buffer> {
|
||||||
|
return await this.provider.getAsset(this.getIconPath(characterId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载角色 KV 立绘资源。
|
||||||
|
*
|
||||||
|
* @param characterId - 角色 ID。
|
||||||
|
*/
|
||||||
|
async getIllustrationBuffer(characterId: number): Promise<Buffer> {
|
||||||
|
return await this.provider.getAsset(this.getIllustrationPath(characterId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载角色名称横幅资源。
|
||||||
|
*
|
||||||
|
* @param characterId - 角色 ID。
|
||||||
|
*/
|
||||||
|
async getNameBannerBuffer(characterId: number): Promise<Buffer> {
|
||||||
|
return await this.provider.getAsset(this.getNameBannerPath(characterId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const characterResourceRepository = new CharacterResourceRepository();
|
||||||
@ -1,9 +1,6 @@
|
|||||||
import mainAPI from '@/qqbot/plugins/bangDream/tsugu/models/main-data-store';
|
import mainAPI from '@/qqbot/plugins/bangDream/tsugu/models/main-data-store';
|
||||||
import { callAPIAndCacheResponse } from '@/qqbot/plugins/bangDream/tsugu/data-clients/api-cache-client';
|
|
||||||
import { Image, loadImage } from 'skia-canvas';
|
import { Image, loadImage } from 'skia-canvas';
|
||||||
import { downloadFileCache } from '@/qqbot/plugins/bangDream/tsugu/data-clients/asset-cache-client';
|
import { characterResourceRepository } from '@/qqbot/plugins/bangDream/tsugu/models/character-resource-repository';
|
||||||
import { formatNumber } from '@/qqbot/plugins/bangDream/tsugu/models/model-utils';
|
|
||||||
import { bestdoriUrl } from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
|
|
||||||
|
|
||||||
export class Character {
|
export class Character {
|
||||||
characterId: number;
|
characterId: number;
|
||||||
@ -98,12 +95,10 @@ export class Character {
|
|||||||
* @param update - update参数,未传入时使用默认值。
|
* @param update - update参数,未传入时使用默认值。
|
||||||
*/
|
*/
|
||||||
async getData(update: boolean = true) {
|
async getData(update: boolean = true) {
|
||||||
const time = update ? 0 : 1 / 0;
|
return await characterResourceRepository.getDetail(
|
||||||
const cardData = await callAPIAndCacheResponse(
|
this.characterId,
|
||||||
`${bestdoriUrl}/api/characters/${this.characterId}.json`,
|
update,
|
||||||
time,
|
|
||||||
);
|
);
|
||||||
return cardData;
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 在 Character 模型中获取图标。
|
* 在 Character 模型中获取图标。
|
||||||
@ -111,8 +106,8 @@ export class Character {
|
|||||||
* @returns 异步处理结果。
|
* @returns 异步处理结果。
|
||||||
*/
|
*/
|
||||||
async getIcon(): Promise<Image> {
|
async getIcon(): Promise<Image> {
|
||||||
const iconBuffer = await downloadFileCache(
|
const iconBuffer = await characterResourceRepository.getIconBuffer(
|
||||||
`${bestdoriUrl}/res/icon/chara_icon_${this.characterId}.png`,
|
this.characterId,
|
||||||
);
|
);
|
||||||
return await loadImage(iconBuffer);
|
return await loadImage(iconBuffer);
|
||||||
}
|
}
|
||||||
@ -122,9 +117,8 @@ export class Character {
|
|||||||
* @returns 异步处理结果。
|
* @returns 异步处理结果。
|
||||||
*/
|
*/
|
||||||
async getIllustration(): Promise<Image> {
|
async getIllustration(): Promise<Image> {
|
||||||
const illustrationBuffer = await downloadFileCache(
|
const illustrationBuffer =
|
||||||
`${bestdoriUrl}/assets/jp/ui/character_kv_image/${formatNumber(this.characterId, 3)}_rip/image.png`,
|
await characterResourceRepository.getIllustrationBuffer(this.characterId);
|
||||||
);
|
|
||||||
return await loadImage(illustrationBuffer);
|
return await loadImage(illustrationBuffer);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@ -133,9 +127,8 @@ export class Character {
|
|||||||
* @returns 异步处理结果。
|
* @returns 异步处理结果。
|
||||||
*/
|
*/
|
||||||
async getNameBanner(): Promise<Image> {
|
async getNameBanner(): Promise<Image> {
|
||||||
const nameBannerBuffer = await downloadFileCache(
|
const nameBannerBuffer =
|
||||||
`${bestdoriUrl}/assets/jp/character_name_rip/name_top_chr${formatNumber(this.characterId, 2)}.png`,
|
await characterResourceRepository.getNameBannerBuffer(this.characterId);
|
||||||
);
|
|
||||||
return await loadImage(nameBannerBuffer);
|
return await loadImage(nameBannerBuffer);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -0,0 +1,81 @@
|
|||||||
|
import type { BangDreamDataProvider } from '@/qqbot/plugins/bangDream/tsugu/data-clients/data-provider';
|
||||||
|
import { CharacterResourceRepository } from '@/qqbot/plugins/bangDream/tsugu/models/character-resource-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 character resource repository', () => {
|
||||||
|
it('routes character detail requests through the provider with explicit cache policy', async () => {
|
||||||
|
const provider = createProviderMock();
|
||||||
|
provider.getJson.mockResolvedValue({ ok: true });
|
||||||
|
const repository = new CharacterResourceRepository(provider);
|
||||||
|
|
||||||
|
await expect(repository.getDetail(1, true)).resolves.toEqual({ ok: true });
|
||||||
|
await expect(repository.getDetail(1, false)).resolves.toEqual({
|
||||||
|
ok: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(provider.getJson).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
'/api/characters/1.json',
|
||||||
|
{ cacheTime: 0 },
|
||||||
|
);
|
||||||
|
expect(provider.getJson).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
'/api/characters/1.json',
|
||||||
|
{ cacheTime: Infinity },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('builds icon, illustration, and name banner paths', () => {
|
||||||
|
const repository = new CharacterResourceRepository(createProviderMock());
|
||||||
|
|
||||||
|
expect(repository.getIconPath(1)).toBe('/res/icon/chara_icon_1.png');
|
||||||
|
expect(repository.getIllustrationPath(1)).toBe(
|
||||||
|
'/assets/jp/ui/character_kv_image/001_rip/image.png',
|
||||||
|
);
|
||||||
|
expect(repository.getNameBannerPath(1)).toBe(
|
||||||
|
'/assets/jp/character_name_rip/name_top_chr01.png',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('downloads character assets through the provider', async () => {
|
||||||
|
const provider = createProviderMock();
|
||||||
|
const iconBuffer = Buffer.from('icon');
|
||||||
|
const illustrationBuffer = Buffer.from('illustration');
|
||||||
|
const nameBannerBuffer = Buffer.from('name-banner');
|
||||||
|
provider.getAsset
|
||||||
|
.mockResolvedValueOnce(iconBuffer)
|
||||||
|
.mockResolvedValueOnce(illustrationBuffer)
|
||||||
|
.mockResolvedValueOnce(nameBannerBuffer);
|
||||||
|
const repository = new CharacterResourceRepository(provider);
|
||||||
|
|
||||||
|
await expect(repository.getIconBuffer(1)).resolves.toBe(iconBuffer);
|
||||||
|
await expect(repository.getIllustrationBuffer(1)).resolves.toBe(
|
||||||
|
illustrationBuffer,
|
||||||
|
);
|
||||||
|
await expect(repository.getNameBannerBuffer(1)).resolves.toBe(
|
||||||
|
nameBannerBuffer,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(provider.getAsset).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
'/res/icon/chara_icon_1.png',
|
||||||
|
);
|
||||||
|
expect(provider.getAsset).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
'/assets/jp/ui/character_kv_image/001_rip/image.png',
|
||||||
|
);
|
||||||
|
expect(provider.getAsset).toHaveBeenNthCalledWith(
|
||||||
|
3,
|
||||||
|
'/assets/jp/character_name_rip/name_top_chr01.png',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user