You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

嵌套资源中祖父模型传值:基于Event计算Participant费用

如何在嵌套资源保存时根据关联Event计算Participant的费用?

你的当前方案不可行,核心问题是:在嵌套表单提交的场景下,Participant的changeset无法直接获取到Event上下文——你的populate_fees函数尝试从params的"event"键提取Event,但实际提交的参数结构里,Event的信息只会通过Registration的event_id传递,Participant的参数中不会包含event字段,导致那个分支永远不会被触发,fees字段无法被正确赋值。

下面是两种推荐的实现方式:


方案1:在Registration的changeset中传递Event给Participant的changeset

这种方式是在处理Registration的嵌套关联时,先获取对应的Event,再把它注入到Participant的参数中,让Participant的changeset可以拿到Event信息来计算费用:

修改Registration的changeset

def changeset(%Registration{} = registration, attrs) do
  # 根据event_id获取对应的Event(如果是新建Registration,从attrs取event_id;如果是更新,从已有记录取)
  event = 
    if registration.event_id do
      Eventyr.Events.get_event!(registration.event_id)
    else
      Eventyr.Events.get_event!(attrs["event_id"])
    end

  registration
  |> cast(attrs, [:conf_number, :event_id])
  |> validate_required([:conf_number, :event_id])
  # 在cast_assoc时,手动调用Participant的changeset并注入event参数
  |> cast_assoc(:participants, required: true, fn participant, participant_attrs ->
    Participant.changeset(participant, Map.put(participant_attrs, "event", event))
  end)
  |> unique_constraint(:conf_number)
end

修改Participant的populate_fees函数

调整分支匹配,确保能识别传入的Event结构体,并处理age为空的边界情况:

defp populate_fees(%Ecto.Changeset{data: %{id: id}} = changeset) when is_integer(id) do
  changeset
end

defp populate_fees(%{params: %{"event" => %Eventyr.Events.Event{} = event}} = changeset) do
  # 优先取修改后的age,没有则取原有值
  age = get_change(changeset, :age) || get_field(changeset, :age)
  
  fees = cond do
    age in 0..5 -> event.fees_first
    age in 6..16 -> event.fees_second
    age > 16 -> event.fees_third
    true -> Decimal.new(0) # 处理age为空或无效的情况,避免报错
  end

  put_change(changeset, :fees, fees)
end

defp populate_fees(changeset) do
  changeset
end

方案2:使用prepare_changes在Registration的changeset中批量设置Participant费用(更安全)

这种方式不需要修改Participant的changeset逻辑,而是在Registration的changeset最后阶段,通过prepare_changes遍历所有Participant的changeset,直接根据数据库中真实的Event信息设置费用,避免用户篡改参数的风险:

def changeset(%Registration{} = registration, attrs) do
  registration
  |> cast(attrs, [:conf_number, :event_id])
  |> validate_required([:conf_number, :event_id])
  |> cast_assoc(:participants, required: true)
  |> prepare_changes(fn changeset ->
    # 从changeset中获取event_id,查询数据库中的真实Event记录
    event_id = get_field(changeset, :event_id) || get_change(changeset, :event_id)
    event = Eventyr.Events.get_event!(event_id)

    # 遍历所有Participant的changeset,设置对应的fees
    Enum.each(get_field(changeset, :participants), fn participant_changeset ->
      age = get_change(participant_changeset, :age) || get_field(participant_changeset, :age)
      
      fees = cond do
        age in 0..5 -> event.fees_first
        age in 6..16 -> event.fees_second
        age > 16 -> event.fees_third
        true -> Decimal.new(0)
      end

      put_change(participant_changeset, :fees, fees)
    end)

    changeset
  end)
  |> unique_constraint(:conf_number)
end

这种方式的优势在于:Event信息是直接从数据库查询的,不会被用户提交的参数篡改,安全性更高;同时不需要修改Participant的changeset逻辑,代码耦合度更低。


额外注意事项

  • 确保Eventyr.Events.get_event!/1函数能正确通过event_id获取Event记录,如果需要处理不存在的情况,可以换成get_event/1并添加错误处理逻辑。
  • 根据业务需求,决定在更新Participant时是否需要重新计算费用——如果需要,可以去掉Participant changeset中populate_fees的第一个分支(仅当已有id时跳过的逻辑)。

内容的提问来源于stack exchange,提问作者Gorav Bhootra

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 07:27:35