create-dwy 0.1.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.
Files changed (48) hide show
  1. package/bin/index.js +31 -0
  2. package/package.json +29 -0
  3. package/src/create.js +93 -0
  4. package/src/sync.js +75 -0
  5. package/src/utils.js +61 -0
  6. package/templates/claude-global/commands/.gitkeep +0 -0
  7. package/templates/claude-global/hooks/.gitkeep +0 -0
  8. package/templates/claude-global/rules/git-security.md +144 -0
  9. package/templates/claude-global/rules/python-code-style.md +875 -0
  10. package/templates/claude-global/rules/server-security.md +376 -0
  11. package/templates/claude-global/skills/.gitkeep +0 -0
  12. package/templates/project/web/.claude/commands/deploy.md +4 -0
  13. package/templates/project/web/.dwy.hbs +5 -0
  14. package/templates/project/web/CLAUDE.md.hbs +52 -0
  15. package/templates/project/web/backend/.python-version +1 -0
  16. package/templates/project/web/backend/Dockerfile +17 -0
  17. package/templates/project/web/backend/alembic/env.py.hbs +44 -0
  18. package/templates/project/web/backend/alembic/script.py.mako +24 -0
  19. package/templates/project/web/backend/alembic/versions/.gitkeep +0 -0
  20. package/templates/project/web/backend/alembic.ini.hbs +36 -0
  21. package/templates/project/web/backend/pyproject.toml.hbs +52 -0
  22. package/templates/project/web/backend/src/app/__init__.py +0 -0
  23. package/templates/project/web/backend/src/app/config.py.hbs +12 -0
  24. package/templates/project/web/backend/src/app/main.py.hbs +40 -0
  25. package/templates/project/web/backend/src/app/models/__init__.py +0 -0
  26. package/templates/project/web/backend/src/app/models/base.py +5 -0
  27. package/templates/project/web/backend/src/app/routers/__init__.py +0 -0
  28. package/templates/project/web/backend/src/app/routers/health.py +11 -0
  29. package/templates/project/web/backend/src/app/schemas/__init__.py +0 -0
  30. package/templates/project/web/backend/src/app/services/__init__.py +0 -0
  31. package/templates/project/web/backend/tests/__init__.py +0 -0
  32. package/templates/project/web/backend/tests/conftest.py.hbs +16 -0
  33. package/templates/project/web/deploy.sh.hbs +113 -0
  34. package/templates/project/web/docker-compose.dev.yml.hbs +19 -0
  35. package/templates/project/web/docker-compose.prod.yml.hbs +31 -0
  36. package/templates/project/web/frontend/index.html.hbs +12 -0
  37. package/templates/project/web/frontend/package.json.hbs +28 -0
  38. package/templates/project/web/frontend/src/App.vue +3 -0
  39. package/templates/project/web/frontend/src/api/client.ts +13 -0
  40. package/templates/project/web/frontend/src/components/.gitkeep +0 -0
  41. package/templates/project/web/frontend/src/main.ts +10 -0
  42. package/templates/project/web/frontend/src/router/index.ts +14 -0
  43. package/templates/project/web/frontend/src/stores/auth.ts +20 -0
  44. package/templates/project/web/frontend/src/style.css +1 -0
  45. package/templates/project/web/frontend/src/views/HomeView.vue +5 -0
  46. package/templates/project/web/frontend/tsconfig.app.json +14 -0
  47. package/templates/project/web/frontend/tsconfig.json +4 -0
  48. package/templates/project/web/frontend/vite.config.ts.hbs +16 -0
