feat: 实现后端多环境配置机制

- 使用 toml crate 替代 config crate 直接读取配置文件
- 创建 config/default.toml、config/development.toml、config/production.toml
- 重写 config.rs 支持多环境配置加载
- 增强 deploy.sh 支持 development/production 环境参数
- 更新 IMPROVEMENTS.md 标记环境区分完成
This commit is contained in:
2026-04-17 11:11:13 +08:00
parent ca7ddcc55e
commit da4e6f557e
8 changed files with 314 additions and 62 deletions

View File

@@ -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<Type, String> {
## 环境变量
```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

60
Cargo.lock generated
View File

@@ -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"

View File

@@ -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"] }

View File

@@ -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

View File

@@ -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

View File

@@ -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<u16>,
}
impl Config {
impl AppConfig {
pub fn load() -> Result<Self, String> {
// 加载.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::<i64>() {
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)
}
}
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"
}
}

View File

@@ -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) // 生产环境可开启

View File

@@ -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<f64>, // 这个已经是 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),
})
}
}