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:
caoqianming 2026-03-25 08:37:09 +08:00
parent 884f6ba5f3
commit 2732238be7
4 changed files with 61 additions and 1 deletions

View File

@ -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)]
pub struct CreateUnitReq {
#[validate(length(min = 1, max = 100))]

View File

@ -243,6 +243,10 @@ fn build_router(state: AppState) -> Router {
"/api/unit/{unit_id}/runtime",
get(handler::control::get_unit_runtime),
)
.route(
"/api/unit/{unit_id}/detail",
get(handler::control::get_unit_detail),
)
.route(
"/api/tag",
get(handler::tag::get_tag_list).post(handler::tag::create_tag),

View File

@ -86,7 +86,7 @@ pub struct Node {
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Serialize, Deserialize, FromRow)]
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
#[allow(dead_code)]
pub struct Point {
pub id: Uuid,

View File

@ -328,6 +328,21 @@ pub async fn get_equipment_by_unit_id(
.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(
pool: &PgPool,
equipment_id: Uuid,