From da4e6f557e69c9723a4a11c04bed0fac3c61bd11 Mon Sep 17 00:00:00 2001 From: Milky0217 Date: Fri, 17 Apr 2026 11:11:13 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E5=90=8E=E7=AB=AF?= =?UTF-8?q?=E5=A4=9A=E7=8E=AF=E5=A2=83=E9=85=8D=E7=BD=AE=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 使用 toml crate 替代 config crate 直接读取配置文件 - 创建 config/default.toml、config/development.toml、config/production.toml - 重写 config.rs 支持多环境配置加载 - 增强 deploy.sh 支持 development/production 环境参数 - 更新 IMPROVEMENTS.md 标记环境区分完成 --- AGENTS.md | 82 +++++++++++++++++++++++++++------- Cargo.lock | 60 +++++++++++++++++++++++++ Cargo.toml | 1 + IMPROVEMENTS.md | 24 +++++++++- deploy.sh | 54 +++++++++++++++------- src/config.rs | 116 +++++++++++++++++++++++++++++++++++++++--------- src/main.rs | 29 +++++++++--- src/models.rs | 10 ++++- 8 files changed, 314 insertions(+), 62 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index baa6de2..5ff0543 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,7 +22,7 @@ src/ ├── auth.rs # JWT 中间件,令牌生成/验证 ├── db.rs # 数据库操作(通过 sqlx 执行原始 SQL) ├── models.rs # 数据结构(Claims、User、WeatherData 等) -├── config.rs # 配置结构体(未使用 — 死代码,待清理) +├── config.rs # 配置加载(支持多环境:config/*.toml) └── handlers/ # 路由处理器模块 ├── mod.rs # 模块导出 ├── auth.rs # 登录相关 (login) @@ -32,6 +32,11 @@ src/ ├── health.rs # 健康检查 (/health) └── static_files.rs # 静态文件服务 (/static/{path}) +config/ +├── default.toml # 默认配置(所有环境的共同默认值) +├── development.toml # 开发/测试环境配置 +└── production.toml # 生产环境配置 + migrations/ └── 001_add_payment_fields.sql # 用于添加支付字段的 ALTER TABLE @@ -40,6 +45,9 @@ static/ tests/ └── integration_test.rs # 基础测试框架 + +deploy.sh # 部署脚本(支持 development/production 参数) +.env.example # 环境变量模板 ``` ## 数据库表结构 @@ -149,18 +157,54 @@ pub async fn function_name(pool: &PgPool, param: i32) -> Result { ## 环境变量 -```env -DATABASE_URL=postgres://user:pass@host:5432/dbname -WECHAT_APPID=wx... -WECHAT_SECRET=... -JWT_SECRET=your_secret_key -SSL_KEY_PATH=/path/to/key -SSL_CERT_PATH=/path/to/cert -RUST_LOG=info -APP_VERSION=0.2.0 -FREE_USER_DATA_LIMIT=20 +### 方式一:使用配置文件(推荐) + +通过 `APP_ENV` 环境变量选择配置文件: + +```bash +APP_ENV=development cargo run # 使用 config/development.toml +APP_ENV=production cargo run # 使用 config/production.toml ``` +**配置文件**: + +| 文件 | 用途 | +|------|------| +| `config/default.toml` | 所有环境的共同默认值 | +| `config/development.toml` | 开发/测试环境覆盖 | +| `config/production.toml` | 生产环境覆盖 | + +**示例 production.toml**: +```toml +database_url = "postgres://user:pass@host:5432/dbname" +wechat_appid = "wx..." +jwt_secret = "your_secret_key" +rust_log = "info" +free_user_data_limit = 20 +server_host = "0.0.0.0" +server_ports = [4433, 8443, 8080, 3000, 8000, 8888] +``` + +### 方式二:直接环境变量 + +最高优先级,可覆盖配置文件: + +```env +APP_DATABASE_URL=postgres://user:pass@host:5432/dbname +APP_JWT_SECRET=your_secret_key +APP_WECHAT_APPID=wx... +APP_RUST_LOG=info +``` + +### systemd service 配置 + +测试服务和生产服务通过不同 systemd unit 和工作目录隔离: + +| 服务 | systemd unit | 工作目录 | APP_ENV | +|------|-------------|---------|---------| +| 测试 | `rust-backend-dev.service` | `/root/rust/rust_backend_dev` | `development` | +| 生产 | `rust-backend.service` | `/root/rust/rust_backend` | (使用 .env) | + ## 约束条件 ### 已知兼容性问题 @@ -178,7 +222,6 @@ FREE_USER_DATA_LIMIT=20 - 在 JWT Claims 中添加 `is_admin`(必须查询数据库) - 信任 JWT 中的 `is_paid` 来做配额决策(必须查询数据库) - 使用 `as any`、`@ts-ignore` 或类型错误抑制 -- 修改 config.rs(死代码) - 添加支付网关集成 - 添加审计日志 @@ -232,10 +275,17 @@ App::new() ## 构建与运行 ```bash -cargo build # 编译 -cargo run # 启动服务器 -cargo test # 运行测试 -cargo clippy # 代码检查 +cargo build # 编译 +APP_ENV=development cargo run # 开发环境运行 +APP_ENV=production cargo run # 生产环境运行 +cargo test # 运行测试 +cargo clippy # 代码检查 +``` + +**部署**: +```bash +./deploy.sh development # 部署到测试服务器 +./deploy.sh production # 部署到生产服务器 ``` 服务器尝试端口顺序:4433、8443、8080、3000、8000、8888 diff --git a/Cargo.lock b/Cargo.lock index 891458c..b5ccc9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1976,6 +1976,7 @@ dependencies = [ "serde_json", "sqlx", "tokio", + "toml", "tracing", "tracing-appender", "tracing-subscriber", @@ -2116,6 +2117,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2706,6 +2716,47 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "tower" version = "0.5.2" @@ -3332,6 +3383,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.45.0" diff --git a/Cargo.toml b/Cargo.toml index 80ca531..5453fbe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.143" sqlx = { version = "0.8.6", features = ["runtime-tokio-rustls", "postgres", "chrono", "uuid"] } tokio = { version = "1", features = ["full"] } +toml = "0.8" [dev-dependencies] tokio = { version = "1", features = ["full"] } diff --git a/IMPROVEMENTS.md b/IMPROVEMENTS.md index cfc8d45..bdddb80 100644 --- a/IMPROVEMENTS.md +++ b/IMPROVEMENTS.md @@ -538,6 +538,28 @@ fi - 本改进与"前后端版本统一管理"(改进路线图 P1)可合并实施 - 本改进与"拆分 main.rs 路由处理器"(改进路线图 P1)有协同效应 +### 实施结果 + +✅ **已完成** (2026-04-17) + +**实际实施方案**:采用简化方案 + TOML 配置文件 + +| 改动项 | 说明 | +|--------|------| +| `config.rs` | 重写为使用 `toml` crate 直接读取配置文件 | +| `config/*.toml` | 创建 `default.toml`、`development.toml`、`production.toml` | +| `Cargo.toml` | 添加 `toml = "0.8"` 依赖,移除 `config = "0.14"` | +| `deploy.sh` | 重写支持 `development`/`production` 环境参数 | +| systemd service | 创建 `rust-backend-dev.service` 测试服务 | + +**关键修复**: +- TOML 文件去掉 `[development]` 等 section 头(config crate 遗留语法) +- `database_url` 使用 `127.0.0.1` 而非 `localhost`(Docker PostgreSQL 监听地址) + +**测试验证**: +- 测试服务运行在端口 8080 +- Nginx 代理 `https://xmclassmate.top/dev/api/login` 正常工作 + --- ## 十二、付费功能系统 @@ -732,5 +754,5 @@ pages/ --- -**最后更新**:2026-04-14 +**最后更新**:2026-04-17 **维护者**:milky diff --git a/deploy.sh b/deploy.sh index 1319792..11530f2 100755 --- a/deploy.sh +++ b/deploy.sh @@ -1,18 +1,28 @@ #!/bin/bash -# ============================================================================== -# 部署脚本 - Rust Backend -# ============================================================================== +set -euo pipefail -# --- 1. 配置 --- PROJECT_NAME="rust-backend" REMOTE_USER="root" REMOTE_HOST="1panel-server" -REMOTE_DIR="/root/rust/rust_backend" -SERVICE_NAME="rust-backend.service" +APP_ENV="${1:-production}" -# --- 2. 脚本初始化 --- -set -euo pipefail +# 根据环境选择远程目录和服务 +case "${APP_ENV}" in + development) + REMOTE_DIR="/root/rust/rust_backend_dev" + SERVICE_NAME="rust-backend-dev.service" + ;; + production) + REMOTE_DIR="/root/rust/rust_backend" + SERVICE_NAME="rust-backend.service" + ;; + *) + echo "无效的环境: ${APP_ENV}" + echo "支持的環境: development, production" + exit 1 + ;; +esac log_info() { echo -e "\033[32m[INFO]\033[0m $1" @@ -23,7 +33,6 @@ log_error() { exit 1 } -# --- 3. 检查依赖 --- check_dependencies() { local deps=("cargo" "rsync" "ssh") for cmd in "${deps[@]}"; do @@ -34,17 +43,15 @@ check_dependencies() { log_info "所有依赖项检查通过。" } -# --- 4. 编译项目 --- build_project() { - log_info "开始编译项目 (release模式)..." - if cargo build --release; then + log_info "开始编译项目 (release模式),环境=${APP_ENV}..." + if APP_ENV="${APP_ENV}" cargo build --release; then log_info "项目编译成功。" else log_error "项目编译失败!" fi } -# --- 5. 上传二进制文件 --- upload_binary() { local binary_path="./target/release/${PROJECT_NAME}" if [ ! -f "$binary_path" ]; then @@ -56,7 +63,18 @@ upload_binary() { log_info "文件上传成功。" } -# --- 6. 重启远程服务 --- +upload_config() { + # development 环境跳过(远程已有正确配置,使用 localhost 连接) + if [ "${APP_ENV}" = "development" ]; then + log_info "跳过配置文件上传(development 环境使用远程已有配置)..." + return 0 + fi + + log_info "正在上传配置文件到 ${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/config/..." + rsync -avzP --progress ./config/ "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/config/" + log_info "配置文件上传成功。" +} + restart_remote_service() { log_info "正在重启远程服务 '${SERVICE_NAME}'..." if ssh "${REMOTE_USER}@${REMOTE_HOST}" "systemctl restart ${SERVICE_NAME}"; then @@ -66,14 +84,16 @@ restart_remote_service() { fi } -# --- 7. 主执行流程 --- main() { - log_info "========== 开始部署 ==========" + log_info "========== 开始部署 (环境: ${APP_ENV}) ==========" + log_info "远程目录: ${REMOTE_DIR}" + log_info "服务名称: ${SERVICE_NAME}" check_dependencies build_project upload_binary + upload_config restart_remote_service log_info "========== 部署完成! ==========" } -main +main \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index d9b0b90..e6c2dce 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,33 +1,105 @@ -use std::env; -use dotenvy::dotenv; -use tracing; +//! 应用配置模块 +//! +//! 支持多环境配置加载,优先级:环境变量 > 环境配置 > 默认配置 -#[derive(Debug, Clone)] -pub struct Config { +use serde::Deserialize; +use std::path::PathBuf; + +#[derive(Debug, Deserialize, Clone)] +pub struct AppConfig { pub database_url: String, - pub jwt_secret: String, pub wechat_appid: String, pub wechat_secret: String, + pub jwt_secret: String, + pub ssl_key_path: String, + pub ssl_cert_path: String, + pub rust_log: String, + pub app_version: String, + pub environment: String, + pub free_user_data_limit: i32, + pub server_host: String, + #[serde(rename = "server_ports")] pub server_ports: Vec, } -impl Config { +impl AppConfig { pub fn load() -> Result { - // 加载.env文件 - if let Err(e) = dotenv() { - tracing::warn!("无法加载.env文件 - {}", e); + let env = std::env::var("APP_ENV").unwrap_or_else(|_| "development".into()); + + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR") + .map(PathBuf::from) + .unwrap_or_else(|_| std::env::current_dir().unwrap()); + + let config_dir = manifest_dir.join("config"); + + let default_path = config_dir.join("default.toml"); + let env_path = config_dir.join(&format!("{}.toml", env)); + + let mut settings = toml::Table::new(); + + // 1. 加载 default.toml + if default_path.exists() { + let content = std::fs::read_to_string(&default_path) + .map_err(|e| format!("读取 default.toml 失败: {}", e))?; + let parsed: toml::Table = + toml::from_str(&content).map_err(|e| format!("解析 default.toml 失败: {}", e))?; + settings.extend(parsed); + } else { + return Err(format!("配置文件不存在: {}", default_path.display())); } - Ok(Self { - database_url: env::var("DATABASE_URL") - .map_err(|_| "环境变量DATABASE_URL未设置".to_string())?, - jwt_secret: env::var("JWT_SECRET") - .map_err(|_| "环境变量JWT_SECRET未设置".to_string())?, - wechat_appid: env::var("WECHAT_APPID") - .map_err(|_| "环境变量WECHAT_APPID未设置".to_string())?, - wechat_secret: env::var("WECHAT_SECRET") - .map_err(|_| "环境变量WECHAT_SECRET未设置".to_string())?, - server_ports: vec![8080, 3000, 8000, 8888], - }) + // 2. 合并环境配置(覆盖默认值) + if env_path.exists() { + let content = std::fs::read_to_string(&env_path) + .map_err(|e| format!("读取 {}.toml 失败: {}", env, e))?; + let parsed: toml::Table = + toml::from_str(&content).map_err(|e| format!("解析 {}.toml 失败: {}", env, e))?; + settings.extend(parsed); + } + + // 3. 从环境变量加载(最高优先级) + if let Ok(val) = std::env::var("APP_DATABASE_URL") { + settings.insert("database_url".into(), toml::Value::String(val)); + } + if let Ok(val) = std::env::var("APP_JWT_SECRET") { + settings.insert("jwt_secret".into(), toml::Value::String(val)); + } + if let Ok(val) = std::env::var("APP_WECHAT_APPID") { + settings.insert("wechat_appid".into(), toml::Value::String(val)); + } + if let Ok(val) = std::env::var("APP_WECHAT_SECRET") { + settings.insert("wechat_secret".into(), toml::Value::String(val)); + } + if let Ok(val) = std::env::var("APP_RUST_LOG") { + settings.insert("rust_log".into(), toml::Value::String(val)); + } + if let Ok(val) = std::env::var("APP_FREE_USER_DATA_LIMIT") { + if let Ok(num) = val.parse::() { + settings.insert("free_user_data_limit".into(), toml::Value::Integer(num)); + } + } + + let app_config: AppConfig = toml::from_str(&settings.to_string()) + .map_err(|e| format!("配置反序列化失败: {}", e))?; + + tracing::info!( + "配置加载成功,环境={},app_version={}", + app_config.environment, + app_config.app_version + ); + + Ok(app_config) } -} \ No newline at end of file + + 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" + } +} diff --git a/src/main.rs b/src/main.rs index 919b973..61e3dc9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,11 +8,13 @@ use sqlx::postgres::PgPool; use std::pin::Pin; mod auth; +mod config; mod db; mod handlers; mod models; use auth::jwt_middleware; +use config::AppConfig; use db::create_pool; use handlers::{ admin_get_user, admin_update_user_payment, delete_weather, generate_temp_token_handler, @@ -79,8 +81,25 @@ fn create_server_config( #[actix_web::main] async fn main() -> std::io::Result<()> { - if let Err(e) = dotenvy::dotenv() { - tracing::warn!("加载.env文件失败,使用系统环境变量: {}", e); + // 加载配置文件(支持多环境) + let app_config = match AppConfig::load() { + Ok(cfg) => cfg, + Err(e) => { + eprintln!("配置加载失败: {}", e); + std::process::exit(1); + } + }; + + // 设置环境变量(向后兼容依赖 env var 的组件) + unsafe { + std::env::set_var("DATABASE_URL", &app_config.database_url); + std::env::set_var("JWT_SECRET", &app_config.jwt_secret); + std::env::set_var("WECHAT_APPID", &app_config.wechat_appid); + std::env::set_var("WECHAT_SECRET", &app_config.wechat_secret); + std::env::set_var("SSL_KEY_PATH", &app_config.ssl_key_path); + std::env::set_var("SSL_CERT_PATH", &app_config.ssl_cert_path); + std::env::set_var("RUST_LOG", &app_config.rust_log); + std::env::set_var("FREE_USER_DATA_LIMIT", app_config.free_user_data_limit.to_string()); } // 初始化文件日志(JSON 格式,带轮转) @@ -102,9 +121,9 @@ async fn main() -> std::io::Result<()> { tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::from_default_env() - .add_directive("rust_backend=info".parse().unwrap()) - .add_directive("actix_web=info".parse().unwrap()) - .add_directive("sqlx=warn".parse().unwrap()) // SQLx 日志太多,降级 + .add_directive(format!("rust_backend={}", app_config.rust_log).parse().unwrap()) + .add_directive(format!("actix_web={}", app_config.rust_log).parse().unwrap()) + .add_directive("sqlx=warn".parse().unwrap()) ) .with_target(true) .with_thread_ids(false) // 生产环境可开启 diff --git a/src/models.rs b/src/models.rs index 6b30cb4..8b06f5c 100644 --- a/src/models.rs +++ b/src/models.rs @@ -149,7 +149,11 @@ pub struct WeatherData { #[sqlx(rename = "averagewindspeed")] pub average_wind_speed: f64, - #[serde(rename = "calculatedWindSpeed", alias = "calculated_wind_speed", alias = "calculatedwindspeed")] + #[serde( + rename = "calculatedWindSpeed", + alias = "calculated_wind_speed", + alias = "calculatedwindspeed" + )] #[sqlx(rename = "calculatedwindspeed")] pub calculated_wind_speed: Option, // 这个已经是 Option,很好 @@ -305,6 +309,7 @@ pub struct AppState { pub jwt_secret: String, pub wechat_appid: String, pub wechat_secret: String, + pub free_user_data_limit: i32, } impl AppState { @@ -316,6 +321,9 @@ impl AppState { .map_err(|_| "环境变量WECHAT_APPID未设置".to_string())?, wechat_secret: std::env::var("WECHAT_SECRET") .map_err(|_| "环境变量WECHAT_SECRET未设置".to_string())?, + free_user_data_limit: std::env::var("FREE_USER_DATA_LIMIT") + .map(|v| v.parse().unwrap_or(20)) + .unwrap_or(20), }) } }