import { BadRequestException, ConflictException, Inject, Injectable } from '@nestjs/common'; import { PlayerWalletPayload } from '../player/player.types'; import { getCurrentWeeklyCycle, getTaskCycleKey, getTaskDefinitions, NEWBIE_CYCLE_KEY, TaskActivityType, TaskDefinition, TaskProgressState, WEEKLY_COMPLETION_BONUS, WEEKLY_TASKS, } from './task_catalog'; import { buildTaskBoard, TaskBoardPayload, TaskClaimResult, TaskProgressRow, TaskProgressStore } from './tasks.types'; interface IUserWalletsService { earn(userId: bigint, amount: number, referenceType: string, referenceId: string, note?: string): Promise<{ wallet: { balance: number } }>; } interface MemoryProgressRow extends TaskProgressRow { user_id: bigint; created_at: Date; updated_at: Date; } @Injectable() export class TaskProgressMemoryService implements TaskProgressStore { private readonly rows = new Map(); constructor(@Inject('IUserWalletsService') private readonly walletService: IUserWalletsService) {} async getBoard(userId: bigint): Promise { const cycle = getCurrentWeeklyCycle(); const rows = this.ensureRows(userId, cycle.key); this.syncWeeklyBonus(rows); return buildTaskBoard(rows, cycle); } async recordActivity(userId: bigint, activity: TaskActivityType, targetId?: string): Promise { const cycle = getCurrentWeeklyCycle(); const rows = this.ensureRows(userId, cycle.key); for (const definition of getTaskDefinitions()) { if (definition.bonus || definition.activity !== activity) continue; const row = this.findRow(rows, definition, cycle.key); this.applyActivity(definition, row, targetId); } this.syncWeeklyBonus(rows); return buildTaskBoard(rows, cycle); } async claim(userId: bigint, taskId: string): Promise { const cycle = getCurrentWeeklyCycle(); const rows = this.ensureRows(userId, cycle.key); this.syncWeeklyBonus(rows); const definition = getTaskDefinitions().find((item) => item.id === taskId); if (!definition) { throw new BadRequestException('任务不存在'); } const row = this.findRow(rows, definition, cycle.key); if (!row.completed_at) { throw new BadRequestException('任务尚未完成'); } if (row.claimed_at) { throw new ConflictException('任务奖励已领取'); } const result = await this.walletService.earn( userId, definition.reward, 'task_reward', `${getTaskCycleKey(definition, cycle)}:${definition.id}`, `任务奖励:${definition.title}`, ); row.claimed_at = new Date(); row.updated_at = new Date(); const wallet: PlayerWalletPayload = { user_id: userId.toString(), balance: result.wallet.balance, currency: 'whale_coin', }; return { board: buildTaskBoard(rows, cycle), wallet }; } private ensureRows(userId: bigint, weeklyCycleKey: string): MemoryProgressRow[] { for (const definition of getTaskDefinitions()) { const cycleKey = definition.group === 'weekly' ? weeklyCycleKey : NEWBIE_CYCLE_KEY; const key = this.rowKey(userId, definition.id, cycleKey); if (!this.rows.has(key)) { const now = new Date(); this.rows.set(key, { user_id: userId, task_id: definition.id, cycle_key: cycleKey, progress: 0, activity_state: {}, completed_at: null, claimed_at: null, created_at: now, updated_at: now, }); } } return getTaskDefinitions().map((definition) => { const cycleKey = definition.group === 'weekly' ? weeklyCycleKey : NEWBIE_CYCLE_KEY; return this.rows.get(this.rowKey(userId, definition.id, cycleKey)) as MemoryProgressRow; }); } private findRow(rows: MemoryProgressRow[], definition: TaskDefinition, weeklyCycleKey: string): MemoryProgressRow { const cycleKey = definition.group === 'weekly' ? weeklyCycleKey : NEWBIE_CYCLE_KEY; const row = rows.find((item) => item.task_id === definition.id && item.cycle_key === cycleKey); if (!row) throw new Error(`任务进度缺失: ${definition.id}`); return row; } private applyActivity(definition: TaskDefinition, row: MemoryProgressRow, targetId?: string): void { if (row.completed_at) return; if (definition.allowed_targets && (!targetId || !definition.allowed_targets.includes(targetId))) return; if (definition.progress_mode === 'unique_target') { if (!targetId) return; const state = row.activity_state as TaskProgressState; const targets = Array.isArray(state.targets) ? state.targets.filter((item): item is string => typeof item === 'string') : []; if (targets.includes(targetId)) return; targets.push(targetId); row.activity_state = { ...state, targets }; row.progress = Math.min(definition.target, targets.length); } else { row.progress = Math.min(definition.target, row.progress + 1); } if (row.progress >= definition.target) row.completed_at = new Date(); row.updated_at = new Date(); } private syncWeeklyBonus(rows: MemoryProgressRow[]): void { const bonus = rows.find((row) => row.task_id === WEEKLY_COMPLETION_BONUS.id); if (!bonus || bonus.completed_at) return; const complete = WEEKLY_TASKS.every((definition) => rows.some((row) => row.task_id === definition.id && row.completed_at)); if (complete) { bonus.progress = 1; bonus.completed_at = new Date(); bonus.updated_at = new Date(); } } private rowKey(userId: bigint, taskId: string, cycleKey: string): string { return `${userId.toString()}:${taskId}:${cycleKey}`; } }