create-dwy 0.5.0 → 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.
Files changed (156) hide show
  1. package/bin/index.js +1 -1
  2. package/package.json +2 -2
  3. package/src/create.js +61 -37
  4. package/src/sync.js +213 -53
  5. package/templates/claude-global/CLAUDE.md +92 -0
  6. package/templates/claude-global/rules/dwy-android-core.md +610 -0
  7. package/templates/claude-global/rules/dwy-api-design.md +110 -0
  8. package/templates/claude-global/rules/dwy-db-migration.md +46 -0
  9. package/templates/claude-global/rules/dwy-flutter-core.md +564 -0
  10. package/templates/claude-global/rules/dwy-flutter-network.md +222 -0
  11. package/templates/claude-global/rules/dwy-flutter-router.md +97 -0
  12. package/templates/claude-global/rules/dwy-flutter-testing.md +94 -0
  13. package/templates/claude-global/rules/dwy-git-commit-convention.md +82 -0
  14. package/templates/claude-global/rules/dwy-git-security.md +53 -0
  15. package/templates/claude-global/rules/dwy-payload-limits.md +178 -0
  16. package/templates/claude-global/rules/{prefer-open-source.md → dwy-prefer-open-source.md} +4 -0
  17. package/templates/claude-global/rules/dwy-project-structure.md +181 -0
  18. package/templates/claude-global/rules/{python-code-style.md → dwy-python-core.md} +314 -357
  19. package/templates/claude-global/rules/dwy-python-fastapi.md +272 -0
  20. package/templates/claude-global/rules/dwy-python-orm.md +61 -0
  21. package/templates/claude-global/rules/{backend-security.md → dwy-python-security.md} +116 -79
  22. package/templates/claude-global/rules/dwy-python-testing.md +53 -0
  23. package/templates/claude-global/rules/{server-security.md → dwy-server-security.md} +1 -2
  24. package/templates/claude-global/rules/dwy-tdd-development.md +70 -0
  25. package/templates/claude-global/rules/dwy-tech-stack.md +60 -0
  26. package/templates/claude-global/rules/dwy-vue-api.md +146 -0
  27. package/templates/claude-global/rules/{vue-code-style.md → dwy-vue-core.md} +361 -152
  28. package/templates/claude-global/rules/dwy-vue-pinia.md +75 -0
  29. package/templates/claude-global/rules/dwy-vue-testing.md +102 -0
  30. package/templates/claude-global/settings.json +20 -0
  31. package/templates/claude-global/skills/dwy-deploy-audit/SKILL.md +481 -0
  32. package/templates/claude-global/skills/dwy-deploy-audit/references/checks-data.md +117 -0
  33. package/templates/claude-global/skills/dwy-deploy-audit/references/checks-network.md +74 -0
  34. package/templates/claude-global/skills/dwy-deploy-audit/references/checks-runtime.md +184 -0
  35. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_base.sh +39 -0
  36. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_capacity.sh +278 -0
  37. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_db.sh +122 -0
  38. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_docker.sh +249 -0
  39. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_env.sh +64 -0
  40. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_https.sh +60 -0
  41. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_logs.sh +209 -0
  42. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_nginx.sh +92 -0
  43. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_resilience.sh +157 -0
  44. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_secrets.sh +327 -0
  45. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_services.sh +77 -0
  46. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_ssh.sh +56 -0
  47. package/templates/claude-global/skills/dwy-deploy-audit/scripts/run_all.sh +105 -0
  48. package/templates/claude-global/skills/dwy-docker/SKILL.md +187 -0
  49. package/templates/claude-global/skills/dwy-docker/references/ask-templates.md +142 -0
  50. package/templates/claude-global/skills/dwy-docker/references/templates.md +479 -0
  51. package/templates/claude-global/skills/dwy-docker/references/version-rules.md +76 -0
  52. package/templates/claude-global/skills/dwy-docker/scripts/query_dockerhub.py +204 -0
  53. package/templates/claude-global/skills/dwy-docker/scripts/scan_images.sh +185 -0
  54. package/templates/claude-global/skills/dwy-eapi/SKILL.md +433 -0
  55. package/templates/claude-global/skills/dwy-eapi/references/tasks-integration-guide.md +387 -0
  56. package/templates/claude-global/skills/dwy-ekit/SKILL.md +85 -0
  57. package/templates/claude-global/skills/dwy-ekit/references/data.md +135 -0
  58. package/templates/claude-global/skills/dwy-ekit/references/form.md +68 -0
  59. package/templates/claude-global/skills/dwy-ekit/references/request.md +183 -0
  60. package/templates/claude-global/skills/dwy-ekit/references/ui.md +109 -0
  61. package/templates/claude-global/skills/{dwy-frontend-eui → dwy-eui}/SKILL.md +129 -15
  62. package/templates/claude-global/skills/dwy-eui/references/eui-design-guide.md +257 -0
  63. package/templates/claude-global/skills/dwy-eui/references/eui-integration-guide.md +302 -0
  64. package/templates/claude-global/skills/dwy-eui/references/eui-landing-design-guide.md +437 -0
  65. package/templates/claude-global/skills/dwy-fullstack-scaffold/SKILL.md +214 -0
  66. package/templates/claude-global/skills/dwy-mirror-source/SKILL.md +275 -0
  67. package/templates/claude-global/skills/dwy-mirror-source/references/dockerfile-snippets.md +228 -0
  68. package/templates/claude-global/skills/dwy-mirror-source/references/mirror-providers.md +150 -0
  69. package/templates/claude-global/skills/dwy-mirror-source/scripts/__pycache__/check_mirrors.cpython-313.pyc +0 -0
  70. package/templates/claude-global/skills/dwy-mirror-source/scripts/apply_mirrors.py +666 -0
  71. package/templates/claude-global/skills/dwy-mirror-source/scripts/check_mirrors.py +868 -0
  72. package/templates/claude-global/skills/dwy-pentest/SKILL.md +344 -0
  73. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__init__.py +1 -0
  74. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/__init__.cpython-311.pyc +0 -0
  75. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/config.cpython-311.pyc +0 -0
  76. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/executor.cpython-311.pyc +0 -0
  77. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/severity.cpython-311.pyc +0 -0
  78. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/validator.cpython-311.pyc +0 -0
  79. package/templates/claude-global/skills/dwy-pentest/scripts/lib/config.py +109 -0
  80. package/templates/claude-global/skills/dwy-pentest/scripts/lib/executor.py +157 -0
  81. package/templates/claude-global/skills/dwy-pentest/scripts/lib/severity.py +96 -0
  82. package/templates/claude-global/skills/dwy-pentest/scripts/lib/validator.py +113 -0
  83. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_auth.py +289 -0
  84. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_recon.py +217 -0
  85. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_report.py +343 -0
  86. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_scan.py +205 -0
  87. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_ssl.py +208 -0
  88. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_vuln.py +205 -0
  89. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_web.py +330 -0
  90. package/templates/claude-global/skills/dwy-pentest/scripts/pyproject.toml +14 -0
  91. package/templates/claude-global/skills/dwy-pentest/scripts/setup_tools.sh +97 -0
  92. package/templates/claude-global/skills/dwy-pentest/scripts/uv.lock +415 -0
  93. package/templates/claude-global/skills/dwy-publish/SKILL.md +135 -0
  94. package/templates/claude-global/skills/dwy-vue-core/SKILL.md +890 -0
  95. package/templates/project/backend/.dwy.hbs +5 -0
  96. package/templates/project/backend/.python-version +1 -0
  97. package/templates/project/backend/CLAUDE.md.hbs +40 -0
  98. package/templates/project/{web/backend → backend}/alembic/script.py.mako +2 -0
  99. package/templates/project/{web/backend → backend}/alembic.ini.hbs +0 -1
  100. package/templates/project/backend/deploy.sh.hbs +18 -0
  101. package/templates/project/{web → backend}/docker-compose.prod.yml.hbs +1 -1
  102. package/templates/project/{web/backend → backend}/pyproject.toml.hbs +4 -5
  103. package/templates/project/{web/backend → backend}/src/app/config.py.hbs +1 -1
  104. package/templates/project/backend/src/app/database.py +10 -0
  105. package/templates/project/backend/src/app/dependencies.py +5 -0
  106. package/templates/project/{web/backend → backend}/src/app/main.py.hbs +3 -3
  107. package/templates/project/backend/src/app/models/base.py +5 -0
  108. package/templates/project/web/CLAUDE.md.hbs +29 -36
  109. package/templates/project/web/{frontend → apps/web}/index.html.hbs +1 -0
  110. package/templates/project/web/{frontend → apps/web}/package.json.hbs +4 -3
  111. package/templates/project/web/apps/web/src/App.vue +9 -0
  112. package/templates/project/web/apps/web/src/api/client.ts +5 -0
  113. package/templates/project/web/apps/web/src/composables/.gitkeep +0 -0
  114. package/templates/project/web/apps/web/src/layouts/DefaultLayout.vue +8 -0
  115. package/templates/project/web/{frontend → apps/web}/src/router/index.ts +1 -1
  116. package/templates/project/web/{frontend → apps/web}/src/stores/auth.ts +4 -2
  117. package/templates/project/web/apps/web/src/style.css +2 -0
  118. package/templates/project/web/apps/web/src/types/router.d.ts +8 -0
  119. package/templates/project/web/apps/web/src/views/HomeView.vue +8 -0
  120. package/templates/project/web/apps/web/tsconfig.app.json +18 -0
  121. package/templates/project/web/apps/web/tsconfig.json +6 -0
  122. package/templates/project/web/{frontend → apps/web}/vite.config.ts.hbs +7 -1
  123. package/templates/project/web/package.json.hbs +16 -0
  124. package/templates/project/web/packages/.gitkeep +0 -0
  125. package/templates/project/web/pnpm-workspace.yaml +3 -0
  126. package/templates/claude-global/rules/agent-team-execution.md +0 -138
  127. package/templates/claude-global/rules/agent-team-patterns.md +0 -157
  128. package/templates/claude-global/rules/git-security.md +0 -144
  129. package/templates/claude-global/skills/dwy-auto-sync/SKILL.md +0 -52
  130. package/templates/claude-global/skills/dwy-backend-base/SKILL.md +0 -279
  131. package/templates/claude-global/skills/dwy-frontend-core/SKILL.md +0 -181
  132. package/templates/project/web/.claude/commands/deploy.md +0 -4
  133. package/templates/project/web/backend/src/app/models/base.py +0 -5
  134. package/templates/project/web/deploy.sh.hbs +0 -113
  135. package/templates/project/web/frontend/src/App.vue +0 -3
  136. package/templates/project/web/frontend/src/api/client.ts +0 -13
  137. package/templates/project/web/frontend/src/style.css +0 -1
  138. package/templates/project/web/frontend/src/views/HomeView.vue +0 -5
  139. package/templates/project/web/frontend/tsconfig.app.json +0 -14
  140. package/templates/project/web/frontend/tsconfig.json +0 -4
  141. /package/templates/{project/web/backend → claude-global/skills/dwy-pentest/scripts}/.python-version +0 -0
  142. /package/templates/project/{web/backend → backend}/Dockerfile +0 -0
  143. /package/templates/project/{web/backend → backend}/alembic/env.py.hbs +0 -0
  144. /package/templates/{claude-global/skills → project/backend/alembic/versions}/.gitkeep +0 -0
  145. /package/templates/project/{web → backend}/docker-compose.dev.yml.hbs +0 -0
  146. /package/templates/project/{web/backend → backend}/src/app/__init__.py +0 -0
  147. /package/templates/project/{web/backend → backend}/src/app/models/__init__.py +0 -0
  148. /package/templates/project/{web/backend → backend}/src/app/routers/__init__.py +0 -0
  149. /package/templates/project/{web/backend → backend}/src/app/routers/health.py +0 -0
  150. /package/templates/project/{web/backend → backend}/src/app/schemas/__init__.py +0 -0
  151. /package/templates/project/{web/backend → backend}/src/app/services/__init__.py +0 -0
  152. /package/templates/project/{web/backend → backend}/tests/__init__.py +0 -0
  153. /package/templates/project/{web/backend → backend}/tests/conftest.py.hbs +0 -0
  154. /package/templates/project/{web/backend/alembic/versions → mobile}/.gitkeep +0 -0
  155. /package/templates/project/web/{frontend → apps/web}/src/components/.gitkeep +0 -0
  156. /package/templates/project/web/{frontend → apps/web}/src/main.ts +0 -0
