sycommon-python-lib 0.1.16__py3-none-any.whl → 0.1.56b1__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.
Files changed (36) hide show
  1. sycommon/config/Config.py +6 -2
  2. sycommon/config/RerankerConfig.py +1 -0
  3. sycommon/database/async_base_db_service.py +36 -0
  4. sycommon/database/async_database_service.py +96 -0
  5. sycommon/database/database_service.py +6 -1
  6. sycommon/health/metrics.py +13 -0
  7. sycommon/llm/__init__.py +0 -0
  8. sycommon/llm/embedding.py +149 -0
  9. sycommon/llm/get_llm.py +177 -0
  10. sycommon/llm/llm_logger.py +126 -0
  11. sycommon/logging/async_sql_logger.py +65 -0
  12. sycommon/logging/kafka_log.py +36 -14
  13. sycommon/logging/logger_levels.py +23 -0
  14. sycommon/logging/sql_logger.py +53 -0
  15. sycommon/middleware/context.py +2 -0
  16. sycommon/middleware/middleware.py +4 -0
  17. sycommon/middleware/traceid.py +155 -32
  18. sycommon/models/mqlistener_config.py +1 -0
  19. sycommon/rabbitmq/rabbitmq_client.py +377 -821
  20. sycommon/rabbitmq/rabbitmq_pool.py +338 -0
  21. sycommon/rabbitmq/rabbitmq_service.py +411 -229
  22. sycommon/services.py +116 -61
  23. sycommon/synacos/example.py +153 -0
  24. sycommon/synacos/example2.py +129 -0
  25. sycommon/synacos/feign.py +90 -413
  26. sycommon/synacos/feign_client.py +335 -0
  27. sycommon/synacos/nacos_service.py +159 -106
  28. sycommon/synacos/param.py +75 -0
  29. sycommon/tools/merge_headers.py +97 -0
  30. sycommon/tools/snowflake.py +296 -7
  31. {sycommon_python_lib-0.1.16.dist-info → sycommon_python_lib-0.1.56b1.dist-info}/METADATA +19 -13
  32. sycommon_python_lib-0.1.56b1.dist-info/RECORD +68 -0
  33. sycommon_python_lib-0.1.16.dist-info/RECORD +0 -52
  34. {sycommon_python_lib-0.1.16.dist-info → sycommon_python_lib-0.1.56b1.dist-info}/WHEEL +0 -0
  35. {sycommon_python_lib-0.1.16.dist-info → sycommon_python_lib-0.1.56b1.dist-info}/entry_points.txt +0 -0
  36. {sycommon_python_lib-0.1.16.dist-info → sycommon_python_lib-0.1.56b1.dist-info}/top_level.txt +0 -0
sycommon/services.py CHANGED
@@ -1,10 +1,13 @@
1
- from typing import Any, Callable, Dict, List, Tuple, Union, Optional, AsyncGenerator
2
1
  import asyncio
3
2
  import logging
3
+ import yaml
4
4
  from contextlib import asynccontextmanager
5
+ from dotenv import load_dotenv
5
6
  from fastapi import FastAPI, applications
6
7
  from pydantic import BaseModel
8
+ from typing import Any, Callable, Dict, List, Tuple, Union, Optional, AsyncGenerator
7
9
  from sycommon.config.Config import SingletonMeta
10
+ from sycommon.logging.logger_levels import setup_logger_levels
8
11
  from sycommon.models.mqlistener_config import RabbitMQListenerConfig
9
12
  from sycommon.models.mqsend_config import RabbitMQSendConfig
10
13
  from sycommon.rabbitmq.rabbitmq_service import RabbitMQService
@@ -16,10 +19,13 @@ class Services(metaclass=SingletonMeta):
16
19
  _config: Optional[dict] = None
17
20
  _initialized: bool = False
18
21
  _registered_senders: List[str] = []
19
- _mq_tasks: List[asyncio.Task] = []
20
22
  _instance: Optional['Services'] = None
21
23
  _app: Optional[FastAPI] = None
22
24
  _user_lifespan: Optional[Callable] = None
25
+ _shutdown_lock: asyncio.Lock = asyncio.Lock()
26
+
27
+ # 用于存储待执行的异步数据库初始化任务
28
+ _pending_async_db_setup: List[Tuple[Callable, str]] = []
23
29
 
