Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
251 changes: 226 additions & 25 deletions src-tauri/src/service/asset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ use crate::api::endpoint;
use crate::api::request::{ApiRequestClient, ApiResponse};
use log::info;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::cmp::Ordering;
use std::collections::HashSet;
use url::Url;

#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default)]
Expand All @@ -12,11 +15,21 @@ pub enum Category {
Windows,
#[serde(rename = "windows_ad")]
WindowsAd,
Unix,
Other,
Database,
Device,
Web,
}

#[derive(Debug, Deserialize)]
struct AssetListResponse {
count: usize,
next: Option<String>,
previous: Option<String>,
results: Vec<Value>,
}

#[derive(Serialize, Deserialize, Default, Debug, Clone)]
pub struct AssetQuery {
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
Expand Down Expand Up @@ -48,9 +61,12 @@ impl AssetQuery {
// r#type 这种形式是因为 type 是 Rust 关键字,所以要写成 r#type
let (r#type, category) = match asset_type {
Category::Database | Category::Device => (None, Some(asset_type)),
Category::Linux | Category::Windows | Category::WindowsAd | Category::Web => {
(Some(asset_type), None)
}
Category::Web => (None, Some(asset_type)),
Category::Linux
| Category::Windows
| Category::WindowsAd
| Category::Unix
| Category::Other => (Some(asset_type), None),
};

Self {
Expand Down Expand Up @@ -111,30 +127,31 @@ impl AssetService {
);
info!("query: {:?}", query);

let (r#type, category) = if favorite {
(None, None)
} else {
match query.get_category() {
Category::Linux => (Some(Category::Linux), None),
Category::Windows => (Some(Category::Windows), None),
Category::WindowsAd => (Some(Category::WindowsAd), None),
Category::Database => (None, Some(Category::Database)),
Category::Device => (None, Some(Category::Device)),
Category::Web => (None, Some(Category::Web)),
}
};
if favorite {
let favorite_query = AssetQuery {
r#type: None,
category: None,
offset: Some(query.offset.unwrap_or(0)),
limit: Some(query.limit.unwrap_or(20)),
search: Some(query.search.clone().unwrap_or_default()),
order: Some(query.order.clone().unwrap_or_default()),
oid: query.oid.clone(),
};

let query = AssetQuery {
r#type,
category,
offset: Some(query.offset.unwrap_or(0)),
limit: Some(query.limit.unwrap_or(20)),
search: Some(query.search.clone().unwrap_or_default()),
order: Some(query.order.clone().unwrap_or_default()),
oid: query.oid.clone(),
};
return self
.api
.get_with_query_response(&url, &favorite_query)
.await;
}

let queries = Self::expand_queries(query);

self.api.get_with_query_response(&url, &query).await
if queries.len() == 1 {
return self.api.get_with_query_response(&url, &queries[0]).await;
}

self.get_combined_category_assets(&url, query, &queries)
.await
}

/// 获取当前用户收藏的资产列表
Expand Down Expand Up @@ -184,4 +201,188 @@ impl AssetService {

self.api.post_json_with_response(&url, &body).await
}

fn expand_queries(query: &AssetQuery) -> Vec<AssetQuery> {
let offset = Some(query.offset.unwrap_or(0));
let limit = Some(query.limit.unwrap_or(20));
let search = Some(query.search.clone().unwrap_or_default());
let order = Some(query.order.clone().unwrap_or_default());
let oid = query.oid.clone();

let exact = |r#type: Option<Category>, category: Option<Category>| AssetQuery {
r#type,
category,
offset,
limit,
search: search.clone(),
order: order.clone(),
oid: oid.clone(),
};

// 菜单和 JumpServer 的原始 type/category 不是一一对应:
// Windows 要合并 windows + windows_ad,Other 要合并 unix + other。
match query.get_category() {
Category::Linux => vec![exact(Some(Category::Linux), None)],
Category::Windows => vec![
exact(Some(Category::Windows), None),
exact(Some(Category::WindowsAd), None),
],
Category::WindowsAd => vec![exact(Some(Category::WindowsAd), None)],
Category::Unix => vec![exact(Some(Category::Unix), None)],
Category::Other => vec![
exact(Some(Category::Unix), None),
exact(Some(Category::Other), None),
],
Category::Database => vec![exact(None, Some(Category::Database))],
Category::Device => vec![exact(None, Some(Category::Device))],
Category::Web => vec![exact(None, Some(Category::Web))],
}
}

async fn get_combined_category_assets(
&self,
url: &str,
query: &AssetQuery,
queries: &[AssetQuery],
) -> ApiResponse {
let mut combined: Vec<Value> = Vec::new();
let mut seen_ids = HashSet::new();

for sub_query in queries {
let response = self.fetch_all_assets(url, sub_query).await;
let list = match response {
Ok(list) => list,
Err(err) => return err,
};

for item in list {
let asset_id = item
.get("id")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string();

// 合并多个接口结果时按资产 ID 去重,避免未来后端类型收敛后重复展示。
if asset_id.is_empty() || seen_ids.insert(asset_id) {
combined.push(item);
}
}
}

// 多个独立查询拼起来后需要重新做一次全量排序,否则列表会按子查询分段。
Self::sort_assets(&mut combined, query.order.as_deref());

let total = combined.len();
let offset = query.offset.unwrap_or(0) as usize;
let limit = query.limit.unwrap_or(20) as usize;
let results = combined
.into_iter()
.skip(offset)
.take(limit)
.collect::<Vec<_>>();

ApiResponse::ok(
200,
json!({
"count": total,
"next": Value::Null,
"previous": Value::Null,
"results": results,
})
.to_string(),
)
}

async fn fetch_all_assets(
&self,
url: &str,
query: &AssetQuery,
) -> Result<Vec<Value>, ApiResponse> {
const BATCH_LIMIT: u32 = 200;

let mut offset = 0;
let mut all_results = Vec::new();

loop {
let page_query = AssetQuery {
r#type: query.r#type,
category: query.category,
offset: Some(offset),
limit: Some(BATCH_LIMIT),
search: Some(query.search.clone().unwrap_or_default()),
order: Some(query.order.clone().unwrap_or_default()),
oid: query.oid.clone(),
};

let response = self.api.get_with_query_response(url, &page_query).await;
if !response.success {
return Err(response);
}

let payload: AssetListResponse = match serde_json::from_str(&response.data) {
Ok(payload) => payload,
Err(error) => {
return Err(ApiResponse::failed(format!(
"parse asset list response failed: {}",
error
)))
}
};

let AssetListResponse {
count,
next: _next,
previous: _previous,
results,
} = payload;

let page_size = results.len();
all_results.extend(results);

// 这里主动翻完后端分页,前端菜单才能在“组合类型”场景下拿到准确总数和切片。
if page_size == 0 || all_results.len() >= count {
break;
}

offset += page_size as u32;
}

Ok(all_results)
}

fn sort_assets(results: &mut [Value], order: Option<&str>) {
let normalized = order
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or("name");

// 当前客户端只有 name / date_updated 这几种排序,空值时退回 name 保证混合结果稳定。
let descending = normalized.starts_with('-');
let field = normalized.trim_start_matches('-');

results.sort_by(|left, right| {
let ordering = Self::compare_asset_field(left, right, field);
if descending {
ordering.reverse()
} else {
ordering
}
});
}

fn compare_asset_field(left: &Value, right: &Value, field: &str) -> Ordering {
let left_value = Self::extract_asset_field(left, field);
let right_value = Self::extract_asset_field(right, field);

left_value.cmp(&right_value).then_with(|| {
Self::extract_asset_field(left, "id").cmp(&Self::extract_asset_field(right, "id"))
})
}

fn extract_asset_field(item: &Value, field: &str) -> String {
item.get(field)
.and_then(Value::as_str)
.unwrap_or_default()
.to_lowercase()
}
}
7 changes: 1 addition & 6 deletions src-tauri/src/service/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,12 +312,7 @@ impl ConfigService {
.parent()
.ok_or_else(|| "invalid config directory".to_string())?;
return PluginService::update_selection(
app,
config_dir,
category,
protocol,
name,
new_path,
app, config_dir, category, protocol, name, new_path,
);
}

Expand Down
2 changes: 1 addition & 1 deletion src-tauri/src/service/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
pub(crate) mod asset;
pub(crate) mod config;
pub(crate) mod plugin;
pub(crate) mod connect;
pub(crate) mod oauth;
pub(crate) mod plugin;
pub(crate) mod setting;
pub(crate) mod token;
pub(crate) mod user;
Expand Down
Loading
Loading