P0 #1: 媒体文件上传增加 MIME 类型白名单校验(jpeg/png/gif/webp/svg/mp4/webm/pdf) 和文件大小限制(10MB),扩展名使用白名单清理防止路径遍历攻击。 P0 #2: OAuth JWT 密钥从环境变量改为 State 注入,消除运行时 env::var 依赖, FHIR 路由中间件使用闭包捕获 jwt_secret 保持类型安全。
864 lines
36 KiB
Rust
864 lines
36 KiB
Rust
use axum::Router;
|
||
use uuid::Uuid;
|
||
|
||
use erp_core::error::AppResult;
|
||
use erp_core::events::EventBus;
|
||
use erp_core::module::{ErpModule, PermissionDescriptor};
|
||
|
||
use crate::handler::{article_handler, banner_handler, ble_gateway_handler};
|
||
|
||
pub struct HealthModule;
|
||
|
||
impl HealthModule {
|
||
pub fn new() -> Self {
|
||
Self
|
||
}
|
||
|
||
/// 启动定时逾期随访检查(每 6 小时运行一次),返回 JoinHandle 用于优雅关闭
|
||
pub fn start_overdue_checker(db: sea_orm::DatabaseConnection) -> tokio::task::JoinHandle<()> {
|
||
tokio::spawn(async move {
|
||
let mut interval = tokio::time::interval(std::time::Duration::from_secs(6 * 3600));
|
||
loop {
|
||
tokio::select! {
|
||
_ = interval.tick() => {
|
||
match crate::service::follow_up_service::check_overdue_tasks(&db).await {
|
||
Ok(count) if count > 0 => tracing::info!(count = count, "随访逾期检查完成"),
|
||
Ok(_) => {}
|
||
Err(e) => tracing::warn!(error = %e, "随访逾期检查失败"),
|
||
}
|
||
}
|
||
_ = tokio::signal::ctrl_c() => {
|
||
tracing::info!("随访逾期检查任务收到关闭信号,正在停止");
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
/// 启动积分过期清理(每 24 小时运行一次),返回 JoinHandle 用于优雅关闭
|
||
pub fn start_points_expiration_checker(
|
||
db: sea_orm::DatabaseConnection,
|
||
event_bus: erp_core::events::EventBus,
|
||
) -> tokio::task::JoinHandle<()> {
|
||
tokio::spawn(async move {
|
||
let mut interval = tokio::time::interval(std::time::Duration::from_secs(24 * 3600));
|
||
loop {
|
||
tokio::select! {
|
||
_ = interval.tick() => {
|
||
match crate::service::points_service::expire_points(&db, &event_bus).await {
|
||
Ok(count) if count > 0 => tracing::info!(count = count, "积分过期清理完成"),
|
||
Ok(_) => {}
|
||
Err(e) => tracing::warn!(error = %e, "积分过期清理失败"),
|
||
}
|
||
}
|
||
_ = tokio::signal::ctrl_c() => {
|
||
tracing::info!("积分过期清理任务收到关闭信号,正在停止");
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
/// 启动预约提醒调度(每 1 小时运行一次),扫描明天有预约的患者发送提醒
|
||
pub fn start_appointment_reminder(
|
||
db: sea_orm::DatabaseConnection,
|
||
event_bus: erp_core::events::EventBus,
|
||
) -> tokio::task::JoinHandle<()> {
|
||
tokio::spawn(async move {
|
||
let mut interval = tokio::time::interval(std::time::Duration::from_secs(3600));
|
||
loop {
|
||
tokio::select! {
|
||
_ = interval.tick() => {
|
||
match crate::service::appointment_service::send_reminders(&db, &event_bus).await {
|
||
Ok(count) if count > 0 => tracing::info!(count = count, "预约提醒发送完成"),
|
||
Ok(_) => {}
|
||
Err(e) => tracing::warn!(error = %e, "预约提醒发送失败"),
|
||
}
|
||
}
|
||
_ = tokio::signal::ctrl_c() => {
|
||
tracing::info!("预约提醒调度任务收到关闭信号,正在停止");
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
/// 启动设备原始数据清理(每 24 小时运行一次),删除超过 90 天的 device_readings
|
||
pub fn start_device_readings_cleanup(
|
||
db: sea_orm::DatabaseConnection,
|
||
) -> tokio::task::JoinHandle<()> {
|
||
tokio::spawn(async move {
|
||
let mut interval = tokio::time::interval(std::time::Duration::from_secs(24 * 3600));
|
||
loop {
|
||
tokio::select! {
|
||
_ = interval.tick() => {
|
||
match crate::service::device_reading_service::cleanup_stale_readings(&db).await {
|
||
Ok(count) if count > 0 => tracing::info!(count = count, "设备原始数据清理完成"),
|
||
Ok(_) => {}
|
||
Err(e) => tracing::warn!(error = %e, "设备原始数据清理失败"),
|
||
}
|
||
}
|
||
_ = tokio::signal::ctrl_c() => {
|
||
tracing::info!("设备原始数据清理任务收到关闭信号,正在停止");
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
/// 启动日聚合任务(每 24 小时运行一次),从前一天的 hourly 数据聚合到 daily
|
||
pub fn start_daily_aggregation(db: sea_orm::DatabaseConnection) -> tokio::task::JoinHandle<()> {
|
||
tokio::spawn(async move {
|
||
let mut interval = tokio::time::interval(std::time::Duration::from_secs(24 * 3600));
|
||
loop {
|
||
tokio::select! {
|
||
_ = interval.tick() => {
|
||
let yesterday = chrono::Local::now().date_naive() - chrono::Duration::days(1);
|
||
tracing::info!(date = %yesterday, "Running daily aggregation");
|
||
match crate::service::vital_signs_daily_service::aggregate_daily_for_all_tenants(&db, yesterday).await {
|
||
Ok(count) if count > 0 => tracing::info!(count = count, date = %yesterday, "日聚合完成"),
|
||
Ok(_) => tracing::info!(date = %yesterday, "日聚合完成(无数据)"),
|
||
Err(e) => tracing::warn!(error = %e, "日聚合任务失败"),
|
||
}
|
||
}
|
||
_ = tokio::signal::ctrl_c() => {
|
||
tracing::info!("日聚合任务收到关闭信号,正在停止");
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
pub fn public_routes<S>() -> Router<S>
|
||
where
|
||
crate::state::HealthState: axum::extract::FromRef<S>,
|
||
S: Clone + Send + Sync + 'static,
|
||
{
|
||
Router::new()
|
||
.route(
|
||
"/oauth/token",
|
||
axum::routing::post(crate::oauth::handler::token),
|
||
)
|
||
.route(
|
||
"/public/banners",
|
||
axum::routing::get(banner_handler::list_public_banners),
|
||
)
|
||
.route(
|
||
"/public/banner-image/{banner_id}",
|
||
axum::routing::get(banner_handler::serve_banner_image),
|
||
)
|
||
.route(
|
||
"/public/articles",
|
||
axum::routing::get(article_handler::list_public_articles),
|
||
)
|
||
.route(
|
||
"/public/articles/{id}",
|
||
axum::routing::get(article_handler::get_public_article),
|
||
)
|
||
}
|
||
|
||
/// FHIR R4 只读路由(使用 OAuth client_credentials 认证)
|
||
pub fn fhir_routes<S>() -> Router<S>
|
||
where
|
||
crate::state::HealthState: axum::extract::FromRef<S>,
|
||
S: Clone + Send + Sync + 'static,
|
||
{
|
||
use crate::fhir::handler as fhir;
|
||
|
||
Router::new()
|
||
.route(
|
||
"/R4/metadata",
|
||
axum::routing::get(fhir::capability_statement),
|
||
)
|
||
// Patient
|
||
.route("/R4/Patient", axum::routing::get(fhir::search_patients))
|
||
.route("/R4/Patient/{id}", axum::routing::get(fhir::get_patient))
|
||
// Observation
|
||
.route(
|
||
"/R4/Observation",
|
||
axum::routing::get(fhir::search_observations),
|
||
)
|
||
// Device
|
||
.route("/R4/Device", axum::routing::get(fhir::search_devices))
|
||
.route("/R4/Device/{id}", axum::routing::get(fhir::get_device))
|
||
// Practitioner
|
||
.route(
|
||
"/R4/Practitioner",
|
||
axum::routing::get(fhir::search_practitioners),
|
||
)
|
||
.route(
|
||
"/R4/Practitioner/{id}",
|
||
axum::routing::get(fhir::get_practitioner),
|
||
)
|
||
// Appointment
|
||
.route(
|
||
"/R4/Appointment",
|
||
axum::routing::get(fhir::search_appointments),
|
||
)
|
||
.route(
|
||
"/R4/Appointment/{id}",
|
||
axum::routing::get(fhir::get_appointment),
|
||
)
|
||
// DiagnosticReport
|
||
.route(
|
||
"/R4/DiagnosticReport",
|
||
axum::routing::get(fhir::search_diagnostic_reports),
|
||
)
|
||
.route(
|
||
"/R4/DiagnosticReport/{id}",
|
||
axum::routing::get(fhir::get_diagnostic_report),
|
||
)
|
||
// Encounter
|
||
.route("/R4/Encounter", axum::routing::get(fhir::search_encounters))
|
||
.route(
|
||
"/R4/Encounter/{id}",
|
||
axum::routing::get(fhir::get_encounter),
|
||
)
|
||
// Task
|
||
.route("/R4/Task", axum::routing::get(fhir::search_tasks))
|
||
.route("/R4/Task/{id}", axum::routing::get(fhir::get_task))
|
||
// $everything
|
||
.route(
|
||
"/R4/Patient/{id}/$everything",
|
||
axum::routing::get(fhir::patient_everything),
|
||
)
|
||
// metadata 端点不需要认证,其他端点需要 OAuth Bearer token
|
||
.layer(axum::middleware::from_fn(
|
||
crate::oauth::middleware::oauth_auth_middleware,
|
||
))
|
||
}
|
||
|
||
pub fn fhir_routes_with_state<S>(jwt_secret: String) -> Router<S>
|
||
where
|
||
crate::state::HealthState: axum::extract::FromRef<S>,
|
||
S: Clone + Send + Sync + 'static,
|
||
{
|
||
use crate::fhir::handler as fhir;
|
||
|
||
Router::new()
|
||
.route(
|
||
"/R4/metadata",
|
||
axum::routing::get(fhir::capability_statement),
|
||
)
|
||
.route("/R4/Patient", axum::routing::get(fhir::search_patients))
|
||
.route("/R4/Patient/{id}", axum::routing::get(fhir::get_patient))
|
||
.route(
|
||
"/R4/Observation",
|
||
axum::routing::get(fhir::search_observations),
|
||
)
|
||
.route("/R4/Device", axum::routing::get(fhir::search_devices))
|
||
.route("/R4/Device/{id}", axum::routing::get(fhir::get_device))
|
||
.route(
|
||
"/R4/Practitioner",
|
||
axum::routing::get(fhir::search_practitioners),
|
||
)
|
||
.route(
|
||
"/R4/Practitioner/{id}",
|
||
axum::routing::get(fhir::get_practitioner),
|
||
)
|
||
.route(
|
||
"/R4/Appointment",
|
||
axum::routing::get(fhir::search_appointments),
|
||
)
|
||
.route(
|
||
"/R4/Appointment/{id}",
|
||
axum::routing::get(fhir::get_appointment),
|
||
)
|
||
.route(
|
||
"/R4/DiagnosticReport",
|
||
axum::routing::get(fhir::search_diagnostic_reports),
|
||
)
|
||
.route(
|
||
"/R4/DiagnosticReport/{id}",
|
||
axum::routing::get(fhir::get_diagnostic_report),
|
||
)
|
||
.route("/R4/Encounter", axum::routing::get(fhir::search_encounters))
|
||
.route(
|
||
"/R4/Encounter/{id}",
|
||
axum::routing::get(fhir::get_encounter),
|
||
)
|
||
.route("/R4/Task", axum::routing::get(fhir::search_tasks))
|
||
.route("/R4/Task/{id}", axum::routing::get(fhir::get_task))
|
||
.route(
|
||
"/R4/Patient/{id}/$everything",
|
||
axum::routing::get(fhir::patient_everything),
|
||
)
|
||
.layer(axum::middleware::from_fn(move |req, next| {
|
||
let secret = jwt_secret.clone();
|
||
async move {
|
||
crate::oauth::middleware::oauth_auth_middleware_with_secret(&secret, req, next)
|
||
.await
|
||
}
|
||
}))
|
||
}
|
||
|
||
pub fn protected_routes<S>() -> Router<S>
|
||
where
|
||
crate::state::HealthState: axum::extract::FromRef<S>,
|
||
S: Clone + Send + Sync + 'static,
|
||
{
|
||
Router::new()
|
||
.merge(crate::routes::patient::routes())
|
||
.merge(crate::routes::health_data::routes())
|
||
.merge(crate::routes::follow_up::routes())
|
||
.merge(crate::routes::appointment::routes())
|
||
.merge(crate::routes::consultation::routes())
|
||
.merge(crate::routes::article::routes())
|
||
.merge(crate::routes::points::routes())
|
||
.merge(crate::routes::stats::routes())
|
||
.merge(crate::routes::alert::routes())
|
||
.merge(crate::routes::device::routes())
|
||
.merge(crate::routes::media::routes())
|
||
.merge(crate::routes::care::routes())
|
||
.merge(crate::routes::admin::routes())
|
||
}
|
||
|
||
/// BLE 网关数据接入路由(裸路由,需在 erp-server 层配合 gateway_auth 中间件使用)
|
||
pub fn gateway_routes<S>() -> Router<S>
|
||
where
|
||
crate::state::HealthState: axum::extract::FromRef<S>,
|
||
S: Clone + Send + Sync + 'static,
|
||
{
|
||
Router::new()
|
||
.route(
|
||
"/upload",
|
||
axum::routing::post(ble_gateway_handler::gateway_upload),
|
||
)
|
||
.route(
|
||
"/heartbeat",
|
||
axum::routing::post(ble_gateway_handler::gateway_heartbeat),
|
||
)
|
||
}
|
||
}
|
||
|
||
impl Default for HealthModule {
|
||
fn default() -> Self {
|
||
Self::new()
|
||
}
|
||
}
|
||
|
||
#[async_trait::async_trait]
|
||
impl ErpModule for HealthModule {
|
||
fn name(&self) -> &str {
|
||
"health"
|
||
}
|
||
|
||
fn version(&self) -> &str {
|
||
env!("CARGO_PKG_VERSION")
|
||
}
|
||
|
||
fn dependencies(&self) -> Vec<&str> {
|
||
vec!["auth"]
|
||
}
|
||
|
||
fn register_event_handlers(&self, _bus: &EventBus) {
|
||
// 事件处理器已迁移到 on_startup,此处保留空实现以兼容 trait 签名
|
||
}
|
||
|
||
async fn on_startup(
|
||
&self,
|
||
ctx: &erp_core::module::ModuleContext,
|
||
) -> erp_core::error::AppResult<()> {
|
||
let crypto = match erp_core::crypto::PiiCrypto::from_kek_hex(
|
||
&std::env::var("ERP__CRYPTO__KEK").unwrap_or_default(),
|
||
) {
|
||
Ok(c) => c,
|
||
Err(_) => {
|
||
#[cfg(debug_assertions)]
|
||
{
|
||
tracing::warn!("ERP__CRYPTO__KEK 未设置或无效,使用开发默认密钥");
|
||
erp_core::crypto::PiiCrypto::dev_default()
|
||
}
|
||
#[cfg(not(debug_assertions))]
|
||
{
|
||
panic!(
|
||
"ERP__CRYPTO__KEK 必须设置为有效的 64 字符 hex 字符串(生产环境不允许回退到开发密钥)"
|
||
);
|
||
}
|
||
}
|
||
};
|
||
|
||
let state = crate::state::HealthState {
|
||
db: ctx.db.clone(),
|
||
event_bus: ctx.event_bus.clone(),
|
||
crypto,
|
||
jwt_secret: "test-jwt-secret".to_string(),
|
||
};
|
||
|
||
crate::event::register_handlers_with_state(state.clone());
|
||
tracing::info!(
|
||
module = "health",
|
||
"Health module event handlers registered via on_startup"
|
||
);
|
||
|
||
// 启动逾期随访检查器(立即执行一次 + 每 6 小时重复)
|
||
{
|
||
let state_clone = state.clone();
|
||
tokio::spawn(async move {
|
||
match crate::service::follow_up_service::check_overdue_and_notify(&state_clone)
|
||
.await
|
||
{
|
||
Ok(count) if count > 0 => {
|
||
tracing::info!(count = count, "启动时逾期随访检查完成")
|
||
}
|
||
Ok(_) => tracing::info!("启动时逾期随访检查完成(无逾期任务)"),
|
||
Err(e) => tracing::warn!(error = %e, "启动时逾期随访检查失败"),
|
||
}
|
||
});
|
||
}
|
||
let _overdue_handle = Self::start_overdue_checker(state.db.clone());
|
||
tracing::info!(module = "health", "Overdue follow-up checker started");
|
||
|
||
// 启动积分过期清理(启动时执行一次 + 每 24 小时重复)
|
||
{
|
||
let db = ctx.db.clone();
|
||
let event_bus = ctx.event_bus.clone();
|
||
tokio::spawn(async move {
|
||
match crate::service::points_service::expire_points(&db, &event_bus).await {
|
||
Ok(count) if count > 0 => {
|
||
tracing::info!(count = count, "启动时积分过期清理完成")
|
||
}
|
||
Ok(_) => tracing::info!("启动时积分过期清理完成(无过期积分)"),
|
||
Err(e) => tracing::warn!(error = %e, "启动时积分过期清理失败"),
|
||
}
|
||
});
|
||
}
|
||
let _expire_handle =
|
||
Self::start_points_expiration_checker(ctx.db.clone(), ctx.event_bus.clone());
|
||
tracing::info!(module = "health", "Points expiration checker started");
|
||
|
||
// 启动预约提醒调度(启动时立即执行一次 + 每 1 小时重复)
|
||
{
|
||
let db = ctx.db.clone();
|
||
let event_bus = ctx.event_bus.clone();
|
||
tokio::spawn(async move {
|
||
match crate::service::appointment_service::send_reminders(&db, &event_bus).await {
|
||
Ok(count) if count > 0 => {
|
||
tracing::info!(count = count, "启动时预约提醒发送完成")
|
||
}
|
||
Ok(_) => tracing::info!("启动时预约提醒检查完成"),
|
||
Err(e) => tracing::warn!(error = %e, "启动时预约提醒发送失败"),
|
||
}
|
||
});
|
||
}
|
||
let _reminder_handle =
|
||
Self::start_appointment_reminder(ctx.db.clone(), ctx.event_bus.clone());
|
||
tracing::info!(module = "health", "Appointment reminder scheduler started");
|
||
|
||
// 启动设备原始数据清理(每 24 小时删除超过 90 天的数据)
|
||
let _cleanup_handle = Self::start_device_readings_cleanup(ctx.db.clone());
|
||
tracing::info!(module = "health", "Device readings cleanup task started");
|
||
|
||
// 启动日聚合任务(每 24 小时从前一天的 hourly 数据聚合到 daily)
|
||
{
|
||
let db = ctx.db.clone();
|
||
tokio::spawn(async move {
|
||
let yesterday = chrono::Local::now().date_naive() - chrono::Duration::days(1);
|
||
match crate::service::vital_signs_daily_service::aggregate_daily_for_all_tenants(
|
||
&db, yesterday,
|
||
)
|
||
.await
|
||
{
|
||
Ok(count) if count > 0 => tracing::info!(count = count, "启动时日聚合完成"),
|
||
Ok(_) => tracing::info!("启动时日聚合完成(无数据)"),
|
||
Err(e) => tracing::warn!(error = %e, "启动时日聚合失败"),
|
||
}
|
||
});
|
||
}
|
||
let _daily_agg_handle = Self::start_daily_aggregation(ctx.db.clone());
|
||
tracing::info!(module = "health", "Daily aggregation task started");
|
||
|
||
Ok(())
|
||
}
|
||
|
||
async fn on_tenant_created(
|
||
&self,
|
||
tenant_id: Uuid,
|
||
db: &sea_orm::DatabaseConnection,
|
||
_event_bus: &EventBus,
|
||
) -> AppResult<()> {
|
||
crate::service::seed::seed_tenant_health(db, tenant_id)
|
||
.await
|
||
.map_err(|e| erp_core::error::AppError::Internal(e.to_string()))?;
|
||
tracing::info!(tenant_id = %tenant_id, "Health module tenant initialized");
|
||
Ok(())
|
||
}
|
||
|
||
async fn on_tenant_deleted(
|
||
&self,
|
||
tenant_id: Uuid,
|
||
db: &sea_orm::DatabaseConnection,
|
||
) -> AppResult<()> {
|
||
crate::service::seed::soft_delete_tenant_data(db, tenant_id)
|
||
.await
|
||
.map_err(|e| erp_core::error::AppError::Internal(e.to_string()))?;
|
||
tracing::info!(tenant_id = %tenant_id, "Health module tenant data soft-deleted");
|
||
Ok(())
|
||
}
|
||
|
||
fn permissions(&self) -> Vec<PermissionDescriptor> {
|
||
vec![
|
||
PermissionDescriptor {
|
||
code: "health.patient.list".into(),
|
||
name: "查看患者列表".into(),
|
||
description: "查看和搜索患者列表、详情".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.patient.manage".into(),
|
||
name: "管理患者".into(),
|
||
description: "创建、编辑、删除患者".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.health-data.list".into(),
|
||
name: "查看健康数据".into(),
|
||
description: "查看体检记录、监测数据、化验报告".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.health-data.manage".into(),
|
||
name: "管理健康数据".into(),
|
||
description: "录入、编辑、删除健康数据".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.appointment.list".into(),
|
||
name: "查看预约".into(),
|
||
description: "查看预约列表和排班".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.appointment.manage".into(),
|
||
name: "管理预约".into(),
|
||
description: "创建、确认、取消预约".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.follow-up.list".into(),
|
||
name: "查看随访".into(),
|
||
description: "查看随访任务和记录".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.follow-up.manage".into(),
|
||
name: "管理随访".into(),
|
||
description: "创建、分配、完成随访任务".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.consultation.list".into(),
|
||
name: "查看咨询".into(),
|
||
description: "查看咨询会话和消息记录".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.consultation.manage".into(),
|
||
name: "管理咨询".into(),
|
||
description: "关闭会话、导出记录".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.doctor.list".into(),
|
||
name: "查看医护".into(),
|
||
description: "查看医护列表和详情".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.doctor.manage".into(),
|
||
name: "管理医护".into(),
|
||
description: "创建、编辑医护档案、排班".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.articles.list".into(),
|
||
name: "查看资讯".into(),
|
||
description: "查看健康资讯文章列表和详情".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.articles.manage".into(),
|
||
name: "管理资讯".into(),
|
||
description: "创建、编辑、删除健康资讯文章".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.articles.review".into(),
|
||
name: "审核资讯".into(),
|
||
description: "审核通过或拒绝资讯文章发布".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.points.list".into(),
|
||
name: "查看积分".into(),
|
||
description: "查看积分规则、订单列表".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.points.manage".into(),
|
||
name: "管理积分".into(),
|
||
description: "创建积分规则、管理商品、核销订单".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.device-readings.list".into(),
|
||
name: "查看设备数据".into(),
|
||
description: "查看患者的设备采集数据".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.device-readings.manage".into(),
|
||
name: "管理设备数据".into(),
|
||
description: "提交设备采集数据".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.devices.list".into(),
|
||
name: "查看设备绑定".into(),
|
||
description: "查看设备绑定记录列表".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.devices.manage".into(),
|
||
name: "管理设备绑定".into(),
|
||
description: "解绑设备".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.alerts.list".into(),
|
||
name: "查看告警".into(),
|
||
description: "查看告警记录".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.alerts.manage".into(),
|
||
name: "管理告警".into(),
|
||
description: "确认/处置告警".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.alert-rules.list".into(),
|
||
name: "查看告警规则".into(),
|
||
description: "查看告警规则配置".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.alert-rules.manage".into(),
|
||
name: "管理告警规则".into(),
|
||
description: "创建/编辑/启停告警规则".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.critical-alerts.list".into(),
|
||
name: "查看危急值告警".into(),
|
||
description: "查看危急值告警列表和详情".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.critical-alerts.manage".into(),
|
||
name: "处理危急值告警".into(),
|
||
description: "确认危急值告警".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.critical-value-thresholds.list".into(),
|
||
name: "查看危急值阈值".into(),
|
||
description: "查看危急值阈值配置".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.critical-value-thresholds.manage".into(),
|
||
name: "管理危急值阈值".into(),
|
||
description: "创建/编辑/删除危急值阈值配置".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.follow-up-templates.list".into(),
|
||
name: "查看随访模板".into(),
|
||
description: "查看随访模板列表和详情".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.follow-up-templates.manage".into(),
|
||
name: "管理随访模板".into(),
|
||
description: "创建/编辑/删除随访模板".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.daily-monitoring.list".into(),
|
||
name: "查看日常监测".into(),
|
||
description: "查看患者日常监测数据".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.daily-monitoring.manage".into(),
|
||
name: "管理日常监测".into(),
|
||
description: "录入/编辑/删除日常监测数据".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.consent.list".into(),
|
||
name: "查看知情同意".into(),
|
||
description: "查看患者知情同意记录".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.consent.manage".into(),
|
||
name: "管理知情同意".into(),
|
||
description: "签署/撤销知情同意".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.medication-records.list".into(),
|
||
name: "查看用药记录".into(),
|
||
description: "查看患者用药记录列表和详情".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.medication-records.manage".into(),
|
||
name: "管理用药记录".into(),
|
||
description: "创建/编辑/删除用药记录".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.medication-reminders.list".into(),
|
||
name: "查看药物提醒".into(),
|
||
description: "查看患者药物提醒列表".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.medication-reminders.manage".into(),
|
||
name: "管理药物提醒".into(),
|
||
description: "创建/编辑/删除药物提醒".into(),
|
||
module: "health".into(),
|
||
},
|
||
// 行动收件箱
|
||
PermissionDescriptor {
|
||
code: "health.action-inbox.list".into(),
|
||
name: "查看行动收件箱".into(),
|
||
description: "查看统一行动收件箱中的待办事项".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.action-inbox.manage".into(),
|
||
name: "管理行动项".into(),
|
||
description: "审批/拒绝/标记行动收件箱中的事项".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.action-inbox.team".into(),
|
||
name: "查看团队概览".into(),
|
||
description: "查看科室团队工作负载和风险分布(主任专属)".into(),
|
||
module: "health".into(),
|
||
},
|
||
// 工作台管理
|
||
PermissionDescriptor {
|
||
code: "health.dashboard.manage".into(),
|
||
name: "工作台管理".into(),
|
||
description: "查看系统健康、用户活跃度、模块状态等管理统计".into(),
|
||
module: "health".into(),
|
||
},
|
||
// OAuth 合作方管理
|
||
PermissionDescriptor {
|
||
code: "health.oauth.list".into(),
|
||
name: "查看合作方".into(),
|
||
description: "查看 FHIR API 合作方列表".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.oauth.manage".into(),
|
||
name: "管理合作方".into(),
|
||
description: "创建/编辑/删除 FHIR API 合作方".into(),
|
||
module: "health".into(),
|
||
},
|
||
// 护理计划
|
||
PermissionDescriptor {
|
||
code: "health.care-plan.list".into(),
|
||
name: "查看护理计划".into(),
|
||
description: "查看护理计划、条目和预后测量".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.care-plan.manage".into(),
|
||
name: "管理护理计划".into(),
|
||
description: "创建/编辑/删除护理计划、条目和预后测量".into(),
|
||
module: "health".into(),
|
||
},
|
||
// 班次管理
|
||
PermissionDescriptor {
|
||
code: "health.shifts.list".into(),
|
||
name: "查看班次".into(),
|
||
description: "查看班次列表、患者分配和交接记录".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.shifts.manage".into(),
|
||
name: "管理班次".into(),
|
||
description: "创建/编辑班次、分配患者、创建交接记录".into(),
|
||
module: "health".into(),
|
||
},
|
||
// BLE 网关管理
|
||
PermissionDescriptor {
|
||
code: "health.ble-gateways.list".into(),
|
||
name: "查看 BLE 网关".into(),
|
||
description: "查看 BLE 网关列表、绑定患者和状态".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.ble-gateways.manage".into(),
|
||
name: "管理 BLE 网关".into(),
|
||
description: "注册/编辑/删除 BLE 网关、管理患者绑定".into(),
|
||
module: "health".into(),
|
||
},
|
||
// 家庭成员健康代理
|
||
PermissionDescriptor {
|
||
code: "health.family-proxy.list".into(),
|
||
name: "查看家庭健康代理".into(),
|
||
description: "家庭成员查看关联患者列表和健康摘要".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.family-proxy.manage".into(),
|
||
name: "管理家庭健康代理".into(),
|
||
description: "授权/撤销家庭成员健康数据访问".into(),
|
||
module: "health".into(),
|
||
},
|
||
// 媒体库
|
||
PermissionDescriptor {
|
||
code: "health.media.list".into(),
|
||
name: "查看媒体库".into(),
|
||
description: "查看媒体文件列表、文件夹和详情".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.media.manage".into(),
|
||
name: "管理媒体库".into(),
|
||
description: "上传/编辑/删除媒体文件、管理文件夹".into(),
|
||
module: "health".into(),
|
||
},
|
||
// 轮播图
|
||
PermissionDescriptor {
|
||
code: "health.banners.list".into(),
|
||
name: "查看轮播图".into(),
|
||
description: "查看轮播图列表和详情".into(),
|
||
module: "health".into(),
|
||
},
|
||
PermissionDescriptor {
|
||
code: "health.banners.manage".into(),
|
||
name: "管理轮播图".into(),
|
||
description: "创建/编辑/删除/排序轮播图".into(),
|
||
module: "health".into(),
|
||
},
|
||
]
|
||
}
|
||
|
||
fn as_any(&self) -> &dyn std::any::Any {
|
||
self
|
||
}
|
||
}
|