域名 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
);
字段说明
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> 这一个值。请读者以作业的形式对该方法进行改写

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

Handler

创建新消息

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

查看消息

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

现在我们来看看 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"
}
要查看完整内容,请先登录