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.
Files changed (30) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +213 -0
  3. data/Rakefile +4 -0
  4. data/examples/demo.rb +134 -0
  5. data/examples/demo_with_post.rb +239 -0
  6. data/lib/alipay/easysdk/kernel/alipay_constants.rb +46 -0
  7. data/lib/alipay/easysdk/kernel/config.rb +38 -0
  8. data/lib/alipay/easysdk/kernel/easy_sdk_kernel.rb +328 -0
  9. data/lib/alipay/easysdk/kernel/factory.rb +95 -0
  10. data/lib/alipay/easysdk/kernel/util/json_util.rb +30 -0
  11. data/lib/alipay/easysdk/kernel/util/response_checker.rb +28 -0
  12. data/lib/alipay/easysdk/kernel/util/sign_content_extractor.rb +50 -0
  13. data/lib/alipay/easysdk/kernel/util/signer.rb +125 -0
  14. data/lib/alipay/easysdk/payment/common/client.rb +190 -0
  15. data/lib/alipay/easysdk/payment/common/models/alipay_data_dataservice_bill_downloadurl_query_response.rb +13 -0
  16. data/lib/alipay/easysdk/payment/common/models/alipay_trade_cancel_response.rb +13 -0
  17. data/lib/alipay/easysdk/payment/common/models/alipay_trade_close_response.rb +13 -0
  18. data/lib/alipay/easysdk/payment/common/models/alipay_trade_create_response.rb +13 -0
  19. data/lib/alipay/easysdk/payment/common/models/alipay_trade_fastpay_refund_query_response.rb +13 -0
  20. data/lib/alipay/easysdk/payment/common/models/alipay_trade_query_response.rb +13 -0
  21. data/lib/alipay/easysdk/payment/common/models/alipay_trade_refund_response.rb +13 -0
  22. data/lib/alipay/easysdk/payment/common/models/base_response.rb +42 -0
  23. data/lib/alipay/easysdk/payment/page/client.rb +80 -0
  24. data/lib/alipay/easysdk/payment/page/models/alipay_trade_page_pay_response.rb +35 -0
  25. data/lib/alipay/easysdk/payment/wap/client.rb +88 -0
  26. data/lib/alipay/easysdk/payment/wap/models/alipay_trade_wap_pay_response.rb +35 -0
  27. data/lib/alipay/easysdk/version.rb +5 -0
  28. data/lib/alipay/easysdk.rb +33 -0
  29. data/sig/alipay/easysdk/ruby.rbs +8 -0
  30. 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
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ task default: %i[]
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