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,8 +1,41 @@
1
1
  ---
2
+ description: Python 后端安全编码(认证/校验/注入/存储/RBAC 等)
2
3
  paths:
3
4
  - "**/*.py"
4
- - "**/alembic/**"
5
- - "**/pyproject.toml"
5
+ ---
6
+
7
+ # Python 后端安全编码
8
+
9
+ ## 十九、安全编码
10
+
11
+ ### 强制规则
12
+ - SQL 查询必须使用参数化绑定,**禁止**字符串拼接
13
+ - 用户输入必须校验后使用,**禁止**直接信任
14
+ - 文件路径操作必须防止路径遍历攻击
15
+ - 序列化/反序列化**禁止**用不安全的反序列化库处理不可信数据
16
+ - 密码存储必须使用 `bcrypt` / `argon2`,**禁止**明文或 MD5/SHA
17
+ - JWT 密钥从环境变量读取,**禁止**硬编码
18
+
19
+ ```python
20
+ # BAD: SQL 注入
21
+ cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")
22
+
23
+ # GOOD: 参数化查询
24
+ cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
25
+
26
+ # BAD: 路径遍历
27
+ file_path = f"/uploads/{user_input_filename}"
28
+
29
+ # GOOD: 路径安全校验
30
+ safe_path = Path("/uploads") / Path(user_input_filename).name
31
+
32
+ # BAD: 硬编码密钥
33
+ SECRET_KEY = "my-super-secret-key"
34
+
35
+ # GOOD: 环境变量
36
+ SECRET_KEY = settings.secret_key
37
+ ```
38
+
6
39
  ---
7
40
 
8
41
  # 后端安全编码规范
@@ -17,7 +50,7 @@ FastAPI + PostgreSQL + Redis + MinIO + DolphinDB 项目的安全编码规则。A
17
50
  ### 强制规则
18
51
 
19
52
  ```python
20
- # ✅ 正确 — ORM 模型同时有 id 和 uuid
53
+ # GOOD: ORM 模型同时有 id 和 uuid
21
54
  import uuid as uuid_lib
22
55
  from sqlalchemy import String
23
56
  from sqlalchemy.orm import Mapped, mapped_column
@@ -30,24 +63,24 @@ class User(Base, TimestampMixin):
30
63
  name: Mapped[str] = mapped_column(String(50))
31
64
 
32
65
 
33
- # ✅ 正确 — 路由用 UUID 查询
66
+ # GOOD: 路由用 UUID 查询
34
67
  @router.get("/users/{user_uuid}", response_model=UserResponse)
35
68
  async def get_user(user_uuid: str, service: UserService = Depends(get_user_service)):
36
69
  return await service.get_by_uuid(user_uuid)
37
70
 
38
71
 
39
- # ✅ 正确 — Response Schema 只返回 uuid,不返回 id
72
+ # GOOD: Response Schema 只返回 uuid,不返回 id
40
73
  class UserResponse(BaseModel):
41
74
  uuid: str
42
75
  name: str
43
76
  # 不暴露 id 字段
44
77
 
45
78
 
46
- # ❌ 违规 — 路由直接用自增 ID
79
+ # BAD: 路由直接用自增 ID
47
80
  @router.get("/users/{user_id}")
48
81
  async def get_user(user_id: int): ...
49
82
 
50
- # ❌ 违规 — Response 暴露自增 ID
83
+ # BAD: Response 暴露自增 ID
51
84
  class UserResponse(BaseModel):
52
85
  id: int # 攻击者可遍历 1,2,3...
53
86
  ```
@@ -60,19 +93,19 @@ class UserResponse(BaseModel):
60
93
  ### 强制规则
61
94
 
62
95
  ```python
63
- # ✅ 正确 — bcrypt 存储
64
- from danweiyuan_base.security import hash_password, verify_password
96
+ # GOOD: bcrypt 存储
97
+ from dwyeapi.security import hash_password, verify_password
65
98
 
66
99
  hashed = hash_password("user_password") # 存储到数据库
67
100
  is_valid = verify_password("input", hashed) # 验证时比对
68
101
 
69
- # ❌ 违规 — 明文存储
102
+ # BAD: 明文存储
70
103
  user.password = "123456"
71
104
 
72
- # ❌ 违规 — MD5/SHA 存储(可被彩虹表破解)
105
+ # BAD: MD5/SHA 存储(可被彩虹表破解)
73
106
  user.password = hashlib.md5(password.encode()).hexdigest()
74
107
 
75
- # ❌ 违规 — 接口返回密码字段
108
+ # BAD: 接口返回密码字段
76
109
  class UserResponse(BaseModel):
77
110
  id: int
78
111
  name: str
@@ -91,18 +124,18 @@ class UserResponse(BaseModel):
91
124
  ### 强制规则
92
125
 
93
126
  ```python
