已完成从 vB3 到 Discourse 的迁移,包括旧版 vB3 的“感谢”功能

刚刚基本完成了迁移工作(仍在测试阶段,但“最难的部分”已基本搞定,迁移运行良好),将一款拥有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 团队!!!

做得好,感谢分享!

感谢 @codinghorror

刚发现一个小问题,我会着手解决。import_likes 例程没有转移“话题发起者”首帖的点赞(只转移了回复的点赞)。

我得卷起袖子好好研究一下,然后发布更新后的 import_likes 例程。

看来我发帖有点 太早了,在宣布胜利之前,还有一个小妖精需要消灭。

更新:

我在 vB3 的 PHP 脚本中添加了以下代码,以补偿 vB3 和 Discourse 在处理话题/线程首帖时的差异:

    <?php
    #vB3 代码,用于处理 Discourse 如何在 post_custom_fields 表中识别话题中的首帖
    $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'];
    }

现在,它按预期运行,vB3 中的“感谢”已成功迁移到 Discourse,包括话题中的首帖;这是通过调整从 vB3 到 Discourse 表 post_custom_fields 的迁移表来实现的。

目前正在进行最终测试,但根据初步测试,看起来应该可以正常工作。

此外,我还在原帖中更新了代码,添加了上述 PHP 代码片段。

我了解到你们没有迁移密码,因为哈希值不同。你可以看看我的密码迁移插件(这里)。它很可能已经支持 VB3 哈希值,如果不支持,我也很乐意添加该功能。

谢谢 Michael,

我们不会迁移密码,而是希望用户自行更改密码(并改为更强、更长的密码)。

无论如何,非常感谢你的帮助提议!我们非常感激。

我们还有一个小问题,也许你能帮我们解决。

不知何故,用户个人资料中的“点赞”总数未能正确更新,尽管其他一切看起来都正常。我尝试过用 rake 任务来解决这个问题,但我的操作并未得到我需要的结果。

你知道有什么好方法可以手动更新这些个人资料的“点赞”计数器吗?

你用了哪个 rake 任务?出了什么问题?是所有数据都偏差很大,还是仅部分用户的数据有问题,还是……?

你好。

首先,我尝试了当时看来唯一合理的 rake 任务(因为问题与 user_actionspost_actions 相关);但并没有取得任何进展(实际上,现在回想起来,我应该先阅读那个 rake 任务,看看它具体做了什么,哈哈):

rake user_actions:rebuild

坦白说,到目前为止,我还没有确定 import_likes 迁移脚本中究竟哪里出了问题。如果我能确切知道问题出在哪里,我本可以修复它(我常花几个小时甚至几天去找出问题,而一旦找到,修复只需一分钟……这就是 编程的精髓,哈哈)。我已经从快照中恢复了好几次,并用几种不同的方式重新运行了 import_likes 流程(添加了额外的 puts 语句等),同时检查了相关表。结果仍不明确。再加上我对 rubydiscourse 整体还不够熟练;我尚未精通后端开发。(不过,我正在逐渐掌握 begin... rescue ... end 结构……哈哈,这已成为我作为 ruby 新手 的新调试工具之一)。

我所观察到的是,临时的 postgres like_data 表(在 discourse 端)没有问题,包含所有数据且格式正确,其条目与 mysql 迁移表以及 vB 站点的数据一致。

问题似乎出现在创建 user_actionspost_actions 的过程中,因为我的测试显示数据未能正确传输到这些关键表中。

请注意,核心的 vB3 迁移没有问题。此问题与一个旧的 vB3“感谢”插件有关,我正在将其转换为 discourse 的点赞功能。我接下来的计划是,如果今天晚些时候有时间,坐下来仔细查看数据从正常的 like_data 表传输到 user_actionspost_actions 时,具体在哪个环节出错了。

基于昨天的测试,我的初步结论是:ruby rake 任务可能无济于事,因为问题出在更新 user_actionspost_actions 表的过程中;因此,我需要确切找出问题出在哪里以及原因。问题可能很简单,比如数据库类型不匹配,或是代码中的其他小故障。

我们的 discourse 站点尚未上线,因此这并非 紧急情况。和许多其他地方的人一样,处理移民或银行等事务已变得 相当麻烦,今天我还得进城处理一些与 discourse 无关的任务。我其实更希望能专注于探索 discourse 并寻找 代码小故障

再次运行了迁移脚本(未对脚本进行任何修改),其中包括将自定义的 vb3 thanks 转换为 Discourse likesimport_likes)的脚本,现在看起来没问题了。

接下来将尝试同步最新的 vB 数据库转储(大 “D”),看看会发生什么!

对 Discourse 非常满意……再次感谢这个堪称“艺术品”的现代论坛。仍在测试中,但很快就会正式上线。

5.6k 个赞……这真是满满的爱 :heart:

在将所有 vb 感谢 迁移到 discourse 点赞 之后,以下是来自 2000 年(即二十年前)的一个迁移后主题帖示例,展示了主题中迁移后的点赞:

……而且我开始期待学习如何编写有趣的插件了…… :slight_smile:

我对 Discourse 及其团队非常满意……尤其是 Ruby 中的 begin .... rescue ...puts "look at me".... end……简直是救命稻草!

事实确实如此……只要给 老狗 扔点骨头,它们也能学会新把戏 :slight_smile:

感谢 Discourse 团队!

20 年前,我们在 vB2 上为 Unix 和 Linux 用户创建了一个论坛;大约 15 年前,我们“升级”到了 vB3。今天对全球 Unix 和 Linux 用户以及我们自身而言都是一个重要的转折点,因为我们迁移到 Discourse 的工作现已正式上线。

非常感谢你们开发了这款软件,并将其作为开源项目向所有人开放。你们的慷慨令人感激不尽。在我看来(我相信许多人也持相同观点),Discourse 无疑是 2020 年全球最好的论坛软件。

对于那些仍在使用旧版 vB3 并希望迁移到 Discourse 的用户,我要说明的是:这次迁移并没有我展示得那么“轻松”。除非你精通编程,并且能够熟练地在命令行中直接与数据库进行集成,否则我建议你考虑聘请 meta.discourse.org 上一些才华横溢且活跃的专业人士提供服务。

再次感谢 Discourse 团队!

附注:我将在新网站的这篇帖子中补充一些从实践中总结的经验教训: