create-dwy 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +212 -61
  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} +292 -358
  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} +99 -72
  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} +338 -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/skills/dwy-deploy-audit/SKILL.md +481 -0
  31. package/templates/claude-global/skills/dwy-deploy-audit/references/checks-data.md +117 -0
  32. package/templates/claude-global/skills/dwy-deploy-audit/references/checks-network.md +74 -0
  33. package/templates/claude-global/skills/dwy-deploy-audit/references/checks-runtime.md +184 -0
  34. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_base.sh +39 -0
  35. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_capacity.sh +278 -0
  36. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_db.sh +122 -0
  37. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_docker.sh +249 -0
  38. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_env.sh +64 -0
  39. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_https.sh +60 -0
  40. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_logs.sh +209 -0
  41. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_nginx.sh +92 -0
  42. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_resilience.sh +157 -0
  43. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_secrets.sh +327 -0
  44. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_services.sh +77 -0
  45. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_ssh.sh +56 -0
  46. package/templates/claude-global/skills/dwy-deploy-audit/scripts/run_all.sh +105 -0
  47. package/templates/claude-global/skills/dwy-docker/SKILL.md +187 -0
  48. package/templates/claude-global/skills/dwy-docker/references/ask-templates.md +142 -0
  49. package/templates/claude-global/skills/dwy-docker/references/templates.md +479 -0
  50. package/templates/claude-global/skills/dwy-docker/references/version-rules.md +76 -0
  51. package/templates/claude-global/skills/dwy-docker/scripts/query_dockerhub.py +204 -0
  52. package/templates/claude-global/skills/dwy-docker/scripts/scan_images.sh +185 -0
  53. package/templates/claude-global/skills/dwy-eapi/SKILL.md +433 -0
  54. package/templates/claude-global/skills/dwy-eapi/references/tasks-integration-guide.md +387 -0
  55. package/templates/claude-global/skills/dwy-ekit/SKILL.md +85 -0
  56. package/templates/claude-global/skills/dwy-ekit/references/data.md +135 -0
  57. package/templates/claude-global/skills/dwy-ekit/references/form.md +68 -0
  58. package/templates/claude-global/skills/dwy-ekit/references/request.md +183 -0
  59. package/templates/claude-global/skills/dwy-ekit/references/ui.md +109 -0
  60. package/templates/claude-global/skills/{dwy-frontend-eui → dwy-eui}/SKILL.md +129 -15
  61. package/templates/claude-global/skills/dwy-eui/references/eui-design-guide.md +257 -0
  62. package/templates/claude-global/skills/dwy-eui/references/eui-integration-guide.md +302 -0
  63. package/templates/claude-global/skills/dwy-eui/references/eui-landing-design-guide.md +437 -0
  64. package/templates/claude-global/skills/dwy-fullstack-scaffold/SKILL.md +214 -0
  65. package/templates/claude-global/skills/dwy-mirror-source/SKILL.md +275 -0
  66. package/templates/claude-global/skills/dwy-mirror-source/references/dockerfile-snippets.md +228 -0
  67. package/templates/claude-global/skills/dwy-mirror-source/references/mirror-providers.md +150 -0
  68. package/templates/claude-global/skills/dwy-mirror-source/scripts/__pycache__/check_mirrors.cpython-313.pyc +0 -0
  69. package/templates/claude-global/skills/dwy-mirror-source/scripts/apply_mirrors.py +666 -0
  70. package/templates/claude-global/skills/dwy-mirror-source/scripts/check_mirrors.py +868 -0
  71. package/templates/claude-global/skills/dwy-pentest/SKILL.md +344 -0
  72. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__init__.py +1 -0
  73. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/__init__.cpython-311.pyc +0 -0
  74. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/config.cpython-311.pyc +0 -0
  75. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/executor.cpython-311.pyc +0 -0
  76. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/severity.cpython-311.pyc +0 -0
  77. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/validator.cpython-311.pyc +0 -0
  78. package/templates/claude-global/skills/dwy-pentest/scripts/lib/config.py +109 -0
  79. package/templates/claude-global/skills/dwy-pentest/scripts/lib/executor.py +157 -0
  80. package/templates/claude-global/skills/dwy-pentest/scripts/lib/severity.py +96 -0
  81. package/templates/claude-global/skills/dwy-pentest/scripts/lib/validator.py +113 -0
  82. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_auth.py +289 -0
  83. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_recon.py +217 -0
  84. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_report.py +343 -0
  85. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_scan.py +205 -0
  86. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_ssl.py +208 -0
  87. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_vuln.py +205 -0
  88. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_web.py +330 -0
  89. package/templates/claude-global/skills/dwy-pentest/scripts/pyproject.toml +14 -0
  90. package/templates/claude-global/skills/dwy-pentest/scripts/setup_tools.sh +97 -0
  91. package/templates/claude-global/skills/dwy-pentest/scripts/uv.lock +415 -0
  92. package/templates/claude-global/skills/{dwy-release → dwy-publish}/SKILL.md +42 -18
  93. package/templates/claude-global/skills/dwy-vue-core/SKILL.md +890 -0
  94. package/templates/project/backend/.dwy.hbs +5 -0
  95. package/templates/project/backend/.python-version +1 -0
  96. package/templates/project/backend/CLAUDE.md.hbs +40 -0
  97. package/templates/project/{web/backend → backend}/alembic/script.py.mako +2 -0
  98. package/templates/project/{web/backend → backend}/alembic.ini.hbs +0 -1
  99. package/templates/project/backend/deploy.sh.hbs +18 -0
  100. package/templates/project/{web → backend}/docker-compose.prod.yml.hbs +1 -1
  101. package/templates/project/{web/backend → backend}/pyproject.toml.hbs +4 -5
  102. package/templates/project/{web/backend → backend}/src/app/config.py.hbs +1 -1
  103. package/templates/project/backend/src/app/database.py +10 -0
  104. package/templates/project/backend/src/app/dependencies.py +5 -0
  105. package/templates/project/{web/backend → backend}/src/app/main.py.hbs +3 -3
  106. package/templates/project/backend/src/app/models/base.py +5 -0
  107. package/templates/project/web/CLAUDE.md.hbs +29 -36
  108. package/templates/project/web/{frontend → apps/web}/index.html.hbs +1 -0
  109. package/templates/project/web/{frontend → apps/web}/package.json.hbs +4 -3
  110. package/templates/project/web/apps/web/src/App.vue +9 -0
  111. package/templates/project/web/apps/web/src/api/client.ts +5 -0
  112. package/templates/project/web/apps/web/src/composables/.gitkeep +0 -0
  113. package/templates/project/web/apps/web/src/layouts/DefaultLayout.vue +8 -0
  114. package/templates/project/web/{frontend → apps/web}/src/router/index.ts +1 -1
  115. package/templates/project/web/{frontend → apps/web}/src/stores/auth.ts +4 -2
  116. package/templates/project/web/apps/web/src/style.css +2 -0
  117. package/templates/project/web/apps/web/src/types/router.d.ts +8 -0
  118. package/templates/project/web/apps/web/src/views/HomeView.vue +8 -0
  119. package/templates/project/web/apps/web/tsconfig.app.json +18 -0
  120. package/templates/project/web/apps/web/tsconfig.json +6 -0
  121. package/templates/project/web/{frontend → apps/web}/vite.config.ts.hbs +7 -1
  122. package/templates/project/web/package.json.hbs +16 -0
  123. package/templates/project/web/packages/.gitkeep +0 -0
  124. package/templates/project/web/pnpm-workspace.yaml +3 -0
  125. package/templates/claude-global/hooks/post-sync-claude-global.sh +0 -72
  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-commit-convention.md +0 -106
  129. package/templates/claude-global/rules/git-security.md +0 -144
  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
