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"
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)
}
}
77 changes: 76 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,28 @@
use log::error;
use log::{error, warn};
use schemars::schema::RootSchema;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use thiserror::Error;

#[derive(Error, Debug)]
pub enum ConfigError {
#[error("Cookie不能为空")]
CookieEmpty,

#[error("门票ID不能为空")]
TicketIdEmpty,

#[error("购票数量无效: {0},必须大于0且不超过10")]
InvalidTicketCount(usize),

#[error("场次索引无效: {0},必须大于0")]
InvalidSessionIndex(usize),

#[error("票档索引无效: {0},必须大于0")]
InvalidGradeIndex(usize),

#[error("备注不能为空")]
RemarkEmpty,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Ticket {
Expand All @@ -10,6 +32,24 @@ pub struct Ticket {
pub grade: usize,
}

impl Ticket {
pub fn validate(&self) -> Result<(), ConfigError> {
if self.id.is_empty() {
return Err(ConfigError::TicketIdEmpty);
}
if self.num == 0 || self.num > 10 {
return Err(ConfigError::InvalidTicketCount(self.num));
}
if self.sessions == 0 {
return Err(ConfigError::InvalidSessionIndex(self.sessions));
}
if self.grade == 0 {
return Err(ConfigError::InvalidGradeIndex(self.grade));
}
Ok(())
}
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Account {
pub cookie: String,
Expand All @@ -19,11 +59,46 @@ pub struct Account {
pub earliest_submit_time: Option<i64>,
}

impl Account {
pub fn validate(&self) -> Result<(), ConfigError> {
if self.cookie.is_empty() {
return Err(ConfigError::CookieEmpty);
}
if self.remark.is_empty() {
warn!("账户备注为空,建议添加备注以便区分不同账户");
}
self.ticket.validate()
}
}

#[derive(Serialize, Deserialize, Debug)]
pub struct Config {
pub accounts: Vec<Account>,
}

impl Config {
pub fn validate(&self) -> Result<Vec<(usize, String)>, Vec<(usize, String)>> {
let mut errors = Vec::new();

if self.accounts.is_empty() {
errors.push((0, "没有配置任何账户".to_string()));
return Err(errors);
}

for (index, account) in self.accounts.iter().enumerate() {
if let Err(e) = account.validate() {
errors.push((index + 1, format!("账户{}配置错误: {}", index + 1, e)));
}
}

if errors.is_empty() {
Ok(Vec::new())
} else {
Err(errors)
}
}
}

fn load_config<T>(path: &str) -> Option<T>
where
T: DeserializeOwned,
Expand Down
Loading