94
- # ✅ 所有 API 必须 JWT 认证(白名单除外)
127
+ # GOOD: 所有 API 必须 JWT 认证(白名单除外)
95
128
  WHITELIST = ["/api/health", "/api/auth/login", "/api/auth/register"]
96
129
 
97
- # ✅ 路由级别认证
130
+ # GOOD: 路由级别认证
98
131
  @router.get("/users", dependencies=[Depends(get_current_user)])
99
132
  async def list_users(): ...
100
133
 
101
- # ✅ 角色检查
134
+ # GOOD: 角色检查
102
135
  @router.delete("/users/{uuid}", dependencies=[Depends(require_admin)])
103
136
  async def delete_user(uuid: str): ...
104
137
 
105
- # ❌ 违规 — 无认证的数据接口
138
+ # BAD: 无认证的数据接口
106
139
  @router.get("/users")
107
140
  async def list_users(): ...
108
141
  ```
@@ -122,23 +155,23 @@ async def list_users(): ...
122
155
  ### 强制规则
123
156
 
124
157
  ```python
125
- # ✅ Pydantic Schema 严格校验
158
+ # GOOD: Pydantic Schema 严格校验
126
159
  class UserCreate(BaseModel):
127
160
  name: str = Field(min_length=2, max_length=50)
128
161
  email: EmailStr
129
162
  password: str = Field(min_length=8, max_length=128)
130
163
  age: int = Field(ge=0, le=150)
131
164
 
132
- # ✅ 列表查询参数限制
165
+ # GOOD: 列表查询参数限制
133
166
  class ListParams(BaseModel):
134
167
  page: int = Field(default=1, ge=1)
135
168
  page_size: int = Field(default=20, ge=1, le=100) # 上限 100
136
169
 
137
- # ❌ 违规 — 不校验直接使用
170
+ # BAD: 不校验直接使用
138
171
  @router.post("/users")
139
172
  async def create_user(data: dict): ...
140
173
 
141
- # ❌ 违规 — page_size 无上限
174
+ # BAD: page_size 无上限
142
175
  class ListParams(BaseModel):
143
176
  page_size: int = 20 # 攻击者可传 999999
144
177
  ```
@@ -147,20 +180,17 @@ class ListParams(BaseModel):
147
180
 
148
181
  | 字段类型 | 校验 |
149
182
  |---------|------|
150
- | 字符串 | `min_length` + `max_length` |
151
- | 数字 | `ge` + `le`(最小值 + 最大值) |
183
+ | 数字 | `ge` + `le`(最小值 + 最大值) |
152
184
  | 邮箱 | `EmailStr` |
153
185
  | 手机号 | `pattern=r"^1[3-9]\d{9}$"` |
154
186
  | URL | `HttpUrl` |
155
- | 列表 | `max_length`(限制数组长度) |
156
- | 文件大小 | 后端二次校验(不信任前端) |
157
187
 
158
188
  ## 五、响应安全
159
189
 
160
190
  ### 强制规则
161
191
 
162
192
  ```python
163
- # ✅ Response Schema 严格控制返回字段
193
+ # GOOD: Response Schema 严格控制返回字段
164
194
  class UserResponse(BaseModel):
165
195
  uuid: str
166
196
  name: str
@@ -170,17 +200,17 @@ class UserResponse(BaseModel):
170
200
 
171
201
  model_config = ConfigDict(from_attributes=True)
172
202
 
173
- # ❌ 违规 — 直接返回 ORM 对象
203
+ # BAD: 直接返回 ORM 对象
174
204
  @router.get("/users/{uuid}")
175
205
  async def get_user(uuid: str, db=Depends(get_db)):
176
206
  user = await db.get(User, uuid)
177
207
  return user # 可能暴露 password、内部 ID 等
178
208
 
179
- # ✅ 列表接口必须分页
209
+ # GOOD: 列表接口必须分页
180
210
  @router.get("/users", response_model=PaginatedResponse[UserResponse])
