sycommon-python-lib 0.1.29__tar.gz → 0.1.30__tar.gz

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 (65) hide show
  1. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/PKG-INFO +1 -1
  2. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/pyproject.toml +1 -1
  3. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/models/mqlistener_config.py +1 -0
  4. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/rabbitmq/rabbitmq_service.py +7 -4
  5. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/services.py +6 -0
  6. sycommon_python_lib-0.1.30/src/sycommon/synacos/example.py +153 -0
  7. sycommon_python_lib-0.1.30/src/sycommon/synacos/example2.py +129 -0
  8. sycommon_python_lib-0.1.30/src/sycommon/synacos/feign.py +137 -0
  9. sycommon_python_lib-0.1.30/src/sycommon/synacos/feign_client.py +317 -0
  10. sycommon_python_lib-0.1.30/src/sycommon/synacos/param.py +75 -0
  11. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon_python_lib.egg-info/PKG-INFO +1 -1
  12. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon_python_lib.egg-info/SOURCES.txt +4 -0
  13. sycommon_python_lib-0.1.29/src/sycommon/synacos/feign.py +0 -563
  14. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/README.md +0 -0
  15. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/setup.cfg +0 -0
  16. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/command/cli.py +0 -0
  17. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/__init__.py +0 -0
  18. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/config/Config.py +0 -0
  19. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/config/DatabaseConfig.py +0 -0
  20. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/config/EmbeddingConfig.py +0 -0
  21. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/config/LLMConfig.py +0 -0
  22. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/config/MQConfig.py +0 -0
  23. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/config/RerankerConfig.py +0 -0
  24. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/config/__init__.py +0 -0
  25. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/database/base_db_service.py +0 -0
  26. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/database/database_service.py +0 -0
  27. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/health/__init__.py +0 -0
  28. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/health/health_check.py +0 -0
  29. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/health/metrics.py +0 -0
  30. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/health/ping.py +0 -0
  31. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/logging/__init__.py +0 -0
  32. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/logging/kafka_log.py +0 -0
  33. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/logging/logger_wrapper.py +0 -0
  34. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/logging/sql_logger.py +0 -0
  35. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/middleware/__init__.py +0 -0
  36. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/middleware/context.py +0 -0
  37. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/middleware/cors.py +0 -0
  38. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/middleware/docs.py +0 -0
  39. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/middleware/exception.py +0 -0
  40. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/middleware/middleware.py +0 -0
  41. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/middleware/monitor_memory.py +0 -0
  42. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/middleware/mq.py +0 -0
  43. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/middleware/timeout.py +0 -0
  44. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/middleware/traceid.py +0 -0
  45. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/models/__init__.py +0 -0
  46. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/models/base_http.py +0 -0
  47. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/models/log.py +0 -0
  48. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/models/mqmsg_model.py +0 -0
  49. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/models/mqsend_config.py +0 -0
  50. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/models/sso_user.py +0 -0
  51. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/rabbitmq/rabbitmq_client.py +0 -0
  52. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/rabbitmq/rabbitmq_pool.py +0 -0
  53. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/sse/__init__.py +0 -0
  54. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/sse/event.py +0 -0
  55. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/sse/sse.py +0 -0
  56. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/synacos/__init__.py +0 -0
  57. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/synacos/nacos_service.py +0 -0
  58. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/tools/__init__.py +0 -0
  59. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/tools/docs.py +0 -0
  60. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/tools/snowflake.py +0 -0
  61. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon/tools/timing.py +0 -0
  62. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon_python_lib.egg-info/dependency_links.txt +0 -0
  63. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon_python_lib.egg-info/entry_points.txt +0 -0
  64. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon_python_lib.egg-info/requires.txt +0 -0
  65. {sycommon_python_lib-0.1.29 → sycommon_python_lib-0.1.30}/src/sycommon_python_lib.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sycommon-python-lib
3
- Version: 0.1.29
3
+ Version: 0.1.30
4
4
  Summary: Add your description here
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sycommon-python-lib"
3
- version = "0.1.29"
3
+ version = "0.1.30"
4
4
  description = "Add your description here"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -31,6 +31,7 @@ class RabbitMQListenerConfig(BaseModel):
