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

19 KiB

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:

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:

{
  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:

// 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:

// 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:

// 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:

// 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:

// 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:

// 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:

// 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:

// 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:

// 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:

// 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:

// 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

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

// 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

// 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

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