181
211
  async def list_users(params: PaginationParams = Depends()): ...
182
212
 
183
- # ❌ 违规 — 无分页返回全量数据
213
+ # BAD: 无分页返回全量数据
184
214
  @router.get("/users")
185
215
  async def list_users():
186
216
  return await db.execute(select(User)) # 全量泄露
@@ -189,7 +219,7 @@ async def list_users():
189
219
  ### PII 脱敏
190
220
 
191
221
  ```python
192
- # ✅ 工具函数
222
+ # GOOD: 工具函数
193
223
  def mask_phone(phone: str) -> str:
194
224
  """138****5678"""
195
225
  if len(phone) != 11:
@@ -202,7 +232,7 @@ def mask_id_card(id_card: str) -> str:
202
232
  return id_card
203
233
  return id_card[:3] + "***********" + id_card[14:]
204
234
 
205
- # ✅ 在 Response Schema 中使用 validator 脱敏
235
+ # GOOD: 在 Response Schema 中使用 validator 脱敏
206
236
  class UserResponse(BaseModel):
207
237
  phone: str
208
238
 
@@ -217,22 +247,22 @@ class UserResponse(BaseModel):
217
247
  ### 强制规则
218
248
 
219
249
  ```python
220
- # ✅ 生产环境只返回通用错误
221
- raise NotFoundError("用户") # → {"code": "NOT_FOUND", "message": "用户不存在"}
222
- raise BusinessError("操作失败") # → {"code": "UNKNOWN_ERROR", "message": "操作失败"}
250
+ # GOOD: 生产环境只返回通用错误
251
+ raise NotFoundError("用户")
252
+ raise BusinessError("操作失败")
223
253
 
224
- # ❌ 违规 — 回显用户输入
254
+ # BAD: 回显用户输入
225
255
  raise BusinessError(f"未找到: {user_input}") # XSS/信息泄露
226
256
 
227
- # ❌ 违规 — 暴露内部错误
257
+ # BAD: 暴露内部错误
228
258
  except Exception as e:
229
259
  return {"error": str(e)} # 可能包含 SQL、文件路径、堆栈
230
260
 
231
- # ❌ 违规 — 暴露数据库结构
261
+ # BAD: 暴露数据库结构
232
262
  except IntegrityError as e:
233
263
  return {"error": f"数据库错误: {e}"} # 暴露表名、字段名
234
264
 
235
- # ✅ 正确 — 统一错误处理
265
+ # GOOD: 统一错误处理
236
266
  except IntegrityError:
237
267
  raise BusinessError("数据冲突,请检查是否重复提交", code="CONFLICT")
238
268
  ```
@@ -253,30 +283,27 @@ except IntegrityError:
253
283
  ### SQL 注入
254
284
 
255
285
  ```python
256
- # ✅ ORM 参数化查询(安全)
286
+ # GOOD: ORM 参数化查询(安全)
257
287
  result = await session.execute(select(User).where(User.id == user_id))
258
288
 
259
- # ✅ 原生 SQL 参数化(安全)
289
+ # GOOD: 原生 SQL 参数化(安全)
260
290
  result = await session.execute(text("SELECT * FROM users WHERE id = :id"), {"id": user_id})
261
291
 
262
- # ❌ 违规 — 字符串拼接(SQL 注入)
292
+ # BAD: 字符串拼接(SQL 注入)
263
293
  result = await session.execute(text(f"SELECT * FROM users WHERE id = {user_id}"))
264
294
  ```
265
295
 
266
296
  ### 命令注入
267
297
 
268
298
  ```python
269
- # ❌ 违规 — shell 命令注入
270
- import os
271
- os.system(f"convert {user_filename} output.png")
272
-
273
- import subprocess
274
- subprocess.run(f"ls {user_path}", shell=True)
299
+ # BAD: shell 命令注入(禁止)
300
+ # 使用 os 模块的 system 调用执行用户输入拼接的命令 → 注入风险
301
+ # 使用 subprocess.run(... , shell=True) 执行用户输入拼接的字符串 → 注入风险
275
302
 
276
- # ✅ 正确 — 参数列表(不经过 shell)
303
+ # GOOD: 参数列表(不经过 shell)
277
304
  subprocess.run(["convert", safe_filename, "output.png"], shell=False)
278
305
 
279
- # ✅ 最佳 — 不调用外部命令,用 Python 库
306
+ # BEST: 不调用外部命令,用 Python 库
280
307
  from PIL import Image
