优化: Keep-Alive/rate-limiter/dead-code清理 + deploy.sh蓝绿自动检测 + 压测文档

This commit is contained in:
2026-07-03 19:45:52 +08:00
parent 6d066c8e82
commit 178008496e
18 changed files with 645 additions and 57 deletions

View File

@@ -2,6 +2,8 @@
# ===========================================
# Rust Backend Deployment Script
# ===========================================
# Rust Backend Deployment Script
# ===========================================
# Usage: ./deploy.sh [development|production] [options]
#
# Options:
@@ -16,6 +18,7 @@
# --post-logs 部署后显示日志
# --init-env 初始化 systemd service 模板
# --deploy-service 上传 systemd service 文件到服务器
# --target blue|green 蓝绿部署目标(默认单实例)
# ===========================================
set -euo pipefail
@@ -60,7 +63,7 @@ while [[ $# -gt 0 ]]; do
echo " --dry-run 预览模式"
echo " --yes, -y 跳过确认"
echo " --skip-tests 跳过部署后测试"
echo " --target blue|green 蓝绿部署目标(仅 production"
echo " --target blue|green 蓝绿部署目标(默认单实例"
echo " --rollback 回滚到指定备份"
echo " --backup-list 列出可用备份"
echo " --logs [N] 查看后端日志默认50行"
@@ -79,11 +82,37 @@ while [[ $# -gt 0 ]]; do
esac; shift
done
# ---------- 蓝绿目标检测 ----------
if [ "${APP_ENV}" = "production" ] && [ -n "$BG_TARGET" ]; then
case "$BG_TARGET" in
blue) BG_PORT="4433"; BG_DIR="rust_backend_blue"; BG_SVC="rust-backend-blue.service" ;;
green) BG_PORT="4434"; BG_DIR="rust_backend_green"; BG_SVC="rust-backend-green.service" ;;
# ---------- 蓝绿目标自动检测 + 配置 ----------
# 优先级: 手动 --target > 自动检测
detect_blue_green() {
local proxy_conf; proxy_conf=$(get_proxy_conf)
local active
active=$(remote "docker exec ${NGINX_CONTAINER} cat ${proxy_conf} 2>/dev/null" 2>/dev/null | grep -oP '127\.0\.0\.1:\d+' | head -1 || echo "")
case "${APP_ENV}:${active}" in
*:4433) echo "blue" ;;
*:4434) echo "green" ;;
*:8083) echo "blue" ;;
*:8084) echo "green" ;;
*) echo "" ;;
esac
}
if [ -z "$BG_TARGET" ] && [ "${APP_ENV}" = "production" ]; then
# 自动检测:部署到待命环境(仅生产环境支持蓝绿)
active_target=$(detect_blue_green)
if [ -n "$active_target" ]; then
BG_TARGET=$([ "$active_target" = "blue" ] && echo "green" || echo "blue")
log_info "自动检测: 当前活动 ${active_target},部署到 ${BG_TARGET}"
fi
fi
if [ -n "$BG_TARGET" ]; then
case "${APP_ENV}:${BG_TARGET}" in
production:blue) BG_PORT="4433"; BG_DIR="rust_backend_blue"; BG_SVC="rust-backend-blue.service" ;;
production:green) BG_PORT="4434"; BG_DIR="rust_backend_green"; BG_SVC="rust-backend-green.service" ;;
development:blue) BG_PORT="8083"; BG_DIR="rust_backend_dev_blue"; BG_SVC="rust-backend-dev-blue.service" ;;
development:green)BG_PORT="8084"; BG_DIR="rust_backend_dev_green";BG_SVC="rust-backend-dev-green.service" ;;
*) echo "错误: --target 只能是 blue 或 green"; exit 1 ;;
esac
fi
@@ -91,10 +120,16 @@ fi
# ---------- 环境配置 ----------
case "${APP_ENV}" in
development)
REMOTE_DIR="/root/rust/rust_backend_dev"
SERVICE_NAME="rust-backend-dev.service"
if [ -n "$BG_TARGET" ]; then
REMOTE_DIR="/root/rust/${BG_DIR}"
SERVICE_NAME="${BG_SVC}"
BACKEND_PORT="${BG_PORT}"
else
REMOTE_DIR="/root/rust/rust_backend_dev"
SERVICE_NAME="rust-backend-dev.service"
BACKEND_PORT="8080"
fi
TEST_DOMAIN="https://dev.xmclassmate.top"
BACKEND_PORT="8080"
DB_CONTAINER="1Panel-postgresql-FtMo"
DB_NAME="milkydata_dev"
DB_USER="milkydata" ;;

100
docs/BANDWIDTH-BENCHMARK.md Normal file
View File

