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
@@ -0,0 +1,328 @@
1
+ require_relative 'alipay_constants'
2
+ require_relative 'config'
3
+ require_relative 'util/json_util'
4
+ require_relative 'util/signer'
5
+ require_relative 'util/sign_content_extractor'
6
+ require 'net/http'
7
+ require 'uri'
8
+ require 'cgi'
9
+
10
+ module Alipay
11
+ module EasySDK
12
+ module Kernel
13
+ class EasySDKKernel
14
+ attr_reader :config, :optional_text_params, :optional_biz_params, :text_params, :biz_params
15
+
16
+ def initialize(config)
17
+ @config = config
18
+ @optional_text_params = {}
19
+ @optional_biz_params = {}
20
+ @text_params = {}
21
+ @biz_params = {}
22
+ end
23
+
24
+ def inject_text_param(key, value)
25
+ if key != nil
26
+ @optional_text_params[key] = value
27
+ end
28
+ end
29
+
30
+ def inject_biz_param(key, value)
31
+ if key != nil
32
+ @optional_biz_params[key] = value
33
+ end
34
+ end
35
+
36
+ def get_timestamp
37
+ return Time.now.strftime("%Y-%m-%d %H:%M:%S")
38
+ end
39
+
40
+ def get_config(key)
41
+ case key
42
+ when 'protocol'
43
+ @config.protocol
44
+ when 'gatewayHost'
45
+ @config.gateway_host
46
+ when 'appId'
47
+ @config.app_id
48
+ when 'merchantPrivateKey'
49
+ @config.merchant_private_key
50
+ when 'alipayPublicKey'
51
+ @config.alipay_public_key
52
+ when 'signType'
53
+ @config.sign_type
54
+ when 'charset'
55
+ @config.charset
56
+ when 'format'
57
+ @config.format
58
+ when 'version'
59
+ @config.version
60
+ else
61
+ nil
62
+ end
63
+ end
64
+
65
+ def get_sdk_version
66
+ AlipayConstants::SDK_VERSION
67
+ end
68
+
69
+ def to_url_encoded_request_body(biz_params)
70
+ sorted_map = get_sorted_map(nil, biz_params, nil)
71
+ if sorted_map.nil? || sorted_map.empty?
72
+ return nil
73
+ end
74
+ build_query_string(sorted_map)
75
+ end
76
+
77
+ def read_as_json(response, method)
78
+ response_body = response.body
79
+ map = {}
80
+ map['body'] = response_body
81
+ map['method'] = method
82
+ return map
83
+ end
84
+
85
+ def get_random_boundary
86
+ Time.now.strftime("%Y-%m-%d %H:%M:%S") + ''
87
+ end
88
+
89
+ def generate_page(method, system_params, biz_params, text_params, sign)
90
+ puts "[DEBUG] generate_page - sign长度: #{sign.length}" if ENV['DEBUG']
91
+ if method == 'GET'
92
+ # 采集并排序所有参数
93
+ sorted_map = get_sorted_map(system_params, biz_params, text_params)
94
+ sorted_map[AlipayConstants::SIGN_FIELD] = sign
95
+ return get_gateway_server_url + '?' + build_query_string(sorted_map)
96
+ elsif method == 'POST'
97
+ # 完全按照PHP版本的逻辑:采集并排序所有参数,不覆盖已注入的参数
98
+ sorted_map = get_sorted_map(system_params, biz_params, text_params)
99
+ sorted_map[AlipayConstants::SIGN_FIELD] = sign
100
+ puts "[DEBUG] generate_page POST - sorted_map中sign长度: #{sorted_map[AlipayConstants::SIGN_FIELD].length}" if ENV['DEBUG']
101
+ return build_form(get_gateway_server_url, sorted_map)
102
+ else
103
+ raise "不支持" + method
104
+ end
105
+ end
106
+
107
+ def get_merchant_cert_sn
108
+ return @config.merchant_cert_sn
109
+ end
110
+
111
+ def get_alipay_cert_sn(resp_map)
112
+ if !@config.merchant_cert_sn.nil? && !@config.merchant_cert_sn.empty?
113
+ body = JSON.parse(resp_map['body'])
114
+ alipay_cert_sn = body['alipay_cert_sn']
115
+ return alipay_cert_sn
116
+ end
117
+ end
118
+
119
+ def get_alipay_root_cert_sn
120
+ return @config.alipay_root_cert_sn
121
+ end
122
+
123
+ def is_cert_mode
124
+ return @config.merchant_cert_sn
125
+ end
126
+
127
+ def extract_alipay_public_key(alipay_cert_sn)
128
+ # Ruby 版本只存储一个版本支付宝公钥
129
+ return @config.alipay_public_key
130
+ end
131
+
132
+ def verify(resp_map, alipay_public_key)
133
+ resp = JSON.parse(resp_map['body'])
134
+ sign = resp[AlipayConstants::SIGN_FIELD]
135
+ sign_content_extractor = Alipay::EasySDK::Kernel::Util::SignContentExtractor.new
136
+ content = sign_content_extractor.get_sign_source_data(resp_map['body'], resp_map['method'])
137
+ signer = Alipay::EasySDK::Kernel::Util::Signer.new
138
+ return signer.verify(content, sign, alipay_public_key)
139
+ end
140
+
141
+ def sign(system_params, biz_params, text_params, private_key)
142
+ sorted_map = get_sorted_map(system_params, biz_params, text_params)
143
+ data = get_sign_content(sorted_map)
144
+ if ENV['DEBUG']
145
+ puts "[DEBUG] sign content: #{data}"
146
+ end
147
+ puts "[DEBUG] 签名内容: #{data[0, 100]}..." if ENV['DEBUG']
148
+ signer = Alipay::EasySDK::Kernel::Util::Signer.new
149
+ signature = signer.sign(data, private_key)
150
+ puts "[DEBUG] 生成签名长度: #{signature.length}" if ENV['DEBUG']
151
+ return signature
152
+ end
153
+
154
+ def generate_order_string(system_params, biz_params, text_params, sign)
155
+ # 采集并排序所有参数
156
+ sorted_map = get_sorted_map(system_params, biz_params, text_params)
157
+ sorted_map[AlipayConstants::SIGN_FIELD] = sign
158
+ URI.encode_www_form(sorted_map)
159
+ end
160
+
161
+ def sort_map(random_map)
162
+ return random_map
163
+ end
164
+
165
+ def to_resp_model(resp_map)
166
+ body = resp_map['body']
167
+ method_name = resp_map['method']
168
+ response_node_name = method_name.gsub(".", "_") + "_response"
169
+
170
+ model = JSON.parse(body)
171
+ if body.include?("error_response")
172
+ result = model["error_response"]
173
+ result['body'] = body
174
+ else
175
+ result = model[response_node_name]
176
+ result['body'] = body
177
+ end
178
+ return result
179
+ end
180
+
181
+ def verify_params(parameters, public_key)
182
+ signer = Alipay::EasySDK::Kernel::Util::Signer.new
183
+ return signer.verify_params(parameters, public_key)
184
+ end
185
+
186
+ def concat_str(a, b)
187
+ return a + b
188
+ end
189
+
190
+ private
191
+
192
+ def build_query_string(sorted_map)
193
+ request_url = nil
194
+ sorted_map.each do |sys_param_key, sys_param_value|
195
+ if request_url.nil?
196
+ request_url = "#{sys_param_key}=" + url_encode(characet(sys_param_value.to_s, AlipayConstants::DEFAULT_CHARSET)) + "&"
197
+ else
198
+ request_url += "#{sys_param_key}=" + url_encode(characet(sys_param_value.to_s, AlipayConstants::DEFAULT_CHARSET)) + "&"
199
+ end
200
+ end
201
+ request_url = request_url[0...-1] unless request_url.nil?
202
+ return request_url
203
+ end
204
+
205
+ def get_sorted_map(system_params, biz_params, text_params)
206
+ @text_params = text_params
207
+ @biz_params = biz_params
208
+ if text_params != nil && !@optional_text_params.empty?
209
+ @text_params = text_params.merge(@optional_text_params)
210
+ elsif text_params == nil
211
+ @text_params = @optional_text_params
212
+ end
213
+ if biz_params != nil && !@optional_biz_params.empty?
214
+ @biz_params = biz_params.merge(@optional_biz_params)
215
+ elsif biz_params == nil
216
+ @biz_params = @optional_biz_params
217
+ end
218
+
219
+ json = Alipay::EasySDK::Kernel::Util::JsonUtil.new
220
+ if @biz_params != nil
221
+ biz_params = json.to_json_string(@biz_params)
222
+ end
223
+ sorted_map = system_params || {}
224
+ if !biz_params.nil? && !biz_params.empty?
225
+ # 模拟PHP json_encode($bizParams, JSON_UNESCAPED_UNICODE)
226
+ json_string = JSON.generate(JSON.parse(biz_params)).gsub('/', "\\/")
227
+ sorted_map[AlipayConstants::BIZ_CONTENT_FIELD] = json_string
228
+ end
229
+ if !@text_params.nil? && !@text_params.empty?
230
+ if !sorted_map.empty?
231
+ sorted_map = sorted_map.merge(@text_params)
232
+ else
233
+ sorted_map = @text_params
234
+ end
235
+ end
236
+ if get_config('notify_url') != nil
237
+ sorted_map['notify_url'] = get_config('notify_url')
238
+ end
239
+ return sorted_map
240
+ end
241
+
242
+ def get_sign_content(params)
243
+ # 模拟PHP的ksort
244
+ sorted_params = params.sort_by { |k, _| k.to_s }.to_h
245
+
246
+ string_to_be_signed = ""
247
+ i = 0
248
+ sorted_params.each do |k, v|
249
+ if !check_empty(v) && v.to_s[0] != "@"
250
+ # 转换成目标字符集
251
+ v = characet(v.to_s, AlipayConstants::DEFAULT_CHARSET)
252
+ if i == 0
253
+ string_to_be_signed += "#{k}=#{v}"
254
+ else
255
+ string_to_be_signed += "&#{k}=#{v}"
256
+ end
257
+ i += 1
258
+ end
259
+ end
260
+ return string_to_be_signed
261
+ end
262
+
263
+ def get_gateway_server_url
264
+ return get_config('protocol') + '://' + get_config('gatewayHost').gsub('/gateway.do', '') + '/gateway.do'
265
+ end
266
+
267
+ def check_empty(value)
268
+ if value.nil?
269
+ return true
270
+ end
271
+ if value == ""
272
+ return true
273
+ end
274
+ if value.to_s.strip == ""
275
+ return true
276
+ end
277
+ return false
278
+ end
279
+
280
+ def characet(data, target_charset)
281
+ if !data.nil? && !data.empty?
282
+ file_type = AlipayConstants::DEFAULT_CHARSET
283
+ if file_type.downcase != target_charset.downcase
284
+ begin
285
+ data = data.encode(target_charset, invalid: :replace, undef: :replace)
286
+ rescue
287
+ data = data.force_encoding(target_charset)
288
+ end
289
+ end
290
+ end
291
+ return data
292
+ end
293
+
294
+ def url_encode(str)
295
+ # 模拟PHP的urlencode
296
+ str.gsub(/[^a-zA-Z0-9\-_.~]/n) do |s|
297
+ '%' + s.unpack('H2' * s.bytesize).join('%').upcase
298
+ end
299
+ end
300
+
301
+ def build_form(url, params)
302
+ puts "[DEBUG] build_form - 原始params keys: #{params.keys.join(', ')}" if ENV['DEBUG']
303
+ puts "[DEBUG] build_form - sign长度: #{params[AlipayConstants::SIGN_FIELD].length}" if ENV['DEBUG'] && params[AlipayConstants::SIGN_FIELD]
304
+
305
+ # 完全按照PHP版本的PageUtil::buildForm方法:过滤空值参数
306
+ form_fields = ""
307
+ params.each do |key, val|
308
+ if !check_empty(val) # 与PHP版本的checkEmpty逻辑完全一致
309
+ # 按照PHP版本:将单引号替换为'
310
+ escaped_val = val.to_s.gsub("'", "'")
311
+ form_fields += "<input type='hidden' name='#{key}' value='#{escaped_val}'/>"
312
+ end
313
+ end
314
+
315
+ # 完全按照PHP版本:在action URL中添加charset参数
316
+ action_url = "#{url}?charset=#{AlipayConstants::DEFAULT_CHARSET}"
317
+
318
+ <<~HTML
319
+ <form id='alipaysubmit' name='alipaysubmit' action='#{action_url}' method='POST'>
320
+ #{form_fields}
321
+ <input type='submit' value='ok' style='display:none;'></form>
322
+ <script>document.forms['alipaysubmit'].submit();</script>
323
+ HTML
324
+ end
325
+ end
326
+ end
327
+ end
328
+ end
@@ -0,0 +1,95 @@
1
+ require_relative 'alipay_constants'
2
+ require_relative 'config'
3
+ require_relative 'easy_sdk_kernel'
4
+ require_relative '../payment/wap/client'
5
+ require_relative '../payment/page/client'
6
+ require_relative '../payment/common/client'
7
+
8
+ module Alipay
9
+ module EasySDK
10
+ module Kernel
11
+ class Factory
12
+ class ConfigurationNotSetError < StandardError; end
13
+
14
+ class << self
15
+ def set_options(options = nil)
16
+ config = normalize_config(options)
17
+ initialize_context(config)
18
+ @instance
19
+ end
20
+
21
+ alias setOptions set_options
22
+
23
+ def config(options = nil)
24
+ return set_options(options) if options
25
+ ensure_context_set!
26
+ @config
27
+ end
28
+
29
+ def payment
30
+ ensure_context_set!
31
+ @payment
32
+ end
33
+
34
+ def wap
35
+ payment.wap
36
+ end
37
+
38
+ def page
39
+ payment.page
40
+ end
41
+
42
+ def common
43
+ payment.common
44
+ end
45
+
46
+ def kernel
47
+ ensure_context_set!
48
+ @kernel
49
+ end
50
+
51
+ def get_sdk_version
52
+ Alipay::EasySDK::Kernel::AlipayConstants::SDK_VERSION
53
+ end
54
+
55
+ private
56
+
57
+ def normalize_config(options)
58
+ raise ArgumentError, '配置参数不能为空' if options.nil?
59
+ return options if options.is_a?(Config)
60
+ Config.new(options)
61
+ end
62
+
63
+ def initialize_context(config)
64
+ @config = config
65
+ @kernel = EasySDKKernel.new(config)
66
+ @payment = Payment.new(@kernel)
67
+ @instance = self
68
+ end
69
+
70
+ def ensure_context_set!
71
+ raise ConfigurationNotSetError, '请先调用Factory.set_options(config)设置SDK配置' unless @config
72
+ end
73
+ end
74
+
75
+ class Payment
76
+ def initialize(kernel)
77
+ @kernel = kernel
78
+ end
79
+
80
+ def wap
81
+ @wap ||= Alipay::EasySDK::Payment::Wap::Client.new(@kernel)
82
+ end
83
+
84
+ def page
85
+ @page ||= Alipay::EasySDK::Payment::Page::Client.new(@kernel)
86
+ end
87
+
88
+ def common
89
+ @common ||= Alipay::EasySDK::Payment::Common::Client.new(@kernel)
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,30 @@
1
+ require 'json'
2
+
3
+ module Alipay
4
+ module EasySDK
5
+ module Kernel
6
+ module Util
7
+ class JsonUtil
8
+ # 完全复制PHP版本的toJsonString方法
9
+ def to_json_string(obj)
10
+ case obj
11
+ when Hash, Array
12
+ JSON.generate(obj)
13
+ when String
14
+ obj
15
+ else
16
+ obj.to_s
17
+ end
18
+ end
19
+
20
+ def self.from_json_string(json_string)
21
+ return nil if json_string.nil? || json_string.empty?
22
+ JSON.parse(json_string)
23
+ rescue JSON::ParserError
24
+ json_string
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,28 @@
1
+ module Alipay
2
+ module EasySDK
3
+ module Kernel
4
+ module Util
5
+ class ResponseChecker
6
+ def success?(response)
7
+ return true if response.respond_to?(:code) && response.code.to_s == '10000'
8
+
9
+ code_blank = !response.respond_to?(:code) || blank?(response.code)
10
+ sub_code_blank = !response.respond_to?(:sub_code) || blank?(response.sub_code)
11
+
12
+ code_blank && sub_code_blank
13
+ end
14
+
15
+ def success(response)
16
+ success?(response)
17
+ end
18
+
19
+ private
20
+
21
+ def blank?(value)
22
+ value.nil? || value.to_s.strip.empty?
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,50 @@
1
+ require_relative '../alipay_constants'
2
+
3
+ module Alipay
4
+ module EasySDK
5
+ module Kernel
6
+ module Util
7
+ class SignContentExtractor
8
+ def initialize
9
+ @response_suffix = "_response"
10
+ @error_response = "error_response"
11
+ end
12
+
13
+ # 完全复制PHP版本的getSignSourceData方法
14
+ def get_sign_source_data(body, method)
15
+ root_node_name = method.gsub(".", "_") + @response_suffix
16
+ root_index = body.index(root_node_name)
17
+ if root_index != body.rindex(root_node_name)
18
+ raise '检测到响应报文中有重复的' + root_node_name + ',验签失败。'
19
+ end
20
+ error_index = body.index(@error_response)
21
+ if root_index && root_index > 0
22
+ return parser_json_source(body, root_node_name, root_index)
23
+ elsif error_index && error_index > 0
24
+ return parser_json_source(body, @error_response, error_index)
25
+ else
26
+ return nil
27
+ end
28
+ end
29
+
30
+ # 完全复制PHP版本的parserJSONSource方法
31
+ def parser_json_source(response_content, node_name, node_index)
32
+ sign_data_start_index = node_index + node_name.length + 2
33
+ if response_content.include?(AlipayConstants::ALIPAY_CERT_SN_FIELD)
34
+ sign_index = response_content.rindex("\"" + AlipayConstants::ALIPAY_CERT_SN_FIELD + "\"")
35
+ else
36
+ sign_index = response_content.rindex("\"" + AlipayConstants::SIGN_FIELD + "\"")
37
+ end
38
+ # 签名前-逗号
39
+ sign_data_end_index = sign_index - 1
40
+ index_len = sign_data_end_index - sign_data_start_index
41
+ if index_len < 0
42
+ return nil
43
+ end
44
+ return response_content[sign_data_start_index, index_len]
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,125 @@
1
+ require 'base64'
2
+ require 'openssl'
3
+
4
+ module Alipay
5
+ module EasySDK
6
+ module Kernel
7
+ module Util
8
+ class Signer
9
+ # 完全复制PHP版本的sign方法
10
+ def sign(content, private_key_pem)
11
+ begin
12
+ pri_key = private_key_pem
13
+
14
+ # 完全按照PHP的逻辑:wordwrap($priKey, 64, "\n", true)
15
+ res = "-----BEGIN RSA PRIVATE KEY-----\n" +
16
+ wordwrap(pri_key, 64, "\n", true) +
17
+ "\n-----END RSA PRIVATE KEY-----"
18
+
19
+ if res.nil? || res.empty?
20
+ raise '您使用的私钥格式错误,请检查RSA私钥配置'
21
+ end
22
+
23
+ # 使用SHA256算法签名,完全模仿PHP的openssl_sign
24
+ private_key = OpenSSL::PKey::RSA.new(res)
25
+ digest = OpenSSL::Digest::SHA256.new
26
+ sign = private_key.sign(digest, content)
27
+
28
+ # 使用标准Base64编码,完全模仿PHP的base64_encode
29
+ Base64.strict_encode64(sign)
30
+ rescue
31
+ raise '您使用的私钥格式错误,请检查RSA私钥配置'
32
+ end
33
+ end
34
+
35
+ # 完全复制PHP版本的verify方法
36
+ def verify(content, sign, public_key_pem)
37
+ begin
38
+ pub_key = public_key_pem
39
+
40
+ # 完全按照PHP的逻辑:wordwrap($pubKey, 64, "\n", true)
41
+ res = "-----BEGIN PUBLIC KEY-----\n" +
42
+ wordwrap(pub_key, 64, "\n", true) +
43
+ "\n-----END PUBLIC KEY-----"
44
+
45
+ if res.nil? || res.empty?
46
+ raise '支付宝RSA公钥错误。请检查公钥文件格式是否正确'
47
+ end
48
+
49
+ # 调用openssl内置方法验签,完全模仿PHP的openssl_verify
50
+ public_key = OpenSSL::PKey::RSA.new(res)
51
+ digest = OpenSSL::Digest::SHA256.new
52
+ decoded_sign = Base64.decode64(sign)
53
+
54
+ result = public_key.verify(digest, decoded_sign, content)
55
+ return result
56
+ rescue
57
+ return false
58
+ end
59
+ end
60
+
61
+ def verify_params(parameters, public_key)
62
+ sign = parameters['sign']
63
+ content = get_sign_content(parameters)
64
+ return verify(content, sign, public_key)
65
+ end
66
+
67
+ def get_sign_content(params)
68
+ # 模拟PHP的ksort
69
+ sorted_params = params.sort_by { |k, _| k.to_s }.to_h
70
+
71
+ # 移除sign和sign_type字段
72
+ sorted_params.delete('sign')
73
+ sorted_params.delete('sign_type')
74
+
75
+ string_to_be_signed = ""
76
+ i = 0
77
+ sorted_params.each do |k, v|
78
+ if v.to_s[0] != "@"
79
+ if i == 0
80
+ string_to_be_signed += "#{k}=#{v}"
81
+ else
82
+ string_to_be_signed += "&#{k}=#{v}"
83
+ end
84
+ i += 1
85
+ end
86
+ end
87
+ return string_to_be_signed
88
+ end
89
+
90
+ private
91
+
92
+ # 完全复制PHP的wordwrap函数
93
+ def wordwrap(str, width = 75, break_str = "\n", cut = false)
94
+ if str.nil? || str.empty?
95
+ return str
96
+ end
97
+
98
+ if cut
99
+ # 如果cut为true,强制在指定宽度处断开
100
+ str.scan(/.{1,#{width}}/).join(break_str)
101
+ else
102
+ # 默认行为,不在单词中间断开
103
+ result = []
104
+ current_line = ""
105
+
106
+ str.split.each do |word|
107
+ if current_line.empty?
108
+ current_line = word
109
+ elsif current_line.length + 1 + word.length <= width
110
+ current_line += " " + word
111
+ else
112
+ result << current_line
113
+ current_line = word
114
+ end
115
+ end
116
+
117
+ result << current_line unless current_line.empty?
118
+ result.join(break_str)
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end