feat(unit): embed equipments with role_points in unit list and get responses

Unit list and single-unit endpoints now include per-unit equipment list
with signal-role points and monitor data, consistent with unit detail.
Uses batch queries to avoid N+1 DB calls.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
caoqianming 2026-03-26 10:26:45 +08:00
parent 5a481a5eb3
commit 0545388b85
2 changed files with 104 additions and 4 deletions

View File

@ -26,11 +26,19 @@ pub struct GetUnitListQuery {
pub pagination: PaginationParams, pub pagination: PaginationParams,
} }
#[derive(serde::Serialize)]
pub struct UnitEquipmentItem {
#[serde(flatten)]
pub equipment: crate::model::Equipment,
pub role_points: Vec<crate::handler::equipment::SignalRolePoint>,
}
#[derive(serde::Serialize)] #[derive(serde::Serialize)]
pub struct UnitWithRuntime { pub struct UnitWithRuntime {
#[serde(flatten)] #[serde(flatten)]
pub unit: crate::model::ControlUnit, pub unit: crate::model::ControlUnit,
pub runtime: Option<crate::control::runtime::UnitRuntime>, pub runtime: Option<crate::control::runtime::UnitRuntime>,
pub equipments: Vec<UnitEquipmentItem>,
} }
pub async fn get_unit_list( pub async fn get_unit_list(
@ -40,7 +48,7 @@ pub async fn get_unit_list(
query.validate()?; query.validate()?;
let total = crate::service::get_units_count(&state.pool, query.keyword.as_deref()).await?; let total = crate::service::get_units_count(&state.pool, query.keyword.as_deref()).await?;
let data = crate::service::get_units_paginated( let units = crate::service::get_units_paginated(
&state.pool, &state.pool,
query.keyword.as_deref(), query.keyword.as_deref(),
query.pagination.page_size, query.pagination.page_size,
@ -49,11 +57,54 @@ pub async fn get_unit_list(
.await?; .await?;
let all_runtimes = state.control_runtime.get_all().await; let all_runtimes = state.control_runtime.get_all().await;
let data = data
let unit_ids: Vec<Uuid> = units.iter().map(|u| u.id).collect();
let all_equipments =
crate::service::get_equipment_by_unit_ids(&state.pool, &unit_ids).await?;
let eq_ids: Vec<Uuid> = all_equipments.iter().map(|e| e.id).collect();
let role_point_rows =
crate::service::get_signal_role_points_batch(&state.pool, &eq_ids).await?;
let monitor_guard = state
.connection_manager
.get_point_monitor_data_read_guard()
.await;
let mut role_points_map: std::collections::HashMap<
Uuid,
Vec<crate::handler::equipment::SignalRolePoint>,
> = std::collections::HashMap::new();
for rp in role_point_rows {
role_points_map
.entry(rp.equipment_id)
.or_default()
.push(crate::handler::equipment::SignalRolePoint {
point_id: rp.point_id,
signal_role: rp.signal_role,
point_monitor: monitor_guard.get(&rp.point_id).cloned(),
});
}
drop(monitor_guard);
let mut equipments_by_unit: std::collections::HashMap<Uuid, Vec<UnitEquipmentItem>> =
std::collections::HashMap::new();
for eq in all_equipments {
let role_points = role_points_map.remove(&eq.id).unwrap_or_default();
if let Some(unit_id) = eq.unit_id {
equipments_by_unit
.entry(unit_id)
.or_default()
.push(UnitEquipmentItem { equipment: eq, role_points });
}
}
let data = units
.into_iter() .into_iter()
.map(|unit| { .map(|unit| {
let runtime = all_runtimes.get(&unit.id).cloned(); let runtime = all_runtimes.get(&unit.id).cloned();
UnitWithRuntime { unit, runtime } let equipments = equipments_by_unit.remove(&unit.id).unwrap_or_default();
UnitWithRuntime { unit, runtime, equipments }
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@ -138,7 +189,41 @@ pub async fn get_unit(
.await? .await?
.ok_or_else(|| ApiErr::NotFound("Unit not found".to_string(), None))?; .ok_or_else(|| ApiErr::NotFound("Unit not found".to_string(), None))?;
let runtime = state.control_runtime.get(unit_id).await; let runtime = state.control_runtime.get(unit_id).await;
Ok(Json(UnitWithRuntime { unit, runtime }))
let all_equipments =
crate::service::get_equipment_by_unit_id(&state.pool, unit_id).await?;
let eq_ids: Vec<Uuid> = all_equipments.iter().map(|e| e.id).collect();
let role_point_rows =
crate::service::get_signal_role_points_batch(&state.pool, &eq_ids).await?;
let monitor_guard = state
.connection_manager
.get_point_monitor_data_read_guard()
.await;
let mut role_points_map: std::collections::HashMap<
Uuid,
Vec<crate::handler::equipment::SignalRolePoint>,
> = std::collections::HashMap::new();
for rp in role_point_rows {
role_points_map
.entry(rp.equipment_id)
.or_default()
.push(crate::handler::equipment::SignalRolePoint {
point_id: rp.point_id,
signal_role: rp.signal_role,
point_monitor: monitor_guard.get(&rp.point_id).cloned(),
});
}
drop(monitor_guard);
let equipments = all_equipments
.into_iter()
.map(|eq| {
let role_points = role_points_map.remove(&eq.id).unwrap_or_default();
UnitEquipmentItem { equipment: eq, role_points }
})
.collect();
Ok(Json(UnitWithRuntime { unit, runtime, equipments }))
} }
#[derive(serde::Serialize)] #[derive(serde::Serialize)]

View File

@ -316,6 +316,21 @@ pub async fn get_all_enabled_units(pool: &PgPool) -> Result<Vec<ControlUnit>, sq
.await .await
} }
pub async fn get_equipment_by_unit_ids(
pool: &PgPool,
unit_ids: &[Uuid],
) -> Result<Vec<crate::model::Equipment>, sqlx::Error> {
if unit_ids.is_empty() {
return Ok(vec![]);
}
sqlx::query_as::<_, crate::model::Equipment>(
r#"SELECT * FROM equipment WHERE unit_id = ANY($1) ORDER BY unit_id, created_at"#,
)
.bind(unit_ids)
.fetch_all(pool)
.await
}
pub async fn get_equipment_by_unit_id( pub async fn get_equipment_by_unit_id(
pool: &PgPool, pool: &PgPool,
unit_id: Uuid, unit_id: Uuid,