alipay-easysdk-ruby 1.0.2 → 1.0.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e0448e76a0838f021e25e4181ca71d3f28586426a636eda913b93ba9ef221740
4
- data.tar.gz: de78cb3ee1ba2e4643e1ee6c7b804f38d5c99fff93b92b59ea5aaced4a9a2702
3
+ metadata.gz: 9fbf7bcd464b1d9f4c42aaec6030ba6dfc9a4beda7eb5c3ea2587722b2fdff01
4
+ data.tar.gz: 43c10156f010ca3f98696361f9d9a07478bd1ea44c1533a086f5c2c066956c9b
5
5
  SHA512:
6
- metadata.gz: 1f6efbf67c297f1bb264c388095e715f6774e3a6bbf555d38d1ddf4a1d28d3d3580c6843b1ec721bb8dc61644261e16c39a816695c5cb06b49026aef82ed6235
7
- data.tar.gz: 0d8aec51df7f3f588bf58aa3255840785fbe3357e1fb36870195abe7e308098f69d8f5e50c385cfcec959a876ee71445f61efb8d257ba2043b4a29878b69edc3
6
+ metadata.gz: d10e78d464ea90c15a6b9e17bcbbf1e6414fb3a842eda95f2662e69e3de245e6209214b096b8b3dd38f8bd0daefcd4c12b664b8947de02feb544b83f0be41b8e
7
+ data.tar.gz: fddc8561482df008189ba3cfcd93fc3751e1ea192d07c9008f201ee6c211f780da96ddd2de9d90395c48d575481e8f50c766a63e0792b05c1c55f5992f95d728
data/CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
1
+ # Changelog
2
+
3
+ ## 1.0.3 - 2024-06-06
4
+
5
+ - Add `payment/app` client and response model to match PHP EasySDK behaviour.
6
+ - Align kernel utilities (Config, Factory, EasySDKKernel, PageUtil, AES, Signer, JsonUtil, ResponseChecker) with the PHP implementation.
7
+ - Update Payment::Common/Page/Wap clients and models to PHP parity while retaining the custom `payment_url` extension.
8
+ - Refresh parity fixtures, expand test coverage, and remove the deprecated `examples/` directory.
9
+
10
+ ## 1.0.2 - 2024-04-15
11
+
12
+ - Align `payment/page`, `payment/wap`, and `payment/common` dependencies with the PHP EasySDK implementation.
13
+ - Extend `Kernel::Config` to support certificate paths, proxy, notify URL, and encryption settings used by PHP clients.
14
+ - Introduce certificate environment utilities to derive certificate serial numbers and cached public keys.
15
+ - Update `Payment::Common::Client` to honour proxy and SSL settings and improve parity with PHP HTTP behaviour.
16
+ - Add comprehensive test coverage for certificate handling, extended configuration, and HTTP proxy behaviour.
17
+
18
+ ## 1.0.1 - 2024-03-01
19
+
20
+ - Initial public release of the Ruby EasySDK port.
@@ -4,41 +4,39 @@ module Alipay
4
4
  module EasySDK
5
5
  module Kernel
6
6
  module AlipayConstants
7
- DEFAULT_CHARSET = "UTF-8"
8
-
9
- # 签名类型
10
- SIGN_TYPE_RSA = "RSA"
11
- SIGN_TYPE_RSA2 = "RSA2"
7
+ # Config键名(对齐PHP版)
8
+ PROTOCOL_CONFIG_KEY = "protocol"
9
+ HOST_CONFIG_KEY = "gatewayHost"
10
+ ALIPAY_CERT_PATH_CONFIG_KEY = "alipayCertPath"
11
+ MERCHANT_CERT_PATH_CONFIG_KEY = "merchantCertPath"
12
+ ALIPAY_ROOT_CERT_PATH_CONFIG_KEY = "alipayRootCertPath"
13
+ SIGN_TYPE_CONFIG_KEY = "signType"
14
+ NOTIFY_URL_CONFIG_KEY = "notifyUrl"
12
15
 
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"
16
+ # 与网关交互使用的字段
21
17
  BIZ_CONTENT_FIELD = "biz_content"
22
- CHARSET_FIELD = "charset"
23
- BODY_FIELD = "body"
24
18
  ALIPAY_CERT_SN_FIELD = "alipay_cert_sn"