286
+ # 按领域拆分为独立的配置类(普通 BaseModel,不是 BaseSettings)
287
+ class OssConfig(BaseModel):
288
+ endpoint: str = ""
289
+ access_key: str = ""
290
+ secret_key: str = ""
291
+ bucket: str = ""
539
292
 
540
- # 数据库
541
- database_url: str
542
- db_pool_size: int = 5
293
+ class AliyunSmsConfig(BaseModel):
294
+ access_key_id: str = ""
295
+ access_key_secret: str = ""
296
+ sign_name: str = ""
297
+ template_code: str = ""
543
298
 
544
- # Redis
545
- redis_url: str = "redis://localhost:6379"
546
-
547
- # 认证
548
- secret_key: str
549
- access_token_expire_minutes: int = 30
299
+ # 主 Settings 类组合嵌套模型
300
+ class Settings(BaseSettings):
301
+ # eapi 内置字段(继承 BaseSettings 时自动包含)
302
+ # database_url, redis_url, secret_key, ...
550
303
 
551
- # 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
  ### 强制规则
@@ -845,6 +601,101 @@ user = cache.get_user(user_id)
845
601
  async def create_user(body: UserCreate) -> UserResponse:
846
602
  """根据提交的信息创建新用户,邮箱不可重复。"""
