aury-boot 0.0.5__py3-none-any.whl → 0.0.7__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.
- aury/boot/_version.py +2 -2
- aury/boot/application/__init__.py +15 -0
- aury/boot/application/adapter/__init__.py +112 -0
- aury/boot/application/adapter/base.py +511 -0
- aury/boot/application/adapter/config.py +242 -0
- aury/boot/application/adapter/decorators.py +259 -0
- aury/boot/application/adapter/exceptions.py +202 -0
- aury/boot/application/adapter/http.py +325 -0
- aury/boot/application/app/middlewares.py +7 -4
- aury/boot/application/config/multi_instance.py +42 -26
- aury/boot/application/config/settings.py +111 -191
- aury/boot/application/middleware/logging.py +14 -1
- aury/boot/commands/generate.py +22 -22
- aury/boot/commands/init.py +41 -9
- aury/boot/commands/templates/project/AGENTS.md.tpl +8 -4
- aury/boot/commands/templates/project/aury_docs/01-model.md.tpl +17 -16
- aury/boot/commands/templates/project/aury_docs/11-logging.md.tpl +82 -43
- aury/boot/commands/templates/project/aury_docs/12-admin.md.tpl +14 -14
- aury/boot/commands/templates/project/aury_docs/13-channel.md.tpl +40 -28
- aury/boot/commands/templates/project/aury_docs/14-mq.md.tpl +9 -9
- aury/boot/commands/templates/project/aury_docs/15-events.md.tpl +8 -8
- aury/boot/commands/templates/project/aury_docs/16-adapter.md.tpl +403 -0
- aury/boot/commands/templates/project/aury_docs/99-cli.md.tpl +19 -19
- aury/boot/commands/templates/project/config.py.tpl +10 -10
- aury/boot/commands/templates/project/env_templates/_header.tpl +10 -0
- aury/boot/commands/templates/project/env_templates/admin.tpl +49 -0
- aury/boot/commands/templates/project/env_templates/cache.tpl +14 -0
- aury/boot/commands/templates/project/env_templates/database.tpl +22 -0
- aury/boot/commands/templates/project/env_templates/log.tpl +18 -0
- aury/boot/commands/templates/project/env_templates/messaging.tpl +46 -0
- aury/boot/commands/templates/project/env_templates/rpc.tpl +28 -0
- aury/boot/commands/templates/project/env_templates/scheduler.tpl +18 -0
- aury/boot/commands/templates/project/env_templates/service.tpl +18 -0
- aury/boot/commands/templates/project/env_templates/storage.tpl +38 -0
- aury/boot/commands/templates/project/env_templates/third_party.tpl +43 -0
- aury/boot/common/logging/__init__.py +26 -674
- aury/boot/common/logging/context.py +132 -0
- aury/boot/common/logging/decorators.py +118 -0
- aury/boot/common/logging/format.py +315 -0
- aury/boot/common/logging/setup.py +214 -0
- aury/boot/infrastructure/database/config.py +6 -14
- aury/boot/infrastructure/tasks/config.py +5 -13
- aury/boot/infrastructure/tasks/manager.py +8 -4
- aury/boot/testing/base.py +2 -2
- {aury_boot-0.0.5.dist-info → aury_boot-0.0.7.dist-info}/METADATA +2 -1
- {aury_boot-0.0.5.dist-info → aury_boot-0.0.7.dist-info}/RECORD +48 -27
- aury/boot/commands/templates/project/env.example.tpl +0 -281
- {aury_boot-0.0.5.dist-info → aury_boot-0.0.7.dist-info}/WHEEL +0 -0
- {aury_boot-0.0.5.dist-info → aury_boot-0.0.7.dist-info}/entry_points.txt +0 -0
|
@@ -2,33 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
用于 SSE(Server-Sent Events)和实时通信。支持 memory 和 redis 后端。
|
|
4
4
|
|
|
5
|
+
## 核心概念
|
|
6
|
+
|
|
7
|
+
- **ChannelManager**:管理器,支持命名多实例(如 sse、notification 独立管理)
|
|
8
|
+
- **channel 参数**:通道名/Topic,用于区分不同的消息流(如 `user:123`、`order:456`)
|
|
9
|
+
|
|
5
10
|
## 13.1 基本用法
|
|
6
11
|
|
|
7
12
|
```python
|
|
8
13
|
from aury.boot.infrastructure.channel import ChannelManager
|
|
9
14
|
|
|
10
|
-
#
|
|
11
|
-
|
|
15
|
+
# 命名多实例(推荐)- 不同业务场景使用不同实例
|
|
16
|
+
sse_channel = ChannelManager.get_instance("sse")
|
|
17
|
+
notification_channel = ChannelManager.get_instance("notification")
|
|
12
18
|
|
|
13
|
-
#
|
|
14
|
-
await
|
|
19
|
+
# Memory 后端(单进程)
|
|
20
|
+
await sse_channel.initialize(backend="memory")
|
|
15
21
|
|
|
16
|
-
#
|
|
22
|
+
# Redis 后端(多进程/分布式)
|
|
17
23
|
from aury.boot.infrastructure.clients.redis import RedisClient
|
|
18
24
|
redis_client = RedisClient.get_instance()
|
|
19
25
|
await redis_client.initialize(url="redis://localhost:6379/0")
|
|
20
|
-
await
|
|
26
|
+
await notification_channel.initialize(backend="redis", redis_client=redis_client)
|
|
21
27
|
```
|
|
22
28
|
|
|
23
|
-
## 13.2
|
|
29
|
+
## 13.2 发布和订阅(Topic 管理)
|
|
24
30
|
|
|
25
31
|
```python
|
|
26
|
-
#
|
|
27
|
-
await
|
|
32
|
+
# 发布消息到指定 Topic
|
|
33
|
+
await sse_channel.publish("user:123", {{"event": "message", "data": "hello"}})
|
|
34
|
+
await sse_channel.publish("order:456", {{"status": "shipped"}})
|
|
28
35
|
|
|
29
36
|
# 发布到多个用户
|
|
30
37
|
for user_id in user_ids:
|
|
31
|
-
await
|
|
38
|
+
await sse_channel.publish(f"user:{{user_id}}", notification)
|
|
32
39
|
```
|
|
33
40
|
|
|
34
41
|
## 13.3 SSE 端点示例
|
|
@@ -42,14 +49,17 @@ from fastapi.responses import StreamingResponse
|
|
|
42
49
|
from aury.boot.infrastructure.channel import ChannelManager
|
|
43
50
|
|
|
44
51
|
router = APIRouter(tags=["SSE"])
|
|
45
|
-
|
|
52
|
+
|
|
53
|
+
# 获取命名实例(在 app 启动时已初始化)
|
|
54
|
+
sse_channel = ChannelManager.get_instance("sse")
|
|
46
55
|
|
|
47
56
|
|
|
48
57
|
@router.get("/sse/{{user_id}}")
|
|
49
58
|
async def sse_stream(user_id: str):
|
|
50
59
|
\"\"\"SSE 实时消息流。\"\"\"
|
|
51
60
|
async def event_generator():
|
|
52
|
-
|
|
61
|
+
# user_id 作为 Topic,区分不同用户的消息流
|
|
62
|
+
async for message in sse_channel.subscribe(f"user:{{user_id}}"):
|
|
53
63
|
yield f"data: {{json.dumps(message)}}\n\n"
|
|
54
64
|
|
|
55
65
|
return StreamingResponse(
|
|
@@ -61,32 +71,34 @@ async def sse_stream(user_id: str):
|
|
|
61
71
|
|
|
62
72
|
@router.post("/notify/{{user_id}}")
|
|
63
73
|
async def send_notification(user_id: str, message: str):
|
|
64
|
-
\"\"\"
|
|
65
|
-
await
|
|
74
|
+
\"\"\"\u53d1\u9001\u901a\u77e5\u3002\"\"\"
|
|
75
|
+
await sse_channel.publish(f"user:{{user_id}}", {{"message": message}})
|
|
66
76
|
return {{"status": "sent"}}
|
|
67
77
|
```
|
|
68
78
|
|
|
69
|
-
## 13.4
|
|
79
|
+
## 13.4 应用场景示例
|
|
70
80
|
|
|
71
81
|
```python
|
|
72
|
-
#
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
await
|
|
82
|
+
# 不同业务场景使用不同的命名实例
|
|
83
|
+
sse_channel = ChannelManager.get_instance("sse") # 前端 SSE 推送
|
|
84
|
+
chat_channel = ChannelManager.get_instance("chat") # 聊天消息
|
|
85
|
+
notify_channel = ChannelManager.get_instance("notify") # 系统通知
|
|
86
|
+
|
|
87
|
+
# 分别初始化(可使用不同后端)
|
|
88
|
+
await sse_channel.initialize(backend="memory") # 单进程即可
|
|
89
|
+
await chat_channel.initialize(backend="redis", redis_client=redis_client) # 需要跨进程
|
|
90
|
+
await notify_channel.initialize(backend="redis", redis_client=redis_client)
|
|
79
91
|
```
|
|
80
92
|
|
|
81
93
|
## 13.5 环境变量
|
|
82
94
|
|
|
83
95
|
```bash
|
|
84
96
|
# 默认实例
|
|
85
|
-
|
|
97
|
+
CHANNEL__BACKEND=memory
|
|
86
98
|
|
|
87
|
-
# 多实例(格式:
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
99
|
+
# 多实例(格式:CHANNEL__{{INSTANCE}}__{{FIELD}})
|
|
100
|
+
CHANNEL__DEFAULT__BACKEND=redis
|
|
101
|
+
CHANNEL__DEFAULT__URL=redis://localhost:6379/3
|
|
102
|
+
CHANNEL__NOTIFICATIONS__BACKEND=redis
|
|
103
|
+
CHANNEL__NOTIFICATIONS__URL=redis://localhost:6379/4
|
|
92
104
|
```
|
|
@@ -85,15 +85,15 @@ await notifications_mq.initialize(backend="redis", url="redis://localhost:6379/5
|
|
|
85
85
|
|
|
86
86
|
```bash
|
|
87
87
|
# 默认实例
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
# 多实例(格式:
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
88
|
+
MQ__BACKEND=redis
|
|
89
|
+
MQ__URL=redis://localhost:6379/0
|
|
90
|
+
|
|
91
|
+
# 多实例(格式:MQ__{{INSTANCE}}__{{FIELD}})
|
|
92
|
+
MQ__DEFAULT__BACKEND=redis
|
|
93
|
+
MQ__DEFAULT__URL=redis://localhost:6379/4
|
|
94
|
+
MQ__ORDERS__BACKEND=rabbitmq
|
|
95
|
+
MQ__ORDERS__URL=amqp://guest:guest@localhost:5672/
|
|
96
|
+
MQ__ORDERS__PREFETCH_COUNT=10
|
|
97
97
|
```
|
|
98
98
|
|
|
99
99
|
## 14.6 与异步任务(Dramatiq)的区别
|
|
@@ -130,14 +130,14 @@ await bus.initialize(
|
|
|
130
130
|
|
|
131
131
|
```bash
|
|
132
132
|
# 默认实例
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
# 多实例(格式:
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
133
|
+
EVENT__BACKEND=memory
|
|
134
|
+
|
|
135
|
+
# 多实例(格式:EVENT__{{INSTANCE}}__{{FIELD}})
|
|
136
|
+
EVENT__DEFAULT__BACKEND=redis
|
|
137
|
+
EVENT__DEFAULT__URL=redis://localhost:6379/5
|
|
138
|
+
EVENT__DOMAIN__BACKEND=rabbitmq
|
|
139
|
+
EVENT__DOMAIN__URL=amqp://guest:guest@localhost:5672/
|
|
140
|
+
EVENT__DOMAIN__EXCHANGE_NAME=domain.events
|
|
141
141
|
```
|
|
142
142
|
|
|
143
143
|
## 15.6 最佳实践
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
# Adapter(第三方接口适配器)
|
|
2
|
+
|
|
3
|
+
## 16.1 概述
|
|
4
|
+
|
|
5
|
+
Adapter 模块用于封装第三方接口(如支付、短信、微信等外部服务)的调用,支持在不同环境(开发、测试、生产)中灵活切换真实调用与 Mock 实现。
|
|
6
|
+
|
|
7
|
+
**核心特性**:
|
|
8
|
+
- 多模式支持:`real`(真实调用)、`sandbox`(沙箱环境)、`mock`(本地 Mock)、`disabled`(禁用)
|
|
9
|
+
- 方法级模式覆盖:同一 Adapter 的不同方法可以使用不同模式
|
|
10
|
+
- 装饰器式 Mock:使用 `.mock` 链式方法定义 Mock 逻辑
|
|
11
|
+
- 调用记录:测试时可追踪所有调用历史
|
|
12
|
+
|
|
13
|
+
## 16.2 基础用法
|
|
14
|
+
|
|
15
|
+
**文件**: `{package_name}/adapters/payment_adapter.py`
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
"""支付适配器。"""
|
|
19
|
+
|
|
20
|
+
from aury.boot.application.adapter import (
|
|
21
|
+
BaseAdapter,
|
|
22
|
+
AdapterSettings,
|
|
23
|
+
adapter_method,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class PaymentAdapter(BaseAdapter):
|
|
28
|
+
"""支付第三方适配器。"""
|
|
29
|
+
|
|
30
|
+
@adapter_method("create_order")
|
|
31
|
+
async def create_order(self, amount: int, order_id: str) -> dict:
|
|
32
|
+
"""创建支付订单(真实实现)。"""
|
|
33
|
+
# 真实调用第三方 API
|
|
34
|
+
response = await self.http_client.post(
|
|
35
|
+
"https://api.payment.com/orders",
|
|
36
|
+
json={"amount": amount, "order_id": order_id},
|
|
37
|
+
)
|
|
38
|
+
return response.json()
|
|
39
|
+
|
|
40
|
+
@create_order.mock
|
|
41
|
+
async def create_order_mock(self, amount: int, order_id: str) -> dict:
|
|
42
|
+
"""创建支付订单(Mock 实现)。"""
|
|
43
|
+
if amount > 100000:
|
|
44
|
+
return {"success": False, "error": "金额超限"}
|
|
45
|
+
return {
|
|
46
|
+
"success": True,
|
|
47
|
+
"transaction_id": f"mock_tx_{order_id}",
|
|
48
|
+
"amount": amount,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@adapter_method("query_order")
|
|
52
|
+
async def query_order(self, transaction_id: str) -> dict:
|
|
53
|
+
"""查询支付订单。"""
|
|
54
|
+
response = await self.http_client.get(
|
|
55
|
+
f"https://api.payment.com/orders/{transaction_id}"
|
|
56
|
+
)
|
|
57
|
+
return response.json()
|
|
58
|
+
|
|
59
|
+
@query_order.mock
|
|
60
|
+
async def query_order_mock(self, transaction_id: str) -> dict:
|
|
61
|
+
"""查询支付订单(Mock)。"""
|
|
62
|
+
return {
|
|
63
|
+
"transaction_id": transaction_id,
|
|
64
|
+
"status": "paid",
|
|
65
|
+
"mock": True,
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## 16.3 Adapter 配置
|
|
70
|
+
|
|
71
|
+
### 环境变量配置
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
# .env
|
|
75
|
+
|
|
76
|
+
# 全局模式:real / sandbox / mock / disabled
|
|
77
|
+
THIRD_PARTY__GATEWAY_MODE=mock
|
|
78
|
+
|
|
79
|
+
# 方法级模式覆盖(JSON 格式)
|
|
80
|
+
# 例如:query 方法使用 real,其他方法使用全局配置
|
|
81
|
+
THIRD_PARTY__METHOD_MODES={"query_order": "real"}
|
|
82
|
+
|
|
83
|
+
# Mock 策略:decorator(装饰器)/ auto(自动生成)
|
|
84
|
+
THIRD_PARTY__MOCK_STRATEGY=decorator
|
|
85
|
+
|
|
86
|
+
# 调试模式
|
|
87
|
+
THIRD_PARTY__DEBUG=true
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 代码配置
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from aury.boot.application.adapter import AdapterSettings
|
|
94
|
+
|
|
95
|
+
# 方式 1:从环境变量加载
|
|
96
|
+
settings = AdapterSettings()
|
|
97
|
+
|
|
98
|
+
# 方式 2:代码显式配置
|
|
99
|
+
settings = AdapterSettings(
|
|
100
|
+
mode="mock",
|
|
101
|
+
method_modes={
|
|
102
|
+
"query_order": "real", # query_order 使用真实调用
|
|
103
|
+
"create_order": "mock", # create_order 使用 Mock
|
|
104
|
+
},
|
|
105
|
+
debug=True,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# 创建 Adapter 实例
|
|
109
|
+
adapter = PaymentAdapter("payment", settings)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## 16.4 使用 HttpAdapter
|
|
113
|
+
|
|
114
|
+
对于 HTTP 类第三方 API,推荐使用 `HttpAdapter`:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
from aury.boot.application.adapter import HttpAdapter, AdapterSettings, adapter_method
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class WechatAdapter(HttpAdapter):
|
|
121
|
+
"""微信 API 适配器。"""
|
|
122
|
+
|
|
123
|
+
def __init__(self, settings: AdapterSettings | None = None):
|
|
124
|
+
super().__init__(
|
|
125
|
+
name="wechat",
|
|
126
|
+
settings=settings,
|
|
127
|
+
base_url="https://api.weixin.qq.com",
|
|
128
|
+
timeout=30.0,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
async def _prepare_headers(self, headers: dict | None) -> dict:
|
|
132
|
+
"""添加认证头。"""
|
|
133
|
+
headers = await super()._prepare_headers(headers)
|
|
134
|
+
headers["Authorization"] = f"Bearer {await self._get_access_token()}"
|
|
135
|
+
return headers
|
|
136
|
+
|
|
137
|
+
@adapter_method("send_message")
|
|
138
|
+
async def send_message(self, openid: str, content: str) -> dict:
|
|
139
|
+
"""发送消息。"""
|
|
140
|
+
return await self._request(
|
|
141
|
+
"POST",
|
|
142
|
+
"/cgi-bin/message/send",
|
|
143
|
+
json={"touser": openid, "content": content},
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
@send_message.mock
|
|
147
|
+
async def send_message_mock(self, openid: str, content: str) -> dict:
|
|
148
|
+
"""发送消息(Mock)。"""
|
|
149
|
+
return {"errcode": 0, "errmsg": "ok", "mock": True}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## 16.5 方法级模式覆盖
|
|
153
|
+
|
|
154
|
+
同一 Adapter 的不同方法可以使用不同模式:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
settings = AdapterSettings(
|
|
158
|
+
mode="mock", # 默认 Mock
|
|
159
|
+
method_modes={
|
|
160
|
+
"query_order": "real", # 查询走真实接口
|
|
161
|
+
"create_order": "mock", # 创建走 Mock
|
|
162
|
+
"refund": "disabled", # 退款禁用
|
|
163
|
+
},
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
adapter = PaymentAdapter("payment", settings)
|
|
167
|
+
|
|
168
|
+
# query_order 会调用真实 API
|
|
169
|
+
result = await adapter.query_order("tx_123")
|
|
170
|
+
|
|
171
|
+
# create_order 会调用 Mock 方法
|
|
172
|
+
result = await adapter.create_order(100, "order_123")
|
|
173
|
+
|
|
174
|
+
# refund 会抛出 AdapterDisabledError
|
|
175
|
+
result = await adapter.refund("tx_123") # 抛出异常
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## 16.6 测试中使用
|
|
179
|
+
|
|
180
|
+
### 调用记录追踪
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
import pytest
|
|
184
|
+
from {package_name}.adapters.payment_adapter import PaymentAdapter
|
|
185
|
+
from aury.boot.application.adapter import AdapterSettings
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@pytest.fixture
|
|
189
|
+
def payment_adapter():
|
|
190
|
+
settings = AdapterSettings(mode="mock")
|
|
191
|
+
return PaymentAdapter("payment", settings)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
async def test_create_order(payment_adapter):
|
|
195
|
+
"""测试创建订单。"""
|
|
196
|
+
result = await payment_adapter.create_order(100, "order_001")
|
|
197
|
+
|
|
198
|
+
assert result["success"] is True
|
|
199
|
+
assert result["mock"] is True
|
|
200
|
+
|
|
201
|
+
# 检查调用记录
|
|
202
|
+
history = payment_adapter.call_history
|
|
203
|
+
assert len(history) == 1
|
|
204
|
+
assert history[0].method == "create_order"
|
|
205
|
+
assert history[0].args == (100, "order_001")
|
|
206
|
+
assert history[0].result == result
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
async def test_amount_limit(payment_adapter):
|
|
210
|
+
"""测试金额超限。"""
|
|
211
|
+
result = await payment_adapter.create_order(200000, "order_002")
|
|
212
|
+
|
|
213
|
+
assert result["success"] is False
|
|
214
|
+
assert "超限" in result["error"]
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
async def test_clear_history(payment_adapter):
|
|
218
|
+
"""测试清除调用记录。"""
|
|
219
|
+
await payment_adapter.create_order(100, "order_001")
|
|
220
|
+
await payment_adapter.query_order("tx_001")
|
|
221
|
+
|
|
222
|
+
assert len(payment_adapter.call_history) == 2
|
|
223
|
+
|
|
224
|
+
payment_adapter.clear_history()
|
|
225
|
+
assert len(payment_adapter.call_history) == 0
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## 16.7 高级用法
|
|
229
|
+
|
|
230
|
+
### 钩子方法
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
class PaymentAdapter(HttpAdapter):
|
|
234
|
+
"""带钩子的支付适配器。"""
|
|
235
|
+
|
|
236
|
+
async def _on_before_call(
|
|
237
|
+
self, method: str, args: tuple, kwargs: dict
|
|
238
|
+
) -> None:
|
|
239
|
+
"""调用前钩子。"""
|
|
240
|
+
logger.info(f"调用 {method},参数: {args}")
|
|
241
|
+
|
|
242
|
+
async def _on_after_call(
|
|
243
|
+
self, method: str, args: tuple, kwargs: dict, result: Any
|
|
244
|
+
) -> None:
|
|
245
|
+
"""调用后钩子。"""
|
|
246
|
+
logger.info(f"{method} 返回: {result}")
|
|
247
|
+
|
|
248
|
+
async def _on_call_error(
|
|
249
|
+
self, method: str, args: tuple, kwargs: dict, error: Exception
|
|
250
|
+
) -> None:
|
|
251
|
+
"""调用异常钩子。"""
|
|
252
|
+
logger.error(f"{method} 异常: {error}")
|
|
253
|
+
# 可以在这里发送告警
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### 复合适配器
|
|
257
|
+
|
|
258
|
+
```python
|
|
259
|
+
class CompositePaymentAdapter(BaseAdapter):
|
|
260
|
+
"""组合多个支付渠道。"""
|
|
261
|
+
|
|
262
|
+
def __init__(self, settings: AdapterSettings | None = None):
|
|
263
|
+
super().__init__("composite_payment", settings)
|
|
264
|
+
self.alipay = AlipayAdapter(settings)
|
|
265
|
+
self.wechat = WechatAdapter(settings)
|
|
266
|
+
|
|
267
|
+
@adapter_method("pay")
|
|
268
|
+
async def pay(self, channel: str, amount: int, order_id: str) -> dict:
|
|
269
|
+
"""根据渠道选择支付方式。"""
|
|
270
|
+
if channel == "alipay":
|
|
271
|
+
return await self.alipay.create_order(amount, order_id)
|
|
272
|
+
elif channel == "wechat":
|
|
273
|
+
return await self.wechat.create_order(amount, order_id)
|
|
274
|
+
else:
|
|
275
|
+
raise ValueError(f"不支持的支付渠道: {channel}")
|
|
276
|
+
|
|
277
|
+
@pay.mock
|
|
278
|
+
async def pay_mock(self, channel: str, amount: int, order_id: str) -> dict:
|
|
279
|
+
"""统一 Mock 实现。"""
|
|
280
|
+
return {
|
|
281
|
+
"success": True,
|
|
282
|
+
"channel": channel,
|
|
283
|
+
"transaction_id": f"mock_{channel}_{order_id}",
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
## 16.8 异常处理
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
from aury.boot.application.adapter import (
|
|
291
|
+
AdapterError,
|
|
292
|
+
AdapterDisabledError,
|
|
293
|
+
AdapterTimeoutError,
|
|
294
|
+
AdapterValidationError,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
result = await adapter.create_order(100, "order_001")
|
|
299
|
+
except AdapterDisabledError:
|
|
300
|
+
logger.warning("支付适配器已禁用")
|
|
301
|
+
# 降级处理
|
|
302
|
+
except AdapterTimeoutError:
|
|
303
|
+
logger.error("支付适配器超时")
|
|
304
|
+
# 重试或告警
|
|
305
|
+
except AdapterValidationError as e:
|
|
306
|
+
logger.error(f"参数校验失败: {e}")
|
|
307
|
+
except AdapterError as e:
|
|
308
|
+
logger.error(f"适配器错误: {e}")
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## 16.9 最佳实践
|
|
312
|
+
|
|
313
|
+
### 1. Adapter 放置位置
|
|
314
|
+
|
|
315
|
+
```
|
|
316
|
+
{package_name}/
|
|
317
|
+
├── adapters/ # 第三方适配器
|
|
318
|
+
│ ├── __init__.py
|
|
319
|
+
│ ├── payment_adapter.py # 支付适配器
|
|
320
|
+
│ ├── sms_adapter.py # 短信适配器
|
|
321
|
+
│ └── wechat_adapter.py # 微信适配器
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### 2. Mock 逻辑应覆盖边界情况
|
|
325
|
+
|
|
326
|
+
```python
|
|
327
|
+
@create_order.mock
|
|
328
|
+
async def create_order_mock(self, amount: int, order_id: str) -> dict:
|
|
329
|
+
"""Mock 应模拟各种场景。"""
|
|
330
|
+
# 模拟金额校验
|
|
331
|
+
if amount <= 0:
|
|
332
|
+
return {"success": False, "error": "金额必须大于0"}
|
|
333
|
+
if amount > 100000:
|
|
334
|
+
return {"success": False, "error": "金额超限"}
|
|
335
|
+
|
|
336
|
+
# 模拟偶发失败(可选)
|
|
337
|
+
import random
|
|
338
|
+
if random.random() < 0.01:
|
|
339
|
+
return {"success": False, "error": "系统繁忙"}
|
|
340
|
+
|
|
341
|
+
return {"success": True, "transaction_id": f"mock_{order_id}"}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### 3. 环境配置建议
|
|
345
|
+
|
|
346
|
+
```bash
|
|
347
|
+
# 开发环境 (.env.development)
|
|
348
|
+
THIRD_PARTY__GATEWAY_MODE=mock
|
|
349
|
+
THIRD_PARTY__DEBUG=true
|
|
350
|
+
|
|
351
|
+
# 测试环境 (.env.testing)
|
|
352
|
+
THIRD_PARTY__GATEWAY_MODE=mock
|
|
353
|
+
THIRD_PARTY__METHOD_MODES={"query": "sandbox"}
|
|
354
|
+
|
|
355
|
+
# 生产环境 (.env.production)
|
|
356
|
+
THIRD_PARTY__GATEWAY_MODE=real
|
|
357
|
+
THIRD_PARTY__DEBUG=false
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### 4. 在 Service 中使用
|
|
361
|
+
|
|
362
|
+
```python
|
|
363
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
364
|
+
|
|
365
|
+
from aury.boot.domain.service.base import BaseService
|
|
366
|
+
from aury.boot.domain.transaction import transactional
|
|
367
|
+
|
|
368
|
+
from {package_name}.adapters.payment_adapter import PaymentAdapter
|
|
369
|
+
from {package_name}.repositories.order_repository import OrderRepository
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
class OrderService(BaseService):
|
|
373
|
+
"""订单服务。"""
|
|
374
|
+
|
|
375
|
+
def __init__(self, session: AsyncSession, payment: PaymentAdapter):
|
|
376
|
+
super().__init__(session)
|
|
377
|
+
self.order_repo = OrderRepository(session)
|
|
378
|
+
self.payment = payment
|
|
379
|
+
|
|
380
|
+
@transactional
|
|
381
|
+
async def create_order(self, user_id: str, amount: int) -> Order:
|
|
382
|
+
"""创建订单并发起支付。"""
|
|
383
|
+
# 1. 创建订单记录
|
|
384
|
+
order = await self.order_repo.create({
|
|
385
|
+
"user_id": user_id,
|
|
386
|
+
"amount": amount,
|
|
387
|
+
"status": "pending",
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
# 2. 调用支付适配器
|
|
391
|
+
pay_result = await self.payment.create_order(amount, str(order.id))
|
|
392
|
+
|
|
393
|
+
if not pay_result["success"]:
|
|
394
|
+
raise PaymentError(pay_result["error"])
|
|
395
|
+
|
|
396
|
+
# 3. 更新订单状态
|
|
397
|
+
await self.order_repo.update(order, {
|
|
398
|
+
"transaction_id": pay_result["transaction_id"],
|
|
399
|
+
"status": "paid",
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
return order
|
|
403
|
+
```
|
|
@@ -36,10 +36,10 @@ aury generate schema user # Pydantic Schema
|
|
|
36
36
|
aury generate model user --fields "name:str,email:str,age:int"
|
|
37
37
|
|
|
38
38
|
# 指定模型基类
|
|
39
|
-
aury generate model user --base
|
|
40
|
-
aury generate model user --base UUIDModel # UUID主键 + 时间戳
|
|
39
|
+
aury generate model user --base AuditableStateModel # int主键 + 软删除(推荐)
|
|
41
40
|
aury generate model user --base Model # int主键 + 时间戳
|
|
42
|
-
aury generate model user --base
|
|
41
|
+
aury generate model user --base FullFeaturedModel # int主键 + 全功能
|
|
42
|
+
aury generate model user --base UUIDAuditableStateModel # UUID主键(如需要)
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
## 数据库迁移
|
|
@@ -65,13 +65,13 @@ aury worker # 运行 Dramatiq Worker
|
|
|
65
65
|
|
|
66
66
|
| 变量 | 说明 | 默认值 |
|
|
67
67
|
|------|------|--------|
|
|
68
|
-
| `
|
|
69
|
-
| `
|
|
70
|
-
| `
|
|
71
|
-
| `
|
|
72
|
-
| `
|
|
73
|
-
| `
|
|
74
|
-
| `
|
|
68
|
+
| `DATABASE__URL` | 数据库连接 URL | `sqlite+aiosqlite:///./dev.db` |
|
|
69
|
+
| `CACHE__CACHE_TYPE` | 缓存类型 (memory/redis) | `memory` |
|
|
70
|
+
| `CACHE__URL` | Redis URL | - |
|
|
71
|
+
| `LOG__LEVEL` | 日志级别 | `INFO` |
|
|
72
|
+
| `LOG__DIR` | 日志目录 | `logs` |
|
|
73
|
+
| `SCHEDULER__ENABLED` | 启用内嵌调度器 | `true` |
|
|
74
|
+
| `TASK__BROKER_URL` | 任务队列 Broker URL | - |
|
|
75
75
|
|
|
76
76
|
## 管理后台(Admin Console)
|
|
77
77
|
|
|
@@ -81,12 +81,12 @@ aury worker # 运行 Dramatiq Worker
|
|
|
81
81
|
|
|
82
82
|
| 变量 | 说明 | 默认值 |
|
|
83
83
|
|------|------|--------|
|
|
84
|
-
| `
|
|
85
|
-
| `
|
|
86
|
-
| `
|
|
87
|
-
| `
|
|
88
|
-
| `
|
|
89
|
-
| `
|
|
90
|
-
| `
|
|
91
|
-
| `
|
|
92
|
-
| `
|
|
84
|
+
| `ADMIN__ENABLED` | 是否启用管理后台 | `false` |
|
|
85
|
+
| `ADMIN__PATH` | 管理后台路径 | `/api/admin-console` |
|
|
86
|
+
| `ADMIN__DATABASE_URL` | 管理后台同步数据库 URL(可覆盖自动推导) | - |
|
|
87
|
+
| `ADMIN__AUTH_MODE` | 认证模式(basic/bearer/none/custom/jwt) | `basic` |
|
|
88
|
+
| `ADMIN__AUTH_SECRET_KEY` | session 签名密钥(生产必配) | - |
|
|
89
|
+
| `ADMIN__AUTH_BASIC_USERNAME` | basic 用户名 | - |
|
|
90
|
+
| `ADMIN__AUTH_BASIC_PASSWORD` | basic 密码 | - |
|
|
91
|
+
| `ADMIN__AUTH_BEARER_TOKENS` | bearer token 白名单 | `[]` |
|
|
92
|
+
| `ADMIN__AUTH_BACKEND` | 自定义认证后端导入路径(module:attr) | - |
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
配置优先级:命令行参数 > 环境变量 > .env 文件 > 默认值
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
环境变量格式(使用双下划线分层):
|
|
6
|
+
DATABASE__URL=postgresql+asyncpg://user:pass@localhost:5432/mydb
|
|
7
|
+
CACHE__CACHE_TYPE=redis
|
|
8
|
+
CACHE__URL=redis://localhost:6379/0
|
|
9
|
+
LOG__LEVEL=INFO
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
from aury.boot.application.config import BaseConfig
|
|
@@ -16,11 +16,11 @@ class AppConfig(BaseConfig):
|
|
|
16
16
|
"""{project_name} 配置。
|
|
17
17
|
|
|
18
18
|
继承 BaseConfig 获得所有默认配置项:
|
|
19
|
-
- server: 服务器配置
|
|
20
|
-
- database: 数据库配置
|
|
21
|
-
- cache: 缓存配置
|
|
22
|
-
- log: 日志配置
|
|
23
|
-
- migration: 迁移配置
|
|
19
|
+
- server: 服务器配置 (SERVER__*)
|
|
20
|
+
- database: 数据库配置 (DATABASE__*)
|
|
21
|
+
- cache: 缓存配置 (CACHE__*)
|
|
22
|
+
- log: 日志配置 (LOG__*)
|
|
23
|
+
- migration: 迁移配置 (MIGRATION__*)
|
|
24
24
|
|
|
25
25
|
可以在这里添加自定义配置项。
|
|
26
26
|
"""
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# {project_name} 环境变量配置
|
|
3
|
+
# =============================================================================
|
|
4
|
+
# 复制此文件为 .env 并根据实际情况修改
|
|
5
|
+
# 所有配置项均有默认值,只需取消注释并修改需要覆盖的项
|
|
6
|
+
#
|
|
7
|
+
# 环境变量格式说明:
|
|
8
|
+
# - 使用双下划线 (__) 作为层级分隔符
|
|
9
|
+
# - 单实例: {{SECTION}}__{{FIELD}}=value
|
|
10
|
+
# - 多实例: {{SECTION}}__{{INSTANCE}}__{{FIELD}}=value
|