19
+ SIGN_FIELD = "sign"
20
+ BODY_FIELD = "http_body"
25
21
  NOTIFY_URL_FIELD = "notify_url"
26
- NOTIFY_URL_CONFIG_KEY = "notifyUrl"
27
- PROTOCOL_CONFIG_KEY = "protocol"
28
- HOST_CONFIG_KEY = "gatewayHost"
22
+ METHOD_FIELD = "method"
29
23
  RESPONSE_SUFFIX = "_response"
30
24
  ERROR_RESPONSE = "error_response"
31
25
 
32
- # 默认值
26
+ DEFAULT_CHARSET = "UTF-8"
33
27
  DEFAULT_FORMAT = "json"
34
28
  DEFAULT_VERSION = "1.0"
35
- DEFAULT_SIGN_TYPE = SIGN_TYPE_RSA2
29
+ DEFAULT_SIGN_TYPE = "RSA2"
30
+
31
+ SIGN_TYPE_RSA = "RSA"
32
+ SIGN_TYPE_RSA2 = "RSA2"
33
+ RSA2 = "RSA2"
34
+ SHA_256_WITH_RSA = "SHA256WithRSA"
35
+ RSA = "RSA"
36
36
 
37
- # 请求方式
38
37
  GET = "GET"
39
38
  POST = "POST"
40
39
 
41
- # SDK信息
42
40
  SDK_VERSION = "alipay-easysdk-ruby-#{Alipay::EasySDK::VERSION}"
43
41
  end
44
42
  end
@@ -0,0 +1,35 @@
1
+ require 'openssl'
2
+ require 'digest/md5'
3
+
4
+ require_relative 'util/ant_certification_util'
5
+
6
+ module Alipay
7
+ module EasySDK
8
+ module Kernel
9
+ class CertEnvironment
10
+ attr_reader :root_cert_sn, :merchant_cert_sn
11
+
12
+ def initialize
13
+ @cert_util = Util::AntCertificationUtil.new
14
+ @root_cert_sn = nil
15
+ @merchant_cert_sn = nil
16
+ @cached_alipay_public_key = nil
17
+ end
18
+
19
+ def setup(merchant_cert_path, alipay_cert_path, alipay_root_cert_path)
20
+ if [merchant_cert_path, alipay_cert_path, alipay_root_cert_path].any? { |path| path.nil? || path.to_s.strip.empty? }
21
+ raise RuntimeError, '证书参数merchantCertPath、alipayCertPath或alipayRootCertPath设置不完整。'
22
+ end
23
+
24
+ @root_cert_sn = @cert_util.root_cert_sn(alipay_root_cert_path)
25
+ @merchant_cert_sn = @cert_util.cert_sn(merchant_cert_path)
26
+ @cached_alipay_public_key = @cert_util.public_key(alipay_cert_path)
27
+ end
28
+
29
+ def cached_alipay_public_key
30
+ @cached_alipay_public_key
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -1,81 +1,108 @@
1
- require_relative 'alipay_constants'
2
-
3
1
  module Alipay
4
2
  module EasySDK
5
3
  module Kernel
6
4
  class Config
7
5
  attr_accessor :protocol,
