功能修复: 1. 患者创建空名称验证:后端添加 name.trim().is_empty() 检查 2. 仪表盘统计容错:单个查询失败返回零值而非 500 3. FHIR 路由修复:从 /fhir 移到 /api/v1/fhir 保持一致 4. 冻结模块后端中间件:新增 frozen_module_middleware 拦截冻结路径 5. 积分端点权限码:health.health-data.list → health.points.list 6. 角色权限迁移:护士补充 devices.list,运营补充 points.list/manage 7. 测试结果文档:R01-R05 角色测试 + T00/T10 结果归档 Clippy 全 workspace 清零(14→0 errors): - erp-core: 修复 empty doc line、collapsible if、redundant closure 等 9 处 - erp-health: 修复 too_many_arguments、unused var、unnecessary parens 等 58 处 - erp-ai: 修复 dead_code、unused import 等 11 处 - erp-plugin: 修复 too_many_arguments、wildcard pattern 等 11 处 - erp-server-migration: 修复 enum_variant_names 5 处 - erp-auth/config/workflow/message: 各 1-3 处 工程改进: - lint-staged 配置迁移到 .lintstagedrc.js(函数式避免文件列表传给 clippy) - cargo fmt 统一格式化
115 lines
4.0 KiB
Rust
115 lines
4.0 KiB
Rust
use sea_orm::{ConnectionTrait, Database, DatabaseBackend, Statement};
|
||
use std::sync::Arc;
|
||
|
||
use erp_server_migration::MigratorTrait;
|
||
|
||
/// 全局信号量:限制同时创建数据库的测试数量,避免 PostgreSQL 连接耗尽
|
||
static DB_SEMAPHORE: std::sync::OnceLock<Arc<tokio::sync::Semaphore>> = std::sync::OnceLock::new();
|
||
|
||
fn db_semaphore() -> &'static Arc<tokio::sync::Semaphore> {
|
||
DB_SEMAPHORE.get_or_init(|| Arc::new(tokio::sync::Semaphore::new(4)))
|
||
}
|
||
|
||
/// 测试数据库 — 使用本地 PostgreSQL 创建隔离测试库
|
||
///
|
||
/// 连接本地 PostgreSQL(wiki/infrastructure.md 配置),为每个测试创建独立的测试数据库。
|
||
/// 不依赖 Docker/Testcontainers,与开发环境一致。
|
||
pub struct TestDb {
|
||
db: Option<sea_orm::DatabaseConnection>,
|
||
db_name: String,
|
||
_permit: Option<tokio::sync::OwnedSemaphorePermit>,
|
||
}
|
||
|
||
impl TestDb {
|
||
pub async fn new() -> Self {
|
||
let permit = db_semaphore()
|
||
.clone()
|
||
.acquire_owned()
|
||
.await
|
||
.expect("信号量获取失败");
|
||
|
||
let db_name = format!("erp_test_{}", uuid::Uuid::now_v7().simple());
|
||
|
||
let admin_url = std::env::var("TEST_DB_URL")
|
||
.unwrap_or_else(|_| "postgres://postgres:123123@localhost:5432/postgres".to_string());
|
||
|
||
let admin_db = Database::connect(&admin_url)
|
||
.await
|
||
.expect("连接本地 PostgreSQL 失败,请确认服务正在运行");
|
||
|
||
admin_db
|
||
.execute(Statement::from_string(
|
||
DatabaseBackend::Postgres,
|
||
format!("CREATE DATABASE \"{}\"", db_name),
|
||
))
|
||
.await
|
||
.expect("创建测试数据库失败");
|
||
|
||
drop(admin_db);
|
||
|
||
// 从 admin_url 推导测试库 URL(替换路径部分)
|
||
let test_url = if let Some(pos) = admin_url.rfind('/') {
|
||
format!("{}/{}", &admin_url[..pos], db_name)
|
||
} else {
|
||
format!("postgres://postgres:123123@localhost:5432/{}", db_name)
|
||
};
|
||
let db = Database::connect(&test_url)
|
||
.await
|
||
.expect("连接测试数据库失败");
|
||
|
||
// 运行所有迁移
|
||
erp_server_migration::Migrator::up(&db, None)
|
||
.await
|
||
.expect("执行数据库迁移失败");
|
||
|
||
Self {
|
||
db: Some(db),
|
||
db_name,
|
||
_permit: Some(permit),
|
||
}
|
||
}
|
||
|
||
/// 获取数据库连接引用
|
||
pub fn db(&self) -> &sea_orm::DatabaseConnection {
|
||
self.db.as_ref().expect("数据库连接已被释放")
|
||
}
|
||
}
|
||
|
||
impl Drop for TestDb {
|
||
fn drop(&mut self) {
|
||
let db_name = self.db_name.clone();
|
||
self.db.take();
|
||
|
||
// 尝试在独立线程中清理,避免在 tokio runtime 内创建新 runtime
|
||
let _ = std::thread::spawn(move || {
|
||
let rt = tokio::runtime::Builder::new_current_thread()
|
||
.enable_all()
|
||
.build();
|
||
if let Ok(rt) = rt {
|
||
rt.block_on(async {
|
||
let admin_url = std::env::var("TEST_DB_URL")
|
||
.unwrap_or_else(|_| "postgres://postgres:123123@localhost:5432/postgres".to_string());
|
||
if let Ok(admin_db) = Database::connect(&admin_url).await {
|
||
let disconnect_sql = format!(
|
||
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '{}'",
|
||
db_name
|
||
);
|
||
admin_db
|
||
.execute(Statement::from_string(DatabaseBackend::Postgres, disconnect_sql))
|
||
.await
|
||
.ok();
|
||
|
||
admin_db
|
||
.execute(Statement::from_string(
|
||
DatabaseBackend::Postgres,
|
||
format!("DROP DATABASE IF EXISTS \"{}\"", db_name),
|
||
))
|
||
.await
|
||
.ok();
|
||
}
|
||
});
|
||
}
|
||
});
|
||
}
|
||
}
|