Skip to main content

从 LeanCloud 迁移

本文档帮助您将项目从 LeanCloud 迁移到云开发 CloudBase。

迁移概览

功能对照

LeanCloudCloudBase说明
数据存储文档型数据库都是 JSON 文档存储
云引擎云函数/云托管后端代码托管
文件服务云存储文件存储服务
用户系统身份认证用户认证服务
即时通讯-需要自行实现或使用第三方
推送服务-需要使用腾讯云推送服务

数据存储迁移

1. 导出 LeanCloud 数据

在 LeanCloud 控制台导出数据:

  1. 进入「数据存储」→「数据管理」
  2. 选择需要导出的 Class
  3. 点击「导出」,选择 JSONL 格式(每行一个 JSON 对象)
导出格式说明

LeanCloud 导出的 JSONL 文件每行包含一条记录,例如:

{"objectId": "6666e6b6b6666666bb66b66b", "createdAt": "2025-07-02T07:58:45.609Z", "updatedAt": "2025-07-02T07:58:53.087Z", "email": "user@example.com", "username": "testuser"}

2. 数据格式转换

LeanCloud 和 CloudBase 的数据格式有差异,需要进行转换。我们提供了一键智能迁移脚本

  • ✅ 自动转换为 CloudBase 格式
  • ✅ 智能处理 JSONL 格式(每行一个对象)
  • ✅ 支持 Pointer 关联关系转换
  • ✅ 支持 GeoPoint 地理位置转换

字段转换对照表

LeanCloud 字段CloudBase 字段说明
objectId_id数据唯一 ID
objectId_openid云开发用户 ID
objectIdleancloud_objectId保留原始 ID,便于数据追溯
createdAt_createTimeISO 8601 → 毫秒时间戳
updatedAt_updateTimeISO 8601 → 毫秒时间戳
Pointer_ref_*关联引用标记(待手动处理)
GeoPoint{type, coordinates}GeoJSON 格式 [经度, 纬度]
ACL(删除)CloudBase 使用安全规则替代
authDatauid提取 uid 字段,删除 authData
其他字段(完整保留)email, username 等

使用迁移脚本

步骤 1:创建目录结构

migration/
├── leancloud-export/ # 放置 LeanCloud 导出的 JSONL 文件
├── cloudbase-import/ # 输出目录(自动创建)
└── cloudbase-migrate-leancloud.cjs # 迁移脚本

步骤 2:创建迁移脚本 cloudbase-migrate-leancloud.cjs

#!/usr/bin/env node

/**
* LeanCloud → CloudBase 数据迁移工具
*
* 功能:
* - 将 LeanCloud 数据格式转换为 CloudBase 格式
* - 映射 objectId → _id 和 _openid
* - 转换时间戳为毫秒格式
* - 支持 Pointer 和 GeoPoint 类型转换
*/

const fs = require('fs');
const path = require('path');

// 配置
const CONFIG = {
inputDir: path.join(__dirname, 'leancloud-export'),
outputDir: path.join(__dirname, 'cloudbase-import'),
keepOriginalId: true,
excludeFields: ['ACL', '__type', 'className'],
};

// 转换 ISO 8601 时间为毫秒时间戳
function convertTimestamp(isoString) {
if (!isoString) return null;
try {
return new Date(isoString).getTime();
} catch (error) {
console.warn(`⚠️ 时间转换失败: ${isoString}`);
return null;
}
}

// 提取 authData 中的 uid
function extractAuthDataUid(authData) {
if (!authData || typeof authData !== 'object') return null;

for (const provider of Object.keys(authData)) {
const providerData = authData[provider];
if (providerData?.uid) return providerData.uid;
}
return null;
}

// 转换 Pointer 类型
function convertPointer(pointer, fieldName) {
if (!pointer || pointer.__type !== 'Pointer') return pointer;

return {
_ref_className: pointer.className,
_ref_objectId: pointer.objectId,
_ref_note: `需手动替换为 CloudBase _id (原字段: ${fieldName})`
};
}

// 转换 GeoPoint 类型为 GeoJSON 格式
function convertGeoPoint(geoPoint) {
if (!geoPoint || geoPoint.__type !== 'GeoPoint') return geoPoint;

const { latitude, longitude } = geoPoint;
if (typeof latitude !== 'number' || typeof longitude !== 'number') {
console.warn(`⚠️ GeoPoint 坐标无效`);
return geoPoint;
}

// CloudBase 使用 GeoJSON 格式: [经度, 纬度]
return {
type: 'Point',
coordinates: [longitude, latitude]
};
}

// 递归转换特殊类型
function convertSpecialTypes(obj, fieldName = 'root') {
if (!obj || typeof obj !== 'object') return obj;

if (obj.__type === 'Pointer') return convertPointer(obj, fieldName);
if (obj.__type === 'GeoPoint') return convertGeoPoint(obj);

if (Array.isArray(obj)) {
return obj.map((item, index) => convertSpecialTypes(item, `${fieldName}[${index}]`));
}

const result = {};
Object.keys(obj).forEach(key => {
result[key] = convertSpecialTypes(obj[key], key);
});
return result;
}

// 转换单条记录
function convertRecord(leancloudRecord) {
const cloudbaseRecord = {};

// 映射 objectId
if (leancloudRecord.objectId) {
cloudbaseRecord._id = leancloudRecord.objectId;
cloudbaseRecord._openid = leancloudRecord.objectId;
if (CONFIG.keepOriginalId) {
cloudbaseRecord.leancloud_objectId = leancloudRecord.objectId;
}
}

// 转换时间戳
if (leancloudRecord.createdAt) {
cloudbaseRecord._createTime = convertTimestamp(leancloudRecord.createdAt);
}
if (leancloudRecord.updatedAt) {
cloudbaseRecord._updateTime = convertTimestamp(leancloudRecord.updatedAt);
}

// 提取 authData 中的 uid
if (leancloudRecord.authData) {
const uid = extractAuthDataUid(leancloudRecord.authData);
if (uid) cloudbaseRecord.uid = uid;
}

// 复制其他字段并转换特殊类型
Object.keys(leancloudRecord).forEach(key => {
if (!['objectId', 'createdAt', 'updatedAt', 'authData', ...CONFIG.excludeFields].includes(key)) {
cloudbaseRecord[key] = convertSpecialTypes(leancloudRecord[key], key);
}
});

return cloudbaseRecord;
}

// 转换 JSONL 文件
function convertFile(inputFile, outputFile) {
console.log(`\n📄 转换文件: ${path.basename(inputFile)}`);

const content = fs.readFileSync(inputFile, 'utf-8').trim();
if (!content) {
console.log(' ⚠️ 文件为空,跳过');
return { records: 0 };
}

const lines = content.split(/\r?\n/).filter(line => line.trim());
console.log(` 找到 ${lines.length} 行数据`);

const cloudbaseRecords = [];
let pointerCount = 0, geoPointCount = 0;

lines.forEach((line, index) => {
try {
const record = convertRecord(JSON.parse(line));
const recordStr = JSON.stringify(record);
if (recordStr.includes('_ref_')) pointerCount++;
if (recordStr.includes('"type":"Point"')) geoPointCount++;
cloudbaseRecords.push(record);
} catch (error) {
console.log(` ⚠️ 第 ${index + 1} 行解析失败: ${error.message}`);
}
});

if (pointerCount > 0) console.log(` 🔗 检测到 ${pointerCount} 条 Pointer 引用`);
if (geoPointCount > 0) console.log(` 📍 检测到 ${geoPointCount} 条 GeoPoint`);

// 输出 JSONL 格式
const jsonlContent = cloudbaseRecords.map(r => JSON.stringify(r)).join('\n');
fs.writeFileSync(outputFile, jsonlContent, 'utf-8');

console.log(` ✅ 转换成功: ${cloudbaseRecords.length} 条记录`);
return { records: cloudbaseRecords.length };
}

