diff --git a/assets/script/game/common/GameDataSync.ts b/assets/script/game/common/GameDataSync.ts index 27d71f90..65533871 100644 --- a/assets/script/game/common/GameDataSync.ts +++ b/assets/script/game/common/GameDataSync.ts @@ -94,60 +94,65 @@ export class GameDataSync { } /** - * 将获取到的云端数据与本地对比并合并 + * 登录阶段与云端双向同步数据(sync 模式)。 + * 时间戳对比在云端完成: + * - 本地较新:云端已被本地数据覆盖,无需额外处理 + * - 云端较新:用云端数据覆盖本地,并写入 localStorage + * @returns true=本次以云端数据为准覆盖了本地;false=以本地数据为准或同步失败 */ - public syncWithCloudData(cloudData: CloudData | null) { + public async syncFromCloud(): Promise { const localData = this.loadFromLocal(); - let cloudTs = cloudData?.data?.timestamp || 0; - let localTs = localData?.timestamp || 0; - - if (!localData || cloudTs > localTs) { - mLogger.log(this.debugMode, 'GameDataSync', `[GameDataSync]: 云端数据较新 (Cloud: ${cloudTs} > Local: ${localTs}), 执行覆盖。`); - if (cloudData) { - smc.overrideLocalDataWithRemote(cloudData); - this.saveToLocal(); // 同步到本地 - } - } else { - mLogger.log(this.debugMode, 'GameDataSync', `[GameDataSync]: 本地数据较新 (Local: ${localTs} >= Cloud: ${cloudTs}), 使用本地数据,触发强制云同步。`); - smc.overrideLocalDataWithRemote({ data: localData }); - // 本地数据较新,需要强制推送到云端以保证多端一致 - this._localDataDirty = true; - this.executeCloudSync(); - } - } - - public getCloudData() { - const localData = this.loadFromLocal(); - - // 未登录微信云端前,先用本地数据顶上(让玩家秒进游戏) - if (localData && !this.isWxClient()) { - smc.overrideLocalDataWithRemote({ data: localData }); - return Promise.resolve(false); - } - - return WxCloudApi.get().then(async (result) => { - if(result.result.code === 200) { - let cloudData = result.result.data as CloudData; - mLogger.log(this.debugMode, 'GameDataSync', `[GameDataSync]: 获取游戏云端数据成功:`, cloudData); - - this.syncWithCloudData(cloudData); - - return true; - } else { - mLogger.warn(this.debugMode, 'GameDataSync', `[GameDataSync]: 获取游戏云端数据失败,使用本地缓存兜底。`); - if (localData) { - smc.overrideLocalDataWithRemote({ data: localData }); - } - return false; - } - }).catch((error) => { - mLogger.error(this.debugMode, 'GameDataSync', `[GameDataSync]: 获取游戏云端数据异常:`, error); + // 非微信客户端:仅使用本地缓存 + if (!this.isWxClient()) { if (localData) { smc.overrideLocalDataWithRemote({ data: localData }); } return false; - }); + } + + try { + // 本地无缓存时用空对象参与对比,云端会直接下发 + const gameData = localData || ({} as GameDate); + gameData.timestamp = gameData.timestamp || 0; + + const result = await WxCloudApi.sync(gameData); + const response = result.result; + + if (response && response.code === 200 && response.data) { + const cloudData: CloudData = { + openid: response.data.openid || '', + data: (response.data.game_data || {}) as GameDate, + }; + + if (response.updated) { + // 本地数据较新,云端已被覆盖 + mLogger.log(this.debugMode, 'GameDataSync', `[GameDataSync]: 本地数据较新,已覆盖云端。`); + if (localData) { + smc.overrideLocalDataWithRemote({ data: localData }); + } + return false; + } else { + // 云端数据较新,覆盖本地 + mLogger.log(this.debugMode, 'GameDataSync', `[GameDataSync]: 云端数据较新,覆盖本地。`); + smc.overrideLocalDataWithRemote(cloudData); + this.saveToLocal(); + return true; + } + } + + mLogger.warn(this.debugMode, 'GameDataSync', `[GameDataSync]: 云端同步失败,使用本地缓存兜底。`, response?.msg); + if (localData) { + smc.overrideLocalDataWithRemote({ data: localData }); + } + return false; + } catch (error) { + mLogger.error(this.debugMode, 'GameDataSync', `[GameDataSync]: 云端同步异常,使用本地缓存兜底。`, error); + if (localData) { + smc.overrideLocalDataWithRemote({ data: localData }); + } + return false; + } } } diff --git a/assets/script/game/common/SingletonModuleComp.ts b/assets/script/game/common/SingletonModuleComp.ts index 0dbd2ee5..73dda1d3 100644 --- a/assets/script/game/common/SingletonModuleComp.ts +++ b/assets/script/game/common/SingletonModuleComp.ts @@ -228,7 +228,7 @@ export class SingletonModuleComp extends ecs.Comp { } getCloudData() { - gameDataSync.getCloudData(); + gameDataSync.syncFromCloud(); } public async overrideLocalDataWithRemote(cloudData: any) { try { diff --git a/assets/script/game/initialize/Initialize.ts b/assets/script/game/initialize/Initialize.ts index 9ded4c91..9fae7416 100644 --- a/assets/script/game/initialize/Initialize.ts +++ b/assets/script/game/initialize/Initialize.ts @@ -128,44 +128,21 @@ export class Initialize extends ecs.Entity { } /** - * 从云端加载数据(微信客户端) + * 从云端加载数据(微信客户端,sync 模式) */ private async loadFromCloud() { try { // 1. 初始化微信云环境 this.initWxCloudEnv(); - // 2. 登录并获取云端数据 - const loginResult = await WxCloudApi.login(); - const response = loginResult.result; - - if (response && response.code === 200) { - mLogger.log(this.debugMode, 'Initialize', "[Initialize]: 云端登录成功"); - - const cloudData = response.data; - - if (cloudData && cloudData.openid) { - smc.data.openid = cloudData.openid; - } - - // 将 login 获取的 game_data 封装为统一下发格式,交给 gameDataSync 对比与合并 - const formattedCloudData = { - openid: cloudData?.openid || '', - data: cloudData?.game_data || {} - }; - - gameDataSync.syncWithCloudData(formattedCloudData); - - } else { - mLogger.log(this.debugMode, 'Initialize', "[Initialize]: 云端登录失败:", response?.msg); - // 登录失败时使用本地数据 - gameDataSync.syncWithCloudData(null); - } + // 2. 一次调用完成登录与双向数据同步(时间戳对比在云端完成) + const cloudNewer = await gameDataSync.syncFromCloud(); + mLogger.log(this.debugMode, 'Initialize', `[Initialize]: 云端同步完成,云端数据较新=${cloudNewer}`); } catch (error) { mLogger.log(this.debugMode, 'Initialize', "[Initialize]: 云端数据加载异常:", error); // 异常时使用本地数据兜底 - gameDataSync.syncWithCloudData(null); + await gameDataSync.syncFromCloud(); } } @@ -174,8 +151,8 @@ export class Initialize extends ecs.Entity { */ private async loadFromLocalDebug() { try { - // 用本地调试数据覆盖客户端数据 - gameDataSync.syncWithCloudData(null); + // 非微信客户端仅使用本地缓存数据 + await gameDataSync.syncFromCloud(); } catch (error) { mLogger.log(this.debugMode, 'Initialize', "[Initialize]: 本地调试数据加载异常:", error); } diff --git a/assets/script/game/wx_clound_client_api/WxCloudApi.ts b/assets/script/game/wx_clound_client_api/WxCloudApi.ts index 22e5e49b..2a9c419d 100644 --- a/assets/script/game/wx_clound_client_api/WxCloudApi.ts +++ b/assets/script/game/wx_clound_client_api/WxCloudApi.ts @@ -91,4 +91,32 @@ export class WxCloudApi{ } }); } + + /** + * @en Sync game data with the cloud (two-way, timestamp-based) + * @zh 与云端双向同步游戏数据:客户端较新则上传覆盖云端,云端较新则返回云端数据 + * @param gameData 本地游戏数据(需包含 timestamp 字段) + * @return 返回结果 + * result.result = { + * code: number, // 200成功,其他都是失败 + * msg?: string, // 失败原因 + * updated: boolean, // true=云端已被本地数据覆盖;false=客户端应使用返回的云端数据 + * data: { + * openid: string, + * game_data: object, // 同步后的最终数据 + * } + * } + */ + public static async sync(gameData: any): Promise & { updated: boolean }>> { + return await wx?.cloud.callFunction({ + name: 'cocos_cloud', + data: { + cmd: 'sync', + data: gameData + } + }); + } } \ No newline at end of file diff --git a/build-templates/wechatgame/cloud_functions/cocos_cloud/index.js b/build-templates/wechatgame/cloud_functions/cocos_cloud/index.js index e94f1fcf..f8720146 100644 --- a/build-templates/wechatgame/cloud_functions/cocos_cloud/index.js +++ b/build-templates/wechatgame/cloud_functions/cocos_cloud/index.js @@ -48,6 +48,53 @@ exports.main = async (event, context) => { msg: `Save fail, ${JSON.stringify(saveDataRes)}` }; } + } else if (cmd === "sync") { + // 双向同步:比较客户端与云端时间戳,新的一方覆盖另一方 + let clientData = event.data || {}; + let user = await getOrCreaterUser(db, wxContext.OPENID); + if (!user) { + return { code: -1, msg: "Get or create user fail" }; + } + + let cloudData = user.game_data || {}; + let clientTs = clientData.timestamp || 0; + let cloudTs = cloudData.timestamp || 0; + + if (clientTs > cloudTs) { + // 客户端较新:写入云端 + let saveDataRes = await db.collection(user_db_name).doc(user._id).update({ + data: { + game_data: _.set(clientData) + } + }); + console.log("Sync: client newer, saved to cloud:", saveDataRes, clientData); + if (saveDataRes?.stats?.updated >= 1) { + return { + code: 200, + updated: true, // 云端已被客户端数据覆盖 + data: { + openid: wxContext.OPENID, + game_data: clientData + } + }; + } else { + return { + code: -1, + msg: `Sync save fail, ${JSON.stringify(saveDataRes)}` + }; + } + } else { + // 云端较新或一致:下发云端数据 + console.log("Sync: cloud newer, return cloud data"); + return { + code: 200, + updated: false, // 客户端应使用云端数据 + data: { + openid: wxContext.OPENID, + game_data: cloudData + } + }; + } } return {