mmap/src/services/page_block_service.rs
tsuki d29679c6f8
Some checks are pending
Docker Build and Push / build (push) Waiting to run
add casbin
2025-08-11 21:26:29 +08:00

1547 lines
49 KiB
Rust

use chrono::{DateTime, Utc};
use sea_query::{Expr, PostgresQueryBuilder, Query};
use sea_query_binder::SqlxBinder;
use sqlx::{PgPool, Row};
use thiserror::Error;
use tracing::error;
use uuid::Uuid;
use crate::models::page_block::*;
#[derive(Debug, Error)]
pub enum PageBlockError {
#[error("数据库错误: {0}")]
DatabaseError(#[from] sqlx::Error),
#[error("未找到: {0}")]
NotFound(String),
#[error("验证错误: {0}")]
ValidationError(String),
#[error("权限错误: {0}")]
PermissionError(String),
}
pub struct PageBlockService {
pool: PgPool,
}
impl PageBlockService {
pub fn new(pool: PgPool) -> Self {
Self { pool }
}
/// 创建新页面
pub async fn create_page(
&self,
input: CreatePageInput,
user_id: Uuid,
) -> Result<Page, PageBlockError> {
let result = sqlx::query_as!(
Page,
r#"
INSERT INTO pages (title, slug, description, is_active, created_by)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, title, slug, description, is_active,
created_at as "created_at: DateTime<Utc>",
updated_at as "updated_at: DateTime<Utc>",
created_by, updated_by
"#,
input.title,
input.slug,
input.description,
input.is_active.unwrap_or(true),
user_id
)
.fetch_one(&self.pool)
.await?;
Ok(Page {
id: result.id,
title: result.title,
slug: result.slug,
description: result.description,
is_active: result.is_active,
created_at: result.created_at,
updated_at: result.updated_at,
created_by: result.created_by,
updated_by: result.updated_by,
})
}
/// 根据ID获取页面
pub async fn get_page_by_id(&self, page_id: Uuid) -> Result<Page, PageBlockError> {
let result = sqlx::query_as!(
Page,
r#"
SELECT id, title, slug, description, is_active,
created_at as "created_at: DateTime<Utc>",
updated_at as "updated_at: DateTime<Utc>",
created_by, updated_by
FROM pages WHERE id = $1 AND is_active = true
"#,
page_id
)
.fetch_optional(&self.pool)
.await?;
match result {
Some(result) => Ok(Page {
id: result.id,
title: result.title,
slug: result.slug,
description: result.description,
is_active: result.is_active,
created_at: result.created_at,
updated_at: result.updated_at,
created_by: result.created_by,
updated_by: result.updated_by,
}),
None => Err(PageBlockError::NotFound(format!("页面 {} 未找到", page_id))),
}
}
/// 根据slug获取页面
pub async fn get_page_by_slug(&self, slug: &str) -> Result<Page, PageBlockError> {
let result = sqlx::query_as!(
Page,
r#"
SELECT id, title, slug, description, is_active,
created_at as "created_at: DateTime<Utc>",
updated_at as "updated_at: DateTime<Utc>",
created_by, updated_by
FROM pages WHERE slug = $1 AND is_active = true
"#,
slug
)
.fetch_optional(&self.pool)
.await?;
match result {
Some(result) => Ok(Page {
id: result.id,
title: result.title,
slug: result.slug,
description: result.description,
is_active: result.is_active,
created_at: result.created_at,
updated_at: result.updated_at,
created_by: result.created_by,
updated_by: result.updated_by,
}),
None => Err(PageBlockError::NotFound(format!(
"页面 slug {} 未找到",
slug
))),
}
}
/// 获取页面列表(使用 sea-query 构建动态查询)
pub async fn get_pages(
&self,
filter: Option<PageFilterInput>,
limit: Option<i64>,
offset: Option<i64>,
) -> Result<Vec<Page>, PageBlockError> {
// 使用 sea-query 构建查询
let mut query = Query::select();
use Pages::Pages;
query
.columns([
(Pages::Table, Pages::Id),
(Pages::Table, Pages::Title),
(Pages::Table, Pages::Slug),
(Pages::Table, Pages::Description),
(Pages::Table, Pages::IsActive),
(Pages::Table, Pages::CreatedAt),
(Pages::Table, Pages::UpdatedAt),
(Pages::Table, Pages::CreatedBy),
(Pages::Table, Pages::UpdatedBy),
])
.from(Pages::Table)
.and_where(Expr::col((Pages::Table, Pages::IsActive)).eq(true));
// 添加过滤条件
if let Some(filter) = filter {
if let Some(title) = filter.title {
query.and_where(
Expr::col((Pages::Table, Pages::Title)).like(format!("%{}%", title)),
);
}
if let Some(search) = filter.search {
query.and_where(
Expr::col((Pages::Table, Pages::Title))
.like(format!("%{}%", search))
.or(Expr::col((Pages::Table, Pages::Description))
.like(format!("%{}%", search))),
);
}
}
// 添加排序
query.order_by((Pages::Table, Pages::CreatedAt), sea_query::Order::Desc);
// 添加分页
if let Some(limit) = limit {
query.limit(limit as u64);
}
if let Some(offset) = offset {
query.offset(offset as u64);
}
// 构建并执行查询
let (sql, values) = query.build_sqlx(PostgresQueryBuilder);
let rows = sqlx::query_with(&sql, values).fetch_all(&self.pool).await?;
let pages = rows
.into_iter()
.map(|row| Page {
id: row.get("id"),
title: row.get("title"),
slug: row.get("slug"),
description: row.get("description"),
is_active: row.get("is_active"),
created_at: chrono::DateTime::from(
row.get::<chrono::DateTime<chrono::Utc>, _>("created_at"),
),
updated_at: chrono::DateTime::from(
row.get::<chrono::DateTime<chrono::Utc>, _>("updated_at"),
),
created_by: row.get("created_by"),
updated_by: row.get("updated_by"),
})
.collect();
Ok(pages)
}
/// 获取页面的所有块
pub async fn get_page_blocks(&self, page_id: Uuid) -> Result<Vec<Block>, PageBlockError> {
let mut blocks = Vec::new();
// 获取文本块
let text_blocks = sqlx::query!(
r#"
SELECT id, page_id, block_order, title, markdown, is_active, created_at as "created_at: DateTime<Utc>", updated_at as "updated_at: DateTime<Utc>"
FROM text_blocks WHERE page_id = $1 AND is_active = true ORDER BY block_order
"#,
page_id
)
.fetch_all(&self.pool)
.await?;
for row in text_blocks {
blocks.push(Block::TextBlock(TextBlock {
id: row.id,
page_id: row.page_id,
block_order: row.block_order,
title: row.title,
markdown: row.markdown,
is_active: row.is_active,
created_at: chrono::DateTime::from(row.created_at),
updated_at: chrono::DateTime::from(row.updated_at),
}));
}
// 获取图表块
let chart_blocks = sqlx::query!(
r#"
SELECT id, page_id, block_order, title, chart_type, config, is_active, created_at as "created_at: DateTime<Utc>", updated_at as "updated_at: DateTime<Utc>"
FROM chart_blocks WHERE page_id = $1 AND is_active = true ORDER BY block_order
"#,
page_id
)
.fetch_all(&self.pool)
.await?;
for row in chart_blocks {
// 获取数据点
let data_points = sqlx::query!(
r#"
SELECT id, chart_block_id, x, y, label, color
FROM data_points WHERE chart_block_id = $1 ORDER BY x
"#,
row.id
)
.fetch_all(&self.pool)
.await?;
let series = data_points
.into_iter()
.map(|dp| DataPoint {
id: dp.id,
chart_block_id: dp.chart_block_id,
x: dp.x,
y: dp.y,
label: dp.label,
color: dp.color,
})
.collect();
blocks.push(Block::ChartBlock(ChartBlock {
id: row.id,
page_id: row.page_id,
block_order: row.block_order,
title: row.title,
chart_type: row.chart_type,
series,
config: row.config,
is_active: row.is_active,
created_at: row.created_at,
updated_at: row.updated_at,
}));
}
// 获取设置块
let settings_blocks = sqlx::query!(
r#"
SELECT id, page_id, block_order, title, category, editable, display_mode, is_active, created_at as "created_at: DateTime<Utc>", updated_at as "updated_at: DateTime<Utc>"
FROM settings_blocks WHERE page_id = $1 AND is_active = true ORDER BY block_order
"#,
page_id
)
.fetch_all(&self.pool)
.await?;
for row in settings_blocks {
blocks.push(Block::SettingsBlock(SettingsBlock {
id: row.id,
page_id: row.page_id,
block_order: row.block_order,
title: row.title,
category: row.category,
editable: row.editable,
display_mode: row.display_mode,
is_active: row.is_active,
created_at: row.created_at,
updated_at: row.updated_at,
}));
}
// 获取表格块
let table_blocks = sqlx::query!(
r#"
SELECT id, page_id, block_order, title, data_source, data_config, is_active, created_at as "created_at: DateTime<Utc>", updated_at as "updated_at: DateTime<Utc>"
FROM table_blocks WHERE page_id = $1 AND is_active = true ORDER BY block_order
"#,
page_id
)
.fetch_all(&self.pool)
.await?;
for row in table_blocks {
// 获取表格列
let columns = sqlx::query!(
r#"
SELECT id, table_block_id, name, label, data_type, is_sortable, is_filterable, width, "order"
FROM table_columns WHERE table_block_id = $1 ORDER BY "order"
"#,
row.id
)
.fetch_all(&self.pool)
.await?;
let table_columns = columns
.into_iter()
.map(|col| TableColumn {
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();
blocks.push(Block::TableBlock(TableBlock {
id: row.id,
page_id: row.page_id,
block_order: row.block_order,
title: row.title,
columns: table_columns,
data_source: row.data_source,
data_config: row.data_config,
is_active: row.is_active,
created_at: chrono::DateTime::from(row.created_at),
updated_at: chrono::DateTime::from(row.updated_at),
}));
}
// 获取英雄块
let hero_blocks = sqlx::query!(
r#"
SELECT id, page_id, block_order, title, subtitle, background_image, background_color, text_color, cta_text, cta_link, is_active, created_at as "created_at: DateTime<Utc>", updated_at as "updated_at: DateTime<Utc>"
FROM hero_blocks WHERE page_id = $1 AND is_active = true ORDER BY block_order
"#,
page_id
)
.fetch_all(&self.pool)
.await?;
for row in hero_blocks {
blocks.push(Block::HeroBlock(HeroBlock {
id: row.id,
page_id: row.page_id,
block_order: row.block_order,
title: row.title,
subtitle: row.subtitle,
background_image: row.background_image,
background_color: row.background_color,
text_color: row.text_color,
cta_text: row.cta_text,
cta_link: row.cta_link,
is_active: row.is_active,
created_at: row.created_at,
updated_at: row.updated_at,
}));
}
Ok(blocks)
}
/// 创建文本块
pub async fn create_text_block(
&self,
input: CreateTextBlockInput,
) -> Result<TextBlock, PageBlockError> {
let result = sqlx::query_as!(
TextBlock,
r#"
INSERT INTO text_blocks (page_id, block_order, title, markdown, is_active)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, page_id, block_order, title, markdown, is_active,
created_at as "created_at: DateTime<Utc>",
updated_at as "updated_at: DateTime<Utc>"
"#,
input.page_id,
input.block_order,
input.title,
input.markdown,
input.is_active.unwrap_or(true)
)
.fetch_one(&self.pool)
.await?;
Ok(TextBlock {
id: result.id,
page_id: result.page_id,
block_order: result.block_order,
title: result.title,
markdown: result.markdown,
is_active: result.is_active,
created_at: chrono::DateTime::from(result.created_at),
updated_at: chrono::DateTime::from(result.updated_at),
})
}
/// 创建图表块
pub async fn create_chart_block(
&self,
input: CreateChartBlockInput,
) -> Result<ChartBlock, PageBlockError> {
// 开始事务
let mut tx = self.pool.begin().await?;
// 创建图表块
let chart_result = sqlx::query!(
r#"
INSERT INTO chart_blocks (page_id, block_order, title, chart_type, config, is_active)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id, page_id, block_order, title, chart_type, config, is_active,
created_at as "created_at: DateTime<Utc>",
updated_at as "updated_at: DateTime<Utc>"
"#,
input.page_id,
input.block_order,
input.title,
input.chart_type,
input.config,
input.is_active.unwrap_or(true)
)
.fetch_one(&mut *tx)
.await?;
// 创建数据点
for data_point in input.series {
sqlx::query!(
r#"
INSERT INTO data_points (chart_block_id, x, y, label, color)
VALUES ($1, $2, $3, $4, $5)
"#,
chart_result.id,
data_point.x,
data_point.y,
data_point.label,
data_point.color
)
.execute(&mut *tx)
.await?;
}
// 提交事务
tx.commit().await?;
// 获取完整的数据点
let data_points = sqlx::query!(
r#"
SELECT id, chart_block_id, x, y, label, color
FROM data_points WHERE chart_block_id = $1 ORDER BY x
"#,
chart_result.id
)
.fetch_all(&self.pool)
.await?;
let series = data_points
.into_iter()
.map(|dp| DataPoint {
id: dp.id,
chart_block_id: dp.chart_block_id,
x: dp.x,
y: dp.y,
label: dp.label,
color: dp.color,
})
.collect();
Ok(ChartBlock {
id: chart_result.id,
page_id: chart_result.page_id,
block_order: chart_result.block_order,
title: chart_result.title,
chart_type: chart_result.chart_type,
series,
config: chart_result.config,
is_active: chart_result.is_active,
created_at: chrono::DateTime::from(chart_result.created_at),
updated_at: chrono::DateTime::from(chart_result.updated_at),
})
}
/// 创建设置块
pub async fn create_settings_block(
&self,
input: CreateSettingsBlockInput,
) -> Result<SettingsBlock, PageBlockError> {
let result = sqlx::query_as!(
SettingsBlock,
r#"
INSERT INTO settings_blocks (page_id, block_order, title, category, editable, display_mode, is_active)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id, page_id, block_order, title, category, editable, display_mode, is_active,
created_at as "created_at: DateTime<Utc>",
updated_at as "updated_at: DateTime<Utc>"
"#,
input.page_id,
input.block_order,
input.title,
input.category,
input.editable,
input.display_mode,
input.is_active.unwrap_or(true)
)
.fetch_one(&self.pool)
.await?;
Ok(SettingsBlock {
id: result.id,
page_id: result.page_id,
block_order: result.block_order,
title: result.title,
category: result.category,
editable: result.editable,
display_mode: result.display_mode,
is_active: result.is_active,
created_at: chrono::DateTime::from(result.created_at),
updated_at: chrono::DateTime::from(result.updated_at),
})
}
/// 删除页面
pub async fn delete_page(&self, page_id: Uuid) -> Result<(), PageBlockError> {
// 开始事务
let mut tx = self.pool.begin().await?;
// 删除所有相关的块
sqlx::query!("DELETE FROM text_blocks WHERE page_id = $1", page_id)
.execute(&mut *tx)
.await?;
sqlx::query!("DELETE FROM chart_blocks WHERE page_id = $1", page_id)
.execute(&mut *tx)
.await?;
sqlx::query!("DELETE FROM data_points WHERE chart_block_id IN (SELECT id FROM chart_blocks WHERE page_id = $1)", page_id)
.execute(&mut *tx)
.await?;
sqlx::query!("DELETE FROM settings_blocks WHERE page_id = $1", page_id)
.execute(&mut *tx)
.await?;
sqlx::query!("DELETE FROM table_blocks WHERE page_id = $1", page_id)
.execute(&mut *tx)
.await?;
sqlx::query!("DELETE FROM table_columns WHERE table_block_id IN (SELECT id FROM table_blocks WHERE page_id = $1)", page_id)
.execute(&mut *tx)
.await?;
sqlx::query!("DELETE FROM hero_blocks WHERE page_id = $1", page_id)
.execute(&mut *tx)
.await?;
// 删除页面
sqlx::query!("DELETE FROM pages WHERE id = $1", page_id)
.execute(&mut *tx)
.await?;
// 提交事务
tx.commit().await?;
Ok(())
}
/// 更新页面
pub async fn update_page(
&self,
page_id: Uuid,
input: UpdatePageInput,
user_id: Uuid,
) -> Result<Page, PageBlockError> {
let result = sqlx::query_as!(
Page,
r#"
UPDATE pages
SET title = COALESCE($1, title),
slug = COALESCE($2, slug),
description = COALESCE($3, description),
is_active = COALESCE($4, is_active),
updated_at = NOW(),
updated_by = $5
WHERE id = $6
RETURNING id, title, slug, description, is_active,
created_at as "created_at: DateTime<Utc>",
updated_at as "updated_at: DateTime<Utc>",
created_by, updated_by
"#,
input.title,
input.slug,
input.description,
input.is_active,
user_id,
page_id
)
.fetch_optional(&self.pool)
.await?;
match result {
Some(result) => Ok(Page {
id: result.id,
title: result.title,
slug: result.slug,
description: result.description,
is_active: result.is_active,
created_at: result.created_at,
updated_at: result.updated_at,
created_by: result.created_by,
updated_by: result.updated_by,
}),
None => Err(PageBlockError::NotFound(format!("页面 {} 未找到", page_id))),
}
}
/// 更新文本块
pub async fn update_text_block(
&self,
block_id: Uuid,
input: UpdateTextBlockInput,
) -> Result<TextBlock, PageBlockError> {
let result = sqlx::query_as!(
TextBlock,
r#"
UPDATE text_blocks
SET title = COALESCE($1, title),
markdown = COALESCE($2, markdown),
block_order = COALESCE($3, block_order),
is_active = COALESCE($4, is_active),
updated_at = NOW()
WHERE id = $5
RETURNING id, page_id, block_order, title, markdown, is_active,
created_at as "created_at: DateTime<Utc>",
updated_at as "updated_at: DateTime<Utc>"
"#,
input.title,
input.markdown,
input.block_order,
input.is_active,
block_id
)
.fetch_optional(&self.pool)
.await?;
match result {
Some(result) => Ok(TextBlock {
id: result.id,
page_id: result.page_id,
block_order: result.block_order,
title: result.title,
markdown: result.markdown,
is_active: result.is_active,
created_at: result.created_at,
updated_at: result.updated_at,
}),
None => Err(PageBlockError::NotFound(format!(
"文本块 {} 未找到",
block_id
))),
}
}
/// 更新图表块
pub async fn update_chart_block(
&self,
block_id: Uuid,
input: UpdateChartBlockInput,
) -> Result<ChartBlock, PageBlockError> {
// 开始事务
let mut tx = self.pool.begin().await?;
// 更新图表块基本信息
let chart_result = sqlx::query!(
r#"
UPDATE chart_blocks
SET title = COALESCE($1, title),
chart_type = COALESCE($2, chart_type),
config = COALESCE($3, config),
block_order = COALESCE($4, block_order),
is_active = COALESCE($5, is_active),
updated_at = NOW()
WHERE id = $6
RETURNING id, page_id, block_order, title, chart_type, config, is_active,
created_at as "created_at: DateTime<Utc>",
updated_at as "updated_at: DateTime<Utc>"
"#,
input.title,
input.chart_type,
input.config,
input.block_order,
input.is_active,
block_id
)
.fetch_optional(&mut *tx)
.await?;
let chart_block = match chart_result {
Some(result) => result,
None => {
tx.rollback().await?;
return Err(PageBlockError::NotFound(format!(
"图表块 {} 未找到",
block_id
)));
}
};
// 如果提供了新的数据系列,则更新数据点
if let Some(series) = input.series {
// 删除旧的数据点
sqlx::query!(
"DELETE FROM data_points WHERE chart_block_id = $1",
block_id
)
.execute(&mut *tx)
.await?;
// 插入新的数据点
for data_point in series {
sqlx::query!(
r#"
INSERT INTO data_points (chart_block_id, x, y, label, color)
VALUES ($1, $2, $3, $4, $5)
"#,
block_id,
data_point.x,
data_point.y,
data_point.label,
data_point.color
)
.execute(&mut *tx)
.await?;
}
}
// 提交事务
tx.commit().await?;
// 获取完整的数据点
let data_points = sqlx::query!(
r#"
SELECT id, chart_block_id, x, y, label, color
FROM data_points WHERE chart_block_id = $1 ORDER BY x
"#,
block_id
)
.fetch_all(&self.pool)
.await?;
let series = data_points
.into_iter()
.map(|dp| DataPoint {
id: dp.id,
chart_block_id: dp.chart_block_id,
x: dp.x,
y: dp.y,
label: dp.label,
color: dp.color,
})
.collect();
Ok(ChartBlock {
id: chart_block.id,
page_id: chart_block.page_id,
block_order: chart_block.block_order,
title: chart_block.title,
chart_type: chart_block.chart_type,
series,
config: chart_block.config,
is_active: chart_block.is_active,
created_at: chart_block.created_at,
updated_at: chart_block.updated_at,
})
}
/// 更新设置块
pub async fn update_settings_block(
&self,
block_id: Uuid,
input: UpdateSettingsBlockInput,
) -> Result<SettingsBlock, PageBlockError> {
let result = sqlx::query_as!(
SettingsBlock,
r#"
UPDATE settings_blocks
SET title = COALESCE($1, title),
category = COALESCE($2, category),
editable = COALESCE($3, editable),
display_mode = COALESCE($4, display_mode),
block_order = COALESCE($5, block_order),
is_active = COALESCE($6, is_active),
updated_at = NOW()
WHERE id = $7
RETURNING id, page_id, block_order, title, category, editable, display_mode, is_active,
created_at as "created_at: DateTime<Utc>",
updated_at as "updated_at: DateTime<Utc>"
"#,
input.title,
input.category,
input.editable,
input.display_mode,
input.block_order,
input.is_active,
block_id
)
.fetch_optional(&self.pool)
.await?;
match result {
Some(result) => Ok(SettingsBlock {
id: result.id,
page_id: result.page_id,
block_order: result.block_order,
title: result.title,
category: result.category,
editable: result.editable,
display_mode: result.display_mode,
is_active: result.is_active,
created_at: result.created_at,
updated_at: result.updated_at,
}),
None => Err(PageBlockError::NotFound(format!(
"设置块 {} 未找到",
block_id
))),
}
}
/// 更新表格块
pub async fn update_table_block(
&self,
block_id: Uuid,
input: UpdateTableBlockInput,
) -> Result<TableBlock, PageBlockError> {
// 开始事务
let mut tx = self.pool.begin().await?;
// 创建临时结构体来接收更新后的基本信息
#[derive(sqlx::FromRow)]
struct TableBlockUpdate {
id: Uuid,
page_id: Uuid,
block_order: i32,
title: Option<String>,
data_source: String,
data_config: Option<serde_json::Value>,
is_active: bool,
created_at: DateTime<Utc>,
updated_at: DateTime<Utc>,
}
// 更新表格块基本信息
let table_result = sqlx::query_as!(
TableBlockUpdate,
r#"
UPDATE table_blocks
SET title = COALESCE($1, title),
data_source = COALESCE($2, data_source),
data_config = COALESCE($3, data_config),
block_order = COALESCE($4, block_order),
is_active = COALESCE($5, is_active),
updated_at = NOW()
WHERE id = $6
RETURNING id, page_id, block_order, title, data_source, data_config, is_active,
created_at as "created_at: DateTime<Utc>",
updated_at as "updated_at: DateTime<Utc>"
"#,
input.title,
input.data_source,
input.data_config,
input.block_order,
input.is_active,
block_id
)
.fetch_optional(&mut *tx)
.await?;
let table_block = match table_result {
Some(result) => result,
None => {
tx.rollback().await?;
return Err(PageBlockError::NotFound(format!(
"表格块 {} 未找到",
block_id
)));
}
};
// 如果提供了新的列配置,则更新列
if let Some(columns) = input.columns {
// 删除旧的列
sqlx::query!(
"DELETE FROM table_columns WHERE table_block_id = $1",
block_id
)
.execute(&mut *tx)
.await?;
// 插入新的列
for column in columns {
sqlx::query!(
r#"
INSERT INTO table_columns (table_block_id, name, label, data_type, is_sortable, is_filterable, width, "order")
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
"#,
block_id,
column.name,
column.label,
column.data_type,
column.is_sortable,
column.is_filterable,
column.width,
column.order
)
.execute(&mut *tx)
.await?;
}
}
// 提交事务
tx.commit().await?;
// 获取完整的列配置
let columns = sqlx::query!(
r#"
SELECT id, table_block_id, name, label, data_type, is_sortable, is_filterable, width, "order"
FROM table_columns WHERE table_block_id = $1 ORDER BY "order"
"#,
block_id
)
.fetch_all(&self.pool)
.await?;
let table_columns = columns
.into_iter()
.map(|col| TableColumn {
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();
Ok(TableBlock {
id: table_block.id,
page_id: table_block.page_id,
block_order: table_block.block_order,
title: table_block.title,
columns: table_columns,
data_source: table_block.data_source,
data_config: table_block.data_config,
is_active: table_block.is_active,
created_at: table_block.created_at,
updated_at: table_block.updated_at,
})
}
/// 更新英雄块
pub async fn update_hero_block(
&self,
block_id: Uuid,
input: UpdateHeroBlockInput,
) -> Result<HeroBlock, PageBlockError> {
let result = sqlx::query_as!(
HeroBlock,
r#"
UPDATE hero_blocks
SET title = COALESCE($1, title),
subtitle = COALESCE($2, subtitle),
background_image = COALESCE($3, background_image),
background_color = COALESCE($4, background_color),
text_color = COALESCE($5, text_color),
cta_text = COALESCE($6, cta_text),
cta_link = COALESCE($7, cta_link),
block_order = COALESCE($8, block_order),
is_active = COALESCE($9, is_active),
updated_at = NOW()
WHERE id = $10
RETURNING id, page_id, block_order, title, subtitle, background_image, background_color, text_color, cta_text, cta_link, is_active,
created_at as "created_at: DateTime<Utc>",
updated_at as "updated_at: DateTime<Utc>"
"#,
input.title,
input.subtitle,
input.background_image,
input.background_color,
input.text_color,
input.cta_text,
input.cta_link,
input.block_order,
input.is_active,
block_id
)
.fetch_optional(&self.pool)
.await?;
match result {
Some(result) => Ok(HeroBlock {
id: result.id,
page_id: result.page_id,
block_order: result.block_order,
title: result.title,
subtitle: result.subtitle,
background_image: result.background_image,
background_color: result.background_color,
text_color: result.text_color,
cta_text: result.cta_text,
cta_link: result.cta_link,
is_active: result.is_active,
created_at: result.created_at,
updated_at: result.updated_at,
}),
None => Err(PageBlockError::NotFound(format!(
"英雄块 {} 未找到",
block_id
))),
}
}
/// 删除文本块
pub async fn delete_text_block(&self, block_id: Uuid) -> Result<(), PageBlockError> {
let result = sqlx::query!(
"DELETE FROM text_blocks WHERE id = $1 RETURNING id",
block_id
)
.fetch_optional(&self.pool)
.await?;
if result.is_none() {
return Err(PageBlockError::NotFound(format!(
"文本块 {} 未找到",
block_id
)));
}
Ok(())
}
/// 删除图表块
pub async fn delete_chart_block(&self, block_id: Uuid) -> Result<(), PageBlockError> {
// 开始事务
let mut tx = self.pool.begin().await?;
// 删除数据点
sqlx::query!(
"DELETE FROM data_points WHERE chart_block_id = $1",
block_id
)
.execute(&mut *tx)
.await?;
// 删除图表块
let result = sqlx::query!(
"DELETE FROM chart_blocks WHERE id = $1 RETURNING id",
block_id
)
.fetch_optional(&mut *tx)
.await?;
if result.is_none() {
tx.rollback().await?;
return Err(PageBlockError::NotFound(format!(
"图表块 {} 未找到",
block_id
)));
}
// 提交事务
tx.commit().await?;
Ok(())
}
/// 删除设置块
pub async fn delete_settings_block(&self, block_id: Uuid) -> Result<(), PageBlockError> {
let result = sqlx::query!(
"DELETE FROM settings_blocks WHERE id = $1 RETURNING id",
block_id
)
.fetch_optional(&self.pool)
.await?;
if result.is_none() {
return Err(PageBlockError::NotFound(format!(
"设置块 {} 未找到",
block_id
)));
}
Ok(())
}
/// 删除表格块
pub async fn delete_table_block(&self, block_id: Uuid) -> Result<(), PageBlockError> {
// 开始事务
let mut tx = self.pool.begin().await?;
// 删除表格列
sqlx::query!(
"DELETE FROM table_columns WHERE table_block_id = $1",
block_id
)
.execute(&mut *tx)
.await?;
// 删除表格块
let result = sqlx::query!(
"DELETE FROM table_blocks WHERE id = $1 RETURNING id",
block_id
)
.fetch_optional(&mut *tx)
.await?;
if result.is_none() {
tx.rollback().await?;
return Err(PageBlockError::NotFound(format!(
"表格块 {} 未找到",
block_id
)));
}
// 提交事务
tx.commit().await?;
Ok(())
}
/// 删除英雄块
pub async fn delete_hero_block(&self, block_id: Uuid) -> Result<(), PageBlockError> {
let result = sqlx::query!(
"DELETE FROM hero_blocks WHERE id = $1 RETURNING id",
block_id
)
.fetch_optional(&self.pool)
.await?;
if result.is_none() {
return Err(PageBlockError::NotFound(format!(
"英雄块 {} 未找到",
block_id
)));
}
Ok(())
}
/// 重新排序页面块
pub async fn reorder_page_blocks(
&self,
page_id: Uuid,
block_orders: Vec<(Uuid, i32)>,
) -> Result<(), PageBlockError> {
// 开始事务
let mut tx = self.pool.begin().await?;
for (block_id, new_order) in block_orders {
// 尝试更新各种类型的块
let mut updated = false;
// 更新文本块
let result = sqlx::query!(
"UPDATE text_blocks SET block_order = $1, updated_at = NOW() WHERE id = $2 AND page_id = $3 RETURNING id",
new_order, block_id, page_id
)
.fetch_optional(&mut *tx)
.await?;
if result.is_some() {
updated = true;
continue;
}
// 更新图表块
let result = sqlx::query!(
"UPDATE chart_blocks SET block_order = $1, updated_at = NOW() WHERE id = $2 AND page_id = $3 RETURNING id",
new_order, block_id, page_id
)
.fetch_optional(&mut *tx)
.await?;
if result.is_some() {
updated = true;
continue;
}
// 更新设置块
let result = sqlx::query!(
"UPDATE settings_blocks SET block_order = $1, updated_at = NOW() WHERE id = $2 AND page_id = $3 RETURNING id",
new_order, block_id, page_id
)
.fetch_optional(&mut *tx)
.await?;
if result.is_some() {
updated = true;
continue;
}
// 更新表格块
let result = sqlx::query!(
"UPDATE table_blocks SET block_order = $1, updated_at = NOW() WHERE id = $2 AND page_id = $3 RETURNING id",
new_order, block_id, page_id
)
.fetch_optional(&mut *tx)
.await?;
if result.is_some() {
updated = true;
continue;
}
// 更新英雄块
let result = sqlx::query!(
"UPDATE hero_blocks SET block_order = $1, updated_at = NOW() WHERE id = $2 AND page_id = $3 RETURNING id",
new_order, block_id, page_id
)
.fetch_optional(&mut *tx)
.await?;
if result.is_some() {
updated = true;
continue;
}
if !updated {
tx.rollback().await?;
return Err(PageBlockError::NotFound(format!(
"{} 未找到或不属于页面 {}",
block_id, page_id
)));
}
}
// 提交事务
tx.commit().await?;
Ok(())
}
/// 获取页面的统计信息
pub async fn get_page_stats(&self, page_id: Uuid) -> Result<PageStats, PageBlockError> {
let text_count = sqlx::query!(
"SELECT COUNT(*) as count FROM text_blocks WHERE page_id = $1 AND is_active = true",
page_id
)
.fetch_one(&self.pool)
.await?
.count;
let chart_count = sqlx::query!(
"SELECT COUNT(*) as count FROM chart_blocks WHERE page_id = $1 AND is_active = true",
page_id
)
.fetch_one(&self.pool)
.await?
.count;
let settings_count = sqlx::query!(
"SELECT COUNT(*) as count FROM settings_blocks WHERE page_id = $1 AND is_active = true",
page_id
)
.fetch_one(&self.pool)
.await?
.count;
let table_count = sqlx::query!(
"SELECT COUNT(*) as count FROM table_blocks WHERE page_id = $1 AND is_active = true",
page_id
)
.fetch_one(&self.pool)
.await?
.count;
let hero_count = sqlx::query!(
"SELECT COUNT(*) as count FROM hero_blocks WHERE page_id = $1 AND is_active = true",
page_id
)
.fetch_one(&self.pool)
.await?
.count;
Ok(PageStats {
text_blocks: text_count.unwrap_or(0) as i32,
chart_blocks: chart_count.unwrap_or(0) as i32,
settings_blocks: settings_count.unwrap_or(0) as i32,
table_blocks: table_count.unwrap_or(0) as i32,
hero_blocks: hero_count.unwrap_or(0) as i32,
total_blocks: (text_count.unwrap_or(0)
+ chart_count.unwrap_or(0)
+ settings_count.unwrap_or(0)
+ table_count.unwrap_or(0)
+ hero_count.unwrap_or(0)) as i32,
})
}
/// 检查页面slug是否唯一
pub async fn is_slug_unique(
&self,
slug: &str,
exclude_id: Option<Uuid>,
) -> Result<bool, PageBlockError> {
let count = match exclude_id {
Some(id) => {
sqlx::query!(
"SELECT COUNT(*) as count FROM pages WHERE slug = $1 AND id != $2",
slug,
id
)
.fetch_one(&self.pool)
.await?
.count
}
None => {
sqlx::query!("SELECT COUNT(*) as count FROM pages WHERE slug = $1", slug)
.fetch_one(&self.pool)
.await?
.count
}
};
Ok(count.unwrap_or(0) == 0)
}
/// 批量获取页面
pub async fn get_pages_by_ids(&self, page_ids: &[Uuid]) -> Result<Vec<Page>, PageBlockError> {
if page_ids.is_empty() {
return Ok(Vec::new());
}
let mut pages = Vec::new();
for page_id in page_ids {
match self.get_page_by_id(*page_id).await {
Ok(page) => pages.push(page),
Err(PageBlockError::NotFound(_)) => continue, // 跳过不存在的页面
Err(e) => return Err(e),
}
}
Ok(pages)
}
/// 搜索页面
pub async fn search_pages(
&self,
query: &str,
limit: Option<i64>,
) -> Result<Vec<Page>, PageBlockError> {
let limit = limit.unwrap_or(10);
let rows = sqlx::query_as!(
Page,
r#"
SELECT id, title, slug, description, is_active,
created_at as "created_at: DateTime<Utc>",
updated_at as "updated_at: DateTime<Utc>",
created_by, updated_by
FROM pages
WHERE is_active = true
AND (title ILIKE $1 OR description ILIKE $1 OR slug ILIKE $1)
ORDER BY
CASE
WHEN title ILIKE $1 THEN 1
WHEN slug ILIKE $1 THEN 2
ELSE 3
END,
updated_at DESC
LIMIT $2
"#,
format!("%{}%", query),
limit
)
.fetch_all(&self.pool)
.await?;
let pages = rows
.into_iter()
.map(|result| Page {
id: result.id,
title: result.title,
slug: result.slug,
description: result.description,
is_active: result.is_active,
created_at: result.created_at,
updated_at: result.updated_at,
created_by: result.created_by,
updated_by: result.updated_by,
})
.collect();
Ok(pages)
}
/// 根据配置分类获取页面
pub async fn get_page_by_category(
&self,
category: &str,
) -> Result<Option<Page>, PageBlockError> {
let result = sqlx::query_as!(
Page,
r#"
SELECT id, title, slug, description, is_active,
created_at as "created_at: DateTime<Utc>",
updated_at as "updated_at: DateTime<Utc>",
created_by, updated_by
FROM pages WHERE slug = $1 AND is_active = true
"#,
format!("{}-settings", category)
)
.fetch_optional(&self.pool)
.await?;
match result {
Some(result) => Ok(Some(Page {
id: result.id,
title: result.title,
slug: result.slug,
description: result.description,
is_active: result.is_active,
created_at: result.created_at,
updated_at: result.updated_at,
created_by: result.created_by,
updated_by: result.updated_by,
})),
None => Ok(None),
}
}
/// 根据配置分类获取页面和统计信息
pub async fn get_category_page_with_stats(
&self,
category: &str,
) -> Result<(Option<Page>, i32, i32, i32), PageBlockError> {
// 获取页面信息
let page = self.get_page_by_category(category).await?;
// 获取该分类下的配置项统计
let stats_result = sqlx::query!(
r#"
SELECT
COUNT(*) as total_count,
COUNT(CASE WHEN is_system = true THEN 1 END) as system_count,
COUNT(CASE WHEN is_editable = true THEN 1 END) as editable_count
FROM settings
WHERE category = $1
"#,
category
)
.fetch_one(&self.pool)
.await?;
let total_count = stats_result.total_count.unwrap_or(0) as i32;
let system_count = stats_result.system_count.unwrap_or(0) as i32;
let editable_count = stats_result.editable_count.unwrap_or(0) as i32;
Ok((page, total_count, system_count, editable_count))
}
/// 获取所有配置分类页面
pub async fn get_all_category_pages(&self) -> Result<Vec<Page>, PageBlockError> {
let result = sqlx::query_as!(
Page,
r#"
SELECT id, title, slug, description, is_active,
created_at as "created_at: DateTime<Utc>",
updated_at as "updated_at: DateTime<Utc>",
created_by, updated_by
FROM pages WHERE slug LIKE '%-settings' AND is_active = true
ORDER BY title
"#
)
.fetch_all(&self.pool)
.await?;
let pages = result
.into_iter()
.map(|result| Page {
id: result.id,
title: result.title,
slug: result.slug,
description: result.description,
is_active: result.is_active,
created_at: result.created_at,
updated_at: result.updated_at,
created_by: result.created_by,
updated_by: result.updated_by,
})
.collect();
Ok(pages)
}
}
// 定义表结构常量
mod Pages {
use sea_query::Iden;
#[derive(Iden)]
pub enum Pages {
#[iden = "pages"]
Table,
Id,
Title,
Slug,
Description,
IsActive,
CreatedAt,
UpdatedAt,
CreatedBy,
UpdatedBy,
}
}