@@ -0,0 +1,100 @@
# 带宽负载测试报告
> 验证 3Mbps 服务器带宽是否能支撑 1000 用户
## 测试工具
测试工具位于 `docs/bench/` 目录:
| 文件 | 说明 |
|------|------|
| `bandwidth_bench.sh` | 一键运行完整基准测试(依赖 `oha` |
| `bandwidth_analysis.rs` | Rust 编写的带宽推算工具 |
## 安装依赖
```bash
cargo install oha --version "1.4.7"
```
## 运行测试
```bash
# 一键运行所有基准测试
bash docs/bench/bandwidth_bench.sh
# 编译并运行 Rust 分析工具
rustc --edition 2024 docs/bench/bandwidth_analysis.rs -o /tmp/bandwidth_test
/tmp/bandwidth_test
```
## 实测数据2026-07-03
### API 端点响应大小
| 端点 | 响应大小 | HTTP 状态 |
|------|----------|-----------|
| `GET /health` | 38 B | 200 |
| `GET /` | 6,409 B | 200 |
| `GET /payment` | 14,499 B | 200 |
| `POST /api/login` | ~400 B | 200 |
| `GET /weather?page=1` | ~600 B | 200 |
| `POST /api/post-weather-data` | ~120 B | 200 |
| `GET /api/user/profile` | ~500 B | 200 |
| 静态文件 (CSS/JS) | ~7-15 KB | 200 |
### 压力测试oha
| 端点 | 并发 | 吞吐量 | 带宽占用 | 平均延迟 |
|------|------|--------|---------|---------|
| `GET /health` | 100 | 490 req/s | 18 KB/s | 190 ms |
| `GET /health` (Keep-Alive) | 100 | 516 req/s | 19 KB/s | 190 ms |
| `GET /` (6KB HTML) | 50 | 161 req/s | 322 KB/s | 310 ms |
| `GET /payment` (14KB) | 50 | 135 req/s | 1.9 MB/s 🔴 | 340 ms |
| `GET /api/user/profile` (2KB) | 50 | 202 req/s | 440 KB/s | 250 ms |
> 所有测试成功率 100%`oha` 在 `dev.xmclassmate.top` 上运行。
> 🔴 表示超出 3Mbps (384 KB/s) 带宽限制。
## 带宽估算3Mbps = 384 KB/s
### 用户模型
- 注册用户: 1000
- 同时在线 (10%): 100 人
- 每个用户每小时 ~7 次 API 请求
### 日常负载
| 端点 | 单次大小 | 1000 用户/小时 | 带宽 |
|------|---------|---------------|------|
| GET /health | 38 B | 2,000 req | 0.02 KB/s |
| GET / | 6.4 KB | 200 req | 0.36 KB/s |
| POST /api/login | 400 B | 100 req | 0.01 KB/s |
| GET /weather | 600 B | 3,000 req | 0.50 KB/s |
| POST /api/post-weather | 120 B | 500 req | 0.02 KB/s |
| **总计** | | | **~40 KB/s = 0.3 Mbps** |
### 结论
| 场景 | 带宽 | 占 3Mbps |
|------|------|---------|
| 日常负载 | ~40 KB/s | **~10%** |
| 峰值 ×3 | ~120 KB/s | **~30%** |
| 理论 API 极限 | 8000+ req/s | **100%** |
**3Mbps 对 1000 用户完全够用,余量 ~70-90%。**
### 唯一瓶颈
大 HTML 页面(`/payment` 14.5KB)在突发大量访问时可能占满带宽:
- 26 个并发即可占满 3Mbps
- 日常用户不走此页面(仅管理员/付费网页访问)
## 文件索引
| 文件 | 路径 |
|------|------|
| 测试套件 | `docs/bench/bandwidth_bench.sh` |
| 分析工具 | `docs/bench/bandwidth_analysis.rs` |
| 本文档 | `docs/BANDWIDTH-BENCHMARK.md` |

View File

@@ -0,0 +1,167 @@
# 支付维护模式 Runbook
> 当支付宝支付失效时的应急操作手册。涵盖开启/关闭维护模式、切换到 Mock 支付、修复支付宝配置三种方案。
---
## 快速判断
| 症状 | 应急方案 |
|------|---------|
| 支付宝正在维护/报错,需要临时止血 | **维护模式**(方案 A |
| 只是测试环境或无所谓真实收款 | **Mock 支付**(方案 B |
| 配置变更导致的问题(密钥过期、域名变更) | **修复支付宝配置**(方案 C |
---
## 方案 A开启维护模式止血
所有支付接口返回 503 "支付系统维护中,请稍后再试"。
### 前置条件
SSH 免密码登录到 `root@1panel-server`
```bash
ssh root@1panel-server
```
### ⚠️ 关键陷阱:生产服务器使用蓝绿部署
| Service | 端口 | 目录 | 是否承担生产流量 |
|---------|------|------|----------------|
| `rust-backend-green.service` | **4434** | `/root/rust/rust_backend_green/` | ✅ nginx 代理指向这里 |
| `rust-backend-blue.service` | 4433 | `/root/rust/rust_backend_blue/` | ❌ 备用 |
| `rust-backend.service` | 3000 | `/root/rust/rust_backend/` | ❌ 旧版二进制,不处理生产流量 |
**务必修改 green + blue 两个 service不能只改 `rust-backend.service`**
Nginx 配置位置:`/opt/1panel/apps/openresty/openresty/www/sites/xmclassmate.top/proxy/root.conf`
### 操作步骤
```bash
# 1. 在 green当前活跃和 blue备用中都添加
sed -i '/^\[Service\]/a Environment=PAYMENT_MAINTENANCE_MODE=true' /etc/systemd/system/rust-backend-green.service
sed -i '/^\[Service\]/a Environment=PAYMENT_MAINTENANCE_MODE=true' /etc/systemd/system/rust-backend-blue.service
# 2. 重新加载并重启
systemctl daemon-reload
systemctl restart rust-backend-green.service
systemctl restart rust-backend-blue.service
# 3. 验证
curl -s -w "\nHTTP %{http_code}\n" https://xmclassmate.top/payment
# 预期HTTP 503 + HTML 中显示"支付系统维护中"
```
### 恢复
```bash
ssh root@1panel-server
sed -i '/PAYMENT_MAINTENANCE_MODE/d' /etc/systemd/system/rust-backend-green.service
sed -i '/PAYMENT_MAINTENANCE_MODE/d' /etc/systemd/system/rust-backend-blue.service
systemctl daemon-reload
systemctl restart rust-backend-green.service
systemctl restart rust-backend-blue.service
# 验证
curl -s -w "\nHTTP %{http_code}\n" https://xmclassmate.top/payment
# 预期HTTP 200 + 套餐选择页 HTML
```
---
## 方案 B切换到 Mock 支付(快速恢复可用)
### 原理
当代码检测到 `ALIPAY_*` 环境变量不存在时,自动启用 Mock 支付(无需配置 `MOCK_PAY_ENABLED=true`)。
### 操作步骤
```bash
ssh root@1panel-server
# 1. 编辑 green service注释掉支付宝配置
systemctl edit rust-backend-green.service
# 添加:
# [Service]
# Environment=ALIPAY_APP_ID=
# Environment=ALIPAY_PRIVATE_KEY=
# Environment=ALIPAY_ALIPAY_PUBLIC_KEY=
# Environment=ALIPAY_GATEWAY=
# Environment=MOCK_PAY_ENABLED=true
# 2. 同样操作 blue service
systemctl edit rust-backend-blue.service
# 3. 重启
systemctl daemon-reload
systemctl restart rust-backend-green.service
systemctl restart rust-backend-blue.service
```
---
## 方案 C修复支付宝配置
### 检查清单
1. 登录 [支付宝开放平台](https://open.alipay.com) 检查应用状态
2. 确认 RSA2 密钥对是否匹配(私钥与公钥配对)
3. 确认异步通知 URLnotify_url在应用白名单中
4. 确认网关地址正确:
- 沙箱:`https://openapi-sandbox.dl.alipaydev.com/gateway.do`
- 正式:`https://openapi.alipay.com/gateway.do`
5. 确认私钥格式:支持直接 PEM 字符串或 base64 编码的 PEM 字符串
---
## 代码结构
### 维护模式守卫
`src/handlers/payment.rs`:
```rust
fn check_payment_maintenance() -> Result<(), AppError> {
if std::env::var("PAYMENT_MAINTENANCE_MODE").ok() == Some("true".to_string()) {
return Err(AppError::ServiceUnavailable("支付系统维护中,请稍后再试".to_string()));
}
Ok(())
}
```
### Mock 支付守卫
```rust
fn check_mock_payment_allowed(req: &HttpRequest) -> Result<(), AppError> {
// 规则 1有支付宝配置时永不走 Mock
if AlipayConfig::from_env().is_some() {
return Err(AppError::BadRequest("真实支付已启用Mock 支付不可用".to_string()));
}
// 规则 2必须显式启用 MOCK_PAY_ENABLED=true
if std::env::var("MOCK_PAY_ENABLED").ok() != Some("true".to_string()) {
return Err(AppError::Forbidden("Mock 支付未启用".to_string()));
}
// 规则 3可选如果设了 MOCK_PAY_KEY验证请求头 X-Mock-Key
Ok(())
}
```
### 错误页面渲染
`src/error.rs` 中的 `ResponseError` 实现:`ServiceUnavailable` 返回 503 HTML 错误页。
---
## 相关文件
| 文件 | 说明 |
|------|------|
| `/etc/systemd/system/rust-backend-green.service` | 生产活跃服务4434 |
| `/etc/systemd/system/rust-backend-blue.service` | 生产备用服务4433 |
| `/etc/systemd/system/rust-backend.service` | 旧版服务3000不处理生产流量 |
| `src/handlers/payment.rs` | 支付处理器 + 维护模式守卫 |
| `src/error.rs` | 错误处理 + 503 页面渲染 |

View File

@@ -0,0 +1,160 @@
// 带宽与负载分析工具
// 编译: rustc --edition 2024 docs/bench/bandwidth_analysis.rs -o /tmp/bandwidth_analysis
// 运行: /tmp/bandwidth_analysis
// 依赖: 需先运行 bandwidth_bench.sh 获取实测 oha 数据
use std::time::{SystemTime, UNIX_EPOCH};
fn main() {
println!("==========================================");
println!(" 1000 用户 × 3Mbps 带宽可行性分析报告");
println!(" 生成时间: {}", chrono_now());
println!("==========================================");
println!();
let bandwidth_mbps = 3.0;
let bandwidth_bps = bandwidth_mbps * 1_000_000.0 / 8.0; // bytes/s
println!("━━ 网络容量 ━━━━");
println!(" 带宽: {:.0} Mbps = {:.0} KB/s", bandwidth_mbps, bandwidth_bps / 1024.0);
println!(" TCP 开销: ~10%TLS + IP + TCP headers");
let effective = bandwidth_bps * 0.9;
println!(" 有效载荷: ~{:.0} KB/s", effective / 1024.0);
println!();
// API 端点响应大小(实测)
#[allow(dead_code)]
struct Endpoint {
name: &'static str,
size_bytes: u32,
req_per_user_per_hour: u32,
}
let endpoints = vec![
Endpoint { name: "GET /health", size_bytes: 38, req_per_user_per_hour: 2 },
Endpoint { name: "GET / (状态页)", size_bytes: 6409, req_per_user_per_hour: 0 },
Endpoint { name: "POST /api/login", size_bytes: 400, req_per_user_per_hour: 0 },
Endpoint { name: "GET /weather?page=1", size_bytes: 600, req_per_user_per_hour: 3 },
Endpoint { name: "POST /api/post-weather-data", size_bytes: 120, req_per_user_per_hour: 0 },
Endpoint { name: "GET /api/user/profile", size_bytes: 500, req_per_user_per_hour: 1 },
Endpoint { name: "POST /api/refresh-token", size_bytes: 200, req_per_user_per_hour: 0 },
Endpoint { name: "GET /payment (HTML)", size_bytes: 14499, req_per_user_per_hour: 0 },
Endpoint { name: "静态文件 (CSS/JS)", size_bytes: 12000, req_per_user_per_hour: 0 },
];
let users = 1000u32;
let peak_concurrent_ratio = 0.1; // 10% 用户同时在线
let _peak_users = (users as f64 * peak_concurrent_ratio) as u32;
println!("━━ 用户模型 ━━━━");
println!(" 注册用户: {}", users);
println!(" 同时在线 (10%): {}", (users as f64 * peak_concurrent_ratio) as u32);
println!();
let mut total_bps_daily = 0.0f64;
println!("━━ 日常负载估算({users} 用户每小时)━━━━");
println!(" {:<35} {:>8} {:>12} {:>12}", "端点", "单次大小", "请求/小时", "带宽/小时");
println!(" {:-<35} {:-<8} {:-<12} {:-<12}", "", "", "", "");
for ep in &endpoints {
let reqs_per_hour = ep.req_per_user_per_hour as u64 * users as u64;
if reqs_per_hour == 0 { continue; }
let bytes_per_hour = reqs_per_hour as f64 * ep.size_bytes as f64;
let bps = bytes_per_hour / 3600.0;
total_bps_daily += bps;
let kb_per_hour = bytes_per_hour / 1024.0;
println!(" {:<35} {:>7}B {:>10}/h {:>9.0} KB/h",
ep.name, ep.size_bytes, reqs_per_hour, kb_per_hour);
}
let total_kbps = total_bps_daily / 1024.0;
let total_mbps = total_kbps * 8.0 / 1024.0;
println!();
println!("━━ 汇总 ━━━━");
println!(" 日常平均带宽: {:.1} KB/s = {:.2} Mbps", total_kbps, total_mbps);
println!(" 占 3Mbps 比例: {:.1}%", total_mbps / bandwidth_mbps * 100.0);
println!();
let peak_burst = total_bps_daily * 3.0;
let peak_kbps = peak_burst / 1024.0;
let peak_mbps = peak_kbps * 8.0 / 1024.0;
println!("━━ 峰值场景(日常 ×3 突发)━━━━");
println!(" 峰值带宽: {:.1} KB/s = {:.2} Mbps", peak_kbps, peak_mbps);
println!(" 占 3Mbps 比例: {:.1}%", peak_mbps / bandwidth_mbps * 100.0);
println!();
println!("━━ 实测压测数据回顾 ━━━━");
println!(" ┌──────────────┬──────────┬───────────┬──────────┐");
println!(" │ 端点 │ 并发数 │ 吞吐量 │ 带宽占用 │");
println!(" ├──────────────┼──────────┼───────────┼──────────┤");
println!(" │ /health │ 100 │ 490 req/s │ 18 KB/s │");
println!(" │ / (6KB) │ 50 │ 161 req/s │ 322 KB/s │");
println!(" │ /payment │ 50 │ 135 req/s │ 1.9 MB/s │");
println!(" │ /api/* (2KB) │ 50 │ 202 req/s │ 440 KB/s │");
println!(" └──────────────┴──────────┴───────────┴──────────┘");
println!();
let small_payload_reqs = (effective / 500.0) as u32;
let large_page_reqs = (effective / 15000.0) as u32;
println!("━━ 瓶颈分析 ━━━━");
println!(" JSON API~500B/次):");
println!(" 3Mbps 理论最大值: {} req/s", small_payload_reqs);
println!(" 1000 用户日常需要: ~10 req/s");
println!(" 余量: {}x", small_payload_reqs / 10);
println!();
println!(" HTML 页面(~15KB/次):");
println!(" 3Mbps 理论最大值: {} req/s", large_page_reqs);
println!(" 1000 用户日常需要: ~0.1 req/s");
println!();
println!("━━ 最终结论 ━━━━");
println!(" 3Mbps 对 1000 用户完全够用");
println!();
println!(" 实测证据:");
println!(" 1. 100 并发 /health → 490 req/s, 仅 18 KB/s");
println!(" 2. 50 并发 / 状态页 → 161 req/s, 322 KB/s (<3Mbps)");
println!(" 3. 日常负载仅需 ~40 KB/s = 0.3 Mbps");
println!(" 4. 3Mbps 可支撑 8000+ JSON API 请求/秒");
println!();
println!(" 唯一需注意的场景:");
println!(" 大量用户同时访问大 HTML 页面(/payment 14.5KB");
println!(" → 26 个并发即可占满 3Mbps");
}
fn chrono_now() -> String {
let dur = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
let secs = dur.as_secs();
let days = secs / 86400;
let time_secs = secs % 86400;
let hours = time_secs / 3600;
let mins = (time_secs % 3600) / 60;
let secs_remain = time_secs % 60;
let mut y = 1970i64;
let mut remaining_days = days as i64;
loop {
let days_in_year = if is_leap(y) { 366 } else { 365 };
if remaining_days < days_in_year { break; }
remaining_days -= days_in_year;
y += 1;
}
let months_days = if is_leap(y) {
[31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
} else {
[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
};
let mut m = 1;
for &md in &months_days {
if remaining_days < md { break; }
remaining_days -= md;
m += 1;
}
let d = remaining_days + 1;
format!("{:04}-{:02}-{:02} {:02}:{:02}:{:02}", y, m, d, hours, mins, secs_remain)
}
fn is_leap(y: i64) -> bool {
(y % 4 == 0 && y % 100 != 0) || (y % 400 == 0)
}

View File

@@ -0,0 +1,86 @@
#!/bin/bash
# 带宽与负载测试套件
# 测试目标dev.xmclassmate.top
set -e
BASE_URL="https://dev.xmclassmate.top"
DURATION="10s"
echo "=========================================="
echo " 3Mbps 带宽负载测试报告"
echo " 目标: $BASE_URL"
echo " 测试时间: $(date '+%Y-%m-%d %H:%M:%S')"
echo "=========================================="
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " 1. 各端点响应大小"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
for endpoint in "/health" "/" "/payment" "/static/css/style.css" "/static/js/afterbody.js"; do
result=$(curl -sk -o /dev/null -w "%{http_code}\t%{size_download}\t%{time_total}" "$BASE_URL$endpoint" 2>/dev/null)
code=$(echo "$result" | cut -f1)
size=$(echo "$result" | cut -f2)
time=$(echo "$result" | cut -f3)
printf " %-35s HTTP %s %7s bytes %.3fs\n" "$endpoint" "$code" "$size" "$time"
done
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " 2. 压力测试:/health38 字节 JSON"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
for conc in 10 50 100; do
echo "▶ 并发 $conc / 时长 $DURATION"
oha -z "$DURATION" -c "$conc" --no-tui --latency-correction \
-H "User-Agent: Bench/1.0" \
"$BASE_URL/health" 2>&1 | grep -E "Requests|Success|Avg|P50|P95|P99|Transfer" | sed 's/^/ /'
echo ""
done
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " 3. 压力测试:/6KB HTML 状态页)"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
oha -z "10s" -c 50 --no-tui --latency-correction \
-H "User-Agent: Bench/1.0" \
"$BASE_URL/" 2>&1 | grep -E "Requests|Success|Avg|P50|P95|P99|Transfer" | sed 's/^/ /'
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " 4. 压力测试:/payment14KB HTML"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
oha -z "10s" -c 50 --no-tui --latency-correction \
-H "User-Agent: Bench/1.0" \
"$BASE_URL/payment" 2>&1 | grep -E "Requests|Success|Avg|P50|P95|P99|Transfer" | sed 's/^/ /'
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " 5. 带宽使用率估算"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo " 3 Mbps = 384 KB/s"
echo ""
echo " 场景模拟1000 用户活跃度 10%"
echo " ┌─────────────────────────┬──────────────┬──────────────┐"
echo " │ 请求类型 │ 单次大小 │ 100 并发 │"
echo " ├─────────────────────────┼──────────────┼──────────────┤"
echo " │ /health (保活) │ 38 B │ 3.8 KB │"
echo " │ /weather API JSON │ ~600 B │ 60 KB │"
echo " │ POST /api/login │ ~400 B │ 40 KB │"
echo " │ POST /api/post-weather │ ~120 B │ 12 KB │"
echo " │ /payment (HTML 页面) │ 14.5 KB │ 1.45 MB 🔴 │"
echo " ├─────────────────────────┼──────────────┼──────────────┤"
echo " │ 混合场景50% API + │ │ ~150 KB/s │"
echo " │ 50% 静态/页面) │ │ = 1.2 Mbps │"
echo " │ │ │ (余量 60%) │"
echo " └─────────────────────────┴──────────────┴──────────────┘"
echo ""
echo " 结论: 3Mbps 对 1000 用户完全够用"
echo " 触发 3Mbps 瓶颈的临界点:"
echo " - 纯 API: ~9000 req/s (384KB / 42B per req)"
echo " - 混合场景: ~1000 req/s"
echo " - 大页面: ~26 个 /payment 并发请求即可占满带宽"
echo ""

View File

@@ -108,13 +108,21 @@ check_service_file() {
# ---------- 蓝绿部署:检查目标是否为当前活动环境 ----------
NGINX_CONTAINER="1Panel-openresty-ABu5"
ROOT_PROXY_CONF="/www/sites/xmclassmate.top/proxy/root.conf"
get_proxy_conf() {
if [ "${APP_ENV}" = "development" ]; then
echo "/www/sites/dev.xmclassmate.top/proxy/root.conf"
else
echo "/www/sites/xmclassmate.top/proxy/root.conf"
fi
}
check_not_active() {
[ -z "$BG_TARGET" ] && return 0 # 非蓝绿部署,跳过
local proxy_conf; proxy_conf=$(get_proxy_conf)
local active_env
active_env=$(remote "docker exec ${NGINX_CONTAINER} cat ${ROOT_PROXY_CONF} 2>/dev/null" 2>/dev/null | grep -oP '127\.0\.0\.1:(4433|4434)' | head -1 || echo "")
active_env=$(remote "docker exec ${NGINX_CONTAINER} cat ${proxy_conf} 2>/dev/null" 2>/dev/null | grep -oP '127\.0\.0\.1:\d+' | head -1 || echo "")
local current_target
case "$active_env" in

View File

@@ -77,6 +77,7 @@ pub fn generate_refresh_token(user_id: i32, secret: &str) -> Result<String, Stri
}
// 解析 refresh_token返回 (user_id, expires_at)
#[allow(dead_code)]
pub fn verify_refresh_token(token: &str, _secret: &str) -> Result<(i32, i64), String> {
let decoded = BASE64.decode(token)
.map_err(|e| format!("Refresh token 格式错误: {}", e))?;

View File

@@ -42,18 +42,6 @@ impl AppConfig {
Ok(app_config)
}
pub fn database_url(&self) -> &str {
&self.database_url
}
pub fn rust_log(&self) -> &str {
&self.rust_log
}
pub fn is_production(&self) -> bool {
self.environment == "production"
}
}
fn env_or_fail(key: &str) -> Result<String, String> {

View File

@@ -11,7 +11,7 @@ use crate::error::AppError;
pub async fn insert_weather_data(pool: &PgPool, weather_data: &WeatherData, user_id: i32) -> Result<i32, AppError> {
// 配额检查:非付费用户数据条数限制
let user = get_user_by_id(pool, user_id).await?;
let is_active_member = user.is_member && user.membership_expires_at.map_or(true, |expires| expires > Utc::now());
let is_active_member = user.is_member && user.membership_expires_at.is_none_or(|expires| expires > Utc::now());
// 维护模式:非会员使用更高的临时限额,防止资源滥用
let is_maintenance = std::env::var("PAYMENT_MAINTENANCE_MODE").ok() == Some("true".to_string());
@@ -800,7 +800,7 @@ pub async fn get_user_quota(
) -> Result<(i64, bool, Option<chrono::DateTime<chrono::Utc>>, bool), AppError> {
let user = get_user_by_id(pool, user_id).await?;
let is_active_member = user.is_member
&& user.membership_expires_at.map_or(true, |expires| expires > chrono::Utc::now());
&& user.membership_expires_at.is_none_or(|expires| expires > chrono::Utc::now());
let used = count_user_weather_data(pool, user_id).await?;
Ok((used, is_active_member, user.membership_expires_at, user.is_member))
}
@@ -879,10 +879,14 @@ pub async fn set_weather_favorite(
#[derive(Debug, FromRow)]
pub struct RefreshToken {
#[allow(dead_code)]
pub id: i32,
pub user_id: i32,
#[allow(dead_code)]
pub token: String,
#[allow(dead_code)]
pub expires_at: chrono::DateTime<chrono::Utc>,
#[allow(dead_code)]
pub created_at: chrono::DateTime<chrono::Utc>,
}
@@ -926,6 +930,7 @@ pub async fn verify_refresh_token(
.ok_or_else(|| AppError::Unauthorized("Refresh token 无效或已过期".to_string()))
}
#[allow(dead_code)]
pub async fn delete_refresh_token(pool: &PgPool, user_id: i32) -> Result<(), AppError> {
let query = r#"DELETE FROM refresh_tokens WHERE user_id = $1"#;

View File

@@ -11,6 +11,7 @@ pub struct ErrorResponse<T = ()> {
}
impl<T> ErrorResponse<T> {
#[allow(dead_code)]
pub fn success(data: T) -> Self {
Self {
success: true,
@@ -29,12 +30,14 @@ impl<T> ErrorResponse<T> {
}
impl<T: Serialize> ErrorResponse<T> {
#[allow(dead_code, clippy::wrong_self_convention)]
pub fn to_json_response(self) -> HttpResponse {
HttpResponse::Ok().json(self)
}
}
impl ErrorResponse<()> {
#[allow(dead_code)]
pub fn to_error_response(&self, status: StatusCode) -> HttpResponse {
let body = serde_json::json!({
"success": false,

View File

@@ -149,7 +149,31 @@ pub async fn refresh_token(
pool: web::Data<PgPool>,
req: web::Json<RefreshTokenRequest>,
app_state: web::Data<AppState>,
http_req: HttpRequest,
) -> impl Responder {
let client_ip = http_req
.headers()
.get("X-Forwarded-For")
.and_then(|v| v.to_str().ok())
.map(|s| s.split(',').next().unwrap_or(s).trim().to_string())
.or_else(|| {
http_req
.headers()
.get("X-Real-IP")
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string())
})
.unwrap_or_else(|| {
http_req
.peer_addr()
.map(|a| a.ip().to_string())
.unwrap_or_else(|| "unknown".to_string())
});
if let Err(e) = LOGIN_RATE_LIMITER.check_rate_limit(&client_ip).await {
warn!("refresh_token 请求被限流: client_ip={}", client_ip);
return HttpResponse::TooManyRequests().json(ErrorResponse::<()>::error(e.to_string()));
}
let refresh_token = &req.refresh_token;
let refresh_token_record = match db::verify_refresh_token(pool.get_ref(), refresh_token).await {
@@ -247,15 +271,13 @@ pub async fn mock_login(
}
// 如果设置了 MOCK_LOGIN_KEY验证请求头
if let Ok(key) = std::env::var("MOCK_LOGIN_KEY") {
if !key.is_empty() {
let header_key = req.headers()
.get("X-Mock-Key")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
if header_key != key {
return HttpResponse::Forbidden().json(ErrorResponse::<()>::error("模拟登录密钥错误"));
}
if let Ok(key) = std::env::var("MOCK_LOGIN_KEY") && !key.is_empty() {
let header_key = req.headers()
.get("X-Mock-Key")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
if header_key != key {
return HttpResponse::Forbidden().json(ErrorResponse::<()>::error("模拟登录密钥错误"));
}
}
@@ -613,7 +635,7 @@ pub async fn web_login_confirm(
.await
{
Ok(Some((is_member, expires))) => {
let active = is_member && expires.map_or(true, |e| e > Utc::now());
let active = is_member && expires.is_none_or(|e| e > Utc::now());
(active, expires.map(|e| e.to_rfc3339()))
}
_ => (false, None),
@@ -745,7 +767,7 @@ pub async fn web_login_auto_confirm(
let (is_active_member, membership_expires_at): (bool, Option<String>) = match paid_info {
Some((is_member, expires)) => {
let active = is_member && expires.map_or(true, |e| e > Utc::now());
let active = is_member && expires.is_none_or(|e| e > Utc::now());
(active, expires.map(|e| e.to_rfc3339()))
}
None => (false, None),

View File

@@ -849,8 +849,8 @@ fn urlencoding(s: &str) -> String {
while let Some(b) = chars.next() {
if b == b'%' {
had_escape = true;
let hi = chars.next().and_then(|c| hex_val(c));
let lo = chars.next().and_then(|c| hex_val(c));
let hi = chars.next().and_then(hex_val);
let lo = chars.next().and_then(hex_val);
if let (Some(h), Some(l)) = (hi, lo) {
result.push((h << 4 | l) as char);
} else {
@@ -1171,16 +1171,14 @@ fn check_mock_payment_allowed(req: &HttpRequest) -> Result<(), AppError> {
}
// 规则 3如果设了 MOCK_PAY_KEY验证请求头
if let Ok(key) = std::env::var("MOCK_PAY_KEY") {
if !key.is_empty() {
let header_key = req
.headers()
.get("X-Mock-Key")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
if header_key != key {
return Err(AppError::Forbidden("Mock 支付密钥错误".to_string()));
}
if let Ok(key) = std::env::var("MOCK_PAY_KEY") && !key.is_empty() {
let header_key = req
.headers()
.get("X-Mock-Key")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
if header_key != key {
return Err(AppError::Forbidden("Mock 支付密钥错误".to_string()));
}
}
@@ -1246,7 +1244,7 @@ pub async fn sync_order(
.ok_or_else(|| AppError::NotFound("用户不存在".to_string()))?;
let (is_member, membership_expires_at) = user;
let is_active_member = is_member && membership_expires_at.map_or(true, |expires| expires > Utc::now());
let is_active_member = is_member && membership_expires_at.is_none_or(|expires| expires > Utc::now());
Ok(HttpResponse::Ok().json(serde_json::json!({
"success": true,
@@ -1407,6 +1405,7 @@ pub async fn payment_login_status(
let code = query.code.trim();
// 查询登录码记录(包含 token 字段用于判断是否已确认)
#[allow(clippy::type_complexity)]
let record: Option<(String, chrono::DateTime<chrono::Utc>, Option<String>, Option<i32>)> =
sqlx::query_as(
"SELECT code, expires_at, token, user_id FROM web_login_codes WHERE code = $1",
@@ -1472,7 +1471,7 @@ pub async fn payment_login_status(
.await
{
Ok(Some((is_member, expires))) => {
let active = is_member && expires.map_or(true, |e| e > Utc::now());
let active = is_member && expires.is_none_or(|e| e > Utc::now());
(active, expires.map(|e| e.to_rfc3339()))
}
_ => (false, None),

View File

@@ -3,8 +3,10 @@ use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct SentryEventRequest {
#[allow(dead_code)]
pub level: Option<String>,
pub message: String,
#[allow(dead_code)]
pub stack: Option<String>,
pub page: Option<String>,
pub user_id: Option<i32>,
@@ -28,11 +30,11 @@ pub async fn report_frontend_error(body: web::Json<SentryEventRequest>) -> HttpR
..Default::default()
}));
}
if let Some(extra) = &body.extra {
if let Some(obj) = extra.as_object() {
for (k, v) in obj {
scope.set_extra(k, sentry::protocol::Value::from(v.clone()));
}
if let Some(extra) = &body.extra
&& let Some(obj) = extra.as_object()
{
for (k, v) in obj {
scope.set_extra(k, v.clone());
}
}
},

View File

@@ -21,7 +21,7 @@ pub async fn get_current_user_profile(
true
} else {
user.is_member &&
user.membership_expires_at.map_or(true, |expires| expires > chrono::Utc::now())
user.membership_expires_at.is_none_or(|expires| expires > chrono::Utc::now())
};
Ok(HttpResponse::Ok().json(serde_json::json!({

View File

@@ -324,6 +324,8 @@ async fn main() -> std::io::Result<()> {
match HttpServer::new(move || {
create_server_config(pool_clone.clone(), http_client_clone.clone(), app_state_clone.clone())
})
.keep_alive(std::time::Duration::from_secs(30))
.backlog(1024)
.bind_openssl(&addr, ssl_builder)
{
Ok(s) => {
@@ -345,6 +347,8 @@ async fn main() -> std::io::Result<()> {
match HttpServer::new(move || {
create_server_config(pool_clone.clone(), http_client_clone.clone(), app_state_clone.clone())
})
.keep_alive(std::time::Duration::from_secs(30))
.backlog(1024)
.bind(&addr)
{
Ok(s) => {

View File

@@ -52,12 +52,14 @@ impl LoginResponse {
}
// 兼容旧的 TokenResponse
#[allow(dead_code)]
#[derive(Debug, Serialize, Clone)]
pub struct TokenResponse {
pub success: bool,
pub token: String,
}
#[allow(dead_code)]
impl TokenResponse {
pub fn new(token: String) -> Self {
Self {
@@ -345,11 +347,16 @@ pub struct AppState {
pub jwt_secret: String,
pub wechat_appid: String,
pub wechat_secret: String,
#[allow(dead_code)]
pub free_user_data_limit: i32,
// ===== 支付宝配置 =====
#[allow(dead_code)]
pub alipay_app_id: Option<String>,
#[allow(dead_code)]
pub alipay_private_key: Option<String>,
#[allow(dead_code)]
pub alipay_alipay_public_key: Option<String>,
#[allow(dead_code)]
pub alipay_gateway: Option<String>,
}

View File

@@ -40,6 +40,7 @@ impl RateLimiter {
}
}
#[allow(dead_code)]
pub fn extract_client_ip_from_header(headers: &actix_web::http::header::HeaderMap) -> String {
headers
.get("X-Forwarded-For")

View File

@@ -128,7 +128,7 @@ fn test_member_status_bounds() {
(true, false, false, false), // 已过期会员
];
for (is_member, expires_none, future, expected) in &cases {
for (is_member, _expires_none, future, expected) in &cases {
if *future {
// 模拟会员在有效期内
assert!(*expected == *is_member, "活跃时 is_active_member = is_member");