31
31
  durable: bool = Field(True, description="是否持久化")
32
32
  auto_delete: bool = Field(False, description="是否自动删除队列")
33
33
  auto_parse_json: bool = Field(True, description="是否自动解析JSON消息")
34
+ prefetch_count: int = Field(2, description="mq同时消费数量")
34
35
 
35
36
  class Config:
36
37
  """模型配置"""
@@ -167,7 +167,7 @@ class RabbitMQService:
167
167
  'max_reconnection_attempts', 5),
168
168
  prefetch_count=kwargs.get('prefetch_count', 2),
169
169
  consumption_stall_threshold=kwargs.get(
170
- 'consumption_stall_threshold', 10)
170
+ 'consumption_stall_threshold', 10),
171
171
  )
172
172
 
173
173
  # 使用declare_queue控制是否声明队列(发送器不声明,监听器声明)
@@ -276,7 +276,7 @@ class RabbitMQService:
276
276
 
277
277
  # 以下方法逻辑与原有保持一致(无需修改)
278
278
  @classmethod
279
- async def setup_senders(cls, senders: List[RabbitMQSendConfig], has_listeners: bool = False) -> None:
279
+ async def setup_senders(cls, senders: List[RabbitMQSendConfig], has_listeners: bool = False, ** kwargs) -> None:
280
280
  """设置消息发送器"""
281
281
  cls._has_senders = True
282
282
  cls._has_listeners = has_listeners
@@ -287,6 +287,7 @@ class RabbitMQService:
287
287
  if not sender_config.queue_name:
288
288
  raise ValueError(f"发送器配置第{idx+1}项缺少queue_name")
289
289
 
290
+ prefetch_count = sender_config.prefetch_count
290
291
  queue_name = sender_config.queue_name
291
292
  app_name = cls._config.get(
292
293
  "APP_NAME", "") if cls._config else ""
@@ -315,7 +316,9 @@ class RabbitMQService:
315
316
  auto_delete=sender_config.auto_delete,
316
317
  auto_parse_json=sender_config.auto_parse_json,
317
318
  queue_name=queue_name,
318
- create_if_not_exists=False
319
+ create_if_not_exists=False,
320
+ prefetch_count=prefetch_count,
321
+ ** kwargs
319
322
  )
320
323
 
321
324
  # 记录客户端
@@ -334,7 +337,7 @@ class RabbitMQService:
334
337
  logger.info(f"消息发送器设置完成,共 {len(cls._sender_client_names)} 个发送器")
335
338
 
336
339
  @classmethod
337
- async def setup_listeners(cls, listeners: List[RabbitMQListenerConfig], has_senders: bool = False) -> None:
340
+ async def setup_listeners(cls, listeners: List[RabbitMQListenerConfig], has_senders: bool = False, ** kwargs) -> None:
338
341
  """设置消息监听器"""
339
342
  cls._has_listeners = True
340
343
  cls._has_senders = has_senders
@@ -145,6 +145,12 @@ class Services(metaclass=SingletonMeta):
145
145
 
146
146
  # 设置发送器,传递是否有监听器的标志
147
147
  if rabbitmq_senders:
148
+ # 判断是否有监听器,如果有遍历监听器列表,队列名一样将prefetch_count属性设置到发送器对象中
149
+ if rabbitmq_listeners:
150
+ for sender in rabbitmq_senders:
151
+ for listener in rabbitmq_listeners:
152
+ if sender.queue_name == listener.queue_name:
153
+ sender.prefetch_count = listener.prefetch_count
148
154
  await self._setup_senders_async(rabbitmq_senders, has_listeners)
149
155
 
150
156
  # 设置监听器,传递是否有发送器的标志