8
- :gateway_host,
9
- :app_id,
10
- :sign_type,
11
- :alipay_public_key,
12
- :merchant_private_key,
13
- :merchant_cert_path,
14
- :alipay_cert_path,
15
- :alipay_root_cert_path,
16
- :merchant_cert_sn,
17
- :alipay_cert_sn,
18
- :alipay_root_cert_sn,
19
- :notify_url,
20
- :encrypt_key,
21
- :http_proxy,
22
- :ignore_ssl,
23
- :charset,
24
- :format,
25
- :version
26
-
27
- def initialize(options = {})
28
- opts = options || {}
29
-
30
- @protocol = fetch_option(opts, :protocol, 'protocol') || 'https'
31
- @gateway_host = fetch_option(opts, :gateway_host, 'gateway_host', :gatewayHost, 'gatewayHost') || 'openapi.alipay.com/gateway.do'
32
- @app_id = fetch_option(opts, :app_id, 'app_id', :appId, 'appId')
33
- @sign_type = fetch_option(opts, :sign_type, 'sign_type', :signType, 'signType') || AlipayConstants::DEFAULT_SIGN_TYPE
34
- @alipay_public_key = fetch_option(opts, :alipay_public_key, 'alipay_public_key', :alipayPublicKey, 'alipayPublicKey')
35
- @merchant_private_key = fetch_option(opts, :merchant_private_key, 'merchant_private_key', :merchantPrivateKey, 'merchantPrivateKey')
36
- @merchant_cert_path = fetch_option(opts, :merchant_cert_path, 'merchant_cert_path', :merchantCertPath, 'merchantCertPath')
37
- @alipay_cert_path = fetch_option(opts, :alipay_cert_path, 'alipay_cert_path', :alipayCertPath, 'alipayCertPath')
38
- @alipay_root_cert_path = fetch_option(opts, :alipay_root_cert_path, 'alipay_root_cert_path', :alipayRootCertPath, 'alipayRootCertPath')
39
- @merchant_cert_sn = fetch_option(opts, :merchant_cert_sn, 'merchant_cert_sn', :merchantCertSN, 'merchantCertSN')
40
- @alipay_cert_sn = fetch_option(opts, :alipay_cert_sn, 'alipay_cert_sn', :alipayCertSN, 'alipayCertSN')
41
- @alipay_root_cert_sn = fetch_option(opts, :alipay_root_cert_sn, 'alipay_root_cert_sn', :alipayRootCertSN, 'alipayRootCertSN')
42
- @notify_url = fetch_option(opts, :notify_url, 'notify_url', :notifyUrl, 'notifyUrl')
43
- @encrypt_key = fetch_option(opts, :encrypt_key, 'encrypt_key', :encryptKey, 'encryptKey')
44
- @http_proxy = fetch_option(opts, :http_proxy, 'http_proxy', :httpProxy, 'httpProxy')
45
- @ignore_ssl = fetch_option(opts, :ignore_ssl, 'ignore_ssl', :ignoreSSL, 'ignoreSSL')
46
- @charset = fetch_option(opts, :charset, 'charset') || AlipayConstants::DEFAULT_CHARSET
47
- @format = fetch_option(opts, :format, 'format') || AlipayConstants::DEFAULT_FORMAT
48
- @version = fetch_option(opts, :version, 'version') || AlipayConstants::DEFAULT_VERSION
49
- end
6
+ :gatewayHost,
7
+ :appId,
8
+ :signType,
9
+ :alipayPublicKey,
10
+ :merchantPrivateKey,
11
+ :merchantCertPath,
12
+ :alipayCertPath,
13
+ :alipayRootCertPath,
14
+ :merchantCertSN,
15
+ :alipayCertSN,
16
+ :alipayRootCertSN,
17
+ :notifyUrl,
18
+ :encryptKey,
19
+ :httpProxy,
20
+ :ignoreSSL
50
21
 
51
- def gateway_url
52
- "#{@protocol}://#{@gateway_host}"
22
+ def initialize(options = nil)
23
+ assign_attributes(options) if options
53
24
  end
54
25
 
55
- def validate
56
- raise "app_id is required" if @app_id.nil? || @app_id.empty?
57
- raise "merchant_private_key is required" if @merchant_private_key.nil? || @merchant_private_key.empty?
58
- raise "alipay_public_key is required" if @alipay_public_key.nil? || @alipay_public_key.empty?
59
- end
26
+ # 下划线风格的访问器保持向后兼容
27
+ alias_method :gateway_host, :gatewayHost
28
+ alias_method :gateway_host=, :gatewayHost=
29
+
30
+ alias_method :app_id, :appId
31
+ alias_method :app_id=, :appId=
32
+
33
+ alias_method :sign_type, :signType
34
+ alias_method :sign_type=, :signType=
35
+
36
+ alias_method :alipay_public_key, :alipayPublicKey
37
+ alias_method :alipay_public_key=, :alipayPublicKey=
38
+
39
+ alias_method :merchant_private_key, :merchantPrivateKey
40
+ alias_method :merchant_private_key=, :merchantPrivateKey=
41
+
42
+ alias_method :merchant_cert_path, :merchantCertPath
43
+ alias_method :merchant_cert_path=, :merchantCertPath=
44
+
45
+ alias_method :alipay_cert_path, :alipayCertPath
46
+ alias_method :alipay_cert_path=, :alipayCertPath=
47
+
48
+ alias_method :alipay_root_cert_path, :alipayRootCertPath
49
+ alias_method :alipay_root_cert_path=, :alipayRootCertPath=
50
+
51
+ alias_method :merchant_cert_sn, :merchantCertSN
52
+ alias_method :merchant_cert_sn=, :merchantCertSN=
53
+
54
+ alias_method :alipay_cert_sn, :alipayCertSN
55
+ alias_method :alipay_cert_sn=, :alipayCertSN=
56
+
57
+ alias_method :alipay_root_cert_sn, :alipayRootCertSN
58
+ alias_method :alipay_root_cert_sn=, :alipayRootCertSN=
59
+
60
+ alias_method :notify_url, :notifyUrl
61
+ alias_method :notify_url=, :notifyUrl=
62
+
63
+ alias_method :encrypt_key, :encryptKey
64
+ alias_method :encrypt_key=, :encryptKey=
65
+
66
+ alias_method :http_proxy, :httpProxy
67
+ alias_method :http_proxy=, :httpProxy=
68
+
69
+ alias_method :ignore_ssl, :ignoreSSL
70
+ alias_method :ignore_ssl=, :ignoreSSL=
60
71
 
61
72
  private
62
73
 
63
- def fetch_option(options, *keys)
64
- keys.each do |key|
65
- if key.is_a?(String)
66
- symbol_key = key.tr('-', '_').to_sym
67
- return options[key] if options.key?(key)
68
- return options[symbol_key] if options.key?(symbol_key)
69
- elsif key.is_a?(Symbol)
70
- string_key = key.to_s
71
- camel_case_key = string_key.gsub(/_([a-z])/) { Regexp.last_match(1).upcase }
72
- return options[key] if options.key?(key)
73
- return options[string_key] if options.key?(string_key)
74
- return options[camel_case_key] if options.key?(camel_case_key)
75
- end
74
+ def assign_attributes(options)
75
+ options.each do |key, value|
76
+ setter = attribute_writer_for(key)
77
+ public_send(setter, value) if setter && respond_to?(setter, true)
76
78
  end
79
+ end
80
+
81
+ def attribute_writer_for(key)
82
+ string_key = key.to_s
83
+ writer = "#{string_key}="
84
+ return writer if respond_to?(writer, true)
85
+
86
+ camel_case = underscore_to_camel(string_key)
87
+ writer = "#{camel_case}="
88
+ return writer if respond_to?(writer, true)
89
+
90
+ snake_case = camel_to_snake(string_key)
91
+ writer = "#{snake_case}="
92
+ return writer if respond_to?(writer, true)
93
+
77
94
  nil
78
95
  end
96
+
97
+ def underscore_to_camel(str)
98
+ str.split('_').inject do |memo, part|
99
+ memo + part.capitalize
100
+ end
101
+ end
102
+
103
+ def camel_to_snake(str)
104
+ str.gsub(/([A-Z]+)/) { "_#{$1.downcase}" }.sub(/^_/, '')
105
+ end
79
106
  end
80
107
  end
81
108
  end
@@ -3,6 +3,8 @@ require_relative 'config'
3
3
  require_relative 'util/json_util'
4
4
  require_relative 'util/signer'
5
5
  require_relative 'util/sign_content_extractor'
6
+ require_relative 'util/aes'
7
+ require_relative 'util/page_util'
6
8
  require 'net/http'
7
9
  require 'uri'
8
10
  require 'cgi'
@@ -39,47 +41,16 @@ module Alipay
39
41
  end
40
42
 
41
43
  def get_config(key)
