create-dwy 0.9.1 → 0.11.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.
- package/package.json +3 -2
- package/src/create.js +5 -2
- package/src/prompts/grouped-checkbox.js +223 -0
- package/src/sync.js +45 -19
- package/templates/claude-global/hooks/pre-git-commit-sensitive-check.sh +0 -0
- package/templates/claude-global/rules/dwy-android-core.md +1 -0
- package/templates/claude-global/rules/dwy-db-migration.md +1 -0
- package/templates/claude-global/rules/dwy-flutter-core.md +1 -0
- package/templates/claude-global/rules/dwy-flutter-network.md +1 -0
- package/templates/claude-global/rules/dwy-flutter-router.md +1 -0
- package/templates/claude-global/rules/dwy-flutter-testing.md +1 -0
- package/templates/claude-global/rules/dwy-postgres.md +1 -0
- package/templates/claude-global/rules/dwy-python-backend.md +44 -3
- package/templates/claude-global/rules/dwy-python-core.md +36 -2
- package/templates/claude-global/rules/dwy-python-testing.md +1 -0
- package/templates/claude-global/rules/dwy-redis.md +1 -0
- package/templates/claude-global/rules/dwy-tdd-development.md +1 -0
- package/templates/claude-global/rules/dwy-vue-core.md +3 -43
- package/templates/claude-global/rules/dwy-vue-pinia.md +1 -0
- package/templates/claude-global/rules/dwy-vue-testing.md +1 -0
- package/templates/claude-global/skills/dwy-cibuildwheel/SKILL.md +355 -0
- package/templates/claude-global/skills/dwy-cibuildwheel/references/release-workflow-template.yml +99 -0
- package/templates/claude-global/skills/dwy-deploy-audit/SKILL.md +1 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_base.sh +0 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_capacity.sh +0 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_db.sh +0 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_docker.sh +0 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_env.sh +0 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_https.sh +0 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_logs.sh +0 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_nginx.sh +0 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_resilience.sh +0 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_secrets.sh +0 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_services.sh +0 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_ssh.sh +0 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/run_all.sh +0 -0
- package/templates/claude-global/skills/dwy-dev-qa/SKILL.md +1 -0
- package/templates/claude-global/skills/dwy-docker/SKILL.md +1 -0
- package/templates/claude-global/skills/dwy-docker/scripts/query_dockerhub.py +0 -0
- package/templates/claude-global/skills/dwy-docker/scripts/scan_images.sh +0 -0
- package/templates/claude-global/skills/dwy-dolphindb/SKILL.md +123 -76
- package/templates/claude-global/skills/dwy-dolphindb/references/create-database-table.md +219 -0
- package/templates/claude-global/skills/dwy-dolphindb/references/crud-operations.md +281 -0
- package/templates/claude-global/skills/dwy-dolphindb/references/data-import.md +271 -0
- package/templates/claude-global/skills/dwy-dolphindb/references/docker-deployment.md +63 -0
- package/templates/claude-global/skills/dwy-dolphindb/references/performance-tuning.md +223 -0
- package/templates/claude-global/skills/dwy-dolphindb/references/pitfalls-and-best-practices.md +352 -0
- package/templates/claude-global/skills/dwy-dolphindb/references/python-api.md +306 -0
- package/templates/claude-global/skills/dwy-dolphindb/references/setup-and-connection.md +143 -0
- package/templates/claude-global/skills/dwy-dolphindb/references/sources.md +46 -0
- package/templates/claude-global/skills/dwy-dolphindb/references/sql-batch-compute.md +206 -0
- package/templates/claude-global/skills/dwy-eapi/SKILL.md +134 -402
- package/templates/claude-global/skills/dwy-ekit/SKILL.md +1 -0
- package/templates/claude-global/skills/dwy-eui/SKILL.md +1 -0
- package/templates/claude-global/skills/dwy-fullstack-scaffold/SKILL.md +14 -7
- package/templates/claude-global/skills/dwy-git-commit/SKILL.md +1 -0
- package/templates/claude-global/skills/dwy-mirror-source/SKILL.md +1 -0
- package/templates/claude-global/skills/dwy-mirror-source/scripts/apply_mirrors.py +0 -0
- package/templates/claude-global/skills/dwy-mirror-source/scripts/check_mirrors.py +0 -0
- package/templates/claude-global/skills/dwy-pentest/SKILL.md +1 -0
- package/templates/claude-global/skills/dwy-publish/SKILL.md +1 -0
- package/templates/claude-global/skills/dwy-tech-stack/SKILL.md +1 -0
- package/templates/project/fullstack-monorepo/CLAUDE.md.hbs +21 -9
- package/templates/project/fullstack-monorepo/backend/Dockerfile.prod +40 -0
- package/templates/project/fullstack-monorepo/backend/_env.example.hbs +26 -0
- package/templates/project/fullstack-monorepo/docker-compose.dev.yml.hbs +4 -4
- package/templates/project/fullstack-monorepo/docker-compose.prod.yml.hbs +116 -0
- package/templates/project/fullstack-monorepo/docs/.gitkeep +0 -0
- package/templates/project/fullstack-monorepo/frontend/_env.example.hbs +19 -0
- package/templates/project/fullstack-monorepo/{dev.sh.hbs → scripts/dev.sh.hbs} +2 -2
- package/templates/claude-global/skills/dwy-dolphindb/evals/evals.json +0 -67
- package/templates/claude-global/skills/dwy-dolphindb/references/dolphindb-rules.md +0 -441
- package/templates/claude-global/skills/dwy-eapi/references/health.md +0 -47
- package/templates/claude-global/skills/dwy-eapi/references/logger.md +0 -60
- package/templates/project/fullstack-monorepo/_env.example.hbs +0 -24
|
@@ -1,356 +1,143 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: dwy-eapi
|
|
3
|
-
description: "dwyeapi FastAPI
|
|
3
|
+
description: "dwyeapi FastAPI 后端基础设施使用指南。涉及以下任何主题,**必须**使用此 skill(即使用户没有明确说 'dwyeapi' / 'eapi'):FastAPI 项目搭建 / Pydantic Settings 配置 / 异步 SQLAlchemy / Redis 缓存 / JWT 认证 / bcrypt 密码哈希 / 异常体系(AppError / NotFoundError / BusinessError)/ 统一响应信封(ApiResponse / PageData)/ 分页 / 日志(loguru)/ 健康检查 / PII 脱敏 / 时区敏感的时间处理 / 异步任务(ARQ)/ 邮件验证码发送。本 skill 是 @dwydev/eapi 的唯一权威导航来源。"
|
|
4
|
+
category: 基础库
|
|
5
|
+
eapi_baseline_version: "0.9.0"
|
|
4
6
|
---
|
|
5
7
|
|
|
6
|
-
# dwyeapi
|
|
8
|
+
# dwyeapi 后端基础设施使用指南
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
Python 3.11+ FastAPI 基础设施包,全异步。单文件模块设计(每个能力一个 .py),便于直接读源码。
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
本 skill 索引基于 dwyeapi **0.9.0**。消费方版本可能不同 — 见 [版本兼容规则](#版本兼容规则)。
|
|
11
13
|
|
|
12
|
-
##
|
|
14
|
+
## 模块清单(13 个)
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
from dwyeapi.masking import mask_phone, mask_email, mask_id_card, mask_name
|
|
31
|
-
|
|
32
|
-
# 任务模块 (需安装 [tasks] extra)
|
|
33
|
-
from dwyeapi.tasks import setup_tasks, task_router, register, TaskContext, TaskStatus, create_worker_settings
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
## 查阅源码
|
|
37
|
-
|
|
38
|
-
每个模块在 `backend/src/dwyeapi/{module}.py`,单文件设计可直接阅读。
|
|
16
|
+
| 模块 | 用途 |
|
|
17
|
+
|------|------|
|
|
18
|
+
| `config` | Pydantic BaseSettings + 运行环境识别 (is_dev / is_prod / get_environment) |
|
|
19
|
+
| `exceptions` | AppError 异常体系 + FastAPI 全局 handler 注册 |
|
|
20
|
+
| `database` | 异步 SQLAlchemy engine/session 工厂 + Base + TimestampMixin |
|
|
21
|
+
| `dependencies` | FastAPI 依赖注入工厂(如 get_db) |
|
|
22
|
+
| `security` | JWT 令牌 + bcrypt 密码哈希(无状态) |
|
|
23
|
+
| `cache` | 异步 Redis 连接管理(懒单例) |
|
|
24
|
+
| `response` | 统一 API 响应信封 `ApiResponse[T]` + `PageData[T]` |
|
|
25
|
+
| `pagination` | 分页参数 PaginationParams + offset/limit 计算 |
|
|
26
|
+
| `dt` | 中国时区敏感的全局时间工具(替代 datetime.now) |
|
|
27
|
+
| `logger` | loguru 全局日志(按天 + 按大小轮转、stdlib 拦截) |
|
|
28
|
+
| `masking` | PII 数据脱敏(手机/邮箱/身份证/银行卡/姓名/地址/IP/车牌) |
|
|
29
|
+
| `health` | 健康检查路由工厂(只探活、不探依赖) |
|
|
30
|
+
| `tasks` | 异步任务系统(基于 ARQ,需 `pip install dwyeapi[tasks]`) |
|
|
31
|
+
| `providers.email` | 邮件验证码发送(可插拔,默认 Resend,可继承注入腾讯 SES / SMTP 等) |
|
|
39
32
|
|
|
40
33
|
---
|
|
41
34
|
|
|
42
|
-
##
|
|
43
|
-
|
|
44
|
-
```python
|
|
45
|
-
from dwyeapi.config import BaseSettings
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
子类化后使用,从 `.env` 或环境变量读取配置。
|
|
49
|
-
|
|
50
|
-
```python
|
|
51
|
-
class Settings(BaseSettings):
|
|
52
|
-
app_name: str = "My API"
|
|
53
|
-
# 继承的必填字段:database_url, redis_url, secret_key
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
| 字段 | 类型 | 默认值 | 说明 |
|
|
57
|
-
|------|------|--------|------|
|
|
58
|
-
| database_url | str | **必填** | 异步数据库 URL |
|
|
59
|
-
| redis_url | str | **必填** | Redis URL |
|
|
60
|
-
| secret_key | str | **必填** | JWT 签名密钥 |
|
|
61
|
-
| jwt_algorithm | str | `"HS256"` | JWT 算法 |
|
|
62
|
-
| access_token_expire_minutes | int | `30` | Token 过期分钟 |
|
|
63
|
-
| environment | `"dev" \| "prod"` | `"prod"` | 运行环境(见下) |
|
|
64
|
-
| allowed_origins | list[str] | `[]` | CORS 允许域名 |
|
|
65
|
-
| task_max_jobs | int | `5` | Worker 最大并发任务数 |
|
|
66
|
-
| task_job_timeout | int | `3600` | 单个任务超时秒数 |
|
|
67
|
-
| task_failure_ttl | int | `86400` | 失败任务 Redis 保留秒数 |
|
|
68
|
-
|
|
69
|
-
### 运行环境识别 (dev / prod)
|
|
35
|
+
## 查 API 标准动作(核心:每次写 dwyeapi 代码前都做)
|
|
70
36
|
|
|
71
|
-
|
|
37
|
+
dwyeapi 的源码 docstring 写得非常详尽(含设计意图、边界行为、Args/Returns/Raises、典型陷阱)。**本文档不再镜像签名表格** —— 因为表格信息密度低于源码 docstring,且会随版本漂移。需要某个 API 的精确签名/语义时,按下面顺序拿:
|
|
72
38
|
|
|
73
|
-
|
|
74
|
-
from dwyeapi import is_dev, is_prod, get_environment
|
|
39
|
+
### 第一步:定位 dwyeapi 真实版本和安装路径
|
|
75
40
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
openapi_url="/openapi.json" if is_dev() else None,
|
|
81
|
-
)
|
|
41
|
+
```bash
|
|
42
|
+
# 拿版本
|
|
43
|
+
pip show dwyeapi | grep -i version
|
|
44
|
+
# 或:python -c "import dwyeapi; print(dwyeapi.__version__)"
|
|
82
45
|
|
|
83
|
-
#
|
|
84
|
-
|
|
46
|
+
# 拿包安装目录
|
|
47
|
+
python -c "import dwyeapi; print(dwyeapi.__file__)"
|
|
48
|
+
# 输出形如 /path/to/.venv/lib/python3.12/site-packages/dwyeapi/__init__.py
|
|
49
|
+
# dirname 后的目录就是 dwyeapi 包根,模块都在里面
|
|
85
50
|
```
|
|
86
51
|
|
|
87
|
-
|
|
52
|
+
### 第二步:读模块源码(按存在性回退)
|
|
88
53
|
|
|
89
|
-
---
|
|
90
|
-
|
|
91
|
-
## database — 异步 SQLAlchemy
|
|
92
|
-
|
|
93
|
-
```python
|
|
94
|
-
from dwyeapi.database import Base, TimestampMixin, create_async_engine_factory, create_session_factory
|
|
95
54
|
```
|
|
55
|
+
a. 优先:<site-packages>/dwyeapi/{module}.py
|
|
56
|
+
—— 下游消费方默认场景。每个模块单文件,含模块级 docstring + 所有公开函数/类的 Args/Returns/Raises
|
|
96
57
|
|
|
97
|
-
|
|
58
|
+
b. 次选:<dwy-shared-root>/backend/src/dwyeapi/{module}.py
|
|
59
|
+
—— 仅当在 dwy-shared monorepo 内或并列 clone 时可用,内容相同
|
|
98
60
|
|
|
99
|
-
|
|
100
|
-
from sqlalchemy.orm import Mapped, mapped_column
|
|
101
|
-
from sqlalchemy import String
|
|
102
|
-
|
|
103
|
-
class User(Base, TimestampMixin):
|
|
104
|
-
__tablename__ = "users"
|
|
105
|
-
id: Mapped[int] = mapped_column(primary_key=True)
|
|
106
|
-
name: Mapped[str] = mapped_column(String(50))
|
|
107
|
-
# TimestampMixin 自动添加 created_at, updated_at
|
|
61
|
+
c. 都拿不到 → 退到本文档下方的 [模块清单](#模块清单13-个)(粗略,仅做导航)
|
|
108
62
|
```
|
|
109
63
|
|
|
110
|
-
###
|
|
64
|
+
### 第三步:tasks 模块查详细文档
|
|
111
65
|
|
|
112
|
-
|
|
113
|
-
engine = create_async_engine_factory(settings.database_url, echo=is_dev())
|
|
114
|
-
# 参数: database_url, pool_size=5, max_overflow=10, echo=False
|
|
115
|
-
# SQLite 自动跳过连接池参数
|
|
66
|
+
tasks 模块功能丰富(注册 / 提交 / 查询 / 取消 / 进度),单独维护一份完整集成指南:
|
|
116
67
|
|
|
117
|
-
session_factory = create_session_factory(engine)
|
|
118
|
-
# 返回 async_sessionmaker[AsyncSession],expire_on_commit=False
|
|
119
68
|
```
|
|
120
|
-
|
|
121
|
-
---
|
|
122
|
-
|
|
123
|
-
## dependencies — FastAPI 依赖注入
|
|
124
|
-
|
|
125
|
-
```python
|
|
126
|
-
from dwyeapi.dependencies import create_get_db
|
|
69
|
+
references/tasks-integration-guide.md
|
|
127
70
|
```
|
|
128
71
|
|
|
129
|
-
|
|
72
|
+
只在用到 tasks 时读,平时不必加载。
|
|
130
73
|
|
|
131
|
-
|
|
74
|
+
### 第四步:冲突时永远信源码
|
|
132
75
|
|
|
133
|
-
|
|
134
|
-
get_db = create_get_db(session_factory)
|
|
135
|
-
|
|
136
|
-
@router.get("/users/{user_id}")
|
|
137
|
-
async def get_user(user_id: int, db: AsyncSession = Depends(get_db)):
|
|
138
|
-
...
|
|
139
|
-
```
|
|
76
|
+
本 SKILL.md 是导航 + 心智模型,**不是 API 真相**。当文档描述与 site-packages 中的 `.py` 不一致:无条件以源码为准。
|
|
140
77
|
|
|
141
78
|
---
|
|
142
79
|
|
|
143
|
-
##
|
|
144
|
-
|
|
145
|
-
```python
|
|
146
|
-
from dwyeapi.security import hash_password, verify_password, create_token, decode_token
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
所有函数无状态,密钥通过参数传入。
|
|
80
|
+
## 版本兼容规则
|
|
150
81
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
| create_token | `(data: dict, secret: str, expires_minutes: int, algorithm="HS256") -> str` | 创建 JWT,自动添加 exp |
|
|
156
|
-
| decode_token | `(token: str, secret: str, algorithm="HS256") -> dict \| None` | 解码 JWT,失败返回 None |
|
|
157
|
-
|
|
158
|
-
```python
|
|
159
|
-
hashed = hash_password("my-password")
|
|
160
|
-
is_valid = verify_password("my-password", hashed)
|
|
161
|
-
|
|
162
|
-
token = create_token({"sub": str(user.id)}, settings.secret_key, settings.access_token_expire_minutes)
|
|
163
|
-
payload = decode_token(token, settings.secret_key) # {"sub": "1", "exp": ...} 或 None
|
|
164
|
-
```
|
|
82
|
+
- 本 skill `eapi_baseline_version: 0.9.0`,模块清单、踩坑提示都基于这个版本
|
|
83
|
+
- 消费方装的是不同版本(更新或更旧)时,**必须**按"查 API 标准动作"流程读 .py,不要默认本文档准确
|
|
84
|
+
- dwyeapi < 0.9.0 可能没有 dt / tasks / providers 模块:先 `ls <site-packages>/dwyeapi/` 确认存在再用
|
|
85
|
+
- 主版本号变更(0.x → 1.x)时,本文档的索引可能完全失效:先 `ls <site-packages>/dwyeapi/` 重建认知
|
|
165
86
|
|
|
166
87
|
---
|
|
167
88
|
|
|
168
|
-
##
|
|
169
|
-
|
|
170
|
-
```python
|
|
171
|
-
from dwyeapi.exceptions import (
|
|
172
|
-
AppError, NotFoundError, BusinessError, PermissionDeniedError, AuthenticationError,
|
|
173
|
-
register_exception_handlers,
|
|
174
|
-
)
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
### 异常层级
|
|
178
|
-
|
|
179
|
-
| 异常类 | HTTP 状态码 | code | message |
|
|
180
|
-
|--------|-----------|------|---------|
|
|
181
|
-
| AppError(message, code) | — | 自定义 | 自定义 |
|
|
182
|
-
| NotFoundError(resource) | 404 | `NOT_FOUND` | `{resource}不存在` |
|
|
183
|
-
| BusinessError(message, code) | 422 | 自定义 | 自定义 |
|
|
184
|
-
| PermissionDeniedError() | 403 | `PERMISSION_DENIED` | `权限不足` |
|
|
185
|
-
| AuthenticationError() | 401 | `AUTHENTICATION_FAILED` | `认证失败` |
|
|
89
|
+
## 重要约束与已知陷阱
|
|
186
90
|
|
|
187
|
-
|
|
91
|
+
源码读得出来但容易忽略的"踩坑经验",写后端代码时遵守:
|
|
188
92
|
|
|
189
|
-
|
|
190
|
-
app = FastAPI()
|
|
191
|
-
register_exception_handlers(app)
|
|
192
|
-
# 之后在 service 层直接 raise NotFoundError("用户") 即可返回 404
|
|
193
|
-
```
|
|
93
|
+
### 业务层架构(service / router 分层)
|
|
194
94
|
|
|
195
|
-
|
|
95
|
+
- **service 层**抛业务异常(`NotFoundError("用户")` / `BusinessError("余额不足", code="INSUFFICIENT_BALANCE")` 等),不感知 HTTP 概念
|
|
96
|
+
- **router 层**不写 try/except,由 `register_exception_handlers(app)` 统一转成 `ApiResponse` 信封 JSON
|
|
97
|
+
- **禁止**在 service 层抛 `HTTPException` —— 会绕过统一 handler,破坏响应结构一致性
|
|
196
98
|
|
|
197
|
-
###
|
|
99
|
+
### 时间处理(避免时区 bug)
|
|
198
100
|
|
|
199
|
-
-
|
|
200
|
-
-
|
|
201
|
-
-
|
|
101
|
+
- 业务时间统一中国时区,**禁止 `datetime.now()`**,统一走 `dwyeapi.dt`
|
|
102
|
+
- `dt.now()` 返回 naive datetime(Asia/Shanghai,用于数据库存储)
|
|
103
|
+
- `dt.utc_now()` 返回 aware UTC datetime(用于 JWT exp 等协议字段)
|
|
104
|
+
- 这两个不能混用:数据库列存 naive 中国时间、JWT exp 存 UTC aware
|
|
202
105
|
|
|
203
|
-
|
|
106
|
+
### 配置
|
|
204
107
|
|
|
205
|
-
|
|
108
|
+
- **没有 `debug` 字段**,所有调试开关统一用 `is_dev()`:避免两个维度(debug + environment)冲突
|
|
109
|
+
- `environment` 默认 `"prod"`(误配置时保守):生产环境必须显式关闭 docs/redoc/openapi 三个端点(`docs_url="/docs" if is_dev() else None`),否则路由元信息会泄露给攻击者
|
|
110
|
+
- `BaseSettings` 必填三个字段:`database_url` / `redis_url` / `secret_key`(继承时不能给默认值)
|
|
206
111
|
|
|
207
|
-
|
|
208
|
-
from dwyeapi.response import success, fail, paginated
|
|
209
|
-
```
|
|
112
|
+
### Redis 连接
|
|
210
113
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
| fail | `(code=400, message="fail") -> dict` | `{"code": ..., "message": ..., "data": None, "timestamp": ...}` |
|
|
215
|
-
| paginated | `(items, total, page, page_size) -> dict` | success 包装 `{"items": [...], "total": N, "page": N, "page_size": N}` |
|
|
114
|
+
- `cache.configure(redis_url)` 启动时调用一次(lifespan);`cache.get_redis()` 首次调用懒建立连接
|
|
115
|
+
- `close_redis()` 可重复调用(安全幂等),lifespan 关闭阶段调用
|
|
116
|
+
- 未 configure 直接 `get_redis()` 会抛 `RuntimeError`
|
|
216
117
|
|
|
217
|
-
|
|
218
|
-
@router.get("/users/{user_id}")
|
|
219
|
-
async def get_user(user_id: int, service=Depends(get_user_service)):
|
|
220
|
-
user = await service.get_by_id(user_id)
|
|
221
|
-
return success(data=UserResponse.model_validate(user))
|
|
222
|
-
|
|
223
|
-
@router.get("/users")
|
|
224
|
-
async def list_users(params: PaginationParams = Depends(), service=Depends(get_user_service)):
|
|
225
|
-
items, total = await service.list(params)
|
|
226
|
-
return paginated(items, total, params.page, params.page_size)
|
|
227
|
-
```
|
|
118
|
+
### tasks 模块
|
|
228
119
|
|
|
229
|
-
|
|
120
|
+
- 需要 `pip install dwyeapi[tasks]` 单独装 extra(避免不用任务的项目背 ARQ 依赖)
|
|
121
|
+
- 任务函数签名固定 `async def task(ctx: TaskContext, params: dict)`,第一个参数 ctx 由框架注入
|
|
122
|
+
- 取消机制是协作式:任务函数内必须主动 `await ctx.is_cancelled()` 检查,否则取消信号无效
|
|
123
|
+
- 完整细节读 `references/tasks-integration-guide.md`
|
|
230
124
|
|
|
231
|
-
|
|
125
|
+
### 邮件 Provider
|
|
232
126
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
```
|
|
127
|
+
- 内置仅 `resend`。业务用其他通道(腾讯 SES / SMTP / 自建):继承 `EmailProviderBase` 实现 `_send` → `register_email_provider("name", factory)` → `.env` 设 `EMAIL__PROVIDER=name`
|
|
128
|
+
- 验证码生成 + Redis 存储 + 品牌化模板 由基类全部复用,子类只管"发出去"
|
|
236
129
|
|
|
237
|
-
###
|
|
130
|
+
### 日志
|
|
238
131
|
|
|
239
|
-
|
|
132
|
+
- `logger.configure()` 启动时调用一次;`logger.close()` 在 lifespan 结束时调用
|
|
133
|
+
- `intercept_stdlib=True`(默认)会把 stdlib logging(uvicorn / SQLAlchemy 等)也接管到统一 sink
|
|
134
|
+
- 多进程部署时必须 `enqueue=True`(默认)——避免日志文件竞争写
|
|
240
135
|
|
|
241
|
-
|
|
242
|
-
|------|------|------|------|
|
|
243
|
-
| page | int | 1 | >= 1 |
|
|
244
|
-
| page_size | int | 20 | 1 ~ 100 |
|
|
136
|
+
### 健康检查
|
|
245
137
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
offset_limit = paginate(params.page, params.page_size)
|
|
250
|
-
# OffsetLimit(offset=0, limit=20) — 可直接用于 SQLAlchemy .offset().limit()
|
|
251
|
-
stmt = select(User).offset(offset_limit.offset).limit(offset_limit.limit)
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
---
|
|
255
|
-
|
|
256
|
-
## cache — 异步 Redis
|
|
257
|
-
|
|
258
|
-
```python
|
|
259
|
-
from dwyeapi import cache
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
| 函数 | 签名 | 说明 |
|
|
263
|
-
|------|------|------|
|
|
264
|
-
| cache.configure | `(redis_url: str) -> None` | 设置 Redis URL(启动时调用一次) |
|
|
265
|
-
| cache.get_redis | `async () -> Redis` | 获取/创建共享连接(未 configure 时抛 RuntimeError) |
|
|
266
|
-
| cache.close_redis | `async () -> None` | 关闭连接(安全幂等) |
|
|
267
|
-
|
|
268
|
-
```python
|
|
269
|
-
# lifespan 中初始化
|
|
270
|
-
@asynccontextmanager
|
|
271
|
-
async def lifespan(app: FastAPI):
|
|
272
|
-
cache.configure(settings.redis_url)
|
|
273
|
-
yield
|
|
274
|
-
await cache.close_redis()
|
|
275
|
-
|
|
276
|
-
# 业务中使用
|
|
277
|
-
redis = await cache.get_redis()
|
|
278
|
-
await redis.set("key", "value", ex=3600)
|
|
279
|
-
val = await redis.get("key")
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
---
|
|
283
|
-
|
|
284
|
-
## dt — 全局时间工具
|
|
285
|
-
|
|
286
|
-
```python
|
|
287
|
-
from dwyeapi import dt
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
业务时间统一 Asia/Shanghai,**禁止在业务代码中直接使用 `datetime.now()`**,统一通过 `dt` 模块访问。
|
|
291
|
-
|
|
292
|
-
| 函数 | 签名 | 说明 |
|
|
293
|
-
|------|------|------|
|
|
294
|
-
| dt.now | `() -> datetime` | 当前中国时间(naive,用于数据库存储) |
|
|
295
|
-
| dt.now_str | `(fmt="%Y-%m-%d %H:%M:%S") -> str` | 格式化的当前中国时间字符串 |
|
|
296
|
-
| dt.today | `() -> date` | 当前中国日期 |
|
|
297
|
-
| dt.timestamp | `() -> float` | 当前 Unix 时间戳(秒) |
|
|
298
|
-
| dt.utc_now | `() -> datetime` | 当前 UTC 时间(aware,用于 JWT exp 等协议字段) |
|
|
299
|
-
|
|
300
|
-
```python
|
|
301
|
-
dt.now() # datetime(2026, 4, 8, 14, 30, 0) — naive, Asia/Shanghai
|
|
302
|
-
dt.now_str() # "2026-04-08 14:30:00"
|
|
303
|
-
dt.today() # date(2026, 4, 8)
|
|
304
|
-
dt.timestamp() # 1775625000.0
|
|
305
|
-
dt.utc_now() # datetime(2026, 4, 8, 6, 30, 0, tzinfo=UTC) — aware
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
常量:`dt.TZ`(Asia/Shanghai)、`dt.UTC`。
|
|
309
|
-
|
|
310
|
-
---
|
|
311
|
-
|
|
312
|
-
## masking — PII 数据脱敏
|
|
313
|
-
|
|
314
|
-
```python
|
|
315
|
-
from dwyeapi.masking import (
|
|
316
|
-
mask_phone, # 138****5678
|
|
317
|
-
mask_email, # z***@gmail.com
|
|
318
|
-
mask_id_card, # 420***********1234
|
|
319
|
-
mask_bank_card, # 6222********1234
|
|
320
|
-
mask_name, # 张*明
|
|
321
|
-
mask_address, # 浙江省杭州市西湖区****
|
|
322
|
-
mask_ip, # 192.168.1.*
|
|
323
|
-
mask_license_plate, # 浙A***8
|
|
324
|
-
mask_text, # 通用脱敏(自定义首尾保留位数)
|
|
325
|
-
)
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
纯函数模块,无外部依赖。所有函数对空字符串、格式不匹配的输入原样返回,不抛异常。
|
|
329
|
-
|
|
330
|
-
### 在 Response Schema 中使用
|
|
331
|
-
|
|
332
|
-
```python
|
|
333
|
-
class UserResponse(BaseModel):
|
|
334
|
-
phone: str
|
|
335
|
-
email: str
|
|
336
|
-
|
|
337
|
-
@field_validator("phone", mode="before")
|
|
338
|
-
@classmethod
|
|
339
|
-
def _mask_phone(cls, v: str) -> str:
|
|
340
|
-
return mask_phone(v) if v else v
|
|
341
|
-
|
|
342
|
-
@field_validator("email", mode="before")
|
|
343
|
-
@classmethod
|
|
344
|
-
def _mask_email(cls, v: str) -> str:
|
|
345
|
-
return mask_email(v) if v else v
|
|
346
|
-
```
|
|
347
|
-
|
|
348
|
-
### mask_text — 通用脱敏
|
|
349
|
-
|
|
350
|
-
```python
|
|
351
|
-
mask_text("hello", start=1, end=1, mask_char="*") # h***o
|
|
352
|
-
mask_text("1234567890", start=2, end=3) # 12*****890
|
|
353
|
-
```
|
|
138
|
+
- `create_health_router()` **只**探活、**不**触达 PostgreSQL / Redis 等依赖
|
|
139
|
+
- 理由:健康端点常对公网开放,内置 dependency check 会被攻击者用来 DoS 放大数据库/缓存
|
|
140
|
+
- 需要 readiness 的业务自行实现并限制内网访问
|
|
354
141
|
|
|
355
142
|
---
|
|
356
143
|
|
|
@@ -362,134 +149,79 @@ from dwyeapi.config import BaseSettings
|
|
|
362
149
|
|
|
363
150
|
class Settings(BaseSettings):
|
|
364
151
|
app_name: str = "My API"
|
|
152
|
+
service_name: str = "my-api"
|
|
365
153
|
|
|
366
|
-
settings = Settings()
|
|
154
|
+
settings = Settings() # 从 .env / 环境变量读 database_url、redis_url、secret_key
|
|
367
155
|
|
|
368
156
|
# database.py
|
|
369
|
-
from dwyeapi.database import
|
|
157
|
+
from dwyeapi.database import create_async_engine_factory, create_session_factory
|
|
370
158
|
from dwyeapi.dependencies import create_get_db
|
|
159
|
+
from dwyeapi import is_dev
|
|
371
160
|
|
|
372
|
-
engine = create_async_engine_factory(settings.database_url)
|
|
161
|
+
engine = create_async_engine_factory(settings.database_url, echo=is_dev())
|
|
373
162
|
session_factory = create_session_factory(engine)
|
|
374
163
|
get_db = create_get_db(session_factory)
|
|
375
164
|
|
|
376
165
|
# main.py
|
|
166
|
+
from contextlib import asynccontextmanager
|
|
167
|
+
from fastapi import FastAPI
|
|
168
|
+
from dwyeapi import cache, logger, is_dev
|
|
377
169
|
from dwyeapi.exceptions import register_exception_handlers
|
|
378
|
-
from dwyeapi import
|
|
170
|
+
from dwyeapi.health import create_health_router
|
|
379
171
|
|
|
380
|
-
|
|
172
|
+
@asynccontextmanager
|
|
173
|
+
async def lifespan(app: FastAPI):
|
|
174
|
+
logger.configure(level="INFO", log_dir="./logs", filename="app")
|
|
175
|
+
cache.configure(settings.redis_url)
|
|
176
|
+
yield
|
|
177
|
+
await cache.close_redis()
|
|
178
|
+
logger.close()
|
|
179
|
+
|
|
180
|
+
app = FastAPI(
|
|
181
|
+
lifespan=lifespan,
|
|
182
|
+
docs_url="/docs" if is_dev() else None,
|
|
183
|
+
redoc_url="/redoc" if is_dev() else None,
|
|
184
|
+
openapi_url="/openapi.json" if is_dev() else None,
|
|
185
|
+
)
|
|
381
186
|
register_exception_handlers(app)
|
|
187
|
+
app.include_router(create_health_router(service_name=settings.service_name, version="1.0.0"))
|
|
382
188
|
```
|
|
383
189
|
|
|
384
190
|
---
|
|
385
191
|
|
|
386
|
-
##
|
|
387
|
-
|
|
388
|
-
```python
|
|
389
|
-
from dwyeapi.tasks import setup_tasks, task_router, register, TaskContext
|
|
390
|
-
```
|
|
192
|
+
## 响应契约速查(前后端对齐用)
|
|
391
193
|
|
|
392
|
-
|
|
194
|
+
所有接口返回统一信封:
|
|
393
195
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
await ctx.update_progress(50, "处理中...")
|
|
402
|
-
if await ctx.is_cancelled():
|
|
403
|
-
return
|
|
404
|
-
return {"result": "ok"} # 框架自动设为 SUCCESS
|
|
405
|
-
|
|
406
|
-
# 2. FastAPI 接入
|
|
407
|
-
await setup_tasks(app, settings, session_factory)
|
|
408
|
-
app.include_router(task_router)
|
|
409
|
-
|
|
410
|
-
# 3. Worker: arq app.worker.WorkerSettings
|
|
196
|
+
```json
|
|
197
|
+
{
|
|
198
|
+
"code": "SUCCESS",
|
|
199
|
+
"message": "success",
|
|
200
|
+
"data": { ... },
|
|
201
|
+
"timestamp": 1775625000
|
|
202
|
+
}
|
|
411
203
|
```
|
|
412
204
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|--------|------|
|
|
417
|
-
| `setup_tasks(app, settings, session_factory)` | lifespan 中调用, 初始化 |
|
|
418
|
-
| `task_router` | 开箱即用 APIRouter (提交/查询/列表/取消) |
|
|
419
|
-
| `register(task_type)` | 装饰器, 注册异步任务 |
|
|
420
|
-
| `TaskContext` | 任务函数上下文 (db/log/progress/cancel) |
|
|
421
|
-
| `TaskStatus` | 枚举: PENDING/RUNNING/SUCCESS/FAILED/CANCELED |
|
|
422
|
-
| `create_worker_settings(settings, session_factory)` | 生成 ARQ Worker 配置 |
|
|
205
|
+
- 成功:`code = "SUCCESS"`,`data` 是业务载荷(单体或 `PageData[T]`)
|
|
206
|
+
- 失败:`code` 是业务错误码(如 `NOT_FOUND` / `BUSINESS_ERROR` / `VALIDATION_ERROR` 等),HTTP 状态保持语义(404 就是 404)
|
|
207
|
+
- 校验错误 `VALIDATION_ERROR`:`data.errors: [{field, message}]`,field 含 `body.` / `query.` / `path.` 前缀
|
|
423
208
|
|
|
424
|
-
|
|
209
|
+
构造响应:用 `ApiResponse.ok(data)` 或 `ApiResponse.page(items, total, page, page_size)`,不直接实例化 `ApiResponse`。
|
|
425
210
|
|
|
426
|
-
|
|
427
|
-
|------|------|------|
|
|
428
|
-
| POST | `/tasks` | 提交任务 |
|
|
429
|
-
| GET | `/tasks/{task_id}` | 查询状态 |
|
|
430
|
-
| GET | `/tasks` | 列表 (分页 + 筛选) |
|
|
431
|
-
| POST | `/tasks/{task_id}/cancel` | 取消任务 |
|
|
432
|
-
|
|
433
|
-
> **完整文档:** [tasks-integration-guide.md](references/tasks-integration-guide.md) — 包含 TaskContext API、数据模型、Worker 配置、取消机制等详细说明。
|
|
211
|
+
错误响应**不要业务代码构造** —— 直接 `raise NotFoundError("用户")`,handler 自动转。
|
|
434
212
|
|
|
435
213
|
---
|
|
436
214
|
|
|
437
|
-
##
|
|
438
|
-
|
|
439
|
-
### logger — 基于 loguru 的全局日志
|
|
215
|
+
## 防腐层硬性约束(写代码前必读)
|
|
440
216
|
|
|
441
|
-
|
|
442
|
-
from dwyeapi import logger
|
|
443
|
-
|
|
444
|
-
logger.configure(level="INFO", log_dir="./logs", filename="app")
|
|
445
|
-
log = logger.get_logger("users")
|
|
446
|
-
log.info("用户 %s 登录", user_id)
|
|
447
|
-
```
|
|
217
|
+
dwyeapi 把基础设施细节封装在自有 API 后面。业务代码**禁止**直接调下面这些底层 API,违反会导致升级困难、行为不一致:
|
|
448
218
|
|
|
449
|
-
|
|
|
219
|
+
| 禁止 | 改用 |
|
|
450
220
|
|------|------|
|
|
451
|
-
| `
|
|
452
|
-
| `
|
|
453
|
-
| `
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
```python
|
|
460
|
-
from dwyeapi.health import create_health_router
|
|
461
|
-
|
|
462
|
-
router = create_health_router(service_name="my-api", version="1.0.0")
|
|
463
|
-
app.include_router(router)
|
|
464
|
-
# GET /health → {"service": "my-api", "version": "1.0.0", "status": "alive"}
|
|
465
|
-
```
|
|
466
|
-
|
|
467
|
-
只探活、不探依赖(防 DoS 放大)。业务如需 readiness 自行实现并限制内网访问。
|
|
468
|
-
|
|
469
|
-
### providers.email — 邮件验证码发送(可插拔)
|
|
470
|
-
|
|
471
|
-
内置仅 `resend`,业务可继承 `EmailProviderBase` 注入自定义通道(腾讯云 SES、AWS SES、SMTP 等)。验证码 + Redis + 品牌化模板由基类复用。
|
|
472
|
-
|
|
473
|
-
```python
|
|
474
|
-
from dwyeapi.providers.email import EmailSettings, make_email_provider, register_email_provider
|
|
475
|
-
```
|
|
476
|
-
|
|
477
|
-
| 导出项 | 说明 |
|
|
478
|
-
|--------|------|
|
|
479
|
-
| `EmailSettings` | 配置模型(code_ttl, brand_name, resend) |
|
|
480
|
-
| `make_email_provider(settings)` | 工厂函数,按 `.env EMAIL__PROVIDER` 创建实例 |
|
|
481
|
-
| `register_email_provider(name, factory)` | 注册自定义 provider |
|
|
482
|
-
| `EmailProviderBase` | 抽象基类,继承后实现 `_send` 即可 |
|
|
483
|
-
|
|
484
|
-
---
|
|
485
|
-
|
|
486
|
-
## 查阅源码
|
|
487
|
-
|
|
488
|
-
所有模块为单文件设计,直接阅读:
|
|
489
|
-
|
|
490
|
-
```
|
|
491
|
-
backend/src/dwyeapi/{module}.py
|
|
492
|
-
backend/src/dwyeapi/providers/email/
|
|
493
|
-
```
|
|
494
|
-
|
|
495
|
-
模块清单:config、database、dependencies、security、exceptions、response、pagination、cache、dt、masking、logger、health、tasks、providers/email。
|
|
221
|
+
| `datetime.now()` / `datetime.utcnow()` | `dt.now()` / `dt.utc_now()` |
|
|
222
|
+
| `import logging; logging.getLogger()` | `from dwyeapi import logger; logger.get_logger()` |
|
|
223
|
+
| 手写 `from redis.asyncio import Redis` 全局变量 | `cache.configure()` + `cache.get_redis()` |
|
|
224
|
+
| 手写 JWT encode/decode(含 `import jwt`) | `security.create_token()` / `security.decode_token()` |
|
|
225
|
+
| 直接 `raise HTTPException(404, ...)` | `raise NotFoundError("资源名")` |
|
|
226
|
+
| 自己写 `{"code": ..., "data": ...}` dict 当响应 | `ApiResponse.ok(data)` / `ApiResponse.page(...)` |
|
|
227
|
+
| 自己写 `?page=&page_size=` Query 参数 | `params: PaginationParams = Depends()` |
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: dwy-ekit
|
|
3
3
|
description: "@dwydev/ekit 工具库使用指南。涉及以下任何主题,**必须**使用此 skill(即使用户没有说 'ekit'):HTTP 请求 / axios 封装 / token 刷新 / dwyeapi 响应契约对接 / localStorage / Cookie / useStorage / useCookie / 日期格式化 / 时区处理 / dayjs / 表单校验 / zod schema / vee-validate / PII 脱敏 / 剪贴板 / useClipboard / 文件下载 / Blob / 查询字符串 / qs / Vue composables(debounce / throttle / click-outside / event-listener / window-size / media-query / intersection-observer / resize-observer / useFormPersist)。本 skill 是 @dwydev/ekit 的唯一权威导航来源,禁止绕过 ekit 直接手写底层 API。"
|
|
4
|
+
category: 基础库
|
|
4
5
|
ekit_baseline_version: "0.8.0"
|
|
5
6
|
---
|
|
6
7
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: dwy-eui
|
|
3
3
|
description: "@dwydev/eui Vue 3 组件库使用指南。当用户在使用 Vue 3 构建页面、选择 UI 组件、查询组件 API、写表单/表格/弹层/导航、配置主题或国际化、使用中后台或落地页设计规范时,**必须**使用此 skill。即使用户没有明确说 'eui',只要涉及 Vue 3 组件选型、页面布局、表单设计、数据展示、弹层交互、主题切换,也应触发。"
|
|
4
|
+
category: 基础库
|
|
4
5
|
eui_baseline_version: "2.1.0"
|
|
5
6
|
---
|
|
6
7
|
|