@@ -0,0 +1,153 @@
1
+ from typing import Dict, Any, Optional, List, Union
2
+
3
+ from sycommon.synacos.param import Body, Cookie, File, Form, Header, Path, Query
4
+ from sycommon.synacos.feign_client import feign_client, feign_request, feign_upload
5
+
6
+
7
+ @feign_client(
8
+ service_name="product-service",
9
+ path_prefix="/api/v1",
10
+ default_headers={
11
+ "User-Agent": "Feign-Client/1.0",
12
+ "Accept": "application/json"
13
+ }
14
+ )
15
+ class ProductServiceClient:
16
+ """商品服务Feign客户端(优化版)"""
17
+
18
+ # ------------------------------
19
+ # 场景1: 基础参数 + 动态Header + Cookie
20
+ # ------------------------------
21
+ @feign_request(
22
+ "GET",
23
+ "/products/{product_id}/reviews",
24
+ headers={"X-Request-Source": "mobile"}, # 固定头
25
+ timeout=10 # 接口级超时(10秒)
26
+ )
27
+ async def get_product_reviews(
28
+ self,
29
+ product_id: int = Path(..., description="商品ID"),
30
+ status: Optional[str] = Query(None, description="评价状态"),
31
+ page: int = Query(1, description="页码"),
32
+ size: int = Query(10, description="每页条数"),
33
+ x_auth_token: str = Header(..., description="动态令牌头"), # 动态头
34
+ session_id: str = Cookie(..., description="会话Cookie") # Cookie
35
+ ) -> Dict[str, Any]:
36
+ """获取商品评价列表"""
37
+ pass
38
+
39
+ # ------------------------------
40
+ # 场景2: 仅Query参数(默认不超时)
41
+ # ------------------------------
42
+ @feign_request("GET", "/products") # 未指定timeout,使用客户端默认(不超时)
43
+ async def search_products(
44
+ self,
45
+ category: str = Query(..., description="商品分类"),
46
+ min_price: Optional[float] = Query(None, description="最低价格"),
47
+ max_price: Optional[float] = Query(None, description="最高价格"),
48
+ sort: str = Query("created_desc", description="排序方式")
49
+ ) -> Dict[str, Any]:
50
+ """搜索商品"""
51
+ pass
52
+
53
+ # ------------------------------
54
+ # 场景3: JSON请求体 + 动态签名头
55
+ # (通过Header参数传递动态生成的签名)
56
+ # ------------------------------
57
+ @feign_request(
58
+ "POST",
59
+ "/products",
60
+ headers={"s-y-version": "2.1"},
61
+ timeout=15 # 15秒超时
62
+ )
63
+ async def create_product(
64
+ self,
65
+ product_data: Dict[str, Any] = Body(..., description="商品信息"),
66
+ x_signature: str = Header(..., description="动态生成的签名头") # 签名头
67
+ ) -> Dict[str, Any]:
68
+ """创建商品(动态签名通过Header参数传递)"""
69
+ pass
70
+
71
+ # ------------------------------
72
+ # 场景4: 文件上传 + 分片上传头
73
+ # ------------------------------
74
+ @feign_upload(field_name="image_file")
75
+ @feign_request(
76
+ "POST",
77
+ "/products/{product_id}/images",
78
+ headers={"X-Upload-Type": "product-image"},
79
+ timeout=60 # 上传超时60秒
80
+ )
81
+ async def upload_product_image(
82
+ self,
83
+ product_id: int = Path(..., description="商品ID"),
84
+ file_paths: Union[str, List[str]] = File(..., description="文件路径"),
85
+ image_type: str = Form(..., description="图片类型"),
86
+ is_primary: bool = Form(False, description="是否主图"),
87
+ x_chunked: bool = Header(False, description="是否分片上传") # 分片上传头
88
+ ) -> Dict[str, Any]:
89
+ """上传商品图片(支持分片上传标记)"""
90
+ pass
91
+
92
+ # ------------------------------
93
+ # 场景5: 表单提交 + 操作时间头
94
+ # ------------------------------
95
+ @feign_request(
96
+ "POST",
97
+ "/products/batch-status",
98
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
99
+ timeout=5
100
+ )
101
+ async def batch_update_status(
102
+ self,
103
+ product_ids: str = Form(..., description="商品ID列表"),
104
+ status: str = Form(..., description="目标状态"),
105
+ operator: str = Form(..., description="操作人"),
106
+ x_operate_time: str = Header(..., description="操作时间戳头") # 动态时间头
107
+ ) -> Dict[str, Any]:
108
+ """批量更新商品状态"""
109
+ pass
110
+
111
+
112
+ # ------------------------------
113
+ # 2. 完整调用示例(含Session和Header用法)
114
+ # ------------------------------
115
+ async def feign_advanced_demo():
116
+ client = ProductServiceClient()
117
+
118
+ # 场景1: 动态Header和Cookie
119
+ reviews = await client.get_product_reviews(
120
+ product_id=10086,
121
+ status="APPROVED",
122
+ x_auth_token="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", # 动态令牌
123
+ session_id="sid_123456" # Cookie
124
+ )
125
+ print(f"场景1 - 评价数: {reviews.get('total', 0)}")
126
+
127
+ # 场景3: 动态生成签名头(直接通过Header参数传递)
128
+ import hashlib
129
+ product_data = {
130
+ "name": "无线蓝牙耳机",
131
+ "price": 299.99,
132
+ "stock": 500
133
+ }
134
+ # 生成签名(业务逻辑)
135
+ sign_str = f"{product_data['name']}_{product_data['price']}_secret"
136
+ signature = hashlib.md5(sign_str.encode()).hexdigest()
137
+ # 传递签名头
138
+ new_product = await client.create_product(
139
+ product_data=product_data,
140
+ x_signature=signature # 动态签名通过Header参数传入
141
+ )
142
+ print(f"场景3 - 商品ID: {new_product.get('id')}")
143
+
144
+ # 场景4: 分片上传(通过x_chunked头控制)
145
+ product_id = new_product.get('id')
146
+ if product_id:
147
+ upload_result = await client.upload_product_image(
148
+ product_id=product_id,
149
+ file_paths=["/tmp/main.jpg", "/tmp/detail.jpg"],
150
+ image_type="detail",
151
+ x_chunked=True # 启用分片上传
152
+ )
153
+ print(f"场景4 - 上传图片数: {len(upload_result.get('image_urls', []))}")
@@ -0,0 +1,129 @@
1
+ from pydantic import BaseModel, Field
2
+ from typing import Any, Optional, List, Dict
3
+
4
+ from sycommon.synacos.feign_client import feign_client, feign_request
5
+ from sycommon.synacos.param import Body, Form, Query
6
+
7
+
8
+ # ------------------------------
9
+ # 请求模型(req)
10
+ # ------------------------------
11
+ class ProductCreateReq(BaseModel):
12
+ """创建商品的请求模型"""
13
+ name: str = Field(..., description="商品名称")
14
+ category: str = Field(..., description="商品分类")
15
+ price: float = Field(..., gt=0, description="商品价格(必须>0)")
16
+ stock: int = Field(..., ge=0, description="库存(必须>=0)")
17
+ attributes: Optional[Dict[str, str]] = Field(None, description="商品属性")
18
+
19
+
20
+ class BatchUpdateStatusReq(BaseModel):
21
+ """批量更新状态的请求模型(表单提交)"""
22
+ product_ids: str = Field(..., description="商品ID列表(逗号分隔)")
23
+ status: str = Field(..., description="目标状态(如ON_SALE/OFF_SALE)")
24
+ operator: str = Field(..., description="操作人")
25
+
26
+
27
+ # ------------------------------
28
+ # 响应模型(resp)
29
+ # ------------------------------
30
+ class ProductResp(BaseModel):
31
+ """商品详情响应模型"""
32
+ id: int = Field(..., description="商品ID")
33
+ name: str = Field(..., description="商品名称")
34
+ category: str = Field(..., description="分类")
35
+ price: float = Field(..., description="价格")
36
+ stock: int = Field(..., description="库存")
37
+ created_at: str = Field(..., description="创建时间")
38
+
39
+
40
+ class PageResp(BaseModel):
41
+ """分页响应模型"""
42
+ total: int = Field(..., description="总条数")
43
+ items: List[ProductResp] = Field(..., description="商品列表")
44
+ page: int = Field(..., description="当前页码")
45
+ size: int = Field(..., description="每页条数")
46
+
47
+
48
+ @feign_client(
49
+ service_name="product-service",
50
+ path_prefix="/api/v2",
51
+ default_headers={"Accept": "application/json"}
52
+ )
53
+ class ProductServiceClient:
54
+ # ------------------------------
55
+ # 场景1: Pydantic 作为请求体(Body)
56
+ # ------------------------------
57
+ @feign_request("POST", "/products", timeout=15)
58
+ async def create_product(
59
+ self,
60
+ # 使用 Pydantic 模型作为请求体
61
+ product: ProductCreateReq = Body(..., description="商品信息")
62
+ ) -> ProductResp: # 响应自动解析为 ProductResp 模型
63
+ """创建商品(Pydantic请求体 + Pydantic响应)"""
64
+ pass
65
+
66
+ # ------------------------------
67
+ # 场景2: Pydantic 作为表单参数(Form)
68
+ # ------------------------------
69
+ @feign_request(
70
+ "POST",
71
+ "/products/batch-status",
72
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
73
+ timeout=5
74
+ )
75
+ async def batch_update_status(
76
+ self,
77
+ # 使用 Pydantic 模型作为表单参数
78
+ update_data: BatchUpdateStatusReq = Form(..., description="批量更新信息")
79
+ ) -> Dict[str, Any]: # 普通字典响应
80
+ """批量更新商品状态(Pydantic表单)"""
81
+ pass
82
+
83
+ # ------------------------------
84
+ # 场景3: 响应为分页模型(嵌套Pydantic)
85
+ # ------------------------------
86
+ @feign_request("GET", "/products", timeout=10)
87
+ async def search_products(
88
+ self,
89
+ category: str = Query(..., description="商品分类"),
90
+ page: int = Query(1, description="页码"),
91
+ size: int = Query(10, description="每页条数")
92
+ ) -> PageResp: # 响应自动解析为 PageResp(嵌套 ProductResp)
93
+ """搜索商品(Pydantic分页响应)"""
94
+ pass
95
+
96
+
97
+ async def pydantic_feign_demo():
98
+ client = ProductServiceClient()
99
+
100
+ # 场景1: 创建商品(Pydantic请求体 + 响应)
101
+ create_req = ProductCreateReq(
102
+ name="无线蓝牙耳机",
103
+ category="electronics",
104
+ price=299.99,
105
+ stock=500,
106
+ attributes={"brand": "Feign", "battery_life": "24h"}
107
+ )
108
+ product = await client.create_product(product=create_req)
109
+ # 直接使用 Pydantic 模型的属性
110
+ print(f"创建商品: ID={product.id}, 名称={product.name}, 价格={product.price}")
111
+
112
+ # 场景2: 批量更新(Pydantic表单)
113
+ batch_req = BatchUpdateStatusReq(
114
+ product_ids=f"{product.id},1002,1003",
115
+ status="ON_SALE",
116
+ operator="system"
117
+ )
118
+ batch_result = await client.batch_update_status(update_data=batch_req)
119
+ print(f"批量更新成功: {batch_result.get('success_count')}个")
120
+
121
+ # 场景3: 搜索商品(Pydantic分页响应)
122
+ page_resp = await client.search_products(
123
+ category="electronics",
124
+ page=1,
125
+ size=10
126
+ )
127
+ # 分页模型的属性和嵌套列表
128
+ print(
129
+ f"搜索结果: 共{page_resp.total}条,第1页商品: {[p.name for p in page_resp.items]}")
@@ -0,0 +1,137 @@
1
+ import io
2
+ import os
3
+ import time
4
+
5
+ import aiohttp
6
+ from sycommon.logging.kafka_log import SYLogger
7
+ from sycommon.synacos.nacos_service import NacosService
8
+
9
+ """
10
+ 支持异步Feign客户端
11
+ 方式一: 使用 @feign_client 和 @feign_request 装饰器
12
+ 方式二: 使用 feign 函数
13
+ """
14
+
15
+
16
+ async def feign(service_name, api_path, method='GET', params=None, headers=None, file_path=None,
17
+ path_params=None, body=None, files=None, form_data=None, timeout=None):
18
+ """
19
+ feign 函数,显式设置JSON请求的Content-Type头
20
+ """
21
+ session = aiohttp.ClientSession()
22
+ try:
23
+ # 初始化headers,确保是可修改的字典
24
+ headers = headers.copy() if headers else {}
25
+
26
+ # 处理JSON请求的Content-Type
27
+ is_json_request = method.upper() in ["POST", "PUT", "PATCH"] and not (
28
+ files or form_data or file_path)
29
+ if is_json_request and "Content-Type" not in headers:
30
+ headers["Content-Type"] = "application/json"
31
+
32
+ nacos_service = NacosService(None)
33
+ version = headers.get('s-y-version')
34
+
35
+ # 获取服务实例
36
+ instances = nacos_service.get_service_instances(
37
+ service_name, target_version=version)
38
+ if not instances:
39
+ SYLogger.error(f"nacos:未找到 {service_name} 的健康实例")
40
+ return None
41
+
42
+ # 简单轮询负载均衡
43
+ instance = instances[int(time.time()) % len(instances)]
44
+
45
+ SYLogger.info(f"nacos:开始调用服务: {service_name}")
46
+ SYLogger.info(f"nacos:请求头: {headers}")
47
+
48
+ ip = instance.get('ip')
49
+ port = instance.get('port')
50
+
51
+ # 处理path参数
52
+ if path_params:
53
+ for key, value in path_params.items():
54
+ api_path = api_path.replace(f"{{{key}}}", str(value))
55
+
56
+ url = f"http://{ip}:{port}{api_path}"
57
+ SYLogger.info(f"nacos:请求地址: {url}")
58
+
59
+ try:
60
+ # 处理文件上传
61
+ if files or form_data or file_path:
62
+ data = aiohttp.FormData()
63
+ if form_data:
64
+ for key, value in form_data.items():
65
+ data.add_field(key, value)
66
+ if files:
67
+ # 兼容处理:同时支持字典(单文件)和列表(多文件)
68
+ if isinstance(files, dict):
69
+ # 处理原有字典格式(单文件)
70
+ # 字典格式:{field_name: (filename, content)}
71
+ for field_name, (filename, content) in files.items():
72
+ data.add_field(field_name, content,
73
+ filename=filename)
74
+ elif isinstance(files, list):
75
+ # 处理新列表格式(多文件)
76
+ # 列表格式:[(field_name, filename, content), ...]
77
+ for item in files:
78
+ if len(item) != 3:
79
+ raise ValueError(
80
+ f"列表元素格式错误,需为 (field_name, filename, content),实际为 {item}")
81
+ field_name, filename, content = item
82
+ data.add_field(field_name, content,
83
+ filename=filename)
84
+ else:
85
+ raise TypeError(f"files 参数必须是字典或列表,实际为 {type(files)}")
86
+ if file_path:
87
+ filename = os.path.basename(file_path)
88
+ with open(file_path, 'rb') as f:
89
+ data.add_field('file', f, filename=filename)
90
+ # 移除Content-Type,让aiohttp自动处理
91
+ headers.pop('Content-Type', None)
92
+ async with session.request(
93
+ method=method.upper(),
94
+ url=url,
95
+ headers=headers,
96
+ params=params,
97
+ data=data,
98
+ timeout=timeout
99
+ ) as response:
100
+ return await _handle_feign_response(response)
101
+ else:
102
+ # 普通JSON请求
103
+ async with session.request(
104
+ method=method.upper(),
105
+ url=url,
106
+ headers=headers,
107
+ params=params,
108
+ json=body,
109
+ timeout=timeout
110
+ ) as response:
111
+ return await _handle_feign_response(response)
112
+ except aiohttp.ClientError as e:
113
+ SYLogger.error(
114
+ f"nacos:请求服务接口时出错ClientError path: {api_path} error:{e}")
115
+ return None
116
+ except Exception as e:
117
+ import traceback
118
+ SYLogger.error(
119
+ f"nacos:请求服务接口时出错 path: {api_path} error:{traceback.format_exc()}")
120
+ return None
121
+ finally:
122
+ await session.close()
123
+
124
+
125
+ async def _handle_feign_response(response):
126
+ """处理Feign请求的响应"""
127
+ if response.status == 200:
128
+ content_type = response.headers.get('Content-Type')
129
+ if 'application/json' in content_type:
130
+ return await response.json()
131
+ else:
132
+ content = await response.read()
133
+ return io.BytesIO(content)
134
+ else:
135
+ error_msg = await response.text()
136
+ SYLogger.error(f"nacos:请求失败,状态码: {response.status},响应内容: {error_msg}")
137
+ return None