yggdrasil/api/posts/
create.rs1#![allow(clippy::too_many_arguments)]
9
10use dioxus::prelude::*;
11
12#[cfg(feature = "server")]
13use super::helpers::{
14 clean_tags, get_current_admin_user, render_post_fields, sync_asset_refs, sync_tags,
15};
16use super::types::CreatePostResponse;
17#[cfg(feature = "server")]
18use crate::api::error::AppError;
19#[cfg(feature = "server")]
20use crate::db::pool::get_conn;
21#[cfg(feature = "server")]
22use crate::models::post::PostStatus;
23
24#[server(CreatePost, "/api")]
29pub async fn create_post(
30 title: String,
31 slug: Option<String>,
32 summary: Option<String>,
33 content_md: String,
34 status: String,
35 tags: Vec<String>,
36 cover_image: Option<String>,
37) -> Result<CreatePostResponse, ServerFnError> {
38 let user = get_current_admin_user().await?;
39
40 if title.trim().is_empty() {
42 return Ok(CreatePostResponse::err("标题不能为空".to_string()));
43 }
44
45 if content_md.trim().is_empty() {
47 return Ok(CreatePostResponse::err("内容不能为空".to_string()));
48 }
49
50 let base_slug = match slug {
52 Some(ref s) if !s.trim().is_empty() => {
53 let s = s.trim();
54 if !crate::api::slug::is_valid_slug(s) {
55 return Ok(CreatePostResponse::err(
56 "slug 格式无效,只能包含字母、数字、连字符和下划线".to_string(),
57 ));
58 }
59 s.to_string()
60 }
61 _ => crate::api::slug::slugify(&title),
62 };
63
64 #[cfg(feature = "server")]
65 {
66 let mut client = get_conn().await.map_err(AppError::db_conn)?;
67
68 let fields = render_post_fields(&content_md, &status, cover_image.as_deref()).await?;
70 let summary = summary
72 .filter(|s| !s.trim().is_empty())
73 .unwrap_or(fields.auto_summary);
74
75 let published_at = if fields.status == PostStatus::Published {
77 Some(chrono::Utc::now())
78 } else {
79 None
80 };
81
82 let tx = client.transaction().await.map_err(AppError::tx)?;
83
84 let final_slug = crate::api::slug::ensure_unique_slug(&tx, &base_slug, None).await?;
86
87 let row = tx
89 .query_one(
90 "INSERT INTO posts (author_id, title, slug, summary, content_md, content_html, toc_html, status, published_at, cover_image, word_count, reading_time)
91 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
92 RETURNING id",
93 &[
94 &user.id,
95 &title.trim(),
96 &final_slug,
97 &summary,
98 &content_md,
99 &fields.content_html,
100 &fields.toc_html,
101 &fields.status.as_str(),
102 &published_at,
103 &fields.cover_image,
104 &fields.word_count,
105 &fields.reading_time,
106 ],
107 )
108 .await
109 .map_err(AppError::tx)?;
110
111 let post_id: i32 = row.get(0);
112
113 let tags_cleaned = clean_tags(&tags);
115 sync_tags(&tx, post_id, &tags_cleaned).await?;
116
117 sync_asset_refs(
119 &tx,
120 post_id,
121 &fields.content_html,
122 fields.cover_image.as_deref(),
123 )
124 .await?;
125
126 tx.commit().await.map_err(AppError::tx)?;
127
128 crate::cache::invalidate_post_metadata();
130 crate::cache::invalidate_post_by_slug(&final_slug).await;
132 crate::cache::invalidate_tag_posts_for(&tags_cleaned).await;
134
135 crate::ssr_cache::invalidate_ssr_all_public();
137 crate::ssr_cache::bump_global_generation();
138
139 Ok(CreatePostResponse::ok(
140 "创建成功".to_string(),
141 post_id,
142 final_slug,
143 ))
144 }
145
146 #[cfg(not(feature = "server"))]
147 {
148 Ok(CreatePostResponse::err("server only".to_string()))
149 }
150}