create-dwy 0.7.1 → 0.9.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 (96) hide show
  1. package/package.json +1 -1
  2. package/src/create.js +50 -3
  3. package/src/sync.js +228 -130
  4. package/src/utils.js +4 -0
  5. package/templates/claude-global/CLAUDE.md +81 -6
  6. package/templates/claude-global/rules/dwy-postgres.md +89 -0
  7. package/templates/claude-global/rules/dwy-python-backend.md +492 -0
  8. package/templates/claude-global/rules/dwy-python-core.md +94 -634
  9. package/templates/claude-global/rules/dwy-python-testing.md +115 -13
  10. package/templates/claude-global/rules/dwy-redis.md +88 -0
  11. package/templates/claude-global/rules/dwy-vue-core.md +341 -645
  12. package/templates/claude-global/rules/dwy-vue-pinia.md +47 -34
  13. package/templates/claude-global/rules/dwy-vue-testing.md +120 -55
  14. package/templates/claude-global/skills/dwy-deploy-audit/references/checks-network.md +1 -1
  15. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_nginx.sh +1 -1
  16. package/templates/claude-global/skills/dwy-dolphindb/SKILL.md +22 -15
  17. package/templates/claude-global/skills/dwy-dolphindb/evals/evals.json +23 -0
  18. package/templates/claude-global/skills/dwy-dolphindb/references/dolphindb-rules.md +100 -1
  19. package/templates/claude-global/skills/dwy-ekit/SKILL.md +142 -276
  20. package/templates/claude-global/skills/dwy-eui/SKILL.md +264 -182
  21. package/templates/claude-global/skills/dwy-fullstack-scaffold/SKILL.md +117 -129
  22. package/templates/claude-global/skills/dwy-git-commit/SKILL.md +125 -0
  23. package/templates/claude-global/skills/dwy-publish/SKILL.md +1 -1
  24. package/templates/claude-global/{rules/dwy-tech-stack.md → skills/dwy-tech-stack/SKILL.md} +4 -3
  25. package/templates/project/fullstack-monorepo/CLAUDE.md.hbs +81 -0
  26. package/templates/project/fullstack-monorepo/backend/.python-version +1 -0
  27. package/templates/project/fullstack-monorepo/backend/CLAUDE.md.hbs +68 -0
  28. package/templates/project/fullstack-monorepo/backend/Dockerfile.dev +23 -0
  29. package/templates/project/fullstack-monorepo/backend/alembic/env.py.hbs +68 -0
  30. package/templates/project/fullstack-monorepo/backend/alembic/script.py.mako +26 -0
  31. package/templates/project/fullstack-monorepo/backend/alembic.ini.hbs +43 -0
  32. package/templates/project/fullstack-monorepo/backend/packages/dwy-shared/pyproject.toml +16 -0
  33. package/templates/project/fullstack-monorepo/backend/packages/dwy-shared/src/dwy_shared/__init__.py +12 -0
  34. package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/pyproject.toml.hbs +20 -0
  35. package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/src/{{scopePrefix}}_{{initialDomain}}/__init__.py.hbs +40 -0
  36. package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/src/{{scopePrefix}}_{{initialDomain}}/config.py +9 -0
  37. package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/src/{{scopePrefix}}_{{initialDomain}}/models.py.hbs +17 -0
  38. package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/src/{{scopePrefix}}_{{initialDomain}}/protocols.py +14 -0
  39. package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/src/{{scopePrefix}}_{{initialDomain}}/router.py.hbs +22 -0
  40. package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/src/{{scopePrefix}}_{{initialDomain}}/schemas.py +21 -0
  41. package/templates/project/fullstack-monorepo/backend/packages/{{scopePrefix}}-{{initialDomain}}/src/{{scopePrefix}}_{{initialDomain}}/service.py +11 -0
  42. package/templates/project/fullstack-monorepo/backend/pyproject.toml.hbs +84 -0
  43. package/templates/project/fullstack-monorepo/backend/src/app/__init__.py +3 -0
  44. package/templates/project/fullstack-monorepo/backend/src/app/config.py.hbs +21 -0
  45. package/templates/project/fullstack-monorepo/backend/src/app/exceptions.py +26 -0
  46. package/templates/project/fullstack-monorepo/backend/src/app/factories.py +16 -0
  47. package/templates/project/fullstack-monorepo/backend/src/app/infra/__init__.py +1 -0
  48. package/templates/project/fullstack-monorepo/backend/src/app/main.py.hbs +65 -0
  49. package/templates/project/fullstack-monorepo/backend/src/app/middleware/__init__.py +1 -0
  50. package/templates/project/fullstack-monorepo/backend/tests/__init__.py +0 -0
  51. package/templates/project/fullstack-monorepo/backend/tests/conftest.py.hbs +42 -0
  52. package/templates/project/fullstack-monorepo/dev.sh.hbs +83 -0
  53. package/templates/project/fullstack-monorepo/docker-compose.dev.yml.hbs +36 -0
  54. package/templates/project/fullstack-monorepo/frontend/CLAUDE.md.hbs +35 -0
  55. package/templates/project/fullstack-monorepo/frontend/Dockerfile +18 -0
  56. package/templates/project/fullstack-monorepo/frontend/apps/web/index.html.hbs +13 -0
  57. package/templates/project/fullstack-monorepo/frontend/apps/web/package.json.hbs +31 -0
  58. package/templates/project/fullstack-monorepo/frontend/apps/web/src/App.vue.hbs +10 -0
  59. package/templates/project/fullstack-monorepo/frontend/apps/web/src/api/client.ts.hbs +11 -0
  60. package/templates/project/fullstack-monorepo/frontend/apps/web/src/features/{{initialDomain}}/.gitkeep +0 -0
  61. package/templates/project/fullstack-monorepo/frontend/apps/web/src/index.css +12 -0
  62. package/templates/project/fullstack-monorepo/frontend/apps/web/src/main.ts +17 -0
  63. package/templates/project/fullstack-monorepo/frontend/apps/web/src/router/index.ts.hbs +24 -0
  64. package/templates/project/fullstack-monorepo/frontend/apps/web/src/stores/.gitkeep +0 -0
  65. package/templates/project/fullstack-monorepo/frontend/apps/web/src/types/.gitkeep +0 -0
  66. package/templates/project/fullstack-monorepo/frontend/apps/web/src/views/HomeView.vue.hbs +36 -0
  67. package/templates/project/fullstack-monorepo/frontend/apps/web/tsconfig.json +7 -0
  68. package/templates/project/fullstack-monorepo/frontend/apps/web/vite.config.ts.hbs +27 -0
  69. package/templates/project/fullstack-monorepo/frontend/nginx.conf +11 -0
  70. package/templates/project/fullstack-monorepo/frontend/package.json.hbs +13 -0
  71. package/templates/project/fullstack-monorepo/frontend/packages/core/package.json.hbs +15 -0
  72. package/templates/project/fullstack-monorepo/frontend/packages/core/src/index.ts +3 -0
  73. package/templates/project/fullstack-monorepo/frontend/packages/features/{{initialDomain}}/package.json.hbs +14 -0
  74. package/templates/project/fullstack-monorepo/frontend/packages/features/{{initialDomain}}/src/index.ts.hbs +16 -0
  75. package/templates/project/fullstack-monorepo/frontend/packages/features/{{initialDomain}}/src/pages/IndexPage.vue.hbs +9 -0
  76. package/templates/project/fullstack-monorepo/frontend/packages/ui/package.json.hbs +15 -0
  77. package/templates/project/fullstack-monorepo/frontend/packages/ui/src/index.ts +3 -0
  78. package/templates/project/fullstack-monorepo/frontend/pnpm-workspace.yaml.hbs +23 -0
  79. package/templates/project/fullstack-monorepo/frontend/tsconfig.base.json.hbs +23 -0
  80. package/templates/claude-global/rules/dwy-api-design.md +0 -110
  81. package/templates/claude-global/rules/dwy-git-commit-convention.md +0 -82
  82. package/templates/claude-global/rules/dwy-git-security.md +0 -53
  83. package/templates/claude-global/rules/dwy-payload-limits.md +0 -186
  84. package/templates/claude-global/rules/dwy-prefer-open-source.md +0 -32
  85. package/templates/claude-global/rules/dwy-project-structure.md +0 -181
  86. package/templates/claude-global/rules/dwy-python-fastapi.md +0 -272
  87. package/templates/claude-global/rules/dwy-python-orm.md +0 -61
  88. package/templates/claude-global/rules/dwy-python-security.md +0 -636
  89. package/templates/claude-global/rules/dwy-server-security.md +0 -518
  90. package/templates/claude-global/rules/dwy-vue-api.md +0 -146
  91. package/templates/claude-global/skills/dwy-ekit/references/data.md +0 -135
  92. package/templates/claude-global/skills/dwy-ekit/references/form.md +0 -68
  93. package/templates/claude-global/skills/dwy-ekit/references/request.md +0 -183
  94. package/templates/claude-global/skills/dwy-ekit/references/ui.md +0 -109
  95. package/templates/claude-global/skills/dwy-vue-core/SKILL.md +0 -890
  96. /package/templates/{claude-global/commands → project/fullstack-monorepo/backend/alembic/versions}/.gitkeep +0 -0
