refactor: 拆分组织树和设备列表为独立接口,支持按组织/项目筛选

This commit is contained in:
12451 2026-07-02 21:20:06 +08:00
parent e3894cb946
commit 2453f680ac
4 changed files with 101 additions and 68 deletions

View File

@ -1,10 +1,12 @@
//! 组织树 API
//!
//! 返回组织->项目->设备完整树状结构。
//! 根据用户角色进行数据隔离:总管理员看全部,企业管理员只看本组织。
//! 两个独立接口:
//! 1. /api/organization-tree — 只返回组织->项目树(不含设备)
//! 2. /api/cabinets/filter — 根据组织/项目筛选设备列表
use axum::extract::State;
use axum::extract::{State, Query};
use axum::Json;
use serde::Deserialize;
use serde_json::{json, Value};
use super::organizations::{CabinetRow, OrganizationRow, ProjectRow, TreeNode};
@ -12,10 +14,7 @@ use crate::error::AppError;
use crate::middleware::auth::{self, CurrentUser};
use crate::commands::AppState;
/// GET /api/organization-tree — 返回组织->项目->设备完整树
///
/// 需要 `device:view` 权限。
/// 数据隔离:总管理员看所有组织,企业管理员只看本组织下的数据。
/// 组织树接口 — 只返回组织->项目(不含设备)
pub async fn organization_tree(
user: CurrentUser,
State(state): State<AppState>,
@ -24,39 +23,28 @@ pub async fn organization_tree(
let db = &state.mysql;
// 根据角色过滤组织:总管理员看所有,企业管理员只看本组织
// 根据角色过滤组织
let orgs: Vec<OrganizationRow> = if user.role_level >= 2 {
sqlx::query_as(
r#"SELECT id, name FROM organizations ORDER BY id"#,
)
sqlx::query_as("SELECT id, name FROM organizations ORDER BY id")
.fetch_all(db)
.await?
} else if let Some(org_id) = user.organization_id {
sqlx::query_as(
r#"SELECT id, name FROM organizations WHERE id = ? ORDER BY id"#,
)
sqlx::query_as("SELECT id, name FROM organizations WHERE id = ? ORDER BY id")
.bind(org_id)
.fetch_all(db)
.await?
} else {
// 无组织关联的用户看不到任何数据
return Ok(Json(json!([])));
};
// 获取所有项目
let projects: Vec<ProjectRow> = sqlx::query_as(
r#"SELECT id, organization_id, name FROM projects ORDER BY id"#,
"SELECT id, organization_id, name FROM projects ORDER BY id"
)
.fetch_all(db)
.await?;
let cabinets: Vec<CabinetRow> = sqlx::query_as(
r#"SELECT id, project_id, abstract_id, imei, iccid, name, address, status
FROM cabinets ORDER BY id"#,
)
.fetch_all(db)
.await?;
// 构建树
// 构建树(只到项目节点)
let tree: Vec<TreeNode> = orgs
.into_iter()
.map(|org| {
@ -68,33 +56,13 @@ pub async fn organization_tree(
let children: Vec<TreeNode> = org_projects
.into_iter()
.map(|proj| {
let proj_cabinets: Vec<CabinetRow> = cabinets
.iter()
.filter(|c| c.project_id == Some(proj.id))
.cloned()
.collect();
let cab_nodes: Vec<TreeNode> = proj_cabinets
.into_iter()
.map(|c| TreeNode {
id: c.id,
name: c.name.unwrap_or_else(|| c.abstract_id.clone()),
children: None,
abstract_id: Some(c.abstract_id),
imei: Some(c.imei),
status: Some(c.status),
})
.collect();
TreeNode {
.map(|proj| TreeNode {
id: proj.id,
name: proj.name,
children: Some(cab_nodes),
children: None,
abstract_id: None,
imei: None,
status: None,
}
})
.collect();
@ -111,3 +79,66 @@ pub async fn organization_tree(
Ok(Json(json!(tree)))
}
/// 设备列表筛选参数
#[derive(Deserialize)]
pub struct CabinetFilterParams {
pub organization_id: Option<i64>,
pub project_id: Option<i64>,
}
/// 设备列表接口 — 根据组织/项目筛选
pub async fn filter_cabinets(
user: CurrentUser,
State(state): State<AppState>,
Query(params): Query<CabinetFilterParams>,
) -> Result<Json<Value>, AppError> {
auth::check_permission(&user, "device:view")?;
let db = &state.mysql;
let rows = if let Some(proj_id) = params.project_id {
// 按项目筛选
sqlx::query_as::<_, CabinetRow>(
"SELECT id, project_id, abstract_id, imei, iccid, name, address, status
FROM cabinets WHERE project_id = ? ORDER BY id"
)
.bind(proj_id)
.fetch_all(db)
.await?
} else if let Some(org_id) = params.organization_id {
// 按组织筛选(通过项目关联)
sqlx::query_as::<_, CabinetRow>(
"SELECT c.id, c.project_id, c.abstract_id, c.imei, c.iccid, c.name, c.address, c.status
FROM cabinets c
JOIN projects p ON c.project_id = p.id
WHERE p.organization_id = ? ORDER BY c.id"
)
.bind(org_id)
.fetch_all(db)
.await?
} else if user.role_level >= 2 {
// 总管理员看所有
sqlx::query_as::<_, CabinetRow>(
"SELECT id, project_id, abstract_id, imei, iccid, name, address, status
FROM cabinets ORDER BY id"
)
.fetch_all(db)
.await?
} else if let Some(org_id) = user.organization_id {
// 企业管理员只看本组织
sqlx::query_as::<_, CabinetRow>(
"SELECT c.id, c.project_id, c.abstract_id, c.imei, c.iccid, c.name, c.address, c.status
FROM cabinets c
JOIN projects p ON c.project_id = p.id
WHERE p.organization_id = ? ORDER BY c.id"
)
.bind(org_id)
.fetch_all(db)
.await?
} else {
vec![]
};
Ok(Json(json!(rows)))
}

View File

@ -63,6 +63,7 @@ pub fn build(state: AppState) -> Router {
.route("/api/cabinets/{id}/regenerate-auth", post(cabinets::regenerate_auth))
// 组织树
.route("/api/organization-tree", get(cabinet_tree::organization_tree))
.route("/api/cabinets/filter", get(cabinet_tree::filter_cabinets))
// 充电记录
.route("/api/charge-records", get(charge_records::list_charge_records))
.route("/api/charge-records/export", get(charge_records::export_charge_records))

View File

@ -89,4 +89,10 @@ export const cabinetApi = {
// 组织树 API
export const treeApi = {
get: () => api.get<TreeNode[]>('/organization-tree'),
filterCabinets: (params: { organization_id?: number; project_id?: number }) => {
const query = new URLSearchParams();
if (params.organization_id) query.set('organization_id', String(params.organization_id));
if (params.project_id) query.set('project_id', String(params.project_id));
return api.get<Cabinet[]>(`/cabinets/filter?${query.toString()}`);
},
};

View File

@ -29,7 +29,6 @@ import {
treeApi,
orgApi,
projectApi,
cabinetApi,
type TreeNode,
type Cabinet,
type Organization,
@ -37,8 +36,6 @@ import {
import {
buildTreeData,
collectCabinets,
findOrgInTree,
type NodeType,
type TreeInternalNode,
} from './OrganizationTree';
@ -48,7 +45,6 @@ import AddCabinetModal from './AddCabinetModal';
export default function Devices() {
const navigate = useNavigate();
const [treeData, setTreeData] = useState<TreeInternalNode[]>([]);
const [rawTree, setRawTree] = useState<TreeNode[]>([]);
const [cabinets, setCabinets] = useState<Cabinet[]>([]);
const [loading, setLoading] = useState(true);
const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
@ -79,7 +75,6 @@ export default function Devices() {
try {
const res = await treeApi.get();
const data = (res as unknown as TreeNode[]) || [];
setRawTree(data);
setTreeData(buildTreeData(data));
} catch {
Message.error('加载组织树失败');
@ -93,21 +88,21 @@ export default function Devices() {
}
setLoading(true);
try {
let res;
if (type === 'all') {
setCabinets(collectCabinets(rawTree));
res = await treeApi.filterCabinets({});
} else if (type === 'organization') {
const org = findOrgInTree(rawTree, id);
setCabinets(org?.children ? collectCabinets(org.children) : []);
res = await treeApi.filterCabinets({ organization_id: id });
} else if (type === 'project') {
const res = await cabinetApi.list(id);
setCabinets((res as unknown as Cabinet[]) || []);
res = await treeApi.filterCabinets({ project_id: id });
}
setCabinets((res as unknown as Cabinet[]) || []);
} catch {
Message.error('加载柜子列表失败');
} finally {
setLoading(false);
}
}, [rawTree, navigate]);
}, [navigate]);
useEffect(() => {
loadTree().then(() => setLoading(false));
@ -350,7 +345,7 @@ export default function Devices() {
</Button>
</div>
<div style={{ flex: 1, overflow: 'auto' }}>
<div style={{ flex: 1, overflow: 'auto', padding: '0 8px' }}>
<CabinetGrid cabinets={cabinets} loading={loading} />
</div>
</div>