接續昨天成果擴充了電話號碼的 QR code 功能後,今天我們打鐵趁熱,要進一步地新增兩種新的 QR code 類型,分別是 Mail 和地址。對於地址部分,會使用 Google 的 Geocoding API 來取得經緯度。
在商務或社交場合,除了電話號碼外,電子郵件和實體地址也是常需要交換的資訊。透過 QR Code,這些信息交換會變得更為便捷和高效。
首先,在 models
資料夾下的 mod.rs
中,我們再次增加新的欄位來儲存電子郵件和地址。
#[derive(serde::Deserialize)]
pub struct Info {
pub url: Option<String>,
pub phone: Option<String>,
pub email: Option<String>,
pub address: Option<String>,
pub foreground: Option<String>,
pub background: Option<String>,
pub dimension: Option<u32>,
}
如同之前一樣的設定,我們使用 Option
來應對不同需求。
為了將地址轉換成經緯度,我們需要調用 Geocoding API。你需要先設定一個 Google Cloud。
接下來要做一些驗證,之後我們就可以取得價值 $300 美元的 Google Cloud 免費抵免額。
需要綁定信用卡以及回答一些問題,這部分都可以按照個人情況選擇回應。
最後會拿到一組專屬你的 API 金鑰,要保管好。
拿到 API 金鑰之後,接下來就可以嘗試看看是否可以使用,以下是 API 的範例,記得 YOUR_API_KEY 替換成自己的 API 金鑰:
https://maps.googleapis.com/maps/api/geocode/json?address=台北市中正區衡陽路7號&key=YOUR_API_KEY
回傳的結果可以拿到我們要的經緯度:
在開始之前,我們需要安裝一些套件來做處理。
cargo add reqwest -F json
cargo add serde_json dotenv
reqwest 會幫助我們來發送 HTTP 請求,因為我們需要跟 Google Geocoding 去請求並回傳結果。
serde_json 則是處理送回來的 JSON,我們需要拿到經緯度的資料。
最後我們不希望把 API 金鑰會跟著程式碼一起上傳到 GitHub,所以會新增一個 .env 的環境變數來存放,這時候就需要 dotenv 來幫我們處理。
現在我們的 Cargo.toml 的 dependencies 應該會是這樣:
[dependencies]
actix-web = "4.4.0"
dotenv = "0.15.0"
image = "0.23.0"
qrcode = "0.12.0"
regex = "1.9.5"
reqwest = { version = "0.11.20", features = ["json"] }
serde = { version = "1.0.188", features = ["derive"] }
serde_json = "1.0.107"
接著引入必要的套件:
use actix_web::{get, post, web, HttpResponse};
use dotenv::dotenv;
use image::{DynamicImage, Luma};
use qrcode::render::svg;
use qrcode::QrCode;
use regex::Regex;
use std::env;
新增一個函式來處理地址轉換經緯度:
async fn get_coordinates(address: &str) -> Result<(f64, f64), reqwest::Error> {
dotenv().ok();
let api_key = env::var("GOOGLE_MAPS_API_KEY").expect("GOOGLE_MAPS_API_KEY must be set");
let url = format!(
"https://maps.googleapis.com/maps/api/geocode/json?address={}&key={}",
address, api_key
);
let response: serde_json::Value = reqwest::get(&url).await?.json().await?;
let lat = response["results"][0]["geometry"]["location"]["lat"]
.as_f64()
.unwrap();
let lng = response["results"][0]["geometry"]["location"]["lng"]
.as_f64()
.unwrap();
Ok((lat, lng))
}
最後在 generate_svg()
新增處理 email
跟 address
:
#[post("/generate_qr_svg")]
async fn generate_svg(data: web::Json<Info>) -> HttpResponse {
// 省略
let code_data = match data
.url
.as_ref()
.map(|url| url.as_bytes().to_vec())
.or_else(|| {
data.phone
.as_ref()
.map(|phone| format!("tel:{}", phone).as_bytes().to_vec())
})
.or_else(|| {
data.email
.as_ref()
.map(|mail| format!("mailto:{}", mail).as_bytes().to_vec())
})
.or_else(|| {
data.address
.as_ref()
.and_then(|address| get_coordinates(address).await.ok())
.map(|(lat, lng)| format!("geo:{},{}", lat, lng).as_bytes().to_vec())
}) {
Some(data) => data,
None => return HttpResponse::BadRequest().body("缺少有效數據"),
};
// 省略
}
不過,執行後會發現有問題,因為預期在處理 address
的回傳資料想要用 await
去等待資料回傳,但是 Cargo 會告訴我們,說 await
只能在 async
函式或區塊中使用。
為了解決這個問題,可以將這部分的邏輯移到一個獨立的 async
函式中,然後在主要函式中再去做 await
這個新函式。
async fn get_code_data(data: &Info) -> Option<Vec<u8>> {
if let Some(url) = &data.url {
return Some(url.as_bytes().to_vec());
}
if let Some(phone) = &data.phone {
return Some(format!("tel:{}", phone).as_bytes().to_vec());
}
if let Some(email) = &data.email {
return Some(format!("mailto:{}", email).as_bytes().to_vec());
}
if let Some(address) = &data.address {
if let Ok((lat, lng)) = get_coordinates(address).await {
return Some(format!("geo:{},{}", lat, lng).as_bytes().to_vec());
}
}
None
}
#[post("/generate_qr_svg")]
async fn generate_svg(data: web::Json<Info>) -> HttpResponse {
// 省略
let code_data = match get_code_data(&data).await {
Some(data) => data,
None => return HttpResponse::BadRequest().body("缺少有效數據"),
};
// 省略
}
在 Postman 測試地址的結果是 OK 的。
測試一下 Email 也是沒問題。
今天新增了兩種新的 QR Code 類型,明天我們將進一步優化和重構我們的程式,以達到更高的效能和可讀性。
我們明天見!👋