Files
web-rust-template-project/src/db.rs
T

383 lines
13 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use crate::config::database::{DatabaseConfig, DatabaseType};
use sea_orm::{
ConnectOptions, ConnectionTrait, Database, DatabaseConnection, DbBackend, EntityName,
EntityTrait, Schema, Statement,
};
use std::time::Duration;
/// 数据库连接池(SeaORM 统一接口)
pub type DbPool = DatabaseConnection;
/// 创建数据库连接池
pub async fn create_pool(config: &DatabaseConfig) -> anyhow::Result<DbPool> {
let url = config
.build_url()
.map_err(|e| anyhow::anyhow!("数据库配置错误: {}", e))?;
tracing::debug!("数据库连接 URL: {}", url);
let mut opt = ConnectOptions::new(&url);
opt.max_connections(config.max_connections)
.min_connections(1)
.connect_timeout(Duration::from_secs(8))
.idle_timeout(Duration::from_secs(8))
.max_lifetime(Duration::from_secs(7200))
.sqlx_logging(true);
let pool = Database::connect(opt)
.await
.map_err(|e| anyhow::anyhow!("数据库连接失败: {}", e))?;
tracing::info!("已连接到数据库: {}", sanitize_url(&url));
Ok(pool)
}
/// 隐藏 URL 中的敏感信息(用于日志输出)
fn sanitize_url(url: &str) -> String {
// 隐藏密码:mysql://user:password@host -> mysql://user:***@host
if let Some(at_pos) = url.find('@') {
if let Some(scheme_end) = url.find("://") {
if scheme_end < at_pos {
return format!("{}***@{}", &url[..scheme_end + 3], &url[at_pos + 1..]);
}
}
}
url.to_string()
}
/// 健康检查(保持向后兼容)
pub async fn health_check(pool: &DbPool) -> anyhow::Result<()> {
// 使用官方推荐的 ping 方法
pool.ping()
.await
.map_err(|e| anyhow::anyhow!("数据库健康检查失败: {}", e))
}
/// 初始化数据库和表结构
/// 每次启动时检查数据库和表是否存在,不存在则创建
pub async fn init_database(config: &DatabaseConfig) -> anyhow::Result<DatabaseConnection> {
match config.database_type {
DatabaseType::MySQL => {
init_mysql_database(config).await?;
}
DatabaseType::PostgreSQL => {
init_postgresql_database(config).await?;
}
DatabaseType::SQLite => {
// 确保 SQLite 数据库文件的目录存在
init_sqlite_database(config).await?;
}
}
// 连接到数据库
let pool = create_pool(config).await?;
// 创建表
create_tables(&pool).await?;
migrate_existing_tables(&pool).await?;
create_indexes(&pool).await?;
Ok(pool)
}
async fn create_indexes(db: &DatabaseConnection) -> anyhow::Result<()> {
let backend = db.get_database_backend();
let statements = match backend {
DbBackend::MySql => vec!["CREATE INDEX idx_users_email ON users(email)", "CREATE INDEX idx_email_logs_user_created ON email_logs(user_id, created_at)"],
_ => vec!["CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)", "CREATE INDEX IF NOT EXISTS idx_email_logs_user_created ON email_logs(user_id, created_at)"],
};
for statement in statements {
if let Err(error) = db.execute(Statement::from_string(backend, statement)).await {
let message = error.to_string().to_lowercase();
if !message.contains("duplicate") && !message.contains("already exists") {
return Err(anyhow::anyhow!("创建索引失败: {error}"));
}
}
}
Ok(())
}
async fn migrate_existing_tables(db: &DatabaseConnection) -> anyhow::Result<()> {
let backend = db.get_database_backend();
let statement = match backend {
DbBackend::MySql => "ALTER TABLE users ADD COLUMN deleted_at DATETIME NULL",
DbBackend::Postgres => "ALTER TABLE users ADD COLUMN deleted_at TIMESTAMP NULL",
DbBackend::Sqlite => "ALTER TABLE users ADD COLUMN deleted_at DATETIME NULL",
};
if let Err(error) = db.execute(Statement::from_string(backend, statement)).await {
let message = error.to_string().to_lowercase();
if !message.contains("duplicate") && !message.contains("already exists") {
return Err(anyhow::anyhow!("迁移 users.deleted_at 失败: {error}"));
}
}
Ok(())
}
/// 获取端口号(根据数据库类型返回默认值)
fn get_database_port(config: &DatabaseConfig) -> u16 {
config.port.unwrap_or(match config.database_type {
DatabaseType::MySQL => 3306,
DatabaseType::PostgreSQL => 5432,
DatabaseType::SQLite => 0,
})
}
/// 为 MySQL 创建数据库(如果不存在)
async fn init_mysql_database(config: &DatabaseConfig) -> anyhow::Result<()> {
let database_name = config
.database
.as_ref()
.ok_or_else(|| anyhow::anyhow!("MySQL 需要配置 database.database"))?;
validate_database_name(database_name)?;
let host = config
.host
.as_ref()
.ok_or_else(|| anyhow::anyhow!("MySQL 需要配置 database.host"))?;
let user = config
.user
.as_ref()
.ok_or_else(|| anyhow::anyhow!("MySQL 需要配置 database.user"))?;
let password = config
.password
.as_ref()
.ok_or_else(|| anyhow::anyhow!("MySQL 需要配置 database.password"))?;
// 连接到 MySQL 服务器(不指定数据库)
let url = format!(
"mysql://{}:{}@{}:{}",
user,
password,
host,
get_database_port(config)
);
let mut opt = ConnectOptions::new(&url);
opt.max_connections(1)
.connect_timeout(Duration::from_secs(8))
.sqlx_logging(true);
let conn = Database::connect(opt)
.await
.map_err(|e| anyhow::anyhow!("连接 MySQL 服务器失败: {}", e))?;
// 检查数据库是否存在,不存在则创建
let query = format!(
"CREATE DATABASE IF NOT EXISTS `{}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci",
database_name
);
conn.execute(Statement::from_string(
sea_orm::DatabaseBackend::MySql,
query,
))
.await
.map_err(|e| anyhow::anyhow!("创建 MySQL 数据库失败: {}", e))?;
tracing::info!("✅ MySQL 数据库 '{}' 检查完成", database_name);
Ok(())
}
/// 为 PostgreSQL 创建数据库(如果不存在)
async fn init_postgresql_database(config: &DatabaseConfig) -> anyhow::Result<()> {
let database_name = config
.database
.as_ref()
.ok_or_else(|| anyhow::anyhow!("PostgreSQL 需要配置 database.database"))?;
validate_database_name(database_name)?;
let host = config
.host
.as_ref()
.ok_or_else(|| anyhow::anyhow!("PostgreSQL 需要配置 database.host"))?;
let user = config
.user
.as_ref()
.ok_or_else(|| anyhow::anyhow!("PostgreSQL 需要配置 database.user"))?;
let password = config
.password
.as_ref()
.ok_or_else(|| anyhow::anyhow!("PostgreSQL 需要配置 database.password"))?;
// 连接到 PostgreSQL 默认数据库(postgres
let url = format!(
"postgresql://{}:{}@{}:{}/postgres",
user,
password,
host,
get_database_port(config)
);
let mut opt = ConnectOptions::new(&url);
opt.max_connections(1)
.connect_timeout(Duration::from_secs(8))
.sqlx_logging(true);
let conn = Database::connect(opt)
.await
.map_err(|e| anyhow::anyhow!("连接 PostgreSQL 服务器失败: {}", e))?;
// 检查数据库是否存在,不存在则创建
// PostgreSQL 不支持 CREATE DATABASE IF NOT EXISTS,需要先查询
let check_query = format!(
"SELECT 1 FROM pg_database WHERE datname='{}'",
database_name
);
let result = conn
.query_one(Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
check_query,
))
.await
.map_err(|e| anyhow::anyhow!("检查 PostgreSQL 数据库失败: {e}"))?;
match result {
Some(_) => {
tracing::info!("PostgreSQL 数据库 '{}' 已存在", database_name);
}
None => {
// 数据库不存在,创建它
let create_query = format!(
"CREATE DATABASE {} WITH ENCODING 'UTF8' LC_COLLATE='en_US.UTF-8' LC_CTYPE='en_US.UTF-8'",
database_name
);
conn.execute(Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
create_query,
))
.await
.map_err(|e| anyhow::anyhow!("创建 PostgreSQL 数据库失败: {}", e))?;
tracing::info!("✅ PostgreSQL 数据库 '{}' 创建成功", database_name);
}
}
Ok(())
}
fn validate_database_name(name: &str) -> anyhow::Result<()> {
if name.is_empty()
|| !name
.bytes()
.all(|value| value.is_ascii_alphanumeric() || value == b'_')
{
anyhow::bail!("数据库名称只能包含字母、数字和下划线");
}
Ok(())
}
/// 为 SQLite 确保数据库文件目录存在
async fn init_sqlite_database(config: &DatabaseConfig) -> anyhow::Result<()> {
let path = config
.path
.as_ref()
.ok_or_else(|| anyhow::anyhow!("SQLite 需要配置 database.path"))?;
// 如果是相对路径,转换为绝对路径
let absolute_path = if path.is_absolute() {
path.clone()
} else {
std::env::current_dir()
.map_err(|e| anyhow::anyhow!("获取当前目录失败: {}", e))?
.join(path)
};
tracing::info!("SQLite 数据库路径: {}", absolute_path.display());
// 获取数据库文件的父目录
if let Some(parent) = absolute_path.parent() {
// 如果父目录不存在,则创建
if !parent.exists() {
std::fs::create_dir_all(parent)
.map_err(|e| anyhow::anyhow!("创建 SQLite 数据库目录失败: {}", e))?;
tracing::info!("✅ SQLite 数据库目录创建成功: {}", parent.display());
} else {
tracing::info!("SQLite 数据库目录已存在: {}", parent.display());
}
}
// 如果数据库文件不存在,创建空文件
if !absolute_path.exists() {
std::fs::File::create(&absolute_path)
.map_err(|e| anyhow::anyhow!("创建 SQLite 数据库文件失败: {}", e))?;
tracing::info!("✅ SQLite 数据库文件创建成功: {}", absolute_path.display());
} else {
tracing::info!("SQLite 数据库文件已存在: {}", absolute_path.display());
}
Ok(())
}
/// 辅助函数:创建单个表(如果不存在)
async fn create_single_table<E>(
db: &DatabaseConnection,
schema: &Schema,
builder: &DbBackend,
entity: E,
table_name: &str,
) -> anyhow::Result<()>
where
E: EntityName + EntityTrait,
{
let create_table = schema.create_table_from_entity(entity);
let sql = match builder {
DbBackend::MySql => {
use sea_orm::sea_query::MysqlQueryBuilder;
create_table.to_string(MysqlQueryBuilder {})
}
DbBackend::Postgres => {
use sea_orm::sea_query::PostgresQueryBuilder;
create_table.to_string(PostgresQueryBuilder {})
}
DbBackend::Sqlite => {
use sea_orm::sea_query::SqliteQueryBuilder;
create_table.to_string(SqliteQueryBuilder {})
}
};
let sql = sql.replace("CREATE TABLE", "CREATE TABLE IF NOT EXISTS");
match db.execute(Statement::from_string(*builder, sql)).await {
Ok(_) => {
tracing::info!("✅ {}检查完成", table_name);
}
Err(e) => {
let err_msg = e.to_string();
if err_msg.contains("already exists")
|| (err_msg.contains("table") && err_msg.contains("exists"))
{
tracing::info!("✅ {}已存在", table_name);
} else {
return Err(anyhow::anyhow!("创建{}失败: {}", table_name, e));
}
}
}
Ok(())
}
/// 创建数据库表结构
async fn create_tables(db: &DatabaseConnection) -> anyhow::Result<()> {
tracing::info!("检查数据库表结构...");
let builder = db.get_database_backend();
let schema = Schema::new(builder);
// 导入所有 entities
use crate::domain::entities::{email_logs, user_profiles, users};
// 创建所有表(添加新表只需一行!)
create_single_table(db, &schema, &builder, users::Entity, "用户表").await?;
create_single_table(db, &schema, &builder, user_profiles::Entity, "用户资料表").await?;
create_single_table(db, &schema, &builder, email_logs::Entity, "邮件日志表").await?;
tracing::info!("✅ 数据库表结构检查完成");
Ok(())
}