281
308
  img = Image.open(safe_path)
282
309
  ```
@@ -284,10 +311,10 @@ img = Image.open(safe_path)
284
311
  ### 路径遍历
285
312
 
286
313
  ```python
287
- # ❌ 违规 — 用户控制文件路径
314
+ # BAD: 用户控制文件路径
288
315
  file_path = f"/uploads/{user_input_filename}"
289
316
 
290
- # ✅ 正确 — 安全提取文件名
317
+ # GOOD: 安全提取文件名
291
318
  from pathlib import Path
292
319
  safe_name = Path(user_input_filename).name # 去除 ../../../ 等
293
320
  file_path = Path("/uploads") / safe_name
@@ -296,11 +323,11 @@ file_path = Path("/uploads") / safe_name
296
323
  ### SSRF(服务端请求伪造)
297
324
 
298
325
  ```python
299
- # ❌ 违规 — 用户控制 URL 发起请求
326
+ # BAD: 用户控制 URL 发起请求
300
327
  url = request_body.url # 用户传入
301
328
  response = await httpx.get(url) # 可能访问内网 169.254.169.254
302
329
 
303
- # ✅ 正确 — 白名单限制
330
+ # GOOD: 白名单限制
304
331
  ALLOWED_HOSTS = ["api.example.com", "cdn.example.com"]
305
332
  parsed = urlparse(url)
306
333
  if parsed.hostname not in ALLOWED_HOSTS:
@@ -312,7 +339,7 @@ if parsed.hostname not in ALLOWED_HOSTS:
312
339
  ### 强制规则
313
340
 
314
341
  ```python
315
- # ✅ 模型添加软删除字段
342
+ # GOOD: 模型添加软删除字段
316
343
  class User(Base, TimestampMixin):
317
344
  __tablename__ = "users"
318
345
 
@@ -320,21 +347,21 @@ class User(Base, TimestampMixin):
320
347
  is_deleted: Mapped[bool] = mapped_column(default=False)
321
348
  deleted_at: Mapped[datetime | None] = mapped_column(default=None)
322
349
 
323
- # ✅ 删除操作只更新标记
350
+ # GOOD: 删除操作只更新标记
324
351
  async def delete_user(self, user_uuid: str) -> None:
325
352
  user = await self.get_by_uuid(user_uuid)
326
353
  user.is_deleted = True
327
354
  user.deleted_at = datetime.now(timezone.utc)
328
355
  await self.db.commit()
329
356
 
330
- # ✅ 查询时自动排除已删除
357
+ # GOOD: 查询时自动排除已删除
331
358
  async def list_users(self) -> list[User]:
332
359
  result = await self.db.execute(
333
360
  select(User).where(User.is_deleted == False)
334
361
  )
335
362
  return list(result.scalars().all())
336
363
 
337
- # ❌ 违规 — 物理删除
364
+ # BAD: 物理删除
338
365
  async def delete_user(self, user_id: int) -> None:
339
366
  user = await self.db.get(User, user_id)
340
367
  await self.db.delete(user) # 数据永久丢失
@@ -362,7 +389,7 @@ async def delete_user(self, user_id: int) -> None:
362
389
  | 导出操作 | 操作人、导出数据范围 |
363
390
 
364
391
  ```python
365
- # ✅ 审计日志示例
392
+ # GOOD: 审计日志示例
366
393
  import logging
367
394
 
368
395
  audit_logger = logging.getLogger("audit")
@@ -402,7 +429,7 @@ GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app_user;
402
429
  ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_user;
403
430
  ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO app_user;
404
431
 
405
- -- ❌ 禁止给应用用户的权限
432
+ -- 禁止给应用用户的权限
406
433
  -- GRANT ALL PRIVILEGES
407
434
  -- SUPERUSER
408
435
  -- CREATEDB
@@ -438,17 +465,17 @@ maxmemory-policy allkeys-lru
438
465
 
439
466
  ### Key 设计规范
440
467
  ```python
441
- # ✅ 正确 — 有前缀、有 TTL
468
+ # GOOD: 有前缀、有 TTL
442
469
  await redis.set("myapp:token:blacklist:abc123", "1", ex=3600)
443
470
  await redis.set("myapp:cache:user:uuid123", json.dumps(user_data), ex=300)
444
471
 
