调整数据库表结构定义
build / build-rust (push) Successful in 1m49s Details

添加feature数据库访问功能及实现
添加feature几个api接口及实现
调整数据库模型枚举字段使用枚举取代i32
添加设计文档
This commit is contained in:
soul-walker 2024-09-26 20:45:48 +08:00
parent 176d9b09e4
commit 3798572b0c
18 changed files with 802 additions and 298 deletions

View File

@ -5,7 +5,7 @@ use base64::Engine;
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use rtss_db::DraftDataAccessor; use rtss_db::DraftDataAccessor;
use rtss_db::RtssDbAccessor; use rtss_db::RtssDbAccessor;
use rtss_dto::common::DataType; use rtss_dto::common::{DataType, Role};
use serde_json::Value; use serde_json::Value;
use crate::apis::{PageDto, PageQueryDto}; use crate::apis::{PageDto, PageQueryDto};
@ -15,7 +15,7 @@ use super::common::{DataOptions, IscsDataOptions};
use super::release_data::ReleaseDataId; use super::release_data::ReleaseDataId;
use super::user::UserId; use super::user::UserId;
use crate::user_auth::{Role, RoleGuard, Token, UserAuthCache}; use crate::user_auth::{RoleGuard, Token, UserAuthCache};
#[derive(Default)] #[derive(Default)]
pub struct DraftDataQuery; pub struct DraftDataQuery;
@ -35,7 +35,7 @@ impl DraftDataQuery {
) -> async_graphql::Result<PageDto<DraftDataDto>> { ) -> async_graphql::Result<PageDto<DraftDataDto>> {
let db_accessor = ctx.data::<RtssDbAccessor>()?; let db_accessor = ctx.data::<RtssDbAccessor>()?;
let paging_result = db_accessor let paging_result = db_accessor
.query_draft_data(query.into(), paging.into()) .paging_query_draft_data(query.into(), paging.into())
.await?; .await?;
Ok(paging_result.into()) Ok(paging_result.into())
} }
@ -55,7 +55,7 @@ impl DraftDataQuery {
query.data_type = Some(DataType::Iscs); query.data_type = Some(DataType::Iscs);
let db_accessor = ctx.data::<RtssDbAccessor>()?; let db_accessor = ctx.data::<RtssDbAccessor>()?;
let paging_result = db_accessor let paging_result = db_accessor
.query_draft_data(query.into(), paging.into()) .paging_query_draft_data(query.into(), paging.into())
.await?; .await?;
Ok(paging_result.into()) Ok(paging_result.into())
} }
@ -70,7 +70,7 @@ impl DraftDataQuery {
let db_accessor = ctx.data::<RtssDbAccessor>()?; let db_accessor = ctx.data::<RtssDbAccessor>()?;
query.data_type = Some(DataType::Iscs); query.data_type = Some(DataType::Iscs);
let paging_result = db_accessor let paging_result = db_accessor
.query_draft_data(query.into(), paging.into()) .paging_query_draft_data(query.into(), paging.into())
.await?; .await?;
Ok(paging_result.into()) Ok(paging_result.into())
} }
@ -86,6 +86,7 @@ impl DraftDataQuery {
async fn draft_data_exist( async fn draft_data_exist(
&self, &self,
ctx: &Context<'_>, ctx: &Context<'_>,
data_type: DataType,
name: String, name: String,
) -> async_graphql::Result<bool> { ) -> async_graphql::Result<bool> {
let user = ctx let user = ctx
@ -94,7 +95,9 @@ impl DraftDataQuery {
.await?; .await?;
let user_id = user.id_i32(); let user_id = user.id_i32();
let db_accessor = ctx.data::<RtssDbAccessor>()?; let db_accessor = ctx.data::<RtssDbAccessor>()?;
let exist = db_accessor.is_draft_data_exist(user_id, &name).await?; let exist = db_accessor
.is_draft_data_exist(user_id, &data_type, &name)
.await?;
Ok(exist) Ok(exist)
} }
} }
@ -328,7 +331,7 @@ impl From<rtss_db::model::DraftDataModel> for DraftDataDto {
Self { Self {
id: value.id, id: value.id,
name: value.name, name: value.name,
data_type: DataType::try_from(value.data_type).unwrap(), data_type: value.data_type,
options: value.options, options: value.options,
data: value data: value
.data .data

View File

@ -0,0 +1,136 @@
use async_graphql::{
dataloader::DataLoader, ComplexObject, Context, InputObject, Object, SimpleObject,
};
use chrono::NaiveDateTime;
use rtss_db::{FeatureAccessor, RtssDbAccessor};
use rtss_dto::common::FeatureType;
use serde_json::Value;
use crate::{
apis::{PageDto, PageQueryDto},
loader::RtssDbLoader,
};
use super::user::UserId;
#[derive(Default)]
pub struct FeatureQuery;
#[derive(Default)]
pub struct FeatureMutation;
#[Object]
impl FeatureQuery {
/// 分页查询特征(系统管理)
async fn feature_paging(
&self,
ctx: &Context<'_>,
page: PageQueryDto,
query: FeatureQueryDto,
) -> async_graphql::Result<PageDto<FeatureDto>> {
let dba = ctx.data::<RtssDbAccessor>()?;
let paging = dba
.paging_query_features(page.into(), &query.into())
.await?;
Ok(paging.into())
}
/// id获取特征
async fn feature(&self, ctx: &Context<'_>, id: i32) -> async_graphql::Result<FeatureDto> {
let dba = ctx.data::<RtssDbAccessor>()?;
let feature = dba.get_feature(id).await?;
Ok(feature.into())
}
/// id列表获取特征
async fn features(
&self,
ctx: &Context<'_>,
ids: Vec<i32>,
) -> async_graphql::Result<Vec<FeatureDto>> {
let dba = ctx.data::<RtssDbAccessor>()?;
let features = dba.get_features(ids.as_slice()).await?;
Ok(features.into_iter().map(|f| f.into()).collect())
}
}
#[Object]
impl FeatureMutation {
/// 上下架特征
async fn publish_feature(
&self,
ctx: &Context<'_>,
id: i32,
is_published: bool,
) -> async_graphql::Result<FeatureDto> {
let dba = ctx.data::<RtssDbAccessor>()?;
let feature = dba.set_feature_published(id, is_published).await?;
Ok(feature.into())
}
}
#[derive(Debug, InputObject)]
pub struct FeatureQueryDto {
pub name: Option<String>,
pub feature_type: Option<i32>,
pub is_published: Option<bool>,
}
impl From<FeatureQueryDto> for rtss_db::FeaturePagingFilter {
fn from(value: FeatureQueryDto) -> Self {
Self {
name: value.name,
feature_type: value.feature_type,
is_published: value.is_published,
}
}
}
#[derive(Debug, SimpleObject)]
#[graphql(complex)]
pub struct FeatureDto {
pub id: i32,
pub feature_type: FeatureType,
pub name: String,
pub description: String,
pub config: Value,
pub is_published: bool,
pub creator_id: i32,
pub updater_id: i32,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
}
#[ComplexObject]
impl FeatureDto {
/// 创建用户name
async fn creator_name(&self, ctx: &Context<'_>) -> async_graphql::Result<Option<String>> {
let loader = ctx.data_unchecked::<DataLoader<RtssDbLoader>>();
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<RtssDbLoader>>();
let name = loader.load_one(UserId::new(self.updater_id)).await?;
Ok(name)
}
}
impl From<rtss_db::model::FeatureModel> for FeatureDto {
fn from(value: rtss_db::model::FeatureModel) -> Self {
Self {
id: value.id,
feature_type: value.feature_type,
name: value.name,
description: value.description,
config: value.config,
is_published: value.is_published,
creator_id: value.creator_id,
updater_id: value.updater_id,
created_at: value.created_at.naive_local(),
updated_at: value.updated_at.naive_local(),
}
}
}

View File

@ -1,5 +1,6 @@
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 release_data::{ReleaseDataMutation, ReleaseDataQuery}; use release_data::{ReleaseDataMutation, ReleaseDataQuery};
mod simulation_definition; mod simulation_definition;
@ -9,15 +10,21 @@ use user::{UserMutation, UserQuery};
mod common; mod common;
mod draft_data; mod draft_data;
mod feature;
mod release_data; mod release_data;
mod simulation; mod simulation;
mod user; mod user;
#[derive(Default, MergedObject)] #[derive(Default, MergedObject)]
pub struct Query(UserQuery, DraftDataQuery, ReleaseDataQuery); pub struct Query(UserQuery, DraftDataQuery, ReleaseDataQuery, FeatureQuery);
#[derive(Default, MergedObject)] #[derive(Default, MergedObject)]
pub struct Mutation(UserMutation, DraftDataMutation, ReleaseDataMutation); pub struct Mutation(
UserMutation,
DraftDataMutation,
ReleaseDataMutation,
FeatureMutation,
);
#[derive(Enum, Copy, Clone, Default, Eq, PartialEq, Debug)] #[derive(Enum, Copy, Clone, Default, Eq, PartialEq, Debug)]
#[graphql(remote = "rtss_db::common::SortOrder")] #[graphql(remote = "rtss_db::common::SortOrder")]
@ -57,6 +64,7 @@ impl From<PageQueryDto> for rtss_db::common::PageQuery {
name = "ReleaseIscsDataPageDto", name = "ReleaseIscsDataPageDto",
params(release_data::ReleaseIscsDataWithoutVersionDto) params(release_data::ReleaseIscsDataWithoutVersionDto)
))] ))]
#[graphql(concrete(name = "FeaturePageDto", params(feature::FeatureDto)))]
pub struct PageDto<T: OutputType> { pub struct PageDto<T: OutputType> {
pub total: i64, pub total: i64,
pub items: Vec<T>, pub items: Vec<T>,

View File

@ -8,7 +8,7 @@ use chrono::NaiveDateTime;
use rtss_db::model::*; use rtss_db::model::*;
use rtss_db::prelude::*; use rtss_db::prelude::*;
use rtss_db::{model::ReleaseDataModel, ReleaseDataAccessor, RtssDbAccessor}; use rtss_db::{model::ReleaseDataModel, ReleaseDataAccessor, RtssDbAccessor};
use rtss_dto::common::DataType; use rtss_dto::common::{DataType, Role};
use serde_json::Value; use serde_json::Value;
use crate::apis::draft_data::DraftDataDto; use crate::apis::draft_data::DraftDataDto;
@ -18,7 +18,7 @@ use super::common::{DataOptions, IscsDataOptions};
use super::user::UserId; use super::user::UserId;
use super::{PageDto, PageQueryDto}; use super::{PageDto, PageQueryDto};
use crate::user_auth::{Role, RoleGuard, Token, UserAuthCache}; use crate::user_auth::{RoleGuard, Token, UserAuthCache};
#[derive(Default)] #[derive(Default)]
pub struct ReleaseDataQuery; pub struct ReleaseDataQuery;
@ -38,7 +38,7 @@ impl ReleaseDataQuery {
) -> async_graphql::Result<PageDto<ReleaseDataDto>> { ) -> async_graphql::Result<PageDto<ReleaseDataDto>> {
let db_accessor = ctx.data::<RtssDbAccessor>()?; let db_accessor = ctx.data::<RtssDbAccessor>()?;
let paging = db_accessor let paging = db_accessor
.query_release_data_list(query.into(), page.into()) .paging_query_release_data_list(query.into(), page.into())
.await?; .await?;
Ok(paging.into()) Ok(paging.into())
} }
@ -54,7 +54,7 @@ impl ReleaseDataQuery {
let db_accessor = ctx.data::<RtssDbAccessor>()?; let db_accessor = ctx.data::<RtssDbAccessor>()?;
query.data_type = Some(DataType::Iscs); query.data_type = Some(DataType::Iscs);
let paging = db_accessor let paging = db_accessor
.query_release_data_list(query.into(), page.into()) .paging_query_release_data_list(query.into(), page.into())
.await?; .await?;
Ok(paging.into()) Ok(paging.into())
} }
@ -96,7 +96,7 @@ impl ReleaseDataQuery {
) -> async_graphql::Result<PageDto<ReleaseDataVersionDto>> { ) -> async_graphql::Result<PageDto<ReleaseDataVersionDto>> {
let db_accessor = ctx.data::<RtssDbAccessor>()?; let db_accessor = ctx.data::<RtssDbAccessor>()?;
let paging = db_accessor let paging = db_accessor
.query_release_data_version_list(data_id, page.into()) .paging_query_release_data_version_list(data_id, page.into())
.await?; .await?;
Ok(paging.into()) Ok(paging.into())
} }
@ -422,7 +422,7 @@ impl From<ReleaseDataModel> for ReleaseDataDto {
Self { Self {
id: model.id, id: model.id,
name: model.name, name: model.name,
data_type: DataType::try_from(model.data_type).unwrap(), data_type: model.data_type,
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,

View File

@ -6,9 +6,10 @@ use rtss_db::{DbAccessError, RtssDbAccessor, UserAccessor};
use crate::{ use crate::{
loader::RtssDbLoader, loader::RtssDbLoader,
user_auth::{build_jwt, Claims, Role, RoleGuard, Token, UserAuthCache, UserInfoDto}, user_auth::{build_jwt, Claims, RoleGuard, Token, UserAuthCache, UserInfoDto},
UserAuthClient, UserAuthClient,
}; };
use rtss_dto::common::Role;
use super::{PageDto, PageQueryDto}; use super::{PageDto, PageQueryDto};
@ -128,7 +129,7 @@ impl From<rtss_db::model::UserModel> for UserDto {
name: value.username, name: value.username,
mobile: value.mobile, mobile: value.mobile,
email: value.email, email: value.email,
roles: serde_json::from_value(value.roles).unwrap(), roles: value.roles.0,
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(),
} }

View File

@ -3,21 +3,16 @@ use std::{
sync::{Arc, Mutex}, sync::{Arc, Mutex},
}; };
use async_graphql::{Enum, Guard}; use async_graphql::Guard;
use axum::http::HeaderMap; use axum::http::HeaderMap;
use chrono::{DateTime, Local}; use chrono::{DateTime, Local};
use rtss_dto::common::Role;
use rtss_log::tracing::error; use rtss_log::tracing::error;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
mod jwt_auth; mod jwt_auth;
pub use jwt_auth::*; pub use jwt_auth::*;
#[derive(Eq, PartialEq, Clone, Copy, Debug, Hash, Enum, Serialize, Deserialize)]
pub enum Role {
Admin,
User,
}
pub struct RoleGuard { pub struct RoleGuard {
role: Role, role: Role,
} }

View File

@ -16,15 +16,20 @@ use super::RtssDbAccessor;
#[allow(async_fn_in_trait)] #[allow(async_fn_in_trait)]
pub trait DraftDataAccessor { pub trait DraftDataAccessor {
/// 查询所有草稿数据 /// 查询所有草稿数据
async fn query_draft_data( async fn paging_query_draft_data(
&self, &self,
query: DraftDataQuery, query: DraftDataQuery,
page: PageQuery, page: PageQuery,
) -> Result<PageResult<DraftDataModel>, DbAccessError>; ) -> Result<PageResult<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+name的数据已存在 /// 是否user_id+data_type+name的数据已存在
async fn is_draft_data_exist(&self, user_id: i32, name: &str) -> Result<bool, DbAccessError>; async fn is_draft_data_exist(
&self,
user_id: i32,
data_type: &DataType,
name: &str,
) -> Result<bool, DbAccessError>;
/// 创建草稿数据基本信息 /// 创建草稿数据基本信息
async fn create_draft_data( async fn create_draft_data(
&self, &self,
@ -201,7 +206,7 @@ impl CreateDraftData {
} }
impl DraftDataAccessor for RtssDbAccessor { impl DraftDataAccessor for RtssDbAccessor {
async fn query_draft_data( async fn paging_query_draft_data(
&self, &self,
query: DraftDataQuery, query: DraftDataQuery,
page: PageQuery, page: PageQuery,
@ -251,19 +256,25 @@ impl DraftDataAccessor for RtssDbAccessor {
Ok(draft_data) Ok(draft_data)
} }
async fn is_draft_data_exist(&self, user_id: i32, name: &str) -> Result<bool, DbAccessError> { async fn is_draft_data_exist(
&self,
user_id: i32,
data_type: &DataType,
name: &str,
) -> Result<bool, DbAccessError> {
let table = DraftDataColumn::Table.name(); let table = DraftDataColumn::Table.name();
let filter = format!( let user_id_column = DraftDataColumn::UserId.name();
"WHERE {} = '{}' AND {} = {}", let data_type_column = DraftDataColumn::DataType.name();
DraftDataColumn::Name.name(), let name_column = DraftDataColumn::Name.name();
name, let sql = format!("SELECT COUNT(*) FROM {table} WHERE {user_id_column} = $1 AND {data_type_column} = $2 AND {name_column} = $3",);
DraftDataColumn::UserId.name(),
user_id
);
let sql = format!("SELECT COUNT(*) FROM {table} {filter}");
// log sql // log sql
debug!("draft data exist check sql: {}", sql); debug!("draft data exist check sql: {}", sql);
let count: i64 = sqlx::query_scalar(&sql).fetch_one(&self.pool).await?; let count: i64 = sqlx::query_scalar(&sql)
.bind(user_id)
.bind(data_type)
.bind(name)
.fetch_one(&self.pool)
.await?;
Ok(count > 0) Ok(count > 0)
} }
@ -274,7 +285,7 @@ impl DraftDataAccessor for RtssDbAccessor {
) -> Result<DraftDataModel, DbAccessError> { ) -> Result<DraftDataModel, DbAccessError> {
// 检查是否已存在 // 检查是否已存在
let exist = self let exist = self
.is_draft_data_exist(create.user_id, &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);
@ -297,7 +308,7 @@ impl DraftDataAccessor for RtssDbAccessor {
// 插入数据 // 插入数据
let draft_data: DraftDataModel = sqlx::query_as(&sql) let draft_data: DraftDataModel = sqlx::query_as(&sql)
.bind(create.name) .bind(create.name)
.bind(create.data_type as i32) .bind(create.data_type)
.bind(create.options) .bind(create.options)
.bind(create.user_id) .bind(create.user_id)
.bind(create.data) .bind(create.data)
@ -411,14 +422,10 @@ impl DraftDataAccessor for RtssDbAccessor {
user_id: i32, user_id: i32,
) -> Result<DraftDataModel, DbAccessError> { ) -> Result<DraftDataModel, DbAccessError> {
let draft_data = self.query_draft_data_by_id(draft_id).await?; let draft_data = self.query_draft_data_by_id(draft_id).await?;
let create = CreateDraftData::new( let create = CreateDraftData::new(name, draft_data.data_type, user_id)
name, .with_option_options(draft_data.options)
DataType::try_from(draft_data.data_type).unwrap(), .with_data(draft_data.data.as_ref().unwrap())
user_id, .with_option_default_release_data_id(draft_data.default_release_data_id);
)
.with_option_options(draft_data.options)
.with_data(draft_data.data.as_ref().unwrap())
.with_option_default_release_data_id(draft_data.default_release_data_id);
self.create_draft_data(create).await self.create_draft_data(create).await
} }
@ -429,7 +436,7 @@ mod tests {
use crate::{SyncUserInfo, UserAccessor}; use crate::{SyncUserInfo, UserAccessor};
use super::*; use super::*;
use rtss_dto::common::IscsStyle; use rtss_dto::common::{IscsStyle, Role};
use rtss_log::tracing::Level; use rtss_log::tracing::Level;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{types::chrono::Local, PgPool}; use sqlx::{types::chrono::Local, PgPool};
@ -439,12 +446,6 @@ mod tests {
pub style: IscsStyle, pub style: IscsStyle,
} }
#[derive(Debug, Clone, Serialize, Deserialize)]
enum Role {
User,
Admin,
}
// 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 basic_use_test(pool: PgPool) -> Result<(), DbAccessError> { async fn basic_use_test(pool: PgPool) -> Result<(), DbAccessError> {
@ -539,7 +540,7 @@ mod tests {
// 查询确认当前数据已删除 // 查询确认当前数据已删除
let page = accessor let page = accessor
.query_draft_data(DraftDataQuery::new(), PageQuery::new(1, 100)) .paging_query_draft_data(DraftDataQuery::new(), PageQuery::new(1, 100))
.await?; .await?;
assert_eq!(page.total, 0); assert_eq!(page.total, 0);
@ -574,7 +575,7 @@ mod tests {
} }
let page = accessor let page = accessor
.query_draft_data( .paging_query_draft_data(
DraftDataQuery::new() DraftDataQuery::new()
.with_user_id(2) .with_user_id(2)
.with_name("test".to_string()) .with_name("test".to_string())
@ -585,13 +586,13 @@ mod tests {
assert_eq!(page.total, 5); assert_eq!(page.total, 5);
let page = accessor let page = accessor
.query_draft_data(DraftDataQuery::new(), PageQuery::new(1, 100)) .paging_query_draft_data(DraftDataQuery::new(), PageQuery::new(1, 100))
.await?; .await?;
assert_eq!(page.total, 20); assert_eq!(page.total, 20);
// 查询共享数据 // 查询共享数据
let page = accessor let page = accessor
.query_draft_data( .paging_query_draft_data(
DraftDataQuery::new().with_is_shared(true), DraftDataQuery::new().with_is_shared(true),
PageQuery::new(1, 10), PageQuery::new(1, 10),
) )
@ -600,7 +601,7 @@ mod tests {
// 查询options // 查询options
let page = accessor let page = accessor
.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::DaShiZhiNeng,

View File

@ -0,0 +1,351 @@
use rtss_dto::common::FeatureType;
use serde_json::Value;
use crate::{
common::{PageQuery, PageResult, TableColumn},
model::{FeatureColumn, FeatureModel},
DbAccessError,
};
use super::RtssDbAccessor;
/// 功能特性管理
#[allow(async_fn_in_trait)]
pub trait FeatureAccessor {
/// 创建功能特性
async fn create_feature(&self, feature: &CreateFeature) -> Result<FeatureModel, DbAccessError>;
/// 更新功能特性
async fn update_feature(&self, feature: &UpdateFeature) -> Result<FeatureModel, DbAccessError>;
/// 上下架功能特性
async fn set_feature_published(
&self,
feature_id: i32,
is_published: bool,
) -> Result<FeatureModel, DbAccessError>;
/// 分页查询功能特性
async fn paging_query_features(
&self,
page: PageQuery,
filter: &FeaturePagingFilter,
) -> Result<PageResult<FeatureModel>, DbAccessError>;
/// 获取id对应的功能特性
async fn get_feature(&self, id: i32) -> Result<FeatureModel, DbAccessError>;
/// 根据id列表获取功能特性基本信息
async fn get_features(&self, ids: &[i32]) -> Result<Vec<FeatureModel>, DbAccessError>;
}
impl FeatureAccessor for RtssDbAccessor {
async fn create_feature(&self, feature: &CreateFeature) -> Result<FeatureModel, DbAccessError> {
let table = FeatureColumn::Table.name();
let feature_type_column = FeatureColumn::FeatureType.name();
let name_column = FeatureColumn::Name.name();
let description_column = FeatureColumn::Description.name();
let config_column = FeatureColumn::Config.name();
let creator_id_column = FeatureColumn::CreatorId.name();
let updater_id_column = FeatureColumn::UpdaterId.name();
let sql = format!(
"INSERT INTO {table} ({feature_type_column}, {name_column}, {description_column}, {config_column}, {creator_id_column}, {updater_id_column}) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *",
);
let row = sqlx::query_as::<_, FeatureModel>(&sql)
.bind(feature.feature_type)
.bind(&feature.name)
.bind(&feature.description)
.bind(&feature.config)
.bind(feature.creator_id)
.bind(feature.creator_id)
.fetch_one(&self.pool)
.await?;
Ok(row)
}
async fn update_feature(&self, feature: &UpdateFeature) -> Result<FeatureModel, DbAccessError> {
let table = FeatureColumn::Table.name();
let name_column = FeatureColumn::Name.name();
let description_column = FeatureColumn::Description.name();
let config_column = FeatureColumn::Config.name();
let updater_id_column = FeatureColumn::UpdaterId.name();
let update_at_column = FeatureColumn::UpdatedAt.name();
let sql = format!(
"UPDATE {table} SET {name_column} = $1, {description_column} = $2, {config_column} = $3, {updater_id_column} = $4, {update_at_column} = 'now()' WHERE id = $5 RETURNING *",
);
let row = sqlx::query_as::<_, FeatureModel>(&sql)
.bind(&feature.name)
.bind(&feature.description)
.bind(&feature.config)
.bind(feature.updater_id)
.bind(feature.id)
.fetch_one(&self.pool)
.await?;
Ok(row)
}
async fn set_feature_published(
&self,
feature_id: i32,
is_published: bool,
) -> Result<FeatureModel, DbAccessError> {
let table = FeatureColumn::Table.name();
let is_published_column = FeatureColumn::IsPublished.name();
let update_at_column = FeatureColumn::UpdatedAt.name();
let sql = format!(
"UPDATE {table} SET {is_published_column} = $1, {update_at_column} = 'now()' WHERE id = $2 RETURNING *",
);
let row = sqlx::query_as::<_, FeatureModel>(&sql)
.bind(is_published)
.bind(feature_id)
.fetch_one(&self.pool)
.await?;
Ok(row)
}
async fn paging_query_features(
&self,
page: PageQuery,
filter: &FeaturePagingFilter,
) -> Result<PageResult<FeatureModel>, DbAccessError> {
let table = FeatureColumn::Table.name();
let where_clause = filter.to_where_clause();
let total =
sqlx::query_scalar::<_, i64>(&format!("SELECT COUNT(*) FROM {table} {where_clause}",))
.fetch_one(&self.pool)
.await?;
if total == 0 {
return Ok(PageResult::new(total, vec![]));
}
let update_at_column = FeatureColumn::UpdatedAt.name();
let select_columns = [
FeatureColumn::Id.name(),
FeatureColumn::FeatureType.name(),
FeatureColumn::Name.name(),
FeatureColumn::Description.name(),
FeatureColumn::IsPublished.name(),
FeatureColumn::CreatorId.name(),
FeatureColumn::UpdaterId.name(),
FeatureColumn::CreatedAt.name(),
FeatureColumn::UpdatedAt.name(),
];
let select_columns = select_columns.join(", ");
let limit_clause = page.to_limit_clause();
let sql = format!(
"SELECT {select_columns} FROM {table} {where_clause} ORDER BY {update_at_column} DESC {limit_clause}",
);
let rows = sqlx::query_as::<_, FeatureModel>(&sql)
.fetch_all(&self.pool)
.await?;
Ok(PageResult::new(total, rows))
}
async fn get_feature(&self, id: i32) -> Result<FeatureModel, DbAccessError> {
let table = FeatureColumn::Table.name();
let id_column = FeatureColumn::Id.name();
let sql = format!("SELECT * FROM {table} WHERE {id_column} = $1",);
let row = sqlx::query_as::<_, FeatureModel>(&sql)
.bind(id)
.fetch_one(&self.pool)
.await?;
Ok(row)
}
async fn get_features(&self, ids: &[i32]) -> Result<Vec<FeatureModel>, DbAccessError> {
let table = FeatureColumn::Table.name();
let id_column = FeatureColumn::Id.name();
let select_columns = [
FeatureColumn::Id.name(),
FeatureColumn::FeatureType.name(),
FeatureColumn::Name.name(),
FeatureColumn::Description.name(),
FeatureColumn::IsPublished.name(),
FeatureColumn::CreatorId.name(),
FeatureColumn::UpdaterId.name(),
FeatureColumn::CreatedAt.name(),
FeatureColumn::UpdatedAt.name(),
];
let select_columns = select_columns.join(", ");
let sql = format!("SELECT {select_columns} FROM {table} WHERE {id_column} = ANY($1)",);
let rows = sqlx::query_as::<_, FeatureModel>(&sql)
.bind(ids)
.fetch_all(&self.pool)
.await?;
Ok(rows)
}
}
#[derive(Debug)]
pub struct CreateFeature {
pub feature_type: FeatureType,
pub name: String,
pub description: String,
pub config: Value,
pub creator_id: i32,
}
#[derive(Debug)]
pub struct UpdateFeature {
pub id: i32,
pub name: String,
pub description: String,
pub config: Value,
pub updater_id: i32,
}
#[derive(Debug, Default)]
pub struct FeaturePagingFilter {
pub name: Option<String>,
pub feature_type: Option<i32>,
pub is_published: Option<bool>,
}
impl FeaturePagingFilter {
fn to_where_clause(&self) -> String {
let mut clauses = vec![];
if let Some(name) = &self.name {
clauses.push(format!("name like '%{}%'", name));
}
if let Some(feature_type) = self.feature_type {
clauses.push(format!("feature_type = {}", feature_type));
}
if let Some(is_published) = self.is_published {
clauses.push(format!("is_published = {}", is_published));
}
if clauses.is_empty() {
"".to_string()
} else {
format!("WHERE {}", clauses.join(" AND "))
}
}
}
#[cfg(test)]
mod tests {
use rtss_dto::common::Role;
use rtss_log::tracing::Level;
use sqlx::{types::chrono::Local, PgPool};
use crate::{SyncUserInfo, UserAccessor};
use super::*;
#[sqlx::test(migrator = "crate::MIGRATOR")]
async fn test_basic_use(pool: PgPool) -> Result<(), DbAccessError> {
rtss_log::Logging::default().with_level(Level::DEBUG).init();
let accessor = crate::db_access::RtssDbAccessor::new(pool);
// 同步10个用户
let mut users = vec![];
for i in 0..10 {
let user = SyncUserInfo {
id: i + 1,
name: format!("user{}", i + 1),
password: "password".to_string(),
roles: serde_json::to_value(&vec![Role::User]).unwrap(),
email: None,
mobile: None,
created_at: Local::now(),
updated_at: None,
};
users.push(user);
}
accessor.sync_user(users.as_slice()).await?;
// 创建测试
let creator_id = 1;
let create_config = Value::String("create".to_string());
let feature = CreateFeature {
feature_type: FeatureType::Ur,
name: "test".to_string(),
description: "test".to_string(),
config: create_config.clone(),
creator_id,
};
let feature = accessor.create_feature(&feature).await?;
println!("create feature: {:?}", feature);
assert_eq!(feature.creator_id, creator_id);
assert_eq!(feature.updater_id, creator_id);
assert_eq!(feature.is_published, true);
assert_eq!(feature.config, create_config);
let feature_id = feature.id;
// 获取功能特性测试
let feature = accessor.get_feature(feature_id).await?;
println!("get feature: {:?}", feature);
assert_eq!(feature.id, feature_id);
assert_eq!(feature.creator_id, creator_id);
assert_eq!(feature.updater_id, creator_id);
assert_eq!(feature.is_published, true);
assert_eq!(feature.config, create_config);
// 更新测试
let updater_id = 2;
let config = Value::String("config".to_string());
let feature = UpdateFeature {
id: feature_id,
name: "test update".to_string(),
description: "test update".to_string(),
config: config.clone(),
updater_id: updater_id,
};
let feature = accessor.update_feature(&feature).await?;
println!("update feature: {:?}", feature);
assert_eq!(feature.updater_id, updater_id);
assert_eq!(feature.config, config);
assert!(feature.updated_at > feature.created_at);
// 上下架测试
let feature = accessor.set_feature_published(feature_id, false).await?;
println!("set feature published: {:?}", feature);
assert_eq!(feature.is_published, false);
// 分页查询测试
// 创建10个功能特性5个发布的5个未发布的
for i in 0..10 {
let feature = CreateFeature {
feature_type: FeatureType::Ur,
name: format!("test{}", i),
description: format!("test{}", i),
config: Value::Null,
creator_id,
};
let feature = accessor.create_feature(&feature).await?;
if i < 5 {
accessor.set_feature_published(feature.id, false).await?;
}
}
// 无过滤条件分页查询测试
let filter = FeaturePagingFilter::default();
let item_per_page = 10;
let page_result = accessor
.paging_query_features(PageQuery::new(1, item_per_page), &filter)
.await?;
assert_eq!(page_result.total, 11);
assert_eq!(page_result.data.len(), item_per_page as usize);
// 上架过滤条件分页查询测试
let filter = FeaturePagingFilter {
is_published: Some(true),
..Default::default()
};
let page_result = accessor
.paging_query_features(PageQuery::new(1, item_per_page), &filter)
.await?;
println!("published features: {:?}", page_result);
assert_eq!(page_result.total, 5);
assert_eq!(page_result.data.len(), 5);
// 名称过滤条件分页查询测试
let filter = FeaturePagingFilter {
name: Some("test".to_string()),
..Default::default()
};
let page_result = accessor
.paging_query_features(PageQuery::new(1, item_per_page), &filter)
.await?;
println!("name filter features: {:?}", page_result);
assert_eq!(page_result.total, 11);
assert_eq!(page_result.data.len(), item_per_page as usize);
// 根据id列表获取功能特性测试
let ids = page_result
.data
.iter()
.map(|f| f.id)
.skip(5)
.collect::<Vec<_>>();
let features = accessor.get_features(&ids).await?;
println!("get features: {:?}", features);
assert_eq!(features.len(), ids.len());
Ok(())
}
}

View File

@ -4,6 +4,8 @@ mod release_data;
pub use release_data::*; pub use release_data::*;
mod user; mod user;
pub use user::*; pub use user::*;
mod feature;
pub use feature::*;
#[derive(Clone)] #[derive(Clone)]
pub struct RtssDbAccessor { pub struct RtssDbAccessor {

View File

@ -31,7 +31,7 @@ pub trait ReleaseDataAccessor {
user_id: Option<i32>, user_id: Option<i32>,
) -> Result<(ReleaseDataModel, ReleaseDataVersionModel), DbAccessError>; ) -> Result<(ReleaseDataModel, ReleaseDataVersionModel), DbAccessError>;
/// 分页查询发布数据列表 /// 分页查询发布数据列表
async fn query_release_data_list( async fn paging_query_release_data_list(
&self, &self,
query: ReleaseDataQuery, query: ReleaseDataQuery,
page: PageQuery, page: PageQuery,
@ -53,7 +53,7 @@ pub trait ReleaseDataAccessor {
release_ids: &[i32], release_ids: &[i32],
) -> Result<Vec<(i32, String)>, DbAccessError>; ) -> Result<Vec<(i32, String)>, DbAccessError>;
/// 查询发布数据所有版本信息 /// 查询发布数据所有版本信息
async fn query_release_data_version_list( async fn paging_query_release_data_version_list(
&self, &self,
release_id: i32, release_id: i32,
page: PageQuery, page: PageQuery,
@ -252,7 +252,7 @@ impl ReleaseDataAccessor for RtssDbAccessor {
} }
// 判断发布数据名称是否已存在 // 判断发布数据名称是否已存在
if self if self
.is_release_data_name_exist(DataType::try_from(draft.data_type).unwrap(), name) .is_release_data_name_exist(draft.data_type, name)
.await? .await?
{ {
return Err(DbAccessError::DataError("发布数据名称已存在".to_string())); return Err(DbAccessError::DataError("发布数据名称已存在".to_string()));
@ -364,7 +364,7 @@ impl ReleaseDataAccessor for RtssDbAccessor {
Ok((rd, rdv)) Ok((rd, rdv))
} }
async fn query_release_data_list( async fn paging_query_release_data_list(
&self, &self,
query: ReleaseDataQuery, query: ReleaseDataQuery,
page: PageQuery, page: PageQuery,
@ -449,7 +449,7 @@ impl ReleaseDataAccessor for RtssDbAccessor {
Ok(rd) Ok(rd)
} }
async fn query_release_data_version_list( async fn paging_query_release_data_version_list(
&self, &self,
release_id: i32, release_id: i32,
page: PageQuery, page: PageQuery,
@ -633,7 +633,7 @@ impl ReleaseDataAccessor for RtssDbAccessor {
// 创建草稿数据 // 创建草稿数据
let draft = self let draft = self
.create_draft_data( .create_draft_data(
CreateDraftData::new(&name, DataType::try_from(rd.data_type).unwrap(), user_id) CreateDraftData::new(&name, rd.data_type, user_id)
.with_option_options(rdv.options) .with_option_options(rdv.options)
.with_data(&rdv.data) .with_data(&rdv.data)
.with_default_release_data_id(rd.id), .with_default_release_data_id(rd.id),
@ -649,7 +649,7 @@ mod tests {
use super::*; use super::*;
use chrono::Local; use chrono::Local;
use rtss_dto::common::IscsStyle; use rtss_dto::common::{IscsStyle, Role};
use rtss_log::tracing::Level; use rtss_log::tracing::Level;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::PgPool; use sqlx::PgPool;
@ -692,12 +692,6 @@ mod tests {
pub style: IscsStyle, pub style: IscsStyle,
} }
#[derive(Debug, Clone, Serialize, Deserialize)]
enum Role {
User,
Admin,
}
// 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> {
@ -818,7 +812,7 @@ mod tests {
// 查询发布数据所有版本 // 查询发布数据所有版本
let versions = accessor let versions = accessor
.query_release_data_version_list( .paging_query_release_data_version_list(
release_data.id, release_data.id,
PageQuery { PageQuery {
page: 1, page: 1,
@ -871,27 +865,27 @@ mod tests {
// 分页查询发布数据,无条件 // 分页查询发布数据,无条件
let query = ReleaseDataQuery::new(); let query = ReleaseDataQuery::new();
let page = PageQuery::new(1, 100); let page = PageQuery::new(1, 100);
let page_result = accessor.query_release_data_list(query, page).await?; let page_result = accessor.paging_query_release_data_list(query, page).await?;
assert_eq!(page_result.total, 9); assert_eq!(page_result.total, 9);
// 分页查询发布数据,按是否上架过滤 // 分页查询发布数据,按是否上架过滤
let query = ReleaseDataQuery::new().with_is_published(true); let query = ReleaseDataQuery::new().with_is_published(true);
let page = PageQuery::new(1, 100); let page = PageQuery::new(1, 100);
let page_result = accessor.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);
// 分页查询发布数据,按名称过滤 // 分页查询发布数据,按名称过滤
let query = ReleaseDataQuery::new().with_name("test_2".to_string()); let query = ReleaseDataQuery::new().with_name("test_2".to_string());
let page = PageQuery::new(1, 10); let page = PageQuery::new(1, 10);
let page_result = accessor.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);
// 分页查询发布数据按用户id过滤 // 分页查询发布数据按用户id过滤
let query = ReleaseDataQuery::new().with_user_id(2); let query = ReleaseDataQuery::new().with_user_id(2);
let page = PageQuery::new(1, 10); let page = PageQuery::new(1, 10);
let page_result = accessor.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(rtss_dto::common::DataType::Em); let query = ReleaseDataQuery::new().with_data_type(rtss_dto::common::DataType::Em);
let page = PageQuery::new(1, 10); let page = PageQuery::new(1, 10);
let page_result = accessor.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);
println!("分页查询发布数据测试成功"); println!("分页查询发布数据测试成功");

View File

@ -1,5 +1,9 @@
use rtss_dto::common::{DataType, FeatureType, Role};
use serde_json::Value; use serde_json::Value;
use sqlx::types::chrono::{DateTime, Local}; use sqlx::types::{
chrono::{DateTime, Local},
Json,
};
use crate::common::TableColumn; use crate::common::TableColumn;
@ -25,7 +29,7 @@ pub struct UserModel {
pub email: Option<String>, pub email: Option<String>,
#[sqlx(default)] #[sqlx(default)]
pub mobile: Option<String>, pub mobile: Option<String>,
pub roles: Value, pub roles: Json<Vec<Role>>,
pub created_at: DateTime<Local>, pub created_at: DateTime<Local>,
pub updated_at: DateTime<Local>, pub updated_at: DateTime<Local>,
} }
@ -50,7 +54,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: i32, pub data_type: DataType,
#[sqlx(default)] #[sqlx(default)]
pub options: Option<Value>, pub options: Option<Value>,
#[sqlx(default)] #[sqlx(default)]
@ -84,7 +88,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: i32, pub data_type: DataType,
/// 从发布版本复制的选项,主要用于查询 /// 从发布版本复制的选项,主要用于查询
#[sqlx(default)] #[sqlx(default)]
pub options: Option<Value>, pub options: Option<Value>,
@ -130,6 +134,7 @@ pub(crate) enum FeatureColumn {
FeatureType, FeatureType,
Name, Name,
Description, Description,
Config,
IsPublished, IsPublished,
CreatorId, CreatorId,
UpdaterId, UpdaterId,
@ -140,9 +145,11 @@ pub(crate) enum FeatureColumn {
#[derive(Debug, sqlx::FromRow)] #[derive(Debug, sqlx::FromRow)]
pub struct FeatureModel { pub struct FeatureModel {
pub id: i32, pub id: i32,
pub feature_type: i32, pub feature_type: FeatureType,
pub name: String, pub name: String,
pub description: String, pub description: String,
#[sqlx(default)]
pub config: Value,
pub is_published: bool, pub is_published: bool,
pub creator_id: i32, pub creator_id: i32,
pub updater_id: i32, pub updater_id: i32,
@ -150,82 +157,25 @@ pub struct FeatureModel {
pub updated_at: DateTime<Local>, pub updated_at: DateTime<Local>,
} }
/// 数据库表 rtss.feature_release_data 列映射 /// 数据库表 rtss.user_config 列映射
#[derive(Debug)] #[derive(Debug)]
#[allow(dead_code)] #[allow(dead_code)]
pub(crate) enum FeatureReleaseDataColumn { pub(crate) enum UserConfigColumn {
Table,
FeatureId,
ReleaseDataId,
}
#[derive(Debug, sqlx::FromRow)]
pub struct FeatureReleaseDataModel {
pub feature_id: i32,
pub release_data_id: i32,
}
/// 数据库表 rtss.feature_group 列映射
#[derive(Debug)]
#[allow(dead_code)]
pub(crate) enum FeatureGroupColumn {
Table,
Id,
Name,
Description,
IsPublished,
CreatorId,
UpdaterId,
CreatedAt,
UpdatedAt,
}
#[derive(Debug, sqlx::FromRow)]
pub struct FeatureGroupModel {
pub id: i32,
pub name: String,
pub description: String,
pub is_published: bool,
pub creator_id: i32,
pub updater_id: i32,
pub created_at: DateTime<Local>,
pub updated_at: DateTime<Local>,
}
/// 数据库表 rtss.feature_group_feature 列映射
#[derive(Debug)]
#[allow(dead_code)]
pub(crate) enum FeatureGroupFeatureColumn {
Table,
FeatureGroupId,
FeatureId,
}
#[derive(Debug, sqlx::FromRow)]
pub struct FeatureGroupFeatureModel {
pub feature_group_id: i32,
pub feature_id: i32,
}
/// 数据库表 rtss.feature_config 列映射
#[derive(Debug)]
#[allow(dead_code)]
pub(crate) enum FeatureConfigColumn {
Table, Table,
Id, Id,
UserId, UserId,
FeatureId, ConfigType,
Config, Config,
CreatedAt, CreatedAt,
UpdatedAt, UpdatedAt,
} }
#[derive(Debug, sqlx::FromRow)] #[derive(Debug, sqlx::FromRow)]
pub struct FeatureConfigModel { pub struct UserConfigModel {
pub id: i32, pub id: i32,
pub user_id: i32, pub user_id: i32,
pub feature_id: i32, pub config_type: i32,
pub config: Vec<u8>, pub config: Value,
pub created_at: DateTime<Local>, pub created_at: DateTime<Local>,
pub updated_at: DateTime<Local>, pub updated_at: DateTime<Local>,
} }
@ -304,6 +254,7 @@ impl TableColumn for FeatureColumn {
FeatureColumn::FeatureType => "feature_type", FeatureColumn::FeatureType => "feature_type",
FeatureColumn::Name => "name", FeatureColumn::Name => "name",
FeatureColumn::Description => "description", FeatureColumn::Description => "description",
FeatureColumn::Config => "config",
FeatureColumn::IsPublished => "is_published", FeatureColumn::IsPublished => "is_published",
FeatureColumn::CreatorId => "creator_id", FeatureColumn::CreatorId => "creator_id",
FeatureColumn::UpdaterId => "updater_id", FeatureColumn::UpdaterId => "updater_id",
@ -313,52 +264,16 @@ impl TableColumn for FeatureColumn {
} }
} }
impl TableColumn for FeatureReleaseDataColumn { impl TableColumn for UserConfigColumn {
fn name(&self) -> &str { fn name(&self) -> &str {
match self { match self {
FeatureReleaseDataColumn::Table => "rtss.feature_release_data", UserConfigColumn::Table => "rtss.user_config",
FeatureReleaseDataColumn::FeatureId => "feature_id", UserConfigColumn::Id => "id",
FeatureReleaseDataColumn::ReleaseDataId => "release_data_id", UserConfigColumn::UserId => "user_id",
} UserConfigColumn::ConfigType => "config_type",
} UserConfigColumn::Config => "config",
} UserConfigColumn::CreatedAt => "created_at",
UserConfigColumn::UpdatedAt => "updated_at",
impl TableColumn for FeatureGroupColumn {
fn name(&self) -> &str {
match self {
FeatureGroupColumn::Table => "rtss.feature_group",
FeatureGroupColumn::Id => "id",
FeatureGroupColumn::Name => "name",
FeatureGroupColumn::Description => "description",
FeatureGroupColumn::IsPublished => "is_published",
FeatureGroupColumn::CreatorId => "creator_id",
FeatureGroupColumn::UpdaterId => "updater_id",
FeatureGroupColumn::CreatedAt => "created_at",
FeatureGroupColumn::UpdatedAt => "updated_at",
}
}
}
impl TableColumn for FeatureGroupFeatureColumn {
fn name(&self) -> &str {
match self {
FeatureGroupFeatureColumn::Table => "rtss.feature_group_feature",
FeatureGroupFeatureColumn::FeatureGroupId => "feature_group_id",
FeatureGroupFeatureColumn::FeatureId => "feature_id",
}
}
}
impl TableColumn for FeatureConfigColumn {
fn name(&self) -> &str {
match self {
FeatureConfigColumn::Table => "rtss.feature_config",
FeatureConfigColumn::Id => "id",
FeatureConfigColumn::UserId => "user_id",
FeatureConfigColumn::FeatureId => "feature_id",
FeatureConfigColumn::Config => "config",
FeatureConfigColumn::CreatedAt => "created_at",
FeatureConfigColumn::UpdatedAt => "updated_at",
} }
} }
} }

View File

@ -21,14 +21,22 @@ fn main() {
} }
Config::new() Config::new()
.out_dir("src/pb") .out_dir("src/pb")
.type_attribute("common.DataType", "#[derive(sqlx::Type)]") .type_attribute(
.type_attribute("common.DataType", "#[derive(async_graphql::Enum)]") "common.Role",
.type_attribute("common.IscsStyle", "#[derive(async_graphql::Enum)]") "#[derive(sqlx::Type, async_graphql::Enum, serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
"common.DataType",
"#[derive(sqlx::Type, async_graphql::Enum, serde::Serialize, serde::Deserialize)]",
)
.type_attribute( .type_attribute(
"common.IscsStyle", "common.IscsStyle",
"#[derive(serde::Serialize, serde::Deserialize)]", "#[derive(async_graphql::Enum, serde::Serialize, serde::Deserialize)]",
)
.type_attribute(
"common.FeatureType",
"#[derive(sqlx::Type, async_graphql::Enum, serde::Serialize, serde::Deserialize)]",
) )
.type_attribute("common.FeatureType", "#[derive(sqlx::Type)]")
.compile_protos( .compile_protos(
&[ &[
"../../rtss-proto-msg/src/em_data.proto", "../../rtss-proto-msg/src/em_data.proto",

View File

@ -88,10 +88,59 @@ pub struct CommonInfo {
#[prost(message, repeated, tag = "4")] #[prost(message, repeated, tag = "4")]
pub child_transforms: ::prost::alloc::vec::Vec<ChildTransform>, pub child_transforms: ::prost::alloc::vec::Vec<ChildTransform>,
} }
/// 用户角色
#[derive(
sqlx::Type,
async_graphql::Enum,
serde::Serialize,
serde::Deserialize,
Clone,
Copy,
Debug,
PartialEq,
Eq,
Hash,
PartialOrd,
Ord,
::prost::Enumeration,
)]
#[repr(i32)]
pub enum Role {
/// 未知
Unknown = 0,
/// 系统管理员
Admin = 1,
/// 普通用户
User = 2,
}
impl Role {
/// String value of the enum field names used in the ProtoBuf definition.
///
/// The values are not transformed in any way and thus are considered stable
/// (if the ProtoBuf definition does not change) and safe for programmatic use.
pub fn as_str_name(&self) -> &'static str {
match self {
Role::Unknown => "Role_Unknown",
Role::Admin => "Role_Admin",
Role::User => "Role_User",
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
match value {
"Role_Unknown" => Some(Self::Unknown),
"Role_Admin" => Some(Self::Admin),
"Role_User" => Some(Self::User),
_ => None,
}
}
}
/// 数据类型 /// 数据类型
#[derive( #[derive(
sqlx::Type, sqlx::Type,
async_graphql::Enum, async_graphql::Enum,
serde::Serialize,
serde::Deserialize,
Clone, Clone,
Copy, Copy,
Debug, Debug,
@ -108,10 +157,6 @@ pub enum DataType {
Unknown = 0, Unknown = 0,
/// 电子地图数据 /// 电子地图数据
Em = 1, Em = 1,
/// IBP数据
Ibp = 2,
/// PSL数据
Psl = 3,
/// ISCS数据 /// ISCS数据
Iscs = 4, Iscs = 4,
} }
@ -124,8 +169,6 @@ impl DataType {
match self { match self {
DataType::Unknown => "DataType_Unknown", DataType::Unknown => "DataType_Unknown",
DataType::Em => "DataType_Em", DataType::Em => "DataType_Em",
DataType::Ibp => "DataType_Ibp",
DataType::Psl => "DataType_Psl",
DataType::Iscs => "DataType_Iscs", DataType::Iscs => "DataType_Iscs",
} }
} }
@ -134,8 +177,6 @@ impl DataType {
match value { match value {
"DataType_Unknown" => Some(Self::Unknown), "DataType_Unknown" => Some(Self::Unknown),
"DataType_Em" => Some(Self::Em), "DataType_Em" => Some(Self::Em),
"DataType_Ibp" => Some(Self::Ibp),
"DataType_Psl" => Some(Self::Psl),
"DataType_Iscs" => Some(Self::Iscs), "DataType_Iscs" => Some(Self::Iscs),
_ => None, _ => None,
} }
@ -170,28 +211,40 @@ impl IscsStyle {
pub fn as_str_name(&self) -> &'static str { pub fn as_str_name(&self) -> &'static str {
match self { match self {
IscsStyle::Unknown => "IscsStyle_Unknown", IscsStyle::Unknown => "IscsStyle_Unknown",
IscsStyle::DaShiZhiNeng => "DaShiZhiNeng", IscsStyle::DaShiZhiNeng => "IscsStyle_DaShiZhiNeng",
} }
} }
/// Creates an enum from field names used in the ProtoBuf definition. /// Creates an enum from field names used in the ProtoBuf definition.
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> { pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
match value { match value {
"IscsStyle_Unknown" => Some(Self::Unknown), "IscsStyle_Unknown" => Some(Self::Unknown),
"DaShiZhiNeng" => Some(Self::DaShiZhiNeng), "IscsStyle_DaShiZhiNeng" => Some(Self::DaShiZhiNeng),
_ => None, _ => None,
} }
} }
} }
/// 功能特性类型 /// 功能特性类型
#[derive( #[derive(
sqlx::Type, Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration, sqlx::Type,
async_graphql::Enum,
serde::Serialize,
serde::Deserialize,
Clone,
Copy,
Debug,
PartialEq,
Eq,
Hash,
PartialOrd,
Ord,
::prost::Enumeration,
)] )]
#[repr(i32)] #[repr(i32)]
pub enum FeatureType { pub enum FeatureType {
/// 未知 /// 未知
Unknown = 0, Unknown = 0,
/// 仿真 /// 城轨信号系统仿真(Urban Rail)
Simulation = 1, Ur = 1,
/// 运行图编制 /// 运行图编制
RunPlan = 2, RunPlan = 2,
} }
@ -203,7 +256,7 @@ impl FeatureType {
pub fn as_str_name(&self) -> &'static str { pub fn as_str_name(&self) -> &'static str {
match self { match self {
FeatureType::Unknown => "FeatureType_Unknown", FeatureType::Unknown => "FeatureType_Unknown",
FeatureType::Simulation => "FeatureType_Simulation", FeatureType::Ur => "FeatureType_Ur",
FeatureType::RunPlan => "FeatureType_RunPlan", FeatureType::RunPlan => "FeatureType_RunPlan",
} }
} }
@ -211,7 +264,7 @@ impl FeatureType {
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> { pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
match value { match value {
"FeatureType_Unknown" => Some(Self::Unknown), "FeatureType_Unknown" => Some(Self::Unknown),
"FeatureType_Simulation" => Some(Self::Simulation), "FeatureType_Ur" => Some(Self::Ur),
"FeatureType_RunPlan" => Some(Self::RunPlan), "FeatureType_RunPlan" => Some(Self::RunPlan),
_ => None, _ => None,
} }

View File

@ -2,16 +2,12 @@
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct IscsGraphicStorage { pub struct IscsGraphicStorage {
#[prost(message, optional, tag = "1")] #[prost(message, repeated, tag = "1")]
pub canvas: ::core::option::Option<super::common::Canvas>, pub cctv_of_station_control_storages: ::prost::alloc::vec::Vec<
CctvOfStationControlStorage,
>,
#[prost(message, repeated, tag = "2")] #[prost(message, repeated, tag = "2")]
pub arrows: ::prost::alloc::vec::Vec<Arrow>, pub fas_platform_alarm_storages: ::prost::alloc::vec::Vec<FasPlatformAlarmStorage>,
#[prost(message, repeated, tag = "3")]
pub iscs_texts: ::prost::alloc::vec::Vec<IscsText>,
#[prost(message, repeated, tag = "4")]
pub rects: ::prost::alloc::vec::Vec<Rect>,
#[prost(message, repeated, tag = "5")]
pub cctv_buttons: ::prost::alloc::vec::Vec<CctvButton>,
} }
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
@ -177,3 +173,33 @@ pub struct TemperatureDetector {
#[prost(string, tag = "2")] #[prost(string, tag = "2")]
pub code: ::prost::alloc::string::String, pub code: ::prost::alloc::string::String,
} }
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct CctvOfStationControlStorage {
#[prost(string, tag = "1")]
pub station_name: ::prost::alloc::string::String,
#[prost(message, optional, tag = "2")]
pub canvas: ::core::option::Option<super::common::Canvas>,
#[prost(message, repeated, tag = "3")]
pub arrows: ::prost::alloc::vec::Vec<Arrow>,
#[prost(message, repeated, tag = "4")]
pub iscs_texts: ::prost::alloc::vec::Vec<IscsText>,
#[prost(message, repeated, tag = "5")]
pub rects: ::prost::alloc::vec::Vec<Rect>,
#[prost(message, repeated, tag = "6")]
pub cctv_buttons: ::prost::alloc::vec::Vec<CctvButton>,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct FasPlatformAlarmStorage {
#[prost(string, tag = "1")]
pub station_name: ::prost::alloc::string::String,
#[prost(message, optional, tag = "2")]
pub canvas: ::core::option::Option<super::common::Canvas>,
#[prost(message, repeated, tag = "3")]
pub arrows: ::prost::alloc::vec::Vec<Arrow>,
#[prost(message, repeated, tag = "4")]
pub iscs_texts: ::prost::alloc::vec::Vec<IscsText>,
#[prost(message, repeated, tag = "5")]
pub rects: ::prost::alloc::vec::Vec<Rect>,
}

8
docs/DESIGN.md Normal file
View File

@ -0,0 +1,8 @@
# 系统设计
## 数据库设计
- 核心数据如下图所示
![alt text](image-1.png)
- 所有数据从草稿数据开始,草稿数据发布为发布数据,功能定义引用发布数据,同时功能还可以引用其他功能实现功能目录树/功能组/功能包

BIN
docs/image-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -14,6 +14,38 @@ CREATE TABLE
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP -- 更新时间 updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP -- 更新时间
); );
-- 创建用户名称索引
CREATE INDEX ON rtss.user (username);
-- 创建用户邮箱索引
CREATE INDEX ON rtss.user (email);
-- 创建用户手机号索引
CREATE INDEX ON rtss.user (mobile);
-- 创建用户角色索引
CREATE INDEX ON rtss.user USING GIN (roles);
-- 注释用户表
COMMENT ON TABLE rtss.user IS '用户表';
-- 注释用户表字段
COMMENT ON COLUMN rtss.user.id IS 'id 自增主键';
COMMENT ON COLUMN rtss.user.username IS '用户名';
COMMENT ON COLUMN rtss.user.password IS '密码';
COMMENT ON COLUMN rtss.user.email IS '邮箱';
COMMENT ON COLUMN rtss.user.mobile IS '手机号';
COMMENT ON COLUMN rtss.user.roles IS '角色列表';
COMMENT ON COLUMN rtss.user.created_at IS '创建时间';
COMMENT ON COLUMN rtss.user.updated_at IS '更新时间';
-- 创建草稿数据表 -- 创建草稿数据表
CREATE TABLE CREATE TABLE
rtss.draft_data ( rtss.draft_data (
@ -28,7 +60,7 @@ CREATE TABLE
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 (user_id) REFERENCES rtss.user (id) ON DELETE CASCADE, -- 用户外键 FOREIGN KEY (user_id) REFERENCES rtss.user (id) ON DELETE CASCADE, -- 用户外键
UNIQUE (name, user_id) -- 一个用户的草稿名称唯一 UNIQUE (name, data_type, user_id) -- 一个用户的某个类型的草稿名称唯一
); );
-- 创建草稿数据用户索引 -- 创建草稿数据用户索引
@ -78,6 +110,18 @@ CREATE TABLE
UNIQUE(data_type, name) -- 数据类型和名称唯一 UNIQUE(data_type, name) -- 数据类型和名称唯一
); );
-- 创建发布数据名称索引
CREATE INDEX ON rtss.release_data (name);
-- 创建发布数据用户索引
CREATE INDEX ON rtss.release_data (user_id);
-- 创建发布数据类型索引
CREATE INDEX ON rtss.release_data (data_type);
-- 创建发布数据配置项索引
CREATE INDEX ON rtss.release_data USING GIN (options);
-- 注释发布数据表 -- 注释发布数据表
COMMENT ON TABLE rtss.release_data IS '发布数据表'; COMMENT ON TABLE rtss.release_data IS '发布数据表';
@ -114,6 +158,15 @@ CREATE TABLE
FOREIGN KEY (release_data_id) REFERENCES rtss.release_data (id) ON DELETE CASCADE FOREIGN KEY (release_data_id) REFERENCES rtss.release_data (id) ON DELETE CASCADE
); );
-- 创建发布数据版本发布数据索引
CREATE INDEX ON rtss.release_data_version (release_data_id);
-- 创建发布数据版本用户索引
CREATE INDEX ON rtss.release_data_version (user_id);
-- 创建发布数据版本配置项索引
CREATE INDEX ON rtss.release_data_version USING GIN (options);
-- 创建发布数据当前版本外键 -- 创建发布数据当前版本外键
ALTER TABLE rtss.release_data ADD FOREIGN KEY (used_version_id) REFERENCES rtss.release_data_version (id) ON DELETE SET NULL; ALTER TABLE rtss.release_data ADD FOREIGN KEY (used_version_id) REFERENCES rtss.release_data_version (id) ON DELETE SET NULL;
@ -145,6 +198,7 @@ CREATE TABLE
feature_type INT NOT NULL, -- feature类型 feature_type INT NOT NULL, -- feature类型
name VARCHAR(128) NOT NULL UNIQUE, -- feature名称 name VARCHAR(128) NOT NULL UNIQUE, -- feature名称
description TEXT NOT NULL, -- feature描述 description TEXT NOT NULL, -- feature描述
config JSONB NOT NULL, -- feature配置
is_published BOOLEAN NOT NULL DEFAULT TRUE, -- 是否上架 is_published BOOLEAN NOT NULL DEFAULT TRUE, -- 是否上架
creator_id INT NOT NULL, -- 创建用户id creator_id INT NOT NULL, -- 创建用户id
updater_id INT NOT NULL, -- 更新用户id updater_id INT NOT NULL, -- 更新用户id
@ -154,6 +208,12 @@ CREATE TABLE
FOREIGN KEY (updater_id) REFERENCES rtss.user (id) ON DELETE CASCADE -- 用户外键 FOREIGN KEY (updater_id) REFERENCES rtss.user (id) ON DELETE CASCADE -- 用户外键
); );
-- 创建feature类型索引
CREATE INDEX ON rtss.feature (feature_type);
-- 创建feature名称索引
CREATE INDEX ON rtss.feature (name);
-- 注释仿真feature表 -- 注释仿真feature表
COMMENT ON TABLE rtss.feature IS 'feature表'; COMMENT ON TABLE rtss.feature IS 'feature表';
@ -166,6 +226,8 @@ COMMENT ON COLUMN rtss.feature.name IS 'feature名称';
COMMENT ON COLUMN rtss.feature.description IS 'feature描述'; COMMENT ON COLUMN rtss.feature.description IS 'feature描述';
COMMENT ON COLUMN rtss.feature.config IS 'feature配置';
COMMENT ON COLUMN rtss.feature.is_published IS '是否上架'; COMMENT ON COLUMN rtss.feature.is_published IS '是否上架';
COMMENT ON COLUMN rtss.feature.creator_id IS '创建用户id'; COMMENT ON COLUMN rtss.feature.creator_id IS '创建用户id';
@ -174,95 +236,36 @@ COMMENT ON COLUMN rtss.feature.created_at IS '创建时间';
COMMENT ON COLUMN rtss.feature.updated_at IS '更新时间'; COMMENT ON COLUMN rtss.feature.updated_at IS '更新时间';
-- 创建仿真feature和发布数据关联 -- 创建用户配置
CREATE TABLE CREATE TABLE
rtss.feature_release_data ( rtss.user_config (
feature_id INT NOT NULL, -- 仿真feature id
release_data_id INT NOT NULL, -- 发布数据id
PRIMARY KEY (feature_id, release_data_id),
FOREIGN KEY (feature_id) REFERENCES rtss.feature (id) ON DELETE CASCADE,
FOREIGN KEY (release_data_id) REFERENCES rtss.release_data (id) ON DELETE CASCADE
);
-- 注释仿真feature和发布数据关联表
COMMENT ON TABLE rtss.feature_release_data IS '仿真feature和发布数据关联表';
-- 注释仿真feature和发布数据关联表字段
COMMENT ON COLUMN rtss.feature_release_data.feature_id IS '仿真feature id';
COMMENT ON COLUMN rtss.feature_release_data.release_data_id IS '发布数据id';
-- 创建feature group表
CREATE TABLE
rtss.feature_group (
id SERIAL PRIMARY KEY, -- id 自增主键
name VARCHAR(128) NOT NULL UNIQUE, -- feature group名称
description TEXT NOT NULL, -- feature group描述
is_published BOOLEAN NOT NULL DEFAULT TRUE, -- 是否上架
creator_id INT NOT NULL, -- 创建用户id
updater_id INT NOT NULL, -- 更新用户id
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 更新时间
FOREIGN KEY (creator_id) REFERENCES rtss.user (id) ON DELETE CASCADE, -- 用户外键
FOREIGN KEY (updater_id) REFERENCES rtss.user (id) ON DELETE CASCADE -- 用户外键
);
-- 注释仿真feature group表
COMMENT ON TABLE rtss.feature_group IS 'feature group表';
-- 注释仿真feature group表字段
COMMENT ON COLUMN rtss.feature_group.id IS 'id 自增主键';
COMMENT ON COLUMN rtss.feature_group.name IS 'feature group名称';
COMMENT ON COLUMN rtss.feature_group.description IS 'feature group描述';
COMMENT ON COLUMN rtss.feature_group.is_published IS '是否上架';
COMMENT ON COLUMN rtss.feature_group.creator_id IS '创建用户id';
COMMENT ON COLUMN rtss.feature_group.created_at IS '创建时间';
COMMENT ON COLUMN rtss.feature_group.updated_at IS '更新时间';
-- 创建feature group和feature关联表
CREATE TABLE
rtss.feature_group_feature (
feature_group_id INT NOT NULL, -- feature group id
feature_id INT NOT NULL, -- feature id
PRIMARY KEY (feature_id, feature_group_id),
FOREIGN KEY (feature_id) REFERENCES rtss.feature (id) ON DELETE CASCADE,
FOREIGN KEY (feature_group_id) REFERENCES rtss.feature_group (id) ON DELETE CASCADE
);
-- 注释仿真feature group和feature关联表
COMMENT ON TABLE rtss.feature_group_feature IS '仿真feature group和feature关联表';
-- 创建用户feature配置表
CREATE TABLE
rtss.feature_config (
id SERIAL PRIMARY KEY, -- id 自增主键 id SERIAL PRIMARY KEY, -- id 自增主键
user_id INT NOT NULL, -- 用户id user_id INT NOT NULL, -- 用户id
feature_id INT NOT NULL, -- 仿真feature id config_type INT NOT NULL, -- 配置类型
config BYTEA NOT NULL, -- 配置 config BYTEA 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 (user_id) REFERENCES rtss.user (id) ON DELETE CASCADE, -- 用户外键 FOREIGN KEY (user_id) REFERENCES rtss.user (id) ON DELETE CASCADE -- 用户外键
FOREIGN KEY (feature_id) REFERENCES rtss.feature (id) ON DELETE CASCADE
); );
-- 创建用户配置用户索引
CREATE INDEX ON rtss.user_config (user_id);
-- 创建用户配置类型索引
CREATE INDEX ON rtss.user_config (config_type);
-- 注释用户feature配置表 -- 注释用户feature配置表
COMMENT ON TABLE rtss.feature_config IS '用户feature配置表'; COMMENT ON TABLE rtss.user_config IS '用户feature配置表';
-- 注释用户feature配置表字段 -- 注释用户feature配置表字段
COMMENT ON COLUMN rtss.feature_config.id IS 'id 自增主键'; COMMENT ON COLUMN rtss.user_config.id IS 'id 自增主键';
COMMENT ON COLUMN rtss.feature_config.user_id IS '用户id'; COMMENT ON COLUMN rtss.user_config.user_id IS '用户id';
COMMENT ON COLUMN rtss.feature_config.feature_id IS '仿真feature id'; COMMENT ON COLUMN rtss.user_config.config_type IS '配置类型';
COMMENT ON COLUMN rtss.feature_config.config IS '配置'; COMMENT ON COLUMN rtss.user_config.config IS '配置';
COMMENT ON COLUMN rtss.feature_config.created_at IS '创建时间'; COMMENT ON COLUMN rtss.user_config.created_at IS '创建时间';
COMMENT ON COLUMN rtss.feature_config.updated_at IS '更新时间'; COMMENT ON COLUMN rtss.user_config.updated_at IS '更新时间';

@ -1 +1 @@
Subproject commit 1672a8c0e2b41c4076c1dc9ec852f425dcba21c3 Subproject commit 02d9ad3b44876fc470e460bb6975c3d08f698b1b