sycommon-python-lib 0.1.28__py3-none-any.whl → 0.1.30__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.

Potentially problematic release.


This version of sycommon-python-lib might be problematic. Click here for more details.

@@ -470,12 +470,22 @@ class SYLogger:
470
470
  thread_info = SYLogger._get_execution_context()
471
471
 
472
472
  # 构建日志结构,添加线程/协程信息到threadName字段
473
- request_log = {
474
- "trace_id": str(trace_id) if trace_id else Snowflake.next_id(),
475
- "message": msg_str,
476
- "level": level,
477
- "threadName": thread_info
478
- }
473
+ request_log = {}
474
+ if level == "ERROR":
475
+ request_log = {
476
+ "trace_id": str(trace_id) if trace_id else Snowflake.next_id(),
477
+ "message": msg_str,
478
+ "traceback": traceback.format_exc(),
479
+ "level": level,
480
+ "threadName": thread_info
481
+ }
482
+ else:
483
+ request_log = {
484
+ "trace_id": str(trace_id) if trace_id else Snowflake.next_id(),
485
+ "message": msg_str,
486
+ "level": level,
487
+ "threadName": thread_info
488
+ }
479
489
 
480
490
  # 选择日志级别
481
491
  _log = ''
@@ -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
  """模型配置"""
@@ -608,6 +608,10 @@ class RabbitMQClient:
608
608
  if not self.message_handler or not self._is_consuming:
609
609
  logger.warning("未设置消息处理器或已停止消费")
610
610
  # await message.ack()
611
+ try:
612
+ await message.reject(requeue=True)
613
+ except Exception as e:
614
+ logger.error(f"拒绝消息失败: {e}")
611
615
  return
612
616
 
613
617
  message_id = message.message_id or str(id(message))
@@ -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
sycommon/services.py CHANGED
@@ -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]}")