域名 AXUM.RS 将于 2025 年 10 月到期。我们无意再对其进行续费,如果你有意接续这个域名,请与我们取得联系。
  • AXUM.RS 现仅需人民币 3000 元(大写:叁仟元整。接受适度议价
  • 按照行业规则,AXUM.RS 到期后,大概率会进入长时间的赎回期,该期间内,如果你想拥有该域名,将要付出高额的费用
  • 我们已启用 AXUM.EU.ORG 域名,并将持续运营
  • 仅接受微信或支付宝交易
如果你对 AXUM.RS 有兴趣,请和我们进行联系:

阅后即焚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
);

模型

// 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)
        }
    }
}
  • build() :用于快速构建一个 Message 模型的实例。如果传入的密码不是空字符,则将密码进行哈希。
  • if_has_password():用于判断某个实例是否有密码。返回值为一个元组:(是否有密码, 密码)

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

数据操作

  • 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())
}

查看消息

// 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())
}
  • MessageResp 结构体用于将指定消息响应给客户端。但它额外做了一件事:通过 need_password 告诉 API 调用者,是否需要提供密码。
  • 对客户端提交的数据进行验证
  • 调用数据操作的 get() 方法获取数据
  • 调用模型的 if_has_password() 判断并获取密码
  • 根据是否需要密码进行不同逻辑
    • 如果需要密码
      • 如果客户端未提供密码,返回需要密码的响应
      • 如果客户端提供了密码
        • 如果密码错误,返回密码错误
        • 如果密码正确,返回数据作为响应
    • 如果不需要密码,直接返回数据作为响应
  • 值得一提的是,在不需要密码和密码正确两个分支里,都通过 tokio::spawn 调用了 delete_viewed_msg()。它其实是为了实现延时删除消息。
  • 休眠指定时长
  • 删除指定信息

测试

// 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"
}
要查看完整内容,请先登录