阅后即焚API

本章我们使用 AXUM 开发阅后即焚的 API。

CREATE TABLE IF NOT EXISTS "messages" (
    "id" CHAR(20) PRIMARY KEY,
    "content" VARCHAR NOT NULL DEFAULT '',
    "password" VARCHAR(72) NOT NULL DEFAULT '',
    "dateline" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
字段说明
id主键
content信息内容
password密码
dateline创建时间

模型

// src/model.rs

#[derive(Serialize, Deserialize, FromRow)]
pub struct Message {
    pub id: String,
    pub content: String,
    #[serde(skip_serializing)]
    pub password: String,
    pub dateline: chrono::DateTime<chrono::Local>,
}
// src/model.rs

impl Message {
    pub fn build(content: String, password: String) -> Result<Self> {
        let password = if password.trim().is_empty() {
            "".to_string()
        } else {
            util::hash_pwd(password.trim())?
        };

        Ok(Self {
            id: util::new_id(),
            content,
            password,
            dateline: chrono::Local::now(),
        })
    }

    pub fn if_has_password(&self) -> (bool, &str) {
        if self.password.is_empty() {
            (false, "")
        } else {
            (true, &self.password)
        }
    }
}

优化建议: if_has_password() 的返回值可以使用 Option<&str> 这一个值。请读者以作业的形式对该方法进行改写

// src/db/message.rs

pub async fn create(e: impl PgExecutor<'_>, message: &model::Message) -> sqlx::Result<&str> {
    sqlx::query("INSERT INTO messages (id, content, password, dateline) VALUES ($1, $2, $3, $4)")
        .bind(&message.id)
        .bind(&message.content)
        .bind(&message.password)
        .bind(&message.dateline)
        .execute(e)
        .await?;

    Ok(&message.id)
}

pub async fn get(e: impl PgExecutor<'_>, id: &str) -> sqlx::Result<Option<model::Message>> {
    sqlx::query_as("SELECT id, content, password, dateline FROM messages WHERE id = $1")
        .bind(id)
        .fetch_optional(e)
        .await
}

pub async fn del(e: impl PgExecutor<'_>, id: &str) -> sqlx::Result<u64> {
    let aff = sqlx::query("DELETE FROM messages WHERE id = $1")
        .bind(id)
        .execute(e)
        .await?
        .rows_affected();
    Ok(aff)
}

  • create():创建一条新消息
  • get():根据ID获取对应消息
  • del():根据ID删除对应消息

Handler

创建新消息

// src/frontend.rs

pub async fn create_message(
    State(state): State<ArcAppState>,
    Json(message): Json<form::message::Create>,
) -> Result<resp::JsonResp<resp::IdResp>> {
    message.validate()?;

    let message = model::Message::build(message.content, message.password)?;
    let id = db::message::create(&state.pool, &message).await?;

    Ok(resp::id_resp(id).to_json())
}
  • 对客户端提交的数据进行验证
  • 调用 Message 模型的 build() 方法,快速创建模型实例
  • 调用数据操作的 create() 方法,将数据入库

查看消息

// src/frontend.rs

#[derive(serde::Serialize)]
pub struct MessageResp {
    pub need_password: bool,
    #[serde(flatten)]
    pub message: Option<model::Message>,
}
pub async fn read_message(
    State(state): State<ArcAppState>,
    Json(frm): Json<form::message::Read>,
) -> Result<resp::JsonResp<MessageResp>> {
    frm.validate()?;

    let m = db::message::get(&state.pool, &frm.id).await?;
    let m = match m {
        Some(v) => v,
        None => return Err(anyhow::anyhow!("不存在的消息").into()),
    };
    let (need_password, password) = m.if_has_password();

    // 需要密码
    if need_password {
        // 未提供密码
        if frm.password.is_none() {
            return Ok(resp::ok(MessageResp {
                need_password: true,
                message: None,
            })
            .to_json());
        }

        let pwd = frm.password.unwrap();
        // 密码错误
        if !util::verify_pwd(&pwd, &password)? {
            return Err(anyhow::anyhow!("密码错误").into());
        }
        // 密码正确
        tokio::spawn(delete_viewed_msg(
            state.pool.clone(),
            Arc::new(frm.id),
            state.cfg.delete_interval,
        ));
        return Ok(resp::ok(MessageResp {
            need_password: false,
            message: Some(m),
        })
        .to_json());
    }

    // 不需要密码
    tokio::spawn(delete_viewed_msg(
        state.pool.clone(),
        Arc::new(frm.id),
        state.cfg.delete_interval,
    ));
    Ok(resp::ok(MessageResp {
        need_password: false,
        message: Some(m),
    })
    .to_json())
}

现在我们来看看 delete_viewed_msg() 的实现:

async fn delete_viewed_msg(p: PgPool, id: Arc<String>, delete_interval: u32) {
    tokio::time::sleep(std::time::Duration::from_secs(delete_interval as u64)).await;
    match db::message::del(&p, &id).await {
        Ok(aff) => tracing::debug!(
            "已删除消息:{},间隔时间:{},受影响的行数:{}",
            id,
            delete_interval,
            aff
        ),
        Err(e) => tracing::error!("删除消息 #{} 失败:{}", id, e),
    };
}
  • 休眠指定时长
  • 删除指定信息

测试

// frontend-api.http

## 创建消息
POST http://127.0.0.1:9527/message
Content-Type: application/json

{
    "content":"Hello, 世界",
    "password":""
}

## 创建带密码的消息
POST http://127.0.0.1:9527/message
Content-Type: application/json

{
    "content":"你好,World",
    "password":"foobar"
}

## 访问无密码消息
POST http://127.0.0.1:9527/message/view
Content-Type: application/json

{
    "id":"d08bmj4drfailna19uog"
}


## 访问带密码消息,但未提供密码
POST http://127.0.0.1:9527/message/view
Content-Type: application/json

{
    "id":"d08c82sdrfatpb5ium90"
}

## 访问带密码消息,提供错误密码
POST http://127.0.0.1:9527/message/view
Content-Type: application/json

{
    "id":"d08c82sdrfatpb5ium90",
    "password":"barfoo"
}

## 访问带密码消息,提供正确密码
POST http://127.0.0.1:9527/message/view
Content-Type: application/json

{
    "id":"d08c82sdrfatpb5ium90",
    "password":"foobar"
}

## 访问不存在的消息
POST http://127.0.0.1:9527/message/view
Content-Type: application/json

{
    "id":"d08bm6sdrfailna19123"
}
要查看完整内容,请先登录