# 梗中作乐 V1.0 - 源代码文档(第 3 部分) 软件名称:梗中作乐 版本号:V1.0 --- ## 第 3 部分:认证存储与网络 本部分包含用户认证、本地存储、HTTP 网络通信三个核心工具模块。 ### 3.1 微信认证管理器 (assets/scripts/utils/AuthManager.ts) ```typescript import { HttpUtil } from './HttpUtil'; import { StorageManager } from './StorageManager'; import { WxSDK } from './WxSDK'; import { API_ENDPOINTS, API_TIMEOUT } from '../config/ApiConfig'; import { ApiEnvelope, WxLoginData, GameData, NextLevelData } from '../types/ApiTypes'; /** * 认证管理器 * 单例模式,负责微信登录和 JWT token 管理 */ export class AuthManager { private static _instance: AuthManager | null = null; private _userId: string = ''; private _isLoggedIn: boolean = false; /** 服务端返回的已完成关卡数量,用于称号体系计算 */ private _completedLevelCount: number = 0; /** game-data 返回的下一关数据,供 PageLoading 传给 LevelDataManager */ private _nextLevel: NextLevelData | null = null; static get instance(): AuthManager { if (!this._instance) { this._instance = new AuthManager(); } return this._instance; } private constructor() {} get isLoggedIn(): boolean { return this._isLoggedIn; } get userId(): string { return this._userId; } get completedLevelCount(): number { return this._completedLevelCount; } /** 获取 game-data 返回的下一关数据 */ get nextLevel(): NextLevelData | null { return this._nextLevel; } addCompletedLevelCount(delta: number = 1): void { this._completedLevelCount = Math.max(0, this._completedLevelCount + delta); } /** * 初始化认证:尝试恢复 token 或执行微信登录 */ async initialize(): Promise { const savedToken = StorageManager.getToken(); if (savedToken) { HttpUtil.setAuthToken(savedToken); try { const valid = await this.validateToken(); if (valid) { console.log('[AuthManager] Token 恢复成功'); return true; } } catch { console.warn('[AuthManager] 本地 token 无效,重新登录'); } } return this.wxLogin(); } private async wxLogin(): Promise { try { let code: string; if (WxSDK.isWechat()) { code = await WxSDK.login(); } else { console.warn('[AuthManager] 非微信环境,使用开发模式 mock code'); code = 'dev_mock_code'; } const response = await HttpUtil.post>( API_ENDPOINTS.WX_LOGIN, { code }, API_TIMEOUT.DEFAULT ); if (!response.success || !response.data) { console.error('[AuthManager] 登录失败:', response.message); return false; } const { token, user } = response.data; HttpUtil.setAuthToken(token); StorageManager.setToken(token); this._userId = user.id; this._isLoggedIn = true; // 登录响应中 stamina 是原始数值(不含实时恢复),先存储默认体力 // 后续通过 game-data 接口获取完整 StaminaInfo console.log(`[AuthManager] 登录成功,用户: ${user.id},体力: ${user.stamina}`); // 获取通关进度和完整体力信息 await this.fetchGameData(); return true; } catch (err) { console.error('[AuthManager] 登录异常:', err); return false; } } private async validateToken(): Promise { const gameData = await this._fetchGameData(); if (!gameData) return false; this._userId = gameData.user.id; this._isLoggedIn = true; StorageManager.setStamina(gameData.user.stamina); this._completedLevelCount = gameData.completedLevelCount; this._nextLevel = gameData.nextLevel; console.log(`[AuthManager] Token 验证成功,体力: ${gameData.user.stamina.current}/${gameData.user.stamina.max},已完成: ${this._completedLevelCount} 关`); return true; } /** * 登录成功后获取游戏数据(体力 + 通关进度 + 下一关) */ private async fetchGameData(): Promise { const gameData = await this._fetchGameData(); if (gameData) { this._completedLevelCount = gameData.completedLevelCount; this._nextLevel = gameData.nextLevel; StorageManager.setStamina(gameData.user.stamina); } } /** * 从服务端获取游戏数据(共用方法) */ private async _fetchGameData(): Promise { try { const response = await HttpUtil.get>( API_ENDPOINTS.USER_GAME_DATA, API_TIMEOUT.SHORT ); return (response.success && response.data) ? response.data : null; } catch { console.warn('[AuthManager] 获取游戏数据失败'); return null; } } } ``` ### 3.2 HTTP 工具类 (assets/scripts/utils/HttpUtil.ts) ```typescript /** * HTTP 请求工具类 * 封装 XMLHttpRequest,支持 GET/POST 请求,支持 JWT 认证 */ export class HttpUtil { /** 认证 token */ private static _authToken: string | null = null; /** * 设置认证 token * @param token JWT token */ static setAuthToken(token: string | null): void { HttpUtil._authToken = token; console.log(`[HttpUtil] Auth token ${token ? '已设置' : '已清除'}`); } /** * 获取认证 token */ static getAuthToken(): string | null { return HttpUtil._authToken; } /** * 发送 GET 请求 * @param url 请求 URL * @param timeout 超时时间(毫秒),默认 10000 * @returns Promise */ static get(url: string, timeout: number = 10000): Promise { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('GET', url, true); xhr.timeout = timeout; xhr.responseType = 'json'; // 设置认证头 if (HttpUtil._authToken) { xhr.setRequestHeader('Authorization', `Bearer ${HttpUtil._authToken}`); } xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { resolve(xhr.response as T); } else { reject(new Error(`HTTP 错误: ${xhr.status}`)); } }; xhr.onerror = () => { reject(new Error('网络请求失败')); }; xhr.ontimeout = () => { reject(new Error('请求超时')); }; xhr.send(); }); } /** * 发送 POST 请求 * @param url 请求 URL * @param data 请求体数据 * @param timeout 超时时间(毫秒),默认 10000 * @returns Promise */ static post(url: string, data: object, timeout: number = 10000): Promise { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('POST', url, true); xhr.timeout = timeout; xhr.responseType = 'json'; xhr.setRequestHeader('Content-Type', 'application/json'); // 设置认证头 if (HttpUtil._authToken) { xhr.setRequestHeader('Authorization', `Bearer ${HttpUtil._authToken}`); } xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { resolve(xhr.response as T); } else { reject(new Error(`HTTP 错误: ${xhr.status}`)); } }; xhr.onerror = () => { reject(new Error('网络请求失败')); }; xhr.ontimeout = () => { reject(new Error('请求超时')); }; xhr.send(JSON.stringify(data)); }); } } ``` ### 3.3 本地存储管理器 (assets/scripts/utils/StorageManager.ts) ```typescript import { sys } from 'cc'; import { StaminaInfo } from '../types/ApiTypes'; /** * 用户进度数据结构 */ interface UserProgress { /** 当前关卡索引(0-based) */ currentLevelIndex: number; /** 已通关的最高关卡索引 */ maxUnlockedLevelIndex: number; } /** * 用户信息结构 */ interface UserInfo { avatarUrl: string; nickName: string; } /** * 本地存储管理器 * 统一管理用户数据的本地持久化存储 */ export class StorageManager { /** 体力值存储键 */ private static readonly KEY_STAMINA = 'game_stamina'; /** 用户进度存储键 */ private static readonly KEY_PROGRESS = 'game_progress'; /** 认证 token 存储键 */ private static readonly KEY_TOKEN = 'auth_token'; /** 用户信息存储键 */ private static readonly KEY_USER_INFO = 'user_info'; /** 默认体力值 */ private static readonly DEFAULT_STAMINA: StaminaInfo = { current: 50, max: 50, nextRecoverAt: null, }; /** 默认进度 */ private static readonly DEFAULT_PROGRESS: UserProgress = { currentLevelIndex: 0, maxUnlockedLevelIndex: 0 }; /** 进度缓存(避免重复读取 localStorage) */ private static _progressCache: UserProgress | null = null; /** 体力缓存(避免重复 JSON 解析) */ private static _staminaCache: StaminaInfo | null = null; // ==================== 体力值管理 ==================== /** * 获取当前体力信息(带内存缓存,避免重复 JSON 解析) */ static getStamina(): StaminaInfo { if (StorageManager._staminaCache) { return { ...StorageManager._staminaCache }; } const stored = sys.localStorage.getItem(StorageManager.KEY_STAMINA); if (stored === null || stored === '') { StorageManager.setStamina(StorageManager.DEFAULT_STAMINA); return { ...StorageManager.DEFAULT_STAMINA }; } try { const stamina = JSON.parse(stored) as StaminaInfo; if (typeof stamina.current !== 'number' || typeof stamina.max !== 'number') { StorageManager.setStamina(StorageManager.DEFAULT_STAMINA); return { ...StorageManager.DEFAULT_STAMINA }; } StorageManager._staminaCache = stamina; return { ...stamina }; } catch { StorageManager.setStamina(StorageManager.DEFAULT_STAMINA); return { ...StorageManager.DEFAULT_STAMINA }; } } /** * 设置体力信息(同时更新缓存) */ static setStamina(stamina: StaminaInfo): void { StorageManager._staminaCache = stamina; sys.localStorage.setItem(StorageManager.KEY_STAMINA, JSON.stringify(stamina)); console.log(`[StorageManager] 体力已更新: ${stamina.current}/${stamina.max}`); } /** * 检查是否有足够的体力 */ static hasStamina(): boolean { return StorageManager.getStamina().current > 0; } // ==================== 认证 Token 管理 ==================== /** * 获取认证 token */ static getToken(): string | null { const token = sys.localStorage.getItem(StorageManager.KEY_TOKEN); return (token === null || token === '') ? null : token; } /** * 设置认证 token */ static setToken(token: string): void { sys.localStorage.setItem(StorageManager.KEY_TOKEN, token); console.log('[StorageManager] Token 已保存'); } /** * 清除认证 token */ static clearToken(): void { sys.localStorage.removeItem(StorageManager.KEY_TOKEN); console.log('[StorageManager] Token 已清除'); } // ==================== 关卡进度管理 ==================== /** * 获取用户进度数据(带缓存) * @returns 用户进度对象的副本 */ private static _getProgress(): UserProgress { // 返回缓存副本 if (StorageManager._progressCache !== null) { return { ...StorageManager._progressCache }; } const stored = sys.localStorage.getItem(StorageManager.KEY_PROGRESS); if (stored === null || stored === '') { // 新用户,返回默认进度 StorageManager._progressCache = { ...StorageManager.DEFAULT_PROGRESS }; return { ...StorageManager._progressCache }; } try { const progress = JSON.parse(stored) as UserProgress; // 验证数据有效性 if (typeof progress.currentLevelIndex !== 'number' || typeof progress.maxUnlockedLevelIndex !== 'number' || progress.currentLevelIndex < 0 || progress.maxUnlockedLevelIndex < 0) { console.warn('[StorageManager] 进度数据无效,使用默认值'); StorageManager._progressCache = { ...StorageManager.DEFAULT_PROGRESS }; } else { StorageManager._progressCache = progress; } return { ...StorageManager._progressCache }; } catch (e) { console.warn('[StorageManager] 解析进度数据失败,使用默认值'); StorageManager._progressCache = { ...StorageManager.DEFAULT_PROGRESS }; return { ...StorageManager._progressCache }; } } /** * 保存用户进度数据 * @param progress 进度对象 */ private static _saveProgress(progress: UserProgress): void { StorageManager._progressCache = progress; sys.localStorage.setItem(StorageManager.KEY_PROGRESS, JSON.stringify(progress)); } /** * 获取当前关卡索引 * @returns 当前关卡索引(0-based) */ static getCurrentLevelIndex(): number { return StorageManager._getProgress().currentLevelIndex; } /** * 设置当前关卡索引 * @param index 关卡索引 */ static setCurrentLevelIndex(index: number): void { if (index < 0) { console.warn('[StorageManager] 关卡索引不能为负数'); return; } const progress = StorageManager._getProgress(); progress.currentLevelIndex = index; StorageManager._saveProgress(progress); console.log(`[StorageManager] 当前关卡已更新: ${progress.currentLevelIndex}`); } /** * 获取已解锁的最高关卡索引 * @returns 最高关卡索引(0-based) */ static getMaxUnlockedLevelIndex(): number { return StorageManager._getProgress().maxUnlockedLevelIndex; } /** * 通关后更新进度 * 当玩家通关第 N 关后,设置当前关卡为 N+1,解锁关卡更新为 max(N, 已解锁) * @param completedLevelIndex 刚通关的关卡索引 */ static onLevelCompleted(completedLevelIndex: number): void { if (completedLevelIndex < 0) { console.warn('[StorageManager] 通关关卡索引不能为负数'); return; } const progress = StorageManager._getProgress(); const nextLevelIndex = completedLevelIndex + 1; // 更新当前关卡为下一关 progress.currentLevelIndex = nextLevelIndex; // 更新最高解锁关卡 progress.maxUnlockedLevelIndex = Math.max(progress.maxUnlockedLevelIndex, completedLevelIndex); StorageManager._saveProgress(progress); console.log(`[StorageManager] 通关第 ${completedLevelIndex + 1} 关,下一关: ${nextLevelIndex + 1}`); } /** * 检查指定关卡是否已解锁 * @param levelIndex 关卡索引 * @returns 是否已解锁 */ static isLevelUnlocked(levelIndex: number): boolean { const progress = StorageManager._getProgress(); return levelIndex <= progress.maxUnlockedLevelIndex; } /** * 重置所有进度 */ static resetProgress(): void { StorageManager._progressCache = null; sys.localStorage.removeItem(StorageManager.KEY_PROGRESS); console.log('[StorageManager] 进度已重置'); } /** * 重置所有数据(体力 + 进度) */ static resetAll(): void { StorageManager.setStamina(StorageManager.DEFAULT_STAMINA); StorageManager.resetProgress(); StorageManager.clearToken(); StorageManager.clearUserInfo(); console.log('[StorageManager] 所有数据已重置'); } // ==================== 用户信息管理 ==================== /** * 保存用户信息(头像、昵称) * @param userInfo 用户信息对象 */ static setUserInfo(userInfo: UserInfo): void { sys.localStorage.setItem(StorageManager.KEY_USER_INFO, JSON.stringify(userInfo)); console.log('[StorageManager] 用户信息已保存'); } /** * 获取本地缓存的用户信息 * @returns 用户信息对象或 null */ static getUserInfo(): UserInfo | null { const data = sys.localStorage.getItem(StorageManager.KEY_USER_INFO); if (!data) return null; try { return JSON.parse(data) as UserInfo; } catch { return null; } } /** * 清除用户信息缓存 */ static clearUserInfo(): void { sys.localStorage.removeItem(StorageManager.KEY_USER_INFO); console.log('[StorageManager] 用户信息已清除'); } } ```