create-dwy 0.8.0 → 0.9.1
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/package.json +1 -1
- package/src/create.js +50 -3
- package/src/utils.js +8 -0
- package/templates/claude-global/CLAUDE.md +81 -6
- package/templates/claude-global/rules/dwy-postgres.md +89 -0
- package/templates/claude-global/rules/dwy-python-backend.md +492 -0
- package/templates/claude-global/rules/dwy-python-core.md +94 -634
- package/templates/claude-global/rules/dwy-python-testing.md +115 -13
- package/templates/claude-global/rules/dwy-redis.md +88 -0
- package/templates/claude-global/rules/dwy-vue-core.md +341 -645
- package/templates/claude-global/rules/dwy-vue-pinia.md +47 -34
- package/templates/claude-global/rules/dwy-vue-testing.md +120 -55
- package/templates/claude-global/skills/dwy-deploy-audit/references/checks-network.md +1 -1
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_nginx.sh +1 -1
- package/templates/claude-global/skills/dwy-dolphindb/SKILL.md +22 -15
- package/templates/claude-global/skills/dwy-dolphindb/evals/evals.json +23 -0
- package/templates/claude-global/skills/dwy-dolphindb/references/dolphindb-rules.md +100 -1
- package/templates/claude-global/skills/dwy-ekit/SKILL.md +142 -276
- package/templates/claude-global/skills/dwy-eui/SKILL.md +264 -182
- package/templates/claude-global/skills/dwy-fullstack-scaffold/SKILL.md +117 -129
- package/templates/claude-global/skills/dwy-git-commit/SKILL.md +125 -0
- package/templates/claude-global/skills/dwy-publish/SKILL.md +1 -1
- package/templates/claude-global/{rules/dwy-tech-stack.md → skills/dwy-tech-stack/SKILL.md} +4 -3
- package/templates/project/fullstack-monorepo/CLAUDE.md.hbs +81 -0
- package/templates/project/fullstack-monorepo/_env.example.hbs +24 -0
- package/templates/project/fullstack-monorepo/_gitignore +37 -0
- package/templates/project/fullstack-monorepo/backend/.python-version +1 -0
- package/templates/project/fullstack-monorepo/backend/CLAUDE.md.hbs +68 -0
- package/templates/project/fullstack-monorepo/backend/Dockerfile.dev +23 -0
- package/templates/project/fullstack-monorepo/backend/alembic/env.py.hbs +68 -0
- package/templates/project/fullstack-monorepo/backend/alembic/script.py.mako +26 -0
- package/templates/project/fullstack-monorepo/backend/alembic.ini.hbs +43 -0
- package/templates/project/fullstack-monorepo/backend/packages/dwy-shared/pyproject.toml +16 -0
- package/templates/project/fullstack-monorepo/backend/packages/dwy-shared/src/dwy_shared/__init__.py +12 -0
- package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/pyproject.toml.hbs +20 -0
- package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/src/{{scopePrefix}}_{{initialDomain}}/__init__.py.hbs +40 -0
- package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/src/{{scopePrefix}}_{{initialDomain}}/config.py +9 -0
- package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/src/{{scopePrefix}}_{{initialDomain}}/models.py.hbs +17 -0
- package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/src/{{scopePrefix}}_{{initialDomain}}/protocols.py +14 -0
- package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/src/{{scopePrefix}}_{{initialDomain}}/router.py.hbs +22 -0
- package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/src/{{scopePrefix}}_{{initialDomain}}/schemas.py +21 -0
- package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/src/{{scopePrefix}}_{{initialDomain}}/service.py +11 -0
- package/templates/project/fullstack-monorepo/backend/pyproject.toml.hbs +84 -0
- package/templates/project/fullstack-monorepo/backend/src/app/__init__.py +3 -0
- package/templates/project/fullstack-monorepo/backend/src/app/config.py.hbs +21 -0
- package/templates/project/fullstack-monorepo/backend/src/app/exceptions.py +26 -0
- package/templates/project/fullstack-monorepo/backend/src/app/factories.py +16 -0
- package/templates/project/fullstack-monorepo/backend/src/app/infra/__init__.py +1 -0
- package/templates/project/fullstack-monorepo/backend/src/app/main.py.hbs +65 -0
- package/templates/project/fullstack-monorepo/backend/src/app/middleware/__init__.py +1 -0
- package/templates/project/fullstack-monorepo/backend/tests/__init__.py +0 -0
- package/templates/project/fullstack-monorepo/backend/tests/conftest.py.hbs +42 -0
- package/templates/project/fullstack-monorepo/dev.sh.hbs +83 -0
- package/templates/project/fullstack-monorepo/docker-compose.dev.yml.hbs +36 -0
- package/templates/project/fullstack-monorepo/frontend/CLAUDE.md.hbs +35 -0
- package/templates/project/fullstack-monorepo/frontend/Dockerfile +18 -0
- package/templates/project/fullstack-monorepo/frontend/_npmrc +3 -0
- package/templates/project/fullstack-monorepo/frontend/apps/web/index.html.hbs +13 -0
- package/templates/project/fullstack-monorepo/frontend/apps/web/package.json.hbs +31 -0
- package/templates/project/fullstack-monorepo/frontend/apps/web/src/App.vue.hbs +10 -0
- package/templates/project/fullstack-monorepo/frontend/apps/web/src/api/client.ts.hbs +11 -0
- package/templates/project/fullstack-monorepo/frontend/apps/web/src/features/{{initialDomain}}/.gitkeep +0 -0
- package/templates/project/fullstack-monorepo/frontend/apps/web/src/index.css +12 -0
- package/templates/project/fullstack-monorepo/frontend/apps/web/src/main.ts +17 -0
- package/templates/project/fullstack-monorepo/frontend/apps/web/src/router/index.ts.hbs +24 -0
- package/templates/project/fullstack-monorepo/frontend/apps/web/src/stores/.gitkeep +0 -0
- package/templates/project/fullstack-monorepo/frontend/apps/web/src/types/.gitkeep +0 -0
- package/templates/project/fullstack-monorepo/frontend/apps/web/src/views/HomeView.vue.hbs +36 -0
- package/templates/project/fullstack-monorepo/frontend/apps/web/tsconfig.json +7 -0
- package/templates/project/fullstack-monorepo/frontend/apps/web/vite.config.ts.hbs +27 -0
- package/templates/project/fullstack-monorepo/frontend/nginx.conf +11 -0
- package/templates/project/fullstack-monorepo/frontend/package.json.hbs +13 -0
- package/templates/project/fullstack-monorepo/frontend/packages/core/package.json.hbs +15 -0
- package/templates/project/fullstack-monorepo/frontend/packages/core/src/index.ts +3 -0
- package/templates/project/fullstack-monorepo/frontend/packages/features/{{initialDomain}}/package.json.hbs +14 -0
- package/templates/project/fullstack-monorepo/frontend/packages/features/{{initialDomain}}/src/index.ts.hbs +16 -0
- package/templates/project/fullstack-monorepo/frontend/packages/features/{{initialDomain}}/src/pages/IndexPage.vue.hbs +9 -0
- package/templates/project/fullstack-monorepo/frontend/packages/ui/package.json.hbs +15 -0
- package/templates/project/fullstack-monorepo/frontend/packages/ui/src/index.ts +3 -0
- package/templates/project/fullstack-monorepo/frontend/pnpm-workspace.yaml.hbs +23 -0
- package/templates/project/fullstack-monorepo/frontend/tsconfig.base.json.hbs +23 -0
- package/templates/claude-global/rules/dwy-api-design.md +0 -110
- package/templates/claude-global/rules/dwy-git-commit-convention.md +0 -82
- package/templates/claude-global/rules/dwy-git-security.md +0 -53
- package/templates/claude-global/rules/dwy-payload-limits.md +0 -186
- package/templates/claude-global/rules/dwy-prefer-open-source.md +0 -32
- package/templates/claude-global/rules/dwy-project-structure.md +0 -181
- package/templates/claude-global/rules/dwy-python-fastapi.md +0 -272
- package/templates/claude-global/rules/dwy-python-orm.md +0 -61
- package/templates/claude-global/rules/dwy-python-security.md +0 -636
- package/templates/claude-global/rules/dwy-server-security.md +0 -518
- package/templates/claude-global/rules/dwy-vue-api.md +0 -146
- package/templates/claude-global/skills/dwy-ekit/references/data.md +0 -135
- package/templates/claude-global/skills/dwy-ekit/references/form.md +0 -68
- package/templates/claude-global/skills/dwy-ekit/references/request.md +0 -183
- package/templates/claude-global/skills/dwy-ekit/references/ui.md +0 -109
- package/templates/claude-global/skills/dwy-vue-core/SKILL.md +0 -890
- /package/templates/{claude-global/commands → project/fullstack-monorepo/backend/alembic/versions}/.gitkeep +0 -0
|
@@ -1,77 +1,32 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Python
|
|
2
|
+
description: Python 通用代码规范
|
|
3
3
|
paths:
|
|
4
4
|
- "**/*.py"
|
|
5
5
|
- "**/pyproject.toml"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
# Python
|
|
8
|
+
# Python 通用代码规范
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
适用于所有 Python 代码(脚本 / 库 / 后端 / 数据处理 / CLI / 测试等)。Web 框架、ORM 等领域专属规范见对应 rules / skills。
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
- 使用 `pyproject.toml` 作为项目元数据和依赖管理的唯一入口(不再使用 `setup.py` / `setup.cfg`)
|
|
14
|
-
- 推荐使用 `uv` 管理依赖,次选 `pip` + `requirements.txt`
|
|
15
|
-
- 必须有 `.gitignore`(至少包含 `__pycache__/`、`*.pyc`、`.env`、`dist/`、`*.egg-info/`)
|
|
16
|
-
- 必须有 `.python-version` 文件声明 Python 版本
|
|
12
|
+
## 一、依赖与虚拟环境
|
|
17
13
|
|
|
18
|
-
###
|
|
19
|
-
```
|
|
20
|
-
project/
|
|
21
|
-
├── src/
|
|
22
|
-
│ └── app/
|
|
23
|
-
│ ├── __init__.py
|
|
24
|
-
│ ├── main.py # FastAPI 应用入口
|
|
25
|
-
│ ├── config.py # Settings 配置
|
|
26
|
-
│ ├── dependencies.py # 全局依赖(数据库会话、认证等)
|
|
27
|
-
│ ├── exceptions.py # 自定义异常 + 异常处理器
|
|
28
|
-
│ ├── models/ # SQLAlchemy / ORM 模型
|
|
29
|
-
│ │ ├── __init__.py
|
|
30
|
-
│ │ └── user.py
|
|
31
|
-
│ ├── schemas/ # Pydantic 请求/响应 schema
|
|
32
|
-
│ │ ├── __init__.py
|
|
33
|
-
│ │ └── user.py
|
|
34
|
-
│ ├── routers/ # 路由模块(按业务域拆分)
|
|
35
|
-
│ │ ├── __init__.py
|
|
36
|
-
│ │ └── user.py
|
|
37
|
-
│ ├── services/ # 业务逻辑层
|
|
38
|
-
│ │ ├── __init__.py
|
|
39
|
-
│ │ └── user.py
|
|
40
|
-
│ └── utils/ # 工具函数
|
|
41
|
-
│ └── __init__.py
|
|
42
|
-
├── tests/
|
|
43
|
-
│ ├── __init__.py
|
|
44
|
-
│ ├── conftest.py
|
|
45
|
-
│ └── test_*.py
|
|
46
|
-
├── alembic/ # 数据库迁移
|
|
47
|
-
│ ├── env.py
|
|
48
|
-
│ └── versions/
|
|
49
|
-
├── alembic.ini
|
|
50
|
-
├── pyproject.toml
|
|
51
|
-
├── Dockerfile
|
|
52
|
-
├── docker-compose.yml
|
|
53
|
-
├── .python-version
|
|
54
|
-
├── .env.example
|
|
55
|
-
└── .gitignore
|
|
56
|
-
```
|
|
14
|
+
### 强制规则
|
|
57
15
|
|
|
58
|
-
-
|
|
59
|
-
- 测试文件统一放在 `tests/` 目录,文件名以 `test_` 开头
|
|
16
|
+
- **强制使用 `uv`** 管理虚拟环境与依赖,**禁止** `pip` / `poetry` / `pipenv` / 原生 `venv` / `conda`
|
|
60
17
|
|
|
61
|
-
###
|
|
18
|
+
### 环境变量
|
|
62
19
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
| `models/` | ORM 模型定义 | 不包含业务逻辑 |
|
|
68
|
-
| `schemas/` | Pydantic 请求/响应模型 | 不依赖 ORM 模型 |
|
|
69
|
-
| `dependencies.py` | FastAPI 依赖注入(DB session、当前用户等) | 不写业务逻辑 |
|
|
20
|
+
- **禁止**在 shell 前台 `export FOO=...` 注入环境变量
|
|
21
|
+
- **禁止**在代码中硬编码配置值(数据库地址、密钥、端口等)
|
|
22
|
+
- 环境变量统一通过 `.env` 文件加载,由 Pydantic Settings 读取(见「配置管理」节)
|
|
23
|
+
- `.env` 必须列入 `.gitignore`,提交 `.env.example` 作为模板
|
|
70
24
|
|
|
71
|
-
## 二、Python
|
|
25
|
+
## 二、Python 版本与现代语法
|
|
72
26
|
|
|
73
27
|
### 强制规则
|
|
74
|
-
|
|
28
|
+
|
|
29
|
+
- 新项目最低支持 Python 3.11+
|
|
75
30
|
- 使用现代语法特性:
|
|
76
31
|
|
|
77
32
|
| 旧写法 | 新写法(强制) |
|
|
@@ -86,12 +41,13 @@ project/
|
|
|
86
41
|
## 三、代码风格
|
|
87
42
|
|
|
88
43
|
### 格式化与 Lint
|
|
44
|
+
|
|
89
45
|
- 使用 `ruff` 作为唯一的 linter + formatter(替代 flake8 / black / isort)
|
|
90
46
|
- `pyproject.toml` 中的最小 ruff 配置:
|
|
91
47
|
|
|
92
48
|
```toml
|
|
93
49
|
[tool.ruff]
|
|
94
|
-
target-version = "
|
|
50
|
+
target-version = "py311"
|
|
95
51
|
line-length = 120
|
|
96
52
|
|
|
97
53
|
[tool.ruff.lint]
|
|
@@ -106,9 +62,6 @@ select = [
|
|
|
106
62
|
"SIM", # flake8-simplify
|
|
107
63
|
"RUF", # ruff-specific
|
|
108
64
|
]
|
|
109
|
-
|
|
110
|
-
[tool.ruff.lint.isort]
|
|
111
|
-
known-first-party = ["app"]
|
|
112
65
|
```
|
|
113
66
|
|
|
114
67
|
### 命名规范
|
|
@@ -123,81 +76,35 @@ known-first-party = ["app"]
|
|
|
123
76
|
| 私有属性/方法 | `_leading_underscore` | `_parse_token()` |
|
|
124
77
|
| 类型变量 | `PascalCase` | `T = TypeVar("T")` |
|
|
125
78
|
| 协议/抽象基类 | `PascalCase` + 语义后缀 | `Serializable`、`BaseRepository` |
|
|
126
|
-
| 路由函数 | `snake_case` 动词开头 | `create_user()`、`list_orders()` |
|
|
127
|
-
| Pydantic Schema | `PascalCase` + 用途后缀 | `UserCreate`、`UserResponse`、`UserUpdate` |
|
|
128
79
|
|
|
129
80
|
### 禁止魔法字符串
|
|
130
81
|
|
|
131
|
-
|
|
82
|
+
同一字符串字面量在同一文件出现 ≥ 2 次 → **必须**提取为常量;跨模块共享 → 提取到 `constants.py`。
|
|
132
83
|
|
|
133
84
|
```python
|
|
134
|
-
# ❌ 魔法字符串散落多处
|
|
135
|
-
redis.set("user:token:123", token)
|
|
136
|
-
cached = redis.get("user:token:123")
|
|
137
|
-
|
|
138
|
-
# ✅ 提取为常量
|
|
139
85
|
USER_TOKEN_KEY = "user:token:{user_id}"
|
|
140
86
|
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
87
|
```
|
|
153
88
|
|
|
154
|
-
**判断标准:** 同一字符串在同一文件出现 ≥ 2 次 → 提取为常量。跨模块使用的 key → 提取到 `constants.py` 共享。
|
|
155
|
-
|
|
156
89
|
### 禁止的写法
|
|
157
90
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
d = get_data()
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
user_data = get_data()
|
|
164
|
-
|
|
165
|
-
# ❌ 匈牙利命名法
|
|
166
|
-
str_name = "Alice"
|
|
167
|
-
lst_users = []
|
|
168
|
-
|
|
169
|
-
# ✅ 直接命名
|
|
170
|
-
name = "Alice"
|
|
171
|
-
users = []
|
|
172
|
-
|
|
173
|
-
# ❌ 布尔变量不带 is/has/can/should 前缀
|
|
174
|
-
active = True
|
|
175
|
-
|
|
176
|
-
# ✅ 布尔变量语义明确
|
|
177
|
-
is_active = True
|
|
178
|
-
has_permission = False
|
|
179
|
-
```
|
|
91
|
+
| ❌ | ✅ |
|
|
92
|
+
|---|---|
|
|
93
|
+
| `d = get_data()` | `user_data = get_data()`(单字母变量仅允许循环索引) |
|
|
94
|
+
| `str_name = "Alice"` / `lst_users = []` | `name = "Alice"` / `users = []`(不用匈牙利命名法) |
|
|
95
|
+
| `active = True` | `is_active = True` / `has_permission = False`(布尔加 is/has/can/should 前缀) |
|
|
180
96
|
|
|
181
97
|
## 四、类型标注
|
|
182
98
|
|
|
183
|
-
### 强制规则
|
|
184
99
|
- 所有公开函数/方法**必须**有参数和返回值类型标注
|
|
185
100
|
- 私有函数**推荐**有类型标注
|
|
186
101
|
- 类属性**必须**有类型标注
|
|
102
|
+
- **禁止** `Any` 作为"懒标注",除非与外部动态库交互且无法确定类型;使用时必须附注释说明原因
|
|
187
103
|
|
|
188
104
|
```python
|
|
189
|
-
|
|
190
|
-
def get_user(user_id):
|
|
191
|
-
...
|
|
192
|
-
|
|
193
|
-
# ✅ 完整标注
|
|
194
|
-
def get_user(user_id: int) -> User | None:
|
|
195
|
-
...
|
|
196
|
-
|
|
197
|
-
# ✅ 复杂类型使用 TypeAlias
|
|
105
|
+
def get_user(user_id: int) -> User | None: ...
|
|
198
106
|
type UserMap = dict[str, list[User]]
|
|
199
107
|
|
|
200
|
-
# ✅ 数据类必须标注
|
|
201
108
|
@dataclass
|
|
202
109
|
class UserProfile:
|
|
203
110
|
name: str
|
|
@@ -205,209 +112,51 @@ class UserProfile:
|
|
|
205
112
|
email: str | None = None
|
|
206
113
|
```
|
|
207
114
|
|
|
208
|
-
|
|
209
|
-
- 不得使用 `Any` 作为"懒标注",除非与外部动态库交互且无法确定类型
|
|
210
|
-
- 使用 `Any` 时必须附注释说明原因
|
|
211
|
-
|
|
212
|
-
## 十、异步编程
|
|
213
|
-
|
|
214
|
-
### 强制规则
|
|
215
|
-
- FastAPI 路由函数统一使用 `async def`
|
|
216
|
-
- 异步函数内**禁止**调用同步阻塞操作(IO、网络、sleep)
|
|
217
|
-
- 使用 `asyncio.to_thread()` 包装不可避免的同步调用
|
|
218
|
-
|
|
219
|
-
```python
|
|
220
|
-
# ❌ 异步函数中调用同步阻塞
|
|
221
|
-
async def get_data():
|
|
222
|
-
result = requests.get(url) # 阻塞事件循环
|
|
223
|
-
data = open("file.txt").read() # 阻塞事件循环
|
|
224
|
-
|
|
225
|
-
# ✅ 使用异步库
|
|
226
|
-
async def get_data():
|
|
227
|
-
async with httpx.AsyncClient() as client:
|
|
228
|
-
result = await client.get(url)
|
|
229
|
-
data = await asyncio.to_thread(Path("file.txt").read_text)
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
- HTTP 客户端使用 `httpx`,**禁止** `requests`
|
|
233
|
-
- 数据库使用异步驱动(`asyncpg`、`aiomysql`)
|
|
234
|
-
|
|
235
|
-
## 十一、中间件与生命周期
|
|
115
|
+
## 五、配置管理
|
|
236
116
|
|
|
237
117
|
### 强制规则
|
|
238
118
|
|
|
239
|
-
```python
|
|
240
|
-
# main.py
|
|
241
|
-
from contextlib import asynccontextmanager
|
|
242
|
-
from collections.abc import AsyncGenerator
|
|
243
|
-
|
|
244
|
-
@asynccontextmanager
|
|
245
|
-
async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
|
|
246
|
-
# 启动时执行
|
|
247
|
-
logger.info("应用启动")
|
|
248
|
-
yield
|
|
249
|
-
# 关闭时执行
|
|
250
|
-
await engine.dispose()
|
|
251
|
-
logger.info("应用关闭")
|
|
252
|
-
|
|
253
|
-
app = FastAPI(
|
|
254
|
-
title="My API",
|
|
255
|
-
version="1.0.0",
|
|
256
|
-
lifespan=lifespan,
|
|
257
|
-
)
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
- 使用 `lifespan` 管理应用生命周期,**禁止** `@app.on_event("startup")`(已废弃)
|
|
261
|
-
- CORS 中间件必须明确指定允许的域名,**禁止**生产环境使用 `allow_origins=["*"]`
|
|
262
|
-
|
|
263
|
-
```python
|
|
264
|
-
from fastapi.middleware.cors import CORSMiddleware
|
|
265
|
-
|
|
266
|
-
app.add_middleware(
|
|
267
|
-
CORSMiddleware,
|
|
268
|
-
allow_origins=settings.allowed_origins, # 从环境变量读取
|
|
269
|
-
allow_credentials=True,
|
|
270
|
-
allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
|
|
271
|
-
allow_headers=["*"],
|
|
272
|
-
)
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
## 十二、配置管理
|
|
276
|
-
|
|
277
|
-
### 强制规则
|
|
278
119
|
- 使用 Pydantic Settings 管理配置
|
|
279
120
|
- **必须使用嵌套模型分组**,禁止所有字段平铺在一个类中
|
|
121
|
+
- **禁止**扁平命名(如 `settings.oss_endpoint`),必须 `settings.oss.endpoint` 属性链访问
|
|
122
|
+
- **禁止**在模块顶层执行副作用(网络请求、数据库连接、文件读取)
|
|
280
123
|
|
|
281
124
|
```python
|
|
282
125
|
# config.py
|
|
283
126
|
from pydantic import BaseModel
|
|
284
127
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
285
128
|
|
|
286
|
-
# 按领域拆分为独立的配置类(普通 BaseModel,不是 BaseSettings)
|
|
287
129
|
class OssConfig(BaseModel):
|
|
288
130
|
endpoint: str = ""
|
|
289
|
-
access_key: str = ""
|
|
290
|
-
secret_key: str = ""
|
|
291
|
-
bucket: str = ""
|
|
292
131
|
|
|
293
|
-
class AliyunSmsConfig(BaseModel):
|
|
294
|
-
access_key_id: str = ""
|
|
295
|
-
access_key_secret: str = ""
|
|
296
|
-
sign_name: str = ""
|
|
297
|
-
template_code: str = ""
|
|
298
|
-
|
|
299
|
-
# 主 Settings 类组合嵌套模型
|
|
300
132
|
class Settings(BaseSettings):
|
|
301
|
-
# eapi 内置字段(继承 BaseSettings 时自动包含)
|
|
302
|
-
# database_url, redis_url, secret_key, ...
|
|
303
|
-
|
|
304
|
-
# 项目专属 — 嵌套分组
|
|
305
133
|
app_name: str = "My App"
|
|
306
134
|
oss: OssConfig = OssConfig()
|
|
307
|
-
aliyun_sms: AliyunSmsConfig = AliyunSmsConfig()
|
|
308
135
|
|
|
309
136
|
model_config = SettingsConfigDict(
|
|
310
137
|
env_file=".env",
|
|
311
|
-
env_file_encoding="utf-8",
|
|
312
138
|
env_nested_delimiter="__", # 关键:启用嵌套分隔符
|
|
313
139
|
)
|
|
314
|
-
|
|
315
|
-
settings = Settings()
|
|
316
140
|
```
|
|
317
141
|
|
|
318
|
-
|
|
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
|
-
```
|
|
142
|
+
`.env` 用 `__` 分隔层级:`OSS__ENDPOINT=http://localhost:9000`。
|
|
351
143
|
|
|
352
144
|
### 分组原则
|
|
353
145
|
|
|
354
146
|
| 场景 | 做法 |
|
|
355
147
|
|------|------|
|
|
356
|
-
| eapi 内置字段(database_url 等) | 保持顶层,不嵌套 |
|
|
357
148
|
| 同一服务/SDK 的多个配置 | 拆成独立 `BaseModel` 嵌套 |
|
|
358
149
|
| 只有 1 个字段 | 保持顶层,不必嵌套 |
|
|
359
150
|
| 3 个以上同前缀字段 | 必须嵌套 |
|
|
360
151
|
|
|
361
|
-
|
|
362
|
-
- **禁止**在模块顶层执行副作用(网络请求、数据库连接、文件读取)
|
|
363
|
-
|
|
364
|
-
## 十三、日志
|
|
365
|
-
|
|
366
|
-
### 强制规则
|
|
367
|
-
- 使用标准库 `logging` 或 `structlog`,**禁止** `print()` 出现在非调试代码中
|
|
368
|
-
- 使用 `%s` 占位符或 structlog 的结构化日志,**禁止** f-string 拼接日志
|
|
369
|
-
|
|
370
|
-
```python
|
|
371
|
-
# ❌ print 调试
|
|
372
|
-
print(f"User {user_id} not found")
|
|
373
|
-
|
|
374
|
-
# ❌ f-string 日志(参数不会延迟求值,且无法被日志聚合工具解析)
|
|
375
|
-
logger.info(f"User {user_id} created")
|
|
376
|
-
|
|
377
|
-
# ✅ 占位符日志
|
|
378
|
-
logger.info("User %s created", user_id)
|
|
379
|
-
|
|
380
|
-
# ✅ structlog
|
|
381
|
-
logger.info("user_created", user_id=user_id)
|
|
382
|
-
```
|
|
383
|
-
|
|
384
|
-
### 日志级别使用规范
|
|
385
|
-
|
|
386
|
-
| 级别 | 使用场景 |
|
|
387
|
-
|---|---|
|
|
388
|
-
| `DEBUG` | 开发调试信息,生产环境不输出 |
|
|
389
|
-
| `INFO` | 关键业务流程节点(用户注册、订单创建等) |
|
|
390
|
-
| `WARNING` | 异常但可恢复的情况(重试、降级) |
|
|
391
|
-
| `ERROR` | 错误但服务仍可用(单次请求失败) |
|
|
392
|
-
| `CRITICAL` | 服务不可用(数据库连接断开、必要服务宕机) |
|
|
393
|
-
|
|
394
|
-
## 十五、函数与类设计
|
|
152
|
+
## 六、函数与类设计
|
|
395
153
|
|
|
396
154
|
### 强制规则
|
|
397
155
|
|
|
398
|
-
|
|
399
|
-
# ❌ 超过 3 个位置参数
|
|
400
|
-
def create_user(name, age, email, role, department):
|
|
401
|
-
...
|
|
402
|
-
|
|
403
|
-
# ✅ 使用关键字参数或 Pydantic model
|
|
404
|
-
def create_user(request: CreateUserRequest) -> User:
|
|
405
|
-
...
|
|
406
|
-
```
|
|
407
|
-
|
|
156
|
+
- 超过 3 个位置参数 → 改用关键字参数或 Pydantic model 包装
|
|
408
157
|
- 函数体不超过 50 行(超过则拆分)
|
|
409
158
|
- 单个文件不超过 500 行(超过则拆分模块)
|
|
410
|
-
- 嵌套不超过 3
|
|
159
|
+
- 嵌套不超过 3 层(用 early return 降低嵌套)
|
|
411
160
|
|
|
412
161
|
```python
|
|
413
162
|
# ❌ 深层嵌套
|
|
@@ -415,42 +164,32 @@ def process(data):
|
|
|
415
164
|
if data:
|
|
416
165
|
if data.is_valid:
|
|
417
166
|
if data.has_items:
|
|
418
|
-
for item in data.items:
|
|
419
|
-
...
|
|
167
|
+
for item in data.items: ...
|
|
420
168
|
|
|
421
|
-
# ✅
|
|
169
|
+
# ✅ early return
|
|
422
170
|
def process(data):
|
|
423
|
-
if not data:
|
|
424
|
-
|
|
425
|
-
if not data.
|
|
426
|
-
|
|
427
|
-
if not data.has_items:
|
|
428
|
-
return
|
|
429
|
-
for item in data.items:
|
|
430
|
-
...
|
|
171
|
+
if not data: return
|
|
172
|
+
if not data.is_valid: return
|
|
173
|
+
if not data.has_items: return
|
|
174
|
+
for item in data.items: ...
|
|
431
175
|
```
|
|
432
176
|
|
|
433
|
-
##
|
|
177
|
+
## 七、数据校验与序列化
|
|
434
178
|
|
|
435
179
|
### 强制规则
|
|
436
|
-
|
|
180
|
+
|
|
181
|
+
- 外部输入(用户请求、外部 API 响应、文件读入)使用 **Pydantic v2** 校验
|
|
437
182
|
- 内部数据结构优先使用 `dataclass`,需要校验时用 Pydantic
|
|
438
|
-
-
|
|
183
|
+
- **禁止**手动校验字典字段(如 `if "name" not in data: raise ...`)
|
|
439
184
|
|
|
440
185
|
```python
|
|
441
|
-
# ❌ 手动校验字典
|
|
442
|
-
def create_user(data: dict):
|
|
443
|
-
if "name" not in data:
|
|
444
|
-
raise ValueError("name required")
|
|
445
|
-
|
|
446
|
-
# ✅ Pydantic 自动校验
|
|
447
186
|
class CreateUserRequest(BaseModel):
|
|
448
187
|
name: str
|
|
449
188
|
age: int = Field(ge=0, le=150)
|
|
450
189
|
email: EmailStr
|
|
451
190
|
```
|
|
452
191
|
|
|
453
|
-
##
|
|
192
|
+
## 八、常见反模式(禁止)
|
|
454
193
|
|
|
455
194
|
| 反模式 | 正确做法 |
|
|
456
195
|
|---|---|
|
|
@@ -466,370 +205,91 @@ class CreateUserRequest(BaseModel):
|
|
|
466
205
|
| `type(x) == SomeType` | `isinstance(x, SomeType)` |
|
|
467
206
|
| `dict.keys()` 遍历 | 直接 `for k in dict` |
|
|
468
207
|
| 嵌套字典传递数据 | `dataclass` / `TypedDict` / Pydantic model |
|
|
469
|
-
|
|
|
470
|
-
|
|
|
471
|
-
| 路由里写数据库查询 | 调用 service 层 |
|
|
472
|
-
| `allow_origins=["*"]`(生产) | 明确列出允许的域名 |
|
|
208
|
+
| 裸 `except:` / `except Exception: pass` | 只捕获预期的具体异常 |
|
|
209
|
+
| `print()` 输出调试信息 | 使用 `logging` |
|
|
473
210
|
|
|
474
|
-
##
|
|
211
|
+
## 九、注释与文档
|
|
475
212
|
|
|
476
213
|
### 强制规则
|
|
477
|
-
- 注释用于解释 **为什么(why)**,而非 **做了什么(what)**
|
|
478
|
-
- 每个 `.py` 文件**必须**在顶部添加模块级 docstring,说明该文件的主要功能
|
|
479
|
-
- 所有类**必须**有 docstring,说明类的职责和用途
|
|
480
|
-
- 所有方法/函数(包括公开和私有)**必须**有 docstring,说明功能、参数和返回值
|
|
481
|
-
- docstring 统一使用 **Google 风格**
|
|
482
214
|
|
|
483
|
-
|
|
215
|
+
- 注释解释 **为什么(why)**,而非 **做了什么(what)**
|
|
216
|
+
- 每个 `.py` 文件**必须**有模块级 docstring
|
|
217
|
+
- 所有类、所有方法/函数(含私有)**必须**有 docstring
|
|
218
|
+
- docstring 统一使用 **Google 风格**
|
|
484
219
|
|
|
485
|
-
|
|
220
|
+
### 完整示例(模块 / 类 / 方法 / 私有方法 docstring)
|
|
486
221
|
|
|
487
222
|
```python
|
|
488
|
-
|
|
489
|
-
"""用户相关的路由模块。
|
|
223
|
+
"""用户数据处理工具。
|
|
490
224
|
|
|
491
|
-
|
|
225
|
+
提供用户对象的序列化、反序列化和缓存键构建。
|
|
492
226
|
"""
|
|
493
227
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
```
|
|
497
|
-
|
|
498
|
-
```python
|
|
499
|
-
# ❌ 缺少模块 docstring
|
|
500
|
-
from fastapi import APIRouter
|
|
501
|
-
...
|
|
502
|
-
```
|
|
503
|
-
|
|
504
|
-
### 类 docstring
|
|
505
|
-
|
|
506
|
-
```python
|
|
507
|
-
# ✅ 类 docstring
|
|
508
|
-
class UserService:
|
|
509
|
-
"""用户业务逻辑服务。
|
|
510
|
-
|
|
511
|
-
负责用户的创建、查询、更新和删除操作,
|
|
512
|
-
封装所有与用户相关的业务规则。
|
|
513
|
-
"""
|
|
228
|
+
class UserRepository:
|
|
229
|
+
"""用户数据仓储。封装用户表的查询、写入和缓存逻辑。"""
|
|
514
230
|
|
|
515
|
-
def __init__(self, db:
|
|
516
|
-
"""初始化
|
|
231
|
+
def __init__(self, db: Database) -> None:
|
|
232
|
+
"""初始化 UserRepository。
|
|
517
233
|
|
|
518
234
|
Args:
|
|
519
|
-
db:
|
|
235
|
+
db: 数据库连接对象。
|
|
520
236
|
"""
|
|
521
237
|
self.db = db
|
|
522
|
-
```
|
|
523
|
-
|
|
524
|
-
```python
|
|
525
|
-
# ❌ 缺少类 docstring
|
|
526
|
-
class UserService:
|
|
527
|
-
def __init__(self, db: AsyncSession) -> None:
|
|
528
|
-
self.db = db
|
|
529
|
-
```
|
|
530
238
|
|
|
531
|
-
|
|
239
|
+
def get_by_id(self, user_id: int) -> User:
|
|
240
|
+
"""根据 ID 查询用户。
|
|
532
241
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
Raises:
|
|
545
|
-
NotFoundError: 用户不存在时抛出。
|
|
546
|
-
"""
|
|
547
|
-
user = await self.db.get(User, user_id)
|
|
548
|
-
if not user:
|
|
549
|
-
raise NotFoundError("用户", user_id)
|
|
550
|
-
return user
|
|
551
|
-
```
|
|
552
|
-
|
|
553
|
-
```python
|
|
554
|
-
# ✅ 简单函数可以使用单行 docstring
|
|
555
|
-
def _build_cache_key(self, user_id: int) -> str:
|
|
556
|
-
"""根据用户 ID 构建缓存键。"""
|
|
557
|
-
return f"user:{user_id}"
|
|
558
|
-
```
|
|
242
|
+
Args:
|
|
243
|
+
user_id: 用户唯一标识。
|
|
244
|
+
Returns:
|
|
245
|
+
匹配的用户对象。
|
|
246
|
+
Raises:
|
|
247
|
+
UserNotFound: 用户不存在时抛出。
|
|
248
|
+
"""
|
|
249
|
+
user = self.db.get(User, user_id)
|
|
250
|
+
if not user:
|
|
251
|
+
raise UserNotFound(user_id)
|
|
252
|
+
return user
|
|
559
253
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
user = await self.db.get(User, user_id)
|
|
564
|
-
if not user:
|
|
565
|
-
raise NotFoundError("用户", user_id)
|
|
566
|
-
return user
|
|
254
|
+
def _build_cache_key(self, user_id: int) -> str:
|
|
255
|
+
"""根据用户 ID 构建缓存键。"""
|
|
256
|
+
return f"user:{user_id}"
|
|
567
257
|
```
|
|
568
258
|
|
|
569
259
|
### docstring 规范
|
|
570
260
|
|
|
571
261
|
| 场景 | 要求 |
|
|
572
262
|
|---|---|
|
|
573
|
-
| 单行可描述清楚 | 使用单行 docstring
|
|
574
|
-
| 有参数/返回值/异常 |
|
|
575
|
-
| `__init__` 方法 |
|
|
576
|
-
| 魔术方法(`__str__` 等) |
|
|
577
|
-
| 私有方法 |
|
|
263
|
+
| 单行可描述清楚 | 使用单行 docstring |
|
|
264
|
+
| 有参数/返回值/异常 | 多行 Google 风格,包含 `Args` / `Returns` / `Raises` |
|
|
265
|
+
| `__init__` 方法 | 必须有,说明初始化参数 |
|
|
266
|
+
| 魔术方法(`__str__` 等) | 必须有,说明行为 |
|
|
267
|
+
| 私有方法 | 必须有,至少单行说明 |
|
|
578
268
|
|
|
579
269
|
### 行内注释
|
|
580
|
-
```python
|
|
581
|
-
# ❌ 无用注释
|
|
582
|
-
# 获取用户
|
|
583
|
-
user = get_user(user_id)
|
|
584
|
-
|
|
585
|
-
# ✅ 解释原因
|
|
586
|
-
# 此处使用缓存查询,因为用户表 QPS 超过 5000
|
|
587
|
-
user = cache.get_user(user_id)
|
|
588
|
-
```
|
|
589
|
-
|
|
590
|
-
### API 文档
|
|
591
|
-
- 路由函数的 docstring 会自动生成 OpenAPI 描述,保持简洁
|
|
592
|
-
- 使用 `summary` 参数替代 docstring 标题
|
|
593
|
-
|
|
594
|
-
```python
|
|
595
|
-
@router.post(
|
|
596
|
-
"/",
|
|
597
|
-
status_code=status.HTTP_201_CREATED,
|
|
598
|
-
response_model=UserResponse,
|
|
599
|
-
summary="创建用户",
|
|
600
|
-
)
|
|
601
|
-
async def create_user(body: UserCreate) -> UserResponse:
|
|
602
|
-
"""根据提交的信息创建新用户,邮箱不可重复。"""
|
|
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
270
|
|
|
647
271
|
```python
|
|
648
|
-
# ❌
|
|
649
|
-
|
|
650
|
-
|
|
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)
|
|
272
|
+
# ❌ 无用:# 获取用户 \n user = get_user(user_id)
|
|
273
|
+
# ✅ 解释原因:# 此处用缓存,因为用户表 QPS 超过 5000
|
|
274
|
+
user = cache.get_user(user_id)
|
|
690
275
|
```
|
|
691
276
|
|
|
692
|
-
### 判断标准
|
|
693
|
-
|
|
694
|
-
写每一行防御代码前问自己:**这个情况在当前上下文下真的会发生吗?**
|
|
695
|
-
|
|
696
|
-
- 如果**会** → 写防御,加注释说明什么情况下触发
|
|
697
|
-
- 如果**不会** → 不写,信任上游保证
|
|
698
|
-
- 如果**不确定** → 查看调用链确认,不要"以防万一"
|
|
699
|
-
|
|
700
277
|
## 代码自检(写代码时强制执行)
|
|
701
278
|
|
|
702
279
|
**每次生成或修改 Python 代码后,必须逐条验证以下清单。任何一条未通过 → STOP,立即修正后再继续。**
|
|
703
280
|
|
|
704
|
-
| # | 检查项 |
|
|
705
|
-
|
|
706
|
-
| 1 |
|
|
707
|
-
| 2 |
|
|
708
|
-
| 3 |
|
|
709
|
-
| 4 |
|
|
710
|
-
| 5 |
|
|
711
|
-
| 6 |
|
|
712
|
-
| 7 |
|
|
713
|
-
| 8 |
|
|
714
|
-
| 9 |
|
|
715
|
-
| 10 |
|
|
716
|
-
| 11 | Pydantic
|
|
717
|
-
| 12 | 嵌套不超过 3 层,函数体不超过 50 行 | ✓ |
|
|
718
|
-
| 13 | 所有模块/类/函数有 docstring(Google 风格) | ✓ |
|
|
719
|
-
| 14 | 依赖通过 `Depends()` 注入,不在路由中直接创建 | ✓ |
|
|
720
|
-
| 15 | 项目依赖 eapi 时,未重复实现 eapi 已有功能 | ✓ |
|
|
281
|
+
| # | 检查项 |
|
|
282
|
+
|---|--------|
|
|
283
|
+
| 1 | 依赖与虚拟环境用 `uv`,不用 pip / poetry / pipenv / venv |
|
|
284
|
+
| 2 | 环境变量走 `.env` + Pydantic Settings,不在 shell 前台 export |
|
|
285
|
+
| 3 | 所有公开函数有参数 + 返回值类型标注 |
|
|
286
|
+
| 4 | 无 `Any` 类型(除非有注释说明原因) |
|
|
287
|
+
| 5 | 使用 `list[int]` / `str \| None` 等现代语法,不用 `typing.List` / `Optional` |
|
|
288
|
+
| 6 | 无裸 `except:` 或 `except Exception: pass` |
|
|
289
|
+
| 7 | 无可变默认参数 `def f(items=[])` |
|
|
290
|
+
| 8 | 嵌套不超过 3 层,函数体不超过 50 行 |
|
|
291
|
+
| 9 | 所有模块/类/函数有 docstring(Google 风格) |
|
|
292
|
+
| 10 | 外部输入用 Pydantic 校验,不手动校验 dict |
|
|
293
|
+
| 11 | 配置必须 Pydantic Settings 嵌套分组,不在代码硬编码 |
|
|
721
294
|
|
|
722
295
|
**不执行自检就提交代码 = 违规。**
|
|
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
|
-
...
|
|
749
|
-
```
|
|
750
|
-
|
|
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 |
|
|
758
|
-
|
|
759
|
-
---
|
|
760
|
-
|
|
761
|
-
## 环境变量管理(后端:eapi BaseSettings + 嵌套分组)
|
|
762
|
-
|
|
763
|
-
**必须使用嵌套模型分组配置**,禁止所有字段平铺。同一服务/SDK 的多个字段拆成独立 `BaseModel`。
|
|
764
|
-
|
|
765
|
-
```python
|
|
766
|
-
# app/config.py
|
|
767
|
-
from pydantic import BaseModel
|
|
768
|
-
from dwyeapi.config import BaseSettings
|
|
769
|
-
|
|
770
|
-
class OssConfig(BaseModel):
|
|
771
|
-
endpoint: str = ""
|
|
772
|
-
access_key: str = ""
|
|
773
|
-
secret_key: str = ""
|
|
774
|
-
bucket: str = ""
|
|
775
|
-
|
|
776
|
-
class Settings(BaseSettings):
|
|
777
|
-
# eapi 内置字段保持顶层:database_url, redis_url, secret_key, ...
|
|
778
|
-
|
|
779
|
-
# 项目专属 — 嵌套分组
|
|
780
|
-
app_name: str = "My App"
|
|
781
|
-
oss: OssConfig = OssConfig()
|
|
782
|
-
```
|
|
783
|
-
|
|
784
|
-
**`.env` 中用 `__` 双下划线分隔层级:**
|
|
785
|
-
|
|
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
|
|
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` 占位符,支持日志聚合延迟求值)
|