jpsclient 0.2.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/LICENSE +21 -0
- data/bin/jpsclient +28 -0
- data/lib/jpsclient/api/app_level.rb +138 -0
- data/lib/jpsclient/api/app_resource.rb +164 -0
- data/lib/jpsclient/api/app_resource_version.rb +52 -0
- data/lib/jpsclient/api/apple_account.rb +140 -0
- data/lib/jpsclient/api/application.rb +268 -0
- data/lib/jpsclient/api/application_category.rb +120 -0
- data/lib/jpsclient/api/application_design.rb +118 -0
- data/lib/jpsclient/api/application_income.rb +54 -0
- data/lib/jpsclient/api/application_sales.rb +52 -0
- data/lib/jpsclient/api/application_version.rb +77 -0
- data/lib/jpsclient/api/assets_category.rb +120 -0
- data/lib/jpsclient/api/bug.rb +140 -0
- data/lib/jpsclient/api/cert.rb +27 -0
- data/lib/jpsclient/api/client.rb +268 -0
- data/lib/jpsclient/api/collect.rb +52 -0
- data/lib/jpsclient/api/collection.rb +162 -0
- data/lib/jpsclient/api/commit_log.rb +104 -0
- data/lib/jpsclient/api/creative.rb +52 -0
- data/lib/jpsclient/api/custom_application.rb +230 -0
- data/lib/jpsclient/api/custom_application_web.rb +52 -0
- data/lib/jpsclient/api/document_text.rb +30 -0
- data/lib/jpsclient/api/experience.rb +143 -0
- data/lib/jpsclient/api/experience_category.rb +97 -0
- data/lib/jpsclient/api/fgui_export.rb +52 -0
- data/lib/jpsclient/api/file.rb +84 -0
- data/lib/jpsclient/api/game_assets.rb +140 -0
- data/lib/jpsclient/api/healthy.rb +30 -0
- data/lib/jpsclient/api/icon_and_snapshot.rb +54 -0
- data/lib/jpsclient/api/idea.rb +121 -0
- data/lib/jpsclient/api/jssdk.rb +30 -0
- data/lib/jpsclient/api/lark_bitable.rb +30 -0
- data/lib/jpsclient/api/lark_chat_group.rb +30 -0
- data/lib/jpsclient/api/lark_comment.rb +118 -0
- data/lib/jpsclient/api/lark_task.rb +140 -0
- data/lib/jpsclient/api/lark_task_list.rb +118 -0
- data/lib/jpsclient/api/lark_task_section.rb +74 -0
- data/lib/jpsclient/api/lark_user.rb +30 -0
- data/lib/jpsclient/api/lark_wiki_node.rb +30 -0
- data/lib/jpsclient/api/lark_wiki_space.rb +30 -0
- data/lib/jpsclient/api/lazy_client.rb +290 -0
- data/lib/jpsclient/api/login.rb +96 -0
- data/lib/jpsclient/api/m3u8.rb +30 -0
- data/lib/jpsclient/api/menu.rb +143 -0
- data/lib/jpsclient/api/modular_client.rb +228 -0
- data/lib/jpsclient/api/project.rb +46 -0
- data/lib/jpsclient/api/project_package.rb +249 -0
- data/lib/jpsclient/api/publisher.rb +165 -0
- data/lib/jpsclient/api/publisher_category.rb +142 -0
- data/lib/jpsclient/api/publisher_group.rb +118 -0
- data/lib/jpsclient/api/publisher_group_category.rb +120 -0
- data/lib/jpsclient/api/requirements.rb +143 -0
- data/lib/jpsclient/api/requirements_category.rb +97 -0
- data/lib/jpsclient/api/resource_category.rb +120 -0
- data/lib/jpsclient/api/role.rb +165 -0
- data/lib/jpsclient/api/simple_search.rb +162 -0
- data/lib/jpsclient/api/sketch.rb +74 -0
- data/lib/jpsclient/api/sketch_category.rb +97 -0
- data/lib/jpsclient/api/sov.rb +30 -0
- data/lib/jpsclient/api/statistics.rb +30 -0
- data/lib/jpsclient/api/store.rb +30 -0
- data/lib/jpsclient/api/survey.rb +143 -0
- data/lib/jpsclient/api/survey_category.rb +97 -0
- data/lib/jpsclient/api/tag.rb +142 -0
- data/lib/jpsclient/api/tool.rb +121 -0
- data/lib/jpsclient/api/tool_category.rb +120 -0
- data/lib/jpsclient/api/trending.rb +30 -0
- data/lib/jpsclient/api/ud_id.rb +118 -0
- data/lib/jpsclient/api/user.rb +99 -0
- data/lib/jpsclient/api/util.rb +30 -0
- data/lib/jpsclient/api/video_cover.rb +30 -0
- data/lib/jpsclient/api/webhook.rb +118 -0
- data/lib/jpsclient/api/workflow.rb +118 -0
- data/lib/jpsclient/auth/auth.rb +676 -0
- data/lib/jpsclient/auth/token.rb +209 -0
- data/lib/jpsclient/base/api_config.rb +225 -0
- data/lib/jpsclient/base/exception.rb +5 -0
- data/lib/jpsclient/http/http_client.rb +148 -0
- data/lib/jpsclient/upload/upload_client.rb +334 -0
- data/lib/jpsclient/upload/upload_config.rb +128 -0
- data/lib/jpsclient/upload/upload_progress.rb +73 -0
- data/lib/jpsclient/utils/aes.rb +49 -0
- data/lib/jpsclient/utils/logger.rb +38 -0
- data/lib/jpsclient/version.rb +3 -0
- data/lib/jpsclient.rb +34 -0
- metadata +269 -0
@@ -0,0 +1,209 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'net/http'
|
6
|
+
require 'uri'
|
7
|
+
require 'jpsclient/utils/aes'
|
8
|
+
require 'jpsclient/base/exception'
|
9
|
+
require 'jpsclient/utils/logger'
|
10
|
+
|
11
|
+
module JPSClient
|
12
|
+
|
13
|
+
# Token 管理类
|
14
|
+
class Token
|
15
|
+
attr_reader :token, :username, :expires_at
|
16
|
+
|
17
|
+
def initialize(config)
|
18
|
+
@config = config
|
19
|
+
@aes_key = config.aes_key if config
|
20
|
+
|
21
|
+
# 只使用新的 token 文件
|
22
|
+
@token_dir = File.expand_path('~/.pindo')
|
23
|
+
@token_file = File.join(@token_dir, '.jpstoken')
|
24
|
+
|
25
|
+
# token 数据
|
26
|
+
@token = nil
|
27
|
+
@username = nil
|
28
|
+
@expires_at = nil
|
29
|
+
@created_at = nil
|
30
|
+
|
31
|
+
# 调试模式
|
32
|
+
@verbose = ENV['PINDO_DEBUG'] == 'true'
|
33
|
+
end
|
34
|
+
|
35
|
+
# 加载 token
|
36
|
+
def load
|
37
|
+
return false unless File.exist?(@token_file)
|
38
|
+
|
39
|
+
begin
|
40
|
+
file_content = File.read(@token_file)
|
41
|
+
|
42
|
+
# 根据是否有 AES 密钥决定解密方式
|
43
|
+
token_data = if @aes_key
|
44
|
+
begin
|
45
|
+
aes = AES.new(@aes_key)
|
46
|
+
decrypted = aes.decrypt(file_content)
|
47
|
+
JSON.parse(decrypted)
|
48
|
+
rescue => e
|
49
|
+
# 解密失败,可能是明文,尝试直接解析
|
50
|
+
puts "尝试解密失败,作为明文读取: #{e.message}" if @verbose
|
51
|
+
JSON.parse(file_content)
|
52
|
+
end
|
53
|
+
else
|
54
|
+
JSON.parse(file_content)
|
55
|
+
end
|
56
|
+
|
57
|
+
@token = token_data['token']
|
58
|
+
@username = token_data['username']
|
59
|
+
@expires_at = token_data['expires_at']
|
60
|
+
@created_at = token_data['created_at']
|
61
|
+
|
62
|
+
return true if @token
|
63
|
+
rescue => e
|
64
|
+
puts "读取 token 失败: #{e.message}" if @verbose
|
65
|
+
clear_corrupted_file
|
66
|
+
end
|
67
|
+
|
68
|
+
false
|
69
|
+
end
|
70
|
+
|
71
|
+
# 保存 token
|
72
|
+
def save(token, username, expires_at)
|
73
|
+
return false unless token
|
74
|
+
|
75
|
+
@token = token
|
76
|
+
@username = username
|
77
|
+
@expires_at = expires_at
|
78
|
+
@created_at = Time.now.to_i
|
79
|
+
|
80
|
+
# 确保目录存在
|
81
|
+
FileUtils.mkdir_p(@token_dir) unless Dir.exist?(@token_dir)
|
82
|
+
|
83
|
+
token_data = {
|
84
|
+
'token' => @token,
|
85
|
+
'username' => @username,
|
86
|
+
'expires_at' => @expires_at,
|
87
|
+
'created_at' => @created_at
|
88
|
+
}
|
89
|
+
|
90
|
+
# 根据是否有 AES 密钥决定加密方式
|
91
|
+
content = if @aes_key
|
92
|
+
aes = AES.new(@aes_key)
|
93
|
+
aes.encrypt(token_data.to_json)
|
94
|
+
else
|
95
|
+
token_data.to_json
|
96
|
+
end
|
97
|
+
|
98
|
+
File.write(@token_file, content)
|
99
|
+
puts "✓ Token 已保存到 #{@token_file}" if @verbose
|
100
|
+
|
101
|
+
true
|
102
|
+
rescue => e
|
103
|
+
puts "保存 token 失败: #{e.message}"
|
104
|
+
false
|
105
|
+
end
|
106
|
+
|
107
|
+
# 验证 token 有效性
|
108
|
+
def valid?
|
109
|
+
return false unless @token
|
110
|
+
|
111
|
+
# 1. 检查本地时间过期
|
112
|
+
if expired?
|
113
|
+
puts "Token 已过期 (本地时间检查)" if @verbose
|
114
|
+
return false
|
115
|
+
end
|
116
|
+
|
117
|
+
# 2. 可选:API 验证
|
118
|
+
# 为了避免频繁调用,只在接近过期时验证
|
119
|
+
if should_verify_with_api?
|
120
|
+
return verify_with_api
|
121
|
+
end
|
122
|
+
|
123
|
+
true
|
124
|
+
end
|
125
|
+
|
126
|
+
# 检查是否过期
|
127
|
+
def expired?
|
128
|
+
return true unless @expires_at
|
129
|
+
Time.now.to_i > @expires_at
|
130
|
+
end
|
131
|
+
|
132
|
+
# API 验证 token
|
133
|
+
def verify_with_api
|
134
|
+
return false unless @token && @config
|
135
|
+
|
136
|
+
begin
|
137
|
+
# 使用配置中的验证端点
|
138
|
+
base_url = @config.api_endpoint.split('/api/')[0] # 获取基础 URL
|
139
|
+
verify_endpoint = @config.respond_to?(:token_verify_endpoint) ? @config.token_verify_endpoint : '/api/user/profile'
|
140
|
+
uri = URI("#{base_url}#{verify_endpoint}")
|
141
|
+
|
142
|
+
request = Net::HTTP::Get.new(uri)
|
143
|
+
request['Authorization'] = "Bearer #{@token}"
|
144
|
+
|
145
|
+
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
146
|
+
http.request(request)
|
147
|
+
end
|
148
|
+
|
149
|
+
if response.code == '200'
|
150
|
+
puts "Token API 验证成功" if @verbose
|
151
|
+
return true
|
152
|
+
else
|
153
|
+
puts "Token API 验证失败: #{response.code}" if @verbose
|
154
|
+
return false
|
155
|
+
end
|
156
|
+
rescue => e
|
157
|
+
puts "Token API 验证出错: #{e.message}" if @verbose
|
158
|
+
# API 验证失败时,回退到本地验证
|
159
|
+
return !expired?
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# 清除 token
|
164
|
+
def clear
|
165
|
+
@token = nil
|
166
|
+
@username = nil
|
167
|
+
@expires_at = nil
|
168
|
+
@created_at = nil
|
169
|
+
|
170
|
+
FileUtils.rm_f(@token_file) if File.exist?(@token_file)
|
171
|
+
puts "✓ Token 已清除" if @verbose
|
172
|
+
end
|
173
|
+
|
174
|
+
# 转换为 Hash(兼容旧代码)
|
175
|
+
def to_h
|
176
|
+
return nil unless @token
|
177
|
+
|
178
|
+
{
|
179
|
+
'token' => @token,
|
180
|
+
'username' => @username,
|
181
|
+
'expires_at' => @expires_at
|
182
|
+
}
|
183
|
+
end
|
184
|
+
|
185
|
+
# 从 Auth 实例更新 token
|
186
|
+
def update_from_auth(auth)
|
187
|
+
return false unless auth.access_token
|
188
|
+
|
189
|
+
save(auth.access_token, auth.username, auth.expires_at)
|
190
|
+
end
|
191
|
+
|
192
|
+
private
|
193
|
+
|
194
|
+
# 是否应该通过 API 验证
|
195
|
+
def should_verify_with_api?
|
196
|
+
return false unless @expires_at
|
197
|
+
|
198
|
+
# 策略:最后 24 小时内进行 API 验证
|
199
|
+
remaining_time = @expires_at - Time.now.to_i
|
200
|
+
remaining_time > 0 && remaining_time < 24 * 60 * 60
|
201
|
+
end
|
202
|
+
|
203
|
+
# 清除损坏的文件
|
204
|
+
def clear_corrupted_file
|
205
|
+
FileUtils.rm_f(@token_file) if File.exist?(@token_file)
|
206
|
+
puts "已清除损坏的 token 文件" if @verbose
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
@@ -0,0 +1,225 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'jpsclient/base/exception'
|
3
|
+
|
4
|
+
module JPSClient
|
5
|
+
# API 配置管理类
|
6
|
+
class ApiConfig
|
7
|
+
attr_reader :endpoints, :base_url
|
8
|
+
|
9
|
+
def initialize(config_json)
|
10
|
+
@base_url = config_json["pgyerapps_base_url"]
|
11
|
+
|
12
|
+
# 兼容旧配置格式
|
13
|
+
if config_json["api_path_config"]
|
14
|
+
@legacy_config = config_json["api_path_config"]
|
15
|
+
@endpoints = convert_legacy_config(@legacy_config)
|
16
|
+
elsif config_json["api_endpoints"]
|
17
|
+
@endpoints = config_json["api_endpoints"]
|
18
|
+
else
|
19
|
+
raise ExceptionError, "API配置格式错误:缺少api_path_config或api_endpoints"
|
20
|
+
end
|
21
|
+
|
22
|
+
@error_codes = config_json["error_codes"] || default_error_codes
|
23
|
+
@rate_limits = config_json["rate_limits"] || default_rate_limits
|
24
|
+
end
|
25
|
+
|
26
|
+
# 获取API端点信息
|
27
|
+
def get_endpoint(category, action)
|
28
|
+
if @legacy_config
|
29
|
+
# 兼容旧格式
|
30
|
+
path = @legacy_config["#{category}_#{action}"] || @legacy_config[action]
|
31
|
+
return nil unless path
|
32
|
+
|
33
|
+
{
|
34
|
+
"path" => path,
|
35
|
+
"method" => guess_method(action),
|
36
|
+
"requires_auth" => !action.include?("login"),
|
37
|
+
"description" => "#{category}.#{action}"
|
38
|
+
}
|
39
|
+
else
|
40
|
+
# 新格式
|
41
|
+
@endpoints.dig(category.to_s, action.to_s)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# 获取完整的API URL
|
46
|
+
def get_url(category, action)
|
47
|
+
endpoint = get_endpoint(category, action)
|
48
|
+
return nil unless endpoint
|
49
|
+
|
50
|
+
"#{@base_url}#{endpoint['path']}"
|
51
|
+
end
|
52
|
+
|
53
|
+
# 获取API方法
|
54
|
+
def get_method(category, action)
|
55
|
+
endpoint = get_endpoint(category, action)
|
56
|
+
endpoint ? endpoint['method'] : 'GET'
|
57
|
+
end
|
58
|
+
|
59
|
+
# 是否需要认证
|
60
|
+
def requires_auth?(category, action)
|
61
|
+
endpoint = get_endpoint(category, action)
|
62
|
+
endpoint ? endpoint.fetch('requires_auth', true) : true
|
63
|
+
end
|
64
|
+
|
65
|
+
# 获取参数定义
|
66
|
+
def get_parameters(category, action)
|
67
|
+
endpoint = get_endpoint(category, action)
|
68
|
+
endpoint ? endpoint.fetch('parameters', {}) : {}
|
69
|
+
end
|
70
|
+
|
71
|
+
# 验证参数
|
72
|
+
def validate_params(category, action, params)
|
73
|
+
param_definitions = get_parameters(category, action)
|
74
|
+
errors = []
|
75
|
+
|
76
|
+
# 检查必需参数
|
77
|
+
param_definitions.each do |name, definition|
|
78
|
+
if definition['required'] && !params.key?(name.to_sym) && !params.key?(name.to_s)
|
79
|
+
errors << "缺少必需参数: #{name}"
|
80
|
+
end
|
81
|
+
|
82
|
+
# 类型检查(如果提供了参数)
|
83
|
+
value = params[name.to_sym] || params[name.to_s]
|
84
|
+
if value && definition['type']
|
85
|
+
unless check_type(value, definition['type'])
|
86
|
+
errors << "参数 #{name} 类型错误,期望 #{definition['type']}"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# 枚举值检查
|
91
|
+
if value && definition['enum'] && !definition['enum'].include?(value)
|
92
|
+
errors << "参数 #{name} 值无效,必须是: #{definition['enum'].join(', ')}"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
errors
|
97
|
+
end
|
98
|
+
|
99
|
+
# 获取错误描述
|
100
|
+
def get_error_message(code)
|
101
|
+
@error_codes[code.to_s] || "未知错误 (#{code})"
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
# 将旧配置格式转换为新格式
|
107
|
+
def convert_legacy_config(legacy)
|
108
|
+
converted = {}
|
109
|
+
|
110
|
+
legacy.each do |key, value|
|
111
|
+
# 处理新格式(包含 http_method 和 url)
|
112
|
+
if value.is_a?(Hash) && value['url']
|
113
|
+
endpoint = {
|
114
|
+
"path" => value['url'],
|
115
|
+
"method" => value['http_method'] || guess_method(key),
|
116
|
+
"alias" => value['alias'] || key,
|
117
|
+
"description" => value['description'],
|
118
|
+
"requires_auth" => !key.include?('login') && !key.include?('send_code')
|
119
|
+
}
|
120
|
+
|
121
|
+
# 根据 key 分类到不同的组
|
122
|
+
category, action = categorize_endpoint(key)
|
123
|
+
converted[category] ||= {}
|
124
|
+
converted[category][action] = endpoint
|
125
|
+
# 处理旧格式(只有路径字符串)
|
126
|
+
elsif value.is_a?(String)
|
127
|
+
endpoint = build_endpoint(value, guess_method(key), !key.include?('login'))
|
128
|
+
category, action = categorize_endpoint(key)
|
129
|
+
converted[category] ||= {}
|
130
|
+
converted[category][action] = endpoint
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
converted
|
135
|
+
end
|
136
|
+
|
137
|
+
def build_endpoint(path, method, requires_auth = true)
|
138
|
+
return nil unless path
|
139
|
+
{
|
140
|
+
"path" => path,
|
141
|
+
"method" => method,
|
142
|
+
"requires_auth" => requires_auth,
|
143
|
+
"parameters" => {}
|
144
|
+
}
|
145
|
+
end
|
146
|
+
|
147
|
+
# 根据 key 分类端点
|
148
|
+
def categorize_endpoint(key)
|
149
|
+
case key
|
150
|
+
when /app_list|app_info/
|
151
|
+
["apps", key.gsub(/^(get_|update_)/, '')]
|
152
|
+
when /upload|signed_url/
|
153
|
+
["upload", key.gsub(/^(post_|multi_)/, '')]
|
154
|
+
when /version|comment/
|
155
|
+
["version", key.gsub(/^(get_|update_)/, '')]
|
156
|
+
when /cert|resign/
|
157
|
+
["cert", key.gsub(/^(get_|post_)/, '')]
|
158
|
+
when /login|send_code|profile/
|
159
|
+
["auth", key.gsub(/^do_/, '')]
|
160
|
+
when /message|notification/
|
161
|
+
["notification", key.gsub(/^send_/, '')]
|
162
|
+
else
|
163
|
+
["misc", key]
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# 根据动作名称猜测HTTP方法
|
168
|
+
def guess_method(action)
|
169
|
+
case action
|
170
|
+
when /^(get|list|fetch|query|search)/i
|
171
|
+
"GET"
|
172
|
+
when /^(post|create|add|send|do|update|upload)/i
|
173
|
+
"POST"
|
174
|
+
when /^(put|update|modify)/i
|
175
|
+
"PUT"
|
176
|
+
when /^(delete|remove)/i
|
177
|
+
"DELETE"
|
178
|
+
else
|
179
|
+
"GET"
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# 检查值类型
|
184
|
+
def check_type(value, expected_type)
|
185
|
+
case expected_type.downcase
|
186
|
+
when 'string'
|
187
|
+
value.is_a?(String)
|
188
|
+
when 'integer', 'int'
|
189
|
+
value.is_a?(Integer)
|
190
|
+
when 'number', 'float'
|
191
|
+
value.is_a?(Numeric)
|
192
|
+
when 'boolean', 'bool'
|
193
|
+
[true, false].include?(value)
|
194
|
+
when 'array'
|
195
|
+
value.is_a?(Array)
|
196
|
+
when 'object', 'hash'
|
197
|
+
value.is_a?(Hash)
|
198
|
+
else
|
199
|
+
true
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
# 默认错误码
|
204
|
+
def default_error_codes
|
205
|
+
{
|
206
|
+
"200" => "成功",
|
207
|
+
"400" => "请求参数错误",
|
208
|
+
"401" => "未授权或token过期",
|
209
|
+
"403" => "权限不足",
|
210
|
+
"404" => "资源不存在",
|
211
|
+
"500" => "服务器内部错误"
|
212
|
+
}
|
213
|
+
end
|
214
|
+
|
215
|
+
# 默认速率限制
|
216
|
+
def default_rate_limits
|
217
|
+
{
|
218
|
+
"default" => {
|
219
|
+
"requests_per_minute" => 60,
|
220
|
+
"requests_per_hour" => 1000
|
221
|
+
}
|
222
|
+
}
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'faraday'
|
3
|
+
require 'faraday/retry'
|
4
|
+
|
5
|
+
module JPSClient
|
6
|
+
# JPS 专用 HTTP 客户端 - 优化版
|
7
|
+
class HttpClient
|
8
|
+
attr_accessor :base_url
|
9
|
+
attr_accessor :token
|
10
|
+
|
11
|
+
def initialize(base_url: nil, token: nil)
|
12
|
+
@base_url = base_url
|
13
|
+
@token = token
|
14
|
+
@connection = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
# GET 请求
|
18
|
+
def get(path, params: nil)
|
19
|
+
request(:get, path, params: params)
|
20
|
+
end
|
21
|
+
|
22
|
+
# POST 请求
|
23
|
+
def post(path, body: nil, timeout: nil)
|
24
|
+
request(:post, path, body: body, timeout: timeout)
|
25
|
+
end
|
26
|
+
|
27
|
+
# 更新 token
|
28
|
+
def update_token(new_token)
|
29
|
+
@token = new_token
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
# 统一的请求处理
|
35
|
+
def request(method, path, params: nil, body: nil, timeout: nil)
|
36
|
+
url = @base_url + path
|
37
|
+
|
38
|
+
begin
|
39
|
+
response = connection.send(method) do |req|
|
40
|
+
req.url url
|
41
|
+
req.headers['Content-Type'] = 'application/json'
|
42
|
+
req.headers['token'] = @token if @token
|
43
|
+
req.params = params if params
|
44
|
+
req.body = body.to_json if body && method == :post
|
45
|
+
req.options.timeout = timeout if timeout
|
46
|
+
end
|
47
|
+
|
48
|
+
parse_response(response)
|
49
|
+
rescue Faraday::Error => e
|
50
|
+
# 处理网络错误
|
51
|
+
{
|
52
|
+
code: 0,
|
53
|
+
body: nil,
|
54
|
+
success: false,
|
55
|
+
error: e.message
|
56
|
+
}
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# 获取或创建连接(复用连接)
|
61
|
+
def connection
|
62
|
+
@connection ||= Faraday.new do |config|
|
63
|
+
# 重试配置
|
64
|
+
config.request :retry, {
|
65
|
+
max: 3,
|
66
|
+
interval: 0.5,
|
67
|
+
backoff_factor: 2,
|
68
|
+
interval_randomness: 0.5,
|
69
|
+
exceptions: [Faraday::TimeoutError, Faraday::ConnectionFailed]
|
70
|
+
}
|
71
|
+
|
72
|
+
# 代理配置
|
73
|
+
if ENV['http_proxy'] || ENV['https_proxy']
|
74
|
+
config.proxy = {
|
75
|
+
uri: ENV['http_proxy'] || ENV['https_proxy'],
|
76
|
+
user: ENV['HTTP_PROXY_USER'],
|
77
|
+
password: ENV['HTTP_PROXY_PASSWORD']
|
78
|
+
}.compact
|
79
|
+
end
|
80
|
+
|
81
|
+
config.adapter Faraday.default_adapter
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# 解析响应
|
86
|
+
def parse_response(response)
|
87
|
+
result = {
|
88
|
+
code: response.status,
|
89
|
+
body: parse_body(response.body),
|
90
|
+
success: response.success?
|
91
|
+
}
|
92
|
+
|
93
|
+
# 标记需要登录
|
94
|
+
result[:need_login] = true if response.status == 401
|
95
|
+
|
96
|
+
result
|
97
|
+
end
|
98
|
+
|
99
|
+
# 解析响应体
|
100
|
+
def parse_body(body)
|
101
|
+
return nil if body.nil? || body.empty?
|
102
|
+
|
103
|
+
JSON.parse(body)
|
104
|
+
rescue JSON::ParserError
|
105
|
+
body
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
|
111
|
+
module JPSClient
|
112
|
+
# JPS API 响应封装类 - 简化版
|
113
|
+
class Response
|
114
|
+
attr_reader :code
|
115
|
+
attr_reader :data
|
116
|
+
attr_reader :msg
|
117
|
+
|
118
|
+
def initialize(http_response)
|
119
|
+
@success = http_response[:success]
|
120
|
+
@need_login = http_response[:need_login]
|
121
|
+
|
122
|
+
if http_response[:body].is_a?(Hash)
|
123
|
+
@code = http_response[:body]['code'] || http_response[:code]
|
124
|
+
@data = http_response[:body]['data']
|
125
|
+
@msg = http_response[:body]['msg'] || http_response[:body]['message']
|
126
|
+
else
|
127
|
+
@code = http_response[:code]
|
128
|
+
@data = http_response[:body]
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def success?
|
133
|
+
@success && @code.to_s == '200'
|
134
|
+
end
|
135
|
+
|
136
|
+
def need_login?
|
137
|
+
@need_login || @code.to_s == '401'
|
138
|
+
end
|
139
|
+
|
140
|
+
def to_h
|
141
|
+
{
|
142
|
+
'code' => @code,
|
143
|
+
'data' => @data,
|
144
|
+
'msg' => @msg
|
145
|
+
}
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|