sycommon-python-lib 0.1.29__py3-none-any.whl → 0.1.38__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 +575 -258
- sycommon/rabbitmq/rabbitmq_service.py +255 -131
- 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.38.dist-info}/METADATA +1 -1
- {sycommon_python_lib-0.1.29.dist-info → sycommon_python_lib-0.1.38.dist-info}/RECORD +15 -11
- {sycommon_python_lib-0.1.29.dist-info → sycommon_python_lib-0.1.38.dist-info}/WHEEL +0 -0
- {sycommon_python_lib-0.1.29.dist-info → sycommon_python_lib-0.1.38.dist-info}/entry_points.txt +0 -0
- {sycommon_python_lib-0.1.29.dist-info → sycommon_python_lib-0.1.38.dist-info}/top_level.txt +0 -0
sycommon/services.py
CHANGED
|
@@ -22,6 +22,7 @@ class Services(metaclass=SingletonMeta):
|
|
|
22
22
|
_instance: Optional['Services'] = None
|
|
23
23
|
_app: Optional[FastAPI] = None
|
|
24
24
|
_user_lifespan: Optional[Callable] = None
|
|
25
|
+
_shutdown_lock: asyncio.Lock = asyncio.Lock()
|
|
25
26
|
|
|
26
27
|
def __init__(self, config: dict, app: FastAPI):
|
|
27
28
|
if not Services._config:
|
|
@@ -66,8 +67,12 @@ class Services(metaclass=SingletonMeta):
|
|
|
66
67
|
if not cls._config:
|
|
67
68
|
config = yaml.safe_load(open('app.yaml', 'r', encoding='utf-8'))
|
|
68
69
|
cls._config = config
|
|
69
|
-
|
|
70
|
-
app.
|
|
70
|
+
# 使用config
|
|
71
|
+
app.state.config = {
|
|
72
|
+
"host": cls._config.get('Host', '0.0.0.0'),
|
|
73
|
+
"port": cls._config.get('Port', 8080),
|
|
74
|
+
"workers": cls._config.get('Workers', 1)
|
|
75
|
+
}
|
|
71
76
|
|
|
72
77
|
# 立即配置非异步服务(在应用启动前)
|
|
73
78
|
if middleware:
|
|
@@ -139,12 +144,26 @@ class Services(metaclass=SingletonMeta):
|
|
|
139
144
|
has_listeners: bool = False,
|
|
140
145
|
has_senders: bool = False,
|
|
141
146
|
):
|
|
142
|
-
"""异步设置MQ
|
|
147
|
+
"""异步设置MQ相关服务(适配新的RabbitMQService)"""
|
|
143
148
|
# 初始化RabbitMQ服务,传递状态
|
|
144
149
|
RabbitMQService.init(self._config, has_listeners, has_senders)
|
|
145
150
|
|
|
151
|
+
# 等待RabbitMQ连接池初始化完成(最多等待30秒)
|
|
152
|
+
start_time = asyncio.get_event_loop().time()
|
|
153
|
+
while not RabbitMQService._connection_pool and not RabbitMQService._is_shutdown:
|
|
154
|
+
if asyncio.get_event_loop().time() - start_time > 30:
|
|
155
|
+
raise TimeoutError("RabbitMQ连接池初始化超时")
|
|
156
|
+
logging.info("等待RabbitMQ连接池初始化...")
|
|
157
|
+
await asyncio.sleep(1)
|
|
158
|
+
|
|
146
159
|
# 设置发送器,传递是否有监听器的标志
|
|
147
160
|
if rabbitmq_senders:
|
|
161
|
+
# 判断是否有监听器,如果有遍历监听器列表,队列名一样将prefetch_count属性设置到发送器对象中
|
|
162
|
+
if rabbitmq_listeners:
|
|
163
|
+
for sender in rabbitmq_senders:
|
|
164
|
+
for listener in rabbitmq_listeners:
|
|
165
|
+
if sender.queue_name == listener.queue_name:
|
|
166
|
+
sender.prefetch_count = listener.prefetch_count
|
|
148
167
|
await self._setup_senders_async(rabbitmq_senders, has_listeners)
|
|
149
168
|
|
|
150
169
|
# 设置监听器,传递是否有发送器的标志
|
|
@@ -153,20 +172,25 @@ class Services(metaclass=SingletonMeta):
|
|
|
153
172
|
|
|
154
173
|
# 验证初始化结果
|
|
155
174
|
if has_listeners:
|
|
175
|
+
# 异步获取客户端数量(适配新的RabbitMQService)
|
|
156
176
|
listener_count = len(RabbitMQService._clients)
|
|
157
177
|
logging.info(f"监听器初始化完成,共启动 {listener_count} 个消费者")
|
|
158
178
|
if listener_count == 0:
|
|
159
179
|
logging.warning("未成功初始化任何监听器,请检查配置")
|
|
160
180
|
|
|
161
181
|
async def _setup_senders_async(self, rabbitmq_senders, has_listeners: bool):
|
|
182
|
+
"""设置发送器(适配新的RabbitMQService异步方法)"""
|
|
162
183
|
Services._registered_senders = [
|
|
163
184
|
sender.queue_name for sender in rabbitmq_senders]
|
|
164
185
|
|
|
165
|
-
# 将是否有监听器的信息传递给RabbitMQService
|
|
186
|
+
# 将是否有监听器的信息传递给RabbitMQService(异步调用)
|
|
166
187
|
await RabbitMQService.setup_senders(rabbitmq_senders, has_listeners)
|
|
188
|
+
# 更新已注册的发送器(从RabbitMQService获取实际注册的名称)
|
|
189
|
+
Services._registered_senders = RabbitMQService._sender_client_names
|
|
167
190
|
logging.info(f"已注册的RabbitMQ发送器: {Services._registered_senders}")
|
|
168
191
|
|
|
169
192
|
async def _setup_listeners_async(self, rabbitmq_listeners, has_senders: bool):
|
|
193
|
+
"""设置监听器(适配新的RabbitMQService异步方法)"""
|
|
170
194
|
await RabbitMQService.setup_listeners(rabbitmq_listeners, has_senders)
|
|
171
195
|
|
|
172
196
|
@classmethod
|
|
@@ -177,23 +201,30 @@ class Services(metaclass=SingletonMeta):
|
|
|
177
201
|
max_retries: int = 3,
|
|
178
202
|
retry_delay: float = 1.0, **kwargs
|
|
179
203
|
) -> None:
|
|
180
|
-
"""
|
|
204
|
+
"""发送消息,添加重试机制(适配新的RabbitMQService异步API)"""
|
|
181
205
|
if not cls._initialized or not cls._loop:
|
|
182
206
|
logging.error("Services not properly initialized!")
|
|
183
207
|
raise ValueError("服务未正确初始化")
|
|
184
208
|
|
|
209
|
+
if RabbitMQService._is_shutdown:
|
|
210
|
+
logging.error("RabbitMQService已关闭,无法发送消息")
|
|
211
|
+
raise RuntimeError("RabbitMQ服务已关闭")
|
|
212
|
+
|
|
185
213
|
for attempt in range(max_retries):
|
|
186
214
|
try:
|
|
215
|
+
# 验证发送器是否注册
|
|
187
216
|
if queue_name not in cls._registered_senders:
|
|
188
217
|
cls._registered_senders = RabbitMQService._sender_client_names
|
|
189
218
|
if queue_name not in cls._registered_senders:
|
|
190
219
|
raise ValueError(f"发送器 {queue_name} 未注册")
|
|
191
220
|
|
|
192
|
-
|
|
221
|
+
# 获取发送器(适配新的异步get_sender方法)
|
|
222
|
+
sender = await RabbitMQService.get_sender(queue_name)
|
|
193
223
|
if not sender:
|
|
194
|
-
raise ValueError(f"发送器 '{queue_name}'
|
|
224
|
+
raise ValueError(f"发送器 '{queue_name}' 不存在或连接无效")
|
|
195
225
|
|
|
196
|
-
|
|
226
|
+
# 发送消息(调用RabbitMQService的异步send_message)
|
|
227
|
+
await RabbitMQService.send_message(data, queue_name, **kwargs)
|
|
197
228
|
logging.info(f"消息发送成功(尝试 {attempt+1}/{max_retries})")
|
|
198
229
|
return
|
|
199
230
|
|
|
@@ -209,17 +240,28 @@ class Services(metaclass=SingletonMeta):
|
|
|
209
240
|
)
|
|
210
241
|
await asyncio.sleep(retry_delay)
|
|
211
242
|
|
|
212
|
-
@
|
|
213
|
-
async def shutdown():
|
|
214
|
-
"""
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
243
|
+
@classmethod
|
|
244
|
+
async def shutdown(cls):
|
|
245
|
+
"""关闭所有服务(线程安全,适配新的RabbitMQService关闭逻辑)"""
|
|
246
|
+
async with cls._shutdown_lock:
|
|
247
|
+
if RabbitMQService._is_shutdown:
|
|
248
|
+
logging.info("RabbitMQService已关闭,无需重复操作")
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
# 取消所有MQ任务
|
|
252
|
+
for task in Services._mq_tasks:
|
|
253
|
+
if not task.done():
|
|
254
|
+
task.cancel()
|
|
255
|
+
try:
|
|
256
|
+
await task
|
|
257
|
+
except asyncio.CancelledError:
|
|
258
|
+
logging.info(f"MQ任务 {task.get_name()} 已取消")
|
|
259
|
+
|
|
260
|
+
# 关闭RabbitMQ服务(异步调用)
|
|
261
|
+
await RabbitMQService.shutdown()
|
|
262
|
+
|
|
263
|
+
# 清理全局状态
|
|
264
|
+
cls._initialized = False
|
|
265
|
+
cls._registered_senders.clear()
|
|
266
|
+
cls._mq_tasks.clear()
|
|
267
|
+
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]}")
|