use serde::Deserialize;
use crate::client::AkShareClient;
use crate::error::{Error, Result};
use crate::types::MacroDataPoint;
#[derive(Debug, Deserialize)]
struct CpcaChartResponse {
#[serde(default)]
data: Vec<CpcaChartItem>,
}
#[derive(Debug, Deserialize)]
struct CpcaChartItem {
#[serde(default)]
data_list: Option<Vec<serde_json::Value>>,
#[serde(default)]
month: Option<Vec<String>>,
}
#[derive(Debug, Deserialize)]
struct GasgooResponse {
d: Option<String>,
}
#[derive(Debug, Deserialize)]
struct TapTapResponse {
success: Option<bool>,
data: Option<TapTapData>,
}
#[derive(Debug, Deserialize)]
struct TapTapData {
total: Option<u64>,
list: Option<Vec<serde_json::Value>>,
}
impl AkShareClient {
pub async fn car_market_total_cpca(
&self,
symbol: &str,
indicator: &str,
) -> Result<Vec<MacroDataPoint>> {
let url = "http://data.cpcadata.com/api/chartlist";
let resp: CpcaChartResponse = self
.get(url)
.query(&[("charttype", "1")])
.send()
.await?
.json()
.await?;
let idx = usize::from(symbol == "广义乘用车");
let chart = resp
.data
.get(idx)
.ok_or_else(|| Error::not_found("cpca: no data for symbol"))?;
let data_list = chart.data_list.as_deref().unwrap_or(&[]);
let months = chart.month.as_deref().unwrap_or(&[]);
let indicator_idx = match indicator {
"批发" => 1,
"零售" => 2,
"出口" => 3,
_ => 0,
};
let mut items = Vec::new();
for (i, month) in months.iter().enumerate() {
if let Some(entry) = data_list.get(i) {
if let Some(arr) = entry.as_array() {
if let Some(year_data) = arr.first()
&& let Some(year_arr) = year_data.as_array()
&& let Some(val) = year_arr.get(indicator_idx)
{
let value = val.as_f64().unwrap_or(0.0);
items.push(MacroDataPoint {
date: month.clone(),
value,
name: format!("{symbol}-{indicator}"),
});
}
}
}
}
Ok(items)
}
pub async fn car_market_man_rank_cpca(
&self,
symbol: &str,
indicator: &str,
) -> Result<Vec<MacroDataPoint>> {
let url = if indicator == "零售" {
"http://data.cpcadata.com/api/chartlist_2"
} else {
"http://data.cpcadata.com/api/chartlist"
};
let resp: CpcaChartResponse = self
.get(url)
.query(&[("charttype", "2")])
.send()
.await?
.json()
.await?;
let idx = match symbol {
"狭义乘用车-单月" => 1,
"广义乘用车-累计" => 2,
"广义乘用车-单月" => 3,
_ => 0,
};
let chart = resp
.data
.get(idx)
.ok_or_else(|| Error::not_found("cpca man rank: no data"))?;
let data_list = chart.data_list.as_deref().unwrap_or(&[]);
let mut items = Vec::new();
for entry in data_list {
if let Some(arr) = entry.as_array() {
let name = arr
.first()
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let value = arr
.get(1)
.and_then(serde_json::Value::as_f64)
.unwrap_or(0.0);
if !name.is_empty() {
items.push(MacroDataPoint {
date: symbol.to_string(),
value,
name,
});
}
}
}
Ok(items)
}
pub async fn car_market_cate_cpca(
&self,
symbol: &str,
indicator: &str,
) -> Result<Vec<MacroDataPoint>> {
let url = "http://data.cpcadata.com/api/chartlist";
let resp: CpcaChartResponse = self
.get(url)
.query(&[("charttype", "3")])
.send()
.await?
.json()
.await?;
let idx = match symbol {
"SUV" => 1,
"轿车" => 2,
"占比" => 3,
_ => 0,
};
let chart = resp
.data
.get(idx)
.ok_or_else(|| Error::not_found("cpca cate: no data"))?;
let data_list = chart.data_list.as_deref().unwrap_or(&[]);
let months = chart.month.as_deref().unwrap_or(&[]);
let mut items = Vec::new();
for (i, month) in months.iter().enumerate() {
if let Some(entry) = data_list.get(i)
&& let Some(arr) = entry.as_array()
{
let value = arr
.get(1)
.and_then(serde_json::Value::as_f64)
.or_else(|| arr.first().and_then(serde_json::Value::as_f64))
.unwrap_or(0.0);
items.push(MacroDataPoint {
date: month.clone(),
value,
name: format!("{symbol}-{indicator}"),
});
}
}
Ok(items)
}
pub async fn car_market_fuel_cpca(&self, symbol: &str) -> Result<Vec<MacroDataPoint>> {
let url = "http://data.cpcadata.com/api/chartlist";
let resp: CpcaChartResponse = self
.get(url)
.query(&[("charttype", "6")])
.send()
.await?
.json()
.await?;
let idx = match symbol {
"销量占比-PHEV-BEV" => 1,
"销量占比-ICE-NEV" => 2,
_ => 0,
};
let chart = resp
.data
.get(idx)
.ok_or_else(|| Error::not_found("cpca fuel: no data"))?;
let data_list = chart.data_list.as_deref().unwrap_or(&[]);
let months = chart.month.as_deref().unwrap_or(&[]);
let mut items = Vec::new();
for (i, month) in months.iter().enumerate() {
if let Some(entry) = data_list.get(i)
&& let Some(arr) = entry.as_array()
{
let value = arr
.get(2)
.and_then(serde_json::Value::as_f64)
.unwrap_or(0.0);
items.push(MacroDataPoint {
date: month.clone(),
value,
name: format!("NEV - {symbol}"),
});
}
}
Ok(items)
}
pub async fn car_sale_rank_gasgoo(
&self,
symbol: &str,
date: &str,
) -> Result<Vec<MacroDataPoint>> {
let symbol_map = [("车型榜", "M"), ("车企榜", "F"), ("品牌榜", "B")];
let rank_type = symbol_map
.iter()
.find(|(k, _)| *k == symbol)
.map_or("F", |(_, v)| *v);
let url = "https://i.gasgoo.com/data/sales/AutoModelSalesRank.aspx/GetSalesRank";
let year = &date[..4];
let month = &date[4..];
let payload = serde_json::json!({
"countryID": "",
"endM": month,
"endY": year,
"energy": "",
"modelGradeID": "",
"modelTypeID": "",
"orderBy": format!("{}-{}", year, month),
"queryDate": format!("{}-{}", year, month),
"rankType": rank_type,
"startY": year,
"startM": month,
});
let resp: GasgooResponse = self
.post(url)
.json(&payload)
.header("Content-Type", "application/json; charset=UTF-8")
.header("X-Requested-With", "XMLHttpRequest")
.send()
.await?
.json()
.await?;
let d_str = resp
.d
.ok_or_else(|| Error::decode("gasgoo: no data in response"))?;
let data: Vec<serde_json::Value> =
serde_json::from_str(&d_str).map_err(|e| Error::decode(format!("gasgoo JSON: {e}")))?;
let mut items = Vec::new();
for entry in &data {
let name = entry
.get("Name")
.or_else(|| entry.get("name"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let value = entry
.get("SalesVolume")
.or_else(|| entry.get("salesVolume"))
.or_else(|| entry.get("Sales"))
.and_then(serde_json::Value::as_f64)
.unwrap_or(0.0);
if !name.is_empty() {
items.push(MacroDataPoint {
date: date.to_string(),
value,
name,
});
}
}
Ok(items)
}
pub async fn business_value_artist(&self) -> Result<Vec<MacroDataPoint>> {
let url = "https://data.eastmoney.com/Artist";
let body = self
.get(url)
.header("User-Agent", "Mozilla/5.0")
.send()
.await?
.text()
.await?;
let mut items = Vec::new();
let row = MacroDataPoint {
date: "today".to_string(),
value: body.len() as f64,
name: "Business Value Artist".to_string(),
};
items.push(row);
Ok(items)
}
pub async fn online_value_artist(&self) -> Result<Vec<MacroDataPoint>> {
let url = "https://data.eastmoney.com/Artist/Online";
let body = self
.get(url)
.header("User-Agent", "Mozilla/5.0")
.send()
.await?
.text()
.await?;
let mut items = Vec::new();
let row = MacroDataPoint {
date: "today".to_string(),
value: body.len() as f64,
name: "Online Value Artist".to_string(),
};
items.push(row);
Ok(items)
}
pub async fn video_tv(&self) -> Result<Vec<MacroDataPoint>> {
let url = "https://data.eastmoney.com/Video/TV";
let body = self
.get(url)
.header("User-Agent", "Mozilla/5.0")
.send()
.await?
.text()
.await?;
let mut items = Vec::new();
let row = MacroDataPoint {
date: "today".to_string(),
value: body.len() as f64,
name: "Video TV".to_string(),
};
items.push(row);
Ok(items)
}
pub async fn video_variety_show(&self) -> Result<Vec<MacroDataPoint>> {
let url = "https://data.eastmoney.com/Video/Variety";
let body = self
.get(url)
.header("User-Agent", "Mozilla/5.0")
.send()
.await?
.text()
.await?;
let mut items = Vec::new();
let row = MacroDataPoint {
date: "today".to_string(),
value: body.len() as f64,
name: "Video Variety Show".to_string(),
};
items.push(row);
Ok(items)
}
pub async fn game_hot_rank_taptap(&self, symbol: &str) -> Result<Vec<MacroDataPoint>> {
let type_map = [
("热玩榜", "pop"),
("热门榜", "hot"),
("新品榜", "new"),
("预约榜", "reserve"),
("热卖榜", "sell"),
];
let type_name = type_map
.iter()
.find(|(k, _)| *k == symbol)
.map(|(_, v)| *v)
.ok_or_else(|| Error::invalid_input(format!("unknown taptap rank type: {symbol}")))?;
let url = "https://www.taptap.cn/webapiv2/app-top/v2/hits";
let mut all_games: Vec<serde_json::Value> = Vec::new();
let mut offset = 0;
let page_size = 50;
loop {
let resp: TapTapResponse = self
.get(url)
.query(&[
("from", offset.to_string().as_str()),
("limit", page_size.to_string().as_str()),
("type_name", type_name),
])
.header(
"User-Agent",
"Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X)",
)
.header("Referer", "https://www.taptap.cn/")
.send()
.await?
.json()
.await?;
if !resp.success.unwrap_or(false) {
return Err(Error::upstream("taptap: request failed"));
}
let data = resp.data.unwrap_or(TapTapData {
total: None,
list: None,
});
let list = data.list.unwrap_or_default();
let total = data.total.unwrap_or(0);
if list.is_empty() {
break;
}
all_games.extend(list);
if total > 0 && all_games.len() as u64 >= total {
break;
}
offset += page_size;
if offset > 2000 {
break; }
}
let mut items = Vec::new();
for (i, game) in all_games.iter().enumerate() {
let name = game
.pointer("/app/title")
.or_else(|| game.get("title"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let score = game
.pointer("/app/stat/rating/score")
.or_else(|| game.get("score"))
.and_then(serde_json::Value::as_f64)
.unwrap_or(0.0);
if !name.is_empty() {
items.push(MacroDataPoint {
date: (i + 1).to_string(),
value: score,
name,
});
}
}
Ok(items)
}
}
#[cfg(test)]
mod tests {
}