24
30
  def __init__(self, config: dict, app: FastAPI):
25
31
  if not Services._config:
@@ -41,26 +47,37 @@ class Services(metaclass=SingletonMeta):
41
47
  def plugins(
42
48
  cls,
43
49
  app: FastAPI,
44
- config: dict,
50
+ config: Optional[dict] = None,
45
51
  middleware: Optional[Callable[[FastAPI, dict], None]] = None,
46
52
  nacos_service: Optional[Callable[[dict], None]] = None,
47
53
  logging_service: Optional[Callable[[dict], None]] = None,
48
54
  database_service: Optional[Union[
49
- Tuple[Callable[[dict, str], None], str],
50
- List[Tuple[Callable[[dict, str], None], str]]
55
+ Tuple[Callable, str],
56
+ List[Tuple[Callable, str]]
51
57
  ]] = None,
52
58
  rabbitmq_listeners: Optional[List[RabbitMQListenerConfig]] = None,
53
59
  rabbitmq_senders: Optional[List[RabbitMQSendConfig]] = None
54
60
  ) -> FastAPI:
55
- # 保存应用实例和配置
61
+ load_dotenv()
62
+ setup_logger_levels()
56
63
  cls._app = app
57
64
  cls._config = config
58
65
  cls._user_lifespan = app.router.lifespan_context
59
- # 设置文档
66
+
60
67
  applications.get_swagger_ui_html = custom_swagger_ui_html
61
68
  applications.get_redoc_html = custom_redoc_html
62
69
 
63
- # 立即配置非异步服务(在应用启动前)
70
+ if not cls._config:
71
+ config = yaml.safe_load(open('app.yaml', 'r', encoding='utf-8'))
72
+ cls._config = config
73
+
74
+ app.state.config = {
75
+ "host": cls._config.get('Host', '0.0.0.0'),
76
+ "port": cls._config.get('Port', 8080),
77
+ "workers": cls._config.get('Workers', 1),
78
+ "h11_max_incomplete_event_size": cls._config.get('H11MaxIncompleteEventSize', 1024 * 1024 * 10)
79
+ }
80
+
64
81
  if middleware:
65
82
  middleware(app, config)
66
83
 
@@ -70,25 +87,62 @@ class Services(metaclass=SingletonMeta):
70
87
  if logging_service:
71
88
  logging_service(config)
72
89
 
90
+ # ========== 处理数据库服务 ==========
91
+ # 清空之前的待执行列表(防止热重载时重复)
92
+ cls._pending_async_db_setup = []
93
+
73
94
  if database_service:
74
- cls._setup_database_static(database_service, config)
95
+ # 解析配置并区分同步/异步
96
+ items = [database_service] if isinstance(
97
+ database_service, tuple) else database_service
98
+ for item in items:
99
+ db_setup_func, db_name = item
100
+ if asyncio.iscoroutinefunction(db_setup_func):
101
+ # 如果是异步函数,加入待执行列表
102
+ logging.info(f"检测到异步数据库服务: {db_name},将在应用启动时初始化")
103
+ cls._pending_async_db_setup.append(item)
104
+ else:
105
+ # 如果是同步函数,立即执行
106
+ logging.info(f"执行同步数据库服务: {db_name}")
107
+ try:
108
+ db_setup_func(config, db_name)
109
+ except Exception as e:
110
+ logging.error(
111
+ f"同步数据库服务 {db_name} 初始化失败: {e}", exc_info=True)
112
+ raise
75
113
 
76
114
  # 创建组合生命周期管理器
77
115
  @asynccontextmanager
78
116
  async def combined_lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
79
117
  # 1. 执行Services自身的初始化
80
118
  instance = cls(config, app)
