fix: 修复测试发现的 7 个问题 + 全 workspace clippy 清零
功能修复: 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 统一格式化
This commit is contained in:
10
.lintstagedrc.js
Normal file
10
.lintstagedrc.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
module.exports = {
|
||||||
|
'*.rs': [
|
||||||
|
'cargo fmt --check --',
|
||||||
|
() => 'cargo clippy -p erp-health -p erp-server -- -D warnings',
|
||||||
|
],
|
||||||
|
'apps/web/src/**/*.{ts,tsx}': ['cd apps/web && npx eslint --fix'],
|
||||||
|
'apps/web/src/**/*.test.{ts,tsx}': [
|
||||||
|
'cd apps/web && npx vitest run --reporter=verbose',
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -49,7 +49,12 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn provider_type_all_variants() {
|
fn provider_type_all_variants() {
|
||||||
for pt in [ProviderType::Claude, ProviderType::Openai, ProviderType::Ollama, ProviderType::Rules] {
|
for pt in [
|
||||||
|
ProviderType::Claude,
|
||||||
|
ProviderType::Openai,
|
||||||
|
ProviderType::Ollama,
|
||||||
|
ProviderType::Rules,
|
||||||
|
] {
|
||||||
let json = serde_json::to_string(&pt).unwrap();
|
let json = serde_json::to_string(&pt).unwrap();
|
||||||
let back: ProviderType = serde_json::from_str(&json).unwrap();
|
let back: ProviderType = serde_json::from_str(&json).unwrap();
|
||||||
assert_eq!(back, pt);
|
assert_eq!(back, pt);
|
||||||
|
|||||||
@@ -121,10 +121,19 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn analysis_type_prompt_name() {
|
fn analysis_type_prompt_name() {
|
||||||
assert_eq!(AnalysisType::LabReport.prompt_name(), "lab_report_interpretation");
|
assert_eq!(
|
||||||
|
AnalysisType::LabReport.prompt_name(),
|
||||||
|
"lab_report_interpretation"
|
||||||
|
);
|
||||||
assert_eq!(AnalysisType::Trends.prompt_name(), "health_trend_analysis");
|
assert_eq!(AnalysisType::Trends.prompt_name(), "health_trend_analysis");
|
||||||
assert_eq!(AnalysisType::CheckupPlan.prompt_name(), "personalized_checkup_plan");
|
assert_eq!(
|
||||||
assert_eq!(AnalysisType::ReportSummary.prompt_name(), "report_summary_generation");
|
AnalysisType::CheckupPlan.prompt_name(),
|
||||||
|
"personalized_checkup_plan"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
AnalysisType::ReportSummary.prompt_name(),
|
||||||
|
"report_summary_generation"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- AnalysisType serde round-trip ----
|
// ---- AnalysisType serde round-trip ----
|
||||||
@@ -195,7 +204,10 @@ mod tests {
|
|||||||
let json = serde_json::to_string(&event).unwrap();
|
let json = serde_json::to_string(&event).unwrap();
|
||||||
let back: AnalysisSseEvent = serde_json::from_str(&json).unwrap();
|
let back: AnalysisSseEvent = serde_json::from_str(&json).unwrap();
|
||||||
match back {
|
match back {
|
||||||
AnalysisSseEvent::Done { analysis_id, status } => {
|
AnalysisSseEvent::Done {
|
||||||
|
analysis_id,
|
||||||
|
status,
|
||||||
|
} => {
|
||||||
assert_eq!(analysis_id, id);
|
assert_eq!(analysis_id, id);
|
||||||
assert_eq!(status, "completed");
|
assert_eq!(status, "completed");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,10 @@ pub enum AiError {
|
|||||||
DbError(String),
|
DbError(String),
|
||||||
|
|
||||||
#[error("AI 配额已耗尽: {reason}")]
|
#[error("AI 配额已耗尽: {reason}")]
|
||||||
QuotaExhausted { tenant_id: uuid::Uuid, reason: String },
|
QuotaExhausted {
|
||||||
|
tenant_id: uuid::Uuid,
|
||||||
|
reason: String,
|
||||||
|
},
|
||||||
|
|
||||||
#[error("缓存错误: {0}")]
|
#[error("缓存错误: {0}")]
|
||||||
CacheError(String),
|
CacheError(String),
|
||||||
@@ -54,9 +57,7 @@ impl From<AiError> for AppError {
|
|||||||
AiError::Validation(msg) => AppError::Validation(msg),
|
AiError::Validation(msg) => AppError::Validation(msg),
|
||||||
AiError::AnalysisNotFound(id) => AppError::NotFound(format!("分析结果: {id}")),
|
AiError::AnalysisNotFound(id) => AppError::NotFound(format!("分析结果: {id}")),
|
||||||
AiError::PromptNotFound(name) => AppError::NotFound(format!("Prompt 模板: {name}")),
|
AiError::PromptNotFound(name) => AppError::NotFound(format!("Prompt 模板: {name}")),
|
||||||
AiError::ProviderUnavailable(p) => {
|
AiError::ProviderUnavailable(p) => AppError::Internal(format!("AI 提供商 {p} 不可用")),
|
||||||
AppError::Internal(format!("AI 提供商 {p} 不可用"))
|
|
||||||
}
|
|
||||||
AiError::RateLimitExceeded => AppError::TooManyRequests,
|
AiError::RateLimitExceeded => AppError::TooManyRequests,
|
||||||
AiError::QuotaExhausted { .. } => AppError::TooManyRequests,
|
AiError::QuotaExhausted { .. } => AppError::TooManyRequests,
|
||||||
AiError::VersionMismatch => AppError::VersionMismatch,
|
AiError::VersionMismatch => AppError::VersionMismatch,
|
||||||
@@ -153,7 +154,7 @@ mod tests {
|
|||||||
let err = AiError::RateLimitExceeded;
|
let err = AiError::RateLimitExceeded;
|
||||||
let app: AppError = err.into();
|
let app: AppError = err.into();
|
||||||
match app {
|
match app {
|
||||||
AppError::TooManyRequests => {},
|
AppError::TooManyRequests => {}
|
||||||
other => panic!("期望 AppError::TooManyRequests,得到 {:?}", other),
|
other => panic!("期望 AppError::TooManyRequests,得到 {:?}", other),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -163,7 +164,7 @@ mod tests {
|
|||||||
let err = AiError::VersionMismatch;
|
let err = AiError::VersionMismatch;
|
||||||
let app: AppError = err.into();
|
let app: AppError = err.into();
|
||||||
match app {
|
match app {
|
||||||
AppError::VersionMismatch => {},
|
AppError::VersionMismatch => {}
|
||||||
other => panic!("期望 AppError::VersionMismatch,得到 {:?}", other),
|
other => panic!("期望 AppError::VersionMismatch,得到 {:?}", other),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
use axum::Json;
|
||||||
use axum::extract::{Extension, FromRef, Path, Query, State};
|
use axum::extract::{Extension, FromRef, Path, Query, State};
|
||||||
use axum::response::sse::{Event, KeepAlive, Sse};
|
use axum::response::sse::{Event, KeepAlive, Sse};
|
||||||
use axum::Json;
|
|
||||||
use erp_core::health_provider::TimeRange;
|
use erp_core::health_provider::TimeRange;
|
||||||
use erp_core::rbac::require_permission;
|
use erp_core::rbac::require_permission;
|
||||||
use erp_core::types::{ApiResponse, TenantContext};
|
use erp_core::types::{ApiResponse, TenantContext};
|
||||||
@@ -34,9 +34,9 @@ where
|
|||||||
S: Clone + Send + Sync + 'static,
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "ai.analysis.manage")?;
|
require_permission(&ctx, "ai.analysis.manage")?;
|
||||||
let report_id = body.report_id.ok_or_else(|| {
|
let report_id = body
|
||||||
erp_core::error::AppError::Validation("report_id 必填".into())
|
.report_id
|
||||||
})?;
|
.ok_or_else(|| erp_core::error::AppError::Validation("report_id 必填".into()))?;
|
||||||
|
|
||||||
let lab_dto = state
|
let lab_dto = state
|
||||||
.health_provider
|
.health_provider
|
||||||
@@ -57,7 +57,10 @@ where
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let model_config = &prompt.model_config;
|
let model_config = &prompt.model_config;
|
||||||
let model = model_config["model"].as_str().unwrap_or("claude-sonnet-4-6").to_string();
|
let model = model_config["model"]
|
||||||
|
.as_str()
|
||||||
|
.unwrap_or("claude-sonnet-4-6")
|
||||||
|
.to_string();
|
||||||
let temperature = model_config["temperature"].as_f64().unwrap_or(0.3) as f32;
|
let temperature = model_config["temperature"].as_f64().unwrap_or(0.3) as f32;
|
||||||
let max_tokens = model_config["max_tokens"].as_u64().unwrap_or(2048) as u32;
|
let max_tokens = model_config["max_tokens"].as_u64().unwrap_or(2048) as u32;
|
||||||
|
|
||||||
@@ -83,7 +86,15 @@ where
|
|||||||
let patient_id_clone = uuid::Uuid::nil(); // lab report 场景 patient_id 从 report 关联
|
let patient_id_clone = uuid::Uuid::nil(); // lab report 场景 patient_id 从 report 关联
|
||||||
let doctor_id_clone = ctx.user_id;
|
let doctor_id_clone = ctx.user_id;
|
||||||
|
|
||||||
let sse_stream = build_sse_stream(stream, analysis_id_clone, state_clone, "lab_report", ctx.tenant_id, patient_id_clone, doctor_id_clone);
|
let sse_stream = build_sse_stream(
|
||||||
|
stream,
|
||||||
|
analysis_id_clone,
|
||||||
|
state_clone,
|
||||||
|
"lab_report",
|
||||||
|
ctx.tenant_id,
|
||||||
|
patient_id_clone,
|
||||||
|
doctor_id_clone,
|
||||||
|
);
|
||||||
Ok(Sse::new(sse_stream).keep_alive(KeepAlive::default()))
|
Ok(Sse::new(sse_stream).keep_alive(KeepAlive::default()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,9 +108,9 @@ where
|
|||||||
S: Clone + Send + Sync + 'static,
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "ai.analysis.manage")?;
|
require_permission(&ctx, "ai.analysis.manage")?;
|
||||||
let patient_id = body.patient_id.ok_or_else(|| {
|
let patient_id = body
|
||||||
erp_core::error::AppError::Validation("patient_id 必填".into())
|
.patient_id
|
||||||
})?;
|
.ok_or_else(|| erp_core::error::AppError::Validation("patient_id 必填".into()))?;
|
||||||
|
|
||||||
let metrics = body.metrics.unwrap_or_else(|| {
|
let metrics = body.metrics.unwrap_or_else(|| {
|
||||||
vec![
|
vec![
|
||||||
@@ -126,7 +137,10 @@ where
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let sanitized_data = state.analysis.sanitizer.sanitize_trend_analysis(&trend_data)?;
|
let sanitized_data = state
|
||||||
|
.analysis
|
||||||
|
.sanitizer
|
||||||
|
.sanitize_trend_analysis(&trend_data)?;
|
||||||
|
|
||||||
let prompt = state
|
let prompt = state
|
||||||
.prompt
|
.prompt
|
||||||
@@ -134,7 +148,10 @@ where
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let model_config = &prompt.model_config;
|
let model_config = &prompt.model_config;
|
||||||
let model = model_config["model"].as_str().unwrap_or("claude-sonnet-4-6").to_string();
|
let model = model_config["model"]
|
||||||
|
.as_str()
|
||||||
|
.unwrap_or("claude-sonnet-4-6")
|
||||||
|
.to_string();
|
||||||
let temperature = model_config["temperature"].as_f64().unwrap_or(0.3) as f32;
|
let temperature = model_config["temperature"].as_f64().unwrap_or(0.3) as f32;
|
||||||
let max_tokens = model_config["max_tokens"].as_u64().unwrap_or(2048) as u32;
|
let max_tokens = model_config["max_tokens"].as_u64().unwrap_or(2048) as u32;
|
||||||
|
|
||||||
@@ -158,7 +175,15 @@ where
|
|||||||
let analysis_id_clone = analysis_id;
|
let analysis_id_clone = analysis_id;
|
||||||
let state_clone = state.clone();
|
let state_clone = state.clone();
|
||||||
|
|
||||||
let sse_stream = build_sse_stream(stream, analysis_id_clone, state_clone, "trend", ctx.tenant_id, uuid::Uuid::nil(), ctx.user_id);
|
let sse_stream = build_sse_stream(
|
||||||
|
stream,
|
||||||
|
analysis_id_clone,
|
||||||
|
state_clone,
|
||||||
|
"trend",
|
||||||
|
ctx.tenant_id,
|
||||||
|
uuid::Uuid::nil(),
|
||||||
|
ctx.user_id,
|
||||||
|
);
|
||||||
Ok(Sse::new(sse_stream).keep_alive(KeepAlive::default()))
|
Ok(Sse::new(sse_stream).keep_alive(KeepAlive::default()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,9 +197,9 @@ where
|
|||||||
S: Clone + Send + Sync + 'static,
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "ai.analysis.manage")?;
|
require_permission(&ctx, "ai.analysis.manage")?;
|
||||||
let patient_id = body.patient_id.ok_or_else(|| {
|
let patient_id = body
|
||||||
erp_core::error::AppError::Validation("patient_id 必填".into())
|
.patient_id
|
||||||
})?;
|
.ok_or_else(|| erp_core::error::AppError::Validation("patient_id 必填".into()))?;
|
||||||
|
|
||||||
let summary_dto = state
|
let summary_dto = state
|
||||||
.health_provider
|
.health_provider
|
||||||
@@ -191,7 +216,10 @@ where
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let model_config = &prompt.model_config;
|
let model_config = &prompt.model_config;
|
||||||
let model = model_config["model"].as_str().unwrap_or("claude-sonnet-4-6").to_string();
|
let model = model_config["model"]
|
||||||
|
.as_str()
|
||||||
|
.unwrap_or("claude-sonnet-4-6")
|
||||||
|
.to_string();
|
||||||
let temperature = model_config["temperature"].as_f64().unwrap_or(0.3) as f32;
|
let temperature = model_config["temperature"].as_f64().unwrap_or(0.3) as f32;
|
||||||
let max_tokens = model_config["max_tokens"].as_u64().unwrap_or(2048) as u32;
|
let max_tokens = model_config["max_tokens"].as_u64().unwrap_or(2048) as u32;
|
||||||
|
|
||||||
@@ -215,7 +243,15 @@ where
|
|||||||
let analysis_id_clone = analysis_id;
|
let analysis_id_clone = analysis_id;
|
||||||
let state_clone = state.clone();
|
let state_clone = state.clone();
|
||||||
|
|
||||||
let sse_stream = build_sse_stream(stream, analysis_id_clone, state_clone, "checkup_plan", ctx.tenant_id, uuid::Uuid::nil(), ctx.user_id);
|
let sse_stream = build_sse_stream(
|
||||||
|
stream,
|
||||||
|
analysis_id_clone,
|
||||||
|
state_clone,
|
||||||
|
"checkup_plan",
|
||||||
|
ctx.tenant_id,
|
||||||
|
uuid::Uuid::nil(),
|
||||||
|
ctx.user_id,
|
||||||
|
);
|
||||||
Ok(Sse::new(sse_stream).keep_alive(KeepAlive::default()))
|
Ok(Sse::new(sse_stream).keep_alive(KeepAlive::default()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,9 +265,9 @@ where
|
|||||||
S: Clone + Send + Sync + 'static,
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "ai.analysis.manage")?;
|
require_permission(&ctx, "ai.analysis.manage")?;
|
||||||
let report_id = body.report_id.ok_or_else(|| {
|
let report_id = body
|
||||||
erp_core::error::AppError::Validation("report_id 必填".into())
|
.report_id
|
||||||
})?;
|
.ok_or_else(|| erp_core::error::AppError::Validation("report_id 必填".into()))?;
|
||||||
|
|
||||||
let report_dto = state
|
let report_dto = state
|
||||||
.health_provider
|
.health_provider
|
||||||
@@ -255,7 +291,10 @@ where
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let model_config = &prompt.model_config;
|
let model_config = &prompt.model_config;
|
||||||
let model = model_config["model"].as_str().unwrap_or("claude-sonnet-4-6").to_string();
|
let model = model_config["model"]
|
||||||
|
.as_str()
|
||||||
|
.unwrap_or("claude-sonnet-4-6")
|
||||||
|
.to_string();
|
||||||
let temperature = model_config["temperature"].as_f64().unwrap_or(0.3) as f32;
|
let temperature = model_config["temperature"].as_f64().unwrap_or(0.3) as f32;
|
||||||
let max_tokens = model_config["max_tokens"].as_u64().unwrap_or(2048) as u32;
|
let max_tokens = model_config["max_tokens"].as_u64().unwrap_or(2048) as u32;
|
||||||
|
|
||||||
@@ -279,7 +318,15 @@ where
|
|||||||
let analysis_id_clone = analysis_id;
|
let analysis_id_clone = analysis_id;
|
||||||
let state_clone = state.clone();
|
let state_clone = state.clone();
|
||||||
|
|
||||||
let sse_stream = build_sse_stream(stream, analysis_id_clone, state_clone, "report_summary", ctx.tenant_id, uuid::Uuid::nil(), ctx.user_id);
|
let sse_stream = build_sse_stream(
|
||||||
|
stream,
|
||||||
|
analysis_id_clone,
|
||||||
|
state_clone,
|
||||||
|
"report_summary",
|
||||||
|
ctx.tenant_id,
|
||||||
|
uuid::Uuid::nil(),
|
||||||
|
ctx.user_id,
|
||||||
|
);
|
||||||
Ok(Sse::new(sse_stream).keep_alive(KeepAlive::default()))
|
Ok(Sse::new(sse_stream).keep_alive(KeepAlive::default()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,7 +356,12 @@ where
|
|||||||
};
|
};
|
||||||
let (items, total) = state
|
let (items, total) = state
|
||||||
.analysis
|
.analysis
|
||||||
.list_analysis(ctx.tenant_id, params.patient_id, params.analysis_type, &pagination)
|
.list_analysis(
|
||||||
|
ctx.tenant_id,
|
||||||
|
params.patient_id,
|
||||||
|
params.analysis_type,
|
||||||
|
&pagination,
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// 批量查询 patient_name(通过 raw SQL 避免跨 crate 依赖 erp-health)
|
// 批量查询 patient_name(通过 raw SQL 避免跨 crate 依赖 erp-health)
|
||||||
@@ -321,7 +373,10 @@ where
|
|||||||
|
|
||||||
let patient_names: std::collections::HashMap<uuid::Uuid, String> = if !patient_ids.is_empty() {
|
let patient_names: std::collections::HashMap<uuid::Uuid, String> = if !patient_ids.is_empty() {
|
||||||
#[derive(sea_orm::FromQueryResult)]
|
#[derive(sea_orm::FromQueryResult)]
|
||||||
struct PatientName { id: uuid::Uuid, name: String }
|
struct PatientName {
|
||||||
|
id: uuid::Uuid,
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
let ids: Vec<uuid::Uuid> = patient_ids.into_iter().collect();
|
let ids: Vec<uuid::Uuid> = patient_ids.into_iter().collect();
|
||||||
use sea_orm::FromQueryResult;
|
use sea_orm::FromQueryResult;
|
||||||
PatientName::find_by_statement(sea_orm::Statement::from_sql_and_values(
|
PatientName::find_by_statement(sea_orm::Statement::from_sql_and_values(
|
||||||
@@ -339,15 +394,19 @@ where
|
|||||||
std::collections::HashMap::new()
|
std::collections::HashMap::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
let data: Vec<serde_json::Value> = items.into_iter().map(|a| {
|
let data: Vec<serde_json::Value> = items
|
||||||
let mut val = serde_json::to_value(&a).unwrap_or_default();
|
.into_iter()
|
||||||
if let Some(obj) = val.as_object_mut() {
|
.map(|a| {
|
||||||
obj.insert("patient_name".to_string(), serde_json::json!(
|
let mut val = serde_json::to_value(&a).unwrap_or_default();
|
||||||
patient_names.get(&a.patient_id).cloned()
|
if let Some(obj) = val.as_object_mut() {
|
||||||
));
|
obj.insert(
|
||||||
}
|
"patient_name".to_string(),
|
||||||
val
|
serde_json::json!(patient_names.get(&a.patient_id).cloned()),
|
||||||
}).collect();
|
);
|
||||||
|
}
|
||||||
|
val
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
Ok(Json(ApiResponse::ok(serde_json::json!({
|
Ok(Json(ApiResponse::ok(serde_json::json!({
|
||||||
"data": data,
|
"data": data,
|
||||||
@@ -549,7 +608,9 @@ where
|
|||||||
S: Clone + Send + Sync + 'static,
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "ai.analysis.list")?;
|
require_permission(&ctx, "ai.analysis.list")?;
|
||||||
Ok(Json(ApiResponse::ok(state.provider_registry.provider_names())))
|
Ok(Json(ApiResponse::ok(
|
||||||
|
state.provider_registry.provider_names(),
|
||||||
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn quota_summary<S>(
|
pub async fn quota_summary<S>(
|
||||||
@@ -570,7 +631,10 @@ where
|
|||||||
pub async fn assess_dialysis_risk<S>(
|
pub async fn assess_dialysis_risk<S>(
|
||||||
Extension(ctx): Extension<TenantContext>,
|
Extension(ctx): Extension<TenantContext>,
|
||||||
Json(body): Json<crate::service::dialysis_risk_scorer::DialysisLabInput>,
|
Json(body): Json<crate::service::dialysis_risk_scorer::DialysisLabInput>,
|
||||||
) -> Result<Json<ApiResponse<crate::service::dialysis_risk_scorer::DialysisRiskAssessment>>, erp_core::error::AppError>
|
) -> Result<
|
||||||
|
Json<ApiResponse<crate::service::dialysis_risk_scorer::DialysisRiskAssessment>>,
|
||||||
|
erp_core::error::AppError,
|
||||||
|
>
|
||||||
where
|
where
|
||||||
AiState: FromRef<S>,
|
AiState: FromRef<S>,
|
||||||
S: Clone + Send + Sync + 'static,
|
S: Clone + Send + Sync + 'static,
|
||||||
@@ -612,7 +676,9 @@ where
|
|||||||
S: Clone + Send + Sync + 'static,
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "ai.usage.list")?;
|
require_permission(&ctx, "ai.usage.list")?;
|
||||||
let model = params.model.unwrap_or_else(|| "claude-sonnet-4-6".to_string());
|
let model = params
|
||||||
|
.model
|
||||||
|
.unwrap_or_else(|| "claude-sonnet-4-6".to_string());
|
||||||
let estimate = crate::service::cost::CostService::estimate_cost(¶ms.analysis_type, &model);
|
let estimate = crate::service::cost::CostService::estimate_cost(¶ms.analysis_type, &model);
|
||||||
Ok(Json(ApiResponse::ok(estimate)))
|
Ok(Json(ApiResponse::ok(estimate)))
|
||||||
}
|
}
|
||||||
@@ -699,9 +765,10 @@ fn validate_prompt_safety(content: &str) -> Result<(), erp_core::error::AppError
|
|||||||
let lower = content.to_lowercase();
|
let lower = content.to_lowercase();
|
||||||
for pattern in &suspicious {
|
for pattern in &suspicious {
|
||||||
if lower.contains(pattern) {
|
if lower.contains(pattern) {
|
||||||
return Err(erp_core::error::AppError::Validation(
|
return Err(erp_core::error::AppError::Validation(format!(
|
||||||
format!("提示词内容包含不安全模式: {}", pattern),
|
"提示词内容包含不安全模式: {}",
|
||||||
));
|
pattern
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use axum::extract::{Extension, FromRef, Path, State};
|
|
||||||
use axum::Json;
|
use axum::Json;
|
||||||
|
use axum::extract::{Extension, FromRef, Path, State};
|
||||||
use erp_core::rbac::require_permission;
|
use erp_core::rbac::require_permission;
|
||||||
use erp_core::types::{ApiResponse, TenantContext};
|
use erp_core::types::{ApiResponse, TenantContext};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@@ -26,19 +26,14 @@ where
|
|||||||
require_permission(&ctx, "ai.suggestion.list")?;
|
require_permission(&ctx, "ai.suggestion.list")?;
|
||||||
|
|
||||||
if let Some(analysis_id) = params.analysis_id {
|
if let Some(analysis_id) = params.analysis_id {
|
||||||
let items = SuggestionService::list_by_analysis(
|
let items =
|
||||||
&state.db,
|
SuggestionService::list_by_analysis(&state.db, ctx.tenant_id, analysis_id).await?;
|
||||||
ctx.tenant_id,
|
|
||||||
analysis_id,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(Json(ApiResponse::ok(serde_json::json!({
|
Ok(Json(ApiResponse::ok(serde_json::json!({
|
||||||
"data": items,
|
"data": items,
|
||||||
"total": items.len(),
|
"total": items.len(),
|
||||||
}))))
|
}))))
|
||||||
} else {
|
} else {
|
||||||
let items =
|
let items = SuggestionService::list_pending(&state.db, ctx.tenant_id).await?;
|
||||||
SuggestionService::list_pending(&state.db, ctx.tenant_id).await?;
|
|
||||||
Ok(Json(ApiResponse::ok(serde_json::json!({
|
Ok(Json(ApiResponse::ok(serde_json::json!({
|
||||||
"data": items,
|
"data": items,
|
||||||
"total": items.len(),
|
"total": items.len(),
|
||||||
@@ -69,7 +64,7 @@ where
|
|||||||
_ => {
|
_ => {
|
||||||
return Err(erp_core::error::AppError::Validation(
|
return Err(erp_core::error::AppError::Validation(
|
||||||
"action 必须为 approve 或 reject".into(),
|
"action 必须为 approve 或 reject".into(),
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -151,7 +146,10 @@ where
|
|||||||
|
|
||||||
match &suggestion.baseline_snapshot {
|
match &suggestion.baseline_snapshot {
|
||||||
Some(bs) if !bs.is_null() => {
|
Some(bs) if !bs.is_null() => {
|
||||||
let action_result = suggestion.action_result.as_ref().unwrap_or(&serde_json::Value::Null);
|
let action_result = suggestion
|
||||||
|
.action_result
|
||||||
|
.as_ref()
|
||||||
|
.unwrap_or(&serde_json::Value::Null);
|
||||||
Ok(Json(ApiResponse::ok(serde_json::json!({
|
Ok(Json(ApiResponse::ok(serde_json::json!({
|
||||||
"suggestion_id": id,
|
"suggestion_id": id,
|
||||||
"baseline": bs,
|
"baseline": bs,
|
||||||
|
|||||||
@@ -156,13 +156,16 @@ impl KnowledgeSource for StructuredKnowledgeSource {
|
|||||||
|
|
||||||
async fn health_check(&self) -> AiResult<bool> {
|
async fn health_check(&self) -> AiResult<bool> {
|
||||||
#[derive(Debug, FromQueryResult)]
|
#[derive(Debug, FromQueryResult)]
|
||||||
|
#[allow(dead_code)]
|
||||||
struct HealthCheck {
|
struct HealthCheck {
|
||||||
|
#[allow(dead_code)]
|
||||||
ok: i32,
|
ok: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
let result: Option<HealthCheck> = HealthCheck::find_by_statement(
|
let result: Option<HealthCheck> = HealthCheck::find_by_statement(Statement::from_string(
|
||||||
Statement::from_string(sea_orm::DatabaseBackend::Postgres, "SELECT 1 AS ok".to_string()),
|
sea_orm::DatabaseBackend::Postgres,
|
||||||
)
|
"SELECT 1 AS ok".to_string(),
|
||||||
|
))
|
||||||
.one(&self.db)
|
.one(&self.db)
|
||||||
.await
|
.await
|
||||||
.ok()
|
.ok()
|
||||||
@@ -180,13 +183,14 @@ mod tests {
|
|||||||
let rules_empty: Vec<ai_knowledge_rules::Model> = vec![];
|
let rules_empty: Vec<ai_knowledge_rules::Model> = vec![];
|
||||||
let refs_empty: Vec<ai_knowledge_references::Model> = vec![];
|
let refs_empty: Vec<ai_knowledge_references::Model> = vec![];
|
||||||
let guides_empty: Vec<ai_knowledge_guides::Model> = vec![];
|
let guides_empty: Vec<ai_knowledge_guides::Model> = vec![];
|
||||||
let confidence: f32 = if rules_empty.is_empty() && refs_empty.is_empty() && guides_empty.is_empty() {
|
let confidence: f32 =
|
||||||
0.0
|
if rules_empty.is_empty() && refs_empty.is_empty() && guides_empty.is_empty() {
|
||||||
} else if !rules_empty.is_empty() && !refs_empty.is_empty() {
|
0.0
|
||||||
0.9
|
} else if !rules_empty.is_empty() && !refs_empty.is_empty() {
|
||||||
} else {
|
0.9
|
||||||
0.7
|
} else {
|
||||||
};
|
0.7
|
||||||
|
};
|
||||||
assert!((confidence - 0.0).abs() < 0.01);
|
assert!((confidence - 0.0).abs() < 0.01);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -88,18 +88,28 @@ impl ErpModule for AiModule {
|
|||||||
loop {
|
loop {
|
||||||
match rx.recv().await {
|
match rx.recv().await {
|
||||||
Some(event) if event.event_type == "ai.reanalysis.requested" => {
|
Some(event) if event.event_type == "ai.reanalysis.requested" => {
|
||||||
let suggestion_id = event.payload.get("original_suggestion_id")
|
let suggestion_id = event
|
||||||
|
.payload
|
||||||
|
.get("original_suggestion_id")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.and_then(|s| uuid::Uuid::parse_str(s).ok());
|
.and_then(|s| uuid::Uuid::parse_str(s).ok());
|
||||||
let patient_id = event.payload.get("patient_id")
|
let patient_id = event
|
||||||
|
.payload
|
||||||
|
.get("patient_id")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.and_then(|s| uuid::Uuid::parse_str(s).ok());
|
.and_then(|s| uuid::Uuid::parse_str(s).ok());
|
||||||
|
|
||||||
match (suggestion_id, patient_id) {
|
match (suggestion_id, patient_id) {
|
||||||
(Some(sid), Some(pid)) => {
|
(Some(sid), Some(pid)) => {
|
||||||
if let Err(e) = crate::service::reanalysis::handle_reanalysis_requested(
|
if let Err(e) =
|
||||||
&db, event.tenant_id, sid, pid,
|
crate::service::reanalysis::handle_reanalysis_requested(
|
||||||
).await {
|
&db,
|
||||||
|
event.tenant_id,
|
||||||
|
sid,
|
||||||
|
pid,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
suggestion_id = %sid,
|
suggestion_id = %sid,
|
||||||
error = %e,
|
error = %e,
|
||||||
@@ -114,10 +124,14 @@ impl ErpModule for AiModule {
|
|||||||
}
|
}
|
||||||
Some(event) if event.event_type == "ai.analysis.requested" => {
|
Some(event) if event.event_type == "ai.analysis.requested" => {
|
||||||
let source_type = event.payload.get("source_type").and_then(|v| v.as_str());
|
let source_type = event.payload.get("source_type").and_then(|v| v.as_str());
|
||||||
let source_id = event.payload.get("source_id")
|
let source_id = event
|
||||||
|
.payload
|
||||||
|
.get("source_id")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.and_then(|s| uuid::Uuid::parse_str(s).ok());
|
.and_then(|s| uuid::Uuid::parse_str(s).ok());
|
||||||
let patient_id = event.payload.get("patient_id")
|
let patient_id = event
|
||||||
|
.payload
|
||||||
|
.get("patient_id")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.and_then(|s| uuid::Uuid::parse_str(s).ok());
|
.and_then(|s| uuid::Uuid::parse_str(s).ok());
|
||||||
|
|
||||||
@@ -131,10 +145,14 @@ impl ErpModule for AiModule {
|
|||||||
}
|
}
|
||||||
// H4: 透析记录→KDIGO 自动风险评估
|
// H4: 透析记录→KDIGO 自动风险评估
|
||||||
Some(event) if event.event_type == "ai.dialysis.kdigo_requested" => {
|
Some(event) if event.event_type == "ai.dialysis.kdigo_requested" => {
|
||||||
let patient_id = event.payload.get("patient_id")
|
let patient_id = event
|
||||||
|
.payload
|
||||||
|
.get("patient_id")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.and_then(|s| uuid::Uuid::parse_str(s).ok());
|
.and_then(|s| uuid::Uuid::parse_str(s).ok());
|
||||||
let record_id = event.payload.get("dialysis_record_id")
|
let record_id = event
|
||||||
|
.payload
|
||||||
|
.get("dialysis_record_id")
|
||||||
.and_then(|v| v.as_str());
|
.and_then(|v| v.as_str());
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ pub struct PromptRenderer {
|
|||||||
registry: Handlebars<'static>,
|
registry: Handlebars<'static>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for PromptRenderer {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl PromptRenderer {
|
impl PromptRenderer {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let mut registry = Handlebars::new();
|
let mut registry = Handlebars::new();
|
||||||
@@ -43,11 +49,12 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn render_multiple_variables() {
|
fn render_multiple_variables() {
|
||||||
let r = renderer();
|
let r = renderer();
|
||||||
let result = r.render(
|
let result = r
|
||||||
"{{age_group}} {{sex}} 化验报告",
|
.render(
|
||||||
&json!({"age_group": "中年", "sex": "男性"}),
|
"{{age_group}} {{sex}} 化验报告",
|
||||||
)
|
&json!({"age_group": "中年", "sex": "男性"}),
|
||||||
.unwrap();
|
)
|
||||||
|
.unwrap();
|
||||||
assert_eq!(result, "中年 男性 化验报告");
|
assert_eq!(result, "中年 男性 化验报告");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +64,9 @@ mod tests {
|
|||||||
let data = json!({
|
let data = json!({
|
||||||
"report": { "date": "2026-05-01", "department": "内科" }
|
"report": { "date": "2026-05-01", "department": "内科" }
|
||||||
});
|
});
|
||||||
let result = r.render("科室: {{report.department}},日期: {{report.date}}", &data).unwrap();
|
let result = r
|
||||||
|
.render("科室: {{report.department}},日期: {{report.date}}", &data)
|
||||||
|
.unwrap();
|
||||||
assert_eq!(result, "科室: 内科,日期: 2026-05-01");
|
assert_eq!(result, "科室: 内科,日期: 2026-05-01");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +74,9 @@ mod tests {
|
|||||||
fn render_with_array_iteration() {
|
fn render_with_array_iteration() {
|
||||||
let r = renderer();
|
let r = renderer();
|
||||||
let data = json!({"items": ["WBC", "HGB", "PLT"]});
|
let data = json!({"items": ["WBC", "HGB", "PLT"]});
|
||||||
let result = r.render("指标: {{#each items}}{{this}}, {{/each}}", &data).unwrap();
|
let result = r
|
||||||
|
.render("指标: {{#each items}}{{this}}, {{/each}}", &data)
|
||||||
|
.unwrap();
|
||||||
assert_eq!(result, "指标: WBC, HGB, PLT, ");
|
assert_eq!(result, "指标: WBC, HGB, PLT, ");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,11 +109,12 @@ mod tests {
|
|||||||
fn render_with_conditional() {
|
fn render_with_conditional() {
|
||||||
let r = renderer();
|
let r = renderer();
|
||||||
let data = json!({"is_abnormal": true, "value": "偏高"});
|
let data = json!({"is_abnormal": true, "value": "偏高"});
|
||||||
let result = r.render(
|
let result = r
|
||||||
"{{#if is_abnormal}}异常: {{value}}{{else}}正常{{/if}}",
|
.render(
|
||||||
&data,
|
"{{#if is_abnormal}}异常: {{value}}{{else}}正常{{/if}}",
|
||||||
)
|
&data,
|
||||||
.unwrap();
|
)
|
||||||
|
.unwrap();
|
||||||
assert_eq!(result, "异常: 偏高");
|
assert_eq!(result, "异常: 偏高");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,15 +129,12 @@ impl AiProvider for ClaudeProvider {
|
|||||||
if data == "[DONE]" {
|
if data == "[DONE]" {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if let Ok(event) = serde_json::from_str::<ClaudeStreamEvent>(data) {
|
if let Ok(event) = serde_json::from_str::<ClaudeStreamEvent>(data)
|
||||||
if event.event_type == "content_block_delta" {
|
&& event.event_type == "content_block_delta"
|
||||||
if let Some(delta) = event.delta {
|
&& let Some(delta) = event.delta
|
||||||
if let Some(text) = delta.text {
|
&& let Some(text) = delta.text {
|
||||||
yield Ok(text);
|
yield Ok(text);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,9 +176,7 @@ impl AiProvider for ClaudeProvider {
|
|||||||
.map_err(|e| AiError::ProviderError(e.to_string()))?;
|
.map_err(|e| AiError::ProviderError(e.to_string()))?;
|
||||||
|
|
||||||
if !status.is_success() {
|
if !status.is_success() {
|
||||||
return Err(AiError::ProviderError(format!(
|
return Err(AiError::ProviderError(format!("Claude {status}: {body}")));
|
||||||
"Claude {status}: {body}"
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let parsed: serde_json::Value = serde_json::from_str(&body)
|
let parsed: serde_json::Value = serde_json::from_str(&body)
|
||||||
|
|||||||
@@ -75,8 +75,10 @@ struct OllamaStreamChunk {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
#[allow(dead_code)]
|
||||||
struct OllamaStreamMessage {
|
struct OllamaStreamMessage {
|
||||||
content: Option<String>,
|
content: Option<String>,
|
||||||
|
#[allow(dead_code)]
|
||||||
thinking: Option<String>,
|
thinking: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +89,10 @@ fn strip_think_block(content: &str) -> String {
|
|||||||
if let Some(end) = content.find("</think") {
|
if let Some(end) = content.find("</think") {
|
||||||
// 跳过 </think 标签及其后的 > 或 \n
|
// 跳过 </think 标签及其后的 > 或 \n
|
||||||
let after_tag = &content[end + 7..]; // skip "</think"
|
let after_tag = &content[end + 7..]; // skip "</think"
|
||||||
let actual = after_tag.trim_start_matches('\n').trim_start_matches('>').trim_start();
|
let actual = after_tag
|
||||||
|
.trim_start_matches('\n')
|
||||||
|
.trim_start_matches('>')
|
||||||
|
.trim_start();
|
||||||
return actual.to_string();
|
return actual.to_string();
|
||||||
}
|
}
|
||||||
content.to_string()
|
content.to_string()
|
||||||
@@ -193,13 +198,11 @@ impl AiProvider for OllamaProvider {
|
|||||||
if chunk.done {
|
if chunk.done {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if let Some(msg) = chunk.message {
|
if let Some(msg) = chunk.message
|
||||||
if let Some(content) = msg.content {
|
&& let Some(content) = msg.content
|
||||||
if !content.is_empty() {
|
&& !content.is_empty() {
|
||||||
yield Ok(content);
|
yield Ok(content);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -252,9 +255,7 @@ impl AiProvider for OllamaProvider {
|
|||||||
.map_err(|e| AiError::ProviderError(e.to_string()))?;
|
.map_err(|e| AiError::ProviderError(e.to_string()))?;
|
||||||
|
|
||||||
if !status.is_success() {
|
if !status.is_success() {
|
||||||
return Err(AiError::ProviderError(format!(
|
return Err(AiError::ProviderError(format!("Ollama {status}: {body}")));
|
||||||
"Ollama {status}: {body}"
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let parsed: OllamaChatResponse = serde_json::from_str(&body)
|
let parsed: OllamaChatResponse = serde_json::from_str(&body)
|
||||||
@@ -307,10 +308,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ollama_provider_construction() {
|
fn ollama_provider_construction() {
|
||||||
let provider = OllamaProvider::new(
|
let provider = OllamaProvider::new("http://localhost:11434".into(), "qwen2.5:7b".into());
|
||||||
"http://localhost:11434".into(),
|
|
||||||
"qwen2.5:7b".into(),
|
|
||||||
);
|
|
||||||
assert_eq!(provider.name(), "ollama");
|
assert_eq!(provider.name(), "ollama");
|
||||||
assert_eq!(provider.default_model, "qwen2.5:7b");
|
assert_eq!(provider.default_model, "qwen2.5:7b");
|
||||||
}
|
}
|
||||||
@@ -367,10 +365,7 @@ mod tests {
|
|||||||
}"#;
|
}"#;
|
||||||
let chunk: OllamaStreamChunk = serde_json::from_str(json).unwrap();
|
let chunk: OllamaStreamChunk = serde_json::from_str(json).unwrap();
|
||||||
assert!(!chunk.done);
|
assert!(!chunk.done);
|
||||||
assert_eq!(
|
assert_eq!(chunk.message.unwrap().content, Some("Hello".to_string()));
|
||||||
chunk.message.unwrap().content,
|
|
||||||
Some("Hello".to_string())
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -388,10 +383,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn base_url_preserved() {
|
fn base_url_preserved() {
|
||||||
let provider = OllamaProvider::new(
|
let provider =
|
||||||
"http://192.168.1.100:11434".into(),
|
OllamaProvider::new("http://192.168.1.100:11434".into(), "llama3.1:8b".into());
|
||||||
"llama3.1:8b".into(),
|
|
||||||
);
|
|
||||||
assert_eq!(provider.base_url, "http://192.168.1.100:11434");
|
assert_eq!(provider.base_url, "http://192.168.1.100:11434");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -202,9 +202,7 @@ impl AiProvider for OpenAIProvider {
|
|||||||
.map_err(|e| AiError::ProviderError(e.to_string()))?;
|
.map_err(|e| AiError::ProviderError(e.to_string()))?;
|
||||||
|
|
||||||
if !status.is_success() {
|
if !status.is_success() {
|
||||||
return Err(AiError::ProviderError(format!(
|
return Err(AiError::ProviderError(format!("OpenAI {status}: {body}")));
|
||||||
"OpenAI {status}: {body}"
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let parsed: ChatResponse = serde_json::from_str(&body)
|
let parsed: ChatResponse = serde_json::from_str(&body)
|
||||||
|
|||||||
@@ -9,9 +9,17 @@ use tokio::sync::RwLock;
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub enum ProviderHealth {
|
pub enum ProviderHealth {
|
||||||
Healthy { last_check: DateTime<Utc> },
|
Healthy {
|
||||||
Degraded { last_check: DateTime<Utc>, error: String },
|
last_check: DateTime<Utc>,
|
||||||
Unavailable { since: DateTime<Utc>, error: String },
|
},
|
||||||
|
Degraded {
|
||||||
|
last_check: DateTime<Utc>,
|
||||||
|
error: String,
|
||||||
|
},
|
||||||
|
Unavailable {
|
||||||
|
since: DateTime<Utc>,
|
||||||
|
error: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProviderHealth {
|
impl ProviderHealth {
|
||||||
@@ -29,6 +37,12 @@ pub struct ProviderRegistry {
|
|||||||
entries: DashMap<String, ProviderEntry>,
|
entries: DashMap<String, ProviderEntry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for ProviderRegistry {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ProviderRegistry {
|
impl ProviderRegistry {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -40,18 +54,19 @@ impl ProviderRegistry {
|
|||||||
let health = Arc::new(RwLock::new(ProviderHealth::Healthy {
|
let health = Arc::new(RwLock::new(ProviderHealth::Healthy {
|
||||||
last_check: Utc::now(),
|
last_check: Utc::now(),
|
||||||
}));
|
}));
|
||||||
self.entries.insert(name, ProviderEntry { provider, health });
|
self.entries
|
||||||
|
.insert(name, ProviderEntry { provider, health });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn resolve(&self, preferred: &str) -> crate::error::AiResult<ResolvedProvider> {
|
pub async fn resolve(&self, preferred: &str) -> crate::error::AiResult<ResolvedProvider> {
|
||||||
// 1. 首选 Provider(实时健康检查)
|
// 1. 首选 Provider(实时健康检查)
|
||||||
if let Some(entry) = self.entries.get(preferred) {
|
if let Some(entry) = self.entries.get(preferred)
|
||||||
if entry.provider.health_check().await.unwrap_or(false) {
|
&& entry.provider.health_check().await.unwrap_or(false)
|
||||||
return Ok(ResolvedProvider {
|
{
|
||||||
provider_name: preferred.to_string(),
|
return Ok(ResolvedProvider {
|
||||||
provider: entry.provider.clone(),
|
provider_name: preferred.to_string(),
|
||||||
});
|
provider: entry.provider.clone(),
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 任何可用 Provider
|
// 2. 任何可用 Provider
|
||||||
@@ -72,7 +87,9 @@ impl ProviderRegistry {
|
|||||||
for entry in self.entries.iter() {
|
for entry in self.entries.iter() {
|
||||||
let healthy = entry.value().provider.health_check().await.unwrap_or(false);
|
let healthy = entry.value().provider.health_check().await.unwrap_or(false);
|
||||||
let new_health = if healthy {
|
let new_health = if healthy {
|
||||||
ProviderHealth::Healthy { last_check: Utc::now() }
|
ProviderHealth::Healthy {
|
||||||
|
last_check: Utc::now(),
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ProviderHealth::Unavailable {
|
ProviderHealth::Unavailable {
|
||||||
since: Utc::now(),
|
since: Utc::now(),
|
||||||
@@ -96,14 +113,22 @@ pub struct ResolvedProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ResolvedProvider {
|
impl ResolvedProvider {
|
||||||
pub fn provider_name(&self) -> &str { &self.provider_name }
|
pub fn provider_name(&self) -> &str {
|
||||||
pub fn provider(&self) -> &dyn AiProvider { self.provider.as_ref() }
|
&self.provider_name
|
||||||
pub fn into_arc(self) -> Arc<dyn AiProvider> { self.provider }
|
}
|
||||||
|
pub fn provider(&self) -> &dyn AiProvider {
|
||||||
|
self.provider.as_ref()
|
||||||
|
}
|
||||||
|
pub fn into_arc(self) -> Arc<dyn AiProvider> {
|
||||||
|
self.provider
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// === 测试桩 ===
|
// === 测试桩 ===
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
struct MockProvider {
|
struct MockProvider {
|
||||||
|
#[allow(dead_code)]
|
||||||
name: String,
|
name: String,
|
||||||
healthy: Arc<std::sync::atomic::AtomicBool>,
|
healthy: Arc<std::sync::atomic::AtomicBool>,
|
||||||
}
|
}
|
||||||
@@ -113,13 +138,18 @@ impl AiProvider for MockProvider {
|
|||||||
async fn stream_generate(
|
async fn stream_generate(
|
||||||
&self,
|
&self,
|
||||||
_req: crate::dto::GenerateRequest,
|
_req: crate::dto::GenerateRequest,
|
||||||
) -> crate::error::AiResult<std::pin::Pin<Box<dyn futures::Stream<Item = crate::error::AiResult<String>> + Send>>> {
|
) -> crate::error::AiResult<
|
||||||
|
std::pin::Pin<Box<dyn futures::Stream<Item = crate::error::AiResult<String>> + Send>>,
|
||||||
|
> {
|
||||||
// 简单返回一个空流
|
// 简单返回一个空流
|
||||||
let s = async_stream::stream! { yield Ok("mock".to_string()); };
|
let s = async_stream::stream! { yield Ok("mock".to_string()); };
|
||||||
Ok(Box::pin(s))
|
Ok(Box::pin(s))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn generate(&self, _req: crate::dto::GenerateRequest) -> crate::error::AiResult<crate::dto::GenerateResponse> {
|
async fn generate(
|
||||||
|
&self,
|
||||||
|
_req: crate::dto::GenerateRequest,
|
||||||
|
) -> crate::error::AiResult<crate::dto::GenerateResponse> {
|
||||||
Ok(crate::dto::GenerateResponse {
|
Ok(crate::dto::GenerateResponse {
|
||||||
content: "mock".to_string(),
|
content: "mock".to_string(),
|
||||||
model: "mock".to_string(),
|
model: "mock".to_string(),
|
||||||
@@ -129,7 +159,9 @@ impl AiProvider for MockProvider {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn name(&self) -> &str { &self.name }
|
fn name(&self) -> &str {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
|
||||||
async fn health_check(&self) -> crate::error::AiResult<bool> {
|
async fn health_check(&self) -> crate::error::AiResult<bool> {
|
||||||
Ok(self.healthy.load(std::sync::atomic::Ordering::Relaxed))
|
Ok(self.healthy.load(std::sync::atomic::Ordering::Relaxed))
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ use crate::error::{AiError, AiResult};
|
|||||||
/// 此服务做二次检查和安全约束注入
|
/// 此服务做二次检查和安全约束注入
|
||||||
pub struct SanitizationService;
|
pub struct SanitizationService;
|
||||||
|
|
||||||
|
impl Default for SanitizationService {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl SanitizationService {
|
impl SanitizationService {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self
|
Self
|
||||||
@@ -77,8 +83,8 @@ impl SanitizationService {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use erp_core::health_provider::{
|
use erp_core::health_provider::{
|
||||||
HealthReportDto, LabReportDto, PatientSummaryDto, ReportSectionDto,
|
HealthReportDto, LabItemDto, LabReportDto, PatientSummaryDto, ReportSectionDto,
|
||||||
VitalSignDto, LabItemDto,
|
VitalSignDto,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn sanitizer() -> SanitizationService {
|
fn sanitizer() -> SanitizationService {
|
||||||
@@ -172,13 +178,24 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn verify_no_pii_detects_all_pii_keys() {
|
fn verify_no_pii_detects_all_pii_keys() {
|
||||||
let svc = sanitizer();
|
let svc = sanitizer();
|
||||||
let pii_keys = ["name", "phone", "id_number", "address", "birth_date", "email"];
|
let pii_keys = [
|
||||||
|
"name",
|
||||||
|
"phone",
|
||||||
|
"id_number",
|
||||||
|
"address",
|
||||||
|
"birth_date",
|
||||||
|
"email",
|
||||||
|
];
|
||||||
for key in pii_keys {
|
for key in pii_keys {
|
||||||
let mut report_json = serde_json::to_value(&clean_lab_report()).unwrap();
|
let mut report_json = serde_json::to_value(&clean_lab_report()).unwrap();
|
||||||
report_json[key] = serde_json::json!("test");
|
report_json[key] = serde_json::json!("test");
|
||||||
let report: LabReportDto = serde_json::from_value(report_json).unwrap();
|
let report: LabReportDto = serde_json::from_value(report_json).unwrap();
|
||||||
let result = svc.sanitize_lab_report(&report);
|
let result = svc.sanitize_lab_report(&report);
|
||||||
assert!(result.is_ok(), "LabReportDto 不包含 {} 字段,反序列化时被丢弃", key);
|
assert!(
|
||||||
|
result.is_ok(),
|
||||||
|
"LabReportDto 不包含 {} 字段,反序列化时被丢弃",
|
||||||
|
key
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,9 +205,19 @@ mod tests {
|
|||||||
fn dto_serialization_contains_no_pii() {
|
fn dto_serialization_contains_no_pii() {
|
||||||
let report = clean_lab_report();
|
let report = clean_lab_report();
|
||||||
let val = serde_json::to_value(&report).unwrap();
|
let val = serde_json::to_value(&report).unwrap();
|
||||||
for key in &["name", "phone", "id_number", "address", "birth_date", "email"] {
|
for key in &[
|
||||||
assert!(!val.as_object().unwrap().contains_key(*key),
|
"name",
|
||||||
"LabReportDto 不应包含 PII 字段: {}", key);
|
"phone",
|
||||||
|
"id_number",
|
||||||
|
"address",
|
||||||
|
"birth_date",
|
||||||
|
"email",
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
!val.as_object().unwrap().contains_key(*key),
|
||||||
|
"LabReportDto 不应包含 PII 字段: {}",
|
||||||
|
key
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
use erp_core::types::Pagination;
|
||||||
use futures::Stream;
|
use futures::Stream;
|
||||||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, Set};
|
use sea_orm::{
|
||||||
|
ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder,
|
||||||
|
QuerySelect, Set,
|
||||||
|
};
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use erp_core::types::Pagination;
|
|
||||||
|
|
||||||
use crate::dto::{AnalysisType, GenerateRequest};
|
use crate::dto::{AnalysisType, GenerateRequest};
|
||||||
use crate::entity::ai_analysis;
|
use crate::entity::ai_analysis;
|
||||||
@@ -38,6 +41,7 @@ impl AnalysisService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 执行流式分析 — 返回 SSE 事件流
|
/// 执行流式分析 — 返回 SSE 事件流
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub async fn stream_analyze(
|
pub async fn stream_analyze(
|
||||||
&self,
|
&self,
|
||||||
tenant_id: Uuid,
|
tenant_id: Uuid,
|
||||||
@@ -64,7 +68,10 @@ impl AnalysisService {
|
|||||||
if let Some(cached) = self.find_cached(tenant_id, &input_hash, 1).await? {
|
if let Some(cached) = self.find_cached(tenant_id, &input_hash, 1).await? {
|
||||||
tracing::info!(analysis = %cached.id, "AI 分析缓存命中,复用已有结果");
|
tracing::info!(analysis = %cached.id, "AI 分析缓存命中,复用已有结果");
|
||||||
let content = cached.result_content.clone().unwrap_or_default();
|
let content = cached.result_content.clone().unwrap_or_default();
|
||||||
let metadata = cached.result_metadata.clone().unwrap_or(serde_json::json!({}));
|
let metadata = cached
|
||||||
|
.result_metadata
|
||||||
|
.clone()
|
||||||
|
.unwrap_or(serde_json::json!({}));
|
||||||
let stream = self.replay_cached(content, metadata);
|
let stream = self.replay_cached(content, metadata);
|
||||||
return Ok((stream, cached.id, provider_name));
|
return Ok((stream, cached.id, provider_name));
|
||||||
}
|
}
|
||||||
@@ -86,7 +93,10 @@ impl AnalysisService {
|
|||||||
confidence = ctx.confidence,
|
confidence = ctx.confidence,
|
||||||
"知识库上下文注入"
|
"知识库上下文注入"
|
||||||
);
|
);
|
||||||
format!("{}\n\n=== 知识库参考 ===\n{}", system_prompt, ctx.context_text)
|
format!(
|
||||||
|
"{}\n\n=== 知识库参考 ===\n{}",
|
||||||
|
system_prompt, ctx.context_text
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Ok(_) => system_prompt,
|
Ok(_) => system_prompt,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -234,11 +244,7 @@ impl AnalysisService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 获取单条分析记录
|
/// 获取单条分析记录
|
||||||
pub async fn get_analysis(
|
pub async fn get_analysis(&self, id: Uuid, tenant_id: Uuid) -> AiResult<ai_analysis::Model> {
|
||||||
&self,
|
|
||||||
id: Uuid,
|
|
||||||
tenant_id: Uuid,
|
|
||||||
) -> AiResult<ai_analysis::Model> {
|
|
||||||
let model = ai_analysis::Entity::find_by_id(id)
|
let model = ai_analysis::Entity::find_by_id(id)
|
||||||
.one(&self.db)
|
.one(&self.db)
|
||||||
.await?
|
.await?
|
||||||
@@ -249,6 +255,7 @@ impl AnalysisService {
|
|||||||
Ok(model)
|
Ok(model)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
async fn create_analysis_record(
|
async fn create_analysis_record(
|
||||||
&self,
|
&self,
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, FromQueryResult, QueryFilter, QueryOrder, Set, Statement};
|
use sea_orm::{ActiveModelTrait, EntityTrait, FromQueryResult, Set, Statement};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::entity::ai_analysis_queue;
|
use crate::entity::ai_analysis_queue;
|
||||||
use crate::error::{AiError, AiResult};
|
use crate::error::{AiError, AiResult};
|
||||||
|
|
||||||
#[derive(Debug, FromQueryResult)]
|
#[derive(Debug, FromQueryResult)]
|
||||||
|
#[allow(dead_code)]
|
||||||
struct QueueRow {
|
struct QueueRow {
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
tenant_id: Uuid,
|
tenant_id: Uuid,
|
||||||
@@ -88,7 +89,10 @@ impl AnalysisQueue {
|
|||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn claim_next(&self, tenant_id: Option<Uuid>) -> AiResult<Option<ai_analysis_queue::Model>> {
|
pub async fn claim_next(
|
||||||
|
&self,
|
||||||
|
tenant_id: Option<Uuid>,
|
||||||
|
) -> AiResult<Option<ai_analysis_queue::Model>> {
|
||||||
let sql = match tenant_id {
|
let sql = match tenant_id {
|
||||||
Some(tid) => format!(
|
Some(tid) => format!(
|
||||||
"SELECT * FROM ai_analysis_queue WHERE tenant_id = '{}' AND status = 'pending' AND deleted_at IS NULL AND scheduled_at <= NOW() ORDER BY priority DESC, scheduled_at ASC LIMIT 1",
|
"SELECT * FROM ai_analysis_queue WHERE tenant_id = '{}' AND status = 'pending' AND deleted_at IS NULL AND scheduled_at <= NOW() ORDER BY priority DESC, scheduled_at ASC LIMIT 1",
|
||||||
@@ -101,19 +105,22 @@ impl AnalysisQueue {
|
|||||||
AND scheduled_at <= NOW()
|
AND scheduled_at <= NOW()
|
||||||
ORDER BY priority DESC, scheduled_at ASC
|
ORDER BY priority DESC, scheduled_at ASC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
"#.to_string(),
|
"#
|
||||||
|
.to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let row: Option<QueueRow> = QueueRow::find_by_statement(
|
let row: Option<QueueRow> = QueueRow::find_by_statement(Statement::from_string(
|
||||||
Statement::from_string(sea_orm::DatabaseBackend::Postgres, sql.to_string()),
|
sea_orm::DatabaseBackend::Postgres,
|
||||||
)
|
sql.to_string(),
|
||||||
|
))
|
||||||
.one(&self.db)
|
.one(&self.db)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
match row {
|
match row {
|
||||||
Some(r) => {
|
Some(r) => {
|
||||||
let now = chrono::Utc::now();
|
let now = chrono::Utc::now();
|
||||||
let mut active: ai_analysis_queue::ActiveModel = self.find_by_id(r.id).await?.into();
|
let mut active: ai_analysis_queue::ActiveModel =
|
||||||
|
self.find_by_id(r.id).await?.into();
|
||||||
active.status = Set("running".to_string());
|
active.status = Set("running".to_string());
|
||||||
active.started_at = Set(Some(now));
|
active.started_at = Set(Some(now));
|
||||||
active.updated_at = Set(now);
|
active.updated_at = Set(now);
|
||||||
@@ -125,11 +132,7 @@ impl AnalysisQueue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn mark_completed(
|
pub async fn mark_completed(&self, id: Uuid, result_analysis_id: Uuid) -> AiResult<()> {
|
||||||
&self,
|
|
||||||
id: Uuid,
|
|
||||||
result_analysis_id: Uuid,
|
|
||||||
) -> AiResult<()> {
|
|
||||||
let job = self.find_by_id(id).await?;
|
let job = self.find_by_id(id).await?;
|
||||||
let now = chrono::Utc::now();
|
let now = chrono::Utc::now();
|
||||||
let mut active: ai_analysis_queue::ActiveModel = job.into();
|
let mut active: ai_analysis_queue::ActiveModel = job.into();
|
||||||
@@ -179,15 +182,14 @@ impl AnalysisQueue {
|
|||||||
GROUP BY status
|
GROUP BY status
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
let rows: Vec<StatusCount> = StatusCount::find_by_statement(
|
let rows: Vec<StatusCount> =
|
||||||
Statement::from_sql_and_values(
|
StatusCount::find_by_statement(Statement::from_sql_and_values(
|
||||||
sea_orm::DatabaseBackend::Postgres,
|
sea_orm::DatabaseBackend::Postgres,
|
||||||
sql,
|
sql,
|
||||||
[tenant_id.into()],
|
[tenant_id.into()],
|
||||||
),
|
))
|
||||||
)
|
.all(&self.db)
|
||||||
.all(&self.db)
|
.await?;
|
||||||
.await?;
|
|
||||||
|
|
||||||
let mut pending = 0i64;
|
let mut pending = 0i64;
|
||||||
let mut running = 0i64;
|
let mut running = 0i64;
|
||||||
|
|||||||
@@ -50,19 +50,13 @@ async fn run_auto_analysis(state: &AiState) -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(total_analyzed, total_errors, "自动趋势分析任务完成");
|
||||||
total_analyzed,
|
|
||||||
total_errors,
|
|
||||||
"自动趋势分析任务完成"
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 查找所有活跃租户 ID
|
/// 查找所有活跃租户 ID
|
||||||
async fn find_active_tenants(
|
async fn find_active_tenants(db: &sea_orm::DatabaseConnection) -> Result<Vec<Uuid>, String> {
|
||||||
db: &sea_orm::DatabaseConnection,
|
|
||||||
) -> Result<Vec<Uuid>, String> {
|
|
||||||
#[derive(Debug, FromQueryResult)]
|
#[derive(Debug, FromQueryResult)]
|
||||||
struct TenantId {
|
struct TenantId {
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
|
|||||||
@@ -16,7 +16,12 @@ pub struct CacheKey {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl CacheKey {
|
impl CacheKey {
|
||||||
pub fn new(tenant_id: Uuid, analysis_type: &str, input: &serde_json::Value, prompt_version: i32) -> Self {
|
pub fn new(
|
||||||
|
tenant_id: Uuid,
|
||||||
|
analysis_type: &str,
|
||||||
|
input: &serde_json::Value,
|
||||||
|
prompt_version: i32,
|
||||||
|
) -> Self {
|
||||||
let canonical = serde_json::to_string(input).unwrap_or_default();
|
let canonical = serde_json::to_string(input).unwrap_or_default();
|
||||||
let mut hasher = Sha256::new();
|
let mut hasher = Sha256::new();
|
||||||
hasher.update(canonical.as_bytes());
|
hasher.update(canonical.as_bytes());
|
||||||
@@ -54,8 +59,16 @@ pub struct CacheService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl CacheService {
|
impl CacheService {
|
||||||
pub fn new(redis: redis::Client, db: sea_orm::DatabaseConnection, default_ttl: Duration) -> Self {
|
pub fn new(
|
||||||
Self { redis, db, default_ttl }
|
redis: redis::Client,
|
||||||
|
db: sea_orm::DatabaseConnection,
|
||||||
|
default_ttl: Duration,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
redis,
|
||||||
|
db,
|
||||||
|
default_ttl,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get(&self, key: &CacheKey) -> AiResult<Option<CachedAnalysis>> {
|
pub async fn get(&self, key: &CacheKey) -> AiResult<Option<CachedAnalysis>> {
|
||||||
@@ -127,12 +140,13 @@ impl CacheService {
|
|||||||
let data: Option<String> = conn.get(key).await?;
|
let data: Option<String> = conn.get(key).await?;
|
||||||
match data {
|
match data {
|
||||||
Some(json) => {
|
Some(json) => {
|
||||||
let cached: CachedAnalysis = serde_json::from_str(&json)
|
let cached: CachedAnalysis = serde_json::from_str(&json).map_err(|e| {
|
||||||
.map_err(|e| redis::RedisError::from((
|
redis::RedisError::from((
|
||||||
redis::ErrorKind::TypeError,
|
redis::ErrorKind::TypeError,
|
||||||
"反序列化失败",
|
"反序列化失败",
|
||||||
e.to_string(),
|
e.to_string(),
|
||||||
)))?;
|
))
|
||||||
|
})?;
|
||||||
Ok(Some(cached))
|
Ok(Some(cached))
|
||||||
}
|
}
|
||||||
None => Ok(None),
|
None => Ok(None),
|
||||||
@@ -141,12 +155,10 @@ impl CacheService {
|
|||||||
|
|
||||||
async fn try_redis_set(&self, key: &str, value: &CachedAnalysis) -> redis::RedisResult<()> {
|
async fn try_redis_set(&self, key: &str, value: &CachedAnalysis) -> redis::RedisResult<()> {
|
||||||
let mut conn = self.redis.get_multiplexed_async_connection().await?;
|
let mut conn = self.redis.get_multiplexed_async_connection().await?;
|
||||||
let json = serde_json::to_string(value).map_err(|e| redis::RedisError::from((
|
let json = serde_json::to_string(value).map_err(|e| {
|
||||||
redis::ErrorKind::TypeError,
|
redis::RedisError::from((redis::ErrorKind::TypeError, "序列化失败", e.to_string()))
|
||||||
"序列化失败",
|
})?;
|
||||||
e.to_string(),
|
let (): () = conn.set_ex(key, json, self.default_ttl.as_secs()).await?;
|
||||||
)))?;
|
|
||||||
let (): () = conn.set_ex(key, json, self.default_ttl.as_secs() as u64).await?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,15 +167,14 @@ impl CacheService {
|
|||||||
let mut count = 0u64;
|
let mut count = 0u64;
|
||||||
let mut cursor: u64 = 0;
|
let mut cursor: u64 = 0;
|
||||||
loop {
|
loop {
|
||||||
let (new_cursor, keys): (u64, Vec<String>) =
|
let (new_cursor, keys): (u64, Vec<String>) = redis::cmd("SCAN")
|
||||||
redis::cmd("SCAN")
|
.arg(cursor)
|
||||||
.arg(cursor)
|
.arg("MATCH")
|
||||||
.arg("MATCH")
|
.arg(pattern)
|
||||||
.arg(pattern)
|
.arg("COUNT")
|
||||||
.arg("COUNT")
|
.arg(100)
|
||||||
.arg(100)
|
.query_async(&mut conn)
|
||||||
.query_async(&mut conn)
|
.await?;
|
||||||
.await?;
|
|
||||||
if !keys.is_empty() {
|
if !keys.is_empty() {
|
||||||
let del_count: u64 = conn.del(&keys).await?;
|
let del_count: u64 = conn.del(&keys).await?;
|
||||||
count += del_count;
|
count += del_count;
|
||||||
|
|||||||
@@ -38,32 +38,35 @@ pub fn generate_comparison(
|
|||||||
// 提取可比较的数值指标
|
// 提取可比较的数值指标
|
||||||
if let (Some(b_obj), Some(c_obj)) = (baseline.as_object(), current.as_object()) {
|
if let (Some(b_obj), Some(c_obj)) = (baseline.as_object(), current.as_object()) {
|
||||||
for key in b_obj.keys() {
|
for key in b_obj.keys() {
|
||||||
if let (Some(b_val), Some(c_val)) = (b_obj.get(key), c_obj.get(key)) {
|
if let (Some(b_val), Some(c_val)) = (b_obj.get(key), c_obj.get(key))
|
||||||
if let (Some(b_num), Some(c_num)) = (b_val.as_f64(), c_val.as_f64()) {
|
&& let (Some(b_num), Some(c_num)) = (b_val.as_f64(), c_val.as_f64())
|
||||||
let change_pct = if b_num.abs() > 0.0001 {
|
{
|
||||||
((c_num - b_num) / b_num.abs()) * 100.0
|
let change_pct = if b_num.abs() > 0.0001 {
|
||||||
} else {
|
((c_num - b_num) / b_num.abs()) * 100.0
|
||||||
0.0
|
} else {
|
||||||
};
|
0.0
|
||||||
let trend = if change_pct.abs() > 5.0 {
|
};
|
||||||
TrendDirection::Worsening
|
let trend = if change_pct.abs() > 5.0 {
|
||||||
} else {
|
TrendDirection::Worsening
|
||||||
TrendDirection::Stable
|
} else {
|
||||||
};
|
TrendDirection::Stable
|
||||||
changes.push(MetricChange {
|
};
|
||||||
metric: key.clone(),
|
changes.push(MetricChange {
|
||||||
baseline_value: b_num,
|
metric: key.clone(),
|
||||||
current_value: c_num,
|
baseline_value: b_num,
|
||||||
change_percent: change_pct,
|
current_value: c_num,
|
||||||
trend,
|
change_percent: change_pct,
|
||||||
});
|
trend,
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 综合趋势判断
|
// 综合趋势判断
|
||||||
let changed = changes.iter().filter(|c| c.trend == TrendDirection::Worsening).count();
|
let changed = changes
|
||||||
|
.iter()
|
||||||
|
.filter(|c| c.trend == TrendDirection::Worsening)
|
||||||
|
.count();
|
||||||
let overall = if changed > 0 {
|
let overall = if changed > 0 {
|
||||||
TrendDirection::Worsening
|
TrendDirection::Worsening
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -74,9 +74,8 @@ impl CostService {
|
|||||||
pub fn estimate_cost(analysis_type: &str, model: &str) -> CostEstimate {
|
pub fn estimate_cost(analysis_type: &str, model: &str) -> CostEstimate {
|
||||||
let (input_tokens, output_tokens) = default_token_estimate(analysis_type);
|
let (input_tokens, output_tokens) = default_token_estimate(analysis_type);
|
||||||
let (input_cost, output_cost) = model_cost_per_million(model);
|
let (input_cost, output_cost) = model_cost_per_million(model);
|
||||||
let estimated_cost_usd =
|
let estimated_cost_usd = (input_tokens as f64 * input_cost / 1_000_000.0)
|
||||||
(input_tokens as f64 * input_cost / 1_000_000.0)
|
+ (output_tokens as f64 * output_cost / 1_000_000.0);
|
||||||
+ (output_tokens as f64 * output_cost / 1_000_000.0);
|
|
||||||
|
|
||||||
CostEstimate {
|
CostEstimate {
|
||||||
analysis_type: analysis_type.to_string(),
|
analysis_type: analysis_type.to_string(),
|
||||||
@@ -143,13 +142,11 @@ impl CostService {
|
|||||||
AND created_at >= DATE_TRUNC('month', CURRENT_DATE)
|
AND created_at >= DATE_TRUNC('month', CURRENT_DATE)
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
let row: Option<TokenSum> = TokenSum::find_by_statement(
|
let row: Option<TokenSum> = TokenSum::find_by_statement(Statement::from_sql_and_values(
|
||||||
Statement::from_sql_and_values(
|
sea_orm::DatabaseBackend::Postgres,
|
||||||
sea_orm::DatabaseBackend::Postgres,
|
sql,
|
||||||
sql,
|
[tenant_id.into()],
|
||||||
[tenant_id.into()],
|
))
|
||||||
),
|
|
||||||
)
|
|
||||||
.one(&self.db)
|
.one(&self.db)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
@@ -179,7 +176,10 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn budget_warning_levels() {
|
fn budget_warning_levels() {
|
||||||
assert_eq!(BudgetWarningLevel::Normal, BudgetWarningLevel::Normal);
|
assert_eq!(BudgetWarningLevel::Normal, BudgetWarningLevel::Normal);
|
||||||
assert!(matches!(BudgetWarningLevel::Exceeded, BudgetWarningLevel::Exceeded));
|
assert!(matches!(
|
||||||
|
BudgetWarningLevel::Exceeded,
|
||||||
|
BudgetWarningLevel::Exceeded
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::dto::suggestion::{RiskLevel, SuggestionType, StructuredSuggestion};
|
use crate::dto::suggestion::{RiskLevel, StructuredSuggestion, SuggestionType};
|
||||||
use crate::service::local_rules::{CompareOp, LocalRule, LocalRulesEngine};
|
use crate::service::local_rules::{CompareOp, LocalRule, LocalRulesEngine};
|
||||||
|
|
||||||
/// 透析患者实验室指标输入
|
/// 透析患者实验室指标输入
|
||||||
@@ -73,6 +73,12 @@ pub struct DialysisRiskScorer {
|
|||||||
engine: LocalRulesEngine,
|
engine: LocalRulesEngine,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for DialysisRiskScorer {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl DialysisRiskScorer {
|
impl DialysisRiskScorer {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let rules = vec![
|
let rules = vec![
|
||||||
@@ -113,8 +119,7 @@ impl DialysisRiskScorer {
|
|||||||
threshold: 7.0,
|
threshold: 7.0,
|
||||||
risk_level: RiskLevel::High,
|
risk_level: RiskLevel::High,
|
||||||
suggestion_type: SuggestionType::Alert,
|
suggestion_type: SuggestionType::Alert,
|
||||||
message_template: "血磷={value}mg/dL严重偏高(>7.0),需紧急评估钙磷代谢"
|
message_template: "血磷={value}mg/dL严重偏高(>7.0),需紧急评估钙磷代谢".into(),
|
||||||
.into(),
|
|
||||||
},
|
},
|
||||||
// 透前血钾 > 6.0 mEq/L:危急高钾
|
// 透前血钾 > 6.0 mEq/L:危急高钾
|
||||||
LocalRule {
|
LocalRule {
|
||||||
@@ -161,8 +166,7 @@ impl DialysisRiskScorer {
|
|||||||
threshold: 5.0,
|
threshold: 5.0,
|
||||||
risk_level: RiskLevel::High,
|
risk_level: RiskLevel::High,
|
||||||
suggestion_type: SuggestionType::Alert,
|
suggestion_type: SuggestionType::Alert,
|
||||||
message_template: "透析间期体重增长{value}%(>5%干体重),容量超负荷风险高"
|
message_template: "透析间期体重增长{value}%(>5%干体重),容量超负荷风险高".into(),
|
||||||
.into(),
|
|
||||||
},
|
},
|
||||||
// 体重增长 > 3.5%:需关注
|
// 体重增长 > 3.5%:需关注
|
||||||
LocalRule {
|
LocalRule {
|
||||||
@@ -199,8 +203,7 @@ impl DialysisRiskScorer {
|
|||||||
threshold: 3.0,
|
threshold: 3.0,
|
||||||
risk_level: RiskLevel::High,
|
risk_level: RiskLevel::High,
|
||||||
suggestion_type: SuggestionType::Alert,
|
suggestion_type: SuggestionType::Alert,
|
||||||
message_template: "白蛋白={value}g/dL严重偏低(<3.0),营养不良增加死亡风险"
|
message_template: "白蛋白={value}g/dL严重偏低(<3.0),营养不良增加死亡风险".into(),
|
||||||
.into(),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
Self {
|
Self {
|
||||||
@@ -221,17 +224,14 @@ impl DialysisRiskScorer {
|
|||||||
|
|
||||||
let suggestions = self.engine.evaluate(&metrics);
|
let suggestions = self.engine.evaluate(&metrics);
|
||||||
|
|
||||||
let mut risk_factors: Vec<String> = suggestions
|
let mut risk_factors: Vec<String> = suggestions.iter().map(|s| s.reason.clone()).collect();
|
||||||
.iter()
|
|
||||||
.map(|s| s.reason.clone())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let kdigo_stage = input.egfr.map(KdigoStage::from_egfr);
|
let kdigo_stage = input.egfr.map(KdigoStage::from_egfr);
|
||||||
|
|
||||||
if let Some(stage) = kdigo_stage {
|
if let Some(stage) = kdigo_stage
|
||||||
if matches!(stage, KdigoStage::G4 | KdigoStage::G5) {
|
&& matches!(stage, KdigoStage::G4 | KdigoStage::G5)
|
||||||
risk_factors.push(format!("KDIGO分期{},肾功能严重受损", stage.label()));
|
{
|
||||||
}
|
risk_factors.push(format!("KDIGO分期{},肾功能严重受损", stage.label()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let overall_risk = if suggestions.iter().any(|s| s.priority == 1) {
|
let overall_risk = if suggestions.iter().any(|s| s.priority == 1) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::dto::suggestion::{RiskLevel, SuggestionType, StructuredSuggestion};
|
use crate::dto::suggestion::{RiskLevel, StructuredSuggestion, SuggestionType};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct LocalRule {
|
pub struct LocalRule {
|
||||||
@@ -120,9 +120,7 @@ impl LocalRulesEngine {
|
|||||||
RiskLevel::Medium => "2周内".into(),
|
RiskLevel::Medium => "2周内".into(),
|
||||||
RiskLevel::Low => "1个月内".into(),
|
RiskLevel::Low => "1个月内".into(),
|
||||||
},
|
},
|
||||||
reason: rule
|
reason: rule.message_template.replace("{value}", &value.to_string()),
|
||||||
.message_template
|
|
||||||
.replace("{value}", &value.to_string()),
|
|
||||||
params: serde_json::json!({
|
params: serde_json::json!({
|
||||||
"metric": rule.metric,
|
"metric": rule.metric,
|
||||||
"value": value,
|
"value": value,
|
||||||
@@ -156,7 +154,8 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn evaluate_all_normal_no_suggestions() {
|
fn evaluate_all_normal_no_suggestions() {
|
||||||
let rules = LocalRulesEngine::default_rules();
|
let rules = LocalRulesEngine::default_rules();
|
||||||
let metrics = serde_json::json!({"systolic_bp": 120.0, "heart_rate": 72.0, "blood_sugar": 5.5});
|
let metrics =
|
||||||
|
serde_json::json!({"systolic_bp": 120.0, "heart_rate": 72.0, "blood_sugar": 5.5});
|
||||||
let suggestions = rules.evaluate(&metrics);
|
let suggestions = rules.evaluate(&metrics);
|
||||||
assert!(suggestions.is_empty());
|
assert!(suggestions.is_empty());
|
||||||
}
|
}
|
||||||
@@ -166,9 +165,11 @@ mod tests {
|
|||||||
let rules = LocalRulesEngine::default_rules();
|
let rules = LocalRulesEngine::default_rules();
|
||||||
let metrics = serde_json::json!({"heart_rate": 110.0});
|
let metrics = serde_json::json!({"heart_rate": 110.0});
|
||||||
let suggestions = rules.evaluate(&metrics);
|
let suggestions = rules.evaluate(&metrics);
|
||||||
assert!(suggestions
|
assert!(
|
||||||
.iter()
|
suggestions
|
||||||
.any(|s| s.suggestion_type == SuggestionType::Followup));
|
.iter()
|
||||||
|
.any(|s| s.suggestion_type == SuggestionType::Followup)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -11,11 +11,10 @@ pub fn parse_dual_channel(raw: &str) -> AiResult<ParsedOutput> {
|
|||||||
.trim()
|
.trim()
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
let structured = extract_section(raw, JSON_MARKER, TEXT_MARKER)
|
let structured = extract_section(raw, JSON_MARKER, TEXT_MARKER).and_then(|json_str| {
|
||||||
.and_then(|json_str| {
|
let parsed: Result<StructuredOutput, _> = serde_json::from_str(json_str.trim());
|
||||||
let parsed: Result<StructuredOutput, _> = serde_json::from_str(json_str.trim());
|
parsed.ok()
|
||||||
parsed.ok()
|
});
|
||||||
});
|
|
||||||
|
|
||||||
Ok(ParsedOutput {
|
Ok(ParsedOutput {
|
||||||
text_content,
|
text_content,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ pub struct PostProcessResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 对完成的分析执行后处理:解析双通道输出、创建建议、发布事件
|
/// 对完成的分析执行后处理:解析双通道输出、创建建议、发布事件
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub async fn post_process_analysis(
|
pub async fn post_process_analysis(
|
||||||
state: &AiState,
|
state: &AiState,
|
||||||
analysis_id: Uuid,
|
analysis_id: Uuid,
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ impl PromptService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 新建 Prompt
|
/// 新建 Prompt
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub async fn create_prompt(
|
pub async fn create_prompt(
|
||||||
&self,
|
&self,
|
||||||
tenant_id: Uuid,
|
tenant_id: Uuid,
|
||||||
@@ -95,6 +96,7 @@ impl PromptService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 更新 Prompt(创建新版本)
|
/// 更新 Prompt(创建新版本)
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub async fn update_prompt(
|
pub async fn update_prompt(
|
||||||
&self,
|
&self,
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
@@ -122,7 +124,9 @@ impl PromptService {
|
|||||||
name: Set(entity.name.clone()),
|
name: Set(entity.name.clone()),
|
||||||
description: Set(description.unwrap_or(entity.description.clone())),
|
description: Set(description.unwrap_or(entity.description.clone())),
|
||||||
system_prompt: Set(system_prompt.unwrap_or(entity.system_prompt.clone())),
|
system_prompt: Set(system_prompt.unwrap_or(entity.system_prompt.clone())),
|
||||||
user_prompt_template: Set(user_prompt_template.unwrap_or(entity.user_prompt_template.clone())),
|
user_prompt_template: Set(
|
||||||
|
user_prompt_template.unwrap_or(entity.user_prompt_template.clone())
|
||||||
|
),
|
||||||
variables_schema: Set(entity.variables_schema.clone()),
|
variables_schema: Set(entity.variables_schema.clone()),
|
||||||
model_config: Set(model_config.unwrap_or(entity.model_config.clone())),
|
model_config: Set(model_config.unwrap_or(entity.model_config.clone())),
|
||||||
version: Set(entity.version + 1),
|
version: Set(entity.version + 1),
|
||||||
@@ -140,11 +144,7 @@ impl PromptService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 激活指定 Prompt(停用同 name+category 的其他版本)
|
/// 激活指定 Prompt(停用同 name+category 的其他版本)
|
||||||
pub async fn activate_prompt(
|
pub async fn activate_prompt(&self, id: Uuid, tenant_id: Uuid) -> AiResult<ai_prompt::Model> {
|
||||||
&self,
|
|
||||||
id: Uuid,
|
|
||||||
tenant_id: Uuid,
|
|
||||||
) -> AiResult<ai_prompt::Model> {
|
|
||||||
let entity = ai_prompt::Entity::find_by_id(id)
|
let entity = ai_prompt::Entity::find_by_id(id)
|
||||||
.one(&self.db)
|
.one(&self.db)
|
||||||
.await?
|
.await?
|
||||||
@@ -179,11 +179,7 @@ impl PromptService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 回滚(= 激活指定旧版本)
|
/// 回滚(= 激活指定旧版本)
|
||||||
pub async fn rollback_prompt(
|
pub async fn rollback_prompt(&self, id: Uuid, tenant_id: Uuid) -> AiResult<ai_prompt::Model> {
|
||||||
&self,
|
|
||||||
id: Uuid,
|
|
||||||
tenant_id: Uuid,
|
|
||||||
) -> AiResult<ai_prompt::Model> {
|
|
||||||
self.activate_prompt(id, tenant_id).await
|
self.activate_prompt(id, tenant_id).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,11 +27,7 @@ impl QuotaService {
|
|||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn check_quota(
|
pub async fn check_quota(&self, tenant_id: Uuid, patient_id: Option<Uuid>) -> AiResult<()> {
|
||||||
&self,
|
|
||||||
tenant_id: Uuid,
|
|
||||||
patient_id: Option<Uuid>,
|
|
||||||
) -> AiResult<()> {
|
|
||||||
if !self.enabled {
|
if !self.enabled {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -81,24 +77,18 @@ impl QuotaService {
|
|||||||
AND created_at >= date_trunc('month', CURRENT_DATE)
|
AND created_at >= date_trunc('month', CURRENT_DATE)
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
let result: Option<TokenSum> = TokenSum::find_by_statement(
|
let result: Option<TokenSum> = TokenSum::find_by_statement(Statement::from_sql_and_values(
|
||||||
Statement::from_sql_and_values(
|
sea_orm::DatabaseBackend::Postgres,
|
||||||
sea_orm::DatabaseBackend::Postgres,
|
sql,
|
||||||
sql,
|
[tenant_id.into()],
|
||||||
[tenant_id.into()],
|
))
|
||||||
),
|
|
||||||
)
|
|
||||||
.one(&self.db)
|
.one(&self.db)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(result.map(|r| r.total_tokens).unwrap_or(0))
|
Ok(result.map(|r| r.total_tokens).unwrap_or(0))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_daily_patient_count(
|
async fn get_daily_patient_count(&self, tenant_id: Uuid, patient_id: Uuid) -> AiResult<i64> {
|
||||||
&self,
|
|
||||||
tenant_id: Uuid,
|
|
||||||
patient_id: Uuid,
|
|
||||||
) -> AiResult<i64> {
|
|
||||||
#[derive(Debug, FromQueryResult)]
|
#[derive(Debug, FromQueryResult)]
|
||||||
struct CountResult {
|
struct CountResult {
|
||||||
count: i64,
|
count: i64,
|
||||||
@@ -113,23 +103,19 @@ impl QuotaService {
|
|||||||
AND created_at >= CURRENT_DATE
|
AND created_at >= CURRENT_DATE
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
let result: Option<CountResult> = CountResult::find_by_statement(
|
let result: Option<CountResult> =
|
||||||
Statement::from_sql_and_values(
|
CountResult::find_by_statement(Statement::from_sql_and_values(
|
||||||
sea_orm::DatabaseBackend::Postgres,
|
sea_orm::DatabaseBackend::Postgres,
|
||||||
sql,
|
sql,
|
||||||
[tenant_id.into(), patient_id.into()],
|
[tenant_id.into(), patient_id.into()],
|
||||||
),
|
))
|
||||||
)
|
.one(&self.db)
|
||||||
.one(&self.db)
|
.await?;
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(result.map(|r| r.count).unwrap_or(0))
|
Ok(result.map(|r| r.count).unwrap_or(0))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_usage_summary(
|
pub async fn get_usage_summary(&self, tenant_id: Uuid) -> AiResult<QuotaSummary> {
|
||||||
&self,
|
|
||||||
tenant_id: Uuid,
|
|
||||||
) -> AiResult<QuotaSummary> {
|
|
||||||
let config = self.get_tenant_config(tenant_id).await?;
|
let config = self.get_tenant_config(tenant_id).await?;
|
||||||
let budget = config
|
let budget = config
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -142,10 +128,7 @@ impl QuotaService {
|
|||||||
tenant_id,
|
tenant_id,
|
||||||
monthly_budget: budget,
|
monthly_budget: budget,
|
||||||
monthly_used: used,
|
monthly_used: used,
|
||||||
daily_patient_limit: config
|
daily_patient_limit: config.as_ref().map(|c| c.daily_patient_limit).unwrap_or(50),
|
||||||
.as_ref()
|
|
||||||
.map(|c| c.daily_patient_limit)
|
|
||||||
.unwrap_or(50),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,15 +21,14 @@ pub async fn handle_reanalysis_requested(
|
|||||||
FROM ai_suggestion
|
FROM ai_suggestion
|
||||||
WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL
|
WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL
|
||||||
"#;
|
"#;
|
||||||
let original: Option<OriginalSuggestion> = OriginalSuggestion::find_by_statement(
|
let original: Option<OriginalSuggestion> =
|
||||||
Statement::from_sql_and_values(
|
OriginalSuggestion::find_by_statement(Statement::from_sql_and_values(
|
||||||
sea_orm::DatabaseBackend::Postgres,
|
sea_orm::DatabaseBackend::Postgres,
|
||||||
sql,
|
sql,
|
||||||
[original_suggestion_id.into(), tenant_id.into()],
|
[original_suggestion_id.into(), tenant_id.into()],
|
||||||
),
|
))
|
||||||
)
|
.one(db)
|
||||||
.one(db)
|
.await?;
|
||||||
.await?;
|
|
||||||
|
|
||||||
match original {
|
match original {
|
||||||
Some(orig) => {
|
Some(orig) => {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use uuid::Uuid;
|
|
||||||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
|
|
||||||
use erp_core::error::AppResult;
|
|
||||||
use crate::dto::suggestion::*;
|
use crate::dto::suggestion::*;
|
||||||
use crate::entity::ai_suggestion;
|
use crate::entity::ai_suggestion;
|
||||||
|
use erp_core::error::AppResult;
|
||||||
|
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub struct SuggestionService;
|
pub struct SuggestionService;
|
||||||
|
|
||||||
@@ -85,9 +85,7 @@ impl SuggestionService {
|
|||||||
.filter(ai_suggestion::Column::DeletedAt.is_null())
|
.filter(ai_suggestion::Column::DeletedAt.is_null())
|
||||||
.one(db)
|
.one(db)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| crate::error::AiError::AnalysisNotFound("建议不存在".into()))?;
|
||||||
crate::error::AiError::AnalysisNotFound("建议不存在".into())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let current_status = parse_status(&item.status);
|
let current_status = parse_status(&item.status);
|
||||||
if !current_status.can_transition_to(new_status) {
|
if !current_status.can_transition_to(new_status) {
|
||||||
@@ -122,13 +120,14 @@ impl SuggestionService {
|
|||||||
.filter(ai_suggestion::Column::DeletedAt.is_null())
|
.filter(ai_suggestion::Column::DeletedAt.is_null())
|
||||||
.one(db)
|
.one(db)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| crate::error::AiError::AnalysisNotFound("建议不存在".into()))?;
|
||||||
crate::error::AiError::AnalysisNotFound("建议不存在".into())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let current_status = parse_status(&item.status);
|
let current_status = parse_status(&item.status);
|
||||||
// 允许从 Pending 或 Approved 直接执行(护士可能跳过审批)
|
// 允许从 Pending 或 Approved 直接执行(护士可能跳过审批)
|
||||||
if !matches!(current_status, SuggestionStatus::Pending | SuggestionStatus::Approved) {
|
if !matches!(
|
||||||
|
current_status,
|
||||||
|
SuggestionStatus::Pending | SuggestionStatus::Approved
|
||||||
|
) {
|
||||||
return Err(crate::error::AiError::Validation(format!(
|
return Err(crate::error::AiError::Validation(format!(
|
||||||
"建议状态为 {},无法执行(需要 pending 或 approved)",
|
"建议状态为 {},无法执行(需要 pending 或 approved)",
|
||||||
current_status.as_str()
|
current_status.as_str()
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, FromQueryResult, QueryFilter, QuerySelect, Set};
|
use sea_orm::{
|
||||||
|
ActiveModelTrait, ColumnTrait, EntityTrait, FromQueryResult, QueryFilter, QuerySelect, Set,
|
||||||
|
};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::entity::ai_analysis;
|
use crate::entity::ai_analysis;
|
||||||
@@ -14,6 +16,7 @@ impl UsageService {
|
|||||||
Self { db }
|
Self { db }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub async fn log_usage(
|
pub async fn log_usage(
|
||||||
&self,
|
&self,
|
||||||
tenant_id: Uuid,
|
tenant_id: Uuid,
|
||||||
|
|||||||
@@ -98,5 +98,4 @@ mod tests {
|
|||||||
other => panic!("Expected Validation, got {:?}", other),
|
other => panic!("Expected Validation, got {:?}", other),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,11 @@ where
|
|||||||
// TODO: 多租户微信登录需要设计租户解析策略(如 per-appid 映射或登录后选择租户)
|
// TODO: 多租户微信登录需要设计租户解析策略(如 per-appid 映射或登录后选择租户)
|
||||||
let tenant_id = state.default_tenant_id;
|
let tenant_id = state.default_tenant_id;
|
||||||
let resp = WechatService::login(&state, tenant_id, &req.code).await?;
|
let resp = WechatService::login(&state, tenant_id, &req.code).await?;
|
||||||
tracing::info!(bound = resp.bound, has_token = resp.token.is_some(), "微信登录结果");
|
tracing::info!(
|
||||||
|
bound = resp.bound,
|
||||||
|
has_token = resp.token.is_some(),
|
||||||
|
"微信登录结果"
|
||||||
|
);
|
||||||
Ok(Json(ApiResponse::ok(resp)))
|
Ok(Json(ApiResponse::ok(resp)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,13 +79,8 @@ where
|
|||||||
|
|
||||||
// TODO: 多租户微信登录需要设计租户解析策略
|
// TODO: 多租户微信登录需要设计租户解析策略
|
||||||
let tenant_id = state.default_tenant_id;
|
let tenant_id = state.default_tenant_id;
|
||||||
let resp = WechatService::bind_phone(
|
let resp =
|
||||||
&state,
|
WechatService::bind_phone(&state, tenant_id, &req.openid, &req.encrypted_data, &req.iv)
|
||||||
tenant_id,
|
.await?;
|
||||||
&req.openid,
|
|
||||||
&req.encrypted_data,
|
|
||||||
&req.iv,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(Json(ApiResponse::ok(resp)))
|
Ok(Json(ApiResponse::ok(resp)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ async fn fetch_permission_data_scopes(
|
|||||||
row.try_get_by_index::<String>(0),
|
row.try_get_by_index::<String>(0),
|
||||||
row.try_get_by_index::<String>(2),
|
row.try_get_by_index::<String>(2),
|
||||||
) {
|
) {
|
||||||
scopes.insert(code, DataScope::from_str(&scope));
|
scopes.insert(code, DataScope::parse_scope(&scope));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
scopes
|
scopes
|
||||||
|
|||||||
@@ -159,13 +159,10 @@ impl ErpModule for AuthModule {
|
|||||||
db: &sea_orm::DatabaseConnection,
|
db: &sea_orm::DatabaseConnection,
|
||||||
_event_bus: &EventBus,
|
_event_bus: &EventBus,
|
||||||
) -> AppResult<()> {
|
) -> AppResult<()> {
|
||||||
let password = std::env::var("ERP__SUPER_ADMIN_PASSWORD")
|
let password = std::env::var("ERP__SUPER_ADMIN_PASSWORD").map_err(|_| {
|
||||||
.map_err(|_| {
|
tracing::error!("环境变量 ERP__SUPER_ADMIN_PASSWORD 未设置,无法初始化租户认证");
|
||||||
tracing::error!("环境变量 ERP__SUPER_ADMIN_PASSWORD 未设置,无法初始化租户认证");
|
erp_core::error::AppError::Internal("ERP__SUPER_ADMIN_PASSWORD 未设置".to_string())
|
||||||
erp_core::error::AppError::Internal(
|
})?;
|
||||||
"ERP__SUPER_ADMIN_PASSWORD 未设置".to_string(),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
crate::service::seed::seed_tenant_auth(db, tenant_id, &password)
|
crate::service::seed::seed_tenant_auth(db, tenant_id, &password)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| erp_core::error::AppError::Internal(e.to_string()))?;
|
.map_err(|e| erp_core::error::AppError::Internal(e.to_string()))?;
|
||||||
@@ -178,8 +175,8 @@ impl ErpModule for AuthModule {
|
|||||||
tenant_id: Uuid,
|
tenant_id: Uuid,
|
||||||
db: &sea_orm::DatabaseConnection,
|
db: &sea_orm::DatabaseConnection,
|
||||||
) -> AppResult<()> {
|
) -> AppResult<()> {
|
||||||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
|
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
|
||||||
|
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
|
|
||||||
@@ -210,29 +207,144 @@ impl ErpModule for AuthModule {
|
|||||||
|
|
||||||
fn permissions(&self) -> Vec<PermissionDescriptor> {
|
fn permissions(&self) -> Vec<PermissionDescriptor> {
|
||||||
vec![
|
vec![
|
||||||
PermissionDescriptor { code: "user.list".into(), name: "查看用户列表".into(), description: "查看用户列表".into(), module: "auth".into() },
|
PermissionDescriptor {
|
||||||
PermissionDescriptor { code: "user.create".into(), name: "创建用户".into(), description: "创建新用户".into(), module: "auth".into() },
|
code: "user.list".into(),
|
||||||
PermissionDescriptor { code: "user.read".into(), name: "查看用户详情".into(), description: "查看用户信息".into(), module: "auth".into() },
|
name: "查看用户列表".into(),
|
||||||
PermissionDescriptor { code: "user.update".into(), name: "编辑用户".into(), description: "编辑用户信息".into(), module: "auth".into() },
|
description: "查看用户列表".into(),
|
||||||
PermissionDescriptor { code: "user.delete".into(), name: "删除用户".into(), description: "软删除用户".into(), module: "auth".into() },
|
module: "auth".into(),
|
||||||
PermissionDescriptor { code: "role.list".into(), name: "查看角色列表".into(), description: "查看角色列表".into(), module: "auth".into() },
|
},
|
||||||
PermissionDescriptor { code: "role.create".into(), name: "创建角色".into(), description: "创建新角色".into(), module: "auth".into() },
|
PermissionDescriptor {
|
||||||
PermissionDescriptor { code: "role.read".into(), name: "查看角色详情".into(), description: "查看角色信息".into(), module: "auth".into() },
|
code: "user.create".into(),
|
||||||
PermissionDescriptor { code: "role.update".into(), name: "编辑角色".into(), description: "编辑角色".into(), module: "auth".into() },
|
name: "创建用户".into(),
|
||||||
PermissionDescriptor { code: "role.delete".into(), name: "删除角色".into(), description: "删除角色".into(), module: "auth".into() },
|
description: "创建新用户".into(),
|
||||||
PermissionDescriptor { code: "permission.list".into(), name: "查看权限".into(), description: "查看权限列表".into(), module: "auth".into() },
|
module: "auth".into(),
|
||||||
PermissionDescriptor { code: "organization.list".into(), name: "查看组织列表".into(), description: "查看组织列表".into(), module: "auth".into() },
|
},
|
||||||
PermissionDescriptor { code: "organization.create".into(), name: "创建组织".into(), description: "创建组织".into(), module: "auth".into() },
|
PermissionDescriptor {
|
||||||
PermissionDescriptor { code: "organization.update".into(), name: "编辑组织".into(), description: "编辑组织".into(), module: "auth".into() },
|
code: "user.read".into(),
|
||||||
PermissionDescriptor { code: "organization.delete".into(), name: "删除组织".into(), description: "删除组织".into(), module: "auth".into() },
|
name: "查看用户详情".into(),
|
||||||
PermissionDescriptor { code: "department.list".into(), name: "查看部门列表".into(), description: "查看部门列表".into(), module: "auth".into() },
|
description: "查看用户信息".into(),
|
||||||
PermissionDescriptor { code: "department.create".into(), name: "创建部门".into(), description: "创建部门".into(), module: "auth".into() },
|
module: "auth".into(),
|
||||||
PermissionDescriptor { code: "department.update".into(), name: "编辑部门".into(), description: "编辑部门".into(), module: "auth".into() },
|
},
|
||||||
PermissionDescriptor { code: "department.delete".into(), name: "删除部门".into(), description: "删除部门".into(), module: "auth".into() },
|
PermissionDescriptor {
|
||||||
PermissionDescriptor { code: "position.list".into(), name: "查看岗位列表".into(), description: "查看岗位列表".into(), module: "auth".into() },
|
code: "user.update".into(),
|
||||||
PermissionDescriptor { code: "position.create".into(), name: "创建岗位".into(), description: "创建岗位".into(), module: "auth".into() },
|
name: "编辑用户".into(),
|
||||||
PermissionDescriptor { code: "position.update".into(), name: "编辑岗位".into(), description: "编辑岗位".into(), module: "auth".into() },
|
description: "编辑用户信息".into(),
|
||||||
PermissionDescriptor { code: "position.delete".into(), name: "删除岗位".into(), description: "删除岗位".into(), module: "auth".into() },
|
module: "auth".into(),
|
||||||
|
},
|
||||||
|
PermissionDescriptor {
|
||||||
|
code: "user.delete".into(),
|
||||||
|
name: "删除用户".into(),
|
||||||
|
description: "软删除用户".into(),
|
||||||
|
module: "auth".into(),
|
||||||
|
},
|
||||||
|
PermissionDescriptor {
|
||||||
|
code: "role.list".into(),
|
||||||
|
name: "查看角色列表".into(),
|
||||||
|
description: "查看角色列表".into(),
|
||||||
|
module: "auth".into(),
|
||||||
|
},
|
||||||
|
PermissionDescriptor {
|
||||||
|
code: "role.create".into(),
|
||||||
|
name: "创建角色".into(),
|
||||||
|
description: "创建新角色".into(),
|
||||||
|
module: "auth".into(),
|
||||||
|
},
|
||||||
|
PermissionDescriptor {
|
||||||
|
code: "role.read".into(),
|
||||||
|
name: "查看角色详情".into(),
|
||||||
|
description: "查看角色信息".into(),
|
||||||
|
module: "auth".into(),
|
||||||
|
},
|
||||||
|
PermissionDescriptor {
|
||||||
|
code: "role.update".into(),
|
||||||
|
name: "编辑角色".into(),
|
||||||
|
description: "编辑角色".into(),
|
||||||
|
module: "auth".into(),
|
||||||
|
},
|
||||||
|
PermissionDescriptor {
|
||||||
|
code: "role.delete".into(),
|
||||||
|
name: "删除角色".into(),
|
||||||
|
description: "删除角色".into(),
|
||||||
|
module: "auth".into(),
|
||||||
|
},
|
||||||
|
PermissionDescriptor {
|
||||||
|
code: "permission.list".into(),
|
||||||
|
name: "查看权限".into(),
|
||||||
|
description: "查看权限列表".into(),
|
||||||
|
module: "auth".into(),
|
||||||
|
},
|
||||||
|
PermissionDescriptor {
|
||||||
|
code: "organization.list".into(),
|
||||||
|
name: "查看组织列表".into(),
|
||||||
|
description: "查看组织列表".into(),
|
||||||
|
module: "auth".into(),
|
||||||
|
},
|
||||||
|
PermissionDescriptor {
|
||||||
|
code: "organization.create".into(),
|
||||||
|
name: "创建组织".into(),
|
||||||
|
description: "创建组织".into(),
|
||||||
|
module: "auth".into(),
|
||||||
|
},
|
||||||
|
PermissionDescriptor {
|
||||||
|
code: "organization.update".into(),
|
||||||
|
name: "编辑组织".into(),
|
||||||
|
description: "编辑组织".into(),
|
||||||
|
module: "auth".into(),
|
||||||
|
},
|
||||||
|
PermissionDescriptor {
|
||||||
|
code: "organization.delete".into(),
|
||||||
|
name: "删除组织".into(),
|
||||||
|
description: "删除组织".into(),
|
||||||
|
module: "auth".into(),
|
||||||
|
},
|
||||||
|
PermissionDescriptor {
|
||||||
|
code: "department.list".into(),
|
||||||
|
name: "查看部门列表".into(),
|
||||||
|
description: "查看部门列表".into(),
|
||||||
|
module: "auth".into(),
|
||||||
|
},
|
||||||
|
PermissionDescriptor {
|
||||||
|
code: "department.create".into(),
|
||||||
|
name: "创建部门".into(),
|
||||||
|
description: "创建部门".into(),
|
||||||
|
module: "auth".into(),
|
||||||
|
},
|
||||||
|
PermissionDescriptor {
|
||||||
|
code: "department.update".into(),
|
||||||
|
name: "编辑部门".into(),
|
||||||
|
description: "编辑部门".into(),
|
||||||
|
module: "auth".into(),
|
||||||
|
},
|
||||||
|
PermissionDescriptor {
|
||||||
|
code: "department.delete".into(),
|
||||||
|
name: "删除部门".into(),
|
||||||
|
description: "删除部门".into(),
|
||||||
|
module: "auth".into(),
|
||||||
|
},
|
||||||
|
PermissionDescriptor {
|
||||||
|
code: "position.list".into(),
|
||||||
|
name: "查看岗位列表".into(),
|
||||||
|
description: "查看岗位列表".into(),
|
||||||
|
module: "auth".into(),
|
||||||
|
},
|
||||||
|
PermissionDescriptor {
|
||||||
|
code: "position.create".into(),
|
||||||
|
name: "创建岗位".into(),
|
||||||
|
description: "创建岗位".into(),
|
||||||
|
module: "auth".into(),
|
||||||
|
},
|
||||||
|
PermissionDescriptor {
|
||||||
|
code: "position.update".into(),
|
||||||
|
name: "编辑岗位".into(),
|
||||||
|
description: "编辑岗位".into(),
|
||||||
|
module: "auth".into(),
|
||||||
|
},
|
||||||
|
PermissionDescriptor {
|
||||||
|
code: "position.delete".into(),
|
||||||
|
name: "删除岗位".into(),
|
||||||
|
description: "删除岗位".into(),
|
||||||
|
module: "auth".into(),
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,11 +64,10 @@ impl AuthService {
|
|||||||
None => {
|
None => {
|
||||||
// 审计:用户不存在(登录失败)
|
// 审计:用户不存在(登录失败)
|
||||||
audit_service::record(
|
audit_service::record(
|
||||||
AuditLog::new(tenant_id, None, "user.login_failed", "user")
|
AuditLog::new(tenant_id, None, "user.login_failed", "user").with_request_info(
|
||||||
.with_request_info(
|
req_info.as_ref().and_then(|r| r.ip.clone()),
|
||||||
req_info.as_ref().and_then(|r| r.ip.clone()),
|
req_info.as_ref().and_then(|r| r.user_agent.clone()),
|
||||||
req_info.as_ref().and_then(|r| r.user_agent.clone()),
|
),
|
||||||
),
|
|
||||||
db,
|
db,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|||||||
@@ -317,13 +317,7 @@ const DEFAULT_PERMISSIONS: &[(&str, &str, &str, &str, &str)] = &[
|
|||||||
"admin",
|
"admin",
|
||||||
"管理插件全生命周期",
|
"管理插件全生命周期",
|
||||||
),
|
),
|
||||||
(
|
("plugin.list", "查看插件", "plugin", "list", "查看插件列表"),
|
||||||
"plugin.list",
|
|
||||||
"查看插件",
|
|
||||||
"plugin",
|
|
||||||
"list",
|
|
||||||
"查看插件列表",
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Indices of read-only (list/read) permissions within DEFAULT_PERMISSIONS.
|
/// Indices of read-only (list/read) permissions within DEFAULT_PERMISSIONS.
|
||||||
|
|||||||
@@ -153,7 +153,11 @@ impl TokenService {
|
|||||||
|
|
||||||
/// Revoke a specific refresh token by database ID.
|
/// Revoke a specific refresh token by database ID.
|
||||||
/// Verifies that the token belongs to the specified user for security.
|
/// Verifies that the token belongs to the specified user for security.
|
||||||
pub async fn revoke_token(token_id: Uuid, user_id: Uuid, db: &DatabaseConnection) -> AuthResult<()> {
|
pub async fn revoke_token(
|
||||||
|
token_id: Uuid,
|
||||||
|
user_id: Uuid,
|
||||||
|
db: &DatabaseConnection,
|
||||||
|
) -> AuthResult<()> {
|
||||||
let token_row = user_token::Entity::find_by_id(token_id)
|
let token_row = user_token::Entity::find_by_id(token_id)
|
||||||
.filter(user_token::Column::UserId.eq(user_id))
|
.filter(user_token::Column::UserId.eq(user_id))
|
||||||
.one(db)
|
.one(db)
|
||||||
|
|||||||
@@ -406,8 +406,7 @@ impl UserService {
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let role_map: HashMap<Uuid, &role::Model> =
|
let role_map: HashMap<Uuid, &role::Model> = roles.iter().map(|r| (r.id, r)).collect();
|
||||||
roles.iter().map(|r| (r.id, r)).collect();
|
|
||||||
|
|
||||||
// 3. 按 user_id 分组
|
// 3. 按 user_id 分组
|
||||||
let mut result: HashMap<Uuid, Vec<RoleResp>> = HashMap::new();
|
let mut result: HashMap<Uuid, Vec<RoleResp>> = HashMap::new();
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, KeyIvInit};
|
use aes::cipher::{BlockDecryptMut, KeyIvInit, block_padding::Pkcs7};
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use chrono::Utc;
|
|
||||||
use cbc::Decryptor;
|
use cbc::Decryptor;
|
||||||
use sea_orm::{
|
use chrono::Utc;
|
||||||
ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set,
|
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
|
||||||
};
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
@@ -59,9 +57,13 @@ impl WechatService {
|
|||||||
code = %code,
|
code = %code,
|
||||||
"fetch_session 开始"
|
"fetch_session 开始"
|
||||||
);
|
);
|
||||||
let session =
|
let session = fetch_session(
|
||||||
fetch_session(&state.wechat_appid, &state.wechat_secret, code, state.wechat_dev_mode)
|
&state.wechat_appid,
|
||||||
.await?;
|
&state.wechat_secret,
|
||||||
|
code,
|
||||||
|
state.wechat_dev_mode,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let openid = session
|
let openid = session
|
||||||
.openid
|
.openid
|
||||||
@@ -69,18 +71,18 @@ impl WechatService {
|
|||||||
.ok_or_else(|| AuthError::Validation("微信登录失败:未获取到 openid".to_string()))?;
|
.ok_or_else(|| AuthError::Validation("微信登录失败:未获取到 openid".to_string()))?;
|
||||||
|
|
||||||
// 缓存 session_key(Redis 优先,内存降级)
|
// 缓存 session_key(Redis 优先,内存降级)
|
||||||
if let Some(sk) = &session.session_key {
|
if let Some(sk) = &session.session_key
|
||||||
if let Err(e) = Self::store_session_key_redis(&state.redis, &openid, sk).await {
|
&& let Err(e) = Self::store_session_key_redis(&state.redis, &openid, sk).await
|
||||||
tracing::warn!(openid = %openid, error = %e, "Redis session_key 存储失败,降级内存");
|
{
|
||||||
let mut cache = MEMORY_FALLBACK.lock().await;
|
tracing::warn!(openid = %openid, error = %e, "Redis session_key 存储失败,降级内存");
|
||||||
cache.insert(
|
let mut cache = MEMORY_FALLBACK.lock().await;
|
||||||
openid.clone(),
|
cache.insert(
|
||||||
SessionEntry {
|
openid.clone(),
|
||||||
session_key: sk.clone(),
|
SessionEntry {
|
||||||
created_at: Instant::now(),
|
session_key: sk.clone(),
|
||||||
},
|
created_at: Instant::now(),
|
||||||
);
|
},
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let existing = wechat_user::Entity::find()
|
let existing = wechat_user::Entity::find()
|
||||||
@@ -141,8 +143,7 @@ impl WechatService {
|
|||||||
return Err(AuthError::Validation("该微信已绑定账号".to_string()));
|
return Err(AuthError::Validation("该微信已绑定账号".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let user_id =
|
let user_id = Self::find_or_create_user_by_phone(&state.db, tenant_id, &phone).await?;
|
||||||
Self::find_or_create_user_by_phone(&state.db, tenant_id, &phone).await?;
|
|
||||||
|
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
let wu = wechat_user::ActiveModel {
|
let wu = wechat_user::ActiveModel {
|
||||||
@@ -248,22 +249,19 @@ impl WechatService {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_session_key(
|
async fn get_session_key(redis: &Option<redis::Client>, openid: &str) -> AuthResult<String> {
|
||||||
redis: &Option<redis::Client>,
|
|
||||||
openid: &str,
|
|
||||||
) -> AuthResult<String> {
|
|
||||||
// 1. 尝试 Redis
|
// 1. 尝试 Redis
|
||||||
if let Some(client) = redis {
|
if let Some(client) = redis
|
||||||
if let Ok(mut conn) = client.get_multiplexed_async_connection().await {
|
&& let Ok(mut conn) = client.get_multiplexed_async_connection().await
|
||||||
let key = format!("{}{}", REDIS_KEY_PREFIX, openid);
|
{
|
||||||
let result: Option<String> = redis::cmd("GETDEL")
|
let key = format!("{}{}", REDIS_KEY_PREFIX, openid);
|
||||||
.arg(&key)
|
let result: Option<String> = redis::cmd("GETDEL")
|
||||||
.query_async::<Option<String>>(&mut conn)
|
.arg(&key)
|
||||||
.await
|
.query_async::<Option<String>>(&mut conn)
|
||||||
.unwrap_or(None);
|
.await
|
||||||
if let Some(sk) = result {
|
.unwrap_or(None);
|
||||||
return Ok(sk);
|
if let Some(sk) = result {
|
||||||
}
|
return Ok(sk);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,11 +283,7 @@ impl WechatService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// AES-128-CBC 解密微信手机号
|
/// AES-128-CBC 解密微信手机号
|
||||||
fn decrypt_phone_number(
|
fn decrypt_phone_number(session_key: &str, encrypted_data: &str, iv: &str) -> AuthResult<String> {
|
||||||
session_key: &str,
|
|
||||||
encrypted_data: &str,
|
|
||||||
iv: &str,
|
|
||||||
) -> AuthResult<String> {
|
|
||||||
let engine = base64::engine::general_purpose::STANDARD;
|
let engine = base64::engine::general_purpose::STANDARD;
|
||||||
|
|
||||||
let key_bytes = engine
|
let key_bytes = engine
|
||||||
@@ -303,9 +297,7 @@ fn decrypt_phone_number(
|
|||||||
.map_err(|e| AuthError::Validation(format!("encrypted_data base64 解码失败: {}", e)))?;
|
.map_err(|e| AuthError::Validation(format!("encrypted_data base64 解码失败: {}", e)))?;
|
||||||
|
|
||||||
if key_bytes.len() != 16 {
|
if key_bytes.len() != 16 {
|
||||||
return Err(AuthError::Validation(
|
return Err(AuthError::Validation("session_key 长度不正确".to_string()));
|
||||||
"session_key 长度不正确".to_string(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
if iv_bytes.len() != 16 {
|
if iv_bytes.len() != 16 {
|
||||||
return Err(AuthError::Validation("iv 长度不正确".to_string()));
|
return Err(AuthError::Validation("iv 长度不正确".to_string()));
|
||||||
@@ -319,8 +311,8 @@ fn decrypt_phone_number(
|
|||||||
.decrypt_padded_mut::<Pkcs7>(&mut buf)
|
.decrypt_padded_mut::<Pkcs7>(&mut buf)
|
||||||
.map_err(|e| AuthError::Validation(format!("AES 解密失败: {}", e)))?;
|
.map_err(|e| AuthError::Validation(format!("AES 解密失败: {}", e)))?;
|
||||||
|
|
||||||
let plaintext =
|
let plaintext = String::from_utf8(decrypted.to_vec())
|
||||||
String::from_utf8(decrypted.to_vec()).map_err(|_| AuthError::Validation("解密结果非 UTF-8".to_string()))?;
|
.map_err(|_| AuthError::Validation("解密结果非 UTF-8".to_string()))?;
|
||||||
|
|
||||||
// 微信返回的 JSON 包含 watermark 等字段,提取 phone_number
|
// 微信返回的 JSON 包含 watermark 等字段,提取 phone_number
|
||||||
let info: serde_json::Value = serde_json::from_str(&plaintext)
|
let info: serde_json::Value = serde_json::from_str(&plaintext)
|
||||||
@@ -358,14 +350,9 @@ async fn build_login_resp(
|
|||||||
jwt.secret,
|
jwt.secret,
|
||||||
jwt.access_ttl_secs,
|
jwt.access_ttl_secs,
|
||||||
)?;
|
)?;
|
||||||
let (refresh_token, _) = TokenService::sign_refresh_token(
|
let (refresh_token, _) =
|
||||||
user_id,
|
TokenService::sign_refresh_token(user_id, tenant_id, db, jwt.secret, jwt.refresh_ttl_secs)
|
||||||
tenant_id,
|
.await?;
|
||||||
db,
|
|
||||||
jwt.secret,
|
|
||||||
jwt.refresh_ttl_secs,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let role_resps = AuthService::get_user_role_resps(user_id, tenant_id, db).await?;
|
let role_resps = AuthService::get_user_role_resps(user_id, tenant_id, db).await?;
|
||||||
|
|
||||||
@@ -424,15 +411,15 @@ async fn fetch_session(
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| AuthError::Validation(format!("微信 API 响应解析失败: {}", e)))?;
|
.map_err(|e| AuthError::Validation(format!("微信 API 响应解析失败: {}", e)))?;
|
||||||
|
|
||||||
if let Some(errcode) = session.errcode {
|
if let Some(errcode) = session.errcode
|
||||||
if errcode != 0 {
|
&& errcode != 0
|
||||||
let msg = session.errmsg.clone().unwrap_or_default();
|
{
|
||||||
tracing::error!(errcode, errmsg = %msg, "微信 jscode2session 返回错误");
|
let msg = session.errmsg.clone().unwrap_or_default();
|
||||||
return Err(AuthError::Validation(format!(
|
tracing::error!(errcode, errmsg = %msg, "微信 jscode2session 返回错误");
|
||||||
"微信登录失败 ({}): {}",
|
return Err(AuthError::Validation(format!(
|
||||||
errcode, msg
|
"微信登录失败 ({}): {}",
|
||||||
)));
|
errcode, msg
|
||||||
}
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
|
|||||||
@@ -101,19 +101,40 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn config_error_display_messages() {
|
fn config_error_display_messages() {
|
||||||
// 验证各变体的 Display 输出包含中文描述
|
// 验证各变体的 Display 输出包含中文描述
|
||||||
assert!(ConfigError::Validation("test".into()).to_string().contains("验证失败"));
|
assert!(
|
||||||
assert!(ConfigError::NotFound("test".into()).to_string().contains("资源未找到"));
|
ConfigError::Validation("test".into())
|
||||||
assert!(ConfigError::DuplicateKey("test".into()).to_string().contains("键已存在"));
|
.to_string()
|
||||||
assert!(ConfigError::NumberingExhausted("test".into()).to_string().contains("编号序列耗尽"));
|
.contains("验证失败")
|
||||||
assert!(ConfigError::VersionMismatch.to_string().contains("版本冲突"));
|
);
|
||||||
|
assert!(
|
||||||
|
ConfigError::NotFound("test".into())
|
||||||
|
.to_string()
|
||||||
|
.contains("资源未找到")
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
ConfigError::DuplicateKey("test".into())
|
||||||
|
.to_string()
|
||||||
|
.contains("键已存在")
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
ConfigError::NumberingExhausted("test".into())
|
||||||
|
.to_string()
|
||||||
|
.contains("编号序列耗尽")
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
ConfigError::VersionMismatch
|
||||||
|
.to_string()
|
||||||
|
.contains("版本冲突")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn transaction_error_connection_maps_to_validation() {
|
fn transaction_error_connection_maps_to_validation() {
|
||||||
// TransactionError::Connection 应该转换为 ConfigError::Validation
|
// TransactionError::Connection 应该转换为 ConfigError::Validation
|
||||||
let config_err: ConfigError =
|
let config_err: ConfigError = sea_orm::TransactionError::Connection(sea_orm::DbErr::Conn(
|
||||||
sea_orm::TransactionError::Connection(sea_orm::DbErr::Conn(sea_orm::RuntimeErr::Internal("连接失败".to_string())))
|
sea_orm::RuntimeErr::Internal("连接失败".to_string()),
|
||||||
.into();
|
))
|
||||||
|
.into();
|
||||||
match config_err {
|
match config_err {
|
||||||
ConfigError::Validation(msg) => assert!(msg.contains("连接失败")),
|
ConfigError::Validation(msg) => assert!(msg.contains("连接失败")),
|
||||||
other => panic!("期望 Validation,实际得到 {:?}", other),
|
other => panic!("期望 Validation,实际得到 {:?}", other),
|
||||||
|
|||||||
@@ -125,8 +125,12 @@ where
|
|||||||
pub async fn get_public_brand() -> JsonResponse<ApiResponse<PublicBrandResp>> {
|
pub async fn get_public_brand() -> JsonResponse<ApiResponse<PublicBrandResp>> {
|
||||||
let defaults = default_theme();
|
let defaults = default_theme();
|
||||||
JsonResponse(ApiResponse::ok(PublicBrandResp {
|
JsonResponse(ApiResponse::ok(PublicBrandResp {
|
||||||
brand_name: defaults.brand_name.unwrap_or_else(|| "HMS 健康管理平台".into()),
|
brand_name: defaults
|
||||||
brand_slogan: defaults.brand_slogan.unwrap_or_else(|| "新一代健康管理平台".into()),
|
.brand_name
|
||||||
|
.unwrap_or_else(|| "HMS 健康管理平台".into()),
|
||||||
|
brand_slogan: defaults
|
||||||
|
.brand_slogan
|
||||||
|
.unwrap_or_else(|| "新一代健康管理平台".into()),
|
||||||
brand_features: defaults
|
brand_features: defaults
|
||||||
.brand_features
|
.brand_features
|
||||||
.unwrap_or_else(|| "患者管理 · 健康监测 · 随访管理 · AI 智能分析".into()),
|
.unwrap_or_else(|| "患者管理 · 健康监测 · 随访管理 · AI 智能分析".into()),
|
||||||
|
|||||||
@@ -64,10 +64,7 @@ impl ConfigModule {
|
|||||||
put(menu_handler::update_menu).delete(menu_handler::delete_menu),
|
put(menu_handler::update_menu).delete(menu_handler::delete_menu),
|
||||||
)
|
)
|
||||||
// User menu tree (no special permission required)
|
// User menu tree (no special permission required)
|
||||||
.route(
|
.route("/menus/user", get(menu_handler::get_user_menus))
|
||||||
"/menus/user",
|
|
||||||
get(menu_handler::get_user_menus),
|
|
||||||
)
|
|
||||||
// Setting routes
|
// Setting routes
|
||||||
.route(
|
.route(
|
||||||
"/config/settings/{key}",
|
"/config/settings/{key}",
|
||||||
@@ -153,24 +150,114 @@ impl ErpModule for ConfigModule {
|
|||||||
|
|
||||||
fn permissions(&self) -> Vec<PermissionDescriptor> {
|
fn permissions(&self) -> Vec<PermissionDescriptor> {
|
||||||
vec![
|
vec![
|
||||||
PermissionDescriptor { code: "dictionary.list".into(), name: "查看字典".into(), description: "查看数据字典".into(), module: "config".into() },
|
PermissionDescriptor {
|
||||||
PermissionDescriptor { code: "dictionary.create".into(), name: "创建字典".into(), description: "创建数据字典".into(), module: "config".into() },
|
code: "dictionary.list".into(),
|
||||||
PermissionDescriptor { code: "dictionary.update".into(), name: "编辑字典".into(), description: "编辑数据字典".into(), module: "config".into() },
|
name: "查看字典".into(),
|
||||||
PermissionDescriptor { code: "dictionary.delete".into(), name: "删除字典".into(), description: "删除数据字典".into(), module: "config".into() },
|
description: "查看数据字典".into(),
|
||||||
PermissionDescriptor { code: "menu.list".into(), name: "查看菜单".into(), description: "查看菜单配置".into(), module: "config".into() },
|
module: "config".into(),
|
||||||
PermissionDescriptor { code: "menu.update".into(), name: "编辑菜单".into(), description: "编辑菜单配置".into(), module: "config".into() },
|
},
|
||||||
PermissionDescriptor { code: "setting.read".into(), name: "查看配置".into(), description: "查看系统参数".into(), module: "config".into() },
|
PermissionDescriptor {
|
||||||
PermissionDescriptor { code: "setting.update".into(), name: "编辑配置".into(), description: "编辑系统参数".into(), module: "config".into() },
|
code: "dictionary.create".into(),
|
||||||
PermissionDescriptor { code: "setting.delete".into(), name: "删除配置".into(), description: "删除系统参数".into(), module: "config".into() },
|
name: "创建字典".into(),
|
||||||
PermissionDescriptor { code: "numbering.list".into(), name: "查看编号规则".into(), description: "查看编号规则".into(), module: "config".into() },
|
description: "创建数据字典".into(),
|
||||||
PermissionDescriptor { code: "numbering.create".into(), name: "创建编号规则".into(), description: "创建编号规则".into(), module: "config".into() },
|
module: "config".into(),
|
||||||
PermissionDescriptor { code: "numbering.update".into(), name: "编辑编号规则".into(), description: "编辑编号规则".into(), module: "config".into() },
|
},
|
||||||
PermissionDescriptor { code: "numbering.delete".into(), name: "删除编号规则".into(), description: "删除编号规则".into(), module: "config".into() },
|
PermissionDescriptor {
|
||||||
PermissionDescriptor { code: "numbering.generate".into(), name: "生成编号".into(), description: "生成文档编号".into(), module: "config".into() },
|
code: "dictionary.update".into(),
|
||||||
PermissionDescriptor { code: "theme.read".into(), name: "查看主题".into(), description: "查看主题设置".into(), module: "config".into() },
|
name: "编辑字典".into(),
|
||||||
PermissionDescriptor { code: "theme.update".into(), name: "编辑主题".into(), description: "编辑主题设置".into(), module: "config".into() },
|
description: "编辑数据字典".into(),
|
||||||
PermissionDescriptor { code: "language.list".into(), name: "查看语言".into(), description: "查看语言配置".into(), module: "config".into() },
|
module: "config".into(),
|
||||||
PermissionDescriptor { code: "language.update".into(), name: "编辑语言".into(), description: "编辑语言设置".into(), module: "config".into() },
|
},
|
||||||
|
PermissionDescriptor {
|
||||||
|
code: "dictionary.delete".into(),
|
||||||
|
name: "删除字典".into(),
|
||||||
|
description: "删除数据字典".into(),
|
||||||
|
module: "config".into(),
|
||||||
|
},
|
||||||
|
PermissionDescriptor {
|
||||||
|
code: "menu.list".into(),
|
||||||
|
name: "查看菜单".into(),
|
||||||
|
description: "查看菜单配置".into(),
|
||||||
|
module: "config".into(),
|
||||||
|
},
|
||||||
|
PermissionDescriptor {
|
||||||
|
code: "menu.update".into(),
|
||||||
|
name: "编辑菜单".into(),
|
||||||
|
description: "编辑菜单配置".into(),
|
||||||
|
module: "config".into(),
|
||||||
|
},
|
||||||
|
PermissionDescriptor {
|
||||||
|
code: "setting.read".into(),
|
||||||
|
name: "查看配置".into(),
|
||||||
|
description: "查看系统参数".into(),
|
||||||
|
module: "config".into(),
|
||||||
|
},
|
||||||
|
PermissionDescriptor {
|
||||||
|
code: "setting.update".into(),
|
||||||
|
name: "编辑配置".into(),
|
||||||
|
description: "编辑系统参数".into(),
|
||||||
|
module: "config".into(),
|
||||||
|
},
|
||||||
|
PermissionDescriptor {
|
||||||
|
code: "setting.delete".into(),
|
||||||
|
name: "删除配置".into(),
|
||||||
|
description: "删除系统参数".into(),
|
||||||
|
module: "config".into(),
|
||||||
|
},
|
||||||
|
PermissionDescriptor {
|
||||||
|
code: "numbering.list".into(),
|
||||||
|
name: "查看编号规则".into(),
|
||||||
|
description: "查看编号规则".into(),
|
||||||
|
module: "config".into(),
|
||||||
|
},
|
||||||
|
PermissionDescriptor {
|
||||||
|
code: "numbering.create".into(),
|
||||||
|
name: "创建编号规则".into(),
|
||||||
|
description: "创建编号规则".into(),
|
||||||
|
module: "config".into(),
|
||||||
|
},
|
||||||
|
PermissionDescriptor {
|
||||||
|
code: "numbering.update".into(),
|
||||||
|
name: "编辑编号规则".into(),
|
||||||
|
description: "编辑编号规则".into(),
|
||||||
|
module: "config".into(),
|
||||||
|
},
|
||||||
|
PermissionDescriptor {
|
||||||
|
code: "numbering.delete".into(),
|
||||||
|
name: "删除编号规则".into(),
|
||||||
|
description: "删除编号规则".into(),
|
||||||
|
module: "config".into(),
|
||||||
|
},
|
||||||
|
PermissionDescriptor {
|
||||||
|
code: "numbering.generate".into(),
|
||||||
|
name: "生成编号".into(),
|
||||||
|
description: "生成文档编号".into(),
|
||||||
|
module: "config".into(),
|
||||||
|
},
|
||||||
|
PermissionDescriptor {
|
||||||
|
code: "theme.read".into(),
|
||||||
|
name: "查看主题".into(),
|
||||||
|
description: "查看主题设置".into(),
|
||||||
|
module: "config".into(),
|
||||||
|
},
|
||||||
|
PermissionDescriptor {
|
||||||
|
code: "theme.update".into(),
|
||||||
|
name: "编辑主题".into(),
|
||||||
|
description: "编辑主题设置".into(),
|
||||||
|
module: "config".into(),
|
||||||
|
},
|
||||||
|
PermissionDescriptor {
|
||||||
|
code: "language.list".into(),
|
||||||
|
name: "查看语言".into(),
|
||||||
|
description: "查看语言配置".into(),
|
||||||
|
module: "config".into(),
|
||||||
|
},
|
||||||
|
PermissionDescriptor {
|
||||||
|
code: "language.update".into(),
|
||||||
|
name: "编辑语言".into(),
|
||||||
|
description: "编辑语言设置".into(),
|
||||||
|
module: "config".into(),
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use sea_orm::{ActiveModelTrait, ColumnTrait, ConnectionTrait, EntityTrait, QueryFilter, QueryOrder, Set};
|
use sea_orm::{
|
||||||
|
ActiveModelTrait, ColumnTrait, ConnectionTrait, EntityTrait, QueryFilter, QueryOrder, Set,
|
||||||
|
};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::dto::{CreateMenuReq, MenuResp};
|
use crate::dto::{CreateMenuReq, MenuResp};
|
||||||
|
|||||||
@@ -35,11 +35,11 @@ pub(crate) fn format_number(
|
|||||||
result.push_str(separator);
|
result.push_str(separator);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(dp) = date_part {
|
if let Some(dp) = date_part
|
||||||
if !dp.is_empty() {
|
&& !dp.is_empty()
|
||||||
result.push_str(dp);
|
{
|
||||||
result.push_str(separator);
|
result.push_str(dp);
|
||||||
}
|
result.push_str(separator);
|
||||||
}
|
}
|
||||||
|
|
||||||
let width = (seq_length.max(1)) as usize;
|
let width = (seq_length.max(1)) as usize;
|
||||||
@@ -398,7 +398,10 @@ impl NumberingService {
|
|||||||
.map_err(|e| ConfigError::Validation(e.to_string()))?;
|
.map_err(|e| ConfigError::Validation(e.to_string()))?;
|
||||||
|
|
||||||
// 拼接编号字符串: {prefix}{separator}{date_part}{separator}{seq_padded}
|
// 拼接编号字符串: {prefix}{separator}{date_part}{separator}{seq_padded}
|
||||||
let date_part = rule.date_format.as_ref().map(|fmt| Utc::now().format(fmt).to_string());
|
let date_part = rule
|
||||||
|
.date_format
|
||||||
|
.as_ref()
|
||||||
|
.map(|fmt| Utc::now().format(fmt).to_string());
|
||||||
|
|
||||||
let number = format_number(
|
let number = format_number(
|
||||||
&rule.prefix,
|
&rule.prefix,
|
||||||
@@ -611,7 +614,8 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn reset_no_last_reset_date_returns_seq_start() {
|
fn reset_no_last_reset_date_returns_seq_start() {
|
||||||
// 从未重置过,使用 seq_start
|
// 从未重置过,使用 seq_start
|
||||||
let result = NumberingService::maybe_reset_sequence(999, 1, "daily", None, date(2026, 4, 15));
|
let result =
|
||||||
|
NumberingService::maybe_reset_sequence(999, 1, "daily", None, date(2026, 4, 15));
|
||||||
assert_eq!(result, 1);
|
assert_eq!(result, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use crate::audit::AuditLog;
|
|||||||
use crate::entity::audit_log;
|
use crate::entity::audit_log;
|
||||||
use crate::request_info::RequestInfo;
|
use crate::request_info::RequestInfo;
|
||||||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, Set};
|
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, Set};
|
||||||
use sha2::{Sha256, Digest};
|
use sha2::{Digest, Sha256};
|
||||||
use tracing;
|
use tracing;
|
||||||
|
|
||||||
/// 持久化审计日志到 audit_logs 表。
|
/// 持久化审计日志到 audit_logs 表。
|
||||||
@@ -16,14 +16,12 @@ use tracing;
|
|||||||
/// 计算 SHA256(id + action + resource_type + resource_id + created_at + prev_hash) 作为 record_hash。
|
/// 计算 SHA256(id + action + resource_type + resource_id + created_at + prev_hash) 作为 record_hash。
|
||||||
pub async fn record(mut log: AuditLog, db: &sea_orm::DatabaseConnection) {
|
pub async fn record(mut log: AuditLog, db: &sea_orm::DatabaseConnection) {
|
||||||
// 自动填充请求来源信息(仅当调用方未显式设置时)
|
// 自动填充请求来源信息(仅当调用方未显式设置时)
|
||||||
if log.ip_address.is_none() || log.user_agent.is_none() {
|
if let Some(info) = RequestInfo::try_current() {
|
||||||
if let Some(info) = RequestInfo::try_current() {
|
if log.ip_address.is_none() {
|
||||||
if log.ip_address.is_none() {
|
log.ip_address = info.ip_address;
|
||||||
log.ip_address = info.ip_address;
|
}
|
||||||
}
|
if log.user_agent.is_none() {
|
||||||
if log.user_agent.is_none() {
|
log.user_agent = info.user_agent;
|
||||||
log.user_agent = info.user_agent;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use aes_gcm::aead::Aead;
|
use aes_gcm::aead::Aead;
|
||||||
use aes_gcm::{Aes256Gcm, KeyInit, Nonce};
|
use aes_gcm::{Aes256Gcm, KeyInit, Nonce};
|
||||||
use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
|
use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
|
||||||
use rand::RngCore;
|
use rand::RngCore;
|
||||||
|
|
||||||
const CIPHER_VERSION: u8 = 0x01;
|
const CIPHER_VERSION: u8 = 0x01;
|
||||||
@@ -41,6 +41,8 @@ pub fn decrypt(key: &[u8; 32], encoded: &str) -> Result<String, String> {
|
|||||||
|
|
||||||
let cipher = Aes256Gcm::new_from_slice(key).map_err(|e| e.to_string())?;
|
let cipher = Aes256Gcm::new_from_slice(key).map_err(|e| e.to_string())?;
|
||||||
let nonce = Nonce::from_slice(nonce_bytes);
|
let nonce = Nonce::from_slice(nonce_bytes);
|
||||||
let plaintext = cipher.decrypt(nonce, ciphertext).map_err(|e| e.to_string())?;
|
let plaintext = cipher
|
||||||
|
.decrypt(nonce, ciphertext)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
String::from_utf8(plaintext).map_err(|e| e.to_string())
|
String::from_utf8(plaintext).map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,15 +46,15 @@ impl DekManager {
|
|||||||
kek: &[u8; 32],
|
kek: &[u8; 32],
|
||||||
) -> AppResult<([u8; 32], u32)> {
|
) -> AppResult<([u8; 32], u32)> {
|
||||||
// 检查缓存
|
// 检查缓存
|
||||||
if let Some(entry) = self.cache.get(&tenant_id) {
|
if let Some(entry) = self.cache.get(&tenant_id)
|
||||||
if entry.loaded_at.elapsed().as_secs() < self.ttl_secs {
|
&& entry.loaded_at.elapsed().as_secs() < self.ttl_secs
|
||||||
return Ok((entry.dek, entry.version));
|
{
|
||||||
}
|
return Ok((entry.dek, entry.version));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从加密 DEK 解密
|
// 从加密 DEK 解密
|
||||||
if let Some(enc_dek) = encrypted_dek {
|
if let Some(enc_dek) = encrypted_dek {
|
||||||
let dek_hex = engine::decrypt(kek, enc_dek).map_err(|e| AppError::Internal(e))?;
|
let dek_hex = engine::decrypt(kek, enc_dek).map_err(AppError::Internal)?;
|
||||||
let dek_bytes = hex::decode(&dek_hex).map_err(|e| AppError::Internal(e.to_string()))?;
|
let dek_bytes = hex::decode(&dek_hex).map_err(|e| AppError::Internal(e.to_string()))?;
|
||||||
if dek_bytes.len() != 32 {
|
if dek_bytes.len() != 32 {
|
||||||
return Err(AppError::Internal("DEK must be 32 bytes".into()));
|
return Err(AppError::Internal("DEK must be 32 bytes".into()));
|
||||||
@@ -64,29 +64,35 @@ impl DekManager {
|
|||||||
|
|
||||||
// 缓存(版本从外部传入时无法确定,使用默认值 1)
|
// 缓存(版本从外部传入时无法确定,使用默认值 1)
|
||||||
self.evict_if_full();
|
self.evict_if_full();
|
||||||
self.cache.insert(tenant_id, CachedDek {
|
self.cache.insert(
|
||||||
dek,
|
tenant_id,
|
||||||
version: 1,
|
CachedDek {
|
||||||
loaded_at: Instant::now(),
|
dek,
|
||||||
});
|
version: 1,
|
||||||
|
loaded_at: Instant::now(),
|
||||||
|
},
|
||||||
|
);
|
||||||
return Ok((dek, 1));
|
return Ok((dek, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 无现有 DEK → 生成新的
|
// 无现有 DEK → 生成新的
|
||||||
let dek = Self::generate_dek();
|
let dek = Self::generate_dek();
|
||||||
self.evict_if_full();
|
self.evict_if_full();
|
||||||
self.cache.insert(tenant_id, CachedDek {
|
self.cache.insert(
|
||||||
dek,
|
tenant_id,
|
||||||
version: 1,
|
CachedDek {
|
||||||
loaded_at: Instant::now(),
|
dek,
|
||||||
});
|
version: 1,
|
||||||
|
loaded_at: Instant::now(),
|
||||||
|
},
|
||||||
|
);
|
||||||
Ok((dek, 1))
|
Ok((dek, 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 使用 KEK 加密 DEK 以便存储
|
/// 使用 KEK 加密 DEK 以便存储
|
||||||
pub fn encrypt_dek_for_storage(dek: &[u8; 32], kek: &[u8; 32]) -> AppResult<String> {
|
pub fn encrypt_dek_for_storage(dek: &[u8; 32], kek: &[u8; 32]) -> AppResult<String> {
|
||||||
let dek_hex = hex::encode(dek);
|
let dek_hex = hex::encode(dek);
|
||||||
engine::encrypt(kek, &dek_hex).map_err(|e| AppError::Internal(e))
|
engine::encrypt(kek, &dek_hex).map_err(AppError::Internal)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 生成新 DEK 并用 KEK 加密,返回 (新 DEK, 加密后的 DEK)
|
/// 生成新 DEK 并用 KEK 加密,返回 (新 DEK, 加密后的 DEK)
|
||||||
@@ -110,7 +116,8 @@ impl DekManager {
|
|||||||
|
|
||||||
fn evict_if_full(&self) {
|
fn evict_if_full(&self) {
|
||||||
if self.cache.len() >= self.max_entries {
|
if self.cache.len() >= self.max_entries {
|
||||||
let to_remove: Vec<Uuid> = self.cache
|
let to_remove: Vec<Uuid> = self
|
||||||
|
.cache
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|e| e.loaded_at.elapsed().as_secs() > self.ttl_secs / 2)
|
.filter(|e| e.loaded_at.elapsed().as_secs() > self.ttl_secs / 2)
|
||||||
.map(|e| *e.key())
|
.map(|e| *e.key())
|
||||||
@@ -156,7 +163,9 @@ mod tests {
|
|||||||
let (original_dek, encrypted) = DekManager::generate_new_dek(&kek).unwrap();
|
let (original_dek, encrypted) = DekManager::generate_new_dek(&kek).unwrap();
|
||||||
let mgr = DekManager::new(300, 100);
|
let mgr = DekManager::new(300, 100);
|
||||||
let tenant_id = test_uuid(1);
|
let tenant_id = test_uuid(1);
|
||||||
let (recovered_dek, _ver) = mgr.get_or_create_dek(tenant_id, Some(&encrypted), &kek).unwrap();
|
let (recovered_dek, _ver) = mgr
|
||||||
|
.get_or_create_dek(tenant_id, Some(&encrypted), &kek)
|
||||||
|
.unwrap();
|
||||||
assert_eq!(original_dek, recovered_dek);
|
assert_eq!(original_dek, recovered_dek);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,7 +197,10 @@ mod tests {
|
|||||||
let (_, encrypted) = DekManager::generate_new_dek(&kek1).unwrap();
|
let (_, encrypted) = DekManager::generate_new_dek(&kek1).unwrap();
|
||||||
let mgr = DekManager::new(300, 100);
|
let mgr = DekManager::new(300, 100);
|
||||||
let tenant_id = test_uuid(4);
|
let tenant_id = test_uuid(4);
|
||||||
assert!(mgr.get_or_create_dek(tenant_id, Some(&encrypted), &kek2).is_err());
|
assert!(
|
||||||
|
mgr.get_or_create_dek(tenant_id, Some(&encrypted), &kek2)
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -204,7 +216,9 @@ mod tests {
|
|||||||
fn max_entries_eviction() {
|
fn max_entries_eviction() {
|
||||||
let mgr = DekManager::new(300, 3);
|
let mgr = DekManager::new(300, 3);
|
||||||
for i in 0..5u8 {
|
for i in 0..5u8 {
|
||||||
let _ = mgr.get_or_create_dek(test_uuid(i), None, &test_kek()).unwrap();
|
let _ = mgr
|
||||||
|
.get_or_create_dek(test_uuid(i), None, &test_kek())
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
assert!(mgr.cache.len() <= 6);
|
assert!(mgr.cache.len() <= 6);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,10 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn mask_phone_normal() {
|
fn mask_phone_normal() {
|
||||||
assert_eq!(Some("138****5678".to_string()), mask_phone(Some("13812345678")));
|
assert_eq!(
|
||||||
|
Some("138****5678".to_string()),
|
||||||
|
mask_phone(Some("13812345678"))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -87,7 +90,10 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn mask_phone_unicode_safe() {
|
fn mask_phone_unicode_safe() {
|
||||||
assert_eq!(Some("你好世****cdef".to_string()), mask_phone(Some("你好世界abcdef")));
|
assert_eq!(
|
||||||
|
Some("你好世****cdef".to_string()),
|
||||||
|
mask_phone(Some("你好世界abcdef"))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ pub mod masking;
|
|||||||
|
|
||||||
pub use engine::{decrypt, encrypt};
|
pub use engine::{decrypt, encrypt};
|
||||||
pub use hmac_index::hmac_hash;
|
pub use hmac_index::hmac_hash;
|
||||||
pub use masking::{mask_id_number, mask_license_number, mask_phone};
|
|
||||||
pub use key_manager::DekManager;
|
pub use key_manager::DekManager;
|
||||||
|
pub use masking::{mask_id_number, mask_license_number, mask_phone};
|
||||||
|
|
||||||
use crate::error::{AppError, AppResult};
|
use crate::error::{AppError, AppResult};
|
||||||
|
|
||||||
@@ -21,10 +21,12 @@ pub struct PiiCrypto {
|
|||||||
impl PiiCrypto {
|
impl PiiCrypto {
|
||||||
/// 从 hex 编码的 KEK 创建。KEK 为 64 字符 hex(32 字节)。
|
/// 从 hex 编码的 KEK 创建。KEK 为 64 字符 hex(32 字节)。
|
||||||
pub fn from_kek_hex(kek_hex: &str) -> AppResult<Self> {
|
pub fn from_kek_hex(kek_hex: &str) -> AppResult<Self> {
|
||||||
let bytes =
|
let bytes = hex::decode(kek_hex)
|
||||||
hex::decode(kek_hex).map_err(|e| AppError::Internal(format!("KEK hex decode failed: {}", e)))?;
|
.map_err(|e| AppError::Internal(format!("KEK hex decode failed: {}", e)))?;
|
||||||
if bytes.len() != 32 {
|
if bytes.len() != 32 {
|
||||||
return Err(AppError::Internal("KEK must be 32 bytes (64 hex chars)".into()));
|
return Err(AppError::Internal(
|
||||||
|
"KEK must be 32 bytes (64 hex chars)".into(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
let mut kek = [0u8; 32];
|
let mut kek = [0u8; 32];
|
||||||
kek.copy_from_slice(&bytes);
|
kek.copy_from_slice(&bytes);
|
||||||
@@ -44,7 +46,7 @@ impl PiiCrypto {
|
|||||||
use sha2::Digest;
|
use sha2::Digest;
|
||||||
let hmac_key = <sha2::Sha256 as Digest>::new()
|
let hmac_key = <sha2::Sha256 as Digest>::new()
|
||||||
.chain_update(b"pii-hmac-index-v1")
|
.chain_update(b"pii-hmac-index-v1")
|
||||||
.chain_update(&kek)
|
.chain_update(kek)
|
||||||
.finalize();
|
.finalize();
|
||||||
let mut hk = [0u8; 32];
|
let mut hk = [0u8; 32];
|
||||||
hk.copy_from_slice(&hmac_key);
|
hk.copy_from_slice(&hmac_key);
|
||||||
@@ -172,7 +174,9 @@ mod tests {
|
|||||||
let crypto = test_crypto();
|
let crypto = test_crypto();
|
||||||
let encrypted = encrypt(crypto.kek(), "test").unwrap();
|
let encrypted = encrypt(crypto.kek(), "test").unwrap();
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
let bytes = base64::engine::general_purpose::STANDARD.decode(&encrypted).unwrap();
|
let bytes = base64::engine::general_purpose::STANDARD
|
||||||
|
.decode(&encrypted)
|
||||||
|
.unwrap();
|
||||||
assert_eq!(bytes[0], 0x01, "密文首字节应为版本号 0x01");
|
assert_eq!(bytes[0], 0x01, "密文首字节应为版本号 0x01");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,11 +193,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
let elapsed = start.elapsed();
|
let elapsed = start.elapsed();
|
||||||
let avg_us = elapsed.as_micros() / 1000;
|
let avg_us = elapsed.as_micros() / 1000;
|
||||||
assert!(
|
assert!(avg_us < 50, "encrypt 平均耗时应 < 50μs, 实际: {}μs", avg_us);
|
||||||
avg_us < 50,
|
|
||||||
"encrypt 平均耗时应 < 50μs, 实际: {}μs",
|
|
||||||
avg_us
|
|
||||||
);
|
|
||||||
eprintln!("encrypt 1000 次: {:?} (avg {}μs)", elapsed, avg_us);
|
eprintln!("encrypt 1000 次: {:?} (avg {}μs)", elapsed, avg_us);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,11 +208,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
let elapsed = start.elapsed();
|
let elapsed = start.elapsed();
|
||||||
let avg_us = elapsed.as_micros() / 1000;
|
let avg_us = elapsed.as_micros() / 1000;
|
||||||
assert!(
|
assert!(avg_us < 50, "decrypt 平均耗时应 < 50μs, 实际: {}μs", avg_us);
|
||||||
avg_us < 50,
|
|
||||||
"decrypt 平均耗时应 < 50μs, 实际: {}μs",
|
|
||||||
avg_us
|
|
||||||
);
|
|
||||||
eprintln!("decrypt 1000 次: {:?} (avg {}μs)", elapsed, avg_us);
|
eprintln!("decrypt 1000 次: {:?} (avg {}μs)", elapsed, avg_us);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
use axum::Json;
|
||||||
use axum::http::StatusCode;
|
use axum::http::StatusCode;
|
||||||
use axum::response::{IntoResponse, Response};
|
use axum::response::{IntoResponse, Response};
|
||||||
use axum::Json;
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
/// 统一错误响应格式
|
/// 统一错误响应格式
|
||||||
|
|||||||
@@ -44,11 +44,11 @@ pub fn build_event_payload(data: serde_json::Value) -> serde_json::Value {
|
|||||||
"schema_version": EVENT_SCHEMA_VERSION,
|
"schema_version": EVENT_SCHEMA_VERSION,
|
||||||
"occurred_at": Utc::now().to_rfc3339(),
|
"occurred_at": Utc::now().to_rfc3339(),
|
||||||
});
|
});
|
||||||
if let serde_json::Value::Object(ref mut map) = envelope {
|
if let serde_json::Value::Object(ref mut map) = envelope
|
||||||
if let serde_json::Value::Object(data_map) = data {
|
&& let serde_json::Value::Object(data_map) = data
|
||||||
for (k, v) in data_map {
|
{
|
||||||
map.insert(k, v);
|
for (k, v) in data_map {
|
||||||
}
|
map.insert(k, v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
envelope
|
envelope
|
||||||
@@ -314,10 +314,10 @@ impl EventBus {
|
|||||||
event = broadcast_rx.recv() => {
|
event = broadcast_rx.recv() => {
|
||||||
match event {
|
match event {
|
||||||
Ok(event) => {
|
Ok(event) => {
|
||||||
if event.event_type.starts_with(&prefix) {
|
if event.event_type.starts_with(&prefix)
|
||||||
if mpsc_tx.send(event).await.is_err() {
|
&& mpsc_tx.send(event).await.is_err()
|
||||||
break;
|
{
|
||||||
}
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(broadcast::error::RecvError::Lagged(n)) => {
|
Err(broadcast::error::RecvError::Lagged(n)) => {
|
||||||
|
|||||||
@@ -9,11 +9,7 @@ use crate::error::AppResult;
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait HealthDataProvider: Send + Sync {
|
pub trait HealthDataProvider: Send + Sync {
|
||||||
/// 获取化验报告(指标列表)
|
/// 获取化验报告(指标列表)
|
||||||
async fn get_lab_report(
|
async fn get_lab_report(&self, tenant_id: Uuid, report_id: Uuid) -> AppResult<LabReportDto>;
|
||||||
&self,
|
|
||||||
tenant_id: Uuid,
|
|
||||||
report_id: Uuid,
|
|
||||||
) -> AppResult<LabReportDto>;
|
|
||||||
|
|
||||||
/// 获取生命体征趋势数据
|
/// 获取生命体征趋势数据
|
||||||
async fn get_vital_signs(
|
async fn get_vital_signs(
|
||||||
@@ -32,11 +28,8 @@ pub trait HealthDataProvider: Send + Sync {
|
|||||||
) -> AppResult<PatientSummaryDto>;
|
) -> AppResult<PatientSummaryDto>;
|
||||||
|
|
||||||
/// 获取完整健康报告(用于摘要生成)
|
/// 获取完整健康报告(用于摘要生成)
|
||||||
async fn get_full_report(
|
async fn get_full_report(&self, tenant_id: Uuid, report_id: Uuid)
|
||||||
&self,
|
-> AppResult<HealthReportDto>;
|
||||||
tenant_id: Uuid,
|
|
||||||
report_id: Uuid,
|
|
||||||
) -> AppResult<HealthReportDto>;
|
|
||||||
|
|
||||||
/// 获取趋势分析预计算数据(统计摘要 + 异常检测)
|
/// 获取趋势分析预计算数据(统计摘要 + 异常检测)
|
||||||
async fn get_trend_analysis_data(
|
async fn get_trend_analysis_data(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
///
|
///
|
||||||
/// 基于 ammonia(html5ever)剥离所有 HTML 标签,防止存储型 XSS。
|
/// 基于 ammonia(html5ever)剥离所有 HTML 标签,防止存储型 XSS。
|
||||||
/// 覆盖场景:用户名、显示名、邮箱、电话等字符串字段。
|
/// 覆盖场景:用户名、显示名、邮箱、电话等字符串字段。
|
||||||
|
///
|
||||||
/// 剥离字符串中的所有 HTML 标签,返回纯文本。
|
/// 剥离字符串中的所有 HTML 标签,返回纯文本。
|
||||||
///
|
///
|
||||||
/// 使用 ammonia 构建 DOM 树,然后用 tendril 收集文本节点。
|
/// 使用 ammonia 构建 DOM 树,然后用 tendril 收集文本节点。
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
//! 每个测试在独立事务中执行,测试结束自动回滚,无数据残留。
|
//! 每个测试在独立事务中执行,测试结束自动回滚,无数据残留。
|
||||||
//! 多个测试共享同一个数据库连接池,无连接竞争。
|
//! 多个测试共享同一个数据库连接池,无连接竞争。
|
||||||
|
|
||||||
use sea_orm::{ConnectOptions, Database, DatabaseConnection, DatabaseTransaction, TransactionTrait};
|
use sea_orm::{
|
||||||
|
ConnectOptions, Database, DatabaseConnection, DatabaseTransaction, TransactionTrait,
|
||||||
|
};
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
use tokio::sync::OnceCell;
|
use tokio::sync::OnceCell;
|
||||||
|
|
||||||
@@ -22,12 +24,8 @@ fn db_url() -> String {
|
|||||||
async fn db_pool() -> &'static DatabaseConnection {
|
async fn db_pool() -> &'static DatabaseConnection {
|
||||||
DB_POOL
|
DB_POOL
|
||||||
.get_or_init(|| async {
|
.get_or_init(|| async {
|
||||||
let opt = ConnectOptions::new(db_url())
|
let opt = ConnectOptions::new(db_url()).max_connections(5).to_owned();
|
||||||
.max_connections(5)
|
Database::connect(opt).await.expect("测试数据库连接失败")
|
||||||
.to_owned();
|
|
||||||
Database::connect(opt)
|
|
||||||
.await
|
|
||||||
.expect("测试数据库连接失败")
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -35,7 +33,5 @@ async fn db_pool() -> &'static DatabaseConnection {
|
|||||||
/// 创建测试用事务。测试结束自动回滚,无数据残留。
|
/// 创建测试用事务。测试结束自动回滚,无数据残留。
|
||||||
pub async fn test_txn() -> DatabaseTransaction {
|
pub async fn test_txn() -> DatabaseTransaction {
|
||||||
let pool = db_pool().await;
|
let pool = db_pool().await;
|
||||||
pool.begin()
|
pool.begin().await.expect("测试事务创建失败")
|
||||||
.await
|
|
||||||
.expect("测试事务创建失败")
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ pub enum DataScope {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DataScope {
|
impl DataScope {
|
||||||
pub fn from_str(s: &str) -> Self {
|
pub fn parse_scope(s: &str) -> Self {
|
||||||
match s {
|
match s {
|
||||||
"self" => Self::SelfOnly,
|
"self" => Self::SelfOnly,
|
||||||
"department" => Self::Department,
|
"department" => Self::Department,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
/// 预留事件处理器注册
|
/// 预留事件处理器注册
|
||||||
pub fn register_handlers_with_state(_state: crate::state::DialysisState) {
|
pub fn register_handlers_with_state(_state: crate::state::DialysisState) {
|
||||||
// 透析业务事件由 erp-health 统一消费(见 erp-health/src/event.rs:425 dialysis_notifier)
|
// 透析业务事件由 erp-health 统一消费(见 erp-health/src/event.rs:425 dialysis_notifier)
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ use erp_core::error::AppError;
|
|||||||
use erp_core::rbac::require_permission;
|
use erp_core::rbac::require_permission;
|
||||||
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
||||||
|
|
||||||
use crate::dto::dialysis_dto::*;
|
|
||||||
use crate::dto::DeleteWithVersion;
|
use crate::dto::DeleteWithVersion;
|
||||||
|
use crate::dto::dialysis_dto::*;
|
||||||
use crate::service::dialysis_service;
|
use crate::service::dialysis_service;
|
||||||
use crate::state::DialysisState;
|
use crate::state::DialysisState;
|
||||||
|
|
||||||
@@ -44,10 +44,9 @@ where
|
|||||||
require_permission(&ctx, "health.dialysis.list")?;
|
require_permission(&ctx, "health.dialysis.list")?;
|
||||||
let page = params.page.unwrap_or(1);
|
let page = params.page.unwrap_or(1);
|
||||||
let page_size = params.page_size.unwrap_or(20);
|
let page_size = params.page_size.unwrap_or(20);
|
||||||
let result = dialysis_service::list_dialysis_records(
|
let result =
|
||||||
&state, ctx.tenant_id, patient_id, page, page_size,
|
dialysis_service::list_dialysis_records(&state, ctx.tenant_id, patient_id, page, page_size)
|
||||||
)
|
.await?;
|
||||||
.await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,10 +60,7 @@ where
|
|||||||
S: Clone + Send + Sync + 'static,
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.dialysis.list")?;
|
require_permission(&ctx, "health.dialysis.list")?;
|
||||||
let result = dialysis_service::get_dialysis_record(
|
let result = dialysis_service::get_dialysis_record(&state, ctx.tenant_id, record_id).await?;
|
||||||
&state, ctx.tenant_id, record_id,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,10 +76,9 @@ where
|
|||||||
require_permission(&ctx, "health.dialysis.manage")?;
|
require_permission(&ctx, "health.dialysis.manage")?;
|
||||||
let mut req = req;
|
let mut req = req;
|
||||||
req.sanitize();
|
req.sanitize();
|
||||||
let result = dialysis_service::create_dialysis_record(
|
let result =
|
||||||
&state, ctx.tenant_id, Some(ctx.user_id), req,
|
dialysis_service::create_dialysis_record(&state, ctx.tenant_id, Some(ctx.user_id), req)
|
||||||
)
|
.await?;
|
||||||
.await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +96,12 @@ where
|
|||||||
let mut data = req.data;
|
let mut data = req.data;
|
||||||
data.sanitize();
|
data.sanitize();
|
||||||
let result = dialysis_service::update_dialysis_record(
|
let result = dialysis_service::update_dialysis_record(
|
||||||
&state, ctx.tenant_id, record_id, Some(ctx.user_id), data, req.version,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
record_id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
data,
|
||||||
|
req.version,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
@@ -119,7 +119,11 @@ where
|
|||||||
{
|
{
|
||||||
require_permission(&ctx, "health.dialysis.manage")?;
|
require_permission(&ctx, "health.dialysis.manage")?;
|
||||||
let result = dialysis_service::review_dialysis_record(
|
let result = dialysis_service::review_dialysis_record(
|
||||||
&state, ctx.tenant_id, record_id, ctx.user_id, req.version,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
record_id,
|
||||||
|
ctx.user_id,
|
||||||
|
req.version,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
@@ -137,7 +141,11 @@ where
|
|||||||
{
|
{
|
||||||
require_permission(&ctx, "health.dialysis.manage")?;
|
require_permission(&ctx, "health.dialysis.manage")?;
|
||||||
let result = dialysis_service::complete_dialysis_record(
|
let result = dialysis_service::complete_dialysis_record(
|
||||||
&state, ctx.tenant_id, record_id, Some(ctx.user_id), req.version,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
record_id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
req.version,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
@@ -155,7 +163,11 @@ where
|
|||||||
{
|
{
|
||||||
require_permission(&ctx, "health.dialysis.manage")?;
|
require_permission(&ctx, "health.dialysis.manage")?;
|
||||||
dialysis_service::delete_dialysis_record(
|
dialysis_service::delete_dialysis_record(
|
||||||
&state, ctx.tenant_id, record_id, Some(ctx.user_id), req.version,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
record_id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
req.version,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(())))
|
Ok(Json(ApiResponse::ok(())))
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ use erp_core::error::AppError;
|
|||||||
use erp_core::rbac::require_permission;
|
use erp_core::rbac::require_permission;
|
||||||
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
||||||
|
|
||||||
use crate::dto::dialysis_prescription_dto::*;
|
|
||||||
use crate::dto::DeleteWithVersion;
|
use crate::dto::DeleteWithVersion;
|
||||||
|
use crate::dto::dialysis_prescription_dto::*;
|
||||||
use crate::service::dialysis_prescription_service;
|
use crate::service::dialysis_prescription_service;
|
||||||
use crate::state::DialysisState;
|
use crate::state::DialysisState;
|
||||||
|
|
||||||
@@ -41,7 +41,12 @@ where
|
|||||||
let page = params.page.unwrap_or(1);
|
let page = params.page.unwrap_or(1);
|
||||||
let page_size = params.page_size.unwrap_or(20);
|
let page_size = params.page_size.unwrap_or(20);
|
||||||
let result = dialysis_prescription_service::list_prescriptions(
|
let result = dialysis_prescription_service::list_prescriptions(
|
||||||
&state, ctx.tenant_id, page, page_size, params.patient_id, params.status,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
page,
|
||||||
|
page_size,
|
||||||
|
params.patient_id,
|
||||||
|
params.status,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
@@ -74,7 +79,10 @@ where
|
|||||||
let mut req = req;
|
let mut req = req;
|
||||||
req.sanitize();
|
req.sanitize();
|
||||||
let result = dialysis_prescription_service::create_prescription(
|
let result = dialysis_prescription_service::create_prescription(
|
||||||
&state, ctx.tenant_id, Some(ctx.user_id), req,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
req,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
@@ -94,7 +102,12 @@ where
|
|||||||
let mut data = req.data;
|
let mut data = req.data;
|
||||||
data.sanitize();
|
data.sanitize();
|
||||||
let result = dialysis_prescription_service::update_prescription(
|
let result = dialysis_prescription_service::update_prescription(
|
||||||
&state, ctx.tenant_id, id, Some(ctx.user_id), data, req.version,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
data,
|
||||||
|
req.version,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
@@ -112,7 +125,11 @@ where
|
|||||||
{
|
{
|
||||||
require_permission(&ctx, "health.dialysis-prescription.manage")?;
|
require_permission(&ctx, "health.dialysis-prescription.manage")?;
|
||||||
dialysis_prescription_service::delete_prescription(
|
dialysis_prescription_service::delete_prescription(
|
||||||
&state, ctx.tenant_id, id, Some(ctx.user_id), req.version,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
req.version,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(())))
|
Ok(Json(ApiResponse::ok(())))
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use axum::extract::{Extension, FromRef, State};
|
|
||||||
use axum::Json;
|
use axum::Json;
|
||||||
|
use axum::extract::{Extension, FromRef, State};
|
||||||
use erp_core::error::AppError;
|
use erp_core::error::AppError;
|
||||||
use erp_core::rbac::require_permission;
|
use erp_core::rbac::require_permission;
|
||||||
use erp_core::types::{ApiResponse, TenantContext};
|
use erp_core::types::{ApiResponse, TenantContext};
|
||||||
@@ -18,6 +18,7 @@ where
|
|||||||
{
|
{
|
||||||
require_permission(&ctx, "health.dialysis.stats")?;
|
require_permission(&ctx, "health.dialysis.stats")?;
|
||||||
let dialysis_state = DialysisState::from_ref(&state);
|
let dialysis_state = DialysisState::from_ref(&state);
|
||||||
let stats = dialysis_stats_service::get_dialysis_statistics(&dialysis_state, ctx.tenant_id).await?;
|
let stats =
|
||||||
|
dialysis_stats_service::get_dialysis_statistics(&dialysis_state, ctx.tenant_id).await?;
|
||||||
Ok(Json(ApiResponse::ok(stats)))
|
Ok(Json(ApiResponse::ok(stats)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,13 @@ pub async fn list_prescriptions(
|
|||||||
let total_pages = total.div_ceil(limit.max(1));
|
let total_pages = total.div_ceil(limit.max(1));
|
||||||
let data = models.into_iter().map(model_to_resp).collect();
|
let data = models.into_iter().map(model_to_resp).collect();
|
||||||
|
|
||||||
Ok(PaginatedResponse { data, total, page, page_size: limit, total_pages })
|
Ok(PaginatedResponse {
|
||||||
|
data,
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
page_size: limit,
|
||||||
|
total_pages,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_prescription(
|
pub async fn get_prescription(
|
||||||
@@ -85,14 +91,24 @@ pub async fn create_prescription(
|
|||||||
tenant_id: Set(tenant_id),
|
tenant_id: Set(tenant_id),
|
||||||
patient_id: Set(req.patient_id),
|
patient_id: Set(req.patient_id),
|
||||||
dialyzer_model: Set(req.dialyzer_model),
|
dialyzer_model: Set(req.dialyzer_model),
|
||||||
membrane_area: Set(req.membrane_area.map(|v| Decimal::from_f64_retain(v).unwrap_or_default())),
|
membrane_area: Set(req
|
||||||
dialysate_potassium: Set(req.dialysate_potassium.map(|v| Decimal::from_f64_retain(v).unwrap_or_default())),
|
.membrane_area
|
||||||
dialysate_calcium: Set(req.dialysate_calcium.map(|v| Decimal::from_f64_retain(v).unwrap_or_default())),
|
.map(|v| Decimal::from_f64_retain(v).unwrap_or_default())),
|
||||||
dialysate_bicarbonate: Set(req.dialysate_bicarbonate.map(|v| Decimal::from_f64_retain(v).unwrap_or_default())),
|
dialysate_potassium: Set(req
|
||||||
|
.dialysate_potassium
|
||||||
|
.map(|v| Decimal::from_f64_retain(v).unwrap_or_default())),
|
||||||
|
dialysate_calcium: Set(req
|
||||||
|
.dialysate_calcium
|
||||||
|
.map(|v| Decimal::from_f64_retain(v).unwrap_or_default())),
|
||||||
|
dialysate_bicarbonate: Set(req
|
||||||
|
.dialysate_bicarbonate
|
||||||
|
.map(|v| Decimal::from_f64_retain(v).unwrap_or_default())),
|
||||||
anticoagulation_type: Set(req.anticoagulation_type),
|
anticoagulation_type: Set(req.anticoagulation_type),
|
||||||
anticoagulation_dose: Set(req.anticoagulation_dose),
|
anticoagulation_dose: Set(req.anticoagulation_dose),
|
||||||
target_ultrafiltration_ml: Set(req.target_ultrafiltration_ml),
|
target_ultrafiltration_ml: Set(req.target_ultrafiltration_ml),
|
||||||
target_dry_weight: Set(req.target_dry_weight.map(|v| Decimal::from_f64_retain(v).unwrap_or_default())),
|
target_dry_weight: Set(req
|
||||||
|
.target_dry_weight
|
||||||
|
.map(|v| Decimal::from_f64_retain(v).unwrap_or_default())),
|
||||||
blood_flow_rate: Set(req.blood_flow_rate),
|
blood_flow_rate: Set(req.blood_flow_rate),
|
||||||
dialysate_flow_rate: Set(req.dialysate_flow_rate),
|
dialysate_flow_rate: Set(req.dialysate_flow_rate),
|
||||||
frequency_per_week: Set(req.frequency_per_week),
|
frequency_per_week: Set(req.frequency_per_week),
|
||||||
@@ -114,10 +130,16 @@ pub async fn create_prescription(
|
|||||||
let m = active.insert(&state.db).await?;
|
let m = active.insert(&state.db).await?;
|
||||||
|
|
||||||
audit_service::record(
|
audit_service::record(
|
||||||
AuditLog::new(tenant_id, operator_id, "dialysis_prescription.created", "dialysis_prescription")
|
AuditLog::new(
|
||||||
.with_resource_id(m.id),
|
tenant_id,
|
||||||
|
operator_id,
|
||||||
|
"dialysis_prescription.created",
|
||||||
|
"dialysis_prescription",
|
||||||
|
)
|
||||||
|
.with_resource_id(m.id),
|
||||||
&state.db,
|
&state.db,
|
||||||
).await;
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
Ok(model_to_resp(m))
|
Ok(model_to_resp(m))
|
||||||
}
|
}
|
||||||
@@ -141,29 +163,71 @@ pub async fn update_prescription(
|
|||||||
let next_ver = check_version(expected_version, model.version)
|
let next_ver = check_version(expected_version, model.version)
|
||||||
.map_err(|_| DialysisError::VersionMismatch)?;
|
.map_err(|_| DialysisError::VersionMismatch)?;
|
||||||
|
|
||||||
if let Some(ref t) = req.anticoagulation_type { validate_anticoagulation_type(Some(t))?; }
|
if let Some(ref t) = req.anticoagulation_type {
|
||||||
if let Some(ref t) = req.vascular_access_type { validate_vascular_access_type(Some(t))?; }
|
validate_anticoagulation_type(Some(t))?;
|
||||||
|
}
|
||||||
|
if let Some(ref t) = req.vascular_access_type {
|
||||||
|
validate_vascular_access_type(Some(t))?;
|
||||||
|
}
|
||||||
|
|
||||||
let mut active: dialysis_prescription::ActiveModel = model.into();
|
let mut active: dialysis_prescription::ActiveModel = model.into();
|
||||||
if let Some(v) = req.dialyzer_model { active.dialyzer_model = Set(Some(v)); }
|
if let Some(v) = req.dialyzer_model {
|
||||||
if let Some(v) = req.membrane_area { active.membrane_area = Set(Some(Decimal::from_f64_retain(v).unwrap_or_default())); }
|
active.dialyzer_model = Set(Some(v));
|
||||||
if let Some(v) = req.dialysate_potassium { active.dialysate_potassium = Set(Some(Decimal::from_f64_retain(v).unwrap_or_default())); }
|
}
|
||||||
if let Some(v) = req.dialysate_calcium { active.dialysate_calcium = Set(Some(Decimal::from_f64_retain(v).unwrap_or_default())); }
|
if let Some(v) = req.membrane_area {
|
||||||
if let Some(v) = req.dialysate_bicarbonate { active.dialysate_bicarbonate = Set(Some(Decimal::from_f64_retain(v).unwrap_or_default())); }
|
active.membrane_area = Set(Some(Decimal::from_f64_retain(v).unwrap_or_default()));
|
||||||
if let Some(v) = req.anticoagulation_type { active.anticoagulation_type = Set(Some(v)); }
|
}
|
||||||
if let Some(v) = req.anticoagulation_dose { active.anticoagulation_dose = Set(Some(v)); }
|
if let Some(v) = req.dialysate_potassium {
|
||||||
if let Some(v) = req.target_ultrafiltration_ml { active.target_ultrafiltration_ml = Set(Some(v)); }
|
active.dialysate_potassium = Set(Some(Decimal::from_f64_retain(v).unwrap_or_default()));
|
||||||
if let Some(v) = req.target_dry_weight { active.target_dry_weight = Set(Some(Decimal::from_f64_retain(v).unwrap_or_default())); }
|
}
|
||||||
if let Some(v) = req.blood_flow_rate { active.blood_flow_rate = Set(Some(v)); }
|
if let Some(v) = req.dialysate_calcium {
|
||||||
if let Some(v) = req.dialysate_flow_rate { active.dialysate_flow_rate = Set(Some(v)); }
|
active.dialysate_calcium = Set(Some(Decimal::from_f64_retain(v).unwrap_or_default()));
|
||||||
if let Some(v) = req.frequency_per_week { active.frequency_per_week = Set(Some(v)); }
|
}
|
||||||
if let Some(v) = req.duration_minutes { active.duration_minutes = Set(Some(v)); }
|
if let Some(v) = req.dialysate_bicarbonate {
|
||||||
if let Some(v) = req.vascular_access_type { active.vascular_access_type = Set(Some(v)); }
|
active.dialysate_bicarbonate = Set(Some(Decimal::from_f64_retain(v).unwrap_or_default()));
|
||||||
if let Some(v) = req.vascular_access_location { active.vascular_access_location = Set(Some(v)); }
|
}
|
||||||
if let Some(v) = req.effective_from { active.effective_from = Set(Some(v)); }
|
if let Some(v) = req.anticoagulation_type {
|
||||||
if let Some(v) = req.effective_to { active.effective_to = Set(Some(v)); }
|
active.anticoagulation_type = Set(Some(v));
|
||||||
if let Some(v) = req.status { active.status = Set(v); }
|
}
|
||||||
if let Some(v) = req.notes { active.notes = Set(Some(v)); }
|
if let Some(v) = req.anticoagulation_dose {
|
||||||
|
active.anticoagulation_dose = Set(Some(v));
|
||||||
|
}
|
||||||
|
if let Some(v) = req.target_ultrafiltration_ml {
|
||||||
|
active.target_ultrafiltration_ml = Set(Some(v));
|
||||||
|
}
|
||||||
|
if let Some(v) = req.target_dry_weight {
|
||||||
|
active.target_dry_weight = Set(Some(Decimal::from_f64_retain(v).unwrap_or_default()));
|
||||||
|
}
|
||||||
|
if let Some(v) = req.blood_flow_rate {
|
||||||
|
active.blood_flow_rate = Set(Some(v));
|
||||||
|
}
|
||||||
|
if let Some(v) = req.dialysate_flow_rate {
|
||||||
|
active.dialysate_flow_rate = Set(Some(v));
|
||||||
|
}
|
||||||
|
if let Some(v) = req.frequency_per_week {
|
||||||
|
active.frequency_per_week = Set(Some(v));
|
||||||
|
}
|
||||||
|
if let Some(v) = req.duration_minutes {
|
||||||
|
active.duration_minutes = Set(Some(v));
|
||||||
|
}
|
||||||
|
if let Some(v) = req.vascular_access_type {
|
||||||
|
active.vascular_access_type = Set(Some(v));
|
||||||
|
}
|
||||||
|
if let Some(v) = req.vascular_access_location {
|
||||||
|
active.vascular_access_location = Set(Some(v));
|
||||||
|
}
|
||||||
|
if let Some(v) = req.effective_from {
|
||||||
|
active.effective_from = Set(Some(v));
|
||||||
|
}
|
||||||
|
if let Some(v) = req.effective_to {
|
||||||
|
active.effective_to = Set(Some(v));
|
||||||
|
}
|
||||||
|
if let Some(v) = req.status {
|
||||||
|
active.status = Set(v);
|
||||||
|
}
|
||||||
|
if let Some(v) = req.notes {
|
||||||
|
active.notes = Set(Some(v));
|
||||||
|
}
|
||||||
active.updated_at = Set(Utc::now());
|
active.updated_at = Set(Utc::now());
|
||||||
active.updated_by = Set(operator_id);
|
active.updated_by = Set(operator_id);
|
||||||
active.version = Set(next_ver);
|
active.version = Set(next_ver);
|
||||||
@@ -171,10 +235,16 @@ pub async fn update_prescription(
|
|||||||
let m = active.update(&state.db).await?;
|
let m = active.update(&state.db).await?;
|
||||||
|
|
||||||
audit_service::record(
|
audit_service::record(
|
||||||
AuditLog::new(tenant_id, operator_id, "dialysis_prescription.updated", "dialysis_prescription")
|
AuditLog::new(
|
||||||
.with_resource_id(m.id),
|
tenant_id,
|
||||||
|
operator_id,
|
||||||
|
"dialysis_prescription.updated",
|
||||||
|
"dialysis_prescription",
|
||||||
|
)
|
||||||
|
.with_resource_id(m.id),
|
||||||
&state.db,
|
&state.db,
|
||||||
).await;
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
Ok(model_to_resp(m))
|
Ok(model_to_resp(m))
|
||||||
}
|
}
|
||||||
@@ -205,10 +275,16 @@ pub async fn delete_prescription(
|
|||||||
active.update(&state.db).await?;
|
active.update(&state.db).await?;
|
||||||
|
|
||||||
audit_service::record(
|
audit_service::record(
|
||||||
AuditLog::new(tenant_id, operator_id, "dialysis_prescription.deleted", "dialysis_prescription")
|
AuditLog::new(
|
||||||
.with_resource_id(id),
|
tenant_id,
|
||||||
|
operator_id,
|
||||||
|
"dialysis_prescription.deleted",
|
||||||
|
"dialysis_prescription",
|
||||||
|
)
|
||||||
|
.with_resource_id(id),
|
||||||
&state.db,
|
&state.db,
|
||||||
).await;
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -252,7 +328,8 @@ fn validate_anticoagulation_type(val: Option<&str>) -> DialysisResult<()> {
|
|||||||
let valid = ["heparin", "lmwh", "heparin_free"];
|
let valid = ["heparin", "lmwh", "heparin_free"];
|
||||||
if !valid.contains(&t) {
|
if !valid.contains(&t) {
|
||||||
return Err(DialysisError::Validation(format!(
|
return Err(DialysisError::Validation(format!(
|
||||||
"anticoagulation_type 必须为: {}", valid.join(", ")
|
"anticoagulation_type 必须为: {}",
|
||||||
|
valid.join(", ")
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -264,7 +341,8 @@ fn validate_vascular_access_type(val: Option<&str>) -> DialysisResult<()> {
|
|||||||
let valid = ["avf", "avg", "cvc"];
|
let valid = ["avf", "avg", "cvc"];
|
||||||
if !valid.contains(&t) {
|
if !valid.contains(&t) {
|
||||||
return Err(DialysisError::Validation(format!(
|
return Err(DialysisError::Validation(format!(
|
||||||
"vascular_access_type 必须为: {}", valid.join(", ")
|
"vascular_access_type 必须为: {}",
|
||||||
|
valid.join(", ")
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,13 @@ pub async fn list_dialysis_records(
|
|||||||
let crypto = &state.crypto;
|
let crypto = &state.crypto;
|
||||||
let data: Vec<DialysisRecordResp> = models.into_iter().map(|m| to_resp(crypto, m)).collect();
|
let data: Vec<DialysisRecordResp> = models.into_iter().map(|m| to_resp(crypto, m)).collect();
|
||||||
|
|
||||||
Ok(PaginatedResponse { data, total, page, page_size: limit, total_pages })
|
Ok(PaginatedResponse {
|
||||||
|
data,
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
page_size: limit,
|
||||||
|
total_pages,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_dialysis_record(
|
pub async fn get_dialysis_record(
|
||||||
@@ -92,15 +98,19 @@ pub async fn create_dialysis_record(
|
|||||||
let kek = state.crypto.kek();
|
let kek = state.crypto.kek();
|
||||||
|
|
||||||
// PII 加密
|
// PII 加密
|
||||||
let encrypted_symptoms = req.symptoms.as_ref()
|
let encrypted_symptoms = req
|
||||||
|
.symptoms
|
||||||
|
.as_ref()
|
||||||
.map(|v| -> DialysisResult<serde_json::Value> {
|
.map(|v| -> DialysisResult<serde_json::Value> {
|
||||||
let json_str = serde_json::to_string(v)
|
let json_str =
|
||||||
.map_err(|e| DialysisError::Validation(e.to_string()))?;
|
serde_json::to_string(v).map_err(|e| DialysisError::Validation(e.to_string()))?;
|
||||||
Ok(serde_json::Value::String(pii::encrypt(kek, &json_str)?))
|
Ok(serde_json::Value::String(pii::encrypt(kek, &json_str)?))
|
||||||
})
|
})
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
|
|
||||||
let encrypted_complication = req.complication_notes.as_ref()
|
let encrypted_complication = req
|
||||||
|
.complication_notes
|
||||||
|
.as_ref()
|
||||||
.map(|c| pii::encrypt(kek, c))
|
.map(|c| pii::encrypt(kek, c))
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
|
|
||||||
@@ -112,9 +122,15 @@ pub async fn create_dialysis_record(
|
|||||||
dialysis_date: Set(req.dialysis_date),
|
dialysis_date: Set(req.dialysis_date),
|
||||||
start_time: Set(req.start_time),
|
start_time: Set(req.start_time),
|
||||||
end_time: Set(req.end_time),
|
end_time: Set(req.end_time),
|
||||||
dry_weight: Set(req.dry_weight.map(|v| Decimal::from_f64_retain(v).unwrap_or_default())),
|
dry_weight: Set(req
|
||||||
pre_weight: Set(req.pre_weight.map(|v| Decimal::from_f64_retain(v).unwrap_or_default())),
|
.dry_weight
|
||||||
post_weight: Set(req.post_weight.map(|v| Decimal::from_f64_retain(v).unwrap_or_default())),
|
.map(|v| Decimal::from_f64_retain(v).unwrap_or_default())),
|
||||||
|
pre_weight: Set(req
|
||||||
|
.pre_weight
|
||||||
|
.map(|v| Decimal::from_f64_retain(v).unwrap_or_default())),
|
||||||
|
post_weight: Set(req
|
||||||
|
.post_weight
|
||||||
|
.map(|v| Decimal::from_f64_retain(v).unwrap_or_default())),
|
||||||
pre_bp_systolic: Set(req.pre_bp_systolic),
|
pre_bp_systolic: Set(req.pre_bp_systolic),
|
||||||
pre_bp_diastolic: Set(req.pre_bp_diastolic),
|
pre_bp_diastolic: Set(req.pre_bp_diastolic),
|
||||||
post_bp_systolic: Set(req.post_bp_systolic),
|
post_bp_systolic: Set(req.post_bp_systolic),
|
||||||
@@ -142,10 +158,16 @@ pub async fn create_dialysis_record(
|
|||||||
let m = active.insert(&state.db).await?;
|
let m = active.insert(&state.db).await?;
|
||||||
|
|
||||||
audit_service::record(
|
audit_service::record(
|
||||||
AuditLog::new(tenant_id, operator_id, "dialysis_record.created", "dialysis_record")
|
AuditLog::new(
|
||||||
.with_resource_id(m.id),
|
tenant_id,
|
||||||
|
operator_id,
|
||||||
|
"dialysis_record.created",
|
||||||
|
"dialysis_record",
|
||||||
|
)
|
||||||
|
.with_resource_id(m.id),
|
||||||
&state.db,
|
&state.db,
|
||||||
).await;
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
// 发布透析记录创建事件
|
// 发布透析记录创建事件
|
||||||
let event = DomainEvent::new(
|
let event = DomainEvent::new(
|
||||||
@@ -182,27 +204,61 @@ pub async fn update_dialysis_record(
|
|||||||
.map_err(|_| DialysisError::VersionMismatch)?;
|
.map_err(|_| DialysisError::VersionMismatch)?;
|
||||||
|
|
||||||
let mut active: dialysis_record::ActiveModel = model.into();
|
let mut active: dialysis_record::ActiveModel = model.into();
|
||||||
if let Some(v) = req.dialysis_date { active.dialysis_date = Set(v); }
|
if let Some(v) = req.dialysis_date {
|
||||||
if let Some(v) = req.start_time { active.start_time = Set(Some(v)); }
|
active.dialysis_date = Set(v);
|
||||||
if let Some(v) = req.end_time { active.end_time = Set(Some(v)); }
|
}
|
||||||
if let Some(v) = req.dry_weight { active.dry_weight = Set(Some(Decimal::from_f64_retain(v).unwrap_or_default())); }
|
if let Some(v) = req.start_time {
|
||||||
if let Some(v) = req.pre_weight { active.pre_weight = Set(Some(Decimal::from_f64_retain(v).unwrap_or_default())); }
|
active.start_time = Set(Some(v));
|
||||||
if let Some(v) = req.post_weight { active.post_weight = Set(Some(Decimal::from_f64_retain(v).unwrap_or_default())); }
|
}
|
||||||
if let Some(v) = req.pre_bp_systolic { active.pre_bp_systolic = Set(Some(v)); }
|
if let Some(v) = req.end_time {
|
||||||
if let Some(v) = req.pre_bp_diastolic { active.pre_bp_diastolic = Set(Some(v)); }
|
active.end_time = Set(Some(v));
|
||||||
if let Some(v) = req.post_bp_systolic { active.post_bp_systolic = Set(Some(v)); }
|
}
|
||||||
if let Some(v) = req.post_bp_diastolic { active.post_bp_diastolic = Set(Some(v)); }
|
if let Some(v) = req.dry_weight {
|
||||||
if let Some(v) = req.pre_heart_rate { active.pre_heart_rate = Set(Some(v)); }
|
active.dry_weight = Set(Some(Decimal::from_f64_retain(v).unwrap_or_default()));
|
||||||
if let Some(v) = req.post_heart_rate { active.post_heart_rate = Set(Some(v)); }
|
}
|
||||||
if let Some(v) = req.ultrafiltration_volume { active.ultrafiltration_volume = Set(Some(v)); }
|
if let Some(v) = req.pre_weight {
|
||||||
if let Some(v) = req.dialysis_duration { active.dialysis_duration = Set(Some(v)); }
|
active.pre_weight = Set(Some(Decimal::from_f64_retain(v).unwrap_or_default()));
|
||||||
if let Some(v) = req.blood_flow_rate { active.blood_flow_rate = Set(Some(v)); }
|
}
|
||||||
if let Some(ref v) = req.dialysis_type { validate_dialysis_type(v)?; active.dialysis_type = Set(v.clone()); }
|
if let Some(v) = req.post_weight {
|
||||||
|
active.post_weight = Set(Some(Decimal::from_f64_retain(v).unwrap_or_default()));
|
||||||
|
}
|
||||||
|
if let Some(v) = req.pre_bp_systolic {
|
||||||
|
active.pre_bp_systolic = Set(Some(v));
|
||||||
|
}
|
||||||
|
if let Some(v) = req.pre_bp_diastolic {
|
||||||
|
active.pre_bp_diastolic = Set(Some(v));
|
||||||
|
}
|
||||||
|
if let Some(v) = req.post_bp_systolic {
|
||||||
|
active.post_bp_systolic = Set(Some(v));
|
||||||
|
}
|
||||||
|
if let Some(v) = req.post_bp_diastolic {
|
||||||
|
active.post_bp_diastolic = Set(Some(v));
|
||||||
|
}
|
||||||
|
if let Some(v) = req.pre_heart_rate {
|
||||||
|
active.pre_heart_rate = Set(Some(v));
|
||||||
|
}
|
||||||
|
if let Some(v) = req.post_heart_rate {
|
||||||
|
active.post_heart_rate = Set(Some(v));
|
||||||
|
}
|
||||||
|
if let Some(v) = req.ultrafiltration_volume {
|
||||||
|
active.ultrafiltration_volume = Set(Some(v));
|
||||||
|
}
|
||||||
|
if let Some(v) = req.dialysis_duration {
|
||||||
|
active.dialysis_duration = Set(Some(v));
|
||||||
|
}
|
||||||
|
if let Some(v) = req.blood_flow_rate {
|
||||||
|
active.blood_flow_rate = Set(Some(v));
|
||||||
|
}
|
||||||
|
if let Some(ref v) = req.dialysis_type {
|
||||||
|
validate_dialysis_type(v)?;
|
||||||
|
active.dialysis_type = Set(v.clone());
|
||||||
|
}
|
||||||
if let Some(v) = req.symptoms {
|
if let Some(v) = req.symptoms {
|
||||||
let kek = state.crypto.kek();
|
let kek = state.crypto.kek();
|
||||||
let encrypted = Some(serde_json::Value::String(
|
let encrypted = Some(serde_json::Value::String(pii::encrypt(
|
||||||
pii::encrypt(kek, &serde_json::to_string(&v).unwrap_or_default())?
|
kek,
|
||||||
));
|
&serde_json::to_string(&v).unwrap_or_default(),
|
||||||
|
)?));
|
||||||
active.symptoms = Set(encrypted);
|
active.symptoms = Set(encrypted);
|
||||||
}
|
}
|
||||||
if let Some(v) = req.complication_notes {
|
if let Some(v) = req.complication_notes {
|
||||||
@@ -218,10 +274,16 @@ pub async fn update_dialysis_record(
|
|||||||
let m = active.update(&state.db).await?;
|
let m = active.update(&state.db).await?;
|
||||||
|
|
||||||
audit_service::record(
|
audit_service::record(
|
||||||
AuditLog::new(tenant_id, operator_id, "dialysis_record.updated", "dialysis_record")
|
AuditLog::new(
|
||||||
.with_resource_id(m.id),
|
tenant_id,
|
||||||
|
operator_id,
|
||||||
|
"dialysis_record.updated",
|
||||||
|
"dialysis_record",
|
||||||
|
)
|
||||||
|
.with_resource_id(m.id),
|
||||||
&state.db,
|
&state.db,
|
||||||
).await;
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
Ok(to_resp(&state.crypto, m))
|
Ok(to_resp(&state.crypto, m))
|
||||||
}
|
}
|
||||||
@@ -255,10 +317,16 @@ pub async fn complete_dialysis_record(
|
|||||||
let m = active.update(&state.db).await?;
|
let m = active.update(&state.db).await?;
|
||||||
|
|
||||||
audit_service::record(
|
audit_service::record(
|
||||||
AuditLog::new(tenant_id, operator_id, "dialysis_record.completed", "dialysis_record")
|
AuditLog::new(
|
||||||
.with_resource_id(m.id),
|
tenant_id,
|
||||||
|
operator_id,
|
||||||
|
"dialysis_record.completed",
|
||||||
|
"dialysis_record",
|
||||||
|
)
|
||||||
|
.with_resource_id(m.id),
|
||||||
&state.db,
|
&state.db,
|
||||||
).await;
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
Ok(to_resp(&state.crypto, m))
|
Ok(to_resp(&state.crypto, m))
|
||||||
}
|
}
|
||||||
@@ -294,10 +362,16 @@ pub async fn review_dialysis_record(
|
|||||||
let m = active.update(&state.db).await?;
|
let m = active.update(&state.db).await?;
|
||||||
|
|
||||||
audit_service::record(
|
audit_service::record(
|
||||||
AuditLog::new(tenant_id, Some(reviewer_id), "dialysis_record.reviewed", "dialysis_record")
|
AuditLog::new(
|
||||||
.with_resource_id(m.id),
|
tenant_id,
|
||||||
|
Some(reviewer_id),
|
||||||
|
"dialysis_record.reviewed",
|
||||||
|
"dialysis_record",
|
||||||
|
)
|
||||||
|
.with_resource_id(m.id),
|
||||||
&state.db,
|
&state.db,
|
||||||
).await;
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
Ok(to_resp(&state.crypto, m))
|
Ok(to_resp(&state.crypto, m))
|
||||||
}
|
}
|
||||||
@@ -328,10 +402,16 @@ pub async fn delete_dialysis_record(
|
|||||||
active.update(&state.db).await?;
|
active.update(&state.db).await?;
|
||||||
|
|
||||||
audit_service::record(
|
audit_service::record(
|
||||||
AuditLog::new(tenant_id, operator_id, "dialysis_record.deleted", "dialysis_record")
|
AuditLog::new(
|
||||||
.with_resource_id(record_id),
|
tenant_id,
|
||||||
|
operator_id,
|
||||||
|
"dialysis_record.deleted",
|
||||||
|
"dialysis_record",
|
||||||
|
)
|
||||||
|
.with_resource_id(record_id),
|
||||||
&state.db,
|
&state.db,
|
||||||
).await;
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -345,7 +425,8 @@ fn validate_dialysis_type(dialysis_type: &str) -> DialysisResult<()> {
|
|||||||
match dialysis_type {
|
match dialysis_type {
|
||||||
"HD" | "HDF" | "HF" => Ok(()),
|
"HD" | "HDF" | "HF" => Ok(()),
|
||||||
_ => Err(DialysisError::Validation(format!(
|
_ => Err(DialysisError::Validation(format!(
|
||||||
"无效的透析类型: {},允许值: HD, HDF, HF", dialysis_type
|
"无效的透析类型: {},允许值: HD, HDF, HF",
|
||||||
|
dialysis_type
|
||||||
))),
|
))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -365,7 +446,8 @@ fn validate_dialysis_status_transition(current: &str, new: &str) -> DialysisResu
|
|||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(DialysisError::InvalidStatusTransition(format!(
|
Err(DialysisError::InvalidStatusTransition(format!(
|
||||||
"dialysis_record.status: 不允许从 '{}' 转换到 '{}'", current, new
|
"dialysis_record.status: 不允许从 '{}' 转换到 '{}'",
|
||||||
|
current, new
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -374,14 +456,18 @@ fn to_resp(crypto: &erp_core::crypto::PiiCrypto, m: dialysis_record::Model) -> D
|
|||||||
let kek = crypto.kek();
|
let kek = crypto.kek();
|
||||||
|
|
||||||
// 解密症状 JSON(加密时存储为 Value::String(ciphertext))
|
// 解密症状 JSON(加密时存储为 Value::String(ciphertext))
|
||||||
let symptoms = m.symptoms.as_ref()
|
let symptoms = m
|
||||||
|
.symptoms
|
||||||
|
.as_ref()
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.and_then(|s| pii::decrypt(kek, s).ok())
|
.and_then(|s| pii::decrypt(kek, s).ok())
|
||||||
.and_then(|s| serde_json::from_str(&s).ok())
|
.and_then(|s| serde_json::from_str(&s).ok())
|
||||||
.or(m.symptoms);
|
.or(m.symptoms);
|
||||||
|
|
||||||
// 解密并发症备注
|
// 解密并发症备注
|
||||||
let complication_notes = m.complication_notes.as_ref()
|
let complication_notes = m
|
||||||
|
.complication_notes
|
||||||
|
.as_ref()
|
||||||
.map(|c| pii::decrypt(kek, c).unwrap_or_else(|_| c.clone()))
|
.map(|c| pii::decrypt(kek, c).unwrap_or_else(|_| c.clone()))
|
||||||
.or(m.complication_notes);
|
.or(m.complication_notes);
|
||||||
|
|
||||||
@@ -421,25 +507,45 @@ mod tests {
|
|||||||
|
|
||||||
// --- validate_dialysis_type ---
|
// --- validate_dialysis_type ---
|
||||||
#[test]
|
#[test]
|
||||||
fn dialysis_type_hd() { assert!(validate_dialysis_type("HD").is_ok()); }
|
fn dialysis_type_hd() {
|
||||||
|
assert!(validate_dialysis_type("HD").is_ok());
|
||||||
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn dialysis_type_hdf() { assert!(validate_dialysis_type("HDF").is_ok()); }
|
fn dialysis_type_hdf() {
|
||||||
|
assert!(validate_dialysis_type("HDF").is_ok());
|
||||||
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn dialysis_type_hf() { assert!(validate_dialysis_type("HF").is_ok()); }
|
fn dialysis_type_hf() {
|
||||||
|
assert!(validate_dialysis_type("HF").is_ok());
|
||||||
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn dialysis_type_invalid() { assert!(validate_dialysis_type("PD").is_err()); }
|
fn dialysis_type_invalid() {
|
||||||
|
assert!(validate_dialysis_type("PD").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
// --- validate_dialysis_status_transition ---
|
// --- validate_dialysis_status_transition ---
|
||||||
#[test]
|
#[test]
|
||||||
fn dial_draft_to_completed() { assert!(validate_dialysis_status_transition("draft", "completed").is_ok()); }
|
fn dial_draft_to_completed() {
|
||||||
|
assert!(validate_dialysis_status_transition("draft", "completed").is_ok());
|
||||||
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn dial_draft_to_reviewed_fails() { assert!(validate_dialysis_status_transition("draft", "reviewed").is_err()); }
|
fn dial_draft_to_reviewed_fails() {
|
||||||
|
assert!(validate_dialysis_status_transition("draft", "reviewed").is_err());
|
||||||
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn dial_completed_to_reviewed() { assert!(validate_dialysis_status_transition("completed", "reviewed").is_ok()); }
|
fn dial_completed_to_reviewed() {
|
||||||
|
assert!(validate_dialysis_status_transition("completed", "reviewed").is_ok());
|
||||||
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn dial_completed_to_draft_fails() { assert!(validate_dialysis_status_transition("completed", "draft").is_err()); }
|
fn dial_completed_to_draft_fails() {
|
||||||
|
assert!(validate_dialysis_status_transition("completed", "draft").is_err());
|
||||||
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn dial_reviewed_to_any_fails() { assert!(validate_dialysis_status_transition("reviewed", "draft").is_err()); }
|
fn dial_reviewed_to_any_fails() {
|
||||||
|
assert!(validate_dialysis_status_transition("reviewed", "draft").is_err());
|
||||||
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn dial_same_status_ok() { assert!(validate_dialysis_status_transition("draft", "draft").is_ok()); }
|
fn dial_same_status_ok() {
|
||||||
|
assert!(validate_dialysis_status_transition("draft", "draft").is_ok());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use sea_orm::{DatabaseBackend, FromQueryResult, Statement};
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::dto::dialysis_stats_dto::{DialysisStatisticsResp, NameValue};
|
use crate::dto::dialysis_stats_dto::{DialysisStatisticsResp, NameValue};
|
||||||
use crate::error::{DialysisResult, DialysisError};
|
use crate::error::{DialysisError, DialysisResult};
|
||||||
use crate::state::DialysisState;
|
use crate::state::DialysisState;
|
||||||
|
|
||||||
pub async fn get_dialysis_statistics(
|
pub async fn get_dialysis_statistics(
|
||||||
@@ -12,7 +12,9 @@ pub async fn get_dialysis_statistics(
|
|||||||
let db = &state.db;
|
let db = &state.db;
|
||||||
|
|
||||||
#[derive(FromQueryResult)]
|
#[derive(FromQueryResult)]
|
||||||
struct CountRow { count: i64 }
|
struct CountRow {
|
||||||
|
count: i64,
|
||||||
|
}
|
||||||
|
|
||||||
let total_records = CountRow::find_by_statement(Statement::from_sql_and_values(
|
let total_records = CountRow::find_by_statement(Statement::from_sql_and_values(
|
||||||
DatabaseBackend::Postgres,
|
DatabaseBackend::Postgres,
|
||||||
@@ -33,12 +35,14 @@ pub async fn get_dialysis_statistics(
|
|||||||
)).one(db).await?.map(|r| r.count).unwrap_or(0);
|
)).one(db).await?.map(|r| r.count).unwrap_or(0);
|
||||||
|
|
||||||
let type_distribution = count_by_field(
|
let type_distribution = count_by_field(
|
||||||
db, tenant_id,
|
db,
|
||||||
|
tenant_id,
|
||||||
"SELECT dialysis_type AS name, COUNT(*) AS value FROM dialysis_record \
|
"SELECT dialysis_type AS name, COUNT(*) AS value FROM dialysis_record \
|
||||||
WHERE tenant_id = $1 AND deleted_at IS NULL \
|
WHERE tenant_id = $1 AND deleted_at IS NULL \
|
||||||
AND created_at >= date_trunc('month', NOW()) \
|
AND created_at >= date_trunc('month', NOW()) \
|
||||||
GROUP BY dialysis_type ORDER BY value DESC",
|
GROUP BY dialysis_type ORDER BY value DESC",
|
||||||
).await?;
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let complication_rate = compute_complication_rate(db, tenant_id).await?;
|
let complication_rate = compute_complication_rate(db, tenant_id).await?;
|
||||||
let avg_ultrafiltration = compute_avg_field(db, tenant_id, "ultrafiltration_volume").await?;
|
let avg_ultrafiltration = compute_avg_field(db, tenant_id, "ultrafiltration_volume").await?;
|
||||||
@@ -61,7 +65,10 @@ async fn count_by_field(
|
|||||||
sql: &str,
|
sql: &str,
|
||||||
) -> DialysisResult<Vec<NameValue>> {
|
) -> DialysisResult<Vec<NameValue>> {
|
||||||
#[derive(FromQueryResult)]
|
#[derive(FromQueryResult)]
|
||||||
struct NameValueRow { name: String, value: i64 }
|
struct NameValueRow {
|
||||||
|
name: String,
|
||||||
|
value: i64,
|
||||||
|
}
|
||||||
|
|
||||||
let rows: Vec<NameValueRow> = FromQueryResult::find_by_statement(
|
let rows: Vec<NameValueRow> = FromQueryResult::find_by_statement(
|
||||||
Statement::from_sql_and_values(DatabaseBackend::Postgres, sql, [tenant_id.into()]),
|
Statement::from_sql_and_values(DatabaseBackend::Postgres, sql, [tenant_id.into()]),
|
||||||
@@ -69,17 +76,29 @@ async fn count_by_field(
|
|||||||
.all(db)
|
.all(db)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(rows.into_iter().map(|r| NameValue { name: r.name, value: r.value }).collect())
|
Ok(rows
|
||||||
|
.into_iter()
|
||||||
|
.map(|r| NameValue {
|
||||||
|
name: r.name,
|
||||||
|
value: r.value,
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, FromQueryResult)]
|
#[derive(Debug, FromQueryResult)]
|
||||||
struct AvgFieldResult { avg_val: Option<f64> }
|
struct AvgFieldResult {
|
||||||
|
avg_val: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
macro_rules! avg_field_sql {
|
macro_rules! avg_field_sql {
|
||||||
($field:literal) => {
|
($field:literal) => {
|
||||||
concat!(
|
concat!(
|
||||||
"SELECT AVG(", $field, ")::FLOAT8 AS avg_val FROM dialysis_record ",
|
"SELECT AVG(",
|
||||||
"WHERE tenant_id = $1 AND deleted_at IS NULL AND ", $field, " IS NOT NULL ",
|
$field,
|
||||||
|
")::FLOAT8 AS avg_val FROM dialysis_record ",
|
||||||
|
"WHERE tenant_id = $1 AND deleted_at IS NULL AND ",
|
||||||
|
$field,
|
||||||
|
" IS NOT NULL ",
|
||||||
"AND created_at >= date_trunc('month', NOW())"
|
"AND created_at >= date_trunc('month', NOW())"
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
@@ -94,7 +113,11 @@ async fn compute_avg_field(
|
|||||||
"ultrafiltration_volume" => avg_field_sql!("ultrafiltration_volume"),
|
"ultrafiltration_volume" => avg_field_sql!("ultrafiltration_volume"),
|
||||||
"dialysis_duration" => avg_field_sql!("dialysis_duration"),
|
"dialysis_duration" => avg_field_sql!("dialysis_duration"),
|
||||||
"blood_flow_rate" => avg_field_sql!("blood_flow_rate"),
|
"blood_flow_rate" => avg_field_sql!("blood_flow_rate"),
|
||||||
_ => return Err(DialysisError::Validation(format!("不允许的字段名: {field}"))),
|
_ => {
|
||||||
|
return Err(DialysisError::Validation(format!(
|
||||||
|
"不允许的字段名: {field}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let result: Option<AvgFieldResult> = FromQueryResult::find_by_statement(
|
let result: Option<AvgFieldResult> = FromQueryResult::find_by_statement(
|
||||||
Statement::from_sql_and_values(DatabaseBackend::Postgres, sql, [tenant_id.into()]),
|
Statement::from_sql_and_values(DatabaseBackend::Postgres, sql, [tenant_id.into()]),
|
||||||
@@ -119,7 +142,10 @@ async fn compute_complication_rate(
|
|||||||
"#;
|
"#;
|
||||||
|
|
||||||
#[derive(Debug, FromQueryResult)]
|
#[derive(Debug, FromQueryResult)]
|
||||||
struct CompResult { with_comp: i64, total: i64 }
|
struct CompResult {
|
||||||
|
with_comp: i64,
|
||||||
|
total: i64,
|
||||||
|
}
|
||||||
|
|
||||||
let result: Option<CompResult> = FromQueryResult::find_by_statement(
|
let result: Option<CompResult> = FromQueryResult::find_by_statement(
|
||||||
Statement::from_sql_and_values(DatabaseBackend::Postgres, sql, [tenant_id.into()]),
|
Statement::from_sql_and_values(DatabaseBackend::Postgres, sql, [tenant_id.into()]),
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
pub mod dialysis_service;
|
|
||||||
pub mod dialysis_prescription_service;
|
pub mod dialysis_prescription_service;
|
||||||
|
pub mod dialysis_service;
|
||||||
pub mod dialysis_stats_service;
|
pub mod dialysis_stats_service;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use aes_gcm::aead::Aead;
|
use aes_gcm::aead::Aead;
|
||||||
use aes_gcm::{Aes256Gcm, KeyInit, Nonce};
|
use aes_gcm::{Aes256Gcm, KeyInit, Nonce};
|
||||||
use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
|
use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
|
||||||
use hmac::{Hmac, Mac};
|
use hmac::{Hmac, Mac};
|
||||||
use sha2::Sha256;
|
use sha2::Sha256;
|
||||||
use zeroize::Zeroizing;
|
use zeroize::Zeroizing;
|
||||||
|
|||||||
@@ -119,7 +119,9 @@ pub struct UpdateArticleReq {
|
|||||||
|
|
||||||
impl UpdateArticleReq {
|
impl UpdateArticleReq {
|
||||||
pub fn sanitize(&mut self) {
|
pub fn sanitize(&mut self) {
|
||||||
if let Some(ref mut v) = self.title { *v = strip_html_tags(v); }
|
if let Some(ref mut v) = self.title {
|
||||||
|
*v = strip_html_tags(v);
|
||||||
|
}
|
||||||
self.summary = sanitize_option(self.summary.take());
|
self.summary = sanitize_option(self.summary.take());
|
||||||
self.content = sanitize_option(self.content.take());
|
self.content = sanitize_option(self.content.take());
|
||||||
self.category = sanitize_option(self.category.take());
|
self.category = sanitize_option(self.category.take());
|
||||||
@@ -205,7 +207,9 @@ pub struct UpdateCategoryReq {
|
|||||||
|
|
||||||
impl UpdateCategoryReq {
|
impl UpdateCategoryReq {
|
||||||
pub fn sanitize(&mut self) {
|
pub fn sanitize(&mut self) {
|
||||||
if let Some(ref mut v) = self.name { *v = strip_html_tags(v); }
|
if let Some(ref mut v) = self.name {
|
||||||
|
*v = strip_html_tags(v);
|
||||||
|
}
|
||||||
self.slug = sanitize_option(self.slug.take());
|
self.slug = sanitize_option(self.slug.take());
|
||||||
self.description = sanitize_option(self.description.take());
|
self.description = sanitize_option(self.description.take());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,8 +26,12 @@ impl CreateDiagnosisReq {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_diagnosis_type() -> String { "primary".to_string() }
|
fn default_diagnosis_type() -> String {
|
||||||
fn default_status() -> String { "active".to_string() }
|
"primary".to_string()
|
||||||
|
}
|
||||||
|
fn default_status() -> String {
|
||||||
|
"active".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, ToSchema)]
|
#[derive(Debug, Deserialize, ToSchema)]
|
||||||
pub struct UpdateDiagnosisReq {
|
pub struct UpdateDiagnosisReq {
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
pub mod appointment_dto;
|
|
||||||
pub mod alert_dto;
|
pub mod alert_dto;
|
||||||
|
pub mod appointment_dto;
|
||||||
|
pub mod article_dto;
|
||||||
pub mod ble_gateway_dto;
|
pub mod ble_gateway_dto;
|
||||||
pub mod care_plan_dto;
|
pub mod care_plan_dto;
|
||||||
pub mod article_dto;
|
|
||||||
pub mod consent_dto;
|
pub mod consent_dto;
|
||||||
pub mod consultation_dto;
|
pub mod consultation_dto;
|
||||||
pub mod daily_monitoring_dto;
|
pub mod daily_monitoring_dto;
|
||||||
pub mod diagnosis_dto;
|
pub mod diagnosis_dto;
|
||||||
pub mod medication_record_dto;
|
|
||||||
pub mod medication_reminder_dto;
|
|
||||||
pub mod doctor_dto;
|
pub mod doctor_dto;
|
||||||
pub mod follow_up_dto;
|
pub mod follow_up_dto;
|
||||||
pub mod follow_up_template_dto;
|
pub mod follow_up_template_dto;
|
||||||
pub mod health_data_dto;
|
pub mod health_data_dto;
|
||||||
|
pub mod medication_record_dto;
|
||||||
|
pub mod medication_reminder_dto;
|
||||||
pub mod patient_dto;
|
pub mod patient_dto;
|
||||||
pub mod points_dto;
|
pub mod points_dto;
|
||||||
pub mod shift_dto;
|
pub mod shift_dto;
|
||||||
|
|||||||
@@ -39,7 +39,10 @@ pub struct CreateShiftReq {
|
|||||||
impl CreateShiftReq {
|
impl CreateShiftReq {
|
||||||
pub fn sanitize(&mut self) {
|
pub fn sanitize(&mut self) {
|
||||||
self.period = erp_core::sanitize::sanitize_string(&self.period);
|
self.period = erp_core::sanitize::sanitize_string(&self.period);
|
||||||
self.notes = self.notes.take().map(|n| erp_core::sanitize::sanitize_string(&n));
|
self.notes = self
|
||||||
|
.notes
|
||||||
|
.take()
|
||||||
|
.map(|n| erp_core::sanitize::sanitize_string(&n));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
pub mod alert_rules;
|
pub mod alert_rules;
|
||||||
pub mod ble_gateway;
|
|
||||||
pub mod api_client;
|
|
||||||
pub mod alerts;
|
pub mod alerts;
|
||||||
|
pub mod api_client;
|
||||||
pub mod appointment;
|
pub mod appointment;
|
||||||
pub mod article;
|
pub mod article;
|
||||||
pub mod article_article_tag;
|
pub mod article_article_tag;
|
||||||
pub mod article_category;
|
pub mod article_category;
|
||||||
pub mod article_revision;
|
pub mod article_revision;
|
||||||
pub mod article_tag;
|
pub mod article_tag;
|
||||||
|
pub mod ble_gateway;
|
||||||
pub mod blind_index;
|
pub mod blind_index;
|
||||||
pub mod critical_value_threshold;
|
pub mod care_plan;
|
||||||
|
pub mod care_plan_item;
|
||||||
|
pub mod care_plan_outcome;
|
||||||
pub mod consent;
|
pub mod consent;
|
||||||
pub mod consultation_message;
|
pub mod consultation_message;
|
||||||
pub mod consultation_session;
|
pub mod consultation_session;
|
||||||
pub mod critical_alert;
|
pub mod critical_alert;
|
||||||
pub mod critical_alert_response;
|
pub mod critical_alert_response;
|
||||||
|
pub mod critical_value_threshold;
|
||||||
pub mod daily_monitoring;
|
pub mod daily_monitoring;
|
||||||
pub mod device_readings;
|
pub mod device_readings;
|
||||||
pub mod diagnosis;
|
pub mod diagnosis;
|
||||||
@@ -25,31 +28,28 @@ pub mod follow_up_task;
|
|||||||
pub mod follow_up_template;
|
pub mod follow_up_template;
|
||||||
pub mod follow_up_template_field;
|
pub mod follow_up_template_field;
|
||||||
pub mod gateway_patient_binding;
|
pub mod gateway_patient_binding;
|
||||||
|
pub mod handoff_log;
|
||||||
pub mod health_record;
|
pub mod health_record;
|
||||||
pub mod health_trend;
|
pub mod health_trend;
|
||||||
pub mod lab_report;
|
pub mod lab_report;
|
||||||
|
pub mod medication_record;
|
||||||
|
pub mod medication_reminder;
|
||||||
|
pub mod offline_event;
|
||||||
|
pub mod offline_event_registration;
|
||||||
pub mod patient;
|
pub mod patient;
|
||||||
|
pub mod patient_assignment;
|
||||||
|
pub mod patient_devices;
|
||||||
pub mod patient_doctor_relation;
|
pub mod patient_doctor_relation;
|
||||||
pub mod patient_family_member;
|
pub mod patient_family_member;
|
||||||
pub mod patient_tag;
|
pub mod patient_tag;
|
||||||
pub mod patient_tag_relation;
|
pub mod patient_tag_relation;
|
||||||
pub mod patient_devices;
|
|
||||||
pub mod points_account;
|
pub mod points_account;
|
||||||
pub mod points_checkin;
|
pub mod points_checkin;
|
||||||
pub mod points_order;
|
pub mod points_order;
|
||||||
pub mod points_product;
|
pub mod points_product;
|
||||||
pub mod points_rule;
|
pub mod points_rule;
|
||||||
pub mod points_transaction;
|
pub mod points_transaction;
|
||||||
pub mod offline_event;
|
|
||||||
pub mod offline_event_registration;
|
|
||||||
pub mod medication_record;
|
|
||||||
pub mod medication_reminder;
|
|
||||||
pub mod vital_signs;
|
|
||||||
pub mod care_plan;
|
|
||||||
pub mod care_plan_item;
|
|
||||||
pub mod care_plan_outcome;
|
|
||||||
pub mod shift;
|
pub mod shift;
|
||||||
pub mod patient_assignment;
|
pub mod vital_signs;
|
||||||
pub mod handoff_log;
|
|
||||||
pub mod vital_signs_daily;
|
pub mod vital_signs_daily;
|
||||||
pub mod vital_signs_hourly;
|
pub mod vital_signs_hourly;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
use crate::entity::{
|
use crate::entity::{
|
||||||
appointment, consultation_session, device_readings, doctor_profile, follow_up_task,
|
appointment, consultation_session, device_readings, doctor_profile, follow_up_task, lab_report,
|
||||||
lab_report, patient, patient_devices,
|
patient, patient_devices,
|
||||||
};
|
};
|
||||||
use crate::fhir::types::{device_type_to_category, device_type_to_loinc, device_type_to_unit};
|
use crate::fhir::types::{device_type_to_category, device_type_to_loinc, device_type_to_unit};
|
||||||
|
|
||||||
@@ -51,9 +51,10 @@ pub fn patient_to_fhir(p: &patient::Model) -> serde_json::Value {
|
|||||||
pub fn device_reading_to_fhir_observations(r: &device_readings::Model) -> Vec<serde_json::Value> {
|
pub fn device_reading_to_fhir_observations(r: &device_readings::Model) -> Vec<serde_json::Value> {
|
||||||
let mut results = Vec::new();
|
let mut results = Vec::new();
|
||||||
let patient_ref = serde_json::json!({"reference": format!("Patient/{}", r.patient_id)});
|
let patient_ref = serde_json::json!({"reference": format!("Patient/{}", r.patient_id)});
|
||||||
let device_ref = r.device_id.as_ref().map(|d| {
|
let device_ref = r
|
||||||
serde_json::json!({"reference": format!("Device/{}", d)})
|
.device_id
|
||||||
});
|
.as_ref()
|
||||||
|
.map(|d| serde_json::json!({"reference": format!("Device/{}", d)}));
|
||||||
let measured = r.measured_at.to_rfc3339();
|
let measured = r.measured_at.to_rfc3339();
|
||||||
let category = device_type_to_category(&r.device_type);
|
let category = device_type_to_category(&r.device_type);
|
||||||
|
|
||||||
@@ -71,29 +72,50 @@ pub fn device_reading_to_fhir_observations(r: &device_readings::Model) -> Vec<se
|
|||||||
|
|
||||||
if let Some(val) = sys {
|
if let Some(val) = sys {
|
||||||
results.push(make_observation(
|
results.push(make_observation(
|
||||||
&r.id, "8480-6", "Systolic blood pressure",
|
&r.id,
|
||||||
category_json.clone(), &patient_ref, device_ref.as_ref(),
|
"8480-6",
|
||||||
&measured, val, "mmHg", "mm[Hg]",
|
"Systolic blood pressure",
|
||||||
|
category_json.clone(),
|
||||||
|
&patient_ref,
|
||||||
|
device_ref.as_ref(),
|
||||||
|
&measured,
|
||||||
|
val,
|
||||||
|
"mmHg",
|
||||||
|
"mm[Hg]",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if let Some(val) = dia {
|
if let Some(val) = dia {
|
||||||
results.push(make_observation(
|
results.push(make_observation(
|
||||||
&r.id, "8462-4", "Diastolic blood pressure",
|
&r.id,
|
||||||
category_json, &patient_ref, device_ref.as_ref(),
|
"8462-4",
|
||||||
&measured, val, "mmHg", "mm[Hg]",
|
"Diastolic blood pressure",
|
||||||
|
category_json,
|
||||||
|
&patient_ref,
|
||||||
|
device_ref.as_ref(),
|
||||||
|
&measured,
|
||||||
|
val,
|
||||||
|
"mmHg",
|
||||||
|
"mm[Hg]",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let (loinc_code, loinc_display) = device_type_to_loinc(&r.device_type)
|
let (loinc_code, loinc_display) =
|
||||||
.unwrap_or(("unknown", "Unknown"));
|
device_type_to_loinc(&r.device_type).unwrap_or(("unknown", "Unknown"));
|
||||||
let (unit_display, unit_code) = device_type_to_unit(&r.device_type);
|
let (unit_display, unit_code) = device_type_to_unit(&r.device_type);
|
||||||
let val = extract_main_value(&r.device_type, &r.raw_value);
|
let val = extract_main_value(&r.device_type, &r.raw_value);
|
||||||
if let Some(v) = val {
|
if let Some(v) = val {
|
||||||
results.push(make_observation(
|
results.push(make_observation(
|
||||||
&r.id, loinc_code, loinc_display,
|
&r.id,
|
||||||
category_json, &patient_ref, device_ref.as_ref(),
|
loinc_code,
|
||||||
&measured, v, unit_display, unit_code,
|
loinc_display,
|
||||||
|
category_json,
|
||||||
|
&patient_ref,
|
||||||
|
device_ref.as_ref(),
|
||||||
|
&measured,
|
||||||
|
v,
|
||||||
|
unit_display,
|
||||||
|
unit_code,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,11 +124,18 @@ pub fn device_reading_to_fhir_observations(r: &device_readings::Model) -> Vec<se
|
|||||||
results
|
results
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn make_observation(
|
fn make_observation(
|
||||||
reading_id: &uuid::Uuid, code: &str, display: &str,
|
reading_id: &uuid::Uuid,
|
||||||
category: serde_json::Value, subject: &serde_json::Value,
|
code: &str,
|
||||||
device: Option<&serde_json::Value>, effective: &str,
|
display: &str,
|
||||||
value: f64, unit_display: &str, unit_code: &str,
|
category: serde_json::Value,
|
||||||
|
subject: &serde_json::Value,
|
||||||
|
device: Option<&serde_json::Value>,
|
||||||
|
effective: &str,
|
||||||
|
value: f64,
|
||||||
|
unit_display: &str,
|
||||||
|
unit_code: &str,
|
||||||
) -> serde_json::Value {
|
) -> serde_json::Value {
|
||||||
let mut obs = serde_json::json!({
|
let mut obs = serde_json::json!({
|
||||||
"resourceType": "Observation",
|
"resourceType": "Observation",
|
||||||
@@ -234,12 +263,10 @@ pub fn appointment_to_fhir(a: &appointment::Model) -> serde_json::Value {
|
|||||||
_ => "booked",
|
_ => "booked",
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut participants = vec![
|
let mut participants = vec![serde_json::json!({
|
||||||
serde_json::json!({
|
"actor": {"reference": format!("Patient/{}", a.patient_id)},
|
||||||
"actor": {"reference": format!("Patient/{}", a.patient_id)},
|
"status": "accepted",
|
||||||
"status": "accepted",
|
})];
|
||||||
}),
|
|
||||||
];
|
|
||||||
if let Some(ref doctor_id) = a.doctor_id {
|
if let Some(ref doctor_id) = a.doctor_id {
|
||||||
participants.push(serde_json::json!({
|
participants.push(serde_json::json!({
|
||||||
"actor": {"reference": format!("Practitioner/{}", doctor_id)},
|
"actor": {"reference": format!("Practitioner/{}", doctor_id)},
|
||||||
@@ -322,8 +349,7 @@ pub fn follow_up_to_fhir(t: &follow_up_task::Model) -> serde_json::Value {
|
|||||||
_ => "requested",
|
_ => "requested",
|
||||||
};
|
};
|
||||||
|
|
||||||
let display = t.content_template.as_deref()
|
let display = t.content_template.as_deref().unwrap_or(&t.follow_up_type);
|
||||||
.unwrap_or(&t.follow_up_type);
|
|
||||||
|
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"resourceType": "Task",
|
"resourceType": "Task",
|
||||||
@@ -343,7 +369,12 @@ fn mask_sensitive(s: &str) -> String {
|
|||||||
if s.len() <= 5 {
|
if s.len() <= 5 {
|
||||||
"*".repeat(s.len())
|
"*".repeat(s.len())
|
||||||
} else {
|
} else {
|
||||||
format!("{}{}{}", &s[..1], "*".repeat(s.len() - 5), &s[s.len() - 4..])
|
format!(
|
||||||
|
"{}{}{}",
|
||||||
|
&s[..1],
|
||||||
|
"*".repeat(s.len() - 5),
|
||||||
|
&s[s.len() - 4..]
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use axum::extract::{FromRef, Path, Query, State};
|
|
||||||
use axum::response::IntoResponse;
|
|
||||||
use axum::Extension;
|
use axum::Extension;
|
||||||
use axum::Json;
|
use axum::Json;
|
||||||
|
use axum::extract::{FromRef, Path, Query, State};
|
||||||
|
use axum::response::IntoResponse;
|
||||||
use sea_orm::*;
|
use sea_orm::*;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@@ -78,11 +78,15 @@ fn enforce_patient_scope(fhir_ctx: &FhirAuthContext, patient_id: Uuid) -> Result
|
|||||||
requested_patient = %patient_id,
|
requested_patient = %patient_id,
|
||||||
"FHIR 客户端尝试访问授权范围外的患者"
|
"FHIR 客户端尝试访问授权范围外的患者"
|
||||||
);
|
);
|
||||||
return Err(AppError::Forbidden("Access denied: patient not in allowed scope".into()));
|
return Err(AppError::Forbidden(
|
||||||
|
"Access denied: patient not in allowed scope".into(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(AppError::Forbidden("OAuth client has no patient access configured".into()));
|
return Err(AppError::Forbidden(
|
||||||
|
"OAuth client has no patient access configured".into(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -112,7 +116,8 @@ pub async fn search_patients(
|
|||||||
.filter(crate::entity::patient::Column::DeletedAt.is_null());
|
.filter(crate::entity::patient::Column::DeletedAt.is_null());
|
||||||
|
|
||||||
if let Some(ref id) = params.id {
|
if let Some(ref id) = params.id {
|
||||||
let uid = Uuid::parse_str(id).map_err(|_| AppError::Validation("Invalid patient id".into()))?;
|
let uid =
|
||||||
|
Uuid::parse_str(id).map_err(|_| AppError::Validation("Invalid patient id".into()))?;
|
||||||
query = query.filter(crate::entity::patient::Column::Id.eq(uid));
|
query = query.filter(crate::entity::patient::Column::Id.eq(uid));
|
||||||
}
|
}
|
||||||
if let Some(ref name) = params.name {
|
if let Some(ref name) = params.name {
|
||||||
@@ -123,21 +128,18 @@ pub async fn search_patients(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 强制执行 allowed_patient_ids 范围
|
// 强制执行 allowed_patient_ids 范围
|
||||||
if let Some(uuids) = resolve_allowed_patient_uuids(&fhir_ctx) {
|
if let Some(uuids) = resolve_allowed_patient_uuids(&fhir_ctx)
|
||||||
if !uuids.is_empty() {
|
&& !uuids.is_empty()
|
||||||
query = query.filter(crate::entity::patient::Column::Id.is_in(uuids));
|
{
|
||||||
}
|
query = query.filter(crate::entity::patient::Column::Id.is_in(uuids));
|
||||||
}
|
}
|
||||||
|
|
||||||
let limit = params.count.unwrap_or(20).min(100);
|
let limit = params.count.unwrap_or(20).min(100);
|
||||||
let offset = params.offset.unwrap_or(0);
|
let offset = params.offset.unwrap_or(0);
|
||||||
let patients = query
|
let patients = query.limit(limit).offset(offset).all(&state.db).await?;
|
||||||
.limit(limit)
|
|
||||||
.offset(offset)
|
|
||||||
.all(&state.db)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let entries: Vec<serde_json::Value> = patients.iter()
|
let entries: Vec<serde_json::Value> = patients
|
||||||
|
.iter()
|
||||||
.map(|p| serde_json::json!({"resource": converter::patient_to_fhir(p)}))
|
.map(|p| serde_json::json!({"resource": converter::patient_to_fhir(p)}))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@@ -189,46 +191,39 @@ pub async fn search_observations(
|
|||||||
.map_err(|_| AppError::Validation("Invalid patient id".into()))?;
|
.map_err(|_| AppError::Validation("Invalid patient id".into()))?;
|
||||||
query = query.filter(crate::entity::device_readings::Column::PatientId.eq(uid));
|
query = query.filter(crate::entity::device_readings::Column::PatientId.eq(uid));
|
||||||
}
|
}
|
||||||
if let Some(ref code) = params.code {
|
if let Some(ref code) = params.code
|
||||||
if let Some(dt) = loinc_to_device_type(code) {
|
&& let Some(dt) = loinc_to_device_type(code)
|
||||||
query = query.filter(crate::entity::device_readings::Column::DeviceType.eq(dt));
|
{
|
||||||
}
|
query = query.filter(crate::entity::device_readings::Column::DeviceType.eq(dt));
|
||||||
}
|
}
|
||||||
if let Some(ref category) = params.category {
|
if let Some(ref category) = params.category {
|
||||||
let types = category_to_device_types(category);
|
let types = category_to_device_types(category);
|
||||||
if !types.is_empty() {
|
if !types.is_empty() {
|
||||||
query = query.filter(
|
query = query.filter(crate::entity::device_readings::Column::DeviceType.is_in(types));
|
||||||
crate::entity::device_readings::Column::DeviceType.is_in(types)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(ref date) = params.date {
|
if let Some(ref date) = params.date {
|
||||||
if let Some(after) = date.strip_prefix("gt") {
|
if let Some(after) = date.strip_prefix("gt") {
|
||||||
if let Ok(dt) = after.parse::<chrono::DateTime<chrono::Utc>>() {
|
if let Ok(dt) = after.parse::<chrono::DateTime<chrono::Utc>>() {
|
||||||
query = query.filter(
|
query = query.filter(crate::entity::device_readings::Column::MeasuredAt.gt(dt));
|
||||||
crate::entity::device_readings::Column::MeasuredAt.gt(dt)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else if let Some(before) = date.strip_prefix("lt") {
|
} else if let Some(before) = date.strip_prefix("lt") {
|
||||||
if let Ok(dt) = before.parse::<chrono::DateTime<chrono::Utc>>() {
|
if let Ok(dt) = before.parse::<chrono::DateTime<chrono::Utc>>() {
|
||||||
query = query.filter(
|
query = query.filter(crate::entity::device_readings::Column::MeasuredAt.lt(dt));
|
||||||
crate::entity::device_readings::Column::MeasuredAt.lt(dt)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else if let Ok(dt) = date.parse::<chrono::DateTime<chrono::Utc>>() {
|
} else if let Ok(dt) = date.parse::<chrono::DateTime<chrono::Utc>>() {
|
||||||
let start = dt.date_naive().and_hms_opt(0, 0, 0).unwrap().and_utc();
|
let start = dt.date_naive().and_hms_opt(0, 0, 0).unwrap().and_utc();
|
||||||
let end = dt.date_naive().and_hms_opt(23, 59, 59).unwrap().and_utc();
|
let end = dt.date_naive().and_hms_opt(23, 59, 59).unwrap().and_utc();
|
||||||
query = query.filter(
|
query = query
|
||||||
crate::entity::device_readings::Column::MeasuredAt.between(start, end)
|
.filter(crate::entity::device_readings::Column::MeasuredAt.between(start, end));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 强制执行 allowed_patient_ids 范围
|
// 强制执行 allowed_patient_ids 范围
|
||||||
if let Some(uuids) = resolve_allowed_patient_uuids(&fhir_ctx) {
|
if let Some(uuids) = resolve_allowed_patient_uuids(&fhir_ctx)
|
||||||
if !uuids.is_empty() {
|
&& !uuids.is_empty()
|
||||||
query = query.filter(crate::entity::device_readings::Column::PatientId.is_in(uuids));
|
{
|
||||||
}
|
query = query.filter(crate::entity::device_readings::Column::PatientId.is_in(uuids));
|
||||||
}
|
}
|
||||||
|
|
||||||
let limit = params.count.unwrap_or(50).min(200);
|
let limit = params.count.unwrap_or(50).min(200);
|
||||||
@@ -274,16 +269,17 @@ pub async fn search_devices(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 强制执行 allowed_patient_ids 范围
|
// 强制执行 allowed_patient_ids 范围
|
||||||
if let Some(uuids) = resolve_allowed_patient_uuids(&fhir_ctx) {
|
if let Some(uuids) = resolve_allowed_patient_uuids(&fhir_ctx)
|
||||||
if !uuids.is_empty() {
|
&& !uuids.is_empty()
|
||||||
query = query.filter(crate::entity::patient_devices::Column::PatientId.is_in(uuids));
|
{
|
||||||
}
|
query = query.filter(crate::entity::patient_devices::Column::PatientId.is_in(uuids));
|
||||||
}
|
}
|
||||||
|
|
||||||
let limit = params.count.unwrap_or(50).min(200);
|
let limit = params.count.unwrap_or(50).min(200);
|
||||||
let devices = query.limit(limit).all(&state.db).await?;
|
let devices = query.limit(limit).all(&state.db).await?;
|
||||||
|
|
||||||
let entries: Vec<serde_json::Value> = devices.iter()
|
let entries: Vec<serde_json::Value> = devices
|
||||||
|
.iter()
|
||||||
.map(|d| serde_json::json!({"resource": converter::patient_device_to_fhir(d)}))
|
.map(|d| serde_json::json!({"resource": converter::patient_device_to_fhir(d)}))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@@ -337,7 +333,8 @@ pub async fn search_practitioners(
|
|||||||
let limit = params.count.unwrap_or(50).min(200);
|
let limit = params.count.unwrap_or(50).min(200);
|
||||||
let doctors = query.limit(limit).all(&state.db).await?;
|
let doctors = query.limit(limit).all(&state.db).await?;
|
||||||
|
|
||||||
let entries: Vec<serde_json::Value> = doctors.iter()
|
let entries: Vec<serde_json::Value> = doctors
|
||||||
|
.iter()
|
||||||
.map(|d| serde_json::json!({"resource": converter::doctor_to_fhir(d)}))
|
.map(|d| serde_json::json!({"resource": converter::doctor_to_fhir(d)}))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@@ -392,10 +389,10 @@ pub async fn search_appointments(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 强制执行 allowed_patient_ids 范围
|
// 强制执行 allowed_patient_ids 范围
|
||||||
if let Some(uuids) = resolve_allowed_patient_uuids(&fhir_ctx) {
|
if let Some(uuids) = resolve_allowed_patient_uuids(&fhir_ctx)
|
||||||
if !uuids.is_empty() {
|
&& !uuids.is_empty()
|
||||||
query = query.filter(crate::entity::appointment::Column::PatientId.is_in(uuids));
|
{
|
||||||
}
|
query = query.filter(crate::entity::appointment::Column::PatientId.is_in(uuids));
|
||||||
}
|
}
|
||||||
|
|
||||||
let limit = params.count.unwrap_or(50).min(200);
|
let limit = params.count.unwrap_or(50).min(200);
|
||||||
@@ -405,7 +402,8 @@ pub async fn search_appointments(
|
|||||||
.all(&state.db)
|
.all(&state.db)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let entries: Vec<serde_json::Value> = appointments.iter()
|
let entries: Vec<serde_json::Value> = appointments
|
||||||
|
.iter()
|
||||||
.map(|a| serde_json::json!({"resource": converter::appointment_to_fhir(a)}))
|
.map(|a| serde_json::json!({"resource": converter::appointment_to_fhir(a)}))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@@ -466,10 +464,10 @@ pub async fn search_diagnostic_reports(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 强制执行 allowed_patient_ids 范围
|
// 强制执行 allowed_patient_ids 范围
|
||||||
if let Some(uuids) = resolve_allowed_patient_uuids(&fhir_ctx) {
|
if let Some(uuids) = resolve_allowed_patient_uuids(&fhir_ctx)
|
||||||
if !uuids.is_empty() {
|
&& !uuids.is_empty()
|
||||||
query = query.filter(crate::entity::lab_report::Column::PatientId.is_in(uuids));
|
{
|
||||||
}
|
query = query.filter(crate::entity::lab_report::Column::PatientId.is_in(uuids));
|
||||||
}
|
}
|
||||||
|
|
||||||
let limit = params.count.unwrap_or(50).min(200);
|
let limit = params.count.unwrap_or(50).min(200);
|
||||||
@@ -479,7 +477,8 @@ pub async fn search_diagnostic_reports(
|
|||||||
.all(&state.db)
|
.all(&state.db)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let entries: Vec<serde_json::Value> = reports.iter()
|
let entries: Vec<serde_json::Value> = reports
|
||||||
|
.iter()
|
||||||
.map(|r| serde_json::json!({"resource": converter::lab_report_to_fhir(r)}))
|
.map(|r| serde_json::json!({"resource": converter::lab_report_to_fhir(r)}))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@@ -537,10 +536,10 @@ pub async fn search_encounters(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 强制执行 allowed_patient_ids 范围
|
// 强制执行 allowed_patient_ids 范围
|
||||||
if let Some(uuids) = resolve_allowed_patient_uuids(&fhir_ctx) {
|
if let Some(uuids) = resolve_allowed_patient_uuids(&fhir_ctx)
|
||||||
if !uuids.is_empty() {
|
&& !uuids.is_empty()
|
||||||
query = query.filter(crate::entity::consultation_session::Column::PatientId.is_in(uuids));
|
{
|
||||||
}
|
query = query.filter(crate::entity::consultation_session::Column::PatientId.is_in(uuids));
|
||||||
}
|
}
|
||||||
|
|
||||||
let limit = params.count.unwrap_or(50).min(200);
|
let limit = params.count.unwrap_or(50).min(200);
|
||||||
@@ -550,7 +549,8 @@ pub async fn search_encounters(
|
|||||||
.all(&state.db)
|
.all(&state.db)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let entries: Vec<serde_json::Value> = sessions.iter()
|
let entries: Vec<serde_json::Value> = sessions
|
||||||
|
.iter()
|
||||||
.map(|s| serde_json::json!({"resource": converter::consultation_to_fhir(s)}))
|
.map(|s| serde_json::json!({"resource": converter::consultation_to_fhir(s)}))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@@ -608,10 +608,10 @@ pub async fn search_tasks(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 强制执行 allowed_patient_ids 范围
|
// 强制执行 allowed_patient_ids 范围
|
||||||
if let Some(uuids) = resolve_allowed_patient_uuids(&fhir_ctx) {
|
if let Some(uuids) = resolve_allowed_patient_uuids(&fhir_ctx)
|
||||||
if !uuids.is_empty() {
|
&& !uuids.is_empty()
|
||||||
query = query.filter(crate::entity::follow_up_task::Column::PatientId.is_in(uuids));
|
{
|
||||||
}
|
query = query.filter(crate::entity::follow_up_task::Column::PatientId.is_in(uuids));
|
||||||
}
|
}
|
||||||
|
|
||||||
let limit = params.count.unwrap_or(50).min(200);
|
let limit = params.count.unwrap_or(50).min(200);
|
||||||
@@ -621,7 +621,8 @@ pub async fn search_tasks(
|
|||||||
.all(&state.db)
|
.all(&state.db)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let entries: Vec<serde_json::Value> = tasks.iter()
|
let entries: Vec<serde_json::Value> = tasks
|
||||||
|
.iter()
|
||||||
.map(|t| serde_json::json!({"resource": converter::follow_up_to_fhir(t)}))
|
.map(|t| serde_json::json!({"resource": converter::follow_up_to_fhir(t)}))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@@ -815,6 +816,9 @@ mod tests {
|
|||||||
..default_fhir_ctx()
|
..default_fhir_ctx()
|
||||||
};
|
};
|
||||||
let result = enforce_patient_scope(&fhir_ctx, Uuid::now_v7());
|
let result = enforce_patient_scope(&fhir_ctx, Uuid::now_v7());
|
||||||
assert!(result.is_err(), "Patient not in allowed list should be denied");
|
assert!(
|
||||||
|
result.is_err(),
|
||||||
|
"Patient not in allowed list should be denied"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,12 @@ pub fn loinc_to_device_type(loinc: &str) -> Option<&'static str> {
|
|||||||
/// FHIR category → device_type 列表
|
/// FHIR category → device_type 列表
|
||||||
pub fn category_to_device_types(category: &str) -> Vec<&'static str> {
|
pub fn category_to_device_types(category: &str) -> Vec<&'static str> {
|
||||||
match category {
|
match category {
|
||||||
"vital-signs" => vec!["heart_rate", "blood_oxygen", "blood_pressure", "temperature"],
|
"vital-signs" => vec![
|
||||||
|
"heart_rate",
|
||||||
|
"blood_oxygen",
|
||||||
|
"blood_pressure",
|
||||||
|
"temperature",
|
||||||
|
],
|
||||||
"laboratory" => vec!["blood_glucose"],
|
"laboratory" => vec!["blood_glucose"],
|
||||||
"activity" => vec!["steps", "sleep", "stress"],
|
"activity" => vec!["steps", "sleep", "stress"],
|
||||||
_ => vec![],
|
_ => vec![],
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
use axum::{
|
use axum::{
|
||||||
|
Json,
|
||||||
extract::{Request, State},
|
extract::{Request, State},
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
middleware::Next,
|
middleware::Next,
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
Json,
|
|
||||||
};
|
};
|
||||||
use sha2::{Digest, Sha256};
|
|
||||||
use uuid::Uuid;
|
|
||||||
use sea_orm::ColumnTrait;
|
use sea_orm::ColumnTrait;
|
||||||
use sea_orm::EntityTrait;
|
use sea_orm::EntityTrait;
|
||||||
use sea_orm::QueryFilter;
|
use sea_orm::QueryFilter;
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::state::HealthState;
|
use crate::state::HealthState;
|
||||||
|
|
||||||
@@ -94,12 +94,11 @@ fn extract_gateway_key(request: &Request) -> Option<String> {
|
|||||||
.headers()
|
.headers()
|
||||||
.get("Authorization")
|
.get("Authorization")
|
||||||
.and_then(|v| v.to_str().ok())
|
.and_then(|v| v.to_str().ok())
|
||||||
|
&& let Some(key) = auth.strip_prefix("Gateway ")
|
||||||
{
|
{
|
||||||
if let Some(key) = auth.strip_prefix("Gateway ") {
|
let key = key.trim();
|
||||||
let key = key.trim();
|
if !key.is_empty() {
|
||||||
if !key.is_empty() {
|
return Some(key.to_string());
|
||||||
return Some(key.to_string());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use axum::extract::{FromRef, Json, Path, Query, State};
|
|
||||||
use axum::Extension;
|
use axum::Extension;
|
||||||
|
use axum::extract::{FromRef, Json, Path, Query, State};
|
||||||
use erp_core::error::AppError;
|
use erp_core::error::AppError;
|
||||||
use erp_core::rbac::require_permission;
|
use erp_core::rbac::require_permission;
|
||||||
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
||||||
@@ -82,8 +82,7 @@ where
|
|||||||
S: Clone + Send + Sync + 'static,
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.action-inbox.team")?;
|
require_permission(&ctx, "health.action-inbox.team")?;
|
||||||
let result =
|
let result = action_inbox_service::get_team_overview(&state.db, ctx.tenant_id).await?;
|
||||||
action_inbox_service::get_team_overview(&state.db, ctx.tenant_id).await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
use axum::Extension;
|
||||||
use axum::extract::{FromRef, Path, Query, State};
|
use axum::extract::{FromRef, Path, Query, State};
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use axum::Extension;
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use utoipa::IntoParams;
|
use utoipa::IntoParams;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@@ -36,9 +36,15 @@ where
|
|||||||
let page_size = query.page_size.unwrap_or(20);
|
let page_size = query.page_size.unwrap_or(20);
|
||||||
|
|
||||||
let (items, total) = alert_service::list_alerts(
|
let (items, total) = alert_service::list_alerts(
|
||||||
&state, ctx.tenant_id, query.patient_id, query.doctor_id, query.status.as_deref(),
|
&state,
|
||||||
page, page_size,
|
ctx.tenant_id,
|
||||||
).await?;
|
query.patient_id,
|
||||||
|
query.doctor_id,
|
||||||
|
query.status.as_deref(),
|
||||||
|
page,
|
||||||
|
page_size,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok(axum::Json(ApiResponse::ok(PaginatedResponse {
|
Ok(axum::Json(ApiResponse::ok(PaginatedResponse {
|
||||||
data: items,
|
data: items,
|
||||||
@@ -74,9 +80,9 @@ where
|
|||||||
S: Clone + Send + Sync + 'static,
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.alerts.manage")?;
|
require_permission(&ctx, "health.alerts.manage")?;
|
||||||
let alert = alert_service::acknowledge_alert(
|
let alert =
|
||||||
&state, ctx.tenant_id, id, ctx.user_id, body.version,
|
alert_service::acknowledge_alert(&state, ctx.tenant_id, id, ctx.user_id, body.version)
|
||||||
).await?;
|
.await?;
|
||||||
Ok(axum::Json(ApiResponse::ok(alert)))
|
Ok(axum::Json(ApiResponse::ok(alert)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,9 +97,8 @@ where
|
|||||||
S: Clone + Send + Sync + 'static,
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.alerts.manage")?;
|
require_permission(&ctx, "health.alerts.manage")?;
|
||||||
let alert = alert_service::dismiss_alert(
|
let alert =
|
||||||
&state, ctx.tenant_id, id, ctx.user_id, body.version,
|
alert_service::dismiss_alert(&state, ctx.tenant_id, id, ctx.user_id, body.version).await?;
|
||||||
).await?;
|
|
||||||
Ok(axum::Json(ApiResponse::ok(alert)))
|
Ok(axum::Json(ApiResponse::ok(alert)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,8 +113,6 @@ where
|
|||||||
S: Clone + Send + Sync + 'static,
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.alerts.manage")?;
|
require_permission(&ctx, "health.alerts.manage")?;
|
||||||
let alert = alert_service::resolve_alert(
|
let alert = alert_service::resolve_alert(&state, ctx.tenant_id, id, body.version).await?;
|
||||||
&state, ctx.tenant_id, id, body.version,
|
|
||||||
).await?;
|
|
||||||
Ok(axum::Json(ApiResponse::ok(alert)))
|
Ok(axum::Json(ApiResponse::ok(alert)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
use axum::Extension;
|
||||||
use axum::extract::{FromRef, Path, Query, State};
|
use axum::extract::{FromRef, Path, Query, State};
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use axum::Extension;
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use utoipa::IntoParams;
|
use utoipa::IntoParams;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@@ -39,8 +39,13 @@ where
|
|||||||
let page_size = query.page_size.unwrap_or(20);
|
let page_size = query.page_size.unwrap_or(20);
|
||||||
|
|
||||||
let (items, total) = alert_rule_service::list_rules(
|
let (items, total) = alert_rule_service::list_rules(
|
||||||
&state, ctx.tenant_id, query.device_type.as_deref(), page, page_size,
|
&state,
|
||||||
).await?;
|
ctx.tenant_id,
|
||||||
|
query.device_type.as_deref(),
|
||||||
|
page,
|
||||||
|
page_size,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok(axum::Json(ApiResponse::ok(PaginatedResponse {
|
Ok(axum::Json(ApiResponse::ok(PaginatedResponse {
|
||||||
data: items,
|
data: items,
|
||||||
@@ -62,9 +67,7 @@ where
|
|||||||
{
|
{
|
||||||
require_permission(&ctx, "health.alert-rules.manage")?;
|
require_permission(&ctx, "health.alert-rules.manage")?;
|
||||||
body.sanitize();
|
body.sanitize();
|
||||||
let rule = alert_rule_service::create_rule(
|
let rule = alert_rule_service::create_rule(&state, ctx.tenant_id, ctx.user_id, body).await?;
|
||||||
&state, ctx.tenant_id, ctx.user_id, body,
|
|
||||||
).await?;
|
|
||||||
Ok(axum::Json(ApiResponse::ok(rule)))
|
Ok(axum::Json(ApiResponse::ok(rule)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,9 +83,8 @@ where
|
|||||||
{
|
{
|
||||||
require_permission(&ctx, "health.alert-rules.manage")?;
|
require_permission(&ctx, "health.alert-rules.manage")?;
|
||||||
body.sanitize();
|
body.sanitize();
|
||||||
let rule = alert_rule_service::update_rule(
|
let rule =
|
||||||
&state, ctx.tenant_id, id, ctx.user_id, body,
|
alert_rule_service::update_rule(&state, ctx.tenant_id, id, ctx.user_id, body).await?;
|
||||||
).await?;
|
|
||||||
Ok(axum::Json(ApiResponse::ok(rule)))
|
Ok(axum::Json(ApiResponse::ok(rule)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,8 +99,6 @@ where
|
|||||||
S: Clone + Send + Sync + 'static,
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.alert-rules.manage")?;
|
require_permission(&ctx, "health.alert-rules.manage")?;
|
||||||
let rule = alert_rule_service::deactivate_rule(
|
let rule = alert_rule_service::deactivate_rule(&state, ctx.tenant_id, id, body.version).await?;
|
||||||
&state, ctx.tenant_id, id, body.version,
|
|
||||||
).await?;
|
|
||||||
Ok(axum::Json(ApiResponse::ok(rule)))
|
Ok(axum::Json(ApiResponse::ok(rule)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,8 +64,14 @@ where
|
|||||||
let page = params.page.unwrap_or(1);
|
let page = params.page.unwrap_or(1);
|
||||||
let page_size = params.page_size.unwrap_or(20);
|
let page_size = params.page_size.unwrap_or(20);
|
||||||
let result = appointment_service::list_appointments(
|
let result = appointment_service::list_appointments(
|
||||||
&state, ctx.tenant_id, page, page_size, params.status, params.patient_id,
|
&state,
|
||||||
params.doctor_id, params.date,
|
ctx.tenant_id,
|
||||||
|
page,
|
||||||
|
page_size,
|
||||||
|
params.status,
|
||||||
|
params.patient_id,
|
||||||
|
params.doctor_id,
|
||||||
|
params.date,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
@@ -83,10 +89,9 @@ where
|
|||||||
require_permission(&ctx, "health.appointment.manage")?;
|
require_permission(&ctx, "health.appointment.manage")?;
|
||||||
let mut req = req;
|
let mut req = req;
|
||||||
req.sanitize();
|
req.sanitize();
|
||||||
let result = appointment_service::create_appointment(
|
let result =
|
||||||
&state, ctx.tenant_id, Some(ctx.user_id), req,
|
appointment_service::create_appointment(&state, ctx.tenant_id, Some(ctx.user_id), req)
|
||||||
)
|
.await?;
|
||||||
.await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,7 +126,12 @@ where
|
|||||||
};
|
};
|
||||||
update_req.sanitize();
|
update_req.sanitize();
|
||||||
let result = appointment_service::update_appointment_status(
|
let result = appointment_service::update_appointment_status(
|
||||||
&state, ctx.tenant_id, id, Some(ctx.user_id), update_req, req.version,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
update_req,
|
||||||
|
req.version,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
@@ -140,7 +150,12 @@ where
|
|||||||
let page = params.page.unwrap_or(1);
|
let page = params.page.unwrap_or(1);
|
||||||
let page_size = params.page_size.unwrap_or(20);
|
let page_size = params.page_size.unwrap_or(20);
|
||||||
let result = appointment_service::list_schedules(
|
let result = appointment_service::list_schedules(
|
||||||
&state, ctx.tenant_id, page, page_size, params.doctor_id, params.date,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
page,
|
||||||
|
page_size,
|
||||||
|
params.doctor_id,
|
||||||
|
params.date,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
@@ -156,10 +171,8 @@ where
|
|||||||
S: Clone + Send + Sync + 'static,
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.appointment.manage")?;
|
require_permission(&ctx, "health.appointment.manage")?;
|
||||||
let result = appointment_service::create_schedule(
|
let result =
|
||||||
&state, ctx.tenant_id, Some(ctx.user_id), req,
|
appointment_service::create_schedule(&state, ctx.tenant_id, Some(ctx.user_id), req).await?;
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,7 +188,12 @@ where
|
|||||||
{
|
{
|
||||||
require_permission(&ctx, "health.appointment.manage")?;
|
require_permission(&ctx, "health.appointment.manage")?;
|
||||||
let result = appointment_service::update_schedule(
|
let result = appointment_service::update_schedule(
|
||||||
&state, ctx.tenant_id, id, Some(ctx.user_id), req.data, req.version,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
req.data,
|
||||||
|
req.version,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
@@ -192,7 +210,11 @@ where
|
|||||||
{
|
{
|
||||||
require_permission(&ctx, "health.appointment.list")?;
|
require_permission(&ctx, "health.appointment.list")?;
|
||||||
let result = appointment_service::calendar_view(
|
let result = appointment_service::calendar_view(
|
||||||
&state, ctx.tenant_id, params.start_date, params.end_date, params.doctor_id,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
params.start_date,
|
||||||
|
params.end_date,
|
||||||
|
params.doctor_id,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
|
|||||||
@@ -34,9 +34,9 @@ where
|
|||||||
{
|
{
|
||||||
require_permission(&ctx, "health.articles.manage")?;
|
require_permission(&ctx, "health.articles.manage")?;
|
||||||
req.sanitize();
|
req.sanitize();
|
||||||
let result = article_category_service::create_category(
|
let result =
|
||||||
&state, ctx.tenant_id, Some(ctx.user_id), req.0,
|
article_category_service::create_category(&state, ctx.tenant_id, Some(ctx.user_id), req.0)
|
||||||
).await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,8 +53,13 @@ where
|
|||||||
require_permission(&ctx, "health.articles.manage")?;
|
require_permission(&ctx, "health.articles.manage")?;
|
||||||
req.sanitize();
|
req.sanitize();
|
||||||
let result = article_category_service::update_category(
|
let result = article_category_service::update_category(
|
||||||
&state, ctx.tenant_id, id, Some(ctx.user_id), req.0,
|
&state,
|
||||||
).await?;
|
ctx.tenant_id,
|
||||||
|
id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
req.0,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +80,12 @@ where
|
|||||||
{
|
{
|
||||||
require_permission(&ctx, "health.articles.manage")?;
|
require_permission(&ctx, "health.articles.manage")?;
|
||||||
article_category_service::delete_category(
|
article_category_service::delete_category(
|
||||||
&state, ctx.tenant_id, id, Some(ctx.user_id), req.version,
|
&state,
|
||||||
).await?;
|
ctx.tenant_id,
|
||||||
|
id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
req.version,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(())))
|
Ok(Json(ApiResponse::ok(())))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ use erp_core::error::AppError;
|
|||||||
use erp_core::rbac::{require_any_permission, require_permission};
|
use erp_core::rbac::{require_any_permission, require_permission};
|
||||||
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
||||||
|
|
||||||
use crate::dto::article_dto::{ArticleListItem, ArticleListParams, ArticleResp, CreateArticleReq, ReviewArticleReq, UpdateArticleReq};
|
use crate::dto::article_dto::{
|
||||||
|
ArticleListItem, ArticleListParams, ArticleResp, CreateArticleReq, ReviewArticleReq,
|
||||||
|
UpdateArticleReq,
|
||||||
|
};
|
||||||
use crate::service::article_service;
|
use crate::service::article_service;
|
||||||
use crate::state::HealthState;
|
use crate::state::HealthState;
|
||||||
|
|
||||||
@@ -22,14 +25,24 @@ where
|
|||||||
let page = params.page.unwrap_or(1);
|
let page = params.page.unwrap_or(1);
|
||||||
let page_size = params.page_size.unwrap_or(20);
|
let page_size = params.page_size.unwrap_or(20);
|
||||||
// 非管理权限用户只能查看已发布文章,防止草稿泄露
|
// 非管理权限用户只能查看已发布文章,防止草稿泄露
|
||||||
let status = if require_any_permission(&ctx, &["health.articles.manage", "health.articles.review"]).is_ok() {
|
let status =
|
||||||
params.status
|
if require_any_permission(&ctx, &["health.articles.manage", "health.articles.review"])
|
||||||
} else {
|
.is_ok()
|
||||||
Some("published".to_string())
|
{
|
||||||
};
|
params.status
|
||||||
|
} else {
|
||||||
|
Some("published".to_string())
|
||||||
|
};
|
||||||
let result = article_service::list_articles(
|
let result = article_service::list_articles(
|
||||||
&state, ctx.tenant_id, page, page_size,
|
&state,
|
||||||
params.category, status, params.category_id, params.tag_id, params.keyword,
|
ctx.tenant_id,
|
||||||
|
page,
|
||||||
|
page_size,
|
||||||
|
params.category,
|
||||||
|
status,
|
||||||
|
params.category_id,
|
||||||
|
params.tag_id,
|
||||||
|
params.keyword,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
@@ -45,7 +58,8 @@ where
|
|||||||
S: Clone + Send + Sync + 'static,
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.articles.list")?;
|
require_permission(&ctx, "health.articles.list")?;
|
||||||
let is_admin = require_any_permission(&ctx, &["health.articles.manage", "health.articles.review"]).is_ok();
|
let is_admin =
|
||||||
|
require_any_permission(&ctx, &["health.articles.manage", "health.articles.review"]).is_ok();
|
||||||
let result = article_service::get_article(&state, ctx.tenant_id, id, is_admin).await?;
|
let result = article_service::get_article(&state, ctx.tenant_id, id, is_admin).await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
@@ -61,9 +75,8 @@ where
|
|||||||
{
|
{
|
||||||
require_permission(&ctx, "health.articles.manage")?;
|
require_permission(&ctx, "health.articles.manage")?;
|
||||||
req.sanitize();
|
req.sanitize();
|
||||||
let result = article_service::create_article(
|
let result =
|
||||||
&state, ctx.tenant_id, Some(ctx.user_id), req.0,
|
article_service::create_article(&state, ctx.tenant_id, Some(ctx.user_id), req.0).await?;
|
||||||
).await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,9 +92,9 @@ where
|
|||||||
{
|
{
|
||||||
require_permission(&ctx, "health.articles.manage")?;
|
require_permission(&ctx, "health.articles.manage")?;
|
||||||
req.sanitize();
|
req.sanitize();
|
||||||
let result = article_service::update_article(
|
let result =
|
||||||
&state, ctx.tenant_id, id, Some(ctx.user_id), req.0,
|
article_service::update_article(&state, ctx.tenant_id, id, Some(ctx.user_id), req.0)
|
||||||
).await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +114,8 @@ where
|
|||||||
S: Clone + Send + Sync + 'static,
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.articles.manage")?;
|
require_permission(&ctx, "health.articles.manage")?;
|
||||||
article_service::delete_article(&state, ctx.tenant_id, id, Some(ctx.user_id), req.version).await?;
|
article_service::delete_article(&state, ctx.tenant_id, id, Some(ctx.user_id), req.version)
|
||||||
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(())))
|
Ok(Json(ApiResponse::ok(())))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,9 +140,9 @@ where
|
|||||||
S: Clone + Send + Sync + 'static,
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.articles.manage")?;
|
require_permission(&ctx, "health.articles.manage")?;
|
||||||
let result = article_service::submit_article(
|
let result =
|
||||||
&state, ctx.tenant_id, id, Some(ctx.user_id), req.version,
|
article_service::submit_article(&state, ctx.tenant_id, id, Some(ctx.user_id), req.version)
|
||||||
).await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,8 +161,14 @@ where
|
|||||||
req.sanitize();
|
req.sanitize();
|
||||||
let version = req.version.unwrap_or(0);
|
let version = req.version.unwrap_or(0);
|
||||||
let result = article_service::approve_article(
|
let result = article_service::approve_article(
|
||||||
&state, ctx.tenant_id, id, Some(ctx.user_id), req.0, version,
|
&state,
|
||||||
).await?;
|
ctx.tenant_id,
|
||||||
|
id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
req.0,
|
||||||
|
version,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,8 +187,14 @@ where
|
|||||||
req.sanitize();
|
req.sanitize();
|
||||||
let version = req.version.unwrap_or(0);
|
let version = req.version.unwrap_or(0);
|
||||||
let result = article_service::reject_article(
|
let result = article_service::reject_article(
|
||||||
&state, ctx.tenant_id, id, Some(ctx.user_id), req.0, version,
|
&state,
|
||||||
).await?;
|
ctx.tenant_id,
|
||||||
|
id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
req.0,
|
||||||
|
version,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,8 +211,13 @@ where
|
|||||||
{
|
{
|
||||||
require_permission(&ctx, "health.articles.manage")?;
|
require_permission(&ctx, "health.articles.manage")?;
|
||||||
let result = article_service::unpublish_article(
|
let result = article_service::unpublish_article(
|
||||||
&state, ctx.tenant_id, id, Some(ctx.user_id), req.version,
|
&state,
|
||||||
).await?;
|
ctx.tenant_id,
|
||||||
|
id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
req.version,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,7 +247,10 @@ pub async fn list_revisions<S>(
|
|||||||
Extension(ctx): Extension<TenantContext>,
|
Extension(ctx): Extension<TenantContext>,
|
||||||
Path(id): Path<uuid::Uuid>,
|
Path(id): Path<uuid::Uuid>,
|
||||||
Query(params): Query<ListRevisionsQuery>,
|
Query(params): Query<ListRevisionsQuery>,
|
||||||
) -> Result<Json<ApiResponse<PaginatedResponse<crate::dto::article_dto::ArticleRevisionResp>>>, AppError>
|
) -> Result<
|
||||||
|
Json<ApiResponse<PaginatedResponse<crate::dto::article_dto::ArticleRevisionResp>>>,
|
||||||
|
AppError,
|
||||||
|
>
|
||||||
where
|
where
|
||||||
HealthState: FromRef<S>,
|
HealthState: FromRef<S>,
|
||||||
S: Clone + Send + Sync + 'static,
|
S: Clone + Send + Sync + 'static,
|
||||||
@@ -224,8 +258,7 @@ where
|
|||||||
require_permission(&ctx, "health.articles.list")?;
|
require_permission(&ctx, "health.articles.list")?;
|
||||||
let page = params.page.unwrap_or(1);
|
let page = params.page.unwrap_or(1);
|
||||||
let page_size = params.page_size.unwrap_or(20);
|
let page_size = params.page_size.unwrap_or(20);
|
||||||
let result = article_service::list_revisions(
|
let result =
|
||||||
&state, ctx.tenant_id, id, page, page_size,
|
article_service::list_revisions(&state, ctx.tenant_id, id, page, page_size).await?;
|
||||||
).await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,9 +34,8 @@ where
|
|||||||
{
|
{
|
||||||
require_permission(&ctx, "health.articles.manage")?;
|
require_permission(&ctx, "health.articles.manage")?;
|
||||||
req.sanitize();
|
req.sanitize();
|
||||||
let result = article_tag_service::create_tag(
|
let result =
|
||||||
&state, ctx.tenant_id, Some(ctx.user_id), req.0,
|
article_tag_service::create_tag(&state, ctx.tenant_id, Some(ctx.user_id), req.0).await?;
|
||||||
).await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,9 +51,9 @@ where
|
|||||||
{
|
{
|
||||||
require_permission(&ctx, "health.articles.manage")?;
|
require_permission(&ctx, "health.articles.manage")?;
|
||||||
req.sanitize();
|
req.sanitize();
|
||||||
let result = article_tag_service::update_tag(
|
let result =
|
||||||
&state, ctx.tenant_id, id, Some(ctx.user_id), req.0,
|
article_tag_service::update_tag(&state, ctx.tenant_id, id, Some(ctx.user_id), req.0)
|
||||||
).await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,8 +73,7 @@ where
|
|||||||
S: Clone + Send + Sync + 'static,
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.articles.manage")?;
|
require_permission(&ctx, "health.articles.manage")?;
|
||||||
article_tag_service::delete_tag(
|
article_tag_service::delete_tag(&state, ctx.tenant_id, id, Some(ctx.user_id), req.version)
|
||||||
&state, ctx.tenant_id, id, Some(ctx.user_id), req.version,
|
.await?;
|
||||||
).await?;
|
|
||||||
Ok(Json(ApiResponse::ok(())))
|
Ok(Json(ApiResponse::ok(())))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
use axum::extract::{FromRef, Json, Path, Query, State};
|
|
||||||
use axum::Extension;
|
use axum::Extension;
|
||||||
|
use axum::extract::{FromRef, Json, Path, Query, State};
|
||||||
use erp_core::error::AppError;
|
use erp_core::error::AppError;
|
||||||
use erp_core::rbac::require_permission;
|
use erp_core::rbac::require_permission;
|
||||||
use erp_core::types::{ApiResponse, TenantContext};
|
use erp_core::types::{ApiResponse, TenantContext};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::dto::ble_gateway_dto::*;
|
|
||||||
use crate::dto::DeleteWithVersion;
|
use crate::dto::DeleteWithVersion;
|
||||||
|
use crate::dto::ble_gateway_dto::*;
|
||||||
use crate::gateway_auth::GatewayAuthContext;
|
use crate::gateway_auth::GatewayAuthContext;
|
||||||
use crate::service::ble_gateway_service;
|
use crate::service::ble_gateway_service;
|
||||||
use crate::state::HealthState;
|
use crate::state::HealthState;
|
||||||
@@ -54,8 +54,7 @@ where
|
|||||||
{
|
{
|
||||||
require_permission(&ctx, "health.ble-gateways.manage")?;
|
require_permission(&ctx, "health.ble-gateways.manage")?;
|
||||||
let result =
|
let result =
|
||||||
ble_gateway_service::create_gateway(&state, ctx.tenant_id, Some(ctx.user_id), body)
|
ble_gateway_service::create_gateway(&state, ctx.tenant_id, Some(ctx.user_id), body).await?;
|
||||||
.await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,14 +178,9 @@ where
|
|||||||
S: Clone + Send + Sync + 'static,
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.ble-gateways.manage")?;
|
require_permission(&ctx, "health.ble-gateways.manage")?;
|
||||||
let result = ble_gateway_service::batch_bind(
|
let result =
|
||||||
&state,
|
ble_gateway_service::batch_bind(&state, ctx.tenant_id, gateway_id, Some(ctx.user_id), body)
|
||||||
ctx.tenant_id,
|
.await?;
|
||||||
gateway_id,
|
|
||||||
Some(ctx.user_id),
|
|
||||||
body,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use axum::extract::{FromRef, Json, Path, Query, State};
|
|
||||||
use axum::Extension;
|
use axum::Extension;
|
||||||
|
use axum::extract::{FromRef, Json, Path, Query, State};
|
||||||
use erp_core::error::AppError;
|
use erp_core::error::AppError;
|
||||||
use erp_core::rbac::require_permission;
|
use erp_core::rbac::require_permission;
|
||||||
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
||||||
@@ -74,14 +74,9 @@ where
|
|||||||
{
|
{
|
||||||
require_permission(&ctx, "health.care-plan.manage")?;
|
require_permission(&ctx, "health.care-plan.manage")?;
|
||||||
req.data.sanitize();
|
req.data.sanitize();
|
||||||
let result = care_plan_service::update_care_plan(
|
let result =
|
||||||
&state,
|
care_plan_service::update_care_plan(&state, ctx.tenant_id, plan_id, Some(ctx.user_id), req)
|
||||||
ctx.tenant_id,
|
.await?;
|
||||||
plan_id,
|
|
||||||
Some(ctx.user_id),
|
|
||||||
req,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,14 +119,9 @@ where
|
|||||||
require_permission(&ctx, "health.care-plan.list")?;
|
require_permission(&ctx, "health.care-plan.list")?;
|
||||||
let page = params.page.unwrap_or(1);
|
let page = params.page.unwrap_or(1);
|
||||||
let page_size = params.page_size.unwrap_or(20);
|
let page_size = params.page_size.unwrap_or(20);
|
||||||
let result = care_plan_service::list_care_plan_items(
|
let result =
|
||||||
&state,
|
care_plan_service::list_care_plan_items(&state, ctx.tenant_id, plan_id, page, page_size)
|
||||||
ctx.tenant_id,
|
.await?;
|
||||||
plan_id,
|
|
||||||
page,
|
|
||||||
page_size,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,14 +212,9 @@ where
|
|||||||
require_permission(&ctx, "health.care-plan.list")?;
|
require_permission(&ctx, "health.care-plan.list")?;
|
||||||
let page = params.page.unwrap_or(1);
|
let page = params.page.unwrap_or(1);
|
||||||
let page_size = params.page_size.unwrap_or(20);
|
let page_size = params.page_size.unwrap_or(20);
|
||||||
let result = care_plan_service::list_care_plan_outcomes(
|
let result =
|
||||||
&state,
|
care_plan_service::list_care_plan_outcomes(&state, ctx.tenant_id, plan_id, page, page_size)
|
||||||
ctx.tenant_id,
|
.await?;
|
||||||
plan_id,
|
|
||||||
page,
|
|
||||||
page_size,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
use axum::Extension;
|
use axum::Extension;
|
||||||
use axum::extract::{FromRef, Json, Path, Query, State};
|
use axum::extract::{FromRef, Json, Path, Query, State};
|
||||||
use serde::Deserialize;
|
|
||||||
use erp_core::error::AppError;
|
use erp_core::error::AppError;
|
||||||
use erp_core::rbac::require_permission;
|
use erp_core::rbac::require_permission;
|
||||||
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::dto::consent_dto::*;
|
use crate::dto::consent_dto::*;
|
||||||
use crate::service::consent_service;
|
use crate::service::consent_service;
|
||||||
@@ -28,10 +28,8 @@ where
|
|||||||
require_permission(&ctx, "health.consent.list")?;
|
require_permission(&ctx, "health.consent.list")?;
|
||||||
let page = params.page.unwrap_or(1);
|
let page = params.page.unwrap_or(1);
|
||||||
let page_size = params.page_size.unwrap_or(20);
|
let page_size = params.page_size.unwrap_or(20);
|
||||||
let result = consent_service::list_consents(
|
let result =
|
||||||
&state, ctx.tenant_id, patient_id, page, page_size,
|
consent_service::list_consents(&state, ctx.tenant_id, patient_id, page, page_size).await?;
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,10 +45,8 @@ where
|
|||||||
require_permission(&ctx, "health.consent.manage")?;
|
require_permission(&ctx, "health.consent.manage")?;
|
||||||
let mut req = req;
|
let mut req = req;
|
||||||
req.sanitize();
|
req.sanitize();
|
||||||
let result = consent_service::grant_consent(
|
let result =
|
||||||
&state, ctx.tenant_id, Some(ctx.user_id), req,
|
consent_service::grant_consent(&state, ctx.tenant_id, Some(ctx.user_id), req).await?;
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,9 +63,8 @@ where
|
|||||||
require_permission(&ctx, "health.consent.manage")?;
|
require_permission(&ctx, "health.consent.manage")?;
|
||||||
let mut req = req;
|
let mut req = req;
|
||||||
req.sanitize();
|
req.sanitize();
|
||||||
let result = consent_service::revoke_consent(
|
let result =
|
||||||
&state, ctx.tenant_id, consent_id, Some(ctx.user_id), req,
|
consent_service::revoke_consent(&state, ctx.tenant_id, consent_id, Some(ctx.user_id), req)
|
||||||
)
|
.await?;
|
||||||
.await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,10 +60,8 @@ where
|
|||||||
S: Clone + Send + Sync + 'static,
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.consultation.manage")?;
|
require_permission(&ctx, "health.consultation.manage")?;
|
||||||
let result = consultation_service::create_session(
|
let result =
|
||||||
&state, ctx.tenant_id, Some(ctx.user_id), req,
|
consultation_service::create_session(&state, ctx.tenant_id, Some(ctx.user_id), req).await?;
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +78,12 @@ where
|
|||||||
let page = params.page.unwrap_or(1);
|
let page = params.page.unwrap_or(1);
|
||||||
let page_size = params.page_size.unwrap_or(20);
|
let page_size = params.page_size.unwrap_or(20);
|
||||||
let result = consultation_service::list_sessions(
|
let result = consultation_service::list_sessions(
|
||||||
&state, ctx.tenant_id, page, page_size, params.status, params.patient_id,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
page,
|
||||||
|
page_size,
|
||||||
|
params.status,
|
||||||
|
params.patient_id,
|
||||||
params.doctor_id,
|
params.doctor_id,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -115,7 +118,12 @@ where
|
|||||||
let page = params.page.unwrap_or(1);
|
let page = params.page.unwrap_or(1);
|
||||||
let page_size = params.page_size.unwrap_or(20);
|
let page_size = params.page_size.unwrap_or(20);
|
||||||
let result = consultation_service::list_messages(
|
let result = consultation_service::list_messages(
|
||||||
&state, ctx.tenant_id, session_id, page, page_size, params.after_id,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
session_id,
|
||||||
|
page,
|
||||||
|
page_size,
|
||||||
|
params.after_id,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
@@ -133,7 +141,11 @@ where
|
|||||||
{
|
{
|
||||||
require_permission(&ctx, "health.consultation.manage")?;
|
require_permission(&ctx, "health.consultation.manage")?;
|
||||||
let result = consultation_service::close_session(
|
let result = consultation_service::close_session(
|
||||||
&state, ctx.tenant_id, id, Some(ctx.user_id), req.version,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
req.version,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
@@ -166,7 +178,12 @@ where
|
|||||||
};
|
};
|
||||||
msg_req.sanitize();
|
msg_req.sanitize();
|
||||||
let result = consultation_service::create_message(
|
let result = consultation_service::create_message(
|
||||||
&state, ctx.tenant_id, Some(ctx.user_id), ctx.user_id, sender_role, msg_req,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
ctx.user_id,
|
||||||
|
sender_role,
|
||||||
|
msg_req,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
@@ -183,8 +200,13 @@ where
|
|||||||
{
|
{
|
||||||
require_permission(&ctx, "health.consultation.list")?;
|
require_permission(&ctx, "health.consultation.list")?;
|
||||||
let result = consultation_service::export_sessions(
|
let result = consultation_service::export_sessions(
|
||||||
&state, ctx.tenant_id, params.status, params.patient_id, params.doctor_id,
|
&state,
|
||||||
params.page, params.page_size,
|
ctx.tenant_id,
|
||||||
|
params.status,
|
||||||
|
params.patient_id,
|
||||||
|
params.doctor_id,
|
||||||
|
params.page,
|
||||||
|
params.page_size,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
@@ -219,10 +241,7 @@ where
|
|||||||
.map_err(|e| AppError::Internal(e.to_string()))?
|
.map_err(|e| AppError::Internal(e.to_string()))?
|
||||||
.is_some();
|
.is_some();
|
||||||
let role = if is_doctor { "doctor" } else { "patient" };
|
let role = if is_doctor { "doctor" } else { "patient" };
|
||||||
consultation_service::mark_session_read(
|
consultation_service::mark_session_read(&state, ctx.tenant_id, id, ctx.user_id, role).await?;
|
||||||
&state, ctx.tenant_id, id, ctx.user_id, role,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(Json(ApiResponse::ok(())))
|
Ok(Json(ApiResponse::ok(())))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,12 +263,13 @@ where
|
|||||||
S: Clone + Send + Sync + 'static,
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.consultation.list")?;
|
require_permission(&ctx, "health.consultation.list")?;
|
||||||
let mut result = consultation_service::get_doctor_dashboard(
|
let mut result =
|
||||||
&state, ctx.tenant_id, ctx.user_id,
|
consultation_service::get_doctor_dashboard(&state, ctx.tenant_id, ctx.user_id).await?;
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
consultation_service::enrich_doctor_dashboard_health(
|
consultation_service::enrich_doctor_dashboard_health(
|
||||||
&state, ctx.tenant_id, ctx.user_id, &mut result,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
ctx.user_id,
|
||||||
|
&mut result,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
use axum::Extension;
|
||||||
use axum::extract::{FromRef, Path, Query, State};
|
use axum::extract::{FromRef, Path, Query, State};
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use axum::Extension;
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use utoipa::IntoParams;
|
use utoipa::IntoParams;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@@ -31,16 +31,19 @@ where
|
|||||||
let page = query.page.unwrap_or(1);
|
let page = query.page.unwrap_or(1);
|
||||||
let page_size = query.page_size.unwrap_or(20);
|
let page_size = query.page_size.unwrap_or(20);
|
||||||
|
|
||||||
let (items, total) = critical_alert_service::list_pending_alerts(
|
let (items, total) =
|
||||||
&state, ctx.tenant_id, page, page_size,
|
critical_alert_service::list_pending_alerts(&state, ctx.tenant_id, page, page_size)
|
||||||
)
|
.await
|
||||||
.await
|
.map_err(|e| {
|
||||||
.map_err(|e| {
|
tracing::error!(error = %e, tenant_id = %ctx.tenant_id, "查询危急值告警列表失败");
|
||||||
tracing::error!(error = %e, tenant_id = %ctx.tenant_id, "查询危急值告警列表失败");
|
e
|
||||||
e
|
})?;
|
||||||
})?;
|
|
||||||
|
|
||||||
let total_pages = if page_size > 0 { total.div_ceil(page_size) } else { 0 };
|
let total_pages = if page_size > 0 {
|
||||||
|
total.div_ceil(page_size)
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
Ok(axum::Json(ApiResponse::ok(PaginatedResponse {
|
Ok(axum::Json(ApiResponse::ok(PaginatedResponse {
|
||||||
data: items,
|
data: items,
|
||||||
@@ -81,13 +84,9 @@ where
|
|||||||
S: Clone + Send + Sync + 'static,
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.critical-alerts.manage")?;
|
require_permission(&ctx, "health.critical-alerts.manage")?;
|
||||||
critical_alert_service::acknowledge_alert(
|
critical_alert_service::acknowledge_alert(&state, ctx.tenant_id, id, ctx.user_id, body.notes)
|
||||||
&state,
|
.await?;
|
||||||
ctx.tenant_id,
|
Ok(axum::Json(ApiResponse::ok(
|
||||||
id,
|
serde_json::json!({"message": "告警已确认"}),
|
||||||
ctx.user_id,
|
)))
|
||||||
body.notes,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(axum::Json(ApiResponse::ok(serde_json::json!({"message": "告警已确认"}))))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
use axum::Extension;
|
use axum::Extension;
|
||||||
use axum::extract::{FromRef, Json, Path, State};
|
use axum::extract::{FromRef, Json, Path, State};
|
||||||
use serde::Deserialize;
|
|
||||||
use erp_core::error::AppError;
|
use erp_core::error::AppError;
|
||||||
use erp_core::rbac::require_permission;
|
use erp_core::rbac::require_permission;
|
||||||
use erp_core::types::{ApiResponse, TenantContext};
|
use erp_core::types::{ApiResponse, TenantContext};
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::service::critical_value_threshold_service;
|
use crate::service::critical_value_threshold_service;
|
||||||
use crate::state::HealthState;
|
use crate::state::HealthState;
|
||||||
@@ -105,8 +105,13 @@ where
|
|||||||
S: Clone + Send + Sync + 'static,
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.critical-value-thresholds.manage")?;
|
require_permission(&ctx, "health.critical-value-thresholds.manage")?;
|
||||||
critical_value_threshold_service::delete_threshold(&state.db, ctx.tenant_id, id, Some(ctx.user_id))
|
critical_value_threshold_service::delete_threshold(
|
||||||
.await?;
|
&state.db,
|
||||||
|
ctx.tenant_id,
|
||||||
|
id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(())))
|
Ok(Json(ApiResponse::ok(())))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ use erp_core::error::AppError;
|
|||||||
use erp_core::rbac::require_permission;
|
use erp_core::rbac::require_permission;
|
||||||
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
||||||
|
|
||||||
use crate::dto::daily_monitoring_dto::*;
|
|
||||||
use crate::dto::DeleteWithVersion;
|
use crate::dto::DeleteWithVersion;
|
||||||
|
use crate::dto::daily_monitoring_dto::*;
|
||||||
use crate::service::daily_monitoring_service;
|
use crate::service::daily_monitoring_service;
|
||||||
use crate::state::HealthState;
|
use crate::state::HealthState;
|
||||||
|
|
||||||
@@ -40,7 +40,11 @@ where
|
|||||||
let page = params.page.unwrap_or(1);
|
let page = params.page.unwrap_or(1);
|
||||||
let page_size = params.page_size.unwrap_or(20);
|
let page_size = params.page_size.unwrap_or(20);
|
||||||
let result = daily_monitoring_service::list_daily_monitoring(
|
let result = daily_monitoring_service::list_daily_monitoring(
|
||||||
&state, ctx.tenant_id, patient_id, page, page_size,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
patient_id,
|
||||||
|
page,
|
||||||
|
page_size,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
@@ -56,10 +60,8 @@ where
|
|||||||
S: Clone + Send + Sync + 'static,
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.daily-monitoring.list")?;
|
require_permission(&ctx, "health.daily-monitoring.list")?;
|
||||||
let result = daily_monitoring_service::get_daily_monitoring(
|
let result =
|
||||||
&state, ctx.tenant_id, record_id,
|
daily_monitoring_service::get_daily_monitoring(&state, ctx.tenant_id, record_id).await?;
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +78,10 @@ where
|
|||||||
let mut req = req;
|
let mut req = req;
|
||||||
req.sanitize();
|
req.sanitize();
|
||||||
let result = daily_monitoring_service::create_daily_monitoring(
|
let result = daily_monitoring_service::create_daily_monitoring(
|
||||||
&state, ctx.tenant_id, Some(ctx.user_id), req,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
req,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
@@ -96,7 +101,12 @@ where
|
|||||||
let mut data = req.data;
|
let mut data = req.data;
|
||||||
data.sanitize();
|
data.sanitize();
|
||||||
let result = daily_monitoring_service::update_daily_monitoring(
|
let result = daily_monitoring_service::update_daily_monitoring(
|
||||||
&state, ctx.tenant_id, record_id, Some(ctx.user_id), data, req.version,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
record_id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
data,
|
||||||
|
req.version,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
@@ -114,7 +124,11 @@ where
|
|||||||
{
|
{
|
||||||
require_permission(&ctx, "health.daily-monitoring.manage")?;
|
require_permission(&ctx, "health.daily-monitoring.manage")?;
|
||||||
daily_monitoring_service::delete_daily_monitoring(
|
daily_monitoring_service::delete_daily_monitoring(
|
||||||
&state, ctx.tenant_id, record_id, Some(ctx.user_id), req.version,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
record_id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
req.version,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(())))
|
Ok(Json(ApiResponse::ok(())))
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
//! 设备管理 API — 设备列表查询与解绑
|
//! 设备管理 API — 设备列表查询与解绑
|
||||||
|
|
||||||
|
use axum::Extension;
|
||||||
use axum::extract::{FromRef, Path, Query, State};
|
use axum::extract::{FromRef, Path, Query, State};
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use axum::Extension;
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use utoipa::IntoParams;
|
use utoipa::IntoParams;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@@ -72,14 +72,8 @@ where
|
|||||||
{
|
{
|
||||||
require_permission(&ctx, "health.devices.manage")?;
|
require_permission(&ctx, "health.devices.manage")?;
|
||||||
|
|
||||||
let device = device_service::unbind_device(
|
let device =
|
||||||
&state,
|
device_service::unbind_device(&state, ctx.tenant_id, id, ctx.user_id, body.version).await?;
|
||||||
ctx.tenant_id,
|
|
||||||
id,
|
|
||||||
ctx.user_id,
|
|
||||||
body.version,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(axum::Json(ApiResponse::ok(device)))
|
Ok(axum::Json(ApiResponse::ok(device)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
use axum::Extension;
|
||||||
use axum::extract::{FromRef, Path, Query, State};
|
use axum::extract::{FromRef, Path, Query, State};
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use axum::Extension;
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use utoipa::IntoParams;
|
use utoipa::IntoParams;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@@ -44,9 +44,9 @@ where
|
|||||||
S: Clone + Send + Sync + 'static,
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.device-readings.manage")?;
|
require_permission(&ctx, "health.device-readings.manage")?;
|
||||||
let result = device_reading_service::batch_create_readings(
|
let result =
|
||||||
&state, ctx.tenant_id, path.patient_id, body,
|
device_reading_service::batch_create_readings(&state, ctx.tenant_id, path.patient_id, body)
|
||||||
).await?;
|
.await?;
|
||||||
Ok(axum::Json(ApiResponse::ok(result)))
|
Ok(axum::Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,9 +64,15 @@ where
|
|||||||
let page = query.page.unwrap_or(1);
|
let page = query.page.unwrap_or(1);
|
||||||
let page_size = query.page_size.unwrap_or(20);
|
let page_size = query.page_size.unwrap_or(20);
|
||||||
let result = device_reading_service::query_device_readings(
|
let result = device_reading_service::query_device_readings(
|
||||||
&state, ctx.tenant_id, path.patient_id,
|
&state,
|
||||||
query.device_type.as_deref(), query.hours, page, page_size,
|
ctx.tenant_id,
|
||||||
).await?;
|
path.patient_id,
|
||||||
|
query.device_type.as_deref(),
|
||||||
|
query.hours,
|
||||||
|
page,
|
||||||
|
page_size,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
Ok(axum::Json(ApiResponse::ok(result)))
|
Ok(axum::Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,8 +91,14 @@ where
|
|||||||
let page_size = query.page_size.unwrap_or(20);
|
let page_size = query.page_size.unwrap_or(20);
|
||||||
let days = query.days.unwrap_or(7);
|
let days = query.days.unwrap_or(7);
|
||||||
let result = device_reading_service::query_hourly_readings(
|
let result = device_reading_service::query_hourly_readings(
|
||||||
&state, ctx.tenant_id, path.patient_id,
|
&state,
|
||||||
&query.device_type, days, page, page_size,
|
ctx.tenant_id,
|
||||||
).await?;
|
path.patient_id,
|
||||||
|
&query.device_type,
|
||||||
|
days,
|
||||||
|
page,
|
||||||
|
page_size,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
Ok(axum::Json(ApiResponse::ok(result)))
|
Ok(axum::Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
use axum::Extension;
|
use axum::Extension;
|
||||||
use axum::extract::{FromRef, Json, Path, Query, State};
|
use axum::extract::{FromRef, Json, Path, Query, State};
|
||||||
use serde::Deserialize;
|
|
||||||
use erp_core::error::AppError;
|
use erp_core::error::AppError;
|
||||||
use erp_core::rbac::require_permission;
|
use erp_core::rbac::require_permission;
|
||||||
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::dto::diagnosis_dto::*;
|
|
||||||
use crate::dto::DeleteWithVersion;
|
use crate::dto::DeleteWithVersion;
|
||||||
|
use crate::dto::diagnosis_dto::*;
|
||||||
use crate::service::diagnosis_service;
|
use crate::service::diagnosis_service;
|
||||||
use crate::state::HealthState;
|
use crate::state::HealthState;
|
||||||
|
|
||||||
@@ -29,10 +29,9 @@ where
|
|||||||
require_permission(&ctx, "health.health-data.list")?;
|
require_permission(&ctx, "health.health-data.list")?;
|
||||||
let page = params.page.unwrap_or(1);
|
let page = params.page.unwrap_or(1);
|
||||||
let page_size = params.page_size.unwrap_or(20);
|
let page_size = params.page_size.unwrap_or(20);
|
||||||
let result = diagnosis_service::list_diagnoses(
|
let result =
|
||||||
&state, ctx.tenant_id, patient_id, page, page_size,
|
diagnosis_service::list_diagnoses(&state, ctx.tenant_id, patient_id, page, page_size)
|
||||||
)
|
.await?;
|
||||||
.await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +49,11 @@ where
|
|||||||
let mut req = req;
|
let mut req = req;
|
||||||
req.sanitize();
|
req.sanitize();
|
||||||
let result = diagnosis_service::create_diagnosis(
|
let result = diagnosis_service::create_diagnosis(
|
||||||
&state, ctx.tenant_id, patient_id, Some(ctx.user_id), req,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
patient_id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
req,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
@@ -70,7 +73,12 @@ where
|
|||||||
let mut data = req.data;
|
let mut data = req.data;
|
||||||
data.sanitize();
|
data.sanitize();
|
||||||
let result = diagnosis_service::update_diagnosis(
|
let result = diagnosis_service::update_diagnosis(
|
||||||
&state, ctx.tenant_id, diagnosis_id, Some(ctx.user_id), data, req.version,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
diagnosis_id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
data,
|
||||||
|
req.version,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
@@ -88,7 +96,11 @@ where
|
|||||||
{
|
{
|
||||||
require_permission(&ctx, "health.health-data.manage")?;
|
require_permission(&ctx, "health.health-data.manage")?;
|
||||||
diagnosis_service::delete_diagnosis(
|
diagnosis_service::delete_diagnosis(
|
||||||
&state, ctx.tenant_id, diagnosis_id, Some(ctx.user_id), req.version,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
diagnosis_id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
req.version,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(())))
|
Ok(Json(ApiResponse::ok(())))
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ use erp_core::error::AppError;
|
|||||||
use erp_core::rbac::require_permission;
|
use erp_core::rbac::require_permission;
|
||||||
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
||||||
|
|
||||||
use crate::dto::doctor_dto::*;
|
|
||||||
use crate::dto::DeleteWithVersion;
|
use crate::dto::DeleteWithVersion;
|
||||||
|
use crate::dto::doctor_dto::*;
|
||||||
use crate::service::doctor_service;
|
use crate::service::doctor_service;
|
||||||
use crate::state::HealthState;
|
use crate::state::HealthState;
|
||||||
|
|
||||||
@@ -42,7 +42,13 @@ where
|
|||||||
let page = params.page.unwrap_or(1);
|
let page = params.page.unwrap_or(1);
|
||||||
let page_size = params.page_size.unwrap_or(20);
|
let page_size = params.page_size.unwrap_or(20);
|
||||||
let result = doctor_service::list_doctors(
|
let result = doctor_service::list_doctors(
|
||||||
&state, ctx.tenant_id, page, page_size, params.search, params.department, params.title,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
page,
|
||||||
|
page_size,
|
||||||
|
params.search,
|
||||||
|
params.department,
|
||||||
|
params.title,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
@@ -60,10 +66,8 @@ where
|
|||||||
require_permission(&ctx, "health.doctor.manage")?;
|
require_permission(&ctx, "health.doctor.manage")?;
|
||||||
let mut req = req;
|
let mut req = req;
|
||||||
req.sanitize();
|
req.sanitize();
|
||||||
let result = doctor_service::create_doctor(
|
let result =
|
||||||
&state, ctx.tenant_id, Some(ctx.user_id), req,
|
doctor_service::create_doctor(&state, ctx.tenant_id, Some(ctx.user_id), req).await?;
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +99,12 @@ where
|
|||||||
let mut data = req.data;
|
let mut data = req.data;
|
||||||
data.sanitize();
|
data.sanitize();
|
||||||
let result = doctor_service::update_doctor(
|
let result = doctor_service::update_doctor(
|
||||||
&state, ctx.tenant_id, id, Some(ctx.user_id), data, req.version,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
data,
|
||||||
|
req.version,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
@@ -112,6 +121,7 @@ where
|
|||||||
S: Clone + Send + Sync + 'static,
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.doctor.manage")?;
|
require_permission(&ctx, "health.doctor.manage")?;
|
||||||
doctor_service::delete_doctor(&state, ctx.tenant_id, id, Some(ctx.user_id), req.version).await?;
|
doctor_service::delete_doctor(&state, ctx.tenant_id, id, Some(ctx.user_id), req.version)
|
||||||
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(())))
|
Ok(Json(ApiResponse::ok(())))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
//! 家庭成员健康代理 Handler — 同意管理 + 健康摘要查看
|
//! 家庭成员健康代理 Handler — 同意管理 + 健康摘要查看
|
||||||
|
|
||||||
use axum::extract::{Json, Path, Query, State};
|
|
||||||
use axum::Extension;
|
use axum::Extension;
|
||||||
|
use axum::extract::{Json, Path, Query, State};
|
||||||
use erp_core::error::AppError;
|
use erp_core::error::AppError;
|
||||||
use erp_core::rbac::require_permission;
|
use erp_core::rbac::require_permission;
|
||||||
use erp_core::types::{ApiResponse, TenantContext};
|
use erp_core::types::{ApiResponse, TenantContext};
|
||||||
@@ -27,9 +27,15 @@ pub async fn grant_family_access(
|
|||||||
) -> Result<Json<ApiResponse<FamilyMemberResp>>, AppError> {
|
) -> Result<Json<ApiResponse<FamilyMemberResp>>, AppError> {
|
||||||
require_permission(&ctx, "health.patient.manage")?;
|
require_permission(&ctx, "health.patient.manage")?;
|
||||||
let result = family_proxy_service::grant_family_access(
|
let result = family_proxy_service::grant_family_access(
|
||||||
&state, ctx.tenant_id, patient_id, family_member_id,
|
&state,
|
||||||
Some(ctx.user_id), req, params.version,
|
ctx.tenant_id,
|
||||||
).await?;
|
patient_id,
|
||||||
|
family_member_id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
req,
|
||||||
|
params.version,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,9 +48,14 @@ pub async fn revoke_family_access(
|
|||||||
) -> Result<Json<ApiResponse<FamilyMemberResp>>, AppError> {
|
) -> Result<Json<ApiResponse<FamilyMemberResp>>, AppError> {
|
||||||
require_permission(&ctx, "health.patient.manage")?;
|
require_permission(&ctx, "health.patient.manage")?;
|
||||||
let result = family_proxy_service::revoke_family_access(
|
let result = family_proxy_service::revoke_family_access(
|
||||||
&state, ctx.tenant_id, patient_id, family_member_id,
|
&state,
|
||||||
Some(ctx.user_id), params.version,
|
ctx.tenant_id,
|
||||||
).await?;
|
patient_id,
|
||||||
|
family_member_id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
params.version,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,9 +64,8 @@ pub async fn list_my_family_patients(
|
|||||||
State(state): State<HealthState>,
|
State(state): State<HealthState>,
|
||||||
Extension(ctx): Extension<TenantContext>,
|
Extension(ctx): Extension<TenantContext>,
|
||||||
) -> Result<Json<ApiResponse<Vec<FamilyPatientSummaryResp>>>, AppError> {
|
) -> Result<Json<ApiResponse<Vec<FamilyPatientSummaryResp>>>, AppError> {
|
||||||
let result = family_proxy_service::list_family_patients(
|
let result =
|
||||||
&state, ctx.tenant_id, ctx.user_id,
|
family_proxy_service::list_family_patients(&state, ctx.tenant_id, ctx.user_id).await?;
|
||||||
).await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,8 +76,12 @@ pub async fn get_family_health_summary(
|
|||||||
Path(patient_id): Path<Uuid>,
|
Path(patient_id): Path<Uuid>,
|
||||||
) -> Result<Json<ApiResponse<FamilyHealthSummaryResp>>, AppError> {
|
) -> Result<Json<ApiResponse<FamilyHealthSummaryResp>>, AppError> {
|
||||||
let result = family_proxy_service::get_family_health_summary(
|
let result = family_proxy_service::get_family_health_summary(
|
||||||
&state, ctx.tenant_id, ctx.user_id, patient_id,
|
&state,
|
||||||
).await?;
|
ctx.tenant_id,
|
||||||
|
ctx.user_id,
|
||||||
|
patient_id,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +92,11 @@ pub async fn link_family_member_user(
|
|||||||
Path(family_member_id): Path<Uuid>,
|
Path(family_member_id): Path<Uuid>,
|
||||||
) -> Result<Json<ApiResponse<FamilyMemberResp>>, AppError> {
|
) -> Result<Json<ApiResponse<FamilyMemberResp>>, AppError> {
|
||||||
let result = family_proxy_service::link_family_member_user(
|
let result = family_proxy_service::link_family_member_user(
|
||||||
&state, ctx.tenant_id, family_member_id, ctx.user_id,
|
&state,
|
||||||
).await?;
|
ctx.tenant_id,
|
||||||
|
family_member_id,
|
||||||
|
ctx.user_id,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ use erp_core::error::AppError;
|
|||||||
use erp_core::rbac::require_permission;
|
use erp_core::rbac::require_permission;
|
||||||
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
||||||
|
|
||||||
use crate::dto::follow_up_dto::*;
|
|
||||||
use crate::dto::DeleteWithVersion;
|
use crate::dto::DeleteWithVersion;
|
||||||
|
use crate::dto::follow_up_dto::*;
|
||||||
use crate::service::follow_up_service;
|
use crate::service::follow_up_service;
|
||||||
use crate::state::HealthState;
|
use crate::state::HealthState;
|
||||||
|
|
||||||
@@ -33,10 +33,9 @@ where
|
|||||||
if req.patient_ids.len() > 100 {
|
if req.patient_ids.len() > 100 {
|
||||||
return Err(AppError::Validation("单次批量最多 100 条".to_string()));
|
return Err(AppError::Validation("单次批量最多 100 条".to_string()));
|
||||||
}
|
}
|
||||||
let result = follow_up_service::batch_create_tasks(
|
let result =
|
||||||
&state, ctx.tenant_id, Some(ctx.user_id), req,
|
follow_up_service::batch_create_tasks(&state, ctx.tenant_id, Some(ctx.user_id), req)
|
||||||
)
|
.await?;
|
||||||
.await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,10 +55,9 @@ where
|
|||||||
if req.task_ids.len() > 100 {
|
if req.task_ids.len() > 100 {
|
||||||
return Err(AppError::Validation("单次批量最多 100 条".to_string()));
|
return Err(AppError::Validation("单次批量最多 100 条".to_string()));
|
||||||
}
|
}
|
||||||
let result = follow_up_service::batch_assign_tasks(
|
let result =
|
||||||
&state, ctx.tenant_id, Some(ctx.user_id), req,
|
follow_up_service::batch_assign_tasks(&state, ctx.tenant_id, Some(ctx.user_id), req)
|
||||||
)
|
.await?;
|
||||||
.await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,10 +77,9 @@ where
|
|||||||
if req.task_ids.len() > 100 {
|
if req.task_ids.len() > 100 {
|
||||||
return Err(AppError::Validation("单次批量最多 100 条".to_string()));
|
return Err(AppError::Validation("单次批量最多 100 条".to_string()));
|
||||||
}
|
}
|
||||||
let result = follow_up_service::batch_complete_tasks(
|
let result =
|
||||||
&state, ctx.tenant_id, Some(ctx.user_id), req,
|
follow_up_service::batch_complete_tasks(&state, ctx.tenant_id, Some(ctx.user_id), req)
|
||||||
)
|
.await?;
|
||||||
.await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +120,12 @@ where
|
|||||||
let page = params.page.unwrap_or(1);
|
let page = params.page.unwrap_or(1);
|
||||||
let page_size = params.page_size.unwrap_or(20);
|
let page_size = params.page_size.unwrap_or(20);
|
||||||
let result = follow_up_service::list_tasks(
|
let result = follow_up_service::list_tasks(
|
||||||
&state, ctx.tenant_id, page, page_size, params.patient_id, params.assigned_to,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
page,
|
||||||
|
page_size,
|
||||||
|
params.patient_id,
|
||||||
|
params.assigned_to,
|
||||||
params.status,
|
params.status,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -156,10 +158,8 @@ where
|
|||||||
require_permission(&ctx, "health.follow-up.manage")?;
|
require_permission(&ctx, "health.follow-up.manage")?;
|
||||||
let mut req = req;
|
let mut req = req;
|
||||||
req.sanitize();
|
req.sanitize();
|
||||||
let result = follow_up_service::create_task(
|
let result =
|
||||||
&state, ctx.tenant_id, Some(ctx.user_id), req,
|
follow_up_service::create_task(&state, ctx.tenant_id, Some(ctx.user_id), req).await?;
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +177,12 @@ where
|
|||||||
let mut data = req.data;
|
let mut data = req.data;
|
||||||
data.sanitize();
|
data.sanitize();
|
||||||
let result = follow_up_service::update_task(
|
let result = follow_up_service::update_task(
|
||||||
&state, ctx.tenant_id, id, Some(ctx.user_id), data, req.version,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
data,
|
||||||
|
req.version,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
@@ -194,7 +199,8 @@ where
|
|||||||
S: Clone + Send + Sync + 'static,
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.follow-up.manage")?;
|
require_permission(&ctx, "health.follow-up.manage")?;
|
||||||
follow_up_service::delete_task(&state, ctx.tenant_id, id, Some(ctx.user_id), req.version).await?;
|
follow_up_service::delete_task(&state, ctx.tenant_id, id, Some(ctx.user_id), req.version)
|
||||||
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(())))
|
Ok(Json(ApiResponse::ok(())))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,14 +216,14 @@ where
|
|||||||
{
|
{
|
||||||
require_permission(&ctx, "health.follow-up.manage")?;
|
require_permission(&ctx, "health.follow-up.manage")?;
|
||||||
if req.task_id != task_id {
|
if req.task_id != task_id {
|
||||||
return Err(AppError::Validation("路径中的 task_id 与请求体不一致".to_string()));
|
return Err(AppError::Validation(
|
||||||
|
"路径中的 task_id 与请求体不一致".to_string(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
let mut req = req;
|
let mut req = req;
|
||||||
req.sanitize();
|
req.sanitize();
|
||||||
let result = follow_up_service::create_record(
|
let result =
|
||||||
&state, ctx.tenant_id, Some(ctx.user_id), req,
|
follow_up_service::create_record(&state, ctx.tenant_id, Some(ctx.user_id), req).await?;
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,7 +240,12 @@ where
|
|||||||
let page = params.page.unwrap_or(1);
|
let page = params.page.unwrap_or(1);
|
||||||
let page_size = params.page_size.unwrap_or(20);
|
let page_size = params.page_size.unwrap_or(20);
|
||||||
let result = follow_up_service::list_records(
|
let result = follow_up_service::list_records(
|
||||||
&state, ctx.tenant_id, page, page_size, params.task_id, params.patient_id,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
page,
|
||||||
|
page_size,
|
||||||
|
params.task_id,
|
||||||
|
params.patient_id,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ use erp_core::error::AppError;
|
|||||||
use erp_core::rbac::require_permission;
|
use erp_core::rbac::require_permission;
|
||||||
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
||||||
|
|
||||||
use crate::dto::follow_up_template_dto::*;
|
|
||||||
use crate::dto::DeleteWithVersion;
|
use crate::dto::DeleteWithVersion;
|
||||||
|
use crate::dto::follow_up_template_dto::*;
|
||||||
use crate::service::follow_up_template_service;
|
use crate::service::follow_up_template_service;
|
||||||
use crate::state::HealthState;
|
use crate::state::HealthState;
|
||||||
|
|
||||||
@@ -41,7 +41,12 @@ where
|
|||||||
let page = params.page.unwrap_or(1);
|
let page = params.page.unwrap_or(1);
|
||||||
let page_size = params.page_size.unwrap_or(20);
|
let page_size = params.page_size.unwrap_or(20);
|
||||||
let result = follow_up_template_service::list_templates(
|
let result = follow_up_template_service::list_templates(
|
||||||
&state, ctx.tenant_id, page, page_size, params.follow_up_type, params.status,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
page,
|
||||||
|
page_size,
|
||||||
|
params.follow_up_type,
|
||||||
|
params.status,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
@@ -73,10 +78,9 @@ where
|
|||||||
require_permission(&ctx, "health.follow-up-templates.manage")?;
|
require_permission(&ctx, "health.follow-up-templates.manage")?;
|
||||||
let mut req = req;
|
let mut req = req;
|
||||||
req.sanitize();
|
req.sanitize();
|
||||||
let result = follow_up_template_service::create_template(
|
let result =
|
||||||
&state, ctx.tenant_id, Some(ctx.user_id), req,
|
follow_up_template_service::create_template(&state, ctx.tenant_id, Some(ctx.user_id), req)
|
||||||
)
|
.await?;
|
||||||
.await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +98,12 @@ where
|
|||||||
let mut data = req.data;
|
let mut data = req.data;
|
||||||
data.sanitize();
|
data.sanitize();
|
||||||
let result = follow_up_template_service::update_template(
|
let result = follow_up_template_service::update_template(
|
||||||
&state, ctx.tenant_id, id, Some(ctx.user_id), data, req.version,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
data,
|
||||||
|
req.version,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
@@ -112,7 +121,11 @@ where
|
|||||||
{
|
{
|
||||||
require_permission(&ctx, "health.follow-up-templates.manage")?;
|
require_permission(&ctx, "health.follow-up-templates.manage")?;
|
||||||
follow_up_template_service::delete_template(
|
follow_up_template_service::delete_template(
|
||||||
&state, ctx.tenant_id, id, Some(ctx.user_id), req.version,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
req.version,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(())))
|
Ok(Json(ApiResponse::ok(())))
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ use erp_core::error::AppError;
|
|||||||
use erp_core::rbac::require_permission;
|
use erp_core::rbac::require_permission;
|
||||||
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
||||||
|
|
||||||
use crate::dto::health_data_dto::*;
|
|
||||||
use crate::dto::DeleteWithVersion;
|
use crate::dto::DeleteWithVersion;
|
||||||
|
use crate::dto::health_data_dto::*;
|
||||||
use crate::service::health_data_service;
|
use crate::service::health_data_service;
|
||||||
use crate::service::trend_service;
|
use crate::service::trend_service;
|
||||||
use crate::state::HealthState;
|
use crate::state::HealthState;
|
||||||
@@ -59,10 +59,9 @@ where
|
|||||||
require_permission(&ctx, "health.health-data.list")?;
|
require_permission(&ctx, "health.health-data.list")?;
|
||||||
let page = params.page.unwrap_or(1);
|
let page = params.page.unwrap_or(1);
|
||||||
let page_size = params.page_size.unwrap_or(20);
|
let page_size = params.page_size.unwrap_or(20);
|
||||||
let result = health_data_service::list_vital_signs(
|
let result =
|
||||||
&state, ctx.tenant_id, patient_id, page, page_size,
|
health_data_service::list_vital_signs(&state, ctx.tenant_id, patient_id, page, page_size)
|
||||||
)
|
.await?;
|
||||||
.await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +79,11 @@ where
|
|||||||
let mut req = req;
|
let mut req = req;
|
||||||
req.sanitize();
|
req.sanitize();
|
||||||
let result = health_data_service::create_vital_signs(
|
let result = health_data_service::create_vital_signs(
|
||||||
&state, ctx.tenant_id, patient_id, Some(ctx.user_id), req,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
patient_id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
req,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
@@ -100,7 +103,13 @@ where
|
|||||||
let mut data = req.data;
|
let mut data = req.data;
|
||||||
data.sanitize();
|
data.sanitize();
|
||||||
let result = health_data_service::update_vital_signs(
|
let result = health_data_service::update_vital_signs(
|
||||||
&state, ctx.tenant_id, patient_id, vid, Some(ctx.user_id), data, req.version,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
patient_id,
|
||||||
|
vid,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
data,
|
||||||
|
req.version,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
@@ -117,7 +126,14 @@ where
|
|||||||
S: Clone + Send + Sync + 'static,
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.health-data.manage")?;
|
require_permission(&ctx, "health.health-data.manage")?;
|
||||||
health_data_service::delete_vital_signs(&state, ctx.tenant_id, vid, Some(ctx.user_id), req.version).await?;
|
health_data_service::delete_vital_signs(
|
||||||
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
vid,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
req.version,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(())))
|
Ok(Json(ApiResponse::ok(())))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,10 +154,9 @@ where
|
|||||||
require_permission(&ctx, "health.health-data.list")?;
|
require_permission(&ctx, "health.health-data.list")?;
|
||||||
let page = params.page.unwrap_or(1);
|
let page = params.page.unwrap_or(1);
|
||||||
let page_size = params.page_size.unwrap_or(20);
|
let page_size = params.page_size.unwrap_or(20);
|
||||||
let result = health_data_service::list_lab_reports(
|
let result =
|
||||||
&state, ctx.tenant_id, patient_id, page, page_size,
|
health_data_service::list_lab_reports(&state, ctx.tenant_id, patient_id, page, page_size)
|
||||||
)
|
.await?;
|
||||||
.await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,7 +174,11 @@ where
|
|||||||
let mut req = req;
|
let mut req = req;
|
||||||
req.sanitize();
|
req.sanitize();
|
||||||
let result = health_data_service::create_lab_report(
|
let result = health_data_service::create_lab_report(
|
||||||
&state, ctx.tenant_id, patient_id, Some(ctx.user_id), req,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
patient_id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
req,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
@@ -179,7 +198,13 @@ where
|
|||||||
let mut data = req.data;
|
let mut data = req.data;
|
||||||
data.sanitize();
|
data.sanitize();
|
||||||
let result = health_data_service::update_lab_report(
|
let result = health_data_service::update_lab_report(
|
||||||
&state, ctx.tenant_id, _patient_id, rid, Some(ctx.user_id), data, req.version,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
_patient_id,
|
||||||
|
rid,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
data,
|
||||||
|
req.version,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
@@ -196,7 +221,14 @@ where
|
|||||||
S: Clone + Send + Sync + 'static,
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.health-data.manage")?;
|
require_permission(&ctx, "health.health-data.manage")?;
|
||||||
health_data_service::delete_lab_report(&state, ctx.tenant_id, rid, Some(ctx.user_id), req.version).await?;
|
health_data_service::delete_lab_report(
|
||||||
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
rid,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
req.version,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(())))
|
Ok(Json(ApiResponse::ok(())))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,7 +246,13 @@ where
|
|||||||
let mut data = req.data;
|
let mut data = req.data;
|
||||||
data.sanitize();
|
data.sanitize();
|
||||||
let result = health_data_service::review_lab_report(
|
let result = health_data_service::review_lab_report(
|
||||||
&state, ctx.tenant_id, _patient_id, rid, ctx.user_id, data, req.version,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
_patient_id,
|
||||||
|
rid,
|
||||||
|
ctx.user_id,
|
||||||
|
data,
|
||||||
|
req.version,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
@@ -238,7 +276,11 @@ where
|
|||||||
let page = params.page.unwrap_or(1);
|
let page = params.page.unwrap_or(1);
|
||||||
let page_size = params.page_size.unwrap_or(20);
|
let page_size = params.page_size.unwrap_or(20);
|
||||||
let result = health_data_service::list_health_records(
|
let result = health_data_service::list_health_records(
|
||||||
&state, ctx.tenant_id, patient_id, page, page_size,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
patient_id,
|
||||||
|
page,
|
||||||
|
page_size,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
@@ -258,7 +300,11 @@ where
|
|||||||
let mut req = req;
|
let mut req = req;
|
||||||
req.sanitize();
|
req.sanitize();
|
||||||
let result = health_data_service::create_health_record(
|
let result = health_data_service::create_health_record(
|
||||||
&state, ctx.tenant_id, patient_id, Some(ctx.user_id), req,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
patient_id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
req,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
@@ -278,7 +324,13 @@ where
|
|||||||
let mut data = req.data;
|
let mut data = req.data;
|
||||||
data.sanitize();
|
data.sanitize();
|
||||||
let result = health_data_service::update_health_record(
|
let result = health_data_service::update_health_record(
|
||||||
&state, ctx.tenant_id, patient_id, rid, Some(ctx.user_id), data, req.version,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
patient_id,
|
||||||
|
rid,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
data,
|
||||||
|
req.version,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
@@ -295,7 +347,14 @@ where
|
|||||||
S: Clone + Send + Sync + 'static,
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.health-data.manage")?;
|
require_permission(&ctx, "health.health-data.manage")?;
|
||||||
health_data_service::delete_health_record(&state, ctx.tenant_id, rid, Some(ctx.user_id), req.version).await?;
|
health_data_service::delete_health_record(
|
||||||
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
rid,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
req.version,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(())))
|
Ok(Json(ApiResponse::ok(())))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,10 +375,8 @@ where
|
|||||||
require_permission(&ctx, "health.health-data.list")?;
|
require_permission(&ctx, "health.health-data.list")?;
|
||||||
let page = params.page.unwrap_or(1);
|
let page = params.page.unwrap_or(1);
|
||||||
let page_size = params.page_size.unwrap_or(20);
|
let page_size = params.page_size.unwrap_or(20);
|
||||||
let result = trend_service::list_trends(
|
let result =
|
||||||
&state, ctx.tenant_id, patient_id, page, page_size,
|
trend_service::list_trends(&state, ctx.tenant_id, patient_id, page, page_size).await?;
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,7 +392,12 @@ where
|
|||||||
{
|
{
|
||||||
require_permission(&ctx, "health.health-data.manage")?;
|
require_permission(&ctx, "health.health-data.manage")?;
|
||||||
let result = trend_service::generate_trend(
|
let result = trend_service::generate_trend(
|
||||||
&state, ctx.tenant_id, patient_id, Some(ctx.user_id), req.period_start, req.period_end,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
patient_id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
req.period_start,
|
||||||
|
req.period_end,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
@@ -353,7 +415,12 @@ where
|
|||||||
{
|
{
|
||||||
require_permission(&ctx, "health.health-data.list")?;
|
require_permission(&ctx, "health.health-data.list")?;
|
||||||
let result = trend_service::get_indicator_timeseries(
|
let result = trend_service::get_indicator_timeseries(
|
||||||
&state, ctx.tenant_id, patient_id, indicator, params.start_date, params.end_date,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
patient_id,
|
||||||
|
indicator,
|
||||||
|
params.start_date,
|
||||||
|
params.end_date,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
@@ -374,7 +441,11 @@ where
|
|||||||
{
|
{
|
||||||
require_permission(&ctx, "health.health-data.list")?;
|
require_permission(&ctx, "health.health-data.list")?;
|
||||||
let result = trend_service::get_mini_trend(
|
let result = trend_service::get_mini_trend(
|
||||||
&state, ctx.tenant_id, ctx.user_id, params.indicator, params.range,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
ctx.user_id,
|
||||||
|
params.indicator,
|
||||||
|
params.range,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
@@ -394,10 +465,9 @@ where
|
|||||||
S: Clone + Send + Sync + 'static,
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.health-data.list")?;
|
require_permission(&ctx, "health.health-data.list")?;
|
||||||
let result = trend_service::get_mini_today(
|
let result =
|
||||||
&state, ctx.tenant_id, ctx.user_id, params.patient_id,
|
trend_service::get_mini_today(&state, ctx.tenant_id, ctx.user_id, params.patient_id)
|
||||||
)
|
.await?;
|
||||||
.await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
use axum::Extension;
|
use axum::Extension;
|
||||||
use axum::extract::{FromRef, Json, Path, Query, State};
|
use axum::extract::{FromRef, Json, Path, Query, State};
|
||||||
use serde::Deserialize;
|
|
||||||
use erp_core::error::AppError;
|
use erp_core::error::AppError;
|
||||||
use erp_core::rbac::require_permission;
|
use erp_core::rbac::require_permission;
|
||||||
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::dto::medication_record_dto::*;
|
|
||||||
use crate::dto::DeleteWithVersion;
|
use crate::dto::DeleteWithVersion;
|
||||||
|
use crate::dto::medication_record_dto::*;
|
||||||
use crate::service::medication_record_service;
|
use crate::service::medication_record_service;
|
||||||
use crate::state::HealthState;
|
use crate::state::HealthState;
|
||||||
|
|
||||||
@@ -31,7 +31,11 @@ where
|
|||||||
let page = params.page.unwrap_or(1);
|
let page = params.page.unwrap_or(1);
|
||||||
let page_size = params.page_size.unwrap_or(20);
|
let page_size = params.page_size.unwrap_or(20);
|
||||||
let result = medication_record_service::list_medications(
|
let result = medication_record_service::list_medications(
|
||||||
&state, ctx.tenant_id, patient_id, page, page_size,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
patient_id,
|
||||||
|
page,
|
||||||
|
page_size,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
@@ -66,13 +70,9 @@ where
|
|||||||
require_permission(&ctx, "health.medication-records.manage")?;
|
require_permission(&ctx, "health.medication-records.manage")?;
|
||||||
let mut req = req;
|
let mut req = req;
|
||||||
req.sanitize();
|
req.sanitize();
|
||||||
let result = medication_record_service::create_medication(
|
let result =
|
||||||
&state,
|
medication_record_service::create_medication(&state, ctx.tenant_id, Some(ctx.user_id), req)
|
||||||
ctx.tenant_id,
|
.await?;
|
||||||
Some(ctx.user_id),
|
|
||||||
req,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ use erp_core::error::AppError;
|
|||||||
use erp_core::rbac::require_permission;
|
use erp_core::rbac::require_permission;
|
||||||
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
||||||
|
|
||||||
use crate::dto::medication_reminder_dto::{CreateMedicationReminderReq, MedicationReminderResp, UpdateMedicationReminderReq};
|
use crate::dto::medication_reminder_dto::{
|
||||||
|
CreateMedicationReminderReq, MedicationReminderResp, UpdateMedicationReminderReq,
|
||||||
|
};
|
||||||
use crate::service::medication_reminder_service;
|
use crate::service::medication_reminder_service;
|
||||||
use crate::state::HealthState;
|
use crate::state::HealthState;
|
||||||
|
|
||||||
@@ -28,8 +30,13 @@ where
|
|||||||
let page = params.page.unwrap_or(1);
|
let page = params.page.unwrap_or(1);
|
||||||
let page_size = params.page_size.unwrap_or(20);
|
let page_size = params.page_size.unwrap_or(20);
|
||||||
let result = medication_reminder_service::list_reminders(
|
let result = medication_reminder_service::list_reminders(
|
||||||
&state, ctx.tenant_id, patient_id, page, page_size,
|
&state,
|
||||||
).await?;
|
ctx.tenant_id,
|
||||||
|
patient_id,
|
||||||
|
page,
|
||||||
|
page_size,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,8 +52,12 @@ where
|
|||||||
require_permission(&ctx, "health.medication-reminders.manage")?;
|
require_permission(&ctx, "health.medication-reminders.manage")?;
|
||||||
req.sanitize();
|
req.sanitize();
|
||||||
let result = medication_reminder_service::create_reminder(
|
let result = medication_reminder_service::create_reminder(
|
||||||
&state, ctx.tenant_id, Some(ctx.user_id), req.0,
|
&state,
|
||||||
).await?;
|
ctx.tenant_id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
req.0,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,8 +82,14 @@ where
|
|||||||
let mut data = req.data;
|
let mut data = req.data;
|
||||||
data.sanitize();
|
data.sanitize();
|
||||||
let result = medication_reminder_service::update_reminder(
|
let result = medication_reminder_service::update_reminder(
|
||||||
&state, ctx.tenant_id, id, Some(ctx.user_id), req.version, data,
|
&state,
|
||||||
).await?;
|
ctx.tenant_id,
|
||||||
|
id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
req.version,
|
||||||
|
data,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +110,12 @@ where
|
|||||||
{
|
{
|
||||||
require_permission(&ctx, "health.medication-reminders.manage")?;
|
require_permission(&ctx, "health.medication-reminders.manage")?;
|
||||||
medication_reminder_service::delete_reminder(
|
medication_reminder_service::delete_reminder(
|
||||||
&state, ctx.tenant_id, id, Some(ctx.user_id), req.version,
|
&state,
|
||||||
).await?;
|
ctx.tenant_id,
|
||||||
|
id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
req.version,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(())))
|
Ok(Json(ApiResponse::ok(())))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
pub mod action_inbox_handler;
|
pub mod action_inbox_handler;
|
||||||
pub mod alert_handler;
|
pub mod alert_handler;
|
||||||
pub mod ble_gateway_handler;
|
|
||||||
pub mod alert_rule_handler;
|
pub mod alert_rule_handler;
|
||||||
pub mod appointment_handler;
|
pub mod appointment_handler;
|
||||||
pub mod article_category_handler;
|
pub mod article_category_handler;
|
||||||
pub mod article_handler;
|
pub mod article_handler;
|
||||||
pub mod article_tag_handler;
|
pub mod article_tag_handler;
|
||||||
|
pub mod ble_gateway_handler;
|
||||||
pub mod care_plan_handler;
|
pub mod care_plan_handler;
|
||||||
pub mod consultation_handler;
|
|
||||||
pub mod consent_handler;
|
pub mod consent_handler;
|
||||||
|
pub mod consultation_handler;
|
||||||
pub mod critical_alert_handler;
|
pub mod critical_alert_handler;
|
||||||
pub mod critical_value_threshold_handler;
|
pub mod critical_value_threshold_handler;
|
||||||
pub mod daily_monitoring_handler;
|
pub mod daily_monitoring_handler;
|
||||||
pub mod device_handler;
|
pub mod device_handler;
|
||||||
pub mod device_reading_handler;
|
pub mod device_reading_handler;
|
||||||
pub mod diagnosis_handler;
|
pub mod diagnosis_handler;
|
||||||
pub mod family_proxy_handler;
|
|
||||||
pub mod medication_record_handler;
|
|
||||||
pub mod medication_reminder_handler;
|
|
||||||
pub mod doctor_handler;
|
pub mod doctor_handler;
|
||||||
|
pub mod family_proxy_handler;
|
||||||
pub mod follow_up_handler;
|
pub mod follow_up_handler;
|
||||||
pub mod follow_up_template_handler;
|
pub mod follow_up_template_handler;
|
||||||
pub mod health_data_handler;
|
pub mod health_data_handler;
|
||||||
|
pub mod medication_record_handler;
|
||||||
|
pub mod medication_reminder_handler;
|
||||||
pub mod patient_handler;
|
pub mod patient_handler;
|
||||||
pub mod points_handler;
|
pub mod points_handler;
|
||||||
pub mod stats_handler;
|
|
||||||
pub mod shift_handler;
|
pub mod shift_handler;
|
||||||
|
pub mod stats_handler;
|
||||||
pub mod vital_signs_daily_handler;
|
pub mod vital_signs_daily_handler;
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ use erp_core::error::AppError;
|
|||||||
use erp_core::rbac::require_permission;
|
use erp_core::rbac::require_permission;
|
||||||
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
|
||||||
|
|
||||||
|
use crate::dto::DeleteWithVersion;
|
||||||
use crate::dto::patient_dto::{
|
use crate::dto::patient_dto::{
|
||||||
CreatePatientReq, FamilyMemberReq, FamilyMemberResp, ManageTagsReq, PatientResp,
|
CreatePatientReq, FamilyMemberReq, FamilyMemberResp, ManageTagsReq, PatientResp,
|
||||||
UpdatePatientReq,
|
UpdatePatientReq,
|
||||||
};
|
};
|
||||||
use crate::dto::DeleteWithVersion;
|
|
||||||
use crate::service::patient_service;
|
use crate::service::patient_service;
|
||||||
use crate::state::HealthState;
|
use crate::state::HealthState;
|
||||||
|
|
||||||
@@ -44,7 +44,12 @@ where
|
|||||||
let page = params.page.unwrap_or(1);
|
let page = params.page.unwrap_or(1);
|
||||||
let page_size = params.page_size.unwrap_or(20);
|
let page_size = params.page_size.unwrap_or(20);
|
||||||
let result = patient_service::list_patients(
|
let result = patient_service::list_patients(
|
||||||
&state, ctx.tenant_id, page, page_size, params.search, params.tag_id,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
page,
|
||||||
|
page_size,
|
||||||
|
params.search,
|
||||||
|
params.tag_id,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
@@ -62,10 +67,11 @@ where
|
|||||||
require_permission(&ctx, "health.patient.manage")?;
|
require_permission(&ctx, "health.patient.manage")?;
|
||||||
let mut req = req;
|
let mut req = req;
|
||||||
req.sanitize();
|
req.sanitize();
|
||||||
let result = patient_service::create_patient(
|
if req.name.trim().is_empty() {
|
||||||
&state, ctx.tenant_id, Some(ctx.user_id), req,
|
return Err(AppError::Validation("患者姓名不能为空".into()));
|
||||||
)
|
}
|
||||||
.await?;
|
let result =
|
||||||
|
patient_service::create_patient(&state, ctx.tenant_id, Some(ctx.user_id), req).await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +118,12 @@ where
|
|||||||
};
|
};
|
||||||
update.sanitize();
|
update.sanitize();
|
||||||
let result = patient_service::update_patient(
|
let result = patient_service::update_patient(
|
||||||
&state, ctx.tenant_id, id, Some(ctx.user_id), update, version,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
update,
|
||||||
|
version,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
@@ -129,7 +140,8 @@ where
|
|||||||
S: Clone + Send + Sync + 'static,
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.patient.manage")?;
|
require_permission(&ctx, "health.patient.manage")?;
|
||||||
patient_service::delete_patient(&state, ctx.tenant_id, id, Some(ctx.user_id), req.version).await?;
|
patient_service::delete_patient(&state, ctx.tenant_id, id, Some(ctx.user_id), req.version)
|
||||||
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(())))
|
Ok(Json(ApiResponse::ok(())))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,10 +201,9 @@ where
|
|||||||
require_permission(&ctx, "health.patient.manage")?;
|
require_permission(&ctx, "health.patient.manage")?;
|
||||||
let mut req = req;
|
let mut req = req;
|
||||||
req.sanitize();
|
req.sanitize();
|
||||||
let result = patient_service::create_family_member(
|
let result =
|
||||||
&state, ctx.tenant_id, id, Some(ctx.user_id), req,
|
patient_service::create_family_member(&state, ctx.tenant_id, id, Some(ctx.user_id), req)
|
||||||
)
|
.await?;
|
||||||
.await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,7 +228,13 @@ where
|
|||||||
};
|
};
|
||||||
update.sanitize();
|
update.sanitize();
|
||||||
let result = patient_service::update_family_member(
|
let result = patient_service::update_family_member(
|
||||||
&state, ctx.tenant_id, _patient_id, member_id, Some(ctx.user_id), update, version,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
_patient_id,
|
||||||
|
member_id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
update,
|
||||||
|
version,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
@@ -235,7 +252,12 @@ where
|
|||||||
{
|
{
|
||||||
require_permission(&ctx, "health.patient.manage")?;
|
require_permission(&ctx, "health.patient.manage")?;
|
||||||
patient_service::delete_family_member(
|
patient_service::delete_family_member(
|
||||||
&state, ctx.tenant_id, patient_id, member_id, Some(ctx.user_id), req.version,
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
patient_id,
|
||||||
|
member_id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
req.version,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(())))
|
Ok(Json(ApiResponse::ok(())))
|
||||||
@@ -257,7 +279,8 @@ where
|
|||||||
ctx.tenant_id,
|
ctx.tenant_id,
|
||||||
id,
|
id,
|
||||||
req.doctor_id,
|
req.doctor_id,
|
||||||
req.relationship_type.unwrap_or_else(|| "primary".to_string()),
|
req.relationship_type
|
||||||
|
.unwrap_or_else(|| "primary".to_string()),
|
||||||
Some(ctx.user_id),
|
Some(ctx.user_id),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -274,7 +297,14 @@ where
|
|||||||
S: Clone + Send + Sync + 'static,
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.patient.manage")?;
|
require_permission(&ctx, "health.patient.manage")?;
|
||||||
patient_service::remove_doctor(&state, ctx.tenant_id, patient_id, doctor_id, Some(ctx.user_id)).await?;
|
patient_service::remove_doctor(
|
||||||
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
patient_id,
|
||||||
|
doctor_id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(())))
|
Ok(Json(ApiResponse::ok(())))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,11 +368,16 @@ where
|
|||||||
{
|
{
|
||||||
require_permission(&ctx, "health.patient.manage")?;
|
require_permission(&ctx, "health.patient.manage")?;
|
||||||
let result = patient_service::create_tag(
|
let result = patient_service::create_tag(
|
||||||
&state, ctx.tenant_id, Some(ctx.user_id),
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
Some(ctx.user_id),
|
||||||
patient_service::CreateTagReq {
|
patient_service::CreateTagReq {
|
||||||
name: req.name, color: req.color, description: req.description,
|
name: req.name,
|
||||||
|
color: req.color,
|
||||||
|
description: req.description,
|
||||||
},
|
},
|
||||||
).await?;
|
)
|
||||||
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,11 +401,18 @@ where
|
|||||||
{
|
{
|
||||||
require_permission(&ctx, "health.patient.manage")?;
|
require_permission(&ctx, "health.patient.manage")?;
|
||||||
let result = patient_service::update_tag(
|
let result = patient_service::update_tag(
|
||||||
&state, ctx.tenant_id, id, Some(ctx.user_id),
|
&state,
|
||||||
|
ctx.tenant_id,
|
||||||
|
id,
|
||||||
|
Some(ctx.user_id),
|
||||||
patient_service::UpdateTagReq {
|
patient_service::UpdateTagReq {
|
||||||
name: req.name, color: req.color, description: req.description, version: req.version,
|
name: req.name,
|
||||||
|
color: req.color,
|
||||||
|
description: req.description,
|
||||||
|
version: req.version,
|
||||||
},
|
},
|
||||||
).await?;
|
)
|
||||||
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,8 +427,6 @@ where
|
|||||||
S: Clone + Send + Sync + 'static,
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.patient.manage")?;
|
require_permission(&ctx, "health.patient.manage")?;
|
||||||
patient_service::delete_tag(
|
patient_service::delete_tag(&state, ctx.tenant_id, id, Some(ctx.user_id), req.version).await?;
|
||||||
&state, ctx.tenant_id, id, Some(ctx.user_id), req.version,
|
|
||||||
).await?;
|
|
||||||
Ok(Json(ApiResponse::ok(())))
|
Ok(Json(ApiResponse::ok(())))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,9 +36,11 @@ pub async fn get_my_account<S>(
|
|||||||
State(state): State<HealthState>,
|
State(state): State<HealthState>,
|
||||||
Extension(ctx): Extension<TenantContext>,
|
Extension(ctx): Extension<TenantContext>,
|
||||||
) -> Result<Json<ApiResponse<PointsAccountResp>>, AppError>
|
) -> Result<Json<ApiResponse<PointsAccountResp>>, AppError>
|
||||||
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
|
where
|
||||||
|
HealthState: FromRef<S>,
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.health-data.list")?;
|
require_permission(&ctx, "health.points.list")?;
|
||||||
let patient_id = resolve_patient_id(&state, ctx.tenant_id, ctx.user_id).await?;
|
let patient_id = resolve_patient_id(&state, ctx.tenant_id, ctx.user_id).await?;
|
||||||
let result = points_service::get_account(&state, ctx.tenant_id, patient_id).await?;
|
let result = points_service::get_account(&state, ctx.tenant_id, patient_id).await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
@@ -48,13 +50,14 @@ pub async fn daily_checkin<S>(
|
|||||||
State(state): State<HealthState>,
|
State(state): State<HealthState>,
|
||||||
Extension(ctx): Extension<TenantContext>,
|
Extension(ctx): Extension<TenantContext>,
|
||||||
) -> Result<Json<ApiResponse<CheckinStatusResp>>, AppError>
|
) -> Result<Json<ApiResponse<CheckinStatusResp>>, AppError>
|
||||||
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
|
where
|
||||||
|
HealthState: FromRef<S>,
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.health-data.list")?;
|
require_permission(&ctx, "health.points.list")?;
|
||||||
let patient_id = resolve_patient_id(&state, ctx.tenant_id, ctx.user_id).await?;
|
let patient_id = resolve_patient_id(&state, ctx.tenant_id, ctx.user_id).await?;
|
||||||
let result = points_service::daily_checkin(
|
let result =
|
||||||
&state, ctx.tenant_id, patient_id, Some(ctx.user_id),
|
points_service::daily_checkin(&state, ctx.tenant_id, patient_id, Some(ctx.user_id)).await?;
|
||||||
).await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,9 +65,11 @@ pub async fn get_checkin_status<S>(
|
|||||||
State(state): State<HealthState>,
|
State(state): State<HealthState>,
|
||||||
Extension(ctx): Extension<TenantContext>,
|
Extension(ctx): Extension<TenantContext>,
|
||||||
) -> Result<Json<ApiResponse<CheckinStatusResp>>, AppError>
|
) -> Result<Json<ApiResponse<CheckinStatusResp>>, AppError>
|
||||||
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
|
where
|
||||||
|
HealthState: FromRef<S>,
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.health-data.list")?;
|
require_permission(&ctx, "health.points.list")?;
|
||||||
let patient_id = resolve_patient_id(&state, ctx.tenant_id, ctx.user_id).await?;
|
let patient_id = resolve_patient_id(&state, ctx.tenant_id, ctx.user_id).await?;
|
||||||
let result = points_service::get_checkin_status(&state, ctx.tenant_id, patient_id).await?;
|
let result = points_service::get_checkin_status(&state, ctx.tenant_id, patient_id).await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
@@ -79,15 +84,17 @@ pub async fn list_my_transactions<S>(
|
|||||||
Extension(ctx): Extension<TenantContext>,
|
Extension(ctx): Extension<TenantContext>,
|
||||||
Query(params): Query<PaginationParams>,
|
Query(params): Query<PaginationParams>,
|
||||||
) -> Result<Json<ApiResponse<PaginatedResponse<PointsTransactionResp>>>, AppError>
|
) -> Result<Json<ApiResponse<PaginatedResponse<PointsTransactionResp>>>, AppError>
|
||||||
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
|
where
|
||||||
|
HealthState: FromRef<S>,
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.health-data.list")?;
|
require_permission(&ctx, "health.points.list")?;
|
||||||
let patient_id = resolve_patient_id(&state, ctx.tenant_id, ctx.user_id).await?;
|
let patient_id = resolve_patient_id(&state, ctx.tenant_id, ctx.user_id).await?;
|
||||||
let page = params.page.unwrap_or(1);
|
let page = params.page.unwrap_or(1);
|
||||||
let page_size = params.page_size.unwrap_or(20);
|
let page_size = params.page_size.unwrap_or(20);
|
||||||
let result = points_service::list_transactions(
|
let result =
|
||||||
&state, ctx.tenant_id, patient_id, page, page_size,
|
points_service::list_transactions(&state, ctx.tenant_id, patient_id, page, page_size)
|
||||||
).await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,14 +104,15 @@ pub async fn list_products<S>(
|
|||||||
Query(params): Query<ProductTypeParam>,
|
Query(params): Query<ProductTypeParam>,
|
||||||
Query(page): Query<PaginationParams>,
|
Query(page): Query<PaginationParams>,
|
||||||
) -> Result<Json<ApiResponse<PaginatedResponse<PointsProductResp>>>, AppError>
|
) -> Result<Json<ApiResponse<PaginatedResponse<PointsProductResp>>>, AppError>
|
||||||
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
|
where
|
||||||
|
HealthState: FromRef<S>,
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.health-data.list")?;
|
require_permission(&ctx, "health.points.list")?;
|
||||||
let p = page.page.unwrap_or(1);
|
let p = page.page.unwrap_or(1);
|
||||||
let ps = page.page_size.unwrap_or(20);
|
let ps = page.page_size.unwrap_or(20);
|
||||||
let result = points_service::list_products(
|
let result =
|
||||||
&state, ctx.tenant_id, params.product_type, p, ps,
|
points_service::list_products(&state, ctx.tenant_id, params.product_type, p, ps).await?;
|
||||||
).await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,9 +121,11 @@ pub async fn get_product<S>(
|
|||||||
Extension(ctx): Extension<TenantContext>,
|
Extension(ctx): Extension<TenantContext>,
|
||||||
Path(product_id): Path<Uuid>,
|
Path(product_id): Path<Uuid>,
|
||||||
) -> Result<Json<ApiResponse<PointsProductResp>>, AppError>
|
) -> Result<Json<ApiResponse<PointsProductResp>>, AppError>
|
||||||
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
|
where
|
||||||
|
HealthState: FromRef<S>,
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.health-data.list")?;
|
require_permission(&ctx, "health.points.list")?;
|
||||||
let result = points_service::get_product(&state, ctx.tenant_id, product_id).await?;
|
let result = points_service::get_product(&state, ctx.tenant_id, product_id).await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
@@ -125,13 +135,15 @@ pub async fn exchange_product<S>(
|
|||||||
Extension(ctx): Extension<TenantContext>,
|
Extension(ctx): Extension<TenantContext>,
|
||||||
Json(req): Json<ExchangeReq>,
|
Json(req): Json<ExchangeReq>,
|
||||||
) -> Result<Json<ApiResponse<PointsOrderResp>>, AppError>
|
) -> Result<Json<ApiResponse<PointsOrderResp>>, AppError>
|
||||||
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
|
where
|
||||||
|
HealthState: FromRef<S>,
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.health-data.manage")?;
|
require_permission(&ctx, "health.points.manage")?;
|
||||||
let patient_id = resolve_patient_id(&state, ctx.tenant_id, ctx.user_id).await?;
|
let patient_id = resolve_patient_id(&state, ctx.tenant_id, ctx.user_id).await?;
|
||||||
let result = points_service::exchange_product(
|
let result =
|
||||||
&state, ctx.tenant_id, patient_id, req, Some(ctx.user_id),
|
points_service::exchange_product(&state, ctx.tenant_id, patient_id, req, Some(ctx.user_id))
|
||||||
).await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,15 +152,16 @@ pub async fn list_my_orders<S>(
|
|||||||
Extension(ctx): Extension<TenantContext>,
|
Extension(ctx): Extension<TenantContext>,
|
||||||
Query(params): Query<PaginationParams>,
|
Query(params): Query<PaginationParams>,
|
||||||
) -> Result<Json<ApiResponse<PaginatedResponse<PointsOrderResp>>>, AppError>
|
) -> Result<Json<ApiResponse<PaginatedResponse<PointsOrderResp>>>, AppError>
|
||||||
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
|
where
|
||||||
|
HealthState: FromRef<S>,
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.health-data.list")?;
|
require_permission(&ctx, "health.points.list")?;
|
||||||
let patient_id = resolve_patient_id(&state, ctx.tenant_id, ctx.user_id).await?;
|
let patient_id = resolve_patient_id(&state, ctx.tenant_id, ctx.user_id).await?;
|
||||||
let page = params.page.unwrap_or(1);
|
let page = params.page.unwrap_or(1);
|
||||||
let page_size = params.page_size.unwrap_or(20);
|
let page_size = params.page_size.unwrap_or(20);
|
||||||
let result = points_service::list_orders(
|
let result =
|
||||||
&state, ctx.tenant_id, patient_id, page, page_size,
|
points_service::list_orders(&state, ctx.tenant_id, patient_id, page, page_size).await?;
|
||||||
).await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,14 +174,15 @@ pub async fn list_offline_events<S>(
|
|||||||
Extension(ctx): Extension<TenantContext>,
|
Extension(ctx): Extension<TenantContext>,
|
||||||
Query(params): Query<PaginationParams>,
|
Query(params): Query<PaginationParams>,
|
||||||
) -> Result<Json<ApiResponse<PaginatedResponse<OfflineEventResp>>>, AppError>
|
) -> Result<Json<ApiResponse<PaginatedResponse<OfflineEventResp>>>, AppError>
|
||||||
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
|
where
|
||||||
|
HealthState: FromRef<S>,
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.health-data.list")?;
|
require_permission(&ctx, "health.points.list")?;
|
||||||
let page = params.page.unwrap_or(1);
|
let page = params.page.unwrap_or(1);
|
||||||
let page_size = params.page_size.unwrap_or(20);
|
let page_size = params.page_size.unwrap_or(20);
|
||||||
let result = points_service::list_offline_events(
|
let result =
|
||||||
&state, ctx.tenant_id, page, page_size,
|
points_service::list_offline_events(&state, ctx.tenant_id, page, page_size).await?;
|
||||||
).await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,13 +191,20 @@ pub async fn register_event<S>(
|
|||||||
Extension(ctx): Extension<TenantContext>,
|
Extension(ctx): Extension<TenantContext>,
|
||||||
Path(event_id): Path<Uuid>,
|
Path(event_id): Path<Uuid>,
|
||||||
) -> Result<Json<ApiResponse<()>>, AppError>
|
) -> Result<Json<ApiResponse<()>>, AppError>
|
||||||
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
|
where
|
||||||
|
HealthState: FromRef<S>,
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.health-data.manage")?;
|
require_permission(&ctx, "health.points.manage")?;
|
||||||
let patient_id = resolve_patient_id(&state, ctx.tenant_id, ctx.user_id).await?;
|
let patient_id = resolve_patient_id(&state, ctx.tenant_id, ctx.user_id).await?;
|
||||||
points_service::register_event(
|
points_service::register_event(
|
||||||
&state, ctx.tenant_id, event_id, patient_id, Some(ctx.user_id),
|
&state,
|
||||||
).await?;
|
ctx.tenant_id,
|
||||||
|
event_id,
|
||||||
|
patient_id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(())))
|
Ok(Json(ApiResponse::ok(())))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,12 +217,13 @@ pub async fn verify_order<S>(
|
|||||||
Extension(ctx): Extension<TenantContext>,
|
Extension(ctx): Extension<TenantContext>,
|
||||||
Json(req): Json<VerifyOrderReq>,
|
Json(req): Json<VerifyOrderReq>,
|
||||||
) -> Result<Json<ApiResponse<PointsOrderResp>>, AppError>
|
) -> Result<Json<ApiResponse<PointsOrderResp>>, AppError>
|
||||||
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
|
where
|
||||||
|
HealthState: FromRef<S>,
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.points.manage")?;
|
require_permission(&ctx, "health.points.manage")?;
|
||||||
let result = points_service::verify_order(
|
let result =
|
||||||
&state, ctx.tenant_id, req.qr_code, ctx.user_id,
|
points_service::verify_order(&state, ctx.tenant_id, req.qr_code, ctx.user_id).await?;
|
||||||
).await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,7 +231,9 @@ pub async fn list_rules<S>(
|
|||||||
State(state): State<HealthState>,
|
State(state): State<HealthState>,
|
||||||
Extension(ctx): Extension<TenantContext>,
|
Extension(ctx): Extension<TenantContext>,
|
||||||
) -> Result<Json<ApiResponse<Vec<PointsRuleResp>>>, AppError>
|
) -> Result<Json<ApiResponse<Vec<PointsRuleResp>>>, AppError>
|
||||||
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
|
where
|
||||||
|
HealthState: FromRef<S>,
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.points.list")?;
|
require_permission(&ctx, "health.points.list")?;
|
||||||
let result = points_service::list_rules(&state, ctx.tenant_id).await?;
|
let result = points_service::list_rules(&state, ctx.tenant_id).await?;
|
||||||
@@ -221,14 +245,14 @@ pub async fn create_rule<S>(
|
|||||||
Extension(ctx): Extension<TenantContext>,
|
Extension(ctx): Extension<TenantContext>,
|
||||||
Json(req): Json<CreatePointsRuleReq>,
|
Json(req): Json<CreatePointsRuleReq>,
|
||||||
) -> Result<Json<ApiResponse<PointsRuleResp>>, AppError>
|
) -> Result<Json<ApiResponse<PointsRuleResp>>, AppError>
|
||||||
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
|
where
|
||||||
|
HealthState: FromRef<S>,
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.points.manage")?;
|
require_permission(&ctx, "health.points.manage")?;
|
||||||
let mut req = req;
|
let mut req = req;
|
||||||
req.sanitize();
|
req.sanitize();
|
||||||
let result = points_service::create_rule(
|
let result = points_service::create_rule(&state, ctx.tenant_id, Some(ctx.user_id), req).await?;
|
||||||
&state, ctx.tenant_id, Some(ctx.user_id), req,
|
|
||||||
).await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,14 +262,22 @@ pub async fn update_rule<S>(
|
|||||||
Path(rule_id): Path<Uuid>,
|
Path(rule_id): Path<Uuid>,
|
||||||
Json(wrapper): Json<crate::dto::points_dto::UpdateRuleWithVersion>,
|
Json(wrapper): Json<crate::dto::points_dto::UpdateRuleWithVersion>,
|
||||||
) -> Result<Json<ApiResponse<PointsRuleResp>>, AppError>
|
) -> Result<Json<ApiResponse<PointsRuleResp>>, AppError>
|
||||||
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
|
where
|
||||||
|
HealthState: FromRef<S>,
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.points.manage")?;
|
require_permission(&ctx, "health.points.manage")?;
|
||||||
let mut data = wrapper.data;
|
let mut data = wrapper.data;
|
||||||
data.sanitize();
|
data.sanitize();
|
||||||
let result = points_service::update_rule(
|
let result = points_service::update_rule(
|
||||||
&state, ctx.tenant_id, rule_id, Some(ctx.user_id), data, wrapper.version,
|
&state,
|
||||||
).await?;
|
ctx.tenant_id,
|
||||||
|
rule_id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
data,
|
||||||
|
wrapper.version,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,12 +287,19 @@ pub async fn delete_rule<S>(
|
|||||||
Path(rule_id): Path<Uuid>,
|
Path(rule_id): Path<Uuid>,
|
||||||
Json(wrapper): Json<crate::dto::DeleteWithVersion>,
|
Json(wrapper): Json<crate::dto::DeleteWithVersion>,
|
||||||
) -> Result<Json<ApiResponse<()>>, AppError>
|
) -> Result<Json<ApiResponse<()>>, AppError>
|
||||||
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
|
where
|
||||||
|
HealthState: FromRef<S>,
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.points.manage")?;
|
require_permission(&ctx, "health.points.manage")?;
|
||||||
points_service::delete_rule(
|
points_service::delete_rule(
|
||||||
&state, ctx.tenant_id, rule_id, Some(ctx.user_id), wrapper.version,
|
&state,
|
||||||
).await?;
|
ctx.tenant_id,
|
||||||
|
rule_id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
wrapper.version,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(())))
|
Ok(Json(ApiResponse::ok(())))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,14 +310,22 @@ pub async fn admin_list_products<S>(
|
|||||||
Query(page): Query<PaginationParams>,
|
Query(page): Query<PaginationParams>,
|
||||||
Query(filter): Query<AdminProductFilter>,
|
Query(filter): Query<AdminProductFilter>,
|
||||||
) -> Result<Json<ApiResponse<PaginatedResponse<PointsProductResp>>>, AppError>
|
) -> Result<Json<ApiResponse<PaginatedResponse<PointsProductResp>>>, AppError>
|
||||||
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
|
where
|
||||||
|
HealthState: FromRef<S>,
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.points.list")?;
|
require_permission(&ctx, "health.points.list")?;
|
||||||
let p = page.page.unwrap_or(1);
|
let p = page.page.unwrap_or(1);
|
||||||
let ps = page.page_size.unwrap_or(20);
|
let ps = page.page_size.unwrap_or(20);
|
||||||
let result = points_service::admin_list_products(
|
let result = points_service::admin_list_products(
|
||||||
&state, ctx.tenant_id, params.product_type, filter.is_active, p, ps,
|
&state,
|
||||||
).await?;
|
ctx.tenant_id,
|
||||||
|
params.product_type,
|
||||||
|
filter.is_active,
|
||||||
|
p,
|
||||||
|
ps,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,14 +334,15 @@ pub async fn admin_create_product<S>(
|
|||||||
Extension(ctx): Extension<TenantContext>,
|
Extension(ctx): Extension<TenantContext>,
|
||||||
Json(req): Json<CreatePointsProductReq>,
|
Json(req): Json<CreatePointsProductReq>,
|
||||||
) -> Result<Json<ApiResponse<PointsProductResp>>, AppError>
|
) -> Result<Json<ApiResponse<PointsProductResp>>, AppError>
|
||||||
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
|
where
|
||||||
|
HealthState: FromRef<S>,
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.points.manage")?;
|
require_permission(&ctx, "health.points.manage")?;
|
||||||
let mut req = req;
|
let mut req = req;
|
||||||
req.sanitize();
|
req.sanitize();
|
||||||
let result = points_service::create_product(
|
let result =
|
||||||
&state, ctx.tenant_id, Some(ctx.user_id), req,
|
points_service::create_product(&state, ctx.tenant_id, Some(ctx.user_id), req).await?;
|
||||||
).await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,14 +352,22 @@ pub async fn admin_update_product<S>(
|
|||||||
Path(product_id): Path<Uuid>,
|
Path(product_id): Path<Uuid>,
|
||||||
Json(wrapper): Json<crate::dto::points_dto::UpdateProductWithVersion>,
|
Json(wrapper): Json<crate::dto::points_dto::UpdateProductWithVersion>,
|
||||||
) -> Result<Json<ApiResponse<PointsProductResp>>, AppError>
|
) -> Result<Json<ApiResponse<PointsProductResp>>, AppError>
|
||||||
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
|
where
|
||||||
|
HealthState: FromRef<S>,
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.points.manage")?;
|
require_permission(&ctx, "health.points.manage")?;
|
||||||
let mut data = wrapper.data;
|
let mut data = wrapper.data;
|
||||||
data.sanitize();
|
data.sanitize();
|
||||||
let result = points_service::update_product(
|
let result = points_service::update_product(
|
||||||
&state, ctx.tenant_id, product_id, Some(ctx.user_id), data, wrapper.version,
|
&state,
|
||||||
).await?;
|
ctx.tenant_id,
|
||||||
|
product_id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
data,
|
||||||
|
wrapper.version,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,12 +377,19 @@ pub async fn admin_delete_product<S>(
|
|||||||
Path(product_id): Path<Uuid>,
|
Path(product_id): Path<Uuid>,
|
||||||
Json(wrapper): Json<crate::dto::DeleteWithVersion>,
|
Json(wrapper): Json<crate::dto::DeleteWithVersion>,
|
||||||
) -> Result<Json<ApiResponse<()>>, AppError>
|
) -> Result<Json<ApiResponse<()>>, AppError>
|
||||||
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
|
where
|
||||||
|
HealthState: FromRef<S>,
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.points.manage")?;
|
require_permission(&ctx, "health.points.manage")?;
|
||||||
points_service::delete_product(
|
points_service::delete_product(
|
||||||
&state, ctx.tenant_id, product_id, Some(ctx.user_id), wrapper.version,
|
&state,
|
||||||
).await?;
|
ctx.tenant_id,
|
||||||
|
product_id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
wrapper.version,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(())))
|
Ok(Json(ApiResponse::ok(())))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,15 +398,15 @@ pub async fn admin_list_orders<S>(
|
|||||||
Extension(ctx): Extension<TenantContext>,
|
Extension(ctx): Extension<TenantContext>,
|
||||||
Query(params): Query<PaginationParams>,
|
Query(params): Query<PaginationParams>,
|
||||||
) -> Result<Json<ApiResponse<PaginatedResponse<PointsOrderResp>>>, AppError>
|
) -> Result<Json<ApiResponse<PaginatedResponse<PointsOrderResp>>>, AppError>
|
||||||
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
|
where
|
||||||
|
HealthState: FromRef<S>,
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.points.list")?;
|
require_permission(&ctx, "health.points.list")?;
|
||||||
let page = params.page.unwrap_or(1);
|
let page = params.page.unwrap_or(1);
|
||||||
let page_size = params.page_size.unwrap_or(20);
|
let page_size = params.page_size.unwrap_or(20);
|
||||||
// 管理端查看所有订单 — 不按 patient_id 过滤
|
// 管理端查看所有订单 — 不按 patient_id 过滤
|
||||||
let result = points_service::admin_list_orders(
|
let result = points_service::admin_list_orders(&state, ctx.tenant_id, page, page_size).await?;
|
||||||
&state, ctx.tenant_id, page, page_size,
|
|
||||||
).await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,14 +419,15 @@ pub async fn admin_create_event<S>(
|
|||||||
Extension(ctx): Extension<TenantContext>,
|
Extension(ctx): Extension<TenantContext>,
|
||||||
Json(req): Json<CreateOfflineEventReq>,
|
Json(req): Json<CreateOfflineEventReq>,
|
||||||
) -> Result<Json<ApiResponse<OfflineEventResp>>, AppError>
|
) -> Result<Json<ApiResponse<OfflineEventResp>>, AppError>
|
||||||
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
|
where
|
||||||
|
HealthState: FromRef<S>,
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.points.manage")?;
|
require_permission(&ctx, "health.points.manage")?;
|
||||||
let mut req = req;
|
let mut req = req;
|
||||||
req.sanitize();
|
req.sanitize();
|
||||||
let result = points_service::create_offline_event(
|
let result =
|
||||||
&state, ctx.tenant_id, Some(ctx.user_id), req,
|
points_service::create_offline_event(&state, ctx.tenant_id, Some(ctx.user_id), req).await?;
|
||||||
).await?;
|
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,14 +437,22 @@ pub async fn admin_update_event<S>(
|
|||||||
Path(event_id): Path<Uuid>,
|
Path(event_id): Path<Uuid>,
|
||||||
Json(wrapper): Json<UpdateOfflineEventWithVersion>,
|
Json(wrapper): Json<UpdateOfflineEventWithVersion>,
|
||||||
) -> Result<Json<ApiResponse<OfflineEventResp>>, AppError>
|
) -> Result<Json<ApiResponse<OfflineEventResp>>, AppError>
|
||||||
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
|
where
|
||||||
|
HealthState: FromRef<S>,
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.points.manage")?;
|
require_permission(&ctx, "health.points.manage")?;
|
||||||
let mut data = wrapper.data;
|
let mut data = wrapper.data;
|
||||||
data.sanitize();
|
data.sanitize();
|
||||||
let result = points_service::update_offline_event(
|
let result = points_service::update_offline_event(
|
||||||
&state, ctx.tenant_id, event_id, Some(ctx.user_id), data, wrapper.version,
|
&state,
|
||||||
).await?;
|
ctx.tenant_id,
|
||||||
|
event_id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
data,
|
||||||
|
wrapper.version,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,12 +462,19 @@ pub async fn admin_delete_event<S>(
|
|||||||
Path(event_id): Path<Uuid>,
|
Path(event_id): Path<Uuid>,
|
||||||
Json(wrapper): Json<crate::dto::DeleteWithVersion>,
|
Json(wrapper): Json<crate::dto::DeleteWithVersion>,
|
||||||
) -> Result<Json<ApiResponse<()>>, AppError>
|
) -> Result<Json<ApiResponse<()>>, AppError>
|
||||||
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
|
where
|
||||||
|
HealthState: FromRef<S>,
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.points.manage")?;
|
require_permission(&ctx, "health.points.manage")?;
|
||||||
points_service::delete_offline_event(
|
points_service::delete_offline_event(
|
||||||
&state, ctx.tenant_id, event_id, Some(ctx.user_id), wrapper.version,
|
&state,
|
||||||
).await?;
|
ctx.tenant_id,
|
||||||
|
event_id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
wrapper.version,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(())))
|
Ok(Json(ApiResponse::ok(())))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,14 +490,21 @@ pub async fn admin_list_events<S>(
|
|||||||
Extension(ctx): Extension<TenantContext>,
|
Extension(ctx): Extension<TenantContext>,
|
||||||
Query(params): Query<AdminListEventsParams>,
|
Query(params): Query<AdminListEventsParams>,
|
||||||
) -> Result<Json<ApiResponse<PaginatedResponse<OfflineEventResp>>>, AppError>
|
) -> Result<Json<ApiResponse<PaginatedResponse<OfflineEventResp>>>, AppError>
|
||||||
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
|
where
|
||||||
|
HealthState: FromRef<S>,
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.points.list")?;
|
require_permission(&ctx, "health.points.list")?;
|
||||||
let page = params.page.unwrap_or(1);
|
let page = params.page.unwrap_or(1);
|
||||||
let page_size = params.page_size.unwrap_or(20);
|
let page_size = params.page_size.unwrap_or(20);
|
||||||
let result = points_service::admin_list_offline_events(
|
let result = points_service::admin_list_offline_events(
|
||||||
&state, ctx.tenant_id, params.status, page, page_size,
|
&state,
|
||||||
).await?;
|
ctx.tenant_id,
|
||||||
|
params.status,
|
||||||
|
page,
|
||||||
|
page_size,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,12 +514,19 @@ pub async fn admin_checkin_event<S>(
|
|||||||
Path(event_id): Path<Uuid>,
|
Path(event_id): Path<Uuid>,
|
||||||
Json(req): Json<AdminCheckinReq>,
|
Json(req): Json<AdminCheckinReq>,
|
||||||
) -> Result<Json<ApiResponse<()>>, AppError>
|
) -> Result<Json<ApiResponse<()>>, AppError>
|
||||||
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
|
where
|
||||||
|
HealthState: FromRef<S>,
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.points.manage")?;
|
require_permission(&ctx, "health.points.manage")?;
|
||||||
points_service::admin_checkin_event(
|
points_service::admin_checkin_event(
|
||||||
&state, ctx.tenant_id, event_id, req.patient_id, Some(ctx.user_id),
|
&state,
|
||||||
).await?;
|
ctx.tenant_id,
|
||||||
|
event_id,
|
||||||
|
req.patient_id,
|
||||||
|
Some(ctx.user_id),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(())))
|
Ok(Json(ApiResponse::ok(())))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -445,7 +538,9 @@ pub async fn get_points_statistics<S>(
|
|||||||
State(state): State<HealthState>,
|
State(state): State<HealthState>,
|
||||||
Extension(ctx): Extension<TenantContext>,
|
Extension(ctx): Extension<TenantContext>,
|
||||||
) -> Result<Json<ApiResponse<PointsStatisticsResp>>, AppError>
|
) -> Result<Json<ApiResponse<PointsStatisticsResp>>, AppError>
|
||||||
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
|
where
|
||||||
|
HealthState: FromRef<S>,
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.points.list")?;
|
require_permission(&ctx, "health.points.list")?;
|
||||||
let result = points_service::get_points_statistics(&state, ctx.tenant_id).await?;
|
let result = points_service::get_points_statistics(&state, ctx.tenant_id).await?;
|
||||||
@@ -461,7 +556,9 @@ pub async fn admin_get_patient_account<S>(
|
|||||||
Extension(ctx): Extension<TenantContext>,
|
Extension(ctx): Extension<TenantContext>,
|
||||||
Path(patient_id): Path<Uuid>,
|
Path(patient_id): Path<Uuid>,
|
||||||
) -> Result<Json<ApiResponse<PointsAccountResp>>, AppError>
|
) -> Result<Json<ApiResponse<PointsAccountResp>>, AppError>
|
||||||
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
|
where
|
||||||
|
HealthState: FromRef<S>,
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.points.list")?;
|
require_permission(&ctx, "health.points.list")?;
|
||||||
let result = points_service::get_account(&state, ctx.tenant_id, patient_id).await?;
|
let result = points_service::get_account(&state, ctx.tenant_id, patient_id).await?;
|
||||||
@@ -474,14 +571,16 @@ pub async fn admin_list_patient_transactions<S>(
|
|||||||
Path(patient_id): Path<Uuid>,
|
Path(patient_id): Path<Uuid>,
|
||||||
Query(params): Query<PaginationParams>,
|
Query(params): Query<PaginationParams>,
|
||||||
) -> Result<Json<ApiResponse<PaginatedResponse<PointsTransactionResp>>>, AppError>
|
) -> Result<Json<ApiResponse<PaginatedResponse<PointsTransactionResp>>>, AppError>
|
||||||
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
|
where
|
||||||
|
HealthState: FromRef<S>,
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
require_permission(&ctx, "health.points.list")?;
|
require_permission(&ctx, "health.points.list")?;
|
||||||
let page = params.page.unwrap_or(1);
|
let page = params.page.unwrap_or(1);
|
||||||
let page_size = params.page_size.unwrap_or(20);
|
let page_size = params.page_size.unwrap_or(20);
|
||||||
let result = points_service::list_transactions(
|
let result =
|
||||||
&state, ctx.tenant_id, patient_id, page, page_size,
|
points_service::list_transactions(&state, ctx.tenant_id, patient_id, page, page_size)
|
||||||
).await?;
|
.await?;
|
||||||
Ok(Json(ApiResponse::ok(result)))
|
Ok(Json(ApiResponse::ok(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user