From 25a63b1fb7fe39d5e2ea303ae585e49e355d9822 Mon Sep 17 00:00:00 2001 From: soul-walker <31162815+soul-walker@users.noreply.github.com> Date: Thu, 7 Nov 2024 18:06:42 +0800 Subject: [PATCH] =?UTF-8?q?1=20=E6=95=B0=E6=8D=AE=E5=BA=93=E5=AE=9A?= =?UTF-8?q?=E4=B9=89=E8=B0=83=E6=95=B4=202=20=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E8=AE=BF=E9=97=AE=E6=A8=A1=E5=9D=97=E7=94=A8=E6=88=B7=E3=80=81?= =?UTF-8?q?=E7=BB=84=E7=BB=87=E3=80=81=E7=BB=84=E7=BB=87=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E5=9F=BA=E7=A1=80=E5=8A=9F=E8=83=BD=E6=8E=A5=E5=8F=A3=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E5=B9=B6=E6=B5=8B=E8=AF=95=203=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93=E8=B6=85=E7=BA=A7=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=91=98=E7=94=A8=E6=88=B7=E5=92=8C=E9=BB=98=E8=AE=A4=E7=BB=84?= =?UTF-8?q?=E7=BB=87=E5=88=9D=E5=A7=8B=E5=8C=96=204=20=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E6=B3=A8=E5=86=8C=E7=99=BB=E5=BD=95=E3=80=81=E7=BB=84=E7=BB=87?= =?UTF-8?q?=E3=80=81=E7=BB=84=E7=BB=87=E7=94=A8=E6=88=B7=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E5=9F=BA=E6=9C=ACGraphQL=E6=8E=A5=E5=8F=A3=E5=BC=80=E5=8F=91?= =?UTF-8?q?=EF=BC=8C=E7=99=BB=E5=BD=95=E6=8E=A5=E5=8F=A3=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 57 +- Cargo.toml | 3 +- crates/rtsa_db/Cargo.toml | 1 + crates/rtsa_db/src/common.rs | 4 +- crates/rtsa_db/src/db_access/draft_data.rs | 113 +- crates/rtsa_db/src/db_access/feature.rs | 35 +- crates/rtsa_db/src/db_access/mod.rs | 14 + crates/rtsa_db/src/db_access/org.rs | 669 +++++++++++ crates/rtsa_db/src/db_access/org_user.rs | 770 ++++++++++++ crates/rtsa_db/src/db_access/release_data.rs | 123 +- crates/rtsa_db/src/db_access/user.rs | 1034 +++++++++++++---- crates/rtsa_db/src/error.rs | 10 +- crates/rtsa_db/src/lib.rs | 3 + crates/rtsa_db/src/model.rs | 376 +++++- crates/rtsa_db/src/password_util.rs | 4 + crates/rtsa_db/src/username_util.rs | 53 + crates/rtsa_dto/src/pb/common.rs | 17 + manager/Cargo.toml | 28 +- manager/conf/default.toml | 7 - manager/conf/dev.toml | 3 - manager/conf/local_test.toml | 3 - manager/crates/rtsa_api/Cargo.toml | 24 - manager/crates/rtsa_api/src/lib.rs | 7 - manager/crates/rtsa_api/src/user_auth/mod.rs | 323 ----- .../src/apis/data_options_def.rs | 0 .../rtsa_api => }/src/apis/draft_data.rs | 40 +- .../{crates/rtsa_api => }/src/apis/feature.rs | 11 +- .../src/apis/feature_config_def.rs | 0 manager/{crates/rtsa_api => }/src/apis/mod.rs | 21 +- manager/src/apis/org.rs | 119 ++ manager/src/apis/org_user.rs | 145 +++ .../rtsa_api => }/src/apis/release_data.rs | 32 +- .../rtsa_api => }/src/apis/sys_info.rs | 0 .../{crates/rtsa_api => }/src/apis/user.rs | 153 ++- manager/src/app_config.rs | 11 - manager/src/commands/cmd.rs | 16 +- manager/src/commands/db.rs | 8 +- manager/src/error.rs | 18 + manager/src/lib.rs | 9 + .../{crates/rtsa_api => }/src/loader/mod.rs | 0 manager/{crates/rtsa_api => }/src/server.rs | 39 +- manager/src/sys_init/mod.rs | 78 ++ .../rtsa_api => }/src/user_auth/jwt_auth.rs | 61 +- manager/src/user_auth/mod.rs | 135 +++ migrations/20240830095636_init.up.sql | 84 +- rtsa-proto-msg | 2 +- 46 files changed, 3633 insertions(+), 1030 deletions(-) create mode 100644 crates/rtsa_db/src/db_access/org.rs create mode 100644 crates/rtsa_db/src/db_access/org_user.rs create mode 100644 crates/rtsa_db/src/password_util.rs create mode 100644 crates/rtsa_db/src/username_util.rs delete mode 100644 manager/crates/rtsa_api/Cargo.toml delete mode 100644 manager/crates/rtsa_api/src/lib.rs delete mode 100644 manager/crates/rtsa_api/src/user_auth/mod.rs rename manager/{crates/rtsa_api => }/src/apis/data_options_def.rs (100%) rename manager/{crates/rtsa_api => }/src/apis/draft_data.rs (92%) rename manager/{crates/rtsa_api => }/src/apis/feature.rs (96%) rename manager/{crates/rtsa_api => }/src/apis/feature_config_def.rs (100%) rename manager/{crates/rtsa_api => }/src/apis/mod.rs (82%) create mode 100644 manager/src/apis/org.rs create mode 100644 manager/src/apis/org_user.rs rename manager/{crates/rtsa_api => }/src/apis/release_data.rs (93%) rename manager/{crates/rtsa_api => }/src/apis/sys_info.rs (100%) rename manager/{crates/rtsa_api => }/src/apis/user.rs (51%) create mode 100644 manager/src/error.rs rename manager/{crates/rtsa_api => }/src/loader/mod.rs (100%) rename manager/{crates/rtsa_api => }/src/server.rs (69%) create mode 100644 manager/src/sys_init/mod.rs rename manager/{crates/rtsa_api => }/src/user_auth/jwt_auth.rs (53%) create mode 100644 manager/src/user_auth/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 82ff930..aadcb44 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1633,14 +1633,27 @@ name = "manager" version = "0.1.0" dependencies = [ "anyhow", + "async-graphql", + "async-graphql-axum", + "axum", + "axum-extra", + "base64 0.22.1", + "chrono", "clap", "config", "enum_dispatch", - "rtsa_api", + "jsonwebtoken", + "reqwest", "rtsa_db", + "rtsa_dto", "rtsa_log", "serde", + "serde_json", + "sqlx", + "sysinfo", + "thiserror", "tokio", + "tower-http", ] [[package]] @@ -2268,14 +2281,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.6" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.7", - "regex-syntax 0.8.4", + "regex-automata 0.4.8", + "regex-syntax 0.8.5", ] [[package]] @@ -2289,13 +2302,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.4", + "regex-syntax 0.8.5", ] [[package]] @@ -2306,9 +2319,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" @@ -2399,35 +2412,13 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rtsa_api" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-graphql", - "async-graphql-axum", - "axum", - "axum-extra", - "base64 0.22.1", - "chrono", - "jsonwebtoken", - "reqwest", - "rtsa_db", - "rtsa_dto", - "rtsa_log", - "serde", - "serde_json", - "sysinfo", - "tokio", - "tower-http", -] - [[package]] name = "rtsa_db" version = "0.1.0" dependencies = [ "anyhow", "lazy_static", + "regex", "rtsa_dto", "rtsa_log", "serde", diff --git a/Cargo.toml b/Cargo.toml index 5287a86..e2fbc15 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] -members = ["crates/*", "manager/crates/*", "manager", "simulation"] +members = ["crates/*", "manager", "simulation"] resolver = "2" [workspace.dependencies] @@ -27,3 +27,4 @@ config = "0.14.1" clap = { version = "4.5.20", features = ["derive"] } enum_dispatch = "0.3" tower = { version = "0.5.1", features = ["util"] } +regex = "1.11.1" diff --git a/crates/rtsa_db/Cargo.toml b/crates/rtsa_db/Cargo.toml index b345804..647b538 100644 --- a/crates/rtsa_db/Cargo.toml +++ b/crates/rtsa_db/Cargo.toml @@ -18,6 +18,7 @@ sqlx = { workspace = true, features = [ ] } thiserror = { workspace = true } lazy_static = { workspace = true } +regex = { workspace = true } rtsa_dto = { path = "../rtsa_dto" } rtsa_log = { path = "../rtsa_log" } diff --git a/crates/rtsa_db/src/common.rs b/crates/rtsa_db/src/common.rs index 4d1e00d..611f9d0 100644 --- a/crates/rtsa_db/src/common.rs +++ b/crates/rtsa_db/src/common.rs @@ -107,12 +107,12 @@ impl Display for SortOrder { } #[derive(Debug)] -pub struct PageResult { +pub struct PageData { pub total: i64, pub data: Vec, } -impl PageResult { +impl PageData { pub fn new(total: i64, data: Vec) -> Self { Self { total, data } } diff --git a/crates/rtsa_db/src/db_access/draft_data.rs b/crates/rtsa_db/src/db_access/draft_data.rs index 6a2fcb8..6fc84ba 100644 --- a/crates/rtsa_db/src/db_access/draft_data.rs +++ b/crates/rtsa_db/src/db_access/draft_data.rs @@ -1,11 +1,10 @@ use std::vec; -use rtsa_dto::common::DataType; use rtsa_log::tracing::debug; use serde_json::Value; use crate::{ - common::{PageQuery, PageResult, Sort, SortOrder, TableColumn}, + common::{PageData, PageQuery, Sort, SortOrder, TableColumn}, model::{DraftDataColumn, DraftDataModel}, DbAccessError, }; @@ -20,14 +19,14 @@ pub trait DraftDataAccessor { &self, query: DraftDataQuery, page: PageQuery, - ) -> Result, DbAccessError>; + ) -> Result, DbAccessError>; /// 根据id查询草稿数据 async fn query_draft_data_by_id(&self, id: i32) -> Result; /// 是否user_id+data_type+name的数据已存在 async fn is_draft_data_exist( &self, user_id: i32, - data_type: &DataType, + data_type: i32, name: &str, ) -> Result; /// 创建草稿数据基本信息 @@ -74,7 +73,7 @@ pub trait DraftDataAccessor { pub struct DraftDataQuery { pub user_id: Option, pub name: Option, - pub data_type: Option, + pub data_type: Option, pub options: Option, pub is_shared: Option, } @@ -94,7 +93,7 @@ impl DraftDataQuery { self } - pub fn with_data_type(mut self, data_type: DataType) -> Self { + pub fn with_data_type(mut self, data_type: i32) -> Self { self.data_type = Some(data_type); self } @@ -130,7 +129,7 @@ impl DraftDataQuery { filters.push(format!( "{} = {}", DraftDataColumn::DataType.name(), - data_type as i32 + data_type )); } if let Some(is_shared) = self.is_shared { @@ -157,7 +156,7 @@ impl DraftDataQuery { pub struct CreateDraftData { name: String, - data_type: DataType, + data_type: i32, options: Option, data: Option>, default_release_data_id: Option, @@ -165,7 +164,7 @@ pub struct CreateDraftData { } impl CreateDraftData { - pub fn new(name: &str, data_type: DataType, user_id: i32) -> Self { + pub fn new(name: &str, data_type: i32, user_id: i32) -> Self { CreateDraftData { name: name.to_string(), data_type, @@ -210,7 +209,7 @@ impl DraftDataAccessor for RtsaDbAccessor { &self, query: DraftDataQuery, page: PageQuery, - ) -> Result, DbAccessError> { + ) -> Result, DbAccessError> { let table = DraftDataColumn::Table.name(); let where_clause = query.build_filter(); let sql = format!("SELECT COUNT(*) FROM {table} {where_clause}"); @@ -220,7 +219,7 @@ impl DraftDataAccessor for RtsaDbAccessor { let total: i64 = sqlx::query_scalar(&sql).fetch_one(&self.pool).await?; if total == 0 { - return Ok(PageResult::new(total, vec![])); + return Ok(PageData::new(total, vec![])); } let select_columns = format!( @@ -245,7 +244,7 @@ impl DraftDataAccessor for RtsaDbAccessor { debug!("paging sql: {}", paging_sql); let list: Vec = sqlx::query_as(&paging_sql).fetch_all(&self.pool).await?; - Ok(PageResult::new(total, list)) + Ok(PageData::new(total, list)) } async fn query_draft_data_by_id(&self, id: i32) -> Result { @@ -259,7 +258,7 @@ impl DraftDataAccessor for RtsaDbAccessor { async fn is_draft_data_exist( &self, user_id: i32, - data_type: &DataType, + data_type: i32, name: &str, ) -> Result { let table = DraftDataColumn::Table.name(); @@ -285,10 +284,12 @@ impl DraftDataAccessor for RtsaDbAccessor { ) -> Result { // 检查是否已存在 let exist = self - .is_draft_data_exist(create.user_id, &create.data_type, &create.name) + .is_draft_data_exist(create.user_id, create.data_type, &create.name) .await?; if exist { - return Err(DbAccessError::RowExist); + return Err(DbAccessError::RowExist( + "uid + data_type + name".to_string(), + )); } // 插入数据 let table = DraftDataColumn::Table.name(); @@ -433,14 +434,57 @@ impl DraftDataAccessor for RtsaDbAccessor { #[cfg(test)] mod tests { - use crate::{SyncUserInfo, UserAccessor}; + use crate::{RegisterUser, UserAccessor}; use super::*; - use rtsa_dto::common::{IscsStyle, Role}; use rtsa_log::tracing::Level; use serde::{Deserialize, Serialize}; - use sqlx::{types::chrono::Local, PgPool}; + use sqlx::PgPool; + /// 数据类型 + #[derive( + sqlx::Type, + serde::Serialize, + serde::Deserialize, + Clone, + Copy, + Debug, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + )] + #[repr(i32)] + pub enum DataType { + /// 数据类型未知 + Unknown = 0, + /// 电子地图数据 + Em = 1, + /// ISCS数据 + Iscs = 4, + } + /// 数据类型 + #[derive( + sqlx::Type, + serde::Serialize, + serde::Deserialize, + Clone, + Copy, + Debug, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + )] + #[repr(i32)] + pub enum IscsStyle { + /// 数据类型未知 + Unknown = 0, + /// 电子地图数据 + Style1 = 1, + } #[derive(Debug, Serialize, Deserialize)] pub struct IscsDataOptions { pub style: IscsStyle, @@ -451,26 +495,15 @@ mod tests { async fn basic_use_test(pool: PgPool) -> Result<(), DbAccessError> { rtsa_log::Logging::default().with_level(Level::DEBUG).init(); let accessor = crate::db_access::RtsaDbAccessor::new(pool); - // 同步10个用户 - let mut users = vec![]; + // 注册10个用户 for i in 0..10 { - let user = SyncUserInfo { - id: i + 1, - name: format!("user{}", i + 1), - password: "password".to_string(), - roles: serde_json::to_value(vec![Role::User]).unwrap(), - email: None, - mobile: None, - created_at: Local::now(), - updated_at: None, - }; - users.push(user); + let user = RegisterUser::new(&format!("test{}", i), "123456"); + accessor.register_user(user).await?; } - accessor.sync_user(users.as_slice()).await?; // 创建草稿数据测试 let res = accessor - .create_draft_data(CreateDraftData::new("test", DataType::Em, 10)) + .create_draft_data(CreateDraftData::new("test", DataType::Em as i32, 10)) .await?; println!("res: {:?}", res); @@ -479,11 +512,11 @@ mod tests { // 重复创建测试 let repeat_create_result = accessor - .create_draft_data(CreateDraftData::new("test", DataType::Em, 10)) + .create_draft_data(CreateDraftData::new("test", DataType::Em as i32, 10)) .await; if let Some(e) = repeat_create_result.err() { match e { - DbAccessError::RowExist => { + DbAccessError::RowExist(_) => { println!("repeat create test pass"); } _ => { @@ -551,10 +584,10 @@ mod tests { if i == 1 { let draft = accessor .create_draft_data( - CreateDraftData::new(&format!("test{}", j), DataType::Iscs, i) + CreateDraftData::new(&format!("test{}", j), DataType::Iscs as i32, i) .with_options( serde_json::to_value(IscsDataOptions { - style: IscsStyle::DaShiZhiNeng, + style: IscsStyle::Style1, }) .unwrap(), ), @@ -566,7 +599,7 @@ mod tests { accessor .create_draft_data(CreateDraftData::new( &format!("test{}", j), - DataType::Em, + DataType::Em as i32, i, )) .await?; @@ -579,7 +612,7 @@ mod tests { DraftDataQuery::new() .with_user_id(2) .with_name("test".to_string()) - .with_data_type(DataType::Em), + .with_data_type(DataType::Em as i32), PageQuery::new(1, 10), ) .await?; @@ -604,7 +637,7 @@ mod tests { .paging_query_draft_data( DraftDataQuery::new().with_options( serde_json::to_value(IscsDataOptions { - style: IscsStyle::DaShiZhiNeng, + style: IscsStyle::Style1, }) .unwrap(), ), diff --git a/crates/rtsa_db/src/db_access/feature.rs b/crates/rtsa_db/src/db_access/feature.rs index a48f7b4..d5f11be 100644 --- a/crates/rtsa_db/src/db_access/feature.rs +++ b/crates/rtsa_db/src/db_access/feature.rs @@ -2,7 +2,7 @@ use rtsa_dto::common::FeatureType; use serde_json::Value; use crate::{ - common::{PageQuery, PageResult, TableColumn}, + common::{PageData, PageQuery, TableColumn}, model::{FeatureColumn, FeatureModel}, DbAccessError, }; @@ -27,7 +27,7 @@ pub trait FeatureAccessor { &self, page: PageQuery, filter: &FeaturePagingFilter, - ) -> Result, DbAccessError>; + ) -> Result, DbAccessError>; /// 获取id对应的功能特性 async fn get_feature(&self, id: i32) -> Result; /// 根据id列表获取功能特性基本信息 @@ -102,7 +102,7 @@ impl FeatureAccessor for RtsaDbAccessor { &self, page: PageQuery, filter: &FeaturePagingFilter, - ) -> Result, DbAccessError> { + ) -> Result, DbAccessError> { let table = FeatureColumn::Table.name(); let where_clause = filter.to_where_clause(); let total = @@ -110,7 +110,7 @@ impl FeatureAccessor for RtsaDbAccessor { .fetch_one(&self.pool) .await?; if total == 0 { - return Ok(PageResult::new(total, vec![])); + return Ok(PageData::new(total, vec![])); } let update_at_column = FeatureColumn::UpdatedAt.name(); let select_columns = [ @@ -132,7 +132,7 @@ impl FeatureAccessor for RtsaDbAccessor { let rows = sqlx::query_as::<_, FeatureModel>(&sql) .fetch_all(&self.pool) .await?; - Ok(PageResult::new(total, rows)) + Ok(PageData::new(total, rows)) } async fn get_feature(&self, id: i32) -> Result { @@ -217,11 +217,11 @@ impl FeaturePagingFilter { #[cfg(test)] mod tests { - use rtsa_dto::common::Role; - use rtsa_log::tracing::Level; - use sqlx::{types::chrono::Local, PgPool}; - use crate::{SyncUserInfo, UserAccessor}; + use rtsa_log::tracing::Level; + use sqlx::PgPool; + + use crate::{RegisterUser, UserAccessor}; use super::*; @@ -229,22 +229,11 @@ mod tests { async fn test_basic_use(pool: PgPool) -> Result<(), DbAccessError> { rtsa_log::Logging::default().with_level(Level::DEBUG).init(); let accessor = crate::db_access::RtsaDbAccessor::new(pool); - // 同步10个用户 - let mut users = vec![]; + // 注册10个用户 for i in 0..10 { - let user = SyncUserInfo { - id: i + 1, - name: format!("user{}", i + 1), - password: "password".to_string(), - roles: serde_json::to_value(vec![Role::User]).unwrap(), - email: None, - mobile: None, - created_at: Local::now(), - updated_at: None, - }; - users.push(user); + let user = RegisterUser::new(&format!("test{}", i), "123456"); + accessor.register_user(user).await?; } - accessor.sync_user(users.as_slice()).await?; // 创建测试 let creator_id = 1; diff --git a/crates/rtsa_db/src/db_access/mod.rs b/crates/rtsa_db/src/db_access/mod.rs index ea9e871..adf2b26 100644 --- a/crates/rtsa_db/src/db_access/mod.rs +++ b/crates/rtsa_db/src/db_access/mod.rs @@ -10,6 +10,10 @@ pub use user::*; mod feature; pub use feature::*; use lazy_static::lazy_static; +mod org; +pub use org::*; +mod org_user; +pub use org_user::*; use crate::{model::MqttClientIdSeq, DbAccessError}; @@ -28,6 +32,16 @@ pub async fn init_default_db_accessor(url: &str) { *rda = Some(accessor); } +/// 设置全局默认数据库访问器 +pub fn set_default_db_accessor(accessor: RtsaDbAccessor) { + if RDA.lock().unwrap().is_some() { + error!("数据库访问器已初始化,请勿重复初始化"); + return; + } + let mut rda = RDA.lock().unwrap(); + *rda = Some(accessor); +} + /// 获取默认数据库访问器 pub fn get_default_db_accessor() -> RtsaDbAccessor { let rda = RDA.lock().unwrap(); diff --git a/crates/rtsa_db/src/db_access/org.rs b/crates/rtsa_db/src/db_access/org.rs new file mode 100644 index 0000000..abaab4a --- /dev/null +++ b/crates/rtsa_db/src/db_access/org.rs @@ -0,0 +1,669 @@ +use serde_json::Value; + +use crate::{ + common::{PageData, PageQuery, TableColumn}, + model::{OrganizationColumn, OrganizationModel}, + DbAccessError, +}; + +use super::RtsaDbAccessor; + +/// 组织数据管理 +#[allow(async_fn_in_trait)] +pub trait OrgAccessor { + /// 组织code是否存在 + async fn is_org_code_exist(&self, code: &str) -> Result; + /// 创建组织 + async fn create_org(&self, create: CreateOrg) -> Result; + /// 获取组织 + async fn query_org(&self, id: i32) -> Result; + /// 根据id列表获取组织name + async fn query_org_names(&self, ids: &[i32]) -> Result, DbAccessError>; + /// 通过code获取组织 + async fn query_org_by_code(&self, code: &str) -> Result; + /// 分页获取顶级组织 + async fn query_org_top_page( + &self, + page: PageQuery, + filter: OrgTopFilter, + ) -> Result, DbAccessError>; + /// 获取所有顶级组织 + async fn query_org_top(&self) -> Result, DbAccessError>; + /// 获取某组织的顶级组织 + async fn query_org_top_of_org(&self, org_id: i32) -> Result; + /// 获取所有下一级子组织 + async fn query_org_children( + &self, + parent_id: i32, + ) -> Result, DbAccessError>; + /// 通过id列表获取组织code和name + async fn query_org_name_by_ids( + &self, + ids: Vec, + ) -> Result, DbAccessError>; + /// 更新组织名称 + async fn update_org_name( + &self, + id: i32, + name: &str, + updater_id: i32, + ) -> Result; + /// 更新组织配置 + async fn update_org_config( + &self, + id: i32, + config: Value, + updater_id: i32, + ) -> Result; + /// 删除组织 + /// 警告:删除组织会关联删除组织关联的所有数据 + async fn delete_org(&self, id: i32) -> Result<(), DbAccessError>; +} + +pub struct CreateOrg { + pub parent_id: Option, + pub code: Option, + pub name: String, + pub config: Option, + pub creator_id: i32, +} + +impl CreateOrg { + pub fn new(name: &str, creator_id: i32) -> CreateOrg { + CreateOrg { + parent_id: None, + code: None, + name: name.to_string(), + config: None, + creator_id, + } + } + + pub fn with_parent_id(mut self, parent_id: i32) -> Self { + self.parent_id = Some(parent_id); + self + } + + pub fn with_code(mut self, code: &str) -> Self { + self.code = Some(code.to_string()); + self + } + + pub fn with_config(mut self, config: Value) -> Self { + self.config = Some(config); + self + } +} + +pub struct OrgTopFilter { + pub code: Option, + pub name: Option, +} + +impl OrgTopFilter { + pub fn to_where_clause(&self) -> String { + let mut clauses = vec![]; + let code_column = OrganizationColumn::Id.name(); + let name_column = OrganizationColumn::Name.name(); + if let Some(code) = &self.code { + clauses.push(format!("{} = '{}'", code_column, code)); + } + if let Some(name) = &self.name { + clauses.push(format!("{} = '{}'", name_column, name)); + } + if clauses.is_empty() { + "".to_string() + } else { + format!("WHERE {}", clauses.join(" AND ")) + } + } +} + +impl OrgAccessor for RtsaDbAccessor { + async fn is_org_code_exist(&self, code: &str) -> Result { + let table = OrganizationColumn::Table.name(); + let code_column = OrganizationColumn::Code.name(); + let sql = format!( + "SELECT COUNT(1) FROM {table} WHERE {code_column} = $1", + table = table, + code_column = code_column + ); + let count: i64 = sqlx::query_scalar(&sql) + .bind(code) + .fetch_one(&self.pool) + .await?; + Ok(count > 0) + } + + async fn create_org(&self, create: CreateOrg) -> Result { + let table = OrganizationColumn::Table.name(); + let parent_id_column = OrganizationColumn::ParentId.name(); + let code_column = OrganizationColumn::Code.name(); + let name_column = OrganizationColumn::Name.name(); + let config_column = OrganizationColumn::Config.name(); + let creator_id_column = OrganizationColumn::CreatorId.name(); + let updater_id_column = OrganizationColumn::UpdaterId.name(); + let sql = format!( + "INSERT INTO {table} ({parent_id_column}, {code_column}, {name_column}, {config_column}, {creator_id_column}, {updater_id_column}) VALUES ($1, $2, $3, $4, $5, $5) RETURNING *", + table = table, + parent_id_column = parent_id_column, + code_column = code_column, + name_column = name_column, + config_column = config_column, + creator_id_column = creator_id_column, + updater_id_column = updater_id_column + ); + let row = sqlx::query_as::<_, OrganizationModel>(&sql) + .bind(create.parent_id) + .bind(create.code) + .bind(create.name) + .bind(create.config) + .bind(create.creator_id) + .bind(create.creator_id) + .fetch_one(&self.pool) + .await?; + Ok(row) + } + + async fn query_org(&self, id: i32) -> Result { + let table = OrganizationColumn::Table.name(); + let id_column = OrganizationColumn::Id.name(); + let sql = format!( + "SELECT * FROM {table} WHERE {id_column} = $1", + table = table, + id_column = id_column + ); + let row = sqlx::query_as::<_, OrganizationModel>(&sql) + .bind(id) + .fetch_one(&self.pool) + .await?; + Ok(row) + } + + async fn query_org_names(&self, ids: &[i32]) -> Result, DbAccessError> { + let table = OrganizationColumn::Table.name(); + let id_column = OrganizationColumn::Id.name(); + let name_column = OrganizationColumn::Name.name(); + let sql = format!( + "SELECT {id_column}, {name_column} FROM {table} WHERE {id_column} = ANY($1)", + id_column = id_column, + name_column = name_column, + table = table + ); + let rows: Vec<(i32, String)> = sqlx::query_as(&sql).bind(ids).fetch_all(&self.pool).await?; + Ok(rows) + } + + async fn query_org_by_code(&self, code: &str) -> Result { + let table = OrganizationColumn::Table.name(); + let code_column = OrganizationColumn::Code.name(); + let sql = format!( + "SELECT * FROM {table} WHERE {code_column} = $1", + table = table, + code_column = code_column + ); + let row = sqlx::query_as::<_, OrganizationModel>(&sql) + .bind(code) + .fetch_one(&self.pool) + .await?; + Ok(row) + } + + async fn query_org_top_page( + &self, + page: PageQuery, + filter: OrgTopFilter, + ) -> Result, DbAccessError> { + let table = OrganizationColumn::Table.name(); + let id_column = OrganizationColumn::Id.name(); + let where_clause = filter.to_where_clause(); + let count_clause = format!("SELECT COUNT({id_column}) FROM {table} {where_clause}"); + let total: i64 = sqlx::query_scalar(&count_clause) + .fetch_one(&self.pool) + .await?; + if total == 0 { + return Ok(PageData::new(total, vec![])); + } + let limit_clause = page.to_limit_clause(); + let sql = format!( + "SELECT * FROM {table} {where_clause} ORDER BY {id_column} {limit_clause}", + table = table, + where_clause = where_clause, + id_column = id_column, + limit_clause = limit_clause + ); + let rows = sqlx::query_as::<_, OrganizationModel>(&sql) + .fetch_all(&self.pool) + .await?; + Ok(PageData::new(total, rows)) + } + + async fn query_org_top(&self) -> Result, DbAccessError> { + let table = OrganizationColumn::Table.name(); + let parent_id_column = OrganizationColumn::ParentId.name(); + let sql = format!( + "SELECT * FROM {table} WHERE {parent_id_column} IS NULL", + table = table, + parent_id_column = parent_id_column + ); + let rows = sqlx::query_as::<_, OrganizationModel>(&sql) + .fetch_all(&self.pool) + .await?; + Ok(rows) + } + + async fn query_org_top_of_org(&self, org_id: i32) -> Result { + let table = OrganizationColumn::Table.name(); + let id_column = OrganizationColumn::Id.name(); + let parent_id_column = OrganizationColumn::ParentId.name(); + let sql = format!( + "WITH RECURSIVE org_tree AS ( + SELECT * FROM {table} WHERE {id_column} = $1 + UNION ALL + SELECT o.* FROM {table} o JOIN org_tree t ON o.{id_column} = t.{parent_id_column} + ) + SELECT * FROM org_tree WHERE {parent_id_column} IS NULL", + table = table, + id_column = id_column, + parent_id_column = parent_id_column + ); + let row = sqlx::query_as::<_, OrganizationModel>(&sql) + .bind(org_id) + .fetch_one(&self.pool) + .await?; + Ok(row) + } + + async fn query_org_children( + &self, + parent_id: i32, + ) -> Result, DbAccessError> { + let table = OrganizationColumn::Table.name(); + let parent_id_column = OrganizationColumn::ParentId.name(); + let sql = format!( + "SELECT * FROM {table} WHERE {parent_id_column} = $1", + table = table, + parent_id_column = parent_id_column + ); + let rows = sqlx::query_as::<_, OrganizationModel>(&sql) + .bind(parent_id) + .fetch_all(&self.pool) + .await?; + Ok(rows) + } + + async fn query_org_name_by_ids( + &self, + ids: Vec, + ) -> Result, DbAccessError> { + let table = OrganizationColumn::Table.name(); + let id_column = OrganizationColumn::Id.name(); + let name_column = OrganizationColumn::Name.name(); + let sql = format!( + "SELECT {id_column}, {name_column} FROM {table} WHERE {id_column} = ANY($1)", + id_column = id_column, + name_column = name_column, + table = table + ); + let rows: Vec<(i32, String)> = sqlx::query_as(&sql).bind(ids).fetch_all(&self.pool).await?; + Ok(rows) + } + + async fn update_org_name( + &self, + id: i32, + name: &str, + updater_id: i32, + ) -> Result { + let table = OrganizationColumn::Table.name(); + let id_column = OrganizationColumn::Id.name(); + let name_column = OrganizationColumn::Name.name(); + let updater_id_column = OrganizationColumn::UpdaterId.name(); + let updated_at_column = OrganizationColumn::UpdatedAt.name(); + let sql = format!( + "UPDATE {table} SET {name_column} = $1, {updater_id_column} = $2, {updated_at_column} = 'now()' WHERE {id_column} = $3 RETURNING *", + table = table, + name_column = name_column, + updater_id_column = updater_id_column, + updated_at_column = updated_at_column, + id_column = id_column + ); + let row = sqlx::query_as::<_, OrganizationModel>(&sql) + .bind(name) + .bind(updater_id) + .bind(id) + .fetch_one(&self.pool) + .await?; + Ok(row) + } + + async fn update_org_config( + &self, + id: i32, + config: Value, + updater_id: i32, + ) -> Result { + let table = OrganizationColumn::Table.name(); + let id_column = OrganizationColumn::Id.name(); + let config_column = OrganizationColumn::Config.name(); + let updater_id_column = OrganizationColumn::UpdaterId.name(); + let updated_at_column = OrganizationColumn::UpdatedAt.name(); + let sql = format!( + "UPDATE {table} SET {config_column} = $1, {updater_id_column} = $2, {updated_at_column} = 'now()' WHERE {id_column} = $3 RETURNING *", + table = table, + config_column = config_column, + updater_id_column = updater_id_column, + updated_at_column = updated_at_column, + id_column = id_column + ); + let row = sqlx::query_as::<_, OrganizationModel>(&sql) + .bind(config) + .bind(updater_id) + .bind(id) + .fetch_one(&self.pool) + .await?; + Ok(row) + } + + async fn delete_org(&self, id: i32) -> Result<(), DbAccessError> { + let table = OrganizationColumn::Table.name(); + let id_column = OrganizationColumn::Id.name(); + let sql = format!( + "DELETE FROM {table} WHERE {id_column} = $1", + table = table, + id_column = id_column + ); + sqlx::query(&sql).bind(id).execute(&self.pool).await?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use sqlx::PgPool; + + use super::*; + use crate::{db_access::UserAccessor, model::UserModel, RegisterUser}; + + async fn init_user(accessor: &RtsaDbAccessor) -> Result { + let new_user = RegisterUser::new("test_user", "test_password"); + let user = accessor.register_user(new_user).await?; + Ok(user) + } + + #[sqlx::test(migrator = "crate::MIGRATOR")] + async fn test_create_org(pool: PgPool) -> Result<(), DbAccessError> { + let accessor = RtsaDbAccessor::new(pool); + // 初始化用户 + let user = init_user(&accessor).await?; + + let create = CreateOrg { + parent_id: None, + code: Some("test".to_string()), + name: "测试".to_string(), + config: None, + creator_id: user.id, + }; + let org = accessor.create_org(create).await?; + assert_eq!(org.code, Some("test".to_string())); + assert_eq!(org.name, "测试"); + assert_eq!(org.creator_id, user.id); + assert_eq!(org.updater_id, user.id); + assert!(org.parent_id.is_none()); + assert!(org.config.is_none()); + Ok(()) + } + + #[sqlx::test(migrator = "crate::MIGRATOR")] + async fn test_is_org_code_exist(pool: PgPool) -> Result<(), DbAccessError> { + let accessor = RtsaDbAccessor::new(pool); + let user = init_user(&accessor).await?; + + // Create a new organization + let create = CreateOrg { + parent_id: None, + code: Some("exist_code".to_string()), + name: "存在组织".to_string(), + config: None, + creator_id: user.id, + }; + accessor.create_org(create).await?; + + // Check if the organization code exists + let exists = accessor.is_org_code_exist("exist_code").await?; + assert!(exists); + + // Check for a non-existent code + let not_exists = accessor.is_org_code_exist("nonexistent_code").await?; + assert!(!not_exists); + + Ok(()) + } + + #[sqlx::test(migrator = "crate::MIGRATOR")] + async fn test_get_org_by_code(pool: PgPool) -> Result<(), DbAccessError> { + let accessor = RtsaDbAccessor::new(pool); + let user = init_user(&accessor).await?; + + // Create a new organization + let create = CreateOrg { + parent_id: None, + code: Some("get_code_test".to_string()), + name: "通过代码获取组织".to_string(), + config: None, + creator_id: user.id, + }; + accessor.create_org(create).await?; + + // Retrieve the organization by code + let org = accessor.query_org_by_code("get_code_test").await?; + assert_eq!(org.code, Some("get_code_test".to_string())); + assert_eq!(org.name, "通过代码获取组织"); + + Ok(()) + } + + #[sqlx::test(migrator = "crate::MIGRATOR")] + async fn test_query_org_top(pool: PgPool) -> Result<(), DbAccessError> { + let accessor = RtsaDbAccessor::new(pool); + let user = init_user(&accessor).await?; + + // Create multiple top-level organizations + for i in 1..4 { + let create = CreateOrg { + parent_id: None, + code: Some(format!("top_org_{}", i)), + name: format!("顶级组织{}", i), + config: None, + creator_id: user.id, + }; + let top = accessor.create_org(create).await?; + // Create child organization + let child = CreateOrg { + parent_id: Some(top.id), + code: None, + name: format!("下级组织{}", i), + config: None, + creator_id: user.id, + }; + accessor.create_org(child).await?; + } + + // Query top-level organizations + let orgs = accessor.query_org_top().await?; + assert_eq!(orgs.len(), 3); + + Ok(()) + } + + #[sqlx::test(migrator = "crate::MIGRATOR")] + async fn test_query_org_top_of_org(pool: PgPool) -> Result<(), DbAccessError> { + let accessor = RtsaDbAccessor::new(pool); + let user = init_user(&accessor).await?; + + // Create a parent organization + let parent_create = CreateOrg { + parent_id: None, + code: Some("parent_org".to_string()), + name: "父组织".to_string(), + config: None, + creator_id: user.id, + }; + let parent_org = accessor.create_org(parent_create).await?; + + // Create a child organization + let child_create = CreateOrg::new("一级子组织", user.id).with_parent_id(parent_org.id); + let child = accessor.create_org(child_create).await?; + + // Create a grandchild organization + let grandchild_create = CreateOrg::new("二级子组织", user.id).with_parent_id(parent_org.id); + let grandchild = accessor.create_org(grandchild_create).await?; + + // Query the top-level organization of the child organization + let top_org = accessor.query_org_top_of_org(grandchild.id).await?; + assert_eq!(top_org.id, parent_org.id); + + let top_org = accessor.query_org_top_of_org(child.id).await?; + assert_eq!(top_org.id, parent_org.id); + + let top_org = accessor.query_org_top_of_org(parent_org.id).await?; + assert_eq!(top_org.id, parent_org.id); + + Ok(()) + } + + #[sqlx::test(migrator = "crate::MIGRATOR")] + async fn test_query_org_children(pool: PgPool) -> Result<(), DbAccessError> { + let accessor = RtsaDbAccessor::new(pool); + let user = init_user(&accessor).await?; + + // Create a parent organization + let parent_create = CreateOrg { + parent_id: None, + code: Some("parent_org".to_string()), + name: "父组织".to_string(), + config: None, + creator_id: user.id, + }; + let parent_org = accessor.create_org(parent_create).await?; + + // Create child organizations + for i in 1..3 { + let create = CreateOrg { + parent_id: Some(parent_org.id), + code: Some(format!("child_org_{}", i)), + name: format!("子组织{}", i), + config: None, + creator_id: user.id, + }; + accessor.create_org(create).await?; + } + + // Query child organizations + let children = accessor.query_org_children(parent_org.id).await?; + assert_eq!(children.len(), 2); + + Ok(()) + } + + #[sqlx::test(migrator = "crate::MIGRATOR")] + async fn test_update_org_name(pool: PgPool) -> Result<(), DbAccessError> { + let accessor = RtsaDbAccessor::new(pool); + let user = init_user(&accessor).await?; + + // Create an organization + let create = CreateOrg { + parent_id: None, + code: Some("update_name_test".to_string()), + name: "旧名称".to_string(), + config: None, + creator_id: user.id, + }; + let org = accessor.create_org(create).await?; + + // Update the organization's name + let updated_org = accessor.update_org_name(org.id, "新名称", user.id).await?; + assert_eq!(updated_org.name, "新名称"); + + Ok(()) + } + + #[sqlx::test(migrator = "crate::MIGRATOR")] + async fn test_update_org_config(pool: PgPool) -> Result<(), DbAccessError> { + let accessor = RtsaDbAccessor::new(pool); + let user = init_user(&accessor).await?; + + // Create an organization + let create = CreateOrg { + parent_id: None, + code: Some("update_config_test".to_string()), + name: "组织配置测试".to_string(), + config: None, + creator_id: user.id, + }; + let org = accessor.create_org(create).await?; + + // Update the organization's config + let new_config = serde_json::json!({ "key": "value" }); + let updated_org = accessor + .update_org_config(org.id, new_config.clone(), user.id) + .await?; + assert_eq!( + updated_org.config, + Some(serde_json::to_value(new_config).unwrap()) + ); + + Ok(()) + } + + #[sqlx::test(migrator = "crate::MIGRATOR")] + async fn test_delete_org(pool: PgPool) -> Result<(), DbAccessError> { + let accessor = RtsaDbAccessor::new(pool); + let user = init_user(&accessor).await?; + + // Create an organization + let create = CreateOrg { + parent_id: None, + code: Some("delete_test".to_string()), + name: "删除组织测试".to_string(), + config: None, + creator_id: user.id, + }; + let org = accessor.create_org(create).await?; + + // Delete the organization + accessor.delete_org(org.id).await?; + + // Attempt to retrieve the deleted organization + let result = accessor.query_org(org.id).await; + assert!(result.is_err()); + + Ok(()) + } + + #[sqlx::test(migrator = "crate::MIGRATOR")] + async fn test_query_org_name_by_ids(pool: PgPool) -> Result<(), DbAccessError> { + let accessor = RtsaDbAccessor::new(pool); + let user = init_user(&accessor).await?; + + // Create organizations + let mut ids = vec![]; + for i in 1..4 { + let create = CreateOrg { + parent_id: None, + code: Some(format!("query_name_test_{}", i)), + name: format!("查询名称{}", i), + config: None, + creator_id: user.id, + }; + let org = accessor.create_org(create).await?; + ids.push(org.id); + } + + // Query organization names by ids + let names = accessor.query_org_name_by_ids(ids).await?; + assert_eq!(names.len(), 3); + + Ok(()) + } +} diff --git a/crates/rtsa_db/src/db_access/org_user.rs b/crates/rtsa_db/src/db_access/org_user.rs new file mode 100644 index 0000000..d61a26d --- /dev/null +++ b/crates/rtsa_db/src/db_access/org_user.rs @@ -0,0 +1,770 @@ +use serde_json::Value; +use sqlx::Postgres; + +use super::RtsaDbAccessor; +use super::{OrgAccessor, RegisterUser, UserAccessor}; +use crate::password_util::verify_password; +use crate::{ + common::{PageData, PageQuery, TableColumn}, + model::{OrganizationUserColumn, OrganizationUserModel, UserModel}, + DbAccessError, +}; + +/// 组织用户管理 +#[allow(async_fn_in_trait)] +pub trait OrgUserAccessor { + /// 创建组织用户 + async fn create_org_user( + &self, + create: CreateOrgUser, + ) -> Result<(OrganizationUserModel, UserModel), DbAccessError>; + /// 绑定组织用户 + async fn bind_org_user( + &self, + bind: BindOrgUser, + ) -> Result; + /// 解绑(删除)组织用户 + async fn unbind_org_user(&self, org_id: i32, user_id: i32) -> Result<(), DbAccessError>; + /// 删除组织用户 + async fn delete_org_user(&self, id: i32) -> Result<(), DbAccessError>; + /// 更新组织用户学工号 + async fn update_org_user_student_id( + &self, + id: i32, + student_id: &str, + updater_id: i32, + ) -> Result; + /// 更新组织用户角色 + async fn update_org_user_roles( + &self, + id: i32, + roles: Value, + updater_id: i32, + ) -> Result; + /// 更新组织用户信息 + async fn update_org_user_info( + &self, + id: i32, + info: Value, + updater_id: i32, + ) -> Result; + /// 查询组织用户登陆 + /// username: 学工号/用户名/邮箱/手机号 + async fn query_org_user_login( + &self, + org_id: i32, + username: &str, + password: &str, + ) -> Result<(OrganizationUserModel, UserModel), DbAccessError>; + /// 获取组织用户 + async fn query_org_user( + &self, + org_id: i32, + user_id: i32, + ) -> Result; + /// 组织用户是否存在 + async fn is_org_user_exist(&self, org_id: i32, user_id: i32) -> Result; + /// 获取组织用户 + async fn query_org_user_by_student_id( + &self, + org_id: i32, + student_id: &str, + ) -> Result; + /// 获取组织用户 + async fn query_org_user_by_id(&self, id: i32) -> Result; + /// 分页查询组织用户 + async fn query_org_user_page( + &self, + page: PageQuery, + filter: OrgUserFilter, + ) -> Result, DbAccessError>; +} + +#[derive(Debug, Clone, Default)] +pub struct OrgUserFilter { + pub org_id: Option, + pub student_id: Option, +} + +impl OrgUserFilter { + fn to_where_clause(&self) -> String { + let mut where_clause = vec![]; + let org_id_col = OrganizationUserColumn::OrganizationId.name(); + let student_id_col = OrganizationUserColumn::StudentId.name(); + if let Some(org_id) = self.org_id { + where_clause.push(format!("{org_id_col} = {org_id}")); + } + if let Some(student_id) = &self.student_id { + where_clause.push(format!("{student_id_col} LIKE '%{student_id}%'")); + } + let where_clause = where_clause.join(" AND "); + format!("WHERE {}", where_clause) + } +} + +pub struct BindOrgUser { + pub org_id: i32, + pub user_id: i32, + pub student_id: Option, + pub roles: Value, + pub info: Value, + pub creator_id: i32, +} + +impl BindOrgUser { + pub fn new(org_id: i32, user_id: i32, creator_id: i32) -> Self { + Self { + org_id, + user_id, + student_id: None, + roles: Value::Array(Default::default()), + info: Value::Object(Default::default()), + creator_id, + } + } + + pub fn with_student_id(mut self, student_id: &str) -> Self { + self.student_id = Some(student_id.to_string()); + self + } + + pub fn with_roles(mut self, roles: Value) -> Self { + self.roles = roles; + self + } + + pub fn with_info(mut self, info: Value) -> Self { + self.info = info; + self + } +} + +#[derive(Debug, Clone)] +pub struct CreateOrgUser { + pub org_code: String, + pub student_id: String, + pub name: String, + pub password: String, + pub roles: Value, + pub info: Value, + pub email: Option, + pub mobile: Option, + pub creator_id: i32, +} + +impl CreateOrgUser { + pub fn new( + org_code: &str, + student_id: &str, + name: &str, + password: &str, + creator_id: i32, + ) -> Self { + Self { + org_code: org_code.to_string(), + student_id: student_id.to_string(), + name: name.to_string(), + password: password.to_string(), + roles: Value::Array(Default::default()), + info: Value::Object(Default::default()), + email: None, + mobile: None, + creator_id, + } + } + + pub fn nickname(&self) -> String { + self.name.clone() + } + + pub fn build_username(&self) -> String { + format!("{}@{}", self.student_id, self.org_code) + } + + pub fn with_roles(mut self, roles: Value) -> Self { + self.roles = roles; + self + } + + pub fn with_email(mut self, email: String) -> Self { + self.email = Some(email); + self + } + + pub fn with_mobile(mut self, mobile: String) -> Self { + self.mobile = Some(mobile); + self + } +} + +impl RtsaDbAccessor { + async fn insert_org_user<'e, 'c: 'e, E>( + &self, + bind: BindOrgUser, + executor: E, + ) -> Result + where + E: 'e + sqlx::Executor<'c, Database = Postgres>, + { + let table = OrganizationUserColumn::Table.name(); + let org_id_col = OrganizationUserColumn::OrganizationId.name(); + let user_id_col = OrganizationUserColumn::UserId.name(); + let student_id_col = OrganizationUserColumn::StudentId.name(); + let roles_col = OrganizationUserColumn::Roles.name(); + let info_col = OrganizationUserColumn::Info.name(); + let creator_id_col = OrganizationUserColumn::CreatorId.name(); + let updater_id_col = OrganizationUserColumn::UpdaterId.name(); + let insert_clause = format!( + "INSERT INTO {table} ({org_id_col}, {user_id_col}, {student_id_col}, {roles_col}, {info_col}, {creator_id_col}, {updater_id_col}) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *", + ); + let insert = sqlx::query_as(&insert_clause) + .bind(bind.org_id) + .bind(bind.user_id) + .bind(bind.student_id) + .bind(bind.roles) + .bind(bind.info) + .bind(bind.creator_id) + .bind(bind.creator_id) + .fetch_one(executor) + .await?; + Ok(insert) + } +} + +impl OrgUserAccessor for RtsaDbAccessor { + async fn is_org_user_exist(&self, org_id: i32, user_id: i32) -> Result { + let table = OrganizationUserColumn::Table.name(); + let org_id_col = OrganizationUserColumn::OrganizationId.name(); + let user_id_col = OrganizationUserColumn::UserId.name(); + let query_clause = + format!("SELECT COUNT(*) FROM {table} WHERE {org_id_col} = $1 AND {user_id_col} = $2",); + let count: i64 = sqlx::query_scalar(&query_clause) + .bind(org_id) + .bind(user_id) + .fetch_one(&self.pool) + .await?; + Ok(count > 0) + } + async fn create_org_user( + &self, + create: CreateOrgUser, + ) -> Result<(OrganizationUserModel, UserModel), DbAccessError> { + let org = self.query_org_by_code(&create.org_code).await?; + let mut tx = self.pool.begin().await?; + // 检查用户是否存在 + let org_user_exist = self.is_user_name_exist(&create.build_username()).await?; + if org_user_exist { + return Err(DbAccessError::RowExist(format!( + "User {} already exists", + create.build_username() + ))); + } + // 创建用户 + let register = RegisterUser::new(&create.build_username(), &create.password) + .with_nickname(&create.name); + let user = self.insert_user(register, &mut *tx).await?; + // 创建组织用户 + let bind = BindOrgUser::new(org.id, user.id, create.creator_id) + .with_student_id(&create.student_id) + .with_roles(create.roles) + .with_info(create.info); + let org_user = self.insert_org_user(bind, &mut *tx).await?; + tx.commit().await?; + Ok((org_user, user)) + } + + async fn bind_org_user( + &self, + bind: BindOrgUser, + ) -> Result { + let org = self.query_org(bind.org_id).await?; + let user = self.query_user(bind.user_id).await?; + // 检查用户是否已绑定 + let org_user_exist = self.is_org_user_exist(bind.org_id, bind.user_id).await?; + if org_user_exist { + return Err(DbAccessError::RowExist(format!( + "User {} already bind to organization {}", + user.username, org.name + ))); + } + // 创建组织用户 + let org_user = self.insert_org_user(bind, &self.pool).await?; + Ok(org_user) + } + + async fn unbind_org_user(&self, org_id: i32, user_id: i32) -> Result<(), DbAccessError> { + let table = OrganizationUserColumn::Table.name(); + let org_id_col = OrganizationUserColumn::OrganizationId.name(); + let user_id_col = OrganizationUserColumn::UserId.name(); + let delete_clause = + format!("DELETE FROM {table} WHERE {org_id_col} = $1 AND {user_id_col} = $2",); + sqlx::query(&delete_clause) + .bind(org_id) + .bind(user_id) + .execute(&self.pool) + .await?; + Ok(()) + } + + async fn delete_org_user(&self, id: i32) -> Result<(), DbAccessError> { + let table = OrganizationUserColumn::Table.name(); + let id_col = OrganizationUserColumn::Id.name(); + let delete_clause = format!("DELETE FROM {table} WHERE {id_col} = $1",); + sqlx::query(&delete_clause) + .bind(id) + .execute(&self.pool) + .await?; + Ok(()) + } + + async fn update_org_user_student_id( + &self, + id: i32, + student_id: &str, + updater_id: i32, + ) -> Result { + let table = OrganizationUserColumn::Table.name(); + let id_col = OrganizationUserColumn::Id.name(); + let student_id_col = OrganizationUserColumn::StudentId.name(); + let updater_id_col = OrganizationUserColumn::UpdaterId.name(); + let updated_at_col = OrganizationUserColumn::UpdatedAt.name(); + let update_clause = + format!("UPDATE {table} SET {student_id_col} = $1, {updater_id_col} = $2, {updated_at_col} = 'now()' WHERE {id_col} = $3 RETURNING *",); + let update = sqlx::query_as(&update_clause) + .bind(student_id) + .bind(updater_id) + .bind(id) + .fetch_one(&self.pool) + .await?; + Ok(update) + } + + async fn update_org_user_roles( + &self, + id: i32, + roles: Value, + updater_id: i32, + ) -> Result { + let table = OrganizationUserColumn::Table.name(); + let id_col = OrganizationUserColumn::Id.name(); + let roles_col = OrganizationUserColumn::Roles.name(); + let updater_id_col = OrganizationUserColumn::UpdaterId.name(); + let updated_at_col = OrganizationUserColumn::UpdatedAt.name(); + let update_clause = + format!("UPDATE {table} SET {roles_col} = $1, {updater_id_col} = $2, {updated_at_col} = 'now()' WHERE {id_col} = $3 RETURNING *",); + let update = sqlx::query_as(&update_clause) + .bind(roles) + .bind(updater_id) + .bind(id) + .fetch_one(&self.pool) + .await?; + Ok(update) + } + + async fn update_org_user_info( + &self, + id: i32, + info: Value, + updater_id: i32, + ) -> Result { + let table = OrganizationUserColumn::Table.name(); + let id_col = OrganizationUserColumn::Id.name(); + let info_col = OrganizationUserColumn::Info.name(); + let updater_id_col = OrganizationUserColumn::UpdaterId.name(); + let updated_at_col = OrganizationUserColumn::UpdatedAt.name(); + let update_clause = + format!("UPDATE {table} SET {info_col} = $1, {updater_id_col} = $2, {updated_at_col} = 'now()' WHERE {id_col} = $3 RETURNING *",); + let update = sqlx::query_as(&update_clause) + .bind(info) + .bind(updater_id) + .bind(id) + .fetch_one(&self.pool) + .await?; + Ok(update) + } + + async fn query_org_user_login( + &self, + org_id: i32, + username: &str, + password: &str, + ) -> Result<(OrganizationUserModel, UserModel), DbAccessError> { + // 查询用户登陆 + let user = self.query_user_login(username, password).await; + match user { + Err(_) => { + // 用户不存在,查询组织用户学工号+用户密码 + // 通过组织id和学工号查询组织用户 + let org_user = self.query_org_user_by_student_id(org_id, username).await?; + let user = self.query_user(org_user.user_id).await?; + // 检查用户密码 + if verify_password(password, &user.password) { + Ok((org_user, user)) + } else { + Err(DbAccessError::InvalidArgument("密码不匹配".to_string())) + } + } + Ok(user) => { + // 用户存在,查询组织用户 + let org_user = self.query_org_user(org_id, user.id).await?; + Ok((org_user, user)) + } + } + } + + async fn query_org_user( + &self, + org_id: i32, + user_id: i32, + ) -> Result { + let table = OrganizationUserColumn::Table.name(); + let org_id_col = OrganizationUserColumn::OrganizationId.name(); + let user_id_col = OrganizationUserColumn::UserId.name(); + let query_clause = + format!("SELECT * FROM {table} WHERE {org_id_col} = $1 AND {user_id_col} = $2",); + let org_user = sqlx::query_as(&query_clause) + .bind(org_id) + .bind(user_id) + .fetch_one(&self.pool) + .await?; + Ok(org_user) + } + + async fn query_org_user_by_student_id( + &self, + org_id: i32, + student_id: &str, + ) -> Result { + let table = OrganizationUserColumn::Table.name(); + let org_id_col = OrganizationUserColumn::OrganizationId.name(); + let student_id_col = OrganizationUserColumn::StudentId.name(); + let query_clause = + format!("SELECT * FROM {table} WHERE {org_id_col} = $1 AND {student_id_col} = $2",); + let org_user = sqlx::query_as(&query_clause) + .bind(org_id) + .bind(student_id) + .fetch_one(&self.pool) + .await?; + Ok(org_user) + } + + async fn query_org_user_by_id(&self, id: i32) -> Result { + let table = OrganizationUserColumn::Table.name(); + let id_col = OrganizationUserColumn::Id.name(); + let query_clause = format!("SELECT * FROM {table} WHERE {id_col} = $1",); + let org_user = sqlx::query_as(&query_clause) + .bind(id) + .fetch_one(&self.pool) + .await?; + Ok(org_user) + } + + async fn query_org_user_page( + &self, + page: PageQuery, + filter: OrgUserFilter, + ) -> Result, DbAccessError> { + let table = OrganizationUserColumn::Table.name(); + let where_clause = filter.to_where_clause(); + let total_clause = format!("SELECT COUNT(*) FROM {table} {where_clause}",); + let total = sqlx::query_scalar(&total_clause) + .fetch_one(&self.pool) + .await?; + if total == 0 { + return Ok(PageData::new(0, vec![])); + } + let limit_clause = page.to_limit_clause(); + let query_clause = + format!("SELECT * FROM {table} {where_clause} ORDER BY id {limit_clause}",); + let query = sqlx::query_as(&query_clause).fetch_all(&self.pool); + + let data = query.await?; + Ok(PageData::new(total, data)) + } +} + +#[cfg(test)] +mod tests { + use serde_json::json; + use sqlx::PgPool; + + use super::*; + use crate::{ + db_access::UserAccessor, + model::{OrganizationModel, UserModel}, + CreateOrg, RegisterUser, + }; + + const DEFAULT_ORG_CODE: &str = "test_org_code"; + const DEFAULT_USER_NAME: &str = "test_user"; + const DEFAULT_USER_PASSWORD: &str = "test_password"; + + async fn init_default_org_and_user( + accessor: &RtsaDbAccessor, + ) -> Result<(UserModel, OrganizationModel), DbAccessError> { + let new_user = RegisterUser::new(DEFAULT_USER_NAME, DEFAULT_USER_PASSWORD); + let user = accessor.register_user(new_user).await?; + let new_org = CreateOrg::new("test_org", user.id).with_code(DEFAULT_ORG_CODE); + let org = accessor.create_org(new_org).await?; + Ok((user, org)) + } + + #[sqlx::test(migrator = "crate::MIGRATOR")] + async fn test_create_org_user(pool: PgPool) -> Result<(), DbAccessError> { + let accessor = RtsaDbAccessor::new(pool); + let (user, org) = init_default_org_and_user(&accessor).await?; + let create = CreateOrgUser::new( + &org.code.unwrap(), + "test_student_id", + "test_name", + "test_password", + user.id, + ); + let (org_user, new_user) = accessor.create_org_user(create.clone()).await?; + println!("org_user: {:?}", org_user); + println!("new_user: {:?}", new_user); + assert_eq!(org_user.organization_id, org.id); + assert_eq!(org_user.user_id, new_user.id); + assert_eq!(org_user.student_id, Some("test_student_id".to_string())); + assert_eq!(org_user.creator_id, user.id); + assert_eq!(org_user.updater_id, user.id); + assert_eq!(new_user.username, create.build_username()); + assert_eq!(new_user.nickname, create.nickname()); + assert_eq!(new_user.password, create.password); + Ok(()) + } + + #[sqlx::test(migrator = "crate::MIGRATOR")] + async fn test_bind_org_user(pool: PgPool) -> Result<(), DbAccessError> { + let accessor = RtsaDbAccessor::new(pool); + let (creator, org) = init_default_org_and_user(&accessor).await?; + // Create a new user + let register = RegisterUser::new("test_user2", "test_password2"); + let new_user = accessor.register_user(register).await?; + // Bind the user to the organization + let bind = + BindOrgUser::new(org.id, new_user.id, creator.id).with_student_id("test_student_id2"); + let org_user = accessor.bind_org_user(bind).await?; + assert_eq!(org_user.organization_id, org.id); + assert_eq!(org_user.user_id, new_user.id); + assert_eq!(org_user.student_id, Some("test_student_id2".to_string())); + assert_eq!(org_user.creator_id, creator.id); + assert_eq!(org_user.updater_id, creator.id); + Ok(()) + } + + #[sqlx::test(migrator = "crate::MIGRATOR")] + async fn test_unbind_org_user(pool: PgPool) -> Result<(), DbAccessError> { + let accessor = RtsaDbAccessor::new(pool); + let (creator, org) = init_default_org_and_user(&accessor).await?; + // Create and bind a new user + let register = RegisterUser::new("test_user3", "test_password3"); + let new_user = accessor.register_user(register).await?; + let bind = BindOrgUser::new(org.id, new_user.id, creator.id); + accessor.bind_org_user(bind).await?; + // Unbind the user from the organization + accessor.unbind_org_user(org.id, new_user.id).await?; + // Verify the user is unbound + let result = accessor.query_org_user(org.id, new_user.id).await; + assert!(matches!(result, Err(DbAccessError::SqlxError(_)))); + Ok(()) + } + + #[sqlx::test(migrator = "crate::MIGRATOR")] + async fn test_update_org_user_student_id(pool: PgPool) -> Result<(), DbAccessError> { + let accessor = RtsaDbAccessor::new(pool); + let (creator, org) = init_default_org_and_user(&accessor).await?; + let create = CreateOrgUser::new( + &org.code.clone().unwrap(), + "old_student_id", + "test_name", + "test_password", + creator.id, + ); + let (org_user, _) = accessor.create_org_user(create).await?; + // Update student ID + let updated_org_user = accessor + .update_org_user_student_id(org_user.id, "new_student_id", creator.id) + .await?; + assert_eq!( + updated_org_user.student_id, + Some("new_student_id".to_string()) + ); + assert!(updated_org_user.updated_at > org_user.updated_at); + Ok(()) + } + + #[sqlx::test(migrator = "crate::MIGRATOR")] + async fn test_query_org_user_login(pool: PgPool) -> Result<(), DbAccessError> { + let accessor = RtsaDbAccessor::new(pool); + let (creator, org) = init_default_org_and_user(&accessor).await?; + let create = CreateOrgUser::new( + &org.code.clone().unwrap(), + "test_student_id", + "test_name", + "test_password", + creator.id, + ); + accessor.create_org_user(create.clone()).await?; + // Attempt to log in with student ID + let (org_user, user) = accessor + .query_org_user_login(org.id, "test_student_id", "test_password") + .await?; + assert_eq!(org_user.organization_id, org.id); + assert_eq!(user.nickname, create.nickname()); + // Attempt to log in with username + let (org_user, user) = accessor + .query_org_user_login(org.id, &create.build_username(), "test_password") + .await?; + assert_eq!(org_user.organization_id, org.id); + assert_eq!(user.nickname, create.nickname()); + // Attempt to log in with wrong password + let result = accessor + .query_org_user_login(org.id, "test_student_id", "wrong_password") + .await; + assert!(matches!(result, Err(DbAccessError::InvalidArgument(_)))); + // Attempt to log in with wrong username + let result = accessor + .query_org_user_login(org.id, "wrong_student_id", "test_password") + .await; + assert!(matches!(result, Err(DbAccessError::SqlxError(_)))); + Ok(()) + } + + #[sqlx::test(migrator = "crate::MIGRATOR")] + async fn test_query_org_user_by_student_id(pool: PgPool) -> Result<(), DbAccessError> { + let accessor = RtsaDbAccessor::new(pool); + let (creator, org) = init_default_org_and_user(&accessor).await?; + let create = CreateOrgUser::new( + &org.code.clone().unwrap(), + "test_student_id", + "test_name", + "test_password", + creator.id, + ); + let (org_user, _) = accessor.create_org_user(create).await?; + let result = accessor + .query_org_user_by_student_id(org.id, "test_student_id") + .await?; + assert_eq!(result, org_user); + Ok(()) + } + + #[sqlx::test(migrator = "crate::MIGRATOR")] + async fn test_query_org_user_by_id(pool: PgPool) -> Result<(), DbAccessError> { + let accessor = RtsaDbAccessor::new(pool); + let (creator, org) = init_default_org_and_user(&accessor).await?; + let create = CreateOrgUser::new( + &org.code.clone().unwrap(), + "test_student_id", + "test_name", + "test_password", + creator.id, + ); + let (org_user, _) = accessor.create_org_user(create).await?; + let result = accessor.query_org_user_by_id(org_user.id).await?; + assert_eq!(result, org_user); + Ok(()) + } + + #[sqlx::test(migrator = "crate::MIGRATOR")] + async fn test_query_org_user_page(pool: PgPool) -> Result<(), DbAccessError> { + let accessor = RtsaDbAccessor::new(pool); + let (creator, org) = init_default_org_and_user(&accessor).await?; + let create = CreateOrgUser::new( + &org.code.clone().unwrap(), + "test_student_id", + "test_name", + "test_password", + creator.id, + ); + accessor.create_org_user(create).await?; + let page = PageQuery::new(1, 10); + let filter = OrgUserFilter { + org_id: Some(org.id), + student_id: None, + }; + let result = accessor.query_org_user_page(page, filter).await?; + assert_eq!(result.total, 1); + assert_eq!(result.data.len(), 1); + Ok(()) + } + + #[sqlx::test(migrator = "crate::MIGRATOR")] + async fn test_is_org_user_exist(pool: PgPool) -> Result<(), DbAccessError> { + let accessor = RtsaDbAccessor::new(pool); + let (creator, org) = init_default_org_and_user(&accessor).await?; + let create = CreateOrgUser::new( + &org.code.clone().unwrap(), + "test_student_id", + "test_name", + "test_password", + creator.id, + ); + let (_, new_user) = accessor.create_org_user(create).await?; + let result = accessor.is_org_user_exist(org.id, new_user.id).await?; + assert!(result); + + let result = accessor.is_org_user_exist(org.id, 0).await?; + assert!(!result); + Ok(()) + } + + #[sqlx::test(migrator = "crate::MIGRATOR")] + async fn test_update_org_user_roles(pool: PgPool) -> Result<(), DbAccessError> { + let accessor = RtsaDbAccessor::new(pool); + let (creator, org) = init_default_org_and_user(&accessor).await?; + let create = CreateOrgUser::new( + &org.code.clone().unwrap(), + "test_student_id", + "test_name", + "test_password", + creator.id, + ); + let (org_user, _) = accessor.create_org_user(create).await?; + let roles = json!(["role1", "role2"]); + let updated_org_user = accessor + .update_org_user_roles(org_user.id, roles.clone(), creator.id) + .await?; + assert_eq!(updated_org_user.roles, roles); + Ok(()) + } + + #[sqlx::test(migrator = "crate::MIGRATOR")] + async fn test_update_org_user_info(pool: PgPool) -> Result<(), DbAccessError> { + let accessor = RtsaDbAccessor::new(pool); + let (creator, org) = init_default_org_and_user(&accessor).await?; + let create = CreateOrgUser::new( + &org.code.clone().unwrap(), + "test_student_id", + "test_name", + "test_password", + creator.id, + ); + let (org_user, _) = accessor.create_org_user(create).await?; + let info = json!({"key": "value"}); + let updated_org_user = accessor + .update_org_user_info(org_user.id, info.clone(), creator.id) + .await?; + assert_eq!(updated_org_user.info, Some(info)); + Ok(()) + } + + #[sqlx::test(migrator = "crate::MIGRATOR")] + async fn test_delete_org_user(pool: PgPool) -> Result<(), DbAccessError> { + let accessor = RtsaDbAccessor::new(pool); + let (creator, org) = init_default_org_and_user(&accessor).await?; + let create = CreateOrgUser::new( + &org.code.clone().unwrap(), + "test_student_id", + "test_name", + "test_password", + creator.id, + ); + let (org_user, _) = accessor.create_org_user(create).await?; + accessor.delete_org_user(org_user.id).await?; + let result = accessor.query_org_user_by_id(org_user.id).await; + assert!(matches!(result, Err(DbAccessError::SqlxError(_)))); + Ok(()) + } +} diff --git a/crates/rtsa_db/src/db_access/release_data.rs b/crates/rtsa_db/src/db_access/release_data.rs index 4785da2..caf025b 100644 --- a/crates/rtsa_db/src/db_access/release_data.rs +++ b/crates/rtsa_db/src/db_access/release_data.rs @@ -1,9 +1,8 @@ -use rtsa_dto::common::DataType; use serde_json::Value; use sqlx::{types::chrono, Postgres}; use crate::{ - common::{PageQuery, PageResult, Sort, SortOrder, TableColumn}, + common::{PageData, PageQuery, Sort, SortOrder, TableColumn}, model::{ DraftDataModel, ReleaseDataColumn, ReleaseDataModel, ReleaseDataVersionColumn, ReleaseDataVersionModel, @@ -35,11 +34,11 @@ pub trait ReleaseDataAccessor { &self, query: ReleaseDataQuery, page: PageQuery, - ) -> Result, DbAccessError>; + ) -> Result, DbAccessError>; /// 检查name是否存在 async fn is_release_data_name_exist( &self, - data_type: DataType, + data_type: i32, name: &str, ) -> Result; /// 查询发布数据 @@ -57,7 +56,7 @@ pub trait ReleaseDataAccessor { &self, release_id: i32, page: PageQuery, - ) -> Result, DbAccessError>; + ) -> Result, DbAccessError>; /// 根据id查询发布版本数据 async fn query_release_data_version_by_id( &self, @@ -109,7 +108,7 @@ pub struct ReleaseFromDraftResult { pub struct ReleaseDataQuery { pub name: Option, pub user_id: Option, - pub data_type: Option, + pub data_type: Option, pub options: Option, pub is_published: Option, } @@ -141,7 +140,7 @@ impl ReleaseDataQuery { self } - pub fn with_data_type(mut self, data_type: rtsa_dto::common::DataType) -> Self { + pub fn with_data_type(mut self, data_type: i32) -> Self { self.data_type = Some(data_type); self } @@ -174,7 +173,7 @@ impl ReleaseDataQuery { filters.push(format!("{rd_user_id} = {user_id}")); } if let Some(data_type) = self.data_type { - filters.push(format!("{rd_data_type} = {}", data_type as i32)); + filters.push(format!("{rd_data_type} = {}", data_type)); } if let Some(options) = &self.options { let options_column = ReleaseDataColumn::Options.name(); @@ -368,7 +367,7 @@ impl ReleaseDataAccessor for RtsaDbAccessor { &self, query: ReleaseDataQuery, page: PageQuery, - ) -> Result, DbAccessError> { + ) -> Result, DbAccessError> { let rd_table = ReleaseDataColumn::Table.name(); let where_clause = query.build_filter(); let count_clause = format!("SELECT COUNT(*) FROM {rd_table} {where_clause}"); @@ -378,7 +377,7 @@ impl ReleaseDataAccessor for RtsaDbAccessor { .fetch_one(&self.pool) .await?; if total == 0 { - return Ok(PageResult::new(0, vec![])); + return Ok(PageData::new(0, vec![])); } let paging_clause = page.to_limit_clause(); let order_by = Sort::new(ReleaseDataColumn::UpdatedAt, SortOrder::Desc); @@ -388,12 +387,12 @@ impl ReleaseDataAccessor for RtsaDbAccessor { let data = sqlx::query_as::<_, ReleaseDataModel>(&query_clause) .fetch_all(&self.pool) .await?; - Ok(PageResult::new(total, data)) + Ok(PageData::new(total, data)) } async fn is_release_data_name_exist( &self, - data_type: DataType, + data_type: i32, name: &str, ) -> Result { let rd_table = ReleaseDataColumn::Table.name(); @@ -406,7 +405,7 @@ impl ReleaseDataAccessor for RtsaDbAccessor { ); let count: i64 = sqlx::query_scalar(&rd_query_clause) .bind(name) - .bind(data_type as i32) + .bind(data_type) .fetch_one(&self.pool) .await?; Ok(count > 0) @@ -453,7 +452,7 @@ impl ReleaseDataAccessor for RtsaDbAccessor { &self, release_id: i32, page: PageQuery, - ) -> Result, DbAccessError> { + ) -> Result, DbAccessError> { // 查询发布数据版本 let rdv_table = ReleaseDataVersionColumn::Table.name(); let rdv_release_id = ReleaseDataVersionColumn::ReleaseDataId.name(); @@ -466,7 +465,7 @@ impl ReleaseDataAccessor for RtsaDbAccessor { .fetch_one(&self.pool) .await?; if total == 0 { - return Ok(PageResult::new(0, vec![])); + return Ok(PageData::new(0, vec![])); } // 查询列,除了data列 let rdv_columns = format!( @@ -490,7 +489,7 @@ impl ReleaseDataAccessor for RtsaDbAccessor { .bind(release_id) .fetch_all(&self.pool) .await?; - Ok(PageResult::new(total, data)) + Ok(PageData::new(total, data)) } async fn query_release_data_version_by_id( @@ -645,15 +644,63 @@ impl ReleaseDataAccessor for RtsaDbAccessor { #[cfg(test)] mod tests { - use crate::{CreateDraftData, DraftDataAccessor, RtsaDbAccessor, SyncUserInfo, UserAccessor}; + use crate::{CreateDraftData, DraftDataAccessor, RegisterUser, RtsaDbAccessor, UserAccessor}; use super::*; - use chrono::Local; - use rtsa_dto::common::{IscsStyle, Role}; use rtsa_log::tracing::Level; use serde::{Deserialize, Serialize}; use sqlx::PgPool; + /// 数据类型 + #[derive( + sqlx::Type, + serde::Serialize, + serde::Deserialize, + Clone, + Copy, + Debug, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + )] + #[repr(i32)] + pub enum DataType { + /// 数据类型未知 + Unknown = 0, + /// 电子地图数据 + Em = 1, + /// ISCS数据 + Iscs = 4, + } + + /// 数据类型 + #[derive( + sqlx::Type, + serde::Serialize, + serde::Deserialize, + Clone, + Copy, + Debug, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + )] + #[repr(i32)] + pub enum IscsStyle { + /// 数据类型未知 + Unknown = 0, + /// 电子地图数据 + Style1 = 1, + } + #[derive(Debug, Serialize, Deserialize)] + pub struct IscsDataOptions { + pub style: IscsStyle, + } + #[test] fn test_release_query() { // 测试构造发布数据查询条件,名称过滤 @@ -668,7 +715,7 @@ mod tests { // 测试构造发布数据查询条件,数据类型过滤 let expects = "WHERE data_type = 1"; - let query_with_data_type = ReleaseDataQuery::new().with_data_type(DataType::Em); + let query_with_data_type = ReleaseDataQuery::new().with_data_type(DataType::Em as i32); assert_eq!(query_with_data_type.build_filter(), expects); // 测试构造发布数据查询条件,是否上架过滤 @@ -682,45 +729,29 @@ mod tests { let query_with_all = ReleaseDataQuery::new() .with_name("test".to_string()) .with_user_id(1) - .with_data_type(DataType::Em) + .with_data_type(DataType::Em as i32) .with_is_published(true); assert_eq!(query_with_all.build_filter(), expects); } - #[derive(Debug, Serialize, Deserialize)] - pub struct IscsDataOptions { - pub style: IscsStyle, - } - // You could also do `use foo_crate::MIGRATOR` and just refer to it as `MIGRATOR` here. #[sqlx::test(migrator = "crate::MIGRATOR")] async fn test_basic_use(pool: PgPool) -> Result<(), DbAccessError> { rtsa_log::Logging::default().with_level(Level::DEBUG).init(); let accessor = RtsaDbAccessor::new(pool); - // 同步10个用户 - let mut users = vec![]; + // 注册10个用户 for i in 0..10 { - let user = SyncUserInfo { - id: i + 1, - name: format!("user{}", i + 1), - password: "password".to_string(), - roles: serde_json::to_value(vec![Role::User]).unwrap(), - email: None, - mobile: None, - created_at: Local::now(), - updated_at: None, - }; - users.push(user); + let user = RegisterUser::new(&format!("test{}", i), "123456"); + accessor.register_user(user).await?; } - accessor.sync_user(users.as_slice()).await?; // 创建草稿 let data = "test".as_bytes(); let draft = accessor .create_draft_data( - CreateDraftData::new("test", rtsa_dto::common::DataType::Iscs, 1) + CreateDraftData::new("test", rtsa_dto::common::DataType::Iscs as i32, 1) .with_options( serde_json::to_value(IscsDataOptions { - style: IscsStyle::DaShiZhiNeng, + style: IscsStyle::Style1, }) .unwrap(), ) @@ -753,7 +784,7 @@ mod tests { // 检查options数据 let options: IscsDataOptions = serde_json::from_value(release_data.options.unwrap()).unwrap(); - assert_eq!(options.style, IscsStyle::DaShiZhiNeng); + assert_eq!(options.style, IscsStyle::Style1); // 检查版本数据 assert_eq!(version1.data, data); // 检查版本描述 @@ -767,7 +798,7 @@ mod tests { // name重复检查 let exist = accessor - .is_release_data_name_exist(rtsa_dto::common::DataType::Iscs, name) + .is_release_data_name_exist(DataType::Iscs as i32, name) .await?; assert!(exist); @@ -849,7 +880,7 @@ mod tests { let data = "test data".as_bytes(); let draft = accessor .create_draft_data( - CreateDraftData::new(&name, rtsa_dto::common::DataType::Em, i).with_data(data), + CreateDraftData::new(&name, DataType::Em as i32, i).with_data(data), ) .await?; let (release_data, _) = accessor @@ -883,7 +914,7 @@ mod tests { let page_result = accessor.paging_query_release_data_list(query, page).await?; assert_eq!(page_result.total, 2); // 分页查询发布数据,按数据类型过滤 - let query = ReleaseDataQuery::new().with_data_type(rtsa_dto::common::DataType::Em); + let query = ReleaseDataQuery::new().with_data_type(DataType::Em as i32); let page = PageQuery::new(1, 10); let page_result = accessor.paging_query_release_data_list(query, page).await?; assert_eq!(page_result.total, 8); diff --git a/crates/rtsa_db/src/db_access/user.rs b/crates/rtsa_db/src/db_access/user.rs index 07da088..bac041f 100644 --- a/crates/rtsa_db/src/db_access/user.rs +++ b/crates/rtsa_db/src/db_access/user.rs @@ -1,9 +1,11 @@ use serde_json::Value; -use sqlx::types::chrono::{DateTime, Local}; +use sqlx::Postgres; use crate::{ - common::{PageQuery, PageResult, TableColumn}, + common::{PageData, PageQuery, TableColumn}, model::{UserColumn, UserModel}, + password_util::verify_password, + username_util::{is_email, is_mobile}, DbAccessError, }; @@ -12,28 +14,100 @@ use super::RtsaDbAccessor; /// 草稿数据管理 #[allow(async_fn_in_trait)] pub trait UserAccessor { - /// 同步用户数据 - async fn sync_user(&self, users: &[SyncUserInfo]) -> Result<(), DbAccessError>; + /// 用户注册 + async fn register_user(&self, user: RegisterUser) -> Result; + /// 用户更新信息 + async fn update_user_info(&self, id: i32, info: Value) -> Result; + /// 用户更新密码 + async fn update_user_password( + &self, + id: i32, + new_password: &str, + ) -> Result; + /// 更新用户手机 + /// 警告:更新手机会检查是否已经存在,如果存在则返回错误 + async fn update_user_mobile( + &self, + id: i32, + new_mobile: &str, + ) -> Result; + /// 更新用户邮箱 + /// 警告:更新邮箱会检查是否已经存在,如果存在则返回错误 + async fn update_user_email(&self, id: i32, new_email: &str) + -> Result; + /// 修改用户角色 + async fn update_user_roles(&self, id: i32, roles: Value) -> Result; + /// 用户登录查询 + /// username: 用户名/邮箱/手机号 + async fn query_user_login( + &self, + username: &str, + password: &str, + ) -> Result; + /// 查询用户数据 + async fn query_user(&self, id: i32) -> Result; + /// 根据username查询用户数据 + async fn query_user_by_username(&self, username: &str) -> Result; + /// 是否用户名已经存在 + async fn is_user_name_exist(&self, name: &str) -> Result; + /// 是否用户email已经存在 + async fn is_user_email_exist(&self, email: &str) -> Result; + /// 是否用户mobile已经存在 + async fn is_user_mobile_exist(&self, mobile: &str) -> Result; /// 根据id列表查询用户name - async fn query_user_name(&self, ids: &[i32]) -> Result, DbAccessError>; + async fn query_user_nicknames(&self, ids: &[i32]) -> Result, DbAccessError>; /// 分页查询用户数据 async fn query_user_page( &self, page: PageQuery, filter: UserPageFilter, - ) -> Result, DbAccessError>; + ) -> Result, DbAccessError>; + /// 删除用户 + /// 警告:删除用户会关联删除用户相关的所有数据 + async fn delete_user(&self, id: i32) -> Result<(), DbAccessError>; } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct UserPageFilter { pub id: Option, - pub name: Option, + pub username: Option, + pub nickname: Option, pub email: Option, pub mobile: Option, pub roles: Option, } impl UserPageFilter { + pub fn with_id(mut self, id: i32) -> Self { + self.id = Some(id); + self + } + + pub fn with_username(mut self, name: &str) -> Self { + self.username = Some(name.to_string()); + self + } + + pub fn with_nickname(mut self, nickname: &str) -> Self { + self.nickname = Some(nickname.to_string()); + self + } + + pub fn with_email(mut self, email: &str) -> Self { + self.email = Some(email.to_string()); + self + } + + pub fn with_mobile(mut self, mobile: &str) -> Self { + self.mobile = Some(mobile.to_string()); + self + } + + pub fn with_roles(mut self, roles: Value) -> Self { + self.roles = Some(roles); + self + } + fn to_where_clause(&self) -> String { let mut clauses = vec![]; let id_column = UserColumn::Id.name(); @@ -48,13 +122,20 @@ impl UserPageFilter { id = id )); } - if let Some(name) = &self.name { + if let Some(name) = &self.username { clauses.push(format!( "{name_column} LIKE '%{name}%'", name_column = name_column, name = name )); } + if let Some(nickname) = &self.nickname { + clauses.push(format!( + "{name_column} LIKE '%{nickname}%'", + name_column = name_column, + nickname = nickname + )); + } if let Some(email) = &self.email { clauses.push(format!( "{email_column} LIKE '%{email}%'", @@ -84,156 +165,105 @@ impl UserPageFilter { } } -#[derive(Debug, Clone)] -pub struct SyncUserInfo { - pub id: i32, +pub struct RegisterUser { pub name: String, + pub nickname: String, pub password: String, - pub roles: Value, pub email: Option, pub mobile: Option, - pub created_at: DateTime, - pub updated_at: Option>, + pub roles: Value, + pub info: Value, +} + +impl RegisterUser { + pub fn new(name: &str, password: &str) -> Self { + Self { + name: name.to_string(), + nickname: name.to_string(), + password: password.to_string(), + email: None, + mobile: None, + roles: Value::Array(Default::default()), + info: Value::Object(Default::default()), + } + } + + pub fn with_nickname(mut self, nickname: &str) -> Self { + self.nickname = nickname.to_string(); + self + } + + pub fn with_email(mut self, email: &str) -> Self { + self.email = Some(email.to_string()); + self + } + + pub fn with_mobile(mut self, mobile: &str) -> Self { + self.mobile = Some(mobile.to_string()); + self + } + + pub fn with_roles(mut self, roles: Value) -> Self { + self.roles = roles; + self + } + + pub fn with_info(mut self, info: Value) -> Self { + self.info = info; + self + } } impl RtsaDbAccessor { - /// 首次同步用户数据 - async fn sync_new_users(&self, users: &[SyncUserInfo]) -> Result<(), DbAccessError> { + pub(crate) async fn insert_user<'e, 'c: 'e, E>( + &self, + user: RegisterUser, + executor: E, + ) -> Result + where + E: 'e + sqlx::Executor<'c, Database = Postgres>, + { let table = UserColumn::Table.name(); - let id = UserColumn::Id.name(); let username = UserColumn::Username.name(); let password = UserColumn::Password.name(); + let nickname = UserColumn::Nickname.name(); let email = UserColumn::Email.name(); let mobile = UserColumn::Mobile.name(); let roles = UserColumn::Roles.name(); - let created_at = UserColumn::CreatedAt.name(); - let updated_at = UserColumn::UpdatedAt.name(); - let insert_columns = format!( - "{id}, {username}, {password}, {email}, {mobile}, {roles}, {created_at}, {updated_at}", - id = id, - username = username, - password = password, - email = email, - mobile = mobile, - roles = roles, - created_at = created_at, - updated_at = updated_at - ); - let insert_values = users - .iter() - .map(|user| { - format!( - "({id}, '{username}', '{password}', {email}, {mobile}, '{roles}', '{created_at}', '{updated_at}')", - id = user.id, - username = user.name, - password = user.password, - email = user.email.as_deref().map(|s| format!("'{s}'")).unwrap_or("NULL".to_string()), - mobile = user.mobile.as_deref().map(|s| format!("'{s}'")).unwrap_or("NULL".to_string()), - roles = user.roles, - created_at = user.created_at, - updated_at = user.updated_at.unwrap_or(user.created_at) - ) - }) - .collect::>() - .join(", "); + let info = UserColumn::Info.name(); let insert_clause = format!( - "INSERT INTO {table} ({insert_columns}) VALUES {insert_values}", + "INSERT INTO {table} ({username}, {password}, {nickname}, {email}, {mobile}, {roles}, {info}) + VALUES ('{new_username}', '{new_password}', '{new_nickname}', {new_email}, {new_mobile}, '{new_roles}', '{new_info}') + RETURNING *", table = table, - insert_columns = insert_columns, - insert_values = insert_values + username = username, + new_username = user.name, + password = password, + new_password = user.password, + nickname = nickname, + new_nickname = user.nickname, + email = email, + new_email = user.email.as_deref().map(|s| format!("'{s}'")).unwrap_or("NULL".to_string()), + mobile = mobile, + new_mobile = user.mobile.as_deref().map(|s| format!("'{s}'")).unwrap_or("NULL".to_string()), + roles = roles, + new_roles = user.roles, + info = info, + new_info = user.info, ); - sqlx::query(&insert_clause).execute(&self.pool).await?; - - Ok(()) - } - - /// 检查并同步用户数据 - async fn check_and_sync_user(&self, users: &[SyncUserInfo]) -> Result<(), DbAccessError> { - // 查询用户表最大的用户id - let table = UserColumn::Table.name(); - let id = UserColumn::Id.name(); - let max_id_clause = format!("SELECT MAX({id}) FROM {table}"); - let max_id: Option = sqlx::query_scalar(&max_id_clause) - .fetch_one(&self.pool) - .await?; - if max_id.is_none() { - self.sync_new_users(users).await?; - return Ok(()); - } - // 遍历用户数据,如果id大于最大id则插入,否则根据更新时间查询是否需要更新,如果需要更新则更新 - // 获取所有id大于最大id的用户数据 - let max_id = max_id.unwrap(); - let mut new_users = vec![]; - for user in users.iter() { - if user.id > max_id { - new_users.push(user.clone()); - } - } - if !new_users.is_empty() { - self.sync_new_users(new_users.as_slice()).await?; - } - // 遍历用户数据,根据更新时间查询是否需要更新,如果需要更新则更新 - for user in users.iter() { - if user.id <= max_id { - let query_clause = format!( - "SELECT {updated_at} FROM {table} WHERE {id} = {user_id}", - updated_at = UserColumn::UpdatedAt.name(), - table = table, - id = id, - user_id = user.id - ); - let updated_at: Option> = sqlx::query_scalar(&query_clause) - .fetch_optional(&self.pool) - .await?; - if let Some(updated_at) = updated_at { - if user.updated_at.unwrap_or(user.created_at) > updated_at { - let username = UserColumn::Username.name(); - let password = UserColumn::Password.name(); - let email = UserColumn::Email.name(); - let mobile = UserColumn::Mobile.name(); - let roles = UserColumn::Roles.name(); - let created_at = UserColumn::CreatedAt.name(); - let updated_at = UserColumn::UpdatedAt.name(); - let update_clause = format!( - "UPDATE {table} SET {username} = '{new_username}', {password} = '{new_password}', {email} = {new_email}, {mobile} = {new_mobile}, {roles} = '{new_roles}', {created_at} = '{new_created_at}', {updated_at} = '{new_updated_at}' WHERE {id} = {user_id}", - table = table, - username = username, - new_username = user.name, - password = password, - new_password = user.password, - email = email, - new_email = user.email.as_deref().map(|s| format!("'{s}'")).unwrap_or("NULL".to_string()), - mobile = mobile, - new_mobile = user.mobile.as_deref().map(|s| format!("'{s}'")).unwrap_or("NULL".to_string()), - roles = roles, - new_roles = user.roles, - created_at = created_at, - new_created_at = user.created_at, - updated_at = updated_at, - new_updated_at = user.updated_at.unwrap_or(user.created_at), - id = id, - user_id = user.id - ); - sqlx::query(&update_clause).execute(&self.pool).await?; - } - } - } - } - Ok(()) + let user: UserModel = sqlx::query_as(&insert_clause).fetch_one(executor).await?; + Ok(user) } } impl UserAccessor for RtsaDbAccessor { - async fn sync_user(&self, users: &[SyncUserInfo]) -> Result<(), DbAccessError> { - self.check_and_sync_user(users).await - } - - async fn query_user_name(&self, ids: &[i32]) -> Result, DbAccessError> { + async fn query_user_nicknames(&self, ids: &[i32]) -> Result, DbAccessError> { let table = UserColumn::Table.name(); - let id = UserColumn::Id.name(); - let username = UserColumn::Username.name(); - let select_columns = format!("{id}, {username}"); - let query_clause = format!("SELECT {select_columns} FROM {table} WHERE {id} = ANY($1)",); + let id_col = UserColumn::Id.name(); + let nickname_col = UserColumn::Nickname.name(); + let select_columns = format!("{id_col}, {nickname_col}",); + let query_clause = + format!("SELECT {select_columns} FROM {table} WHERE {id_col} = ANY($1)",); let rows = sqlx::query_as::<_, (i32, String)>(&query_clause) .bind(ids) .fetch_all(&self.pool) @@ -246,7 +276,7 @@ impl UserAccessor for RtsaDbAccessor { &self, page: PageQuery, filter: UserPageFilter, - ) -> Result, DbAccessError> { + ) -> Result, DbAccessError> { let table = UserColumn::Table.name(); let id_column = UserColumn::Id.name(); let where_clause = filter.to_where_clause(); @@ -255,7 +285,7 @@ impl UserAccessor for RtsaDbAccessor { .fetch_one(&self.pool) .await?; if total == 0 { - return Ok(PageResult::new(total, vec![])); + return Ok(PageData::new(total, vec![])); } let limit_clause = page.to_limit_clause(); let query_clause = @@ -263,135 +293,631 @@ impl UserAccessor for RtsaDbAccessor { let rows = sqlx::query_as::<_, UserModel>(&query_clause) .fetch_all(&self.pool) .await?; - Ok(PageResult::new(total, rows)) + Ok(PageData::new(total, rows)) + } + + async fn register_user(&self, user: RegisterUser) -> Result { + self.insert_user(user, &self.pool).await + } + + async fn query_user_login( + &self, + username: &str, + password: &str, + ) -> Result { + let table = UserColumn::Table.name(); + let username_column = UserColumn::Username.name(); + let email_column = UserColumn::Email.name(); + let mobile_column = UserColumn::Mobile.name(); + if is_email(username) { + let query_clause = format!( + "SELECT * FROM {table} WHERE {email_column} = $1 LIMIT 1", + table = table, + email_column = email_column, + ); + let user: Result = sqlx::query_as(&query_clause) + .bind(username) + .fetch_one(&self.pool) + .await; + match user { + Ok(user) => { + if verify_password(password, &user.password) { + Ok(user) + } else { + Err(DbAccessError::InvalidArgument("密码不匹配".to_string())) + } + } + Err(sqlx::Error::RowNotFound) => Err(DbAccessError::InvalidArgument( + "用户不存在(email)".to_string(), + )), + Err(e) => Err(DbAccessError::SqlxError(e)), + } + } else if is_mobile(username) { + let query_clause = format!( + "SELECT * FROM {table} WHERE {mobile_column} = $1 LIMIT 1", + table = table, + mobile_column = mobile_column, + ); + let user: Result = sqlx::query_as(&query_clause) + .bind(username) + .fetch_one(&self.pool) + .await; + match user { + Ok(user) => { + if verify_password(password, &user.password) { + return Ok(user); + } else { + return Err(DbAccessError::InvalidArgument("密码不匹配".to_string())); + } + } + Err(sqlx::Error::RowNotFound) => { + return Err(DbAccessError::InvalidArgument( + "用户不存在(mobile)".to_string(), + )); + } + Err(e) => { + return Err(DbAccessError::SqlxError(e)); + } + } + } else { + let query_clause = format!( + "SELECT * FROM {table} WHERE {username_column} = $1 LIMIT 1", + table = table, + username_column = username_column, + ); + let user: Result = sqlx::query_as(&query_clause) + .bind(username) + .fetch_one(&self.pool) + .await; + match user { + Ok(user) => { + if verify_password(password, &user.password) { + return Ok(user); + } else { + return Err(DbAccessError::InvalidArgument("密码不匹配".to_string())); + } + } + Err(sqlx::Error::RowNotFound) => { + return Err(DbAccessError::InvalidArgument( + "用户不存在(username)".to_string(), + )); + } + Err(e) => { + return Err(DbAccessError::SqlxError(e)); + } + } + } + } + + async fn query_user(&self, id: i32) -> Result { + let table = UserColumn::Table.name(); + let id_column = UserColumn::Id.name(); + let query_clause = format!("SELECT * FROM {table} WHERE {id_column} = {id}",); + let user: UserModel = sqlx::query_as(&query_clause) + .bind(id) + .fetch_one(&self.pool) + .await?; + Ok(user) + } + + async fn query_user_by_username(&self, username: &str) -> Result { + let table = UserColumn::Table.name(); + let username_column = UserColumn::Username.name(); + let query_clause = format!( + "SELECT * FROM {table} WHERE {username_column} = $1 LIMIT 1", + table = table, + username_column = username_column, + ); + let user = sqlx::query_as(&query_clause) + .bind(username) + .fetch_one(&self.pool) + .await?; + Ok(user) + } + + async fn update_user_info(&self, id: i32, info: Value) -> Result { + let table = UserColumn::Table.name(); + let id_column = UserColumn::Id.name(); + let info_column = UserColumn::Info.name(); + let updated_at_column = UserColumn::UpdatedAt.name(); + if info.is_null() { + Err(DbAccessError::InvalidArgument("info is null".to_string())) + } else { + let update_clause = format!( + "UPDATE {table} SET {info_column} = '{new_info}', {updated_at_column} = 'now()' WHERE {id_column} = {id} RETURNING *", + table = table, + info_column = info_column, + new_info = info, + id_column = id_column, + id = id + ); + let user: UserModel = sqlx::query_as(&update_clause).fetch_one(&self.pool).await?; + Ok(user) + } + } + + async fn update_user_password( + &self, + id: i32, + new_password: &str, + ) -> Result { + if new_password.is_empty() || new_password.len() < 6 { + return Err(DbAccessError::InvalidArgument( + "new_password is empty or too short".to_string(), + )); + } + if new_password.len() > 64 { + return Err(DbAccessError::InvalidArgument( + "new_password is too long".to_string(), + )); + } + let table = UserColumn::Table.name(); + let id_column = UserColumn::Id.name(); + let password_column = UserColumn::Password.name(); + let updated_at_column = UserColumn::UpdatedAt.name(); + let update_clause = format!( + "UPDATE {table} SET {password_column} = '{new_password}', {updated_at_column} = 'now()' WHERE {id_column} = {id} RETURNING *", + table = table, + password_column = password_column, + new_password = new_password, + id_column = id_column, + id = id + ); + let user: UserModel = sqlx::query_as(&update_clause).fetch_one(&self.pool).await?; + Ok(user) + } + + async fn update_user_mobile( + &self, + id: i32, + new_mobile: &str, + ) -> Result { + if new_mobile.is_empty() { + return Err(DbAccessError::InvalidArgument( + "mobile is empty".to_string(), + )); + } + if self.is_user_mobile_exist(new_mobile).await? { + return Err(DbAccessError::InvalidArgument( + "mobile already exists".to_string(), + )); + } + let table = UserColumn::Table.name(); + let id_column = UserColumn::Id.name(); + let mobile_column = UserColumn::Mobile.name(); + let updated_at_column = UserColumn::UpdatedAt.name(); + let update_clause = format!( + "UPDATE {table} SET {mobile_column} = '{new_mobile}', {updated_at_column} = 'now()' WHERE {id_column} = {id} RETURNING *", + table = table, + mobile_column = mobile_column, + new_mobile = new_mobile, + id_column = id_column, + id = id + ); + let user: UserModel = sqlx::query_as(&update_clause).fetch_one(&self.pool).await?; + Ok(user) + } + + async fn update_user_email( + &self, + id: i32, + new_email: &str, + ) -> Result { + if new_email.is_empty() || !new_email.contains('@') { + return Err(DbAccessError::InvalidArgument( + "email is invalid".to_string(), + )); + } + if self.is_user_email_exist(new_email).await? { + return Err(DbAccessError::InvalidArgument( + "email already exists".to_string(), + )); + } + let table = UserColumn::Table.name(); + let id_column = UserColumn::Id.name(); + let email_column = UserColumn::Email.name(); + let updated_at_column = UserColumn::UpdatedAt.name(); + let update_clause = format!( + "UPDATE {table} SET {email_column} = '{new_email}', {updated_at_column} = 'now()' WHERE {id_column} = {id} RETURNING *", + table = table, + email_column = email_column, + new_email = new_email, + id_column = id_column, + id = id + ); + let user: UserModel = sqlx::query_as(&update_clause).fetch_one(&self.pool).await?; + Ok(user) + } + + async fn update_user_roles(&self, id: i32, roles: Value) -> Result { + let table = UserColumn::Table.name(); + let id_column = UserColumn::Id.name(); + let roles_column = UserColumn::Roles.name(); + if roles.is_null() { + return Err(DbAccessError::InvalidArgument("roles is null".to_string())); + } + let updated_at_column = UserColumn::UpdatedAt.name(); + let update_clause = format!( + "UPDATE {table} SET {roles_column} = '{new_roles}', {updated_at_column} = 'now()' WHERE {id_column} = {id} RETURNING *", + table = table, + roles_column = roles_column, + new_roles = serde_json::to_value(roles).unwrap(), + id_column = id_column, + id = id + ); + let user: UserModel = sqlx::query_as(&update_clause).fetch_one(&self.pool).await?; + Ok(user) + } + + async fn is_user_name_exist(&self, name: &str) -> Result { + let table = UserColumn::Table.name(); + let username_column = UserColumn::Username.name(); + let query_clause = format!( + "SELECT COUNT(*) FROM {table} WHERE {username_column} = '{name}'", + table = table, + username_column = username_column, + name = name + ); + let count: i64 = sqlx::query_scalar(&query_clause) + .fetch_one(&self.pool) + .await?; + Ok(count > 0) + } + + async fn is_user_email_exist(&self, email: &str) -> Result { + let table = UserColumn::Table.name(); + let email_column = UserColumn::Email.name(); + let query_clause = format!( + "SELECT COUNT(*) FROM {table} WHERE {email_column} = '{email}'", + table = table, + email_column = email_column, + email = email + ); + let count: i64 = sqlx::query_scalar(&query_clause) + .fetch_one(&self.pool) + .await?; + Ok(count > 0) + } + + async fn is_user_mobile_exist(&self, mobile: &str) -> Result { + let table = UserColumn::Table.name(); + let mobile_column = UserColumn::Mobile.name(); + let query_clause = format!( + "SELECT COUNT(*) FROM {table} WHERE {mobile_column} = '{mobile}'", + table = table, + mobile_column = mobile_column, + mobile = mobile + ); + let count: i64 = sqlx::query_scalar(&query_clause) + .fetch_one(&self.pool) + .await?; + Ok(count > 0) + } + + async fn delete_user(&self, id: i32) -> Result<(), DbAccessError> { + let table = UserColumn::Table.name(); + let id_column = UserColumn::Id.name(); + let delete_clause = format!("DELETE FROM {table} WHERE {id_column} = {id}",); + sqlx::query(&delete_clause) + .bind(id) + .execute(&self.pool) + .await?; + Ok(()) } } #[cfg(test)] mod tests { - use std::time::Duration; - - use rtsa_log::tracing::Level; - use serde::{Deserialize, Serialize}; + use rtsa_dto::common::Role; use sqlx::PgPool; use super::*; - #[derive(Debug, Clone, Serialize, Deserialize)] - enum Role { - User, - Admin, - } - #[test] - fn test_role_value_format() { - let roles = vec![Role::User, Role::Admin]; - let value = serde_json::to_value(&roles).unwrap(); - println!("{}", value); - println!("{}", serde_json::to_string(&value).unwrap()); + #[sqlx::test(migrator = "crate::MIGRATOR")] + async fn test_register_user(pool: PgPool) -> Result<(), DbAccessError> { + let accessor = RtsaDbAccessor { pool }; + + let new_user = RegisterUser::new("test_user", "test_password") + .with_roles(serde_json::to_value(vec![Role::User]).unwrap()) + .with_email("test_user@example.com") + .with_mobile("13345678901") + .with_info(serde_json::json!({"from": "cgy"})); + accessor.register_user(new_user).await?; + + let queried_user = accessor + .query_user_login("test_user@example.com", "test_password") + .await?; + assert_eq!(queried_user.username, "test_user"); + assert_eq!( + queried_user.email, + Some("test_user@example.com".to_string()) + ); + assert_eq!(queried_user.mobile, Some("13345678901".to_string())); + assert_eq!( + queried_user.roles, + serde_json::to_value(vec![Role::User]).unwrap() + ); + assert_eq!(queried_user.info, serde_json::json!({"from": "cgy"})); + Ok(()) } #[sqlx::test(migrator = "crate::MIGRATOR")] - async fn test_sync_user(pool: PgPool) -> Result<(), DbAccessError> { - // 日志初始化 - rtsa_log::Logging::default().with_level(Level::DEBUG).init(); - let accessor = RtsaDbAccessor::new(pool); - let users = vec![ - SyncUserInfo { - id: 1, - name: "test1".to_string(), - password: "password".to_string(), - roles: serde_json::to_value(vec![Role::User]).unwrap(), - email: None, - mobile: None, - created_at: Local::now(), - updated_at: None, - }, - SyncUserInfo { - id: 2, - name: "test2".to_string(), - password: "password".to_string(), - roles: serde_json::to_value(vec![Role::Admin]).unwrap(), - email: None, - mobile: None, - created_at: Local::now(), - updated_at: None, - }, - ]; - accessor.sync_user(users.as_slice()).await?; - // 分页查询检查是否插入成功 - let page = PageQuery { - page: 1, - items_per_page: 10, - }; - let filter = UserPageFilter { - id: None, - name: None, - email: None, - mobile: None, - roles: None, - }; - let page_result = accessor - .query_user_page(page.clone(), filter.clone()) - .await?; - assert_eq!(page_result.total, 2); - assert_eq!(page_result.data.len(), 2); - println!("{:?}", page_result); - // 同时新增和更新用户 - let users = vec![ - SyncUserInfo { - id: 1, - name: "test1".to_string(), - password: "password".to_string(), - roles: serde_json::to_value(vec![Role::User]).unwrap(), - email: Some("walker@163.com".to_string()), - mobile: None, - created_at: Local::now() - Duration::from_secs(60), - updated_at: Some(Local::now()), - }, - SyncUserInfo { - id: 2, - name: "test2".to_string(), - password: "password".to_string(), - roles: serde_json::to_value(vec![Role::Admin, Role::User]).unwrap(), - email: None, - mobile: Some("123456789".to_string()), - created_at: Local::now() - Duration::from_secs(60), - updated_at: Some(Local::now()), - }, - SyncUserInfo { - id: 3, - name: "test3".to_string(), - password: "password".to_string(), - roles: serde_json::to_value(vec![Role::User]).unwrap(), - email: None, - mobile: None, - created_at: Local::now(), - updated_at: None, - }, - ]; - accessor.sync_user(users.as_slice()).await?; - // 分页查询检查是否更新成功 - let page_result = accessor - .query_user_page(page.clone(), filter.clone()) - .await?; - assert_eq!(page_result.total, 3); - assert_eq!(page_result.data.len(), 3); - println!("{:?}", page_result); + async fn test_update_user_info(pool: PgPool) -> Result<(), DbAccessError> { + let accessor = RtsaDbAccessor { pool }; - // 带过滤条件的分页查询 - let filter = UserPageFilter { - id: Some(1), - name: None, - email: None, - mobile: None, - roles: None, - }; - let page_result = accessor - .query_user_page(page.clone(), filter.clone()) + // Register a new user + let new_user = RegisterUser::new("test_user", "test_password") + .with_info(serde_json::json!({"from": "cgy"})); + let user = accessor.register_user(new_user).await?; + + // Update user info + let updated_user = accessor + .update_user_info(user.id, serde_json::json!({"from": "updated"})) .await?; - assert_eq!(page_result.total, 1); - assert_eq!(page_result.data.len(), 1); + + // Verify the update + assert_eq!(updated_user.info, serde_json::json!({"from": "updated"})); + + let updated_user = accessor.update_user_info(user.id, Value::Null).await; + assert!(updated_user.is_err()); Ok(()) } + + #[sqlx::test(migrator = "crate::MIGRATOR")] + async fn test_update_user_password(pool: PgPool) -> Result<(), DbAccessError> { + let accessor = RtsaDbAccessor { pool }; + + // Register a new user + let new_user = + RegisterUser::new("test_user", "old_password").with_email("test_user@example.com"); + let user = accessor.register_user(new_user).await?; + + // Update user password with empty password + let result = accessor.update_user_password(user.id, "").await; + assert!(result.is_err()); + + // Update user password with short password + let result = accessor.update_user_password(user.id, "short").await; + assert!(result.is_err()); + + // Update user password + let _ = accessor + .update_user_password(user.id, "new_password") + .await?; + + // Verify the password has changed by attempting to login + let result = accessor + .query_user_login("test_user@example.com", "new_password") + .await; + assert!(result.is_ok()); + + let result = accessor + .query_user_login("test_user@example.com", "old_password") + .await; + assert!(result.is_err()); + Ok(()) + } + + #[sqlx::test(migrator = "crate::MIGRATOR")] + async fn test_update_user_mobile(pool: PgPool) -> Result<(), DbAccessError> { + let accessor = RtsaDbAccessor { pool }; + + // Register a new user with mobile number + let new_user = RegisterUser::new("test_user", "test_password").with_mobile("13345678901"); + let user = accessor.register_user(new_user).await?; + + // Update user mobile + let updated_user = accessor.update_user_mobile(user.id, "0987654321").await?; + + // Verify the update + assert_eq!(updated_user.mobile, Some("0987654321".to_string())); + Ok(()) + } + + #[sqlx::test(migrator = "crate::MIGRATOR")] + async fn test_update_user_email(pool: PgPool) -> Result<(), DbAccessError> { + let accessor = RtsaDbAccessor { pool }; + + // Register a new user with email + let new_user = + RegisterUser::new("test_user", "test_password").with_email("test_user@example.com"); + let user = accessor.register_user(new_user).await?; + + // Update user email + let updated_user = accessor + .update_user_email(user.id, "newemail@example.com") + .await?; + + // Verify the update + assert_eq!(updated_user.email, Some("newemail@example.com".to_string())); + Ok(()) + } + + #[sqlx::test(migrator = "crate::MIGRATOR")] + async fn test_update_user_roles(pool: PgPool) -> Result<(), DbAccessError> { + let accessor = RtsaDbAccessor { pool }; + + // Register a new user with default role + let new_user = RegisterUser::new("test_user", "test_password"); + let user = accessor.register_user(new_user).await?; + + // Update user roles + let new_roles = vec![Role::Admin, Role::User]; + let updated_user = accessor + .update_user_roles(user.id, serde_json::to_value(new_roles.clone()).unwrap()) + .await?; + + // Verify the update + assert_eq!(updated_user.roles, serde_json::to_value(new_roles).unwrap()); + Ok(()) + } + + #[sqlx::test(migrator = "crate::MIGRATOR")] + async fn test_query_user(pool: PgPool) -> Result<(), DbAccessError> { + let accessor = RtsaDbAccessor { pool }; + + // 准备测试数据 + let new_user = RegisterUser::new("test_user", "test_password"); + let um = accessor.register_user(new_user).await?; + + // Assume a user with ID exists + let user = accessor.query_user(um.id).await?; + assert_eq!(user.id, um.id); + Ok(()) + } + + #[sqlx::test(migrator = "crate::MIGRATOR")] + async fn test_query_user_by_username(pool: PgPool) -> Result<(), DbAccessError> { + let accessor = RtsaDbAccessor { pool }; + + // 准备测试数据 + let new_user = RegisterUser::new("test_user", "test_password"); + let um = accessor.register_user(new_user).await?; + + // Assume a user with username exists + let user = accessor.query_user_by_username("test_user").await?; + assert_eq!(user.id, um.id); + Ok(()) + } + + #[sqlx::test(migrator = "crate::MIGRATOR")] + async fn test_query_user_login(pool: PgPool) -> Result<(), DbAccessError> { + let accessor = RtsaDbAccessor { pool }; + + // 准备测试数据 + let new_user = RegisterUser::new("test_user", "test_password") + .with_email("test_user@example.com") + .with_mobile("13345678901"); + accessor.register_user(new_user).await?; + + // Test with correct credentials + let user = accessor + .query_user_login("test_user@example.com", "test_password") + .await?; + assert_eq!(user.username, "test_user"); + + let user = accessor + .query_user_login("test_user", "test_password") + .await; + assert!(user.is_ok()); + + let user = accessor + .query_user_login("13345678901", "test_password") + .await; + assert!(user.is_ok()); + + // Test with incorrect credentials + let result = accessor + .query_user_login("test_user@example.com", "wrongpassword") + .await; + assert!(result.is_err()); + Ok(()) + } + + #[sqlx::test(migrator = "crate::MIGRATOR")] + async fn test_query_user_name(pool: PgPool) -> Result<(), DbAccessError> { + let accessor = RtsaDbAccessor { pool }; + + let mut ids = Vec::new(); + // 初始化数据 + for i in 0..3 { + let new_user = RegisterUser::new(&format!("test_user{}", i), "test_password"); + let model = accessor.register_user(new_user).await?; + ids.push(model.id); + } + + let names = accessor.query_user_nicknames(&ids).await?; + assert_eq!(names.len(), ids.len()); + Ok(()) + } + + #[sqlx::test(migrator = "crate::MIGRATOR")] + async fn test_query_user_page(pool: PgPool) -> Result<(), DbAccessError> { + let accessor = RtsaDbAccessor { pool }; + + // 初始化测试数据 + let new_user = RegisterUser::new("test_user", "test_password"); + accessor.register_user(new_user).await?; + + let page_query = PageQuery::new(1, 10); + let filter = UserPageFilter::default().with_username("test"); + let page_result = accessor.query_user_page(page_query, filter).await?; + assert!(page_result.total >= 0); + Ok(()) + } + + #[sqlx::test(migrator = "crate::MIGRATOR")] + async fn test_is_user_email_exist(pool: PgPool) -> Result<(), DbAccessError> { + let accessor = RtsaDbAccessor { pool }; + + // Register a new user with email + let new_user = + RegisterUser::new("test_user", "test_password").with_email("user@example.com"); + accessor.register_user(new_user).await?; + + // Test with existing email + let result = accessor.is_user_email_exist("user@example.com").await?; + assert!(result); + + // Test with non-existing email + let result = accessor + .is_user_email_exist("non-exist@example.com") + .await?; + assert!(!result); + Ok(()) + } + + #[sqlx::test(migrator = "crate::MIGRATOR")] + async fn test_is_user_mobile_exist(pool: PgPool) -> Result<(), DbAccessError> { + let accessor = RtsaDbAccessor { pool }; + + // Register a new user with mobile + let new_user = RegisterUser::new("test_user", "test_password").with_mobile("13345678901"); + accessor.register_user(new_user).await?; + + // Test with existing mobile + let result = accessor.is_user_mobile_exist("13345678901").await?; + assert!(result); + + // Test with non-existing mobile + let result = accessor.is_user_mobile_exist("0987654321").await?; + assert!(!result); + Ok(()) + } + + #[sqlx::test(migrator = "crate::MIGRATOR")] + async fn test_is_user_name_exist(pool: PgPool) -> Result<(), DbAccessError> { + let accessor = RtsaDbAccessor { pool }; + + // Register a new user with name + let new_user = RegisterUser::new("test_user", "test_password"); + accessor.register_user(new_user).await?; + + // Test with existing name + let result = accessor.is_user_name_exist("test_user").await?; + assert!(result); + + // Test with non-existing name + let result = accessor.is_user_name_exist("non-exist").await?; + assert!(!result); + Ok(()) + } + + #[sqlx::test(migrator = "crate::MIGRATOR")] + async fn test_delete_user(pool: PgPool) -> Result<(), DbAccessError> { + let accessor = RtsaDbAccessor { pool }; + + // Register a new user + let new_user = RegisterUser::new("test_user", "test_password"); + let user = accessor.register_user(new_user).await?; + + // Delete the user + accessor.delete_user(user.id).await?; + + // Verify the user has been deleted + let result = accessor.query_user(user.id).await; + assert!(result.is_err()); + Ok(()) + } } diff --git a/crates/rtsa_db/src/error.rs b/crates/rtsa_db/src/error.rs index 4610ac2..6673c76 100644 --- a/crates/rtsa_db/src/error.rs +++ b/crates/rtsa_db/src/error.rs @@ -3,11 +3,15 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum DbAccessError { #[error("未知的数据库访问错误")] - Unknown, + Unknown(#[from] anyhow::Error), #[error("数据访问错误: {0}")] SqlxError(#[from] sqlx::Error), - #[error("数据已存在")] - RowExist, + #[error("数据已存在: {0}")] + RowExist(String), + #[error("数据不存在: {0}")] + RowNotExist(String), #[error("数据错误:{0}")] DataError(String), + #[error("非法参数:{0}")] + InvalidArgument(String), } diff --git a/crates/rtsa_db/src/lib.rs b/crates/rtsa_db/src/lib.rs index ac873b9..898d5f3 100644 --- a/crates/rtsa_db/src/lib.rs +++ b/crates/rtsa_db/src/lib.rs @@ -4,6 +4,8 @@ mod error; pub mod model; pub use db_access::*; pub use error::*; +pub mod password_util; +pub mod username_util; use sqlx::pool::PoolOptions; pub use sqlx::types::chrono::*; @@ -23,5 +25,6 @@ pub mod prelude { pub use crate::common::*; pub use crate::db_access::*; pub use crate::model::*; + pub use crate::password_util::*; pub use crate::DbAccessError; } diff --git a/crates/rtsa_db/src/model.rs b/crates/rtsa_db/src/model.rs index 3b86283..cfae2a7 100644 --- a/crates/rtsa_db/src/model.rs +++ b/crates/rtsa_db/src/model.rs @@ -1,9 +1,5 @@ -use rtsa_dto::common::{DataType, FeatureType, Role}; use serde_json::Value; -use sqlx::types::{ - chrono::{DateTime, Local}, - Json, -}; +use sqlx::types::chrono::{DateTime, Local}; use crate::common::TableColumn; @@ -25,9 +21,11 @@ pub enum UserColumn { Id, Username, Password, + Nickname, Email, Mobile, Roles, + Info, CreatedAt, UpdatedAt, } @@ -37,15 +35,182 @@ pub struct UserModel { pub id: i32, pub username: String, pub password: String, + pub nickname: String, #[sqlx(default)] pub email: Option, #[sqlx(default)] pub mobile: Option, - pub roles: Json>, + pub roles: Value, + pub info: Value, pub created_at: DateTime, pub updated_at: DateTime, } +/// 数据库表 rtsa.user_data 列映射 +#[derive(Debug)] +pub enum UserDataColumn { + Table, + Id, + UserId, + DataType, + Data, + CreatedAt, + UpdatedAt, +} + +#[derive(Debug, sqlx::FromRow)] +pub struct UserDataModel { + pub id: i32, + pub user_id: i32, + pub data_type: i32, + pub data: Value, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +/// 数据库表 rtsa.organization 列映射 +#[derive(Debug)] +pub enum OrganizationColumn { + Table, + Id, + Code, + Name, + Config, + ParentId, + CreatorId, + CreatedAt, + UpdaterId, + UpdatedAt, +} + +#[derive(Debug, sqlx::FromRow)] +pub struct OrganizationModel { + pub id: i32, + pub code: Option, + pub name: String, + #[sqlx(default)] + pub config: Option, + pub parent_id: Option, + pub creator_id: i32, + pub created_at: DateTime, + pub updater_id: i32, + pub updated_at: DateTime, +} + +/// 数据库表 rtsa.organization_user 列映射 +#[derive(Debug)] +pub enum OrganizationUserColumn { + Table, + Id, + OrganizationId, + UserId, + StudentId, + Roles, + Info, + CreatorId, + CreatedAt, + UpdaterId, + UpdatedAt, +} + +#[derive(Debug, sqlx::FromRow, Clone, PartialEq, Eq)] +pub struct OrganizationUserModel { + pub id: i32, + pub organization_id: i32, + pub user_id: i32, + pub student_id: Option, + pub roles: Value, + pub info: Option, + pub creator_id: i32, + pub created_at: DateTime, + pub updater_id: i32, + pub updated_at: DateTime, +} + +/// 数据库表 rtsa.organization_data 列映射 +#[derive(Debug)] +pub enum OrganizationDataColumn { + Table, + Id, + OrganizationId, + DataType, + Data, + CreatorId, + CreatedAt, + UpdaterId, + UpdatedAt, +} + +#[derive(Debug, sqlx::FromRow)] +pub struct OrganizationDataModel { + pub id: i32, + pub organization_id: i32, + pub data_type: i32, + pub data: Value, + pub creator_id: i32, + pub created_at: DateTime, + pub updater_id: i32, + pub updated_at: DateTime, +} + +/// 数据库表 rtsa.feature 列映射 +#[derive(Debug)] +#[allow(dead_code)] +pub enum FeatureColumn { + Table, + Id, + FeatureType, + Name, + Description, + Config, + IsPublished, + CreatorId, + UpdaterId, + CreatedAt, + UpdatedAt, +} + +#[derive(Debug, sqlx::FromRow)] +pub struct FeatureModel { + pub id: i32, + pub feature_type: i32, + pub name: String, + pub description: String, + #[sqlx(default)] + pub config: Value, + pub is_published: bool, + pub creator_id: i32, + pub updater_id: i32, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +/// 数据库表 rtsa.organization_feature 列映射 +#[derive(Debug)] +pub enum OrganizationFeatureColumn { + Table, + Id, + OrganizationId, + FeatureId, + Config, + CreatorId, + CreatedAt, + UpdaterId, + UpdatedAt, +} + +#[derive(Debug, sqlx::FromRow)] +pub struct OrganizationFeatureModel { + pub id: i32, + pub organization_id: i32, + pub feature_id: i32, + pub config: Value, + pub creator_id: i32, + pub created_at: DateTime, + pub updater_id: i32, + pub updated_at: DateTime, +} + /// 数据库表 rtsa.draft_data 列映射 #[derive(Debug)] pub enum DraftDataColumn { @@ -66,7 +231,7 @@ pub enum DraftDataColumn { pub struct DraftDataModel { pub id: i32, pub name: String, - pub data_type: DataType, + pub data_type: i32, #[sqlx(default)] pub options: Option, #[sqlx(default)] @@ -100,7 +265,7 @@ pub enum ReleaseDataColumn { pub struct ReleaseDataModel { pub id: i32, pub name: String, - pub data_type: DataType, + pub data_type: i32, /// 从发布版本复制的选项,主要用于查询 #[sqlx(default)] pub options: Option, @@ -137,59 +302,75 @@ pub struct ReleaseDataVersionModel { pub created_at: DateTime, } -/// 数据库表 rtsa.feature 列映射 +/// 数据库表 rtsa.release_data_set 列映射 #[derive(Debug)] -#[allow(dead_code)] -pub enum FeatureColumn { +pub enum ReleaseDataSetColumn { Table, Id, - FeatureType, + DataSetType, Name, Description, Config, IsPublished, CreatorId, - UpdaterId, CreatedAt, + UpdaterId, UpdatedAt, } #[derive(Debug, sqlx::FromRow)] -pub struct FeatureModel { +pub struct ReleaseDataSetModel { pub id: i32, - pub feature_type: FeatureType, + pub data_set_type: i32, pub name: String, pub description: String, #[sqlx(default)] pub config: Value, pub is_published: bool, pub creator_id: i32, - pub updater_id: i32, pub created_at: DateTime, + pub updater_id: i32, pub updated_at: DateTime, } -/// 数据库表 rtsa.user_config 列映射 +/// 数据库表 rtsa.release_data_set_data 列映射 #[derive(Debug)] -#[allow(dead_code)] -pub enum UserConfigColumn { +pub enum ReleaseDataSetDataColumn { Table, Id, - UserId, - ConfigType, - Config, - CreatedAt, - UpdatedAt, + ReleaseDataSetId, + ReleaseDataId, + OrderIndex, } #[derive(Debug, sqlx::FromRow)] -pub struct UserConfigModel { +pub struct ReleaseDataSetDataModel { pub id: i32, + pub release_data_set_id: i32, + pub release_data_id: i32, + pub order_index: i32, +} + +/// 数据库表 rtsa.user_operation_log 列映射 +#[derive(Debug)] +pub enum UserOperationLogColumn { + Table, + Id, + OrganizationId, + UserId, + LogType, + LogData, + CreatedAt, +} + +#[derive(Debug, sqlx::FromRow)] +pub struct UserOperationLogModel { + pub id: i32, + pub organization_id: i32, pub user_id: i32, - pub config_type: i32, - pub config: Value, + pub log_type: i32, + pub log_data: Value, pub created_at: DateTime, - pub updated_at: DateTime, } impl TableColumn for UserColumn { @@ -199,15 +380,116 @@ impl TableColumn for UserColumn { UserColumn::Id => "id", UserColumn::Username => "username", UserColumn::Password => "password", + UserColumn::Nickname => "nickname", UserColumn::Email => "email", UserColumn::Mobile => "mobile", UserColumn::Roles => "roles", + UserColumn::Info => "info", UserColumn::CreatedAt => "created_at", UserColumn::UpdatedAt => "updated_at", } } } +impl TableColumn for UserDataColumn { + fn name(&self) -> &str { + match self { + UserDataColumn::Table => "rtsa.user_data", + UserDataColumn::Id => "id", + UserDataColumn::UserId => "user_id", + UserDataColumn::DataType => "data_type", + UserDataColumn::Data => "data", + UserDataColumn::CreatedAt => "created_at", + UserDataColumn::UpdatedAt => "updated_at", + } + } +} + +impl TableColumn for OrganizationColumn { + fn name(&self) -> &str { + match self { + OrganizationColumn::Table => "rtsa.organization", + OrganizationColumn::Id => "id", + OrganizationColumn::Code => "code", + OrganizationColumn::Name => "name", + OrganizationColumn::Config => "config", + OrganizationColumn::ParentId => "parent_id", + OrganizationColumn::CreatorId => "creator_id", + OrganizationColumn::CreatedAt => "created_at", + OrganizationColumn::UpdaterId => "updater_id", + OrganizationColumn::UpdatedAt => "updated_at", + } + } +} + +impl TableColumn for OrganizationUserColumn { + fn name(&self) -> &str { + match self { + OrganizationUserColumn::Table => "rtsa.organization_user", + OrganizationUserColumn::Id => "id", + OrganizationUserColumn::OrganizationId => "organization_id", + OrganizationUserColumn::UserId => "user_id", + OrganizationUserColumn::StudentId => "student_id", + OrganizationUserColumn::Roles => "roles", + OrganizationUserColumn::Info => "info", + OrganizationUserColumn::CreatorId => "creator_id", + OrganizationUserColumn::CreatedAt => "created_at", + OrganizationUserColumn::UpdaterId => "updater_id", + OrganizationUserColumn::UpdatedAt => "updated_at", + } + } +} + +impl TableColumn for OrganizationDataColumn { + fn name(&self) -> &str { + match self { + OrganizationDataColumn::Table => "rtsa.organization_data", + OrganizationDataColumn::Id => "id", + OrganizationDataColumn::OrganizationId => "organization_id", + OrganizationDataColumn::DataType => "data_type", + OrganizationDataColumn::Data => "data", + OrganizationDataColumn::CreatorId => "creator_id", + OrganizationDataColumn::CreatedAt => "created_at", + OrganizationDataColumn::UpdaterId => "updater_id", + OrganizationDataColumn::UpdatedAt => "updated_at", + } + } +} + +impl TableColumn for FeatureColumn { + fn name(&self) -> &str { + match self { + FeatureColumn::Table => "rtsa.feature", + FeatureColumn::Id => "id", + FeatureColumn::FeatureType => "feature_type", + FeatureColumn::Name => "name", + FeatureColumn::Description => "description", + FeatureColumn::Config => "config", + FeatureColumn::IsPublished => "is_published", + FeatureColumn::CreatorId => "creator_id", + FeatureColumn::UpdaterId => "updater_id", + FeatureColumn::CreatedAt => "created_at", + FeatureColumn::UpdatedAt => "updated_at", + } + } +} + +impl TableColumn for OrganizationFeatureColumn { + fn name(&self) -> &str { + match self { + OrganizationFeatureColumn::Table => "rtsa.organization_feature", + OrganizationFeatureColumn::Id => "id", + OrganizationFeatureColumn::OrganizationId => "organization_id", + OrganizationFeatureColumn::FeatureId => "feature_id", + OrganizationFeatureColumn::Config => "config", + OrganizationFeatureColumn::CreatorId => "creator_id", + OrganizationFeatureColumn::CreatedAt => "created_at", + OrganizationFeatureColumn::UpdaterId => "updater_id", + OrganizationFeatureColumn::UpdatedAt => "updated_at", + } + } +} + impl TableColumn for DraftDataColumn { fn name(&self) -> &str { match self { @@ -258,34 +540,32 @@ impl TableColumn for ReleaseDataVersionColumn { } } -impl TableColumn for FeatureColumn { +impl TableColumn for ReleaseDataSetColumn { fn name(&self) -> &str { match self { - FeatureColumn::Table => "rtsa.feature", - FeatureColumn::Id => "id", - FeatureColumn::FeatureType => "feature_type", - FeatureColumn::Name => "name", - FeatureColumn::Description => "description", - FeatureColumn::Config => "config", - FeatureColumn::IsPublished => "is_published", - FeatureColumn::CreatorId => "creator_id", - FeatureColumn::UpdaterId => "updater_id", - FeatureColumn::CreatedAt => "created_at", - FeatureColumn::UpdatedAt => "updated_at", + ReleaseDataSetColumn::Table => "rtsa.release_data_set", + ReleaseDataSetColumn::Id => "id", + ReleaseDataSetColumn::DataSetType => "data_set_type", + ReleaseDataSetColumn::Name => "name", + ReleaseDataSetColumn::Description => "description", + ReleaseDataSetColumn::Config => "config", + ReleaseDataSetColumn::IsPublished => "is_published", + ReleaseDataSetColumn::CreatorId => "creator_id", + ReleaseDataSetColumn::CreatedAt => "created_at", + ReleaseDataSetColumn::UpdaterId => "updater_id", + ReleaseDataSetColumn::UpdatedAt => "updated_at", } } } -impl TableColumn for UserConfigColumn { +impl TableColumn for ReleaseDataSetDataColumn { fn name(&self) -> &str { match self { - UserConfigColumn::Table => "rtsa.user_config", - UserConfigColumn::Id => "id", - UserConfigColumn::UserId => "user_id", - UserConfigColumn::ConfigType => "config_type", - UserConfigColumn::Config => "config", - UserConfigColumn::CreatedAt => "created_at", - UserConfigColumn::UpdatedAt => "updated_at", + ReleaseDataSetDataColumn::Table => "rtsa.release_data_set_data", + ReleaseDataSetDataColumn::Id => "id", + ReleaseDataSetDataColumn::ReleaseDataSetId => "release_data_set_id", + ReleaseDataSetDataColumn::ReleaseDataId => "release_data_id", + ReleaseDataSetDataColumn::OrderIndex => "order_index", } } } diff --git a/crates/rtsa_db/src/password_util.rs b/crates/rtsa_db/src/password_util.rs new file mode 100644 index 0000000..c53c40c --- /dev/null +++ b/crates/rtsa_db/src/password_util.rs @@ -0,0 +1,4 @@ +/// 验证密码是否正确 +pub fn verify_password(password: &str, hash: &str) -> bool { + password == hash +} diff --git a/crates/rtsa_db/src/username_util.rs b/crates/rtsa_db/src/username_util.rs new file mode 100644 index 0000000..2ca837c --- /dev/null +++ b/crates/rtsa_db/src/username_util.rs @@ -0,0 +1,53 @@ +use lazy_static::lazy_static; +use regex::Regex; + +pub fn is_email(email: &str) -> bool { + lazy_static! { + static ref RE: Regex = + Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap(); + } + RE.is_match(email) +} + +pub fn is_mobile(mobile: &str) -> bool { + lazy_static! { + static ref RE: Regex = Regex::new(r"^1[3-9]\d{9}$").unwrap(); + } + RE.is_match(mobile) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_is_email() { + assert!(!is_email("test")); + assert!(!is_email("test@")); + assert!(!is_email("test@163")); + assert!(!is_email("test@163.")); + assert!(!is_email("test@163.c")); + assert!(is_email("test@163.cn")); + } + + #[test] + fn test_is_mobile() { + assert!(!is_mobile("test")); + assert!(!is_mobile("12345678901")); + assert!(!is_mobile("22345678901")); + assert!(!is_mobile("32345678901")); + assert!(!is_mobile("42345678901")); + assert!(!is_mobile("52345678901")); + assert!(!is_mobile("62345678901")); + assert!(!is_mobile("72345678901")); + assert!(!is_mobile("82345678901")); + assert!(!is_mobile("92345678901")); + assert!(is_mobile("13345678901")); + assert!(is_mobile("14345678901")); + assert!(is_mobile("15345678901")); + assert!(is_mobile("16345678901")); + assert!(is_mobile("17345678901")); + assert!(is_mobile("18345678901")); + assert!(is_mobile("19345678901")); + } +} diff --git a/crates/rtsa_dto/src/pb/common.rs b/crates/rtsa_dto/src/pb/common.rs index 67a07e3..2ba4331 100644 --- a/crates/rtsa_dto/src/pb/common.rs +++ b/crates/rtsa_dto/src/pb/common.rs @@ -112,6 +112,15 @@ pub enum Role { Admin = 1, /// 普通用户 User = 2, + /// -----组织用户角色----- + /// 组织管理员 + OrgManager = 11, + /// 组织教师 + OrgTeacher = 12, + /// 组织学生 + OrgStudent = 13, + /// 组织访客 + OrgGuest = 14, } impl Role { /// String value of the enum field names used in the ProtoBuf definition. @@ -123,6 +132,10 @@ impl Role { Role::Unknown => "Role_Unknown", Role::Admin => "Role_Admin", Role::User => "Role_User", + Role::OrgManager => "Role_OrgManager", + Role::OrgTeacher => "Role_OrgTeacher", + Role::OrgStudent => "Role_OrgStudent", + Role::OrgGuest => "Role_OrgGuest", } } /// Creates an enum from field names used in the ProtoBuf definition. @@ -131,6 +144,10 @@ impl Role { "Role_Unknown" => Some(Self::Unknown), "Role_Admin" => Some(Self::Admin), "Role_User" => Some(Self::User), + "Role_OrgManager" => Some(Self::OrgManager), + "Role_OrgTeacher" => Some(Self::OrgTeacher), + "Role_OrgStudent" => Some(Self::OrgStudent), + "Role_OrgGuest" => Some(Self::OrgGuest), _ => None, } } diff --git a/manager/Cargo.toml b/manager/Cargo.toml index bb6c07f..159f101 100644 --- a/manager/Cargo.toml +++ b/manager/Cargo.toml @@ -4,12 +4,36 @@ version = "0.1.0" edition = "2021" [dependencies] -tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } rtsa_log = { path = "../crates/rtsa_log" } -rtsa_api = { path = "crates/rtsa_api" } rtsa_db = { path = "../crates/rtsa_db" } +rtsa_dto = { path = "../crates/rtsa_dto" } + +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } serde = { workspace = true } +serde_json = { workspace = true } config = { workspace = true } clap = { workspace = true, features = ["derive"] } enum_dispatch = { workspace = true } anyhow = { workspace = true } +thiserror = { workspace = true } +chrono = { version = "0.4.38", features = ["serde"] } +axum = "0.7.5" +axum-extra = { version = "0.9.3", features = ["typed-header"] } +jsonwebtoken = "9.3.0" +tower-http = { version = "0.6.0", features = ["cors"] } +async-graphql = { version = "7.0.7", features = ["chrono", "dataloader"] } +async-graphql-axum = "7.0.6" +base64 = "0.22.1" +sysinfo = "0.31.3" +reqwest = { version = "0.12.7", default-features = false, features = ["rustls-tls", "json"] } + +[dev-dependencies] +sqlx = { workspace = true, features = [ + "runtime-tokio", + "macros", + "chrono", + "json", + "derive", + "postgres", + "uuid", +] } diff --git a/manager/conf/default.toml b/manager/conf/default.toml index 1a2dd81..7ba1be5 100644 --- a/manager/conf/default.toml +++ b/manager/conf/default.toml @@ -5,10 +5,3 @@ port = 8765 [log] level = "debug" - -[sso] -base_url = "http://localhost:8080" -login_url = "/api/login" -logout_url = "/api/login/logout" -user_info_url = "/api/login/getUserInfo" -sync_user_url = "/api/userinfo/list/all" diff --git a/manager/conf/dev.toml b/manager/conf/dev.toml index 839879b..3f2297a 100644 --- a/manager/conf/dev.toml +++ b/manager/conf/dev.toml @@ -1,5 +1,2 @@ [database] url = "postgresql://joylink:Joylink@0503@localhost:5432/joylink" - -[sso] -base_url = "http://192.168.33.233/rtss-server" diff --git a/manager/conf/local_test.toml b/manager/conf/local_test.toml index c876abf..465fe5c 100644 --- a/manager/conf/local_test.toml +++ b/manager/conf/local_test.toml @@ -3,6 +3,3 @@ url = "postgresql://joylink:Joylink@0503@10.11.11.2:5432/joylink" [log] level = "debug" - -[sso] -base_url = "http://192.168.33.233/rtss-server" diff --git a/manager/crates/rtsa_api/Cargo.toml b/manager/crates/rtsa_api/Cargo.toml deleted file mode 100644 index c196377..0000000 --- a/manager/crates/rtsa_api/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "rtsa_api" -version = "0.1.0" -edition = "2021" - -[dependencies] -anyhow = { workspace = true } -tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -chrono = { version = "0.4.38", features = ["serde"] } -axum = "0.7.5" -axum-extra = { version = "0.9.3", features = ["typed-header"] } -jsonwebtoken = "9.3.0" -tower-http = { version = "0.6.0", features = ["cors"] } -async-graphql = { version = "7.0.7", features = ["chrono", "dataloader"] } -async-graphql-axum = "7.0.6" -base64 = "0.22.1" -sysinfo = "0.31.3" -reqwest = { version = "0.12.7", default-features = false, features = ["rustls-tls", "json"] } - -rtsa_log = { path = "../../../crates/rtsa_log" } -rtsa_db = { path = "../../../crates/rtsa_db" } -rtsa_dto = { path = "../../../crates/rtsa_dto" } diff --git a/manager/crates/rtsa_api/src/lib.rs b/manager/crates/rtsa_api/src/lib.rs deleted file mode 100644 index 37142f1..0000000 --- a/manager/crates/rtsa_api/src/lib.rs +++ /dev/null @@ -1,7 +0,0 @@ -// mod jwt_auth; -mod apis; -mod loader; -mod server; -mod user_auth; - -pub use server::*; diff --git a/manager/crates/rtsa_api/src/user_auth/mod.rs b/manager/crates/rtsa_api/src/user_auth/mod.rs deleted file mode 100644 index 854757c..0000000 --- a/manager/crates/rtsa_api/src/user_auth/mod.rs +++ /dev/null @@ -1,323 +0,0 @@ -use std::{ - collections::{HashMap, HashSet}, - sync::{Arc, Mutex}, -}; - -use async_graphql::Guard; -use axum::http::HeaderMap; -use chrono::{DateTime, Local}; -use rtsa_dto::common::Role; -use rtsa_log::tracing::error; -use serde::{Deserialize, Serialize}; - -mod jwt_auth; -pub use jwt_auth::*; - -pub struct RoleGuard { - role: Role, -} - -impl RoleGuard { - pub fn new(role: Role) -> Self { - Self { role } - } -} - -impl Guard for RoleGuard { - async fn check(&self, ctx: &async_graphql::Context<'_>) -> async_graphql::Result<()> { - if let Some(token) = ctx.data_opt::() { - // 从ctx中获取UserAuthCache, 从cache中获取用户信息 - let user_auth_cache = ctx.data::().unwrap(); - let user_info = user_auth_cache.query_user(&token.0).await?; - // 判断用户角色 - if user_info.roles().contains(&Role::Admin) { - return Ok(()); - } - if user_info.roles().contains(&self.role) { - return Ok(()); - } - } - Err(async_graphql::Error::new("Unauthorized")) - } -} - -pub struct UserAuthCache { - // TODO: 使用 LRU 等缓存策略,而不是简单的 HashMap - cache: Arc>>, - client: UserAuthClient, -} - -impl UserAuthCache { - pub fn new(user_auth_client: UserAuthClient) -> Self { - Self { - cache: Arc::new(Mutex::new(HashMap::with_capacity(100))), - client: user_auth_client, - } - } -} - -pub struct Token(pub String); - -pub fn get_token_from_headers(headers: &HeaderMap) -> Option { - headers - .get("Token") - .and_then(|token| token.to_str().map(|s| Token(s.to_string())).ok()) -} - -#[derive(Debug, Clone)] -pub struct UserAuthClient { - pub base_url: String, - pub login_url: String, - pub logout_url: String, - pub user_info_url: String, - pub sync_user_url: String, -} - -impl UserAuthClient { - #[allow(dead_code)] - async fn login(&self, login_info: LoginInfo) -> anyhow::Result { - let url = format!("{}{}", self.base_url, self.login_url); - let response = reqwest::Client::new() - .post(&url) - .json(&login_info) - .send() - .await?; - - let common = response.json::().await?; - - if common.code != 200 { - // 记录详细日志 - error!("Login failed with code {}: {}", common.code, common.message); - return Err(anyhow::anyhow!(common.message)); - } - - // 安全地处理 Option 类型 - match common.data { - Some(data) => { - if let Some(token_str) = data.as_str() { - Ok(token_str.to_string()) - } else { - // 记录详细日志 - error!("Data is not a string"); - Err(anyhow::anyhow!("Data is not a string")) - } - } - None => { - // 记录详细日志 - error!("No data returned"); - Err(anyhow::anyhow!("No data returned")) - } - } - } - - async fn query_user_info(&self, token: &str) -> anyhow::Result { - let url = format!("{}{}?token={token}", self.base_url, self.user_info_url); - let response = reqwest::get(url).await?; - let common = response.json::().await?; - if common.code != 200 { - return Err(anyhow::anyhow!(common.message)); - } - let user_info = serde_json::from_value(common.data.unwrap())?; - Ok(user_info) - } - - pub async fn query_all_users(&self, token: &Token) -> anyhow::Result> { - let url = format!("{}{}", self.base_url, self.sync_user_url); - let response = reqwest::Client::new() - .get(&url) - .header("X-Token", &token.0) - .send() - .await?; - let common = response.json::().await?; - if common.code != 200 { - return Err(anyhow::anyhow!(common.message)); - } - let user_info_list = serde_json::from_value(common.data.unwrap())?; - Ok(user_info_list) - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct LoginInfo { - pub account: String, - pub password: String, - #[serde(rename = "clientId")] - pub client_id: String, - pub secret: String, - pub project: String, -} - -impl Default for LoginInfo { - fn default() -> Self { - Self { - account: "17791995809".to_string(), - password: "e10adc3949ba59abbe56e057f20f883e".to_string(), - client_id: "1".to_string(), - secret: "joylink".to_string(), - project: "DEFAULT".to_string(), - } - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct CommonResponseDto { - pub code: i32, - pub message: String, - pub data: Option, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct UserInfoDto { - pub id: String, - pub name: Option, - pub nickname: Option, - pub roles: Vec, - pub mobile: Option, - pub email: Option, - #[serde(rename = "createTime")] - pub create_time: String, - #[serde(rename = "updateTime")] - pub update_time: Option, -} -impl UserInfoDto { - pub fn id_i32(&self) -> i32 { - self.id - .parse::() - .expect("parse UserInfoDto.id to i32 failed") - } - - pub fn name(&self) -> String { - self.name - .clone() - .or(self.nickname.clone()) - .unwrap_or_default() - } - - pub fn roles(&self) -> Vec { - let mut unique_roles = HashSet::new(); - for role in &self.roles { - match role.as_str() { - "04" | "05" => { - unique_roles.insert(Role::Admin); - } - "01" | "03" => { - unique_roles.insert(Role::User); - } - _ => {} - } - } - unique_roles.into_iter().collect() - } - - pub fn created_at(&self) -> DateTime { - parse_to_date_time(&self.create_time) - } - - pub fn updated_at(&self) -> DateTime { - parse_to_date_time(self.update_time.as_deref().unwrap_or(&self.create_time)) - } -} - -fn parse_to_date_time(s: &str) -> chrono::DateTime { - chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S") - .expect("parse date_time failed") - .and_local_timezone(Local) - .unwrap() -} - -impl From for rtsa_db::SyncUserInfo { - fn from(user_info: UserInfoDto) -> Self { - Self { - id: user_info.id_i32(), - name: user_info.name().replace("'", ""), // 需要处理name中带“'”字符的情况 - password: "".to_string(), // 暂时先不同步 - email: user_info.email.clone(), - mobile: user_info.mobile.clone(), - roles: serde_json::to_value(user_info.roles()).unwrap(), - created_at: parse_to_date_time(&user_info.create_time), - updated_at: user_info.update_time.map(|s| parse_to_date_time(&s)), - } - } -} - -impl UserAuthCache { - #[allow(dead_code)] - pub fn len(&self) -> usize { - let cache = self.cache.lock().unwrap(); - cache.len() - } - - #[allow(dead_code)] - pub fn get_all(&self) -> HashMap { - let cache = self.cache.lock().unwrap(); - cache.clone() - } - - fn insert(&self, key: String, value: UserInfoDto) { - let mut cache = self.cache.lock().unwrap(); - cache.insert(key, value); - } - - fn get(&self, key: &str) -> Option { - let cache = self.cache.lock().unwrap(); - cache.get(key).cloned() - } - - pub async fn query_user(&self, token: &str) -> anyhow::Result { - if let Some(user_info) = self.get(token) { - Ok(user_info) - } else { - let user_info = self.client.query_user_info(token).await?; - self.insert(token.to_string(), user_info.clone()); - Ok(user_info) - } - } -} - -#[cfg(test)] -mod tests { - - use super::*; - - #[test] - fn test_login_info_serialize() { - let login_info = LoginInfo::default(); - let json = serde_json::to_string(&login_info).unwrap(); - println!("{}", json); - assert_eq!( - json, - r#"{"account":"17791995809","password":"e10adc3949ba59abbe56e057f20f883e","clientId":"1","secret":"joylink","project":"DEFAULT"}"# - ); - let login_info: LoginInfo = serde_json::from_str(&json).unwrap(); - assert_eq!(login_info.account, "17791995809"); - } - - #[test] - fn test_chrono_datetime_parse() { - let time_str = "2021-08-31 10:00:00"; - let dt = parse_to_date_time(time_str); - println!("{:?}", dt); - } - - // #[tokio::test] - // async fn test_user_auth_cache() -> anyhow::Result<()> { - // rtsa_log::Logging::default().with_level(Level::DEBUG).init(); - // let client = UserAuthClient { - // base_url: "http://192.168.33.233/rtsa-server".to_string(), - // login_url: "/api/login".to_string(), - // logout_url: "/api/login/logout".to_string(), - // user_info_url: "/api/login/getUserInfo".to_string(), - // sync_user_url: "/api/userinfo/list/all".to_string(), - // }; - // let cache = UserAuthCache::new(client.clone()); - // let token = cache.client.login(LoginInfo::default()).await?; - // let user = cache.query_user(&token).await?; - // println!("token: {}, {:?}", token, user); - // assert_eq!(cache.len(), 1); - - // let user_list = client.query_all_users(&Token(token)).await?; - // println!("{:?}", user_list); - - // Ok(()) - // } -} diff --git a/manager/crates/rtsa_api/src/apis/data_options_def.rs b/manager/src/apis/data_options_def.rs similarity index 100% rename from manager/crates/rtsa_api/src/apis/data_options_def.rs rename to manager/src/apis/data_options_def.rs diff --git a/manager/crates/rtsa_api/src/apis/draft_data.rs b/manager/src/apis/draft_data.rs similarity index 92% rename from manager/crates/rtsa_api/src/apis/draft_data.rs rename to manager/src/apis/draft_data.rs index 457fb3b..b46856a 100644 --- a/manager/crates/rtsa_api/src/apis/draft_data.rs +++ b/manager/src/apis/draft_data.rs @@ -15,7 +15,7 @@ use super::data_options_def::{DataOptions, IscsDataOptions}; use super::release_data::ReleaseDataId; use super::user::UserId; -use crate::user_auth::{RoleGuard, Token, UserAuthCache}; +use crate::user_auth::{Jwt, RoleGuard}; #[derive(Default)] pub struct DraftDataQuery; @@ -47,11 +47,8 @@ impl DraftDataQuery { paging: PageQueryDto, mut query: UserDraftDataFilterDto, ) -> async_graphql::Result> { - let user = ctx - .data::()? - .query_user(&ctx.data::()?.0) - .await?; - query.user_id = user.id_i32(); + let claims = ctx.data::()?.decode()?; + query.user_id = claims.uid; query.data_type = Some(DataType::Iscs); let db_accessor = ctx.data::()?; let paging_result = db_accessor @@ -89,14 +86,11 @@ impl DraftDataQuery { data_type: DataType, name: String, ) -> async_graphql::Result { - let user = ctx - .data::()? - .query_user(&ctx.data::()?.0) - .await?; - let user_id = user.id_i32(); + let claims = ctx.data::()?.decode()?; + let user_id = claims.uid; let db_accessor = ctx.data::()?; let exist = db_accessor - .is_draft_data_exist(user_id, &data_type, &name) + .is_draft_data_exist(user_id, data_type as i32, &name) .await?; Ok(exist) } @@ -111,11 +105,8 @@ impl DraftDataMutation { ctx: &Context<'_>, mut input: CreateDraftDataDto, ) -> async_graphql::Result { - let user = ctx - .data::()? - .query_user(&ctx.data::()?.0) - .await?; - input = input.with_data_type_and_user_id(DataType::Iscs, user.id_i32()); + let claims = ctx.data::()?.decode()?; + input = input.with_data_type_and_user_id(DataType::Iscs, claims.uid); let db_accessor = ctx.data::()?; let draft_data = db_accessor.create_draft_data(input.into()).await?; Ok(draft_data.into()) @@ -193,11 +184,8 @@ impl DraftDataMutation { id: i32, name: String, ) -> async_graphql::Result { - let user = ctx - .data::()? - .query_user(&ctx.data::()?.0) - .await?; - let user_id = user.id_i32(); + let claims = ctx.data::()?.decode()?; + let user_id = claims.uid; let db_accessor = ctx.data::()?; let draft_data = db_accessor.save_as_new_draft(id, &name, user_id).await?; Ok(draft_data.into()) @@ -227,7 +215,7 @@ impl From> for rtsa_db::CreateDraftData { fn from(value: CreateDraftDataDto) -> Self { let cdd = Self::new( &value.name, - value.data_type.expect("need data_type"), + value.data_type.expect("need data_type") as i32, value.user_id.expect("CreateDraftDataDto need user_id"), ); if value.options.is_some() { @@ -256,7 +244,7 @@ impl From> for rtsa_db::DraftDataQuery Self { user_id: Some(value.user_id), name: value.name, - data_type: value.data_type, + data_type: value.data_type.map(|dt| dt as i32), options: value.options.map(|o| serde_json::to_value(o).unwrap()), is_shared: value.is_shared, } @@ -282,7 +270,7 @@ impl From> for rtsa_db::DraftDataQuery { Self { user_id: value.user_id, name: value.name, - data_type: value.data_type, + data_type: value.data_type.map(|dt| dt as i32), is_shared: Some(true), options: value.options.map(|o| serde_json::to_value(o).unwrap()), } @@ -334,7 +322,7 @@ impl From for DraftDataDto { Self { id: value.id, name: value.name, - data_type: value.data_type, + data_type: DataType::try_from(value.data_type).unwrap(), options: value.options, data: value .data diff --git a/manager/crates/rtsa_api/src/apis/feature.rs b/manager/src/apis/feature.rs similarity index 96% rename from manager/crates/rtsa_api/src/apis/feature.rs rename to manager/src/apis/feature.rs index beeb5ac..7534562 100644 --- a/manager/crates/rtsa_api/src/apis/feature.rs +++ b/manager/src/apis/feature.rs @@ -1,7 +1,7 @@ use crate::{ apis::{PageDto, PageQueryDto}, loader::RtsaDbLoader, - user_auth::{RoleGuard, Token, UserAuthCache}, + user_auth::{Jwt, RoleGuard}, }; use async_graphql::{ dataloader::DataLoader, ComplexObject, Context, InputObject, Object, SimpleObject, @@ -84,11 +84,8 @@ impl FeatureMutation { mut input: CreateFeatureDto, ) -> async_graphql::Result { let dba = ctx.data::()?; - let user = ctx - .data::()? - .query_user(&ctx.data::()?.0) - .await?; - input = input.with_feature_type_and_user_id(FeatureType::Ur, user.id_i32()); + let claims = ctx.data::()?.decode()?; + input = input.with_feature_type_and_user_id(FeatureType::Ur, claims.uid); let feature = dba.create_feature(&input.into()).await?; Ok(feature.into()) } @@ -214,7 +211,7 @@ impl From for FeatureDto { fn from(value: rtsa_db::model::FeatureModel) -> Self { Self { id: value.id, - feature_type: value.feature_type, + feature_type: FeatureType::try_from(value.feature_type).unwrap(), name: value.name, description: value.description, config: value.config, diff --git a/manager/crates/rtsa_api/src/apis/feature_config_def.rs b/manager/src/apis/feature_config_def.rs similarity index 100% rename from manager/crates/rtsa_api/src/apis/feature_config_def.rs rename to manager/src/apis/feature_config_def.rs diff --git a/manager/crates/rtsa_api/src/apis/mod.rs b/manager/src/apis/mod.rs similarity index 82% rename from manager/crates/rtsa_api/src/apis/mod.rs rename to manager/src/apis/mod.rs index ed4ef19..ec0cd10 100644 --- a/manager/crates/rtsa_api/src/apis/mod.rs +++ b/manager/src/apis/mod.rs @@ -1,6 +1,7 @@ use async_graphql::{Enum, InputObject, MergedObject, OutputType, SimpleObject}; use draft_data::{DraftDataMutation, DraftDataQuery}; use feature::{FeatureMutation, FeatureQuery}; +use org::{OrgMutation, OrgQuery}; use release_data::{ReleaseDataMutation, ReleaseDataQuery}; mod sys_info; @@ -10,15 +11,29 @@ mod data_options_def; mod draft_data; mod feature; mod feature_config_def; +mod org; +mod org_user; mod release_data; mod user; +use org_user::{OrgUserMutation, OrgUserQuery}; + +pub use user::UserLoginDto; #[derive(Default, MergedObject)] -pub struct Query(UserQuery, DraftDataQuery, ReleaseDataQuery, FeatureQuery); +pub struct Query( + UserQuery, + OrgQuery, + OrgUserQuery, + DraftDataQuery, + ReleaseDataQuery, + FeatureQuery, +); #[derive(Default, MergedObject)] pub struct Mutation( UserMutation, + OrgMutation, + OrgUserMutation, DraftDataMutation, ReleaseDataMutation, FeatureMutation, @@ -74,8 +89,8 @@ impl PageDto { } } -impl> From> for PageDto { - fn from(value: rtsa_db::common::PageResult) -> Self { +impl> From> for PageDto { + fn from(value: rtsa_db::common::PageData) -> Self { Self::new( value.total, value.data.into_iter().map(|m| m.into()).collect(), diff --git a/manager/src/apis/org.rs b/manager/src/apis/org.rs new file mode 100644 index 0000000..e4aed07 --- /dev/null +++ b/manager/src/apis/org.rs @@ -0,0 +1,119 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use async_graphql::dataloader::Loader; +use async_graphql::{Context, InputObject, Object, SimpleObject}; +use chrono::NaiveDateTime; +use rtsa_db::prelude::*; +use rtsa_dto::common::Role; + +use crate::sys_init::DEFAULT_ORG_CODE; +use crate::{ + loader::RtsaDbLoader, + user_auth::{Jwt, RoleGuard}, +}; + +#[derive(Default)] +pub struct OrgQuery; + +#[derive(Default)] +pub struct OrgMutation; + +#[Object] +impl OrgQuery { + /// 获取组织信息 + async fn get_org(&self, ctx: &Context<'_>, id: i32) -> async_graphql::Result { + let dba = ctx.data::()?; + let org = dba.query_org(id).await?; + Ok(org.into()) + } + + /// 获取默认组织信息 + async fn get_default_org(&self, ctx: &Context<'_>) -> async_graphql::Result { + let dba = ctx.data::()?; + let org = dba.query_org_by_code(DEFAULT_ORG_CODE).await?; + Ok(org.into()) + } +} + +#[Object] +impl OrgMutation { + /// 创建顶级组织 + #[graphql(guard = "RoleGuard::new(Role::Admin)")] + async fn create_top_org( + &self, + ctx: &Context<'_>, + mut c: CreateTopOrgDto, + ) -> async_graphql::Result { + let claims = ctx.data::()?.decode()?; + let dba = ctx.data::()?; + c.creator_id = Some(claims.uid); + let org = dba.create_org(c.into()).await?; + Ok(org.into()) + } +} + +#[derive(Debug, InputObject)] +pub struct CreateTopOrgDto { + pub code: String, + pub name: String, + #[graphql(skip)] + pub creator_id: Option, +} + +impl From for CreateOrg { + fn from(c: CreateTopOrgDto) -> Self { + Self::new(&c.name, c.creator_id.unwrap()).with_code(&c.code) + } +} + +#[derive(Debug, SimpleObject)] +pub struct OrgDto { + pub id: i32, + pub code: Option, + pub name: String, + pub creator_id: i32, + pub created_at: NaiveDateTime, + pub updater_id: i32, + pub updated_at: NaiveDateTime, +} + +impl From for OrgDto { + fn from(org: OrganizationModel) -> Self { + Self { + id: org.id, + code: org.code, + name: org.name, + creator_id: org.creator_id, + created_at: org.created_at.naive_local(), + updater_id: org.updater_id, + updated_at: org.updated_at.naive_local(), + } + } +} + +#[derive(Clone, Copy, Hash, PartialEq, Eq)] +pub struct OrgId { + pub id: i32, +} + +impl OrgId { + pub fn new(id: i32) -> Self { + Self { id } + } +} + +impl Loader for RtsaDbLoader { + type Value = String; + type Error = Arc; + + async fn load(&self, keys: &[OrgId]) -> Result, Self::Error> { + let ids: Vec = keys.iter().map(|k| k.id).collect(); + let rows = self.db_accessor.query_org_names(ids.as_slice()).await?; + let map: HashMap = rows + .into_iter() + .map(|row| (OrgId::new(row.0), row.1)) + .collect(); + Ok(map) + } +} diff --git a/manager/src/apis/org_user.rs b/manager/src/apis/org_user.rs new file mode 100644 index 0000000..61353a4 --- /dev/null +++ b/manager/src/apis/org_user.rs @@ -0,0 +1,145 @@ +use crate::{ + loader::RtsaDbLoader, + user_auth::{Jwt, RoleGuard}, +}; +use async_graphql::{ + dataloader::DataLoader, ComplexObject, Context, InputObject, Object, SimpleObject, +}; +use chrono::NaiveDateTime; +use rtsa_db::prelude::*; +use rtsa_dto::common::Role; + +use super::{org::OrgId, user::UserId}; + +#[derive(Default)] +pub struct OrgUserQuery; + +#[derive(Default)] +pub struct OrgUserMutation; + +#[Object] +impl OrgUserQuery { + /// 获取组织用户信息 + #[graphql(guard = "RoleGuard::new(Role::User)")] + async fn org_user_info( + &self, + ctx: &Context<'_>, + org_id: i32, + user_id: i32, + ) -> async_graphql::Result { + let org_user = ctx + .data::()? + .query_org_user(org_id, user_id) + .await?; + Ok(org_user.into()) + } +} + +#[Object] +impl OrgUserMutation { + /// 绑定组织用户(用于组织和用户都已经存在的情况) + #[graphql( + guard = "RoleGuard::new(Role::Admin).or(RoleGuard::new(Role::OrgManager)).or(RoleGuard::new(Role::OrgTeacher))" + )] + async fn bind_org_user( + &self, + ctx: &Context<'_>, + mut bind: BindOrgUserDto, + ) -> async_graphql::Result { + let claims = ctx.data::()?.decode()?; + bind.creator_id = Some(claims.uid); + let org_user = ctx + .data::()? + .bind_org_user(bind.into()) + .await?; + Ok(org_user.into()) + } + /// 删除组织用户 + #[graphql(guard = "RoleGuard::new(Role::Admin)")] + async fn delete_org_user(&self, ctx: &Context<'_>, id: i32) -> async_graphql::Result { + ctx.data::()?.delete_org_user(id).await?; + Ok(true) + } +} + +/// 绑定组织用户输入对象 +#[derive(InputObject)] +pub struct BindOrgUserDto { + pub org_id: i32, + pub user_id: i32, + pub student_id: Option, + pub roles: Vec, + #[graphql(skip)] + pub creator_id: Option, +} + +impl From for BindOrgUser { + fn from(value: BindOrgUserDto) -> Self { + let mut obj = Self::new(value.org_id, value.user_id, value.creator_id.unwrap()) + .with_roles(serde_json::to_value(value.roles).unwrap()); + if let Some(student_id) = value.student_id { + obj = obj.with_student_id(&student_id); + } + obj + } +} + +#[derive(Debug, SimpleObject)] +pub struct OrgUserDto { + pub id: i32, + pub org_id: i32, + pub user_id: i32, + pub student_id: Option, + pub roles: Vec, + pub creator_id: i32, + pub created_at: NaiveDateTime, + pub updater_id: i32, + pub updated_at: NaiveDateTime, +} + +#[ComplexObject] +impl OrgUserDto { + /// 获取组织name + async fn org_name(&self, ctx: &Context<'_>) -> async_graphql::Result> { + let loader = ctx.data_unchecked::>(); + let name = loader.load_one(OrgId::new(self.org_id)).await?; + Ok(name) + } + + /// 获取用户name + async fn user_name(&self, ctx: &Context<'_>) -> async_graphql::Result> { + let loader = ctx.data_unchecked::>(); + let name = loader.load_one(UserId::new(self.user_id)).await?; + Ok(name) + } + + /// 获取创建者name + async fn creator_name(&self, ctx: &Context<'_>) -> async_graphql::Result> { + let loader = ctx.data_unchecked::>(); + let name = loader.load_one(UserId::new(self.creator_id)).await?; + Ok(name) + } + + /// 获取更新者name + async fn updater_name(&self, ctx: &Context<'_>) -> async_graphql::Result> { + let loader = ctx.data_unchecked::>(); + let name = loader.load_one(UserId::new(self.updater_id)).await?; + Ok(name) + } +} + +impl From for OrgUserDto { + fn from(value: OrganizationUserModel) -> Self { + Self { + id: value.id, + org_id: value.organization_id, + user_id: value.user_id, + student_id: value.student_id.clone(), + roles: serde_json::from_value(value.roles).unwrap(), + creator_id: value.creator_id, + created_at: value.created_at.naive_local(), + updater_id: value.updater_id, + updated_at: value.updated_at.naive_local(), + } + } +} diff --git a/manager/crates/rtsa_api/src/apis/release_data.rs b/manager/src/apis/release_data.rs similarity index 93% rename from manager/crates/rtsa_api/src/apis/release_data.rs rename to manager/src/apis/release_data.rs index 7a4f066..1ba3e0e 100644 --- a/manager/crates/rtsa_api/src/apis/release_data.rs +++ b/manager/src/apis/release_data.rs @@ -18,7 +18,7 @@ use super::data_options_def::{DataOptions, IscsDataOptions}; use super::user::UserId; use super::{PageDto, PageQueryDto}; -use crate::user_auth::{RoleGuard, Token, UserAuthCache}; +use crate::user_auth::{Jwt, RoleGuard}; #[derive(Default)] pub struct ReleaseDataQuery; @@ -81,7 +81,7 @@ impl ReleaseDataQuery { ) -> async_graphql::Result { let db_accessor = ctx.data::()?; let result = db_accessor - .is_release_data_name_exist(data_type, &name) + .is_release_data_name_exist(data_type as i32, &name) .await?; Ok(result) } @@ -127,14 +127,10 @@ impl ReleaseDataMutation { name: String, description: String, ) -> async_graphql::Result { - let user = ctx - .data::()? - .query_user(&ctx.data::()?.0) - .await?; - let user_id = user.id_i32(); + let claims = ctx.data::()?.decode()?; let db_accessor = ctx.data::()?; let result = db_accessor - .release_new_from_draft(draft_id, &name, &description, Some(user_id)) + .release_new_from_draft(draft_id, &name, &description, Some(claims.uid)) .await?; Ok(result.into()) } @@ -147,14 +143,10 @@ impl ReleaseDataMutation { draft_id: i32, description: String, ) -> async_graphql::Result { - let user = ctx - .data::()? - .query_user(&ctx.data::()?.0) - .await?; - let user_id = user.id_i32(); + let claims = ctx.data::()?.decode()?; let db_accessor = ctx.data::()?; let result = db_accessor - .release_to_existing(draft_id, &description, Some(user_id)) + .release_to_existing(draft_id, &description, Some(claims.uid)) .await?; Ok(result.into()) } @@ -209,14 +201,10 @@ impl ReleaseDataMutation { ctx: &Context<'_>, version_id: i32, ) -> async_graphql::Result { - let user = ctx - .data::()? - .query_user(&ctx.data::()?.0) - .await?; - let user_id = user.id_i32(); + let claims = ctx.data::()?.decode()?; let db_accessor = ctx.data::()?; let result = db_accessor - .create_draft_from_release_version(version_id, user_id) + .create_draft_from_release_version(version_id, claims.uid) .await?; Ok(result.into()) } @@ -240,7 +228,7 @@ impl From> for rtsa_db::ReleaseData Self { name: value.name, user_id: value.user_id, - data_type: value.data_type, + data_type: value.data_type.map(|dt| dt as i32), options: value.options.map(|o| serde_json::to_value(o).unwrap()), is_published: value.is_published, } @@ -422,7 +410,7 @@ impl From for ReleaseDataDto { Self { id: model.id, name: model.name, - data_type: model.data_type, + data_type: DataType::try_from(model.data_type).unwrap(), options: model.options, used_version_id: model.used_version_id, user_id: model.user_id, diff --git a/manager/crates/rtsa_api/src/apis/sys_info.rs b/manager/src/apis/sys_info.rs similarity index 100% rename from manager/crates/rtsa_api/src/apis/sys_info.rs rename to manager/src/apis/sys_info.rs diff --git a/manager/crates/rtsa_api/src/apis/user.rs b/manager/src/apis/user.rs similarity index 51% rename from manager/crates/rtsa_api/src/apis/user.rs rename to manager/src/apis/user.rs index b9e240c..a9059a5 100644 --- a/manager/crates/rtsa_api/src/apis/user.rs +++ b/manager/src/apis/user.rs @@ -2,12 +2,12 @@ use std::{collections::HashMap, sync::Arc}; use async_graphql::{dataloader::Loader, Context, InputObject, Object, SimpleObject}; use chrono::NaiveDateTime; -use rtsa_db::{DbAccessError, RtsaDbAccessor, UserAccessor}; +use rtsa_db::{model::UserModel, DbAccessError, RtsaDbAccessor, UserAccessor}; +use serde_json::json; use crate::{ loader::RtsaDbLoader, - user_auth::{build_jwt, Claims, RoleGuard, Token, UserAuthCache, UserInfoDto}, - UserAuthClient, + user_auth::{handle_login, Jwt, RoleGuard}, }; use rtsa_dto::common::Role; @@ -16,29 +16,31 @@ use super::{PageDto, PageQueryDto}; #[derive(Default)] pub struct UserQuery; +#[derive(Default)] +pub struct UserMutation; + #[Object] impl UserQuery { + /// 用户登陆 + async fn user_login( + &self, + ctx: &Context<'_>, + info: UserLoginDto, + ) -> async_graphql::Result { + let db_accessor = ctx.data::()?; + let jwt = handle_login(db_accessor, info).await?; + Ok(jwt.0) + } + /// 获取用户信息 #[graphql(guard = "RoleGuard::new(Role::User)")] async fn login_user_info(&self, ctx: &Context<'_>) -> async_graphql::Result { - let user = ctx - .data::()? - .query_user(&ctx.data::()?.0) - .await?; + let claims = ctx.data::()?.decode()?; + let db_accessor = ctx.data::()?; + let user = db_accessor.query_user(claims.uid).await?; Ok(user.into()) } - /// 获取jwt令牌(mqtt验证) - #[graphql(guard = "RoleGuard::new(Role::User)")] - async fn get_jwt(&self, ctx: &Context<'_>) -> async_graphql::Result { - let user = ctx - .data::()? - .query_user(&ctx.data::()?.0) - .await?; - let jwt = build_jwt(Claims::new(user.id_i32()))?; - Ok(jwt.0) - } - /// 分页查询用户(系统管理) #[graphql(guard = "RoleGuard::new(Role::Admin)")] async fn user_paging( @@ -53,33 +55,74 @@ impl UserQuery { } } -#[derive(Default)] -pub struct UserMutation; - #[Object] impl UserMutation { - /// 同步用户 - #[graphql(guard = "RoleGuard::new(Role::Admin)")] - async fn sync_user(&self, ctx: &Context<'_>) -> async_graphql::Result { - let http_client = ctx.data::()?; - let users = http_client.query_all_users(ctx.data::()?).await?; + /// 用户注册 + async fn register_user( + &self, + ctx: &Context<'_>, + register: RegisterUserDto, + ) -> async_graphql::Result { let dba = ctx.data::()?; - dba.sync_user( - users - .into_iter() - .map(|u| u.into()) - .collect::>() - .as_slice(), - ) - .await?; - Ok(true) + let user = dba.register_user(register.into()).await?; + Ok(user.into()) + } + + /// 更新用户角色 + #[graphql(guard = "RoleGuard::new(Role::Admin)")] + async fn update_user_roles( + &self, + ctx: &Context<'_>, + user_id: i32, + roles: Vec, + ) -> async_graphql::Result { + let dba = ctx.data::()?; + let user = dba + .update_user_roles(user_id, serde_json::to_value(roles).unwrap()) + .await?; + Ok(user.into()) + } +} + +#[derive(Debug, InputObject)] +pub struct UserLoginDto { + pub username: String, + pub password: String, + /// 组织id,组织用户登陆需要 + pub org_id: Option, +} + +#[derive(Debug, InputObject)] +pub struct RegisterUserDto { + /// 用户名, 必填,唯一,用于登陆 + pub username: String, + /// 昵称, 必填 + pub nickname: String, + pub mobile: Option, + pub email: Option, + pub password: String, +} + +impl From for rtsa_db::RegisterUser { + fn from(value: RegisterUserDto) -> Self { + let mut user = Self::new(&value.username, &value.password) + .with_nickname(&value.nickname) + .with_roles(json!([Role::User])); + if let Some(mobile) = value.mobile { + user = user.with_mobile(&mobile); + } + if let Some(email) = value.email { + user = user.with_email(&email); + } + user } } #[derive(Debug, InputObject)] pub struct UserQueryDto { pub id: Option, - pub name: Option, + pub username: Option, + pub nickname: Option, pub email: Option, pub mobile: Option, pub roles: Option>, @@ -89,7 +132,8 @@ impl From for rtsa_db::UserPageFilter { fn from(value: UserQueryDto) -> Self { Self { id: value.id, - name: value.name, + username: value.username, + nickname: value.nickname, email: value.email, mobile: value.mobile, roles: value.roles.map(|r| serde_json::to_value(r).unwrap()), @@ -100,7 +144,8 @@ impl From for rtsa_db::UserPageFilter { #[derive(Debug, SimpleObject)] pub struct UserDto { pub id: i32, - pub name: String, + pub username: String, + pub nickname: String, pub mobile: Option, pub email: Option, pub roles: Vec, @@ -108,28 +153,15 @@ pub struct UserDto { pub updated_at: NaiveDateTime, } -impl From for UserDto { - fn from(value: UserInfoDto) -> Self { - Self { - id: value.id_i32(), - name: value.name(), - mobile: value.mobile.clone(), - email: value.email.clone(), - roles: value.roles(), - created_at: value.created_at().naive_local(), - updated_at: value.updated_at().naive_local(), - } - } -} - -impl From for UserDto { - fn from(value: rtsa_db::model::UserModel) -> Self { +impl From for UserDto { + fn from(value: UserModel) -> Self { Self { id: value.id, - name: value.username, - mobile: value.mobile, - email: value.email, - roles: value.roles.0, + username: value.username.clone(), + nickname: value.nickname.clone(), + mobile: value.mobile.clone(), + email: value.email.clone(), + roles: serde_json::from_value(value.roles).unwrap(), created_at: value.created_at.naive_local(), updated_at: value.updated_at.naive_local(), } @@ -153,7 +185,10 @@ impl Loader for RtsaDbLoader { async fn load(&self, keys: &[UserId]) -> Result, Self::Error> { let ids: Vec = keys.iter().map(|k| k.id).collect(); - let rows = self.db_accessor.query_user_name(ids.as_slice()).await?; + let rows = self + .db_accessor + .query_user_nicknames(ids.as_slice()) + .await?; let map: HashMap = rows .into_iter() .map(|row| (UserId::new(row.0), row.1)) diff --git a/manager/src/app_config.rs b/manager/src/app_config.rs index 3b10d4b..8934563 100644 --- a/manager/src/app_config.rs +++ b/manager/src/app_config.rs @@ -29,23 +29,12 @@ impl From for rtsa_log::Logging { } } -#[derive(Debug, Deserialize)] -#[allow(unused)] -pub struct Sso { - pub base_url: String, - pub login_url: String, - pub logout_url: String, - pub user_info_url: String, - pub sync_user_url: String, -} - #[derive(Debug, Deserialize)] #[allow(unused)] pub struct AppConfig { pub server: Server, pub log: Log, pub database: Database, - pub sso: Sso, } impl AppConfig { diff --git a/manager/src/commands/cmd.rs b/manager/src/commands/cmd.rs index fc2583a..04f294b 100644 --- a/manager/src/commands/cmd.rs +++ b/manager/src/commands/cmd.rs @@ -1,7 +1,7 @@ use clap::Parser; use enum_dispatch::enum_dispatch; -use crate::{app_config, CmdExecutor}; +use crate::{app_config, server, CmdExecutor}; use super::DbSubCommand; @@ -33,16 +33,10 @@ impl CmdExecutor for ServerOpts { app_config::AppConfig::new(&self.config_path).expect("Failed to load app config"); let log: rtsa_log::Logging = app_config.log.into(); log.init(); - rtsa_api::serve( - rtsa_api::ServerConfig::new(&app_config.database.url, app_config.server.port) - .with_user_auth_client(rtsa_api::UserAuthClient { - base_url: app_config.sso.base_url, - login_url: app_config.sso.login_url, - logout_url: app_config.sso.logout_url, - user_info_url: app_config.sso.user_info_url, - sync_user_url: app_config.sso.sync_user_url, - }), - ) + server::serve(server::ServerConfig::new( + &app_config.database.url, + app_config.server.port, + )) .await } } diff --git a/manager/src/commands/db.rs b/manager/src/commands/db.rs index c2e0844..42a4561 100644 --- a/manager/src/commands/db.rs +++ b/manager/src/commands/db.rs @@ -1,7 +1,7 @@ use clap::Parser; use enum_dispatch::enum_dispatch; -use crate::{app_config, CmdExecutor}; +use crate::{app_config, sys_init::init_default_user_and_org, CmdExecutor}; #[derive(Parser, Debug)] #[enum_dispatch(CmdExecutor)] @@ -22,6 +22,10 @@ impl CmdExecutor for MigrateOpts { async fn execute(&self) -> anyhow::Result<()> { let app_config = app_config::AppConfig::new(&self.config_path).expect("Failed to load app config"); - rtsa_db::run_migrations(&app_config.database.url).await + let log: rtsa_log::Logging = app_config.log.into(); + log.init(); + rtsa_db::run_migrations(&app_config.database.url).await?; + rtsa_db::init_default_db_accessor(&app_config.database.url).await; + init_default_user_and_org().await } } diff --git a/manager/src/error.rs b/manager/src/error.rs new file mode 100644 index 0000000..5eca38e --- /dev/null +++ b/manager/src/error.rs @@ -0,0 +1,18 @@ +use thiserror::Error; + +#[allow(dead_code)] +#[derive(Error, Debug)] +pub enum BusinessError { + #[error("未知错误: {0}")] + Unknown(String), + #[error("{0}")] + DbError(#[from] rtsa_db::DbAccessError), + #[error(transparent)] + Other(#[from] anyhow::Error), + #[error("非法参数:{0}")] + InvalidArgument(String), + #[error("认证失败:{0}")] + AuthError(String), + #[error("JWT错误:{0}")] + JwtError(#[from] jsonwebtoken::errors::Error), +} diff --git a/manager/src/lib.rs b/manager/src/lib.rs index e891c93..748b622 100644 --- a/manager/src/lib.rs +++ b/manager/src/lib.rs @@ -1,4 +1,13 @@ mod app_config; mod commands; +mod sys_init; +// mod jwt_auth; +mod apis; +mod error; +mod loader; +mod server; +mod user_auth; + +pub use server::*; pub use commands::*; diff --git a/manager/crates/rtsa_api/src/loader/mod.rs b/manager/src/loader/mod.rs similarity index 100% rename from manager/crates/rtsa_api/src/loader/mod.rs rename to manager/src/loader/mod.rs diff --git a/manager/crates/rtsa_api/src/server.rs b/manager/src/server.rs similarity index 69% rename from manager/crates/rtsa_api/src/server.rs rename to manager/src/server.rs index 6da2201..f55e412 100644 --- a/manager/crates/rtsa_api/src/server.rs +++ b/manager/src/server.rs @@ -19,13 +19,10 @@ use crate::apis::{Mutation, Query}; use crate::loader::RtsaDbLoader; use crate::user_auth; -pub use crate::user_auth::UserAuthClient; - #[derive(Clone)] pub struct ServerConfig { pub database_url: String, pub port: u16, - pub user_auth_client: Option, } impl ServerConfig { @@ -33,27 +30,17 @@ impl ServerConfig { Self { database_url: database_url.to_string(), port, - user_auth_client: None, } } - pub fn with_user_auth_client(mut self, user_auth_client: user_auth::UserAuthClient) -> Self { - self.user_auth_client = Some(user_auth_client); - self - } - pub fn to_socket_addr(&self) -> String { format!("0.0.0.0:{}", self.port) } } pub async fn serve(config: ServerConfig) -> anyhow::Result<()> { - let client = config - .user_auth_client - .clone() - .expect("user auth client not configured"); let dba = rtsa_db::get_db_accessor(&config.database_url).await; - let schema = new_schema(SchemaOptions::new(client, dba)); + let schema = new_schema(SchemaOptions::new(dba)); let app = Router::new() .route("/", get(graphiql).post(graphql_handler)) @@ -95,27 +82,18 @@ async fn graphiql() -> impl IntoResponse { pub type RtsaAppSchema = Schema; pub struct SchemaOptions { - pub user_auth_client: UserAuthClient, - pub user_info_cache: user_auth::UserAuthCache, pub rtsa_dba: RtsaDbAccessor, } impl SchemaOptions { - pub fn new(user_auth_client: UserAuthClient, rtsa_dba: RtsaDbAccessor) -> Self { - let user_info_cache = user_auth::UserAuthCache::new(user_auth_client.clone()); - Self { - user_auth_client, - user_info_cache, - rtsa_dba, - } + pub fn new(rtsa_dba: RtsaDbAccessor) -> Self { + Self { rtsa_dba } } } pub fn new_schema(options: SchemaOptions) -> RtsaAppSchema { let loader = RtsaDbLoader::new(options.rtsa_dba.clone()); Schema::build(Query::default(), Mutation::default(), EmptySubscription) - .data(options.user_auth_client) - .data(options.user_info_cache) .data(options.rtsa_dba) .data(DataLoader::new(loader, tokio::spawn)) // .data(MutexSimulationManager::default()) @@ -131,15 +109,6 @@ mod tests { let dba = rtsa_db::get_db_accessor("postgresql://joylink:Joylink@0503@localhost:5432/joylink") .await; - let _ = new_schema(SchemaOptions::new( - crate::UserAuthClient { - base_url: "".to_string(), - login_url: "".to_string(), - logout_url: "".to_string(), - user_info_url: "".to_string(), - sync_user_url: "".to_string(), - }, - dba, - )); + let _ = new_schema(SchemaOptions::new(dba)); } } diff --git a/manager/src/sys_init/mod.rs b/manager/src/sys_init/mod.rs new file mode 100644 index 0000000..7968eb4 --- /dev/null +++ b/manager/src/sys_init/mod.rs @@ -0,0 +1,78 @@ +//! 系统初始化 + +use rtsa_db::{OrgAccessor, UserAccessor}; +use rtsa_dto::common::Role; +use rtsa_log::tracing::{debug, error, info}; +use serde_json::json; + +pub const ADMIN_USER_NAME: &str = "_jl_admin_"; +pub const ADMIN_USER_PASSWORD: &str = "joylink0503"; + +pub const DEFAULT_ORG_CODE: &str = "default"; +const DEFAULT_ORG_NAME: &str = "默认机构"; + +/// 初始化默认用户及组织 +/// 注意:需要在初始化默认数据库后调用,初始化默认数据库方法: [rtsa_db::init_default_db_accessor] or [rtsa_db::set_default_db_accessor] +pub(crate) async fn init_default_user_and_org() -> anyhow::Result<()> { + let admin_user = rtsa_db::RegisterUser::new(ADMIN_USER_NAME, ADMIN_USER_PASSWORD) + .with_roles(json!([Role::Admin])); + + let db_accessor = rtsa_db::get_default_db_accessor(); + let admin = db_accessor.register_user(admin_user).await; + match admin { + Ok(admin) => { + let org = + rtsa_db::CreateOrg::new(DEFAULT_ORG_NAME, admin.id).with_code(DEFAULT_ORG_CODE); + db_accessor.create_org(org).await?; + info!( + "初始化默认用户及组织成功. 用户: {}, 密码: {}, 组织: {}", + ADMIN_USER_NAME, ADMIN_USER_PASSWORD, DEFAULT_ORG_NAME + ); + Ok(()) + } + Err(e) => { + if db_accessor.is_user_name_exist(ADMIN_USER_NAME).await? + && db_accessor.is_org_code_exist(DEFAULT_ORG_CODE).await? + { + debug!("默认用户及组织已存在"); + } else { + error!("初始化默认用户失败: {:?}", e); + } + Ok(()) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rtsa_db::RtsaDbAccessor; + use rtsa_log::{tracing::Level, Logging}; + use sqlx::PgPool; + + #[sqlx::test(migrator = "rtsa_db::MIGRATOR")] + async fn test_init_default_user_and_org(pool: PgPool) -> anyhow::Result<()> { + Logging::default().with_level(Level::DEBUG).init(); + let accessor = RtsaDbAccessor::new(pool); + rtsa_db::set_default_db_accessor(accessor); + init_default_user_and_org().await.unwrap(); + + // 重复初始化 + init_default_user_and_org().await?; + + // 验证用户及组织是否存在 + let db_accessor = rtsa_db::get_default_db_accessor(); + let user = db_accessor.query_user_by_username(ADMIN_USER_NAME).await?; + assert_eq!(user.username, ADMIN_USER_NAME); + assert_eq!(user.password, ADMIN_USER_PASSWORD); + assert_eq!(user.nickname, ADMIN_USER_NAME); + assert_eq!(user.roles, json!([Role::Admin])); + + let org = db_accessor.query_org_by_code(DEFAULT_ORG_CODE).await?; + assert_eq!(org.code.unwrap(), DEFAULT_ORG_CODE); + assert_eq!(org.name, DEFAULT_ORG_NAME); + assert_eq!(org.creator_id, user.id); + + Ok(()) + } +} diff --git a/manager/crates/rtsa_api/src/user_auth/jwt_auth.rs b/manager/src/user_auth/jwt_auth.rs similarity index 53% rename from manager/crates/rtsa_api/src/user_auth/jwt_auth.rs rename to manager/src/user_auth/jwt_auth.rs index e49ba48..3336cd3 100644 --- a/manager/crates/rtsa_api/src/user_auth/jwt_auth.rs +++ b/manager/src/user_auth/jwt_auth.rs @@ -1,9 +1,11 @@ use std::sync::LazyLock; -use async_graphql::Result; +use axum::http::HeaderMap; use jsonwebtoken::{decode, DecodingKey, EncodingKey, Validation}; use serde::{Deserialize, Serialize}; +use crate::error::BusinessError; + static KEYS: LazyLock = LazyLock::new(|| { // let secret = std::env::var("JWT_SECRET").expect("JWT_SECRET must be set"); let secret = "joylink".to_string(); @@ -24,12 +26,34 @@ impl Keys { } } -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Jwt(pub String); +impl Jwt { + pub fn build_from(claims: Claims) -> Result { + let token = + jsonwebtoken::encode(&jsonwebtoken::Header::default(), &claims, &KEYS.encoding)?; + Ok(Self(token)) + } + + pub fn decode(&self) -> Result { + let data = decode::(&self.0, &KEYS.decoding, &Validation::default())?; + Ok(data.claims) + } +} + +pub fn get_token_from_headers(headers: &HeaderMap) -> Option { + headers + .get("Token") + .and_then(|token| token.to_str().map(|s| Jwt(s.to_string())).ok()) +} + #[derive(Debug, Serialize, Deserialize)] pub struct Claims { - pub id: i32, + /// 用户ID + pub uid: i32, + /// 组织ID + pub oid: i32, exp: usize, // 过期时间,单位秒 } @@ -41,28 +65,18 @@ pub fn get_current_timestamp() -> u64 { .as_secs() } +const EXP_TIME: usize = 3600 * 24 * 5; // 5天 + impl Claims { - pub fn new(id: i32) -> Self { + pub fn new(uid: i32, oid: i32) -> Self { Self { - id, - exp: get_current_timestamp() as usize + 3600 * 24 * 7, // 7天 + uid, + oid, + exp: get_current_timestamp() as usize + EXP_TIME, } } } -/// 构建jwt -pub fn build_jwt(claims: Claims) -> Result { - let token = jsonwebtoken::encode(&jsonwebtoken::Header::default(), &claims, &KEYS.encoding)?; - Ok(Jwt(token)) -} - -/// 解析jwt -#[allow(dead_code)] -pub fn decode_jwt(token: &str) -> Result { - let data = decode::(token, &KEYS.decoding, &Validation::default())?; - Ok(data.claims) -} - #[cfg(test)] mod tests { @@ -71,13 +85,14 @@ mod tests { #[test] fn test_jwt() { rtsa_log::Logging::default().init(); - let claim = Claims::new(5); - let jwt = build_jwt(claim).unwrap(); + let claim = Claims::new(5, 1); + let jwt = Jwt::build_from(claim).unwrap(); println!("jwt: {}", jwt.0); - let result = decode_jwt(&jwt.0); + let result = jwt.decode(); match result { Ok(claims) => { - assert_eq!(claims.id, 5); + assert_eq!(claims.uid, 5); + assert_eq!(claims.oid, 1); } Err(e) => { panic!("Error: {:?}", e); diff --git a/manager/src/user_auth/mod.rs b/manager/src/user_auth/mod.rs new file mode 100644 index 0000000..bcab119 --- /dev/null +++ b/manager/src/user_auth/mod.rs @@ -0,0 +1,135 @@ +use async_graphql::Guard; +use rtsa_db::prelude::*; +use rtsa_dto::common::Role; +use rtsa_log::tracing::warn; + +mod jwt_auth; +use crate::{apis::UserLoginDto, error::BusinessError, sys_init::DEFAULT_ORG_CODE}; +pub use jwt_auth::*; + +pub struct RoleGuard { + role: Role, +} + +impl RoleGuard { + pub fn new(role: Role) -> Self { + Self { role } + } +} + +impl Guard for RoleGuard { + async fn check(&self, ctx: &async_graphql::Context<'_>) -> async_graphql::Result<()> { + if let Some(jwt) = ctx.data_opt::() { + // 从ctx中获取UserAuthCache, 从cache中获取用户信息 + let claims = jwt.decode()?; + let dba = ctx.data::()?; + let user = dba.query_user(claims.uid).await?; + let user_roles: Vec = serde_json::from_value(user.roles)?; + // 判断用户角色, 如果是管理员则直接通过 + if user_roles.contains(&Role::Admin) { + return Ok(()); + } + // 其他用户, 则判断是否有权限 + if user_roles.contains(&self.role) { + return Ok(()); + } + // 判断组织用户角色 + let org_user_roles: Vec; + let org_user = dba.query_org_user(claims.oid, claims.uid).await; + if let Ok(org_user) = org_user { + org_user_roles = serde_json::from_value(org_user.roles)?; + } else { + // 组织用户不存在,构造组织游客角色用户 + org_user_roles = vec![Role::OrgGuest]; + } + if org_user_roles.contains(&self.role) { + return Ok(()); + } + } + Err(async_graphql::Error::new("Unauthorized")) + } +} + +/// 处理用户登录 +pub(crate) async fn handle_login( + db_accessor: &RtsaDbAccessor, + info: UserLoginDto, +) -> Result { + let org_id; + if let Some(oid) = info.org_id { + org_id = oid; + } else { + let default_org = db_accessor.query_org_by_code(DEFAULT_ORG_CODE).await?; + org_id = default_org.id; + } + // 查询用户登陆 + let user = db_accessor + .query_user_login(&info.username, &info.password) + .await; + match user { + Ok(user) => { + // 用户存在 + Ok(Jwt::build_from(Claims::new(user.id, org_id))?) + } + Err(_) => { + // 用户不存在,查询组织用户学工号+用户密码 + // 通过组织id和学工号查询组织用户 + let org_user = db_accessor + .query_org_user_by_student_id(org_id, &info.username) + .await; + if org_user.is_err() { + warn!("用户不存在: username={}, org_id={}", info.username, org_id); + return Err(BusinessError::AuthError( + "用户不存在或密码不正确".to_string(), + )); + } + let org_user = org_user.unwrap(); + let user = db_accessor.query_user(org_user.user_id).await?; + // 检查用户密码 + if rtsa_db::password_util::verify_password(&info.password, &user.password) { + Ok(Jwt::build_from(Claims::new(user.id, org_id))?) + } else { + warn!("密码不匹配: username={}, org_id={}", info.username, org_id); + Err(BusinessError::AuthError( + "用户不存在或密码不正确".to_string(), + )) + } + } + } +} + +#[cfg(test)] +mod tests { + use crate::sys_init::{init_default_user_and_org, ADMIN_USER_NAME, ADMIN_USER_PASSWORD}; + + use super::*; + use rtsa_log::{ + tracing::{info, Level}, + Logging, + }; + + use rtsa_db::RtsaDbAccessor; + use sqlx::PgPool; + + #[sqlx::test(migrator = "rtsa_db::MIGRATOR")] + async fn test_handle_login(pool: PgPool) -> anyhow::Result<()> { + Logging::default().with_level(Level::DEBUG).init(); + let accessor = RtsaDbAccessor::new(pool); + rtsa_db::set_default_db_accessor(accessor.clone()); + init_default_user_and_org().await.unwrap(); + + let info = UserLoginDto { + username: ADMIN_USER_NAME.to_string(), + password: ADMIN_USER_PASSWORD.to_string(), + org_id: None, + }; + let jwt = handle_login(&accessor, info).await.unwrap(); + info!("jwt: {:?}", jwt); + let claims = jwt.decode().unwrap(); + let user = accessor.query_user_by_username(ADMIN_USER_NAME).await?; + assert_eq!(claims.uid, user.id); + let org = accessor.query_org_by_code(DEFAULT_ORG_CODE).await?; + assert_eq!(claims.oid, org.id); + Ok(()) + } +} diff --git a/migrations/20240830095636_init.up.sql b/migrations/20240830095636_init.up.sql index 886cdd7..090870a 100644 --- a/migrations/20240830095636_init.up.sql +++ b/migrations/20240830095636_init.up.sql @@ -8,8 +8,9 @@ CREATE SEQUENCE rtsa.mqtt_client_id_seq; CREATE TABLE rtsa.user ( id SERIAL PRIMARY KEY, -- id 自增主键 - username VARCHAR(128) NOT NULL, -- 用户名 + username VARCHAR(128) NOT NULL UNIQUE, -- 用户名 password VARCHAR(256) NOT NULL, -- 密码 + nickname VARCHAR(128) NOT NULL DEFAULT 'user', -- 昵称 email VARCHAR(128) NULL UNIQUE, -- 邮箱 mobile VARCHAR(16) NULL UNIQUE, -- 手机号 roles JSONB NOT NULL DEFAULT '[]', -- 角色列表 @@ -21,6 +22,12 @@ CREATE TABLE -- 创建用户名称索引 CREATE INDEX ON rtsa.user (username); +-- 创建用户密码索引 +CREATE INDEX ON rtsa.user (password); + +-- 创建用户昵称索引 +CREATE INDEX ON rtsa.user (nickname); + -- 创建用户邮箱索引 CREATE INDEX ON rtsa.user (email); @@ -40,6 +47,8 @@ COMMENT ON COLUMN rtsa.user.username IS '用户名'; COMMENT ON COLUMN rtsa.user.password IS '密码'; +COMMENT ON COLUMN rtsa.user.nickname IS '昵称'; + COMMENT ON COLUMN rtsa.user.email IS '邮箱'; COMMENT ON COLUMN rtsa.user.mobile IS '手机号'; @@ -56,9 +65,9 @@ COMMENT ON COLUMN rtsa.user.updated_at IS '更新时间'; CREATE TABLE rtsa.organization ( id SERIAL PRIMARY KEY, -- id 自增主键 - code VARCHAR(128) NOT NULL UNIQUE, -- 组织编码 + code VARCHAR(128) NULL UNIQUE, -- 组织编码 name VARCHAR(128) NOT NULL, -- 组织名称 - config JSONB NOT NULL DEFAULT '{}', -- 配置数据 + config JSONB NULL, -- 配置数据 parent_id INT NULL, -- 父组织id creator_id INT NOT NULL, -- 创建用户id created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间 @@ -112,8 +121,9 @@ CREATE TABLE id SERIAL PRIMARY KEY, -- id 自增主键 organization_id INT NOT NULL, -- 组织id user_id INT NOT NULL, -- 用户id - student_id VARCHAR(128) NULL, -- 学工号 + student_id VARCHAR(32) NULL, -- 学工号 roles JSONB NOT NULL DEFAULT '[]', -- 组织角色列表 + info JSONB NULL, -- 组织用户额外信息 creator_id INT NOT NULL, -- 创建用户id created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间 updater_id INT NOT NULL, -- 更新用户id @@ -122,7 +132,8 @@ CREATE TABLE FOREIGN KEY (user_id) REFERENCES rtsa.user (id) ON DELETE CASCADE, -- 用户外键 FOREIGN KEY (creator_id) REFERENCES rtsa.user (id) ON DELETE CASCADE, -- 创建用户外键 FOREIGN KEY (updater_id) REFERENCES rtsa.user (id) ON DELETE CASCADE, -- 更新用户外键 - UNIQUE(organization_id, user_id) -- 组织id和用户id唯一 + UNIQUE(organization_id, student_id), -- 组织id+学工号唯一 + UNIQUE(organization_id, user_id) -- 组织id+用户id唯一 ); -- 创建组织用户组织索引 @@ -157,6 +168,8 @@ COMMENT ON COLUMN rtsa.organization_user.student_id IS '学工号'; COMMENT ON COLUMN rtsa.organization_user.roles IS '组织角色列表'; +COMMENT ON COLUMN rtsa.organization_user.info IS '组织用户其他信息'; + COMMENT ON COLUMN rtsa.organization_user.creator_id IS '创建用户id'; COMMENT ON COLUMN rtsa.organization_user.created_at IS '创建时间'; @@ -165,6 +178,60 @@ COMMENT ON COLUMN rtsa.organization_user.updater_id IS '更新用户id'; COMMENT ON COLUMN rtsa.organization_user.updated_at IS '更新时间'; +-- 创建组织数据表 +CREATE TABLE + rtsa.organization_data ( + id SERIAL PRIMARY KEY, -- id 自增主键 + organization_id INT NOT NULL, -- 组织id + data_type INT NOT NULL, -- 数据类型 + data JSONB NOT NULL, -- 数据 + creator_id INT NOT NULL, -- 创建用户id + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间 + updater_id INT NOT NULL, -- 更新用户id + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 更新时间 + FOREIGN KEY (organization_id) REFERENCES rtsa.organization (id) ON DELETE CASCADE, -- 组织外键 + FOREIGN KEY (creator_id) REFERENCES rtsa.user (id) ON DELETE CASCADE, -- 创建用户外键 + FOREIGN KEY (updater_id) REFERENCES rtsa.user (id) ON DELETE CASCADE -- 更新用户外键 + ); + +-- 创建组织数据组织索引 +CREATE INDEX ON rtsa.organization_data (organization_id); + +-- 创建组织数据类型索引 +CREATE INDEX ON rtsa.organization_data (data_type); + +-- 创建组织数据配置项索引 +CREATE INDEX ON rtsa.organization_data USING GIN (data); + +-- 创建组织数据创建用户索引 +CREATE INDEX ON rtsa.organization_data (creator_id); + +-- 创建组织数据更新用户索引 +CREATE INDEX ON rtsa.organization_data (updater_id); + +-- 创建组织数据索引 +CREATE INDEX ON rtsa.organization_data (organization_id, data_type); + +-- 注释组织数据表 +COMMENT ON TABLE rtsa.organization_data IS '组织数据表'; + +-- 注释组织数据表字段 +COMMENT ON COLUMN rtsa.organization_data.id IS 'id 自增主键'; + +COMMENT ON COLUMN rtsa.organization_data.organization_id IS '组织id'; + +COMMENT ON COLUMN rtsa.organization_data.data_type IS '数据类型'; + +COMMENT ON COLUMN rtsa.organization_data.data IS '数据'; + +COMMENT ON COLUMN rtsa.organization_data.creator_id IS '创建用户id'; + +COMMENT ON COLUMN rtsa.organization_data.created_at IS '创建时间'; + +COMMENT ON COLUMN rtsa.organization_data.updater_id IS '更新用户id'; + +COMMENT ON COLUMN rtsa.organization_data.updated_at IS '更新时间'; + -- 创建feature表 CREATE TABLE rtsa.feature ( @@ -503,19 +570,14 @@ COMMENT ON COLUMN rtsa.release_data_set_data.order_index IS '排序索引'; CREATE TABLE rtsa.user_data ( id SERIAL PRIMARY KEY, -- id 自增主键 - organization_id INT NOT NULL, -- 组织id user_id INT NOT NULL, -- 用户id data_type INT NOT NULL, -- 数据类型 data JSONB NOT NULL, -- 数据 created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间 updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 更新时间 - FOREIGN KEY (organization_id) REFERENCES rtsa.organization (id) ON DELETE CASCADE, -- 组织外键 FOREIGN KEY (user_id) REFERENCES rtsa.user (id) ON DELETE CASCADE -- 用户外键 ); --- 创建用户数据组织索引 -CREATE INDEX ON rtsa.user_data (organization_id); - -- 创建用户数据用户索引 CREATE INDEX ON rtsa.user_data (user_id); @@ -526,7 +588,7 @@ CREATE INDEX ON rtsa.user_data (data_type); CREATE INDEX ON rtsa.user_data USING GIN (data); -- 创建用户数据索引 -CREATE INDEX ON rtsa.user_data (organization_id, user_id, data_type); +CREATE INDEX ON rtsa.user_data (user_id, data_type); -- 注释用户数据表 COMMENT ON TABLE rtsa.user_data IS '用户数据表'; diff --git a/rtsa-proto-msg b/rtsa-proto-msg index 1f53057..867c075 160000 --- a/rtsa-proto-msg +++ b/rtsa-proto-msg @@ -1 +1 @@ -Subproject commit 1f53057b3f87790ef27c91399a5bb7e890f05549 +Subproject commit 867c075833ffbe64b0291db921adddc9d5d5a7a3