42
- lookup = {
43
- 'protocol' => :protocol,
44
- 'gatewayHost' => :gateway_host,
45
- 'gateway_host' => :gateway_host,
46
- 'appId' => :app_id,
47
- 'app_id' => :app_id,
48
- 'merchantPrivateKey' => :merchant_private_key,
49
- 'merchant_private_key' => :merchant_private_key,
50
- 'alipayPublicKey' => :alipay_public_key,
51
- 'alipay_public_key' => :alipay_public_key,
52
- 'signType' => :sign_type,
53
- 'sign_type' => :sign_type,
54
- 'charset' => :charset,
55
- 'format' => :format,
56
- 'version' => :version,
57
- 'notifyUrl' => :notify_url,
58
- 'notify_url' => :notify_url,
59
- 'encryptKey' => :encrypt_key,
60
- 'encrypt_key' => :encrypt_key,
61
- 'httpProxy' => :http_proxy,
62
- 'http_proxy' => :http_proxy,
63
- 'ignoreSSL' => :ignore_ssl,
64
- 'ignore_ssl' => :ignore_ssl,
65
- 'merchantCertPath' => :merchant_cert_path,
66
- 'merchant_cert_path' => :merchant_cert_path,
67
- 'alipayCertPath' => :alipay_cert_path,
68
- 'alipay_cert_path' => :alipay_cert_path,
69
- 'alipayRootCertPath' => :alipay_root_cert_path,
70
- 'alipay_root_cert_path' => :alipay_root_cert_path,
71
- 'merchantCertSN' => :merchant_cert_sn,
72
- 'merchant_cert_sn' => :merchant_cert_sn,
73
- 'alipayCertSN' => :alipay_cert_sn,
74
- 'alipay_cert_sn' => :alipay_cert_sn,
75
- 'alipayRootCertSN' => :alipay_root_cert_sn,
76
- 'alipay_root_cert_sn' => :alipay_root_cert_sn
77
- }
78
-
79
- method = lookup[key]
80
- return unless method
81
-
82
- @config.public_send(method)
44
+ method = key.to_s
45
+ return @config.public_send(method) if @config.respond_to?(method)
46
+
47
+ snake = method.gsub(/([A-Z]+)/) { "_#{$1.downcase}" }.sub(/^_/, '')
48
+ return @config.public_send(snake) if @config.respond_to?(snake)
49
+
50
+ camel = snake.split('_').inject { |memo, part| memo + part.capitalize }
51
+ return @config.public_send(camel) if camel && @config.respond_to?(camel)
52
+
53
+ nil
83
54
  end
84
55
 
85
56
  def get_sdk_version
@@ -97,9 +68,9 @@ module Alipay
97
68
  def read_as_json(response, method)
98
69
  response_body = response.body
99
70
  map = {}
100
- map['body'] = response_body
101
- map['method'] = method
102
- return map
71
+ map[AlipayConstants::BODY_FIELD] = response_body
72
+ map[AlipayConstants::METHOD_FIELD] = method
73
+ map
103
74
  end
104
75
 
105
76
  def get_random_boundary
@@ -109,11 +80,12 @@ module Alipay
109
80
  def generate_page(method, system_params, biz_params, text_params, sign)
110
81
  puts "[DEBUG] generate_page - sign长度: #{sign.length}" if ENV['DEBUG']
111
82
  signed_params = build_signed_params(system_params, biz_params, text_params, sign)
112
- if method == 'GET'
113
- return build_gateway_url(signed_params)
114
- elsif method == 'POST'
83
+ case method
84
+ when AlipayConstants::GET
85
+ build_gateway_url(signed_params)
86
+ when AlipayConstants::POST
115
87
  puts "[DEBUG] generate_page POST - sorted_map中sign长度: #{signed_params[AlipayConstants::SIGN_FIELD].length}" if ENV['DEBUG']
116
- return build_form(get_gateway_server_url, signed_params)
88
+ build_form(get_gateway_server_url, signed_params)
117
89
  else
118
90
  raise "不支持" + method
119
91
  end
@@ -130,8 +102,8 @@ module Alipay
130
102
 
131
103
  def get_alipay_cert_sn(resp_map)
132
104
  if !@config.merchant_cert_sn.nil? && !@config.merchant_cert_sn.empty?
133
- body = JSON.parse(resp_map['body'])
134
- alipay_cert_sn = body['alipay_cert_sn']
105
+ body = JSON.parse(resp_map[AlipayConstants::BODY_FIELD])
106
+ alipay_cert_sn = body[AlipayConstants::ALIPAY_CERT_SN_FIELD]
135
107
  return alipay_cert_sn
136
108
  end
137
109
  end
@@ -150,10 +122,10 @@ module Alipay
150
122
  end
151
123
 
152
124
  def verify(resp_map, alipay_public_key)
153
- resp = JSON.parse(resp_map['body'])
125
+ resp = JSON.parse(resp_map[AlipayConstants::BODY_FIELD])
154
126
  sign = resp[AlipayConstants::SIGN_FIELD]
155
127
  sign_content_extractor = Alipay::EasySDK::Kernel::Util::SignContentExtractor.new
