refactor: 收口 BangDream 档线榜资源仓储

This commit is contained in:
sunlei 2026-06-07 03:07:59 +08:00
parent dbc2fab5af
commit fe8542c687
7 changed files with 187 additions and 41 deletions

View File

@ -309,6 +309,7 @@ export interface TsuguHook {
- 已新增 `render-blocks/card-art-resource-repository.ts`,把 `card-art.ts` 的卡牌小图/插画边框资源下载从 `bestdoriUrl + downloadFileCache` 收口到 provider-backed repository`card-art-spec.ts` 改为只生成 `/res/image/...` 相对资源路径;`card-art-resource-repository.spec.ts` 覆盖边框下载调用,本地 `/查卡 472` 图片 smoke 保持非空输出。
- 已新增 `models/costume-resource-repository.ts`,把 `Costume` 的服装详情请求和 Live2D 缩略图素材路径从 `bestdoriUrl + callAPIAndCacheResponse/downloadFile` 收口到 provider-backed repository`costume-resource-repository.spec.ts` 覆盖详情路径、`sdchara.png` 路径和非内存缓存下载调用,本地 `/查卡 472` 图片 smoke 保持演出缩略图正常输出。
- 已新增 `models/item-resource-repository.ts`,把 `Item` 的道具缩略图路径从 `bestdoriUrl + downloadFileCache` 收口到 provider-backed repository`item-resource-repository.spec.ts` 覆盖 material、star 和 common item 三类路径,本地 `/查卡池 259` 图片 smoke 输出非空。
- 已新增 `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 输出非空。
- 固化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,59 @@
import { bangDreamBestdoriProvider } from '@/qqbot/plugins/bangDream/tsugu/data-clients/bestdori-provider';
import type { BangDreamDataProvider } from '@/qqbot/plugins/bangDream/tsugu/data-clients/data-provider';
import { Server } from '@/qqbot/plugins/bangDream/tsugu/models/server';
export interface CutoffEventTopPoint {
time: number;
uid: number;
value: number;
}
export interface CutoffEventTopUser {
currentPt: number;
degrees: number[];
introduction: string;
name: string;
rank: number;
ranking: number;
sid: number;
strained: number;
uid: number;
}
export interface CutoffEventTopData {
points: CutoffEventTopPoint[];
users: CutoffEventTopUser[];
}
export class CutoffEventTopRepository {
constructor(
private readonly provider: BangDreamDataProvider = bangDreamBestdoriProvider,
) {}
/**
*
*
* @param eventId - ID
* @param server -
*/
getTopDataPath(eventId: number, server: Server): string {
return `/api/eventtop/data?server=${server}&event=${eventId}&mid=0&interval=3600000`;
}
/**
*
*
* @param eventId - ID
* @param server -
*/
async getTopData(
eventId: number,
server: Server,
): Promise<CutoffEventTopData | undefined> {
return await this.provider.getJson<CutoffEventTopData>(
this.getTopDataPath(eventId, server),
);
}
}
export const cutoffEventTopRepository = new CutoffEventTopRepository();

View File

@ -1,8 +1,11 @@
import { Server } from '@/qqbot/plugins/bangDream/tsugu/models/server';
import { Event } from '@/qqbot/plugins/bangDream/tsugu/models/event';
import { callAPIAndCacheResponse } from '@/qqbot/plugins/bangDream/tsugu/data-clients/api-cache-client';
import { bestdoriUrl } from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
import { BangDreamEventStatus } from '@/qqbot/plugins/bangDream/tsugu/models/bangdream-constants';
import {
cutoffEventTopRepository,
type CutoffEventTopPoint,
type CutoffEventTopUser,
} from '@/qqbot/plugins/bangDream/tsugu/models/cutoff-event-top-repository';
export class CutoffEventTop {
eventId: number;
@ -12,22 +15,8 @@ export class CutoffEventTop {
status: BangDreamEventStatus;
isInitfull: boolean = false;
isExist = false;
points: {
time: number;
uid: number;
value: number;
}[];
users: {
uid: number;
name: string;
introduction: string;
rank: number;
sid: number;
strained: number;
degrees: number[];
ranking: number;
currentPt: number;
}[];
points: CutoffEventTopPoint[];
users: CutoffEventTopUser[];
/**
* CutoffEventTop
*
@ -64,30 +53,17 @@ export class CutoffEventTop {
if (this.isInitfull) {
return;
}
const topData = await callAPIAndCacheResponse(
`${bestdoriUrl}/api/eventtop/data?server=${<number>this.server}&event=${this.eventId}&mid=0&interval=3600000`,
const topData = await cutoffEventTopRepository.getTopData(
this.eventId,
this.server,
);
if (topData == undefined) {
this.isExist = false;
return;
}
this.isExist = true;
this.points = topData['points'] as {
time: number;
uid: number;
value: number;
}[];
this.users = topData['users'] as {
uid: number;
name: string;
introduction: string;
rank: number;
sid: number;
strained: number;
degrees: number[];
ranking: number;
currentPt: number;
}[];
this.points = topData.points;
this.users = topData.users;
if (this.points.length == 0 || this.users.length == 0) {
//如果没有数据,返回不存在
this.isExist = false;

View File

@ -6,8 +6,7 @@ import { drawDegree } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/degree
import { Server } from '@/qqbot/plugins/bangDream/tsugu/models/server';
import { Degree } from '@/qqbot/plugins/bangDream/tsugu/models/degree';
import { drawText } from '@/qqbot/plugins/bangDream/tsugu/canvas/text';
import { downloadFileCache } from '@/qqbot/plugins/bangDream/tsugu/data-clients/asset-cache-client';
import { bestdoriUrl } from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
import { playerRankingResourceRepository } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/player-ranking-resource-repository';
interface User {
uid: number;
@ -55,9 +54,11 @@ export async function drawPlayerRankingInList(
if (user.ranking == undefined) {
return;
} else if (user.ranking > 0 && user.ranking <= 3) {
const rankIamgeBuffer = await downloadFileCache(
`${bestdoriUrl}/res/image/${Server[server]}_${user.ranking}.png`,
);
const rankIamgeBuffer =
await playerRankingResourceRepository.getRankImageBuffer(
server,
user.ranking,
);
rankingImage = await loadImage(rankIamgeBuffer);
ctx.drawImage(rankingImage, 12, 45, 45, 21);
} else {

View File

@ -0,0 +1,32 @@
import { bangDreamBestdoriProvider } from '@/qqbot/plugins/bangDream/tsugu/data-clients/bestdori-provider';
import type { BangDreamDataProvider } from '@/qqbot/plugins/bangDream/tsugu/data-clients/data-provider';
import { Server } from '@/qqbot/plugins/bangDream/tsugu/models/server';
export class PlayerRankingResourceRepository {
constructor(
private readonly provider: BangDreamDataProvider = bangDreamBestdoriProvider,
) {}
/**
*
*
* @param server -
* @param ranking -
*/
getRankImagePath(server: Server, ranking: number): string {
return `/res/image/${Server[server]}_${ranking}.png`;
}
/**
*
*
* @param server -
* @param ranking -
*/
async getRankImageBuffer(server: Server, ranking: number): Promise<Buffer> {
return await this.provider.getAsset(this.getRankImagePath(server, ranking));
}
}
export const playerRankingResourceRepository =
new PlayerRankingResourceRepository();

View File

@ -0,0 +1,41 @@
import type { BangDreamDataProvider } from '@/qqbot/plugins/bangDream/tsugu/data-clients/data-provider';
import { CutoffEventTopRepository } from '@/qqbot/plugins/bangDream/tsugu/models/cutoff-event-top-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}`),
};
}
describe('BangDream cutoff event top repository', () => {
it('builds event top data paths', () => {
const repository = new CutoffEventTopRepository(createProviderMock());
expect(repository.getTopDataPath(50, Server.cn)).toBe(
'/api/eventtop/data?server=3&event=50&mid=0&interval=3600000',
);
});
it('routes event top data requests through the provider', async () => {
const provider = createProviderMock();
provider.getJson.mockResolvedValue({
points: [{ time: 1, uid: 2, value: 3 }],
users: [],
});
const repository = new CutoffEventTopRepository(provider);
await expect(repository.getTopData(50, Server.cn)).resolves.toEqual({
points: [{ time: 1, uid: 2, value: 3 }],
users: [],
});
expect(provider.getJson).toHaveBeenCalledWith(
'/api/eventtop/data?server=3&event=50&mid=0&interval=3600000',
);
});
});

View File

@ -0,0 +1,36 @@
import type { BangDreamDataProvider } from '@/qqbot/plugins/bangDream/tsugu/data-clients/data-provider';
import { Server } from '@/qqbot/plugins/bangDream/tsugu/models/server';
import { PlayerRankingResourceRepository } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/player-ranking-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 player ranking resource repository', () => {
it('builds player ranking image paths', () => {
const repository = new PlayerRankingResourceRepository(createProviderMock());
expect(repository.getRankImagePath(Server.cn, 1)).toBe(
'/res/image/cn_1.png',
);
});
it('downloads player ranking images through the provider', async () => {
const provider = createProviderMock();
const rankBuffer = Buffer.from('rank');
provider.getAsset.mockResolvedValue(rankBuffer);
const repository = new PlayerRankingResourceRepository(provider);
await expect(repository.getRankImageBuffer(Server.cn, 1)).resolves.toBe(
rankBuffer,
);
expect(provider.getAsset).toHaveBeenCalledWith('/res/image/cn_1.png');
});
});