新手问题 关于 UserMailer.welcome (@user).deliver_later 使用中遇到的一个问题

jk0 · 2015年06月20日 · 最后由 jk0 回复于 2015年06月20日 · 2866 次阅读

刚刚学 ROR,跟着《Ruby on Rails Tutorial》做的例子,部署的时候使用了 mailer 发送邮件; 无意间翻到文档中说相比使用 deliver_now 即时发送,使用 deliver_later 异步发送比较好。 这个时候报了一个错误给我,说是生成邮件模板中的一个具名路由方法缺少 ID,这个 ID 是我在 User 类中定义的属性 activation_token, 使用 deliver_now 时没有问题,但是在使用 deliver_later 的时候这个值丢失了;在邮件模板中将变量改成一个常量,异步发送邮件成功(邮箱有收到)。 也就是说问题就在参数值丢失这里。ruby 没学好理不知道原理,发上来请大家帮忙看一下。下面贴上关键代码

首先是 controllers/users_controller.rb 通过 create 开始整个流程

class UsersController < ApplicationController

  def create
    @user = User.new(user_params)
    if @user.save
      # 处理注册成功的情况
      @user.send_activation_email # 发送激活邮件
      flash[:info] = "Please check your email to activate your account."
      redirect_to root_url
    else
      render 'new'
    end
  end
end

然后是 models/user.rb,里面有发送激活账号的邮件方法,用 debugger 在这里调试 self.activation_token 还有值

class User < ActiveRecord::Base
  attr_accessor :remember_token, :activation_token, :reset_token
  before_create :create_activation_digest

  #返回指定字符串的哈希摘要
  def User.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : 
                                                  BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
  end

  # 返回一个随机令牌
  def User.new_token
    SecureRandom.urlsafe_base64
  end

  # 发送激活账号的邮件
  def send_activation_email
# debugger
    UserMailer.account_activation(self).deliver_later
  end

  # 设置密码重设相关的属性
  def create_reset_digest
    self.reset_token = User.new_token
    update_columns(reset_digest:  User.digest(reset_token),
                   reset_sent_at: Time.zone.now)
    # update_attribute(:reset_digest,  User.digest(reset_token))
    # update_attribute(:reset_sent_at, Time.zone.now)
  end

  # 发送密码重设的邮件
  def send_password_reset_email
    UserMailer.password_reset(self).deliver_now
  end

  private

    # def update_columns(params={})
    #   params.each do |key, value|
    #     self.update_attribute(key, value)
    #   end
    # end

    def downcate_email
      self.email = email.downcase
    end

    def create_activation_digest
      self.activation_token = User.new_token
      self.activation_digest = User.digest(activation_token)
    end

end

mailers/user_mailer.rb,这里执行 mail 方法绑定邮件模板生成邮件,就是这儿 user.activation_token 值变成了 nil

class UserMailer < ApplicationMailer
  default from: "[email protected]"

  # Subject can be set in your I18n file at config/locales/en.yml
  # with the following lookup:
  #
  #   en.user_mailer.account_activation.subject
  #
  def account_activation(user)
    @user = user
    #@greeting = "Hi"
    mail to: user.email, subject: "account activation"
  end

  # Subject can be set in your I18n file at config/locales/en.yml
  # with the following lookup:
  #
  #   en.user_mailer.password_reset.subject
  #
  def password_reset(user)
    @user = user
    #@greeting = "Hi"

    mail to: user.email, subject: "Password reset"
  end
end

生成邮件模板的 html 文件 /views/user_mailer/account_activation.html.erb,text 版大同小异就不发了

<h1>Sample App</h1>

<p>Hi <%= @user.name %>,</p>

<p>
Welcome to the Sample App! Click on the link below to activate your account:
</p>

<%= link_to "Activate", edit_account_activation_url(@user.activation_token,
                                                    email: @user.email) %>

本身.net 转过来的,我觉着这地方就是学得太浅没理解到位的问题。。请各位朋友指点一下,非常感谢。

补上错误日志,我自己描述可能还是不太准确。。出错的地方就是在 /views/user_mailer/account_activation.html.erb 里面。

