use crate::auth::get_auth_user; use crate::graphql::guards::{ RequireMultiplePermissions, RequirePermission, RequireReadPermission, }; use crate::graphql::types::*; use crate::models::blog::{ Blog, BlogCategory, BlogCategoryFilterInput, BlogDetail, BlogFilterInput, BlogSortInput, BlogStats, BlogTag, BlogTagFilterInput, }; use crate::models::invite_code::InviteCode; use crate::models::page_block::{PaginatedResult, PaginationInput}; use crate::models::settings::SettingFilter; use crate::models::user::{User, UserInfoRow}; use crate::services::blog_service::BlogService; use crate::services::casbin_service::CasbinService; use crate::services::invite_code_service::InviteCodeService; use crate::services::page_block_service::PageBlockService; use crate::services::settings_service::SettingsService; use crate::services::user_service::UserService; use async_graphql::Error as GraphQLError; use async_graphql::{Context, Object, Result}; use tracing::info; use uuid::Uuid; pub struct QueryRoot; #[Object] impl QueryRoot { async fn health_check(&self) -> &str { "OK" } #[graphql(guard = "RequireReadPermission::new(\"users\")")] async fn current_user(&self, ctx: &Context<'_>) -> Result { let auth_user = get_auth_user(ctx).await?; let user_service = ctx.data::()?; user_service .get_user_by_id(auth_user.id) .await? .ok_or_else(|| async_graphql::Error::new("User not found")) } #[graphql(guard = "RequireReadPermission::new(\"admin\")")] async fn secret_data(&self, _ctx: &Context<'_>) -> &str { "This is super secret admin data!" } #[graphql(guard = "RequireReadPermission::new(\"users\")")] async fn users( &self, ctx: &Context<'_>, offset: Option, limit: Option, sort_by: Option, sort_order: Option, filter: Option, ) -> Result> { let user_service = ctx.data::()?; info!("users im here"); let offset = offset.unwrap_or(0); let limit = limit.unwrap_or(20); let sort_by = sort_by.unwrap_or("created_at".to_string()); let sort_order = sort_order.unwrap_or("desc".to_string()); user_service .get_all_users(offset, limit, sort_by, sort_order, filter) .await } #[graphql(guard = "RequireReadPermission::new(\"invite_codes\")")] async fn my_invite_codes(&self, ctx: &Context<'_>) -> Result> { let auth_user = get_auth_user(ctx).await?; let invite_code_service = ctx.data::()?; invite_code_service .get_invite_codes_by_creator(auth_user.id) .await } #[graphql(guard = "RequirePermission::new(\"invite_codes\", \"write\")")] async fn validate_invite_code(&self, ctx: &Context<'_>, code: String) -> Result { let invite_code_service = ctx.data::()?; invite_code_service .validate_invite_code(crate::models::invite_code::ValidateInviteCodeInput { code }) .await } #[graphql(guard = "RequireReadPermission::new(\"users\")")] async fn users_info( &self, ctx: &Context<'_>, offset: Option, limit: Option, sort_by: Option, sort_order: Option, filter: Option, ) -> Result { let user_service = ctx.data::()?; let offset = offset.unwrap_or(0); let limit = limit.unwrap_or(20); let sort_by = sort_by.unwrap_or("created_at".to_string()); let sort_order = sort_order.unwrap_or("desc".to_string()); user_service .users_info(offset, limit, sort_by, sort_order, filter) .await } // Settings queries #[graphql(guard = "RequireReadPermission::new(\"settings\")")] async fn settings( &self, ctx: &Context<'_>, filter: Option, ) -> Result> { let settings_service = ctx.data::()?; let filter = filter.map(|f| SettingFilter { category: f.category, is_system: f.is_system, is_editable: f.is_editable, search: f.search, }); let settings = if let Some(filter) = filter { settings_service.get_settings_with_filter(&filter).await? } else { settings_service.get_all_settings().await? }; Ok(settings .into_iter() .map(|s| SettingType { id: s.id, key: s.key, value: s.value, value_type: s.value_type, description: s.description, category: s.category, is_encrypted: s.is_encrypted, is_system: s.is_system, is_editable: s.is_editable, created_at: s.created_at, updated_at: s.updated_at, created_by: s.created_by, updated_by: s.updated_by, }) .collect()) } #[graphql(guard = "RequireReadPermission::new(\"settings\")")] async fn setting_by_key(&self, ctx: &Context<'_>, key: String) -> Result> { let settings_service = ctx.data::()?; let setting = settings_service.get_setting_by_key(&key).await?; Ok(setting.map(|s| SettingType { id: s.id, key: s.key, value: s.value, value_type: s.value_type, description: s.description, category: s.category, is_encrypted: s.is_encrypted, is_system: s.is_system, is_editable: s.is_editable, created_at: s.created_at, updated_at: s.updated_at, created_by: s.created_by, updated_by: s.updated_by, })) } #[graphql(guard = "RequireReadPermission::new(\"settings\")")] async fn setting_by_id(&self, ctx: &Context<'_>, id: Uuid) -> Result> { let settings_service = ctx.data::()?; let setting = settings_service.get_setting_by_id(id).await?; Ok(setting.map(|s| SettingType { id: s.id, key: s.key, value: s.value, value_type: s.value_type, description: s.description, category: s.category, is_encrypted: s.is_encrypted, is_system: s.is_system, is_editable: s.is_editable, created_at: s.created_at, updated_at: s.updated_at, created_by: s.created_by, updated_by: s.updated_by, })) } #[graphql( guard = "RequireMultiplePermissions::new(&[(\"settings\", \"read\"), (\"pages\", \"read\")])" )] async fn setting_categories(&self, ctx: &Context<'_>) -> Result> { let page_block_service = ctx.data::()?; let settings_service = ctx.data::()?; // 获取所有配置分类 let categories = settings_service.get_categories().await?; let mut category_pages = Vec::new(); // 为每个分类创建 CategoryPageType for category in categories { // 获取页面和统计信息 let (page, total_count, system_count, editable_count) = page_block_service .get_category_page_with_stats(&category) .await?; // 获取该分类下的所有配置项 let filter = SettingFilter { category: Some(category.clone()), is_system: None, is_editable: None, search: None, }; let settings = settings_service.get_settings_with_filter(&filter).await?; // 转换为 GraphQL 类型 let settings_types = settings .into_iter() .map(|s| SettingCenterType { id: s.id, key: s.key, value: if s.is_encrypted.unwrap_or(false) { Some("***".to_string()) // 占位符,不返回明文 } else { s.value }, value_type: s.value_type, is_encrypted: s.is_encrypted, is_editable: s.is_editable, is_system: s.is_system, description: s.description, updated_at: s.updated_at, }) .collect(); // 转换页面类型 let page_type = page.map(|p| PageType { id: p.id, title: p.title, slug: p.slug, description: p.description, is_active: p.is_active, created_at: p.created_at, updated_at: p.updated_at, created_by: p.created_by, updated_by: p.updated_by, }); category_pages.push(CategoryPageType { page: page_type, settings: settings_types, category, settings_count: total_count, system_settings_count: system_count, editable_settings_count: editable_count, }); } Ok(category_pages) } #[graphql(guard = "RequireReadPermission::new(\"settings\")")] async fn settings_stats(&self, ctx: &Context<'_>) -> Result { let settings_service = ctx.data::()?; let categories = settings_service.get_categories().await?; let stats = settings_service.get_settings_stats().await?; Ok(SettingsStatsType { categories, stats }) } #[graphql(guard = "RequireReadPermission::new(\"settings\")")] async fn setting_history( &self, ctx: &Context<'_>, setting_id: Uuid, ) -> Result> { let settings_service = ctx.data::()?; let history = settings_service.get_setting_history(setting_id).await?; Ok(history .into_iter() .map(|h| SettingHistoryType { id: h.id, setting_id: h.setting_id, old_value: h.old_value, new_value: h.new_value, changed_by: h.changed_by, change_reason: h.change_reason, created_at: h.created_at, }) .collect()) } // Page Block queries #[graphql(guard = "RequireReadPermission::new(\"pages\")")] async fn pages( &self, ctx: &Context<'_>, filter: Option, limit: Option, offset: Option, ) -> Result> { let page_block_service = ctx.data::()?; let filter = filter.map(|f| crate::models::page_block::PageFilterInput { title: f.title, slug: f.slug, is_active: f.is_active, search: f.search, }); let pages = page_block_service.get_pages(filter, limit, offset).await?; Ok(pages .into_iter() .map(|p| PageType { id: p.id, title: p.title, slug: p.slug, description: p.description, is_active: p.is_active, created_at: p.created_at, updated_at: p.updated_at, created_by: p.created_by, updated_by: p.updated_by, }) .collect()) } #[graphql(guard = "RequireReadPermission::new(\"pages\")")] async fn page_by_id(&self, ctx: &Context<'_>, id: Uuid) -> Result> { let page_block_service = ctx.data::()?; match page_block_service.get_page_by_id(id).await { Ok(page) => Ok(Some(PageType { id: page.id, title: page.title, slug: page.slug, description: page.description, is_active: page.is_active, created_at: page.created_at, updated_at: page.updated_at, created_by: page.created_by, updated_by: page.updated_by, })), Err(_) => Ok(None), } } #[graphql(guard = "RequireReadPermission::new(\"pages\")")] async fn page_by_slug(&self, ctx: &Context<'_>, slug: String) -> Result> { let page_block_service = ctx.data::()?; match page_block_service.get_page_by_slug(&slug).await { Ok(page) => Ok(Some(PageType { id: page.id, title: page.title, slug: page.slug, description: page.description, is_active: page.is_active, created_at: page.created_at, updated_at: page.updated_at, created_by: page.created_by, updated_by: page.updated_by, })), Err(_) => Ok(None), } } #[graphql(guard = "RequireReadPermission::new(\"page_blocks\")")] async fn page_blocks(&self, ctx: &Context<'_>, page_id: Uuid) -> Result> { let page_block_service = ctx.data::()?; let blocks = page_block_service.get_page_blocks(page_id).await?; Ok(blocks .into_iter() .map(|block| match block { crate::models::page_block::Block::TextBlock(tb) => { BlockType::TextBlock(TextBlockType { id: tb.id, page_id: tb.page_id, block_order: tb.block_order, title: tb.title, markdown: tb.markdown, is_active: tb.is_active, created_at: tb.created_at, updated_at: tb.updated_at, }) } crate::models::page_block::Block::ChartBlock(cb) => { BlockType::ChartBlock(ChartBlockType { id: cb.id, page_id: cb.page_id, block_order: cb.block_order, title: cb.title, chart_type: cb.chart_type, series: cb .series .into_iter() .map(|dp| DataPointType { id: dp.id, chart_block_id: dp.chart_block_id, x: dp.x, y: dp.y, label: dp.label, color: dp.color, }) .collect(), config: cb.config, is_active: cb.is_active, created_at: cb.created_at, updated_at: cb.updated_at, }) } crate::models::page_block::Block::SettingsBlock(sb) => { BlockType::SettingsBlock(SettingsBlockType { id: sb.id, page_id: sb.page_id, block_order: sb.block_order, title: sb.title, category: sb.category, editable: sb.editable, display_mode: sb.display_mode, is_active: sb.is_active, created_at: sb.created_at, updated_at: sb.updated_at, }) } crate::models::page_block::Block::TableBlock(tb) => { BlockType::TableBlock(TableBlockType { id: tb.id, page_id: tb.page_id, block_order: tb.block_order, title: tb.title, columns: tb .columns .into_iter() .map(|col| TableColumnType { id: col.id, table_block_id: col.table_block_id, name: col.name, label: col.label, data_type: col.data_type, is_sortable: col.is_sortable, is_filterable: col.is_filterable, width: col.width, order: col.order, }) .collect(), data_source: tb.data_source, data_config: tb.data_config, is_active: tb.is_active, created_at: tb.created_at, updated_at: tb.updated_at, }) } crate::models::page_block::Block::HeroBlock(hb) => { BlockType::HeroBlock(HeroBlockType { id: hb.id, page_id: hb.page_id, block_order: hb.block_order, title: hb.title, subtitle: hb.subtitle, background_image: hb.background_image, background_color: hb.background_color, text_color: hb.text_color, cta_text: hb.cta_text, cta_link: hb.cta_link, is_active: hb.is_active, created_at: hb.created_at, updated_at: hb.updated_at, }) } }) .collect()) } /// 根据配置分类获取对应的页面 #[graphql( guard = "RequireMultiplePermissions::new(&[(\"pages\", \"read\"), (\"settings\", \"read\")])" )] async fn page_by_category( &self, ctx: &Context<'_>, category: String, ) -> Result { let page_block_service = ctx.data::()?; let settings_service = ctx.data::()?; // 获取页面和统计信息 let (page, total_count, system_count, editable_count) = page_block_service .get_category_page_with_stats(&category) .await?; // 获取该分类下的所有配置项 let filter = SettingFilter { category: Some(category.clone()), is_system: None, is_editable: None, search: None, }; let settings = settings_service.get_settings_with_filter(&filter).await?; // 转换为 GraphQL 类型 let settings_types = settings .into_iter() .map(|s| SettingCenterType { id: s.id, key: s.key, value: if s.is_encrypted.unwrap_or(false) { Some("***".to_string()) // 占位符,不返回明文 } else { s.value }, value_type: s.value_type, is_encrypted: s.is_encrypted, is_editable: s.is_editable, is_system: s.is_system, description: s.description, updated_at: s.updated_at, }) .collect(); // 转换页面类型 let page_type = page.map(|p| PageType { id: p.id, title: p.title, slug: p.slug, description: p.description, is_active: p.is_active, created_at: p.created_at, updated_at: p.updated_at, created_by: p.created_by, updated_by: p.updated_by, }); Ok(CategoryPageType { page: page_type, settings: settings_types, category, settings_count: total_count, system_settings_count: system_count, editable_settings_count: editable_count, }) } /// 获取所有配置分类页面 #[graphql(guard = "RequireReadPermission::new(\"pages\")")] async fn all_category_pages(&self, ctx: &Context<'_>) -> Result> { let page_block_service = ctx.data::()?; let pages = page_block_service.get_all_category_pages().await?; Ok(pages .into_iter() .map(|p| PageType { id: p.id, title: p.title, slug: p.slug, description: p.description, is_active: p.is_active, created_at: p.created_at, updated_at: p.updated_at, created_by: p.created_by, updated_by: p.updated_by, }) .collect()) } // Enhanced Settings queries for settings center #[graphql(guard = "RequireReadPermission::new(\"settings\")")] async fn settings_by_category( &self, ctx: &Context<'_>, category: String, ) -> Result> { let settings_service = ctx.data::()?; let filter = SettingFilter { category: Some(category), is_system: None, is_editable: None, search: None, }; let settings = settings_service.get_settings_with_filter(&filter).await?; Ok(settings .into_iter() .map(|s| SettingCenterType { id: s.id, key: s.key, value: if s.is_encrypted.unwrap_or(false) { Some("***".to_string()) // 占位符,不返回明文 } else { s.value }, value_type: s.value_type, is_encrypted: s.is_encrypted, is_editable: s.is_editable, is_system: s.is_system, description: s.description, updated_at: s.updated_at, }) .collect()) } // 权限管理查询 #[graphql(guard = "RequireReadPermission::new(\"permissions\")")] async fn check_permission( &self, ctx: &Context<'_>, resource: String, action: String, ) -> Result { let user = get_auth_user(ctx).await?; let casbin_service = ctx.data::()?; let has_permission = casbin_service .check_permission(&user.id.to_string(), &resource, &action) .await?; let roles = casbin_service.get_user_roles(&user.id.to_string()).await?; Ok(PermissionCheckResult { user_id: user.id.to_string(), resource, action, has_permission, roles, }) } #[graphql(guard = "RequireReadPermission::new(\"permissions\")")] async fn get_user_roles(&self, ctx: &Context<'_>) -> Result> { let user = get_auth_user(ctx).await?; let casbin_service = ctx.data::()?; let roles = casbin_service.get_user_roles(&user.id.to_string()).await?; Ok(roles) } #[graphql(guard = "RequireReadPermission::new(\"permissions\")")] async fn get_all_policies(&self, ctx: &Context<'_>) -> Result> { let casbin_service = ctx.data::()?; let policies = casbin_service.get_all_policies().await?; Ok(policies .into_iter() .filter(|p| p.len() >= 3) .map(|p| PolicyType { role: p[0].clone(), resource: p[1].clone(), action: p[2].clone(), }) .collect()) } #[graphql(guard = "RequireReadPermission::new(\"permissions\")")] async fn get_role_permissions( &self, ctx: &Context<'_>, role_name: String, ) -> Result> { let casbin_service = ctx.data::()?; let permissions = casbin_service.get_role_permissions(&role_name).await?; Ok(permissions .into_iter() .map(|p| PermissionPair { resource: p.0, action: p.1, }) .collect()) } #[graphql(guard = "RequireReadPermission::new(\"permissions\")")] async fn can_read(&self, ctx: &Context<'_>, resource: String) -> Result { let user = get_auth_user(ctx).await?; let casbin_service = ctx.data::()?; let can_read = casbin_service .can_read(&user.id.to_string(), &resource) .await?; Ok(can_read) } #[graphql(guard = "RequireReadPermission::new(\"permissions\")")] async fn can_write(&self, ctx: &Context<'_>, resource: String) -> Result { let user = get_auth_user(ctx).await?; let casbin_service = ctx.data::()?; let can_write = casbin_service .can_write(&user.id.to_string(), &resource) .await?; Ok(can_write) } #[graphql(guard = "RequireReadPermission::new(\"permissions\")")] async fn can_delete(&self, ctx: &Context<'_>, resource: String) -> Result { let user = get_auth_user(ctx).await?; let casbin_service = ctx.data::()?; let can_delete = casbin_service .can_delete(&user.id.to_string(), &resource) .await?; Ok(can_delete) } // 站点与运营配置查询方法 async fn site_ops_config(&self, ctx: &Context<'_>) -> Result { let settings_service = ctx.data::()?; // 获取站点配置 let site_name = settings_service .get_setting_by_key("site.name") .await? .and_then(|s| s.get_string().ok()) .unwrap_or_else(|| "MMAP System".to_string()); let locale_default = settings_service .get_setting_by_key("site.locale_default") .await? .and_then(|s| s.get_string().ok()) .unwrap_or_else(|| "zh-CN".to_string()); let locales_supported = settings_service .get_setting_by_key("site.locales_supported") .await? .and_then(|s| s.get_json().ok()) .and_then(|v| v.as_array().cloned()) .map(|arr| { arr.iter() .filter_map(|v| v.as_str().map(|s| s.to_string())) .collect() }) .unwrap_or_else(|| vec!["zh-CN".to_string(), "en".to_string()]); // 获取品牌配置 let logo_url = settings_service .get_setting_by_key("site.brand.logo_url") .await? .and_then(|s| s.get_string().ok()) .unwrap_or_else(|| "/images/logo.png".to_string()); let primary_color = settings_service .get_setting_by_key("site.brand.primary_color") .await? .and_then(|s| s.get_string().ok()) .unwrap_or_else(|| "#3B82F6".to_string()); let dark_mode_default = settings_service .get_setting_by_key("site.brand.dark_mode_default") .await? .and_then(|s| s.get_bool().ok()) .unwrap_or(false); // 获取页脚链接 let footer_links = settings_service .get_setting_by_key("site.footer_links") .await? .and_then(|s| s.get_json().ok()) .and_then(|v| v.as_array().cloned()) .map(|arr| { arr.iter() .filter_map(|v| { if let (Some(name), Some(url), Some(visible)) = ( v.get("name").and_then(|n| n.as_str()), v.get("url").and_then(|u| u.as_str()), v.get("visible_to_guest").and_then(|vis| vis.as_bool()), ) { Some(FooterLinkType { name: name.to_string(), url: url.to_string(), visible_to_guest: visible, }) } else { None } }) .collect() }) .unwrap_or_else(|| { vec![ FooterLinkType { name: "关于我们".to_string(), url: "/about".to_string(), visible_to_guest: true, }, FooterLinkType { name: "联系我们".to_string(), url: "/contact".to_string(), visible_to_guest: true, }, ] }); // 获取横幅公告配置 let banner_enabled = settings_service .get_setting_by_key("notice.banner.enabled") .await? .and_then(|s| s.get_bool().ok()) .unwrap_or(false); let banner_text = settings_service .get_setting_by_key("notice.banner.text") .await? .and_then(|s| s.get_json().ok()) .and_then(|v| v.as_object().cloned()) .map(|obj| { obj.iter() .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string()))) .collect() }) .unwrap_or_else(|| { let mut map = std::collections::HashMap::new(); map.insert("zh-CN".to_string(), "欢迎使用MMAP系统".to_string()); map.insert("en".to_string(), "Welcome to MMAP System".to_string()); map }); // 获取维护窗口配置 let maintenance_config = settings_service.get_setting_by_key("maintenance.window").await? .and_then(|s| s.get_json().ok()) .unwrap_or_else(|| serde_json::json!({ "enabled": false, "start_time": null, "end_time": null, "message": {"zh-CN": "系统维护中,请稍后再试", "en": "System maintenance in progress"} })); let maintenance_enabled = maintenance_config .get("enabled") .and_then(|v| v.as_bool()) .unwrap_or(false); let maintenance_start_time = maintenance_config .get("start_time") .and_then(|v| v.as_str()) .and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok()) .map(|dt| dt.with_timezone(&chrono::Utc)); let maintenance_end_time = maintenance_config .get("end_time") .and_then(|v| v.as_str()) .and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok()) .map(|dt| dt.with_timezone(&chrono::Utc)); let maintenance_message = maintenance_config .get("message") .and_then(|v| v.as_object()) .map(|obj| { obj.iter() .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string()))) .collect() }) .unwrap_or_else(|| { let mut map = std::collections::HashMap::new(); map.insert("zh-CN".to_string(), "系统维护中,请稍后再试".to_string()); map.insert( "en".to_string(), "System maintenance in progress".to_string(), ); map }); // 获取弹窗公告 let modal_announcements = settings_service .get_setting_by_key("modal.announcements") .await? .and_then(|s| s.get_json().ok()) .and_then(|v| v.as_array().cloned()) .map(|arr| { arr.iter() .filter_map(|v| { if let ( Some(id), Some(title), Some(content), Some(start_time), Some(end_time), Some(audience), Some(priority), ) = ( v.get("id").and_then(|id| id.as_str()), v.get("title").and_then(|t| t.as_object()), v.get("content").and_then(|c| c.as_object()), v.get("start_time").and_then(|st| st.as_str()), v.get("end_time").and_then(|et| et.as_str()), v.get("audience").and_then(|a| a.as_array()), v.get("priority").and_then(|p| p.as_str()), ) { let title_map: std::collections::HashMap = title .iter() .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string()))) .collect(); let content_map: std::collections::HashMap = content .iter() .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string()))) .collect(); let audience_vec: Vec = audience .iter() .filter_map(|v| v.as_str().map(|s| s.to_string())) .collect(); if let (Ok(start_dt), Ok(end_dt)) = ( chrono::DateTime::parse_from_rfc3339(start_time), chrono::DateTime::parse_from_rfc3339(end_time), ) { Some(ModalAnnouncementType { id: id.to_string(), title: title_map, content: content_map, start_time: start_dt.with_timezone(&chrono::Utc), end_time: end_dt.with_timezone(&chrono::Utc), audience: audience_vec, priority: priority.to_string(), }) } else { None } } else { None } }) .collect() }) .unwrap_or_default(); // 获取文档链接 let docs_links = settings_service .get_setting_by_key("docs.links") .await? .and_then(|s| s.get_json().ok()) .and_then(|v| v.as_array().cloned()) .map(|arr| { arr.iter() .filter_map(|v| { if let (Some(name), Some(url), Some(description)) = ( v.get("name").and_then(|n| n.as_str()), v.get("url").and_then(|u| u.as_str()), v.get("description").and_then(|d| d.as_str()), ) { Some(DocLinkType { name: name.to_string(), url: url.to_string(), description: description.to_string(), }) } else { None } }) .collect() }) .unwrap_or_default(); // 获取支持渠道 let support_channels = settings_service .get_setting_by_key("support.channels") .await? .and_then(|s| s.get_json().ok()) .unwrap_or_else(|| { serde_json::json!({ "email": "support@mapp.com", "ticket_system": "/support/tickets", "chat_groups": [], "working_hours": {"zh-CN": "周一至周五 9:00-18:00", "en": "Mon-Fri 9:00-18:00"} }) }); let support_email = support_channels .get("email") .and_then(|v| v.as_str()) .unwrap_or("support@mapp.com") .to_string(); let ticket_system = support_channels .get("ticket_system") .and_then(|v| v.as_str()) .unwrap_or("/support/tickets") .to_string(); let chat_groups = support_channels .get("chat_groups") .and_then(|v| v.as_array()) .map(|arr| { arr.iter() .filter_map(|v| { if let (Some(name), Some(description)) = ( v.get("name").and_then(|n| n.as_str()), v.get("description").and_then(|d| d.as_str()), ) { let url = v.get("url").and_then(|u| u.as_str()).map(|s| s.to_string()); let qr_code = v .get("qr_code") .and_then(|q| q.as_str()) .map(|s| s.to_string()); Some(ChatGroupType { name: name.to_string(), url, qr_code, description: description.to_string(), }) } else { None } }) .collect() }) .unwrap_or_default(); let working_hours = support_channels .get("working_hours") .and_then(|v| v.as_object()) .map(|obj| { obj.iter() .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string()))) .collect() }) .unwrap_or_else(|| { let mut map = std::collections::HashMap::new(); map.insert("zh-CN".to_string(), "周一至周五 9:00-18:00".to_string()); map.insert("en".to_string(), "Mon-Fri 9:00-18:00".to_string()); map }); // 获取运营配置 let ops_features = settings_service .get_setting_by_key("ops.features.registration_enabled") .await? .and_then(|s| s.get_bool().ok()) .unwrap_or(true); let invite_code_required = settings_service .get_setting_by_key("ops.features.invite_code_required") .await? .and_then(|s| s.get_bool().ok()) .unwrap_or(true); let email_verification = settings_service .get_setting_by_key("ops.features.email_verification") .await? .and_then(|s| s.get_bool().ok()) .unwrap_or(false); let max_users = settings_service .get_setting_by_key("ops.limits.max_users") .await? .and_then(|s| s.get_number().ok()) .map(|n| n as i32) .unwrap_or(1000); let max_invite_codes = settings_service .get_setting_by_key("ops.limits.max_invite_codes_per_user") .await? .and_then(|s| s.get_number().ok()) .map(|n| n as i32) .unwrap_or(10); let session_timeout = settings_service .get_setting_by_key("ops.limits.session_timeout_hours") .await? .and_then(|s| s.get_number().ok()) .map(|n| n as i32) .unwrap_or(24); let welcome_email = settings_service .get_setting_by_key("ops.notifications.welcome_email") .await? .and_then(|s| s.get_bool().ok()) .unwrap_or(true); let system_announcements = settings_service .get_setting_by_key("ops.notifications.system_announcements") .await? .and_then(|s| s.get_bool().ok()) .unwrap_or(true); let maintenance_alerts = settings_service .get_setting_by_key("ops.notifications.maintenance_alerts") .await? .and_then(|s| s.get_bool().ok()) .unwrap_or(true); Ok(SiteOpsConfigType { site: SiteConfigType { info: SiteInfoType { name: site_name, locale_default, locales_supported, }, brand: BrandConfigType { logo_url, primary_color, dark_mode_default, }, footer_links, }, notice_maintenance: NoticeMaintenanceType { banner: BannerNoticeType { enabled: banner_enabled, text: banner_text, }, maintenance_window: MaintenanceWindowType { enabled: maintenance_enabled, start_time: maintenance_start_time, end_time: maintenance_end_time, message: maintenance_message, }, modal_announcements, }, docs_support: DocsSupportType { links: docs_links, channels: SupportChannelsType { email: support_email, ticket_system, chat_groups, working_hours, }, }, ops: OpsConfigType { features: FeatureSwitchesType { registration_enabled: ops_features, invite_code_required, email_verification, }, limits: LimitsConfigType { max_users, max_invite_codes_per_user: max_invite_codes, session_timeout_hours: session_timeout, }, notifications: NotificationConfigType { welcome_email, system_announcements, maintenance_alerts, }, }, }) } /// 获取站点配置 #[graphql(guard = "RequireReadPermission::new(\"settings\")")] async fn site_config(&self, ctx: &Context<'_>) -> Result { let full_config = self.site_ops_config(ctx).await?; Ok(full_config.site) } /// 获取公告维护配置 #[graphql(guard = "RequireReadPermission::new(\"settings\")")] async fn notice_maintenance_config(&self, ctx: &Context<'_>) -> Result { let full_config = self.site_ops_config(ctx).await?; Ok(full_config.notice_maintenance) } /// 获取文档支持配置 #[graphql(guard = "RequireReadPermission::new(\"settings\")")] async fn docs_support_config(&self, ctx: &Context<'_>) -> Result { let full_config = self.site_ops_config(ctx).await?; Ok(full_config.docs_support) } /// 获取运营配置 #[graphql(guard = "RequireReadPermission::new(\"settings\")")] async fn ops_config(&self, ctx: &Context<'_>) -> Result { let full_config = self.site_ops_config(ctx).await?; Ok(full_config.ops) } /// 验证配置有效性 #[graphql(guard = "RequireReadPermission::new(\"settings\")")] async fn validate_config(&self, ctx: &Context<'_>) -> Result { let settings_service = ctx.data::()?; let mut errors = Vec::new(); let mut warnings = Vec::new(); // 验证站点名称 if let Some(setting) = settings_service.get_setting_by_key("site.name").await? { if let Ok(name) = setting.get_string() { if name.trim().is_empty() { errors.push("站点名称不能为空".to_string()); } } } // 验证默认语言 if let Some(setting) = settings_service .get_setting_by_key("site.locale_default") .await? { if let Ok(locale) = setting.get_string() { if !["zh-CN", "en"].contains(&locale.as_str()) { errors.push("默认语言必须是 zh-CN 或 en".to_string()); } } } // 验证主题色格式 if let Some(setting) = settings_service .get_setting_by_key("site.brand.primary_color") .await? { if let Ok(color) = setting.get_string() { if !color.starts_with('#') || color.len() != 7 { warnings.push("主题色格式建议使用 #RRGGBB 格式".to_string()); } } } // 验证维护窗口时间 if let Some(setting) = settings_service .get_setting_by_key("maintenance.window") .await? { if let Ok(config) = setting.get_json() { if let (Some(enabled), Some(start), Some(end)) = ( config.get("enabled").and_then(|v| v.as_bool()), config.get("start_time").and_then(|v| v.as_str()), config.get("end_time").and_then(|v| v.as_str()), ) { if enabled { if let (Ok(start_dt), Ok(end_dt)) = ( chrono::DateTime::parse_from_rfc3339(start), chrono::DateTime::parse_from_rfc3339(end), ) { if start_dt >= end_dt { errors.push("维护开始时间必须早于结束时间".to_string()); } } } } } } Ok(ConfigValidationResultType { valid: errors.is_empty(), errors, warnings, }) } // ==================== Blog 相关查询 ==================== async fn blogs( &self, ctx: &Context<'_>, filter: Option, sort: Option, pagination: Option, ) -> Result> { let blog_service = ctx.data::()?; blog_service .get_blogs(filter, sort, pagination) .await .map_err(|e| GraphQLError::new(e.to_string())) } /// 根据ID获取博客文章 async fn blog(&self, ctx: &Context<'_>, id: Uuid) -> Result { let blog_service = ctx.data::()?; blog_service .get_blog_by_id(id) .await .map_err(|e| GraphQLError::new(e.to_string())) } /// 根据slug获取博客文章 async fn blog_by_slug(&self, ctx: &Context<'_>, slug: String) -> Result { let blog_service = ctx.data::()?; blog_service .get_blog_by_slug(&slug) .await .map_err(|e| GraphQLError::new(e.to_string())) } async fn blog_detail(&self, ctx: &Context<'_>, id: Uuid) -> Result { let blog_service = ctx.data::()?; blog_service .get_blog_detail(id) .await .map_err(|e| GraphQLError::new(e.to_string())) } async fn blog_stats(&self, ctx: &Context<'_>) -> Result { let blog_service = ctx.data::()?; blog_service .get_blog_stats() .await .map_err(|e| GraphQLError::new(e.to_string())) } async fn blog_categories( &self, ctx: &Context<'_>, filter: Option, ) -> Result> { let blog_service = ctx.data::()?; blog_service .get_categories(filter) .await .map_err(|e| GraphQLError::new(e.to_string())) } async fn blog_category(&self, ctx: &Context<'_>, id: Uuid) -> Result { let blog_service = ctx.data::()?; blog_service .get_category_by_id(id) .await .map_err(|e| GraphQLError::new(e.to_string())) } async fn blog_tags( &self, ctx: &Context<'_>, filter: Option, ) -> Result> { let blog_service = ctx.data::()?; blog_service .get_tags(filter) .await .map_err(|e| GraphQLError::new(e.to_string())) } async fn blog_tag(&self, ctx: &Context<'_>, id: Uuid) -> Result { let blog_service = ctx.data::()?; blog_service .get_tag_by_id(id) .await .map_err(|e| GraphQLError::new(e.to_string())) } }