k3cloud-sdk 0.4.7 → 0.6.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.
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "k3cloud/errors/configuration_error"
4
+
3
5
  module K3cloud
4
6
  class Configuration
5
7
  # 服务Url地址(私有云必须配置金蝶云星空产品地址,K3Cloud/结尾。若为公有云则必须置空)
@@ -29,7 +31,26 @@ module K3cloud
29
31
  # 组织编码,启用多组织时配置对应的组织编码才有效
30
32
  attr_accessor :org_num
31
33
 
32
- attr_accessor :connect_timeout, :request_timeout
34
+ # 连接/请求超时(秒)。
35
+ # verify_ssl 仅对 https 生效(http URL 自动跳过 SSL 校验);
36
+ # 私有云 https 自签证书时可设为 false 关闭校验,默认 true。
37
+ attr_accessor :connect_timeout, :request_timeout, :verify_ssl
38
+
39
+ # 日志输出格式::text(默认,常规 Logger 格式)或 :json(每行一个 JSON)
40
+ attr_accessor :log_format
41
+
42
+ # 请求重试:对超时与 5xx 进行重试。retry_max=0 表示不重试
43
+ attr_accessor :retry_max, :retry_interval, :retry_backoff
44
+
45
+ DEFAULT_LCID = 2052
46
+ DEFAULT_CONNECT_TIMEOUT = 30
47
+ DEFAULT_REQUEST_TIMEOUT = 60
48
+ DEFAULT_LOG_FORMAT = :text
49
+ DEFAULT_RETRY_MAX = 3
50
+ DEFAULT_RETRY_INTERVAL = 1
51
+ DEFAULT_RETRY_BACKOFF = 2
52
+
53
+ REQUIRED_FIELDS = %i[acct_id user_name app_id app_secret].freeze
33
54
 
34
55
  def initialize(options = {})
35
56
  @acct_id = options[:acct_id]
@@ -39,14 +60,36 @@ module K3cloud
39
60
  @app_secret = options[:app_secret]
40
61
  @server_url = options[:server_url]
41
62
  @org_num = options[:org_num]
42
- @connect_timeout = options[:connect_timeout]
43
- @request_timeout = options[:request_timeout]
63
+ @lcid = options.fetch(:lcid, DEFAULT_LCID)
64
+ @connect_timeout = options.fetch(:connect_timeout, DEFAULT_CONNECT_TIMEOUT)
65
+ @request_timeout = options.fetch(:request_timeout, DEFAULT_REQUEST_TIMEOUT)
66
+ @verify_ssl = options.fetch(:verify_ssl, true)
67
+ @log_format = options.fetch(:log_format, DEFAULT_LOG_FORMAT)
68
+ @retry_max = options.fetch(:retry_max, DEFAULT_RETRY_MAX)
69
+ @retry_interval = options.fetch(:retry_interval, DEFAULT_RETRY_INTERVAL)
70
+ @retry_backoff = options.fetch(:retry_backoff, DEFAULT_RETRY_BACKOFF)
44
71
 
45
72
  yield(self) if block_given?
46
73
  end
47
74
 
48
- def self.default
49
- @@default ||= Configuration.new
75
+ # 校验必填字段。nil 视为未配置,空串视为兼容历史不阻断。
76
+ def validate!
77
+ missing = REQUIRED_FIELDS.select { |f| public_send(f).nil? }
78
+ return if missing.empty?
79
+
80
+ raise K3cloud::ConfigurationError,
81
+ "K3cloud configuration missing: #{missing.join(", ")}. Please set them in K3cloud.configure."
82
+ end
83
+
84
+ class << self
85
+ def default
86
+ @default ||= Configuration.new
87
+ end
88
+
89
+ # 测试或重新配置时重置默认配置
90
+ def reset_default!
91
+ @default = nil
92
+ end
50
93
  end
51
94
  end
52
95
  end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module K3cloud
4
+ # 配置缺失或非法时抛出
5
+ class ConfigurationError < StandardError; end
6
+ end
@@ -14,17 +14,22 @@ module K3cloud
14
14
  end
15
15
 
16
16
  def self.parse(json)
17
+ return nil if json.nil? || json.to_s.empty?
18
+
19
+ json = json.to_s
17
20
  index = json.index("{")
18
- json = json[index..-1] if index >= 0
21
+ json = json[index..-1] if index
19
22
 
20
23
  parsed_json = JSON.parse(json)