调试的图片,使用 deliver_later 发送邮件时的调试

调试的图片,使用 deliver_now 发送邮件时的调试

最后问一个无关的,为什么我在使用 mailer 的时候,user_mailer.rb 中 default from: "[email protected]" 这一行注释掉,然后在 config/environments/development.rb(production.rb) 中使用 config.action_mailer.default_options = {from: '[email protected]'} 不起作用,仍然会因为发送邮箱和发送人不一致的问题导致 smtp 登录失败,现在我都是这样写 default from: "[email protected]" 才能登陆成功。

看到底没看到错误日志在哪里,楼主不要自己解释错误,把原本的日志贴出来。

#1 楼 @rei 补了个图,你看这个行不行 😄

activation_token 没看到有设值和持久化的代码,所以它是 nil,说说你是怎么设计的?

#3 楼 @rei activation_token 的设置在 models/user.rb 里面,使用 before_create 调用的 create_reset_digest 方法设置值。 我再理一下过程,上面看起来还是有点乱。 1.注册账号的时候进入 controllers/users_controller.rb 的 create 方法。 2.create 方法通过实例化对象 @user 获取 User.new(form_params),这个时候 acvication_token 是有值的。 3.@user 保存成功后使用 models/user.rb 也就是自身的一个方法 send_activation_email 发送邮件。 4.send_activation_email 中使用 UserMailer.account_activation(self).deliver_later 发送邮件。 5.被调用的 UserMailer(mailers/user_mailer.rb ) 会将传递过来的对象赋值给一个新的实例对象 @user 以供在邮件模板 /views/user_mailer/account_activation.html.erb 中使用。 6.邮件模板中使用了具名路由生成链接 edit_account_activation_url(@user.activation_token,email: @user.email),但是 @user.activation_token 值是 nil,然后报错。

在第 3 步的时候还有值,打到这里我突然想起 deliver_later 使用了 ActiveJob 我应该去看一下日志。。然后发现一件事情,deliver_now 是直接把 整个对象传递过去的,而使用 deliver_later 的时候是新建了一个 user 到数据库去取的值,问题应该是在这里了,请问应该怎么解决呢。 调试的图片我去编辑原文补上。

没看到 create_activation_digest 方法。

attr_accessor :remember_token, :activation_token, :reset_token

这几个 token 都是实例变量,没有持久化,重新生成实例就丢失了,job 里面也是重新生成实例。持久化是指在数据库里面加一个字段保存。

另外是有办法不加字段生成 token 的,不过希望先搞清楚这里实例变量的生存周期。

#5 楼 @rei 多谢站长(是站长吧?),刚刚啃完初级的书,能不能推荐一本进阶的看。

#7 楼 @rei 哈哈,这个我正在看,问题解决了再来改成已解决。

你遇到的问题是异步任务里收到的参数和当初传进来的不一样,为什么不一样,是因为 ActiveJob 序列化参数的时候,某些对象使用 class/id对 这样的方式存储。 http://guides.rubyonrails.org/active_job_basics.html#globalid

如何解决:

  1. 持久化,即 Rei 所说
  2. 通过参数传过去,示例:UserMailer.account_activation(self, activation_token: 'token').deliver_later

#9 楼 @lolychee 谢谢解惑,目前我是增加了 token 参数传递解决这个问题(你最后提出的方法),因为觉得这个字段不应该持久化。rei 所说有办法不加字段生成 token,我正在看资料增进对 rails 和 ruby 的了解。 😄

#10 楼 @jk0 不持久化,用户点击链接的时候怎么拿来校验?

Application Message Verifier http://zhaowen.me/blog/2014/04/09/whats-new-in-rails-4-dot-1/

#11 楼 @rei 是这样的,数据库里存了 activation_digest,也就是 token 的哈希摘要,这个 token 只存在于用户收到的邮件链接里面,核对的时候取摘要和数据库里面的对比。 create_activation_digest 方法是我失误删掉了,回去补上。

Lancelot Sidekiq 发送邮件始终出现错误 提及了此话题。 07月03日 09:29
需要 登录 后方可回复, 如果你还没有账号请 注册新账号