@@ -1,18 +1,11 @@
1
1
  ---
2
- name: python-code-style
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
- class Settings(BaseSettings):
536
- # 应用
537
- app_name: str = "My API"
538
- debug: bool = False
539
-
540
- # 数据库
541
- database_url: str
542
- db_pool_size: int = 5
286
+ # 按领域拆分为独立的配置类(普通 BaseModel,不是 BaseSettings)
287
+ class OssConfig(BaseModel):
288
+ endpoint: str = ""
289
+ access_key: str = ""
290
+ secret_key: str = ""
291
+ bucket: str = ""
543
292
 
544
- # Redis
545
- redis_url: str = "redis://localhost:6379"
293
+ class AliyunSmsConfig(BaseModel):
294
+ access_key_id: str = ""
295
+ access_key_secret: str = ""
296
+ sign_name: str = ""
297
+ template_code: str = ""
546
298
 
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
- # CORS
552
- allowed_origins: list[str] = ["http://localhost:3000"]
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
  ### 强制规则
@@ -847,32 +603,233 @@ async def create_user(body: UserCreate) -> UserResponse:
847
603
  ...
848
604
  ```
849
605
 
850
- ## 十九、安全编码
606
+ ## 二十、dwyeapi 优先使用
607
+
608
+ ### 适用条件
609
+
610
+ 项目依赖中包含 `dwyeapi` 时,以下规则生效。
851
611
 
852
612
  ### 强制规则
853
- - SQL 查询必须使用参数化绑定,**禁止**字符串拼接
854
- - 用户输入必须校验后使用,**禁止**直接信任
855
- - 文件路径操作必须防止路径遍历攻击
856
- - 序列化/反序列化**禁止** `pickle.loads()` 处理不可信数据
857
- - 密码存储必须使用 `bcrypt` / `argon2`,**禁止**明文或 MD5/SHA
858
- - JWT 密钥从环境变量读取,**禁止**硬编码
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
+ ### 禁止的冗余模式
859
646
 
860
647
  ```python