81
- has_listeners = bool(
119
+
120
+ # ========== 执行挂起的异步数据库初始化 ==========
121
+ if cls._pending_async_db_setup:
122
+ logging.info("开始执行异步数据库初始化...")
123
+ for db_setup_func, db_name in cls._pending_async_db_setup:
124
+ try:
125
+ await db_setup_func(config, db_name)
126
+ logging.info(f"异步数据库服务 {db_name} 初始化成功")
127
+ except Exception as e:
128
+ logging.error(
129
+ f"异步数据库服务 {db_name} 初始化失败: {e}", exc_info=True)
130
+ raise
131
+
132
+ # ========== 初始化 MQ ==========
133
+ has_valid_listeners = bool(
82
134
  rabbitmq_listeners and len(rabbitmq_listeners) > 0)
83
- has_senders = bool(rabbitmq_senders and len(rabbitmq_senders) > 0)
135
+ has_valid_senders = bool(
136
+ rabbitmq_senders and len(rabbitmq_senders) > 0)
84
137
 
85
138
  try:
86
- await instance._setup_mq_async(
87
- rabbitmq_listeners=rabbitmq_listeners,
88
- rabbitmq_senders=rabbitmq_senders,
89
- has_listeners=has_listeners,
90
- has_senders=has_senders
91
- )
139
+ if has_valid_listeners or has_valid_senders:
140
+ await instance._setup_mq_async(
141
+ rabbitmq_listeners=rabbitmq_listeners if has_valid_listeners else None,
142
+ rabbitmq_senders=rabbitmq_senders if has_valid_senders else None,
143
+ has_listeners=has_valid_listeners,
144
+ has_senders=has_valid_senders
145
+ )
92
146
  cls._initialized = True
93
147
  logging.info("Services初始化完成")
94
148
  except Exception as e:
@@ -100,28 +154,18 @@ class Services(metaclass=SingletonMeta):
100
154
  # 2. 执行用户定义的生命周期
101
155
  if cls._user_lifespan:
102
156
  async with cls._user_lifespan(app):
103
- yield # 应用运行阶段
157
+ yield
104
158
  else:
105
- yield # 没有用户生命周期时直接 yield
159
+ yield
106
160
 
107
161
  # 3. 执行Services的关闭逻辑
108
162
  await cls.shutdown()
109
163
  logging.info("Services已关闭")
110
164
 
111
- # 设置组合生命周期
112
165
  app.router.lifespan_context = combined_lifespan
113
-
114
166
  return app
115
167
 
116
- @staticmethod
117
- def _setup_database_static(database_service, config):
118
- """静态方法:设置数据库服务"""
119
- if isinstance(database_service, tuple):
120
- db_setup, db_name = database_service
121
- db_setup(config, db_name)
122
- elif isinstance(database_service, list):
123
- for db_setup, db_name in database_service:
124
- db_setup(config, db_name)
168
+ # 移除了 _setup_database_static,因为逻辑已内联到 plugins 中
125
169
 
126
170
  async def _setup_mq_async(
127
171
  self,
@@ -131,33 +175,46 @@ class Services(metaclass=SingletonMeta):
131
175
  has_senders: bool = False,
132
176
  ):
133
177
  """异步设置MQ相关服务"""
134
- # 初始化RabbitMQ服务,传递状态
178
+ if not (has_listeners or has_senders):
179
+ logging.info("无RabbitMQ监听器/发送器配置,跳过RabbitMQService初始化")
180
+ return
181
+
135
182
  RabbitMQService.init(self._config, has_listeners, has_senders)
136
183
 
137
- # 设置发送器,传递是否有监听器的标志
138
- if rabbitmq_senders:
184
+ start_time = asyncio.get_event_loop().time()
185
+ while not (RabbitMQService._connection_pool and RabbitMQService._connection_pool._initialized) and not RabbitMQService._is_shutdown:
186
+ if asyncio.get_event_loop().time() - start_time > 30:
187
+ raise TimeoutError("RabbitMQ连接池初始化超时(30秒)")
188
+ logging.info("等待RabbitMQ连接池初始化...")
189
+ await asyncio.sleep(0.5)
190
+
191
+ if has_senders and rabbitmq_senders:
192
+ if has_listeners and rabbitmq_listeners:
193
+ for sender in rabbitmq_senders:
194
+ for listener in rabbitmq_listeners:
195
+ if sender.queue_name == listener.queue_name:
196
+ sender.prefetch_count = listener.prefetch_count
139
197
  await self._setup_senders_async(rabbitmq_senders, has_listeners)
140
198
 
141
- # 设置监听器,传递是否有发送器的标志
142
- if rabbitmq_listeners:
199
+ if has_listeners and rabbitmq_listeners:
143
200
  await self._setup_listeners_async(rabbitmq_listeners, has_senders)
144
201
 
145
- # 验证初始化结果
146
202
  if has_listeners:
147
- listener_count = len(RabbitMQService._clients)
203
+ listener_count = len(RabbitMQService._consumer_tasks)
148
204
  logging.info(f"监听器初始化完成,共启动 {listener_count} 个消费者")
149
205
  if listener_count == 0:
150
- logging.warning("未成功初始化任何监听器,请检查配置")
206
+ logging.warning("未成功初始化任何监听器,请检查配置或MQ服务状态")
151
207
 
152
208
  async def _setup_senders_async(self, rabbitmq_senders, has_listeners: bool):
209
+ """设置发送器"""
153
210
  Services._registered_senders = [
154
211
  sender.queue_name for sender in rabbitmq_senders]
155
-
156
- # 将是否有监听器的信息传递给RabbitMQService
157
212
  await RabbitMQService.setup_senders(rabbitmq_senders, has_listeners)
213
+ Services._registered_senders = RabbitMQService._sender_client_names
158
214
  logging.info(f"已注册的RabbitMQ发送器: {Services._registered_senders}")
159
215
 
160
216
  async def _setup_listeners_async(self, rabbitmq_listeners, has_senders: bool):
217
+ """设置监听器"""
161
218
  await RabbitMQService.setup_listeners(rabbitmq_listeners, has_senders)
162
219
 
163
220
  @classmethod
@@ -168,11 +225,15 @@ class Services(metaclass=SingletonMeta):
168
225
  max_retries: int = 3,
169
226
  retry_delay: float = 1.0, **kwargs
170
227
  ) -> None:
171
- """发送消息,添加重试机制"""
228
+ """发送消息"""
172
229
  if not cls._initialized or not cls._loop:
173
230
  logging.error("Services not properly initialized!")
174
231
  raise ValueError("服务未正确初始化")
175
232
 
233
+ if RabbitMQService._is_shutdown:
234
+ logging.error("RabbitMQService已关闭,无法发送消息")
235
+ raise RuntimeError("RabbitMQ服务已关闭")
236
+
176
237
  for attempt in range(max_retries):
177
238
  try:
178
239
  if queue_name not in cls._registered_senders:
@@ -180,11 +241,11 @@ class Services(metaclass=SingletonMeta):
180
241
  if queue_name not in cls._registered_senders:
181
242
  raise ValueError(f"发送器 {queue_name} 未注册")
182
243
 
183
- sender = RabbitMQService.get_sender(queue_name)
244
+ sender = await RabbitMQService.get_sender(queue_name)
184
245
  if not sender:
185
- raise ValueError(f"发送器 '{queue_name}' 不存在")
246
+ raise ValueError(f"发送器 '{queue_name}' 不存在或连接无效")
186
247
 
187
- await RabbitMQService.send_message(data, queue_name, ** kwargs)
248
+ await RabbitMQService.send_message(data, queue_name, **kwargs)
188
249
  logging.info(f"消息发送成功(尝试 {attempt+1}/{max_retries})")
189
250
  return
190
251
 
@@ -193,24 +254,18 @@ class Services(metaclass=SingletonMeta):
193
254
  logging.error(
194
255
  f"消息发送失败(已尝试 {max_retries} 次): {str(e)}", exc_info=True)
195
256
  raise
196
-
197
257
  logging.warning(
198
- f"消息发送失败(尝试 {attempt+1}/{max_retries}): {str(e)},"
199
- f"{retry_delay}秒后重试..."
200
- )
258
+ f"消息发送失败(尝试 {attempt+1}/{max_retries}): {str(e)},{retry_delay}秒后重试...")
201
259
  await asyncio.sleep(retry_delay)
202
260
 
203
- @staticmethod
204
- async def shutdown():
261
+ @classmethod
262
+ async def shutdown(cls):
205
263
  """关闭所有服务"""
206
- # 取消所有MQ任务
207
- for task in Services._mq_tasks:
208
- if not task.done():
209
- task.cancel()
210
- try:
211
- await task
212
- except asyncio.CancelledError:
213
- pass
214
-
215
- # 关闭RabbitMQ服务
216
- await RabbitMQService.shutdown()
264
+ async with cls._shutdown_lock:
265
+ if RabbitMQService._is_shutdown:
266
+ logging.info("RabbitMQService已关闭,无需重复操作")
267
+ return
268
+ await RabbitMQService.shutdown()
269
+ cls._initialized = False
270
+ cls._registered_senders.clear()
271
+ logging.info("所有服务已关闭")
@@ -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]}")