21
24
  kd_error = K3cloudError.new
22
25
  kd_error.message = parsed_json["Message"]
23
- kd_error.inner_ex_wrapper = K3cloudError.parse(parsed_json["InnerExWrapper"].to_json)
24
- kd_error.inner_exception = K3cloudError.parse(parsed_json["InnerException"].to_json)
26
+ inner_wrapper = parsed_json["InnerExWrapper"]
27
+ inner_exception = parsed_json["InnerException"]
28
+ kd_error.inner_ex_wrapper = inner_wrapper ? K3cloudError.parse(inner_wrapper.to_json) : nil
29
+ kd_error.inner_exception = inner_exception ? K3cloudError.parse(inner_exception.to_json) : nil
25
30
  kd_error
26
31
  rescue StandardError => e
27
- K3cloud.logger.error({ errmsg: "Failed to parse exception message. #{e.message}", backtrace: "#{e.backtrace}", type: 'error', lever: 'ERROR' })
32
+ K3cloud.logger.error("Failed to parse exception message: #{e.message}")
28
33
  nil
29
34
  end
30
35
  end
data/lib/k3cloud/http.rb CHANGED
@@ -9,51 +9,116 @@ require "json"
9
9
  module K3cloud
10
10
  # HTTP Client
11
11
  class Http
12
- attr_accessor :url, :header, :body, :connect_timeout, :request_timeout, :status_code
12
+ attr_accessor :url, :header, :body, :connect_timeout, :request_timeout, :verify_ssl, :status_code
13
+ attr_reader :retry_count
13
14
 
14
- def initialize(url, header, body, connect_timeout = 120, request_timeout = 120)
15
+ # retry_max/interval/backoff: 失败重试参数,仅对超时与 5xx 生效。
16
+ def initialize(
17
+ url:, header:, body:,
18
+ connect_timeout: Configuration::DEFAULT_CONNECT_TIMEOUT,
19
+ request_timeout: Configuration::DEFAULT_REQUEST_TIMEOUT, verify_ssl: true,
20
+ retry_max: 0, retry_interval: 1, retry_backoff: 2
21
+ )
15
22
  @url = url
16
23
  @header = header || {}
17
24
  @body = body
18
25
  @connect_timeout = connect_timeout
19
26
  @request_timeout = request_timeout
27
+ @verify_ssl = verify_ssl
28
+ @retry_max = retry_max.to_i
29
+ @retry_interval = retry_interval.to_f
30
+ @retry_backoff = retry_backoff.to_f
31
+ @retry_count = 0
20
32
  end
21
33
 
22
34
  def post
23
35
  uri = URI.parse(@url)
24
- http = Net::HTTP.new(uri.host, uri.port)
25
- # SSL/TLS configuration
26
- if uri.scheme == "https"
27
- http.use_ssl = true
28
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE
29
- end
36
+ http = build_http(uri)
37
+ request = build_request(uri)
38
+ body_str = perform_with_retry(http, request)
39
+ log_trace(body_str)
40
+ body_str
41
+ end
30
42
 
31
- http.open_timeout = @connect_timeout
32
- http.read_timeout = @request_timeout
33
- request = Net::HTTP::Post.new(uri.request_uri)
34
- request.initialize_http_header(@header)
35
- request["Content-Type"] = "application/json"
36
- request["User-Agent"] = generate_user_agent
37
- request.body = @body.is_a?(String) ? @body : @body.to_json
43
+ private
44
+
45
+ def perform_with_retry(http, request)
46
+ started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
47
+ begin
48
+ send_request(http, request)
49
+ rescue RetryableError => e
50
+ if @retry_count < @retry_max
51
+ @retry_count += 1
52
+ wait = @retry_interval * (@retry_backoff**(@retry_count - 1))
53
+ K3cloud.logger.debug("Request failed, retrying #{@retry_count}/#{@retry_max} " \
54
+ "after #{wait}s: #{e.message} url=#{@url}")
55
+ sleep(wait)
56
+ retry
57
+ end
58
+ raise K3cloud::ResponseError, "Request failed after #{@retry_count} retries: #{e.message}"
59
+ ensure
60
+ @last_elapsed_ms = elapsed_ms_since(started_at)
61
+ end
62
+ end
38
63
 
64
+ def send_request(http, request)
39
65
  response = http.request(request)
40
66
  @status_code = response.code.to_i
