import { Injectable, NotFoundException, BadRequestException, } from '@nestjs/common'; import { nanoid } from 'nanoid'; import { ShareConfigRepository } from './repositories/share-config.repository'; import { ShareParticipantRepository } from './repositories/share-participant.repository'; import { ShareLevelProgressRepository } from './repositories/share-level-progress.repository'; import { LevelRepository } from '../wechat-game/repositories/level.repository'; import { pickLevelImageFields } from '../wechat-game/level-fields.helper'; import { CreateShareDto } from './dto/create-share.dto'; import { ReportLevelProgressDto } from './dto/report-level-progress.dto'; import { CreateShareResponseDto, CreatedShareListResponseDto, JoinShareResponseDto, ShareLevelDto, } from './dto/share-response.dto'; import { ReportLevelProgressResponseDto } from './dto/share-level-progress-response.dto'; @Injectable() export class ShareService { constructor( private readonly shareConfigRepository: ShareConfigRepository, private readonly shareParticipantRepository: ShareParticipantRepository, private readonly levelRepository: LevelRepository, private readonly shareLevelProgressRepository: ShareLevelProgressRepository, ) {} async createShare( userId: string, dto: CreateShareDto, ): Promise { const uniqueIds = [...new Set(dto.levelIds)]; if (uniqueIds.length !== 6) { throw new BadRequestException('关卡ID不能重复,需要恰好6个不同的关卡'); } // 单次查询验证所有关卡存在 const levels = await this.levelRepository.findByIds(uniqueIds); if (levels.length !== uniqueIds.length) { const foundIds = new Set(levels.map((l) => l.id)); const missing = uniqueIds.filter((id) => !foundIds.has(id)); throw new NotFoundException(`以下关卡不存在: ${missing.join(', ')}`); } // 生成 8 位分享码(碰撞重试) let shareCode: string; let attempts = 0; do { shareCode = nanoid(8); const existing = await this.shareConfigRepository.findByShareCode(shareCode); if (!existing) break; attempts++; } while (attempts < 3); if (attempts >= 3) { throw new BadRequestException('生成分享码失败,请重试'); } const config = await this.shareConfigRepository.create({ shareCode, title: dto.title, sharerId: userId, levelIds: dto.levelIds, }); return { shareCode: config.shareCode, title: config.title, levelCount: config.levelIds.length, }; } async joinShare(userId: string, code: string): Promise { const config = await this.shareConfigRepository.findByShareCode(code); if (!config) { throw new NotFoundException('分享不存在或已过期'); } if (userId !== config.sharerId) { await this.shareParticipantRepository.addParticipant(config.id, userId); } // Single query, then reorder to match levelIds sequence const allLevels = await this.levelRepository.findByIds(config.levelIds); const levelMap = new Map(allLevels.map((l) => [l.id, l])); const levels: ShareLevelDto[] = config.levelIds.map((id, index) => { const level = levelMap.get(id); if (!level) { throw new NotFoundException(`关卡 ${id} 不存在`); } return { id: level.id, level: index + 1, ...pickLevelImageFields(level), answer: level.answer, sortOrder: level.sortOrder, }; }); return { shareCode: config.shareCode, title: config.title, levels, }; } async getCreatedShares(userId: string): Promise { const configs = await this.shareConfigRepository.findBySharerId(userId); if (configs.length === 0) { return { items: [] }; } const shareConfigIds = configs.map((config) => config.id); const [participantCountMap, rankingRows] = await Promise.all([ this.shareParticipantRepository.countByShareConfigIds(shareConfigIds), this.shareLevelProgressRepository.summarizeByShareConfigIds( shareConfigIds, ), ]); const rankingsByShareConfigId = new Map(); for (const config of configs) { const completedRankings = rankingRows .filter( (row) => row.shareConfigId === config.id && Number(row.passedLevelCount) === config.levelIds.length, ) .sort((a, b) => { const totalTimeDiff = Number(a.totalTimeSpent) - Number(b.totalTimeSpent); if (totalTimeDiff !== 0) { return totalTimeDiff; } return a.participantId.localeCompare(b.participantId); }) .map((row) => row.participantId); rankingsByShareConfigId.set(config.id, completedRankings); } return { items: configs.map((config) => { const rankings = rankingsByShareConfigId.get(config.id) ?? []; const rankingIndex = rankings.findIndex( (participantId) => participantId === userId, ); return { id: config.id, shareCode: config.shareCode, title: config.title, levelCount: config.levelIds.length, participantCount: participantCountMap.get(config.id) ?? 0, userRank: rankingIndex >= 0 ? rankingIndex + 1 : null, createdAt: config.createdAt.toISOString(), }; }), }; } async reportLevelProgress( userId: string, dto: ReportLevelProgressDto, ): Promise { const [config, level] = await Promise.all([ this.shareConfigRepository.findByShareCode(dto.shareCode), this.levelRepository.findById(dto.levelId), ]); if (!config) { throw new NotFoundException('分享不存在或已过期'); } if (!level) { throw new NotFoundException('关卡不存在'); } if (!config.levelIds.includes(dto.levelId)) { throw new BadRequestException('该关卡不属于此分享挑战'); } let participant = await this.shareParticipantRepository.findByShareConfigAndParticipant( config.id, userId, ); if (!participant) { participant = this.shareParticipantRepository.create({ shareConfigId: config.id, participantId: userId, }); participant = await this.shareParticipantRepository.save(participant); } const progress = await this.shareLevelProgressRepository.findByParticipantAndLevel( participant.id, dto.levelId, ); if (dto.passed && progress?.passed) { return { passed: true, timeLimit: level.timeLimit, withinTimeLimit: this.isWithinTimeLimit( level.timeLimit, progress.timeSpent, ), }; } const withinTimeLimit = dto.passed ? this.isWithinTimeLimit(level.timeLimit, dto.timeSpent) : false; const updatedProgress = progress ? Object.assign(this.shareLevelProgressRepository.create(progress), { passed: dto.passed, timeSpent: dto.timeSpent, completedAt: dto.passed ? new Date() : progress.completedAt, }) : this.shareLevelProgressRepository.create({ participantId: participant.id, levelId: dto.levelId, passed: dto.passed, timeSpent: dto.timeSpent, completedAt: dto.passed ? new Date() : null, }); await this.shareLevelProgressRepository.save(updatedProgress); return { passed: dto.passed, timeLimit: level.timeLimit, withinTimeLimit, }; } private isWithinTimeLimit( timeLimit: number | null, timeSpent: number, ): boolean { return timeLimit === null || timeSpent <= timeLimit; } }