@@ -0,0 +1,875 @@
1
+ ---
2
+ name: python-code-style
3
+ description: Python + FastAPI 代码规范
4
+ type: claude-rule
5
+ tags: [python, fastapi, code-style]
6
+ version: 1.0.0
7
+ author: chances
8
+ ---
9
+
10
+ # Python + FastAPI 代码规范
11
+
12
+ 所有 Python / FastAPI 项目必须遵循以下规范。AI 生成或修改代码时,必须严格按照这些规则执行。
13
+
14
+ ## 一、项目结构
15
+
16
+ ### 强制规则
17
+ - 使用 `pyproject.toml` 作为项目元数据和依赖管理的唯一入口(不再使用 `setup.py` / `setup.cfg`)
18
+ - 推荐使用 `uv` 管理依赖,次选 `pip` + `requirements.txt`
19
+ - 必须有 `.gitignore`(至少包含 `__pycache__/`、`*.pyc`、`.env`、`dist/`、`*.egg-info/`)
20
+ - 必须有 `.python-version` 文件声明 Python 版本
21
+
22
+ ### 标准目录结构
23
+ ```
24
+ project/
25
+ ├── src/
26
+ │ └── app/
27
+ │ ├── __init__.py
28
+ │ ├── main.py # FastAPI 应用入口
29
+ │ ├── config.py # Settings 配置
30
+ │ ├── dependencies.py # 全局依赖(数据库会话、认证等)
31
+ │ ├── exceptions.py # 自定义异常 + 异常处理器
32
+ │ ├── models/ # SQLAlchemy / ORM 模型
33
+ │ │ ├── __init__.py
34
+ │ │ └── user.py
35
+ │ ├── schemas/ # Pydantic 请求/响应 schema
36
+ │ │ ├── __init__.py
37
+ │ │ └── user.py
38
+ │ ├── routers/ # 路由模块(按业务域拆分)
39
+ │ │ ├── __init__.py
40
+ │ │ └── user.py
41
+ │ ├── services/ # 业务逻辑层
42
+ │ │ ├── __init__.py
43
+ │ │ └── user.py
44
+ │ └── utils/ # 工具函数
45
+ │ └── __init__.py
46
+ ├── tests/
47
+ │ ├── __init__.py
48
+ │ ├── conftest.py
49
+ │ └── test_*.py
50
+ ├── alembic/ # 数据库迁移
51
+ │ ├── env.py
52
+ │ └── versions/
53
+ ├── alembic.ini
54
+ ├── pyproject.toml
55
+ ├── Dockerfile
56
+ ├── docker-compose.yml
57
+ ├── .python-version
58
+ ├── .env.example
59
+ └── .gitignore
60
+ ```
61
+
62
+ - 小型脚本/单文件项目可以不使用 `src/` 布局
63
+ - 测试文件统一放在 `tests/` 目录,文件名以 `test_` 开头
64
+
65
+ ### 分层原则
66
+
67
+ | 层 | 职责 | 禁止 |
68
+ |---|---|---|
69
+ | `routers/` | 接收请求、参数校验、调用 service、返回响应 | 不写业务逻辑、不直接操作数据库 |
70
+ | `services/` | 业务逻辑、事务编排 | 不感知 HTTP 概念(Request/Response/状态码) |
71
+ | `models/` | ORM 模型定义 | 不包含业务逻辑 |
72
+ | `schemas/` | Pydantic 请求/响应模型 | 不依赖 ORM 模型 |
73
+ | `dependencies.py` | FastAPI 依赖注入(DB session、当前用户等) | 不写业务逻辑 |
74
+
75
+ ## 二、Python 版本与兼容性
76
+
77
+ ### 强制规则
78
+ - 新项目最低支持 Python 3.10+
79
+ - 使用现代语法特性:
80
+
81
+ | 旧写法 | 新写法(强制) |
82
+ |---|---|
83
+ | `typing.List[int]` | `list[int]` |
84
+ | `typing.Dict[str, int]` | `dict[str, int]` |
85
+ | `typing.Optional[str]` | `str \| None` |
86
+ | `typing.Union[str, int]` | `str \| int` |
87
+ | `typing.Tuple[int, ...]` | `tuple[int, ...]` |
88
+ | `from __future__ import annotations` | 不需要(3.10+ 原生支持) |
89
+
90
+ ## 三、代码风格
91
+
92
+ ### 格式化与 Lint
93
+ - 使用 `ruff` 作为唯一的 linter + formatter(替代 flake8 / black / isort)
94
+ - `pyproject.toml` 中的最小 ruff 配置:
95
+
96
+ ```toml
97
+ [tool.ruff]
98
+ target-version = "py310"
99
+ line-length = 120
100
+
101
+ [tool.ruff.lint]
102
+ select = [
103
+ "E", # pycodestyle errors
104
+ "W", # pycodestyle warnings
105
+ "F", # pyflakes
106
+ "I", # isort
107
+ "N", # pep8-naming
108
+ "UP", # pyupgrade
109
+ "B", # flake8-bugbear
110
+ "SIM", # flake8-simplify
111
+ "RUF", # ruff-specific
112
+ ]
113
+
114
+ [tool.ruff.lint.isort]
115
+ known-first-party = ["app"]
116
+ ```
117
+
118
+ ### 命名规范
119
+
120
+ | 类型 | 风格 | 示例 |
121
+ |---|---|---|
122
+ | 模块/包 | `snake_case` | `user_service.py` |
123
+ | 函数/方法 | `snake_case` | `get_user_by_id()` |
124
+ | 变量 | `snake_case` | `user_count` |
125
+ | 类 | `PascalCase` | `UserService` |
126
+ | 常量 | `UPPER_SNAKE_CASE` | `MAX_RETRY_COUNT` |
127
+ | 私有属性/方法 | `_leading_underscore` | `_parse_token()` |
128
+ | 类型变量 | `PascalCase` | `T = TypeVar("T")` |
129
+ | 协议/抽象基类 | `PascalCase` + 语义后缀 | `Serializable`、`BaseRepository` |
130
+ | 路由函数 | `snake_case` 动词开头 | `create_user()`、`list_orders()` |
131
+ | Pydantic Schema | `PascalCase` + 用途后缀 | `UserCreate`、`UserResponse`、`UserUpdate` |
132
+
133
+ ### 禁止的写法
134
+
135
+ ```python
136
+ # ❌ 单字母变量(循环索引除外)
137
+ d = get_data()
138
+
139
+ # ✅ 有意义的名称
140
+ user_data = get_data()
141
+
142
+ # ❌ 匈牙利命名法
143
+ str_name = "Alice"
144
+ lst_users = []
145
+
146
+ # ✅ 直接命名
147
+ name = "Alice"
148
+ users = []
149
+
150
+ # ❌ 布尔变量不带 is/has/can/should 前缀
151
+ active = True
152
+
153
+ # ✅ 布尔变量语义明确
154
+ is_active = True
155
+ has_permission = False
156
+ ```
157
+
158
+ ## 四、类型标注
159
+
160
+ ### 强制规则
161
+ - 所有公开函数/方法**必须**有参数和返回值类型标注
162
+ - 私有函数**推荐**有类型标注
163
+ - 类属性**必须**有类型标注
164
+
165
+ ```python
166
+ # ❌ 缺少类型标注
167
+ def get_user(user_id):
168
+ ...
169
+
170
+ # ✅ 完整标注
171
+ def get_user(user_id: int) -> User | None:
172
+ ...
173
+
174
+ # ✅ 复杂类型使用 TypeAlias
175
+ type UserMap = dict[str, list[User]]
176
+
177
+ # ✅ 数据类必须标注
178
+ @dataclass
179
+ class UserProfile:
180
+ name: str
181
+ age: int
182
+ email: str | None = None
183
+ ```
184
+
185
+ ### 禁止 `Any`
186
+ - 不得使用 `Any` 作为"懒标注",除非与外部动态库交互且无法确定类型
187
+ - 使用 `Any` 时必须附注释说明原因
188
+
189
+ ## 五、FastAPI 路由与接口设计
190
+
191
+ ### 路由组织
192
+
193
+ ```python
194
+ # routers/user.py
195
+ from fastapi import APIRouter, Depends, status
196
+
197
+ router = APIRouter(prefix="/users", tags=["users"])
198
+
199
+ @router.post("/", status_code=status.HTTP_201_CREATED, response_model=UserResponse)
200
+ async def create_user(
201
+ body: UserCreate,
202
+ service: UserService = Depends(get_user_service),
203
+ ) -> UserResponse:
204
+ return await service.create(body)
205
+ ```
206
+
207
+ ### 强制规则
208
+ - 每个路由模块使用独立 `APIRouter`,在 `main.py` 中统一注册
209
+ - **禁止**所有路由写在 `main.py` 中(超过 2 个路由必须拆分)
210
+ - 路由函数只做:参数接收 → 调用 service → 返回结果,**不写业务逻辑**
211
+ - 必须显式声明 `status_code` 和 `response_model`
212
+ - 路由路径使用复数名词 + RESTful 风格
213
+
214
+ ### RESTful 路由规范
215
+
216
+ | 操作 | 方法 | 路径 | 状态码 |
217
+ |---|---|---|---|
218
+ | 列表查询 | `GET` | `/users` | 200 |
219
+ | 详情查询 | `GET` | `/users/{user_id}` | 200 |
220
+ | 创建 | `POST` | `/users` | 201 |
221
+ | 全量更新 | `PUT` | `/users/{user_id}` | 200 |
222
+ | 部分更新 | `PATCH` | `/users/{user_id}` | 200 |
223
+ | 删除 | `DELETE` | `/users/{user_id}` | 204 |
224
+
225
+ ### 分页查询
226
+
227
+ ```python
228
+ # schemas/common.py
229
+ class PaginationParams(BaseModel):
230
+ page: int = Field(default=1, ge=1)
231
+ page_size: int = Field(default=20, ge=1, le=100)
232
+
233
+ class PaginatedResponse[T](BaseModel):
234
+ items: list[T]
235
+ total: int
236
+ page: int
237
+ page_size: int
238
+
239
+ # routers/user.py
240
+ @router.get("/", response_model=PaginatedResponse[UserResponse])
241
+ async def list_users(
242
+ pagination: PaginationParams = Depends(),
243
+ service: UserService = Depends(get_user_service),
244
+ ) -> PaginatedResponse[UserResponse]:
245
+ return await service.list(pagination)
246
+ ```
247
+
248
+ ## 六、Pydantic Schema 规范
249
+
250
+ ### 强制规则
251
+ - 请求和响应使用不同的 Schema,**禁止**复用同一个 Model
252
+
253
+ ```python
254
+ # schemas/user.py
255
+
256
+ class UserBase(BaseModel):
257
+ """共享字段基类"""
258
+ name: str = Field(max_length=50)
259
+ email: EmailStr
260
+
261
+ class UserCreate(UserBase):
262
+ """创建请求"""
263
+ password: str = Field(min_length=8)
264
+
265
+ class UserUpdate(BaseModel):
266
+ """更新请求 - 所有字段可选"""
267
+ name: str | None = Field(default=None, max_length=50)
268
+ email: EmailStr | None = None
269
+
270
+ class UserResponse(UserBase):
271
+ """响应"""
272
+ id: int
273
+ is_active: bool
274
+ created_at: datetime
275
+
276
+ model_config = ConfigDict(from_attributes=True)
277
+ ```
278
+
279
+ - **禁止**在响应 Schema 中暴露密码、内部 ID、敏感字段
280
+ - 使用 `Field()` 添加校验约束(`min_length`、`ge`、`le`、`pattern` 等)
281
+ - 更新 Schema 的字段必须全部可选(`None` 默认值)
282
+
283
+ ## 七、依赖注入
284
+
285
+ ### 强制规则
286
+ - 数据库会话、当前用户、配置等通过 `Depends()` 注入
287
+ - **禁止**在路由函数中直接创建数据库会话或实例化 service
288
+
289
+ ```python
290
+ # dependencies.py
291
+ from collections.abc import AsyncGenerator
292
+
293
+ async def get_db() -> AsyncGenerator[AsyncSession]:
294
+ async with async_session_factory() as session:
295
+ yield session
296
+
297
+ async def get_current_user(
298
+ token: str = Depends(oauth2_scheme),
299
+ db: AsyncSession = Depends(get_db),
300
+ ) -> User:
301
+ user = await authenticate(token, db)
302
+ if not user:
303
+ raise HTTPException(status_code=401, detail="认证失败")
304
+ return user
305
+
306
+ def get_user_service(db: AsyncSession = Depends(get_db)) -> UserService:
307
+ return UserService(db)
308
+ ```
309
+
310
+ ```python
311
+ # ❌ 路由中直接创建会话
312
+ @router.get("/users/{user_id}")
313
+ async def get_user(user_id: int):
314
+ async with async_session_factory() as db:
315
+ user = await db.get(User, user_id)
316
+ return user
317
+
318
+ # ✅ 依赖注入
319
+ @router.get("/users/{user_id}", response_model=UserResponse)
320
+ async def get_user(
321
+ user_id: int,
322
+ service: UserService = Depends(get_user_service),
323
+ ) -> UserResponse:
324
+ return await service.get_by_id(user_id)
325
+ ```
326
+
327
+ ## 八、异常处理
328
+
329
+ ### 统一异常体系
330
+
331
+ ```python
332
+ # exceptions.py
333
+ from fastapi import FastAPI, Request, status
334
+ from fastapi.responses import JSONResponse
335
+
336
+ # 业务异常基类
337
+ class AppError(Exception):
338
+ def __init__(self, message: str, code: str = "UNKNOWN_ERROR") -> None:
339
+ self.message = message
340
+ self.code = code
341
+
342
+ class NotFoundError(AppError):
343
+ def __init__(self, resource: str, resource_id: int | str) -> None:
344
+ super().__init__(
345
+ message=f"{resource} 不存在",
346
+ code="NOT_FOUND",
347
+ )
348
+ self.resource = resource
349
+ self.resource_id = resource_id
350
+
351
+ class BusinessError(AppError):
352
+ """业务规则校验失败"""
353
+ pass
354
+
355
+ class PermissionDeniedError(AppError):
356
+ """权限不足"""
357
+ pass
358
+
359
+ # 注册异常处理器
360
+ def register_exception_handlers(app: FastAPI) -> None:
361
+ @app.exception_handler(NotFoundError)
362
+ async def not_found_handler(request: Request, exc: NotFoundError) -> JSONResponse:
363
+ return JSONResponse(
364
+ status_code=status.HTTP_404_NOT_FOUND,
365
+ content={"code": exc.code, "message": exc.message},
366
+ )
367
+
368
+ @app.exception_handler(BusinessError)
369
+ async def business_error_handler(request: Request, exc: BusinessError) -> JSONResponse:
370
+ return JSONResponse(
371
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
372
+ content={"code": exc.code, "message": exc.message},
373
+ )
374
+
375
+ @app.exception_handler(PermissionDeniedError)
376
+ async def permission_denied_handler(request: Request, exc: PermissionDeniedError) -> JSONResponse:
377
+ return JSONResponse(
378
+ status_code=status.HTTP_403_FORBIDDEN,
379
+ content={"code": exc.code, "message": exc.message},
380
+ )
381
+ ```
382
+
383
+ ### 强制规则
384
+ - **禁止**在 service 层抛出 `HTTPException`(service 不感知 HTTP)
385
+ - service 层抛出业务异常,由异常处理器统一转换为 HTTP 响应
386
+ - **禁止**裸 `except:` 或 `except Exception: pass`
387
+ - 错误响应格式统一为 `{"code": "ERROR_CODE", "message": "描述"}`
388
+ - **禁止**在错误消息中回显用户输入
389
+
390
+ ```python
391
+ # ❌ service 中抛 HTTPException
392
+ class UserService:
393
+ async def get_by_id(self, user_id: int) -> User:
394
+ user = await self.db.get(User, user_id)
395
+ if not user:
396
+ raise HTTPException(status_code=404) # 耦合 HTTP
397
+ return user
398
+
399
+ # ✅ service 中抛业务异常
400
+ class UserService:
401
+ async def get_by_id(self, user_id: int) -> User:
402
+ user = await self.db.get(User, user_id)
403
+ if not user:
404
+ raise NotFoundError("用户", user_id)
405
+ return user
406
+ ```
407
+
408
+ ## 九、数据库与 ORM
409
+
410
+ ### 强制规则
411
+ - 使用 **SQLAlchemy 2.0** 风格(声明式映射 + async)
412
+ - 数据库迁移使用 **Alembic**,**禁止**手动执行 DDL
413
+ - **禁止**在路由层直接写 SQL 查询
414
+
415
+ ```python
416
+ # models/base.py
417
+ from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
418
+ from sqlalchemy import func
419
+ from datetime import datetime
420
+
421
+ class Base(DeclarativeBase):
422
+ pass
423
+
424
+ class TimestampMixin:
425
+ created_at: Mapped[datetime] = mapped_column(server_default=func.now())
426
+ updated_at: Mapped[datetime] = mapped_column(server_default=func.now(), onupdate=func.now())
427
+
428
+ # models/user.py
429
+ class User(Base, TimestampMixin):
430
+ __tablename__ = "users"
431
+
432
+ id: Mapped[int] = mapped_column(primary_key=True)
433
+ name: Mapped[str] = mapped_column(String(50))
434
+ email: Mapped[str] = mapped_column(String(100), unique=True, index=True)
435
+ hashed_password: Mapped[str] = mapped_column(String(128))
436
+ is_active: Mapped[bool] = mapped_column(default=True)
437
+ ```
438
+
439
+ ### 异步数据库会话
440
+
441
+ ```python
442
+ # config.py / database.py
443
+ from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
444
+
445
+ engine = create_async_engine(settings.database_url, echo=settings.debug)
446
+ async_session_factory = async_sessionmaker(engine, expire_on_commit=False)
447
+ ```
448
+
449
+ ### 查询规范
450
+
451
+ ```python
452
+ # ❌ 旧式查询
453
+ session.query(User).filter(User.id == user_id).first()
454
+
455
+ # ✅ SQLAlchemy 2.0 风格
456
+ result = await session.execute(select(User).where(User.id == user_id))
457
+ user = result.scalar_one_or_none()
458
+ ```
459
+
460
+ ## 十、异步编程
461
+
462
+ ### 强制规则
463
+ - FastAPI 路由函数统一使用 `async def`
464
+ - 异步函数内**禁止**调用同步阻塞操作(IO、网络、sleep)
465
+ - 使用 `asyncio.to_thread()` 包装不可避免的同步调用
466
+
467
+ ```python
468
+ # ❌ 异步函数中调用同步阻塞
469
+ async def get_data():
470
+ result = requests.get(url) # 阻塞事件循环
471
+ data = open("file.txt").read() # 阻塞事件循环
472
+
473
+ # ✅ 使用异步库
474
+ async def get_data():
475
+ async with httpx.AsyncClient() as client:
476
+ result = await client.get(url)
477
+ data = await asyncio.to_thread(Path("file.txt").read_text)
478
+ ```
479
+
480
+ - HTTP 客户端使用 `httpx`,**禁止** `requests`
481
+ - 数据库使用异步驱动(`asyncpg`、`aiomysql`)
482
+
483
+ ## 十一、中间件与生命周期
484
+
485
+ ### 强制规则
486
+
487
+ ```python
488
+ # main.py
489
+ from contextlib import asynccontextmanager
490
+ from collections.abc import AsyncGenerator
491
+
492
+ @asynccontextmanager
493
+ async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
494
+ # 启动时执行
495
+ logger.info("应用启动")
496
+ yield
497
+ # 关闭时执行
498
+ await engine.dispose()
499
+ logger.info("应用关闭")
500
+
501
+ app = FastAPI(
502
+ title="My API",
503
+ version="1.0.0",
504
+ lifespan=lifespan,
505
+ )
506
+ ```
507
+
508
+ - 使用 `lifespan` 管理应用生命周期,**禁止** `@app.on_event("startup")`(已废弃)
509
+ - CORS 中间件必须明确指定允许的域名,**禁止**生产环境使用 `allow_origins=["*"]`
510
+
511
+ ```python
512
+ from fastapi.middleware.cors import CORSMiddleware
513
+
514
+ app.add_middleware(
515
+ CORSMiddleware,
516
+ allow_origins=settings.allowed_origins, # 从环境变量读取
517
+ allow_credentials=True,
518
+ allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
519
+ allow_headers=["*"],
520
+ )
521
+ ```
522
+
523
+ ## 十二、配置管理
524
+
525
+ ### 强制规则
526
+ - 使用 Pydantic Settings 管理配置
527
+
528
+ ```python
529
+ # config.py
530
+ from pydantic_settings import BaseSettings, SettingsConfigDict
531
+
532
+ class Settings(BaseSettings):
533
+ # 应用
534
+ app_name: str = "My API"
535
+ debug: bool = False
536
+
537
+ # 数据库
538
+ database_url: str
539
+ db_pool_size: int = 5
540
+
541
+ # Redis
542
+ redis_url: str = "redis://localhost:6379"
543
+
544
+ # 认证
545
+ secret_key: str
546
+ access_token_expire_minutes: int = 30
547
+
548
+ # CORS
549
+ allowed_origins: list[str] = ["http://localhost:3000"]
550
+
551
+ model_config = SettingsConfigDict(
552
+ env_file=".env",
553
+ env_file_encoding="utf-8",
554
+ )
555
+
556
+ settings = Settings()
557
+ ```
558
+
559
+ - **禁止**在代码中硬编码配置值(数据库地址、密钥、端口等)
560
+ - **禁止**在模块顶层执行副作用(网络请求、数据库连接、文件读取)
561
+
562
+ ## 十三、日志
563
+
564
+ ### 强制规则
565
+ - 使用标准库 `logging` 或 `structlog`,**禁止** `print()` 出现在非调试代码中
566
+ - 使用 `%s` 占位符或 structlog 的结构化日志,**禁止** f-string 拼接日志
567
+
568
+ ```python
569
+ # ❌ print 调试
570
+ print(f"User {user_id} not found")
571
+
572
+ # ❌ f-string 日志(参数不会延迟求值,且无法被日志聚合工具解析)
573
+ logger.info(f"User {user_id} created")
574
+
575
+ # ✅ 占位符日志
576
+ logger.info("User %s created", user_id)
577
+
578
+ # ✅ structlog
579
+ logger.info("user_created", user_id=user_id)
580
+ ```
581
+
582
+ ### 日志级别使用规范
583
+
584
+ | 级别 | 使用场景 |
585
+ |---|---|
586
+ | `DEBUG` | 开发调试信息,生产环境不输出 |
587
+ | `INFO` | 关键业务流程节点(用户注册、订单创建等) |
588
+ | `WARNING` | 异常但可恢复的情况(重试、降级) |
589
+ | `ERROR` | 错误但服务仍可用(单次请求失败) |
590
+ | `CRITICAL` | 服务不可用(数据库连接断开、必要服务宕机) |
591
+
592
+ ## 十四、测试
593
+
594
+ ### 强制规则
595
+ - 使用 `pytest` + `pytest-asyncio` + `httpx`
596
+ - 使用 `TestClient` 或 `AsyncClient` 测试 API
597
+
598
+ ```python
599
+ # conftest.py
600
+ import pytest
601
+ from httpx import ASGITransport, AsyncClient
602
+ from app.main import app
603
+
604
+ @pytest.fixture
605
+ async def client():
606
+ async with AsyncClient(
607
+ transport=ASGITransport(app=app),
608
+ base_url="http://test",
609
+ ) as ac:
610
+ yield ac
611
+
612
+ # test_user.py
613
+ @pytest.mark.anyio
614
+ async def test_create_user_returns_201(client: AsyncClient):
615
+ response = await client.post("/users/", json={
616
+ "name": "Alice",
617
+ "email": "alice@example.com",
618
+ "password": "securepass123",
619
+ })
620
+ assert response.status_code == 201
621
+ data = response.json()
622
+ assert data["name"] == "Alice"
623
+ assert "password" not in data # 响应不包含密码
624
+
625
+ @pytest.mark.anyio
626
+ async def test_get_nonexistent_user_returns_404(client: AsyncClient):
627
+ response = await client.get("/users/99999")
628
+ assert response.status_code == 404
629
+ ```
630
+
631
+ - 测试函数名描述行为而非实现
632
+ - 测试数据使用 `pytest.fixture`,不要硬编码
633
+ - 测试结构遵循 Arrange-Act-Assert
634
+
635
+ ## 十五、函数与类设计
636
+
637
+ ### 强制规则
638
+
639
+ ```python
640
+ # ❌ 超过 3 个位置参数
641
+ def create_user(name, age, email, role, department):
642
+ ...
643
+
644
+ # ✅ 使用关键字参数或 Pydantic model
645
+ def create_user(request: CreateUserRequest) -> User:
646
+ ...
647
+ ```
648
+
649
+ - 函数体不超过 50 行(超过则拆分)
650
+ - 单个文件不超过 500 行(超过则拆分模块)
651
+ - 嵌套不超过 3 层(使用 early return 降低嵌套)
652
+
653
+ ```python
654
+ # ❌ 深层嵌套
655
+ def process(data):
656
+ if data:
657
+ if data.is_valid:
658
+ if data.has_items:
659
+ for item in data.items:
660
+ ...
661
+
662
+ # ✅ 提前返回
663
+ def process(data):
664
+ if not data:
665
+ return
666
+ if not data.is_valid:
667
+ return
668
+ if not data.has_items:
669
+ return
670
+ for item in data.items:
671
+ ...
672
+ ```
673
+
674
+ ## 十六、数据校验与序列化
675
+
676
+ ### 强制规则
677
+ - API 输入/输出使用 **Pydantic v2** 进行校验
678
+ - 内部数据结构优先使用 `dataclass`,需要校验时用 Pydantic
679
+ - **禁止**在业务逻辑中手动校验字典字段
680
+
681
+ ```python
682
+ # ❌ 手动校验字典
683
+ def create_user(data: dict):
684
+ if "name" not in data:
685
+ raise ValueError("name required")
686
+
687
+ # ✅ Pydantic 自动校验
688
+ class CreateUserRequest(BaseModel):
689
+ name: str
690
+ age: int = Field(ge=0, le=150)
691
+ email: EmailStr
692
+ ```
693
+
694
+ ## 十七、常见反模式(禁止)
695
+
696
+ | 反模式 | 正确做法 |
697
+ |---|---|
698
+ | 可变默认参数 `def f(items=[])` | `def f(items: list \| None = None)` |
699
+ | `import *` | 显式导入 |
700
+ | 循环导入 | 重构模块或延迟导入 |
701
+ | 全局可变状态 | 依赖注入 |
702
+ | `os.path` 字符串拼接 | `pathlib.Path` |
703
+ | `datetime.now()` 无时区 | `datetime.now(tz=UTC)` |
704
+ | 手动拼接 SQL | ORM 或参数化查询 |
705
+ | `== True` / `== None` | `is True` / `is None` |
706
+ | `len(x) == 0` | `not x` |
707
+ | `type(x) == SomeType` | `isinstance(x, SomeType)` |
708
+ | `dict.keys()` 遍历 | 直接 `for k in dict` |
709
+ | 嵌套字典传递数据 | `dataclass` / `TypedDict` / Pydantic model |
710
+ | `@app.on_event("startup")` | `lifespan` context manager |
711
+ | service 层抛 `HTTPException` | 抛业务异常,异常处理器转换 |
712
+ | 路由里写数据库查询 | 调用 service 层 |
713
+ | `allow_origins=["*"]`(生产) | 明确列出允许的域名 |
714
+
715
+ ## 十八、注释与文档
716
+
717
+ ### 强制规则
718
+ - 注释用于解释 **为什么(why)**,而非 **做了什么(what)**
719
+ - 每个 `.py` 文件**必须**在顶部添加模块级 docstring,说明该文件的主要功能
720
+ - 所有类**必须**有 docstring,说明类的职责和用途
721
+ - 所有方法/函数(包括公开和私有)**必须**有 docstring,说明功能、参数和返回值
722
+ - docstring 统一使用 **Google 风格**
723
+
724
+ ### 模块级 docstring
725
+
726
+ 每个 `.py` 文件的第一行必须是模块 docstring:
727
+
728
+ ```python
729
+ # ✅ 模块 docstring
730
+ """用户相关的路由模块。
731
+
732
+ 提供用户的 CRUD 接口,包括注册、查询、更新和删除。
733
+ """
734
+
735
+ from fastapi import APIRouter
736
+ ...
737
+ ```
738
+
739
+ ```python
740
+ # ❌ 缺少模块 docstring
741
+ from fastapi import APIRouter
742
+ ...
743
+ ```
744
+
745
+ ### 类 docstring
746
+
747
+ ```python
748
+ # ✅ 类 docstring
749
+ class UserService:
750
+ """用户业务逻辑服务。
751
+
752
+ 负责用户的创建、查询、更新和删除操作,
753
+ 封装所有与用户相关的业务规则。
754
+ """
755
+
756
+ def __init__(self, db: AsyncSession) -> None:
757
+ """初始化 UserService。
758
+
759
+ Args:
760
+ db: 异步数据库会话。
761
+ """
762
+ self.db = db
763
+ ```
764
+
765
+ ```python
766
+ # ❌ 缺少类 docstring
767
+ class UserService:
768
+ def __init__(self, db: AsyncSession) -> None:
769
+ self.db = db
770
+ ```
771
+
772
+ ### 方法/函数 docstring
773
+
774
+ ```python
775
+ # ✅ 完整的 Google 风格 docstring
776
+ async def get_by_id(self, user_id: int) -> User:
777
+ """根据 ID 查询用户。
778
+
779
+ Args:
780
+ user_id: 用户唯一标识。
781
+
782
+ Returns:
783
+ 匹配的用户对象。
784
+
785
+ Raises:
786
+ NotFoundError: 用户不存在时抛出。
787
+ """
788
+ user = await self.db.get(User, user_id)
789
+ if not user:
790
+ raise NotFoundError("用户", user_id)
791
+ return user
792
+ ```
793
+
794
+ ```python
795
+ # ✅ 简单函数可以使用单行 docstring
796
+ def _build_cache_key(self, user_id: int) -> str:
797
+ """根据用户 ID 构建缓存键。"""
798
+ return f"user:{user_id}"
799
+ ```
800
+
801
+ ```python
802
+ # ❌ 缺少 docstring
803
+ async def get_by_id(self, user_id: int) -> User:
804
+ user = await self.db.get(User, user_id)
805
+ if not user:
806
+ raise NotFoundError("用户", user_id)
807
+ return user
808
+ ```
809
+
810
+ ### docstring 规范
811
+
812
+ | 场景 | 要求 |
813
+ |---|---|
814
+ | 单行可描述清楚 | 使用单行 docstring:`"""根据用户 ID 构建缓存键。"""` |
815
+ | 有参数/返回值/异常 | 使用多行 Google 风格,包含 `Args`/`Returns`/`Raises` |
816
+ | `__init__` 方法 | 必须有 docstring,说明初始化参数 |
817
+ | 魔术方法(`__str__` 等) | 必须有 docstring,说明行为 |
818
+ | 私有方法 | 必须有 docstring,至少单行说明 |
819
+
820
+ ### 行内注释
821
+ ```python
822
+ # ❌ 无用注释
823
+ # 获取用户
824
+ user = get_user(user_id)
825
+
826
+ # ✅ 解释原因
827
+ # 此处使用缓存查询,因为用户表 QPS 超过 5000
828
+ user = cache.get_user(user_id)
829
+ ```
830
+
831
+ ### API 文档
832
+ - 路由函数的 docstring 会自动生成 OpenAPI 描述,保持简洁
833
+ - 使用 `summary` 参数替代 docstring 标题
834
+
835
+ ```python
836
+ @router.post(
837
+ "/",
838
+ status_code=status.HTTP_201_CREATED,
839
+ response_model=UserResponse,
840
+ summary="创建用户",
841
+ )
842
+ async def create_user(body: UserCreate) -> UserResponse:
843
+ """根据提交的信息创建新用户,邮箱不可重复。"""
844
+ ...
845
+ ```
846
+
847
+ ## 十九、安全编码
848
+
849
+ ### 强制规则
850
+ - SQL 查询必须使用参数化绑定,**禁止**字符串拼接
851
+ - 用户输入必须校验后使用,**禁止**直接信任
852
+ - 文件路径操作必须防止路径遍历攻击
853
+ - 序列化/反序列化**禁止** `pickle.loads()` 处理不可信数据
854
+ - 密码存储必须使用 `bcrypt` / `argon2`,**禁止**明文或 MD5/SHA
855
+ - JWT 密钥从环境变量读取,**禁止**硬编码
856
+
857
+ ```python
858
+ # ❌ SQL 注入
859
+ cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")
860
+
861
+ # ✅ 参数化查询
862
+ cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
863
+
864
+ # ❌ 路径遍历
865
+ file_path = f"/uploads/{user_input_filename}"
866
+
867
+ # ✅ 路径安全校验
868
+ safe_path = Path("/uploads") / Path(user_input_filename).name
869
+
870
+ # ❌ 硬编码密钥
871
+ SECRET_KEY = "my-super-secret-key"
872
+
873
+ # ✅ 环境变量
874
+ SECRET_KEY = settings.secret_key
875
+ ```