前言
相信大家都知道了,LeanCloud 将于 2027 年 1 月 12 日停止对外提供服务,本站有俩个服务依赖着 LeanCloud 服务,今天有空把它们都迁移一下。
一. Do you like me 小组将
1. 部署
单击此处 一键部署 Deploy with Vercel。
项目地址https://github.com/5ime/likeMe
成功后去仪表盘,切换至
Storage创建数据库并连接。


连接成功去检查环境变量,选择项目的
Project Settings找到Environment Variables检查环境变量是否存在POSTGRES_URL。

重新部署。

检验是否数据库连接成功。
1
2
3
4
5
6
7
8最简单的方式:访问一次接口触发建表,然后再看是否能正常返回。
GET /healthz:服务是否存活(返回 ok)
GET /info:会读取数据库并返回当前点赞数
如果数据库连不上,你通常会在 Vercel 的 Logs 里看到与连接/鉴权相关的报错(例如环境变量缺失、连接串无效等)。
本项目会在首次请求时自动建表(like_count / like_user),无需手动建表。至此部署部分就应该结束了,以下是我的示例 Demo 。

用法请参考之前的文章。
题外话,这里为了适配主题的
pjax我对likeme.js进行修改,以便切换页面时失效。1
const likeMe=(()=>{let instance;let initialCallOptions=null;let retryCount=0;const MAX_RETRIES=20;const RETRY_INTERVAL=50;function tryInitializeInstance(options){const targetElement=document.querySelector(options.el);if(targetElement){instance=new LikeMeSingleton(options);instance.reinitialize(options.el);initialCallOptions=null;retryCount=0}else if(retryCount<MAX_RETRIES){console.warn(`LikeMe: Target element ${options.el} not found yet. Retrying in ${RETRY_INTERVAL}ms (attempt ${retryCount+1}/${MAX_RETRIES}).`);retryCount++;setTimeout(()=>tryInitializeInstance(options),RETRY_INTERVAL)}else{console.error(`LikeMe: Target element ${options.el} not found after ${MAX_RETRIES} retries. Initialization failed.`);initialCallOptions=null;retryCount=0}}return options=>{if(!instance){if(!initialCallOptions){initialCallOptions=options};tryInitializeInstance(initialCallOptions)}else{instance.url=options.serverURL||instance.url;instance.color=options.color||instance.color;instance.reinitialize(options.el)}return instance}})();class LikeMeSingleton{constructor({el,serverURL,color='#ff9797'}){Object.assign(this,{el,url:serverURL,color});this.isLiking=false}async reinitialize(newEl=this.el){this.el=newEl;const currentElement=document.querySelector(this.el);if(currentElement){currentElement.innerHTML=''}else{console.warn(`LikeMe target element ${this.el} not found during reinitialization. Skipping render.`);return}try{await this.renderUI();this.attachEvents();this.updateCount()}catch(error){console.error('LikeMe reinitialize error:',error)}}async init(){return this.reinitialize(this.el)}async renderUI(){try{const response=await fetch(this.url);const html=await response.text();const element=document.querySelector(this.el);if(element){const parser=new DOMParser();const doc=parser.parseFromString(html,'text/html');element.innerHTML=doc.body.innerHTML;const card=element.querySelector('.likeCard');if(card){card.style.setProperty('background-color',this.color)}}}catch(error){console.error('Render UI error:',error)}}attachEvents(){const card=document.querySelector(`${this.el} .likeCard`);if(card){card.addEventListener('click',()=>this.handleLike())}}async updateCount(){try{const response=await fetch(`${this.url}/info`);const data=await response.json().catch(()=>null);if(response.ok&&data){const count=data.data?.count||0;const textElement=document.querySelector(`${this.el} .likeCard-text`);if(textElement){textElement.textContent=`❤ ${count}`}}}catch(error){console.error('Update count error:',error)}}setUIState(isLoading,text,isSuccess=false){const card=document.querySelector(`${this.el} .likeCard`);const textElement=document.querySelector(`${this.el} .likeCard-text`);if(card&&textElement){card.classList.remove('loading','success');if(isLoading){card.classList.add('loading')}else if(isSuccess){card.classList.add('success')}textElement.textContent=text}}async handleLike(){if(this.isLiking)return;this.isLiking=true;this.setUIState(true,'❤ 爱意传递中...');try{const response=await fetch(`${this.url}/like`);const payload=await response.json().catch(()=>null);if(!payload){this.setUIState(false,'❤ 网络错误');setTimeout(()=>{this.updateCount()},1500);return}const{code,data={},msg}=payload;const count=data.count||0;if(response.ok&&code==='200'){this.setUIState(false,'❤ 传递成功~',true);setTimeout(()=>{this.setUIState(false,`❤ ${count}`)},500);return}this.setUIState(false,msg||'❤ 传递失败~');setTimeout(()=>{this.updateCount()},1500)}catch(error){console.error('Handle like error:',error);this.setUIState(false,'❤ 网络错误');setTimeout(()=>{this.updateCount()},1500)}finally{this.isLiking=false}}}
2. 数据迁移
导出数据。

