create-dwy 0.5.1 → 0.6.0
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.
- package/bin/index.js +1 -1
- package/package.json +2 -2
- package/src/create.js +61 -37
- package/src/sync.js +212 -61
- package/templates/claude-global/CLAUDE.md +92 -0
- package/templates/claude-global/rules/dwy-android-core.md +610 -0
- package/templates/claude-global/rules/dwy-api-design.md +110 -0
- package/templates/claude-global/rules/dwy-db-migration.md +46 -0
- package/templates/claude-global/rules/dwy-flutter-core.md +564 -0
- package/templates/claude-global/rules/dwy-flutter-network.md +222 -0
- package/templates/claude-global/rules/dwy-flutter-router.md +97 -0
- package/templates/claude-global/rules/dwy-flutter-testing.md +94 -0
- package/templates/claude-global/rules/dwy-git-commit-convention.md +82 -0
- package/templates/claude-global/rules/dwy-git-security.md +53 -0
- package/templates/claude-global/rules/dwy-payload-limits.md +178 -0
- package/templates/claude-global/rules/{prefer-open-source.md → dwy-prefer-open-source.md} +4 -0
- package/templates/claude-global/rules/dwy-project-structure.md +181 -0
- package/templates/claude-global/rules/{python-code-style.md → dwy-python-core.md} +292 -358
- package/templates/claude-global/rules/dwy-python-fastapi.md +272 -0
- package/templates/claude-global/rules/dwy-python-orm.md +61 -0
- package/templates/claude-global/rules/{backend-security.md → dwy-python-security.md} +99 -72
- package/templates/claude-global/rules/dwy-python-testing.md +53 -0
- package/templates/claude-global/rules/{server-security.md → dwy-server-security.md} +1 -2
- package/templates/claude-global/rules/dwy-tdd-development.md +70 -0
- package/templates/claude-global/rules/dwy-tech-stack.md +60 -0
- package/templates/claude-global/rules/dwy-vue-api.md +146 -0
- package/templates/claude-global/rules/{vue-code-style.md → dwy-vue-core.md} +338 -152
- package/templates/claude-global/rules/dwy-vue-pinia.md +75 -0
- package/templates/claude-global/rules/dwy-vue-testing.md +102 -0
- package/templates/claude-global/skills/dwy-deploy-audit/SKILL.md +481 -0
- package/templates/claude-global/skills/dwy-deploy-audit/references/checks-data.md +117 -0
- package/templates/claude-global/skills/dwy-deploy-audit/references/checks-network.md +74 -0
- package/templates/claude-global/skills/dwy-deploy-audit/references/checks-runtime.md +184 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_base.sh +39 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_capacity.sh +278 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_db.sh +122 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_docker.sh +249 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_env.sh +64 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_https.sh +60 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_logs.sh +209 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_nginx.sh +92 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_resilience.sh +157 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_secrets.sh +327 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_services.sh +77 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_ssh.sh +56 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/run_all.sh +105 -0
- package/templates/claude-global/skills/dwy-docker/SKILL.md +187 -0
- package/templates/claude-global/skills/dwy-docker/references/ask-templates.md +142 -0
- package/templates/claude-global/skills/dwy-docker/references/templates.md +479 -0
- package/templates/claude-global/skills/dwy-docker/references/version-rules.md +76 -0
- package/templates/claude-global/skills/dwy-docker/scripts/query_dockerhub.py +204 -0
- package/templates/claude-global/skills/dwy-docker/scripts/scan_images.sh +185 -0
- package/templates/claude-global/skills/dwy-eapi/SKILL.md +433 -0
- package/templates/claude-global/skills/dwy-eapi/references/tasks-integration-guide.md +387 -0
- package/templates/claude-global/skills/dwy-ekit/SKILL.md +85 -0
- package/templates/claude-global/skills/dwy-ekit/references/data.md +135 -0
- package/templates/claude-global/skills/dwy-ekit/references/form.md +68 -0
- package/templates/claude-global/skills/dwy-ekit/references/request.md +183 -0
- package/templates/claude-global/skills/dwy-ekit/references/ui.md +109 -0
- package/templates/claude-global/skills/{dwy-frontend-eui → dwy-eui}/SKILL.md +129 -15
- package/templates/claude-global/skills/dwy-eui/references/eui-design-guide.md +257 -0
- package/templates/claude-global/skills/dwy-eui/references/eui-integration-guide.md +302 -0
- package/templates/claude-global/skills/dwy-eui/references/eui-landing-design-guide.md +437 -0
- package/templates/claude-global/skills/dwy-fullstack-scaffold/SKILL.md +214 -0
- package/templates/claude-global/skills/dwy-mirror-source/SKILL.md +275 -0
- package/templates/claude-global/skills/dwy-mirror-source/references/dockerfile-snippets.md +228 -0
- package/templates/claude-global/skills/dwy-mirror-source/references/mirror-providers.md +150 -0
- package/templates/claude-global/skills/dwy-mirror-source/scripts/__pycache__/check_mirrors.cpython-313.pyc +0 -0
- package/templates/claude-global/skills/dwy-mirror-source/scripts/apply_mirrors.py +666 -0
- package/templates/claude-global/skills/dwy-mirror-source/scripts/check_mirrors.py +868 -0
- package/templates/claude-global/skills/dwy-pentest/SKILL.md +344 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__init__.py +1 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/__init__.cpython-311.pyc +0 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/config.cpython-311.pyc +0 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/executor.cpython-311.pyc +0 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/severity.cpython-311.pyc +0 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/validator.cpython-311.pyc +0 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/config.py +109 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/executor.py +157 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/severity.py +96 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/validator.py +113 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_auth.py +289 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_recon.py +217 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_report.py +343 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_scan.py +205 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_ssl.py +208 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_vuln.py +205 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_web.py +330 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pyproject.toml +14 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/setup_tools.sh +97 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/uv.lock +415 -0
- package/templates/claude-global/skills/{dwy-release → dwy-publish}/SKILL.md +42 -18
- package/templates/claude-global/skills/dwy-vue-core/SKILL.md +890 -0
- package/templates/project/backend/.dwy.hbs +5 -0
- package/templates/project/backend/.python-version +1 -0
- package/templates/project/backend/CLAUDE.md.hbs +40 -0
- package/templates/project/{web/backend → backend}/alembic/script.py.mako +2 -0
- package/templates/project/{web/backend → backend}/alembic.ini.hbs +0 -1
- package/templates/project/backend/deploy.sh.hbs +18 -0
- package/templates/project/{web → backend}/docker-compose.prod.yml.hbs +1 -1
- package/templates/project/{web/backend → backend}/pyproject.toml.hbs +4 -5
- package/templates/project/{web/backend → backend}/src/app/config.py.hbs +1 -1
- package/templates/project/backend/src/app/database.py +10 -0
- package/templates/project/backend/src/app/dependencies.py +5 -0
- package/templates/project/{web/backend → backend}/src/app/main.py.hbs +3 -3
- package/templates/project/backend/src/app/models/base.py +5 -0
- package/templates/project/web/CLAUDE.md.hbs +29 -36
- package/templates/project/web/{frontend → apps/web}/index.html.hbs +1 -0
- package/templates/project/web/{frontend → apps/web}/package.json.hbs +4 -3
- package/templates/project/web/apps/web/src/App.vue +9 -0
- package/templates/project/web/apps/web/src/api/client.ts +5 -0
- package/templates/project/web/apps/web/src/composables/.gitkeep +0 -0
- package/templates/project/web/apps/web/src/layouts/DefaultLayout.vue +8 -0
- package/templates/project/web/{frontend → apps/web}/src/router/index.ts +1 -1
- package/templates/project/web/{frontend → apps/web}/src/stores/auth.ts +4 -2
- package/templates/project/web/apps/web/src/style.css +2 -0
- package/templates/project/web/apps/web/src/types/router.d.ts +8 -0
- package/templates/project/web/apps/web/src/views/HomeView.vue +8 -0
- package/templates/project/web/apps/web/tsconfig.app.json +18 -0
- package/templates/project/web/apps/web/tsconfig.json +6 -0
- package/templates/project/web/{frontend → apps/web}/vite.config.ts.hbs +7 -1
- package/templates/project/web/package.json.hbs +16 -0
- package/templates/project/web/packages/.gitkeep +0 -0
- package/templates/project/web/pnpm-workspace.yaml +3 -0
- package/templates/claude-global/hooks/post-sync-claude-global.sh +0 -72
- package/templates/claude-global/rules/agent-team-execution.md +0 -138
- package/templates/claude-global/rules/agent-team-patterns.md +0 -157
- package/templates/claude-global/rules/git-commit-convention.md +0 -106
- package/templates/claude-global/rules/git-security.md +0 -144
- package/templates/claude-global/skills/dwy-backend-base/SKILL.md +0 -279
- package/templates/claude-global/skills/dwy-frontend-core/SKILL.md +0 -181
- package/templates/project/web/.claude/commands/deploy.md +0 -4
- package/templates/project/web/backend/src/app/models/base.py +0 -5
- package/templates/project/web/deploy.sh.hbs +0 -113
- package/templates/project/web/frontend/src/App.vue +0 -3
- package/templates/project/web/frontend/src/api/client.ts +0 -13
- package/templates/project/web/frontend/src/style.css +0 -1
- package/templates/project/web/frontend/src/views/HomeView.vue +0 -5
- package/templates/project/web/frontend/tsconfig.app.json +0 -14
- package/templates/project/web/frontend/tsconfig.json +0 -4
- /package/templates/{project/web/backend → claude-global/skills/dwy-pentest/scripts}/.python-version +0 -0
- /package/templates/project/{web/backend → backend}/Dockerfile +0 -0
- /package/templates/project/{web/backend → backend}/alembic/env.py.hbs +0 -0
- /package/templates/{claude-global/skills → project/backend/alembic/versions}/.gitkeep +0 -0
- /package/templates/project/{web → backend}/docker-compose.dev.yml.hbs +0 -0
- /package/templates/project/{web/backend → backend}/src/app/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/src/app/models/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/src/app/routers/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/src/app/routers/health.py +0 -0
- /package/templates/project/{web/backend → backend}/src/app/schemas/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/src/app/services/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/tests/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/tests/conftest.py.hbs +0 -0
- /package/templates/project/{web/backend/alembic/versions → mobile}/.gitkeep +0 -0
- /package/templates/project/web/{frontend → apps/web}/src/components/.gitkeep +0 -0
- /package/templates/project/web/{frontend → apps/web}/src/main.ts +0 -0
|
@@ -1,18 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
description: Python + FastAPI 代码规范
|
|
4
|
-
type: claude-rule
|
|
5
|
-
tags: [python, fastapi, code-style]
|
|
2
|
+
description: Python + FastAPI 基础风格(命名/类型/异步/配置/日志/错误处理等)
|
|
6
3
|
paths:
|
|
7
4
|
- "**/*.py"
|
|
8
5
|
- "**/pyproject.toml"
|
|
9
|
-
version: 1.0.0
|
|
10
|
-
author: chances
|
|
11
6
|
---
|
|
12
7
|
|
|
13
|
-
# Python + FastAPI
|
|
14
|
-
|
|
15
|
-
所有 Python / FastAPI 项目必须遵循以下规范。AI 生成或修改代码时,必须严格按照这些规则执行。
|
|
8
|
+
# Python + FastAPI 基础风格
|
|
16
9
|
|
|
17
10
|
## 一、项目结构
|
|
18
11
|
|
|
@@ -133,6 +126,33 @@ known-first-party = ["app"]
|
|
|
133
126
|
| 路由函数 | `snake_case` 动词开头 | `create_user()`、`list_orders()` |
|
|
134
127
|
| Pydantic Schema | `PascalCase` + 用途后缀 | `UserCreate`、`UserResponse`、`UserUpdate` |
|
|
135
128
|
|
|
129
|
+
### 禁止魔法字符串
|
|
130
|
+
|
|
131
|
+
同一个字符串字面量在文件内出现 2 次及以上时,**必须**提取为常量。
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
# ❌ 魔法字符串散落多处
|
|
135
|
+
redis.set("user:token:123", token)
|
|
136
|
+
cached = redis.get("user:token:123")
|
|
137
|
+
|
|
138
|
+
# ✅ 提取为常量
|
|
139
|
+
USER_TOKEN_KEY = "user:token:{user_id}"
|
|
140
|
+
redis.set(USER_TOKEN_KEY.format(user_id=123), token)
|
|
141
|
+
cached = redis.get(USER_TOKEN_KEY.format(user_id=123))
|
|
142
|
+
|
|
143
|
+
# ❌ cache key、队列名、header 名等重复字面量
|
|
144
|
+
request.headers.get("X-Request-Id")
|
|
145
|
+
response.headers["X-Request-Id"] = rid
|
|
146
|
+
|
|
147
|
+
# ✅ 跨模块共享的 key 放到常量模块
|
|
148
|
+
# constants.py
|
|
149
|
+
HEADER_REQUEST_ID = "X-Request-Id"
|
|
150
|
+
CACHE_PREFIX_USER = "user:"
|
|
151
|
+
TASK_QUEUE_DEFAULT = "default"
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**判断标准:** 同一字符串在同一文件出现 ≥ 2 次 → 提取为常量。跨模块使用的 key → 提取到 `constants.py` 共享。
|
|
155
|
+
|
|
136
156
|
### 禁止的写法
|
|
137
157
|
|
|
138
158
|
```python
|
|
@@ -189,277 +209,6 @@ class UserProfile:
|
|
|
189
209
|
- 不得使用 `Any` 作为"懒标注",除非与外部动态库交互且无法确定类型
|
|
190
210
|
- 使用 `Any` 时必须附注释说明原因
|
|
191
211
|
|
|
192
|
-
## 五、FastAPI 路由与接口设计
|
|
193
|
-
|
|
194
|
-
### 路由组织
|
|
195
|
-
|
|
196
|
-
```python
|
|
197
|
-
# routers/user.py
|
|
198
|
-
from fastapi import APIRouter, Depends, status
|
|
199
|
-
|
|
200
|
-
router = APIRouter(prefix="/users", tags=["users"])
|
|
201
|
-
|
|
202
|
-
@router.post("/", status_code=status.HTTP_201_CREATED, response_model=UserResponse)
|
|
203
|
-
async def create_user(
|
|
204
|
-
body: UserCreate,
|
|
205
|
-
service: UserService = Depends(get_user_service),
|
|
206
|
-
) -> UserResponse:
|
|
207
|
-
return await service.create(body)
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
### 强制规则
|
|
211
|
-
- 每个路由模块使用独立 `APIRouter`,在 `main.py` 中统一注册
|
|
212
|
-
- **禁止**所有路由写在 `main.py` 中(超过 2 个路由必须拆分)
|
|
213
|
-
- 路由函数只做:参数接收 → 调用 service → 返回结果,**不写业务逻辑**
|
|
214
|
-
- 必须显式声明 `status_code` 和 `response_model`
|
|
215
|
-
- 路由路径使用复数名词 + RESTful 风格
|
|
216
|
-
|
|
217
|
-
### RESTful 路由规范
|
|
218
|
-
|
|
219
|
-
| 操作 | 方法 | 路径 | 状态码 |
|
|
220
|
-
|---|---|---|---|
|
|
221
|
-
| 列表查询 | `GET` | `/users` | 200 |
|
|
222
|
-
| 详情查询 | `GET` | `/users/{user_id}` | 200 |
|
|
223
|
-
| 创建 | `POST` | `/users` | 201 |
|
|
224
|
-
| 全量更新 | `PUT` | `/users/{user_id}` | 200 |
|
|
225
|
-
| 部分更新 | `PATCH` | `/users/{user_id}` | 200 |
|
|
226
|
-
| 删除 | `DELETE` | `/users/{user_id}` | 204 |
|
|
227
|
-
|
|
228
|
-
### 分页查询
|
|
229
|
-
|
|
230
|
-
```python
|
|
231
|
-
# schemas/common.py
|
|
232
|
-
class PaginationParams(BaseModel):
|
|
233
|
-
page: int = Field(default=1, ge=1)
|
|
234
|
-
page_size: int = Field(default=20, ge=1, le=100)
|
|
235
|
-
|
|
236
|
-
class PaginatedResponse[T](BaseModel):
|
|
237
|
-
items: list[T]
|
|
238
|
-
total: int
|
|
239
|
-
page: int
|
|
240
|
-
page_size: int
|
|
241
|
-
|
|
242
|
-
# routers/user.py
|
|
243
|
-
@router.get("/", response_model=PaginatedResponse[UserResponse])
|
|
244
|
-
async def list_users(
|
|
245
|
-
pagination: PaginationParams = Depends(),
|
|
246
|
-
service: UserService = Depends(get_user_service),
|
|
247
|
-
) -> PaginatedResponse[UserResponse]:
|
|
248
|
-
return await service.list(pagination)
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
## 六、Pydantic Schema 规范
|
|
252
|
-
|
|
253
|
-
### 强制规则
|
|
254
|
-
- 请求和响应使用不同的 Schema,**禁止**复用同一个 Model
|
|
255
|
-
|
|
256
|
-
```python
|
|
257
|
-
# schemas/user.py
|
|
258
|
-
|
|
259
|
-
class UserBase(BaseModel):
|
|
260
|
-
"""共享字段基类"""
|
|
261
|
-
name: str = Field(max_length=50)
|
|
262
|
-
email: EmailStr
|
|
263
|
-
|
|
264
|
-
class UserCreate(UserBase):
|
|
265
|
-
"""创建请求"""
|
|
266
|
-
password: str = Field(min_length=8)
|
|
267
|
-
|
|
268
|
-
class UserUpdate(BaseModel):
|
|
269
|
-
"""更新请求 - 所有字段可选"""
|
|
270
|
-
name: str | None = Field(default=None, max_length=50)
|
|
271
|
-
email: EmailStr | None = None
|
|
272
|
-
|
|
273
|
-
class UserResponse(UserBase):
|
|
274
|
-
"""响应"""
|
|
275
|
-
id: int
|
|
276
|
-
is_active: bool
|
|
277
|
-
created_at: datetime
|
|
278
|
-
|
|
279
|
-
model_config = ConfigDict(from_attributes=True)
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
- **禁止**在响应 Schema 中暴露密码、内部 ID、敏感字段
|
|
283
|
-
- 使用 `Field()` 添加校验约束(`min_length`、`ge`、`le`、`pattern` 等)
|
|
284
|
-
- 更新 Schema 的字段必须全部可选(`None` 默认值)
|
|
285
|
-
|
|
286
|
-
## 七、依赖注入
|
|
287
|
-
|
|
288
|
-
### 强制规则
|
|
289
|
-
- 数据库会话、当前用户、配置等通过 `Depends()` 注入
|
|
290
|
-
- **禁止**在路由函数中直接创建数据库会话或实例化 service
|
|
291
|
-
|
|
292
|
-
```python
|
|
293
|
-
# dependencies.py
|
|
294
|
-
from collections.abc import AsyncGenerator
|
|
295
|
-
|
|
296
|
-
async def get_db() -> AsyncGenerator[AsyncSession]:
|
|
297
|
-
async with async_session_factory() as session:
|
|
298
|
-
yield session
|
|
299
|
-
|
|
300
|
-
async def get_current_user(
|
|
301
|
-
token: str = Depends(oauth2_scheme),
|
|
302
|
-
db: AsyncSession = Depends(get_db),
|
|
303
|
-
) -> User:
|
|
304
|
-
user = await authenticate(token, db)
|
|
305
|
-
if not user:
|
|
306
|
-
raise HTTPException(status_code=401, detail="认证失败")
|
|
307
|
-
return user
|
|
308
|
-
|
|
309
|
-
def get_user_service(db: AsyncSession = Depends(get_db)) -> UserService:
|
|
310
|
-
return UserService(db)
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
```python
|
|
314
|
-
# ❌ 路由中直接创建会话
|
|
315
|
-
@router.get("/users/{user_id}")
|
|
316
|
-
async def get_user(user_id: int):
|
|
317
|
-
async with async_session_factory() as db:
|
|
318
|
-
user = await db.get(User, user_id)
|
|
319
|
-
return user
|
|
320
|
-
|
|
321
|
-
# ✅ 依赖注入
|
|
322
|
-
@router.get("/users/{user_id}", response_model=UserResponse)
|
|
323
|
-
async def get_user(
|
|
324
|
-
user_id: int,
|
|
325
|
-
service: UserService = Depends(get_user_service),
|
|
326
|
-
) -> UserResponse:
|
|
327
|
-
return await service.get_by_id(user_id)
|
|
328
|
-
```
|
|
329
|
-
|
|
330
|
-
## 八、异常处理
|
|
331
|
-
|
|
332
|
-
### 统一异常体系
|
|
333
|
-
|
|
334
|
-
```python
|
|
335
|
-
# exceptions.py
|
|
336
|
-
from fastapi import FastAPI, Request, status
|
|
337
|
-
from fastapi.responses import JSONResponse
|
|
338
|
-
|
|
339
|
-
# 业务异常基类
|
|
340
|
-
class AppError(Exception):
|
|
341
|
-
def __init__(self, message: str, code: str = "UNKNOWN_ERROR") -> None:
|
|
342
|
-
self.message = message
|
|
343
|
-
self.code = code
|
|
344
|
-
|
|
345
|
-
class NotFoundError(AppError):
|
|
346
|
-
def __init__(self, resource: str, resource_id: int | str) -> None:
|
|
347
|
-
super().__init__(
|
|
348
|
-
message=f"{resource} 不存在",
|
|
349
|
-
code="NOT_FOUND",
|
|
350
|
-
)
|
|
351
|
-
self.resource = resource
|
|
352
|
-
self.resource_id = resource_id
|
|
353
|
-
|
|
354
|
-
class BusinessError(AppError):
|
|
355
|
-
"""业务规则校验失败"""
|
|
356
|
-
pass
|
|
357
|
-
|
|
358
|
-
class PermissionDeniedError(AppError):
|
|
359
|
-
"""权限不足"""
|
|
360
|
-
pass
|
|
361
|
-
|
|
362
|
-
# 注册异常处理器
|
|
363
|
-
def register_exception_handlers(app: FastAPI) -> None:
|
|
364
|
-
@app.exception_handler(NotFoundError)
|
|
365
|
-
async def not_found_handler(request: Request, exc: NotFoundError) -> JSONResponse:
|
|
366
|
-
return JSONResponse(
|
|
367
|
-
status_code=status.HTTP_404_NOT_FOUND,
|
|
368
|
-
content={"code": exc.code, "message": exc.message},
|
|
369
|
-
)
|
|
370
|
-
|
|
371
|
-
@app.exception_handler(BusinessError)
|
|
372
|
-
async def business_error_handler(request: Request, exc: BusinessError) -> JSONResponse:
|
|
373
|
-
return JSONResponse(
|
|
374
|
-
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
375
|
-
content={"code": exc.code, "message": exc.message},
|
|
376
|
-
)
|
|
377
|
-
|
|
378
|
-
@app.exception_handler(PermissionDeniedError)
|
|
379
|
-
async def permission_denied_handler(request: Request, exc: PermissionDeniedError) -> JSONResponse:
|
|
380
|
-
return JSONResponse(
|
|
381
|
-
status_code=status.HTTP_403_FORBIDDEN,
|
|
382
|
-
content={"code": exc.code, "message": exc.message},
|
|
383
|
-
)
|
|
384
|
-
```
|
|
385
|
-
|
|
386
|
-
### 强制规则
|
|
387
|
-
- **禁止**在 service 层抛出 `HTTPException`(service 不感知 HTTP)
|
|
388
|
-
- service 层抛出业务异常,由异常处理器统一转换为 HTTP 响应
|
|
389
|
-
- **禁止**裸 `except:` 或 `except Exception: pass`
|
|
390
|
-
- 错误响应格式统一为 `{"code": "ERROR_CODE", "message": "描述"}`
|
|
391
|
-
- **禁止**在错误消息中回显用户输入
|
|
392
|
-
|
|
393
|
-
```python
|
|
394
|
-
# ❌ service 中抛 HTTPException
|
|
395
|
-
class UserService:
|
|
396
|
-
async def get_by_id(self, user_id: int) -> User:
|
|
397
|
-
user = await self.db.get(User, user_id)
|
|
398
|
-
if not user:
|
|
399
|
-
raise HTTPException(status_code=404) # 耦合 HTTP
|
|
400
|
-
return user
|
|
401
|
-
|
|
402
|
-
# ✅ service 中抛业务异常
|
|
403
|
-
class UserService:
|
|
404
|
-
async def get_by_id(self, user_id: int) -> User:
|
|
405
|
-
user = await self.db.get(User, user_id)
|
|
406
|
-
if not user:
|
|
407
|
-
raise NotFoundError("用户", user_id)
|
|
408
|
-
return user
|
|
409
|
-
```
|
|
410
|
-
|
|
411
|
-
## 九、数据库与 ORM
|
|
412
|
-
|
|
413
|
-
### 强制规则
|
|
414
|
-
- 使用 **SQLAlchemy 2.0** 风格(声明式映射 + async)
|
|
415
|
-
- 数据库迁移使用 **Alembic**,**禁止**手动执行 DDL
|
|
416
|
-
- **禁止**在路由层直接写 SQL 查询
|
|
417
|
-
|
|
418
|
-
```python
|
|
419
|
-
# models/base.py
|
|
420
|
-
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
|
421
|
-
from sqlalchemy import func
|
|
422
|
-
from datetime import datetime
|
|
423
|
-
|
|
424
|
-
class Base(DeclarativeBase):
|
|
425
|
-
pass
|
|
426
|
-
|
|
427
|
-
class TimestampMixin:
|
|
428
|
-
created_at: Mapped[datetime] = mapped_column(server_default=func.now())
|
|
429
|
-
updated_at: Mapped[datetime] = mapped_column(server_default=func.now(), onupdate=func.now())
|
|
430
|
-
|
|
431
|
-
# models/user.py
|
|
432
|
-
class User(Base, TimestampMixin):
|
|
433
|
-
__tablename__ = "users"
|
|
434
|
-
|
|
435
|
-
id: Mapped[int] = mapped_column(primary_key=True)
|
|
436
|
-
name: Mapped[str] = mapped_column(String(50))
|
|
437
|
-
email: Mapped[str] = mapped_column(String(100), unique=True, index=True)
|
|
438
|
-
hashed_password: Mapped[str] = mapped_column(String(128))
|
|
439
|
-
is_active: Mapped[bool] = mapped_column(default=True)
|
|
440
|
-
```
|
|
441
|
-
|
|
442
|
-
### 异步数据库会话
|
|
443
|
-
|
|
444
|
-
```python
|
|
445
|
-
# config.py / database.py
|
|
446
|
-
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
|
|
447
|
-
|
|
448
|
-
engine = create_async_engine(settings.database_url, echo=settings.debug)
|
|
449
|
-
async_session_factory = async_sessionmaker(engine, expire_on_commit=False)
|
|
450
|
-
```
|
|
451
|
-
|
|
452
|
-
### 查询规范
|
|
453
|
-
|
|
454
|
-
```python
|
|
455
|
-
# ❌ 旧式查询
|
|
456
|
-
session.query(User).filter(User.id == user_id).first()
|
|
457
|
-
|
|
458
|
-
# ✅ SQLAlchemy 2.0 风格
|
|
459
|
-
result = await session.execute(select(User).where(User.id == user_id))
|
|
460
|
-
user = result.scalar_one_or_none()
|
|
461
|
-
```
|
|
462
|
-
|
|
463
212
|
## 十、异步编程
|
|
464
213
|
|
|
465
214
|
### 强制规则
|
|
@@ -527,38 +276,88 @@ app.add_middleware(
|
|
|
527
276
|
|
|
528
277
|
### 强制规则
|
|
529
278
|
- 使用 Pydantic Settings 管理配置
|
|
279
|
+
- **必须使用嵌套模型分组**,禁止所有字段平铺在一个类中
|
|
530
280
|
|
|
531
281
|
```python
|
|
532
282
|
# config.py
|
|
283
|
+
from pydantic import BaseModel
|
|
533
284
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
534
285
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
286
|
+
# 按领域拆分为独立的配置类(普通 BaseModel,不是 BaseSettings)
|
|
287
|
+
class OssConfig(BaseModel):
|
|
288
|
+
endpoint: str = ""
|
|
289
|
+
access_key: str = ""
|
|
290
|
+
secret_key: str = ""
|
|
291
|
+
bucket: str = ""
|
|
539
292
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
293
|
+
class AliyunSmsConfig(BaseModel):
|
|
294
|
+
access_key_id: str = ""
|
|
295
|
+
access_key_secret: str = ""
|
|
296
|
+
sign_name: str = ""
|
|
297
|
+
template_code: str = ""
|
|
543
298
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
#
|
|
548
|
-
secret_key: str
|
|
549
|
-
access_token_expire_minutes: int = 30
|
|
299
|
+
# 主 Settings 类组合嵌套模型
|
|
300
|
+
class Settings(BaseSettings):
|
|
301
|
+
# eapi 内置字段(继承 BaseSettings 时自动包含)
|
|
302
|
+
# database_url, redis_url, secret_key, ...
|
|
550
303
|
|
|
551
|
-
#
|
|
552
|
-
|
|
304
|
+
# 项目专属 — 嵌套分组
|
|
305
|
+
app_name: str = "My App"
|
|
306
|
+
oss: OssConfig = OssConfig()
|
|
307
|
+
aliyun_sms: AliyunSmsConfig = AliyunSmsConfig()
|
|
553
308
|
|
|
554
309
|
model_config = SettingsConfigDict(
|
|
555
310
|
env_file=".env",
|
|
556
311
|
env_file_encoding="utf-8",
|
|
312
|
+
env_nested_delimiter="__", # 关键:启用嵌套分隔符
|
|
557
313
|
)
|
|
558
314
|
|
|
559
315
|
settings = Settings()
|
|
560
316
|
```
|
|
561
317
|
|
|
318
|
+
**`.env` 文件中用 `__` 分隔层级:**
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
# 基础字段(eapi 内置)
|
|
322
|
+
DATABASE_URL=postgresql+asyncpg://user:pass@localhost:5432/mydb
|
|
323
|
+
REDIS_URL=redis://localhost:6379/0
|
|
324
|
+
SECRET_KEY=your-secret-key
|
|
325
|
+
|
|
326
|
+
# 嵌套字段 — 用双下划线 __ 分隔
|
|
327
|
+
OSS__ENDPOINT=http://localhost:9000
|
|
328
|
+
OSS__ACCESS_KEY=minioadmin
|
|
329
|
+
OSS__SECRET_KEY=minioadmin
|
|
330
|
+
OSS__BUCKET=my-app
|
|
331
|
+
|
|
332
|
+
ALIYUN_SMS__ACCESS_KEY_ID=AKID123
|
|
333
|
+
ALIYUN_SMS__ACCESS_KEY_SECRET=secret456
|
|
334
|
+
ALIYUN_SMS__SIGN_NAME=我的应用
|
|
335
|
+
ALIYUN_SMS__TEMPLATE_CODE=SMS_123456
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
**代码中通过属性链访问:**
|
|
339
|
+
|
|
340
|
+
```python
|
|
341
|
+
# ✅ 分组清晰
|
|
342
|
+
settings.oss.endpoint
|
|
343
|
+
settings.oss.bucket
|
|
344
|
+
settings.aliyun_sms.access_key_id
|
|
345
|
+
|
|
346
|
+
# ❌ 禁止扁平命名
|
|
347
|
+
settings.oss_endpoint
|
|
348
|
+
settings.oss_access_key
|
|
349
|
+
settings.aliyun_sms_access_key_id
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### 分组原则
|
|
353
|
+
|
|
354
|
+
| 场景 | 做法 |
|
|
355
|
+
|------|------|
|
|
356
|
+
| eapi 内置字段(database_url 等) | 保持顶层,不嵌套 |
|
|
357
|
+
| 同一服务/SDK 的多个配置 | 拆成独立 `BaseModel` 嵌套 |
|
|
358
|
+
| 只有 1 个字段 | 保持顶层,不必嵌套 |
|
|
359
|
+
| 3 个以上同前缀字段 | 必须嵌套 |
|
|
360
|
+
|
|
562
361
|
- **禁止**在代码中硬编码配置值(数据库地址、密钥、端口等)
|
|
563
362
|
- **禁止**在模块顶层执行副作用(网络请求、数据库连接、文件读取)
|
|
564
363
|
|
|
@@ -592,49 +391,6 @@ logger.info("user_created", user_id=user_id)
|
|
|
592
391
|
| `ERROR` | 错误但服务仍可用(单次请求失败) |
|
|
593
392
|
| `CRITICAL` | 服务不可用(数据库连接断开、必要服务宕机) |
|
|
594
393
|
|
|
595
|
-
## 十四、测试
|
|
596
|
-
|
|
597
|
-
### 强制规则
|
|
598
|
-
- 使用 `pytest` + `pytest-asyncio` + `httpx`
|
|
599
|
-
- 使用 `TestClient` 或 `AsyncClient` 测试 API
|
|
600
|
-
|
|
601
|
-
```python
|
|
602
|
-
# conftest.py
|
|
603
|
-
import pytest
|
|
604
|
-
from httpx import ASGITransport, AsyncClient
|
|
605
|
-
from app.main import app
|
|
606
|
-
|
|
607
|
-
@pytest.fixture
|
|
608
|
-
async def client():
|
|
609
|
-
async with AsyncClient(
|
|
610
|
-
transport=ASGITransport(app=app),
|
|
611
|
-
base_url="http://test",
|
|
612
|
-
) as ac:
|
|
613
|
-
yield ac
|
|
614
|
-
|
|
615
|
-
# test_user.py
|
|
616
|
-
@pytest.mark.anyio
|
|
617
|
-
async def test_create_user_returns_201(client: AsyncClient):
|
|
618
|
-
response = await client.post("/users/", json={
|
|
619
|
-
"name": "Alice",
|
|
620
|
-
"email": "alice@example.com",
|
|
621
|
-
"password": "securepass123",
|
|
622
|
-
})
|
|
623
|
-
assert response.status_code == 201
|
|
624
|
-
data = response.json()
|
|
625
|
-
assert data["name"] == "Alice"
|
|
626
|
-
assert "password" not in data # 响应不包含密码
|
|
627
|
-
|
|
628
|
-
@pytest.mark.anyio
|
|
629
|
-
async def test_get_nonexistent_user_returns_404(client: AsyncClient):
|
|
630
|
-
response = await client.get("/users/99999")
|
|
631
|
-
assert response.status_code == 404
|
|
632
|
-
```
|
|
633
|
-
|
|
634
|
-
- 测试函数名描述行为而非实现
|
|
635
|
-
- 测试数据使用 `pytest.fixture`,不要硬编码
|
|
636
|
-
- 测试结构遵循 Arrange-Act-Assert
|
|
637
|
-
|
|
638
394
|
## 十五、函数与类设计
|
|
639
395
|
|
|
640
396
|
### 强制规则
|
|
@@ -845,6 +601,101 @@ user = cache.get_user(user_id)
|
|
|
845
601
|
async def create_user(body: UserCreate) -> UserResponse:
|
|
846
602
|
"""根据提交的信息创建新用户,邮箱不可重复。"""
|
|
847
603
|
...
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
## 二十、dwyeapi 优先使用
|
|
607
|
+
|
|
608
|
+
### 适用条件
|
|
609
|
+
|
|
610
|
+
项目依赖中包含 `dwyeapi` 时,以下规则生效。
|
|
611
|
+
|
|
612
|
+
### 强制规则
|
|
613
|
+
|
|
614
|
+
编写 FastAPI 后端代码时,**必须优先使用 eapi 已有的类和函数**,禁止重复实现。
|
|
615
|
+
|
|
616
|
+
| 需求 | 使用 eapi | 禁止自建 |
|
|
617
|
+
|------|----------|---------|
|
|
618
|
+
| 配置管理 | `dwyeapi.config.BaseSettings` | 自写 Pydantic Settings 基类 |
|
|
619
|
+
| ORM 基类 | `dwyeapi.database.Base` + `TimestampMixin` | 自定义 DeclarativeBase |
|
|
620
|
+
| 异步引擎 | `create_async_engine_factory()` + `create_session_factory()` | 手动创建 engine/session |
|
|
621
|
+
| 依赖注入 | `create_get_db(session_factory)` | 手写 get_db 生成器 |
|
|
622
|
+
| 密码哈希 | `hash_password()` / `verify_password()` | 直接调用 bcrypt |
|
|
623
|
+
| JWT | `create_token()` / `decode_token()` | 直接调用 python-jose |
|
|
624
|
+
| 异常体系 | `NotFoundError` / `BusinessError` / `register_exception_handlers()` | 自定义异常 + 手写 handler |
|
|
625
|
+
| 响应格式 | `success()` / `fail()` / `paginated()` | 自定义响应包装函数 |
|
|
626
|
+
| 分页 | `PaginationParams` + `paginate()` | 自写分页参数类 |
|
|
627
|
+
| Redis 缓存 | `cache.configure()` / `cache.get_redis()` | 手动管理 Redis 连接 |
|
|
628
|
+
| 异步任务 | `tasks.setup_tasks()` / `@register` / `TaskContext` (需 `[tasks]` extra) | 手写 ARQ/Celery 集成 |
|
|
629
|
+
|
|
630
|
+
### 决策流程
|
|
631
|
+
|
|
632
|
+
```
|
|
633
|
+
需要某项基础设施功能
|
|
634
|
+
→ 1. 检查 eapi 是否已提供(查阅 dwy-eapi skill)
|
|
635
|
+
→ 已提供:直接使用,不重复实现
|
|
636
|
+
→ 未提供:自行实现,代码注释中说明 eapi 不覆盖此功能
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
## 二十一、代码简约原则
|
|
640
|
+
|
|
641
|
+
### 核心思想
|
|
642
|
+
|
|
643
|
+
代码只写**必要的逻辑**,不写"以防万一"的冗余代码。信任内部代码和框架保证,只在系统边界(用户输入、外部 API)做校验。
|
|
644
|
+
|
|
645
|
+
### 禁止的冗余模式
|
|
646
|
+
|
|
647
|
+
```python
|
|
648
|
+
# ❌ 不必要的 fallback — 值已经有明确来源
|
|
649
|
+
name = user.name or "" # user.name 不可能是 None
|
|
650
|
+
result = data.get("key", None) # get 默认就返回 None
|
|
651
|
+
|
|
652
|
+
# ✅ 直接使用
|
|
653
|
+
name = user.name
|
|
654
|
+
result = data.get("key")
|
|
655
|
+
|
|
656
|
+
# ❌ 不必要的类型转换 — 值已经是目标类型
|
|
657
|
+
count = int(params.page) # Pydantic 已校验为 int
|
|
658
|
+
text = str(title) # title 声明为 str
|
|
659
|
+
|
|
660
|
+
# ✅ 直接使用
|
|
661
|
+
count = params.page
|
|
662
|
+
text = title
|
|
663
|
+
|
|
664
|
+
# ❌ 不必要的条件检查 — 逻辑上不可能走到 else
|
|
665
|
+
if task:
|
|
666
|
+
return task
|
|
667
|
+
else:
|
|
668
|
+
return None # 上面已经 return 了
|
|
669
|
+
|
|
670
|
+
# ✅ 直接返回
|
|
671
|
+
return task
|
|
672
|
+
|
|
673
|
+
# ❌ 不必要的默认值 — 调用方已保证传值
|
|
674
|
+
def process(data: dict = None): # 所有调用方都传了 data
|
|
675
|
+
if data is None:
|
|
676
|
+
data = {}
|
|
677
|
+
|
|
678
|
+
# ✅ 去掉默认值和检查
|
|
679
|
+
def process(data: dict):
|
|
680
|
+
...
|
|
681
|
+
|
|
682
|
+
# ❌ 不必要的异常兜底 — 掩盖真正的 bug
|
|
683
|
+
try:
|
|
684
|
+
result = calculate(x)
|
|
685
|
+
except Exception:
|
|
686
|
+
result = 0 # 吞掉异常,bug 永远不会被发现
|
|
687
|
+
|
|
688
|
+
# ✅ 让异常暴露,或只捕获预期的异常
|
|
689
|
+
result = calculate(x)
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
### 判断标准
|
|
693
|
+
|
|
694
|
+
写每一行防御代码前问自己:**这个情况在当前上下文下真的会发生吗?**
|
|
695
|
+
|
|
696
|
+
- 如果**会** → 写防御,加注释说明什么情况下触发
|
|
697
|
+
- 如果**不会** → 不写,信任上游保证
|
|
698
|
+
- 如果**不确定** → 查看调用链确认,不要"以防万一"
|
|
848
699
|
|
|
849
700
|
## 代码自检(写代码时强制执行)
|
|
850
701
|
|
|
@@ -866,36 +717,119 @@ async def create_user(body: UserCreate) -> UserResponse:
|
|
|
866
717
|
| 12 | 嵌套不超过 3 层,函数体不超过 50 行 | ✓ |
|
|
867
718
|
| 13 | 所有模块/类/函数有 docstring(Google 风格) | ✓ |
|
|
868
719
|
| 14 | 依赖通过 `Depends()` 注入,不在路由中直接创建 | ✓ |
|
|
720
|
+
| 15 | 项目依赖 eapi 时,未重复实现 eapi 已有功能 | ✓ |
|
|
869
721
|
|
|
870
722
|
**不执行自检就提交代码 = 违规。**
|
|
723
|
+
|
|
724
|
+
---
|
|
725
|
+
|
|
726
|
+
## 错误处理规范(后端)
|
|
727
|
+
|
|
728
|
+
**唯一方式:** 使用 eapi exceptions 体系。
|
|
729
|
+
|
|
730
|
+
```python
|
|
731
|
+
from dwyeapi.exceptions import (
|
|
732
|
+
NotFoundError, # 404 — 资源不存在
|
|
733
|
+
BusinessError, # 422 — 业务规则不允许
|
|
734
|
+
PermissionDeniedError, # 403 — 无权限
|
|
735
|
+
AuthenticationError, # 401 — 认证失败
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
# services 层
|
|
739
|
+
async def get_user(db: AsyncSession, user_id: int) -> User:
|
|
740
|
+
user = await db.get(User, user_id)
|
|
741
|
+
if user is None:
|
|
742
|
+
raise NotFoundError("用户") # → {"code": "NOT_FOUND", "message": "用户不存在"}
|
|
743
|
+
return user
|
|
744
|
+
|
|
745
|
+
async def create_order(db: AsyncSession, data: OrderCreate) -> Order:
|
|
746
|
+
if data.amount <= 0:
|
|
747
|
+
raise BusinessError("订单金额必须大于 0", code="INVALID_AMOUNT")
|
|
748
|
+
...
|
|
871
749
|
```
|
|
872
750
|
|
|
873
|
-
|
|
751
|
+
**禁止清单:**
|
|
752
|
+
| 禁止写法 | 正确写法 |
|
|
753
|
+
|---------|---------|
|
|
754
|
+
| `raise HTTPException(status_code=404)` | `raise NotFoundError("资源名")` |
|
|
755
|
+
| `try: ... except Exception: pass` | 只捕获预期的具体异常 |
|
|
756
|
+
| 自定义异常类不继承 AppError | 项目异常必须继承 `AppError` |
|
|
757
|
+
| 在 router 中 try/except 包 service 调用 | 让异常自然冒泡到 handler |
|
|
874
758
|
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
- 密码存储必须使用 `bcrypt` / `argon2`,**禁止**明文或 MD5/SHA
|
|
881
|
-
- JWT 密钥从环境变量读取,**禁止**硬编码
|
|
759
|
+
---
|
|
760
|
+
|
|
761
|
+
## 环境变量管理(后端:eapi BaseSettings + 嵌套分组)
|
|
762
|
+
|
|
763
|
+
**必须使用嵌套模型分组配置**,禁止所有字段平铺。同一服务/SDK 的多个字段拆成独立 `BaseModel`。
|
|
882
764
|
|
|
883
765
|
```python
|
|
884
|
-
#
|
|
885
|
-
|
|
766
|
+
# app/config.py
|
|
767
|
+
from pydantic import BaseModel
|
|
768
|
+
from dwyeapi.config import BaseSettings
|
|
886
769
|
|
|
887
|
-
|
|
888
|
-
|
|
770
|
+
class OssConfig(BaseModel):
|
|
771
|
+
endpoint: str = ""
|
|
772
|
+
access_key: str = ""
|
|
773
|
+
secret_key: str = ""
|
|
774
|
+
bucket: str = ""
|
|
889
775
|
|
|
890
|
-
|
|
891
|
-
|
|
776
|
+
class Settings(BaseSettings):
|
|
777
|
+
# eapi 内置字段保持顶层:database_url, redis_url, secret_key, ...
|
|
892
778
|
|
|
893
|
-
#
|
|
894
|
-
|
|
779
|
+
# 项目专属 — 嵌套分组
|
|
780
|
+
app_name: str = "My App"
|
|
781
|
+
oss: OssConfig = OssConfig()
|
|
782
|
+
```
|
|
895
783
|
|
|
896
|
-
|
|
897
|
-
SECRET_KEY = "my-super-secret-key"
|
|
784
|
+
**`.env` 中用 `__` 双下划线分隔层级:**
|
|
898
785
|
|
|
899
|
-
|
|
900
|
-
|
|
786
|
+
```bash
|
|
787
|
+
DATABASE_URL=postgresql+asyncpg://user:pass@localhost:5432/mydb
|
|
788
|
+
OSS__ENDPOINT=http://localhost:9000
|
|
789
|
+
OSS__ACCESS_KEY=minioadmin
|
|
790
|
+
OSS__SECRET_KEY=minioadmin
|
|
791
|
+
OSS__BUCKET=my-app
|
|
901
792
|
```
|
|
793
|
+
|
|
794
|
+
**代码中通过属性链访问:** `settings.oss.endpoint`,禁止 `settings.oss_endpoint`。
|
|
795
|
+
|
|
796
|
+
详细规则见 `dwy-python-core.md` 第十二节。
|
|
797
|
+
|
|
798
|
+
---
|
|
799
|
+
|
|
800
|
+
## 日志规范
|
|
801
|
+
|
|
802
|
+
### 后端
|
|
803
|
+
|
|
804
|
+
使用 Python 标准 `logging` 模块:
|
|
805
|
+
|
|
806
|
+
```python
|
|
807
|
+
import logging
|
|
808
|
+
|
|
809
|
+
logger = logging.getLogger(__name__)
|
|
810
|
+
|
|
811
|
+
# 正确用法
|
|
812
|
+
logger.info("用户 %s 登录成功", user_id)
|
|
813
|
+
logger.error("导出任务失败: task_id=%s, error=%s", task_id, str(e))
|
|
814
|
+
|
|
815
|
+
# 禁止
|
|
816
|
+
print("debug info") # 禁止 print
|
|
817
|
+
logger.info(f"用户 {user_id} 登录") # 禁止 f-string(无法被日志聚合)
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
### 日志级别规则
|
|
821
|
+
|
|
822
|
+
| 级别 | 用途 | 示例 |
|
|
823
|
+
|------|------|------|
|
|
824
|
+
| DEBUG | 开发调试信息,生产环境不输出 | 变量值、SQL 语句 |
|
|
825
|
+
| INFO | 关键业务流程节点 | 用户登录、任务提交、导出完成 |
|
|
826
|
+
| WARNING | 异常但可恢复的情况 | 重试成功、降级处理 |
|
|
827
|
+
| ERROR | 错误但服务仍可用(单次请求失败) | 外部 API 超时、单个任务失败 |
|
|
828
|
+
| CRITICAL | 服务不可用 | 数据库连接断开、必要服务宕机 |
|
|
829
|
+
|
|
830
|
+
### 禁止事项
|
|
831
|
+
|
|
832
|
+
- 禁止 `print()`,全部用 `logger`
|
|
833
|
+
- 禁止在循环体内打 INFO/WARNING 日志(大量重复)
|
|
834
|
+
- 禁止日志中包含密码、token、密钥等敏感信息
|
|
835
|
+
- 禁止用 f-string 格式化日志消息(用 `%s` 占位符,支持日志聚合延迟求值)
|