Files
MemeMind-Server/CLIENT_SERVER_INTEGRATION.md
2026-04-05 13:38:12 +08:00

837 lines
19 KiB
Markdown

# MemeMind Client-Server Integration Guide
## Overview
This document explains how the Cocos Creator client communicates with the MemeMind-Server backend and what extensions would be needed to support the full game flow including user authentication, progress tracking, and point/life management.
---
## Part 1: Current Integration (Read-Only)
### Current API Call: Get All Levels
**Client File**: `/Users/richard/Documents/code/cocosProject/mp-xieyingeng/assets/scripts/managers/LevelDataManager.ts`
**Current Implementation**:
```typescript
async initialize(): Promise<void> {
try {
// Initialize() is called by PageLoading during startup
const response = await HttpUtil.get<ApiResponse>(
'https://ilookai.cn/api/v1/wechat-game/levels'
);
if (response.success && response.data?.levels) {
this._apiData = response.data.levels;
this._levelDataCache.clear();
// Preload next level images asynchronously
this.preloadNextLevel(0);
}
} catch (error) {
console.error('Failed to load level data', error);
}
}
```
**Server Endpoint**:
```
GET /api/v1/wechat-game/levels
Status: 200
Response: ApiResponseDto<LevelListResponseDto>
```
**Response Format**:
```typescript
{
success: true,
data: {
levels: [
{
level: 1, // Level number (1-indexed)
id: "level-001", // Unique ID
imageUrl: "https://...", // Level image URL
answer: "meme", // Correct answer
hint1: "image", // First hint (free)
hint2: "funny", // Second hint (costs 1 life)
hint3: null, // Third hint (costs 1 life)
sortOrder: 0, // Display order
createdAt: "2026-04-01T...",
updatedAt: "2026-04-05T..."
},
...
],
total: 50
},
message: null,
timestamp: "2026-04-05T10:30:00Z"
}
```
### Data Flow
```
Client Startup
PageLoading.ts: _startPreload()
├─ LevelDataManager.initialize()
│ │
│ └─> HttpUtil.get('/api/v1/wechat-game/levels')
│ │
│ ▼
│ MemeMind-Server
│ │
│ ├─> WechatGameController.getAllLevels()
│ ├─> WechatGameService.getAllLevels()
│ ├─> LevelRepository.findAllOrdered()
│ └─> MySQL: SELECT * FROM levels ORDER BY sort_order
│ Response returned
│ │
│ ▼
│ LevelDataManager._apiData = levels
│ Preload images
├─ Progress: 80% -> 100%
PageHome displayed
User clicks "Start Game"
PageLevel loaded
├─> Reads from _apiData
├─> Displays level image, hints, input
└─> Ready for gameplay
```
---
## Part 2: Missing Features for Full Integration
### 1. User Authentication
**What's needed**:
- WeChat OpenID extraction via `wx.login()`
- Backend user registration/login endpoint
- JWT token generation and validation
- User context in requests
**Implementation Plan**:
**Server Addition**:
```typescript
// src/modules/users/users.module.ts
@Module({
controllers: [UsersController],
providers: [UsersService, UsersRepository],
imports: [TypeOrmModule.forFeature([User])],
})
export class UsersModule {}
// src/modules/users/entities/user.entity.ts
@Entity('users')
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ unique: true })
wxOpenId: string; // WeChat OpenID
@Column()
nickname: string;
@Column({ default: 10 })
currentLives: number;
@Column({ default: 0 })
currentLevelIndex: number;
@Column({ default: 0 })
totalPoints: number;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
// Endpoint: POST /api/v1/users/login
@Post('login')
async login(@Body() dto: LoginDto): Promise<ApiResponseDto<LoginResponseDto>> {
// dto.code from wx.login()
// Exchange code for wxOpenId via WeChat API
// Create or fetch user
// Generate JWT token
// Return user + token
}
```
**Client Addition**:
```typescript
// In PageLoading.ts or main.ts
async function initializeUser() {
try {
const code = await wx.login();
const response = await HttpUtil.post('/api/v1/users/login', {
code: code.code
});
if (response.success) {
const { user, token } = response.data;
StorageManager.setToken(token); // New: Store JWT
StorageManager.setUserId(user.id); // New: Store user ID
return user;
}
} catch (error) {
console.error('Login failed', error);
}
}
```
---
### 2. Level Submission & Answer Validation
**What's needed**:
- POST endpoint for level submissions
- Answer validation (case-insensitive, trim whitespace)
- Update user progress
- Award points/lives
- Handle wrong answers
**Implementation Plan**:
**Server Addition**:
```typescript
// src/modules/levels/level-submission.module.ts
@Module({
controllers: [LevelSubmissionController],
providers: [LevelSubmissionService],
imports: [
TypeOrmModule.forFeature([UserProgress, Level]),
UsersModule,
WechatGameModule,
],
})
export class LevelSubmissionModule {}
// Endpoint: POST /api/v1/levels/:levelId/submit
@Post(':levelId/submit')
@UseGuards(JwtAuthGuard) // Require authentication
async submitAnswer(
@Param('levelId') levelId: string,
@Body() dto: SubmitAnswerDto,
@Req() request: any,
): Promise<ApiResponseDto<SubmitAnswerResponseDto>> {
// request.user.id from JWT
// Validate answer (case-insensitive, trim)
// If correct:
// - Award 1 life
// - Update currentLevelIndex
// - Record submission
// If wrong:
// - Record wrong attempt
// - Maybe deduct lives?
// Return result
}
// src/modules/levels/dto/submit-answer.dto.ts
export class SubmitAnswerDto {
@IsString()
@IsNotEmpty()
answer: string;
@IsNumber()
timeTaken: number; // Seconds to solve
@IsNumber()
hintsUsed: number; // How many hints revealed
}
export class SubmitAnswerResponseDto {
success: boolean; // Correct answer?
message: string; // "Correct!" or "Wrong!"
newLives: number; // Updated lives
newLevel: number; // Next level index
pointsEarned: number; // Points for this solve
totalPoints: number; // Total accumulated points
}
```
**Client Change**:
```typescript
// In PageLevel.ts: showSuccess() method
private async showSuccess(): void {
this.stopCountdown();
this.playSuccessSound();
// NEW: Submit to server
try {
const token = StorageManager.getToken();
const response = await HttpUtil.post(
`/api/v1/levels/${this._currentLevel.id}/submit`,
{
answer: this._userAnswer,
timeTaken: this._elapsedTime,
hintsUsed: this._hintsUsed
},
{
headers: {
Authorization: `Bearer ${token}`
}
}
);
if (response.success) {
// Update local storage with new progress
StorageManager.setLives(response.data.newLives);
StorageManager.onLevelCompleted(response.data.newLevel - 1);
// Show points earned
this.showPointsNotification(response.data.pointsEarned);
}
} catch (error) {
console.error('Submission failed', error);
}
this._showPassModal();
}
```
---
### 3. User Progress Tracking
**What's needed**:
- Endpoint to get user progress
- Endpoint to update progress
- Sync between client localStorage and server
- Handle offline mode
**Implementation Plan**:
**Server Addition**:
```typescript
// Endpoint: GET /api/v1/users/me/progress
@Get('me/progress')
@UseGuards(JwtAuthGuard)
async getProgress(
@Req() request: any,
): Promise<ApiResponseDto<UserProgressDto>> {
// request.user.id from JWT
// Return user progress
}
export class UserProgressDto {
id: string;
userId: string;
currentLevelIndex: number;
maxLevelUnlocked: number;
totalPoints: number;
currentLives: number;
completedLevels: {
levelId: string;
completedAt: Date;
timeTaken: number;
hintsUsed: number;
}[];
}
// Endpoint: POST /api/v1/users/me/progress/sync
@Post('me/progress/sync')
@UseGuards(JwtAuthGuard)
async syncProgress(
@Req() request: any,
@Body() dto: SyncProgressDto,
): Promise<ApiResponseDto<SyncProgressResponseDto>> {
// Merge client progress with server
// Handle conflicts (prefer latest)
// Return merged progress
}
```
**Client Update**:
```typescript
// In StorageManager.ts: Add sync methods
static async syncWithServer(): Promise<void> {
try {
const token = this.getToken();
const localProgress = this.getCurrentLevelIndex();
const localLives = this.getLives();
const response = await HttpUtil.post(
'/api/v1/users/me/progress/sync',
{
currentLevelIndex: localProgress,
currentLives: localLives,
},
{
headers: { Authorization: `Bearer ${token}` }
}
);
if (response.success) {
// Update local with server's merged data
this.setLives(response.data.currentLives);
// Update progress for each completed level
response.data.completedLevels.forEach((level) => {
this.onLevelCompleted(level.levelIndex);
});
}
} catch (error) {
console.error('Sync failed', error);
// Continue with local data
}
}
// Call on app startup and periodically
```
---
### 4. Hint Usage & Cost Management
**What's needed**:
- Track which hints have been used
- Deduct lives when using premium hints
- Prevent excessive hint usage
**Implementation Plan**:
**Server Addition**:
```typescript
// Track hint usage per level per user
@Entity('level_hints')
export class LevelHint {
@PrimaryGeneratedColumn('uuid')
id: string;
@ManyToOne(() => User)
user: User;
@Column()
levelId: string;
@Column()
hint1Revealed: boolean; // Always free
@Column()
hint2Revealed: boolean; // Costs 1 life
@Column()
hint3Revealed: boolean; // Costs 1 life
@CreateDateColumn()
createdAt: Date;
}
// Endpoint: POST /api/v1/levels/:levelId/reveal-hint
@Post(':levelId/reveal-hint')
@UseGuards(JwtAuthGuard)
async revealHint(
@Param('levelId') levelId: string,
@Body() dto: RevealHintDto, // { hintNumber: 2 }
@Req() request: any,
): Promise<ApiResponseDto<RevealHintResponseDto>> {
// Validate hint number
// Check if already revealed
// If hint 2 or 3: deduct 1 life
// Update hint tracking
// Return hint text
}
```
**Client Change**:
```typescript
// In PageLevel.ts: onUnlockClue() method
private async onUnlockClue(clueIndex: number): void {
const token = StorageManager.getToken();
// Hint index 1 is always free (hint1)
// Hints 2 and 3 cost 1 life each
if (clueIndex > 0 && !this._freeCluePassed) {
const currentLives = StorageManager.getLives();
if (currentLives <= 0) {
this._showToast('没有生命了!(No lives left!)');
return;
}
}
try {
const response = await HttpUtil.post(
`/api/v1/levels/${this._currentLevel.id}/reveal-hint`,
{ hintNumber: clueIndex + 1 },
{ headers: { Authorization: `Bearer ${token}` } }
);
if (response.success) {
const hint = response.data.hintText;
this._showHint(hint);
// Deduct life if premium hint
if (clueIndex > 0) {
StorageManager.consumeLife();
StorageManager.setLives(response.data.remainingLives);
this._updateLivesDisplay();
}
}
} catch (error) {
console.error('Failed to unlock hint', error);
}
}
```
---
### 5. Leaderboard & Statistics
**What's needed**:
- Track total points per user
- Track completion time
- Leaderboard endpoint
- User statistics endpoint
**Implementation Plan**:
**Server Addition**:
```typescript
// Endpoint: GET /api/v1/leaderboard?limit=100
@Get('leaderboard')
async getLeaderboard(
@Query('limit') limit: number = 100,
): Promise<ApiResponseDto<LeaderboardDto>> {
// Select top users by totalPoints
// Include rank, nickname, points, completedLevels
}
// Endpoint: GET /api/v1/users/me/statistics
@Get('me/statistics')
@UseGuards(JwtAuthGuard)
async getStatistics(
@Req() request: any,
): Promise<ApiResponseDto<StatisticsDto>> {
// Return user statistics
// Total levels completed
// Average time per level
// Total hints used
// Current streak, etc.
}
export class StatisticsDto {
totalLevelsCompleted: number;
currentLevelIndex: number;
totalPoints: number;
currentLives: number;
averageTimePerLevel: number; // Seconds
totalTimeSpent: number; // Seconds
totalHintsUsed: number;
perfectSolves: number; // Solved in first try
longestStreak: number;
}
```
---
## Part 3: API Authentication Pattern
### JWT Guard Implementation
**Server**:
```typescript
// src/modules/auth/guards/jwt.guard.ts
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(private readonly configService: ConfigService) {}
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException('No token provided');
}
try {
const secret = this.configService.get('JWT_SECRET');
const decoded = verify(token, secret);
request.user = decoded; // Attach to request
return true;
} catch (error) {
throw new UnauthorizedException('Invalid token');
}
}
private extractTokenFromHeader(request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
// Usage on endpoints
@UseGuards(JwtAuthGuard)
@Get('me/progress')
async getProgress(@Req() request: any) {
const userId = request.user.id; // From JWT payload
// ...
}
```
**Client**:
```typescript
// In HttpUtil.ts: Add auth header
static async post<T>(
url: string,
data: any,
options?: RequestOptions
): Promise<ApiResponse<T>> {
const token = StorageManager.getToken();
const headers = {
...options?.headers,
'Content-Type': 'application/json',
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
// Make request with headers
}
```
---
## Part 4: Data Sync Strategy
### Scenario 1: Online Mode
```
User completes level
[Client] Submits answer
├─ HTTP POST /api/v1/levels/:id/submit
[Server] Validates and updates
├─ Check answer
├─ Award life/points
├─ Update progress
├─ Store submission
[Response] Returned to client
├─ Update localStorage
├─ Show success modal
├─ Move to next level
Progress synced
```
### Scenario 2: Offline Mode
```
User completes level (no connection)
├─ Submit fails (no network)
[Client] Stores locally
├─ StorageManager.recordOfflineSubmission()
├─ Update lives/progress locally
├─ Show success modal (assume correct)
When connection returns
├─ StorageManager.syncPendingSubmissions()
[Server] Receives batch of submissions
├─ Validate all answers
├─ Apply corrections if needed
├─ Return merged state
[Client] Reconciles state
├─ If conflicts: server wins
├─ Update localStorage
├─ Show notification of changes
Progress synced
```
---
## Part 5: Implementation Roadmap
### Phase 1: Basic Auth (Week 1)
- [ ] User entity & table
- [ ] Login endpoint with wx.login() code exchange
- [ ] JWT token generation
- [ ] JwtAuthGuard for protected routes
- [ ] Client login integration
- [ ] Token storage in StorageManager
### Phase 2: Progress Tracking (Week 2)
- [ ] UserProgress entity & table
- [ ] GET /api/v1/users/me/progress
- [ ] POST /api/v1/levels/:id/submit
- [ ] Update client PageLevel.ts to submit answers
- [ ] Sync endpoint for merging progress
- [ ] Offline submission queue
### Phase 3: Hint System Integration (Week 3)
- [ ] LevelHint entity & tracking
- [ ] POST /api/v1/levels/:id/reveal-hint
- [ ] Life deduction logic
- [ ] Client hint unlock cost
### Phase 4: Leaderboard & Stats (Week 4)
- [ ] Statistics calculation
- [ ] Leaderboard endpoint
- [ ] Client leaderboard page
- [ ] Personal statistics page
### Phase 5: Polish & Optimization (Week 5)
- [ ] Caching layer (@nestjs/cache-manager)
- [ ] Rate limiting (@nestjs/throttler)
- [ ] Request logging middleware
- [ ] Performance monitoring
- [ ] Database indexing optimization
---
## Part 6: Environment Configuration
### Server .env.production
```bash
NODE_ENV=production
PORT=3000
# Database
DB_HOST=production-db-host
DB_PORT=3306
DB_USERNAME=prod_user
DB_PASSWORD=secure_password
DB_DATABASE=meme_mind_prod
# Authentication
JWT_SECRET=very-secure-secret-key-32-chars-min
JWT_EXPIRATION=7d
# WeChat
WECHAT_APPID=your_wechat_appid
WECHAT_SECRET=your_wechat_secret
# API
API_BASE_URL=https://ilookai.cn/api
CORS_ORIGIN=https://yourdomain.com
```
### Client Storage Keys
```typescript
// New keys needed:
- 'auth_token' JWT token
- 'user_id' Current user ID
- 'offline_submissions' Queue of submissions to send
- 'last_sync' Timestamp of last sync
```
---
## Part 7: Error Handling
### Common API Errors
```typescript
// 401 Unauthorized
{
success: false,
data: null,
message: 'Invalid token',
path: '/api/v1/users/me/progress'
}
// 404 Not Found
{
success: false,
data: null,
message: 'Level with id "xyz" not found',
path: '/api/v1/levels/xyz'
}
// 400 Bad Request (validation)
{
success: false,
data: null,
message: 'Validation failed: answer must be a string',
path: '/api/v1/levels/123/submit'
}
// 500 Server Error
{
success: false,
data: null,
message: 'Internal server error',
path: '/api/v1/levels'
}
```
### Client Handling
```typescript
async function handleApiError(error: unknown) {
if (axios.isAxiosError(error)) {
const status = error.response?.status;
const message = error.response?.data?.message;
if (status === 401) {
// Token expired - redirect to login
StorageManager.clearToken();
navigateTo('PageHome');
} else if (status === 404) {
showToast(`Not found: ${message}`);
} else if (status === 400) {
showToast(`Invalid input: ${message}`);
} else {
showToast(`Error: ${message || 'Unknown error'}`);
}
}
}
```
---
## Summary
The current integration only handles reading level data. To support the full game loop with:
- ✅ User authentication (JWT)
- ✅ Answer submission & validation
- ✅ Progress tracking & sync
- ✅ Hint system with cost management
- ✅ Leaderboard & statistics
You need to implement the endpoints, models, and client logic described in this guide. The roadmap suggests a 5-week implementation with phases for each feature.
---
*Generated: 2026-04-05 | For MemeMind Project*