# TapTap 云存档接入指南

# 1. 简要说明

TapTap 云存档服务允许 Minigame 和 H5 游戏将玩家进度保存到云端,实现跨设备同步。

# 核心特性

  • 无需安装 SDKtap 是全局对象,直接使用。
  • 双层存储:数据需先写入本地文件系统 (tapfile://),再同步到云端。
  • 主要限制:单个存档最大 10MB,每分钟最多上传 1 次。

# 接入流程

# 2. 主要接口说明

详细参数请参考 官方 API 文档

# 核心管理器

  • CloudSaveManager: const cloud = tap.getCloudSaveManager() - 管理云端存档。
  • FileSystemManager: const fs = tap.getFileSystemManager() - 管理本地文件。

# 常用 API

方法 描述 文档链接
createArchive 创建新存档(需先写入本地文件) API 详情
updateArchive 更新已有存档(覆盖旧文件) API 详情
getArchiveList 获取当前用户的所有存档列表 API 详情
getArchiveData 下载云端存档到本地 API 详情
getArchiveCover 下载存档封面图片到本地 API 详情
deleteArchive 删除云端存档 API 详情
writeFile 将数据写入本地文件系统 API 详情
readFile 读取本地文件内容 API 详情

# 3. 代码示例

以下是一个封装好的存档管理器类,处理了本地读写、封面处理和云端同步的完整流程。

/**
 * 存档槽管理器 - 封装常用的云存档操作
 */
class SaveSlotManager {
  constructor() {
    this.cloudSaveManager = tap.getCloudSaveManager();
    this.fs = tap.getFileSystemManager();
    this.basePath = tap.env.USER_DATA_PATH; // 推荐存储路径: tapfile://usr
  }

  /**
   * 保存到指定槽位
   * @param {number} slotId - 槽位ID
   * @param {Object} gameData - 游戏数据
   * @param {string} [coverPath] - 封面图片路径(可选,需先保存到本地)
   */
  async save(slotId, gameData, coverPath = null) {
    const filePath = `${this.basePath}/slot_${slotId}.json`;
    const saveData = { ...gameData, savedAt: Date.now() };

    return new Promise((resolve, reject) => {
      // 1. 写入本地文件
      this.fs.writeFile({
        filePath,
        data: JSON.stringify(saveData),
        encoding: 'utf8',
        success: async () => {
          try {
            // 2. 检查是否存在该槽位
            const slots = await this.getSlots();
            const existing = slots.find(s => s.name === `slot_${slotId}`);
            
            const options = {
              archiveMetaData: {
                name: `slot_${slotId}`, // 注意:存档名不能含空格/中文
                summary: `Level ${gameData.level || 1} - ${new Date().toLocaleString()}`,
                playtime: gameData.playTime || 0
              },
              archiveFilePath: filePath,
              archiveCoverPath: coverPath, // 封面图片路径
              success: resolve,
              fail: reject
            };

            // 3. 更新或创建云端存档
            if (existing) {
              this.cloudSaveManager.updateArchive({ archiveUUID: existing.uuid, ...options });
            } else {
              this.cloudSaveManager.createArchive(options);
            }
          } catch (err) {
            reject(err);
          }
        },
        fail: reject
      });
    });
  }

  /**
   * 从指定槽位加载
   * @param {number} slotId - 槽位ID
   */
  async load(slotId) {
    const slots = await this.getSlots();
    const slot = slots.find(s => s.name === `slot_${slotId}`);
    if (!slot) throw new Error('存档不存在');

    return new Promise((resolve, reject) => {
      // 1. 下载存档
      this.cloudSaveManager.getArchiveData({
        archiveUUID: slot.uuid,
        archiveFileId: slot.fileId,
        targetFilePath: `${this.basePath}/slot_${slotId}_temp.json`,
        success: (res) => {
          // 2. 读取文件
          this.fs.readFile({
            filePath: res.filePath,
            encoding: 'utf8',
            success: (fileRes) => resolve(JSON.parse(fileRes.data)),
            fail: reject
          });
        },
        fail: reject
      });
    });
  }

  /**
   * 下载并获取封面图片路径
   * @param {number} slotId - 槽位ID
   */
  async loadCover(slotId) {
    const slots = await this.getSlots();
    const slot = slots.find(s => s.name === `slot_${slotId}`);
    if (!slot) return null;

    return new Promise((resolve, reject) => {
      this.cloudSaveManager.getArchiveCover({
        archiveUUID: slot.uuid,
        archiveFileId: slot.fileId,
        targetFilePath: `${this.basePath}/slot_${slotId}_cover.png`,
        success: (res) => resolve(res.filePath),
        fail: reject
      });
    });
  }

  /**
   * 获取所有存档列表
   */
  getSlots() {
    return new Promise((resolve, reject) => {
      this.cloudSaveManager.getArchiveList({
        success: (res) => resolve(res.saves),
        fail: reject
      });
    });
  }
}

// 使用示例
const saveManager = new SaveSlotManager();

// 保存(带封面)
// 假设已将截图保存到临时目录
const coverPath = `${tap.env.TEMP_DATA_PATH}/screenshot.png`;
saveManager.save(1, { level: 5, gold: 100 }, coverPath)
  .then(() => console.log('保存成功'));

// 加载数据
saveManager.load(1).then(data => console.log('加载数据:', data));

// 加载封面
saveManager.loadCover(1).then(path => console.log('封面已下载至:', path));

# 4. QA

Q: 为什么提示 "tap is not defined"? A: 请确保代码在 TapTap 客户端或 Minigame 环境中运行,不要在普通浏览器或 Node.js 中测试。

Q: 存档名有什么限制? A: 最大 60 字节,不能包含空格或中文字符,推荐使用 slot_1, auto_save 等格式。

Q: 如何处理上传频率限制(400001)? A: 云存档每分钟只能上传 1 次。建议在本地缓存数据,只在关键节点(如通关、手动保存)同步到云端。

Q: 存档文件和封面大小限制? A: 单个存档文件最大 10MB;封面图片最大 512KB,支持 PNG/JPG 格式。

Q: 如何处理多设备冲突? A: 在加载时对比本地与云端存档的 modifiedTime 或自定义版本号,引导用户选择保留哪一份。