2026-07-02 05:38:01 +08:00
//! 组织树 API
//!
2026-07-02 21:20:06 +08:00
//! 两个独立接口:
2026-07-02 21:32:06 +08:00
//! 1. /api/organization-tree — 返回多级组织->项目树
2026-07-02 21:20:06 +08:00
//! 2. /api/cabinets/filter — 根据组织/项目筛选设备列表
2026-07-02 05:38:01 +08:00
2026-07-02 21:20:06 +08:00
use axum ::extract ::{ State , Query } ;
2026-07-02 05:38:01 +08:00
use axum ::Json ;
2026-07-02 21:20:06 +08:00
use serde ::Deserialize ;
2026-07-02 05:38:01 +08:00
use serde_json ::{ json , Value } ;
2026-09-20 00:01:01 +08:00
use std ::collections ::HashMap ;
2026-07-02 05:38:01 +08:00
use super ::organizations ::{ CabinetRow , OrganizationRow , ProjectRow , TreeNode } ;
use crate ::error ::AppError ;
use crate ::middleware ::auth ::{ self , CurrentUser } ;
use crate ::commands ::AppState ;
2026-07-03 02:45:00 +08:00
/// SQL查询专用结构体( 只包含数据库字段)
#[ derive(Debug, Clone, sqlx::FromRow) ]
struct CabinetSqlRow {
id : i64 ,
project_id : Option < i64 > ,
abstract_id : Option < String > ,
imei : String ,
iccid : Option < String > ,
name : Option < String > ,
address : Option < String > ,
2026-09-19 23:40:46 +08:00
project_address : Option < String > ,
2026-07-03 02:45:00 +08:00
status : i8 ,
board_count : Option < i64 > ,
channel_count : Option < i64 > ,
idle_count : Option < i64 > ,
charging_count : Option < i64 > ,
full_count : Option < i64 > ,
fault_count : Option < i64 > ,
}
impl From < CabinetSqlRow > for CabinetRow {
fn from ( row : CabinetSqlRow ) -> Self {
CabinetRow {
id : row . id ,
project_id : row . project_id ,
abstract_id : row . abstract_id ,
imei : row . imei ,
iccid : row . iccid ,
name : row . name ,
address : row . address ,
2026-09-19 23:40:46 +08:00
project_address : row . project_address ,
2026-07-03 02:45:00 +08:00
status : row . status ,
board_count : row . board_count ,
channel_count : row . channel_count ,
idle_count : row . idle_count ,
charging_count : row . charging_count ,
full_count : row . full_count ,
fault_count : row . fault_count ,
rssi : None ,
pow_fail_dc : None ,
pow_fail_ac : None ,
is_online : None ,
2026-09-19 23:40:46 +08:00
org_disabled : None ,
2026-07-03 02:45:00 +08:00
}
}
}
2026-09-20 00:01:01 +08:00
// ---------------------------------------------------------------------------
// SQL 常量
//
// 说明:以下 SQL 均由原函数体**原样搬移**,未改动任何 SELECT 字段列表、
// 子查询、JOIN 条件与 WHERE 条件(含 CAST(... AS CHAR) 与 DBX 占位符 `?`)。
// ---------------------------------------------------------------------------
/// 全部组织(组织树用)
const ORGS_ALL_SQL : & str = " SELECT id, name, parent_id, disabled FROM organizations ORDER BY id " ;
/// 全部项目(经纬度转 CHAR, 规避 sqlx 的 DECIMAL 解码问题)
const PROJECTS_ALL_SQL : & str = " SELECT id, organization_id, name, address, CAST(latitude AS CHAR) AS latitude, CAST(longitude AS CHAR) AS longitude FROM projects ORDER BY id " ;
/// 各项目下的设备数量
const PROJECT_CABINET_COUNTS_SQL : & str =
" SELECT project_id, COUNT(*) as cnt FROM cabinets GROUP BY project_id " ;
/// 设备列表基础 SQL: 联表查询获取通道状态统计( 子查询 idle_count/charging_count/... 原样保留)
const CABINET_FILTER_BASE_SQL : & str = "
SELECT
c . id , c . project_id , c . abstract_id , c . imei , c . iccid , c . name , c . address ,
( SELECT address FROM projects WHERE id = c . project_id ) as project_address ,
c . status ,
( SELECT COUNT ( * ) FROM cabin_boards cb WHERE cb . cabinet_id = c . id ) as board_count ,
( SELECT COUNT ( * ) FROM cabin_boards cb2
JOIN compartments comp ON comp . cabin_board_id = cb2 . id
WHERE cb2 . cabinet_id = c . id ) as channel_count ,
( SELECT COUNT ( * ) FROM cabin_boards cb3
JOIN compartments comp2 ON comp2 . cabin_board_id = cb3 . id
WHERE cb3 . cabinet_id = c . id AND comp2 . status = 0 ) as idle_count ,
( SELECT COUNT ( * ) FROM cabin_boards cb4
JOIN compartments comp3 ON comp3 . cabin_board_id = cb4 . id
WHERE cb4 . cabinet_id = c . id AND comp3 . status = 1 ) as charging_count ,
( SELECT COUNT ( * ) FROM cabin_boards cb5
JOIN compartments comp4 ON comp4 . cabin_board_id = cb5 . id
WHERE cb5 . cabinet_id = c . id AND comp4 . status = 2 ) as full_count ,
( SELECT COUNT ( * ) FROM cabin_boards cb6
JOIN compartments comp5 ON comp5 . cabin_board_id = cb6 . id
WHERE cb6 . cabinet_id = c . id AND comp5 . status = 3 ) as fault_count
FROM cabinets c
" ;
/// 设备归属校验(非总管理员):设备必须属于当前用户组织
const DEVICE_ACCESS_SQL : & str = " SELECT EXISTS(SELECT 1 FROM cabinets c \
JOIN projects p ON c . project_id = p . id \
WHERE c . imei = ? AND p . organization_id = ? ) " ;
// ---------------------------------------------------------------------------
// 组织树
// ---------------------------------------------------------------------------
2026-07-02 21:42:52 +08:00
/// 组织树接口 — 返回多级组织->项目树(含设备数量)
2026-07-02 05:38:01 +08:00
pub async fn organization_tree (
user : CurrentUser ,
State ( state ) : State < AppState > ,
) -> Result < Json < Value > , AppError > {
auth ::check_permission ( & user , " device:view " ) ? ;
let db = & state . mysql ;
2026-07-02 21:42:52 +08:00
// 获取所有组织
2026-09-20 00:01:01 +08:00
let orgs : Vec < OrganizationRow > = sqlx ::query_as ( ORGS_ALL_SQL ) . fetch_all ( db ) . await ? ;
2026-07-02 05:38:01 +08:00
2026-07-02 21:20:06 +08:00
// 获取所有项目
2026-09-20 00:01:01 +08:00
let projects : Vec < ProjectRow > = sqlx ::query_as ( PROJECTS_ALL_SQL ) . fetch_all ( db ) . await ? ;
2026-07-02 05:38:01 +08:00
2026-07-02 21:42:52 +08:00
// 获取每个项目的设备数量
2026-09-20 00:01:01 +08:00
let project_count_map = fetch_project_counts ( db ) . await ? ;
2026-07-02 21:42:52 +08:00
2026-07-02 21:32:06 +08:00
// 递归构建树
2026-07-02 21:42:52 +08:00
let tree = build_tree ( & orgs , & projects , & project_count_map , None ) ;
2026-07-02 05:38:01 +08:00
2026-09-19 23:40:46 +08:00
Ok ( Json ( json! ( { " code " : 0 , " data " : tree , " message " : " ok " } ) ) )
2026-07-02 05:38:01 +08:00
}
2026-07-02 21:20:06 +08:00
2026-09-20 00:01:01 +08:00
/// 查询「项目ID -> 设备数量」映射
async fn fetch_project_counts ( db : & sqlx ::MySqlPool ) -> Result < HashMap < i64 , i64 > , AppError > {
let project_counts : Vec < ( i64 , i64 ) > = sqlx ::query_as ( PROJECT_CABINET_COUNTS_SQL )
. fetch_all ( db )
. await ? ;
Ok ( project_counts . into_iter ( ) . collect ( ) )
}
/// 递归构建「组织 -> 项目」树节点,项目名称携带设备数量
fn build_tree (
orgs : & [ OrganizationRow ] ,
projects : & [ ProjectRow ] ,
project_count_map : & HashMap < i64 , i64 > ,
parent_id : Option < i64 > ,
) -> Vec < TreeNode > {
orgs . iter ( )
. filter ( | o | o . parent_id = = parent_id )
. map ( | org | {
// 递归获取子组织
let children = build_tree ( orgs , projects , project_count_map , Some ( org . id ) ) ;
// 获取该组织下的项目
let org_projects : Vec < TreeNode > = projects
. iter ( )
. filter ( | p | p . organization_id = = Some ( org . id ) )
. map ( | p | {
let count = project_count_map . get ( & p . id ) . copied ( ) . unwrap_or ( 0 ) ;
TreeNode {
id : p . id ,
name : format ! ( " {} ({}) " , p . name , count ) ,
children : None ,
abstract_id : None ,
imei : None ,
status : None ,
disabled : None ,
address : p . address . clone ( ) ,
latitude : p . latitude . clone ( ) ,
longitude : p . longitude . clone ( ) ,
}
} )
. collect ( ) ;
// 合并子组织和项目
let mut all_children = children ;
all_children . extend ( org_projects ) ;
// 计算本组织下的总设备数量
let total_count : i64 = all_children
. iter ( )
. map ( | c | {
// 从项目名称中提取数量
if let Some ( start ) = c . name . rfind ( '(' ) {
if let Some ( end ) = c . name . rfind ( ')' ) {
c . name [ start + 1 .. end ] . parse ( ) . unwrap_or ( 0 )
} else {
0
}
} else {
0
}
} )
. sum ( ) ;
TreeNode {
id : org . id ,
name : format ! ( " {} ({}) " , org . name , total_count ) ,
children : if all_children . is_empty ( ) {
None
} else {
Some ( all_children )
} ,
abstract_id : None ,
imei : None ,
status : None ,
disabled : org . disabled ,
address : None ,
latitude : None ,
longitude : None ,
}
} )
. collect ( )
}
// ---------------------------------------------------------------------------
// 设备列表筛选
// ---------------------------------------------------------------------------
2026-07-02 21:20:06 +08:00
/// 设备列表筛选参数
#[ derive(Deserialize) ]
pub struct CabinetFilterParams {
pub organization_id : Option < i64 > ,
pub project_id : Option < i64 > ,
2026-07-03 02:45:00 +08:00
pub page : Option < i64 > ,
pub page_size : Option < i64 > ,
pub bound : Option < String > ,
2026-07-02 21:20:06 +08:00
}
/// 设备列表接口 — 根据组织/项目筛选
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 ;
2026-07-03 02:45:00 +08:00
let page = params . page . unwrap_or ( 1 ) . max ( 1 ) ;
let page_size = params . page_size . unwrap_or ( 12 ) . clamp ( 1 , 100 ) ;
let offset = ( page - 1 ) * page_size ;
2026-07-02 21:20:06 +08:00
2026-09-20 00:01:01 +08:00
// 组织/项目过滤 + 已绑定/未绑定过滤
let ( where_clause , binds ) = build_filter_where ( & params , & user ) ;
2026-07-02 22:09:19 +08:00
2026-09-20 00:01:01 +08:00
// 总数量
let total = fetch_filter_total ( db , & where_clause , & binds ) . await ;
if total = = 0 | | offset > = total {
return Ok ( Json ( json! ( { " code " : 0 , " data " : { " total " : 0 , " data " : [ ] } , " message " : " ok " } ) ) ) ;
}
// 分页数据
let sql_rows = fetch_filter_page ( db , & where_clause , & binds , page_size , offset ) . await ? ;
// 转换为CabinetRow
let mut rows : Vec < CabinetRow > = sql_rows . into_iter ( ) . map ( CabinetRow ::from ) . collect ( ) ;
// 从Redis获取实时状态( Pipeline批量HGET, 1次网络往返)
attach_realtime_status ( & state , & mut rows ) . await ? ;
// 查询柜子所属组织是否禁用(甲方禁用客户组织 → 前端显示徽标)
attach_org_disabled ( db , & mut rows ) . await ;
Ok ( Json ( json! ( { " code " : 0 , " data " : { " total " : total , " data " : rows } , " message " : " ok " } ) ) )
}
/// 构造设备列表的 WHERE/JOIN 片段及其绑定参数(分支顺序与语义与原实现一致)
fn build_filter_where (
params : & CabinetFilterParams ,
user : & CurrentUser ,
) -> ( String , Vec < String > ) {
let ( where_clause , binds ) : ( String , Vec < String > ) = if let Some ( proj_id ) = params . project_id {
2026-07-03 02:45:00 +08:00
( " WHERE c.project_id = ? " . into ( ) , vec! [ proj_id . to_string ( ) ] )
2026-07-02 21:20:06 +08:00
} else if let Some ( org_id ) = params . organization_id {
2026-07-03 02:45:00 +08:00
( " JOIN projects p ON c.project_id = p.id
WHERE p . organization_id IN (
SELECT id FROM organizations WHERE id = ? OR parent_id = ?
) " .into(), vec![org_id.to_string(), org_id.to_string()])
2026-07-02 21:20:06 +08:00
} else if user . role_level > = 2 {
2026-07-03 02:45:00 +08:00
( String ::new ( ) , vec! [ ] )
2026-07-02 21:20:06 +08:00
} else if let Some ( org_id ) = user . organization_id {
2026-07-03 02:45:00 +08:00
( " JOIN projects p ON c.project_id = p.id
WHERE p . organization_id = ? " .into(), vec![org_id.to_string()])
2026-07-02 21:20:06 +08:00
} else {
2026-07-03 02:45:00 +08:00
( " WHERE 1=0 " . into ( ) , vec! [ ] )
2026-07-02 21:20:06 +08:00
} ;
2026-09-19 23:40:46 +08:00
// 已绑定/未绑定过滤(绑定 = 分配到项目)
2026-09-20 00:01:01 +08:00
let where_clause = match params . bound . as_deref ( ) {
Some ( " 1 " ) = > append_bound_filter ( where_clause , " WHERE c.project_id IS NOT NULL " , " AND c.project_id IS NOT NULL " ) ,
Some ( " 0 " ) = > append_bound_filter ( where_clause , " WHERE c.project_id IS NULL " , " AND c.project_id IS NULL " ) ,
_ = > where_clause ,
} ;
( where_clause , binds )
}
/// 追加「已绑定/未绑定」条件:片段为空时用 `where_new`,否则用 `and_tail` 续接
fn append_bound_filter ( where_clause : String , where_new : & str , and_tail : & str ) -> String {
if where_clause . is_empty ( ) {
where_new . to_string ( )
} else {
let mut clause = where_clause ;
clause . push_str ( and_tail ) ;
clause
2026-07-03 02:45:00 +08:00
}
2026-09-20 00:01:01 +08:00
}
2026-07-03 02:45:00 +08:00
2026-09-20 00:01:01 +08:00
/// 统计符合条件的设备总数(查询失败按 0 处理,与原实现一致)
async fn fetch_filter_total ( db : & sqlx ::MySqlPool , where_clause : & str , binds : & [ String ] ) -> i64 {
2026-07-03 02:45:00 +08:00
let count_sql = format! ( " SELECT COUNT(*) FROM cabinets c {} " , where_clause ) ;
let mut count_query = sqlx ::query_scalar ::< _ , i64 > ( & count_sql ) ;
2026-09-20 00:01:01 +08:00
for b in binds {
2026-07-03 02:45:00 +08:00
count_query = count_query . bind ( b ) ;
}
2026-09-20 00:01:01 +08:00
count_query . fetch_one ( db ) . await . unwrap_or ( 0 )
}
2026-07-03 02:45:00 +08:00
2026-09-20 00:01:01 +08:00
/// 查询当前页设备行(分页参数由 WHERE 之后的 LIMIT ? OFFSET ? 绑定)
async fn fetch_filter_page (
db : & sqlx ::MySqlPool ,
where_clause : & str ,
binds : & [ String ] ,
page_size : i64 ,
offset : i64 ,
) -> Result < Vec < CabinetSqlRow > , AppError > {
let data_sql = format! ( " {} {} ORDER BY c.id LIMIT ? OFFSET ? " , CABINET_FILTER_BASE_SQL , where_clause ) ;
2026-07-03 02:45:00 +08:00
let mut data_query = sqlx ::query_as ::< _ , CabinetSqlRow > ( & data_sql ) ;
2026-09-20 00:01:01 +08:00
for b in binds {
2026-07-03 02:45:00 +08:00
data_query = data_query . bind ( b ) ;
}
2026-09-20 00:01:01 +08:00
let rows : Vec < CabinetSqlRow > = data_query
2026-07-03 02:45:00 +08:00
. bind ( page_size )
. bind ( offset )
2026-09-20 00:01:01 +08:00
. fetch_all ( db )
. await ? ;
Ok ( rows )
}
2026-07-03 02:45:00 +08:00
2026-09-20 00:01:01 +08:00
/// 从 Redis 批量补齐实时状态( Pipeline HGET, 1 次网络往返)
async fn attach_realtime_status ( state : & AppState , rows : & mut [ CabinetRow ] ) -> Result < ( ) , AppError > {
// 使用多路复用连接( MultiplexedConnection) , 替代已废弃的 aio::Connection
let mut conn = open_redis_conn ( state ) . await ? ;
// 用Pipeline一次发所有HGET命令( 每个设备1次HGET取4个字段)
let mut pipe = redis ::pipe ( ) ;
for row in rows . iter ( ) {
let key = format! ( " device: {} " , row . imei ) ;
pipe . cmd ( " HGET " ) . arg ( & key ) . arg ( " online " ) ;
pipe . cmd ( " HGET " ) . arg ( & key ) . arg ( " rssi " ) ;
pipe . cmd ( " HGET " ) . arg ( & key ) . arg ( " pow_fail_dc " ) ;
pipe . cmd ( " HGET " ) . arg ( & key ) . arg ( " pow_fail_ac " ) ;
}
2026-07-03 02:45:00 +08:00
2026-09-20 00:01:01 +08:00
// 一次发送,获取所有结果
let results : Vec < Option < String > > = pipe . query_async ( & mut conn ) . await . unwrap_or_default ( ) ;
// 每4个一组解析
for ( i , row ) in rows . iter_mut ( ) . enumerate ( ) {
let base = i * 4 ;
row . is_online = results . get ( base ) . and_then ( | v | v . as_ref ( ) ) . map ( | v | v = = " 1 " ) ;
row . rssi = results
. get ( base + 1 )
. and_then ( | v | v . as_ref ( ) )
. and_then ( | v | v . parse ::< i32 > ( ) . ok ( ) ) ;
row . pow_fail_dc = results
. get ( base + 2 )
. and_then ( | v | v . as_ref ( ) )
. map ( | v | v = = " 1 " ) ;
row . pow_fail_ac = results
. get ( base + 3 )
. and_then ( | v | v . as_ref ( ) )
. map ( | v | v = = " 1 " ) ;
2026-07-03 02:45:00 +08:00
}
2026-09-20 00:01:01 +08:00
Ok ( ( ) )
}
/// 标记柜子所属组织是否被禁用(甲方禁用客户组织 → 前端显示徽标)
async fn attach_org_disabled ( db : & sqlx ::MySqlPool , rows : & mut [ CabinetRow ] ) {
let project_ids : Vec < i64 > = rows . iter ( ) . filter_map ( | r | r . project_id ) . collect ( ) ;
if project_ids . is_empty ( ) {
return ;
}
let placeholders : Vec < String > = project_ids . iter ( ) . map ( | _ | " ? " . to_string ( ) ) . collect ( ) ;
let sql = format! (
r #" SELECT p.id, o.disabled FROM projects p
2026-09-19 23:40:46 +08:00
LEFT JOIN organizations o ON p . organization_id = o . id
WHERE p . id IN ( { } ) " #,
2026-09-20 00:01:01 +08:00
placeholders . join ( " , " )
) ;
let mut q = sqlx ::query_as ::< _ , ( i64 , i8 ) > ( & sql ) ;
for pid in & project_ids {
q = q . bind ( pid ) ;
}
if let Ok ( results ) = q . fetch_all ( db ) . await {
let disabled_map : HashMap < i64 , bool > =
results . into_iter ( ) . map ( | ( pid , d ) | ( pid , d = = 1 ) ) . collect ( ) ;
for row in rows . iter_mut ( ) {
row . org_disabled = row . project_id . and_then ( | pid | disabled_map . get ( & pid ) ) . copied ( ) ;
2026-09-19 23:40:46 +08:00
}
}
2026-07-03 02:45:00 +08:00
}
2026-09-20 00:01:01 +08:00
// ---------------------------------------------------------------------------
// 设备实时状态
// ---------------------------------------------------------------------------
2026-07-03 02:45:00 +08:00
/// 设备实时状态参数
#[ derive(Deserialize) ]
pub struct DeviceStatusParams {
pub imei : String ,
}
2026-09-20 00:01:01 +08:00
/// 打开 Redis 多路复用连接(失败按原错误信息返回内部错误)
async fn open_redis_conn ( state : & AppState ) -> Result < redis ::aio ::MultiplexedConnection , AppError > {
let client = redis ::Client ::open ( state . redis_url . as_str ( ) ) . map_err ( | e | {
AppError ::Internal ( format! ( " Redis客户端创建失败: {} " , e ) )
} ) ? ;
// 使用多路复用连接( MultiplexedConnection) , 替代已废弃的 aio::Connection
client . get_multiplexed_async_connection ( ) . await . map_err ( | e | {
AppError ::Internal ( format! ( " Redis连接失败: {} " , e ) )
} )
}
2026-07-03 02:45:00 +08:00
/// 设备实时状态接口 — 返回板级+通道级状态
pub async fn device_realtime_status (
user : CurrentUser ,
State ( state ) : State < AppState > ,
Query ( params ) : Query < DeviceStatusParams > ,
) -> Result < Json < Value > , AppError > {
auth ::check_permission ( & user , " device:view " ) ? ;
2026-09-19 23:40:46 +08:00
// 校验设备归属
2026-09-20 00:01:01 +08:00
ensure_device_access ( & state , & user , & params . imei ) . await ? ;
let mut conn = open_redis_conn ( & state ) . await ? ;
let device_status = load_device_status ( & mut conn , & params . imei ) . await ;
let boards = load_boards_status ( & mut conn , & params . imei ) . await ;
Ok ( Json ( json! ( {
" code " : 0 ,
" data " : {
" device " : device_status ,
" boards " : boards ,
} ,
" message " : " ok "
} ) ) )
}
/// 设备归属校验:非总管理员必须能通过「设备 → 项目 → 组织」链路命中本组织
async fn ensure_device_access (
state : & AppState ,
user : & CurrentUser ,
imei : & str ,
) -> Result < ( ) , AppError > {
2026-09-19 23:40:46 +08:00
if user . role_level < 2 {
if let Some ( org_id ) = user . organization_id {
2026-09-20 00:01:01 +08:00
let has_access : bool = sqlx ::query_scalar ( DEVICE_ACCESS_SQL )
. bind ( imei )
. bind ( org_id )
. fetch_one ( & state . mysql )
. await
. unwrap_or ( false ) ;
2026-09-19 23:40:46 +08:00
if ! has_access {
return Err ( AppError ::Forbidden ( " 无权访问该设备 " . into ( ) ) ) ;
}
} else {
return Err ( AppError ::Forbidden ( " 无权访问该设备 " . into ( ) ) ) ;
}
}
2026-09-20 00:01:01 +08:00
Ok ( ( ) )
}
2026-09-19 23:40:46 +08:00
2026-09-20 00:01:01 +08:00
/// 读取设备级字段( online / rssi / pow_fail_dc / pow_fail_ac)
async fn load_device_status (
conn : & mut redis ::aio ::MultiplexedConnection ,
imei : & str ,
) -> Value {
let device_key = format! ( " device: {} " , imei ) ;
2026-07-03 02:45:00 +08:00
// 获取设备级字段
let fields = vec! [ " online " , " rssi " , " pow_fail_dc " , " pow_fail_ac " ] ;
let values : Vec < Option < String > > = redis ::cmd ( " HMGET " )
. arg ( & device_key )
. arg ( & fields )
2026-09-20 00:01:01 +08:00
. query_async ( & mut * conn )
2026-07-03 02:45:00 +08:00
. await
. unwrap_or_default ( ) ;
2026-09-20 00:01:01 +08:00
json! ( {
2026-07-03 02:45:00 +08:00
" online " : values . get ( 0 ) . and_then ( | v | v . as_ref ( ) ) . map ( | v | v = = " 1 " ) . unwrap_or ( false ) ,
" rssi " : values . get ( 1 ) . and_then ( | v | v . as_ref ( ) ) . and_then ( | v | v . parse ::< i32 > ( ) . ok ( ) ) ,
" pow_fail_dc " : values . get ( 2 ) . and_then ( | v | v . as_ref ( ) ) . map ( | v | v = = " 1 " ) . unwrap_or ( false ) ,
" pow_fail_ac " : values . get ( 3 ) . and_then ( | v | v . as_ref ( ) ) . map ( | v | v = = " 1 " ) . unwrap_or ( false ) ,
2026-09-20 00:01:01 +08:00
} )
}
2026-07-03 02:45:00 +08:00
2026-09-20 00:01:01 +08:00
/// 扫描并组装全部仓控板状态(板级 + 通道级)
async fn load_boards_status (
conn : & mut redis ::aio ::MultiplexedConnection ,
imei : & str ,
) -> Value {
2026-07-03 02:45:00 +08:00
// 扫描板级key: device:{imei}:board:*
2026-09-20 00:01:01 +08:00
let board_pattern = format! ( " device: {} :board:* " , imei ) ;
2026-07-03 02:45:00 +08:00
let board_keys : Vec < String > = redis ::cmd ( " KEYS " )
. arg ( & board_pattern )
2026-09-20 00:01:01 +08:00
. query_async ( & mut * conn )
2026-07-03 02:45:00 +08:00
. await
. unwrap_or_default ( ) ;
let mut boards = json! ( { } ) ;
for board_key in & board_keys {
// 提取board_idx
let board_idx = board_key . split ( " : " ) . last ( ) . unwrap_or ( " " ) ;
if board_idx . is_empty ( ) {
continue ;
}
2026-09-20 00:01:01 +08:00
boards [ board_idx ] = load_single_board ( conn , board_key ) . await ;
}
2026-07-03 02:45:00 +08:00
2026-09-20 00:01:01 +08:00
boards
}
/// 读取单块仓控板的板级字段,并扫描其下的通道级 key
async fn load_single_board (
conn : & mut redis ::aio ::MultiplexedConnection ,
board_key : & str ,
) -> Value {
// 获取板级字段
let board_fields = vec! [ " status_hex " ] ;
let board_values : Vec < Option < String > > = redis ::cmd ( " HMGET " )
. arg ( board_key )
. arg ( & board_fields )
. query_async ( & mut * conn )
. await
. unwrap_or_default ( ) ;
let mut board_data = json! ( {
" status_hex " : board_values . get ( 0 ) . and_then ( | v | v . as_ref ( ) ) . cloned ( ) . unwrap_or_default ( ) ,
} ) ;
// 扫描通道级key: device:{imei}:board:{idx}:ch:*
let ch_pattern = format! ( " {} :ch:* " , board_key ) ;
let ch_keys : Vec < String > = redis ::cmd ( " KEYS " )
. arg ( & ch_pattern )
. query_async ( & mut * conn )
. await
. unwrap_or_default ( ) ;
let mut channels = json! ( { } ) ;
for ch_key in & ch_keys {
let ch_idx = ch_key . split ( " : " ) . last ( ) . unwrap_or ( " " ) ;
if ch_idx . is_empty ( ) {
continue ;
2026-07-03 02:45:00 +08:00
}
2026-09-20 00:01:01 +08:00
channels [ ch_idx ] = load_channel_status ( conn , ch_key ) . await ;
2026-07-03 02:45:00 +08:00
}
2026-09-20 00:01:01 +08:00
board_data [ " channels " ] = channels ;
board_data
}
/// 读取单个通道状态( 状态映射优先级: fault > full > on > idle)
async fn load_channel_status (
conn : & mut redis ::aio ::MultiplexedConnection ,
ch_key : & str ,
) -> Value {
let ch_fields = vec! [ " on " , " full " , " fault " , " hex " , " temp " ] ;
let ch_values : Vec < Option < String > > = redis ::cmd ( " HMGET " )
. arg ( ch_key )
. arg ( & ch_fields )
. query_async ( & mut * conn )
. await
. unwrap_or_default ( ) ;
// 状态映射: fault > full > on > idle
let fault = ch_values
. get ( 0 )
. and_then ( | v | v . as_ref ( ) )
. map ( | v | v = = " 1 " )
. unwrap_or ( false ) ;
let full = ch_values
. get ( 1 )
. and_then ( | v | v . as_ref ( ) )
. map ( | v | v = = " 1 " )
. unwrap_or ( false ) ;
let on = ch_values
. get ( 2 )
. and_then ( | v | v . as_ref ( ) )
. map ( | v | v = = " 1 " )
. unwrap_or ( false ) ;
let status = if fault {
" fault "
} else if full {
" full "
} else if on {
" charging "
} else {
" idle "
} ;
json! ( {
" status " : status ,
" on " : on ,
" full " : full ,
" fault " : fault ,
" hex " : ch_values . get ( 3 ) . and_then ( | v | v . as_ref ( ) ) . cloned ( ) . unwrap_or_default ( ) ,
" temp " : ch_values . get ( 4 ) . and_then ( | v | v . as_ref ( ) ) . and_then ( | v | v . parse ::< i64 > ( ) . ok ( ) ) ,
} )
2026-07-02 21:20:06 +08:00
}