feat(share): 分享挑战关卡进度记录功能
- 新增 Level.timeLimit 字段支持关卡时间限制 - 新增 ShareLevelProgress 实体记录单关通关进度 - 新增 ShareLevelProgressRepository - 新增 DTO: ReportLevelProgressDto, ReportLevelProgressResponseDto - 新增 POST /v1/share/progress 接口用于上报进度 - 支持仅首次通关有效判断 - 支持时间限制内通关判断 - 不可变模式更新进度记录 - 数据库迁移脚本 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,509 @@
|
|||||||
|
# 分享挑战关卡进度记录功能实现计划
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** 实现用户在分享挑战中的单关通关进度上报功能,支持记录通关时间、是否通过、时间限制判断。
|
||||||
|
|
||||||
|
**Architecture:** 在现有 Share 模块基础上,新增 ShareLevelProgress 实体和 Repository,通过 ShareService.reportLevelProgress 方法处理业务逻辑,在 ShareController 新增 POST /v1/share/progress 接口。
|
||||||
|
|
||||||
|
**Tech Stack:** NestJS, TypeORM, MySQL
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 文件清单
|
||||||
|
|
||||||
|
| 操作 | 文件路径 |
|
||||||
|
|------|----------|
|
||||||
|
| 新增 | src/modules/share/entities/share-level-progress.entity.ts |
|
||||||
|
| 新增 | src/modules/share/repositories/share-level-progress.repository.ts |
|
||||||
|
| 新增 | src/modules/share/dto/report-level-progress.dto.ts |
|
||||||
|
| 新增 | src/modules/share/dto/share-level-progress-response.dto.ts |
|
||||||
|
| 修改 | src/modules/share/repositories/share-participant.repository.ts |
|
||||||
|
| 修改 | src/modules/share/share.service.ts |
|
||||||
|
| 修改 | src/modules/share/share.controller.ts |
|
||||||
|
| 修改 | src/modules/share/share.module.ts |
|
||||||
|
| 修改 | src/modules/wechat-game/entities/level.entity.ts |
|
||||||
|
| 修改 | src/database/migrations/*.sql |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: Level 实体增加 timeLimit 字段
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/modules/wechat-game/entities/level.entity.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 修改 Level 实体**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/modules/wechat-game/entities/level.entity.ts
|
||||||
|
// 在 sortOrder 字段后添加
|
||||||
|
|
||||||
|
@Column({ type: 'int', name: 'time_limit', nullable: true, default: null })
|
||||||
|
timeLimit!: number | null;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/modules/wechat-game/entities/level.entity.ts
|
||||||
|
git commit -m "feat(level): add timeLimit field for level time restriction"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: 创建 ShareLevelProgress 实体
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/modules/share/entities/share-level-progress.entity.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 创建实体文件**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
ManyToOne,
|
||||||
|
JoinColumn,
|
||||||
|
Index,
|
||||||
|
Unique,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { ShareParticipant } from './share-participant.entity';
|
||||||
|
import { Level } from '../../wechat-game/entities/level.entity';
|
||||||
|
|
||||||
|
@Entity('share_level_progress')
|
||||||
|
@Unique('uq_participant_level', ['participantId', 'levelId'])
|
||||||
|
export class ShareLevelProgress {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@Index('idx_slp_participant_id')
|
||||||
|
@Column({ type: 'char', length: 36, name: 'participant_id' })
|
||||||
|
participantId!: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => ShareParticipant)
|
||||||
|
@JoinColumn({ name: 'participant_id' })
|
||||||
|
participant!: ShareParticipant;
|
||||||
|
|
||||||
|
@Index('idx_slp_level_id')
|
||||||
|
@Column({ type: 'char', length: 191, name: 'level_id' })
|
||||||
|
levelId!: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => Level)
|
||||||
|
@JoinColumn({ name: 'level_id' })
|
||||||
|
level!: Level;
|
||||||
|
|
||||||
|
@Column({ type: 'tinyint', width: 1, default: 0 })
|
||||||
|
passed!: boolean;
|
||||||
|
|
||||||
|
@Column({ type: 'int', default: 0, name: 'time_spent' })
|
||||||
|
timeSpent!: number;
|
||||||
|
|
||||||
|
@CreateDateColumn({ name: 'completed_at' })
|
||||||
|
completedAt!: Date;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/modules/share/entities/share-level-progress.entity.ts
|
||||||
|
git commit -m "feat(share): add ShareLevelProgress entity"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: 创建 ShareLevelProgressRepository
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/modules/share/repositories/share-level-progress.repository.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 创建 Repository**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { ShareLevelProgress } from '../entities/share-level-progress.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ShareLevelProgressRepository {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(ShareLevelProgress)
|
||||||
|
private readonly repository: Repository<ShareLevelProgress>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async findByParticipantId(participantId: string): Promise<ShareLevelProgress[]> {
|
||||||
|
return this.repository.find({ where: { participantId } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByParticipantAndLevel(
|
||||||
|
participantId: string,
|
||||||
|
levelId: string,
|
||||||
|
): Promise<ShareLevelProgress | null> {
|
||||||
|
return this.repository.findOne({ where: { participantId, levelId } });
|
||||||
|
}
|
||||||
|
|
||||||
|
create(data: Partial<ShareLevelProgress>): ShareLevelProgress {
|
||||||
|
return this.repository.create(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async save(progress: ShareLevelProgress): Promise<ShareLevelProgress> {
|
||||||
|
return this.repository.save(progress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/modules/share/repositories/share-level-progress.repository.ts
|
||||||
|
git commit -m "feat(share): add ShareLevelProgressRepository"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4: 创建 DTO 文件
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/modules/share/dto/report-level-progress.dto.ts`
|
||||||
|
- Create: `src/modules/share/dto/share-level-progress-response.dto.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 创建 ReportLevelProgressDto**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsBoolean, IsNotEmpty, IsNumber, IsString, Min } from 'class-validator';
|
||||||
|
|
||||||
|
export class ReportLevelProgressDto {
|
||||||
|
@ApiProperty({ description: '分享码' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
shareCode!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '关卡 ID' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
levelId!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '是否通过' })
|
||||||
|
@IsBoolean()
|
||||||
|
passed!: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '通关时间(秒)' })
|
||||||
|
@IsNumber()
|
||||||
|
@Min(0)
|
||||||
|
timeSpent!: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 创建 ReportLevelProgressResponseDto**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class ReportLevelProgressResponseDto {
|
||||||
|
@ApiProperty({ description: '是否通过' })
|
||||||
|
passed!: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '该关卡时间限制(秒),null 表示无限制' })
|
||||||
|
timeLimit!: number | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '是否在时间限制内通过' })
|
||||||
|
withinTimeLimit!: boolean;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/modules/share/dto/report-level-progress.dto.ts src/modules/share/dto/share-level-progress-response.dto.ts
|
||||||
|
git commit -m "feat(share): add DTOs for level progress reporting"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5: ShareParticipantRepository 补充方法
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/modules/share/repositories/share-participant.repository.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 读取现有文件确认内容**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/modules/share/repositories/share-participant.repository.ts
|
||||||
|
// 在现有方法后添加 findByShareConfigAndParticipant 方法
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 添加新方法**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async findByShareConfigAndParticipant(
|
||||||
|
shareConfigId: string,
|
||||||
|
participantId: string,
|
||||||
|
): Promise<ShareParticipant | null> {
|
||||||
|
return this.repository.findOne({ where: { shareConfigId, participantId } });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/modules/share/repositories/share-participant.repository.ts
|
||||||
|
git commit -m "feat(share): add findByShareConfigAndParticipant to ShareParticipantRepository"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 6: ShareService 新增 reportLevelProgress 方法
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/modules/share/share.service.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 读取现有文件确认 import 和 constructor**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 需要新增的 imports
|
||||||
|
import { ShareLevelProgressRepository } from './repositories/share-level-progress.repository';
|
||||||
|
import { ReportLevelProgressDto } from './dto/report-level-progress.dto';
|
||||||
|
import { ReportLevelProgressResponseDto } from './dto/share-level-progress-response.dto';
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 在 ShareService 中添加方法**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async reportLevelProgress(
|
||||||
|
userId: string,
|
||||||
|
dto: ReportLevelProgressDto,
|
||||||
|
): Promise<ReportLevelProgressResponseDto> {
|
||||||
|
// 1. 查找分享配置
|
||||||
|
const config = await this.shareConfigRepository.findByShareCode(dto.shareCode);
|
||||||
|
if (!config) {
|
||||||
|
throw new NotFoundException('分享不存在或已过期');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 查找或创建 ShareParticipant
|
||||||
|
let participant = await this.shareParticipantRepository.findByShareConfigAndParticipant(
|
||||||
|
config.id,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
if (!participant) {
|
||||||
|
participant = await this.shareParticipantRepository.create({
|
||||||
|
shareConfigId: config.id,
|
||||||
|
participantId: userId,
|
||||||
|
});
|
||||||
|
participant = await this.shareParticipantRepository.save(participant);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 如果 passed=true,检查是否已有通关记录
|
||||||
|
if (dto.passed) {
|
||||||
|
const existing = await this.shareLevelProgressRepository.findByParticipantAndLevel(
|
||||||
|
participant.id,
|
||||||
|
dto.levelId,
|
||||||
|
);
|
||||||
|
if (existing?.passed) {
|
||||||
|
return {
|
||||||
|
passed: true,
|
||||||
|
timeLimit: null,
|
||||||
|
withinTimeLimit: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 查找关卡获取时间限制
|
||||||
|
const level = await this.levelRepository.findById(dto.levelId);
|
||||||
|
if (!level) {
|
||||||
|
throw new NotFoundException('关卡不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 判断是否在时间限制内通过
|
||||||
|
const withinTimeLimit = dto.passed
|
||||||
|
? level.timeLimit === null || dto.timeSpent <= level.timeLimit
|
||||||
|
: false;
|
||||||
|
|
||||||
|
// 6. 创建或更新进度
|
||||||
|
let progress = await this.shareLevelProgressRepository.findByParticipantAndLevel(
|
||||||
|
participant.id,
|
||||||
|
dto.levelId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (progress) {
|
||||||
|
progress.passed = dto.passed;
|
||||||
|
progress.timeSpent = dto.timeSpent;
|
||||||
|
if (dto.passed) {
|
||||||
|
progress.completedAt = new Date();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
progress = 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(progress);
|
||||||
|
|
||||||
|
return {
|
||||||
|
passed: dto.passed,
|
||||||
|
timeLimit: level.timeLimit,
|
||||||
|
withinTimeLimit,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/modules/share/share.service.ts
|
||||||
|
git commit -m "feat(share): add reportLevelProgress method"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 7: ShareController 新增接口
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/modules/share/share.controller.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 添加 import**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ReportLevelProgressDto } from './dto/report-level-progress.dto';
|
||||||
|
import { ReportLevelProgressResponseDto } from './dto/share-level-progress-response.dto';
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 添加 Controller 方法**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@Post('progress')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '上报单关进度',
|
||||||
|
description: '用户在分享挑战中通关后上报进度,仅首次通关(passed=true)有效',
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 200, description: '成功' })
|
||||||
|
@ApiResponse({ status: 404, description: '分享或关卡不存在' })
|
||||||
|
async reportLevelProgress(
|
||||||
|
@CurrentUser() user: JwtPayload,
|
||||||
|
@Body() dto: ReportLevelProgressDto,
|
||||||
|
): Promise<ApiResponseDto<ReportLevelProgressResponseDto>> {
|
||||||
|
const data = await this.shareService.reportLevelProgress(user.sub, dto);
|
||||||
|
return ApiResponseDto.success(data);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/modules/share/share.controller.ts
|
||||||
|
git commit -m "feat(share): add POST /v1/share/progress endpoint"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 8: ShareModule 更新
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/modules/share/share.module.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 修改 import 和 providers**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ShareLevelProgress } from './entities/share-level-progress.entity';
|
||||||
|
import { ShareLevelProgressRepository } from './repositories/share-level-progress.repository';
|
||||||
|
|
||||||
|
// TypeOrmModule.forFeature 中添加 ShareLevelProgress
|
||||||
|
TypeOrmModule.forFeature([ShareConfig, ShareParticipant, ShareLevelProgress]),
|
||||||
|
|
||||||
|
// providers 中添加 ShareLevelProgressRepository
|
||||||
|
providers: [
|
||||||
|
ShareService,
|
||||||
|
ShareConfigRepository,
|
||||||
|
ShareParticipantRepository,
|
||||||
|
ShareLevelProgressRepository,
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/modules/share/share.module.ts
|
||||||
|
git commit -m "feat(share): register ShareLevelProgress in ShareModule"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 9: 数据库迁移 SQL
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/database/migrations/002_add_share_level_progress.sql`(或按项目规范)
|
||||||
|
|
||||||
|
- [ ] **Step 1: 创建迁移 SQL**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Level 表增加 time_limit 字段
|
||||||
|
ALTER TABLE levels
|
||||||
|
ADD COLUMN time_limit INT DEFAULT NULL COMMENT '通关时间限制(秒),NULL 表示无限制'
|
||||||
|
AFTER sort_order;
|
||||||
|
|
||||||
|
-- 新建 share_level_progress 表
|
||||||
|
CREATE TABLE IF NOT EXISTS share_level_progress (
|
||||||
|
id CHAR(36) PRIMARY KEY,
|
||||||
|
participant_id CHAR(36) NOT NULL COMMENT '关联 share_participants.id',
|
||||||
|
level_id CHAR(191) NOT NULL COMMENT '关卡ID',
|
||||||
|
passed TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否通过',
|
||||||
|
time_spent INT NOT NULL DEFAULT 0 COMMENT '通关时间(秒)',
|
||||||
|
completed_at DATETIME DEFAULT NULL COMMENT '通关时间戳',
|
||||||
|
|
||||||
|
UNIQUE KEY uq_participant_level (participant_id, level_id),
|
||||||
|
INDEX idx_slp_participant_id (participant_id),
|
||||||
|
INDEX idx_slp_level_id (level_id),
|
||||||
|
|
||||||
|
FOREIGN KEY (participant_id) REFERENCES share_participants(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/database/migrations/002_add_share_level_progress.sql
|
||||||
|
git commit -m "chore: add migration for share level progress tables"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 10: 编译验证
|
||||||
|
|
||||||
|
- [ ] **Step 1: 运行 TypeScript 编译检查**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/richard/Documents/code/xieyingeng/MemeMind-Server && npx tsc --noEmit
|
||||||
|
```
|
||||||
|
|
||||||
|
预期:无编译错误
|
||||||
|
|
||||||
|
- [ ] **Step 2: 如果有错误,修复后重新编译**
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit(如果有代码修改)**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 自检清单
|
||||||
|
|
||||||
|
完成实现后,对照设计文档检查:
|
||||||
|
|
||||||
|
- [ ] Level 实体有 `timeLimit` 字段
|
||||||
|
- [ ] ShareLevelProgress 实体有 `participantId`, `levelId`, `passed`, `timeSpent`, `completedAt` 字段
|
||||||
|
- [ ] ShareLevelProgress 有唯一索引 `(participantId, levelId)`
|
||||||
|
- [ ] ShareLevelProgressRepository 有 `findByParticipantId`, `findByParticipantAndLevel`, `create`, `save` 方法
|
||||||
|
- [ ] ShareParticipantRepository 有 `findByShareConfigAndParticipant` 方法
|
||||||
|
- [ ] ShareService.reportLevelProgress 实现了完整业务逻辑
|
||||||
|
- [ ] Controller 接口为 `POST /v1/share/progress`
|
||||||
|
- [ ] DTO 包含 `shareCode`, `levelId`, `passed`, `timeSpent`
|
||||||
|
- [ ] 响应 DTO 包含 `passed`, `timeLimit`, `withinTimeLimit`
|
||||||
|
- [ ] ShareModule 注册了 ShareLevelProgress 实体和 Repository
|
||||||
|
- [ ] 数据库迁移 SQL 包含 Level 表修改和 share_level_progress 表创建
|
||||||
174
docs/superpowers/specs/2026-04-07-share-level-progress-design.md
Normal file
174
docs/superpowers/specs/2026-04-07-share-level-progress-design.md
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
# 分享挑战关卡进度记录功能设计
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
现有好友挑战分享能力支持用户加入分享挑战并获取6个关卡的数据。需要在分享挑战场景下记录用户的单关通关进度,为后续的排行统计和规定时间内通关统计提供数据基础。
|
||||||
|
|
||||||
|
## 需求理解
|
||||||
|
|
||||||
|
1. **记录用户在分享挑战下的单关通关数据**(通关时间、是否通过)
|
||||||
|
2. **不包含积分**(积分已有独立体系)
|
||||||
|
3. **仅首次通关有效**(passed=true 时不可覆盖)
|
||||||
|
4. **每个关卡有独立时间限制**(存储在 Level 表的 `timeLimit` 字段)
|
||||||
|
5. **同一关卡在不同分享挑战中分别记录**
|
||||||
|
|
||||||
|
## 数据库设计
|
||||||
|
|
||||||
|
### 1. Level 表扩展
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ALTER TABLE levels ADD COLUMN time_limit INT DEFAULT NULL COMMENT '通关时间限制(秒),NULL 表示无限制';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 新建 ShareLevelProgress 表
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE share_level_progress (
|
||||||
|
id CHAR(36) PRIMARY KEY,
|
||||||
|
participant_id CHAR(36) NOT NULL COMMENT '关联 share_participants.id',
|
||||||
|
level_id CHAR(191) NOT NULL COMMENT '关卡ID',
|
||||||
|
passed TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否通过',
|
||||||
|
time_spent INT NOT NULL DEFAULT 0 COMMENT '通关时间(秒)',
|
||||||
|
completed_at DATETIME DEFAULT NULL COMMENT '通关时间戳',
|
||||||
|
|
||||||
|
UNIQUE KEY uq_participant_level (participant_id, level_id),
|
||||||
|
INDEX idx_participant_id (participant_id),
|
||||||
|
INDEX idx_level_id (level_id),
|
||||||
|
|
||||||
|
FOREIGN KEY (participant_id) REFERENCES share_participants(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. ER 关系
|
||||||
|
|
||||||
|
```
|
||||||
|
ShareConfig (1) ──< ShareParticipant (多)
|
||||||
|
│
|
||||||
|
└─< ShareLevelProgress (多) >── Level (1)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 实体设计
|
||||||
|
|
||||||
|
### ShareLevelProgress 实体
|
||||||
|
|
||||||
|
文件:`src/modules/share/entities/share-level-progress.entity.ts`
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| id | uuid | 主键 |
|
||||||
|
| participantId | varchar | 关联 ShareParticipant |
|
||||||
|
| levelId | varchar | 关卡ID |
|
||||||
|
| passed | boolean | 是否通过 |
|
||||||
|
| timeSpent | int | 通关时间(秒) |
|
||||||
|
| completedAt | datetime | 通关时间戳 |
|
||||||
|
|
||||||
|
唯一索引:`uq_participant_level(participant_id, level_id)`
|
||||||
|
|
||||||
|
## Repository 设计
|
||||||
|
|
||||||
|
### IShareLevelProgressRepository 接口
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface IShareLevelProgressRepository {
|
||||||
|
findByParticipantId(participantId: string): Promise<ShareLevelProgress[]>;
|
||||||
|
findByParticipantAndLevel(participantId: string, levelId: string): Promise<ShareLevelProgress | null>;
|
||||||
|
create(data: Partial<ShareLevelProgress>): ShareLevelProgress;
|
||||||
|
save(progress: ShareLevelProgress): Promise<ShareLevelProgress>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
实现:`src/modules/share/repositories/share-level-progress.repository.ts`
|
||||||
|
|
||||||
|
### ShareParticipantRepository 补充方法
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
findByShareConfigAndParticipant(
|
||||||
|
shareConfigId: string,
|
||||||
|
participantId: string,
|
||||||
|
): Promise<ShareParticipant | null>;
|
||||||
|
```
|
||||||
|
|
||||||
|
## DTO 设计
|
||||||
|
|
||||||
|
### ReportLevelProgressDto(请求)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 校验 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| shareCode | string | @IsString, @IsNotEmpty | 分享码 |
|
||||||
|
| levelId | string | @IsString, @IsNotEmpty | 关卡ID |
|
||||||
|
| passed | boolean | @IsBoolean | 是否通过 |
|
||||||
|
| timeSpent | number | @IsNumber, @Min(0) | 通关时间(秒) |
|
||||||
|
|
||||||
|
### ReportLevelProgressResponseDto(响应)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| passed | boolean | 是否通过 |
|
||||||
|
| timeLimit | number \| null | 该关卡时间限制(秒),null 表示无限制 |
|
||||||
|
| withinTimeLimit | boolean | 是否在时间限制内通过(仅 passed=true 时有效) |
|
||||||
|
|
||||||
|
## Service 层设计
|
||||||
|
|
||||||
|
### 新增方法:reportLevelProgress
|
||||||
|
|
||||||
|
业务逻辑:
|
||||||
|
|
||||||
|
1. 根据 shareCode 查找 ShareConfig
|
||||||
|
2. 查找或创建 ShareParticipant(分享者和参与者都需要)
|
||||||
|
3. 如果 passed=true,检查是否已有通关记录(存在则直接返回,不覆盖)
|
||||||
|
4. 查找关卡获取 timeLimit
|
||||||
|
5. 判断是否在时间限制内通过
|
||||||
|
6. 创建或更新进度记录
|
||||||
|
|
||||||
|
### 核心规则
|
||||||
|
|
||||||
|
- `passed=true` 时仅首次有效,不可覆盖
|
||||||
|
- `passed=false` 时允许重复上报(记录用户重玩)
|
||||||
|
- `withinTimeLimit = passed && (timeLimit === null || timeSpent <= timeLimit)`
|
||||||
|
|
||||||
|
## Controller 设计
|
||||||
|
|
||||||
|
### 接口
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /v1/share/progress
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async reportLevelProgress(
|
||||||
|
@CurrentUser() user: JwtPayload,
|
||||||
|
@Body() dto: ReportLevelProgressDto,
|
||||||
|
): Promise<ApiResponseDto<ReportLevelProgressResponseDto>>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Module 变更
|
||||||
|
|
||||||
|
### ShareModule
|
||||||
|
|
||||||
|
- 导入 `ShareLevelProgress` 实体
|
||||||
|
- 注册 `ShareLevelProgressRepository`
|
||||||
|
|
||||||
|
## 后续扩展
|
||||||
|
|
||||||
|
以下功能不在本次范围内,仅记录设计预留:
|
||||||
|
|
||||||
|
1. **排行查询**:按分享挑战统计用户总通关时间排名
|
||||||
|
2. **规定时间内通过关数统计**:统计某用户在挑战中的 withinTimeLimit 关数
|
||||||
|
3. **历史记录**:支持查看用户的重玩记录
|
||||||
|
|
||||||
|
## 文件清单
|
||||||
|
|
||||||
|
| 操作 | 文件路径 |
|
||||||
|
|------|----------|
|
||||||
|
| 新增 | src/modules/share/entities/share-level-progress.entity.ts |
|
||||||
|
| 新增 | src/modules/share/repositories/share-level-progress.repository.interface.ts |
|
||||||
|
| 新增 | src/modules/share/repositories/share-level-progress.repository.ts |
|
||||||
|
| 新增 | src/modules/share/dto/report-level-progress.dto.ts |
|
||||||
|
| 新增 | src/modules/share/dto/share-level-progress-response.dto.ts |
|
||||||
|
| 修改 | src/modules/share/repositories/share-participant.repository.ts |
|
||||||
|
| 修改 | src/modules/share/share.service.ts |
|
||||||
|
| 修改 | src/modules/share/share.controller.ts |
|
||||||
|
| 修改 | src/modules/share/share.module.ts |
|
||||||
|
| 修改 | src/modules/wechat-game/entities/level.entity.ts |
|
||||||
|
| SQL | 数据库迁移脚本(Level 表 + ShareLevelProgress 表) |
|
||||||
20
src/database/migrations/002_add_share_level_progress.sql
Normal file
20
src/database/migrations/002_add_share_level_progress.sql
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
-- Level 表增加 time_limit 字段
|
||||||
|
ALTER TABLE levels
|
||||||
|
ADD COLUMN time_limit INT DEFAULT NULL COMMENT '通关时间限制(秒),NULL 表示无限制'
|
||||||
|
AFTER sort_order;
|
||||||
|
|
||||||
|
-- 新建 share_level_progress 表
|
||||||
|
CREATE TABLE IF NOT EXISTS share_level_progress (
|
||||||
|
id CHAR(36) PRIMARY KEY,
|
||||||
|
participant_id CHAR(36) NOT NULL COMMENT '关联 share_participants.id',
|
||||||
|
level_id CHAR(191) NOT NULL COMMENT '关卡ID',
|
||||||
|
passed TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否通过',
|
||||||
|
time_spent INT NOT NULL DEFAULT 0 COMMENT '通关时间(秒)',
|
||||||
|
completed_at DATETIME DEFAULT NULL COMMENT '通关时间戳',
|
||||||
|
|
||||||
|
UNIQUE KEY uq_participant_level (participant_id, level_id),
|
||||||
|
INDEX idx_slp_participant_id (participant_id),
|
||||||
|
INDEX idx_slp_level_id (level_id),
|
||||||
|
|
||||||
|
FOREIGN KEY (participant_id) REFERENCES share_participants(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
23
src/modules/share/dto/report-level-progress.dto.ts
Normal file
23
src/modules/share/dto/report-level-progress.dto.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsBoolean, IsNotEmpty, IsNumber, IsString, Min } from 'class-validator';
|
||||||
|
|
||||||
|
export class ReportLevelProgressDto {
|
||||||
|
@ApiProperty({ description: '分享码' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
shareCode!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '关卡 ID' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
levelId!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '是否通过' })
|
||||||
|
@IsBoolean()
|
||||||
|
passed!: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '通关时间(秒)' })
|
||||||
|
@IsNumber()
|
||||||
|
@Min(0)
|
||||||
|
timeSpent!: number;
|
||||||
|
}
|
||||||
12
src/modules/share/dto/share-level-progress-response.dto.ts
Normal file
12
src/modules/share/dto/share-level-progress-response.dto.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class ReportLevelProgressResponseDto {
|
||||||
|
@ApiProperty({ description: '是否通过' })
|
||||||
|
passed!: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '该关卡时间限制(秒),null 表示无限制' })
|
||||||
|
timeLimit!: number | null;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '是否在时间限制内通过' })
|
||||||
|
withinTimeLimit!: boolean;
|
||||||
|
}
|
||||||
44
src/modules/share/entities/share-level-progress.entity.ts
Normal file
44
src/modules/share/entities/share-level-progress.entity.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
ManyToOne,
|
||||||
|
JoinColumn,
|
||||||
|
Index,
|
||||||
|
Unique,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { ShareParticipant } from './share-participant.entity';
|
||||||
|
import { Level } from '../../wechat-game/entities/level.entity';
|
||||||
|
|
||||||
|
@Entity('share_level_progress')
|
||||||
|
@Unique('uq_participant_level', ['participantId', 'levelId'])
|
||||||
|
export class ShareLevelProgress {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@Index('idx_slp_participant_id')
|
||||||
|
@Column({ type: 'char', length: 36, name: 'participant_id' })
|
||||||
|
participantId!: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => ShareParticipant)
|
||||||
|
@JoinColumn({ name: 'participant_id' })
|
||||||
|
participant!: ShareParticipant;
|
||||||
|
|
||||||
|
@Index('idx_slp_level_id')
|
||||||
|
@Column({ type: 'char', length: 191, name: 'level_id' })
|
||||||
|
levelId!: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => Level)
|
||||||
|
@JoinColumn({ name: 'level_id' })
|
||||||
|
level!: Level;
|
||||||
|
|
||||||
|
@Column({ type: 'tinyint', width: 1, default: 0 })
|
||||||
|
passed!: boolean;
|
||||||
|
|
||||||
|
@Column({ type: 'int', default: 0, name: 'time_spent' })
|
||||||
|
timeSpent!: number;
|
||||||
|
|
||||||
|
@CreateDateColumn({ name: 'completed_at' })
|
||||||
|
completedAt!: Date;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { ShareLevelProgress } from '../entities/share-level-progress.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ShareLevelProgressRepository {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(ShareLevelProgress)
|
||||||
|
private readonly repository: Repository<ShareLevelProgress>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async findByParticipantId(participantId: string): Promise<ShareLevelProgress[]> {
|
||||||
|
return this.repository.find({ where: { participantId } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByParticipantAndLevel(
|
||||||
|
participantId: string,
|
||||||
|
levelId: string,
|
||||||
|
): Promise<ShareLevelProgress | null> {
|
||||||
|
return this.repository.findOne({ where: { participantId, levelId } });
|
||||||
|
}
|
||||||
|
|
||||||
|
create(data: Partial<ShareLevelProgress>): ShareLevelProgress {
|
||||||
|
return this.repository.create(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async save(progress: ShareLevelProgress): Promise<ShareLevelProgress> {
|
||||||
|
return this.repository.save(progress);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,4 +27,19 @@ export class ShareParticipantRepository {
|
|||||||
async countByShareConfigId(shareConfigId: string): Promise<number> {
|
async countByShareConfigId(shareConfigId: string): Promise<number> {
|
||||||
return this.repository.count({ where: { shareConfigId } });
|
return this.repository.count({ where: { shareConfigId } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findByShareConfigAndParticipant(
|
||||||
|
shareConfigId: string,
|
||||||
|
participantId: string,
|
||||||
|
): Promise<ShareParticipant | null> {
|
||||||
|
return this.repository.findOne({ where: { shareConfigId, participantId } });
|
||||||
|
}
|
||||||
|
|
||||||
|
create(data: Partial<ShareParticipant>): ShareParticipant {
|
||||||
|
return this.repository.create(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async save(participant: ShareParticipant): Promise<ShareParticipant> {
|
||||||
|
return this.repository.save(participant);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import { ApiResponseDto } from '../../common/dto/api-response.dto';
|
|||||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||||
import type { JwtPayload } from '../../common/guards/jwt-auth.guard';
|
import type { JwtPayload } from '../../common/guards/jwt-auth.guard';
|
||||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||||
|
import { ReportLevelProgressDto } from './dto/report-level-progress.dto';
|
||||||
|
import { ReportLevelProgressResponseDto } from './dto/share-level-progress-response.dto';
|
||||||
|
|
||||||
@ApiTags('分享挑战')
|
@ApiTags('分享挑战')
|
||||||
@Controller('v1/share')
|
@Controller('v1/share')
|
||||||
@@ -54,4 +56,21 @@ export class ShareController {
|
|||||||
const data = await this.shareService.joinShare(user.sub, code);
|
const data = await this.shareService.joinShare(user.sub, code);
|
||||||
return ApiResponseDto.success(data);
|
return ApiResponseDto.success(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('progress')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '上报单关进度',
|
||||||
|
description: '用户在分享挑战中通关后上报进度,仅首次通关(passed=true)有效',
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 200, description: '成功' })
|
||||||
|
@ApiResponse({ status: 404, description: '分享或关卡不存在' })
|
||||||
|
async reportLevelProgress(
|
||||||
|
@CurrentUser() user: JwtPayload,
|
||||||
|
@Body() dto: ReportLevelProgressDto,
|
||||||
|
): Promise<ApiResponseDto<ReportLevelProgressResponseDto>> {
|
||||||
|
const data = await this.shareService.reportLevelProgress(user.sub, dto);
|
||||||
|
return ApiResponseDto.success(data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,18 +4,29 @@ import { ShareController } from './share.controller';
|
|||||||
import { ShareService } from './share.service';
|
import { ShareService } from './share.service';
|
||||||
import { ShareConfig } from './entities/share-config.entity';
|
import { ShareConfig } from './entities/share-config.entity';
|
||||||
import { ShareParticipant } from './entities/share-participant.entity';
|
import { ShareParticipant } from './entities/share-participant.entity';
|
||||||
|
import { ShareLevelProgress } from './entities/share-level-progress.entity';
|
||||||
import { ShareConfigRepository } from './repositories/share-config.repository';
|
import { ShareConfigRepository } from './repositories/share-config.repository';
|
||||||
import { ShareParticipantRepository } from './repositories/share-participant.repository';
|
import { ShareParticipantRepository } from './repositories/share-participant.repository';
|
||||||
|
import { ShareLevelProgressRepository } from './repositories/share-level-progress.repository';
|
||||||
import { WechatGameModule } from '../wechat-game/wechat-game.module';
|
import { WechatGameModule } from '../wechat-game/wechat-game.module';
|
||||||
import { AuthModule } from '../auth/auth.module';
|
import { AuthModule } from '../auth/auth.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([ShareConfig, ShareParticipant]),
|
TypeOrmModule.forFeature([
|
||||||
|
ShareConfig,
|
||||||
|
ShareParticipant,
|
||||||
|
ShareLevelProgress,
|
||||||
|
]),
|
||||||
WechatGameModule,
|
WechatGameModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
],
|
],
|
||||||
controllers: [ShareController],
|
controllers: [ShareController],
|
||||||
providers: [ShareService, ShareConfigRepository, ShareParticipantRepository],
|
providers: [
|
||||||
|
ShareService,
|
||||||
|
ShareConfigRepository,
|
||||||
|
ShareParticipantRepository,
|
||||||
|
ShareLevelProgressRepository,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class ShareModule {}
|
export class ShareModule {}
|
||||||
|
|||||||
@@ -6,13 +6,16 @@ import {
|
|||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import { ShareConfigRepository } from './repositories/share-config.repository';
|
import { ShareConfigRepository } from './repositories/share-config.repository';
|
||||||
import { ShareParticipantRepository } from './repositories/share-participant.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 { LevelRepository } from '../wechat-game/repositories/level.repository';
|
||||||
import { CreateShareDto } from './dto/create-share.dto';
|
import { CreateShareDto } from './dto/create-share.dto';
|
||||||
|
import { ReportLevelProgressDto } from './dto/report-level-progress.dto';
|
||||||
import {
|
import {
|
||||||
CreateShareResponseDto,
|
CreateShareResponseDto,
|
||||||
JoinShareResponseDto,
|
JoinShareResponseDto,
|
||||||
ShareLevelDto,
|
ShareLevelDto,
|
||||||
} from './dto/share-response.dto';
|
} from './dto/share-response.dto';
|
||||||
|
import { ReportLevelProgressResponseDto } from './dto/share-level-progress-response.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ShareService {
|
export class ShareService {
|
||||||
@@ -20,6 +23,7 @@ export class ShareService {
|
|||||||
private readonly shareConfigRepository: ShareConfigRepository,
|
private readonly shareConfigRepository: ShareConfigRepository,
|
||||||
private readonly shareParticipantRepository: ShareParticipantRepository,
|
private readonly shareParticipantRepository: ShareParticipantRepository,
|
||||||
private readonly levelRepository: LevelRepository,
|
private readonly levelRepository: LevelRepository,
|
||||||
|
private readonly shareLevelProgressRepository: ShareLevelProgressRepository,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async createShare(
|
async createShare(
|
||||||
@@ -105,4 +109,97 @@ export class ShareService {
|
|||||||
levels,
|
levels,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async reportLevelProgress(
|
||||||
|
userId: string,
|
||||||
|
dto: ReportLevelProgressDto,
|
||||||
|
): Promise<ReportLevelProgressResponseDto> {
|
||||||
|
// 1. 查找分享配置
|
||||||
|
const config = await this.shareConfigRepository.findByShareCode(
|
||||||
|
dto.shareCode,
|
||||||
|
);
|
||||||
|
if (!config) {
|
||||||
|
throw new NotFoundException('分享不存在或已过期');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 查找或创建 ShareParticipant
|
||||||
|
let participant =
|
||||||
|
await this.shareParticipantRepository.findByShareConfigAndParticipant(
|
||||||
|
config.id,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
if (!participant) {
|
||||||
|
participant = await this.shareParticipantRepository.create({
|
||||||
|
shareConfigId: config.id,
|
||||||
|
participantId: userId,
|
||||||
|
});
|
||||||
|
participant = await this.shareParticipantRepository.save(participant);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 如果 passed=true,检查是否已有通关记录
|
||||||
|
if (dto.passed) {
|
||||||
|
const existing =
|
||||||
|
await this.shareLevelProgressRepository.findByParticipantAndLevel(
|
||||||
|
participant.id,
|
||||||
|
dto.levelId,
|
||||||
|
);
|
||||||
|
if (existing?.passed) {
|
||||||
|
const existingLevel = await this.levelRepository.findById(dto.levelId);
|
||||||
|
if (!existingLevel) {
|
||||||
|
throw new NotFoundException('关卡不存在');
|
||||||
|
}
|
||||||
|
const wasWithinTimeLimit =
|
||||||
|
existingLevel.timeLimit === null ||
|
||||||
|
existing.timeSpent <= existingLevel.timeLimit;
|
||||||
|
return {
|
||||||
|
passed: true,
|
||||||
|
timeLimit: existingLevel.timeLimit,
|
||||||
|
withinTimeLimit: wasWithinTimeLimit,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 查找关卡获取时间限制
|
||||||
|
const level = await this.levelRepository.findById(dto.levelId);
|
||||||
|
if (!level) {
|
||||||
|
throw new NotFoundException('关卡不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 判断是否在时间限制内通过
|
||||||
|
const withinTimeLimit = dto.passed
|
||||||
|
? level.timeLimit === null || dto.timeSpent <= level.timeLimit
|
||||||
|
: false;
|
||||||
|
|
||||||
|
// 6. 创建或更新进度
|
||||||
|
let progress =
|
||||||
|
await this.shareLevelProgressRepository.findByParticipantAndLevel(
|
||||||
|
participant.id,
|
||||||
|
dto.levelId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (progress) {
|
||||||
|
progress = this.shareLevelProgressRepository.create({
|
||||||
|
...progress,
|
||||||
|
passed: dto.passed,
|
||||||
|
timeSpent: dto.timeSpent,
|
||||||
|
completedAt: dto.passed ? new Date() : progress.completedAt,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
progress = this.shareLevelProgressRepository.create({
|
||||||
|
participantId: participant.id,
|
||||||
|
levelId: dto.levelId,
|
||||||
|
passed: dto.passed,
|
||||||
|
timeSpent: dto.timeSpent,
|
||||||
|
completedAt: dto.passed ? new Date() : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.shareLevelProgressRepository.save(progress);
|
||||||
|
|
||||||
|
return {
|
||||||
|
passed: dto.passed,
|
||||||
|
timeLimit: level.timeLimit,
|
||||||
|
withinTimeLimit,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ export class Level {
|
|||||||
@Column({ type: 'int', name: 'sort_order', default: 0 })
|
@Column({ type: 'int', name: 'sort_order', default: 0 })
|
||||||
sortOrder!: number;
|
sortOrder!: number;
|
||||||
|
|
||||||
|
@Column({ type: 'int', name: 'time_limit', nullable: true, default: null })
|
||||||
|
timeLimit!: number | null;
|
||||||
|
|
||||||
@CreateDateColumn({ name: 'created_at' })
|
@CreateDateColumn({ name: 'created_at' })
|
||||||
createdAt!: Date;
|
createdAt!: Date;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user