关于使用AI助手开发的彩票插件中的MessageBus实时更新求助

大家好,

我正在为 Discourse 开发一个彩票插件,用户可以通过回复彩票主题来参与。坦白说:我不是程序员——我完全是在 AI 的帮助下(Claude Code)构建这个插件的,所以我可能缺少一些基本概念。 基本功能已经可以工作了,但在使用 MessageBus 进行实时更新时遇到了一些问题。我希望能得到一些关于正确方法的指导。

我想实现的目标

  • 使用 BBCode [lottery]...[/lottery] 创建一个彩票帖子。
  • 在帖子中显示一个彩票卡片,显示参与者数量。
  • 当用户回复参与时,所有观看者都应该实时看到参与者数量更新,而无需刷新页面。
  • 类似于 discourse-calendar 插件更新事件信息的方式。

当前实现

后端 (Ruby)

Lottery 模型 (app/models/lottery.rb):

def publish_update!
  channel = "/lottery/#{self.post.topic_id}"
  message = { id: self.id }

  MessageBus.publish(channel, message)
end

LotteryParticipant 模型 (app/models/lottery_participant.rb):

after_commit :publish_lottery_update, on: [:create, :destroy]

private

def publish_lottery_update
  self.lottery.publish_update!
end

后端日志确认 MessageBus.publish 已成功调用:

[Lottery] 📡 Publishing MessageBus update:
  Channel: /lottery/27
  Message: {:id=>52}
[Lottery] ✅ MessageBus.publish completed

前端 (JavaScript)

Decorator (discourse-lottery-decorator.gjs):

api.decorateCookedElement((cooked, helper) => {
  const post = helper.getModel();

  if (!post?.lottery_data) return;

  // 检查是否已装饰(AI 建议使用 data 属性)
  if (cooked.dataset.lotteryDecorated === "true") return;

  const lotteryNode = cooked.querySelector(".discourse-lottery");
  if (!lotteryNode) return;

  const wrapper = document.createElement("div");
  lotteryNode.before(wrapper);

  const lottery = Lottery.create(post.lottery_data);

  helper.renderGlimmer(
    wrapper,
    `<template><DiscourseLottery @lotteryData={{lottery}} /></template>`
  );

  lotteryNode.remove();
  cooked.dataset.lotteryDecorated = "true";
}, { id: "discourse-lottery" });

Component (discourse-lottery/index.gjs):

export default class DiscourseLottery extends Component {
  @service messageBus;
  @service lotteryApi;

  constructor() {
    super(...arguments);

    const { lotteryData } = this.args;
    if (lotteryData?.topicId) {
      this.lotteryPath = `/lottery/${lotteryData.topicId}`;
      this.lotteryData = lotteryData;

      // AI 建议手动绑定回调
      this._boundOnLotteryUpdate = this._onLotteryUpdate.bind(this);
      this.messageBus.subscribe(this.lotteryPath, this._boundOnLotteryUpdate);

      registerDestructor(this, () => {
        this.messageBus.unsubscribe(this.lotteryPath, this._boundOnLotteryUpdate);
      });
    }
  }

  async _onLotteryUpdate(msg) {
    console.log('[Lottery Component] 🔔 MessageBus message received!');

    const updatedData = await this.lotteryApi.lottery(this.lotteryData.topicId);
    this.lotteryData.updateFromLottery(updatedData);
  }
}

Model (models/lottery.js):

export default class Lottery {
  @tracked _stats;

  set stats(stats) {
    this._stats = LotteryStats.create(stats || {});
  }

  updateFromLottery(lottery) {
    // 更新所有属性,包括 stats
    this.stats = lottery.stats || {};
    // ... 其他属性
  }
}

问题

只有参与者的页面会实时更新,而其他观看者的页面在不刷新页面的情况下不会更新。

有效的部分:

  • :white_check_mark: 当用户回复参与时,他们自己的页面会实时显示参与者数量更新。
  • :white_check_mark: 后端成功发布 MessageBus 消息。
  • :white_check_mark: 参与者的页面收到了 MessageBus 消息(日志显示“:bell: MessageBus message received!”)。
  • :white_check_mark: 模型已更新为新的参与者数量(日志显示数量从 0 增加到 1)。

无效的部分:

  • :cross_mark: 其他观看彩票主题的用户看不到数量实时更新。
  • :cross_mark: 彩票创建者的页面不会更新。
  • :cross_mark: 打开了彩票主题的任何其他用户的页面都不会更新。
  • 他们都需要手动刷新页面才能看到更新的参与者数量。

此外,我在控制台中看到组件被多次销毁和重新创建:

[Lottery Component] ✅ MessageBus subscription established
[Lottery Component] 🧹 Cleaning up MessageBus for topic: 27
[Lottery Component] ✅ MessageBus subscription established
[Lottery Component] 🧹 Cleaning up MessageBus for topic: 27

这表明 decorateCookedElement 被触发了多次,导致组件被销毁和重新创建,这可能会破坏其他观看者的 MessageBus 订阅。

问题

  1. 使用已装饰元素上的 data-decorated 属性是防止重复装饰的正确方法吗? AI 建议了这种方法,但我找不到官方文档确认这是推荐的模式。

  2. 为什么 UI 没有更新,即使跟踪的模型属性发生了变化? @tracked _stats 在通过 this.stats = lottery.stats 更新时应该会触发重新渲染,但显示的参与者数量保持不变。我使用 @tracked 的方式有什么问题吗?

  3. 我应该完全采用不同的方法吗? 我让 AI 遵循 discourse-calendar 的模式,但也许我遗漏了 Discourse 处理实时更新的基本知识。

  4. 移除原始的 lotteryNode 是否会导致问题? AI 最初尝试不移除它,但这会导致彩票卡片被 CSS 隐藏(.cooked > .discourse-lottery { display: none; })。有没有更好的模式?

由于我不是程序员,我可能会问一些基本问题——任何指向文档或示例的链接都会非常有帮助!谢谢!

2 个赞