445
- # ❌ 违规 — 无前缀
472
+ # BAD: 无前缀
446
473
  await redis.set("abc123", "1")
447
474
 
448
- # ❌ 违规 — 无 TTL
475
+ # BAD: 无 TTL
449
476
  await redis.set("myapp:data:xxx", value) # 永不过期,内存泄漏
450
477
 
451
- # ❌ 违规 — 存储敏感原文
478
+ # BAD: 存储敏感原文
452
479
  await redis.set("myapp:user:password", "123456")
453
480
  ```
454
481
 
@@ -465,7 +492,7 @@ await redis.set("myapp:user:password", "123456")
465
492
  | 文件大小 | 后端二次校验大小限制(不信任前端或 Nginx) |
466
493
 
467
494
  ```python
468
- # ✅ 正确 — UUID 重命名 + 预签名 URL
495
+ # GOOD: UUID 重命名 + 预签名 URL
469
496
  import uuid as uuid_lib
470
497
  from minio import Minio
471
498
 
@@ -488,13 +515,13 @@ def get_download_url(filename: str) -> str:
488
515
  return client.presigned_get_object("uploads", filename, expires=timedelta(minutes=15))
489
516
 
490
517
 
491
- # ❌ 违规 — 用户原始文件名
518
+ # BAD: 用户原始文件名
492
519
  safe_name = file.filename # 可能包含 ../../../etc/passwd
493
520
 
494
- # ❌ 违规 — 公开 bucket
521
+ # BAD: 公开 bucket
495
522
  client.set_bucket_policy("uploads", public_read_policy)
496
523
 
497
- # ❌ 违规 — 只检查扩展名
524
+ # BAD: 只检查扩展名
498
525
  if file.filename.endswith(".jpg"): # 攻击者可改扩展名
499
526
  ```
500
527
 
@@ -510,7 +537,7 @@ if file.filename.endswith(".jpg"): # 攻击者可改扩展名
510
537
  | 端口隔离 | DolphinDB 端口**禁止对外暴露**,仅 Docker 内部网络或 127.0.0.1 |
511
538
 
512
539
  ```python
513
- # ✅ 正确 — 白名单验证后再插入脚本
540
+ # GOOD: 白名单验证后再插入脚本
514
541
  import re
515
542
 
516
543
  SAFE_COLUMN_NAME = re.compile(r'^[a-zA-Z_][a-zA-Z0-9_]{0,63}$')
@@ -526,17 +553,17 @@ col = validate_column_name(user_input_column)
526
553
  script = f"select {col} from loadTable('dfs://mydb', 'mytable')"
527
554
 
528
555
 
529
- # ❌ 违规 — 直接拼接用户输入
556
+ # BAD: 直接拼接用户输入
530
557
  script = f"select * from loadTable('dfs://mydb', '{user_input}')" # DolphinDB 注入
531
558
 
532
- # ❌ 违规 — 无验证
559
+ # BAD: 无验证
533
560
  factor_name = request.query_params.get("factor")
534
561
  script = f"select * from factors where factor_name = '{factor_name}'"
535
562
  ```
536
563
 
537
564
  ### DolphinDB 连接安全
538
565
  ```python
539
- # ✅ 正确 — 从环境变量读取
566
+ # GOOD: 从环境变量读取
540
567
  ddb_conn = ddb.Session()
