//! 设备管理 API — CRUD + 详情 + 级联下拉 //! //! 提供柜子的增删改查、详情查询、安全码重新生成, //! 以及柜子/仓板/仓体的级联下拉选项。 use axum::extract::{Path, Query, State}; use axum::Json; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use sqlx::MySqlPool; use super::organizations::{ generate_abstract_id, generate_auth_str, CabinetDetail, CabinetRow, CabinBoardRow, CabinBoardWithCompartments, CompartmentRow, CreateCabinetBody, UpdateCabinetBody, }; use crate::error::AppError; use crate::middleware::auth::{self, CurrentUser}; // --------------------------------------------------------------------------- // 项目访问权限验证 // --------------------------------------------------------------------------- /// 验证用户是否有权访问指定项目 /// /// 总管理员(role_level >= 2)可访问所有项目; /// 企业管理员只能访问本组织下的项目。 async fn check_project_access( user: &CurrentUser, pool: &MySqlPool, project_id: i64, ) -> Result<(), AppError> { // 总管理员可访问所有项目 if user.role_level >= 2 { return Ok(()); } if let Some(org_id) = user.organization_id { // 验证项目是否属于用户的组织 let project: Option<(i64,)> = sqlx::query_as( "SELECT id FROM projects WHERE id = ? AND organization_id = ?", ) .bind(project_id) .bind(org_id) .fetch_optional(pool) .await?; if project.is_none() { return Err(AppError::Forbidden("无权访问该项目".into())); } } else { return Err(AppError::Forbidden("无组织关联".into())); } Ok(()) } // --------------------------------------------------------------------------- // 设备 CRUD // --------------------------------------------------------------------------- /// GET /api/projects/:project_id/cabinets — 按项目查询柜子列表 pub async fn list_cabinets( user: CurrentUser, State(state): State, Path(project_id): Path, ) -> Result, AppError> { auth::check_permission(&user, "device:view")?; check_project_access(&user, &state.mysql, project_id).await?; let db = &state.mysql; let rows = sqlx::query_as::<_, CabinetRow>( r#"SELECT id, project_id, abstract_id, imei, name, status FROM cabinets WHERE project_id = ? ORDER BY id"#, ) .bind(project_id) .fetch_all(db) .await?; Ok(Json(json!(rows))) } /// POST /api/cabinets — 批量添加柜子 /// /// 每个 IMEI 创建一个柜子,同时生成默认仓控板和 6 个仓体。 pub async fn create_cabinet( user: CurrentUser, State(state): State, Json(body): Json, ) -> Result, AppError> { auth::check_permission(&user, "device:create")?; let db = &state.mysql; if body.imeis.is_empty() { return Err(AppError::BadRequest("IMEI 列表不能为空".into())); } let mut unique_imeis: Vec = Vec::new(); for imei in &body.imeis { let trimmed = imei.trim().to_string(); if trimmed.len() != 15 { return Err(AppError::BadRequest( format!("IMEI 必须为15位数字: {}", trimmed), )); } if !unique_imeis.contains(&trimmed) { unique_imeis.push(trimmed); } } let mut created_ids: Vec = Vec::new(); for imei in &unique_imeis { let abstract_id = generate_abstract_id(imei); let auth_str = generate_auth_str(); let mut tx = db.begin().await.map_err(AppError::Database)?; let result = sqlx::query( r#"INSERT INTO cabinets (project_id, abstract_id, imei, auth_str) VALUES (?, ?, ?, ?)"#, ) .bind(body.project_id) .bind(&abstract_id) .bind(imei) .bind(&auth_str) .execute(&mut *tx) .await?; let cabinet_id = result.last_insert_id() as i64; let board_result = sqlx::query( r#"INSERT INTO cabin_boards (cabinet_id, board_id) VALUES (?, 1)"#, ) .bind(cabinet_id) .execute(&mut *tx) .await?; let board_pk = board_result.last_insert_id() as i64; for ch in 1..=6 { sqlx::query( r#"INSERT INTO compartments (cabin_board_id, channel_id) VALUES (?, ?)"#, ) .bind(board_pk) .bind(ch) .execute(&mut *tx) .await?; } tx.commit().await.map_err(AppError::Database)?; created_ids.push(cabinet_id); tracing::info!("创建柜子 id={} abstract_id={} imei={}", cabinet_id, abstract_id, imei); } Ok(Json(json!({ "ids": created_ids }))) } /// PUT /api/cabinets/:id — 更新柜子(划拨项目等) pub async fn update_cabinet( user: CurrentUser, State(state): State, Path(id): Path, Json(body): Json, ) -> Result, AppError> { auth::check_permission(&user, "device:edit")?; let db = &state.mysql; let rows = sqlx::query( r#"UPDATE cabinets SET project_id = COALESCE(?, project_id), name = COALESCE(?, name) WHERE id = ?"#, ) .bind(body.project_id) .bind(&body.name) .bind(id) .execute(db) .await? .rows_affected(); if rows == 0 { return Err(AppError::NotFound(format!("柜子 {} 不存在", id))); } tracing::info!("更新柜子 id={}", id); Ok(Json(json!({}))) } /// DELETE /api/cabinets/:id — 删除柜子(级联删除仓控板和仓体) pub async fn delete_cabinet( user: CurrentUser, State(state): State, Path(id): Path, ) -> Result, AppError> { auth::check_permission(&user, "device:delete")?; let db = &state.mysql; let mut tx = db.begin().await.map_err(AppError::Database)?; sqlx::query( r#"DELETE compartments FROM compartments INNER JOIN cabin_boards ON compartments.cabin_board_id = cabin_boards.id WHERE cabin_boards.cabinet_id = ?"#, ) .bind(id) .execute(&mut *tx) .await?; sqlx::query(r#"DELETE FROM cabin_boards WHERE cabinet_id = ?"#) .bind(id) .execute(&mut *tx) .await?; let rows = sqlx::query(r#"DELETE FROM cabinets WHERE id = ?"#) .bind(id) .execute(&mut *tx) .await? .rows_affected(); tx.commit().await.map_err(AppError::Database)?; if rows == 0 { return Err(AppError::NotFound(format!("柜子 {} 不存在", id))); } tracing::info!("删除柜子 id={}", id); Ok(Json(json!({}))) } /// POST /api/cabinets/:id/regenerate-auth — 重新生成安全码 pub async fn regenerate_auth( user: CurrentUser, State(state): State, Path(id): Path, ) -> Result, AppError> { auth::check_permission(&user, "device:edit")?; let db = &state.mysql; let auth_str = generate_auth_str(); let rows = sqlx::query(r#"UPDATE cabinets SET auth_str = ? WHERE id = ?"#) .bind(&auth_str) .bind(id) .execute(db) .await? .rows_affected(); if rows == 0 { return Err(AppError::NotFound(format!("柜子 {} 不存在", id))); } tracing::info!("重新生成 auth_str id={}", id); Ok(Json(json!({ "auth_str": auth_str }))) } // --------------------------------------------------------------------------- // 柜子详情(含仓控板 + 仓体) // --------------------------------------------------------------------------- /// GET /api/cabinets/:id — 柜子详情,包含仓控板和仓体 pub async fn get_cabinet_detail( user: CurrentUser, State(state): State, Path(id): Path, ) -> Result, AppError> { auth::check_permission(&user, "device:view")?; let db = &state.mysql; let cabinet = sqlx::query_as::<_, CabinetRow>( r#"SELECT id, project_id, abstract_id, imei, name, status FROM cabinets WHERE id = ?"#, ) .bind(id) .fetch_optional(db) .await? .ok_or_else(|| AppError::NotFound(format!("柜子 {} 不存在", id)))?; // 验证用户是否有权访问该柜子所属的项目 if let Some(pid) = cabinet.project_id { check_project_access(&user, db, pid).await?; } let boards: Vec = sqlx::query_as( r#"SELECT id, cabinet_id, board_id, status FROM cabin_boards WHERE cabinet_id = ? ORDER BY board_id"#, ) .bind(id) .fetch_all(db) .await?; let mut cabin_boards = Vec::new(); for board in boards { let compartments: Vec = sqlx::query_as( r#"SELECT id, cabin_board_id, channel_id, status FROM compartments WHERE cabin_board_id = ? ORDER BY channel_id"#, ) .bind(board.id) .fetch_all(db) .await?; cabin_boards.push(CabinBoardWithCompartments { id: board.id, board_id: board.board_id, status: board.status, compartments, }); } Ok(Json(CabinetDetail { id: cabinet.id, project_id: cabinet.project_id, abstract_id: cabinet.abstract_id, imei: cabinet.imei, name: cabinet.name, status: cabinet.status, cabin_boards, })) } // --------------------------------------------------------------------------- // 级联下拉选项 // --------------------------------------------------------------------------- /// 柜子下拉查询参数 #[derive(Debug, Deserialize)] pub struct CabinetListQuery { pub project_id: Option, } /// 仓板下拉查询参数 #[derive(Debug, Deserialize)] pub struct CabinBoardListQuery { pub cabinet_id: i64, } /// 仓体下拉查询参数 #[derive(Debug, Deserialize)] pub struct CompartmentListQuery { pub cabin_board_id: i64, } /// 柜子下拉选项 #[derive(Debug, Serialize, sqlx::FromRow)] pub struct CabinetOption { pub id: i64, pub name: Option, pub abstract_id: String, } /// 仓板下拉选项 #[derive(Debug, Serialize, sqlx::FromRow)] pub struct CabinBoardOption { pub id: i64, pub board_id: i8, } /// 仓体下拉选项 #[derive(Debug, Serialize, sqlx::FromRow)] pub struct CompartmentOption { pub id: i64, pub channel_id: i8, } /// GET /api/cabinets/options — 柜子下拉列表 pub async fn list_cabinet_options( user: CurrentUser, State(state): State, Query(params): Query, ) -> Result, AppError> { auth::check_permission(&user, "device:view")?; let rows = if let Some(pid) = params.project_id { sqlx::query_as::<_, CabinetOption>( "SELECT id, name, abstract_id FROM cabinets WHERE project_id = ? ORDER BY id" ) .bind(pid) .fetch_all(&state.mysql) .await? } else { sqlx::query_as::<_, CabinetOption>( "SELECT id, name, abstract_id FROM cabinets ORDER BY id" ) .fetch_all(&state.mysql) .await? }; Ok(Json(json!({ "code": 0, "data": rows, "message": "ok" }))) } /// GET /api/cabin-boards/options — 仓板下拉列表 pub async fn list_cabin_board_options( user: CurrentUser, State(state): State, Query(params): Query, ) -> Result, AppError> { auth::check_permission(&user, "device:view")?; let rows = sqlx::query_as::<_, CabinBoardOption>( "SELECT id, board_id FROM cabin_boards WHERE cabinet_id = ? ORDER BY board_id" ) .bind(params.cabinet_id) .fetch_all(&state.mysql) .await?; Ok(Json(json!({ "code": 0, "data": rows, "message": "ok" }))) } /// GET /api/compartments/options — 仓体下拉列表 pub async fn list_compartment_options( user: CurrentUser, State(state): State, Query(params): Query, ) -> Result, AppError> { auth::check_permission(&user, "device:view")?; let rows = sqlx::query_as::<_, CompartmentOption>( "SELECT id, channel_id FROM compartments WHERE cabin_board_id = ? ORDER BY channel_id" ) .bind(params.cabin_board_id) .fetch_all(&state.mysql) .await?; Ok(Json(json!({ "code": 0, "data": rows, "message": "ok" }))) }