如何实现Contest模型published_at字段设置后30分钟触发邮件任务?
Alright, let's figure out how to fix this problem. The main issue with your current code is that it schedules the email job immediately when a Contest is created—even if published_at is blank. And since published_at might be added later via an update, we need a way to only trigger the job when published_at is actually set, not just on record creation. Here are a few solid solutions:
Solution 1: Use after_commit with conditional checks for published_at
Instead of after_create, switch to an after_commit callback that triggers only when published_at is either set on creation or added later via an update. We'll add a helper method to check if published_at was just set:
class Contest < ApplicationRecord after_commit :schedule_contest_email, on: [:create, :update], if: :published_at_just_set? private def published_at_just_set? # Two valid scenarios: # 1. The contest was just created with a non-blank published_at # 2. The contest was updated, and published_at changed from nil to a value published_at.present? && (new_record? || saved_change_to_published_at?(from: nil)) end def schedule_contest_email SendContestJob.set(wait: 30.minutes).perform_later(self) end end
This way, the job only gets scheduled when published_at is actually present, whether that's on creation or a later update.
Solution 2: Add a flag to prevent duplicate job scheduling
If you're worried about users accidentally updating published_at multiple times (which could trigger multiple job schedules), add a boolean flag to track if the email has already been scheduled:
First, generate a migration to add the flag:
rails generate migration AddEmailScheduledToContests email_scheduled:boolean default:false rails db:migrate
Then update the model:
class Contest < ApplicationRecord after_commit :schedule_contest_email, on: [:create, :update], if: :should_schedule_email? private def should_schedule_email? published_at.present? && !email_scheduled && (new_record? || saved_change_to_published_at?(from: nil)) end def schedule_contest_email SendContestJob.set(wait: 30.minutes).perform_later(self) # Use update_column to skip callbacks and avoid infinite loops update_column(:email_scheduled, true) end end
This ensures the email job is only scheduled once, even if published_at is modified again later.
Solution 3: Add a final check in the Job itself
As an extra layer of protection, add a check directly in your SendContestJob to make sure published_at is still present before sending the email. This guards against edge cases (like published_at being cleared after the job was scheduled):
class SendContestJob < ApplicationJob queue_as :default def perform(contest) # Refresh the contest record from the database to get the latest state contest.reload if contest.published_at.present? # Execute your email sending logic here ContestMailer.notify_contest(contest).deliver_now end end end
Recommended Combination
For the most robust setup, combine Solution 1 (conditional callback scheduling) with Solution 3 (final job check). This ensures the job is only scheduled when needed, and even if something slips through, the job won't send an email if published_at is missing.
内容的提问来源于stack exchange,提问作者Alex Zakruzhetskyi