861
- # ❌ SQL 注入
862
- cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")
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
+ - 如果**不确定** → 查看调用链确认,不要"以防万一"
863
699
 
864
- # ✅ 参数化查询
865
- cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
700
+ ## 代码自检(写代码时强制执行)
866
701
 
867
- # ❌ 路径遍历
868
- file_path = f"/uploads/{user_input_filename}"
702
+ **每次生成或修改 Python 代码后,必须逐条验证以下清单。任何一条未通过 → STOP,立即修正后再继续。**
869
703
 
870
- # ✅ 路径安全校验
871
- safe_path = Path("/uploads") / Path(user_input_filename).name
704
+ | # | 检查项 | 违规即 STOP |
705
+ |---|--------|------------|
706
+ | 1 | 所有公开函数有参数 + 返回值类型标注 | ✓ |
707
+ | 2 | 无 `Any` 类型(除非有注释说明原因) | ✓ |
708
+ | 3 | 使用 `list[int]` / `str \| None` 等现代语法,不用 `typing.List` / `Optional` | ✓ |
709
+ | 4 | service 层不抛 `HTTPException`,只抛业务异常 | ✓ |
710
+ | 5 | 无裸 `except:` 或 `except Exception: pass` | ✓ |
711
+ | 6 | 路由函数只做参数接收 → 调用 service → 返回结果,无业务逻辑 | ✓ |
712
+ | 7 | HTTP 请求用 `httpx`,不用 `requests` | ✓ |
713
+ | 8 | 日志用 `%s` 占位符或 structlog,不用 f-string | ✓ |
714
+ | 9 | 生命周期用 `lifespan`,不用 `@app.on_event` | ✓ |
715
+ | 10 | 无可变默认参数 `def f(items=[])` | ✓ |
716
+ | 11 | Pydantic Schema 请求/响应分离,不复用 | ✓ |
717
+ | 12 | 嵌套不超过 3 层,函数体不超过 50 行 | ✓ |
718
+ | 13 | 所有模块/类/函数有 docstring(Google 风格) | ✓ |
719
+ | 14 | 依赖通过 `Depends()` 注入,不在路由中直接创建 | ✓ |
720
+ | 15 | 项目依赖 eapi 时,未重复实现 eapi 已有功能 | ✓ |
872
721
 
873
- # ❌ 硬编码密钥
874
- SECRET_KEY = "my-super-secret-key"
722
+ **不执行自检就提交代码 = 违规。**
875
723
 
876
- # ✅ 环境变量
877
- SECRET_KEY = settings.secret_key
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
878
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` 占位符,支持日志聚合延迟求值)