@@ -0,0 +1,492 @@
1
+ ---
2
+ description: Python 后端(FastAPI + SQLAlchemy)应用层开发规范 — 路由 / Schema / 依赖注入 / ORM / 安全编码 / 注入防护 / 限流 / 审计 / 违规检测清单
3
+ paths:
4
+ - "**/*.py"
5
+ ---
6
+
7
+ # Python 后端开发规范(FastAPI + ORM + 安全)
8
+
9
+ 适用于基于 FastAPI + SQLAlchemy 的应用层规范。基础设施专项规范(PostgreSQL / Redis / 对象存储 / 时序数据库等)见对应独立 rule。
10
+
11
+ ---
12
+
13
+ ## 零、优先使用 `dwyeapi` 基础框架库(强制)
14
+
15
+ `dwyeapi` 是团队内部 PyPI 基础设施包,已封装后端项目所需的通用能力。**凡是 dwyeapi 已提供的能力,禁止自行实现,也禁止引入其他库重复造轮子。**
16
+
17
+ ### 必须复用 dwyeapi 的能力域
18
+
19
+ | 能力域 | 内容 |
20
+ |--------|------|
21
+ | 配置管理 | 应用 Settings 基类、运行环境识别(dev / prod) |
22
+ | 异常体系 | 业务异常基类与全局异常处理器注册 |
23
+ | 统一响应 | `{ code, message, data, timestamp }` 包装 |
24
+ | 分页 | 分页查询参数模型与分页响应封装 |
25
+ | 数据库 | ORM 声明式基类、时间戳 Mixin、异步引擎与会话工厂 |
26
+ | 依赖注入 | FastAPI `get_db` 工厂 |
27
+ | 安全 | 密码哈希、JWT 创建与解码 |
28
+ | 缓存 | 共享异步 Redis 客户端 |
29
+ | 时间 | 业务时间统一入口(Asia/Shanghai) |
30
+ | 脱敏 | PII 数据脱敏函数 |
31
+ | 日志 | 全局日志配置与门面 |
32
+ | 健康检查 | 健康检查路由工厂 |
33
+ | 异步任务 | 耗时任务队列与上下文 |
34
+ | 邮件 | 邮件验证码可插拔通道 |
35
+
36
+ ### 查阅方式(关键)
37
+
38
+ **在编写任何后端代码前,以及调用任何 dwyeapi 提供的 API 之前,必须先用 `Skill` 工具加载 `dwy-eapi` skill 查阅当前最新接口。** 本文件**只规定约束与边界**,**不固化** dwyeapi 的具体模块路径、类名、函数签名、参数 —— 这些会随版本演进,固化在 rules 里会与实际包不同步。
39
+
40
+ ```
41
+ Skill 工具调用:skill="dwy-eapi"
42
+ ```
43
+
44
+ ### 强制规则
45
+
46
+ - 涉及上表任一能力域,**先查 skill 拿到当前 API,再写代码**
47
+ - **禁止**自造异常基类,必须复用 dwyeapi 异常体系
48
+ - **禁止**手写 bcrypt / PyJWT 调用,必须走 dwyeapi 安全模块
49
+ - **禁止**自定义统一响应包装函数,必须用 dwyeapi 响应模块
50
+ - **禁止**自定义分页模型,必须用 dwyeapi 分页模块
51
+ - **禁止**自定义 `DeclarativeBase`,模型必须继承 dwyeapi 提供的基类
52
+ - **禁止**在路由函数中直接创建数据库会话,必须用 dwyeapi 依赖工厂
53
+ - **禁止**自建 Redis 连接池,必须用 dwyeapi 共享客户端
54
+ - **禁止**在业务代码中直接使用 `datetime.now()`,必须用 dwyeapi 时间模块
55
+ - **禁止**自行 `logging.basicConfig`,必须用 dwyeapi 日志模块
56
+
57
+ ---
58
+
59
+ ## 一、项目结构与业务聚合(强制)
60
+
61
+ ### 核心原则
62
+
63
+ **按功能模块(feature / domain)聚合,禁止按技术层散落。** 同一业务功能的 router / schema / service / model / dependency 必须集中在同一个功能目录下;**禁止**全局存在 `routers/` / `services/` / `models/` / `schemas/` 等"技术层"顶级目录,把不相关功能的同类型文件强行揉到一起。
64
+
65
+ ### 为什么
66
+
67
+ - **跨项目迁移**:复制单个功能目录即可携带该功能全部代码;技术层散落需在 4+ 目录里翻找
68
+ - **可读性**:阅读功能时所有相关代码在同一目录,无需跨目录跳转
69
+ - **变更影响域清晰**:一次改动 diff 集中,code review 更高效
70
+ - **删除友好**:删功能时整个目录删掉即可,不会留下孤儿文件
71
+
72
+ ### 标准结构(参考)
73
+
74
+ ```
75
+ app/
76
+ ├── users/ # 用户功能(一个业务模块自成一个目录)
77
+ │ ├── __init__.py
78
+ │ ├── router.py # FastAPI 路由
79
+ │ ├── schemas.py # Pydantic 请求 / 响应模型
80
+ │ ├── service.py # 业务逻辑
81
+ │ ├── models.py # ORM 模型
82
+ │ ├── dependencies.py # 该功能专用 Depends(可选)
83
+ │ └── exceptions.py # 该功能专用异常(可选)
84
+ ├── orders/
85
+ │ ├── router.py
86
+ │ ├── schemas.py
87
+ │ ├── service.py
88
+ │ └── models.py
89
+ ├── core/ # 跨功能共享:配置 / 全局中间件 / 启动逻辑
90
+ │ ├── config.py
91
+ │ ├── middleware.py
92
+ │ └── lifespan.py
93
+ └── main.py # 注册各功能 router
94
+ ```
95
+
96
+ ### 强制规则
97
+
98
+ - **禁止**顶级 `routers/` / `services/` / `models/` / `schemas/` 把不同功能的同类文件混放
99
+ - 一个功能目录内文件名固定:`router.py` / `schemas.py` / `service.py` / `models.py`(单数,不带功能前缀)
100
+ - 跨功能真正共享的代码放 `core/` 或 `shared/`,**禁止**为"可能复用"把单一功能逻辑提前抽离
101
+ - 单文件超过约 400 行时再考虑拆分(如 `service.py` → `services/create.py` + `services/query.py`),**禁止**未到规模就预拆分
102
+ - 功能间引用通过明确 import,**禁止**循环依赖
103
+ - 测试镜像功能聚合结构:`tests/users/test_router.py` / `tests/users/test_service.py`(详见第九节)
104
+
105
+ ### 例外
106
+ - 通用基础设施代码(如 dwyeapi 内部)可保持技术层结构
107
+
108
+ ---
109
+
110
+ ## 二、FastAPI 路由与接口设计
111
+
112
+ ### 强制规则
113
+
114
+ - 每个路由模块独立 `APIRouter`,在 `main.py` 统一注册
115
+ - **禁止**所有路由写在 `main.py` 中(超过 2 个路由必须拆分)
116
+ - 路由函数只做:参数接收 → 调用 service → 返回结果,**不写业务逻辑**
117
+ - 必须显式声明 `status_code` 和 `response_model`
118
+ - 所有路由统一加 `/api` 前缀
119
+
120
+ ### URL 命名
121
+
122
+ - 资源名用**复数**、**kebab-case**:`/api/upload-tasks`,**禁止** `/api/uploadTasks` 或 `/api/upload_task`
123
+ - 路径参数用 `{xxx_uuid}: str`(详见 7.1 ID 设计),**禁止** `{id}: int`
124
+ - 查询参数用 **snake_case**:`?page_size=20&task_type=export`
125
+ - **禁止**在 URL 中包含动词(用 HTTP 方法表达 CRUD)
126
+ - 非 CRUD 动作用 `POST + 动词`:`POST /api/users/{uuid}/activate`、`POST /api/tasks/{uuid}/cancel`
127
+
128
+ ### RESTful 路由规范
129
+
130
+ | 操作 | 方法 | 路径 | 状态码 |
131
+ |---|---|---|---|
132
+ | 列表查询 | `GET` | `/api/users` | 200 |
133
+ | 详情查询 | `GET` | `/api/users/{user_uuid}` | 200 |
134
+ | 创建 | `POST` | `/api/users` | 201 |
135
+ | 全量更新 | `PUT` | `/api/users/{user_uuid}` | 200 |
136
+ | 部分更新 | `PATCH` | `/api/users/{user_uuid}` | 200 |
137
+ | 删除 | `DELETE` | `/api/users/{user_uuid}` | 204 |
138
+ | 子资源 | `GET` | `/api/users/{user_uuid}/orders` | 200 |
139
+ | 动作 | `POST` | `/api/users/{user_uuid}/activate` | 200 |
140
+
141
+ ### HTTP 状态码
142
+
143
+ | 场景 | 状态码 |
144
+ |------|--------|
145
+ | 查询 / 更新成功 | 200 |
146
+ | 创建成功 | 201 |
147
+ | 删除成功 | 204(无响应体) |
148
+ | 参数格式错误 | 400 |
149
+ | 未认证(Token 缺失或过期) | 401 |
150
+ | 无权限(认证通过但权限不足) | 403 |
151
+ | 资源不存在 | 404 |
152
+ | 业务规则不允许 | 422 |
153
+
154
+ ```python
155
+ # routers/user.py
156
+ from fastapi import APIRouter, Depends, status
157
+
158
+ router = APIRouter(prefix="/api/users", tags=["users"])
159
+
160
+ @router.post("/", status_code=status.HTTP_201_CREATED, response_model=UserResponse)
161
+ async def create_user(
162
+ body: UserCreate,
163
+ service: UserService = Depends(get_user_service),
164
+ ) -> UserResponse:
165
+ return await service.create(body)
166
+ ```
167
+
168
+ ### 统一响应格式
169
+
170
+ 所有成功 / 分页 / 错误响应统一走 dwyeapi 响应与异常模块(具体 API 与字段结构查 `dwy-eapi` skill),**禁止**自造响应包装函数,**禁止**在 router 中手动构造 `{"success": true, ...}` 等非标准格式。
171
+
172
+ ### 分页
173
+
174
+ 分页参数模型与响应包装统一走 dwyeapi(具体 API 查 `dwy-eapi` skill),**禁止**自定义。
175
+
176
+ ---
177
+
178
+ ## 三、Pydantic Schema 规范
179
+
180
+ ### 命名约定
181
+
182
+ | Schema 类型 | 命名 | 用途 |
183
+ |------------|------|------|
184
+ | 创建请求 | `XxxCreate` | POST body |
185
+ | 更新请求(字段全可选) | `XxxUpdate` | PATCH body |
186
+ | 响应(完整字段) | `XxxResponse` | 详情接口返回 |
187
+ | 响应(精简字段) | `XxxBrief` | 列表接口返回 |
188
+
189
+ ### 强制规则
190
+
191
+ - 请求与响应使用**不同**的 Schema,**禁止**复用同一个 Model
192
+ - 更新 Schema 的字段必须全部可选
193
+ - **禁止**在响应 Schema 中暴露密码、内部自增 ID、敏感字段
194
+ - 必须使用 `Field()` 添加校验约束(`min_length` / `max_length` / `ge` / `le` / `pattern` 等)
195
+ - **禁止**用松散字典 / `dict` / `Any` 直接接收 body,必须有显式 Schema
196
+
197
+ ```python
198
+ class UserBase(BaseModel):
199
+ name: str = Field(max_length=50)
200
+ email: EmailStr
201
+
202
+ class UserCreate(UserBase):
203
+ password: str = Field(min_length=8, max_length=128)
204
+
205
+ class UserUpdate(BaseModel):
206
+ name: str | None = Field(default=None, max_length=50)
207
+ email: EmailStr | None = None
208
+
209
+ class UserResponse(UserBase):
210
+ uuid: str
211
+ is_active: bool
212
+ created_at: datetime
213
+
214
+ model_config = ConfigDict(from_attributes=True)
215
+ ```
216
+
217
+ ### 输入校验清单
218
+
219
+ | 字段类型 | 校验 |
220
+ |---------|------|
221
+ | 数字 | `ge` + `le`(最小值 + 最大值) |
222
+ | 邮箱 | `EmailStr` |
223
+ | 手机号 | `pattern=r"^1[3-9]\d{9}$"` |
224
+ | URL | `HttpUrl` |
225
+ | 分页 page_size | 必须有上限(≤ 100) |
226
+ | 字符串 | 必须显式 `max_length` |
227
+ | 文件上传 | 后端二次校验大小 + magic bytes,**禁止**只信任前端或 Nginx |
228
+
229
+ ---
230
+
231
+ ## 四、依赖注入
232
+
233
+ ### 强制规则
234
+
235
+ - 数据库会话、当前用户、配置等**必须**通过 `Depends()` 注入
236
+ - **禁止**在路由函数中直接创建数据库会话或实例化 service
237
+ - `get_db` 依赖必须用 dwyeapi 提供的工厂生成(具体 API 查 `dwy-eapi` skill)
238
+
239
+ ```python
240
+ # 反例:路由中直接创建会话
241
+ @router.get("/users/{user_uuid}")
242
+ async def get_user(user_uuid: str):
243
+ async with async_session_factory() as db:
244
+ ...
245
+
246
+ # 正例:依赖注入
247
+ @router.get("/users/{user_uuid}", response_model=UserResponse)
248
+ async def get_user(
249
+ user_uuid: str,
250
+ service: UserService = Depends(get_user_service),
251
+ ) -> UserResponse:
252
+ return await service.get_by_uuid(user_uuid)
253
+ ```
254
+
255
+ ---
256
+
257
+ ## 五、异常处理
258
+
259
+ ### 强制规则
260
+
261
+ - **必须**复用 dwyeapi 异常体系与全局处理器(具体异常类与注册方法查 `dwy-eapi` skill),**禁止**自造异常基类
262
+ - 项目级扩展异常必须继承 dwyeapi 异常基类
263
+ - **service 层**抛业务异常,**禁止**抛 `HTTPException`(service 不感知 HTTP)
264
+ - **router 层**不写 `try / except`,由全局 handler 统一处理
265
+ - **禁止**裸 `except:` 或 `except Exception: pass`
266
+ - **禁止**在错误消息中回显用户输入
267
+ - **禁止**将原始异常信息(`str(e)`)泄露给客户端
268
+
269
+ ---
270
+
271
+ ## 六、数据库与 ORM(SQLAlchemy 2.0 异步)
272
+
273
+ ### 强制规则
274
+
275
+ - 必须使用 **SQLAlchemy 2.0** 声明式映射 + async
276
+ - 模型继承 dwyeapi 提供的声明式基类与时间戳 Mixin(具体类名查 `dwy-eapi` skill),**禁止**自定义 `DeclarativeBase`
277
+ - 数据库迁移使用 **Alembic**,**禁止**手动执行 DDL
278
+ - **禁止**在路由层直接写 SQL 查询
279
+ - **禁止**使用 SQLAlchemy 1.x 风格的 `session.query(...)`
280
+
281
+ ```python
282
+ from sqlalchemy.orm import Mapped, mapped_column
283
+ from sqlalchemy import String
284
+ import uuid as uuid_lib
285
+
286
+ class User(BaseFromDwyeapi, TimestampMixinFromDwyeapi): # 实际基类名以 skill 为准
287
+ __tablename__ = "users"
288
+
289
+ id: Mapped[int] = mapped_column(primary_key=True)
290
+ uuid: Mapped[str] = mapped_column(
291
+ String(36),
292
+ default=lambda: str(uuid_lib.uuid4()),
293
+ unique=True,
294
+ index=True,
295
+ )
296
+ name: Mapped[str] = mapped_column(String(50))
297
+ email: Mapped[str] = mapped_column(String(100), unique=True, index=True)
298
+ hashed_password: Mapped[str] = mapped_column(String(128))
299
+ is_active: Mapped[bool] = mapped_column(default=True)
300
+ is_deleted: Mapped[bool] = mapped_column(default=False)
301
+ deleted_at: Mapped[datetime | None] = mapped_column(default=None)
302
+ ```
303
+
304
+ ### 查询规范
305
+
306
+ ```python
307
+ # 反例:旧式查询
308
+ session.query(User).filter(User.id == user_id).first()
309
+
310
+ # 正例:SQLAlchemy 2.0
311
+ result = await session.execute(select(User).where(User.uuid == user_uuid))
312
+ user = result.scalar_one_or_none()
313
+ ```
314
+
315
+ ---
316
+
317
+ ## 七、安全编码
318
+
319
+ ### 7.1 ID 设计(防遍历攻击)
320
+
321
+ **核心原则**:数据库内部用自增 ID(性能),对外暴露 UUID(安全)。
322
+
323
+ - 所有 ORM 模型必须同时有 `id`(自增)和 `uuid`(对外)
324
+ - 路由路径参数**必须**为 `{xxx_uuid}: str`,**禁止** `{xxx_id}: int`
325
+ - Response Schema **只返回 uuid**,**禁止**暴露自增 id
326
+
327
+ ### 7.2 密码安全
328
+
329
+ | 规则 | 说明 |
330
+ |------|------|
331
+ | 存储算法 | **只允许 bcrypt**,必须走 dwyeapi 安全模块,**禁止**手写 bcrypt 调用或使用 MD5 / SHA / 明文 |
332
+ | 接口返回 | **永远不返回**密码字段,Response Schema 中不包含 |
333
+ | 密码强度 | 用户密码最少 8 位,管理员密码最少 12 位 |
334
+ | 登录错误 | 返回"用户名或密码错误",**不区分**是用户名不存在还是密码错误 |
335
+
336
+ ### 7.3 接口认证与授权
337
+
338
+ - 所有 API **必须** JWT 认证(白名单除外:`/health` / `/auth/login` / `/auth/register`)
339
+ - JWT 创建与解码**必须**走 dwyeapi 安全模块,**禁止**直接调用 PyJWT
340
+ - Token 黑名单存 Redis(登出后失效,TTL = 剩余有效期)
341
+ - Secret Key 必须从环境变量读取,最少 32 位随机字符串,**禁止**硬编码
342
+
343
+ | Token 类型 | 有效期 |
344
+ |------------|--------|
345
+ | Access Token | 30 分钟(最长 ≤ 2 小时) |
346
+ | Refresh Token | 7 天 |
347
+
348
+ ### 7.4 响应安全
349
+
350
+ - **禁止**直接返回 ORM 对象(可能暴露 password、内部 ID 等)
351
+ - 列表接口**必须**分页,**禁止**返回全量数据
352
+ - PII 字段(手机号 / 邮箱 / 身份证 / 银行卡 / 姓名 / 地址 / IP / 车牌)必须使用 dwyeapi 脱敏函数处理
353
+
354
+ ### 7.5 错误处理安全
355
+
356
+ 生产环境**禁止**返回以下内容:
357
+
358
+ | 禁止内容 | 原因 |
359
+ |---------|------|
360
+ | 完整异常堆栈 | 暴露代码结构和文件路径 |
361
+ | 原始 SQL 语句 | 暴露表结构和查询逻辑 |
362
+ | 文件系统路径 | 暴露服务器目录结构 |
363
+ | 用户输入回显 | XSS 和信息泄露 |
364
+ | 数据库字段名 | 暴露数据模型 |
365
+ | 第三方 API Key | 凭证泄露 |
366
+
367
+ ### 7.6 注入防护
368
+
369
+ #### SQL 注入
370
+
371
+ ```python
372
+ # 反例:字符串拼接
373
+ result = await session.execute(text(f"SELECT * FROM users WHERE id = {user_id}"))
374
+
375
+ # 正例:ORM 参数化
376
+ result = await session.execute(select(User).where(User.id == user_id))
377
+
378
+ # 正例:原生 SQL 参数化
379
+ result = await session.execute(text("SELECT * FROM users WHERE id = :id"), {"id": user_id})
380
+ ```
381
+
382
+ #### 命令注入
383
+
384
+ - **禁止**通过 shell 拼接用户输入执行外部命令
385
+ - 必须使用参数列表 + `shell=False` 调用 `subprocess`
386
+ - 最佳实践:改用 Python 库实现同等功能(如图像处理用 Pillow,文件压缩用 zipfile),避免外部命令
387
+
388
+ #### 路径遍历
389
+
390
+ ```python
391
+ # 反例
392
+ file_path = f"/uploads/{user_input_filename}"
393
+
394
+ # 正例
395
+ from pathlib import Path
396
+ safe_name = Path(user_input_filename).name
397
+ file_path = Path("/uploads") / safe_name
398
+ ```
399
+
400
+ #### SSRF(服务端请求伪造)
401
+
402
+ 用户控制的 URL 必须经白名单 host 校验后再发起请求,防止访问内网(如 `169.254.169.254`)。
403
+
404
+ ### 7.7 软删除
405
+
406
+ - 业务数据**必须**软删除(`is_deleted` + `deleted_at`),**禁止**物理删除
407
+ - 查询时自动 `where(Model.is_deleted == False)`
408
+ - **例外**:GDPR 被遗忘权、验证码 / session 等临时数据,允许物理删除
409
+
410
+ ### 7.8 审计日志
411
+
412
+ 通过 dwyeapi 日志模块获取审计 logger,记录以下操作:
413
+
414
+ | 操作 | 记录内容 |
415
+ |------|---------|
416
+ | 登录成功/失败 | 用户名、IP、时间、User-Agent |
417
+ | 权限变更 | 操作人、目标用户、旧角色、新角色 |
418
+ | 数据删除 | 操作人、删除资源类型和 UUID |
419
+ | 敏感数据查询 | 查询人、查询条件(脱敏) |
420
+ | 密码修改 | 操作人、时间 |
421
+ | 导出操作 | 操作人、导出数据范围 |
422
+
423
+ ---
424
+
425
+ ## 八、API 限流
426
+
427
+ 使用 `slowapi`,按接口类型设置不同限流策略:
428
+
429
+ | 接口类型 | 限流 | 原因 |
430
+ |---------|------|------|
431
+ | 登录 `/auth/login` | 3 次/分钟/IP | 防暴力破解 |
432
+ | 注册 `/auth/register` | 5 次/分钟/IP | 防批量注册 |
433
+ | 普通查询 | 60 次/分钟/IP | 防爬虫 |
434
+ | 数据导出 | 5 次/分钟/IP | 防数据泄露 |
435
+ | 文件上传 | 10 次/分钟/IP | 防资源滥用 |
436
+
437
+ ---
438
+
439
+ ## 九、测试组织
440
+
441
+ ### 核心约束(强制)
442
+
443
+ - **测试目录与业务源码并列、独立存放**,禁止混入源码目录
444
+ - **禁止**在源码目录中放任何 `test_*.py` / `*_test.py` 文件(如 `app/services/user.py` 旁放 `app/services/test_user.py` 是违规)
445
+ - 测试目录内部结构应**镜像**对应源码结构,便于定位被测对象
446
+ - 测试文件命名:`test_{被测模块名}.py`
447
+ - pytest 配置在 `pyproject.toml`(`[tool.pytest.ini_options]`)中显式声明 `testpaths`
448
+ - 测试相关依赖放 `[dev]` 或独立 `[test]` extras / group,**禁止**进生产依赖
449
+ - 跨包发布的代码不得 import 测试目录下的内容
450
+
451
+ > 具体目录布局由项目结构决定(单包、monorepo 子包、扁平 `app/` 等),AI 自行判断;唯一不变的是"测试目录独立、与源码并列"。
452
+
453
+ 具体 pytest fixture、AsyncClient、Arrange-Act-Assert 等编写规范见 `dwy-python-testing` rule。
454
+
455
+ ---
456
+
457
+ ## 十、违规检测清单
458
+
459
+ AI 编写或审查后端代码时,**必须**检查以下违规模式,按严重程度执行对应动作:
460
+
461
+ ### 严重程度与动作
462
+
463
+ | 严重程度 | 动作 |
464
+ |---------|------|
465
+ | **致命** | **立即 STOP,不得继续编写或提交代码**,必须修正后重新检查 |
466
+ | **高** | 必须修正后才能继续,向用户说明违规点和修正方案 |
467
+ | **中** | 提示用户存在风险,建议修正,用户确认后可继续 |
468
+
469
+ ### 检查清单
470
+
471
+ | 检查项 | 违规模式 | 严重程度 |
472
+ |--------|---------|---------|
473
+ | 重复造轮子 | 自造异常基类 / 自封装统一响应 / 手写 bcrypt 或 JWT 调用 / 自建 Redis 连接池 等 dwyeapi 已提供能力 | 高 |
474
+ | 未查 skill | 调用 dwyeapi 能力域却未先查 `dwy-eapi` skill 拿当前 API | 高 |
475
+ | 技术层散落 | 顶级出现 `routers/` / `services/` / `models/` / `schemas/` 等技术层目录把不同功能文件混放,未按功能聚合 | 高 |
476
+ | 测试与源码混放 | 测试文件放在源码目录中(如 `app/users/test_router.py`),未独立到测试目录 | 高 |
477
+ | 密码明文 | `password` 字段出现在 Response Schema | **致命 → STOP** |
478
+ | 密码弱存储 | 使用 MD5 / SHA 而非 bcrypt | **致命 → STOP** |
479
+ | SQL 拼接 | f-string 拼接 SQL 语句 | **致命 → STOP** |
480
+ | 命令注入 | shell 拼接用户输入执行外部命令 | **致命 → STOP** |
481
+ | 硬编码密钥 | SECRET_KEY / DB 密码等硬编码 | **致命 → STOP** |
482
+ | 自增 ID 暴露 | 路由参数为 `int` 类型的 ID / Response 含 `id: int` | 高 |
483
+ | 无认证接口 | 数据接口无 `Depends(get_current_user)` | 高 |
484
+ | 全量返回 | 列表接口无分页限制 | 高 |
485
+ | 松散 body | 用 `dict` / `Any` 接收请求体 | 高 |
486
+ | 错误回显 | `str(exception)` 返回给客户端 / 错误消息含用户输入 | 高 |
487
+ | 文件名不安全 | 使用用户原始文件名做存储路径 | 高 |
488
+ | service 抛 HTTPException | service 层直接 `raise HTTPException` | 高 |
489
+ | 路由中开会话 | 路由函数中 `async with session_factory()` | 高 |
490
+ | 旧式查询 | `session.query(...)` 而非 `select(...)` | 高 |
491
+ | `datetime.now()` 直用 | 业务代码直接 `datetime.now()` 而非走 dwyeapi 时间模块 | 高 |
492
+ | 物理删除业务数据 | 对业务数据执行物理删除 | 中 |