Implement compression quota refunds and admin manual subscription
This commit is contained in:
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 加速下载
|
||||
Reference in New Issue
Block a user