刚刚基本完成了迁移工作(仍在测试阶段,但“最难的部分”已基本搞定,迁移运行良好),将一款拥有15年历史的 vb3 论坛迁移到了 Discourse。该论坛最初(信不信由你)是2000年建立的 vb2 论坛。
这个论坛已经运营了大约20年(包含难以想象的自定义 PHP 代码、数据表、插件等),而在我看来,Discourse 是唯一“值得”进行 迁移项目 的论坛软件。我从大约一周前开始着手,从可行性验证到基本完成。
首先,我要向整个 Discourse 团队致以诚挚的感谢,感谢你们打造了如此卓越的“现代论坛艺术品”。在我看来,Discourse 确实令人惊叹(我相信许多人也有同感)。
其次,我要感谢编写原始 vbulletin.rb 迁移脚本的团队。该脚本大约95%可用,我对其进行了修改,使其能够正确支持 vb3。例如,vb3 不像 vb4 那样拥有 filedata 表(与附件相关),因此我修改了 vbulletin.rb 以适配这一差异。此外,在将 vb3 的子论坛作为分类导入时也曾遇到问题,但我修改了代码,将所有论坛作为顶级分类导入,随后编写了一些 postgres psql 脚本来建立新的父子分类关系(或手动配置)。还有一些其他的“陷阱”(留待日后详述),但这些小麻烦让整个过程既有趣又充满挑战,但并非不可克服。
第三,我要感谢 Sam 及其代码 lithium.rb。我将其中的 import_likes 例程作为基础,用于将 vb3 的“感谢”(来自一个遗留插件,并非 vB 原生功能)迁移到 Discourse 的“点赞”功能。我对 import_likes 例程做了少量修改,经过几小时的调试后成功运行。
从下面这篇2018年的帖子(今天已迁移)中你可以看到效果:
我认为将用户的 vb 感谢 迁移到 Discourse 点赞 非常重要,因此以下是我为其他 迷失的 vb3 论坛用户 迁移点赞功能所使用的代码:
首先,我创建了一个名为 vb3 mysql 表 的 user_actions 表,并编写了一个 PHP 脚本,从遗留的 vb post_thanks 表中填充数据,如下所示(虽未完全优化,但已可用):
ubuntu:/var/www/includes/cron# cat thanks_to_discourse.php
<?php
/**************************************************
+-----------+------------+-----------+-----------+
| thanker | unixtime | id | thanked |
+-----------+------------+-----------+-----------+
| 1 | 1584149592 | 303045211 | 302093876 |
| 302153369 | 1584136706 | 303045214 | 302116191 |
| 302108573 | 1584128526 | 303045211 | 302093876 |
| 302153369 | 1584126659 | 303042175 | 302116191 |
| 302153369 | 1584126400 | 303045174 | 302116191 |
| 302153369 | 1584117711 | 303045184 | 1 |
| 37898 | 1584108187 | 303045175 | 1 |
| 302181242 | 1584106664 | 303045201 | 302122047 |
| 302181242 | 1584104642 | 303045074 | 302052697 |
| 302025710 | 1584103722 | 303045184 | 1 |
+-----------+------------+-----------+-----------+
**************************************************/
$query = 'SELECT p.threadid AS threadid,t.userid AS thanker,t.date AS unixtime,t.postid AS postid,p.userid AS thanked from post_thanks as t LEFT JOIN post p ON p.postid = t.postid';
$allthanks = $vbulletin->db->query_read($query);
/****************************************
mysql> describe user_actions;
+-----------------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-----------------+------------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| action_type | int(11) unsigned | NO | | NULL | |
| user_id | int(11) unsigned | NO | | NULL | |
| target_topic_id | int(11) unsigned | YES | | NULL | |
| target_post_id | varchar(16) | YES | | NULL | |
| target_user_id | int(11) unsigned | YES | | NULL | |
| acting_user_id | int(11) unsigned | YES | | NULL | |
| created_at | timestamp | YES | | NULL | |
| updated_at | timestamp | YES | | NULL | |
+-----------------+------------------+------+-----+---------+----------------+
9 rows in set (0.00 sec)
INSERT INTO table_name (column1, column2, column3, ...)
VALUES (value1, value2, value3, ...);
*************************************/
$action_code = 2; // discourse like action == 2
$target_user_id = "";
while ($action = $vbulletin->db->fetch_array($allthanks)) {
#code to deal with how discourse identifies the first post in a topic in the post_custom_fields table
$query = 'SELECT firstpostid FROM thread WHERE threadid =' . $action['threadid'] . ' LIMIT 1';
$threadinfo = $vbulletin->db->query_first($query);
if ($threadinfo['firstpostid'] == $action['postid']) {
$action['postid'] = 'thread-' . $action['threadid'];
}
$update = 'INSERT INTO user_actions (action_type, user_id, target_topic_id, target_post_id, target_user_id,acting_user_id, created_at, updated_at)' .
' VALUES (' . $action_code . ',' .
'"' . $action['thanked'] . '",' .
'"' . $action['threadid'] . '",' .
'"' . $action['postid'] . '",' .
'"' . $target_user_id . '",' .
'"' . $action['thanker'] . '",' .
'FROM_UNIXTIME(' . $action['unixtime'] . '),' .
'FROM_UNIXTIME(' . $action['unixtime'] . '))';
$doit = $vbulletin->db->query_write($update);
}
因此,这个 mysql user_actions 表已包含在迁移转储文件中。
以下是我用于在 Discourse 端完成迁移的修改版 import_likes 例程:
def import_likes
puts "\nimporting likes..."
# created mysql user_actions table in vb3 using PHP script and included that table with migration dump
sql = "select acting_user_id as user_id, target_post_id as post_id, created_at from user_actions"
results = mysql_query(sql)
puts "length() method form : #{results.count}\n\n"
puts "skip loading unique id map"
existing_map = {}
PostCustomField.where(name: 'import_id').pluck(:post_id, :value).each do |post_id, import_id|
existing_map[import_id] = post_id
#puts "postcustomfield existing_map post_id: #{post_id} import_id #{import_id}\n"
end
puts "loading data into temp table"
#manually created the temp like_data table so I could check the table after session ends
#DB.exec("create temp table like_data(user_id integer, post_id integer, created_at timestamp without time zone)")
puts "like_data temp table created"
PostAction.transaction do
results.each do |result|
result["user_id"] = user_id_from_imported_user_id(result["user_id"].to_s)
result["post_id"] = existing_map[result["post_id"].to_s]
next unless result["user_id"] && result["post_id"]
puts "insert like table user_id: #{result["user_id"]} post_id #{result["post_id"]}\n"
DB.exec("INSERT INTO like_data VALUES (:user_id,:post_id,:created_at)",
user_id: result["user_id"],
post_id: result["post_id"],
created_at: result["created_at"]
)
end
end
puts "creating missing post actions"
DB.exec <<~SQL
INSERT INTO post_actions (post_id, user_id, post_action_type_id, created_at, updated_at)
SELECT l.post_id, l.user_id, 2, l.created_at, l.created_at FROM like_data l
LEFT JOIN post_actions a ON a.post_id = l.post_id AND l.user_id = a.user_id AND a.post_action_type_id = 2
WHERE a.id IS NULL
SQL
puts "creating missing user actions"
DB.exec <<~SQL
INSERT INTO user_actions (user_id, action_type, target_topic_id, target_post_id, acting_user_id, created_at, updated_at)
SELECT pa.user_id, 1, p.topic_id, p.id, pa.user_id, pa.created_at, pa.created_at
FROM post_actions pa
JOIN posts p ON p.id = pa.post_id
LEFT JOIN user_actions ua ON action_type = 1 AND ua.target_post_id = pa.post_id AND ua.user_id = pa.user_id
WHERE ua.id IS NULL AND pa.post_action_type_id = 2
SQL
# reverse action
DB.exec <<~SQL
INSERT INTO user_actions (user_id, action_type, target_topic_id, target_post_id, acting_user_id, created_at, updated_at)
SELECT p.user_id, 2, p.topic_id, p.id, pa.user_id, pa.created_at, pa.created_at
FROM post_actions pa
JOIN posts p ON p.id = pa.post_id
LEFT JOIN user_actions ua ON action_type = 2 AND ua.target_post_id = pa.post_id AND
ua.acting_user_id = pa.user_id AND ua.user_id = p.user_id
WHERE ua.id IS NULL AND pa.post_action_type_id = 2
SQL
puts "updating like counts on posts"
DB.exec <<~SQL
UPDATE posts SET like_count = coalesce(cnt,0)
FROM (
SELECT post_id, count(*) cnt
FROM post_actions
WHERE post_action_type_id = 2 AND deleted_at IS NULL
GROUP BY post_id
) x
WHERE posts.like_count <> x.cnt AND posts.id = x.post_id
SQL
puts "updating like counts on topics"
DB.exec <<-SQL
UPDATE topics SET like_count = coalesce(cnt,0)
FROM (
SELECT topic_id, sum(like_count) cnt
FROM posts
WHERE deleted_at IS NULL
GROUP BY topic_id
) x
WHERE topics.like_count <> x.cnt AND topics.id = x.topic_id
SQL
end
end
我希望这篇帖子能帮助其他可能从 vB3 迁移到 Discourse 的人,因为许多人都长期使用过遗留的 vb3“感谢”插件。
此外,我在 Discourse 数据库 和大量 Discourse Ruby 脚本 中花费了大量时间,因此这次迁移颇具挑战性(绝非 胆小者 所能胜任),但完全可行。
实际上,我很享受这个过程。这是我第一次接触 Ruby,因此学习 Ruby 非常有趣;而作为一名长期与 MySQL 打交道的人,PostgreSQL 部分对我来说基本是常规操作。
我们即将正式上线……感谢 Discourse 团队!!!



