从 LeanCloud 迁移
本文档帮助您将项目从 LeanCloud 迁移到云开发 CloudBase。
迁移概览
功能对照
| LeanCloud | CloudBase | 说明 |
|---|---|---|
| 数据存储 | 文档型数据库 | 都是 JSON 文档存储 |
| 云引擎 | 云函数/云托管 | 后端代码托管 |
| 文件服务 | 云存储 | 文件存储服务 |
| 用户系统 | 身份认证 | 用户认证服务 |
| 即时通讯 | - | 需要自行实现或使用第三方 |
| 推送服务 | - | 需要使用腾讯云推送服务 |
数据存储迁移
1. 导出 LeanCloud 数据
在 LeanCloud 控制台导出数据:
- 进入「数据存储」→「数据管理」
- 选择需要导出的 Class
- 点击「导出」,选择 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 |
objectId | leancloud_objectId | 保留原始 ID,便于数据追溯 |
createdAt | _createTime | ISO 8601 → 毫秒时间戳 |
updatedAt | _updateTime | ISO 8601 → 毫秒时间戳 |
Pointer | _ref_* | 关联引用标记(待手动处理) |
GeoPoint | {type, coordinates} | GeoJSON 格式 [经度, 纬度] |
ACL | (删除) | CloudBase 使用安全规则替代 |
authData | uid | 提取 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 引用会被标记为 _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
在控制台操作:
- 登录 云开发控制台
- 进入「文档型数据库」→「数据管理」
- 创建对应的集合
- 点击「导入」上传转换后的 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
- 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_ID | ENV_ID | CloudBase 环境 ID |
LEANCLOUD_APP_KEY | 不需要 | 云托管内部自动鉴权 |
LEANCLOUD_APP_MASTER_KEY | 不需要 | 云托管内部自动鉴权 |
LEANCLOUD_APP_PORT | PORT | 服务监听端口 |
LEANCLOUD_APP_ENV | TCB_ENV | 环境标识(可自定义) |
原有在 LeanCloud 控制台配置的自定义环境变量,需要在云托管或云函数控制台重新配置。
Hook 迁移方案
CloudBase 不支持数据库触发器,LeanCloud 的 beforeSave、afterSave 等 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
_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 配置:
- 登录 LeanCloud 控制台
- 选择你的应用
- 进入「设置」→「应用凭证」,获取
App ID和App Key - 进入「设置」→「域名绑定」,获取 API 域名(
serverURL)和文件域名(fileDomain)
腾讯云开发配置:
- 登录 腾讯云开发平台
- 选择你的环境,获取环境 ID(
env) - 登录 腾讯云 API 密钥管理
- 创建或查看密钥,获取
SecretId和SecretKey
⚠️ 重要:配置安全域名
- 登录 腾讯云开发平台
- 进入「环境管理」→「安全来源」
- 添加你的域名到安全域名列表(避免 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 // 请求超时时间(毫秒)
};
迁移流程
- 查询文件列表:从 LeanCloud
_File表查询所有文件 - 下载文件:根据
key拼接 URL 并下载到本地临时目录 - 上传文件:将文件上传到腾讯云开发云存储
- 生成报告:生成
migration_report.json报告文件 - 清理临时文件:删除本地临时文件
输出结果
迁移完成后,会生成 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": "下载超时"
}
]
}
注意事项
- 网络稳定性:确保网络连接稳定,大文件迁移可能需要较长时间
- 磁盘空间:确保有足够的磁盘空间存储临时文件
- 并发数量:根据网络带宽调整并发数,避免过高导致请求失败
- 重试机制:失败的文件会自动重试,最终失败的会记录在报告中
- 云端路径:所有文件会上传到
migrated/目录下,可根据需要修改 - fileID 保存:上传后的
fileID会记录在报告中,建议保存以便后续使用
故障排查
下载失败
- 检查
fileDomain配置是否正确 - 检查网络连接是否正常
- 尝试增加
timeout超时时间
上传失败
- 确认腾讯云开发环境 ID 正确
- 检查是否配置了安全域名
- 确认云存储配额是否充足
CORS 错误
- 登录腾讯云开发控制台
- 在「安全配置」→「安全来源」中添加你的域名
用户系统迁移
本节帮助你将 LeanCloud 用户体系(_User 表)迁移到 CloudBase 用户体系。
迁移目标
- 将 LeanCloud
objectId写入 CloudBase 用户表(使用管理端 API 的Uid字段),实现原 ID 保持不变 - 同步用户关联字段(如
name、昵称、手机号、邮箱、头像等) - 迁移完成后,依据安全规则重新配置你的业务权限逻辑(替代 LeanCloud ACL)
登录方式对比
迁移方案会强依赖你当前使用的登录方式:
- 如果你使用 用户名/邮箱/手机号 + 密码:需要提前规划"密码无法直接迁移"的过渡方案(验证码登录优先 / 临时密码 / 双栈)。
- 如果你使用 邮箱/手机号 + 验证码:迁移相对简单,重点是把邮箱/手机号字段写对,并保证安全规则与业务权限正确。
| 登录方式 | LeanCloud | CloudBase |
|---|---|---|
| 用户名 + 密码 | ✅ 内置支持 | ✅ 内置支持 |
| 邮箱 + 验证码 | ✅ 内置支持 | ✅ 内置支持 |
| 手机号 + 验证码 | ✅ 内置支持 | ✅ 内置支持 |
| 微信登录 | ✅ 第三方登录 | ✅ 深度支持 |
| UnionID 打通 | ✅ 支持 | ✅ 支持 |
| QQ 登录 | ✅ 第三方登录 | ✅ OAuth 支持 |
| 微博登录 | ✅ 第三方登录 | ✅ OAuth 支持 |
| Apple 登录 | ✅ 内置支持 | 🕐 即将支持 |
| 匿名登录 | ✅ 内置支持 | ✅ 内置支持 |
| 自定义登录 | ❌ 不支持 | ✅ 支持 |
CloudBase 与 LeanCloud 用户体系的差异
- 用户表结构:CloudBase 用户体系的内置字段是固定集合(如
Uid、Name、Phone、Email等),你的业务自定义字段建议放入自建集合(如user_profile),用Uid关联。 - 密码迁移:LeanCloud 通常不会允许导出明文密码;即使你能拿到 hash,也无法直接写入 CloudBase 用户体系。你需要选择一种"迁移后重置/重设"的策略(下文提供方案)。
- 用户名校验规则差异:
- CloudBase 管理端 API 创建用户时,
Name支持@等特殊字符(更宽松)。 - 但"用户自主注册"的前端/客户端校验规则可能更严格(例如不允许
@),两者不完全一致。
- CloudBase 管理端 API 创建用户时,
迁移流程
- 导出 LeanCloud 用户数据(从
_User导出为 JSON) - 字段映射与清洗(
objectId→Uid,处理name/phone/email的对应关系) - 调用 CloudBase 管理端 API 批量创建用户(
CreateUser) - 同步自定义业务字段(写入自建集合,按
Uid关联) - 配置安全规则与权限逻辑(替代 LeanCloud ACL)
- 抽样校验与回滚预案(
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_ID | CloudBase 环境 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 对照表,帮助您快速完成代码迁移。
初始化
| 操作 | LeanCloud | CloudBase |
|---|---|---|
| 初始化 SDK | AV.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' });
数据库操作
| 操作 | LeanCloud | CloudBase |
|---|---|---|
| 获取数据库引用 | 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();
云存储操作
| 操作 | LeanCloud | CloudBase |
|---|---|---|
| 上传文件 | new AV.File(name, data).save() | app.uploadFile({ cloudPath, filePath }) |
| 获取文件 URL | file.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;
云函数操作
| 操作 | LeanCloud | CloudBase |
|---|---|---|
| 调用云函数 | AV.Cloud.run('functionName', params) | app.callFunction({ name, data }) |
| 定义云函数 | AV.Cloud.define('name', handler) | exports.main = async (event, context) => {} |
| 获取调用者信息 | request.currentUser | context.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}!` };
};
身份认证操作
| 操作 | LeanCloud | CloudBase |
|---|---|---|
| 获取 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() !== null | auth.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'
});
实时数据库
| 操作 | LeanCloud | CloudBase |
|---|---|---|
| 实时监听 | 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();
更多 API 参考
常见问题
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:迁移过程中如何保证业务连续性?
建议采用渐进式迁移:
- 先部署 CloudBase 环境,进行功能测试
- 使用双写策略,新数据同时写入两个平台
- 完成数据迁移后,逐步切换流量
- 确认稳定后,下线 LeanCloud 服务
迁移检查清单
在完成迁移前,请确认以下事项:
- 选择迁移方案(云托管/云函数)
- 导出并转换 LeanCloud 数据
- 导入数据到 CloudBase 数据库
- 移除 LeanCloud SDK 依赖
- 添加 CloudBase SDK 依赖
- 改造数据库操作代码
- 配置启动命令和端口(云托管)
- 配置环境变量
- 部署到云托管/云函数
- 迁移定时任务(如有)
- 通过云函数封装 Hook 逻辑(如有 beforeSave/afterSave)
- 迁移文件存储
- 迁移用户系统
- 配置自定义域名
- 配置安全规则(替代 ACL)
- 测试所有 API 接口
- 测试定时任务
- 测试 Hook 逻辑(云函数)
- 验证用户登录流程