aury-boot 0.0.4__py3-none-any.whl → 0.0.5__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/__init__.py +2 -2
- aury/boot/_version.py +2 -2
- aury/boot/application/__init__.py +45 -36
- aury/boot/application/app/__init__.py +12 -8
- aury/boot/application/app/base.py +12 -0
- aury/boot/application/app/components.py +137 -44
- aury/boot/application/app/middlewares.py +2 -0
- aury/boot/application/app/startup.py +249 -0
- aury/boot/application/config/__init__.py +36 -1
- aury/boot/application/config/multi_instance.py +200 -0
- aury/boot/application/config/settings.py +341 -12
- aury/boot/application/constants/components.py +6 -0
- aury/boot/application/errors/handlers.py +17 -3
- aury/boot/application/middleware/logging.py +8 -120
- aury/boot/application/rpc/__init__.py +2 -2
- aury/boot/commands/__init__.py +30 -10
- aury/boot/commands/app.py +131 -1
- aury/boot/commands/docs.py +104 -17
- aury/boot/commands/init.py +27 -8
- aury/boot/commands/server/app.py +2 -3
- aury/boot/commands/templates/project/AGENTS.md.tpl +217 -0
- aury/boot/commands/templates/project/README.md.tpl +2 -2
- aury/boot/commands/templates/project/aury_docs/00-overview.md.tpl +59 -0
- aury/boot/commands/templates/project/aury_docs/01-model.md.tpl +183 -0
- aury/boot/commands/templates/project/aury_docs/02-repository.md.tpl +206 -0
- aury/boot/commands/templates/project/aury_docs/03-service.md.tpl +398 -0
- aury/boot/commands/templates/project/aury_docs/04-schema.md.tpl +95 -0
- aury/boot/commands/templates/project/aury_docs/05-api.md.tpl +116 -0
- aury/boot/commands/templates/project/aury_docs/06-exception.md.tpl +118 -0
- aury/boot/commands/templates/project/aury_docs/07-cache.md.tpl +122 -0
- aury/boot/commands/templates/project/aury_docs/08-scheduler.md.tpl +32 -0
- aury/boot/commands/templates/project/aury_docs/09-tasks.md.tpl +38 -0
- aury/boot/commands/templates/project/aury_docs/10-storage.md.tpl +115 -0
- aury/boot/commands/templates/project/aury_docs/11-logging.md.tpl +92 -0
- aury/boot/commands/templates/project/aury_docs/12-admin.md.tpl +56 -0
- aury/boot/commands/templates/project/aury_docs/13-channel.md.tpl +92 -0
- aury/boot/commands/templates/project/aury_docs/14-mq.md.tpl +102 -0
- aury/boot/commands/templates/project/aury_docs/15-events.md.tpl +147 -0
- aury/boot/commands/templates/project/config.py.tpl +1 -1
- aury/boot/commands/templates/project/env.example.tpl +73 -5
- aury/boot/commands/templates/project/modules/tasks.py.tpl +1 -1
- aury/boot/contrib/admin_console/auth.py +2 -3
- aury/boot/contrib/admin_console/install.py +1 -1
- aury/boot/domain/models/mixins.py +48 -1
- aury/boot/domain/pagination/__init__.py +94 -0
- aury/boot/domain/repository/impl.py +1 -1
- aury/boot/domain/repository/interface.py +1 -1
- aury/boot/domain/transaction/__init__.py +8 -9
- aury/boot/infrastructure/__init__.py +86 -29
- aury/boot/infrastructure/cache/backends.py +102 -18
- aury/boot/infrastructure/cache/base.py +12 -0
- aury/boot/infrastructure/cache/manager.py +153 -91
- aury/boot/infrastructure/channel/__init__.py +24 -0
- aury/boot/infrastructure/channel/backends/__init__.py +9 -0
- aury/boot/infrastructure/channel/backends/memory.py +83 -0
- aury/boot/infrastructure/channel/backends/redis.py +88 -0
- aury/boot/infrastructure/channel/base.py +92 -0
- aury/boot/infrastructure/channel/manager.py +203 -0
- aury/boot/infrastructure/clients/__init__.py +22 -0
- aury/boot/infrastructure/clients/rabbitmq/__init__.py +9 -0
- aury/boot/infrastructure/clients/rabbitmq/config.py +46 -0
- aury/boot/infrastructure/clients/rabbitmq/manager.py +288 -0
- aury/boot/infrastructure/clients/redis/__init__.py +28 -0
- aury/boot/infrastructure/clients/redis/config.py +51 -0
- aury/boot/infrastructure/clients/redis/manager.py +264 -0
- aury/boot/infrastructure/database/config.py +1 -2
- aury/boot/infrastructure/database/manager.py +16 -38
- aury/boot/infrastructure/events/__init__.py +18 -21
- aury/boot/infrastructure/events/backends/__init__.py +11 -0
- aury/boot/infrastructure/events/backends/memory.py +86 -0
- aury/boot/infrastructure/events/backends/rabbitmq.py +193 -0
- aury/boot/infrastructure/events/backends/redis.py +162 -0
- aury/boot/infrastructure/events/base.py +127 -0
- aury/boot/infrastructure/events/manager.py +224 -0
- aury/boot/infrastructure/mq/__init__.py +24 -0
- aury/boot/infrastructure/mq/backends/__init__.py +9 -0
- aury/boot/infrastructure/mq/backends/rabbitmq.py +179 -0
- aury/boot/infrastructure/mq/backends/redis.py +167 -0
- aury/boot/infrastructure/mq/base.py +143 -0
- aury/boot/infrastructure/mq/manager.py +239 -0
- aury/boot/infrastructure/scheduler/manager.py +7 -3
- aury/boot/infrastructure/storage/__init__.py +9 -9
- aury/boot/infrastructure/storage/base.py +17 -5
- aury/boot/infrastructure/storage/factory.py +0 -1
- aury/boot/infrastructure/tasks/__init__.py +2 -2
- aury/boot/infrastructure/tasks/manager.py +47 -29
- aury/boot/testing/base.py +2 -2
- {aury_boot-0.0.4.dist-info → aury_boot-0.0.5.dist-info}/METADATA +19 -2
- aury_boot-0.0.5.dist-info/RECORD +176 -0
- aury/boot/commands/templates/project/DEVELOPMENT.md.tpl +0 -1397
- aury/boot/infrastructure/events/bus.py +0 -362
- aury/boot/infrastructure/events/config.py +0 -52
- aury/boot/infrastructure/events/consumer.py +0 -134
- aury/boot/infrastructure/events/models.py +0 -63
- aury_boot-0.0.4.dist-info/RECORD +0 -137
- /aury/boot/commands/templates/project/{CLI.md.tpl → aury_docs/99-cli.md.tpl} +0 -0
- {aury_boot-0.0.4.dist-info → aury_boot-0.0.5.dist-info}/WHEEL +0 -0
- {aury_boot-0.0.4.dist-info → aury_boot-0.0.5.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
# Service(业务逻辑层)
|
|
2
|
+
|
|
3
|
+
## 3.1 Service 编写示例
|
|
4
|
+
|
|
5
|
+
**文件**: `{package_name}/services/user_service.py`
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
"""User 业务逻辑层。"""
|
|
9
|
+
|
|
10
|
+
from uuid import UUID
|
|
11
|
+
|
|
12
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
13
|
+
|
|
14
|
+
from aury.boot.application.errors import AlreadyExistsError, NotFoundError
|
|
15
|
+
from aury.boot.domain.service.base import BaseService
|
|
16
|
+
from aury.boot.domain.transaction import transactional
|
|
17
|
+
|
|
18
|
+
from {package_name}.models.user import User
|
|
19
|
+
from {package_name}.repositories.user_repository import UserRepository
|
|
20
|
+
from {package_name}.schemas.user import UserCreate, UserUpdate
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class UserService(BaseService):
|
|
24
|
+
"""User 服务。"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, session: AsyncSession) -> None:
|
|
27
|
+
super().__init__(session)
|
|
28
|
+
self.repo = UserRepository(session, User)
|
|
29
|
+
|
|
30
|
+
async def get(self, id: UUID) -> User:
|
|
31
|
+
"""获取 User。"""
|
|
32
|
+
entity = await self.repo.get(id)
|
|
33
|
+
if not entity:
|
|
34
|
+
raise NotFoundError("User 不存在", resource=id)
|
|
35
|
+
return entity
|
|
36
|
+
|
|
37
|
+
async def list(self, skip: int = 0, limit: int = 100) -> list[User]:
|
|
38
|
+
"""获取 User 列表。"""
|
|
39
|
+
return await self.repo.list(skip=skip, limit=limit)
|
|
40
|
+
|
|
41
|
+
@transactional
|
|
42
|
+
async def create(self, data: UserCreate) -> User:
|
|
43
|
+
"""创建 User。"""
|
|
44
|
+
if await self.repo.exists(email=data.email):
|
|
45
|
+
raise AlreadyExistsError(f"邮箱 {{data.email}} 已存在")
|
|
46
|
+
return await self.repo.create(data.model_dump())
|
|
47
|
+
|
|
48
|
+
@transactional
|
|
49
|
+
async def update(self, id: UUID, data: UserUpdate) -> User:
|
|
50
|
+
"""更新 User。"""
|
|
51
|
+
entity = await self.get(id)
|
|
52
|
+
return await self.repo.update(entity, data.model_dump(exclude_unset=True))
|
|
53
|
+
|
|
54
|
+
@transactional
|
|
55
|
+
async def delete(self, id: UUID) -> None:
|
|
56
|
+
"""删除 User。"""
|
|
57
|
+
entity = await self.get(id)
|
|
58
|
+
await self.repo.delete(entity)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## 3.2 跨 Service 调用(事务共享)
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
class OrderService(BaseService):
|
|
65
|
+
def __init__(self, session: AsyncSession) -> None:
|
|
66
|
+
super().__init__(session)
|
|
67
|
+
self.order_repo = OrderRepository(session, Order)
|
|
68
|
+
# 复用 session 实现事务共享
|
|
69
|
+
self.user_service = UserService(session)
|
|
70
|
+
self.inventory_service = InventoryService(session)
|
|
71
|
+
|
|
72
|
+
@transactional
|
|
73
|
+
async def create_order(self, data: OrderCreate) -> Order:
|
|
74
|
+
"""创建订单(跨 Service 事务)。"""
|
|
75
|
+
user = await self.user_service.get(data.user_id)
|
|
76
|
+
await self.inventory_service.deduct(data.product_id, data.quantity)
|
|
77
|
+
order = await self.order_repo.create(...)
|
|
78
|
+
return order # 整个流程在同一事务中
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 数据库事务
|
|
84
|
+
|
|
85
|
+
### 3.3 事务装饰器(推荐)
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from aury.boot.domain.transaction import transactional
|
|
89
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
90
|
+
|
|
91
|
+
# 自动识别 session 参数,自动提交/回滚
|
|
92
|
+
@transactional
|
|
93
|
+
async def create_user(session: AsyncSession, name: str, email: str):
|
|
94
|
+
"""创建用户,自动在事务中执行。"""
|
|
95
|
+
repo = UserRepository(session)
|
|
96
|
+
user = await repo.create({{"name": name, "email": email}})
|
|
97
|
+
# 成功:自动 commit
|
|
98
|
+
# 异常:自动 rollback
|
|
99
|
+
return user
|
|
100
|
+
|
|
101
|
+
# 在类方法中使用(自动识别 self.session)
|
|
102
|
+
class UserService:
|
|
103
|
+
def __init__(self, session: AsyncSession):
|
|
104
|
+
self.session = session
|
|
105
|
+
|
|
106
|
+
@transactional
|
|
107
|
+
async def create_with_profile(self, name: str):
|
|
108
|
+
"""自动使用 self.session。"""
|
|
109
|
+
user = await self.repo.create({{"name": name}})
|
|
110
|
+
await self.profile_repo.create({{"user_id": user.id}})
|
|
111
|
+
return user
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### 3.4 事务上下文管理器
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
from aury.boot.domain.transaction import transactional_context
|
|
118
|
+
from aury.boot.infrastructure.database import DatabaseManager
|
|
119
|
+
|
|
120
|
+
db = DatabaseManager.get_instance()
|
|
121
|
+
|
|
122
|
+
async with db.session() as session:
|
|
123
|
+
async with transactional_context(session):
|
|
124
|
+
repo1 = UserRepository(session)
|
|
125
|
+
repo2 = ProfileRepository(session)
|
|
126
|
+
|
|
127
|
+
user = await repo1.create({{"name": "Alice"}})
|
|
128
|
+
await repo2.create({{"user_id": user.id}})
|
|
129
|
+
# 自动提交或回滚
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### 3.5 事务传播(嵌套事务)
|
|
133
|
+
|
|
134
|
+
框架自动支持嵌套事务,内层事务会复用外层事务:
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
@transactional
|
|
138
|
+
async def outer_operation(session: AsyncSession):
|
|
139
|
+
"""外层事务。"""
|
|
140
|
+
repo1 = UserRepository(session)
|
|
141
|
+
user = await repo1.create({{"name": "Alice"}})
|
|
142
|
+
|
|
143
|
+
# 嵌套调用另一个 @transactional 函数
|
|
144
|
+
result = await inner_operation(session)
|
|
145
|
+
# 不会重复开启事务,复用外层事务
|
|
146
|
+
# 只有外层事务提交时才会真正提交
|
|
147
|
+
|
|
148
|
+
return user, result
|
|
149
|
+
|
|
150
|
+
@transactional
|
|
151
|
+
async def inner_operation(session: AsyncSession):
|
|
152
|
+
"""内层事务,自动复用外层事务。"""
|
|
153
|
+
repo2 = OrderRepository(session)
|
|
154
|
+
return await repo2.create({{"user_id": 1}})
|
|
155
|
+
# 检测到已在事务中,直接执行,不重复提交
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**传播行为**:
|
|
159
|
+
- 如果已在事务中:直接执行,不开启新事务
|
|
160
|
+
- 如果不在事务中:开启新事务,自动提交/回滚
|
|
161
|
+
- 嵌套事务共享同一个数据库连接和事务上下文
|
|
162
|
+
|
|
163
|
+
### 3.6 非事务的数据库使用
|
|
164
|
+
|
|
165
|
+
对于只读操作或不需要事务的场景,可以直接使用 session:
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
from aury.boot.infrastructure.database import DatabaseManager
|
|
169
|
+
|
|
170
|
+
db = DatabaseManager.get_instance()
|
|
171
|
+
|
|
172
|
+
# 方式 1:使用 session 上下文管理器(推荐)
|
|
173
|
+
async with db.session() as session:
|
|
174
|
+
repo = UserRepository(session)
|
|
175
|
+
# 只读操作,不需要事务
|
|
176
|
+
users = await repo.list(skip=0, limit=10)
|
|
177
|
+
user = await repo.get(1)
|
|
178
|
+
|
|
179
|
+
# 方式 2:在 FastAPI 路由中使用(自动注入)
|
|
180
|
+
from fastapi import Depends
|
|
181
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
182
|
+
|
|
183
|
+
@router.get("/users")
|
|
184
|
+
async def list_users(
|
|
185
|
+
session: AsyncSession = Depends(db.get_session),
|
|
186
|
+
):
|
|
187
|
+
"""只读操作,不需要事务。"""
|
|
188
|
+
repo = UserRepository(session)
|
|
189
|
+
return await repo.list()
|
|
190
|
+
|
|
191
|
+
# 方式 3:手动控制(需要手动关闭)
|
|
192
|
+
session = await db.create_session()
|
|
193
|
+
try:
|
|
194
|
+
repo = UserRepository(session)
|
|
195
|
+
users = await repo.list()
|
|
196
|
+
finally:
|
|
197
|
+
await session.close()
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**何时使用非事务**:
|
|
201
|
+
- 只读查询(SELECT)
|
|
202
|
+
- 不需要原子性的操作
|
|
203
|
+
- 性能敏感的场景(避免事务开销)
|
|
204
|
+
|
|
205
|
+
**何时必须使用事务**:
|
|
206
|
+
- 写操作(INSERT/UPDATE/DELETE)
|
|
207
|
+
- 需要原子性的多个操作
|
|
208
|
+
- 需要回滚的场景
|
|
209
|
+
|
|
210
|
+
### 3.7 写入不使用事务装饰器
|
|
211
|
+
|
|
212
|
+
某些场景下,你可能希望在 Service 内不加 `@transactional`,而由上层控制事务边界:
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
# 场景:Service 内部不加装饰器,由调用方控制事务
|
|
216
|
+
class UserWriteService(BaseService):
|
|
217
|
+
async def create_no_tx(self, data: UserCreate) -> User:
|
|
218
|
+
if await self.repo.exists(email=data.email):
|
|
219
|
+
raise AlreadyExistsError(f"邮箱 {{data.email}} 已存在")
|
|
220
|
+
# 只做 flush/refresh,不做 commit
|
|
221
|
+
return await self.repo.create(data.model_dump())
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# 调用方显式管理事务边界
|
|
225
|
+
async def create_user_flow(session: AsyncSession, data: UserCreate) -> User:
|
|
226
|
+
service = UserWriteService(session)
|
|
227
|
+
try:
|
|
228
|
+
user = await service.create_no_tx(data)
|
|
229
|
+
await session.commit()
|
|
230
|
+
return user
|
|
231
|
+
except Exception:
|
|
232
|
+
await session.rollback()
|
|
233
|
+
raise
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# 或使用 transactional_context
|
|
237
|
+
from aury.boot.domain.transaction import transactional_context
|
|
238
|
+
|
|
239
|
+
async def create_user_flow(session: AsyncSession, data: UserCreate) -> User:
|
|
240
|
+
async with transactional_context(session):
|
|
241
|
+
service = UserWriteService(session)
|
|
242
|
+
return await service.create_no_tx(data)
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
适用场景:
|
|
246
|
+
- 一个用例调用多个 Service,统一提交
|
|
247
|
+
- 手动控制 commit 时机(分步 flush、条件提交)
|
|
248
|
+
- 上层已有事务边界(如作业层)
|
|
249
|
+
|
|
250
|
+
### 3.8 Savepoints(保存点)
|
|
251
|
+
|
|
252
|
+
保存点允许在事务中设置回滚点,实现部分回滚而不影响整个事务:
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
from aury.boot.domain.transaction import TransactionManager
|
|
256
|
+
|
|
257
|
+
async def complex_operation(session: AsyncSession):
|
|
258
|
+
"""使用保存点实现部分回滚。"""
|
|
259
|
+
tm = TransactionManager(session)
|
|
260
|
+
|
|
261
|
+
await tm.begin()
|
|
262
|
+
repo = UserRepository(session)
|
|
263
|
+
|
|
264
|
+
try:
|
|
265
|
+
# 第一步:创建主记录
|
|
266
|
+
user = await repo.create({{"name": "alice"}})
|
|
267
|
+
|
|
268
|
+
# 创建保存点
|
|
269
|
+
sp_id = await tm.savepoint("before_optional")
|
|
270
|
+
|
|
271
|
+
try:
|
|
272
|
+
# 第二步:可选操作(可能失败)
|
|
273
|
+
await risky_operation(session)
|
|
274
|
+
# 成功:提交保存点
|
|
275
|
+
await tm.savepoint_commit(sp_id)
|
|
276
|
+
except RiskyOperationError:
|
|
277
|
+
# 失败:回滚到保存点,但 user 创建仍然保留
|
|
278
|
+
await tm.savepoint_rollback(sp_id)
|
|
279
|
+
logger.warning("可选操作失败,已回滚,继续主流程")
|
|
280
|
+
|
|
281
|
+
# 第三步:继续其他操作(不受保存点回滚影响)
|
|
282
|
+
await repo.update(user.id, {{"status": "active"}})
|
|
283
|
+
|
|
284
|
+
await tm.commit()
|
|
285
|
+
return user
|
|
286
|
+
except Exception:
|
|
287
|
+
await tm.rollback()
|
|
288
|
+
raise
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
**保存点 API**:
|
|
292
|
+
- `savepoint(name)` - 创建保存点,返回保存点 ID
|
|
293
|
+
- `savepoint_commit(sp_id)` - 提交保存点(释放保存点,变更生效)
|
|
294
|
+
- `savepoint_rollback(sp_id)` - 回滚到保存点(撤销保存点后的变更)
|
|
295
|
+
|
|
296
|
+
### 3.9 on_commit 回调
|
|
297
|
+
|
|
298
|
+
注册在事务成功提交后执行的回调函数,适合发送通知、触发后续任务等副作用操作:
|
|
299
|
+
|
|
300
|
+
```python
|
|
301
|
+
from aury.boot.domain.transaction import transactional, on_commit
|
|
302
|
+
|
|
303
|
+
@transactional
|
|
304
|
+
async def create_order(session: AsyncSession, order_data: dict):
|
|
305
|
+
"""创建订单并在提交后发送通知。"""
|
|
306
|
+
repo = OrderRepository(session)
|
|
307
|
+
order = await repo.create(order_data)
|
|
308
|
+
|
|
309
|
+
# 注册回调:事务成功后执行
|
|
310
|
+
on_commit(lambda: send_order_notification(order.id))
|
|
311
|
+
on_commit(lambda: update_inventory_cache(order.items))
|
|
312
|
+
|
|
313
|
+
# 如果后续发生异常导致回滚,回调不会执行
|
|
314
|
+
await validate_order(order)
|
|
315
|
+
|
|
316
|
+
return order
|
|
317
|
+
# 事务 commit 后,所有 on_commit 回调按注册顺序执行
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
**在 TransactionManager 中使用**:
|
|
321
|
+
|
|
322
|
+
```python
|
|
323
|
+
async def manual_with_callback(session: AsyncSession):
|
|
324
|
+
tm = TransactionManager(session)
|
|
325
|
+
|
|
326
|
+
await tm.begin()
|
|
327
|
+
try:
|
|
328
|
+
user = await create_user(session)
|
|
329
|
+
tm.on_commit(lambda: print(f"用户 {{user.id}} 创建成功"))
|
|
330
|
+
await tm.commit() # 提交后执行回调
|
|
331
|
+
except Exception:
|
|
332
|
+
await tm.rollback() # 回滚时回调被清除,不执行
|
|
333
|
+
raise
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
**回调特性**:
|
|
337
|
+
- 事务成功 `commit()` 后立即执行
|
|
338
|
+
- 事务回滚时,已注册的回调被清除,不执行
|
|
339
|
+
- 按注册顺序执行
|
|
340
|
+
- 同步和异步函数都支持
|
|
341
|
+
|
|
342
|
+
### 3.10 SELECT FOR UPDATE(行级锁)
|
|
343
|
+
|
|
344
|
+
在并发场景下锁定查询的行,防止其他事务修改:
|
|
345
|
+
|
|
346
|
+
```python
|
|
347
|
+
from aury.boot.domain.repository.query_builder import QueryBuilder
|
|
348
|
+
|
|
349
|
+
class AccountRepository(BaseRepository[Account]):
|
|
350
|
+
async def get_for_update(self, account_id: str) -> Account | None:
|
|
351
|
+
"""获取并锁定账户记录。"""
|
|
352
|
+
qb = QueryBuilder(Account)
|
|
353
|
+
query = qb.filter(Account.id == account_id).for_update().build()
|
|
354
|
+
result = await self.session.execute(query)
|
|
355
|
+
return result.scalar_one_or_none()
|
|
356
|
+
|
|
357
|
+
async def get_for_update_nowait(self, account_id: str) -> Account | None:
|
|
358
|
+
"""获取并锁定,如果已被锁定则立即失败。"""
|
|
359
|
+
qb = QueryBuilder(Account)
|
|
360
|
+
query = qb.filter(Account.id == account_id).for_update(nowait=True).build()
|
|
361
|
+
result = await self.session.execute(query)
|
|
362
|
+
return result.scalar_one_or_none()
|
|
363
|
+
|
|
364
|
+
async def get_for_update_skip_locked(self, ids: list[str]) -> list[Account]:
|
|
365
|
+
"""获取并锁定,跳过已被锁定的行。"""
|
|
366
|
+
qb = QueryBuilder(Account)
|
|
367
|
+
query = qb.filter(Account.id.in_(ids)).for_update(skip_locked=True).build()
|
|
368
|
+
result = await self.session.execute(query)
|
|
369
|
+
return list(result.scalars().all())
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
**for_update 参数**:
|
|
373
|
+
- `nowait=True` - 如果行已被锁定,立即报错而不是等待
|
|
374
|
+
- `skip_locked=True` - 跳过已被锁定的行(常用于队列场景)
|
|
375
|
+
- `of=(Column,...)` - 指定锁定的列(用于 JOIN 场景)
|
|
376
|
+
|
|
377
|
+
**注意**:`nowait` 和 `skip_locked` 互斥,不能同时使用。
|
|
378
|
+
|
|
379
|
+
### 3.11 事务隔离级别配置
|
|
380
|
+
|
|
381
|
+
通过环境变量配置数据库的默认事务隔离级别:
|
|
382
|
+
|
|
383
|
+
```bash
|
|
384
|
+
# .env
|
|
385
|
+
DATABASE_ISOLATION_LEVEL=REPEATABLE READ
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
**支持的隔离级别**:
|
|
389
|
+
- `READ UNCOMMITTED` - 最低隔离,可读未提交数据(脏读)
|
|
390
|
+
- `READ COMMITTED` - 只读已提交数据(PostgreSQL/Oracle 默认)
|
|
391
|
+
- `REPEATABLE READ` - 可重复读,同一事务内读取结果一致(MySQL 默认)
|
|
392
|
+
- `SERIALIZABLE` - 最高隔离,完全串行化执行
|
|
393
|
+
- `AUTOCOMMIT` - 每条语句自动提交
|
|
394
|
+
|
|
395
|
+
**选择建议**:
|
|
396
|
+
- 大多数场景:`READ COMMITTED`(平衡性能和一致性)
|
|
397
|
+
- 报表/统计查询:`REPEATABLE READ`(保证读取一致性)
|
|
398
|
+
- 金融交易:`SERIALIZABLE`(最强一致性,性能较低)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Schema(Pydantic 模型)
|
|
2
|
+
|
|
3
|
+
## 4.1 Schema 编写示例
|
|
4
|
+
|
|
5
|
+
**文件**: `{package_name}/schemas/user.py`
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
"""User Pydantic 模型。"""
|
|
9
|
+
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from uuid import UUID
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, ConfigDict, Field, EmailStr
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class UserBase(BaseModel):
|
|
17
|
+
"""User 基础模型。"""
|
|
18
|
+
name: str = Field(..., min_length=1, max_length=100, description="用户名")
|
|
19
|
+
email: EmailStr = Field(..., description="邮箱")
|
|
20
|
+
is_active: bool = Field(default=True, description="是否激活")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class UserCreate(UserBase):
|
|
24
|
+
"""创建 User 请求。"""
|
|
25
|
+
password: str = Field(..., min_length=6, description="密码")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class UserUpdate(BaseModel):
|
|
29
|
+
"""更新 User 请求(所有字段可选)。"""
|
|
30
|
+
name: str | None = Field(default=None, min_length=1, max_length=100)
|
|
31
|
+
email: EmailStr | None = None
|
|
32
|
+
is_active: bool | None = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class UserResponse(UserBase):
|
|
36
|
+
"""User 响应。"""
|
|
37
|
+
id: UUID
|
|
38
|
+
created_at: datetime
|
|
39
|
+
updated_at: datetime
|
|
40
|
+
|
|
41
|
+
model_config = ConfigDict(from_attributes=True)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 4.2 Schema 使用说明
|
|
45
|
+
|
|
46
|
+
Schema(Pydantic 模型)在框架中扮演三个角色:
|
|
47
|
+
|
|
48
|
+
1. **请求验证**:验证 API 请求数据(Create/Update)
|
|
49
|
+
2. **响应序列化**:将 ORM 模型转换为 JSON 响应(Response)
|
|
50
|
+
3. **类型提示**:为 Service 层提供类型安全
|
|
51
|
+
|
|
52
|
+
**数据流转**:
|
|
53
|
+
```
|
|
54
|
+
API 请求 → Schema 验证 → Service 处理 → Repository 操作 → ORM Model
|
|
55
|
+
↓ ↓
|
|
56
|
+
model_dump() model_validate()
|
|
57
|
+
↓
|
|
58
|
+
Response Schema → JSON 响应
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**关键方法**:
|
|
62
|
+
- `model_dump()`:将 Schema 转为字典(传给 Repository)
|
|
63
|
+
- `model_dump(exclude_unset=True)`:只转换设置过的字段(用于更新)
|
|
64
|
+
- `model_validate(orm_obj)`:从 ORM 模型创建 Schema(需要 `from_attributes=True`)
|
|
65
|
+
|
|
66
|
+
> **重要提示**:不要自定义通用响应 Schema(如 `OkResponse`、`ErrorResponse`)。
|
|
67
|
+
> 框架已内置 `BaseResponse` 和 `PaginationResponse`,直接使用即可。
|
|
68
|
+
> 异常响应由全局异常处理中间件自动处理,无需手动定义。
|
|
69
|
+
|
|
70
|
+
## 4.3 常用验证
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from pydantic import BaseModel, Field, field_validator
|
|
74
|
+
from typing import Literal
|
|
75
|
+
|
|
76
|
+
class ExampleSchema(BaseModel):
|
|
77
|
+
# 长度限制
|
|
78
|
+
name: str = Field(..., min_length=1, max_length=100)
|
|
79
|
+
|
|
80
|
+
# 数值范围
|
|
81
|
+
age: int = Field(..., ge=0, le=150)
|
|
82
|
+
price: float = Field(..., gt=0)
|
|
83
|
+
|
|
84
|
+
# 正则验证
|
|
85
|
+
phone: str = Field(..., pattern=r"^1[3-9]\d{{9}}$")
|
|
86
|
+
|
|
87
|
+
# 枚举
|
|
88
|
+
status: Literal["active", "inactive", "pending"]
|
|
89
|
+
|
|
90
|
+
# 字段验证器
|
|
91
|
+
@field_validator("name")
|
|
92
|
+
@classmethod
|
|
93
|
+
def name_strip(cls, v: str) -> str:
|
|
94
|
+
return v.strip()
|
|
95
|
+
```
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# API(路由层)
|
|
2
|
+
|
|
3
|
+
## 5.1 API 编写示例
|
|
4
|
+
|
|
5
|
+
**文件**: `{package_name}/api/user.py`
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
"""User API 路由。"""
|
|
9
|
+
|
|
10
|
+
from uuid import UUID
|
|
11
|
+
|
|
12
|
+
from fastapi import APIRouter, Depends
|
|
13
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
14
|
+
|
|
15
|
+
from aury.boot.application.interfaces.egress import (
|
|
16
|
+
BaseResponse,
|
|
17
|
+
Pagination,
|
|
18
|
+
PaginationResponse,
|
|
19
|
+
)
|
|
20
|
+
from aury.boot.infrastructure.database import DatabaseManager
|
|
21
|
+
|
|
22
|
+
from {package_name}.schemas.user import UserCreate, UserResponse, UserUpdate
|
|
23
|
+
from {package_name}.services.user_service import UserService
|
|
24
|
+
|
|
25
|
+
router = APIRouter(prefix="/v1/users", tags=["User"])
|
|
26
|
+
db_manager = DatabaseManager.get_instance()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def get_service(
|
|
30
|
+
session: AsyncSession = Depends(db_manager.get_session),
|
|
31
|
+
) -> UserService:
|
|
32
|
+
return UserService(session)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@router.get("", response_model=PaginationResponse[UserResponse])
|
|
36
|
+
async def list_users(
|
|
37
|
+
skip: int = 0,
|
|
38
|
+
limit: int = 20,
|
|
39
|
+
service: UserService = Depends(get_service),
|
|
40
|
+
) -> PaginationResponse[UserResponse]:
|
|
41
|
+
"""获取列表。"""
|
|
42
|
+
items = await service.list(skip=skip, limit=limit)
|
|
43
|
+
return PaginationResponse(
|
|
44
|
+
code=200,
|
|
45
|
+
message="获取成功",
|
|
46
|
+
data=Pagination(
|
|
47
|
+
total=len(items),
|
|
48
|
+
items=[UserResponse.model_validate(item) for item in items],
|
|
49
|
+
page=skip // limit + 1,
|
|
50
|
+
size=limit,
|
|
51
|
+
),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@router.get("/{{id}}", response_model=BaseResponse[UserResponse])
|
|
56
|
+
async def get_user(
|
|
57
|
+
id: UUID,
|
|
58
|
+
service: UserService = Depends(get_service),
|
|
59
|
+
) -> BaseResponse[UserResponse]:
|
|
60
|
+
"""获取详情。"""
|
|
61
|
+
entity = await service.get(id)
|
|
62
|
+
return BaseResponse(
|
|
63
|
+
code=200,
|
|
64
|
+
message="获取成功",
|
|
65
|
+
data=UserResponse.model_validate(entity),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@router.post("", response_model=BaseResponse[UserResponse])
|
|
70
|
+
async def create_user(
|
|
71
|
+
data: UserCreate,
|
|
72
|
+
service: UserService = Depends(get_service),
|
|
73
|
+
) -> BaseResponse[UserResponse]:
|
|
74
|
+
"""创建。"""
|
|
75
|
+
entity = await service.create(data)
|
|
76
|
+
return BaseResponse(
|
|
77
|
+
code=200,
|
|
78
|
+
message="创建成功",
|
|
79
|
+
data=UserResponse.model_validate(entity),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@router.put("/{{id}}", response_model=BaseResponse[UserResponse])
|
|
84
|
+
async def update_user(
|
|
85
|
+
id: UUID,
|
|
86
|
+
data: UserUpdate,
|
|
87
|
+
service: UserService = Depends(get_service),
|
|
88
|
+
) -> BaseResponse[UserResponse]:
|
|
89
|
+
"""更新。"""
|
|
90
|
+
entity = await service.update(id, data)
|
|
91
|
+
return BaseResponse(
|
|
92
|
+
code=200,
|
|
93
|
+
message="更新成功",
|
|
94
|
+
data=UserResponse.model_validate(entity),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@router.delete("/{{id}}", response_model=BaseResponse[None])
|
|
99
|
+
async def delete_user(
|
|
100
|
+
id: UUID,
|
|
101
|
+
service: UserService = Depends(get_service),
|
|
102
|
+
) -> BaseResponse[None]:
|
|
103
|
+
"""删除。"""
|
|
104
|
+
await service.delete(id)
|
|
105
|
+
return BaseResponse(code=200, message="删除成功", data=None)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## 5.2 注册路由
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from fastapi import FastAPI
|
|
112
|
+
from {package_name}.api.user import router as user_router
|
|
113
|
+
|
|
114
|
+
app = FastAPI()
|
|
115
|
+
app.include_router(user_router)
|
|
116
|
+
```
|