847
603
  ...
604
+ ```
605
+
606
+ ## 二十、dwyeapi 优先使用
607
+
608
+ ### 适用条件
609
+
610
+ 项目依赖中包含 `dwyeapi` 时,以下规则生效。
611
+
612
+ ### 强制规则
613
+
614
+ 编写 FastAPI 后端代码时,**必须优先使用 eapi 已有的类和函数**,禁止重复实现。
615
+
616
+ | 需求 | 使用 eapi | 禁止自建 |
617
+ |------|----------|---------|
618
+ | 配置管理 | `dwyeapi.config.BaseSettings` | 自写 Pydantic Settings 基类 |
619
+ | ORM 基类 | `dwyeapi.database.Base` + `TimestampMixin` | 自定义 DeclarativeBase |
620
+ | 异步引擎 | `create_async_engine_factory()` + `create_session_factory()` | 手动创建 engine/session |
621
+ | 依赖注入 | `create_get_db(session_factory)` | 手写 get_db 生成器 |
622
+ | 密码哈希 | `hash_password()` / `verify_password()` | 直接调用 bcrypt |
623
+ | JWT | `create_token()` / `decode_token()` | 直接调用 python-jose |
624
+ | 异常体系 | `NotFoundError` / `BusinessError` / `register_exception_handlers()` | 自定义异常 + 手写 handler |
625
+ | 响应格式 | `success()` / `fail()` / `paginated()` | 自定义响应包装函数 |
626
+ | 分页 | `PaginationParams` + `paginate()` | 自写分页参数类 |
627
+ | Redis 缓存 | `cache.configure()` / `cache.get_redis()` | 手动管理 Redis 连接 |
628
+ | 异步任务 | `tasks.setup_tasks()` / `@register` / `TaskContext` (需 `[tasks]` extra) | 手写 ARQ/Celery 集成 |
629
+
630
+ ### 决策流程
631
+
632
+ ```
633
+ 需要某项基础设施功能
634
+ → 1. 检查 eapi 是否已提供(查阅 dwy-eapi skill)
635
+ → 已提供:直接使用,不重复实现
636
+ → 未提供:自行实现,代码注释中说明 eapi 不覆盖此功能
637
+ ```
638
+
639
+ ## 二十一、代码简约原则
640
+
641
+ ### 核心思想
642
+
643
+ 代码只写**必要的逻辑**,不写"以防万一"的冗余代码。信任内部代码和框架保证,只在系统边界(用户输入、外部 API)做校验。
644
+
645
+ ### 禁止的冗余模式
646
+
647
+ ```python
648
+ # ❌ 不必要的 fallback — 值已经有明确来源
649
+ name = user.name or "" # user.name 不可能是 None
650
+ result = data.get("key", None) # get 默认就返回 None
651
+
652
+ # ✅ 直接使用
653
+ name = user.name
654
+ result = data.get("key")
655
+
656
+ # ❌ 不必要的类型转换 — 值已经是目标类型
657
+ count = int(params.page) # Pydantic 已校验为 int
658
+ text = str(title) # title 声明为 str
659
+
660
+ # ✅ 直接使用
661
+ count = params.page
662
+ text = title
663
+
664
+ # ❌ 不必要的条件检查 — 逻辑上不可能走到 else
665
+ if task:
666
+ return task
667
+ else:
668
+ return None # 上面已经 return 了
669
+
670
+ # ✅ 直接返回
671
+ return task
672
+
673
+ # ❌ 不必要的默认值 — 调用方已保证传值
674
+ def process(data: dict = None): # 所有调用方都传了 data
675
+ if data is None:
676
+ data = {}
677
+
678
+ # ✅ 去掉默认值和检查
679
+ def process(data: dict):
680
+ ...
681
+
682
+ # ❌ 不必要的异常兜底 — 掩盖真正的 bug
683
+ try:
684
+ result = calculate(x)
685
+ except Exception:
686
+ result = 0 # 吞掉异常,bug 永远不会被发现
687
+
688
+ # ✅ 让异常暴露,或只捕获预期的异常
689
+ result = calculate(x)
690
+ ```
691
+
692
+ ### 判断标准
693
+
694
+ 写每一行防御代码前问自己:**这个情况在当前上下文下真的会发生吗?**
695
+
696
+ - 如果**会** → 写防御,加注释说明什么情况下触发
697
+ - 如果**不会** → 不写,信任上游保证
698
+ - 如果**不确定** → 查看调用链确认,不要"以防万一"
848
699
 
849
700
  ## 代码自检(写代码时强制执行)
850
701
 
@@ -866,36 +717,119 @@ async def create_user(body: UserCreate) -> UserResponse:
866
717
  | 12 | 嵌套不超过 3 层,函数体不超过 50 行 | ✓ |
867
718
  | 13 | 所有模块/类/函数有 docstring(Google 风格) | ✓ |
868
719
  | 14 | 依赖通过 `Depends()` 注入,不在路由中直接创建 | ✓ |
720
+ | 15 | 项目依赖 eapi 时,未重复实现 eapi 已有功能 | ✓ |
869
721
 
870
722
  **不执行自检就提交代码 = 违规。**
723
+
724
+ ---
725
+
726
+ ## 错误处理规范(后端)
727
+
728
+ **唯一方式:** 使用 eapi exceptions 体系。
729
+
730
+ ```python
731
+ from dwyeapi.exceptions import (
732
+ NotFoundError, # 404 — 资源不存在
733
+ BusinessError, # 422 — 业务规则不允许
734
+ PermissionDeniedError, # 403 — 无权限
735
+ AuthenticationError, # 401 — 认证失败
736
+ )
737
+
738
+ # services 层
739
+ async def get_user(db: AsyncSession, user_id: int) -> User:
740
+ user = await db.get(User, user_id)
741
+ if user is None:
742
+ raise NotFoundError("用户") # → {"code": "NOT_FOUND", "message": "用户不存在"}
743
+ return user
744
+
745
+ async def create_order(db: AsyncSession, data: OrderCreate) -> Order:
746
+ if data.amount <= 0:
747
+ raise BusinessError("订单金额必须大于 0", code="INVALID_AMOUNT")
748
+ ...
871
749
  ```
872
750
 
873
- ## 十九、安全编码
751
+ **禁止清单:**
752
+ | 禁止写法 | 正确写法 |
753
+ |---------|---------|
754
+ | `raise HTTPException(status_code=404)` | `raise NotFoundError("资源名")` |
755
+ | `try: ... except Exception: pass` | 只捕获预期的具体异常 |
756
+ | 自定义异常类不继承 AppError | 项目异常必须继承 `AppError` |
757
+ | 在 router 中 try/except 包 service 调用 | 让异常自然冒泡到 handler |
874
758
 
875
- ### 强制规则
876
- - SQL 查询必须使用参数化绑定,**禁止**字符串拼接
877
- - 用户输入必须校验后使用,**禁止**直接信任
878
- - 文件路径操作必须防止路径遍历攻击
879
- - 序列化/反序列化**禁止** `pickle.loads()` 处理不可信数据
880
- - 密码存储必须使用 `bcrypt` / `argon2`,**禁止**明文或 MD5/SHA
881
- - JWT 密钥从环境变量读取,**禁止**硬编码
759
+ ---
760
+
761
+ ## 环境变量管理(后端:eapi BaseSettings + 嵌套分组)
762
+
763
+ **必须使用嵌套模型分组配置**,禁止所有字段平铺。同一服务/SDK 的多个字段拆成独立 `BaseModel`。
882
764
 
883
765
  ```python
