554 lines
18 KiB
Rust
554 lines
18 KiB
Rust
|
|
//! 能耗管理 API — 总览、排行、趋势、导出
|
|||
|
|
//!
|
|||
|
|
//! 基于 energy_stats 表按日期聚合的能耗数据进行统计分析。
|
|||
|
|
//! 电费估算按 ¥0.80/kWh 计算(可配置)。
|
|||
|
|
|
|||
|
|
use axum::extract::{Query, State};
|
|||
|
|
use axum::Json;
|
|||
|
|
use serde::Deserialize;
|
|||
|
|
use serde_json::{json, Value};
|
|||
|
|
|
|||
|
|
use crate::error::AppError;
|
|||
|
|
use crate::middleware::auth::{self, CurrentUser};
|
|||
|
|
|
|||
|
|
/// 电费单价(元/kWh)
|
|||
|
|
const COST_PER_KWH: f64 = 0.80;
|
|||
|
|
|
|||
|
|
/// 趋势查询参数
|
|||
|
|
#[derive(Debug, Deserialize)]
|
|||
|
|
pub struct TrendQuery {
|
|||
|
|
/// day | week | month
|
|||
|
|
#[serde(default = "default_granularity")]
|
|||
|
|
pub granularity: String,
|
|||
|
|
/// 天数范围,默认30天
|
|||
|
|
#[serde(default = "default_range")]
|
|||
|
|
pub range: i64,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 导出查询参数
|
|||
|
|
#[derive(Debug, Deserialize)]
|
|||
|
|
pub struct ExportQuery {
|
|||
|
|
pub cabinet_id: Option<i64>,
|
|||
|
|
pub project_id: Option<i64>,
|
|||
|
|
pub start_date: Option<String>,
|
|||
|
|
pub end_date: Option<String>,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 柜子排行行
|
|||
|
|
#[derive(Debug, sqlx::FromRow)]
|
|||
|
|
struct CabinetRankRow {
|
|||
|
|
id: i64,
|
|||
|
|
abstract_id: String,
|
|||
|
|
name: Option<String>,
|
|||
|
|
today_energy: Option<f64>,
|
|||
|
|
month_energy: Option<f64>,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 项目统计行
|
|||
|
|
#[derive(Debug, sqlx::FromRow)]
|
|||
|
|
struct ProjectStatRow {
|
|||
|
|
id: i64,
|
|||
|
|
name: String,
|
|||
|
|
cabinet_count: i64,
|
|||
|
|
today_energy: Option<f64>,
|
|||
|
|
month_energy: Option<f64>,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 趋势数据行
|
|||
|
|
#[derive(Debug, sqlx::FromRow)]
|
|||
|
|
struct TrendRow {
|
|||
|
|
period: String, // 日期或周/月标识
|
|||
|
|
energy: Option<f64>,
|
|||
|
|
charge_count: Option<i64>,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 能耗明细行(用于导出)
|
|||
|
|
#[derive(Debug, sqlx::FromRow)]
|
|||
|
|
struct EnergyDetailRow {
|
|||
|
|
stat_date: chrono::NaiveDate,
|
|||
|
|
cabinet_id: i64,
|
|||
|
|
cabinet_name: Option<String>,
|
|||
|
|
project_name: Option<String>,
|
|||
|
|
total_energy: Option<f64>,
|
|||
|
|
charge_count: Option<i64>,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn default_granularity() -> String { "day".to_string() }
|
|||
|
|
fn default_range() -> i64 { 30 }
|
|||
|
|
|
|||
|
|
/// GET /api/energy-stats/summary — 总览
|
|||
|
|
///
|
|||
|
|
/// 返回今日/本月能耗、电费估算、柜子/在线数量。
|
|||
|
|
pub async fn get_summary(
|
|||
|
|
user: CurrentUser,
|
|||
|
|
State(state): State<super::AppState>,
|
|||
|
|
) -> Result<Json<Value>, AppError> {
|
|||
|
|
auth::check_permission(&user, "energy:view")?;
|
|||
|
|
let pool = &state.mysql;
|
|||
|
|
|
|||
|
|
// 组织过滤子查询
|
|||
|
|
let (org_filter, org_binds): (String, Vec<String>) = if user.role_level >= 2 {
|
|||
|
|
(String::new(), vec![])
|
|||
|
|
} else if let Some(oid) = user.organization_id {
|
|||
|
|
(
|
|||
|
|
" AND es.cabinet_id IN (SELECT c.id FROM cabinets c JOIN projects p ON c.project_id = p.id WHERE p.organization_id = ?)".to_string(),
|
|||
|
|
vec![oid.to_string()],
|
|||
|
|
)
|
|||
|
|
} else {
|
|||
|
|
(" AND 1=0".to_string(), vec![])
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 今日总能耗
|
|||
|
|
let today_sql = format!(
|
|||
|
|
"SELECT COALESCE(SUM(total_energy), 0) FROM energy_stats es WHERE stat_date = CURDATE(){}",
|
|||
|
|
org_filter
|
|||
|
|
);
|
|||
|
|
let mut today_q = sqlx::query_scalar::<_, f64>(&today_sql);
|
|||
|
|
for v in &org_binds { today_q = today_q.bind(v); }
|
|||
|
|
let today_energy: f64 = today_q.fetch_one(pool).await?;
|
|||
|
|
|
|||
|
|
// 本月总能耗
|
|||
|
|
let month_sql = format!(
|
|||
|
|
"SELECT COALESCE(SUM(total_energy), 0) FROM energy_stats es \
|
|||
|
|
WHERE stat_date >= DATE_FORMAT(CURDATE(), '%Y-%m-01'){}",
|
|||
|
|
org_filter
|
|||
|
|
);
|
|||
|
|
let mut month_q = sqlx::query_scalar::<_, f64>(&month_sql);
|
|||
|
|
for v in &org_binds { month_q = month_q.bind(v); }
|
|||
|
|
let month_energy: f64 = month_q.fetch_one(pool).await?;
|
|||
|
|
|
|||
|
|
// 柜子总数(含组织过滤)
|
|||
|
|
let cabinet_count: i64 = if user.role_level >= 2 {
|
|||
|
|
sqlx::query_scalar("SELECT COUNT(*) FROM cabinets")
|
|||
|
|
.fetch_one(pool).await?
|
|||
|
|
} else if let Some(oid) = user.organization_id {
|
|||
|
|
sqlx::query_scalar(
|
|||
|
|
"SELECT COUNT(*) FROM cabinets c JOIN projects p ON c.project_id = p.id WHERE p.organization_id = ?"
|
|||
|
|
)
|
|||
|
|
.bind(oid)
|
|||
|
|
.fetch_one(pool).await?
|
|||
|
|
} else {
|
|||
|
|
0
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 在线柜子数
|
|||
|
|
let online_count: i64 = if user.role_level >= 2 {
|
|||
|
|
sqlx::query_scalar("SELECT COUNT(*) FROM cabinets WHERE status = 1")
|
|||
|
|
.fetch_one(pool).await?
|
|||
|
|
} else if let Some(oid) = user.organization_id {
|
|||
|
|
sqlx::query_scalar(
|
|||
|
|
"SELECT COUNT(*) FROM cabinets c JOIN projects p ON c.project_id = p.id WHERE c.status = 1 AND p.organization_id = ?"
|
|||
|
|
)
|
|||
|
|
.bind(oid)
|
|||
|
|
.fetch_one(pool).await?
|
|||
|
|
} else {
|
|||
|
|
0
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
let today = today_energy;
|
|||
|
|
let month = month_energy;
|
|||
|
|
|
|||
|
|
Ok(Json(json!({
|
|||
|
|
"code": 0,
|
|||
|
|
"data": {
|
|||
|
|
"today_energy": (today * 100.0).round() / 100.0,
|
|||
|
|
"month_energy": (month * 100.0).round() / 100.0,
|
|||
|
|
"estimated_cost": ((month * COST_PER_KWH) * 100.0).round() / 100.0,
|
|||
|
|
"cabinet_count": cabinet_count,
|
|||
|
|
"online_count": online_count,
|
|||
|
|
},
|
|||
|
|
"message": "ok"
|
|||
|
|
})))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// GET /api/energy-stats/cabinets — 柜子能耗排行
|
|||
|
|
///
|
|||
|
|
/// 按今日能耗降序排列,返回柜子列表及今日/本月能耗。
|
|||
|
|
pub async fn get_cabinet_ranking(
|
|||
|
|
user: CurrentUser,
|
|||
|
|
State(state): State<super::AppState>,
|
|||
|
|
) -> Result<Json<Value>, AppError> {
|
|||
|
|
auth::check_permission(&user, "energy:view")?;
|
|||
|
|
let pool = &state.mysql;
|
|||
|
|
|
|||
|
|
let (org_join, org_binds): (String, Vec<String>) = if user.role_level >= 2 {
|
|||
|
|
(String::new(), vec![])
|
|||
|
|
} else if let Some(oid) = user.organization_id {
|
|||
|
|
(
|
|||
|
|
" JOIN projects p ON c.project_id = p.id AND p.organization_id = ?".to_string(),
|
|||
|
|
vec![oid.to_string()],
|
|||
|
|
)
|
|||
|
|
} else {
|
|||
|
|
(" JOIN projects p ON 1=0".to_string(), vec![])
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
let sql = format!(
|
|||
|
|
"SELECT c.id, c.abstract_id, c.name, \
|
|||
|
|
COALESCE(es_today.total_energy, 0) AS today_energy, \
|
|||
|
|
COALESCE(es_month.total_energy, 0) AS month_energy \
|
|||
|
|
FROM cabinets c \
|
|||
|
|
LEFT JOIN energy_stats es_today \
|
|||
|
|
ON c.id = es_today.cabinet_id AND es_today.stat_date = CURDATE() \
|
|||
|
|
LEFT JOIN energy_stats es_month \
|
|||
|
|
ON c.id = es_month.cabinet_id \
|
|||
|
|
AND es_month.stat_date >= DATE_FORMAT(CURDATE(), '%Y-%m-01') \
|
|||
|
|
{} \
|
|||
|
|
ORDER BY today_energy DESC, c.id ASC",
|
|||
|
|
org_join
|
|||
|
|
);
|
|||
|
|
let mut query = sqlx::query_as::<_, CabinetRankRow>(&sql);
|
|||
|
|
for v in &org_binds { query = query.bind(v); }
|
|||
|
|
let rows = query.fetch_all(pool).await?;
|
|||
|
|
|
|||
|
|
let list: Vec<Value> = rows.iter().map(|r| {
|
|||
|
|
json!({
|
|||
|
|
"cabinet_id": r.id,
|
|||
|
|
"abstract_id": r.abstract_id,
|
|||
|
|
"name": r.name,
|
|||
|
|
"today_energy": (r.today_energy.unwrap_or(0.0) * 100.0).round() / 100.0,
|
|||
|
|
"month_energy": (r.month_energy.unwrap_or(0.0) * 100.0).round() / 100.0,
|
|||
|
|
})
|
|||
|
|
}).collect();
|
|||
|
|
|
|||
|
|
Ok(Json(json!({
|
|||
|
|
"code": 0,
|
|||
|
|
"data": list,
|
|||
|
|
"message": "ok"
|
|||
|
|
})))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// GET /api/energy-stats/projects — 项目能耗统计
|
|||
|
|
///
|
|||
|
|
/// 按今日能耗降序排列,返回项目级聚合数据。
|
|||
|
|
pub async fn get_project_stats(
|
|||
|
|
user: CurrentUser,
|
|||
|
|
State(state): State<super::AppState>,
|
|||
|
|
) -> Result<Json<Value>, AppError> {
|
|||
|
|
auth::check_permission(&user, "energy:view")?;
|
|||
|
|
let pool = &state.mysql;
|
|||
|
|
|
|||
|
|
let (org_filter, org_binds): (String, Vec<String>) = if user.role_level >= 2 {
|
|||
|
|
(String::new(), vec![])
|
|||
|
|
} else if let Some(oid) = user.organization_id {
|
|||
|
|
(" WHERE p.organization_id = ?".to_string(), vec![oid.to_string()])
|
|||
|
|
} else {
|
|||
|
|
(" WHERE 1=0".to_string(), vec![])
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
let sql = format!(
|
|||
|
|
"SELECT p.id, p.name, \
|
|||
|
|
COUNT(DISTINCT c.id) AS cabinet_count, \
|
|||
|
|
COALESCE(SUM(es_today.total_energy), 0) AS today_energy, \
|
|||
|
|
COALESCE(SUM(es_month.total_energy), 0) AS month_energy \
|
|||
|
|
FROM projects p \
|
|||
|
|
LEFT JOIN cabinets c ON p.id = c.project_id \
|
|||
|
|
LEFT JOIN energy_stats es_today \
|
|||
|
|
ON c.id = es_today.cabinet_id AND es_today.stat_date = CURDATE() \
|
|||
|
|
LEFT JOIN energy_stats es_month \
|
|||
|
|
ON c.id = es_month.cabinet_id \
|
|||
|
|
AND es_month.stat_date >= DATE_FORMAT(CURDATE(), '%Y-%m-01') \
|
|||
|
|
{} \
|
|||
|
|
GROUP BY p.id, p.name \
|
|||
|
|
ORDER BY today_energy DESC, p.id ASC",
|
|||
|
|
org_filter
|
|||
|
|
);
|
|||
|
|
let mut query = sqlx::query_as::<_, ProjectStatRow>(&sql);
|
|||
|
|
for v in &org_binds { query = query.bind(v); }
|
|||
|
|
let rows = query.fetch_all(pool).await?;
|
|||
|
|
|
|||
|
|
let list: Vec<Value> = rows.iter().map(|r| {
|
|||
|
|
json!({
|
|||
|
|
"project_id": r.id,
|
|||
|
|
"name": r.name,
|
|||
|
|
"cabinet_count": r.cabinet_count,
|
|||
|
|
"today_energy": (r.today_energy.unwrap_or(0.0) * 100.0).round() / 100.0,
|
|||
|
|
"month_energy": (r.month_energy.unwrap_or(0.0) * 100.0).round() / 100.0,
|
|||
|
|
})
|
|||
|
|
}).collect();
|
|||
|
|
|
|||
|
|
Ok(Json(json!({
|
|||
|
|
"code": 0,
|
|||
|
|
"data": list,
|
|||
|
|
"message": "ok"
|
|||
|
|
})))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// GET /api/energy-stats/trend — 趋势数据
|
|||
|
|
///
|
|||
|
|
/// granularity: day | week | month, 默认 day
|
|||
|
|
/// range: 天数范围, 默认 30
|
|||
|
|
pub async fn get_trend(
|
|||
|
|
user: CurrentUser,
|
|||
|
|
State(state): State<super::AppState>,
|
|||
|
|
Query(params): Query<TrendQuery>,
|
|||
|
|
) -> Result<Json<Value>, AppError> {
|
|||
|
|
auth::check_permission(&user, "energy:view")?;
|
|||
|
|
let pool = &state.mysql;
|
|||
|
|
|
|||
|
|
// 组织过滤:es.cabinet_id → cabinets.project_id → projects.organization_id
|
|||
|
|
let (org_filter, org_binds): (String, Vec<String>) = if user.role_level >= 2 {
|
|||
|
|
(String::new(), vec![])
|
|||
|
|
} else if let Some(oid) = user.organization_id {
|
|||
|
|
(
|
|||
|
|
" AND es.cabinet_id IN (SELECT c.id FROM cabinets c JOIN projects p ON c.project_id = p.id WHERE p.organization_id = ?)".to_string(),
|
|||
|
|
vec![oid.to_string()],
|
|||
|
|
)
|
|||
|
|
} else {
|
|||
|
|
(" AND 1=0".to_string(), vec![])
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
let rows = match params.granularity.as_str() {
|
|||
|
|
"week" => {
|
|||
|
|
let sql = format!(
|
|||
|
|
"SELECT DATE_FORMAT(stat_date, '%Y-%u') AS period, \
|
|||
|
|
SUM(total_energy) AS energy, SUM(charge_count) AS charge_count \
|
|||
|
|
FROM energy_stats es \
|
|||
|
|
WHERE stat_date >= DATE_SUB(CURDATE(), INTERVAL ? DAY){} \
|
|||
|
|
GROUP BY period ORDER BY period ASC",
|
|||
|
|
org_filter
|
|||
|
|
);
|
|||
|
|
let mut q = sqlx::query_as::<_, TrendRow>(&sql).bind(params.range);
|
|||
|
|
for v in &org_binds { q = q.bind(v); }
|
|||
|
|
q.fetch_all(pool).await?
|
|||
|
|
}
|
|||
|
|
"month" => {
|
|||
|
|
let sql = format!(
|
|||
|
|
"SELECT DATE_FORMAT(stat_date, '%Y-%m') AS period, \
|
|||
|
|
SUM(total_energy) AS energy, SUM(charge_count) AS charge_count \
|
|||
|
|
FROM energy_stats es \
|
|||
|
|
WHERE stat_date >= DATE_SUB(CURDATE(), INTERVAL ? DAY){} \
|
|||
|
|
GROUP BY period ORDER BY period ASC",
|
|||
|
|
org_filter
|
|||
|
|
);
|
|||
|
|
let mut q = sqlx::query_as::<_, TrendRow>(&sql).bind(params.range);
|
|||
|
|
for v in &org_binds { q = q.bind(v); }
|
|||
|
|
q.fetch_all(pool).await?
|
|||
|
|
}
|
|||
|
|
_ => {
|
|||
|
|
// day
|
|||
|
|
let sql = format!(
|
|||
|
|
"SELECT stat_date AS period, \
|
|||
|
|
total_energy AS energy, charge_count AS charge_count \
|
|||
|
|
FROM energy_stats es \
|
|||
|
|
WHERE stat_date >= DATE_SUB(CURDATE(), INTERVAL ? DAY){} \
|
|||
|
|
ORDER BY stat_date ASC",
|
|||
|
|
org_filter
|
|||
|
|
);
|
|||
|
|
let mut q = sqlx::query_as::<_, TrendRow>(&sql).bind(params.range);
|
|||
|
|
for v in &org_binds { q = q.bind(v); }
|
|||
|
|
q.fetch_all(pool).await?
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
let list: Vec<Value> = rows.iter().map(|r| {
|
|||
|
|
json!({
|
|||
|
|
"period": r.period,
|
|||
|
|
"energy": (r.energy.unwrap_or(0.0) * 100.0).round() / 100.0,
|
|||
|
|
"charge_count": r.charge_count.unwrap_or(0),
|
|||
|
|
})
|
|||
|
|
}).collect();
|
|||
|
|
|
|||
|
|
Ok(Json(json!({
|
|||
|
|
"code": 0,
|
|||
|
|
"data": {
|
|||
|
|
"granularity": params.granularity,
|
|||
|
|
"list": list,
|
|||
|
|
},
|
|||
|
|
"message": "ok"
|
|||
|
|
})))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// GET /api/energy-stats/export — 异步导出能耗报表
|
|||
|
|
pub async fn export_energy_stats(
|
|||
|
|
user: CurrentUser,
|
|||
|
|
State(state): State<super::AppState>,
|
|||
|
|
Query(params): Query<ExportQuery>,
|
|||
|
|
) -> Result<Json<Value>, AppError> {
|
|||
|
|
auth::check_permission(&user, "energy:export")?;
|
|||
|
|
let user_id = user.user_id;
|
|||
|
|
let file_name = generate_export_filename(¶ms);
|
|||
|
|
|
|||
|
|
// 序列化查询参数
|
|||
|
|
let query_json = serde_json::to_string(&json!({
|
|||
|
|
"cabinet_id": params.cabinet_id,
|
|||
|
|
"project_id": params.project_id,
|
|||
|
|
"start_date": params.start_date,
|
|||
|
|
"end_date": params.end_date,
|
|||
|
|
}))
|
|||
|
|
.unwrap_or_default();
|
|||
|
|
|
|||
|
|
// 插入下载任务
|
|||
|
|
let result = sqlx::query(
|
|||
|
|
"INSERT INTO download_tasks (user_id, task_type, status, file_name, error_message) \
|
|||
|
|
VALUES (?, ?, 0, ?, ?)"
|
|||
|
|
)
|
|||
|
|
.bind(user_id)
|
|||
|
|
.bind("energy_stats")
|
|||
|
|
.bind(&file_name)
|
|||
|
|
.bind(&query_json)
|
|||
|
|
.execute(&state.mysql)
|
|||
|
|
.await?;
|
|||
|
|
|
|||
|
|
let task_id = result.last_insert_id() as i64;
|
|||
|
|
|
|||
|
|
// 后台异步生成 Excel
|
|||
|
|
let pool = state.mysql.clone();
|
|||
|
|
tokio::spawn(async move {
|
|||
|
|
if let Err(e) = generate_energy_excel(pool, task_id).await {
|
|||
|
|
tracing::error!("导出能耗统计失败 task_id={}: {}", task_id, e);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
Ok(Json(json!({
|
|||
|
|
"code": 0,
|
|||
|
|
"data": { "task_id": task_id },
|
|||
|
|
"message": "导出任务已创建"
|
|||
|
|
})))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 生成导出文件名
|
|||
|
|
fn generate_export_filename(params: &ExportQuery) -> String {
|
|||
|
|
let start = params
|
|||
|
|
.start_date
|
|||
|
|
.as_ref()
|
|||
|
|
.map(|s| s.replace('-', ""))
|
|||
|
|
.unwrap_or_default();
|
|||
|
|
let end = params
|
|||
|
|
.end_date
|
|||
|
|
.as_ref()
|
|||
|
|
.map(|s| s.replace('-', ""))
|
|||
|
|
.unwrap_or_default();
|
|||
|
|
format!("能耗统计_{}-{}.xlsx", start, end)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 构建导出条件
|
|||
|
|
fn build_export_clause(
|
|||
|
|
cabinet_id: Option<i64>,
|
|||
|
|
project_id: Option<i64>,
|
|||
|
|
start_date: Option<&str>,
|
|||
|
|
end_date: Option<&str>,
|
|||
|
|
) -> (String, Vec<String>) {
|
|||
|
|
let mut conditions = Vec::new();
|
|||
|
|
let mut bind_values = Vec::new();
|
|||
|
|
|
|||
|
|
if let Some(id) = cabinet_id {
|
|||
|
|
conditions.push("es.cabinet_id = ?");
|
|||
|
|
bind_values.push(id.to_string());
|
|||
|
|
}
|
|||
|
|
if let Some(id) = project_id {
|
|||
|
|
conditions.push("c.project_id = ?");
|
|||
|
|
bind_values.push(id.to_string());
|
|||
|
|
}
|
|||
|
|
if let Some(d) = start_date {
|
|||
|
|
conditions.push("es.stat_date >= ?");
|
|||
|
|
bind_values.push(d.to_string());
|
|||
|
|
}
|
|||
|
|
if let Some(d) = end_date {
|
|||
|
|
conditions.push("es.stat_date <= ?");
|
|||
|
|
bind_values.push(d.to_string());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let clause = if conditions.is_empty() {
|
|||
|
|
String::new()
|
|||
|
|
} else {
|
|||
|
|
format!("WHERE {}", conditions.join(" AND "))
|
|||
|
|
};
|
|||
|
|
(clause, bind_values)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 后台异步生成能耗 Excel 文件
|
|||
|
|
async fn generate_energy_excel(
|
|||
|
|
pool: sqlx::MySqlPool,
|
|||
|
|
task_id: i64,
|
|||
|
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
|
|
// 更新状态为处理中
|
|||
|
|
sqlx::query("UPDATE download_tasks SET status = 1 WHERE id = ?")
|
|||
|
|
.bind(task_id)
|
|||
|
|
.execute(&pool)
|
|||
|
|
.await?;
|
|||
|
|
|
|||
|
|
// 获取查询参数
|
|||
|
|
let row: Option<(String, String)> = sqlx::query_as(
|
|||
|
|
"SELECT file_name, error_message FROM download_tasks WHERE id = ?"
|
|||
|
|
)
|
|||
|
|
.bind(task_id)
|
|||
|
|
.fetch_optional(&pool)
|
|||
|
|
.await?;
|
|||
|
|
|
|||
|
|
let (file_name, query_json) = row.ok_or("任务不存在")?;
|
|||
|
|
let params: Value = serde_json::from_str(&query_json).unwrap_or(json!({}));
|
|||
|
|
|
|||
|
|
let (where_clause, bind_values) = build_export_clause(
|
|||
|
|
params.get("cabinet_id").and_then(|v| v.as_i64()),
|
|||
|
|
params.get("project_id").and_then(|v| v.as_i64()),
|
|||
|
|
params.get("start_date").and_then(|v| v.as_str()),
|
|||
|
|
params.get("end_date").and_then(|v| v.as_str()),
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// 查询能耗明细
|
|||
|
|
let sql = format!(
|
|||
|
|
"SELECT es.stat_date, es.cabinet_id, c.name AS cabinet_name, \
|
|||
|
|
p.name AS project_name, \
|
|||
|
|
CAST(es.total_energy AS DOUBLE) AS total_energy, \
|
|||
|
|
es.charge_count \
|
|||
|
|
FROM energy_stats es \
|
|||
|
|
LEFT JOIN cabinets c ON es.cabinet_id = c.id \
|
|||
|
|
LEFT JOIN projects p ON c.project_id = p.id \
|
|||
|
|
{} ORDER BY es.stat_date DESC, es.cabinet_id ASC",
|
|||
|
|
where_clause
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
let mut query = sqlx::query_as::<_, EnergyDetailRow>(&sql);
|
|||
|
|
for val in &bind_values {
|
|||
|
|
query = query.bind(val);
|
|||
|
|
}
|
|||
|
|
let rows = query.fetch_all(&pool).await?;
|
|||
|
|
|
|||
|
|
// 更新进度 50%
|
|||
|
|
sqlx::query("UPDATE download_tasks SET progress = 50 WHERE id = ?")
|
|||
|
|
.bind(task_id)
|
|||
|
|
.execute(&pool)
|
|||
|
|
.await?;
|
|||
|
|
|
|||
|
|
// 生成 Excel
|
|||
|
|
let mut workbook = rust_xlsxwriter::Workbook::new();
|
|||
|
|
let sheet = workbook.add_worksheet();
|
|||
|
|
sheet.set_name("能耗统计")?;
|
|||
|
|
|
|||
|
|
let headers = [
|
|||
|
|
"日期", "柜子ID", "柜子名称", "所属项目", "能耗(kWh)", "充电次数",
|
|||
|
|
];
|
|||
|
|
for (col, header) in headers.iter().enumerate() {
|
|||
|
|
sheet.write_string(0, col as u16, *header)?;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for (row_idx, record) in rows.iter().enumerate() {
|
|||
|
|
let row = (row_idx + 1) as u32;
|
|||
|
|
sheet.write_string(row, 0, record.stat_date.to_string())?;
|
|||
|
|
sheet.write_number(row, 1, record.cabinet_id as f64)?;
|
|||
|
|
sheet.write_string(row, 2, record.cabinet_name.as_deref().unwrap_or("-"))?;
|
|||
|
|
sheet.write_string(row, 3, record.project_name.as_deref().unwrap_or("-"))?;
|
|||
|
|
sheet.write_number(row, 4, record.total_energy.unwrap_or(0.0))?;
|
|||
|
|
sheet.write_number(row, 5, record.charge_count.unwrap_or(0) as f64)?;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 保存文件
|
|||
|
|
let file_dir = "./exports";
|
|||
|
|
tokio::fs::create_dir_all(file_dir).await?;
|
|||
|
|
let file_path = format!("{}/{}", file_dir, file_name);
|
|||
|
|
workbook.save(&file_path)?;
|
|||
|
|
|
|||
|
|
// 更新任务状态为完成
|
|||
|
|
sqlx::query(
|
|||
|
|
"UPDATE download_tasks SET status = 2, progress = 100, file_path = ?, \
|
|||
|
|
error_message = NULL, completed_at = NOW() WHERE id = ?"
|
|||
|
|
)
|
|||
|
|
.bind(&file_path)
|
|||
|
|
.bind(task_id)
|
|||
|
|
.execute(&pool)
|
|||
|
|
.await?;
|
|||
|
|
|
|||
|
|
tracing::info!("能耗统计导出完成 task_id={} file={}", task_id, file_path);
|
|||
|
|
Ok(())
|
|||
|
|
}
|