从 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 | 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;
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报告文件 - 清理临时文件:删除本地临时文件