156
- content = sign_content_extractor.get_sign_source_data(resp_map['body'], resp_map['method'])
128
+ content = sign_content_extractor.get_sign_source_data(resp_map[AlipayConstants::BODY_FIELD], resp_map[AlipayConstants::METHOD_FIELD])
157
129
  signer = Alipay::EasySDK::Kernel::Util::Signer.new
158
130
  return signer.verify(content, sign, alipay_public_key)
159
131
  end
@@ -178,18 +150,55 @@ module Alipay
178
150
  URI.encode_www_form(sorted_map)
179
151
  end
180
152
 
153
+ def to_multipart_request_body(text_params, file_params, boundary)
154
+ @text_params = text_params
155
+ @biz_params = nil
156
+ if text_params != nil && !@optional_text_params.empty?
157
+ @text_params = text_params.merge(@optional_text_params)
158
+ elsif text_params == nil
159
+ @text_params = @optional_text_params.dup
160
+ end
161
+
162
+ parts = []
163
+ (@text_params || {}).each do |key, value|
164
+ parts << build_multipart_text_part(boundary, key, value)
165
+ end
166
+
167
+ (file_params || {}).each do |key, path|
168
+ raise "文件#{path}不存在" unless File.exist?(path.to_s)
169
+ file_content = File.binread(path)
170
+ filename = File.basename(path)
171
+ parts << build_multipart_file_part(boundary, key, filename, file_content)
172
+ end
173
+
174
+ return nil if parts.empty?
175
+
176
+ parts << "--#{boundary}--\r\n"
177
+ parts.join
178
+ end
179
+
180
+ def aes_encrypt(content, encrypt_key)
181
+ aes = Alipay::EasySDK::Kernel::Util::AES.new
182
+ aes.aes_encrypt(content, encrypt_key)
183
+ end
184
+
185
+ def aes_decrypt(content, encrypt_key)
186
+ aes = Alipay::EasySDK::Kernel::Util::AES.new
187
+ aes.aes_decrypt(content, encrypt_key)
188
+ end
189
+
181
190
  def sort_map(random_map)
182
191
  return random_map
183
192
  end
184
193
 
185
194
  def to_resp_model(resp_map)
186
- body = resp_map['body']
187
- method_name = resp_map['method']
195
+ body = resp_map[AlipayConstants::BODY_FIELD]
196
+ method_name = resp_map[AlipayConstants::METHOD_FIELD]
188
197
  response_node_name = method_name.gsub(".", "_") + "_response"
189
198
 
190
199
  model = JSON.parse(body)
191
- if body.include?("error_response")
192
- result = model["error_response"]
200
+ if body.include?(AlipayConstants::ERROR_RESPONSE)
201
+ result = model[AlipayConstants::ERROR_RESPONSE]
193
202
  result['body'] = body
194
203
  else
195
204
  result = model[response_node_name]
@@ -236,15 +245,15 @@ module Alipay
236
245
  @biz_params = @optional_biz_params
237
246
  end
238
247
 
239
- json = Alipay::EasySDK::Kernel::Util::JsonUtil.new
240
- if @biz_params != nil
241
- biz_params = json.to_json_string(@biz_params)
242
- end
248
+ json_util = Alipay::EasySDK::Kernel::Util::JsonUtil.new
249
+ biz_content = json_util.to_json_string(@biz_params) unless @biz_params.nil?
250
+
243
251
  sorted_map = (system_params || {}).dup
244
- if !biz_params.nil? && !biz_params.empty?
245
- # 模拟PHP json_encode($bizParams, JSON_UNESCAPED_UNICODE)
246
- json_string = JSON.generate(JSON.parse(biz_params)).gsub('/', "\\/")
247
- sorted_map[AlipayConstants::BIZ_CONTENT_FIELD] = json_string
252
+ if !biz_content.nil?
253
+ unless biz_content.respond_to?(:empty?) && biz_content.empty?
254
+ serialized = JSON.generate(biz_content, ascii_only: false).gsub('/', '\\/')
255
+ sorted_map[AlipayConstants::BIZ_CONTENT_FIELD] = serialized
256
+ end
248
257
  end
249
258
  if !@text_params.nil? && !@text_params.empty?
250
259
  if !sorted_map.empty?
@@ -253,15 +262,19 @@ module Alipay
253
262
  sorted_map = @text_params
254
263
  end
255
264
  end
