aury-boot 0.0.2__py3-none-any.whl → 0.0.4__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 (138) hide show
  1. aury/boot/__init__.py +66 -0
  2. aury/boot/_version.py +2 -2
  3. aury/boot/application/__init__.py +120 -0
  4. aury/boot/application/app/__init__.py +39 -0
  5. aury/boot/application/app/base.py +511 -0
  6. aury/boot/application/app/components.py +434 -0
  7. aury/boot/application/app/middlewares.py +101 -0
  8. aury/boot/application/config/__init__.py +44 -0
  9. aury/boot/application/config/settings.py +663 -0
  10. aury/boot/application/constants/__init__.py +19 -0
  11. aury/boot/application/constants/components.py +50 -0
  12. aury/boot/application/constants/scheduler.py +28 -0
  13. aury/boot/application/constants/service.py +29 -0
  14. aury/boot/application/errors/__init__.py +55 -0
  15. aury/boot/application/errors/chain.py +80 -0
  16. aury/boot/application/errors/codes.py +67 -0
  17. aury/boot/application/errors/exceptions.py +238 -0
  18. aury/boot/application/errors/handlers.py +320 -0
  19. aury/boot/application/errors/response.py +120 -0
  20. aury/boot/application/interfaces/__init__.py +76 -0
  21. aury/boot/application/interfaces/egress.py +224 -0
  22. aury/boot/application/interfaces/ingress.py +98 -0
  23. aury/boot/application/middleware/__init__.py +22 -0
  24. aury/boot/application/middleware/logging.py +451 -0
  25. aury/boot/application/migrations/__init__.py +13 -0
  26. aury/boot/application/migrations/manager.py +685 -0
  27. aury/boot/application/migrations/setup.py +237 -0
  28. aury/boot/application/rpc/__init__.py +63 -0
  29. aury/boot/application/rpc/base.py +108 -0
  30. aury/boot/application/rpc/client.py +294 -0
  31. aury/boot/application/rpc/discovery.py +218 -0
  32. aury/boot/application/scheduler/__init__.py +13 -0
  33. aury/boot/application/scheduler/runner.py +123 -0
  34. aury/boot/application/server/__init__.py +296 -0
  35. aury/boot/commands/__init__.py +30 -0
  36. aury/boot/commands/add.py +76 -0
  37. aury/boot/commands/app.py +105 -0
  38. aury/boot/commands/config.py +177 -0
  39. aury/boot/commands/docker.py +367 -0
  40. aury/boot/commands/docs.py +284 -0
  41. aury/boot/commands/generate.py +1277 -0
  42. aury/boot/commands/init.py +892 -0
  43. aury/boot/commands/migrate/__init__.py +37 -0
  44. aury/boot/commands/migrate/app.py +54 -0
  45. aury/boot/commands/migrate/commands.py +303 -0
  46. aury/boot/commands/scheduler.py +124 -0
  47. aury/boot/commands/server/__init__.py +21 -0
  48. aury/boot/commands/server/app.py +541 -0
  49. aury/boot/commands/templates/generate/api.py.tpl +105 -0
  50. aury/boot/commands/templates/generate/model.py.tpl +17 -0
  51. aury/boot/commands/templates/generate/repository.py.tpl +19 -0
  52. aury/boot/commands/templates/generate/schema.py.tpl +29 -0
  53. aury/boot/commands/templates/generate/service.py.tpl +48 -0
  54. aury/boot/commands/templates/project/CLI.md.tpl +92 -0
  55. aury/boot/commands/templates/project/DEVELOPMENT.md.tpl +1397 -0
  56. aury/boot/commands/templates/project/README.md.tpl +111 -0
  57. aury/boot/commands/templates/project/admin_console_init.py.tpl +50 -0
  58. aury/boot/commands/templates/project/config.py.tpl +30 -0
  59. aury/boot/commands/templates/project/conftest.py.tpl +26 -0
  60. aury/boot/commands/templates/project/env.example.tpl +213 -0
  61. aury/boot/commands/templates/project/gitignore.tpl +128 -0
  62. aury/boot/commands/templates/project/main.py.tpl +41 -0
  63. aury/boot/commands/templates/project/modules/api.py.tpl +19 -0
  64. aury/boot/commands/templates/project/modules/exceptions.py.tpl +84 -0
  65. aury/boot/commands/templates/project/modules/schedules.py.tpl +18 -0
  66. aury/boot/commands/templates/project/modules/tasks.py.tpl +20 -0
  67. aury/boot/commands/worker.py +143 -0
  68. aury/boot/common/__init__.py +35 -0
  69. aury/boot/common/exceptions/__init__.py +114 -0
  70. aury/boot/common/i18n/__init__.py +16 -0
  71. aury/boot/common/i18n/translator.py +272 -0
  72. aury/boot/common/logging/__init__.py +716 -0
  73. aury/boot/contrib/__init__.py +10 -0
  74. aury/boot/contrib/admin_console/__init__.py +18 -0
  75. aury/boot/contrib/admin_console/auth.py +137 -0
  76. aury/boot/contrib/admin_console/discovery.py +69 -0
  77. aury/boot/contrib/admin_console/install.py +172 -0
  78. aury/boot/contrib/admin_console/utils.py +44 -0
  79. aury/boot/domain/__init__.py +79 -0
  80. aury/boot/domain/exceptions/__init__.py +132 -0
  81. aury/boot/domain/models/__init__.py +51 -0
  82. aury/boot/domain/models/base.py +69 -0
  83. aury/boot/domain/models/mixins.py +135 -0
  84. aury/boot/domain/models/models.py +96 -0
  85. aury/boot/domain/pagination/__init__.py +279 -0
  86. aury/boot/domain/repository/__init__.py +23 -0
  87. aury/boot/domain/repository/impl.py +423 -0
  88. aury/boot/domain/repository/interceptors.py +47 -0
  89. aury/boot/domain/repository/interface.py +106 -0
  90. aury/boot/domain/repository/query_builder.py +348 -0
  91. aury/boot/domain/service/__init__.py +11 -0
  92. aury/boot/domain/service/base.py +73 -0
  93. aury/boot/domain/transaction/__init__.py +404 -0
  94. aury/boot/infrastructure/__init__.py +104 -0
  95. aury/boot/infrastructure/cache/__init__.py +31 -0
  96. aury/boot/infrastructure/cache/backends.py +348 -0
  97. aury/boot/infrastructure/cache/base.py +68 -0
  98. aury/boot/infrastructure/cache/exceptions.py +37 -0
  99. aury/boot/infrastructure/cache/factory.py +94 -0
  100. aury/boot/infrastructure/cache/manager.py +274 -0
  101. aury/boot/infrastructure/database/__init__.py +39 -0
  102. aury/boot/infrastructure/database/config.py +71 -0
  103. aury/boot/infrastructure/database/exceptions.py +44 -0
  104. aury/boot/infrastructure/database/manager.py +317 -0
  105. aury/boot/infrastructure/database/query_tools/__init__.py +164 -0
  106. aury/boot/infrastructure/database/strategies/__init__.py +198 -0
  107. aury/boot/infrastructure/di/__init__.py +15 -0
  108. aury/boot/infrastructure/di/container.py +393 -0
  109. aury/boot/infrastructure/events/__init__.py +33 -0
  110. aury/boot/infrastructure/events/bus.py +362 -0
  111. aury/boot/infrastructure/events/config.py +52 -0
  112. aury/boot/infrastructure/events/consumer.py +134 -0
  113. aury/boot/infrastructure/events/middleware.py +51 -0
  114. aury/boot/infrastructure/events/models.py +63 -0
  115. aury/boot/infrastructure/monitoring/__init__.py +529 -0
  116. aury/boot/infrastructure/scheduler/__init__.py +19 -0
  117. aury/boot/infrastructure/scheduler/exceptions.py +37 -0
  118. aury/boot/infrastructure/scheduler/manager.py +478 -0
  119. aury/boot/infrastructure/storage/__init__.py +38 -0
  120. aury/boot/infrastructure/storage/base.py +164 -0
  121. aury/boot/infrastructure/storage/exceptions.py +37 -0
  122. aury/boot/infrastructure/storage/factory.py +88 -0
  123. aury/boot/infrastructure/tasks/__init__.py +24 -0
  124. aury/boot/infrastructure/tasks/config.py +45 -0
  125. aury/boot/infrastructure/tasks/constants.py +37 -0
  126. aury/boot/infrastructure/tasks/exceptions.py +37 -0
  127. aury/boot/infrastructure/tasks/manager.py +490 -0
  128. aury/boot/testing/__init__.py +24 -0
  129. aury/boot/testing/base.py +122 -0
  130. aury/boot/testing/client.py +163 -0
  131. aury/boot/testing/factory.py +154 -0
  132. aury/boot/toolkit/__init__.py +21 -0
  133. aury/boot/toolkit/http/__init__.py +367 -0
  134. {aury_boot-0.0.2.dist-info → aury_boot-0.0.4.dist-info}/METADATA +3 -2
  135. aury_boot-0.0.4.dist-info/RECORD +137 -0
  136. aury_boot-0.0.2.dist-info/RECORD +0 -5
  137. {aury_boot-0.0.2.dist-info → aury_boot-0.0.4.dist-info}/WHEEL +0 -0
  138. {aury_boot-0.0.2.dist-info → aury_boot-0.0.4.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,1397 @@
1
+ # {project_name} 开发指南
2
+
3
+ 本文档基于 [Aury Boot](https://github.com/AuriMyth/aury-boot) 框架。
4
+
5
+ CLI 命令参考请查看 [CLI.md](./CLI.md)。
6
+
7
+ ---
8
+
9
+ ## 目录结构
10
+
11
+ ```
12
+ {project_name}/
13
+ ├── app/ # 代码包(默认 app,可通过 aury init <pkg> 自定义)
14
+ │ ├── models/ # SQLAlchemy ORM 模型
15
+ │ ├── repositories/ # 数据访问层
16
+ │ ├── services/ # 业务逻辑层
17
+ │ ├── schemas/ # Pydantic 请求/响应模型
18
+ │ ├── api/ # FastAPI 路由
19
+ │ ├── exceptions/ # 业务异常
20
+ │ ├── tasks/ # 异步任务(Dramatiq)
21
+ │ └── schedules/ # 定时任务(Scheduler)
22
+ ├── tests/ # 测试
23
+ ├── migrations/ # 数据库迁移
24
+ └── main.py # 应用入口
25
+ ```
26
+
27
+ ---
28
+
29
+ ## 1. Model(数据模型)
30
+
31
+ ### 1.1 模型基类选择
32
+
33
+ 框架提供多种预组合基类,按需选择:
34
+
35
+ | 基类 | 主键 | 时间戳 | 软删除 | 乐观锁 | 场景 |
36
+ |------|------|--------|--------|--------|------|
37
+ | `Model` | int | ✓ | ✗ | ✗ | 简单实体 |
38
+ | `AuditableStateModel` | int | ✓ | ✓ | ✗ | 需软删除 |
39
+ | `UUIDModel` | UUID | ✓ | ✗ | ✗ | 分布式 |
40
+ | `UUIDAuditableStateModel` | UUID | ✓ | ✓ | ✗ | **推荐** |
41
+ | `VersionedModel` | int | ✗ | ✗ | ✓ | 乐观锁 |
42
+ | `VersionedUUIDModel` | UUID | ✓ | ✗ | ✓ | UUID+乐观锁 |
43
+ | `FullFeaturedUUIDModel` | UUID | ✓ | ✓ | ✓ | 全功能 |
44
+
45
+ ### 1.2 基类自动提供的字段
46
+
47
+ **IDMixin** (int 主键):
48
+ ```python
49
+ id: Mapped[int] # 自增主键
50
+ ```
51
+
52
+ **UUIDMixin** (UUID 主键):
53
+ ```python
54
+ import uuid
55
+ from sqlalchemy.types import Uuid as SQLAlchemyUuid
56
+ from sqlalchemy.orm import Mapped, mapped_column
57
+
58
+ id: Mapped[uuid.UUID] = mapped_column(
59
+ SQLAlchemyUuid(as_uuid=True), # SQLAlchemy 2.0 自动适配 PG(uuid) 和 MySQL(char(36))
60
+ primary_key=True,
61
+ default=uuid.uuid4,
62
+ )
63
+ ```
64
+
65
+ > **关于 SQLAlchemyUuid**:
66
+ > - 使用 `sqlalchemy.types.Uuid`(导入为 `SQLAlchemyUuid`)而非直接使用 `uuid.UUID`
67
+ > - `as_uuid=True` 确保 Python 层面使用 UUID 对象而非字符串
68
+ > - 框架会自动适配不同数据库:PostgreSQL 使用原生 UUID 类型,MySQL/SQLite 使用 CHAR(36)
69
+ > - 如需手动定义 UUID 字段,请使用 `SQLAlchemyUuid(as_uuid=True)` 而非 `Uuid(as_uuid=True)`
70
+
71
+ **TimestampMixin** (时间戳):
72
+ ```python
73
+ created_at: Mapped[datetime] # 创建时间,自动设置
74
+ updated_at: Mapped[datetime] # 更新时间,自动更新
75
+ ```
76
+
77
+ **AuditableStateMixin** (软删除):
78
+ ```python
79
+ deleted_at: Mapped[int] # 删除时间戳,0=未删除,>0=删除时间
80
+ # 自动提供:is_deleted 属性、mark_deleted() 方法、restore() 方法
81
+ ```
82
+
83
+ > **注意**:使用软删除的模型不要单独使用 `unique=True`,否则删除后再插入相同值会报错。
84
+ > 应使用复合唯一索引:`UniqueConstraint("email", "deleted_at", name="uq_users_email_deleted")`
85
+
86
+ **VersionMixin** (乐观锁):
87
+ ```python
88
+ version: Mapped[int] # 版本号,自动管理
89
+ ```
90
+
91
+ ### 1.3 Model 编写示例
92
+
93
+ **文件**: `app/models/user.py`
94
+
95
+ ```python
96
+ """User 数据模型。"""
97
+
98
+ from sqlalchemy import String, Boolean, UniqueConstraint
99
+ from sqlalchemy.orm import Mapped, mapped_column
100
+
101
+ from aury.boot.domain.models import UUIDAuditableStateModel
102
+
103
+
104
+ class User(UUIDAuditableStateModel):
105
+ """User 模型。
106
+
107
+ 继承 UUIDAuditableStateModel 自动获得:
108
+ - id: UUID 主键(使用 SQLAlchemyUuid 自动适配数据库)
109
+ - created_at, updated_at: 时间戳
110
+ - deleted_at: 软删除支持
111
+ """
112
+
113
+ __tablename__ = "users"
114
+
115
+ name: Mapped[str] = mapped_column(String(100), nullable=False, comment="用户名")
116
+ email: Mapped[str] = mapped_column(String(255), nullable=False, index=True, comment="邮箱")
117
+ is_active: Mapped[bool] = mapped_column(Boolean, default=True, comment="是否激活")
118
+
119
+ # 软删除模型必须使用复合唯一约束(包含 deleted_at),避免删除后无法插入相同值
120
+ # 注意:复合约束必须使用 __table_args__,这是 SQLAlchemy 的要求
121
+ __table_args__ = (
122
+ UniqueConstraint("email", "deleted_at", name="uq_users_email_deleted"),
123
+ )
124
+ ```
125
+
126
+ ### 1.4 字段类型映射
127
+
128
+ | Python 类型 | SQLAlchemy 类型 | 说明 |
129
+ |-------------|----------------|------|
130
+ | `str` | `String(length)` | 必须指定长度 |
131
+ | `int` | `Integer` | 整数 |
132
+ | `float` | `Float` | 浮点数 |
133
+ | `bool` | `Boolean` | 布尔值 |
134
+ | `datetime` | `DateTime(timezone=True)` | 带时区 |
135
+ | `date` | `Date` | 日期 |
136
+ | `Decimal` | `Numeric(precision, scale)` | 精确小数 |
137
+ | `dict`/`list` | `JSON` | JSON 数据 |
138
+ | `uuid.UUID` | `SQLAlchemyUuid(as_uuid=True)` | UUID(推荐使用 `from sqlalchemy.types import Uuid as SQLAlchemyUuid`) |
139
+
140
+ ### 1.5 常用字段约束
141
+
142
+ ```python
143
+ from sqlalchemy import String, Integer, ForeignKey, Index, UniqueConstraint
144
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
145
+ import uuid
146
+
147
+ class Example(UUIDAuditableStateModel):
148
+ __tablename__ = "examples"
149
+
150
+ # 可选字段
151
+ description: Mapped[str | None] = mapped_column(String(500), nullable=True)
152
+
153
+ # 带默认值
154
+ status: Mapped[int] = mapped_column(Integer, default=0, server_default="0", index=True)
155
+
156
+ # 单列索引:直接在 mapped_column 中使用 index=True(推荐)
157
+ code: Mapped[str] = mapped_column(String(50), index=True, comment="编码")
158
+
159
+ # 单列唯一约束:直接在 mapped_column 中使用 unique=True(仅非软删除模型)
160
+ # 注意:软删除模型不能单独使用 unique=True,必须使用复合唯一约束
161
+
162
+ # 外键关联
163
+ category_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("categories.id"), index=True)
164
+ category: Mapped["Category"] = relationship(back_populates="examples")
165
+
166
+ # 复合索引和复合唯一约束:必须使用 __table_args__(SQLAlchemy 要求)
167
+ # 软删除模型必须使用复合唯一约束(包含 deleted_at),避免删除后无法插入相同值
168
+ __table_args__ = (
169
+ Index("ix_examples_status_created", "status", "created_at"), # 复合索引
170
+ UniqueConstraint("code", "deleted_at", name="uq_examples_code_deleted"), # 复合唯一约束
171
+ )
172
+
173
+
174
+ # 非软删除模型可以直接使用 unique=True 和 index=True
175
+ class Config(UUIDModel): # UUIDModel 不包含软删除
176
+ __tablename__ = "configs"
177
+
178
+ # 单列唯一约束:直接在 mapped_column 中使用(推荐)
179
+ key: Mapped[str] = mapped_column(String(100), unique=True, index=True, comment="配置键")
180
+ value: Mapped[str] = mapped_column(String(500), comment="配置值")
181
+
182
+ # 单列索引:直接在 mapped_column 中使用(推荐)
183
+ name: Mapped[str] = mapped_column(String(100), index=True, comment="配置名称")
184
+ ```
185
+
186
+ **约束定义最佳实践**:
187
+
188
+ 1. **单列索引**:使用 `index=True` 在 `mapped_column` 中(推荐)
189
+ ```python
190
+ email: Mapped[str] = mapped_column(String(255), index=True)
191
+ ```
192
+
193
+ 2. **单列唯一约束**:
194
+ - 非软删除模型:使用 `unique=True` 在 `mapped_column` 中(推荐)
195
+ - 软删除模型:必须使用复合唯一约束(包含 `deleted_at`)
196
+
197
+ 3. **复合索引/唯一约束**:必须使用 `__table_args__`(SQLAlchemy 要求)
198
+ ```python
199
+ __table_args__ = (
200
+ Index("ix_name", "col1", "col2"), # 复合索引
201
+ UniqueConstraint("col1", "col2", name="uq_name"), # 复合唯一约束
202
+ )
203
+ ```
204
+
205
+ ---
206
+
207
+ ## 2. Repository(数据访问层)
208
+
209
+ ### 2.1 Repository 编写示例
210
+
211
+ **文件**: `app/repositories/user_repository.py`
212
+
213
+ ```python
214
+ """User 数据访问层。"""
215
+
216
+ from aury.boot.domain.repository.impl import BaseRepository
217
+
218
+ from app.models.user import User
219
+
220
+
221
+ class UserRepository(BaseRepository[User]):
222
+ """User 仓储。
223
+
224
+ 继承 BaseRepository 自动获得:
225
+ - get(id): 按 ID 获取
226
+ - get_by(**filters): 按条件获取单个
227
+ - list(skip, limit, **filters): 获取列表
228
+ - paginate(params, **filters): 分页获取
229
+ - count(**filters): 计数
230
+ - exists(**filters): 是否存在
231
+ - create(data): 创建
232
+ - update(entity, data): 更新
233
+ - delete(entity, soft=True): 删除(默认软删除)
234
+ - batch_create(data_list): 批量创建
235
+ - bulk_insert(data_list): 高性能批量插入
236
+ """
237
+
238
+ async def get_by_email(self, email: str) -> User | None:
239
+ """按邮箱查询用户。"""
240
+ return await self.get_by(email=email)
241
+
242
+ async def list_active(self, skip: int = 0, limit: int = 100) -> list[User]:
243
+ """获取激活用户列表。"""
244
+ return await self.list(skip=skip, limit=limit, is_active=True)
245
+ ```
246
+
247
+ ### 2.2 BaseRepository 方法详解
248
+
249
+ ```python
250
+ from sqlalchemy.ext.asyncio import AsyncSession
251
+
252
+ # 初始化(默认 auto_commit=True)
253
+ repo = UserRepository(session, User)
254
+
255
+ # === 查询 ===
256
+ user = await repo.get(user_id) # 按 ID(支持 int/UUID)
257
+ user = await repo.get_by(email="a@b.com") # 按条件(简单 AND 过滤)
258
+ users = await repo.list(skip=0, limit=10) # 列表
259
+ users = await repo.list(is_active=True) # 带过滤
260
+ count = await repo.count(is_active=True) # 计数
261
+ exists = await repo.exists(email="a@b.com") # 是否存在
262
+
263
+ # === 分页 ===
264
+ from aury.boot.domain.pagination import PaginationParams, SortParams
265
+
266
+ result = await repo.paginate(
267
+ pagination_params=PaginationParams(page=1, size=20),
268
+ sort_params=SortParams(sorts=[("created_at", "desc")]),
269
+ is_active=True,
270
+ )
271
+ # result.items, result.total, result.page, result.size, result.pages
272
+
273
+ # === 创建 ===
274
+ user = await repo.create({{"name": "Alice", "email": "a@b.com"}})
275
+ users = await repo.batch_create([{{"name": "A"}}, {{"name": "B"}}]) # 返回实体
276
+ await repo.bulk_insert([{{"name": "A"}}, {{"name": "B"}}]) # 高性能,无返回
277
+
278
+ # === 更新 ===
279
+ user = await repo.update(user, {{"name": "Bob"}})
280
+
281
+ # === 删除 ===
282
+ await repo.delete(user) # 软删除
283
+ await repo.delete(user, soft=False) # 硬删除
284
+ await repo.hard_delete(user) # 硬删除别名
285
+ deleted = await repo.delete_by_id(user_id) # 按 ID 删除
286
+ ```
287
+
288
+ #### 2.2.1 Filters 语法(增强)
289
+
290
+ BaseRepository 的 `get_by/list/paginate/count/exists` 的 `**filters` 支持下列操作符(与 `QueryBuilder.filter` 对齐):
291
+ - `__gt`, `__lt`, `__gte`, `__lte`, `__in`, `__like`, `__ilike`, `__isnull`, `__ne`
292
+
293
+ 示例:
294
+
295
+ ```python
296
+ # 模糊匹配 + 范围 + IN + 为空判断(条件之间为 AND 关系)
297
+ users = await repo.list(
298
+ name__ilike="%foo%",
299
+ age__gte=18,
300
+ id__in=[u1, u2, u3],
301
+ deleted_at__isnull=True,
302
+ )
303
+
304
+ # 单个实体(不等于)
305
+ user = await repo.get_by(status__ne="archived")
306
+ ```
307
+
308
+ > 注意:filters 条件之间用 AND 组合;如需 AND/OR/NOT 的复杂组合,请使用 `QueryBuilder`(见 2.4)。
309
+
310
+ #### 2.2.2 查询全部(limit=None)
311
+
312
+ `list()` 支持 `limit=None` 返回全部记录(谨慎使用大表):
313
+
314
+ ```python
315
+ all_users = await repo.list(limit=None) # 全量
316
+ active_all = await repo.list(limit=None, is_active=True)
317
+ ```
318
+
319
+ ### 2.3 自动提交机制
320
+
321
+ BaseRepository 支持智能的自动提交机制,优于 Django 的设计:
322
+
323
+ | 场景 | 行为 |
324
+ |------|------|
325
+ | 非事务中 + `auto_commit=True` | 写操作后自动 commit |
326
+ | 非事务中 + `auto_commit=False` | 只 flush,需手动管理或使用 `.with_commit()` |
327
+ | 在事务中(`@transactional` 等) | **永不自动提交**,由事务统一管理 |
328
+
329
+ ```python
330
+ # 默认行为:非事务中自动提交
331
+ repo = UserRepository(session, User) # auto_commit=True
332
+ await repo.create({{"name": "test"}}) # 自动 commit
333
+
334
+ # 禁用自动提交
335
+ repo = UserRepository(session, User, auto_commit=False)
336
+ await repo.create({{"name": "test"}}) # 只 flush,不 commit
337
+
338
+ # 单次强制提交(auto_commit=False 时)
339
+ await repo.with_commit().create({{"name": "test2"}}) # 强制 commit
340
+
341
+ # 在事务中:无论 auto_commit 是什么,都不会自动提交
342
+ @transactional
343
+ async def create_with_profile(session: AsyncSession):
344
+ repo = UserRepository(session, User) # auto_commit=True 但不生效
345
+ user = await repo.create({{"name": "a"}}) # 只 flush
346
+ profile = await profile_repo.create({{"user_id": user.id}}) # 只 flush
347
+ # 事务结束时统一 commit
348
+ ```
349
+
350
+ **设计优势**(对比 Django):
351
+ - Django:每个 `save()` 默认独立事务,容易无意识地失去原子性
352
+ - Foundation Kit:默认自动提交,但**事务上下文自动接管**,更显式可控
353
+
354
+ ### 2.4 复杂查询示例
355
+
356
+ ```python
357
+ async def search_users(
358
+ self,
359
+ keyword: str | None = None,
360
+ status: int | None = None,
361
+ ) -> list[User]:
362
+ """复杂搜索。"""
363
+ query = self.query() # 自动排除软删除
364
+
365
+ if keyword:
366
+ # 使用 QueryBuilder 的 or_ 与 filter_by 组合复杂条件
367
+ query = query.filter_by(
368
+ query.or_(
369
+ User.name.ilike(f"%{{keyword}}%"),
370
+ User.email.ilike(f"%{{keyword}}%"),
371
+ )
372
+ )
373
+
374
+ if status is not None:
375
+ # 简单等值可直接使用 filter(支持 **kwargs)
376
+ query = query.filter(status=status)
377
+
378
+ query = query.order_by("-created_at").limit(100)
379
+ result = await self.session.execute(query.build())
380
+ return list(result.scalars().all())
381
+ ```
382
+
383
+ ---
384
+
385
+ ## 3. Service(业务逻辑层)
386
+
387
+ ### 3.1 Service 编写示例
388
+
389
+ **文件**: `app/services/user_service.py`
390
+
391
+ ```python
392
+ """User 业务逻辑层。"""
393
+
394
+ from uuid import UUID
395
+
396
+ from sqlalchemy.ext.asyncio import AsyncSession
397
+
398
+ from aury.boot.application.errors import AlreadyExistsError, NotFoundError
399
+ from aury.boot.domain.service.base import BaseService
400
+ from aury.boot.domain.transaction import transactional
401
+
402
+ from app.models.user import User
403
+ from app.repositories.user_repository import UserRepository
404
+ from app.schemas.user import UserCreate, UserUpdate
405
+
406
+
407
+ class UserService(BaseService):
408
+ """User 服务。"""
409
+
410
+ def __init__(self, session: AsyncSession) -> None:
411
+ super().__init__(session)
412
+ self.repo = UserRepository(session, User)
413
+
414
+ async def get(self, id: UUID) -> User:
415
+ """获取 User。"""
416
+ entity = await self.repo.get(id)
417
+ if not entity:
418
+ raise NotFoundError("User 不存在", resource=id)
419
+ return entity
420
+
421
+ async def list(self, skip: int = 0, limit: int = 100) -> list[User]:
422
+ """获取 User 列表。"""
423
+ return await self.repo.list(skip=skip, limit=limit)
424
+
425
+ @transactional
426
+ async def create(self, data: UserCreate) -> User:
427
+ """创建 User。"""
428
+ if await self.repo.exists(email=data.email):
429
+ raise AlreadyExistsError(f"邮箱 {{data.email}} 已存在")
430
+ return await self.repo.create(data.model_dump())
431
+
432
+ @transactional
433
+ async def update(self, id: UUID, data: UserUpdate) -> User:
434
+ """更新 User。"""
435
+ entity = await self.get(id)
436
+ return await self.repo.update(entity, data.model_dump(exclude_unset=True))
437
+
438
+ @transactional
439
+ async def delete(self, id: UUID) -> None:
440
+ """删除 User。"""
441
+ entity = await self.get(id)
442
+ await self.repo.delete(entity)
443
+ ```
444
+
445
+ ### 3.2 跨 Service 调用(事务共享)
446
+
447
+ ```python
448
+ class OrderService(BaseService):
449
+ def __init__(self, session: AsyncSession) -> None:
450
+ super().__init__(session)
451
+ self.order_repo = OrderRepository(session, Order)
452
+ # 复用 session 实现事务共享
453
+ self.user_service = UserService(session)
454
+ self.inventory_service = InventoryService(session)
455
+
456
+ @transactional
457
+ async def create_order(self, data: OrderCreate) -> Order:
458
+ """创建订单(跨 Service 事务)。"""
459
+ user = await self.user_service.get(data.user_id)
460
+ await self.inventory_service.deduct(data.product_id, data.quantity)
461
+ order = await self.order_repo.create(...)
462
+ return order # 整个流程在同一事务中
463
+ ```
464
+
465
+ ---
466
+
467
+ ## 4. Schema(Pydantic 模型)
468
+
469
+ ### 4.1 Schema 编写示例
470
+
471
+ **文件**: `app/schemas/user.py`
472
+
473
+ ```python
474
+ """User Pydantic 模型。"""
475
+
476
+ from datetime import datetime
477
+ from uuid import UUID
478
+
479
+ from pydantic import BaseModel, ConfigDict, Field, EmailStr
480
+
481
+
482
+ class UserBase(BaseModel):
483
+ """User 基础模型。"""
484
+ name: str = Field(..., min_length=1, max_length=100, description="用户名")
485
+ email: EmailStr = Field(..., description="邮箱")
486
+ is_active: bool = Field(default=True, description="是否激活")
487
+
488
+
489
+ class UserCreate(UserBase):
490
+ """创建 User 请求。"""
491
+ password: str = Field(..., min_length=6, description="密码")
492
+
493
+
494
+ class UserUpdate(BaseModel):
495
+ """更新 User 请求(所有字段可选)。"""
496
+ name: str | None = Field(default=None, min_length=1, max_length=100)
497
+ email: EmailStr | None = None
498
+ is_active: bool | None = None
499
+
500
+
501
+ class UserResponse(UserBase):
502
+ """User 响应。"""
503
+ id: UUID
504
+ created_at: datetime
505
+ updated_at: datetime
506
+
507
+ model_config = ConfigDict(from_attributes=True)
508
+ ```
509
+
510
+ ### 4.2 Schema 使用说明
511
+
512
+ Schema(Pydantic 模型)在框架中扮演三个角色:
513
+
514
+ 1. **请求验证**:验证 API 请求数据(Create/Update)
515
+ 2. **响应序列化**:将 ORM 模型转换为 JSON 响应(Response)
516
+ 3. **类型提示**:为 Service 层提供类型安全
517
+
518
+ **数据流转**:
519
+ ```
520
+ API 请求 → Schema 验证 → Service 处理 → Repository 操作 → ORM Model
521
+ ↓ ↓
522
+ model_dump() model_validate()
523
+
524
+ Response Schema → JSON 响应
525
+ ```
526
+
527
+ **关键方法**:
528
+ - `model_dump()`:将 Schema 转为字典(传给 Repository)
529
+ - `model_dump(exclude_unset=True)`:只转换设置过的字段(用于更新)
530
+ - `model_validate(orm_obj)`:从 ORM 模型创建 Schema(需要 `from_attributes=True`)
531
+
532
+ > **重要提示**:不要自定义通用响应 Schema(如 `OkResponse`、`ErrorResponse`)。
533
+ > 框架已内置 `BaseResponse` 和 `PaginationResponse`,直接使用即可。
534
+ > 异常响应由全局异常处理中间件自动处理,无需手动定义。
535
+
536
+ ### 4.3 常用验证
537
+
538
+ ```python
539
+ from pydantic import BaseModel, Field, field_validator
540
+ from typing import Literal
541
+
542
+ class ExampleSchema(BaseModel):
543
+ # 长度限制
544
+ name: str = Field(..., min_length=1, max_length=100)
545
+
546
+ # 数值范围
547
+ age: int = Field(..., ge=0, le=150)
548
+ price: float = Field(..., gt=0)
549
+
550
+ # 正则验证
551
+ phone: str = Field(..., pattern=r"^1[3-9]\d{{9}}$")
552
+
553
+ # 枚举
554
+ status: Literal["active", "inactive", "pending"]
555
+
556
+ # 字段验证器
557
+ @field_validator("name")
558
+ @classmethod
559
+ def name_strip(cls, v: str) -> str:
560
+ return v.strip()
561
+ ```
562
+
563
+ ---
564
+
565
+ ## 5. API(路由层)
566
+
567
+ ### 5.1 API 编写示例
568
+
569
+ **文件**: `app/api/user.py`
570
+
571
+ ```python
572
+ """User API 路由。"""
573
+
574
+ from uuid import UUID
575
+
576
+ from fastapi import APIRouter, Depends
577
+ from sqlalchemy.ext.asyncio import AsyncSession
578
+
579
+ from aury.boot.application.interfaces.egress import (
580
+ BaseResponse,
581
+ Pagination,
582
+ PaginationResponse,
583
+ )
584
+ from aury.boot.infrastructure.database import DatabaseManager
585
+
586
+ from app.schemas.user import UserCreate, UserResponse, UserUpdate
587
+ from app.services.user_service import UserService
588
+
589
+ router = APIRouter(prefix="/v1/users", tags=["User"])
590
+ db_manager = DatabaseManager.get_instance()
591
+
592
+
593
+ async def get_service(
594
+ session: AsyncSession = Depends(db_manager.get_session),
595
+ ) -> UserService:
596
+ return UserService(session)
597
+
598
+
599
+ @router.get("", response_model=PaginationResponse[UserResponse])
600
+ async def list_users(
601
+ skip: int = 0,
602
+ limit: int = 20,
603
+ service: UserService = Depends(get_service),
604
+ ) -> PaginationResponse[UserResponse]:
605
+ """获取列表。"""
606
+ items = await service.list(skip=skip, limit=limit)
607
+ return PaginationResponse(
608
+ code=200,
609
+ message="获取成功",
610
+ data=Pagination(
611
+ total=len(items),
612
+ items=[UserResponse.model_validate(item) for item in items],
613
+ page=skip // limit + 1,
614
+ size=limit,
615
+ ),
616
+ )
617
+
618
+
619
+ @router.get("/{{id}}", response_model=BaseResponse[UserResponse])
620
+ async def get_user(
621
+ id: UUID,
622
+ service: UserService = Depends(get_service),
623
+ ) -> BaseResponse[UserResponse]:
624
+ """获取详情。"""
625
+ entity = await service.get(id)
626
+ return BaseResponse(
627
+ code=200,
628
+ message="获取成功",
629
+ data=UserResponse.model_validate(entity),
630
+ )
631
+
632
+
633
+ @router.post("", response_model=BaseResponse[UserResponse])
634
+ async def create_user(
635
+ data: UserCreate,
636
+ service: UserService = Depends(get_service),
637
+ ) -> BaseResponse[UserResponse]:
638
+ """创建。"""
639
+ entity = await service.create(data)
640
+ return BaseResponse(
641
+ code=200,
642
+ message="创建成功",
643
+ data=UserResponse.model_validate(entity),
644
+ )
645
+
646
+
647
+ @router.put("/{{id}}", response_model=BaseResponse[UserResponse])
648
+ async def update_user(
649
+ id: UUID,
650
+ data: UserUpdate,
651
+ service: UserService = Depends(get_service),
652
+ ) -> BaseResponse[UserResponse]:
653
+ """更新。"""
654
+ entity = await service.update(id, data)
655
+ return BaseResponse(
656
+ code=200,
657
+ message="更新成功",
658
+ data=UserResponse.model_validate(entity),
659
+ )
660
+
661
+
662
+ @router.delete("/{{id}}", response_model=BaseResponse[None])
663
+ async def delete_user(
664
+ id: UUID,
665
+ service: UserService = Depends(get_service),
666
+ ) -> BaseResponse[None]:
667
+ """删除。"""
668
+ await service.delete(id)
669
+ return BaseResponse(code=200, message="删除成功", data=None)
670
+ ```
671
+
672
+ ### 5.2 注册路由
673
+
674
+ ```python
675
+ from fastapi import FastAPI
676
+ from app.api.user import router as user_router
677
+
678
+ app = FastAPI()
679
+ app.include_router(user_router)
680
+ ```
681
+
682
+ ---
683
+
684
+ ## 核心组件使用
685
+
686
+ ---
687
+
688
+ ## 6. 数据库事务
689
+
690
+ ### 6.1 事务装饰器(推荐)
691
+
692
+ ```python
693
+ from aury.boot.domain.transaction import transactional
694
+ from sqlalchemy.ext.asyncio import AsyncSession
695
+
696
+ # 自动识别 session 参数,自动提交/回滚
697
+ @transactional
698
+ async def create_user(session: AsyncSession, name: str, email: str):
699
+ """创建用户,自动在事务中执行。"""
700
+ repo = UserRepository(session)
701
+ user = await repo.create({{"name": name, "email": email}})
702
+ # 成功:自动 commit
703
+ # 异常:自动 rollback
704
+ return user
705
+
706
+ # 在类方法中使用(自动识别 self.session)
707
+ class UserService:
708
+ def __init__(self, session: AsyncSession):
709
+ self.session = session
710
+
711
+ @transactional
712
+ async def create_with_profile(self, name: str):
713
+ """自动使用 self.session。"""
714
+ user = await self.repo.create({{"name": name}})
715
+ await self.profile_repo.create({{"user_id": user.id}})
716
+ return user
717
+ ```
718
+
719
+ ### 6.2 事务上下文管理器
720
+
721
+ ```python
722
+ from aury.boot.domain.transaction import transactional_context
723
+ from aury.boot.infrastructure.database import DatabaseManager
724
+
725
+ db = DatabaseManager.get_instance()
726
+
727
+ async with db.session() as session:
728
+ async with transactional_context(session):
729
+ repo1 = UserRepository(session)
730
+ repo2 = ProfileRepository(session)
731
+
732
+ user = await repo1.create({{"name": "Alice"}})
733
+ await repo2.create({{"user_id": user.id}})
734
+ # 自动提交或回滚
735
+ ```
736
+
737
+ ### 6.3 事务传播(嵌套事务)
738
+
739
+ 框架自动支持嵌套事务,内层事务会复用外层事务:
740
+
741
+ ```python
742
+ @transactional
743
+ async def outer_operation(session: AsyncSession):
744
+ """外层事务。"""
745
+ repo1 = UserRepository(session)
746
+ user = await repo1.create({{"name": "Alice"}})
747
+
748
+ # 嵌套调用另一个 @transactional 函数
749
+ result = await inner_operation(session)
750
+ # 不会重复开启事务,复用外层事务
751
+ # 只有外层事务提交时才会真正提交
752
+
753
+ return user, result
754
+
755
+ @transactional
756
+ async def inner_operation(session: AsyncSession):
757
+ """内层事务,自动复用外层事务。"""
758
+ repo2 = OrderRepository(session)
759
+ return await repo2.create({{"user_id": 1}})
760
+ # 检测到已在事务中,直接执行,不重复提交
761
+ ```
762
+
763
+ **传播行为**:
764
+ - 如果已在事务中:直接执行,不开启新事务
765
+ - 如果不在事务中:开启新事务,自动提交/回滚
766
+ - 嵌套事务共享同一个数据库连接和事务上下文
767
+
768
+ ### 6.4 非事务的数据库使用
769
+
770
+ 对于只读操作或不需要事务的场景,可以直接使用 session:
771
+
772
+ ```python
773
+ from aury.boot.infrastructure.database import DatabaseManager
774
+
775
+ db = DatabaseManager.get_instance()
776
+
777
+ # 方式 1:使用 session 上下文管理器(推荐)
778
+ async with db.session() as session:
779
+ repo = UserRepository(session)
780
+ # 只读操作,不需要事务
781
+ users = await repo.list(skip=0, limit=10)
782
+ user = await repo.get(1)
783
+
784
+ # 方式 2:在 FastAPI 路由中使用(自动注入)
785
+ from fastapi import Depends
786
+ from sqlalchemy.ext.asyncio import AsyncSession
787
+
788
+ @router.get("/users")
789
+ async def list_users(
790
+ session: AsyncSession = Depends(db.get_session),
791
+ ):
792
+ """只读操作,不需要事务。"""
793
+ repo = UserRepository(session)
794
+ return await repo.list()
795
+
796
+ # 方式 3:手动控制(需要手动关闭)
797
+ session = await db.create_session()
798
+ try:
799
+ repo = UserRepository(session)
800
+ users = await repo.list()
801
+ finally:
802
+ await session.close()
803
+ ```
804
+
805
+ **何时使用非事务**:
806
+ - 只读查询(SELECT)
807
+ - 不需要原子性的操作
808
+ - 性能敏感的场景(避免事务开销)
809
+
810
+ **何时必须使用事务**:
811
+ - 写操作(INSERT/UPDATE/DELETE)
812
+ - 需要原子性的多个操作
813
+ - 需要回滚的场景
814
+
815
+ ### 6.5 写入不使用事务装饰器
816
+
817
+ 某些场景下,你可能希望在 Service 内不加 `@transactional`,而由上层控制事务边界:
818
+
819
+ ```python
820
+ # 场景:Service 内部不加装饰器,由调用方控制事务
821
+ class UserWriteService(BaseService):
822
+ async def create_no_tx(self, data: UserCreate) -> User:
823
+ if await self.repo.exists(email=data.email):
824
+ raise AlreadyExistsError(f"邮箱 {{data.email}} 已存在")
825
+ # 只做 flush/refresh,不做 commit
826
+ return await self.repo.create(data.model_dump())
827
+
828
+
829
+ # 调用方显式管理事务边界
830
+ async def create_user_flow(session: AsyncSession, data: UserCreate) -> User:
831
+ service = UserWriteService(session)
832
+ try:
833
+ user = await service.create_no_tx(data)
834
+ await session.commit()
835
+ return user
836
+ except Exception:
837
+ await session.rollback()
838
+ raise
839
+
840
+
841
+ # 或使用 transactional_context
842
+ from aury.boot.domain.transaction import transactional_context
843
+
844
+ async def create_user_flow(session: AsyncSession, data: UserCreate) -> User:
845
+ async with transactional_context(session):
846
+ service = UserWriteService(session)
847
+ return await service.create_no_tx(data)
848
+ ```
849
+
850
+ 适用场景:
851
+ - 一个用例调用多个 Service,统一提交
852
+ - 手动控制 commit 时机(分步 flush、条件提交)
853
+ - 上层已有事务边界(如作业层)
854
+
855
+ ### 6.6 Savepoints(保存点)
856
+
857
+ 保存点允许在事务中设置回滚点,实现部分回滚而不影响整个事务:
858
+
859
+ ```python
860
+ from aury.boot.domain.transaction import TransactionManager
861
+
862
+ async def complex_operation(session: AsyncSession):
863
+ """使用保存点实现部分回滚。"""
864
+ tm = TransactionManager(session)
865
+
866
+ await tm.begin()
867
+ repo = UserRepository(session)
868
+
869
+ try:
870
+ # 第一步:创建主记录
871
+ user = await repo.create({{"name": "alice"}})
872
+
873
+ # 创建保存点
874
+ sp_id = await tm.savepoint("before_optional")
875
+
876
+ try:
877
+ # 第二步:可选操作(可能失败)
878
+ await risky_operation(session)
879
+ # 成功:提交保存点
880
+ await tm.savepoint_commit(sp_id)
881
+ except RiskyOperationError:
882
+ # 失败:回滚到保存点,但 user 创建仍然保留
883
+ await tm.savepoint_rollback(sp_id)
884
+ logger.warning("可选操作失败,已回滚,继续主流程")
885
+
886
+ # 第三步:继续其他操作(不受保存点回滚影响)
887
+ await repo.update(user.id, {{"status": "active"}})
888
+
889
+ await tm.commit()
890
+ return user
891
+ except Exception:
892
+ await tm.rollback()
893
+ raise
894
+ ```
895
+
896
+ **保存点 API**:
897
+ - `savepoint(name)` - 创建保存点,返回保存点 ID
898
+ - `savepoint_commit(sp_id)` - 提交保存点(释放保存点,变更生效)
899
+ - `savepoint_rollback(sp_id)` - 回滚到保存点(撤销保存点后的变更)
900
+
901
+ ### 6.7 on_commit 回调
902
+
903
+ 注册在事务成功提交后执行的回调函数,适合发送通知、触发后续任务等副作用操作:
904
+
905
+ ```python
906
+ from aury.boot.domain.transaction import transactional, on_commit
907
+
908
+ @transactional
909
+ async def create_order(session: AsyncSession, order_data: dict):
910
+ """创建订单并在提交后发送通知。"""
911
+ repo = OrderRepository(session)
912
+ order = await repo.create(order_data)
913
+
914
+ # 注册回调:事务成功后执行
915
+ on_commit(lambda: send_order_notification(order.id))
916
+ on_commit(lambda: update_inventory_cache(order.items))
917
+
918
+ # 如果后续发生异常导致回滚,回调不会执行
919
+ await validate_order(order)
920
+
921
+ return order
922
+ # 事务 commit 后,所有 on_commit 回调按注册顺序执行
923
+ ```
924
+
925
+ **在 TransactionManager 中使用**:
926
+
927
+ ```python
928
+ async def manual_with_callback(session: AsyncSession):
929
+ tm = TransactionManager(session)
930
+
931
+ await tm.begin()
932
+ try:
933
+ user = await create_user(session)
934
+ tm.on_commit(lambda: print(f"用户 {{user.id}} 创建成功"))
935
+ await tm.commit() # 提交后执行回调
936
+ except Exception:
937
+ await tm.rollback() # 回滚时回调被清除,不执行
938
+ raise
939
+ ```
940
+
941
+ **回调特性**:
942
+ - 事务成功 `commit()` 后立即执行
943
+ - 事务回滚时,已注册的回调被清除,不执行
944
+ - 按注册顺序执行
945
+ - 同步和异步函数都支持
946
+
947
+ ### 6.8 SELECT FOR UPDATE(行级锁)
948
+
949
+ 在并发场景下锁定查询的行,防止其他事务修改:
950
+
951
+ ```python
952
+ from aury.boot.domain.repository.query_builder import QueryBuilder
953
+
954
+ class AccountRepository(BaseRepository[Account]):
955
+ async def get_for_update(self, account_id: str) -> Account | None:
956
+ """获取并锁定账户记录。"""
957
+ qb = QueryBuilder(Account)
958
+ query = qb.filter(Account.id == account_id).for_update().build()
959
+ result = await self.session.execute(query)
960
+ return result.scalar_one_or_none()
961
+
962
+ async def get_for_update_nowait(self, account_id: str) -> Account | None:
963
+ """获取并锁定,如果已被锁定则立即失败。"""
964
+ qb = QueryBuilder(Account)
965
+ query = qb.filter(Account.id == account_id).for_update(nowait=True).build()
966
+ result = await self.session.execute(query)
967
+ return result.scalar_one_or_none()
968
+
969
+ async def get_for_update_skip_locked(self, ids: list[str]) -> list[Account]:
970
+ """获取并锁定,跳过已被锁定的行。"""
971
+ qb = QueryBuilder(Account)
972
+ query = qb.filter(Account.id.in_(ids)).for_update(skip_locked=True).build()
973
+ result = await self.session.execute(query)
974
+ return list(result.scalars().all())
975
+ ```
976
+
977
+ **for_update 参数**:
978
+ - `nowait=True` - 如果行已被锁定,立即报错而不是等待
979
+ - `skip_locked=True` - 跳过已被锁定的行(常用于队列场景)
980
+ - `of=(Column,...)` - 指定锁定的列(用于 JOIN 场景)
981
+
982
+ **注意**:`nowait` 和 `skip_locked` 互斥,不能同时使用。
983
+
984
+ ### 6.9 事务隔离级别配置
985
+
986
+ 通过环境变量配置数据库的默认事务隔离级别:
987
+
988
+ ```bash
989
+ # .env
990
+ DATABASE_ISOLATION_LEVEL=REPEATABLE READ
991
+ ```
992
+
993
+ **支持的隔离级别**:
994
+ - `READ UNCOMMITTED` - 最低隔离,可读未提交数据(脏读)
995
+ - `READ COMMITTED` - 只读已提交数据(PostgreSQL/Oracle 默认)
996
+ - `REPEATABLE READ` - 可重复读,同一事务内读取结果一致(MySQL 默认)
997
+ - `SERIALIZABLE` - 最高隔离,完全串行化执行
998
+ - `AUTOCOMMIT` - 每条语句自动提交
999
+
1000
+ **选择建议**:
1001
+ - 大多数场景:`READ COMMITTED`(平衡性能和一致性)
1002
+ - 报表/统计查询:`REPEATABLE READ`(保证读取一致性)
1003
+ - 金融交易:`SERIALIZABLE`(最强一致性,性能较低)
1004
+
1005
+ ---
1006
+
1007
+ ## 7. 缓存
1008
+
1009
+ ```python
1010
+ from aury.boot.infrastructure.cache import CacheManager, cached
1011
+
1012
+ # 方式 1:装饰器
1013
+ @cached(ttl=300) # 缓存 5 分钟
1014
+ async def get_user(user_id: int):
1015
+ ...
1016
+
1017
+ # 方式 2:手动操作
1018
+ cache = CacheManager.get_instance()
1019
+ await cache.set("key", value, ttl=300)
1020
+ value = await cache.get("key")
1021
+ await cache.delete("key")
1022
+ ```
1023
+
1024
+ ---
1025
+
1026
+ ## 8. 定时任务(Scheduler)
1027
+
1028
+ **文件**: `app/schedules/__init__.py`
1029
+
1030
+ ```python
1031
+ """定时任务模块。"""
1032
+
1033
+ from aury.boot.common.logging import logger
1034
+ from aury.boot.infrastructure.scheduler import SchedulerManager
1035
+
1036
+ scheduler = SchedulerManager.get_instance()
1037
+
1038
+
1039
+ @scheduler.scheduled_job("interval", seconds=60)
1040
+ async def every_minute():
1041
+ """每 60 秒执行。"""
1042
+ logger.info("定时任务执行中...")
1043
+
1044
+
1045
+ @scheduler.scheduled_job("cron", hour=0, minute=0)
1046
+ async def daily_task():
1047
+ """每天凌晨执行。"""
1048
+ logger.info("每日任务执行中...")
1049
+
1050
+
1051
+ @scheduler.scheduled_job("cron", day_of_week="mon", hour=9)
1052
+ async def weekly_report():
1053
+ """每周一 9 点执行。"""
1054
+ logger.info("周报任务执行中...")
1055
+ ```
1056
+
1057
+ 启用方式:配置 `SCHEDULER_ENABLED=true`,框架自动加载 `app/schedules/` 模块。
1058
+
1059
+ ---
1060
+
1061
+ ## 9. 异步任务(Dramatiq)
1062
+
1063
+ **文件**: `app/tasks/__init__.py`
1064
+
1065
+ ```python
1066
+ """异步任务模块。"""
1067
+
1068
+ from aury.boot.common.logging import logger
1069
+ from aury.boot.infrastructure.tasks import conditional_task
1070
+
1071
+
1072
+ @conditional_task
1073
+ def send_email(to: str, subject: str, body: str):
1074
+ """异步发送邮件。"""
1075
+ logger.info(f"发送邮件到 {{to}}: {{subject}}")
1076
+ # 实际发送逻辑...
1077
+ return {{"status": "sent"}}
1078
+
1079
+
1080
+ @conditional_task
1081
+ def process_order(order_id: str):
1082
+ """异步处理订单。"""
1083
+ logger.info(f"处理订单: {{order_id}}")
1084
+ ```
1085
+
1086
+ 调用方式:
1087
+
1088
+ ```python
1089
+ # 异步执行(发送到队列)
1090
+ send_email.send("user@example.com", "Hello", "World")
1091
+
1092
+ # 延迟执行
1093
+ send_email.send_with_options(args=("user@example.com", "Hello", "World"), delay=60000) # 60秒后
1094
+ ```
1095
+
1096
+ 启用方式:
1097
+ 1. 配置 `TASK_BROKER_URL`(如 `redis://localhost:6379/0`)
1098
+ 2. 运行 Worker:`aury worker`
1099
+
1100
+ ---
1101
+
1102
+ ## 10. 对象存储(基于 aury-sdk-storage)
1103
+
1104
+ ### 10.1 安装
1105
+
1106
+ ```bash
1107
+ # 完整安装(S3/COS/OSS + STS 支持)
1108
+ uv add "aury-sdk-storage[aws]"
1109
+ # 或
1110
+ pip install "aury-sdk-storage[aws]"
1111
+ ```
1112
+
1113
+ ### 10.2 基本用法(StorageManager)
1114
+
1115
+ ```python
1116
+ from io import BytesIO
1117
+ from aury.boot.infrastructure.storage import (
1118
+ StorageManager, StorageConfig, StorageBackend, StorageFile,
1119
+ )
1120
+
1121
+ # 初始化(一般由 StorageComponent 自动完成,以下仅演示手动调用)
1122
+ storage = StorageManager.get_instance()
1123
+ await storage.init(StorageConfig(
1124
+ backend=StorageBackend.COS,
1125
+ bucket_name="my-bucket-1250000000",
1126
+ region="ap-guangzhou",
1127
+ endpoint="https://cos.ap-guangzhou.myqcloud.com",
1128
+ access_key_id="AKIDxxxxx",
1129
+ access_key_secret="xxxxx",
1130
+ ))
1131
+
1132
+ # 上传文件(返回 URL)
1133
+ url = await storage.upload_file(
1134
+ StorageFile(
1135
+ object_name="user/123/avatar.png",
1136
+ data=BytesIO(image_bytes),
1137
+ content_type="image/png",
1138
+ )
1139
+ )
1140
+
1141
+ # 下载文件
1142
+ content = await storage.download_file("user/123/avatar.png")
1143
+
1144
+ # 获取预签名 URL
1145
+ url = await storage.get_file_url("user/123/avatar.png", expires_in=3600)
1146
+
1147
+ # 检查文件是否存在
1148
+ exists = await storage.file_exists("user/123/avatar.png")
1149
+
1150
+ # 删除文件
1151
+ await storage.delete_file("user/123/avatar.png")
1152
+ ```
1153
+
1154
+ ### 10.3 STS 临时凭证(前端直传)
1155
+
1156
+ ```python
1157
+ from aury.sdk.storage.sts import (
1158
+ STSProviderFactory, ProviderType, STSRequest, ActionType,
1159
+ )
1160
+
1161
+ # 创建腾讯云 STS Provider
1162
+ provider = STSProviderFactory.create(
1163
+ ProviderType.TENCENT,
1164
+ secret_id="AKIDxxxxx",
1165
+ secret_key="xxxxx",
1166
+ )
1167
+
1168
+ # 签发临时上传凭证
1169
+ credentials = await provider.get_credentials(
1170
+ STSRequest(
1171
+ bucket="my-bucket-1250000000",
1172
+ region="ap-guangzhou",
1173
+ allow_path="user/123/",
1174
+ action_type=ActionType.WRITE,
1175
+ duration_seconds=900,
1176
+ )
1177
+ )
1178
+
1179
+ # 返回给前端
1180
+ return {{
1181
+ "accessKeyId": credentials.access_key_id,
1182
+ "secretAccessKey": credentials.secret_access_key,
1183
+ "sessionToken": credentials.session_token,
1184
+ "expiration": credentials.expiration.isoformat(),
1185
+ "bucket": credentials.bucket,
1186
+ "region": credentials.region,
1187
+ "endpoint": credentials.endpoint,
1188
+ }}
1189
+ ```
1190
+
1191
+ ### 10.4 本地存储(开发测试)
1192
+
1193
+ ```python
1194
+ from aury.boot.infrastructure.storage import (
1195
+ StorageManager, StorageConfig, StorageBackend, StorageFile,
1196
+ )
1197
+
1198
+ storage = StorageManager.get_instance()
1199
+ await storage.init(StorageConfig(
1200
+ backend=StorageBackend.LOCAL,
1201
+ base_path="./dev_storage",
1202
+ ))
1203
+
1204
+ url = await storage.upload_file(
1205
+ StorageFile(object_name="test.txt", data=b"hello")
1206
+ )
1207
+ # url: file:///path/to/dev_storage/default/test.txt
1208
+ ```
1209
+
1210
+ ---
1211
+
1212
+ ## 11. 日志
1213
+
1214
+ ```python
1215
+ from aury.boot.common.logging import logger
1216
+
1217
+ logger.info("操作成功")
1218
+ logger.warning("警告信息")
1219
+ logger.error("错误信息", exc_info=True)
1220
+
1221
+ # 绑定上下文
1222
+ logger.bind(user_id=123).info("用户操作")
1223
+ ```
1224
+
1225
+ ---
1226
+
1227
+ ## 12. 管理后台(Admin Console,基于 SQLAdmin)
1228
+
1229
+ 默认提供可选的 SQLAdmin 后台(组件自动装配)。启用后路径默认为 `/api/admin-console`。
1230
+
1231
+ - 组件开关与配置由环境变量控制;启用后框架会在启动时自动挂载后台路由。
1232
+ - SQLAdmin 通常需要同步 SQLAlchemy Engine;如果你使用的是异步 `DATABASE_URL`,建议单独设置同步的 `ADMIN_DATABASE_URL`(框架也会尝试自动推导常见驱动映射)。
1233
+
1234
+ 快速启用(.env)
1235
+
1236
+ ```bash
1237
+ # 启用与基本路径
1238
+ ADMIN_ENABLED=true
1239
+ ADMIN_PATH=/api/admin-console
1240
+
1241
+ # 认证(二选一,推荐 basic 或 bearer)
1242
+ ADMIN_AUTH_MODE=basic
1243
+ ADMIN_AUTH_SECRET_KEY=CHANGE_ME_TO_A_RANDOM_SECRET
1244
+ ADMIN_AUTH_BASIC_USERNAME=admin
1245
+ ADMIN_AUTH_BASIC_PASSWORD=change_me
1246
+
1247
+ # 如果使用 bearer
1248
+ # ADMIN_AUTH_MODE=bearer
1249
+ # ADMIN_AUTH_SECRET_KEY=CHANGE_ME
1250
+ # ADMIN_AUTH_BEARER_TOKENS=["token1","token2"]
1251
+
1252
+ # 如需显式提供同步数据库 URL(可选)
1253
+ # ADMIN_DATABASE_URL=postgresql+psycopg://user:pass@localhost:5432/{project_name_snake}
1254
+ ```
1255
+
1256
+ 注册后台视图(在 `admin_console.py` 内)
1257
+
1258
+ ```python
1259
+ from sqladmin import ModelView
1260
+ from {project_name_snake}.models.user import User
1261
+
1262
+ class UserAdmin(ModelView, model=User):
1263
+ column_list = [User.id, User.username, User.email]
1264
+
1265
+ # 方式一:声明式(简单)
1266
+ ADMIN_VIEWS = [UserAdmin]
1267
+
1268
+ # 方式二:函数注册(更灵活)
1269
+ # def register_admin(admin):
1270
+ # admin.add_view(UserAdmin)
1271
+ ```
1272
+
1273
+ 自定义认证(可选,高阶)
1274
+ - 通过 `ADMIN_AUTH_BACKEND=module:attr` 指定自定义 backend;或在 `admin_console.py` 实现 `register_admin_auth(config)` 返回 SQLAdmin 的 `AuthenticationBackend`。
1275
+ - 生产环境下必须设置 `ADMIN_AUTH_SECRET_KEY`,不允许 `none` 模式。
1276
+
1277
+ 访问
1278
+ - 启动服务后访问:`http://127.0.0.1:8000/api/admin-console`(或你配置的 `ADMIN_PATH`)。
1279
+
1280
+ ---
1281
+
1282
+ ## 12. 异常处理
1283
+
1284
+ 框架提供了统一的异常处理机制,所有异常都会被全局异常处理中间件捕获并转换为标准的 HTTP 响应。
1285
+
1286
+ ### 12.1 异常处理流程
1287
+
1288
+ ```
1289
+ 请求 → API 路由 → Service → Repository → 抛出异常
1290
+
1291
+ 全局异常处理中间件(Middleware)
1292
+
1293
+ 转换为 ErrorResponse Schema
1294
+
1295
+ JSON 响应返回客户端
1296
+ ```
1297
+
1298
+ **流程说明**:
1299
+ 1. 业务代码抛出异常(如 `NotFoundError`)
1300
+ 2. 框架的全局异常处理中间件自动捕获
1301
+ 3. 根据异常类型转换为对应的 HTTP 状态码和错误响应
1302
+ 4. 返回统一格式的 JSON 错误响应
1303
+
1304
+ **响应格式**:
1305
+ ```json
1306
+ {{
1307
+ "code": "NOT_FOUND",
1308
+ "message": "用户不存在",
1309
+ "data": null
1310
+ }}
1311
+ ```
1312
+
1313
+ ### 12.2 内置异常
1314
+
1315
+ ```python
1316
+ from aury.boot.application.errors import (
1317
+ BaseError,
1318
+ NotFoundError, # 404
1319
+ AlreadyExistsError, # 409
1320
+ ValidationError, # 422
1321
+ UnauthorizedError, # 401
1322
+ ForbiddenError, # 403
1323
+ BusinessError, # 400
1324
+ )
1325
+
1326
+ # 使用示例
1327
+ raise NotFoundError("用户不存在", resource=user_id)
1328
+ raise AlreadyExistsError(f"邮箱 {{email}} 已被注册")
1329
+ raise UnauthorizedError("未登录或登录已过期")
1330
+ ```
1331
+
1332
+ ### 12.3 自定义异常
1333
+
1334
+ **文件**: `app/exceptions/order.py`
1335
+
1336
+ ```python
1337
+ from fastapi import status
1338
+ from aury.boot.application.errors import BaseError
1339
+
1340
+
1341
+ # 自定义异常(只需设置类属性)
1342
+ class OrderError(BaseError):
1343
+ default_message = "订单错误"
1344
+ default_code = "ORDER_ERROR"
1345
+ default_status_code = status.HTTP_400_BAD_REQUEST
1346
+
1347
+
1348
+ class OrderNotFoundError(OrderError):
1349
+ default_message = "订单不存在"
1350
+ default_code = "ORDER_NOT_FOUND"
1351
+ default_status_code = status.HTTP_404_NOT_FOUND
1352
+
1353
+
1354
+ class InsufficientStockError(OrderError):
1355
+ default_message = "库存不足"
1356
+ default_code = "INSUFFICIENT_STOCK"
1357
+ default_status_code = status.HTTP_400_BAD_REQUEST
1358
+
1359
+
1360
+ # 使用
1361
+ raise OrderNotFoundError() # 使用默认值
1362
+ raise OrderError(message="订单ID无效") # 自定义消息
1363
+ raise InsufficientStockError(message=f"商品 {{product_id}} 库存不足")
1364
+ ```
1365
+
1366
+ ### 12.4 异常与 Schema 的关系
1367
+
1368
+ 异常处理中间件会自动将异常转换为 Schema 响应:
1369
+
1370
+ ```python
1371
+ # Service 层抛出异常
1372
+ raise NotFoundError("用户不存在")
1373
+
1374
+ # 中间件捕获并转换为响应
1375
+ # HTTP 状态码:404
1376
+ # 响应体:
1377
+ # {{
1378
+ # "code": "NOT_FOUND",
1379
+ # "message": "用户不存在",
1380
+ # "data": null
1381
+ # }}
1382
+ ```
1383
+
1384
+ **最佳实践**:
1385
+ - 在 Service 层抛出业务异常,不要在 API 层手动处理
1386
+ - 使用框架内置异常或自定义异常,不要直接抛出 `Exception`
1387
+ - 自定义异常继承 `BaseError`,框架会自动处理
1388
+
1389
+ ---
1390
+
1391
+ ## 最佳实践
1392
+
1393
+ 1. **分层架构**:API → Service → Repository → Model
1394
+ 2. **事务管理**:在 Service 层使用 `@transactional`,只读操作可不加
1395
+ 3. **错误处理**:使用框架异常类,全局异常处理器统一处理
1396
+ 4. **配置管理**:使用 `.env` 文件,不提交到版本库
1397
+ 5. **日志记录**:使用框架 logger,支持结构化日志和链路追踪