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,217 @@
1
+ # AGENTS.md
2
+
3
+ > 这是给 AI 编程助手阅读的项目上下文文件。根据开发任务类型,阅读下方「开发任务文档索引」中对应的文档。
4
+
5
+ ## 项目概述
6
+
7
+ - **项目名称**: {project_name}
8
+ - **包名**: {package_name}
9
+ - **框架**: [Aury Boot](https://github.com/AuriMyth/aury-boot)(基于 FastAPI + SQLAlchemy 2.0 的微服务框架)
10
+ - **Python 版本**: >= 3.13
11
+ - **包管理**: uv(推荐)或 pip
12
+
13
+ ## 常用命令
14
+
15
+ ```bash
16
+ # 开发服务器(热重载)
17
+ aury server dev
18
+
19
+ # 数据库迁移
20
+ aury migrate make -m "描述" # 生成迁移
21
+ aury migrate up # 执行迁移
22
+ aury migrate down # 回滚迁移
23
+ aury migrate status # 查看状态
24
+ aury migrate show # 查看历史
25
+
26
+ # 测试
27
+ pytest
28
+ pytest tests/test_xxx.py -v
29
+
30
+ # 代码检查
31
+ ruff check .
32
+ mypy {package_name}/
33
+ ```
34
+
35
+ > ⚠️ **重要**:数据库迁移文件**必须**使用 `aury migrate make` 命令生成,**禁止**手写迁移文件!
36
+
37
+ ## 项目结构
38
+
39
+ ```
40
+ {project_name}/
41
+ ├── {package_name}/ # 主代码包
42
+ │ ├── models/ # SQLAlchemy ORM 模型
43
+ │ ├── repositories/ # 数据访问层(Repository 模式)
44
+ │ ├── services/ # 业务逻辑层
45
+ │ ├── schemas/ # Pydantic 请求/响应模型
46
+ │ ├── api/ # FastAPI 路由
47
+ │ ├── exceptions/ # 业务异常
48
+ │ ├── tasks/ # 异步任务(Dramatiq)
49
+ │ └── schedules/ # 定时任务
50
+ ├── aury_docs/ # 开发文档(由 aury docs dev 生成)
51
+ ├── tests/ # 测试
52
+ ├── migrations/ # 数据库迁移
53
+ └── main.py # 应用入口
54
+ ```
55
+
56
+ ## 重要提醒
57
+
58
+ > **修改代码前,必须先阅读对应的文档!**
59
+ >
60
+ > 框架的 API 可能与常见框架不同,不要猜测。请查看 `aury_docs/` 下对应的文档。
61
+
62
+ ## 开发任务文档索引
63
+
64
+ 根据你要开发的功能类型,**必须阅读**对应的文档:
65
+
66
+ ### CRUD / 数据库相关(最常见)
67
+
68
+ 开发 CRUD 功能时,按顺序阅读以下文档:
69
+
70
+ 1. **[aury_docs/01-model.md](./aury_docs/01-model.md)** - Model 定义规范
71
+ - 基类选择(UUIDAuditableStateModel 等)
72
+ - 字段类型映射
73
+ - 约束定义(软删除模型的复合唯一约束)
74
+
75
+ 2. **[aury_docs/02-repository.md](./aury_docs/02-repository.md)** - Repository 使用
76
+ - BaseRepository API
77
+ - Filters 语法(__gt, __like 等)
78
+ - 自动提交机制
79
+
80
+ 3. **[aury_docs/03-service.md](./aury_docs/03-service.md)** - Service 编写与事务
81
+ - 事务装饰器 @transactional
82
+ - 跨 Service 调用
83
+ - 事务传播 / Savepoints / on_commit 回调
84
+ - SELECT FOR UPDATE
85
+
86
+ 4. **[aury_docs/04-schema.md](./aury_docs/04-schema.md)** - Pydantic Schema
87
+ - 请求/响应模型
88
+ - 常用验证
89
+
90
+ 5. **[aury_docs/05-api.md](./aury_docs/05-api.md)** - API 路由
91
+ - 路由编写示例
92
+ - 依赖注入模式
93
+
94
+ 6. **[aury_docs/06-exception.md](./aury_docs/06-exception.md)** - 异常处理
95
+ - 内置异常
96
+ - 自定义异常
97
+
98
+ ### 异步任务 / 定时任务
99
+
100
+ - **[aury_docs/08-scheduler.md](./aury_docs/08-scheduler.md)** - 定时任务(APScheduler)
101
+ - **[aury_docs/09-tasks.md](./aury_docs/09-tasks.md)** - 异步任务(Dramatiq)
102
+
103
+ ### 基础设施
104
+
105
+ - **[aury_docs/07-cache.md](./aury_docs/07-cache.md)** - 缓存
106
+ - **[aury_docs/10-storage.md](./aury_docs/10-storage.md)** - 对象存储(S3/COS/OSS)
107
+ - **[aury_docs/11-logging.md](./aury_docs/11-logging.md)** - 日志
108
+ - **[aury_docs/12-admin.md](./aury_docs/12-admin.md)** - 管理后台(SQLAdmin)
109
+ - **[aury_docs/13-channel.md](./aury_docs/13-channel.md)** - 流式通道(SSE)
110
+ - **[aury_docs/14-mq.md](./aury_docs/14-mq.md)** - 消息队列
111
+ - **[aury_docs/15-events.md](./aury_docs/15-events.md)** - 事件总线
112
+
113
+ ### 配置 / CLI / 环境变量
114
+
115
+ - **[aury_docs/00-overview.md](./aury_docs/00-overview.md)** - 项目概览与最佳实践
116
+ - **[aury_docs/99-cli.md](./aury_docs/99-cli.md)** - CLI 命令参考
117
+ - **[.env.example](./.env.example)** - 所有可用环境变量
118
+
119
+ ## 代码规范
120
+
121
+ ### Model 规范
122
+
123
+ - **必须**继承框架预定义基类,**不要**直接继承 `Base`
124
+ - **推荐**使用 `UUIDAuditableStateModel`(UUID 主键 + 时间戳 + 软删除)
125
+ - 软删除模型**必须**使用复合唯一约束(包含 `deleted_at`),不能单独使用 `unique=True`
126
+ - **不建议**使用数据库外键(`ForeignKey`),通过程序控制关系,便于分库分表和微服务拆分
127
+
128
+ ```python
129
+ # ✅ 正确
130
+ from aury.boot.domain.models import UUIDAuditableStateModel
131
+
132
+ class User(UUIDAuditableStateModel):
133
+ __tablename__ = "users"
134
+ email: Mapped[str] = mapped_column(String(255), index=True)
135
+ __table_args__ = (
136
+ UniqueConstraint("email", "deleted_at", name="uq_users_email_deleted"),
137
+ )
138
+
139
+ # ❌ 错误:直接继承 Base
140
+ from aury.boot.domain.models.base import Base
141
+ class User(Base): ...
142
+ ```
143
+
144
+ ### Service 规范
145
+
146
+ - 写操作**必须**使用 `@transactional` 装饰器
147
+ - 只读操作可以不加事务装饰器
148
+ - 跨 Service 调用通过共享 session 实现事务共享
149
+
150
+ ```python
151
+ from aury.boot.domain.transaction import transactional
152
+
153
+ class UserService(BaseService):
154
+ @transactional
155
+ async def create(self, data: UserCreate) -> User:
156
+ # 自动事务管理
157
+ return await self.repo.create(data.model_dump())
158
+ ```
159
+
160
+ ### Manager API 规范
161
+
162
+ 所有基础设施 Manager 统一使用 `initialize()` 方法初始化:
163
+
164
+ ```python
165
+ # ✅ 正确
166
+ from aury.boot.infrastructure.cache import CacheManager
167
+ cache = CacheManager.get_instance()
168
+ await cache.initialize(backend="redis", url="redis://localhost:6379")
169
+
170
+ from aury.boot.infrastructure.storage import StorageManager, StorageConfig
171
+ storage = StorageManager.get_instance()
172
+ await storage.initialize(StorageConfig(...))
173
+
174
+ from aury.boot.infrastructure.events import EventBusManager
175
+ events = EventBusManager.get_instance()
176
+ await events.initialize(backend="memory")
177
+
178
+ # ❗ 注意:没有 configure() 方法,配置直接传入 initialize()
179
+ ```
180
+
181
+ ### 异常规范
182
+
183
+ - **必须**继承框架异常类,**不要**直接继承 `Exception`
184
+ - 使用框架内置异常:`NotFoundError`, `AlreadyExistsError`, `UnauthorizedError` 等
185
+
186
+ ```python
187
+ # ✅ 正确
188
+ from aury.boot.application.errors import NotFoundError
189
+ raise NotFoundError("用户不存在", resource=user_id)
190
+
191
+ # ❌ 错误
192
+ raise Exception("用户不存在")
193
+ ```
194
+
195
+ ### 响应规范
196
+
197
+ - **不要**自定义通用响应 Schema
198
+ - 使用框架内置:`BaseResponse`, `PaginationResponse`
199
+
200
+ ```python
201
+ from aury.boot.application.interfaces.egress import BaseResponse
202
+ return BaseResponse(code=200, message="成功", data=user)
203
+ ```
204
+
205
+ ## 禁止操作
206
+
207
+ - ❌ 不要直接修改 `aury/boot/` 框架代码
208
+ - ❌ 不要在 Model 中直接使用 `unique=True`(软删除模型)
209
+ - ❌ 不要自定义通用响应 Schema
210
+ - ❌ 不要在 Repository 之外直接操作 session
211
+ - ❌ 不要提交 `.env` 文件到版本库
212
+
213
+ ## 需要确认的操作
214
+
215
+ - ⚠️ 添加新的 pip 依赖前请确认
216
+ - ⚠️ 修改数据库迁移前请确认
217
+ - ⚠️ 删除文件前请确认
@@ -106,6 +106,6 @@ aury worker
106
106
 
107
107
  ## 文档
108
108
 
109
- - [DEVELOPMENT.md](./DEVELOPMENT.md) - 开发指南(代码组织与规范)
110
- - [CLI.md](./CLI.md) - CLI 命令参考
109
+ - [AGENTS.md](./AGENTS.md) - AI 编程助手上下文
110
+ - [aury_docs/](./aury_docs/) - 开发文档(包含 Model/Service/API 等指南)
111
111
  - [Aury Boot 文档](https://github.com/AuriMyth/aury-boot)
@@ -0,0 +1,59 @@
1
+ # {project_name} 开发指南
2
+
3
+ 本文档基于 [Aury Boot](https://github.com/AuriMyth/aury-boot) 框架。
4
+
5
+ CLI 命令参考请查看 [99-cli.md](./99-cli.md)。
6
+
7
+ ---
8
+
9
+ ## 目录结构
10
+
11
+ ```
12
+ {project_name}/
13
+ ├── {package_name}/ # 代码包(默认 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
+ ## 最佳实践
30
+
31
+ 1. **分层架构**:API → Service → Repository → Model
32
+ 2. **事务管理**:在 Service 层使用 `@transactional`,只读操作可不加
33
+ 3. **错误处理**:使用框架异常类,全局异常处理器统一处理
34
+ 4. **配置管理**:使用 `.env` 文件,不提交到版本库
35
+ 5. **日志记录**:使用框架 logger,支持结构化日志和链路追踪
36
+ 6. **多实例配置**:环境变量格式 `{{PREFIX}}_{{INSTANCE}}_{{FIELD}}`
37
+
38
+ ---
39
+
40
+ ## 文档索引
41
+
42
+ ### CRUD / 数据库
43
+ - [01-model.md](./01-model.md) - Model 定义
44
+ - [02-repository.md](./02-repository.md) - Repository 使用
45
+ - [03-service.md](./03-service.md) - Service 与事务
46
+ - [04-schema.md](./04-schema.md) - Pydantic Schema
47
+ - [05-api.md](./05-api.md) - API 路由
48
+ - [06-exception.md](./06-exception.md) - 异常处理
49
+
50
+ ### 基础设施
51
+ - [07-cache.md](./07-cache.md) - 缓存
52
+ - [08-scheduler.md](./08-scheduler.md) - 定时任务
53
+ - [09-tasks.md](./09-tasks.md) - 异步任务
54
+ - [10-storage.md](./10-storage.md) - 对象存储
55
+ - [11-logging.md](./11-logging.md) - 日志
56
+ - [12-admin.md](./12-admin.md) - 管理后台
57
+ - [13-channel.md](./13-channel.md) - 流式通道(SSE)
58
+ - [14-mq.md](./14-mq.md) - 消息队列
59
+ - [15-events.md](./15-events.md) - 事件总线
@@ -0,0 +1,183 @@
1
+ # Model(数据模型)
2
+
3
+ ## 1.1 模型基类选择
4
+
5
+ 框架提供多种预组合基类,按需选择:
6
+
7
+ | 基类 | 主键 | 时间戳 | 软删除 | 乐观锁 | 场景 |
8
+ |------|------|--------|--------|--------|------|
9
+ | `Model` | int | ✓ | ✗ | ✗ | 简单实体 |
10
+ | `AuditableStateModel` | int | ✓ | ✓ | ✗ | 需软删除 |
11
+ | `UUIDModel` | UUID | ✓ | ✗ | ✗ | 分布式 |
12
+ | `UUIDAuditableStateModel` | UUID | ✓ | ✓ | ✗ | **推荐** |
13
+ | `VersionedModel` | int | ✗ | ✗ | ✓ | 乐观锁 |
14
+ | `VersionedUUIDModel` | UUID | ✓ | ✗ | ✓ | UUID+乐观锁 |
15
+ | `FullFeaturedUUIDModel` | UUID | ✓ | ✓ | ✓ | 全功能 |
16
+
17
+ ## 1.2 基类自动提供的字段
18
+
19
+ **IDMixin** (int 主键):
20
+ ```python
21
+ id: Mapped[int] # 自增主键
22
+ ```
23
+
24
+ **UUIDMixin** (UUID 主键):
25
+ ```python
26
+ import uuid
27
+ from sqlalchemy.types import Uuid as SQLAlchemyUuid
28
+ from sqlalchemy.orm import Mapped, mapped_column
29
+
30
+ id: Mapped[uuid.UUID] = mapped_column(
31
+ SQLAlchemyUuid(as_uuid=True), # SQLAlchemy 2.0 自动适配 PG(uuid) 和 MySQL(char(36))
32
+ primary_key=True,
33
+ default=uuid.uuid4,
34
+ )
35
+ ```
36
+
37
+ > **关于 SQLAlchemyUuid**:
38
+ > - 使用 `sqlalchemy.types.Uuid`(导入为 `SQLAlchemyUuid`)而非直接使用 `uuid.UUID`
39
+ > - `as_uuid=True` 确保 Python 层面使用 UUID 对象而非字符串
40
+ > - 框架会自动适配不同数据库:PostgreSQL 使用原生 UUID 类型,MySQL/SQLite 使用 CHAR(36)
41
+ > - 如需手动定义 UUID 字段,请使用 `SQLAlchemyUuid(as_uuid=True)` 而非 `Uuid(as_uuid=True)`
42
+
43
+ **TimestampMixin** (时间戳):
44
+ ```python
45
+ created_at: Mapped[datetime] # 创建时间,自动设置
46
+ updated_at: Mapped[datetime] # 更新时间,自动更新
47
+ ```
48
+
49
+ **AuditableStateMixin** (软删除):
50
+ ```python
51
+ deleted_at: Mapped[int] # 删除时间戳,0=未删除,>0=删除时间
52
+ # 自动提供:is_deleted 属性、mark_deleted() 方法、restore() 方法
53
+ ```
54
+
55
+ > **注意**:使用软删除的模型不要单独使用 `unique=True`,否则删除后再插入相同值会报错。
56
+ > 应使用复合唯一索引:`UniqueConstraint("email", "deleted_at", name="uq_users_email_deleted")`
57
+
58
+ **VersionMixin** (乐观锁):
59
+ ```python
60
+ version: Mapped[int] # 版本号,自动管理
61
+ ```
62
+
63
+ ## 1.3 Model 编写示例
64
+
65
+ **文件**: `{package_name}/models/user.py`
66
+
67
+ ```python
68
+ """User 数据模型。"""
69
+
70
+ from sqlalchemy import String, Boolean, UniqueConstraint
71
+ from sqlalchemy.orm import Mapped, mapped_column
72
+
73
+ from aury.boot.domain.models import UUIDAuditableStateModel
74
+
75
+
76
+ class User(UUIDAuditableStateModel):
77
+ """User 模型。
78
+
79
+ 继承 UUIDAuditableStateModel 自动获得:
80
+ - id: UUID 主键(使用 SQLAlchemyUuid 自动适配数据库)
81
+ - created_at, updated_at: 时间戳
82
+ - deleted_at: 软删除支持
83
+ """
84
+
85
+ __tablename__ = "users"
86
+
87
+ name: Mapped[str] = mapped_column(String(100), nullable=False, comment="用户名")
88
+ email: Mapped[str] = mapped_column(String(255), nullable=False, index=True, comment="邮箱")
89
+ is_active: Mapped[bool] = mapped_column(Boolean, default=True, comment="是否激活")
90
+
91
+ # 软删除模型必须使用复合唯一约束(包含 deleted_at),避免删除后无法插入相同值
92
+ # 注意:复合约束必须使用 __table_args__,这是 SQLAlchemy 的要求
93
+ __table_args__ = (
94
+ UniqueConstraint("email", "deleted_at", name="uq_users_email_deleted"),
95
+ )
96
+ ```
97
+
98
+ ## 1.4 字段类型映射
99
+
100
+ | Python 类型 | SQLAlchemy 类型 | 说明 |
101
+ |-------------|----------------|------|
102
+ | `str` | `String(length)` | 必须指定长度 |
103
+ | `int` | `Integer` | 整数 |
104
+ | `float` | `Float` | 浮点数 |
105
+ | `bool` | `Boolean` | 布尔值 |
106
+ | `datetime` | `DateTime(timezone=True)` | 带时区 |
107
+ | `date` | `Date` | 日期 |
108
+ | `Decimal` | `Numeric(precision, scale)` | 精确小数 |
109
+ | `dict`/`list` | `JSON` | JSON 数据 |
110
+ | `uuid.UUID` | `SQLAlchemyUuid(as_uuid=True)` | UUID(推荐使用 `from sqlalchemy.types import Uuid as SQLAlchemyUuid`) |
111
+
112
+ ## 1.5 常用字段约束
113
+
114
+ ```python
115
+ from sqlalchemy import String, Integer, Index, UniqueConstraint
116
+ from sqlalchemy.orm import Mapped, mapped_column
117
+ import uuid
118
+
119
+ class Example(UUIDAuditableStateModel):
120
+ __tablename__ = "examples"
121
+
122
+ # 可选字段
123
+ description: Mapped[str | None] = mapped_column(String(500), nullable=True)
124
+
125
+ # 带默认值
126
+ status: Mapped[int] = mapped_column(Integer, default=0, server_default="0", index=True)
127
+
128
+ # 单列索引:直接在 mapped_column 中使用 index=True(推荐)
129
+ code: Mapped[str] = mapped_column(String(50), index=True, comment="编码")
130
+
131
+ # 单列唯一约束:直接在 mapped_column 中使用 unique=True(仅非软删除模型)
132
+ # 注意:软删除模型不能单独使用 unique=True,必须使用复合唯一约束
133
+
134
+ # 关联字段(不使用数据库外键,通过程序控制关系)
135
+ category_id: Mapped[uuid.UUID | None] = mapped_column(index=True)
136
+
137
+ # 复合索引和复合唯一约束:必须使用 __table_args__(SQLAlchemy 要求)
138
+ # 软删除模型必须使用复合唯一约束(包含 deleted_at),避免删除后无法插入相同值
139
+ __table_args__ = (
140
+ Index("ix_examples_status_created", "status", "created_at"),
141
+ UniqueConstraint("code", "deleted_at", name="uq_examples_code_deleted"),
142
+ )
143
+
144
+
145
+ # 非软删除模型可以直接使用 unique=True 和 index=True
146
+ class Config(UUIDModel): # UUIDModel 不包含软删除
147
+ __tablename__ = "configs"
148
+
149
+ # 单列唯一约束:直接在 mapped_column 中使用(推荐)
150
+ key: Mapped[str] = mapped_column(String(100), unique=True, index=True, comment="配置键")
151
+ value: Mapped[str] = mapped_column(String(500), comment="配置值")
152
+
153
+ # 单列索引:直接在 mapped_column 中使用(推荐)
154
+ name: Mapped[str] = mapped_column(String(100), index=True, comment="配置名称")
155
+ ```
156
+
157
+ **约束定义最佳实践**:
158
+
159
+ 1. **单列索引**:使用 `index=True` 在 `mapped_column` 中(推荐)
160
+ ```python
161
+ email: Mapped[str] = mapped_column(String(255), index=True)
162
+ ```
163
+
164
+ 2. **单列唯一约束**:
165
+ - 非软删除模型:使用 `unique=True` 在 `mapped_column` 中(推荐)
166
+ - 软删除模型:必须使用复合唯一约束(包含 `deleted_at`)
167
+
168
+ 3. **复合索引/唯一约束**:必须使用 `__table_args__`(SQLAlchemy 要求)
169
+ ```python
170
+ __table_args__ = (
171
+ Index("ix_name", "col1", "col2"),
172
+ UniqueConstraint("col1", "col2", name="uq_name"),
173
+ )
174
+ ```
175
+
176
+ 4. **关联关系**:**不建议使用数据库外键**,通过程序控制关系
177
+ - 便于分库分表、微服务拆分
178
+ - 避免级联操作影响性能
179
+ - 简化数据迁移
180
+ ```python
181
+ # 只存储关联 ID,不使用 ForeignKey
182
+ category_id: Mapped[uuid.UUID | None] = mapped_column(index=True)
183
+ ```
@@ -0,0 +1,206 @@
1
+ # Repository(数据访问层)
2
+
3
+ ## 2.1 Repository 编写示例
4
+
5
+ **文件**: `{package_name}/repositories/user_repository.py`
6
+
7
+ ```python
8
+ """User 数据访问层。"""
9
+
10
+ from aury.boot.domain.repository.impl import BaseRepository
11
+
12
+ from {package_name}.models.user import User
13
+
14
+
15
+ class UserRepository(BaseRepository[User]):
16
+ """User 仓储。
17
+
18
+ 继承 BaseRepository 自动获得:
19
+ - get(id): 按 ID 获取
20
+ - get_by(**filters): 按条件获取单个
21
+ - list(skip, limit, **filters): 获取列表
22
+ - paginate(params, **filters): 分页获取
23
+ - count(**filters): 计数
24
+ - exists(**filters): 是否存在
25
+ - create(data): 创建
26
+ - update(entity, data): 更新
27
+ - delete(entity, soft=True): 删除(默认软删除)
28
+ - batch_create(data_list): 批量创建
29
+ - bulk_insert(data_list): 高性能批量插入
30
+ """
31
+
32
+ async def get_by_email(self, email: str) -> User | None:
33
+ """按邮箱查询用户。"""
34
+ return await self.get_by(email=email)
35
+
36
+ async def list_active(self, skip: int = 0, limit: int = 100) -> list[User]:
37
+ """获取激活用户列表。"""
38
+ return await self.list(skip=skip, limit=limit, is_active=True)
39
+ ```
40
+
41
+ ## 2.2 BaseRepository 方法详解
42
+
43
+ ```python
44
+ from sqlalchemy.ext.asyncio import AsyncSession
45
+
46
+ # 初始化(默认 auto_commit=True)
47
+ repo = UserRepository(session, User)
48
+
49
+ # === 查询 ===
50
+ user = await repo.get(user_id) # 按 ID(支持 int/UUID)
51
+ user = await repo.get_by(email="a@b.com") # 按条件(简单 AND 过滤)
52
+ users = await repo.list(skip=0, limit=10) # 列表
53
+ users = await repo.list(is_active=True) # 带过滤
54
+ count = await repo.count(is_active=True) # 计数
55
+ exists = await repo.exists(email="a@b.com") # 是否存在
56
+
57
+ # === 分页 ===
58
+ from aury.boot.domain.pagination import PaginationParams, SortParams
59
+
60
+ result = await repo.paginate(
61
+ pagination_params=PaginationParams(page=1, page_size=20),
62
+ sort_params=SortParams.from_string("-created_at"),
63
+ is_active=True,
64
+ )
65
+
66
+ # PaginationResult 结构:
67
+ # - result.items: list[T] # 数据列表
68
+ # - result.total: int # 总记录数
69
+ # - result.page: int # 当前页码
70
+ # - result.page_size: int # 每页数量
71
+ # - result.total_pages: int # 总页数
72
+ # - result.has_next: bool # 是否有下一页
73
+ # - result.has_prev: bool # 是否有上一页
74
+
75
+ # === 创建 ===
76
+ user = await repo.create({{"name": "Alice", "email": "a@b.com"}})
77
+ users = await repo.batch_create([{{"name": "A"}}, {{"name": "B"}}]) # 返回实体
78
+ await repo.bulk_insert([{{"name": "A"}}, {{"name": "B"}}]) # 高性能,无返回
79
+
80
+ # === 更新 ===
81
+ user = await repo.update(user, {{"name": "Bob"}})
82
+
83
+ # === 删除 ===
84
+ await repo.delete(user) # 软删除
85
+ await repo.delete(user, soft=False) # 硬删除
86
+ await repo.hard_delete(user) # 硬删除别名
87
+ deleted = await repo.delete_by_id(user_id) # 按 ID 删除
88
+ ```
89
+
90
+ ### 2.2.1 Filters 语法(增强)
91
+
92
+ BaseRepository 的 `get_by/list/paginate/count/exists` 的 `**filters` 支持下列操作符(与 `QueryBuilder.filter` 对齐):
93
+ - `__gt`, `__lt`, `__gte`, `__lte`, `__in`, `__like`, `__ilike`, `__isnull`, `__ne`
94
+
95
+ 示例:
96
+
97
+ ```python
98
+ # 模糊匹配 + 范围 + IN + 为空判断(条件之间为 AND 关系)
99
+ users = await repo.list(
100
+ name__ilike="%foo%",
101
+ age__gte=18,
102
+ id__in=[u1, u2, u3],
103
+ deleted_at__isnull=True,
104
+ )
105
+
106
+ # 单个实体(不等于)
107
+ user = await repo.get_by(status__ne="archived")
108
+ ```
109
+
110
+ > 注意:filters 条件之间用 AND 组合;如需 AND/OR/NOT 的复杂组合,请使用 `QueryBuilder`(见 2.4)。
111
+
112
+ ### 2.2.2 排序参数(SortParams)
113
+
114
+ `SortParams.from_string()` 支持两种语法:
115
+
116
+ ```python
117
+ from aury.boot.domain.pagination import SortParams
118
+
119
+ # 简洁语法:"-" 前缀表示降序
120
+ sort_params = SortParams.from_string("-created_at")
121
+ sort_params = SortParams.from_string("-created_at,priority") # 多字段
122
+
123
+ # 完整语法:字段:方向
124
+ sort_params = SortParams.from_string("created_at:desc,priority:asc")
125
+
126
+ # 带字段白名单验证(防止 SQL 注入)
127
+ ALLOWED_FIELDS = {{"id", "created_at", "priority", "status"}}
128
+ sort_params = SortParams.from_string(
129
+ "-created_at",
130
+ allowed_fields=ALLOWED_FIELDS
131
+ )
132
+ # 传入非法字段会抛出 ValueError
133
+ ```
134
+
135
+ ### 2.2.3 查询全部(limit=None)
136
+
137
+ `list()` 支持 `limit=None` 返回全部记录(谨慎使用大表):
138
+
139
+ ```python
140
+ all_users = await repo.list(limit=None) # 全量
141
+ active_all = await repo.list(limit=None, is_active=True)
142
+ ```
143
+
144
+ ## 2.3 自动提交机制
145
+
146
+ BaseRepository 支持智能的自动提交机制,优于 Django 的设计:
147
+
148
+ | 场景 | 行为 |
149
+ |------|------|
150
+ | 非事务中 + `auto_commit=True` | 写操作后自动 commit |
151
+ | 非事务中 + `auto_commit=False` | 只 flush,需手动管理或使用 `.with_commit()` |
152
+ | 在事务中(`@transactional` 等) | **永不自动提交**,由事务统一管理 |
153
+
154
+ ```python
155
+ # 默认行为:非事务中自动提交
156
+ repo = UserRepository(session, User) # auto_commit=True
157
+ await repo.create({{"name": "test"}}) # 自动 commit
158
+
159
+ # 禁用自动提交
160
+ repo = UserRepository(session, User, auto_commit=False)
161
+ await repo.create({{"name": "test"}}) # 只 flush,不 commit
162
+
163
+ # 单次强制提交(auto_commit=False 时)
164
+ await repo.with_commit().create({{"name": "test2"}}) # 强制 commit
165
+
166
+ # 在事务中:无论 auto_commit 是什么,都不会自动提交
167
+ @transactional
168
+ async def create_with_profile(session: AsyncSession):
169
+ repo = UserRepository(session, User) # auto_commit=True 但不生效
170
+ user = await repo.create({{"name": "a"}}) # 只 flush
171
+ profile = await profile_repo.create({{"user_id": user.id}}) # 只 flush
172
+ # 事务结束时统一 commit
173
+ ```
174
+
175
+ **设计优势**(对比 Django):
176
+ - Django:每个 `save()` 默认独立事务,容易无意识地失去原子性
177
+ - Aury Boot:默认自动提交,但**事务上下文自动接管**,更显式可控
178
+
179
+ ## 2.4 复杂查询示例
180
+
181
+ ```python
182
+ async def search_users(
183
+ self,
184
+ keyword: str | None = None,
185
+ status: int | None = None,
186
+ ) -> list[User]:
187
+ """复杂搜索。"""
188
+ query = self.query() # 自动排除软删除
189
+
190
+ if keyword:
191
+ # 使用 QueryBuilder 的 or_ 与 filter_by 组合复杂条件
192
+ query = query.filter_by(
193
+ query.or_(
194
+ User.name.ilike(f"%{{keyword}}%"),
195
+ User.email.ilike(f"%{{keyword}}%"),
196
+ )
197
+ )
198
+
199
+ if status is not None:
200
+ # 简单等值可直接使用 filter(支持 **kwargs)
201
+ query = query.filter(status=status)
202
+
203
+ query = query.order_by("-created_at").limit(100)
204
+ result = await self.session.execute(query.build())
205
+ return list(result.scalars().all())
206
+ ```