提交 b677aded 编写于 作者: E Edouard CHIN

Move the `ActiveModel:Errors#full_message` method to the `Error` class:

- One regression introduced by the "AM errors as object" features is
  about the `full_messages` method.

  It's currently impossible to call that method if the `base` object
  passed in the constructor of `AM::Errors` doesn't respond to the
  `errors` method.
  That's because `full_messages` now makes a weird back and forth trip

  `AM::Errors#full_messages` -> `AM::Error#full_message` -> `AM::Errors#full_message`

  Since `full_message` (singular) isn't needed by AM::Errors, I moved
  it to the `AM::Error` (singular) class. This way we don't need to
  grab the `AM::Errors` object from the base.
上级 5a9301ce
......@@ -53,6 +53,7 @@ module ActiveModel
eager_autoload do
autoload :Errors
autoload :Error
autoload :RangeError, "active_model/errors"
autoload :StrictValidationFailed, "active_model/errors"
autoload :UnknownAttributeError, "active_model/errors"
......
......@@ -8,6 +8,92 @@ class Error
CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank, :strict]
MESSAGE_OPTIONS = [:message]
class << self
attr_accessor :i18n_customize_full_message # :nodoc:
end
self.i18n_customize_full_message = false
def self.full_message(attribute, message, base_class) # :nodoc:
return message if attribute == :base
attribute = attribute.to_s
if i18n_customize_full_message && base_class.respond_to?(:i18n_scope)
attribute = attribute.remove(/\[\d\]/)
parts = attribute.split(".")
attribute_name = parts.pop
namespace = parts.join("/") unless parts.empty?
attributes_scope = "#{base_class.i18n_scope}.errors.models"
if namespace
defaults = base_class.lookup_ancestors.map do |klass|
[
:"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.attributes.#{attribute_name}.format",
:"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.format",
]
end
else
defaults = base_class.lookup_ancestors.map do |klass|
[
:"#{attributes_scope}.#{klass.model_name.i18n_key}.attributes.#{attribute_name}.format",
:"#{attributes_scope}.#{klass.model_name.i18n_key}.format",
]
end
end
defaults.flatten!
else
defaults = []
end
defaults << :"errors.format"
defaults << "%{attribute} %{message}"
attr_name = attribute.tr(".", "_").humanize
attr_name = base_class.human_attribute_name(attribute, default: attr_name)
I18n.t(defaults.shift,
default: defaults,
attribute: attr_name,
message: message)
end
def self.generate_message(attribute, type, base, options) # :nodoc:
type = options.delete(:message) if options[:message].is_a?(Symbol)
value = (attribute != :base ? base.send(:read_attribute_for_validation, attribute) : nil)
options = {
model: base.model_name.human,
attribute: base.class.human_attribute_name(attribute),
value: value,
object: base
}.merge!(options)
if base.class.respond_to?(:i18n_scope)
i18n_scope = base.class.i18n_scope.to_s
defaults = base.class.lookup_ancestors.flat_map do |klass|
[ :"#{i18n_scope}.errors.models.#{klass.model_name.i18n_key}.attributes.#{attribute}.#{type}",
:"#{i18n_scope}.errors.models.#{klass.model_name.i18n_key}.#{type}" ]
end
defaults << :"#{i18n_scope}.errors.messages.#{type}"
catch(:exception) do
translation = I18n.translate(defaults.first, options.merge(default: defaults.drop(1), throw: true))
return translation unless translation.nil?
end unless options[:message]
else
defaults = []
end
defaults << :"errors.attributes.#{attribute}.#{type}"
defaults << :"errors.messages.#{type}"
key = defaults.shift
defaults = options.delete(:message) if options[:message]
options[:default] = defaults
I18n.translate(key, options)
end
def initialize(base, attribute, type = :invalid, **options)
@base = base
@attribute = attribute
......@@ -28,7 +114,7 @@ def initialize_dup(other)
def message
case raw_type
when Symbol
base.errors.generate_message(attribute, raw_type, options.except(*CALLBACKS_OPTIONS))
self.class.generate_message(attribute, raw_type, @base, options.except(*CALLBACKS_OPTIONS))
else
raw_type
end
......@@ -39,7 +125,7 @@ def detail
end
def full_message
base.errors.full_message(attribute, message)
self.class.full_message(attribute, message, @base.class)
end
# See if error matches provided +attribute+, +type+ and +options+.
......
......@@ -69,11 +69,6 @@ class Errors
LEGACY_ATTRIBUTES = [:messages, :details].freeze
class << self
attr_accessor :i18n_customize_full_message # :nodoc:
end
self.i18n_customize_full_message = false
attr_reader :errors
alias :objects :errors
......@@ -467,47 +462,7 @@ def messages_for(attribute)
#
# person.errors.full_message(:name, 'is invalid') # => "Name is invalid"
def full_message(attribute, message)
return message if attribute == :base
attribute = attribute.to_s
if self.class.i18n_customize_full_message && @base.class.respond_to?(:i18n_scope)
attribute = attribute.remove(/\[\d\]/)
parts = attribute.split(".")
attribute_name = parts.pop
namespace = parts.join("/") unless parts.empty?
attributes_scope = "#{@base.class.i18n_scope}.errors.models"
if namespace
defaults = @base.class.lookup_ancestors.map do |klass|
[
:"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.attributes.#{attribute_name}.format",
:"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.format",
]
end
else
defaults = @base.class.lookup_ancestors.map do |klass|
[
:"#{attributes_scope}.#{klass.model_name.i18n_key}.attributes.#{attribute_name}.format",
:"#{attributes_scope}.#{klass.model_name.i18n_key}.format",
]
end
end
defaults.flatten!
else
defaults = []
end
defaults << :"errors.format"
defaults << "%{attribute} %{message}"
attr_name = attribute.tr(".", "_").humanize
attr_name = @base.class.human_attribute_name(attribute, default: attr_name)
I18n.t(defaults.shift,
default: defaults,
attribute: attr_name,
message: message)
Error.full_message(attribute, message, @base.class)
end
# Translates an error message in its default scope
......@@ -535,40 +490,7 @@ def full_message(attribute, message)
# * <tt>errors.attributes.title.blank</tt>
# * <tt>errors.messages.blank</tt>
def generate_message(attribute, type = :invalid, options = {})
type = options.delete(:message) if options[:message].is_a?(Symbol)
value = (attribute != :base ? @base.send(:read_attribute_for_validation, attribute) : nil)
options = {
model: @base.model_name.human,
attribute: @base.class.human_attribute_name(attribute),
value: value,
object: @base
}.merge!(options)
if @base.class.respond_to?(:i18n_scope)
i18n_scope = @base.class.i18n_scope.to_s
defaults = @base.class.lookup_ancestors.flat_map do |klass|
[ :"#{i18n_scope}.errors.models.#{klass.model_name.i18n_key}.attributes.#{attribute}.#{type}",
:"#{i18n_scope}.errors.models.#{klass.model_name.i18n_key}.#{type}" ]
end
defaults << :"#{i18n_scope}.errors.messages.#{type}"
catch(:exception) do
translation = I18n.translate(defaults.first, options.merge(default: defaults.drop(1), throw: true))
return translation unless translation.nil?
end unless options[:message]
else
defaults = []
end
defaults << :"errors.attributes.#{attribute}.#{type}"
defaults << :"errors.messages.#{type}"
key = defaults.shift
defaults = options.delete(:message) if options[:message]
options[:default] = defaults
I18n.translate(key, options)
Error.generate_message(attribute, type, @base, options)
end
def marshal_load(array) # :nodoc:
......
......@@ -14,7 +14,7 @@ class Railtie < Rails::Railtie # :nodoc:
end
initializer "active_model.i18n_customize_full_message" do
ActiveModel::Errors.i18n_customize_full_message = config.active_model.delete(:i18n_customize_full_message) || false
ActiveModel::Error.i18n_customize_full_message = config.active_model.delete(:i18n_customize_full_message) || false
end
end
end
......@@ -482,6 +482,27 @@ def test_no_key
assert_nil person.errors.as_json.default_proc
end
test "full_messages doesn't require the base object to respond to `:errors" do
model = Class.new do
def initialize
@errors = ActiveModel::Errors.new(self)
@errors.add(:name, "bar")
end
def self.human_attribute_name(attr, options = {})
"foo"
end
def call
error_wrapper = Struct.new(:model_errors)
error_wrapper.new(@errors)
end
end
assert_equal(["foo bar"], model.new.call.model_errors.full_messages)
end
test "full_messages creates a list of error messages with the attribute name included" do
person = Person.new
person.errors.add(:name, "cannot be blank")
......
......@@ -35,20 +35,20 @@ def setup
test "i18n customize full message defaults to false" do
@app.initialize!
assert_equal false, ActiveModel::Errors.i18n_customize_full_message
assert_equal false, ActiveModel::Error.i18n_customize_full_message
end
test "i18n customize full message can be disabled" do
@app.config.active_model.i18n_customize_full_message = false
@app.initialize!
assert_equal false, ActiveModel::Errors.i18n_customize_full_message
assert_equal false, ActiveModel::Error.i18n_customize_full_message
end
test "i18n customize full message can be enabled" do
@app.config.active_model.i18n_customize_full_message = true
@app.initialize!
assert_equal true, ActiveModel::Errors.i18n_customize_full_message
assert_equal true, ActiveModel::Error.i18n_customize_full_message
end
end
......@@ -13,8 +13,8 @@ def setup
I18n.backend = I18n::Backend::Simple.new
I18n.backend.store_translations("en", errors: { messages: { custom: nil } })
@original_i18n_customize_full_message = ActiveModel::Errors.i18n_customize_full_message
ActiveModel::Errors.i18n_customize_full_message = true
@original_i18n_customize_full_message = ActiveModel::Error.i18n_customize_full_message
ActiveModel::Error.i18n_customize_full_message = true
end
def teardown
......@@ -24,7 +24,7 @@ def teardown
I18n.load_path.replace @old_load_path
I18n.backend = @old_backend
I18n.backend.reload!
ActiveModel::Errors.i18n_customize_full_message = @original_i18n_customize_full_message
ActiveModel::Error.i18n_customize_full_message = @original_i18n_customize_full_message
end
def test_full_message_encoding
......@@ -49,7 +49,7 @@ def test_errors_full_messages_uses_format
end
def test_errors_full_messages_doesnt_use_attribute_format_without_config
ActiveModel::Errors.i18n_customize_full_message = false
ActiveModel::Error.i18n_customize_full_message = false
I18n.backend.store_translations("en", activemodel: {
errors: { models: { person: { attributes: { name: { format: "%{message}" } } } } } })
......@@ -60,7 +60,7 @@ def test_errors_full_messages_doesnt_use_attribute_format_without_config
end
def test_errors_full_messages_uses_attribute_format
ActiveModel::Errors.i18n_customize_full_message = true
ActiveModel::Error.i18n_customize_full_message = true
I18n.backend.store_translations("en", activemodel: {
errors: { models: { person: { attributes: { name: { format: "%{message}" } } } } } })
......@@ -71,7 +71,7 @@ def test_errors_full_messages_uses_attribute_format
end
def test_errors_full_messages_uses_model_format
ActiveModel::Errors.i18n_customize_full_message = true
ActiveModel::Error.i18n_customize_full_message = true
I18n.backend.store_translations("en", activemodel: {
errors: { models: { person: { format: "%{message}" } } } })
......@@ -82,7 +82,7 @@ def test_errors_full_messages_uses_model_format
end
def test_errors_full_messages_uses_deeply_nested_model_attributes_format
ActiveModel::Errors.i18n_customize_full_message = true
ActiveModel::Error.i18n_customize_full_message = true
I18n.backend.store_translations("en", activemodel: {
errors: { models: { 'person/contacts/addresses': { attributes: { street: { format: "%{message}" } } } } } })
......@@ -93,7 +93,7 @@ def test_errors_full_messages_uses_deeply_nested_model_attributes_format
end
def test_errors_full_messages_uses_deeply_nested_model_model_format
ActiveModel::Errors.i18n_customize_full_message = true
ActiveModel::Error.i18n_customize_full_message = true
I18n.backend.store_translations("en", activemodel: {
errors: { models: { 'person/contacts/addresses': { format: "%{message}" } } } })
......@@ -104,7 +104,7 @@ def test_errors_full_messages_uses_deeply_nested_model_model_format
end
def test_errors_full_messages_with_indexed_deeply_nested_attributes_and_attributes_format
ActiveModel::Errors.i18n_customize_full_message = true
ActiveModel::Error.i18n_customize_full_message = true
I18n.backend.store_translations("en", activemodel: {
errors: { models: { 'person/contacts/addresses': { attributes: { street: { format: "%{message}" } } } } } })
......@@ -115,7 +115,7 @@ def test_errors_full_messages_with_indexed_deeply_nested_attributes_and_attribut
end
def test_errors_full_messages_with_indexed_deeply_nested_attributes_and_model_format
ActiveModel::Errors.i18n_customize_full_message = true
ActiveModel::Error.i18n_customize_full_message = true
I18n.backend.store_translations("en", activemodel: {
errors: { models: { 'person/contacts/addresses': { format: "%{message}" } } } })
......@@ -126,7 +126,7 @@ def test_errors_full_messages_with_indexed_deeply_nested_attributes_and_model_fo
end
def test_errors_full_messages_with_indexed_deeply_nested_attributes_and_i18n_attribute_name
ActiveModel::Errors.i18n_customize_full_message = true
ActiveModel::Error.i18n_customize_full_message = true
I18n.backend.store_translations("en", activemodel: {
attributes: { 'person/contacts/addresses': { country: "Country" } }
......@@ -138,7 +138,7 @@ def test_errors_full_messages_with_indexed_deeply_nested_attributes_and_i18n_att
end
def test_errors_full_messages_with_indexed_deeply_nested_attributes_without_i18n_config
ActiveModel::Errors.i18n_customize_full_message = false
ActiveModel::Error.i18n_customize_full_message = false
I18n.backend.store_translations("en", activemodel: {
errors: { models: { 'person/contacts/addresses': { attributes: { street: { format: "%{message}" } } } } } })
......@@ -149,7 +149,7 @@ def test_errors_full_messages_with_indexed_deeply_nested_attributes_without_i18n
end
def test_errors_full_messages_with_i18n_attribute_name_without_i18n_config
ActiveModel::Errors.i18n_customize_full_message = false
ActiveModel::Error.i18n_customize_full_message = false
I18n.backend.store_translations("en", activemodel: {
attributes: { 'person/contacts[0]/addresses[0]': { country: "Country" } }
......@@ -178,8 +178,8 @@ def test_errors_full_messages_with_i18n_attribute_name_without_i18n_config
test "validates_confirmation_of on generated message #{name}" do
person_class.validates_confirmation_of :title, validation_options
@person.title_confirmation = "foo"
call = [:title_confirmation, :confirmation, generate_message_options.merge(attribute: "Title")]
assert_called_with(@person.errors, :generate_message, call) do
call = [:title_confirmation, :confirmation, @person, generate_message_options.merge(attribute: "Title")]
assert_called_with(ActiveModel::Error, :generate_message, call) do
@person.valid?
@person.errors.messages
end
......@@ -189,8 +189,8 @@ def test_errors_full_messages_with_i18n_attribute_name_without_i18n_config
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_acceptance_of on generated message #{name}" do
person_class.validates_acceptance_of :title, validation_options.merge(allow_nil: false)
call = [:title, :accepted, generate_message_options]
assert_called_with(@person.errors, :generate_message, call) do
call = [:title, :accepted, @person, generate_message_options]
assert_called_with(ActiveModel::Error, :generate_message, call) do
@person.valid?
@person.errors.messages
end
......@@ -200,8 +200,8 @@ def test_errors_full_messages_with_i18n_attribute_name_without_i18n_config
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_presence_of on generated message #{name}" do
person_class.validates_presence_of :title, validation_options
call = [:title, :blank, generate_message_options]
assert_called_with(@person.errors, :generate_message, call) do
call = [:title, :blank, @person, generate_message_options]
assert_called_with(ActiveModel::Error, :generate_message, call) do
@person.valid?
@person.errors.messages
end
......@@ -211,8 +211,8 @@ def test_errors_full_messages_with_i18n_attribute_name_without_i18n_config
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_length_of for :within on generated message when too short #{name}" do
person_class.validates_length_of :title, validation_options.merge(within: 3..5)
call = [:title, :too_short, generate_message_options.merge(count: 3)]
assert_called_with(@person.errors, :generate_message, call) do
call = [:title, :too_short, @person, generate_message_options.merge(count: 3)]
assert_called_with(ActiveModel::Error, :generate_message, call) do
@person.valid?
@person.errors.messages
end
......@@ -223,8 +223,8 @@ def test_errors_full_messages_with_i18n_attribute_name_without_i18n_config
test "validates_length_of for :too_long generated message #{name}" do
person_class.validates_length_of :title, validation_options.merge(within: 3..5)
@person.title = "this title is too long"
call = [:title, :too_long, generate_message_options.merge(count: 5)]
assert_called_with(@person.errors, :generate_message, call) do
call = [:title, :too_long, @person, generate_message_options.merge(count: 5)]
assert_called_with(ActiveModel::Error, :generate_message, call) do
@person.valid?
@person.errors.messages
end
......@@ -234,8 +234,8 @@ def test_errors_full_messages_with_i18n_attribute_name_without_i18n_config
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_length_of for :is on generated message #{name}" do
person_class.validates_length_of :title, validation_options.merge(is: 5)
call = [:title, :wrong_length, generate_message_options.merge(count: 5)]
assert_called_with(@person.errors, :generate_message, call) do
call = [:title, :wrong_length, @person, generate_message_options.merge(count: 5)]
assert_called_with(ActiveModel::Error, :generate_message, call) do
@person.valid?
@person.errors.messages
end
......@@ -246,8 +246,8 @@ def test_errors_full_messages_with_i18n_attribute_name_without_i18n_config
test "validates_format_of on generated message #{name}" do
person_class.validates_format_of :title, validation_options.merge(with: /\A[1-9][0-9]*\z/)
@person.title = "72x"
call = [:title, :invalid, generate_message_options.merge(value: "72x")]
assert_called_with(@person.errors, :generate_message, call) do
call = [:title, :invalid, @person, generate_message_options.merge(value: "72x")]
assert_called_with(ActiveModel::Error, :generate_message, call) do
@person.valid?
@person.errors.messages
end
......@@ -258,8 +258,8 @@ def test_errors_full_messages_with_i18n_attribute_name_without_i18n_config
test "validates_inclusion_of on generated message #{name}" do
person_class.validates_inclusion_of :title, validation_options.merge(in: %w(a b c))
@person.title = "z"
call = [:title, :inclusion, generate_message_options.merge(value: "z")]
assert_called_with(@person.errors, :generate_message, call) do
call = [:title, :inclusion, @person, generate_message_options.merge(value: "z")]
assert_called_with(ActiveModel::Error, :generate_message, call) do
@person.valid?
@person.errors.messages
end
......@@ -270,8 +270,8 @@ def test_errors_full_messages_with_i18n_attribute_name_without_i18n_config
test "validates_inclusion_of using :within on generated message #{name}" do
person_class.validates_inclusion_of :title, validation_options.merge(within: %w(a b c))
@person.title = "z"
call = [:title, :inclusion, generate_message_options.merge(value: "z")]
assert_called_with(@person.errors, :generate_message, call) do
call = [:title, :inclusion, @person, generate_message_options.merge(value: "z")]
assert_called_with(ActiveModel::Error, :generate_message, call) do
@person.valid?
@person.errors.messages
end
......@@ -282,8 +282,8 @@ def test_errors_full_messages_with_i18n_attribute_name_without_i18n_config
test "validates_exclusion_of generated message #{name}" do
person_class.validates_exclusion_of :title, validation_options.merge(in: %w(a b c))
@person.title = "a"
call = [:title, :exclusion, generate_message_options.merge(value: "a")]
assert_called_with(@person.errors, :generate_message, call) do
call = [:title, :exclusion, @person, generate_message_options.merge(value: "a")]
assert_called_with(ActiveModel::Error, :generate_message, call) do
@person.valid?
@person.errors.messages
end
......@@ -294,8 +294,8 @@ def test_errors_full_messages_with_i18n_attribute_name_without_i18n_config
test "validates_exclusion_of using :within generated message #{name}" do
person_class.validates_exclusion_of :title, validation_options.merge(within: %w(a b c))
@person.title = "a"
call = [:title, :exclusion, generate_message_options.merge(value: "a")]
assert_called_with(@person.errors, :generate_message, call) do
call = [:title, :exclusion, @person, generate_message_options.merge(value: "a")]
assert_called_with(ActiveModel::Error, :generate_message, call) do
@person.valid?
@person.errors.messages
end
......@@ -306,8 +306,8 @@ def test_errors_full_messages_with_i18n_attribute_name_without_i18n_config
test "validates_numericality_of generated message #{name}" do
person_class.validates_numericality_of :title, validation_options
@person.title = "a"
call = [:title, :not_a_number, generate_message_options.merge(value: "a")]
assert_called_with(@person.errors, :generate_message, call) do
call = [:title, :not_a_number, @person, generate_message_options.merge(value: "a")]
assert_called_with(ActiveModel::Error, :generate_message, call) do
@person.valid?
@person.errors.messages
end
......@@ -318,8 +318,8 @@ def test_errors_full_messages_with_i18n_attribute_name_without_i18n_config
test "validates_numericality_of for :only_integer on generated message #{name}" do
person_class.validates_numericality_of :title, validation_options.merge(only_integer: true)
@person.title = "0.0"
call = [:title, :not_an_integer, generate_message_options.merge(value: "0.0")]
assert_called_with(@person.errors, :generate_message, call) do
call = [:title, :not_an_integer, @person, generate_message_options.merge(value: "0.0")]
assert_called_with(ActiveModel::Error, :generate_message, call) do
@person.valid?
@person.errors.messages
end
......@@ -330,8 +330,8 @@ def test_errors_full_messages_with_i18n_attribute_name_without_i18n_config
test "validates_numericality_of for :odd on generated message #{name}" do
person_class.validates_numericality_of :title, validation_options.merge(only_integer: true, odd: true)
@person.title = 0
call = [:title, :odd, generate_message_options.merge(value: 0)]
assert_called_with(@person.errors, :generate_message, call) do
call = [:title, :odd, @person, generate_message_options.merge(value: 0)]
assert_called_with(ActiveModel::Error, :generate_message, call) do
@person.valid?
@person.errors.messages
end
......@@ -342,8 +342,8 @@ def test_errors_full_messages_with_i18n_attribute_name_without_i18n_config
test "validates_numericality_of for :less_than on generated message #{name}" do
person_class.validates_numericality_of :title, validation_options.merge(only_integer: true, less_than: 0)
@person.title = 1
call = [:title, :less_than, generate_message_options.merge(value: 1, count: 0)]
assert_called_with(@person.errors, :generate_message, call) do
call = [:title, :less_than, @person, generate_message_options.merge(value: 1, count: 0)]
assert_called_with(ActiveModel::Error, :generate_message, call) do
@person.valid?
@person.errors.messages
end
......
......@@ -51,7 +51,7 @@ def replied_topic
test "validates_uniqueness_of on generated message #{name}" do
Topic.validates_uniqueness_of :title, validation_options
@topic.title = unique_topic.title
assert_called_with(@topic.errors, :generate_message, [:title, :taken, generate_message_options.merge(value: "unique!")]) do
assert_called_with(ActiveModel::Error, :generate_message, [:title, :taken, @topic, generate_message_options.merge(value: "unique!")]) do
@topic.valid?
@topic.errors.messages
end
......@@ -61,7 +61,7 @@ def replied_topic
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_associated on generated message #{name}" do
Topic.validates_associated :replies, validation_options
assert_called_with(replied_topic.errors, :generate_message, [:replies, :invalid, generate_message_options.merge(value: replied_topic.replies)]) do
assert_called_with(ActiveModel::Error, :generate_message, [:replies, :invalid, replied_topic, generate_message_options.merge(value: replied_topic.replies)]) do
replied_topic.save
replied_topic.errors.messages
end
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册