if Code.ensure_loaded?(Plug) do
defmodule WeChat.Pay.EventHandler do
@moduledoc """
微信支付 回调通知处理器
[官方文档](https://pay.weixin.qq.com/docs/merchant/apis/jsapi-payment/payment-notice.html)
注意
对后台通知交互时,如果微信收到应答不是成功或超时,微信认为通知失败,
微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功
同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。
推荐的做法是,当商户系统收到通知进行处理时,先检查对应业务数据的状态,
并判断该通知是否已经处理。如果未处理,则再进行处理;如果已处理,则直接返回结果成功。
在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。
如果在所有通知频率后没有收到微信侧回调。商户应调用查询订单接口确认订单状态。
## 通知规则
用户支付完成后,微信会把相关支付结果和用户信息发送给商户,商户需要接收处理该消息,并返回应答。
对后台通知交互时,如果微信收到商户的应答不符合规范或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,
但微信不保证通知最终能成功。
通知频率为 `15s`/`15s`/`30s`/`3m`/`10m`/`20m`/`30m`/`30m`/`30m`/`60m`/`3h`/`3h`/`3h`/`6h`/`6h` - 总计 `24h4m`
## Usage
### For Plug
defmodule YourAppWeb.PayEventRouter do
use Plug.Router
plug :match
plug :dispatch
Code.ensure_compiled!(WechatPayDemo.PayClient)
post "/api/pay/callback",
to: #{inspect(__MODULE__)},
init_opts: [client: WxPay, event_handler: &YourModule.handle_event/3]
match _, do: conn
end
### For Phoenix
建议是定义一个上方的 `PayEventRouter`, 然后接入到 `endpoint` 中 `plug Plug.Parsers` 的上一行:
defmodule YourAppWeb.Endpoint do
# ...
plug Plug.RequestId
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
plug YourAppWeb.PayEventRouter # <<== add to here, before Plug.Parsers
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Phoenix.json_library()
plug Plug.MethodOverride
plug Plug.Head
plug Plug.Session, @session_options
plug YourAppWeb.Router
# ...
end
** 注意 **, `Plug.Parsers` 会解析 `body`, 请确保此 `plug` 传入的 `body` 为 `binary` 格式, 否则将会导致验签失败
如果确认 `body` 未被解析, 亦可使用下面方式接入到 `router` 里面:
post "/wx/pay/event", #{inspect(__MODULE__)},
client: WxPay,
event_handler: &YourModule.handle_event/3
before phoenix 1.17:
forward "/wx/pay/event", #{inspect(__MODULE__)},
client: WxPay,
event_handler: &YourModule.handle_event/3
## Options
- `event_handler`: 必填, [定义](`t:event_handler/0`)
- `client`: 必填, [定义](`t:WeChat.Pay.client/0`)
"""
import Plug.Conn
require Logger
alias WeChat.Pay.{Crypto, Certificates}
@behaviour Plug
@typedoc """
事件处理回调返回值
返回值说明:
- `:ok`: 成功
- `:error`: 返回错误
- `{:error, any}`: 返回错误
"""
@type event_handler_return :: :ok | :error | {:error, any} | Plug.Conn.t()
@typedoc "事件处理回调函数"
@type event_handler :: (Pay.client(), Plug.Conn.t(), message :: map -> event_handler_return)
@doc false
def init(opts) do
opts = Map.new(opts)
event_handler =
with {:ok, handler} <- Map.fetch(opts, :event_handler),
true <- is_function(handler, 3) do
handler
else
:error ->
raise ArgumentError, "please set :event_handler when using #{inspect(__MODULE__)}"
false ->
raise ArgumentError,
"the :event_handler must arg 3 function when using #{inspect(__MODULE__)}"
end
case Map.fetch(opts, :client) do
{:ok, client} when is_atom(client) ->
if function_exported?(client, :api_secret_key, 0) do
%{event_handler: event_handler, client: client}
else
raise ArgumentError, "please set WeChat.Pay :client when using #{inspect(__MODULE__)}"
end
_ ->
raise ArgumentError, "please set :client when using #{inspect(__MODULE__)}"
end
end
@doc false
def call(%{method: "POST"} = conn, %{client: client, event_handler: event_handler}) do
handle_event_request(conn, client, event_handler)
end
def call(conn, _), do: json(conn, 400, %{"code" => "FAIL", "message" => "Bad Request"})
def handle_event_request(conn, client, event_handler) do
with nonce when is_binary(nonce) <- get_header(conn, "wechatpay-nonce"),
signature when is_binary(signature) <- get_header(conn, "wechatpay-signature"),
timestamp when is_binary(timestamp) <- get_header(conn, "wechatpay-timestamp"),
serial_no when is_binary(serial_no) <- get_header(conn, "wechatpay-serial"),
public_key when not is_nil(public_key) <- Certificates.get_cert(client, serial_no),
{:ok, body, body_map, conn} <- check_and_read_body(conn),
true <- Crypto.verify(signature, timestamp, nonce, body, public_key) do
try do
message =
with %{
"resource_type" => "encrypt-resource",
"resource" => %{
"algorithm" => "AEAD_AES_256_GCM",
"nonce" => iv,
"ciphertext" => ciphertext,
"associated_data" => associated_data
}
} = message <- body_map do
data =
Crypto.decrypt_aes_256_gcm(
client.api_secret_key(),
ciphertext,
associated_data,
iv
)
|> Jason.decode!()
Map.put(message, "data", data)
end
event_handler.(client, conn, message)
rescue
error ->
Logger.error(
"Handle request for #{inspect(client)} failed!!! body:#{body}, error: #{inspect(error)}"
)
json(conn, 500, %{"code" => "FAIL", "message" => "Internal Server Error"})
else
:ok -> send_resp(conn, 200, "")
:error -> json(conn, 500, %{"code" => "FAIL", "message" => "Unexpected Error"})
{:error, _} -> json(conn, 500, %{"code" => "FAIL", "message" => "Internal Error"})
conn -> conn
end
else
_ -> json(conn, 400, %{"code" => "FAIL", "message" => "Bad Request"})
end
|> halt()
end
defp get_header(conn, key) do
case get_req_header(conn, key) do
[v | _] -> v
[] -> nil
end
end
defp json(conn, status, data) do
conn |> put_resp_content_type("application/json") |> send_resp(status, Jason.encode!(data))
end
defp check_and_read_body(%{body_params: body_params} = conn) do
case body_params do
b when is_struct(b) ->
with {:ok, body, conn} when is_binary(body) <- read_body(conn),
{:ok, body_map} when is_map(body_map) <- Jason.decode(body) do
{:ok, body, body_map, conn}
else
_ -> :bad_request
end
body when is_binary(body) ->
case Jason.decode(body) do
{:ok, body_map} when is_map(body_map) -> {:ok, body, body_map, conn}
_ -> :bad_request
end
body when is_map(body) ->
Logger.warning(
"#{inspect(__MODULE__)} handle parsed body: #{inspect(body)}, please use origin body."
)
:bad_request
end
end
end
end