884
- # ❌ SQL 注入
885
- cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")
766
+ # app/config.py
767
+ from pydantic import BaseModel
768
+ from dwyeapi.config import BaseSettings
886
769
 
887
- # ✅ 参数化查询
888
- cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
770
+ class OssConfig(BaseModel):
771
+ endpoint: str = ""
772
+ access_key: str = ""
773
+ secret_key: str = ""
774
+ bucket: str = ""
889
775
 
890
- # ❌ 路径遍历
891
- file_path = f"/uploads/{user_input_filename}"
776
+ class Settings(BaseSettings):
777
+ # eapi 内置字段保持顶层:database_url, redis_url, secret_key, ...
892
778
 
893
- # ✅ 路径安全校验
894
- safe_path = Path("/uploads") / Path(user_input_filename).name
779
+ # 项目专属 — 嵌套分组
780
+ app_name: str = "My App"
781
+ oss: OssConfig = OssConfig()
782
+ ```
895
783
 
896
- # ❌ 硬编码密钥
897
- SECRET_KEY = "my-super-secret-key"
784
+ **`.env` 中用 `__` 双下划线分隔层级:**
898
785
 
899
- # ✅ 环境变量
900
- SECRET_KEY = settings.secret_key
786
+ ```bash
787
+ DATABASE_URL=postgresql+asyncpg://user:pass@localhost:5432/mydb
788
+ OSS__ENDPOINT=http://localhost:9000
789
+ OSS__ACCESS_KEY=minioadmin
790
+ OSS__SECRET_KEY=minioadmin
791
+ OSS__BUCKET=my-app
901
792
  ```
793
+
794
+ **代码中通过属性链访问:** `settings.oss.endpoint`,禁止 `settings.oss_endpoint`。
795
+
796
+ 详细规则见 `dwy-python-core.md` 第十二节。
797
+
798
+ ---
799
+
800
+ ## 日志规范
801
+
802
+ ### 后端
803
+
804
+ 使用 Python 标准 `logging` 模块:
805
+
806
+ ```python
807
+ import logging
808
+
809
+ logger = logging.getLogger(__name__)
810
+
811
+ # 正确用法
812
+ logger.info("用户 %s 登录成功", user_id)
813
+ logger.error("导出任务失败: task_id=%s, error=%s", task_id, str(e))
814
+
815
+ # 禁止
816
+ print("debug info") # 禁止 print
817
+ logger.info(f"用户 {user_id} 登录") # 禁止 f-string(无法被日志聚合)
818
+ ```
819
+
820
+ ### 日志级别规则
821
+
822
+ | 级别 | 用途 | 示例 |
823
+ |------|------|------|
824
+ | DEBUG | 开发调试信息,生产环境不输出 | 变量值、SQL 语句 |
825
+ | INFO | 关键业务流程节点 | 用户登录、任务提交、导出完成 |
826
+ | WARNING | 异常但可恢复的情况 | 重试成功、降级处理 |
827
+ | ERROR | 错误但服务仍可用(单次请求失败) | 外部 API 超时、单个任务失败 |
828
+ | CRITICAL | 服务不可用 | 数据库连接断开、必要服务宕机 |
829
+
830
+ ### 禁止事项
831
+
832
+ - 禁止 `print()`,全部用 `logger`
833
+ - 禁止在循环体内打 INFO/WARNING 日志(大量重复)
834
+ - 禁止日志中包含密码、token、密钥等敏感信息
835
+ - 禁止用 f-string 格式化日志消息(用 `%s` 占位符,支持日志聚合延迟求值)