Skip to content

Commit

Permalink
Merge pull request #3 from feng19/pay
Browse files Browse the repository at this point in the history
Support Wechat Pay
  • Loading branch information
feng19 authored Oct 13, 2023
2 parents 3da496b + 762b4cc commit 123a295
Show file tree
Hide file tree
Showing 34 changed files with 1,204 additions and 366 deletions.
27 changes: 13 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,19 @@
**WeChat SDK for Elixir**

- 目前 `Elixir` 中支持最完善的微信SDK
- 已支持:
- [公众号](https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Overview.html)
- [小程序](https://developers.weixin.qq.com/miniprogram/dev/framework/)
- [第三方应用](https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/getting_started/how_to_read.html)
- [企业微信](https://developer.work.weixin.qq.com/document/path/90556)
- [微信支付](https://pay.weixin.qq.com/wiki/doc/apiv3/index.shtml)
- WIP: 企业微信服务商
- 支持:
- `WeChat` => [公众号](https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Overview.html)
- `WeChat` => [小程序](https://developers.weixin.qq.com/miniprogram/dev/framework/)
- `WeChat` => [第三方应用](https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/getting_started/how_to_read.html)
- `WeChat.Work` => [企业微信](https://developer.work.weixin.qq.com/document/path/90556)
- `WeChat.Pay` => [微信支付](https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pages/index.shtml)

### Links

- [开发前必读](https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Getting_Started_Guide.html)
- [在线文档](https://hex.pm/packages/wechat_sdk)
- [WeChat SDK 使用指南](https://feng19.com/2022/07/08/wechat_for_elixir_usage/)
- [示例项目](https://github.com/feng19/wechat_demo)
- [WeChat SDK 使用指南](https://feng19.com/2022/07/08/wechat_for_elixir_usage/)(by Feng19)
- [示例项目 - github - feng19/wechat_demo](https://github.com/feng19/wechat_demo)
- [微信官方文档 - 开发前必读](https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Getting_Started_Guide.html)
- [微信官方文档 - 在线文档](https://hex.pm/packages/wechat_sdk)

## Installation

Expand All @@ -38,7 +37,7 @@ end

## Usage

### 定义公众号 `Client` 模块
### 定义公众号 Client 模块

```elixir
defmodule YourApp.WeChatAppCodeName do
Expand All @@ -51,9 +50,9 @@ end

其他类型定义请看 [WeChat](https://hexdocs.pm/wechat_sdk/WeChat.html#module-定义-client-模块)

详细参数说明请看 [options](https://hexdocs.pm/wechat_sdk/WeChat.html#t:options/0)
定义参数说明请看 [Options](https://hexdocs.pm/wechat_sdk/WeChat.html#t:options/0)

### 自动刷新 `AccessToken`
### 自动刷新 AccessToken

在调用接口之前,必须先获取 [`AccessToken`](https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html) 才能 调用接口,
[官方说明](https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Getting_Started_Guide.html#_1-5-%E9%87%8D%E8%A6%81%E4%BA%8B%E6%83%85%E6%8F%90%E5%89%8D%E4%BA%A4%E4%BB%A3)
Expand Down
24 changes: 11 additions & 13 deletions lib/wechat.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ defmodule WeChat do
@moduledoc """
WeChat SDK for Elixir
## 定义 `Client` 模块
## 定义 Client 模块
### 公众号(默认)
Expand Down Expand Up @@ -34,16 +34,7 @@ defmodule WeChat do
component_appid: "wx-third-appid", # 第三方 appid
end
### 企业微信
defmodule YourApp.WeChatAppCodeName do
@moduledoc "CodeName"
use WeChat.Work,
corp_id: "corp_id",
agents: [%Work.Agent{name: :agent_name, id: 10000, secret: "your_secret"}, ...]
end
## 参数说明
## 定义参数说明
请看 `t:options/0`
Expand All @@ -59,6 +50,13 @@ defmodule WeChat do
`WeChat.Material.batch_get_material(YourApp.WeChatAppCodeName, :image, 2)`
## 企业微信
详情请看 `WeChat.Work`
## 微信支付
详情请看 `WeChat.Pay`
"""
import WeChat.Utils, only: [doc_link_prefix: 0]
alias WeChat.{Refresher, HubClient, HubServer}
Expand Down Expand Up @@ -163,8 +161,8 @@ defmodule WeChat do
token: token | env_option,
requester: module
]
@type client :: module()
@type requester :: module()
@type client :: module
@type requester :: module
@type response :: Tesla.Env.result()
@type start_options :: %{
optional(:hub_springboard_url) => HubClient.hub_springboard_url(),
Expand Down
112 changes: 92 additions & 20 deletions lib/wechat/builder/pay.ex
Original file line number Diff line number Diff line change
@@ -1,53 +1,125 @@
defmodule WeChat.Builder.Pay do
@moduledoc false
alias WeChat.Builder.Utils

defmacro __using__(options \\ []) do
options = Map.new(options)
client = __CALLER__.module
check_options!(options, client)
options = options |> Macro.prewalk(&Macro.expand(&1, __CALLER__)) |> Map.new()
requester = Map.get(options, :requester, WeChat.Requester.Pay)
storage = Map.get(options, :storage, WeChat.Storage.PayFile)
public_key = WeChat.Pay.Utils.decode_key(options.client_cert)
# private_key = WeChat.Pay.Utils.decode_key(options.client_key)
private_key = WeChat.Pay.Crypto.load_pem!(options.client_key)
public_key = private_key |> X509.PublicKey.derive() |> Macro.escape()
private_key = Macro.escape(private_key)

api_secret_key =
with :not_handle <- Utils.handle_env_option(client, :api_secret_key, options.api_secret_key) do
quote do
def api_secret_key, do: unquote(options.api_secret_key)
end
end

quote do
use Supervisor

@spec start_link(WeChat.Pay.start_options()) :: Supervisor.on_start()
def start_link(opts) do
opts = Map.new(opts)
requester_a = WeChat.Pay.get_requester_spec(:A, __MODULE__, opts.cacerts)
requester_b = WeChat.Pay.get_requester_spec(:B, __MODULE__, opts.cacerts)
WeChat.Pay.put_requester_opts(__MODULE__, :A, opts.serial_no)
Supervisor.start_link(__MODULE__, Map.new(opts), name: :"#{__MODULE__}.Supervisor")
end

@impl true
def init(opts) do
refresher = Map.get(opts, :refresher, WeChat.Refresher.Pay)
children = [{refresher, {__MODULE__, opts}}, requester_a, requester_b]
opts = [strategy: :one_for_one, name: :"#{__MODULE__}.Supervisor"]
Supervisor.start_link(children, opts)

Map.get_lazy(opts, :cacerts, fn ->
# Load Cacerts From Storage
{:ok, cacerts} = unquote(storage).restore(unquote(options.mch_id), :cacerts)
cacerts
end)
|> WeChat.Pay.Certificates.put_certs(__MODULE__)

children = [
{refresher, Map.put(opts, :client, __MODULE__)},
WeChat.Pay.get_requester_spec(__MODULE__)
]

Supervisor.init(children, strategy: :one_for_one)
end

@spec get(url :: binary) :: WeChat.response()
def get(url), do: get(url, [])

@spec get(url :: binary, opts :: keyword) :: WeChat.response()
def get(url, opts) do
%{name: name, serial_no: serial_no} = WeChat.Pay.get_requester_opts(__MODULE__)

__MODULE__
|> unquote(requester).new(__MODULE__, name, serial_no)
unquote(requester).new(__MODULE__)
|> Tesla.get(url, opts)
end

@spec post(url :: binary, body :: any) :: WeChat.response()
def post(url, body), do: post(url, body, [])

@spec post(url :: binary, body :: any, opts :: keyword) :: WeChat.response()
def post(url, body, opts) do
%{name: name, serial_no: serial_no} = WeChat.Pay.get_requester_opts(__MODULE__)

__MODULE__
|> unquote(requester).new(__MODULE__, name, serial_no)
unquote(requester).new(__MODULE__)
|> Tesla.post(url, body, opts)
end

@spec mch_id() :: WeChat.Pay.mch_id()
def mch_id, do: unquote(options.mch_id)
def api_secret_key, do: unquote(options.api_secret_key)
@spec api_secret_key() :: WeChat.Pay.api_secret_key()
unquote(api_secret_key)
@spec client_serial_no() :: WeChat.Pay.client_serial_no()
def client_serial_no, do: unquote(options.client_serial_no)
@spec storage() :: WeChat.Storage.Adapter.t()
def storage, do: unquote(storage)
@spec client_cert() :: WeChat.Pay.client_cert()
def client_cert, do: unquote(options.client_cert)
@spec client_key() :: WeChat.Pay.client_key()
def client_key, do: unquote(options.client_key)
@doc false
def public_key, do: unquote(public_key)
# def private_key, do: unquote(private_key)
@doc false
def private_key, do: unquote(private_key)

@doc "加密敏感信息"
def encrypt_secret_data(data) do
WeChat.Pay.Crypto.encrypt_secret_data(data, unquote(public_key))
end

@doc "解密敏感信息"
def decrypt_secret_data(cipher_text) do
WeChat.Pay.Crypto.decrypt_secret_data(cipher_text, unquote(private_key))
end
end
end

defp check_options!(options, client) do
unless Keyword.get(options, :mch_id) |> is_binary() do
raise ArgumentError, "please set mch_id option for #{inspect(client)}"
end

unless Keyword.get(options, :client_serial_no) |> is_binary() do
raise ArgumentError, "please set client_serial_no option for #{inspect(client)}"
end

api_secret_key = Keyword.get(options, :api_secret_key)

unless is_binary(api_secret_key) or Utils.check_env_option?(api_secret_key) do
raise ArgumentError, "please set api_secret_key option for #{inspect(client)}"
end

unless Keyword.get(options, :client_cert) |> check_pem_file?() do
raise ArgumentError, "please set client_cert option for #{inspect(client)}"
end

unless Keyword.get(options, :client_key) |> check_pem_file?() do
raise ArgumentError, "please set client_key option for #{inspect(client)}"
end
end

defp check_pem_file?({:app_dir, app, path}) when is_atom(app) and is_binary(path),
do: Application.app_dir(app, path) |> File.exists?()

defp check_pem_file?({:file, path}) when is_binary(path), do: File.exists?(path)
defp check_pem_file?(_), do: false
end
6 changes: 6 additions & 0 deletions lib/wechat/builder/utils.ex
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,12 @@ defmodule WeChat.Builder.Utils do

defp ast_transform(ast, _acc), do: ast

def check_env_option?(:runtime_env), do: true
def check_env_option?({:runtime_env, app}) when is_atom(app), do: true
def check_env_option?(:compile_env), do: true
def check_env_option?({:compile_env, app}) when is_atom(app), do: true
def check_env_option?(_), do: false

def handle_env_option(_client, key, :runtime_env) do
quote do
def unquote(key)(),
Expand Down
2 changes: 1 addition & 1 deletion lib/wechat/mini_program/code.ex
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ defmodule WeChat.MiniProgram.Code do
)
end

@spec download(file_path :: Path.t(), create_fun :: (() -> WeChat.response())) ::
@spec download(file_path :: Path.t(), create_fun :: (-> WeChat.response())) ::
WeChat.response() | :ok | {:error, File.posix()}
def download(file_path, create_fun) do
file_path
Expand Down
4 changes: 3 additions & 1 deletion lib/wechat/official_account/card_distributing.ex
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ defmodule WeChat.CardDistributing do
"""
@spec create_landing_page(WeChat.client(), body :: map) :: WeChat.response()
def create_landing_page(client, body) do
client.post("/card/landingpage/create", body, query: [access_token: client.get_access_token()])
client.post("/card/landingpage/create", body,
query: [access_token: client.get_access_token()]
)
end

@doc """
Expand Down
23 changes: 0 additions & 23 deletions lib/wechat/pay/authorization.ex

This file was deleted.

31 changes: 31 additions & 0 deletions lib/wechat/pay/authorization_middleware.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
defmodule WeChat.Pay.AuthorizationMiddleware do
import WeChat.Utils, only: [pay_doc_link_prefix: 0]

@moduledoc """
微信支付 V3 Authorization 签名生成
Tesla Middleware
- [如何生成请求签名](#{pay_doc_link_prefix()}/merchant/development/interface-rules/signature-generation.html){:target="_blank"}
- [签名相关问题](#{pay_doc_link_prefix()}/merchant/development/interface-rules/signature-faqs.html){:target="_blank"}
"""
@behaviour Tesla.Middleware
alias WeChat.Pay.Crypto

@impl Tesla.Middleware
def call(env, next, client) do
token = gen_token(client.mch_id(), client.client_serial_no(), client.private_key(), env)

env
|> Tesla.put_headers([{"authorization", "WECHATPAY2-SHA256-RSA2048 #{token}"}])
|> Tesla.run(next)
end

def gen_token(mch_id, serial_no, private_key, env) do
timestamp = WeChat.Utils.now_unix()
nonce_str = :crypto.strong_rand_bytes(16) |> Base.encode16()
signature = Crypto.sign(env, timestamp, nonce_str, private_key)

~s(mchid="#{mch_id}",nonce_str="#{nonce_str}",timestamp="#{timestamp}",serial_no="#{serial_no}",signature="#{signature}")
end
end
22 changes: 22 additions & 0 deletions lib/wechat/pay/bill.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
defmodule WeChat.Pay.Bill do
@moduledoc "微信支付-交易账单"
import Jason.Helpers
import WeChat.Utils, only: [pay_doc_link_prefix: 0]

@doc """
申请交易账单 -
[官方文档](#{pay_doc_link_prefix()}/merchant/apis/bill-download/trade-bill/get-trade-bill.html){:target="_blank"}
"""
def tradebill(client, bill_date, bill_type \\ "ALL", zip? \\ false)

def tradebill(client, bill_date, bill_type, false) do
client.post(
"/v3/bill/tradebill",
json_map(bill_date: bill_date, bill_type: bill_type, tar_type: "GZIP")
)
end

def tradebill(client, bill_date, bill_type, true) do
client.post("/v3/bill/tradebill", json_map(bill_date: bill_date, bill_type: bill_type))
end
end
Loading

0 comments on commit 123a295

Please sign in to comment.