forked from xiangwang25/whale-town-end-v2
feat: add task reward economy APIs
This commit is contained in:
147
src/business/tasks/task_progress_memory.service.ts
Normal file
147
src/business/tasks/task_progress_memory.service.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
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<string, MemoryProgressRow>();
|
||||
|
||||
constructor(@Inject('IUserWalletsService') private readonly walletService: IUserWalletsService) {}
|
||||
|
||||
async getBoard(userId: bigint): Promise<TaskBoardPayload> {
|
||||
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<TaskBoardPayload> {
|
||||
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<TaskClaimResult> {
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user