67
+ raise RetryableError, "server error status=#{@status_code}" if @status_code >= 500
68
+
41
69
  raise K3cloud::ResponseError, "status: #{response.code}, desc: #{response.body}" if @status_code >= 400
42
70
 
43
71
  response.body
72
+ rescue RetryableError, K3cloud::ResponseError
73
+ raise
44
74
  rescue Errno::ETIMEDOUT, Net::OpenTimeout, Net::ReadTimeout => e
45
- raise K3cloud::ResponseError, "Request timed out: #{e.message}"
75
+ raise RetryableError, "timeout: #{e.message}"
46
76
  rescue Net::HTTPServerError => e
47
- raise K3cloud::ResponseError, "Server error: #{e.message}"
48
- rescue => e
77
+ raise RetryableError, "server error: #{e.message}"
78
+ rescue StandardError => e
49
79
  raise K3cloud::ResponseError, "Unexpected error: #{e.message}"
50
80
  end
51
81
 
52
- private
82
+ def build_request(uri)
83
+ request = Net::HTTP::Post.new(uri.request_uri)
84
+ request.initialize_http_header(@header)
85
+ request["Content-Type"] = "application/json"
86
+ request["User-Agent"] = generate_user_agent
87
+ request.body = @body.is_a?(String) ? @body : @body.to_json
88
+ request
89
+ end
90
+
91
+ def build_http(uri)
92
+ http = Net::HTTP.new(uri.host, uri.port)
93
+ configure_ssl!(http) if uri.scheme == "https"
94
+ http.open_timeout = @connect_timeout
95
+ http.read_timeout = @request_timeout
96
+ http.write_timeout = @request_timeout if http.respond_to?(:write_timeout=)
97
+ http.ssl_timeout = @request_timeout if http.respond_to?(:ssl_timeout=) && uri.scheme == "https"
98
+ http
99
+ end
100
+
101
+ # 仅 https 生效。http URL 不调用此方法,verify_ssl 配置对其无意义。
102
+ def configure_ssl!(http)
103
+ http.use_ssl = true
104
+ http.verify_mode = @verify_ssl ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
105
+ end
106
+
107
+ def log_trace(body_str)
108
+ size = body_str ? body_str.bytesize : 0
109
+ K3cloud.logger.debug("HTTP completed POST #{@url} status=#{@status_code} \
110
+ elapsed_ms=#{(@last_elapsed_ms || 0).round(2)} retries=#{@retry_count} resp_bytes=#{size}")
111
+ end
112
+
113
+ def elapsed_ms_since(started_at)
114
+ (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000.0
115
+ end
53
116
 
54
- # Generate User-Agent string with client info
55
117
  def generate_user_agent
56
118
  "Kingdee/Ruby WebApi SDK(v#{K3cloud::VERSION}) (Ruby #{RUBY_VERSION}; #{RUBY_PLATFORM})"
57
119
  end
120
+
121
+ # 内部异常:可重试的瞬时错误(超时/5xx),区别于最终抛出的 ResponseError。
122
+ class RetryableError < StandardError; end
58
123
  end
59
124
  end
@@ -46,6 +46,24 @@ module K3cloud
46
46
  handle_query_result(rows)
47
47
  end
48
48
 
49
+ # 分页拉取全部单据。data 为基础查询参数(FormId/FieldKeys/FilterString 等),
50
+ # 自动注入 StartRow与 Limit,按分页连续请求直到返回行数少于 limit。
51
+ # 返回合并后的二维数组。limit 建议不超过 1000。
52
+ def execute_bill_query_all(data, limit: 1000)
53
+ base = data.dup
54
+ all = []
55
+ start_row = 0
56
+ loop do
57
+ page = base.merge("StartRow" => start_row, "Limit" => limit)
58
+ rows = execute_bill_query(page)
59
+ all.concat(rows)
60
+ break if rows.nil? || rows.size < limit
61
+
62
+ start_row += limit
63
+ end
64
+ all
65
+ end
66
+
49
67
  # 删除
50
68
  def delete(form_id, data)
51
69
  execute("Kingdee.BOS.WebApi.ServicesStub.DynamicFormService.Delete", [form_id, data])
@@ -135,13 +153,34 @@ module K3cloud
135
153
 
136
154
  private
137
155
 
138
- def handle_query_result(rows)
139
- if !rows[0].nil? && !rows[0].empty? && (result = rows[0][0]).is_a?(Hash)
140
- K3cloud.logger.error({ errmsg: result, type: 'error', lever: 'ERROR' })
141
- []
142
- else
143
- rows
156
+ # 金蝶 ExecuteBillQuery 正常返回二维数组;出错时返回值结构不一:
157
+ # 可能是首行首列放置一个含 Message/InnerEx Hash,也可能是顶层 Hash
158
+ # 这里在多种形态下识别错误,避免把错误对象当作业务数据返回。
159
+ def handle_query_result(result)
160
+ return result if result.nil? || (result.respond_to?(:empty?) && result.empty?)
161
+
162
+ if error_result?(result)
163
+ K3cloud.logger.error("ExecuteBillQuery error: #{result.inspect}")
164
+ return []
144
165
  end
166
+ result
167
+ end
168
+
169
+ def error_result?(result)
170
+ return true if error_hash?(result)
171
+
172
+ first_row = result.is_a?(Array) ? result[0] : nil
173
+ return false unless first_row.is_a?(Array)
174
+
175
+ candidate = first_row.find { |cell| cell.is_a?(Hash) }
176
+ candidate && error_hash?(candidate)
177
+ end
178
+
179
+ def error_hash?(obj)
180
+ return false unless obj.is_a?(Hash)
181
+
182
+ # 错误对象通常含 Message / InnerExWrapper / InnerException 等字段
183
+ ERROR_MARKERS.any? { |k| obj.key?(k) }
145
184
  end
146
185
  end
147
186
  end
@@ -2,18 +2,16 @@
2
2
 
3
3
  require "base64"
4
4
 
5
- module Base64Utils
6
- def self.encoding_to_base64(buffer)
7
- b64buffer = Base64.strict_encode64(buffer.pack("C*"))
8
- b64buffer.force_encoding("UTF-8")
9
- rescue StandardError => e
10
- raise e
11
- end
5
+ module K3cloud
6
+ module Base64Utils
7
+ def self.encoding_to_base64(buffer)
8
+ b64buffer = Base64.strict_encode64(buffer.pack("C*"))
9
+ b64buffer.force_encoding("UTF-8")
10
+ end
12
11
 
13
- def self.decoding_from_base64(base64)
14
- buffer = base64.encode("UTF-8")
15
- Base64.strict_decode64(buffer).bytes
16
- rescue StandardError => e
17
- raise e
12
+ def self.decoding_from_base64(base64)
13
+ buffer = base64.encode("UTF-8")
14
+ Base64.strict_decode64(buffer).bytes
15
+ end
18
16
  end
19
17
  end
@@ -1,43 +1,45 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module ConstDefine
4
- X_API_CLIENT_ID = "X-Api-ClientID"
3
+ module K3cloud
4
+ module ConstDefine
5
+ X_API_CLIENT_ID = "X-Api-ClientID"
5
6
 
6
- X_API_AUTH_VERSION = "X-Api-Auth-Version"
7
+ X_API_AUTH_VERSION = "X-Api-Auth-Version"
7
8
 
8
- X_API_TIMESTAMP = "x-api-timestamp"
9
+ X_API_TIMESTAMP = "x-api-timestamp"
9
10
 
10
- X_API_NONCE = "x-api-nonce"
11
+ X_API_NONCE = "x-api-nonce"
11
12
 
12
- X_API_SIGN_HEADERS = "x-api-signheaders"
13
+ X_API_SIGN_HEADERS = "x-api-signheaders"
13
14
 
14
- X_API_SIGNATURE = "X-Api-Signature"
15
+ X_API_SIGNATURE = "X-Api-Signature"
15
16
 
16
- X_KD_APP_KEY = "X-Kd-Appkey"
17
+ X_KD_APP_KEY = "X-Kd-Appkey"
17
18
 
18
- X_KD_APP_DATA = "X-Kd-Appdata"
19
+ X_KD_APP_DATA = "X-Kd-Appdata"
19
20
 
20
- X_KD_SIGNATURE = "X-Kd-Signature"
21
+ X_KD_SIGNATURE = "X-Kd-Signature"
21
22
 
22
- KD_SERVICE_SESSIONID = "kdservice-sessionid"
23
+ KD_SERVICE_SESSIONID = "kdservice-sessionid"
23
24
 
24
- SESSION_ID = "ASP.NET_SessionId"
25
+ SESSION_ID = "ASP.NET_SessionId"
25
26
 
26
- X_KD_SID = "SID"
27
+ X_KD_SID = "SID"
27
28
 
28
- COOKIE_SET = "Set-Cookie"
29
+ COOKIE_SET = "Set-Cookie"
29
30
 
30
- BEGIN_METHOD_HEADER = "beginmethod"
31
+ BEGIN_METHOD_HEADER = "beginmethod"
31
32
 
32
- QUERY_METHOD_HEADER = "querymethod"
33
+ QUERY_METHOD_HEADER = "querymethod"
33
34
 
34
- BEGIN_METHOD_METHOD = "BeginQueryImpl"
35
+ BEGIN_METHOD_METHOD = "BeginQueryImpl"
35
36
 
36
- QUERY_METHOD_METHOD = "QueryAsyncResult"
37
+ QUERY_METHOD_METHOD = "QueryAsyncResult"
37
38
 
38
- QUERY_INVODE_STATE_PENDING = 0
39
+ QUERY_INVODE_STATE_PENDING = 0
39
40
 
40
- QUERY_INVODE_STATE_RUNNING = 1
41
+ QUERY_INVODE_STATE_RUNNING = 1
41
42
 
42
- QUERY_INVODE_STATE_COMPLETE = 2
43
+ QUERY_INVODE_STATE_COMPLETE = 2
44
+ end
43
45
  end
@@ -3,33 +3,26 @@
3
3
  require "digest"
4
4
  require "base64"
5
5
 
6
- module MD5Utils
7
- def self.encrypt(data_str)
8
- m = Digest::MD5.new
9
- m.update(data_str.encode("UTF-8"))
10
- s = m.digest
11
- result = ""
6
+ module K3cloud
7
+ module MD5Utils
8
+ UNSECURE_KEY = "0054f397c6234378b09ca7d3e5debce7"
12
9
 
13
- s.each_byte do |byte|
14
- result += (byte & 0xFF | -256).to_s(16)[6..-1]
10
+ # 返回 32 位小写十六进制 MD5。原来的逐字节位运算 hack 在 Ruby 下会得到 `nil`,
11
+ # 且被 rescue 吞掉返回空串,故直接使用 Digest::MD5.hexdigest。
12
+ def self.encrypt(data_str)
13
+ Digest::MD5.hexdigest(data_str.encode("UTF-8"))
15
14
  end
16
15
 
17
- result
18
- rescue StandardError => e
19
- e.backtrace
20
- ""
21
- end
16
+ def self.hash_mac(data, secret)
17
+ raise ArgumentError, "secret is required for HMAC" if secret.nil? || secret.to_s.empty?
22
18
 
23
- def self.hash_mac(data, secret)
24
- kd_mac = OpenSSL::HMAC.digest(OpenSSL::Digest.new("sha256"), secret, data)
25
- hex_string = bytes_to_hex(kd_mac.bytes)
26
- Base64.strict_encode64(hex_string)
27
- rescue StandardError => e
28
- e.backtrace
29
- nil
30
- end
19
+ kd_mac = OpenSSL::HMAC.digest(OpenSSL::Digest.new("sha256"), secret, data)
20
+ hex_string = bytes_to_hex(kd_mac.bytes)
21
+ Base64.strict_encode64(hex_string)
22
+ end
31
23
 
32
- def self.bytes_to_hex(bytes)
33
- bytes.map { |byte| byte.to_s(16).rjust(2, "0") }.join
24
+ def self.bytes_to_hex(bytes)
25
+ bytes.map { |byte| byte.to_s(16).rjust(2, "0") }.join
26
+ end
34
27
  end
35
28
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module K3cloud
4
- VERSION = "0.4.7"
4
+ VERSION = "0.6.0"
5
5
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "cgi"
3
4
  require "k3cloud/errors/k3cloud_error"
5
+ require "k3cloud/errors/configuration_error"
4
6
  require "k3cloud/errors/response_error"
5
7
  require "k3cloud/utils/md5_utils"
6
8
  require "k3cloud/utils/base64_utils"
@@ -10,19 +12,29 @@ require "k3cloud/http"
10
12
 
11
13
  module K3cloud
12
14
  class WebApiClient
15
+ # 金蝶 ExecuteBillQuery/ResponseError 中标识错误的关键字
16
+ ERROR_MARKERS = %w[Message InnerExWrapper InnerException].freeze
17
+
18
+ # 金蝶 app_id 内嵌 secret 的 XOR 密钥,协议固定值,来源见 README。
19
+ # https://vip.kingdee.com/article/163905337846351872?productLineId=1
20
+ XOR_SEC_KEY = "0054f397c6234378b09ca7d3e5debce7"
21
+
13
22
  attr_accessor :config
14
23
 
15
24
  def initialize(config = Configuration.default)
16
25
  @config = config
26
+ @validated = false
17
27
  end
18
28
 
19
29
  def execute(service_name, parameters)
30
+ ensure_configured!
20
31
  result = execute_json(service_name, parameters)
21
32
  if result.start_with?("response_error:")
22
33
  error = K3cloudError.parse(result)
23
34
  raise K3cloud::ResponseError, result if error.nil?
24
35
 
25
- message = error.inner_ex_wrapper.nil? ? error.message : "#{error.message} --> #{error.inner_ex_wrapper&.message}"
36
+ inner_msg = error.inner_ex_wrapper&.message
37
+ message = inner_msg ? "#{error.message} --> #{inner_msg}" : error.message
26
38
  raise K3cloud::ResponseError, message
27
39
 
28
40
  else
@@ -36,34 +48,54 @@ module K3cloud
36
48
 
37
49
  private
38
50
 
51
+ def ensure_configured!
52
+ return if @validated
53
+
54
+ @config.validate!
55
+ @validated = true
56
+ end
57
+
39
58
  def execute_json(service_name, parameters)
40
59
  url = build_url(service_name)
41
60
  K3cloud.logger.info("Request URL: #{url}")
42
61
 
43
62
  header = build_header(url_path(url))
44
63
  body = { parameters: parameters }
45
- request = Http.new(url, header, body, @config.connect_timeout, @config.request_timeout)
64
+ request = Http.new(
65
+ url: url,
66
+ header: header,
67
+ body: body,
68
+ connect_timeout: @config.connect_timeout,
69
+ request_timeout: @config.request_timeout,
70
+ verify_ssl: @config.verify_ssl,
71
+ retry_max: @config.retry_max,
72
+ retry_interval: @config.retry_interval,
73
+ retry_backoff: @config.retry_backoff
74
+ )
46
75
  request.post
47
76
  end
48
77
 
49
78
  def build_url(service_name)
50
- base_url = if @config.server_url.nil? || @config.server_url.empty?
51
- "https://api.kingdee.com/galaxyapi/"
52
- else
53
- @config.server_url.to_s.strip.chomp("/")
54
- end
55
79
  "#{base_url}/#{service_name}.common.kdsvc"
56
80
  end
57
81
 
58
- def url_path(url)
59
- if url.start_with?("http")
60
- index = url.index("/", 10)
61
- index > -1 ? url[index..-1] : url
82
+ def base_url
83
+ server = @config.server_url
84
+ if server.nil? || server.empty?
85
+ "https://api.kingdee.com/galaxyapi"
62
86
  else
63
- url
87
+ server.to_s.strip.chomp("/")
64
88
  end
65
89
  end
66
90
 
91
+ # 仅 https/http 的 URL 才能切片出签名用 path,否则说明 URL 构造异常。
92
+ def url_path(url)
93
+ raise ArgumentError, "Unsupported URL scheme: #{url.inspect}" unless url.start_with?("http")
94
+
95
+ index = url.index("/", 10)
96
+ index ? url[index..-1] : "/"
97
+ end
98
+
67
99
  # @note: https://vip.kingdee.com/knowledge/specialDetail/229961573895771136?category=229964512944566016&id=423060878259269120&productLineId=1
68
100
  def build_header(url)
69
101
  header = {}
@@ -94,9 +126,6 @@ module K3cloud
94
126
  header[ConstDefine::X_KD_SIGNATURE] = kd_signature
95
127
  end
96
128
  header
97
- rescue StandardError => e
98
- K3cloud.logger.error({ errmsg: "Build header exception.", backtrace: "#{e.backtrace}", type: 'error', lever: 'ERROR' })
99
- {}
100
129
  end
101
130
 
102
131
  def decode_sec(sec)
@@ -106,8 +135,7 @@ module K3cloud
106
135
  end
107
136
 
108
137
  def x_or_sec(buffer)
109
- sec_key = "0054f397c6234378b09ca7d3e5debce7"
110
- pwd = sec_key.encode("UTF-8").bytes
138
+ pwd = XOR_SEC_KEY.encode("UTF-8").bytes
111
139
 
112
140
  buffer.each_with_index do |byte, i|
113
141
  buffer[i] = byte ^ pwd[i % pwd.length]