Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,6 @@ md5 = {version="0.7.0"}
dotenv = {version= "0.15.0"}
futures = {version="0.3.28"}
chrono = {version="0.4.24", features = ["unstable-locales"] }
async-channel={version = "1.8"}
async-channel={version = "1.8"}
backoff = { version = "0.4.0", default-features = false, features = ["tokio"] }
thiserror = "1.0.50"
11 changes: 8 additions & 3 deletions config/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,14 @@ accounts:
id: "719540964775"
# 需要抢购的门票数量
num: 1
# 需要抢购的场次序号.
sessions: 1
# 需要抢购的票档序号
# 需要抢购的场次优先级列表(按顺序尝试,第1个为最高优先级)
# 当某个场次售罄或抢购失败时,会自动切换到下一个场次继续尝试
# 场次序号从1开始,如 [1, 2, 3] 表示先尝试场次1,失败后尝试场次2,再失败尝试场次3
sessions:
- 1
- 2
- 3
# 需要抢购的票档序号(所有场次使用相同的票档序号)
grade: 3


Expand Down
126 changes: 109 additions & 17 deletions src/client.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,36 @@
use std::{env, time::Instant};
use std::{env, time::Duration, time::Instant};

use anyhow::Result;
use log::{debug, warn};
use backoff::future::retry;
use backoff::ExponentialBackoff;
use log::{debug, error, warn};
use reqwest::{
header::{HeaderMap, HeaderValue},
Client,
};
use serde_json::{json, Value};
use thiserror::Error;

use crate::models::{ticket::TicketInfoParams, DmRes, DmToken};

const API_TIMEOUT: Duration = Duration::from_secs(30);
const MAX_RETRIES: usize = 3;

#[derive(Error, Debug)]
pub enum ApiError {
#[error("请求错误: {0}")]
RequestError(#[from] reqwest::Error),

#[error("API返回错误: {0:?}")]
ApiError(Vec<String>),

#[error("解析错误: {0}")]
ParseError(#[from] serde_json::Error),

#[error("操作超时")]
Timeout,
}

const SUCCESS_CODE: u64 = 200;
const SYSTEM_ERROR_CODE: u16 = 500;

Expand All @@ -19,27 +40,53 @@ pub struct TokenClient {

impl TokenClient {
pub fn new() -> Result<Self> {
let client = reqwest::Client::builder().build()?;
let client = reqwest::Client::builder()
.timeout(API_TIMEOUT)
.connect_timeout(API_TIMEOUT)
.build()?;
Ok(Self { client })
}

// Get value from api.
pub async fn get_value(&self, key: &str) -> Result<String> {
async fn get_value_with_retry(&self, key: &str) -> Result<String> {
let backoff = ExponentialBackoff {
max_elapsed_time: Some(Duration::from_secs(60)),
..Default::default()
};

retry(backoff, || async {
match self.get_value_internal(key).await {
Ok(value) => Ok(value),
Err(e) => {
if e.is_timeout() || e.is_connect() {
warn!("Token请求错误,准备重试: {}", e);
Err(backoff::Error::transient(e))
} else {
Err(backoff::Error::permanent(e))
}
}
}
})
.await
.map_err(|e| anyhow::anyhow!("获取token失败: {}", e))
}

async fn get_value_internal(&self, key: &str) -> Result<String, reqwest::Error> {
let url = env::var("TOKEN_SERVER_URL").unwrap();

let params = json!({
"key": key,
});

let data = self
let response = self
.client
.get(url)
.get(&url)
.query(&params)
.timeout(API_TIMEOUT)
.send()
.await?
.json::<Value>()
.await?;

let data: Value = response.json().await?;

let code = data
.get("code")
.unwrap_or(&SYSTEM_ERROR_CODE.into())
Expand All @@ -48,7 +95,7 @@ impl TokenClient {

Ok(match code {
SUCCESS_CODE => {
let value = data["data"]["value"].as_str().unwrap().to_string();
let value = data["data"]["value"].as_str().unwrap_or("").to_string();
debug!("Get {}:{}", key, value);
value
}
Expand All @@ -59,15 +106,17 @@ impl TokenClient {
})
}

// Get bx ua.
pub async fn get_value(&self, key: &str) -> Result<String> {
self.get_value_with_retry(key).await
}

pub async fn get_bx_ua(&self) -> Result<String> {
let start = Instant::now();
let bx_ua = self.get_value("bx_ua").await?;
debug!("获取bx_ua: {:?}, 花费时间:{:?}", bx_ua, start.elapsed());
Ok(bx_ua)
}

/// Get bx token.
pub async fn get_bx_token(&self) -> Result<String> {
let start = Instant::now();
let bx_token = self.get_value("bx_token").await?;
Expand All @@ -88,6 +137,25 @@ pub struct DmClient {
}

pub async fn get_token(cookie: &str) -> Result<DmToken> {
let backoff = ExponentialBackoff {
max_elapsed_time: Some(Duration::from_secs(60)),
..Default::default()
};

retry(backoff, || async {
match get_token_internal(cookie).await {
Ok(token) => Ok(token),
Err(e) => {
error!("获取Token失败,准备重试: {}", e);
Err(backoff::Error::transient(e))
}
}
})
.await
.map_err(|e| anyhow::anyhow!("获取Token最终失败: {}", e))
}

async fn get_token_internal(cookie: &str) -> Result<DmToken> {
let mut headers = HeaderMap::new();
let url = "https://mtop.damai.cn/";

Expand All @@ -98,6 +166,8 @@ pub async fn get_token(cookie: &str) -> Result<DmToken> {
.default_headers(headers)
.cookie_store(true)
.http2_prior_knowledge()
.timeout(API_TIMEOUT)
.connect_timeout(API_TIMEOUT)
.build()?;

let mut token = DmToken {
Expand All @@ -108,7 +178,7 @@ pub async fn get_token(cookie: &str) -> Result<DmToken> {

let url = "https://mtop.damai.cn/h5/mtop.damai.wireless.search.broadcast.list/1.0/?";
let params = TicketInfoParams::build()?;
let response = client.get(url).form(&params).send().await?;
let response = client.get(url).form(&params).timeout(API_TIMEOUT).send().await?;

for cookie in response.cookies() {
if cookie.name() == "_m_h5_tk" {
Expand Down Expand Up @@ -152,6 +222,8 @@ impl DmClient {
.http2_prior_knowledge()
.user_agent("Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3")
.use_rustls_tls()
.timeout(API_TIMEOUT)
.connect_timeout(API_TIMEOUT)
.build()?;
Ok(Self {
client,
Expand All @@ -162,6 +234,27 @@ impl DmClient {
}

pub async fn request(&self, url: &str, mut params: Value, data: Value) -> Result<DmRes> {
let backoff = ExponentialBackoff {
max_elapsed_time: Some(Duration::from_secs(60)),
..Default::default()
};

let url = url.to_string();

retry(backoff, || async {
match self.request_internal(&url, params.clone(), data.clone()).await {
Ok(res) => Ok(res),
Err(e) => {
error!("API请求失败,准备重试: {}", e);
Err(backoff::Error::transient(e))
}
}
})
.await
.map_err(|e| anyhow::anyhow!("API请求最终失败: {}", e))
}

async fn request_internal(&self, url: &str, mut params: Value, data: Value) -> Result<DmRes> {
let s = format!(
"{}&{}&{}&{}",
self.token.token,
Expand All @@ -179,20 +272,19 @@ impl DmClient {

let form = json!({
"data": serde_json::to_string(&data)?,
// "bx-umidtoken": params["bx-umidtoken"],
// "bx-ua": params["bx-ua"]
});

let response = self
.client
.post(url)
.query(&params)
.form(&form)
.timeout(API_TIMEOUT)
.send()
.await?;

let data = response.json::<DmRes>().await?;
let response_data = response.json::<DmRes>().await?;

Ok(data)
Ok(response_data)
}
}
Loading