// 主函数
function main() {
console.log('🚀 开始 LeanCloud → CloudBase 数据迁移\n');

if (!fs.existsSync(CONFIG.inputDir)) {
console.error(`❌ 输入目录不存在: ${CONFIG.inputDir}`);
return;
}

if (!fs.existsSync(CONFIG.outputDir)) {
fs.mkdirSync(CONFIG.outputDir, { recursive: true });
}

const files = fs.readdirSync(CONFIG.inputDir).filter(f => f.endsWith('.jsonl'));

if (files.length === 0) {
console.error('❌ 未找到 JSONL 文件');
return;
}

console.log(`📁 找到 ${files.length} 个文件待处理`);

let totalRecords = 0;
files.forEach(file => {
const inputFile = path.join(CONFIG.inputDir, file);
const outputFile = path.join(CONFIG.outputDir, file.replace('.jsonl', '.json'));
const result = convertFile(inputFile, outputFile);
totalRecords += result.records;
});

console.log('\n============================================================');
console.log(`🎉 迁移完成! 共转换 ${totalRecords} 条记录`);
console.log(` 输出目录: ${CONFIG.outputDir}`);
console.log('\n📌 下一步: 在 CloudBase 控制台导入转换后的文件');
}

main();

步骤 3:运行迁移脚本

node cloudbase-migrate-leancloud.cjs

输出示例

🚀 开始 LeanCloud → CloudBase 数据迁移

📁 找到 1 个文件待处理

📄 转换文件: lc_user_masked.jsonl
找到 1162 行数据
🔗 检测到 25 条 Pointer 引用
📍 检测到 10 条 GeoPoint
✅ 转换成功: 1162 条记录

============================================================
🎉 迁移完成! 共转换 1162 条记录
输出目录: cloudbase-import

📌 下一步: 在 CloudBase 控制台导入转换后的文件

转换示例

LeanCloud 原始数据

{"objectId": "6666e6b6b6666666bb66b66b", "createdAt": "2025-07-02T07:58:45.609Z", "updatedAt": "2025-07-02T07:58:53.087Z", "email": "user@example.com", "username": "testuser"}

CloudBase 转换后

{"_id":"6666e6b6b6666666bb66b66b","_openid":"6666e6b6b6666666bb66b66b","leancloud_objectId":"6666e6b6b6666666bb66b66b","_createTime":1751443125609,"_updateTime":1751443133087,"email":"user@example.com","username":"testuser"}

Pointer 关联关系转换

LeanCloud 原始数据

{
"objectId": "post123",
"title": "测试文章",
"author": {
"__type": "Pointer",
"className": "_User",
"objectId": "user123"
}
}

CloudBase 转换后

{
"_id": "post123",
"title": "测试文章",
"author": {
"_ref_className": "_User",
"_ref_objectId": "user123",
"_ref_note": "需手动替换为 CloudBase _id (原字段: author)"
}
}
Pointer 处理

Pointer 引用会被标记为 _ref_* 字段,导入后需要手动或通过脚本将 _ref_objectId 替换为对应的 CloudBase _id

GeoPoint 地理位置转换

LeanCloud 原始数据

{
"location": {
"__type": "GeoPoint",
"latitude": 22.5431,
"longitude": 114.0579
}
}

CloudBase 转换后(GeoJSON 格式)

{
"location": {
"type": "Point",
"coordinates": [114.0579, 22.5431]
}
}

GeoPoint 会自动转换为 GeoJSON 格式,坐标顺序为 [经度, 纬度],CloudBase 导入时会自动识别。

3. 导入到 CloudBase

在控制台操作:

  1. 登录 云开发控制台
  2. 进入「文档型数据库」→「数据管理」
  3. 创建对应的集合
  4. 点击「导入」上传转换后的 JSON 文件
文件大小限制

控制台导入限制最大 50MB。如果数据文件超过此限制,请使用下方的批量写入脚本。

4. 大数据量批量写入

当数据量较大(超过 50MB)时,使用脚本批量写入:

// batch-import.js
const cloudbase = require('@cloudbase/node-sdk');
const fs = require('fs');

const app = cloudbase.init({
env: 'your-env-id', // 替换为你的环境 ID
secretId: 'your-secret-id', // 替换为你的 SecretId
secretKey: 'your-secret-key', // 替换为你的 SecretKey
});

const db = app.database();
const BATCH_SIZE = 100; // 每批写入条数

async function batchImport(collectionName, jsonFile) {
const content = fs.readFileSync(jsonFile, 'utf-8').trim();
const lines = content.split(/\r?\n/).filter(line => line.trim());
const total = lines.length;
let imported = 0;

console.log(`开始导入 ${total} 条数据到 ${collectionName}...`);

for (let i = 0; i < total; i += BATCH_SIZE) {
const batch = lines.slice(i, i + BATCH_SIZE).map(line => JSON.parse(line));

const tasks = batch.map(item => db.collection(collectionName).add(item));
await Promise.all(tasks);

imported += batch.length;
console.log(`进度: ${imported}/${total} (${(imported/total*100).toFixed(1)}%)`);
}

console.log(`导入完成!共 ${imported} 条数据`);
}

// 使用示例
batchImport('your-collection', 'cloudbase-import/your-file.json');

安装依赖并运行:

npm install @cloudbase/node-sdk
node batch-import.js
tip
  • SecretId 和 SecretKey 可在 腾讯云访问管理 获取
  • 建议将大文件拆分为多个小文件,便于断点续传

5. 配置安全规则

由于 CloudBase 使用安全规则替代 LeanCloud 的 ACL 权限,导入数据后需要配置安全规则:

示例 1:公开读,仅创建者可写

{
"read": true,
"write": "doc._openid == auth.openid"
}

示例 2:仅登录用户可读写

{
"read": "auth != null",
"write": "auth != null"
}

示例 3:基于角色的权限

{
"read": "auth != null",
"write": "get('database.users.${auth.uid}').role == 'admin'"
}

更多安全规则配置请参考 安全规则文档

云引擎迁移

LeanCloud 云引擎项目可以迁移到 CloudBase 云托管或云函数。我们提供了详细的迁移指南,包括:

  • leanengine.yaml 配置映射:如何将 LeanCloud 的配置转换为云托管部署配置
  • 无 Dockerfile 部署:云托管支持自动识别框架,无需编写 Dockerfile
  • Hook 迁移:beforeSave/afterSave 等钩子如何通过云函数封装实现
  • 定时任务迁移:Cron 任务如何迁移到云函数定时触发器
  • 完整的功能对照表:LeanCloud API 与 CloudBase API 的对应关系
完整迁移指南

请参阅 LeanCloud 云引擎迁移至云托管/云函数指南,获取详细的迁移步骤和代码示例。

快速对照

LeanCloud 云引擎功能CloudBase 对应方案
leanengine.yaml 配置云托管部署配置
AV.Cloud.define()云函数 exports.main
AV.Cloud.beforeSave()云函数封装数据操作
AV.Cloud.afterSave()云函数封装数据操作
定时任务(Cron)定时触发器
环境变量云托管/云函数环境变量

