alipay-easysdk-ruby 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +213 -0
- data/Rakefile +4 -0
- data/examples/demo.rb +134 -0
- data/examples/demo_with_post.rb +239 -0
- data/lib/alipay/easysdk/kernel/alipay_constants.rb +46 -0
- data/lib/alipay/easysdk/kernel/config.rb +38 -0
- data/lib/alipay/easysdk/kernel/easy_sdk_kernel.rb +328 -0
- data/lib/alipay/easysdk/kernel/factory.rb +95 -0
- data/lib/alipay/easysdk/kernel/util/json_util.rb +30 -0
- data/lib/alipay/easysdk/kernel/util/response_checker.rb +28 -0
- data/lib/alipay/easysdk/kernel/util/sign_content_extractor.rb +50 -0
- data/lib/alipay/easysdk/kernel/util/signer.rb +125 -0
- data/lib/alipay/easysdk/payment/common/client.rb +190 -0
- data/lib/alipay/easysdk/payment/common/models/alipay_data_dataservice_bill_downloadurl_query_response.rb +13 -0
- data/lib/alipay/easysdk/payment/common/models/alipay_trade_cancel_response.rb +13 -0
- data/lib/alipay/easysdk/payment/common/models/alipay_trade_close_response.rb +13 -0
- data/lib/alipay/easysdk/payment/common/models/alipay_trade_create_response.rb +13 -0
- data/lib/alipay/easysdk/payment/common/models/alipay_trade_fastpay_refund_query_response.rb +13 -0
- data/lib/alipay/easysdk/payment/common/models/alipay_trade_query_response.rb +13 -0
- data/lib/alipay/easysdk/payment/common/models/alipay_trade_refund_response.rb +13 -0
- data/lib/alipay/easysdk/payment/common/models/base_response.rb +42 -0
- data/lib/alipay/easysdk/payment/page/client.rb +80 -0
- data/lib/alipay/easysdk/payment/page/models/alipay_trade_page_pay_response.rb +35 -0
- data/lib/alipay/easysdk/payment/wap/client.rb +88 -0
- data/lib/alipay/easysdk/payment/wap/models/alipay_trade_wap_pay_response.rb +35 -0
- data/lib/alipay/easysdk/version.rb +5 -0
- data/lib/alipay/easysdk.rb +33 -0
- data/sig/alipay/easysdk/ruby.rbs +8 -0
- metadata +130 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 950be18124104ec0c4c69f4ac301691d6b588a4d6ed07c2e7f1723f4aa8a149e
|
|
4
|
+
data.tar.gz: 05d57395bbad8ef967c48d7b6e761a70234426d0c8365bb2a887bb67bb98668e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 752c207bf37bb7a3449f70f26e0f458673b466c50e16a36c245968a383a470f5c370946690c60c5ba234e8433dbe63db32dd5f7b5988381c644b6b8cec7a9d39
|
|
7
|
+
data.tar.gz: 67f873284da478d30ee7a5eda21034f56a967c7092b6c7af3d04a7fb80c365df88f8c3b5346455a6921698466503d1b87b913dd7f2468e9875f8bdbacf82648a
|
data/README.md
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# Alipay EasySDK Ruby
|
|
2
|
+
|
|
3
|
+
支付宝开放平台Alipay EasySDK的Ruby版本实现,提供完整的支付宝支付功能支持。
|
|
4
|
+
|
|
5
|
+
## 🚀 特性
|
|
6
|
+
|
|
7
|
+
- ✅ **完整的支付功能** - 支持当面付、手机网站支付、APP支付、页面支付、花呗分期
|
|
8
|
+
- ✅ **简化API调用** - 提供链式调用支持,代码简洁优雅
|
|
9
|
+
- ✅ **双重签名模式** - 支持证书模式和公钥模式
|
|
10
|
+
- ✅ **异步通知验签** - 内置验签功能,安全可靠
|
|
11
|
+
- ✅ **AES加密解密** - 支持敏感数据加密传输
|
|
12
|
+
- ✅ **标准Gem包** - 遵循Ruby Gem最佳实践
|
|
13
|
+
- ✅ **完整测试覆盖** - 提供全面的单元测试
|
|
14
|
+
|
|
15
|
+
## 📦 安装
|
|
16
|
+
|
|
17
|
+
将以下代码添加到你的Gemfile中:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
gem 'alipay-easysdk-ruby'
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
然后执行:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
$ bundle install
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
或者直接安装:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
$ gem install alipay-easysdk-ruby
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## 🔧 使用
|
|
36
|
+
|
|
37
|
+
### 1. 基本配置
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
require 'alipay/easysdk'
|
|
41
|
+
|
|
42
|
+
# 全局配置(只需设置一次)
|
|
43
|
+
Alipay::EasySDK.configure(
|
|
44
|
+
protocol: 'https',
|
|
45
|
+
gateway_host: 'openapi.alipay.com', # 正式环境
|
|
46
|
+
# gateway_host: 'openapi-sandbox.dl.alipaydev.com', # 沙盒环境
|
|
47
|
+
app_id: 'your-app-id',
|
|
48
|
+
merchant_private_key: 'your-private-key',
|
|
49
|
+
alipay_public_key: 'your-alipay-public-key'
|
|
50
|
+
# encrypt_key: 'your-aes-key' # AES密钥(可选)
|
|
51
|
+
)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 2. 手机网站支付
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
# 创建手机网站支付订单
|
|
58
|
+
response = Alipay::EasySDK.wap.pay(
|
|
59
|
+
"商品名称", # subject
|
|
60
|
+
"20231001001", # out_trade_no
|
|
61
|
+
"99.00", # total_amount
|
|
62
|
+
"https://quit.example.com", # quit_url
|
|
63
|
+
"https://return.example.com" # return_url
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if response.success?
|
|
67
|
+
puts "支付表单HTML: #{response.form}"
|
|
68
|
+
# 可以直接输出response.form到网页中
|
|
69
|
+
else
|
|
70
|
+
puts "调用失败: #{response.error_message}"
|
|
71
|
+
end
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 3. 当面付
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
# 创建二维码支付
|
|
78
|
+
response = Alipay::EasySDK.facetoface.pre_create(
|
|
79
|
+
"Apple iPhone11 128G",
|
|
80
|
+
"2234567890",
|
|
81
|
+
"5799.00"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if response.success?
|
|
85
|
+
puts "二维码内容: #{response.qr_code}"
|
|
86
|
+
end
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 4. 查询订单
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
# 查询订单状态
|
|
93
|
+
response = Alipay::EasySDK.common.query("20231001001")
|
|
94
|
+
|
|
95
|
+
if response.success?
|
|
96
|
+
puts "订单状态: #{response.trade_status}"
|
|
97
|
+
puts "支付金额: #{response.total_amount}"
|
|
98
|
+
end
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### 5. 退款
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
# 发起退款
|
|
105
|
+
response = Alipay::EasySDK.common.refund(
|
|
106
|
+
"20231001001", # out_trade_no
|
|
107
|
+
"10.00", # refund_amount
|
|
108
|
+
"商品质量问题" # refund_reason
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if response.success?
|
|
112
|
+
puts "退款成功,退款金额: #{response.refund_fee}"
|
|
113
|
+
end
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### 6. 链式调用
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
# ISV代商户调用,支持链式调用
|
|
120
|
+
response = Alipay::EasySDK.facetoface
|
|
121
|
+
.agent("app_auth_token")
|
|
122
|
+
.async_notify("https://your-callback-url.com")
|
|
123
|
+
.optional("timeout_express", "30m")
|
|
124
|
+
.optional("goods_detail", [
|
|
125
|
+
{
|
|
126
|
+
"goods_id" => "1001",
|
|
127
|
+
"goods_name" => "iPhone 15",
|
|
128
|
+
"quantity" => 1,
|
|
129
|
+
"price" => "7999.00"
|
|
130
|
+
}
|
|
131
|
+
])
|
|
132
|
+
.pre_create("Apple iPhone15", "20231001006", "7999.00")
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### 7. 异步通知验签
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
# 在异步通知回调中验签
|
|
139
|
+
def alipay_notify_callback(params)
|
|
140
|
+
if Alipay::EasySDK.common.verify_notify(params)
|
|
141
|
+
# 验签成功,处理业务逻辑
|
|
142
|
+
puts "验签成功,订单号: #{params['out_trade_no']}"
|
|
143
|
+
# 处理订单更新等业务逻辑
|
|
144
|
+
render text: "success"
|
|
145
|
+
else
|
|
146
|
+
# 验签失败
|
|
147
|
+
puts "验签失败"
|
|
148
|
+
render text: "fail"
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## 📋 API支持
|
|
154
|
+
|
|
155
|
+
### 通用支付 (Payment::Common)
|
|
156
|
+
- ✅ `query()` - 查询订单
|
|
157
|
+
- ✅ `refund()` - 申请退款
|
|
158
|
+
- ✅ `close()` - 关闭订单
|
|
159
|
+
- ✅ `cancel()` - 撤销订单
|
|
160
|
+
- ✅ `query_refund()` - 查询退款
|
|
161
|
+
- ✅ `download_bill()` - 下载对账单
|
|
162
|
+
- ✅ `verify_notify()` - 异步通知验签
|
|
163
|
+
|
|
164
|
+
### 当面付 (Payment::FaceToFace)
|
|
165
|
+
- ✅ `pay()` - 收银员扫码收款
|
|
166
|
+
- ✅ `pre_create()` - 生成支付二维码
|
|
167
|
+
|
|
168
|
+
### 手机网站支付 (Payment::Wap)
|
|
169
|
+
- ✅ `pay()` - 创建手机网站支付
|
|
170
|
+
|
|
171
|
+
### 电脑网站支付 (Payment::Page)
|
|
172
|
+
- ✅ `pay()` - 创建电脑网站支付
|
|
173
|
+
|
|
174
|
+
### APP支付 (Payment::App)
|
|
175
|
+
- ✅ `pay()` - 创建APP支付
|
|
176
|
+
|
|
177
|
+
### 花呗分期 (Payment::Huabei)
|
|
178
|
+
- ✅ `create()` - 花呗分期支付
|
|
179
|
+
- ✅ `huabei_config()` - 花呗分期配置
|
|
180
|
+
|
|
181
|
+
## 🧪 运行测试
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
$ bundle exec rspec
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## 📝 示例
|
|
188
|
+
|
|
189
|
+
查看 `examples/` 目录中的完整示例代码。
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
$ ruby examples/demo.rb
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## 🔗 相关链接
|
|
196
|
+
|
|
197
|
+
- [支付宝开放平台](https://open.alipay.com)
|
|
198
|
+
- [Alipay EasySDK文档](https://github.com/alipay/alipay-easysdk)
|
|
199
|
+
- [API文档](https://opendocs.alipay.com/open)
|
|
200
|
+
|
|
201
|
+
## 📄 许可证
|
|
202
|
+
|
|
203
|
+
MIT License - 详见 [LICENSE](LICENSE) 文件
|
|
204
|
+
|
|
205
|
+
## 🤝 贡献
|
|
206
|
+
|
|
207
|
+
欢迎提交 Issue 和 Pull Request!
|
|
208
|
+
|
|
209
|
+
1. Fork 本项目
|
|
210
|
+
2. 创建你的特性分支 (`git checkout -b my-new-feature`)
|
|
211
|
+
3. 提交你的更改 (`git commit -am 'Add some feature'`)
|
|
212
|
+
4. 推送到分支 (`git push origin my-new-feature`)
|
|
213
|
+
5. 创建一个 Pull Request
|
data/Rakefile
ADDED
data/examples/demo.rb
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require 'alipay/easysdk'
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'uri'
|
|
6
|
+
require 'cgi'
|
|
7
|
+
require 'openssl'
|
|
8
|
+
|
|
9
|
+
# ------------------ 辅助方法(完全对应 PHP 版逻辑) ------------------
|
|
10
|
+
|
|
11
|
+
def get_options
|
|
12
|
+
Alipay::EasySDK::Kernel::Config.new(
|
|
13
|
+
protocol: 'https',
|
|
14
|
+
gateway_host: 'openapi-sandbox.dl.alipaydev.com',
|
|
15
|
+
sign_type: 'RSA2',
|
|
16
|
+
app_id: '9021000156667919',
|
|
17
|
+
merchant_private_key: 'MIIEpAIBAAKCAQEAvj1if+DJ6Q+D5m72q2cT2nFiYjNREM4l4gvr1w/DuNEs6+1d4j6oCxHstIb718TIqF1FWfJc3K0RQgBOOsxkcXvyvPIhwumEN6sXLMQiEuq5mDKvaDcoIQJ0FC6784LzhORzy1IAGNFCZpeg4DfgukOM9x8v+Q8/NGbcvyVWbPRWoQq52DPSLFMK1Q2ZYpMQMFMix8g3Gf4BJLB9Ya9n5aVcxrFWmjstSEqn5H+lCxBzwWkTsFBOK5itashHvNCTKy7An0U2xh4slh8wiYL8QwD5Qz7S++UkU+vY2HhY81qOTBQbrjapx7qy03C7Sy8cOeRMeXdn64y0xD+E1vTL6QIDAQABAoIBACx5+X9gNJRydin3o1/rV27ot1GyIa/GIoE4vEipfN7GuLPn6N0uPOdpp2eFb3fAoBEMzVv8F83YAILnw2JnysvlaJjYGyCQq8LAE0j6CeVWT1HP98ZrrswY4L6fNn32DazyJEhSwYcL1XRa2tfQ+I9Tn69e8T5PXD2KFu3xcsVB615ipPQ5TEToi2fJ31apir1AYKfDK4dd/0JwK6HUhPXuOf5GZ4Vdq6zkZYSLV8rJA+5MsQYPA3TwszEIrOe1YeQelvNmB2Rl2oCV6rigIPtfnaEFi9GfaKfGiE4r2bBb24NliS6ruflel92jeiJ0+mAT8GE7zVXnX5Sx8BVYHqECgYEA64haWxKv5Q/ujkSbfRRM+CnD1JWdQdPEdh/IxG9fsdgCS72YPypWkSwikAObOrijQf2GTp1bne9Zk9hueQ2tVOrUG9XxEELlJNqkXCufCgrABTEvSUJV+qAZgyalFDe00Z+nJPHZtRmWFjnFLDCJtEnYsVFcQ6v/KvmVO7bMOj0CgYEAzsVzEBiEN/4jL7w7lxnDNp8VznG6W6bnuu4xGlIcziwPIpqeOWzMf90zHRvsh6AsDfzzMDoEvHdj44JQN83xQm/L+Q/R8s+XSB4d6UavfVbApxHGE1iINS2Au0R8SfnPCpQl6WY8IWftL/YCccQUgBRuG9sDI69iaCP+g661Lx0CgYAqkdFq5ny+FNwUAJhtye6DZ+EKGiR7ElBO3T3HKy4LkbQQhmru97L/uA9jIhO7UEXJlo3gxZYafHkfPJ3y6SLr1ymRAmD4hG6v84iDVCsBgKHmDlaykffCPY9+4cwyVEMtJALsrX2gusgiqjxV2Uv6NuKgYckgPgT3enabfVV6LQKBgQCXcqPejDZ71JbtJc/30pTbcxZDyaUX8F4W2tP4VWBn2nmTfPCbWwdGODxx+7v5cuYRsM5m6ngBmuj9ALvExAEMClq6KE48rLQ/zF9YN7/d7Cbbt/b+wH+zg4qgn37xqBlvxCcolws/5KEj2ercbSQe09f6ayYXgyRu5r5KsTJgOQKBgQDcyFC5taNAfGHU5b1k5TLPdTPSEQiBd3//uCH2wXSnj7K8mMLEigS0GIlNlQPjn1h9Hoed9me4zJiRLAKdMTEZQ0ZtevELYFkkC+D9IHpLdKvIFycGpKR/DpZBSTr2LETn1WeZc3LR3ijDixpxH4PqUivkSZl/vnsTSPZsNgCsMg==',
|
|
18
|
+
alipay_public_key: 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvj1if+DJ6Q+D5m72q2cT2nFiYjNREM4l4gvr1w/DuNEs6+1d4j6oCxHstIb718TIqF1FWfJc3K0RQgBOOsxkcXvyvPIhwumEN6sXLMQiEuq5mDKvaDcoIQJ0FC6784LzhORzy1IAGNFCZpeg4DfgukOM9x8v+Q8/NGbcvyVWbPRWoQq52DPSLFMK1Q2ZYpMQMFMix8g3Gf4BJLB9Ya9n5aVcxrFWmjstSEqn5H+lCxBzwWkTsFBOK5itashHvNCTKy7An0U2xh4slh8wiYL8QwD5Qz7S++UkU+vY2HhY81qOTBQbrjapx7qy03C7Sy8cOeRMeXdn64y0xD+E1vTL6QIDAQAB',
|
|
19
|
+
encrypt_key: 'rEoolKE9DfJIQHMMelZapw=='
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def extract_action_url(form_html)
|
|
24
|
+
match = form_html.match(/action=['"](.*?)['"]/)
|
|
25
|
+
return nil unless match
|
|
26
|
+
|
|
27
|
+
url = match[1]
|
|
28
|
+
begin
|
|
29
|
+
parsed_uri = URI.parse(url)
|
|
30
|
+
"#{parsed_uri.scheme}://#{parsed_uri.host}#{parsed_uri.path}"
|
|
31
|
+
rescue URI::InvalidURIError
|
|
32
|
+
url
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def extract_all_form_params(form_html)
|
|
37
|
+
params = {}
|
|
38
|
+
form_html.scan(/<input[^>]*name=['"](.*?)['"][^>]*value=['"](.*?)['"][^>]*>/i) do |name, value|
|
|
39
|
+
params[name] = CGI.unescapeHTML(value)
|
|
40
|
+
end
|
|
41
|
+
params
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def send_post_request(url_string, params)
|
|
45
|
+
uri = URI.parse(url_string)
|
|
46
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
47
|
+
http.use_ssl = uri.scheme == 'https'
|
|
48
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE if uri.scheme == 'https'
|
|
49
|
+
|
|
50
|
+
post_data = params.map { |key, value| "#{CGI.escape(key)}=#{CGI.escape(value)}" }.join('&')
|
|
51
|
+
|
|
52
|
+
puts "发送POST请求到: #{url_string}"
|
|
53
|
+
puts "POST数据: #{post_data}"
|
|
54
|
+
|
|
55
|
+
headers = {
|
|
56
|
+
'Content-Type' => 'application/x-www-form-urlencoded; charset=UTF-8',
|
|
57
|
+
'Accept' => '*/*',
|
|
58
|
+
'Accept-Charset' => 'UTF-8',
|
|
59
|
+
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
request = Net::HTTP::Post.new(uri.request_uri, headers)
|
|
63
|
+
request.body = post_data
|
|
64
|
+
|
|
65
|
+
response = http.start { |client| client.request(request) }
|
|
66
|
+
|
|
67
|
+
if response.code.to_i >= 300 && response.code.to_i < 400
|
|
68
|
+
location = response['location']
|
|
69
|
+
if location
|
|
70
|
+
puts "检测到重定向到: #{location}"
|
|
71
|
+
return send_post_request(location, params)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
puts "HTTP响应码: #{response.code}"
|
|
76
|
+
puts "响应长度: #{response.body.length} 字节"
|
|
77
|
+
|
|
78
|
+
unless response.code.to_i == 200
|
|
79
|
+
puts "响应内容预览: #{response.body[0, 500]}..."
|
|
80
|
+
raise "HTTP请求失败,响应码: #{response.code}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
response.body
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def save_response_to_file(content, filename)
|
|
87
|
+
File.write(filename, content)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# ------------------ 主流程 ------------------
|
|
91
|
+
|
|
92
|
+
begin
|
|
93
|
+
Alipay::EasySDK::Kernel::Factory.set_options(get_options)
|
|
94
|
+
|
|
95
|
+
response = Alipay::EasySDK::Kernel::Factory.payment
|
|
96
|
+
.wap
|
|
97
|
+
.optional('seller_id', '2088102147948060')
|
|
98
|
+
.pay(
|
|
99
|
+
'1123',
|
|
100
|
+
'70501111111S001111119',
|
|
101
|
+
'9.00',
|
|
102
|
+
'https://your-quit-url.com',
|
|
103
|
+
'https://your-return-url.com'
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
response_checker = Alipay::EasySDK::Kernel::Util::ResponseChecker.new
|
|
107
|
+
|
|
108
|
+
if response_checker.success(response)
|
|
109
|
+
puts '调用成功'
|
|
110
|
+
puts "支付表单HTML: #{response.body}"
|
|
111
|
+
|
|
112
|
+
form_html = response.body
|
|
113
|
+
action_url = extract_action_url(form_html)
|
|
114
|
+
all_params = extract_all_form_params(form_html)
|
|
115
|
+
|
|
116
|
+
puts "Action URL: #{action_url}"
|
|
117
|
+
puts "表单参数数量: #{all_params.size}"
|
|
118
|
+
all_params.each do |key, value|
|
|
119
|
+
puts " #{key}: #{value}"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
if action_url && !all_params.empty?
|
|
123
|
+
post_response = send_post_request(action_url, all_params)
|
|
124
|
+
save_response_to_file(post_response, 'alipay_ruby_response.txt')
|
|
125
|
+
puts '响应已保存到 alipay_ruby_response.txt'
|
|
126
|
+
end
|
|
127
|
+
else
|
|
128
|
+
puts "调用失败,原因:#{response.error_message}"
|
|
129
|
+
end
|
|
130
|
+
rescue => e
|
|
131
|
+
puts "调用遭遇异常,原因:#{e.message}"
|
|
132
|
+
puts e.backtrace.join("\n") if ENV['DEBUG']
|
|
133
|
+
raise e
|
|
134
|
+
end
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require 'alipay/easysdk'
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'uri'
|
|
6
|
+
require 'json'
|
|
7
|
+
require 'cgi'
|
|
8
|
+
|
|
9
|
+
# 如果没有nokogiri,使用简单的正则表达式解析
|
|
10
|
+
begin
|
|
11
|
+
require 'nokogiri'
|
|
12
|
+
rescue LoadError
|
|
13
|
+
# Nokogiri not available, will use regex fallback
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# 从HTML表单中提取action URL - 按照PHP版本逻辑
|
|
17
|
+
def extract_action_url(form_html)
|
|
18
|
+
# 使用正则表达式提取action URL
|
|
19
|
+
if match = form_html.match(/action=['"](.*?)['"]/)
|
|
20
|
+
url = match[1]
|
|
21
|
+
# 移除URL中的查询参数,因为不应该在URL中包含参数
|
|
22
|
+
begin
|
|
23
|
+
parsed_uri = URI.parse(url)
|
|
24
|
+
"#{parsed_uri.scheme}://#{parsed_uri.host}#{parsed_uri.path}"
|
|
25
|
+
rescue
|
|
26
|
+
url
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# 从HTML表单中提取所有input参数 - 按照PHP版本逻辑
|
|
32
|
+
def extract_all_form_params(form_html)
|
|
33
|
+
params = {}
|
|
34
|
+
|
|
35
|
+
# 尝试使用Nokogiri
|
|
36
|
+
begin
|
|
37
|
+
doc = Nokogiri::HTML(form_html)
|
|
38
|
+
doc.css('input').each do |input|
|
|
39
|
+
name = input['name']
|
|
40
|
+
value = input['value']
|
|
41
|
+
|
|
42
|
+
if name && !name.empty?
|
|
43
|
+
# HTML解码
|
|
44
|
+
decoded_value = CGI.unescapeHTML(value)
|
|
45
|
+
params[name] = decoded_value
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
rescue
|
|
49
|
+
# Nokogiri不可用时,使用正则表达式
|
|
50
|
+
form_html.scan(/<input[^>]*name=['"](.*?)['"][^>]*value=['"](.*?)['"][^>]*>/) do |match|
|
|
51
|
+
name = match[0]
|
|
52
|
+
value = match[1]
|
|
53
|
+
if name && !name.empty?
|
|
54
|
+
decoded_value = CGI.unescapeHTML(value)
|
|
55
|
+
params[name] = decoded_value
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
params
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# 从action URL中提取查询参数 - Ruby版本特有
|
|
64
|
+
def extract_url_params(action_url_with_params)
|
|
65
|
+
params = {}
|
|
66
|
+
|
|
67
|
+
begin
|
|
68
|
+
parsed_uri = URI.parse(action_url_with_params)
|
|
69
|
+
if parsed_uri.query
|
|
70
|
+
# 解析查询参数
|
|
71
|
+
query_params = CGI.parse(parsed_uri.query)
|
|
72
|
+
query_params.each do |key, values|
|
|
73
|
+
# CGI.parse返回数组,我们取第一个值
|
|
74
|
+
params[key] = values[0] unless values.empty?
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
rescue => e
|
|
78
|
+
puts "解析URL参数时出错: #{e.message}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
params
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# 发送POST请求 - 按照PHP版本逻辑
|
|
85
|
+
def send_post_request(url_string, params)
|
|
86
|
+
puts "调试:发送POST请求到URL = #{url_string}"
|
|
87
|
+
uri = URI.parse(url_string)
|
|
88
|
+
puts "调试:URI解析结果 scheme=#{uri.scheme}, host=#{uri.host}, port=#{uri.port}"
|
|
89
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
90
|
+
|
|
91
|
+
# 正确处理HTTPS
|
|
92
|
+
if uri.scheme == 'https'
|
|
93
|
+
puts "调试:启用HTTPS"
|
|
94
|
+
http.use_ssl = true
|
|
95
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
|
96
|
+
else
|
|
97
|
+
puts "调试:使用HTTP(scheme=#{uri.scheme})"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# 构建POST数据,包含所有参数(包括sign) - 按字母顺序排列(与签名生成时一致)
|
|
101
|
+
# 注意:sign参数需要包含在POST请求中,但不参与签名排序
|
|
102
|
+
post_params = params.dup
|
|
103
|
+
sign_value = post_params.delete('sign') # 临时移除sign参数
|
|
104
|
+
|
|
105
|
+
# 对其他参数按字母排序
|
|
106
|
+
sorted_params = Hash[post_params.sort_by { |key, _| key }]
|
|
107
|
+
|
|
108
|
+
# 构建POST数据(先排序的参数,最后加上sign)
|
|
109
|
+
post_data_parts = sorted_params.map { |key, value|
|
|
110
|
+
"#{URI.encode_www_form_component(key)}=#{URI.encode_www_form_component(value)}"
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# 如果有sign参数,添加到最后
|
|
114
|
+
if sign_value
|
|
115
|
+
post_data_parts << "#{URI.encode_www_form_component('sign')}=#{URI.encode_www_form_component(sign_value)}"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
post_data = post_data_parts.join('&')
|
|
119
|
+
|
|
120
|
+
headers = {
|
|
121
|
+
'Content-Type' => 'application/x-www-form-urlencoded; charset=UTF-8',
|
|
122
|
+
'Accept' => '*/*',
|
|
123
|
+
'Accept-Charset' => 'UTF-8',
|
|
124
|
+
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
puts "发送POST请求到: #{url_string}"
|
|
128
|
+
puts "POST数据: #{post_data}"
|
|
129
|
+
|
|
130
|
+
request = Net::HTTP::Post.new(uri.request_uri, headers)
|
|
131
|
+
request.body = post_data
|
|
132
|
+
|
|
133
|
+
# 跟随重定向(按照PHP版本的curl设置)
|
|
134
|
+
http.use_ssl = true if uri.scheme == 'https'
|
|
135
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE if uri.scheme == 'https'
|
|
136
|
+
|
|
137
|
+
# 启用自动重定向跟随
|
|
138
|
+
http.start do |http|
|
|
139
|
+
response = http.request(request)
|
|
140
|
+
|
|
141
|
+
puts "HTTP响应码: #{response.code}"
|
|
142
|
+
puts "响应长度: #{response.body.length} 字节"
|
|
143
|
+
|
|
144
|
+
# 检查重定向并跟随(类似PHP版本的CURLOPT_FOLLOWLOCATION)
|
|
145
|
+
if response.code.to_i >= 300 && response.code.to_i < 400
|
|
146
|
+
location = response['location']
|
|
147
|
+
if location
|
|
148
|
+
puts "检测到重定向到: #{location}"
|
|
149
|
+
# 如果是重定向,递归调用
|
|
150
|
+
return send_post_request(location, params)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
if response.code.to_i != 200
|
|
155
|
+
puts "HTTP响应头信息:"
|
|
156
|
+
response.each_header do |key, value|
|
|
157
|
+
if key.start_with?('http') || key.downcase == 'location'
|
|
158
|
+
puts " #{key}: #{value}"
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
puts "响应内容预览:"
|
|
162
|
+
puts response.body[0, 500] + "..." if response.body.length > 500
|
|
163
|
+
raise "HTTP请求失败,响应码: #{response.code}"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
return response.body
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# 将响应保存到文件
|
|
171
|
+
def save_response_to_file(content, filename)
|
|
172
|
+
File.write(filename, content)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
begin
|
|
176
|
+
# 1. 设置配置(全局只需设置一次)- 使用与PHP版本完全相同的配置
|
|
177
|
+
Alipay::EasySDK.configure(
|
|
178
|
+
protocol: 'https',
|
|
179
|
+
gateway_host: 'openapi-sandbox.dl.alipaydev.com',
|
|
180
|
+
sign_type: 'RSA2',
|
|
181
|
+
app_id: '9021000156667919',
|
|
182
|
+
merchant_private_key: 'MIIEpAIBAAKCAQEAvj1if+DJ6Q+D5m72q2cT2nFiYjNREM4l4gvr1w/DuNEs6+1d4j6oCxHstIb718TIqF1FWfJc3K0RQgBOOsxkcXvyvPIhwumEN6sXLMQiEuq5mDKvaDcoIQJ0FC6784LzhORzy1IAGNFCZpeg4DfgukOM9x8v+Q8/NGbcvyVWbPRWoQq52DPSLFMK1Q2ZYpMQMFMix8g3Gf4BJLB9Ya9n5aVcxrFWmjstSEqn5H+lCxBzwWkTsFBOK5itashHvNCTKy7An0U2xh4slh8wiYL8QwD5Qz7S++UkU+vY2HhY81qOTBQbrjapx7qy03C7Sy8cOeRMeXdn64y0xD+E1vTL6QIDAQABAoIBACx5+X9gNJRydin3o1/rV27ot1GyIa/GIoE4vEipfN7GuLPn6N0uPOdpp2eFb3fAoBEMzVv8F83YAILnw2JnysvlaJjYGyCQq8LAE0j6CeVWT1HP98ZrrswY4L6fNn32DazyJEhSwYcL1XRa2tfQ+I9Tn69e8T5PXD2KFu3xcsVB615ipPQ5TEToi2fJ31apir1AYKfDK4dd/0JwK6HUhPXuOf5GZ4Vdq6zkZYSLV8rJA+5MsQYPA3TwszEIrOe1YeQelvNmB2Rl2oCV6rigIPtfnaEFi9GfaKfGiE4r2bBb24NliS6ruflel92jeiJ0+mAT8GE7zVXnX5Sx8BVYHqECgYEA64haWxKv5Q/ujkSbfRRM+CnD1JWdQdPEdh/IxG9fsdgCS72YPypWkSwikAObOrijQf2GTp1bne9Zk9hueQ2tVOrUG9XxEELlJNqkXCufCgrABTEvSUJV+qAZgyalFDe00Z+nJPHZtRmWFjnFLDCJtEnYsVFcQ6v/KvmVO7bMOj0CgYEAzsVzEBiEN/4jL7w7lxnDNp8VznG6W6bnuu4xGlIcziwPIpqeOWzMf90zHRvsh6AsDfzzMDoEvHdj44JQN83xQm/L+Q/R8s+XSB4d6UavfVbApxHGE1iINS2Au0R8SfnPCpQl6WY8IWftL/YCccQUgBRuG9sDI69iaCP+g661Lx0CgYAqkdFq5ny+FNwUAJhtye6DZ+EKGiR7ElBO3T3HKy4LkbQQhmru97L/uA9jIhO7UEXJlo3gxZYafHkfPJ3y6SLr1ymRAmD4hG6v84iDVCsBgKHmDlaykffCPY9+4cwyVEMtJALsrX2gusgiqjxV2Uv6NuKgYckgPgT3enabfVV6LQKBgQCXcqPejDZ71JbtJc/30pTbcxZDyaUX8F4W2tP4VWBn2nmTfPCbWwdGODxx+7v5cuYRsM5m6ngBmuj9ALvExAEMClq6KE48rLQ/zF9YN7/d7Cbbt/b+wH+zg4qgn37xqBlvxCcolws/5KEj2ercbSQe09f6ayYXgyRu5r5KsTJgOQKBgQDcyFC5taNAfGHU5b1k5TLPdTPSEQiBd3//uCH2wXSnj7K8mMLEigS0GIlNlQPjn1h9Hoed9me4zJiRLAKdMTEZQ0ZtevELYFkkC+D9IHpLdKvIFycGpKR/DpZBSTr2LETn1WeZc3LR3ijDixpxH4PqUivkSZl/vnsTSPZsNgCsMg==',
|
|
183
|
+
alipay_public_key: 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvj1if+DJ6Q+D5m72q2cT2nFiYjNREM4l4gvr1w/DuNEs6+1d4j6oCxHstIb718TIqF1FWfJc3K0RQgBOOsxkcXvyvPIhwumEN6sXLMQiEuq5mDKvaDcoIQJ0FC6784LzhORzy1IAGNFCZpeg4DfgukOM9x8v+Q8/NGbcvyVWbPRWoQq52DPSLFMK1Q2ZYpMQMFMix8g3Gf4BJLB9Ya9n5aVcxrFWmjstSEqn5H+lCxBzwWkTsFBOK5itashHvNCTKy7An0U2xh4slh8wiYL8QwD5Qz7S++UkU+vY2HhY81qOTBQbrjapx7qy03C7Sy8cOeRMeXdn64y0xD+E1vTL6QIDAQAB',
|
|
184
|
+
encrypt_key: 'rEoolKE9DfJIQHMMelZapw=='
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# 2. 发起WAP支付API调用 - 与PHP版本完全相同的参数
|
|
188
|
+
response = Alipay::EasySDK.wap
|
|
189
|
+
.optional("seller_id", "2088102147948060") # 可选:设置卖家支付宝用户ID
|
|
190
|
+
.pay(
|
|
191
|
+
"1123", # 商品名称 - 与PHP版本一致
|
|
192
|
+
"70501111111S001111119", # 商户订单号
|
|
193
|
+
"9.00", # 支付金额
|
|
194
|
+
"https://your-quit-url.com", # 用户付款中途退出返回商户网站的地址
|
|
195
|
+
"https://your-return-url.com" # 用户付款成功返回商户网站的地址
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# 3. 处理响应或异常
|
|
199
|
+
if response.success?
|
|
200
|
+
puts "调用成功"
|
|
201
|
+
puts "支付表单HTML: #{response.form}" if response.form
|
|
202
|
+
|
|
203
|
+
# 4. 提取form表单中的action URL和所有参数 - 完全按照PHP版本逻辑
|
|
204
|
+
form_html = response.form
|
|
205
|
+
action_url = extract_action_url(form_html)
|
|
206
|
+
all_params = extract_all_form_params(form_html)
|
|
207
|
+
|
|
208
|
+
puts "Action URL: #{action_url}"
|
|
209
|
+
puts "表单参数数量: #{all_params.length}"
|
|
210
|
+
all_params.each do |key, value|
|
|
211
|
+
puts " #{key}: #{value}"
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# 5. action URL已经在第4步提取完成,无需重复提取
|
|
215
|
+
|
|
216
|
+
# 6. 现在Ruby版本的表单结构与PHP一致,所有参数都在input字段中
|
|
217
|
+
# 直接使用提取的input参数
|
|
218
|
+
combined_params = all_params
|
|
219
|
+
|
|
220
|
+
puts "表单参数总数: #{combined_params.length}"
|
|
221
|
+
|
|
222
|
+
# 7. 发送POST请求 - 使用基础URL和所有参数
|
|
223
|
+
if action_url && !combined_params.empty?
|
|
224
|
+
post_response = send_post_request(action_url, combined_params)
|
|
225
|
+
|
|
226
|
+
# 8. 将响应保存到文件
|
|
227
|
+
save_response_to_file(post_response, "alipay_ruby_response.txt")
|
|
228
|
+
puts "响应已保存到 alipay_ruby_response.txt"
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
else
|
|
232
|
+
puts "调用失败,原因:#{response.error_message}"
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
rescue => e
|
|
236
|
+
puts "调用遭遇异常,原因:#{e.message}"
|
|
237
|
+
puts e.backtrace.join("\n") if ENV['DEBUG']
|
|
238
|
+
raise e
|
|
239
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
require_relative '../version'
|
|
2
|
+
|
|
3
|
+
module Alipay
|
|
4
|
+
module EasySDK
|
|
5
|
+
module Kernel
|
|
6
|
+
module AlipayConstants
|
|
7
|
+
DEFAULT_CHARSET = "UTF-8"
|
|
8
|
+
|
|
9
|
+
# 签名类型
|
|
10
|
+
SIGN_TYPE_RSA = "RSA"
|
|
11
|
+
SIGN_TYPE_RSA2 = "RSA2"
|
|
12
|
+
|
|
13
|
+
# 字段常量
|
|
14
|
+
APP_ID_FIELD = "app_id"
|
|
15
|
+
METHOD_FIELD = "method"
|
|
16
|
+
FORMAT_FIELD = "format"
|
|
17
|
+
TIMESTAMP_FIELD = "timestamp"
|
|
18
|
+
VERSION_FIELD = "version"
|
|
19
|
+
SIGN_TYPE_FIELD = "sign_type"
|
|
20
|
+
SIGN_FIELD = "sign"
|
|
21
|
+
BIZ_CONTENT_FIELD = "biz_content"
|
|
22
|
+
CHARSET_FIELD = "charset"
|
|
23
|
+
BODY_FIELD = "body"
|
|
24
|
+
ALIPAY_CERT_SN_FIELD = "alipay_cert_sn"
|
|
25
|
+
NOTIFY_URL_FIELD = "notify_url"
|
|
26
|
+
NOTIFY_URL_CONFIG_KEY = "notifyUrl"
|
|
27
|
+
PROTOCOL_CONFIG_KEY = "protocol"
|
|
28
|
+
HOST_CONFIG_KEY = "gatewayHost"
|
|
29
|
+
RESPONSE_SUFFIX = "_response"
|
|
30
|
+
ERROR_RESPONSE = "error_response"
|
|
31
|
+
|
|
32
|
+
# 默认值
|
|
33
|
+
DEFAULT_FORMAT = "json"
|
|
34
|
+
DEFAULT_VERSION = "1.0"
|
|
35
|
+
DEFAULT_SIGN_TYPE = SIGN_TYPE_RSA2
|
|
36
|
+
|
|
37
|
+
# 请求方式
|
|
38
|
+
GET = "GET"
|
|
39
|
+
POST = "POST"
|
|
40
|
+
|
|
41
|
+
# SDK信息
|
|
42
|
+
SDK_VERSION = "alipay-easysdk-ruby-#{Alipay::EasySDK::VERSION}"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
require_relative 'alipay_constants'
|
|
2
|
+
|
|
3
|
+
module Alipay
|
|
4
|
+
module EasySDK
|
|
5
|
+
module Kernel
|
|
6
|
+
class Config
|
|
7
|
+
attr_accessor :protocol, :gateway_host, :app_id, :merchant_private_key,
|
|
8
|
+
:alipay_public_key, :sign_type, :charset, :format, :version,
|
|
9
|
+
:merchant_cert_sn, :alipay_root_cert_sn, :notify_url
|
|
10
|
+
|
|
11
|
+
def initialize(options = {})
|
|
12
|
+
@protocol = options[:protocol] || 'https'
|
|
13
|
+
@gateway_host = options[:gateway_host] || 'openapi.alipay.com/gateway.do'
|
|
14
|
+
@app_id = options[:app_id]
|
|
15
|
+
@merchant_private_key = options[:merchant_private_key]
|
|
16
|
+
@alipay_public_key = options[:alipay_public_key]
|
|
17
|
+
@sign_type = options[:sign_type] || AlipayConstants::DEFAULT_SIGN_TYPE
|
|
18
|
+
@charset = options[:charset] || AlipayConstants::DEFAULT_CHARSET
|
|
19
|
+
@format = options[:format] || AlipayConstants::DEFAULT_FORMAT
|
|
20
|
+
@version = options[:version] || AlipayConstants::DEFAULT_VERSION
|
|
21
|
+
@merchant_cert_sn = options[:merchant_cert_sn]
|
|
22
|
+
@alipay_root_cert_sn = options[:alipay_root_cert_sn]
|
|
23
|
+
@notify_url = options[:notify_url]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def gateway_url
|
|
27
|
+
"#{@protocol}://#{@gateway_host}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def validate
|
|
31
|
+
raise "app_id is required" if @app_id.nil? || @app_id.empty?
|
|
32
|
+
raise "merchant_private_key is required" if @merchant_private_key.nil? || @merchant_private_key.empty?
|
|
33
|
+
raise "alipay_public_key is required" if @alipay_public_key.nil? || @alipay_public_key.empty?
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|