541
568
  ddb_conn.connect(
542
569
  host=settings.dolphindb_host,
@@ -545,7 +572,7 @@ ddb_conn.connect(
545
572
  password=settings.dolphindb_password,
546
573
  )
547
574
 
548
- # ❌ 违规 — 硬编码
575
+ # BAD: 硬编码
549
576
  ddb_conn.connect("localhost", 8848, "admin", "123456")
550
577
  ```
551
578
 
@@ -564,7 +591,7 @@ ddb_conn.connect("localhost", 8848, "admin", "123456")
564
591
  | 文件上传 | 10 次/分钟/IP | 防资源滥用 |
565
592
 
566
593
  ```python
567
- # ✅ 使用 slowapi 限流
594
+ # GOOD: 使用 slowapi 限流
568
595
  from slowapi import Limiter
569
596
  from slowapi.util import get_remote_address
570
597
 
@@ -581,19 +608,29 @@ async def export_users(request: Request): ...
581
608
 
582
609
  ## 十五、违规检测清单
583
610
 
584
- AI 在编写或审查代码时,必须检查以下违规模式:
611
+ **AI 在编写或审查代码时,必须检查以下违规模式。检测到违规时按严重程度执行对应动作:**
612
+
613
+ ### 严重程度与动作
614
+
615
+ | 严重程度 | 动作 |
616
+ |---------|------|
617
+ | **致命** | **立即 STOP,不得继续编写或提交代码。** 必须修正后重新检查,修正前不执行任何其他操作。 |
618
+ | **高** | 必须修正后才能继续。向用户说明违规点和修正方案。 |
619
+ | **中** | 提示用户存在风险,建议修正。用户确认后可继续。 |
620
+
621
+ ### 检查清单
585
622
 
586
623
  | 检查项 | 违规模式 | 严重程度 |
587
624
  |--------|---------|---------|
625
+ | 密码明文 | `password` 字段出现在 Response Schema | **致命 → STOP** |
626
+ | 密码弱存储 | 使用 MD5/SHA 而非 bcrypt | **致命 → STOP** |
627
+ | SQL 拼接 | f-string 拼接 SQL 语句 | **致命 → STOP** |
628
+ | 命令注入 | 直接执行 shell 命令或 `shell=True` | **致命 → STOP** |
629
+ | DolphinDB 拼接 | 用户输入直接进 DolphinDB 脚本 | **致命 → STOP** |
588
630
  | 自增 ID 暴露 | 路由参数为 `int` 类型的 ID | 高 |
589
- | 密码明文 | `password` 字段出现在 Response Schema | 致命 |
590
- | 密码弱存储 | 使用 MD5/SHA 而非 bcrypt | 致命 |
591
631
  | 无认证接口 | 数据接口无 `Depends(get_current_user)` | 高 |
592
- | SQL 拼接 | `f"SELECT ... {user_input}"` | 致命 |
593
- | 命令注入 | `os.system()` / `subprocess(shell=True)` | 致命 |
594
632
  | 全量返回 | 列表接口无分页限制 | 高 |
595
633
  | 错误回显 | `str(exception)` 返回给客户端 | 高 |
634
+ | 文件名不安全 | 使用用户原始文件名做存储路径 | 高 |
596
635
  | 物理删除 | `session.delete()` 用于业务数据 | 中 |
597
636
  | Redis 无 TTL | `redis.set()` 没有 `ex` 参数 | 中 |
598
- | 文件名不安全 | 使用用户原始文件名做存储路径 | 高 |
599
- | DolphinDB 拼接 | 用户输入直接进 DolphinDB 脚本 | 致命 |
@@ -0,0 +1,53 @@
1
+ ---
2
+ description: Python pytest 测试规范
3
+ paths:
4
+ - "**/tests/**/*.py"
5
+ - "**/test_*.py"
6
+ - "**/*_test.py"
7
+ - "**/conftest.py"
8
+ ---
9
+
10
+ # Python pytest 测试规范
11
+
12
+ ## 十四、测试
13
+
14
+ ### 强制规则
15
+ - 使用 `pytest` + `pytest-asyncio` + `httpx`
16
+ - 使用 `TestClient` 或 `AsyncClient` 测试 API
17
+
18
+ ```python
19
+ # conftest.py
20
+ import pytest
21
+ from httpx import ASGITransport, AsyncClient
22
+ from app.main import app
23
+
24
+ @pytest.fixture
25
+ async def client():
26
+ async with AsyncClient(
27
+ transport=ASGITransport(app=app),
28
+ base_url="http://test",
29
+ ) as ac:
30
+ yield ac
31
+
32
+ # test_user.py
33
+ @pytest.mark.anyio
34
+ async def test_create_user_returns_201(client: AsyncClient):
35
+ response = await client.post("/users/", json={
36
+ "name": "Alice",
37
+ "email": "alice@example.com",
38
+ "password": "securepass123",
39
+ })
40
+ assert response.status_code == 201
41
+ data = response.json()
42
+ assert data["name"] == "Alice"
43
+ assert "password" not in data # 响应不包含密码
44
+
45
+ @pytest.mark.anyio
46
+ async def test_get_nonexistent_user_returns_404(client: AsyncClient):
47
+ response = await client.get("/users/99999")
48
+ assert response.status_code == 404
49
+ ```
50
+
51
+ - 测试函数名描述行为而非实现
52
+ - 测试数据使用 `pytest.fixture`,不要硬编码
53
+ - 测试结构遵循 Arrange-Act-Assert
@@ -1,4 +1,5 @@
1
1
  ---
2
+ description: 服务器/容器/Nginx/SSH/CORS/部署安全规范
2
3
  paths:
3
4
  - "**/docker-compose*.yml"
4
5
  - "**/Dockerfile*"
@@ -131,7 +132,6 @@ certbot --nginx -d example.com
131
132
 
132
133
  **4. 全局限制(server 块)**
133
134
  ```nginx
134
- client_max_body_size 1m; # 无文件上传业务时默认 1m
135
135
  client_body_timeout 10s;
136
136
  client_header_timeout 10s;
137
137
  limit_conn conn_limit 20;
@@ -202,7 +202,6 @@ if ($is_scanner) {
202
202
  ### 违规检测
203
203
  如果 Nginx 配置中出现以下情况,必须提示用户:
204
204
  - 缺少 `limit_req` 频率限制
205
- - `client_max_body_size` 大于 10m(除非有文件上传业务)
206
205
  - API 路径允许 PUT/DELETE 但业务不需要
207
206
  - 缺少 `location ~ /\.` 敏感文件屏蔽
208
207
  - 使用 `proxy_pass` 但未设置 `proxy_connect_timeout`
@@ -0,0 +1,70 @@
1
+ ---
2
+ description: TDD 开发流程(新功能/bug 修复/重构的测试先行与回归策略)
3
+ ---
4
+
5
+ # TDD 开发规则
6
+
7
+ 所有代码变更必须遵循测试驱动开发流程。测试不是事后补的,是开发的起点。
8
+
9
+ ## 开发流程
10
+
11
+ ### 新功能
12
+
13
+ 1. 先写测试 — 定义预期行为,测试此时应失败
14
+ 2. 再写实现 — 用最简代码让测试通过
15
+ 3. 重构优化 — 在测试保护下重构,确保始终通过
16
+
17
+ ### Bug 修复
18
+
19
+ 1. 先写复现测试 — 写一个能暴露 bug 的测试用例,确认它失败
20
+ 2. 再修代码 — 修复 bug,确认测试通过
21
+ 3. 跑回归 — 确认没有引入新问题
22
+
23
+ ### 重构
24
+
25
+ 1. 先跑现有测试 — 确认当前全部通过
26
+ 2. 再重构 — 修改代码
27
+ 3. 再跑测试 — 确认行为没变
28
+
29
+ ## 回归测试
30
+
31
+ ### 开发中:只跑受影响的测试
32
+
33
+ 每次代码改动后,只运行受影响的测试文件,提高开发效率:
34
+
35
+ ```bash
36
+ # 后端:只跑改动涉及的测试文件
37
+ uv run pytest tests/test_factor.py -v # 改了 factor 相关代码
38
+ uv run pytest tests/test_tick.py tests/test_upload.py -v # 改了多个模块
39
+
40
+ # 前端:只跑对应目录
41
+ pnpm vitest run tests/stores/ # 改了 store
42
+ pnpm vitest run tests/utils/format.test.ts # 改了单个工具函数
43
+ ```
44
+
45
+ 判断"受影响的测试"的规则:
46
+ - 改了 `services/xxx.py` → 跑 `tests/test_xxx.py`
47
+ - 改了 `routers/xxx.py` → 跑 `tests/test_xxx.py`
48
+ - 改了 `schemas/` 或 `models/` → 跑引用了它们的测试
49
+ - 改了 `conftest.py` 或公共依赖 → 全量回归
50
+ - 不确定影响范围 → 全量回归
51
+
52
+ ### 提交前:跑受影响的测试即可
53
+
54
+ 提交前确保受影响的测试通过即可,不要求全量回归。全量测试留给 CI 或手动触发。
55
+
56
+ ## TEST_CASES.md
57
+
58
+ 每个包/项目维护一份 `TEST_CASES.md`,记录所有测试用例的清单:
59
+
60
+ - 人类可读,方便审查和手动编辑
61
+ - 新增/修改/删除功能时必须同步更新
62
+ - 内容与实际测试文件保持一致
63
+ - 作为版本回测基准
64
+
65
+ ## 禁止事项
66
+
67
+ - **禁止**不写测试就提交功能代码
68
+ - **禁止**写完代码再补测试(先测试后代码)
69
+ - **禁止**测试不通过就提交
70
+ - **禁止** TEST_CASES.md 与实际测试不同步