环境变量迁移

在云托管或云函数控制台配置环境变量,替换原有的 LeanCloud 环境变量:

LeanCloud 环境变量CloudBase 替代说明
LEANCLOUD_APP_IDENV_IDCloudBase 环境 ID
LEANCLOUD_APP_KEY不需要云托管内部自动鉴权
LEANCLOUD_APP_MASTER_KEY不需要云托管内部自动鉴权
LEANCLOUD_APP_PORTPORT服务监听端口
LEANCLOUD_APP_ENVTCB_ENV环境标识(可自定义)

原有在 LeanCloud 控制台配置的自定义环境变量,需要在云托管或云函数控制台重新配置。

Hook 迁移方案

CloudBase 不支持数据库触发器,LeanCloud 的 beforeSaveafterSave 等 Hook 需要通过云函数封装数据操作来实现:

迁移思路:将数据库操作封装在云函数中,客户端通过调用云函数来操作数据,而不是直接操作数据库。

LeanCloud beforeSave Hook:

// LeanCloud - 自动在保存前处理
AV.Cloud.beforeSave('Todo', async (request) => {
const todo = request.object;
if (!todo.get('title')) {
throw new AV.Cloud.Error('标题不能为空');
}
todo.set('status', 'pending');
});

CloudBase 云函数封装:

// CloudBase 云函数 - functions/addTodo/index.js
const cloudbase = require('@cloudbase/node-sdk');

const app = cloudbase.init({ env: process.env.ENV_ID });
const db = app.database();

exports.main = async (event, context) => {
const { title, content } = event;

// beforeSave 逻辑:验证和预处理
if (!title) {
return { success: false, error: '标题不能为空' };
}

// 添加数据
const result = await db.collection('Todo').add({
title,
content,
status: 'pending', // 自动设置默认值
_createTime: Date.now(),
});

// afterSave 逻辑:后续处理(如发送通知)
// await sendNotification(result.id);

return { success: true, id: result.id };
};

客户端调用:

// 客户端通过云函数操作数据,而非直接操作数据库
const result = await app.callFunction({
name: 'addTodo',
data: { title: '新任务', content: '任务内容' }
});
迁移建议
  • 将所有需要 Hook 的数据操作封装为云函数
  • 客户端统一通过云函数进行数据操作
  • 可以在云函数中实现验证、默认值、关联操作等逻辑

简单示例

LeanCloud 云函数:

// LeanCloud
const AV = require('leanengine');

AV.Cloud.define('hello', async (request) => {
const { name } = request.params;
return { message: `Hello, ${name}!` };
});

CloudBase 云函数:

// CloudBase 云函数
exports.main = async (event, context) => {
const { name } = event;
return { message: `Hello, ${name}!` };
};

CloudBase 云托管(Express):

// CloudBase 云托管
const express = require('express');
const app = express();

app.use(express.json());

app.post('/hello', (req, res) => {
const { name } = req.body;
res.json({ message: `Hello, ${name}!` });
});

const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});

文件存储迁移

我们提供了官方迁移脚本工具,可以帮助你将 LeanCloud 存储的文件批量迁移到腾讯云开发云存储。

下载迁移脚本

本迁移方案需要使用官方提供的迁移脚本工具。请先下载脚本包:

➡️ 下载 LeanCloud 迁移工具

🚀 下载后解压到本地,请仔细阅读 README.md 文档,按照以下步骤进行配置和运行。

功能特性

  • ✅ 从 LeanCloud _File 表读取所有文件元数据
  • ✅ 自动拼接 URL 并批量下载文件
  • ✅ 批量上传到腾讯云开发云存储
  • ✅ 支持并发控制,提高迁移效率
  • ✅ 自动重试失败的下载
  • ✅ 生成详细的迁移报告

项目结构

原始结构(解压后)

这是你解压 zip 包后看到的文件结构:

leancloud_storage_migrate/
├── migrate.js # 主迁移脚本(核心文件)
├── package.json # 项目依赖配置文件
└── README.md # 项目说明文档
说明

这 3 个文件是项目的原始文件,需要手动配置后才能使用。

安装依赖、脚本运行后的结构

leancloud_storage_migrate/
├── migrate.js # 主迁移脚本(核心文件)
├── package.json # 项目依赖配置文件
├── package-lock.json # 依赖版本锁定文件(自动生成)
├── README.md # 项目说明文档
├── .gitignore # Git 忽略文件配置
├── node_modules/ # 依赖包目录(npm install 后生成)
├── temp_files/ # 临时文件存储目录(运行时自动创建,完成后自动删除)
└── migration_report.json # 迁移报告文件(运行完成后生成)

快速开始

第一步:安装 Node.js 依赖

在项目目录下运行以下命令安装所需依赖:

cd /path/to/leancloud_storage_migrate
npm install

这将自动安装以下依赖包:

  • leancloud-storage - LeanCloud SDK
  • @cloudbase/node-sdk - 腾讯云开发 Node.js SDK

第二步:配置参数

打开 migrate.js 文件,找到配置区域(约在第 17-32 行),填入你的配置信息:

// LeanCloud 配置
const LEANCLOUD_CONFIG = {
appId: 'YOUR_LEANCLOUD_APP_ID', // 👈 替换为你的 LeanCloud App ID
appKey: 'YOUR_LEANCLOUD_APP_KEY', // 👈 替换为你的 LeanCloud App Key
serverURL: 'https://YOUR_LEANCLOUD_SERVER_URL', // 👈 替换为你的 API 域名
fileDomain: 'https://YOUR_FILE_DOMAIN' // 👈 替换为你的文件域名
};

// 腾讯云开发配置
const CLOUDBASE_CONFIG = {
env: 'YOUR_ENV_ID', // 腾讯云开发环境 ID
secretId: 'YOUR_SECRET_ID', // 腾讯云 API 密钥 SecretId
secretKey: 'YOUR_SECRET_KEY' // 腾讯云 API 密钥 SecretKey
};
获取配置信息说明

