Files
csm/crates/server/src/api/assets.rs
iven e99ea53eba feat: 全面重构前端UI及完善后端功能
前端重构:
- 重构Layout为左侧导航+顶栏的现代管理后台布局
- 重构设备管理页面(Devices.vue):左侧分组面板+右侧设备列表
- 重构设备详情(DeviceDetail.vue):集成硬件资产/软件资产/变更记录标签页
- 移除独立资产管理页面,功能合并至设备详情
- 重构Dashboard/登录/设置/告警/水印/上网管控等页面样式
- 新增全局CSS变量和统一样式系统
- 添加分组管理UI:新建/重命名/删除分组,移动设备到分组

后端完善:
- 新增分组CRUD API(groups.rs):创建/重命名/删除分组,设备分组移动
- 客户端硬件采集:完善GPU/主板/序列号/磁盘信息采集(Windows PowerShell)
- 客户端软件采集:通过Windows注册表读取已安装软件列表
- 新增SoftwareAssetReport消息类型(0x09)及处理链路
- 数据库新增upsert_software方法处理软件资产存储
- 服务端推送软件资产配置给新注册设备
- 修复密码修改功能,添加旧密码验证
2026-04-06 13:09:43 +08:00

147 lines
5.8 KiB
Rust

use axum::{extract::{State, Query}, Json};
use serde::Deserialize;
use sqlx::Row;
use crate::AppState;
use super::{ApiResponse, Pagination};
#[derive(Debug, Deserialize)]
pub struct AssetListParams {
pub device_uid: Option<String>,
pub search: Option<String>,
pub page: Option<u32>,
pub page_size: Option<u32>,
}
pub async fn list_hardware(
State(state): State<AppState>,
Query(params): Query<AssetListParams>,
) -> Json<ApiResponse<serde_json::Value>> {
let limit = params.page_size.unwrap_or(20).min(100);
let offset = params.page.unwrap_or(1).saturating_sub(1) * limit;
// Normalize empty strings to None
let device_uid = params.device_uid.as_deref().filter(|s| !s.is_empty()).map(String::from);
let search = params.search.as_deref().filter(|s| !s.is_empty()).map(String::from);
let rows = sqlx::query(
"SELECT id, device_uid, cpu_model, cpu_cores, memory_total_mb, disk_model, disk_total_mb,
gpu_model, motherboard, serial_number, reported_at
FROM hardware_assets WHERE 1=1
AND (? IS NULL OR device_uid = ?)
AND (? IS NULL OR cpu_model LIKE '%' || ? || '%' OR gpu_model LIKE '%' || ? || '%')
ORDER BY reported_at DESC LIMIT ? OFFSET ?"
)
.bind(&device_uid).bind(&device_uid)
.bind(&search).bind(&search).bind(&search)
.bind(limit).bind(offset)
.fetch_all(&state.db)
.await;
match rows {
Ok(records) => {
let items: Vec<serde_json::Value> = records.iter().map(|r| serde_json::json!({
"id": r.get::<i64, _>("id"),
"device_uid": r.get::<String, _>("device_uid"),
"cpu_model": r.get::<String, _>("cpu_model"),
"cpu_cores": r.get::<i32, _>("cpu_cores"),
"memory_total_mb": r.get::<i64, _>("memory_total_mb"),
"disk_model": r.get::<String, _>("disk_model"),
"disk_total_mb": r.get::<i64, _>("disk_total_mb"),
"gpu_model": r.get::<Option<String>, _>("gpu_model"),
"motherboard": r.get::<Option<String>, _>("motherboard"),
"serial_number": r.get::<Option<String>, _>("serial_number"),
"reported_at": r.get::<String, _>("reported_at"),
})).collect();
Json(ApiResponse::ok(serde_json::json!({
"hardware": items,
"page": params.page.unwrap_or(1),
"page_size": limit,
})))
}
Err(e) => Json(ApiResponse::internal_error("query hardware assets", e)),
}
}
pub async fn list_software(
State(state): State<AppState>,
Query(params): Query<AssetListParams>,
) -> Json<ApiResponse<serde_json::Value>> {
let limit = params.page_size.unwrap_or(20).min(100);
let offset = params.page.unwrap_or(1).saturating_sub(1) * limit;
// Normalize empty strings to None
let device_uid = params.device_uid.as_deref().filter(|s| !s.is_empty()).map(String::from);
let search = params.search.as_deref().filter(|s| !s.is_empty()).map(String::from);
let rows = sqlx::query(
"SELECT id, device_uid, name, version, publisher, install_date, install_path
FROM software_assets WHERE 1=1
AND (? IS NULL OR device_uid = ?)
AND (? IS NULL OR name LIKE '%' || ? || '%' OR publisher LIKE '%' || ? || '%')
ORDER BY name ASC LIMIT ? OFFSET ?"
)
.bind(&device_uid).bind(&device_uid)
.bind(&search).bind(&search).bind(&search)
.bind(limit).bind(offset)
.fetch_all(&state.db)
.await;
match rows {
Ok(records) => {
let items: Vec<serde_json::Value> = records.iter().map(|r| serde_json::json!({
"id": r.get::<i64, _>("id"),
"device_uid": r.get::<String, _>("device_uid"),
"name": r.get::<String, _>("name"),
"version": r.get::<Option<String>, _>("version"),
"publisher": r.get::<Option<String>, _>("publisher"),
"install_date": r.get::<Option<String>, _>("install_date"),
"install_path": r.get::<Option<String>, _>("install_path"),
})).collect();
Json(ApiResponse::ok(serde_json::json!({
"software": items,
"page": params.page.unwrap_or(1),
"page_size": limit,
})))
}
Err(e) => Json(ApiResponse::internal_error("query software assets", e)),
}
}
pub async fn list_changes(
State(state): State<AppState>,
Query(params): Query<AssetListParams>,
) -> Json<ApiResponse<serde_json::Value>> {
let limit = params.page_size.unwrap_or(20).min(100);
let offset = params.page.unwrap_or(1).saturating_sub(1) * limit;
let device_uid = params.device_uid.as_deref().filter(|s| !s.is_empty()).map(String::from);
let rows = sqlx::query(
"SELECT id, device_uid, change_type, change_detail, detected_at
FROM asset_changes WHERE 1=1
AND (? IS NULL OR device_uid = ?)
ORDER BY detected_at DESC LIMIT ? OFFSET ?"
)
.bind(&device_uid).bind(&device_uid)
.bind(limit).bind(offset)
.fetch_all(&state.db)
.await;
match rows {
Ok(records) => {
let items: Vec<serde_json::Value> = records.iter().map(|r| serde_json::json!({
"id": r.get::<i64, _>("id"),
"device_uid": r.get::<String, _>("device_uid"),
"change_type": r.get::<String, _>("change_type"),
"change_detail": r.get::<String, _>("change_detail"),
"detected_at": r.get::<String, _>("detected_at"),
})).collect();
Json(ApiResponse::ok(serde_json::json!({
"changes": items,
"page": params.page.unwrap_or(1),
"page_size": limit,
})))
}
Err(e) => Json(ApiResponse::internal_error("query asset changes", e)),
}
}