连接数据库(我这里以
Navicat为例),你也可以用Neon自带的进行操作。
打开
likeCount这个表来修改数据,这个数据从刚刚导出的文件中likeCount.0.jsonl查看,修改成一样的数值。
进阶,把之前的ip地址和时间也直接导入。
- 首先,先了解一下数据的结构。
LeanCloud 1
{"ACL":{"*":{"read":true}},"ip":"0.0.0.0","createdAt":"2024-11-06T05:24:53.869Z","updatedAt":"2024-11-06T05:24:53.869Z","objectId":"672afda5ff1aa61512697fa9"}
- 这是用ai生产的一个转换成sql语句的脚本。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59const fs = require('fs');
const readline = require('readline');
// 定义输入和输出文件
const inputFilePath = 'likeUser.0.jsonl';
const outputFilePath = 'insert_like_user.sql';
// 创建可读流和可写流
const readStream = fs.createReadStream(inputFilePath);
const writeStream = fs.createWriteStream(outputFilePath);
const rl = readline.createInterface({
input: readStream,
crlfDelay: Infinity // 检测所有CRLF行尾
});
let lineNumber = 0;
rl.on('line', (line) => {
lineNumber++;
// 跳过文件头注释行和空行
if (line.startsWith('#') || line.trim() === '') {
return;
}
try {
const data = JSON.parse(line);
const ip = data.ip;
const createdAt = data.createdAt; // e.g., "2024-11-06T05:24:53.869Z"
// 从 createdAt 中提取日期部分作为 'day'
const dateObj = new Date(createdAt);
const day = dateObj.toISOString().split('T')[0]; // YYYY-MM-DD
// 构建 SQL INSERT 语句,使用 ON CONFLICT DO NOTHING
const sql = `INSERT INTO "public"."like_user" ("ip", "day", "created_at") VALUES ('${ip}', '${day}', '${createdAt}') ON CONFLICT ("ip", "day") DO NOTHING;\n`;
writeStream.write(sql);
} catch (error) {
console.error(`Error parsing JSON on line ${lineNumber}: ${line}`);
console.error(error);
}
});
rl.on('close', () => {
console.log(`Conversion complete. SQL INSERT statements (ignoring duplicates) written to ${outputFilePath}`);
writeStream.end(); // 关闭输出文件流
});
readStream.on('error', (err) => {
console.error(`Error reading input file: ${err.message}`);
});
writeStream.on('error', (err) => {
console.error(`Error writing output file: ${err.message}`);
});将脚本的生产出来的文件打开,复制所有到新建查询里并运行,你也可以用Neon自带的进行操作。


至此,完成所有数据迁移
- 首先,先了解一下数据的结构。
二. Waline 评论
- 去Waline的管理后台将所有数据导出。

- 按照官方文档进行重新部署。
- 导入刚刚导出来的数据。

- 最后,记得将之前的项目中的环境变量全部移至新项目中,可以使用命令更快。
结尾
至此,教程结束。希望对你有所帮助,有任何问题请在下方留言。可以关注我的 公众号以及订阅我的文章 ,感谢你的支持,是对我最大的动力,当然了,更多的是因为热爱。





