diff --git a/crates/erp-config/src/dto.rs b/crates/erp-config/src/dto.rs index 14fd801..5bff8f5 100644 --- a/crates/erp-config/src/dto.rs +++ b/crates/erp-config/src/dto.rs @@ -224,6 +224,23 @@ pub struct ThemeResp { pub logo_url: Option, #[serde(skip_serializing_if = "Option::is_none")] pub sidebar_style: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub brand_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub brand_slogan: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub brand_features: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub brand_copyright: Option, +} + +/// 品牌信息公开响应(不含内部配置) +#[derive(Debug, Serialize, Deserialize, ToSchema, Clone)] +pub struct PublicBrandResp { + pub brand_name: String, + pub brand_slogan: String, + pub brand_features: String, + pub brand_copyright: String, } // --- Language DTOs (stored via settings) --- diff --git a/crates/erp-config/src/handler/theme_handler.rs b/crates/erp-config/src/handler/theme_handler.rs index cb6691c..c141876 100644 --- a/crates/erp-config/src/handler/theme_handler.rs +++ b/crates/erp-config/src/handler/theme_handler.rs @@ -7,7 +7,7 @@ use erp_core::rbac::require_permission; use erp_core::types::{ApiResponse, TenantContext}; use crate::config_state::ConfigState; -use crate::dto::{SetSettingParams, ThemeResp}; +use crate::dto::{PublicBrandResp, SetSettingParams, ThemeResp}; use crate::error::ConfigError; use crate::service::setting_service::SettingService; @@ -17,6 +17,10 @@ fn default_theme() -> ThemeResp { primary_color: None, logo_url: None, sidebar_style: None, + brand_name: Some("HMS 健康管理平台".into()), + brand_slogan: Some("新一代健康管理平台".into()), + brand_features: Some("患者管理 · 健康监测 · 随访管理 · AI 智能分析".into()), + brand_copyright: Some("HMS 健康管理平台 · ©汕头市智界科技有限公司".into()), } } @@ -107,16 +111,45 @@ where Ok(JsonResponse(ApiResponse::ok(req))) } +#[utoipa::path( + get, + path = "/api/v1/public/brand", + responses( + (status = 200, description = "成功", body = ApiResponse), + ), + tag = "主题设置" +)] +/// GET /api/v1/public/brand +/// +/// 获取公开品牌信息(无需认证)。 +pub async fn get_public_brand() -> JsonResponse> { + let defaults = default_theme(); + JsonResponse(ApiResponse::ok(PublicBrandResp { + brand_name: defaults.brand_name.unwrap_or_else(|| "HMS 健康管理平台".into()), + brand_slogan: defaults.brand_slogan.unwrap_or_else(|| "新一代健康管理平台".into()), + brand_features: defaults + .brand_features + .unwrap_or_else(|| "患者管理 · 健康监测 · 随访管理 · AI 智能分析".into()), + brand_copyright: defaults + .brand_copyright + .unwrap_or_else(|| "HMS 健康管理平台 · ©汕头市智界科技有限公司".into()), + })) +} + #[cfg(test)] mod tests { use super::*; #[test] - fn default_theme_all_fields_none() { + fn default_theme_has_brand_defaults() { let theme = default_theme(); assert!(theme.primary_color.is_none()); assert!(theme.logo_url.is_none()); assert!(theme.sidebar_style.is_none()); + assert_eq!(theme.brand_name, Some("HMS 健康管理平台".to_string())); + assert_eq!(theme.brand_slogan, Some("新一代健康管理平台".to_string())); + assert!(theme.brand_features.is_some()); + assert!(theme.brand_copyright.is_some()); } #[test] @@ -125,11 +158,15 @@ mod tests { primary_color: Some("#1890ff".to_string()), logo_url: None, sidebar_style: Some("dark".to_string()), + brand_name: Some("测试平台".to_string()), + brand_slogan: None, + brand_features: None, + brand_copyright: None, }; let json = serde_json::to_string(&theme).unwrap(); let back: ThemeResp = serde_json::from_str(&json).unwrap(); assert_eq!(back.primary_color, Some("#1890ff".to_string())); - assert_eq!(back.logo_url, None); - assert_eq!(back.sidebar_style, Some("dark".to_string())); + assert_eq!(back.brand_name, Some("测试平台".to_string())); + assert!(back.brand_slogan.is_none()); } } diff --git a/crates/erp-config/src/module.rs b/crates/erp-config/src/module.rs index 17522b8..f31fea8 100644 --- a/crates/erp-config/src/module.rs +++ b/crates/erp-config/src/module.rs @@ -102,6 +102,14 @@ impl ConfigModule { put(language_handler::update_language), ) } + + /// Build public (unauthenticated) routes for the config module. + pub fn public_routes() -> Router + where + S: Clone + Send + Sync + 'static, + { + Router::new().route("/public/brand", get(theme_handler::get_public_brand)) + } } impl Default for ConfigModule { diff --git a/crates/erp-server/src/main.rs b/crates/erp-server/src/main.rs index b655b8a..0a380d3 100644 --- a/crates/erp-server/src/main.rs +++ b/crates/erp-server/src/main.rs @@ -535,13 +535,14 @@ async fn main() -> anyhow::Result<()> { )) .with_state(state.clone()); - // Unthrottled public routes (health, docs) — no rate limiting + // Unthrottled public routes (health, docs, brand) — no rate limiting let unthrottled_routes = Router::new() .merge(handlers::health::health_check_router()) .route( "/docs/openapi.json", axum::routing::get(handlers::openapi::openapi_spec), ) + .merge(erp_config::ConfigModule::public_routes()) .with_state(state.clone()); // Clone jwt_secret for upload auth before protected_routes closure moves it