Files
MemeMind-Server/src/modules/share/share.service.ts
2026-04-19 13:27:10 +08:00

252 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<CreateShareResponseDto> {
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<JoinShareResponseDto> {
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<CreatedShareListResponseDto> {
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<string, string[]>();
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<ReportLevelProgressResponseDto> {
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;
}
}