refactor: 收口 BangDream 玩家数据请求

This commit is contained in:
sunlei 2026-06-07 04:03:03 +08:00
parent d429b799da
commit 68b55764c2
4 changed files with 162 additions and 23 deletions

View File

@ -313,6 +313,7 @@ export interface TsuguHook {
- 已新增 `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` 这类指定服务器查询出现横幅和背景不一致。
- 已新增 `models/player-data-repository.ts`,把 `Player``/api/player/{server}/{playerId}` 详情请求、mode/cacheTime 策略和 mode=1 后台刷新行为从领域模型中收口到 provider-backed repository`player-data-repository.spec.ts` 覆盖玩家详情路径、缓存参数和后台刷新调用,本地 `/查玩家 26591455 jp` 图片 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,78 @@
import { bangDreamBestdoriProvider } from '@/qqbot/plugins/bangDream/tsugu/data-clients/bestdori-provider';
import type { BangDreamDataProvider } from '@/qqbot/plugins/bangDream/tsugu/data-clients/data-provider';
import { logger } from '@/qqbot/plugins/bangDream/tsugu/runtime/logger';
import { Server } from './server';
export type PlayerDetailMode = 0 | 1 | 2 | 3;
export interface PlayerDetailResponse {
data?: {
cache?: boolean;
profile?: Record<string, any> | null;
time?: number;
};
result?: boolean;
}
export class PlayerDataRepository {
constructor(
private readonly provider: BangDreamDataProvider = bangDreamBestdoriProvider,
) {}
/**
* API
*
* @param playerId - ID
* @param server -
* @param mode - Bestdori
*/
getDetailPath(playerId: number, server: Server, mode: PlayerDetailMode) {
return `/api/player/${Server[server]}/${playerId}?mode=${mode}`;
}
/**
* 1
*
* @param playerId - ID
* @param server -
* @param useCache -
* @param mode - Bestdori
*/
async getDetail(
playerId: number,
server: Server,
useCache: boolean,
mode: PlayerDetailMode,
): Promise<PlayerDetailResponse> {
const cacheTime = useCache ? Infinity : 0;
const path = this.getDetailPath(playerId, server, mode);
const playerData = await this.provider.getJson<PlayerDetailResponse>(path, {
cacheTime,
retryCount: 1,
});
if (mode === 1 && !Number.isFinite(cacheTime)) {
this.refreshCache(playerId, server, mode);
}
return playerData;
}
/**
*
*
* @param playerId - ID
* @param server -
* @param mode - Bestdori
*/
refreshCache(playerId: number, server: Server, mode: PlayerDetailMode): void {
this.provider
.getJson(this.getDetailPath(playerId, server, mode), {
cacheTime: 300,
retryCount: 1,
})
.catch((err) => {
logger('InitPlayer', err);
});
}
}
export const playerDataRepository = new PlayerDataRepository();

View File

@ -1,4 +1,3 @@
import { bangDreamBestdoriProvider } from '@/qqbot/plugins/bangDream/tsugu/data-clients/bestdori-provider';
import { Server } from '@/qqbot/plugins/bangDream/tsugu/models/server';
import {
Card,
@ -8,7 +7,11 @@ import {
import { AreaItem } from '@/qqbot/plugins/bangDream/tsugu/models/area-item';
import { Event } from '@/qqbot/plugins/bangDream/tsugu/models/event';
import { difficultyNameList } from '@/qqbot/plugins/bangDream/tsugu/models/song';
import { logger } from '@/qqbot/plugins/bangDream/tsugu/runtime/logger';
import {
playerDataRepository,
type PlayerDataRepository,
type PlayerDetailMode,
} from './player-data-repository';
/*
- mode=0
@ -185,7 +188,11 @@ export class Player {
* @param playerId - ID参数
* @param server -
*/
constructor(playerId: number, server: Server) {
constructor(
playerId: number,
server: Server,
private readonly dataRepository: PlayerDataRepository = playerDataRepository,
) {
this.playerId = playerId;
this.server = server;
}
@ -195,43 +202,34 @@ export class Player {
* @param useCache - use缓存参数使
* @param mode - mode参数使
*/
async initFull(useCache: boolean = false, mode: 0 | 1 | 2 | 3 = 2) {
async initFull(useCache: boolean = false, mode: PlayerDetailMode = 2) {
if (this.isInitfull) {
return;
}
const cacheTime = useCache ? 1 / 0 : 0;
let playerData;
try {
playerData = await bangDreamBestdoriProvider.getJson(
`/api/player/${Server[this.server]}/${this.playerId}?mode=${mode}`,
{ cacheTime, retryCount: 1 },
playerData = await this.dataRepository.getDetail(
this.playerId,
this.server,
useCache,
mode,
);
if (mode == 1 && !isFinite(cacheTime)) {
// 后台更新玩家信息,确保下一次用最新的缓存去显示
bangDreamBestdoriProvider
.getJson(
`/api/player/${Server[this.server]}/${this.playerId}?mode=${mode}`,
{ cacheTime: 300, retryCount: 1 },
)
.catch((err) => {
logger('InitPlayer', err);
});
}
} catch {
this.isExist = false;
this.initError = true;
return;
}
if (!playerData['result'] || playerData['data']['profile'] == null) {
const responseData = playerData.data;
if (!playerData.result || responseData?.profile == null) {
this.isExist = false;
this.initError = true;
return;
}
this.isExist = true;
this.cache = playerData['data']['cache'];
this.time = playerData['data']['time'];
this.profile = playerData['data']['profile'];
this.cache = responseData.cache as boolean;
this.time = responseData.time as number;
this.profile = responseData.profile as typeof this.profile;
//卡牌列表
this.profile.cardList = [];
for (

View File

@ -0,0 +1,62 @@
import type { BangDreamDataProvider } from '@/qqbot/plugins/bangDream/tsugu/data-clients/data-provider';
import { PlayerDataRepository } from '@/qqbot/plugins/bangDream/tsugu/models/player-data-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 player data repository', () => {
it('builds player detail paths from server and mode', () => {
const repository = new PlayerDataRepository(createProviderMock());
expect(repository.getDetailPath(26591455, Server.jp, 3)).toBe(
'/api/player/jp/26591455?mode=3',
);
});
it('routes player detail requests through provider cache policy', async () => {
const provider = createProviderMock();
provider.getJson.mockResolvedValue({ result: true });
const repository = new PlayerDataRepository(provider);
await repository.getDetail(26591455, Server.jp, false, 3);
await repository.getDetail(26591455, Server.jp, true, 0);
expect(provider.getJson).toHaveBeenNthCalledWith(
1,
'/api/player/jp/26591455?mode=3',
{ cacheTime: 0, retryCount: 1 },
);
expect(provider.getJson).toHaveBeenNthCalledWith(
2,
'/api/player/jp/26591455?mode=0',
{ cacheTime: Infinity, retryCount: 1 },
);
});
it('keeps Bestdori mode 1 background refresh behavior', async () => {
const provider = createProviderMock();
provider.getJson.mockResolvedValue({ result: true });
const repository = new PlayerDataRepository(provider);
await repository.getDetail(26591455, Server.jp, true, 1);
expect(provider.getJson).toHaveBeenNthCalledWith(
1,
'/api/player/jp/26591455?mode=1',
{ cacheTime: Infinity, retryCount: 1 },
);
expect(provider.getJson).toHaveBeenNthCalledWith(
2,
'/api/player/jp/26591455?mode=1',
{ cacheTime: 300, retryCount: 1 },
);
});
});