256
- if get_config('notify_url') != nil
257
- sorted_map['notify_url'] = get_config('notify_url')
265
+ notify_value = get_config(AlipayConstants::NOTIFY_URL_CONFIG_KEY)
266
+ if !notify_value.nil? && notify_value.to_s.strip != ''
267
+ sorted_map[AlipayConstants::NOTIFY_URL_FIELD] = notify_value
258
268
  end
259
269
  return sorted_map
260
270
  end
261
271
 
262
272
  def get_sign_content(params)
263
273
  # 模拟PHP的ksort
264
- sorted_params = params.sort_by { |k, _| k.to_s }.to_h
274
+ normalized = params.each_with_object({}) do |(key, value), acc|
275
+ acc[key.to_s] = value
276
+ end
277
+ sorted_params = normalized.sort.to_h
265
278
 
266
279
  string_to_be_signed = ""
267
280
  i = 0
@@ -281,7 +294,9 @@ module Alipay
281
294
  end
282
295
 
283
296
  def get_gateway_server_url
284
- return get_config('protocol') + '://' + get_config('gatewayHost').gsub('/gateway.do', '') + '/gateway.do'
297
+ protocol = get_config(AlipayConstants::PROTOCOL_CONFIG_KEY)
298
+ host = get_config(AlipayConstants::HOST_CONFIG_KEY)
299
+ return protocol + '://' + host.gsub('/gateway.do', '') + '/gateway.do'
285
300
  end
286
301
 
287
302
  def check_empty(value)
@@ -322,25 +337,8 @@ module Alipay
322
337
  puts "[DEBUG] build_form - 原始params keys: #{params.keys.join(', ')}" if ENV['DEBUG']
323
338
  puts "[DEBUG] build_form - sign长度: #{params[AlipayConstants::SIGN_FIELD].length}" if ENV['DEBUG'] && params[AlipayConstants::SIGN_FIELD]
324
339
 
325
- # 完全按照PHP版本的PageUtil::buildForm方法:过滤空值参数
326
- form_fields = ""
327
- params.each do |key, val|
328
- if !check_empty(val) # 与PHP版本的checkEmpty逻辑完全一致
329
- # 按照PHP版本:将单引号替换为&amp;apos;
330
- escaped_val = val.to_s.gsub("'", "&apos;")
331
- form_fields += "<input type='hidden' name='#{key}' value='#{escaped_val}'/>"
332
- end
333
- end
334
-
335
- # 完全按照PHP版本:在action URL中添加charset参数
336
- action_url = "#{url}?charset=#{AlipayConstants::DEFAULT_CHARSET}"
337
-
338
- <<~HTML
339
- <form id='alipaysubmit' name='alipaysubmit' action='#{action_url}' method='POST'>
340
- #{form_fields}
341
- <input type='submit' value='ok' style='display:none;'></form>
342
- <script>document.forms['alipaysubmit'].submit();</script>
343
- HTML
340
+ page_util = Alipay::EasySDK::Kernel::Util::PageUtil.new
341
+ page_util.build_form(url, params)
344
342
  end
345
343
 
346
344
  def build_signed_params(system_params, biz_params, text_params, sign)
@@ -350,10 +348,25 @@ module Alipay
350
348
  end
351
349
 
352
350
  def build_gateway_url(signed_params)
353
- base_url = get_gateway_server_url + "?charset=#{AlipayConstants::DEFAULT_CHARSET}"
351
+ base_url = get_gateway_server_url
354
352
  query = build_query_string(signed_params)
355
353
  return base_url if query.nil? || query.empty?
356
- base_url + '&' + query
354
+ base_url + '?' + query
355
+ end
356
+
357
+ def build_multipart_text_part(boundary, key, value)
358
+ "--#{boundary}\r\n" \
359
+ "Content-Disposition: form-data; name=\"#{key}\"\r\n" \
360
+ "\r\n" \
361
+ "#{value}\r\n"
362
+ end
363
+
364
+ def build_multipart_file_part(boundary, key, filename, content)
365
+ "--#{boundary}\r\n" \
366
+ "Content-Disposition: form-data; name=\"#{key}\"; filename=\"#{filename}\"\r\n" \
367
+ "Content-Type: application/octet-stream\r\n" \
368
+ "\r\n" \
369
+ "#{content}\r\n"
357
370
  end
358
371
  end
359
372
  end