diff --git a/docs/qqbot-bangdream-tsugu-global-refactor-plan.md b/docs/qqbot-bangdream-tsugu-global-refactor-plan.md index 4d63ec5..15f8fcc 100644 --- a/docs/qqbot-bangdream-tsugu-global-refactor-plan.md +++ b/docs/qqbot-bangdream-tsugu-global-refactor-plan.md @@ -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/.spec.ts ...`,路径用正斜杠。 ### Phase 4:搜索 specification 和 matcher diff --git a/src/qqbot/plugins/bangDream/tsugu/models/player-data-repository.ts b/src/qqbot/plugins/bangDream/tsugu/models/player-data-repository.ts new file mode 100644 index 0000000..d9beb43 --- /dev/null +++ b/src/qqbot/plugins/bangDream/tsugu/models/player-data-repository.ts @@ -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 | 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 { + const cacheTime = useCache ? Infinity : 0; + const path = this.getDetailPath(playerId, server, mode); + const playerData = await this.provider.getJson(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(); diff --git a/src/qqbot/plugins/bangDream/tsugu/models/player.ts b/src/qqbot/plugins/bangDream/tsugu/models/player.ts index 70bab34..a355c29 100644 --- a/src/qqbot/plugins/bangDream/tsugu/models/player.ts +++ b/src/qqbot/plugins/bangDream/tsugu/models/player.ts @@ -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 ( diff --git a/test/qqbot/plugins/bangDream/tsugu/player-data-repository.spec.ts b/test/qqbot/plugins/bangDream/tsugu/player-data-repository.spec.ts new file mode 100644 index 0000000..cdc5270 --- /dev/null +++ b/test/qqbot/plugins/bangDream/tsugu/player-data-repository.spec.ts @@ -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 { + 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 }, + ); + }); +});