mmap/src/graphql/query.rs
tsuki fb3706ff38
Some checks are pending
Docker Build and Push / build (push) Waiting to run
rate limit
2025-08-13 22:00:56 +08:00

1337 lines
49 KiB
Rust

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<User> {
let auth_user = get_auth_user(ctx).await?;
let user_service = ctx.data::<UserService>()?;
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<u64>,
limit: Option<u64>,
sort_by: Option<String>,
sort_order: Option<String>,
filter: Option<String>,
) -> Result<Vec<UserInfoRow>> {
let user_service = ctx.data::<UserService>()?;
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<Vec<InviteCode>> {
let auth_user = get_auth_user(ctx).await?;
let invite_code_service = ctx.data::<InviteCodeService>()?;
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<bool> {
let invite_code_service = ctx.data::<InviteCodeService>()?;
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<u64>,
limit: Option<u64>,
sort_by: Option<String>,
sort_order: Option<String>,
filter: Option<String>,
) -> Result<UserInfoRespnose> {
let user_service = ctx.data::<UserService>()?;
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<SettingFilterInput>,
) -> Result<Vec<SettingType>> {
let settings_service = ctx.data::<SettingsService>()?;
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<Option<SettingType>> {
let settings_service = ctx.data::<SettingsService>()?;
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<Option<SettingType>> {
let settings_service = ctx.data::<SettingsService>()?;
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<Vec<CategoryPageType>> {
let page_block_service = ctx.data::<PageBlockService>()?;
let settings_service = ctx.data::<SettingsService>()?;
// 获取所有配置分类
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<SettingsStatsType> {
let settings_service = ctx.data::<SettingsService>()?;
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<Vec<SettingHistoryType>> {
let settings_service = ctx.data::<SettingsService>()?;
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<PageFilterInputType>,
limit: Option<i64>,
offset: Option<i64>,
) -> Result<Vec<PageType>> {
let page_block_service = ctx.data::<PageBlockService>()?;
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<Option<PageType>> {
let page_block_service = ctx.data::<PageBlockService>()?;
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<Option<PageType>> {
let page_block_service = ctx.data::<PageBlockService>()?;
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<Vec<BlockType>> {
let page_block_service = ctx.data::<PageBlockService>()?;
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<CategoryPageType> {
let page_block_service = ctx.data::<PageBlockService>()?;
let settings_service = ctx.data::<SettingsService>()?;
// 获取页面和统计信息
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<Vec<PageType>> {
let page_block_service = ctx.data::<PageBlockService>()?;
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<Vec<SettingCenterType>> {
let settings_service = ctx.data::<SettingsService>()?;
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<PermissionCheckResult> {
let user = get_auth_user(ctx).await?;
let casbin_service = ctx.data::<CasbinService>()?;
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<Vec<String>> {
let user = get_auth_user(ctx).await?;
let casbin_service = ctx.data::<CasbinService>()?;
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<Vec<PolicyType>> {
let casbin_service = ctx.data::<CasbinService>()?;
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<Vec<PermissionPair>> {
let casbin_service = ctx.data::<CasbinService>()?;
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<bool> {
let user = get_auth_user(ctx).await?;
let casbin_service = ctx.data::<CasbinService>()?;
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<bool> {
let user = get_auth_user(ctx).await?;
let casbin_service = ctx.data::<CasbinService>()?;
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<bool> {
let user = get_auth_user(ctx).await?;
let casbin_service = ctx.data::<CasbinService>()?;
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<SiteOpsConfigType> {
let settings_service = ctx.data::<SettingsService>()?;
// 获取站点配置
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<String, String> = title
.iter()
.filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
.collect();
let content_map: std::collections::HashMap<String, String> = content
.iter()
.filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
.collect();
let audience_vec: Vec<String> = 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<SiteConfigType> {
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<NoticeMaintenanceType> {
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<DocsSupportType> {
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<OpsConfigType> {
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<ConfigValidationResultType> {
let settings_service = ctx.data::<SettingsService>()?;
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<BlogFilterInput>,
sort: Option<BlogSortInput>,
pagination: Option<PaginationInput>,
) -> Result<PaginatedResult<Blog>> {
let blog_service = ctx.data::<BlogService>()?;
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<Blog> {
let blog_service = ctx.data::<BlogService>()?;
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<Blog> {
let blog_service = ctx.data::<BlogService>()?;
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<BlogDetail> {
let blog_service = ctx.data::<BlogService>()?;
blog_service
.get_blog_detail(id)
.await
.map_err(|e| GraphQLError::new(e.to_string()))
}
async fn blog_stats(&self, ctx: &Context<'_>) -> Result<BlogStats> {
let blog_service = ctx.data::<BlogService>()?;
blog_service
.get_blog_stats()
.await
.map_err(|e| GraphQLError::new(e.to_string()))
}
async fn blog_categories(
&self,
ctx: &Context<'_>,
filter: Option<BlogCategoryFilterInput>,
) -> Result<Vec<BlogCategory>> {
let blog_service = ctx.data::<BlogService>()?;
blog_service
.get_categories(filter)
.await
.map_err(|e| GraphQLError::new(e.to_string()))
}
async fn blog_category(&self, ctx: &Context<'_>, id: Uuid) -> Result<BlogCategory> {
let blog_service = ctx.data::<BlogService>()?;
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<BlogTagFilterInput>,
) -> Result<Vec<BlogTag>> {
let blog_service = ctx.data::<BlogService>()?;
blog_service
.get_tags(filter)
.await
.map_err(|e| GraphQLError::new(e.to_string()))
}
async fn blog_tag(&self, ctx: &Context<'_>, id: Uuid) -> Result<BlogTag> {
let blog_service = ctx.data::<BlogService>()?;
blog_service
.get_tag_by_id(id)
.await
.map_err(|e| GraphQLError::new(e.to_string()))
}
}