Implement compression quota refunds and admin manual subscription

This commit is contained in:
2025-12-19 23:28:32 +08:00
commit 11f48fd3dd
106 changed files with 27848 additions and 0 deletions

685
docs/api.md Normal file
View File

@@ -0,0 +1,685 @@
# API 接口文档v1- ImageForge
面向两类使用者:
- **网站Web**:上传/批量/历史/账单等(可能包含匿名试用)。
- **对外 APIDeveloper 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 或 SSESSE 更易穿透代理)。当前先保留 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
View 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
View 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
View 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+800:00 重置;采用 Cookie + IP 双限制。
3) **批量任务遇到额度不足时的行为**
- 当前写法:`POST /compress/batch` 若本周期剩余单位不足以覆盖上传文件数,直接返回 `402`,不创建任务。
4) **默认套餐参数(可改)**
- Free500 / 月5MB 单文件10/批量,保留 24h
- Pro10,000 / 订阅周期20MB 单文件50/批量,保留 7 天
- Business100,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
View 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
View 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
# 可选MinIOS3 兼容,本地开发更接近生产)
# 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
View 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 时效控制
- 邮箱验证 Token24 小时有效
- 密码重置 Token1 小时有效
### 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
View 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/icoGIF 仅静态)
- 文件大小与数量(按匿名/登录/套餐提示不同上限)
- 匿名试用:每日 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避免 localStorageXSS 风险),优先 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
View 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建议起点
- APIP95 < 300ms不含压缩直返、错误率 < 0.5%
- Worker队列积压 < N按规模定义失败率 < 1%
### 4.2 告警
可用性:
- `http 5xx` 激增
- `/health` 探活失败
队列健康:
- pending/inflight 持续上升
- 单任务耗时异常增长
计费正确性:
- webhook 处理失败
- 订阅状态异常active->incomplete 回退等)
滥用风险:
- 单 key/单 IP 用量突增
- 格式探测失败率异常

162
docs/prd.md Normal file
View 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 对外 APIDeveloper 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 + SDKTS/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
View 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
View 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 权限与限制
- 最小权限permissionscompress/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 密集型:放到 WorkerAPI 只做编排与轻量校验。
- 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
View File

@@ -0,0 +1,117 @@
# 服务条款(示例模板)- ImageForge
> 提示:本文为通用模板,**不构成法律意见**。上线前建议由专业人士结合你的主体信息、所在地法律与实际业务流程进行审核与调整。
最后更新2025-12-18
---
## 1. 接受条款
欢迎使用 ImageForge下称“本服务”。当你访问或使用本服务包括网站与 API即表示你已阅读、理解并同意受本条款约束。如果你不同意本条款请停止使用本服务。
---
## 2. 服务内容
本服务提供图片压缩与格式转换能力,包括但不限于:
- 网站上传压缩与批量压缩(含匿名试用)
- 开发者 APIAPI 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
View 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 导航
- 颜色对比:状态色满足可读性
- 大文件/批量:明确“后台处理中,可关闭页面稍后回来”
- 移动端:首页只保留必要参数,高级参数折叠