从 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 处理
Pointer 引用会被标记为 _ref_* 字段,导入后需要手动或通过脚本将 _ref_objectId 替换为对应的 CloudBase _id。