create-dwy 0.8.0 → 0.9.1

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