2 数据库访问模块用户、组织、组织用户基础功能接口实现并测试 3 添加数据库超级管理员用户和默认组织初始化 4 用户注册登录、组织、组织用户相关基本GraphQL接口开发,登录接口测试
This commit is contained in:
parent
15df0ca22c
commit
25a63b1fb7
57
Cargo.lock
generated
57
Cargo.lock
generated
@ -1633,14 +1633,27 @@ name = "manager"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"async-graphql",
|
||||||
|
"async-graphql-axum",
|
||||||
|
"axum",
|
||||||
|
"axum-extra",
|
||||||
|
"base64 0.22.1",
|
||||||
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"config",
|
"config",
|
||||||
"enum_dispatch",
|
"enum_dispatch",
|
||||||
"rtsa_api",
|
"jsonwebtoken",
|
||||||
|
"reqwest",
|
||||||
"rtsa_db",
|
"rtsa_db",
|
||||||
|
"rtsa_dto",
|
||||||
"rtsa_log",
|
"rtsa_log",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"sqlx",
|
||||||
|
"sysinfo",
|
||||||
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tower-http",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2268,14 +2281,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.10.6"
|
version = "1.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
|
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
"regex-automata 0.4.7",
|
"regex-automata 0.4.8",
|
||||||
"regex-syntax 0.8.4",
|
"regex-syntax 0.8.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2289,13 +2302,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-automata"
|
name = "regex-automata"
|
||||||
version = "0.4.7"
|
version = "0.4.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
|
checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
"regex-syntax 0.8.4",
|
"regex-syntax 0.8.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2306,9 +2319,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-syntax"
|
name = "regex-syntax"
|
||||||
version = "0.8.4"
|
version = "0.8.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
|
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
@ -2399,35 +2412,13 @@ dependencies = [
|
|||||||
"zeroize",
|
"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]]
|
[[package]]
|
||||||
name = "rtsa_db"
|
name = "rtsa_db"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
|
"regex",
|
||||||
"rtsa_dto",
|
"rtsa_dto",
|
||||||
"rtsa_log",
|
"rtsa_log",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["crates/*", "manager/crates/*", "manager", "simulation"]
|
members = ["crates/*", "manager", "simulation"]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
@ -27,3 +27,4 @@ config = "0.14.1"
|
|||||||
clap = { version = "4.5.20", features = ["derive"] }
|
clap = { version = "4.5.20", features = ["derive"] }
|
||||||
enum_dispatch = "0.3"
|
enum_dispatch = "0.3"
|
||||||
tower = { version = "0.5.1", features = ["util"] }
|
tower = { version = "0.5.1", features = ["util"] }
|
||||||
|
regex = "1.11.1"
|
||||||
|
@ -18,6 +18,7 @@ sqlx = { workspace = true, features = [
|
|||||||
] }
|
] }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
lazy_static = { workspace = true }
|
lazy_static = { workspace = true }
|
||||||
|
regex = { workspace = true }
|
||||||
|
|
||||||
rtsa_dto = { path = "../rtsa_dto" }
|
rtsa_dto = { path = "../rtsa_dto" }
|
||||||
rtsa_log = { path = "../rtsa_log" }
|
rtsa_log = { path = "../rtsa_log" }
|
||||||
|
@ -107,12 +107,12 @@ impl Display for SortOrder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct PageResult<T> {
|
pub struct PageData<T> {
|
||||||
pub total: i64,
|
pub total: i64,
|
||||||
pub data: Vec<T>,
|
pub data: Vec<T>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> PageResult<T> {
|
impl<T> PageData<T> {
|
||||||
pub fn new(total: i64, data: Vec<T>) -> Self {
|
pub fn new(total: i64, data: Vec<T>) -> Self {
|
||||||
Self { total, data }
|
Self { total, data }
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
use std::vec;
|
use std::vec;
|
||||||
|
|
||||||
use rtsa_dto::common::DataType;
|
|
||||||
use rtsa_log::tracing::debug;
|
use rtsa_log::tracing::debug;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
common::{PageQuery, PageResult, Sort, SortOrder, TableColumn},
|
common::{PageData, PageQuery, Sort, SortOrder, TableColumn},
|
||||||
model::{DraftDataColumn, DraftDataModel},
|
model::{DraftDataColumn, DraftDataModel},
|
||||||
DbAccessError,
|
DbAccessError,
|
||||||
};
|
};
|
||||||
@ -20,14 +19,14 @@ pub trait DraftDataAccessor {
|
|||||||
&self,
|
&self,
|
||||||
query: DraftDataQuery,
|
query: DraftDataQuery,
|
||||||
page: PageQuery,
|
page: PageQuery,
|
||||||
) -> Result<PageResult<DraftDataModel>, DbAccessError>;
|
) -> Result<PageData<DraftDataModel>, DbAccessError>;
|
||||||
/// 根据id查询草稿数据
|
/// 根据id查询草稿数据
|
||||||
async fn query_draft_data_by_id(&self, id: i32) -> Result<DraftDataModel, DbAccessError>;
|
async fn query_draft_data_by_id(&self, id: i32) -> Result<DraftDataModel, DbAccessError>;
|
||||||
/// 是否user_id+data_type+name的数据已存在
|
/// 是否user_id+data_type+name的数据已存在
|
||||||
async fn is_draft_data_exist(
|
async fn is_draft_data_exist(
|
||||||
&self,
|
&self,
|
||||||
user_id: i32,
|
user_id: i32,
|
||||||
data_type: &DataType,
|
data_type: i32,
|
||||||
name: &str,
|
name: &str,
|
||||||
) -> Result<bool, DbAccessError>;
|
) -> Result<bool, DbAccessError>;
|
||||||
/// 创建草稿数据基本信息
|
/// 创建草稿数据基本信息
|
||||||
@ -74,7 +73,7 @@ pub trait DraftDataAccessor {
|
|||||||
pub struct DraftDataQuery {
|
pub struct DraftDataQuery {
|
||||||
pub user_id: Option<i32>,
|
pub user_id: Option<i32>,
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub data_type: Option<DataType>,
|
pub data_type: Option<i32>,
|
||||||
pub options: Option<Value>,
|
pub options: Option<Value>,
|
||||||
pub is_shared: Option<bool>,
|
pub is_shared: Option<bool>,
|
||||||
}
|
}
|
||||||
@ -94,7 +93,7 @@ impl DraftDataQuery {
|
|||||||
self
|
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.data_type = Some(data_type);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@ -130,7 +129,7 @@ impl DraftDataQuery {
|
|||||||
filters.push(format!(
|
filters.push(format!(
|
||||||
"{} = {}",
|
"{} = {}",
|
||||||
DraftDataColumn::DataType.name(),
|
DraftDataColumn::DataType.name(),
|
||||||
data_type as i32
|
data_type
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if let Some(is_shared) = self.is_shared {
|
if let Some(is_shared) = self.is_shared {
|
||||||
@ -157,7 +156,7 @@ impl DraftDataQuery {
|
|||||||
|
|
||||||
pub struct CreateDraftData {
|
pub struct CreateDraftData {
|
||||||
name: String,
|
name: String,
|
||||||
data_type: DataType,
|
data_type: i32,
|
||||||
options: Option<Value>,
|
options: Option<Value>,
|
||||||
data: Option<Vec<u8>>,
|
data: Option<Vec<u8>>,
|
||||||
default_release_data_id: Option<i32>,
|
default_release_data_id: Option<i32>,
|
||||||
@ -165,7 +164,7 @@ pub struct CreateDraftData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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 {
|
CreateDraftData {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
data_type,
|
data_type,
|
||||||
@ -210,7 +209,7 @@ impl DraftDataAccessor for RtsaDbAccessor {
|
|||||||
&self,
|
&self,
|
||||||
query: DraftDataQuery,
|
query: DraftDataQuery,
|
||||||
page: PageQuery,
|
page: PageQuery,
|
||||||
) -> Result<PageResult<DraftDataModel>, DbAccessError> {
|
) -> Result<PageData<DraftDataModel>, DbAccessError> {
|
||||||
let table = DraftDataColumn::Table.name();
|
let table = DraftDataColumn::Table.name();
|
||||||
let where_clause = query.build_filter();
|
let where_clause = query.build_filter();
|
||||||
let sql = format!("SELECT COUNT(*) FROM {table} {where_clause}");
|
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?;
|
let total: i64 = sqlx::query_scalar(&sql).fetch_one(&self.pool).await?;
|
||||||
|
|
||||||
if total == 0 {
|
if total == 0 {
|
||||||
return Ok(PageResult::new(total, vec![]));
|
return Ok(PageData::new(total, vec![]));
|
||||||
}
|
}
|
||||||
|
|
||||||
let select_columns = format!(
|
let select_columns = format!(
|
||||||
@ -245,7 +244,7 @@ impl DraftDataAccessor for RtsaDbAccessor {
|
|||||||
debug!("paging sql: {}", paging_sql);
|
debug!("paging sql: {}", paging_sql);
|
||||||
let list: Vec<DraftDataModel> = sqlx::query_as(&paging_sql).fetch_all(&self.pool).await?;
|
let list: Vec<DraftDataModel> = 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<DraftDataModel, DbAccessError> {
|
async fn query_draft_data_by_id(&self, id: i32) -> Result<DraftDataModel, DbAccessError> {
|
||||||
@ -259,7 +258,7 @@ impl DraftDataAccessor for RtsaDbAccessor {
|
|||||||
async fn is_draft_data_exist(
|
async fn is_draft_data_exist(
|
||||||
&self,
|
&self,
|
||||||
user_id: i32,
|
user_id: i32,
|
||||||
data_type: &DataType,
|
data_type: i32,
|
||||||
name: &str,
|
name: &str,
|
||||||
) -> Result<bool, DbAccessError> {
|
) -> Result<bool, DbAccessError> {
|
||||||
let table = DraftDataColumn::Table.name();
|
let table = DraftDataColumn::Table.name();
|
||||||
@ -285,10 +284,12 @@ impl DraftDataAccessor for RtsaDbAccessor {
|
|||||||
) -> Result<DraftDataModel, DbAccessError> {
|
) -> Result<DraftDataModel, DbAccessError> {
|
||||||
// 检查是否已存在
|
// 检查是否已存在
|
||||||
let exist = self
|
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?;
|
.await?;
|
||||||
if exist {
|
if exist {
|
||||||
return Err(DbAccessError::RowExist);
|
return Err(DbAccessError::RowExist(
|
||||||
|
"uid + data_type + name".to_string(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
// 插入数据
|
// 插入数据
|
||||||
let table = DraftDataColumn::Table.name();
|
let table = DraftDataColumn::Table.name();
|
||||||
@ -433,14 +434,57 @@ impl DraftDataAccessor for RtsaDbAccessor {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::{SyncUserInfo, UserAccessor};
|
use crate::{RegisterUser, UserAccessor};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use rtsa_dto::common::{IscsStyle, Role};
|
|
||||||
use rtsa_log::tracing::Level;
|
use rtsa_log::tracing::Level;
|
||||||
use serde::{Deserialize, Serialize};
|
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)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct IscsDataOptions {
|
pub struct IscsDataOptions {
|
||||||
pub style: IscsStyle,
|
pub style: IscsStyle,
|
||||||
@ -451,26 +495,15 @@ mod tests {
|
|||||||
async fn basic_use_test(pool: PgPool) -> Result<(), DbAccessError> {
|
async fn basic_use_test(pool: PgPool) -> Result<(), DbAccessError> {
|
||||||
rtsa_log::Logging::default().with_level(Level::DEBUG).init();
|
rtsa_log::Logging::default().with_level(Level::DEBUG).init();
|
||||||
let accessor = crate::db_access::RtsaDbAccessor::new(pool);
|
let accessor = crate::db_access::RtsaDbAccessor::new(pool);
|
||||||
// 同步10个用户
|
// 注册10个用户
|
||||||
let mut users = vec![];
|
|
||||||
for i in 0..10 {
|
for i in 0..10 {
|
||||||
let user = SyncUserInfo {
|
let user = RegisterUser::new(&format!("test{}", i), "123456");
|
||||||
id: i + 1,
|
accessor.register_user(user).await?;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
accessor.sync_user(users.as_slice()).await?;
|
|
||||||
|
|
||||||
// 创建草稿数据测试
|
// 创建草稿数据测试
|
||||||
let res = accessor
|
let res = accessor
|
||||||
.create_draft_data(CreateDraftData::new("test", DataType::Em, 10))
|
.create_draft_data(CreateDraftData::new("test", DataType::Em as i32, 10))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
println!("res: {:?}", res);
|
println!("res: {:?}", res);
|
||||||
@ -479,11 +512,11 @@ mod tests {
|
|||||||
|
|
||||||
// 重复创建测试
|
// 重复创建测试
|
||||||
let repeat_create_result = accessor
|
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;
|
.await;
|
||||||
if let Some(e) = repeat_create_result.err() {
|
if let Some(e) = repeat_create_result.err() {
|
||||||
match e {
|
match e {
|
||||||
DbAccessError::RowExist => {
|
DbAccessError::RowExist(_) => {
|
||||||
println!("repeat create test pass");
|
println!("repeat create test pass");
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
@ -551,10 +584,10 @@ mod tests {
|
|||||||
if i == 1 {
|
if i == 1 {
|
||||||
let draft = accessor
|
let draft = accessor
|
||||||
.create_draft_data(
|
.create_draft_data(
|
||||||
CreateDraftData::new(&format!("test{}", j), DataType::Iscs, i)
|
CreateDraftData::new(&format!("test{}", j), DataType::Iscs as i32, i)
|
||||||
.with_options(
|
.with_options(
|
||||||
serde_json::to_value(IscsDataOptions {
|
serde_json::to_value(IscsDataOptions {
|
||||||
style: IscsStyle::DaShiZhiNeng,
|
style: IscsStyle::Style1,
|
||||||
})
|
})
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
),
|
),
|
||||||
@ -566,7 +599,7 @@ mod tests {
|
|||||||
accessor
|
accessor
|
||||||
.create_draft_data(CreateDraftData::new(
|
.create_draft_data(CreateDraftData::new(
|
||||||
&format!("test{}", j),
|
&format!("test{}", j),
|
||||||
DataType::Em,
|
DataType::Em as i32,
|
||||||
i,
|
i,
|
||||||
))
|
))
|
||||||
.await?;
|
.await?;
|
||||||
@ -579,7 +612,7 @@ mod tests {
|
|||||||
DraftDataQuery::new()
|
DraftDataQuery::new()
|
||||||
.with_user_id(2)
|
.with_user_id(2)
|
||||||
.with_name("test".to_string())
|
.with_name("test".to_string())
|
||||||
.with_data_type(DataType::Em),
|
.with_data_type(DataType::Em as i32),
|
||||||
PageQuery::new(1, 10),
|
PageQuery::new(1, 10),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@ -604,7 +637,7 @@ mod tests {
|
|||||||
.paging_query_draft_data(
|
.paging_query_draft_data(
|
||||||
DraftDataQuery::new().with_options(
|
DraftDataQuery::new().with_options(
|
||||||
serde_json::to_value(IscsDataOptions {
|
serde_json::to_value(IscsDataOptions {
|
||||||
style: IscsStyle::DaShiZhiNeng,
|
style: IscsStyle::Style1,
|
||||||
})
|
})
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
),
|
),
|
||||||
|
@ -2,7 +2,7 @@ use rtsa_dto::common::FeatureType;
|
|||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
common::{PageQuery, PageResult, TableColumn},
|
common::{PageData, PageQuery, TableColumn},
|
||||||
model::{FeatureColumn, FeatureModel},
|
model::{FeatureColumn, FeatureModel},
|
||||||
DbAccessError,
|
DbAccessError,
|
||||||
};
|
};
|
||||||
@ -27,7 +27,7 @@ pub trait FeatureAccessor {
|
|||||||
&self,
|
&self,
|
||||||
page: PageQuery,
|
page: PageQuery,
|
||||||
filter: &FeaturePagingFilter,
|
filter: &FeaturePagingFilter,
|
||||||
) -> Result<PageResult<FeatureModel>, DbAccessError>;
|
) -> Result<PageData<FeatureModel>, DbAccessError>;
|
||||||
/// 获取id对应的功能特性
|
/// 获取id对应的功能特性
|
||||||
async fn get_feature(&self, id: i32) -> Result<FeatureModel, DbAccessError>;
|
async fn get_feature(&self, id: i32) -> Result<FeatureModel, DbAccessError>;
|
||||||
/// 根据id列表获取功能特性基本信息
|
/// 根据id列表获取功能特性基本信息
|
||||||
@ -102,7 +102,7 @@ impl FeatureAccessor for RtsaDbAccessor {
|
|||||||
&self,
|
&self,
|
||||||
page: PageQuery,
|
page: PageQuery,
|
||||||
filter: &FeaturePagingFilter,
|
filter: &FeaturePagingFilter,
|
||||||
) -> Result<PageResult<FeatureModel>, DbAccessError> {
|
) -> Result<PageData<FeatureModel>, DbAccessError> {
|
||||||
let table = FeatureColumn::Table.name();
|
let table = FeatureColumn::Table.name();
|
||||||
let where_clause = filter.to_where_clause();
|
let where_clause = filter.to_where_clause();
|
||||||
let total =
|
let total =
|
||||||
@ -110,7 +110,7 @@ impl FeatureAccessor for RtsaDbAccessor {
|
|||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await?;
|
.await?;
|
||||||
if total == 0 {
|
if total == 0 {
|
||||||
return Ok(PageResult::new(total, vec![]));
|
return Ok(PageData::new(total, vec![]));
|
||||||
}
|
}
|
||||||
let update_at_column = FeatureColumn::UpdatedAt.name();
|
let update_at_column = FeatureColumn::UpdatedAt.name();
|
||||||
let select_columns = [
|
let select_columns = [
|
||||||
@ -132,7 +132,7 @@ impl FeatureAccessor for RtsaDbAccessor {
|
|||||||
let rows = sqlx::query_as::<_, FeatureModel>(&sql)
|
let rows = sqlx::query_as::<_, FeatureModel>(&sql)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(PageResult::new(total, rows))
|
Ok(PageData::new(total, rows))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_feature(&self, id: i32) -> Result<FeatureModel, DbAccessError> {
|
async fn get_feature(&self, id: i32) -> Result<FeatureModel, DbAccessError> {
|
||||||
@ -217,11 +217,11 @@ impl FeaturePagingFilter {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
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::*;
|
use super::*;
|
||||||
|
|
||||||
@ -229,22 +229,11 @@ mod tests {
|
|||||||
async fn test_basic_use(pool: PgPool) -> Result<(), DbAccessError> {
|
async fn test_basic_use(pool: PgPool) -> Result<(), DbAccessError> {
|
||||||
rtsa_log::Logging::default().with_level(Level::DEBUG).init();
|
rtsa_log::Logging::default().with_level(Level::DEBUG).init();
|
||||||
let accessor = crate::db_access::RtsaDbAccessor::new(pool);
|
let accessor = crate::db_access::RtsaDbAccessor::new(pool);
|
||||||
// 同步10个用户
|
// 注册10个用户
|
||||||
let mut users = vec![];
|
|
||||||
for i in 0..10 {
|
for i in 0..10 {
|
||||||
let user = SyncUserInfo {
|
let user = RegisterUser::new(&format!("test{}", i), "123456");
|
||||||
id: i + 1,
|
accessor.register_user(user).await?;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
accessor.sync_user(users.as_slice()).await?;
|
|
||||||
|
|
||||||
// 创建测试
|
// 创建测试
|
||||||
let creator_id = 1;
|
let creator_id = 1;
|
||||||
|
@ -10,6 +10,10 @@ pub use user::*;
|
|||||||
mod feature;
|
mod feature;
|
||||||
pub use feature::*;
|
pub use feature::*;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
|
mod org;
|
||||||
|
pub use org::*;
|
||||||
|
mod org_user;
|
||||||
|
pub use org_user::*;
|
||||||
|
|
||||||
use crate::{model::MqttClientIdSeq, DbAccessError};
|
use crate::{model::MqttClientIdSeq, DbAccessError};
|
||||||
|
|
||||||
@ -28,6 +32,16 @@ pub async fn init_default_db_accessor(url: &str) {
|
|||||||
*rda = Some(accessor);
|
*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 {
|
pub fn get_default_db_accessor() -> RtsaDbAccessor {
|
||||||
let rda = RDA.lock().unwrap();
|
let rda = RDA.lock().unwrap();
|
||||||
|
669
crates/rtsa_db/src/db_access/org.rs
Normal file
669
crates/rtsa_db/src/db_access/org.rs
Normal file
@ -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<bool, DbAccessError>;
|
||||||
|
/// 创建组织
|
||||||
|
async fn create_org(&self, create: CreateOrg) -> Result<OrganizationModel, DbAccessError>;
|
||||||
|
/// 获取组织
|
||||||
|
async fn query_org(&self, id: i32) -> Result<OrganizationModel, DbAccessError>;
|
||||||
|
/// 根据id列表获取组织name
|
||||||
|
async fn query_org_names(&self, ids: &[i32]) -> Result<Vec<(i32, String)>, DbAccessError>;
|
||||||
|
/// 通过code获取组织
|
||||||
|
async fn query_org_by_code(&self, code: &str) -> Result<OrganizationModel, DbAccessError>;
|
||||||
|
/// 分页获取顶级组织
|
||||||
|
async fn query_org_top_page(
|
||||||
|
&self,
|
||||||
|
page: PageQuery,
|
||||||
|
filter: OrgTopFilter,
|
||||||
|
) -> Result<PageData<OrganizationModel>, DbAccessError>;
|
||||||
|
/// 获取所有顶级组织
|
||||||
|
async fn query_org_top(&self) -> Result<Vec<OrganizationModel>, DbAccessError>;
|
||||||
|
/// 获取某组织的顶级组织
|
||||||
|
async fn query_org_top_of_org(&self, org_id: i32) -> Result<OrganizationModel, DbAccessError>;
|
||||||
|
/// 获取所有下一级子组织
|
||||||
|
async fn query_org_children(
|
||||||
|
&self,
|
||||||
|
parent_id: i32,
|
||||||
|
) -> Result<Vec<OrganizationModel>, DbAccessError>;
|
||||||
|
/// 通过id列表获取组织code和name
|
||||||
|
async fn query_org_name_by_ids(
|
||||||
|
&self,
|
||||||
|
ids: Vec<i32>,
|
||||||
|
) -> Result<Vec<(i32, String)>, DbAccessError>;
|
||||||
|
/// 更新组织名称
|
||||||
|
async fn update_org_name(
|
||||||
|
&self,
|
||||||
|
id: i32,
|
||||||
|
name: &str,
|
||||||
|
updater_id: i32,
|
||||||
|
) -> Result<OrganizationModel, DbAccessError>;
|
||||||
|
/// 更新组织配置
|
||||||
|
async fn update_org_config(
|
||||||
|
&self,
|
||||||
|
id: i32,
|
||||||
|
config: Value,
|
||||||
|
updater_id: i32,
|
||||||
|
) -> Result<OrganizationModel, DbAccessError>;
|
||||||
|
/// 删除组织
|
||||||
|
/// 警告:删除组织会关联删除组织关联的所有数据
|
||||||
|
async fn delete_org(&self, id: i32) -> Result<(), DbAccessError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CreateOrg {
|
||||||
|
pub parent_id: Option<i32>,
|
||||||
|
pub code: Option<String>,
|
||||||
|
pub name: String,
|
||||||
|
pub config: Option<Value>,
|
||||||
|
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<String>,
|
||||||
|
pub name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<bool, DbAccessError> {
|
||||||
|
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<OrganizationModel, DbAccessError> {
|
||||||
|
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<OrganizationModel, DbAccessError> {
|
||||||
|
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<Vec<(i32, String)>, 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<OrganizationModel, DbAccessError> {
|
||||||
|
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<PageData<OrganizationModel>, 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<Vec<OrganizationModel>, 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<OrganizationModel, DbAccessError> {
|
||||||
|
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<Vec<OrganizationModel>, 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<i32>,
|
||||||
|
) -> Result<Vec<(i32, String)>, 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<OrganizationModel, DbAccessError> {
|
||||||
|
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<OrganizationModel, DbAccessError> {
|
||||||
|
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<UserModel, DbAccessError> {
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
770
crates/rtsa_db/src/db_access/org_user.rs
Normal file
770
crates/rtsa_db/src/db_access/org_user.rs
Normal file
@ -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<OrganizationUserModel, DbAccessError>;
|
||||||
|
/// 解绑(删除)组织用户
|
||||||
|
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<OrganizationUserModel, DbAccessError>;
|
||||||
|
/// 更新组织用户角色
|
||||||
|
async fn update_org_user_roles(
|
||||||
|
&self,
|
||||||
|
id: i32,
|
||||||
|
roles: Value,
|
||||||
|
updater_id: i32,
|
||||||
|
) -> Result<OrganizationUserModel, DbAccessError>;
|
||||||
|
/// 更新组织用户信息
|
||||||
|
async fn update_org_user_info(
|
||||||
|
&self,
|
||||||
|
id: i32,
|
||||||
|
info: Value,
|
||||||
|
updater_id: i32,
|
||||||
|
) -> Result<OrganizationUserModel, DbAccessError>;
|
||||||
|
/// 查询组织用户登陆
|
||||||
|
/// 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<OrganizationUserModel, DbAccessError>;
|
||||||
|
/// 组织用户是否存在
|
||||||
|
async fn is_org_user_exist(&self, org_id: i32, user_id: i32) -> Result<bool, DbAccessError>;
|
||||||
|
/// 获取组织用户
|
||||||
|
async fn query_org_user_by_student_id(
|
||||||
|
&self,
|
||||||
|
org_id: i32,
|
||||||
|
student_id: &str,
|
||||||
|
) -> Result<OrganizationUserModel, DbAccessError>;
|
||||||
|
/// 获取组织用户
|
||||||
|
async fn query_org_user_by_id(&self, id: i32) -> Result<OrganizationUserModel, DbAccessError>;
|
||||||
|
/// 分页查询组织用户
|
||||||
|
async fn query_org_user_page(
|
||||||
|
&self,
|
||||||
|
page: PageQuery,
|
||||||
|
filter: OrgUserFilter,
|
||||||
|
) -> Result<PageData<OrganizationUserModel>, DbAccessError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct OrgUserFilter {
|
||||||
|
pub org_id: Option<i32>,
|
||||||
|
pub student_id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<String>,
|
||||||
|
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<String>,
|
||||||
|
pub mobile: Option<String>,
|
||||||
|
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<OrganizationUserModel, DbAccessError>
|
||||||
|
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<bool, DbAccessError> {
|
||||||
|
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<OrganizationUserModel, DbAccessError> {
|
||||||
|
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<OrganizationUserModel, DbAccessError> {
|
||||||
|
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<OrganizationUserModel, DbAccessError> {
|
||||||
|
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<OrganizationUserModel, DbAccessError> {
|
||||||
|
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<OrganizationUserModel, DbAccessError> {
|
||||||
|
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<OrganizationUserModel, DbAccessError> {
|
||||||
|
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<OrganizationUserModel, DbAccessError> {
|
||||||
|
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<PageData<OrganizationUserModel>, 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(())
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,8 @@
|
|||||||
use rtsa_dto::common::DataType;
|
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use sqlx::{types::chrono, Postgres};
|
use sqlx::{types::chrono, Postgres};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
common::{PageQuery, PageResult, Sort, SortOrder, TableColumn},
|
common::{PageData, PageQuery, Sort, SortOrder, TableColumn},
|
||||||
model::{
|
model::{
|
||||||
DraftDataModel, ReleaseDataColumn, ReleaseDataModel, ReleaseDataVersionColumn,
|
DraftDataModel, ReleaseDataColumn, ReleaseDataModel, ReleaseDataVersionColumn,
|
||||||
ReleaseDataVersionModel,
|
ReleaseDataVersionModel,
|
||||||
@ -35,11 +34,11 @@ pub trait ReleaseDataAccessor {
|
|||||||
&self,
|
&self,
|
||||||
query: ReleaseDataQuery,
|
query: ReleaseDataQuery,
|
||||||
page: PageQuery,
|
page: PageQuery,
|
||||||
) -> Result<PageResult<ReleaseDataModel>, DbAccessError>;
|
) -> Result<PageData<ReleaseDataModel>, DbAccessError>;
|
||||||
/// 检查name是否存在
|
/// 检查name是否存在
|
||||||
async fn is_release_data_name_exist(
|
async fn is_release_data_name_exist(
|
||||||
&self,
|
&self,
|
||||||
data_type: DataType,
|
data_type: i32,
|
||||||
name: &str,
|
name: &str,
|
||||||
) -> Result<bool, DbAccessError>;
|
) -> Result<bool, DbAccessError>;
|
||||||
/// 查询发布数据
|
/// 查询发布数据
|
||||||
@ -57,7 +56,7 @@ pub trait ReleaseDataAccessor {
|
|||||||
&self,
|
&self,
|
||||||
release_id: i32,
|
release_id: i32,
|
||||||
page: PageQuery,
|
page: PageQuery,
|
||||||
) -> Result<PageResult<ReleaseDataVersionModel>, DbAccessError>;
|
) -> Result<PageData<ReleaseDataVersionModel>, DbAccessError>;
|
||||||
/// 根据id查询发布版本数据
|
/// 根据id查询发布版本数据
|
||||||
async fn query_release_data_version_by_id(
|
async fn query_release_data_version_by_id(
|
||||||
&self,
|
&self,
|
||||||
@ -109,7 +108,7 @@ pub struct ReleaseFromDraftResult {
|
|||||||
pub struct ReleaseDataQuery {
|
pub struct ReleaseDataQuery {
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub user_id: Option<i32>,
|
pub user_id: Option<i32>,
|
||||||
pub data_type: Option<rtsa_dto::common::DataType>,
|
pub data_type: Option<i32>,
|
||||||
pub options: Option<Value>,
|
pub options: Option<Value>,
|
||||||
pub is_published: Option<bool>,
|
pub is_published: Option<bool>,
|
||||||
}
|
}
|
||||||
@ -141,7 +140,7 @@ impl ReleaseDataQuery {
|
|||||||
self
|
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.data_type = Some(data_type);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@ -174,7 +173,7 @@ impl ReleaseDataQuery {
|
|||||||
filters.push(format!("{rd_user_id} = {user_id}"));
|
filters.push(format!("{rd_user_id} = {user_id}"));
|
||||||
}
|
}
|
||||||
if let Some(data_type) = self.data_type {
|
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 {
|
if let Some(options) = &self.options {
|
||||||
let options_column = ReleaseDataColumn::Options.name();
|
let options_column = ReleaseDataColumn::Options.name();
|
||||||
@ -368,7 +367,7 @@ impl ReleaseDataAccessor for RtsaDbAccessor {
|
|||||||
&self,
|
&self,
|
||||||
query: ReleaseDataQuery,
|
query: ReleaseDataQuery,
|
||||||
page: PageQuery,
|
page: PageQuery,
|
||||||
) -> Result<PageResult<ReleaseDataModel>, DbAccessError> {
|
) -> Result<PageData<ReleaseDataModel>, DbAccessError> {
|
||||||
let rd_table = ReleaseDataColumn::Table.name();
|
let rd_table = ReleaseDataColumn::Table.name();
|
||||||
let where_clause = query.build_filter();
|
let where_clause = query.build_filter();
|
||||||
let count_clause = format!("SELECT COUNT(*) FROM {rd_table} {where_clause}");
|
let count_clause = format!("SELECT COUNT(*) FROM {rd_table} {where_clause}");
|
||||||
@ -378,7 +377,7 @@ impl ReleaseDataAccessor for RtsaDbAccessor {
|
|||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await?;
|
.await?;
|
||||||
if total == 0 {
|
if total == 0 {
|
||||||
return Ok(PageResult::new(0, vec![]));
|
return Ok(PageData::new(0, vec![]));
|
||||||
}
|
}
|
||||||
let paging_clause = page.to_limit_clause();
|
let paging_clause = page.to_limit_clause();
|
||||||
let order_by = Sort::new(ReleaseDataColumn::UpdatedAt, SortOrder::Desc);
|
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)
|
let data = sqlx::query_as::<_, ReleaseDataModel>(&query_clause)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(PageResult::new(total, data))
|
Ok(PageData::new(total, data))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn is_release_data_name_exist(
|
async fn is_release_data_name_exist(
|
||||||
&self,
|
&self,
|
||||||
data_type: DataType,
|
data_type: i32,
|
||||||
name: &str,
|
name: &str,
|
||||||
) -> Result<bool, DbAccessError> {
|
) -> Result<bool, DbAccessError> {
|
||||||
let rd_table = ReleaseDataColumn::Table.name();
|
let rd_table = ReleaseDataColumn::Table.name();
|
||||||
@ -406,7 +405,7 @@ impl ReleaseDataAccessor for RtsaDbAccessor {
|
|||||||
);
|
);
|
||||||
let count: i64 = sqlx::query_scalar(&rd_query_clause)
|
let count: i64 = sqlx::query_scalar(&rd_query_clause)
|
||||||
.bind(name)
|
.bind(name)
|
||||||
.bind(data_type as i32)
|
.bind(data_type)
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(count > 0)
|
Ok(count > 0)
|
||||||
@ -453,7 +452,7 @@ impl ReleaseDataAccessor for RtsaDbAccessor {
|
|||||||
&self,
|
&self,
|
||||||
release_id: i32,
|
release_id: i32,
|
||||||
page: PageQuery,
|
page: PageQuery,
|
||||||
) -> Result<PageResult<ReleaseDataVersionModel>, DbAccessError> {
|
) -> Result<PageData<ReleaseDataVersionModel>, DbAccessError> {
|
||||||
// 查询发布数据版本
|
// 查询发布数据版本
|
||||||
let rdv_table = ReleaseDataVersionColumn::Table.name();
|
let rdv_table = ReleaseDataVersionColumn::Table.name();
|
||||||
let rdv_release_id = ReleaseDataVersionColumn::ReleaseDataId.name();
|
let rdv_release_id = ReleaseDataVersionColumn::ReleaseDataId.name();
|
||||||
@ -466,7 +465,7 @@ impl ReleaseDataAccessor for RtsaDbAccessor {
|
|||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await?;
|
.await?;
|
||||||
if total == 0 {
|
if total == 0 {
|
||||||
return Ok(PageResult::new(0, vec![]));
|
return Ok(PageData::new(0, vec![]));
|
||||||
}
|
}
|
||||||
// 查询列,除了data列
|
// 查询列,除了data列
|
||||||
let rdv_columns = format!(
|
let rdv_columns = format!(
|
||||||
@ -490,7 +489,7 @@ impl ReleaseDataAccessor for RtsaDbAccessor {
|
|||||||
.bind(release_id)
|
.bind(release_id)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(PageResult::new(total, data))
|
Ok(PageData::new(total, data))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn query_release_data_version_by_id(
|
async fn query_release_data_version_by_id(
|
||||||
@ -645,15 +644,63 @@ impl ReleaseDataAccessor for RtsaDbAccessor {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::{CreateDraftData, DraftDataAccessor, RtsaDbAccessor, SyncUserInfo, UserAccessor};
|
use crate::{CreateDraftData, DraftDataAccessor, RegisterUser, RtsaDbAccessor, UserAccessor};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use chrono::Local;
|
|
||||||
use rtsa_dto::common::{IscsStyle, Role};
|
|
||||||
use rtsa_log::tracing::Level;
|
use rtsa_log::tracing::Level;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::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,
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_release_query() {
|
fn test_release_query() {
|
||||||
// 测试构造发布数据查询条件,名称过滤
|
// 测试构造发布数据查询条件,名称过滤
|
||||||
@ -668,7 +715,7 @@ mod tests {
|
|||||||
|
|
||||||
// 测试构造发布数据查询条件,数据类型过滤
|
// 测试构造发布数据查询条件,数据类型过滤
|
||||||
let expects = "WHERE data_type = 1";
|
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);
|
assert_eq!(query_with_data_type.build_filter(), expects);
|
||||||
|
|
||||||
// 测试构造发布数据查询条件,是否上架过滤
|
// 测试构造发布数据查询条件,是否上架过滤
|
||||||
@ -682,45 +729,29 @@ mod tests {
|
|||||||
let query_with_all = ReleaseDataQuery::new()
|
let query_with_all = ReleaseDataQuery::new()
|
||||||
.with_name("test".to_string())
|
.with_name("test".to_string())
|
||||||
.with_user_id(1)
|
.with_user_id(1)
|
||||||
.with_data_type(DataType::Em)
|
.with_data_type(DataType::Em as i32)
|
||||||
.with_is_published(true);
|
.with_is_published(true);
|
||||||
assert_eq!(query_with_all.build_filter(), expects);
|
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.
|
// You could also do `use foo_crate::MIGRATOR` and just refer to it as `MIGRATOR` here.
|
||||||
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
#[sqlx::test(migrator = "crate::MIGRATOR")]
|
||||||
async fn test_basic_use(pool: PgPool) -> Result<(), DbAccessError> {
|
async fn test_basic_use(pool: PgPool) -> Result<(), DbAccessError> {
|
||||||
rtsa_log::Logging::default().with_level(Level::DEBUG).init();
|
rtsa_log::Logging::default().with_level(Level::DEBUG).init();
|
||||||
let accessor = RtsaDbAccessor::new(pool);
|
let accessor = RtsaDbAccessor::new(pool);
|
||||||
// 同步10个用户
|
// 注册10个用户
|
||||||
let mut users = vec![];
|
|
||||||
for i in 0..10 {
|
for i in 0..10 {
|
||||||
let user = SyncUserInfo {
|
let user = RegisterUser::new(&format!("test{}", i), "123456");
|
||||||
id: i + 1,
|
accessor.register_user(user).await?;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
accessor.sync_user(users.as_slice()).await?;
|
|
||||||
// 创建草稿
|
// 创建草稿
|
||||||
let data = "test".as_bytes();
|
let data = "test".as_bytes();
|
||||||
let draft = accessor
|
let draft = accessor
|
||||||
.create_draft_data(
|
.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(
|
.with_options(
|
||||||
serde_json::to_value(IscsDataOptions {
|
serde_json::to_value(IscsDataOptions {
|
||||||
style: IscsStyle::DaShiZhiNeng,
|
style: IscsStyle::Style1,
|
||||||
})
|
})
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
@ -753,7 +784,7 @@ mod tests {
|
|||||||
// 检查options数据
|
// 检查options数据
|
||||||
let options: IscsDataOptions =
|
let options: IscsDataOptions =
|
||||||
serde_json::from_value(release_data.options.unwrap()).unwrap();
|
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);
|
assert_eq!(version1.data, data);
|
||||||
// 检查版本描述
|
// 检查版本描述
|
||||||
@ -767,7 +798,7 @@ mod tests {
|
|||||||
|
|
||||||
// name重复检查
|
// name重复检查
|
||||||
let exist = accessor
|
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?;
|
.await?;
|
||||||
assert!(exist);
|
assert!(exist);
|
||||||
|
|
||||||
@ -849,7 +880,7 @@ mod tests {
|
|||||||
let data = "test data".as_bytes();
|
let data = "test data".as_bytes();
|
||||||
let draft = accessor
|
let draft = accessor
|
||||||
.create_draft_data(
|
.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?;
|
.await?;
|
||||||
let (release_data, _) = accessor
|
let (release_data, _) = accessor
|
||||||
@ -883,7 +914,7 @@ mod tests {
|
|||||||
let page_result = accessor.paging_query_release_data_list(query, page).await?;
|
let page_result = accessor.paging_query_release_data_list(query, page).await?;
|
||||||
assert_eq!(page_result.total, 2);
|
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 = PageQuery::new(1, 10);
|
||||||
let page_result = accessor.paging_query_release_data_list(query, page).await?;
|
let page_result = accessor.paging_query_release_data_list(query, page).await?;
|
||||||
assert_eq!(page_result.total, 8);
|
assert_eq!(page_result.total, 8);
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -3,11 +3,15 @@ use thiserror::Error;
|
|||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum DbAccessError {
|
pub enum DbAccessError {
|
||||||
#[error("未知的数据库访问错误")]
|
#[error("未知的数据库访问错误")]
|
||||||
Unknown,
|
Unknown(#[from] anyhow::Error),
|
||||||
#[error("数据访问错误: {0}")]
|
#[error("数据访问错误: {0}")]
|
||||||
SqlxError(#[from] sqlx::Error),
|
SqlxError(#[from] sqlx::Error),
|
||||||
#[error("数据已存在")]
|
#[error("数据已存在: {0}")]
|
||||||
RowExist,
|
RowExist(String),
|
||||||
|
#[error("数据不存在: {0}")]
|
||||||
|
RowNotExist(String),
|
||||||
#[error("数据错误:{0}")]
|
#[error("数据错误:{0}")]
|
||||||
DataError(String),
|
DataError(String),
|
||||||
|
#[error("非法参数:{0}")]
|
||||||
|
InvalidArgument(String),
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,8 @@ mod error;
|
|||||||
pub mod model;
|
pub mod model;
|
||||||
pub use db_access::*;
|
pub use db_access::*;
|
||||||
pub use error::*;
|
pub use error::*;
|
||||||
|
pub mod password_util;
|
||||||
|
pub mod username_util;
|
||||||
|
|
||||||
use sqlx::pool::PoolOptions;
|
use sqlx::pool::PoolOptions;
|
||||||
pub use sqlx::types::chrono::*;
|
pub use sqlx::types::chrono::*;
|
||||||
@ -23,5 +25,6 @@ pub mod prelude {
|
|||||||
pub use crate::common::*;
|
pub use crate::common::*;
|
||||||
pub use crate::db_access::*;
|
pub use crate::db_access::*;
|
||||||
pub use crate::model::*;
|
pub use crate::model::*;
|
||||||
|
pub use crate::password_util::*;
|
||||||
pub use crate::DbAccessError;
|
pub use crate::DbAccessError;
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,5 @@
|
|||||||
use rtsa_dto::common::{DataType, FeatureType, Role};
|
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use sqlx::types::{
|
use sqlx::types::chrono::{DateTime, Local};
|
||||||
chrono::{DateTime, Local},
|
|
||||||
Json,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::common::TableColumn;
|
use crate::common::TableColumn;
|
||||||
|
|
||||||
@ -25,9 +21,11 @@ pub enum UserColumn {
|
|||||||
Id,
|
Id,
|
||||||
Username,
|
Username,
|
||||||
Password,
|
Password,
|
||||||
|
Nickname,
|
||||||
Email,
|
Email,
|
||||||
Mobile,
|
Mobile,
|
||||||
Roles,
|
Roles,
|
||||||
|
Info,
|
||||||
CreatedAt,
|
CreatedAt,
|
||||||
UpdatedAt,
|
UpdatedAt,
|
||||||
}
|
}
|
||||||
@ -37,15 +35,182 @@ pub struct UserModel {
|
|||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
|
pub nickname: String,
|
||||||
#[sqlx(default)]
|
#[sqlx(default)]
|
||||||
pub email: Option<String>,
|
pub email: Option<String>,
|
||||||
#[sqlx(default)]
|
#[sqlx(default)]
|
||||||
pub mobile: Option<String>,
|
pub mobile: Option<String>,
|
||||||
pub roles: Json<Vec<Role>>,
|
pub roles: Value,
|
||||||
|
pub info: Value,
|
||||||
pub created_at: DateTime<Local>,
|
pub created_at: DateTime<Local>,
|
||||||
pub updated_at: DateTime<Local>,
|
pub updated_at: DateTime<Local>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 数据库表 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<Local>,
|
||||||
|
pub updated_at: DateTime<Local>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 数据库表 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<String>,
|
||||||
|
pub name: String,
|
||||||
|
#[sqlx(default)]
|
||||||
|
pub config: Option<Value>,
|
||||||
|
pub parent_id: Option<i32>,
|
||||||
|
pub creator_id: i32,
|
||||||
|
pub created_at: DateTime<Local>,
|
||||||
|
pub updater_id: i32,
|
||||||
|
pub updated_at: DateTime<Local>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 数据库表 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<String>,
|
||||||
|
pub roles: Value,
|
||||||
|
pub info: Option<Value>,
|
||||||
|
pub creator_id: i32,
|
||||||
|
pub created_at: DateTime<Local>,
|
||||||
|
pub updater_id: i32,
|
||||||
|
pub updated_at: DateTime<Local>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 数据库表 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<Local>,
|
||||||
|
pub updater_id: i32,
|
||||||
|
pub updated_at: DateTime<Local>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 数据库表 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<Local>,
|
||||||
|
pub updated_at: DateTime<Local>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 数据库表 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<Local>,
|
||||||
|
pub updater_id: i32,
|
||||||
|
pub updated_at: DateTime<Local>,
|
||||||
|
}
|
||||||
|
|
||||||
/// 数据库表 rtsa.draft_data 列映射
|
/// 数据库表 rtsa.draft_data 列映射
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum DraftDataColumn {
|
pub enum DraftDataColumn {
|
||||||
@ -66,7 +231,7 @@ pub enum DraftDataColumn {
|
|||||||
pub struct DraftDataModel {
|
pub struct DraftDataModel {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub data_type: DataType,
|
pub data_type: i32,
|
||||||
#[sqlx(default)]
|
#[sqlx(default)]
|
||||||
pub options: Option<Value>,
|
pub options: Option<Value>,
|
||||||
#[sqlx(default)]
|
#[sqlx(default)]
|
||||||
@ -100,7 +265,7 @@ pub enum ReleaseDataColumn {
|
|||||||
pub struct ReleaseDataModel {
|
pub struct ReleaseDataModel {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub data_type: DataType,
|
pub data_type: i32,
|
||||||
/// 从发布版本复制的选项,主要用于查询
|
/// 从发布版本复制的选项,主要用于查询
|
||||||
#[sqlx(default)]
|
#[sqlx(default)]
|
||||||
pub options: Option<Value>,
|
pub options: Option<Value>,
|
||||||
@ -137,59 +302,75 @@ pub struct ReleaseDataVersionModel {
|
|||||||
pub created_at: DateTime<Local>,
|
pub created_at: DateTime<Local>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 数据库表 rtsa.feature 列映射
|
/// 数据库表 rtsa.release_data_set 列映射
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
#[allow(dead_code)]
|
pub enum ReleaseDataSetColumn {
|
||||||
pub enum FeatureColumn {
|
|
||||||
Table,
|
Table,
|
||||||
Id,
|
Id,
|
||||||
FeatureType,
|
DataSetType,
|
||||||
Name,
|
Name,
|
||||||
Description,
|
Description,
|
||||||
Config,
|
Config,
|
||||||
IsPublished,
|
IsPublished,
|
||||||
CreatorId,
|
CreatorId,
|
||||||
UpdaterId,
|
|
||||||
CreatedAt,
|
CreatedAt,
|
||||||
|
UpdaterId,
|
||||||
UpdatedAt,
|
UpdatedAt,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, sqlx::FromRow)]
|
#[derive(Debug, sqlx::FromRow)]
|
||||||
pub struct FeatureModel {
|
pub struct ReleaseDataSetModel {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub feature_type: FeatureType,
|
pub data_set_type: i32,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
#[sqlx(default)]
|
#[sqlx(default)]
|
||||||
pub config: Value,
|
pub config: Value,
|
||||||
pub is_published: bool,
|
pub is_published: bool,
|
||||||
pub creator_id: i32,
|
pub creator_id: i32,
|
||||||
pub updater_id: i32,
|
|
||||||
pub created_at: DateTime<Local>,
|
pub created_at: DateTime<Local>,
|
||||||
|
pub updater_id: i32,
|
||||||
pub updated_at: DateTime<Local>,
|
pub updated_at: DateTime<Local>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 数据库表 rtsa.user_config 列映射
|
/// 数据库表 rtsa.release_data_set_data 列映射
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
#[allow(dead_code)]
|
pub enum ReleaseDataSetDataColumn {
|
||||||
pub enum UserConfigColumn {
|
|
||||||
Table,
|
Table,
|
||||||
Id,
|
Id,
|
||||||
UserId,
|
ReleaseDataSetId,
|
||||||
ConfigType,
|
ReleaseDataId,
|
||||||
Config,
|
OrderIndex,
|
||||||
CreatedAt,
|
|
||||||
UpdatedAt,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, sqlx::FromRow)]
|
#[derive(Debug, sqlx::FromRow)]
|
||||||
pub struct UserConfigModel {
|
pub struct ReleaseDataSetDataModel {
|
||||||
pub id: i32,
|
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 user_id: i32,
|
||||||
pub config_type: i32,
|
pub log_type: i32,
|
||||||
pub config: Value,
|
pub log_data: Value,
|
||||||
pub created_at: DateTime<Local>,
|
pub created_at: DateTime<Local>,
|
||||||
pub updated_at: DateTime<Local>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TableColumn for UserColumn {
|
impl TableColumn for UserColumn {
|
||||||
@ -199,15 +380,116 @@ impl TableColumn for UserColumn {
|
|||||||
UserColumn::Id => "id",
|
UserColumn::Id => "id",
|
||||||
UserColumn::Username => "username",
|
UserColumn::Username => "username",
|
||||||
UserColumn::Password => "password",
|
UserColumn::Password => "password",
|
||||||
|
UserColumn::Nickname => "nickname",
|
||||||
UserColumn::Email => "email",
|
UserColumn::Email => "email",
|
||||||
UserColumn::Mobile => "mobile",
|
UserColumn::Mobile => "mobile",
|
||||||
UserColumn::Roles => "roles",
|
UserColumn::Roles => "roles",
|
||||||
|
UserColumn::Info => "info",
|
||||||
UserColumn::CreatedAt => "created_at",
|
UserColumn::CreatedAt => "created_at",
|
||||||
UserColumn::UpdatedAt => "updated_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 {
|
impl TableColumn for DraftDataColumn {
|
||||||
fn name(&self) -> &str {
|
fn name(&self) -> &str {
|
||||||
match self {
|
match self {
|
||||||
@ -258,34 +540,32 @@ impl TableColumn for ReleaseDataVersionColumn {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TableColumn for FeatureColumn {
|
impl TableColumn for ReleaseDataSetColumn {
|
||||||
fn name(&self) -> &str {
|
fn name(&self) -> &str {
|
||||||
match self {
|
match self {
|
||||||
FeatureColumn::Table => "rtsa.feature",
|
ReleaseDataSetColumn::Table => "rtsa.release_data_set",
|
||||||
FeatureColumn::Id => "id",
|
ReleaseDataSetColumn::Id => "id",
|
||||||
FeatureColumn::FeatureType => "feature_type",
|
ReleaseDataSetColumn::DataSetType => "data_set_type",
|
||||||
FeatureColumn::Name => "name",
|
ReleaseDataSetColumn::Name => "name",
|
||||||
FeatureColumn::Description => "description",
|
ReleaseDataSetColumn::Description => "description",
|
||||||
FeatureColumn::Config => "config",
|
ReleaseDataSetColumn::Config => "config",
|
||||||
FeatureColumn::IsPublished => "is_published",
|
ReleaseDataSetColumn::IsPublished => "is_published",
|
||||||
FeatureColumn::CreatorId => "creator_id",
|
ReleaseDataSetColumn::CreatorId => "creator_id",
|
||||||
FeatureColumn::UpdaterId => "updater_id",
|
ReleaseDataSetColumn::CreatedAt => "created_at",
|
||||||
FeatureColumn::CreatedAt => "created_at",
|
ReleaseDataSetColumn::UpdaterId => "updater_id",
|
||||||
FeatureColumn::UpdatedAt => "updated_at",
|
ReleaseDataSetColumn::UpdatedAt => "updated_at",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TableColumn for UserConfigColumn {
|
impl TableColumn for ReleaseDataSetDataColumn {
|
||||||
fn name(&self) -> &str {
|
fn name(&self) -> &str {
|
||||||
match self {
|
match self {
|
||||||
UserConfigColumn::Table => "rtsa.user_config",
|
ReleaseDataSetDataColumn::Table => "rtsa.release_data_set_data",
|
||||||
UserConfigColumn::Id => "id",
|
ReleaseDataSetDataColumn::Id => "id",
|
||||||
UserConfigColumn::UserId => "user_id",
|
ReleaseDataSetDataColumn::ReleaseDataSetId => "release_data_set_id",
|
||||||
UserConfigColumn::ConfigType => "config_type",
|
ReleaseDataSetDataColumn::ReleaseDataId => "release_data_id",
|
||||||
UserConfigColumn::Config => "config",
|
ReleaseDataSetDataColumn::OrderIndex => "order_index",
|
||||||
UserConfigColumn::CreatedAt => "created_at",
|
|
||||||
UserConfigColumn::UpdatedAt => "updated_at",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
4
crates/rtsa_db/src/password_util.rs
Normal file
4
crates/rtsa_db/src/password_util.rs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/// 验证密码是否正确
|
||||||
|
pub fn verify_password(password: &str, hash: &str) -> bool {
|
||||||
|
password == hash
|
||||||
|
}
|
53
crates/rtsa_db/src/username_util.rs
Normal file
53
crates/rtsa_db/src/username_util.rs
Normal file
@ -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"));
|
||||||
|
}
|
||||||
|
}
|
@ -112,6 +112,15 @@ pub enum Role {
|
|||||||
Admin = 1,
|
Admin = 1,
|
||||||
/// 普通用户
|
/// 普通用户
|
||||||
User = 2,
|
User = 2,
|
||||||
|
/// -----组织用户角色-----
|
||||||
|
/// 组织管理员
|
||||||
|
OrgManager = 11,
|
||||||
|
/// 组织教师
|
||||||
|
OrgTeacher = 12,
|
||||||
|
/// 组织学生
|
||||||
|
OrgStudent = 13,
|
||||||
|
/// 组织访客
|
||||||
|
OrgGuest = 14,
|
||||||
}
|
}
|
||||||
impl Role {
|
impl Role {
|
||||||
/// String value of the enum field names used in the ProtoBuf definition.
|
/// String value of the enum field names used in the ProtoBuf definition.
|
||||||
@ -123,6 +132,10 @@ impl Role {
|
|||||||
Role::Unknown => "Role_Unknown",
|
Role::Unknown => "Role_Unknown",
|
||||||
Role::Admin => "Role_Admin",
|
Role::Admin => "Role_Admin",
|
||||||
Role::User => "Role_User",
|
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.
|
/// Creates an enum from field names used in the ProtoBuf definition.
|
||||||
@ -131,6 +144,10 @@ impl Role {
|
|||||||
"Role_Unknown" => Some(Self::Unknown),
|
"Role_Unknown" => Some(Self::Unknown),
|
||||||
"Role_Admin" => Some(Self::Admin),
|
"Role_Admin" => Some(Self::Admin),
|
||||||
"Role_User" => Some(Self::User),
|
"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,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,12 +4,36 @@ version = "0.1.0"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
|
|
||||||
rtsa_log = { path = "../crates/rtsa_log" }
|
rtsa_log = { path = "../crates/rtsa_log" }
|
||||||
rtsa_api = { path = "crates/rtsa_api" }
|
|
||||||
rtsa_db = { path = "../crates/rtsa_db" }
|
rtsa_db = { path = "../crates/rtsa_db" }
|
||||||
|
rtsa_dto = { path = "../crates/rtsa_dto" }
|
||||||
|
|
||||||
|
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
config = { workspace = true }
|
config = { workspace = true }
|
||||||
clap = { workspace = true, features = ["derive"] }
|
clap = { workspace = true, features = ["derive"] }
|
||||||
enum_dispatch = { workspace = true }
|
enum_dispatch = { workspace = true }
|
||||||
anyhow = { 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",
|
||||||
|
] }
|
||||||
|
@ -5,10 +5,3 @@ port = 8765
|
|||||||
|
|
||||||
[log]
|
[log]
|
||||||
level = "debug"
|
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"
|
|
||||||
|
@ -1,5 +1,2 @@
|
|||||||
[database]
|
[database]
|
||||||
url = "postgresql://joylink:Joylink@0503@localhost:5432/joylink"
|
url = "postgresql://joylink:Joylink@0503@localhost:5432/joylink"
|
||||||
|
|
||||||
[sso]
|
|
||||||
base_url = "http://192.168.33.233/rtss-server"
|
|
||||||
|
@ -3,6 +3,3 @@ url = "postgresql://joylink:Joylink@0503@10.11.11.2:5432/joylink"
|
|||||||
|
|
||||||
[log]
|
[log]
|
||||||
level = "debug"
|
level = "debug"
|
||||||
|
|
||||||
[sso]
|
|
||||||
base_url = "http://192.168.33.233/rtss-server"
|
|
||||||
|
@ -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" }
|
|
@ -1,7 +0,0 @@
|
|||||||
// mod jwt_auth;
|
|
||||||
mod apis;
|
|
||||||
mod loader;
|
|
||||||
mod server;
|
|
||||||
mod user_auth;
|
|
||||||
|
|
||||||
pub use server::*;
|
|
@ -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::<Token>() {
|
|
||||||
// 从ctx中获取UserAuthCache, 从cache中获取用户信息
|
|
||||||
let user_auth_cache = ctx.data::<UserAuthCache>().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<Mutex<HashMap<String, UserInfoDto>>>,
|
|
||||||
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<Token> {
|
|
||||||
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<String> {
|
|
||||||
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::<CommonResponseDto>().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<UserInfoDto> {
|
|
||||||
let url = format!("{}{}?token={token}", self.base_url, self.user_info_url);
|
|
||||||
let response = reqwest::get(url).await?;
|
|
||||||
let common = response.json::<CommonResponseDto>().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<Vec<UserInfoDto>> {
|
|
||||||
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::<CommonResponseDto>().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<serde_json::Value>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct UserInfoDto {
|
|
||||||
pub id: String,
|
|
||||||
pub name: Option<String>,
|
|
||||||
pub nickname: Option<String>,
|
|
||||||
pub roles: Vec<String>,
|
|
||||||
pub mobile: Option<String>,
|
|
||||||
pub email: Option<String>,
|
|
||||||
#[serde(rename = "createTime")]
|
|
||||||
pub create_time: String,
|
|
||||||
#[serde(rename = "updateTime")]
|
|
||||||
pub update_time: Option<String>,
|
|
||||||
}
|
|
||||||
impl UserInfoDto {
|
|
||||||
pub fn id_i32(&self) -> i32 {
|
|
||||||
self.id
|
|
||||||
.parse::<i32>()
|
|
||||||
.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<Role> {
|
|
||||||
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<Local> {
|
|
||||||
parse_to_date_time(&self.create_time)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn updated_at(&self) -> DateTime<Local> {
|
|
||||||
parse_to_date_time(self.update_time.as_deref().unwrap_or(&self.create_time))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_to_date_time(s: &str) -> chrono::DateTime<Local> {
|
|
||||||
chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S")
|
|
||||||
.expect("parse date_time failed")
|
|
||||||
.and_local_timezone(Local)
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<UserInfoDto> 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<String, UserInfoDto> {
|
|
||||||
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<UserInfoDto> {
|
|
||||||
let cache = self.cache.lock().unwrap();
|
|
||||||
cache.get(key).cloned()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn query_user(&self, token: &str) -> anyhow::Result<UserInfoDto> {
|
|
||||||
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(())
|
|
||||||
// }
|
|
||||||
}
|
|
@ -15,7 +15,7 @@ use super::data_options_def::{DataOptions, IscsDataOptions};
|
|||||||
use super::release_data::ReleaseDataId;
|
use super::release_data::ReleaseDataId;
|
||||||
use super::user::UserId;
|
use super::user::UserId;
|
||||||
|
|
||||||
use crate::user_auth::{RoleGuard, Token, UserAuthCache};
|
use crate::user_auth::{Jwt, RoleGuard};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct DraftDataQuery;
|
pub struct DraftDataQuery;
|
||||||
@ -47,11 +47,8 @@ impl DraftDataQuery {
|
|||||||
paging: PageQueryDto,
|
paging: PageQueryDto,
|
||||||
mut query: UserDraftDataFilterDto<IscsDataOptions>,
|
mut query: UserDraftDataFilterDto<IscsDataOptions>,
|
||||||
) -> async_graphql::Result<PageDto<DraftIscsDataDto>> {
|
) -> async_graphql::Result<PageDto<DraftIscsDataDto>> {
|
||||||
let user = ctx
|
let claims = ctx.data::<Jwt>()?.decode()?;
|
||||||
.data::<UserAuthCache>()?
|
query.user_id = claims.uid;
|
||||||
.query_user(&ctx.data::<Token>()?.0)
|
|
||||||
.await?;
|
|
||||||
query.user_id = user.id_i32();
|
|
||||||
query.data_type = Some(DataType::Iscs);
|
query.data_type = Some(DataType::Iscs);
|
||||||
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
let paging_result = db_accessor
|
let paging_result = db_accessor
|
||||||
@ -89,14 +86,11 @@ impl DraftDataQuery {
|
|||||||
data_type: DataType,
|
data_type: DataType,
|
||||||
name: String,
|
name: String,
|
||||||
) -> async_graphql::Result<bool> {
|
) -> async_graphql::Result<bool> {
|
||||||
let user = ctx
|
let claims = ctx.data::<Jwt>()?.decode()?;
|
||||||
.data::<UserAuthCache>()?
|
let user_id = claims.uid;
|
||||||
.query_user(&ctx.data::<Token>()?.0)
|
|
||||||
.await?;
|
|
||||||
let user_id = user.id_i32();
|
|
||||||
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
let exist = db_accessor
|
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?;
|
.await?;
|
||||||
Ok(exist)
|
Ok(exist)
|
||||||
}
|
}
|
||||||
@ -111,11 +105,8 @@ impl DraftDataMutation {
|
|||||||
ctx: &Context<'_>,
|
ctx: &Context<'_>,
|
||||||
mut input: CreateDraftDataDto<IscsDataOptions>,
|
mut input: CreateDraftDataDto<IscsDataOptions>,
|
||||||
) -> async_graphql::Result<DraftIscsDataDto> {
|
) -> async_graphql::Result<DraftIscsDataDto> {
|
||||||
let user = ctx
|
let claims = ctx.data::<Jwt>()?.decode()?;
|
||||||
.data::<UserAuthCache>()?
|
input = input.with_data_type_and_user_id(DataType::Iscs, claims.uid);
|
||||||
.query_user(&ctx.data::<Token>()?.0)
|
|
||||||
.await?;
|
|
||||||
input = input.with_data_type_and_user_id(DataType::Iscs, user.id_i32());
|
|
||||||
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
let draft_data = db_accessor.create_draft_data(input.into()).await?;
|
let draft_data = db_accessor.create_draft_data(input.into()).await?;
|
||||||
Ok(draft_data.into())
|
Ok(draft_data.into())
|
||||||
@ -193,11 +184,8 @@ impl DraftDataMutation {
|
|||||||
id: i32,
|
id: i32,
|
||||||
name: String,
|
name: String,
|
||||||
) -> async_graphql::Result<DraftDataDto> {
|
) -> async_graphql::Result<DraftDataDto> {
|
||||||
let user = ctx
|
let claims = ctx.data::<Jwt>()?.decode()?;
|
||||||
.data::<UserAuthCache>()?
|
let user_id = claims.uid;
|
||||||
.query_user(&ctx.data::<Token>()?.0)
|
|
||||||
.await?;
|
|
||||||
let user_id = user.id_i32();
|
|
||||||
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
let draft_data = db_accessor.save_as_new_draft(id, &name, user_id).await?;
|
let draft_data = db_accessor.save_as_new_draft(id, &name, user_id).await?;
|
||||||
Ok(draft_data.into())
|
Ok(draft_data.into())
|
||||||
@ -227,7 +215,7 @@ impl<T: DataOptions> From<CreateDraftDataDto<T>> for rtsa_db::CreateDraftData {
|
|||||||
fn from(value: CreateDraftDataDto<T>) -> Self {
|
fn from(value: CreateDraftDataDto<T>) -> Self {
|
||||||
let cdd = Self::new(
|
let cdd = Self::new(
|
||||||
&value.name,
|
&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"),
|
value.user_id.expect("CreateDraftDataDto need user_id"),
|
||||||
);
|
);
|
||||||
if value.options.is_some() {
|
if value.options.is_some() {
|
||||||
@ -256,7 +244,7 @@ impl<T: DataOptions> From<UserDraftDataFilterDto<T>> for rtsa_db::DraftDataQuery
|
|||||||
Self {
|
Self {
|
||||||
user_id: Some(value.user_id),
|
user_id: Some(value.user_id),
|
||||||
name: value.name,
|
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()),
|
options: value.options.map(|o| serde_json::to_value(o).unwrap()),
|
||||||
is_shared: value.is_shared,
|
is_shared: value.is_shared,
|
||||||
}
|
}
|
||||||
@ -282,7 +270,7 @@ impl<T: DataOptions> From<DraftDataFilterDto<T>> for rtsa_db::DraftDataQuery {
|
|||||||
Self {
|
Self {
|
||||||
user_id: value.user_id,
|
user_id: value.user_id,
|
||||||
name: value.name,
|
name: value.name,
|
||||||
data_type: value.data_type,
|
data_type: value.data_type.map(|dt| dt as i32),
|
||||||
is_shared: Some(true),
|
is_shared: Some(true),
|
||||||
options: value.options.map(|o| serde_json::to_value(o).unwrap()),
|
options: value.options.map(|o| serde_json::to_value(o).unwrap()),
|
||||||
}
|
}
|
||||||
@ -334,7 +322,7 @@ impl From<rtsa_db::model::DraftDataModel> for DraftDataDto {
|
|||||||
Self {
|
Self {
|
||||||
id: value.id,
|
id: value.id,
|
||||||
name: value.name,
|
name: value.name,
|
||||||
data_type: value.data_type,
|
data_type: DataType::try_from(value.data_type).unwrap(),
|
||||||
options: value.options,
|
options: value.options,
|
||||||
data: value
|
data: value
|
||||||
.data
|
.data
|
@ -1,7 +1,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
apis::{PageDto, PageQueryDto},
|
apis::{PageDto, PageQueryDto},
|
||||||
loader::RtsaDbLoader,
|
loader::RtsaDbLoader,
|
||||||
user_auth::{RoleGuard, Token, UserAuthCache},
|
user_auth::{Jwt, RoleGuard},
|
||||||
};
|
};
|
||||||
use async_graphql::{
|
use async_graphql::{
|
||||||
dataloader::DataLoader, ComplexObject, Context, InputObject, Object, SimpleObject,
|
dataloader::DataLoader, ComplexObject, Context, InputObject, Object, SimpleObject,
|
||||||
@ -84,11 +84,8 @@ impl FeatureMutation {
|
|||||||
mut input: CreateFeatureDto<UrFeatureConfig>,
|
mut input: CreateFeatureDto<UrFeatureConfig>,
|
||||||
) -> async_graphql::Result<FeatureDto> {
|
) -> async_graphql::Result<FeatureDto> {
|
||||||
let dba = ctx.data::<RtsaDbAccessor>()?;
|
let dba = ctx.data::<RtsaDbAccessor>()?;
|
||||||
let user = ctx
|
let claims = ctx.data::<Jwt>()?.decode()?;
|
||||||
.data::<UserAuthCache>()?
|
input = input.with_feature_type_and_user_id(FeatureType::Ur, claims.uid);
|
||||||
.query_user(&ctx.data::<Token>()?.0)
|
|
||||||
.await?;
|
|
||||||
input = input.with_feature_type_and_user_id(FeatureType::Ur, user.id_i32());
|
|
||||||
let feature = dba.create_feature(&input.into()).await?;
|
let feature = dba.create_feature(&input.into()).await?;
|
||||||
Ok(feature.into())
|
Ok(feature.into())
|
||||||
}
|
}
|
||||||
@ -214,7 +211,7 @@ impl From<rtsa_db::model::FeatureModel> for FeatureDto {
|
|||||||
fn from(value: rtsa_db::model::FeatureModel) -> Self {
|
fn from(value: rtsa_db::model::FeatureModel) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: value.id,
|
id: value.id,
|
||||||
feature_type: value.feature_type,
|
feature_type: FeatureType::try_from(value.feature_type).unwrap(),
|
||||||
name: value.name,
|
name: value.name,
|
||||||
description: value.description,
|
description: value.description,
|
||||||
config: value.config,
|
config: value.config,
|
@ -1,6 +1,7 @@
|
|||||||
use async_graphql::{Enum, InputObject, MergedObject, OutputType, SimpleObject};
|
use async_graphql::{Enum, InputObject, MergedObject, OutputType, SimpleObject};
|
||||||
use draft_data::{DraftDataMutation, DraftDataQuery};
|
use draft_data::{DraftDataMutation, DraftDataQuery};
|
||||||
use feature::{FeatureMutation, FeatureQuery};
|
use feature::{FeatureMutation, FeatureQuery};
|
||||||
|
use org::{OrgMutation, OrgQuery};
|
||||||
use release_data::{ReleaseDataMutation, ReleaseDataQuery};
|
use release_data::{ReleaseDataMutation, ReleaseDataQuery};
|
||||||
|
|
||||||
mod sys_info;
|
mod sys_info;
|
||||||
@ -10,15 +11,29 @@ mod data_options_def;
|
|||||||
mod draft_data;
|
mod draft_data;
|
||||||
mod feature;
|
mod feature;
|
||||||
mod feature_config_def;
|
mod feature_config_def;
|
||||||
|
mod org;
|
||||||
|
mod org_user;
|
||||||
mod release_data;
|
mod release_data;
|
||||||
mod user;
|
mod user;
|
||||||
|
use org_user::{OrgUserMutation, OrgUserQuery};
|
||||||
|
|
||||||
|
pub use user::UserLoginDto;
|
||||||
|
|
||||||
#[derive(Default, MergedObject)]
|
#[derive(Default, MergedObject)]
|
||||||
pub struct Query(UserQuery, DraftDataQuery, ReleaseDataQuery, FeatureQuery);
|
pub struct Query(
|
||||||
|
UserQuery,
|
||||||
|
OrgQuery,
|
||||||
|
OrgUserQuery,
|
||||||
|
DraftDataQuery,
|
||||||
|
ReleaseDataQuery,
|
||||||
|
FeatureQuery,
|
||||||
|
);
|
||||||
|
|
||||||
#[derive(Default, MergedObject)]
|
#[derive(Default, MergedObject)]
|
||||||
pub struct Mutation(
|
pub struct Mutation(
|
||||||
UserMutation,
|
UserMutation,
|
||||||
|
OrgMutation,
|
||||||
|
OrgUserMutation,
|
||||||
DraftDataMutation,
|
DraftDataMutation,
|
||||||
ReleaseDataMutation,
|
ReleaseDataMutation,
|
||||||
FeatureMutation,
|
FeatureMutation,
|
||||||
@ -74,8 +89,8 @@ impl<T: OutputType> PageDto<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: OutputType, M: Into<T>> From<rtsa_db::common::PageResult<M>> for PageDto<T> {
|
impl<T: OutputType, M: Into<T>> From<rtsa_db::common::PageData<M>> for PageDto<T> {
|
||||||
fn from(value: rtsa_db::common::PageResult<M>) -> Self {
|
fn from(value: rtsa_db::common::PageData<M>) -> Self {
|
||||||
Self::new(
|
Self::new(
|
||||||
value.total,
|
value.total,
|
||||||
value.data.into_iter().map(|m| m.into()).collect(),
|
value.data.into_iter().map(|m| m.into()).collect(),
|
119
manager/src/apis/org.rs
Normal file
119
manager/src/apis/org.rs
Normal file
@ -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<OrgDto> {
|
||||||
|
let dba = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let org = dba.query_org(id).await?;
|
||||||
|
Ok(org.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取默认组织信息
|
||||||
|
async fn get_default_org(&self, ctx: &Context<'_>) -> async_graphql::Result<OrgDto> {
|
||||||
|
let dba = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
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<OrgDto> {
|
||||||
|
let claims = ctx.data::<Jwt>()?.decode()?;
|
||||||
|
let dba = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
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<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CreateTopOrgDto> 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<String>,
|
||||||
|
pub name: String,
|
||||||
|
pub creator_id: i32,
|
||||||
|
pub created_at: NaiveDateTime,
|
||||||
|
pub updater_id: i32,
|
||||||
|
pub updated_at: NaiveDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<OrganizationModel> 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<OrgId> for RtsaDbLoader {
|
||||||
|
type Value = String;
|
||||||
|
type Error = Arc<DbAccessError>;
|
||||||
|
|
||||||
|
async fn load(&self, keys: &[OrgId]) -> Result<HashMap<OrgId, Self::Value>, Self::Error> {
|
||||||
|
let ids: Vec<i32> = keys.iter().map(|k| k.id).collect();
|
||||||
|
let rows = self.db_accessor.query_org_names(ids.as_slice()).await?;
|
||||||
|
let map: HashMap<OrgId, String> = rows
|
||||||
|
.into_iter()
|
||||||
|
.map(|row| (OrgId::new(row.0), row.1))
|
||||||
|
.collect();
|
||||||
|
Ok(map)
|
||||||
|
}
|
||||||
|
}
|
145
manager/src/apis/org_user.rs
Normal file
145
manager/src/apis/org_user.rs
Normal file
@ -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<OrgUserDto> {
|
||||||
|
let org_user = ctx
|
||||||
|
.data::<RtsaDbAccessor>()?
|
||||||
|
.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<OrgUserDto> {
|
||||||
|
let claims = ctx.data::<Jwt>()?.decode()?;
|
||||||
|
bind.creator_id = Some(claims.uid);
|
||||||
|
let org_user = ctx
|
||||||
|
.data::<RtsaDbAccessor>()?
|
||||||
|
.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<bool> {
|
||||||
|
ctx.data::<RtsaDbAccessor>()?.delete_org_user(id).await?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 绑定组织用户输入对象
|
||||||
|
#[derive(InputObject)]
|
||||||
|
pub struct BindOrgUserDto {
|
||||||
|
pub org_id: i32,
|
||||||
|
pub user_id: i32,
|
||||||
|
pub student_id: Option<String>,
|
||||||
|
pub roles: Vec<Role>,
|
||||||
|
#[graphql(skip)]
|
||||||
|
pub creator_id: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<BindOrgUserDto> 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<String>,
|
||||||
|
pub roles: Vec<Role>,
|
||||||
|
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<Option<String>> {
|
||||||
|
let loader = ctx.data_unchecked::<DataLoader<RtsaDbLoader>>();
|
||||||
|
let name = loader.load_one(OrgId::new(self.org_id)).await?;
|
||||||
|
Ok(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取用户name
|
||||||
|
async fn user_name(&self, ctx: &Context<'_>) -> async_graphql::Result<Option<String>> {
|
||||||
|
let loader = ctx.data_unchecked::<DataLoader<RtsaDbLoader>>();
|
||||||
|
let name = loader.load_one(UserId::new(self.user_id)).await?;
|
||||||
|
Ok(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取创建者name
|
||||||
|
async fn creator_name(&self, ctx: &Context<'_>) -> async_graphql::Result<Option<String>> {
|
||||||
|
let loader = ctx.data_unchecked::<DataLoader<RtsaDbLoader>>();
|
||||||
|
let name = loader.load_one(UserId::new(self.creator_id)).await?;
|
||||||
|
Ok(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取更新者name
|
||||||
|
async fn updater_name(&self, ctx: &Context<'_>) -> async_graphql::Result<Option<String>> {
|
||||||
|
let loader = ctx.data_unchecked::<DataLoader<RtsaDbLoader>>();
|
||||||
|
let name = loader.load_one(UserId::new(self.updater_id)).await?;
|
||||||
|
Ok(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<OrganizationUserModel> 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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -18,7 +18,7 @@ use super::data_options_def::{DataOptions, IscsDataOptions};
|
|||||||
use super::user::UserId;
|
use super::user::UserId;
|
||||||
use super::{PageDto, PageQueryDto};
|
use super::{PageDto, PageQueryDto};
|
||||||
|
|
||||||
use crate::user_auth::{RoleGuard, Token, UserAuthCache};
|
use crate::user_auth::{Jwt, RoleGuard};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct ReleaseDataQuery;
|
pub struct ReleaseDataQuery;
|
||||||
@ -81,7 +81,7 @@ impl ReleaseDataQuery {
|
|||||||
) -> async_graphql::Result<bool> {
|
) -> async_graphql::Result<bool> {
|
||||||
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
let result = db_accessor
|
let result = db_accessor
|
||||||
.is_release_data_name_exist(data_type, &name)
|
.is_release_data_name_exist(data_type as i32, &name)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
@ -127,14 +127,10 @@ impl ReleaseDataMutation {
|
|||||||
name: String,
|
name: String,
|
||||||
description: String,
|
description: String,
|
||||||
) -> async_graphql::Result<ReleaseDataWithUsedVersionDto> {
|
) -> async_graphql::Result<ReleaseDataWithUsedVersionDto> {
|
||||||
let user = ctx
|
let claims = ctx.data::<Jwt>()?.decode()?;
|
||||||
.data::<UserAuthCache>()?
|
|
||||||
.query_user(&ctx.data::<Token>()?.0)
|
|
||||||
.await?;
|
|
||||||
let user_id = user.id_i32();
|
|
||||||
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
let result = db_accessor
|
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?;
|
.await?;
|
||||||
Ok(result.into())
|
Ok(result.into())
|
||||||
}
|
}
|
||||||
@ -147,14 +143,10 @@ impl ReleaseDataMutation {
|
|||||||
draft_id: i32,
|
draft_id: i32,
|
||||||
description: String,
|
description: String,
|
||||||
) -> async_graphql::Result<ReleaseDataWithUsedVersionDto> {
|
) -> async_graphql::Result<ReleaseDataWithUsedVersionDto> {
|
||||||
let user = ctx
|
let claims = ctx.data::<Jwt>()?.decode()?;
|
||||||
.data::<UserAuthCache>()?
|
|
||||||
.query_user(&ctx.data::<Token>()?.0)
|
|
||||||
.await?;
|
|
||||||
let user_id = user.id_i32();
|
|
||||||
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
let result = db_accessor
|
let result = db_accessor
|
||||||
.release_to_existing(draft_id, &description, Some(user_id))
|
.release_to_existing(draft_id, &description, Some(claims.uid))
|
||||||
.await?;
|
.await?;
|
||||||
Ok(result.into())
|
Ok(result.into())
|
||||||
}
|
}
|
||||||
@ -209,14 +201,10 @@ impl ReleaseDataMutation {
|
|||||||
ctx: &Context<'_>,
|
ctx: &Context<'_>,
|
||||||
version_id: i32,
|
version_id: i32,
|
||||||
) -> async_graphql::Result<DraftDataDto> {
|
) -> async_graphql::Result<DraftDataDto> {
|
||||||
let user = ctx
|
let claims = ctx.data::<Jwt>()?.decode()?;
|
||||||
.data::<UserAuthCache>()?
|
|
||||||
.query_user(&ctx.data::<Token>()?.0)
|
|
||||||
.await?;
|
|
||||||
let user_id = user.id_i32();
|
|
||||||
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
let result = db_accessor
|
let result = db_accessor
|
||||||
.create_draft_from_release_version(version_id, user_id)
|
.create_draft_from_release_version(version_id, claims.uid)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(result.into())
|
Ok(result.into())
|
||||||
}
|
}
|
||||||
@ -240,7 +228,7 @@ impl<T: DataOptions> From<ReleaseTypedDataFilterDto<T>> for rtsa_db::ReleaseData
|
|||||||
Self {
|
Self {
|
||||||
name: value.name,
|
name: value.name,
|
||||||
user_id: value.user_id,
|
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()),
|
options: value.options.map(|o| serde_json::to_value(o).unwrap()),
|
||||||
is_published: value.is_published,
|
is_published: value.is_published,
|
||||||
}
|
}
|
||||||
@ -422,7 +410,7 @@ impl From<ReleaseDataModel> for ReleaseDataDto {
|
|||||||
Self {
|
Self {
|
||||||
id: model.id,
|
id: model.id,
|
||||||
name: model.name,
|
name: model.name,
|
||||||
data_type: model.data_type,
|
data_type: DataType::try_from(model.data_type).unwrap(),
|
||||||
options: model.options,
|
options: model.options,
|
||||||
used_version_id: model.used_version_id,
|
used_version_id: model.used_version_id,
|
||||||
user_id: model.user_id,
|
user_id: model.user_id,
|
@ -2,12 +2,12 @@ use std::{collections::HashMap, sync::Arc};
|
|||||||
|
|
||||||
use async_graphql::{dataloader::Loader, Context, InputObject, Object, SimpleObject};
|
use async_graphql::{dataloader::Loader, Context, InputObject, Object, SimpleObject};
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use rtsa_db::{DbAccessError, RtsaDbAccessor, UserAccessor};
|
use rtsa_db::{model::UserModel, DbAccessError, RtsaDbAccessor, UserAccessor};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
loader::RtsaDbLoader,
|
loader::RtsaDbLoader,
|
||||||
user_auth::{build_jwt, Claims, RoleGuard, Token, UserAuthCache, UserInfoDto},
|
user_auth::{handle_login, Jwt, RoleGuard},
|
||||||
UserAuthClient,
|
|
||||||
};
|
};
|
||||||
use rtsa_dto::common::Role;
|
use rtsa_dto::common::Role;
|
||||||
|
|
||||||
@ -16,29 +16,31 @@ use super::{PageDto, PageQueryDto};
|
|||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct UserQuery;
|
pub struct UserQuery;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct UserMutation;
|
||||||
|
|
||||||
#[Object]
|
#[Object]
|
||||||
impl UserQuery {
|
impl UserQuery {
|
||||||
|
/// 用户登陆
|
||||||
|
async fn user_login(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
info: UserLoginDto,
|
||||||
|
) -> async_graphql::Result<String> {
|
||||||
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let jwt = handle_login(db_accessor, info).await?;
|
||||||
|
Ok(jwt.0)
|
||||||
|
}
|
||||||
|
|
||||||
/// 获取用户信息
|
/// 获取用户信息
|
||||||
#[graphql(guard = "RoleGuard::new(Role::User)")]
|
#[graphql(guard = "RoleGuard::new(Role::User)")]
|
||||||
async fn login_user_info(&self, ctx: &Context<'_>) -> async_graphql::Result<UserDto> {
|
async fn login_user_info(&self, ctx: &Context<'_>) -> async_graphql::Result<UserDto> {
|
||||||
let user = ctx
|
let claims = ctx.data::<Jwt>()?.decode()?;
|
||||||
.data::<UserAuthCache>()?
|
let db_accessor = ctx.data::<RtsaDbAccessor>()?;
|
||||||
.query_user(&ctx.data::<Token>()?.0)
|
let user = db_accessor.query_user(claims.uid).await?;
|
||||||
.await?;
|
|
||||||
Ok(user.into())
|
Ok(user.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取jwt令牌(mqtt验证)
|
|
||||||
#[graphql(guard = "RoleGuard::new(Role::User)")]
|
|
||||||
async fn get_jwt(&self, ctx: &Context<'_>) -> async_graphql::Result<String> {
|
|
||||||
let user = ctx
|
|
||||||
.data::<UserAuthCache>()?
|
|
||||||
.query_user(&ctx.data::<Token>()?.0)
|
|
||||||
.await?;
|
|
||||||
let jwt = build_jwt(Claims::new(user.id_i32()))?;
|
|
||||||
Ok(jwt.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 分页查询用户(系统管理)
|
/// 分页查询用户(系统管理)
|
||||||
#[graphql(guard = "RoleGuard::new(Role::Admin)")]
|
#[graphql(guard = "RoleGuard::new(Role::Admin)")]
|
||||||
async fn user_paging(
|
async fn user_paging(
|
||||||
@ -53,33 +55,74 @@ impl UserQuery {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct UserMutation;
|
|
||||||
|
|
||||||
#[Object]
|
#[Object]
|
||||||
impl UserMutation {
|
impl UserMutation {
|
||||||
/// 同步用户
|
/// 用户注册
|
||||||
#[graphql(guard = "RoleGuard::new(Role::Admin)")]
|
async fn register_user(
|
||||||
async fn sync_user(&self, ctx: &Context<'_>) -> async_graphql::Result<bool> {
|
&self,
|
||||||
let http_client = ctx.data::<UserAuthClient>()?;
|
ctx: &Context<'_>,
|
||||||
let users = http_client.query_all_users(ctx.data::<Token>()?).await?;
|
register: RegisterUserDto,
|
||||||
|
) -> async_graphql::Result<UserDto> {
|
||||||
let dba = ctx.data::<RtsaDbAccessor>()?;
|
let dba = ctx.data::<RtsaDbAccessor>()?;
|
||||||
dba.sync_user(
|
let user = dba.register_user(register.into()).await?;
|
||||||
users
|
Ok(user.into())
|
||||||
.into_iter()
|
}
|
||||||
.map(|u| u.into())
|
|
||||||
.collect::<Vec<rtsa_db::SyncUserInfo>>()
|
/// 更新用户角色
|
||||||
.as_slice(),
|
#[graphql(guard = "RoleGuard::new(Role::Admin)")]
|
||||||
)
|
async fn update_user_roles(
|
||||||
|
&self,
|
||||||
|
ctx: &Context<'_>,
|
||||||
|
user_id: i32,
|
||||||
|
roles: Vec<Role>,
|
||||||
|
) -> async_graphql::Result<UserDto> {
|
||||||
|
let dba = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let user = dba
|
||||||
|
.update_user_roles(user_id, serde_json::to_value(roles).unwrap())
|
||||||
.await?;
|
.await?;
|
||||||
Ok(true)
|
Ok(user.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, InputObject)]
|
||||||
|
pub struct UserLoginDto {
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
/// 组织id,组织用户登陆需要
|
||||||
|
pub org_id: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, InputObject)]
|
||||||
|
pub struct RegisterUserDto {
|
||||||
|
/// 用户名, 必填,唯一,用于登陆
|
||||||
|
pub username: String,
|
||||||
|
/// 昵称, 必填
|
||||||
|
pub nickname: String,
|
||||||
|
pub mobile: Option<String>,
|
||||||
|
pub email: Option<String>,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RegisterUserDto> 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)]
|
#[derive(Debug, InputObject)]
|
||||||
pub struct UserQueryDto {
|
pub struct UserQueryDto {
|
||||||
pub id: Option<i32>,
|
pub id: Option<i32>,
|
||||||
pub name: Option<String>,
|
pub username: Option<String>,
|
||||||
|
pub nickname: Option<String>,
|
||||||
pub email: Option<String>,
|
pub email: Option<String>,
|
||||||
pub mobile: Option<String>,
|
pub mobile: Option<String>,
|
||||||
pub roles: Option<Vec<Role>>,
|
pub roles: Option<Vec<Role>>,
|
||||||
@ -89,7 +132,8 @@ impl From<UserQueryDto> for rtsa_db::UserPageFilter {
|
|||||||
fn from(value: UserQueryDto) -> Self {
|
fn from(value: UserQueryDto) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: value.id,
|
id: value.id,
|
||||||
name: value.name,
|
username: value.username,
|
||||||
|
nickname: value.nickname,
|
||||||
email: value.email,
|
email: value.email,
|
||||||
mobile: value.mobile,
|
mobile: value.mobile,
|
||||||
roles: value.roles.map(|r| serde_json::to_value(r).unwrap()),
|
roles: value.roles.map(|r| serde_json::to_value(r).unwrap()),
|
||||||
@ -100,7 +144,8 @@ impl From<UserQueryDto> for rtsa_db::UserPageFilter {
|
|||||||
#[derive(Debug, SimpleObject)]
|
#[derive(Debug, SimpleObject)]
|
||||||
pub struct UserDto {
|
pub struct UserDto {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub name: String,
|
pub username: String,
|
||||||
|
pub nickname: String,
|
||||||
pub mobile: Option<String>,
|
pub mobile: Option<String>,
|
||||||
pub email: Option<String>,
|
pub email: Option<String>,
|
||||||
pub roles: Vec<Role>,
|
pub roles: Vec<Role>,
|
||||||
@ -108,28 +153,15 @@ pub struct UserDto {
|
|||||||
pub updated_at: NaiveDateTime,
|
pub updated_at: NaiveDateTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<UserInfoDto> for UserDto {
|
impl From<UserModel> for UserDto {
|
||||||
fn from(value: UserInfoDto) -> Self {
|
fn from(value: UserModel) -> 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<rtsa_db::model::UserModel> for UserDto {
|
|
||||||
fn from(value: rtsa_db::model::UserModel) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
id: value.id,
|
id: value.id,
|
||||||
name: value.username,
|
username: value.username.clone(),
|
||||||
mobile: value.mobile,
|
nickname: value.nickname.clone(),
|
||||||
email: value.email,
|
mobile: value.mobile.clone(),
|
||||||
roles: value.roles.0,
|
email: value.email.clone(),
|
||||||
|
roles: serde_json::from_value(value.roles).unwrap(),
|
||||||
created_at: value.created_at.naive_local(),
|
created_at: value.created_at.naive_local(),
|
||||||
updated_at: value.updated_at.naive_local(),
|
updated_at: value.updated_at.naive_local(),
|
||||||
}
|
}
|
||||||
@ -153,7 +185,10 @@ impl Loader<UserId> for RtsaDbLoader {
|
|||||||
|
|
||||||
async fn load(&self, keys: &[UserId]) -> Result<HashMap<UserId, Self::Value>, Self::Error> {
|
async fn load(&self, keys: &[UserId]) -> Result<HashMap<UserId, Self::Value>, Self::Error> {
|
||||||
let ids: Vec<i32> = keys.iter().map(|k| k.id).collect();
|
let ids: Vec<i32> = 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<UserId, String> = rows
|
let map: HashMap<UserId, String> = rows
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|row| (UserId::new(row.0), row.1))
|
.map(|row| (UserId::new(row.0), row.1))
|
@ -29,23 +29,12 @@ impl From<Log> 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)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub struct AppConfig {
|
pub struct AppConfig {
|
||||||
pub server: Server,
|
pub server: Server,
|
||||||
pub log: Log,
|
pub log: Log,
|
||||||
pub database: Database,
|
pub database: Database,
|
||||||
pub sso: Sso,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppConfig {
|
impl AppConfig {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use enum_dispatch::enum_dispatch;
|
use enum_dispatch::enum_dispatch;
|
||||||
|
|
||||||
use crate::{app_config, CmdExecutor};
|
use crate::{app_config, server, CmdExecutor};
|
||||||
|
|
||||||
use super::DbSubCommand;
|
use super::DbSubCommand;
|
||||||
|
|
||||||
@ -33,16 +33,10 @@ impl CmdExecutor for ServerOpts {
|
|||||||
app_config::AppConfig::new(&self.config_path).expect("Failed to load app config");
|
app_config::AppConfig::new(&self.config_path).expect("Failed to load app config");
|
||||||
let log: rtsa_log::Logging = app_config.log.into();
|
let log: rtsa_log::Logging = app_config.log.into();
|
||||||
log.init();
|
log.init();
|
||||||
rtsa_api::serve(
|
server::serve(server::ServerConfig::new(
|
||||||
rtsa_api::ServerConfig::new(&app_config.database.url, app_config.server.port)
|
&app_config.database.url,
|
||||||
.with_user_auth_client(rtsa_api::UserAuthClient {
|
app_config.server.port,
|
||||||
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,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use enum_dispatch::enum_dispatch;
|
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)]
|
#[derive(Parser, Debug)]
|
||||||
#[enum_dispatch(CmdExecutor)]
|
#[enum_dispatch(CmdExecutor)]
|
||||||
@ -22,6 +22,10 @@ impl CmdExecutor for MigrateOpts {
|
|||||||
async fn execute(&self) -> anyhow::Result<()> {
|
async fn execute(&self) -> anyhow::Result<()> {
|
||||||
let app_config =
|
let app_config =
|
||||||
app_config::AppConfig::new(&self.config_path).expect("Failed to load 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
18
manager/src/error.rs
Normal file
18
manager/src/error.rs
Normal file
@ -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),
|
||||||
|
}
|
@ -1,4 +1,13 @@
|
|||||||
mod app_config;
|
mod app_config;
|
||||||
mod commands;
|
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::*;
|
pub use commands::*;
|
||||||
|
@ -19,13 +19,10 @@ use crate::apis::{Mutation, Query};
|
|||||||
use crate::loader::RtsaDbLoader;
|
use crate::loader::RtsaDbLoader;
|
||||||
use crate::user_auth;
|
use crate::user_auth;
|
||||||
|
|
||||||
pub use crate::user_auth::UserAuthClient;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ServerConfig {
|
pub struct ServerConfig {
|
||||||
pub database_url: String,
|
pub database_url: String,
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
pub user_auth_client: Option<user_auth::UserAuthClient>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServerConfig {
|
impl ServerConfig {
|
||||||
@ -33,27 +30,17 @@ impl ServerConfig {
|
|||||||
Self {
|
Self {
|
||||||
database_url: database_url.to_string(),
|
database_url: database_url.to_string(),
|
||||||
port,
|
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 {
|
pub fn to_socket_addr(&self) -> String {
|
||||||
format!("0.0.0.0:{}", self.port)
|
format!("0.0.0.0:{}", self.port)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn serve(config: ServerConfig) -> anyhow::Result<()> {
|
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 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()
|
let app = Router::new()
|
||||||
.route("/", get(graphiql).post(graphql_handler))
|
.route("/", get(graphiql).post(graphql_handler))
|
||||||
@ -95,27 +82,18 @@ async fn graphiql() -> impl IntoResponse {
|
|||||||
pub type RtsaAppSchema = Schema<Query, Mutation, EmptySubscription>;
|
pub type RtsaAppSchema = Schema<Query, Mutation, EmptySubscription>;
|
||||||
|
|
||||||
pub struct SchemaOptions {
|
pub struct SchemaOptions {
|
||||||
pub user_auth_client: UserAuthClient,
|
|
||||||
pub user_info_cache: user_auth::UserAuthCache,
|
|
||||||
pub rtsa_dba: RtsaDbAccessor,
|
pub rtsa_dba: RtsaDbAccessor,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SchemaOptions {
|
impl SchemaOptions {
|
||||||
pub fn new(user_auth_client: UserAuthClient, rtsa_dba: RtsaDbAccessor) -> Self {
|
pub fn new(rtsa_dba: RtsaDbAccessor) -> Self {
|
||||||
let user_info_cache = user_auth::UserAuthCache::new(user_auth_client.clone());
|
Self { rtsa_dba }
|
||||||
Self {
|
|
||||||
user_auth_client,
|
|
||||||
user_info_cache,
|
|
||||||
rtsa_dba,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_schema(options: SchemaOptions) -> RtsaAppSchema {
|
pub fn new_schema(options: SchemaOptions) -> RtsaAppSchema {
|
||||||
let loader = RtsaDbLoader::new(options.rtsa_dba.clone());
|
let loader = RtsaDbLoader::new(options.rtsa_dba.clone());
|
||||||
Schema::build(Query::default(), Mutation::default(), EmptySubscription)
|
Schema::build(Query::default(), Mutation::default(), EmptySubscription)
|
||||||
.data(options.user_auth_client)
|
|
||||||
.data(options.user_info_cache)
|
|
||||||
.data(options.rtsa_dba)
|
.data(options.rtsa_dba)
|
||||||
.data(DataLoader::new(loader, tokio::spawn))
|
.data(DataLoader::new(loader, tokio::spawn))
|
||||||
// .data(MutexSimulationManager::default())
|
// .data(MutexSimulationManager::default())
|
||||||
@ -131,15 +109,6 @@ mod tests {
|
|||||||
let dba =
|
let dba =
|
||||||
rtsa_db::get_db_accessor("postgresql://joylink:Joylink@0503@localhost:5432/joylink")
|
rtsa_db::get_db_accessor("postgresql://joylink:Joylink@0503@localhost:5432/joylink")
|
||||||
.await;
|
.await;
|
||||||
let _ = new_schema(SchemaOptions::new(
|
let _ = new_schema(SchemaOptions::new(dba));
|
||||||
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,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
78
manager/src/sys_init/mod.rs
Normal file
78
manager/src/sys_init/mod.rs
Normal file
@ -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(())
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,11 @@
|
|||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
use async_graphql::Result;
|
use axum::http::HeaderMap;
|
||||||
use jsonwebtoken::{decode, DecodingKey, EncodingKey, Validation};
|
use jsonwebtoken::{decode, DecodingKey, EncodingKey, Validation};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::error::BusinessError;
|
||||||
|
|
||||||
static KEYS: LazyLock<Keys> = LazyLock::new(|| {
|
static KEYS: LazyLock<Keys> = LazyLock::new(|| {
|
||||||
// let secret = std::env::var("JWT_SECRET").expect("JWT_SECRET must be set");
|
// let secret = std::env::var("JWT_SECRET").expect("JWT_SECRET must be set");
|
||||||
let secret = "joylink".to_string();
|
let secret = "joylink".to_string();
|
||||||
@ -24,12 +26,34 @@ impl Keys {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct Jwt(pub String);
|
pub struct Jwt(pub String);
|
||||||
|
|
||||||
|
impl Jwt {
|
||||||
|
pub fn build_from(claims: Claims) -> Result<Self, BusinessError> {
|
||||||
|
let token =
|
||||||
|
jsonwebtoken::encode(&jsonwebtoken::Header::default(), &claims, &KEYS.encoding)?;
|
||||||
|
Ok(Self(token))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode(&self) -> Result<Claims, BusinessError> {
|
||||||
|
let data = decode::<Claims>(&self.0, &KEYS.decoding, &Validation::default())?;
|
||||||
|
Ok(data.claims)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_token_from_headers(headers: &HeaderMap) -> Option<Jwt> {
|
||||||
|
headers
|
||||||
|
.get("Token")
|
||||||
|
.and_then(|token| token.to_str().map(|s| Jwt(s.to_string())).ok())
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct Claims {
|
pub struct Claims {
|
||||||
pub id: i32,
|
/// 用户ID
|
||||||
|
pub uid: i32,
|
||||||
|
/// 组织ID
|
||||||
|
pub oid: i32,
|
||||||
exp: usize, // 过期时间,单位秒
|
exp: usize, // 过期时间,单位秒
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,28 +65,18 @@ pub fn get_current_timestamp() -> u64 {
|
|||||||
.as_secs()
|
.as_secs()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const EXP_TIME: usize = 3600 * 24 * 5; // 5天
|
||||||
|
|
||||||
impl Claims {
|
impl Claims {
|
||||||
pub fn new(id: i32) -> Self {
|
pub fn new(uid: i32, oid: i32) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id,
|
uid,
|
||||||
exp: get_current_timestamp() as usize + 3600 * 24 * 7, // 7天
|
oid,
|
||||||
|
exp: get_current_timestamp() as usize + EXP_TIME,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 构建jwt
|
|
||||||
pub fn build_jwt(claims: Claims) -> Result<Jwt> {
|
|
||||||
let token = jsonwebtoken::encode(&jsonwebtoken::Header::default(), &claims, &KEYS.encoding)?;
|
|
||||||
Ok(Jwt(token))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 解析jwt
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn decode_jwt(token: &str) -> Result<Claims> {
|
|
||||||
let data = decode::<Claims>(token, &KEYS.decoding, &Validation::default())?;
|
|
||||||
Ok(data.claims)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
@ -71,13 +85,14 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_jwt() {
|
fn test_jwt() {
|
||||||
rtsa_log::Logging::default().init();
|
rtsa_log::Logging::default().init();
|
||||||
let claim = Claims::new(5);
|
let claim = Claims::new(5, 1);
|
||||||
let jwt = build_jwt(claim).unwrap();
|
let jwt = Jwt::build_from(claim).unwrap();
|
||||||
println!("jwt: {}", jwt.0);
|
println!("jwt: {}", jwt.0);
|
||||||
let result = decode_jwt(&jwt.0);
|
let result = jwt.decode();
|
||||||
match result {
|
match result {
|
||||||
Ok(claims) => {
|
Ok(claims) => {
|
||||||
assert_eq!(claims.id, 5);
|
assert_eq!(claims.uid, 5);
|
||||||
|
assert_eq!(claims.oid, 1);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
panic!("Error: {:?}", e);
|
panic!("Error: {:?}", e);
|
135
manager/src/user_auth/mod.rs
Normal file
135
manager/src/user_auth/mod.rs
Normal file
@ -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::<Jwt>() {
|
||||||
|
// 从ctx中获取UserAuthCache, 从cache中获取用户信息
|
||||||
|
let claims = jwt.decode()?;
|
||||||
|
let dba = ctx.data::<RtsaDbAccessor>()?;
|
||||||
|
let user = dba.query_user(claims.uid).await?;
|
||||||
|
let user_roles: Vec<Role> = 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<Role>;
|
||||||
|
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<Jwt, BusinessError> {
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
@ -8,8 +8,9 @@ CREATE SEQUENCE rtsa.mqtt_client_id_seq;
|
|||||||
CREATE TABLE
|
CREATE TABLE
|
||||||
rtsa.user (
|
rtsa.user (
|
||||||
id SERIAL PRIMARY KEY, -- id 自增主键
|
id SERIAL PRIMARY KEY, -- id 自增主键
|
||||||
username VARCHAR(128) NOT NULL, -- 用户名
|
username VARCHAR(128) NOT NULL UNIQUE, -- 用户名
|
||||||
password VARCHAR(256) NOT NULL, -- 密码
|
password VARCHAR(256) NOT NULL, -- 密码
|
||||||
|
nickname VARCHAR(128) NOT NULL DEFAULT 'user', -- 昵称
|
||||||
email VARCHAR(128) NULL UNIQUE, -- 邮箱
|
email VARCHAR(128) NULL UNIQUE, -- 邮箱
|
||||||
mobile VARCHAR(16) NULL UNIQUE, -- 手机号
|
mobile VARCHAR(16) NULL UNIQUE, -- 手机号
|
||||||
roles JSONB NOT NULL DEFAULT '[]', -- 角色列表
|
roles JSONB NOT NULL DEFAULT '[]', -- 角色列表
|
||||||
@ -21,6 +22,12 @@ CREATE TABLE
|
|||||||
-- 创建用户名称索引
|
-- 创建用户名称索引
|
||||||
CREATE INDEX ON rtsa.user (username);
|
CREATE INDEX ON rtsa.user (username);
|
||||||
|
|
||||||
|
-- 创建用户密码索引
|
||||||
|
CREATE INDEX ON rtsa.user (password);
|
||||||
|
|
||||||
|
-- 创建用户昵称索引
|
||||||
|
CREATE INDEX ON rtsa.user (nickname);
|
||||||
|
|
||||||
-- 创建用户邮箱索引
|
-- 创建用户邮箱索引
|
||||||
CREATE INDEX ON rtsa.user (email);
|
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.password IS '密码';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN rtsa.user.nickname IS '昵称';
|
||||||
|
|
||||||
COMMENT ON COLUMN rtsa.user.email IS '邮箱';
|
COMMENT ON COLUMN rtsa.user.email IS '邮箱';
|
||||||
|
|
||||||
COMMENT ON COLUMN rtsa.user.mobile IS '手机号';
|
COMMENT ON COLUMN rtsa.user.mobile IS '手机号';
|
||||||
@ -56,9 +65,9 @@ COMMENT ON COLUMN rtsa.user.updated_at IS '更新时间';
|
|||||||
CREATE TABLE
|
CREATE TABLE
|
||||||
rtsa.organization (
|
rtsa.organization (
|
||||||
id SERIAL PRIMARY KEY, -- id 自增主键
|
id SERIAL PRIMARY KEY, -- id 自增主键
|
||||||
code VARCHAR(128) NOT NULL UNIQUE, -- 组织编码
|
code VARCHAR(128) NULL UNIQUE, -- 组织编码
|
||||||
name VARCHAR(128) NOT NULL, -- 组织名称
|
name VARCHAR(128) NOT NULL, -- 组织名称
|
||||||
config JSONB NOT NULL DEFAULT '{}', -- 配置数据
|
config JSONB NULL, -- 配置数据
|
||||||
parent_id INT NULL, -- 父组织id
|
parent_id INT NULL, -- 父组织id
|
||||||
creator_id INT NOT NULL, -- 创建用户id
|
creator_id INT NOT NULL, -- 创建用户id
|
||||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间
|
||||||
@ -112,8 +121,9 @@ CREATE TABLE
|
|||||||
id SERIAL PRIMARY KEY, -- id 自增主键
|
id SERIAL PRIMARY KEY, -- id 自增主键
|
||||||
organization_id INT NOT NULL, -- 组织id
|
organization_id INT NOT NULL, -- 组织id
|
||||||
user_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 '[]', -- 组织角色列表
|
roles JSONB NOT NULL DEFAULT '[]', -- 组织角色列表
|
||||||
|
info JSONB NULL, -- 组织用户额外信息
|
||||||
creator_id INT NOT NULL, -- 创建用户id
|
creator_id INT NOT NULL, -- 创建用户id
|
||||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间
|
||||||
updater_id INT NOT NULL, -- 更新用户id
|
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 (user_id) REFERENCES rtsa.user (id) ON DELETE CASCADE, -- 用户外键
|
||||||
FOREIGN KEY (creator_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, -- 更新用户外键
|
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.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.creator_id IS '创建用户id';
|
||||||
|
|
||||||
COMMENT ON COLUMN rtsa.organization_user.created_at IS '创建时间';
|
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 '更新时间';
|
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表
|
-- 创建feature表
|
||||||
CREATE TABLE
|
CREATE TABLE
|
||||||
rtsa.feature (
|
rtsa.feature (
|
||||||
@ -503,19 +570,14 @@ COMMENT ON COLUMN rtsa.release_data_set_data.order_index IS '排序索引';
|
|||||||
CREATE TABLE
|
CREATE TABLE
|
||||||
rtsa.user_data (
|
rtsa.user_data (
|
||||||
id SERIAL PRIMARY KEY, -- id 自增主键
|
id SERIAL PRIMARY KEY, -- id 自增主键
|
||||||
organization_id INT NOT NULL, -- 组织id
|
|
||||||
user_id INT NOT NULL, -- 用户id
|
user_id INT NOT NULL, -- 用户id
|
||||||
data_type INT NOT NULL, -- 数据类型
|
data_type INT NOT NULL, -- 数据类型
|
||||||
data JSONB NOT NULL, -- 数据
|
data JSONB NOT NULL, -- 数据
|
||||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间
|
||||||
updated_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 -- 用户外键
|
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);
|
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 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 '用户数据表';
|
COMMENT ON TABLE rtsa.user_data IS '用户数据表';
|
||||||
|
@ -1 +1 @@
|
|||||||
Subproject commit 1f53057b3f87790ef27c91399a5bb7e890f05549
|
Subproject commit 867c075833ffbe64b0291db921adddc9d5d5a7a3
|
Loading…
Reference in New Issue
Block a user