create-dwy 0.5.1 → 0.6.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.
- package/bin/index.js +6 -15
- package/package.json +3 -3
- package/src/create.js +61 -37
- package/src/sync.js +206 -61
- package/templates/claude-global/CLAUDE.md +92 -0
- package/templates/claude-global/rules/dwy-android-core.md +610 -0
- package/templates/claude-global/rules/dwy-api-design.md +110 -0
- package/templates/claude-global/rules/dwy-db-migration.md +46 -0
- package/templates/claude-global/rules/dwy-flutter-core.md +564 -0
- package/templates/claude-global/rules/dwy-flutter-network.md +222 -0
- package/templates/claude-global/rules/dwy-flutter-router.md +97 -0
- package/templates/claude-global/rules/dwy-flutter-testing.md +94 -0
- package/templates/claude-global/rules/dwy-git-commit-convention.md +82 -0
- package/templates/claude-global/rules/dwy-git-security.md +53 -0
- package/templates/claude-global/rules/dwy-payload-limits.md +178 -0
- package/templates/claude-global/rules/{prefer-open-source.md → dwy-prefer-open-source.md} +4 -0
- package/templates/claude-global/rules/dwy-project-structure.md +181 -0
- package/templates/claude-global/rules/{python-code-style.md → dwy-python-core.md} +292 -358
- package/templates/claude-global/rules/dwy-python-fastapi.md +272 -0
- package/templates/claude-global/rules/dwy-python-orm.md +61 -0
- package/templates/claude-global/rules/{backend-security.md → dwy-python-security.md} +99 -72
- package/templates/claude-global/rules/dwy-python-testing.md +53 -0
- package/templates/claude-global/rules/{server-security.md → dwy-server-security.md} +1 -2
- package/templates/claude-global/rules/dwy-tdd-development.md +70 -0
- package/templates/claude-global/rules/dwy-tech-stack.md +60 -0
- package/templates/claude-global/rules/dwy-vue-api.md +146 -0
- package/templates/claude-global/rules/{vue-code-style.md → dwy-vue-core.md} +338 -152
- package/templates/claude-global/rules/dwy-vue-pinia.md +75 -0
- package/templates/claude-global/rules/dwy-vue-testing.md +102 -0
- package/templates/claude-global/skills/dwy-deploy-audit/SKILL.md +481 -0
- package/templates/claude-global/skills/dwy-deploy-audit/references/checks-data.md +117 -0
- package/templates/claude-global/skills/dwy-deploy-audit/references/checks-network.md +74 -0
- package/templates/claude-global/skills/dwy-deploy-audit/references/checks-runtime.md +184 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_base.sh +39 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_capacity.sh +278 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_db.sh +122 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_docker.sh +249 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_env.sh +64 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_https.sh +60 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_logs.sh +209 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_nginx.sh +92 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_resilience.sh +157 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_secrets.sh +327 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_services.sh +77 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_ssh.sh +56 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/run_all.sh +105 -0
- package/templates/claude-global/skills/dwy-docker/SKILL.md +187 -0
- package/templates/claude-global/skills/dwy-docker/references/ask-templates.md +142 -0
- package/templates/claude-global/skills/dwy-docker/references/templates.md +479 -0
- package/templates/claude-global/skills/dwy-docker/references/version-rules.md +76 -0
- package/templates/claude-global/skills/dwy-docker/scripts/query_dockerhub.py +204 -0
- package/templates/claude-global/skills/dwy-docker/scripts/scan_images.sh +185 -0
- package/templates/claude-global/skills/dwy-eapi/SKILL.md +433 -0
- package/templates/claude-global/skills/dwy-eapi/references/tasks-integration-guide.md +387 -0
- package/templates/claude-global/skills/dwy-ekit/SKILL.md +85 -0
- package/templates/claude-global/skills/dwy-ekit/references/data.md +135 -0
- package/templates/claude-global/skills/dwy-ekit/references/form.md +68 -0
- package/templates/claude-global/skills/dwy-ekit/references/request.md +183 -0
- package/templates/claude-global/skills/dwy-ekit/references/ui.md +109 -0
- package/templates/claude-global/skills/{dwy-frontend-eui → dwy-eui}/SKILL.md +129 -15
- package/templates/claude-global/skills/dwy-eui/references/eui-design-guide.md +257 -0
- package/templates/claude-global/skills/dwy-eui/references/eui-integration-guide.md +302 -0
- package/templates/claude-global/skills/dwy-eui/references/eui-landing-design-guide.md +437 -0
- package/templates/claude-global/skills/dwy-fullstack-scaffold/SKILL.md +214 -0
- package/templates/claude-global/skills/dwy-mirror-source/SKILL.md +275 -0
- package/templates/claude-global/skills/dwy-mirror-source/references/dockerfile-snippets.md +228 -0
- package/templates/claude-global/skills/dwy-mirror-source/references/mirror-providers.md +150 -0
- package/templates/claude-global/skills/dwy-mirror-source/scripts/__pycache__/check_mirrors.cpython-313.pyc +0 -0
- package/templates/claude-global/skills/dwy-mirror-source/scripts/apply_mirrors.py +666 -0
- package/templates/claude-global/skills/dwy-mirror-source/scripts/check_mirrors.py +868 -0
- package/templates/claude-global/skills/dwy-pentest/SKILL.md +344 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__init__.py +1 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/__init__.cpython-311.pyc +0 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/config.cpython-311.pyc +0 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/executor.cpython-311.pyc +0 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/severity.cpython-311.pyc +0 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/validator.cpython-311.pyc +0 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/config.py +109 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/executor.py +157 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/severity.py +96 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/lib/validator.py +113 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_auth.py +289 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_recon.py +217 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_report.py +343 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_scan.py +205 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_ssl.py +208 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_vuln.py +205 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pentest_web.py +330 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/pyproject.toml +14 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/setup_tools.sh +97 -0
- package/templates/claude-global/skills/dwy-pentest/scripts/uv.lock +415 -0
- package/templates/claude-global/skills/{dwy-release → dwy-publish}/SKILL.md +42 -18
- package/templates/claude-global/skills/dwy-vue-core/SKILL.md +890 -0
- package/templates/project/backend/.dwy.hbs +5 -0
- package/templates/project/backend/.python-version +1 -0
- package/templates/project/backend/CLAUDE.md.hbs +40 -0
- package/templates/project/{web/backend → backend}/alembic/script.py.mako +2 -0
- package/templates/project/{web/backend → backend}/alembic.ini.hbs +0 -1
- package/templates/project/backend/deploy.sh.hbs +18 -0
- package/templates/project/{web → backend}/docker-compose.prod.yml.hbs +1 -1
- package/templates/project/{web/backend → backend}/pyproject.toml.hbs +4 -5
- package/templates/project/{web/backend → backend}/src/app/config.py.hbs +1 -1
- package/templates/project/backend/src/app/database.py +10 -0
- package/templates/project/backend/src/app/dependencies.py +5 -0
- package/templates/project/{web/backend → backend}/src/app/main.py.hbs +3 -3
- package/templates/project/backend/src/app/models/base.py +5 -0
- package/templates/project/web/CLAUDE.md.hbs +29 -36
- package/templates/project/web/{frontend → apps/web}/index.html.hbs +1 -0
- package/templates/project/web/{frontend → apps/web}/package.json.hbs +4 -3
- package/templates/project/web/apps/web/src/App.vue +9 -0
- package/templates/project/web/apps/web/src/api/client.ts +5 -0
- package/templates/project/web/apps/web/src/composables/.gitkeep +0 -0
- package/templates/project/web/apps/web/src/layouts/DefaultLayout.vue +8 -0
- package/templates/project/web/{frontend → apps/web}/src/router/index.ts +1 -1
- package/templates/project/web/{frontend → apps/web}/src/stores/auth.ts +4 -2
- package/templates/project/web/apps/web/src/style.css +2 -0
- package/templates/project/web/apps/web/src/types/router.d.ts +8 -0
- package/templates/project/web/apps/web/src/views/HomeView.vue +8 -0
- package/templates/project/web/apps/web/tsconfig.app.json +18 -0
- package/templates/project/web/apps/web/tsconfig.json +6 -0
- package/templates/project/web/{frontend → apps/web}/vite.config.ts.hbs +7 -1
- package/templates/project/web/package.json.hbs +16 -0
- package/templates/project/web/packages/.gitkeep +0 -0
- package/templates/project/web/pnpm-workspace.yaml +3 -0
- package/templates/claude-global/hooks/post-sync-claude-global.sh +0 -72
- package/templates/claude-global/rules/agent-team-execution.md +0 -138
- package/templates/claude-global/rules/agent-team-patterns.md +0 -157
- package/templates/claude-global/rules/git-commit-convention.md +0 -106
- package/templates/claude-global/rules/git-security.md +0 -144
- package/templates/claude-global/skills/dwy-backend-base/SKILL.md +0 -279
- package/templates/claude-global/skills/dwy-frontend-core/SKILL.md +0 -181
- package/templates/project/web/.claude/commands/deploy.md +0 -4
- package/templates/project/web/backend/src/app/models/base.py +0 -5
- package/templates/project/web/deploy.sh.hbs +0 -113
- package/templates/project/web/frontend/src/App.vue +0 -3
- package/templates/project/web/frontend/src/api/client.ts +0 -13
- package/templates/project/web/frontend/src/style.css +0 -1
- package/templates/project/web/frontend/src/views/HomeView.vue +0 -5
- package/templates/project/web/frontend/tsconfig.app.json +0 -14
- package/templates/project/web/frontend/tsconfig.json +0 -4
- /package/templates/{project/web/backend → claude-global/skills/dwy-pentest/scripts}/.python-version +0 -0
- /package/templates/project/{web/backend → backend}/Dockerfile +0 -0
- /package/templates/project/{web/backend → backend}/alembic/env.py.hbs +0 -0
- /package/templates/{claude-global/skills → project/backend/alembic/versions}/.gitkeep +0 -0
- /package/templates/project/{web → backend}/docker-compose.dev.yml.hbs +0 -0
- /package/templates/project/{web/backend → backend}/src/app/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/src/app/models/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/src/app/routers/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/src/app/routers/health.py +0 -0
- /package/templates/project/{web/backend → backend}/src/app/schemas/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/src/app/services/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/tests/__init__.py +0 -0
- /package/templates/project/{web/backend → backend}/tests/conftest.py.hbs +0 -0
- /package/templates/project/{web/backend/alembic/versions → mobile}/.gitkeep +0 -0
- /package/templates/project/web/{frontend → apps/web}/src/components/.gitkeep +0 -0
- /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
|
-
|
|
5
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
79
|
+
# BAD: 路由直接用自增 ID
|
|
47
80
|
@router.get("/users/{user_id}")
|
|
48
81
|
async def get_user(user_id: int): ...
|
|
49
82
|
|
|
50
|
-
#
|
|
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
|
-
#
|
|
64
|
-
from
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
|
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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("用户")
|
|
222
|
-
raise BusinessError("操作失败")
|
|
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
|
-
#
|
|
286
|
+
# GOOD: ORM 参数化查询(安全)
|
|
257
287
|
result = await session.execute(select(User).where(User.id == user_id))
|
|
258
288
|
|
|
259
|
-
#
|
|
289
|
+
# GOOD: 原生 SQL 参数化(安全)
|
|
260
290
|
result = await session.execute(text("SELECT * FROM users WHERE id = :id"), {"id": user_id})
|
|
261
291
|
|
|
262
|
-
#
|
|
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
|
-
#
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
#
|
|
303
|
+
# GOOD: 参数列表(不经过 shell)
|
|
277
304
|
subprocess.run(["convert", safe_filename, "output.png"], shell=False)
|
|
278
305
|
|
|
279
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
594
|
+
# GOOD: 使用 slowapi 限流
|
|
568
595
|
from slowapi import Limiter
|
|
569
596
|
from slowapi.util import get_remote_address
|
|
570
597
|
|
|
@@ -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 与实际测试不同步
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: 跨栈技术选型强制约束(FastAPI/Vue3/Flutter/UV/pnpm/Docker/S3 等,全局加载)
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# 跨栈技术选型强制约束
|
|
6
|
+
|
|
7
|
+
每个领域只有一个方案,AI 不得选替代,不得"建议改用 XX"。
|
|
8
|
+
|
|
9
|
+
## 后端
|
|
10
|
+
|
|
11
|
+
| 领域 | 唯一方案 | 禁止替代 |
|
|
12
|
+
|------|---------|---------|
|
|
13
|
+
| Web 框架 | FastAPI | Django, Flask, Tornado, Sanic |
|
|
14
|
+
| ORM | SQLAlchemy 2.0 (异步) | Tortoise ORM, Peewee, Django ORM |
|
|
15
|
+
| 关系数据库 | PostgreSQL | MySQL, MariaDB, SQLite(生产) |
|
|
16
|
+
| 缓存 / 消息队列 | Redis | Memcached, RabbitMQ, Kafka |
|
|
17
|
+
| 异步任务 | ARQ (eapi[tasks]) | Celery, Dramatiq, Huey |
|
|
18
|
+
| 数据库迁移 | Alembic | 手动 DDL, Django migrations |
|
|
19
|
+
| 包管理 | UV | pip, Poetry, Pipenv |
|
|
20
|
+
| 密码哈希 | bcrypt (eapi security) | argon2, scrypt, 自实现 |
|
|
21
|
+
| JWT | python-jose (eapi security) | PyJWT, 自实现 |
|
|
22
|
+
| 数据校验 | Pydantic v2 | marshmallow, attrs, 手动校验 |
|
|
23
|
+
| 基础设施层 | dwyeapi | 自造框架、手写基础设施 |
|
|
24
|
+
| Linter/Formatter | Ruff | Black, isort, flake8, pylint |
|
|
25
|
+
|
|
26
|
+
## 前端(Web)
|
|
27
|
+
|
|
28
|
+
| 领域 | 唯一方案 | 禁止替代 |
|
|
29
|
+
|------|---------|---------|
|
|
30
|
+
| 框架 | Vue 3 (Composition API) | React, Svelte, Angular, Vue 2 |
|
|
31
|
+
| 构建工具 | Vite | Webpack, Rollup, esbuild 直接用 |
|
|
32
|
+
| 组件库 | @dwydev/eui | Element Plus, Ant Design Vue, Naive UI |
|
|
33
|
+
| 工具库 | @dwydev/ekit | 手写 request/storage/validators |
|
|
34
|
+
| 状态管理 | Pinia (Setup Store) | Vuex, 手动 reactive/provide |
|
|
35
|
+
| 样式 | Tailwind CSS 4 | Sass/Less 手写、CSS Modules、UnoCSS |
|
|
36
|
+
| 包管理 | pnpm | npm, yarn |
|
|
37
|
+
| 项目结构 | pnpm monorepo (workspace) | Lerna, Nx, Turborepo |
|
|
38
|
+
| 表单校验 | vee-validate + zod | 手动 v-model 校验 |
|
|
39
|
+
| 路由 | Vue Router 4 | — |
|
|
40
|
+
| 测试 | Vitest | Jest |
|
|
41
|
+
|
|
42
|
+
## 移动端 / 桌面端
|
|
43
|
+
|
|
44
|
+
| 领域 | 唯一方案 | 禁止替代 |
|
|
45
|
+
|------|---------|---------|
|
|
46
|
+
| 框架 | Flutter | React Native, Kotlin Multiplatform, MAUI |
|
|
47
|
+
| 状态管理 | Riverpod | GetX, Bloc, Provider, MobX |
|
|
48
|
+
| HTTP 客户端 | Dio | http, Chopper, Retrofit |
|
|
49
|
+
| 路由 | GoRouter | auto_route, Routemaster |
|
|
50
|
+
| 本地存储 | shared_preferences + flutter_secure_storage | Hive, Isar, sqflite(非必要时) |
|
|
51
|
+
| 代码生成 | riverpod_generator + freezed + json_serializable | 手写 fromJson/toJson |
|
|
52
|
+
|
|
53
|
+
## 基础设施
|
|
54
|
+
|
|
55
|
+
| 领域 | 唯一方案 |
|
|
56
|
+
|------|---------|
|
|
57
|
+
| 容器化 | Docker + Docker Compose |
|
|
58
|
+
| 对象存储(开发) | MinIO (S3 协议) |
|
|
59
|
+
| 对象存储(生产) | 任何 S3 兼容云服务 |
|
|
60
|
+
| 时序数据库(量化金融) | DolphinDB |
|