feat: 接入激励视频
This commit is contained in:
578
软著源代码/03-认证存储与网络.md
Normal file
578
软著源代码/03-认证存储与网络.md
Normal file
@@ -0,0 +1,578 @@
|
||||
# 梗中作乐 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<boolean> {
|
||||
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<boolean> {
|
||||
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<ApiEnvelope<WxLoginData>>(
|
||||
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<boolean> {
|
||||
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<void> {
|
||||
const gameData = await this._fetchGameData();
|
||||
if (gameData) {
|
||||
this._completedLevelCount = gameData.completedLevelCount;
|
||||
this._nextLevel = gameData.nextLevel;
|
||||
StorageManager.setStamina(gameData.user.stamina);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从服务端获取游戏数据(共用方法)
|
||||
*/
|
||||
private async _fetchGameData(): Promise<GameData | null> {
|
||||
try {
|
||||
const response = await HttpUtil.get<ApiEnvelope<GameData>>(
|
||||
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<Response>
|
||||
*/
|
||||
static get<T>(url: string, timeout: number = 10000): Promise<T> {
|
||||
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<Response>
|
||||
*/
|
||||
static post<T>(url: string, data: object, timeout: number = 10000): Promise<T> {
|
||||
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] 用户信息已清除');
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user