mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-05-07 18:24:36 +00:00
[easytier-uptime] support tag in node list (#1487)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
use std::ops::{Div, Mul};
|
||||
|
||||
use axum::extract::{Path, Query, State};
|
||||
use axum::extract::{Path, State};
|
||||
use axum::Json;
|
||||
use sea_orm::{
|
||||
ColumnTrait, Condition, EntityTrait, IntoActiveModel, ModelTrait, Order, PaginatorTrait,
|
||||
@@ -16,6 +16,7 @@ use crate::api::{
|
||||
use crate::db::entity::{self, health_records, shared_nodes};
|
||||
use crate::db::{operations::*, Db};
|
||||
use crate::health_checker_manager::HealthCheckerManager;
|
||||
use axum_extra::extract::Query;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -60,6 +61,35 @@ pub async fn get_nodes(
|
||||
);
|
||||
}
|
||||
|
||||
// 标签过滤(支持单标签与多标签 OR)
|
||||
let mut filtered_ids: Option<Vec<i32>> = None;
|
||||
if !filters.tags.is_empty() {
|
||||
let ids_any =
|
||||
NodeOperations::filter_node_ids_by_tags_any(&app_state.db, &filters.tags).await?;
|
||||
filtered_ids = match filtered_ids {
|
||||
Some(mut existing) => {
|
||||
// 合并去重
|
||||
existing.extend(ids_any);
|
||||
existing.sort();
|
||||
existing.dedup();
|
||||
Some(existing)
|
||||
}
|
||||
None => Some(ids_any),
|
||||
};
|
||||
}
|
||||
if let Some(ids) = filtered_ids {
|
||||
if ids.is_empty() {
|
||||
return Ok(Json(ApiResponse::success(PaginatedResponse {
|
||||
items: vec![],
|
||||
total: 0,
|
||||
page,
|
||||
per_page,
|
||||
total_pages: 0,
|
||||
})));
|
||||
}
|
||||
query = query.filter(entity::shared_nodes::Column::Id.is_in(ids));
|
||||
}
|
||||
|
||||
let total = query.clone().count(app_state.db.orm_db()).await?;
|
||||
let nodes = query
|
||||
.order_by_asc(entity::shared_nodes::Column::Id)
|
||||
@@ -71,6 +101,13 @@ pub async fn get_nodes(
|
||||
let mut node_responses: Vec<NodeResponse> = nodes.into_iter().map(NodeResponse::from).collect();
|
||||
let total_pages = total.div_ceil(per_page as u64);
|
||||
|
||||
// 补充标签
|
||||
let ids: Vec<i32> = node_responses.iter().map(|n| n.id).collect();
|
||||
let tags_map = NodeOperations::get_nodes_tags_map(&app_state.db, &ids).await?;
|
||||
for n in &mut node_responses {
|
||||
n.tags = tags_map.get(&n.id).cloned().unwrap_or_default();
|
||||
}
|
||||
|
||||
// 为每个节点添加健康状态信息
|
||||
for node_response in &mut node_responses {
|
||||
if let Some(mut health_record) = app_state
|
||||
@@ -99,7 +136,6 @@ pub async fn get_nodes(
|
||||
|
||||
// remove sensitive information
|
||||
node_responses.iter_mut().for_each(|node| {
|
||||
tracing::info!("node: {:?}", node);
|
||||
node.network_name = None;
|
||||
node.network_secret = None;
|
||||
|
||||
@@ -161,7 +197,10 @@ pub async fn get_node(
|
||||
.await?
|
||||
.ok_or_else(|| ApiError::NotFound(format!("Node with id {} not found", id)))?;
|
||||
|
||||
Ok(Json(ApiResponse::success(NodeResponse::from(node))))
|
||||
let mut resp = NodeResponse::from(node);
|
||||
resp.tags = NodeOperations::get_node_tags(&app_state.db, resp.id).await?;
|
||||
|
||||
Ok(Json(ApiResponse::success(resp)))
|
||||
}
|
||||
|
||||
pub async fn get_node_health(
|
||||
@@ -325,6 +364,39 @@ pub async fn admin_get_nodes(
|
||||
);
|
||||
}
|
||||
|
||||
// 标签过滤(支持单标签与多标签 OR)
|
||||
let mut filtered_ids: Option<Vec<i32>> = None;
|
||||
if let Some(tag) = filters.tag {
|
||||
let ids = NodeOperations::filter_node_ids_by_tag(&app_state.db, &tag).await?;
|
||||
filtered_ids = Some(ids);
|
||||
}
|
||||
if let Some(tags) = filters.tags {
|
||||
if !tags.is_empty() {
|
||||
let ids_any = NodeOperations::filter_node_ids_by_tags_any(&app_state.db, &tags).await?;
|
||||
filtered_ids = match filtered_ids {
|
||||
Some(mut existing) => {
|
||||
existing.extend(ids_any);
|
||||
existing.sort();
|
||||
existing.dedup();
|
||||
Some(existing)
|
||||
}
|
||||
None => Some(ids_any),
|
||||
};
|
||||
}
|
||||
}
|
||||
if let Some(ids) = filtered_ids {
|
||||
if ids.is_empty() {
|
||||
return Ok(Json(ApiResponse::success(PaginatedResponse {
|
||||
items: vec![],
|
||||
total: 0,
|
||||
page,
|
||||
per_page,
|
||||
total_pages: 0,
|
||||
})));
|
||||
}
|
||||
query = query.filter(entity::shared_nodes::Column::Id.is_in(ids));
|
||||
}
|
||||
|
||||
let total = query.clone().count(app_state.db.orm_db()).await?;
|
||||
|
||||
let nodes = query
|
||||
@@ -334,7 +406,14 @@ pub async fn admin_get_nodes(
|
||||
.all(app_state.db.orm_db())
|
||||
.await?;
|
||||
|
||||
let node_responses: Vec<NodeResponse> = nodes.into_iter().map(NodeResponse::from).collect();
|
||||
let mut node_responses: Vec<NodeResponse> = nodes.into_iter().map(NodeResponse::from).collect();
|
||||
|
||||
// 补充标签
|
||||
let ids: Vec<i32> = node_responses.iter().map(|n| n.id).collect();
|
||||
let tags_map = NodeOperations::get_nodes_tags_map(&app_state.db, &ids).await?;
|
||||
for n in &mut node_responses {
|
||||
n.tags = tags_map.get(&n.id).cloned().unwrap_or_default();
|
||||
}
|
||||
|
||||
let total_pages = (total as f64 / per_page as f64).ceil() as u32;
|
||||
|
||||
@@ -366,7 +445,10 @@ pub async fn admin_approve_node(
|
||||
.exec(app_state.db.orm_db())
|
||||
.await?;
|
||||
|
||||
Ok(Json(ApiResponse::success(NodeResponse::from(updated_node))))
|
||||
let mut resp = NodeResponse::from(updated_node);
|
||||
resp.tags = NodeOperations::get_node_tags(&app_state.db, resp.id).await?;
|
||||
|
||||
Ok(Json(ApiResponse::success(resp)))
|
||||
}
|
||||
|
||||
pub async fn admin_update_node(
|
||||
@@ -432,7 +514,15 @@ pub async fn admin_update_node(
|
||||
.exec(app_state.db.orm_db())
|
||||
.await?;
|
||||
|
||||
Ok(Json(ApiResponse::success(NodeResponse::from(updated_node))))
|
||||
// 更新标签
|
||||
if let Some(tags) = request.tags {
|
||||
NodeOperations::set_node_tags(&app_state.db, updated_node.id, tags).await?;
|
||||
}
|
||||
|
||||
let mut resp = NodeResponse::from(updated_node);
|
||||
resp.tags = NodeOperations::get_node_tags(&app_state.db, resp.id).await?;
|
||||
|
||||
Ok(Json(ApiResponse::success(resp)))
|
||||
}
|
||||
|
||||
pub async fn admin_revoke_approval(
|
||||
@@ -454,7 +544,10 @@ pub async fn admin_revoke_approval(
|
||||
.exec(app_state.db.orm_db())
|
||||
.await?;
|
||||
|
||||
Ok(Json(ApiResponse::success(NodeResponse::from(updated_node))))
|
||||
let mut resp = NodeResponse::from(updated_node);
|
||||
resp.tags = NodeOperations::get_node_tags(&app_state.db, resp.id).await?;
|
||||
|
||||
Ok(Json(ApiResponse::success(resp)))
|
||||
}
|
||||
|
||||
pub async fn admin_delete_node(
|
||||
@@ -505,3 +598,10 @@ fn verify_admin_token(headers: &HeaderMap) -> ApiResult<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_all_tags(
|
||||
State(app_state): State<AppState>,
|
||||
) -> ApiResult<Json<ApiResponse<Vec<String>>>> {
|
||||
let tags = NodeOperations::get_all_tags(&app_state.db).await?;
|
||||
Ok(Json(ApiResponse::success(tags)))
|
||||
}
|
||||
|
||||
@@ -162,6 +162,9 @@ pub struct UpdateNodeRequest {
|
||||
|
||||
#[validate(email)]
|
||||
pub mail: Option<String>,
|
||||
|
||||
// 标签字段(仅管理员可用)
|
||||
pub tags: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -198,6 +201,7 @@ pub struct NodeResponse {
|
||||
pub qq_number: Option<String>,
|
||||
pub wechat: Option<String>,
|
||||
pub mail: Option<String>,
|
||||
pub tags: Vec<String>,
|
||||
}
|
||||
|
||||
impl From<entity::shared_nodes::Model> for NodeResponse {
|
||||
@@ -247,6 +251,7 @@ impl From<entity::shared_nodes::Model> for NodeResponse {
|
||||
} else {
|
||||
Some(node.mail)
|
||||
},
|
||||
tags: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -281,6 +286,8 @@ pub struct NodeFilterParams {
|
||||
pub is_active: Option<bool>,
|
||||
pub protocol: Option<String>,
|
||||
pub search: Option<String>,
|
||||
#[serde(default)]
|
||||
pub tags: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -313,4 +320,6 @@ pub struct AdminNodeFilterParams {
|
||||
pub is_approved: Option<bool>,
|
||||
pub protocol: Option<String>,
|
||||
pub search: Option<String>,
|
||||
pub tag: Option<String>,
|
||||
pub tags: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use tower_http::cors::CorsLayer;
|
||||
use super::handlers::AppState;
|
||||
use super::handlers::{
|
||||
admin_approve_node, admin_delete_node, admin_get_nodes, admin_login, admin_revoke_approval,
|
||||
admin_update_node, admin_verify_token, create_node, get_node, get_node_health,
|
||||
admin_update_node, admin_verify_token, create_node, get_all_tags, get_node, get_node_health,
|
||||
get_node_health_stats, get_nodes, health_check,
|
||||
};
|
||||
use crate::api::{get_node_connect_url, test_connection};
|
||||
@@ -38,6 +38,7 @@ pub fn create_routes() -> Router<AppState> {
|
||||
.route("/node/{id}", get(get_node_connect_url))
|
||||
.route("/health", get(health_check))
|
||||
.route("/api/nodes", get(get_nodes).post(create_node))
|
||||
.route("/api/tags", get(get_all_tags))
|
||||
.route("/api/test_connection", post(test_connection))
|
||||
.route("/api/nodes/{id}/health", get(get_node_health))
|
||||
.route("/api/nodes/{id}/health/stats", get(get_node_health_stats))
|
||||
|
||||
@@ -2,6 +2,8 @@ use std::env;
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use easytier::common::config::{ConsoleLoggerConfig, FileLoggerConfig, LoggingConfig};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AppConfig {
|
||||
pub server: ServerConfig,
|
||||
@@ -32,12 +34,6 @@ pub struct HealthCheckConfig {
|
||||
pub max_retries: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LoggingConfig {
|
||||
pub level: String,
|
||||
pub rust_log: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CorsConfig {
|
||||
pub allowed_origins: Vec<String>,
|
||||
@@ -100,8 +96,14 @@ impl AppConfig {
|
||||
};
|
||||
|
||||
let logging_config = LoggingConfig {
|
||||
level: env::var("LOG_LEVEL").unwrap_or_else(|_| "info".to_string()),
|
||||
rust_log: env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()),
|
||||
file_logger: Some(FileLoggerConfig {
|
||||
level: Some(env::var("LOG_LEVEL").unwrap_or_else(|_| "info".to_string())),
|
||||
file: Some("easytier-uptime.log".to_string()),
|
||||
..Default::default()
|
||||
}),
|
||||
console_logger: Some(ConsoleLoggerConfig {
|
||||
level: Some(env::var("LOG_LEVEL").unwrap_or_else(|_| "info".to_string())),
|
||||
}),
|
||||
};
|
||||
|
||||
let cors_config = CorsConfig {
|
||||
@@ -161,8 +163,14 @@ impl AppConfig {
|
||||
max_retries: 3,
|
||||
},
|
||||
logging: LoggingConfig {
|
||||
level: "info".to_string(),
|
||||
rust_log: "info".to_string(),
|
||||
file_logger: Some(FileLoggerConfig {
|
||||
level: Some("info".to_string()),
|
||||
file: Some("easytier-uptime.log".to_string()),
|
||||
..Default::default()
|
||||
}),
|
||||
console_logger: Some(ConsoleLoggerConfig {
|
||||
level: Some("info".to_string()),
|
||||
}),
|
||||
},
|
||||
cors: CorsConfig {
|
||||
allowed_origins: vec![
|
||||
|
||||
@@ -3,4 +3,5 @@
|
||||
pub mod prelude;
|
||||
|
||||
pub mod health_records;
|
||||
pub mod node_tags;
|
||||
pub mod shared_nodes;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
//! `SeaORM` Entity for node tags
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "node_tags")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub node_id: i32,
|
||||
pub tag: String,
|
||||
pub created_at: DateTimeWithTimeZone,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::shared_nodes::Entity",
|
||||
from = "Column::NodeId",
|
||||
to = "super::shared_nodes::Column::Id"
|
||||
)]
|
||||
SharedNodes,
|
||||
}
|
||||
|
||||
impl Related<super::shared_nodes::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::SharedNodes.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -1,4 +1,5 @@
|
||||
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
|
||||
|
||||
pub use super::health_records::Entity as HealthRecords;
|
||||
pub use super::node_tags::Entity as NodeTags;
|
||||
pub use super::shared_nodes::Entity as SharedNodes;
|
||||
|
||||
@@ -33,6 +33,9 @@ pub struct Model {
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_many = "super::health_records::Entity")]
|
||||
HealthRecords,
|
||||
// add relation to node_tags
|
||||
#[sea_orm(has_many = "super::node_tags::Entity")]
|
||||
NodeTags,
|
||||
}
|
||||
|
||||
impl Related<super::health_records::Entity> for Entity {
|
||||
@@ -41,4 +44,10 @@ impl Related<super::health_records::Entity> for Entity {
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::node_tags::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::NodeTags.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::db::Db;
|
||||
use crate::db::HealthStats;
|
||||
use crate::db::HealthStatus;
|
||||
use sea_orm::*;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
/// 节点管理操作
|
||||
pub struct NodeOperations;
|
||||
@@ -229,6 +230,128 @@ impl HealthOperations {
|
||||
Ok(result.rows_affected)
|
||||
}
|
||||
}
|
||||
impl NodeOperations {
|
||||
/// 获取节点的全部标签
|
||||
pub async fn get_node_tags(db: &Db, node_id: i32) -> Result<Vec<String>, DbErr> {
|
||||
let tags = node_tags::Entity::find()
|
||||
.filter(node_tags::Column::NodeId.eq(node_id))
|
||||
.all(db.orm_db())
|
||||
.await?;
|
||||
Ok(tags.into_iter().map(|m| m.tag).collect())
|
||||
}
|
||||
|
||||
/// 批量获取节点的标签映射
|
||||
pub async fn get_nodes_tags_map(
|
||||
db: &Db,
|
||||
node_ids: &[i32],
|
||||
) -> Result<HashMap<i32, Vec<String>>, DbErr> {
|
||||
if node_ids.is_empty() {
|
||||
return Ok(HashMap::new());
|
||||
}
|
||||
let tags = node_tags::Entity::find()
|
||||
.filter(node_tags::Column::NodeId.is_in(node_ids.to_vec()))
|
||||
.order_by_asc(node_tags::Column::NodeId)
|
||||
.all(db.orm_db())
|
||||
.await?;
|
||||
let mut map: HashMap<i32, Vec<String>> = HashMap::new();
|
||||
for t in tags {
|
||||
map.entry(t.node_id).or_default().push(t.tag);
|
||||
}
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
/// 使用标签过滤节点(返回节点ID)
|
||||
pub async fn filter_node_ids_by_tag(db: &Db, tag: &str) -> Result<Vec<i32>, DbErr> {
|
||||
let tagged = node_tags::Entity::find()
|
||||
.filter(node_tags::Column::Tag.eq(tag))
|
||||
.all(db.orm_db())
|
||||
.await?;
|
||||
Ok(tagged.into_iter().map(|m| m.node_id).collect())
|
||||
}
|
||||
|
||||
/// 设置节点标签(替换为给定集合)
|
||||
pub async fn set_node_tags(db: &Db, node_id: i32, tags: Vec<String>) -> Result<(), DbErr> {
|
||||
// 去重与清理空白
|
||||
let mut set: HashSet<String> = HashSet::new();
|
||||
for tag in tags.into_iter() {
|
||||
let trimmed = tag.trim();
|
||||
if !trimmed.is_empty() {
|
||||
set.insert(trimmed.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// 取出当前标签
|
||||
let existing = node_tags::Entity::find()
|
||||
.filter(node_tags::Column::NodeId.eq(node_id))
|
||||
.all(db.orm_db())
|
||||
.await?;
|
||||
|
||||
let existing_set: HashSet<String> = existing.iter().map(|m| m.tag.clone()).collect();
|
||||
|
||||
// 需要删除的
|
||||
let to_delete: Vec<i32> = existing
|
||||
.iter()
|
||||
.filter(|m| !set.contains(&m.tag))
|
||||
.map(|m| m.id)
|
||||
.collect();
|
||||
|
||||
// 需要新增的
|
||||
let to_insert: Vec<String> = set
|
||||
.into_iter()
|
||||
.filter(|t| !existing_set.contains(t))
|
||||
.collect();
|
||||
|
||||
// 执行删除
|
||||
if !to_delete.is_empty() {
|
||||
node_tags::Entity::delete_many()
|
||||
.filter(node_tags::Column::Id.is_in(to_delete))
|
||||
.exec(db.orm_db())
|
||||
.await?;
|
||||
}
|
||||
|
||||
// 执行新增
|
||||
for t in to_insert {
|
||||
let now = chrono::Utc::now().fixed_offset();
|
||||
let am = node_tags::ActiveModel {
|
||||
id: NotSet,
|
||||
node_id: Set(node_id),
|
||||
tag: Set(t),
|
||||
created_at: Set(now),
|
||||
};
|
||||
node_tags::Entity::insert(am).exec(db.orm_db()).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// 新增:获取所有唯一标签(按字母排序)
|
||||
pub async fn get_all_tags(db: &Db) -> Result<Vec<String>, DbErr> {
|
||||
let rows = node_tags::Entity::find().all(db.orm_db()).await?;
|
||||
let mut set: HashSet<String> = HashSet::new();
|
||||
for r in rows {
|
||||
set.insert(r.tag);
|
||||
}
|
||||
let mut list: Vec<String> = set.into_iter().collect();
|
||||
list.sort();
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
// 新增:使用多标签(OR 语义)过滤节点,返回匹配的节点ID
|
||||
pub async fn filter_node_ids_by_tags_any(db: &Db, tags: &[String]) -> Result<Vec<i32>, DbErr> {
|
||||
if tags.is_empty() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
let tagged = node_tags::Entity::find()
|
||||
.filter(node_tags::Column::Tag.is_in(tags.to_vec()))
|
||||
.all(db.orm_db())
|
||||
.await?;
|
||||
let mut set: HashSet<i32> = HashSet::new();
|
||||
for m in tagged {
|
||||
set.insert(m.node_id);
|
||||
}
|
||||
Ok(set.into_iter().collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
@@ -11,6 +11,7 @@ use api::routes::create_routes;
|
||||
use clap::Parser;
|
||||
use config::AppConfig;
|
||||
use db::{operations::NodeOperations, Db};
|
||||
use easytier::utils::init_logger;
|
||||
use health_checker::HealthChecker;
|
||||
use health_checker_manager::HealthCheckerManager;
|
||||
use std::env;
|
||||
@@ -36,18 +37,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
let config = AppConfig::default();
|
||||
|
||||
// 初始化日志
|
||||
tracing_subscriber::fmt()
|
||||
.with_max_level(match config.logging.level.as_str() {
|
||||
"debug" => tracing::Level::DEBUG,
|
||||
"info" => tracing::Level::INFO,
|
||||
"warn" => tracing::Level::WARN,
|
||||
"error" => tracing::Level::ERROR,
|
||||
_ => tracing::Level::INFO,
|
||||
})
|
||||
.with_target(false)
|
||||
.with_thread_ids(true)
|
||||
.with_env_filter(EnvFilter::new("easytier_uptime"))
|
||||
.init();
|
||||
let _ = init_logger(&config.logging, false);
|
||||
|
||||
// 解析命令行参数
|
||||
let args = Args::parse();
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
use sea_orm_migration::{prelude::*, schema::*};
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum NodeTags {
|
||||
Table,
|
||||
Id,
|
||||
NodeId,
|
||||
Tag,
|
||||
CreatedAt,
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum SharedNodes {
|
||||
Table,
|
||||
Id,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
// 创建 node_tags 表
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(NodeTags::Table)
|
||||
.if_not_exists()
|
||||
.col(pk_auto(NodeTags::Id).not_null())
|
||||
.col(integer(NodeTags::NodeId).not_null())
|
||||
.col(string(NodeTags::Tag).not_null())
|
||||
.col(
|
||||
timestamp_with_time_zone(NodeTags::CreatedAt)
|
||||
.not_null()
|
||||
.default(Expr::current_timestamp()),
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("fk_node_tags_node")
|
||||
.from(NodeTags::Table, NodeTags::NodeId)
|
||||
.to(SharedNodes::Table, SharedNodes::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade)
|
||||
.on_update(ForeignKeyAction::Cascade),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 索引:NodeId
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.name("idx_node_tags_node")
|
||||
.table(NodeTags::Table)
|
||||
.col(NodeTags::NodeId)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 索引:Tag
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.name("idx_node_tags_tag")
|
||||
.table(NodeTags::Table)
|
||||
.col(NodeTags::Tag)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 唯一索引:每个节点的标签唯一
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.name("uniq_node_tag_per_node")
|
||||
.table(NodeTags::Table)
|
||||
.col(NodeTags::NodeId)
|
||||
.col(NodeTags::Tag)
|
||||
.unique()
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
// 先删除索引
|
||||
manager
|
||||
.drop_index(
|
||||
Index::drop()
|
||||
.name("idx_node_tags_node")
|
||||
.table(NodeTags::Table)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
manager
|
||||
.drop_index(
|
||||
Index::drop()
|
||||
.name("idx_node_tags_tag")
|
||||
.table(NodeTags::Table)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
manager
|
||||
.drop_index(
|
||||
Index::drop()
|
||||
.name("uniq_node_tag_per_node")
|
||||
.table(NodeTags::Table)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.drop_table(Table::drop().table(NodeTags::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,16 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
mod m20250101_000001_create_tables;
|
||||
mod m20250101_000002_create_node_tags;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigratorTrait for Migrator {
|
||||
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
|
||||
vec![Box::new(m20250101_000001_create_tables::Migration)]
|
||||
vec![
|
||||
Box::new(m20250101_000001_create_tables::Migration),
|
||||
Box::new(m20250101_000002_create_node_tags::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user