use axum::Extension; use axum::extract::{FromRef, Json, State}; use axum::response::Json as JsonResponse; use erp_core::error::AppError; use erp_core::rbac::require_permission; use erp_core::types::{ApiResponse, TenantContext}; use crate::config_state::ConfigState; use crate::dto::{PublicBrandResp, SetSettingParams, ThemeResp}; use crate::error::ConfigError; use crate::service::setting_service::SettingService; /// 默认主题配置。 fn default_theme() -> ThemeResp { 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()), } } #[utoipa::path( get, path = "/api/v1/themes", responses( (status = 200, description = "成功", body = ApiResponse), (status = 401, description = "未授权"), (status = 403, description = "权限不足"), ), security(("bearer_auth" = [])), tag = "主题设置" )] /// GET /api/v1/theme /// /// 获取当前租户的主题配置。 /// 主题配置存储在 settings 表中,key 为 "theme",scope 为 "tenant"。 /// 当没有任何主题配置时,返回默认主题(所有字段为 null)。 /// 需要 `theme.read` 权限。 pub async fn get_theme( State(state): State, Extension(ctx): Extension, ) -> Result>, AppError> where ConfigState: FromRef, S: Clone + Send + Sync + 'static, { require_permission(&ctx, "theme.read")?; let theme = match SettingService::get("theme", "tenant", &None, ctx.tenant_id, &state.db).await { Ok(setting) => serde_json::from_value(setting.setting_value) .map_err(|e| AppError::Validation(format!("主题配置解析失败: {e}")))?, Err(ConfigError::NotFound(_)) => default_theme(), Err(e) => return Err(e.into()), }; Ok(JsonResponse(ApiResponse::ok(theme))) } #[utoipa::path( put, path = "/api/v1/themes", request_body = ThemeResp, responses( (status = 200, description = "成功", body = ApiResponse), (status = 401, description = "未授权"), (status = 403, description = "权限不足"), ), security(("bearer_auth" = [])), tag = "主题设置" )] /// PUT /api/v1/theme /// /// 更新当前租户的主题配置。 /// 将主题配置序列化为 JSON 存储到 settings 表。 /// 需要 `theme.update` 权限。 pub async fn update_theme( State(state): State, Extension(ctx): Extension, Json(req): Json, ) -> Result>, AppError> where ConfigState: FromRef, S: Clone + Send + Sync + 'static, { require_permission(&ctx, "theme.update")?; let value = serde_json::to_value(&req) .map_err(|e| AppError::Validation(format!("主题配置序列化失败: {e}")))?; SettingService::set( SetSettingParams { key: "theme".to_string(), scope: "tenant".to_string(), scope_id: None, value, version: None, }, ctx.tenant_id, ctx.user_id, &state.db, &state.event_bus, ) .await?; 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_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] fn theme_resp_serde_roundtrip() { let theme = ThemeResp { 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.brand_name, Some("测试平台".to_string())); assert!(back.brand_slogan.is_none()); } }