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
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: FastAPI 路由/Schema/依赖注入/异常/OSS 封装规范
|
|
3
|
+
paths:
|
|
4
|
+
- "**/routers/**/*.py"
|
|
5
|
+
- "**/schemas/**/*.py"
|
|
6
|
+
- "**/services/**/*.py"
|
|
7
|
+
- "**/dependencies.py"
|
|
8
|
+
- "**/exceptions.py"
|
|
9
|
+
- "**/main.py"
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# FastAPI 路由/Schema/依赖注入/异常/OSS 封装规范
|
|
13
|
+
|
|
14
|
+
## 五、FastAPI 路由与接口设计
|
|
15
|
+
|
|
16
|
+
### 路由组织
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
# routers/user.py
|
|
20
|
+
from fastapi import APIRouter, Depends, status
|
|
21
|
+
|
|
22
|
+
router = APIRouter(prefix="/users", tags=["users"])
|
|
23
|
+
|
|
24
|
+
@router.post("/", status_code=status.HTTP_201_CREATED, response_model=UserResponse)
|
|
25
|
+
async def create_user(
|
|
26
|
+
body: UserCreate,
|
|
27
|
+
service: UserService = Depends(get_user_service),
|
|
28
|
+
) -> UserResponse:
|
|
29
|
+
return await service.create(body)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### 强制规则
|
|
33
|
+
- 每个路由模块使用独立 `APIRouter`,在 `main.py` 中统一注册
|
|
34
|
+
- **禁止**所有路由写在 `main.py` 中(超过 2 个路由必须拆分)
|
|
35
|
+
- 路由函数只做:参数接收 → 调用 service → 返回结果,**不写业务逻辑**
|
|
36
|
+
- 必须显式声明 `status_code` 和 `response_model`
|
|
37
|
+
- 路由路径使用复数名词 + RESTful 风格
|
|
38
|
+
|
|
39
|
+
### RESTful 路由规范
|
|
40
|
+
|
|
41
|
+
| 操作 | 方法 | 路径 | 状态码 |
|
|
42
|
+
|---|---|---|---|
|
|
43
|
+
| 列表查询 | `GET` | `/users` | 200 |
|
|
44
|
+
| 详情查询 | `GET` | `/users/{user_id}` | 200 |
|
|
45
|
+
| 创建 | `POST` | `/users` | 201 |
|
|
46
|
+
| 全量更新 | `PUT` | `/users/{user_id}` | 200 |
|
|
47
|
+
| 部分更新 | `PATCH` | `/users/{user_id}` | 200 |
|
|
48
|
+
| 删除 | `DELETE` | `/users/{user_id}` | 204 |
|
|
49
|
+
|
|
50
|
+
### 分页查询
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
# schemas/common.py
|
|
54
|
+
class PaginationParams(BaseModel):
|
|
55
|
+
page: int = Field(default=1, ge=1)
|
|
56
|
+
page_size: int = Field(default=20, ge=1, le=100)
|
|
57
|
+
|
|
58
|
+
class PaginatedResponse[T](BaseModel):
|
|
59
|
+
items: list[T]
|
|
60
|
+
total: int
|
|
61
|
+
page: int
|
|
62
|
+
page_size: int
|
|
63
|
+
|
|
64
|
+
# routers/user.py
|
|
65
|
+
@router.get("/", response_model=PaginatedResponse[UserResponse])
|
|
66
|
+
async def list_users(
|
|
67
|
+
pagination: PaginationParams = Depends(),
|
|
68
|
+
service: UserService = Depends(get_user_service),
|
|
69
|
+
) -> PaginatedResponse[UserResponse]:
|
|
70
|
+
return await service.list(pagination)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## 六、Pydantic Schema 规范
|
|
74
|
+
|
|
75
|
+
### 强制规则
|
|
76
|
+
- 请求和响应使用不同的 Schema,**禁止**复用同一个 Model
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
# schemas/user.py
|
|
80
|
+
|
|
81
|
+
class UserBase(BaseModel):
|
|
82
|
+
"""共享字段基类"""
|
|
83
|
+
name: str = Field(max_length=50)
|
|
84
|
+
email: EmailStr
|
|
85
|
+
|
|
86
|
+
class UserCreate(UserBase):
|
|
87
|
+
"""创建请求"""
|
|
88
|
+
password: str = Field(min_length=8)
|
|
89
|
+
|
|
90
|
+
class UserUpdate(BaseModel):
|
|
91
|
+
"""更新请求 - 所有字段可选"""
|
|
92
|
+
name: str | None = Field(default=None, max_length=50)
|
|
93
|
+
email: EmailStr | None = None
|
|
94
|
+
|
|
95
|
+
class UserResponse(UserBase):
|
|
96
|
+
"""响应"""
|
|
97
|
+
id: int
|
|
98
|
+
is_active: bool
|
|
99
|
+
created_at: datetime
|
|
100
|
+
|
|
101
|
+
model_config = ConfigDict(from_attributes=True)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
- **禁止**在响应 Schema 中暴露密码、内部 ID、敏感字段
|
|
105
|
+
- 使用 `Field()` 添加校验约束(`min_length`、`ge`、`le`、`pattern` 等)
|
|
106
|
+
- 更新 Schema 的字段必须全部可选(`None` 默认值)
|
|
107
|
+
|
|
108
|
+
## 七、依赖注入
|
|
109
|
+
|
|
110
|
+
### 强制规则
|
|
111
|
+
- 数据库会话、当前用户、配置等通过 `Depends()` 注入
|
|
112
|
+
- **禁止**在路由函数中直接创建数据库会话或实例化 service
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
# dependencies.py
|
|
116
|
+
from collections.abc import AsyncGenerator
|
|
117
|
+
|
|
118
|
+
async def get_db() -> AsyncGenerator[AsyncSession]:
|
|
119
|
+
async with async_session_factory() as session:
|
|
120
|
+
yield session
|
|
121
|
+
|
|
122
|
+
async def get_current_user(
|
|
123
|
+
token: str = Depends(oauth2_scheme),
|
|
124
|
+
db: AsyncSession = Depends(get_db),
|
|
125
|
+
) -> User:
|
|
126
|
+
user = await authenticate(token, db)
|
|
127
|
+
if not user:
|
|
128
|
+
raise HTTPException(status_code=401, detail="认证失败")
|
|
129
|
+
return user
|
|
130
|
+
|
|
131
|
+
def get_user_service(db: AsyncSession = Depends(get_db)) -> UserService:
|
|
132
|
+
return UserService(db)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
# ❌ 路由中直接创建会话
|
|
137
|
+
@router.get("/users/{user_id}")
|
|
138
|
+
async def get_user(user_id: int):
|
|
139
|
+
async with async_session_factory() as db:
|
|
140
|
+
user = await db.get(User, user_id)
|
|
141
|
+
return user
|
|
142
|
+
|
|
143
|
+
# ✅ 依赖注入
|
|
144
|
+
@router.get("/users/{user_id}", response_model=UserResponse)
|
|
145
|
+
async def get_user(
|
|
146
|
+
user_id: int,
|
|
147
|
+
service: UserService = Depends(get_user_service),
|
|
148
|
+
) -> UserResponse:
|
|
149
|
+
return await service.get_by_id(user_id)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## 八、异常处理
|
|
153
|
+
|
|
154
|
+
### 统一异常体系
|
|
155
|
+
|
|
156
|
+
直接使用 `dwyeapi` 提供的异常体系,禁止自造异常基类:
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
# 从 eapi 导入(禁止自定义 AppError 子类体系)
|
|
160
|
+
from dwyeapi.exceptions import (
|
|
161
|
+
AppError, # 基类
|
|
162
|
+
NotFoundError, # 404 — NotFoundError("用户") → {"code": "NOT_FOUND", "message": "用户不存在"}
|
|
163
|
+
BusinessError, # 422 — BusinessError("余额不足", code="INSUFFICIENT_BALANCE")
|
|
164
|
+
PermissionDeniedError, # 403 — PermissionDeniedError()
|
|
165
|
+
AuthenticationError, # 401 — AuthenticationError()
|
|
166
|
+
register_exception_handlers, # 一次性注册全部 handler
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# main.py 中注册
|
|
170
|
+
from dwyeapi.exceptions import register_exception_handlers
|
|
171
|
+
register_exception_handlers(app)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
项目如需扩展异常,必须继承 `AppError`:
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
# app/exceptions.py — 项目级异常(继承 eapi AppError)
|
|
178
|
+
from dwyeapi.exceptions import AppError
|
|
179
|
+
|
|
180
|
+
class QuotaExceededError(AppError):
|
|
181
|
+
def __init__(self) -> None:
|
|
182
|
+
super().__init__(message="配额已用尽", code="QUOTA_EXCEEDED")
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### 强制规则
|
|
186
|
+
- **禁止**在 service 层抛出 `HTTPException`(service 不感知 HTTP)
|
|
187
|
+
- service 层抛出业务异常,由异常处理器统一转换为 HTTP 响应
|
|
188
|
+
- **禁止**裸 `except:` 或 `except Exception: pass`
|
|
189
|
+
- 错误响应格式统一为 `{"code": "ERROR_CODE", "message": "描述"}`
|
|
190
|
+
- **禁止**在错误消息中回显用户输入
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
# ❌ service 中抛 HTTPException
|
|
194
|
+
class UserService:
|
|
195
|
+
async def get_by_id(self, user_id: int) -> User:
|
|
196
|
+
user = await self.db.get(User, user_id)
|
|
197
|
+
if not user:
|
|
198
|
+
raise HTTPException(status_code=404) # 耦合 HTTP
|
|
199
|
+
return user
|
|
200
|
+
|
|
201
|
+
# ✅ service 中抛业务异常
|
|
202
|
+
class UserService:
|
|
203
|
+
async def get_by_id(self, user_id: int) -> User:
|
|
204
|
+
user = await self.db.get(User, user_id)
|
|
205
|
+
if not user:
|
|
206
|
+
raise NotFoundError("用户")
|
|
207
|
+
return user
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## OSS 规范(MinIO / S3 兼容)
|
|
213
|
+
|
|
214
|
+
### 核心原则
|
|
215
|
+
|
|
216
|
+
**代码只用 S3 协议,不用任何云厂商专有 API。** 通过环境变量切换 dev/prod 存储。
|
|
217
|
+
|
|
218
|
+
### 后端封装
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
# services/oss.py
|
|
222
|
+
import boto3
|
|
223
|
+
from app.config import settings
|
|
224
|
+
|
|
225
|
+
def get_s3_client():
|
|
226
|
+
return boto3.client(
|
|
227
|
+
"s3",
|
|
228
|
+
endpoint_url=settings.oss_endpoint, # dev: http://localhost:19000
|
|
229
|
+
aws_access_key_id=settings.oss_access_key,
|
|
230
|
+
aws_secret_access_key=settings.oss_secret_key,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
async def upload_file(key: str, data: bytes, content_type: str = "") -> str:
|
|
234
|
+
client = get_s3_client()
|
|
235
|
+
client.put_object(
|
|
236
|
+
Bucket=settings.oss_bucket,
|
|
237
|
+
Key=key,
|
|
238
|
+
Body=data,
|
|
239
|
+
ContentType=content_type,
|
|
240
|
+
)
|
|
241
|
+
return key
|
|
242
|
+
|
|
243
|
+
async def get_presigned_url(key: str, expires: int = 3600) -> str:
|
|
244
|
+
client = get_s3_client()
|
|
245
|
+
return client.generate_presigned_url(
|
|
246
|
+
"get_object",
|
|
247
|
+
Params={"Bucket": settings.oss_bucket, "Key": key},
|
|
248
|
+
ExpiresIn=expires,
|
|
249
|
+
)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### 环境变量
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
# .env.development(MinIO)
|
|
256
|
+
OSS_ENDPOINT=http://localhost:19000
|
|
257
|
+
OSS_ACCESS_KEY=minioadmin
|
|
258
|
+
OSS_SECRET_KEY=minioadmin
|
|
259
|
+
OSS_BUCKET=my-app
|
|
260
|
+
|
|
261
|
+
# .env.production(阿里云 OSS / AWS S3)
|
|
262
|
+
OSS_ENDPOINT=https://oss-cn-hangzhou.aliyuncs.com
|
|
263
|
+
OSS_ACCESS_KEY=your-key
|
|
264
|
+
OSS_SECRET_KEY=your-secret
|
|
265
|
+
OSS_BUCKET=my-app-prod
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### 禁止事项
|
|
269
|
+
|
|
270
|
+
- 禁止使用 `minio` Python 库(用 `boto3`,S3 协议通用)
|
|
271
|
+
- 禁止代码中出现 MinIO / 阿里云 / AWS 等厂商名称判断逻辑
|
|
272
|
+
- 禁止硬编码 bucket 名、endpoint 地址
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: SQLAlchemy 2.0 异步 ORM 规范
|
|
3
|
+
paths:
|
|
4
|
+
- "**/models/**/*.py"
|
|
5
|
+
- "**/database.py"
|
|
6
|
+
- "**/db.py"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# SQLAlchemy 2.0 异步 ORM 规范
|
|
10
|
+
|
|
11
|
+
## 九、数据库与 ORM
|
|
12
|
+
|
|
13
|
+
### 强制规则
|
|
14
|
+
- 使用 **SQLAlchemy 2.0** 风格(声明式映射 + async)
|
|
15
|
+
- 数据库迁移使用 **Alembic**,**禁止**手动执行 DDL
|
|
16
|
+
- **禁止**在路由层直接写 SQL 查询
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
# models/base.py
|
|
20
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
|
21
|
+
from sqlalchemy import func
|
|
22
|
+
from datetime import datetime
|
|
23
|
+
|
|
24
|
+
class Base(DeclarativeBase):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
class TimestampMixin:
|
|
28
|
+
created_at: Mapped[datetime] = mapped_column(server_default=func.now())
|
|
29
|
+
updated_at: Mapped[datetime] = mapped_column(server_default=func.now(), onupdate=func.now())
|
|
30
|
+
|
|
31
|
+
# models/user.py
|
|
32
|
+
class User(Base, TimestampMixin):
|
|
33
|
+
__tablename__ = "users"
|
|
34
|
+
|
|
35
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
36
|
+
name: Mapped[str] = mapped_column(String(50))
|
|
37
|
+
email: Mapped[str] = mapped_column(String(100), unique=True, index=True)
|
|
38
|
+
hashed_password: Mapped[str] = mapped_column(String(128))
|
|
39
|
+
is_active: Mapped[bool] = mapped_column(default=True)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 异步数据库会话
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
# config.py / database.py
|
|
46
|
+
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
|
|
47
|
+
|
|
48
|
+
engine = create_async_engine(settings.database_url, echo=settings.debug)
|
|
49
|
+
async_session_factory = async_sessionmaker(engine, expire_on_commit=False)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 查询规范
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
# ❌ 旧式查询
|
|
56
|
+
session.query(User).filter(User.id == user_id).first()
|
|
57
|
+
|
|
58
|
+
# ✅ SQLAlchemy 2.0 风格
|
|
59
|
+
result = await session.execute(select(User).where(User.id == user_id))
|
|
60
|
+
user = result.scalar_one_or_none()
|
|
61
|
+
```
|