LeanCloud 配置:

  1. 登录 LeanCloud 控制台
  2. 选择你的应用
  3. 进入「设置」→「应用凭证」,获取 App IDApp Key
  4. 进入「设置」→「域名绑定」,获取 API 域名(serverURL)和文件域名(fileDomain

腾讯云开发配置:

  1. 登录 腾讯云开发平台
  2. 选择你的环境,获取环境 ID(env
  3. 登录 腾讯云 API 密钥管理
  4. 创建或查看密钥,获取 SecretIdSecretKey

⚠️ 重要:配置安全域名

  • 登录 腾讯云开发平台
  • 进入「环境管理」→「安全来源」
  • 添加你的域名到安全域名列表(避免 CORS 错误)

第三步:运行迁移脚本

配置完成后,执行以下命令启动迁移:

npm start

或者直接使用 Node.js 运行:

node migrate.js

第四步:查看运行结果

脚本运行时会实时显示进度信息:

========================================
LeanCloud → 腾讯云开发 文件迁移工具
========================================

开始从 LeanCloud 查询 _File 表...
已查询 50 个文件...
✓ 共查询到 50 个文件

开始迁移文件,并发数: 5
----------------------------------------

[1] 处理文件: example.jpg
下载 URL: https://xxx.com/xxx.jpg
正在下载...
✓ 下载完成
正在上传到云开发: migrated/example.jpg
✓ 上传完成,fileID: cloud://xxx...

[2] 处理文件: photo.png
下载 URL: https://xxx.com/yyy.png
正在下载...
✓ 下载完成
正在上传到云开发: migrated/photo.png
✓ 上传完成,fileID: cloud://yyy...

...

========================================
迁移完成!
----------------------------------------

总计: 50 个文件
成功: 48 个
失败: 2 个

迁移报告已保存到: ./migration_report.json

完成后会在当前目录生成 migration_report.json 报告文件。

可选配置

你可以调整以下配置来优化迁移性能:

const DOWNLOAD_CONFIG = {
tempDir: './temp_files', // 临时文件存储目录
concurrency: 5, // 并发下载数量(建议 3-10)
retryTimes: 3, // 下载失败重试次数
timeout: 30000 // 请求超时时间(毫秒)
};

迁移流程

  1. 查询文件列表:从 LeanCloud _File 表查询所有文件
  2. 下载文件:根据 key 拼接 URL 并下载到本地临时目录
  3. 上传文件:将文件上传到腾讯云开发云存储
  4. 生成报告:生成 migration_report.json 报告文件
  5. 清理临时文件:删除本地临时文件

输出结果

迁移完成后,会生成 migration_report.json 报告文件,包含:

{
"timestamp": "2026-01-21T10:30:00.000Z",
"summary": {
"total": 100,
"success": 98,
"failed": 2
},
"successList": [
{
"objectId": "xxx",
"name": "example.jpg",
"fileID": "cloud://xxx.cloudbase.net/xxx",
"cloudPath": "migrated/example.jpg"
}
],
"errorList": [
{
"objectId": "yyy",
"name": "failed.png",
"error": "下载超时"
}
]
}

注意事项

  1. 网络稳定性:确保网络连接稳定,大文件迁移可能需要较长时间
  2. 磁盘空间:确保有足够的磁盘空间存储临时文件
  3. 并发数量:根据网络带宽调整并发数,避免过高导致请求失败
  4. 重试机制:失败的文件会自动重试,最终失败的会记录在报告中
  5. 云端路径:所有文件会上传到 migrated/ 目录下,可根据需要修改
  6. fileID 保存:上传后的 fileID 会记录在报告中,建议保存以便后续使用

故障排查

下载失败

  • 检查 fileDomain 配置是否正确
  • 检查网络连接是否正常
  • 尝试增加 timeout 超时时间

上传失败

  • 确认腾讯云开发环境 ID 正确
  • 检查是否配置了安全域名
  • 确认云存储配额是否充足

CORS 错误

  • 登录腾讯云开发控制台
  • 在「安全配置」→「安全来源」中添加你的域名

用户系统迁移

本节帮助你将 LeanCloud 用户体系(_User 表)迁移到 CloudBase 用户体系。

迁移目标

  • 将 LeanCloud objectId 写入 CloudBase 用户表(使用管理端 API 的 Uid 字段),实现原 ID 保持不变
  • 同步用户关联字段(如 name、昵称、手机号、邮箱、头像等)
  • 迁移完成后,依据安全规则重新配置你的业务权限逻辑(替代 LeanCloud ACL)

登录方式对比

迁移方案会强依赖你当前使用的登录方式:

  • 如果你使用 用户名/邮箱/手机号 + 密码:需要提前规划"密码无法直接迁移"的过渡方案(验证码登录优先 / 临时密码 / 双栈)。
  • 如果你使用 邮箱/手机号 + 验证码:迁移相对简单,重点是把邮箱/手机号字段写对,并保证安全规则与业务权限正确。
登录方式LeanCloudCloudBase
用户名 + 密码✅ 内置支持内置支持
邮箱 + 验证码✅ 内置支持内置支持
手机号 + 验证码✅ 内置支持内置支持
微信登录✅ 第三方登录深度支持
UnionID 打通✅ 支持✅ 支持
QQ 登录✅ 第三方登录✅ OAuth 支持
微博登录✅ 第三方登录✅ OAuth 支持
Apple 登录✅ 内置支持🕐 即将支持
匿名登录✅ 内置支持✅ 内置支持
自定义登录❌ 不支持支持

CloudBase 与 LeanCloud 用户体系的差异

  • 用户表结构:CloudBase 用户体系的内置字段是固定集合(如 UidNamePhoneEmail 等),你的业务自定义字段建议放入自建集合(如 user_profile),用 Uid 关联。
  • 密码迁移:LeanCloud 通常不会允许导出明文密码;即使你能拿到 hash,也无法直接写入 CloudBase 用户体系。你需要选择一种"迁移后重置/重设"的策略(下文提供方案)。
  • 用户名校验规则差异
    • CloudBase 管理端 API 创建用户时,Name 支持 @ 等特殊字符(更宽松)。
    • 但"用户自主注册"的前端/客户端校验规则可能更严格(例如不允许 @),两者不完全一致。

迁移流程

  1. 导出 LeanCloud 用户数据(从 _User 导出为 JSON)
  2. 字段映射与清洗objectIdUid,处理 name/phone/email 的对应关系)
  3. 调用 CloudBase 管理端 API 批量创建用户CreateUser
  4. 同步自定义业务字段(写入自建集合,按 Uid 关联)
  5. 配置安全规则与权限逻辑(替代 LeanCloud ACL)
  6. 抽样校验与回滚预案DescribeUserList / DeleteUsers

前置准备

  • CloudBase 环境:准备好 EnvId(环境 ID)。
  • 访问凭证:准备好可调用 CloudBase 管理端 API 的腾讯云密钥(SecretId/SecretKey)。建议:
    • 使用子账号 + 最小权限策略
    • 密钥通过环境变量注入,避免写入代码或文档
  • 接口文档CloudBase 身份认证 / 用户管理 API 文档

字段映射

以 LeanCloud _User 常见字段为例(你的实际字段以导出为准):

LeanCloud 字段说明CloudBase 字段写入方式
objectId用户唯一标识Uid必填:写入 CreateUser.Uid,实现"原 ID 保持不变"
name / username登录使用的用户名(可能是用户名 / 手机号 / 邮箱地址)Name建议优先写入 Name
mobilePhoneNumber手机号Phone如存在且合规,写入 Phone,写入后用户可以使用手机号验证码登录
email邮箱Email如存在且合规,写入 Email,写入后用户可以使用邮箱验证码登录
nickname昵称NickName可选
avatar / avatarUrl头像链接AvatarUrl可选(需公网可访问)
其他业务字段如会员等级、渠道、邀请关系等不建议写入用户体系表建议写入自建集合 user_profile,以 Uid 关联
关于"手机号/邮箱 + 密码登录"的特殊说明

如果你的 LeanCloud 业务中,用户使用 手机号 + 密码邮箱 + 密码 登录,而你又把手机号/邮箱存放在 name(或 username)里,那么迁移到 CloudBase 时:

  • 除了把 LeanCloud 的 name 写入 CloudBase 的 Name ,还需要把 LeanCloud 的 name 同步到 CloudBase 的 Phone 和/或 Email 字段,否则用户将无法按原方式登录。
  • 实操建议:
    • name 形如邮箱(包含 @ 且满足邮箱格式)→ 写入 Email = name
    • name 形如手机号(11 位数字等)→ 写入 Phone = name
    • 如果 name 不是手机号/邮箱,则不要强行写入 Phone/Email,请使用你原本的 mobilePhoneNumber/email 字段,或引导用户改用验证码登录。

密码迁移策略

因为无法迁移原密码,通常有三种可落地方案:

方案说明适用场景
方案 1:验证码登录优先迁移后先引导用户用短信/邮件验证码登录,再在"设置密码"页面重新设置密码推荐,用户体验好
方案 2:设置临时密码导入时为每个用户生成临时密码(脚本可生成随机强密码),首次登录后强制修改需要通知用户临时密码
方案 3:登录态迁移如果你有自研鉴权/SSO,可短期双栈:登录走旧系统,业务数据逐步切换到 CloudBase;最终再统一密码体系复杂场景

迁移脚本

我们提供了一键迁移脚本,功能包括:

  • ✅ 读取 LeanCloud 导出的用户 JSON(数组)
  • ✅ 调用 CloudBase 管理端 API CreateUser 批量创建用户
  • ✅ 将 objectId 写入 Uid,保持原 ID 不变
  • ✅ 若用户已存在(返回重复数据),自动调用 ModifyUser 做字段更新(默认不重置密码
  • ✅ 可选:将 name 镜像到 Phone/Email(用于手机号/邮箱密码登录场景)
  • ✅ 支持多种密码模式:不写入密码 / 统一默认密码 / 为每个用户生成随机临时密码

1. 准备 LeanCloud 导出文件

在 LeanCloud 控制台导出 _User 表为 JSON 数组格式,示例:

[
{
"objectId": "6864e6b5b9570274df85a79e",
"username": "sample@163.com",
"email": "sample@163.com",
"emailVerified": true,
"mobilePhoneVerified": false,
"nickname": "sample1",
"createdAt": "2025-07-02T07:58:45.609Z",
"updatedAt": "2025-07-02T07:58:53.087Z",
"ACL": { "*": { "read": true, "write": true } }
},
{
"objectId": "6870cf46bf6125518c3c7979",
"username": "88888888@qq.com",
"email": "88888888@qq.com",
"emailVerified": false,
"mobilePhoneVerified": false,
"nickname": "sample2",
"createdAt": "2025-07-11T08:45:58.532Z",
"updatedAt": "2025-07-11T08:45:58.532Z",
"ACL": { "*": { "read": true, "write": true } }
},
{
"objectId": "6893900f6f00df1bd11a0499",
"username": "4umxxxxfb0",
"emailVerified": false,
"mobilePhoneVerified": false,
"nickname": "sample3",
"createdAt": "2025-08-06T17:25:35.704Z",
"updatedAt": "2026-01-10T11:37:08.378Z",
"authData": {
"lc_apple": {
"uid": "001842.56ca44c39bf84bb4aaa377c87fe57613.1555",
"token_type": "Bearer",
"expires_in": 3600
}
},
"ACL": { "*": { "read": true, "write": true } }
}
]

2. 创建迁移脚本

创建 migrate-leancloud-users-to-cloudbase.js 文件:

/*
* LeanCloud -> CloudBase 用户迁移脚本示例
*
* 功能:
* - 读取 LeanCloud 导出的用户 JSON(数组)
* - 调用 CloudBase 管理端 API CreateUser 批量创建用户
* - 将 objectId 写入 Uid,Name/Phone/Email/NickName 等字段按映射写入
*
* 安全说明:
* - 不要在代码中硬编码 SecretId/SecretKey,请用环境变量注入
* - 生产环境建议使用子账号最小权限
*/

'use strict';

const fs = require('fs');
const https = require('https');
const crypto = require('crypto');

function mustGetEnv(name) {
const v = process.env[name];
if (!v) throw new Error(`Missing required env: ${name}`);
return v;
}

function getEnv(name, defaultValue) {
const v = process.env[name];
return v == null || v === '' ? defaultValue : v;
}

function sha256Hex(str) {
return crypto.createHash('sha256').update(str, 'utf8').digest('hex');
}

function hmacSha256(key, msg, output = 'buffer') {
return crypto.createHmac('sha256', key).update(msg, 'utf8').digest(output);
}

function toDateYmd(tsSeconds) {
const d = new Date(tsSeconds * 1000);
const yyyy = d.getUTCFullYear();
const mm = String(d.getUTCMonth() + 1).padStart(2, '0');
const dd = String(d.getUTCDate()).padStart(2, '0');
return `${yyyy}-${mm}-${dd}`;
}

function isLikelyEmail(s) {
if (!s) return false;
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(s).trim());
}

function isLikelyCnPhone(s) {
if (!s) return false;
return /^\d{11}$/.test(String(s).trim());
}

function normalizeUserType(v) {
const t = String(v || '').trim();
if (!t) return 'internalUser';
if (t === 'internalUser' || t === 'externalUser') return t;
throw new Error('USER_TYPE must be one of: internalUser | externalUser');
}

function generateStrongPassword(length = 16) {
const lower = 'abcdefghijklmnopqrstuvwxyz';
const upper = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
const digits = '0123456789';
const special = '()!@#$%^&*|?><_-';

const pick = (chars) => chars[crypto.randomInt(0, chars.length)];

const first = crypto.randomInt(0, 2) === 0 ? pick(lower + upper) : pick(digits);
const required = [pick(lower), pick(upper), pick(digits), pick(special)];

const all = lower + upper + digits + special;
const restLen = Math.max(8, Math.min(32, length)) - 1 - required.length;
const rest = Array.from({ length: restLen }, () => pick(all));

const arr = [...required, ...rest];
for (let i = arr.length - 1; i > 0; i--) {
const j = crypto.randomInt(0, i + 1);
[arr[i], arr[j]] = [arr[j], arr[i]];
}

return first + arr.join('');
}

function tc3BuildAuthHeader({
secretId,
secretKey,
service,
host,
action,
timestamp,
region,
payloadJson,
}) {
const algorithm = 'TC3-HMAC-SHA256';
const date = toDateYmd(timestamp);

const httpRequestMethod = 'POST';
const canonicalUri = '/';
const canonicalQuerystring = '';

const canonicalHeaders =
`content-type:application/json; charset=utf-8\n` +
`host:${host}\n`;

const signedHeaders = 'content-type;host';
const hashedRequestPayload = sha256Hex(payloadJson);

const canonicalRequest =
`${httpRequestMethod}\n` +
`${canonicalUri}\n` +
`${canonicalQuerystring}\n` +
`${canonicalHeaders}\n` +
`${signedHeaders}\n` +
`${hashedRequestPayload}`;

const credentialScope = `${date}/${service}/tc3_request`;
const stringToSign =
`${algorithm}\n` +
`${timestamp}\n` +
`${credentialScope}\n` +
`${sha256Hex(canonicalRequest)}`;

const secretDate = hmacSha256(`TC3${secretKey}`, date);
const secretService = hmacSha256(secretDate, service);
const secretSigning = hmacSha256(secretService, 'tc3_request');
const signature = hmacSha256(secretSigning, stringToSign, 'hex');

const authorization =
`${algorithm} ` +
`Credential=${secretId}/${credentialScope}, ` +
`SignedHeaders=${signedHeaders}, ` +
`Signature=${signature}`;

const headers = {
Authorization: authorization,
'Content-Type': 'application/json; charset=utf-8',
Host: host,
'X-TC-Action': action,
'X-TC-Timestamp': String(timestamp),
'X-TC-Version': '2018-06-08',
};

if (region) headers['X-TC-Region'] = region;

return headers;
}

function httpsJsonRequest({ host, path, headers, bodyJson }) {
return new Promise((resolve, reject) => {
const req = https.request(
{
hostname: host,
method: 'POST',
path,
headers,
},
(res) => {
let raw = '';
res.setEncoding('utf8');
res.on('data', (chunk) => (raw += chunk));
res.on('end', () => {
let parsed;
try {
parsed = JSON.parse(raw);
} catch (e) {
return reject(new Error(`Non-JSON response: ${raw.slice(0, 500)}`));
}

const resp = parsed && parsed.Response;
if (resp && resp.Error) {
const err = new Error(`${resp.Error.Code}: ${resp.Error.Message}`);
err.code = resp.Error.Code;
err.requestId = resp.RequestId;
err.httpStatus = res.statusCode;
return reject(err);
}

resolve({ statusCode: res.statusCode, data: parsed });
});
}
);

req.on('error', reject);
req.write(bodyJson);
req.end();
});
}

async function callTencentCloud({ action, payload }) {
const secretId = mustGetEnv('TENCENT_SECRET_ID');
const secretKey = mustGetEnv('TENCENT_SECRET_KEY');
const region = getEnv('TENCENT_REGION', '');
const host = getEnv('TENCENT_ENDPOINT', 'tcb.tencentcloudapi.com');

const timestamp = Math.floor(Date.now() / 1000);
const payloadJson = JSON.stringify(payload);

const headers = tc3BuildAuthHeader({
secretId,
secretKey,
service: 'tcb',
host,
action,
timestamp,
region,
payloadJson,
});

const resp = await httpsJsonRequest({
host,
path: '/',
headers,
bodyJson: payloadJson,
});

return resp.data;
}

function normalizeLeanCloudUser(u) {
const objectId = u.objectId || u.objectID || u.id;
const name = u.name || u.username || u.userName;

return {
objectId: objectId ? String(objectId) : '',
name: name ? String(name) : '',
phone: (u.mobilePhoneNumber || u.phone || u.mobile || '').toString(),
email: (u.email || '').toString(),
nickname: (u.nickname || u.nickName || '').toString(),
avatarUrl: (u.avatarUrl || u.avatar || '').toString(),
description: (u.description || '').toString(),
raw: u,
};
}

function mapToCloudBaseCreateUser({ envId, leanUser, userType, mirrorNameToLogin, setPasswordMode, defaultPassword }) {
if (!leanUser.objectId) throw new Error('Missing objectId');

const payload = {
EnvId: envId,
Uid: leanUser.objectId,
Name: leanUser.name || leanUser.objectId,
Type: userType,
UserStatus: 'ACTIVE',
};

if (leanUser.nickname) payload.NickName = leanUser.nickname;
if (leanUser.avatarUrl) payload.AvatarUrl = leanUser.avatarUrl;
if (leanUser.description) payload.Description = leanUser.description;

const trimmedName = (leanUser.name || '').trim();

if (leanUser.phone && isLikelyCnPhone(leanUser.phone)) {
payload.Phone = leanUser.phone.trim();
} else if (mirrorNameToLogin && isLikelyCnPhone(trimmedName)) {
payload.Phone = trimmedName;
}

if (leanUser.email && isLikelyEmail(leanUser.email)) {
payload.Email = leanUser.email.trim();
} else if (mirrorNameToLogin && isLikelyEmail(trimmedName)) {
payload.Email = trimmedName;
}

if (setPasswordMode === 'default') {
if (!defaultPassword) throw new Error('SET_PASSWORD_MODE=default requires DEFAULT_PASSWORD');
payload.Password = defaultPassword;
} else if (setPasswordMode === 'random') {
payload.Password = generateStrongPassword(16);
}

return payload;
}

function mapToCloudBaseModifyUser({ envId, leanUser, mirrorNameToLogin, updatePasswordOnDuplicate, setPasswordMode, defaultPassword }) {
if (!leanUser.objectId) throw new Error('Missing objectId');

const payload = {
EnvId: envId,
Uid: leanUser.objectId,
};

if (leanUser.name) payload.Name = leanUser.name;
if (leanUser.nickname) payload.NickName = leanUser.nickname;
if (leanUser.avatarUrl) payload.AvatarUrl = leanUser.avatarUrl;
if (leanUser.description) payload.Description = leanUser.description;

const trimmedName = (leanUser.name || '').trim();

if (leanUser.phone && isLikelyCnPhone(leanUser.phone)) {
payload.Phone = leanUser.phone.trim();
} else if (mirrorNameToLogin && isLikelyCnPhone(trimmedName)) {
payload.Phone = trimmedName;
}

if (leanUser.email && isLikelyEmail(leanUser.email)) {
payload.Email = leanUser.email.trim();
} else if (mirrorNameToLogin && isLikelyEmail(trimmedName)) {
payload.Email = trimmedName;
}

if (updatePasswordOnDuplicate) {
if (setPasswordMode === 'default') {
if (!defaultPassword) throw new Error('SET_PASSWORD_MODE=default requires DEFAULT_PASSWORD');
payload.Password = defaultPassword;
} else if (setPasswordMode === 'random') {
payload.Password = generateStrongPassword(16);
}
}

return payload;
}

function sleep(ms) {
return new Promise((r) => setTimeout(r, ms));
}

function isRetryableError(err) {
const code = err && (err.code || '');
if (!code) return false;
return [
'RequestLimitExceeded',
'InternalError',
'FailedOperation',
'ResourceUnavailable',
].includes(code);
}

async function withRetry(fn, { maxAttempts = 5, baseDelayMs = 300 } = {}) {
let attempt = 0;
while (true) {
attempt += 1;
try {
return await fn();
} catch (err) {
if (attempt >= maxAttempts || !isRetryableError(err)) throw err;
const delay = baseDelayMs * Math.pow(2, attempt - 1) + crypto.randomInt(0, 200);
await sleep(delay);
}
}
}

async function main() {
const envId = mustGetEnv('TENCENT_ENV_ID');
const inputFile = getEnv('INPUT_FILE', './leancloud_users.json');

const mirrorNameToLogin = String(getEnv('MIRROR_NAME_TO_LOGIN', 'false')).toLowerCase() === 'true';
const userType = normalizeUserType(getEnv('USER_TYPE', 'internalUser'));

const setPasswordMode = String(getEnv('SET_PASSWORD_MODE', 'none')).toLowerCase();
const defaultPassword = getEnv('DEFAULT_PASSWORD', '');

console.log(
`[config] envId=${envId} inputFile=${inputFile} userType=${userType} mirrorNameToLogin=${mirrorNameToLogin} passwordMode=${setPasswordMode}`
);
if (!['none', 'default', 'random'].includes(setPasswordMode)) {
throw new Error('SET_PASSWORD_MODE must be one of: none | default | random');
}

const updatePasswordOnDuplicate =
String(getEnv('UPDATE_PASSWORD_ON_DUPLICATE', 'false')).toLowerCase() === 'true';

const concurrency = Math.max(1, parseInt(getEnv('CONCURRENCY', '3'), 10));

const raw = fs.readFileSync(inputFile, 'utf8');
const users = JSON.parse(raw);
if (!Array.isArray(users)) throw new Error('Input JSON must be an array');

const normalized = users.map(normalizeLeanCloudUser);

let created = 0;
let updated = 0;
let failed = 0;
const failures = [];

let idx = 0;
async function worker() {
while (true) {
const cur = idx;
idx += 1;
if (cur >= normalized.length) return;

const leanUser = normalized[cur];
try {
const payload = mapToCloudBaseCreateUser({
envId,
leanUser,
userType,
mirrorNameToLogin,
setPasswordMode,
defaultPassword,
});

await withRetry(() => callTencentCloud({ action: 'CreateUser', payload }), {
maxAttempts: 5,
baseDelayMs: 300,
});

created += 1;
const done = created + updated + failed;
if (done % 50 === 0) {
console.log(
`[progress] created=${created}, updated=${updated}, failed=${failed}, total=${normalized.length}`
);
}
} catch (err) {
if (err && err.code === 'FailedOperation.DuplicatedData') {
try {
const modifyPayload = mapToCloudBaseModifyUser({
envId,
leanUser,
mirrorNameToLogin,
updatePasswordOnDuplicate,
setPasswordMode,
defaultPassword,
});

await withRetry(() => callTencentCloud({ action: 'ModifyUser', payload: modifyPayload }), {
maxAttempts: 5,
baseDelayMs: 300,
});

updated += 1;
continue;
} catch (modifyErr) {
err = modifyErr;
}
}

failed += 1;
failures.push({
objectId: leanUser.objectId,
name: leanUser.name,
message: err.message,
code: err.code,
requestId: err.requestId,
});
console.error(`[fail] objectId=${leanUser.objectId} code=${err.code || 'unknown'} msg=${err.message}`);
}
}
}

const workers = Array.from({ length: concurrency }, () => worker());
await Promise.all(workers);

console.log(
`[done] created=${created}, updated=${updated}, failed=${failed}, total=${normalized.length}`
);
if (failures.length) {
fs.writeFileSync('migrate-failures.json', JSON.stringify(failures, null, 2), 'utf8');
console.log('Wrote failures to migrate-failures.json');
}
}

main().catch((e) => {
console.error(e);
process.exit(1);
});

3. 设置环境变量

环境变量说明是否必填
TENCENT_SECRET_ID腾讯云 SecretId,前往获取✅ 必填
TENCENT_SECRET_KEY腾讯云 SecretKey,前往获取✅ 必填
TENCENT_ENV_IDCloudBase 环境 ID✅ 必填
INPUT_FILE导入文件路径(默认 ./leancloud_users.json可选
USER_TYPE用户类型:internalUser(内部)/ externalUser(外部)可选
TENCENT_REGION地域(如 ap-shanghai可选
TENCENT_ENDPOINT接口地址(默认 tcb.tencentcloudapi.com可选
CONCURRENCY并发数(默认 3)可选
用户类型选择

对于"业务 App 的真实终端用户"(LeanCloud 原用户),通常应选择 externalUser,避免占用环境的"组织成员数"配额。

密码相关环境变量(任选其一):

模式环境变量说明
不写入密码(默认)SET_PASSWORD_MODE=none用户需要通过验证码登录后重设密码
统一默认密码SET_PASSWORD_MODE=default + DEFAULT_PASSWORD=xxx不推荐,仅用于测试环境
随机临时密码SET_PASSWORD_MODE=random推荐,为每个用户生成随机强密码

重复导入行为

  • 默认:若 CreateUser 返回重复数据,脚本会改用 ModifyUser 更新字段,不会重置密码
  • UPDATE_PASSWORD_ON_DUPLICATE=true:允许在重复导入时也按 SET_PASSWORD_MODE 更新密码(一般不建议)

登录名镜像

  • MIRROR_NAME_TO_LOGIN=true:当 name 看起来像手机号/邮箱时,自动同步到 Phone/Email

4. 运行脚本

# 设置环境变量
export TENCENT_SECRET_ID="your-secret-id"
export TENCENT_SECRET_KEY="your-secret-key"
export TENCENT_ENV_ID="your-env-id"
export USER_TYPE="externalUser"
export INPUT_FILE="./leancloud_users.json"

# 运行迁移脚本
node migrate-leancloud-users-to-cloudbase.js

输出示例

[config] envId=your-env-id inputFile=./leancloud_users.json userType=externalUser mirrorNameToLogin=false passwordMode=none
[progress] created=50, updated=0, failed=0, total=1162
[progress] created=100, updated=0, failed=0, total=1162
...
[done] created=1162, updated=0, failed=0, total=1162

脚本会输出成功/失败统计,并将失败条目记录到本地 migrate-failures.json

校验和回滚

校验

  • 抽样在 CloudBase 侧调用 DescribeUserList 查询是否存在对应 Uid/Name/Phone/Email
  • 业务侧发起登录验证(验证码登录/密码登录),需要先在控制台 / 身份认证 / 登录方式开启相关登录能力

回滚

  • 如果需要删除本次导入的用户,可调用 DeleteUsers,一次最多 100 个 Uid

权限迁移

迁移完用户后,你需要把原本 LeanCloud 的 ACL/角色权限,落到 CloudBase 的安全规则与业务鉴权上。

推荐做法

  • auth.uid(登录态里的用户 ID)作为权限判断的根
  • 对每个集合设置"仅本人可读写""仅管理员可写"等规则
  • 复杂权限(组织架构、角色、资源授权)建议在数据库中维护 roles/permissions,安全规则做基本兜底,核心鉴权放在云函数/服务端

参考 安全规则文档

用户迁移 FAQ

Q:我能把 LeanCloud 的密码直接迁过去吗?

A:通常不行。建议使用验证码登录过渡,或设置临时密码并引导用户首次登录修改。

Q:name 里有 @ 会不会创建失败?

A:使用管理端 API 创建用户时,Name 支持 @(与用户自主注册校验不同)。你仍应确保 Email/Phone 字段满足格式要求。

客户端适配

LeanCloud 登录:

// LeanCloud
const user = await AV.User.logIn(username, password);

CloudBase 登录:

// CloudBase
import cloudbase from '@cloudbase/js-sdk';

const app = cloudbase.init({ env: 'your-env-id' });

// 用户名密码登录
const auth = app.auth();
await auth.signInWithUsernameAndPassword(username, password);

SDK 对照表

以下是 LeanCloud SDK 与 CloudBase SDK 的 API 对照表,帮助您快速完成代码迁移。

初始化

操作LeanCloudCloudBase
初始化 SDKAV.init({ appId, appKey })cloudbase.init({ env })

LeanCloud:

const AV = require('leancloud-storage');
AV.init({
appId: 'your-app-id',
appKey: 'your-app-key',
});

CloudBase:

import cloudbase from '@cloudbase/js-sdk';
const app = cloudbase.init({ env: 'your-env-id' });

📖 CloudBase SDK 初始化文档


数据库操作

操作LeanCloudCloudBase
获取数据库引用new AV.Query('ClassName')app.database().collection('collectionName')
查询全部query.find()collection.get()
查询单条query.get(objectId)collection.doc(id).get()
条件查询query.equalTo('field', value)collection.where({ field: value })
大于query.greaterThan('field', value)collection.where({ field: _.gt(value) })
小于query.lessThan('field', value)collection.where({ field: _.lt(value) })
包含query.containedIn('field', [values])collection.where({ field: _.in([values]) })
排序query.ascending('field')collection.orderBy('field', 'asc')
限制数量query.limit(10)collection.limit(10)
跳过记录query.skip(10)collection.skip(10)
新增数据object.save()collection.add(data)
更新数据object.set('field', value).save()collection.doc(id).update({ field: value })
删除数据object.destroy()collection.doc(id).remove()
批量删除循环 destroy()collection.where(condition).remove()
计数query.count()collection.count()

查询示例对比:

// LeanCloud
const query = new AV.Query('Todo');
query.equalTo('status', 'pending');
query.greaterThan('priority', 5);
query.ascending('createdAt');
query.limit(20);
const results = await query.find();

// CloudBase
const db = app.database();
const _ = db.command;
const results = await db.collection('Todo')
.where({
status: 'pending',
priority: _.gt(5)
})
.orderBy('createdAt', 'asc')
.limit(20)
.get();

📖 CloudBase 数据库 API 文档


云存储操作

操作LeanCloudCloudBase
上传文件new AV.File(name, data).save()app.uploadFile({ cloudPath, filePath })
获取文件 URLfile.get('url')app.getTempFileURL({ fileList })
删除文件file.destroy()app.deleteFile({ fileList })
下载文件通过 URL 下载app.downloadFile({ fileID })

上传文件示例对比:

// LeanCloud
const file = new AV.File('avatar.png', fileInput.files[0]);
const savedFile = await file.save();
const url = savedFile.get('url');

// CloudBase
const result = await app.uploadFile({
cloudPath: 'avatars/avatar.png',
filePath: fileInput.files[0],
onUploadProgress: (progressEvent) => {
const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total);
console.log(`上传进度: ${percent}%`);
}
});
const fileID = result.fileID;

获取文件 URL 示例对比:

// LeanCloud
const url = file.get('url');

// CloudBase
const result = await app.getTempFileURL({
fileList: ['cloud://env-id.xxx/avatars/avatar.png']
});
const url = result.fileList[0].tempFileURL;

📖 CloudBase 云存储 API 文档


云函数操作

操作LeanCloudCloudBase
调用云函数AV.Cloud.run('functionName', params)app.callFunction({ name, data })
定义云函数AV.Cloud.define('name', handler)exports.main = async (event, context) => {}
获取调用者信息request.currentUsercontext.auth / event.userInfo

调用云函数示例对比:

// LeanCloud 客户端
const result = await AV.Cloud.run('hello', { name: 'World' });

// CloudBase 客户端
const result = await app.callFunction({
name: 'hello',
data: { name: 'World' }
});
console.log(result.result);

定义云函数示例对比:

// LeanCloud 云引擎
AV.Cloud.define('hello', async (request) => {
const { name } = request.params;
const user = request.currentUser;
return { message: `Hello, ${name}!` };
});

// CloudBase 云函数
exports.main = async (event, context) => {
const { name } = event;
const { openId, appId } = context.auth || {};
return { message: `Hello, ${name}!` };
};

📖 CloudBase 云函数 API 文档


身份认证操作

操作LeanCloudCloudBase
获取 Auth 对象-app.auth()
用户名密码注册AV.User.signUp(username, password)auth.signUp({ username, password })
用户名密码登录AV.User.logIn(username, password)auth.signInWithUsernameAndPassword(username, password)
手机验证码登录AV.User.signUpOrlogInWithMobilePhone(phone, code)auth.signInWithPhoneCode(phone, code)
邮箱密码登录AV.User.logIn(email, password)auth.signInWithEmailAndPassword(email, password)
匿名登录AV.User.loginAnonymously()auth.signInAnonymously()
获取当前用户AV.User.current()auth.currentUser
获取登录状态AV.User.current() !== nullauth.hasLoginState()
退出登录AV.User.logOut()auth.signOut()
发送验证码AV.Cloud.requestSmsCode(phone)auth.getVerification({ phone_number })
重置密码AV.User.requestPasswordReset(email)auth.resetPassword({ email })
更新用户信息user.set('field', value).save()auth.currentUser.update({ field: value })

登录示例对比:

// LeanCloud - 用户名密码登录
const user = await AV.User.logIn('username', 'password');
console.log('登录成功:', user.get('username'));

// CloudBase - 用户名密码登录
const auth = app.auth();
const loginState = await auth.signInWithUsernameAndPassword('username', 'password');
console.log('登录成功:', loginState.user);

注册示例对比:

// LeanCloud - 用户名密码注册
const user = await AV.User.signUp('username', 'password');

// CloudBase - 手机号注册
const auth = app.auth();
// 1. 发送验证码
const verification = await auth.getVerification({ phone_number: '+86 13800000000' });
// 2. 验证并注册
const verifyResult = await auth.verify({
phone_number: '+86 13800000000',
verification_code: '123456',
verification_id: verification.verification_id
});
// 3. 完成注册
const loginState = await auth.signUp({
phone_number: '+86 13800000000',
verification_token: verifyResult.verification_token,
password: 'yourpassword'
});

📖 CloudBase 身份认证 API 文档


实时数据库

操作LeanCloudCloudBase
实时监听liveQuery.subscribe()collection.watch()
取消监听liveQuery.unsubscribe()watcher.close()

实时监听示例对比:

// LeanCloud LiveQuery
const query = new AV.Query('Message');
query.equalTo('roomId', 'room1');
const liveQuery = await query.subscribe();
liveQuery.on('create', (message) => {
console.log('新消息:', message);
});

// CloudBase 实时数据推送
const db = app.database();
const watcher = db.collection('Message')
.where({ roomId: 'room1' })
.watch({
onChange: (snapshot) => {
console.log('数据变化:', snapshot.docChanges);
},
onError: (error) => {
console.error('监听错误:', error);
}
});

// 取消监听
watcher.close();

📖 CloudBase 实时数据推送文档


更多 API 参考

类别文档链接
Web SDK 完整文档https://docs.cloudbase.net/api-reference/webv2/initialization
Node.js SDK 文档https://docs.cloudbase.net/api-reference/server/initialization
数据库安全规则https://docs.cloudbase.net/database/security-rules
云函数开发指南https://docs.cloudbase.net/cloud-function/introduce
身份认证指南https://docs.cloudbase.net/authentication/auth/introduce
云存储使用指南https://docs.cloudbase.net/storage/introduce

常见问题

Q:LeanCloud 的 ACL 如何迁移?

CloudBase 使用安全规则替代 ACL,需要在控制台配置数据库安全规则。参考 安全规则文档

Q:实时通信功能如何替代?

CloudBase 支持实时数据推送,可以使用数据库实时监听功能:

const db = app.database();
db.collection('messages')
.where({ roomId: 'xxx' })
.watch({
onChange: (snapshot) => {
console.log('数据变化:', snapshot.docs);
},
});

Q:迁移过程中如何保证业务连续性?

建议采用渐进式迁移:

  1. 先部署 CloudBase 环境,进行功能测试
  2. 使用双写策略,新数据同时写入两个平台
  3. 完成数据迁移后,逐步切换流量
  4. 确认稳定后,下线 LeanCloud 服务

迁移检查清单

在完成迁移前,请确认以下事项:

  • 选择迁移方案(云托管/云函数)
  • 导出并转换 LeanCloud 数据
  • 导入数据到 CloudBase 数据库
  • 移除 LeanCloud SDK 依赖
  • 添加 CloudBase SDK 依赖
  • 改造数据库操作代码
  • 配置启动命令和端口(云托管)
  • 配置环境变量
  • 部署到云托管/云函数
  • 迁移定时任务(如有)
  • 通过云函数封装 Hook 逻辑(如有 beforeSave/afterSave)
  • 迁移文件存储
  • 迁移用户系统
  • 配置自定义域名
  • 配置安全规则(替代 ACL)
  • 测试所有 API 接口
  • 测试定时任务
  • 测试 Hook 逻辑(云函数)
  • 验证用户登录流程