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() -> Router where crate::state::HealthState: axum::extract::FromRef, 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() -> Router where crate::state::HealthState: axum::extract::FromRef, 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(jwt_secret: String) -> Router where crate::state::HealthState: axum::extract::FromRef, 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() -> Router where crate::state::HealthState: axum::extract::FromRef, 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() -> Router where crate::state::HealthState: axum::extract::FromRef, 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 { 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 } }