use axum::extract::Path; use axum::http::StatusCode; use axum::response::Html; use axum::routing::get; use axum::{Extension, Router}; use chrono::{DateTime, Utc}; use serde_json::Value; use sqlx::PgPool; use sqlx::prelude::FromRow; type HtmlError = (StatusCode, Html); #[derive(Clone)] struct UpdateRouteState { db: PgPool, } #[derive(Debug, Clone, FromRow)] struct UpgradeLog { version: String, released_at: DateTime, title: String, description: String, changes: Value, } pub fn routes(db: PgPool) -> Router where S: Clone + Send + Sync + 'static, { Router::new() .route("/updates", get(show_updates)) .route("/updates/{version}", get(show_update)) .layer(Extension(UpdateRouteState { db })) } async fn show_updates( Extension(state): Extension, ) -> Result, HtmlError> { let logs = list_upgrade_logs(&state.db) .await .map_err(html_internal_error)?; Ok(Html(render_updates_page(&logs))) } async fn show_update( Extension(state): Extension, Path(version): Path, ) -> Result, HtmlError> { let log = find_upgrade_log(&state.db, &version) .await .map_err(html_internal_error)? .ok_or_else(html_not_found)?; Ok(Html(render_update_page(&log))) } async fn list_upgrade_logs(pool: &PgPool) -> anyhow::Result> { let rows = sqlx::query_as::<_, UpgradeLog>( r#" SELECT version, released_at, title, description, changes FROM ias_upgrade_logs ORDER BY released_at DESC, version DESC "#, ) .fetch_all(pool) .await?; Ok(rows) } async fn find_upgrade_log(pool: &PgPool, version: &str) -> anyhow::Result> { let row = sqlx::query_as::<_, UpgradeLog>( r#" SELECT version, released_at, title, description, changes FROM ias_upgrade_logs WHERE version = $1 "#, ) .bind(version.trim()) .fetch_optional(pool) .await?; Ok(row) } fn render_updates_page(logs: &[UpgradeLog]) -> String { let items = logs .iter() .map(|log| { format!( r#"
  • v{version}{date}{title}

    {description}

  • "#, version = escape_html(&log.version), date = escape_html(&format_date(log.released_at)), title = escape_html(&log.title), description = escape_html(&log.description), ) }) .collect::>() .join(""); render_shell( "更新日志", r#"

    服务版本更新记录。

    "#, &format!(r#"
      {items}
    "#), ) } fn render_update_page(log: &UpgradeLog) -> String { let changes = render_changes(&log.changes); let body = format!( r#"

    {description}

    版本:v{version} 发布时间:{date}

    变更内容

    {changes}

    查看全部更新

    "#, description = escape_html(&log.description), version = escape_html(&log.version), date = escape_html(&format_date(log.released_at)), changes = changes, ); render_shell(&format!("v{} {}", log.version, log.title), "", &body) } fn render_changes(value: &Value) -> String { let Some(items) = value.as_array() else { return format!("
    {}
    ", escape_html(&value.to_string())); }; let items = items .iter() .filter_map(Value::as_str) .map(|item| format!("
  • {}
  • ", escape_html(item))) .collect::>() .join(""); format!("
      {items}
    ") } fn render_shell(title: &str, intro: &str, body: &str) -> String { format!( r#" {title}

    {title}

    {intro} {body}
    "#, title = escape_html(title), intro = intro, body = body, ) } fn format_date(value: DateTime) -> String { value.format("%Y-%m-%d %H:%M:%S UTC").to_string() } fn html_not_found() -> HtmlError { ( StatusCode::NOT_FOUND, Html(render_shell( "更新日志不存在", r#"

    没有找到对应版本的更新日志。

    "#, r#"

    查看全部更新

    "#, )), ) } fn html_internal_error(error: anyhow::Error) -> HtmlError { eprintln!("读取更新日志失败: {error}"); ( StatusCode::INTERNAL_SERVER_ERROR, Html(render_shell( "更新日志暂时不可用", r#"

    读取更新日志失败。

    "#, "", )), ) } fn escape_html(value: &str) -> String { let mut escaped = String::with_capacity(value.len()); for ch in value.chars() { match ch { '&' => escaped.push_str("&"), '<' => escaped.push_str("<"), '>' => escaped.push_str(">"), '"' => escaped.push_str("""), '\'' => escaped.push_str("'"), _ => escaped.push(ch), } } escaped }