Implement compression quota refunds and admin manual subscription
This commit is contained in:
685
docs/api.md
Normal file
685
docs/api.md
Normal file
@@ -0,0 +1,685 @@
|
||||
# API 接口文档(v1)- ImageForge
|
||||
|
||||
面向两类使用者:
|
||||
- **网站(Web)**:上传/批量/历史/账单等(可能包含匿名试用)。
|
||||
- **对外 API(Developer API)**:API Key 调用、可计量可计费、适配 CI/CD 与服务端集成。
|
||||
|
||||
产品范围与计费口径见:
|
||||
- `docs/prd.md`
|
||||
- `docs/billing.md`
|
||||
|
||||
---
|
||||
|
||||
## 1. 基础信息
|
||||
|
||||
- **Base URL**: `https://your-domain.com/api/v1`
|
||||
- **数据格式**: JSON(除明确标注“返回二进制”接口)
|
||||
- **时间格式**: ISO 8601 / UTC(如:`2025-01-15T10:30:00Z`)
|
||||
- **ID 格式**: UUID 字符串
|
||||
|
||||
---
|
||||
|
||||
## 2. 认证
|
||||
|
||||
支持三种身份:
|
||||
|
||||
### 2.1 JWT(网站/管理后台)
|
||||
```http
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
### 2.2 API Key(对外 API)
|
||||
```http
|
||||
X-API-Key: <your-api-key>
|
||||
```
|
||||
|
||||
> **注意**:仅 **Pro** 和 **Business** 套餐用户可创建 API Key。Free 用户尝试创建时返回 `FORBIDDEN`(HTTP `403`)。
|
||||
|
||||
### 2.3 匿名试用(仅网站场景)
|
||||
- 不提供 API Key;
|
||||
- 通过 Cookie 维持匿名会话(服务端签发),仅允许较小文件与较低频率。
|
||||
- 每日 10 次(以成功压缩文件数计);超出返回 `QUOTA_EXCEEDED`(HTTP `402`)。
|
||||
- 日界:自然日(UTC+8),次日 00:00 重置。
|
||||
- **匿名试用硬限制:Cookie + IP 双限制**(两者任一超出都拒绝),降低刷会话绕过风险。
|
||||
|
||||
---
|
||||
|
||||
## 3. 通用约定
|
||||
|
||||
### 3.1 幂等(强烈建议)
|
||||
对会产生计费/创建任务的接口,建议客户端传:
|
||||
```http
|
||||
Idempotency-Key: <uuid-or-random-string>
|
||||
```
|
||||
|
||||
规则(建议口径):
|
||||
- 同一个 `Idempotency-Key` 在 TTL 内重复请求,若请求参数一致则返回首次结果(不重复扣费/不重复创建任务)。
|
||||
- 若参数不一致,返回 `409 IDEMPOTENCY_CONFLICT`。
|
||||
|
||||
### 3.2 限流(Rate Limit)
|
||||
超出限制返回:
|
||||
- HTTP `429`
|
||||
- 头:`Retry-After: <seconds>`
|
||||
|
||||
建议头(可选):
|
||||
- `RateLimit-Limit`
|
||||
- `RateLimit-Remaining`
|
||||
- `RateLimit-Reset`
|
||||
|
||||
### 3.3 配额(Quota / Billing)
|
||||
配额不足(当期额度耗尽)返回:
|
||||
- HTTP `402`
|
||||
- 错误码:`QUOTA_EXCEEDED`
|
||||
|
||||
配额周期:
|
||||
- Pro/Business(付费):按订阅周期重置(`period_start` ~ `period_end`),不是自然月。
|
||||
- Free(未订阅):按自然月(UTC+8)重置。
|
||||
- 匿名试用:按自然日(UTC+8)重置。
|
||||
|
||||
建议头(可选):
|
||||
- `X-Quota-Limit`
|
||||
- `X-Quota-Remaining`
|
||||
- `X-Quota-Reset-At`
|
||||
|
||||
### 3.4 通用响应格式(JSON)
|
||||
成功:
|
||||
```json
|
||||
{ "success": true, "data": {} }
|
||||
```
|
||||
|
||||
错误:
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "ERROR_CODE",
|
||||
"message": "错误描述",
|
||||
"request_id": "req_..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.5 错误码(建议集合)
|
||||
| 错误码 | HTTP | 说明 |
|
||||
|---|---:|---|
|
||||
| `INVALID_REQUEST` | 400 | 参数不合法 |
|
||||
| `INVALID_IMAGE` | 400 | 图片解码失败/文件损坏 |
|
||||
| `UNSUPPORTED_FORMAT` | 400 | 不支持的格式 |
|
||||
| `TOO_MANY_PIXELS` | 400 | 像素超限(防图片炸弹) |
|
||||
| `UNAUTHORIZED` | 401 | 未认证 |
|
||||
| `FORBIDDEN` | 403 | 权限不足 |
|
||||
| `NOT_FOUND` | 404 | 资源不存在 |
|
||||
| `IDEMPOTENCY_CONFLICT` | 409 | 幂等 key 冲突 |
|
||||
| `QUOTA_EXCEEDED` | 402 | 配额不足 |
|
||||
| `FILE_TOO_LARGE` | 413 | 文件过大 |
|
||||
| `RATE_LIMITED` | 429 | 请求过于频繁 |
|
||||
| `EMAIL_NOT_VERIFIED` | 403 | 邮箱未验证 |
|
||||
| `INVALID_TOKEN` | 400 | Token 无效或已过期 |
|
||||
| `COMPRESSION_FAILED` | 500 | 压缩失败 |
|
||||
| `STORAGE_UNAVAILABLE` | 503 | 存储不可用 |
|
||||
| `MAIL_SEND_FAILED` | 500 | 邮件发送失败 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 认证接口
|
||||
|
||||
### 4.1 用户注册
|
||||
```http
|
||||
POST /auth/register
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
请求体:
|
||||
```json
|
||||
{ "email": "user@example.com", "password": "securepassword123", "username": "myusername" }
|
||||
```
|
||||
|
||||
响应:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"user": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"email": "user@example.com",
|
||||
"username": "myusername",
|
||||
"email_verified": false,
|
||||
"created_at": "2025-01-15T10:30:00Z"
|
||||
},
|
||||
"token": "eyJhbGciOi...",
|
||||
"message": "注册成功,验证邮件已发送至您的邮箱"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **注意**:注册后自动发送验证邮件。用户需验证邮箱后才能使用压缩功能(未验证时调用压缩接口返回 `EMAIL_NOT_VERIFIED`)。
|
||||
|
||||
### 4.2 用户登录
|
||||
```http
|
||||
POST /auth/login
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
请求体:
|
||||
```json
|
||||
{ "email": "user@example.com", "password": "securepassword123" }
|
||||
```
|
||||
|
||||
响应:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"token": "eyJhbGciOi...",
|
||||
"expires_at": "2025-01-22T10:30:00Z",
|
||||
"user": { "id": "550e8400-e29b-41d4-a716-446655440000", "email": "user@example.com", "username": "myusername", "role": "user" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 刷新 Token
|
||||
```http
|
||||
POST /auth/refresh
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
### 4.4 登出
|
||||
```http
|
||||
POST /auth/logout
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
### 4.5 发送验证邮件
|
||||
用户注册后自动发送一次;此接口用于重新发送。
|
||||
|
||||
```http
|
||||
POST /auth/send-verification
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
**限流**:同一用户 1 分钟内最多 1 次
|
||||
|
||||
响应:
|
||||
```json
|
||||
{ "success": true, "data": { "message": "验证邮件已发送,请查收" } }
|
||||
```
|
||||
|
||||
### 4.6 验证邮箱
|
||||
```http
|
||||
POST /auth/verify-email
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
请求体:
|
||||
```json
|
||||
{ "token": "verification-token-from-email" }
|
||||
```
|
||||
|
||||
响应:
|
||||
```json
|
||||
{ "success": true, "data": { "message": "邮箱验证成功" } }
|
||||
```
|
||||
|
||||
### 4.7 请求密码重置
|
||||
```http
|
||||
POST /auth/forgot-password
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
请求体:
|
||||
```json
|
||||
{ "email": "user@example.com" }
|
||||
```
|
||||
|
||||
**限流**:同一 IP 1 分钟内最多 3 次
|
||||
|
||||
响应(无论邮箱是否存在都返回成功,防止枚举):
|
||||
```json
|
||||
{ "success": true, "data": { "message": "如果该邮箱已注册,您将收到重置邮件" } }
|
||||
```
|
||||
|
||||
### 4.8 重置密码
|
||||
```http
|
||||
POST /auth/reset-password
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
请求体:
|
||||
```json
|
||||
{ "token": "reset-token-from-email", "new_password": "new-secure-password" }
|
||||
```
|
||||
|
||||
响应:
|
||||
```json
|
||||
{ "success": true, "data": { "message": "密码重置成功,请重新登录" } }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 图片压缩接口
|
||||
|
||||
### 5.1 单图压缩(同步,返回 JSON + 下载链接)
|
||||
适用于网站与轻量同步调用(服务端可选择是否落盘/落对象存储)。
|
||||
|
||||
```http
|
||||
POST /compress
|
||||
Content-Type: multipart/form-data
|
||||
Authorization: Bearer <token> # 或 X-API-Key;网站匿名试用可不带
|
||||
Idempotency-Key: <key> # 建议
|
||||
```
|
||||
|
||||
表单字段:
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|---|---|---:|---|
|
||||
| `file` | File | 是 | 图片文件 |
|
||||
| `compression_rate` | Integer | 否 | 压缩率 1-100(数值越大压缩越强),优先级高于 `level` |
|
||||
| `level` | String | 否 | `high` / `medium` / `low`(兼容参数,默认 `medium`) |
|
||||
| `output_format` | String | 否 | 已停用,仅支持保持原格式 |
|
||||
| `max_width` | Integer | 否 | 最大宽度(等比缩放) |
|
||||
| `max_height` | Integer | 否 | 最大高度(等比缩放) |
|
||||
| `preserve_metadata` | Boolean | 否 | 是否保留元数据(默认 `false`) |
|
||||
|
||||
响应:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"task_id": "550e8400-e29b-41d4-a716-446655440100",
|
||||
"file_id": "550e8400-e29b-41d4-a716-446655440101",
|
||||
"format_in": "png",
|
||||
"format_out": "png",
|
||||
"original_size": 1024000,
|
||||
"compressed_size": 256000,
|
||||
"saved_bytes": 768000,
|
||||
"saved_percent": 75.0,
|
||||
"download_url": "/downloads/550e8400-e29b-41d4-a716-446655440101",
|
||||
"expires_at": "2025-01-15T11:30:00Z",
|
||||
"billing": { "units_charged": 1 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 单图压缩(同步,直接返回二进制)
|
||||
更贴近开发者体验,适用于 SDK/CI。
|
||||
|
||||
```http
|
||||
POST /compress/direct
|
||||
Content-Type: multipart/form-data
|
||||
X-API-Key: <your-api-key> # 或 Bearer token(不建议匿名)
|
||||
Idempotency-Key: <key> # 建议
|
||||
```
|
||||
|
||||
成功响应:
|
||||
- HTTP `200`
|
||||
- Body:压缩后的图片二进制
|
||||
- `Content-Type`: `image/png` / `image/jpeg` / `image/webp` / `image/avif` / `image/gif` / `image/bmp` / `image/tiff` / `image/x-icon`
|
||||
|
||||
建议响应头(示例):
|
||||
```http
|
||||
ImageForge-Original-Size: 1024000
|
||||
ImageForge-Compressed-Size: 256000
|
||||
ImageForge-Saved-Bytes: 768000
|
||||
ImageForge-Saved-Percent: 75.0
|
||||
ImageForge-Units-Charged: 1
|
||||
```
|
||||
|
||||
### 5.3 批量压缩(异步任务)
|
||||
适用于多文件或大文件;由 Worker 处理并持续更新进度。
|
||||
|
||||
```http
|
||||
POST /compress/batch
|
||||
Content-Type: multipart/form-data
|
||||
Authorization: Bearer <token> # 或 X-API-Key
|
||||
Idempotency-Key: <key> # 建议
|
||||
```
|
||||
|
||||
表单字段:
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|---|---|---:|---|
|
||||
| `files[]` | File[] | 是 | 图片文件数组(上限由套餐决定) |
|
||||
| `compression_rate` | Integer | 否 | 压缩率 1-100(数值越大压缩越强),优先级高于 `level` |
|
||||
| `level` | String | 否 | `high` / `medium` / `low`(兼容参数) |
|
||||
| `output_format` | String | 否 | 已停用,仅支持保持原格式 |
|
||||
| `preserve_metadata` | Boolean | 否 | 是否保留元数据(默认 `false`) |
|
||||
|
||||
响应:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"task_id": "550e8400-e29b-41d4-a716-446655440200",
|
||||
"total_files": 10,
|
||||
"status": "pending",
|
||||
"status_url": "/compress/tasks/550e8400-e29b-41d4-a716-446655440200"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
配额规则补充:
|
||||
- 若本周期剩余单位不足以覆盖本次上传的文件数,服务端应直接返回 `402 QUOTA_EXCEEDED`(不创建任务)。
|
||||
|
||||
### 5.4 查询任务状态
|
||||
```http
|
||||
GET /compress/tasks/{task_id}
|
||||
Authorization: Bearer <token> # 或 X-API-Key;匿名试用需携带 Cookie 会话
|
||||
```
|
||||
|
||||
响应:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"task_id": "550e8400-e29b-41d4-a716-446655440200",
|
||||
"status": "completed",
|
||||
"progress": 100,
|
||||
"total_files": 10,
|
||||
"completed_files": 10,
|
||||
"failed_files": 0,
|
||||
"files": [
|
||||
{
|
||||
"file_id": "550e8400-e29b-41d4-a716-446655440201",
|
||||
"original_name": "photo1.png",
|
||||
"original_size": 1024000,
|
||||
"compressed_size": 256000,
|
||||
"saved_percent": 75.0,
|
||||
"status": "completed",
|
||||
"download_url": "/downloads/550e8400-e29b-41d4-a716-446655440201"
|
||||
}
|
||||
],
|
||||
"download_all_url": "/downloads/tasks/550e8400-e29b-41d4-a716-446655440200",
|
||||
"created_at": "2025-01-15T10:30:00Z",
|
||||
"completed_at": "2025-01-15T10:31:00Z",
|
||||
"expires_at": "2025-01-22T10:30:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.5 取消任务(可选)
|
||||
```http
|
||||
POST /compress/tasks/{task_id}/cancel
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
### 5.6 删除任务与文件(隐私/合规)
|
||||
```http
|
||||
DELETE /compress/tasks/{task_id}
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 下载接口
|
||||
|
||||
### 6.1 下载单个文件
|
||||
```http
|
||||
GET /downloads/{file_id}
|
||||
Authorization: Bearer <token> # 或 X-API-Key;匿名试用需 Cookie 会话
|
||||
```
|
||||
|
||||
### 6.2 下载批量 ZIP
|
||||
```http
|
||||
GET /downloads/tasks/{task_id}
|
||||
Authorization: Bearer <token> # 或 X-API-Key;匿名试用需 Cookie 会话
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 用户接口
|
||||
|
||||
### 7.1 获取当前用户信息
|
||||
```http
|
||||
GET /user/profile
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
响应(示例):
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"email": "user@example.com",
|
||||
"username": "myusername",
|
||||
"role": "user"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 更新用户信息
|
||||
```http
|
||||
PUT /user/profile
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
### 7.3 修改密码
|
||||
```http
|
||||
PUT /user/password
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
### 7.4 获取压缩历史
|
||||
```http
|
||||
GET /user/history?page=1&limit=20
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. API Key 管理
|
||||
|
||||
### 8.1 获取 API Key 列表
|
||||
```http
|
||||
GET /user/api-keys
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
### 8.2 创建 API Key
|
||||
```http
|
||||
POST /user/api-keys
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
请求体:
|
||||
```json
|
||||
{ "name": "Production Server", "permissions": ["compress", "batch_compress"] }
|
||||
```
|
||||
|
||||
响应:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440300",
|
||||
"name": "Production Server",
|
||||
"key": "if_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
"message": "请保存此 Key,它只会显示一次"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8.3 轮换 API Key(可选)
|
||||
```http
|
||||
POST /user/api-keys/{key_id}/rotate
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
### 8.4 删除/禁用 API Key
|
||||
```http
|
||||
DELETE /user/api-keys/{key_id}
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 计费与用量(Billing)
|
||||
|
||||
### 9.1 获取套餐列表(公开)
|
||||
```http
|
||||
GET /billing/plans
|
||||
```
|
||||
|
||||
响应(示例):
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"plans": [
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440900",
|
||||
"code": "pro_monthly",
|
||||
"name": "Pro(月付)",
|
||||
"currency": "CNY",
|
||||
"amount_cents": 1999,
|
||||
"interval": "monthly",
|
||||
"included_units_per_period": 10000,
|
||||
"max_file_size_mb": 20,
|
||||
"max_files_per_batch": 50,
|
||||
"retention_days": 7,
|
||||
"features": { "webhook": true }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 9.2 获取当前订阅
|
||||
```http
|
||||
GET /billing/subscription
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
### 9.3 获取当期用量
|
||||
```http
|
||||
GET /billing/usage
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
响应:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"period_start": "2025-01-01T00:00:00Z",
|
||||
"period_end": "2025-02-01T00:00:00Z",
|
||||
"used_units": 120,
|
||||
"included_units": 10000,
|
||||
"bonus_units": 500,
|
||||
"total_units": 10500,
|
||||
"remaining_units": 10380
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 9.4 创建 Checkout(订阅/升级)
|
||||
```http
|
||||
POST /billing/checkout
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
Idempotency-Key: <key>
|
||||
```
|
||||
|
||||
请求体:
|
||||
```json
|
||||
{ "plan_id": "550e8400-e29b-41d4-a716-446655440900" }
|
||||
```
|
||||
|
||||
响应:
|
||||
```json
|
||||
{ "success": true, "data": { "checkout_url": "https://pay.example.com/..." } }
|
||||
```
|
||||
|
||||
### 9.5 打开客户 Portal(管理支付方式/取消订阅)
|
||||
```http
|
||||
POST /billing/portal
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
### 9.6 发票列表
|
||||
```http
|
||||
GET /billing/invoices?page=1&limit=20
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Webhooks(支付回调)
|
||||
|
||||
> 无需登录;必须验签与幂等处理,详见 `docs/billing.md` 与 `docs/security.md`。
|
||||
|
||||
### 10.1 Stripe 回调(示例)
|
||||
```http
|
||||
POST /webhooks/stripe
|
||||
Content-Type: application/json
|
||||
Stripe-Signature: t=...,v1=...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. 管理员接口
|
||||
|
||||
> 需要管理员权限(`role: admin`)
|
||||
|
||||
### 11.1 获取系统统计
|
||||
```http
|
||||
GET /admin/stats
|
||||
Authorization: Bearer <admin_token>
|
||||
```
|
||||
|
||||
### 11.2 用户管理(示例)
|
||||
```http
|
||||
GET /admin/users?page=1&limit=20&search=keyword
|
||||
Authorization: Bearer <admin_token>
|
||||
```
|
||||
|
||||
### 11.3 系统配置
|
||||
```http
|
||||
GET /admin/config
|
||||
Authorization: Bearer <admin_token>
|
||||
```
|
||||
|
||||
```http
|
||||
PUT /admin/config
|
||||
Authorization: Bearer <admin_token>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
### 11.4 任务管理
|
||||
```http
|
||||
GET /admin/tasks?status=processing&page=1
|
||||
Authorization: Bearer <admin_token>
|
||||
```
|
||||
|
||||
```http
|
||||
POST /admin/tasks/{task_id}/cancel
|
||||
Authorization: Bearer <admin_token>
|
||||
```
|
||||
|
||||
### 11.5 计费管理(建议)
|
||||
```http
|
||||
GET /admin/billing/subscriptions?page=1&limit=20
|
||||
Authorization: Bearer <admin_token>
|
||||
```
|
||||
|
||||
```http
|
||||
POST /admin/billing/credits
|
||||
Authorization: Bearer <admin_token>
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. WebSocket(网站任务进度)
|
||||
|
||||
网站侧可用 WebSocket 或 SSE(SSE 更易穿透代理)。当前先保留 WebSocket 方案:
|
||||
|
||||
```
|
||||
ws://your-domain.com/ws/tasks/{task_id}?token=<jwt_token>
|
||||
```
|
||||
|
||||
消息(示例):
|
||||
```json
|
||||
{ "type": "progress", "data": { "task_id": "550e8400-e29b-41d4-a716-446655440200", "progress": 50, "completed_files": 5 } }
|
||||
```
|
||||
334
docs/architecture.md
Normal file
334
docs/architecture.md
Normal file
@@ -0,0 +1,334 @@
|
||||
# 技术架构设计
|
||||
|
||||
> 目标:支撑“网站 + 对外 API + 计费”的商用闭环。产品范围见 `docs/prd.md`,计费口径见 `docs/billing.md`。
|
||||
|
||||
## 系统架构图
|
||||
|
||||
```
|
||||
┌───────────────────────────────┐
|
||||
│ CDN (静态资源/下载可选加速) │
|
||||
└──────────────┬────────────────┘
|
||||
│
|
||||
┌──────▼──────┐
|
||||
│ Nginx/Caddy │
|
||||
│ TLS/反向代理 │
|
||||
└──────┬──────┘
|
||||
┌───────────────────────────┼───────────────────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ Web 前端 (Vue3) │ │ API 服务 (Axum) │ │ Admin 前端(Vue3)│
|
||||
│ 上传/结果/账单 │ │ 认证/计费/接口 │ │ 运营/风控/配置 │
|
||||
└─────────────────┘ └───────┬─────────┘ └─────────────────┘
|
||||
│
|
||||
┌──────────────────┼──────────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ PostgreSQL (DB) │ │ Redis (缓存/队列)│ │ 对象存储 (S3) │
|
||||
│ 用户/任务/账单/用量│ │ Streams/RateLimit│ │ 原图/压缩结果 │
|
||||
└─────────┬───────┘ └───────┬─────────┘ └─────────┬───────┘
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌───────────────┐ │
|
||||
│ │ Worker (Rust) │ │
|
||||
│ │ 压缩/计量/回写 │ │
|
||||
│ └───────────────┘ │
|
||||
│ │
|
||||
▼ ▼
|
||||
┌─────────────────┐ ┌─────────────────┐
|
||||
│ 支付渠道/网关 │◄──────Webhooks───────│ API(Webhook处理) │
|
||||
│ Stripe │ │ 订阅/发票/状态 │
|
||||
└─────────────────┘ └─────────────────┘
|
||||
```
|
||||
|
||||
## 核心组件设计
|
||||
|
||||
### 0. 服务拆分(推荐)
|
||||
|
||||
为避免 CPU 密集的压缩任务影响 API 延迟,建议最小拆分为:
|
||||
- **API 服务**:认证、限流、计费/订阅、任务编排、回调、签名 URL、管理后台 API。
|
||||
- **Worker 服务**:执行图片压缩(CPU 密集)、写入结果、落用量账本、推送进度。
|
||||
|
||||
本地开发可以合并进一个进程(feature flag);生产建议分开部署并可独立扩容。
|
||||
|
||||
### 1. 压缩引擎
|
||||
|
||||
```rust
|
||||
// 压缩配置
|
||||
pub enum CompressionLevel {
|
||||
/// 高压缩比 - 有损压缩,文件最小
|
||||
High,
|
||||
/// 中等压缩 - 平衡模式
|
||||
Medium,
|
||||
/// 低压缩比 - 无损/近无损,质量优先
|
||||
Low,
|
||||
}
|
||||
|
||||
pub struct CompressionConfig {
|
||||
pub level: CompressionLevel,
|
||||
pub output_format: Option<ImageFormat>, // 可选转换格式
|
||||
pub max_width: Option<u32>, // 可选调整尺寸
|
||||
pub max_height: Option<u32>,
|
||||
pub preserve_metadata: bool, // 是否保留元数据(默认 false)
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 压缩策略
|
||||
|
||||
| 格式 | 高压缩比(有损) | 低压缩比(无损) | 使用库 |
|
||||
|------|-----------------|-----------------|--------|
|
||||
| PNG | pngquant 量化到 256 色 | oxipng 无损优化 | `imagequant` + `oxipng` |
|
||||
| JPEG | mozjpeg quality=60 | mozjpeg quality=90 | `mozjpeg` |
|
||||
| WebP | lossy quality=75 | lossless | `webp` |
|
||||
| AVIF | quality=50 | quality=90 | `ravif` |
|
||||
|
||||
```rust
|
||||
// 压缩核心逻辑
|
||||
pub trait ImageCompressor: Send + Sync {
|
||||
async fn compress(&self, input: &[u8], config: &CompressionConfig) -> Result<Vec<u8>>;
|
||||
fn supported_formats(&self) -> Vec<ImageFormat>;
|
||||
}
|
||||
|
||||
pub struct PngCompressor;
|
||||
pub struct JpegCompressor;
|
||||
pub struct WebpCompressor;
|
||||
pub struct AvifCompressor;
|
||||
|
||||
// 统一压缩入口
|
||||
pub struct CompressionEngine {
|
||||
compressors: HashMap<ImageFormat, Box<dyn ImageCompressor>>,
|
||||
}
|
||||
|
||||
impl CompressionEngine {
|
||||
pub async fn compress(&self, input: &[u8], config: &CompressionConfig) -> Result<CompressionResult> {
|
||||
let format = detect_format(input)?;
|
||||
let compressor = self.compressors.get(&format)
|
||||
.ok_or(Error::UnsupportedFormat)?;
|
||||
|
||||
let output = compressor.compress(input, config).await?;
|
||||
|
||||
Ok(CompressionResult {
|
||||
original_size: input.len(),
|
||||
compressed_size: output.len(),
|
||||
format,
|
||||
data: output,
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 任务处理模型
|
||||
|
||||
支持两种模式:
|
||||
|
||||
#### 同步模式(小文件/单文件)
|
||||
```
|
||||
请求 -> 压缩 -> 直接返回结果
|
||||
```
|
||||
|
||||
#### 异步模式(大文件/批量)
|
||||
```
|
||||
请求 -> 创建任务 -> 返回任务ID
|
||||
↓
|
||||
后台Worker处理
|
||||
↓
|
||||
客户端轮询/WebSocket通知
|
||||
↓
|
||||
下载结果
|
||||
```
|
||||
|
||||
```rust
|
||||
pub enum TaskStatus {
|
||||
Pending,
|
||||
Processing,
|
||||
Completed,
|
||||
Failed,
|
||||
}
|
||||
|
||||
pub struct CompressionTask {
|
||||
pub id: Uuid,
|
||||
pub user_id: Option<Uuid>, // 游客为空
|
||||
pub session_id: Option<String>, // 游客会话(Cookie)
|
||||
pub status: TaskStatus,
|
||||
pub files: Vec<FileTask>,
|
||||
pub config: CompressionConfig,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub completed_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
pub struct FileTask {
|
||||
pub id: Uuid,
|
||||
pub original_name: String,
|
||||
pub original_size: u64,
|
||||
pub compressed_size: Option<u64>,
|
||||
pub status: TaskStatus,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 用户认证系统
|
||||
|
||||
```rust
|
||||
// JWT Claims
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Claims {
|
||||
pub sub: Uuid, // 用户ID
|
||||
pub role: UserRole, // 用户角色
|
||||
pub exp: i64, // 过期时间
|
||||
}
|
||||
|
||||
pub enum UserRole {
|
||||
User,
|
||||
Admin,
|
||||
}
|
||||
|
||||
// API Key 认证
|
||||
pub struct ApiKey {
|
||||
pub id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub key_prefix: String, // 前缀索引(仅展示用)
|
||||
pub key_hash: String, // 推荐:HMAC-SHA256(key, server_pepper) 或 sha256+pepper
|
||||
pub name: String,
|
||||
pub permissions: Vec<Permission>,
|
||||
pub rate_limit: u32, // 每分钟请求数
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub last_used_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
pub enum Permission {
|
||||
Compress,
|
||||
BatchCompress,
|
||||
ReadStats,
|
||||
BillingRead, // 查看账单/用量(可选)
|
||||
WebhookManage, // 管理 Webhook(可选)
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 限流与配额(Rate Limit & Quota)
|
||||
|
||||
用途区分:
|
||||
- **限流**:保护服务(超出返回 HTTP `429`)
|
||||
- **配额**:试用/计费限制(超出返回 HTTP `402` / `QUOTA_EXCEEDED`)
|
||||
|
||||
默认建议(最终以 `system_config` + 套餐 `plans.*` 为准):
|
||||
- 匿名试用:`10 req/min` + `10 units/day` + `5MB/文件` + `5 文件/批量`
|
||||
- 登录用户:`60 req/min`;文件大小/批量/保留期/周期额度由套餐决定
|
||||
- API Key:`100 req/min`(可配置);文件大小/批量/周期额度由套餐/Key 覆盖决定
|
||||
|
||||
### 6. 用量计量与计费(Metering & Billing)
|
||||
|
||||
计量口径见 `docs/billing.md`,架构上建议:
|
||||
- Worker 在每个文件成功输出后写入 `usage_events`(账本明细),并更新 `usage_periods`(按订阅周期聚合)。
|
||||
- API 在创建任务/接收同步压缩请求时做**配额预检**(快速失败),Worker 做**最终扣减**(账本落地,保证一致性)。
|
||||
- 对外 API 强烈建议支持 `Idempotency-Key`;DB 侧存储“幂等记录 + 响应摘要”,避免重复扣减与重复任务。
|
||||
|
||||
### 7. 支付回调(Webhooks)
|
||||
|
||||
Stripe 通常通过 webhook 推送订阅/支付状态变更:
|
||||
- API 服务提供 `/webhooks/{provider}` 入口,**验签 + 幂等 + 可重放**。
|
||||
- webhook 事件入库后异步处理(避免回调超时),更新订阅/发票状态,并写审计日志。
|
||||
|
||||
## 核心依赖
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
# Web 框架
|
||||
axum = "0.7"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tower = "0.4"
|
||||
tower-http = { version = "0.5", features = ["cors", "fs", "compression"] }
|
||||
|
||||
# 图片处理
|
||||
image = "0.25"
|
||||
oxipng = "9"
|
||||
imagequant = "4" # PNG 有损压缩(pngquant 核心)
|
||||
mozjpeg = "0.10" # JPEG 压缩
|
||||
webp = "0.3" # WebP 编解码
|
||||
ravif = "0.11" # AVIF 编码
|
||||
|
||||
# 数据库
|
||||
sqlx = { version = "0.7", features = ["runtime-tokio", "postgres", "uuid", "chrono"] }
|
||||
|
||||
# Redis
|
||||
redis = { version = "0.24", features = ["tokio-comp"] }
|
||||
|
||||
# 认证
|
||||
jsonwebtoken = "9"
|
||||
argon2 = "0.5" # 密码哈希
|
||||
|
||||
# 工具
|
||||
uuid = { version = "1", features = ["v4", "serde"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
thiserror = "1"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
|
||||
# 配置
|
||||
config = "0.14"
|
||||
dotenvy = "0.15"
|
||||
```
|
||||
|
||||
## 性能考虑
|
||||
|
||||
### 1. 并发处理
|
||||
- 使用 Tokio 异步运行时
|
||||
- 图片压缩使用 `spawn_blocking` 避免阻塞异步线程
|
||||
- 可配置 Worker 线程数
|
||||
|
||||
```rust
|
||||
// 在独立线程池中执行 CPU 密集型压缩
|
||||
let result = tokio::task::spawn_blocking(move || {
|
||||
compress_image_sync(&data, &config)
|
||||
}).await??;
|
||||
```
|
||||
|
||||
### 2. 内存管理
|
||||
- 流式处理大文件
|
||||
- 限制并发压缩任务数
|
||||
- 压缩完成后立即清理临时文件
|
||||
|
||||
### 3. 缓存策略
|
||||
- Redis 缓存用户会话
|
||||
- 可选:相同图片哈希缓存结果(去重)
|
||||
|
||||
## 安全考虑
|
||||
|
||||
1. **输入验证**:检查文件魔数,不仅依赖扩展名
|
||||
2. **文件大小限制**:防止 DoS
|
||||
3. **像素/维度限制**:防止“图片炸弹”(解码后超大)
|
||||
4. **路径遍历防护**:存储时使用 UUID 命名
|
||||
5. **SQL 注入防护**:使用参数化查询(SQLx 自动处理)
|
||||
6. **XSS 防护**:前端输出转义
|
||||
7. **CSRF 防护**:SameSite Cookie + Token
|
||||
8. **速率限制**:防止滥用
|
||||
9. **默认移除元数据**:避免泄露定位/设备信息(除非用户明确开启保留)
|
||||
|
||||
## 可扩展性
|
||||
|
||||
### 水平扩展
|
||||
```
|
||||
┌─────────────┐
|
||||
│ Load Balancer│
|
||||
└──────┬──────┘
|
||||
┌───────────────┼───────────────┐
|
||||
▼ ▼ ▼
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||
│ API 实例1 │ │ API 实例2 │ │ API 实例3 │
|
||||
└──────────┘ └──────────┘ └──────────┘
|
||||
│ │ │
|
||||
└───────────────┼───────────────┘
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ 共享 PostgreSQL │
|
||||
│ 共享 Redis │
|
||||
│ 共享 S3 存储 │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
### 后续可添加功能
|
||||
- 消息队列(RabbitMQ/NATS)处理异步任务
|
||||
- 分布式任务调度
|
||||
- CDN 加速下载
|
||||
191
docs/billing.md
Normal file
191
docs/billing.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# 计费与用量设计(Stripe + 硬配额)- ImageForge
|
||||
|
||||
目标:形成“套餐/订阅/用量/发票/支付/风控”可落地的闭环,为后续实现提供清晰边界与数据模型依据。
|
||||
|
||||
---
|
||||
|
||||
## 1. 计费模型(已确认)
|
||||
|
||||
已确认口径:
|
||||
- 支付渠道:**Stripe**
|
||||
- 超额策略:**硬配额**(超过当期额度返回 `QUOTA_EXCEEDED` / HTTP `402`)
|
||||
- 配额周期:**按订阅周期**(Stripe `current_period_start` ~ `current_period_end`),不是自然月
|
||||
|
||||
### 1.0 周期定义(统一口径)
|
||||
为避免前后端与账本口径不一致,本项目将“周期”分为三类:
|
||||
- **Pro/Business(付费)**:按 Stripe 订阅周期(`current_period_start` ~ `current_period_end`)。
|
||||
- **Free(未订阅)**:按自然月(UTC+8)重置,用于展示“本月已用/剩余/重置时间”。
|
||||
- **匿名试用**:按自然日(UTC+8)重置,仅用于网站试用(默认每日 10 次)。
|
||||
|
||||
### 1.1 计量单位
|
||||
**compression_unit**:每成功压缩 1 个输出文件计 1 单位。
|
||||
|
||||
规则:
|
||||
- 同一任务中失败的文件不计费。
|
||||
- 同一请求重试若携带相同 `Idempotency-Key`,不重复计费(返回相同结果)。
|
||||
- 输出格式转换(png->webp 等)不额外加价(首期),后续可按“高级功能”计价。
|
||||
|
||||
### 1.2 套餐(Plan)
|
||||
套餐是“功能 + 配额 + 限制”的集合,建议存为可配置(DB 或配置中心)。
|
||||
|
||||
最小字段:
|
||||
- `included_units_per_period`:周期含量(对付费为订阅周期;对 Free 为自然月)。
|
||||
- `max_file_size_mb` / `max_files_per_batch` / `concurrency_limit`
|
||||
- `retention_days`:结果保留期。
|
||||
- `features`:例如 webhook、团队、IP 白名单等开关。
|
||||
|
||||
### 1.3 订阅(Subscription)
|
||||
采用“月度订阅 + 含量(硬配额)”:
|
||||
- 用户在每个订阅周期获得固定含量(`included_units_per_period`)。
|
||||
- 超出含量:直接拒绝(`QUOTA_EXCEEDED` / HTTP `402`)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 用量计算与配额扣减(Metering)
|
||||
|
||||
### 2.1 何时扣减
|
||||
- **成功生成输出**时扣减(以文件粒度)。
|
||||
- 对于异步任务:Worker 完成文件压缩后写入用量事件;API/前端通过任务查询看到实时用量。
|
||||
|
||||
### 2.2 幂等与去重
|
||||
需要两层保护:
|
||||
1) **请求幂等**:`Idempotency-Key` 防止重复创建任务/重复扣减。
|
||||
2) **用量幂等**:每个输出文件生成唯一 `usage_event_id`(或以 `task_file_id` 唯一)确保不会重复入账。
|
||||
|
||||
### 2.3 用量数据结构(建议)
|
||||
- `usage_events`:明细账本(append-only),用于可追溯与对账。
|
||||
- `user_id` / `api_key_id` / `source`(web/api)
|
||||
- `task_id` / `task_file_id`
|
||||
- `units`(通常为 1)
|
||||
- `bytes_in` / `bytes_out` / `format_in` / `format_out`
|
||||
- `occurred_at`
|
||||
- `usage_periods`:按订阅周期聚合(加速配额判断)。
|
||||
- `user_id` + `period_start` + `period_end` 唯一
|
||||
- `used_units`、`bytes_in`、`bytes_out` 等
|
||||
|
||||
配额判断:
|
||||
- 写入 `usage_events` 前先检查本周期 `used_units` 是否已达 `included_units_per_period`(并发场景需原子性:事务/锁/原子计数)。
|
||||
- 失败时返回 `QUOTA_EXCEEDED`,并在响应中附带“已用/上限/重置时间”。
|
||||
|
||||
### 2.4 并发安全的扣减算法(推荐)
|
||||
目标:在高并发下也不能“超扣”。
|
||||
|
||||
推荐做法(Worker 侧每成功文件 1 次扣减):
|
||||
- 先拿到当期 `included_units`(来自 plan)。
|
||||
- 用单条 SQL 做“条件更新”,只有未超额时才 +1:
|
||||
|
||||
```sql
|
||||
UPDATE usage_periods
|
||||
SET used_units = used_units + 1,
|
||||
bytes_in = bytes_in + $1,
|
||||
bytes_out = bytes_out + $2,
|
||||
updated_at = NOW()
|
||||
WHERE user_id = $3
|
||||
AND period_start = $4
|
||||
AND period_end = $5
|
||||
AND used_units + 1 <= $6
|
||||
RETURNING used_units;
|
||||
```
|
||||
|
||||
如果 `RETURNING` 无行(0 行更新),表示额度已耗尽:该文件应标记为 `QUOTA_EXCEEDED`,并丢弃输出(不返回/不落盘/不入账)。
|
||||
|
||||
> 说明:为保持“成功才扣减”的口径,2.4 默认按“成功后尝试扣减”实现;在配额临界点可能出现“先压缩、后发现额度不足”的少量无效计算。若要完全避免,可升级为“预留额度 + 失败回滚”的方案(复杂度更高,后续可做 V1+ 优化)。
|
||||
|
||||
### 2.5 批量任务与配额(硬配额)
|
||||
为尽量减少“上传后才发现额度不足”的体验问题,建议双层策略:
|
||||
- **创建任务前预检**:`remaining_units < files_count` 时直接返回 `402 QUOTA_EXCEEDED`(不创建任务)。
|
||||
- **扣减时最终校验**:写入账本/聚合时仍按 2.4 做原子扣减,防止并发竞态导致超额。
|
||||
|
||||
说明:
|
||||
- 批量任务的计量仍以“成功文件数”为准;失败文件(含 `QUOTA_EXCEEDED`)不计费。
|
||||
- 前端建议在上传前调用 `GET /billing/usage`(登录)或读取配额头(API)做本地提示/拦截。
|
||||
|
||||
---
|
||||
|
||||
## 3. 订阅生命周期(状态机)
|
||||
|
||||
### 3.1 状态
|
||||
建议:
|
||||
- `trialing`:试用中(可选)
|
||||
- `active`:正常
|
||||
- `past_due`:欠费(支付失败)
|
||||
- `paused`:暂停(可选)
|
||||
- `canceled`:已取消
|
||||
- `incomplete`:创建未完成(例如支付页未完成)
|
||||
|
||||
### 3.2 关键动作
|
||||
- 升级/降级套餐:下周期生效(首期建议),避免按天结算复杂度。
|
||||
- 取消订阅:`cancel_at_period_end=true`,到期生效;到期后自动降级到 Free(不续费)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 支付集成(Stripe)
|
||||
|
||||
### 4.1 核心对象映射(建议)
|
||||
- `users.billing_customer_id` ↔ Stripe `customer.id`
|
||||
- `plans` ↔ Stripe `product/price`(建议每个 plan 对应一个 Stripe `price`)
|
||||
- `subscriptions.provider_subscription_id` ↔ Stripe `subscription.id`
|
||||
- `invoices.provider_invoice_id` ↔ Stripe `invoice.id`
|
||||
- `payments.provider_payment_id` ↔ Stripe `payment_intent.id`(或 charge id,按实现选)
|
||||
|
||||
### 4.2 Checkout / Portal
|
||||
- Checkout:后端创建 Stripe Checkout Session,前端跳转 `checkout_url`。
|
||||
- Portal:后端创建 Stripe Billing Portal Session,前端跳转管理支付方式/取消订阅。
|
||||
|
||||
### 4.3 Stripe Webhook(商用必须)
|
||||
要求:
|
||||
- **验签**:使用 `STRIPE_WEBHOOK_SECRET` 校验 `Stripe-Signature`。
|
||||
- **事件幂等**:按 `provider_event_id` 去重(落库 `webhook_events`)。
|
||||
- **乱序容忍**:以 Stripe 事件时间 + 当前 DB 状态做“只前进不回退”更新。
|
||||
- **可重放**:保存原始 payload(脱敏)用于排查。
|
||||
|
||||
建议首期处理的事件(示例):
|
||||
- `checkout.session.completed`
|
||||
- `customer.subscription.created`
|
||||
- `customer.subscription.updated`
|
||||
- `customer.subscription.deleted`
|
||||
- `invoice.paid`
|
||||
- `invoice.payment_failed`
|
||||
|
||||
---
|
||||
|
||||
## 5. 发票与对账
|
||||
|
||||
### 5.1 发票(Invoice)
|
||||
发票用于:
|
||||
- 向用户展示本期费用、税务信息(后续)、支付状态。
|
||||
- 与支付记录/订阅周期关联。
|
||||
|
||||
最小字段:
|
||||
- `invoice_number`(展示用)
|
||||
- `currency`、`total_amount`
|
||||
- `status`:draft/open/paid/void/uncollectible(可简化)
|
||||
- `period_start` / `period_end`
|
||||
- `paid_at`
|
||||
|
||||
### 5.2 对账原则
|
||||
- 以 `usage_events` 为最终真相(source of truth)。
|
||||
- 以 `invoice` 为结算结果(面向用户)。
|
||||
- 管理后台可以按用户/周期拉取用量明细,支持导出 CSV。
|
||||
|
||||
---
|
||||
|
||||
## 6. 风控策略(与计费强相关)
|
||||
|
||||
### 6.1 防盗刷
|
||||
- API Key 支持:权限范围、速率限制、(可选)IP 白名单、(可选)域名白名单(仅对 Web 场景有效)。
|
||||
- 异常检测:短时间内用量突增、来自异常国家/ASN、重复失败率高。
|
||||
|
||||
### 6.2 欠费与滥用处理
|
||||
- 欠费(past_due):建议限制创建新任务,并在 UI 引导用户去 Portal 完成支付。
|
||||
- 严重滥用:冻结用户/API Key,并记录审计日志。
|
||||
|
||||
---
|
||||
|
||||
## 7. 需要在文档中保持一致的“最终口径”
|
||||
|
||||
上线前必须定稿并在 `docs/api.md`、`docs/database.md`、`docs/ui.md` 同步:
|
||||
- 套餐表(默认配额值)。
|
||||
- 配额刷新周期(按订阅周期)。
|
||||
- 超额策略(硬配额 / HTTP 402)。
|
||||
- 匿名试用是否开放、开放到什么程度。
|
||||
46
docs/confirm.md
Normal file
46
docs/confirm.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# 开工前确认清单 - ImageForge
|
||||
|
||||
目的:把“产品口径/计费口径/关键体验”一次性定稿,避免边做边改导致返工。
|
||||
|
||||
---
|
||||
|
||||
## 1. 已确认(按你的要求已写入各文档)
|
||||
|
||||
- 支付:Stripe
|
||||
- 超额策略:硬配额(超出返回 `402 QUOTA_EXCEEDED`)
|
||||
- 订阅周期:按 Stripe 订阅周期(`current_period_start` ~ `current_period_end`),不是自然月
|
||||
- 匿名试用:支持;每日 10 次(以成功压缩文件数计)
|
||||
- Free 套餐:不开放对外 API(仅 Pro/Business 可创建 API Key)
|
||||
- 邮件:注册邮箱验证 + 密码重置(SMTP)
|
||||
- 默认语言:中文
|
||||
|
||||
---
|
||||
|
||||
## 2. 需要你确认的默认口径(我已在文档里按“建议默认值”写死)
|
||||
|
||||
1) **Free 配额周期**
|
||||
- 当前写法:Free(未订阅)按自然月(UTC+8)重置;Pro/Business 按订阅周期。
|
||||
|
||||
2) **匿名试用的“日界”与识别**
|
||||
- 当前写法:匿名试用按自然日(UTC+8)00:00 重置;采用 Cookie + IP 双限制。
|
||||
|
||||
3) **批量任务遇到额度不足时的行为**
|
||||
- 当前写法:`POST /compress/batch` 若本周期剩余单位不足以覆盖上传文件数,直接返回 `402`,不创建任务。
|
||||
|
||||
4) **默认套餐参数(可改)**
|
||||
- Free:500 / 月,5MB 单文件,10/批量,保留 24h
|
||||
- Pro:10,000 / 订阅周期,20MB 单文件,50/批量,保留 7 天
|
||||
- Business:100,000+ / 订阅周期,50MB 单文件,200/批量,保留 30 天
|
||||
|
||||
5) **邮箱未验证是否禁止压缩**
|
||||
- 当前文档口径:注册用户未验证邮箱时,调用压缩接口返回 `EMAIL_NOT_VERIFIED`(匿名试用不受影响)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 你只需要回复我 5 个点(同意/修改)
|
||||
|
||||
- Free 配额周期:按自然月(UTC+8)是否 OK?
|
||||
- 匿名试用:按自然日(UTC+8)是否 OK?是否要“仅 Cookie”还是“Cookie + IP 双限制”?
|
||||
- 批量额度不足:是否坚持“直接 402 不建任务”,还是允许“部分成功/部分失败”?
|
||||
- 套餐默认值:Free/Pro/Business 的配额、大小、保留期是否调整?
|
||||
- 邮箱验证:是否必须验证后才能压缩?
|
||||
583
docs/database.md
Normal file
583
docs/database.md
Normal file
@@ -0,0 +1,583 @@
|
||||
# 数据库设计(PostgreSQL + Redis)- ImageForge
|
||||
|
||||
目标:支撑“网站 + 对外 API + 计费”的核心数据闭环,并为后续实现提供可迁移的表结构参考。
|
||||
|
||||
相关口径:
|
||||
- 产品范围:`docs/prd.md`
|
||||
- 计费与用量:`docs/billing.md`
|
||||
- 安全:`docs/security.md`
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
### 1.1 技术选型
|
||||
- **PostgreSQL**:系统真相来源(用户/任务/用量账本/订阅/发票/审计)。
|
||||
- **Redis**:缓存(会话、API Key 缓存)、限流计数、队列(Streams)与进度推送(PubSub 可选)。
|
||||
|
||||
### 1.2 设计原则
|
||||
- **用量可追溯**:以 `usage_events`(明细账本)作为最终真相,可对账/可追责。
|
||||
- **幂等可落地**:`idempotency_keys` 保障重试不重复扣费/不重复建任务。
|
||||
- **多租户可扩展**:后续可加 team/org,不影响现有表的主键与关系。
|
||||
- **安全默认**:不存明文 API Key;审计日志不写入敏感明文。
|
||||
|
||||
---
|
||||
|
||||
## 2. PostgreSQL 扩展与类型
|
||||
|
||||
### 2.1 UUID 生成
|
||||
本设计默认使用 `gen_random_uuid()`,需要启用 `pgcrypto`:
|
||||
```sql
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||
```
|
||||
|
||||
> 注意:如果改用 `uuid-ossp`,则应统一改为 `uuid_generate_v4()`,避免文档与实现不一致。
|
||||
|
||||
### 2.2 枚举类型(建议)
|
||||
```sql
|
||||
CREATE TYPE user_role AS ENUM ('user', 'admin');
|
||||
CREATE TYPE task_status AS ENUM ('pending', 'processing', 'completed', 'failed', 'cancelled');
|
||||
CREATE TYPE file_status AS ENUM ('pending', 'processing', 'completed', 'failed');
|
||||
CREATE TYPE compression_level AS ENUM ('high', 'medium', 'low');
|
||||
CREATE TYPE task_source AS ENUM ('web', 'api');
|
||||
|
||||
CREATE TYPE subscription_status AS ENUM ('trialing', 'active', 'past_due', 'paused', 'canceled', 'incomplete');
|
||||
CREATE TYPE invoice_status AS ENUM ('draft', 'open', 'paid', 'void', 'uncollectible');
|
||||
CREATE TYPE payment_status AS ENUM ('pending', 'succeeded', 'failed', 'refunded');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. ER 图(核心关系)
|
||||
|
||||
```
|
||||
┌──────────────┐ ┌──────────────┐
|
||||
│ plans │◄───────│ subscriptions│
|
||||
└──────┬───────┘ └──────┬───────┘
|
||||
│ │
|
||||
│ ▼
|
||||
│ ┌──────────────┐
|
||||
│ │ invoices │◄────┐
|
||||
│ └──────┬───────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────┐ │
|
||||
│ │ payments │ │
|
||||
│ └──────────────┘ │
|
||||
│ │
|
||||
▼ │
|
||||
┌──────────────┐ ┌──────────────┐ │
|
||||
│ users │──────►│ api_keys │ │
|
||||
└──────┬───────┘ └──────────────┘ │
|
||||
│ │
|
||||
▼ │
|
||||
┌──────────────┐ ┌──────────────┐ │
|
||||
│ tasks │──────►│ task_files │ │
|
||||
└──────┬───────┘ └──────┬───────┘ │
|
||||
│ │ │
|
||||
▼ ▼ │
|
||||
┌──────────────┐ ┌──────────────┐ │
|
||||
│ usage_events │◄──────│ idempotency │ │
|
||||
└──────┬───────┘ └──────────────┘ │
|
||||
│ │
|
||||
▼ │
|
||||
┌──────────────┐ │
|
||||
│ usage_periods│ │
|
||||
└──────────────┘ │
|
||||
│
|
||||
┌───────────────────────────────▼┐
|
||||
│ webhook_events │
|
||||
└────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 表结构(建议)
|
||||
|
||||
> 以下 SQL 用于“设计说明”;真正落地时建议拆分迁移文件(core/billing/metering)。
|
||||
|
||||
### 4.1 users - 用户
|
||||
```sql
|
||||
CREATE TABLE users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
email VARCHAR(255) NOT NULL UNIQUE,
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
role user_role NOT NULL DEFAULT 'user',
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
|
||||
-- 邮箱验证
|
||||
email_verified_at TIMESTAMPTZ,
|
||||
|
||||
-- 计费侧映射(可选,取决于支付渠道)
|
||||
billing_customer_id VARCHAR(200),
|
||||
|
||||
-- 覆盖限制(运营用;NULL 表示用套餐默认)
|
||||
rate_limit_override INTEGER,
|
||||
storage_limit_mb INTEGER,
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_users_role ON users(role);
|
||||
CREATE INDEX idx_users_created_at ON users(created_at);
|
||||
CREATE INDEX idx_users_email_verified ON users(email_verified_at) WHERE email_verified_at IS NULL;
|
||||
```
|
||||
|
||||
### 4.2 api_keys - API Key
|
||||
```sql
|
||||
CREATE TABLE api_keys (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
|
||||
name VARCHAR(100) NOT NULL,
|
||||
key_prefix VARCHAR(20) NOT NULL, -- 展示/索引用(如 "if_live_abcd")
|
||||
key_hash VARCHAR(255) NOT NULL, -- 推荐:HMAC-SHA256(full_key, API_KEY_PEPPER)
|
||||
|
||||
permissions JSONB NOT NULL DEFAULT '["compress"]',
|
||||
rate_limit INTEGER NOT NULL DEFAULT 100,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
|
||||
last_used_at TIMESTAMPTZ,
|
||||
last_used_ip INET,
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_api_keys_user_id ON api_keys(user_id);
|
||||
CREATE UNIQUE INDEX idx_api_keys_prefix ON api_keys(key_prefix);
|
||||
CREATE INDEX idx_api_keys_is_active ON api_keys(is_active);
|
||||
```
|
||||
|
||||
### 4.3 email_verifications - 邮箱验证
|
||||
```sql
|
||||
CREATE TABLE email_verifications (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
token_hash VARCHAR(64) NOT NULL, -- SHA256(token),不存明文
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
verified_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX idx_email_verifications_token ON email_verifications(token_hash);
|
||||
CREATE INDEX idx_email_verifications_user_id ON email_verifications(user_id);
|
||||
CREATE INDEX idx_email_verifications_expires ON email_verifications(expires_at) WHERE verified_at IS NULL;
|
||||
```
|
||||
|
||||
### 4.4 password_resets - 密码重置
|
||||
```sql
|
||||
CREATE TABLE password_resets (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
token_hash VARCHAR(64) NOT NULL, -- SHA256(token),不存明文
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
used_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX idx_password_resets_token ON password_resets(token_hash);
|
||||
CREATE INDEX idx_password_resets_user_id ON password_resets(user_id);
|
||||
CREATE INDEX idx_password_resets_expires ON password_resets(expires_at) WHERE used_at IS NULL;
|
||||
```
|
||||
|
||||
### 4.5 plans - 套餐
|
||||
```sql
|
||||
CREATE TABLE plans (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
code VARCHAR(50) NOT NULL UNIQUE, -- 展示/运营用(如 "free", "pro_monthly")
|
||||
name VARCHAR(100) NOT NULL,
|
||||
description TEXT,
|
||||
|
||||
-- Stripe 映射(对接 Checkout 时使用)
|
||||
stripe_product_id VARCHAR(200),
|
||||
stripe_price_id VARCHAR(200),
|
||||
|
||||
currency VARCHAR(10) NOT NULL DEFAULT 'CNY',
|
||||
amount_cents INTEGER NOT NULL DEFAULT 0,
|
||||
interval VARCHAR(20) NOT NULL DEFAULT 'monthly', -- 可后续改 ENUM
|
||||
|
||||
included_units_per_period INTEGER NOT NULL,
|
||||
max_file_size_mb INTEGER NOT NULL,
|
||||
max_files_per_batch INTEGER NOT NULL,
|
||||
concurrency_limit INTEGER NOT NULL,
|
||||
retention_days INTEGER NOT NULL,
|
||||
|
||||
features JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_plans_is_active ON plans(is_active);
|
||||
```
|
||||
|
||||
### 4.4 subscriptions - 订阅
|
||||
```sql
|
||||
CREATE TABLE subscriptions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
plan_id UUID NOT NULL REFERENCES plans(id),
|
||||
|
||||
status subscription_status NOT NULL DEFAULT 'incomplete',
|
||||
current_period_start TIMESTAMPTZ NOT NULL,
|
||||
current_period_end TIMESTAMPTZ NOT NULL,
|
||||
|
||||
cancel_at_period_end BOOLEAN NOT NULL DEFAULT false,
|
||||
canceled_at TIMESTAMPTZ,
|
||||
|
||||
provider VARCHAR(20) NOT NULL DEFAULT 'none', -- none/stripe
|
||||
provider_customer_id VARCHAR(200),
|
||||
provider_subscription_id VARCHAR(200),
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_subscriptions_user_id ON subscriptions(user_id);
|
||||
CREATE INDEX idx_subscriptions_status ON subscriptions(status);
|
||||
```
|
||||
|
||||
### 4.5 invoices - 发票/账单
|
||||
```sql
|
||||
CREATE TABLE invoices (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
subscription_id UUID REFERENCES subscriptions(id) ON DELETE SET NULL,
|
||||
|
||||
invoice_number VARCHAR(50) NOT NULL UNIQUE,
|
||||
status invoice_status NOT NULL DEFAULT 'open',
|
||||
currency VARCHAR(10) NOT NULL DEFAULT 'CNY',
|
||||
total_amount_cents INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
period_start TIMESTAMPTZ,
|
||||
period_end TIMESTAMPTZ,
|
||||
|
||||
provider VARCHAR(20) NOT NULL DEFAULT 'none',
|
||||
provider_invoice_id VARCHAR(200),
|
||||
hosted_invoice_url TEXT,
|
||||
pdf_url TEXT,
|
||||
|
||||
due_at TIMESTAMPTZ,
|
||||
paid_at TIMESTAMPTZ,
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_invoices_user_id ON invoices(user_id);
|
||||
CREATE INDEX idx_invoices_status ON invoices(status);
|
||||
```
|
||||
|
||||
### 4.6 payments - 支付记录
|
||||
```sql
|
||||
CREATE TABLE payments (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
invoice_id UUID REFERENCES invoices(id) ON DELETE SET NULL,
|
||||
|
||||
provider VARCHAR(20) NOT NULL DEFAULT 'none',
|
||||
provider_payment_id VARCHAR(200),
|
||||
|
||||
status payment_status NOT NULL DEFAULT 'pending',
|
||||
currency VARCHAR(10) NOT NULL DEFAULT 'CNY',
|
||||
amount_cents INTEGER NOT NULL DEFAULT 0,
|
||||
paid_at TIMESTAMPTZ,
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_payments_user_id ON payments(user_id);
|
||||
CREATE INDEX idx_payments_status ON payments(status);
|
||||
```
|
||||
|
||||
### 4.7 webhook_events - Webhook 事件(支付回调)
|
||||
```sql
|
||||
CREATE TABLE webhook_events (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
provider VARCHAR(20) NOT NULL,
|
||||
provider_event_id VARCHAR(200) NOT NULL,
|
||||
event_type VARCHAR(200) NOT NULL,
|
||||
payload JSONB NOT NULL,
|
||||
received_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
processed_at TIMESTAMPTZ,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'received', -- received/processed/failed
|
||||
error_message TEXT
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX idx_webhook_events_unique ON webhook_events(provider, provider_event_id);
|
||||
CREATE INDEX idx_webhook_events_status ON webhook_events(status);
|
||||
```
|
||||
|
||||
### 4.8 tasks - 压缩任务
|
||||
```sql
|
||||
CREATE TABLE tasks (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
user_id UUID REFERENCES users(id) ON DELETE SET NULL, -- 游客为空
|
||||
session_id VARCHAR(100), -- 游客会话
|
||||
api_key_id UUID REFERENCES api_keys(id) ON DELETE SET NULL, -- API 调用可记录
|
||||
|
||||
source task_source NOT NULL DEFAULT 'web',
|
||||
status task_status NOT NULL DEFAULT 'pending',
|
||||
|
||||
compression_level compression_level NOT NULL DEFAULT 'medium',
|
||||
output_format VARCHAR(10), -- NULL 表示保持原格式
|
||||
max_width INTEGER,
|
||||
max_height INTEGER,
|
||||
preserve_metadata BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
total_files INTEGER NOT NULL DEFAULT 0,
|
||||
completed_files INTEGER NOT NULL DEFAULT 0,
|
||||
failed_files INTEGER NOT NULL DEFAULT 0,
|
||||
total_original_size BIGINT NOT NULL DEFAULT 0,
|
||||
total_compressed_size BIGINT NOT NULL DEFAULT 0,
|
||||
|
||||
error_message TEXT,
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
started_at TIMESTAMPTZ,
|
||||
completed_at TIMESTAMPTZ,
|
||||
|
||||
-- 到期清理:匿名可默认 24h;登录用户应由应用按套餐写入更长 retention
|
||||
expires_at TIMESTAMPTZ NOT NULL DEFAULT (NOW() + INTERVAL '24 hours')
|
||||
);
|
||||
|
||||
CREATE INDEX idx_tasks_user_id ON tasks(user_id);
|
||||
CREATE INDEX idx_tasks_session_id ON tasks(session_id);
|
||||
CREATE INDEX idx_tasks_status ON tasks(status);
|
||||
CREATE INDEX idx_tasks_created_at ON tasks(created_at);
|
||||
CREATE INDEX idx_tasks_expires_at ON tasks(expires_at);
|
||||
```
|
||||
|
||||
### 4.9 task_files - 任务文件
|
||||
```sql
|
||||
CREATE TABLE task_files (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
task_id UUID NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
||||
|
||||
original_name VARCHAR(255) NOT NULL,
|
||||
original_format VARCHAR(10) NOT NULL,
|
||||
output_format VARCHAR(10) NOT NULL,
|
||||
|
||||
original_size BIGINT NOT NULL,
|
||||
compressed_size BIGINT,
|
||||
saved_percent DECIMAL(6, 2),
|
||||
|
||||
storage_path VARCHAR(500), -- S3 key 或本地路径
|
||||
status file_status NOT NULL DEFAULT 'pending',
|
||||
error_message TEXT,
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
completed_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX idx_task_files_task_id ON task_files(task_id);
|
||||
CREATE INDEX idx_task_files_status ON task_files(status);
|
||||
```
|
||||
|
||||
### 4.10 idempotency_keys - 幂等记录
|
||||
```sql
|
||||
CREATE TABLE idempotency_keys (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
||||
api_key_id UUID REFERENCES api_keys(id) ON DELETE CASCADE,
|
||||
|
||||
idempotency_key VARCHAR(128) NOT NULL,
|
||||
request_hash VARCHAR(64) NOT NULL,
|
||||
|
||||
response_status INTEGER NOT NULL,
|
||||
response_body JSONB, -- JSON 接口可存;二进制接口建议存“元信息 + 对象指针”
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
expires_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX idx_idempotency_user_key ON idempotency_keys(user_id, idempotency_key) WHERE user_id IS NOT NULL;
|
||||
CREATE UNIQUE INDEX idx_idempotency_api_key_key ON idempotency_keys(api_key_id, idempotency_key) WHERE api_key_id IS NOT NULL;
|
||||
CREATE INDEX idx_idempotency_expires_at ON idempotency_keys(expires_at);
|
||||
```
|
||||
|
||||
### 4.11 usage_events - 用量账本(明细)
|
||||
```sql
|
||||
CREATE TABLE usage_events (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
|
||||
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
api_key_id UUID REFERENCES api_keys(id) ON DELETE SET NULL,
|
||||
source task_source NOT NULL,
|
||||
|
||||
task_id UUID REFERENCES tasks(id) ON DELETE SET NULL,
|
||||
task_file_id UUID REFERENCES task_files(id) ON DELETE SET NULL,
|
||||
|
||||
units INTEGER NOT NULL DEFAULT 1,
|
||||
bytes_in BIGINT NOT NULL DEFAULT 0,
|
||||
bytes_out BIGINT NOT NULL DEFAULT 0,
|
||||
format_in VARCHAR(10),
|
||||
format_out VARCHAR(10),
|
||||
|
||||
occurred_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX idx_usage_events_task_file_unique ON usage_events(task_file_id) WHERE task_file_id IS NOT NULL;
|
||||
CREATE INDEX idx_usage_events_user_time ON usage_events(user_id, occurred_at);
|
||||
CREATE INDEX idx_usage_events_api_key_time ON usage_events(api_key_id, occurred_at);
|
||||
```
|
||||
|
||||
### 4.12 usage_periods - 用量聚合(按订阅周期)
|
||||
```sql
|
||||
CREATE TABLE usage_periods (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
subscription_id UUID REFERENCES subscriptions(id) ON DELETE SET NULL,
|
||||
|
||||
period_start TIMESTAMPTZ NOT NULL,
|
||||
period_end TIMESTAMPTZ NOT NULL,
|
||||
|
||||
used_units INTEGER NOT NULL DEFAULT 0,
|
||||
bytes_in BIGINT NOT NULL DEFAULT 0,
|
||||
bytes_out BIGINT NOT NULL DEFAULT 0,
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
UNIQUE(user_id, period_start, period_end)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_usage_periods_user_period ON usage_periods(user_id, period_start);
|
||||
```
|
||||
|
||||
### 4.13 system_config - 系统配置
|
||||
```sql
|
||||
CREATE TABLE system_config (
|
||||
key VARCHAR(100) PRIMARY KEY,
|
||||
value JSONB NOT NULL,
|
||||
description TEXT,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_by UUID REFERENCES users(id)
|
||||
);
|
||||
```
|
||||
|
||||
### 4.14 audit_logs - 审计日志
|
||||
```sql
|
||||
CREATE TABLE audit_logs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
action VARCHAR(50) NOT NULL, -- login/compress/config_change/billing/...
|
||||
resource_type VARCHAR(50),
|
||||
resource_id UUID,
|
||||
details JSONB,
|
||||
ip_address INET,
|
||||
user_agent TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_audit_logs_user_id ON audit_logs(user_id);
|
||||
CREATE INDEX idx_audit_logs_action ON audit_logs(action);
|
||||
CREATE INDEX idx_audit_logs_created_at ON audit_logs(created_at);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Redis 数据结构(建议)
|
||||
|
||||
### 5.1 会话(匿名/网站)
|
||||
```
|
||||
Key: session:{session_id}
|
||||
Value: JSON { user_id, role, created_at, expires_at }
|
||||
TTL: 7 days
|
||||
```
|
||||
|
||||
### 5.2 限流计数
|
||||
```
|
||||
Key: rate_limit:{scope}:{id}:{minute}
|
||||
Value: request_count
|
||||
TTL: 60 seconds
|
||||
```
|
||||
|
||||
### 5.2.1 匿名试用每日额度(硬限制)
|
||||
```
|
||||
Key: anon_quota:{session_id}:{yyyy-mm-dd}
|
||||
Value: used_units
|
||||
TTL: 48 hours
|
||||
|
||||
# yyyy-mm-dd:自然日(UTC+8),次日 00:00 重置
|
||||
|
||||
# 必须:叠加 IP 维度,降低刷 session 的风险(两者任一超出都拒绝)
|
||||
Key: anon_quota_ip:{ip}:{yyyy-mm-dd}
|
||||
Value: used_units
|
||||
TTL: 48 hours
|
||||
```
|
||||
|
||||
### 5.3 队列(Redis Streams)
|
||||
```
|
||||
Stream: stream:compress_jobs
|
||||
Group: compress_workers
|
||||
Message fields: { task_id, priority, created_at }
|
||||
```
|
||||
|
||||
### 5.4 任务进度(可选)
|
||||
```
|
||||
PubSub: pubsub:task:{task_id}
|
||||
Message: JSON { progress, completed_files, current_file }
|
||||
```
|
||||
|
||||
### 5.5 API Key 缓存(可选)
|
||||
```
|
||||
Key: api_key:{key_prefix}
|
||||
Value: JSON { api_key_id, user_id, permissions, rate_limit, ... }
|
||||
TTL: 5 minutes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 数据清理策略(必须)
|
||||
|
||||
- **过期任务**:按 `tasks.expires_at` 清理任务/文件/对象存储。
|
||||
- **幂等记录**:按 `idempotency_keys.expires_at` 清理。
|
||||
- **邮箱验证 Token**:按 `email_verifications.expires_at` 清理已过期且未验证的记录。
|
||||
- **密码重置 Token**:按 `password_resets.expires_at` 清理已过期或已使用的记录。
|
||||
- **Webhook 事件**:保留 30~90 天(便于排查),过期清理或归档。
|
||||
- **用量账本**:`usage_events` 建议长期保留(对账),必要时做分区(按月/按季度)。
|
||||
|
||||
示例:
|
||||
```sql
|
||||
-- 清理过期任务
|
||||
DELETE FROM tasks WHERE expires_at < NOW();
|
||||
|
||||
-- 清理幂等记录
|
||||
DELETE FROM idempotency_keys WHERE expires_at < NOW();
|
||||
|
||||
-- 清理邮箱验证 Token(保留已验证的,清理过期未验证的)
|
||||
DELETE FROM email_verifications WHERE expires_at < NOW() AND verified_at IS NULL;
|
||||
|
||||
-- 清理密码重置 Token(保留 7 天内的记录用于审计)
|
||||
DELETE FROM password_resets WHERE expires_at < NOW() - INTERVAL '7 days';
|
||||
|
||||
-- 清理 Webhook 事件
|
||||
DELETE FROM webhook_events WHERE received_at < NOW() - INTERVAL '90 days';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 初始化数据(建议)
|
||||
|
||||
### 7.1 默认套餐
|
||||
```sql
|
||||
INSERT INTO plans (code, name, stripe_price_id, currency, amount_cents, interval, included_units_per_period, max_file_size_mb, max_files_per_batch, concurrency_limit, retention_days, features)
|
||||
VALUES
|
||||
('free', 'Free', NULL, 'CNY', 0, 'monthly', 500, 5, 10, 2, 1, '{"api": false, "webhook": false}'),
|
||||
('pro_monthly', 'Pro(月付)', 'price_xxx_pro_monthly', 'CNY', 1999, 'monthly', 10000, 20, 50, 8, 7, '{"api": true, "webhook": true}'),
|
||||
('business_monthly', 'Business(月付)', 'price_xxx_business_monthly', 'CNY', 9999, 'monthly', 100000, 50, 200, 32, 30, '{"api": true, "webhook": true, "ip_allowlist": true}');
|
||||
```
|
||||
|
||||
### 7.2 默认系统配置
|
||||
```sql
|
||||
INSERT INTO system_config (key, value, description) VALUES
|
||||
('features', '{"registration_enabled": true, "api_key_enabled": true, "anonymous_upload_enabled": true, "email_verification_required": true}', '功能开关'),
|
||||
('rate_limits', '{"anonymous_per_minute": 10, "anonymous_units_per_day": 10, "user_per_minute": 60, "api_key_per_minute": 100}', '速率限制默认值'),
|
||||
('file_limits', '{"max_image_pixels": 40000000}', '图片安全限制(像素上限等)'),
|
||||
('mail', '{"enabled": true, "provider": "custom", "from": "noreply@example.com", "from_name": "ImageForge"}', '邮件服务配置(密码加密存储)');
|
||||
```
|
||||
711
docs/deployment.md
Normal file
711
docs/deployment.md
Normal file
@@ -0,0 +1,711 @@
|
||||
# 部署指南
|
||||
|
||||
## 环境准备
|
||||
|
||||
### 系统要求
|
||||
|
||||
- Linux (Ubuntu 22.04+ / Debian 12+ 推荐)
|
||||
- 2+ CPU 核心(启用独立 Worker 建议 4+)
|
||||
- 4GB+ 内存
|
||||
- 50GB+ 磁盘空间
|
||||
|
||||
### 依赖安装
|
||||
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
sudo apt update
|
||||
sudo apt install -y \
|
||||
build-essential \
|
||||
pkg-config \
|
||||
libssl-dev \
|
||||
libpq-dev \
|
||||
cmake \
|
||||
nasm \
|
||||
libjpeg-dev \
|
||||
libpng-dev \
|
||||
libwebp-dev
|
||||
|
||||
# 安装 Rust
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
source ~/.cargo/env
|
||||
|
||||
# 初始化数据库会用到 psql(建议安装 PostgreSQL client)
|
||||
sudo apt install -y postgresql-client
|
||||
|
||||
# 安装 Node.js (前端构建)
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
||||
sudo apt install -y nodejs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 本地开发
|
||||
|
||||
### 1. 启动数据库服务
|
||||
|
||||
```bash
|
||||
# 使用 Docker Compose 启动 PostgreSQL 和 Redis
|
||||
docker-compose -f docker/docker-compose.dev.yml up -d
|
||||
```
|
||||
|
||||
`docker/docker-compose.dev.yml`:
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_USER: imageforge
|
||||
POSTGRES_PASSWORD: devpassword
|
||||
POSTGRES_DB: imageforge
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
|
||||
# 可选:MinIO(S3 兼容,本地开发更接近生产)
|
||||
# minio:
|
||||
# image: minio/minio:RELEASE.2024-01-28T20-20-01Z
|
||||
# command: server /data --console-address ":9001"
|
||||
# environment:
|
||||
# MINIO_ROOT_USER: minioadmin
|
||||
# MINIO_ROOT_PASSWORD: minioadmin
|
||||
# ports:
|
||||
# - "9000:9000"
|
||||
# - "9001:9001"
|
||||
# volumes:
|
||||
# - minio_data:/data
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
minio_data:
|
||||
```
|
||||
|
||||
### 2. 配置环境变量
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
`.env.example`:
|
||||
```bash
|
||||
# 运行模式:建议将 API 与 Worker 分开运行
|
||||
IMAGEFORGE_ROLE=api # api | worker
|
||||
|
||||
# 服务配置
|
||||
HOST=0.0.0.0
|
||||
PORT=8080
|
||||
PUBLIC_BASE_URL=http://localhost:8080
|
||||
RUST_LOG=info,imageforge=debug
|
||||
|
||||
# 数据库
|
||||
DATABASE_URL=postgres://imageforge:devpassword@localhost:5432/imageforge
|
||||
|
||||
# Redis
|
||||
REDIS_URL=redis://localhost:6379
|
||||
|
||||
# JWT(网站/管理后台)
|
||||
JWT_SECRET=your-super-secret-key-change-in-production
|
||||
JWT_EXPIRY_HOURS=168
|
||||
|
||||
# API Key
|
||||
API_KEY_PEPPER=please-change-this-in-production
|
||||
|
||||
# 存储(生产建议 S3/MinIO + 预签名 URL)
|
||||
STORAGE_TYPE=local # local | s3
|
||||
STORAGE_PATH=./uploads
|
||||
# 预签名下载链接过期(分钟)
|
||||
SIGNED_URL_TTL_MINUTES=60
|
||||
|
||||
# S3 配置(如果使用 S3/MinIO)
|
||||
# S3_ENDPOINT=http://localhost:9000
|
||||
# S3_BUCKET=your-bucket
|
||||
# S3_REGION=us-east-1
|
||||
# S3_ACCESS_KEY=xxx
|
||||
# S3_SECRET_KEY=xxx
|
||||
|
||||
# 计费(已确认:Stripe)
|
||||
BILLING_PROVIDER=stripe
|
||||
STRIPE_SECRET_KEY=sk_test_xxx
|
||||
STRIPE_WEBHOOK_SECRET=whsec_xxx
|
||||
|
||||
# 限制(默认值;最终以套餐/用户覆盖为准)
|
||||
ALLOW_ANONYMOUS_UPLOAD=true
|
||||
ANON_MAX_FILE_SIZE_MB=5
|
||||
ANON_MAX_FILES_PER_BATCH=5
|
||||
ANON_DAILY_UNITS=10
|
||||
MAX_IMAGE_PIXELS=40000000
|
||||
IDEMPOTENCY_TTL_HOURS=24
|
||||
|
||||
# 结果保留(匿名默认;登录用户按套餐 retention_days)
|
||||
ANON_RETENTION_HOURS=24
|
||||
|
||||
# 管理员初始账户
|
||||
ADMIN_EMAIL=admin@example.com
|
||||
ADMIN_PASSWORD=changeme123
|
||||
```
|
||||
|
||||
### 3. 初始化数据库
|
||||
|
||||
```bash
|
||||
# 运行首期迁移(可重复执行)
|
||||
psql "$DATABASE_URL" -f migrations/001_init.sql
|
||||
```
|
||||
|
||||
### 4. 启动开发服务器
|
||||
|
||||
```bash
|
||||
# 后端 API (热重载)
|
||||
cargo install cargo-watch
|
||||
IMAGEFORGE_ROLE=api cargo watch -x run
|
||||
|
||||
# 后端 Worker(另一个终端,处理异步/批量任务)
|
||||
IMAGEFORGE_ROLE=worker cargo watch -x run
|
||||
|
||||
# 前端 (另一个终端)
|
||||
cd frontend
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 5. Stripe Webhook 本地调试(可选)
|
||||
|
||||
本地调试 Stripe 订阅/支付状态,通常需要将 Stripe Webhook 转发到本机:
|
||||
|
||||
```bash
|
||||
# 1) 安装并登录 Stripe CLI(按官方文档)
|
||||
# 2) 监听并转发到你的后端回调地址
|
||||
stripe listen --forward-to http://localhost:8080/api/v1/webhooks/stripe
|
||||
|
||||
# CLI 会输出一个 whsec_...,写入 .env 的 STRIPE_WEBHOOK_SECRET
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 生产部署
|
||||
|
||||
> 注意:以下内容为生产部署模板示例;仓库当前首期仅提供开发用 `docker/docker-compose.dev.yml`,生产 compose/Dockerfile/nginx/k8s 等可在开工阶段按需落地并调整。
|
||||
|
||||
### 方案一:Docker Compose(推荐小规模)
|
||||
|
||||
`docker/docker-compose.prod.yml`:
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
api:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: docker/Dockerfile
|
||||
environment:
|
||||
- IMAGEFORGE_ROLE=api
|
||||
- BILLING_PROVIDER=stripe
|
||||
- PUBLIC_BASE_URL=https://your-domain.com
|
||||
- STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY}
|
||||
- STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET}
|
||||
- DATABASE_URL=postgres://imageforge:${DB_PASSWORD}@postgres:5432/imageforge
|
||||
- REDIS_URL=redis://redis:6379
|
||||
- JWT_SECRET=${JWT_SECRET}
|
||||
- STORAGE_TYPE=local
|
||||
- STORAGE_PATH=/app/uploads
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- uploads:/app/uploads
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
restart: unless-stopped
|
||||
|
||||
worker:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: docker/Dockerfile
|
||||
environment:
|
||||
- IMAGEFORGE_ROLE=worker
|
||||
- DATABASE_URL=postgres://imageforge:${DB_PASSWORD}@postgres:5432/imageforge
|
||||
- REDIS_URL=redis://redis:6379
|
||||
- JWT_SECRET=${JWT_SECRET}
|
||||
- STORAGE_TYPE=local
|
||||
- STORAGE_PATH=/app/uploads
|
||||
volumes:
|
||||
- uploads:/app/uploads
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
restart: unless-stopped
|
||||
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_USER: imageforge
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
POSTGRES_DB: imageforge
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
restart: unless-stopped
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
restart: unless-stopped
|
||||
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
- /etc/letsencrypt:/etc/letsencrypt:ro
|
||||
depends_on:
|
||||
- api
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
uploads:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
```
|
||||
|
||||
`docker/Dockerfile`:
|
||||
```dockerfile
|
||||
FROM rust:1.92-bookworm AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
cmake \
|
||||
nasm \
|
||||
pkg-config \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY src ./src
|
||||
COPY migrations ./migrations
|
||||
COPY templates ./templates
|
||||
|
||||
RUN cargo build --release
|
||||
|
||||
# 前端构建阶段
|
||||
FROM node:20-alpine AS frontend-builder
|
||||
|
||||
WORKDIR /app/frontend
|
||||
COPY frontend/package*.json ./
|
||||
RUN npm ci
|
||||
COPY frontend ./
|
||||
RUN npm run build
|
||||
|
||||
# 运行阶段
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/target/release/imageforge ./imageforge
|
||||
COPY --from=frontend-builder /app/frontend/dist ./static
|
||||
COPY migrations ./migrations
|
||||
|
||||
RUN mkdir -p uploads
|
||||
|
||||
ENV HOST=0.0.0.0
|
||||
ENV PORT=8080
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
CMD ["./imageforge"]
|
||||
```
|
||||
|
||||
`docker/nginx.conf`:
|
||||
```nginx
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# 日志
|
||||
access_log /var/log/nginx/access.log;
|
||||
error_log /var/log/nginx/error.log;
|
||||
|
||||
# 文件上传大小限制
|
||||
client_max_body_size 100M;
|
||||
|
||||
# Gzip
|
||||
gzip on;
|
||||
gzip_types text/plain text/css application/json application/javascript;
|
||||
|
||||
upstream backend {
|
||||
server api:8080;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name your-domain.com;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
|
||||
|
||||
# SSL 配置
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
|
||||
ssl_prefer_server_ciphers off;
|
||||
|
||||
# 静态文件
|
||||
location /static/ {
|
||||
proxy_pass http://backend;
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# WebSocket
|
||||
location /ws/ {
|
||||
proxy_pass http://backend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_read_timeout 86400;
|
||||
}
|
||||
|
||||
# API 和其他请求
|
||||
location / {
|
||||
proxy_pass http://backend;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 部署步骤
|
||||
|
||||
```bash
|
||||
# 1. 创建 .env 文件
|
||||
cat > .env << EOF
|
||||
DB_PASSWORD=your-secure-db-password
|
||||
JWT_SECRET=your-very-long-random-jwt-secret-at-least-32-chars
|
||||
STRIPE_SECRET_KEY=sk_live_xxx
|
||||
STRIPE_WEBHOOK_SECRET=whsec_xxx
|
||||
EOF
|
||||
|
||||
# 2. 获取 SSL 证书
|
||||
sudo certbot certonly --standalone -d your-domain.com
|
||||
|
||||
# 3. 构建并启动
|
||||
docker-compose -f docker/docker-compose.prod.yml up -d --build
|
||||
|
||||
# 4. 查看日志
|
||||
docker-compose -f docker/docker-compose.prod.yml logs -f api
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 方案二:Kubernetes(大规模)
|
||||
|
||||
`k8s/deployment.yaml`:
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: imageforge
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: imageforge
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: imageforge
|
||||
spec:
|
||||
containers:
|
||||
- name: imageforge
|
||||
image: your-registry/imageforge:latest
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
env:
|
||||
- name: IMAGEFORGE_ROLE
|
||||
value: api
|
||||
- name: DATABASE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: imageforge-secrets
|
||||
key: database-url
|
||||
- name: REDIS_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: imageforge-secrets
|
||||
key: redis-url
|
||||
- name: JWT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: imageforge-secrets
|
||||
key: jwt-secret
|
||||
resources:
|
||||
requests:
|
||||
memory: "512Mi"
|
||||
cpu: "500m"
|
||||
limits:
|
||||
memory: "2Gi"
|
||||
cpu: "2000m"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8080
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 30
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8080
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: imageforge
|
||||
spec:
|
||||
selector:
|
||||
app: imageforge
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 8080
|
||||
type: ClusterIP
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: imageforge
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: nginx
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
spec:
|
||||
tls:
|
||||
- hosts:
|
||||
- your-domain.com
|
||||
secretName: imageforge-tls
|
||||
rules:
|
||||
- host: your-domain.com
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: imageforge
|
||||
port:
|
||||
number: 80
|
||||
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: imageforge-worker
|
||||
spec:
|
||||
replicas: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
app: imageforge-worker
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: imageforge-worker
|
||||
spec:
|
||||
containers:
|
||||
- name: imageforge-worker
|
||||
image: your-registry/imageforge:latest
|
||||
env:
|
||||
- name: IMAGEFORGE_ROLE
|
||||
value: worker
|
||||
- name: DATABASE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: imageforge-secrets
|
||||
key: database-url
|
||||
- name: REDIS_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: imageforge-secrets
|
||||
key: redis-url
|
||||
- name: JWT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: imageforge-secrets
|
||||
key: jwt-secret
|
||||
resources:
|
||||
requests:
|
||||
memory: "512Mi"
|
||||
cpu: "500m"
|
||||
limits:
|
||||
memory: "4Gi"
|
||||
cpu: "4000m"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 监控与日志
|
||||
|
||||
### Prometheus 指标
|
||||
|
||||
应用暴露 `/metrics` 端点:
|
||||
|
||||
```rust
|
||||
// 在代码中添加指标
|
||||
use prometheus::{Counter, Histogram};
|
||||
|
||||
lazy_static! {
|
||||
static ref COMPRESSION_REQUESTS: Counter = Counter::new(
|
||||
"imageforge_compression_requests_total",
|
||||
"Total number of compression requests"
|
||||
).unwrap();
|
||||
|
||||
static ref COMPRESSION_DURATION: Histogram = Histogram::with_opts(
|
||||
HistogramOpts::new(
|
||||
"imageforge_compression_duration_seconds",
|
||||
"Time spent compressing images"
|
||||
)
|
||||
).unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
### Grafana 仪表板
|
||||
|
||||
监控项目:
|
||||
- 请求量 / QPS
|
||||
- 响应时间 P50/P95/P99
|
||||
- 错误率
|
||||
- 压缩任务队列长度
|
||||
- CPU / 内存使用率
|
||||
- 磁盘使用率
|
||||
|
||||
### 日志聚合
|
||||
|
||||
使用 ELK Stack 或 Loki:
|
||||
|
||||
```yaml
|
||||
# docker-compose 添加 Loki
|
||||
loki:
|
||||
image: grafana/loki:2.9.0
|
||||
ports:
|
||||
- "3100:3100"
|
||||
command: -config.file=/etc/loki/local-config.yaml
|
||||
|
||||
promtail:
|
||||
image: grafana/promtail:2.9.0
|
||||
volumes:
|
||||
- /var/log:/var/log
|
||||
- ./promtail-config.yml:/etc/promtail/config.yml
|
||||
command: -config.file=/etc/promtail/config.yml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 备份策略
|
||||
|
||||
### 数据库备份
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# backup.sh
|
||||
|
||||
DATE=$(date +%Y%m%d_%H%M%S)
|
||||
BACKUP_DIR=/backups
|
||||
|
||||
# PostgreSQL 备份
|
||||
docker exec postgres pg_dump -U imageforge imageforge | gzip > $BACKUP_DIR/db_$DATE.sql.gz
|
||||
|
||||
# 保留最近 7 天的备份
|
||||
find $BACKUP_DIR -name "db_*.sql.gz" -mtime +7 -delete
|
||||
|
||||
# 可选:上传到 S3
|
||||
# aws s3 cp $BACKUP_DIR/db_$DATE.sql.gz s3://your-bucket/backups/
|
||||
```
|
||||
|
||||
添加到 crontab:
|
||||
```bash
|
||||
0 3 * * * /path/to/backup.sh
|
||||
```
|
||||
|
||||
### 上传文件备份
|
||||
|
||||
如果使用本地存储,定期同步到 S3:
|
||||
```bash
|
||||
aws s3 sync /app/uploads s3://your-bucket/uploads --delete
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 常见问题
|
||||
|
||||
**1. 数据库连接失败**
|
||||
```bash
|
||||
# 检查 PostgreSQL 状态
|
||||
docker-compose logs postgres
|
||||
|
||||
# 测试连接
|
||||
docker exec -it postgres psql -U imageforge -d imageforge -c "SELECT 1"
|
||||
```
|
||||
|
||||
**2. 压缩失败**
|
||||
```bash
|
||||
# 检查应用日志
|
||||
docker-compose logs api | grep ERROR
|
||||
docker-compose logs worker | grep ERROR
|
||||
|
||||
# 检查磁盘空间
|
||||
df -h
|
||||
```
|
||||
|
||||
**3. 内存不足**
|
||||
```bash
|
||||
# 查看内存使用
|
||||
docker stats
|
||||
|
||||
# 调整容器内存限制
|
||||
```
|
||||
|
||||
**4. 上传超时**
|
||||
```bash
|
||||
# 检查 Nginx 配置
|
||||
# client_max_body_size 和 proxy_read_timeout
|
||||
```
|
||||
|
||||
### 健康检查端点
|
||||
|
||||
```
|
||||
GET /health
|
||||
{
|
||||
"status": "healthy",
|
||||
"database": "connected",
|
||||
"redis": "connected",
|
||||
"storage": "available",
|
||||
"uptime": 3600
|
||||
}
|
||||
```
|
||||
471
docs/email.md
Normal file
471
docs/email.md
Normal file
@@ -0,0 +1,471 @@
|
||||
# 邮件服务设计 - ImageForge
|
||||
|
||||
目标:提供开箱即用的邮件服务,支持注册验证和密码重置,管理员只需配置邮箱地址和授权码即可使用。
|
||||
|
||||
---
|
||||
|
||||
## 1. 功能范围
|
||||
|
||||
### 1.1 首期必须
|
||||
- **注册邮箱验证**:用户注册后发送验证邮件,点击链接完成激活
|
||||
- **密码重置**:用户申请重置密码,发送重置链接邮件
|
||||
|
||||
### 1.2 后续可选(V1+)
|
||||
- 订阅到期提醒
|
||||
- 配额即将用尽提醒
|
||||
- 支付成功/失败通知
|
||||
- 安全告警(异地登录、API Key 创建等)
|
||||
|
||||
---
|
||||
|
||||
## 2. 技术方案
|
||||
|
||||
### 2.1 SMTP 直连
|
||||
使用标准 SMTP 协议,通过 `lettre` (Rust) 发送邮件,无需第三方 SaaS 依赖。
|
||||
|
||||
```toml
|
||||
# Cargo.toml
|
||||
[dependencies]
|
||||
lettre = { version = "0.11", default-features = false, features = ["tokio1", "tokio1-rustls-tls", "builder", "smtp-transport"] }
|
||||
```
|
||||
|
||||
### 2.2 预置服务商模板
|
||||
|
||||
管理员只需选择服务商并填写邮箱/授权码,系统自动填充 SMTP 配置:
|
||||
|
||||
| 服务商 | SMTP 地址 | 端口 | 加密 | 备注 |
|
||||
|--------|-----------|------|------|------|
|
||||
| QQ 邮箱 | `smtp.qq.com` | 465 | SSL | 需开启 SMTP 服务并获取授权码 |
|
||||
| 163 邮箱 | `smtp.163.com` | 465 | SSL | 需开启 SMTP 服务并获取授权码 |
|
||||
| 阿里企业邮箱 | `smtp.qiye.aliyun.com` | 465 | SSL | 使用邮箱密码 |
|
||||
| 腾讯企业邮箱 | `smtp.exmail.qq.com` | 465 | SSL | 需获取授权码 |
|
||||
| Gmail | `smtp.gmail.com` | 587 | STARTTLS | 需开启两步验证并生成应用专用密码 |
|
||||
| Outlook/365 | `smtp.office365.com` | 587 | STARTTLS | 使用账号密码 |
|
||||
| 自定义 | 用户填写 | 用户填写 | 用户选择 | 支持任意 SMTP 服务器 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 配置设计
|
||||
|
||||
### 3.1 环境变量
|
||||
|
||||
```bash
|
||||
# .env.example
|
||||
|
||||
# 邮件服务配置
|
||||
MAIL_ENABLED=true
|
||||
# 开发环境:当 MAIL_ENABLED=false 时,可打开该开关把验证/重置链接打印到日志(便于本地联调)
|
||||
MAIL_LOG_LINKS_WHEN_DISABLED=true
|
||||
|
||||
# 预置服务商(可选:qq / 163 / aliyun_enterprise / tencent_enterprise / gmail / outlook / custom)
|
||||
MAIL_PROVIDER=qq
|
||||
|
||||
# 发件邮箱(必填)
|
||||
MAIL_FROM=noreply@example.com
|
||||
|
||||
# 授权码/密码(必填)
|
||||
MAIL_PASSWORD=your-smtp-authorization-code
|
||||
|
||||
# 发件人名称(可选,默认 "ImageForge")
|
||||
MAIL_FROM_NAME=ImageForge
|
||||
|
||||
# === 以下仅 MAIL_PROVIDER=custom 时需要 ===
|
||||
# MAIL_SMTP_HOST=smtp.example.com
|
||||
# MAIL_SMTP_PORT=465
|
||||
# MAIL_SMTP_ENCRYPTION=ssl # ssl / starttls / none
|
||||
```
|
||||
|
||||
### 3.2 数据库配置(管理后台可改)
|
||||
|
||||
```sql
|
||||
-- system_config 表
|
||||
INSERT INTO system_config (key, value, description) VALUES
|
||||
('mail', '{
|
||||
"enabled": true,
|
||||
"provider": "qq",
|
||||
"from": "noreply@example.com",
|
||||
"from_name": "ImageForge",
|
||||
"password_encrypted": "...",
|
||||
"custom_smtp": null
|
||||
}', '邮件服务配置');
|
||||
```
|
||||
|
||||
### 3.3 Rust 配置结构
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct MailConfig {
|
||||
pub enabled: bool,
|
||||
pub provider: MailProvider,
|
||||
pub from: String,
|
||||
pub from_name: String,
|
||||
pub password: String, // 运行时解密
|
||||
pub custom_smtp: Option<CustomSmtpConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub enum MailProvider {
|
||||
QQ,
|
||||
NetEase163,
|
||||
AliyunEnterprise,
|
||||
TencentEnterprise,
|
||||
Gmail,
|
||||
Outlook,
|
||||
Custom,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct CustomSmtpConfig {
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub encryption: SmtpEncryption,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub enum SmtpEncryption {
|
||||
Ssl,
|
||||
StartTls,
|
||||
None,
|
||||
}
|
||||
|
||||
impl MailProvider {
|
||||
pub fn smtp_config(&self) -> (String, u16, SmtpEncryption) {
|
||||
match self {
|
||||
Self::QQ => ("smtp.qq.com".into(), 465, SmtpEncryption::Ssl),
|
||||
Self::NetEase163 => ("smtp.163.com".into(), 465, SmtpEncryption::Ssl),
|
||||
Self::AliyunEnterprise => ("smtp.qiye.aliyun.com".into(), 465, SmtpEncryption::Ssl),
|
||||
Self::TencentEnterprise => ("smtp.exmail.qq.com".into(), 465, SmtpEncryption::Ssl),
|
||||
Self::Gmail => ("smtp.gmail.com".into(), 587, SmtpEncryption::StartTls),
|
||||
Self::Outlook => ("smtp.office365.com".into(), 587, SmtpEncryption::StartTls),
|
||||
Self::Custom => panic!("Custom provider requires explicit config"),
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 邮件模板
|
||||
|
||||
### 4.1 模板存储
|
||||
|
||||
建议使用内嵌模板(编译时包含),支持变量替换:
|
||||
|
||||
```
|
||||
templates/
|
||||
├── email_verification.html
|
||||
├── email_verification.txt
|
||||
├── password_reset.html
|
||||
└── password_reset.txt
|
||||
```
|
||||
|
||||
### 4.2 注册验证邮件
|
||||
|
||||
**主题**:`验证您的 ImageForge 账号`
|
||||
|
||||
**HTML 模板** (`email_verification.html`):
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>验证您的邮箱</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
|
||||
.container { max-width: 600px; margin: 0 auto; padding: 40px 20px; }
|
||||
.header { text-align: center; margin-bottom: 30px; }
|
||||
.logo { font-size: 24px; font-weight: bold; color: #4a90d9; }
|
||||
.content { background: #f9fafb; border-radius: 8px; padding: 30px; margin-bottom: 30px; }
|
||||
.button { display: inline-block; background: #4a90d9; color: #fff !important; text-decoration: none; padding: 12px 30px; border-radius: 6px; font-weight: 500; }
|
||||
.button:hover { background: #357abd; }
|
||||
.footer { text-align: center; font-size: 12px; color: #666; }
|
||||
.link { word-break: break-all; color: #4a90d9; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<div class="logo">ImageForge</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<h2>欢迎注册 ImageForge</h2>
|
||||
<p>您好,{{username}}!</p>
|
||||
<p>感谢您注册 ImageForge。请点击下方按钮验证您的邮箱地址:</p>
|
||||
<p style="text-align: center; margin: 30px 0;">
|
||||
<a href="{{verification_url}}" class="button">验证邮箱</a>
|
||||
</p>
|
||||
<p>或复制以下链接到浏览器打开:</p>
|
||||
<p class="link">{{verification_url}}</p>
|
||||
<p style="margin-top: 20px; font-size: 14px; color: #666;">
|
||||
此链接将在 <strong>24 小时</strong>后失效。如果您没有注册 ImageForge 账号,请忽略此邮件。
|
||||
</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>© {{year}} ImageForge. All rights reserved.</p>
|
||||
<p>此邮件由系统自动发送,请勿直接回复。</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
**纯文本模板** (`email_verification.txt`):
|
||||
|
||||
```text
|
||||
欢迎注册 ImageForge
|
||||
|
||||
您好,{{username}}!
|
||||
|
||||
感谢您注册 ImageForge。请点击以下链接验证您的邮箱地址:
|
||||
|
||||
{{verification_url}}
|
||||
|
||||
此链接将在 24 小时后失效。如果您没有注册 ImageForge 账号,请忽略此邮件。
|
||||
|
||||
---
|
||||
© {{year}} ImageForge
|
||||
此邮件由系统自动发送,请勿直接回复。
|
||||
```
|
||||
|
||||
### 4.3 密码重置邮件
|
||||
|
||||
**主题**:`重置您的 ImageForge 密码`
|
||||
|
||||
**HTML 模板** (`password_reset.html`):
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>重置密码</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
|
||||
.container { max-width: 600px; margin: 0 auto; padding: 40px 20px; }
|
||||
.header { text-align: center; margin-bottom: 30px; }
|
||||
.logo { font-size: 24px; font-weight: bold; color: #4a90d9; }
|
||||
.content { background: #f9fafb; border-radius: 8px; padding: 30px; margin-bottom: 30px; }
|
||||
.button { display: inline-block; background: #4a90d9; color: #fff !important; text-decoration: none; padding: 12px 30px; border-radius: 6px; font-weight: 500; }
|
||||
.footer { text-align: center; font-size: 12px; color: #666; }
|
||||
.link { word-break: break-all; color: #4a90d9; }
|
||||
.warning { background: #fef3c7; border-left: 4px solid #f59e0b; padding: 12px; margin: 20px 0; font-size: 14px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<div class="logo">ImageForge</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<h2>重置您的密码</h2>
|
||||
<p>您好,{{username}}!</p>
|
||||
<p>我们收到了重置您 ImageForge 账号密码的请求。请点击下方按钮设置新密码:</p>
|
||||
<p style="text-align: center; margin: 30px 0;">
|
||||
<a href="{{reset_url}}" class="button">重置密码</a>
|
||||
</p>
|
||||
<p>或复制以下链接到浏览器打开:</p>
|
||||
<p class="link">{{reset_url}}</p>
|
||||
<div class="warning">
|
||||
<strong>安全提示:</strong>此链接将在 <strong>1 小时</strong>后失效。如果您没有请求重置密码,请忽略此邮件,您的账号仍然安全。
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>© {{year}} ImageForge. All rights reserved.</p>
|
||||
<p>此邮件由系统自动发送,请勿直接回复。</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 数据库设计
|
||||
|
||||
### 5.1 邮箱验证 Token
|
||||
|
||||
```sql
|
||||
CREATE TABLE email_verifications (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
token_hash VARCHAR(64) NOT NULL, -- SHA256(token)
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
verified_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX idx_email_verifications_token ON email_verifications(token_hash);
|
||||
CREATE INDEX idx_email_verifications_user_id ON email_verifications(user_id);
|
||||
CREATE INDEX idx_email_verifications_expires_at ON email_verifications(expires_at);
|
||||
```
|
||||
|
||||
### 5.2 密码重置 Token
|
||||
|
||||
```sql
|
||||
CREATE TABLE password_resets (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
token_hash VARCHAR(64) NOT NULL, -- SHA256(token)
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
used_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX idx_password_resets_token ON password_resets(token_hash);
|
||||
CREATE INDEX idx_password_resets_user_id ON password_resets(user_id);
|
||||
CREATE INDEX idx_password_resets_expires_at ON password_resets(expires_at);
|
||||
```
|
||||
|
||||
### 5.3 用户表扩展
|
||||
|
||||
```sql
|
||||
ALTER TABLE users ADD COLUMN email_verified_at TIMESTAMPTZ;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. API 接口
|
||||
|
||||
### 6.1 发送验证邮件
|
||||
|
||||
```http
|
||||
POST /auth/send-verification
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{ "success": true, "data": { "message": "验证邮件已发送,请查收" } }
|
||||
```
|
||||
|
||||
**限流**:同一用户 1 分钟内最多发送 1 次
|
||||
|
||||
### 6.2 验证邮箱
|
||||
|
||||
```http
|
||||
POST /auth/verify-email
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{ "token": "verification-token-from-email" }
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{ "success": true, "data": { "message": "邮箱验证成功" } }
|
||||
```
|
||||
|
||||
### 6.3 请求密码重置
|
||||
|
||||
```http
|
||||
POST /auth/forgot-password
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{ "email": "user@example.com" }
|
||||
```
|
||||
|
||||
**响应**(无论邮箱是否存在都返回成功,防止枚举):
|
||||
```json
|
||||
{ "success": true, "data": { "message": "如果该邮箱已注册,您将收到重置邮件" } }
|
||||
```
|
||||
|
||||
**限流**:同一 IP 1 分钟内最多请求 3 次
|
||||
|
||||
### 6.4 重置密码
|
||||
|
||||
```http
|
||||
POST /auth/reset-password
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"token": "reset-token-from-email",
|
||||
"new_password": "new-secure-password"
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{ "success": true, "data": { "message": "密码重置成功,请重新登录" } }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 安全考虑
|
||||
|
||||
### 7.1 Token 安全
|
||||
- Token 使用 `crypto-secure random`(32 字节 base64)
|
||||
- 数据库只存 `SHA256(token)`,不存明文
|
||||
- Token 单次有效,使用后立即标记 `used_at`
|
||||
|
||||
### 7.2 时效控制
|
||||
- 邮箱验证 Token:24 小时有效
|
||||
- 密码重置 Token:1 小时有效
|
||||
|
||||
### 7.3 防滥用
|
||||
- 发送邮件接口严格限流
|
||||
- 密码重置不泄露"邮箱是否存在"
|
||||
- 失败尝试记录审计日志
|
||||
|
||||
### 7.4 授权码加密存储
|
||||
- SMTP 授权码在数据库中加密存储(AES-256-GCM)
|
||||
- 密钥来自环境变量或密钥管理服务
|
||||
|
||||
---
|
||||
|
||||
## 8. 管理后台配置界面
|
||||
|
||||
管理后台提供邮件服务配置页面:
|
||||
|
||||
```
|
||||
邮件服务配置
|
||||
├── 启用状态:[开关]
|
||||
├── 服务商选择:[下拉:QQ邮箱 / 163邮箱 / 阿里企业邮 / 腾讯企业邮 / Gmail / Outlook / 自定义]
|
||||
├── 发件邮箱:[输入框]
|
||||
├── 授权码/密码:[密码输入框]
|
||||
├── 发件人名称:[输入框,默认 ImageForge]
|
||||
├── (自定义时显示)
|
||||
│ ├── SMTP 服务器:[输入框]
|
||||
│ ├── 端口:[输入框]
|
||||
│ └── 加密方式:[下拉:SSL / STARTTLS / 无]
|
||||
└── [测试发送] [保存配置]
|
||||
```
|
||||
|
||||
**测试发送**:向管理员邮箱发送测试邮件,验证配置是否正确。
|
||||
|
||||
---
|
||||
|
||||
## 9. 常见邮件服务商配置指南
|
||||
|
||||
### 9.1 QQ 邮箱
|
||||
1. 登录 QQ 邮箱 → 设置 → 账户
|
||||
2. 找到「POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务」
|
||||
3. 开启「SMTP 服务」
|
||||
4. 按提示发送短信获取授权码
|
||||
5. 将授权码填入系统配置
|
||||
|
||||
### 9.2 163 邮箱
|
||||
1. 登录 163 邮箱 → 设置 → POP3/SMTP/IMAP
|
||||
2. 开启「SMTP 服务」
|
||||
3. 设置客户端授权密码
|
||||
4. 将授权密码填入系统配置
|
||||
|
||||
### 9.3 Gmail
|
||||
1. 登录 Google 账号 → 安全性
|
||||
2. 开启「两步验证」
|
||||
3. 生成「应用专用密码」(选择"邮件"+"其他")
|
||||
4. 将应用专用密码填入系统配置
|
||||
|
||||
### 9.4 阿里企业邮箱
|
||||
1. 使用邮箱地址和登录密码即可
|
||||
2. SMTP 服务默认开启
|
||||
158
docs/frontend.md
Normal file
158
docs/frontend.md
Normal file
@@ -0,0 +1,158 @@
|
||||
# 前端工程设计(Vue3)- ImageForge
|
||||
|
||||
目标:支撑“网站压缩 + 开发者 API 控制台 + 计费/发票 + 管理后台”的一个 Vue3 SPA(或同仓多入口)。
|
||||
|
||||
UI/UX 规格见 `docs/ui.md`。
|
||||
|
||||
---
|
||||
|
||||
## 1. 技术栈(确定)
|
||||
|
||||
- Vue 3 + TypeScript + Vite
|
||||
- 路由:Vue Router
|
||||
- 状态:Pinia
|
||||
- 网络:Fetch 或 Axios(统一封装,支持幂等头、错误归一)
|
||||
- 样式:Tailwind CSS(推荐)或 CSS Variables + 自研组件
|
||||
- 工具:VueUse、Day.js(或 date-fns)、Zod(表单校验可选)
|
||||
|
||||
---
|
||||
|
||||
## 2. 路由与页面
|
||||
|
||||
### 2.1 公共
|
||||
```
|
||||
/ 首页压缩
|
||||
/pricing 套餐与 FAQ
|
||||
/docs 开发者文档(引导)
|
||||
/login
|
||||
/register
|
||||
/terms
|
||||
/privacy
|
||||
```
|
||||
|
||||
### 2.2 用户控制台(登录)
|
||||
```
|
||||
/dashboard
|
||||
/dashboard/history
|
||||
/dashboard/api-keys
|
||||
/dashboard/billing
|
||||
/dashboard/settings
|
||||
```
|
||||
|
||||
### 2.3 管理后台(管理员)
|
||||
```
|
||||
/admin
|
||||
/admin/users
|
||||
/admin/tasks
|
||||
/admin/billing
|
||||
/admin/config
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 前端项目结构(建议)
|
||||
|
||||
```
|
||||
src/
|
||||
app/ # 路由、布局、鉴权守卫
|
||||
pages/ # 页面(route components)
|
||||
components/ # 通用组件(UI、上传、表格等)
|
||||
features/
|
||||
compress/ # 压缩:上传、任务、下载
|
||||
billing/ # 套餐、订阅、发票、用量
|
||||
apiKeys/ # API Key 管理
|
||||
admin/ # 管理后台
|
||||
services/ # API 封装(http client + endpoints)
|
||||
stores/ # Pinia stores
|
||||
styles/ # 主题变量、tailwind 入口
|
||||
utils/ # 格式化、文件校验、错误处理
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. API 调用规范(前端约定)
|
||||
|
||||
### 4.1 Base URL
|
||||
- 统一使用 `/api/v1`
|
||||
|
||||
### 4.2 幂等与重试
|
||||
- 对 `POST /compress/*`、`POST /billing/checkout` 等请求默认注入 `Idempotency-Key`(UUID)。
|
||||
- 网络重试仅限“明确幂等”的请求(否则会重复扣费/重复建任务)。
|
||||
|
||||
### 4.3 错误处理
|
||||
将后端错误码映射为统一 UI 提示:
|
||||
- `QUOTA_EXCEEDED`:引导升级/查看账单页
|
||||
- `RATE_LIMITED`:展示倒计时(读取 `Retry-After`)
|
||||
- `FILE_TOO_LARGE` / `TOO_MANY_PIXELS`:定位到具体文件并提示如何处理
|
||||
|
||||
---
|
||||
|
||||
## 5. 压缩流程(Web)
|
||||
|
||||
### 5.1 同步 vs 异步
|
||||
- 小文件/少量:可直接调用 `POST /compress`,拿到 `download_url`。
|
||||
- 批量/大文件:调用 `POST /compress/batch`,拿到 `task_id` 后:
|
||||
- 优先 WebSocket/SSE 订阅进度;
|
||||
- fallback:轮询 `GET /compress/tasks/{task_id}`。
|
||||
|
||||
### 5.2 上传前校验
|
||||
前端必须做“用户体验级校验”(后端仍需二次校验):
|
||||
- 格式白名单(png/jpg/jpeg/webp/avif/gif/bmp/tiff/ico,GIF 仅静态)
|
||||
- 文件大小与数量(按匿名/登录/套餐提示不同上限)
|
||||
- 匿名试用:每日 10 次限制提示(达到后引导登录/升级)
|
||||
- 可选:读取图片宽高(避免明显超限)
|
||||
|
||||
---
|
||||
|
||||
## 6. 计费与用量(前端展示)
|
||||
|
||||
对接 `docs/api.md` 的 Billing 模块:
|
||||
- `/pricing` 页面:读取 `GET /billing/plans`
|
||||
- 控制台概览:读取 `GET /billing/usage`、`GET /billing/subscription`
|
||||
- 订阅升级:调用 `POST /billing/checkout` 获取 `checkout_url` 并跳转
|
||||
- 支付方式/取消订阅:调用 `POST /billing/portal` 获取 portal 链接
|
||||
- 发票列表:`GET /billing/invoices`
|
||||
|
||||
UI 必须展示:
|
||||
- 当期已用/剩余、重置时间
|
||||
- 当前订阅状态(active/past_due/canceled)
|
||||
|
||||
---
|
||||
|
||||
## 7. API Key 控制台(开发者体验)
|
||||
|
||||
页面提供三类信息:
|
||||
1) Key 管理:创建/禁用/轮换(创建时只展示一次完整 Key)
|
||||
2) 用量:本周期已用/剩余(与 Billing 用量一致)
|
||||
3) 快速接入:curl 示例 + 常见错误码 + 幂等建议
|
||||
|
||||
---
|
||||
|
||||
## 8. 安全建议(前端侧)
|
||||
|
||||
- 若使用 Bearer Token:避免 localStorage(XSS 风险),优先 HttpOnly Cookie 会话(需要 CSRF 策略)。
|
||||
- 上传与下载链接:明确到期时间与隐私说明(默认去 EXIF)。
|
||||
- 管理后台路由加守卫:`role=admin` 才可进入。
|
||||
|
||||
---
|
||||
|
||||
## 9. 主题变量(CSS Variables)
|
||||
|
||||
首期可用 Tailwind 或自研组件,但建议保留一层 CSS 变量,方便后续主题化/暗色模式:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--bg: 248 250 252;
|
||||
--card: 255 255 255;
|
||||
--text: 15 23 42;
|
||||
--muted: 71 85 105;
|
||||
--border: 226 232 240;
|
||||
|
||||
--brand: 99 102 241;
|
||||
--brand-strong: 79 70 229;
|
||||
|
||||
--success: 34 197 94;
|
||||
--warning: 245 158 11;
|
||||
--danger: 239 68 68;
|
||||
}
|
||||
```
|
||||
99
docs/observability.md
Normal file
99
docs/observability.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# 可观测性设计(日志/指标/追踪)- ImageForge
|
||||
|
||||
目标:让“压缩效果、性能瓶颈、队列健康、计费正确性、滥用风险”都能被观测与告警,便于商用运营。
|
||||
|
||||
---
|
||||
|
||||
## 1. 统一规范
|
||||
|
||||
### 1.1 请求标识
|
||||
- 每个 HTTP 请求生成 `request_id`(或从网关透传),写入:
|
||||
- 响应头:`X-Request-Id`
|
||||
- 日志字段:`request_id`
|
||||
- Trace:`trace_id/span_id`(如启用 OpenTelemetry)
|
||||
|
||||
### 1.2 日志格式
|
||||
- 结构化日志(JSON)优先,便于 Loki/ELK 聚合。
|
||||
- 禁止记录:明文密码、JWT、API Key、Webhook secret。
|
||||
|
||||
建议最小字段:
|
||||
- `timestamp`、`level`、`service`(api/worker)、`request_id`
|
||||
- `user_id`(可空)、`api_key_id`(可空)、`ip`、`user_agent`
|
||||
- `route`、`method`、`status`、`latency_ms`
|
||||
- `task_id`、`task_file_id`(压缩链路)
|
||||
- `bytes_in`、`bytes_out`、`format_in/out`、`compression_level`
|
||||
|
||||
---
|
||||
|
||||
## 2. 指标(Prometheus)
|
||||
|
||||
### 2.1 API 服务指标
|
||||
请求类:
|
||||
- `http_requests_total{route,method,status}`
|
||||
- `http_request_duration_seconds_bucket{route,method}`
|
||||
|
||||
鉴权与风控:
|
||||
- `auth_fail_total{reason}`
|
||||
- `rate_limited_total{scope}`(anonymous/user/api_key)
|
||||
- `quota_exceeded_total{plan}`
|
||||
|
||||
计费链路:
|
||||
- `billing_webhook_total{provider,event_type,result}`
|
||||
- `subscription_state_total{state}`
|
||||
- `invoice_total{status}`
|
||||
|
||||
### 2.2 Worker 指标
|
||||
队列与吞吐:
|
||||
- `jobs_received_total`
|
||||
- `jobs_inflight`
|
||||
- `jobs_completed_total{result}`
|
||||
- `job_duration_seconds_bucket{format,level}`
|
||||
|
||||
压缩效果:
|
||||
- `bytes_in_total`、`bytes_out_total`、`bytes_saved_total`
|
||||
- `compression_ratio_bucket{format,level}`
|
||||
|
||||
资源与异常:
|
||||
- `decode_failed_total{reason}`
|
||||
- `pixel_limit_hit_total`
|
||||
|
||||
### 2.3 Redis/队列指标(可选)
|
||||
- Streams 消费延迟、pending 数量、dead-letter 数量(如实现)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 追踪(Tracing)
|
||||
|
||||
建议:API 与 Worker 使用 OpenTelemetry,打通跨服务链路:
|
||||
- API:`create_task` span、`auth` span、`db` span、`redis` span
|
||||
- Worker:`fetch_job` span、`download_input` span、`compress` span、`upload_output` span、`metering` span
|
||||
|
||||
价值:
|
||||
- 发现耗时集中点(解码/编码/S3/DB)。
|
||||
- 对账问题定位(用量事件写入失败/重复)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 仪表板与告警(建议)
|
||||
|
||||
### 4.1 SLO(建议起点)
|
||||
- API:P95 < 300ms(不含压缩直返)、错误率 < 0.5%
|
||||
- Worker:队列积压 < N(按规模定义),失败率 < 1%
|
||||
|
||||
### 4.2 告警
|
||||
可用性:
|
||||
- `http 5xx` 激增
|
||||
- `/health` 探活失败
|
||||
|
||||
队列健康:
|
||||
- pending/inflight 持续上升
|
||||
- 单任务耗时异常增长
|
||||
|
||||
计费正确性:
|
||||
- webhook 处理失败
|
||||
- 订阅状态异常(active->incomplete 回退等)
|
||||
|
||||
滥用风险:
|
||||
- 单 key/单 IP 用量突增
|
||||
- 格式探测失败率异常
|
||||
|
||||
162
docs/prd.md
Normal file
162
docs/prd.md
Normal file
@@ -0,0 +1,162 @@
|
||||
# 产品需求文档(PRD)- ImageForge
|
||||
|
||||
目标:做一个接近商用的图片压缩网站 + 对外 API + 计费系统(可订阅/可计量)。
|
||||
|
||||
> 本文只描述“做什么/做到什么程度”。技术实现细节在 `docs/architecture.md`、`docs/api.md`、`docs/database.md`、`docs/deployment.md`、`docs/ui.md`。
|
||||
|
||||
---
|
||||
|
||||
## 1. 产品定位
|
||||
|
||||
### 1.1 一句话
|
||||
提供高质量、稳定、可规模化的图片压缩服务:既能让普通用户在网页上批量压缩,也能让开发者通过 API Key 集成到 CI/CD 或业务系统,并按使用量/套餐计费。
|
||||
|
||||
### 1.2 核心价值
|
||||
- **效果**:压缩比与画质控制可预期(可选有损/无损/近无损)。
|
||||
- **体验**:网站拖拽即用、批量任务、可下载 ZIP、历史可追溯。
|
||||
- **工程化**:对外 API 稳定、可观测、可限流、可计量、可计费。
|
||||
- **安全合规**:默认去除隐私元数据、明确保留期限、支持删除。
|
||||
|
||||
### 1.3 非目标(明确不做/后做)
|
||||
- 图像编辑(裁剪/滤镜/水印)不作为核心能力(后续可扩展)。
|
||||
- CDN 图片处理(按 URL 在线压缩、自动适配)不作为首期必须。
|
||||
- 视频/动图(GIF/APNG)暂不纳入首期(除非强需求)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 用户与角色
|
||||
|
||||
### 2.1 用户类型
|
||||
- **游客(Anonymous)**:无需注册即可试用网站压缩(强限制、短保留)。
|
||||
- **注册用户(User)**:使用网站 + 管理 API Key + 查看用量/发票。
|
||||
- **企业/团队用户(Team,可选)**:多人协作、共享额度、角色权限(可作为 V1+)。
|
||||
- **管理员(Admin)**:风控、配置、用户/任务/账单审核、运营数据。
|
||||
|
||||
### 2.2 典型场景
|
||||
- 设计师:批量压缩并打包下载,保留 7 天内历史。
|
||||
- 开发者:CI 里调用 API,在发布前批量压缩静态资源。
|
||||
- 运营:导出周期用量、查看节省带宽、按部门拆分账单(团队版)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 功能范围(按模块)
|
||||
|
||||
### 3.1 网站压缩(Web)
|
||||
- 拖拽/选择图片(单次多文件)。
|
||||
- 压缩参数:
|
||||
- 压缩率:1-100(数值越大压缩越强)。
|
||||
- 输出格式:保持原格式。
|
||||
- 可选:限制宽高(等比缩放)。
|
||||
- 可选:是否保留元数据(默认不保留)。
|
||||
- 结果展示:
|
||||
- 逐文件:原始大小、压缩后大小、压缩率、状态、下载。
|
||||
- 汇总:总节省、下载 ZIP、失败文件原因。
|
||||
- 历史记录:
|
||||
- 最近任务列表(可筛选:日期/格式/状态)。
|
||||
- 可再次下载(未过期)、或重新发起压缩(使用相同参数)。
|
||||
- 游客试用:
|
||||
- 自动创建匿名会话(Cookie),只允许较小文件/较少数量。
|
||||
- 到期自动清理,不提供“历史”永久保存。
|
||||
|
||||
### 3.2 对外 API(Developer API)
|
||||
- API Key:创建/禁用/轮换/权限范围(最小权限)。
|
||||
- 压缩接口:
|
||||
- 同步单图(可返回二进制或下载链接)。
|
||||
- 批量/大文件异步任务(任务状态、下载 ZIP)。
|
||||
- 可选:Webhook 回调(替代轮询/WS,用于服务端集成)。
|
||||
- 工程化能力:
|
||||
- 幂等(Idempotency-Key),避免重复扣费/重复任务。
|
||||
- 配额与用量头信息(本订阅周期已用/剩余/上限)。
|
||||
- 速率限制(标准 `Retry-After` + 速率头)。
|
||||
|
||||
### 3.3 计费与用量(Billing & Metering)
|
||||
详见 `docs/billing.md`,PRD 层面要求:
|
||||
- 具备 **套餐**(Free/Pro/Business)与 **配额**(每订阅周期压缩次数、文件大小/批量上限、保留期等)。
|
||||
- 具备 **订阅** 生命周期:试用、激活、到期、欠费、取消、恢复。
|
||||
- 具备 **发票**(Invoice)与 **支付记录**(Payment)可追溯。
|
||||
- 具备 **用量计量**:以“成功压缩的文件数”为主计量单位(可扩展到字节、转换格式等)。
|
||||
- 具备 **风控策略**:异常调用/盗刷/滥用限制与告警。
|
||||
|
||||
### 3.4 管理后台(Admin)
|
||||
- 用户管理:冻结/解冻、限流覆盖、手动调整套餐/额度、查看登录/调用记录。
|
||||
- 任务管理:查看任务队列/失败原因、取消任务、重试。
|
||||
- 计费管理:查看订阅与发票、手动赠送额度、处理退款(首期可做“手动退款登记”)。
|
||||
- 系统配置:全局限流、文件限制、保留期、功能开关(注册开关等)。
|
||||
- 监控面板:QPS、延迟、错误率、队列长度、CPU/内存、S3/DB/Redis 状态。
|
||||
|
||||
---
|
||||
|
||||
## 4. 套餐与配额(建议默认值,可调)
|
||||
|
||||
> 这些是“产品默认建议”,最终可在上线前确认并固化到配置/数据库。
|
||||
|
||||
| 项 | Free | Pro | Business |
|
||||
|---|---:|---:|---:|
|
||||
| 每周期压缩次数(成功文件数) | 500 | 10,000 | 100,000+ |
|
||||
| 单文件大小上限 | 5 MB | 20 MB | 50 MB |
|
||||
| 单次批量上限 | 10 | 50 | 200 |
|
||||
| 并发(建议) | 2 | 8 | 32 |
|
||||
| 结果保留期 | 24 小时 | 7 天 | 30 天 |
|
||||
| API 访问 | ❌ | ✅ | ✅ |
|
||||
| Webhook | ❌ | ✅ | ✅ |
|
||||
| SSO/团队 | ❌ | ❌ | 可选 |
|
||||
|
||||
周期定义(用于“本周期已用/剩余/重置时间”的展示与扣减):
|
||||
- **Pro/Business(付费)**:按 Stripe 订阅周期(`current_period_start` ~ `current_period_end`),不是自然月。
|
||||
- **Free(未订阅)**:按自然月重置(UTC+8)。
|
||||
|
||||
匿名试用(无需登录):
|
||||
- 每日 10 次(以成功压缩文件数计,失败不计)
|
||||
- 日界:自然日(UTC+8),次日 00:00 重置
|
||||
- 不提供 API Key
|
||||
- 结果保留 24 小时
|
||||
|
||||
计量单位:
|
||||
- **compression_unit**:每成功压缩 1 个输出文件计 1。
|
||||
- 对于批量任务:按文件粒度计量;失败文件不计量。
|
||||
- 幂等:同一个 Idempotency-Key 重试不重复计量。
|
||||
|
||||
---
|
||||
|
||||
## 5. 关键体验与质量指标(NFR)
|
||||
|
||||
### 5.1 性能与稳定性(建议目标)
|
||||
- 99% 的同步单图压缩在 3s 内完成(小图、常见格式)。
|
||||
- 批量任务在可预期时间内完成(提供进度/预估)。
|
||||
- 系统可水平扩展:API 与 Worker 可独立扩容。
|
||||
|
||||
### 5.2 安全与合规(必须)
|
||||
- 默认移除 EXIF 等隐私元数据(可配置允许保留,但需明确提示)。
|
||||
- 上传内容按保留期自动删除,且支持用户主动删除。
|
||||
- API Key 仅创建时显示一次;支持轮换与禁用。
|
||||
- 具备基础风控:IP/账号/API Key 限流、异常突增告警。
|
||||
|
||||
---
|
||||
|
||||
## 6. MVP / V1 里程碑建议
|
||||
|
||||
### MVP(可上线收费的最小闭环)
|
||||
- 网站压缩(同步 + 批量异步)+ 下载/ZIP + 历史(登录用户)。
|
||||
- API Key + 同步单图(直接返回二进制)+ 异步批量(任务/下载)。
|
||||
- 用量计量 + 套餐配额(Free/Pro)+ 订阅(至少一种支付渠道)+ 发票列表。
|
||||
- 管理后台:用户/任务/配置/用量查看 + 手动赠送额度。
|
||||
|
||||
### V1+(商用增强)
|
||||
- Webhook + SDK(TS/Python/Go)+ OpenAPI 自动生成。
|
||||
- 团队/组织、多 Key 管理、细粒度权限、IP 白名单。
|
||||
- 企业发票/税务字段、对公转账/线下支付流程。
|
||||
- 风控升级:验证码、黑名单、设备指纹、异常画像。
|
||||
|
||||
---
|
||||
|
||||
## 7. 已确认口径(开工前)
|
||||
- 支付渠道:Stripe
|
||||
- 计费策略:硬配额(超额返回 `QUOTA_EXCEEDED` / HTTP 402)
|
||||
- 配额周期:按订阅周期(非自然月)
|
||||
- 匿名试用:支持(每日 10 次),不提供 API Key
|
||||
- Free 套餐 API:不开放(仅 Pro/Business 可创建 API Key)
|
||||
- 邮件服务:注册需邮箱验证 + 密码重置(SMTP,预置多服务商模板)
|
||||
- 默认语言:中文
|
||||
|
||||
## 8. 待完成清单(上线前必须定稿)
|
||||
- 法务页面:隐私政策、服务条款、数据保留与删除说明。(已提供模板,建议上线前法务审核)
|
||||
114
docs/privacy.md
Normal file
114
docs/privacy.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# 隐私政策(示例模板)- ImageForge
|
||||
|
||||
> 提示:本文为通用模板,**不构成法律意见**。上线前建议由专业人士结合你的主体信息、所在地法律与实际数据流进行审核与调整。
|
||||
|
||||
最后更新:2025-12-18
|
||||
|
||||
---
|
||||
|
||||
## 1. 我们收集哪些信息
|
||||
|
||||
### 1.1 账号信息
|
||||
- 邮箱、用户名、密码哈希(不存明文)
|
||||
- 邮箱验证状态
|
||||
|
||||
### 1.2 使用与设备信息(日志/审计)
|
||||
- IP 地址、User-Agent、请求时间、接口路径、错误信息
|
||||
- 与安全、对账相关的审计记录(不包含明文密码、JWT、API Key)
|
||||
|
||||
### 1.3 计费信息
|
||||
- 订阅状态、发票与支付记录(通过 Stripe)
|
||||
- Stripe 返回的客户/订阅/支付标识(如 customer_id、subscription_id 等)
|
||||
|
||||
### 1.4 你上传的内容
|
||||
- 你上传的图片文件(用于压缩处理与结果下载)
|
||||
- 与图片相关的必要元信息(如格式、大小、压缩比例)
|
||||
|
||||
> 默认情况下我们会移除图片 EXIF 等元数据(定位/设备信息),除非你在压缩时明确选择保留。
|
||||
|
||||
---
|
||||
|
||||
## 2. 我们如何使用信息
|
||||
|
||||
我们使用上述信息用于:
|
||||
- 提供与改进图片压缩服务(生成压缩结果、任务状态、下载)
|
||||
- 账号与安全(登录、邮箱验证、密码重置、防滥用)
|
||||
- 计费与对账(订阅、发票、支付状态同步)
|
||||
- 客服与故障排查(定位问题、处理投诉与支持)
|
||||
|
||||
---
|
||||
|
||||
## 3. 我们如何共享信息
|
||||
|
||||
我们不会出售你的个人信息。我们可能在以下场景共享必要信息:
|
||||
- **Stripe**:用于订阅、支付与账单管理
|
||||
- **邮件服务商/SMTP**:用于发送验证邮件与密码重置邮件
|
||||
- **基础设施服务**(如对象存储/CDN):用于存储与分发压缩结果
|
||||
|
||||
我们仅在提供服务所必需范围内共享信息,并尽力要求第三方采取合理安全措施。
|
||||
|
||||
---
|
||||
|
||||
## 4. 数据存储与保留
|
||||
|
||||
### 4.1 图片与结果文件
|
||||
- 匿名试用与不同套餐有不同的保留期(例如:匿名 24 小时;Pro 7 天;Business 30 天)。
|
||||
- 过期后系统会自动删除任务与相关文件(可能存在一定延迟)。
|
||||
- 你可能可以在控制台手动删除任务/文件(如提供该功能)。
|
||||
|
||||
### 4.2 日志与审计
|
||||
出于安全与对账需要,我们可能会保留部分审计记录更长时间(并尽量脱敏)。
|
||||
|
||||
---
|
||||
|
||||
## 5. Cookies 与本地存储
|
||||
|
||||
我们可能使用 Cookie/本地存储用于:
|
||||
- 匿名试用会话(维持试用状态与配额计数)
|
||||
- 登录状态(如使用 Cookie 会话)
|
||||
- 安全控制(如 CSRF 防护)
|
||||
|
||||
你可以通过浏览器设置清除 Cookie,但这可能影响部分功能可用性。
|
||||
|
||||
---
|
||||
|
||||
## 6. 你的权利
|
||||
|
||||
你可以:
|
||||
- 访问与修改账号信息
|
||||
- 请求删除账号(如提供该功能)
|
||||
- 请求删除任务/文件(如提供该功能)
|
||||
|
||||
如需人工协助,请通过页面或邮箱联系我们。
|
||||
|
||||
---
|
||||
|
||||
## 7. 安全措施
|
||||
|
||||
我们采取合理措施保护数据安全,例如:
|
||||
- 密码使用安全算法哈希存储
|
||||
- API Key 不以明文存储
|
||||
- 访问控制、限流与审计日志
|
||||
|
||||
但互联网并非绝对安全,我们无法保证百分之百安全。
|
||||
|
||||
---
|
||||
|
||||
## 8. 未成年人
|
||||
|
||||
本服务不面向未成年人提供。如你是未成年人,请在监护人同意与指导下使用。
|
||||
|
||||
---
|
||||
|
||||
## 9. 本政策的变更
|
||||
|
||||
我们可能更新本隐私政策。重大变更会通过站内公告、邮件或其他方式提示。你继续使用服务即视为接受更新后的政策。
|
||||
|
||||
---
|
||||
|
||||
## 10. 联系方式
|
||||
|
||||
如对隐私政策有疑问或请求,请联系:
|
||||
- 邮箱:privacy@your-domain.com
|
||||
- 网站:https://your-domain.com
|
||||
|
||||
119
docs/security.md
Normal file
119
docs/security.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# 安全与风控设计 - ImageForge
|
||||
|
||||
目标:在“上传文件 + 对外 API + 计费”场景下,将最常见、最致命的安全与滥用风险前置到设计阶段,确保后续实现时有统一口径。
|
||||
|
||||
---
|
||||
|
||||
## 1. 威胁模型(摘要)
|
||||
|
||||
核心资产:
|
||||
- 用户账号、API Key、订阅与账单数据
|
||||
- 计算资源(CPU/内存/带宽/存储)与服务可用性
|
||||
- 用户上传图片(可能包含隐私/商业机密)
|
||||
|
||||
主要攻击面:
|
||||
- 上传入口(文件炸弹、DoS、恶意内容、路径/存储穿越)
|
||||
- 认证入口(撞库、弱密码、Token 泄露)
|
||||
- API Key(盗用、重放、暴力猜测)
|
||||
- Webhook(伪造事件、重放、乱序)
|
||||
- 管理后台(权限越权、配置投毒)
|
||||
|
||||
---
|
||||
|
||||
## 2. 认证与会话
|
||||
|
||||
### 2.1 用户登录
|
||||
- 密码哈希:`argon2id`(带独立 salt,参数可配置)。
|
||||
- 登录保护:基础限速 + 失败次数冷却;可选验证码(V1+)。
|
||||
- 账号状态:`is_active=false` 直接拒绝登录与 API。
|
||||
|
||||
### 2.2 JWT 使用建议
|
||||
- 对外 API:支持 Bearer Token(适合 CLI/SDK)。
|
||||
- 网站(Vue3):优先使用 HttpOnly Cookie 承载会话(降低 XSS 泄露风险),如使用 localStorage 必须配合严格 CSP。
|
||||
|
||||
---
|
||||
|
||||
## 3. API Key 安全
|
||||
|
||||
### 3.1 Key 生成与展示
|
||||
- Key 仅在创建时展示一次(前端明确提示“请立即保存”)。
|
||||
- Key 前缀(`key_prefix`)用于列表展示与快速检索。
|
||||
|
||||
### 3.2 Key 存储与校验
|
||||
推荐:`key_hash = HMAC-SHA256(full_key, API_KEY_PEPPER)`,只存 hash,不存明文。
|
||||
|
||||
理由:
|
||||
- 校验快,适合高 QPS;
|
||||
- pepper 作为服务器秘密(配置/密钥管理系统),泄露风险可控;
|
||||
- 避免 bcrypt/argon2 用在高频 key 校验导致性能瓶颈。
|
||||
|
||||
### 3.3 权限与限制
|
||||
- 最小权限:permissions(compress/batch/read_stats/billing_read 等)。
|
||||
- 支持禁用/轮换;可选 IP 白名单(Business/V1+)。
|
||||
- 每次请求记录 `last_used_at/last_used_ip/user_agent`(审计)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 上传与图片处理安全
|
||||
|
||||
### 4.1 输入校验
|
||||
- 只依赖扩展名不安全:必须校验魔数/探测真实格式。
|
||||
- 设定上限:
|
||||
- `max_file_size_mb`
|
||||
- `max_pixels`(宽×高)
|
||||
- `max_dimension`(单边)
|
||||
- 解码超时(Worker 层,避免卡死)
|
||||
|
||||
### 4.2 资源隔离
|
||||
- 压缩属 CPU 密集型:放到 Worker;API 只做编排与轻量校验。
|
||||
- Worker 限制并发:按“用户/套餐”与“全局”双维度控制。
|
||||
- 对异常图片:快速失败并记录审计与指标(格式错误/像素超限/解码失败)。
|
||||
|
||||
### 4.3 元数据(隐私)
|
||||
- 默认移除 EXIF(定位/设备信息),除非用户明确开启 `preserve_metadata=true`。
|
||||
- UI 必须清晰提示该开关的隐私含义。
|
||||
|
||||
---
|
||||
|
||||
## 5. 计费风控(防盗刷/滥用)
|
||||
|
||||
- **幂等**:`Idempotency-Key` 防止重试导致重复扣费。
|
||||
- **配额硬限制**:到达当期额度返回 `QUOTA_EXCEEDED`(HTTP 402)。
|
||||
- **匿名试用**:每日 10 次(成功文件数计),采用 **Cookie + IP** 双维度 Redis 计数做硬限制。
|
||||
- **异常检测**(告警即可,首期不必自动封禁):
|
||||
- 短时间内用量突增
|
||||
- 失败率异常升高(疑似 fuzzing/探测)
|
||||
- 单 Key 多 IP 快速切换
|
||||
|
||||
---
|
||||
|
||||
## 6. Webhook 安全
|
||||
|
||||
必须要求:
|
||||
- 验签(provider 签名 + webhook secret)。
|
||||
- 事件幂等:按 `provider_event_id` 去重。
|
||||
- 重放保护:记录 `received_at` 与处理状态,拒绝重复处理。
|
||||
- 最小暴露:webhook 路由不接受浏览器跨域调用,不返回敏感信息。
|
||||
|
||||
---
|
||||
|
||||
## 7. Web 安全(前端/网关)
|
||||
|
||||
### 7.1 HTTP 安全头(建议由 Nginx 设置)
|
||||
- `Strict-Transport-Security`
|
||||
- `Content-Security-Policy`(至少限制脚本来源;如用第三方支付跳转按需放开)
|
||||
- `X-Content-Type-Options: nosniff`
|
||||
- `Referrer-Policy`
|
||||
- `Permissions-Policy`
|
||||
|
||||
### 7.2 CORS 策略
|
||||
- 若前后端同域:尽量不启用宽松 CORS。
|
||||
- 若分离部署:CORS 白名单仅放行前端域名;对 `/webhooks/*` 禁止 CORS。
|
||||
|
||||
---
|
||||
|
||||
## 8. 数据安全与保留
|
||||
|
||||
- 结果保留期:按套餐(Free 24h、Pro 7d、Business 30d 等),匿名更短。
|
||||
- 支持用户主动删除任务/文件(立即删除对象存储 + DB 标记/审计)。
|
||||
- 审计日志留存与脱敏:保留必要字段(IP、UA、动作、对象 ID),避免写入明文密钥/Token。
|
||||
117
docs/terms.md
Normal file
117
docs/terms.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# 服务条款(示例模板)- ImageForge
|
||||
|
||||
> 提示:本文为通用模板,**不构成法律意见**。上线前建议由专业人士结合你的主体信息、所在地法律与实际业务流程进行审核与调整。
|
||||
|
||||
最后更新:2025-12-18
|
||||
|
||||
---
|
||||
|
||||
## 1. 接受条款
|
||||
|
||||
欢迎使用 ImageForge(下称“本服务”)。当你访问或使用本服务(包括网站与 API)时,即表示你已阅读、理解并同意受本条款约束。如果你不同意本条款,请停止使用本服务。
|
||||
|
||||
---
|
||||
|
||||
## 2. 服务内容
|
||||
|
||||
本服务提供图片压缩与格式转换能力,包括但不限于:
|
||||
- 网站上传压缩与批量压缩(含匿名试用)
|
||||
- 开发者 API(API Key 调用)
|
||||
- 订阅与计费(Stripe)
|
||||
- 任务历史、下载与结果存储(按保留期自动清理)
|
||||
|
||||
我们可能随时调整服务功能、参数与限制(例如文件大小、并发、速率、保留期等),并在合理范围内进行公告或提示。
|
||||
|
||||
---
|
||||
|
||||
## 3. 账号与安全
|
||||
|
||||
3.1 你可能需要注册账号才能使用部分功能。你应提供真实、准确、完整的信息并及时更新。
|
||||
3.2 你应妥善保管账号凭据与 API Key。因你保管不善导致的损失由你自行承担。
|
||||
3.3 我们可能对异常登录、滥用行为采取限制、冻结或终止服务措施。
|
||||
|
||||
---
|
||||
|
||||
## 4. 匿名试用与限制
|
||||
|
||||
4.1 匿名试用用于体验本服务,存在使用限制(例如:每日次数、文件大小、批量数量、保留期等)。
|
||||
4.2 我们可基于安全与风控原因随时调整匿名试用策略或停止匿名试用。
|
||||
|
||||
---
|
||||
|
||||
## 5. API 使用与开发者责任
|
||||
|
||||
5.1 开发者 API 仅对符合条件的用户开放(例如:Pro/Business 套餐)。
|
||||
5.2 你不得绕过鉴权、限流、配额或其他安全控制。
|
||||
5.3 你应在你的产品中向最终用户提供必要的告知(如你上传处理了用户图片、保留期、隐私政策等)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 计费、订阅与退款
|
||||
|
||||
6.1 本服务通过 Stripe 提供订阅计费能力。你在购买订阅时应确认价格、周期、包含额度、超额策略与取消规则。
|
||||
6.2 **硬配额**:当期额度耗尽后,新增压缩请求将被拒绝(可能返回 `402 QUOTA_EXCEEDED`)。
|
||||
6.3 取消订阅通常在当前计费周期结束后生效。
|
||||
6.4 退款政策:可根据你的实际运营策略另行约定(建议在上线前补充清晰规则)。
|
||||
|
||||
---
|
||||
|
||||
## 7. 内容与知识产权
|
||||
|
||||
7.1 你上传的图片及其相关权利归你或权利人所有。你应确保你拥有上传、处理该内容的合法权利。
|
||||
7.2 你授予我们在提供本服务所必需范围内处理、存储、传输该内容的许可(例如生成压缩结果、提供下载链接、用于排障日志中的必要元信息)。
|
||||
7.3 本服务的软件、界面、商标、文档等知识产权归我们或权利人所有,除非另有明确授权。
|
||||
|
||||
---
|
||||
|
||||
## 8. 禁止行为
|
||||
|
||||
你不得利用本服务进行包括但不限于以下行为:
|
||||
- 上传或传播违法、侵权、恶意内容
|
||||
- 使用自动化方式进行超出合理范围的抓取、压测、滥用
|
||||
- 试图入侵、绕过鉴权、伪造请求或篡改数据
|
||||
- 传播病毒、木马或其他破坏性代码
|
||||
- 违反适用法律法规或本条款的其他行为
|
||||
|
||||
---
|
||||
|
||||
## 9. 数据保留与删除
|
||||
|
||||
9.1 压缩结果与任务记录会按你的套餐/设置保留一定时间,过期后自动删除。
|
||||
9.2 你可能可以在控制台手动删除任务/文件(如提供该功能)。
|
||||
9.3 与服务安全、对账、审计相关的必要日志可能会保留更长时间(并进行脱敏处理)。
|
||||
|
||||
---
|
||||
|
||||
## 10. 免责声明
|
||||
|
||||
10.1 本服务按“现状”提供。我们将尽力提供稳定服务,但不对完全无故障、无中断作出承诺。
|
||||
10.2 由于网络、第三方服务(如 Stripe、邮件服务商、对象存储)等原因造成的延迟或失败,我们将在合理范围内协助处理,但不承担超出法律允许范围的责任。
|
||||
|
||||
---
|
||||
|
||||
## 11. 责任限制
|
||||
|
||||
在适用法律允许的范围内,我们对因使用或无法使用本服务所导致的间接损失、利润损失、数据丢失等不承担责任。若法律要求承担责任,我们的责任上限可按你最近一个计费周期实际支付金额(或合理上限)计算(具体上限可按你运营策略补充)。
|
||||
|
||||
---
|
||||
|
||||
## 12. 终止
|
||||
|
||||
12.1 你可以停止使用本服务并按指引取消订阅。
|
||||
12.2 如你违反本条款或存在风险行为,我们可暂停或终止向你提供服务,并保留追究责任的权利。
|
||||
|
||||
---
|
||||
|
||||
## 13. 条款变更
|
||||
|
||||
我们可能会更新本条款。重大变更将通过站内公告、邮件或其他方式提示。你继续使用服务即视为接受更新后的条款。
|
||||
|
||||
---
|
||||
|
||||
## 14. 联系方式
|
||||
|
||||
如对本条款有疑问,请联系:
|
||||
- 邮箱:support@your-domain.com
|
||||
- 网站:https://your-domain.com
|
||||
|
||||
134
docs/ui.md
Normal file
134
docs/ui.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# UI/UX 设计文档 - ImageForge
|
||||
|
||||
目标:做一个接近商用的体验(简单、可信、可解释),同时覆盖“网站压缩 + 开发者 API + 计费”三条主线。
|
||||
|
||||
---
|
||||
|
||||
## 1. 设计原则
|
||||
|
||||
1) **一眼可用**:首页即工具,不把用户“逼进登录”才能体验。
|
||||
2) **结果可信**:清晰展示“压缩前后对比、节省多少、是否去除元数据、链接多久过期”。
|
||||
3) **开发者友好**:API Key、用量、示例代码和错误处理在控制台里一站式找到。
|
||||
4) **商用闭环**:升级/取消/发票/支付状态明确,避免“扣费但不知道扣了什么”。
|
||||
|
||||
---
|
||||
|
||||
## 2. 信息架构(站点地图)
|
||||
|
||||
### 2.1 公共区域(无需登录)
|
||||
- `/` 首页(压缩工具)
|
||||
- `/pricing` 价格页(套餐对比、FAQ)
|
||||
- `/docs` 开发者文档入口(API 概览、SDK、示例)
|
||||
- `/login` `/register`
|
||||
- `/terms` `/privacy`
|
||||
|
||||
### 2.2 用户控制台(需要登录)
|
||||
- `/dashboard` 概览(当期用量、套餐、最近任务)
|
||||
- `/dashboard/history` 历史任务
|
||||
- `/dashboard/api-keys` API Key 管理 + 用量头说明
|
||||
- `/dashboard/billing` 订阅与发票
|
||||
- `/dashboard/settings` 账号设置(密码、删除账号等)
|
||||
|
||||
### 2.3 管理后台(管理员)
|
||||
- `/admin` 概览(QPS、错误率、队列长度、订阅状态分布)
|
||||
- `/admin/users` 用户管理(冻结、限流覆盖、赠送额度)
|
||||
- `/admin/tasks` 任务管理(取消/重试、失败原因)
|
||||
- `/admin/billing` 订阅/发票/支付事件(Webhook)排查
|
||||
- `/admin/config` 全局配置(开关、限流、文件限制、保留期默认)
|
||||
|
||||
---
|
||||
|
||||
## 3. 关键页面规格(线框级)
|
||||
|
||||
### 3.1 首页(压缩工具)
|
||||
核心组件:
|
||||
- 顶栏:Logo + `/pricing` `/docs` + 登录/注册(登录后显示头像菜单)
|
||||
- 上传区:拖拽 + 点击选择 + 限制提示(格式/大小/数量)
|
||||
- 参数区:压缩率、尺寸限制、元数据开关(输出格式保持原图)
|
||||
- 结果区:文件列表(缩略图、大小、节省%、状态、下载/重试)
|
||||
- 汇总区:总节省、下载 ZIP、清空
|
||||
- 信任区:隐私说明(默认去 EXIF)、保留期说明、状态页/联系入口
|
||||
|
||||
交互要点:
|
||||
- 上传后立即生成本地缩略图与原始大小;压缩进度逐文件显示。
|
||||
- 默认提示“匿名试用:每日 10 次”,登录后提示“升级解锁更大额度/保留期”。
|
||||
|
||||
### 3.2 价格页(Pricing)
|
||||
结构建议:
|
||||
- Hero:一句话价值 + CTA(开始压缩 / 查看 API)
|
||||
- 套餐卡片:Free / Pro / Business(三列)
|
||||
- 对比表:文件大小/批量上限/保留期/Webhook/团队
|
||||
- FAQ:计量单位、超额策略、退款、隐私与保留
|
||||
|
||||
### 3.3 控制台概览(Dashboard)
|
||||
上半区(KPI 卡片):
|
||||
- 当期已用/剩余(进度条)
|
||||
- 节省流量累计(本周期/总计)
|
||||
- API 调用数(本周期)
|
||||
- 当前套餐与到期时间
|
||||
|
||||
下半区:
|
||||
- 最近任务列表(状态、文件数、节省、操作)
|
||||
- 升级提示(当剩余额度 < 20%)
|
||||
|
||||
### 3.4 API Key 管理
|
||||
列表字段:
|
||||
- 名称、前缀、权限、限流、最后使用时间/IP、状态(启用/禁用)
|
||||
|
||||
创建/轮换:
|
||||
- 创建时弹窗展示一次完整 Key(支持“一键复制”)
|
||||
- 轮换说明:首期默认“立即轮换”(生成新 Key 并立即禁用旧 Key),避免双 Key 过渡带来的复杂度
|
||||
|
||||
开发者引导:
|
||||
- 显示 curl 示例(调用 `POST /compress/direct`)
|
||||
- 显示常见错误码(`QUOTA_EXCEEDED`、`RATE_LIMITED`)与重试策略(Idempotency-Key)
|
||||
|
||||
### 3.5 订阅与发票(Billing)
|
||||
模块:
|
||||
- 当前订阅:套餐、周期、状态(active/past_due)、升级/取消按钮
|
||||
- 发票列表:编号、金额、状态、周期、下载/跳转支付(provider)
|
||||
- 支付方式入口:跳转客户 Portal(如 Stripe portal)
|
||||
|
||||
### 3.6 历史任务(History)
|
||||
筛选:
|
||||
- 时间范围、状态、来源(web/api)、格式、压缩模式
|
||||
|
||||
列表:
|
||||
- 任务创建时间、文件数、节省、到期时间、下载 ZIP、删除(隐私)
|
||||
|
||||
---
|
||||
|
||||
## 4. 视觉与组件规范(建议)
|
||||
|
||||
### 4.1 设计风格
|
||||
- 清爽、留白、强调数据对比(节省% 是视觉重点)
|
||||
- 状态色:成功/警告/失败明确
|
||||
|
||||
### 4.2 主题变量(示例)
|
||||
沿用 `docs/frontend.md` 的 CSS 变量,并补充:
|
||||
- `--info`、`--border`、`--bg`、`--text` 等
|
||||
- 暗色模式(V1+)
|
||||
|
||||
### 4.3 组件清单(公共)
|
||||
- `AppLayout` / `AuthLayout` / `AdminLayout`
|
||||
- `DropZone`、`FileCard`、`OptionsPanel`
|
||||
- `UsageMeter`、`PlanCard`、`InvoiceTable`
|
||||
- `Toast`、`Modal`、`Skeleton`、`EmptyState`
|
||||
|
||||
---
|
||||
|
||||
## 5. 关键文案(必须在 UI 中出现)
|
||||
|
||||
- 元数据:默认提示“默认会移除 EXIF 等元数据(定位/设备信息)”
|
||||
- 保留期:下载链接到期时间(例如“24 小时后自动删除”)
|
||||
- 计量:说明“成功压缩 1 个文件计 1 次”
|
||||
- 错误:配额不足/限流时给出“如何解决”(登录/升级/稍后重试)
|
||||
|
||||
---
|
||||
|
||||
## 6. 可访问性与体验细节
|
||||
|
||||
- 键盘可达:上传区、弹窗、按钮可 Tab 导航
|
||||
- 颜色对比:状态色满足可读性
|
||||
- 大文件/批量:明确“后台处理中,可关闭页面稍后回来”
|
||||
- 移动端:首页只保留必要参数,高级参数折叠
|
||||
Reference in New Issue
Block a user