Files
hms/crates/erp-health/src/module.rs
iven 6841c45846 fix(security): 文件上传 MIME 白名单 + OAuth JWT 密钥路径统一
P0 #1: 媒体文件上传增加 MIME 类型白名单校验(jpeg/png/gif/webp/svg/mp4/webm/pdf)
       和文件大小限制(10MB),扩展名使用白名单清理防止路径遍历攻击。
P0 #2: OAuth JWT 密钥从环境变量改为 State 注入,消除运行时 env::var 依赖,
       FHIR 路由中间件使用闭包捕获 jwt_secret 保持类型安全。
2026-05-17 12:40:02 +08:00

864 lines
36 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use 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
}
}