- Base platform from base.git (ERP base: auth, core, config, message, workflow, plugin) - Created erp-diary module skeleton (lib.rs, dto.rs, error.rs, event.rs, state.rs) - Integrated erp-diary into workspace and erp-server - Added DiaryModule registration in main.rs - Added DiaryState FromRef in state.rs - Diary routes mounted (empty routes, ready for implementation) - Product design spec v1.2 preserved in docs/ - Implementation plan preserved in plans/ Cargo check: OK Cargo test: OK (78+ base tests passing)
157 lines
4.1 KiB
Rust
157 lines
4.1 KiB
Rust
use serde::Deserialize;
|
||
|
||
#[derive(Debug, Clone, Deserialize)]
|
||
pub struct AppConfig {
|
||
pub server: ServerConfig,
|
||
pub database: DatabaseConfig,
|
||
pub redis: RedisConfig,
|
||
pub jwt: JwtConfig,
|
||
pub auth: AuthConfig,
|
||
pub log: LogConfig,
|
||
pub cors: CorsConfig,
|
||
pub wechat: WechatConfig,
|
||
pub crypto: CryptoConfig,
|
||
pub storage: StorageConfig,
|
||
#[serde(default)]
|
||
pub rate_limit: RateLimitConfig,
|
||
}
|
||
|
||
#[derive(Debug, Clone, Deserialize)]
|
||
pub struct ServerConfig {
|
||
pub host: String,
|
||
pub port: u16,
|
||
#[serde(default = "default_metrics_port")]
|
||
pub metrics_port: u16,
|
||
}
|
||
|
||
fn default_metrics_port() -> u16 {
|
||
9090
|
||
}
|
||
|
||
#[derive(Debug, Clone, Deserialize)]
|
||
pub struct DatabaseConfig {
|
||
pub url: String,
|
||
pub max_connections: u32,
|
||
pub min_connections: u32,
|
||
}
|
||
|
||
#[derive(Debug, Clone, Deserialize)]
|
||
pub struct RedisConfig {
|
||
pub url: String,
|
||
}
|
||
|
||
#[derive(Debug, Clone, Deserialize)]
|
||
pub struct JwtConfig {
|
||
pub secret: String,
|
||
pub access_token_ttl: String,
|
||
pub refresh_token_ttl: String,
|
||
}
|
||
|
||
#[derive(Debug, Clone, Deserialize)]
|
||
pub struct LogConfig {
|
||
pub level: String,
|
||
}
|
||
|
||
#[derive(Debug, Clone, Deserialize)]
|
||
pub struct AuthConfig {
|
||
pub super_admin_password: String,
|
||
}
|
||
|
||
#[derive(Debug, Clone, Deserialize)]
|
||
pub struct CorsConfig {
|
||
/// Comma-separated list of allowed origins.
|
||
/// Use "*" to allow all origins (development only).
|
||
pub allowed_origins: String,
|
||
}
|
||
|
||
#[derive(Debug, Clone, Deserialize)]
|
||
pub struct WechatConfig {
|
||
pub appid: String,
|
||
pub secret: String,
|
||
#[serde(default)]
|
||
pub dev_mode: bool,
|
||
}
|
||
|
||
#[derive(Debug, Clone, Deserialize)]
|
||
pub struct CryptoConfig {
|
||
/// Master KEK (64 字符 hex 编码,32 字节)。用于加密保护每租户 DEK。
|
||
/// Phase A 阶段同时作为全局数据加密密钥使用。
|
||
pub kek: String,
|
||
}
|
||
|
||
#[derive(Debug, Clone, Deserialize)]
|
||
pub struct StorageConfig {
|
||
/// 文件上传目录(本地存储)
|
||
pub upload_dir: String,
|
||
/// 单文件最大大小(如 "10MB")
|
||
pub max_file_size: String,
|
||
/// 签名 URL 密钥(HMAC-SHA256)
|
||
#[serde(default = "default_secret_key")]
|
||
pub secret_key: String,
|
||
}
|
||
|
||
fn default_secret_key() -> String {
|
||
#[cfg(debug_assertions)]
|
||
{
|
||
"dev-only-secret-key-change-in-production".to_string()
|
||
}
|
||
#[cfg(not(debug_assertions))]
|
||
{
|
||
panic!("ERP__STORAGE__SECRET_KEY 必须设置(生产环境不允许使用默认签名密钥)")
|
||
}
|
||
}
|
||
|
||
impl StorageConfig {
|
||
/// 解析 max_file_size 为字节数
|
||
pub fn max_file_size_bytes(&self) -> u64 {
|
||
let s = self.max_file_size.to_uppercase();
|
||
if let Some(num) = s.strip_suffix("MB") {
|
||
num.trim().parse::<u64>().unwrap_or(10) * 1024 * 1024
|
||
} else if let Some(num) = s.strip_suffix("KB") {
|
||
num.trim().parse::<u64>().unwrap_or(1024) * 1024
|
||
} else if let Some(num) = s.strip_suffix("GB") {
|
||
num.trim().parse::<u64>().unwrap_or(1) * 1024 * 1024 * 1024
|
||
} else {
|
||
s.parse::<u64>().unwrap_or(10 * 1024 * 1024)
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, Clone, Deserialize)]
|
||
pub struct RateLimitConfig {
|
||
/// Redis 不可达时是否拒绝请求(fail-close)。
|
||
/// true = 安全优先,Redis 故障时返回 503。
|
||
/// false = 可用性优先,Redis 故障时放行。
|
||
#[serde(default = "default_fail_close")]
|
||
pub fail_close: bool,
|
||
}
|
||
|
||
fn default_fail_close() -> bool {
|
||
true
|
||
}
|
||
|
||
impl Default for RateLimitConfig {
|
||
fn default() -> Self {
|
||
Self {
|
||
fail_close: default_fail_close(),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl AppConfig {
|
||
pub fn load() -> anyhow::Result<Self> {
|
||
let config = config::Config::builder()
|
||
.add_source(config::File::with_name("config/default"))
|
||
.add_source(config::Environment::with_prefix("ERP").separator("__"))
|
||
.build()?;
|
||
let app_config: Self = config.try_deserialize()?;
|
||
|
||
// 安全检查:禁止在生产使用默认 JWT 密钥
|
||
if app_config.jwt.secret == "change-me-in-production" {
|
||
tracing::warn!("⚠️ JWT 密钥使用默认值,请通过 ERP__JWT__SECRET 环境变量设置安全密钥");
|
||
}
|
||
|
||
Ok(app_config)
|
||
}
|
||
}
|