sycommon-python-lib 0.1.29__py3-none-any.whl → 0.1.40__py3-none-any.whl
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.
- sycommon/models/mqlistener_config.py +1 -0
- sycommon/rabbitmq/rabbitmq_client.py +205 -588
- sycommon/rabbitmq/rabbitmq_pool.py +141 -65
- sycommon/rabbitmq/rabbitmq_service.py +269 -145
- sycommon/services.py +64 -22
- sycommon/synacos/example.py +153 -0
- sycommon/synacos/example2.py +129 -0
- sycommon/synacos/feign.py +51 -436
- sycommon/synacos/feign_client.py +317 -0
- sycommon/synacos/nacos_service.py +1 -1
- sycommon/synacos/param.py +75 -0
- {sycommon_python_lib-0.1.29.dist-info → sycommon_python_lib-0.1.40.dist-info}/METADATA +1 -1
- {sycommon_python_lib-0.1.29.dist-info → sycommon_python_lib-0.1.40.dist-info}/RECORD +16 -12
- {sycommon_python_lib-0.1.29.dist-info → sycommon_python_lib-0.1.40.dist-info}/WHEEL +0 -0
- {sycommon_python_lib-0.1.29.dist-info → sycommon_python_lib-0.1.40.dist-info}/entry_points.txt +0 -0
- {sycommon_python_lib-0.1.29.dist-info → sycommon_python_lib-0.1.40.dist-info}/top_level.txt +0 -0
sycommon/synacos/feign.py
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import io
|
|
2
2
|
import os
|
|
3
3
|
import time
|
|
4
|
-
import inspect
|
|
5
|
-
from urllib.parse import urljoin
|
|
6
4
|
|
|
7
5
|
import aiohttp
|
|
8
6
|
from sycommon.logging.kafka_log import SYLogger
|
|
@@ -14,430 +12,6 @@ from sycommon.synacos.nacos_service import NacosService
|
|
|
14
12
|
方式二: 使用 feign 函数
|
|
15
13
|
"""
|
|
16
14
|
|
|
17
|
-
# 示例Feign客户端接口
|
|
18
|
-
# 1. 定义完整的Feign客户端接口
|
|
19
|
-
# @feign_client(service_name="product-service", path_prefix="/api/v2")
|
|
20
|
-
# class ProductServiceClient:
|
|
21
|
-
# """商品服务Feign客户端示例,涵盖所有参数类型"""
|
|
22
|
-
#
|
|
23
|
-
# # ------------------------------
|
|
24
|
-
# # 场景1: Path参数 + Query参数
|
|
25
|
-
# # 请求示例: GET /products/{product_id}/reviews?status=APPROVED&page=1&size=10
|
|
26
|
-
# # ------------------------------
|
|
27
|
-
# @feign_request("GET", "/products/{product_id}/reviews")
|
|
28
|
-
# async def get_product_reviews(
|
|
29
|
-
# self,
|
|
30
|
-
# product_id: int, # Path参数 (URL路径中的占位符)
|
|
31
|
-
# status: Optional[str] = None, # Query参数 (URL查询字符串)
|
|
32
|
-
# page: int = 1, # Query参数 (默认值)
|
|
33
|
-
# size: int = 10 # Query参数 (默认值)
|
|
34
|
-
# ) -> Dict[str, Any]:
|
|
35
|
-
# """获取商品评价列表"""
|
|
36
|
-
# pass
|
|
37
|
-
#
|
|
38
|
-
# # ------------------------------
|
|
39
|
-
# # 场景2: 仅Query参数
|
|
40
|
-
# # 请求示例: GET /products?category=electronics&min_price=100&max_price=500&sort=price_asc
|
|
41
|
-
# # ------------------------------
|
|
42
|
-
# @feign_request("GET", "/products")
|
|
43
|
-
# async def search_products(
|
|
44
|
-
# self,
|
|
45
|
-
# category: str, # 必选Query参数
|
|
46
|
-
# min_price: Optional[float] = None, # 可选Query参数
|
|
47
|
-
# max_price: Optional[float] = None, # 可选Query参数
|
|
48
|
-
# sort: str = "created_desc" # 带默认值的Query参数
|
|
49
|
-
# ) -> Dict[str, Any]:
|
|
50
|
-
# """搜索商品(仅查询参数)"""
|
|
51
|
-
# pass
|
|
52
|
-
#
|
|
53
|
-
# # ------------------------------
|
|
54
|
-
# # 场景3: JSON Body参数 (POST)
|
|
55
|
-
# # 请求示例: POST /products (请求体为JSON)
|
|
56
|
-
# # ------------------------------
|
|
57
|
-
# @feign_request("POST", "/products", headers={"s-y-version": "2.1"})
|
|
58
|
-
# async def create_product(
|
|
59
|
-
# self,
|
|
60
|
-
# product_data: Dict[str, Any] # JSON请求体
|
|
61
|
-
# ) -> Dict[str, Any]:
|
|
62
|
-
# """创建商品(JSON请求体)"""
|
|
63
|
-
# pass
|
|
64
|
-
#
|
|
65
|
-
# # ------------------------------
|
|
66
|
-
# # 场景4: Path参数 + JSON Body (PUT)
|
|
67
|
-
# # 请求示例: PUT /products/{product_id} (请求体为JSON)
|
|
68
|
-
# # ------------------------------
|
|
69
|
-
# @feign_request("PUT", "/products/{product_id}")
|
|
70
|
-
# async def update_product(
|
|
71
|
-
# self,
|
|
72
|
-
# product_id: int, # Path参数
|
|
73
|
-
# update_data: Dict[str, Any] # JSON请求体
|
|
74
|
-
# ) -> Dict[str, Any]:
|
|
75
|
-
# """更新商品信息"""
|
|
76
|
-
# pass
|
|
77
|
-
#
|
|
78
|
-
# # ------------------------------
|
|
79
|
-
# # 场景5: FormData表单提交 (x-www-form-urlencoded)
|
|
80
|
-
# # 请求示例: POST /products/batch-status (表单字段)
|
|
81
|
-
# # ------------------------------
|
|
82
|
-
# @feign_request(
|
|
83
|
-
# "POST",
|
|
84
|
-
# "/products/batch-status",
|
|
85
|
-
# headers={"Content-Type": "application/x-www-form-urlencoded"}
|
|
86
|
-
# )
|
|
87
|
-
# async def batch_update_status(
|
|
88
|
-
# self,
|
|
89
|
-
# product_ids: str, # 表单字段 (多个ID用逗号分隔)
|
|
90
|
-
# status: str, # 表单字段 (目标状态)
|
|
91
|
-
# operator: str # 表单字段 (操作人)
|
|
92
|
-
# ) -> Dict[str, Any]:
|
|
93
|
-
# """批量更新商品状态(表单提交)"""
|
|
94
|
-
# pass
|
|
95
|
-
#
|
|
96
|
-
# # ------------------------------
|
|
97
|
-
# # 场景6: 多文件上传 + 表单字段混合
|
|
98
|
-
# # 请求示例: POST /products/{product_id}/images (multipart/form-data)
|
|
99
|
-
# # 支持同时上传多个文件,共用字段名 "image_file"
|
|
100
|
-
# # ------------------------------
|
|
101
|
-
# @feign_upload(field_name="image_file") # 指定所有文件的表单字段名
|
|
102
|
-
# @feign_request("POST", "/products/{product_id}/images")
|
|
103
|
-
# async def upload_product_image(
|
|
104
|
-
# self,
|
|
105
|
-
# product_id: int, # Path参数(URL路径中的占位符)
|
|
106
|
-
# file_paths: str | list[str], # 本地文件路径(单个路径字符串或多个路径列表)
|
|
107
|
-
# image_type: str, # 表单字段(图片类型,如"main"、"detail")
|
|
108
|
-
# is_primary: bool = False, # 表单字段(是否主图)
|
|
109
|
-
# remark: Optional[str] = None # 可选表单字段(备注信息)
|
|
110
|
-
# ) -> Dict[str, Any]:
|
|
111
|
-
# """上传商品图片(支持多文件 + 表单字段混合)"""
|
|
112
|
-
# pass
|
|
113
|
-
#
|
|
114
|
-
# # ------------------------------
|
|
115
|
-
# # 场景7: 多Path参数 + DELETE请求
|
|
116
|
-
# # 请求示例: DELETE /products/{product_id}/skus/{sku_id}
|
|
117
|
-
# # ------------------------------
|
|
118
|
-
# @feign_request("DELETE", "/products/{product_id}/skus/{sku_id}")
|
|
119
|
-
# async def delete_product_sku(
|
|
120
|
-
# self,
|
|
121
|
-
# product_id: int, # Path参数1
|
|
122
|
-
# sku_id: int # Path参数2
|
|
123
|
-
# ) -> Dict[str, Any]:
|
|
124
|
-
# """删除商品SKU(多路径参数)"""
|
|
125
|
-
# pass
|
|
126
|
-
#
|
|
127
|
-
# # ------------------------------
|
|
128
|
-
# # 场景8: 复杂JSON Body + Query参数
|
|
129
|
-
# # 请求示例: POST /products/filter?include_out_of_stock=false
|
|
130
|
-
# # ------------------------------
|
|
131
|
-
# @feign_request("POST", "/products/filter")
|
|
132
|
-
# async def advanced_filter(
|
|
133
|
-
# self,
|
|
134
|
-
# filter_condition: Dict[str, Any], # JSON请求体(复杂筛选条件)
|
|
135
|
-
# include_out_of_stock: bool = False, # Query参数
|
|
136
|
-
# page: int = 1, # Query参数
|
|
137
|
-
# size: int = 20 # Query参数
|
|
138
|
-
# ) -> Dict[str, Any]:
|
|
139
|
-
# """高级筛选商品(JSON体+查询参数)"""
|
|
140
|
-
# pass
|
|
141
|
-
#
|
|
142
|
-
#
|
|
143
|
-
# # 2. 完整调用示例
|
|
144
|
-
# async def feign_complete_demo():
|
|
145
|
-
# # ------------------------------
|
|
146
|
-
# # 调用场景1: Path参数 + Query参数
|
|
147
|
-
# # ------------------------------
|
|
148
|
-
# reviews = await ProductServiceClient().get_product_reviews(
|
|
149
|
-
# product_id=10086, # Path参数
|
|
150
|
-
# status="APPROVED", # Query参数
|
|
151
|
-
# page=1,
|
|
152
|
-
# size=20
|
|
153
|
-
# )
|
|
154
|
-
# print(f"场景1 - 商品评价: {reviews.get('total', 0)}条评价")
|
|
155
|
-
#
|
|
156
|
-
# # ------------------------------
|
|
157
|
-
# # 调用场景2: 仅Query参数
|
|
158
|
-
# # ------------------------------
|
|
159
|
-
# electronics = await ProductServiceClient().search_products(
|
|
160
|
-
# category="electronics", # 必选Query
|
|
161
|
-
# min_price=1000,
|
|
162
|
-
# max_price=5000,
|
|
163
|
-
# sort="price_asc"
|
|
164
|
-
# )
|
|
165
|
-
# print(f"场景2 - 搜索结果: {len(electronics.get('items', []))}个商品")
|
|
166
|
-
#
|
|
167
|
-
# # ------------------------------
|
|
168
|
-
# # 调用场景3: JSON Body参数 (POST)
|
|
169
|
-
# # ------------------------------
|
|
170
|
-
# new_product = await ProductServiceClient().create_product({
|
|
171
|
-
# "name": "无线蓝牙耳机",
|
|
172
|
-
# "category": "electronics",
|
|
173
|
-
# "price": 299.99,
|
|
174
|
-
# "stock": 500,
|
|
175
|
-
# "attributes": {
|
|
176
|
-
# "brand": "Feign",
|
|
177
|
-
# "battery_life": "24h"
|
|
178
|
-
# }
|
|
179
|
-
# })
|
|
180
|
-
# print(f"场景3 - 新建商品: ID={new_product.get('id')}")
|
|
181
|
-
# product_id = new_product.get('id') # 用于后续示例
|
|
182
|
-
#
|
|
183
|
-
# # ------------------------------
|
|
184
|
-
# # 调用场景4: Path参数 + JSON Body (PUT)
|
|
185
|
-
# # ------------------------------
|
|
186
|
-
# if product_id:
|
|
187
|
-
# updated = await ProductServiceClient().update_product(
|
|
188
|
-
# product_id=product_id,
|
|
189
|
-
# update_data={
|
|
190
|
-
# "price": 279.99, # 降价
|
|
191
|
-
# "stock": 600
|
|
192
|
-
# }
|
|
193
|
-
# )
|
|
194
|
-
# print(f"场景4 - 商品更新: 状态={updated.get('success')}")
|
|
195
|
-
#
|
|
196
|
-
# # ------------------------------
|
|
197
|
-
# # 调用场景5: FormData表单提交
|
|
198
|
-
# # ------------------------------
|
|
199
|
-
# batch_result = await ProductServiceClient().batch_update_status(
|
|
200
|
-
# product_ids="1001,1002,1003", # 多个ID用逗号分隔
|
|
201
|
-
# status="ON_SALE",
|
|
202
|
-
# operator="system"
|
|
203
|
-
# )
|
|
204
|
-
# print(f"场景5 - 批量更新: 成功{batch_result.get('success_count')}个")
|
|
205
|
-
#
|
|
206
|
-
# # ------------------------------
|
|
207
|
-
# # 调用场景6: 多文件上传 + 表单字段
|
|
208
|
-
# # 支持两种调用方式:单文件上传 / 多文件上传
|
|
209
|
-
# # ------------------------------
|
|
210
|
-
# if product_id:
|
|
211
|
-
# single_upload_result = await ProductServiceClient().upload_product_image(
|
|
212
|
-
# product_id=product_id,
|
|
213
|
-
# file_paths="/tmp/product_main.jpg", # 单个文件路径或多个路径[""]
|
|
214
|
-
# image_type="main",
|
|
215
|
-
# is_primary=True,
|
|
216
|
-
# remark="商品主图(单文件)"
|
|
217
|
-
# )
|
|
218
|
-
# print(f"场景6 - 单文件上传: 主图URL={single_upload_result.get('image_urls')[0]}")
|
|
219
|
-
#
|
|
220
|
-
# # ------------------------------
|
|
221
|
-
# # 调用场景7: 多Path参数 + DELETE
|
|
222
|
-
# # ------------------------------
|
|
223
|
-
# delete_result = await ProductServiceClient().delete_product_sku(
|
|
224
|
-
# product_id=10086,
|
|
225
|
-
# sku_id=5001
|
|
226
|
-
# )
|
|
227
|
-
# print(f"场景7 - 删除SKU: {delete_result.get('message')}")
|
|
228
|
-
#
|
|
229
|
-
# # ------------------------------
|
|
230
|
-
# # 调用场景8: 复杂JSON Body + Query参数
|
|
231
|
-
# # ------------------------------
|
|
232
|
-
# filtered = await ProductServiceClient().advanced_filter(
|
|
233
|
-
# filter_condition={ # 复杂JSON条件
|
|
234
|
-
# "categories": ["electronics", "home"],
|
|
235
|
-
# "price_range": {"min": 500, "max": 3000},
|
|
236
|
-
# "tags": ["new", "promotion"]
|
|
237
|
-
# },
|
|
238
|
-
# include_out_of_stock=False, # Query参数
|
|
239
|
-
# page=1,
|
|
240
|
-
# size=10
|
|
241
|
-
# )
|
|
242
|
-
# print(f"场景8 - 高级筛选: {filtered.get('total')}个匹配商品")
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
def feign_client(service_name: str, path_prefix: str = "", default_timeout: float | None = None):
|
|
246
|
-
"""Feign客户端装饰器,每次请求后自动关闭会话"""
|
|
247
|
-
def decorator(cls):
|
|
248
|
-
class FeignWrapper:
|
|
249
|
-
def __init__(self):
|
|
250
|
-
self.service_name = service_name
|
|
251
|
-
self.path_prefix = path_prefix
|
|
252
|
-
self.default_timeout = default_timeout
|
|
253
|
-
self.nacos_manager = None # 延迟初始化Nacos
|
|
254
|
-
self.session = None # 延迟初始化aiohttp会话
|
|
255
|
-
|
|
256
|
-
def __getattr__(self, name):
|
|
257
|
-
"""动态获取方法并包装为Feign调用,请求后自动关闭会话"""
|
|
258
|
-
func = getattr(cls, name)
|
|
259
|
-
|
|
260
|
-
async def wrapper(*args, **kwargs):
|
|
261
|
-
# 确保会话初始化
|
|
262
|
-
if self.session is None:
|
|
263
|
-
self.session = aiohttp.ClientSession()
|
|
264
|
-
if self.nacos_manager is None:
|
|
265
|
-
self.nacos_manager = NacosService(None)
|
|
266
|
-
|
|
267
|
-
try:
|
|
268
|
-
# 1. 解析参数
|
|
269
|
-
sig = inspect.signature(func)
|
|
270
|
-
param_names = list(sig.parameters.keys())
|
|
271
|
-
try:
|
|
272
|
-
if param_names and param_names[0] == 'self':
|
|
273
|
-
bound_args = sig.bind(self, *args, **kwargs)
|
|
274
|
-
else:
|
|
275
|
-
bound_args = sig.bind(*args, **kwargs)
|
|
276
|
-
bound_args.apply_defaults()
|
|
277
|
-
params = dict(bound_args.arguments)
|
|
278
|
-
params.pop('self', None)
|
|
279
|
-
SYLogger.debug(f"解析参数: {params}")
|
|
280
|
-
except TypeError as e:
|
|
281
|
-
SYLogger.error(f"参数绑定失败: {str(e)}")
|
|
282
|
-
raise
|
|
283
|
-
|
|
284
|
-
# 2. 构建请求
|
|
285
|
-
request_meta = getattr(func, "_feign_meta", {})
|
|
286
|
-
method = request_meta.get("method", "GET")
|
|
287
|
-
path = request_meta.get("path", "")
|
|
288
|
-
headers = request_meta.get("headers", {}).copy()
|
|
289
|
-
timeout = kwargs.pop('timeout', self.default_timeout)
|
|
290
|
-
|
|
291
|
-
full_path = f"{self.path_prefix}{path}"
|
|
292
|
-
for param_name, param_value in params.items():
|
|
293
|
-
if param_value is not None:
|
|
294
|
-
full_path = full_path.replace(
|
|
295
|
-
f"{{{param_name}}}", str(param_value))
|
|
296
|
-
|
|
297
|
-
is_json_request = method.upper() in ["POST", "PUT", "PATCH"] and \
|
|
298
|
-
not request_meta.get("is_upload", False)
|
|
299
|
-
if is_json_request and "Content-Type" not in headers:
|
|
300
|
-
headers["Content-Type"] = "application/json"
|
|
301
|
-
|
|
302
|
-
# 3. 服务发现
|
|
303
|
-
version = headers.get('s-y-version')
|
|
304
|
-
instances = self.nacos_manager.get_service_instances(
|
|
305
|
-
self.service_name, target_version=version)
|
|
306
|
-
if not instances:
|
|
307
|
-
raise RuntimeError(
|
|
308
|
-
f"未找到服务 {self.service_name} 的健康实例")
|
|
309
|
-
|
|
310
|
-
instance = instances[int(time.time()) % len(instances)]
|
|
311
|
-
base_url = f"http://{instance['ip']}:{instance['port']}"
|
|
312
|
-
url = urljoin(base_url, full_path)
|
|
313
|
-
SYLogger.info(f"请求: {method} {url}")
|
|
314
|
-
|
|
315
|
-
# 4. 准备请求参数
|
|
316
|
-
query_params = {k: v for k, v in params.items()
|
|
317
|
-
if f"{{{k}}}" not in path and v is not None}
|
|
318
|
-
request_data = None
|
|
319
|
-
files = None
|
|
320
|
-
|
|
321
|
-
if request_meta.get("is_upload", False):
|
|
322
|
-
files = aiohttp.FormData()
|
|
323
|
-
file_path = params.get('file_path')
|
|
324
|
-
if file_path and os.path.exists(file_path):
|
|
325
|
-
file_field = request_meta.get(
|
|
326
|
-
"upload_field", "file")
|
|
327
|
-
with open(file_path, 'rb') as f:
|
|
328
|
-
files.add_field(
|
|
329
|
-
file_field,
|
|
330
|
-
f.read(),
|
|
331
|
-
filename=os.path.basename(file_path)
|
|
332
|
-
)
|
|
333
|
-
for key, value in params.items():
|
|
334
|
-
if key != 'file_path' and value is not None:
|
|
335
|
-
files.add_field(key, str(value))
|
|
336
|
-
headers.pop('Content-Type', None)
|
|
337
|
-
elif is_json_request:
|
|
338
|
-
body_params = [k for k in params if k not in query_params
|
|
339
|
-
and f"{{{k}}}" not in path]
|
|
340
|
-
if body_params:
|
|
341
|
-
request_data = params[body_params[0]] if len(body_params) == 1 else \
|
|
342
|
-
{k: params[k] for k in body_params}
|
|
343
|
-
|
|
344
|
-
# 5. 发送请求并获取响应
|
|
345
|
-
async with self.session.request(
|
|
346
|
-
method=method,
|
|
347
|
-
url=url,
|
|
348
|
-
headers=headers,
|
|
349
|
-
params=query_params,
|
|
350
|
-
json=request_data if not files else None,
|
|
351
|
-
data=files,
|
|
352
|
-
timeout=timeout
|
|
353
|
-
) as response:
|
|
354
|
-
return await self._handle_response(response)
|
|
355
|
-
|
|
356
|
-
finally:
|
|
357
|
-
# 请求完成后自动关闭会话
|
|
358
|
-
if self.session:
|
|
359
|
-
await self.session.close()
|
|
360
|
-
self.session = None # 重置会话,下次调用重新创建
|
|
361
|
-
SYLogger.info(
|
|
362
|
-
f"自动关闭aiohttp会话: {self.service_name}")
|
|
363
|
-
|
|
364
|
-
return wrapper
|
|
365
|
-
|
|
366
|
-
async def _handle_response(self, response):
|
|
367
|
-
"""处理响应结果(保持不变)"""
|
|
368
|
-
status = response.status
|
|
369
|
-
if 200 <= status < 300:
|
|
370
|
-
content_type = response.headers.get('Content-Type', '')
|
|
371
|
-
if 'application/json' in content_type:
|
|
372
|
-
return await response.json()
|
|
373
|
-
else:
|
|
374
|
-
return await response.read()
|
|
375
|
-
else:
|
|
376
|
-
error_msg = await response.text()
|
|
377
|
-
SYLogger.error(f"响应错误: {status} - {error_msg}")
|
|
378
|
-
raise RuntimeError(f"HTTP {status}: {error_msg}")
|
|
379
|
-
|
|
380
|
-
return FeignWrapper
|
|
381
|
-
return decorator
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
def feign_request(method: str, path: str, headers: dict = None):
|
|
385
|
-
"""定义请求元数据的装饰器"""
|
|
386
|
-
def decorator(func):
|
|
387
|
-
func._feign_meta = {
|
|
388
|
-
"method": method.upper(),
|
|
389
|
-
"path": path,
|
|
390
|
-
"headers": headers.copy() if headers else {}
|
|
391
|
-
}
|
|
392
|
-
return func
|
|
393
|
-
return decorator
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
def feign_upload(field_name: str = "file"):
|
|
397
|
-
"""处理多文件上传的装饰器(支持同字段名多文件 + 表单字段混合)"""
|
|
398
|
-
def decorator(func):
|
|
399
|
-
async def wrapper(*args, **kwargs):
|
|
400
|
-
# 获取文件路径列表(支持单个文件路径字符串或多个文件路径列表)
|
|
401
|
-
file_paths = kwargs.get('file_paths')
|
|
402
|
-
if not file_paths:
|
|
403
|
-
raise ValueError("缺少文件路径参数: file_paths(可为单个路径字符串或列表)")
|
|
404
|
-
|
|
405
|
-
# 统一转为列表格式(兼容单个文件的情况)
|
|
406
|
-
if isinstance(file_paths, str):
|
|
407
|
-
file_paths = [file_paths]
|
|
408
|
-
if not isinstance(file_paths, list):
|
|
409
|
-
raise ValueError("file_paths 必须是字符串或列表")
|
|
410
|
-
|
|
411
|
-
# 验证所有文件是否存在
|
|
412
|
-
for path in file_paths:
|
|
413
|
-
if not os.path.exists(path):
|
|
414
|
-
raise FileNotFoundError(f"文件不存在: {path}")
|
|
415
|
-
|
|
416
|
-
# 构建 multipart/form-data 表单数据
|
|
417
|
-
form_data = aiohttp.FormData()
|
|
418
|
-
|
|
419
|
-
# 添加所有文件(共用同一个 field_name)
|
|
420
|
-
for file_path in file_paths:
|
|
421
|
-
with open(file_path, 'rb') as f:
|
|
422
|
-
form_data.add_field(
|
|
423
|
-
field_name, # 所有文件使用相同的表单字段名
|
|
424
|
-
f.read(),
|
|
425
|
-
filename=os.path.basename(file_path) # 保留原文件名
|
|
426
|
-
)
|
|
427
|
-
|
|
428
|
-
# 添加其他表单字段(从 kwargs 中提取非文件参数)
|
|
429
|
-
form_fields = {k: v for k, v in kwargs.items() if k !=
|
|
430
|
-
'file_paths'}
|
|
431
|
-
for key, value in form_fields.items():
|
|
432
|
-
if value is not None:
|
|
433
|
-
form_data.add_field(key, str(value)) # 表单字段转为字符串
|
|
434
|
-
|
|
435
|
-
# 将构建好的表单数据传入原函数
|
|
436
|
-
kwargs['form_data'] = form_data
|
|
437
|
-
return await func(*args, **kwargs)
|
|
438
|
-
return wrapper
|
|
439
|
-
return decorator
|
|
440
|
-
|
|
441
15
|
|
|
442
16
|
async def feign(service_name, api_path, method='GET', params=None, headers=None, file_path=None,
|
|
443
17
|
path_params=None, body=None, files=None, form_data=None, timeout=None):
|
|
@@ -549,15 +123,56 @@ async def feign(service_name, api_path, method='GET', params=None, headers=None,
|
|
|
549
123
|
|
|
550
124
|
|
|
551
125
|
async def _handle_feign_response(response):
|
|
552
|
-
"""
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
126
|
+
"""
|
|
127
|
+
处理Feign请求的响应,统一返回格式
|
|
128
|
+
调整逻辑:先判断状态码,再处理内容
|
|
129
|
+
- 200状态:优先识别JSON/文本,其他均按文件流(二进制)处理
|
|
130
|
+
- 非200状态:统一返回错误字典
|
|
131
|
+
"""
|
|
132
|
+
try:
|
|
133
|
+
status_code = response.status
|
|
134
|
+
content_type = response.headers.get('Content-Type', '').lower()
|
|
135
|
+
response_body = None
|
|
136
|
+
|
|
137
|
+
if status_code == 200:
|
|
138
|
+
if 'application/json' in content_type:
|
|
139
|
+
response_body = await response.json()
|
|
140
|
+
elif 'text/' in content_type:
|
|
141
|
+
# 文本类型(text/plain、text/html等):按文本读取
|
|
142
|
+
try:
|
|
143
|
+
response_body = await response.text(encoding='utf-8')
|
|
144
|
+
except UnicodeDecodeError:
|
|
145
|
+
# 兼容中文编码(gbk)
|
|
146
|
+
response_body = await response.text(encoding='gbk')
|
|
147
|
+
else:
|
|
148
|
+
# 其他类型(PDF、图片、octet-stream等):按文件流(二进制)读取
|
|
149
|
+
binary_data = await response.read()
|
|
150
|
+
SYLogger.info(
|
|
151
|
+
f"按文件流处理响应,类型:{content_type},大小:{len(binary_data)/1024:.2f}KB")
|
|
152
|
+
return io.BytesIO(binary_data) # 返回BytesIO,支持read()
|
|
153
|
+
return response_body
|
|
557
154
|
else:
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
155
|
+
# 非200状态:统一读取响应体(兼容文本/二进制错误信息)
|
|
156
|
+
try:
|
|
157
|
+
if 'application/json' in content_type:
|
|
158
|
+
response_body = await response.json()
|
|
159
|
+
else:
|
|
160
|
+
response_body = await response.text(encoding='utf-8', errors='ignore')
|
|
161
|
+
except Exception:
|
|
162
|
+
binary_data = await response.read()
|
|
163
|
+
response_body = f"非200状态,响应无法解码:{binary_data[:100].hex()}"
|
|
164
|
+
|
|
165
|
+
error_msg = f"请求失败,状态码: {status_code},响应内容: {str(response_body)[:500]}"
|
|
166
|
+
SYLogger.error(error_msg)
|
|
167
|
+
return {
|
|
168
|
+
"success": False,
|
|
169
|
+
"code": status_code,
|
|
170
|
+
"message": error_msg,
|
|
171
|
+
"data": response_body
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
except Exception as e:
|
|
175
|
+
import traceback
|
|
176
|
+
error_detail = f"处理响应异常: {str(e)}\n{traceback.format_exc()}"
|
|
177
|
+
SYLogger.error(f"nacos:处理响应时出错: {error_detail}")
|
|
563
178
|
return None
|