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.
- package/bin/index.js +1 -1
- package/package.json +2 -2
- package/src/create.js +61 -37
- package/src/sync.js +213 -53
- 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} +314 -357
- 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} +116 -79
- 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} +361 -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/settings.json +20 -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-publish/SKILL.md +135 -0
- 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/rules/agent-team-execution.md +0 -138
- package/templates/claude-global/rules/agent-team-patterns.md +0 -157
- package/templates/claude-global/rules/git-security.md +0 -144
- package/templates/claude-global/skills/dwy-auto-sync/SKILL.md +0 -52
- 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,387 @@
|
|
|
1
|
+
# dwyeapi Tasks 集成指南
|
|
2
|
+
|
|
3
|
+
基于 ARQ 的全异步耗时任务处理系统。开箱即用,3 步接入。
|
|
4
|
+
|
|
5
|
+
## 1. 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install dwyeapi[tasks]
|
|
9
|
+
# 或
|
|
10
|
+
uv add "dwyeapi[tasks]"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
自动安装 `arq` 异步任务队列依赖。需要运行中的 Redis 实例。
|
|
14
|
+
|
|
15
|
+
## 2. 快速接入
|
|
16
|
+
|
|
17
|
+
### 第一步:写任务函数
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
# app/tasks.py
|
|
21
|
+
from dwyeapi.tasks import register, TaskContext
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@register("process_data")
|
|
25
|
+
async def process_data(ctx: TaskContext, params: dict):
|
|
26
|
+
"""数据处理任务。"""
|
|
27
|
+
file_path = params["file"]
|
|
28
|
+
await ctx.log(f"开始处理文件: {file_path}")
|
|
29
|
+
|
|
30
|
+
async with ctx.db() as session:
|
|
31
|
+
# 查询数据库
|
|
32
|
+
result = await session.execute(select(RawData).where(...))
|
|
33
|
+
rows = result.scalars().all()
|
|
34
|
+
|
|
35
|
+
for i, row in enumerate(rows):
|
|
36
|
+
# 业务处理...
|
|
37
|
+
await ctx.update_progress(int((i + 1) / len(rows) * 100))
|
|
38
|
+
|
|
39
|
+
if await ctx.is_cancelled():
|
|
40
|
+
await ctx.log("用户取消, 已停止")
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
return {"processed": len(rows)}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@register("export_file")
|
|
47
|
+
async def export_file(ctx: TaskContext, params: dict):
|
|
48
|
+
"""文件导出任务。"""
|
|
49
|
+
await ctx.log("开始导出")
|
|
50
|
+
# ... 业务逻辑 ...
|
|
51
|
+
return {"file_url": "/files/report.csv"}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 第二步:FastAPI 接入
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
# app/main.py
|
|
58
|
+
from contextlib import asynccontextmanager
|
|
59
|
+
|
|
60
|
+
from fastapi import FastAPI
|
|
61
|
+
|
|
62
|
+
from dwyeapi.database import create_async_engine_factory, create_session_factory
|
|
63
|
+
from dwyeapi.exceptions import register_exception_handlers
|
|
64
|
+
from dwyeapi.tasks import setup_tasks, task_router
|
|
65
|
+
|
|
66
|
+
from app.config import settings
|
|
67
|
+
|
|
68
|
+
import app.tasks # noqa: 触发 @register 装饰器
|
|
69
|
+
|
|
70
|
+
engine = create_async_engine_factory(settings.database_url)
|
|
71
|
+
session_factory = create_session_factory(engine)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@asynccontextmanager
|
|
75
|
+
async def lifespan(app: FastAPI):
|
|
76
|
+
await setup_tasks(app, settings, session_factory)
|
|
77
|
+
yield
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
app = FastAPI(lifespan=lifespan)
|
|
81
|
+
register_exception_handlers(app)
|
|
82
|
+
app.include_router(task_router) # 挂载 /tasks API
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 第三步:启动 Worker
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
# app/worker.py
|
|
89
|
+
from dwyeapi.database import create_async_engine_factory, create_session_factory
|
|
90
|
+
from dwyeapi.tasks import create_worker_settings
|
|
91
|
+
|
|
92
|
+
from app.config import settings
|
|
93
|
+
|
|
94
|
+
import app.tasks # noqa: 触发 @register 装饰器
|
|
95
|
+
|
|
96
|
+
engine = create_async_engine_factory(settings.database_url)
|
|
97
|
+
session_factory = create_session_factory(engine)
|
|
98
|
+
WorkerSettings = create_worker_settings(settings, session_factory=session_factory)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# 终端 1: FastAPI
|
|
103
|
+
uvicorn app.main:app --reload
|
|
104
|
+
|
|
105
|
+
# 终端 2: Worker
|
|
106
|
+
arq app.worker.WorkerSettings
|
|
107
|
+
|
|
108
|
+
# 终端 3 (可选): Redis
|
|
109
|
+
redis-server
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## 3. .env 配置
|
|
113
|
+
|
|
114
|
+
```env
|
|
115
|
+
DATABASE_URL=postgresql+asyncpg://user:pass@localhost/mydb
|
|
116
|
+
REDIS_URL=redis://localhost:6379/0
|
|
117
|
+
SECRET_KEY=your-secret-key
|
|
118
|
+
|
|
119
|
+
# 任务模块配置 (均有默认值, 可不配)
|
|
120
|
+
TASK_MAX_JOBS=5 # Worker 最大并发任务数 (默认 5)
|
|
121
|
+
TASK_JOB_TIMEOUT=3600 # 单个任务超时秒数 (默认 3600)
|
|
122
|
+
TASK_FAILURE_TTL=86400 # 失败任务在 Redis 中保留秒数 (默认 86400)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## 4. API 接口
|
|
126
|
+
|
|
127
|
+
挂载 `task_router` 后自动获得以下接口:
|
|
128
|
+
|
|
129
|
+
| 方法 | 路径 | 说明 |
|
|
130
|
+
|------|------|------|
|
|
131
|
+
| POST | `/tasks` | 提交任务 |
|
|
132
|
+
| GET | `/tasks/{task_id}` | 查询单个任务状态 |
|
|
133
|
+
| GET | `/tasks` | 任务列表 (分页 + 筛选) |
|
|
134
|
+
| POST | `/tasks/{task_id}/cancel` | 取消任务 |
|
|
135
|
+
|
|
136
|
+
### 提交任务
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
curl -X POST http://localhost:8000/tasks \
|
|
140
|
+
-H "Content-Type: application/json" \
|
|
141
|
+
-d '{"task_type": "process_data", "params": {"file": "data.csv"}}'
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
```json
|
|
145
|
+
{
|
|
146
|
+
"code": 200,
|
|
147
|
+
"message": "success",
|
|
148
|
+
"data": {
|
|
149
|
+
"id": "task_a1b2c3d4...",
|
|
150
|
+
"task_type": "process_data",
|
|
151
|
+
"status": "pending",
|
|
152
|
+
"progress": 0,
|
|
153
|
+
"logs": "[2026-04-05 10:00:00] 任务已创建, 等待执行\n",
|
|
154
|
+
"result": null,
|
|
155
|
+
"params": {"file": "data.csv"},
|
|
156
|
+
"created_at": "2026-04-05T10:00:00",
|
|
157
|
+
"updated_at": "2026-04-05T10:00:00"
|
|
158
|
+
},
|
|
159
|
+
"timestamp": 1775000000
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### 查询状态 (轮询)
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
curl http://localhost:8000/tasks/task_a1b2c3d4...
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`status` 会从 `pending` → `running` → `success`/`failed`/`canceled` 变化,`progress` 从 0 递增到 100。
|
|
170
|
+
|
|
171
|
+
### 任务列表
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
# 分页 + 状态筛选
|
|
175
|
+
curl "http://localhost:8000/tasks?page=1&page_size=20&status=running"
|
|
176
|
+
|
|
177
|
+
# 按类型筛选
|
|
178
|
+
curl "http://localhost:8000/tasks?task_type=process_data"
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### 取消任务
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
curl -X POST http://localhost:8000/tasks/task_a1b2c3d4.../cancel
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
仅 `pending` 和 `running` 状态的任务可取消。已结束的任务返回 400。
|
|
188
|
+
|
|
189
|
+
### 自定义路由前缀
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
app.include_router(task_router, prefix="/api/jobs") # /api/jobs/*
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## 5. TaskContext API
|
|
196
|
+
|
|
197
|
+
任务函数收到的 `ctx` 对象,提供以下方法:
|
|
198
|
+
|
|
199
|
+
| 方法 | 签名 | 说明 |
|
|
200
|
+
|------|------|------|
|
|
201
|
+
| ctx.log | `async (message: str) -> None` | 追加带时间戳的日志 |
|
|
202
|
+
| ctx.update_progress | `async (value: int, message: str = "") -> None` | 更新进度 0-100, 可选附日志 |
|
|
203
|
+
| ctx.is_cancelled | `async () -> bool` | 检查是否被取消 (读 Redis 标记) |
|
|
204
|
+
| ctx.db | `() -> AsyncContextManager[AsyncSession]` | 获取数据库 session |
|
|
205
|
+
|
|
206
|
+
### 属性
|
|
207
|
+
|
|
208
|
+
| 属性 | 类型 | 说明 |
|
|
209
|
+
|------|------|------|
|
|
210
|
+
| ctx.task_id | str | 当前任务唯一 ID |
|
|
211
|
+
| ctx.task_type | str | 任务类型 |
|
|
212
|
+
| ctx.params | dict | 提交时的入参 |
|
|
213
|
+
|
|
214
|
+
### 生命周期 (框架自动管理)
|
|
215
|
+
|
|
216
|
+
```
|
|
217
|
+
任务入队
|
|
218
|
+
→ 状态设为 RUNNING + 记日志 "任务开始执行"
|
|
219
|
+
→ 调用业务函数 func(ctx, params)
|
|
220
|
+
→ 正常返回 dict → 状态设为 SUCCESS, result = 返回值, progress = 100
|
|
221
|
+
→ 抛异常 → 状态设为 FAILED, result = {"error": "..."}
|
|
222
|
+
→ 取消退出 → 状态设为 CANCELED
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
业务代码**不需要**手动管理 RUNNING/SUCCESS/FAILED 状态转换。
|
|
226
|
+
|
|
227
|
+
## 6. 任务注册
|
|
228
|
+
|
|
229
|
+
### @register 装饰器
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
from dwyeapi.tasks import register, TaskContext
|
|
233
|
+
|
|
234
|
+
@register("unique_task_type")
|
|
235
|
+
async def my_task(ctx: TaskContext, params: dict):
|
|
236
|
+
...
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
- `task_type` 全局唯一,重复注册抛 `ValueError`
|
|
240
|
+
- 必须是 `async def`
|
|
241
|
+
- 签名固定为 `(ctx: TaskContext, params: dict)`
|
|
242
|
+
- 返回 `dict` 或 `None`
|
|
243
|
+
|
|
244
|
+
### 确保装饰器被执行
|
|
245
|
+
|
|
246
|
+
任务模块必须被导入才能触发 `@register`:
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
# main.py 和 worker.py 中都要加
|
|
250
|
+
import app.tasks # noqa
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## 7. 任务取消机制
|
|
254
|
+
|
|
255
|
+
协作式取消 — 框架设置 Redis 标记,任务函数自行检查并退出。
|
|
256
|
+
|
|
257
|
+
```python
|
|
258
|
+
@register("batch_import")
|
|
259
|
+
async def batch_import(ctx: TaskContext, params: dict):
|
|
260
|
+
for batch in batches:
|
|
261
|
+
# 每批处理前检查
|
|
262
|
+
if await ctx.is_cancelled():
|
|
263
|
+
await ctx.log("用户取消, 已导入到第 N 批")
|
|
264
|
+
return # 框架自动设为 CANCELED
|
|
265
|
+
|
|
266
|
+
await process_batch(batch)
|
|
267
|
+
await ctx.update_progress(...)
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
**注意:** 取消不会回滚已执行的操作。已插入的数据不会被撤销。如需回滚,在检测到取消后执行自定义清理逻辑。
|
|
271
|
+
|
|
272
|
+
## 8. 任务状态
|
|
273
|
+
|
|
274
|
+
| 状态 | 值 | 说明 |
|
|
275
|
+
|------|------|------|
|
|
276
|
+
| PENDING | `"pending"` | 已创建, 等待 Worker 执行 |
|
|
277
|
+
| RUNNING | `"running"` | 正在执行中 |
|
|
278
|
+
| SUCCESS | `"success"` | 执行成功 |
|
|
279
|
+
| FAILED | `"failed"` | 执行失败 (异常) |
|
|
280
|
+
| CANCELED | `"canceled"` | 已取消 |
|
|
281
|
+
|
|
282
|
+
终态:SUCCESS / FAILED / CANCELED,不可再变更。
|
|
283
|
+
|
|
284
|
+
## 9. Task 数据模型
|
|
285
|
+
|
|
286
|
+
Task 表自动随业务数据库创建 (复用 eapi 的 `Base`)。
|
|
287
|
+
|
|
288
|
+
| 字段 | 类型 | 说明 |
|
|
289
|
+
|------|------|------|
|
|
290
|
+
| id | String(64) | 主键, 自动生成 `task_{uuid}` |
|
|
291
|
+
| task_type | String(50) | 任务类型, 有索引 |
|
|
292
|
+
| status | Enum(TaskStatus) | 任务状态 |
|
|
293
|
+
| params | JSON | 提交时的入参 |
|
|
294
|
+
| result | JSON, nullable | 执行结果 |
|
|
295
|
+
| progress | Integer | 进度 0-100 |
|
|
296
|
+
| logs | Text | 带时间戳的执行日志 |
|
|
297
|
+
| created_at | DateTime | 创建时间 (TimestampMixin) |
|
|
298
|
+
| updated_at | DateTime | 更新时间 (TimestampMixin) |
|
|
299
|
+
|
|
300
|
+
## 10. Worker 配置
|
|
301
|
+
|
|
302
|
+
### create_worker_settings
|
|
303
|
+
|
|
304
|
+
从 eapi 的 `BaseSettings` 自动生成 ARQ Worker 配置:
|
|
305
|
+
|
|
306
|
+
```python
|
|
307
|
+
from dwyeapi.tasks import create_worker_settings
|
|
308
|
+
|
|
309
|
+
WorkerSettings = create_worker_settings(settings, session_factory=session_factory)
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
自动映射:
|
|
313
|
+
- `settings.redis_url` → ARQ `RedisSettings`
|
|
314
|
+
- `settings.task_max_jobs` → `max_jobs` (默认 5)
|
|
315
|
+
- `settings.task_job_timeout` → `job_timeout` (默认 3600s)
|
|
316
|
+
|
|
317
|
+
### 并发控制
|
|
318
|
+
|
|
319
|
+
```env
|
|
320
|
+
# 单 Worker, 5 个协程并发
|
|
321
|
+
TASK_MAX_JOBS=5
|
|
322
|
+
|
|
323
|
+
# 多 Worker 实例: 总并发 = max_jobs x Worker 数
|
|
324
|
+
# 终端 1: arq app.worker.WorkerSettings
|
|
325
|
+
# 终端 2: arq app.worker.WorkerSettings
|
|
326
|
+
# 总并发 = 5 + 5 = 10
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
IO 密集型任务 (文件导出、第三方 API) 可设 5-10;CPU 密集型任务 (AI 推理) 建议等于 CPU 核心数。
|
|
330
|
+
|
|
331
|
+
## 11. 公共 API 速查
|
|
332
|
+
|
|
333
|
+
```python
|
|
334
|
+
from dwyeapi.tasks import (
|
|
335
|
+
setup_tasks, # 一站式初始化
|
|
336
|
+
task_router, # 开箱即用 APIRouter
|
|
337
|
+
register, # @register 装饰器
|
|
338
|
+
TaskContext, # 任务上下文类型
|
|
339
|
+
TaskStatus, # 状态枚举
|
|
340
|
+
create_worker_settings, # Worker 工厂
|
|
341
|
+
)
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
| 导出项 | 用途 |
|
|
345
|
+
|--------|------|
|
|
346
|
+
| `setup_tasks(app, settings, session_factory)` | lifespan 中调用, 初始化 ARQ pool |
|
|
347
|
+
| `task_router` | APIRouter, include_router 挂载 |
|
|
348
|
+
| `register(task_type)` | 装饰器, 注册异步任务函数 |
|
|
349
|
+
| `TaskContext` | 任务函数第一个参数的类型 |
|
|
350
|
+
| `TaskStatus` | 枚举: PENDING/RUNNING/SUCCESS/FAILED/CANCELED |
|
|
351
|
+
| `create_worker_settings(settings, session_factory)` | 生成 ARQ WorkerSettings 类 |
|
|
352
|
+
|
|
353
|
+
## 12. 完整最小示例
|
|
354
|
+
|
|
355
|
+
```
|
|
356
|
+
myproject/
|
|
357
|
+
├── app/
|
|
358
|
+
│ ├── config.py # Settings(BaseSettings)
|
|
359
|
+
│ ├── main.py # FastAPI + setup_tasks + task_router
|
|
360
|
+
│ ├── tasks.py # @register 任务函数
|
|
361
|
+
│ └── worker.py # WorkerSettings
|
|
362
|
+
├── .env
|
|
363
|
+
└── pyproject.toml
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
```toml
|
|
367
|
+
# pyproject.toml
|
|
368
|
+
[project]
|
|
369
|
+
dependencies = ["dwyeapi[tasks]", "asyncpg"]
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
```env
|
|
373
|
+
# .env
|
|
374
|
+
DATABASE_URL=postgresql+asyncpg://user:pass@localhost/mydb
|
|
375
|
+
REDIS_URL=redis://localhost:6379/0
|
|
376
|
+
SECRET_KEY=your-secret-key
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
```bash
|
|
380
|
+
# 启动
|
|
381
|
+
uvicorn app.main:app --reload # API
|
|
382
|
+
arq app.worker.WorkerSettings # Worker
|
|
383
|
+
|
|
384
|
+
# 测试
|
|
385
|
+
curl -X POST http://localhost:8000/tasks \
|
|
386
|
+
-d '{"task_type": "process_data", "params": {"file": "test.csv"}}'
|
|
387
|
+
```
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dwy-ekit
|
|
3
|
+
description: "@dwydev/ekit 前端工具库速查(v0.7.0)。触发条件:HTTP 请求、dwyeapi 响应契约对接、localStorage/Cookie、表单校验、日期/时区、剪贴板、查询字符串、文件下载、PII 脱敏、Vue composable 工具。"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @dwydev/ekit 工具库速查(v0.7.0)
|
|
7
|
+
|
|
8
|
+
Vue 3 项目通用工具库,**薄封装层**:底层用 axios / dayjs / zod / js-cookie / qs / file-saver / @vueuse/core,**对外只暴露 ekit 自有契约**,不泄露底层库类型。
|
|
9
|
+
|
|
10
|
+
包含 10 个模块:`request` / `storage` / `cookie` / `date` / `validators` / `copy` / `qs` / `file` / `hooks` / `masking`。
|
|
11
|
+
|
|
12
|
+
## 安装
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pnpm add @dwydev/ekit
|
|
16
|
+
# peerDependencies: vue ^3.5.0, axios ^1.0.0, dayjs ^1.11.0, zod ^3.25.0, @vueuse/core ^14.2.1
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import {
|
|
21
|
+
createRequest, useStorage, useCookie,
|
|
22
|
+
formatDateTime, isPhone, copyText, stringify, downloadFile,
|
|
23
|
+
useDebounce, maskPhone,
|
|
24
|
+
} from '@dwydev/ekit'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 防腐层硬性约束(写代码前必读)
|
|
28
|
+
|
|
29
|
+
ekit 对外类型 **不允许** 出现底层库类名。下面这些导入 **会失败** —— 因为 ekit 不再 re-export:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
// ❌ 错误:ekit 不导出底层类型
|
|
33
|
+
import type { AxiosInstance, IStringifyOptions, CookieAttributes } from '@dwydev/ekit'
|
|
34
|
+
import { dayjs } from '@dwydev/ekit'
|
|
35
|
+
|
|
36
|
+
// ✅ 正确:用 ekit 自有契约
|
|
37
|
+
import type { HttpClient, StringifyOptions, CookieOptions } from '@dwydev/ekit'
|
|
38
|
+
import { now, formatTimestamp } from '@dwydev/ekit' // 不需要 dayjs 实例
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
业务代码也 **禁止** 直接调底层 API:
|
|
42
|
+
|
|
43
|
+
| 禁止 | 改用 |
|
|
44
|
+
|------|------|
|
|
45
|
+
| `new Date()` / `toLocaleString()` | `now()` / `formatTimestamp()` / `formatDateTime()` |
|
|
46
|
+
| `localStorage.setItem(...)` | `storage.set()` 或 `useStorage()` |
|
|
47
|
+
| `navigator.clipboard.writeText(...)` | `copyText()` 或 `useClipboard()` |
|
|
48
|
+
| `document.cookie = ...` | `cookie.set()` 或 `useCookie()` |
|
|
49
|
+
| `document.addEventListener('click', ...)` | `useEventListener()` |
|
|
50
|
+
| 手写 axios 实例 | `createRequest()` |
|
|
51
|
+
|
|
52
|
+
## 查阅源码
|
|
53
|
+
|
|
54
|
+
每个模块源码在 `/Users/chances/WebstormProjects/dwy-shared/frontend/ekit/src/{module}/`,函数都很短可以直接读。
|
|
55
|
+
|
|
56
|
+
## 模块索引
|
|
57
|
+
|
|
58
|
+
按需读取对应 references 文件,避免一次性加载全部 API 文档。
|
|
59
|
+
|
|
60
|
+
| 模块 | 用途 | 详见 |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| `request` | HTTP 客户端(防腐层 + 4 个内置插件 + dwyeapi 响应契约 + 业务错误捕获) | `references/request.md` |
|
|
63
|
+
| `storage` | localStorage 封装(@vueuse/core 的 useStorage + 静态 storage 对象) | `references/data.md` § storage |
|
|
64
|
+
| `cookie` | Cookie 读写(js-cookie 薄封装 + useCookie) | `references/data.md` § cookie |
|
|
65
|
+
| `date` | 日期 / 时间 / 时区格式化(基于 dayjs,不暴露实例) | `references/data.md` § date |
|
|
66
|
+
| `qs` | 查询字符串序列化 / 解析(基于 qs) | `references/data.md` § qs |
|
|
67
|
+
| `validators` | 表单校验(布尔函数 + zod schema 配合 vee-validate) | `references/form.md` § validators |
|
|
68
|
+
| `masking` | PII 数据脱敏(手机/邮箱/身份证/银行卡/姓名/地址等) | `references/form.md` § masking |
|
|
69
|
+
| `copy` | 剪贴板(copyText + @vueuse/core 的 useClipboard) | `references/ui.md` § copy |
|
|
70
|
+
| `file` | 文件下载 / Blob / 文件大小格式化 | `references/ui.md` § file |
|
|
71
|
+
| `hooks` | Vue Composables(全部再导出 @vueuse/core) | `references/ui.md` § hooks |
|
|
72
|
+
|
|
73
|
+
## v0.6.0 迁移要点
|
|
74
|
+
|
|
75
|
+
| 旧 | 新 |
|
|
76
|
+
|----|----|
|
|
77
|
+
| `createRequest(): AxiosInstance` | `createRequest(): HttpClient` |
|
|
78
|
+
| `RequestPlugin` | `HttpPlugin` |
|
|
79
|
+
| `import { dayjs } from '@dwydev/ekit'` | 用 `now / formatTimestamp / formatInTimezone / formatDateTime` 等函数 |
|
|
80
|
+
| `CookieAttributes` | `CookieOptions` |
|
|
81
|
+
| `IStringifyOptions / IParseOptions` | `StringifyOptions / ParseOptions` |
|
|
82
|
+
| `DownloadOptions.requestInstance: AxiosInstance` | `DownloadOptions.requester: FileRequester` |
|
|
83
|
+
| 自写 `useDebounce` 返回可写 `Ref` | VueUse `refDebounced` 返回 `Readonly<Ref>` |
|
|
84
|
+
| 自写 `useClipboard` 返回 `{ text, copy, copied, isSupported }`(isSupported 是 boolean) | VueUse 版(isSupported 是 `ComputedRef<boolean>`) |
|
|
85
|
+
| `refreshTokenPlugin` 自动重放 | 必须传 `retry: (cfg) => client.request(cfg)` |
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# 数据与查询字符串(storage / cookie / date / qs)
|
|
2
|
+
|
|
3
|
+
> 何时读这份:当用户使用本模块的 API 或问法涉及本模块功能时读取(localStorage 读写、Cookie 读写、日期/时间/时区格式化、查询字符串序列化/解析)。
|
|
4
|
+
|
|
5
|
+
## storage — localStorage
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { useStorage, storage } from '@dwydev/ekit'
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
### useStorage(key, defaultValue) — 直接再导出 @vueuse/core
|
|
12
|
+
|
|
13
|
+
跨标签页同步、SSR-safe、自动 JSON 序列化。**API 与 VueUse 一致**(不再是 ekit 自写)。
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
const token = useStorage<string>('token', '')
|
|
17
|
+
const config = useStorage('config', { theme: 'light' }, localStorage, { mergeDefaults: true })
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### storage — 静态对象(非组件场景)
|
|
21
|
+
|
|
22
|
+
| 方法 | 签名 |
|
|
23
|
+
|------|------|
|
|
24
|
+
| `get<T>(key, defaultValue?)` | 返回 `T \| undefined`,JSON 解析失败按字符串返回 |
|
|
25
|
+
| `set(key, value)` | 自动 `JSON.stringify` |
|
|
26
|
+
| `remove(key)` | 删除 |
|
|
27
|
+
| `clear()` | 清空全部(慎用) |
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## cookie — Cookie 读写
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { useCookie, cookie } from '@dwydev/ekit'
|
|
35
|
+
import type { CookieOptions } from '@dwydev/ekit' // 旧名 CookieAttributes 已废弃
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### CookieOptions
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
interface CookieOptions {
|
|
42
|
+
expires?: number | Date // 天数或 Date;不传 = 会话级
|
|
43
|
+
path?: string // 默认 '/'
|
|
44
|
+
domain?: string
|
|
45
|
+
secure?: boolean // 仅 HTTPS
|
|
46
|
+
sameSite?: 'strict' | 'lax' | 'none'
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### cookie(静态)
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
cookie.get<User>('user')
|
|
54
|
+
cookie.set('user', { id: 1, name: 'Alice' }, { expires: 7, secure: true })
|
|
55
|
+
cookie.remove('user', { path: '/' }) // 删除时 path/domain 必须与写入一致
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### useCookie(key, defaultValue?) → Ref
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
const lang = useCookie('lang', 'zh-CN')
|
|
62
|
+
lang.value = 'en-US' // 自动写入 cookie
|
|
63
|
+
lang.value = undefined // 自动删除
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## date — 日期 / 时间 / 时区
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import {
|
|
72
|
+
now, formatTimestamp, formatInTimezone,
|
|
73
|
+
formatDate, formatDateTime, formatTime, formatRelativeTime, formatBy,
|
|
74
|
+
} from '@dwydev/ekit'
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
底层是 dayjs(含 utc + timezone + relativeTime + zh-cn locale 插件),**不导出 dayjs 实例**。所有函数对外用 `string | number | Date`。
|
|
78
|
+
|
|
79
|
+
### 时间戳工具(v0.6.0 新增)
|
|
80
|
+
|
|
81
|
+
| 函数 | 签名 | 说明 |
|
|
82
|
+
|------|------|------|
|
|
83
|
+
| `now()` | `(): number` | 当前 UTC Unix 毫秒戳,等价于 `Date.now()` |
|
|
84
|
+
| `formatTimestamp(ts, fmt?)` | `(ts: number, fmt?: string): string` | 把毫秒戳格式化为本地时区字符串。默认 `'YYYY-MM-DD HH:mm:ss'` |
|
|
85
|
+
| `formatInTimezone(tz, ts?, fmt?)` | `(tz: string, ts?: number, fmt?: string): string` | 在指定 IANA 时区下格式化(如 `'Asia/Shanghai'` / `'America/New_York'` / `'UTC'`),ts 不传用当前时间 |
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
const ts = now() // 1745318445000
|
|
89
|
+
formatTimestamp(ts) // '2026-04-22 18:40:45'
|
|
90
|
+
formatTimestamp(ts, 'YYYY/MM/DD') // '2026/04/22'
|
|
91
|
+
formatInTimezone('America/New_York', ts) // '2026-04-22 06:40:45'
|
|
92
|
+
formatInTimezone('UTC', ts, 'HH:mm') // '10:40'
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 普通格式化
|
|
96
|
+
|
|
97
|
+
| 函数 | 输出 | 输入 |
|
|
98
|
+
|------|------|------|
|
|
99
|
+
| `formatDate(input)` | `YYYY-MM-DD` | `string \| number \| Date` |
|
|
100
|
+
| `formatDateTime(input)` | `YYYY-MM-DD HH:mm:ss` | 同上 |
|
|
101
|
+
| `formatTime(input)` | `HH:mm` | 同上 |
|
|
102
|
+
| `formatRelativeTime(input)` | `刚刚` / `N 分钟前` / `N 小时前` / `N 天前`;> 30 天退化为 `YYYY-MM-DD` | 同上 |
|
|
103
|
+
| `formatBy(input, template)` | 自定义 dayjs 模板 | `formatBy(ts, 'YYYY年MM月DD日')` → `2026年04月22日` |
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## qs — 查询字符串
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { stringify, parse } from '@dwydev/ekit'
|
|
111
|
+
import type { StringifyOptions, ParseOptions } from '@dwydev/ekit' // 旧名 IStringifyOptions / IParseOptions 已废弃
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
stringify({ ids: [1, 2], name: 'a' }) // 'ids=1&ids=2&name=a' 默认 arrayFormat: 'repeat'
|
|
116
|
+
stringify({ ids: [1, 2] }, { arrayFormat: 'brackets' }) // 'ids[]=1&ids[]=2'
|
|
117
|
+
stringify({ a: 1 }, { addQueryPrefix: true }) // '?a=1'
|
|
118
|
+
|
|
119
|
+
parse('?a=1&b=2') // { a: '1', b: '2' } 默认 ignoreQueryPrefix: true
|
|
120
|
+
parse('a.b=1', { allowDots: true }) // { a: { b: '1' } }
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### StringifyOptions / ParseOptions
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
interface StringifyOptions {
|
|
127
|
+
arrayFormat?: 'repeat' | 'brackets' | 'indices' | 'comma'
|
|
128
|
+
addQueryPrefix?: boolean
|
|
129
|
+
skipNulls?: boolean
|
|
130
|
+
}
|
|
131
|
+
interface ParseOptions {
|
|
132
|
+
ignoreQueryPrefix?: boolean
|
|
133
|
+
allowDots?: boolean
|
|
134
|
+
}
|
|
135
|
+
```
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# 表单与脱敏(validators / masking)
|
|
2
|
+
|
|
3
|
+
> 何时读这份:当用户使用本模块的 API 或问法涉及本模块功能时读取(表单校验、zod schema、vee-validate 集成、PII 数据脱敏)。
|
|
4
|
+
|
|
5
|
+
## validators — 表单校验
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import {
|
|
9
|
+
isPhone, isEmail, isIdCard, isUrl, isRequired, minLength, maxLength,
|
|
10
|
+
// 也可用 zod schema 配合 vee-validate
|
|
11
|
+
phoneSchema, emailSchema, idCardSchema, urlSchema,
|
|
12
|
+
requiredSchema, minLengthSchema, maxLengthSchema,
|
|
13
|
+
} from '@dwydev/ekit'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### 布尔校验函数
|
|
17
|
+
|
|
18
|
+
| 函数 | 签名 | 规则 |
|
|
19
|
+
|------|------|------|
|
|
20
|
+
| `isPhone(value)` | `string => boolean` | 中国手机号 1[3-9] 开头 11 位 |
|
|
21
|
+
| `isEmail(value)` | `string => boolean` | 基础邮箱 |
|
|
22
|
+
| `isIdCard(value)` | `string => boolean` | 18 位身份证(末位允许 X/x) |
|
|
23
|
+
| `isUrl(value)` | `string => boolean` | 合法 URL |
|
|
24
|
+
| `isRequired(value)` | `any => boolean` | 非空(0 / false 视为有效) |
|
|
25
|
+
| `minLength(value, n)` | `(string, number) => boolean` | 最小长度 |
|
|
26
|
+
| `maxLength(value, n)` | `(string, number) => boolean` | 最大长度 |
|
|
27
|
+
|
|
28
|
+
### Zod schema(配合 vee-validate)
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { useForm } from 'vee-validate'
|
|
32
|
+
import { toTypedSchema } from '@vee-validate/zod'
|
|
33
|
+
import { z } from 'zod'
|
|
34
|
+
import { phoneSchema, emailSchema } from '@dwydev/ekit'
|
|
35
|
+
|
|
36
|
+
const schema = toTypedSchema(z.object({
|
|
37
|
+
phone: phoneSchema,
|
|
38
|
+
email: emailSchema,
|
|
39
|
+
}))
|
|
40
|
+
const { defineField, errors } = useForm({ validationSchema: schema })
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
> `minLengthSchema(n)` / `maxLengthSchema(n)` 是工厂函数,调用后返回 schema。
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## masking — PII 脱敏
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import {
|
|
51
|
+
maskPhone, maskEmail, maskIdCard, maskBankCard,
|
|
52
|
+
maskName, maskAddress, maskIp, maskLicensePlate, maskText,
|
|
53
|
+
} from '@dwydev/ekit'
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
纯函数,无外部依赖。**输入不符合格式时原样返回,不抛异常**。
|
|
57
|
+
|
|
58
|
+
| 函数 | 规则 | 示例 |
|
|
59
|
+
|------|------|------|
|
|
60
|
+
| `maskPhone(phone)` | 前 3 后 4 | `'138****5678'` |
|
|
61
|
+
| `maskEmail(email)` | 首字符 + `***` + @域名 | `'z***@gmail.com'` |
|
|
62
|
+
| `maskIdCard(id)` | 前 3 后 4(18 位) | `'420***********1234'` |
|
|
63
|
+
| `maskBankCard(card)` | 前 4 后 4(13~19 位) | `'6222********1234'` |
|
|
64
|
+
| `maskName(name)` | 2 字 → 姓+`*`;3+ 字 → 姓+`*`×(n-2)+末字 | `'张*'` / `'张*明'` |
|
|
65
|
+
| `maskAddress(addr)` | 保留省/市/区/县/镇/旗前缀 + `****` | `'浙江省杭州市西湖区****'` |
|
|
66
|
+
| `maskIp(ip)` | IPv4 末段替换 `*` | `'192.168.1.*'` |
|
|
67
|
+
| `maskLicensePlate(plate)` | 前 2 后 1,中间 `***` | `'浙A***8'` |
|
|
68
|
+
| `maskText(text, start=1, end=1, ch='*')` | 通用:保留首尾 N 位 | `maskText('1234567890', 2, 3)` → `'12*****890'` |
|