feat(api): add GET /api/unit/{id}/detail with nested equipment and points
Returns unit with its equipments, each embedding their bound points. Uses 2 queries (equipment list + points via ANY) to avoid N+1. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
884f6ba5f3
commit
2732238be7
|
|
@ -114,6 +114,47 @@ pub async fn get_unit(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
pub struct EquipmentDetail {
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub equipment: crate::model::Equipment,
|
||||||
|
pub points: Vec<crate::model::Point>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
pub struct UnitDetail {
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub unit: crate::model::ControlUnit,
|
||||||
|
pub equipments: Vec<EquipmentDetail>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_unit_detail(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Path(unit_id): Path<Uuid>,
|
||||||
|
) -> Result<impl IntoResponse, ApiErr> {
|
||||||
|
let unit = crate::service::get_unit_by_id(&state.pool, unit_id)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| ApiErr::NotFound("Unit not found".to_string(), None))?;
|
||||||
|
|
||||||
|
let equipments = crate::service::get_equipment_by_unit_id(&state.pool, unit_id).await?;
|
||||||
|
let equipment_ids: Vec<Uuid> = equipments.iter().map(|e| e.id).collect();
|
||||||
|
let all_points = crate::service::get_points_by_equipment_ids(&state.pool, &equipment_ids).await?;
|
||||||
|
|
||||||
|
let equipments = equipments
|
||||||
|
.into_iter()
|
||||||
|
.map(|eq| {
|
||||||
|
let points = all_points
|
||||||
|
.iter()
|
||||||
|
.filter(|p| p.equipment_id == Some(eq.id))
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
EquipmentDetail { equipment: eq, points }
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(Json(UnitDetail { unit, equipments }))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Validate)]
|
#[derive(Debug, Deserialize, Validate)]
|
||||||
pub struct CreateUnitReq {
|
pub struct CreateUnitReq {
|
||||||
#[validate(length(min = 1, max = 100))]
|
#[validate(length(min = 1, max = 100))]
|
||||||
|
|
|
||||||
|
|
@ -243,6 +243,10 @@ fn build_router(state: AppState) -> Router {
|
||||||
"/api/unit/{unit_id}/runtime",
|
"/api/unit/{unit_id}/runtime",
|
||||||
get(handler::control::get_unit_runtime),
|
get(handler::control::get_unit_runtime),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/api/unit/{unit_id}/detail",
|
||||||
|
get(handler::control::get_unit_detail),
|
||||||
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/tag",
|
"/api/tag",
|
||||||
get(handler::tag::get_tag_list).post(handler::tag::create_tag),
|
get(handler::tag::get_tag_list).post(handler::tag::create_tag),
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ pub struct Node {
|
||||||
pub updated_at: DateTime<Utc>,
|
pub updated_at: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub struct Point {
|
pub struct Point {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
|
|
|
||||||
|
|
@ -328,6 +328,21 @@ pub async fn get_equipment_by_unit_id(
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_points_by_equipment_ids(
|
||||||
|
pool: &PgPool,
|
||||||
|
equipment_ids: &[Uuid],
|
||||||
|
) -> Result<Vec<crate::model::Point>, sqlx::Error> {
|
||||||
|
if equipment_ids.is_empty() {
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
sqlx::query_as::<_, crate::model::Point>(
|
||||||
|
r#"SELECT * FROM point WHERE equipment_id = ANY($1) ORDER BY equipment_id, created_at"#,
|
||||||
|
)
|
||||||
|
.bind(equipment_ids)
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_equipment_role_points(
|
pub async fn get_equipment_role_points(
|
||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
equipment_id: Uuid,
|
equipment_id: Uuid,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue