昨天建立了用來寄送email的組件,接下來就要把它放到API中了,首先要對資料表進行異動,除了增加一個欄位用來紀錄訂閱的狀態以外,也要增加一張表用來紀錄認證信所需的token,當訂戶打開驗證連結時使用token來修改訂閱狀態:
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_type(
Type::create()
.as_enum(SubscriptionStatus::Table)
.values(SubscriptionStatus::iter().skip(1))
.to_owned(),
)
.await?;
manager
.alter_table(
Table::alter()
.table(Subscriptions::Table)
.add_column(
ColumnDef::new(Subscriptions::Status)
.enumeration(
SubscriptionStatus::Table,
SubscriptionStatus::iter().skip(1),
)
.not_null()
.default("inactive"),
)
.to_owned(),
)
.await?;
manager
.create_table(
Table::create()
.table(SubscriptionTokens::Table)
.if_not_exists()
.col(
ColumnDef::new(SubscriptionTokens::SubscriptionToken)
.text()
.not_null()
.primary_key(),
)
.col(
ColumnDef::new(SubscriptionTokens::SubscriberId)
.uuid()
.not_null(),
)
.to_owned(),
)
.await?;
manager
.create_foreign_key(
ForeignKey::create()
.from(SubscriptionTokens::Table, SubscriptionTokens::SubscriberId)
.to(Subscriptions::Table, Subscriptions::Id)
.on_delete(ForeignKeyAction::Cascade)
.on_update(ForeignKeyAction::Cascade)
.to_owned(),
)
.await?;
Ok(())
}
第一段的create_type
是用來在postgres
中增加一個enum,這是seaORM中對於posrgres特有的支援,可以用更語意化的方式紀錄狀態,也對該欄位的內容進行約束。
第二段alter_table
中,為了指定狀態欄位使用前面的enum,需要使用enumeration
方法,其中的SubscriptionStatus::iter().skip(1)
也可以改成寫死的Vec
。
最後在建立新表的時候,則需要注意外鍵關聯的建立。當腳本完成之後就可以進行遷移了。
接下來則是回過頭修改subscribe
handler,在紀錄訂閱資料的同時要關聯認證用的token:
pub async fn subscribe(state: State<AppState>, Form(data): Form<NewSubscriber>) -> Result<StatusCode,StatusCode> {
let new_subscriber = Subscriber::try_from(data)
.map_err(|_| StatusCode::BAD_REQUEST)?;
let subscriber = subscriptions::ActiveModel {
id: Set(Uuid::new_v4()),
email: Set(new_subscriber.email.as_ref().to_owned()),
name: Set(new_subscriber.name.as_ref().to_owned()),
subscribed_at: Set(chrono::Utc::now().into()),
..Default::default()
}
.insert(&state.database)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let subscription_token = subscription_tokens::ActiveModel {
subscriber_id: Set(subscriber.id),
subscription_token: Set(Uuid::new_v4().to_string()),
}
.insert(&state.database)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(StatusCode::Ok)
}
但是目前的寫法是兩次執行的,也就是說可能會發生新增了訂閱紀錄,卻沒有產生認證token,因此需要把它用交易包起來:
pub async fn subscribe(state: State<AppState>, Form(data): Form<NewSubscriber>) -> Result<StatusCode,StatusCode> {
let new_subscriber = Subscriber::try_from(data)
.map_err(|_| StatusCode::BAD_REQUEST)?;
//// 交易開始
let txn = state.database.begin().await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let subscriber = subscriptions::ActiveModel {
id: Set(Uuid::new_v4()),
email: Set(new_subscriber.email.as_ref().to_owned()),
name: Set(new_subscriber.name.as_ref().to_owned()),
subscribed_at: Set(chrono::Utc::now().into()),
..Default::default()
}
.insert(&state.database)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let subscription_token = subscription_tokens::ActiveModel {
subscriber_id: Set(subscriber.id),
subscription_token: Set(Uuid::new_v4().to_string()),
}
.insert(&state.database)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
//// 交易結束
txn.commit().await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(StatusCode::Ok)
}
我們要來利用前一天做好的email_client
,昨天提到了這個結構需要在整個應用程式中重複利用,所以就把它加入AppState
中,要調整的地方有configuration.rs
用於讀取第三方服務的設定、Application.rs
中於路由的部份加入email_client
,最後再調整subscribe
handler,由於我們希望訂閱紀錄、寄送信件都要一次做完,所以要把寄信加入交易的區間內。
pub async fn subscribe(state: State<AppState>, Form(data): Form<NewSubscriber>) -> Result<StatusCode,StatusCode> {
let new_subscriber = Subscriber::try_from(data)
.map_err(|_| StatusCode::BAD_REQUEST)?;
let txn = state.database.begin().await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let subscriber = subscriptions::ActiveModel {
id: Set(Uuid::new_v4()),
email: Set(new_subscriber.email.as_ref().to_owned()),
name: Set(new_subscriber.name.as_ref().to_owned()),
subscribed_at: Set(chrono::Utc::now().into()),
..Default::default()
}
.insert(&state.database)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let subscription_token = subscription_tokens::ActiveModel {
subscriber_id: Set(subscriber.id),
subscription_token: Set(Uuid::new_v4().to_string()),
}
.insert(&state.database)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
//// 取得email內容
let html_body = get_email_content(&state, subscription_token);
//// 寄送信件,因為不需要回傳值所以使用_
_ = &state
.email_client
.send_email(&new_subscriber.email, "Welcome!", &html_body)
.await?;
txn.commit().await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
Ok(StatusCode::Ok)
}
完整的實做可以參考這裡