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.
Files changed (98) hide show
  1. aury/boot/__init__.py +2 -2
  2. aury/boot/_version.py +2 -2
  3. aury/boot/application/__init__.py +45 -36
  4. aury/boot/application/app/__init__.py +12 -8
  5. aury/boot/application/app/base.py +12 -0
  6. aury/boot/application/app/components.py +137 -44
  7. aury/boot/application/app/middlewares.py +2 -0
  8. aury/boot/application/app/startup.py +249 -0
  9. aury/boot/application/config/__init__.py +36 -1
  10. aury/boot/application/config/multi_instance.py +200 -0
  11. aury/boot/application/config/settings.py +341 -12
  12. aury/boot/application/constants/components.py +6 -0
  13. aury/boot/application/errors/handlers.py +17 -3
  14. aury/boot/application/middleware/logging.py +8 -120
  15. aury/boot/application/rpc/__init__.py +2 -2
  16. aury/boot/commands/__init__.py +30 -10
  17. aury/boot/commands/app.py +131 -1
  18. aury/boot/commands/docs.py +104 -17
  19. aury/boot/commands/init.py +27 -8
  20. aury/boot/commands/server/app.py +2 -3
  21. aury/boot/commands/templates/project/AGENTS.md.tpl +217 -0
  22. aury/boot/commands/templates/project/README.md.tpl +2 -2
  23. aury/boot/commands/templates/project/aury_docs/00-overview.md.tpl +59 -0
  24. aury/boot/commands/templates/project/aury_docs/01-model.md.tpl +183 -0
  25. aury/boot/commands/templates/project/aury_docs/02-repository.md.tpl +206 -0
  26. aury/boot/commands/templates/project/aury_docs/03-service.md.tpl +398 -0
  27. aury/boot/commands/templates/project/aury_docs/04-schema.md.tpl +95 -0
  28. aury/boot/commands/templates/project/aury_docs/05-api.md.tpl +116 -0
  29. aury/boot/commands/templates/project/aury_docs/06-exception.md.tpl +118 -0
  30. aury/boot/commands/templates/project/aury_docs/07-cache.md.tpl +122 -0
  31. aury/boot/commands/templates/project/aury_docs/08-scheduler.md.tpl +32 -0
  32. aury/boot/commands/templates/project/aury_docs/09-tasks.md.tpl +38 -0
  33. aury/boot/commands/templates/project/aury_docs/10-storage.md.tpl +115 -0
  34. aury/boot/commands/templates/project/aury_docs/11-logging.md.tpl +92 -0
  35. aury/boot/commands/templates/project/aury_docs/12-admin.md.tpl +56 -0
  36. aury/boot/commands/templates/project/aury_docs/13-channel.md.tpl +92 -0
  37. aury/boot/commands/templates/project/aury_docs/14-mq.md.tpl +102 -0
  38. aury/boot/commands/templates/project/aury_docs/15-events.md.tpl +147 -0
  39. aury/boot/commands/templates/project/config.py.tpl +1 -1
  40. aury/boot/commands/templates/project/env.example.tpl +73 -5
  41. aury/boot/commands/templates/project/modules/tasks.py.tpl +1 -1
  42. aury/boot/contrib/admin_console/auth.py +2 -3
  43. aury/boot/contrib/admin_console/install.py +1 -1
  44. aury/boot/domain/models/mixins.py +48 -1
  45. aury/boot/domain/pagination/__init__.py +94 -0
  46. aury/boot/domain/repository/impl.py +1 -1
  47. aury/boot/domain/repository/interface.py +1 -1
  48. aury/boot/domain/transaction/__init__.py +8 -9
  49. aury/boot/infrastructure/__init__.py +86 -29
  50. aury/boot/infrastructure/cache/backends.py +102 -18
  51. aury/boot/infrastructure/cache/base.py +12 -0
  52. aury/boot/infrastructure/cache/manager.py +153 -91
  53. aury/boot/infrastructure/channel/__init__.py +24 -0
  54. aury/boot/infrastructure/channel/backends/__init__.py +9 -0
  55. aury/boot/infrastructure/channel/backends/memory.py +83 -0
  56. aury/boot/infrastructure/channel/backends/redis.py +88 -0
  57. aury/boot/infrastructure/channel/base.py +92 -0
  58. aury/boot/infrastructure/channel/manager.py +203 -0
  59. aury/boot/infrastructure/clients/__init__.py +22 -0
  60. aury/boot/infrastructure/clients/rabbitmq/__init__.py +9 -0
  61. aury/boot/infrastructure/clients/rabbitmq/config.py +46 -0
  62. aury/boot/infrastructure/clients/rabbitmq/manager.py +288 -0
  63. aury/boot/infrastructure/clients/redis/__init__.py +28 -0
  64. aury/boot/infrastructure/clients/redis/config.py +51 -0
  65. aury/boot/infrastructure/clients/redis/manager.py +264 -0
  66. aury/boot/infrastructure/database/config.py +1 -2
  67. aury/boot/infrastructure/database/manager.py +16 -38
  68. aury/boot/infrastructure/events/__init__.py +18 -21
  69. aury/boot/infrastructure/events/backends/__init__.py +11 -0
  70. aury/boot/infrastructure/events/backends/memory.py +86 -0
  71. aury/boot/infrastructure/events/backends/rabbitmq.py +193 -0
  72. aury/boot/infrastructure/events/backends/redis.py +162 -0
  73. aury/boot/infrastructure/events/base.py +127 -0
  74. aury/boot/infrastructure/events/manager.py +224 -0
  75. aury/boot/infrastructure/mq/__init__.py +24 -0
  76. aury/boot/infrastructure/mq/backends/__init__.py +9 -0
  77. aury/boot/infrastructure/mq/backends/rabbitmq.py +179 -0
  78. aury/boot/infrastructure/mq/backends/redis.py +167 -0
  79. aury/boot/infrastructure/mq/base.py +143 -0
  80. aury/boot/infrastructure/mq/manager.py +239 -0
  81. aury/boot/infrastructure/scheduler/manager.py +7 -3
  82. aury/boot/infrastructure/storage/__init__.py +9 -9
  83. aury/boot/infrastructure/storage/base.py +17 -5
  84. aury/boot/infrastructure/storage/factory.py +0 -1
  85. aury/boot/infrastructure/tasks/__init__.py +2 -2
  86. aury/boot/infrastructure/tasks/manager.py +47 -29
  87. aury/boot/testing/base.py +2 -2
  88. {aury_boot-0.0.4.dist-info → aury_boot-0.0.5.dist-info}/METADATA +19 -2
  89. aury_boot-0.0.5.dist-info/RECORD +176 -0
  90. aury/boot/commands/templates/project/DEVELOPMENT.md.tpl +0 -1397
  91. aury/boot/infrastructure/events/bus.py +0 -362
  92. aury/boot/infrastructure/events/config.py +0 -52
  93. aury/boot/infrastructure/events/consumer.py +0 -134
  94. aury/boot/infrastructure/events/models.py +0 -63
  95. aury_boot-0.0.4.dist-info/RECORD +0 -137
  96. /aury/boot/commands/templates/project/{CLI.md.tpl → aury_docs/99-cli.md.tpl} +0 -0
  97. {aury_boot-0.0.4.dist-info → aury_boot-0.0.5.dist-info}/WHEEL +0 -0
  98. {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
+ ```